@ultimat3/cli 21.0.0 → 22.0.0

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.
Files changed (234) hide show
  1. package/CLAUDE.md +84 -1650
  2. package/README.md +40 -2
  3. package/package.json +30 -30
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-load.ts +37 -5
  8. package/src/app-openapi.ts +27 -0
  9. package/src/app-permissions.ts +0 -0
  10. package/src/app-root.ts +1 -1
  11. package/src/async-pages.ts +32 -0
  12. package/src/bin.ts +7 -1
  13. package/src/boundary-findings.ts +36 -0
  14. package/src/browser-launcher-fake-html.ts +80 -0
  15. package/src/browser-launcher-fake.ts +165 -0
  16. package/src/browser-launcher-port.ts +159 -0
  17. package/src/browser-launcher.ts +26 -133
  18. package/src/budgets.ts +11 -1
  19. package/src/cdp-shot-a11y.ts +77 -0
  20. package/src/cdp-shot-clock.ts +14 -0
  21. package/src/cdp-shot-driver.ts +150 -0
  22. package/src/cdp-shot-element.ts +147 -0
  23. package/src/cdp-shot-errors.ts +62 -0
  24. package/src/cdp-shot-keys.ts +152 -0
  25. package/src/cdp-shot-page.ts +230 -0
  26. package/src/cdp-shot-watch.ts +241 -0
  27. package/src/cmd-affected-spec.ts +24 -0
  28. package/src/cmd-affected.ts +3 -19
  29. package/src/cmd-build-spec.ts +16 -0
  30. package/src/cmd-build.ts +46 -20
  31. package/src/cmd-ci-spec.ts +24 -0
  32. package/src/cmd-ci.ts +17 -21
  33. package/src/cmd-db-branch.ts +2 -2
  34. package/src/cmd-db-spec.ts +99 -0
  35. package/src/cmd-db.ts +15 -94
  36. package/src/cmd-deploy-helm.ts +136 -0
  37. package/src/cmd-deploy-spec.ts +40 -0
  38. package/src/cmd-deploy.ts +111 -43
  39. package/src/cmd-dev-spec.ts +25 -0
  40. package/src/cmd-dev.ts +55 -36
  41. package/src/cmd-docs-spec.ts +16 -0
  42. package/src/cmd-docs.ts +2 -11
  43. package/src/cmd-doctor-spec.ts +21 -0
  44. package/src/cmd-doctor.ts +4 -18
  45. package/src/cmd-env-spec.ts +18 -0
  46. package/src/cmd-env.ts +4 -13
  47. package/src/cmd-errors-spec.ts +23 -0
  48. package/src/cmd-errors.ts +3 -17
  49. package/src/cmd-fix-spec.ts +18 -0
  50. package/src/cmd-fix.ts +4 -13
  51. package/src/cmd-generate-spec.ts +31 -0
  52. package/src/cmd-generate.ts +46 -42
  53. package/src/cmd-i18n-spec.ts +16 -0
  54. package/src/cmd-i18n.ts +7 -14
  55. package/src/cmd-jobs-spec.ts +56 -0
  56. package/src/cmd-jobs.ts +3 -50
  57. package/src/cmd-manifest-spec.ts +15 -0
  58. package/src/cmd-manifest.ts +19 -20
  59. package/src/cmd-mcp-spec.ts +21 -0
  60. package/src/cmd-mcp.ts +2 -15
  61. package/src/cmd-new-spec.ts +33 -0
  62. package/src/cmd-new.ts +38 -33
  63. package/src/cmd-policy-spec.ts +13 -0
  64. package/src/cmd-policy.ts +2 -9
  65. package/src/cmd-pr-spec.ts +39 -0
  66. package/src/cmd-pr.ts +5 -65
  67. package/src/cmd-registries-spec.ts +32 -0
  68. package/src/cmd-registries.ts +4 -25
  69. package/src/cmd-routes-spec.ts +12 -0
  70. package/src/cmd-routes.ts +2 -7
  71. package/src/cmd-secrets-spec.ts +19 -0
  72. package/src/cmd-secrets.ts +22 -34
  73. package/src/cmd-shot-island.ts +12 -13
  74. package/src/cmd-shot-spec.ts +53 -0
  75. package/src/cmd-shot.ts +11 -60
  76. package/src/cmd-tasks-spec.ts +21 -0
  77. package/src/cmd-tasks.ts +2 -16
  78. package/src/cmd-test-spec.ts +54 -0
  79. package/src/cmd-test.ts +4 -49
  80. package/src/cmd-verify-spec.ts +28 -0
  81. package/src/cmd-verify.ts +3 -23
  82. package/src/db-seed.ts +4 -2
  83. package/src/dev-dashboard.ts +4 -4
  84. package/src/dev-lock.ts +18 -2
  85. package/src/dev-port.ts +21 -0
  86. package/src/dev-route-table.ts +3 -3
  87. package/src/dispatch.ts +12 -5
  88. package/src/document-styles.ts +1 -1
  89. package/src/drift.ts +4 -18
  90. package/src/error-catalog.ts +16 -9
  91. package/src/error-codes.ts +28 -30
  92. package/src/error-pages.ts +19 -7
  93. package/src/errors.ts +14 -0
  94. package/src/favicon.ts +2 -2
  95. package/src/fix-imports.ts +1 -1
  96. package/src/fix-scan.ts +2 -9
  97. package/src/flag-reads.ts +1 -2
  98. package/src/foreign-text.ts +36 -0
  99. package/src/framework-schema.ts +5 -6
  100. package/src/generate-feature.ts +42 -0
  101. package/src/generate-files.ts +7 -3
  102. package/src/generate-grants.ts +83 -0
  103. package/src/generate-kinds.ts +56 -4
  104. package/src/i18n-index.ts +59 -10
  105. package/src/icon-assets.ts +1 -1
  106. package/src/image-prepare.ts +14 -0
  107. package/src/import-scan.ts +63 -0
  108. package/src/index.ts +67 -348
  109. package/src/invocation-flags.ts +26 -0
  110. package/src/island-bundle.ts +6 -1
  111. package/src/island-capture.ts +6 -6
  112. package/src/island-harness-script.ts +6 -2
  113. package/src/island-realtime.ts +15 -8
  114. package/src/island-shot.ts +5 -0
  115. package/src/island-store.ts +131 -0
  116. package/src/island-verdict.ts +1 -1
  117. package/src/job-registration.ts +42 -0
  118. package/src/jobs-driver.ts +2 -2
  119. package/src/load-findings.ts +51 -0
  120. package/src/mcp-db-target.ts +1 -1
  121. package/src/mcp-errors.ts +28 -27
  122. package/src/mcp-host.ts +27 -12
  123. package/src/mcp-ui-diff.ts +27 -0
  124. package/src/mcp-ui-inspect.ts +4 -4
  125. package/src/mcp-ui-interact.ts +21 -15
  126. package/src/mcp-ui.ts +19 -15
  127. package/src/measure-database.ts +73 -0
  128. package/src/measure-paths.ts +74 -0
  129. package/src/measure-scope.ts +74 -0
  130. package/src/messages.ts +1 -3
  131. package/src/metrics-endpoint.ts +1 -1
  132. package/src/otlp-export.ts +1 -1
  133. package/src/output.ts +6 -0
  134. package/src/permission-grants.ts +86 -0
  135. package/src/prerender-out.ts +25 -0
  136. package/src/prerender.ts +121 -102
  137. package/src/pwa-artifacts.ts +3 -3
  138. package/src/reexport-manifest.ts +2 -1
  139. package/src/registry.ts +80 -56
  140. package/src/role-realtime.ts +36 -0
  141. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  142. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  143. package/src/role-start-types.ts +112 -0
  144. package/src/{dev-roles.ts → role-start.ts} +42 -115
  145. package/src/{dev-sync.ts → role-sync.ts} +18 -8
  146. package/src/root-env.ts +67 -0
  147. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  148. package/src/{dev-services.ts → runtime-bindings.ts} +39 -16
  149. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  150. package/src/runtime-jobs.ts +87 -0
  151. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +18 -5
  152. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  153. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  154. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  155. package/src/runtime-realtime.ts +55 -0
  156. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  157. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  158. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  159. package/src/scaffold-fixture.ts +28 -6
  160. package/src/scaffold-typecheck.ts +6 -3
  161. package/src/schema-drift.ts +7 -1
  162. package/src/script-csp.ts +5 -2
  163. package/src/secrets-rotation.ts +59 -0
  164. package/src/serve-boot.ts +192 -0
  165. package/src/serve-drain.ts +24 -0
  166. package/src/serve-entry.ts +6 -0
  167. package/src/serve-env.ts +116 -0
  168. package/src/serve-types.ts +55 -0
  169. package/src/serve.ts +44 -347
  170. package/src/shot-server.ts +2 -2
  171. package/src/shot-settle.ts +10 -1
  172. package/src/shot-theme.ts +3 -3
  173. package/src/shot-verdict.ts +16 -7
  174. package/src/signal-shred.ts +27 -0
  175. package/src/solid-loader.ts +26 -2
  176. package/src/static-report.ts +8 -1
  177. package/src/templates/action.ts +30 -16
  178. package/src/templates/entity.ts +12 -7
  179. package/src/templates/index.ts +1 -1
  180. package/src/templates/job.ts +10 -7
  181. package/src/templates/policy.ts +20 -2
  182. package/src/templates/resource-create.ts +127 -0
  183. package/src/templates/resource-form-island.ts +49 -12
  184. package/src/templates/resource.ts +10 -4
  185. package/src/templates/scaffold-app.ts +4 -1
  186. package/src/templates/scaffold-auth.ts +3 -1
  187. package/src/templates/scaffold-container-compose.ts +184 -0
  188. package/src/templates/scaffold-container.ts +24 -159
  189. package/src/templates/scaffold-dashboard-example.ts +2 -2
  190. package/src/templates/scaffold-db-package.ts +16 -3
  191. package/src/templates/scaffold-demo-org.ts +41 -0
  192. package/src/templates/scaffold-entries.ts +1 -1
  193. package/src/templates/scaffold-helm-templates.ts +66 -7
  194. package/src/templates/scaffold-helm.ts +27 -5
  195. package/src/templates/scaffold-i18n.ts +15 -10
  196. package/src/templates/scaffold-repo.ts +13 -10
  197. package/src/templates/scaffold-roles.ts +38 -10
  198. package/src/templates/slice-foundation.ts +1 -1
  199. package/src/templates/wrap.ts +4 -1
  200. package/src/test-passes.ts +2 -1
  201. package/src/test-workers.ts +26 -0
  202. package/src/ts-scan.ts +3 -6
  203. package/src/tsconfig-references.ts +1 -2
  204. package/src/verify-checks.ts +41 -39
  205. package/src/verify-e2e.ts +10 -7
  206. package/src/verify-run.ts +11 -1
  207. package/src/verify-step.ts +3 -3
  208. package/src/verify-tests.ts +1 -26
  209. package/src/verify-typecheck.ts +28 -0
  210. package/src/web-binding.ts +2 -2
  211. package/src/workspace-graph.ts +10 -33
  212. package/src/cdp-browser.ts +0 -94
  213. package/src/cdp-connection.ts +0 -247
  214. package/src/cdp-e2e-page.ts +0 -180
  215. package/src/cdp-e2e-session.ts +0 -199
  216. package/src/cdp-errors.ts +0 -56
  217. package/src/cdp-launch.ts +0 -193
  218. package/src/cdp-offline-script.ts +0 -73
  219. package/src/cdp-pipe.ts +0 -77
  220. package/src/e2e-app.ts +0 -103
  221. package/src/e2e-browser-handle.ts +0 -55
  222. package/src/e2e-dom-fixture.ts +0 -117
  223. package/src/e2e-driver.ts +0 -117
  224. package/src/e2e-errors.ts +0 -117
  225. package/src/e2e-evaluate.ts +0 -156
  226. package/src/e2e-locator.ts +0 -86
  227. package/src/e2e-page.ts +0 -153
  228. package/src/e2e-preload.ts +0 -64
  229. package/src/e2e-probe.ts +0 -23
  230. package/src/e2e-selection.ts +0 -182
  231. package/src/e2e-spawn.ts +0 -169
  232. package/src/measurement-actor.ts +0 -26
  233. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
  234. /package/src/{dev-render.ts → runtime-render.ts} +0 -0
