cf-orbs 0.2.2 → 0.2.4

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 CHANGED
@@ -70,7 +70,7 @@ What `init` does:
70
70
  amp -ox "hello from cf-orbs" --orb-provider <name>
71
71
  ```
72
72
 
73
- The name after `--orb-provider` must match the provider name in Amp Settings. In the Amp app, pick the provider under **Self-Hosted** when you start a thread.
73
+ The name after `--orb-provider` must match the provider name in Amp Settings. The default name is `cloudflare`. A saved name from an earlier `init` wins. In the Amp app, pick the provider under **Self-Hosted** when you start a thread.
74
74
 
75
75
  `init` is safe to run again. It keeps the control token, and it keeps the Amp token secret unless you give a new one.
76
76
 
@@ -120,6 +120,7 @@ Before the deploy, `init` stops with one `Failed: … — fix: …` line when:
120
120
  - An extra image key has no `image/<key>.Dockerfile`.
121
121
  - The account has no workers.dev subdomain and you gave no `--subdomain` (in a terminal, `init` asks for one).
122
122
  - `wrangler containers list` fails with a clear plan or login error (Workers Paid is needed). Any other error is a warning.
123
+ - The folder has no saved deployment and a Worker of the same name already exists in the account. `init` stops before it makes a control token, sets a secret or deploys, so it cannot overwrite a Worker that another folder made. Pass `--adopt` to take it over: a new control token replaces the old one, and this folder's config and `image/` are deployed. A rerun in the folder that made the Worker is not checked. If the check cannot run, it is a warning.
123
124
 
124
125
  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.
125
126
 
@@ -184,7 +185,7 @@ Example: 120 awake hours a month at 20% CPU costs about $27.50 a month in total
184
185
 
185
186
  | Command | Does |
186
187
  | --- | --- |
187
- | `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) |
188
+ | `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), `--adopt` (take over an existing Worker of the same name; see Back up) |
188
189
  | `cf-orbs status [orb]` | Install status (provider, awake orbs, snapshots), or one orb |
189
190
  | `cf-orbs logs <orb>` | Redacted bootstrap log of one orb. `--lines N` (default 200) |
190
191
  | `cf-orbs logs --provider` | Redacted provider log (last 20 lines) |
@@ -225,6 +226,10 @@ After `cf-orbs deploy`, an orb that is awake keeps the old Worker code until it
225
226
 
226
227
  `cf-orbs deploy` then starts `prewarm` throwaway instances in the background (`prewarm: started 2 instances in the background`); `cf-orbs status` shows how fast they were ready.
227
228
 
229
+ ### Back up
230
+
231
+ Running orbs need nothing on your machine: the Worker, the orbs and the snapshots are in Cloudflare. Back up `cf-orbs.config.jsonc` and `image/` (a private git repo is enough; the `.gitignore` that `init` writes keeps the control token out). Keep the Amp provider token in a password manager if you want to restore without making a new one. The control token can be made again. To restore in a new folder, run `init --adopt`: without it, `init` stops because the Worker already exists. See [Back up and restore](https://cf-orbs-site.mdick85.workers.dev/docs/guides/back-up-and-restore/).
232
+
228
233
  ## Troubleshooting
229
234
 
230
235
  Run `cf-orbs doctor` first.
@@ -275,7 +280,7 @@ To report a vulnerability, see [SECURITY.md](https://github.com/meirdick/cf-orbs
275
280
 
276
281
  ## Status and known gaps
277
282
 
278
- v0.2.2. 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.
283
+ v0.2.4. 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.
279
284
 
280
285
  Known gaps:
281
286
 
@@ -14,7 +14,7 @@ import { dockerOk, nodeMajor, whoami } from "../probes.js";
14
14
  import { cliVersion, GIT_DEPLOY_COMMAND, generateToken, gitPackageJson, materialize, missingDockerfiles, readControlToken, readState, withSecretsFile, writeControlToken, writeState, writeWranglerConfig, } from "../project.js";
15
15
  import { checkSubdomain, registerSubdomain, suggestSubdomain, wranglerBearer } from "../subdomain.js";
16
16
  import { ask, askHidden } from "../ui.js";
17
- import { classifyDeployFailure, deployFailureError, lastErrorLine, makeWrangler, WRANGLER_PIN, } from "../wrangler.js";
17
+ import { classifyDeployFailure, deployFailureError, lastErrorLine, lookupWorker, makeWrangler, WRANGLER_PIN, } from "../wrangler.js";
18
18
  import { deployProject, startPrewarm } from "./deploy.js";
19
19
  export const PROVIDER_WAIT_MS = 90_000;
20
20
  /** How long init waits for the prewarm instances (a first start on a cold host pulls the image). */
@@ -22,6 +22,8 @@ export const SELFTEST_WAIT_MS = 180_000;
22
22
  /** Provider must stay up this long with no reject, no second-provider line and no restart. */
23
23
  export const PROVIDER_STABLE_MS = 15_000;
24
24
  const CRON_PERIOD_MS = 5 * 60_000;
25
+ /** One line at the end of a successful init, before the amp command. */
26
+ export const BACKUP_LINE = "back up: cf-orbs.config.jsonc and image/. Guide: https://cf-orbs-site.mdick85.workers.dev/docs/guides/back-up-and-restore/";
25
27
  export function helloCommand(providerName) {
26
28
  return `amp -ox "hello from cf-orbs" --orb-provider ${providerName}`;
27
29
  }
@@ -132,6 +134,12 @@ export function plan(o, p) {
132
134
  { kind: "plan-check" },
133
135
  { kind: "scaffold", mode: o.mode, git: Boolean(o.noDeploy) },
134
136
  ];
137
+ // A folder with no saved deployment and no control token must not overwrite a Worker it did not create.
138
+ // A folder with a control token made the Worker before (a run that failed before it saved the URL) or holds a
139
+ // restored backup: it keeps the same token, so nothing is lost. Runs after scaffold (the Worker name is in the
140
+ // config then) and before the control token, the secrets and the deploy.
141
+ if (!p.deployed && !p.hasControlToken)
142
+ steps.push({ kind: "adopt-check", adopt: Boolean(o.adopt) });
135
143
  steps.push({ kind: "control-token", action: p.hasControlToken ? "reuse" : "generate" });
136
144
  if (ampSource)
137
145
  steps.push({ kind: "amp-token", source: ampSource });
@@ -328,6 +336,19 @@ export async function runInit(ctx, o, steps) {
328
336
  await writeState(paths, state);
329
337
  break;
330
338
  }
339
+ case "adopt-check": {
340
+ if (!config)
341
+ throw new Error("init plan bug: adopt-check before scaffold");
342
+ const found = await lookupWorker(makeWrangler(ctx.run, { accountId }), config.name);
343
+ if (found.kind === "found") {
344
+ if (!s.adopt)
345
+ throw new CliError(`Worker ${config.name} already exists in this account and this folder did not create it`, "run init in the folder that made it, restore that folder (see Back up and restore), pass --name <new> for a second install, or pass --adopt to take it over");
346
+ ui.warn(`adopt: taking over Worker ${config.name}; a new control token replaces the old one (the folder that made it loses CLI access); this folder's config and image/ are deployed`);
347
+ }
348
+ else if (found.kind === "unknown")
349
+ ui.warn(`existing-Worker check could not run (${found.reason}); init goes on`);
350
+ break;
351
+ }
331
352
  case "control-token":
