@ultimat3/cli 1.2.0 → 2.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 (138) hide show
  1. package/CLAUDE.md +724 -0
  2. package/README.md +41 -9
  3. package/package.json +25 -23
  4. package/src/api-routes.ts +16 -0
  5. package/src/app-auth.ts +32 -0
  6. package/src/app-entities.ts +18 -0
  7. package/src/app-env.ts +103 -0
  8. package/src/app-load.ts +20 -3
  9. package/src/bin.ts +4 -3
  10. package/src/budgets.ts +114 -9
  11. package/src/cmd-build.ts +69 -21
  12. package/src/cmd-db-branch.ts +215 -0
  13. package/src/cmd-db.ts +332 -155
  14. package/src/cmd-deploy.ts +59 -6
  15. package/src/cmd-dev.ts +83 -16
  16. package/src/cmd-docs.ts +167 -0
  17. package/src/cmd-doctor.ts +64 -9
  18. package/src/cmd-env.ts +95 -0
  19. package/src/cmd-errors.ts +33 -13
  20. package/src/cmd-fix.ts +5 -1
  21. package/src/cmd-generate.ts +146 -111
  22. package/src/cmd-help.ts +16 -5
  23. package/src/cmd-i18n.ts +2 -0
  24. package/src/cmd-jobs.ts +47 -33
  25. package/src/cmd-mcp.ts +11 -2
  26. package/src/cmd-new.ts +13 -7
  27. package/src/cmd-planned.ts +55 -10
  28. package/src/cmd-policy.ts +1 -0
  29. package/src/cmd-registries.ts +3 -0
  30. package/src/cmd-secrets.ts +368 -0
  31. package/src/cmd-tasks.ts +1 -0
  32. package/src/cmd-test.ts +17 -23
  33. package/src/cmd-verify.ts +177 -23
  34. package/src/db-backfill.ts +401 -0
  35. package/src/db-branch.ts +251 -0
  36. package/src/db-destructive.ts +29 -0
  37. package/src/db-finding.ts +28 -0
  38. package/src/db-generate.ts +112 -0
  39. package/src/db-snapshot.ts +24 -0
  40. package/src/dev-assets.ts +86 -20
  41. package/src/dev-cache.ts +122 -0
  42. package/src/dev-dashboard.ts +19 -4
  43. package/src/dev-hooks.ts +27 -2
  44. package/src/dev-n-plus-one.ts +191 -0
  45. package/src/dev-queue.ts +105 -19
  46. package/src/dev-render.ts +158 -26
  47. package/src/dev-roles-fixture.ts +67 -0
  48. package/src/dev-roles.ts +165 -78
  49. package/src/dev-runtime.ts +117 -40
  50. package/src/dev-services.ts +15 -0
  51. package/src/dev-storage.ts +245 -0
  52. package/src/dev-sync.ts +107 -0
  53. package/src/dev-traces.ts +11 -3
  54. package/src/dispatch.ts +4 -2
  55. package/src/document-styles.ts +54 -0
  56. package/src/drift.ts +37 -9
  57. package/src/error-catalog.ts +7 -18
  58. package/src/error-codes.ts +186 -0
  59. package/src/error-contract.ts +29 -7
  60. package/src/error-fixes.ts +114 -0
  61. package/src/errors.ts +201 -138
  62. package/src/fix-command.ts +268 -0
  63. package/src/flag-number.ts +56 -0
  64. package/src/framework-scope.ts +49 -0
  65. package/src/generate-kinds.ts +97 -0
  66. package/src/guards.ts +186 -0
  67. package/src/index.ts +84 -14
  68. package/src/island-bundle.ts +166 -0
  69. package/src/island-routes.ts +50 -0
  70. package/src/jobs-driver.ts +33 -0
  71. package/src/jobs-json.ts +24 -0
  72. package/src/jobs-report.ts +17 -4
  73. package/src/mcp-db-target.ts +52 -27
  74. package/src/mcp-errors.ts +120 -19
  75. package/src/mcp-host.ts +44 -25
  76. package/src/messages.ts +81 -2
  77. package/src/metrics-endpoint.ts +4 -3
  78. package/src/migrations.ts +37 -4
  79. package/src/otlp-export.ts +64 -0
  80. package/src/output.ts +46 -16
  81. package/src/parse.ts +41 -3
  82. package/src/policy-facts.ts +38 -6
  83. package/src/policy-fixture.ts +14 -7
  84. package/src/prerender.ts +111 -2
  85. package/src/registry.ts +21 -3
  86. package/src/runtime-overrides.ts +66 -0
  87. package/src/safe-url-label.ts +24 -0
  88. package/src/scaffold-fixture.ts +10 -0
  89. package/src/scaffold-typecheck.ts +16 -38
  90. package/src/serve.ts +170 -10
  91. package/src/source-files.ts +4 -0
  92. package/src/statement-loop.ts +74 -0
  93. package/src/style-csp.ts +18 -0
  94. package/src/sync-authenticator.ts +59 -0
  95. package/src/templates/action.ts +15 -30
  96. package/src/templates/admin-page.ts +103 -0
  97. package/src/templates/admin.ts +11 -7
  98. package/src/templates/backfill.ts +212 -0
  99. package/src/templates/entity.ts +72 -31
  100. package/src/templates/guard.ts +143 -0
  101. package/src/templates/index.ts +12 -1
  102. package/src/templates/island.ts +67 -0
  103. package/src/templates/job.ts +53 -13
  104. package/src/templates/naming.ts +17 -1
  105. package/src/templates/policy.ts +35 -28
  106. package/src/templates/query.ts +24 -5
  107. package/src/templates/resource.ts +19 -11
  108. package/src/templates/route.ts +90 -15
  109. package/src/templates/scaffold-app.ts +142 -45
  110. package/src/templates/scaffold-claude-agents.ts +149 -0
  111. package/src/templates/scaffold-claude-commands.ts +221 -0
  112. package/src/templates/scaffold-claude.ts +134 -0
  113. package/src/templates/scaffold-container.ts +46 -2
  114. package/src/templates/scaffold-db-package.ts +91 -0
  115. package/src/templates/scaffold-docs.ts +24 -5
  116. package/src/templates/scaffold-domain-package.ts +90 -0
  117. package/src/templates/scaffold-env.ts +87 -0
  118. package/src/templates/scaffold-i18n.ts +4 -1
  119. package/src/templates/scaffold-mcp-package.ts +49 -0
  120. package/src/templates/scaffold-package-shape.ts +25 -4
  121. package/src/templates/scaffold-repo.ts +116 -257
  122. package/src/templates/scaffold-roles.ts +68 -0
  123. package/src/templates/scaffold-ui-package.ts +56 -0
  124. package/src/templates/slice-foundation.ts +88 -0
  125. package/src/templates/wrap.ts +95 -0
  126. package/src/test-counts.ts +35 -0
  127. package/src/test-select.ts +30 -15
  128. package/src/test-shards.ts +21 -3
  129. package/src/test-workers.ts +47 -0
  130. package/src/ts-scan.ts +271 -13
  131. package/src/tsconfig-references.ts +78 -0
  132. package/src/verify-floor.ts +133 -0
  133. package/src/verify-step.ts +19 -0
  134. package/src/verify-test-run.ts +72 -0
  135. package/src/verify-tests.ts +160 -71
  136. package/src/version-loader.ts +20 -3
  137. package/src/workspace-checks.ts +87 -16
  138. package/src/write-line.ts +34 -0
