naro-cli 0.6.0 → 0.7.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/dist/index.js CHANGED
@@ -3,16 +3,20 @@ import {
3
3
  NaroClient,
4
4
  NaroError,
5
5
  clearSession,
6
+ compareMigrations,
6
7
  controlPlaneUrl,
7
8
  createAuthClient,
8
9
  deployDirectory,
9
10
  detectGitInfo,
10
11
  ensureGitignoreHasNaro,
12
+ generateTypes,
11
13
  isExpired,
12
14
  isNaroError,
13
15
  isTerminalStatus,
16
+ listMigrationFiles,
14
17
  loadStoredSession,
15
18
  login,
19
+ nextMigrationFileName,
16
20
  openBrowser,
17
21
  parseGitHubRemote,
18
22
  projectPath,
@@ -20,13 +24,16 @@ import {
20
24
  readProjectLink,
21
25
  refreshStoredSession,
22
26
  removeProjectLink,
27
+ renderRows,
23
28
  resolveAuth,
29
+ resolveMigrationsDir,
24
30
  saveSession,
25
31
  serializeDotenv,
26
32
  slugify,
33
+ summarizeStatement,
27
34
  toNaroError,
28
35
  writeProjectLink
29
- } from "./chunk-TYBC3YSK.js";
36
+ } from "./chunk-E2M6CQL7.js";
30
37
 
31
38
  // src/args.ts
