@inwink-mcp/backoffice-extension-helpers 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,7 +75,21 @@ Contrôles effectués :
75
75
  - `extension.manifest.json` présent, `name` (slug) et `version` (semver) valides ;
76
76
  - `dist/` contient `index.html` + `extension.manifest.json` à la racine + `assets/` ;
77
77
  - le zip contient `index.html` et `extension.manifest.json` à la racine, sans `node_modules/`, `src/` ni `dist/` ;
78
- - avertissements : `base: './'` absent de `vite.config.ts`, usage de `BrowserRouter`.
78
+ - avertissements : `base: './'` absent de `vite.config.ts`, usage de `BrowserRouter`, `docs/inwink-api/` différent de la doc de cette version de la CLI.
79
+
80
+ ### `update-docs`
81
+
82
+ Met à jour la doc API d'une extension **déjà générée** : `docs/inwink-api/` est remplacé en miroir par celui de cette version (fichiers ajoutés, modifiés, et supprimés s'ils ont disparu du template). Rien d'autre n'est touché, la commande peut être relancée sans risque.
83
+
84
+ ```bash
85
+ npx @inwink-mcp/backoffice-extension-helpers@latest update-docs
86
+ ```
87
+
88
+ | Option | Description |
89
+ |--------|-------------|
90
+ | `--dir <path>` | Dossier du projet (défaut : dossier courant) |
91
+
92
+ Ce dossier appartient à la CLI : une modification locale est écrasée au prochain `update-docs`. `AGENTS.md` et le code du squelette ne sont pas mis à jour.
79
93
 
80
94
  ## Templates
81
95
 
package/dist/cli.js CHANGED
@@ -340,12 +340,70 @@ function bootstrap(opts) {
340
340
  }
341
341
 
342
342
  // src/commands/pack.ts
343
- import path7 from "path";
343
+ import path8 from "path";
344
344
 
345
345
  // src/lib/verify.ts
346
+ import fs5 from "fs";
347
+ import path6 from "path";
348
+ import AdmZip from "adm-zip";
349
+
350
+ // src/lib/docs.ts
346
351
  import fs4 from "fs";
347
352
  import path5 from "path";
