naro-cli 0.6.1 → 0.8.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 +52 -4
- package/dist/artifact-types.d.ts +15 -0
- package/dist/{chunk-WKJELBOU.js → chunk-ONALKL7O.js} +3030 -603
- package/dist/deploy.d.ts +4 -2
- package/dist/deploy.js +1 -1
- package/dist/index.js +964 -48
- package/dist/output.d.ts +5 -0
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
BOOKMARK_FORMAT_MESSAGE,
|
|
3
4
|
NaroClient,
|
|
4
5
|
NaroError,
|
|
6
|
+
RESTORE_OUTCOME_UNKNOWN_CODES,
|
|
5
7
|
clearSession,
|
|
8
|
+
compareMigrations,
|
|
6
9
|
controlPlaneUrl,
|
|
7
10
|
createAuthClient,
|
|
8
11
|
deployDirectory,
|
|
9
12
|
detectGitInfo,
|
|
10
13
|
ensureGitignoreHasNaro,
|
|
14
|
+
generateTypes,
|
|
15
|
+
isBookmark,
|
|
11
16
|
isExpired,
|
|
12
17
|
isNaroError,
|
|
13
18
|
isTerminalStatus,
|
|
19
|
+
listMigrationFiles,
|
|
14
20
|
loadStoredSession,
|
|
15
21
|
login,
|
|
22
|
+
nextMigrationFileName,
|
|
16
23
|
openBrowser,
|
|
17
24
|
parseGitHubRemote,
|
|
18
25
|
projectPath,
|
|
@@ -20,13 +27,16 @@ import {
|
|
|
20
27
|
readProjectLink,
|
|
21
28
|
refreshStoredSession,
|
|
22
29
|
removeProjectLink,
|
|
30
|
+
renderRows,
|
|
23
31
|
resolveAuth,
|
|
32
|
+
resolveMigrationsDir,
|
|
24
33
|
saveSession,
|
|
25
34
|
serializeDotenv,
|
|
26
35
|
slugify,
|
|
36
|
+
summarizeStatement,
|
|
27
37
|
toNaroError,
|
|
28
38
|
writeProjectLink
|
|
29
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-ONALKL7O.js";
|
|
30
40
|
|
|
31
41
|
// src/args.ts
|
|
32
42
|
var BOOLEAN_FLAGS = {
|
|
@@ -42,7 +52,12 @@ var BOOLEAN_FLAGS = {
|
|
|
42
52
|
"--production": "prod",
|
|
43
53
|
"--follow": "follow",
|
|
44
54
|
"-f": "follow",
|
|
45
|
-
"--reveal": "reveal"
|
|
55
|
+
"--reveal": "reveal",
|
|
56
|
+
"--wait": "wait",
|
|
57
|
+
"--no-www": "noWww",
|
|
58
|
+
"--dry-run": "dryRun",
|
|
59
|
+
"--schema-only": "schemaOnly",
|
|
60
|
+
"--data-only": "dataOnly"
|
|
46
61
|
};
|
|
47
62
|
var STRING_FLAGS = {
|
|
48
63
|
"--token": "token",
|
|
@@ -54,8 +69,18 @@ var STRING_FLAGS = {
|
|
|
54
69
|
"--branch": "branch",
|
|
55
70
|
"--type": "type",
|
|
56
71
|
"--env": "env",
|
|
57
|
-
"--level": "level"
|
|
72
|
+
"--level": "level",
|
|
73
|
+
"--file": "file",
|
|
74
|
+
"--output": "output",
|
|
75
|
+
"--dir": "dir",
|
|
76
|
+
"--table": "table",
|
|
77
|
+
"--timestamp": "timestamp",
|
|
78
|
+
"--bookmark": "bookmark"
|
|
58
79
|
};
|
|
80
|
+
function listFlag(value) {
|
|
81
|
+
if (value === void 0) return [];
|
|
82
|
+
return value.split(",").map((piece) => piece.trim()).filter((piece) => piece.length > 0);
|
|
83
|
+
}
|
|
59
84
|
function parseArgs(argv) {
|
|
60
85
|
const flags = {
|
|
61
86
|
json: false,
|
|
@@ -65,7 +90,9 @@ function parseArgs(argv) {
|
|
|
65
90
|
version: false,
|
|
66
91
|
prod: false,
|
|
67
92
|
follow: false,
|
|
68
|
-
reveal: false
|
|
93
|
+
reveal: false,
|
|
94
|
+
wait: false,
|
|
95
|
+
noWww: false
|
|
69
96
|
};
|
|
70
97
|
const positionals = [];
|
|
71
98
|
const unknown = [];
|
|
@@ -122,6 +149,14 @@ var Output = class {
|
|
|
122
149
|
if (this.json) this.streams.stderr(`${message}
|
|
123
150
|
`);
|
|
124
151
|
else this.streams.stdout(`${message}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A warning that must never land in stdout, in either mode - for commands
|
|
156
|
+
* whose stdout is the deliverable itself (`naro gen types > db.d.ts`).
|
|
157
|
+
*/
|
|
158
|
+
warn(message) {
|
|
159
|
+
this.streams.stderr(`${message}
|
|
125
160
|
`);
|
|
126
161
|
}
|
|
127
162
|
/** Extra diagnostics, only with --debug. */
|
|
@@ -197,7 +232,7 @@ var TtyPrompter = class {
|
|
|
197
232
|
});
|
|
198
233
|
try {
|
|
199
234
|
return await new Promise(
|
|
200
|
-
(
|
|
235
|
+
(resolve4) => rl.question(question, resolve4)
|
|
201
236
|
);
|
|
202
237
|
} finally {
|
|
203
238
|
rl.close();
|
|
@@ -218,7 +253,7 @@ var TtyPrompter = class {
|
|
|
218
253
|
});
|
|
219
254
|
try {
|
|
220
255
|
const answer = new Promise(
|
|
221
|
-
(
|
|
256
|
+
(resolve4) => rl.question(question, resolve4)
|
|
222
257
|
);
|
|
223
258
|
muted = true;
|
|
224
259
|
const value = await answer;
|
|
@@ -329,8 +364,8 @@ function createContext(flags, positionals = []) {
|
|
|
329
364
|
}
|
|
330
365
|
|
|
331
366
|
// src/router.ts
|
|
332
|
-
import { readFileSync } from "fs";
|
|
333
|
-
import { dirname, join as
|
|
367
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
368
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
334
369
|
import { fileURLToPath } from "url";
|
|
335
370
|
|
|
336
371
|
// src/commands/auth.ts
|
|
@@ -414,11 +449,603 @@ async function runToken(context) {
|
|
|
414
449
|
);
|
|
415
450
|
}
|
|
416
451
|
|
|
417
|
-
// src/commands/
|
|
452
|
+
// src/commands/confirm.ts
|
|
453
|
+
async function confirmOrAbort(context, question) {
|
|
454
|
+
if (context.flags.yes) return;
|
|
455
|
+
if (context.flags.json || !isInteractive()) {
|
|
456
|
+
throw new NaroError(
|
|
457
|
+
"CONFIRMATION_REQUIRED",
|
|
458
|
+
`${question} Re-run with --yes to confirm.`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const confirmed = await context.prompter.confirm(question);
|
|
462
|
+
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/commands/cancel.ts
|
|
466
|
+
async function runCancel(context) {
|
|
467
|
+
const [deploymentId] = context.positionalOrThrow(
|
|
468
|
+
1,
|
|
469
|
+
"Usage: naro cancel <deployment-id>"
|
|
470
|
+
);
|
|
471
|
+
const client = await context.client();
|
|
472
|
+
const projectId = await context.resolveProjectId();
|
|
473
|
+
await confirmOrAbort(context, `Cancel the build ${String(deploymentId)}?`);
|
|
474
|
+
const canceled = await client.deployments.cancel(
|
|
475
|
+
projectId,
|
|
476
|
+
deploymentId
|
|
477
|
+
);
|
|
478
|
+
context.output.result(
|
|
479
|
+
{
|
|
480
|
+
deploymentId: canceled.id,
|
|
481
|
+
projectId,
|
|
482
|
+
status: canceled.status,
|
|
483
|
+
errorCode: canceled.errorCode
|
|
484
|
+
},
|
|
485
|
+
[`\u2705 Canceled ${canceled.id}`]
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/commands/db.ts
|
|
490
|
+
import { readFileSync } from "fs";
|
|
491
|
+
import { resolve } from "path";
|
|
492
|
+
var processStdin = {
|
|
493
|
+
stdinIsTty: () => process.stdin.isTTY === true,
|
|
494
|
+
readStdin
|
|
495
|
+
};
|
|
496
|
+
var USAGE = "Usage: naro db query <sql> | --file <path> | (stdin)";
|
|
497
|
+
var ROW_CAP_NOTICE = "\u26A0 Output was cut at 10,000 rows. Add a LIMIT to see the rest.";
|
|
498
|
+
function humanSize(bytes) {
|
|
499
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
500
|
+
let value = bytes;
|
|
501
|
+
let unit = 0;
|
|
502
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
503
|
+
value /= 1024;
|
|
504
|
+
unit += 1;
|
|
505
|
+
}
|
|
506
|
+
return `${unit === 0 ? String(value) : value.toFixed(1)} ${units[unit] ?? "B"}`;
|
|
507
|
+
}
|
|
508
|
+
function describeLines(info) {
|
|
509
|
+
const lines = [
|
|
510
|
+
`Provider: ${info.provider}`,
|
|
511
|
+
`Status: ${info.status}`,
|
|
512
|
+
`Binding: env.${info.binding}`
|
|
513
|
+
];
|
|
514
|
+
if (info.resourceName)
|
|
515
|
+
lines.push(
|
|
516
|
+
`Database: ${info.resourceName}${info.resourceId ? ` (${info.resourceId})` : ""}`
|
|
517
|
+
);
|
|
518
|
+
if (info.cloudflare) {
|
|
519
|
+
const size = info.cloudflare.fileSize === null ? "?" : humanSize(info.cloudflare.fileSize);
|
|
520
|
+
const tables = info.cloudflare.numTables === null ? "?" : String(info.cloudflare.numTables);
|
|
521
|
+
lines.push(`Size: ${size} \xB7 Tables: ${tables}`);
|
|
522
|
+
}
|
|
523
|
+
const { limits } = info;
|
|
524
|
+
lines.push(
|
|
525
|
+
`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`
|
|
526
|
+
);
|
|
527
|
+
if (info.provider === "none")
|
|
528
|
+
lines.push("Run `naro db enable` to create the database.");
|
|
529
|
+
return lines;
|
|
530
|
+
}
|
|
531
|
+
async function runDbInfo(context) {
|
|
532
|
+
const client = await context.client();
|
|
533
|
+
const projectId = await context.resolveProjectId();
|
|
534
|
+
const database = await client.db.info(projectId);
|
|
535
|
+
context.output.result({ projectId, database }, describeLines(database));
|
|
536
|
+
}
|
|
537
|
+
async function runDbEnable(context) {
|
|
538
|
+
const client = await context.client();
|
|
539
|
+
const projectId = await context.resolveProjectId();
|
|
540
|
+
const database = await client.db.enable(projectId);
|
|
541
|
+
context.output.result({ projectId, database }, [
|
|
542
|
+
`\u2705 Database enabled \u2014 ${database.resourceName ?? "(pending)"} (${database.status})`,
|
|
543
|
+
...describeLines(database)
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
async function readSql(context, deps) {
|
|
547
|
+
const positional = context.positionals[0];
|
|
548
|
+
let sql;
|
|
549
|
+
if (positional !== void 0) {
|
|
550
|
+
sql = positional;
|
|
551
|
+
} else if (context.flags.file !== void 0) {
|
|
552
|
+
try {
|
|
553
|
+
sql = readFileSync(resolve(context.cwd, context.flags.file), "utf8");
|
|
554
|
+
} catch {
|
|
555
|
+
throw new NaroError(
|
|
556
|
+
"VALIDATION_FAILED",
|
|
557
|
+
`Could not read ${context.flags.file}.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
} else if (!deps.stdinIsTty()) {
|
|
561
|
+
sql = await deps.readStdin();
|
|
562
|
+
}
|
|
563
|
+
if (sql === void 0 || sql.trim().length === 0)
|
|
564
|
+
throw new NaroError("VALIDATION_FAILED", USAGE);
|
|
565
|
+
return sql;
|
|
566
|
+
}
|
|
567
|
+
async function runDbQuery(context, deps = processStdin) {
|
|
568
|
+
const sql = await readSql(context, deps);
|
|
569
|
+
const client = await context.client();
|
|
570
|
+
const projectId = await context.resolveProjectId();
|
|
571
|
+
const result = await client.db.query(projectId, { sql });
|
|
572
|
+
const lines = [];
|
|
573
|
+
result.statements.forEach((statement, index) => {
|
|
574
|
+
if (index > 0) lines.push("");
|
|
575
|
+
lines.push(...renderRows(statement), summarizeStatement(statement));
|
|
576
|
+
});
|
|
577
|
+
if (result.truncated) lines.push(ROW_CAP_NOTICE);
|
|
578
|
+
context.output.result({ projectId, result }, lines);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/commands/db-backup.ts
|
|
582
|
+
import {
|
|
583
|
+
createWriteStream,
|
|
584
|
+
mkdirSync as mkdirSync2,
|
|
585
|
+
renameSync,
|
|
586
|
+
rmSync,
|
|
587
|
+
statSync,
|
|
588
|
+
writeFileSync as writeFileSync2
|
|
589
|
+
} from "fs";
|
|
590
|
+
import { basename, dirname, join as join2, resolve as resolve2 } from "path";
|
|
591
|
+
import { Transform } from "stream";
|
|
592
|
+
import { pipeline } from "stream/promises";
|
|
593
|
+
|
|
594
|
+
// src/commands/migration.ts
|
|
595
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
596
|
+
import { join, relative } from "path";
|
|
597
|
+
var processStdin2 = {
|
|
598
|
+
stdinIsTty: () => process.stdin.isTTY === true,
|
|
599
|
+
readStdin
|
|
600
|
+
};
|
|
601
|
+
function migrationsDir(context) {
|
|
602
|
+
return resolveMigrationsDir({
|
|
603
|
+
baseDir: context.cwd,
|
|
604
|
+
rootDirectory: context.flags.root,
|
|
605
|
+
dir: context.flags.dir
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function relativeToCwd(context, path) {
|
|
609
|
+
const rel = relative(context.cwd, path);
|
|
610
|
+
return rel === "" ? "." : rel;
|
|
611
|
+
}
|
|
612
|
+
async function appliedMigrations(client, projectId) {
|
|
613
|
+
try {
|
|
614
|
+
return await client.db.migrations(projectId);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (isNaroError(error) && error.code === "DATABASE_NOT_READY") return [];
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function printable(text) {
|
|
621
|
+
let out = "";
|
|
622
|
+
for (const char of text) {
|
|
623
|
+
const code = char.charCodeAt(0);
|
|
624
|
+
const control = code < 32 || code >= 127 && code <= 159;
|
|
625
|
+
out += control ? "?" : char;
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
function warningLine(warning) {
|
|
630
|
+
if (warning === "out-of-order")
|
|
631
|
+
return " \u26A0 older than an applied migration \u2014 db push will still apply it";
|
|
632
|
+
if (warning === "ledger-name-missing-extension")
|
|
633
|
+
return " \u26A0 recorded without .sql in the ledger";
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
async function runMigrationNew(context, deps = processStdin2) {
|
|
637
|
+
const [name] = context.positionalOrThrow(
|
|
638
|
+
1,
|
|
639
|
+
"Usage: naro migration new <name>"
|
|
640
|
+
);
|
|
641
|
+
const dir = migrationsDir(context);
|
|
642
|
+
const fileName = nextMigrationFileName(
|
|
643
|
+
listMigrationFiles(dir),
|
|
644
|
+
name
|
|
645
|
+
);
|
|
646
|
+
mkdirSync(dir, { recursive: true });
|
|
647
|
+
const piped = !deps.stdinIsTty() ? await deps.readStdin() : "";
|
|
648
|
+
const fromStdin = piped.length > 0;
|
|
649
|
+
const body = fromStdin ? piped.endsWith("\n") ? piped : `${piped}
|
|
650
|
+
` : `-- Migration: ${fileName}
|
|
651
|
+
`;
|
|
652
|
+
const path = join(dir, fileName);
|
|
653
|
+
writeFileSync(path, body, { flag: "wx" });
|
|
654
|
+
const relPath = relativeToCwd(context, path);
|
|
655
|
+
context.output.result({ file: fileName, path: relPath, fromStdin }, [
|
|
656
|
+
`\u2705 Created ${relPath}`
|
|
657
|
+
]);
|
|
658
|
+
}
|
|
659
|
+
async function runMigrationList(context) {
|
|
660
|
+
const client = await context.client();
|
|
661
|
+
const projectId = await context.resolveProjectId();
|
|
662
|
+
const dir = migrationsDir(context);
|
|
663
|
+
const local = listMigrationFiles(dir);
|
|
664
|
+
const ledger = await appliedMigrations(client, projectId);
|
|
665
|
+
const migrations = compareMigrations(local, ledger);
|
|
666
|
+
const relDir = relativeToCwd(context, dir);
|
|
667
|
+
const lines = migrations.length === 0 ? ["No migrations yet. Create one with `naro migration new <name>`."] : [
|
|
668
|
+
`Migrations in ${relDir} (${String(migrations.length)}):`,
|
|
669
|
+
...migrations.flatMap((row) => {
|
|
670
|
+
const line = ` ${row.status.padEnd(12)}${printable(row.name)}${row.appliedAt ? ` ${printable(row.appliedAt)}` : ""}`;
|
|
671
|
+
const warning = warningLine(row.warning);
|
|
672
|
+
return warning ? [line, warning] : [line];
|
|
673
|
+
})
|
|
674
|
+
];
|
|
675
|
+
context.output.result({ projectId, dir: relDir, migrations }, lines);
|
|
676
|
+
}
|
|
677
|
+
async function runMigrationUp(context) {
|
|
678
|
+
const client = await context.client();
|
|
679
|
+
const projectId = await context.resolveProjectId();
|
|
680
|
+
const dir = migrationsDir(context);
|
|
681
|
+
const local = listMigrationFiles(dir);
|
|
682
|
+
const ledger = await appliedMigrations(client, projectId);
|
|
683
|
+
const pending = compareMigrations(local, ledger).filter((row) => row.status === "pending").map((row) => row.name);
|
|
684
|
+
if (pending.length === 0) {
|
|
685
|
+
context.output.result({ projectId, applied: [], pending: [] }, [
|
|
686
|
+
"No pending migrations."
|
|
687
|
+
]);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (context.flags.dryRun) {
|
|
691
|
+
context.output.result({ projectId, dryRun: true, pending }, [
|
|
692
|
+
`Pending migrations (${String(pending.length)}):`,
|
|
693
|
+
...pending.map((name) => ` ${printable(name)}`)
|
|
694
|
+
]);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
context.output.log(`Pending migrations (${String(pending.length)}):`);
|
|
698
|
+
for (const name of pending) context.output.log(` ${printable(name)}`);
|
|
699
|
+
await confirmOrAbort(
|
|
700
|
+
context,
|
|
701
|
+
`Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
|
|
702
|
+
);
|
|
703
|
+
const applied = [];
|
|
704
|
+
for (const name of pending) {
|
|
705
|
+
const sql = readFileSync2(join(dir, name), "utf8");
|
|
706
|
+
try {
|
|
707
|
+
await client.db.applyMigration(projectId, { name, sql });
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (!isNaroError(error)) throw error;
|
|
710
|
+
const shown = context.output.json ? name : printable(name);
|
|
711
|
+
throw new NaroError(error.code, `${shown}: ${error.message}`, {
|
|
712
|
+
...error.status === void 0 ? {} : { status: error.status },
|
|
713
|
+
details: { ...error.details, migration: name, applied },
|
|
714
|
+
cause: error
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
context.output.log(`\u2713 ${printable(name)}`);
|
|
718
|
+
applied.push(name);
|
|
719
|
+
}
|
|
720
|
+
context.output.result({ projectId, applied, pending: [] }, [
|
|
721
|
+
`\u2705 Applied ${String(applied.length)} migration(s).`
|
|
722
|
+
]);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/commands/db-backup.ts
|
|
418
726
|
var defaults = {
|
|
727
|
+
fetch: (input, init) => fetch(input, init),
|
|
728
|
+
stdout: process.stdout,
|
|
729
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
730
|
+
writeFile: (path, data) => {
|
|
731
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
732
|
+
writeFileSync2(path, data, { flag: "wx" });
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
var EXPORT_NOTICE = "Exporting the project database. Cloudflare holds other queries to this database until the export finishes.";
|
|
736
|
+
function reasonOf(error) {
|
|
737
|
+
return error instanceof Error ? error.message : String(error);
|
|
738
|
+
}
|
|
739
|
+
function withoutUrl(text, url) {
|
|
740
|
+
return text.split(url).join("the signed URL").replace(/\S*X-Amz-Signature=\S*/gi, "the signed URL");
|
|
741
|
+
}
|
|
742
|
+
function downloadFailed(url, reason, original) {
|
|
743
|
+
const scrubbed = withoutUrl(reason, url);
|
|
744
|
+
let cause;
|
|
745
|
+
if (original !== void 0) {
|
|
746
|
+
cause = new Error(scrubbed);
|
|
747
|
+
if (original instanceof Error) {
|
|
748
|
+
cause.name = original.name;
|
|
749
|
+
if (original.stack !== void 0)
|
|
750
|
+
cause.stack = withoutUrl(original.stack, url);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return new NaroError(
|
|
754
|
+
"DATABASE_UNAVAILABLE",
|
|
755
|
+
`Could not download the dump (${scrubbed}). Run naro db dump again.`,
|
|
756
|
+
cause === void 0 ? {} : { cause }
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
function writeFailed(shown, error) {
|
|
760
|
+
return new NaroError(
|
|
761
|
+
"INTERNAL_ERROR",
|
|
762
|
+
`Could not write ${shown}: ${reasonOf(error)}.`,
|
|
763
|
+
{ cause: error }
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
var DownloadStreamError = class extends Error {
|
|
767
|
+
constructor(original) {
|
|
768
|
+
super(reasonOf(original));
|
|
769
|
+
this.original = original;
|
|
770
|
+
this.name = "DownloadStreamError";
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
async function openDownload(deps, url) {
|
|
774
|
+
let response;
|
|
775
|
+
try {
|
|
776
|
+
response = await deps.fetch(url);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
throw downloadFailed(url, reasonOf(error), error);
|
|
779
|
+
}
|
|
780
|
+
if (!response.ok || response.body === null) {
|
|
781
|
+
await response.body?.cancel().catch(() => {
|
|
782
|
+
});
|
|
783
|
+
throw downloadFailed(url, `HTTP ${String(response.status)}`);
|
|
784
|
+
}
|
|
785
|
+
return response.body;
|
|
786
|
+
}
|
|
787
|
+
async function* chunksOf(body) {
|
|
788
|
+
const reader = body.getReader();
|
|
789
|
+
const read = async () => {
|
|
790
|
+
try {
|
|
791
|
+
return await reader.read();
|
|
792
|
+
} catch (error) {
|
|
793
|
+
throw new DownloadStreamError(error);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
try {
|
|
797
|
+
for (; ; ) {
|
|
798
|
+
const next = await read();
|
|
799
|
+
if (next.done) return;
|
|
800
|
+
yield next.value;
|
|
801
|
+
}
|
|
802
|
+
} finally {
|
|
803
|
+
await reader.cancel().catch(() => {
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function streamInto(body, destination, end) {
|
|
808
|
+
let bytes = 0;
|
|
809
|
+
const counter = new Transform({
|
|
810
|
+
transform(chunk, _encoding, done) {
|
|
811
|
+
bytes += chunk.length;
|
|
812
|
+
done(null, chunk);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
await pipeline(chunksOf(body), counter, destination, { end });
|
|
816
|
+
return bytes;
|
|
817
|
+
}
|
|
818
|
+
function streamFailed(error, url, shown) {
|
|
819
|
+
return error instanceof DownloadStreamError ? downloadFailed(url, reasonOf(error.original), error.original) : writeFailed(shown, error);
|
|
820
|
+
}
|
|
821
|
+
function refuseDirectory(path, shown) {
|
|
822
|
+
let isDirectory = false;
|
|
823
|
+
try {
|
|
824
|
+
isDirectory = statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
if (isDirectory)
|
|
828
|
+
throw new NaroError(
|
|
829
|
+
"VALIDATION_FAILED",
|
|
830
|
+
`--output ${shown} is a directory. Pass a file path.`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
function makeOutputDirectory(path, shown) {
|
|
834
|
+
try {
|
|
835
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
836
|
+
} catch (error) {
|
|
837
|
+
throw writeFailed(shown, error);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async function downloadToFile(deps, url, path, shown) {
|
|
841
|
+
const body = await openDownload(deps, url);
|
|
842
|
+
const partial = join2(
|
|
843
|
+
dirname(path),
|
|
844
|
+
`.${basename(path)}.${String(process.pid)}.part`
|
|
845
|
+
);
|
|
846
|
+
try {
|
|
847
|
+
const bytes = await streamInto(body, createWriteStream(partial), true);
|
|
848
|
+
renameSync(partial, path);
|
|
849
|
+
return bytes;
|
|
850
|
+
} catch (error) {
|
|
851
|
+
try {
|
|
852
|
+
rmSync(partial, { force: true });
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
throw streamFailed(error, url, shown);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
async function downloadToStdout(deps, url) {
|
|
859
|
+
const body = await openDownload(deps, url);
|
|
860
|
+
try {
|
|
861
|
+
return await streamInto(body, deps.stdout, false);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
throw streamFailed(error, url, "stdout");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function runDbDump(context, overrides = {}) {
|
|
867
|
+
const deps = { ...defaults, ...overrides };
|
|
868
|
+
const { flags } = context;
|
|
869
|
+
if (flags.schemaOnly && flags.dataOnly)
|
|
870
|
+
throw new NaroError(
|
|
871
|
+
"VALIDATION_FAILED",
|
|
872
|
+
"Pass --schema-only or --data-only, not both."
|
|
873
|
+
);
|
|
874
|
+
if (flags.json && flags.output === void 0)
|
|
875
|
+
throw new NaroError(
|
|
876
|
+
"VALIDATION_FAILED",
|
|
877
|
+
"--json needs --output: the dump and the JSON result would share stdout."
|
|
878
|
+
);
|
|
879
|
+
const tables = listFlag(flags.table);
|
|
880
|
+
if (flags.table !== void 0 && tables.length === 0)
|
|
881
|
+
throw new NaroError(
|
|
882
|
+
"VALIDATION_FAILED",
|
|
883
|
+
"--table needs at least one table name."
|
|
884
|
+
);
|
|
885
|
+
const options = {
|
|
886
|
+
...flags.schemaOnly ? { schemaOnly: true } : {},
|
|
887
|
+
...flags.dataOnly ? { dataOnly: true } : {},
|
|
888
|
+
...tables.length > 0 ? { tables } : {}
|
|
889
|
+
};
|
|
890
|
+
const output = flags.output === void 0 ? null : { path: resolve2(context.cwd, flags.output), shown: flags.output };
|
|
891
|
+
if (output) refuseDirectory(output.path, output.shown);
|
|
892
|
+
const client = await context.client();
|
|
893
|
+
const projectId = await context.resolveProjectId();
|
|
894
|
+
if (output) makeOutputDirectory(output.path, output.shown);
|
|
895
|
+
context.output.warn(EXPORT_NOTICE);
|
|
896
|
+
const step = await client.db.export(projectId, options, {
|
|
897
|
+
sleep: deps.sleep
|
|
898
|
+
});
|
|
899
|
+
const file = step.file;
|
|
900
|
+
if (file === null)
|
|
901
|
+
throw new NaroError(
|
|
902
|
+
"DATABASE_UNAVAILABLE",
|
|
903
|
+
"Could not download the dump (Cloudflare finished the export without a file). Run naro db dump again."
|
|
904
|
+
);
|
|
905
|
+
const bytes = output === null ? await downloadToStdout(deps, file.url) : await downloadToFile(deps, file.url, output.path, output.shown);
|
|
906
|
+
const bookmark = step.atBookmark;
|
|
907
|
+
if (!context.output.json) {
|
|
908
|
+
context.output.warn(
|
|
909
|
+
`\u2713 Wrote ${flags.output ?? "stdout"} (${humanSize(bytes)}) at bookmark ${bookmark}.`
|
|
910
|
+
);
|
|
911
|
+
context.output.warn(
|
|
912
|
+
` naro db restore --bookmark ${bookmark} returns the database to this point.`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
context.output.result({ output: flags.output, bytes, atBookmark: bookmark });
|
|
916
|
+
}
|
|
917
|
+
async function bookmarkNow(context, client, projectId) {
|
|
918
|
+
try {
|
|
919
|
+
const { bookmark } = await client.db.bookmark(projectId);
|
|
920
|
+
return isBookmark(bookmark) ? bookmark : null;
|
|
921
|
+
} catch (error) {
|
|
922
|
+
context.output.debugLog(
|
|
923
|
+
`Could not read the current bookmark before restoring: ${reasonOf(error)}`
|
|
924
|
+
);
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function withWayBack(context, error, before) {
|
|
929
|
+
if (before === null || !isNaroError(error) || !RESTORE_OUTCOME_UNKNOWN_CODES.has(error.code))
|
|
930
|
+
return error;
|
|
931
|
+
if (!context.output.json)
|
|
932
|
+
context.output.warn(
|
|
933
|
+
`Before this attempt the database was at bookmark ${before}. If the restore ran anyway, naro db restore --bookmark ${before} undoes it.`
|
|
934
|
+
);
|
|
935
|
+
return new NaroError(error.code, error.message, {
|
|
936
|
+
...error.status === void 0 ? {} : { status: error.status },
|
|
937
|
+
details: { ...error.details, previousBookmark: before },
|
|
938
|
+
cause: error
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
async function runDbRestore(context) {
|
|
942
|
+
const { bookmark, timestamp } = context.flags;
|
|
943
|
+
const given = bookmark !== void 0 && timestamp === void 0 ? { bookmark } : timestamp !== void 0 && bookmark === void 0 ? { timestamp } : null;
|
|
944
|
+
if (given === null)
|
|
945
|
+
throw new NaroError(
|
|
946
|
+
"VALIDATION_FAILED",
|
|
947
|
+
"Pass --timestamp <time> or --bookmark <id> (exactly one)."
|
|
948
|
+
);
|
|
949
|
+
if ("bookmark" in given && !isBookmark(given.bookmark))
|
|
950
|
+
throw new NaroError("VALIDATION_FAILED", BOOKMARK_FORMAT_MESSAGE);
|
|
951
|
+
const client = await context.client();
|
|
952
|
+
const projectId = await context.resolveProjectId();
|
|
953
|
+
let target;
|
|
954
|
+
let at = null;
|
|
955
|
+
if ("bookmark" in given) {
|
|
956
|
+
target = given.bookmark;
|
|
957
|
+
} else {
|
|
958
|
+
const resolved = await client.db.bookmark(projectId, {
|
|
959
|
+
timestamp: given.timestamp
|
|
960
|
+
});
|
|
961
|
+
target = resolved.bookmark;
|
|
962
|
+
at = resolved.timestamp ?? given.timestamp;
|
|
963
|
+
}
|
|
964
|
+
const label = at === null ? `bookmark ${target}` : `${at} (bookmark ${target})`;
|
|
965
|
+
if (context.flags.dryRun) {
|
|
966
|
+
context.output.result(
|
|
967
|
+
{
|
|
968
|
+
dryRun: true,
|
|
969
|
+
bookmark: target,
|
|
970
|
+
...at === null ? {} : { timestamp: at }
|
|
971
|
+
},
|
|
972
|
+
[`Would restore to ${label}. Nothing was changed.`]
|
|
973
|
+
);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
await confirmOrAbort(
|
|
977
|
+
context,
|
|
978
|
+
`Restore the project database to ${label}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
|
|
979
|
+
);
|
|
980
|
+
const before = await bookmarkNow(context, client, projectId);
|
|
981
|
+
let restored;
|
|
982
|
+
try {
|
|
983
|
+
restored = await client.db.restore(projectId, { bookmark: target });
|
|
984
|
+
} catch (error) {
|
|
985
|
+
throw withWayBack(context, error, before);
|
|
986
|
+
}
|
|
987
|
+
context.output.result(
|
|
988
|
+
{
|
|
989
|
+
bookmark: restored.bookmark,
|
|
990
|
+
previousBookmark: restored.previousBookmark
|
|
991
|
+
},
|
|
992
|
+
[
|
|
993
|
+
`\u2713 Restored the project database to bookmark ${restored.bookmark}.`,
|
|
994
|
+
` To undo: naro db restore --bookmark ${restored.previousBookmark}`
|
|
995
|
+
]
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
async function runDbPull(context, overrides = {}) {
|
|
999
|
+
const deps = { ...defaults, ...overrides };
|
|
1000
|
+
const dir = migrationsDir(context);
|
|
1001
|
+
const existing = listMigrationFiles(dir);
|
|
1002
|
+
if (existing.length > 0)
|
|
1003
|
+
throw new NaroError(
|
|
1004
|
+
"VALIDATION_FAILED",
|
|
1005
|
+
`${relativeToCwd(context, dir)} already has ${String(existing.length)} migration file(s). naro db pull only baselines a project without migrations; to see the current schema, run naro db dump --schema-only.`
|
|
1006
|
+
);
|
|
1007
|
+
const name = nextMigrationFileName(existing, "remote_schema");
|
|
1008
|
+
const path = join2(dir, name);
|
|
1009
|
+
const shown = relativeToCwd(context, path);
|
|
1010
|
+
const client = await context.client();
|
|
1011
|
+
const projectId = await context.resolveProjectId();
|
|
1012
|
+
await confirmOrAbort(
|
|
1013
|
+
context,
|
|
1014
|
+
`Write ${shown} with the database's current schema and record it as already applied?`
|
|
1015
|
+
);
|
|
1016
|
+
const pulled = await client.db.pull(projectId, { name });
|
|
1017
|
+
try {
|
|
1018
|
+
deps.writeFile(path, `${pulled.sql}
|
|
1019
|
+
`);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (!context.output.json) context.output.log(pulled.sql);
|
|
1022
|
+
throw new NaroError(
|
|
1023
|
+
"INTERNAL_ERROR",
|
|
1024
|
+
`Recorded ${pulled.name} as applied but could not write ${shown}: ${reasonOf(error)}. The SQL is on stdout \u2014 save it as that file.`,
|
|
1025
|
+
{
|
|
1026
|
+
details: { migration: pulled.name, path: shown, sql: pulled.sql },
|
|
1027
|
+
cause: error
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
context.output.result(
|
|
1032
|
+
{
|
|
1033
|
+
projectId,
|
|
1034
|
+
name: pulled.name,
|
|
1035
|
+
path: shown,
|
|
1036
|
+
statements: pulled.statements
|
|
1037
|
+
},
|
|
1038
|
+
[
|
|
1039
|
+
`\u2713 Wrote ${shown} (${String(pulled.statements)} ${pulled.statements === 1 ? "statement" : "statements"}) and recorded it as applied.`
|
|
1040
|
+
]
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/commands/git.ts
|
|
1045
|
+
var defaults2 = {
|
|
419
1046
|
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
420
1047
|
openBrowser,
|
|
421
|
-
sleep: (ms) => new Promise((
|
|
1048
|
+
sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms))
|
|
422
1049
|
};
|
|
423
1050
|
function connected(context, project) {
|
|
424
1051
|
const branch = project.defaultBranch;
|
|
@@ -437,7 +1064,7 @@ function connected(context, project) {
|
|
|
437
1064
|
);
|
|
438
1065
|
}
|
|
439
1066
|
async function runGitConnect(context, overrides = {}) {
|
|
440
|
-
const deps = { ...
|
|
1067
|
+
const deps = { ...defaults2, ...overrides };
|
|
441
1068
|
const { output } = context;
|
|
442
1069
|
const explicit = context.positionals[0];
|
|
443
1070
|
let fullName;
|
|
@@ -524,7 +1151,7 @@ async function runGitDisconnect(context) {
|
|
|
524
1151
|
]);
|
|
525
1152
|
}
|
|
526
1153
|
async function offerAutoDeploy(context, projectId, overrides) {
|
|
527
|
-
const deps = { ...
|
|
1154
|
+
const deps = { ...defaults2, ...overrides };
|
|
528
1155
|
if (overrides.canPrompt === false) return;
|
|
529
1156
|
try {
|
|
530
1157
|
if (readProjectLink(context.cwd)?.autoDeployDeclined === true) return;
|
|
@@ -904,32 +1531,89 @@ async function runInspect(context) {
|
|
|
904
1531
|
]);
|
|
905
1532
|
}
|
|
906
1533
|
|
|
907
|
-
// src/commands/
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1534
|
+
// src/commands/domains.ts
|
|
1535
|
+
var defaults3 = {
|
|
1536
|
+
sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
|
|
1537
|
+
now: () => Date.now()
|
|
1538
|
+
};
|
|
1539
|
+
var FIRST_DELAY_MS = 5e3;
|
|
1540
|
+
var MAX_DELAY_MS = 3e4;
|
|
1541
|
+
var WAIT_TIMEOUT_MS = 10 * 6e4;
|
|
1542
|
+
function isReady(domain) {
|
|
1543
|
+
return domain.ready ?? (domain.status === "active" && domain.sslStatus === "active");
|
|
1544
|
+
}
|
|
1545
|
+
function isTerminal(domain) {
|
|
1546
|
+
return domain.ownership === "deleted" || domain.status === "blocked" || domain.status === "moved" || domain.status === "deleted";
|
|
1547
|
+
}
|
|
1548
|
+
var AXIS_DONE = /* @__PURE__ */ new Set(["verified", "active", "live"]);
|
|
1549
|
+
var AXIS_FAILED = /* @__PURE__ */ new Set(["failed", "deleted"]);
|
|
1550
|
+
function axisMark(value) {
|
|
1551
|
+
if (AXIS_DONE.has(value)) return "\u2713";
|
|
1552
|
+
if (AXIS_FAILED.has(value)) return "\u2717";
|
|
1553
|
+
return "\xB7";
|
|
1554
|
+
}
|
|
1555
|
+
function axisLines(domain) {
|
|
1556
|
+
const axes = [
|
|
1557
|
+
["ownership", domain.ownership],
|
|
1558
|
+
["certificate", domain.certificate],
|
|
1559
|
+
["routing", domain.routing]
|
|
1560
|
+
];
|
|
1561
|
+
return axes.filter((axis) => axis[1] !== void 0).map(
|
|
1562
|
+
([name, value]) => ` ${`${name}:`.padEnd(13)}${axisMark(value)} ${value}`
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
function axisSummary(domain) {
|
|
1566
|
+
const axes = [
|
|
1567
|
+
domain.ownership,
|
|
1568
|
+
domain.certificate,
|
|
1569
|
+
domain.routing
|
|
1570
|
+
].filter((value) => value !== void 0);
|
|
1571
|
+
return axes.length > 0 ? axes.join(" \xB7 ") : `${domain.status} \xB7 ssl ${domain.sslStatus}`;
|
|
1572
|
+
}
|
|
1573
|
+
function recordLines(domain) {
|
|
1574
|
+
const instructions = domain.instructions ?? [];
|
|
1575
|
+
if (instructions.length > 0) {
|
|
1576
|
+
const lines2 = [" Required DNS records:"];
|
|
1577
|
+
for (const instruction of instructions) {
|
|
1578
|
+
lines2.push(
|
|
1579
|
+
` ${instruction.type.padEnd(5)} ${instruction.name}`,
|
|
1580
|
+
` host: ${instruction.host.padEnd(24)} value: ${instruction.value}`
|
|
1581
|
+
);
|
|
1582
|
+
if (instruction.note) lines2.push(` note: ${instruction.note}`);
|
|
1583
|
+
}
|
|
1584
|
+
return lines2;
|
|
1585
|
+
}
|
|
1586
|
+
if (domain.verification.length === 0) return [];
|
|
1587
|
+
const lines = [" Required DNS records:"];
|
|
1588
|
+
for (const record of domain.verification) {
|
|
1589
|
+
lines.push(
|
|
1590
|
+
` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
|
|
1591
|
+
` Value: ${record.value}`
|
|
914
1592
|
);
|
|
915
1593
|
}
|
|
916
|
-
|
|
917
|
-
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
1594
|
+
return lines;
|
|
918
1595
|
}
|
|
919
|
-
|
|
920
|
-
// src/commands/domains.ts
|
|
921
1596
|
function domainLines(domain) {
|
|
922
1597
|
const lines = [
|
|
923
|
-
` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}
|
|
1598
|
+
` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}`,
|
|
1599
|
+
...axisLines(domain)
|
|
924
1600
|
];
|
|
925
|
-
if (domain.
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1601
|
+
if (domain.redirectTo) {
|
|
1602
|
+
const status = domain.redirectStatus ?? 308;
|
|
1603
|
+
lines.push(` redirects to ${domain.redirectTo} (${String(status)})`);
|
|
1604
|
+
}
|
|
1605
|
+
if (domain.cause) {
|
|
1606
|
+
lines.push(` \u2717 ${domain.cause.message}`);
|
|
1607
|
+
if (domain.cause.fix) lines.push(` \u2192 ${domain.cause.fix}`);
|
|
1608
|
+
}
|
|
1609
|
+
if (isReady(domain)) return lines;
|
|
1610
|
+
lines.push(...recordLines(domain));
|
|
1611
|
+
const guidance = domain.guidance;
|
|
1612
|
+
if (guidance && guidance.notes.length > 0) {
|
|
1613
|
+
lines.push(
|
|
1614
|
+
guidance.apex ? ` Apex domain (${domain.hostname}):` : " Note:"
|
|
1615
|
+
);
|
|
1616
|
+
for (const note of guidance.notes) lines.push(` ${note}`);
|
|
933
1617
|
}
|
|
934
1618
|
return lines;
|
|
935
1619
|
}
|
|
@@ -937,28 +1621,141 @@ async function runDomainsList(context) {
|
|
|
937
1621
|
const client = await context.client();
|
|
938
1622
|
const projectId = await context.resolveProjectId();
|
|
939
1623
|
const domains = await client.domains.list(projectId);
|
|
1624
|
+
const pending = domains.filter((domain) => !isReady(domain));
|
|
940
1625
|
context.output.result(
|
|
941
1626
|
{ projectId, domains },
|
|
942
1627
|
domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
|
|
943
1628
|
`Domains (${String(domains.length)}):`,
|
|
944
|
-
...domains.flatMap(domainLines)
|
|
1629
|
+
...domains.flatMap(domainLines),
|
|
1630
|
+
// list reads our stored copy; only verify goes and looks again.
|
|
1631
|
+
...pending.length > 0 ? ["", "Re-check DNS now: naro domains verify"] : []
|
|
945
1632
|
]
|
|
946
1633
|
);
|
|
947
1634
|
}
|
|
948
1635
|
async function runDomainsAdd(context) {
|
|
949
1636
|
const [hostname] = context.positionalOrThrow(
|
|
950
1637
|
1,
|
|
951
|
-
"Usage: naro domains add <hostname>"
|
|
1638
|
+
"Usage: naro domains add <hostname> [--no-www]"
|
|
952
1639
|
);
|
|
953
1640
|
const client = await context.client();
|
|
954
1641
|
const projectId = await context.resolveProjectId();
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1642
|
+
const withWww = context.flags.noWww !== true;
|
|
1643
|
+
const domain = await client.domains.add(projectId, hostname, {
|
|
1644
|
+
withWww
|
|
1645
|
+
});
|
|
1646
|
+
const companion = domain.companion;
|
|
1647
|
+
const apex = domain.apex ?? domain.guidance?.apex ?? false;
|
|
1648
|
+
const alternative = domain.guidance?.alternative ?? `www.${domain.hostname}`;
|
|
1649
|
+
context.output.result(
|
|
1650
|
+
{ projectId, domain, withWww, ...companion ? { companion } : {} },
|
|
1651
|
+
[
|
|
1652
|
+
`\u2705 Domain added: ${domain.hostname}`,
|
|
1653
|
+
"",
|
|
1654
|
+
...domainLines(domain),
|
|
1655
|
+
...companion ? [
|
|
1656
|
+
"",
|
|
1657
|
+
` Also registered ${companion.hostname} \u2014 an apex domain often cannot hold`,
|
|
1658
|
+
" the CNAME above, and this form always can.",
|
|
1659
|
+
"",
|
|
1660
|
+
...domainLines(companion)
|
|
1661
|
+
] : [],
|
|
1662
|
+
...!companion && apex && !withWww ? [
|
|
1663
|
+
"",
|
|
1664
|
+
` --no-www: ${domain.hostname} was registered on its own. If your registrar`,
|
|
1665
|
+
` refuses the record above, run \`naro domains add ${alternative}\`.`
|
|
1666
|
+
] : [],
|
|
1667
|
+
"",
|
|
1668
|
+
isReady(domain) ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains verify`."
|
|
1669
|
+
]
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
async function verifyAll(client, projectId, hostname, auto = false) {
|
|
1673
|
+
if (hostname !== void 0)
|
|
1674
|
+
return [
|
|
1675
|
+
await client.domains.verifyByHostname(projectId, hostname, { auto })
|
|
1676
|
+
];
|
|
1677
|
+
const listed = await client.domains.list(projectId);
|
|
1678
|
+
const verified = [];
|
|
1679
|
+
for (const domain of listed)
|
|
1680
|
+
verified.push(await client.domains.verify(projectId, domain.id, { auto }));
|
|
1681
|
+
return verified;
|
|
1682
|
+
}
|
|
1683
|
+
async function runDomainsVerify(context, overrides = {}) {
|
|
1684
|
+
const deps = { ...defaults3, ...overrides };
|
|
1685
|
+
const client = await context.client();
|
|
1686
|
+
const projectId = await context.resolveProjectId();
|
|
1687
|
+
const hostname = context.positionals[0];
|
|
1688
|
+
const wait = context.flags.wait === true;
|
|
1689
|
+
const progress = (line) => {
|
|
1690
|
+
if (!context.flags.json) context.output.log(line);
|
|
1691
|
+
};
|
|
1692
|
+
let domains = await verifyAll(client, projectId, hostname);
|
|
1693
|
+
if (domains.length === 0) {
|
|
1694
|
+
context.output.result({ projectId, domains, ready: false }, [
|
|
1695
|
+
"No custom domains. Add one with `naro domains add example.com`."
|
|
1696
|
+
]);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
let timedOut = false;
|
|
1700
|
+
let abandoned = false;
|
|
1701
|
+
if (wait && !domains.every(isReady)) {
|
|
1702
|
+
const deadline = deps.now() + WAIT_TIMEOUT_MS;
|
|
1703
|
+
let delay = FIRST_DELAY_MS;
|
|
1704
|
+
for (; ; ) {
|
|
1705
|
+
const pending = domains.filter((domain) => !isReady(domain));
|
|
1706
|
+
if (pending.length > 0 && pending.every(isTerminal)) {
|
|
1707
|
+
abandoned = true;
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
const remaining = deadline - deps.now();
|
|
1711
|
+
if (remaining <= 0) {
|
|
1712
|
+
timedOut = true;
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
for (const domain of pending)
|
|
1716
|
+
progress(
|
|
1717
|
+
isTerminal(domain) ? `\u26A0 ${domain.hostname}: ${axisSummary(domain)} \u2014 waiting cannot fix this one` : `\u25B8 ${domain.hostname}: ${axisSummary(domain)}`
|
|
1718
|
+
);
|
|
1719
|
+
await deps.sleep(Math.min(delay, remaining));
|
|
1720
|
+
delay = Math.min(delay * 2, MAX_DELAY_MS);
|
|
1721
|
+
domains = await verifyAll(client, projectId, hostname, true);
|
|
1722
|
+
if (domains.every(isReady)) break;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const ready = domains.every(isReady);
|
|
1726
|
+
const body = domains.flatMap(domainLines);
|
|
1727
|
+
if (!ready) {
|
|
1728
|
+
for (const line of body) progress(line);
|
|
1729
|
+
const notReady = domains.filter((domain) => !isReady(domain));
|
|
1730
|
+
const names = notReady.map((domain) => domain.hostname).join(", ");
|
|
1731
|
+
const details = { projectId, domains, ready: false };
|
|
1732
|
+
if (timedOut) {
|
|
1733
|
+
const cause2 = notReady[0]?.cause;
|
|
1734
|
+
throw new NaroError(
|
|
1735
|
+
"TIMEOUT",
|
|
1736
|
+
`${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.`,
|
|
1737
|
+
{ details }
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
if (abandoned) {
|
|
1741
|
+
const blocked = domains.find(isTerminal);
|
|
1742
|
+
throw new NaroError(
|
|
1743
|
+
blocked?.cause?.code ?? "DOMAIN_VERIFICATION_FAILED",
|
|
1744
|
+
blocked?.cause?.message ?? `${names} cannot be verified (status: ${blocked?.status ?? "unknown"}). Remove it and add it again.`,
|
|
1745
|
+
{ details }
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
const cause = notReady[0]?.cause;
|
|
1749
|
+
throw new NaroError(
|
|
1750
|
+
cause?.code ?? "DOMAIN_NOT_READY",
|
|
1751
|
+
`${names} is not live yet.${cause ? ` ${cause.message}` : ""} Add the records above, then run \`naro domains verify --wait\`.`,
|
|
1752
|
+
{ details }
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
context.output.result({ projectId, domains, ready }, [
|
|
1756
|
+
...body,
|
|
960
1757
|
"",
|
|
961
|
-
|
|
1758
|
+
domains.length === 1 ? `\u2705 ${domains[0]?.hostname ?? ""} is live.` : "\u2705 Every domain is live."
|
|
962
1759
|
]);
|
|
963
1760
|
}
|
|
964
1761
|
async function runDomainsRemove(context) {
|
|
@@ -979,8 +1776,8 @@ async function runDomainsRemove(context) {
|
|
|
979
1776
|
}
|
|
980
1777
|
|
|
981
1778
|
// src/commands/env.ts
|
|
982
|
-
import { existsSync, writeFileSync } from "fs";
|
|
983
|
-
import { join } from "path";
|
|
1779
|
+
import { existsSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1780
|
+
import { join as join3 } from "path";
|
|
984
1781
|
var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
|
|
985
1782
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
986
1783
|
function parseTarget(value) {
|
|
@@ -1083,12 +1880,12 @@ async function runEnvPull(context) {
|
|
|
1083
1880
|
const vars = Object.fromEntries(
|
|
1084
1881
|
selected.map((row) => [row.key, row.value ?? ""])
|
|
1085
1882
|
);
|
|
1086
|
-
const path =
|
|
1883
|
+
const path = join3(context.cwd, file);
|
|
1087
1884
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
1088
1885
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
1089
1886
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
1090
1887
|
}
|
|
1091
|
-
|
|
1888
|
+
writeFileSync3(
|
|
1092
1889
|
path,
|
|
1093
1890
|
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
1094
1891
|
${serializeDotenv(vars)}`,
|
|
@@ -1100,10 +1897,48 @@ ${serializeDotenv(vars)}`,
|
|
|
1100
1897
|
);
|
|
1101
1898
|
}
|
|
1102
1899
|
|
|
1900
|
+
// src/commands/gen.ts
|
|
1901
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1902
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
1903
|
+
async function runGenTypes(context) {
|
|
1904
|
+
const client = await context.client();
|
|
1905
|
+
const projectId = await context.resolveProjectId();
|
|
1906
|
+
const tables = await client.db.tables(projectId);
|
|
1907
|
+
const types = generateTypes(tables);
|
|
1908
|
+
const undescribed = tables.flatMap(
|
|
1909
|
+
(table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
|
|
1910
|
+
);
|
|
1911
|
+
for (const { name, error } of undescribed)
|
|
1912
|
+
context.output.warn(
|
|
1913
|
+
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
1914
|
+
);
|
|
1915
|
+
const extra = undescribed.length > 0 ? { undescribed } : {};
|
|
1916
|
+
if (context.flags.output) {
|
|
1917
|
+
const path = resolve3(context.cwd, context.flags.output);
|
|
1918
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1919
|
+
writeFileSync4(path, types);
|
|
1920
|
+
context.output.result(
|
|
1921
|
+
{
|
|
1922
|
+
projectId,
|
|
1923
|
+
output: context.flags.output,
|
|
1924
|
+
tables: tables.length,
|
|
1925
|
+
...extra
|
|
1926
|
+
},
|
|
1927
|
+
[
|
|
1928
|
+
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
1929
|
+
]
|
|
1930
|
+
);
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
context.output.result({ projectId, types, ...extra }, [
|
|
1934
|
+
types.replace(/\n$/, "")
|
|
1935
|
+
]);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1103
1938
|
// src/commands/logs.ts
|
|
1104
1939
|
var FOLLOW_INTERVAL_MS = 3e3;
|
|
1105
1940
|
function sleep(ms) {
|
|
1106
|
-
return new Promise((
|
|
1941
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1107
1942
|
}
|
|
1108
1943
|
function formatBuildLog(log) {
|
|
1109
1944
|
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
@@ -1367,6 +2202,7 @@ Usage:
|
|
|
1367
2202
|
[--root <dir>] [--name <name>] --root points at the app in a monorepo
|
|
1368
2203
|
naro deployments List deployments of the linked project
|
|
1369
2204
|
naro inspect <deployment-id> Show one deployment + build logs
|
|
2205
|
+
naro cancel <deployment-id> Stop a build that is still running
|
|
1370
2206
|
|
|
1371
2207
|
naro git connect [owner/repo] Deploy on every push (defaults to your origin remote)
|
|
1372
2208
|
naro git disconnect Stop deploying on push
|
|
@@ -1379,7 +2215,27 @@ Usage:
|
|
|
1379
2215
|
naro env remove KEY [environment] Remove
|
|
1380
2216
|
naro env pull [file] Write env vars to .env.local
|
|
1381
2217
|
|
|
1382
|
-
naro domains list | add <host> | remove <host>
|
|
2218
|
+
naro domains list | add <host> [--no-www] | remove <host>
|
|
2219
|
+
add example.com also connects www.example.com and redirects the apex to
|
|
2220
|
+
it, because most registrars cannot put a CNAME on an apex; --no-www
|
|
2221
|
+
registers the apex alone
|
|
2222
|
+
naro domains verify [host] [--wait] Re-check DNS and print what is missing
|
|
2223
|
+
exits non-zero while a domain is not live yet
|
|
2224
|
+
|
|
2225
|
+
naro db enable Create the project's database (Cloudflare D1)
|
|
2226
|
+
naro db info Database status, size, table count and limits
|
|
2227
|
+
naro db query <sql> | --file <f> Run SQL against it (reads stdin when piped)
|
|
2228
|
+
naro db push [--dry-run] Apply pending migrations from ./migrations (asks first; --yes to skip)
|
|
2229
|
+
naro db dump [--output <f>] Download a SQL dump of the database (stdout by default)
|
|
2230
|
+
[--schema-only|--data-only] [--table a,b] Cloudflare holds other queries until it finishes
|
|
2231
|
+
naro db restore --bookmark <id> Roll the database back (Time Travel, last 30 days; asks first)
|
|
2232
|
+
| --timestamp <time> [--dry-run] prints the command that undoes it
|
|
2233
|
+
naro db pull [--dir <d>] Write the remote schema as migrations/0001_remote_schema.sql
|
|
2234
|
+
and record it as applied - a baseline (asks first)
|
|
2235
|
+
naro migration new <name> Create migrations/NNNN_<name>.sql (stdin becomes the body when piped)
|
|
2236
|
+
naro migration list Local vs applied migrations
|
|
2237
|
+
naro migration up [--dry-run] Same as db push
|
|
2238
|
+
naro gen types [--output <file>] TypeScript types for the schema (stdout by default; TypeScript only)
|
|
1383
2239
|
|
|
1384
2240
|
naro promote <deployment-id> Publish a build to production
|
|
1385
2241
|
naro rollback [deployment-id] Roll production back to the previous build
|
|
@@ -1394,6 +2250,17 @@ Global options:
|
|
|
1394
2250
|
--team <slug> Team scope for project listing/linking
|
|
1395
2251
|
--cwd <dir> Run as if started in <dir>
|
|
1396
2252
|
--prod Deploy/target production
|
|
2253
|
+
--wait Poll until the domain goes live (naro domains verify)
|
|
2254
|
+
--no-www Register the apex alone (naro domains add)
|
|
2255
|
+
--file <path> SQL file to run (naro db query)
|
|
2256
|
+
--dry-run Show what would change without changing it (naro db push, db restore)
|
|
2257
|
+
--output <file> Write to a file instead of stdout (naro gen types, db dump)
|
|
2258
|
+
--dir <path> Migrations directory (default ./migrations, or <--root>/migrations)
|
|
2259
|
+
--schema-only Dump the schema without rows (naro db dump)
|
|
2260
|
+
--data-only Dump rows without the schema (naro db dump)
|
|
2261
|
+
--table <a,b> Dump only these tables, comma-separated (naro db dump)
|
|
2262
|
+
--bookmark <id> Restore point: a bookmark from db dump or db restore (naro db restore)
|
|
2263
|
+
--timestamp <time> Restore point: RFC 3339 with a timezone, or Unix seconds (naro db restore)
|
|
1397
2264
|
--debug Verbose diagnostics
|
|
1398
2265
|
|
|
1399
2266
|
Credentials: \`naro login\` opens a browser (GitHub). CI: NARO_TOKEN=$(naro token).
|
|
@@ -1432,6 +2299,8 @@ function route(parsed) {
|
|
|
1432
2299
|
return { handler: runDeployments, positionals };
|
|
1433
2300
|
case "inspect":
|
|
1434
2301
|
return { handler: runInspect, positionals };
|
|
2302
|
+
case "cancel":
|
|
2303
|
+
return { handler: runCancel, positionals };
|
|
1435
2304
|
case "logs":
|
|
1436
2305
|
return { handler: runLogs, positionals };
|
|
1437
2306
|
case "env":
|
|
@@ -1464,9 +2333,56 @@ function route(parsed) {
|
|
|
1464
2333
|
handler: runDomainsRemove,
|
|
1465
2334
|
positionals: positionals.slice(1)
|
|
1466
2335
|
};
|
|
2336
|
+
case "verify":
|
|
2337
|
+
case "check":
|
|
2338
|
+
return {
|
|
2339
|
+
handler: runDomainsVerify,
|
|
2340
|
+
positionals: positionals.slice(1)
|
|
2341
|
+
};
|
|
2342
|
+
default:
|
|
2343
|
+
return null;
|
|
2344
|
+
}
|
|
2345
|
+
case "db":
|
|
2346
|
+
switch (sub) {
|
|
2347
|
+
case void 0:
|
|
2348
|
+
case "info":
|
|
2349
|
+
return { handler: runDbInfo, positionals: positionals.slice(1) };
|
|
2350
|
+
case "enable":
|
|
2351
|
+
return { handler: runDbEnable, positionals: positionals.slice(1) };
|
|
2352
|
+
case "query":
|
|
2353
|
+
return { handler: runDbQuery, positionals: positionals.slice(1) };
|
|
2354
|
+
case "push":
|
|
2355
|
+
return { handler: runMigrationUp, positionals: positionals.slice(1) };
|
|
2356
|
+
case "dump":
|
|
2357
|
+
return { handler: runDbDump, positionals: positionals.slice(1) };
|
|
2358
|
+
case "restore":
|
|
2359
|
+
return { handler: runDbRestore, positionals: positionals.slice(1) };
|
|
2360
|
+
case "pull":
|
|
2361
|
+
return { handler: runDbPull, positionals: positionals.slice(1) };
|
|
2362
|
+
default:
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
case "migration":
|
|
2366
|
+
switch (sub) {
|
|
2367
|
+
case void 0:
|
|
2368
|
+
case "list":
|
|
2369
|
+
case "ls":
|
|
2370
|
+
return {
|
|
2371
|
+
handler: runMigrationList,
|
|
2372
|
+
positionals: positionals.slice(1)
|
|
2373
|
+
};
|
|
2374
|
+
case "new":
|
|
2375
|
+
return {
|
|
2376
|
+
handler: runMigrationNew,
|
|
2377
|
+
positionals: positionals.slice(1)
|
|
2378
|
+
};
|
|
2379
|
+
case "up":
|
|
2380
|
+
return { handler: runMigrationUp, positionals: positionals.slice(1) };
|
|
1467
2381
|
default:
|
|
1468
2382
|
return null;
|
|
1469
2383
|
}
|
|
2384
|
+
case "gen":
|
|
2385
|
+
return sub === "types" ? { handler: runGenTypes, positionals: positionals.slice(1) } : null;
|
|
1470
2386
|
case "promote":
|
|
1471
2387
|
return { handler: runPromote, positionals };
|
|
1472
2388
|
case "rollback":
|
|
@@ -1487,8 +2403,8 @@ function route(parsed) {
|
|
|
1487
2403
|
function version() {
|
|
1488
2404
|
try {
|
|
1489
2405
|
const pkg = JSON.parse(
|
|
1490
|
-
|
|
1491
|
-
|
|
2406
|
+
readFileSync3(
|
|
2407
|
+
join4(dirname3(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
1492
2408
|
"utf8"
|
|
1493
2409
|
)
|
|
1494
2410
|
);
|