32
39
  var BOOLEAN_FLAGS = {
@@ -42,7 +49,10 @@ var BOOLEAN_FLAGS = {
42
49
  "--production": "prod",
43
50
  "--follow": "follow",
44
51
  "-f": "follow",
45
- "--reveal": "reveal"
52
+ "--reveal": "reveal",
53
+ "--wait": "wait",
54
+ "--no-www": "noWww",
55
+ "--dry-run": "dryRun"
46
56
  };
47
57
  var STRING_FLAGS = {
48
58
  "--token": "token",
@@ -54,7 +64,10 @@ var STRING_FLAGS = {
54
64
  "--branch": "branch",
55
65
  "--type": "type",
56
66
  "--env": "env",
57
- "--level": "level"
67
+ "--level": "level",
68
+ "--file": "file",
69
+ "--output": "output",
70
+ "--dir": "dir"
58
71
  };
59
72
  function parseArgs(argv) {
60
73
  const flags = {
@@ -65,7 +78,9 @@ function parseArgs(argv) {
65
78
  version: false,
66
79
  prod: false,
67
80
  follow: false,
68
- reveal: false
81
+ reveal: false,
82
+ wait: false,
83
+ noWww: false
69
84
  };
70
85
  const positionals = [];
71
86
  const unknown = [];
@@ -122,6 +137,14 @@ var Output = class {
122
137
  if (this.json) this.streams.stderr(`${message}
123
138
  `);
124
139
  else this.streams.stdout(`${message}
140
+ `);
141
+ }
142
+ /**
143
+ * A warning that must never land in stdout, in either mode - for commands
144
+ * whose stdout is the deliverable itself (`naro gen types > db.d.ts`).
145
+ */
146
+ warn(message) {
147
+ this.streams.stderr(`${message}
125
148
  `);
126
149
  }
127
150
  /** Extra diagnostics, only with --debug. */
@@ -197,7 +220,7 @@ var TtyPrompter = class {
197
220
  });
198
221
  try {
199
222
  return await new Promise(
200
- (resolve) => rl.question(question, resolve)
223
+ (resolve3) => rl.question(question, resolve3)
201
224
  );
202
225
  } finally {
203
226
  rl.close();
@@ -218,7 +241,7 @@ var TtyPrompter = class {
218
241
  });
219
242
  try {
220
243
  const answer = new Promise(
221
- (resolve) => rl.question(question, resolve)
244
+ (resolve3) => rl.question(question, resolve3)
222
245
  );
223
246
  muted = true;
224
247
  const value = await answer;
@@ -329,8 +352,8 @@ function createContext(flags, positionals = []) {
329
352
  }
330
353
 
331
354
  // src/router.ts
332
- import { readFileSync } from "fs";
333
- import { dirname, join as join2 } from "path";
355
+ import { readFileSync as readFileSync3 } from "fs";
356
+ import { dirname as dirname2, join as join3 } from "path";
334
357
  import { fileURLToPath } from "url";
335
358
 
336
359
  // src/commands/auth.ts
@@ -414,11 +437,140 @@ async function runToken(context) {
414
437
  );
415
438
  }
416
439
 
440
+ // src/commands/confirm.ts
441
+ async function confirmOrAbort(context, question) {
442
+ if (context.flags.yes) return;
443
+ if (context.flags.json || !isInteractive()) {
444
+ throw new NaroError(
445
+ "CONFIRMATION_REQUIRED",
446
+ `${question} Re-run with --yes to confirm.`
447
+ );
448
+ }
449
+ const confirmed = await context.prompter.confirm(question);
450
+ if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
451
+ }
452
+
453
+ // src/commands/cancel.ts
454
+ async function runCancel(context) {
455
+ const [deploymentId] = context.positionalOrThrow(
456
+ 1,
457
+ "Usage: naro cancel <deployment-id>"
458
+ );
459
+ const client = await context.client();
460
+ const projectId = await context.resolveProjectId();
461
+ await confirmOrAbort(context, `Cancel the build ${String(deploymentId)}?`);
462
+ const canceled = await client.deployments.cancel(
463
+ projectId,
464
+ deploymentId
465
+ );
466
+ context.output.result(
467
+ {
468
+ deploymentId: canceled.id,
469
+ projectId,
470
+ status: canceled.status,
471
+ errorCode: canceled.errorCode
472
+ },
473
+ [`\u2705 Canceled ${canceled.id}`]
474
+ );
475
+ }
476
+
477
+ // src/commands/db.ts
478
+ import { readFileSync } from "fs";
479
+ import { resolve } from "path";
480
+ var processStdin = {
481
+ stdinIsTty: () => process.stdin.isTTY === true,
482
+ readStdin
483
+ };
484
+ var USAGE = "Usage: naro db query <sql> | --file <path> | (stdin)";
485
+ var ROW_CAP_NOTICE = "\u26A0 Output was cut at 10,000 rows. Add a LIMIT to see the rest.";
486
+ function humanSize(bytes) {
487
+ const units = ["B", "KB", "MB", "GB"];
488
+ let value = bytes;
489
+ let unit = 0;
490
+ while (value >= 1024 && unit < units.length - 1) {
491
+ value /= 1024;
492
+ unit += 1;
493
+ }
494
+ return `${unit === 0 ? String(value) : value.toFixed(1)} ${units[unit] ?? "B"}`;
495
+ }
496
+ function describeLines(info) {
497
+ const lines = [
498
+ `Provider: ${info.provider}`,
499
+ `Status: ${info.status}`,
500
+ `Binding: env.${info.binding}`
501
+ ];
502
+ if (info.resourceName)
503
+ lines.push(
504
+ `Database: ${info.resourceName}${info.resourceId ? ` (${info.resourceId})` : ""}`
505
+ );
506
+ if (info.cloudflare) {
507
+ const size = info.cloudflare.fileSize === null ? "?" : humanSize(info.cloudflare.fileSize);
508
+ const tables = info.cloudflare.numTables === null ? "?" : String(info.cloudflare.numTables);
509
+ lines.push(`Size: ${size} \xB7 Tables: ${tables}`);
510
+ }
511
+ const { limits } = info;
512
+ lines.push(
513
+ `Limits: ${humanSize(limits.maxSizeBytes)} per database \xB7 ${humanSize(limits.maxStatementBytes)} per statement \xB7 ${String(limits.maxParams)} params \xB7 ${String(limits.maxStatements)} statements per request \xB7 ${String(limits.maxQuerySeconds)} s per query`
514
+ );
515
+ if (info.provider === "none")
516
+ lines.push("Run `naro db enable` to create the database.");
517
+ return lines;
518
+ }
519
+ async function runDbInfo(context) {
520
+ const client = await context.client();
521
+ const projectId = await context.resolveProjectId();
522
+ const database = await client.db.info(projectId);
523
+ context.output.result({ projectId, database }, describeLines(database));
524
+ }
525
+ async function runDbEnable(context) {
526
+ const client = await context.client();
527
+ const projectId = await context.resolveProjectId();
528
+ const database = await client.db.enable(projectId);
529
+ context.output.result({ projectId, database }, [
530
+ `\u2705 Database enabled \u2014 ${database.resourceName ?? "(pending)"} (${database.status})`,
531
+ ...describeLines(database)
532
+ ]);
533
+ }
534
+ async function readSql(context, deps) {
535
+ const positional = context.positionals[0];
536
+ let sql;
537
+ if (positional !== void 0) {
538
+ sql = positional;
539
+ } else if (context.flags.file !== void 0) {
540
+ try {
541
+ sql = readFileSync(resolve(context.cwd, context.flags.file), "utf8");
542
+ } catch {
543
+ throw new NaroError(
544
+ "VALIDATION_FAILED",
545
+ `Could not read ${context.flags.file}.`
546
+ );
547
+ }
548
+ } else if (!deps.stdinIsTty()) {
549
+ sql = await deps.readStdin();
550
+ }
551
+ if (sql === void 0 || sql.trim().length === 0)
552
+ throw new NaroError("VALIDATION_FAILED", USAGE);
553
+ return sql;
554
+ }
555
+ async function runDbQuery(context, deps = processStdin) {
556
+ const sql = await readSql(context, deps);
557
+ const client = await context.client();
558
+ const projectId = await context.resolveProjectId();
559
+ const result = await client.db.query(projectId, { sql });
560
+ const lines = [];
561
+ result.statements.forEach((statement, index) => {
562
+ if (index > 0) lines.push("");
563
+ lines.push(...renderRows(statement), summarizeStatement(statement));
564
+ });
565
+ if (result.truncated) lines.push(ROW_CAP_NOTICE);
566
+ context.output.result({ projectId, result }, lines);
567
+ }
568
+
417
569
  // src/commands/git.ts
418
570
  var defaults = {
419
571
  detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
420
572
  openBrowser,
421
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
573
+ sleep: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms))
422
574
  };
423
575
  function connected(context, project) {
424
576
  const branch = project.defaultBranch;
@@ -904,32 +1056,89 @@ async function runInspect(context) {
904
1056
  ]);
905
1057
  }