348
- import AdmZip from "adm-zip";
353
+ var DOCS_REL_DIR = "docs/inwink-api";
354
+ var DOCS_TEMPLATE = "vite-extension";
355
+ var UPDATE_DOCS_COMMAND = "npx @inwink-mcp/backoffice-extension-helpers@latest update-docs";
356
+ function templateDocsDir() {
357
+ return path5.join(templatesRoot, DOCS_TEMPLATE, DOCS_REL_DIR);
358
+ }
359
+ function listFiles(root, current = root) {
360
+ if (!fs4.existsSync(current)) return [];
361
+ const files = [];
362
+ for (const entry of fs4.readdirSync(current, { withFileTypes: true })) {
363
+ const full = path5.join(current, entry.name);
364
+ if (entry.isDirectory()) files.push(...listFiles(root, full));
365
+ else files.push(path5.relative(root, full).split(path5.sep).join("/"));
366
+ }
367
+ return files.sort();
368
+ }
369
+ function readNormalized(file) {
370
+ return fs4.readFileSync(file, "utf8").replaceAll("\r\n", "\n");
371
+ }
372
+ function diffDocs(projectDir) {
373
+ const src = templateDocsDir();
374
+ const dest = path5.join(projectDir, DOCS_REL_DIR);
375
+ const srcFiles = listFiles(src);
376
+ const destFiles = new Set(listFiles(dest));
377
+ const diff = { added: [], changed: [], removed: [] };
378
+ for (const rel of srcFiles) {
379
+ if (!destFiles.has(rel)) diff.added.push(rel);
380
+ else if (readNormalized(path5.join(src, rel)) !== readNormalized(path5.join(dest, rel))) {
381
+ diff.changed.push(rel);
382
+ }
383
+ destFiles.delete(rel);
384
+ }
385
+ diff.removed = [...destFiles];
386
+ return diff;
387
+ }
388
+ function isDocsUpToDate(diff) {
389
+ return diff.added.length + diff.changed.length + diff.removed.length === 0;
390
+ }
391
+ function syncDocs(projectDir) {
392
+ const diff = diffDocs(projectDir);
393
+ const src = templateDocsDir();
394
+ const dest = path5.join(projectDir, DOCS_REL_DIR);
395
+ for (const rel of [...diff.added, ...diff.changed]) {
396
+ const target = path5.join(dest, rel);
397
+ fs4.mkdirSync(path5.dirname(target), { recursive: true });
398
+ fs4.copyFileSync(path5.join(src, rel), target);
399
+ }
400
+ for (const rel of diff.removed) {
401
+ fs4.rmSync(path5.join(dest, rel));
402
+ }
403
+ return diff;
404
+ }
405
+
406
+ // src/lib/verify.ts
349
407
  function verifyExtension(options) {
350
408
  const { projectDir, requireDist = false } = options;
351
409
  const errors = [];
@@ -366,40 +424,49 @@ function verifyExtension(options) {
366
424
  } catch (err) {
367
425
  errors.push(err.message);
368
426
  }
369
- const distDir = path5.join(projectDir, "dist");
370
- if (!fs4.existsSync(distDir)) {
427
+ const distDir = path6.join(projectDir, "dist");
428
+ if (!fs5.existsSync(distDir)) {
371
429
  if (requireDist) {
372
430
  errors.push("Dossier dist/ introuvable apr\xE8s le build.");
373
431
  }
374
432
  } else {
375
- if (!fs4.existsSync(path5.join(distDir, "index.html"))) {
433
+ if (!fs5.existsSync(path6.join(distDir, "index.html"))) {
376
434
  errors.push("dist/index.html manquant.");
377
435
  }
378
- if (!fs4.existsSync(path5.join(distDir, "extension.manifest.json"))) {
436
+ if (!fs5.existsSync(path6.join(distDir, "extension.manifest.json"))) {
379
437
  errors.push("dist/extension.manifest.json manquant (le plugin Vite doit le copier).");
380
438
  }
381
- const assetsDir = path5.join(distDir, "assets");
382
- if (!fs4.existsSync(assetsDir) || !fs4.statSync(assetsDir).isDirectory()) {
439
+ const assetsDir = path6.join(distDir, "assets");
440
+ if (!fs5.existsSync(assetsDir) || !fs5.statSync(assetsDir).isDirectory()) {
383
441
  warnings.push("dist/assets/ introuvable \u2014 build Vite incomplet ?");
384
442
  }
385
443
  }
386
444
  const zipPath = resolveZipPath(options, projectDir, name, version);
387
- if (zipPath && fs4.existsSync(zipPath)) {
445
+ if (zipPath && fs5.existsSync(zipPath)) {
388
446
  verifyZip(zipPath, errors);
389
447
  } else if (options.zipPath) {
390
448
  errors.push(`Zip introuvable : ${options.zipPath}`);
391
449
  }
392
450
  checkViteBase(projectDir, warnings);
393
451
  checkRouter(projectDir, warnings);
452
+ checkDocs(projectDir, warnings);
394
453
  return { errors, warnings };
395
454
  }
455
+ function checkDocs(projectDir, warnings) {
456
+ const diff = diffDocs(projectDir);
457
+ if (isDocsUpToDate(diff)) return;
458
+ const count = diff.added.length + diff.changed.length + diff.removed.length;
459
+ warnings.push(
460
+ `${DOCS_REL_DIR}/ diff\xE8re de la doc API actuelle (${count} fichier(s)). Mettez-la \xE0 jour avec : ${UPDATE_DOCS_COMMAND}`
461
+ );
462
+ }
396
463
  function resolveZipPath(options, projectDir, name, version) {
397
464
  if (options.zipPath) {
398
- return path5.isAbsolute(options.zipPath) ? options.zipPath : path5.join(projectDir, options.zipPath);
465
+ return path6.isAbsolute(options.zipPath) ? options.zipPath : path6.join(projectDir, options.zipPath);
399
466
  }
400
467
  if (name && version) {
401
- const candidate = path5.join(projectDir, `${name}-${version}.zip`);
402
- return fs4.existsSync(candidate) ? candidate : void 0;
468
+ const candidate = path6.join(projectDir, `${name}-${version}.zip`);
469
+ return fs5.existsSync(candidate) ? candidate : void 0;
403
470
  }
404
471
  return void 0;
405
472
  }
@@ -408,7 +475,7 @@ function verifyZip(zipPath, errors) {
408
475
  try {
409
476
  entries = new AdmZip(zipPath).getEntries();
410
477
  } catch (err) {
411
- errors.push(`Impossible de lire le zip ${path5.basename(zipPath)} : ${err.message}`);
478
+ errors.push(`Impossible de lire le zip ${path6.basename(zipPath)} : ${err.message}`);
412
479
  return;
413
480
  }
414
481
  const names = entries.map((e) => e.entryName.split("\\").join("/"));
@@ -429,60 +496,60 @@ function verifyZip(zipPath, errors) {
429
496
  }
430
497
  }
431
498
  function checkViteBase(projectDir, warnings) {
432
- const file = path5.join(projectDir, "vite.config.ts");
433
- if (!fs4.existsSync(file)) return;
434
- const content = fs4.readFileSync(file, "utf8");
499
+ const file = path6.join(projectDir, "vite.config.ts");
500
+ if (!fs5.existsSync(file)) return;
501
+ const content = fs5.readFileSync(file, "utf8");
435
502
  if (!/base:\s*['"]\.\/['"]/.test(content)) {
436
503
  warnings.push("vite.config.ts : `base: './'` introuvable \u2014 les assets risquent de casser apr\xE8s d\xE9ploiement.");
437
504
  }
438
505
  }
439
506
  function checkRouter(projectDir, warnings) {
440
- const srcDir = path5.join(projectDir, "src");
441
- if (!fs4.existsSync(srcDir)) return;
507
+ const srcDir = path6.join(projectDir, "src");
508
+ if (!fs5.existsSync(srcDir)) return;
442
509
  if (sourceUsesBrowserRouter(srcDir)) {
443
510
  warnings.push("Usage de BrowserRouter d\xE9tect\xE9 \u2014 utilisez HashRouter pour l'h\xE9bergement versionn\xE9.");
444
511
  }
445
512
  }
446
513
  function sourceUsesBrowserRouter(dir) {
447
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
448
- const full = path5.join(dir, entry.name);
514
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
515
+ const full = path6.join(dir, entry.name);
449
516
  if (entry.isDirectory()) {
450
517
  if (sourceUsesBrowserRouter(full)) return true;
451
518
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
452
- if (/\bBrowserRouter\b/.test(fs4.readFileSync(full, "utf8"))) return true;
519
+ if (/\bBrowserRouter\b/.test(fs5.readFileSync(full, "utf8"))) return true;
453
520
  }
454
521
  }
455
522
  return false;
456
523
  }
457
524
 
458
525
  // src/lib/zip.ts
459
- import fs5 from "fs";
460
- import path6 from "path";
526
+ import fs6 from "fs";
527
+ import path7 from "path";
461
528
  import AdmZip2 from "adm-zip";
462
529
  function createSourceZip(projectDir) {
463
530
  const sourceZip = new AdmZip2();
464
- const rootFiles = fs5.readdirSync(projectDir);
531
+ const rootFiles = fs6.readdirSync(projectDir);
465
532
  for (const file of rootFiles) {
466
533
  if (file === "node_modules" || file === "dist" || file.endsWith(".zip")) {
467
534
  continue;
468
535
  }
469
- const filePath = path6.join(projectDir, file);
470
- const stat = fs5.statSync(filePath);
536
+ const filePath = path7.join(projectDir, file);
537
+ const stat = fs6.statSync(filePath);
471
538
  if (stat.isDirectory()) {
472
539
  sourceZip.addLocalFolder(filePath, file);
473
540
  } else {
474
- sourceZip.addFile(file, fs5.readFileSync(filePath));
541
+ sourceZip.addFile(file, fs6.readFileSync(filePath));
475
542
  }
476
543
  }
477
544
  return sourceZip.toBuffer();
478
545
  }
479
546
  function zipDist(projectDir, name, version) {
480
- const distDir = path6.join(projectDir, "dist");
481
- if (!fs5.existsSync(distDir)) {
547
+ const distDir = path7.join(projectDir, "dist");
548
+ if (!fs6.existsSync(distDir)) {
482
549
  throw new Error("Dossier dist/ introuvable. Lancez le build avant de zipper.");
483
550
  }
484
551
  const zipFileName = `${name}-${version}.zip`;
485
- const zipFilePath = path6.join(projectDir, zipFileName);
552
+ const zipFilePath = path7.join(projectDir, zipFileName);
486
553
  const zip = new AdmZip2();
487
554
  zip.addLocalFolder(distDir);
488
555
  const sourceZipBuffer = createSourceZip(projectDir);
@@ -493,7 +560,7 @@ function zipDist(projectDir, name, version) {
493
560
 
494
561
  // src/commands/pack.ts
495
562
  function pack(opts) {
496
- const projectDir = path7.resolve(opts.dir ?? process.cwd());
563
+ const projectDir = path8.resolve(opts.dir ?? process.cwd());
497
564
  let name;
498
565
  let version;
499
566
  try {
@@ -515,7 +582,7 @@ function pack(opts) {
515
582
  error(err.message);
516
583
  return 1;
517
584
  }
518
- success(`Zip cr\xE9\xE9 : ${path7.basename(zipPath)}`);
585
+ success(`Zip cr\xE9\xE9 : ${path8.basename(zipPath)}`);
519
586
  info(`Version : ${version}`);
520
587
  info("Mise \xE0 jour de la version avec la date du jour\u2026");
521
588
  try {
@@ -531,10 +598,39 @@ function pack(opts) {
531
598
  return ok ? 0 : 1;
532
599
  }
533
600
 
601
+ // src/commands/update-docs.ts
602
+ import path9 from "path";
603
+ import pc4 from "picocolors";
604
+ function updateDocs(opts) {
605
+ const projectDir = path9.resolve(opts.dir ?? process.cwd());
606
+ try {
607
+ readManifest(projectDir);
608
+ } catch (err) {
609
+ error(`Projet cible invalide : ${err.message}`);
610
+ return 1;
611
+ }
612
+ let diff;
613
+ try {
614
+ diff = syncDocs(projectDir);
615
+ } catch (err) {
616
+ error(`\xC9chec de la mise \xE0 jour : ${err.message}`);
617
+ return 1;
618
+ }
619
+ if (isDocsUpToDate(diff)) {
620
+ success(`${DOCS_REL_DIR}/ est d\xE9j\xE0 \xE0 jour.`);
621
+ return 0;
622
+ }
623
+ success(`${DOCS_REL_DIR}/ mis \xE0 jour :`);
624
+ for (const file of diff.added) info(` ${pc4.green("+")} ${file}`);
625
+ for (const file of diff.changed) info(` ${pc4.dim("~")} ${file}`);
626
+ for (const file of diff.removed) info(` ${pc4.red("-")} ${file}`);
627
+ return 0;
628
+ }
629
+
534
630
  // src/commands/verify.ts
535
- import path8 from "path";
631
+ import path10 from "path";
536
632
  function verify(opts) {
537
- const projectDir = path8.resolve(opts.dir ?? process.cwd());
633
+ const projectDir = path10.resolve(opts.dir ?? process.cwd());
538
634
  info("V\xE9rification de conformit\xE9\u2026");
539
635
  const result = verifyExtension({ projectDir, zipPath: opts.zip });
540
636
  const ok = printVerifyResult(result);
@@ -550,6 +646,9 @@ program.command("bootstrap").description("G\xE9n\xE8re le squelette d'une extens
550
646
  program.command("apply").description("Applique un template par-dessus un projet existant (overlay).").argument("<template>", "template \xE0 appliquer (sous-dossier de templates/)").option("--dir <path>", "dossier du projet (d\xE9faut : dossier courant)").action((template, options) => {
551
647
  process.exitCode = apply(template, options);
552
648
  });
649
+ program.command("update-docs").description("Remplace la doc API (docs/inwink-api/) d'une extension existante par celle de cette version.").option("--dir <path>", "dossier du projet (d\xE9faut : dossier courant)").action((options) => {
650
+ process.exitCode = updateDocs(options);
651
+ });
553
652
  program.command("package").description("Build + zip + v\xE9rification de conformit\xE9 de l'extension.").option("--dir <path>", "dossier du projet (d\xE9faut : dossier courant)").action((options) => {
554
653
  process.exitCode = pack(options);
555
654
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inwink-mcp/backoffice-extension-helpers",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "author": "inwink",
5
5
  "license": "ISC",
6
6
  "description": "CLI pour bootstrapper, packager et vérifier les extensions backoffice inwink.",
@@ -56,7 +56,7 @@ Avant d'écrire **tout** appel API, utilise les outils MCP inwink pour découvri
56
56
 
57
57
  ### Référence des patterns d'API (`docs/inwink-api/`)
58
58
 
59
- Les outils MCP donnent les endpoints/champs **exacts** ; la doc statique dans `docs/inwink-api/` décrit les **patterns d'appel** (verbes, forme des payloads, langage de requête). Lis uniquement le fichier correspondant à ta tâche :
59
+ Les outils MCP donnent les endpoints/champs **exacts** ; la doc statique dans `docs/inwink-api/` décrit les **patterns d'appel** (verbes, forme des payloads, langage de requête). Ce dossier est une copie de la CLI helper, figée à la génération : mets-le à jour une fois par session de travail (pas avant chaque lecture) avec `npx @inwink-mcp/backoffice-extension-helpers@latest update-docs` (ne réécrit que `docs/inwink-api/`), et ne le modifie pas à la main. Lis uniquement le fichier correspondant à ta tâche :
60
60
 
61
61
  | Tâche | Fichier |
62
62
  |-------|---------|
@@ -223,6 +223,7 @@ Packaging et vérification via la CLI helper :
223
223
  ```powershell
224
224
  npx @inwink-mcp/backoffice-extension-helpers@latest package # build + zip + vérification de conformité
225
225
  npx @inwink-mcp/backoffice-extension-helpers@latest verify # vérification de conformité seule
226
+ npx @inwink-mcp/backoffice-extension-helpers@latest update-docs # met à jour docs/inwink-api/
226
227
  ```
227
228
 
228
229
  ## Conventions de code
@@ -2,6 +2,8 @@
2
2
 
3
3
  Reference for consuming the inwink APIs from the extension. Calls always go through the `useInwink*Api` hooks, which set the Bearer token and the base URL: the paths below are relative to that base.
4
4
 
5
+ > **Keep this folder current.** It is a snapshot of the helper CLI taken when the extension was generated, and it is never edited by hand. Refresh it once per working session, not before each read, with `npx @inwink-mcp/backoffice-extension-helpers@latest update-docs` (rewrites `docs/inwink-api/` only).
6
+
5
7
  > **Dynamic discovery first.** This documentation describes call *patterns* (verbs, payload shapes, query language). The **exact endpoints, fields and entities** are discovered with the MCP tools (`inwink_mcp_find_api_endpoint`, `inwink_mcp_get_entity_description`) — never guess a route or a schema.
6
8
 
7
9
  ## Contents
@@ -20,4 +20,6 @@ POST {api}/{shardId}/{entity}/massdelete
20
20
 
21
21
  Bulk DELETE, like `DELETE … WHERE` in SQL. Relies on a filter (see [query-language.md](./query-language.md#filters)).
22
22
 
23
+ Body `{ "filters": … }`. With `?direct=true`: runs in the call and returns `{ "deleted": n }`; without: returns `{ "taskId" }` (queued). Count first with the same filter (`count`).
24
+
23
25
  > A deleted "With History" entity remains readable through `history/removed/query` (see [history.md](./history.md)).
@@ -30,3 +30,10 @@
30
30
  | Massdelete | `POST {api}/{shardId}/{entity}/massdelete` | Bulk DELETE, like `DELETE … WHERE` in SQL — see [delete.md](./delete.md) |
31
31
  | Export | | Returns entities as an Excel file |
32
32
  | File | | File upload — see [files.md](./files.md) |
33
+
34
+ ## Emails and push notifications (dedicated routes)
35
+
36
+ Email templates (`mailtemplate` on events, `communitymailtemplate` on communities) and scheduled email sends (`mailschedule` / `mailschedulecommunity`) are ordinary entities (create, update, delete, query). Sending and push notifications use dedicated routes. Filters (`expression`) have the same shape as `filters` in `query`. Sending reaches real people and cannot be undone: count first.
37
+
38
+ | Purpose | Pattern | Notes |
39
+ |
@@ -20,5 +20,36 @@ POST {api}/{shardId}/{entity}/import
20
20
  ```
21
21
 
22
22
  - If an entity includes `id`, it is treated as an **update**.
23
- - Without `id`, the `identifiers` map acts as a uniqueness constraint to decide create vs. update. Above, `mail` is used to match existing records.
24
- - Much faster than individual create/edit calls.
23
+ - Without `id`, the `identifiers` map acts as a uniqueness constraint to decide create vs. update. Above, `mail` is used to match existing records (case-insensitive). Several matches → nothing is done for that row.
24
+ - Much faster than individual create/edit calls. Send batches of **200** rows (what the back-office does).
25
+ - `arrayMergeKeys` (optional, e.g. `["kinds"]`): array fields to **merge** with the existing values instead of replacing them.
26
+ - Synchronous: the response has one entry per input row, in the same order:
27
+
28
+ ```json
29
+ {
30
+ "entities": [
31
+ { "entityId": "uuid", "modelErrors": [], "modelWarnings": [], "validationErrors": [] },
32
+ { "entityIds": ["uuid1", "uuid2"], "modelErrors": [ { "property": "mail", "code": "required" } ] }
33
+ ],
34
+ "exceptionMessage": null
35
+ }
36
+ ```
37
+
38
+ A row without `entityId` was **not** written. `exceptionMessage` (top level or per row) reports a database error.
39
+
40
+ ## Import preview (check without writing)
41
+
42
+ ```
43
+ POST {api}/{shardId}/{entity}/import/preview
44
+ ```
45
+
46
+ Same body as the import (`selects` projects the matched record). Nothing is written. One entry per row:
47
+
48
+ ```json
49
+ { "entities": [ { "entity": { "id": "uuid", "mail": "…" }, "entities": null, "modelErrors": [], "modelWarnings": [], "validationErrors": [] } ] }
50
+ ```
51
+
52
+ - `entity` = the existing record that would be **updated**; `null` = the row would be **created**.
53
+ - `entities` with several items = ambiguous match (nothing would be done).
54
+ - Errors on fields the row did not send are only warnings on an update; on a create every mandatory field must be present.
55
+ - Run the preview, show the result to the user, then send the **same** body to `/import`.
@@ -33,4 +33,6 @@ POST {api}/{shardId}/{entity}/massupdate
33
33
 
34
34
  Bulk EDIT, like `UPDATE … WHERE` in SQL. Relies on a filter (see [query-language.md](./query-language.md#filters)).
35
35
 
36
+ Body `{ "query": { "filters": … }, "input": { field: value } }`. With `?direct=true`: runs in the call and returns `{ "updated": n }`; without: returns `{ "taskId" }` (queued). Count first with the same filter (`count`).
37
+
36
38
  > To update a batch of entities identified one by one, see also [import.md](./import.md).