naro-cli 0.6.1 → 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/README.md +38 -4
- package/dist/artifact-types.d.ts +15 -0
- package/dist/{chunk-WKJELBOU.js → chunk-E2M6CQL7.js} +2931 -628
- package/dist/deploy.d.ts +4 -2
- package/dist/deploy.js +1 -1
- package/dist/index.js +598 -41
- package/dist/output.d.ts +5 -0
- package/package.json +5 -1
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-
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
|
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((
|
|
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/
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
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.
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
1491
|
-
|
|
2047
|
+
readFileSync3(
|
|
2048
|
+
join3(dirname2(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
1492
2049
|
"utf8"
|
|
1493
2050
|
)
|
|
1494
2051
|
);
|