naro-cli 0.19.0 → 0.20.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 +313 -13
- package/dist/{chunk-DEEFHVDZ.js → chunk-SAA66XLD.js} +2082 -410
- package/dist/deploy.d.ts +3 -3
- package/dist/deploy.js +2 -2
- package/dist/index.js +415 -42
- package/package.json +5 -5
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,
|
|
@@ -50,7 +53,7 @@ import {
|
|
|
50
53
|
nextMigrationFileName,
|
|
51
54
|
openBrowser,
|
|
52
55
|
parseCacheControl,
|
|
53
|
-
|
|
56
|
+
parseDotenvDetailed,
|
|
54
57
|
parseGitHubRemote,
|
|
55
58
|
parsePolicyNode,
|
|
56
59
|
parsePolicyStatement,
|
|
@@ -60,6 +63,7 @@ import {
|
|
|
60
63
|
policiesForTypes,
|
|
61
64
|
policiesLostByReset,
|
|
62
65
|
projectPath,
|
|
66
|
+
promoteTargetUnsupported,
|
|
63
67
|
promptForMissingEnv,
|
|
64
68
|
pullEnv,
|
|
65
69
|
quoteIdent,
|
|
@@ -71,6 +75,7 @@ import {
|
|
|
71
75
|
resetFileRefusedMessage,
|
|
72
76
|
resetPoliciesLostMessage,
|
|
73
77
|
resetPoliciesWithTablesMessage,
|
|
78
|
+
resetStorageOutdatedMessage,
|
|
74
79
|
resolveAuth,
|
|
75
80
|
resolveMigrationsDir,
|
|
76
81
|
saveSession,
|
|
@@ -80,6 +85,7 @@ import {
|
|
|
80
85
|
snapshotFromRows,
|
|
81
86
|
splitPolicyStatements,
|
|
82
87
|
splitStatements,
|
|
88
|
+
storageRefusalOfFiles,
|
|
83
89
|
storedPolicySql,
|
|
84
90
|
summarizeStatement,
|
|
85
91
|
summarizeTeamUsage,
|
|
@@ -91,7 +97,7 @@ import {
|
|
|
91
97
|
usageMonthOf,
|
|
92
98
|
userForAccessToken,
|
|
93
99
|
writeProjectLink
|
|
94
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-SAA66XLD.js";
|
|
95
101
|
import "./chunk-K3NQKI34.js";
|
|
96
102
|
|
|
97
103
|
// src/args.ts
|
|
@@ -190,7 +196,8 @@ var STRING_FLAGS = {
|
|
|
190
196
|
"--search": "search",
|
|
191
197
|
"--copy-env": "copyEnv",
|
|
192
198
|
"--database": "database",
|
|
193
|
-
"--protection": "protection"
|
|
199
|
+
"--protection": "protection",
|
|
200
|
+
"--to": "to"
|
|
194
201
|
};
|
|
195
202
|
var SECRET_FLAGS = {
|
|
196
203
|
"--api-key": "--api-key-stdin",
|
|
@@ -2644,12 +2651,45 @@ async function hasBackend(client, projectId) {
|
|
|
2644
2651
|
throw error;
|
|
2645
2652
|
}
|
|
2646
2653
|
}
|
|
2647
|
-
async function
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2654
|
+
async function refuseStorageOutdated(client, projectId, storageTable, files, texts) {
|
|
2655
|
+
let refused = storageRefusalOfFiles(texts, {
|
|
2656
|
+
storage: storageTable,
|
|
2657
|
+
storageSubqueries: false
|
|
2658
|
+
});
|
|
2659
|
+
if (refused?.needs === "subqueries" && storageTable) {
|
|
2660
|
+
const answer = await client.db.query(projectId, {
|
|
2661
|
+
sql: STORAGE_SUBQUERIES_SQL
|
|
2662
|
+
});
|
|
2663
|
+
if ((answer.statements[0]?.rows.length ?? 0) > 0)
|
|
2664
|
+
refused = storageRefusalOfFiles(texts, {
|
|
2665
|
+
storage: true,
|
|
2666
|
+
storageSubqueries: true
|
|
2667
|
+
});
|
|
2652
2668
|
}
|
|
2669
|
+
if (refused)
|
|
2670
|
+
throw new NaroError(
|
|
2671
|
+
"BACKEND_OUTDATED",
|
|
2672
|
+
resetStorageOutdatedMessage(files[refused.file] ?? "", refused),
|
|
2673
|
+
{
|
|
2674
|
+
details: {
|
|
2675
|
+
migration: files[refused.file] ?? "",
|
|
2676
|
+
statement: refused.statement
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
async function policyPlan(client, projectId, schema, files, texts) {
|
|
2682
|
+
if (!schema.policyTable && await hasBackend(client, projectId))
|
|
2683
|
+
throw new NaroError("BACKEND_OUTDATED", RESET_BACKEND_OUTDATED);
|
|
2684
|
+
await refuseStorageOutdated(
|
|
2685
|
+
client,
|
|
2686
|
+
projectId,
|
|
2687
|
+
schema.storageTable,
|
|
2688
|
+
files,
|
|
2689
|
+
texts
|
|
2690
|
+
);
|
|
2691
|
+
if (!schema.policyTable)
|
|
2692
|
+
return { clearPolicies: false, lost: [], withTables: [] };
|
|
2653
2693
|
const answer = await client.db.query(projectId, {
|
|
2654
2694
|
sql: NAMED_POLICY_SQL.list
|
|
2655
2695
|
});
|
|
@@ -2678,7 +2718,7 @@ async function policyPlan(client, projectId, schema, dir, files) {
|
|
|
2678
2718
|
...policiesLostByReset({
|
|
2679
2719
|
current,
|
|
2680
2720
|
tables: schema.tables,
|
|
2681
|
-
files:
|
|
2721
|
+
files: texts,
|
|
2682
2722
|
// Storage's table stays; its policies are emptied with the rest (PR
|
|
2683
2723
|
// #85 리뷰). Realtime's messages need no flag: a virtual target, which
|
|
2684
2724
|
// reset-plan counts as a table always there (Realtime 3-2a).
|
|
@@ -2716,6 +2756,7 @@ async function runDbReset(context) {
|
|
|
2716
2756
|
${problemLines.join("\n")}`,
|
|
2717
2757
|
{ details: { problems } }
|
|
2718
2758
|
);
|
|
2759
|
+
const texts = files.map((name) => readFileSync3(join3(dir, name), "utf8"));
|
|
2719
2760
|
const client = await context.client();
|
|
2720
2761
|
const projectId = await context.resolveProjectId();
|
|
2721
2762
|
const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
|
|
@@ -2731,8 +2772,8 @@ ${problemLines.join("\n")}`,
|
|
|
2731
2772
|
client,
|
|
2732
2773
|
projectId,
|
|
2733
2774
|
{ policyTable: policyTable2, storageTable, tables },
|
|
2734
|
-
|
|
2735
|
-
|
|
2775
|
+
files,
|
|
2776
|
+
texts
|
|
2736
2777
|
);
|
|
2737
2778
|
const policiesNotInFiles = policies.lost.map((policy) => ({
|
|
2738
2779
|
name: `${policy.table}.${policy.name}`,
|
|
@@ -4074,13 +4115,16 @@ async function runGitConnect(context, overrides = {}) {
|
|
|
4074
4115
|
if (cannotAsk(context)) {
|
|
4075
4116
|
throw new NaroError(
|
|
4076
4117
|
"REPOSITORY_NOT_ACCESSIBLE",
|
|
4077
|
-
`Naro cannot reach ${fullName} yet.
|
|
4118
|
+
`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`,
|
|
4078
4119
|
{ details: { projectId, installUrl: first.installUrl } }
|
|
4079
4120
|
);
|
|
4080
4121
|
}
|
|
4081
4122
|
output.log(`
|
|
4082
4123
|
\u25B8 Naro needs access to ${fullName}.`);
|
|
4083
4124
|
output.log(` Opening ${first.installUrl}`);
|
|
4125
|
+
output.log(
|
|
4126
|
+
" If the repository is not among the ones the Naro GitHub App was granted on GitHub, add it there first."
|
|
4127
|
+
);
|
|
4084
4128
|
deps.openBrowser(first.installUrl);
|
|
4085
4129
|
output.log(" Waiting for the installation to finish...\n");
|
|
4086
4130
|
await waitForInstall(context, projectId, fullName, deps);
|
|
@@ -4848,6 +4892,7 @@ var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
4848
4892
|
var KEY_MAX_LENGTH = 256;
|
|
4849
4893
|
var IMPORT_BATCH_SIZE = 100;
|
|
4850
4894
|
var VALUE_MAX_BYTES = 5 * 1024;
|
|
4895
|
+
var STATIC_ASSETS_BINDING = "ASSETS";
|
|
4851
4896
|
var TARGET_USAGE = "[production|preview|all|<environment>]";
|
|
4852
4897
|
function validKey(key) {
|
|
4853
4898
|
return KEY_RE.test(key) && key.length <= KEY_MAX_LENGTH;
|
|
@@ -4960,7 +5005,14 @@ async function runEnvImport(context) {
|
|
|
4960
5005
|
} catch {
|
|
4961
5006
|
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
4962
5007
|
}
|
|
4963
|
-
const
|
|
5008
|
+
const parsed = parseDotenvDetailed(text);
|
|
5009
|
+
const [unclosed] = parsed.unclosedQuotes;
|
|
5010
|
+
if (unclosed !== void 0)
|
|
5011
|
+
throw new NaroError(
|
|
5012
|
+
"VALIDATION_FAILED",
|
|
5013
|
+
`Unclosed quote in ${file} at line ${String(unclosed)}.`
|
|
5014
|
+
);
|
|
5015
|
+
const vars = Object.entries(parsed.vars);
|
|
4964
5016
|
if (vars.length === 0)
|
|
4965
5017
|
throw new NaroError("VALIDATION_FAILED", `No variables found in ${file}.`);
|
|
4966
5018
|
const invalid = vars.map(([key]) => key).filter((key) => !validKey(key));
|
|
@@ -4969,6 +5021,11 @@ async function runEnvImport(context) {
|
|
|
4969
5021
|
"VALIDATION_FAILED",
|
|
4970
5022
|
`Invalid keys in ${file}: ${invalid.join(", ")}.`
|
|
4971
5023
|
);
|
|
5024
|
+
if (vars.some(([key]) => key === STATIC_ASSETS_BINDING))
|
|
5025
|
+
throw new NaroError(
|
|
5026
|
+
"VALIDATION_FAILED",
|
|
5027
|
+
`${STATIC_ASSETS_BINDING} is reserved for the project's static files; pick another name.`
|
|
5028
|
+
);
|
|
4972
5029
|
const oversized = vars.filter(([, value]) => Buffer.byteLength(value, "utf8") > VALUE_MAX_BYTES).map(([key]) => key);
|
|
4973
5030
|
if (oversized.length > 0)
|
|
4974
5031
|
throw new NaroError(
|
|
@@ -4978,7 +5035,9 @@ async function runEnvImport(context) {
|
|
|
4978
5035
|
const projectId = await context.resolveProjectId();
|
|
4979
5036
|
context.output.log(`Variables in ${file}:`);
|
|
4980
5037
|
for (const [key, value] of vars)
|
|
4981
|
-
context.output.log(
|
|
5038
|
+
context.output.log(
|
|
5039
|
+
` ${key.padEnd(32)} ${value === "" ? "(empty)" : maskEnvValue(key, value)}`
|
|
5040
|
+
);
|
|
4982
5041
|
await confirmOrAbort(
|
|
4983
5042
|
context,
|
|
4984
5043
|
`Upload ${variables(vars.length)} to ${target}?`
|
|
@@ -4990,6 +5049,15 @@ async function runEnvImport(context) {
|
|
|
4990
5049
|
target,
|
|
4991
5050
|
isSecret: isSecretKey(key)
|
|
4992
5051
|
}));
|
|
5052
|
+
if (entries.length > IMPORT_BATCH_SIZE) {
|
|
5053
|
+
const taken = new Set(await client.env.takenBindingNames(projectId));
|
|
5054
|
+
const conflict = entries.find((entry) => taken.has(entry.key));
|
|
5055
|
+
if (conflict)
|
|
5056
|
+
throw new NaroError(
|
|
5057
|
+
"VALIDATION_FAILED",
|
|
5058
|
+
`${conflict.key} is already used by a storage binding.`
|
|
5059
|
+
);
|
|
5060
|
+
}
|
|
4993
5061
|
for (let start = 0; start < entries.length; start += IMPORT_BATCH_SIZE)
|
|
4994
5062
|
await client.env.set(
|
|
4995
5063
|
projectId,
|
|
@@ -5017,17 +5085,14 @@ async function runEnvPull(context) {
|
|
|
5017
5085
|
const rows = await client.env.list(projectId, { reveal: true });
|
|
5018
5086
|
const vars = pullEnv(rows, surface);
|
|
5019
5087
|
const count2 = Object.keys(vars).length;
|
|
5088
|
+
const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
5089
|
+
${serializeDotenv(vars)}`;
|
|
5020
5090
|
const path = resolve4(context.cwd, file);
|
|
5021
5091
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
5022
5092
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
5023
5093
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
5024
5094
|
}
|
|
5025
|
-
writeFileSync4(
|
|
5026
|
-
path,
|
|
5027
|
-
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
5028
|
-
${serializeDotenv(vars)}`,
|
|
5029
|
-
{ mode: 384 }
|
|
5030
|
-
);
|
|
5095
|
+
writeFileSync4(path, text, { mode: 384 });
|
|
5031
5096
|
chmodSync(path, 384);
|
|
5032
5097
|
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5033
5098
|
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
@@ -5460,17 +5525,27 @@ async function runProjectInspect(context) {
|
|
|
5460
5525
|
}
|
|
5461
5526
|
|
|
5462
5527
|
// src/commands/promote.ts
|
|
5463
|
-
|
|
5528
|
+
var PROMOTE_USAGE = "Usage: naro promote <deployment-id> [--to <environment>]";
|
|
5529
|
+
var ROLLBACK_USAGE = "Usage: naro rollback [deployment-id]";
|
|
5530
|
+
var REBUILD_TIMEOUT_MS = 15 * 6e4;
|
|
5531
|
+
var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
5532
|
+
async function promoteAndWait(context, client, project, sourceDeploymentId, action, target) {
|
|
5464
5533
|
const promoted = await client.deployments.promote(
|
|
5465
5534
|
project.id,
|
|
5466
|
-
sourceDeploymentId
|
|
5535
|
+
sourceDeploymentId,
|
|
5536
|
+
target !== void 0 ? { target } : void 0
|
|
5467
5537
|
);
|
|
5538
|
+
const rebuild = promoted.mode === "rebuild";
|
|
5468
5539
|
context.output.log(
|
|
5469
|
-
`\u25B8 ${action === "promote" ? "promoting" : "rolling back to"} ${sourceDeploymentId}`
|
|
5540
|
+
`\u25B8 ${action === "promote" ? "promoting" : "rolling back to"} ${sourceDeploymentId}${target !== void 0 ? ` to ${target} (${promoted.mode})` : ""}`
|
|
5541
|
+
);
|
|
5542
|
+
const detail = await client.deployments.wait(
|
|
5543
|
+
promoted.id,
|
|
5544
|
+
rebuild ? {
|
|
5545
|
+
timeoutMs: REBUILD_TIMEOUT_MS,
|
|
5546
|
+
onLog: (log) => context.output.log(formatBuildLog(log))
|
|
5547
|
+
} : { timeoutMs: REUPLOAD_TIMEOUT_MS }
|
|
5470
5548
|
);
|
|
5471
|
-
const detail = await client.deployments.wait(promoted.id, {
|
|
5472
|
-
timeoutMs: 5 * 6e4
|
|
5473
|
-
});
|
|
5474
5549
|
if (detail.deployment.status !== "READY") {
|
|
5475
5550
|
throw new NaroError(
|
|
5476
5551
|
detail.deployment.errorCode ?? "DEPLOY_FAILED",
|
|
@@ -5478,42 +5553,104 @@ async function promoteAndWait(context, client, project, sourceDeploymentId, acti
|
|
|
5478
5553
|
{ details: { deploymentId: promoted.id } }
|
|
5479
5554
|
);
|
|
5480
5555
|
}
|
|
5556
|
+
const serving = target === void 0 || target === "production" ? "Production" : target;
|
|
5481
5557
|
context.output.result(
|
|
5482
5558
|
{
|
|
5483
5559
|
deploymentId: promoted.id,
|
|
5484
5560
|
sourceDeploymentId,
|
|
5485
5561
|
projectId: project.id,
|
|
5562
|
+
// Both say the mode the control plane answered (MCP's rollback does
|
|
5563
|
+
// too). Only a promotion has a target: a rollback is production's.
|
|
5564
|
+
...action === "promote" ? { target: target ?? null } : {},
|
|
5565
|
+
mode: promoted.mode,
|
|
5486
5566
|
status: detail.deployment.status,
|
|
5487
5567
|
url: detail.deployment.url
|
|
5488
5568
|
},
|
|
5489
5569
|
[
|
|
5490
|
-
`\u2705
|
|
5570
|
+
`\u2705 ${serving} now serves ${rebuild ? "a rebuild of " : ""}${sourceDeploymentId}`,
|
|
5491
5571
|
"",
|
|
5492
5572
|
detail.deployment.url ?? ""
|
|
5493
5573
|
]
|
|
5494
5574
|
);
|
|
5495
5575
|
}
|
|
5576
|
+
function promoteTarget(flags) {
|
|
5577
|
+
if (flags.to === void 0) return void 0;
|
|
5578
|
+
const target = flags.to.trim();
|
|
5579
|
+
if (target === "" || target === "preview")
|
|
5580
|
+
throw new NaroError(
|
|
5581
|
+
"VALIDATION_FAILED",
|
|
5582
|
+
"--to must be production or an environment name."
|
|
5583
|
+
);
|
|
5584
|
+
return target;
|
|
5585
|
+
}
|
|
5586
|
+
function refuseWhatIsNotRead(context, usage, envMessage) {
|
|
5587
|
+
if (context.positionals.length > 1)
|
|
5588
|
+
throw new NaroError("VALIDATION_FAILED", usage);
|
|
5589
|
+
if (context.flags.env !== void 0)
|
|
5590
|
+
throw new NaroError("VALIDATION_FAILED", envMessage);
|
|
5591
|
+
}
|
|
5592
|
+
async function previewPromotion(context, client, projectId, sourceDeploymentId, target) {
|
|
5593
|
+
let check;
|
|
5594
|
+
try {
|
|
5595
|
+
check = await client.deployments.promoteCheck(
|
|
5596
|
+
projectId,
|
|
5597
|
+
sourceDeploymentId,
|
|
5598
|
+
target
|
|
5599
|
+
);
|
|
5600
|
+
} catch (error) {
|
|
5601
|
+
if (!isNaroError(error) || error.status !== 405) throw error;
|
|
5602
|
+
if (target !== void 0 && target !== "production")
|
|
5603
|
+
throw promoteTargetUnsupported(error);
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
context.output.log(
|
|
5607
|
+
check.mode === "rebuild" ? `\u25B8 rebuild: builds the same commit again with ${target ?? "production"}'s variables` : "\u25B8 reupload: publishes the stored build"
|
|
5608
|
+
);
|
|
5609
|
+
for (const warning of check.warnings)
|
|
5610
|
+
for (const line of warningLines(warning)) context.output.log(line);
|
|
5611
|
+
}
|
|
5496
5612
|
async function runPromote(context) {
|
|
5497
|
-
const [deploymentId] = context.positionalOrThrow(
|
|
5498
|
-
|
|
5499
|
-
|
|
5613
|
+
const [deploymentId] = context.positionalOrThrow(1, PROMOTE_USAGE);
|
|
5614
|
+
refuseWhatIsNotRead(
|
|
5615
|
+
context,
|
|
5616
|
+
PROMOTE_USAGE,
|
|
5617
|
+
"--env is not an option of naro promote: use --to <environment>."
|
|
5500
5618
|
);
|
|
5619
|
+
const target = promoteTarget(context.flags);
|
|
5501
5620
|
const client = await context.client();
|
|
5502
5621
|
const projectId = await context.resolveProjectId();
|
|
5503
5622
|
const project = await client.projects.get(projectId);
|
|
5623
|
+
await previewPromotion(
|
|
5624
|
+
context,
|
|
5625
|
+
client,
|
|
5626
|
+
projectId,
|
|
5627
|
+
deploymentId,
|
|
5628
|
+
target
|
|
5629
|
+
);
|
|
5504
5630
|
await confirmOrAbort(
|
|
5505
5631
|
context,
|
|
5506
|
-
`Promote ${String(deploymentId)} to production (${project.subdomain})?`
|
|
5632
|
+
target === void 0 || target === "production" ? `Promote ${String(deploymentId)} to production (${project.subdomain})?` : `Promote ${String(deploymentId)} to ${target} (${project.subdomain}--${target})?`
|
|
5507
5633
|
);
|
|
5508
5634
|
await promoteAndWait(
|
|
5509
5635
|
context,
|
|
5510
5636
|
client,
|
|
5511
5637
|
project,
|
|
5512
5638
|
deploymentId,
|
|
5513
|
-
"promote"
|
|
5639
|
+
"promote",
|
|
5640
|
+
target
|
|
5514
5641
|
);
|
|
5515
5642
|
}
|
|
5516
5643
|
async function runRollback(context) {
|
|
5644
|
+
if (context.flags.to !== void 0)
|
|
5645
|
+
throw new NaroError(
|
|
5646
|
+
"VALIDATION_FAILED",
|
|
5647
|
+
"--to is for naro promote: naro rollback always rolls production back."
|
|
5648
|
+
);
|
|
5649
|
+
refuseWhatIsNotRead(
|
|
5650
|
+
context,
|
|
5651
|
+
ROLLBACK_USAGE,
|
|
5652
|
+
"--env is not an option of naro rollback: naro rollback always rolls production back."
|
|
5653
|
+
);
|
|
5517
5654
|
const client = await context.client();
|
|
5518
5655
|
const projectId = await context.resolveProjectId();
|
|
5519
5656
|
const project = await client.projects.get(projectId);
|
|
@@ -5625,6 +5762,7 @@ var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --priva
|
|
|
5625
5762
|
var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
|
|
5626
5763
|
var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
|
|
5627
5764
|
var ENABLE_FIRST = "Storage is not enabled yet; enabling it first.";
|
|
5765
|
+
var DISABLE_QUESTION3 = "Turn Storage off for this project? Its Storage calls and file URLs answer 404 until naro storage enable; buckets and files are kept.";
|
|
5628
5766
|
var NO_BUCKETS = "No buckets yet. `naro storage bucket create <name>` makes one.";
|
|
5629
5767
|
var IDLE_PAGES = 3;
|
|
5630
5768
|
function bucketLine(bucket) {
|
|
@@ -5645,6 +5783,12 @@ function utcClock(iso) {
|
|
|
5645
5783
|
const at = new Date(iso);
|
|
5646
5784
|
return Number.isNaN(at.getTime()) ? null : at.toISOString().slice(11, 16);
|
|
5647
5785
|
}
|
|
5786
|
+
function utcMinute(iso) {
|
|
5787
|
+
const at = new Date(iso);
|
|
5788
|
+
if (Number.isNaN(at.getTime())) return printable(iso);
|
|
5789
|
+
const text = at.toISOString();
|
|
5790
|
+
return `${text.slice(0, 10)} ${text.slice(11, 16)} UTC`;
|
|
5791
|
+
}
|
|
5648
5792
|
function warnedPercent(value, limit) {
|
|
5649
5793
|
if (limit === null || !(limit > 0)) return null;
|
|
5650
5794
|
const percent2 = Math.floor(value * 100 / limit);
|
|
@@ -5686,6 +5830,13 @@ function meterLines(info) {
|
|
|
5686
5830
|
return lines;
|
|
5687
5831
|
}
|
|
5688
5832
|
function storageLines(info) {
|
|
5833
|
+
if (info.disabledAt !== null)
|
|
5834
|
+
return [
|
|
5835
|
+
`Storage: off since ${utcMinute(info.disabledAt)} \u2014 buckets and files are kept`,
|
|
5836
|
+
"Run `naro storage enable` to turn it on again: the same buckets and files come back.",
|
|
5837
|
+
info.buckets.length === 0 ? "Buckets: none" : `Buckets: ${String(info.buckets.length)}`,
|
|
5838
|
+
...info.buckets.map(bucketLine)
|
|
5839
|
+
];
|
|
5689
5840
|
if (!info.enabled)
|
|
5690
5841
|
return [
|
|
5691
5842
|
"Storage: off",
|
|
@@ -5714,6 +5865,19 @@ async function enableStorage(client, projectId) {
|
|
|
5714
5865
|
throw error;
|
|
5715
5866
|
}
|
|
5716
5867
|
}
|
|
5868
|
+
async function disableStorage(client, projectId) {
|
|
5869
|
+
try {
|
|
5870
|
+
return await client.storage.disable(projectId);
|
|
5871
|
+
} catch (error) {
|
|
5872
|
+
if (isNaroError(error) && error.code === "TIMEOUT")
|
|
5873
|
+
throw new NaroError(
|
|
5874
|
+
"TIMEOUT",
|
|
5875
|
+
"Turning Storage off did not answer within 2 minutes. `naro storage` shows where it got to, and `naro storage disable` again picks up where it stopped.",
|
|
5876
|
+
{ cause: error }
|
|
5877
|
+
);
|
|
5878
|
+
throw error;
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5717
5881
|
function bucketName(context, usage) {
|
|
5718
5882
|
if (context.positionals.length !== 1)
|
|
5719
5883
|
throw new NaroError("VALIDATION_FAILED", usage);
|
|
@@ -5750,6 +5914,24 @@ async function runStorageEnable(context) {
|
|
|
5750
5914
|
...storageLines(storage)
|
|
5751
5915
|
]);
|
|
5752
5916
|
}
|
|
5917
|
+
async function runStorageDisable(context) {
|
|
5918
|
+
const client = await context.client();
|
|
5919
|
+
const projectId = await context.resolveProjectId();
|
|
5920
|
+
const current = await client.storage.info(projectId);
|
|
5921
|
+
if (current.bucket === null) {
|
|
5922
|
+
context.output.result({ projectId, storage: current }, [
|
|
5923
|
+
"Storage is already off."
|
|
5924
|
+
]);
|
|
5925
|
+
return;
|
|
5926
|
+
}
|
|
5927
|
+
if (current.disabledAt === null)
|
|
5928
|
+
await confirmOrAbort(context, DISABLE_QUESTION3);
|
|
5929
|
+
const storage = await disableStorage(client, projectId);
|
|
5930
|
+
context.output.result({ projectId, storage }, [
|
|
5931
|
+
"\u2705 Storage is off. Buckets and files are kept; naro storage enable turns it on again.",
|
|
5932
|
+
...storageLines(storage)
|
|
5933
|
+
]);
|
|
5934
|
+
}
|
|
5753
5935
|
async function runStorageBucketList(context) {
|
|
5754
5936
|
const client = await context.client();
|
|
5755
5937
|
const projectId = await context.resolveProjectId();
|
|
@@ -8055,6 +8237,7 @@ var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (
|
|
|
8055
8237
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
8056
8238
|
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");
|
|
8057
8239
|
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";
|
|
8240
|
+
var isMoved = (value) => isRecord(value) && typeof value.message === "string";
|
|
8058
8241
|
var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
|
|
8059
8242
|
function listOf(guard) {
|
|
8060
8243
|
return (value) => Array.isArray(value) && value.every(guard);
|
|
@@ -8210,9 +8393,76 @@ var StorageFileApi = class {
|
|
|
8210
8393
|
listOf(isFileObject)
|
|
8211
8394
|
);
|
|
8212
8395
|
}
|
|
8396
|
+
/**
|
|
8397
|
+
* The body a move and a copy send (Storage 3-1i): the bucket and both
|
|
8398
|
+
* names, as supabase-js sends them. The names are not checked here — they
|
|
8399
|
+
* are in the body, and the backend answers a bad one `INVALID_REQUEST`
|
|
8400
|
+
* (`details.path` "sourceKey" or "destinationKey"), as `remove` does.
|
|
8401
|
+
*/
|
|
8402
|
+
#transferBody(fromPath, toPath, options) {
|
|
8403
|
+
const { destinationBucket, upsert } = options;
|
|
8404
|
+
return {
|
|
8405
|
+
bucketId: this.#bucket,
|
|
8406
|
+
sourceKey: fromPath,
|
|
8407
|
+
destinationKey: toPath,
|
|
8408
|
+
...destinationBucket === void 0 ? {} : { destinationBucket },
|
|
8409
|
+
...upsert === true ? { upsert: true } : {}
|
|
8410
|
+
};
|
|
8411
|
+
}
|
|
8412
|
+
/**
|
|
8413
|
+
* Renames an object inside its bucket (Storage 3-1i): the same object — its
|
|
8414
|
+
* id and owner — under `toPath`, its old name gone. It never replaces an
|
|
8415
|
+
* object that is there (`OBJECT_EXISTS`), takes files up to 5 GiB − 5 MiB
|
|
8416
|
+
* (`OBJECT_TOO_LARGE`), and needs room in the project for a second copy
|
|
8417
|
+
* until it is done (`STORAGE_QUOTA_EXCEEDED`). Its signed links stop
|
|
8418
|
+
* working at the old name: a link is bound to the name, the object's id
|
|
8419
|
+
* and its bytes, so moving it back can open them again. A backend whose
|
|
8420
|
+
* Storage was turned on before this method came answers `NOT_FOUND`:
|
|
8421
|
+
* update it with `naro backend enable`.
|
|
8422
|
+
*/
|
|
8423
|
+
async move(fromPath, toPath, options = {}) {
|
|
8424
|
+
const bucket = encodeBucket(this.#bucket);
|
|
8425
|
+
if (!bucket.ok) return fail(bucket.error);
|
|
8426
|
+
const { destinationBucket } = options ?? {};
|
|
8427
|
+
return this.#json(
|
|
8428
|
+
{
|
|
8429
|
+
method: "POST",
|
|
8430
|
+
path: "/storage/object/move",
|
|
8431
|
+
body: this.#transferBody(
|
|
8432
|
+
fromPath,
|
|
8433
|
+
toPath,
|
|
8434
|
+
destinationBucket === void 0 ? {} : { destinationBucket }
|
|
8435
|
+
)
|
|
8436
|
+
},
|
|
8437
|
+
isMoved
|
|
8438
|
+
);
|
|
8439
|
+
}
|
|
8440
|
+
/**
|
|
8441
|
+
* Copies an object inside its bucket (Storage 3-1i): the source's bytes,
|
|
8442
|
+
* type and cache-control under `toPath` — a new object of the caller's,
|
|
8443
|
+
* or with `upsert: true` the one that is there, which keeps its owner.
|
|
8444
|
+
* Files up to 5 GiB − 5 MiB, within the bucket's limits as they are now.
|
|
8445
|
+
* It answers `{ id, path, fullPath }` — `path` is the name inside the
|
|
8446
|
+
* bucket; supabase-js's copy puts the bucket in front of it. A backend
|
|
8447
|
+
* whose Storage was turned on before this method came answers
|
|
8448
|
+
* `NOT_FOUND`: update it with `naro backend enable`.
|
|
8449
|
+
*/
|
|
8450
|
+
async copy(fromPath, toPath, options = {}) {
|
|
8451
|
+
const bucket = encodeBucket(this.#bucket);
|
|
8452
|
+
if (!bucket.ok) return fail(bucket.error);
|
|
8453
|
+
return this.#json(
|
|
8454
|
+
{
|
|
8455
|
+
method: "POST",
|
|
8456
|
+
path: "/storage/object/copy",
|
|
8457
|
+
body: this.#transferBody(fromPath, toPath, options ?? {})
|
|
8458
|
+
},
|
|
8459
|
+
isUploadResult
|
|
8460
|
+
);
|
|
8461
|
+
}
|
|
8213
8462
|
/**
|
|
8214
8463
|
* A link to the object for `expiresIn` seconds (1–604,800). It stops
|
|
8215
|
-
* working once the object is replaced or removed
|
|
8464
|
+
* working once the object is replaced or removed, and while the object is
|
|
8465
|
+
* moved away from `path` (moved back, it can open it again).
|
|
8216
8466
|
*/
|
|
8217
8467
|
async createSignedUrl(path, expiresIn, options = {}) {
|
|
8218
8468
|
const target = this.#objectPath("sign/", path);
|
|
@@ -8503,10 +8753,12 @@ function createClient(options) {
|
|
|
8503
8753
|
|
|
8504
8754
|
// src/commands/storage-objects.ts
|
|
8505
8755
|
var NARO_SCHEME = "naro://";
|
|
8506
|
-
var CP_USAGE = "Usage: naro storage cp <from> <to> (
|
|
8756
|
+
var CP_USAGE = "Usage: naro storage cp <from> <to> (a local file and naro://<bucket>/<path>, or two naro://<bucket>/<path> in one bucket)";
|
|
8757
|
+
var MV_USAGE = "Usage: naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>";
|
|
8507
8758
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
8508
8759
|
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
|
|
8509
8760
|
var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
|
|
8761
|
+
var STORAGE_OFF_MESSAGE = "Storage is turned off for this project. Turn it on with `naro storage enable` first.";
|
|
8510
8762
|
var STORAGE_OUTDATED_MESSAGE = "The project backend is older than this Storage command. Run `naro backend enable` to update it, then try again.";
|
|
8511
8763
|
var OLD_SEARCH_REASON = "search is not supported yet";
|
|
8512
8764
|
function isOldSearchRefusal(error) {
|
|
@@ -8516,6 +8768,10 @@ function isOldSearchRefusal(error) {
|
|
|
8516
8768
|
const { path, reason } = details;
|
|
8517
8769
|
return path === "search" && reason === OLD_SEARCH_REASON;
|
|
8518
8770
|
}
|
|
8771
|
+
var NO_ENDPOINT = "There is no Naro endpoint at this path.";
|
|
8772
|
+
function isOldRouteRefusal(error) {
|
|
8773
|
+
return error !== null && error.code === "NOT_FOUND" && error.message === NO_ENDPOINT;
|
|
8774
|
+
}
|
|
8519
8775
|
var FOUR_GIB = 4294967296;
|
|
8520
8776
|
var PAGE = 1e3;
|
|
8521
8777
|
var MAX_OFFSET = 1e5;
|
|
@@ -8595,6 +8851,9 @@ async function objects(context) {
|
|
|
8595
8851
|
"This project has no backend yet. Run `naro backend enable`, then `naro storage enable`."
|
|
8596
8852
|
);
|
|
8597
8853
|
if (!backend.enabled) throw new NaroError("BACKEND_OFF", BACKEND_OFF_MESSAGE);
|
|
8854
|
+
const state = await client.storage.info(projectId);
|
|
8855
|
+
if (state.disabledAt !== null)
|
|
8856
|
+
throw new NaroError("STORAGE_OFF", STORAGE_OFF_MESSAGE);
|
|
8598
8857
|
let serviceKey = process.env.NARO_SERVICE_KEY?.trim() ?? "";
|
|
8599
8858
|
if (serviceKey !== "" && !ONE_KEY.test(serviceKey))
|
|
8600
8859
|
throw new NaroError(
|
|
@@ -8982,17 +9241,106 @@ async function download(context, source, local) {
|
|
|
8982
9241
|
]
|
|
8983
9242
|
);
|
|
8984
9243
|
}
|
|
8985
|
-
|
|
9244
|
+
function unwrapTransfer(result) {
|
|
9245
|
+
if (isOldRouteRefusal(result.error))
|
|
9246
|
+
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
9247
|
+
return unwrap(result);
|
|
9248
|
+
}
|
|
9249
|
+
function refuseBadName(bucket, name) {
|
|
9250
|
+
const route2 = objectRoute(bucket, name);
|
|
9251
|
+
if (!route2.ok) throw new NaroError(route2.error.code, route2.error.message);
|
|
9252
|
+
const checked = checkObjectName(name);
|
|
9253
|
+
if (!checked.ok) {
|
|
9254
|
+
const { code, message } = dataError("INVALID_PATH", {
|
|
9255
|
+
reason: checked.reason
|
|
9256
|
+
});
|
|
9257
|
+
throw new NaroError(code, message);
|
|
9258
|
+
}
|
|
9259
|
+
}
|
|
9260
|
+
function insideStorage(source, target, command) {
|
|
9261
|
+
if (source.bucket !== target.bucket)
|
|
9262
|
+
throw new NaroError(
|
|
9263
|
+
"VALIDATION_FAILED",
|
|
9264
|
+
"naro storage cp and mv work inside one bucket: both sides must be in naro://<bucket>."
|
|
9265
|
+
);
|
|
9266
|
+
if (source.path === "" || source.path.endsWith("/"))
|
|
9267
|
+
throw new NaroError(
|
|
9268
|
+
"VALIDATION_FAILED",
|
|
9269
|
+
`naro storage ${command} ${command === "mv" ? "moves" : "copies"} one object: name it, naro://<bucket>/<path>.`
|
|
9270
|
+
);
|
|
9271
|
+
const name = source.path.slice(source.path.lastIndexOf("/") + 1);
|
|
9272
|
+
const to = target.path === "" || target.path.endsWith("/") ? `${target.path}${name}` : target.path;
|
|
9273
|
+
refuseBadName(source.bucket, source.path);
|
|
9274
|
+
refuseBadName(source.bucket, to);
|
|
9275
|
+
if (to === source.path)
|
|
9276
|
+
throw new NaroError(
|
|
9277
|
+
"VALIDATION_FAILED",
|
|
9278
|
+
`naro storage ${command} needs a different destination: ${printable(`${NARO_SCHEME}${source.bucket}/${source.path}`)} is the source.`
|
|
9279
|
+
);
|
|
9280
|
+
return { bucket: source.bucket, from: source.path, to };
|
|
9281
|
+
}
|
|
9282
|
+
async function copyInside(context, source, target) {
|
|
9283
|
+
const { contentType, cacheControl } = context.flags;
|
|
9284
|
+
if (contentType !== void 0 || cacheControl !== void 0)
|
|
9285
|
+
throw new NaroError(
|
|
9286
|
+
"VALIDATION_FAILED",
|
|
9287
|
+
"A copy inside Storage keeps the object's type and cache-control: --content-type and --cache-control are for uploads."
|
|
9288
|
+
);
|
|
9289
|
+
if (context.flags.recursive === true)
|
|
9290
|
+
throw new NaroError(
|
|
9291
|
+
"VALIDATION_FAILED",
|
|
9292
|
+
"A copy inside Storage takes no --recursive: it copies one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
|
|
9293
|
+
);
|
|
9294
|
+
const { bucket, from, to } = insideStorage(source, target, "cp");
|
|
9295
|
+
const { projectId, storage } = await objects(context);
|
|
9296
|
+
const object = unwrapTransfer(
|
|
9297
|
+
await storage.from(bucket).copy(from, to, { upsert: context.flags.upsert === true })
|
|
9298
|
+
);
|
|
9299
|
+
const fromUrl = `${NARO_SCHEME}${bucket}/${from}`;
|
|
9300
|
+
const toUrl = `${NARO_SCHEME}${bucket}/${to}`;
|
|
9301
|
+
context.output.result({ projectId, from: fromUrl, to: toUrl, object }, [
|
|
9302
|
+
`\u2705 Copied ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
|
|
9303
|
+
]);
|
|
9304
|
+
}
|
|
9305
|
+
async function storageMv(context) {
|
|
8986
9306
|
const [from, to, ...extra] = context.positionals;
|
|
8987
9307
|
if (from === void 0 || to === void 0 || extra.length > 0)
|
|
8988
|
-
throw new NaroError("VALIDATION_FAILED",
|
|
9308
|
+
throw new NaroError("VALIDATION_FAILED", MV_USAGE);
|
|
8989
9309
|
const source = parseStorageUrl(from);
|
|
8990
9310
|
const target = parseStorageUrl(to);
|
|
8991
|
-
if (source
|
|
9311
|
+
if (source === null || target === null)
|
|
9312
|
+
throw new NaroError(
|
|
9313
|
+
"VALIDATION_FAILED",
|
|
9314
|
+
"naro storage mv moves inside Storage: both sides are naro://<bucket>/<path>."
|
|
9315
|
+
);
|
|
9316
|
+
const { upsert, contentType, cacheControl } = context.flags;
|
|
9317
|
+
if (upsert === true || contentType !== void 0 || cacheControl !== void 0)
|
|
9318
|
+
throw new NaroError(
|
|
9319
|
+
"VALIDATION_FAILED",
|
|
9320
|
+
"naro storage mv takes no --upsert, --content-type or --cache-control: a move keeps the object as it is and never replaces one."
|
|
9321
|
+
);
|
|
9322
|
+
if (context.flags.recursive === true)
|
|
8992
9323
|
throw new NaroError(
|
|
8993
9324
|
"VALIDATION_FAILED",
|
|
8994
|
-
"naro storage
|
|
9325
|
+
"naro storage mv takes no --recursive: it moves one object, naro://<bucket>/<path>. --recursive is for naro storage ls and rm."
|
|
8995
9326
|
);
|
|
9327
|
+
const moved = insideStorage(source, target, "mv");
|
|
9328
|
+
const { projectId, storage } = await objects(context);
|
|
9329
|
+
unwrapTransfer(await storage.from(moved.bucket).move(moved.from, moved.to));
|
|
9330
|
+
const fromUrl = `${NARO_SCHEME}${moved.bucket}/${moved.from}`;
|
|
9331
|
+
const toUrl = `${NARO_SCHEME}${moved.bucket}/${moved.to}`;
|
|
9332
|
+
context.output.result({ projectId, from: fromUrl, to: toUrl }, [
|
|
9333
|
+
`\u2705 Moved ${printable(fromUrl)} \u2192 ${printable(toUrl)}.`
|
|
9334
|
+
]);
|
|
9335
|
+
}
|
|
9336
|
+
async function storageCp(context) {
|
|
9337
|
+
const [from, to, ...extra] = context.positionals;
|
|
9338
|
+
if (from === void 0 || to === void 0 || extra.length > 0)
|
|
9339
|
+
throw new NaroError("VALIDATION_FAILED", CP_USAGE);
|
|
9340
|
+
const source = parseStorageUrl(from);
|
|
9341
|
+
const target = parseStorageUrl(to);
|
|
9342
|
+
if (source !== null && target !== null)
|
|
9343
|
+
return copyInside(context, source, target);
|
|
8996
9344
|
if (target !== null) return upload(context, from, target);
|
|
8997
9345
|
if (source !== null) return download(context, source, to);
|
|
8998
9346
|
throw new NaroError("VALIDATION_FAILED", CP_USAGE);
|
|
@@ -9106,6 +9454,9 @@ function runStorageLs(context) {
|
|
|
9106
9454
|
function runStorageCp(context) {
|
|
9107
9455
|
return explainingKey(context, storageCp);
|
|
9108
9456
|
}
|
|
9457
|
+
function runStorageMv(context) {
|
|
9458
|
+
return explainingKey(context, storageMv);
|
|
9459
|
+
}
|
|
9109
9460
|
function runStorageRm(context) {
|
|
9110
9461
|
return explainingKey(context, storageRm);
|
|
9111
9462
|
}
|
|
@@ -9372,6 +9723,8 @@ Usage:
|
|
|
9372
9723
|
naro storage [status] Naro Storage: on or off, limits, what is stored, today's operations, the buckets
|
|
9373
9724
|
naro storage enable Turn Storage on: the project's R2 bucket, bound to its backend
|
|
9374
9725
|
(safe to re-run; applies a plan change's limits)
|
|
9726
|
+
naro storage disable [--yes] Turn Storage off and keep its files: its calls and file URLs answer 404
|
|
9727
|
+
until naro storage enable binds the same bucket again (asks first)
|
|
9375
9728
|
naro storage bucket list The buckets: public or private, objects, bytes, limits
|
|
9376
9729
|
naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]
|
|
9377
9730
|
<size>: bytes, 5MB (1000s) or 5MiB (1024s); turns Storage on first if needed
|
|
@@ -9388,12 +9741,18 @@ Usage:
|
|
|
9388
9741
|
One file between this machine and naro://<bucket>/<path>, up to the object limit (over 50 MiB in parts),
|
|
9389
9742
|
with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
|
|
9390
9743
|
A big upload shows its progress on stderr; a file of 4 GiB or more cannot be uploaded yet.
|
|
9391
|
-
A download replaces a local file of the same name
|
|
9744
|
+
A download replaces a local file of the same name.
|
|
9745
|
+
Two naro:// paths in one bucket: a copy inside Storage, up to 5 GiB \u2212 5 MiB (--upsert replaces)
|
|
9746
|
+
naro storage mv naro://<bucket>/<from> naro://<bucket>/<to>
|
|
9747
|
+
Move an object inside its bucket, up to 5 GiB \u2212 5 MiB; it never replaces one that is there
|
|
9392
9748
|
naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
|
|
9393
9749
|
Remove objects; --recursive removes what is under a folder (asks first):
|
|
9394
9750
|
<path>/ is the folder alone, <path> also an object of that name
|
|
9395
9751
|
|
|
9396
|
-
naro promote <deployment-id>
|
|
9752
|
+
naro promote <deployment-id> [--to <environment>]
|
|
9753
|
+
Publish a build to production, or to an environment with --to; it says what it will do, then asks.
|
|
9754
|
+
A build moving between an environment and another surface is built again from its commit with the
|
|
9755
|
+
target's variables, which takes up to 15 minutes; any other build is published as stored.
|
|
9397
9756
|
naro rollback [deployment-id] Roll production back to the previous build
|
|
9398
9757
|
|
|
9399
9758
|
Global options:
|
|
@@ -9417,6 +9776,7 @@ Global options:
|
|
|
9417
9776
|
--device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
|
|
9418
9777
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
9419
9778
|
--os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
|
|
9779
|
+
--to <environment> production (the default) or an environment name: where naro promote publishes the build (naro promote)
|
|
9420
9780
|
--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
9781
|
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
9422
9782
|
branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
|
|
@@ -9458,8 +9818,8 @@ Global options:
|
|
|
9458
9818
|
--recursive, -r Everything under a folder (naro storage ls, naro storage rm)
|
|
9459
9819
|
--search <text> Names below the prefix that start with <text>, ASCII case ignored (naro storage ls)
|
|
9460
9820
|
--upsert Replace an object that is there (naro storage cp)
|
|
9461
|
-
--content-type <t> The object's type; by default from the file's extension (naro storage cp)
|
|
9462
|
-
--cache-control <seconds> How long a browser may keep
|
|
9821
|
+
--content-type <t> The uploaded object's type; by default from the file's extension (naro storage cp, uploads only)
|
|
9822
|
+
--cache-control <seconds> How long a browser may keep an upload; 3600 by default (naro storage cp, uploads only)
|
|
9463
9823
|
--select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
|
|
9464
9824
|
--owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
|
|
9465
9825
|
--select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
|
|
@@ -9561,6 +9921,8 @@ function routeStorage(words) {
|
|
|
9561
9921
|
return words.length <= 1 ? { handler: runStorageStatus, positionals: [] } : null;
|
|
9562
9922
|
case "enable":
|
|
9563
9923
|
return words.length === 1 ? { handler: runStorageEnable, positionals: [] } : null;
|
|
9924
|
+
case "disable":
|
|
9925
|
+
return words.length === 1 ? { handler: runStorageDisable, positionals: [] } : null;
|
|
9564
9926
|
case "bucket":
|
|
9565
9927
|
case "buckets":
|
|
9566
9928
|
switch (action) {
|
|
@@ -9597,13 +9959,24 @@ function routeStorage(words) {
|
|
|
9597
9959
|
return { handler: runStorageLs, positionals: words.slice(1) };
|
|
9598
9960
|
case "cp":
|
|
9599
9961
|
return { handler: runStorageCp, positionals: words.slice(1) };
|
|
9962
|
+
case "mv":
|
|
9963
|
+
return { handler: runStorageMv, positionals: words.slice(1) };
|
|
9600
9964
|
case "rm":
|
|
9601
9965
|
return { handler: runStorageRm, positionals: words.slice(1) };
|
|
9602
9966
|
default:
|
|
9603
9967
|
return null;
|
|
9604
9968
|
}
|
|
9605
9969
|
}
|
|
9970
|
+
async function refuseTo() {
|
|
9971
|
+
throw new NaroError("VALIDATION_FAILED", "--to is only for naro promote.");
|
|
9972
|
+
}
|
|
9606
9973
|
function route(parsed) {
|
|
9974
|
+
const routed = routeCommand(parsed);
|
|
9975
|
+
if (routed && parsed.flags.to !== void 0 && parsed.command !== "promote" && parsed.command !== "rollback")
|
|
9976
|
+
return { handler: refuseTo, positionals: routed.positionals };
|
|
9977
|
+
return routed;
|
|
9978
|
+
}
|
|
9979
|
+
function routeCommand(parsed) {
|
|
9607
9980
|
const { command, positionals } = parsed;
|
|
9608
9981
|
const sub = positionals[0];
|
|
9609
9982
|
switch (command) {
|