naro-cli 0.7.0 → 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 +14 -0
- package/dist/{chunk-E2M6CQL7.js → chunk-ONALKL7O.js} +124 -0
- package/dist/deploy.js +1 -1
- package/dist/index.js +518 -159
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -86,6 +86,12 @@ naro db enable Create the project's database (Cloudflare D1)
|
|
|
86
86
|
naro db info Status, size, table count, limits
|
|
87
87
|
naro db query "select 1" Run SQL; --file query.sql, or pipe stdin
|
|
88
88
|
naro db push [--dry-run] Apply pending ./migrations (asks first; --yes in CI)
|
|
89
|
+
naro db dump --output backup.sql SQL dump, streamed from Cloudflare (stdout without
|
|
90
|
+
--output); --schema-only | --data-only, --table a,b
|
|
91
|
+
naro db restore --bookmark <id> Time Travel, last 30 days (or --timestamp <time>);
|
|
92
|
+
asks first, prints the command that undoes it
|
|
93
|
+
naro db pull Existing database, no migrations yet: writes its schema
|
|
94
|
+
as migrations/0001_remote_schema.sql, recorded as applied
|
|
89
95
|
naro migration new create_users Creates migrations/0001_create_users.sql
|
|
90
96
|
naro migration list Local files vs the d1_migrations ledger
|
|
91
97
|
naro gen types --output src/db.d.ts TypeScript types for the schema
|
|
@@ -108,6 +114,14 @@ An apex (`example.com`) is added as a pair by default because most registrars
|
|
|
108
114
|
goes on `www.example.com` and the apex answers with a 308 redirect to it.
|
|
109
115
|
`--no-www` registers the apex alone.
|
|
110
116
|
|
|
117
|
+
`naro db dump` asks Cloudflare for the dump and streams it straight from
|
|
118
|
+
Cloudflare's storage to the file — the bytes never pass through naro. While the
|
|
119
|
+
export runs, Cloudflare holds every other query to the database, so dump a busy
|
|
120
|
+
production database when a short pause is acceptable. It ends with the bookmark
|
|
121
|
+
the dump reflects; `naro db restore --bookmark <that>` brings the database back
|
|
122
|
+
to it. A restore rolls back everything written after that point — the
|
|
123
|
+
`d1_migrations` ledger included — and prints the bookmark that undoes it.
|
|
124
|
+
|
|
111
125
|
## Machine-readable output (CI / AI agents)
|
|
112
126
|
|
|
113
127
|
Every command supports `--json`: stdout carries exactly one JSON object,
|
|
@@ -18833,6 +18833,9 @@ var LogsApi = class {
|
|
|
18833
18833
|
return body.entries;
|
|
18834
18834
|
}
|
|
18835
18835
|
};
|
|
18836
|
+
var EXPORT_FIRST_POLL_MS = 1e3;
|
|
18837
|
+
var EXPORT_MAX_POLL_MS = 5e3;
|
|
18838
|
+
var EXPORT_TIMEOUT_MS = 15 * 6e4;
|
|
18836
18839
|
var DbApi = class {
|
|
18837
18840
|
constructor(client) {
|
|
18838
18841
|
this.client = client;
|
|
@@ -18903,9 +18906,127 @@ var DbApi = class {
|
|
|
18903
18906
|
}
|
|
18904
18907
|
);
|
|
18905
18908
|
}
|
|
18909
|
+
/**
|
|
18910
|
+
* The current Time Travel bookmark, or the one `timestamp` (RFC 3339 with a
|
|
18911
|
+
* timezone, or Unix seconds) names. The server checks the range - the last
|
|
18912
|
+
* 30 days, not before the database existed - before asking Cloudflare.
|
|
18913
|
+
*/
|
|
18914
|
+
async bookmark(projectId, options = {}) {
|
|
18915
|
+
const query = options.timestamp === void 0 ? "" : `?timestamp=${encodeURIComponent(options.timestamp)}`;
|
|
18916
|
+
const body = await this.client.request(
|
|
18917
|
+
this.path(projectId, `/bookmark${query}`),
|
|
18918
|
+
{ context: "Read bookmark" }
|
|
18919
|
+
);
|
|
18920
|
+
return { bookmark: body.bookmark, timestamp: body.timestamp };
|
|
18921
|
+
}
|
|
18922
|
+
/**
|
|
18923
|
+
* Rolls the database back to a bookmark or a timestamp - rows, schema and
|
|
18924
|
+
* the migration ledger alike - and cancels queries in flight. Restoring to
|
|
18925
|
+
* `previousBookmark` undoes it.
|
|
18926
|
+
*/
|
|
18927
|
+
async restore(projectId, target) {
|
|
18928
|
+
const given = target;
|
|
18929
|
+
const body = await this.client.request(
|
|
18930
|
+
this.path(projectId, "/restore"),
|
|
18931
|
+
{
|
|
18932
|
+
method: "POST",
|
|
18933
|
+
body: {
|
|
18934
|
+
...given.bookmark !== void 0 ? { bookmark: given.bookmark } : {},
|
|
18935
|
+
...given.timestamp !== void 0 ? { timestamp: given.timestamp } : {}
|
|
18936
|
+
},
|
|
18937
|
+
context: "Restore database"
|
|
18938
|
+
}
|
|
18939
|
+
);
|
|
18940
|
+
return { bookmark: body.bookmark, previousBookmark: body.previousBookmark };
|
|
18941
|
+
}
|
|
18942
|
+
/**
|
|
18943
|
+
* One step of an export: starts it, or - with the first step's
|
|
18944
|
+
* `atBookmark` as `currentBookmark` - polls it. See `export` for the loop.
|
|
18945
|
+
*/
|
|
18946
|
+
async exportStep(projectId, options = {}) {
|
|
18947
|
+
const body = await this.client.request(
|
|
18948
|
+
this.path(projectId, "/export"),
|
|
18949
|
+
{
|
|
18950
|
+
method: "POST",
|
|
18951
|
+
// Only the keys given: the server validates each one it sees.
|
|
18952
|
+
body: {
|
|
18953
|
+
...options.schemaOnly !== void 0 ? { schemaOnly: options.schemaOnly } : {},
|
|
18954
|
+
...options.dataOnly !== void 0 ? { dataOnly: options.dataOnly } : {},
|
|
18955
|
+
...options.tables !== void 0 ? { tables: options.tables } : {},
|
|
18956
|
+
...options.currentBookmark !== void 0 ? { currentBookmark: options.currentBookmark } : {}
|
|
18957
|
+
},
|
|
18958
|
+
context: "Export database"
|
|
18959
|
+
}
|
|
18960
|
+
);
|
|
18961
|
+
return body.export;
|
|
18962
|
+
}
|
|
18963
|
+
/**
|
|
18964
|
+
* Exports the database and polls until the dump is ready. The server takes
|
|
18965
|
+
* one Cloudflare step per call - a big database runs for minutes, longer
|
|
18966
|
+
* than a Worker should wait - so the loop is here: 1 s, then 1.5x per poll
|
|
18967
|
+
* up to 5 s. Cloudflare holds every other query to the database while the
|
|
18968
|
+
* export runs.
|
|
18969
|
+
*
|
|
18970
|
+
* On TIMEOUT the export is still running at Cloudflare; nothing was
|
|
18971
|
+
* cancelled.
|
|
18972
|
+
*/
|
|
18973
|
+
async export(projectId, options = {}, wait = {}) {
|
|
18974
|
+
const timeoutMs = wait.timeoutMs ?? EXPORT_TIMEOUT_MS;
|
|
18975
|
+
const sleep2 = wait.sleep ?? ((ms) => new Promise((resolve10) => setTimeout(resolve10, ms)));
|
|
18976
|
+
const given = wait.pollIntervalMs;
|
|
18977
|
+
const firstPollMs = given !== void 0 && Number.isFinite(given) && given > 0 ? given : EXPORT_FIRST_POLL_MS;
|
|
18978
|
+
const maxPollMs = Math.max(EXPORT_MAX_POLL_MS, firstPollMs);
|
|
18979
|
+
const startedAt = Date.now();
|
|
18980
|
+
let step = await this.exportStep(projectId, options);
|
|
18981
|
+
wait.onProgress?.(step);
|
|
18982
|
+
const currentBookmark = step.atBookmark;
|
|
18983
|
+
const polled = {
|
|
18984
|
+
...options,
|
|
18985
|
+
...step.tables !== void 0 ? { tables: step.tables } : {}
|
|
18986
|
+
};
|
|
18987
|
+
let intervalMs = firstPollMs;
|
|
18988
|
+
while (step.status !== "complete") {
|
|
18989
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
18990
|
+
throw new NaroError(
|
|
18991
|
+
"TIMEOUT",
|
|
18992
|
+
`The export was still running after ${String(Math.round(timeoutMs / 6e4))} minutes. Cloudflare keeps it going; run naro db dump again later.`,
|
|
18993
|
+
{ details: { atBookmark: currentBookmark } }
|
|
18994
|
+
);
|
|
18995
|
+
}
|
|
18996
|
+
await sleep2(intervalMs);
|
|
18997
|
+
intervalMs = Math.min(intervalMs * 1.5, maxPollMs);
|
|
18998
|
+
step = await this.exportStep(projectId, { ...polled, currentBookmark });
|
|
18999
|
+
wait.onProgress?.(step);
|
|
19000
|
+
}
|
|
19001
|
+
return step;
|
|
19002
|
+
}
|
|
19003
|
+
/**
|
|
19004
|
+
* Records the remote schema as the already-applied migration `name` and
|
|
19005
|
+
* returns its SQL - a baseline for a database that has none. Refused with
|
|
19006
|
+
* DATABASE_HAS_MIGRATIONS when the ledger has rows, DATABASE_EMPTY when
|
|
19007
|
+
* there is no schema. The caller writes the file.
|
|
19008
|
+
*/
|
|
19009
|
+
async pull(projectId, input) {
|
|
19010
|
+
return this.client.request(this.path(projectId, "/pull"), {
|
|
19011
|
+
method: "POST",
|
|
19012
|
+
body: { name: input.name },
|
|
19013
|
+
context: "Pull schema"
|
|
19014
|
+
});
|
|
19015
|
+
}
|
|
18906
19016
|
};
|
|
18907
19017
|
|
|
18908
19018
|
// ../../packages/sdk/src/database.ts
|
|
19019
|
+
var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
|
|
19020
|
+
var BOOKMARK_FORMAT_MESSAGE = "bookmark must look like 00000001-00000008-000050ed-272153b8520cfb93b49834d93a1a1ab3 (four hyphen-separated hex groups, 59 characters).";
|
|
19021
|
+
function isBookmark(value) {
|
|
19022
|
+
return typeof value === "string" && BOOKMARK_RE.test(value);
|
|
19023
|
+
}
|
|
19024
|
+
var RESTORE_OUTCOME_UNKNOWN_CODES = /* @__PURE__ */ new Set([
|
|
19025
|
+
"DATABASE_RESTORE_FAILED",
|
|
19026
|
+
"DATABASE_UNAVAILABLE",
|
|
19027
|
+
"TIMEOUT",
|
|
19028
|
+
"NETWORK_ERROR"
|
|
19029
|
+
]);
|
|
18909
19030
|
var MAX_CELL_CHARS = 60;
|
|
18910
19031
|
function formatCell(value) {
|
|
18911
19032
|
if (value === null || value === void 0) return "NULL";
|
|
@@ -19905,6 +20026,9 @@ export {
|
|
|
19905
20026
|
openBrowser,
|
|
19906
20027
|
isTerminalStatus,
|
|
19907
20028
|
NaroClient,
|
|
20029
|
+
BOOKMARK_FORMAT_MESSAGE,
|
|
20030
|
+
isBookmark,
|
|
20031
|
+
RESTORE_OUTCOME_UNKNOWN_CODES,
|
|
19908
20032
|
renderRows,
|
|
19909
20033
|
summarizeStatement,
|
|
19910
20034
|
serializeDotenv,
|
package/dist/deploy.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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,
|
|
6
8
|
compareMigrations,
|
|
7
9
|
controlPlaneUrl,
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
detectGitInfo,
|
|
11
13
|
ensureGitignoreHasNaro,
|
|
12
14
|
generateTypes,
|
|
15
|
+
isBookmark,
|
|
13
16
|
isExpired,
|
|
14
17
|
isNaroError,
|
|
15
18
|
isTerminalStatus,
|
|
@@ -33,7 +36,7 @@ import {
|
|
|
33
36
|
summarizeStatement,
|
|
34
37
|
toNaroError,
|
|
35
38
|
writeProjectLink
|
|
36
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-ONALKL7O.js";
|
|
37
40
|
|
|
38
41
|
// src/args.ts
|
|
39
42
|
var BOOLEAN_FLAGS = {
|
|
@@ -52,7 +55,9 @@ var BOOLEAN_FLAGS = {
|
|
|
52
55
|
"--reveal": "reveal",
|
|
53
56
|
"--wait": "wait",
|
|
54
57
|
"--no-www": "noWww",
|
|
55
|
-
"--dry-run": "dryRun"
|
|
58
|
+
"--dry-run": "dryRun",
|
|
59
|
+
"--schema-only": "schemaOnly",
|
|
60
|
+
"--data-only": "dataOnly"
|
|
56
61
|
};
|
|
57
62
|
var STRING_FLAGS = {
|
|
58
63
|
"--token": "token",
|
|
@@ -67,8 +72,15 @@ var STRING_FLAGS = {
|
|
|
67
72
|
"--level": "level",
|
|
68
73
|
"--file": "file",
|
|
69
74
|
"--output": "output",
|
|
70
|
-
"--dir": "dir"
|
|
75
|
+
"--dir": "dir",
|
|
76
|
+
"--table": "table",
|
|
77
|
+
"--timestamp": "timestamp",
|
|
78
|
+
"--bookmark": "bookmark"
|
|
71
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
|
+
}
|
|
72
84
|
function parseArgs(argv) {
|
|
73
85
|
const flags = {
|
|
74
86
|
json: false,
|
|
@@ -220,7 +232,7 @@ var TtyPrompter = class {
|
|
|
220
232
|
});
|
|
221
233
|
try {
|
|
222
234
|
return await new Promise(
|
|
223
|
-
(
|
|
235
|
+
(resolve4) => rl.question(question, resolve4)
|
|
224
236
|
);
|
|
225
237
|
} finally {
|
|
226
238
|
rl.close();
|
|
@@ -241,7 +253,7 @@ var TtyPrompter = class {
|
|
|
241
253
|
});
|
|
242
254
|
try {
|
|
243
255
|
const answer = new Promise(
|
|
244
|
-
(
|
|
256
|
+
(resolve4) => rl.question(question, resolve4)
|
|
245
257
|
);
|
|
246
258
|
muted = true;
|
|
247
259
|
const value = await answer;
|
|
@@ -353,7 +365,7 @@ function createContext(flags, positionals = []) {
|
|
|
353
365
|
|
|
354
366
|
// src/router.ts
|
|
355
367
|
import { readFileSync as readFileSync3 } from "fs";
|
|
356
|
-
import { dirname as
|
|
368
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
357
369
|
import { fileURLToPath } from "url";
|
|
358
370
|
|
|
359
371
|
// src/commands/auth.ts
|
|
@@ -566,11 +578,474 @@ async function runDbQuery(context, deps = processStdin) {
|
|
|
566
578
|
context.output.result({ projectId, result }, lines);
|
|
567
579
|
}
|
|
568
580
|
|
|
569
|
-
// src/commands/
|
|
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
|
|
570
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 = {
|
|
571
1046
|
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
572
1047
|
openBrowser,
|
|
573
|
-
sleep: (ms) => new Promise((
|
|
1048
|
+
sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms))
|
|
574
1049
|
};
|
|
575
1050
|
function connected(context, project) {
|
|
576
1051
|
const branch = project.defaultBranch;
|
|
@@ -589,7 +1064,7 @@ function connected(context, project) {
|
|
|
589
1064
|
);
|
|
590
1065
|
}
|
|
591
1066
|
async function runGitConnect(context, overrides = {}) {
|
|
592
|
-
const deps = { ...
|
|
1067
|
+
const deps = { ...defaults2, ...overrides };
|
|
593
1068
|
const { output } = context;
|
|
594
1069
|
const explicit = context.positionals[0];
|
|
595
1070
|
let fullName;
|
|
@@ -676,7 +1151,7 @@ async function runGitDisconnect(context) {
|
|
|
676
1151
|
]);
|
|
677
1152
|
}
|
|
678
1153
|
async function offerAutoDeploy(context, projectId, overrides) {
|
|
679
|
-
const deps = { ...
|
|
1154
|
+
const deps = { ...defaults2, ...overrides };
|
|
680
1155
|
if (overrides.canPrompt === false) return;
|
|
681
1156
|
try {
|
|
682
1157
|
if (readProjectLink(context.cwd)?.autoDeployDeclined === true) return;
|
|
@@ -1057,8 +1532,8 @@ async function runInspect(context) {
|
|
|
1057
1532
|
}
|
|
1058
1533
|
|
|
1059
1534
|
// src/commands/domains.ts
|
|
1060
|
-
var
|
|
1061
|
-
sleep: (ms) => new Promise((
|
|
1535
|
+
var defaults3 = {
|
|
1536
|
+
sleep: (ms) => new Promise((resolve4) => setTimeout(resolve4, ms)),
|
|
1062
1537
|
now: () => Date.now()
|
|
1063
1538
|
};
|
|
1064
1539
|
var FIRST_DELAY_MS = 5e3;
|
|
@@ -1206,7 +1681,7 @@ async function verifyAll(client, projectId, hostname, auto = false) {
|
|
|
1206
1681
|
return verified;
|
|
1207
1682
|
}
|
|
1208
1683
|
async function runDomainsVerify(context, overrides = {}) {
|
|
1209
|
-
const deps = { ...
|
|
1684
|
+
const deps = { ...defaults3, ...overrides };
|
|
1210
1685
|
const client = await context.client();
|
|
1211
1686
|
const projectId = await context.resolveProjectId();
|
|
1212
1687
|
const hostname = context.positionals[0];
|
|
@@ -1301,8 +1776,8 @@ async function runDomainsRemove(context) {
|
|
|
1301
1776
|
}
|
|
1302
1777
|
|
|
1303
1778
|
// src/commands/env.ts
|
|
1304
|
-
import { existsSync, writeFileSync } from "fs";
|
|
1305
|
-
import { join } from "path";
|
|
1779
|
+
import { existsSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1780
|
+
import { join as join3 } from "path";
|
|
1306
1781
|
var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
|
|
1307
1782
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1308
1783
|
function parseTarget(value) {
|
|
@@ -1405,12 +1880,12 @@ async function runEnvPull(context) {
|
|
|
1405
1880
|
const vars = Object.fromEntries(
|
|
1406
1881
|
selected.map((row) => [row.key, row.value ?? ""])
|
|
1407
1882
|
);
|
|
1408
|
-
const path =
|
|
1883
|
+
const path = join3(context.cwd, file);
|
|
1409
1884
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
1410
1885
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
1411
1886
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
1412
1887
|
}
|
|
1413
|
-
|
|
1888
|
+
writeFileSync3(
|
|
1414
1889
|
path,
|
|
1415
1890
|
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
1416
1891
|
${serializeDotenv(vars)}`,
|
|
@@ -1423,141 +1898,8 @@ ${serializeDotenv(vars)}`,
|
|
|
1423
1898
|
}
|
|
1424
1899
|
|
|
1425
1900
|
// src/commands/gen.ts
|
|
1426
|
-
import { mkdirSync as
|
|
1427
|
-
import { dirname, resolve as
|
|
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
|
|
1901
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1902
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
1561
1903
|
async function runGenTypes(context) {
|
|
1562
1904
|
const client = await context.client();
|
|
1563
1905
|
const projectId = await context.resolveProjectId();
|
|
@@ -1572,9 +1914,9 @@ async function runGenTypes(context) {
|
|
|
1572
1914
|
);
|
|
1573
1915
|
const extra = undescribed.length > 0 ? { undescribed } : {};
|
|
1574
1916
|
if (context.flags.output) {
|
|
1575
|
-
const path =
|
|
1576
|
-
|
|
1577
|
-
|
|
1917
|
+
const path = resolve3(context.cwd, context.flags.output);
|
|
1918
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1919
|
+
writeFileSync4(path, types);
|
|
1578
1920
|
context.output.result(
|
|
1579
1921
|
{
|
|
1580
1922
|
projectId,
|
|
@@ -1596,7 +1938,7 @@ async function runGenTypes(context) {
|
|
|
1596
1938
|
// src/commands/logs.ts
|
|
1597
1939
|
var FOLLOW_INTERVAL_MS = 3e3;
|
|
1598
1940
|
function sleep(ms) {
|
|
1599
|
-
return new Promise((
|
|
1941
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1600
1942
|
}
|
|
1601
1943
|
function formatBuildLog(log) {
|
|
1602
1944
|
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
@@ -1884,6 +2226,12 @@ Usage:
|
|
|
1884
2226
|
naro db info Database status, size, table count and limits
|
|
1885
2227
|
naro db query <sql> | --file <f> Run SQL against it (reads stdin when piped)
|
|
1886
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)
|
|
1887
2235
|
naro migration new <name> Create migrations/NNNN_<name>.sql (stdin becomes the body when piped)
|
|
1888
2236
|
naro migration list Local vs applied migrations
|
|
1889
2237
|
naro migration up [--dry-run] Same as db push
|
|
@@ -1905,9 +2253,14 @@ Global options:
|
|
|
1905
2253
|
--wait Poll until the domain goes live (naro domains verify)
|
|
1906
2254
|
--no-www Register the apex alone (naro domains add)
|
|
1907
2255
|
--file <path> SQL file to run (naro db query)
|
|
1908
|
-
--dry-run
|
|
1909
|
-
--output <file> Write to a file instead of stdout (naro gen types)
|
|
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)
|
|
1910
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)
|
|
1911
2264
|
--debug Verbose diagnostics
|
|
1912
2265
|
|
|
1913
2266
|
Credentials: \`naro login\` opens a browser (GitHub). CI: NARO_TOKEN=$(naro token).
|
|
@@ -2000,6 +2353,12 @@ function route(parsed) {
|
|
|
2000
2353
|
return { handler: runDbQuery, positionals: positionals.slice(1) };
|
|
2001
2354
|
case "push":
|
|
2002
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) };
|
|
2003
2362
|
default:
|
|
2004
2363
|
return null;
|
|
2005
2364
|
}
|
|
@@ -2045,7 +2404,7 @@ function version() {
|
|
|
2045
2404
|
try {
|
|
2046
2405
|
const pkg = JSON.parse(
|
|
2047
2406
|
readFileSync3(
|
|
2048
|
-
|
|
2407
|
+
join4(dirname3(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
2049
2408
|
"utf8"
|
|
2050
2409
|
)
|
|
2051
2410
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"typescript": "7.0.2",
|
|
34
34
|
"vitest": "3.2.7",
|
|
35
35
|
"@next-deploy/build": "0.1.0",
|
|
36
|
-
"@next-deploy/
|
|
36
|
+
"@next-deploy/config": "0.1.0",
|
|
37
37
|
"@next-deploy/sdk": "0.1.0",
|
|
38
|
-
"@next-deploy/
|
|
38
|
+
"@next-deploy/core": "0.1.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsup && pnpm build:types",
|