naro-cli 0.17.0 → 0.19.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 +384 -24
- package/dist/{chunk-PYRN5XR6.js → chunk-DEEFHVDZ.js} +1616 -230
- package/dist/deploy.d.ts +38 -1
- package/dist/deploy.js +8 -4
- package/dist/env-consent.d.ts +9 -1
- package/dist/env-surface.d.ts +12 -0
- package/dist/index.js +782 -237
- package/dist/prompt.d.ts +16 -2
- package/dist/working-tree.d.ts +74 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -33,8 +33,10 @@ import {
|
|
|
33
33
|
generateTypes,
|
|
34
34
|
indexesFromRows,
|
|
35
35
|
isBookmark,
|
|
36
|
+
isEnvTargetName,
|
|
36
37
|
isExpired,
|
|
37
38
|
isNaroError,
|
|
39
|
+
isReservedEnvTarget,
|
|
38
40
|
isStalledBackend,
|
|
39
41
|
isTerminalStatus,
|
|
40
42
|
lintSchema,
|
|
@@ -42,11 +44,13 @@ import {
|
|
|
42
44
|
loadPolicy,
|
|
43
45
|
loadStoredSession,
|
|
44
46
|
login,
|
|
47
|
+
maskEnvValue,
|
|
45
48
|
migrationNameMismatch,
|
|
46
49
|
migrationsPendingError,
|
|
47
50
|
nextMigrationFileName,
|
|
48
51
|
openBrowser,
|
|
49
52
|
parseCacheControl,
|
|
53
|
+
parseDotenv,
|
|
50
54
|
parseGitHubRemote,
|
|
51
55
|
parsePolicyNode,
|
|
52
56
|
parsePolicyStatement,
|
|
@@ -57,6 +61,7 @@ import {
|
|
|
57
61
|
policiesLostByReset,
|
|
58
62
|
projectPath,
|
|
59
63
|
promptForMissingEnv,
|
|
64
|
+
pullEnv,
|
|
60
65
|
quoteIdent,
|
|
61
66
|
quotePolicyName,
|
|
62
67
|
readProjectLink,
|
|
@@ -77,12 +82,16 @@ import {
|
|
|
77
82
|
splitStatements,
|
|
78
83
|
storedPolicySql,
|
|
79
84
|
summarizeStatement,
|
|
85
|
+
summarizeTeamUsage,
|
|
86
|
+
suppliedToken,
|
|
80
87
|
systemTablePredicate,
|
|
81
88
|
tableNames,
|
|
82
89
|
toNaroError,
|
|
83
90
|
unreadablePolicyComment,
|
|
91
|
+
usageMonthOf,
|
|
92
|
+
userForAccessToken,
|
|
84
93
|
writeProjectLink
|
|
85
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-DEEFHVDZ.js";
|
|
86
95
|
import "./chunk-K3NQKI34.js";
|
|
87
96
|
|
|
88
97
|
// src/args.ts
|
|
@@ -121,7 +130,8 @@ var BOOLEAN_FLAGS = {
|
|
|
121
130
|
"--empty": "empty",
|
|
122
131
|
"--recursive": "recursive",
|
|
123
132
|
"-r": "recursive",
|
|
124
|
-
"--upsert": "upsert"
|
|
133
|
+
"--upsert": "upsert",
|
|
134
|
+
"--no-branch": "noBranch"
|
|
125
135
|
};
|
|
126
136
|
var REPEATED_FLAGS = {
|
|
127
137
|
"--column": "columns"
|
|
@@ -139,6 +149,7 @@ var STRING_FLAGS = {
|
|
|
139
149
|
"--level": "level",
|
|
140
150
|
"--metric": "metric",
|
|
141
151
|
"--range": "range",
|
|
152
|
+
"--month": "month",
|
|
142
153
|
"--route": "route",
|
|
143
154
|
"--path": "path",
|
|
144
155
|
"--referrer": "referrer",
|
|
@@ -175,7 +186,11 @@ var STRING_FLAGS = {
|
|
|
175
186
|
"--file-size-limit": "fileSizeLimit",
|
|
176
187
|
"--allowed-mime-types": "allowedMimeTypes",
|
|
177
188
|
"--content-type": "contentType",
|
|
178
|
-
"--cache-control": "cacheControl"
|
|
189
|
+
"--cache-control": "cacheControl",
|
|
190
|
+
"--search": "search",
|
|
191
|
+
"--copy-env": "copyEnv",
|
|
192
|
+
"--database": "database",
|
|
193
|
+
"--protection": "protection"
|
|
179
194
|
};
|
|
180
195
|
var SECRET_FLAGS = {
|
|
181
196
|
"--api-key": "--api-key-stdin",
|
|
@@ -352,6 +367,17 @@ var Output = class {
|
|
|
352
367
|
// src/prompt.ts
|
|
353
368
|
import { createInterface } from "readline";
|
|
354
369
|
import { Writable } from "stream";
|
|
370
|
+
function confirmHint(defaultYes) {
|
|
371
|
+
return defaultYes ? "(Y/n)" : "(y/N)";
|
|
372
|
+
}
|
|
373
|
+
function readConfirmAnswer(answer, defaultYes) {
|
|
374
|
+
const normalized = answer.trim().toLowerCase();
|
|
375
|
+
if (normalized === "") return defaultYes;
|
|
376
|
+
if (normalized === "y" || normalized === "yes") return true;
|
|
377
|
+
if (!defaultYes) return false;
|
|
378
|
+
if (normalized === "n" || normalized === "no") return false;
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
355
381
|
function isInteractive() {
|
|
356
382
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
357
383
|
}
|
|
@@ -368,7 +394,7 @@ var TtyPrompter = class {
|
|
|
368
394
|
});
|
|
369
395
|
try {
|
|
370
396
|
return await new Promise(
|
|
371
|
-
(
|
|
397
|
+
(resolve7) => rl.question(question, resolve7)
|
|
372
398
|
);
|
|
373
399
|
} finally {
|
|
374
400
|
rl.close();
|
|
@@ -389,7 +415,7 @@ var TtyPrompter = class {
|
|
|
389
415
|
});
|
|
390
416
|
try {
|
|
391
417
|
const answer = new Promise(
|
|
392
|
-
(
|
|
418
|
+
(resolve7) => rl.question(question, resolve7)
|
|
393
419
|
);
|
|
394
420
|
muted = true;
|
|
395
421
|
const value = await answer;
|
|
@@ -399,9 +425,15 @@ var TtyPrompter = class {
|
|
|
399
425
|
rl.close();
|
|
400
426
|
}
|
|
401
427
|
}
|
|
402
|
-
async confirm(question) {
|
|
403
|
-
const
|
|
404
|
-
|
|
428
|
+
async confirm(question, options = {}) {
|
|
429
|
+
const defaultYes = options.defaultYes ?? false;
|
|
430
|
+
for (; ; ) {
|
|
431
|
+
const read = readConfirmAnswer(
|
|
432
|
+
await this.ask(`${question} ${confirmHint(defaultYes)} `),
|
|
433
|
+
defaultYes
|
|
434
|
+
);
|
|
435
|
+
if (read !== null) return read;
|
|
436
|
+
}
|
|
405
437
|
}
|
|
406
438
|
async select(question, options) {
|
|
407
439
|
process.stdout.write(`${question}
|
|
@@ -500,8 +532,8 @@ function createContext(flags, positionals = []) {
|
|
|
500
532
|
}
|
|
501
533
|
|
|
502
534
|
// src/router.ts
|
|
503
|
-
import { readFileSync as
|
|
504
|
-
import { dirname as dirname4, join as
|
|
535
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
536
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
505
537
|
import { fileURLToPath } from "url";
|
|
506
538
|
|
|
507
539
|
// src/commands/migration.ts
|
|
@@ -1107,22 +1139,31 @@ async function currentSession() {
|
|
|
1107
1139
|
}
|
|
1108
1140
|
return session;
|
|
1109
1141
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const
|
|
1142
|
+
var SOURCE_LABEL = { flag: "--token", env: "NARO_TOKEN" };
|
|
1143
|
+
async function runWhoami(context, deps = { userForAccessToken }) {
|
|
1144
|
+
const supplied = suppliedToken({ explicitToken: context.flags.token });
|
|
1145
|
+
const user = supplied ? await deps.userForAccessToken(supplied.token) : (await currentSession()).user;
|
|
1146
|
+
const handle = user.github ? ` (${user.github})` : "";
|
|
1147
|
+
const via = supplied ? ` \u2014 from ${SOURCE_LABEL[supplied.source]}` : "";
|
|
1113
1148
|
context.output.result(
|
|
1114
1149
|
{
|
|
1115
|
-
user: {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
github: session.user.github
|
|
1119
|
-
}
|
|
1150
|
+
user: { id: user.id, email: user.email, github: user.github },
|
|
1151
|
+
// The name and values MCP whoami already answers with.
|
|
1152
|
+
tokenSource: supplied?.source ?? "session"
|
|
1120
1153
|
},
|
|
1121
|
-
[`Logged in as ${
|
|
1154
|
+
[`Logged in as ${user.email ?? user.id}${handle}${via}`]
|
|
1122
1155
|
);
|
|
1123
1156
|
}
|
|
1124
1157
|
async function runToken(context) {
|
|
1125
|
-
const
|
|
1158
|
+
const supplied = suppliedToken({ explicitToken: context.flags.token });
|
|
1159
|
+
const session = await currentSession().catch((error) => {
|
|
1160
|
+
if (supplied)
|
|
1161
|
+
throw new NaroError(
|
|
1162
|
+
"NOT_LOGGED_IN",
|
|
1163
|
+
`\`naro token\` makes a token from a \`naro login\` session, and there is none here. ${SOURCE_LABEL[supplied.source]} is already a token: the other commands use it as is.`
|
|
1164
|
+
);
|
|
1165
|
+
throw error;
|
|
1166
|
+
});
|
|
1126
1167
|
process.stderr.write(
|
|
1127
1168
|
"\u26A0 Access tokens expire after about an hour \u2014 generate one per CI run.\n"
|
|
1128
1169
|
);
|
|
@@ -2504,7 +2545,7 @@ async function runDbLint(context) {
|
|
|
2504
2545
|
(message) => context.output.warn(message)
|
|
2505
2546
|
);
|
|
2506
2547
|
const input = {
|
|
2507
|
-
tables: tables.filter((
|
|
2548
|
+
tables: tables.filter((table3) => !shadow.has(table3.name)),
|
|
2508
2549
|
foreignKeyCheck,
|
|
2509
2550
|
quickCheck: quick.rows.map(firstCell),
|
|
2510
2551
|
triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
|
|
@@ -2639,7 +2680,8 @@ async function policyPlan(client, projectId, schema, dir, files) {
|
|
|
2639
2680
|
tables: schema.tables,
|
|
2640
2681
|
files: files.map((name) => readFileSync3(join3(dir, name), "utf8")),
|
|
2641
2682
|
// Storage's table stays; its policies are emptied with the rest (PR
|
|
2642
|
-
// #85 리뷰).
|
|
2683
|
+
// #85 리뷰). Realtime's messages need no flag: a virtual target, which
|
|
2684
|
+
// reset-plan counts as a table always there (Realtime 3-2a).
|
|
2643
2685
|
storage: schema.storageTable
|
|
2644
2686
|
})
|
|
2645
2687
|
};
|
|
@@ -3230,7 +3272,7 @@ function conditionsFrom(flags) {
|
|
|
3230
3272
|
);
|
|
3231
3273
|
return conditions;
|
|
3232
3274
|
}
|
|
3233
|
-
function patchFromFile(context, path,
|
|
3275
|
+
function patchFromFile(context, path, table3) {
|
|
3234
3276
|
let text;
|
|
3235
3277
|
try {
|
|
3236
3278
|
text = readFileSync4(resolve3(context.cwd, path), "utf8");
|
|
@@ -3252,9 +3294,9 @@ function patchFromFile(context, path, table2) {
|
|
|
3252
3294
|
if (Object.hasOwn(body, "table")) {
|
|
3253
3295
|
if (typeof named !== "string")
|
|
3254
3296
|
refuse2(`${path}: table must be a string, the table's name.`);
|
|
3255
|
-
if (
|
|
3297
|
+
if (table3 !== void 0 && asciiLower(named) !== asciiLower(table3))
|
|
3256
3298
|
refuse2(
|
|
3257
|
-
`${path} is the policy of ${printable(named)}, not ${printable(
|
|
3299
|
+
`${path} is the policy of ${printable(named)}, not ${printable(table3)}. Remove its "table" to set it on ${printable(table3)}.`
|
|
3258
3300
|
);
|
|
3259
3301
|
}
|
|
3260
3302
|
const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
|
|
@@ -3267,7 +3309,7 @@ function patchFromFile(context, path, table2) {
|
|
|
3267
3309
|
function asciiLower(text) {
|
|
3268
3310
|
return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
3269
3311
|
}
|
|
3270
|
-
function policyPatch(flags, context,
|
|
3312
|
+
function policyPatch(flags, context, table3) {
|
|
3271
3313
|
if (flags.fromFile !== void 0) {
|
|
3272
3314
|
if (OTHER_FLAGS.some(
|
|
3273
3315
|
(key) => flags[key] !== void 0 && flags[key] !== false
|
|
@@ -3276,7 +3318,7 @@ function policyPatch(flags, context, table2) {
|
|
|
3276
3318
|
"--from-file takes the whole policy: leave out the other policy options."
|
|
3277
3319
|
);
|
|
3278
3320
|
if (!context) refuse2(SET_USAGE);
|
|
3279
|
-
return patchFromFile(context, flags.fromFile,
|
|
3321
|
+
return patchFromFile(context, flags.fromFile, table3);
|
|
3280
3322
|
}
|
|
3281
3323
|
const patch = {};
|
|
3282
3324
|
for (const action of POLICY_ACTIONS) {
|
|
@@ -3415,7 +3457,7 @@ function policyTable(policies) {
|
|
|
3415
3457
|
const widths = header.map(
|
|
3416
3458
|
(title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
|
|
3417
3459
|
);
|
|
3418
|
-
const
|
|
3460
|
+
const table3 = [header, ...rows].map(
|
|
3419
3461
|
(cells) => cells.map(
|
|
3420
3462
|
(cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
|
|
3421
3463
|
).join(" ").trimEnd()
|
|
@@ -3432,10 +3474,10 @@ function policyTable(policies) {
|
|
|
3432
3474
|
}
|
|
3433
3475
|
)
|
|
3434
3476
|
);
|
|
3435
|
-
if (lines.length === 0) return
|
|
3477
|
+
if (lines.length === 0) return table3;
|
|
3436
3478
|
const width = Math.max(...lines.map((line) => line.label.length));
|
|
3437
3479
|
return [
|
|
3438
|
-
...
|
|
3480
|
+
...table3,
|
|
3439
3481
|
"",
|
|
3440
3482
|
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
|
|
3441
3483
|
];
|
|
@@ -3452,39 +3494,39 @@ function statementLine(policy) {
|
|
|
3452
3494
|
);
|
|
3453
3495
|
return shownStatement(policy, policy.table, "naro policy list --json");
|
|
3454
3496
|
}
|
|
3455
|
-
function policyNamed(name,
|
|
3456
|
-
return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(
|
|
3497
|
+
function policyNamed(name, table3) {
|
|
3498
|
+
return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table3))}`;
|
|
3457
3499
|
}
|
|
3458
|
-
function shownStatement(statement,
|
|
3500
|
+
function shownStatement(statement, table3, exact) {
|
|
3459
3501
|
if (isPrintable(statement.sql)) return statement.sql;
|
|
3460
3502
|
return printable(
|
|
3461
|
-
`-- ${policyNamed(statement.name,
|
|
3503
|
+
`-- ${policyNamed(statement.name, table3)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
|
|
3462
3504
|
);
|
|
3463
3505
|
}
|
|
3464
3506
|
function policyStatements(policies) {
|
|
3465
3507
|
const lines = [];
|
|
3466
|
-
let
|
|
3508
|
+
let table3;
|
|
3467
3509
|
for (const policy of policies) {
|
|
3468
|
-
if (
|
|
3469
|
-
|
|
3510
|
+
if (table3 !== void 0 && policy.table !== table3) lines.push("");
|
|
3511
|
+
table3 = policy.table;
|
|
3470
3512
|
lines.push(statementLine(policy));
|
|
3471
3513
|
}
|
|
3472
3514
|
return lines;
|
|
3473
3515
|
}
|
|
3474
3516
|
async function runPolicyList(context) {
|
|
3475
|
-
const
|
|
3517
|
+
const table3 = context.flags.table;
|
|
3476
3518
|
const client = await context.client();
|
|
3477
3519
|
const projectId = await context.resolveProjectId();
|
|
3478
3520
|
const policies = await client.backend.rlsPolicies(
|
|
3479
3521
|
projectId,
|
|
3480
|
-
|
|
3522
|
+
table3 === void 0 ? {} : { table: table3 }
|
|
3481
3523
|
);
|
|
3482
3524
|
let lines;
|
|
3483
3525
|
if (policies.length > 0)
|
|
3484
3526
|
lines = [...policyStatements(policies), "", DEFAULT_DENY];
|
|
3485
|
-
else if (
|
|
3527
|
+
else if (table3 !== void 0)
|
|
3486
3528
|
lines = [
|
|
3487
|
-
`No policies on ${printable(
|
|
3529
|
+
`No policies on ${printable(table3)}: anon and signed-in requests to it are denied.`
|
|
3488
3530
|
];
|
|
3489
3531
|
else
|
|
3490
3532
|
lines = [
|
|
@@ -3580,17 +3622,17 @@ async function runPolicyCreate(context) {
|
|
|
3580
3622
|
}
|
|
3581
3623
|
async function runPolicyDrop(context) {
|
|
3582
3624
|
const [name] = context.positionalOrThrow(1, DROP_USAGE);
|
|
3583
|
-
const
|
|
3584
|
-
if (
|
|
3625
|
+
const table3 = context.flags.table;
|
|
3626
|
+
if (table3 === void 0 || table3 === "") refuse2(DROP_USAGE);
|
|
3585
3627
|
await confirmOrAbort(
|
|
3586
3628
|
context,
|
|
3587
|
-
`Drop policy ${printable(name)} on ${printable(
|
|
3629
|
+
`Drop policy ${printable(name)} on ${printable(table3)}? It stops applying within about 10 seconds.`
|
|
3588
3630
|
);
|
|
3589
3631
|
const client = await context.client();
|
|
3590
3632
|
const projectId = await context.resolveProjectId();
|
|
3591
3633
|
const dropped = await client.backend.dropRlsPolicy(
|
|
3592
3634
|
projectId,
|
|
3593
|
-
|
|
3635
|
+
table3,
|
|
3594
3636
|
name
|
|
3595
3637
|
);
|
|
3596
3638
|
context.output.result({ projectId, dropped }, [
|
|
@@ -3598,13 +3640,13 @@ async function runPolicyDrop(context) {
|
|
|
3598
3640
|
]);
|
|
3599
3641
|
}
|
|
3600
3642
|
async function runPolicySet(context) {
|
|
3601
|
-
const [
|
|
3602
|
-
const patch = policyPatch(context.flags, context,
|
|
3643
|
+
const [table3] = context.positionalOrThrow(1, SET_USAGE);
|
|
3644
|
+
const patch = policyPatch(context.flags, context, table3);
|
|
3603
3645
|
const client = await context.client();
|
|
3604
3646
|
const projectId = await context.resolveProjectId();
|
|
3605
3647
|
const policy = await client.backend.setPolicy(
|
|
3606
3648
|
projectId,
|
|
3607
|
-
|
|
3649
|
+
table3,
|
|
3608
3650
|
patch
|
|
3609
3651
|
);
|
|
3610
3652
|
let named;
|
|
@@ -3625,14 +3667,14 @@ async function runPolicySet(context) {
|
|
|
3625
3667
|
);
|
|
3626
3668
|
}
|
|
3627
3669
|
async function runPolicyRemove(context) {
|
|
3628
|
-
const [
|
|
3670
|
+
const [table3] = context.positionalOrThrow(1, REMOVE_USAGE);
|
|
3629
3671
|
await confirmOrAbort(
|
|
3630
3672
|
context,
|
|
3631
|
-
`Remove every policy on ${printable(
|
|
3673
|
+
`Remove every policy on ${printable(table3)}? Anon and signed-in requests to it are denied from then on.`
|
|
3632
3674
|
);
|
|
3633
3675
|
const client = await context.client();
|
|
3634
3676
|
const projectId = await context.resolveProjectId();
|
|
3635
|
-
const policy = await client.backend.removePolicy(projectId,
|
|
3677
|
+
const policy = await client.backend.removePolicy(projectId, table3);
|
|
3636
3678
|
const names = policy.policies ?? [];
|
|
3637
3679
|
context.output.result({ projectId, removed: true, policy }, [
|
|
3638
3680
|
names.length === 0 ? `\u2705 Removed the policy for ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.` : `\u2705 Removed ${names.length === 1 ? "policy" : "policies"} ${names.map(printable).join(", ")} on ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.`
|
|
@@ -3719,14 +3761,14 @@ function parseColumnSpec(spec) {
|
|
|
3719
3761
|
column.default = defaultValue(value);
|
|
3720
3762
|
else if (value !== void 0 && key === "references") {
|
|
3721
3763
|
const dot = value.lastIndexOf(".");
|
|
3722
|
-
const
|
|
3764
|
+
const table3 = dot < 0 ? "" : value.slice(0, dot);
|
|
3723
3765
|
const target = dot < 0 ? "" : value.slice(dot + 1);
|
|
3724
|
-
if (!
|
|
3766
|
+
if (!table3 || !target)
|
|
3725
3767
|
throw new NaroError(
|
|
3726
3768
|
"VALIDATION_FAILED",
|
|
3727
3769
|
`${shown}: references needs <table>.<column>.`
|
|
3728
3770
|
);
|
|
3729
|
-
column.references = { table:
|
|
3771
|
+
column.references = { table: table3, column: target };
|
|
3730
3772
|
} else if (value !== void 0 && key === "ondelete") {
|
|
3731
3773
|
onDelete = ON_DELETE[value.toLowerCase()];
|
|
3732
3774
|
if (!onDelete)
|
|
@@ -3806,14 +3848,6 @@ function removedLines(policy, dryRun) {
|
|
|
3806
3848
|
}
|
|
3807
3849
|
return lines;
|
|
3808
3850
|
}
|
|
3809
|
-
function removedBeforeFailure(error) {
|
|
3810
|
-
if (!isNaroError(error)) return [];
|
|
3811
|
-
const removed = error.details?.removedPolicies;
|
|
3812
|
-
if (!Array.isArray(removed)) return [];
|
|
3813
|
-
return removed.filter(
|
|
3814
|
-
(entry) => typeof entry === "object" && entry !== null && typeof entry.table === "string" && entry.ownerColumn !== void 0
|
|
3815
|
-
);
|
|
3816
|
-
}
|
|
3817
3851
|
async function runSchemaChange(context, operations, question) {
|
|
3818
3852
|
const dryRun = context.flags.dryRun === true;
|
|
3819
3853
|
if (question !== void 0 && !dryRun)
|
|
@@ -3839,12 +3873,6 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3839
3873
|
const result = await client.db.schema(projectId, sent, {
|
|
3840
3874
|
...dryRun ? { dryRun: true } : {},
|
|
3841
3875
|
name
|
|
3842
|
-
}).catch((error) => {
|
|
3843
|
-
if (!context.flags.json)
|
|
3844
|
-
for (const policy of removedBeforeFailure(error))
|
|
3845
|
-
for (const line of removedLines(policy, false))
|
|
3846
|
-
context.output.warn(line);
|
|
3847
|
-
throw error;
|
|
3848
3876
|
});
|
|
3849
3877
|
const sqlLines = result.sql.trimEnd().split("\n").map(printable);
|
|
3850
3878
|
const removed = result.removedPolicies ?? [];
|
|
@@ -3905,16 +3933,16 @@ async function runDbIndexUsage() {
|
|
|
3905
3933
|
throw new NaroError("VALIDATION_FAILED", USAGE2.index);
|
|
3906
3934
|
}
|
|
3907
3935
|
async function runDbTableCreate(context) {
|
|
3908
|
-
const [
|
|
3936
|
+
const [table3] = context.positionals;
|
|
3909
3937
|
const specs = columnsFlag(context);
|
|
3910
|
-
if (!
|
|
3938
|
+
if (!table3 || specs.length === 0)
|
|
3911
3939
|
throw new NaroError("VALIDATION_FAILED", USAGE2.create);
|
|
3912
3940
|
const columns = specs.map(parseColumnSpec);
|
|
3913
3941
|
const owner = context.flags.ownerColumn;
|
|
3914
3942
|
await runSchemaChange(context, [
|
|
3915
3943
|
{
|
|
3916
3944
|
op: "createTable",
|
|
3917
|
-
table:
|
|
3945
|
+
table: table3,
|
|
3918
3946
|
columns,
|
|
3919
3947
|
...context.flags.ifNotExists ? { ifNotExists: true } : {},
|
|
3920
3948
|
...owner === void 0 ? {} : { owner: { column: owner } }
|
|
@@ -3922,56 +3950,56 @@ async function runDbTableCreate(context) {
|
|
|
3922
3950
|
]);
|
|
3923
3951
|
}
|
|
3924
3952
|
async function runDbTableAddColumn(context) {
|
|
3925
|
-
const [
|
|
3953
|
+
const [table3] = context.positionals;
|
|
3926
3954
|
const specs = columnsFlag(context);
|
|
3927
3955
|
const [spec] = specs;
|
|
3928
|
-
if (!
|
|
3956
|
+
if (!table3 || spec === void 0 || specs.length !== 1)
|
|
3929
3957
|
throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
|
|
3930
3958
|
await runSchemaChange(context, [
|
|
3931
|
-
{ op: "addColumn", table:
|
|
3959
|
+
{ op: "addColumn", table: table3, column: parseColumnSpec(spec) }
|
|
3932
3960
|
]);
|
|
3933
3961
|
}
|
|
3934
3962
|
async function runDbTableRenameColumn(context) {
|
|
3935
|
-
const [
|
|
3936
|
-
if (!
|
|
3963
|
+
const [table3, column, to] = context.positionals;
|
|
3964
|
+
if (!table3 || !column || !to)
|
|
3937
3965
|
throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
|
|
3938
|
-
await runSchemaChange(context, [{ op: "renameColumn", table:
|
|
3966
|
+
await runSchemaChange(context, [{ op: "renameColumn", table: table3, column, to }]);
|
|
3939
3967
|
}
|
|
3940
3968
|
async function runDbTableRename(context) {
|
|
3941
|
-
const [
|
|
3942
|
-
if (!
|
|
3943
|
-
await runSchemaChange(context, [{ op: "renameTable", table:
|
|
3969
|
+
const [table3, to] = context.positionals;
|
|
3970
|
+
if (!table3 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
|
|
3971
|
+
await runSchemaChange(context, [{ op: "renameTable", table: table3, to }]);
|
|
3944
3972
|
}
|
|
3945
3973
|
async function runDbTableDropColumn(context) {
|
|
3946
|
-
const [
|
|
3947
|
-
if (!
|
|
3974
|
+
const [table3, column] = context.positionals;
|
|
3975
|
+
if (!table3 || !column)
|
|
3948
3976
|
throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
|
|
3949
|
-
const target = `${
|
|
3977
|
+
const target = `${table3}.${column}`;
|
|
3950
3978
|
await runSchemaChange(
|
|
3951
3979
|
context,
|
|
3952
|
-
[{ op: "dropColumn", table:
|
|
3980
|
+
[{ op: "dropColumn", table: table3, column, confirm: target }],
|
|
3953
3981
|
`Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
|
|
3954
3982
|
);
|
|
3955
3983
|
}
|
|
3956
3984
|
async function runDbTableDrop(context) {
|
|
3957
|
-
const [
|
|
3958
|
-
if (!
|
|
3985
|
+
const [table3] = context.positionals;
|
|
3986
|
+
if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
|
|
3959
3987
|
await runSchemaChange(
|
|
3960
3988
|
context,
|
|
3961
|
-
[{ op: "dropTable", table:
|
|
3962
|
-
`Drop table ${printable(
|
|
3989
|
+
[{ op: "dropTable", table: table3, confirm: table3 }],
|
|
3990
|
+
`Drop table ${printable(table3)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
|
|
3963
3991
|
);
|
|
3964
3992
|
}
|
|
3965
3993
|
async function runDbIndexCreate(context) {
|
|
3966
|
-
const [
|
|
3994
|
+
const [table3, list] = context.positionals;
|
|
3967
3995
|
const columns = listFlag(list);
|
|
3968
|
-
if (!
|
|
3996
|
+
if (!table3 || columns.length === 0)
|
|
3969
3997
|
throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
|
|
3970
3998
|
const name = context.flags.name;
|
|
3971
3999
|
await runSchemaChange(context, [
|
|
3972
4000
|
{
|
|
3973
4001
|
op: "createIndex",
|
|
3974
|
-
table:
|
|
4002
|
+
table: table3,
|
|
3975
4003
|
columns,
|
|
3976
4004
|
...context.flags.unique ? { unique: true } : {},
|
|
3977
4005
|
...name === void 0 ? {} : { name }
|
|
@@ -3988,7 +4016,7 @@ async function runDbIndexDrop(context) {
|
|
|
3988
4016
|
var defaults2 = {
|
|
3989
4017
|
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
3990
4018
|
openBrowser,
|
|
3991
|
-
sleep: (ms) => new Promise((
|
|
4019
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms))
|
|
3992
4020
|
};
|
|
3993
4021
|
function connected(context, project) {
|
|
3994
4022
|
const branch = project.defaultBranch;
|
|
@@ -4043,6 +4071,13 @@ async function runGitConnect(context, overrides = {}) {
|
|
|
4043
4071
|
`Naro cannot reach ${fullName}, and no install URL was offered. Connect it from the dashboard.`
|
|
4044
4072
|
);
|
|
4045
4073
|
}
|
|
4074
|
+
if (cannotAsk(context)) {
|
|
4075
|
+
throw new NaroError(
|
|
4076
|
+
"REPOSITORY_NOT_ACCESSIBLE",
|
|
4077
|
+
`Naro cannot reach ${fullName} yet. Install the Naro GitHub App at ${first.installUrl}, then run: naro git connect`,
|
|
4078
|
+
{ details: { projectId, installUrl: first.installUrl } }
|
|
4079
|
+
);
|
|
4080
|
+
}
|
|
4046
4081
|
output.log(`
|
|
4047
4082
|
\u25B8 Naro needs access to ${fullName}.`);
|
|
4048
4083
|
output.log(` Opening ${first.installUrl}`);
|
|
@@ -4189,7 +4224,8 @@ async function resolveTeam(context, teams, deps = {}) {
|
|
|
4189
4224
|
context.output.log(`
|
|
4190
4225
|
${account} \u2192 ${label3}`);
|
|
4191
4226
|
const ok = await context.prompter.confirm(
|
|
4192
|
-
"Deploy to this account and team?"
|
|
4227
|
+
"Deploy to this account and team?",
|
|
4228
|
+
{ defaultYes: true }
|
|
4193
4229
|
);
|
|
4194
4230
|
if (ok) return only;
|
|
4195
4231
|
return switchAndRetry(context, deps);
|
|
@@ -4296,10 +4332,30 @@ async function runUnlink(context) {
|
|
|
4296
4332
|
}
|
|
4297
4333
|
|
|
4298
4334
|
// src/commands/deploy.ts
|
|
4335
|
+
function deployTarget(flags) {
|
|
4336
|
+
if (flags.env === void 0)
|
|
4337
|
+
return { production: flags.prod, environment: void 0 };
|
|
4338
|
+
if (flags.env === "")
|
|
4339
|
+
throw new NaroError(
|
|
4340
|
+
"VALIDATION_FAILED",
|
|
4341
|
+
"--env must be production, preview or an environment name."
|
|
4342
|
+
);
|
|
4343
|
+
if (flags.prod)
|
|
4344
|
+
throw new NaroError(
|
|
4345
|
+
"VALIDATION_FAILED",
|
|
4346
|
+
"--env and --prod cannot be used together."
|
|
4347
|
+
);
|
|
4348
|
+
if (flags.env === "production")
|
|
4349
|
+
return { production: true, environment: void 0 };
|
|
4350
|
+
if (flags.env === "preview")
|
|
4351
|
+
return { production: false, environment: void 0 };
|
|
4352
|
+
return { production: false, environment: flags.env };
|
|
4353
|
+
}
|
|
4299
4354
|
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
4300
4355
|
const { flags, output } = context;
|
|
4356
|
+
const target = deployTarget(flags);
|
|
4301
4357
|
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
4302
|
-
if (
|
|
4358
|
+
if (target.production && !flags.yes && !flags.json && isInteractive()) {
|
|
4303
4359
|
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
4304
4360
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
4305
4361
|
}
|
|
@@ -4319,10 +4375,20 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4319
4375
|
teamSlug: team.slug,
|
|
4320
4376
|
rootDirectory: flags.root ?? null,
|
|
4321
4377
|
branch: flags.branch,
|
|
4322
|
-
production:
|
|
4378
|
+
production: target.production,
|
|
4379
|
+
...target.environment !== void 0 && {
|
|
4380
|
+
environment: target.environment
|
|
4381
|
+
},
|
|
4323
4382
|
...canPrompt && {
|
|
4324
4383
|
onMissingEnv: (keys) => promptForMissingEnv(
|
|
4325
|
-
{
|
|
4384
|
+
{
|
|
4385
|
+
cwd: context.cwd,
|
|
4386
|
+
prompter: context.prompter,
|
|
4387
|
+
output,
|
|
4388
|
+
...target.environment !== void 0 && {
|
|
4389
|
+
environment: target.environment
|
|
4390
|
+
}
|
|
4391
|
+
},
|
|
4326
4392
|
keys
|
|
4327
4393
|
)
|
|
4328
4394
|
},
|
|
@@ -4349,20 +4415,22 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4349
4415
|
teamSlug: result.teamSlug
|
|
4350
4416
|
});
|
|
4351
4417
|
const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
|
|
4418
|
+
const surface = result.environment ?? (result.production ? "production" : "preview");
|
|
4352
4419
|
context.output.result(
|
|
4353
4420
|
{
|
|
4354
4421
|
deploymentId: result.deploymentId,
|
|
4355
4422
|
projectId: result.projectId,
|
|
4356
4423
|
status: result.status,
|
|
4357
4424
|
url: result.url,
|
|
4358
|
-
target:
|
|
4425
|
+
target: surface,
|
|
4426
|
+
environment: result.environment ?? null,
|
|
4359
4427
|
teamSlug: result.teamSlug,
|
|
4360
4428
|
inspectUrl: inspect,
|
|
4361
4429
|
missingEnvKeys: result.missingEnvKeys
|
|
4362
4430
|
},
|
|
4363
4431
|
[
|
|
4364
4432
|
"",
|
|
4365
|
-
`\u2705 Deployment ready (${
|
|
4433
|
+
`\u2705 Deployment ready (${surface})`,
|
|
4366
4434
|
"",
|
|
4367
4435
|
` ${result.url}`,
|
|
4368
4436
|
` Inspect ${inspect}`
|
|
@@ -4476,7 +4544,7 @@ async function runInspect(context) {
|
|
|
4476
4544
|
|
|
4477
4545
|
// src/commands/domains.ts
|
|
4478
4546
|
var defaults3 = {
|
|
4479
|
-
sleep: (ms) => new Promise((
|
|
4547
|
+
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
4480
4548
|
now: () => Date.now()
|
|
4481
4549
|
};
|
|
4482
4550
|
var FIRST_DELAY_MS = 5e3;
|
|
@@ -4774,18 +4842,29 @@ async function runDomainsRemove(context) {
|
|
|
4774
4842
|
}
|
|
4775
4843
|
|
|
4776
4844
|
// src/commands/env.ts
|
|
4777
|
-
import { existsSync, writeFileSync as writeFileSync4 } from "fs";
|
|
4778
|
-
import {
|
|
4779
|
-
var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
|
|
4845
|
+
import { chmodSync, existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
4846
|
+
import { resolve as resolve4 } from "path";
|
|
4780
4847
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4848
|
+
var KEY_MAX_LENGTH = 256;
|
|
4849
|
+
var IMPORT_BATCH_SIZE = 100;
|
|
4850
|
+
var VALUE_MAX_BYTES = 5 * 1024;
|
|
4851
|
+
var TARGET_USAGE = "[production|preview|all|<environment>]";
|
|
4852
|
+
function validKey(key) {
|
|
4853
|
+
return KEY_RE.test(key) && key.length <= KEY_MAX_LENGTH;
|
|
4854
|
+
}
|
|
4855
|
+
function isSecretKey(key) {
|
|
4856
|
+
return !key.startsWith("NEXT_PUBLIC_");
|
|
4857
|
+
}
|
|
4858
|
+
function variables(count2) {
|
|
4859
|
+
return `${String(count2)} variable${count2 === 1 ? "" : "s"}`;
|
|
4860
|
+
}
|
|
4781
4861
|
function parseTarget(value) {
|
|
4782
4862
|
if (value === void 0) return "all";
|
|
4783
|
-
if (
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
return value;
|
|
4863
|
+
if (isEnvTargetName(value)) return value;
|
|
4864
|
+
throw new NaroError(
|
|
4865
|
+
"VALIDATION_FAILED",
|
|
4866
|
+
"Target must be production, preview, all or an environment name."
|
|
4867
|
+
);
|
|
4789
4868
|
}
|
|
4790
4869
|
function maskedValue(row) {
|
|
4791
4870
|
if (!row.isSecret) return row.value ?? "";
|
|
@@ -4816,38 +4895,48 @@ async function runEnvList(context) {
|
|
|
4816
4895
|
);
|
|
4817
4896
|
}
|
|
4818
4897
|
async function runEnvAdd(context) {
|
|
4819
|
-
context.positionalOrThrow(
|
|
4820
|
-
1,
|
|
4821
|
-
"Usage: naro env add KEY [production|preview|all]"
|
|
4822
|
-
);
|
|
4898
|
+
context.positionalOrThrow(1, `Usage: naro env add KEY ${TARGET_USAGE}`);
|
|
4823
4899
|
const [key, targetArg] = context.positionals;
|
|
4824
|
-
if (!key || !
|
|
4900
|
+
if (!key || !validKey(key))
|
|
4825
4901
|
throw new NaroError(
|
|
4826
4902
|
"VALIDATION_FAILED",
|
|
4827
4903
|
"Keys must look like MY_VARIABLE (letters, digits, underscore)."
|
|
4828
4904
|
);
|
|
4829
4905
|
const target = parseTarget(targetArg);
|
|
4906
|
+
const projectId = await context.resolveProjectId();
|
|
4907
|
+
const client = await context.client();
|
|
4908
|
+
const typed = process.stdin.isTTY === true;
|
|
4909
|
+
if (typed) {
|
|
4910
|
+
await client.projects.get(projectId);
|
|
4911
|
+
if (!isReservedEnvTarget(target))
|
|
4912
|
+
await client.environments.get(projectId, target);
|
|
4913
|
+
}
|
|
4830
4914
|
let value;
|
|
4831
|
-
if (!
|
|
4915
|
+
if (!typed) {
|
|
4832
4916
|
value = await readStdin();
|
|
4833
4917
|
} else {
|
|
4834
4918
|
value = await context.prompter.askHidden(`? What's the value of ${key}? `);
|
|
4835
4919
|
}
|
|
4836
4920
|
if (value.length === 0)
|
|
4837
4921
|
throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
|
|
4838
|
-
const isSecret =
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4922
|
+
const isSecret = isSecretKey(key);
|
|
4923
|
+
let saved = target;
|
|
4924
|
+
try {
|
|
4925
|
+
await client.env.set(projectId, [{ key, value, target, isSecret }]);
|
|
4926
|
+
} catch (error) {
|
|
4927
|
+
if (!typed || target !== "all" || !(error instanceof NaroError) || error.code !== "FORBIDDEN" || !await context.prompter.confirm(
|
|
4928
|
+
`${error.message} Save ${key} for preview only?`
|
|
4929
|
+
))
|
|
4930
|
+
throw error;
|
|
4931
|
+
saved = "preview";
|
|
4932
|
+
await client.env.set(projectId, [{ key, value, target: saved, isSecret }]);
|
|
4933
|
+
}
|
|
4934
|
+
context.output.result({ projectId, key, target: saved, isSecret }, [
|
|
4935
|
+
`\u2705 Added ${key} (${saved}${isSecret ? ", secret" : ""})`
|
|
4844
4936
|
]);
|
|
4845
4937
|
}
|
|
4846
4938
|
async function runEnvRemove(context) {
|
|
4847
|
-
context.positionalOrThrow(
|
|
4848
|
-
1,
|
|
4849
|
-
"Usage: naro env remove KEY [production|preview|all]"
|
|
4850
|
-
);
|
|
4939
|
+
context.positionalOrThrow(1, `Usage: naro env remove KEY ${TARGET_USAGE}`);
|
|
4851
4940
|
const [key, targetArg] = context.positionals;
|
|
4852
4941
|
const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
|
|
4853
4942
|
await confirmOrAbort(
|
|
@@ -4861,24 +4950,74 @@ async function runEnvRemove(context) {
|
|
|
4861
4950
|
`\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
|
|
4862
4951
|
]);
|
|
4863
4952
|
}
|
|
4864
|
-
async function
|
|
4865
|
-
|
|
4866
|
-
const
|
|
4867
|
-
|
|
4953
|
+
async function runEnvImport(context) {
|
|
4954
|
+
context.positionalOrThrow(1, `Usage: naro env import <file> ${TARGET_USAGE}`);
|
|
4955
|
+
const [file, targetArg] = context.positionals;
|
|
4956
|
+
const target = parseTarget(targetArg);
|
|
4957
|
+
let text;
|
|
4958
|
+
try {
|
|
4959
|
+
text = readFileSync5(resolve4(context.cwd, file), "utf8");
|
|
4960
|
+
} catch {
|
|
4961
|
+
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
4962
|
+
}
|
|
4963
|
+
const vars = Object.entries(parseDotenv(text));
|
|
4964
|
+
if (vars.length === 0)
|
|
4965
|
+
throw new NaroError("VALIDATION_FAILED", `No variables found in ${file}.`);
|
|
4966
|
+
const invalid = vars.map(([key]) => key).filter((key) => !validKey(key));
|
|
4967
|
+
if (invalid.length > 0)
|
|
4868
4968
|
throw new NaroError(
|
|
4869
4969
|
"VALIDATION_FAILED",
|
|
4870
|
-
|
|
4970
|
+
`Invalid keys in ${file}: ${invalid.join(", ")}.`
|
|
4971
|
+
);
|
|
4972
|
+
const oversized = vars.filter(([, value]) => Buffer.byteLength(value, "utf8") > VALUE_MAX_BYTES).map(([key]) => key);
|
|
4973
|
+
if (oversized.length > 0)
|
|
4974
|
+
throw new NaroError(
|
|
4975
|
+
"VALIDATION_FAILED",
|
|
4976
|
+
`Values over 5 KB in ${file}: ${oversized.join(", ")}.`
|
|
4871
4977
|
);
|
|
4872
|
-
const client = await context.client();
|
|
4873
4978
|
const projectId = await context.resolveProjectId();
|
|
4874
|
-
|
|
4875
|
-
const
|
|
4876
|
-
(
|
|
4979
|
+
context.output.log(`Variables in ${file}:`);
|
|
4980
|
+
for (const [key, value] of vars)
|
|
4981
|
+
context.output.log(` ${key.padEnd(32)} ${maskEnvValue(key, value)}`);
|
|
4982
|
+
await confirmOrAbort(
|
|
4983
|
+
context,
|
|
4984
|
+
`Upload ${variables(vars.length)} to ${target}?`
|
|
4877
4985
|
);
|
|
4878
|
-
const
|
|
4879
|
-
|
|
4986
|
+
const client = await context.client();
|
|
4987
|
+
const entries = vars.map(([key, value]) => ({
|
|
4988
|
+
key,
|
|
4989
|
+
value,
|
|
4990
|
+
target,
|
|
4991
|
+
isSecret: isSecretKey(key)
|
|
4992
|
+
}));
|
|
4993
|
+
for (let start = 0; start < entries.length; start += IMPORT_BATCH_SIZE)
|
|
4994
|
+
await client.env.set(
|
|
4995
|
+
projectId,
|
|
4996
|
+
entries.slice(start, start + IMPORT_BATCH_SIZE)
|
|
4997
|
+
);
|
|
4998
|
+
context.output.result({ projectId, file, target, count: entries.length }, [
|
|
4999
|
+
`\u2705 Imported ${variables(entries.length)} (${target})`
|
|
5000
|
+
]);
|
|
5001
|
+
}
|
|
5002
|
+
function parsePullSurface(value) {
|
|
5003
|
+
if (value === void 0) return "preview";
|
|
5004
|
+
if (value !== "all" && isEnvTargetName(value)) return value;
|
|
5005
|
+
throw new NaroError(
|
|
5006
|
+
"VALIDATION_FAILED",
|
|
5007
|
+
"--env must be production, preview or an environment name."
|
|
4880
5008
|
);
|
|
4881
|
-
|
|
5009
|
+
}
|
|
5010
|
+
async function runEnvPull(context) {
|
|
5011
|
+
const file = context.positionals[0] ?? ".env.local";
|
|
5012
|
+
const surface = parsePullSurface(context.flags.env);
|
|
5013
|
+
const client = await context.client();
|
|
5014
|
+
const projectId = await context.resolveProjectId();
|
|
5015
|
+
if (!isReservedEnvTarget(surface))
|
|
5016
|
+
await client.environments.get(projectId, surface);
|
|
5017
|
+
const rows = await client.env.list(projectId, { reveal: true });
|
|
5018
|
+
const vars = pullEnv(rows, surface);
|
|
5019
|
+
const count2 = Object.keys(vars).length;
|
|
5020
|
+
const path = resolve4(context.cwd, file);
|
|
4882
5021
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
4883
5022
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
4884
5023
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
@@ -4889,15 +5028,143 @@ async function runEnvPull(context) {
|
|
|
4889
5028
|
${serializeDotenv(vars)}`,
|
|
4890
5029
|
{ mode: 384 }
|
|
4891
5030
|
);
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
5031
|
+
chmodSync(path, 384);
|
|
5032
|
+
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5033
|
+
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
5034
|
+
]);
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
// src/commands/environments.ts
|
|
5038
|
+
var CREATE_USAGE2 = "Usage: naro environments create <slug> [--branch <branch>] [--copy-env preview|production|none] [--database schema|empty]";
|
|
5039
|
+
var SET_USAGE2 = "Usage: naro environments set <slug> [--branch <branch> | --no-branch] [--protection on|off]";
|
|
5040
|
+
var DELETE_USAGE = "Usage: naro environments delete <slug> [--yes]";
|
|
5041
|
+
var NO_BRANCH = "\u2014";
|
|
5042
|
+
function parseCopyEnv(value) {
|
|
5043
|
+
if (value === void 0) return void 0;
|
|
5044
|
+
if (value === "preview" || value === "production") return value;
|
|
5045
|
+
if (value === "none") return null;
|
|
5046
|
+
throw new NaroError(
|
|
5047
|
+
"VALIDATION_FAILED",
|
|
5048
|
+
"--copy-env must be preview, production or none."
|
|
5049
|
+
);
|
|
5050
|
+
}
|
|
5051
|
+
function parseDatabase(value) {
|
|
5052
|
+
if (value === void 0) return void 0;
|
|
5053
|
+
if (value === "schema" || value === "empty") return value;
|
|
5054
|
+
throw new NaroError(
|
|
5055
|
+
"VALIDATION_FAILED",
|
|
5056
|
+
"--database must be schema or empty."
|
|
5057
|
+
);
|
|
5058
|
+
}
|
|
5059
|
+
function parseProtection(value) {
|
|
5060
|
+
if (value === void 0) return void 0;
|
|
5061
|
+
if (value === "on") return true;
|
|
5062
|
+
if (value === "off") return false;
|
|
5063
|
+
throw new NaroError("VALIDATION_FAILED", "--protection must be on or off.");
|
|
5064
|
+
}
|
|
5065
|
+
function table2(rows) {
|
|
5066
|
+
const widths = (rows[0] ?? []).map(
|
|
5067
|
+
(_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
|
|
5068
|
+
);
|
|
5069
|
+
return rows.map(
|
|
5070
|
+
(row) => row.map(
|
|
5071
|
+
(cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)
|
|
5072
|
+
).join(" ")
|
|
5073
|
+
);
|
|
5074
|
+
}
|
|
5075
|
+
function environmentRow(environment) {
|
|
5076
|
+
return [
|
|
5077
|
+
environment.slug,
|
|
5078
|
+
environment.url,
|
|
5079
|
+
environment.branch ?? NO_BRANCH,
|
|
5080
|
+
environment.protectionEnabled ? "on" : "off"
|
|
5081
|
+
];
|
|
5082
|
+
}
|
|
5083
|
+
async function runEnvironmentsList(context) {
|
|
5084
|
+
const projectId = await context.resolveProjectId();
|
|
5085
|
+
const client = await context.client();
|
|
5086
|
+
const { environments, limit } = await client.environments.list(projectId);
|
|
5087
|
+
const count2 = String(environments.length);
|
|
5088
|
+
context.output.result({ projectId, environments, limit }, [
|
|
5089
|
+
...environments.length === 0 ? ["No environments. Create one with `naro environments create qa`."] : table2([
|
|
5090
|
+
["slug", "url", "branch", "protection"],
|
|
5091
|
+
...environments.map(environmentRow)
|
|
5092
|
+
]),
|
|
5093
|
+
limit === null ? `${count2} environment${environments.length === 1 ? "" : "s"}` : `${count2} of ${String(limit)} environments`
|
|
5094
|
+
]);
|
|
5095
|
+
}
|
|
5096
|
+
async function runEnvironmentsCreate(context) {
|
|
5097
|
+
const [slug] = context.positionalOrThrow(1, CREATE_USAGE2);
|
|
5098
|
+
const { branch, copyEnv, database } = context.flags;
|
|
5099
|
+
const copyEnvFrom = parseCopyEnv(copyEnv);
|
|
5100
|
+
const seed = parseDatabase(database);
|
|
5101
|
+
const projectId = await context.resolveProjectId();
|
|
5102
|
+
const client = await context.client();
|
|
5103
|
+
const environment = await client.environments.create(projectId, {
|
|
5104
|
+
slug,
|
|
5105
|
+
...branch !== void 0 && { branch },
|
|
5106
|
+
// null is a value here (copy nothing): only undefined leaves the key out.
|
|
5107
|
+
...copyEnvFrom !== void 0 && { copyEnvFrom },
|
|
5108
|
+
...seed !== void 0 && { database: seed }
|
|
5109
|
+
});
|
|
5110
|
+
context.output.result({ projectId, environment }, [
|
|
5111
|
+
`\u2705 Created environment ${environment.slug} \u2192 ${environment.url}`
|
|
5112
|
+
]);
|
|
5113
|
+
}
|
|
5114
|
+
async function runEnvironmentsSet(context) {
|
|
5115
|
+
const [slug] = context.positionalOrThrow(1, SET_USAGE2);
|
|
5116
|
+
const { branch, noBranch, protection } = context.flags;
|
|
5117
|
+
if (branch !== void 0 && noBranch === true)
|
|
5118
|
+
throw new NaroError(
|
|
5119
|
+
"VALIDATION_FAILED",
|
|
5120
|
+
"--branch and --no-branch cannot be used together."
|
|
5121
|
+
);
|
|
5122
|
+
const protectionEnabled = parseProtection(protection);
|
|
5123
|
+
if (branch === void 0 && noBranch !== true && protectionEnabled === void 0)
|
|
5124
|
+
throw new NaroError(
|
|
5125
|
+
"VALIDATION_FAILED",
|
|
5126
|
+
"Nothing to change: pass --branch, --no-branch or --protection."
|
|
5127
|
+
);
|
|
5128
|
+
const patch = {
|
|
5129
|
+
...branch !== void 0 && { branch },
|
|
5130
|
+
...noBranch === true && { branch: null },
|
|
5131
|
+
...protectionEnabled !== void 0 && { protectionEnabled }
|
|
5132
|
+
};
|
|
5133
|
+
const projectId = await context.resolveProjectId();
|
|
5134
|
+
if (protectionEnabled === false)
|
|
5135
|
+
await confirmOrAbort(
|
|
5136
|
+
context,
|
|
5137
|
+
`Turn off protection for ${slug}? Anyone with the link can open it.`
|
|
5138
|
+
);
|
|
5139
|
+
const client = await context.client();
|
|
5140
|
+
const found = await client.environments.get(projectId, slug);
|
|
5141
|
+
const environment = await client.environments.update(
|
|
5142
|
+
projectId,
|
|
5143
|
+
found.id,
|
|
5144
|
+
patch
|
|
5145
|
+
);
|
|
5146
|
+
context.output.result({ projectId, environment }, [
|
|
5147
|
+
`\u2705 Updated environment ${slug}`
|
|
5148
|
+
]);
|
|
5149
|
+
}
|
|
5150
|
+
async function runEnvironmentsDelete(context) {
|
|
5151
|
+
const [slug] = context.positionalOrThrow(1, DELETE_USAGE);
|
|
5152
|
+
const projectId = await context.resolveProjectId();
|
|
5153
|
+
await confirmOrAbort(
|
|
5154
|
+
context,
|
|
5155
|
+
`Delete environment ${slug}? Its worker and protection key are removed.`
|
|
4895
5156
|
);
|
|
5157
|
+
const client = await context.client();
|
|
5158
|
+
const found = await client.environments.get(projectId, slug);
|
|
5159
|
+
await client.environments.delete(projectId, found.id);
|
|
5160
|
+
context.output.result({ projectId, slug, deleted: true }, [
|
|
5161
|
+
`\u2705 Deleted environment ${slug}`
|
|
5162
|
+
]);
|
|
4896
5163
|
}
|
|
4897
5164
|
|
|
4898
5165
|
// src/commands/gen.ts
|
|
4899
5166
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
4900
|
-
import { dirname as dirname2, resolve as
|
|
5167
|
+
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
4901
5168
|
var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
|
|
4902
5169
|
async function runGenTypes(context) {
|
|
4903
5170
|
const { lang } = context.flags;
|
|
@@ -4912,7 +5179,7 @@ async function runGenTypes(context) {
|
|
|
4912
5179
|
read.policies === void 0 ? {} : { policies: read.policies }
|
|
4913
5180
|
);
|
|
4914
5181
|
const undescribed = tables.flatMap(
|
|
4915
|
-
(
|
|
5182
|
+
(table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
|
|
4916
5183
|
);
|
|
4917
5184
|
for (const { name, error } of undescribed)
|
|
4918
5185
|
context.output.warn(
|
|
@@ -4927,7 +5194,7 @@ async function runGenTypes(context) {
|
|
|
4927
5194
|
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
4928
5195
|
};
|
|
4929
5196
|
if (context.flags.output) {
|
|
4930
|
-
const path =
|
|
5197
|
+
const path = resolve5(context.cwd, context.flags.output);
|
|
4931
5198
|
mkdirSync4(dirname2(path), { recursive: true });
|
|
4932
5199
|
writeFileSync5(path, types);
|
|
4933
5200
|
context.output.result(
|
|
@@ -5017,8 +5284,9 @@ function runInsightsDisable(context) {
|
|
|
5017
5284
|
|
|
5018
5285
|
// src/commands/logs.ts
|
|
5019
5286
|
var FOLLOW_INTERVAL_MS = 3e3;
|
|
5287
|
+
var FOLLOW_TIMEOUT_MS = 30 * 6e4;
|
|
5020
5288
|
function sleep(ms) {
|
|
5021
|
-
return new Promise((
|
|
5289
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
5022
5290
|
}
|
|
5023
5291
|
function formatBuildLog(log) {
|
|
5024
5292
|
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
@@ -5028,52 +5296,44 @@ function formatRuntimeLog(entry) {
|
|
|
5028
5296
|
const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
|
|
5029
5297
|
return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
|
|
5030
5298
|
}
|
|
5031
|
-
async function
|
|
5032
|
-
const
|
|
5033
|
-
|
|
5034
|
-
(deployment) => deployment.status === "READY"
|
|
5035
|
-
);
|
|
5036
|
-
const fallback = deployments[0];
|
|
5037
|
-
const chosen = latest ?? fallback;
|
|
5038
|
-
if (!chosen)
|
|
5299
|
+
async function latestDeploymentId(client, projectId) {
|
|
5300
|
+
const [latest] = await client.deployments.list(projectId);
|
|
5301
|
+
if (!latest)
|
|
5039
5302
|
throw new NaroError(
|
|
5040
5303
|
"DEPLOYMENT_NOT_FOUND",
|
|
5041
5304
|
"This project has no deployments yet."
|
|
5042
5305
|
);
|
|
5043
|
-
return
|
|
5306
|
+
return latest.id;
|
|
5044
5307
|
}
|
|
5045
5308
|
async function buildLogs(context, client, deploymentId) {
|
|
5046
5309
|
if (!context.flags.follow) {
|
|
5047
|
-
const
|
|
5310
|
+
const detail2 = await client.deployments.get(deploymentId);
|
|
5311
|
+
const { status } = detail2.deployment;
|
|
5312
|
+
const empty = isTerminalStatus(status) ? `(no build logs for ${deploymentId} \u2014 ${status})` : `(no build logs yet \u2014 ${deploymentId} is ${status}; add --follow to stream)`;
|
|
5048
5313
|
context.output.result(
|
|
5049
|
-
{ deploymentId, type: "build", logs:
|
|
5050
|
-
|
|
5314
|
+
{ deploymentId, type: "build", status, logs: detail2.logs },
|
|
5315
|
+
detail2.logs.length > 0 ? detail2.logs.map(formatBuildLog) : [empty]
|
|
5051
5316
|
);
|
|
5052
5317
|
return;
|
|
5053
5318
|
}
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
);
|
|
5073
|
-
return;
|
|
5074
|
-
}
|
|
5075
|
-
await sleep(FOLLOW_INTERVAL_MS);
|
|
5076
|
-
}
|
|
5319
|
+
const detail = await client.deployments.wait(deploymentId, {
|
|
5320
|
+
intervalMs: FOLLOW_INTERVAL_MS,
|
|
5321
|
+
timeoutMs: FOLLOW_TIMEOUT_MS,
|
|
5322
|
+
onLog: (log) => context.output.log(formatBuildLog(log))
|
|
5323
|
+
});
|
|
5324
|
+
const result = {
|
|
5325
|
+
deploymentId,
|
|
5326
|
+
type: "build",
|
|
5327
|
+
status: detail.deployment.status,
|
|
5328
|
+
logs: detail.logs
|
|
5329
|
+
};
|
|
5330
|
+
if (detail.deployment.status === "FAILED")
|
|
5331
|
+
throw new NaroError(
|
|
5332
|
+
detail.deployment.errorCode ?? "DEPLOY_FAILED",
|
|
5333
|
+
detail.deployment.errorMessage ?? "The deployment failed.",
|
|
5334
|
+
{ details: result }
|
|
5335
|
+
);
|
|
5336
|
+
context.output.result(result, [`\u2014 deployment ${detail.deployment.status} \u2014`]);
|
|
5077
5337
|
}
|
|
5078
5338
|
async function runtimeLogs(context, client, projectId) {
|
|
5079
5339
|
const environment = context.flags.env === "preview" ? "preview" : "production";
|
|
@@ -5117,7 +5377,6 @@ async function runtimeLogs(context, client, projectId) {
|
|
|
5117
5377
|
}
|
|
5118
5378
|
}
|
|
5119
5379
|
async function runLogs(context) {
|
|
5120
|
-
const client = await context.client();
|
|
5121
5380
|
const [deploymentId] = context.positionals;
|
|
5122
5381
|
const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
|
|
5123
5382
|
if (type !== "build" && type !== "runtime")
|
|
@@ -5125,6 +5384,17 @@ async function runLogs(context) {
|
|
|
5125
5384
|
"VALIDATION_FAILED",
|
|
5126
5385
|
"--type must be build or runtime."
|
|
5127
5386
|
);
|
|
5387
|
+
if (type === "runtime" && context.flags.follow && context.flags.json)
|
|
5388
|
+
throw new NaroError(
|
|
5389
|
+
"VALIDATION_FAILED",
|
|
5390
|
+
"--follow streams runtime logs until you stop it, so it has no JSON result. Drop --json to stream, or drop --follow for the current entries as JSON."
|
|
5391
|
+
);
|
|
5392
|
+
if (type === "build" && context.flags.env)
|
|
5393
|
+
throw new NaroError(
|
|
5394
|
+
"VALIDATION_FAILED",
|
|
5395
|
+
"--env is for runtime logs. Build logs belong to one deployment: pass its id (naro deployments lists them)."
|
|
5396
|
+
);
|
|
5397
|
+
const client = await context.client();
|
|
5128
5398
|
if (type === "runtime") {
|
|
5129
5399
|
if (deploymentId)
|
|
5130
5400
|
throw new NaroError(
|
|
@@ -5135,10 +5405,11 @@ async function runLogs(context) {
|
|
|
5135
5405
|
await runtimeLogs(context, client, projectId);
|
|
5136
5406
|
return;
|
|
5137
5407
|
}
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
await context.resolveProjectId()
|
|
5141
|
-
|
|
5408
|
+
let target = deploymentId;
|
|
5409
|
+
if (target === void 0) {
|
|
5410
|
+
target = await latestDeploymentId(client, await context.resolveProjectId());
|
|
5411
|
+
context.output.log(`Build logs of the latest deployment, ${target}`);
|
|
5412
|
+
}
|
|
5142
5413
|
await buildLogs(context, client, target);
|
|
5143
5414
|
}
|
|
5144
5415
|
|
|
@@ -5349,7 +5620,7 @@ function count(n, noun) {
|
|
|
5349
5620
|
}
|
|
5350
5621
|
|
|
5351
5622
|
// src/commands/storage.ts
|
|
5352
|
-
var
|
|
5623
|
+
var CREATE_USAGE3 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
|
|
5353
5624
|
var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]";
|
|
5354
5625
|
var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
|
|
5355
5626
|
var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
|
|
@@ -5489,7 +5760,7 @@ async function runStorageBucketList(context) {
|
|
|
5489
5760
|
);
|
|
5490
5761
|
}
|
|
5491
5762
|
async function runStorageBucketCreate(context) {
|
|
5492
|
-
const name = bucketName(context,
|
|
5763
|
+
const name = bucketName(context, CREATE_USAGE3);
|
|
5493
5764
|
const input = { name, ...bucketFields(context.flags) };
|
|
5494
5765
|
const client = await context.client();
|
|
5495
5766
|
const projectId = await context.resolveProjectId();
|
|
@@ -5519,11 +5790,19 @@ async function runStorageBucketUpdate(context) {
|
|
|
5519
5790
|
const client = await context.client();
|
|
5520
5791
|
const projectId = await context.resolveProjectId();
|
|
5521
5792
|
const bucket = await client.storage.updateBucket(projectId, name, patch);
|
|
5793
|
+
const takesEffect = patch.public === false ? "It takes effect within about 10 seconds; copies Naro's edge already holds can be served for up to 5 minutes." : "It takes effect within about 10 seconds.";
|
|
5522
5794
|
context.output.result({ projectId, bucket }, [
|
|
5523
|
-
`\u2705 Updated bucket ${printable(bucket.name)}.
|
|
5795
|
+
`\u2705 Updated bucket ${printable(bucket.name)}. ${takesEffect}`,
|
|
5524
5796
|
bucketLine(bucket)
|
|
5525
5797
|
]);
|
|
5526
5798
|
}
|
|
5799
|
+
function refuseSearch(context, why) {
|
|
5800
|
+
if (context.flags.search !== void 0)
|
|
5801
|
+
throw new NaroError(
|
|
5802
|
+
"VALIDATION_FAILED",
|
|
5803
|
+
`--search is for naro storage ls: ${why}`
|
|
5804
|
+
);
|
|
5805
|
+
}
|
|
5527
5806
|
async function untilDone(context, bucket, page) {
|
|
5528
5807
|
let deleted = 0;
|
|
5529
5808
|
let idle = 0;
|
|
@@ -5546,6 +5825,10 @@ function objectsSentence(objects2) {
|
|
|
5546
5825
|
}
|
|
5547
5826
|
async function runStorageBucketEmpty(context) {
|
|
5548
5827
|
const name = bucketName(context, EMPTY_USAGE);
|
|
5828
|
+
refuseSearch(
|
|
5829
|
+
context,
|
|
5830
|
+
`bucket empty deletes every object in the bucket. ${EMPTY_USAGE}`
|
|
5831
|
+
);
|
|
5549
5832
|
const client = await context.client();
|
|
5550
5833
|
const projectId = await context.resolveProjectId();
|
|
5551
5834
|
const bucket = await client.storage.getBucket(projectId, name);
|
|
@@ -5565,6 +5848,10 @@ async function runStorageBucketEmpty(context) {
|
|
|
5565
5848
|
async function runStorageBucketRemove(context) {
|
|
5566
5849
|
const name = bucketName(context, REMOVE_USAGE2);
|
|
5567
5850
|
const empty = context.flags.empty === true;
|
|
5851
|
+
refuseSearch(
|
|
5852
|
+
context,
|
|
5853
|
+
empty ? `bucket remove --empty deletes every object in the bucket before it removes the bucket. ${REMOVE_USAGE2}` : `bucket remove removes the whole bucket. ${REMOVE_USAGE2}`
|
|
5854
|
+
);
|
|
5568
5855
|
const client = await context.client();
|
|
5569
5856
|
const projectId = await context.resolveProjectId();
|
|
5570
5857
|
let question = `Remove bucket ${name}?`;
|
|
@@ -5590,7 +5877,7 @@ async function runStorageBucketRemove(context) {
|
|
|
5590
5877
|
import { randomUUID } from "crypto";
|
|
5591
5878
|
import { createWriteStream as createWriteStream2, openAsBlob, rmSync as rmSync2 } from "fs";
|
|
5592
5879
|
import { rename, rm, stat } from "fs/promises";
|
|
5593
|
-
import { basename as basename2, dirname as dirname3, extname, join as
|
|
5880
|
+
import { basename as basename2, dirname as dirname3, extname, join as join5, resolve as resolve6 } from "path";
|
|
5594
5881
|
import { Readable } from "stream";
|
|
5595
5882
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
5596
5883
|
|
|
@@ -5685,6 +5972,10 @@ function errorFromBody(status, body) {
|
|
|
5685
5972
|
if (status >= 200 && status < 300) return null;
|
|
5686
5973
|
return invalidResponse(status);
|
|
5687
5974
|
}
|
|
5975
|
+
var AUTH_PREFIX = "/auth/";
|
|
5976
|
+
function storeFor(config, path) {
|
|
5977
|
+
return path.startsWith(AUTH_PREFIX) ? config.tokens : config.accessJwt ?? null;
|
|
5978
|
+
}
|
|
5688
5979
|
async function receive(config, options, init) {
|
|
5689
5980
|
const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
|
|
5690
5981
|
if (controller) init.signal = controller.signal;
|
|
@@ -5707,10 +5998,10 @@ async function receive(config, options, init) {
|
|
|
5707
5998
|
const { timeoutMs } = options;
|
|
5708
5999
|
if (timeoutMs === void 0) return request;
|
|
5709
6000
|
let timer;
|
|
5710
|
-
const expired = new Promise((
|
|
6001
|
+
const expired = new Promise((resolve7) => {
|
|
5711
6002
|
timer = setTimeout(() => {
|
|
5712
6003
|
controller?.abort();
|
|
5713
|
-
|
|
6004
|
+
resolve7({ error: requestTimeout(timeoutMs) });
|
|
5714
6005
|
}, timeoutMs);
|
|
5715
6006
|
});
|
|
5716
6007
|
try {
|
|
@@ -5728,7 +6019,8 @@ async function call(config, options) {
|
|
|
5728
6019
|
return { error: invalidQuery("", "is not JSON"), answer: null };
|
|
5729
6020
|
}
|
|
5730
6021
|
}
|
|
5731
|
-
const
|
|
6022
|
+
const store = storeFor(config, options.path);
|
|
6023
|
+
const token = options.withToken && store ? await store.get() : null;
|
|
5732
6024
|
const headers = {
|
|
5733
6025
|
...options.headers,
|
|
5734
6026
|
apikey: config.apiKey
|
|
@@ -5762,17 +6054,18 @@ async function call(config, options) {
|
|
|
5762
6054
|
const error = errorFromBody(status, body);
|
|
5763
6055
|
if (error === null) return { error: null, answer };
|
|
5764
6056
|
return {
|
|
5765
|
-
error: await settled(
|
|
6057
|
+
error: await settled(store, error, responseHeaders, token),
|
|
5766
6058
|
answer
|
|
5767
6059
|
};
|
|
5768
6060
|
}
|
|
5769
|
-
async function settled(
|
|
5770
|
-
if (error.code === "INVALID_SESSION" && token !== null)
|
|
5771
|
-
await
|
|
6061
|
+
async function settled(store, error, headers, token) {
|
|
6062
|
+
if (error.code === "INVALID_SESSION" && token !== null && store)
|
|
6063
|
+
await store.clearIf(token);
|
|
5772
6064
|
return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
|
|
5773
6065
|
}
|
|
5774
6066
|
async function send(config, options) {
|
|
5775
|
-
const
|
|
6067
|
+
const store = storeFor(config, options.path);
|
|
6068
|
+
const token = options.withToken && store ? await store.get() : null;
|
|
5776
6069
|
const headers = {
|
|
5777
6070
|
...options.headers,
|
|
5778
6071
|
apikey: config.apiKey
|
|
@@ -5823,7 +6116,7 @@ async function send(config, options) {
|
|
|
5823
6116
|
}
|
|
5824
6117
|
const error = errorFromBody(status, body) ?? invalidResponse(status);
|
|
5825
6118
|
return {
|
|
5826
|
-
error: await settled(
|
|
6119
|
+
error: await settled(store, error, response.headers, token),
|
|
5827
6120
|
status
|
|
5828
6121
|
};
|
|
5829
6122
|
}
|
|
@@ -6008,6 +6301,7 @@ async function createPkcePair() {
|
|
|
6008
6301
|
return { verifier, challenge: base64url(new Uint8Array(digest)) };
|
|
6009
6302
|
}
|
|
6010
6303
|
var TOKEN_HEADER = "set-auth-token";
|
|
6304
|
+
var JWT_HEADER = "set-auth-jwt";
|
|
6011
6305
|
var FRAGMENT_PREFIX = "naro_";
|
|
6012
6306
|
var OAUTH_PROVIDERS = ["google", "github"];
|
|
6013
6307
|
var FLOW_TYPES = [
|
|
@@ -6026,6 +6320,9 @@ function nonEmpty(value) {
|
|
|
6026
6320
|
function tokenFrom(answer) {
|
|
6027
6321
|
return nonEmpty(answer.headers.get(TOKEN_HEADER));
|
|
6028
6322
|
}
|
|
6323
|
+
function jwtFrom(answer) {
|
|
6324
|
+
return nonEmpty(answer.headers.get(JWT_HEADER));
|
|
6325
|
+
}
|
|
6029
6326
|
function userFrom(body) {
|
|
6030
6327
|
return isRecord(body) && isRecord(body.user) ? body.user : null;
|
|
6031
6328
|
}
|
|
@@ -6188,7 +6485,7 @@ var AuthClient = class {
|
|
|
6188
6485
|
const user = userFrom(body);
|
|
6189
6486
|
if (token === null || user === null || !isRecord(body) || !isRecord(body.session))
|
|
6190
6487
|
return { data: signedOut, error: invalidResponse(result.answer.status) };
|
|
6191
|
-
await this.#
|
|
6488
|
+
await this.#storeSession(token, jwtFrom(result.answer));
|
|
6192
6489
|
await this.#flow.recovery.forget();
|
|
6193
6490
|
return { data: { session: body.session, user }, error: null };
|
|
6194
6491
|
}
|
|
@@ -6224,6 +6521,7 @@ var AuthClient = class {
|
|
|
6224
6521
|
}
|
|
6225
6522
|
if (type === "recovery" && fragment.naro_token) {
|
|
6226
6523
|
await this.#http.tokens.clear();
|
|
6524
|
+
await this.#http.accessJwt?.clear();
|
|
6227
6525
|
await this.#flow.recovery.keep(fragment.naro_token);
|
|
6228
6526
|
return { data: none, error: null };
|
|
6229
6527
|
}
|
|
@@ -6314,6 +6612,7 @@ var AuthClient = class {
|
|
|
6314
6612
|
withToken: true
|
|
6315
6613
|
});
|
|
6316
6614
|
await tokens.clearIf(result.answer?.token ?? token);
|
|
6615
|
+
await this.#http.accessJwt?.clear();
|
|
6317
6616
|
return { data: null, error: result.error };
|
|
6318
6617
|
}
|
|
6319
6618
|
/**
|
|
@@ -6335,7 +6634,10 @@ var AuthClient = class {
|
|
|
6335
6634
|
if (result.error !== null) return { data: signedOut, error: result.error };
|
|
6336
6635
|
const { body, token } = result.answer;
|
|
6337
6636
|
if (body === null) {
|
|
6338
|
-
if (token !== null)
|
|
6637
|
+
if (token !== null) {
|
|
6638
|
+
await tokens.clearIf(token);
|
|
6639
|
+
await this.#http.accessJwt?.clear();
|
|
6640
|
+
}
|
|
6339
6641
|
return { data: signedOut, error: invalidSession() };
|
|
6340
6642
|
}
|
|
6341
6643
|
const user = userFrom(body);
|
|
@@ -6366,6 +6668,15 @@ var AuthClient = class {
|
|
|
6366
6668
|
async #pendingUrl() {
|
|
6367
6669
|
if (this.#fromUrl) await this.#fromUrl;
|
|
6368
6670
|
}
|
|
6671
|
+
/**
|
|
6672
|
+
* Keeps a new session: its token, and the access JWT beside it. An answer
|
|
6673
|
+
* without a JWT drops the old one — it belonged to the previous session.
|
|
6674
|
+
*/
|
|
6675
|
+
async #storeSession(token, jwt) {
|
|
6676
|
+
await this.#http.tokens.set(token);
|
|
6677
|
+
if (jwt !== null) await this.#http.accessJwt?.set(jwt);
|
|
6678
|
+
else await this.#http.accessJwt?.clear();
|
|
6679
|
+
}
|
|
6369
6680
|
async #signIn(path, body, needsToken) {
|
|
6370
6681
|
await this.#flow.recovery.forget();
|
|
6371
6682
|
const result = await call(this.#http, {
|
|
@@ -6385,7 +6696,7 @@ var AuthClient = class {
|
|
|
6385
6696
|
data: { user: null },
|
|
6386
6697
|
error: invalidResponse(result.answer.status)
|
|
6387
6698
|
};
|
|
6388
|
-
if (token !== null) await this.#
|
|
6699
|
+
if (token !== null) await this.#storeSession(token, jwtFrom(result.answer));
|
|
6389
6700
|
return { data: { user }, error: null };
|
|
6390
6701
|
}
|
|
6391
6702
|
};
|
|
@@ -6533,6 +6844,9 @@ function defaultStorage() {
|
|
|
6533
6844
|
function storageKey(baseUrl) {
|
|
6534
6845
|
return `naro-auth-token:${baseUrl}`;
|
|
6535
6846
|
}
|
|
6847
|
+
function accessJwtKey(baseUrl) {
|
|
6848
|
+
return `naro-auth-jwt:${baseUrl}`;
|
|
6849
|
+
}
|
|
6536
6850
|
function verifierKey(baseUrl) {
|
|
6537
6851
|
return `naro-auth-code-verifiers:${baseUrl}`;
|
|
6538
6852
|
}
|
|
@@ -7063,8 +7377,8 @@ function patternOf(pattern) {
|
|
|
7063
7377
|
}
|
|
7064
7378
|
var MAX_LIMIT = 1e3;
|
|
7065
7379
|
var CAP = `one select reads at most ${String(MAX_LIMIT)} rows, by limit() or range()`;
|
|
7066
|
-
function initialState(
|
|
7067
|
-
return { table:
|
|
7380
|
+
function initialState(table3) {
|
|
7381
|
+
return { table: table3, problem: null, filters: [], columns: "*", order: [] };
|
|
7068
7382
|
}
|
|
7069
7383
|
function copyRow(row) {
|
|
7070
7384
|
return { ...row };
|
|
@@ -7297,11 +7611,11 @@ var SelectBuilder = class extends FilterBuilder {
|
|
|
7297
7611
|
return this.next({ single: "maybe" });
|
|
7298
7612
|
}
|
|
7299
7613
|
build() {
|
|
7300
|
-
const { table:
|
|
7614
|
+
const { table: table3, columns, order, limit, offset, single } = this.state;
|
|
7301
7615
|
return {
|
|
7302
7616
|
version: 1,
|
|
7303
7617
|
action: "select",
|
|
7304
|
-
table:
|
|
7618
|
+
table: table3,
|
|
7305
7619
|
columns: columns === "*" ? "*" : [...columns],
|
|
7306
7620
|
...this.filtersPart(),
|
|
7307
7621
|
...order.length === 0 ? {} : { order: order.map((term) => ({ ...term })) },
|
|
@@ -7317,11 +7631,11 @@ var InsertBuilder = class extends Builder {
|
|
|
7317
7631
|
return this.withReturning(columns);
|
|
7318
7632
|
}
|
|
7319
7633
|
build() {
|
|
7320
|
-
const { table:
|
|
7634
|
+
const { table: table3, rows = [], returning } = this.state;
|
|
7321
7635
|
return {
|
|
7322
7636
|
version: 1,
|
|
7323
7637
|
action: "insert",
|
|
7324
|
-
table:
|
|
7638
|
+
table: table3,
|
|
7325
7639
|
rows: rows.map(copyRow),
|
|
7326
7640
|
...returning === void 0 ? {} : { returning }
|
|
7327
7641
|
};
|
|
@@ -7338,7 +7652,7 @@ var MutationBuilder = class extends FilterBuilder {
|
|
|
7338
7652
|
}
|
|
7339
7653
|
build() {
|
|
7340
7654
|
const {
|
|
7341
|
-
table:
|
|
7655
|
+
table: table3,
|
|
7342
7656
|
action,
|
|
7343
7657
|
values = {},
|
|
7344
7658
|
returning,
|
|
@@ -7349,10 +7663,10 @@ var MutationBuilder = class extends FilterBuilder {
|
|
|
7349
7663
|
...returning === void 0 ? {} : { returning },
|
|
7350
7664
|
...allowUnfiltered === void 0 ? {} : { allowUnfiltered }
|
|
7351
7665
|
};
|
|
7352
|
-
return action === "delete" ? { version: 1, action: "delete", table:
|
|
7666
|
+
return action === "delete" ? { version: 1, action: "delete", table: table3, ...tail } : {
|
|
7353
7667
|
version: 1,
|
|
7354
7668
|
action: "update",
|
|
7355
|
-
table:
|
|
7669
|
+
table: table3,
|
|
7356
7670
|
values: copyRow(values),
|
|
7357
7671
|
...tail
|
|
7358
7672
|
};
|
|
@@ -7361,9 +7675,9 @@ var MutationBuilder = class extends FilterBuilder {
|
|
|
7361
7675
|
var TableBuilder = class {
|
|
7362
7676
|
#run;
|
|
7363
7677
|
#table;
|
|
7364
|
-
constructor(run,
|
|
7678
|
+
constructor(run, table3) {
|
|
7365
7679
|
this.#run = run;
|
|
7366
|
-
this.#table =
|
|
7680
|
+
this.#table = table3;
|
|
7367
7681
|
}
|
|
7368
7682
|
/** `"*"` (the default) or `"id, title"`. */
|
|
7369
7683
|
select(columns) {
|
|
@@ -7739,6 +8053,7 @@ function withoutToken(error, token) {
|
|
|
7739
8053
|
var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
|
|
7740
8054
|
var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
|
|
7741
8055
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
8056
|
+
var isSignedEntry = (value) => isRecord(value) && typeof value.path === "string" && (value.signedUrl === null || typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX)) && (value.error === null || typeof value.error === "string");
|
|
7742
8057
|
var isSignedUpload = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_UPLOAD_PREFIX) && typeof value.token === "string" && value.token !== "" && typeof value.path === "string";
|
|
7743
8058
|
var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
|
|
7744
8059
|
function listOf(guard) {
|
|
@@ -7868,7 +8183,11 @@ var StorageFileApi = class {
|
|
|
7868
8183
|
}
|
|
7869
8184
|
/**
|
|
7870
8185
|
* What is directly below `prefix` (the whole bucket by default): folders
|
|
7871
|
-
* first, then files.
|
|
8186
|
+
* first, then files. `search` keeps the names below the prefix that start
|
|
8187
|
+
* with it, ASCII case ignored (Storage 3-1g); a backend whose Storage was
|
|
8188
|
+
* turned on before it answers `INVALID_REQUEST` (`details.path`
|
|
8189
|
+
* `search`) — update it with `naro backend enable`. The options go to the
|
|
8190
|
+
* worker as they are given.
|
|
7872
8191
|
*/
|
|
7873
8192
|
async list(prefix, options = {}) {
|
|
7874
8193
|
const target = this.#bucketPath("list/");
|
|
@@ -7916,6 +8235,44 @@ var StorageFileApi = class {
|
|
|
7916
8235
|
error: null
|
|
7917
8236
|
};
|
|
7918
8237
|
}
|
|
8238
|
+
/**
|
|
8239
|
+
* Links to several objects at once (Storage 3-1g): up to 100 paths, each
|
|
8240
|
+
* for `expiresIn` seconds (1–604,800), with the same `download`. One entry
|
|
8241
|
+
* per path, in the order given — a path given twice is answered twice.
|
|
8242
|
+
* A path that is not there, or that the caller cannot read, has
|
|
8243
|
+
* `signedUrl: null` and `error` "Object <path> was not found in bucket
|
|
8244
|
+
* <bucket>." — the same words for both. The paths go in the body, so the
|
|
8245
|
+
* backend checks them: a bad one fails the call with `INVALID_REQUEST`
|
|
8246
|
+
* (`details.path` `paths[<i>]`). A backend whose Storage was turned on
|
|
8247
|
+
* before this method came answers `NOT_FOUND`: update it with
|
|
8248
|
+
* `naro backend enable`.
|
|
8249
|
+
*/
|
|
8250
|
+
async createSignedUrls(paths, expiresIn, options = {}) {
|
|
8251
|
+
const target = this.#bucketPath("sign/");
|
|
8252
|
+
if (!target.ok) return fail(target.error);
|
|
8253
|
+
const { download: download2 } = options ?? {};
|
|
8254
|
+
const signed = await this.#json(
|
|
8255
|
+
{
|
|
8256
|
+
method: "POST",
|
|
8257
|
+
path: target.path,
|
|
8258
|
+
body: {
|
|
8259
|
+
expiresIn,
|
|
8260
|
+
paths,
|
|
8261
|
+
...download2 === void 0 || download2 === false ? {} : { download: download2 }
|
|
8262
|
+
}
|
|
8263
|
+
},
|
|
8264
|
+
listOf(isSignedEntry)
|
|
8265
|
+
);
|
|
8266
|
+
if (signed.error !== null) return signed;
|
|
8267
|
+
return {
|
|
8268
|
+
data: signed.data.map((entry) => ({
|
|
8269
|
+
path: entry.path,
|
|
8270
|
+
signedUrl: entry.signedUrl === null ? null : `${this.#http.baseUrl}${entry.signedUrl}`,
|
|
8271
|
+
error: entry.error
|
|
8272
|
+
})),
|
|
8273
|
+
error: null
|
|
8274
|
+
};
|
|
8275
|
+
}
|
|
7919
8276
|
/**
|
|
7920
8277
|
* A token that lets whoever holds it upload `path` for two hours (Storage
|
|
7921
8278
|
* 3-1e-2), as supabase-js's: made where the key is — a server with the
|
|
@@ -8118,7 +8475,8 @@ var NaroClient2 = class {
|
|
|
8118
8475
|
baseUrl,
|
|
8119
8476
|
apiKey,
|
|
8120
8477
|
fetch: options.fetch,
|
|
8121
|
-
tokens: new TokenStore(storage, storageKey(baseUrl))
|
|
8478
|
+
tokens: new TokenStore(storage, storageKey(baseUrl)),
|
|
8479
|
+
accessJwt: new TokenStore(storage, accessJwtKey(baseUrl))
|
|
8122
8480
|
};
|
|
8123
8481
|
this.auth = new AuthClient(http, {
|
|
8124
8482
|
verifiers: new FlowVerifiers(
|
|
@@ -8135,8 +8493,8 @@ var NaroClient2 = class {
|
|
|
8135
8493
|
this.storage = new StorageClient(http, this.auth);
|
|
8136
8494
|
}
|
|
8137
8495
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
|
8138
|
-
from(
|
|
8139
|
-
return new TableBuilder(this.#run,
|
|
8496
|
+
from(table3) {
|
|
8497
|
+
return new TableBuilder(this.#run, table3);
|
|
8140
8498
|
}
|
|
8141
8499
|
};
|
|
8142
8500
|
function createClient(options) {
|
|
@@ -8147,8 +8505,17 @@ function createClient(options) {
|
|
|
8147
8505
|
var NARO_SCHEME = "naro://";
|
|
8148
8506
|
var CP_USAGE = "Usage: naro storage cp <from> <to> (one side naro://<bucket>/<path>, the other a local file)";
|
|
8149
8507
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
8150
|
-
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]";
|
|
8508
|
+
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
|
|
8151
8509
|
var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
|
|
8510
|
+
var STORAGE_OUTDATED_MESSAGE = "The project backend is older than this Storage command. Run `naro backend enable` to update it, then try again.";
|
|
8511
|
+
var OLD_SEARCH_REASON = "search is not supported yet";
|
|
8512
|
+
function isOldSearchRefusal(error) {
|
|
8513
|
+
if (error === null || error.code !== "INVALID_REQUEST") return false;
|
|
8514
|
+
const { details } = error;
|
|
8515
|
+
if (typeof details !== "object" || details === null) return false;
|
|
8516
|
+
const { path, reason } = details;
|
|
8517
|
+
return path === "search" && reason === OLD_SEARCH_REASON;
|
|
8518
|
+
}
|
|
8152
8519
|
var FOUR_GIB = 4294967296;
|
|
8153
8520
|
var PAGE = 1e3;
|
|
8154
8521
|
var MAX_OFFSET = 1e5;
|
|
@@ -8268,7 +8635,7 @@ function fileEntry(path, object) {
|
|
|
8268
8635
|
updatedAt: object.updated_at
|
|
8269
8636
|
};
|
|
8270
8637
|
}
|
|
8271
|
-
async function listAll(storage, bucket, prefix, recursive) {
|
|
8638
|
+
async function listAll(storage, bucket, prefix, recursive, search) {
|
|
8272
8639
|
const entries = [];
|
|
8273
8640
|
let truncated = false;
|
|
8274
8641
|
const folders = [prefix];
|
|
@@ -8278,9 +8645,14 @@ async function listAll(storage, bucket, prefix, recursive) {
|
|
|
8278
8645
|
truncated = true;
|
|
8279
8646
|
break;
|
|
8280
8647
|
}
|
|
8281
|
-
const
|
|
8282
|
-
|
|
8283
|
-
|
|
8648
|
+
const listed = await storage.from(bucket).list(folder, {
|
|
8649
|
+
limit: PAGE,
|
|
8650
|
+
offset,
|
|
8651
|
+
...search === "" ? {} : { search }
|
|
8652
|
+
});
|
|
8653
|
+
if (isOldSearchRefusal(listed.error))
|
|
8654
|
+
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
8655
|
+
const page = unwrap(listed);
|
|
8284
8656
|
for (const entry of page) {
|
|
8285
8657
|
const path = folder === "" ? entry.name : `${folder}/${entry.name}`;
|
|
8286
8658
|
if (isFolder(entry)) {
|
|
@@ -8310,7 +8682,13 @@ function entryLine(bucket, entry) {
|
|
|
8310
8682
|
async function storageLs(context) {
|
|
8311
8683
|
const [target, ...extra] = context.positionals;
|
|
8312
8684
|
if (extra.length > 0) throw new NaroError("VALIDATION_FAILED", LS_USAGE);
|
|
8685
|
+
const search = context.flags.search ?? "";
|
|
8313
8686
|
if (target === void 0) {
|
|
8687
|
+
if (search !== "")
|
|
8688
|
+
throw new NaroError(
|
|
8689
|
+
"VALIDATION_FAILED",
|
|
8690
|
+
`--search needs a naro:// path. ${LS_USAGE}`
|
|
8691
|
+
);
|
|
8314
8692
|
const client = await context.client();
|
|
8315
8693
|
const projectId2 = await context.resolveProjectId();
|
|
8316
8694
|
const buckets = await client.storage.listBuckets(projectId2);
|
|
@@ -8332,17 +8710,28 @@ async function storageLs(context) {
|
|
|
8332
8710
|
storage,
|
|
8333
8711
|
url.bucket,
|
|
8334
8712
|
prefix,
|
|
8335
|
-
context.flags.recursive === true
|
|
8713
|
+
context.flags.recursive === true,
|
|
8714
|
+
search
|
|
8336
8715
|
);
|
|
8337
8716
|
if (truncated) context.output.warn(TRUNCATED_WARNING);
|
|
8338
|
-
if (entries.length === 0 && prefix !== "" && !url.path.endsWith("/")) {
|
|
8717
|
+
if (search === "" && entries.length === 0 && prefix !== "" && !url.path.endsWith("/")) {
|
|
8339
8718
|
const found = await storage.from(url.bucket).info(prefix);
|
|
8340
8719
|
if (found.error === null) entries.push(fileEntry(prefix, found.data));
|
|
8341
8720
|
else if (found.error.code !== "OBJECT_NOT_FOUND") unwrap(found);
|
|
8342
8721
|
}
|
|
8722
|
+
const where = `${NARO_SCHEME}${url.bucket}/${printable(prefix)}`;
|
|
8343
8723
|
context.output.result(
|
|
8344
|
-
{
|
|
8345
|
-
|
|
8724
|
+
{
|
|
8725
|
+
projectId,
|
|
8726
|
+
bucket: url.bucket,
|
|
8727
|
+
prefix,
|
|
8728
|
+
...search === "" ? {} : { search },
|
|
8729
|
+
entries,
|
|
8730
|
+
truncated
|
|
8731
|
+
},
|
|
8732
|
+
entries.length === 0 ? [
|
|
8733
|
+
search === "" ? `Nothing under ${where}.` : `No name under ${where} starts with ${printable(search)}.`
|
|
8734
|
+
] : entries.map((entry) => entryLine(url.bucket, entry))
|
|
8346
8735
|
);
|
|
8347
8736
|
}
|
|
8348
8737
|
var INTERRUPTS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
@@ -8392,7 +8781,7 @@ function stopOnInterrupt(onFirst) {
|
|
|
8392
8781
|
};
|
|
8393
8782
|
}
|
|
8394
8783
|
async function writeAtomically(bytes, destination) {
|
|
8395
|
-
const partial =
|
|
8784
|
+
const partial = join5(dirname3(destination), `.${randomUUID()}.naro-partial`);
|
|
8396
8785
|
const stopListening = removeOnInterrupt(partial);
|
|
8397
8786
|
try {
|
|
8398
8787
|
await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
|
|
@@ -8433,7 +8822,7 @@ function progressReporter(output, total) {
|
|
|
8433
8822
|
};
|
|
8434
8823
|
}
|
|
8435
8824
|
async function upload(context, local, target) {
|
|
8436
|
-
const file =
|
|
8825
|
+
const file = resolve6(context.cwd, local);
|
|
8437
8826
|
const info = await stat(file).catch(() => null);
|
|
8438
8827
|
if (info === null)
|
|
8439
8828
|
throw new NaroError(
|
|
@@ -8567,9 +8956,9 @@ async function download(context, source, local) {
|
|
|
8567
8956
|
"VALIDATION_FAILED",
|
|
8568
8957
|
"naro storage cp downloads one object: name it, naro://<bucket>/<path>."
|
|
8569
8958
|
);
|
|
8570
|
-
const resolved =
|
|
8959
|
+
const resolved = resolve6(context.cwd, local);
|
|
8571
8960
|
const isDirectory = local.endsWith("/") || (await stat(resolved).catch(() => null))?.isDirectory() === true;
|
|
8572
|
-
const destination = isDirectory ?
|
|
8961
|
+
const destination = isDirectory ? join5(resolved, savedAs(source.path)) : resolved;
|
|
8573
8962
|
const folder = await stat(dirname3(destination)).catch(() => null);
|
|
8574
8963
|
if (folder === null || !folder.isDirectory())
|
|
8575
8964
|
throw new NaroError(
|
|
@@ -8615,7 +9004,7 @@ async function namesUnder(storage, bucket, prefixes) {
|
|
|
8615
9004
|
const prefix = written.replace(/\/+$/, "");
|
|
8616
9005
|
if (prefix !== "" && !written.endsWith("/") && unwrap(await storage.from(bucket).exists(prefix)))
|
|
8617
9006
|
names.add(prefix);
|
|
8618
|
-
const found = await listAll(storage, bucket, prefix, true);
|
|
9007
|
+
const found = await listAll(storage, bucket, prefix, true, "");
|
|
8619
9008
|
for (const entry of found.entries) names.add(entry.path);
|
|
8620
9009
|
truncated ||= found.truncated;
|
|
8621
9010
|
}
|
|
@@ -8624,6 +9013,11 @@ async function namesUnder(storage, bucket, prefixes) {
|
|
|
8624
9013
|
async function storageRm(context) {
|
|
8625
9014
|
if (context.positionals.length === 0)
|
|
8626
9015
|
throw new NaroError("VALIDATION_FAILED", RM_USAGE);
|
|
9016
|
+
if (context.flags.search !== void 0)
|
|
9017
|
+
throw new NaroError(
|
|
9018
|
+
"VALIDATION_FAILED",
|
|
9019
|
+
`--search is for naro storage ls: rm removes everything under the path it is given. ${RM_USAGE}`
|
|
9020
|
+
);
|
|
8627
9021
|
const recursive = context.flags.recursive === true;
|
|
8628
9022
|
const byBucket = /* @__PURE__ */ new Map();
|
|
8629
9023
|
for (const arg of context.positionals) {
|
|
@@ -8716,6 +9110,101 @@ function runStorageRm(context) {
|
|
|
8716
9110
|
return explainingKey(context, storageRm);
|
|
8717
9111
|
}
|
|
8718
9112
|
|
|
9113
|
+
// src/commands/usage-format.ts
|
|
9114
|
+
var PLAN_NAMES = {
|
|
9115
|
+
hobby: "Hobby",
|
|
9116
|
+
pro: "Pro",
|
|
9117
|
+
enterprise: "Enterprise"
|
|
9118
|
+
};
|
|
9119
|
+
var METRIC_LABELS = {
|
|
9120
|
+
requests: "Requests",
|
|
9121
|
+
builds: "Builds",
|
|
9122
|
+
domains: "Custom domains"
|
|
9123
|
+
};
|
|
9124
|
+
var BUILDS_NOTE = "Builds are builds that started; the Hobby limit counts deployments created.";
|
|
9125
|
+
var LAG_NOTE = "Requests are counted every 15 minutes. Blocked requests (firewall, pauses) do not count toward the limit.";
|
|
9126
|
+
function planName(plan) {
|
|
9127
|
+
return PLAN_NAMES[plan] ?? printable(plan);
|
|
9128
|
+
}
|
|
9129
|
+
function statusLine2(status, limit) {
|
|
9130
|
+
const amount = typeof limit === "number" ? `${formatCount(limit)} requests` : "requests";
|
|
9131
|
+
switch (status) {
|
|
9132
|
+
case "paused":
|
|
9133
|
+
return `\u2717 This team used the Hobby plan's ${amount} this month. Its sites are paused until the 1st of next month (UTC).`;
|
|
9134
|
+
case "over":
|
|
9135
|
+
return `\u26A0 This team used more than the Hobby plan's ${amount} this month. Sites are not paused yet.`;
|
|
9136
|
+
case "near":
|
|
9137
|
+
return `\u26A0 This team has used 80% of the Hobby plan's ${amount} this month.`;
|
|
9138
|
+
}
|
|
9139
|
+
}
|
|
9140
|
+
function meterRow(meter, limitsKnown) {
|
|
9141
|
+
const label3 = METRIC_LABELS[meter.metric];
|
|
9142
|
+
const used = formatCount(meter.used);
|
|
9143
|
+
if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
|
|
9144
|
+
if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
|
|
9145
|
+
if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
|
|
9146
|
+
return [label3, used, formatCount(meter.limit), usedPercent(meter)];
|
|
9147
|
+
}
|
|
9148
|
+
function usedPercent(meter) {
|
|
9149
|
+
const percent2 = meter.percent ?? 0;
|
|
9150
|
+
return percent2 === 0 && meter.used > 0 ? "<1%" : `${String(percent2)}%`;
|
|
9151
|
+
}
|
|
9152
|
+
function projectName(project) {
|
|
9153
|
+
if (project.deleted) return `Deleted project ${printable(project.projectId)}`;
|
|
9154
|
+
if (project.moved)
|
|
9155
|
+
return `Moved to another team ${printable(project.projectId)}`;
|
|
9156
|
+
return printable(project.name ?? project.projectId);
|
|
9157
|
+
}
|
|
9158
|
+
function usageLines2(report, summary) {
|
|
9159
|
+
const lines = [
|
|
9160
|
+
`${printable(report.team.slug)} \xB7 ${planName(report.team.plan)} plan \xB7 ${printable(report.month)} (UTC)`
|
|
9161
|
+
];
|
|
9162
|
+
if (summary.status)
|
|
9163
|
+
lines.push(statusLine2(summary.status, report.limits?.requests));
|
|
9164
|
+
lines.push(
|
|
9165
|
+
"",
|
|
9166
|
+
...table([
|
|
9167
|
+
["Metric", "Used", "Included", "Used %"],
|
|
9168
|
+
...summary.meters.map((meter) => meterRow(meter, summary.limitsKnown))
|
|
9169
|
+
]),
|
|
9170
|
+
""
|
|
9171
|
+
);
|
|
9172
|
+
if (report.team.plan === "hobby") lines.push(BUILDS_NOTE);
|
|
9173
|
+
lines.push(LAG_NOTE, "");
|
|
9174
|
+
if (report.projects.length === 0) {
|
|
9175
|
+
lines.push("Nothing was recorded for this month.");
|
|
9176
|
+
return lines;
|
|
9177
|
+
}
|
|
9178
|
+
lines.push(
|
|
9179
|
+
"By project",
|
|
9180
|
+
...table([
|
|
9181
|
+
["Requests", "Blocked", "Builds", "Domains (peak)", "Project"],
|
|
9182
|
+
...report.projects.map((project) => [
|
|
9183
|
+
formatCount(project.requests),
|
|
9184
|
+
formatCount(project.requestsBlocked),
|
|
9185
|
+
formatCount(project.builds),
|
|
9186
|
+
formatCount(project.domains),
|
|
9187
|
+
projectName(project)
|
|
9188
|
+
])
|
|
9189
|
+
])
|
|
9190
|
+
);
|
|
9191
|
+
return lines;
|
|
9192
|
+
}
|
|
9193
|
+
|
|
9194
|
+
// src/commands/usage.ts
|
|
9195
|
+
async function runUsage(context) {
|
|
9196
|
+
const client = await context.client();
|
|
9197
|
+
const teams = await client.teams.list();
|
|
9198
|
+
const team = await resolveTeam(context, teams, { intent: "read" });
|
|
9199
|
+
const month = context.flags.month;
|
|
9200
|
+
const report = await client.teams.usage(
|
|
9201
|
+
team.slug,
|
|
9202
|
+
month !== void 0 ? { month } : {}
|
|
9203
|
+
);
|
|
9204
|
+
const summary = summarizeTeamUsage(report, usageMonthOf(/* @__PURE__ */ new Date()));
|
|
9205
|
+
context.output.result({ ...report, summary }, usageLines2(report, summary));
|
|
9206
|
+
}
|
|
9207
|
+
|
|
8719
9208
|
// src/router.ts
|
|
8720
9209
|
var HELP = `naro \u2014 deploy Next.js apps to Naro (https://naro.sh)
|
|
8721
9210
|
|
|
@@ -8727,9 +9216,12 @@ Usage:
|
|
|
8727
9216
|
naro unlink Remove the link (.naro/)
|
|
8728
9217
|
naro projects [--team <slug>] List projects
|
|
8729
9218
|
naro project inspect Show the linked project
|
|
9219
|
+
naro usage [--month YYYY-MM] [--team <slug>]
|
|
9220
|
+
Team usage: requests, builds and custom domains against the plan, then by project
|
|
8730
9221
|
|
|
8731
9222
|
naro deploy [--prod] Build locally and deploy (preview by default)
|
|
8732
9223
|
[--root <dir>] [--name <name>] --root points at the app in a monorepo
|
|
9224
|
+
[--env <name>] Deploy to a named environment instead of preview (not with --prod)
|
|
8733
9225
|
naro deployments List deployments of the linked project
|
|
8734
9226
|
naro inspect <deployment-id> Show one deployment + build logs
|
|
8735
9227
|
naro cancel <deployment-id> Stop a build that is still running
|
|
@@ -8755,10 +9247,19 @@ Usage:
|
|
|
8755
9247
|
naro protection enable | disable Protect the preview URL (members only) or open it to anyone with the link (disable asks first; --yes in CI)
|
|
8756
9248
|
naro protection bypass [remove] Create (shown once) or remove the secret automation sends as x-naro-protection-bypass (replacing or removing asks first; --yes in CI)
|
|
8757
9249
|
|
|
8758
|
-
naro env list List env vars (secrets masked)
|
|
8759
|
-
naro env add KEY [
|
|
8760
|
-
naro env remove KEY [
|
|
9250
|
+
naro env list List env vars (secrets masked); an environment's value shows its name
|
|
9251
|
+
naro env add KEY [target] Add/update (value from prompt or stdin)
|
|
9252
|
+
naro env remove KEY [target] Remove; with no target only production, preview and all, never an environment's
|
|
9253
|
+
naro env import <file> [target] Upload a dotenv file's variables (asks first; --yes in CI)
|
|
8761
9254
|
naro env pull [file] Write env vars to .env.local
|
|
9255
|
+
target is production, preview, all or an environment name; add and import use all when none is given
|
|
9256
|
+
|
|
9257
|
+
naro environments List the project's environments: address, branch, protection, and how many the plan allows
|
|
9258
|
+
naro environments create <slug> [--branch <b>] [--copy-env preview|production|none] [--database schema|empty]
|
|
9259
|
+
A QA or staging surface served at <subdomain>--<slug>; starts with preview's variables
|
|
9260
|
+
naro environments set <slug> [--branch <b> | --no-branch] [--protection on|off]
|
|
9261
|
+
Change the branch whose pushes deploy here, or who can open it (protection off asks first; --yes in CI)
|
|
9262
|
+
naro environments delete <slug> Delete it with its worker and protection key (asks first; --yes in CI)
|
|
8762
9263
|
|
|
8763
9264
|
naro domains list | add <host> [--no-www] | remove <host>
|
|
8764
9265
|
add example.com also connects www.example.com and redirects the apex to
|
|
@@ -8879,8 +9380,10 @@ Usage:
|
|
|
8879
9380
|
naro storage bucket empty <name> Delete every object in it and keep the bucket (asks first)
|
|
8880
9381
|
naro storage bucket remove <name> [--empty]
|
|
8881
9382
|
Remove an empty bucket; --empty deletes its objects first (asks first)
|
|
8882
|
-
naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]
|
|
8883
|
-
The buckets, or the folders and objects under a prefix
|
|
9383
|
+
naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]
|
|
9384
|
+
The buckets, or the folders and objects under a prefix;
|
|
9385
|
+
--search keeps the names below it that start with <text>, ASCII case ignored
|
|
9386
|
+
(with --recursive, on every level: it does not find a name deep below)
|
|
8884
9387
|
naro storage cp <from> <to> [--upsert] [--content-type <t>] [--cache-control <seconds>]
|
|
8885
9388
|
One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
|
|
8886
9389
|
with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
|
|
@@ -8900,12 +9403,13 @@ Global options:
|
|
|
8900
9403
|
--project <id> Target project (overrides .naro/project.json)
|
|
8901
9404
|
--root <dir> App directory inside the repo (monorepos), e.g. apps/web
|
|
8902
9405
|
--name <name> Project name for a first deploy (defaults to package.json); the index name (naro db index create)
|
|
8903
|
-
--team <slug> Team scope for project listing/linking
|
|
9406
|
+
--team <slug> Team scope for project listing/linking and naro usage
|
|
8904
9407
|
--cwd <dir> Run as if started in <dir>
|
|
8905
9408
|
--prod Deploy/target production
|
|
8906
9409
|
--wait Poll until the domain goes live (naro domains verify)
|
|
8907
9410
|
--metric <m> lcp, inp, cls, fcp or ttfb; lcp by default (naro insights)
|
|
8908
9411
|
--range <r> 24h, 7d or 30d; 7d by default (naro analytics, naro insights)
|
|
9412
|
+
--month <YYYY-MM> A UTC calendar month, e.g. 2026-09; this month by default (naro usage)
|
|
8909
9413
|
--route <pattern> A route from the pages table, e.g. '/posts/[id]': the paths under it (naro insights)
|
|
8910
9414
|
--path <p> Only this page, starting with /, e.g. /pricing (naro analytics, naro insights)
|
|
8911
9415
|
--referrer <host> Only visits that came from this lowercase host, e.g. t.co (naro analytics only: Speed Insights has no referrer)
|
|
@@ -8913,7 +9417,14 @@ Global options:
|
|
|
8913
9417
|
--device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
|
|
8914
9418
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
8915
9419
|
--os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
|
|
8916
|
-
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull: preview by default)
|
|
9420
|
+
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name)
|
|
9421
|
+
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
9422
|
+
branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
|
|
9423
|
+
--no-branch No branch deploys to the environment any more (naro environments set; not with --branch)
|
|
9424
|
+
--copy-env <source> Where a new environment's variables come from: preview (default), production or none (naro environments create)
|
|
9425
|
+
--database <seed> What a new environment's database starts with: schema (default) or empty (naro environments create)
|
|
9426
|
+
--protection <on|off> on: only members of the project can open the environment; off: anyone with the link can
|
|
9427
|
+
(naro environments set; off asks first, --yes in CI)
|
|
8917
9428
|
--no-www Register the apex alone (naro domains add)
|
|
8918
9429
|
--file <path> SQL file to run (naro db query, naro policy create)
|
|
8919
9430
|
--dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
|
|
@@ -8945,6 +9456,7 @@ Global options:
|
|
|
8945
9456
|
--allowed-mime-types <t,\u2026> The types a bucket takes, like image/png,image/*; any clears it
|
|
8946
9457
|
--empty Delete the bucket's objects first (naro storage bucket remove)
|
|
8947
9458
|
--recursive, -r Everything under a folder (naro storage ls, naro storage rm)
|
|
9459
|
+
--search <text> Names below the prefix that start with <text>, ASCII case ignored (naro storage ls)
|
|
8948
9460
|
--upsert Replace an object that is there (naro storage cp)
|
|
8949
9461
|
--content-type <t> The object's type; by default from the file's extension (naro storage cp)
|
|
8950
9462
|
--cache-control <seconds> How long a browser may keep it; 3600 by default (naro storage cp)
|
|
@@ -9110,6 +9622,9 @@ function route(parsed) {
|
|
|
9110
9622
|
return { handler: runUnlink, positionals };
|
|
9111
9623
|
case "projects":
|
|
9112
9624
|
return { handler: runProjects, positionals };
|
|
9625
|
+
// Spec 2026-10-18-usage-cli-mcp §2.
|
|
9626
|
+
case "usage":
|
|
9627
|
+
return positionals.length === 0 ? { handler: runUsage, positionals: [] } : null;
|
|
9113
9628
|
case "project":
|
|
9114
9629
|
if (sub === void 0 || sub === "inspect")
|
|
9115
9630
|
return {
|
|
@@ -9175,11 +9690,41 @@ function route(parsed) {
|
|
|
9175
9690
|
case "remove":
|
|
9176
9691
|
case "rm":
|
|
9177
9692
|
return { handler: runEnvRemove, positionals: positionals.slice(1) };
|
|
9693
|
+
case "import":
|
|
9694
|
+
return { handler: runEnvImport, positionals: positionals.slice(1) };
|
|
9178
9695
|
case "pull":
|
|
9179
9696
|
return { handler: runEnvPull, positionals: positionals.slice(1) };
|
|
9180
9697
|
default:
|
|
9181
9698
|
return null;
|
|
9182
9699
|
}
|
|
9700
|
+
// Spec 2026-10-18-named-environments §14.2.
|
|
9701
|
+
case "environments":
|
|
9702
|
+
switch (sub) {
|
|
9703
|
+
case void 0:
|
|
9704
|
+
case "list":
|
|
9705
|
+
case "ls":
|
|
9706
|
+
return {
|
|
9707
|
+
handler: runEnvironmentsList,
|
|
9708
|
+
positionals: positionals.slice(1)
|
|
9709
|
+
};
|
|
9710
|
+
case "create":
|
|
9711
|
+
return {
|
|
9712
|
+
handler: runEnvironmentsCreate,
|
|
9713
|
+
positionals: positionals.slice(1)
|
|
9714
|
+
};
|
|
9715
|
+
case "set":
|
|
9716
|
+
return {
|
|
9717
|
+
handler: runEnvironmentsSet,
|
|
9718
|
+
positionals: positionals.slice(1)
|
|
9719
|
+
};
|
|
9720
|
+
case "delete":
|
|
9721
|
+
return {
|
|
9722
|
+
handler: runEnvironmentsDelete,
|
|
9723
|
+
positionals: positionals.slice(1)
|
|
9724
|
+
};
|
|
9725
|
+
default:
|
|
9726
|
+
return null;
|
|
9727
|
+
}
|
|
9183
9728
|
case "domains":
|
|
9184
9729
|
switch (sub) {
|
|
9185
9730
|
case void 0:
|
|
@@ -9345,8 +9890,8 @@ function route(parsed) {
|
|
|
9345
9890
|
function version() {
|
|
9346
9891
|
try {
|
|
9347
9892
|
const pkg = JSON.parse(
|
|
9348
|
-
|
|
9349
|
-
|
|
9893
|
+
readFileSync6(
|
|
9894
|
+
join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
9350
9895
|
"utf8"
|
|
9351
9896
|
)
|
|
9352
9897
|
);
|