@ultimat3/cli 19.4.0 → 20.1.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 +56 -3
- package/package.json +29 -29
- package/src/budgets.ts +47 -2
- package/src/cmd-doctor.ts +97 -3
- package/src/cmd-shot-island.ts +120 -15
- package/src/cmd-shot.ts +45 -20
- package/src/index.ts +8 -2
- package/src/island-capture.ts +278 -0
- package/src/island-harness-script.ts +10 -1
- package/src/island-shot-index.ts +155 -0
- package/src/island-shot.ts +106 -276
- package/src/island-verdict.ts +90 -1
- package/src/messages.ts +2 -1
- package/src/prerender.ts +16 -1
- package/src/sw-artifacts.ts +9 -2
- package/src/templates/github/ci.yml.ts +74 -0
- package/src/templates/guard-animated-layout-property.ts +269 -0
- package/src/templates/guard-focus-visible.ts +240 -0
- package/src/templates/guard-image-dimensions.ts +225 -0
- package/src/templates/guard-island-without-states.ts +128 -0
- package/src/templates/guard-raw-colour.ts +112 -8
- package/src/templates/guard-semantic-interactive.ts +244 -0
- package/src/templates/guard-untranslated-string.ts +54 -6
- package/src/templates/island.ts +44 -1
- package/src/templates/resource-form-island.ts +67 -0
- package/src/templates/scaffold-claude-agents.ts +10 -1
- package/src/templates/scaffold-claude-commands.ts +20 -9
- package/src/templates/scaffold-docs.ts +40 -5
- package/src/templates/scaffold-guards.ts +15 -0
- package/src/templates/scaffold-repo.ts +22 -2
|
@@ -2,9 +2,11 @@
|
|
|
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
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
|
+
// `.claude/` harness that reads AGENTS.md is `scaffold-claude.ts`; the one workflow is
|
|
6
|
+
// `github/ci.yml.ts`.
|
|
6
7
|
|
|
7
8
|
import { LINE_CEILING } from '../workspace-checks';
|
|
9
|
+
import { githubFiles } from './github/ci.yml';
|
|
8
10
|
import type { GeneratedFile, NameSet } from './naming';
|
|
9
11
|
import { claudeFiles } from './scaffold-claude';
|
|
10
12
|
import { containerFiles } from './scaffold-container';
|
|
@@ -19,7 +21,7 @@ not exist — five of these had an empty column and were each measured green on
|
|
|
19
21
|
|
|
20
22
|
| Rule | Detail | Refused by |
|
|
21
23
|
|---|---|---|
|
|
22
|
-
| One gate | \`x
|
|
24
|
+
| One gate | \`bin/check\` — \`x build\` and then \`x verify\`. Green means shippable; never merge red. | the gate itself, and \`.github/workflows/ci.yml\` on every push and pull request |
|
|
23
25
|
| One way | generators, not hand-rolled files: \`x g resource\`, \`x g action\`, \`x g route\` | review |
|
|
24
26
|
| Surfaces | \`site/\` is 0kb JS and may not import \`app/\`; \`shared/\` is a leaf | \`X_BOUNDARY_SITE_TO_APP\` |
|
|
25
27
|
| Data | routes call actions and queries; only \`repo.ts\` touches the database | \`X_BOUNDARY_ROUTE_TO_DB\` |
|
|
@@ -28,11 +30,22 @@ not exist — five of these had an empty column and were each measured green on
|
|
|
28
30
|
| Time | store UTC, format with an explicit IANA time zone | \`guards/unzoned-date.ts\` |
|
|
29
31
|
| Strings | every user-facing string goes through \`t()\` | \`guards/untranslated-string.ts\` |
|
|
30
32
|
| Colour | semantic tokens only, never a raw hex | \`guards/raw-colour.ts\` |
|
|
33
|
+
| Interaction | a click is answered by a control — never a \`<div onClick>\`, and never a \`role=\` where the native tag exists | \`guards/semantic-interactive.ts\` |
|
|
34
|
+
| Focus | \`outline: none\` replaces the ring in the same rule or the one beside it, or it does not remove it | \`guards/focus-visible.ts\` |
|
|
35
|
+
| Images | every image carries width + height or an aspect-ratio, and the priority one is never \`loading="lazy"\` | \`guards/image-dimensions.ts\` |
|
|
36
|
+
| Motion | animate \`transform\` and \`opacity\` — never a layout property, never \`transition: all\` | \`guards/animated-layout-property.ts\` |
|
|
37
|
+
| Islands | every \`*.island.tsx\` has a sibling \`*.island.states.ts\`, so \`x shot --island\` can photograph its failures | \`guards/island-without-states.ts\` |
|
|
31
38
|
| Size | one file, one job — ${LINE_CEILING} lines of reviewable logic, and split past it | \`X_FILE_TOO_LONG\` |
|
|
32
39
|
|
|
33
40
|
\`guards/\` is yours: each file is one rule, discovered by \`x verify\` and run inside its
|
|
34
41
|
\`boundaries\` step. Delete one to drop the rule, and \`x g guard <name>\` writes the next.
|
|
35
42
|
|
|
43
|
+
The last five rows are about what this app is like to USE, and each is decidable from the file
|
|
44
|
+
alone — which is why they are those five and not the many that are not. A role is a promise:
|
|
45
|
+
\`role="button"\` obliges you to answer Space, Enter, focus and disabled, and the native element
|
|
46
|
+
already does. Only \`transform\` and \`opacity\` animate without a layout pass. An image with no box
|
|
47
|
+
moves everything under it when its bytes land.
|
|
48
|
+
|
|
36
49
|
Size is a hard line and not a style note: past ${LINE_CEILING} lines a file has stopped being the
|
|
37
50
|
unit of review, and \`x verify\` refuses it. The one exemption is a file that is nothing but re-exports —
|
|
38
51
|
it has one job by construction, and its length tracks the API's size rather than its complexity;
|
|
@@ -43,7 +56,19 @@ see, and the type already fires — measured, \`price: 19.99\` in a seed is
|
|
|
43
56
|
\`TS2322: Type 'number' is not assignable to type 'MoneyInput'\`. A guard that pretended to check
|
|
44
57
|
it would be worse than the type that really does.
|
|
45
58
|
|
|
46
|
-
|
|
59
|
+
\`bin/check\` is the gate and \`x verify\` is only its second half: the first is
|
|
60
|
+
\`x build --target static\`, and the build is what writes the \`.x/build-stats.json\` the
|
|
61
|
+
\`budgets\` step measures. Run \`x verify\` on a tree nobody has built and \`budgets\` is red with
|
|
62
|
+
X_BUDGET_UNMEASURED — the gate reporting on a file that does not exist, not on your code.
|
|
63
|
+
|
|
64
|
+
The platform's \`.dz/\` is ADDITIVE, in both directions. developerz.ai keeps its own files under
|
|
65
|
+
\`.dz/maintainer/\` and \`.dz/pipeline/\`; nothing \`x new\` writes lands under \`.dz/\` and nothing
|
|
66
|
+
here is generated from it. So the scaffold never clobbers a maintainer policy, the platform never
|
|
67
|
+
clobbers \`AGENTS.md\`, \`bin/\`, \`.claude/\` or \`.github/\`, and deleting either side leaves the
|
|
68
|
+
other exactly as it was.
|
|
69
|
+
|
|
70
|
+
Commands: \`bin/setup\`, \`bin/dev\`, \`bin/check\`, \`x g <primitive>\`, \`x g guard <name>\`,
|
|
71
|
+
\`x db branch create <name>\`, \`x doctor\`.
|
|
47
72
|
|
|
48
73
|
Project notes for ${app.kebab}: replace this line with the conventions a newcomer could not guess.
|
|
49
74
|
`;
|
|
@@ -52,7 +77,12 @@ const claude = (app: NameSet): string => `# CLAUDE.md
|
|
|
52
77
|
|
|
53
78
|
${app.kebab} — Ultimate app. Read AGENTS.md first; it is the same content in the same order.
|
|
54
79
|
|
|
55
|
-
- Gate: \`
|
|
80
|
+
- Gate: \`bin/check\` (add \`--json\` for machine output — it reaches both halves). It is
|
|
81
|
+
\`x build --target static\` and then \`x verify\`; \`x verify\` alone leaves \`budgets\` with
|
|
82
|
+
nothing to measure. \`.github/workflows/ci.yml\` runs \`bin/setup && bin/check\` on every push
|
|
83
|
+
and pull request, so CI and your terminal run the same two commands.
|
|
84
|
+
- \`.dz/\` belongs to the developerz.ai platform and is additive both ways: the scaffold writes
|
|
85
|
+
nothing there, and nothing there is generated from this repo. Neither side clobbers the other.
|
|
56
86
|
- Scaffold, do not hand-write: \`x g <kind> <name>\` — \`x g --help\` lists every kind, and is the
|
|
57
87
|
only place that list is stated.
|
|
58
88
|
- Destructive DB work goes in a branch: \`x db branch create <name>\`, never the shared dev DB.
|
|
@@ -72,7 +102,7 @@ Built with [Ultimate](https://github.com/developerz-ai/ultimate). Bun-only, Post
|
|
|
72
102
|
\`\`\`sh
|
|
73
103
|
bin/setup # prerequisites, deps, env, the first migration, migrate, seed, the manifest
|
|
74
104
|
bin/dev # all roles in one process, embedded Postgres, /_x mounted
|
|
75
|
-
bin/check # the gate: typecheck, lint, boundaries, tests, drift, budgets
|
|
105
|
+
bin/check # the gate: a static build, then typecheck, lint, boundaries, tests, drift, budgets
|
|
76
106
|
\`\`\`
|
|
77
107
|
|
|
78
108
|
\`packages/db/migrations\` starts empty and \`x db gen\` is its only writer — \`bin/setup\` runs
|
|
@@ -91,6 +121,7 @@ with \`X_DB_DRIFT\`, and that is the fix it names.
|
|
|
91
121
|
| \`packages/*\` | domain, db, i18n, ui, mcp |
|
|
92
122
|
| \`app.config.ts\` | the one config file |
|
|
93
123
|
| \`x.manifest.json\` | generated facts: routes, actions, jobs, policies |
|
|
124
|
+
| \`.github/workflows/ci.yml\` | \`bin/setup\` then \`bin/check\`, on push and pull request |
|
|
94
125
|
`;
|
|
95
126
|
|
|
96
127
|
const binSetup = (): string => `#!/usr/bin/env bash
|
|
@@ -193,6 +224,10 @@ export function docsFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
193
224
|
// The harness half of the same job AGENTS.md does. It lands in the app's own repo rather than
|
|
194
225
|
// in a global config, so it is visible in the scaffold's diff and deletable in one line.
|
|
195
226
|
...claudeFiles(app),
|
|
227
|
+
// The same two commands `README.md` opens with, run by a machine that has never seen this
|
|
228
|
+
// repository. Registered here and not in `scaffold-repo.ts` because it is documentation of the
|
|
229
|
+
// gate in executable form, which is the job this file has.
|
|
230
|
+
...githubFiles(app),
|
|
196
231
|
...containerFiles(app),
|
|
197
232
|
];
|
|
198
233
|
}
|
|
@@ -6,9 +6,19 @@
|
|
|
6
6
|
// (`packages/cli/src/guards.ts`). What was missing is any guard to discover. Four of the five are
|
|
7
7
|
// here; the fifth, money-as-float, has no static signature and is answered by the `Money` type
|
|
8
8
|
// instead — `scaffold-docs.ts` says so where an author reads it.
|
|
9
|
+
//
|
|
10
|
+
// The other five are about what the app is like to USE, which no row of `AGENTS.md` had ever been
|
|
11
|
+
// about: a control a keyboard cannot reach, a focus ring taken away and not replaced, an image with
|
|
12
|
+
// no box, an animation the compositor cannot run, and an island nobody has seen fail. Each is
|
|
13
|
+
// statically decidable from the file alone — the reason those five and not the many that are not.
|
|
9
14
|
|
|
15
|
+
import { animatedLayoutPropertyGuardFiles } from './guard-animated-layout-property';
|
|
10
16
|
import { bareErrorGuardFiles } from './guard-bare-error';
|
|
17
|
+
import { focusVisibleGuardFiles } from './guard-focus-visible';
|
|
18
|
+
import { imageDimensionsGuardFiles } from './guard-image-dimensions';
|
|
19
|
+
import { islandWithoutStatesGuardFiles } from './guard-island-without-states';
|
|
11
20
|
import { rawColourGuardFiles } from './guard-raw-colour';
|
|
21
|
+
import { semanticInteractiveGuardFiles } from './guard-semantic-interactive';
|
|
12
22
|
import { untranslatedStringGuardFiles } from './guard-untranslated-string';
|
|
13
23
|
import { unzonedDateGuardFiles } from './guard-unzoned-date';
|
|
14
24
|
import type { GeneratedFile } from './naming';
|
|
@@ -19,8 +29,13 @@ import type { GeneratedFile } from './naming';
|
|
|
19
29
|
* `x g guard <name>`, the same shape.
|
|
20
30
|
*/
|
|
21
31
|
export const scaffoldGuardFiles = (): readonly GeneratedFile[] => [
|
|
32
|
+
...animatedLayoutPropertyGuardFiles(),
|
|
22
33
|
...bareErrorGuardFiles(),
|
|
34
|
+
...focusVisibleGuardFiles(),
|
|
35
|
+
...imageDimensionsGuardFiles(),
|
|
36
|
+
...islandWithoutStatesGuardFiles(),
|
|
23
37
|
...rawColourGuardFiles(),
|
|
38
|
+
...semanticInteractiveGuardFiles(),
|
|
24
39
|
...untranslatedStringGuardFiles(),
|
|
25
40
|
...unzonedDateGuardFiles(),
|
|
26
41
|
];
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// container files scaffold-container.ts.
|
|
6
6
|
|
|
7
7
|
import { ENV_EXAMPLE_PATH } from '@ultimat3/core';
|
|
8
|
+
import { REQUIRED_BUN } from '../app-root';
|
|
8
9
|
import { VERIFY_FLOOR_FILE } from '../verify-floor';
|
|
9
10
|
import type { VerifyStepName } from '../verify-step';
|
|
10
11
|
import type { GeneratedFile, NameSet } from './naming';
|
|
@@ -26,6 +27,25 @@ import { uiPackageFiles } from './scaffold-ui-package';
|
|
|
26
27
|
*/
|
|
27
28
|
const BIOME_VERSION = '2.5.8';
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The checker a scaffolded app typechecks with, and it must not lag the one the FRAMEWORK is built
|
|
32
|
+
* and gated on: `@ultimat3/*` ships `.d.ts` emitted by this compiler, so an app pinned a major
|
|
33
|
+
* behind reads the types its own dependencies were written against through an older checker.
|
|
34
|
+
* It drifted for exactly the reason `BIOME_VERSION` did not — Biome's was spelled once as a
|
|
35
|
+
* constant and TypeScript's was a literal buried in a dependency block, so the framework moved to
|
|
36
|
+
* 7.x and every `x new` kept scaffolding `^6.0.3`. `scaffold-repo.test.ts` now pins this against
|
|
37
|
+
* the repo's own root `package.json`, so the next bump cannot leave the scaffold behind in silence.
|
|
38
|
+
*/
|
|
39
|
+
const TYPESCRIPT_VERSION = '^7.0.2';
|
|
40
|
+
|
|
41
|
+
// `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
|
|
42
|
+
// (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
|
|
43
|
+
// BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
|
|
44
|
+
// next line of `bin/setup` died `X_BUN_VERSION: Bun 1.3.14 is older than the required 1.4.0` — a
|
|
45
|
+
// floor admitting a runtime the app's own first command refuses. Interpolated, the two cannot
|
|
46
|
+
// disagree; `scripts/bun-pin.test.ts` reads the emitted string as one more pin site, and
|
|
47
|
+
// `github/ci.yml.ts` pins the workflow's Bun to the same constant.
|
|
48
|
+
//
|
|
29
49
|
// `version` is not decoration: the manifest's app version IS the contract's compatibility gate,
|
|
30
50
|
// and the manifest never fabricates one — so an app scaffolded without it failed `x manifest`,
|
|
31
51
|
// the `manifest` verify step and every production boot with X_APP_PACKAGE_INVALID.
|
|
@@ -54,7 +74,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
54
74
|
"@electric-sql/pglite": "^0.5.4",
|
|
55
75
|
"@types/bun": "^1.4.0",
|
|
56
76
|
"@ultimat3/testing": "^${version}",
|
|
57
|
-
"typescript": "
|
|
77
|
+
"typescript": "${TYPESCRIPT_VERSION}"
|
|
58
78
|
},
|
|
59
79
|
"dependencies": {
|
|
60
80
|
"@ultimat3/action": "^${version}",
|
|
@@ -78,7 +98,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
78
98
|
"solid-js": "1.9.14"
|
|
79
99
|
},
|
|
80
100
|
"engines": {
|
|
81
|
-
"bun": "
|
|
101
|
+
"bun": ">=${REQUIRED_BUN}"
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
`;
|