@ultimat3/cli 6.0.0 → 8.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 (91) hide show
  1. package/CLAUDE.md +65 -5
  2. package/README.md +8 -3
  3. package/package.json +25 -24
  4. package/src/affected.ts +320 -0
  5. package/src/app-boundaries.ts +55 -5
  6. package/src/bin.ts +6 -3
  7. package/src/browser-launcher.ts +109 -0
  8. package/src/ci-log.ts +0 -0
  9. package/src/ci-runs.ts +179 -0
  10. package/src/cmd-affected.ts +109 -0
  11. package/src/cmd-build.ts +29 -3
  12. package/src/cmd-ci.ts +273 -0
  13. package/src/cmd-db-backfill.ts +240 -0
  14. package/src/cmd-db-branch.ts +3 -2
  15. package/src/cmd-db.ts +35 -156
  16. package/src/cmd-deploy.ts +37 -3
  17. package/src/cmd-dev.ts +7 -1
  18. package/src/cmd-errors.ts +2 -3
  19. package/src/cmd-fix.ts +3 -3
  20. package/src/cmd-i18n.ts +67 -5
  21. package/src/cmd-jobs.ts +27 -4
  22. package/src/cmd-mcp.ts +18 -9
  23. package/src/cmd-new.ts +91 -4
  24. package/src/cmd-policy.ts +3 -2
  25. package/src/cmd-pr.ts +359 -0
  26. package/src/cmd-registries.ts +3 -2
  27. package/src/cmd-shot.ts +382 -0
  28. package/src/cmd-tasks.ts +9 -4
  29. package/src/cmd-test.ts +96 -7
  30. package/src/cmd-verify.ts +47 -6
  31. package/src/dev-cache.ts +1 -1
  32. package/src/dev-lock.ts +124 -12
  33. package/src/dev-queue.ts +12 -7
  34. package/src/dev-replicator.ts +3 -7
  35. package/src/dev-roles-fixture.ts +1 -1
  36. package/src/dev-roles.ts +40 -8
  37. package/src/dev-runtime.ts +96 -4
  38. package/src/dev-sync.ts +9 -4
  39. package/src/dispatch.ts +35 -5
  40. package/src/drift.ts +52 -7
  41. package/src/error-codes.ts +21 -0
  42. package/src/framework-scope.ts +57 -5
  43. package/src/generate-kinds.ts +19 -1
  44. package/src/gh-target.ts +118 -0
  45. package/src/gh.ts +204 -0
  46. package/src/i18n-registration.ts +67 -4
  47. package/src/index.ts +38 -1
  48. package/src/island-bundle.ts +62 -3
  49. package/src/island-solid-production.ts +129 -0
  50. package/src/island-styles.ts +41 -0
  51. package/src/jobs-report.ts +10 -13
  52. package/src/mcp-errors.ts +12 -0
  53. package/src/messages.ts +76 -0
  54. package/src/output.ts +22 -2
  55. package/src/parse.ts +81 -37
  56. package/src/pr-threads.ts +291 -0
  57. package/src/prerender.ts +52 -10
  58. package/src/realtime-browser-probe-fixture.ts +9 -0
  59. package/src/registry.ts +8 -0
  60. package/src/runtime-overrides.ts +11 -3
  61. package/src/shot-settle.ts +57 -0
  62. package/src/shot-verdict.ts +360 -0
  63. package/src/static-report.ts +219 -0
  64. package/src/sync-authenticator.ts +86 -14
  65. package/src/templates/guard-bare-error.ts +122 -0
  66. package/src/templates/guard-raw-colour.ts +138 -0
  67. package/src/templates/guard-untranslated-string.ts +138 -0
  68. package/src/templates/guard-unzoned-date.ts +142 -0
  69. package/src/templates/index.ts +4 -0
  70. package/src/templates/island-fixture.ts +76 -0
  71. package/src/templates/island.ts +130 -18
  72. package/src/templates/resource-form-island.ts +279 -0
  73. package/src/templates/resource.ts +20 -41
  74. package/src/templates/route.ts +15 -2
  75. package/src/templates/scaffold-app.ts +13 -78
  76. package/src/templates/scaffold-container.ts +30 -4
  77. package/src/templates/scaffold-db-package.ts +46 -7
  78. package/src/templates/scaffold-docs.ts +24 -13
  79. package/src/templates/scaffold-entries.ts +131 -0
  80. package/src/templates/scaffold-guards.ts +26 -0
  81. package/src/templates/scaffold-mcp-package.ts +35 -2
  82. package/src/templates/scaffold-package-shape.ts +7 -2
  83. package/src/templates/scaffold-repo.ts +37 -6
  84. package/src/test-select.ts +4 -3
  85. package/src/test-shards.ts +19 -3
  86. package/src/verify-checks.ts +11 -1
  87. package/src/verify-run.ts +25 -3
  88. package/src/verify-step.ts +11 -2
  89. package/src/verify-tests.ts +11 -3
  90. package/src/workspace-graph.ts +241 -0
  91. package/src/write-line.ts +23 -5
