naro-cli 0.18.0 → 0.19.1
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 +392 -25
- package/dist/{chunk-WLC42Y6U.js → chunk-ZO2BEC46.js} +2133 -415
- package/dist/deploy.d.ts +12 -0
- package/dist/deploy.js +1 -1
- package/dist/env-consent.d.ts +9 -1
- package/dist/env-surface.d.ts +12 -0
- package/dist/index.js +672 -166
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -18,14 +18,17 @@ import {
|
|
|
18
18
|
SPEED_INSIGHTS_METRICS,
|
|
19
19
|
SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
|
|
20
20
|
STORAGE_OBJECTS_TABLE,
|
|
21
|
+
STORAGE_SUBQUERIES_SQL,
|
|
21
22
|
buildResetPlan,
|
|
22
23
|
cellText,
|
|
24
|
+
checkObjectName,
|
|
23
25
|
clearSession,
|
|
24
26
|
commentText,
|
|
25
27
|
compareMigrations,
|
|
26
28
|
controlPlaneUrl,
|
|
27
29
|
countLintedTables,
|
|
28
30
|
createAuthClient,
|
|
31
|
+
dataError,
|
|
29
32
|
deployDirectory,
|
|
30
33
|
detectGitInfo,
|
|
31
34
|
ensureGitignoreHasNaro,
|
|
@@ -33,8 +36,10 @@ import {
|
|
|
33
36
|
generateTypes,
|
|
34
37
|
indexesFromRows,
|
|
35
38
|
isBookmark,
|
|
39
|
+
isEnvTargetName,
|
|
36
40
|
isExpired,
|
|
37
41
|
isNaroError,
|
|
42
|
+
isReservedEnvTarget,
|
|
38
43
|
isStalledBackend,
|
|
39
44
|
isTerminalStatus,
|
|
40
45
|
lintSchema,
|
|
@@ -42,11 +47,13 @@ import {
|
|
|
42
47
|
loadPolicy,
|
|
43
48
|
loadStoredSession,
|
|
44
49
|
login,
|
|
50
|
+
maskEnvValue,
|
|
45
51
|
migrationNameMismatch,
|
|
46
52
|
migrationsPendingError,
|
|
47
53
|
nextMigrationFileName,
|
|
48
54
|
openBrowser,
|
|
49
55
|
parseCacheControl,
|
|
56
|
+
parseDotenvDetailed,
|
|
50
57
|
parseGitHubRemote,
|
|
51
58
|
parsePolicyNode,
|
|
52
59
|
parsePolicyStatement,
|
|
@@ -57,6 +64,7 @@ import {
|
|
|
57
64
|
policiesLostByReset,
|
|
58
65
|
projectPath,
|
|
59
66
|
promptForMissingEnv,
|
|
67
|
+
pullEnv,
|
|
60
68
|
quoteIdent,
|
|
61
69
|
quotePolicyName,
|
|
62
70
|
readProjectLink,
|
|
@@ -66,6 +74,7 @@ import {
|
|
|
66
74
|
resetFileRefusedMessage,
|
|
67
75
|
resetPoliciesLostMessage,
|
|
68
76
|
resetPoliciesWithTablesMessage,
|
|
77
|
+
resetStorageOutdatedMessage,
|
|
69
78
|
resolveAuth,
|
|
70
79
|
resolveMigrationsDir,
|
|
71
80
|
saveSession,
|
|
@@ -75,6 +84,7 @@ import {
|
|
|
75
84
|
snapshotFromRows,
|
|
76
85
|
splitPolicyStatements,
|
|
77
86
|
splitStatements,
|
|
87
|
+
storageRefusalOfFiles,
|
|
78
88
|
storedPolicySql,
|
|
79
89
|
summarizeStatement,
|
|
80
90
|
summarizeTeamUsage,
|
|
@@ -86,7 +96,7 @@ import {
|
|
|
86
96
|
usageMonthOf,
|
|
87
97
|
userForAccessToken,
|
|
88
98
|
writeProjectLink
|
|
89
|
-
} from "./chunk-
|
|
99
|
+
} from "./chunk-ZO2BEC46.js";
|
|
90
100
|
import "./chunk-K3NQKI34.js";
|
|
91
101
|
|
|
92
102
|
// src/args.ts
|
|
@@ -125,7 +135,8 @@ var BOOLEAN_FLAGS = {
|
|
|
125
135
|
"--empty": "empty",
|
|
126
136
|
"--recursive": "recursive",
|
|
127
137
|
"-r": "recursive",
|
|
128
|
-
"--upsert": "upsert"
|
|
138
|
+
"--upsert": "upsert",
|
|
139
|
+
"--no-branch": "noBranch"
|
|
129
140
|
};
|
|
130
141
|
var REPEATED_FLAGS = {
|
|
131
142
|
"--column": "columns"
|
|
@@ -181,7 +192,10 @@ var STRING_FLAGS = {
|
|
|
181
192
|
"--allowed-mime-types": "allowedMimeTypes",
|
|
182
193
|
"--content-type": "contentType",
|
|
183
194
|
"--cache-control": "cacheControl",
|
|
184
|
-
"--search": "search"
|
|
195
|
+
"--search": "search",
|
|
196
|
+
"--copy-env": "copyEnv",
|
|
197
|
+
"--database": "database",
|
|
198
|
+
"--protection": "protection"
|
|
185
199
|
};
|
|
186
200
|
var SECRET_FLAGS = {
|
|
187
201
|
"--api-key": "--api-key-stdin",
|
|
@@ -523,7 +537,7 @@ function createContext(flags, positionals = []) {
|
|
|
523
537
|
}
|
|
524
538
|
|
|
525
539
|
// src/router.ts
|
|
526
|
-
import { readFileSync as
|
|
540
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
527
541
|
import { dirname as dirname4, join as join6 } from "path";
|
|
528
542
|
import { fileURLToPath } from "url";
|
|
529
543
|
|
|
@@ -2536,7 +2550,7 @@ async function runDbLint(context) {
|
|
|
2536
2550
|
(message) => context.output.warn(message)
|
|
2537
2551
|
);
|
|
2538
2552
|
const input = {
|
|
2539
|
-
tables: tables.filter((
|
|
2553
|
+
tables: tables.filter((table3) => !shadow.has(table3.name)),
|
|
2540
2554
|
foreignKeyCheck,
|
|
2541
2555
|
quickCheck: quick.rows.map(firstCell),
|
|
2542
2556
|
triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
|
|
@@ -2635,12 +2649,45 @@ async function hasBackend(client, projectId) {
|
|
|
2635
2649
|
throw error;
|
|
2636
2650
|
}
|
|
2637
2651
|
}
|
|
2638
|
-
async function
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2652
|
+
async function refuseStorageOutdated(client, projectId, storageTable, files, texts) {
|
|
2653
|
+
let refused = storageRefusalOfFiles(texts, {
|
|
2654
|
+
storage: storageTable,
|
|
2655
|
+
storageSubqueries: false
|
|
2656
|
+
});
|
|
2657
|
+
if (refused?.needs === "subqueries" && storageTable) {
|
|
2658
|
+
const answer = await client.db.query(projectId, {
|
|
2659
|
+
sql: STORAGE_SUBQUERIES_SQL
|
|
2660
|
+
});
|
|
2661
|
+
if ((answer.statements[0]?.rows.length ?? 0) > 0)
|
|
2662
|
+
refused = storageRefusalOfFiles(texts, {
|
|
2663
|
+
storage: true,
|
|
2664
|
+
storageSubqueries: true
|
|
2665
|
+
});
|
|
2643
2666
|
}
|
|
2667
|
+
if (refused)
|
|
2668
|
+
throw new NaroError(
|
|
2669
|
+
"BACKEND_OUTDATED",
|
|
2670
|
+
resetStorageOutdatedMessage(files[refused.file] ?? "", refused),
|
|
2671
|
+
{
|
|
2672
|
+
details: {
|
|
2673
|
+
migration: files[refused.file] ?? "",
|
|
2674
|
+
statement: refused.statement
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
async function policyPlan(client, projectId, schema, files, texts) {
|
|
2680
|
+
if (!schema.policyTable && await hasBackend(client, projectId))
|
|
2681
|
+
throw new NaroError("BACKEND_OUTDATED", RESET_BACKEND_OUTDATED);
|
|
2682
|
+
await refuseStorageOutdated(
|
|
2683
|
+
client,
|
|
2684
|
+
projectId,
|
|
2685
|
+
schema.storageTable,
|
|
2686
|
+
files,
|
|
2687
|
+
texts
|
|
2688
|
+
);
|
|
2689
|
+
if (!schema.policyTable)
|
|
2690
|
+
return { clearPolicies: false, lost: [], withTables: [] };
|
|
2644
2691
|
const answer = await client.db.query(projectId, {
|
|
2645
2692
|
sql: NAMED_POLICY_SQL.list
|
|
2646
2693
|
});
|
|
@@ -2669,7 +2716,7 @@ async function policyPlan(client, projectId, schema, dir, files) {
|
|
|
2669
2716
|
...policiesLostByReset({
|
|
2670
2717
|
current,
|
|
2671
2718
|
tables: schema.tables,
|
|
2672
|
-
files:
|
|
2719
|
+
files: texts,
|
|
2673
2720
|
// Storage's table stays; its policies are emptied with the rest (PR
|
|
2674
2721
|
// #85 리뷰). Realtime's messages need no flag: a virtual target, which
|
|
2675
2722
|
// reset-plan counts as a table always there (Realtime 3-2a).
|
|
@@ -2707,6 +2754,7 @@ async function runDbReset(context) {
|
|
|
2707
2754
|
${problemLines.join("\n")}`,
|
|
2708
2755
|
{ details: { problems } }
|
|
2709
2756
|
);
|
|
2757
|
+
const texts = files.map((name) => readFileSync3(join3(dir, name), "utf8"));
|
|
2710
2758
|
const client = await context.client();
|
|
2711
2759
|
const projectId = await context.resolveProjectId();
|
|
2712
2760
|
const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
|
|
@@ -2722,8 +2770,8 @@ ${problemLines.join("\n")}`,
|
|
|
2722
2770
|
client,
|
|
2723
2771
|
projectId,
|
|
2724
2772
|
{ policyTable: policyTable2, storageTable, tables },
|
|
2725
|
-
|
|
2726
|
-
|
|
2773
|
+
files,
|
|
2774
|
+
texts
|
|
2727
2775
|
);
|
|
2728
2776
|
const policiesNotInFiles = policies.lost.map((policy) => ({
|
|
2729
2777
|
name: `${policy.table}.${policy.name}`,
|
|
@@ -3263,7 +3311,7 @@ function conditionsFrom(flags) {
|
|
|
3263
3311
|
);
|
|
3264
3312
|
return conditions;
|
|
3265
3313
|
}
|
|
3266
|
-
function patchFromFile(context, path,
|
|
3314
|
+
function patchFromFile(context, path, table3) {
|
|
3267
3315
|
let text;
|
|
3268
3316
|
try {
|
|
3269
3317
|
text = readFileSync4(resolve3(context.cwd, path), "utf8");
|
|
@@ -3285,9 +3333,9 @@ function patchFromFile(context, path, table2) {
|
|
|
3285
3333
|
if (Object.hasOwn(body, "table")) {
|
|
3286
3334
|
if (typeof named !== "string")
|
|
3287
3335
|
refuse2(`${path}: table must be a string, the table's name.`);
|
|
3288
|
-
if (
|
|
3336
|
+
if (table3 !== void 0 && asciiLower(named) !== asciiLower(table3))
|
|
3289
3337
|
refuse2(
|
|
3290
|
-
`${path} is the policy of ${printable(named)}, not ${printable(
|
|
3338
|
+
`${path} is the policy of ${printable(named)}, not ${printable(table3)}. Remove its "table" to set it on ${printable(table3)}.`
|
|
3291
3339
|
);
|
|
3292
3340
|
}
|
|
3293
3341
|
const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
|
|
@@ -3300,7 +3348,7 @@ function patchFromFile(context, path, table2) {
|
|
|
3300
3348
|
function asciiLower(text) {
|
|
3301
3349
|
return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
3302
3350
|
}
|
|
3303
|
-
function policyPatch(flags, context,
|
|
3351
|
+
function policyPatch(flags, context, table3) {
|
|
3304
3352
|
if (flags.fromFile !== void 0) {
|
|
3305
3353
|
if (OTHER_FLAGS.some(
|
|
3306
3354
|
(key) => flags[key] !== void 0 && flags[key] !== false
|
|
@@ -3309,7 +3357,7 @@ function policyPatch(flags, context, table2) {
|
|
|
3309
3357
|
"--from-file takes the whole policy: leave out the other policy options."
|
|
3310
3358
|
);
|
|
3311
3359
|
if (!context) refuse2(SET_USAGE);
|
|
3312
|
-
return patchFromFile(context, flags.fromFile,
|
|
3360
|
+
return patchFromFile(context, flags.fromFile, table3);
|
|
3313
3361
|
}
|
|
3314
3362
|
const patch = {};
|
|
3315
3363
|
for (const action of POLICY_ACTIONS) {
|
|
@@ -3448,7 +3496,7 @@ function policyTable(policies) {
|
|
|
3448
3496
|
const widths = header.map(
|
|
3449
3497
|
(title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
|
|
3450
3498
|
);
|
|
3451
|
-
const
|
|
3499
|
+
const table3 = [header, ...rows].map(
|
|
3452
3500
|
(cells) => cells.map(
|
|
3453
3501
|
(cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
|
|
3454
3502
|
).join(" ").trimEnd()
|
|
@@ -3465,10 +3513,10 @@ function policyTable(policies) {
|
|
|
3465
3513
|
}
|
|
3466
3514
|
)
|
|
3467
3515
|
);
|
|
3468
|
-
if (lines.length === 0) return
|
|
3516
|
+
if (lines.length === 0) return table3;
|
|
3469
3517
|
const width = Math.max(...lines.map((line) => line.label.length));
|
|
3470
3518
|
return [
|
|
3471
|
-
...
|
|
3519
|
+
...table3,
|
|
3472
3520
|
"",
|
|
3473
3521
|
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
|
|
3474
3522
|
];
|
|
@@ -3485,39 +3533,39 @@ function statementLine(policy) {
|
|
|
3485
3533
|
);
|
|
3486
3534
|
return shownStatement(policy, policy.table, "naro policy list --json");
|
|
3487
3535
|
}
|
|
3488
|
-
function policyNamed(name,
|
|
3489
|
-
return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(
|
|
3536
|
+
function policyNamed(name, table3) {
|
|
3537
|
+
return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table3))}`;
|
|
3490
3538
|
}
|
|
3491
|
-
function shownStatement(statement,
|
|
3539
|
+
function shownStatement(statement, table3, exact) {
|
|
3492
3540
|
if (isPrintable(statement.sql)) return statement.sql;
|
|
3493
3541
|
return printable(
|
|
3494
|
-
`-- ${policyNamed(statement.name,
|
|
3542
|
+
`-- ${policyNamed(statement.name, table3)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
|
|
3495
3543
|
);
|
|
3496
3544
|
}
|
|
3497
3545
|
function policyStatements(policies) {
|
|
3498
3546
|
const lines = [];
|
|
3499
|
-
let
|
|
3547
|
+
let table3;
|
|
3500
3548
|
for (const policy of policies) {
|
|
3501
|
-
if (
|
|
3502
|
-
|
|
3549
|
+
if (table3 !== void 0 && policy.table !== table3) lines.push("");
|
|
3550
|
+
table3 = policy.table;
|
|
3503
3551
|
lines.push(statementLine(policy));
|
|
3504
3552
|
}
|
|
3505
3553
|
return lines;
|
|
3506
3554
|
}
|
|
3507
3555
|
async function runPolicyList(context) {
|
|
3508
|
-
const
|
|
3556
|
+
const table3 = context.flags.table;
|
|
3509
3557
|
const client = await context.client();
|
|
3510
3558
|
const projectId = await context.resolveProjectId();
|
|
3511
3559
|
const policies = await client.backend.rlsPolicies(
|
|
3512
3560
|
projectId,
|
|
3513
|
-
|
|
3561
|
+
table3 === void 0 ? {} : { table: table3 }
|
|
3514
3562
|
);
|
|
3515
3563
|
let lines;
|
|
3516
3564
|
if (policies.length > 0)
|
|
3517
3565
|
lines = [...policyStatements(policies), "", DEFAULT_DENY];
|
|
3518
|
-
else if (
|
|
3566
|
+
else if (table3 !== void 0)
|
|
3519
3567
|
lines = [
|
|
3520
|
-
`No policies on ${printable(
|
|
3568
|
+
`No policies on ${printable(table3)}: anon and signed-in requests to it are denied.`
|
|
3521
3569
|
];
|
|
3522
3570
|
else
|
|
3523
3571
|
lines = [
|
|
@@ -3613,17 +3661,17 @@ async function runPolicyCreate(context) {
|
|
|
3613
3661
|
}
|
|
3614
3662
|
async function runPolicyDrop(context) {
|
|
3615
3663
|
const [name] = context.positionalOrThrow(1, DROP_USAGE);
|
|
3616
|
-
const
|
|
3617
|
-
if (
|
|
3664
|
+
const table3 = context.flags.table;
|
|
3665
|
+
if (table3 === void 0 || table3 === "") refuse2(DROP_USAGE);
|
|
3618
3666
|
await confirmOrAbort(
|
|
3619
3667
|
context,
|
|
3620
|
-
`Drop policy ${printable(name)} on ${printable(
|
|
3668
|
+
`Drop policy ${printable(name)} on ${printable(table3)}? It stops applying within about 10 seconds.`
|
|
3621
3669
|
);
|
|
3622
3670
|
const client = await context.client();
|
|
3623
3671
|
const projectId = await context.resolveProjectId();
|
|
3624
3672
|
const dropped = await client.backend.dropRlsPolicy(
|
|
3625
3673
|
projectId,
|
|
3626
|
-
|
|
3674
|
+
table3,
|
|
3627
3675
|
name
|
|
3628
3676
|
);
|
|
3629
3677
|
context.output.result({ projectId, dropped }, [
|
|
@@ -3631,13 +3679,13 @@ async function runPolicyDrop(context) {
|
|
|
3631
3679
|
]);
|
|
3632
3680
|
}
|
|
3633
3681
|
async function runPolicySet(context) {
|
|
3634
|
-
const [
|
|
3635
|
-
const patch = policyPatch(context.flags, context,
|
|
3682
|
+
const [table3] = context.positionalOrThrow(1, SET_USAGE);
|
|
3683
|
+
const patch = policyPatch(context.flags, context, table3);
|
|
3636
3684
|
const client = await context.client();
|
|
3637
3685
|
const projectId = await context.resolveProjectId();
|
|
3638
3686
|
const policy = await client.backend.setPolicy(
|
|
3639
3687
|
projectId,
|
|
3640
|
-
|
|
3688
|
+
table3,
|
|
3641
3689
|
patch
|
|
3642
3690
|
);
|
|
3643
3691
|
let named;
|
|
@@ -3658,14 +3706,14 @@ async function runPolicySet(context) {
|
|
|
3658
3706
|
);
|
|
3659
3707
|
}
|
|
3660
3708
|
async function runPolicyRemove(context) {
|
|
3661
|
-
const [
|
|
3709
|
+
const [table3] = context.positionalOrThrow(1, REMOVE_USAGE);
|
|
3662
3710
|
await confirmOrAbort(
|
|
3663
3711
|
context,
|
|
3664
|
-
`Remove every policy on ${printable(
|
|
3712
|
+
`Remove every policy on ${printable(table3)}? Anon and signed-in requests to it are denied from then on.`
|
|
3665
3713
|
);
|
|
3666
3714
|
const client = await context.client();
|
|
3667
3715
|
const projectId = await context.resolveProjectId();
|
|
3668
|
-
const policy = await client.backend.removePolicy(projectId,
|
|
3716
|
+
const policy = await client.backend.removePolicy(projectId, table3);
|
|
3669
3717
|
const names = policy.policies ?? [];
|
|
3670
3718
|
context.output.result({ projectId, removed: true, policy }, [
|
|
3671
3719
|
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.`
|
|
@@ -3752,14 +3800,14 @@ function parseColumnSpec(spec) {
|
|
|
3752
3800
|
column.default = defaultValue(value);
|
|
3753
3801
|
else if (value !== void 0 && key === "references") {
|
|
3754
3802
|
const dot = value.lastIndexOf(".");
|
|
3755
|
-
const
|
|
3803
|
+
const table3 = dot < 0 ? "" : value.slice(0, dot);
|
|
3756
3804
|
const target = dot < 0 ? "" : value.slice(dot + 1);
|
|
3757
|
-
if (!
|
|
3805
|
+
if (!table3 || !target)
|
|
3758
3806
|
throw new NaroError(
|
|
3759
3807
|
"VALIDATION_FAILED",
|
|
3760
3808
|
`${shown}: references needs <table>.<column>.`
|
|
3761
3809
|
);
|
|
3762
|
-
column.references = { table:
|
|
3810
|
+
column.references = { table: table3, column: target };
|
|
3763
3811
|
} else if (value !== void 0 && key === "ondelete") {
|
|
3764
3812
|
onDelete = ON_DELETE[value.toLowerCase()];
|
|
3765
3813
|
if (!onDelete)
|
|
@@ -3839,14 +3887,6 @@ function removedLines(policy, dryRun) {
|
|
|
3839
3887
|
}
|
|
3840
3888
|
return lines;
|
|
3841
3889
|
}
|
|
3842
|
-
function removedBeforeFailure(error) {
|
|
3843
|
-
if (!isNaroError(error)) return [];
|
|
3844
|
-
const removed = error.details?.removedPolicies;
|
|
3845
|
-
if (!Array.isArray(removed)) return [];
|
|
3846
|
-
return removed.filter(
|
|
3847
|
-
(entry) => typeof entry === "object" && entry !== null && typeof entry.table === "string" && entry.ownerColumn !== void 0
|
|
3848
|
-
);
|
|
3849
|
-
}
|
|
3850
3890
|
async function runSchemaChange(context, operations, question) {
|
|
3851
3891
|
const dryRun = context.flags.dryRun === true;
|
|
3852
3892
|
if (question !== void 0 && !dryRun)
|
|
@@ -3872,12 +3912,6 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3872
3912
|
const result = await client.db.schema(projectId, sent, {
|
|
3873
3913
|
...dryRun ? { dryRun: true } : {},
|
|
3874
3914
|
name
|
|
3875
|
-
}).catch((error) => {
|
|
3876
|
-
if (!context.flags.json)
|
|
3877
|
-
for (const policy of removedBeforeFailure(error))
|
|
3878
|
-
for (const line of removedLines(policy, false))
|
|
3879
|
-
context.output.warn(line);
|
|
3880
|
-
throw error;
|
|
3881
3915
|
});
|
|
3882
3916
|
const sqlLines = result.sql.trimEnd().split("\n").map(printable);
|
|
3883
3917
|
const removed = result.removedPolicies ?? [];
|
|
@@ -3938,16 +3972,16 @@ async function runDbIndexUsage() {
|
|
|
3938
3972
|
throw new NaroError("VALIDATION_FAILED", USAGE2.index);
|
|
3939
3973
|
}
|
|
3940
3974
|
async function runDbTableCreate(context) {
|
|
3941
|
-
const [
|
|
3975
|
+
const [table3] = context.positionals;
|
|
3942
3976
|
const specs = columnsFlag(context);
|
|
3943
|
-
if (!
|
|
3977
|
+
if (!table3 || specs.length === 0)
|
|
3944
3978
|
throw new NaroError("VALIDATION_FAILED", USAGE2.create);
|
|
3945
3979
|
const columns = specs.map(parseColumnSpec);
|
|
3946
3980
|
const owner = context.flags.ownerColumn;
|
|
3947
3981
|
await runSchemaChange(context, [
|
|
3948
3982
|
{
|
|
3949
3983
|
op: "createTable",
|
|
3950
|
-
table:
|
|
3984
|
+
table: table3,
|
|
3951
3985
|
columns,
|
|
3952
3986
|
...context.flags.ifNotExists ? { ifNotExists: true } : {},
|
|
3953
3987
|
...owner === void 0 ? {} : { owner: { column: owner } }
|
|
@@ -3955,56 +3989,56 @@ async function runDbTableCreate(context) {
|
|
|
3955
3989
|
]);
|
|
3956
3990
|
}
|
|
3957
3991
|
async function runDbTableAddColumn(context) {
|
|
3958
|
-
const [
|
|
3992
|
+
const [table3] = context.positionals;
|
|
3959
3993
|
const specs = columnsFlag(context);
|
|
3960
3994
|
const [spec] = specs;
|
|
3961
|
-
if (!
|
|
3995
|
+
if (!table3 || spec === void 0 || specs.length !== 1)
|
|
3962
3996
|
throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
|
|
3963
3997
|
await runSchemaChange(context, [
|
|
3964
|
-
{ op: "addColumn", table:
|
|
3998
|
+
{ op: "addColumn", table: table3, column: parseColumnSpec(spec) }
|
|
3965
3999
|
]);
|
|
3966
4000
|
}
|
|
3967
4001
|
async function runDbTableRenameColumn(context) {
|
|
3968
|
-
const [
|
|
3969
|
-
if (!
|
|
4002
|
+
const [table3, column, to] = context.positionals;
|
|
4003
|
+
if (!table3 || !column || !to)
|
|
3970
4004
|
throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
|
|
3971
|
-
await runSchemaChange(context, [{ op: "renameColumn", table:
|
|
4005
|
+
await runSchemaChange(context, [{ op: "renameColumn", table: table3, column, to }]);
|
|
3972
4006
|
}
|
|
3973
4007
|
async function runDbTableRename(context) {
|
|
3974
|
-
const [
|
|
3975
|
-
if (!
|
|
3976
|
-
await runSchemaChange(context, [{ op: "renameTable", table:
|
|
4008
|
+
const [table3, to] = context.positionals;
|
|
4009
|
+
if (!table3 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
|
|
4010
|
+
await runSchemaChange(context, [{ op: "renameTable", table: table3, to }]);
|
|
3977
4011
|
}
|
|
3978
4012
|
async function runDbTableDropColumn(context) {
|
|
3979
|
-
const [
|
|
3980
|
-
if (!
|
|
4013
|
+
const [table3, column] = context.positionals;
|
|
4014
|
+
if (!table3 || !column)
|
|
3981
4015
|
throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
|
|
3982
|
-
const target = `${
|
|
4016
|
+
const target = `${table3}.${column}`;
|
|
3983
4017
|
await runSchemaChange(
|
|
3984
4018
|
context,
|
|
3985
|
-
[{ op: "dropColumn", table:
|
|
4019
|
+
[{ op: "dropColumn", table: table3, column, confirm: target }],
|
|
3986
4020
|
`Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
|
|
3987
4021
|
);
|
|
3988
4022
|
}
|
|
3989
4023
|
async function runDbTableDrop(context) {
|
|
3990
|
-
const [
|
|
3991
|
-
if (!
|
|
4024
|
+
const [table3] = context.positionals;
|
|
4025
|
+
if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
|
|
3992
4026
|
await runSchemaChange(
|
|
3993
4027
|
context,
|
|
3994
|
-
[{ op: "dropTable", table:
|
|
3995
|
-
`Drop table ${printable(
|
|
4028
|
+
[{ op: "dropTable", table: table3, confirm: table3 }],
|
|
4029
|
+
`Drop table ${printable(table3)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
|
|
3996
4030
|
);
|
|
3997
4031
|
}
|
|
3998
4032
|
async function runDbIndexCreate(context) {
|
|
3999
|
-
const [
|
|
4033
|
+
const [table3, list] = context.positionals;
|
|
4000
4034
|
const columns = listFlag(list);
|
|
4001
|
-
if (!
|
|
4035
|
+
if (!table3 || columns.length === 0)
|
|
4002
4036
|
throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
|
|
4003
4037
|
const name = context.flags.name;
|
|
4004
4038
|
await runSchemaChange(context, [
|
|
4005
4039
|
{
|
|
4006
4040
|
op: "createIndex",
|
|
4007
|
-
table:
|
|
4041
|
+
table: table3,
|
|
4008
4042
|
columns,
|
|
4009
4043
|
...context.flags.unique ? { unique: true } : {},
|
|
4010
4044
|
...name === void 0 ? {} : { name }
|
|
@@ -4079,13 +4113,16 @@ async function runGitConnect(context, overrides = {}) {
|
|
|
4079
4113
|
if (cannotAsk(context)) {
|
|
4080
4114
|
throw new NaroError(
|
|
4081
4115
|
"REPOSITORY_NOT_ACCESSIBLE",
|
|
4082
|
-
`Naro cannot reach ${fullName} yet.
|
|
4116
|
+
`Naro cannot reach ${fullName} yet. Open ${first.installUrl} to give Naro access (if the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first), then run: naro git connect`,
|
|
4083
4117
|
{ details: { projectId, installUrl: first.installUrl } }
|
|
4084
4118
|
);
|
|
4085
4119
|
}
|
|
4086
4120
|
output.log(`
|
|
4087
4121
|
\u25B8 Naro needs access to ${fullName}.`);
|
|
4088
4122
|
output.log(` Opening ${first.installUrl}`);
|
|
4123
|
+
output.log(
|
|
4124
|
+
" If the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first."
|
|
4125
|
+
);
|
|
4089
4126
|
deps.openBrowser(first.installUrl);
|
|
4090
4127
|
output.log(" Waiting for the installation to finish...\n");
|
|
4091
4128
|
await waitForInstall(context, projectId, fullName, deps);
|
|
@@ -4337,10 +4374,30 @@ async function runUnlink(context) {
|
|
|
4337
4374
|
}
|
|
4338
4375
|
|
|
4339
4376
|
// src/commands/deploy.ts
|
|
4377
|
+
function deployTarget(flags) {
|
|
4378
|
+
if (flags.env === void 0)
|
|
4379
|
+
return { production: flags.prod, environment: void 0 };
|
|
4380
|
+
if (flags.env === "")
|
|
4381
|
+
throw new NaroError(
|
|
4382
|
+
"VALIDATION_FAILED",
|
|
4383
|
+
"--env must be production, preview or an environment name."
|
|
4384
|
+
);
|
|
4385
|
+
if (flags.prod)
|
|
4386
|
+
throw new NaroError(
|
|
4387
|
+
"VALIDATION_FAILED",
|
|
4388
|
+
"--env and --prod cannot be used together."
|
|
4389
|
+
);
|
|
4390
|
+
if (flags.env === "production")
|
|
4391
|
+
return { production: true, environment: void 0 };
|
|
4392
|
+
if (flags.env === "preview")
|
|
4393
|
+
return { production: false, environment: void 0 };
|
|
4394
|
+
return { production: false, environment: flags.env };
|
|
4395
|
+
}
|
|
4340
4396
|
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
4341
4397
|
const { flags, output } = context;
|
|
4398
|
+
const target = deployTarget(flags);
|
|
4342
4399
|
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
4343
|
-
if (
|
|
4400
|
+
if (target.production && !flags.yes && !flags.json && isInteractive()) {
|
|
4344
4401
|
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
4345
4402
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
4346
4403
|
}
|
|
@@ -4360,10 +4417,20 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4360
4417
|
teamSlug: team.slug,
|
|
4361
4418
|
rootDirectory: flags.root ?? null,
|
|
4362
4419
|
branch: flags.branch,
|
|
4363
|
-
production:
|
|
4420
|
+
production: target.production,
|
|
4421
|
+
...target.environment !== void 0 && {
|
|
4422
|
+
environment: target.environment
|
|
4423
|
+
},
|
|
4364
4424
|
...canPrompt && {
|
|
4365
4425
|
onMissingEnv: (keys) => promptForMissingEnv(
|
|
4366
|
-
{
|
|
4426
|
+
{
|
|
4427
|
+
cwd: context.cwd,
|
|
4428
|
+
prompter: context.prompter,
|
|
4429
|
+
output,
|
|
4430
|
+
...target.environment !== void 0 && {
|
|
4431
|
+
environment: target.environment
|
|
4432
|
+
}
|
|
4433
|
+
},
|
|
4367
4434
|
keys
|
|
4368
4435
|
)
|
|
4369
4436
|
},
|
|
@@ -4390,20 +4457,22 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4390
4457
|
teamSlug: result.teamSlug
|
|
4391
4458
|
});
|
|
4392
4459
|
const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
|
|
4460
|
+
const surface = result.environment ?? (result.production ? "production" : "preview");
|
|
4393
4461
|
context.output.result(
|
|
4394
4462
|
{
|
|
4395
4463
|
deploymentId: result.deploymentId,
|
|
4396
4464
|
projectId: result.projectId,
|
|
4397
4465
|
status: result.status,
|
|
4398
4466
|
url: result.url,
|
|
4399
|
-
target:
|
|
4467
|
+
target: surface,
|
|
4468
|
+
environment: result.environment ?? null,
|
|
4400
4469
|
teamSlug: result.teamSlug,
|
|
4401
4470
|
inspectUrl: inspect,
|
|
4402
4471
|
missingEnvKeys: result.missingEnvKeys
|
|
4403
4472
|
},
|
|
4404
4473
|
[
|
|
4405
4474
|
"",
|
|
4406
|
-
`\u2705 Deployment ready (${
|
|
4475
|
+
`\u2705 Deployment ready (${surface})`,
|
|
4407
4476
|
"",
|
|
4408
4477
|
` ${result.url}`,
|
|
4409
4478
|
` Inspect ${inspect}`
|
|
@@ -4815,18 +4884,30 @@ async function runDomainsRemove(context) {
|
|
|
4815
4884
|
}
|
|
4816
4885
|
|
|
4817
4886
|
// src/commands/env.ts
|
|
4818
|
-
import { chmodSync, existsSync, writeFileSync as writeFileSync4 } from "fs";
|
|
4887
|
+
import { chmodSync, existsSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
4819
4888
|
import { resolve as resolve4 } from "path";
|
|
4820
|
-
var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
|
|
4821
4889
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4890
|
+
var KEY_MAX_LENGTH = 256;
|
|
4891
|
+
var IMPORT_BATCH_SIZE = 100;
|
|
4892
|
+
var VALUE_MAX_BYTES = 5 * 1024;
|
|
4893
|
+
var STATIC_ASSETS_BINDING = "ASSETS";
|
|
4894
|
+
var TARGET_USAGE = "[production|preview|all|<environment>]";
|
|
4895
|
+
function validKey(key) {
|
|
4896
|
+
return KEY_RE.test(key) && key.length <= KEY_MAX_LENGTH;
|
|
4897
|
+
}
|
|
4898
|
+
function isSecretKey(key) {
|
|
4899
|
+
return !key.startsWith("NEXT_PUBLIC_");
|
|
4900
|
+
}
|
|
4901
|
+
function variables(count2) {
|
|
4902
|
+
return `${String(count2)} variable${count2 === 1 ? "" : "s"}`;
|
|
4903
|
+
}
|
|
4822
4904
|
function parseTarget(value) {
|
|
4823
4905
|
if (value === void 0) return "all";
|
|
4824
|
-
if (
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
return value;
|
|
4906
|
+
if (isEnvTargetName(value)) return value;
|
|
4907
|
+
throw new NaroError(
|
|
4908
|
+
"VALIDATION_FAILED",
|
|
4909
|
+
"Target must be production, preview, all or an environment name."
|
|
4910
|
+
);
|
|
4830
4911
|
}
|
|
4831
4912
|
function maskedValue(row) {
|
|
4832
4913
|
if (!row.isSecret) return row.value ?? "";
|
|
@@ -4857,12 +4938,9 @@ async function runEnvList(context) {
|
|
|
4857
4938
|
);
|
|
4858
4939
|
}
|
|
4859
4940
|
async function runEnvAdd(context) {
|
|
4860
|
-
context.positionalOrThrow(
|
|
4861
|
-
1,
|
|
4862
|
-
"Usage: naro env add KEY [production|preview|all]"
|
|
4863
|
-
);
|
|
4941
|
+
context.positionalOrThrow(1, `Usage: naro env add KEY ${TARGET_USAGE}`);
|
|
4864
4942
|
const [key, targetArg] = context.positionals;
|
|
4865
|
-
if (!key || !
|
|
4943
|
+
if (!key || !validKey(key))
|
|
4866
4944
|
throw new NaroError(
|
|
4867
4945
|
"VALIDATION_FAILED",
|
|
4868
4946
|
"Keys must look like MY_VARIABLE (letters, digits, underscore)."
|
|
@@ -4870,25 +4948,38 @@ async function runEnvAdd(context) {
|
|
|
4870
4948
|
const target = parseTarget(targetArg);
|
|
4871
4949
|
const projectId = await context.resolveProjectId();
|
|
4872
4950
|
const client = await context.client();
|
|
4951
|
+
const typed = process.stdin.isTTY === true;
|
|
4952
|
+
if (typed) {
|
|
4953
|
+
await client.projects.get(projectId);
|
|
4954
|
+
if (!isReservedEnvTarget(target))
|
|
4955
|
+
await client.environments.get(projectId, target);
|
|
4956
|
+
}
|
|
4873
4957
|
let value;
|
|
4874
|
-
if (!
|
|
4958
|
+
if (!typed) {
|
|
4875
4959
|
value = await readStdin();
|
|
4876
4960
|
} else {
|
|
4877
4961
|
value = await context.prompter.askHidden(`? What's the value of ${key}? `);
|
|
4878
4962
|
}
|
|
4879
4963
|
if (value.length === 0)
|
|
4880
4964
|
throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
|
|
4881
|
-
const isSecret =
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4965
|
+
const isSecret = isSecretKey(key);
|
|
4966
|
+
let saved = target;
|
|
4967
|
+
try {
|
|
4968
|
+
await client.env.set(projectId, [{ key, value, target, isSecret }]);
|
|
4969
|
+
} catch (error) {
|
|
4970
|
+
if (!typed || target !== "all" || !(error instanceof NaroError) || error.code !== "FORBIDDEN" || !await context.prompter.confirm(
|
|
4971
|
+
`${error.message} Save ${key} for preview only?`
|
|
4972
|
+
))
|
|
4973
|
+
throw error;
|
|
4974
|
+
saved = "preview";
|
|
4975
|
+
await client.env.set(projectId, [{ key, value, target: saved, isSecret }]);
|
|
4976
|
+
}
|
|
4977
|
+
context.output.result({ projectId, key, target: saved, isSecret }, [
|
|
4978
|
+
`\u2705 Added ${key} (${saved}${isSecret ? ", secret" : ""})`
|
|
4885
4979
|
]);
|
|
4886
4980
|
}
|
|
4887
4981
|
async function runEnvRemove(context) {
|
|
4888
|
-
context.positionalOrThrow(
|
|
4889
|
-
1,
|
|
4890
|
-
"Usage: naro env remove KEY [production|preview|all]"
|
|
4891
|
-
);
|
|
4982
|
+
context.positionalOrThrow(1, `Usage: naro env remove KEY ${TARGET_USAGE}`);
|
|
4892
4983
|
const [key, targetArg] = context.positionals;
|
|
4893
4984
|
const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
|
|
4894
4985
|
await confirmOrAbort(
|
|
@@ -4902,40 +4993,237 @@ async function runEnvRemove(context) {
|
|
|
4902
4993
|
`\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
|
|
4903
4994
|
]);
|
|
4904
4995
|
}
|
|
4905
|
-
async function
|
|
4906
|
-
|
|
4907
|
-
const
|
|
4908
|
-
|
|
4996
|
+
async function runEnvImport(context) {
|
|
4997
|
+
context.positionalOrThrow(1, `Usage: naro env import <file> ${TARGET_USAGE}`);
|
|
4998
|
+
const [file, targetArg] = context.positionals;
|
|
4999
|
+
const target = parseTarget(targetArg);
|
|
5000
|
+
let text;
|
|
5001
|
+
try {
|
|
5002
|
+
text = readFileSync5(resolve4(context.cwd, file), "utf8");
|
|
5003
|
+
} catch {
|
|
5004
|
+
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5005
|
+
}
|
|
5006
|
+
const parsed = parseDotenvDetailed(text);
|
|
5007
|
+
const [unclosed] = parsed.unclosedQuotes;
|
|
5008
|
+
if (unclosed !== void 0)
|
|
4909
5009
|
throw new NaroError(
|
|
4910
5010
|
"VALIDATION_FAILED",
|
|
4911
|
-
|
|
5011
|
+
`Unclosed quote in ${file} at line ${String(unclosed)}.`
|
|
5012
|
+
);
|
|
5013
|
+
const vars = Object.entries(parsed.vars);
|
|
5014
|
+
if (vars.length === 0)
|
|
5015
|
+
throw new NaroError("VALIDATION_FAILED", `No variables found in ${file}.`);
|
|
5016
|
+
const invalid = vars.map(([key]) => key).filter((key) => !validKey(key));
|
|
5017
|
+
if (invalid.length > 0)
|
|
5018
|
+
throw new NaroError(
|
|
5019
|
+
"VALIDATION_FAILED",
|
|
5020
|
+
`Invalid keys in ${file}: ${invalid.join(", ")}.`
|
|
5021
|
+
);
|
|
5022
|
+
if (vars.some(([key]) => key === STATIC_ASSETS_BINDING))
|
|
5023
|
+
throw new NaroError(
|
|
5024
|
+
"VALIDATION_FAILED",
|
|
5025
|
+
`${STATIC_ASSETS_BINDING} is reserved for the project's static files; pick another name.`
|
|
5026
|
+
);
|
|
5027
|
+
const oversized = vars.filter(([, value]) => Buffer.byteLength(value, "utf8") > VALUE_MAX_BYTES).map(([key]) => key);
|
|
5028
|
+
if (oversized.length > 0)
|
|
5029
|
+
throw new NaroError(
|
|
5030
|
+
"VALIDATION_FAILED",
|
|
5031
|
+
`Values over 5 KB in ${file}: ${oversized.join(", ")}.`
|
|
4912
5032
|
);
|
|
4913
|
-
const client = await context.client();
|
|
4914
5033
|
const projectId = await context.resolveProjectId();
|
|
4915
|
-
|
|
4916
|
-
const
|
|
4917
|
-
|
|
5034
|
+
context.output.log(`Variables in ${file}:`);
|
|
5035
|
+
for (const [key, value] of vars)
|
|
5036
|
+
context.output.log(
|
|
5037
|
+
` ${key.padEnd(32)} ${value === "" ? "(empty)" : maskEnvValue(key, value)}`
|
|
5038
|
+
);
|
|
5039
|
+
await confirmOrAbort(
|
|
5040
|
+
context,
|
|
5041
|
+
`Upload ${variables(vars.length)} to ${target}?`
|
|
4918
5042
|
);
|
|
4919
|
-
const
|
|
4920
|
-
|
|
5043
|
+
const client = await context.client();
|
|
5044
|
+
const entries = vars.map(([key, value]) => ({
|
|
5045
|
+
key,
|
|
5046
|
+
value,
|
|
5047
|
+
target,
|
|
5048
|
+
isSecret: isSecretKey(key)
|
|
5049
|
+
}));
|
|
5050
|
+
if (entries.length > IMPORT_BATCH_SIZE) {
|
|
5051
|
+
const taken = new Set(await client.env.takenBindingNames(projectId));
|
|
5052
|
+
const conflict = entries.find((entry) => taken.has(entry.key));
|
|
5053
|
+
if (conflict)
|
|
5054
|
+
throw new NaroError(
|
|
5055
|
+
"VALIDATION_FAILED",
|
|
5056
|
+
`${conflict.key} is already used by a storage binding.`
|
|
5057
|
+
);
|
|
5058
|
+
}
|
|
5059
|
+
for (let start = 0; start < entries.length; start += IMPORT_BATCH_SIZE)
|
|
5060
|
+
await client.env.set(
|
|
5061
|
+
projectId,
|
|
5062
|
+
entries.slice(start, start + IMPORT_BATCH_SIZE)
|
|
5063
|
+
);
|
|
5064
|
+
context.output.result({ projectId, file, target, count: entries.length }, [
|
|
5065
|
+
`\u2705 Imported ${variables(entries.length)} (${target})`
|
|
5066
|
+
]);
|
|
5067
|
+
}
|
|
5068
|
+
function parsePullSurface(value) {
|
|
5069
|
+
if (value === void 0) return "preview";
|
|
5070
|
+
if (value !== "all" && isEnvTargetName(value)) return value;
|
|
5071
|
+
throw new NaroError(
|
|
5072
|
+
"VALIDATION_FAILED",
|
|
5073
|
+
"--env must be production, preview or an environment name."
|
|
4921
5074
|
);
|
|
5075
|
+
}
|
|
5076
|
+
async function runEnvPull(context) {
|
|
5077
|
+
const file = context.positionals[0] ?? ".env.local";
|
|
5078
|
+
const surface = parsePullSurface(context.flags.env);
|
|
5079
|
+
const client = await context.client();
|
|
5080
|
+
const projectId = await context.resolveProjectId();
|
|
5081
|
+
if (!isReservedEnvTarget(surface))
|
|
5082
|
+
await client.environments.get(projectId, surface);
|
|
5083
|
+
const rows = await client.env.list(projectId, { reveal: true });
|
|
5084
|
+
const vars = pullEnv(rows, surface);
|
|
5085
|
+
const count2 = Object.keys(vars).length;
|
|
5086
|
+
const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
5087
|
+
${serializeDotenv(vars)}`;
|
|
4922
5088
|
const path = resolve4(context.cwd, file);
|
|
4923
5089
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
4924
5090
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
4925
5091
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
4926
5092
|
}
|
|
4927
|
-
writeFileSync4(
|
|
4928
|
-
path,
|
|
4929
|
-
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
4930
|
-
${serializeDotenv(vars)}`,
|
|
4931
|
-
{ mode: 384 }
|
|
4932
|
-
);
|
|
5093
|
+
writeFileSync4(path, text, { mode: 384 });
|
|
4933
5094
|
chmodSync(path, 384);
|
|
4934
|
-
context.output.result(
|
|
4935
|
-
|
|
4936
|
-
|
|
5095
|
+
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5096
|
+
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
5097
|
+
]);
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
// src/commands/environments.ts
|
|
5101
|
+
var CREATE_USAGE2 = "Usage: naro environments create <slug> [--branch <branch>] [--copy-env preview|production|none] [--database schema|empty]";
|
|
5102
|
+
var SET_USAGE2 = "Usage: naro environments set <slug> [--branch <branch> | --no-branch] [--protection on|off]";
|
|
5103
|
+
var DELETE_USAGE = "Usage: naro environments delete <slug> [--yes]";
|
|
5104
|
+
var NO_BRANCH = "\u2014";
|
|
5105
|
+
function parseCopyEnv(value) {
|
|
5106
|
+
if (value === void 0) return void 0;
|
|
5107
|
+
if (value === "preview" || value === "production") return value;
|
|
5108
|
+
if (value === "none") return null;
|
|
5109
|
+
throw new NaroError(
|
|
5110
|
+
"VALIDATION_FAILED",
|
|
5111
|
+
"--copy-env must be preview, production or none."
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
function parseDatabase(value) {
|
|
5115
|
+
if (value === void 0) return void 0;
|
|
5116
|
+
if (value === "schema" || value === "empty") return value;
|
|
5117
|
+
throw new NaroError(
|
|
5118
|
+
"VALIDATION_FAILED",
|
|
5119
|
+
"--database must be schema or empty."
|
|
5120
|
+
);
|
|
5121
|
+
}
|
|
5122
|
+
function parseProtection(value) {
|
|
5123
|
+
if (value === void 0) return void 0;
|
|
5124
|
+
if (value === "on") return true;
|
|
5125
|
+
if (value === "off") return false;
|
|
5126
|
+
throw new NaroError("VALIDATION_FAILED", "--protection must be on or off.");
|
|
5127
|
+
}
|
|
5128
|
+
function table2(rows) {
|
|
5129
|
+
const widths = (rows[0] ?? []).map(
|
|
5130
|
+
(_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length))
|
|
5131
|
+
);
|
|
5132
|
+
return rows.map(
|
|
5133
|
+
(row) => row.map(
|
|
5134
|
+
(cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)
|
|
5135
|
+
).join(" ")
|
|
4937
5136
|
);
|
|
4938
5137
|
}
|
|
5138
|
+
function environmentRow(environment) {
|
|
5139
|
+
return [
|
|
5140
|
+
environment.slug,
|
|
5141
|
+
environment.url,
|
|
5142
|
+
environment.branch ?? NO_BRANCH,
|
|
5143
|
+
environment.protectionEnabled ? "on" : "off"
|
|
5144
|
+
];
|
|
5145
|
+
}
|
|
5146
|
+
async function runEnvironmentsList(context) {
|
|
5147
|
+
const projectId = await context.resolveProjectId();
|
|
5148
|
+
const client = await context.client();
|
|
5149
|
+
const { environments, limit } = await client.environments.list(projectId);
|
|
5150
|
+
const count2 = String(environments.length);
|
|
5151
|
+
context.output.result({ projectId, environments, limit }, [
|
|
5152
|
+
...environments.length === 0 ? ["No environments. Create one with `naro environments create qa`."] : table2([
|
|
5153
|
+
["slug", "url", "branch", "protection"],
|
|
5154
|
+
...environments.map(environmentRow)
|
|
5155
|
+
]),
|
|
5156
|
+
limit === null ? `${count2} environment${environments.length === 1 ? "" : "s"}` : `${count2} of ${String(limit)} environments`
|
|
5157
|
+
]);
|
|
5158
|
+
}
|
|
5159
|
+
async function runEnvironmentsCreate(context) {
|
|
5160
|
+
const [slug] = context.positionalOrThrow(1, CREATE_USAGE2);
|
|
5161
|
+
const { branch, copyEnv, database } = context.flags;
|
|
5162
|
+
const copyEnvFrom = parseCopyEnv(copyEnv);
|
|
5163
|
+
const seed = parseDatabase(database);
|
|
5164
|
+
const projectId = await context.resolveProjectId();
|
|
5165
|
+
const client = await context.client();
|
|
5166
|
+
const environment = await client.environments.create(projectId, {
|
|
5167
|
+
slug,
|
|
5168
|
+
...branch !== void 0 && { branch },
|
|
5169
|
+
// null is a value here (copy nothing): only undefined leaves the key out.
|
|
5170
|
+
...copyEnvFrom !== void 0 && { copyEnvFrom },
|
|
5171
|
+
...seed !== void 0 && { database: seed }
|
|
5172
|
+
});
|
|
5173
|
+
context.output.result({ projectId, environment }, [
|
|
5174
|
+
`\u2705 Created environment ${environment.slug} \u2192 ${environment.url}`
|
|
5175
|
+
]);
|
|
5176
|
+
}
|
|
5177
|
+
async function runEnvironmentsSet(context) {
|
|
5178
|
+
const [slug] = context.positionalOrThrow(1, SET_USAGE2);
|
|
5179
|
+
const { branch, noBranch, protection } = context.flags;
|
|
5180
|
+
if (branch !== void 0 && noBranch === true)
|
|
5181
|
+
throw new NaroError(
|
|
5182
|
+
"VALIDATION_FAILED",
|
|
5183
|
+
"--branch and --no-branch cannot be used together."
|
|
5184
|
+
);
|
|
5185
|
+
const protectionEnabled = parseProtection(protection);
|
|
5186
|
+
if (branch === void 0 && noBranch !== true && protectionEnabled === void 0)
|
|
5187
|
+
throw new NaroError(
|
|
5188
|
+
"VALIDATION_FAILED",
|
|
5189
|
+
"Nothing to change: pass --branch, --no-branch or --protection."
|
|
5190
|
+
);
|
|
5191
|
+
const patch = {
|
|
5192
|
+
...branch !== void 0 && { branch },
|
|
5193
|
+
...noBranch === true && { branch: null },
|
|
5194
|
+
...protectionEnabled !== void 0 && { protectionEnabled }
|
|
5195
|
+
};
|
|
5196
|
+
const projectId = await context.resolveProjectId();
|
|
5197
|
+
if (protectionEnabled === false)
|
|
5198
|
+
await confirmOrAbort(
|
|
5199
|
+
context,
|
|
5200
|
+
`Turn off protection for ${slug}? Anyone with the link can open it.`
|
|
5201
|
+
);
|
|
5202
|
+
const client = await context.client();
|
|
5203
|
+
const found = await client.environments.get(projectId, slug);
|
|
5204
|
+
const environment = await client.environments.update(
|
|
5205
|
+
projectId,
|
|
5206
|
+
found.id,
|
|
5207
|
+
patch
|
|
5208
|
+
);
|
|
5209
|
+
context.output.result({ projectId, environment }, [
|
|
5210
|
+
`\u2705 Updated environment ${slug}`
|
|
5211
|
+
]);
|
|
5212
|
+
}
|
|
5213
|
+
async function runEnvironmentsDelete(context) {
|
|
5214
|
+
const [slug] = context.positionalOrThrow(1, DELETE_USAGE);
|
|
5215
|
+
const projectId = await context.resolveProjectId();
|
|
5216
|
+
await confirmOrAbort(
|
|
5217
|
+
context,
|
|
5218
|
+
`Delete environment ${slug}? Its worker and protection key are removed.`
|
|
5219
|
+
);
|
|
5220
|
+
const client = await context.client();
|
|
5221
|
+
const found = await client.environments.get(projectId, slug);
|
|
5222
|
+
await client.environments.delete(projectId, found.id);
|
|
5223
|
+
context.output.result({ projectId, slug, deleted: true }, [
|
|
5224
|
+
`\u2705 Deleted environment ${slug}`
|
|
5225
|
+
]);
|
|
5226
|
+
}
|
|
4939
5227
|
|
|
4940
5228
|
// src/commands/gen.ts
|
|
4941
5229
|
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
@@ -4954,7 +5242,7 @@ async function runGenTypes(context) {
|
|
|
4954
5242
|
read.policies === void 0 ? {} : { policies: read.policies }
|
|
4955
5243
|
);
|
|
4956
5244
|
const undescribed = tables.flatMap(
|
|
4957
|
-
(
|
|
5245
|
+
(table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
|
|
4958
5246
|
);
|
|
4959
5247
|
for (const { name, error } of undescribed)
|
|
4960
5248
|
context.output.warn(
|
|
@@ -5395,7 +5683,7 @@ function count(n, noun) {
|
|
|
5395
5683
|
}
|
|
5396
5684
|
|
|
5397
5685
|
// src/commands/storage.ts
|
|
5398
|
-
var
|
|
5686
|
+
var CREATE_USAGE3 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
|
|
5399
5687
|
var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]";
|
|
5400
5688
|
var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
|
|
5401
5689
|
var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
|
|
@@ -5535,7 +5823,7 @@ async function runStorageBucketList(context) {
|
|
|
5535
5823
|
);
|
|
5536
5824
|
}
|
|
5537
5825
|
async function runStorageBucketCreate(context) {
|
|
5538
|
-
const name = bucketName(context,
|
|
5826
|
+
const name = bucketName(context, CREATE_USAGE3);
|
|
5539
5827
|
const input = { name, ...bucketFields(context.flags) };
|
|
5540
5828
|
const client = await context.client();
|
|
5541
5829
|
const projectId = await context.resolveProjectId();
|
|
@@ -7152,8 +7440,8 @@ function patternOf(pattern) {
|
|
|
7152
7440
|
}
|
|
7153
7441
|
var MAX_LIMIT = 1e3;
|
|
7154
7442
|
var CAP = `one select reads at most ${String(MAX_LIMIT)} rows, by limit() or range()`;
|
|
7155
|
-
function initialState(
|
|
7156
|
-
return { table:
|
|
7443
|
+
function initialState(table3) {
|
|
7444
|
+
return { table: table3, problem: null, filters: [], columns: "*", order: [] };
|
|
7157
7445
|
}
|
|
7158
7446
|
function copyRow(row) {
|
|
7159
7447
|
return { ...row };
|
|
@@ -7386,11 +7674,11 @@ var SelectBuilder = class extends FilterBuilder {
|
|
|
7386
7674
|
return this.next({ single: "maybe" });
|
|
7387
7675
|
}
|
|
7388
7676
|
build() {
|
|
7389
|
-
const { table:
|
|
7677
|
+
const { table: table3, columns, order, limit, offset, single } = this.state;
|
|
7390
7678
|
return {
|
|
7391
7679
|
version: 1,
|
|
7392
7680
|
action: "select",
|
|
7393
|
-
table:
|
|
7681
|
+
table: table3,
|
|
7394
7682
|
columns: columns === "*" ? "*" : [...columns],
|
|
7395
7683
|
...this.filtersPart(),
|
|
7396
7684
|
...order.length === 0 ? {} : { order: order.map((term) => ({ ...term })) },
|
|
@@ -7406,11 +7694,11 @@ var InsertBuilder = class extends Builder {
|
|
|
7406
7694
|
return this.withReturning(columns);
|
|
7407
7695
|
}
|
|
7408
7696
|
build() {
|
|
7409
|
-
const { table:
|
|
7697
|
+
const { table: table3, rows = [], returning } = this.state;
|
|
7410
7698
|
return {
|
|
7411
7699
|
version: 1,
|
|
7412
7700
|
action: "insert",
|
|
7413
|
-
table:
|
|
7701
|
+
table: table3,
|
|
7414
7702
|
rows: rows.map(copyRow),
|
|
7415
7703
|
...returning === void 0 ? {} : { returning }
|
|
7416
7704
|
};
|
|
@@ -7427,7 +7715,7 @@ var MutationBuilder = class extends FilterBuilder {
|
|
|
7427
7715
|
}
|
|
7428
7716
|
build() {
|
|
7429
7717
|
const {
|
|
7430
|
-
table:
|
|
7718
|
+
table: table3,
|
|
7431
7719
|
action,
|
|
7432
7720
|
values = {},
|
|
7433
7721
|
returning,
|
|
@@ -7438,10 +7726,10 @@ var MutationBuilder = class extends FilterBuilder {
|
|
|
7438
7726
|
...returning === void 0 ? {} : { returning },
|
|
7439
7727
|
...allowUnfiltered === void 0 ? {} : { allowUnfiltered }
|
|
7440
7728
|
};
|
|
7441
|
-
return action === "delete" ? { version: 1, action: "delete", table:
|
|
7729
|
+
return action === "delete" ? { version: 1, action: "delete", table: table3, ...tail } : {
|
|
7442
7730
|
version: 1,
|
|
7443
7731
|
action: "update",
|
|
7444
|
-
table:
|
|
7732
|
+
table: table3,
|
|
7445
7733
|
values: copyRow(values),
|
|
7446
7734
|
...tail
|
|
7447
7735
|
};
|
|
@@ -7450,9 +7738,9 @@ var MutationBuilder = class extends FilterBuilder {
|
|
|
7450
7738
|
var TableBuilder = class {
|
|
7451
7739
|
#run;
|
|
7452
7740
|
#table;
|
|
7453
|
-
constructor(run,
|
|
7741
|
+
constructor(run, table3) {
|
|
7454
7742
|
this.#run = run;
|
|
7455
|
-
this.#table =
|
|
7743
|
+
this.#table = table3;
|
|
7456
7744
|
}
|
|
7457
7745
|
/** `"*"` (the default) or `"id, title"`. */
|
|
7458
7746
|
select(columns) {
|
|
@@ -7830,6 +8118,7 @@ var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (
|
|
|
7830
8118
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
7831
8119
|
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");
|
|
7832
8120
|
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";
|
|
8121
|
+
var isMoved = (value) => isRecord(value) && typeof value.message === "string";
|
|
7833
8122
|
var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
|
|
7834
8123
|
function listOf(guard) {
|
|
7835
8124
|
return (value) => Array.isArray(value) && value.every(guard);
|
|
@@ -7985,9 +8274,76 @@ var StorageFileApi = class {
|
|
|
7985
8274
|
listOf(isFileObject)
|
|
7986
8275
|
);
|
|
7987
8276
|
}
|
|
8277
|
+
/**
|
|
8278
|
+
* The body a move and a copy send (Storage 3-1i): the bucket and both
|
|
8279
|
+
* names, as supabase-js sends them. The names are not checked here — they
|
|
8280
|
+
* are in the body, and the backend answers a bad one `INVALID_REQUEST`
|
|
8281
|
+
* (`details.path` "sourceKey" or "destinationKey"), as `remove` does.
|
|
8282
|
+
*/
|
|
8283
|
+
#transferBody(fromPath, toPath, options) {
|
|
8284
|
+
const { destinationBucket, upsert } = options;
|
|
8285
|
+
return {
|
|
8286
|
+
bucketId: this.#bucket,
|
|
8287
|
+
sourceKey: fromPath,
|
|
8288
|
+
destinationKey: toPath,
|
|
8289
|
+
...destinationBucket === void 0 ? {} : { destinationBucket },
|
|
8290
|
+
...upsert === true ? { upsert: true } : {}
|
|
8291
|
+
};
|
|
8292
|
+
}
|
|
8293
|
+
/**
|
|
8294
|
+
* Renames an object inside its bucket (Storage 3-1i): the same object — its
|
|
8295
|
+
* id and owner — under `toPath`, its old name gone. It never replaces an
|
|
8296
|
+
* object that is there (`OBJECT_EXISTS`), takes files up to 5 GiB − 5 MiB
|
|
8297
|
+
* (`OBJECT_TOO_LARGE`), and needs room in the project for a second copy
|
|
8298
|
+
* until it is done (`STORAGE_QUOTA_EXCEEDED`). Its signed links stop
|
|
8299
|
+
* working at the old name: a link is bound to the name, the object's id
|
|
8300
|
+
* and its bytes, so moving it back can open them again. A backend whose
|
|
8301
|
+
* Storage was turned on before this method came answers `NOT_FOUND`:
|
|
8302
|
+
* update it with `naro backend enable`.
|
|
8303
|
+
*/
|
|
8304
|
+
async move(fromPath, toPath, options = {}) {
|
|
8305
|
+
const bucket = encodeBucket(this.#bucket);
|
|
8306
|
+
if (!bucket.ok) return fail(bucket.error);
|
|
8307
|
+
const { destinationBucket } = options ?? {};
|
|
8308
|
+
return this.#json(
|
|
8309
|
+
{
|
|
8310
|
+
method: "POST",
|
|
8311
|
+
path: "/storage/object/move",
|
|
8312
|
+
body: this.#transferBody(
|
|
8313
|
+
fromPath,
|
|
8314
|
+
toPath,
|
|
8315
|
+
destinationBucket === void 0 ? {} : { destinationBucket }
|
|
8316
|
+
)
|
|
8317
|
+
},
|
|
8318
|
+
isMoved
|
|
8319
|
+
);
|
|
8320
|
+
}
|
|
8321
|
+
/**
|
|
8322
|
+
* Copies an object inside its bucket (Storage 3-1i): the source's bytes,
|
|
8323
|
+
* type and cache-control under `toPath` — a new object of the caller's,
|
|
8324
|
+
* or with `upsert: true` the one that is there, which keeps its owner.
|
|
8325
|
+
* Files up to 5 GiB − 5 MiB, within the bucket's limits as they are now.
|
|
8326
|
+
* It answers `{ id, path, fullPath }` — `path` is the name inside the
|
|
8327
|
+
* bucket; supabase-js's copy puts the bucket in front of it. A backend
|
|
8328
|
+
* whose Storage was turned on before this method came answers
|
|
8329
|
+
* `NOT_FOUND`: update it with `naro backend enable`.
|
|
8330
|
+
*/
|
|
8331
|
+
async copy(fromPath, toPath, options = {}) {
|
|
8332
|
+
const bucket = encodeBucket(this.#bucket);
|
|
8333
|
+
if (!bucket.ok) return fail(bucket.error);
|
|
8334
|
+
return this.#json(
|
|
8335
|
+
{
|
|
8336
|
+
method: "POST",
|
|
8337
|
+
path: "/storage/object/copy",
|
|
8338
|
+
body: this.#transferBody(fromPath, toPath, options ?? {})
|
|
8339
|
+
},
|
|
8340
|
+
isUploadResult
|
|
8341
|
+
);
|
|
8342
|
+
}
|
|
7988
8343
|
/**
|
|
7989
8344
|
* A link to the object for `expiresIn` seconds (1–604,800). It stops
|
|
7990
|
-
* working once the object is replaced or removed
|
|
8345
|
+
* working once the object is replaced or removed, and while the object is
|
|
8346
|
+
* moved away from `path` (moved back, it can open it again).
|
|
7991
8347
|
*/
|
|
7992
8348
|
async createSignedUrl(path, expiresIn, options = {}) {
|
|
7993
8349
|
const target = this.#objectPath("sign/", path);
|
|
@@ -8268,8 +8624,8 @@ var NaroClient2 = class {
|
|
|
8268
8624
|
this.storage = new StorageClient(http, this.auth);
|
|
8269
8625
|
}
|
|
8270
8626
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
|
8271
|
-
from(
|
|
8272
|
-
return new TableBuilder(this.#run,
|
|
8627
|
+
from(table3) {
|
|
8628
|
+
return new TableBuilder(this.#run, table3);
|
|
8273
8629
|
}
|
|
8274
8630
|
};
|
|
8275
8631
|
function createClient(options) {
|
|
@@ -8278,7 +8634,8 @@ function createClient(options) {
|
|
|
8278
8634
|
|
|
8279
8635
|
// src/commands/storage-objects.ts
|
|
8280
8636
|
var NARO_SCHEME = "naro://";
|
|
8281
|
-
var CP_USAGE = "Usage: naro storage cp <from> <to> (
|
|
8637
|
+
var CP_USAGE = "Usage: naro storage cp <from> <to> (a local file and naro://<bucket>/<path>, or two naro://<bucket>/<path> in one bucket)";
|
|
8638
|
+
var MV_USAGE = "Usage: naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>";
|
|
8282
8639
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
8283
8640
|
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
|
|
8284
8641
|
var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
|
|
@@ -8291,6 +8648,10 @@ function isOldSearchRefusal(error) {
|
|
|
8291
8648
|
const { path, reason } = details;
|
|
8292
8649
|
return path === "search" && reason === OLD_SEARCH_REASON;
|
|
8293
8650
|
}
|
|
8651
|
+
var NO_ENDPOINT = "There is no Naro endpoint at this path.";
|
|
8652
|
+
function isOldRouteRefusal(error) {
|
|
8653
|
+
return error !== null && error.code === "NOT_FOUND" && error.message === NO_ENDPOINT;
|
|
8654
|
+
}
|
|
8294
8655
|
var FOUR_GIB = 4294967296;
|
|
8295
8656
|
var PAGE = 1e3;
|
|
8296
8657
|
var MAX_OFFSET = 1e5;
|
|
@@ -8757,17 +9118,106 @@ async function download(context, source, local) {
|
|
|
8757
9118
|
]
|
|
8758
9119
|
);
|
|
8759
9120
|
}
|
|
8760
|
-
|
|
9121
|
+
function unwrapTransfer(result) {
|
|
9122
|
+
if (isOldRouteRefusal(result.error))
|
|
9123
|
+
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
9124
|
+
return unwrap(result);
|
|
9125
|
+
}
|
|
9126
|
+
function refuseBadName(bucket, name) {
|
|
9127
|
+
const route2 = objectRoute(bucket, name);
|
|
9128
|
+
if (!route2.ok) throw new NaroError(route2.error.code, route2.error.message);
|
|
9129
|
+
const checked = checkObjectName(name);
|
|
9130
|
+
if (!checked.ok) {
|
|
9131
|
+
const { code, message } = dataError("INVALID_PATH", {
|
|
9132
|
+
reason: checked.reason
|
|
9133
|
+
});
|
|
9134
|
+
throw new NaroError(code, message);
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
function insideStorage(source, target, command) {
|
|
9138
|
+
if (source.bucket !== target.bucket)
|
|
9139
|
+
throw new NaroError(
|
|
9140
|
+
"VALIDATION_FAILED",
|
|
9141
|
+
"naro storage cp and mv work inside one bucket: both sides must be in naro://<bucket>."
|
|
9142
|
+
);
|
|
9143
|
+
if (source.path === "" || source.path.endsWith("/"))
|
|
9144
|
+
throw new NaroError(
|
|
9145
|
+
"VALIDATION_FAILED",
|
|
9146
|
+
`naro storage ${command} ${command === "mv" ? "moves" : "copies"} one object: name it, naro://<bucket>/<path>.`
|
|
9147
|
+
);
|
|
9148
|
+
const name = source.path.slice(source.path.lastIndexOf("/") + 1);
|
|
9149
|
+
const to = target.path === "" || target.path.endsWith("/") ? `${target.path}${name}` : target.path;
|
|
9150
|
+
refuseBadName(source.bucket, source.path);
|
|
9151
|
+
refuseBadName(source.bucket, to);
|
|
9152
|
+
if (to === source.path)
|
|
9153
|
+
throw new NaroError(
|
|
9154
|
+
"VALIDATION_FAILED",
|
|
9155
|
+
`naro storage ${command} needs a different destination: ${printable(`${NARO_SCHEME}${source.bucket}/${source.path}`)} is the source.`
|
|
9156
|
+
);
|
|
9157
|
+
return { bucket: source.bucket, from: source.path, to };
|
|
9158
|
+
}
|
|
9159
|
+
async function copyInside(context, source, target) {
|
|
9160
|
+
const { contentType, cacheControl } = context.flags;
|
|
9161
|
+
if (contentType !== void 0 || cacheControl !== void 0)
|
|
9162
|
+
throw new NaroError(
|
|
9163
|
+
"VALIDATION_FAILED",
|
|
9164
|
+
"A copy inside Storage keeps the object's type and cache-control: --content-type and --cache-control are for uploads."
|
|
9165
|
+
);
|
|
9166
|
+
if (context.flags.recursive === true)
|
|
9167
|
+
throw new NaroError(
|
|
9168
|
+
"VALIDATION_FAILED",
|
|
9169
|
+
"A copy inside Storage takes no --recursive: it copies one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
|
|
9170
|
+
);
|
|
9171
|
+
const { bucket, from, to } = insideStorage(source, target, "cp");
|
|
9172
|
+
const { projectId, storage } = await objects(context);
|
|
9173
|
+
const object = unwrapTransfer(
|
|
9174
|
+
await storage.from(bucket).copy(from, to, { upsert: context.flags.upsert === true })
|
|
9175
|
+
);
|
|
9176
|
+
const fromUrl = `${NARO_SCHEME}${bucket}/${from}`;
|
|
9177
|
+
const toUrl = `${NARO_SCHEME}${bucket}/${to}`;
|
|
9178
|
+
context.output.result({ projectId, from: fromUrl, to: toUrl, object }, [
|
|
9179
|
+
`\u2705 Copied ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
|
|
9180
|
+
]);
|
|
9181
|
+
}
|
|
9182
|
+
async function storageMv(context) {
|
|
8761
9183
|
const [from, to, ...extra] = context.positionals;
|
|
8762
9184
|
if (from === void 0 || to === void 0 || extra.length > 0)
|
|
8763
|
-
throw new NaroError("VALIDATION_FAILED",
|
|
9185
|
+
throw new NaroError("VALIDATION_FAILED", MV_USAGE);
|
|
8764
9186
|
const source = parseStorageUrl(from);
|
|
8765
9187
|
const target = parseStorageUrl(to);
|
|
8766
|
-
if (source
|
|
9188
|
+
if (source === null || target === null)
|
|
8767
9189
|
throw new NaroError(
|
|
8768
9190
|
"VALIDATION_FAILED",
|
|
8769
|
-
"naro storage
|
|
9191
|
+
"naro storage mv moves inside Storage: both sides are naro://<bucket>/<path>."
|
|
8770
9192
|
);
|
|
9193
|
+
const { upsert, contentType, cacheControl } = context.flags;
|
|
9194
|
+
if (upsert === true || contentType !== void 0 || cacheControl !== void 0)
|
|
9195
|
+
throw new NaroError(
|
|
9196
|
+
"VALIDATION_FAILED",
|
|
9197
|
+
"naro storage mv takes no --upsert, --content-type or --cache-control: a move keeps the object as it is and never replaces one."
|
|
9198
|
+
);
|
|
9199
|
+
if (context.flags.recursive === true)
|
|
9200
|
+
throw new NaroError(
|
|
9201
|
+
"VALIDATION_FAILED",
|
|
9202
|
+
"naro storage mv takes no --recursive: it moves one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
|
|
9203
|
+
);
|
|
9204
|
+
const moved = insideStorage(source, target, "mv");
|
|
9205
|
+
const { projectId, storage } = await objects(context);
|
|
9206
|
+
unwrapTransfer(await storage.from(moved.bucket).move(moved.from, moved.to));
|
|
9207
|
+
const fromUrl = `${NARO_SCHEME}${moved.bucket}/${moved.from}`;
|
|
9208
|
+
const toUrl = `${NARO_SCHEME}${moved.bucket}/${moved.to}`;
|
|
9209
|
+
context.output.result({ projectId, from: fromUrl, to: toUrl }, [
|
|
9210
|
+
`\u2705 Moved ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
|
|
9211
|
+
]);
|
|
9212
|
+
}
|
|
9213
|
+
async function storageCp(context) {
|
|
9214
|
+
const [from, to, ...extra] = context.positionals;
|
|
9215
|
+
if (from === void 0 || to === void 0 || extra.length > 0)
|
|
9216
|
+
throw new NaroError("VALIDATION_FAILED", CP_USAGE);
|
|
9217
|
+
const source = parseStorageUrl(from);
|
|
9218
|
+
const target = parseStorageUrl(to);
|
|
9219
|
+
if (source !== null && target !== null)
|
|
9220
|
+
return copyInside(context, source, target);
|
|
8771
9221
|
if (target !== null) return upload(context, from, target);
|
|
8772
9222
|
if (source !== null) return download(context, source, to);
|
|
8773
9223
|
throw new NaroError("VALIDATION_FAILED", CP_USAGE);
|
|
@@ -8881,6 +9331,9 @@ function runStorageLs(context) {
|
|
|
8881
9331
|
function runStorageCp(context) {
|
|
8882
9332
|
return explainingKey(context, storageCp);
|
|
8883
9333
|
}
|
|
9334
|
+
function runStorageMv(context) {
|
|
9335
|
+
return explainingKey(context, storageMv);
|
|
9336
|
+
}
|
|
8884
9337
|
function runStorageRm(context) {
|
|
8885
9338
|
return explainingKey(context, storageRm);
|
|
8886
9339
|
}
|
|
@@ -8918,15 +9371,16 @@ function meterRow(meter, limitsKnown) {
|
|
|
8918
9371
|
if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
|
|
8919
9372
|
if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
|
|
8920
9373
|
if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
|
|
8921
|
-
return [
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
];
|
|
9374
|
+
return [label3, used, formatCount(meter.limit), usedPercent(meter)];
|
|
9375
|
+
}
|
|
9376
|
+
function usedPercent(meter) {
|
|
9377
|
+
const percent2 = meter.percent ?? 0;
|
|
9378
|
+
return percent2 === 0 && meter.used > 0 ? "<1%" : `${String(percent2)}%`;
|
|
8927
9379
|
}
|
|
8928
9380
|
function projectName(project) {
|
|
8929
9381
|
if (project.deleted) return `Deleted project ${printable(project.projectId)}`;
|
|
9382
|
+
if (project.moved)
|
|
9383
|
+
return `Moved to another team ${printable(project.projectId)}`;
|
|
8930
9384
|
return printable(project.name ?? project.projectId);
|
|
8931
9385
|
}
|
|
8932
9386
|
function usageLines2(report, summary) {
|
|
@@ -8995,6 +9449,7 @@ Usage:
|
|
|
8995
9449
|
|
|
8996
9450
|
naro deploy [--prod] Build locally and deploy (preview by default)
|
|
8997
9451
|
[--root <dir>] [--name <name>] --root points at the app in a monorepo
|
|
9452
|
+
[--env <name>] Deploy to a named environment instead of preview (not with --prod)
|
|
8998
9453
|
naro deployments List deployments of the linked project
|
|
8999
9454
|
naro inspect <deployment-id> Show one deployment + build logs
|
|
9000
9455
|
naro cancel <deployment-id> Stop a build that is still running
|
|
@@ -9020,10 +9475,19 @@ Usage:
|
|
|
9020
9475
|
naro protection enable | disable Protect the preview URL (members only) or open it to anyone with the link (disable asks first; --yes in CI)
|
|
9021
9476
|
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)
|
|
9022
9477
|
|
|
9023
|
-
naro env list List env vars (secrets masked)
|
|
9024
|
-
naro env add KEY [
|
|
9025
|
-
naro env remove KEY [
|
|
9478
|
+
naro env list List env vars (secrets masked); an environment's value shows its name
|
|
9479
|
+
naro env add KEY [target] Add/update (value from prompt or stdin)
|
|
9480
|
+
naro env remove KEY [target] Remove; with no target only production, preview and all, never an environment's
|
|
9481
|
+
naro env import <file> [target] Upload a dotenv file's variables (asks first; --yes in CI)
|
|
9026
9482
|
naro env pull [file] Write env vars to .env.local
|
|
9483
|
+
target is production, preview, all or an environment name; add and import use all when none is given
|
|
9484
|
+
|
|
9485
|
+
naro environments List the project's environments: address, branch, protection, and how many the plan allows
|
|
9486
|
+
naro environments create <slug> [--branch <b>] [--copy-env preview|production|none] [--database schema|empty]
|
|
9487
|
+
A QA or staging surface served at <subdomain>--<slug>; starts with preview's variables
|
|
9488
|
+
naro environments set <slug> [--branch <b> | --no-branch] [--protection on|off]
|
|
9489
|
+
Change the branch whose pushes deploy here, or who can open it (protection off asks first; --yes in CI)
|
|
9490
|
+
naro environments delete <slug> Delete it with its worker and protection key (asks first; --yes in CI)
|
|
9027
9491
|
|
|
9028
9492
|
naro domains list | add <host> [--no-www] | remove <host>
|
|
9029
9493
|
add example.com also connects www.example.com and redirects the apex to
|
|
@@ -9152,7 +9616,10 @@ Usage:
|
|
|
9152
9616
|
One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
|
|
9153
9617
|
with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
|
|
9154
9618
|
A big upload shows its progress on stderr; a file of 4 GiB or more cannot be uploaded yet.
|
|
9155
|
-
A download replaces a local file of the same name
|
|
9619
|
+
A download replaces a local file of the same name.
|
|
9620
|
+
Two naro:// paths in one bucket: a copy inside Storage, up to 5 GiB \u2212 5 MiB (--upsert replaces)
|
|
9621
|
+
naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>
|
|
9622
|
+
Move an object inside its bucket, up to 5 GiB \u2212 5 MiB; it never replaces one that is there
|
|
9156
9623
|
naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
|
|
9157
9624
|
Remove objects; --recursive removes what is under a folder (asks first):
|
|
9158
9625
|
<path>/ is the folder alone, <path> also an object of that name
|
|
@@ -9181,7 +9648,14 @@ Global options:
|
|
|
9181
9648
|
--device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
|
|
9182
9649
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
9183
9650
|
--os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
|
|
9184
|
-
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull: preview by default)
|
|
9651
|
+
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name)
|
|
9652
|
+
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
9653
|
+
branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
|
|
9654
|
+
--no-branch No branch deploys to the environment any more (naro environments set; not with --branch)
|
|
9655
|
+
--copy-env <source> Where a new environment's variables come from: preview (default), production or none (naro environments create)
|
|
9656
|
+
--database <seed> What a new environment's database starts with: schema (default) or empty (naro environments create)
|
|
9657
|
+
--protection <on|off> on: only members of the project can open the environment; off: anyone with the link can
|
|
9658
|
+
(naro environments set; off asks first, --yes in CI)
|
|
9185
9659
|
--no-www Register the apex alone (naro domains add)
|
|
9186
9660
|
--file <path> SQL file to run (naro db query, naro policy create)
|
|
9187
9661
|
--dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
|
|
@@ -9215,8 +9689,8 @@ Global options:
|
|
|
9215
9689
|
--recursive, -r Everything under a folder (naro storage ls, naro storage rm)
|
|
9216
9690
|
--search <text> Names below the prefix that start with <text>, ASCII case ignored (naro storage ls)
|
|
9217
9691
|
--upsert Replace an object that is there (naro storage cp)
|
|
9218
|
-
--content-type <t> The object's type; by default from the file's extension (naro storage cp)
|
|
9219
|
-
--cache-control <seconds> How long a browser may keep
|
|
9692
|
+
--content-type <t> The uploaded object's type; by default from the file's extension (naro storage cp, uploads only)
|
|
9693
|
+
--cache-control <seconds> How long a browser may keep an upload; 3600 by default (naro storage cp, uploads only)
|
|
9220
9694
|
--select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
|
|
9221
9695
|
--owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
|
|
9222
9696
|
--select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
|
|
@@ -9354,6 +9828,8 @@ function routeStorage(words) {
|
|
|
9354
9828
|
return { handler: runStorageLs, positionals: words.slice(1) };
|
|
9355
9829
|
case "cp":
|
|
9356
9830
|
return { handler: runStorageCp, positionals: words.slice(1) };
|
|
9831
|
+
case "mv":
|
|
9832
|
+
return { handler: runStorageMv, positionals: words.slice(1) };
|
|
9357
9833
|
case "rm":
|
|
9358
9834
|
return { handler: runStorageRm, positionals: words.slice(1) };
|
|
9359
9835
|
default:
|
|
@@ -9447,11 +9923,41 @@ function route(parsed) {
|
|
|
9447
9923
|
case "remove":
|
|
9448
9924
|
case "rm":
|
|
9449
9925
|
return { handler: runEnvRemove, positionals: positionals.slice(1) };
|
|
9926
|
+
case "import":
|
|
9927
|
+
return { handler: runEnvImport, positionals: positionals.slice(1) };
|
|
9450
9928
|
case "pull":
|
|
9451
9929
|
return { handler: runEnvPull, positionals: positionals.slice(1) };
|
|
9452
9930
|
default:
|
|
9453
9931
|
return null;
|
|
9454
9932
|
}
|
|
9933
|
+
// Spec 2026-10-18-named-environments §14.2.
|
|
9934
|
+
case "environments":
|
|
9935
|
+
switch (sub) {
|
|
9936
|
+
case void 0:
|
|
9937
|
+
case "list":
|
|
9938
|
+
case "ls":
|
|
9939
|
+
return {
|
|
9940
|
+
handler: runEnvironmentsList,
|
|
9941
|
+
positionals: positionals.slice(1)
|
|
9942
|
+
};
|
|
9943
|
+
case "create":
|
|
9944
|
+
return {
|
|
9945
|
+
handler: runEnvironmentsCreate,
|
|
9946
|
+
positionals: positionals.slice(1)
|
|
9947
|
+
};
|
|
9948
|
+
case "set":
|
|
9949
|
+
return {
|
|
9950
|
+
handler: runEnvironmentsSet,
|
|
9951
|
+
positionals: positionals.slice(1)
|
|
9952
|
+
};
|
|
9953
|
+
case "delete":
|
|
9954
|
+
return {
|
|
9955
|
+
handler: runEnvironmentsDelete,
|
|
9956
|
+
positionals: positionals.slice(1)
|
|
9957
|
+
};
|
|
9958
|
+
default:
|
|
9959
|
+
return null;
|
|
9960
|
+
}
|
|
9455
9961
|
case "domains":
|
|
9456
9962
|
switch (sub) {
|
|
9457
9963
|
case void 0:
|
|
@@ -9617,7 +10123,7 @@ function route(parsed) {
|
|
|
9617
10123
|
function version() {
|
|
9618
10124
|
try {
|
|
9619
10125
|
const pkg = JSON.parse(
|
|
9620
|
-
|
|
10126
|
+
readFileSync6(
|
|
9621
10127
|
join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
9622
10128
|
"utf8"
|
|
9623
10129
|
)
|