906
1058
 
907
- // src/commands/confirm.ts
908
- async function confirmOrAbort(context, question) {
909
- if (context.flags.yes) return;
910
- if (context.flags.json || !isInteractive()) {
911
- throw new NaroError(
912
- "CONFIRMATION_REQUIRED",
913
- `${question} Re-run with --yes to confirm.`
1059
+ // src/commands/domains.ts
1060
+ var defaults2 = {
1061
+ sleep: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms)),
1062
+ now: () => Date.now()
1063
+ };
1064
+ var FIRST_DELAY_MS = 5e3;
1065
+ var MAX_DELAY_MS = 3e4;
1066
+ var WAIT_TIMEOUT_MS = 10 * 6e4;
1067
+ function isReady(domain) {
1068
+ return domain.ready ?? (domain.status === "active" && domain.sslStatus === "active");
1069
+ }
1070
+ function isTerminal(domain) {
1071
+ return domain.ownership === "deleted" || domain.status === "blocked" || domain.status === "moved" || domain.status === "deleted";
1072
+ }
1073
+ var AXIS_DONE = /* @__PURE__ */ new Set(["verified", "active", "live"]);
1074
+ var AXIS_FAILED = /* @__PURE__ */ new Set(["failed", "deleted"]);
1075
+ function axisMark(value) {
1076
+ if (AXIS_DONE.has(value)) return "\u2713";
1077
+ if (AXIS_FAILED.has(value)) return "\u2717";
1078
+ return "\xB7";
1079
+ }
1080
+ function axisLines(domain) {
1081
+ const axes = [
1082
+ ["ownership", domain.ownership],
1083
+ ["certificate", domain.certificate],
1084
+ ["routing", domain.routing]
1085
+ ];
1086
+ return axes.filter((axis) => axis[1] !== void 0).map(
1087
+ ([name, value]) => ` ${`${name}:`.padEnd(13)}${axisMark(value)} ${value}`
1088
+ );
1089
+ }
1090
+ function axisSummary(domain) {
1091
+ const axes = [
1092
+ domain.ownership,
1093
+ domain.certificate,
1094
+ domain.routing
1095
+ ].filter((value) => value !== void 0);
1096
+ return axes.length > 0 ? axes.join(" \xB7 ") : `${domain.status} \xB7 ssl ${domain.sslStatus}`;
1097
+ }
1098
+ function recordLines(domain) {
1099
+ const instructions = domain.instructions ?? [];
1100
+ if (instructions.length > 0) {
1101
+ const lines2 = [" Required DNS records:"];
1102
+ for (const instruction of instructions) {
1103
+ lines2.push(
1104
+ ` ${instruction.type.padEnd(5)} ${instruction.name}`,
1105
+ ` host: ${instruction.host.padEnd(24)} value: ${instruction.value}`
1106
+ );
1107
+ if (instruction.note) lines2.push(` note: ${instruction.note}`);
1108
+ }
1109
+ return lines2;
1110
+ }
1111
+ if (domain.verification.length === 0) return [];
1112
+ const lines = [" Required DNS records:"];
1113
+ for (const record of domain.verification) {
1114
+ lines.push(
1115
+ ` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
1116
+ ` Value: ${record.value}`
914
1117
  );
915
1118
  }
916
- const confirmed = await context.prompter.confirm(question);
917
- if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
1119
+ return lines;
918
1120
  }
919
-
920
- // src/commands/domains.ts
921
1121
  function domainLines(domain) {
922
1122
  const lines = [
923
- ` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}`
1123
+ ` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}`,
1124
+ ...axisLines(domain)
924
1125
  ];
925
- if (domain.status !== "active" && domain.verification.length > 0) {
926
- lines.push(" Required DNS records:");
927
- for (const record of domain.verification) {
928
- lines.push(
929
- ` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
930
- ` Value: ${record.value}`
931
- );
932
- }
1126
+ if (domain.redirectTo) {
1127
+ const status = domain.redirectStatus ?? 308;
1128
+ lines.push(` redirects to ${domain.redirectTo} (${String(status)})`);
1129
+ }
1130
+ if (domain.cause) {
1131
+ lines.push(` \u2717 ${domain.cause.message}`);
1132
+ if (domain.cause.fix) lines.push(` \u2192 ${domain.cause.fix}`);
1133
+ }
1134
+ if (isReady(domain)) return lines;
1135
+ lines.push(...recordLines(domain));
1136
+ const guidance = domain.guidance;
1137
+ if (guidance && guidance.notes.length > 0) {
1138
+ lines.push(
1139
+ guidance.apex ? ` Apex domain (${domain.hostname}):` : " Note:"
1140
+ );
1141
+ for (const note of guidance.notes) lines.push(` ${note}`);
933
1142
  }
