@ultimat3/cli 1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/package.json +60 -0
  4. package/src/app-agents-md.ts +27 -0
  5. package/src/app-boundaries.ts +206 -0
  6. package/src/app-evals.ts +74 -0
  7. package/src/app-load.ts +136 -0
  8. package/src/app-manifest.ts +137 -0
  9. package/src/app-openapi.ts +12 -0
  10. package/src/app-root.ts +57 -0
  11. package/src/bin.ts +17 -0
  12. package/src/boundary-cuts.ts +219 -0
  13. package/src/budgets.ts +92 -0
  14. package/src/cmd-build.ts +109 -0
  15. package/src/cmd-db.ts +187 -0
  16. package/src/cmd-deploy.ts +124 -0
  17. package/src/cmd-dev.ts +286 -0
  18. package/src/cmd-doctor.ts +178 -0
  19. package/src/cmd-errors.ts +99 -0
  20. package/src/cmd-fix.ts +126 -0
  21. package/src/cmd-generate.ts +434 -0
  22. package/src/cmd-help.ts +94 -0
  23. package/src/cmd-i18n.ts +212 -0
  24. package/src/cmd-jobs.ts +237 -0
  25. package/src/cmd-manifest.ts +97 -0
  26. package/src/cmd-mcp.ts +176 -0
  27. package/src/cmd-new.ts +133 -0
  28. package/src/cmd-planned.ts +119 -0
  29. package/src/cmd-policy.ts +136 -0
  30. package/src/cmd-registries.ts +195 -0
  31. package/src/cmd-routes.ts +73 -0
  32. package/src/cmd-tasks.ts +151 -0
  33. package/src/cmd-test.ts +109 -0
  34. package/src/cmd-verify.ts +265 -0
  35. package/src/command.ts +33 -0
  36. package/src/dev-assets.ts +177 -0
  37. package/src/dev-dashboard.ts +242 -0
  38. package/src/dev-hooks.ts +51 -0
  39. package/src/dev-policy.ts +82 -0
  40. package/src/dev-queue.ts +109 -0
  41. package/src/dev-render.ts +129 -0
  42. package/src/dev-replicator.ts +92 -0
  43. package/src/dev-roles.ts +246 -0
  44. package/src/dev-runtime.ts +203 -0
  45. package/src/dev-services.ts +75 -0
  46. package/src/dev-traces.ts +141 -0
  47. package/src/dispatch.ts +98 -0
  48. package/src/drift.ts +86 -0
  49. package/src/error-catalog.ts +156 -0
  50. package/src/error-contract.ts +212 -0
  51. package/src/errors.ts +367 -0
  52. package/src/exec.ts +70 -0
  53. package/src/hold.ts +48 -0
  54. package/src/i18n-audit.ts +183 -0
  55. package/src/index.ts +179 -0
  56. package/src/jobs-drain.ts +151 -0
  57. package/src/jobs-json.ts +134 -0
  58. package/src/jobs-report.ts +132 -0
  59. package/src/jobs-table.ts +34 -0
  60. package/src/json-merge.ts +40 -0
  61. package/src/mcp-db-target.ts +50 -0
  62. package/src/mcp-errors.ts +99 -0
  63. package/src/mcp-host.ts +282 -0
  64. package/src/mcp-test-output.ts +57 -0
  65. package/src/messages.ts +119 -0
  66. package/src/output.ts +174 -0
  67. package/src/parse.ts +243 -0
  68. package/src/policy-facts.ts +196 -0
  69. package/src/policy-fixture.ts +71 -0
  70. package/src/registry.ts +73 -0
  71. package/src/scaffold-fixture.ts +69 -0
  72. package/src/scaffold-typecheck.ts +240 -0
  73. package/src/source-files.ts +38 -0
  74. package/src/table.ts +19 -0
  75. package/src/tasks-facts.ts +113 -0
  76. package/src/templates/action.ts +193 -0
  77. package/src/templates/admin.ts +46 -0
  78. package/src/templates/catalog-json.ts +17 -0
  79. package/src/templates/entity.ts +157 -0
  80. package/src/templates/index.ts +23 -0
  81. package/src/templates/job.ts +148 -0
  82. package/src/templates/locales.ts +93 -0
  83. package/src/templates/naming.ts +97 -0
  84. package/src/templates/policy.ts +120 -0
  85. package/src/templates/query.ts +116 -0
  86. package/src/templates/resource.ts +199 -0
  87. package/src/templates/route.ts +138 -0
  88. package/src/templates/scaffold-app.ts +320 -0
  89. package/src/templates/scaffold-docs.ts +156 -0
  90. package/src/templates/scaffold-i18n.ts +149 -0
  91. package/src/templates/scaffold-icon.ts +54 -0
  92. package/src/templates/scaffold-package-shape.ts +49 -0
  93. package/src/templates/scaffold-repo.ts +427 -0
  94. package/src/test-select.ts +130 -0
  95. package/src/test-shards.ts +188 -0
  96. package/src/thrown-by.ts +24 -0
  97. package/src/ts-scan.ts +217 -0
  98. package/src/verify-step.ts +83 -0
  99. package/src/verify-tests.ts +166 -0
  100. package/src/version-loader.ts +16 -0
  101. package/src/workspace-checks.ts +288 -0