@@ -1,12 +1,28 @@
1
- // The config half of what `x new` writes: the one config file, the tooling configs and the
2
- // workspace packages. Committed defaults only — a fresh clone boots with `x dev` and no env
3
- // scavenger hunt. The docs and shims live in scaffold-docs.ts, the container files in
4
- // scaffold-container.ts.
5
-
1
+ // The REPO ROOT half of what `x new` writes: `app.config.ts`, the tooling configs, the committed
2
+ // env files — and the one list that names every other scaffold module in write order. Committed
3
+ // defaults only, so a fresh clone boots with `x dev` and no env scavenger hunt. Each workspace
4
+ // package owns its own files (`scaffold-<name>-package.ts`); docs and shims are scaffold-docs.ts,
5
+ // container files scaffold-container.ts.
6
+
7
+ import { ENV_EXAMPLE_PATH } from '@ultimat3/core';
8
+ import { VERIFY_FLOOR_FILE } from '../verify-floor';
9
+ import type { VerifyStepName } from '../verify-step';
6
10
  import type { GeneratedFile, NameSet } from './naming';
11
+ import { dbPackageFiles } from './scaffold-db-package';
7
12
  import { docsFiles } from './scaffold-docs';
13
+ import { domainPackageFiles } from './scaffold-domain-package';
14
+ import { envExampleSource, envSchemaSource } from './scaffold-env';
8
15
  import { i18nFiles } from './scaffold-i18n';
9
- import { packageShapeFiles } from './scaffold-package-shape';
16
+ import { mcpPackageFiles } from './scaffold-mcp-package';
17
+ import { uiPackageFiles } from './scaffold-ui-package';
18
+
19
+ /**
20
+ * Spelled once and pinned EXACTLY, because two places named it and a caret let them disagree:
21
+ * `"^2.4.15"` beside a `$schema` of `2.4.15` installed 2.5.8, whose own parser then reported the
22
+ * config as out of date on every `bun run lint`. A formatter is a build input — a range that floats
23
+ * is a `lint` step whose verdict depends on the day the app was installed.
24
+ */
25
+ const BIOME_VERSION = '2.5.8';
10
26
 
