cf-orbs 0.1.1 → 0.2.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 +37 -4
- package/dist/commands/deploy.js +4 -2
- package/dist/commands/doctor.js +13 -3
- package/dist/commands/init.js +352 -18
- package/dist/commands/upgrade.js +11 -2
- package/dist/file-checks.js +194 -0
- package/dist/index.js +5 -0
- package/dist/project.js +34 -3
- package/dist/wrangler-template.js +3 -0
- package/dist/wrangler.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ When Amp pauses an orb, cf-orbs saves the disk as a snapshot and stops the conta
|
|
|
35
35
|
| You need | Check |
|
|
36
36
|
| --- | --- |
|
|
37
37
|
| Node 22 or later (wrangler 4 needs it) | `node --version` |
|
|
38
|
-
| Docker, running. wrangler builds the orb image on your machine | `docker info` |
|
|
38
|
+
| Docker, running. wrangler builds the orb image on your machine (not for the [Workers Builds path](#install-without-docker-workers-builds-preview)) | `docker info` |
|
|
39
39
|
| A Cloudflare account on **Workers Paid** ($5/month; Containers need it) | Dashboard → Workers & Pages → Plans |
|
|
40
40
|
| wrangler logged in to that account | `npx wrangler login`, then `npx wrangler whoami` |
|
|
41
41
|
| An Amp provider token (`aop_live_…`) | Amp → Settings → Orbs → Add Provider. Note the provider name you type there |
|
|
@@ -95,6 +95,38 @@ node cli/bin/cf-orbs.js init ../my-orbs --provider-name <name> # asks for the
|
|
|
95
95
|
|
|
96
96
|
Then use `node <repo>/cli/bin/cf-orbs.js` wherever this README says `cf-orbs`, with `--dir ../my-orbs` or from inside that directory.
|
|
97
97
|
|
|
98
|
+
### Install without Docker (Workers Builds, preview)
|
|
99
|
+
|
|
100
|
+
No Docker on your machine? `init --no-deploy` makes a git repo and the Worker with its secrets, but does not deploy. You push the repo to GitHub or GitLab and connect it in the Cloudflare dashboard. Workers Builds then builds both images in your account and deploys on each push. Measured: first build 10.6 min, upgrade 5.6 min, plus 2-4 min in the queue.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npx cf-orbs init --no-deploy --provider-name <name from Amp> --token-file <file>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It prints the dashboard settings to use. Full procedure and caveats: [docs/install-workers-builds.md](https://github.com/meirdick/cf-orbs/blob/main/docs/install-workers-builds.md). This is a preview: the default install above (local Docker) and all defaults do not change.
|
|
107
|
+
|
|
108
|
+
### What init validates
|
|
109
|
+
|
|
110
|
+
Before the deploy, `init` stops with one `Failed: … — fix: …` line when:
|
|
111
|
+
|
|
112
|
+
- Node is older than 22.
|
|
113
|
+
- Docker does not answer `docker info` (not checked with `--no-deploy`).
|
|
114
|
+
- wrangler is not logged in, sees no account, or sees several and you gave no `--account-id` (or an id that `wrangler whoami` does not list).
|
|
115
|
+
- There is no Amp token, or it is empty or has spaces (a token without `aop_live_` gives a warning). With `--no-deploy` the token is optional.
|
|
116
|
+
- `cf-orbs.config.jsonc` is not valid: unknown keys, sizes out of range, a bad name or image key.
|
|
117
|
+
- An extra image key has no `image/<key>.Dockerfile`.
|
|
118
|
+
- The account has no workers.dev subdomain and you gave no `--subdomain` (in a terminal, `init` asks for one).
|
|
119
|
+
- `wrangler containers list` fails with a clear plan or login error (Workers Paid is needed). Any other error is a warning.
|
|
120
|
+
|
|
121
|
+
After the deploy, `init` checks that `/v1/health` answers, that `/v1/status` accepts the control token, that at least one prewarm instance starts from the new image (self-test; skipped when `prewarm` is 0, and with `--no-deploy` until a build has run), and that the provider runs for 15 s with no token rejection and no second provider on the token.
|
|
122
|
+
|
|
123
|
+
`cf-orbs doctor` also checks the project files, in every mode:
|
|
124
|
+
|
|
125
|
+
- `image/Dockerfile` (and each extra image key) and the provider image use `FROM <image>@sha256:<digest>`.
|
|
126
|
+
- `debugExec` is `false` (WARN when `true`).
|
|
127
|
+
- No `aop_live_` token and no control token value is in a file that git tracks (SKIP outside a git repo).
|
|
128
|
+
- `--no-deploy` projects only: `.gitignore` keeps out `.cf-orbs/control-token` and `.cf-orbs/state.json`, and `package.json` pins the wrangler version that cf-orbs uses.
|
|
129
|
+
|
|
98
130
|
## What you get
|
|
99
131
|
|
|
100
132
|
- Orbs that run on your Cloudflare account. The repo checkout, build output and saved disks stay there.
|
|
@@ -149,7 +181,7 @@ Example: 120 awake hours a month at 20% CPU costs about $27.50 a month in total
|
|
|
149
181
|
|
|
150
182
|
| Command | Does |
|
|
151
183
|
| --- | --- |
|
|
152
|
-
| `cf-orbs init [dir]` | Scaffold, deploy and check a provider. Flags: `--provider cloudflare\|external`, `--token-file`, `--account-id`, `--provider-name`, `--subdomain`, `--vcpu <1\|2\|3\|4
|
|
184
|
+
| `cf-orbs init [dir]` | Scaffold, deploy and check a provider. Flags: `--provider cloudflare\|external`, `--token-file`, `--account-id`, `--provider-name`, `--subdomain`, `--vcpu <1\|2\|3\|4>`, `--name <worker>`, `--no-deploy` (preview: [Workers Builds](https://github.com/meirdick/cf-orbs/blob/main/docs/install-workers-builds.md), no Docker) |
|
|
153
185
|
| `cf-orbs status [orb]` | Install status (provider, awake orbs, snapshots), or one orb |
|
|
154
186
|
| `cf-orbs logs <orb>` | Redacted bootstrap log of one orb. `--lines N` (default 200) |
|
|
155
187
|
| `cf-orbs logs --provider` | Redacted provider log (last 20 lines) |
|
|
@@ -240,11 +272,11 @@ To report a vulnerability, see [SECURITY.md](https://github.com/meirdick/cf-orbs
|
|
|
240
272
|
|
|
241
273
|
## Status and known gaps
|
|
242
274
|
|
|
243
|
-
v0.1. The control plane, dispatch program, provider host and CLI are built and unit tested. On 2026-10-30 a real Amp thread ran end to end on a fresh install: spawn from the image, setup, pause with a project snapshot, wake from the snapshot in 2.3 s, first answer 41 s after the message.
|
|
275
|
+
v0.2.1. The control plane, dispatch program, provider host and CLI are built and unit tested. On 2026-10-30 a real Amp thread ran end to end on a fresh install: spawn from the image, setup, pause with a project snapshot, wake from the snapshot in 2.3 s, first answer 41 s after the message.
|
|
244
276
|
|
|
245
277
|
Known gaps:
|
|
246
278
|
|
|
247
|
-
-
|
|
279
|
+
- The plan check before the first deploy is best effort: it asks wrangler to list Containers and stops only on a clear plan or login error. Any other error is a warning, and a Free account may still find out from the deploy error.
|
|
248
280
|
- Paused orbs restore after an image rebuild. A restored disk keeps its old content; new image content reaches only orbs started from the image.
|
|
249
281
|
- Orb lost mid-turn: Amp spawns the orb again and the agent runs the failed tool call again. The disk is kept; running processes and shell state are lost.
|
|
250
282
|
- Not tested yet: the provider's memory on `lite` over 24 h, restore after the source image is deleted from the registry, the Desktop pane, and Docker inside an orb. Snapshot storage has no published price (beta).
|
|
@@ -263,6 +295,7 @@ Method, results and costs: [docs/benchmarks.md](https://github.com/meirdick/cf-o
|
|
|
263
295
|
| Doc | Covers |
|
|
264
296
|
| --- | --- |
|
|
265
297
|
| [AGENTS.md](https://github.com/meirdick/cf-orbs/blob/main/AGENTS.md) | Install guide for an AI agent at a terminal |
|
|
298
|
+
| [Install without Docker](https://github.com/meirdick/cf-orbs/blob/main/docs/install-workers-builds.md) | Preview: Workers Builds builds and deploys from a git repo |
|
|
266
299
|
| [Provider host](https://github.com/meirdick/cf-orbs/blob/main/provider/README.md) | Cloudflare-hosted vs external provider, token rotation |
|
|
267
300
|
| [Orb image](https://github.com/meirdick/cf-orbs/blob/main/image/README.md) | The pinned image, toolchains, why snapshots are tied to it |
|
|
268
301
|
| [Dispatch](https://github.com/meirdick/cf-orbs/blob/main/dispatch/README.md) | The program Amp runs per job, and its protocol |
|
package/dist/commands/deploy.js
CHANGED
|
@@ -12,7 +12,8 @@ import { deployWithRetry, lastErrorLine, makeWrangler } from "../wrangler.js";
|
|
|
12
12
|
export async function deployProject(ctx, input) {
|
|
13
13
|
const { ui, paths } = ctx;
|
|
14
14
|
const { config, state } = input;
|
|
15
|
-
|
|
15
|
+
const git = state.mode === "git";
|
|
16
|
+
await materialize(paths, { templates: ctx.templates, git });
|
|
16
17
|
const missing = missingDockerfiles(paths, config);
|
|
17
18
|
if (missing.length > 0) {
|
|
18
19
|
throw new CliError(`image Dockerfile missing: ${missing.join(", ")}`, "create it, or remove its key from images in cf-orbs.config.jsonc");
|
|
@@ -38,7 +39,8 @@ export async function deployProject(ctx, input) {
|
|
|
38
39
|
...state,
|
|
39
40
|
url,
|
|
40
41
|
workerName: config.name,
|
|
41
|
-
mode:
|
|
42
|
+
// git mode stays git mode: Workers Builds still deploys from the repo
|
|
43
|
+
mode: git ? "git" : config.providerHost,
|
|
42
44
|
deployedAt: new Date(ctx.now()).toISOString(),
|
|
43
45
|
cliVersion: await cliVersion(),
|
|
44
46
|
});
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// cf-orbs doctor: PASS/FAIL per check with a one-line fix, plus INFO hints
|
|
1
|
+
// cf-orbs doctor: PASS/FAIL per check with a one-line fix, plus INFO hints and WARN lines that never fail.
|
|
2
|
+
// Read-only: never starts a provider or an orb.
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { collectImageKeys, expectOk, providerInfo } from "../api.js";
|
|
4
5
|
import { CONFIG_FILE, loadConfig, orbImageKeys } from "../config.js";
|
|
5
6
|
import { connect } from "../connect.js";
|
|
6
7
|
import { CliError, toFailedLine } from "../errors.js";
|
|
8
|
+
import { fileChecks } from "../file-checks.js";
|
|
7
9
|
import { fileMode, readState } from "../project.js";
|
|
8
10
|
import { classifyDeployFailure, lastErrorLine, makeWrangler } from "../wrangler.js";
|
|
9
11
|
import { AMP_CACHE_HINT } from "./snapshots.js";
|
|
@@ -77,6 +79,10 @@ export async function runDoctor(ctx) {
|
|
|
77
79
|
}
|
|
78
80
|
else
|
|
79
81
|
checks.push(pass("control token", "present, mode 600"));
|
|
82
|
+
checks.push(...(await fileChecks(ctx.run, ctx.paths, config, {
|
|
83
|
+
git: state.mode === "git",
|
|
84
|
+
templates: ctx.templates,
|
|
85
|
+
})));
|
|
80
86
|
const name = config?.name ?? state.workerName ?? "cf-orbs";
|
|
81
87
|
const w = makeWrangler(ctx.run, { accountId: state.accountId });
|
|
82
88
|
const d = await w.run(["deployments", "list", "--json", "--name", name], { timeoutMs: 60_000 });
|
|
@@ -106,20 +112,24 @@ export async function runDoctor(ctx) {
|
|
|
106
112
|
checks.push(failed("health", e));
|
|
107
113
|
}
|
|
108
114
|
let status;
|
|
115
|
+
// git mode: Workers Builds deploys, so the fix is the build, not cf-orbs deploy
|
|
116
|
+
const apiFailed = (n, e) => state.mode === "git" && e instanceof CliError
|
|
117
|
+
? failed(n, new CliError(e.cause_, `wait for the Workers Builds build to finish (Workers & Pages → ${name} → Deployments), or open its build log`))
|
|
118
|
+
: failed(n, e);
|
|
109
119
|
if (api) {
|
|
110
120
|
try {
|
|
111
121
|
expectOk(await api.get("/v1/health"), "health");
|
|
112
122
|
checks.push(pass("health", `${api.url}/v1/health ok`));
|
|
113
123
|
}
|
|
114
124
|
catch (e) {
|
|
115
|
-
checks.push(
|
|
125
|
+
checks.push(apiFailed("health", e));
|
|
116
126
|
}
|
|
117
127
|
try {
|
|
118
128
|
status = expectOk(await api.get("/v1/status"), "status");
|
|
119
129
|
checks.push(pass("status", "/v1/status ok (control token accepted)"));
|
|
120
130
|
}
|
|
121
131
|
catch (e) {
|
|
122
|
-
checks.push(
|
|
132
|
+
checks.push(apiFailed("status", e));
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
// Names only: wrangler secret list never returns values.
|
package/dist/commands/init.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
// cf-orbs init [dir]: zero to a live provider. plan() is pure (options + probes → steps); runInit() executes.
|
|
2
2
|
// Re-runnable: reuses the control token, keeps the Amp token secret when none is given, keeps user files.
|
|
3
|
+
// --no-deploy (git mode): no Docker, no deploy. Scaffold a repo for Workers Builds, create the Worker with its
|
|
4
|
+
// secrets, print the dashboard steps. Workers Builds then builds the images and deploys on each push.
|
|
3
5
|
import { existsSync } from "node:fs";
|
|
4
6
|
import { readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
5
8
|
import { expectOk, makeApi, providerInfo } from "../api.js";
|
|
6
9
|
import { flagBool, flagString } from "../args.js";
|
|
7
10
|
import { CONFIG_FILE, findTopLevelValue, instanceForVcpu, loadConfig, setTopLevelJsonc, } from "../config.js";
|
|
8
11
|
import { CliError, registerSecret } from "../errors.js";
|
|
12
|
+
import { filesWithSecrets, gitTrackedFiles } from "../file-checks.js";
|
|
9
13
|
import { dockerOk, nodeMajor, whoami } from "../probes.js";
|
|
10
|
-
import { generateToken, materialize, readControlToken, readState, writeControlToken, writeState, } from "../project.js";
|
|
14
|
+
import { cliVersion, GIT_DEPLOY_COMMAND, generateToken, gitPackageJson, materialize, missingDockerfiles, readControlToken, readState, withSecretsFile, writeControlToken, writeState, writeWranglerConfig, } from "../project.js";
|
|
11
15
|
import { checkSubdomain, registerSubdomain, suggestSubdomain, wranglerBearer } from "../subdomain.js";
|
|
12
16
|
import { ask, askHidden } from "../ui.js";
|
|
13
|
-
import { makeWrangler } from "../wrangler.js";
|
|
17
|
+
import { classifyDeployFailure, deployFailureError, lastErrorLine, makeWrangler, WRANGLER_PIN, } from "../wrangler.js";
|
|
14
18
|
import { deployProject, startPrewarm } from "./deploy.js";
|
|
15
19
|
export const PROVIDER_WAIT_MS = 90_000;
|
|
20
|
+
/** How long init waits for the prewarm instances (a first start on a cold host pulls the image). */
|
|
21
|
+
export const SELFTEST_WAIT_MS = 180_000;
|
|
16
22
|
/** Provider must stay up this long with no reject, no second-provider line and no restart. */
|
|
17
23
|
export const PROVIDER_STABLE_MS = 15_000;
|
|
18
24
|
const CRON_PERIOD_MS = 5 * 60_000;
|
|
@@ -29,7 +35,13 @@ export function plan(o, p) {
|
|
|
29
35
|
error: new CliError(`Node ${p.nodeVersion} is too old; cf-orbs needs Node 22 or later`, "install Node 22+ (https://nodejs.org) and rerun"),
|
|
30
36
|
});
|
|
31
37
|
}
|
|
32
|
-
if (
|
|
38
|
+
if (o.noDeploy && o.mode === "external") {
|
|
39
|
+
fails.push({
|
|
40
|
+
kind: "fail",
|
|
41
|
+
error: new CliError("--no-deploy supports only --provider cloudflare", "drop --provider external, or run cf-orbs init --provider external without --no-deploy"),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!p.docker && !o.noDeploy) {
|
|
33
45
|
fails.push({
|
|
34
46
|
kind: "fail",
|
|
35
47
|
error: new CliError("Docker is not running (wrangler builds the orb image locally)", "start Docker Desktop or the docker daemon, check `docker info`, then rerun"),
|
|
@@ -80,7 +92,17 @@ export function plan(o, p) {
|
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
let ampSource;
|
|
83
|
-
|
|
95
|
+
const hadProvider = p.prevMode === "cloudflare" || p.prevMode === "git";
|
|
96
|
+
if (o.noDeploy) {
|
|
97
|
+
// the Amp token is optional here: set it now or later, the Worker starts the provider once it has one
|
|
98
|
+
if (o.tokenFile)
|
|
99
|
+
ampSource = "file";
|
|
100
|
+
else if (p.envToken)
|
|
101
|
+
ampSource = "env";
|
|
102
|
+
else if (interactive)
|
|
103
|
+
ampSource = "prompt";
|
|
104
|
+
}
|
|
105
|
+
else if (o.mode === "cloudflare") {
|
|
84
106
|
if (o.tokenFile)
|
|
85
107
|
ampSource = "file";
|
|
86
108
|
else if (p.envToken)
|
|
@@ -88,7 +110,7 @@ export function plan(o, p) {
|
|
|
88
110
|
else if (interactive)
|
|
89
111
|
ampSource = "prompt";
|
|
90
112
|
// Keep only a secret that exists: external mode deletes it from the Worker.
|
|
91
|
-
else if (p.deployed &&
|
|
113
|
+
else if (p.deployed && hadProvider)
|
|
92
114
|
ampSource = "keep";
|
|
93
115
|
else if (p.deployed) {
|
|
94
116
|
fails.push({
|
|
@@ -105,15 +127,24 @@ export function plan(o, p) {
|
|
|
105
127
|
}
|
|
106
128
|
if (fails.length > 0)
|
|
107
129
|
return fails;
|
|
108
|
-
const steps = [
|
|
130
|
+
const steps = [
|
|
131
|
+
account,
|
|
132
|
+
{ kind: "plan-check" },
|
|
133
|
+
{ kind: "scaffold", mode: o.mode, git: Boolean(o.noDeploy) },
|
|
134
|
+
];
|
|
109
135
|
steps.push({ kind: "control-token", action: p.hasControlToken ? "reuse" : "generate" });
|
|
110
136
|
if (ampSource)
|
|
111
137
|
steps.push({ kind: "amp-token", source: ampSource });
|
|
138
|
+
if (o.noDeploy) {
|
|
139
|
+
steps.push({ kind: "subdomain" }, { kind: "git-files" }, { kind: "git-secrets" }, { kind: "selftest" }, { kind: "git-next" });
|
|
140
|
+
steps.push({ kind: "done", command: helloCommand(o.providerName) });
|
|
141
|
+
return steps;
|
|
142
|
+
}
|
|
112
143
|
const secrets = ["CONTROL_TOKEN"];
|
|
113
144
|
// "prompt" may still end as keep (Enter on a re-run); the executor drops the secret then.
|
|
114
145
|
if (ampSource && ampSource !== "keep")
|
|
115
146
|
secrets.push("AMP_ORB_PROVIDER_TOKEN");
|
|
116
|
-
steps.push({ kind: "subdomain" }, { kind: "deploy", secrets }, { kind: "health" }, { kind: "status" });
|
|
147
|
+
steps.push({ kind: "subdomain" }, { kind: "deploy", secrets }, { kind: "health" }, { kind: "status" }, { kind: "selftest" });
|
|
117
148
|
if (o.mode === "cloudflare") {
|
|
118
149
|
steps.push({ kind: "start-provider" }, { kind: "wait-provider", timeoutMs: PROVIDER_WAIT_MS });
|
|
119
150
|
}
|
|
@@ -122,9 +153,10 @@ export function plan(o, p) {
|
|
|
122
153
|
steps.push({ kind: "done", command: helloCommand(o.providerName) });
|
|
123
154
|
return steps;
|
|
124
155
|
}
|
|
125
|
-
|
|
156
|
+
/** noDeploy: git mode needs no Docker, so the probe is skipped. */
|
|
157
|
+
export async function gatherProbes(ctx, o = {}) {
|
|
126
158
|
const [docker, who, ct, state] = await Promise.all([
|
|
127
|
-
dockerOk(ctx.run),
|
|
159
|
+
o.noDeploy ? false : dockerOk(ctx.run),
|
|
128
160
|
whoami(ctx.run),
|
|
129
161
|
readControlToken(ctx.paths),
|
|
130
162
|
readState(ctx.paths),
|
|
@@ -150,6 +182,41 @@ function checkAmpToken(ctx, t) {
|
|
|
150
182
|
registerSecret(t);
|
|
151
183
|
return t;
|
|
152
184
|
}
|
|
185
|
+
function warmLast(body) {
|
|
186
|
+
const b = (body && typeof body === "object" ? body : {});
|
|
187
|
+
return (b.last && typeof b.last === "object" ? b.last : {});
|
|
188
|
+
}
|
|
189
|
+
/** One finished prewarm run: ok > 0 passes, ok = 0 fails init. */
|
|
190
|
+
function reportSelftest(ui, view) {
|
|
191
|
+
const count = Number(view.count) || 0;
|
|
192
|
+
const ok = Number(view.ok) || 0;
|
|
193
|
+
const ms = Array.isArray(view.msPerInstance) ? view.msPerInstance : [];
|
|
194
|
+
if (ok === 0) {
|
|
195
|
+
const jobs = Array.isArray(view.jobs) ? view.jobs : [];
|
|
196
|
+
const why = jobs.find((j) => typeof j.error === "string")?.error;
|
|
197
|
+
throw new CliError(`orb self-test failed: no instance started from the new image${why ? ` (${String(why)})` : ""}`, "run cf-orbs logs --provider and cf-orbs status, check the image build, then run cf-orbs deploy");
|
|
198
|
+
}
|
|
199
|
+
const t = ms.length > 0 ? ` in ${ms.map((m) => `${(m / 1000).toFixed(0)} s`).join(" and ")}` : "";
|
|
200
|
+
if (ok < count)
|
|
201
|
+
ui.warn(`self-test: only ${ok} of ${count} instances started${t}`);
|
|
202
|
+
else
|
|
203
|
+
ui.step(`self-test: orb image starts (${ok} of ${count} ready${t})`);
|
|
204
|
+
}
|
|
205
|
+
/** --no-deploy: the last prewarm run of an already built Worker, or undefined. Never throws. */
|
|
206
|
+
async function lastPrewarm(url, token, ctx) {
|
|
207
|
+
if (!url || !token)
|
|
208
|
+
return undefined;
|
|
209
|
+
try {
|
|
210
|
+
const r = await makeApi(url, token, ctx.fetch).get("/v1/warm");
|
|
211
|
+
if (r.status !== 200)
|
|
212
|
+
return undefined;
|
|
213
|
+
const last = warmLast(r.body);
|
|
214
|
+
return typeof last.at === "string" ? last : undefined;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
153
220
|
export async function runInit(ctx, o, steps) {
|
|
154
221
|
const { ui, paths } = ctx;
|
|
155
222
|
let state = await readState(paths);
|
|
@@ -165,6 +232,11 @@ export async function runInit(ctx, o, steps) {
|
|
|
165
232
|
let providerStartedByCron = false;
|
|
166
233
|
let accountName;
|
|
167
234
|
let providerName = o.providerName;
|
|
235
|
+
let prewarmStarted = -1;
|
|
236
|
+
let prewarmAt = 0;
|
|
237
|
+
let subdomain;
|
|
238
|
+
let repoExisted = false;
|
|
239
|
+
let git;
|
|
168
240
|
// Show every failed precondition in one run, not one per rerun.
|
|
169
241
|
const fails = steps.filter((s) => s.kind === "fail");
|
|
170
242
|
for (const f of fails.slice(0, -1))
|
|
@@ -197,6 +269,7 @@ export async function runInit(ctx, o, steps) {
|
|
|
197
269
|
break; // unknown auth type: wrangler deploy reports a missing subdomain itself
|
|
198
270
|
const c = await checkSubdomain(ctx.fetch, accountId, bearer);
|
|
199
271
|
if (c.kind === "has") {
|
|
272
|
+
subdomain = c.subdomain;
|
|
200
273
|
ui.step(`workers.dev subdomain: ${c.subdomain}`);
|
|
201
274
|
break;
|
|
202
275
|
}
|
|
@@ -211,6 +284,7 @@ export async function runInit(ctx, o, steps) {
|
|
|
211
284
|
name = (await ask(`This account has no workers.dev subdomain. Name for it [${def}]: `)) || def;
|
|
212
285
|
}
|
|
213
286
|
await registerSubdomain(ctx.fetch, accountId, bearer, name);
|
|
287
|
+
subdomain = name;
|
|
214
288
|
ui.step(`workers.dev subdomain: registered ${name}.workers.dev`);
|
|
215
289
|
break;
|
|
216
290
|
}
|
|
@@ -218,6 +292,7 @@ export async function runInit(ctx, o, steps) {
|
|
|
218
292
|
const r = await materialize(paths, {
|
|
219
293
|
templates: ctx.templates,
|
|
220
294
|
configOverrides: { providerHost: s.mode },
|
|
295
|
+
git: s.git,
|
|
221
296
|
});
|
|
222
297
|
if (r.wroteConfig)
|
|
223
298
|
ui.step(`scaffold: wrote ${CONFIG_FILE}`);
|
|
@@ -231,6 +306,11 @@ export async function runInit(ctx, o, steps) {
|
|
|
231
306
|
config = await loadConfig(paths.config);
|
|
232
307
|
ui.step(`scaffold: providerHost set to ${s.mode}`);
|
|
233
308
|
}
|
|
309
|
+
if (o.name !== undefined && config.name !== o.name) {
|
|
310
|
+
await writeFile(paths.config, setTopLevelJsonc(await readFile(paths.config, "utf8"), "name", o.name));
|
|
311
|
+
config = await loadConfig(paths.config);
|
|
312
|
+
ui.step(`scaffold: name set to ${o.name}`);
|
|
313
|
+
}
|
|
234
314
|
// --vcpu overwrites "instance"; without it a re-run keeps what the config has
|
|
235
315
|
if (o.vcpu !== undefined && !sameInstance(config.instance, instanceForVcpu(o.vcpu))) {
|
|
236
316
|
const t = await readFile(paths.config, "utf8");
|
|
@@ -238,7 +318,13 @@ export async function runInit(ctx, o, steps) {
|
|
|
238
318
|
config = await loadConfig(paths.config);
|
|
239
319
|
ui.step(`scaffold: instance set to ${o.vcpu} vCPU, ${o.vcpu * 3} GiB, 20 GB disk`);
|
|
240
320
|
}
|
|
241
|
-
state = {
|
|
321
|
+
state = {
|
|
322
|
+
...state,
|
|
323
|
+
accountId,
|
|
324
|
+
providerName: o.providerName,
|
|
325
|
+
mode: s.git ? "git" : s.mode,
|
|
326
|
+
workerName: config.name,
|
|
327
|
+
};
|
|
242
328
|
await writeState(paths, state);
|
|
243
329
|
break;
|
|
244
330
|
}
|
|
@@ -263,9 +349,16 @@ export async function runInit(ctx, o, steps) {
|
|
|
263
349
|
}
|
|
264
350
|
else if (s.source === "prompt") {
|
|
265
351
|
// Keep only a secret that exists: external mode deleted it from the Worker.
|
|
266
|
-
const keep = Boolean(state.url) && prevMode === "cloudflare";
|
|
267
|
-
const
|
|
268
|
-
|
|
352
|
+
const keep = Boolean(state.url) && (prevMode === "cloudflare" || prevMode === "git");
|
|
353
|
+
const enter = o.noDeploy
|
|
354
|
+
? "; Enter skips (set it later)"
|
|
355
|
+
: keep
|
|
356
|
+
? "; Enter keeps the current one"
|
|
357
|
+
: "";
|
|
358
|
+
const t = await askHidden(`Amp provider token (aop_live_…, not shown)${enter}: `);
|
|
359
|
+
if (t === "" && o.noDeploy)
|
|
360
|
+
ui.step("amp token: not given; set it later (see the steps below)");
|
|
361
|
+
else if (t === "" && keep)
|
|
269
362
|
ui.step("amp token: keeping the current Worker secret");
|
|
270
363
|
else
|
|
271
364
|
ampToken = checkAmpToken(ctx, t);
|
|
@@ -320,8 +413,69 @@ export async function runInit(ctx, o, steps) {
|
|
|
320
413
|
lastStatus = expectOk(await api.get("/v1/status"), "status check");
|
|
321
414
|
ui.step("health: /v1/status ok (control token accepted)");
|
|
322
415
|
if (config)
|
|
323
|
-
await startPrewarm(ctx, api, config);
|
|
416
|
+
prewarmStarted = (await startPrewarm(ctx, api, config)).started;
|
|
417
|
+
prewarmAt = ctx.now();
|
|
324
418
|
break;
|
|
419
|
+
case "selftest": {
|
|
420
|
+
if (o.noDeploy) {
|
|
421
|
+
// Workers Builds deploys later, so this run started no prewarm. Report an earlier run if the
|
|
422
|
+
// Worker has one (a rerun after a build); else skip.
|
|
423
|
+
const last = await lastPrewarm(url, controlToken, ctx);
|
|
424
|
+
if (last && Number(last.pending) === 0 && Number(last.count) > 0) {
|
|
425
|
+
// an old failed run does not fail init: the next build may fix the image
|
|
426
|
+
if (Number(last.ok) > 0)
|
|
427
|
+
reportSelftest(ui, last);
|
|
428
|
+
else
|
|
429
|
+
ui.warn("self-test: the last prewarm run started no instance; check the build, then cf-orbs status");
|
|
430
|
+
}
|
|
431
|
+
else
|
|
432
|
+
ui.step("self-test: skipped (Workers Builds deploys later; after the first build, cf-orbs status shows the prewarm result)");
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
// The prewarm run starts throwaway orbs from the new image. If one becomes ready, the image,
|
|
436
|
+
// the registry and the container runtime work. No Amp thread and no debug route needed.
|
|
437
|
+
if (prewarmStarted === 0) {
|
|
438
|
+
ui.step('self-test: skipped (no prewarm instance was started; set "prewarm" above 0 to enable)');
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
const until = ctx.now() + SELFTEST_WAIT_MS;
|
|
442
|
+
let view = {};
|
|
443
|
+
for (;;) {
|
|
444
|
+
const r = await api.get("/v1/warm");
|
|
445
|
+
const last = warmLast(r.body);
|
|
446
|
+
// Ignore the result of an earlier run.
|
|
447
|
+
if (typeof last.at === "string" && Date.parse(last.at) >= prewarmAt - 10_000) {
|
|
448
|
+
view = last;
|
|
449
|
+
if (Number(last.pending) === 0)
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
if (ctx.now() >= until)
|
|
453
|
+
break;
|
|
454
|
+
await ctx.sleep(3_000);
|
|
455
|
+
}
|
|
456
|
+
if (Number(view.count) > 0 && Number(view.pending) === 0)
|
|
457
|
+
reportSelftest(ui, view);
|
|
458
|
+
else
|
|
459
|
+
ui.warn("self-test: still running after 3 min; the first start on a new host pulls the image. Check cf-orbs status");
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case "plan-check": {
|
|
463
|
+
// Before the image build: a Free account fails here in seconds, not after a 1.8 GB push.
|
|
464
|
+
// Best effort: only a clear plan or login error stops init; any other error is a warning.
|
|
465
|
+
const r = await makeWrangler(ctx.run, { accountId }).run(["containers", "list", "--json"], {
|
|
466
|
+
timeoutMs: 60_000,
|
|
467
|
+
});
|
|
468
|
+
if (r.code === 0) {
|
|
469
|
+
ui.step("plan: Containers are available on this account");
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
473
|
+
const kind = classifyDeployFailure(out);
|
|
474
|
+
if (kind === "paid" || kind === "auth")
|
|
475
|
+
throw deployFailureError(kind, out);
|
|
476
|
+
ui.warn(`plan check skipped (wrangler containers list failed: ${lastErrorLine(out)})`);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
325
479
|
case "start-provider": {
|
|
326
480
|
const r = await api.post("/v1/provider/start");
|
|
327
481
|
if (r.status === 404 || r.status === 405) {
|
|
@@ -365,6 +519,52 @@ export async function runInit(ctx, o, steps) {
|
|
|
365
519
|
}
|
|
366
520
|
break;
|
|
367
521
|
}
|
|
522
|
+
case "git-files": {
|
|
523
|
+
if (!config)
|
|
524
|
+
throw new Error("init plan bug: git-files before scaffold");
|
|
525
|
+
const missing = missingDockerfiles(paths, config);
|
|
526
|
+
if (missing.length > 0)
|
|
527
|
+
throw new CliError(`image Dockerfile missing: ${missing.join(", ")}`, "create it, or remove its key from images in cf-orbs.config.jsonc");
|
|
528
|
+
await writeWranglerConfig(paths, config, accountId);
|
|
529
|
+
ui.step("git: wrote .cf-orbs/wrangler.jsonc (Workers Builds deploys with it)");
|
|
530
|
+
if (!existsSync(paths.packageJson)) {
|
|
531
|
+
await writeFile(paths.packageJson, gitPackageJson(config.name, WRANGLER_PIN));
|
|
532
|
+
ui.step(`git: wrote package.json (wrangler ${WRANGLER_PIN} for Workers Builds)`);
|
|
533
|
+
}
|
|
534
|
+
await writeLockfile(ctx);
|
|
535
|
+
repoExisted = await gitCommitNew(ctx, ampToken);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case "git-secrets": {
|
|
539
|
+
if (!config || !controlToken)
|
|
540
|
+
throw new Error("init plan bug: git-secrets before scaffold");
|
|
541
|
+
const secrets = { CONTROL_TOKEN: controlToken };
|
|
542
|
+
if (ampToken)
|
|
543
|
+
secrets.AMP_ORB_PROVIDER_TOKEN = ampToken;
|
|
544
|
+
await putSecrets(ctx, config.name, accountId, secrets);
|
|
545
|
+
url = subdomain ? `https://${config.name}.${subdomain}.workers.dev` : state.url;
|
|
546
|
+
if (!url)
|
|
547
|
+
ui.warn("Failed: could not read the account's workers.dev subdomain, so state.json has no URL — fix: after the first build, pass --url https://<name>.<subdomain>.workers.dev to status and doctor");
|
|
548
|
+
state = {
|
|
549
|
+
...state,
|
|
550
|
+
url,
|
|
551
|
+
accountId,
|
|
552
|
+
mode: "git",
|
|
553
|
+
workerName: config.name,
|
|
554
|
+
cliVersion: await cliVersion(),
|
|
555
|
+
};
|
|
556
|
+
await writeState(paths, state);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case "git-next":
|
|
560
|
+
git = gitNext({
|
|
561
|
+
worker: config?.name ?? "cf-orbs",
|
|
562
|
+
repoExisted,
|
|
563
|
+
ampTokenGiven: Boolean(ampToken),
|
|
564
|
+
});
|
|
565
|
+
for (const l of git.lines)
|
|
566
|
+
ui.out(l);
|
|
567
|
+
break;
|
|
368
568
|
case "print-external-env":
|
|
369
569
|
ui.out("External provider mode. On an always-on host with the amp CLI, run:");
|
|
370
570
|
ui.out(" export AMP_ORB_PROVIDER_TOKEN=<your aop_live_ token>");
|
|
@@ -379,7 +579,123 @@ export async function runInit(ctx, o, steps) {
|
|
|
379
579
|
break;
|
|
380
580
|
}
|
|
381
581
|
}
|
|
382
|
-
return { url, command, providerName };
|
|
582
|
+
return { url, command, providerName, ...(git ? { git } : {}) };
|
|
583
|
+
}
|
|
584
|
+
/** Workers Builds settings for the dashboard (Settings → Builds → Connect), as tested on 2026-11-02. */
|
|
585
|
+
export const GIT_PATH_FILTERS = ["image/**", ".cf-orbs/**", "cf-orbs.config.jsonc", "package*.json"];
|
|
586
|
+
/** The next steps init --no-deploy prints. Pure. */
|
|
587
|
+
export function gitNext(o) {
|
|
588
|
+
const settings = {
|
|
589
|
+
worker: o.worker,
|
|
590
|
+
branch: "main",
|
|
591
|
+
buildCommand: "",
|
|
592
|
+
deployCommand: GIT_DEPLOY_COMMAND,
|
|
593
|
+
rootDirectory: "/",
|
|
594
|
+
previews: false,
|
|
595
|
+
apiToken: "default build token",
|
|
596
|
+
buildWatchPaths: GIT_PATH_FILTERS,
|
|
597
|
+
};
|
|
598
|
+
const push = o.repoExisted
|
|
599
|
+
? [
|
|
600
|
+
"1. Commit these files, then push the repo to GitHub or GitLab:",
|
|
601
|
+
" git add -A && git commit -m 'cf-orbs init'",
|
|
602
|
+
" git push",
|
|
603
|
+
]
|
|
604
|
+
: [
|
|
605
|
+
"1. Make an empty repo on GitHub or GitLab. Push this directory to it:",
|
|
606
|
+
" git remote add origin <repo URL>",
|
|
607
|
+
" git push -u origin main",
|
|
608
|
+
];
|
|
609
|
+
const lines = [
|
|
610
|
+
"",
|
|
611
|
+
"Next steps. Workers Builds builds the images in your Cloudflare account; this machine needs no Docker.",
|
|
612
|
+
...push,
|
|
613
|
+
`2. Connect the repo: Cloudflare dashboard → Workers & Pages → ${o.worker} → Settings → Builds → Connect.`,
|
|
614
|
+
" Repository: the repo from step 1",
|
|
615
|
+
` Branch: ${settings.branch}${o.repoExisted ? " (the branch you push)" : ""}`,
|
|
616
|
+
" Build command: (leave empty)",
|
|
617
|
+
` Deploy command: ${settings.deployCommand}`,
|
|
618
|
+
` Root directory: ${settings.rootDirectory}`,
|
|
619
|
+
" Previews (builds for non-production branches): off",
|
|
620
|
+
" API token: the default build token",
|
|
621
|
+
` Build watch paths: include ${settings.buildWatchPaths.join(", ")}`,
|
|
622
|
+
" If no build starts, push a commit. Measured: first build 10.6 min, plus 2-4 min in the queue.",
|
|
623
|
+
"3. After the first build: cf-orbs status",
|
|
624
|
+
" The 5-min cron starts the provider. The first spawn on each host pulls the image (40-50 s).",
|
|
625
|
+
...(o.ampTokenGiven
|
|
626
|
+
? []
|
|
627
|
+
: [
|
|
628
|
+
"4. No Amp token given. If the Worker has none yet, set it with:",
|
|
629
|
+
" cf-orbs init --no-deploy --token-file <file> (no build needed)",
|
|
630
|
+
]),
|
|
631
|
+
"Upgrades: edit image/Dockerfile or cf-orbs.config.jsonc (or cf-orbs upgrade --digest sha256:<hex> --no-deploy), commit, push.",
|
|
632
|
+
"Docs: https://github.com/meirdick/cf-orbs/blob/main/docs/install-workers-builds.md",
|
|
633
|
+
"Then try it (spends Amp credits):",
|
|
634
|
+
];
|
|
635
|
+
return { settings, lines };
|
|
636
|
+
}
|
|
637
|
+
/** package-lock.json for npm clean-install in Workers Builds. A failure is one warning. */
|
|
638
|
+
async function writeLockfile(ctx) {
|
|
639
|
+
const { paths, ui } = ctx;
|
|
640
|
+
if (existsSync(join(paths.dir, "package-lock.json")))
|
|
641
|
+
return;
|
|
642
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
643
|
+
const r = await ctx.run(npm, ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"], {
|
|
644
|
+
cwd: paths.dir,
|
|
645
|
+
timeoutMs: 180_000,
|
|
646
|
+
});
|
|
647
|
+
if (r.code === 0)
|
|
648
|
+
ui.step("git: wrote package-lock.json");
|
|
649
|
+
else
|
|
650
|
+
ui.warn(`Failed: could not write package-lock.json (${lastErrorLine(`${r.stdout}\n${r.stderr}`)}) — fix: run npm install --package-lock-only in ${paths.dir}, then commit it`);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* git init + first commit when dir is in no git work tree. Returns true when a repo was there already.
|
|
654
|
+
* Checks the staged files for tokens before the commit.
|
|
655
|
+
*/
|
|
656
|
+
async function gitCommitNew(ctx, ampToken) {
|
|
657
|
+
const { paths, ui } = ctx;
|
|
658
|
+
const g = (args) => ctx.run("git", ["-C", paths.dir, ...args], { timeoutMs: 60_000 });
|
|
659
|
+
if ((await g(["rev-parse", "--is-inside-work-tree"])).code === 0) {
|
|
660
|
+
ui.step("git: this directory is in a git repo already; commit and push the changes yourself");
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
const init = await g(["init", "-b", "main"]);
|
|
664
|
+
if (init.code !== 0) {
|
|
665
|
+
ui.warn(`Failed: git init did not run (${lastErrorLine(`${init.stdout}\n${init.stderr}`)}) — fix: install git, then run git init -b main && git add -A && git commit -m 'cf-orbs init' in ${paths.dir}`);
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
await g(["add", "-A"]);
|
|
669
|
+
const ct = await readControlToken(paths);
|
|
670
|
+
const staged = (await gitTrackedFiles(ctx.run, paths.dir)) ?? [];
|
|
671
|
+
const hits = await filesWithSecrets(paths.dir, staged, [
|
|
672
|
+
...(ct ? [ct] : []),
|
|
673
|
+
...(ampToken ? [ampToken] : []),
|
|
674
|
+
]);
|
|
675
|
+
if (hits.length > 0) {
|
|
676
|
+
await g(["reset", "-q"]);
|
|
677
|
+
throw new CliError(`a token is in file(s) staged for the first commit: ${hits.join(", ")}`, "remove the token from those files (or add them to .gitignore), then rerun cf-orbs init --no-deploy");
|
|
678
|
+
}
|
|
679
|
+
const c = await g(["commit", "-q", "-m", "cf-orbs init (Workers Builds)"]);
|
|
680
|
+
if (c.code === 0)
|
|
681
|
+
ui.step(`git: made a repo on branch main with a first commit (${staged.length} files)`);
|
|
682
|
+
else
|
|
683
|
+
ui.warn(`Failed: git commit did not run (${lastErrorLine(`${c.stdout}\n${c.stderr}`)}) — fix: set git config user.name and user.email, then git commit -m 'cf-orbs init' in ${paths.dir}`);
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
/** wrangler secret bulk from a 0600 temp file. Creates the Worker (a placeholder) when it does not exist. */
|
|
687
|
+
async function putSecrets(ctx, name, accountId, secrets) {
|
|
688
|
+
const w = makeWrangler(ctx.run, { configPath: ctx.paths.wrangler, accountId });
|
|
689
|
+
const r = await withSecretsFile(secrets, (file) => w.run(["secret", "bulk", file], { env: { CI: "1" }, timeoutMs: 120_000 }));
|
|
690
|
+
const out = `${r.stdout}\n${r.stderr}`;
|
|
691
|
+
if (r.code !== 0 || /Secrets failed to upload|No content found/i.test(out)) {
|
|
692
|
+
throw classifyDeployFailure(out) === "auth"
|
|
693
|
+
? new CliError("wrangler is not logged in or the token lacks permissions", "run npx wrangler login, then rerun")
|
|
694
|
+
: new CliError(`wrangler secret bulk failed: ${lastErrorLine(out)}`, "check npx wrangler whoami, then rerun cf-orbs init --no-deploy");
|
|
695
|
+
}
|
|
696
|
+
if (/Creating new Worker/i.test(out))
|
|
697
|
+
ctx.ui.step(`worker: created ${name} (a placeholder until the first build)`);
|
|
698
|
+
ctx.ui.step(`secrets: set ${Object.keys(secrets).join(" and ")} on Worker ${name}`);
|
|
383
699
|
}
|
|
384
700
|
/**
|
|
385
701
|
* Read the provider log tail from /v1/status. Only lines since the last container start count.
|
|
@@ -425,16 +741,22 @@ export function parseVcpuFlag(v) {
|
|
|
425
741
|
throw new CliError(`--vcpu must be 1, 2, 3 or 4, got ${JSON.stringify(v)}`, "pass --vcpu 4 (default, 12 GiB) or --vcpu 2 (6 GiB, about half the cost)");
|
|
426
742
|
return Number(v);
|
|
427
743
|
}
|
|
744
|
+
const WORKER_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
428
745
|
export function initOptions(ctx, existing) {
|
|
429
746
|
const modeFlag = flagString(ctx.args, "provider");
|
|
430
747
|
if (modeFlag !== undefined && modeFlag !== "cloudflare" && modeFlag !== "external") {
|
|
431
748
|
throw new CliError(`--provider must be cloudflare or external, got ${modeFlag}`, "pass --provider cloudflare (default) or --provider external");
|
|
432
749
|
}
|
|
750
|
+
const name = flagString(ctx.args, "name");
|
|
751
|
+
if (name !== undefined && !WORKER_NAME_RE.test(name))
|
|
752
|
+
throw new CliError(`--name ${JSON.stringify(name)} is not a valid Worker name (a-z, 0-9, -; max 63)`, "pass --name cf-orbs or another lowercase name");
|
|
433
753
|
const explicitName = flagString(ctx.args, "provider-name") ?? ctx.env.AMP_ORB_PROVIDER_NAME ?? existing?.providerName;
|
|
434
754
|
return {
|
|
435
755
|
providerNameExplicit: explicitName !== undefined,
|
|
436
756
|
subdomain: flagString(ctx.args, "subdomain") ?? ctx.env.CF_ORBS_SUBDOMAIN ?? undefined,
|
|
437
757
|
vcpu: parseVcpuFlag(flagString(ctx.args, "vcpu")),
|
|
758
|
+
name,
|
|
759
|
+
noDeploy: flagBool(ctx.args, "no-deploy"),
|
|
438
760
|
mode: modeFlag ?? existing?.providerHost ?? "cloudflare",
|
|
439
761
|
nonInteractive: flagBool(ctx.args, "non-interactive") || Boolean(ctx.env.CI),
|
|
440
762
|
tokenFile: flagString(ctx.args, "token-file"),
|
|
@@ -455,11 +777,23 @@ export async function initCommand(ctx) {
|
|
|
455
777
|
}
|
|
456
778
|
existing.providerName = (await readState(paths)).providerName;
|
|
457
779
|
const o = initOptions(ctx, existing);
|
|
458
|
-
ui.step(`cf-orbs init in ${paths.dir} (provider host: ${o.mode})`);
|
|
459
|
-
const probes = await gatherProbes(ctx);
|
|
780
|
+
ui.step(`cf-orbs init in ${paths.dir} (provider host: ${o.mode}${o.noDeploy ? "; --no-deploy: Workers Builds deploys" : ""})`);
|
|
781
|
+
const probes = await gatherProbes(ctx, o);
|
|
460
782
|
const steps = plan(o, probes);
|
|
461
783
|
const r = await runInit(ctx, o, steps);
|
|
462
|
-
|
|
784
|
+
if (r.git)
|
|
785
|
+
ui.result({
|
|
786
|
+
ok: true,
|
|
787
|
+
mode: "git",
|
|
788
|
+
providerHost: o.mode,
|
|
789
|
+
url: r.url,
|
|
790
|
+
providerName: r.providerName,
|
|
791
|
+
settings: r.git.settings,
|
|
792
|
+
steps: r.git.lines.filter((l) => l !== ""),
|
|
793
|
+
next: r.command,
|
|
794
|
+
});
|
|
795
|
+
else
|
|
796
|
+
ui.result({ ok: true, url: r.url, mode: o.mode, providerName: r.providerName, next: r.command });
|
|
463
797
|
if (r.command)
|
|
464
798
|
ui.out(r.command);
|
|
465
799
|
return 0;
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -8,7 +8,7 @@ import { makeApi } from "../api.js";
|
|
|
8
8
|
import { flagBool, flagString } from "../args.js";
|
|
9
9
|
import { AMP_DEFAULT_IMAGE_REPO, CONFIG_FILE, loadConfig, parseConfigText, setTopLevelJsonc, } from "../config.js";
|
|
10
10
|
import { CliError } from "../errors.js";
|
|
11
|
-
import { readState, requireControlToken } from "../project.js";
|
|
11
|
+
import { materialize, readState, requireControlToken, writeWranglerConfig } from "../project.js";
|
|
12
12
|
import { deployProject, restartProviderAfterDeploy, startPrewarm } from "./deploy.js";
|
|
13
13
|
import { AMP_CACHE_STEPS } from "./snapshots.js";
|
|
14
14
|
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -73,7 +73,16 @@ export async function upgradeCommand(ctx) {
|
|
|
73
73
|
ui.warn("paused orbs may not restore after upgrade (their snapshots name image key orb, which is now the new image); push or commit their work first");
|
|
74
74
|
ui.warn("awake orbs keep the old Worker code until they pause; keep the old key until their snapshots expire (30 days)");
|
|
75
75
|
if (flagBool(ctx.args, "no-deploy")) {
|
|
76
|
-
|
|
76
|
+
const state = await readState(paths);
|
|
77
|
+
if (state.mode === "git") {
|
|
78
|
+
// Workers Builds deploys the committed wrangler.jsonc, so write it now with the new images entry
|
|
79
|
+
await materialize(paths, { templates: ctx.templates, git: true });
|
|
80
|
+
await writeWranglerConfig(paths, await loadConfig(paths.config), state.accountId);
|
|
81
|
+
ui.step("upgrade: wrote .cf-orbs/wrangler.jsonc");
|
|
82
|
+
}
|
|
83
|
+
ui.out(state.mode === "git"
|
|
84
|
+
? "Commit and push: Workers Builds builds and deploys the new image."
|
|
85
|
+
: "Run cf-orbs deploy to build and push the new image.");
|
|
77
86
|
for (const l of AMP_CACHE_STEPS)
|
|
78
87
|
ui.out(l);
|
|
79
88
|
ui.result({ ok: true, changed: true, deployed: false, oldKey: p.oldKey, nextSteps: AMP_CACHE_STEPS });
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Checks on the project files: image pins, debugExec, secrets in tracked files, git mode files.
|
|
2
|
+
// Used by doctor (all modes) and by init --no-deploy before its first commit. Read-only.
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { CONFIG_FILE } from "./config.js";
|
|
7
|
+
import { CliError, toFailedLine } from "./errors.js";
|
|
8
|
+
import { readControlToken, templatesDir } from "./project.js";
|
|
9
|
+
import { WRANGLER_PIN } from "./wrangler.js";
|
|
10
|
+
const pass = (name, detail) => ({ name, status: "PASS", detail });
|
|
11
|
+
const failed = (name, e) => ({ name, status: "FAIL", detail: toFailedLine(e) });
|
|
12
|
+
const skip = (name, detail) => ({ name, status: "SKIP", detail });
|
|
13
|
+
const DIGEST_REF = /@sha256:[0-9a-f]{64}$/;
|
|
14
|
+
/** Base image refs in a Dockerfile that have no @sha256 digest. Earlier stage names and scratch are fine. */
|
|
15
|
+
export function unpinnedFroms(dockerfile) {
|
|
16
|
+
const stages = new Set(["scratch"]);
|
|
17
|
+
const out = [];
|
|
18
|
+
for (const line of dockerfile.split(/\r?\n/)) {
|
|
19
|
+
const m = /^\s*FROM\s+(.+)$/i.exec(line);
|
|
20
|
+
if (!m)
|
|
21
|
+
continue;
|
|
22
|
+
const words = m[1].trim().split(/\s+/);
|
|
23
|
+
const ref = words.find((w) => !w.startsWith("--"));
|
|
24
|
+
if (!ref)
|
|
25
|
+
continue;
|
|
26
|
+
const known = stages.has(ref.toLowerCase());
|
|
27
|
+
const as = words.findIndex((w) => w.toLowerCase() === "as");
|
|
28
|
+
if (as !== -1 && words[as + 1])
|
|
29
|
+
stages.add(words[as + 1].toLowerCase());
|
|
30
|
+
if (!known && !DIGEST_REF.test(ref))
|
|
31
|
+
out.push(ref);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** "<repo>@sha256:0123456789ab" for a PASS line. */
|
|
36
|
+
function shortRefs(dockerfile) {
|
|
37
|
+
const refs = [...dockerfile.matchAll(/^\s*FROM\s+(?:--\S+\s+)*(\S+)/gim)].map((m) => m[1].replace(/@sha256:([0-9a-f]{12})[0-9a-f]{52}$/, "@sha256:$1…"));
|
|
38
|
+
return refs.join(", ");
|
|
39
|
+
}
|
|
40
|
+
async function pinCheck(name, file, label, fix) {
|
|
41
|
+
let text;
|
|
42
|
+
try {
|
|
43
|
+
text = await readFile(file, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return failed(name, new CliError(`${label} not found`, "run cf-orbs init in this directory"));
|
|
47
|
+
}
|
|
48
|
+
const bad = unpinnedFroms(text);
|
|
49
|
+
if (bad.length > 0)
|
|
50
|
+
return failed(name, new CliError(`${label} FROM ${bad.join(", ")} has no @sha256 digest`, fix));
|
|
51
|
+
return pass(name, `${label} FROM ${shortRefs(text)} (by digest)`);
|
|
52
|
+
}
|
|
53
|
+
/** image/Dockerfile and the Dockerfile of every extra image key: each FROM is pinned by digest. */
|
|
54
|
+
export async function orbImagePinCheck(p, c) {
|
|
55
|
+
const keys = [...new Set(Object.values(c?.images ?? {}))];
|
|
56
|
+
const files = [
|
|
57
|
+
["image/Dockerfile", p.imageDockerfile],
|
|
58
|
+
...keys.map((k) => [`image/${k}.Dockerfile`, join(p.imageDir, `${k}.Dockerfile`)]),
|
|
59
|
+
];
|
|
60
|
+
const fix = "pin the base image as <image>@sha256:<digest> (for the Amp image: cf-orbs upgrade --digest sha256:<hex>)";
|
|
61
|
+
let first;
|
|
62
|
+
let extra = 0;
|
|
63
|
+
for (const [label, file] of files) {
|
|
64
|
+
if (label !== "image/Dockerfile" && !existsSync(file))
|
|
65
|
+
continue; // deploy reports a missing one
|
|
66
|
+
const c1 = await pinCheck("orb image pin", file, label, fix);
|
|
67
|
+
if (c1.status === "FAIL")
|
|
68
|
+
return c1;
|
|
69
|
+
if (first)
|
|
70
|
+
extra++;
|
|
71
|
+
first ??= c1;
|
|
72
|
+
}
|
|
73
|
+
return extra > 0 && first
|
|
74
|
+
? { ...first, detail: `${first.detail}; ${extra} extra image key(s) pinned too` }
|
|
75
|
+
: first;
|
|
76
|
+
}
|
|
77
|
+
/** The provider image cf-orbs builds: the project copy (.cf-orbs/provider) if there is one, else the template. */
|
|
78
|
+
export async function providerImagePinCheck(p, templates = templatesDir()) {
|
|
79
|
+
const copy = join(p.stateDir, "provider", "Dockerfile");
|
|
80
|
+
const tpl = join(templates, "provider", "Dockerfile");
|
|
81
|
+
// In a source checkout the templates are synced at build time; before that, read the source file.
|
|
82
|
+
const src = join(templates, "..", "..", "provider", "Dockerfile");
|
|
83
|
+
const [file, label] = existsSync(copy)
|
|
84
|
+
? [copy, ".cf-orbs/provider/Dockerfile"]
|
|
85
|
+
: [existsSync(tpl) || !existsSync(src) ? tpl : src, "provider/Dockerfile (CLI template)"];
|
|
86
|
+
return pinCheck("provider image pin", file, label, "reinstall cf-orbs (the provider image is a CLI template), then cf-orbs deploy");
|
|
87
|
+
}
|
|
88
|
+
/** debugExec lets the control token run commands in orbs. WARN, never FAIL: the substrate bench needs it. */
|
|
89
|
+
export function debugExecCheck(c, git) {
|
|
90
|
+
if (!c)
|
|
91
|
+
return skip("debugExec", "needs a valid config");
|
|
92
|
+
if (!c.debugExec)
|
|
93
|
+
return pass("debugExec", "false (the control token cannot run commands in orbs)");
|
|
94
|
+
const then = git ? "commit and push" : "run cf-orbs deploy";
|
|
95
|
+
return {
|
|
96
|
+
name: "debugExec",
|
|
97
|
+
status: "WARN",
|
|
98
|
+
detail: toFailedLine(new CliError("debugExec is true: whoever has the control token can run any command in any orb", `set "debugExec": false in ${CONFIG_FILE}, then ${then} (keep it on only for the substrate bench)`)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const AOP_TOKEN = /aop_live_[A-Za-z0-9_-]{8,}/;
|
|
102
|
+
const MAX_SCAN_BYTES = 10_000_000;
|
|
103
|
+
/**
|
|
104
|
+
* Files (relative to dir) that hold an Amp provider token or one of the given secret values.
|
|
105
|
+
* Never returns the values. Skips files over 10 MB.
|
|
106
|
+
*/
|
|
107
|
+
export async function filesWithSecrets(dir, files, values) {
|
|
108
|
+
const hits = [];
|
|
109
|
+
const vals = values.filter((v) => v.length >= 8);
|
|
110
|
+
for (const f of files) {
|
|
111
|
+
const path = join(dir, f);
|
|
112
|
+
try {
|
|
113
|
+
if ((await stat(path)).size > MAX_SCAN_BYTES)
|
|
114
|
+
continue;
|
|
115
|
+
const text = await readFile(path, "latin1");
|
|
116
|
+
if (AOP_TOKEN.test(text) || vals.some((v) => text.includes(v)))
|
|
117
|
+
hits.push(f);
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
}
|
|
121
|
+
return hits;
|
|
122
|
+
}
|
|
123
|
+
/** `git ls-files` of dir, or undefined when dir is not in a git work tree (or git is missing). */
|
|
124
|
+
export async function gitTrackedFiles(run, dir) {
|
|
125
|
+
const r = await run("git", ["-C", dir, "ls-files", "-z"], { timeoutMs: 30_000 });
|
|
126
|
+
if (r.code !== 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
return r.stdout.split("\0").filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
/** No aop_live_ token and no control token value in a file git tracks. SKIP outside a git work tree. */
|
|
131
|
+
export async function trackedSecretsCheck(run, p, extra = []) {
|
|
132
|
+
const name = "secrets in git";
|
|
133
|
+
const files = await gitTrackedFiles(run, p.dir);
|
|
134
|
+
if (!files)
|
|
135
|
+
return skip(name, "not a git repository");
|
|
136
|
+
const ct = await readControlToken(p);
|
|
137
|
+
const hits = await filesWithSecrets(p.dir, files, [...(ct ? [ct] : []), ...extra]);
|
|
138
|
+
if (hits.length === 0)
|
|
139
|
+
return pass(name, `${files.length} tracked file(s), no aop_live_ or control token`);
|
|
140
|
+
return failed(name, new CliError(`a token is in tracked file(s): ${hits.slice(0, 5).join(", ")}${hits.length > 5 ? ", …" : ""}`, "git rm --cached those files (or remove the token), make a new token, and purge it from history if you pushed"));
|
|
141
|
+
}
|
|
142
|
+
/** Git mode: .cf-orbs/control-token and .cf-orbs/state.json are ignored and not tracked. */
|
|
143
|
+
export async function gitignoreCheck(run, p) {
|
|
144
|
+
const name = "gitignore";
|
|
145
|
+
const paths = [".cf-orbs/control-token", ".cf-orbs/state.json"];
|
|
146
|
+
const notIgnored = [];
|
|
147
|
+
for (const f of paths) {
|
|
148
|
+
// exit 0: ignored; 1: not ignored or tracked; 128: no repo. Fall back to the .gitignore lines then.
|
|
149
|
+
const r = await run("git", ["-C", p.dir, "check-ignore", "-q", f], { timeoutMs: 30_000 });
|
|
150
|
+
if (r.code === 0)
|
|
151
|
+
continue;
|
|
152
|
+
if (r.code === 1) {
|
|
153
|
+
notIgnored.push(f);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let lines = [];
|
|
157
|
+
try {
|
|
158
|
+
lines = (await readFile(p.gitignore, "utf8")).split(/\r?\n/).map((l) => l.trim());
|
|
159
|
+
}
|
|
160
|
+
catch { }
|
|
161
|
+
if (!lines.includes(f))
|
|
162
|
+
notIgnored.push(f);
|
|
163
|
+
}
|
|
164
|
+
if (notIgnored.length === 0)
|
|
165
|
+
return pass(name, `${paths.join(" and ")} are ignored`);
|
|
166
|
+
return failed(name, new CliError(`${notIgnored.join(" and ")} not ignored by git (or tracked)`, `add ${notIgnored.join(" and ")} to .gitignore and run git rm --cached on any that git tracks`));
|
|
167
|
+
}
|
|
168
|
+
/** Git mode: package.json pins the wrangler version the CLI uses (Workers Builds installs that one). */
|
|
169
|
+
export async function wranglerPinCheck(p) {
|
|
170
|
+
const name = "wrangler pin";
|
|
171
|
+
let pkg;
|
|
172
|
+
try {
|
|
173
|
+
pkg = JSON.parse(await readFile(p.packageJson, "utf8"));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return failed(name, new CliError("package.json is missing or not valid JSON", "rerun cf-orbs init --no-deploy (it writes one)"));
|
|
177
|
+
}
|
|
178
|
+
const v = pkg.devDependencies?.wrangler ?? pkg.dependencies?.wrangler;
|
|
179
|
+
if (v === WRANGLER_PIN)
|
|
180
|
+
return pass(name, `package.json pins wrangler ${v}, same as cf-orbs`);
|
|
181
|
+
return failed(name, new CliError(`package.json ${v ? `pins wrangler ${v}` : "has no wrangler dependency"}; cf-orbs uses ${WRANGLER_PIN}`, `set devDependencies.wrangler to "${WRANGLER_PIN}", run npm install, commit and push`));
|
|
182
|
+
}
|
|
183
|
+
/** All file checks for doctor, in output order. git: state.mode is "git". */
|
|
184
|
+
export async function fileChecks(run, p, c, o) {
|
|
185
|
+
const out = [
|
|
186
|
+
await orbImagePinCheck(p, c),
|
|
187
|
+
await providerImagePinCheck(p, o.templates),
|
|
188
|
+
debugExecCheck(c, o.git),
|
|
189
|
+
await trackedSecretsCheck(run, p),
|
|
190
|
+
];
|
|
191
|
+
if (o.git)
|
|
192
|
+
out.push(await gitignoreCheck(run, p), await wranglerPinCheck(p));
|
|
193
|
+
return out;
|
|
194
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,9 @@ export const COMMANDS = {
|
|
|
33
33
|
"--provider-name <name> Amp provider name, as in Amp Settings → Orbs (default cf-orbs)",
|
|
34
34
|
"--subdomain <name> workers.dev subdomain to register if the account has none",
|
|
35
35
|
"--vcpu <1|2|3|4> orb size: N vCPU, N x 3 GiB, 20 GB disk (default 4; 2 is cheaper)",
|
|
36
|
+
"--name <name> Worker name (default cf-orbs; a second install needs another)",
|
|
37
|
+
"--no-deploy preview: no Docker, no deploy. Make a git repo for Workers Builds,",
|
|
38
|
+
" create the Worker and its secrets, print the dashboard steps",
|
|
36
39
|
],
|
|
37
40
|
flags: {
|
|
38
41
|
provider: { type: "string" },
|
|
@@ -41,6 +44,8 @@ export const COMMANDS = {
|
|
|
41
44
|
"provider-name": { type: "string" },
|
|
42
45
|
subdomain: { type: "string" },
|
|
43
46
|
vcpu: { type: "string" },
|
|
47
|
+
name: { type: "string" },
|
|
48
|
+
"no-deploy": { type: "boolean" },
|
|
44
49
|
},
|
|
45
50
|
dirPositional: true,
|
|
46
51
|
run: initCommand,
|
package/dist/project.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// cf-orbs.config.jsonc user config (source of truth)
|
|
7
7
|
// image/Dockerfile orb image, user-editable; upgrade bumps its digest; image/<key>.Dockerfile for extra keys
|
|
8
8
|
// .gitignore generated; keeps .cf-orbs/ (and its control-token) out of git
|
|
9
|
+
// (git mode: only control-token and state.json; Workers Builds needs the rest)
|
|
10
|
+
// package.json git mode only: pins wrangler for Workers Builds
|
|
9
11
|
// .cf-orbs/
|
|
10
12
|
// wrangler.jsonc generated
|
|
11
13
|
// worker/src/** copied from templates on every deploy
|
|
@@ -35,6 +37,7 @@ export function projectPaths(dir) {
|
|
|
35
37
|
state: join(s, "state.json"),
|
|
36
38
|
controlToken: join(s, "control-token"),
|
|
37
39
|
gitignore: join(d, ".gitignore"),
|
|
40
|
+
packageJson: join(d, "package.json"),
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
43
|
/** cli/templates, resolved from this file (works from src/ under vitest and from dist/ when installed). */
|
|
@@ -105,15 +108,31 @@ export const GITIGNORE_LINES = [
|
|
|
105
108
|
".wrangler/",
|
|
106
109
|
"node_modules/",
|
|
107
110
|
];
|
|
111
|
+
/** Git mode (init --no-deploy): the generated files are committed, so only these stay out of git. */
|
|
112
|
+
export const GIT_MODE_GITIGNORE_LINES = [
|
|
113
|
+
".cf-orbs/control-token",
|
|
114
|
+
".cf-orbs/state.json",
|
|
115
|
+
".wrangler/",
|
|
116
|
+
"node_modules/",
|
|
117
|
+
];
|
|
118
|
+
/** Lines of the local layout that hide the generated files. Git mode removes them. */
|
|
119
|
+
const LOCAL_ONLY_LINES = new Set([".cf-orbs/*", "!.cf-orbs/state.json"]);
|
|
108
120
|
/** Add the cf-orbs lines to <dir>/.gitignore, keeping existing lines. */
|
|
109
|
-
export async function ensureGitignore(p) {
|
|
121
|
+
export async function ensureGitignore(p, git = false) {
|
|
110
122
|
let cur = "";
|
|
111
123
|
try {
|
|
112
124
|
cur = await readFile(p.gitignore, "utf8");
|
|
113
125
|
}
|
|
114
126
|
catch { }
|
|
127
|
+
if (git && cur.split(/\r?\n/).some((l) => LOCAL_ONLY_LINES.has(l.trim()))) {
|
|
128
|
+
cur = cur
|
|
129
|
+
.split(/(?<=\n)/)
|
|
130
|
+
.filter((l) => !LOCAL_ONLY_LINES.has(l.trim()))
|
|
131
|
+
.join("");
|
|
132
|
+
await writeFile(p.gitignore, cur);
|
|
133
|
+
}
|
|
115
134
|
const have = new Set(cur.split(/\r?\n/).map((l) => l.trim()));
|
|
116
|
-
const add = GITIGNORE_LINES.filter((l) => !have.has(l));
|
|
135
|
+
const add = (git ? GIT_MODE_GITIGNORE_LINES : GITIGNORE_LINES).filter((l) => !have.has(l));
|
|
117
136
|
if (add.length === 0)
|
|
118
137
|
return;
|
|
119
138
|
const sep = cur === "" || cur.endsWith("\n") ? "" : "\n";
|
|
@@ -155,7 +174,7 @@ export async function materialize(p, opts = {}) {
|
|
|
155
174
|
await writeFile(p.config, text);
|
|
156
175
|
wroteConfig = true;
|
|
157
176
|
}
|
|
158
|
-
await ensureGitignore(p);
|
|
177
|
+
await ensureGitignore(p, opts.git);
|
|
159
178
|
return { wroteConfig, wroteDockerfile };
|
|
160
179
|
}
|
|
161
180
|
/** The files the provider image is built from, relative to .cf-orbs/ (its build context). */
|
|
@@ -213,3 +232,15 @@ export async function cliVersion() {
|
|
|
213
232
|
return "0.0.0";
|
|
214
233
|
}
|
|
215
234
|
}
|
|
235
|
+
/** Git mode deploy command: Workers Builds runs it in the repo root. */
|
|
236
|
+
export const GIT_DEPLOY_COMMAND = "npx wrangler deploy --config .cf-orbs/wrangler.jsonc";
|
|
237
|
+
/** Root package.json for git mode: Workers Builds installs the wrangler version it names. */
|
|
238
|
+
export function gitPackageJson(name, wrangler) {
|
|
239
|
+
const pkg = {
|
|
240
|
+
name,
|
|
241
|
+
private: true,
|
|
242
|
+
scripts: { deploy: "wrangler deploy --config .cf-orbs/wrangler.jsonc" },
|
|
243
|
+
devDependencies: { wrangler },
|
|
244
|
+
};
|
|
245
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
246
|
+
}
|
|
@@ -40,6 +40,9 @@ export function buildWranglerConfig(c, o = {}) {
|
|
|
40
40
|
main: "worker/src/index.ts",
|
|
41
41
|
compatibility_date: COMPATIBILITY_DATE,
|
|
42
42
|
compatibility_flags: ["nodejs_compat"],
|
|
43
|
+
// explicit, so wrangler does not warn: the control plane is on workers.dev; preview URLs are not used
|
|
44
|
+
workers_dev: true,
|
|
45
|
+
preview_urls: false,
|
|
43
46
|
observability: { enabled: true },
|
|
44
47
|
containers: [
|
|
45
48
|
orbContainer,
|
package/dist/wrangler.js
CHANGED
|
@@ -37,6 +37,8 @@ export const runProcess = (cmd, args, o = {}) => new Promise((resolve) => {
|
|
|
37
37
|
resolve({ code: code ?? 1, stdout, stderr });
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
|
+
/** The wrangler version the CLI depends on (cli/package.json). Git mode pins the same version in package.json. */
|
|
41
|
+
export const WRANGLER_PIN = "4.156.0";
|
|
40
42
|
/** [command, ...prefix args] that runs the pinned wrangler. Prefers the CLI's own dependency over npx. */
|
|
41
43
|
export function wranglerCommand() {
|
|
42
44
|
try {
|
|
@@ -46,7 +48,7 @@ export function wranglerCommand() {
|
|
|
46
48
|
return [process.execPath, [bin]];
|
|
47
49
|
}
|
|
48
50
|
catch { }
|
|
49
|
-
return [process.platform === "win32" ? "npx.cmd" : "npx", ["--yes",
|
|
51
|
+
return [process.platform === "win32" ? "npx.cmd" : "npx", ["--yes", `wrangler@${WRANGLER_PIN}`]];
|
|
50
52
|
}
|
|
51
53
|
export function makeWrangler(run = runProcess, base = {}) {
|
|
52
54
|
const [cmd, prefix] = wranglerCommand();
|