@ultimat3/cli 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +83 -16
- 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 +165 -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 +201 -138
- 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 +84 -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 +4 -3
- 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 +170 -10
- 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,56 @@
|
|
|
1
|
+
// One integer-flag reader for every command that takes one. `Number.parseInt` alone accepts a
|
|
2
|
+
// prefix and answers `NaN` for the rest, and three commands took it bare: `x doctor --port abc`
|
|
3
|
+
// probed `NaN`, which `portFree` reports as free — a check that CANNOT FAIL — `x dev --port abc`
|
|
4
|
+
// handed `NaN` to `Bun.serve` and bound an arbitrary port, and `x test --workers 4abc` ran four.
|
|
5
|
+
|
|
6
|
+
import { BadFlagError } from './errors';
|
|
7
|
+
import type { ParsedArgs } from './parse';
|
|
8
|
+
import { flagString } from './parse';
|
|
9
|
+
|
|
10
|
+
export interface IntFlag {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** The command as it appears in the cause line, e.g. `doctor` for `x doctor`. */
|
|
13
|
+
readonly command: string;
|
|
14
|
+
readonly min: number;
|
|
15
|
+
readonly max?: number;
|
|
16
|
+
/** A runnable invocation carrying a good value — never a `<placeholder>`. */
|
|
17
|
+
readonly example: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const bound = (flag: IntFlag): string =>
|
|
21
|
+
flag.max === undefined ? `>= ${flag.min}` : `from ${flag.min} to ${flag.max}`;
|
|
22
|
+
|
|
23
|
+
/** `/^\d+$/` first: it is the only test that refuses `4abc`, `4.9`, `0x10`, `+4` and ` 4`. */
|
|
24
|
+
export function parseIntFlag(raw: string, flag: IntFlag): number {
|
|
25
|
+
const value = /^\d+$/.test(raw) ? Number.parseInt(raw, 10) : Number.NaN;
|
|
26
|
+
if (
|
|
27
|
+
!Number.isInteger(value) ||
|
|
28
|
+
value < flag.min ||
|
|
29
|
+
(flag.max !== undefined && value > flag.max)
|
|
30
|
+
) {
|
|
31
|
+
throw new BadFlagError({
|
|
32
|
+
flag: flag.name,
|
|
33
|
+
command: flag.command,
|
|
34
|
+
reason: `expects an integer ${bound(flag)}, got "${raw}"`,
|
|
35
|
+
fix: flag.example,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The flag's value, or `undefined` when it was not given. Throws `X_CLI_BAD_FLAG` on a bad one. */
|
|
42
|
+
export function readIntFlag(args: ParsedArgs, flag: IntFlag): number | undefined {
|
|
43
|
+
const raw = flagString(args, flag.name);
|
|
44
|
+
return raw === undefined ? undefined : parseIntFlag(raw, flag);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The same, with a default — for a flag whose spec already declares one. */
|
|
48
|
+
export const intFlagOr = (args: ParsedArgs, flag: IntFlag, fallback: number): number =>
|
|
49
|
+
readIntFlag(args, flag) ?? fallback;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every port flag answers to the same bounds `serve.ts`'s `portValue` already enforces on `PORT`,
|
|
53
|
+
* 0 included — 0 is "let the kernel pick", which is how `x dev --port 0` boots a test server on a
|
|
54
|
+
* free port. Two ranges for one concept is the drift this constant exists to prevent.
|
|
55
|
+
*/
|
|
56
|
+
export const PORT_RANGE = { min: 0, max: 65_535 } as const;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Where the `@ultimat3` packages THIS `x` is pinned against live on disk. One resolver, because
|
|
2
|
+
// two answers to "which framework is installed" is two answers to every question read off it —
|
|
3
|
+
// the docs `x docs` quotes and the `fix:` lines `x errors explain` projects come from the same
|
|
4
|
+
// directory or they describe different builds.
|
|
5
|
+
|
|
6
|
+
// `node:fs`/`node:path` because Bun ships neither: `dirname` walks a resolved module up to the
|
|
7
|
+
// directory that owns it, and `existsSync` is what says which directory that is.
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Deep enough for `src/index.ts` and for any entry an `exports` map could point at, shallow enough
|
|
13
|
+
* that a resolver answering something unexpected stops rather than walking to `/`.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_DEPTH = 6;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolved from the CLI's own dependency on `@ultimat3/core` rather than from the user's cwd:
|
|
19
|
+
* these are the packages this `x` would actually run. Resolution follows the symlink, so a
|
|
20
|
+
* workspace-linked app lands on the same `packages/` this monorepo does — one code path for both,
|
|
21
|
+
* and a directory listing rather than a hardcoded package list, because an app installs the
|
|
22
|
+
* subset it uses.
|
|
23
|
+
*
|
|
24
|
+
* The **exported entry** is what gets resolved, never `@ultimat3/core/package.json`: every package
|
|
25
|
+
* here declares `"exports": { ".": "./src/index.ts" }` and nothing else, so a subpath specifier is
|
|
26
|
+
* asking a resolver for something the package does not publish. Bun 1.3 happens to answer it
|
|
27
|
+
* anyway; a resolver that enforced `exports` would answer `undefined`, and the failure would be
|
|
28
|
+
* silent — `x docs` reporting no installed packages and `x errors explain` reporting that no
|
|
29
|
+
* framework raises the code. Walking up from the entry to the directory that owns its
|
|
30
|
+
* `package.json` depends on nothing but the entry that is already imported.
|
|
31
|
+
*
|
|
32
|
+
* `undefined` means the CLI cannot see its own dependency, which is a broken install and not
|
|
33
|
+
* merely an undocumented one; every caller reports that rather than answering emptily.
|
|
34
|
+
*/
|
|
35
|
+
export function frameworkScopeDir(): string | undefined {
|
|
36
|
+
let dir: string;
|
|
37
|
+
try {
|
|
38
|
+
dir = dirname(Bun.resolveSync('@ultimat3/core', import.meta.dir));
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
|
|
43
|
+
if (existsSync(join(dir, 'package.json'))) return dirname(dir);
|
|
44
|
+
const parent = dirname(dir);
|
|
45
|
+
if (parent === dir) return undefined;
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Which generators exist, and how one is named on a command line. Split from `cmd-generate.ts`
|
|
2
|
+
// because "what a generator emits" and "which spelling reaches it" are two jobs — and the file
|
|
3
|
+
// that held both had reached the 500-line ceiling, one generator short of failing its own gate.
|
|
4
|
+
|
|
5
|
+
import { BadFlagError, MissingPositionalError, UnknownCommandError } from './errors';
|
|
6
|
+
import type { Surface } from './templates';
|
|
7
|
+
|
|
8
|
+
export const GENERATORS = [
|
|
9
|
+
'resource',
|
|
10
|
+
'action',
|
|
11
|
+
'mutator',
|
|
12
|
+
'backfill',
|
|
13
|
+
'job',
|
|
14
|
+
'route',
|
|
15
|
+
'policy',
|
|
16
|
+
'entity',
|
|
17
|
+
'query',
|
|
18
|
+
'task',
|
|
19
|
+
'island',
|
|
20
|
+
'admin:page',
|
|
21
|
+
'guard',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type Generator = (typeof GENERATORS)[number];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A resource slice is app-surface by construction: it ships a live query, a form with a signal,
|
|
28
|
+
* two actions and a policy, and `site/` is the 0kb, never-hydrated surface that may not import
|
|
29
|
+
* `app/`. The documented shape is the slice in `app/` plus a separate public route
|
|
30
|
+
* (`docs/architecture/15-adding-a-feature.md`), so the flag is refused before anything is written
|
|
31
|
+
* rather than emitting a slice that fails the app's own budget and boundary gates.
|
|
32
|
+
*/
|
|
33
|
+
export function assertSurfaceSupported(kind: Generator, surface: Surface, name: string): void {
|
|
34
|
+
if (kind !== 'resource' || surface !== 'site') return;
|
|
35
|
+
throw new BadFlagError({
|
|
36
|
+
flag: 'surface',
|
|
37
|
+
command: 'g resource',
|
|
38
|
+
reason: 'a resource slice is app-surface — site/ ships 0kb JS and may not import app/',
|
|
39
|
+
// The caller's own name, not `<name>`: a `fix:` is copied and run verbatim, and `x g resource
|
|
40
|
+
// <name>` is a shell redirect (`bash: name: No such file or directory`), not a command.
|
|
41
|
+
fix: `x g resource ${name} && x g route ${name} --surface site`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readKind(raw: string | undefined): Generator {
|
|
46
|
+
const kinds: readonly string[] = GENERATORS;
|
|
47
|
+
if (raw !== undefined && kinds.includes(raw)) return raw as Generator;
|
|
48
|
+
throw new UnknownCommandError({
|
|
49
|
+
path: `g ${raw ?? ''}`.trim(),
|
|
50
|
+
known: GENERATORS,
|
|
51
|
+
suggestion: 'g resource',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Two surfaces, spelled exactly. A typo that fell through to `app` would scaffold the wrong one. */
|
|
56
|
+
export function readSurface(raw: string | undefined, kind: Generator, name: string): Surface {
|
|
57
|
+
if (raw === undefined || raw === 'app') return 'app';
|
|
58
|
+
if (raw === 'site') return 'site';
|
|
59
|
+
throw new BadFlagError({
|
|
60
|
+
flag: 'surface',
|
|
61
|
+
command: 'g',
|
|
62
|
+
reason: `"${raw}" is not a surface (site, app)`,
|
|
63
|
+
fix: `x g ${kind} ${name} --surface app`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The missing `<name>` positional. It used to throw `X_CLI_UNKNOWN_COMMAND` — for a command form
|
|
69
|
+
* that IS known — with `fix: "x g route <name>"`, which pasted into a shell is a redirect
|
|
70
|
+
* (`bash: name: No such file or directory`). The code now says what is actually wrong, and the fix
|
|
71
|
+
* is a command that runs.
|
|
72
|
+
*/
|
|
73
|
+
export function readName(raw: string | undefined, kind: Generator): string {
|
|
74
|
+
if (raw !== undefined) return raw;
|
|
75
|
+
throw new MissingPositionalError({
|
|
76
|
+
command: `g ${kind}`,
|
|
77
|
+
positional: 'name',
|
|
78
|
+
example: `x g ${kind} ${EXAMPLE_NAME[kind]}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One runnable example per generator, so the `fix:` is a command and not a shape. */
|
|
83
|
+
const EXAMPLE_NAME: Readonly<Record<Generator, string>> = {
|
|
84
|
+
resource: 'invoice',
|
|
85
|
+
action: 'publish-post',
|
|
86
|
+
mutator: 'rename-post',
|
|
87
|
+
backfill: 'backfill-slugs',
|
|
88
|
+
job: 'send-digest',
|
|
89
|
+
route: 'posts',
|
|
90
|
+
policy: 'post',
|
|
91
|
+
entity: 'post',
|
|
92
|
+
query: 'recent-posts',
|
|
93
|
+
task: 'nightly-digest',
|
|
94
|
+
island: 'counter',
|
|
95
|
+
'admin:page': 'ops',
|
|
96
|
+
guard: 'migration-safety',
|
|
97
|
+
};
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// An app's own convention, made into a build error (axiom 3). A file in `guards/` exports one
|
|
2
|
+
// `guard`; the gate discovers the directory and runs each one inside the `boundaries` step. The
|
|
3
|
+
// framework decides what a guard IS — a function returning findings, held to the error contract —
|
|
4
|
+
// and nothing about what a guard may check.
|
|
5
|
+
|
|
6
|
+
// Bun ships no equivalent for either: `existsSync` answers whether this app has a guards
|
|
7
|
+
// directory, `join` builds the host-separator path, and `pathToFileURL` is the only spelling of an
|
|
8
|
+
// absolute path `import()` accepts on every host.
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { renderCauseValue, renderThrowable } from '@ultimat3/core';
|
|
13
|
+
import { fixProblem } from './error-contract';
|
|
14
|
+
import type { Finding } from './output';
|
|
15
|
+
import type { HostCheck } from './verify-step';
|
|
16
|
+
|
|
17
|
+
/** The directory IS the registration. An app-side list is a list an app can forget to add to. */
|
|
18
|
+
export const GUARD_DIR = 'guards';
|
|
19
|
+
|
|
20
|
+
export interface Guard {
|
|
21
|
+
/** What this app refuses, in one line. It names the rule when the guard itself is the problem. */
|
|
22
|
+
readonly summary: string;
|
|
23
|
+
/**
|
|
24
|
+
* The rule, over the app root. Returns findings — it never prints, never decides an exit code
|
|
25
|
+
* and never throws for a normal result, because `--json`, the step table and the exit code are
|
|
26
|
+
* all projections of what it returns (axiom 2).
|
|
27
|
+
*/
|
|
28
|
+
check(root: string): Promise<readonly Finding[]> | readonly Finding[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
32
|
+
typeof value === 'object' && value !== null;
|
|
33
|
+
|
|
34
|
+
const isGuard = (value: unknown): value is Guard =>
|
|
35
|
+
isRecord(value) &&
|
|
36
|
+
typeof value['summary'] === 'string' &&
|
|
37
|
+
value['summary'].trim() !== '' &&
|
|
38
|
+
typeof value['check'] === 'function';
|
|
39
|
+
|
|
40
|
+
const CODE = /^X_[A-Z0-9_]+$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A value named in a cause, without ever throwing to name it — the one thing this validator may
|
|
44
|
+
* never do is fail while it is explaining a failure, because then a bug in an app's guard reaches
|
|
45
|
+
* its author as a stack trace out of framework internals. The rendering itself is
|
|
46
|
+
* `@ultimat3/core`'s `renderCauseValue`: the local copy this used to hold called `String(value)` on
|
|
47
|
+
* an unnarrowed `unknown`, so a guard returning an object with a throwing `toString` destroyed the
|
|
48
|
+
* refusal — the case a scan over `String(` cannot see, because the call is one helper away.
|
|
49
|
+
* The `typeof` prefix stays: "object null" and "number 42" say what a bare literal does not.
|
|
50
|
+
*/
|
|
51
|
+
const shown = (value: unknown): string =>
|
|
52
|
+
typeof value === 'string' ? `"${value}"` : `${typeof value} ${renderCauseValue(value)}`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Why a returned value is not a finding, or `undefined` when it is one. The `fix:` half is
|
|
56
|
+
* `fixProblem` — the identical rule `x verify`'s `errors` step applies to every shipped `fix:` in
|
|
57
|
+
* the framework — because a mechanism for producing errors that are not instructions is worse than
|
|
58
|
+
* no mechanism. It runs on the returned value rather than on the source, which is the half a
|
|
59
|
+
* static scan cannot reach: a `fix` assembled at run time has no literal to read.
|
|
60
|
+
*/
|
|
61
|
+
export function findingProblem(value: unknown): string | undefined {
|
|
62
|
+
if (!isRecord(value)) return `${shown(value)} is not a finding object`;
|
|
63
|
+
const code = value['code'];
|
|
64
|
+
if (typeof code !== 'string' || !CODE.test(code)) {
|
|
65
|
+
return `${shown(code)} is not an X_SCREAMING_SNAKE code, so nothing can explain it`;
|
|
66
|
+
}
|
|
67
|
+
const cause = value['cause'];
|
|
68
|
+
if (typeof cause !== 'string' || cause.trim() === '') return `${code} states no cause`;
|
|
69
|
+
const fix = value['fix'];
|
|
70
|
+
if (typeof fix !== 'string') return `${code} carries no fix line`;
|
|
71
|
+
return fixProblem(fix);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Only what a finding may carry, so a guard cannot smuggle fields the renderers never show. */
|
|
75
|
+
function findingOf(value: Record<string, unknown>, at: string): Finding {
|
|
76
|
+
const docs = value['docs'];
|
|
77
|
+
const located = value['at'];
|
|
78
|
+
return {
|
|
79
|
+
code: value['code'] as string,
|
|
80
|
+
cause: value['cause'] as string,
|
|
81
|
+
fix: value['fix'] as string,
|
|
82
|
+
...(typeof docs === 'string' ? { docs } : {}),
|
|
83
|
+
at: typeof located === 'string' && located !== '' ? located : at,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Every guard file, app-root-relative and sorted, so two machines report the same findings in the
|
|
89
|
+
* same order. A `*.test.ts` beside a guard is its test, never a second guard — the generator emits
|
|
90
|
+
* one, and importing it would run the suite inside the gate.
|
|
91
|
+
*/
|
|
92
|
+
export async function guardPaths(root: string): Promise<readonly string[]> {
|
|
93
|
+
const dir = join(root, GUARD_DIR);
|
|
94
|
+
if (!existsSync(dir)) return [];
|
|
95
|
+
const paths: string[] = [];
|
|
96
|
+
for await (const entry of new Bun.Glob('*.{ts,tsx}').scan({ cwd: dir, absolute: false })) {
|
|
97
|
+
const path = entry.split('\\').join('/');
|
|
98
|
+
if (/\.(?:test|d)\.tsx?$/.test(path)) continue;
|
|
99
|
+
paths.push(`${GUARD_DIR}/${path}`);
|
|
100
|
+
}
|
|
101
|
+
return paths.sort();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const failed = (path: string, cause: string): Finding => ({
|
|
105
|
+
code: 'X_GUARD_FAILED',
|
|
106
|
+
cause,
|
|
107
|
+
fix: `return a finding from ${path} instead of throwing, then: x verify`,
|
|
108
|
+
docs: 'https://ultimate.dev/errors/X_GUARD_FAILED',
|
|
109
|
+
at: path,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const invalid = (path: string, cause: string): Finding => ({
|
|
113
|
+
code: 'X_GUARD_INVALID',
|
|
114
|
+
cause,
|
|
115
|
+
fix: `export a \`guard\` object — { summary, check } — from ${path}, then: x verify`,
|
|
116
|
+
docs: 'https://ultimate.dev/errors/X_GUARD_INVALID',
|
|
117
|
+
at: path,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const findingInvalid = (path: string, cause: string): Finding => ({
|
|
121
|
+
code: 'X_GUARD_FINDING_INVALID',
|
|
122
|
+
cause: `${path} returned a finding that is not one: ${cause}`,
|
|
123
|
+
fix: `rewrite what ${path} returns as a code, a cause and a fix naming a command or a file, then: x verify`,
|
|
124
|
+
docs: 'https://ultimate.dev/errors/X_GUARD_FINDING_INVALID',
|
|
125
|
+
at: path,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/** Same reason as `shown`: an app's guard may throw a value that fights every way of reading it. */
|
|
129
|
+
const messageOf = (error: unknown): string => renderThrowable(error);
|
|
130
|
+
|
|
131
|
+
/** One guard: import it, run it, and hold what it returns to the contract. Never throws. */
|
|
132
|
+
async function runGuard(root: string, path: string): Promise<readonly Finding[]> {
|
|
133
|
+
let loaded: unknown;
|
|
134
|
+
try {
|
|
135
|
+
loaded = await import(pathToFileURL(join(root, path)).href);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return [failed(path, `${path} could not be imported: ${messageOf(error)}`)];
|
|
138
|
+
}
|
|
139
|
+
const exported = isRecord(loaded) ? loaded['guard'] : undefined;
|
|
140
|
+
if (!isGuard(exported)) {
|
|
141
|
+
return [
|
|
142
|
+
invalid(
|
|
143
|
+
path,
|
|
144
|
+
exported === undefined
|
|
145
|
+
? `${path} exports no \`guard\`, so a file in ${GUARD_DIR}/ enforces nothing`
|
|
146
|
+
: `${path} exports a \`guard\` with no summary and no check()`,
|
|
147
|
+
),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
let returned: unknown;
|
|
151
|
+
try {
|
|
152
|
+
returned = await exported.check(root);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return [failed(path, `${path} ("${exported.summary}") threw: ${messageOf(error)}`)];
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(returned)) {
|
|
157
|
+
return [findingInvalid(path, `check() answered ${typeof returned}, not a list of findings`)];
|
|
158
|
+
}
|
|
159
|
+
const findings: Finding[] = [];
|
|
160
|
+
for (const candidate of returned as readonly unknown[]) {
|
|
161
|
+
// Reading a candidate can throw on its own — a getter that raises, a proxy that refuses — and
|
|
162
|
+
// `findingProblem` is total only for values it can read. Per candidate, so one unreadable
|
|
163
|
+
// entry costs its own line and not the readable findings beside it.
|
|
164
|
+
try {
|
|
165
|
+
const problem = findingProblem(candidate);
|
|
166
|
+
if (problem !== undefined) findings.push(findingInvalid(path, problem));
|
|
167
|
+
else findings.push(findingOf(candidate as Record<string, unknown>, path));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
findings.push(findingInvalid(path, `it could not be read: ${messageOf(error)}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Every guard this app declares, as findings the step it rides on adds to its own. Typed as a
|
|
177
|
+
* `HostCheck` because that is exactly the seam's shape — a rule the repo enforces on itself,
|
|
178
|
+
* contributed to a step that already exists. A guard can never add, remove, reorder or skip a
|
|
179
|
+
* step, which is what keeps "green" meaning one thing (axiom 5) while the app still gets to make
|
|
180
|
+
* its own convention a build error.
|
|
181
|
+
*/
|
|
182
|
+
export const guardFindings: HostCheck = async (root) => {
|
|
183
|
+
const findings: Finding[] = [];
|
|
184
|
+
for (const path of await guardPaths(root)) findings.push(...(await runGuard(root, path)));
|
|
185
|
+
return findings;
|
|
186
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// on these, and a barrel that re-exports everything would make every internal a compatibility
|
|
3
3
|
// promise.
|
|
4
4
|
|
|
5
|
+
/** The app's own API over HTTP — the one table `x dev` and a container both mount. */
|
|
6
|
+
export { apiRoutes } from './api-routes';
|
|
5
7
|
export type { BoundaryCode, SourceFile } from './app-boundaries';
|
|
6
8
|
export {
|
|
7
9
|
appImportGraph,
|
|
@@ -32,7 +34,8 @@ export {
|
|
|
32
34
|
readTarget,
|
|
33
35
|
requireEntry,
|
|
34
36
|
} from './cmd-build';
|
|
35
|
-
export {
|
|
37
|
+
export { dbCommand } from './cmd-db';
|
|
38
|
+
export { runBranchCommand } from './cmd-db-branch';
|
|
36
39
|
export type { DeployPlan } from './cmd-deploy';
|
|
37
40
|
export { deployCommand, planDeploy } from './cmd-deploy';
|
|
38
41
|
export type { DevServer, StartDevOptions } from './cmd-dev';
|
|
@@ -50,14 +53,30 @@ export type { McpHttpServer } from './cmd-mcp';
|
|
|
50
53
|
export { mcpCommand, startMcpHttp } from './cmd-mcp';
|
|
51
54
|
export type { NewAppOptions, WrittenApp } from './cmd-new';
|
|
52
55
|
export { newCommand, planNewApp, writeNewApp } from './cmd-new';
|
|
53
|
-
export type { PlannedCommand } from './cmd-planned';
|
|
54
|
-
export {
|
|
56
|
+
export type { PlannedCommand, PlannedSubcommand } from './cmd-planned';
|
|
57
|
+
export {
|
|
58
|
+
PLANNED_COMMANDS,
|
|
59
|
+
PLANNED_SUBCOMMANDS,
|
|
60
|
+
plannedCommands,
|
|
61
|
+
plannedSubcommand,
|
|
62
|
+
} from './cmd-planned';
|
|
55
63
|
export { actionsCommand, entitiesCommand, queriesCommand } from './cmd-registries';
|
|
56
64
|
export { renderRouteTable, routesCommand } from './cmd-routes';
|
|
57
|
-
export {
|
|
65
|
+
export { testCommand } from './cmd-test';
|
|
58
66
|
export { runVerify, VERIFY_STEPS, verifyCommand, verifyStepNames } from './cmd-verify';
|
|
59
67
|
export type { CliCommand, CommandContext } from './command';
|
|
60
68
|
export { failed, ok } from './command';
|
|
69
|
+
export type { BranchRow, BranchSubcommand } from './db-branch';
|
|
70
|
+
export {
|
|
71
|
+
BRANCH_SUBCOMMANDS,
|
|
72
|
+
branchDatabaseName,
|
|
73
|
+
branchNameOf,
|
|
74
|
+
isBranchSubcommand,
|
|
75
|
+
pgliteBranchName,
|
|
76
|
+
previewUrl,
|
|
77
|
+
} from './db-branch';
|
|
78
|
+
export type { GeneratedFiles, GenerateMigrationOptions } from './db-generate';
|
|
79
|
+
export { generateAppMigration, migrationSql } from './db-generate';
|
|
61
80
|
export type { AssetRoutesOptions } from './dev-assets';
|
|
62
81
|
export {
|
|
63
82
|
assetRoutes,
|
|
@@ -80,7 +99,8 @@ export type { DevServices, ServiceBinding } from './dev-services';
|
|
|
80
99
|
export { describeServices, resolveServices } from './dev-services';
|
|
81
100
|
export type { DispatchOptions } from './dispatch';
|
|
82
101
|
export { dispatch } from './dispatch';
|
|
83
|
-
export {
|
|
102
|
+
export type { DeclaredEntityCount } from './drift';
|
|
103
|
+
export { checkSourceDrift, recordedHashes, schemaHash, writeSchemaHash } from './drift';
|
|
84
104
|
export type { ErrorCatalog } from './error-catalog';
|
|
85
105
|
export {
|
|
86
106
|
buildErrorCatalog,
|
|
@@ -89,6 +109,8 @@ export {
|
|
|
89
109
|
registeredErrorCodes,
|
|
90
110
|
resetErrorCatalog,
|
|
91
111
|
} from './error-catalog';
|
|
112
|
+
export type { CliErrorCode } from './error-codes';
|
|
113
|
+
export { CLI_ERROR_CODES, CLI_ERROR_TITLES } from './error-codes';
|
|
92
114
|
export {
|
|
93
115
|
BANNED_PHRASES,
|
|
94
116
|
COMMAND_TOKENS,
|
|
@@ -102,19 +124,26 @@ export {
|
|
|
102
124
|
RESERVED_HEADING,
|
|
103
125
|
staticFix,
|
|
104
126
|
} from './error-contract';
|
|
105
|
-
export type {
|
|
127
|
+
export type { CodeFixIndex, CodeFixScan } from './error-fixes';
|
|
128
|
+
export {
|
|
129
|
+
codeFixes,
|
|
130
|
+
codeFixScan,
|
|
131
|
+
loadCodeFixes,
|
|
132
|
+
resetCodeFixes,
|
|
133
|
+
scanScopeFixes,
|
|
134
|
+
} from './error-fixes';
|
|
106
135
|
export {
|
|
107
136
|
BadFlagError,
|
|
108
137
|
BuildEntryMissingError,
|
|
109
138
|
BunVersionError,
|
|
110
139
|
CatalogExistsError,
|
|
111
|
-
CLI_ERROR_CODES,
|
|
112
|
-
CLI_ERROR_TITLES,
|
|
113
140
|
CliNotImplementedError,
|
|
114
141
|
DeclarationUnknownError,
|
|
115
142
|
ErrorCodeUnknownError,
|
|
116
143
|
FixTargetUnknownError,
|
|
117
144
|
JobUnknownError,
|
|
145
|
+
MissingPositionalError,
|
|
146
|
+
MissingSubcommandError,
|
|
118
147
|
NoTestFilesError,
|
|
119
148
|
NotInAppError,
|
|
120
149
|
PortInvalidError,
|
|
@@ -124,6 +153,16 @@ export {
|
|
|
124
153
|
} from './errors';
|
|
125
154
|
export type { ExecOptions, ExecResult, Runner } from './exec';
|
|
126
155
|
export { exec, execOutput } from './exec';
|
|
156
|
+
export type { CitationFault, CitationRules, CommandCatalog, FixCitation } from './fix-command';
|
|
157
|
+
export {
|
|
158
|
+
citationFault,
|
|
159
|
+
citationProblem,
|
|
160
|
+
citedCommandProblem,
|
|
161
|
+
fixCitations,
|
|
162
|
+
loadCommandCatalog,
|
|
163
|
+
} from './fix-command';
|
|
164
|
+
export type { Guard } from './guards';
|
|
165
|
+
export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
|
|
127
166
|
export type { DrainFailure, DrainOutcome, DrainSkip } from './jobs-drain';
|
|
128
167
|
export { drainJobs } from './jobs-drain';
|
|
129
168
|
export type { JobsListFilter, JobsListResult } from './jobs-report';
|
|
@@ -134,7 +173,14 @@ export { createDevMcpServer, DEV_TOOL_SCOPES, localCaller } from './mcp-host';
|
|
|
134
173
|
export { messageKeys, msg } from './messages';
|
|
135
174
|
export type { MetricsEndpoint, MetricsEndpointOptions } from './metrics-endpoint';
|
|
136
175
|
export { DEFAULT_METRICS_PORT, startMetricsEndpoint } from './metrics-endpoint';
|
|
137
|
-
export {
|
|
176
|
+
export {
|
|
177
|
+
hashFileName,
|
|
178
|
+
MIGRATIONS_DIR,
|
|
179
|
+
migrationName,
|
|
180
|
+
parseMigrationSql,
|
|
181
|
+
readMigrations,
|
|
182
|
+
snapshotFileName,
|
|
183
|
+
} from './migrations';
|
|
138
184
|
export type { CommandResult, Finding, JsonValue, StepResult } from './output';
|
|
139
185
|
export {
|
|
140
186
|
exitCodeFor,
|
|
@@ -148,9 +194,14 @@ export {
|
|
|
148
194
|
} from './output';
|
|
149
195
|
export type { CommandSpec, FlagSpec, ParsedArgs } from './parse';
|
|
150
196
|
export { flagBool, flagList, flagString, GLOBAL_FLAGS, nearest, parseArgs } from './parse';
|
|
151
|
-
export type {
|
|
197
|
+
export type {
|
|
198
|
+
PrerenderedPage,
|
|
199
|
+
PrerenderOptions,
|
|
200
|
+
PrerenderReport,
|
|
201
|
+
UnmeasuredRoute,
|
|
202
|
+
} from './prerender';
|
|
152
203
|
export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
|
|
153
|
-
export {
|
|
204
|
+
export { COMMANDS, cliVersion, commandFor, SPECS } from './registry';
|
|
154
205
|
export type { MigratedApp, ServedApp, ServeOptions, StartedApp } from './serve';
|
|
155
206
|
export {
|
|
156
207
|
CONTAINER_BINDING,
|
|
@@ -169,19 +220,36 @@ export {
|
|
|
169
220
|
isVendored,
|
|
170
221
|
SOURCE_GLOBS,
|
|
171
222
|
} from './source-files';
|
|
223
|
+
export type { TestCounts } from './test-counts';
|
|
224
|
+
export { countsOf } from './test-counts';
|
|
172
225
|
export type { TestFile } from './test-select';
|
|
173
226
|
export { belongsToType, discoverTests, sampleFiles } from './test-select';
|
|
174
227
|
export type { ReproduceOptions, RunShardsOptions, Shard } from './test-shards';
|
|
175
228
|
export { planShards, quoteArg, reproduceFor, runShards, shardArgs } from './test-shards';
|
|
176
|
-
export
|
|
229
|
+
export { availableCpus, defaultWorkers, WORKER_CEILING } from './test-workers';
|
|
230
|
+
export type { CodeFixSite, CodeSite, FixSite, SourceSite } from './ts-scan';
|
|
177
231
|
export {
|
|
178
232
|
isCodeRegistry,
|
|
179
233
|
maskLiterals,
|
|
180
234
|
scanBorrowedCodes,
|
|
235
|
+
scanCodeFixSites,
|
|
181
236
|
scanCodes,
|
|
182
237
|
scanFixes,
|
|
183
238
|
stripComments,
|
|
184
239
|
} from './ts-scan';
|
|
240
|
+
// The one spelling rule for a `references` entry. Exported because the two gate scripts ask the
|
|
241
|
+
// same question this package's `package-shape` step does, and three answers is a duplicate entry.
|
|
242
|
+
export { normalizeReferencePath } from './tsconfig-references';
|
|
243
|
+
export type { VerifyFloor } from './verify-floor';
|
|
244
|
+
export {
|
|
245
|
+
floorProblemFindings,
|
|
246
|
+
floorRequires,
|
|
247
|
+
parseVerifyFloor,
|
|
248
|
+
readVerifyFloor,
|
|
249
|
+
skippedSuiteFinding,
|
|
250
|
+
VERIFY_FLOOR_FILE,
|
|
251
|
+
vanishedSuiteFinding,
|
|
252
|
+
} from './verify-floor';
|
|
185
253
|
export type {
|
|
186
254
|
HostCheck,
|
|
187
255
|
StepOutcome,
|
|
@@ -191,8 +259,8 @@ export type {
|
|
|
191
259
|
} from './verify-step';
|
|
192
260
|
export { VERIFY_STEP_NAMES } from './verify-step';
|
|
193
261
|
export type { TestType } from './verify-tests';
|
|
194
|
-
export { TEST_STEPS, TEST_TYPES, testStepCommand,
|
|
195
|
-
export type { ManifestFacts } from './workspace-checks';
|
|
262
|
+
export { TEST_STEPS, TEST_TYPES, testStepCommand, typeFiltersOf } from './verify-tests';
|
|
263
|
+
export type { ManifestFacts, PackageShapeOptions } from './workspace-checks';
|
|
196
264
|
export {
|
|
197
265
|
checkFileSizes,
|
|
198
266
|
checkLockstep,
|
|
@@ -201,5 +269,7 @@ export {
|
|
|
201
269
|
hasWorkspacePackages,
|
|
202
270
|
LINE_CEILING,
|
|
203
271
|
PACKAGE_FILES,
|
|
272
|
+
SEMVER,
|
|
204
273
|
workspacePackages,
|
|
205
274
|
} from './workspace-checks';
|
|
275
|
+
export { writeLine } from './write-line';
|