11
27
  // `version` is not decoration: the manifest's app version IS the contract's compatibility gate,
12
28
  // and the manifest never fabricates one — so an app scaffolded without it failed `x manifest`,
@@ -32,7 +48,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
32
48
  "db:seed": "bun run packages/db/src/seed.ts"
33
49
  },
34
50
  "devDependencies": {
35
- "@biomejs/biome": "^2.4.15",
51
+ "@biomejs/biome": "${BIOME_VERSION}",
36
52
  "@electric-sql/pglite": "^0.5.4",
37
53
  "@types/bun": "^1.3.14",
38
54
  "@ultimat3/testing": "^${version}",
@@ -40,6 +56,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
40
56
  },
41
57
  "dependencies": {
42
58
  "@ultimat3/action": "^${version}",
59
+ "@ultimat3/admin": "^${version}",
43
60
  "@ultimat3/cache": "^${version}",
44
61
  "@ultimat3/cli": "^${version}",
45
62
  "@ultimat3/core": "^${version}",
@@ -52,8 +69,9 @@ const rootPackage = (app: NameSet, version: string): string => `{
52
69
  "@ultimat3/pwa": "^${version}",
53
70
  "@ultimat3/query": "^${version}",
54
71
  "@ultimat3/render": "^${version}",
72
+ "@ultimat3/schema": "^${version}",
55
73
  "@ultimat3/ui": "^${version}",
56
- "solid-js": "2.0.0-experimental.16"
74
+ "solid-js": "1.9.14"
57
75
  },
58
76
  "engines": {
59
77
  "bun": ">=1.3.0"
@@ -88,12 +106,29 @@ const rootTsconfig = (app: NameSet): string => `{
88
106
  }
89
107
  `;
90
108
 
109
+ /**
110
+ * The env half of `app.config.ts`, projected from `SCAFFOLD_ENV_SCHEMA` — never typed out here.
111
+ * `envSchema` is a named export on purpose and not an inline argument: `defineEnv()` returns the
112
+ * resolved VALUES, so an inline record is unreachable afterwards, and `.env.example`, `x env
113
+ * check` and the gate's drift check are all projections of the record rather than of the values.
114
+ */
115
+ const envDeclaration = (): string => `${envSchemaSource()}
116
+
117
+ /**
118
+ * Validated once, at module scope, before anything listens: a missing or malformed key fails the
119
+ * boot in ~40ms naming every offender at once, never as a 500 an hour later.
120
+ */
121
+ export const env = defineEnv(envSchema);`;
122
+
91
123
  const appConfig = (
92
124
  app: NameSet,
93
125
  ): string => `// The one config file. Everything the app needs to boot is here, typed and validated at startup —
94
126
  // a missing value fails the boot with the exact command that fixes it, never at the first request.
95
127
  // A named export, never a default: the CLI and the runtime both import \`config\` by name.
96
- import { defineConfig } from '@ultimat3/core';
128
+ import type { EnvSchema } from '@ultimat3/core';
129
+ import { defineConfig, defineEnv } from '@ultimat3/core';
130
+
131
+ ${envDeclaration()}
97
132
 
98
133
  export const config = defineConfig({
99
134
  name: '${app.kebab}',
@@ -101,8 +136,8 @@ export const config = defineConfig({
101
136
  defaultLocale: 'en',
102
137
  defaultTimeZone: 'UTC',
103
138
  defaultCurrency: 'USD',
104
- // Env KEYS, never the value: the same image deploys to every environment.
105
- database: { urlEnv: 'DATABASE_URL', poolSize: 10 },
139
+ // Env KEYS, never the value: the same image deploys to every environment. The database is
140
+ // configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
106
141
  cache: { driver: 'memory', tiers: ['memo', 'lru'] },
107
142
  jobs: { driver: 'postgres', queues: ['${app.kebab}-default'], concurrency: 4 },
108
143
  // In-process transport by default; set urlEnv and transport: 'nats' to scale past one node.
@@ -116,14 +151,21 @@ export const config = defineConfig({
116
151
  // scaffolded app fail its first `x verify` on the config rather than on the code. The note that
117
152
  // used to be a comment lives here, where it is read by the person who would have changed the line:
118
153
  // x.manifest.json and openapi.json are emitted byte-for-byte by `x manifest`, so a formatter
119
- // rewriting them puts `x manifest` and `x verify` in a loop neither can win.
154
+ // rewriting them puts `x manifest` and `x verify` in a loop neither can win. `**/migrations` is the
155
+ // same rule for the same reason and the same glob this repo's own biome.json carries: `x db gen`
156
+ // writes the `.sql` and its `.snapshot.json` sidecar, and an app that narrows `lineWidth` would
157
+ // otherwise fail `lint` on a file no author typed and `x db gen` would rewrite anyway.
158
+ // `preset`, not `recommended`: the older key is deprecated from 2.5 on and every `bun run lint`
159
+ // in the scaffolded app printed the migration notice for a config the app never wrote by hand.
120
160
  const biome = (): string => `{
121
- "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
122
- "files": { "includes": ["**", "!x.manifest.json", "!openapi.json"] },
161
+ "$schema": "https://biomejs.dev/schemas/${BIOME_VERSION}/schema.json",
162
+ "files": {
163
+ "includes": ["**", "!**/migrations", "!x.manifest.json", "!openapi.json"]
164
+ },
123
165
  "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 100 },
124
166
  "linter": {
125
167
  "rules": {
126
- "recommended": true,
168
+ "preset": "recommended",
127
169
  "suspicious": { "noExplicitAny": "error" },
128
170
  "correctness": { "noUnusedVariables": "error", "noUnusedImports": "error" }
129
171
  }
@@ -134,6 +176,41 @@ const biome = (): string => `{
134
176
  }
135
177
  `;
136
178
 
179
+ /**
180
+ * The suite ratchet, committed on day one. Without it `readVerifyFloor` answers "no file is no
181
+ * floor" and a deleted suite turns its step from green into skipped-and-green — so
182
+ * `X_VERIFY_SUITE_VANISHED` was unreachable in every generated app, in the one repo shape that
183
+ * grows suites fastest.
184
+ *
185
+ * Every name here is a step this scaffold has proved it can run: the SIX that declare no `applies`
186
+ * at all — typecheck, lint, boundaries, filesize, errors, manifest — plus `package-shape` (five
187
+ * workspace packages), `unit` (every generator emits a `<file>.test.ts`; like every suite step it
188
+ * applies on its own file list, `verify-tests.ts`), and `eval`, `drift` and `budgets`, which apply
189
+ * to any root with an `app.config.ts`. Typed as `VerifyStepName`, so a name the gate does not run
190
+ * is a compile error rather than a floor that covers nothing.
191
+ *
192
+ * Four are deliberately absent. `contract`, `live` and `job` have no scaffolded file; `e2e` has
193
+ * one, and it is an `e2eTest` — `test.skip` until the app registers a browser driver, so the step
194
+ * would run zero tests and fail the ratchet on the scaffold's own placeholder. `contract-diff`
195
+ * needs a committed `x.manifest.json`, which `x manifest` writes later. Each joins the list in the
196
+ * commit that makes the app's own gate run it.
197
+ */
198
+ const SCAFFOLD_FLOOR: readonly VerifyStepName[] = [
199
+ 'typecheck',
200
+ 'lint',
201
+ 'boundaries',
202
+ 'filesize',
203
+ 'package-shape',
204
+ 'errors',
205
+ 'unit',
206
+ 'eval',
207
+ 'drift',
208
+ 'budgets',
209
+ 'manifest',
210
+ ];
211
+
212
+ const verifyFloor = (): string => `${JSON.stringify({ steps: SCAFFOLD_FLOOR }, null, 2)}\n`;
213
+
137
214
  const bunfig = (): string => `[test]
138
215
  root = "."
139
216
  # Frozen clock, seeded RNG, sealed network — nondeterminism in a test is a bug.
@@ -148,6 +225,14 @@ declare module '*.module.scss' {
148
225
  const classes: Readonly<Record<string, string>>;
149
226
  export default classes;
150
227
  }
228
+
229
+ // A plain stylesheet is the global layer: it emits top-level CSS and has no class map worth
230
+ // binding, so \`shared/global.ts\` imports it for the side effect alone. Without this declaration
231
+ // \`tsc\` reports TS2307 on the one import that puts the app's tokens in the document.
232
+ declare module '*.scss' {
233
+ const classes: Readonly<Record<string, string>>;
234
+ export default classes;
235
+ }
151
236
  `;
152
237
 
153
238
  const gitignore = (): string => `node_modules/
@@ -161,226 +246,19 @@ playwright-report/
161
246
  test-results/
162
247
  `;
163
248
 
249
+ // Values, not declarations — the declaration is `envSchema` and `.env.example` is its projection.
250
+ // Every key here is one `envSchema` declares, plus `ROLE`, which `@ultimat3/core` reads directly
251
+ // (`roles.ts`) and no app schema may redeclare.
164
252
  const envDevelopment =
165
253
  (): string => `# Committed non-secret defaults. Per-box secrets go in .env.development.local, which wins.
166
254
  # Empty DATABASE_URL means "embedded": x dev runs PGlite in-process, no Docker required.
167
255
  DATABASE_URL=
168
256
  NATS_URL=
169
- S3_ENDPOINT=
170
257
  PORT=3000
258
+ SESSION_SECRET=dev-only-not-a-real-secret
171
259
  ROLE=web
172
260
  `;
173
261
 
174
- const domainPackage = (app: NameSet, name: string, description: string): string => `{
175
- "name": "@${app.kebab}/${name}",
176
- "version": "0.0.0",
177
- "private": true,
178
- "type": "module",
179
- "description": "${description}",
180
- "exports": {
181
- ".": "./src/index.ts"
182
- },
183
- "scripts": {
184
- "typecheck": "tsc --noEmit -p ../../tsconfig.json"
185
- }
186
- }
187
- `;
188
-
189
- const domainIndex =
190
- (): string => `// Pure types and constants. No I/O of any kind: no fs, no network, no database, no env reads.
191
- export const ROLES = ['owner', 'member', 'viewer'] as const;
192
-
193
- export type Role = (typeof ROLES)[number];
194
-
195
- export interface Money {
196
- readonly minor: number;
197
- readonly currency: string;
198
- }
199
-
200
- export const zero = (currency: string): Money => ({ minor: 0, currency });
201
-
202
- export const add = (a: Money, b: Money): Money => {
203
- if (a.currency !== b.currency) throw new RangeError(\`cannot add \${a.currency} to \${b.currency}\`);
204
- return { minor: a.minor + b.minor, currency: a.currency };
205
- };
206
- `;
207
-
208
- const domainTest = (): string => `import { expect } from 'bun:test';
209
- import { unitTest } from '@ultimat3/testing';
210
- import { add, zero } from './index';
211
-
212
- unitTest('money adds in minor units', () => {
213
- expect(add({ minor: 1050, currency: 'USD' }, { minor: 250, currency: 'USD' })).toEqual({
214
- minor: 1300,
215
- currency: 'USD',
216
- });
217
- });
218
-
219
- unitTest('money refuses to add across currencies', () => {
220
- expect(() => add(zero('USD'), zero('EUR'))).toThrow();
221
- });
222
- `;
223
-
224
- const dbIndex =
225
- (): string => `// Schema and migrations only — no business logic lives in this package. The client itself is
226
- // @ultimat3/db's: one connection pool, sized by ROLE, shared by every package in the app.
227
- export type { DbClient, SqlFragment } from '@ultimat3/db';
228
- export { db, sql, withTransaction } from '@ultimat3/db';
229
- export * as schema from './schema';
230
- `;
231
-
232
- // The four pieces below describe the example slice's table. Under `--no-example` that slice is
233
- // never written, so each one ships its empty counterpart instead of a reference to a file that is
234
- // not there — `export { post } from …` alone made `x new --no-example` an app that cannot compile.
235
-
236
- const SCHEMA_HEADER = `// Every entity the app declares, re-exported here. This list is what the migration generator
237
- // reads, so an entity that is not exported here does not exist as far as the database is concerned.`;
238
-
239
- /**
240
- * `bun run db:seed`'s entry point. Identical either way — only the rows differ. Interpolated, not
241
- * nested, so it carries exactly the escaping a single template literal needs.
242
- */
243
- const SEED_MAIN = `
244
-
245
- if (import.meta.main) {
246
- const count = await seed();
247
- // Bun's stdout, not process.stdout: one runtime, one API. Awaited because the write resolves
248
- // asynchronously, and this JSON line is the whole output of \`bun run db:seed\`.
249
- await Bun.stdout.write(\`\${JSON.stringify({ ok: true, seeded: count })}\\n\`);
250
- }
251
- `;
252
-
253
- const dbSchema = (app: NameSet, example: boolean): string =>
254
- example
255
- ? `${SCHEMA_HEADER}
256
- export { post } from '@${app.kebab}/web/app/post/entity';
257
- `
258
- : `${SCHEMA_HEADER}
259
- // \`x g entity <name>\` writes the entity; add its export here so the database learns about it.
260
- export {};
261
- `;
262
-
263
- const dbSeed = (app: NameSet, example: boolean): string =>
264
- example
265
- ? `// Deterministic seed: same rows every time, so a test and a demo see the same database.
266
- import { db, sql } from '@ultimat3/db';
267
-
268
- const ORG = '00000000-0000-0000-0000-000000000002';
269
-
270
- export async function seed(): Promise<number> {
271
- const rows = [
272
- { id: '00000000-0000-0000-0000-000000000101', title: 'Hello ${app.pascal}', minor: 0 },
273
- { id: '00000000-0000-0000-0000-000000000102', title: 'Second post', minor: 1900 },
274
- ];
275
- for (const row of rows) {
276
- // Idempotent by primary key, so re-seeding a branch database is a no-op rather than a crash.
277
- await db().execute(sql\`
278
- insert into posts (id, org_id, title, price_minor, price_currency)
279
- values (\${row.id}, \${ORG}, \${row.title}, \${row.minor}, 'USD')
280
- on conflict (id) do nothing\`);
281
- }
282
- return rows.length;
283
- }${SEED_MAIN}`
284
- : `// Deterministic seed: same rows every time, so a test and a demo see the same database.
285
- // No entity is declared yet, so there is nothing to insert — the shape stays, so the first
286
- // \`x g entity\` has one obvious place to seed from.
287
-
288
- export async function seed(): Promise<number> {
289
- return 0;
290
- }${SEED_MAIN}`;
291
-
292
- const migration = (example: boolean): string =>
293
- example
294
- ? `-- 0000_initial: the example feature slice. Reversible: the down section is required.
295
- CREATE TABLE IF NOT EXISTS posts (
296
- id uuid PRIMARY KEY,
297
- org_id uuid NOT NULL,
298
- title varchar(200) NOT NULL,
299
- price_minor integer NOT NULL DEFAULT 0,
300
- price_currency char(3) NOT NULL DEFAULT 'USD',
301
- created_at timestamptz NOT NULL DEFAULT now()
302
- );
303
- CREATE INDEX IF NOT EXISTS posts_org_created_idx ON posts (org_id, created_at);
304
-
305
- -- down
306
- -- DROP INDEX IF EXISTS posts_org_created_idx;
307
- -- DROP TABLE IF EXISTS posts;
308
- `
309
- : `-- 0000_initial: no entity is declared yet, so this migration creates nothing. It exists so the
310
- -- schema hash beside it has a migration to belong to, and \`x verify\` sees no drift on run one.
311
- -- Reversible: the down section is required.
312
-
313
- -- down
314
- `;
315
-
316
- const uiIndex =
317
- (): string => `// App components on top of @ultimat3/ui. Same byte budgets as shared/: this package is imported
318
- // by site/, so a chart library in here costs the landing page.
319
- export { Card } from './card';
320
- `;
321
-
322
- const uiCard = (): string => `import type { JSX } from 'solid-js';
323
- import styles from './card.module.scss';
324
-
325
- export interface CardProps {
326
- readonly title: string;
327
- readonly children?: JSX.Element;
328
- }
329
-
330
- export function Card(props: CardProps) {
331
- return (
332
- <section class={styles.card}>
333
- <h2 class={styles.title}>{props.title}</h2>
334
- {props.children}
335
- </section>
336
- );
337
- }
338
- `;
339
-
340
- const uiCardStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
341
-
342
- .card {
343
- padding: tokens.$space-4;
344
- border-radius: tokens.$radius-md;
345
- background: tokens.$surface-raised;
346
- color: tokens.$text-primary;
347
- }
348
-
349
- .title {
350
- font: tokens.$text-heading-sm;
351
- }
352
- `;
353
-
354
- const mcpIndex = (
355
- app: NameSet,
356
- ): string => `// The app's own MCP tools. Every action with mcp.expose is already a tool; add app-specific
357
- // read-only helpers here. Authorization is the action's policy, unchanged.
358
- import * as api from '@${app.kebab}/web/api/health';
359
- import { registerActions } from '@ultimat3/action';
360
- import { defineAppMcp } from '@ultimat3/mcp';
361
-
362
- // Names come from export names, so the registry agrees with the module the app already wrote.
363
- registerActions(api);
364
-
365
- // \`include: 'exposed'\` projects straight from the registry. Re-listing the actions here would
366
- // copy \`mcp: { expose: true }\` into a second place, and the copy goes stale in silence.
367
- export const mcp = defineAppMcp({
368
- name: '${app.kebab}',
369
- include: 'exposed',
370
- });
371
- `;
372
-
373
- const mcpTest = (): string => `import { expect, unitTest } from '@ultimat3/testing';
374
- import { mcp } from './index';
375
-
376
- unitTest('the app exposes its actions as MCP tools', () => {
377
- expect(mcp.tools.length).toBeGreaterThan(0);
378
- // Every projected tool must describe itself: an agent picks a tool by its description. Assert
379
- // on the value, not its length — a failure then prints the empty description, not "0 > 0".
380
- for (const tool of mcp.tools) expect(tool.description).not.toBe('');
381
- });
382
- `;
383
-
384
262
  /**
385
263
  * `example` reaches only the four files that describe the slice's table — schema, seed, initial
386
264
  * migration, and nothing in the catalog. Everything else is the same app either way, which is what
@@ -398,40 +276,21 @@ export function repoFiles(
398
276
  { path: 'biome.json', contents: biome() },
399
277
  { path: 'bunfig.toml', contents: bunfig() },
400
278
  { path: 'app.config.ts', contents: appConfig(app) },
279
+ { path: VERIFY_FLOOR_FILE, contents: verifyFloor() },
401
280
  { path: 'types/scss.d.ts', contents: scssTypes() },
402
281
  { path: '.gitignore', contents: gitignore() },
403
282
  { path: '.env.development', contents: envDevelopment() },
404
- {
405
- path: 'packages/domain/package.json',
406
- contents: domainPackage(app, 'domain', 'Pure types and constants, no I/O'),
407
- },
408
- ...packageShapeFiles(app, 'domain', 'Pure types and constants, no I/O'),
409
- { path: 'packages/domain/src/index.ts', contents: domainIndex() },
410
- { path: 'packages/domain/src/index.test.ts', contents: domainTest() },
411
- {
412
- path: 'packages/db/package.json',
413
- contents: domainPackage(app, 'db', 'Entity re-exports and SQL migrations, no business logic'),
414
- },
415
- ...packageShapeFiles(app, 'db', 'Entity re-exports and SQL migrations, no business logic'),
416
- { path: 'packages/db/src/index.ts', contents: dbIndex() },
417
- { path: 'packages/db/src/schema.ts', contents: dbSchema(app, example) },
418
- { path: 'packages/db/src/seed.ts', contents: dbSeed(app, example) },
419
- { path: 'packages/db/migrations/0000_initial.sql', contents: migration(example) },
283
+ // Committed, and generated: `x env example` rewrites this file from `envSchema`, and the
284
+ // gate's `manifest` step fails with X_ENV_EXAMPLE_DRIFT when the two stop agreeing. A
285
+ // scaffold that shipped a hand-written one would fail its own first `x verify`.
286
+ { path: ENV_EXAMPLE_PATH, contents: envExampleSource() },
287
+ // One call per workspace package, in write order. Each owns its own files (`scaffold-i18n.ts`
288
+ // already did), so this list stays a table of contents rather than a second copy of every
289
+ // package's contents.
290
+ ...domainPackageFiles(app),
291
+ ...dbPackageFiles(app, example),
420
292
  ...i18nFiles(app, version),
421
- {
422
- path: 'packages/ui/package.json',
423
- contents: domainPackage(app, 'ui', 'App components on @ultimat3/ui'),
424
- },
425
- ...packageShapeFiles(app, 'ui', 'App components on @ultimat3/ui'),
426
- { path: 'packages/ui/src/index.ts', contents: uiIndex() },
427
- { path: 'packages/ui/src/card.tsx', contents: uiCard() },
428
- { path: 'packages/ui/src/card.module.scss', contents: uiCardStyle() },
429
- {
430
- path: 'packages/mcp/package.json',
431
- contents: domainPackage(app, 'mcp', "The app's own MCP tools"),
432
- },
433
- ...packageShapeFiles(app, 'mcp', "The app's own MCP tools"),
434
- { path: 'packages/mcp/src/index.ts', contents: mcpIndex(app) },
435
- { path: 'packages/mcp/src/index.test.ts', contents: mcpTest() },
293
+ ...uiPackageFiles(app),
294
+ ...mcpPackageFiles(app),
436
295
  ];
437
296
  }
@@ -0,0 +1,68 @@
1
+ // The one place a scaffolded app's roles live, decided rather than left to each feature to invent.
2
+ // `defineRoles()` MERGES, so a second call in a feature folder is legal and silent — which is
3
+ // exactly why the location has to ship: without a scaffolded file, "where do roles live?" has as
4
+ // many answers as the app has folders, and the framework's two tracked apps already disagree.
5
+
6
+ import type { GeneratedFile } from './naming';
7
+
8
+ const rolesSource =
9
+ (): string => `// Who holds which permission, for the whole app. Roles are sugar: every one expands to a flat
10
+ // permission set before any policy runs, so a rule never reasons about the hierarchy.
11
+ //
12
+ // ONE file, and it lives in shared/ — the leaf both site/ and app/ already import, and the one the
13
+ // boot scan loads, so the map is filled before the first request. \`defineRoles()\` merges into that
14
+ // map rather than replacing it, and refuses a role two modules define differently
15
+ // (X_ROLE_REDEFINED, naming both declaration sites). A feature that needs a new grant adds it to a
16
+ // role HERE; calling defineRoles() again from a feature folder works and is the drift this file
17
+ // exists to prevent.
18
+ //
19
+ // \`x g policy <feature>\` declares \`<feature>:read\` and \`<feature>:write\`. Granting them is this
20
+ // file's job — a permission no role holds is one no actor can ever exercise.
21
+
22
+ import { defineRoles } from '@ultimat3/policy';
23
+
24
+ export const roles = defineRoles({
25
+ member: {
26
+ description: 'Signed in. Reads the app surface.',
27
+ grants: ['dashboard:read'],
28
+ },
29
+ admin: {
30
+ description: 'Runs the app: the /admin surface, plus everything a member may do.',
31
+ grants: ['admin:read'],
32
+ inherits: ['member'],
33
+ },
34
+ });
35
+ `;
36
+
37
+ const rolesTest =
38
+ (): string => `// The app's role map, expanded: what each role grants once inheritance is flattened, and which
39
+ // roles hold a given permission. An undeclared role must grant nothing at all.
40
+ import { expandRoles, rolesGranting } from '@ultimat3/policy';
41
+ import { expect, unitTest } from '@ultimat3/testing';
42
+ import { roles } from './roles';
43
+
44
+ // The map is passed explicitly rather than read off the module-global one: a test that depended on
45
+ // which module imported first would pass alone and fail inside a suite.
46
+
47
+ unitTest('admin inherits every member grant and adds its own', () => {
48
+ expect(expandRoles(['member'], roles)).toEqual(['dashboard:read']);
49
+ expect(expandRoles(['admin'], roles)).toEqual(['admin:read', 'dashboard:read']);
50
+ });
51
+
52
+ unitTest('a role nobody declared grants nothing', () => {
53
+ expect(expandRoles(['visitor'], roles)).toEqual([]);
54
+ });
55
+
56
+ unitTest('every permission the app enforces is held by some role', () => {
57
+ expect(rolesGranting('dashboard:read', roles)).toEqual(['admin', 'member']);
58
+ expect(rolesGranting('admin:read', roles)).toEqual(['admin']);
59
+ });
60
+ `;
61
+
62
+ /** `apps/web/shared/roles.ts` and its test. Written by `x new`, with or without the example slice. */
63
+ export function rolesFiles(): readonly GeneratedFile[] {
64
+ return [
65
+ { path: 'apps/web/shared/roles.ts', contents: rolesSource() },
66
+ { path: 'apps/web/shared/roles.test.ts', contents: rolesTest() },
67
+ ];
68
+ }
@@ -0,0 +1,56 @@
1
+ // The generated app's `packages/ui`: one example component on top of @ultimat3/ui, so the app has
2
+ // a worked instance of the semantic-token rule before it writes its own. Imported by `site/`, so
3
+ // its byte budget is the landing page's.
4
+
5
+ import type { GeneratedFile, NameSet } from './naming';
6
+ import { packageShapeFiles, workspacePackageJson } from './scaffold-package-shape';
7
+
8
+ const DESCRIPTION = 'App components on @ultimat3/ui';
9
+
10
+ const uiIndex =
11
+ (): string => `// App components on top of @ultimat3/ui. Same byte budgets as shared/: this package is imported
12
+ // by site/, so a chart library in here costs the landing page.
13
+ export { Card } from './card';
14
+ `;
15
+
16
+ const uiCard = (): string => `import type { JSX } from 'solid-js';
17
+ import styles from './card.module.scss';
18
+
19
+ export interface CardProps {
20
+ readonly title: string;
21
+ readonly children?: JSX.Element;
22
+ }
23
+
24
+ export function Card(props: CardProps) {
25
+ return (
26
+ <section class={styles.card}>
27
+ <h2 class={styles.title}>{props.title}</h2>
28
+ {props.children}
29
+ </section>
30
+ );
31
+ }
32
+ `;
33
+
34
+ const uiCardStyle = (): string => `@use '@ultimat3/ui/tokens' as tokens;
35
+
36
+ .card {
37
+ padding: tokens.space(4);
38
+ border-radius: tokens.radius('md');
39
+ background: tokens.role('surface-raised');
40
+ color: tokens.role('fg');
41
+ }
42
+
43
+ .title {
44
+ font-size: tokens.text('lg');
45
+ font-weight: tokens.weight('semibold');
46
+ }
47
+ `;
48
+
49
+ /** Every file the `packages/ui` workspace ships, in the order `x new` writes them. */
50
+ export const uiPackageFiles = (app: NameSet): readonly GeneratedFile[] => [
51
+ { path: 'packages/ui/package.json', contents: workspacePackageJson(app, 'ui', DESCRIPTION) },
52
+ ...packageShapeFiles(app, 'ui', DESCRIPTION),
53
+ { path: 'packages/ui/src/index.ts', contents: uiIndex() },
54
+ { path: 'packages/ui/src/card.tsx', contents: uiCard() },
55
+ { path: 'packages/ui/src/card.module.scss', contents: uiCardStyle() },
56
+ ];