934
1143
  return lines;
935
1144
  }
@@ -937,28 +1146,141 @@ async function runDomainsList(context) {
937
1146
  const client = await context.client();
938
1147
  const projectId = await context.resolveProjectId();
939
1148
  const domains = await client.domains.list(projectId);
1149
+ const pending = domains.filter((domain) => !isReady(domain));
940
1150
  context.output.result(
941
1151
  { projectId, domains },
942
1152
  domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
943
1153
  `Domains (${String(domains.length)}):`,
944
- ...domains.flatMap(domainLines)
1154
+ ...domains.flatMap(domainLines),
1155
+ // list reads our stored copy; only verify goes and looks again.
1156
+ ...pending.length > 0 ? ["", "Re-check DNS now: naro domains verify"] : []
945
1157
  ]
946
1158
  );
947
1159
  }
948
1160
  async function runDomainsAdd(context) {
949
1161
  const [hostname] = context.positionalOrThrow(
950
1162
  1,
951
- "Usage: naro domains add <hostname>"
1163
+ "Usage: naro domains add <hostname> [--no-www]"
952
1164
  );
953
1165
  const client = await context.client();
954
1166
  const projectId = await context.resolveProjectId();
955
- const domain = await client.domains.add(projectId, hostname);
956
- context.output.result({ projectId, domain }, [
957
- `\u2705 Domain added: ${domain.hostname}`,
958
- "",
959
- ...domainLines(domain),
1167
+ const withWww = context.flags.noWww !== true;
1168
+ const domain = await client.domains.add(projectId, hostname, {
1169
+ withWww
1170
+ });
1171
+ const companion = domain.companion;
1172
+ const apex = domain.apex ?? domain.guidance?.apex ?? false;
1173
+ const alternative = domain.guidance?.alternative ?? `www.${domain.hostname}`;
1174
+ context.output.result(
1175
+ { projectId, domain, withWww, ...companion ? { companion } : {} },
1176
+ [
1177
+ `\u2705 Domain added: ${domain.hostname}`,
1178
+ "",
1179
+ ...domainLines(domain),
1180
+ ...companion ? [
1181
+ "",
1182
+ ` Also registered ${companion.hostname} \u2014 an apex domain often cannot hold`,
1183
+ " the CNAME above, and this form always can.",
1184
+ "",
1185
+ ...domainLines(companion)
1186
+ ] : [],
1187
+ ...!companion && apex && !withWww ? [
1188
+ "",
1189
+ ` --no-www: ${domain.hostname} was registered on its own. If your registrar`,
1190
+ ` refuses the record above, run \`naro domains add ${alternative}\`.`
1191
+ ] : [],
1192
+ "",
1193
+ isReady(domain) ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains verify`."
1194
+ ]
1195
+ );
1196
+ }
1197
+ async function verifyAll(client, projectId, hostname, auto = false) {
1198
+ if (hostname !== void 0)
1199
+ return [
1200
+ await client.domains.verifyByHostname(projectId, hostname, { auto })
1201
+ ];
1202
+ const listed = await client.domains.list(projectId);
1203
+ const verified = [];
1204
+ for (const domain of listed)
1205
+ verified.push(await client.domains.verify(projectId, domain.id, { auto }));
1206
+ return verified;
1207
+ }
1208
+ async function runDomainsVerify(context, overrides = {}) {
1209
+ const deps = { ...defaults2, ...overrides };
1210
+ const client = await context.client();
1211
+ const projectId = await context.resolveProjectId();
1212
+ const hostname = context.positionals[0];
1213
+ const wait = context.flags.wait === true;
1214
+ const progress = (line) => {
1215
+ if (!context.flags.json) context.output.log(line);
1216
+ };
1217
+ let domains = await verifyAll(client, projectId, hostname);
1218
+ if (domains.length === 0) {
1219
+ context.output.result({ projectId, domains, ready: false }, [
1220
+ "No custom domains. Add one with `naro domains add example.com`."
1221
+ ]);
1222
+ return;
1223
+ }
1224
+ let timedOut = false;
1225
+ let abandoned = false;
1226
+ if (wait && !domains.every(isReady)) {
1227
+ const deadline = deps.now() + WAIT_TIMEOUT_MS;
1228
+ let delay = FIRST_DELAY_MS;
1229
+ for (; ; ) {
1230
+ const pending = domains.filter((domain) => !isReady(domain));
1231
+ if (pending.length > 0 && pending.every(isTerminal)) {
1232
+ abandoned = true;
1233
+ break;
1234
+ }
1235
+ const remaining = deadline - deps.now();
1236
+ if (remaining <= 0) {
1237
+ timedOut = true;
1238
+ break;
1239
+ }
1240
+ for (const domain of pending)
1241
+ progress(
1242
+ isTerminal(domain) ? `\u26A0 ${domain.hostname}: ${axisSummary(domain)} \u2014 waiting cannot fix this one` : `\u25B8 ${domain.hostname}: ${axisSummary(domain)}`
1243
+ );
1244
+ await deps.sleep(Math.min(delay, remaining));
1245
+ delay = Math.min(delay * 2, MAX_DELAY_MS);
1246
+ domains = await verifyAll(client, projectId, hostname, true);
1247
+ if (domains.every(isReady)) break;
1248
+ }
1249
+ }
1250
+ const ready = domains.every(isReady);
1251
+ const body = domains.flatMap(domainLines);
1252
+ if (!ready) {
1253
+ for (const line of body) progress(line);
1254
+ const notReady = domains.filter((domain) => !isReady(domain));
1255
+ const names = notReady.map((domain) => domain.hostname).join(", ");
1256
+ const details = { projectId, domains, ready: false };
1257
+ if (timedOut) {
1258
+ const cause2 = notReady[0]?.cause;
1259
+ throw new NaroError(
1260
+ "TIMEOUT",
1261
+ `${names} did not go live within 10 minutes.${cause2 ? ` ${cause2.message}` : ""} DNS changes can take longer to propagate \u2014 re-run \`naro domains verify\` later.`,
1262
+ { details }
1263
+ );
1264
+ }
1265
+ if (abandoned) {
1266
+ const blocked = domains.find(isTerminal);
1267
+ throw new NaroError(
1268
+ blocked?.cause?.code ?? "DOMAIN_VERIFICATION_FAILED",
1269
+ blocked?.cause?.message ?? `${names} cannot be verified (status: ${blocked?.status ?? "unknown"}). Remove it and add it again.`,
1270
+ { details }
1271
+ );
1272
+ }
1273
+ const cause = notReady[0]?.cause;
1274
+ throw new NaroError(
1275
+ cause?.code ?? "DOMAIN_NOT_READY",
1276
+ `${names} is not live yet.${cause ? ` ${cause.message}` : ""} Add the records above, then run \`naro domains verify --wait\`.`,
1277
+ { details }
1278
+ );
1279
+ }
1280
+ context.output.result({ projectId, domains, ready }, [
1281
+ ...body,
960
1282
  "",
961
- domain.status === "active" ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains list` to refresh."
1283
+ domains.length === 1 ? `\u2705 ${domains[0]?.hostname ?? ""} is live.` : "\u2705 Every domain is live."
962
1284
  ]);
963
1285
  }
964
1286
  async function runDomainsRemove(context) {
@@ -1100,10 +1422,181 @@ ${serializeDotenv(vars)}`,
1100
1422
  );
1101
1423
  }
1102
1424
 
1425
+ // src/commands/gen.ts
1426
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1427
+ import { dirname, resolve as resolve2 } from "path";
1428
+
1429
+ // src/commands/migration.ts
1430
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1431
+ import { join as join2, relative } from "path";
1432
+ var processStdin2 = {
1433
+ stdinIsTty: () => process.stdin.isTTY === true,
1434
+ readStdin
1435
+ };
1436
+ function migrationsDir(context) {
1437
+ return resolveMigrationsDir({
1438
+ baseDir: context.cwd,
1439
+ rootDirectory: context.flags.root,
1440
+ dir: context.flags.dir
1441
+ });
1442
+ }
1443
+ function relativeToCwd(context, path) {
1444
+ const rel = relative(context.cwd, path);
1445
+ return rel === "" ? "." : rel;
1446
+ }
1447
+ async function appliedMigrations(client, projectId) {
1448
+ try {
1449
+ return await client.db.migrations(projectId);
1450
+ } catch (error) {
1451
+ if (isNaroError(error) && error.code === "DATABASE_NOT_READY") return [];
1452
+ throw error;
1453
+ }
1454
+ }
1455
+ function printable(text) {
1456
+ let out = "";
1457
+ for (const char of text) {
1458
+ const code = char.charCodeAt(0);
1459
+ const control = code < 32 || code >= 127 && code <= 159;
1460
+ out += control ? "?" : char;
1461
+ }
1462
+ return out;
1463
+ }
1464
+ function warningLine(warning) {
1465
+ if (warning === "out-of-order")
1466
+ return " \u26A0 older than an applied migration \u2014 db push will still apply it";
1467
+ if (warning === "ledger-name-missing-extension")
1468
+ return " \u26A0 recorded without .sql in the ledger";
1469
+ return null;
1470
+ }
1471
+ async function runMigrationNew(context, deps = processStdin2) {
1472
+ const [name] = context.positionalOrThrow(
1473
+ 1,
1474
+ "Usage: naro migration new <name>"
1475
+ );
1476
+ const dir = migrationsDir(context);
1477
+ const fileName = nextMigrationFileName(
1478
+ listMigrationFiles(dir),
1479
+ name
1480
+ );
1481
+ mkdirSync(dir, { recursive: true });
1482
+ const piped = !deps.stdinIsTty() ? await deps.readStdin() : "";
1483
+ const fromStdin = piped.length > 0;
1484
+ const body = fromStdin ? piped.endsWith("\n") ? piped : `${piped}
1485
+ ` : `-- Migration: ${fileName}
1486
+ `;
1487
+ const path = join2(dir, fileName);
1488
+ writeFileSync2(path, body, { flag: "wx" });
1489
+ const relPath = relativeToCwd(context, path);
1490
+ context.output.result({ file: fileName, path: relPath, fromStdin }, [
1491
+ `\u2705 Created ${relPath}`
1492
+ ]);
1493
+ }
1494
+ async function runMigrationList(context) {
1495
+ const client = await context.client();
1496
+ const projectId = await context.resolveProjectId();
1497
+ const dir = migrationsDir(context);
1498
+ const local = listMigrationFiles(dir);
1499
+ const ledger = await appliedMigrations(client, projectId);
1500
+ const migrations = compareMigrations(local, ledger);
1501
+ const relDir = relativeToCwd(context, dir);
1502
+ const lines = migrations.length === 0 ? ["No migrations yet. Create one with `naro migration new <name>`."] : [
1503
+ `Migrations in ${relDir} (${String(migrations.length)}):`,
1504
+ ...migrations.flatMap((row) => {
1505
+ const line = ` ${row.status.padEnd(12)}${printable(row.name)}${row.appliedAt ? ` ${printable(row.appliedAt)}` : ""}`;
1506
+ const warning = warningLine(row.warning);
1507
+ return warning ? [line, warning] : [line];
1508
+ })
1509
+ ];
1510
+ context.output.result({ projectId, dir: relDir, migrations }, lines);
1511
+ }
1512
+ async function runMigrationUp(context) {
1513
+ const client = await context.client();
1514
+ const projectId = await context.resolveProjectId();
1515
+ const dir = migrationsDir(context);
1516
+ const local = listMigrationFiles(dir);
1517
+ const ledger = await appliedMigrations(client, projectId);
1518
+ const pending = compareMigrations(local, ledger).filter((row) => row.status === "pending").map((row) => row.name);
1519
+ if (pending.length === 0) {
1520
+ context.output.result({ projectId, applied: [], pending: [] }, [
1521
+ "No pending migrations."
1522
+ ]);
1523
+ return;
1524
+ }
1525
+ if (context.flags.dryRun) {
1526
+ context.output.result({ projectId, dryRun: true, pending }, [
1527
+ `Pending migrations (${String(pending.length)}):`,
1528
+ ...pending.map((name) => ` ${printable(name)}`)
1529
+ ]);
1530
+ return;
1531
+ }
1532
+ context.output.log(`Pending migrations (${String(pending.length)}):`);
1533
+ for (const name of pending) context.output.log(` ${printable(name)}`);
1534
+ await confirmOrAbort(
1535
+ context,
1536
+ `Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
1537
+ );
1538
+ const applied = [];
1539
+ for (const name of pending) {
1540
+ const sql = readFileSync2(join2(dir, name), "utf8");
1541
+ try {
1542
+ await client.db.applyMigration(projectId, { name, sql });
1543
+ } catch (error) {
1544
+ if (!isNaroError(error)) throw error;
1545
+ const shown = context.output.json ? name : printable(name);
1546
+ throw new NaroError(error.code, `${shown}: ${error.message}`, {
1547
+ ...error.status === void 0 ? {} : { status: error.status },
1548
+ details: { ...error.details, migration: name, applied },
1549
+ cause: error
1550
+ });
1551
+ }
1552
+ context.output.log(`\u2713 ${printable(name)}`);
1553
+ applied.push(name);
1554
+ }
1555
+ context.output.result({ projectId, applied, pending: [] }, [
1556
+ `\u2705 Applied ${String(applied.length)} migration(s).`
1557
+ ]);
1558
+ }
1559
+
1560
+ // src/commands/gen.ts
1561
+ async function runGenTypes(context) {
1562
+ const client = await context.client();
1563
+ const projectId = await context.resolveProjectId();
1564
+ const tables = await client.db.tables(projectId);
1565
+ const types = generateTypes(tables);
1566
+ const undescribed = tables.flatMap(
1567
+ (table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
1568
+ );
1569
+ for (const { name, error } of undescribed)
1570
+ context.output.warn(
1571
+ `\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
1572
+ );
1573
+ const extra = undescribed.length > 0 ? { undescribed } : {};
1574
+ if (context.flags.output) {
1575
+ const path = resolve2(context.cwd, context.flags.output);
1576
+ mkdirSync2(dirname(path), { recursive: true });
1577
+ writeFileSync3(path, types);
1578
+ context.output.result(
1579
+ {
1580
+ projectId,
1581
+ output: context.flags.output,
1582
+ tables: tables.length,
1583
+ ...extra
1584
+ },
1585
+ [
1586
+ `\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
1587
+ ]
1588
+ );
1589
+ return;
1590
+ }
1591
+ context.output.result({ projectId, types, ...extra }, [
1592
+ types.replace(/\n$/, "")
1593
+ ]);
1594
+ }
1595
+
1103
1596
  // src/commands/logs.ts
1104
1597
  var FOLLOW_INTERVAL_MS = 3e3;
1105
1598
  function sleep(ms) {
1106
- return new Promise((resolve) => setTimeout(resolve, ms));
1599
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1107
1600
  }
1108
1601
  function formatBuildLog(log) {
1109
1602
  return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
@@ -1367,6 +1860,7 @@ Usage:
1367
1860
  [--root <dir>] [--name <name>] --root points at the app in a monorepo
1368
1861
  naro deployments List deployments of the linked project
1369
1862
  naro inspect <deployment-id> Show one deployment + build logs
1863
+ naro cancel <deployment-id> Stop a build that is still running
1370
1864
 
1371
1865
  naro git connect [owner/repo] Deploy on every push (defaults to your origin remote)
1372
1866
  naro git disconnect Stop deploying on push
@@ -1379,7 +1873,21 @@ Usage:
1379
1873
  naro env remove KEY [environment] Remove
1380
1874
  naro env pull [file] Write env vars to .env.local
1381
1875
 
1382
- naro domains list | add <host> | remove <host>
1876
+ naro domains list | add <host> [--no-www] | remove <host>
1877
+ add example.com also connects www.example.com and redirects the apex to
1878
+ it, because most registrars cannot put a CNAME on an apex; --no-www
1879
+ registers the apex alone
1880
+ naro domains verify [host] [--wait] Re-check DNS and print what is missing
1881
+ exits non-zero while a domain is not live yet
1882
+
1883
+ naro db enable Create the project's database (Cloudflare D1)
1884
+ naro db info Database status, size, table count and limits
1885
+ naro db query <sql> | --file <f> Run SQL against it (reads stdin when piped)
1886
+ naro db push [--dry-run] Apply pending migrations from ./migrations (asks first; --yes to skip)
1887
+ naro migration new <name> Create migrations/NNNN_<name>.sql (stdin becomes the body when piped)
1888
+ naro migration list Local vs applied migrations
1889
+ naro migration up [--dry-run] Same as db push
1890
+ naro gen types [--output <file>] TypeScript types for the schema (stdout by default; TypeScript only)
1383
1891
 
1384
1892
  naro promote <deployment-id> Publish a build to production
1385
1893
  naro rollback [deployment-id] Roll production back to the previous build
@@ -1394,6 +1902,12 @@ Global options:
1394
1902
  --team <slug> Team scope for project listing/linking
1395
1903
  --cwd <dir> Run as if started in <dir>
1396
1904
  --prod Deploy/target production
1905
+ --wait Poll until the domain goes live (naro domains verify)
1906
+ --no-www Register the apex alone (naro domains add)
1907
+ --file <path> SQL file to run (naro db query)
1908
+ --dry-run List pending migrations without applying (naro db push)
1909
+ --output <file> Write to a file instead of stdout (naro gen types)
1910
+ --dir <path> Migrations directory (default ./migrations, or <--root>/migrations)
1397
1911
  --debug Verbose diagnostics
1398
1912
 
1399
1913
  Credentials: \`naro login\` opens a browser (GitHub). CI: NARO_TOKEN=$(naro token).
@@ -1432,6 +1946,8 @@ function route(parsed) {
1432
1946
  return { handler: runDeployments, positionals };
1433
1947
  case "inspect":
1434
1948
  return { handler: runInspect, positionals };
1949
+ case "cancel":
1950
+ return { handler: runCancel, positionals };
1435
1951
  case "logs":
1436
1952
  return { handler: runLogs, positionals };
1437
1953
  case "env":
@@ -1464,9 +1980,50 @@ function route(parsed) {
1464
1980
  handler: runDomainsRemove,
1465
1981
  positionals: positionals.slice(1)
1466
1982
  };
1983
+ case "verify":
1984
+ case "check":
1985
+ return {
1986
+ handler: runDomainsVerify,
1987
+ positionals: positionals.slice(1)
1988
+ };
1989
+ default:
1990
+ return null;
1991
+ }
1992
+ case "db":
1993
+ switch (sub) {
1994
+ case void 0:
1995
+ case "info":
1996
+ return { handler: runDbInfo, positionals: positionals.slice(1) };
1997
+ case "enable":
1998
+ return { handler: runDbEnable, positionals: positionals.slice(1) };
1999
+ case "query":
2000
+ return { handler: runDbQuery, positionals: positionals.slice(1) };
2001
+ case "push":
2002
+ return { handler: runMigrationUp, positionals: positionals.slice(1) };
2003
+ default:
2004
+ return null;
2005
+ }
2006
+ case "migration":
2007
+ switch (sub) {
2008
+ case void 0:
2009
+ case "list":
2010
+ case "ls":
2011
+ return {
2012
+ handler: runMigrationList,
2013
+ positionals: positionals.slice(1)
2014
+ };
2015
+ case "new":
2016
+ return {
2017
+ handler: runMigrationNew,
2018
+ positionals: positionals.slice(1)
2019
+ };
2020
+ case "up":
2021
+ return { handler: runMigrationUp, positionals: positionals.slice(1) };
1467
2022
  default:
1468
2023
  return null;
1469
2024
  }
2025
+ case "gen":
2026
+ return sub === "types" ? { handler: runGenTypes, positionals: positionals.slice(1) } : null;
1470
2027
  case "promote":
1471
2028
  return { handler: runPromote, positionals };
1472
2029
  case "rollback":
@@ -1487,8 +2044,8 @@ function route(parsed) {
1487
2044
  function version() {
1488
2045
  try {
1489
2046
  const pkg = JSON.parse(
1490
- readFileSync(
1491
- join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
2047
+ readFileSync3(
2048
+ join3(dirname2(fileURLToPath(import.meta.url)), "..", "package.json"),
1492
2049
  "utf8"
1493
2050
  )
1494
2051
  );