@@ -12,6 +12,7 @@ import {
12
12
  catalogRegistrationGaps,
13
13
  catalogsNeverRegistered,
14
14
  catalogUnregistered,
15
+ pluralVariantsOf,
15
16
  registeredLocales,
16
17
  } from '@ultimat3/i18n';
17
18
  import { loadApp } from './app-load';
@@ -105,9 +106,68 @@ export async function checkRegistration(input: RegistrationInput): Promise<Regis
105
106
  }
106
107
 
107
108
  /**
108
- * The file half: a key source uses that a locale's catalog does not define. Built here rather than
109
- * in `cmd-i18n.ts` because `x verify`'s `i18n` step reports the same finding, and a second
110
- * construction of it is two renderers of one fact waiting to drift.
109
+ * `⟦key⟧` — `@ultimat3/i18n`'s own loud miss, spelled ONCE for the whole CLI. `x i18n sync <default>`
110
+ * writes it and the two checks below refuse it, so a second spelling would be a placeholder one
111
+ * half of this package writes and the other half cannot see.
112
+ */
113
+ const MISS_OPEN = '\u27E6';
114
+ const MISS_CLOSE = '\u27E7';
115
+
116
+ /** What `x i18n sync` seeds a key with when there is no catalog above it to copy a value from. */
117
+ export const loudMiss = (key: string): string => `${MISS_OPEN}${key}${MISS_CLOSE}`;
118
+
119
+ /**
120
+ * A value that is a placeholder rather than a translation. Any `⟦…⟧`, not just `⟦<this key>⟧`:
121
+ * an author who renames a key and leaves the old marker behind still ships a placeholder.
122
+ */
123
+ export const isLoudMiss = (value: string | undefined): boolean =>
124
+ value?.startsWith(MISS_OPEN) === true && value.endsWith(MISS_CLOSE);
125
+
126
+ /**
127
+ * Whether `locale` answers `key` with a placeholder. Every spelling `definesKey` accepts is
128
+ * checked — `pluralVariantsOf` is `@ultimat3/i18n`'s own list, the same one `auditCatalogs` uses,
129
+ * so "defined" and "defined with a real string" can never disagree about which entries count.
130
+ * `some`, not `every`: a plural family with one untranslated category renders `⟦items_many⟧` on
131
+ * exactly the rows that hit it.
132
+ */
133
+ const answersWithPlaceholder = (catalog: Catalog, key: string): boolean =>
134
+ [key, ...pluralVariantsOf(key)].some(
135
+ (candidate) => Object.hasOwn(catalog, candidate) && isLoudMiss(catalog[candidate]),
136
+ );
137
+
138
+ /**
139
+ * The audit, with every placeholder counted as the missing key it stands in for.
140
+ *
141
+ * `auditCatalogs` asks `Object.hasOwn` and nothing else, so a key present with ANY value is not
142
+ * missing — including `⟦key⟧`, which is the value `x i18n sync <defaultLocale>` writes for every
143
+ * gap it closes. Without this, following `X_CATALOG_MISSING_KEYS`'s own `fix:` turns the `i18n`
144
+ * gate step green over strings no human has ever read: issue #249's ending, reached by running the
145
+ * command the error recommends. The hole predates the seeding — a hand-written `"TODO"` bought the
146
+ * same green — but one command now writes sixteen of them, so it is a hole with a shortcut to it.
147
+ *
148
+ * Applied HERE and not in `auditCatalogs`: `⟦…⟧` is what the CLI writes, and `@ultimat3/i18n`'s
149
+ * audit answering "is this key defined" is a different question from "is this app shippable".
150
+ */
151
+ export function withPlaceholdersMissing(
152
+ report: ExtractReport,
153
+ catalogs: Readonly<Record<Locale, Catalog>>,
154
+ ): ExtractReport {
155
+ const locales = report.locales.map((audit) => {
156
+ const catalog = catalogs[audit.locale] ?? {};
157
+ const known = new Set(audit.missing);
158
+ const placeheld = report.used.filter(
159
+ (key) => !known.has(key) && answersWithPlaceholder(catalog, key),
160
+ );
161
+ if (placeheld.length === 0) return audit;
162
+ return { ...audit, missing: [...audit.missing, ...placeheld].sort() };
163
+ });
164
+ return { ...report, locales, ok: locales.every((audit) => audit.missing.length === 0) };
165
+ }
166
+
167
+ /**
168
+ * The file half: a key source uses that a locale's catalog does not define, or defines with a
169
+ * placeholder. Built here rather than in `cmd-i18n.ts` because `x verify`'s `i18n` step reports
170
+ * the same finding, and a second construction of it is two renderers of one fact waiting to drift.
111
171
  */
