naro-cli 0.19.1 → 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 +118 -5
- package/dist/{chunk-ZO2BEC46.js → chunk-SAA66XLD.js} +225 -39
- package/dist/deploy.js +1 -1
- package/dist/index.js +155 -15
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -198,6 +198,7 @@ naro policy remove todos Drop every policy on the table: back to deny
|
|
|
198
198
|
|
|
199
199
|
naro storage Naro Storage: on or off, limits, usage, today's operations, buckets
|
|
200
200
|
naro storage enable Turn it on: the project's R2 bucket, bound to the backend
|
|
201
|
+
naro storage disable [--yes] Turn it off and keep the files: Storage calls and file URLs answer 404 until enable (asks first)
|
|
201
202
|
naro storage bucket create avatars --public --file-size-limit 5MiB --allowed-mime-types image/png,image/jpeg
|
|
202
203
|
naro storage bucket update avatars --private | --file-size-limit none | --allowed-mime-types any
|
|
203
204
|
naro storage bucket update docs --public (asks first: anyone with an object's URL can download it)
|
|
@@ -210,7 +211,7 @@ naro storage mv naro://docs/a.pdf naro://docs/old/a.pdf Inside one bucke
|
|
|
210
211
|
naro storage rm naro://avatars/u1/old.png [--recursive] (asks first)
|
|
211
212
|
naro storage rm naro://avatars/u1/ --recursive Everything under u1/; written u1, an object u1 too
|
|
212
213
|
|
|
213
|
-
naro promote <deployment-id>
|
|
214
|
+
naro promote <deployment-id> [--to <environment>] Publish a build to production, or to an environment (0.20.0); says rebuild or reupload first, then asks
|
|
214
215
|
naro rollback [deployment-id] Back to the previous production build
|
|
215
216
|
```
|
|
216
217
|
|
|
@@ -594,8 +595,16 @@ tables: a policy on a table that does not exist
|
|
|
594
595
|
(`relation "nope" does not exist`), a name already taken, or a 33rd policy on
|
|
595
596
|
one table fails the whole request with `400 DATABASE_QUERY_FAILED`, names the
|
|
596
597
|
statement, and applies nothing — not even the `CREATE TABLE` before it. Policy
|
|
597
|
-
statements,
|
|
598
|
-
`params` (`400 VALIDATION_FAILED`): write
|
|
598
|
+
statements, statements that create, drop or rename a table, and statements
|
|
599
|
+
that add a generated column take no `params` (`400 VALIDATION_FAILED`): write
|
|
600
|
+
the values into the SQL.
|
|
601
|
+
|
|
602
|
+
`ALTER TABLE t ADD COLUMN c … AS (…)` computes the generated column for the
|
|
603
|
+
rows already in `t` right after it, as Postgres does: a row it cannot be
|
|
604
|
+
computed for (`json_extract` over text that is not JSON) fails the request
|
|
605
|
+
with SQLite's error (`400 DATABASE_QUERY_FAILED` `malformed JSON:
|
|
606
|
+
SQLITE_ERROR`) and nothing changes. Fix those rows first, in an earlier
|
|
607
|
+
statement.
|
|
599
608
|
|
|
600
609
|
Policies go with their table, as in Postgres: `DROP TABLE` drops them, and a
|
|
601
610
|
new table never inherits the leftovers of an earlier one with the same name
|
|
@@ -837,6 +846,110 @@ An `--update-check` you give replaces the default: without
|
|
|
837
846
|
|
|
838
847
|
## Release notes
|
|
839
848
|
|
|
849
|
+
### 0.20.0
|
|
850
|
+
|
|
851
|
+
0.19.1 went out on 2026-10-24 without these.
|
|
852
|
+
|
|
853
|
+
- `naro promote <deployment-id> --to <environment>` publishes a build to a
|
|
854
|
+
named environment. `--to` takes `production` (what no `--to` does) or an
|
|
855
|
+
environment's slug; an empty value, only blanks or `preview` fails before
|
|
856
|
+
any request with `VALIDATION_FAILED`
|
|
857
|
+
`--to must be production or an environment name.` The usage line is
|
|
858
|
+
`Usage: naro promote <deployment-id> [--to <environment>]`.
|
|
859
|
+
- Before it asks, it says what the promotion will do, from the control
|
|
860
|
+
plane's preview (`GET …/promote?target=`, which changes nothing):
|
|
861
|
+
`▸ rebuild: builds the same commit again with qa's variables` or
|
|
862
|
+
`▸ reupload: publishes the stored build`, then each warning as
|
|
863
|
+
`⚠ <warning>`. Without `--to` the same line is printed for production
|
|
864
|
+
(`… with production's variables`); that is one request more than 0.19.1
|
|
865
|
+
sent.
|
|
866
|
+
- A build moving between an environment and another surface (a preview or
|
|
867
|
+
production build to `qa`, a `qa` build to `staging` or to production) is
|
|
868
|
+
built again from its commit with the target's variables, and counts
|
|
869
|
+
against the plan's builds. Any other build is published as stored: one
|
|
870
|
+
promoted within its own surface, and a preview to production.
|
|
871
|
+
- The question is `Promote <id> to qa (<subdomain>--qa)?`; for production it
|
|
872
|
+
is the one it always was, `Promote <id> to production (<subdomain>)?`.
|
|
873
|
+
`--json` or no terminal without `--yes` still fails
|
|
874
|
+
`CONFIRMATION_REQUIRED`.
|
|
875
|
+
- Progress is `▸ promoting <id> to qa (rebuild)` (`(reupload)`), and the
|
|
876
|
+
result `✅ qa now serves a rebuild of <id>` or `✅ qa now serves <id>`; for
|
|
877
|
+
production `✅ Production now serves a rebuild of <id>` or, as before,
|
|
878
|
+
`✅ Production now serves <id>`. A rebuild is waited for up to 15 minutes
|
|
879
|
+
(a published build, 5 as before), and a failed one fails with its own
|
|
880
|
+
code and message. While a rebuild runs its build log lines print the way
|
|
881
|
+
`naro logs --type build --follow` prints them (`[BUILDING] next build`,
|
|
882
|
+
`[BUILDING] ✗ <error>`), on stderr with `--json` so stdout stays one
|
|
883
|
+
object; a published build prints none.
|
|
884
|
+
- `--json` adds `target` (`null` without `--to`) and `mode` (`rebuild` or
|
|
885
|
+
`reupload`): `{ deploymentId, sourceDeploymentId, projectId, target, mode, status, url }`.
|
|
886
|
+
`naro rollback`'s result gains `mode` too (as MCP's `rollback_deployment`
|
|
887
|
+
says it) and has no `target`: a rollback is always production's.
|
|
888
|
+
- `409 PROMOTE_NEEDS_GIT_SOURCE`
|
|
889
|
+
`Only deployments built from Git can be promoted to another environment. Deploy the code again with naro deploy instead.`
|
|
890
|
+
for a deployment `naro deploy` uploaded. This and every other refusal of
|
|
891
|
+
the preview (`404 ENVIRONMENT_NOT_FOUND` `Environment not found.`) stop
|
|
892
|
+
it in the control plane's words before it asks, and nothing is promoted.
|
|
893
|
+
- A rebuild checks out the pinned commit as a detached HEAD, not the
|
|
894
|
+
branch, so a build script that reads the branch name from git
|
|
895
|
+
(`git branch --show-current`, `git rev-parse --abbrev-ref HEAD`) sees
|
|
896
|
+
none.
|
|
897
|
+
- Against a control plane from before named environments 2a, which has no
|
|
898
|
+
preview (it answers 405) and whose promote ignores the target and
|
|
899
|
+
republishes to production, `--to <environment>` stops before it promotes
|
|
900
|
+
anything: `PROMOTE_TARGET_UNSUPPORTED`
|
|
901
|
+
`This control plane cannot promote to an environment yet. Deploy the latest web first, then try again.`
|
|
902
|
+
With no `--to` or `--to production` the 405 is ignored: no preview line,
|
|
903
|
+
and the promotion goes as it always did.
|
|
904
|
+
- `--to` belongs to `naro promote`; `naro rollback` refuses it before any
|
|
905
|
+
request with `VALIDATION_FAILED`
|
|
906
|
+
`--to is for naro promote: naro rollback always rolls production back.`
|
|
907
|
+
(it always targets production, and ignoring `--to` would roll production
|
|
908
|
+
back for someone who named an environment).
|
|
909
|
+
- Any other command given `--to` fails before any request with
|
|
910
|
+
`VALIDATION_FAILED` `--to is only for naro promote.`, so a flag a command
|
|
911
|
+
does not read is never dropped silently.
|
|
912
|
+
- `naro promote` and `naro rollback` refuse `--env` and any word after the
|
|
913
|
+
deployment id before any request, with `VALIDATION_FAILED`
|
|
914
|
+
`--env is not an option of naro promote: use --to <environment>.`,
|
|
915
|
+
`--env is not an option of naro rollback: naro rollback always rolls production back.`
|
|
916
|
+
or the usage line (`Usage: naro promote <deployment-id> [--to <environment>]`,
|
|
917
|
+
`Usage: naro rollback [deployment-id]`), instead of acting on production for
|
|
918
|
+
someone who named an environment (`naro promote d1 --env qa --yes` and
|
|
919
|
+
`naro promote d1 qa --yes` promoted to production in 0.19.1).
|
|
920
|
+
- A control plane with named environments 2a rebuilds a deployment of an
|
|
921
|
+
environment that is promoted to production, which it used to refuse with
|
|
922
|
+
`400 DEPLOYMENT_NOT_PROMOTABLE`. `naro promote <id>` says so before it
|
|
923
|
+
asks (`▸ rebuild: … with production's variables`) and waits up to 15
|
|
924
|
+
minutes; `naro rollback <id>` waits 15 minutes for such a rebuild too and
|
|
925
|
+
prints `✅ Production now serves a rebuild of <id>`. The build that
|
|
926
|
+
`naro rollback` picks on its own is never one.
|
|
927
|
+
- `naro storage disable [--yes]` turns the project's Storage off and keeps
|
|
928
|
+
its files (Storage 3-1k). It asks first —
|
|
929
|
+
`Turn Storage off for this project? Its Storage calls and file URLs answer 404 until naro storage enable; buckets and files are kept.`
|
|
930
|
+
— and without a terminal or with `--json` only `--yes` answers it. The
|
|
931
|
+
backend is uploaded again without the bucket: the app's Storage calls and
|
|
932
|
+
file URLs answer `404 STORAGE_NOT_ENABLED` until `naro storage enable`
|
|
933
|
+
binds the same bucket again, and the buckets, the files and their storage
|
|
934
|
+
cost stay. To delete the files instead, remove the buckets with
|
|
935
|
+
`naro storage bucket remove <name> --empty` while Storage is on. Storage
|
|
936
|
+
that was never turned on prints `Storage is already off.` and changes
|
|
937
|
+
nothing; Storage that is already off is finished without asking again (a
|
|
938
|
+
turn-off whose upload stopped answers `BACKEND_PROVISION_FAILED` and says
|
|
939
|
+
to run it again).
|
|
940
|
+
- `naro storage` says
|
|
941
|
+
`Storage: off since <YYYY-MM-DD HH:MM> UTC — buckets and files are kept`
|
|
942
|
+
while it is off, with the buckets.
|
|
943
|
+
- `naro storage ls`, `cp`, `mv` and `rm` read the Storage state before the
|
|
944
|
+
service key and stop with `STORAGE_OFF`
|
|
945
|
+
``Storage is turned off for this project. Turn it on with `naro storage enable` first.``
|
|
946
|
+
while it is off. `naro storage bucket create` on Storage that is off gets
|
|
947
|
+
the control plane's `409 STORAGE_OFF`: it turns Storage on by itself only
|
|
948
|
+
when Storage was never on.
|
|
949
|
+
- Published naro-cli 0.19.1 and earlier show Storage that is off as `off`
|
|
950
|
+
and pass the backend's `404 STORAGE_NOT_ENABLED` on from `ls`, `cp`,
|
|
951
|
+
`mv` and `rm`; their `bucket create` stops at `409 STORAGE_OFF` too.
|
|
952
|
+
|
|
840
953
|
### 0.19.1
|
|
841
954
|
|
|
842
955
|
0.19.0 went out on 2026-10-19 without these.
|
|
@@ -996,7 +1109,7 @@ RENAME` the way the control plane now reads it: a column rename whose
|
|
|
996
1109
|
too), migration files the backend would stop at for Storage: a policy that
|
|
997
1110
|
reads `storage.objects` in a subquery while the backend's ledger lacks
|
|
998
1111
|
system migration 7 (it reads the ledger only when a file has one), and
|
|
999
|
-
|
|
1112
|
+
a statement that needs `storage.objects` (CREATE POLICY, DROP POLICY without
|
|
1000
1113
|
IF EXISTS, the `naro:owner` COMMENT) on a backend from before Storage or a
|
|
1001
1114
|
project with none. It fails `BACKEND_OUTDATED`
|
|
1002
1115
|
``0002_b.sql needs storage.objects (statement 2 of 2), which is not available on this backend yet, so the reset would stop there with the schema already dropped. Nothing was changed. Run `naro backend enable` to update it, then try again.``
|
|
@@ -1556,7 +1669,7 @@ not fail but no strategy can ship it, `NO_DEPLOYMENT_STRATEGY`. Either way the m
|
|
|
1556
1669
|
what each strategy failed at.
|
|
1557
1670
|
|
|
1558
1671
|
`--yes` (or `-y`) suppresses every prompt — destructive actions
|
|
1559
|
-
(production rollback
|
|
1672
|
+
(production rollback, promote to production or to an environment, env/domain/environment removal, `naro env import`, turning off an environment's protection, database reset, backend key
|
|
1560
1673
|
rotation, policy removal and drop, opening the preview with
|
|
1561
1674
|
`naro protection disable`, replacing or removing the bypass secret) run
|
|
1562
1675
|
unconfirmed, so use it deliberately in
|
|
@@ -345,6 +345,8 @@ var DEPLOYMENT_ERROR_CODES = [
|
|
|
345
345
|
"FORBIDDEN",
|
|
346
346
|
"NOT_FOUND",
|
|
347
347
|
"DEPLOYMENT_NOT_PROMOTABLE",
|
|
348
|
+
/** A promotion to another surface rebuilds the commit, and the source was not built from Git (spec 2026-10-18-named-environments §9.1). */
|
|
349
|
+
"PROMOTE_NEEDS_GIT_SOURCE",
|
|
348
350
|
"DEPLOYMENT_NOT_CANCELABLE",
|
|
349
351
|
"SUBDOMAIN_TAKEN",
|
|
350
352
|
"FLAG_EXISTS",
|
|
@@ -511,6 +513,9 @@ function projectPath(teamSlug, projectSlug) {
|
|
|
511
513
|
return `/${teamSlug}/projects/${projectSlug}`;
|
|
512
514
|
}
|
|
513
515
|
|
|
516
|
+
// ../../packages/core/src/platform-mail.ts
|
|
517
|
+
var encoder = new TextEncoder();
|
|
518
|
+
|
|
514
519
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
515
520
|
var external_exports = {};
|
|
516
521
|
__export(external_exports, {
|
|
@@ -18506,6 +18511,7 @@ function forkedAppRoot(checkout, appRoot, forkCheckout) {
|
|
|
18506
18511
|
}
|
|
18507
18512
|
|
|
18508
18513
|
// ../../packages/build/src/local-build-executor.ts
|
|
18514
|
+
var PINNED_COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
18509
18515
|
function concurrencyFromEnvironment() {
|
|
18510
18516
|
const raw = Number(process.env.NARO_RESOLVER_CONCURRENCY ?? "1");
|
|
18511
18517
|
return Number.isInteger(raw) && raw > 0 ? raw : 1;
|
|
@@ -18650,6 +18656,68 @@ var LocalBuildExecutor = class {
|
|
|
18650
18656
|
constructor(dependencies = defaultLocalBuildDependencies) {
|
|
18651
18657
|
this.dependencies = dependencies;
|
|
18652
18658
|
}
|
|
18659
|
+
/**
|
|
18660
|
+
* Checks out one commit, whichever ref reaches it: GitHub serves any
|
|
18661
|
+
* reachable commit by SHA, which `clone --branch` cannot ask for. The URL
|
|
18662
|
+
* carries the installation token and is only ever an argument — runCommand
|
|
18663
|
+
* logs a command's output (redacted), never the command line, and a failed
|
|
18664
|
+
* command's message names the command, not its arguments.
|
|
18665
|
+
*
|
|
18666
|
+
* The commit is an argument of `git fetch`, so it is checked before any git
|
|
18667
|
+
* command runs: a value git could read as an option (`--upload-pack=…`) or a
|
|
18668
|
+
* ref name must not get that far. The build container checks it on the way
|
|
18669
|
+
* in; the local runner passes claim's input straight through, so this is the
|
|
18670
|
+
* check that covers it.
|
|
18671
|
+
*/
|
|
18672
|
+
async checkoutPinnedCommit(commitSha, repositoryUrl, workspace, checkout, observer) {
|
|
18673
|
+
if (!PINNED_COMMIT_SHA.test(commitSha)) {
|
|
18674
|
+
throw new DeploymentError(
|
|
18675
|
+
"CLONE_FAILED",
|
|
18676
|
+
"The pinned commit is not a 40-character lowercase hex SHA."
|
|
18677
|
+
);
|
|
18678
|
+
}
|
|
18679
|
+
const git2 = (args) => this.dependencies.runCommand(
|
|
18680
|
+
{
|
|
18681
|
+
command: "git",
|
|
18682
|
+
args,
|
|
18683
|
+
cwd: workspace,
|
|
18684
|
+
timeoutMs: 12e4,
|
|
18685
|
+
stage: "CLONING",
|
|
18686
|
+
errorCode: "CLONE_FAILED"
|
|
18687
|
+
},
|
|
18688
|
+
observer
|
|
18689
|
+
);
|
|
18690
|
+
await git2(["init", "--quiet", checkout]);
|
|
18691
|
+
await git2(["-C", checkout, "remote", "add", "origin", "--", repositoryUrl]);
|
|
18692
|
+
try {
|
|
18693
|
+
await git2([
|
|
18694
|
+
"-C",
|
|
18695
|
+
checkout,
|
|
18696
|
+
"fetch",
|
|
18697
|
+
"--quiet",
|
|
18698
|
+
"--depth",
|
|
18699
|
+
"1",
|
|
18700
|
+
"origin",
|
|
18701
|
+
commitSha
|
|
18702
|
+
]);
|
|
18703
|
+
} catch (error51) {
|
|
18704
|
+
if (!(error51 instanceof CommandExecutionError)) throw error51;
|
|
18705
|
+
if (error51.timedOut) throw error51;
|
|
18706
|
+
throw new DeploymentError(
|
|
18707
|
+
"CLONE_FAILED",
|
|
18708
|
+
`Could not fetch commit ${commitSha.slice(0, 7)}. It may have been removed from the repository.`,
|
|
18709
|
+
{ cause: error51 }
|
|
18710
|
+
);
|
|
18711
|
+
}
|
|
18712
|
+
await git2([
|
|
18713
|
+
"-C",
|
|
18714
|
+
checkout,
|
|
18715
|
+
"checkout",
|
|
18716
|
+
"--quiet",
|
|
18717
|
+
"--detach",
|
|
18718
|
+
"FETCH_HEAD"
|
|
18719
|
+
]);
|
|
18720
|
+
}
|
|
18653
18721
|
async build(input, observer, cache) {
|
|
18654
18722
|
const workspace = await this.dependencies.createWorkspace();
|
|
18655
18723
|
const checkout = join18(workspace, "checkout");
|
|
@@ -18675,27 +18743,37 @@ var LocalBuildExecutor = class {
|
|
|
18675
18743
|
message: `Build cache disabled: ${compressor} is not installed. Install it to reuse dependencies between deploys.`
|
|
18676
18744
|
});
|
|
18677
18745
|
}
|
|
18678
|
-
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18684
|
-
|
|
18685
|
-
|
|
18686
|
-
|
|
18687
|
-
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
|
|
18691
|
-
|
|
18692
|
-
|
|
18693
|
-
|
|
18694
|
-
|
|
18695
|
-
|
|
18696
|
-
|
|
18697
|
-
|
|
18698
|
-
|
|
18746
|
+
if (input.commitSha !== void 0) {
|
|
18747
|
+
await this.checkoutPinnedCommit(
|
|
18748
|
+
input.commitSha,
|
|
18749
|
+
input.repositoryUrl,
|
|
18750
|
+
workspace,
|
|
18751
|
+
checkout,
|
|
18752
|
+
observer
|
|
18753
|
+
);
|
|
18754
|
+
} else {
|
|
18755
|
+
await this.dependencies.runCommand(
|
|
18756
|
+
{
|
|
18757
|
+
command: "git",
|
|
18758
|
+
args: [
|
|
18759
|
+
"clone",
|
|
18760
|
+
"--depth",
|
|
18761
|
+
"1",
|
|
18762
|
+
"--branch",
|
|
18763
|
+
input.branch,
|
|
18764
|
+
"--single-branch",
|
|
18765
|
+
"--",
|
|
18766
|
+
input.repositoryUrl,
|
|
18767
|
+
checkout
|
|
18768
|
+
],
|
|
18769
|
+
cwd: workspace,
|
|
18770
|
+
timeoutMs: 12e4,
|
|
18771
|
+
stage: "CLONING",
|
|
18772
|
+
errorCode: "CLONE_FAILED"
|
|
18773
|
+
},
|
|
18774
|
+
observer
|
|
18775
|
+
);
|
|
18776
|
+
}
|
|
18699
18777
|
const revision = await this.dependencies.runCommand(
|
|
18700
18778
|
{
|
|
18701
18779
|
command: "git",
|
|
@@ -18714,6 +18792,12 @@ var LocalBuildExecutor = class {
|
|
|
18714
18792
|
"Git returned an invalid commit SHA."
|
|
18715
18793
|
);
|
|
18716
18794
|
}
|
|
18795
|
+
if (input.commitSha !== void 0 && gitCommitSha !== input.commitSha) {
|
|
18796
|
+
throw new DeploymentError(
|
|
18797
|
+
"CLONE_FAILED",
|
|
18798
|
+
`Checked out ${gitCommitSha.slice(0, 7)} instead of the pinned commit ${input.commitSha.slice(0, 7)}.`
|
|
18799
|
+
);
|
|
18800
|
+
}
|
|
18717
18801
|
const reportedSha = gitCommitSha.length === 40 ? gitCommitSha : null;
|
|
18718
18802
|
if (!reportedSha)
|
|
18719
18803
|
await info2(
|
|
@@ -19129,6 +19213,14 @@ var NaroError = class extends Error {
|
|
|
19129
19213
|
if (options?.details !== void 0) this.details = options.details;
|
|
19130
19214
|
}
|
|
19131
19215
|
};
|
|
19216
|
+
var PROMOTE_TARGET_UNSUPPORTED_MESSAGE = "This control plane cannot promote to an environment yet. Deploy the latest web first, then try again.";
|
|
19217
|
+
function promoteTargetUnsupported(cause) {
|
|
19218
|
+
return new NaroError(
|
|
19219
|
+
"PROMOTE_TARGET_UNSUPPORTED",
|
|
19220
|
+
PROMOTE_TARGET_UNSUPPORTED_MESSAGE,
|
|
19221
|
+
cause !== void 0 ? { cause } : void 0
|
|
19222
|
+
);
|
|
19223
|
+
}
|
|
19132
19224
|
function isNaroError(error51) {
|
|
19133
19225
|
return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
|
|
19134
19226
|
}
|
|
@@ -19234,7 +19326,7 @@ var STYLES = `
|
|
|
19234
19326
|
*{box-sizing:border-box}
|
|
19235
19327
|
:root{
|
|
19236
19328
|
--bg:#000;--surface:#0a0a0a;--surface-2:#111;--border:#232323;
|
|
19237
|
-
--text:#ededed;--muted:#a1a1a1;--faint:#
|
|
19329
|
+
--text:#ededed;--muted:#a1a1a1;--faint:#808080;
|
|
19238
19330
|
--sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
19239
19331
|
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
19240
19332
|
}
|
|
@@ -19698,6 +19790,8 @@ function deploymentInfo(row) {
|
|
|
19698
19790
|
errorMessage: row.error_message,
|
|
19699
19791
|
gitCommitSha: row.git_commit_sha,
|
|
19700
19792
|
gitCommitMessage: row.git_commit_message,
|
|
19793
|
+
pinnedCommitSha: row.pinned_commit_sha ?? null,
|
|
19794
|
+
rebuiltFromDeploymentId: row.rebuilt_from_deployment_id ?? null,
|
|
19701
19795
|
createdAt: row.created_at,
|
|
19702
19796
|
startedAt: row.started_at,
|
|
19703
19797
|
finishedAt: row.finished_at
|
|
@@ -19949,16 +20043,65 @@ var DeploymentsApi = class {
|
|
|
19949
20043
|
return { current, target: production.slice(1).find(differs) ?? null };
|
|
19950
20044
|
}
|
|
19951
20045
|
/**
|
|
19952
|
-
*
|
|
19953
|
-
*
|
|
19954
|
-
* (older production build → production).
|
|
20046
|
+
* Promotes an existing READY deployment: with no `target` (or
|
|
20047
|
+
* `production`) it goes to production — `naro promote` (preview →
|
|
20048
|
+
* production) and `naro rollback` (older production build → production).
|
|
20049
|
+
* A deployment from an environment is rebuilt from its commit with
|
|
20050
|
+
* production's variables (`mode: "rebuild"`); any other is published as
|
|
20051
|
+
* stored (`mode: "reupload"`). With an environment `target` the control
|
|
20052
|
+
* plane likewise rebuilds the deployment's commit with that environment's
|
|
20053
|
+
* variables or publishes the stored build.
|
|
20054
|
+
*
|
|
20055
|
+
* No `target` sends no body, exactly as clients before targets did. A
|
|
20056
|
+
* control plane from before targets ignores `target` and promotes to
|
|
20057
|
+
* production, so an environment target is first checked with
|
|
20058
|
+
* `promoteCheck`: a 405 there (no such route) stops with
|
|
20059
|
+
* `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
|
|
20060
|
+
* trimmed once, as the control plane trims it, so `" production"` is
|
|
20061
|
+
* production and asks no preview.
|
|
20062
|
+
*/
|
|
20063
|
+
async promote(projectId, deploymentId, options = {}) {
|
|
20064
|
+
const target2 = options.target?.trim();
|
|
20065
|
+
if (target2 !== void 0 && target2 !== "production") {
|
|
20066
|
+
try {
|
|
20067
|
+
await this.promoteCheck(projectId, deploymentId, target2);
|
|
20068
|
+
} catch (error51) {
|
|
20069
|
+
if (isNaroError(error51) && error51.status === 405) {
|
|
20070
|
+
throw promoteTargetUnsupported(error51);
|
|
20071
|
+
}
|
|
20072
|
+
throw error51;
|
|
20073
|
+
}
|
|
20074
|
+
}
|
|
20075
|
+
const body = await this.client.request(this.promotePath(projectId, deploymentId), {
|
|
20076
|
+
method: "POST",
|
|
20077
|
+
...target2 !== void 0 ? { body: { target: target2 } } : {},
|
|
20078
|
+
context: "Promote deployment"
|
|
20079
|
+
});
|
|
20080
|
+
return {
|
|
20081
|
+
...deploymentInfo(body.deployment),
|
|
20082
|
+
mode: body.mode ?? "reupload"
|
|
20083
|
+
};
|
|
20084
|
+
}
|
|
20085
|
+
/**
|
|
20086
|
+
* What promoting to `target` would do, changing nothing: how it gets its
|
|
20087
|
+
* build, the migrations the target's database has not applied (null when
|
|
20088
|
+
* they cannot be compared) and the warnings to show first. Answers 405 from
|
|
20089
|
+
* a control plane that predates promote targets.
|
|
19955
20090
|
*/
|
|
19956
|
-
async
|
|
20091
|
+
async promoteCheck(projectId, deploymentId, target2) {
|
|
20092
|
+
const query = target2 !== void 0 ? `?target=${encodeURIComponent(target2)}` : "";
|
|
19957
20093
|
const body = await this.client.request(
|
|
19958
|
-
|
|
19959
|
-
{
|
|
20094
|
+
`${this.promotePath(projectId, deploymentId)}${query}`,
|
|
20095
|
+
{ context: "Check promotion" }
|
|
19960
20096
|
);
|
|
19961
|
-
return
|
|
20097
|
+
return {
|
|
20098
|
+
mode: body.mode ?? "reupload",
|
|
20099
|
+
pendingMigrations: body.pendingMigrations ?? null,
|
|
20100
|
+
warnings: body.warnings ?? []
|
|
20101
|
+
};
|
|
20102
|
+
}
|
|
20103
|
+
promotePath(projectId, deploymentId) {
|
|
20104
|
+
return `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`;
|
|
19962
20105
|
}
|
|
19963
20106
|
/**
|
|
19964
20107
|
* Stops an in-flight build (QUEUED through BUILDING) and marks the row
|
|
@@ -20713,6 +20856,7 @@ function storageInfo(body) {
|
|
|
20713
20856
|
return {
|
|
20714
20857
|
enabled: body.enabled,
|
|
20715
20858
|
bucket: body.bucket ?? null,
|
|
20859
|
+
disabledAt: body.disabledAt ?? null,
|
|
20716
20860
|
limits: {
|
|
20717
20861
|
bytes: body.limits.bytes,
|
|
20718
20862
|
objectBytes: body.limits.objectBytes,
|
|
@@ -20724,6 +20868,18 @@ function storageInfo(body) {
|
|
|
20724
20868
|
buckets: (body.buckets ?? []).map(storageBucket)
|
|
20725
20869
|
};
|
|
20726
20870
|
}
|
|
20871
|
+
function storageRunFailure(error51) {
|
|
20872
|
+
const details = isNaroError(error51) ? error51.details : void 0;
|
|
20873
|
+
if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
|
|
20874
|
+
return new NaroError(error51.code, error51.message, {
|
|
20875
|
+
...error51.status !== void 0 ? { status: error51.status } : {},
|
|
20876
|
+
details: {
|
|
20877
|
+
...details,
|
|
20878
|
+
backend: backendInfo(details.backend)
|
|
20879
|
+
}
|
|
20880
|
+
});
|
|
20881
|
+
return error51;
|
|
20882
|
+
}
|
|
20727
20883
|
function bucketFields(fields, into = {}) {
|
|
20728
20884
|
for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
|
|
20729
20885
|
if (fields[key] !== void 0) into[key] = fields[key];
|
|
@@ -21152,16 +21308,45 @@ var StorageApi = class {
|
|
|
21152
21308
|
}
|
|
21153
21309
|
);
|
|
21154
21310
|
} catch (error51) {
|
|
21155
|
-
|
|
21156
|
-
|
|
21157
|
-
|
|
21158
|
-
|
|
21159
|
-
|
|
21160
|
-
|
|
21161
|
-
|
|
21162
|
-
|
|
21163
|
-
|
|
21164
|
-
|
|
21311
|
+
throw storageRunFailure(error51);
|
|
21312
|
+
}
|
|
21313
|
+
return storageInfo(body.storage);
|
|
21314
|
+
}
|
|
21315
|
+
/**
|
|
21316
|
+
* Turns Storage off and keeps its files (Storage 3-1k): the Naro worker is
|
|
21317
|
+
* uploaded again without the bucket, so the app's Storage calls and file
|
|
21318
|
+
* URLs answer `404 STORAGE_NOT_ENABLED` until `enable` binds the same
|
|
21319
|
+
* bucket again. The bucket, its files and their storage cost stay; while
|
|
21320
|
+
* it is off, bucket writes and the object routes answer `409
|
|
21321
|
+
* STORAGE_OFF`, and reads go on. Safe to repeat: Storage never on, or
|
|
21322
|
+
* already off, comes back as it is, and a turn-off that stopped is
|
|
21323
|
+
* finished. Two minutes, like `enable`.
|
|
21324
|
+
*
|
|
21325
|
+
* Refusals, in the order they are checked: `BACKEND_DISABLED`,
|
|
21326
|
+
* `FORBIDDEN` (`storage.manage`), `DATABASE_NOT_ENABLED`,
|
|
21327
|
+
* `BACKEND_NOT_READY` (no backend), `BACKEND_OFF`, `BACKEND_BUSY` (another
|
|
21328
|
+
* change holds the backend — or turning Storage on again took this
|
|
21329
|
+
* turn-off over, and Storage is on), `STORAGE_DISABLE_FAILED` (502: the
|
|
21330
|
+
* turn-off could not be recorded; nothing changed), and
|
|
21331
|
+
* `BACKEND_PROVISION_FAILED` (502) when it is
|
|
21332
|
+
* recorded but the worker's upload — or a step after it — stopped:
|
|
21333
|
+
* `details` holds `{ storage: { applied: false, enabled: false }, backend
|
|
21334
|
+
* }`, and `disable` again finishes. Never `STORAGE_DISABLED`: the control
|
|
21335
|
+
* plane's Storage switch does not hold a removal back.
|
|
21336
|
+
*/
|
|
21337
|
+
async disable(projectId) {
|
|
21338
|
+
let body;
|
|
21339
|
+
try {
|
|
21340
|
+
body = await this.client.request(
|
|
21341
|
+
this.path(projectId),
|
|
21342
|
+
{
|
|
21343
|
+
method: "DELETE",
|
|
21344
|
+
context: "Turn off Storage",
|
|
21345
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
21346
|
+
}
|
|
21347
|
+
);
|
|
21348
|
+
} catch (error51) {
|
|
21349
|
+
throw storageRunFailure(error51);
|
|
21165
21350
|
}
|
|
21166
21351
|
return storageInfo(body.storage);
|
|
21167
21352
|
}
|
|
@@ -31628,6 +31813,7 @@ export {
|
|
|
31628
31813
|
splitStatements,
|
|
31629
31814
|
ANALYTICS_FILTER_KEYS,
|
|
31630
31815
|
NaroError,
|
|
31816
|
+
promoteTargetUnsupported,
|
|
31631
31817
|
isNaroError,
|
|
31632
31818
|
toNaroError,
|
|
31633
31819
|
saveSession,
|
package/dist/deploy.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
policiesForTypes,
|
|
64
64
|
policiesLostByReset,
|
|
65
65
|
projectPath,
|
|
66
|
+
promoteTargetUnsupported,
|
|
66
67
|
promptForMissingEnv,
|
|
67
68
|
pullEnv,
|
|
68
69
|
quoteIdent,
|
|
@@ -96,7 +97,7 @@ import {
|
|
|
96
97
|
usageMonthOf,
|
|
97
98
|
userForAccessToken,
|
|
98
99
|
writeProjectLink
|
|
99
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-SAA66XLD.js";
|
|
100
101
|
import "./chunk-K3NQKI34.js";
|
|
101
102
|
|
|
102
103
|
// src/args.ts
|
|
@@ -195,7 +196,8 @@ var STRING_FLAGS = {
|
|
|
195
196
|
"--search": "search",
|
|
196
197
|
"--copy-env": "copyEnv",
|
|
197
198
|
"--database": "database",
|
|
198
|
-
"--protection": "protection"
|
|
199
|
+
"--protection": "protection",
|
|
200
|
+
"--to": "to"
|
|
199
201
|
};
|
|
200
202
|
var SECRET_FLAGS = {
|
|
201
203
|
"--api-key": "--api-key-stdin",
|
|
@@ -5523,17 +5525,27 @@ async function runProjectInspect(context) {
|
|
|
5523
5525
|
}
|
|
5524
5526
|
|
|
5525
5527
|
// src/commands/promote.ts
|
|
5526
|
-
|
|
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) {
|
|
5527
5533
|
const promoted = await client.deployments.promote(
|
|
5528
5534
|
project.id,
|
|
5529
|
-
sourceDeploymentId
|
|
5535
|
+
sourceDeploymentId,
|
|
5536
|
+
target !== void 0 ? { target } : void 0
|
|
5530
5537
|
);
|
|
5538
|
+
const rebuild = promoted.mode === "rebuild";
|
|
5531
5539
|
context.output.log(
|
|
5532
|
-
`\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 }
|
|
5533
5548
|
);
|
|
5534
|
-
const detail = await client.deployments.wait(promoted.id, {
|
|
5535
|
-
timeoutMs: 5 * 6e4
|
|
5536
|
-
});
|
|
5537
5549
|
if (detail.deployment.status !== "READY") {
|
|
5538
5550
|
throw new NaroError(
|
|
5539
5551
|
detail.deployment.errorCode ?? "DEPLOY_FAILED",
|
|
@@ -5541,42 +5553,104 @@ async function promoteAndWait(context, client, project, sourceDeploymentId, acti
|
|
|
5541
5553
|
{ details: { deploymentId: promoted.id } }
|
|
5542
5554
|
);
|
|
5543
5555
|
}
|
|
5556
|
+
const serving = target === void 0 || target === "production" ? "Production" : target;
|
|
5544
5557
|
context.output.result(
|
|
5545
5558
|
{
|
|
5546
5559
|
deploymentId: promoted.id,
|
|
5547
5560
|
sourceDeploymentId,
|
|
5548
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,
|
|
5549
5566
|
status: detail.deployment.status,
|
|
5550
5567
|
url: detail.deployment.url
|
|
5551
5568
|
},
|
|
5552
5569
|
[
|
|
5553
|
-
`\u2705
|
|
5570
|
+
`\u2705 ${serving} now serves ${rebuild ? "a rebuild of " : ""}${sourceDeploymentId}`,
|
|
5554
5571
|
"",
|
|
5555
5572
|
detail.deployment.url ?? ""
|
|
5556
5573
|
]
|
|
5557
5574
|
);
|
|
5558
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
|
+
}
|
|
5559
5612
|
async function runPromote(context) {
|
|
5560
|
-
const [deploymentId] = context.positionalOrThrow(
|
|
5561
|
-
|
|
5562
|
-
|
|
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>."
|
|
5563
5618
|
);
|
|
5619
|
+
const target = promoteTarget(context.flags);
|
|
5564
5620
|
const client = await context.client();
|
|
5565
5621
|
const projectId = await context.resolveProjectId();
|
|
5566
5622
|
const project = await client.projects.get(projectId);
|
|
5623
|
+
await previewPromotion(
|
|
5624
|
+
context,
|
|
5625
|
+
client,
|
|
5626
|
+
projectId,
|
|
5627
|
+
deploymentId,
|
|
5628
|
+
target
|
|
5629
|
+
);
|
|
5567
5630
|
await confirmOrAbort(
|
|
5568
5631
|
context,
|
|
5569
|
-
`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})?`
|
|
5570
5633
|
);
|
|
5571
5634
|
await promoteAndWait(
|
|
5572
5635
|
context,
|
|
5573
5636
|
client,
|
|
5574
5637
|
project,
|
|
5575
5638
|
deploymentId,
|
|
5576
|
-
"promote"
|
|
5639
|
+
"promote",
|
|
5640
|
+
target
|
|
5577
5641
|
);
|
|
5578
5642
|
}
|
|
5579
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
|
+
);
|
|
5580
5654
|
const client = await context.client();
|
|
5581
5655
|
const projectId = await context.resolveProjectId();
|
|
5582
5656
|
const project = await client.projects.get(projectId);
|
|
@@ -5688,6 +5762,7 @@ var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --priva
|
|
|
5688
5762
|
var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
|
|
5689
5763
|
var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
|
|
5690
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.";
|
|
5691
5766
|
var NO_BUCKETS = "No buckets yet. `naro storage bucket create <name>` makes one.";
|
|
5692
5767
|
var IDLE_PAGES = 3;
|
|
5693
5768
|
function bucketLine(bucket) {
|
|
@@ -5708,6 +5783,12 @@ function utcClock(iso) {
|
|
|
5708
5783
|
const at = new Date(iso);
|
|
5709
5784
|
return Number.isNaN(at.getTime()) ? null : at.toISOString().slice(11, 16);
|
|
5710
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
|
+
}
|
|
5711
5792
|
function warnedPercent(value, limit) {
|
|
5712
5793
|
if (limit === null || !(limit > 0)) return null;
|
|
5713
5794
|
const percent2 = Math.floor(value * 100 / limit);
|
|
@@ -5749,6 +5830,13 @@ function meterLines(info) {
|
|
|
5749
5830
|
return lines;
|
|
5750
5831
|
}
|
|
5751
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
|
+
];
|
|
5752
5840
|
if (!info.enabled)
|
|
5753
5841
|
return [
|
|
5754
5842
|
"Storage: off",
|
|
@@ -5777,6 +5865,19 @@ async function enableStorage(client, projectId) {
|
|
|
5777
5865
|
throw error;
|
|
5778
5866
|
}
|
|
5779
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
|
+
}
|
|
5780
5881
|
function bucketName(context, usage) {
|
|
5781
5882
|
if (context.positionals.length !== 1)
|
|
5782
5883
|
throw new NaroError("VALIDATION_FAILED", usage);
|
|
@@ -5813,6 +5914,24 @@ async function runStorageEnable(context) {
|
|
|
5813
5914
|
...storageLines(storage)
|
|
5814
5915
|
]);
|
|
5815
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
|
+
}
|
|
5816
5935
|
async function runStorageBucketList(context) {
|
|
5817
5936
|
const client = await context.client();
|
|
5818
5937
|
const projectId = await context.resolveProjectId();
|
|
@@ -8639,6 +8758,7 @@ var MV_USAGE = "Usage: naro storage mv naro://<bucket>/<from> naro://<bucket>/<t
|
|
|
8639
8758
|
var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
|
|
8640
8759
|
var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive] [--search <text>]";
|
|
8641
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.";
|
|
8642
8762
|
var STORAGE_OUTDATED_MESSAGE = "The project backend is older than this Storage command. Run `naro backend enable` to update it, then try again.";
|
|
8643
8763
|
var OLD_SEARCH_REASON = "search is not supported yet";
|
|
8644
8764
|
function isOldSearchRefusal(error) {
|
|
@@ -8731,6 +8851,9 @@ async function objects(context) {
|
|
|
8731
8851
|
"This project has no backend yet. Run `naro backend enable`, then `naro storage enable`."
|
|
8732
8852
|
);
|
|
8733
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);
|
|
8734
8857
|
let serviceKey = process.env.NARO_SERVICE_KEY?.trim() ?? "";
|
|
8735
8858
|
if (serviceKey !== "" && !ONE_KEY.test(serviceKey))
|
|
8736
8859
|
throw new NaroError(
|
|
@@ -9600,6 +9723,8 @@ Usage:
|
|
|
9600
9723
|
naro storage [status] Naro Storage: on or off, limits, what is stored, today's operations, the buckets
|
|
9601
9724
|
naro storage enable Turn Storage on: the project's R2 bucket, bound to its backend
|
|
9602
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)
|
|
9603
9728
|
naro storage bucket list The buckets: public or private, objects, bytes, limits
|
|
9604
9729
|
naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]
|
|
9605
9730
|
<size>: bytes, 5MB (1000s) or 5MiB (1024s); turns Storage on first if needed
|
|
@@ -9624,7 +9749,10 @@ Usage:
|
|
|
9624
9749
|
Remove objects; --recursive removes what is under a folder (asks first):
|
|
9625
9750
|
<path>/ is the folder alone, <path> also an object of that name
|
|
9626
9751
|
|
|
9627
|
-
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.
|
|
9628
9756
|
naro rollback [deployment-id] Roll production back to the previous build
|
|
9629
9757
|
|
|
9630
9758
|
Global options:
|
|
@@ -9648,6 +9776,7 @@ Global options:
|
|
|
9648
9776
|
--device <d> Only this device: desktop, mobile or tablet (naro analytics, naro insights)
|
|
9649
9777
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
9650
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)
|
|
9651
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)
|
|
9652
9781
|
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
9653
9782
|
branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
|
|
@@ -9792,6 +9921,8 @@ function routeStorage(words) {
|
|
|
9792
9921
|
return words.length <= 1 ? { handler: runStorageStatus, positionals: [] } : null;
|
|
9793
9922
|
case "enable":
|
|
9794
9923
|
return words.length === 1 ? { handler: runStorageEnable, positionals: [] } : null;
|
|
9924
|
+
case "disable":
|
|
9925
|
+
return words.length === 1 ? { handler: runStorageDisable, positionals: [] } : null;
|
|
9795
9926
|
case "bucket":
|
|
9796
9927
|
case "buckets":
|
|
9797
9928
|
switch (action) {
|
|
@@ -9836,7 +9967,16 @@ function routeStorage(words) {
|
|
|
9836
9967
|
return null;
|
|
9837
9968
|
}
|
|
9838
9969
|
}
|
|
9970
|
+
async function refuseTo() {
|
|
9971
|
+
throw new NaroError("VALIDATION_FAILED", "--to is only for naro promote.");
|
|
9972
|
+
}
|
|
9839
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) {
|
|
9840
9980
|
const { command, positionals } = parsed;
|
|
9841
9981
|
const sub = positionals[0];
|
|
9842
9982
|
switch (command) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@next-deploy/build": "0.1.0",
|
|
38
38
|
"@next-deploy/core": "0.1.0",
|
|
39
39
|
"@next-deploy/config": "0.1.0",
|
|
40
|
-
"@next-deploy/
|
|
41
|
-
"@next-deploy/
|
|
40
|
+
"@next-deploy/data-core": "0.1.0",
|
|
41
|
+
"@next-deploy/sdk": "0.1.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup && pnpm build:types",
|