@@ -0,0 +1,18 @@
1
+ // `x fix`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-fix.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const FIX_SUBCOMMANDS = ['boundary'] as const;
7
+
8
+ export const fixSpec: CommandSpec = {
9
+ name: 'fix',
10
+ // Says "plan" in the one line `x help` prints. The name is kept — five packages' `fix:` lines
11
+ // cite `x fix boundary <file>` and renaming a shipped command breaks every one of them — so
12
+ // the honest move is to stop the summary from promising a repair the command never performs.
13
+ summary: 'plan the minimal cut for an import that crossed a surface boundary (never rewrites)',
14
+ usage: 'x fix boundary <file> [--json]',
15
+ requiresApp: true,
16
+ subcommands: FIX_SUBCOMMANDS,
17
+ defaultSubcommand: 'boundary',
18
+ };
package/src/cmd-fix.ts CHANGED
@@ -8,16 +8,17 @@ import { appImportGraph, readAppSources } from './app-boundaries';
8
8
  import { requireAppRoot } from './app-root';
9
9
  import type { BoundaryCut } from './boundary-cuts';
10
10
  import { planBoundaryCuts } from './boundary-cuts';
11
+ import { fixSpec } from './cmd-fix-spec';
11
12
  import type { CliCommand, CommandContext } from './command';
