@ultimat3/cli 12.0.0 → 13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "12.0.0",
3
+ "version": "13.0.0",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,33 +37,34 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
40
- "@ultimat3/action": "12.0.0",
41
- "@ultimat3/admin": "12.0.0",
42
- "@ultimat3/ai": "12.0.0",
43
- "@ultimat3/auth": "12.0.0",
44
- "@ultimat3/cache": "12.0.0",
45
- "@ultimat3/core": "12.0.0",
46
- "@ultimat3/db": "12.0.0",
47
- "@ultimat3/entity": "12.0.0",
48
- "@ultimat3/flags": "12.0.0",
49
- "@ultimat3/http": "12.0.0",
50
- "@ultimat3/i18n": "12.0.0",
51
- "@ultimat3/jobs": "12.0.0",
52
- "@ultimat3/mail": "12.0.0",
53
- "@ultimat3/manifest": "12.0.0",
54
- "@ultimat3/mcp": "12.0.0",
55
- "@ultimat3/money": "12.0.0",
56
- "@ultimat3/policy": "12.0.0",
57
- "@ultimat3/pwa": "12.0.0",
58
- "@ultimat3/query": "12.0.0",
59
- "@ultimat3/realtime": "12.0.0",
60
- "@ultimat3/render": "12.0.0",
61
- "@ultimat3/schema": "12.0.0",
62
- "@ultimat3/scraping": "12.0.0",
63
- "@ultimat3/seo": "12.0.0",
64
- "@ultimat3/storage": "12.0.0",
65
- "@ultimat3/testing": "12.0.0",
66
- "@ultimat3/time": "12.0.0",
40
+ "@ultimat3/action": "13.0.0",
41
+ "@ultimat3/admin": "13.0.0",
42
+ "@ultimat3/ai": "13.0.0",
43
+ "@ultimat3/auth": "13.0.0",
44
+ "@ultimat3/cache": "13.0.0",
45
+ "@ultimat3/core": "13.0.0",
46
+ "@ultimat3/db": "13.0.0",
47
+ "@ultimat3/entity": "13.0.0",
48
+ "@ultimat3/flags": "13.0.0",
49
+ "@ultimat3/http": "13.0.0",
50
+ "@ultimat3/i18n": "13.0.0",
51
+ "@ultimat3/jobs": "13.0.0",
52
+ "@ultimat3/mail": "13.0.0",
53
+ "@ultimat3/manifest": "13.0.0",
54
+ "@ultimat3/mcp": "13.0.0",
55
+ "@ultimat3/money": "13.0.0",
56
+ "@ultimat3/notify": "13.0.0",
57
+ "@ultimat3/policy": "13.0.0",
58
+ "@ultimat3/pwa": "13.0.0",
59
+ "@ultimat3/query": "13.0.0",
60
+ "@ultimat3/realtime": "13.0.0",
61
+ "@ultimat3/render": "13.0.0",
62
+ "@ultimat3/schema": "13.0.0",
63
+ "@ultimat3/scraping": "13.0.0",
64
+ "@ultimat3/seo": "13.0.0",
65
+ "@ultimat3/storage": "13.0.0",
66
+ "@ultimat3/testing": "13.0.0",
67
+ "@ultimat3/time": "13.0.0",
67
68
  "babel-preset-solid": "^1.9.15"
68
69
  }
69
70
  }
package/src/dev-queue.ts CHANGED
@@ -8,11 +8,8 @@ import {
8
8
  type PostgresIdempotencyStore,
9
9
  postgresIdempotencyStore,
10
10
  resetIdempotency,
11
- SQL_AUDIT_TABLE,
12
- SQL_IDEMPOTENCY_TABLE,
13
11
  setIdempotencyStore,
14
12
  } from '@ultimat3/action';
15
- import { SQL_AUTH_LIMIT_TABLES } from '@ultimat3/auth';
16
13
  import type { DbClient, PgliteClient, PostgresClient, SqlFragment } from '@ultimat3/db';
