@ultimat3/cli 11.2.0 → 12.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 +52 -2
- package/README.md +4 -4
- package/package.json +28 -28
- package/src/app-permissions.ts +0 -0
- package/src/browser-launcher.ts +79 -9
- package/src/cmd-dev.ts +6 -0
- package/src/cmd-doctor.ts +74 -16
- package/src/cmd-errors.ts +6 -0
- package/src/cmd-generate.ts +2 -27
- package/src/cmd-i18n.ts +16 -1
- package/src/cmd-shot-island.ts +5 -0
- package/src/cmd-shot.ts +17 -10
- package/src/compile-externals.ts +7 -4
- package/src/dev-queue.ts +49 -9
- package/src/dev-replica.ts +99 -0
- package/src/dev-roles-fixture.ts +7 -0
- package/src/dev-roles.ts +44 -30
- package/src/dev-sync.ts +45 -1
- package/src/error-codes.ts +16 -0
- package/src/i18n-index.ts +40 -0
- package/src/i18n-registration.ts +43 -1
- package/src/island-bundle.ts +18 -7
- package/src/mcp-errors.ts +12 -0
- package/src/messages.ts +6 -1
- package/src/parse.ts +39 -6
- package/src/port-probe.ts +22 -0
- package/src/serve.ts +7 -1
- package/src/shot-browser.ts +84 -0
- package/src/templates/scaffold-app.ts +2 -0
- package/src/templates/scaffold-docs.ts +10 -4
- package/src/templates/scaffold-http.ts +84 -0
- package/src/templates/scaffold-repo.ts +20 -0
- package/src/templates/scaffold-roles.ts +31 -3
- package/src/verify-checks.ts +36 -0
- package/src/verify-step.ts +8 -0
- package/src/island-solid-production.ts +0 -129
package/src/verify-checks.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { checkAppBoundaries } from './app-boundaries';
|
|
|
19
19
|
import { envExampleFindings } from './app-env';
|
|
20
20
|
import { appManifest, readAppManifest } from './app-manifest';
|
|
21
21
|
import { OPENAPI_FILE, openApiJson } from './app-openapi';
|
|
22
|
+
import { policyFindings } from './app-permissions';
|
|
22
23
|
import { APP_CONFIG_FILE } from './app-root';
|
|
23
24
|
import { checkBudgets, readBuildStats } from './budgets';
|
|
24
25
|
import { checkDestructiveMigrations } from './db-destructive';
|
|
@@ -265,6 +266,15 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
265
266
|
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
266
267
|
run: async (ctx) => fromFindings(await catalogFindings(ctx.root)),
|
|
267
268
|
},
|
|
269
|
+
{
|
|
270
|
+
name: 'policy',
|
|
271
|
+
summary: 'every permission this app grants or requires is one it declares',
|
|
272
|
+
// A repo with no `app.config.ts` is the framework monorepo, which declares no roles and
|
|
273
|
+
// registers no routes — SKIPPED there, never passed, for the reason `i18n` gives: a step that
|
|
274
|
+
// answers `ok` about nothing is the vacuous green these checks exist to refuse.
|
|
275
|
+
applies: async (ctx) => existsSync(join(ctx.root, APP_CONFIG_FILE)),
|
|
276
|
+
run: async (ctx) => fromFindings(await policyFindings(ctx.root)),
|
|
277
|
+
},
|
|
268
278
|
{
|
|
269
279
|
name: 'manifest',
|
|
270
280
|
summary: 'the files an agent reads: generated facts, hand-written conventions, the env example',
|
|
@@ -283,6 +293,7 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
283
293
|
async run(ctx) {
|
|
284
294
|
const agents = await checkAgentsMd(ctx.root);
|
|
285
295
|
const findings = [
|
|
296
|
+
...manifestMissingFindings(ctx.root),
|
|
286
297
|
...(await driftFindings(ctx.root)),
|
|
287
298
|
...(await envExampleFindings(ctx.root)),
|
|
288
299
|
...floorProblemFindings(await readVerifyFloor(ctx.root)),
|
|
@@ -312,6 +323,31 @@ export const VERIFY_STEPS: readonly VerifyStep[] = [
|
|
|
312
323
|
},
|
|
313
324
|
];
|
|
314
325
|
|
|
326
|
+
/**
|
|
327
|
+
* The half that had never been asked: is the file there at all? `driftFindings` returns nothing
|
|
328
|
+
* when it is absent — correctly, it has nothing to compare — and `AGENTS.md` line 3 tells an agent
|
|
329
|
+
* that facts live in `x.manifest.json`, `x dev` prints its path, and `x manifest` is the only
|
|
330
|
+
* thing that writes it. Nothing ran it: after `x new`, `bin/setup` and all thirteen generators,
|
|
331
|
+
* `find . -name '*.manifest.json'` returned nothing while this step reported green (#F7).
|
|
332
|
+
*
|
|
333
|
+
* An app root, never this repo: the framework monorepo emits `framework.manifest.json` and has no
|
|
334
|
+
* `x.manifest.json` to be missing, so the `app.config.ts` is what decides — the same discriminator
|
|
335
|
+
* `drift`, `budgets`, `seo`, `i18n` and `policy` each use.
|
|
336
|
+
*/
|
|
337
|
+
function manifestMissingFindings(root: string): readonly Finding[] {
|
|
338
|
+
if (!existsSync(join(root, APP_CONFIG_FILE))) return [];
|
|
339
|
+
if (existsSync(join(root, MANIFEST_FILENAME))) return [];
|
|
340
|
+
return [
|
|
341
|
+
{
|
|
342
|
+
code: 'X_MANIFEST_MISSING',
|
|
343
|
+
cause: `${MANIFEST_FILENAME} does not exist, so every fact an agent reads about this app — route table, action schemas, policies, error codes — is unavailable`,
|
|
344
|
+
fix: 'x manifest',
|
|
345
|
+
docs: ERROR_DOCS_URL,
|
|
346
|
+
at: MANIFEST_FILENAME,
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
|
|
315
351
|
/** `assertNoDrift` throws `X_MANIFEST_DRIFT`; a step reports, so the error becomes a finding. */
|
|
316
352
|
async function driftFindings(root: string): Promise<readonly Finding[]> {
|
|
317
353
|
const path = join(root, MANIFEST_FILENAME);
|
package/src/verify-step.ts
CHANGED
|
@@ -41,6 +41,14 @@ export const VERIFY_STEP_NAMES = [
|
|
|
41
41
|
// registries that load filled. Until it existed, an app could ship every user-facing string as
|
|
42
42
|
// `⟦key⟧` with `x verify` green, because nothing in the gate ever asked (issue #249).
|
|
43
43
|
'i18n',
|
|
44
|
+
// Twentieth, by the same test `seo` and `i18n` each passed: a rider must ask the SAME question
|
|
45
|
+
// off the same data, and "was this import legal?" is not "does the permission this app grants
|
|
46
|
+
// and requires exist?". Reported under `budgets` it would hand the reader a byte budget for an
|
|
47
|
+
// authz defect (axiom 4). Until it existed, `x new` shipped an app that answered HTTP 500 with
|
|
48
|
+
// X_PERMISSION_UNKNOWN on two of its three routes under a green gate: `defineRoles()` accepts an
|
|
49
|
+
// undeclared grant in silence and `RouteGuard.permission` is a bare string. It costs no second
|
|
50
|
+
// app load — `budgets` already imported every module, and this reads the registries that filled.
|
|
51
|
+
'policy',
|
|
44
52
|
'manifest',
|
|
45
53
|
'roadmap',
|
|
46
54
|
] as const;
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// Every `solid-js` import in an island chunk resolves to Solid's PRODUCTION browser build.
|
|
2
|
-
// `Bun.build({ target: 'browser' })` always adds the `development` export condition and offers no
|
|
3
|
-
// option that removes it — `conditions`, `production`, `env` and `define` were each measured under
|
|
4
|
-
// Bun 1.4 and none of them does — so without this seam an island ships the dev bundle silently.
|
|
5
|
-
|
|
6
|
-
// `node:path` by necessity: Bun ships no path API, and this file resolves a package entry back
|
|
7
|
-
// to the directory its `exports` map is relative to.
|
|
8
|
-
import { dirname, join } from 'node:path';
|
|
9
|
-
import type { BunPlugin } from 'bun';
|
|
10
|
-
import { IslandBuildFailedError } from './errors';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The conditions an island's `solid-js` subpath is resolved under. `development` is the one NOT in
|
|
14
|
-
* the set, which is the whole point of the file; `production` is in it because an island chunk is
|
|
15
|
-
* only ever built to be shipped — `x dev` serves the same chunk the container does, so a second
|
|
16
|
-
* answer here would be a bundle the byte budget never measured.
|
|
17
|
-
*/
|
|
18
|
-
const ISLAND_CONDITIONS: ReadonlySet<string> = new Set([
|
|
19
|
-
'production',
|
|
20
|
-
'browser',
|
|
21
|
-
'module',
|
|
22
|
-
'import',
|
|
23
|
-
'default',
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
/** `solid-js` and its subpaths, and nothing else: Solid is the runtime an island is compiled for. */
|
|
27
|
-
const SOLID_SPECIFIER = /^solid-js(?:\/|$)/;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Node's conditional-exports walk, restricted to what this file needs: the first key of the object
|
|
31
|
-
* that the build's condition set contains, depth-first, with an array as an ordered fallback list.
|
|
32
|
-
* Written out rather than delegated to `Bun.resolveSync` because the ONE thing it has to do
|
|
33
|
-
* differently from Bun's resolver is refuse `development` — and `types` with it, which would
|
|
34
|
-
* otherwise win on Solid's map and hand the bundler a `.d.ts`.
|
|
35
|
-
*/
|
|
36
|
-
export function selectCondition(node: unknown, conditions: ReadonlySet<string>): string | null {
|
|
37
|
-
if (typeof node === 'string') return node;
|
|
38
|
-
if (Array.isArray(node)) {
|
|
39
|
-
for (const alternative of node as readonly unknown[]) {
|
|
40
|
-
const picked = selectCondition(alternative, conditions);
|
|
41
|
-
if (picked !== null) return picked;
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
if (typeof node !== 'object' || node === null) return null;
|
|
46
|
-
for (const [condition, value] of Object.entries(node)) {
|
|
47
|
-
if (!conditions.has(condition)) continue;
|
|
48
|
-
const picked = selectCondition(value, conditions);
|
|
49
|
-
if (picked !== null) return picked;
|
|
50
|
-
}
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** One parse per manifest: an island imports Solid from several files, and every file asks again. */
|
|
55
|
-
const exportsCache = new Map<string, unknown>();
|
|
56
|
-
|
|
57
|
-
async function exportsOf(manifest: string): Promise<unknown> {
|
|
58
|
-
const hit = exportsCache.get(manifest);
|
|
59
|
-
if (hit !== undefined || exportsCache.has(manifest)) return hit;
|
|
60
|
-
const parsed: unknown = JSON.parse(await Bun.file(manifest).text());
|
|
61
|
-
const field =
|
|
62
|
-
typeof parsed === 'object' && parsed !== null && 'exports' in parsed
|
|
63
|
-
? (parsed as { readonly exports?: unknown }).exports
|
|
64
|
-
: undefined;
|
|
65
|
-
exportsCache.set(manifest, field);
|
|
66
|
-
return field;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Test seam: the cache is process-global because `x dev` rebuilds in one process. */
|
|
70
|
-
export function clearSolidExportsCache(): void {
|
|
71
|
-
exportsCache.clear();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The absolute file `specifier` must resolve to, or `null` for "Bun's own answer is already the
|
|
76
|
-
* right one" — which is every subpath Solid declares as a plain string or a pattern, since a
|
|
77
|
-
* declaration with no conditions on it cannot select the development build.
|
|
78
|
-
*/
|
|
79
|
-
export async function solidProductionEntry(
|
|
80
|
-
specifier: string,
|
|
81
|
-
resolveDir: string,
|
|
82
|
-
importer: string,
|
|
83
|
-
): Promise<string | null> {
|
|
84
|
-
let manifest: string;
|
|
85
|
-
try {
|
|
86
|
-
// `solid-js/package.json` is an `exports` entry of Solid's own map, so this reaches the exact
|
|
87
|
-
// copy the island would have imported — not a hoisted sibling at a different version.
|
|
88
|
-
manifest = Bun.resolveSync('solid-js/package.json', resolveDir);
|
|
89
|
-
} catch {
|
|
90
|
-
// Solid is not installed here. Bun's resolver says so, in its own words, naming the importer.
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const field = await exportsOf(manifest);
|
|
94
|
-
const subpath = specifier === 'solid-js' ? '.' : `.${specifier.slice('solid-js'.length)}`;
|
|
95
|
-
if (typeof field !== 'object' || field === null || !(subpath in field)) return null;
|
|
96
|
-
|
|
97
|
-
const entry = selectCondition((field as Record<string, unknown>)[subpath], ISLAND_CONDITIONS);
|
|
98
|
-
const file = entry === null ? null : join(dirname(manifest), entry);
|
|
99
|
-
if (file === null || !(await Bun.file(file).exists())) {
|
|
100
|
-
throw new IslandBuildFailedError({
|
|
101
|
-
file: importer.length > 0 ? importer : specifier,
|
|
102
|
-
logs:
|
|
103
|
-
`${specifier} has no production browser entry: ${manifest} answers ` +
|
|
104
|
-
`${entry === null ? 'nothing' : JSON.stringify(entry)} under ` +
|
|
105
|
-
`[${[...ISLAND_CONDITIONS].join(', ')}], and an island may not ship Solid's ` +
|
|
106
|
-
'development build',
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return file;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The plugin `island-bundle.ts` hands `Bun.build`, beside `solidJsxPlugin`. Stateless apart from
|
|
114
|
-
* the manifest cache, so one frozen descriptor serves every concurrent island build.
|
|
115
|
-
*
|
|
116
|
-
* The absolute path it answers with no longer matches `SOLID_SPECIFIER`, so Solid's own internal
|
|
117
|
-
* `import … from 'solid-js'` is the only re-entry — and that one is wanted: it is how `web.js`
|
|
118
|
-
* reaches `solid.js` rather than `dev.js`.
|
|
119
|
-
*/
|
|
120
|
-
export const solidProductionPlugin: BunPlugin = {
|
|
121
|
-
name: 'ultimate-island-solid-production',
|
|
122
|
-
setup(build): void {
|
|
123
|
-
build.onResolve({ filter: SOLID_SPECIFIER }, async ({ path, importer, resolveDir }) => {
|
|
124
|
-
const from = resolveDir.length > 0 ? resolveDir : dirname(importer);
|
|
125
|
-
const entry = await solidProductionEntry(path, from, importer);
|
|
126
|
-
return entry === null ? undefined : { path: entry };
|
|
127
|
-
});
|
|
128
|
-
},
|
|
129
|
-
};
|