12
13
  import { BadFlagError, FixTargetUnknownError, MissingPositionalError } from './errors';
13
14
  import { msg } from './messages';
14
15
  import type { CommandResult, Finding, JsonValue } from './output';
15
16
 
17
+ export { FIX_SUBCOMMANDS } from './cmd-fix-spec';
18
+
16
19
  export type { BoundaryCut };
17
20
  export { planBoundaryCuts };
18
21
 
19
- export const FIX_SUBCOMMANDS = ['boundary'] as const;
20
-
21
22
  /**
22
23
  * Accept either an app-root-relative path or a suffix that matches exactly one scanned file —
23
24
  * an agent copying the path out of a `fix:` line has the short form
@@ -88,17 +89,7 @@ const editCount = (cuts: readonly BoundaryCut[]): number =>
88
89
  new Set(cuts.map((cut) => JSON.stringify([cut.edge.from, cut.edge.to]))).size;
89
90
 
90
91
  export const fixCommand: CliCommand = {
91
- spec: {
92
- name: 'fix',
93
- // Says "plan" in the one line `x help` prints. The name is kept — five packages' `fix:` lines
94
- // cite `x fix boundary <file>` and renaming a shipped command breaks every one of them — so
95
- // the honest move is to stop the summary from promising a repair the command never performs.
96
- summary: 'plan the minimal cut for an import that crossed a surface boundary (never rewrites)',
97
- usage: 'x fix boundary <file> [--json]',
98
- requiresApp: true,
99
- subcommands: FIX_SUBCOMMANDS,
100
- defaultSubcommand: 'boundary',
101
- },
92
+ spec: fixSpec,
102
93
  async run(ctx: CommandContext): Promise<CommandResult> {
103
94
  const root = requireAppRoot('fix', ctx.cwd).dir;
104
95
  // Refused before the scan, never defaulted to `''`: an empty string reached `resolveTarget` as
@@ -0,0 +1,31 @@
1
+ // `x generate`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-generate.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import { GENERATORS } from './generate-kinds';
5
+ import type { CommandSpec } from './parse';
6
+
7
+ export const generateSpec: CommandSpec = {
8
+ name: 'g',
9
+ aliases: ['generate'],
10
+ summary: 'scaffold a primitive with its passing test',
11
+ // Projected from `GENERATORS`, never restated: the literal that used to live here had already
12
+ // drifted — it omitted `backfill` — and a usage line that can disagree with the list it
13
+ // describes is exactly the second source of truth axiom 2 forbids.
14
+ usage: `x g ${GENERATORS.join('|')} <name> [--feature f]`,
15
+ // Declared from the SAME constant `readKind` validates against: without it `fix-command.ts`
16
+ // has no set to judge the word after `x g`, and two shipped `@ultimat3/admin` fix lines said
17
+ // `x g migration` — a generator that has never existed — straight through the `errors` gate.
18
+ positionalChoices: GENERATORS,
19
+ requiresApp: true,
20
+ flags: [
21
+ { name: 'feature', type: 'string', summary: 'feature slice to write into' },
22
+ { name: 'surface', type: 'string', summary: 'site | app', default: 'app' },
23
+ { name: 'live', type: 'boolean', summary: 'subscribable query' },
24
+ { name: 'admin', type: 'boolean', summary: 'resource: also emit the admin override' },
25
+ { name: 'locales', type: 'string', summary: 'comma-separated locales, default en' },
26
+ { name: 'at', type: 'string', summary: 'island, admin:page: directory to write into' },
27
+ { name: 'permission', type: 'string', summary: 'admin:page: the permission it needs' },
28
+ { name: 'force', type: 'boolean', summary: 'overwrite existing files' },
29
+ { name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
30
+ ],
31
+ };
@@ -4,19 +4,27 @@
4
4
 
5
5
  import { existsSync } from 'node:fs';
6
6
  import { MANIFEST_FILENAME } from '@ultimat3/manifest';
7
- import { appManifest, writeAppManifest } from './app-manifest';
7
+ import { registerGeneratedPrimitives } from './api-registration';
8
+ import { writeAppArtifacts } from './app-artifacts';
9
+ import { appManifest } from './app-manifest';
8
10
  import { requireAppRoot } from './app-root';
11
+ import { generateSpec } from './cmd-generate-spec';
9
12
  import type { CliCommand, CommandContext } from './command';
13
+ import { invocationOf } from './command';
14
+ import { assertFeatureExists } from './generate-feature';
10
15
  import { generate, sliceDir } from './generate-files';
16
+ import { grantGeneratedPermissions } from './generate-grants';
11
17
  import type { Generator } from './generate-kinds';
12
- import { GENERATORS, readKind, readName, readPermission, readSurface } from './generate-kinds';
18
+ import { readFeature, readKind, readName, readPermission, readSurface } from './generate-kinds';
13
19
  import { containedPath, writeFiles } from './generate-write';
14
20
  import { resolveCatalogModule } from './i18n-audit';
15
21
  import { syncI18nIndex } from './i18n-index';
22
+ import { reproducedFlags } from './invocation-flags';
16
23
  import { msg } from './messages';
17
24
  import type { CommandResult, Finding } from './output';
18
25
  import { flagBool, flagList, flagString } from './parse';
19
- import { resolveLocales } from './templates';
26
+ import { quoteArg } from './shell-quote';
27
+ import { kebab, resolveLocales } from './templates';
20
28
 
21
29
  // One import path for the generator, unchanged by the split: `index.ts`, `x new` and the scaffold
22
30
  // fixture reach the kinds, the pure file list and the writer through this module, and a second path
@@ -29,36 +37,12 @@ export type { WriteReport } from './generate-write';
29
37
  export { dedupe, writeFiles } from './generate-write';
30
38
 
31
39
  export const generateCommand: CliCommand = {
32
- spec: {
33
- name: 'g',
34
- aliases: ['generate'],
35
- summary: 'scaffold a primitive with its passing test',
36
- // Projected from `GENERATORS`, never restated: the literal that used to live here had already
37
- // drifted — it omitted `backfill` — and a usage line that can disagree with the list it
38
- // describes is exactly the second source of truth axiom 2 forbids.
39
- usage: `x g ${GENERATORS.join('|')} <name> [--feature f]`,
40
- // Declared from the SAME constant `readKind` validates against: without it `fix-command.ts`
41
- // has no set to judge the word after `x g`, and two shipped `@ultimat3/admin` fix lines said
42
- // `x g migration` — a generator that has never existed — straight through the `errors` gate.
43
- positionalChoices: GENERATORS,
44
- requiresApp: true,
45
- flags: [
46
- { name: 'feature', type: 'string', summary: 'feature slice to write into' },
47
- { name: 'surface', type: 'string', summary: 'site | app', default: 'app' },
48
- { name: 'live', type: 'boolean', summary: 'subscribable query' },
49
- { name: 'admin', type: 'boolean', summary: 'resource: also emit the admin override' },
50
- { name: 'locales', type: 'string', summary: 'comma-separated locales, default en' },
51
- { name: 'at', type: 'string', summary: 'island, admin:page: directory to write into' },
52
- { name: 'permission', type: 'string', summary: 'admin:page: the permission it needs' },
53
- { name: 'force', type: 'boolean', summary: 'overwrite existing files' },
54
- { name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
55
- ],
56
- },
40
+ spec: generateSpec,
57
41
  async run(ctx: CommandContext): Promise<CommandResult> {
58
42
  const root = requireAppRoot('g', ctx.cwd).dir;
59
43
  const kind = readKind(ctx.args.positionals[0]);
60
44
  const name = readName(ctx.args.positionals[1], kind);
61
- const featureFlag = flagString(ctx.args, 'feature');
45
+ const featureFlag = readFeature(flagString(ctx.args, 'feature'), kind);
62
46
  // Both flags are resolved before a single file is planned: a bad surface or a locale that is
63
47
  // really a path fails here, with nothing written and nothing to undo.
64
48
  const surface = readSurface(flagString(ctx.args, 'surface'), kind, name);
@@ -72,7 +56,9 @@ export const generateCommand: CliCommand = {
72
56
  // imports `useT()` from is a fact about THIS app, and `generate` is a pure function.
73
57
  const catalogModule = await resolveCatalogModule(root);
74
58
  // Read for the same reason: which errors the slice declares is written on THIS app's disk.
75
- const slice = sliceDir(surface, featureFlag ?? name);
59
+ const slice = sliceDir(surface, kebab(featureFlag ?? name));
60
+ // A named slice that is not there is refused, never invented (X_FEATURE_UNKNOWN).
61
+ assertFeatureExists(root, kind, featureFlag, slice);
76
62
  const sliceErrors = await readSliceErrors(root, kind, slice);
77
63
  // Same reason again: whether `job`/`task` may assume the tenant-scoped shape is a fact about
78
64
  // THIS feature's own `entity.ts`/`repo.ts`, not a default the template gets to assume.
@@ -102,16 +88,27 @@ export const generateCommand: CliCommand = {
102
88
  lines: files.map((file) => msg('cli.file.added', { path: file.path })),
103
89
  };
104
90
  }
105
- const report = await writeFiles(
106
- root,
107
- files,
108
- flagBool(ctx.args, 'force'),
109
- `x g ${kind} ${name}`,
110
- );
91
+ // The caller's own invocation, EVERY flag it set included: without `--feature` the fix wrote
92
+ // a second slice beside the one that conflicted.
93
+ const invocation = [
94
+ invocationOf(ctx, 'g'),
95
+ kind,
96
+ quoteArg(name),
97
+ ...reproducedFlags(generateCommand.spec, ctx.args),
98
+ ].join(' ');
99
+ const report = await writeFiles(root, files, flagBool(ctx.args, 'force'), invocation);
100
+ // The two edits a generated primitive needs outside its own slice, performed rather than left
101
+ // as findings: a declared permission granted to a role, and a job listed in `defineApi`.
102
+ // Before the manifest load below, so the projection sees both.
103
+ const edited = [
104
+ ...(await grantGeneratedPermissions(root, report.written)),
105
+ ...(await registerGeneratedPrimitives(root, report.written)),
106
+ ];
111
107
  // A locale's catalog existing on disk and the app being able to select it are two different
112
108
  // facts — see `syncI18nIndex`. Runs before the manifest load below so a route or resource
113
109
  // this same invocation just wrote never gets projected against a stale catalog registration.
114
- if (report.written.length > 0) await syncI18nIndex(root);
110
+ const indexSync =
111
+ report.written.length > 0 ? await syncI18nIndex(root) : { registered: true, findings: [] };
115
112
  // Facts, not prose: every `x g` run leaves the route/action/entity/job/policy table current,
116
113
  // the same guarantee `x manifest` makes on its own — an agent reading it after `x g` never
117
114
  // sees a resource that exists on disk but not in the manifest.
@@ -124,18 +121,23 @@ export const generateCommand: CliCommand = {
124
121
  // partial load would replace the compatibility contract with a subset of the app. The scaffold
125
122
  // stays on disk — only the projection is withheld, and the load failures travel as findings.
126
123
  const loadFailures: Finding[] = [];
124
+ // `openapi.json` rides with it (`writeAppArtifacts`): refreshing one contract and not the other
125
+ // made this command's own output fail the `contract-diff` step.
126
+ const artifacts: string[] = [];
127
127
  if (report.written.length > 0 && existsSync(containedPath(root, MANIFEST_FILENAME))) {
128
128
  const { manifest, findings } = await appManifest(root);
129
129
  if (findings.length === 0) {
130
- await writeAppManifest(root, manifest);
130
+ artifacts.push(
131
+ ...(await writeAppArtifacts(root, manifest, { openapi: true, onlyExisting: true })),
132
+ );
131
133
  buildId = manifest.buildId;
132
134
  } else loadFailures.push(...findings);
133
135
  }
134
- const findings = [...report.conflicts, ...loadFailures];
136
+ const findings = [...report.conflicts, ...indexSync.findings, ...loadFailures];
135
137
  // One list behind all three renderings. The manifest was printed as a `+` line while the count
136
138
  // beside it came from `report.written` alone, so `x g island` said "wrote 2 file(s)" over three
137
139
  // lines — and `--json` carried the shorter list, which is the drift `--json` exists to prevent.
138
- const written = [...report.written, ...(buildId === undefined ? [] : [MANIFEST_FILENAME])];
140
+ const written = [...report.written, ...edited, ...artifacts];
139
141
  return {
140
142
  ok: findings.length === 0,
141
143
  command: 'g',
@@ -166,7 +168,7 @@ async function readSliceErrors(
166
168
  }
167
169
 
168
170
  /**
169
- * `job` and `task` only: the slice's `entity.ts`/`repo.ts` as they stand on disk, absent when the
171
+ * `job`, `task`, `action` and `mutator`: the slice's `entity.ts`/`repo.ts` as they stand on disk, absent when the
170
172
  * generator's kind is neither or the file does not exist yet. `readSliceErrors`'s reason —
171
173
  * whichever generator reads it decides on THIS app's disk, not on a default the template assumes.
172
174
  */
