@bigrandall/rrangler 0.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/rrangler.mjs +222 -0
- package/commands/index.mjs +631 -0
- package/lib/api.mjs +88 -0
- package/lib/config.mjs +55 -0
- package/lib/project-config.mjs +53 -0
- package/lib/ui.mjs +62 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Randall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# rrangler
|
|
2
|
+
|
|
3
|
+
A **wrangler-compatible CLI for RandallFlare** — the edge platform inside
|
|
4
|
+
bigrandall.io. Where `wrangler` talks to Cloudflare, `rrangler` talks to your
|
|
5
|
+
RandallFlare account's edge API (`/api/edge/v1/*`). Same command shape, so
|
|
6
|
+
muscle memory (and `wrangler.toml`) carry over.
|
|
7
|
+
|
|
8
|
+
Zero dependencies — plain Node ≥ 20 ESM.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm i -g @bigrandall/rrangler # published package (the command is `rrangler`)
|
|
14
|
+
rrangler --version
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or run without installing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @bigrandall/rrangler whoami
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
From a checkout of this repo:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm i -g ./rrangler # or `npm link` inside rrangler/
|
|
27
|
+
# or directly: node rrangler/bin/rrangler.mjs <command>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Zero runtime dependencies — just Node ≥ 20.
|
|
31
|
+
|
|
32
|
+
## Auth
|
|
33
|
+
|
|
34
|
+
Create an **API token** in bigrandall.io → `/me/edge` → API tokens (pick the
|
|
35
|
+
scopes you need: `worker:*`, `d1:*`, `r2:*`, `kv:*`, or `*`). Then:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
rrangler login --token rft_xxx # saved to ~/.rrangler.json (chmod 600)
|
|
39
|
+
rrangler whoami
|
|
40
|
+
rrangler doctor # connectivity + which endpoints your scopes allow
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or per-invocation: `RRANGLER_TOKEN=… rrangler …` / `--token …`.
|
|
44
|
+
Point at another instance with `RRANGLER_API` / `--api https://…`.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
Mirrors wrangler — `publish` aliases to `deploy`, `kv:namespace` works too.
|
|
49
|
+
|
|
50
|
+
| rrangler | does |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `init [name]` | scaffold `rrangler.json` + `main.js` |
|
|
53
|
+
| `deploy [--name s]` | push local files to a worker (reads `rrangler.json`, `rrangler.toml`, or `wrangler.toml`) |
|
|
54
|
+
| `worker list` / `worker get <slug>` | list / inspect workers |
|
|
55
|
+
| `tail <slug>` | stream runtime logs |
|
|
56
|
+
| `worker env set <slug> K=V …` / `delete` | env vars (`--secret` to mask) |
|
|
57
|
+
| `worker builds <slug>` / `build <slug> [--branch]` / `build-log <slug> <id>` | git builds |
|
|
58
|
+
| `worker cron <slug> [--dlq]` / `cron replay <slug> <runId>` | cron runs + DLQ replay |
|
|
59
|
+
| `secret put <KEY> --worker <slug>` | set a secret (value from `--value` or stdin) |
|
|
60
|
+
| `secret list \| delete` | manage secrets |
|
|
61
|
+
| `d1 list \| create <name> \| info <db>` | Durable SQLite databases |
|
|
62
|
+
| `d1 execute <db> --command '<sql>'` | run SQL (`--file f.sql` too) |
|
|
63
|
+
| `d1 export <db> [--output f]` | dump schema + data as SQL |
|
|
64
|
+
| `d1 migrations create \| list \| apply` | file-based migrations (`--dir migrations`) |
|
|
65
|
+
| `kv namespace list \| create <title>` | KV namespaces |
|
|
66
|
+
| `kv key put <ns> <key> <value>` / `get` / `list` / `delete` | KV entries |
|
|
67
|
+
| `kv bulk put \| delete <ns> <file.json>` | bulk KV |
|
|
68
|
+
| `dev [--port]` | run the worker locally on **workerd** (prod runtime) |
|
|
69
|
+
| `types [--out f]` | generate a bindings `Env` d.ts |
|
|
70
|
+
| `secret bulk <file.json> --worker <slug>` | bulk secrets |
|
|
71
|
+
| `r2 bucket list \| create <name> [--public]` | R2 buckets |
|
|
72
|
+
| `r2 object put <bucket> <key> <file>` / `get` / `delete` | R2 objects |
|
|
73
|
+
| `pages list` / `get <slug>` | Pages projects |
|
|
74
|
+
| `pages env set\|delete` / `builds` / `build [--branch]` / `build-log` / `tail` | Pages ops |
|
|
75
|
+
| `api <METHOD> <path> [--data '<json>']` | raw API passthrough |
|
|
76
|
+
|
|
77
|
+
Global flags: `--json` (machine-readable stdout), `--api`, `--token`.
|
|
78
|
+
|
|
79
|
+
## Deploy config
|
|
80
|
+
|
|
81
|
+
`rrangler.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"name": "my-worker",
|
|
86
|
+
"main": "main.js",
|
|
87
|
+
"files": ["main.js", "lib/util.js"],
|
|
88
|
+
"compatibilityDate": "2026-07-05"
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`name` is the worker slug (create the worker once in `/me/edge`, then deploy
|
|
93
|
+
to it). `files` is optional — omit it to send just `main`. A `wrangler.toml`
|
|
94
|
+
with `name` / `main` is read as a fallback for drop-in compatibility.
|
|
95
|
+
|
|
96
|
+
## Beyond wrangler
|
|
97
|
+
|
|
98
|
+
- `doctor` — one-shot health + capability probe (what your token can actually do).
|
|
99
|
+
- `api` — raw authenticated call to any `/api/edge/v1/*` endpoint for scripting.
|
|
100
|
+
- `--json` on every read command for piping into `jq`.
|
|
101
|
+
- Reads `rrangler.json` **or** `wrangler.toml`, so an existing CF project deploys
|
|
102
|
+
to RandallFlare without a rewrite.
|
package/bin/rrangler.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rrangler — a wrangler-compatible CLI for RandallFlare (bigrandall.io).
|
|
3
|
+
//
|
|
4
|
+
// Usage: rrangler <command> [subcommand] [args] [--flags]
|
|
5
|
+
// See `rrangler help`.
|
|
6
|
+
|
|
7
|
+
import * as cmd from "../commands/index.mjs";
|
|
8
|
+
import { style, fail } from "../lib/ui.mjs";
|
|
9
|
+
import { ApiError } from "../lib/api.mjs";
|
|
10
|
+
|
|
11
|
+
const ROUTES = {
|
|
12
|
+
login: cmd.login,
|
|
13
|
+
logout: cmd.logout,
|
|
14
|
+
whoami: cmd.whoami,
|
|
15
|
+
doctor: cmd.doctor,
|
|
16
|
+
init: cmd.init,
|
|
17
|
+
deploy: cmd.deploy,
|
|
18
|
+
dev: cmd.dev,
|
|
19
|
+
tail: cmd.tail,
|
|
20
|
+
types: cmd.types,
|
|
21
|
+
api: cmd.apiPassthrough,
|
|
22
|
+
"deployments list": cmd.workerBuilds,
|
|
23
|
+
"versions list": cmd.workerBuilds,
|
|
24
|
+
|
|
25
|
+
"worker list": cmd.workerList,
|
|
26
|
+
"worker get": cmd.workerGet,
|
|
27
|
+
"worker builds": cmd.workerBuilds,
|
|
28
|
+
"worker build": cmd.workerBuild,
|
|
29
|
+
"worker build-log": cmd.workerBuildLog,
|
|
30
|
+
"worker env set": cmd.workerEnvSet,
|
|
31
|
+
"worker env delete": cmd.workerEnvDelete,
|
|
32
|
+
"worker cron": cmd.workerCron,
|
|
33
|
+
"worker cron replay": cmd.workerCronReplay,
|
|
34
|
+
|
|
35
|
+
"secret put": cmd.secretPut,
|
|
36
|
+
"secret list": cmd.secretList,
|
|
37
|
+
"secret delete": cmd.secretDelete,
|
|
38
|
+
"secret bulk": cmd.secretBulk,
|
|
39
|
+
|
|
40
|
+
"d1 list": cmd.d1List,
|
|
41
|
+
"d1 create": cmd.d1Create,
|
|
42
|
+
"d1 execute": cmd.d1Execute,
|
|
43
|
+
"d1 export": cmd.d1Export,
|
|
44
|
+
"d1 info": cmd.d1Info,
|
|
45
|
+
"d1 migrations create": cmd.d1MigrationsCreate,
|
|
46
|
+
"d1 migrations list": cmd.d1MigrationsList,
|
|
47
|
+
"d1 migrations apply": cmd.d1MigrationsApply,
|
|
48
|
+
|
|
49
|
+
"kv namespace list": cmd.kvNamespaceList,
|
|
50
|
+
"kv namespace create": cmd.kvNamespaceCreate,
|
|
51
|
+
"kv key get": cmd.kvKeyGet,
|
|
52
|
+
"kv key put": cmd.kvKeyPut,
|
|
53
|
+
"kv key list": cmd.kvKeyList,
|
|
54
|
+
"kv key delete": cmd.kvKeyDelete,
|
|
55
|
+
"kv bulk put": cmd.kvBulkPut,
|
|
56
|
+
"kv bulk delete": cmd.kvBulkDelete,
|
|
57
|
+
|
|
58
|
+
"r2 bucket list": cmd.r2BucketList,
|
|
59
|
+
"r2 bucket create": cmd.r2BucketCreate,
|
|
60
|
+
"r2 object get": cmd.r2ObjectGet,
|
|
61
|
+
"r2 object put": cmd.r2ObjectPut,
|
|
62
|
+
"r2 object delete": cmd.r2ObjectDelete,
|
|
63
|
+
|
|
64
|
+
"pages list": cmd.pagesList,
|
|
65
|
+
"pages project list": cmd.pagesList,
|
|
66
|
+
"pages deployment list": cmd.pagesBuilds,
|
|
67
|
+
"pages deploy": cmd.pagesBuild,
|
|
68
|
+
"pages get": cmd.pagesGet,
|
|
69
|
+
"pages env set": cmd.pagesEnvSet,
|
|
70
|
+
"pages env delete": cmd.pagesEnvDelete,
|
|
71
|
+
"pages builds": cmd.pagesBuilds,
|
|
72
|
+
"pages build": cmd.pagesBuild,
|
|
73
|
+
"pages build-log": cmd.pagesBuildLog,
|
|
74
|
+
"pages tail": cmd.pagesTail,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// wrangler-compat rewrites applied to the raw command tokens.
|
|
78
|
+
function normalize(tokens) {
|
|
79
|
+
return tokens
|
|
80
|
+
.flatMap((t) => t.split(":")) // kv:namespace → kv namespace
|
|
81
|
+
.map((t) => {
|
|
82
|
+
if (t === "publish") return "deploy"; // wrangler alias
|
|
83
|
+
if (t === "ls") return "list";
|
|
84
|
+
if (t === "rm" || t === "del") return "delete";
|
|
85
|
+
if (t === "buckets") return "bucket";
|
|
86
|
+
if (t === "namespaces") return "namespace";
|
|
87
|
+
return t;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseArgs(argv) {
|
|
92
|
+
const pos = [];
|
|
93
|
+
const flags = {};
|
|
94
|
+
for (let i = 0; i < argv.length; i++) {
|
|
95
|
+
const a = argv[i];
|
|
96
|
+
if (a.startsWith("--")) {
|
|
97
|
+
const eq = a.indexOf("=");
|
|
98
|
+
if (eq >= 0) {
|
|
99
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
100
|
+
} else {
|
|
101
|
+
const key = a.slice(2);
|
|
102
|
+
const next = argv[i + 1];
|
|
103
|
+
if (next === undefined || next.startsWith("--")) flags[key] = true;
|
|
104
|
+
else {
|
|
105
|
+
flags[key] = next;
|
|
106
|
+
i++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
pos.push(a);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { pos, flags };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const HELP = `${style.bold("rrangler")} — wrangler-compatible CLI for RandallFlare (bigrandall.io)
|
|
117
|
+
|
|
118
|
+
${style.bold("auth")}
|
|
119
|
+
rrangler login --token <t> save an API token (from /me/edge → API tokens)
|
|
120
|
+
rrangler logout
|
|
121
|
+
rrangler whoami identity + scopes
|
|
122
|
+
rrangler doctor check token, connectivity, allowed endpoints
|
|
123
|
+
|
|
124
|
+
${style.bold("workers")}
|
|
125
|
+
rrangler init [name] scaffold rrangler.json + main.js
|
|
126
|
+
rrangler deploy [--name s] push local files to a worker (reads wrangler.toml too)
|
|
127
|
+
rrangler worker list
|
|
128
|
+
rrangler worker get <slug>
|
|
129
|
+
rrangler tail <slug> stream runtime logs
|
|
130
|
+
rrangler worker env set <slug> K=V ... [--secret]
|
|
131
|
+
rrangler worker env delete <slug> K ...
|
|
132
|
+
rrangler worker builds <slug>
|
|
133
|
+
rrangler worker build <slug> [--branch b] trigger a git build
|
|
134
|
+
rrangler worker build-log <slug> <buildId> [--tail N]
|
|
135
|
+
rrangler worker cron <slug> [--dlq] [--limit N]
|
|
136
|
+
rrangler worker cron replay <slug> <runId>
|
|
137
|
+
rrangler secret put <KEY> --worker <slug> [--value v]
|
|
138
|
+
rrangler secret list --worker <slug>
|
|
139
|
+
rrangler secret delete <KEY> --worker <slug>
|
|
140
|
+
rrangler secret bulk <file.json> --worker <slug>
|
|
141
|
+
|
|
142
|
+
${style.bold("dev")}
|
|
143
|
+
rrangler dev [--port 8787] run the worker locally on workerd (prod runtime)
|
|
144
|
+
rrangler types [--out f] generate a bindings Env d.ts
|
|
145
|
+
|
|
146
|
+
${style.bold("d1")}
|
|
147
|
+
rrangler d1 list | create <name> | info <db>
|
|
148
|
+
rrangler d1 execute <db> (--command '<sql>' | --file <f.sql>)
|
|
149
|
+
rrangler d1 export <db> [--output f] [--no-data]
|
|
150
|
+
rrangler d1 migrations create <name> | list <db> | apply <db> [--dir migrations]
|
|
151
|
+
|
|
152
|
+
${style.bold("kv")}
|
|
153
|
+
rrangler kv namespace list|create <title>
|
|
154
|
+
rrangler kv key put <ns> <key> <value> [--ttl s]
|
|
155
|
+
rrangler kv key get|delete <ns> <key>
|
|
156
|
+
rrangler kv key list <ns> [--prefix p]
|
|
157
|
+
rrangler kv bulk put|delete <ns> <file.json>
|
|
158
|
+
|
|
159
|
+
${style.bold("r2")}
|
|
160
|
+
rrangler r2 bucket list|create <name> [--public]
|
|
161
|
+
rrangler r2 object put <bucket> <key> <file>
|
|
162
|
+
rrangler r2 object get <bucket> <key> [--output f]
|
|
163
|
+
rrangler r2 object delete <bucket> <key>
|
|
164
|
+
|
|
165
|
+
${style.bold("pages")}
|
|
166
|
+
rrangler pages list
|
|
167
|
+
rrangler pages get <slug>
|
|
168
|
+
rrangler pages env set <slug> K=V ... [--secret]
|
|
169
|
+
rrangler pages env delete <slug> K ...
|
|
170
|
+
rrangler pages builds <slug>
|
|
171
|
+
rrangler pages build <slug> [--branch b]
|
|
172
|
+
rrangler pages build-log <slug> <buildId>
|
|
173
|
+
rrangler pages tail <slug>
|
|
174
|
+
|
|
175
|
+
${style.bold("power")}
|
|
176
|
+
rrangler api <METHOD> <path> [--data '<json>'] raw API call
|
|
177
|
+
|
|
178
|
+
${style.bold("global flags")}: --json (machine output) --api <url> --token <t>
|
|
179
|
+
env: RRANGLER_TOKEN, RRANGLER_API`;
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const argv = process.argv.slice(2);
|
|
183
|
+
if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
|
|
184
|
+
process.stdout.write(HELP + "\n");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
|
|
188
|
+
process.stdout.write("rrangler 0.1.0\n");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { pos: rawPos, flags } = parseArgs(argv);
|
|
193
|
+
const tokens = normalize(rawPos);
|
|
194
|
+
|
|
195
|
+
for (let len = 3; len >= 1; len--) {
|
|
196
|
+
const key = tokens.slice(0, len).join(" ");
|
|
197
|
+
if (ROUTES[key]) {
|
|
198
|
+
const handler = ROUTES[key];
|
|
199
|
+
const pos = tokens.slice(len);
|
|
200
|
+
// let --token / --api flags override the stored config for this call
|
|
201
|
+
if (flags.token) process.env.RRANGLER_TOKEN = String(flags.token);
|
|
202
|
+
if (flags.api) process.env.RRANGLER_API = String(flags.api);
|
|
203
|
+
await handler({ pos, flags });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
fail(`unknown command: ${style.bold(tokens.join(" "))}`);
|
|
208
|
+
process.stderr.write(`run ${style.cyan("rrangler help")} for usage\n`);
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main().catch((e) => {
|
|
213
|
+
if (e instanceof ApiError) {
|
|
214
|
+
fail(e.message);
|
|
215
|
+
if (e.status === 401 || e.status === 403) {
|
|
216
|
+
process.stderr.write(style.dim(" → check your token + its scopes (rrangler whoami)\n"));
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
fail(String(e?.message || e));
|
|
220
|
+
}
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
});
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
// rrangler command implementations. Each command is an async fn taking
|
|
2
|
+
// ({ pos, flags }) — positionals array + parsed flags object. Grouped by
|
|
3
|
+
// resource, mirroring wrangler's command tree.
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { getConfig, loadStore, saveStore, requireToken } from "../lib/config.mjs";
|
|
8
|
+
import { api, resolveWorker, resolveD1, resolveKv, resolvePages } from "../lib/api.mjs";
|
|
9
|
+
import { loadProjectConfig } from "../lib/project-config.mjs";
|
|
10
|
+
import { style, ok, info, warn, fail, printJson, table, withSpinner } from "../lib/ui.mjs";
|
|
11
|
+
|
|
12
|
+
const asJson = (flags) => flags.json === true;
|
|
13
|
+
|
|
14
|
+
// ── auth / meta ────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export async function login({ flags }) {
|
|
17
|
+
const token = flags.token || process.env.RRANGLER_TOKEN;
|
|
18
|
+
if (!token) throw new Error("provide a token: rrangler login --token <token>");
|
|
19
|
+
const store = loadStore();
|
|
20
|
+
store.token = token;
|
|
21
|
+
if (flags.api) store.api = String(flags.api).replace(/\/$/, "");
|
|
22
|
+
saveStore(store);
|
|
23
|
+
// Verify it works.
|
|
24
|
+
const me = await api("GET", "/api/edge/v1", { token });
|
|
25
|
+
ok(`logged in as ${style.bold(me.me?.username ?? "?")} (scopes: ${me.scopes?.join(", ") ?? "*"})`);
|
|
26
|
+
info(`saved to ${getConfig().configPath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function logout() {
|
|
30
|
+
const store = loadStore();
|
|
31
|
+
delete store.token;
|
|
32
|
+
saveStore(store);
|
|
33
|
+
ok("logged out (token removed)");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function whoami({ flags }) {
|
|
37
|
+
const me = await api("GET", "/api/edge/v1");
|
|
38
|
+
if (asJson(flags)) return printJson(me);
|
|
39
|
+
info(`${style.bold(me.me?.name ?? me.me?.username ?? "?")} @${me.me?.username} <${me.me?.email}>`);
|
|
40
|
+
info(`role: ${me.me?.role}${me.isAdmin ? style.dim(" (admin)") : ""}`);
|
|
41
|
+
info(`auth: ${me.authMode}${me.tokenId ? ` · token ${me.tokenId}` : ""}`);
|
|
42
|
+
info(`scopes: ${me.scopes ? me.scopes.join(", ") : style.green("* (full)")}`);
|
|
43
|
+
info(`api: ${getConfig().api}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function doctor({ flags }) {
|
|
47
|
+
const cfg = getConfig();
|
|
48
|
+
info(`api: ${cfg.api}`);
|
|
49
|
+
info(`token: ${cfg.token ? style.green("present") : style.red("missing")}`);
|
|
50
|
+
if (!cfg.token) return fail("no token — run `rrangler login --token <token>`");
|
|
51
|
+
try {
|
|
52
|
+
const me = await withSpinner("probing api…", () => api("GET", "/api/edge/v1"));
|
|
53
|
+
ok(`reachable — ${me.me?.username}, scopes: ${me.scopes?.join(", ") ?? "*"}`);
|
|
54
|
+
const cando = Object.entries(me.endpoints || {})
|
|
55
|
+
.filter(([, v]) => (v.scopes || []).every((s) => !me.scopes || me.scopes.includes("*") || me.scopes.includes(s)))
|
|
56
|
+
.length;
|
|
57
|
+
info(`you can call ${cando}/${Object.keys(me.endpoints || {}).length} endpoints with this token`);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
fail(String(e.message || e));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Raw API passthrough: rrangler api GET /api/edge/v1/workers */
|
|
64
|
+
export async function apiPassthrough({ pos, flags }) {
|
|
65
|
+
const [method = "GET", path] = pos;
|
|
66
|
+
if (!path) throw new Error("usage: rrangler api <METHOD> <path> [--data '<json>']");
|
|
67
|
+
const json = flags.data ? JSON.parse(flags.data) : undefined;
|
|
68
|
+
const out = await api(method.toUpperCase(), path, { json });
|
|
69
|
+
printJson(out);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── workers ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export async function workerList({ flags }) {
|
|
75
|
+
const list = await api("GET", "workers");
|
|
76
|
+
const workers = list.workers || list || [];
|
|
77
|
+
if (asJson(flags)) return printJson(workers);
|
|
78
|
+
table(["SLUG", "TITLE", "VERSION", "CREATED"], workers.map((w) => [w.slug, w.title ?? "", w.version ?? "", (w.createdAt ?? "").slice(0, 10)]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function workerGet({ pos, flags }) {
|
|
82
|
+
const w = await resolveWorker(pos[0]);
|
|
83
|
+
const detail = await api("GET", `workers/${w.id}`);
|
|
84
|
+
printJson(detail);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* deploy — push local files to a worker. Config (rrangler.json /
|
|
89
|
+
* wrangler.toml) supplies `name` (worker slug) and `main` (entry).
|
|
90
|
+
* `files` (rrangler.json array) lists extra modules; otherwise just
|
|
91
|
+
* the entry is sent.
|
|
92
|
+
*/
|
|
93
|
+
export async function deploy({ flags }) {
|
|
94
|
+
requireToken();
|
|
95
|
+
const cfg = loadProjectConfig();
|
|
96
|
+
if (!cfg) throw new Error("no rrangler.json / rrangler.toml / wrangler.toml in this directory");
|
|
97
|
+
const slug = flags.name || cfg.name;
|
|
98
|
+
const main = flags.main || cfg.main || "main.js";
|
|
99
|
+
if (!slug) throw new Error("missing worker `name` in config (or pass --name)");
|
|
100
|
+
if (!existsSync(main)) throw new Error(`entry file not found: ${main}`);
|
|
101
|
+
|
|
102
|
+
const paths = Array.isArray(cfg.files) && cfg.files.length ? cfg.files : [main];
|
|
103
|
+
const files = paths.map((p) => ({ path: basename(p), content: readFileSync(p, "utf8") }));
|
|
104
|
+
const entryFile = basename(main);
|
|
105
|
+
|
|
106
|
+
const worker = await resolveWorker(slug);
|
|
107
|
+
const res = await withSpinner(`deploying ${style.bold(slug)} (${files.length} file${files.length > 1 ? "s" : ""})…`, () =>
|
|
108
|
+
api("PUT", `workers/${worker.id}/files`, { json: { files, entryFile } }),
|
|
109
|
+
);
|
|
110
|
+
ok(`deployed ${style.bold(slug)} → version ${res.version ?? "?"}`);
|
|
111
|
+
if (worker.hostname || res.hostname) info(` https://${res.hostname || worker.hostname}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function tail({ pos, flags }) {
|
|
115
|
+
const w = await resolveWorker(pos[0]);
|
|
116
|
+
await streamLogs(`workers/${w.id}/logs`, w.slug, flags);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Shared log tail loop for workers + pages (same log shape). */
|
|
120
|
+
async function streamLogs(logPath, name, flags) {
|
|
121
|
+
info(style.dim(`tailing ${name} — Ctrl-C to stop`));
|
|
122
|
+
let since = Date.now();
|
|
123
|
+
// eslint-disable-next-line no-constant-condition
|
|
124
|
+
while (true) {
|
|
125
|
+
try {
|
|
126
|
+
const out = await api("GET", logPath, { query: { since } });
|
|
127
|
+
const logs = out.logs || out.lines || [];
|
|
128
|
+
for (const l of logs) {
|
|
129
|
+
const t = new Date(l.time || l.calledAt || Date.now()).toISOString().slice(11, 23);
|
|
130
|
+
const status = l.statusCode ? style.dim(`${l.statusCode}`) : "";
|
|
131
|
+
info(`${style.gray(t)} ${l.method || ""} ${l.path || l.message || ""} ${status}`);
|
|
132
|
+
}
|
|
133
|
+
if (out.now) since = out.now;
|
|
134
|
+
else if (logs.length) since = Date.now();
|
|
135
|
+
} catch (e) {
|
|
136
|
+
warn(String(e.message || e));
|
|
137
|
+
}
|
|
138
|
+
await sleep(Number(flags.interval) || 2000);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── worker builds / env / cron ─────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export async function workerBuilds({ pos, flags }) {
|
|
145
|
+
const w = await resolveWorker(pos[0]);
|
|
146
|
+
const out = await api("GET", `workers/${w.id}/builds`);
|
|
147
|
+
const builds = out.builds || out || [];
|
|
148
|
+
if (asJson(flags)) return printJson(builds);
|
|
149
|
+
table(["ID", "STATUS", "BRANCH", "AT"], builds.map((b) => [b.id, b.status, b.branch ?? "", (b.createdAt ?? b.startedAt ?? "").slice(0, 19)]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function workerBuild({ pos, flags }) {
|
|
153
|
+
const w = await resolveWorker(pos[0]);
|
|
154
|
+
const out = await api("POST", `workers/${w.id}/builds`, { json: flags.branch ? { branch: flags.branch } : {} });
|
|
155
|
+
ok(`build triggered for ${w.slug}${out.build?.id ? ` (id ${out.build.id})` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function workerBuildLog({ pos, flags }) {
|
|
159
|
+
const w = await resolveWorker(pos[0]);
|
|
160
|
+
const buildId = pos[1];
|
|
161
|
+
if (!buildId) throw new Error("usage: rrangler worker build-log <slug> <buildId> [--tail N]");
|
|
162
|
+
const out = await api("GET", `workers/${w.id}/builds/${buildId}`, { query: { logTail: flags.tail } });
|
|
163
|
+
if (asJson(flags)) return printJson(out);
|
|
164
|
+
info(`status: ${out.status}`);
|
|
165
|
+
process.stdout.write((out.log || out.logs || "") + "\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseKvPairs(pairs) {
|
|
169
|
+
const set = {};
|
|
170
|
+
for (const p of pairs) {
|
|
171
|
+
const i = p.indexOf("=");
|
|
172
|
+
if (i < 0) throw new Error(`bad KEY=VALUE: ${p}`);
|
|
173
|
+
set[p.slice(0, i)] = p.slice(i + 1);
|
|
174
|
+
}
|
|
175
|
+
return set;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function workerEnvSet({ pos, flags }) {
|
|
179
|
+
const w = await resolveWorker(pos[0]);
|
|
180
|
+
const set = parseKvPairs(pos.slice(1));
|
|
181
|
+
if (!Object.keys(set).length) throw new Error("usage: rrangler worker env set <slug> KEY=VAL ...");
|
|
182
|
+
await api("POST", `workers/${w.id}/env`, { json: { set, secretKeys: flags.secret ? Object.keys(set) : [] } });
|
|
183
|
+
ok(`set ${Object.keys(set).join(", ")} on ${w.slug}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function workerEnvDelete({ pos }) {
|
|
187
|
+
const w = await resolveWorker(pos[0]);
|
|
188
|
+
const del = pos.slice(1);
|
|
189
|
+
await api("POST", `workers/${w.id}/env`, { json: { delete: del } });
|
|
190
|
+
ok(`deleted ${del.join(", ")} from ${w.slug}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function workerCron({ pos, flags }) {
|
|
194
|
+
const w = await resolveWorker(pos[0]);
|
|
195
|
+
const out = await api("GET", `workers/${w.id}/cron-runs`, { query: { dlq: flags.dlq ? 1 : undefined, limit: flags.limit } });
|
|
196
|
+
const runs = out.runs || out || [];
|
|
197
|
+
if (asJson(flags)) return printJson(runs);
|
|
198
|
+
table(["ID", "STATUS", "EXPR", "AT"], runs.map((r) => [r.id, r.status, r.expression ?? r.cron ?? "", (r.firedAt ?? r.createdAt ?? "").slice(0, 19)]));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function workerCronReplay({ pos }) {
|
|
202
|
+
const w = await resolveWorker(pos[0]);
|
|
203
|
+
const runId = pos[1];
|
|
204
|
+
if (!runId) throw new Error("usage: rrangler worker cron replay <slug> <runId>");
|
|
205
|
+
await api("POST", `workers/${w.id}/cron-runs/${runId}/replay`);
|
|
206
|
+
ok(`replayed cron run ${runId}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── pages ──────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export async function pagesGet({ pos }) {
|
|
212
|
+
const p = await resolvePages(pos[0]);
|
|
213
|
+
printJson(await api("GET", `pages/${p.id}`));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function pagesEnvSet({ pos, flags }) {
|
|
217
|
+
const p = await resolvePages(pos[0]);
|
|
218
|
+
const set = parseKvPairs(pos.slice(1));
|
|
219
|
+
await api("POST", `pages/${p.id}/env`, { json: { set, secretKeys: flags.secret ? Object.keys(set) : [] } });
|
|
220
|
+
ok(`set ${Object.keys(set).join(", ")} on ${p.slug}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function pagesEnvDelete({ pos }) {
|
|
224
|
+
const p = await resolvePages(pos[0]);
|
|
225
|
+
await api("POST", `pages/${p.id}/env`, { json: { delete: pos.slice(1) } });
|
|
226
|
+
ok(`deleted ${pos.slice(1).join(", ")} from ${p.slug}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function pagesBuilds({ pos, flags }) {
|
|
230
|
+
const p = await resolvePages(pos[0]);
|
|
231
|
+
const out = await api("GET", `pages/${p.id}/builds`);
|
|
232
|
+
const builds = out.builds || out || [];
|
|
233
|
+
if (asJson(flags)) return printJson(builds);
|
|
234
|
+
table(["ID", "STATUS", "BRANCH", "AT"], builds.map((b) => [b.id, b.status, b.branch ?? "", (b.createdAt ?? "").slice(0, 19)]));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function pagesBuild({ pos, flags }) {
|
|
238
|
+
const p = await resolvePages(pos[0]);
|
|
239
|
+
await api("POST", `pages/${p.id}/builds`, { json: flags.branch ? { branch: flags.branch } : {} });
|
|
240
|
+
ok(`build triggered for ${p.slug}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function pagesBuildLog({ pos, flags }) {
|
|
244
|
+
const p = await resolvePages(pos[0]);
|
|
245
|
+
const out = await api("GET", `pages/${p.id}/builds/${pos[1]}`, { query: { logTail: flags.tail } });
|
|
246
|
+
if (asJson(flags)) return printJson(out);
|
|
247
|
+
info(`status: ${out.status}`);
|
|
248
|
+
process.stdout.write((out.log || out.logs || "") + "\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function pagesTail({ pos, flags }) {
|
|
252
|
+
const p = await resolvePages(pos[0]);
|
|
253
|
+
await streamLogs(`pages/${p.id}/logs`, p.slug, flags);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── secrets (worker env) ───────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export async function secretPut({ pos, flags }) {
|
|
259
|
+
const key = pos[0];
|
|
260
|
+
if (!key) throw new Error("usage: rrangler secret put <KEY> --worker <slug> [--value <v>]");
|
|
261
|
+
const w = await resolveWorker(flags.worker || flags.name);
|
|
262
|
+
const value = flags.value != null ? String(flags.value) : (await readStdin()).trim();
|
|
263
|
+
await api("POST", `workers/${w.id}/env`, { json: { set: { [key]: value }, secretKeys: [key] } });
|
|
264
|
+
ok(`secret ${style.bold(key)} set on ${w.slug}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function secretList({ flags }) {
|
|
268
|
+
const w = await resolveWorker(flags.worker || flags.name);
|
|
269
|
+
const detail = await api("GET", `workers/${w.id}`);
|
|
270
|
+
const keys = detail.envSecretKeys || detail.secretKeys || [];
|
|
271
|
+
if (asJson(flags)) return printJson(keys);
|
|
272
|
+
keys.length ? keys.forEach((k) => info(k)) : info(style.dim("(no secrets)"));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function secretDelete({ pos, flags }) {
|
|
276
|
+
const key = pos[0];
|
|
277
|
+
const w = await resolveWorker(flags.worker || flags.name);
|
|
278
|
+
await api("POST", `workers/${w.id}/env`, { json: { delete: [key] } });
|
|
279
|
+
ok(`secret ${key} deleted from ${w.slug}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── D1 ─────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
export async function d1List({ flags }) {
|
|
285
|
+
const list = await api("GET", "d1");
|
|
286
|
+
const dbs = list.databases || list || [];
|
|
287
|
+
if (asJson(flags)) return printJson(dbs);
|
|
288
|
+
table(["NAME", "ID"], dbs.map((d) => [d.name, d.id]));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function d1Create({ pos }) {
|
|
292
|
+
const name = pos[0];
|
|
293
|
+
if (!name) throw new Error("usage: rrangler d1 create <name>");
|
|
294
|
+
const out = await api("POST", "d1", { json: { name } });
|
|
295
|
+
ok(`D1 database ${style.bold(name)} created (id ${out.id || out.database?.id})`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function d1Execute({ pos, flags }) {
|
|
299
|
+
const db = await resolveD1(pos[0]);
|
|
300
|
+
let sql = flags.command;
|
|
301
|
+
if (flags.file) sql = readFileSync(flags.file, "utf8");
|
|
302
|
+
if (!sql) throw new Error("usage: rrangler d1 execute <db> (--command '<sql>' | --file <f.sql>)");
|
|
303
|
+
// Multi-statement / DDL → /exec; single SELECT → /query.
|
|
304
|
+
const multi = /;\s*\S/.test(sql.trim().replace(/;\s*$/, ""));
|
|
305
|
+
const endpoint = multi || !/^\s*select/i.test(sql) ? "exec" : "query";
|
|
306
|
+
const out = await api("POST", `d1/${db.id}/${endpoint}`, { json: { sql } });
|
|
307
|
+
printJson(out);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── KV ─────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
export async function kvNamespaceList({ flags }) {
|
|
313
|
+
const list = await api("GET", "kv");
|
|
314
|
+
const ns = list.namespaces || list || [];
|
|
315
|
+
if (asJson(flags)) return printJson(ns);
|
|
316
|
+
table(["TITLE", "ID"], ns.map((n) => [n.title || n.name, n.id]));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function kvNamespaceCreate({ pos }) {
|
|
320
|
+
const title = pos[0];
|
|
321
|
+
if (!title) throw new Error("usage: rrangler kv namespace create <title>");
|
|
322
|
+
const out = await api("POST", "kv", { json: { title } });
|
|
323
|
+
ok(`KV namespace ${style.bold(title)} created (id ${out.id || out.namespace?.id})`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function kvKeyPut({ pos, flags }) {
|
|
327
|
+
const [nsName, key] = pos;
|
|
328
|
+
const value = pos[2] != null ? pos[2] : flags.value;
|
|
329
|
+
if (!nsName || !key || value == null) throw new Error("usage: rrangler kv key put <ns> <key> <value>");
|
|
330
|
+
const ns = await resolveKv(nsName);
|
|
331
|
+
await api("PUT", `kv/${ns.id}/${encodeURIComponent(key)}`, {
|
|
332
|
+
headers: { "content-type": "text/plain" },
|
|
333
|
+
body: String(value),
|
|
334
|
+
query: flags.ttl ? { expiration_ttl: flags.ttl } : undefined,
|
|
335
|
+
});
|
|
336
|
+
ok(`kv ${nsName}/${key} set`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function kvKeyGet({ pos }) {
|
|
340
|
+
const [nsName, key] = pos;
|
|
341
|
+
const ns = await resolveKv(nsName);
|
|
342
|
+
const res = await api("GET", `kv/${ns.id}/${encodeURIComponent(key)}`, { raw: true });
|
|
343
|
+
if (!res.ok) throw new Error(`kv get ${res.status}`);
|
|
344
|
+
process.stdout.write((await res.text()) + "\n");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function kvKeyList({ pos, flags }) {
|
|
348
|
+
const ns = await resolveKv(pos[0]);
|
|
349
|
+
const out = await api("GET", `kv/${ns.id}`, { query: { prefix: flags.prefix } });
|
|
350
|
+
printJson(out.keys || out.items || out);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function kvKeyDelete({ pos }) {
|
|
354
|
+
const [nsName, key] = pos;
|
|
355
|
+
const ns = await resolveKv(nsName);
|
|
356
|
+
await api("DELETE", `kv/${ns.id}/${encodeURIComponent(key)}`);
|
|
357
|
+
ok(`kv ${nsName}/${key} deleted`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── R2 ─────────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
export async function r2BucketList({ flags }) {
|
|
363
|
+
const list = await api("GET", "r2");
|
|
364
|
+
const buckets = list.buckets || list || [];
|
|
365
|
+
if (asJson(flags)) return printJson(buckets);
|
|
366
|
+
table(["NAME", "ID"], buckets.map((b) => [b.name, b.id]));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function r2BucketCreate({ pos, flags }) {
|
|
370
|
+
const name = pos[0];
|
|
371
|
+
if (!name) throw new Error("usage: rrangler r2 bucket create <name> [--public]");
|
|
372
|
+
const out = await api("POST", "r2", { json: { name, publicAccess: !!flags.public } });
|
|
373
|
+
ok(`R2 bucket ${style.bold(name)} created (id ${out.id || out.bucket?.id})`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function r2ObjectPut({ pos }) {
|
|
377
|
+
const [bucket, key, file] = pos;
|
|
378
|
+
if (!bucket || !key || !file) throw new Error("usage: rrangler r2 object put <bucket> <key> <file>");
|
|
379
|
+
const body = readFileSync(file);
|
|
380
|
+
await api("PUT", `r2/${encodeURIComponent(bucket)}/${key}`, { body });
|
|
381
|
+
ok(`r2 ${bucket}/${key} uploaded (${body.length} bytes)`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export async function r2ObjectGet({ pos, flags }) {
|
|
385
|
+
const [bucket, key] = pos;
|
|
386
|
+
const res = await api("GET", `r2/${encodeURIComponent(bucket)}/${key}`, { raw: true });
|
|
387
|
+
if (!res.ok) throw new Error(`r2 get ${res.status}`);
|
|
388
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
389
|
+
if (flags.output) {
|
|
390
|
+
writeFileSync(flags.output, buf);
|
|
391
|
+
ok(`saved ${buf.length} bytes → ${flags.output}`);
|
|
392
|
+
} else {
|
|
393
|
+
process.stdout.write(buf);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function r2ObjectDelete({ pos }) {
|
|
398
|
+
const [bucket, key] = pos;
|
|
399
|
+
await api("DELETE", `r2/${encodeURIComponent(bucket)}/${key}`);
|
|
400
|
+
ok(`r2 ${bucket}/${key} deleted`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── pages ──────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
export async function pagesList({ flags }) {
|
|
406
|
+
const list = await api("GET", "pages");
|
|
407
|
+
const projects = list.projects || list || [];
|
|
408
|
+
if (asJson(flags)) return printJson(projects);
|
|
409
|
+
table(["SLUG", "NAME"], projects.map((p) => [p.slug, p.name ?? ""]));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── init (scaffold) ────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
export async function init({ pos }) {
|
|
415
|
+
const name = pos[0] || basename(process.cwd());
|
|
416
|
+
if (!existsSync("rrangler.json")) {
|
|
417
|
+
writeFileSync("rrangler.json", JSON.stringify({ name, main: "main.js", compatibilityDate: new Date().toISOString().slice(0, 10) }, null, 2) + "\n");
|
|
418
|
+
ok("wrote rrangler.json");
|
|
419
|
+
}
|
|
420
|
+
if (!existsSync("main.js")) {
|
|
421
|
+
writeFileSync("main.js", `export default {\n async fetch(request, env, ctx) {\n return new Response("hello from ${name}");\n },\n};\n`);
|
|
422
|
+
ok("wrote main.js");
|
|
423
|
+
}
|
|
424
|
+
info(`next: create the worker in bigrandall.io → /me/edge, then \`rrangler deploy\``);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── d1: export / info / migrations ─────────────────────────────────
|
|
428
|
+
|
|
429
|
+
/** Run a SQL statement on a D1 db, returning the rows array. */
|
|
430
|
+
async function d1q(id, sql, params) {
|
|
431
|
+
const out = await api("POST", `d1/${id}/query`, { json: { sql, ...(params ? { params } : {}) } });
|
|
432
|
+
return out.results || out.rows || out.result?.rows || out.result || [];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function sqlLiteral(v) {
|
|
436
|
+
if (v === null || v === undefined) return "NULL";
|
|
437
|
+
if (typeof v === "number") return Number.isFinite(v) ? String(v) : "NULL";
|
|
438
|
+
if (typeof v === "boolean") return v ? "1" : "0";
|
|
439
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function d1Export({ pos, flags }) {
|
|
443
|
+
const db = await resolveD1(pos[0]);
|
|
444
|
+
const tables = await d1q(db.id, "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name");
|
|
445
|
+
let dump = `-- rrangler d1 export of ${db.name}\nPRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n`;
|
|
446
|
+
for (const t of tables) {
|
|
447
|
+
const name = t.name;
|
|
448
|
+
if (t.sql) dump += `${t.sql};\n`;
|
|
449
|
+
const rows = flags["no-data"] ? [] : await d1q(db.id, `SELECT * FROM "${name}"`);
|
|
450
|
+
for (const row of rows) {
|
|
451
|
+
const cols = Object.keys(row);
|
|
452
|
+
dump += `INSERT INTO "${name}" (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${cols.map((c) => sqlLiteral(row[c])).join(", ")});\n`;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
dump += "COMMIT;\n";
|
|
456
|
+
if (flags.output) {
|
|
457
|
+
writeFileSync(flags.output, dump);
|
|
458
|
+
ok(`exported ${db.name} → ${flags.output} (${tables.length} tables)`);
|
|
459
|
+
} else {
|
|
460
|
+
process.stdout.write(dump);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function d1Info({ pos, flags }) {
|
|
465
|
+
const db = await resolveD1(pos[0]);
|
|
466
|
+
const tables = await d1q(db.id, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
|
|
467
|
+
const info2 = { name: db.name, id: db.id, tables: tables.map((t) => t.name) };
|
|
468
|
+
if (asJson(flags)) return printJson(info2);
|
|
469
|
+
info(`${style.bold(db.name)} (${db.id})`);
|
|
470
|
+
info(`tables: ${info2.tables.join(", ") || "(none)"}`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function migrationsDir(flags) {
|
|
474
|
+
return flags.dir || "migrations";
|
|
475
|
+
}
|
|
476
|
+
function readSqlDir(dir) {
|
|
477
|
+
if (!existsSync(dir)) return [];
|
|
478
|
+
return readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function d1MigrationsCreate({ pos, flags }) {
|
|
482
|
+
const name = pos[1] || pos[0];
|
|
483
|
+
const dir = migrationsDir(flags);
|
|
484
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
485
|
+
const stamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
486
|
+
const file = join(dir, `${stamp}_${(name || "migration").replace(/\W+/g, "_")}.sql`);
|
|
487
|
+
writeFileSync(file, "-- migration\n");
|
|
488
|
+
ok(`created ${file}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export async function d1MigrationsList({ pos, flags }) {
|
|
492
|
+
const db = await resolveD1(pos[0]);
|
|
493
|
+
const dir = migrationsDir(flags);
|
|
494
|
+
const files = readSqlDir(dir);
|
|
495
|
+
let applied = new Set();
|
|
496
|
+
try {
|
|
497
|
+
const rows = await d1q(db.id, "SELECT name FROM d1_migrations");
|
|
498
|
+
applied = new Set(rows.map((r) => r.name));
|
|
499
|
+
} catch {
|
|
500
|
+
/* table not created yet */
|
|
501
|
+
}
|
|
502
|
+
if (asJson(flags)) return printJson(files.map((f) => ({ name: f, applied: applied.has(f) })));
|
|
503
|
+
files.forEach((f) => info(`${applied.has(f) ? style.green("✔") : style.dim("·")} ${f}`));
|
|
504
|
+
if (!files.length) info(style.dim(`(no .sql files in ${dir})`));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function d1MigrationsApply({ pos, flags }) {
|
|
508
|
+
const db = await resolveD1(pos[0]);
|
|
509
|
+
const dir = migrationsDir(flags);
|
|
510
|
+
const files = readSqlDir(dir);
|
|
511
|
+
await api("POST", `d1/${db.id}/exec`, { json: { sql: "CREATE TABLE IF NOT EXISTS d1_migrations (id INTEGER PRIMARY KEY, name TEXT UNIQUE, applied_at TEXT)" } });
|
|
512
|
+
const appliedRows = await d1q(db.id, "SELECT name FROM d1_migrations");
|
|
513
|
+
const applied = new Set(appliedRows.map((r) => r.name));
|
|
514
|
+
let n = 0;
|
|
515
|
+
for (const f of files) {
|
|
516
|
+
if (applied.has(f)) continue;
|
|
517
|
+
const sql = readFileSync(join(dir, f), "utf8");
|
|
518
|
+
await withSpinner(`applying ${f}…`, () => api("POST", `d1/${db.id}/exec`, { json: { sql } }));
|
|
519
|
+
await api("POST", `d1/${db.id}/exec`, { json: { sql: "INSERT INTO d1_migrations (name, applied_at) VALUES (?, ?)", params: [f, new Date().toISOString()] } });
|
|
520
|
+
ok(`applied ${f}`);
|
|
521
|
+
n++;
|
|
522
|
+
}
|
|
523
|
+
info(n ? `${n} migration(s) applied` : "already up to date");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ── kv / secret bulk ───────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
export async function kvBulkPut({ pos }) {
|
|
529
|
+
const ns = await resolveKv(pos[0]);
|
|
530
|
+
const entries = JSON.parse(readFileSync(pos[1], "utf8"));
|
|
531
|
+
for (const e of entries) {
|
|
532
|
+
await api("PUT", `kv/${ns.id}/${encodeURIComponent(e.key)}`, {
|
|
533
|
+
headers: { "content-type": "text/plain" },
|
|
534
|
+
body: String(e.value),
|
|
535
|
+
query: e.expiration_ttl ? { expiration_ttl: e.expiration_ttl } : undefined,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
ok(`put ${entries.length} keys into ${pos[0]}`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function kvBulkDelete({ pos }) {
|
|
542
|
+
const ns = await resolveKv(pos[0]);
|
|
543
|
+
const keys = JSON.parse(readFileSync(pos[1], "utf8"));
|
|
544
|
+
for (const k of keys) await api("DELETE", `kv/${ns.id}/${encodeURIComponent(k)}`);
|
|
545
|
+
ok(`deleted ${keys.length} keys from ${pos[0]}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export async function secretBulk({ pos, flags }) {
|
|
549
|
+
const w = await resolveWorker(flags.worker || flags.name);
|
|
550
|
+
const set = JSON.parse(readFileSync(pos[0], "utf8"));
|
|
551
|
+
await api("POST", `workers/${w.id}/env`, { json: { set, secretKeys: Object.keys(set) } });
|
|
552
|
+
ok(`set ${Object.keys(set).length} secrets on ${w.slug}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── types (generate bindings d.ts) ─────────────────────────────────
|
|
556
|
+
|
|
557
|
+
export async function types({ pos, flags }) {
|
|
558
|
+
const cfg = loadProjectConfig();
|
|
559
|
+
const slug = flags.name || cfg?.name || pos[0];
|
|
560
|
+
if (!slug) throw new Error("no worker name (config or --name)");
|
|
561
|
+
const w = await resolveWorker(slug);
|
|
562
|
+
const detail = await api("GET", `workers/${w.id}`);
|
|
563
|
+
const lines = ["interface Env {"];
|
|
564
|
+
for (const b of detail.kvBindings || []) lines.push(` ${b.bindingName}: KVNamespace;`);
|
|
565
|
+
for (const b of detail.d1Bindings || []) lines.push(` ${b.bindingName}: D1Database;`);
|
|
566
|
+
for (const b of detail.r2Bindings || []) lines.push(` ${b.bindingName}: R2Bucket;`);
|
|
567
|
+
for (const b of detail.doBindings || []) lines.push(` ${b.bindingName}: DurableObjectNamespace;`);
|
|
568
|
+
for (const k of Object.keys(detail.envJson || {})) lines.push(` ${k}: string;`);
|
|
569
|
+
for (const k of detail.envSecretKeys || []) lines.push(` ${k}: string;`);
|
|
570
|
+
lines.push("}");
|
|
571
|
+
const out = lines.join("\n") + "\n";
|
|
572
|
+
const dest = flags.out || "worker-configuration.d.ts";
|
|
573
|
+
writeFileSync(dest, out);
|
|
574
|
+
ok(`wrote ${dest}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── dev (local workerd) ────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
export async function dev({ flags }) {
|
|
580
|
+
const cp = await import("node:child_process");
|
|
581
|
+
const workerd = flags.workerd || whichWorkerd(cp);
|
|
582
|
+
if (!workerd) {
|
|
583
|
+
fail("workerd not found on PATH.");
|
|
584
|
+
info("install it (`npm i -g workerd`) or pass --workerd <path>. `rrangler dev` runs your worker on the same runtime as production.");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const cfg = loadProjectConfig() || {};
|
|
588
|
+
const main = flags.main || cfg.main || "main.js";
|
|
589
|
+
if (!existsSync(main)) throw new Error(`entry not found: ${main}`);
|
|
590
|
+
const port = Number(flags.port) || 8787;
|
|
591
|
+
const paths = Array.isArray(cfg.files) && cfg.files.length ? cfg.files : [main];
|
|
592
|
+
const modules = paths.map((p) => ` (name = "${basename(p)}", esModule = embed "${p}")`).join(",\n");
|
|
593
|
+
const capnp = `using Workerd = import "/workerd/workerd.capnp";
|
|
594
|
+
const config :Workerd.Config = (
|
|
595
|
+
services = [ (name = "main", worker = .mainWorker) ],
|
|
596
|
+
sockets = [ (name = "http", address = "*:${port}", http = (), service = "main") ],
|
|
597
|
+
);
|
|
598
|
+
const mainWorker :Workerd.Worker = (
|
|
599
|
+
modules = [
|
|
600
|
+
${modules}
|
|
601
|
+
],
|
|
602
|
+
compatibilityDate = "${cfg.compatibilityDate || "2024-11-01"}",
|
|
603
|
+
);
|
|
604
|
+
`;
|
|
605
|
+
const tmp = join(process.cwd(), ".rrangler-dev.capnp");
|
|
606
|
+
writeFileSync(tmp, capnp);
|
|
607
|
+
ok(`workerd serving ${style.bold(main)} on http://127.0.0.1:${port} (Ctrl-C to stop)`);
|
|
608
|
+
const child = cp.spawn(workerd, ["serve", tmp], { stdio: "inherit" });
|
|
609
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function whichWorkerd(cp) {
|
|
613
|
+
try {
|
|
614
|
+
return cp.execSync("command -v workerd", { encoding: "utf8" }).trim() || null;
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── helpers ────────────────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
function sleep(ms) {
|
|
623
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
624
|
+
}
|
|
625
|
+
function readStdin() {
|
|
626
|
+
return new Promise((resolve) => {
|
|
627
|
+
let data = "";
|
|
628
|
+
process.stdin.on("data", (c) => (data += c));
|
|
629
|
+
process.stdin.on("end", () => resolve(data));
|
|
630
|
+
});
|
|
631
|
+
}
|
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// API client for the RandallFlare edge REST API (/api/edge/v1/*).
|
|
2
|
+
// Bearer-token auth; friendly errors. Node's global fetch (>=18).
|
|
3
|
+
|
|
4
|
+
import { getConfig } from "./config.mjs";
|
|
5
|
+
|
|
6
|
+
export class ApiError extends Error {
|
|
7
|
+
constructor(status, body, path) {
|
|
8
|
+
super(
|
|
9
|
+
`API ${status} on ${path}` +
|
|
10
|
+
(body?.error ? `: ${body.error}` : body ? `: ${JSON.stringify(body).slice(0, 200)}` : ""),
|
|
11
|
+
);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.body = body;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Call the edge API.
|
|
19
|
+
* method - GET/POST/PUT/DELETE
|
|
20
|
+
* path - "/api/edge/v1/..." (leading slash) OR "workers" shorthand
|
|
21
|
+
* opts - { json, body, query, headers, raw, token, api }
|
|
22
|
+
*
|
|
23
|
+
* Returns parsed JSON by default; { raw:true } returns the Response.
|
|
24
|
+
*/
|
|
25
|
+
export async function api(method, path, opts = {}) {
|
|
26
|
+
const cfg = getConfig();
|
|
27
|
+
const token = opts.token || cfg.token;
|
|
28
|
+
const base = (opts.api || cfg.api).replace(/\/$/, "");
|
|
29
|
+
let p = path.startsWith("/") ? path : `/api/edge/v1/${path}`;
|
|
30
|
+
if (opts.query) {
|
|
31
|
+
const qs = new URLSearchParams(
|
|
32
|
+
Object.entries(opts.query).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]),
|
|
33
|
+
).toString();
|
|
34
|
+
if (qs) p += (p.includes("?") ? "&" : "?") + qs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const headers = { accept: "application/json", ...(opts.headers || {}) };
|
|
38
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
39
|
+
let body = opts.body;
|
|
40
|
+
if (opts.json !== undefined) {
|
|
41
|
+
headers["content-type"] = "application/json";
|
|
42
|
+
body = JSON.stringify(opts.json);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const res = await fetch(base + p, { method, headers, body });
|
|
46
|
+
if (opts.raw) return res;
|
|
47
|
+
|
|
48
|
+
const ct = res.headers.get("content-type") || "";
|
|
49
|
+
const parsed = ct.includes("json") ? await res.json().catch(() => null) : await res.text();
|
|
50
|
+
if (!res.ok) throw new ApiError(res.status, parsed, p);
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Resolve a worker slug → id (list + match). Accepts an id directly too. */
|
|
55
|
+
export async function resolveWorker(slugOrId) {
|
|
56
|
+
const list = await api("GET", "workers");
|
|
57
|
+
const workers = list.workers || list.items || list || [];
|
|
58
|
+
const hit = workers.find((w) => w.slug === slugOrId || w.id === slugOrId);
|
|
59
|
+
if (!hit) throw new Error(`worker "${slugOrId}" not found (yours: ${workers.map((w) => w.slug).join(", ") || "none"})`);
|
|
60
|
+
return hit;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Resolve a D1 database name → id. */
|
|
64
|
+
export async function resolveD1(nameOrId) {
|
|
65
|
+
const list = await api("GET", "d1");
|
|
66
|
+
const dbs = list.databases || list.items || list || [];
|
|
67
|
+
const hit = dbs.find((d) => d.name === nameOrId || d.id === nameOrId);
|
|
68
|
+
if (!hit) throw new Error(`D1 database "${nameOrId}" not found`);
|
|
69
|
+
return hit;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve a KV namespace name/title → id. */
|
|
73
|
+
export async function resolveKv(nameOrId) {
|
|
74
|
+
const list = await api("GET", "kv");
|
|
75
|
+
const ns = list.namespaces || list.items || list || [];
|
|
76
|
+
const hit = ns.find((n) => n.title === nameOrId || n.name === nameOrId || n.id === nameOrId);
|
|
77
|
+
if (!hit) throw new Error(`KV namespace "${nameOrId}" not found`);
|
|
78
|
+
return hit;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve a Pages project slug/name → id. */
|
|
82
|
+
export async function resolvePages(slugOrId) {
|
|
83
|
+
const list = await api("GET", "pages");
|
|
84
|
+
const projects = list.projects || list.items || list || [];
|
|
85
|
+
const hit = projects.find((p) => p.slug === slugOrId || p.name === slugOrId || p.id === slugOrId);
|
|
86
|
+
if (!hit) throw new Error(`Pages project "${slugOrId}" not found`);
|
|
87
|
+
return hit;
|
|
88
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Config + credentials resolution.
|
|
2
|
+
//
|
|
3
|
+
// Precedence (highest first):
|
|
4
|
+
// 1. env RRANGLER_TOKEN / RRANGLER_API
|
|
5
|
+
// 2. ~/.rrangler.json (written by `rrangler login`)
|
|
6
|
+
// 3. defaults
|
|
7
|
+
//
|
|
8
|
+
// The token is a RandallFlare EdgeAccessToken (created in the
|
|
9
|
+
// bigrandall.io backend → /me/edge → API tokens). Scopes on the token
|
|
10
|
+
// gate what commands work.
|
|
11
|
+
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { readFileSync, writeFileSync, chmodSync, existsSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
const CONFIG_PATH = join(homedir(), ".rrangler.json");
|
|
17
|
+
const DEFAULT_API = "https://bigrandall.io";
|
|
18
|
+
|
|
19
|
+
export function loadStore() {
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(CONFIG_PATH)) {
|
|
22
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
/* corrupt config → treat as empty */
|
|
26
|
+
}
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function saveStore(store) {
|
|
31
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(store, null, 2) + "\n");
|
|
32
|
+
try {
|
|
33
|
+
chmodSync(CONFIG_PATH, 0o600); // token is a secret
|
|
34
|
+
} catch {
|
|
35
|
+
/* best effort */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getConfig() {
|
|
40
|
+
const store = loadStore();
|
|
41
|
+
const api = (process.env.RRANGLER_API || store.api || DEFAULT_API).replace(/\/$/, "");
|
|
42
|
+
const token = process.env.RRANGLER_TOKEN || store.token || null;
|
|
43
|
+
return { api, token, configPath: CONFIG_PATH };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function requireToken() {
|
|
47
|
+
const cfg = getConfig();
|
|
48
|
+
if (!cfg.token) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"not logged in — run `rrangler login --token <token>` or set RRANGLER_TOKEN.\n" +
|
|
51
|
+
"Create a token in bigrandall.io → /me/edge → API tokens.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return cfg;
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Read a project's deploy config from the current directory.
|
|
2
|
+
//
|
|
3
|
+
// Supports (in order): rrangler.json, rrangler.toml, wrangler.toml
|
|
4
|
+
// (wrangler compat — we read the fields we need: name, main,
|
|
5
|
+
// compatibility_date). Full TOML isn't parsed; a small line reader
|
|
6
|
+
// pulls the top-level scalars, which covers the deploy path.
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
function readMiniToml(text) {
|
|
12
|
+
const out = {};
|
|
13
|
+
let section = "";
|
|
14
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
15
|
+
const line = raw.replace(/#.*$/, "").trim();
|
|
16
|
+
if (!line) continue;
|
|
17
|
+
if (line.startsWith("[")) {
|
|
18
|
+
section = line.replace(/[[\]]/g, "");
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const m = /^([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(line);
|
|
22
|
+
if (!m || section) continue; // only top-level scalars
|
|
23
|
+
let v = m[2].trim();
|
|
24
|
+
if (/^".*"$/.test(v) || /^'.*'$/.test(v)) v = v.slice(1, -1);
|
|
25
|
+
else if (v === "true" || v === "false") v = v === "true";
|
|
26
|
+
else if (/^-?\d+$/.test(v)) v = Number(v);
|
|
27
|
+
out[m[1]] = v;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadProjectConfig(dir = process.cwd()) {
|
|
33
|
+
const jsonPath = join(dir, "rrangler.json");
|
|
34
|
+
if (existsSync(jsonPath)) {
|
|
35
|
+
return { source: "rrangler.json", ...JSON.parse(readFileSync(jsonPath, "utf8")) };
|
|
36
|
+
}
|
|
37
|
+
for (const [file, label] of [
|
|
38
|
+
["rrangler.toml", "rrangler.toml"],
|
|
39
|
+
["wrangler.toml", "wrangler.toml"],
|
|
40
|
+
]) {
|
|
41
|
+
const p = join(dir, file);
|
|
42
|
+
if (existsSync(p)) {
|
|
43
|
+
const t = readMiniToml(readFileSync(p, "utf8"));
|
|
44
|
+
return {
|
|
45
|
+
source: label,
|
|
46
|
+
name: t.name,
|
|
47
|
+
main: t.main,
|
|
48
|
+
compatibilityDate: t.compatibility_date,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
package/lib/ui.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Tiny terminal UI helpers — colors, tables, spinners. Zero deps.
|
|
2
|
+
|
|
3
|
+
const TTY = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4
|
+
const c = (code) => (s) => (TTY ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
5
|
+
|
|
6
|
+
export const style = {
|
|
7
|
+
bold: c("1"),
|
|
8
|
+
dim: c("2"),
|
|
9
|
+
red: c("31"),
|
|
10
|
+
green: c("32"),
|
|
11
|
+
yellow: c("33"),
|
|
12
|
+
blue: c("34"),
|
|
13
|
+
magenta: c("35"),
|
|
14
|
+
cyan: c("36"),
|
|
15
|
+
gray: c("90"),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function info(msg) {
|
|
19
|
+
process.stderr.write(msg + "\n");
|
|
20
|
+
}
|
|
21
|
+
export function ok(msg) {
|
|
22
|
+
process.stderr.write(style.green("✔ ") + msg + "\n");
|
|
23
|
+
}
|
|
24
|
+
export function warn(msg) {
|
|
25
|
+
process.stderr.write(style.yellow("▲ ") + msg + "\n");
|
|
26
|
+
}
|
|
27
|
+
export function fail(msg) {
|
|
28
|
+
process.stderr.write(style.red("✘ ") + msg + "\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Print JSON to stdout (the machine-readable channel). */
|
|
32
|
+
export function printJson(obj) {
|
|
33
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Simple left-aligned column table for stdout. rows: string[][]. */
|
|
37
|
+
export function table(headers, rows) {
|
|
38
|
+
const all = [headers, ...rows];
|
|
39
|
+
const widths = headers.map((_, i) =>
|
|
40
|
+
Math.max(...all.map((r) => String(r[i] ?? "").length)),
|
|
41
|
+
);
|
|
42
|
+
const line = (r) =>
|
|
43
|
+
r.map((cell, i) => String(cell ?? "").padEnd(widths[i])).join(" ");
|
|
44
|
+
process.stdout.write(style.bold(line(headers)) + "\n");
|
|
45
|
+
for (const r of rows) process.stdout.write(line(r) + "\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A minimal indeterminate spinner around an async task. */
|
|
49
|
+
export async function withSpinner(label, fn) {
|
|
50
|
+
if (!TTY) return fn();
|
|
51
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
52
|
+
let i = 0;
|
|
53
|
+
const t = setInterval(() => {
|
|
54
|
+
process.stderr.write(`\r${style.cyan(frames[i++ % frames.length])} ${label}`);
|
|
55
|
+
}, 80);
|
|
56
|
+
try {
|
|
57
|
+
return await fn();
|
|
58
|
+
} finally {
|
|
59
|
+
clearInterval(t);
|
|
60
|
+
process.stderr.write("\r\x1b[2K");
|
|
61
|
+
}
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bigrandall/rrangler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "wrangler-compatible CLI for RandallFlare — the self-hosted Cloudflare-Workers edge platform in bigrandall.io. Deploy workers, manage D1/KV/R2, tail logs, run locally on workerd.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rrangler": "bin/rrangler.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"commands",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node bin/rrangler.mjs --version",
|
|
20
|
+
"prepublishOnly": "node bin/rrangler.mjs --version"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"rrangler",
|
|
24
|
+
"wrangler",
|
|
25
|
+
"cloudflare",
|
|
26
|
+
"workers",
|
|
27
|
+
"workerd",
|
|
28
|
+
"edge",
|
|
29
|
+
"serverless",
|
|
30
|
+
"cli",
|
|
31
|
+
"randallflare",
|
|
32
|
+
"bigrandall",
|
|
33
|
+
"d1",
|
|
34
|
+
"kv",
|
|
35
|
+
"r2",
|
|
36
|
+
"durable-objects"
|
|
37
|
+
],
|
|
38
|
+
"author": "Randall <me@bigrandall.io>",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"homepage": "https://github.com/RandallAnjie/bigrandall.io/tree/main/rrangler#readme",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/RandallAnjie/bigrandall.io.git",
|
|
44
|
+
"directory": "rrangler"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/RandallAnjie/bigrandall.io/issues"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|