@@ -0,0 +1,16 @@
1
+ // The CLI's own version, read lazily. Lazily because `registry.ts` evaluates every command module
2
+ // at import, and a top-level cross-package import from there is what broke module initialisation
3
+ // order before — so the value is fetched at the call, not at module scope.
4
+
5
+ // Bun has no path-join primitive; `import.meta.dir` is this module's directory in both the
6
+ // checked-out `src/` layout and the published `dist/` one, each one level below the package root.
7
+ import { resolve } from 'node:path';
8
+ import { readPackageVersion } from '@ultimat3/core';
9
+
10
+ /** `@ultimat3/cli`'s own manifest — released in lockstep with the rest of `@ultimat3/*`. */
11
+ export const CLI_MANIFEST = resolve(import.meta.dir, '..', 'package.json');
12
+
13
+ /** Throws `X_INVARIANT` if this package shipped without a version — see `readPackageVersion`. */
14
+ export function loadVersion(): string {
15
+ return readPackageVersion(CLI_MANIFEST);
16
+ }
@@ -0,0 +1,288 @@
1
+ // Three shape rules the gate owns: one file, one job (a hard line ceiling), every workspace
2
+ // package shipping the same contract files, and every published package's tarball matching what
3
+ // its manifest promises. All report findings — a shape rule that is only written down is not a
4
+ // rule (axiom 3).
5
+
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import type { Finding } from './output';
9
+ import { eachSourceFile } from './source-files';
10
+
11
+ export const LINE_CEILING = 500;
12
+
13
+ export const PACKAGE_FILES = ['README.md', 'CLAUDE.md', 'tsconfig.json', 'src/index.ts'] as const;
14
+
15
+ const docs = (code: string): string => `https://ultimate.dev/errors/${code}`;
16
+
17
+ export const tooLongFinding = (path: string, lines: number): Finding => ({
18
+ code: 'X_FILE_TOO_LONG',
19
+ cause: `${path} is ${lines} lines, over the ${LINE_CEILING} line ceiling`,
20
+ fix: `split ${path}: one file, one responsibility`,
21
+ docs: docs('X_FILE_TOO_LONG'),
22
+ at: path,
23
+ });
24
+
25
+ /**
26
+ * A trailing newline terminates the last line, it does not start another one. Counting the split
27
+ * parts instead made the real ceiling 499 and reported every count one too high — every correctly
28
+ * formatted file here ends with a newline, which is exactly the case that was wrong.
29
+ */
30
+ export const countLines = (text: string): number =>
31
+ text === '' ? 0 : text.split('\n').length - (text.endsWith('\n') ? 1 : 0);
32
+
33
+ /** Files are the unit of review: one file, one job, hard ceiling 500 lines. */
34
+ export async function checkFileSizes(root: string): Promise<readonly Finding[]> {
35
+ const findings: Finding[] = [];
36
+ for await (const path of eachSourceFile(root)) {
37
+ const lines = countLines(await Bun.file(join(root, path)).text());
38
+ if (lines > LINE_CEILING) findings.push(tooLongFinding(path, lines));
39
+ }
40
+ return findings;
41
+ }
42
+
43
+ export const missingFileFinding = (dir: string, file: string, scaffolder: boolean): Finding => ({
44
+ code: 'X_PACKAGE_SHAPE',
45
+ cause: `packages/${dir} has no ${file}`,
46
+ fix: scaffolder
47
+ ? `bun run scripts/new-package.ts ${dir} --only ${file}`
48
+ : `add packages/${dir}/${file}, shaped like the one in a sibling package`,
49
+ docs: docs('X_PACKAGE_SHAPE'),
50
+ at: `packages/${dir}/${file}`,
51
+ });
52
+
53
+ /**
54
+ * A published package reports its own version by reading its own `package.json` at runtime
55
+ * (`@ultimat3/core`'s `FRAMEWORK_VERSION`, the CLI's `CLI_VERSION`, every dependency `x new` pins).
56
+ * A manifest with no semver `version` therefore breaks the MCP handshake and every scaffold — so
57
+ * the gate refuses the publish here, where the fix is one line, rather than at someone's install.
58
+ */
59
+ export const SEMVER = /^\d+\.\d+\.\d+(?:[-+][\w.-]+)*$/;
60
+
61
+ export const badVersionFinding = (dir: string, found: unknown): Finding => ({
62
+ code: 'X_PACKAGE_SHAPE',
63
+ cause: `packages/${dir}/package.json has no semver "version" (found ${JSON.stringify(found)})`,
64
+ fix: `set a semver "version" in packages/${dir}/package.json, then: bun run verify`,
65
+ docs: docs('X_PACKAGE_SHAPE'),
66
+ at: `packages/${dir}/package.json`,
67
+ });
68
+
69
+ /**
70
+ * What the lockstep rule needs from one manifest. Read once, in `checkPackageShape`, so the gate
71
+ * opens each package.json a single time.
72
+ */
73
+ export interface ManifestFacts {
74
+ readonly dir: string;
75
+ readonly name: string;
76
+ readonly version: string;
77
+ readonly private: boolean;
78
+ /** Every `@ultimat3/*` pin npm publishes and the range it is pinned to, in declaration order. */
79
+ readonly frameworkDeps: readonly (readonly [name: string, range: string])[];
80
+ /** The manifest's `files`, verbatim — what the tarball promises to carry. */
81
+ readonly files: readonly string[];
82
+ }
83
+
84
+ /**
85
+ * The manifest fields a published package carries to the registry. `devDependencies` is absent
86
+ * deliberately: npm does not install it for a consumer, so a stale one there cannot break anybody's
87
+ * install — while a stale `peerDependencies` or `optionalDependencies` pin resolves at install time
88
+ * exactly like `dependencies` does, and skipping them let skew reach the registry unreported.
89
+ */
90
+ export const PUBLISHED_DEP_FIELDS = [
91
+ 'dependencies',
92
+ 'peerDependencies',
93
+ 'optionalDependencies',
94
+ ] as const;
95
+
96
+ export const versionSkewFinding = (dir: string, found: string, expected: string): Finding => ({
97
+ code: 'X_RELEASE_VERSION_SKEW',
98
+ cause: `packages/${dir} is at ${found}, not the lockstep version ${expected}`,
99
+ fix: `bun run scripts/release.ts --version ${expected}`,
100
+ docs: docs('X_RELEASE_VERSION_SKEW'),
101
+ at: `packages/${dir}/package.json`,
102
+ });
103
+
104
+ export const pinSkewFinding = (
105
+ dir: string,
106
+ dep: string,
107
+ range: string,
108
+ expected: string,
109
+ ): Finding => ({
110
+ code: 'X_RELEASE_VERSION_SKEW',
111
+ cause: `packages/${dir} pins ${dep} at ${range}, not the lockstep version ${expected}`,
112
+ fix: `bun run scripts/release.ts --version ${expected}`,
113
+ docs: docs('X_RELEASE_VERSION_SKEW'),
114
+ at: `packages/${dir}/package.json`,
115
+ });
116
+
117
+ /**
118
+ * Lockstep versioning, as a build error rather than a paragraph in PUBLISHING.md. Two ways a
119
+ * release breaks silently: a package left behind at the old version, and — the one that actually
120
+ * shipped — every package's own version bumped while its sibling pins stayed put, so
121
+ * `@ultimat3/jobs@1.0.0` names `@ultimat3/core@0.0.1`, a version that is not on the registry.
122
+ * npm resolves that at somebody else's install, which is far too late.
123
+ *
124
+ * Private packages are exempt on both counts: a generated app's `packages/*` are private, carry
125
+ * their own version line and depend on the framework by caret range.
126
+ */
127
+ export function checkLockstep(manifests: readonly ManifestFacts[]): readonly Finding[] {
128
+ const published = manifests.filter((manifest) => !manifest.private);
129
+ // `core` is tier 0 and everything depends on it, so it is the version the rest must match.
130
+ const anchor = published.find((manifest) => manifest.dir === 'core') ?? published[0];
131
+ if (anchor === undefined) return [];
132
+ const findings: Finding[] = [];
133
+ for (const manifest of published) {
134
+ if (manifest.version !== anchor.version) {
135
+ findings.push(versionSkewFinding(manifest.dir, manifest.version, anchor.version));
136
+ }
137
+ for (const [dep, range] of manifest.frameworkDeps) {
138
+ if (range !== anchor.version) {
139
+ findings.push(pinSkewFinding(manifest.dir, dep, range, anchor.version));
140
+ }
141
+ }
142
+ }
143
+ return findings;
144
+ }
145
+
146
+ /**
147
+ * The exclusion every published package carries. Exact, not "some pattern that happens to match
148
+ * tests": a second spelling of one rule is the drift the gate exists to prevent.
149
+ */
150
+ export const TEST_EXCLUSION = '!src/**/*.test.ts';
151
+
152
+ export const missingPublishedFileFinding = (dir: string, entry: string): Finding => ({
153
+ code: 'X_PACKAGE_SHAPE',
154
+ cause: `packages/${dir}/package.json ships "${entry}" in "files", but packages/${dir}/${entry} does not exist`,
155
+ fix: `add packages/${dir}/${entry}, or drop "${entry}" from "files" in packages/${dir}/package.json`,
156
+ docs: docs('X_PACKAGE_SHAPE'),
157
+ at: `packages/${dir}/package.json`,
158
+ });
159
+
160
+ export const publishesTestsFinding = (dir: string): Finding => ({
161
+ code: 'X_PACKAGE_SHAPE',
162
+ cause: `packages/${dir}/package.json does not exclude ${TEST_EXCLUSION} from "files"`,
163
+ fix: `add "${TEST_EXCLUSION}" to "files" in packages/${dir}/package.json, after "src"`,
164
+ docs: docs('X_PACKAGE_SHAPE'),
165
+ at: `packages/${dir}/package.json`,
166
+ });
167
+
168
+ /**
169
+ * Reported apart from the exclusion above so the `fix:` stays runnable. Told to "add an entry to
170
+ * `files`" when there is no `files` at all, an author edits a key that is not there — and axiom 4
171
+ * is that an error names the exact fix, not an approximate one.
172
+ */
173
+ export const noFilesAllowlistFinding = (dir: string): Finding => ({
174
+ code: 'X_PACKAGE_SHAPE',
175
+ cause: `packages/${dir}/package.json publishes with no "files" allowlist, so the tarball carries whatever is in the directory`,
176
+ fix: `add "files": ["src", "${TEST_EXCLUSION}", "README.md", "LICENSE"] to packages/${dir}/package.json`,
177
+ docs: docs('X_PACKAGE_SHAPE'),
178
+ at: `packages/${dir}/package.json`,
179
+ });
180
+
181
+ /**
182
+ * What the tarball actually carries, as a build error rather than a paragraph in PUBLISHING.md.
183
+ *
184
+ * npm **silently skips** a `files` entry with no file behind it, so a manifest can promise a
185
+ * `LICENSE` it never ships and publish green: all 28 packages declared `"license": "MIT"`, named
186
+ * `LICENSE` in `files`, and shipped the grant in none of them. It reads as correct in review, in
187
+ * `npm publish`, and on the package page — right up to the point somebody needs the license text
188
+ * that is not in the artifact they received. And a publish cannot be undone.
189
+ *
190
+ * The second half is `src` sweeping in every `*.test.ts` beside it. Tests are the framework's own,
191
+ * they run against a preloaded frozen clock and a sealed network, and a consumer's test runner
192
+ * collecting them is a failure nobody asked for — 393 files, over half of `@ultimat3/cli`'s
193
+ * tarball.
194
+ *
195
+ * Private packages are exempt: a generated app's `packages/*` never reach a registry, carry no
196
+ * `files` and need no license of their own.
197
+ */
198
+ export function checkPublishShape(root: string, manifests: readonly ManifestFacts[]): Finding[] {
199
+ const findings: Finding[] = [];
200
+ for (const manifest of manifests) {
201
+ if (manifest.private) continue;
202
+ if (manifest.files.length === 0) {
203
+ findings.push(noFilesAllowlistFinding(manifest.dir));
204
+ continue;
205
+ }
206
+ for (const entry of manifest.files) {
207
+ // A negation removes files; only a positive literal can promise one that is not there.
208
+ if (entry.startsWith('!') || /[*?[\]]/.test(entry)) continue;
209
+ if (existsSync(join(root, 'packages', manifest.dir, entry))) continue;
210
+ findings.push(missingPublishedFileFinding(manifest.dir, entry));
211
+ }
212
+ if (!manifest.files.includes(TEST_EXCLUSION)) {
213
+ findings.push(publishesTestsFinding(manifest.dir));
214
+ }
215
+ }
216
+ return findings;
217
+ }
218
+
219
+ export function filesOf(manifest: unknown): readonly string[] {
220
+ const record = (typeof manifest === 'object' && manifest !== null ? manifest : {}) as {
221
+ files?: unknown;
222
+ };
223
+ return Array.isArray(record.files)
224
+ ? record.files.filter((entry): entry is string => typeof entry === 'string')
225
+ : [];
226
+ }
227
+
228
+ export function frameworkDepsOf(manifest: unknown): ManifestFacts['frameworkDeps'] {
229
+ const record = (typeof manifest === 'object' && manifest !== null ? manifest : {}) as Record<
230
+ string,
231
+ unknown
232
+ >;
233
+ return PUBLISHED_DEP_FIELDS.flatMap((field) => {
234
+ const deps = record[field];
235
+ if (typeof deps !== 'object' || deps === null) return [];
236
+ return Object.entries(deps).flatMap(([name, range]) =>
237
+ name.startsWith('@ultimat3/') && typeof range === 'string' ? [[name, range] as const] : [],
238
+ );
239
+ });
240
+ }
241
+
242
+ export async function workspacePackages(root: string): Promise<readonly string[]> {
243
+ const dirs: string[] = [];
244
+ for await (const path of new Bun.Glob('packages/*/package.json').scan({
245
+ cwd: root,
246
+ absolute: false,
247
+ })) {
248
+ const dir = path.split('/')[1];
249
+ if (dir !== undefined) dirs.push(dir);
250
+ }
251
+ return dirs.sort();
252
+ }
253
+
254
+ export const hasWorkspacePackages = async (root: string): Promise<boolean> =>
255
+ (await workspacePackages(root)).length > 0;
256
+
257
+ /** Every package ships the same contract files; a missing one is a build error, not a chore. */
258
+ export async function checkPackageShape(root: string): Promise<readonly Finding[]> {
259
+ const scaffolder = existsSync(join(root, 'scripts', 'new-package.ts'));
260
+ const findings: Finding[] = [];
261
+ const facts: ManifestFacts[] = [];
262
+ for (const dir of await workspacePackages(root)) {
263
+ for (const file of PACKAGE_FILES) {
264
+ if (existsSync(join(root, 'packages', dir, file))) continue;
265
+ findings.push(missingFileFinding(dir, file, scaffolder));
266
+ }
267
+ const manifest: unknown = await Bun.file(join(root, 'packages', dir, 'package.json')).json();
268
+ const record = (typeof manifest === 'object' && manifest !== null ? manifest : {}) as {
269
+ name?: unknown;
270
+ version?: unknown;
271
+ private?: unknown;
272
+ };
273
+ const version = record.version;
274
+ if (typeof version !== 'string' || !SEMVER.test(version)) {
275
+ findings.push(badVersionFinding(dir, version));
276
+ continue;
277
+ }
278
+ facts.push({
279
+ dir,
280
+ name: typeof record.name === 'string' ? record.name : `@ultimat3/${dir}`,
281
+ version,
282
+ private: record.private === true,
283
+ frameworkDeps: frameworkDepsOf(manifest),
284
+ files: filesOf(manifest),
285
+ });
286
+ }
287
+ return [...findings, ...checkLockstep(facts), ...checkPublishShape(root, facts)];
288
+ }