@@ -176,7 +178,9 @@ async function readSliceFile(
176
178
  slice: string,
177
179
  name: 'entity.ts' | 'repo.ts',
178
180
  ): Promise<string | undefined> {
179
- if (kind !== 'job' && kind !== 'task') return undefined;
181
+ if (kind !== 'job' && kind !== 'task' && kind !== 'action' && kind !== 'mutator') {
182
+ return undefined;
183
+ }
180
184
  const file = containedPath(root, `${slice}/${name}`);
181
185
  return existsSync(file) ? await Bun.file(file).text() : undefined;
182
186
  }
@@ -0,0 +1,16 @@
1
+ // `x i18n`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-i18n.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const I18N_SUBCOMMANDS = ['check', 'add', 'sync'] as const;
7
+
8
+ export const i18nSpec: CommandSpec = {
9
+ name: 'i18n',
10
+ summary: 'catalogs: add a locale, sync keys, check for gaps',
11
+ usage: 'x i18n [check|add <locale>|sync <locale>] [--json]',
12
+ requiresApp: true,
13
+ subcommands: I18N_SUBCOMMANDS,
14
+ // The bare `x i18n` audits; `add` and `sync` write catalogs and must be asked for.
15
+ defaultSubcommand: 'check',
16
+ };
package/src/cmd-i18n.ts CHANGED
@@ -13,6 +13,7 @@ import type { Catalog } from '@ultimat3/i18n';
13
13
  import { auditCatalogs, catalogKeys } from '@ultimat3/i18n';