332
353
  controlToken = s.action === "reuse" ? await readControlToken(paths) : undefined;
333
354
  if (!controlToken) {
@@ -340,8 +361,7 @@ export async function runInit(ctx, o, steps) {
340
361
  break;
341
362
  case "amp-token":
342
363
  // No interactive name question here: say so if the name is a fallback, not given
343
- if (s.source !== "prompt" &&
344
- (o.providerNameSource === "config" || o.providerNameSource === "default"))
364
+ if (s.source !== "prompt" && o.providerNameSource === "default")
345
365
  ui.warn(`provider name ${providerName} was not given explicitly; it must match the name in Amp Settings → Orbs (pass --provider-name <name> if it differs)`);
346
366
  if (s.source === "env")
347
367
  ampToken = checkAmpToken(ctx, (ctx.env.AMP_ORB_PROVIDER_TOKEN ?? "").trim());
@@ -580,6 +600,7 @@ export async function runInit(ctx, o, steps) {
580
600
  case "done":
581
601
  command = s.command === helloCommand(o.providerName) ? helloCommand(providerName) : s.command;
582
602
  ui.step(`provider name ${providerName} must match the name in Amp Settings → Orbs`);
603
+ ui.out(BACKUP_LINE);
583
604
  break;
584
605
  }
585
606
  }
@@ -745,6 +766,8 @@ export function parseVcpuFlag(v) {
745
766
  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)");
746
767
  return Number(v);
747
768
  }
769
+ /** Default Amp provider name when none is given. The Worker name default is separate: cf-orbs. */
770
+ export const DEFAULT_PROVIDER_NAME = "cloudflare";
748
771
  const WORKER_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
