Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
pull_request:
branches: ['**']

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint-format-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm run lint

- name: Format check
run: pnpm run format:check

- name: Test
run: pnpm test
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ CLI pour connecter un environnement local aux environnements de recette et injec

- **Node.js** >= 24
- **pnpm** (gestionnaire de paquets)
- Les applications cibles ont une structure `/<racine_apps>/<application>/frontend/.env`
- Les applications cibles ont une structure :
- à la racine : `/<racine_apps>/<application>/frontend/.env`
- ou sous entcore : `/<racine_apps>/entcore/<application>/frontend/.env`

## Installation

Expand Down Expand Up @@ -40,7 +42,7 @@ pnpm run dev onboard
node bin/dev-auth-fetcher onboard
```

Vous serez invité à saisir le chemin du répertoire contenant vos applications (chaque app ayant un dossier `frontend`). Les fichiers dans `config/environments/` (recette-ode1, recette-ode2, recette-release, local) sont créés par défaut.
Vous serez invité à saisir le chemin du répertoire contenant vos applications : soit des dossiers `<app>/frontend` à la racine, soit (ou en plus) un sous-dossier `entcore` avec des apps `entcore/<app>/frontend`. Les fichiers dans `config/environments/` (recette-ode1, recette-ode2, recette-release, local) sont créés par défaut.

### Connexion et injection des cookies

Expand All @@ -55,7 +57,7 @@ node bin/dev-auth-fetcher connect
Options de la commande `connect` :

- `-e, --env <id>` : identifiant de l'environnement (ex. `recette-ode1`)
- `-a, --app <name>` : nom d'une application cible
- `-a, --app <name>` : nom ou id d'application (ex. `mon-app` ou `entcore/mediacentre` pour une app sous entcore)
- `--all` : cibler toutes les applications détectées
- `-l, --login <login>` : login utilisateur (sinon demandé en interactif)

Expand All @@ -66,6 +68,7 @@ Exemples :
```bash
dev-auth-fetcher connect --env recette-ode1 --all
dev-auth-fetcher connect -e recette-ode2 -a mon-app -l mon.login
dev-auth-fetcher connect -e recette-ode1 -a entcore/mediacentre # app sous entcore
```

### Reconnexion automatique (reconnect-last)
Expand Down Expand Up @@ -95,7 +98,7 @@ dev-auth-fetcher list-apps
- **Environnements** : un fichier par environnement dans `config/environments/`
- Ex. `recette-ode1.json` : `{ "id", "label", "url" }`

- **Fichier .env cible** (dans chaque `application/frontend/.env`) :
- **Fichier .env cible** (dans chaque `application/frontend/.env` ou `entcore/application/frontend/.env`) :
- `VITE_XSRF_TOKEN=...`
- `VITE_ONE_SESSION_ID=...`
- `VITE_RECETTE=<url>`
Expand Down
9 changes: 5 additions & 4 deletions src/cli/commands/reconnect-last.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export async function runReconnectLastCommand(): Promise<void> {
return;
}

const hasAppSelection = last.allApps === true || (last.appNames?.length ?? 0) > 0;
const appIdsOrNames = last.appIds ?? last.appNames;
const hasAppSelection = last.allApps === true || (appIdsOrNames?.length ?? 0) > 0;
logger.info(
hasAppSelection
? `Reconnexion automatique : ${last.envId} / ${last.login} (${last.allApps ? 'toutes les apps' : last.appNames!.join(', ')})`
? `Reconnexion automatique : ${last.envId} / ${last.login} (${last.allApps ? 'toutes les apps' : appIdsOrNames!.join(', ')})`
: `Reconnexion : ${last.envId} / ${last.login} (sélection des apps à choisir)`
);

Expand All @@ -26,8 +27,8 @@ export async function runReconnectLastCommand(): Promise<void> {
login: last.login,
...(last.allApps === true
? { all: true }
: last.appNames?.length
? { apps: last.appNames }
: appIdsOrNames?.length
? { apps: appIdsOrNames }
: {}),
skipConfirm: hasAppSelection,
});
Expand Down
7 changes: 5 additions & 2 deletions src/config/credentialsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export interface LastConnection {
login: string;
/** true si "toutes les applications" avaient été sélectionnées */
allApps?: boolean;
/** noms des applications sélectionnées (vide si allApps) */
/** ids des applications sélectionnées (prioritaire pour reconnect) */
appIds?: string[];
/** noms des applications (rétrocompatibilité, affichage) */
appNames?: string[];
}

Expand Down Expand Up @@ -114,13 +116,14 @@ export async function getLastConnection(): Promise<LastConnection | null> {
export async function setLastConnection(
envId: string,
login: string,
appSelection?: { allApps: boolean; appNames: string[] }
appSelection?: { allApps: boolean; appIds: string[]; appNames?: string[] }
): Promise<void> {
const store = await loadUserCredentialsStore();
store.lastConnection = {
envId,
login,
allApps: appSelection?.allApps,
appIds: appSelection?.appIds?.length ? appSelection.appIds : undefined,
appNames: appSelection?.appNames?.length ? appSelection.appNames : undefined,
};
await saveUserCredentialsStore(store);
Expand Down
37 changes: 35 additions & 2 deletions src/core/apps/AppDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export interface AppSummary {

/**
* Parcourt appsRoot et détecte les applications ayant un dossier frontend.
* Vérifie ou crée frontend/.env.
* Deux schémas supportés :
* - À la racine : {application}/frontend
* - Sous entcore : entcore/{application}/frontend (id = "entcore/{application}")
*/
export async function discoverApps(appsRoot: string): Promise<AppSummary[]> {
let entries: string[];
Expand All @@ -29,6 +31,8 @@ export async function discoverApps(appsRoot: string): Promise<AppSummary[]> {
}

const apps: AppSummary[] = [];

// Schéma racine : {application}/frontend
for (const name of entries) {
const appPath = path.join(appsRoot, name);
const stat = await fs.stat(appPath).catch(() => null);
Expand All @@ -39,13 +43,42 @@ export async function discoverApps(appsRoot: string): Promise<AppSummary[]> {
if (!frontendStat?.isDirectory()) continue;

const envPath = path.join(frontendPath, '.env');
// On ne crée pas le .env ici; EnvManager le fera à la première écriture.
apps.push({
id: name,
name,
path: appPath,
envPath,
});
}

// Schéma entcore : entcore/{application}/frontend (id = "entcore/{application}")
const entcorePath = path.join(appsRoot, 'entcore');
const entcoreStat = await fs.stat(entcorePath).catch(() => null);
if (entcoreStat?.isDirectory()) {
let entcoreEntries: string[];
try {
entcoreEntries = await fs.readdir(entcorePath);
} catch {
entcoreEntries = [];
}
for (const appName of entcoreEntries) {
const appPath = path.join(entcorePath, appName);
const stat = await fs.stat(appPath).catch(() => null);
if (!stat?.isDirectory()) continue;

const frontendPath = path.join(appPath, 'frontend');
const frontendStat = await fs.stat(frontendPath).catch(() => null);
if (!frontendStat?.isDirectory()) continue;

const envPath = path.join(frontendPath, '.env');
apps.push({
id: `entcore/${appName}`,
name: appName,
path: appPath,
envPath,
});
}
}

return apps.sort((a, b) => a.name.localeCompare(b.name));
}
2 changes: 1 addition & 1 deletion src/core/auth/FetchAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class FetchAuthClient implements IAuthClient {
);
}

let sessionId = cookieMap.get('oneSessionId') ?? '';
const sessionId = cookieMap.get('oneSessionId') ?? '';
let xsrfToken = cookieMap.get('XSRF-TOKEN') ?? '';

if (!xsrfToken || !sessionId) {
Expand Down
14 changes: 8 additions & 6 deletions src/services/EnvSyncService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface ConnectOptions {
env?: string;
app?: string;
all?: boolean;
/** Liste de noms d'apps (utilisée par reconnect-last). */
/** Liste d'ids ou noms d'apps (ex. app1, entcore/mediacentre). Utilisée par reconnect-last. */
apps?: string[];
login?: string;
/** Si true, ne pas demander de confirmation (reconnect-last entièrement automatique). */
Expand Down Expand Up @@ -68,8 +68,8 @@ export class EnvSyncService {
const appsDesc =
last.allApps === true
? 'toutes les apps'
: last.appNames?.length
? last.appNames.join(', ')
: (last.appIds?.length ?? last.appNames?.length)
? (last.appIds ?? last.appNames)!.join(', ')
: 'apps à choisir';
const quickReconnectLabel = `🔄 Reconnexion rapide : ${last.envId} / ${last.login} (${appsDesc})`;
envChoices.unshift({ name: quickReconnectLabel, value: RECONNECT_CHOICE });
Expand All @@ -85,14 +85,15 @@ export class EnvSyncService {
]);

if (selectedEnvId === RECONNECT_CHOICE && last) {
const hasAppSelection = last.allApps === true || (last.appNames?.length ?? 0) > 0;
const appIdsOrNames = last.appIds ?? last.appNames;
const hasAppSelection = last.allApps === true || (appIdsOrNames?.length ?? 0) > 0;
await this.runInteractive({
env: last.envId,
login: last.login,
...(last.allApps === true
? { all: true }
: last.appNames?.length
? { apps: last.appNames }
: appIdsOrNames?.length
? { apps: appIdsOrNames }
: {}),
skipConfirm: hasAppSelection,
});
Expand Down Expand Up @@ -223,6 +224,7 @@ export class EnvSyncService {
// Mémoriser la dernière connexion (envId, login, apps) pour reconnect-last.
await setLastConnection(envId, login, {
allApps: allSelected,
appIds: apps.map((a) => a.id),
appNames: apps.map((a) => a.name),
});

Expand Down
17 changes: 13 additions & 4 deletions src/steps/connect/SelectAppsStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,35 @@ export async function selectAppsStep(
}

if (options.apps && options.apps.length > 0) {
const apps = discovered.filter((a) => options.apps!.includes(a.name));
return { apps, allSelected: false };
const byId = discovered.filter((a) => options.apps!.includes(a.id));
if (byId.length > 0) return { apps: byId, allSelected: false };
const byName = discovered.filter((a) => options.apps!.includes(a.name));
return { apps: byName, allSelected: false };
}

if (options.app) {
if (options.app.startsWith('entcore/')) {
const found = discovered.find((a) => a.id === options.app);
return { apps: found ? [found] : [], allSelected: false };
}
const found = discovered.find((a) => a.name === options.app);
return { apps: found ? [found] : [], allSelected: false };
}

const choices = [
{ name: 'Toutes les applications', value: CHOICE_ALL },
new inquirer.Separator(),
...discovered.map((a) => ({ name: a.name, value: a.id })),
...discovered.map((a) => ({
name: a.id !== a.name ? `${a.name} (${a.id})` : a.name,
value: a.id,
})),
];

const { selected } = await inquirer.prompt<{ selected: string[] }>([
{
type: 'checkbox',
name: 'selected',
message: "Sélectionnez les applications à mettre à jour :",
message: 'Sélectionnez les applications à mettre à jour :',
choices,
},
]);
Expand Down
3 changes: 1 addition & 2 deletions src/steps/onboarding/AskRootDirectoryStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export async function askRootDirectoryStep(currentAppsRoot?: string): Promise<As
{
type: 'input',
name: 'appsRoot',
message:
"Chemin racine des applications (répertoire contenant les dossiers d'apps avec frontend) :",
message: 'Chemin racine des applications ({app}/frontend ou entcore/{app}/frontend) :',
default: defaultPath,
validate: (input: string) => {
if (!input?.trim()) return 'Le chemin ne peut pas être vide.';
Expand Down
5 changes: 3 additions & 2 deletions tests/config/envConfigs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
} from '../../src/config/envConfigs';

describe('envConfigs', () => {
it('DEFAULT_ENVIRONMENTS contient les 4 environnements attendus', () => {
expect(DEFAULT_ENVIRONMENTS).toHaveLength(4);
it('DEFAULT_ENVIRONMENTS contient les 5 environnements attendus', () => {
expect(DEFAULT_ENVIRONMENTS).toHaveLength(5);
const ids = DEFAULT_ENVIRONMENTS.map((e) => e.id);
expect(ids).toContain('recette-ode1');
expect(ids).toContain('recette-ode2');
expect(ids).toContain('recette-ode3');
expect(ids).toContain('recette-release');
expect(ids).toContain('local');
});
Expand Down
31 changes: 31 additions & 0 deletions tests/core/apps/AppDiscovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,35 @@ describe('AppDiscovery', () => {
it("lance AppDiscoveryError si le répertoire n'existe pas", async () => {
await expect(discoverApps(join(tempRoot, 'nonexistent'))).rejects.toThrow(AppDiscoveryError);
});

it('détecte les applications sous entcore (entcore/{application}/frontend)', async () => {
await mkdir(join(tempRoot, 'entcore', 'app1', 'frontend'), { recursive: true });
await mkdir(join(tempRoot, 'entcore', 'app2', 'frontend'), { recursive: true });
const apps = await discoverApps(tempRoot);
expect(apps).toHaveLength(2);
const app1 = apps.find((a) => a.id === 'entcore/app1');
const app2 = apps.find((a) => a.id === 'entcore/app2');
expect(app1).toBeDefined();
expect(app2).toBeDefined();
expect(app1!.name).toBe('app1');
expect(app1!.envPath).toContain('entcore');
expect(app1!.envPath).toContain('app1');
expect(app1!.envPath).toContain('frontend');
expect(app1!.envPath).toContain('.env');
});

it('détecte à la fois les apps à la racine et sous entcore (ids distincts)', async () => {
await mkdir(join(tempRoot, 'app1', 'frontend'), { recursive: true });
await mkdir(join(tempRoot, 'entcore', 'app1', 'frontend'), { recursive: true });
const apps = await discoverApps(tempRoot);
expect(apps).toHaveLength(2);
const rootApp = apps.find((a) => a.id === 'app1');
const entcoreApp = apps.find((a) => a.id === 'entcore/app1');
expect(rootApp).toBeDefined();
expect(entcoreApp).toBeDefined();
expect(rootApp!.name).toBe('app1');
expect(entcoreApp!.name).toBe('app1');
expect(rootApp!.path).not.toContain('entcore');
expect(entcoreApp!.path).toContain('entcore');
});
});