@ultimat3/cli 22.15.0 → 24.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 +36 -13
- package/README.md +31 -8
- package/package.json +29 -28
- package/src/admin-catalog-keys.ts +16 -0
- package/src/admin-registration.ts +192 -0
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +125 -11
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +6 -20
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-load.ts +55 -11
- package/src/app-manifest.ts +16 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +0 -0
- package/src/app-reload-graph.ts +7 -30
- package/src/app-transport.ts +219 -0
- package/src/boundary-findings.ts +16 -4
- package/src/browser-transport.ts +160 -0
- package/src/budgets.ts +27 -34
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +10 -3
- package/src/cmd-build.ts +107 -11
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +66 -16
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +75 -24
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +195 -47
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs-spec.ts +19 -3
- package/src/cmd-jobs.ts +136 -7
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-new.ts +15 -1
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +27 -3
- package/src/cmd-secrets-ring.ts +76 -0
- package/src/cmd-secrets-spec.ts +10 -2
- package/src/cmd-secrets.ts +95 -18
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +84 -15
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +13 -5
- package/src/comment-safe.ts +16 -0
- package/src/coverage-floor.ts +334 -0
- package/src/coverage-lcov.ts +193 -0
- package/src/coverage-source.ts +136 -0
- package/src/db-object-drift.ts +30 -0
- package/src/db-replay-engine.ts +115 -0
- package/src/db-schema-dump.ts +150 -0
- package/src/db-schema-refresh.ts +122 -0
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dev-route-table.ts +9 -0
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-coverage.ts +39 -0
- package/src/doctor-guards.ts +54 -0
- package/src/doctor-sealed.ts +132 -0
- package/src/document-graph.ts +82 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +55 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +28 -2
- package/src/generate-files.ts +79 -12
- package/src/generate-grant-findings.ts +76 -0
- package/src/generate-grants.ts +77 -3
- package/src/generate-kinds.ts +52 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +62 -40
- package/src/generated-imports.ts +59 -0
- package/src/gh-target.ts +12 -7
- package/src/guard-sources.ts +104 -0
- package/src/guards.ts +23 -7
- package/src/handle-registration.ts +217 -0
- package/src/i18n-audit.ts +4 -1
- package/src/i18n-registration.ts +36 -0
- package/src/image-prepare.ts +51 -8
- package/src/import-closure.ts +165 -0
- package/src/index.ts +12 -5
- package/src/island-bundle.ts +3 -15
- package/src/island-store.ts +24 -16
- package/src/jobs-drain.ts +95 -21
- package/src/jobs-json.ts +50 -0
- package/src/jobs-report.ts +7 -4
- package/src/json-merge.ts +46 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +41 -4
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +35 -0
- package/src/metrics-endpoint.ts +95 -24
- package/src/migration-extensions.ts +29 -0
- package/src/module-imports.ts +47 -0
- package/src/output.ts +24 -9
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/permission-grants.ts +41 -9
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/publish-closure.ts +101 -0
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-load.ts +143 -0
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +27 -5
- package/src/role-sync.ts +55 -20
- package/src/role-wake.ts +45 -0
- package/src/runtime-admin.ts +141 -0
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-hooks.ts +5 -2
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +39 -11
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +68 -35
- package/src/runtime-services.ts +22 -18
- package/src/runtime-storage.ts +10 -5
- package/src/scaffold-typecheck.ts +1 -0
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/schema-dump-drift.ts +64 -0
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +114 -158
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-prebuilt-paths.ts +19 -0
- package/src/serve-prebuilt.ts +84 -0
- package/src/serve-web.ts +169 -0
- package/src/serve.ts +6 -9
- package/src/server-barrels.ts +54 -0
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-edit.ts +49 -0
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -26
- package/src/templates/action.ts +147 -11
- package/src/templates/admin-catalog.ts +58 -0
- package/src/templates/admin-page.ts +36 -8
- package/src/templates/admin.ts +57 -17
- package/src/templates/backfill.ts +121 -87
- package/src/templates/entity.ts +140 -44
- package/src/templates/guard-animated-layout-property.ts +4 -11
- package/src/templates/guard-bare-error.ts +9 -13
- package/src/templates/guard-focus-visible.ts +4 -11
- package/src/templates/guard-image-dimensions.ts +6 -6
- package/src/templates/guard-island-without-states.ts +2 -4
- package/src/templates/guard-raw-breakpoint.ts +210 -0
- package/src/templates/guard-raw-colour.ts +4 -11
- package/src/templates/guard-raw-length.ts +375 -0
- package/src/templates/guard-raw-motion.ts +213 -0
- package/src/templates/guard-raw-shadow.ts +187 -0
- package/src/templates/guard-raw-z-index.ts +166 -0
- package/src/templates/guard-repo-raw-sql.ts +345 -0
- package/src/templates/guard-semantic-interactive.ts +6 -6
- package/src/templates/guard-undeclared-custom-property.ts +217 -0
- package/src/templates/guard-undefined-style-class.ts +215 -0
- package/src/templates/guard-untranslated-string.ts +6 -6
- package/src/templates/guard-unzoned-date.ts +9 -13
- package/src/templates/index.ts +3 -2
- package/src/templates/island.ts +34 -51
- package/src/templates/job-body-test.ts +121 -0
- package/src/templates/job.ts +23 -8
- package/src/templates/naming.ts +28 -0
- package/src/templates/policy.ts +16 -9
- package/src/templates/query.ts +242 -26
- package/src/templates/resource-create.ts +62 -18
- package/src/templates/resource-form-island.ts +58 -80
- package/src/templates/resource-page.ts +269 -0
- package/src/templates/resource-service.ts +96 -0
- package/src/templates/resource.ts +86 -61
- package/src/templates/route.ts +41 -35
- package/src/templates/scaffold-api.ts +55 -1
- package/src/templates/scaffold-app.ts +177 -53
- package/src/templates/scaffold-auth.ts +83 -19
- package/src/templates/scaffold-browser-client.ts +90 -0
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +20 -2
- package/src/templates/scaffold-dashboard-bare.ts +64 -2
- package/src/templates/scaffold-dashboard-example.ts +124 -31
- package/src/templates/scaffold-dashboard-shared.ts +1 -20
- package/src/templates/scaffold-dashboard.ts +1 -2
- package/src/templates/scaffold-db-client.ts +144 -0
- package/src/templates/scaffold-db-package.ts +90 -8
- package/src/templates/scaffold-docs.ts +50 -5
- package/src/templates/scaffold-guards.ts +54 -11
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-http.ts +9 -7
- package/src/templates/scaffold-repo.ts +61 -3
- package/src/templates/scaffold-roles.ts +56 -9
- package/src/templates/scaffold-shell.ts +74 -105
- package/src/templates/scaffold-site.ts +45 -19
- package/src/templates/scaffold-ui-package.ts +18 -0
- package/src/templates/slice-foundation.ts +46 -8
- package/src/test-counts.ts +14 -2
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/transport-calls.ts +105 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/unscanned-admin.ts +54 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +43 -53
- package/src/verify-coverage-run.ts +190 -0
- package/src/verify-deadline.ts +353 -0
- package/src/verify-floor.ts +36 -9
- package/src/verify-merge.ts +171 -45
- package/src/verify-part-step.ts +90 -0
- package/src/verify-progress.ts +25 -0
- package/src/verify-role-load-probe.ts +54 -0
- package/src/verify-role-load.ts +148 -0
- package/src/verify-run.ts +116 -37
- package/src/verify-shard.ts +33 -11
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +42 -1
- package/src/verify-tests.ts +83 -8
- package/src/workspace-checks.ts +50 -8
- package/src/workspace-dep-edit.ts +127 -0
- package/src/build-errors.ts +0 -54
- package/src/cmd-dev-fixture.ts +0 -204
- package/src/dev-live-fixture.ts +0 -122
- package/src/policy-fixture.ts +0 -78
- package/src/realtime-browser-probe-fixture.ts +0 -9
- package/src/role-start-fixture.ts +0 -78
- package/src/scaffold-fixture.ts +0 -160
- package/src/templates/island-fixture.ts +0 -76
- package/src/verify-errors.ts +0 -49
package/src/cmd-generate.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { MANIFEST_FILENAME } from '@ultimat3/manifest';
|
|
7
|
-
import {
|
|
7
|
+
import { registerAdminResources } from './admin-registration';
|
|
8
|
+
import { indexBindingFindings, registerGeneratedPrimitives } from './api-registration';
|
|
8
9
|
import { writeAppArtifacts } from './app-artifacts';
|
|
9
10
|
import { appManifest } from './app-manifest';
|
|
10
11
|
import { requireAppRoot } from './app-root';
|
|
@@ -13,18 +14,23 @@ import type { CliCommand, CommandContext } from './command';
|
|
|
13
14
|
import { invocationOf } from './command';
|
|
14
15
|
import { assertFeatureExists } from './generate-feature';
|
|
15
16
|
import { generate, sliceDir } from './generate-files';
|
|
17
|
+
import { ungrantedByGenerator } from './generate-grant-findings';
|
|
16
18
|
import { grantGeneratedPermissions } from './generate-grants';
|
|
17
19
|
import type { Generator } from './generate-kinds';
|
|
18
20
|
import { readFeature, readKind, readName, readPermission, readSurface } from './generate-kinds';
|
|
19
|
-
import {
|
|
21
|
+
import { refuseShadowedTypes } from './generate-shadow';
|
|
22
|
+
import { containedPath, planWrites, writeFiles } from './generate-write';
|
|
23
|
+
import { declareGeneratedImports } from './generated-imports';
|
|
24
|
+
import { registerGeneratedEntities, resolveDbModule } from './handle-registration';
|
|
20
25
|
import { resolveCatalogModule } from './i18n-audit';
|
|
21
|
-
import { syncI18nIndex } from './i18n-index';
|
|
26
|
+
import { catalogLocales, syncI18nIndex } from './i18n-index';
|
|
22
27
|
import { reproducedFlags } from './invocation-flags';
|
|
23
28
|
import { msg } from './messages';
|
|
24
29
|
import type { CommandResult, Finding } from './output';
|
|
25
30
|
import { flagBool, flagList, flagString } from './parse';
|
|
26
31
|
import { quoteArg } from './shell-quote';
|
|
27
|
-
import {
|
|
32
|
+
import type { GeneratedFile } from './templates';
|
|
33
|
+
import { kebab, names, resolveLocales } from './templates';
|
|
28
34
|
|
|
29
35
|
// One import path for the generator, unchanged by the split: `index.ts`, `x new` and the scaffold
|
|
30
36
|
// fixture reach the kinds, the pure file list and the writer through this module, and a second path
|
|
@@ -36,6 +42,24 @@ export { GENERATORS } from './generate-kinds';
|
|
|
36
42
|
export type { WriteReport } from './generate-write';
|
|
37
43
|
export { dedupe, writeFiles } from './generate-write';
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Names to suggest when the API index already holds the requested one, in order: the feature's
|
|
47
|
+
* prefix, the kind's suffix, then that suffix numbered — kebab output, so each pastes into a shell
|
|
48
|
+
* as one argument. The caller re-plans each and offers the first the index does not hold either.
|
|
49
|
+
*/
|
|
50
|
+
const freeNames = (
|
|
51
|
+
kind: Generator,
|
|
52
|
+
name: string,
|
|
53
|
+
feature: string | undefined,
|
|
54
|
+
): readonly string[] => {
|
|
55
|
+
const suffixed = `${kebab(name)}-${kind.split(':').at(-1) ?? kind}`;
|
|
56
|
+
return [
|
|
57
|
+
...(feature === undefined ? [] : [`${kebab(feature)}-${kebab(name)}`]),
|
|
58
|
+
suffixed,
|
|
59
|
+
...Array.from({ length: 8 }, (_unused, index) => `${suffixed}-${index + 2}`),
|
|
60
|
+
];
|
|
61
|
+
};
|
|
62
|
+
|
|
39
63
|
export const generateCommand: CliCommand = {
|
|
40
64
|
spec: generateSpec,
|
|
41
65
|
async run(ctx: CommandContext): Promise<CommandResult> {
|
|
@@ -46,7 +70,7 @@ export const generateCommand: CliCommand = {
|
|
|
46
70
|
// Both flags are resolved before a single file is planned: a bad surface or a locale that is
|
|
47
71
|
// really a path fails here, with nothing written and nothing to undo.
|
|
48
72
|
const surface = readSurface(flagString(ctx.args, 'surface'), kind, name);
|
|
49
|
-
const locales = resolveLocales(flagList(ctx.args, 'locales'));
|
|
73
|
+
const locales = resolveLocales(await namedOrAppLocales(root, flagList(ctx.args, 'locales')));
|
|
50
74
|
const at = flagString(ctx.args, 'at');
|
|
51
75
|
// Read with the two above, and refused here for their reason: the value is spliced into the
|
|
52
76
|
// emitted source three times, and a value that is not a `<resource>:<verb>` is a page the app
|
|
@@ -55,6 +79,10 @@ export const generateCommand: CliCommand = {
|
|
|
55
79
|
// Read before a file is planned, like the flags above: which module a generated component
|
|
56
80
|
// imports `useT()` from is a fact about THIS app, and `generate` is a pure function.
|
|
57
81
|
const catalogModule = await resolveCatalogModule(root);
|
|
82
|
+
// And for the same reason: which package a generated `repo.ts` imports the typed handle from.
|
|
83
|
+
const dbModule = await resolveDbModule(root);
|
|
84
|
+
// And whether the app has the frame a resource's page sits in.
|
|
85
|
+
const shell = existsSync(containedPath(root, SHELL_MODULE));
|
|
58
86
|
// Read for the same reason: which errors the slice declares is written on THIS app's disk.
|
|
59
87
|
const slice = sliceDir(surface, kebab(featureFlag ?? name));
|
|
60
88
|
// A named slice that is not there is refused, never invented (X_FEATURE_UNKNOWN).
|
|
@@ -64,46 +92,91 @@ export const generateCommand: CliCommand = {
|
|
|
64
92
|
// THIS feature's own `entity.ts`/`repo.ts`, not a default the template gets to assume.
|
|
65
93
|
const sliceEntity = await readSliceFile(root, kind, slice, 'entity.ts');
|
|
66
94
|
const sliceRepo = await readSliceFile(root, kind, slice, 'repo.ts');
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
const planFor = (planned: string) =>
|
|
96
|
+
generate({
|
|
97
|
+
kind,
|
|
98
|
+
name: planned,
|
|
99
|
+
...(featureFlag === undefined ? {} : { feature: featureFlag }),
|
|
100
|
+
...(sliceErrors === undefined ? {} : { sliceErrors }),
|
|
101
|
+
...(sliceEntity === undefined ? {} : { sliceEntity }),
|
|
102
|
+
...(sliceRepo === undefined ? {} : { sliceRepo }),
|
|
103
|
+
...(at === undefined ? {} : { at }),
|
|
104
|
+
...(permission === undefined ? {} : { permission }),
|
|
105
|
+
surface,
|
|
106
|
+
live: flagBool(ctx.args, 'live'),
|
|
107
|
+
admin: flagBool(ctx.args, 'admin'),
|
|
108
|
+
locales,
|
|
109
|
+
...(catalogModule === undefined ? {} : { catalogModule }),
|
|
110
|
+
...(dbModule === undefined ? {} : { dbModule }),
|
|
111
|
+
shell,
|
|
112
|
+
});
|
|
113
|
+
const files = planFor(name);
|
|
114
|
+
// On the planned files, before a dry run answers or anything lands: a name whose type spelling
|
|
115
|
+
// the emitted code also uses as a global is a slice that does not compile.
|
|
116
|
+
refuseShadowedTypes(files, kind, name);
|
|
117
|
+
// The caller's own invocation, EVERY flag it set included: without `--feature` the fix wrote
|
|
118
|
+
// a second slice beside the one that conflicted.
|
|
119
|
+
const flags = reproducedFlags(generateCommand.spec, ctx.args);
|
|
120
|
+
const invocation = [invocationOf(ctx, 'g'), kind, quoteArg(name), ...flags].join(' ');
|
|
121
|
+
// A module the API index would list under a name it already holds: refused before anything is
|
|
122
|
+
// written, with the same run under a name that is free — re-planned and re-checked, so the
|
|
123
|
+
// suggestion is never itself a conflict.
|
|
124
|
+
const pathsOf = (planned: readonly GeneratedFile[]) => planned.map((file) => file.path);
|
|
125
|
+
const held = await indexBindingFindings(root, pathsOf(files), '');
|
|
126
|
+
let bindings: readonly Finding[] = [];
|
|
127
|
+
if (held.length > 0) {
|
|
128
|
+
let free = freeNames(kind, name, featureFlag).at(-1) ?? name;
|
|
129
|
+
for (const candidate of freeNames(kind, name, featureFlag)) {
|
|
130
|
+
if ((await indexBindingFindings(root, pathsOf(planFor(candidate)), '')).length > 0)
|
|
131
|
+
continue;
|
|
132
|
+
free = candidate;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const renamed = [invocationOf(ctx, 'g'), kind, quoteArg(free), ...flags].join(' ');
|
|
136
|
+
bindings = held.map((finding) => ({ ...finding, fix: renamed }));
|
|
137
|
+
}
|
|
138
|
+
const force = flagBool(ctx.args, 'force');
|
|
82
139
|
if (flagBool(ctx.args, 'dry-run')) {
|
|
140
|
+
// The write plan, never the bare file list: what the real run would write, skip and refuse.
|
|
141
|
+
const plan = await planWrites(root, files, force, invocation);
|
|
142
|
+
const findings = [...plan.conflicts, ...bindings];
|
|
143
|
+
const planned = findings.length === 0 ? plan.written : [];
|
|
83
144
|
return {
|
|
84
|
-
ok:
|
|
145
|
+
ok: findings.length === 0,
|
|
85
146
|
command: 'g',
|
|
86
|
-
summary: msg('cli.generate.planned', { count:
|
|
87
|
-
data: { files:
|
|
88
|
-
lines:
|
|
147
|
+
summary: msg('cli.generate.planned', { count: planned.length, kind, name }),
|
|
148
|
+
data: { files: planned, dryRun: true },
|
|
149
|
+
lines: planned.map((file) => msg('cli.file.added', { path: file })),
|
|
150
|
+
findings,
|
|
89
151
|
};
|
|
90
152
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
153
|
+
const report =
|
|
154
|
+
bindings.length > 0
|
|
155
|
+
? { written: [], conflicts: bindings }
|
|
156
|
+
: await writeFiles(root, files, force, invocation);
|
|
157
|
+
// The three edits a generated primitive needs outside its own slice, performed rather than
|
|
158
|
+
// left as findings: a declared permission granted to a role, a job listed in `defineApi`, and
|
|
159
|
+
// an entity added to the typed handle its `repo.ts` reads through. Before the manifest load
|
|
160
|
+
// below, so the projection sees all three — a repo reading a table the handle lacks would not
|
|
161
|
+
// load at all.
|
|
162
|
+
const handle = await registerGeneratedEntities(root, report.written, dbModule);
|
|
163
|
+
// And a fourth, for `--admin`: the override it wrote, listed where `defineAdmin()` reads it.
|
|
164
|
+
const adminWiring = await registerAdminResources(root, report.written);
|
|
165
|
+
// And a fifth, last because the others add imports too: every sibling workspace a written file
|
|
166
|
+
// imports, declared in the manifest it landed under — or `package-shape` refuses the output.
|
|
103
167
|
const edited = [
|
|
104
|
-
...
|
|
105
|
-
|
|
168
|
+
...new Set([
|
|
169
|
+
...(await grantGeneratedPermissions(root, report.written)),
|
|
170
|
+
...(await registerGeneratedPrimitives(root, report.written)),
|
|
171
|
+
...handle.edited,
|
|
172
|
+
...adminWiring.edited,
|
|
173
|
+
...(await declareGeneratedImports(root, report.written)),
|
|
174
|
+
]),
|
|
106
175
|
];
|
|
176
|
+
// A grant that edit could not place is said so, with the role each permission belongs to: an
|
|
177
|
+
// app whose role map is not the scaffold's got no grant and no word, and a 403 on every
|
|
178
|
+
// endpoint the run had just written.
|
|
179
|
+
const ungranted = await ungrantedByGenerator(root, report.written);
|
|
107
180
|
// A locale's catalog existing on disk and the app being able to select it are two different
|
|
108
181
|
// facts — see `syncI18nIndex`. Runs before the manifest load below so a route or resource
|
|
109
182
|
// this same invocation just wrote never gets projected against a stale catalog registration.
|
|
@@ -133,17 +206,36 @@ export const generateCommand: CliCommand = {
|
|
|
133
206
|
buildId = manifest.buildId;
|
|
134
207
|
} else loadFailures.push(...findings);
|
|
135
208
|
}
|
|
136
|
-
const findings = [
|
|
209
|
+
const findings = [
|
|
210
|
+
...report.conflicts,
|
|
211
|
+
...handle.findings,
|
|
212
|
+
...ungranted,
|
|
213
|
+
...adminWiring.findings,
|
|
214
|
+
...indexSync.findings,
|
|
215
|
+
...loadFailures,
|
|
216
|
+
];
|
|
137
217
|
// One list behind all three renderings. The manifest was printed as a `+` line while the count
|
|
138
218
|
// beside it came from `report.written` alone, so `x g island` said "wrote 2 file(s)" over three
|
|
139
219
|
// lines — and `--json` carried the shorter list, which is the drift `--json` exists to prevent.
|
|
140
220
|
const written = [...report.written, ...edited, ...artifacts];
|
|
221
|
+
// Named only over a clean run: with a finding, the finding's own `fix:` is the next command.
|
|
222
|
+
const table = names(isEntityKind(kind) ? name : (featureFlag ?? name)).table;
|
|
223
|
+
const next = findings.length === 0 ? nextSteps(report.written, edited, table) : [];
|
|
141
224
|
return {
|
|
142
225
|
ok: findings.length === 0,
|
|
143
226
|
command: 'g',
|
|
144
|
-
summary:
|
|
227
|
+
summary:
|
|
228
|
+
next.length === 0
|
|
229
|
+
? msg('cli.generate.wrote', { count: written.length, kind, name })
|
|
230
|
+
: msg('cli.generate.wroteNext', {
|
|
231
|
+
count: written.length,
|
|
232
|
+
kind,
|
|
233
|
+
name,
|
|
234
|
+
next: next.join(' && '),
|
|
235
|
+
}),
|
|
145
236
|
data: {
|
|
146
237
|
files: written,
|
|
238
|
+
...(next.length === 0 ? {} : { next }),
|
|
147
239
|
...(buildId === undefined ? {} : { manifest: { buildId } }),
|
|
148
240
|
},
|
|
149
241
|
lines: written.map((path) => msg('cli.file.added', { path })),
|
|
@@ -152,6 +244,59 @@ export const generateCommand: CliCommand = {
|
|
|
152
244
|
},
|
|
153
245
|
};
|
|
154
246
|
|
|
247
|
+
/**
|
|
248
|
+
* `--locales` when the caller named any, and otherwise every locale the app already has a catalog
|
|
249
|
+
* for: a key written to `en` alone in an app that also ships `es` renders ⟦key⟧ for every Spanish
|
|
250
|
+
* reader. A stem that is not a locale tag is some other JSON file, and not a catalog to write to.
|
|
251
|
+
*/
|
|
252
|
+
async function namedOrAppLocales(
|
|
253
|
+
root: string,
|
|
254
|
+
named: readonly string[],
|
|
255
|
+
): Promise<readonly string[]> {
|
|
256
|
+
if (named.length > 0) return named;
|
|
257
|
+
return (await catalogLocales(root)).filter((stem) => {
|
|
258
|
+
try {
|
|
259
|
+
return Intl.getCanonicalLocales(stem).length === 1;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const isEntityKind = (kind: Generator): boolean => kind === 'entity' || kind === 'resource';
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* What a run that declared a table owes before the app can read it, in the order it has to
|
|
270
|
+
* happen — each one a command that runs as printed. `bunx x`, never a bare `x`: `bun install`
|
|
271
|
+
* links the binary into `./node_modules/.bin` and nowhere on PATH.
|
|
272
|
+
*
|
|
273
|
+
* `bun install` leads when a manifest gained a workspace edge: the handle imports the entity from
|
|
274
|
+
* the web workspace, and an edge only a manifest declares is not linked until the install runs.
|
|
275
|
+
* `x db gen` then writes the migration AND `packages/db/schema/`, which the `drift` step holds.
|
|
276
|
+
*/
|
|
277
|
+
export function nextSteps(
|
|
278
|
+
written: readonly string[],
|
|
279
|
+
edited: readonly string[],
|
|
280
|
+
table: string,
|
|
281
|
+
): readonly string[] {
|
|
282
|
+
if (!written.some((path) => path.endsWith('/entity.ts'))) return [];
|
|
283
|
+
return [
|
|
284
|
+
...(edited.some((path) => path.endsWith('package.json')) ? ['bun install'] : []),
|
|
285
|
+
`bunx x db gen "create ${table}"`,
|
|
286
|
+
'bunx x db migrate',
|
|
287
|
+
// A page that mounts an island has a byte budget only a build can weigh — measured on `x new`'s
|
|
288
|
+
// app (`RESOURCE_PAGE_BUDGET`), and an app that charges more per document is over it on this
|
|
289
|
+
// run rather than on the next `bin/check`.
|
|
290
|
+
...(written.some((path) => path.endsWith('/page.tsx')) ? [MEASURE_PAGE] : []),
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The two commands that weigh a page against its budget: the build writes the bytes it reads. */
|
|
295
|
+
export const MEASURE_PAGE = 'bunx x build --target static && bunx x verify --only budgets';
|
|
296
|
+
|
|
297
|
+
/** The frame `x new` writes: a resource's page renders inside it when the app has one. */
|
|
298
|
+
export const SHELL_MODULE = 'apps/web/shared/shell.tsx';
|
|
299
|
+
|
|
155
300
|
/**
|
|
156
301
|
* The slice's `errors.ts`, for the two generators whose template throws from it. Only those two:
|
|
157
302
|
* a route or an island names no slice, and the "feature" the fallback derives for them is a path
|
|
@@ -167,20 +312,23 @@ async function readSliceErrors(
|
|
|
167
312
|
return existsSync(file) ? await Bun.file(file).text() : undefined;
|
|
168
313
|
}
|
|
169
314
|
|
|
315
|
+
/** The generators whose output depends on what the slice's `entity.ts` / `repo.ts` declare. */
|
|
316
|
+
const READS_SLICE: ReadonlySet<Generator> = new Set(['job', 'task', 'action', 'mutator', 'query']);
|
|
317
|
+
|
|
170
318
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
319
|
+
* The slice's `entity.ts`/`repo.ts` as they stand on disk, absent when the generator's kind reads
|
|
320
|
+
* neither or the file does not exist yet. `readSliceErrors`'s reason — whichever generator reads
|
|
321
|
+
* it decides on THIS app's disk, not on a default the template assumes. `query` reads them for its
|
|
322
|
+
* test alone: whether it may store a row is a fact about the entity an author may have reshaped.
|
|
323
|
+
* Exported so a test can ask which kinds read the disk without running a whole generation.
|
|
174
324
|
*/
|
|
175
|
-
async function readSliceFile(
|
|
325
|
+
export async function readSliceFile(
|
|
176
326
|
root: string,
|
|
177
327
|
kind: Generator,
|
|
178
328
|
slice: string,
|
|
179
329
|
name: 'entity.ts' | 'repo.ts',
|
|
180
330
|
): Promise<string | undefined> {
|
|
181
|
-
if (kind
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
331
|
+
if (!READS_SLICE.has(kind)) return undefined;
|
|
184
332
|
const file = containedPath(root, `${slice}/${name}`);
|
|
185
333
|
return existsSync(file) ? await Bun.file(file).text() : undefined;
|
|
186
334
|
}
|
package/src/cmd-help.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// never describe a flag that does not exist. Built as a factory over the spec list to keep the
|
|
3
3
|
// registry acyclic.
|
|
4
4
|
|
|
5
|
+
import { nearestName } from '@ultimat3/core';
|
|
5
6
|
import type { CliCommand, CommandContext } from './command';
|
|
7
|
+
import { UnknownCommandError } from './errors';
|
|
6
8
|
import { msg } from './messages';
|
|
7
9
|
import type { CommandResult, JsonValue } from './output';
|
|
8
10
|
import type { CommandSpec, FlagSpec } from './parse';
|
|
@@ -18,17 +20,28 @@ const flagLine = (flag: FlagSpec): string => {
|
|
|
18
20
|
* The one resolution of a topic, read by both renderers. `--json` filtered on `spec.name === topic`
|
|
19
21
|
* of its own, so the two disagreed about exactly the inputs a caller is least sure of: `x help
|
|
20
22
|
* generate --json` answered `[]` — "that command does not exist" — while the page beside it printed
|
|
21
|
-
* `g
|
|
23
|
+
* `g`. A topic naming no command is refused, as `x <topic>` itself is: answering the catalogue
|
|
24
|
+
* with exit 0 read as that command's usage to a caller that never compared the names.
|
|
22
25
|
*/
|
|
23
26
|
const specFor = (
|
|
24
27
|
specs: readonly CommandSpec[],
|
|
25
28
|
topic: string | undefined,
|
|
26
|
-
): CommandSpec | undefined =>
|
|
27
|
-
topic === undefined
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
): CommandSpec | undefined => {
|
|
30
|
+
if (topic === undefined) return undefined;
|
|
31
|
+
const found = specs.find(
|
|
32
|
+
(entry) => entry.name === topic || (entry.aliases ?? []).includes(topic),
|
|
33
|
+
);
|
|
34
|
+
if (found !== undefined) return found;
|
|
35
|
+
const names = specs.map((entry) => entry.name);
|
|
36
|
+
const nearest = nearestName(topic, [...names, ...specs.flatMap((entry) => entry.aliases ?? [])]);
|
|
37
|
+
throw new UnknownCommandError({
|
|
38
|
+
path: `help ${topic}`,
|
|
39
|
+
known: names,
|
|
40
|
+
suggestion: nearest === undefined ? 'help' : `help ${nearest}`,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
30
43
|
|
|
31
|
-
/** What `--json` reports: the one resolved command, or —
|
|
44
|
+
/** What `--json` reports: the one resolved command, or — with no topic — all of them. */
|
|
32
45
|
export function helpTopic(
|
|
33
46
|
specs: readonly CommandSpec[],
|
|
34
47
|
topic: string | undefined,
|
package/src/cmd-i18n.ts
CHANGED
|
@@ -196,14 +196,15 @@ async function runAdd(root: string, ctx: CommandContext): Promise<CommandResult>
|
|
|
196
196
|
const { registered, findings: indexFindings } = await syncI18nIndex(root);
|
|
197
197
|
|
|
198
198
|
const keys = catalogKeys(seeded).length;
|
|
199
|
+
// The catalog is on disk either way, but a finding fails the run: a `packages/i18n/src/index.ts`
|
|
200
|
+
// that would not import is why `from` may be the framework's `en` instead of the app's own
|
|
201
|
+
// default, and an index the locale could not be registered in is a red `i18n` gate step.
|
|
202
|
+
const findings = [...indexFindings, ...app.findings];
|
|
199
203
|
return {
|
|
200
|
-
|
|
201
|
-
// rather than flip that: a `packages/i18n/src/index.ts` that would not import is why `from` may
|
|
202
|
-
// be the framework's `en` instead of the app's own default, and silence there is the bug.
|
|
203
|
-
ok: true,
|
|
204
|
+
ok: findings.length === 0,
|
|
204
205
|
command: 'i18n',
|
|
205
206
|
summary: msg('cli.i18n.added', { locale, keys, from: from ?? locale }),
|
|
206
|
-
findings
|
|
207
|
+
findings,
|
|
207
208
|
// `registered` is false only for an app with no `packages/i18n` at all — a fact a caller has
|
|
208
209
|
// to be able to read, because it is the one case where the locale is on disk and unselectable.
|
|
209
210
|
data: { locale, from: from ?? locale, keys, path, registered },
|
|
@@ -274,16 +275,16 @@ async function runSync(root: string, ctx: CommandContext): Promise<CommandResult
|
|
|
274
275
|
const { registered, findings: indexFindings } = await syncI18nIndex(root);
|
|
275
276
|
|
|
276
277
|
const total = catalogKeys(merged).length;
|
|
278
|
+
// Same as `runAdd`: the merge landed, and a finding still fails the run.
|
|
279
|
+
const findings = [...indexFindings, ...app.findings];
|
|
277
280
|
return {
|
|
278
|
-
|
|
279
|
-
// own default or the framework's fallback standing in for an i18n module that would not import.
|
|
280
|
-
ok: true,
|
|
281
|
+
ok: findings.length === 0,
|
|
281
282
|
command: 'i18n',
|
|
282
283
|
summary: msg('cli.i18n.synced', { locale, from: from ?? locale, added: added.length, total }),
|
|
283
284
|
// The keys themselves, raw — `runCheck` lists gaps the same way, because a key is a value an
|
|
284
285
|
// author copies and never prose the catalog owns.
|
|
285
286
|
lines: seeded ? added.map((key) => ` ${key}`) : [],
|
|
286
|
-
findings
|
|
287
|
+
findings,
|
|
287
288
|
data: {
|
|
288
289
|
locale,
|
|
289
290
|
from: from ?? locale,
|
package/src/cmd-jobs-spec.ts
CHANGED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CommandSpec } from './parse';
|
|
5
5
|
|
|
6
|
-
export const JOBS_SUBCOMMANDS = [
|
|
6
|
+
export const JOBS_SUBCOMMANDS = [
|
|
7
|
+
'ls',
|
|
8
|
+
'show',
|
|
9
|
+
'retry',
|
|
10
|
+
'cancel',
|
|
11
|
+
'rm',
|
|
12
|
+
'promote',
|
|
13
|
+
'pause',
|
|
14
|
+
'resume',
|
|
15
|
+
'drain',
|
|
16
|
+
] as const;
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* The drivers a drain may move work ONTO — every one of them durable, and that is the whole rule.
|
|
@@ -13,9 +23,9 @@ export const DRAIN_TARGETS = ['redis', 'nats'] as const;
|
|
|
13
23
|
|
|
14
24
|
export const jobsSpec: CommandSpec = {
|
|
15
25
|
name: 'jobs',
|
|
16
|
-
summary: 'list, show, retry, cancel and
|
|
26
|
+
summary: 'list, show, retry, cancel, remove and promote jobs; pause and resume a queue; drain',
|
|
17
27
|
usage:
|
|
18
|
-
'x jobs [ls|show <id>|retry <id>|cancel <id>|drain --to <driver>] [--queue q] [--state s] [--limit n] [--from-step name] [--reason text] [--to driver] [--dry-run] [--json]',
|
|
28
|
+
'x jobs [ls|show <id>|retry <id>|cancel <id>|rm <id>|promote <id>|pause <queue>|resume <queue>|drain --to <driver>] [--queue q] [--state s] [--name n] [--limit n] [--after cursor] [--from-step name] [--reason text] [--to driver] [--dry-run] [--json]',
|
|
19
29
|
requiresApp: true,
|
|
20
30
|
subcommands: JOBS_SUBCOMMANDS,
|
|
21
31
|
// The bare `x jobs` lists; it never retries, cancels or drains anything.
|
|
@@ -25,6 +35,12 @@ export const jobsSpec: CommandSpec = {
|
|
|
25
35
|
{ name: 'state', type: 'string', summary: 'filter by job state' },
|
|
26
36
|
{ name: 'limit', type: 'string', summary: 'max rows to return' },
|
|
27
37
|
{ name: 'name', type: 'string', summary: 'filter by job name' },
|
|
38
|
+
{
|
|
39
|
+
name: 'after',
|
|
40
|
+
type: 'string',
|
|
41
|
+
summary: 'ls: the next page — the cursor the previous page printed',
|
|
42
|
+
subcommands: ['ls'],
|
|
43
|
+
},
|
|
28
44
|
// Each of these is read by ONE subcommand — `retryJob`, `cancelJob`, `runDrain` — and says
|
|
29
45
|
// so in its own summary. The scope is what makes the parser refuse it anywhere else instead
|
|
30
46
|
// of accepting it and ignoring it: `x db gen --dry-run` parsed and wrote the migration.
|