749
772
  export function initOptions(ctx, existing) {
750
773
  const modeFlag = flagString(ctx.args, "provider");
@@ -761,6 +784,7 @@ export function initOptions(ctx, existing) {
761
784
  vcpu: parseVcpuFlag(flagString(ctx.args, "vcpu")),
762
785
  name,
763
786
  noDeploy: flagBool(ctx.args, "no-deploy"),
787
+ adopt: flagBool(ctx.args, "adopt"),
764
788
  mode: modeFlag ?? existing?.providerHost ?? "cloudflare",
765
789
  nonInteractive: flagBool(ctx.args, "non-interactive") || Boolean(ctx.env.CI),
766
790
  tokenFile: flagString(ctx.args, "token-file"),
@@ -768,17 +792,14 @@ export function initOptions(ctx, existing) {
768
792
  providerName: flagString(ctx.args, "provider-name") ??
769
793
  ctx.env.AMP_ORB_PROVIDER_NAME ??
770
794
  existing?.providerName ??
771
- existing?.name ??
772
- "cf-orbs",
795
+ DEFAULT_PROVIDER_NAME,
773
796
  providerNameSource: flagString(ctx.args, "provider-name") !== undefined
774
797
  ? "flag"
775
798
  : ctx.env.AMP_ORB_PROVIDER_NAME !== undefined
776
799
  ? "env"
777
800
  : existing?.providerName !== undefined
778
801
  ? "saved"
779
- : existing?.name !== undefined
780
- ? "config"
781
- : "default",
802
+ : "default",
782
803
  };
783
804
  }
784
805
  export async function initCommand(ctx) {
package/dist/index.js CHANGED
@@ -30,12 +30,14 @@ export const COMMANDS = {
30
30
  "--provider cloudflare|external where amp orb provider runs (default cloudflare)",
31
31
  "--token-file <file> Amp provider token file (else AMP_ORB_PROVIDER_TOKEN, else prompt)",
32
32
  "--account-id <id> Cloudflare account (else CLOUDFLARE_ACCOUNT_ID, else ask)",
33
- "--provider-name <name> Amp provider name, as in Amp Settings → Orbs (else AMP_ORB_PROVIDER_NAME, saved name, config name, then cf-orbs)",
33
+ "--provider-name <name> Amp provider name, as in Amp Settings → Orbs (default cloudflare; AMP_ORB_PROVIDER_NAME or a saved name wins)",
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
36
  "--name <name> Worker name (default cf-orbs; a second install needs another)",
37
37
  "--no-deploy preview: no Docker, no deploy. Make a git repo for Workers Builds,",
38
38
  " create the Worker and its secrets, print the dashboard steps",
39
+ "--adopt take over a Worker of the same name that this folder did not create;",
40
+ " init stops without it. A new control token replaces the old one",
39
41
  ],
40
42
  flags: {
41
43
  provider: { type: "string" },
@@ -46,6 +48,7 @@ export const COMMANDS = {
46
48
  vcpu: { type: "string" },
47
49
  name: { type: "string" },
48
50
  "no-deploy": { type: "boolean" },
51
+ adopt: { type: "boolean" },
49
52
  },
50
53
  dirPositional: true,
51
54
  run: initCommand,
package/dist/wrangler.js CHANGED
@@ -135,6 +135,27 @@ export function deployFailureError(kind, output) {
135
135
  return new CliError(`wrangler deploy failed: ${lastErrorLine(output)}`, "fix the error above, then rerun cf-orbs deploy");
136
136
  }
137
137
  }
138
+ /**
139
+ * Does a Worker with this name exist in the account? Runs `wrangler deployments list --json --name <name>`.
140
+ * Verified with wrangler 4.156.0: an existing Worker exits 0 and prints a JSON array. A missing one exits 1,
141
+ * prints nothing on stdout and prints `This Worker does not exist on your account. [code: 10007]` on stderr.
142
+ * Any other result (auth, network, bad account id, odd output) is "unknown".
143
+ */
144
+ export async function lookupWorker(w, name) {
145
+ const r = await w.run(["deployments", "list", "--json", "--name", name], { timeoutMs: 60_000 });
146
+ const out = `${r.stdout}\n${r.stderr}`;
147
+ if (r.code === 0) {
148
+ try {
149
+ if (Array.isArray(JSON.parse(r.stdout.slice(Math.max(0, r.stdout.indexOf("["))))))
150
+ return { kind: "found" };
151
+ }
152
+ catch { }
153
+ return { kind: "unknown", reason: "unreadable wrangler output" };
154
+ }
155
+ if (/\[code: 10007\]|This Worker does not exist/.test(out))
156
+ return { kind: "missing" };
157
+ return { kind: "unknown", reason: lastErrorLine(out) };
158
+ }
138
159
  /** Last meaningful line of wrangler output, preferring the "✘ [ERROR]" line. */
139
160
  export function lastErrorLine(output) {
140
161
  const clean = output.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-orbs",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Run Amp orbs on Cloudflare Containers: self-hosted orb provider, one command to install.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -18,7 +18,7 @@ import { type Env, orbDoName, PROVIDER_DO_NAME, REGISTRY_DO_NAME } from "./env";
18
18
  import { BENCH_ORB_RE, type BenchStartRequest, parseBenchStart } from "./orb/bench";
19
19
  import { restartProvider } from "./provider-restart";
20
20
 
21
- export const API_VERSION = "0.2.2";
21
+ export const API_VERSION = "0.2.4";
22
22
 
23
23
  /** D2. Amp sends `amp-<thread id>`. Never starts with `_` or `.`, so it cannot look like an internal name. */
24
24
  export const ORB_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;