112
172
  export function missingKeyFindings(report: ExtractReport): readonly Finding[] {
113
173
  return report.locales
@@ -126,5 +186,8 @@ export function missingKeyFindings(report: ExtractReport): readonly Finding[] {
126
186
  export async function catalogFindings(root: string): Promise<readonly Finding[]> {
127
187
  const { report, catalogs, extraction, ignoreUnused } = await auditApp(root);
128
188
  const registration = await checkRegistration({ root, catalogs, extraction, ignoreUnused });
129
- return [...missingKeyFindings(report), ...registration.findings];
189
+ return [
190
+ ...missingKeyFindings(withPlaceholdersMissing(report, catalogs)),
191
+ ...registration.findings,
192
+ ];
130
193
  }
package/src/index.ts CHANGED
@@ -60,6 +60,12 @@ export {
60
60
  plannedCommands,
61
61
  plannedSubcommand,
62
62
  } from './cmd-planned';
63
+ // `shotCommand`, `prCommand` and `ciCommand` are deliberately NOT re-exported here. They reach
64
+ // `x` through `registry.ts`, which is the only thing that makes a command exist — and the barrel
65
+ // is the surface an APP imports. Exporting them puts `cmd-shot.ts` in the module graph of every
66
+ // app that imports `@ultimat3/cli`, which then has to resolve `@ultimat3/scraping` — a browser
67
+ // driver it never uses. Measured: it reds `tsc -b` on `dummy/social-media-clone` with five
68
+ // TS2307s in files that app never calls. The app path does not pay for the tool path.
63
69
  export { actionsCommand, entitiesCommand, queriesCommand } from './cmd-registries';
64
70
  export { renderRouteTable, routesCommand } from './cmd-routes';
65
71
  export { testCommand } from './cmd-test';
@@ -177,6 +183,13 @@ export type { DeclaredFlag } from './flag-reads';
177
183
  export { checkFlagReads, declaredFlags, readsFlag } from './flag-reads';
178
184
  export type { Guard } from './guards';
179
185
  export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
186
+ // The island bundler, and only its entry point. An island is the one module Ultimate ships to a
187
+ // browser, so an app has to be able to build one to TEST one — `mountIsland` from
188
+ // `@ultimat3/testing` takes this function as its `build` parameter (issue #260). `discoverIslands`,
189
+ // `islandBundle`, `writeIslands`, `ISLAND_BASE_PATH` and `ISLAND_GLOB` stay internal: they are
190
+ // `x build`'s and `x dev`'s wiring, and every name here is a semver promise forever.
191
+ export type { IslandBundle, IslandChunk } from './island-bundle';
192
+ export { buildIslands } from './island-bundle';
180
193
  export type { DrainFailure, DrainOutcome, DrainSkip } from './jobs-drain';
181
194
  export { drainJobs } from './jobs-drain';
182
195
  export type { JobsListFilter, JobsListResult } from './jobs-report';
@@ -235,6 +248,24 @@ export {
235
248
  isVendored,
236
249
  SOURCE_GLOBS,
237
250
  } from './source-files';
251
+ export type {
252
+ EmittedPage,
253
+ RouteFacts,
254
+ SkippedRoute,
255
+ SkipReason,
256
+ StaticReport,
257
+ } from './static-report';
258
+ export {
259
+ parseStaticReport,
260
+ readStaticReport,
261
+ removeStaticReport,
262
+ renderStaticReport,
263
+ SKIP_REASONS,
264
+ STATIC_REPORT_FILE,
265
+ skippedRoute,
266
+ skipReasonFor,
267
+ writeStaticReport,
268
+ } from './static-report';
238
269
  export type { TestCounts } from './test-counts';
239
270
  export { countsOf } from './test-counts';
240
271
  export type { TestFile } from './test-select';
@@ -286,4 +317,10 @@ export {
286
317
  SEMVER,
287
318
  workspacePackages,
288
319
  } from './workspace-checks';
289
- export { writeLine } from './write-line';
320
+ export type { WorkspaceNode, WorkspaceScan } from './workspace-graph';
321
+ // The graph itself, not just the gate's verdict on it: issue #239's complaint is that a
322
+ // scaffolded repo's dependency graph exists only inside `tsc`, so an app's own tooling has
323
+ // nothing to read. `checkWorkspaceDependencies` stays internal — it is reached through
324
+ // `x verify`, which is the one way a rule is enforced here.
325
+ export { readWorkspaceGraph, scanWorkspaces } from './workspace-graph';
326
+ export { writeErrorLine, writeLine } from './write-line';
@@ -13,6 +13,8 @@ import {
13
13
  islandModuleId,
14
14
  } from '@ultimat3/render';
15
15
  import { IslandBuildFailedError } from './errors';
16
+ import { solidProductionPlugin } from './island-solid-production';
17
+ import { islandStylesPlugin } from './island-styles';
16
18
  import { solidJsxPlugin } from './solid-loader';
17
19
 
18
20
  /**
@@ -84,7 +86,13 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
84
86
  // an island. The app's tsconfig says `jsx: "preserve"`, which makes the bundler fall back to
85
87
  // classic `React.createElement` — emitted into a browser chunk that imports no React, with
86
88
  // `success: true` and no log. Every island shipped that way through five majors.
87
- plugins: [solidJsxPlugin],
89
+ //
90
+ // The other two close the same shape of failure — a wrong answer `Bun.build` reports as
91
+ // `success: true`: without the second, `target: 'browser'` resolves the `development`
92
+ // export condition and the chunk carries Solid's dev build; without the third, Bun's file
93
+ // loader resolves a `.module.scss` to its asset PATH, so `styles['x']` is `undefined` and
94
+ // every element renders unclassed.
95
+ plugins: [solidJsxPlugin, solidProductionPlugin, islandStylesPlugin],
88
96
  });
89
97
  } catch (error) {
90
98
  throw new IslandBuildFailedError({ file, logs: describeBuildError(error) });
@@ -121,13 +129,64 @@ function describeBuildError(error: unknown): string {
121
129
  return error instanceof Error ? error.message : String(error);
122
130
  }
123
131
 
132
+ export interface BuildIslandsOptions {
133
+ /**
134
+ * Build ONE island, named app-root-relative — the whole option surface. A test that mounts a
135
+ * single island otherwise pays every OTHER island's Babel pass and `Bun.build` on every file,
136
+ * and the reference app is the one that feels it.
137
+ *
138
+ * Optional, and it must stay optional: `buildIslands` is on `@ultimat3/cli`'s public surface and
139
+ * `@ultimat3/testing`'s `IslandBuilder` satisfies it STRUCTURALLY as `(root: string) => …`, which
140
+ * is what keeps the `cli -> testing` edge pointing the one legal way.
141
+ */
142
+ readonly only?: string;
143
+ }
144
+
124
145
  /** Build every island in the app. An app with none returns an empty bundle and costs one glob. */
125
- export async function buildIslands(root: string): Promise<IslandBundle> {
126
- const files = await discoverIslands(root);
146
+ export async function buildIslands(
147
+ root: string,
148
+ options: BuildIslandsOptions = {},
149
+ ): Promise<IslandBundle> {
150
+ const discovered = await discoverIslands(root);
151
+ const only = options.only;
152
+ const files = only === undefined ? discovered : discovered.filter((file) => file === only);
153
+ // A filter that matches nothing is a typo in the CALLER, never an app with no islands. Answering
154
+ // an empty bundle here would surface two steps later, as a chunk table with no entry for a file
155
+ // the caller can see on disk.
156
+ if (only !== undefined && files.length === 0) throw onlyMissing(only, discovered);
127
157
  const chunks = await Promise.all(files.map((file) => buildOne(root, file)));
128
158
  return islandBundle(chunks);
129
159
  }
130
160
 
161
+ /**
162
+ * Same code as an unbuildable `src`: "this path cannot become a client entry" is one condition.
163
+ *
164
+ * Two fixes, because there are two causes and only one of them can be repaired by naming a path.
165
+ * The line was `pass only: '<app-root-relative path>.island.tsx'` — a placeholder nobody can run,
166
+ * which no gate could see: `fixProblem` fails a fix only for ADVICE with no command token, and a
167
+ * sentence with neither is not advice. Both forms below are constructed from what the caller
168
+ * already handed in, so neither can name a path this app does not have.
169
+ */
170
+ function onlyMissing(only: string, discovered: readonly string[]): IslandInvalidError {
171
+ const cause =
172
+ `buildIslands was asked for ${JSON.stringify(only)} alone, which is not one of the ` +
173
+ `${discovered.length} islands this app has (${discovered.length === 0 ? 'none' : discovered.join(', ')})`;
174
+ // The basename match first: a filter that misses normally missed on the PREFIX — a route-relative
175
+ // specifier where `discoverIslands`' app-root-relative path was wanted — and the filename
176
+ // survives that. Falling back to the first keeps the fix a real path rather than a shape.
177
+ const nearest =
178
+ discovered.find((file) => posix.basename(file) === posix.basename(only)) ?? discovered[0];
179
+ // An app with no islands cannot be pointed at one, so the fix WRITES the file that was asked
180
+ // for — the same command `entryMissing` hands back, split off the same path.
181
+ if (nearest === undefined) {
182
+ return new IslandInvalidError(
183
+ cause,
184
+ `x g island ${posix.basename(only, ISLAND_EXTENSION)} --at ${posix.dirname(only)}`,
185
+ );
186
+ }
187
+ return new IslandInvalidError(cause, `buildIslands(root, { only: '${nearest}' })`);
188
+ }
189
+
131
190
  export function islandBundle(chunks: readonly IslandChunk[]): IslandBundle {
132
191
  const byFile = new Map(chunks.map((chunk) => [chunk.file, chunk]));
133
192
  const byUrl = new Map(chunks.map((chunk) => [chunk.url, chunk]));
@@ -0,0 +1,129 @@
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
+ };
@@ -0,0 +1,41 @@
1
+ // The stylesheet half of an island build: `import styles from './x.module.scss'` answers the class
2
+ // map the SERVER hashed, and the CSS lands in the same registry a document renders from. Bun's
3
+ // default loader answers the asset PATH — a string — so `styles['track']` is `undefined`, every
4
+ // element renders unclassed, and `Bun.build` reports `success: true` with no log.
5
+
6
+ import { renderThrowable, UltimateError } from '@ultimat3/core';
7
+ import { loadStylesheet } from '@ultimat3/render';
8
+ import type { BunPlugin } from 'bun';
9
+ import { IslandBuildFailedError } from './errors';
10
+
11
+ /**
12
+ * The same spelling `installRenderLoader` filters on, so there is ONE answer to "what is a
13
+ * stylesheet import" across the server loader and the island bundler. A plain `.css`/`.scss` is in
14
+ * deliberately: it compiles to an empty class map and registers its rules, which is what a global
15
+ * stylesheet an island imports has to do.
16
+ */
17
+ const STYLESHEET = /\.s?css$/;
18
+
19
+ /**
20
+ * `loadStylesheet`, not `compileStylesheet`: compiling alone answers the class names and drops the
21
+ * RULES on the floor, and an island is the one importer a document's own module graph never sees.
22
+ * Registering here is what puts them in `stylesFor(surface)` — `buildIslands` runs before the first
23
+ * document is rendered, in `x dev` and in `prerenderSite` alike.
24
+ */
25
+ export const islandStylesPlugin: BunPlugin = {
26
+ name: 'ultimate-island-styles',
27
+ setup(build): void {
28
+ build.onLoad({ filter: STYLESHEET }, async ({ path }) => {
29
+ const source = await Bun.file(path).text();
30
+ try {
31
+ return { contents: loadStylesheet(path, source), loader: 'js' };
32
+ } catch (error) {
33
+ // A coded failure already names the file and carries a fix — re-wrapping it would bury
34
+ // both. Anything else is rendered through `renderThrowable`, because this is the plugin's
35
+ // last frame and a value that fights being read would escape a Bun plugin as a bare throw.
36
+ if (error instanceof UltimateError) throw error;
37
+ throw new IslandBuildFailedError({ file: path, logs: renderThrowable(error) });
38
+ }
39
+ });
40
+ },
41
+ };
@@ -18,23 +18,20 @@ import {
18
18
  inspectJob,
19
19
  inspectJobList,
20
20
  inspectQueues,
21
+ isJobState,
22
+ JOB_STATES,
21
23
  retryFromStep,
22
24
  } from '@ultimat3/jobs';
23
25
  import { BadFlagError, JobUnknownError } from './errors';
24
26
 
25
- /** Mirrors `JobState` from `@ultimat3/jobs`, which exports the type but no runtime list. */
26
- export const JOB_STATES: readonly JobState[] = [
27
- 'ready',
28
- 'delayed',
29
- 'running',
30
- 'suspended',
31
- 'done',
32
- 'failed',
33
- 'dead',
34
- ];
35
-
36
- const isJobState = (value: string): value is JobState =>
37
- (JOB_STATES as readonly string[]).includes(value);
27
+ /**
28
+ * The queue's own vocabulary, re-exported rather than restated — this file carried a copy of it,
29
+ * and the copy was one member short. `cancelled` shipped in `@ultimat3/jobs` and never here, so
30
+ * `x jobs cancel` created a state `x jobs ls --state cancelled` then refused to filter on: two
31
+ * commands of one CLI disagreeing about what a job can be. Kept on this module's surface because
32
+ * `index.ts` exports it from here.
33
+ */
34
+ export { JOB_STATES } from '@ultimat3/jobs';
38
35
 
39
36
  export function parseStateFlag(value: string | undefined): JobState | undefined {
40
37
  if (value === undefined) return undefined;
package/src/mcp-errors.ts CHANGED
@@ -48,6 +48,15 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
48
48
  X_JOB_UNKNOWN: 'x jobs ls --json',
49
49
  X_FIX_TARGET_UNKNOWN: 'x fix boundary apps/web/site/page.tsx --json',
50
50
  X_ERROR_FIX_INVALID: 'x verify --json # the finding names the file, the line and the fix text',
51
+ X_WORKSPACE_DEP_UNDECLARED:
52
+ 'x verify --json # the package-shape finding carries the dependency line to add',
53
+ X_SHOT_BROWSER_MISSING: 'bun add -d puppeteer-core',
54
+ X_GH_UNAVAILABLE: 'gh auth login # install first from https://cli.github.com',
55
+ X_GH_NOT_AUTHENTICATED: 'gh auth login',
56
+ X_GH_COMMAND_FAILED: 'x ci --json # the finding carries the gh invocation that failed',
57
+ X_GH_RESPONSE_INVALID: 'x pr review --json # the finding names the field that did not parse',
58
+ X_PR_NOT_FOUND: 'x pr review --pr 1 --json # or open one first with: gh pr create',
59
+ X_CI_RUN_NOT_FOUND: 'x ci --branch main --json',
51
60
  X_ERROR_CODE_UNDOCUMENTED: 'x verify --json # the finding names the code and the missing page',
52
61
  X_ERROR_CODE_UNREGISTERED:
53
62
  'x errors list --json # register the code in its package src/errors.ts, or move its row under "Reserved codes"',
@@ -93,6 +102,9 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
93
102
  X_PORT_IN_USE: 'x dev --port 3001 --json',
94
103
  X_DEV_ALREADY_RUNNING:
95
104
  'x dev --json # after stopping the x dev that already owns this checkout',
105
+ // The path, not `x dev`: the boot has already refused and rerunning it refuses again. The
106
+ // error's own `fix:` carries the resolved `.x/dev.lock`; this table cannot know the state dir.
107
+ X_DEV_LOCK_UNREADABLE: 'rm .x/dev.lock # then: x dev --json',
96
108
  // Not `x db status`: there is no such subcommand (`x db` is gen, migrate, reset, studio, branch),
97
109
  // so the fix answered a failed step with X_CLI_UNKNOWN_COMMAND. `x doctor` is what reports
98
110
  // reachability and drift, and is already this table's answer for X_DB_STUDIO_FAILED.
package/src/messages.ts CHANGED
@@ -139,6 +139,11 @@ const CATALOG = {
139
139
  // the app has a schema no migration records and `x verify`'s drift step says so until it runs.
140
140
  'cli.new.done':
141
141
  'created {name} — next: cd {name} && bun install && x db gen "initial" && x db migrate && x dev',
142
+ // The two prose lines of `x new`'s report. The `run: cd … && git init …` line beneath the second
143
+ // one stays inline in `cmd-new.ts`: it is an instruction to paste verbatim, and a translated
144
+ // command is a broken one — the same split `Finding.fix` already makes.
145
+ 'cli.new.wrote': ' {count} files in {dir}',
146
+ 'cli.new.noRepository': ' no repository — {problem}',
142
147
  'cli.policy.count':
143
148
  '{permissions} permission(s), {roles} role(s), {enforced} enforced by a declaration',
144
149
  // One row per (declaration, actor) pair, never per role: a permission two declarations enforce
@@ -159,11 +164,82 @@ const CATALOG = {
159
164
  'cli.routes.empty': 'no routes in the manifest — run `x manifest` first',
160
165
  'cli.tasks.count': '{count} task(s)',
161
166
  'cli.tasks.shown': '{name} — {cron} ({tz}), next {next}',
167
+ 'cli.affected.count':
168
+ '{count} workspace(s) affected by {base}...HEAD, from {changed} changed file(s)',
169
+ 'cli.affected.dirty':
170
+ ' including the working tree (--dirty): every uncommitted change in this checkout, whoever made it',
171
+ 'cli.affected.none': 'no workspace is affected by {base}...HEAD, from {changed} changed file(s)',
172
+ 'cli.affected.rootWide':
173
+ ' every workspace: {files} belongs to none of them and changes what all of them compile',
174
+ // `x shot` — the picture is the `lines`, the verdict is the artifact. The summary names the
175
+ // GATING fact, and a redirect comes first: a photograph of the sign-in page with every island
176
+ // missing reads as a bug in the app, and it is a bug in the capture.
177
+ 'cli.shot.ok': '{route} clean — {islands} island(s) mounted, nothing logged and nothing threw',
178
+ 'cli.shot.errors': '{route}: {errors} console error(s) — verdict.json names each one',
179
+ 'cli.shot.redirected': '{route} redirected to {url} — the picture is not the route asked for',
180
+ 'cli.shot.server.booted': ' server booted for this shot on {url}',
181
+ 'cli.shot.server.reused': ' server the x dev already running on {url}',
182
+ 'cli.shot.canvas': ' canvas {width}x{height}',
183
+ 'cli.shot.canvasUnreadable': ' canvas unreadable — {bytes} byte(s), not a decodable image',
184
+ 'cli.shot.islands': ' islands {booted} of {declared} mounted ({strategies})',
185
+ 'cli.shot.islandsUnknown': ' islands not counted — the page answered no probe',
186
+ // The failure a picture cannot show and a console count cannot see: a rejected mount promise
187
+ // calls no console method, so the FIRST one is named here rather than left to verdict.json.
188
+ 'cli.shot.islandFailed': '{route}: {failed} island(s) failed to mount — {island}: {message}',
189
+ 'cli.shot.network': ' network {requests} request(s), {refused} refused, {dropped} dropped',
190
+ 'cli.shot.console': ' console {level}: {text}',
191
+ 'cli.shot.threw': '{route}: {thrown} uncaught exception(s) — {first}',
192
+ 'cli.shot.pageError': ' threw {message} {at}',
193
+ 'cli.shot.picture': ' picture {path}',
194
+ 'cli.shot.verdict': ' verdict {path}',
195
+ 'cli.shot.blind.status':
196
+ 'HTTP response status is not observed — the port records requests, never responses',
197
+ 'cli.ci.failed':
198
+ '{failed} of {runs} workflow run(s) on {branch} failed — {findings} finding(s) recovered from the log',
199
+ 'cli.ci.green': 'every one of {runs} workflow run(s) on {branch} passed',
200
+ 'cli.ci.job': ' {conclusion} {job} ({steps})',
201
+ 'cli.ci.jobs.other': ' {count} other job(s) in this run',
202
+ 'cli.ci.logs.empty': ' the failed step wrote no log — {url}',
203
+ /** The conclusion of a run GitHub has not finished — a value, not a column key. */
204
+ 'cli.ci.pending': 'pending',
205
+ 'cli.ci.run': '{conclusion} {workflow} {url}',
206
+ 'cli.ci.running':
207
+ '{running} of {runs} workflow run(s) on {branch} has not finished — nothing has failed yet',
208
+ 'cli.ci.tail': ' log tail, {job}:',
209
+ 'cli.pr.body.truncated': ' … {hidden} more line(s) — re-run with --full',
210
+ /** The line of a thread whose anchor GitHub answers null for — a value, not a column key. */
211
+ 'cli.pr.line.unknown': '-',
212
+ 'cli.pr.replied': 'replied on thread {id}: {url}',
213
+ // Resolving closes a CONVERSATION. Whether the finding is fixed is a fact about the code that
214
+ // no GitHub mutation observes, and a summary saying "addressed" would assert one from the other.
215
+ 'cli.pr.resolved':
216
+ 'thread {id} is marked resolved on GitHub — that records the conversation, not that the finding is fixed',
217
+ 'cli.pr.review.count':
218
+ '{unresolved} unresolved and {resolved} resolved review thread(s) on {repo}#{pr}',
219
+ 'cli.pr.review.current': ' submitted against the current head {head}',
220
+ 'cli.pr.review.decision': ' review: {decision} by {author} at {submitted}',
221
+ 'cli.pr.review.none': 'no review thread is anchored to a line on {repo}#{pr}',
222
+ 'cli.pr.review.stale':
223
+ ' submitted against {commit}; the head is now {head} ({committed}) — this decision predates the current code',
224
+ 'cli.pr.review.truncated':
225
+ ' more than {count} threads — this is the first page, not the whole review',
226
+ 'cli.pr.review.undecided': ' GitHub reports no review decision yet',
227
+ 'cli.pr.thread.closed': ' resolved {path}:{line} {id}',
228
+ 'cli.pr.thread.comment': ' {author} at {createdAt}',
229
+ 'cli.pr.thread.more': ' {hidden} more comment(s) on this thread',
230
+ 'cli.pr.thread.open': ' unresolved {path}:{line} {id}',
231
+ 'cli.pr.thread.outdated':
232
+ ' the diff has moved under this thread — {line} is where the comment was written',
162
233
  'cli.test.fail': '{failed} of {workers} shard(s) failed',
234
+ 'cli.test.affected.none': 'nothing is affected by {base}...HEAD — 0 test file(s) ran',
163
235
  'cli.test.pass': '{files} test file(s) on {workers} worker(s) passed in {ms}ms',
164
236
  'cli.test.sampled': 'sampled {kept} of {total} {type} file(s)',
165
237
  'cli.test.type.fail': '{type} — {failed} of {workers} shard(s) failed',
166
238
  'cli.test.type.pass': '{type} — {files} test file(s) on {workers} worker(s) passed in {ms}ms',
239
+ // The banner a `--only` run carries, in front of whichever summary above it renders. Rendered
240
+ // output, so it lives here — `data.notAGateRun` is the machine marker, and a reader testing for
241
+ // one narrowed run reads that boolean rather than substring-matching this line.
242
+ 'cli.verify.notAGateRun': 'NOT A GATE RUN — {summary}',
167
243
  'cli.verify.pass': 'all {count} steps passed in {ms}ms',
168
244
  'cli.verify.fail': '{failed} of {count} steps failed',
169
245
  // A skipped step is not a passed one, so the two counts never share a sentence — and the skipped
package/src/output.ts CHANGED
@@ -52,6 +52,17 @@ export interface CommandResult {
52
52
  * could show is how the two drift.
53
53
  */
54
54
  readonly hold?: () => Promise<void>;
55
+ /**
56
+ * Which fd this result is written to. `stdout` for every command, absent included — and
57
+ * `stderr` for the one case where fd 1 is not the command's to write on: `x mcp serve
58
+ * --transport stdio`, whose stdout carries JSON-RPC frames, and where the `✓ mcp stdio serving
59
+ * 13 tools` line printed after the loop exits is a malformed frame to whatever is reading.
60
+ *
61
+ * Behaviour, not a fact, exactly like `hold` above — so NEITHER renderer carries it. It says
62
+ * where a rendered line goes, and a payload that also claimed it would be a second answer to a
63
+ * question `dispatch` has already answered by choosing the sink.
64
+ */
65
+ readonly stream?: 'stdout' | 'stderr';
55
66
  }
56
67
 
57
68
  export interface UltimateErrorShape {
@@ -158,13 +169,22 @@ export function renderHuman(result: CommandResult, verbose = false): string {
158
169
  for (const step of result.steps ?? []) {
159
170
  out.push(` ${mark(step)} ${step.name.padEnd(18)} ${step.durationMs}ms${width(step)}`);
160
171
  for (const finding of step.findings) out.push(renderFinding(finding, ' '));
172
+ // NOT escaped, and that is the one exception: `output` is this process's own captured
173
+ // subprocess stdout — `bun test`'s colour is the reason a human reads it at all, and it is
174
+ // already split on its real newlines rather than carrying them inside one entry.
161
175
  if (step.output !== undefined && step.output.length > 0 && (verbose || !step.ok)) {
162
176
  for (const line of step.output.trimEnd().split('\n')) out.push(` | ${line}`);
163
177
  }
164
178
  }
165
- for (const line of result.lines ?? []) out.push(line);
179
+ // Every free-text line through the SAME `singleLine` the 3-line format runs, because this is
180
+ // where text the CLI did not write reaches fd 1: a GitHub review body (`x pr`), a CI log tail
181
+ // (`x ci`), a page's own console (`x shot`). It was emitted verbatim, so an ESC byte in a PR
182
+ // comment retitled the window and cleared the screen, and a newline in one entry printed a
183
+ // second line a reader — or the agent this command exists for — takes for the renderer's own.
184
+ for (const line of result.lines ?? []) out.push(singleLine(line));
166
185
  for (const finding of result.findings ?? []) out.push(renderFinding(finding, ' '));
167
- out.push(`${result.ok ? '✓' : '✗'} ${result.summary}`);
186
+ // The summary is foreign too: `cli.shot.threw` interpolates a page's own error message.
187
+ out.push(`${result.ok ? '✓' : '✗'} ${singleLine(result.summary)}`);
168
188
  return out.join('\n');
169
189
  }
170
190