17
14
  import {
18
15
  createPgliteClient,
@@ -23,7 +20,6 @@ import {
23
20
  setDbClient,
24
21
  } from '@ultimat3/db';
25
22
  import type { Tx } from '@ultimat3/entity';
26
- import { SQL_RATE_LIMIT_TABLE } from '@ultimat3/http';
27
23
  import type { EventBus, JobDriver, OutboxStore, PgExecutor } from '@ultimat3/jobs';
28
24
  import {
29
25
  createJobsFacade,
@@ -32,13 +28,13 @@ import {
32
28
  createPgOutboxStore,
33
29
  resetJobDriver,
34
30
  resetJobsFacade,
35
- SQL_JOBS_TABLE,
36
31
  setEventBus,
37
32
  setJobDriver,
38
33
  setJobsFacade,
39
34
  } from '@ultimat3/jobs';
40
35
  import { attachReplica, type ReplicaEnv, replicaUrlFor } from './dev-replica';
41
36
  import type { DevServices } from './dev-services';
37
+ import { applyFrameworkSchema } from './framework-schema';
42
38
  import type { RuntimeOverrides } from './runtime-overrides';
43
39
 
44
40
  /** Both embedded and external clients boot lazily and close explicitly. */
@@ -113,40 +109,18 @@ export function pgExecutorFor(client: DbClient): PgExecutor {
113
109
  /**
114
110
  * Every table this process's framework packages own, applied before anything reads one.
115
111
  *
116
- * PGlite speaks the extended protocol, which carries one statement per round trip, so the DDL is
117
- * applied statement by statement. Safe to split on `;`: every constant is fixed, with no semicolon
118
- * inside a literal, and each package's own SQL test is where that stays true.
112
+ * The LIST is `FRAMEWORK_SCHEMA` and lives in `framework-schema.ts`, not here: this function is on
113
+ * every boot path the framework has — `x dev`, each served role, `x jobs`, `x db backfill`,
114
+ * `x mcp serve` and `ROLE=migrate` all reach it through `startQueue` — so the list it reads is the
115
+ * one place a framework table can be forgotten, and it is worth being a table somebody can read
116
+ * rather than an array literal inside a boot function.
119
117
  *
120
- * `SQL_IDEMPOTENCY_TABLE`, `SQL_RATE_LIMIT_TABLE` and `SQL_AUTH_LIMIT_TABLES` are here and not in
121
- * `@ultimat3/action`, `@ultimat3/http` or `@ultimat3/auth` because a package that holds no
122
- * database dependency cannot apply its own schema
123
- * — the same reason `SQL_JOBS_TABLE` is applied here. Each one absent is the same failure at a
124
- * different door: a retried `POST /api/payments/charge` charges the card twice, and the FIRST
125
- * request a `rateLimitStore` deployment serves dies on a missing `x_rate_limit` relation. The
126
- * table is installed whether or not this boot passes `runtime.rateLimitStore` — `create table if
127
- * not exists` on an unused table costs one round trip at boot, and a store installed later must
128
- * not be the thing that discovers the schema was never applied. The auth pair is the strongest
129
- * case for that rule: `defineAuth` builds its limiter when the APP's modules import, which is
130
- * after this, so the first failed sign-in would otherwise be what discovers the missing relation.
118
+ * Each package's DDL is here and not in `@ultimat3/action`, `@ultimat3/http`, `@ultimat3/auth` or
119
+ * `@ultimat3/notify` because a package that holds no database dependency cannot apply its own
120
+ * schema — the same reason `SQL_JOBS_TABLE` is applied by the boot.
131
121
  */
132
122
  async function applySchema(client: DevDbClient): Promise<void> {
133
- for (const ddl of [
134
- SQL_JOBS_TABLE,
135
- SQL_IDEMPOTENCY_TABLE,
136
- // The DDL only, and deliberately NO `setAuditSink` beside `setIdempotencyStore` below: there
137
- // is no default audit sink on purpose, so `X_AUDIT_SINK_MISSING` keeps firing at boot for an
138
- // app that declares `audit: true` and installs none. Applying the table without installing a
139
- // sink is the same call `SQL_RATE_LIMIT_TABLE` already makes — one round trip at boot on a
140
- // possibly-unused table, against `postgresAuditSink` failing its first write with
141
- // `relation "x_audit" does not exist`.
142
- SQL_AUDIT_TABLE,
143
- SQL_RATE_LIMIT_TABLE,
144
- SQL_AUTH_LIMIT_TABLES,
145
- ]) {
146
- for (const statement of ddl.split(';')) {
147
- if (statement.trim().length > 0) await client.execute(raw(statement));
148
- }
149
- }
123
+ await applyFrameworkSchema((statement) => client.execute(raw(statement)));
150
124
  }
151
125
 
152
126
  /**
@@ -30,6 +30,7 @@ export const CATALOG_PACKAGES = [
30
30
  '@ultimat3/manifest',
31
31
  '@ultimat3/mcp',
32
32
  '@ultimat3/money',
33
+ '@ultimat3/notify',
33
34
  '@ultimat3/policy',
34
35
  '@ultimat3/pwa',
35
36
  '@ultimat3/query',
@@ -49,6 +49,10 @@ export const CLI_OWNED_ERROR_CODES = [
49
49
  // reading — the hole that let `scripts/` hold seven type errors under a green gate.
50
50
  'X_PACKAGE_UNREFERENCED',
51
51
  'X_RELEASE_VERSION_SKEW',
52
+ // The framework's own tables, refused by name rather than by the driver's rejection: a raw
53
+ // `permission denied for schema public` says which statement failed and neither which framework
54
+ // table it was creating nor which package wants it.
55
+ 'X_FRAMEWORK_SCHEMA_FAILED',
52
56
  'X_STORAGE_UNWRITABLE',
53
57
  'X_STORAGE_SECRET_DEV',
54
58
  'X_MANIFEST_STALE',
@@ -198,6 +202,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
198
202
  X_ERROR_CODE_UNDOCUMENTED: 'a shipped error code has no row in the error reference',
199
203
  X_ERROR_CODE_UNREGISTERED: 'the error reference documents a code no package registers',
200
204
  X_ERROR_CODE_UNRESOLVED: 'an error code is written as a name this repository cannot resolve',
205
+ X_FRAMEWORK_SCHEMA_FAILED: 'a framework table could not be created at boot',
201
206
  X_STORAGE_UNWRITABLE: 'the storage disk this process needs cannot be written to',
202
207
  X_STORAGE_SECRET_DEV: 'upload grants would be signed with the shipped development key',
203
208
  X_CLI_UNEXPECTED: 'the CLI itself failed',
@@ -0,0 +1,152 @@
1
+ // The framework's OWN tables, as one table: which package declares each, which relations it
2
+ // creates, and the DDL. One list, read by the one applier, so "installed in dev and not in
3
+ // production" is not a state this framework can be in — `startQueue` is on every boot path there
4
+ // is, `ROLE=migrate` included.
5
+ //
6
+ // A framework table an app has to install by hand is a table that will be missing in production on
7
+ // the one code path that needs it, and it surfaces as a Postgres `42P01` from inside a worker.
8
+
9
+ import { SQL_AUDIT_TABLE, SQL_IDEMPOTENCY_TABLE } from '@ultimat3/action';
10
+ import { AUTH_TABLE_NAMES, AUTH_TABLES, SQL_AUTH_LIMIT_TABLES } from '@ultimat3/auth';
11
+ import { SQL_RATE_LIMIT_TABLE } from '@ultimat3/http';
12
+ import { SQL_JOBS_TABLE } from '@ultimat3/jobs';
13
+ import { SQL_NOTIFY_DELIVERIES_TABLE, SQL_NOTIFY_INBOX_TABLE } from '@ultimat3/notify';
14
+ import { FrameworkSchemaFailedError } from './schema-errors';
15
+
16
+ export interface FrameworkSchema {
17
+ /** The package whose source declares the DDL — where to look when a column is wrong. */
18
+ readonly pkg: string;
19
+ /**
20
+ * Every relation this entry creates. Read by the refusal below, so an operator learns which
21
+ * tables were being installed rather than only which statement failed — and pinned against the
22
+ * DDL text by `framework-schema.test.ts`, so a row cannot claim a table its SQL never creates.
23
+ */
24
+ readonly tables: readonly string[];
25
+ /** One or more statements each; `;` separates. */
26
+ readonly ddl: readonly string[];
27
+ }
28
+
29
+ /**
30
+ * Applied unconditionally, whether or not this boot installs a store behind it.
31
+ *
32
+ * `create table if not exists` on an unused table costs one round trip at boot. The alternative
33
+ * costs a request: a store installed later must never be the thing that discovers the schema was
34
+ * never applied, and several of these are installed AFTER this runs — `defineAuth` builds its
35
+ * limiter when the app's modules import, and `setNotifyStores` is an app's boot line.
36
+ *
37
+ * Ordered so a foreign key never precedes its target. Only `AUTH_TABLES` has any, and they are
38
+ * internal to that entry, which is why it ships as an ordered list rather than one string.
39
+ */
40
+ export const FRAMEWORK_SCHEMA: readonly FrameworkSchema[] = Object.freeze([
41
+ Object.freeze({
42
+ pkg: '@ultimat3/jobs',
43
+ tables: Object.freeze([
44
+ 'x_jobs',
45
+ 'x_job_steps',
46
+ 'x_backfills',
47
+ 'x_outbox',
48
+ 'x_scheduler_state',
49
+ 'x_scheduler_leader',
50
+ 'x_job_leases',
51
+ 'x_job_events',
52
+ ]),
53
+ ddl: Object.freeze([SQL_JOBS_TABLE]),
54
+ }),
55
+ Object.freeze({
56
+ pkg: '@ultimat3/action',
57
+ tables: Object.freeze(['x_idempotency']),
58
+ ddl: Object.freeze([SQL_IDEMPOTENCY_TABLE]),
59
+ }),
60
+ // The DDL only, and deliberately NO `setAuditSink`: there is no default audit sink on purpose,
61
+ // so `X_AUDIT_SINK_MISSING` keeps firing at boot for an app that declares `audit: true` and
62
+ // installs none.
63
+ Object.freeze({
64
+ pkg: '@ultimat3/action',
65
+ tables: Object.freeze(['x_audit']),
66
+ ddl: Object.freeze([SQL_AUDIT_TABLE]),
67
+ }),
68
+ Object.freeze({
69
+ pkg: '@ultimat3/http',
70
+ tables: Object.freeze(['x_rate_limit']),
71
+ ddl: Object.freeze([SQL_RATE_LIMIT_TABLE]),
72
+ }),
73
+ Object.freeze({
74
+ pkg: '@ultimat3/auth',
75
+ tables: Object.freeze(['x_auth_failures', 'x_auth_lockouts']),
76
+ ddl: Object.freeze([SQL_AUTH_LIMIT_TABLES]),
77
+ }),
78
+ /**
79
+ * The five tables `BuiltinAdapter` reads, and the oldest hole in this list.
80
+ *
81
+ * `packages/auth/src/tables.ts` exports them "so an app can paste them into a migration", and
82
+ * nothing in the framework has ever applied them — while `x db gen` diffs `describeEntities()`
83
+ * and these are not `entity()` declarations, so neither half was a file anybody could
84
+ * hand-write. `examples/dummy/CLAUDE.md` records the consequence in its own words: nobody can
85
+ * hold a session in the reference app. Applied here on exactly the rule the rate-limit and audit
86
+ * rows already follow.
87
+ *
88
+ * `AUTH_TABLE_NAMES` rather than five literals: @ultimat3/auth already publishes the list, and a
89
+ * second copy is a second thing to keep right when a table is added.
90
+ */
91
+ Object.freeze({
92
+ pkg: '@ultimat3/auth',
93
+ tables: AUTH_TABLE_NAMES,
94
+ ddl: AUTH_TABLES,
95
+ }),
96
+ /**
97
+ * The delivery ledger is what stops a replayed notifier job sending twice, and it is the entry
98
+ * whose absence is least visible: without the table the ledger's first `claim` raises `42P01`
99
+ * from inside a worker, which reads as a dead-lettered notification rather than as a missing
100
+ * schema. Installed whether or not this boot calls `setNotifyStores`, for the same reason as
101
+ * every row above it — that call is an APP's boot line and runs after this one.
102
+ */
103
+ Object.freeze({
104
+ pkg: '@ultimat3/notify',
105
+ tables: Object.freeze(['x_notify_deliveries']),
106
+ ddl: Object.freeze([SQL_NOTIFY_DELIVERIES_TABLE]),
107
+ }),
108
+ Object.freeze({
109
+ pkg: '@ultimat3/notify',
110
+ tables: Object.freeze(['x_notify_inbox']),
111
+ ddl: Object.freeze([SQL_NOTIFY_INBOX_TABLE]),
112
+ }),
113
+ ]);
114
+
115
+ /** Every relation this boot creates, flattened. */
116
+ export const frameworkTableNames = (): readonly string[] =>
117
+ FRAMEWORK_SCHEMA.flatMap((entry) => [...entry.tables]);
118
+
119
+ /**
120
+ * PGlite speaks the extended protocol, which carries one statement per round trip, so the DDL is
121
+ * applied statement by statement. Safe to split on `;`: every constant is fixed, with no semicolon
122
+ * inside a literal, and each package's own SQL test is where that stays true.
123
+ */
124
+ export const schemaStatements = (ddl: readonly string[]): readonly string[] =>
125
+ ddl.flatMap((text) => text.split(';')).filter((statement) => statement.trim().length > 0);
126
+
127
+ /** One statement, executed. The caller owns the connection; this file owns no database import. */
128
+ export type SchemaExecutor = (statement: string) => Promise<unknown>;
129
+
130
+ /**
131
+ * Apply every entry, in order, and answer what was created.
132
+ *
133
+ * The refusal is the point of the `pkg`/`tables` columns: a raw `permission denied for schema
134
+ * public` names neither the framework table it was creating nor the package that wants it, and a
135
+ * boot failure is read by an operator who has no source tree open.
136
+ */
137
+ export async function applyFrameworkSchema(execute: SchemaExecutor): Promise<readonly string[]> {
138
+ for (const entry of FRAMEWORK_SCHEMA) {
139
+ for (const statement of schemaStatements(entry.ddl)) {
140
+ try {
141
+ await execute(statement);
142
+ } catch (error) {
143
+ throw new FrameworkSchemaFailedError({
144
+ pkg: entry.pkg,
145
+ tables: entry.tables,
146
+ cause: error,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ return frameworkTableNames();
152
+ }
package/src/index.ts CHANGED
@@ -196,6 +196,15 @@ export type { FixHelper, FixScan } from './fix-scan';
196
196
  export { scanFixes, scanFixHelpers, scanFixSites } from './fix-scan';
197
197
  export type { DeclaredFlag } from './flag-reads';
198
198
  export { checkFlagReads, declaredFlags, readsFlag } from './flag-reads';
199
+ export type { FrameworkSchema, SchemaExecutor } from './framework-schema';
200
+ // The framework's own tables, as data. Exported so `scripts/` can read the applier's list without
201
+ // re-deriving it — the shape a ratchet over declared-but-never-applied DDL needs.
202
+ export {
203
+ applyFrameworkSchema,
204
+ FRAMEWORK_SCHEMA,
205
+ frameworkTableNames,
206
+ schemaStatements,
207
+ } from './framework-schema';
199
208
  export type { Guard } from './guards';
200
209
  export { findingProblem, GUARD_DIR, guardFindings, guardPaths } from './guards';
201
210
  // The island bundler, and only its entry point. An island is the one module Ultimate ships to a
@@ -239,6 +248,7 @@ export { flagBool, flagList, flagString, GLOBAL_FLAGS, nearest, parseArgs } from
239
248
  export type { PrerenderedPage, PrerenderOptions, PrerenderReport } from './prerender';
240
249
  export { DEFAULT_ORIGIN, isPrerenderable, prerenderSite } from './prerender';
241
250
  export { COMMANDS, cliVersion, commandFor, SPECS } from './registry';
251
+ export { FrameworkSchemaFailedError } from './schema-errors';
242
252
  export type { MigratedApp, ServedApp, ServeOptions, StartedApp } from './serve';
243
253
  export {
244
254
  CONTAINER_BINDING,
package/src/mcp-errors.ts CHANGED
@@ -96,6 +96,9 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
96
96
  X_RELEASE_VERSION_SKEW: 'bun run scripts/release.ts --bump patch --dry-run --json',
97
97
  // Two real remedies and the command cannot know which one this deployment wants, so it names
98
98
  // the one that inspects the binding rather than guessing between a volume and a bucket.
99
+ // `x db migrate --json` and not `x doctor`: this fires from inside `startQueue`, so the command
100
+ // that re-runs exactly the failing step is the migrate role, and it reports what it applied.
101
+ X_FRAMEWORK_SCHEMA_FAILED: 'x db migrate --json',
99
102
  X_STORAGE_UNWRITABLE: 'x doctor --json',
100
103
  X_STORAGE_SECRET_DEV: 'export STORAGE_SIGNING_SECRET="$(openssl rand -hex 32)"',
101
104
  X_MANIFEST_STALE: 'x manifest --json',
@@ -0,0 +1,28 @@
1
+ // The one code raised while installing the framework's own tables. Apart from `errors.ts` for the
2
+ // reason `packages/jobs/src/backfill-errors.ts` is apart from that package's: one file, one job,
3
+ // and `errors.ts` is at 486 lines against the 500 the `filesize` step enforces. The code, its
4
+ // title and its registration stay in `error-codes.ts`, where every other CLI code lives.
5
+
6
+ import { renderThrowable, UltimateError } from '@ultimat3/core';
7
+
8
+ /**
9
+ * A statement in `FRAMEWORK_SCHEMA` did not apply. Raised in place of the driver's own rejection,
10
+ * which names neither the framework table being created nor the package that wants it — and a boot
11
+ * failure is read by an operator with no source tree open.
12
+ *
13
+ * `renderThrowable` and never `${cause}`: a `catch` binding is annotated by nobody, and a pool
14
+ * rejection is routinely an object whose `toString` throws.
15
+ */
16
+ export class FrameworkSchemaFailedError extends UltimateError {
17
+ constructor(input: { pkg: string; tables: readonly string[]; cause: unknown }) {
18
+ super({
19
+ code: 'X_FRAMEWORK_SCHEMA_FAILED',
20
+ cause: `${input.pkg} could not create ${input.tables.join(', ')}: ${renderThrowable(input.cause)}`,
21
+ // An EDIT plus the command that CONFIRMS it, which is the house shape for a repair the gate
22
+ // cannot perform: the two real causes are a role without `create`, and a relation of that
23
+ // name already present with an incompatible shape.
24
+ fix: `grant create on schema public to the role in DATABASE_URL, then re-run: x db migrate`,
25
+ meta: { pkg: input.pkg, tables: [...input.tables] },
26
+ });
27
+ }
28
+ }