@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// The slash commands `x new` writes into `.claude/commands/`: the three workflows every audited
|
|
2
|
+
// repo re-invented by hand — build a feature, plan one, run the gate. Deliberately NOT one command
|
|
3
|
+
// per generator: `x g <kind>` already is that command, and a slash wrapper over a shipped CLI
|
|
4
|
+
// command is the second path axiom 1 forbids.
|
|
5
|
+
|
|
6
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
7
|
+
|
|
8
|
+
const feature = (app: NameSet): string => `---
|
|
9
|
+
description: Build or fix one thing in ${app.kebab} end to end — name the primitive, generate it, wire it inside the boundaries, gate it with \`x verify\`.
|
|
10
|
+
argument-hint: <what you want built or fixed, plain language>
|
|
11
|
+
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, Skill
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# /feature
|
|
15
|
+
|
|
16
|
+
You are a senior engineer on **${app.kebab}**, an Ultimate app. Read \`AGENTS.md\` before designing
|
|
17
|
+
anything — it is the short form of every rule below, and it wins where the two disagree.
|
|
18
|
+
|
|
19
|
+
**Done means \`x verify\` green.** A passing unit test is not done. A working \`x dev\` is not done.
|
|
20
|
+
Report what you actually ran, never what you assume passed.
|
|
21
|
+
|
|
22
|
+
## Request
|
|
23
|
+
$ARGUMENTS
|
|
24
|
+
|
|
25
|
+
**The prompt is the context.** Scope, autonomy, whether to commit: read it from the words. Stop for
|
|
26
|
+
a real blocker — a destructive migration, a request that needs a ninth primitive, a dependency you
|
|
27
|
+
cannot justify. You cannot ask a subagent's user anything, so decide and flag it, or stop and say
|
|
28
|
+
why.
|
|
29
|
+
|
|
30
|
+
## 1. Name the primitive first
|
|
31
|
+
|
|
32
|
+
Eight, closed: \`entity\` · \`policy\` · \`action\` · \`mutator\` · \`query\` · \`job\` · \`route\` · \`task\`.
|
|
33
|
+
|
|
34
|
+
| The ask sounds like | It is |
|
|
35
|
+
|---|---|
|
|
36
|
+
| "store a …", "a … has fields" | \`entity\` |
|
|
37
|
+
| "only the owner may …" | \`policy\` |
|
|
38
|
+
| "when the user clicks submit …" | \`action\` (writes) / \`mutator\` (writes one entity) |
|
|
39
|
+
| "show me the list of …" | \`query\` |
|
|
40
|
+
| "send it afterwards", "retry until it works" | \`job\` |
|
|
41
|
+
| "a page at /…" | \`route\` |
|
|
42
|
+
| "every night at 3am" | \`task\` |
|
|
43
|
+
|
|
44
|
+
A request that fits none is not one feature — split it until every piece is one of the eight. There
|
|
45
|
+
is no ninth: a new capability is a **function that returns** one of these, never a new kind of thing.
|
|
46
|
+
Say which primitive and which slice out loud before you write a file. When the answer is not
|
|
47
|
+
obvious, hand the request to the \`shape\` subagent — that pass is the whole reason it exists.
|
|
48
|
+
|
|
49
|
+
## 2. Generate it — do not hand-write it
|
|
50
|
+
|
|
51
|
+
\`\`\`sh
|
|
52
|
+
x g <kind> <name> --feature <slice> # x g --help is the only list of kinds
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
The generator writes the source, its test and its i18n keys as one unit and registers it. A
|
|
56
|
+
hand-written primitive compiles and then goes missing from \`x routes\`, \`x actions\`, \`x queries\`,
|
|
57
|
+
\`x jobs\`, \`x tasks\`, \`x policy list\` and \`x.manifest.json\` — every surface that is supposed to
|
|
58
|
+
project it. Edit what the generator wrote; never reproduce it.
|
|
59
|
+
|
|
60
|
+
Schema changes are generated too: edit \`entity.ts\`, then \`x db gen "what changed"\`. Never hand-write
|
|
61
|
+
SQL into \`packages/db/migrations/\`.
|
|
62
|
+
|
|
63
|
+
## 3. Wire it inside the boundaries
|
|
64
|
+
|
|
65
|
+
| Boundary | The rule | What breaks without it |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| \`apps/web/site/\` | 0kb JS, may not import \`apps/web/app/\` | the static path pays the app's bundle |
|
|
68
|
+
| \`apps/web/app/\` | authed, streaming, hydrated | — |
|
|
69
|
+
| \`apps/web/api/\` | actions only, \`route.ts\` | a second HTTP surface |
|
|
70
|
+
| \`apps/web/shared/\` | a leaf: imports nothing of yours | an import cycle across surfaces |
|
|
71
|
+
| \`repo.ts\` | the only file that touches the database | authz bypassed by a raw read |
|
|
72
|
+
| routes | call actions and queries, never a repo | policy skipped |
|
|
73
|
+
|
|
74
|
+
Route files: \`page.tsx\` under \`site/\`/\`app/\`, \`route.ts\` under \`api/\`. **The directory is the URL** —
|
|
75
|
+
the filename never is. One interactive control on a 0kb page is an island: \`x g island <name> --at <dir>\`.
|
|
76
|
+
|
|
77
|
+
## 4. Gate it
|
|
78
|
+
|
|
79
|
+
\`\`\`sh
|
|
80
|
+
x verify # the gate. green = shippable
|
|
81
|
+
x verify --json # the same steps, machine-readable
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
Red is instructions, not a verdict: **every finding carries an executable \`fix:\` — run it verbatim
|
|
85
|
+
before improvising**, and \`x errors explain <CODE>\` expands any code it names. Never narrow the gate
|
|
86
|
+
to make it pass: there is no \`--only\` and no \`--skip\`, on purpose, and disabling a lint rule or
|
|
87
|
+
loosening a compiler flag is the same move wearing a different hat.
|
|
88
|
+
|
|
89
|
+
While you iterate, narrow the *feedback*, not the gate:
|
|
90
|
+
|
|
91
|
+
| | Command |
|
|
92
|
+
|---|---|
|
|
93
|
+
| one test file | \`bun test <path>/<file>.test.ts\` |
|
|
94
|
+
| one test by name | \`bun test -t '<name>'\` |
|
|
95
|
+
| a whole type | \`x test unit\` · \`x test contract\` · \`x test e2e\` |
|
|
96
|
+
| lint the files you touched | \`bunx biome check --write <paths>\` |
|
|
97
|
+
| types, once, when otherwise done | \`bun run typecheck\` |
|
|
98
|
+
| a broken environment | \`x doctor\` |
|
|
99
|
+
|
|
100
|
+
## 5. Hard rules
|
|
101
|
+
|
|
102
|
+
Never a bare \`throw new Error\` — subclass \`UltimateError\` with a stable code, a cause and a
|
|
103
|
+
runnable \`fix:\`. No \`any\`; use \`unknown\` and parse. Named exports only. \`import type\` for types.
|
|
104
|
+
Tests next to the source as \`<file>.test.ts\`, failure case first — a test that cannot fail is not a
|
|
105
|
+
test. One file, one job. Every user-facing string through \`t()\`. Semantic tokens, never a raw
|
|
106
|
+
colour. Every date formatted with an explicit IANA time zone. Money is integer minor units plus an
|
|
107
|
+
ISO code, never a float. Bun only.
|
|
108
|
+
|
|
109
|
+
## Output
|
|
110
|
+
|
|
111
|
+
\`\`\`
|
|
112
|
+
Primitive: <which of the eight> Slice: <dir>
|
|
113
|
+
Generated: <the x g invocations you ran>
|
|
114
|
+
Changed: <files>
|
|
115
|
+
Gate: x verify ✓ | ✗ <failing steps>
|
|
116
|
+
Deferred: <what you did not do, and why> [never omit this line]
|
|
117
|
+
\`\`\`
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const planx = (app: NameSet): string => `---
|
|
121
|
+
description: Write a short, self-contained plan for ${app.kebab} to docs/plans/, for another agent to execute.
|
|
122
|
+
argument-hint: [what you want done]
|
|
123
|
+
allowed-tools: Read, Glob, Grep, Bash, Write, Agent
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
# /planx
|
|
127
|
+
|
|
128
|
+
Plan only. No implementation, no edits outside the plan file.
|
|
129
|
+
|
|
130
|
+
## Goal
|
|
131
|
+
$ARGUMENTS
|
|
132
|
+
|
|
133
|
+
## Steps
|
|
134
|
+
|
|
135
|
+
1. **Read the code before planning against it.** If a claim in the ask is already false in the tree,
|
|
136
|
+
say so under *Risks* with the \`file:line\` that disproves it, and plan what is actually true.
|
|
137
|
+
2. **Resolve the path.** The date comes from Bun in a named zone, never from the host's ambient one —
|
|
138
|
+
\`date +%F\` gives a different answer on two machines at the same instant, and this app formats no
|
|
139
|
+
date without an explicit IANA time zone.
|
|
140
|
+
|
|
141
|
+
\`\`\`sh
|
|
142
|
+
bun -e "console.log(new Intl.DateTimeFormat('en-CA', { timeZone: 'Etc/UTC' }).format(new Date()))"
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
Then \`docs/plans/<YYYY-MM-DD>-<slug>.md\`. Slug is kebab-case, five words maximum. One file — a
|
|
146
|
+
plan split across a directory is a plan nobody reads to the end.
|
|
147
|
+
3. **Write it.** Sections, in this order:
|
|
148
|
+
|
|
149
|
+
\`\`\`markdown
|
|
150
|
+
# <Title>
|
|
151
|
+
|
|
152
|
+
## Goal
|
|
153
|
+
One or two sentences: what, and why.
|
|
154
|
+
|
|
155
|
+
## Primitive
|
|
156
|
+
Which of the eight (\`entity · policy · action · mutator · query · job · route · task\`) and which
|
|
157
|
+
slice it lives in. If it fits none, the design is wrong — say so here instead of planning a ninth.
|
|
158
|
+
|
|
159
|
+
## Files to change
|
|
160
|
+
- \`path:line\` — what changes, and why.
|
|
161
|
+
|
|
162
|
+
## Steps
|
|
163
|
+
1. Ordered, concrete. Name the \`x g\` invocation where one applies. Point at code, do not paste it.
|
|
164
|
+
|
|
165
|
+
## Tests
|
|
166
|
+
- What to add, next to the source as \`<file>.test.ts\`. Command to run it.
|
|
167
|
+
|
|
168
|
+
## Done when
|
|
169
|
+
- Acceptance criteria, ending in \`x verify\` green.
|
|
170
|
+
|
|
171
|
+
## Risks
|
|
172
|
+
- Anything the executor must decide, and every claim in the ask the code disproves.
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
## Rules
|
|
176
|
+
|
|
177
|
+
- Fragments over sentences. \`file:line\` refs over prose. Tables for anything with three or more rows.
|
|
178
|
+
- Reference-only: point at the code, never re-explain it.
|
|
179
|
+
- No checkboxes. The plan is a map, not a tracker.
|
|
180
|
+
- The plan must obey the app's own rules — one way to do each thing, generators over hand-written
|
|
181
|
+
files, imports that never cross a surface boundary, a stable error code with a runnable \`fix:\` for
|
|
182
|
+
every new failure, and \`x verify\` green as the last line of *Done when*.
|
|
183
|
+
|
|
184
|
+
## Output
|
|
185
|
+
|
|
186
|
+
\`\`\`
|
|
187
|
+
✓ docs/plans/<YYYY-MM-DD>-<slug>.md
|
|
188
|
+
Next: run it, or /feature it.
|
|
189
|
+
\`\`\`
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const verify = (): string => `---
|
|
193
|
+
description: Run the gate and fix everything it reports.
|
|
194
|
+
allowed-tools: Read, Write, Edit, Glob, Grep, Bash
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
# /verify
|
|
198
|
+
|
|
199
|
+
Run \`x verify\`.
|
|
200
|
+
|
|
201
|
+
Green: say so and stop.
|
|
202
|
+
|
|
203
|
+
Red: fix every finding, then re-run until green. Each finding carries a stable code, a cause and an
|
|
204
|
+
executable \`fix:\` — **run the \`fix:\` verbatim before improvising**, and use \`x errors explain <CODE>\`
|
|
205
|
+
when the cause is not enough. \`x verify --json\` gives the same steps machine-readably; \`x doctor\`
|
|
206
|
+
covers the case where the environment, not the code, is what is broken.
|
|
207
|
+
|
|
208
|
+
Do not narrow the gate to make it pass. There is no \`--only\` and no \`--skip\`; disabling a lint rule,
|
|
209
|
+
loosening a compiler flag, or deleting an assertion is the same evasion. Fix the code.
|
|
210
|
+
|
|
211
|
+
Report one line per fix: the code, the file, what changed.
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
/** The three workflows, in the order a new app meets them. */
|
|
215
|
+
export function claudeCommandFiles(app: NameSet): readonly GeneratedFile[] {
|
|
216
|
+
return [
|
|
217
|
+
{ path: '.claude/commands/feature.md', contents: feature(app) },
|
|
218
|
+
{ path: '.claude/commands/planx.md', contents: planx(app) },
|
|
219
|
+
{ path: '.claude/commands/verify.md', contents: verify() },
|
|
220
|
+
];
|
|
221
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The `.claude/` directory `x new` writes: the harness half of what an agent reads, next to the
|
|
2
|
+
// `AGENTS.md`/`CLAUDE.md` half that already shipped. Every file lands in the app's own repo, shows
|
|
3
|
+
// up in the scaffold's diff and is deletable in one line — the framework ships the mechanism, the
|
|
4
|
+
// app keeps or replaces the convention. Nothing here reaches outside the project directory.
|
|
5
|
+
|
|
6
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
7
|
+
import { claudeAgentFiles } from './scaffold-claude-agents';
|
|
8
|
+
import { claudeCommandFiles } from './scaffold-claude-commands';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read-only commands and the two write commands whose blast radius is a file the gate checks.
|
|
12
|
+
* Deliberately absent: `x db reset`, `x db backfill --write`, `x secrets`, `x deploy` — each is
|
|
13
|
+
* destructive or reaches production, and an allowlist that covers them is a prompt nobody reads.
|
|
14
|
+
*
|
|
15
|
+
* The hook is the one part of the bundle that ACTS: `--write` rewrites the file just edited, in
|
|
16
|
+
* place. JSON takes no comment (the lesson `scaffold-repo.ts`'s `biome.json` already paid for), so
|
|
17
|
+
* the disclosure lives in `.claude/README.md` under "The hook" — and a test pins it there.
|
|
18
|
+
*/
|
|
19
|
+
const settings = (): string => `{
|
|
20
|
+
"permissions": {
|
|
21
|
+
"allow": [
|
|
22
|
+
"Bash(bun install)",
|
|
23
|
+
"Bash(bun test:*)",
|
|
24
|
+
"Bash(bun run typecheck)",
|
|
25
|
+
"Bash(bun run lint)",
|
|
26
|
+
"Bash(bunx biome check:*)",
|
|
27
|
+
"Bash(x verify:*)",
|
|
28
|
+
"Bash(x test:*)",
|
|
29
|
+
"Bash(x doctor:*)",
|
|
30
|
+
"Bash(x g:*)",
|
|
31
|
+
"Bash(x routes:*)",
|
|
32
|
+
"Bash(x actions:*)",
|
|
33
|
+
"Bash(x queries:*)",
|
|
34
|
+
"Bash(x entities:*)",
|
|
35
|
+
"Bash(x jobs ls:*)",
|
|
36
|
+
"Bash(x jobs show:*)",
|
|
37
|
+
"Bash(x tasks:*)",
|
|
38
|
+
"Bash(x policy:*)",
|
|
39
|
+
"Bash(x i18n check:*)",
|
|
40
|
+
"Bash(x errors:*)",
|
|
41
|
+
"Bash(x env check:*)",
|
|
42
|
+
"Bash(x manifest:*)",
|
|
43
|
+
"Bash(x db gen:*)",
|
|
44
|
+
"Bash(x db migrate:*)",
|
|
45
|
+
"Bash(x db branch:*)",
|
|
46
|
+
"Bash(git status)",
|
|
47
|
+
"Bash(git diff:*)",
|
|
48
|
+
"Bash(git log:*)"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"hooks": {
|
|
52
|
+
"PostToolUse": [
|
|
53
|
+
{
|
|
54
|
+
"matcher": "Edit|Write",
|
|
55
|
+
"hooks": [
|
|
56
|
+
{
|
|
57
|
+
"type": "command",
|
|
58
|
+
"command": "bunx biome check --no-errors-on-unmatched --write \\"$CLAUDE_FILE_PATHS\\" 2>/dev/null || true"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const readme = (app: NameSet): string => `# .claude/
|
|
68
|
+
|
|
69
|
+
What Claude Code reads when it works on ${app.kebab}. Written by \`x new\`, owned by you from the
|
|
70
|
+
moment it lands: edit any file, or delete any file, and nothing in the app breaks. Nothing here is
|
|
71
|
+
enforced by \`x verify\`, and nothing here reaches outside this repository.
|
|
72
|
+
|
|
73
|
+
One thing in it does *write*: the \`PostToolUse\` hook in \`settings.json\` reformats each file an agent
|
|
74
|
+
edits, in place, the moment it is saved. That is the only thing here that touches your code on its
|
|
75
|
+
own — see [The hook](#the-hook), and delete the \`hooks\` block if you would rather format by hand.
|
|
76
|
+
|
|
77
|
+
| Path | What it costs | When you pay it |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| \`settings.json\` | nothing in context | read once per session; the hook runs per edit |
|
|
80
|
+
| \`commands/*.md\` | nothing until invoked | the file is read when you type \`/<name>\` |
|
|
81
|
+
| \`agents/*.md\` | one \`description\` line each | the body is read only when that agent is dispatched |
|
|
82
|
+
|
|
83
|
+
So the whole bundle is three description lines of standing cost. The rest is paid on use.
|
|
84
|
+
|
|
85
|
+
## What is here
|
|
86
|
+
|
|
87
|
+
| File | For |
|
|
88
|
+
|---|---|
|
|
89
|
+
| \`commands/feature.md\` | \`/feature\` — build or fix one thing end to end, gated on \`x verify\` |
|
|
90
|
+
| \`commands/planx.md\` | \`/planx\` — write a plan to \`docs/plans/\` for another agent to execute |
|
|
91
|
+
| \`commands/verify.md\` | \`/verify\` — run the gate and fix what it reports |
|
|
92
|
+
| \`agents/shape.md\` | the idea-stage pass: which primitive, which slice, or "do not build this" |
|
|
93
|
+
| \`agents/data.md\` | \`packages/db/\`, \`entity.ts\`, \`repo.ts\` |
|
|
94
|
+
| \`agents/server.md\` | actions, mutators, queries, jobs, tasks, policies, \`apps/web/api/\` |
|
|
95
|
+
| \`agents/web.md\` | \`apps/web/site/\`, pages, \`packages/ui/\`, tokens, i18n |
|
|
96
|
+
|
|
97
|
+
The agents are scoped by **boundary**, not by role — each one's brief is a file set it may write and
|
|
98
|
+
a line it may not cross. That is what makes two of them safe to run at once.
|
|
99
|
+
|
|
100
|
+
## What is deliberately not here
|
|
101
|
+
|
|
102
|
+
**No command that wraps \`x g\`.** \`x g <kind> <name>\` already is that command, and \`x g --help\` is
|
|
103
|
+
the only list of kinds — a slash command restating it is a second copy that drifts the first time a
|
|
104
|
+
kind is added.
|
|
105
|
+
|
|
106
|
+
**No size budget on this directory.** How much your app writes down is your convention, not the
|
|
107
|
+
framework's.
|
|
108
|
+
|
|
109
|
+
## The hook
|
|
110
|
+
|
|
111
|
+
One \`PostToolUse\` hook: \`bunx biome check --write\` on the file that was just edited. **\`--write\`
|
|
112
|
+
means it rewrites that file in place** — the same safe fixes \`biome check --write .\` would make
|
|
113
|
+
repo-wide, but your working tree does change after every agent edit. Scoped to one file, so it costs
|
|
114
|
+
milliseconds, and it settles formatting arguments before they reach a diff.
|
|
115
|
+
|
|
116
|
+
It is not a typecheck, because a *scoped* one does not exist: \`tsc\` needs the project, and \`x verify\`
|
|
117
|
+
takes no \`--only\` and no \`--skip\` by design — narrowing the gate would make "green" mean whatever
|
|
118
|
+
the caller chose. If you want types on every edit, \`bun run typecheck\` is the whole project and you
|
|
119
|
+
are choosing to pay for it.
|
|
120
|
+
|
|
121
|
+
## Where the rules live
|
|
122
|
+
|
|
123
|
+
\`AGENTS.md\` at the repo root — the conventions. \`.claude/\` is only the harness that reads them.
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
/** The agent harness for a new app: three commands, four boundary agents, settings, and a map. */
|
|
127
|
+
export function claudeFiles(app: NameSet): readonly GeneratedFile[] {
|
|
128
|
+
return [
|
|
129
|
+
{ path: '.claude/README.md', contents: readme(app) },
|
|
130
|
+
{ path: '.claude/settings.json', contents: settings() },
|
|
131
|
+
...claudeCommandFiles(app),
|
|
132
|
+
...claudeAgentFiles(app),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
@@ -91,6 +91,11 @@ const composeProd = (
|
|
|
91
91
|
# What \`x deploy --method compose\` runs. migrate runs to completion before anything serves.
|
|
92
92
|
#
|
|
93
93
|
# IMAGE=ghcr.io/you/${app.kebab}:1.2.3 x deploy --image ghcr.io/you/${app.kebab}:1.2.3
|
|
94
|
+
#
|
|
95
|
+
# A published host port has exactly one binder, so \`web\` and \`sync\` run at 1 here. Compose is one
|
|
96
|
+
# box; horizontal scaling of those two belongs to an orchestrator (copy \`docker/helm\` from the
|
|
97
|
+
# framework repo). To scale them on one box anyway, drop \`ports:\` and put your own proxy on this
|
|
98
|
+
# network — the service name resolves to every replica over the compose DNS round robin.
|
|
94
99
|
name: ${app.kebab}
|
|
95
100
|
|
|
96
101
|
x-image: &image
|
|
@@ -120,13 +125,33 @@ services:
|
|
|
120
125
|
environment: [ROLE=migrate]
|
|
121
126
|
restart: 'no'
|
|
122
127
|
|
|
128
|
+
# Run-once, AFTER the new version serves. Deliberately NOT part of the release gate: a slow
|
|
129
|
+
# UPDATE there holds the deploy open against a database still serving the previous version.
|
|
130
|
+
# Dry run is the default, so \`--write\` is explicit.
|
|
131
|
+
backfill:
|
|
132
|
+
<<: *image
|
|
133
|
+
# The image's ENTRYPOINT is \`bun apps/web/server.ts\`, and that entry reads ROLE and PORT and
|
|
134
|
+
# NOTHING ELSE — argv never reaches a parser. A bare \`command:\` is appended to it and silently
|
|
135
|
+
# discarded, so this service used to serve HTTP as ROLE=web under a name that said otherwise.
|
|
136
|
+
# Overriding the entrypoint is what makes the words below a command. The file path, not
|
|
137
|
+
# \`node_modules/.bin/x\`: it needs no bin symlink and no executable bit inside the image.
|
|
138
|
+
entrypoint: ['bun', 'node_modules/@ultimat3/cli/src/bin.ts']
|
|
139
|
+
command: ['db', 'backfill', '--all', '--write', '--json']
|
|
140
|
+
depends_on:
|
|
141
|
+
db: { condition: service_healthy }
|
|
142
|
+
migrate: { condition: service_completed_successfully }
|
|
143
|
+
# The barrier, not the ordering. \`docker compose up -d\` returns when a container STARTS, so
|
|
144
|
+
# listing this last would only look like "after". The image's HEALTHCHECK is what makes it true.
|
|
145
|
+
web: { condition: service_healthy }
|
|
146
|
+
restart: 'no'
|
|
147
|
+
|
|
123
148
|
web:
|
|
124
149
|
<<: *image
|
|
125
150
|
environment: [ROLE=web]
|
|
126
151
|
depends_on:
|
|
127
152
|
db: { condition: service_healthy }
|
|
128
153
|
migrate: { condition: service_completed_successfully }
|
|
129
|
-
deploy: { replicas:
|
|
154
|
+
deploy: { replicas: 1 } # stateless, scales on RPS — pinned by the published port
|
|
130
155
|
ports: ['3000:3000']
|
|
131
156
|
|
|
132
157
|
sync:
|
|
@@ -135,7 +160,8 @@ services:
|
|
|
135
160
|
depends_on:
|
|
136
161
|
db: { condition: service_healthy }
|
|
137
162
|
migrate: { condition: service_completed_successfully }
|
|
138
|
-
deploy: { replicas: 1 } # scales on concurrent websockets
|
|
163
|
+
deploy: { replicas: 1 } # scales on concurrent websockets, no sticky sessions — pinned by the port
|
|
164
|
+
# The sync role binds PORT + 1. PORT is unset here, so it is 3000 and this listens on 3001.
|
|
139
165
|
ports: ['3001:3001']
|
|
140
166
|
|
|
141
167
|
worker:
|
|
@@ -190,6 +216,11 @@ docker compose -f docker/docker-compose.prod.yml up -d # db → migrate →
|
|
|
190
216
|
x deploy --image ${app.kebab}:dev --dry-run --json # the same plan, printed
|
|
191
217
|
\`\`\`
|
|
192
218
|
|
|
219
|
+
\`web\` and \`sync\` publish a host port, so both sit at \`replicas: 1\`: one host port has exactly one
|
|
220
|
+
binder, and a second container dies on \`port is already allocated\`. \`worker\` publishes nothing and
|
|
221
|
+
scales freely. To scale the two serving roles on one box, delete their \`ports:\` lines and put your
|
|
222
|
+
own proxy on the compose network. To scale them properly, use an orchestrator — see below.
|
|
223
|
+
|
|
193
224
|
## The other two build targets
|
|
194
225
|
|
|
195
226
|
\`\`\`sh
|
|
@@ -235,6 +266,19 @@ that no longer matches an applied migration stops the release instead of corrupt
|
|
|
235
266
|
There is no \`x db migrate\` in that list on purpose: it is the developer's command and it needs the
|
|
236
267
|
toolchain, while the release phase runs the shipped image and nothing else.
|
|
237
268
|
|
|
269
|
+
### One-off commands need a new entrypoint, not arguments
|
|
270
|
+
|
|
271
|
+
\`ENTRYPOINT\` is \`bun apps/web/server.ts\`, and that entry reads \`ROLE\` and \`PORT\` and **nothing
|
|
272
|
+
else** — argv never reaches a parser. So arguments appended to it are discarded in silence:
|
|
273
|
+
|
|
274
|
+
\`\`\`sh
|
|
275
|
+
docker run ${app.kebab}:dev db backfill --all --write # serves ROLE=web, forever
|
|
276
|
+
docker run --entrypoint bun ${app.kebab}:dev node_modules/@ultimat3/cli/src/bin.ts db backfill --all --write --json
|
|
277
|
+
\`\`\`
|
|
278
|
+
|
|
279
|
+
The \`backfill\` service in \`docker-compose.prod.yml\` is the second form. A Kubernetes \`Job\` running
|
|
280
|
+
a one-off command sets \`command:\` (the entrypoint) as well as \`args:\`, for the same reason.
|
|
281
|
+
|
|
238
282
|
## Environment
|
|
239
283
|
|
|
240
284
|
| Key | Meaning | Unset means |
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The generated app's `packages/db`: the entity re-export list the migration generator reads and
|
|
2
|
+
// the deterministic seed. No business logic — that is the package's own stated boundary, and it is
|
|
3
|
+
// why `example` reaches only the two files describing the slice's table.
|
|
4
|
+
//
|
|
5
|
+
// No migration. `x db gen` is the ONE writer of `packages/db/migrations`, and a scaffold that hand-
|
|
6
|
+
// wrote `0000_initial.sql` was a second one: it declared a `posts` table the generator had never
|
|
7
|
+
// diffed, so the first `x db gen` saw a schema the ledger already claimed and the two disagreed
|
|
8
|
+
// about what "initial" meant. `x db gen "initial"` is the app's first command instead — it writes
|
|
9
|
+
// the `.sql`, the `.snapshot.json` and the `.hash` together, which no hand-written file can.
|
|
10
|
+
|
|
11
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
12
|
+
import { packageShapeFiles, workspacePackageJson } from './scaffold-package-shape';
|
|
13
|
+
|
|
14
|
+
const DESCRIPTION = 'Entity re-exports and SQL migrations, no business logic';
|
|
15
|
+
|
|
16
|
+
const dbIndex =
|
|
17
|
+
(): string => `// Schema and migrations only — no business logic lives in this package. The client itself is
|
|
18
|
+
// @ultimat3/db's: one connection pool, sized by ROLE, shared by every package in the app.
|
|
19
|
+
export type { DbClient, SqlFragment } from '@ultimat3/db';
|
|
20
|
+
export { db, sql, withTransaction } from '@ultimat3/db';
|
|
21
|
+
export * as schema from './schema';
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
// The four pieces below describe the example slice's table. Under `--no-example` that slice is
|
|
25
|
+
// never written, so each one ships its empty counterpart instead of a reference to a file that is
|
|
26
|
+
// not there — `export { post } from …` alone made `x new --no-example` an app that cannot compile.
|
|
27
|
+
|
|
28
|
+
const SCHEMA_HEADER = `// Every entity the app declares, re-exported here. This list is what the migration generator
|
|
29
|
+
// reads, so an entity that is not exported here does not exist as far as the database is concerned.`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `bun run db:seed`'s entry point. Identical either way — only the rows differ. Interpolated, not
|
|
33
|
+
* nested, so it carries exactly the escaping a single template literal needs.
|
|
34
|
+
*/
|
|
35
|
+
const SEED_MAIN = `
|
|
36
|
+
|
|
37
|
+
if (import.meta.main) {
|
|
38
|
+
const count = await seed();
|
|
39
|
+
// Bun's stdout, not process.stdout: one runtime, one API. Awaited because the write resolves
|
|
40
|
+
// asynchronously, and this JSON line is the whole output of \`bun run db:seed\`.
|
|
41
|
+
await Bun.stdout.write(\`\${JSON.stringify({ ok: true, seeded: count })}\\n\`);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const dbSchema = (app: NameSet, example: boolean): string =>
|
|
46
|
+
example
|
|
47
|
+
? `${SCHEMA_HEADER}
|
|
48
|
+
export { post } from '@${app.kebab}/web/app/post/entity';
|
|
49
|
+
`
|
|
50
|
+
: `${SCHEMA_HEADER}
|
|
51
|
+
// \`x g entity <name>\` writes the entity; add its export here so the database learns about it.
|
|
52
|
+
export {};
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const dbSeed = (app: NameSet, example: boolean): string =>
|
|
56
|
+
example
|
|
57
|
+
? `// Deterministic seed: same rows every time, so a test and a demo see the same database.
|
|
58
|
+
import { db, sql } from '@ultimat3/db';
|
|
59
|
+
|
|
60
|
+
const ORG = '00000000-0000-0000-0000-000000000002';
|
|
61
|
+
|
|
62
|
+
export async function seed(): Promise<number> {
|
|
63
|
+
const rows = [
|
|
64
|
+
{ id: '00000000-0000-0000-0000-000000000101', title: 'Hello ${app.pascal}', minor: 0 },
|
|
65
|
+
{ id: '00000000-0000-0000-0000-000000000102', title: 'Second post', minor: 1900 },
|
|
66
|
+
];
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
// Idempotent by primary key, so re-seeding a branch database is a no-op rather than a crash.
|
|
69
|
+
await db().execute(sql\`
|
|
70
|
+
insert into posts (id, org_id, title, price_minor, price_currency)
|
|
71
|
+
values (\${row.id}, \${ORG}, \${row.title}, \${row.minor}, 'USD')
|
|
72
|
+
on conflict (id) do nothing\`);
|
|
73
|
+
}
|
|
74
|
+
return rows.length;
|
|
75
|
+
}${SEED_MAIN}`
|
|
76
|
+
: `// Deterministic seed: same rows every time, so a test and a demo see the same database.
|
|
77
|
+
// No entity is declared yet, so there is nothing to insert — the shape stays, so the first
|
|
78
|
+
// \`x g entity\` has one obvious place to seed from.
|
|
79
|
+
|
|
80
|
+
export async function seed(): Promise<number> {
|
|
81
|
+
return 0;
|
|
82
|
+
}${SEED_MAIN}`;
|
|
83
|
+
|
|
84
|
+
/** Every file the `packages/db` workspace ships, in the order `x new` writes them. */
|
|
85
|
+
export const dbPackageFiles = (app: NameSet, example: boolean): readonly GeneratedFile[] => [
|
|
86
|
+
{ path: 'packages/db/package.json', contents: workspacePackageJson(app, 'db', DESCRIPTION) },
|
|
87
|
+
...packageShapeFiles(app, 'db', DESCRIPTION),
|
|
88
|
+
{ path: 'packages/db/src/index.ts', contents: dbIndex() },
|
|
89
|
+
{ path: 'packages/db/src/schema.ts', contents: dbSchema(app, example) },
|
|
90
|
+
{ path: 'packages/db/src/seed.ts', contents: dbSeed(app, example) },
|
|
91
|
+
];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// The human-authored half of what `x new` writes: the READMEs, the agent-facing convention files,
|
|
2
2
|
// the bin/ shims and the optional dev compose. Separated from the config half so neither file has
|
|
3
3
|
// to be scrolled to find the other — one file, one job applies to templates too. The image, its
|
|
4
|
-
// ignore file, the production topology and the deploy page are `scaffold-container.ts
|
|
4
|
+
// ignore file, the production topology and the deploy page are `scaffold-container.ts`; the
|
|
5
|
+
// `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`.
|
|
5
6
|
|
|
6
7
|
import type { GeneratedFile, NameSet } from './naming';
|
|
8
|
+
import { claudeFiles } from './scaffold-claude';
|
|
7
9
|
import { containerFiles } from './scaffold-container';
|
|
8
10
|
|
|
9
11
|
const agents = (app: NameSet): string => `# AGENTS.md
|
|
@@ -23,7 +25,7 @@ agent cannot infer from the code.
|
|
|
23
25
|
| Strings | every user-facing string goes through \`t()\` |
|
|
24
26
|
| Colour | semantic tokens only, never a raw hex |
|
|
25
27
|
|
|
26
|
-
Commands: \`x dev\`, \`x verify\`, \`x g <primitive>\`, \`x db branch <name>\`, \`x doctor\`.
|
|
28
|
+
Commands: \`x dev\`, \`x verify\`, \`x g <primitive>\`, \`x db branch create <name>\`, \`x doctor\`.
|
|
27
29
|
|
|
28
30
|
Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
|
|
29
31
|
`;
|
|
@@ -33,9 +35,14 @@ const claude = (app: NameSet): string => `# CLAUDE.md
|
|
|
33
35
|
${app.kebab} — Ultimate app. Read AGENTS.md first; it is the same content in the same order.
|
|
34
36
|
|
|
35
37
|
- Gate: \`x verify\` (add \`--json\` for machine output).
|
|
36
|
-
- Scaffold, do not hand-write: \`x g
|
|
37
|
-
|
|
38
|
+
- Scaffold, do not hand-write: \`x g <kind> <name>\` — \`x g --help\` lists every kind, and is the
|
|
39
|
+
only place that list is stated.
|
|
40
|
+
- Destructive DB work goes in a branch: \`x db branch create <name>\`, never the shared dev DB.
|
|
38
41
|
- \`x doctor\` explains a broken environment and prints the fix command for every finding.
|
|
42
|
+
|
|
43
|
+
\`.claude/\` holds the harness that reads this file: \`/feature\`, \`/planx\`, \`/verify\` and four
|
|
44
|
+
boundary-scoped subagents. It is yours — \`.claude/README.md\` says what each one costs, and every
|
|
45
|
+
file in it is deletable.
|
|
39
46
|
`;
|
|
40
47
|
|
|
41
48
|
const readme = (app: NameSet): string => `# ${app.pascal}
|
|
@@ -45,11 +52,15 @@ Built with [Ultimate](https://ultimate.dev). Bun-only, Postgres, SolidJS.
|
|
|
45
52
|
## 🚀 Start
|
|
46
53
|
|
|
47
54
|
\`\`\`sh
|
|
48
|
-
bin/setup # prerequisites, deps, env, migrate, seed
|
|
55
|
+
bin/setup # prerequisites, deps, env, the first migration, migrate, seed
|
|
49
56
|
x dev # all roles in one process, embedded Postgres, /_x mounted
|
|
50
57
|
x verify # the gate: typecheck, lint, boundaries, tests, drift, budgets
|
|
51
58
|
\`\`\`
|
|
52
59
|
|
|
60
|
+
\`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bin/setup\` runs
|
|
61
|
+
\`x db gen "initial"\` for you on a fresh clone. Until it has, \`x verify\`'s \`drift\` step is red
|
|
62
|
+
with \`X_DB_DRIFT\`, and that is the fix it names.
|
|
63
|
+
|
|
53
64
|
## 🗺 Layout
|
|
54
65
|
|
|
55
66
|
| Path | Holds |
|
|
@@ -71,6 +82,11 @@ cd "$(dirname "$0")/.."
|
|
|
71
82
|
command -v bun >/dev/null || { echo "X_BUN_MISSING: install bun — https://bun.sh"; exit 1; }
|
|
72
83
|
bun install
|
|
73
84
|
[ -f .env.development.local ] || printf '# per-box secrets, gitignored, wins over .env.development\\n' > .env.development.local
|
|
85
|
+
# \`x db gen\` is the ONE writer of packages/db/migrations — the scaffold no longer hand-writes a
|
|
86
|
+
# 0000_initial.sql, because a second writer is how the source and the ledger ended up disagreeing
|
|
87
|
+
# about what "initial" meant. Guarded on the directory rather than on the generator being a no-op:
|
|
88
|
+
# this script is documented idempotent, and the guard is what makes that true here.
|
|
89
|
+
ls packages/db/migrations/*.sql >/dev/null 2>&1 || bunx x db gen "initial"
|
|
74
90
|
bunx x db migrate "$@"
|
|
75
91
|
bun run db:seed
|
|
76
92
|
echo "setup complete — next: x dev"
|
|
@@ -126,6 +142,9 @@ export function docsFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
126
142
|
{ path: 'bin/dev', contents: binDev() },
|
|
127
143
|
{ path: 'bin/check', contents: binCheck() },
|
|
128
144
|
{ path: 'docker/docker-compose.dev.yml', contents: composeDev(app) },
|
|
145
|
+
// The harness half of the same job AGENTS.md does. It lands in the app's own repo rather than
|
|
146
|
+
// in a global config, so it is visible in the scaffold's diff and deletable in one line.
|
|
147
|
+
...claudeFiles(app),
|
|
129
148
|
...containerFiles(app),
|
|
130
149
|
];
|
|
131
150
|
}
|