@ultimat3/cli 1.1.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.
- package/CLAUDE.md +724 -0
- package/README.md +41 -9
- package/package.json +25 -23
- package/src/api-routes.ts +16 -0
- package/src/app-auth.ts +32 -0
- package/src/app-entities.ts +18 -0
- package/src/app-env.ts +103 -0
- package/src/app-load.ts +20 -3
- package/src/bin.ts +4 -3
- package/src/budgets.ts +114 -9
- package/src/cmd-build.ts +69 -21
- package/src/cmd-db-branch.ts +215 -0
- package/src/cmd-db.ts +332 -155
- package/src/cmd-deploy.ts +59 -6
- package/src/cmd-dev.ts +87 -17
- package/src/cmd-docs.ts +167 -0
- package/src/cmd-doctor.ts +64 -9
- package/src/cmd-env.ts +95 -0
- package/src/cmd-errors.ts +33 -13
- package/src/cmd-fix.ts +5 -1
- package/src/cmd-generate.ts +146 -111
- package/src/cmd-help.ts +16 -5
- package/src/cmd-i18n.ts +2 -0
- package/src/cmd-jobs.ts +47 -33
- package/src/cmd-mcp.ts +11 -2
- package/src/cmd-new.ts +13 -7
- package/src/cmd-planned.ts +55 -10
- package/src/cmd-policy.ts +1 -0
- package/src/cmd-registries.ts +3 -0
- package/src/cmd-secrets.ts +368 -0
- package/src/cmd-tasks.ts +1 -0
- package/src/cmd-test.ts +17 -23
- package/src/cmd-verify.ts +177 -23
- package/src/db-backfill.ts +401 -0
- package/src/db-branch.ts +251 -0
- package/src/db-destructive.ts +29 -0
- package/src/db-finding.ts +28 -0
- package/src/db-generate.ts +112 -0
- package/src/db-snapshot.ts +24 -0
- package/src/dev-assets.ts +86 -20
- package/src/dev-cache.ts +122 -0
- package/src/dev-dashboard.ts +19 -4
- package/src/dev-hooks.ts +27 -2
- package/src/dev-n-plus-one.ts +191 -0
- package/src/dev-queue.ts +105 -19
- package/src/dev-render.ts +158 -26
- package/src/dev-roles-fixture.ts +67 -0
- package/src/dev-roles.ts +186 -78
- package/src/dev-runtime.ts +117 -40
- package/src/dev-services.ts +15 -0
- package/src/dev-storage.ts +245 -0
- package/src/dev-sync.ts +107 -0
- package/src/dev-traces.ts +11 -3
- package/src/dispatch.ts +4 -2
- package/src/document-styles.ts +54 -0
- package/src/drift.ts +37 -9
- package/src/error-catalog.ts +7 -18
- package/src/error-codes.ts +186 -0
- package/src/error-contract.ts +29 -7
- package/src/error-fixes.ts +114 -0
- package/src/errors.ts +205 -140
- package/src/fix-command.ts +268 -0
- package/src/flag-number.ts +56 -0
- package/src/framework-scope.ts +49 -0
- package/src/generate-kinds.ts +97 -0
- package/src/guards.ts +186 -0
- package/src/index.ts +87 -14
- package/src/island-bundle.ts +166 -0
- package/src/island-routes.ts +50 -0
- package/src/jobs-driver.ts +33 -0
- package/src/jobs-json.ts +24 -0
- package/src/jobs-report.ts +17 -4
- package/src/mcp-db-target.ts +52 -27
- package/src/mcp-errors.ts +120 -19
- package/src/mcp-host.ts +44 -25
- package/src/messages.ts +81 -2
- package/src/metrics-endpoint.ts +73 -0
- package/src/migrations.ts +37 -4
- package/src/otlp-export.ts +64 -0
- package/src/output.ts +46 -16
- package/src/parse.ts +41 -3
- package/src/policy-facts.ts +38 -6
- package/src/policy-fixture.ts +14 -7
- package/src/prerender.ts +111 -2
- package/src/registry.ts +21 -3
- package/src/runtime-overrides.ts +66 -0
- package/src/safe-url-label.ts +24 -0
- package/src/scaffold-fixture.ts +10 -0
- package/src/scaffold-typecheck.ts +16 -38
- package/src/serve.ts +202 -18
- package/src/source-files.ts +4 -0
- package/src/statement-loop.ts +74 -0
- package/src/style-csp.ts +18 -0
- package/src/sync-authenticator.ts +59 -0
- package/src/templates/action.ts +15 -30
- package/src/templates/admin-page.ts +103 -0
- package/src/templates/admin.ts +11 -7
- package/src/templates/backfill.ts +212 -0
- package/src/templates/entity.ts +72 -31
- package/src/templates/guard.ts +143 -0
- package/src/templates/index.ts +12 -1
- package/src/templates/island.ts +67 -0
- package/src/templates/job.ts +53 -13
- package/src/templates/naming.ts +17 -1
- package/src/templates/policy.ts +35 -28
- package/src/templates/query.ts +24 -5
- package/src/templates/resource.ts +19 -11
- package/src/templates/route.ts +90 -15
- package/src/templates/scaffold-app.ts +142 -45
- package/src/templates/scaffold-claude-agents.ts +149 -0
- package/src/templates/scaffold-claude-commands.ts +221 -0
- package/src/templates/scaffold-claude.ts +134 -0
- package/src/templates/scaffold-container.ts +46 -2
- package/src/templates/scaffold-db-package.ts +91 -0
- package/src/templates/scaffold-docs.ts +24 -5
- package/src/templates/scaffold-domain-package.ts +90 -0
- package/src/templates/scaffold-env.ts +87 -0
- package/src/templates/scaffold-i18n.ts +4 -1
- package/src/templates/scaffold-mcp-package.ts +49 -0
- package/src/templates/scaffold-package-shape.ts +25 -4
- package/src/templates/scaffold-repo.ts +116 -257
- package/src/templates/scaffold-roles.ts +68 -0
- package/src/templates/scaffold-ui-package.ts +56 -0
- package/src/templates/slice-foundation.ts +88 -0
- package/src/templates/wrap.ts +95 -0
- package/src/test-counts.ts +35 -0
- package/src/test-select.ts +30 -15
- package/src/test-shards.ts +21 -3
- package/src/test-workers.ts +47 -0
- package/src/ts-scan.ts +271 -13
- package/src/tsconfig-references.ts +78 -0
- package/src/verify-floor.ts +133 -0
- package/src/verify-step.ts +19 -0
- package/src/verify-test-run.ts +72 -0
- package/src/verify-tests.ts +160 -71
- package/src/version-loader.ts +20 -3
- package/src/workspace-checks.ts +87 -16
- package/src/write-line.ts +34 -0
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// scaffold-
|
|
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 {
|
|
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": "
|
|
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": "
|
|
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 {
|
|
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
|
-
|
|
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/
|
|
122
|
-
"files": {
|
|
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
|
-
"
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
},
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
+
];
|