14
14
  import { loadApp } from './app-load';
15
15
  import { requireAppRoot } from './app-root';
16
+ import { i18nSpec } from './cmd-i18n-spec';
16
17
  import type { CliCommand, CommandContext } from './command';
17
18
  import { BadFlagError, CatalogExistsError, MissingPositionalError } from './errors';
18
19
  import {
@@ -36,7 +37,7 @@ import type { CommandResult, Finding, JsonValue } from './output';
36
37
  import { renderTable } from './table';
37
38
  import { catalogPath, resolveLocales } from './templates/locales';
38
39
 
39
- export const I18N_SUBCOMMANDS = ['check', 'add', 'sync'] as const;
40
+ export { I18N_SUBCOMMANDS } from './cmd-i18n-spec';
40
41
 
41
42
  /** `ExtractReport` is plain JSON by construction — same idiom as `cmd-registries.ts`'s `asJson`. */
42
43
  const asJson = (value: object): Record<string, JsonValue> => value as Record<string, JsonValue>;
@@ -192,7 +193,7 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
192
193
  // answered `X_CATALOG_UNREGISTERED` with a fix naming an edit that had already been made. Same
193
194
  // writer `x g --locales` already uses, so a catalog on disk and a selectable locale can never be
194
195
  // two different sets (#F4).
195
- const registered = await syncI18nIndex(root);
196
+ const { registered, findings: indexFindings } = await syncI18nIndex(root);
196
197
 
197
198
  const keys = catalogKeys(seeded).length;
198
199
  return {
@@ -202,7 +203,7 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
202
203
  ok: true,
203
204
  command: 'i18n',
204
205
  summary: msg('cli.i18n.added', { locale, keys, from: from ?? locale }),
205
- findings: app.findings,
206
+ findings: [...indexFindings, ...app.findings],
206
207
  // `registered` is false only for an app with no `packages/i18n` at all — a fact a caller has
207
208
  // to be able to read, because it is the one case where the locale is on disk and unselectable.
208
209
  data: { locale, from: from ?? locale, keys, path, registered },
@@ -270,7 +271,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
270
271
  // hand-created file, a `git merge` — is exactly the unregistered locale the gate refuses, and
271
272
  // this is the command its `fix:` names. Re-deriving an index that is already correct writes the
272
273
  // same bytes.
273
- const registered = await syncI18nIndex(root);
274
+ const { registered, findings: indexFindings } = await syncI18nIndex(root);
274
275
 
275
276
  const total = catalogKeys(merged).length;
276
277
  return {
@@ -282,7 +283,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
282
283
  // The keys themselves, raw — `runCheck` lists gaps the same way, because a key is a value an
283
284
  // author copies and never prose the catalog owns.
284
285
  lines: seeded ? added.map((key) => ` ${key}`) : [],
285
- findings: app.findings,
286
+ findings: [...indexFindings, ...app.findings],
286
287
  data: {
287
288
  locale,
288
289
  from: from ?? locale,
@@ -298,15 +299,7 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
298
299
  }
299
300
 
300
301
  export const i18nCommand: CliCommand = {
301
- spec: {
302
- name: 'i18n',
303
- summary: 'catalogs: add a locale, sync keys, check for gaps',
304
- usage: 'x i18n [check|add <locale>|sync <locale>] [--json]',
305
- requiresApp: true,
306
- subcommands: I18N_SUBCOMMANDS,
307
- // The bare `x i18n` audits; `add` and `sync` write catalogs and must be asked for.
308
- defaultSubcommand: 'check',
309
- },
302
+ spec: i18nSpec,
310
303
  async run(ctx: CommandContext): Promise<CommandResult> {
311
304
  const root = requireAppRoot('i18n', ctx.cwd).dir;
312
305
  const sub = ctx.args.subcommand ?? 'check';
@@ -0,0 +1,56 @@
1
+ // `x jobs`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-jobs.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const JOBS_SUBCOMMANDS = ['ls', 'show', 'retry', 'cancel', 'drain'] as const;
7
+
8
+ /**
9
+ * The drivers a drain may move work ONTO — every one of them durable, and that is the whole rule.
10
+ * Closed, and read three ways: the flag summary, the refusal, and the `memory` case below.
11
+ */
12
+ export const DRAIN_TARGETS = ['redis', 'nats'] as const;
13
+
14
+ export const jobsSpec: CommandSpec = {
15
+ name: 'jobs',
16
+ summary: 'list, show, retry, cancel and drain the job queue',
17
+ usage:
18
+ 'x jobs [ls|show <id>|retry <id>|cancel <id>|drain --to <driver>] [--queue q] [--state s] [--limit n] [--from-step name] [--reason text] [--to driver] [--dry-run] [--json]',
19
+ requiresApp: true,
20
+ subcommands: JOBS_SUBCOMMANDS,
21
+ // The bare `x jobs` lists; it never retries, cancels or drains anything.
22
+ defaultSubcommand: 'ls',
23
+ flags: [
24
+ { name: 'queue', type: 'string', summary: 'filter by queue name' },
25
+ { name: 'state', type: 'string', summary: 'filter by job state' },
26
+ { name: 'limit', type: 'string', summary: 'max rows to return' },
27
+ { name: 'name', type: 'string', summary: 'filter by job name' },
28
+ // Each of these is read by ONE subcommand — `retryJob`, `cancelJob`, `runDrain` — and says
29
+ // so in its own summary. The scope is what makes the parser refuse it anywhere else instead
30
+ // of accepting it and ignoring it: `x db gen --dry-run` parsed and wrote the migration.
31
+ {
32
+ name: 'from-step',
33
+ type: 'string',
34
+ summary: 'retry: drop this step so it re-executes',
35
+ subcommands: ['retry'],
36
+ },
37
+ {
38
+ name: 'reason',
39
+ type: 'string',
40
+ summary: 'cancel: why, recorded on the job',
41
+ subcommands: ['cancel'],
42
+ },
43
+ {
44
+ name: 'to',
45
+ type: 'string',
46
+ summary: `drain: target driver — ${DRAIN_TARGETS.join(', ')}`,
47
+ subcommands: ['drain'],
48
+ },
49
+ {
50
+ name: 'dry-run',
51
+ type: 'boolean',
52
+ summary: 'drain: report the plan, move nothing',
53
+ subcommands: ['drain'],
54
+ },
55
+ ],
56
+ };
package/src/cmd-jobs.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import type { JobDriver } from '@ultimat3/jobs';
8
8
  import { cancelJob, createNatsDriver, createRedisDriver } from '@ultimat3/jobs';
9
9
  import { requireAppRoot } from './app-root';
10
+ import { DRAIN_TARGETS, jobsSpec } from './cmd-jobs-spec';
10
11
  import type { CliCommand, CommandContext } from './command';
11
12
  import { BadFlagError, JobUnknownError, MissingPositionalError } from './errors';
12
13
  import type { DrainOutcome } from './jobs-drain';
@@ -27,13 +28,7 @@ import { msg } from './messages';
27
28
  import type { CommandResult } from './output';
28
29
  import { flagBool, flagString } from './parse';
29
30
 
30
- export const JOBS_SUBCOMMANDS = ['ls', 'show', 'retry', 'cancel', 'drain'] as const;
31
-
32
- /**
33
- * The drivers a drain may move work ONTO — every one of them durable, and that is the whole rule.
34
- * Closed, and read three ways: the flag summary, the refusal, and the `memory` case below.
35
- */
36
- export const DRAIN_TARGETS = ['redis', 'nats'] as const;
31
+ export { DRAIN_TARGETS, JOBS_SUBCOMMANDS } from './cmd-jobs-spec';
37
32
 
38
33
  /**
39
34
  * `memory` was on that list until 2026-09 and could not be: `createMemoryDriver()` is a `Map` in
@@ -252,49 +247,7 @@ async function runDrain(
252
247
  }
253
248
 
254
249
  export const jobsCommand: CliCommand = {
255
- spec: {
256
- name: 'jobs',
257
- summary: 'list, show, retry, cancel and drain the job queue',
258
- usage:
259
- 'x jobs [ls|show <id>|retry <id>|cancel <id>|drain --to <driver>] [--queue q] [--state s] [--limit n] [--from-step name] [--reason text] [--to driver] [--dry-run] [--json]',
260
- requiresApp: true,
261
- subcommands: JOBS_SUBCOMMANDS,
262
- // The bare `x jobs` lists; it never retries, cancels or drains anything.
263
- defaultSubcommand: 'ls',
264
- flags: [
265
- { name: 'queue', type: 'string', summary: 'filter by queue name' },
266
- { name: 'state', type: 'string', summary: 'filter by job state' },
267
- { name: 'limit', type: 'string', summary: 'max rows to return' },
268
- { name: 'name', type: 'string', summary: 'filter by job name' },
269
- // Each of these is read by ONE subcommand — `retryJob`, `cancelJob`, `runDrain` — and says
270
- // so in its own summary. The scope is what makes the parser refuse it anywhere else instead
271
- // of accepting it and ignoring it: `x db gen --dry-run` parsed and wrote the migration.
272
- {
273
- name: 'from-step',
274
- type: 'string',
275
- summary: 'retry: drop this step so it re-executes',
276
- subcommands: ['retry'],
277
- },
278
- {
279
- name: 'reason',
280
- type: 'string',
281
- summary: 'cancel: why, recorded on the job',
282
- subcommands: ['cancel'],
283
- },
284
- {
285
- name: 'to',
286
- type: 'string',
287
- summary: `drain: target driver — ${DRAIN_TARGETS.join(', ')}`,
288
- subcommands: ['drain'],
289
- },
290
- {
291
- name: 'dry-run',
292
- type: 'boolean',
293
- summary: 'drain: report the plan, move nothing',
294
- subcommands: ['drain'],
295
- },
296
- ],
297
- },
250
+ spec: jobsSpec,
298
251
  async run(ctx: CommandContext): Promise<CommandResult> {
299
252
  const root = requireAppRoot('jobs', ctx.cwd).dir;
300
253
  const sub = ctx.args.subcommand ?? 'ls';
@@ -0,0 +1,15 @@
1
+ // `x manifest`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-manifest.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const manifestSpec: CommandSpec = {
7
+ name: 'manifest',
8
+ summary: 'regenerate x.manifest.json and openapi.json from the code',
9
+ usage: 'x manifest [--check] [--json]',
10
+ requiresApp: true,
11
+ flags: [
12
+ { name: 'check', type: 'boolean', summary: 'fail if the committed files are stale' },
13
+ { name: 'openapi', type: 'boolean', summary: 'also write openapi.json', default: true },
14
+ ],
15
+ };
@@ -5,9 +5,11 @@
5
5
  import { join } from 'node:path';
6
6
  import type { Manifest } from '@ultimat3/manifest';
7
7
  import { assertNoDrift, MANIFEST_FILENAME } from '@ultimat3/manifest';
8
- import { appManifest, writeAppManifest } from './app-manifest';
9
- import { OPENAPI_FILE, openApiJson } from './app-openapi';
8
+ import { writeAppArtifacts } from './app-artifacts';
9
+ import { appManifest } from './app-manifest';
10
+ import { openApiStaleness } from './app-openapi';
10
11
  import { requireAppRoot } from './app-root';
12
+ import { manifestSpec } from './cmd-manifest-spec';
11
13
  import type { CliCommand, CommandContext } from './command';
12
14
  import { msg } from './messages';
13
15
  import type { CommandResult, Finding, JsonValue } from './output';
@@ -41,28 +43,24 @@ async function staleness(root: string, manifest: Manifest): Promise<Finding | un
41
43
  }
42
44
 
43
45
  export const manifestCommand: CliCommand = {
44
- spec: {
45
- name: 'manifest',
46
- summary: 'regenerate x.manifest.json and openapi.json from the code',
47
- usage: 'x manifest [--check] [--json]',
48
- requiresApp: true,
49
- flags: [
50
- { name: 'check', type: 'boolean', summary: 'fail if the committed files are stale' },
51
- { name: 'openapi', type: 'boolean', summary: 'also write openapi.json', default: true },
52
- ],
53
- },
46
+ spec: manifestSpec,
54
47
  async run(ctx: CommandContext): Promise<CommandResult> {
55
48
  const root = requireAppRoot('manifest', ctx.cwd).dir;
56
49
  const { manifest, findings } = await appManifest(root);
57
50
  const counts = countsOf(manifest);
58
51
 
59
52
  if (flagBool(ctx.args, 'check')) {
60
- const stale = await staleness(root, manifest);
53
+ // BOTH files the command writes: `--check` compared only `x.manifest.json`, so a stale
54
+ // `openapi.json` — the one the typed client is generated from — read as fresh.
55
+ const stale = [
56
+ ...[await staleness(root, manifest)].filter((one) => one !== undefined),
57
+ ...(await openApiStaleness(root, manifest)),
58
+ ];
61
59
  return {
62
- ok: stale === undefined && findings.length === 0,
60
+ ok: stale.length === 0 && findings.length === 0,
63
61
  command: 'manifest',
64
- summary: stale === undefined ? msg('cli.manifest.fresh') : msg('cli.manifest.stale'),
65
- findings: stale === undefined ? findings : [...findings, stale],
62
+ summary: stale.length === 0 ? msg('cli.manifest.fresh') : msg('cli.manifest.stale'),
63
+ findings: [...findings, ...stale],
66
64
  data: { buildId: manifest.buildId, counts },
67
65
  };
68
66
  }
@@ -79,10 +77,11 @@ export const manifestCommand: CliCommand = {
79
77
  };
80
78
  }
81
79
 
82
- const path = await writeAppManifest(root, manifest);
83
- if (ctx.args.flags.get('openapi') !== false) {
84
- await Bun.write(join(root, OPENAPI_FILE), openApiJson(manifest));
85
- }
80
+ await writeAppArtifacts(root, manifest, {
81
+ openapi: ctx.args.flags.get('openapi') !== false,
82
+ onlyExisting: false,
83
+ });
84
+ const path = join(root, MANIFEST_FILENAME);
86
85
  return {
87
86
  ok: true,
88
87
  command: 'manifest',
@@ -0,0 +1,21 @@
1
+ // `x mcp`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-mcp.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const DEFAULT_PORT = 9229;
7
+
8
+ export const mcpSpec: CommandSpec = {
9
+ name: 'mcp',
10
+ summary: 'serve the dev tools: routes, schema, policies, db, queues, logs, tests, verify',
11
+ usage: 'x mcp tools | x mcp serve [--transport stdio|http] [--port 9229] [--json]',
12
+ requiresApp: true,
13
+ subcommands: ['serve', 'tools'],
14
+ // No default, deliberately: `wiki/CLI-Reference.md` already says to write `x mcp serve` rather
15
+ // than a bare `x mcp`, and the parser's old first-element guess made the bare form START A
16
+ // SERVER — the one thing a word typed by mistake must not do. Refusing enforces the guidance.
17
+ flags: [
18
+ { name: 'transport', type: 'string', summary: 'stdio | http', default: 'stdio' },
19
+ { name: 'port', type: 'string', summary: 'HTTP port', default: String(DEFAULT_PORT) },
20
+ ],
21
+ };
package/src/cmd-mcp.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { markListening, nanoid, timingSafeEqual } from '@ultimat3/core';
7
7
  import { mcpHttpRoute, serveStdio } from '@ultimat3/mcp';
8
8
  import { requireAppRoot } from './app-root';
9
+ import { DEFAULT_PORT, mcpSpec } from './cmd-mcp-spec';
9
10
  import type { CliCommand, CommandContext } from './command';
10
11
  import { BadFlagError } from './errors';
11
12
  import { intFlagOr, PORT_RANGE } from './flag-number';
@@ -16,7 +17,6 @@ import { msg } from './messages';
16
17
  import type { CommandResult } from './output';
17
18
  import { flagString } from './parse';
18
19
 
19
- const DEFAULT_PORT = 9229;
20
20
  const TRANSPORTS = ['stdio', 'http'] as const;
21
21
  type Transport = (typeof TRANSPORTS)[number];
22
22
 
@@ -156,20 +156,7 @@ const readPort = (ctx: CommandContext): number =>
156
156
  );
157
157
 
158
158
  export const mcpCommand: CliCommand = {
159
- spec: {
160
- name: 'mcp',
161
- summary: 'serve the dev tools: routes, schema, policies, db, queues, logs, tests, verify',
162
- usage: 'x mcp tools | x mcp serve [--transport stdio|http] [--port 9229] [--json]',
163
- requiresApp: true,
164
- subcommands: ['serve', 'tools'],
165
- // No default, deliberately: `wiki/CLI-Reference.md` already says to write `x mcp serve` rather
166
- // than a bare `x mcp`, and the parser's old first-element guess made the bare form START A
167
- // SERVER — the one thing a word typed by mistake must not do. Refusing enforces the guidance.
168
- flags: [
169
- { name: 'transport', type: 'string', summary: 'stdio | http', default: 'stdio' },
170
- { name: 'port', type: 'string', summary: 'HTTP port', default: String(DEFAULT_PORT) },
171
- ],
172
- },
159
+ spec: mcpSpec,
173
160
  async run(ctx: CommandContext): Promise<CommandResult> {
174
161
  const root = requireAppRoot('mcp', ctx.cwd).dir;
175
162
  const transport = flagString(ctx.args, 'transport') ?? 'stdio';
@@ -0,0 +1,33 @@
1
+ // `x new`'s declaration, apart from its body: the parser, `x help` and the `errors` step
2
+ // read it without loading `cmd-new.ts`, which `registry.ts` imports only when the command runs.
3
+
4
+ import type { CommandSpec } from './parse';
5
+
6
+ export const newSpec: CommandSpec = {
7
+ name: 'new',
8
+ summary: 'scaffold a new Ultimate monorepo that already runs',
9
+ // Every flag the table below declares, in the spelling that turns it off where the default is
10
+ // on: the usage line offered `--no-example` while the table listed `--example`, and a reader
11
+ // had to reconcile the two to answer "which one do I get if I type neither".
12
+ usage: 'x new <name> [--dir path] [--no-example] [--no-git] [--dry-run] [--force] [--json]',
13
+ flags: [
14
+ { name: 'dir', type: 'string', summary: 'parent directory (default: cwd)' },
15
+ {
16
+ // The summary carries the default and the negation because the page has to answer "which
17
+ // one do I get if I type neither": the usage line offered `--no-example`, this table said
18
+ // `--example`, and `default: true` is a field only `--json` renders. 136 files against 109.
19
+ name: 'example',
20
+ type: 'boolean',
21
+ summary: 'include the example feature slice (default: on; --no-example for an empty app/)',
22
+ default: true,
23
+ },
24
+ {
25
+ name: 'git',
26
+ type: 'boolean',
27
+ summary: 'git init and commit the scaffold (default: on; --no-git for a bare directory)',
28
+ default: true,
29
+ },
30
+ { name: 'dry-run', type: 'boolean', summary: 'print the file list, write nothing' },
31
+ { name: 'force', type: 'boolean', summary: 'write into a directory that already exists' },
32
+ ],
33
+ };