@onreza/sqlx-js 0.5.0 → 0.6.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.
@@ -2,13 +2,7 @@
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { PrepareFatalError, runPrepare, } from "../src/commands/prepare.js";
6
- import { runWatch } from "../src/commands/watch.js";
7
- import { migrateArchiveList, migrateArchiveRestore, migrateCheck, migrateDev, migrateRun, migrateInfo, migrateRevert, migrateAdd, migrateSquash, migrateVerify, } from "../src/commands/migrate.js";
8
- import { applyShadowMigrations, runSchemaCheck, runSchemaDump } from "../src/commands/schema.js";
9
- import { runInit } from "../src/commands/init.js";
10
- import { runPgschemaCommand, runPgschemaInstall } from "../src/commands/pgschema.js";
11
- import { runDoctor } from "../src/commands/doctor.js";
5
+ import { parseArgs } from "node:util";
12
6
  import { assertSupportedRuntime, loadConfig, loadRootEnv } from "../src/config.js";
13
7
  function packageVersion() {
14
8
  const here = dirname(fileURLToPath(import.meta.url));
@@ -22,17 +16,20 @@ function packageVersion() {
22
16
  throw new Error("sqlx-js: cannot locate package.json for version");
23
17
  }
24
18
  const VERSION = packageVersion();
25
- function help() {
26
- console.error(`sqlx-js — compile-time-checked SQL for TypeScript + Postgres (v${VERSION})
19
+ const HELP = {
20
+ root: `sqlx-js — compile-time-checked SQL for TypeScript + Postgres (v${VERSION})
27
21
 
28
22
  usage:
29
23
  sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
30
- sqlx-js doctor [--root <dir>] [--json]
31
- sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
32
- sqlx-js prepare [--check | --verify | --watch] [--json] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
33
- sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
34
- sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
24
+ sqlx-js doctor [--root <dir>] [--dts <path>] [--json]
25
+ sqlx-js db install | check [--root <dir>]
26
+ sqlx-js db plan | apply [--root <dir>] [-- <pgschema args>]
27
+ sqlx-js prepare [--check | --verify | --watch] [--json] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
28
+ sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
29
+ sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
30
+ sqlx-js schema check [--schema <path>] [--shadow-url <url>]
35
31
  sqlx-js --version
32
+ sqlx-js-diagnostics github|unix < prepare-diagnostics.json
36
33
 
37
34
  env:
38
35
  DATABASE_URL=postgres://... (supports sslmode, cert paths, application_name, connect_timeout, statement_timeout)
@@ -49,6 +46,7 @@ flags:
49
46
  --migrations <dir> migrations directory (default: <root>/migrations)
50
47
  --dry-run validate and print migrate run/revert plan without applying migrations
51
48
  --json machine-readable output for prepare and migration inspection/dry-run commands
49
+ --strict-inference fail when query inference degrades or emits unknown types
52
50
  --force allow archive restore to overwrite existing migration files
53
51
  --lock-timeout <ms> advisory-lock acquisition timeout for migrate run/revert/dev/verify/squash
54
52
  --shadow-url <url> use an existing disposable shadow DB instead of auto-creating one
@@ -59,38 +57,208 @@ flags:
59
57
  --manifest <path> LLM schema manifest path (default: <root>/.sqlx-js/schema/schema.md)
60
58
  --no-manifest skip writing the LLM schema manifest during schema dump
61
59
  --schema-provider <name> init schema workflow: builtin (default) or pgschema
62
- `);
63
- process.exit(2);
60
+ `,
61
+ init: `usage: sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]`,
62
+ doctor: `usage: sqlx-js doctor [--root <dir>] [--dts <path>] [--json]`,
63
+ db: `usage: sqlx-js db install | check [--root <dir>] | plan | apply [--root <dir>] [-- <pgschema args>]`,
64
+ prepare: `usage: sqlx-js prepare [--check | --verify | --watch] [--json] [--strict-inference] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]`,
65
+ migrate: `usage: sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] [--strict-inference] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]`,
66
+ schema: `usage: sqlx-js schema dump [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>] | check [--schema <path>] [--shadow-url <url>]`,
67
+ };
68
+ function printHelp(scope, error = false) {
69
+ (error ? console.error : console.log)(HELP[scope]);
64
70
  }
65
- const passthroughIndex = process.argv.indexOf("--");
66
- const cliArgv = passthroughIndex >= 0 ? process.argv.slice(0, passthroughIndex) : process.argv;
67
- const passthroughArgs = passthroughIndex >= 0 ? process.argv.slice(passthroughIndex + 1) : [];
68
- function arg(name, def) {
69
- const eq = `${name}=`;
70
- for (let i = 0; i < cliArgv.length; i++) {
71
- const a = cliArgv[i];
72
- if (a === name)
73
- return cliArgv[i + 1] ?? def;
74
- if (a.startsWith(eq))
75
- return a.slice(eq.length);
76
- }
77
- return def;
71
+ function exitHelp(scope) {
72
+ printHelp(scope);
73
+ process.exit(0);
78
74
  }
79
- function flag(name) {
80
- for (const a of cliArgv) {
81
- if (a === name)
82
- return true;
83
- }
84
- return false;
75
+ function usageError(message, scope = "root") {
76
+ console.error(`sqlx-js: ${message}`);
77
+ printHelp(scope, true);
78
+ process.exit(2);
85
79
  }
86
- const cmd = process.argv[2];
80
+ const rawArgv = process.argv.slice(2);
81
+ const passthroughIndex = rawArgv.indexOf("--");
82
+ const cliArgv = passthroughIndex >= 0 ? rawArgv.slice(0, passthroughIndex) : rawArgv;
83
+ const passthroughArgs = passthroughIndex >= 0 ? rawArgv.slice(passthroughIndex + 1) : [];
84
+ const cmd = cliArgv[0];
85
+ const scopes = new Set(["init", "doctor", "db", "prepare", "migrate", "schema"]);
87
86
  if (cmd === "--version" || cmd === "-v") {
88
87
  console.log(VERSION);
89
88
  process.exit(0);
90
89
  }
91
- if (cmd === "--help" || cmd === "-h" || !cmd) {
92
- help();
90
+ if (!cmd || cmd === "--help" || cmd === "-h")
91
+ exitHelp("root");
92
+ if (!scopes.has(cmd))
93
+ usageError(`unknown command ${JSON.stringify(cmd)}`);
94
+ const scope = cmd;
95
+ if (cliArgv.includes("--help") || cliArgv.includes("-h"))
96
+ exitHelp(scope);
97
+ if (passthroughIndex >= 0 && cmd !== "db")
98
+ usageError("arguments after -- are only supported by sqlx-js db", scope);
99
+ const ROOT_OPTIONS = {
100
+ root: { type: "string" },
101
+ help: { type: "boolean", short: "h" },
102
+ };
103
+ function optionsFor(command, subcommand) {
104
+ if (command === "init")
105
+ return { ...ROOT_OPTIONS, "schema-provider": { type: "string" } };
106
+ if (command === "doctor")
107
+ return { ...ROOT_OPTIONS, dts: { type: "string" }, json: { type: "boolean" } };
108
+ if (command === "db")
109
+ return ROOT_OPTIONS;
110
+ if (command === "prepare") {
111
+ return {
112
+ ...ROOT_OPTIONS,
113
+ dts: { type: "string" },
114
+ check: { type: "boolean" },
115
+ verify: { type: "boolean" },
116
+ watch: { type: "boolean" },
117
+ json: { type: "boolean" },
118
+ "no-prune": { type: "boolean" },
119
+ "shadow-url": { type: "string" },
120
+ migrations: { type: "string" },
121
+ "strict-inference": { type: "boolean" },
122
+ };
123
+ }
124
+ if (command === "schema") {
125
+ const common = {
126
+ ...ROOT_OPTIONS,
127
+ schema: { type: "string" },
128
+ "shadow-url": { type: "string" },
129
+ migrations: { type: "string" },
130
+ };
131
+ return subcommand === "dump"
132
+ ? { ...common, manifest: { type: "string" }, "no-manifest": { type: "boolean" } }
133
+ : common;
134
+ }
135
+ const common = { ...ROOT_OPTIONS, migrations: { type: "string" } };
136
+ if (subcommand === "run") {
137
+ return { ...common, "dry-run": { type: "boolean" }, json: { type: "boolean" }, "lock-timeout": { type: "string" } };
138
+ }
139
+ if (subcommand === "info" || subcommand === "check")
140
+ return { ...common, json: { type: "boolean" } };
141
+ if (subcommand === "dev") {
142
+ return {
143
+ ...common,
144
+ "shadow-admin-url": { type: "string" },
145
+ "shadow-url": { type: "string" },
146
+ "lock-timeout": { type: "string" },
147
+ "no-prune": { type: "boolean" },
148
+ "strict-inference": { type: "boolean" },
149
+ };
150
+ }
151
+ if (subcommand === "verify") {
152
+ return {
153
+ ...common,
154
+ "shadow-admin-url": { type: "string" },
155
+ "shadow-url": { type: "string" },
156
+ "lock-timeout": { type: "string" },
157
+ "strict-inference": { type: "boolean" },
158
+ };
159
+ }
160
+ if (subcommand === "revert") {
161
+ return {
162
+ ...common,
163
+ "dry-run": { type: "boolean" },
164
+ json: { type: "boolean" },
165
+ "shadow-admin-url": { type: "string" },
166
+ "shadow-url": { type: "string" },
167
+ "lock-timeout": { type: "string" },
168
+ };
169
+ }
170
+ if (subcommand === "squash") {
171
+ return {
172
+ ...common,
173
+ "shadow-admin-url": { type: "string" },
174
+ "shadow-url": { type: "string" },
175
+ "lock-timeout": { type: "string" },
176
+ replace: { type: "boolean" },
177
+ "pg-dump": { type: "string" },
178
+ };
179
+ }
180
+ if (subcommand === "archive")
181
+ return { ...common, force: { type: "boolean" } };
182
+ return common;
93
183
  }
184
+ const commandArgv = cliArgv.slice(1);
185
+ const subcommand = commandArgv[0]?.startsWith("-") ? undefined : commandArgv[0];
186
+ let parsed;
187
+ try {
188
+ parsed = parseArgs({
189
+ args: commandArgv,
190
+ options: optionsFor(cmd, subcommand),
191
+ strict: true,
192
+ allowPositionals: true,
193
+ });
194
+ }
195
+ catch (error) {
196
+ usageError(error.message, scope);
197
+ }
198
+ const values = parsed.values;
199
+ const positionals = parsed.positionals;
200
+ function arg(name, def) {
201
+ const value = values[name.replace(/^--/, "")];
202
+ return typeof value === "string" ? value : def;
203
+ }
204
+ function flag(name) {
205
+ return values[name.replace(/^--/, "")] === true;
206
+ }
207
+ function requirePositionals(min, max, label) {
208
+ if (positionals.length < min || positionals.length > max) {
209
+ usageError(`${label}: expected ${min === max ? min : `${min} to ${max}`} positional argument(s)`, scope);
210
+ }
211
+ }
212
+ function validateInvocation() {
213
+ if (cmd === "init" || cmd === "doctor" || cmd === "prepare") {
214
+ requirePositionals(0, 0, cmd);
215
+ return;
216
+ }
217
+ if (cmd === "db") {
218
+ requirePositionals(1, 1, "db");
219
+ const sub = positionals[0];
220
+ if (sub !== "install" && sub !== "check" && sub !== "plan" && sub !== "apply") {
221
+ usageError(`unknown db command ${JSON.stringify(sub)}`, "db");
222
+ }
223
+ if (passthroughArgs.length > 0 && sub !== "plan" && sub !== "apply") {
224
+ usageError(`db ${sub} does not accept arguments after --`, "db");
225
+ }
226
+ return;
227
+ }
228
+ if (cmd === "schema") {
229
+ requirePositionals(1, 1, "schema");
230
+ const sub = positionals[0];
231
+ if (sub !== "dump" && sub !== "check")
232
+ usageError(`unknown schema command ${JSON.stringify(sub)}`, "schema");
233
+ return;
234
+ }
235
+ const sub = positionals[0];
236
+ if (!sub)
237
+ usageError("migrate command is required", "migrate");
238
+ if (["dev", "verify", "run", "info", "check", "revert"].includes(sub)) {
239
+ requirePositionals(1, 1, `migrate ${sub}`);
240
+ return;
241
+ }
242
+ if (sub === "add" || sub === "squash") {
243
+ requirePositionals(2, 2, `migrate ${sub}`);
244
+ return;
245
+ }
246
+ if (sub === "archive") {
247
+ const action = positionals[1];
248
+ if (action === "list") {
249
+ requirePositionals(2, 2, "migrate archive list");
250
+ if (flag("--force"))
251
+ usageError("--force is only supported by migrate archive restore", "migrate");
252
+ }
253
+ else if (action === "restore")
254
+ requirePositionals(3, 3, "migrate archive restore");
255
+ else
256
+ usageError(`unknown migrate archive command ${JSON.stringify(action)}`, "migrate");
257
+ return;
258
+ }
259
+ usageError(`unknown migrate command ${JSON.stringify(sub)}`, "migrate");
260
+ }
261
+ validateInvocation();
94
262
  const root = resolve(arg("--root", process.cwd()));
95
263
  if (cmd !== "doctor") {
96
264
  try {
@@ -102,14 +270,21 @@ if (cmd !== "doctor") {
102
270
  }
103
271
  }
104
272
  let envError;
105
- try {
106
- loadRootEnv(root);
107
- }
108
- catch (e) {
109
- envError = e.message;
110
- if (cmd !== "doctor") {
111
- console.error(envError);
112
- process.exit(2);
273
+ const needsEnvironment = cmd === "doctor" ||
274
+ cmd === "schema" ||
275
+ (cmd === "db" && (positionals[0] === "plan" || positionals[0] === "apply")) ||
276
+ (cmd === "prepare" && !flag("--check")) ||
277
+ (cmd === "migrate" && !["add", "check", "archive"].includes(positionals[0]));
278
+ if (needsEnvironment) {
279
+ try {
280
+ loadRootEnv(root);
281
+ }
282
+ catch (e) {
283
+ envError = e.message;
284
+ if (cmd !== "doctor") {
285
+ console.error(envError);
286
+ process.exit(2);
287
+ }
113
288
  }
114
289
  }
115
290
  const databaseUrl = process.env.DATABASE_URL ?? "";
@@ -118,13 +293,14 @@ const shadowUrl = shadowUrlArg ?? process.env.SHADOW_DATABASE_URL;
118
293
  const shadowAdminUrl = arg("--shadow-admin-url") ?? process.env.SHADOW_ADMIN_DATABASE_URL;
119
294
  const cacheDir = join(root, ".sqlx-js");
120
295
  const dtsArg = arg("--dts");
121
- const dtsPath = dtsArg ? resolve(dtsArg) : join(root, "sqlx-js-env.d.ts");
296
+ const dtsPath = dtsArg ? resolve(root, dtsArg) : join(root, "sqlx-js-env.d.ts");
122
297
  const migrationsDir = join(root, arg("--migrations", "migrations"));
123
298
  const schemaArg = arg("--schema");
124
- const schemaPath = schemaArg ? resolve(schemaArg) : join(root, ".sqlx-js/schema/schema.json");
299
+ const schemaPath = schemaArg ? resolve(root, schemaArg) : join(root, ".sqlx-js/schema/schema.json");
125
300
  const manifestArg = arg("--manifest");
126
- const manifestPath = manifestArg ? resolve(manifestArg) : join(root, ".sqlx-js/schema/schema.md");
301
+ const manifestPath = manifestArg ? resolve(root, manifestArg) : join(root, ".sqlx-js/schema/schema.md");
127
302
  if (cmd === "init") {
303
+ const { runInit } = await import("../src/commands/init.js");
128
304
  const provider = arg("--schema-provider", "builtin");
129
305
  if (provider !== "builtin" && provider !== "pgschema") {
130
306
  console.error("--schema-provider must be builtin or pgschema");
@@ -133,10 +309,12 @@ if (cmd === "init") {
133
309
  runInit({ root, schemaProvider: provider });
134
310
  }
135
311
  else if (cmd === "doctor") {
312
+ const { runDoctor } = await import("../src/commands/doctor.js");
136
313
  await runDoctor({ root, databaseUrl, cacheDir, dtsPath, json: flag("--json"), envError });
137
314
  }
138
315
  else if (cmd === "db") {
139
- const sub = process.argv[3];
316
+ const { runPgschemaCommand, runPgschemaInstall } = await import("../src/commands/pgschema.js");
317
+ const sub = positionals[0];
140
318
  if (sub === "install") {
141
319
  try {
142
320
  await runPgschemaInstall({ root });
@@ -147,8 +325,6 @@ else if (cmd === "db") {
147
325
  }
148
326
  }
149
327
  else {
150
- if (sub !== "check" && sub !== "plan" && sub !== "apply")
151
- help();
152
328
  try {
153
329
  runPgschemaCommand({
154
330
  root,
@@ -165,6 +341,7 @@ else if (cmd === "db") {
165
341
  }
166
342
  }
167
343
  else if (cmd === "prepare") {
344
+ const { PrepareFatalError, runPrepare } = await import("../src/commands/prepare.js");
168
345
  const prepareCheck = flag("--check");
169
346
  const prepareVerify = flag("--verify");
170
347
  const prepareWatch = flag("--watch");
@@ -198,6 +375,9 @@ else if (cmd === "prepare") {
198
375
  if (prepareWatch && prepareJson) {
199
376
  failPrepare("--watch and --json are mutually exclusive", "config");
200
377
  }
378
+ if ((prepareCheck || prepareVerify) && flag("--no-prune")) {
379
+ failPrepare("--no-prune is only supported by prepare and prepare --watch", "config");
380
+ }
201
381
  const prepareShadowUrl = prepareCheck ? undefined : shadowUrl;
202
382
  const prepareDatabaseUrl = prepareShadowUrl ?? databaseUrl;
203
383
  if (!prepareCheck && !prepareDatabaseUrl) {
@@ -212,8 +392,13 @@ else if (cmd === "prepare") {
212
392
  verify: prepareVerify,
213
393
  json: prepareJson,
214
394
  prune: !flag("--no-prune"),
395
+ strictInference: flag("--strict-inference"),
215
396
  };
397
+ const applyShadowMigrations = prepareShadowUrl
398
+ ? (await import("../src/commands/schema.js")).applyShadowMigrations
399
+ : undefined;
216
400
  if (prepareWatch) {
401
+ const { runWatch } = await import("../src/commands/watch.js");
217
402
  await runWatch({
218
403
  ...opts,
219
404
  ...(prepareShadowUrl
@@ -252,7 +437,8 @@ else if (cmd === "prepare") {
252
437
  }
253
438
  }
254
439
  else if (cmd === "schema") {
255
- const sub = process.argv[3];
440
+ const { runSchemaCheck, runSchemaDump } = await import("../src/commands/schema.js");
441
+ const sub = positionals[0];
256
442
  const schemaDatabaseUrl = shadowUrl ?? databaseUrl;
257
443
  if (!schemaDatabaseUrl) {
258
444
  console.error("DATABASE_URL is required for schema commands (or pass --shadow-url)");
@@ -270,19 +456,21 @@ else if (cmd === "schema") {
270
456
  await runSchemaDump(opts);
271
457
  else if (sub === "check")
272
458
  await runSchemaCheck(opts);
273
- else
274
- help();
275
459
  }
276
460
  else if (cmd === "migrate") {
277
- const sub = process.argv[3];
461
+ const { migrateArchiveList, migrateArchiveRestore, migrateCheck, migrateDev, migrateRun, migrateInfo, migrateRevert, migrateAdd, migrateSquash, migrateVerify, } = await import("../src/commands/migrate.js");
462
+ const sub = positionals[0];
278
463
  const revertDryRun = sub === "revert" && flag("--dry-run");
279
464
  const workflowShadowOnly = ((sub === "dev" || sub === "verify" || sub === "squash") && !!shadowUrl) || (revertDryRun && !!shadowUrl);
280
- if (!databaseUrl && sub !== "add" && sub !== "check" && sub !== "squash" && sub !== "archive" && !workflowShadowOnly) {
465
+ if (!databaseUrl && sub !== "add" && sub !== "check" && sub !== "archive" && !workflowShadowOnly) {
281
466
  console.error("DATABASE_URL is required");
282
467
  process.exit(2);
283
468
  }
284
469
  const tRaw = arg("--lock-timeout");
285
470
  const lockTimeoutMs = tRaw ? Number(tRaw) : undefined;
471
+ if (lockTimeoutMs !== undefined && !Number.isFinite(lockTimeoutMs)) {
472
+ usageError("--lock-timeout must be a finite number of milliseconds", "migrate");
473
+ }
286
474
  if (sub === "dev") {
287
475
  await migrateDev({
288
476
  root,
@@ -294,6 +482,7 @@ else if (cmd === "migrate") {
294
482
  shadowUrl,
295
483
  shadowAdminUrl,
296
484
  lockTimeoutMs,
485
+ strictInference: flag("--strict-inference"),
297
486
  });
298
487
  }
299
488
  else if (sub === "verify") {
@@ -306,15 +495,18 @@ else if (cmd === "migrate") {
306
495
  shadowUrl,
307
496
  shadowAdminUrl,
308
497
  lockTimeoutMs,
498
+ strictInference: flag("--strict-inference"),
309
499
  });
310
500
  }
311
501
  else if (sub === "run") {
312
502
  await migrateRun({ databaseUrl, migrationsDir, lockTimeoutMs, dryRun: flag("--dry-run"), json: flag("--json") });
313
503
  }
314
- else if (sub === "info")
504
+ else if (sub === "info") {
315
505
  await migrateInfo({ databaseUrl, migrationsDir, json: flag("--json") });
316
- else if (sub === "check")
506
+ }
507
+ else if (sub === "check") {
317
508
  migrateCheck({ migrationsDir, json: flag("--json") });
509
+ }
318
510
  else if (sub === "revert") {
319
511
  await migrateRevert({
320
512
  databaseUrl,
@@ -327,19 +519,11 @@ else if (cmd === "migrate") {
327
519
  });
328
520
  }
329
521
  else if (sub === "add") {
330
- const name = process.argv[4];
331
- if (!name) {
332
- console.error("migrate add: name required");
333
- process.exit(2);
334
- }
522
+ const name = positionals[1];
335
523
  migrateAdd({ databaseUrl, migrationsDir, name });
336
524
  }
337
525
  else if (sub === "squash") {
338
- const name = process.argv[4];
339
- if (!name) {
340
- console.error("migrate squash: name required");
341
- process.exit(2);
342
- }
526
+ const name = positionals[1];
343
527
  await migrateSquash({
344
528
  databaseUrl,
345
529
  migrationsDir,
@@ -352,23 +536,16 @@ else if (cmd === "migrate") {
352
536
  });
353
537
  }
354
538
  else if (sub === "archive") {
355
- const action = process.argv[4];
356
- if (action === "list")
539
+ const action = positionals[1];
540
+ if (action === "list") {
357
541
  migrateArchiveList({ migrationsDir });
542
+ }
358
543
  else if (action === "restore") {
359
- const name = process.argv[5];
360
- if (!name) {
361
- console.error("migrate archive restore: name required");
362
- process.exit(2);
363
- }
544
+ const name = positionals[2];
364
545
  migrateArchiveRestore({ migrationsDir, name, force: flag("--force") });
365
546
  }
366
- else
367
- help();
368
547
  }
369
- else
370
- help();
371
548
  }
372
549
  else {
373
- help();
550
+ usageError(`unknown command ${JSON.stringify(cmd)}`);
374
551
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  const CONFIG_TEMPLATE = `import { defineConfig } from "@onreza/sqlx-js";
4
4
 
@@ -33,10 +33,23 @@ const PGSCHEMA_TEMPLATE = `-- Desired PostgreSQL schema managed by pgschema.
33
33
  -- sqlx-js db plan -- --output-json plan.json
34
34
  -- sqlx-js db apply -- --auto-approve
35
35
  `;
36
+ const DTS_TEMPLATE = `// AUTO-GENERATED by sqlx-js. Do not edit.
37
+ // Run \`sqlx-js prepare\` to regenerate.
38
+
39
+ declare module "@onreza/sqlx-js" {
40
+ interface KnownQueries {}
41
+ interface KnownFileQueries {}
42
+ interface KnownFunctions {}
43
+ }
44
+
45
+ export {};
46
+ `;
36
47
  export function runInit(opts) {
37
48
  const log = opts.log ?? console.log;
38
49
  const created = [];
50
+ const updated = [];
39
51
  const skipped = [];
52
+ const notes = [];
40
53
  const ensureFile = (rel, content) => {
41
54
  const full = join(opts.root, rel);
42
55
  if (existsSync(full)) {
@@ -63,10 +76,81 @@ export function runInit(opts) {
63
76
  else
64
77
  ensureDir("migrations");
65
78
  ensureFile(".env.example", ENV_TEMPLATE);
79
+ ensureFile("sqlx-js-env.d.ts", DTS_TEMPLATE);
80
+ const updateJson = (rel, mutate) => {
81
+ const full = join(opts.root, rel);
82
+ if (!existsSync(full))
83
+ return;
84
+ const source = readFileSync(full, "utf8");
85
+ let value;
86
+ try {
87
+ value = JSON.parse(source);
88
+ }
89
+ catch {
90
+ notes.push(`${rel}: manual update required because the file is not strict JSON`);
91
+ return;
92
+ }
93
+ if (!value || typeof value !== "object" || Array.isArray(value))
94
+ return;
95
+ if (!mutate(value))
96
+ return;
97
+ const indent = source.match(/\n([ \t]+)"/)?.[1] ?? " ";
98
+ writeFileSync(full, JSON.stringify(value, null, indent) + "\n");
99
+ updated.push(rel);
100
+ };
101
+ updateJson("tsconfig.json", (value) => {
102
+ const files = value.files;
103
+ if (files !== undefined && (!Array.isArray(files) || !files.every((item) => typeof item === "string"))) {
104
+ notes.push("tsconfig.json: manual update required because files is not an array of strings");
105
+ return false;
106
+ }
107
+ if (Array.isArray(files)) {
108
+ if (files.includes("sqlx-js-env.d.ts"))
109
+ return false;
110
+ files.push("sqlx-js-env.d.ts");
111
+ return true;
112
+ }
113
+ const include = value.include;
114
+ if (include !== undefined && (!Array.isArray(include) || !include.every((item) => typeof item === "string"))) {
115
+ notes.push("tsconfig.json: manual update required because include is not an array of strings");
116
+ return false;
117
+ }
118
+ if (Array.isArray(include)) {
119
+ if (include.includes("sqlx-js-env.d.ts"))
120
+ return false;
121
+ include.push("sqlx-js-env.d.ts");
122
+ return true;
123
+ }
124
+ return false;
125
+ });
126
+ updateJson("package.json", (value) => {
127
+ if (value.scripts !== undefined && (!value.scripts || typeof value.scripts !== "object" || Array.isArray(value.scripts))) {
128
+ notes.push("package.json: manual script update required because scripts is not an object");
129
+ return false;
130
+ }
131
+ const scripts = (value.scripts ??= {});
132
+ const defaults = {
133
+ "sqlx:prepare": "sqlx-js prepare",
134
+ "sqlx:check": "sqlx-js prepare --check",
135
+ "sqlx:verify": "sqlx-js prepare --verify --strict-inference",
136
+ };
137
+ let changed = false;
138
+ for (const [name, command] of Object.entries(defaults)) {
139
+ if (scripts[name] !== undefined)
140
+ continue;
141
+ scripts[name] = command;
142
+ changed = true;
143
+ }
144
+ return changed;
145
+ });
66
146
  for (const f of created)
67
147
  log(`created ${f}`);
148
+ for (const f of updated)
149
+ log(`updated ${f}`);
68
150
  for (const f of skipped)
69
151
  log(`exists ${f} (left unchanged)`);
152
+ for (const note of notes)
153
+ log(`manual ${note}`);
70
154
  log("");
71
155
  log("Next steps:");
72
156
  log(" 1. Set DATABASE_URL (see .env.example).");
@@ -78,7 +162,7 @@ export function runInit(opts) {
78
162
  }
79
163
  else {
80
164
  log(" 2. Add a migration: sqlx-js migrate add init");
81
- log(" 3. Make sure tsconfig.json \"include\" covers sqlx-js-env.d.ts.");
82
- log(" 4. Develop locally: sqlx-js migrate dev");
165
+ log(" 3. Check the project setup: sqlx-js doctor");
166
+ log(" 4. Develop locally: sqlx-js migrate dev --strict-inference");
83
167
  }
84
168
  }