@smoothbricks/cli 0.9.0 → 0.10.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.
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
2
|
-
/** First `[env.<name>]`
|
|
2
|
+
/** First `[env.<name>]` (source order; also derived from a nested `[env.<name>.*]`), or null when no env declared. */
|
|
3
3
|
export declare function firstWranglerEnv(toml: string): string | null;
|
|
4
|
-
/** True if the toml declares `[env.<env>]
|
|
4
|
+
/** True if the toml declares `[env.<env>]` (or any `[env.<env>.*]` sub-table). */
|
|
5
5
|
export declare function hasEnvBlock(toml: string, env: string): boolean;
|
|
6
6
|
/** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
|
|
7
7
|
export declare function getVar(toml: string, env: string, name: string): string | null;
|
|
8
|
-
/** Set a `[env.<env>.vars]` value, preserving any trailing comment. Throws if the key
|
|
8
|
+
/** Set a `[env.<env>.vars]` value (JSON-encoded), preserving any trailing comment. Throws if the key is absent (scaffold first). */
|
|
9
9
|
export declare function setVar(toml: string, env: string, name: string, value: string): string;
|
|
10
10
|
/** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
|
|
11
11
|
export declare function getKvId(toml: string, env: string, binding: string): string | null;
|
|
12
|
-
/** Rewrite the `id = ...` line of the
|
|
12
|
+
/** Rewrite the `id = ...` line (value + trailing comment) of the kv_namespaces table whose binding matches. Throws if absent. */
|
|
13
13
|
export declare function setKvId(toml: string, env: string, binding: string, id: string, title: string): string;
|
|
14
14
|
/**
|
|
15
|
-
* Append a copy of
|
|
16
|
-
*
|
|
17
|
-
* toml. Throws if `<from>` is absent or `<to>` already exists (blank/fill instead).
|
|
15
|
+
* Append a copy of every `[env.<from>...]` table rewritten for `<to>` (headers
|
|
16
|
+
* only — values copied verbatim). Throws if `<from>` is absent or `<to>` exists.
|
|
18
17
|
*/
|
|
19
18
|
export declare function cloneEnvBlock(toml: string, from: string, to: string): string;
|
|
20
|
-
/** Blank
|
|
19
|
+
/** Blank the named `[env.<env>.vars]` values to `""` (preserving comments), skipping absent keys. */
|
|
21
20
|
export declare function blankEnvVars(toml: string, env: string, keys: string[]): string;
|
|
22
|
-
/** Blank every `id
|
|
21
|
+
/** Blank every kv_namespaces `id` under the env to `""` (drops trailing comment; binding/name survive). */
|
|
23
22
|
export declare function blankKvIds(toml: string, env: string): string;
|
|
23
|
+
/** The database_id written for `[[env.<env>.d1_databases]]` binding, or null if absent/empty. */
|
|
24
|
+
export declare function getD1Id(toml: string, env: string, binding: string): string | null;
|
|
25
|
+
/** The `database_name` for `[[env.<env>.d1_databases]]` binding, or null. Source of truth for which DB to create/reuse. */
|
|
26
|
+
export declare function getD1Name(toml: string, env: string, binding: string): string | null;
|
|
27
|
+
/** Rewrite the `database_id = ...` line (value + trailing comment) of the d1 table whose binding matches; the name line is untouched. Throws if absent. */
|
|
28
|
+
export declare function setD1Id(toml: string, env: string, binding: string, id: string, name: string): string;
|
|
29
|
+
/** Blank every d1 `database_id` under the env to `""` (drops trailing comment; binding/name survive). */
|
|
30
|
+
export declare function blankD1Ids(toml: string, env: string): string;
|
|
24
31
|
/** Parse-guard: throws if `toml` no longer parses, so a bad edit can't be written to disk. */
|
|
25
32
|
export declare function assertToml(toml: string): void;
|
|
26
33
|
/** Persist a toml string after confirming it still parses. */
|
|
@@ -28,6 +35,8 @@ export declare function saveToml(path: string, toml: string): void;
|
|
|
28
35
|
export interface Manifest {
|
|
29
36
|
/** KV binding names declared under `[[env.<env>.kv_namespaces]]` (resources to provision). */
|
|
30
37
|
kvBindings: string[];
|
|
38
|
+
/** D1 binding names declared under `[[env.<env>.d1_databases]]` (resources to provision). */
|
|
39
|
+
d1Bindings: string[];
|
|
31
40
|
/** Public var names declared in `[env.<env>.vars]` (committed config). */
|
|
32
41
|
vars: string[];
|
|
33
42
|
/** Secret names declared in `.dev.vars.example` (values live on the worker, never in the repo). */
|
|
@@ -61,6 +70,18 @@ export declare function listNamespaces(cwd: string): Promise<KvNamespace[]>;
|
|
|
61
70
|
* so we re-list rather than scrape. Returns the id, or null on failure.
|
|
62
71
|
*/
|
|
63
72
|
export declare function ensureNamespace(logical: string, existing: KvNamespace[], cwd: string): Promise<string | null>;
|
|
73
|
+
export interface D1Database {
|
|
74
|
+
uuid: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}
|
|
77
|
+
/** Live D1 databases on the account (`d1 list --json`); `[]` when offline/unauthenticated. */
|
|
78
|
+
export declare function listD1Databases(cwd: string): Promise<D1Database[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Reuse the D1 database whose name matches, else create it. The uuid comes from
|
|
81
|
+
* the structured `list` JSON (create prints freeform text), so we re-list rather
|
|
82
|
+
* than scrape. Returns the uuid, or null on failure.
|
|
83
|
+
*/
|
|
84
|
+
export declare function ensureD1Database(name: string, existing: D1Database[], cwd: string): Promise<string | null>;
|
|
64
85
|
/** Current secret names on the worker for `env`; `[]` when none/unreachable (Cloudflare allows setting pre-deploy). */
|
|
65
86
|
export declare function listSecretNames(env: string, cwd: string): Promise<string[]>;
|
|
66
87
|
/** `wrangler secret put <name> --env <env>` piping `value` on stdin (never a CLI arg). Returns success. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepare-env.d.ts","sourceRoot":"","sources":["../../src/wrangler/prepare-env.ts"],"names":[],"mappings":"AAyBA,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEjE;
|
|
1
|
+
{"version":3,"file":"prepare-env.d.ts","sourceRoot":"","sources":["../../src/wrangler/prepare-env.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,23 +19,68 @@
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { $ } from 'bun';
|
|
22
|
-
import {
|
|
23
|
-
// ── runtime narrowing (wrangler JSON
|
|
22
|
+
import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
23
|
+
// ── runtime narrowing (wrangler JSON is external data — no casts) ─────────────
|
|
24
24
|
export function isRecord(v) {
|
|
25
25
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// ── wrangler.toml editors (CST: read via getStaticTOMLValue, write via
|
|
28
|
+
// node-range splice — comment/format-preserving by construction, no regex) ─────
|
|
29
|
+
/** All top-level tables (`[env.x]`, `[env.x.vars]`, `[[env.x.d1_databases]]`, …) — each carries `resolvedKey` (incl. array index). */
|
|
30
|
+
function tables(toml) {
|
|
31
|
+
return parseTOML(toml).body[0].body.filter((n) => n.type === 'TOMLTable');
|
|
29
32
|
}
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
/** Whole doc as a JS object (comments/format dropped — reads only). */
|
|
34
|
+
function tomlValue(toml) {
|
|
35
|
+
const value = getStaticTOMLValue(parseTOML(toml));
|
|
36
|
+
return isRecord(value) ? value : {};
|
|
37
|
+
}
|
|
38
|
+
function envRecord(toml, env) {
|
|
39
|
+
const envs = tomlValue(toml).env;
|
|
40
|
+
const block = isRecord(envs) ? envs[env] : undefined;
|
|
41
|
+
return isRecord(block) ? block : null;
|
|
42
|
+
}
|
|
43
|
+
function sameKey(a, b) {
|
|
44
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
45
|
+
}
|
|
46
|
+
/** The `key = value` node named `key` in a table (single-segment keys; matched by source text). */
|
|
47
|
+
function kvIn(toml, table, key) {
|
|
48
|
+
for (const kv of table.body) {
|
|
49
|
+
if (toml.slice(kv.key.range[0], kv.key.range[1]).trim() === key)
|
|
50
|
+
return kv;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** The `<field>` KV of the `[[env.<env>.<arrayKey>]]` table whose `binding` matches, or null. */
|
|
55
|
+
function arrayTableKv(toml, env, arrayKey, binding, field) {
|
|
56
|
+
const block = envRecord(toml, env);
|
|
57
|
+
const rows = isRecord(block) && Array.isArray(block[arrayKey]) ? block[arrayKey] : [];
|
|
58
|
+
const index = rows.findIndex((r) => isRecord(r) && r.binding === binding);
|
|
59
|
+
if (index < 0)
|
|
60
|
+
return null;
|
|
61
|
+
const table = tables(toml).find((t) => t.kind === 'array' && sameKey(t.resolvedKey, ['env', env, arrayKey, index]));
|
|
62
|
+
return table ? kvIn(toml, table, field) : null;
|
|
63
|
+
}
|
|
64
|
+
function splice(toml, start, end, text) {
|
|
65
|
+
return toml.slice(0, start) + text + toml.slice(end);
|
|
66
|
+
}
|
|
67
|
+
/** End of the physical line containing `offset` (exclusive of the newline). */
|
|
68
|
+
function endOfLine(toml, offset) {
|
|
69
|
+
const nl = toml.indexOf('\n', offset);
|
|
70
|
+
return nl === -1 ? toml.length : nl;
|
|
71
|
+
}
|
|
72
|
+
/** First `[env.<name>]` (source order; also derived from a nested `[env.<name>.*]`), or null when no env declared. */
|
|
32
73
|
export function firstWranglerEnv(toml) {
|
|
33
|
-
const
|
|
34
|
-
|
|
74
|
+
const envs = tomlValue(toml).env;
|
|
75
|
+
if (!isRecord(envs))
|
|
76
|
+
return null;
|
|
77
|
+
const [first] = Object.keys(envs);
|
|
78
|
+
return first ?? null;
|
|
35
79
|
}
|
|
36
|
-
/** True if the toml declares `[env.<env>]
|
|
80
|
+
/** True if the toml declares `[env.<env>]` (or any `[env.<env>.*]` sub-table). */
|
|
37
81
|
export function hasEnvBlock(toml, env) {
|
|
38
|
-
|
|
82
|
+
const envs = tomlValue(toml).env;
|
|
83
|
+
return isRecord(envs) && env in envs;
|
|
39
84
|
}
|
|
40
85
|
/** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
|
|
41
86
|
export function getVar(toml, env, name) {
|
|
@@ -44,95 +89,118 @@ export function getVar(toml, env, name) {
|
|
|
44
89
|
const value = vars[name];
|
|
45
90
|
return typeof value === 'string' && value ? value : null;
|
|
46
91
|
}
|
|
47
|
-
/** Set a `[env.<env>.vars]` value, preserving any trailing comment. Throws if the key
|
|
92
|
+
/** Set a `[env.<env>.vars]` value (JSON-encoded), preserving any trailing comment. Throws if the key is absent (scaffold first). */
|
|
48
93
|
export function setVar(toml, env, name, value) {
|
|
49
|
-
const
|
|
50
|
-
|
|
94
|
+
const table = tables(toml).find((t) => sameKey(t.resolvedKey, ['env', env, 'vars']));
|
|
95
|
+
const kv = table ? kvIn(toml, table, name) : null;
|
|
96
|
+
if (!kv)
|
|
51
97
|
throw new Error(`vars key "${name}" not found under [env.${env}.vars]`);
|
|
52
|
-
|
|
53
|
-
return toml.replace(re, (_m, prefix) => `${prefix}${JSON.stringify(value)}`);
|
|
98
|
+
return splice(toml, kv.value.range[0], kv.value.range[1], JSON.stringify(value));
|
|
54
99
|
}
|
|
55
100
|
/** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
|
|
56
101
|
export function getKvId(toml, env, binding) {
|
|
57
102
|
const block = envRecord(toml, env);
|
|
58
103
|
const rows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
|
|
59
104
|
for (const row of rows) {
|
|
60
|
-
if (isRecord(row) && row.binding === binding && typeof row.id === 'string')
|
|
105
|
+
if (isRecord(row) && row.binding === binding && typeof row.id === 'string')
|
|
61
106
|
return row.id || null;
|
|
62
|
-
}
|
|
63
107
|
}
|
|
64
108
|
return null;
|
|
65
109
|
}
|
|
66
|
-
/** Rewrite the `id = ...` line of the
|
|
110
|
+
/** Rewrite the `id = ...` line (value + trailing comment) of the kv_namespaces table whose binding matches. Throws if absent. */
|
|
67
111
|
export function setKvId(toml, env, binding, id, title) {
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
112
|
+
const kv = arrayTableKv(toml, env, 'kv_namespaces', binding, 'id');
|
|
113
|
+
if (!kv)
|
|
70
114
|
throw new Error(`kv_namespaces binding "${binding}" not found under [env.${env}]`);
|
|
71
|
-
}
|
|
72
|
-
return toml.replace(re, `$1"${id}" # ${title}`);
|
|
115
|
+
return splice(toml, kv.value.range[0], endOfLine(toml, kv.value.range[1]), `${JSON.stringify(id)} # ${title}`);
|
|
73
116
|
}
|
|
74
117
|
/**
|
|
75
|
-
* Append a copy of
|
|
76
|
-
*
|
|
77
|
-
* toml. Throws if `<from>` is absent or `<to>` already exists (blank/fill instead).
|
|
118
|
+
* Append a copy of every `[env.<from>...]` table rewritten for `<to>` (headers
|
|
119
|
+
* only — values copied verbatim). Throws if `<from>` is absent or `<to>` exists.
|
|
78
120
|
*/
|
|
79
121
|
export function cloneEnvBlock(toml, from, to) {
|
|
80
122
|
if (!hasEnvBlock(toml, from))
|
|
81
123
|
throw new Error(`source [env.${from}] not found`);
|
|
82
124
|
if (hasEnvBlock(toml, to))
|
|
83
125
|
throw new Error(`target [env.${to}] already exists — blank/fill it instead`);
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
out.push(line);
|
|
93
|
-
}
|
|
94
|
-
else if (capturing && isAnyEnvHeader(line) && !isFromHeader(line)) {
|
|
95
|
-
capturing = false; // a different env began — stop
|
|
96
|
-
}
|
|
97
|
-
else if (capturing) {
|
|
98
|
-
out.push(line);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const cloned = out
|
|
102
|
-
.map((l) => l.replace(new RegExp(`(\\[+env\\.)${escapeRe(from)}(\\.|\\])`, 'g'), `$1${to}$2`))
|
|
103
|
-
.join('\n');
|
|
104
|
-
return `${toml.replace(/\s*$/, '')}\n\n${cloned}\n`;
|
|
126
|
+
const cloned = tables(toml)
|
|
127
|
+
.filter((t) => t.resolvedKey[0] === 'env' && t.resolvedKey[1] === from)
|
|
128
|
+
.map((t) => {
|
|
129
|
+
const seg = t.key.keys[1].range; // the `<from>` segment of this table's header
|
|
130
|
+
const text = toml.slice(t.range[0], t.range[1]);
|
|
131
|
+
return text.slice(0, seg[0] - t.range[0]) + to + text.slice(seg[1] - t.range[0]);
|
|
132
|
+
});
|
|
133
|
+
return `${toml.replace(/\s*$/, '')}\n\n${cloned.join('\n\n')}\n`;
|
|
105
134
|
}
|
|
106
|
-
/** Blank
|
|
135
|
+
/** Blank the named `[env.<env>.vars]` values to `""` (preserving comments), skipping absent keys. */
|
|
107
136
|
export function blankEnvVars(toml, env, keys) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return
|
|
137
|
+
const table = tables(toml).find((t) => sameKey(t.resolvedKey, ['env', env, 'vars']));
|
|
138
|
+
if (!table)
|
|
139
|
+
return toml;
|
|
140
|
+
const edits = keys
|
|
141
|
+
.map((key) => kvIn(toml, table, key))
|
|
142
|
+
.flatMap((kv) => (kv ? [{ start: kv.value.range[0], end: kv.value.range[1] }] : []));
|
|
143
|
+
return applyBlanks(toml, edits);
|
|
115
144
|
}
|
|
116
|
-
/** Blank every `id
|
|
145
|
+
/** Blank every kv_namespaces `id` under the env to `""` (drops trailing comment; binding/name survive). */
|
|
117
146
|
export function blankKvIds(toml, env) {
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
return blankArrayField(toml, env, 'kv_namespaces', 'id');
|
|
148
|
+
}
|
|
149
|
+
/** The database_id written for `[[env.<env>.d1_databases]]` binding, or null if absent/empty. */
|
|
150
|
+
export function getD1Id(toml, env, binding) {
|
|
151
|
+
const block = envRecord(toml, env);
|
|
152
|
+
const rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
|
|
153
|
+
for (const row of rows) {
|
|
154
|
+
if (isRecord(row) && row.binding === binding && typeof row.database_id === 'string')
|
|
155
|
+
return row.database_id || null;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/** The `database_name` for `[[env.<env>.d1_databases]]` binding, or null. Source of truth for which DB to create/reuse. */
|
|
160
|
+
export function getD1Name(toml, env, binding) {
|
|
161
|
+
const block = envRecord(toml, env);
|
|
162
|
+
const rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
if (isRecord(row) && row.binding === binding && typeof row.database_name === 'string')
|
|
165
|
+
return row.database_name || null;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
/** Rewrite the `database_id = ...` line (value + trailing comment) of the d1 table whose binding matches; the name line is untouched. Throws if absent. */
|
|
170
|
+
export function setD1Id(toml, env, binding, id, name) {
|
|
171
|
+
const kv = arrayTableKv(toml, env, 'd1_databases', binding, 'database_id');
|
|
172
|
+
if (!kv)
|
|
173
|
+
throw new Error(`d1_databases binding "${binding}" not found under [env.${env}]`);
|
|
174
|
+
return splice(toml, kv.value.range[0], endOfLine(toml, kv.value.range[1]), `${JSON.stringify(id)} # ${name}`);
|
|
175
|
+
}
|
|
176
|
+
/** Blank every d1 `database_id` under the env to `""` (drops trailing comment; binding/name survive). */
|
|
177
|
+
export function blankD1Ids(toml, env) {
|
|
178
|
+
return blankArrayField(toml, env, 'd1_databases', 'database_id');
|
|
179
|
+
}
|
|
180
|
+
/** Blank one field across every `[[env.<env>.<arrayKey>]]` table (value..EOL -> `""`). */
|
|
181
|
+
function blankArrayField(toml, env, arrayKey, field) {
|
|
182
|
+
const edits = tables(toml)
|
|
183
|
+
.filter((t) => t.kind === 'array' && t.resolvedKey[0] === 'env' && t.resolvedKey[1] === env && t.resolvedKey[2] === arrayKey)
|
|
184
|
+
.map((t) => kvIn(toml, t, field))
|
|
185
|
+
.flatMap((kv) => (kv ? [{ start: kv.value.range[0], end: endOfLine(toml, kv.value.range[1]) }] : []));
|
|
186
|
+
return applyBlanks(toml, edits);
|
|
187
|
+
}
|
|
188
|
+
/** Apply `""` at each range, descending so earlier offsets stay valid. */
|
|
189
|
+
function applyBlanks(toml, edits) {
|
|
190
|
+
let out = toml;
|
|
191
|
+
for (const e of [...edits].sort((a, b) => b.start - a.start))
|
|
192
|
+
out = splice(out, e.start, e.end, '""');
|
|
193
|
+
return out;
|
|
120
194
|
}
|
|
121
195
|
/** Parse-guard: throws if `toml` no longer parses, so a bad edit can't be written to disk. */
|
|
122
196
|
export function assertToml(toml) {
|
|
123
|
-
|
|
197
|
+
parseTOML(toml);
|
|
124
198
|
}
|
|
125
199
|
/** Persist a toml string after confirming it still parses. */
|
|
126
200
|
export function saveToml(path, toml) {
|
|
127
201
|
assertToml(toml);
|
|
128
202
|
writeFileSync(path, toml);
|
|
129
203
|
}
|
|
130
|
-
function envRecord(toml, env) {
|
|
131
|
-
const parsed = parse(toml);
|
|
132
|
-
const envs = isRecord(parsed) && isRecord(parsed.env) ? parsed.env : {};
|
|
133
|
-
const block = envs[env];
|
|
134
|
-
return isRecord(block) ? block : null;
|
|
135
|
-
}
|
|
136
204
|
/** Secret NAMES from a `.dev.vars.example` file (KEY=... / KEY="..."), ignoring comments/blanks. */
|
|
137
205
|
export function parseDevVarsExample(text) {
|
|
138
206
|
const names = [];
|
|
@@ -158,9 +226,11 @@ export function readManifest(root, env) {
|
|
|
158
226
|
const vars = Object.keys(varsRec);
|
|
159
227
|
const kvRows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
|
|
160
228
|
const kvBindings = kvRows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
|
|
229
|
+
const d1Rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
|
|
230
|
+
const d1Bindings = d1Rows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
|
|
161
231
|
const examplePath = join(root, '.dev.vars.example');
|
|
162
232
|
const secrets = existsSync(examplePath) ? parseDevVarsExample(readFileSync(examplePath, 'utf8')) : [];
|
|
163
|
-
return { kvBindings, vars, secrets };
|
|
233
|
+
return { kvBindings, d1Bindings, vars, secrets };
|
|
164
234
|
}
|
|
165
235
|
// ── PEM (a standard, reusable) ───────────────────────────────────────────────
|
|
166
236
|
/** True if `value` looks like a PEM block (any `-----BEGIN ...-----`). */
|
|
@@ -232,6 +302,46 @@ export async function ensureNamespace(logical, existing, cwd) {
|
|
|
232
302
|
async function reList(logical, cwd) {
|
|
233
303
|
return (await listNamespaces(cwd)).find((n) => n.title === logical)?.id ?? null;
|
|
234
304
|
}
|
|
305
|
+
function asD1Databases(json) {
|
|
306
|
+
if (!Array.isArray(json))
|
|
307
|
+
return [];
|
|
308
|
+
const out = [];
|
|
309
|
+
for (const row of json) {
|
|
310
|
+
if (isRecord(row) && typeof row.uuid === 'string' && typeof row.name === 'string') {
|
|
311
|
+
out.push({ uuid: row.uuid, name: row.name });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
/** Live D1 databases on the account (`d1 list --json`); `[]` when offline/unauthenticated. */
|
|
317
|
+
export async function listD1Databases(cwd) {
|
|
318
|
+
try {
|
|
319
|
+
return asD1Databases(await $ `bunx wrangler d1 list --json`.cwd(cwd).quiet().json());
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Reuse the D1 database whose name matches, else create it. The uuid comes from
|
|
327
|
+
* the structured `list` JSON (create prints freeform text), so we re-list rather
|
|
328
|
+
* than scrape. Returns the uuid, or null on failure.
|
|
329
|
+
*/
|
|
330
|
+
export async function ensureD1Database(name, existing, cwd) {
|
|
331
|
+
const match = existing.find((d) => d.name === name);
|
|
332
|
+
if (match)
|
|
333
|
+
return match.uuid;
|
|
334
|
+
try {
|
|
335
|
+
await $ `bunx wrangler d1 create ${name}`.cwd(cwd).quiet();
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return errText(err).includes('already') ? reListD1(name, cwd) : null;
|
|
339
|
+
}
|
|
340
|
+
return reListD1(name, cwd);
|
|
341
|
+
}
|
|
342
|
+
async function reListD1(name, cwd) {
|
|
343
|
+
return (await listD1Databases(cwd)).find((d) => d.name === name)?.uuid ?? null;
|
|
344
|
+
}
|
|
235
345
|
/** Current secret names on the worker for `env`; `[]` when none/unreachable (Cloudflare allows setting pre-deploy). */
|
|
236
346
|
export async function listSecretNames(env, cwd) {
|
|
237
347
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"publint": "^0.3.18",
|
|
59
59
|
"semver": "^7.7.4",
|
|
60
60
|
"sherif": "^1.11.1",
|
|
61
|
-
"
|
|
61
|
+
"toml-eslint-parser": "^1.0.3",
|
|
62
62
|
"tslib": "^2.8.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -4,10 +4,13 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import {
|
|
6
6
|
assertToml,
|
|
7
|
+
blankD1Ids,
|
|
7
8
|
blankEnvVars,
|
|
8
9
|
blankKvIds,
|
|
9
10
|
cloneEnvBlock,
|
|
10
11
|
firstWranglerEnv,
|
|
12
|
+
getD1Id,
|
|
13
|
+
getD1Name,
|
|
11
14
|
getKvId,
|
|
12
15
|
getVar,
|
|
13
16
|
hasEnvBlock,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
parseDevVarsExample,
|
|
17
20
|
readManifest,
|
|
18
21
|
readPemFile,
|
|
22
|
+
setD1Id,
|
|
19
23
|
setKvId,
|
|
20
24
|
setVar,
|
|
21
25
|
} from './prepare-env.js';
|
|
@@ -161,6 +165,81 @@ describe('blankEnvVars / blankKvIds', () => {
|
|
|
161
165
|
});
|
|
162
166
|
});
|
|
163
167
|
|
|
168
|
+
// D1 shell-outs (listD1Databases / ensureD1Database) invoke `bunx wrangler d1`
|
|
169
|
+
// against a live account and are integration-only — deliberately not exercised.
|
|
170
|
+
|
|
171
|
+
describe('getD1Id / getD1Name', () => {
|
|
172
|
+
const toml =
|
|
173
|
+
'[env.staging]\n\n' +
|
|
174
|
+
'[[env.staging.d1_databases]]\nbinding = "DB"\ndatabase_id = "abc123"\ndatabase_name = "app-staging-db"\n\n' +
|
|
175
|
+
'[env.production]\n\n' +
|
|
176
|
+
'[[env.production.d1_databases]]\nbinding = "DB"\ndatabase_id = ""\ndatabase_name = "app-db"\n';
|
|
177
|
+
|
|
178
|
+
it('reads the id for a matching binding and null for empty id, absent binding, and absent env', () => {
|
|
179
|
+
expect(getD1Id(toml, 'staging', 'DB')).toBe('abc123');
|
|
180
|
+
expect(getD1Id(toml, 'production', 'DB')).toBeNull(); // production id is ""
|
|
181
|
+
expect(getD1Id(toml, 'staging', 'MISSING')).toBeNull(); // absent binding
|
|
182
|
+
expect(getD1Id(toml, 'qa', 'DB')).toBeNull(); // absent env
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('reads the database_name for a matching binding and null when the binding or env is absent', () => {
|
|
186
|
+
expect(getD1Name(toml, 'staging', 'DB')).toBe('app-staging-db');
|
|
187
|
+
expect(getD1Name(toml, 'production', 'DB')).toBe('app-db'); // present even though its id is blank
|
|
188
|
+
expect(getD1Name(toml, 'staging', 'MISSING')).toBeNull();
|
|
189
|
+
expect(getD1Name(toml, 'qa', 'DB')).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('setD1Id', () => {
|
|
194
|
+
const toml =
|
|
195
|
+
'[[env.staging.d1_databases]]\nbinding = "DB"\ndatabase_id = ""\ndatabase_name = "app-staging-db"\n\n' +
|
|
196
|
+
'[[env.staging.d1_databases]]\nbinding = "ANALYTICS"\ndatabase_id = ""\ndatabase_name = "app-staging-analytics"\n\n' +
|
|
197
|
+
'[[env.production.d1_databases]]\nbinding = "DB"\ndatabase_id = "prod-existing"\ndatabase_name = "app-db"\n';
|
|
198
|
+
|
|
199
|
+
it('writes "<id>" # <name> against the matching env+binding, leaving siblings and other envs intact', () => {
|
|
200
|
+
const out = setD1Id(toml, 'staging', 'DB', 'new-staging-id', 'app-staging-db');
|
|
201
|
+
expect(out).toContain('"new-staging-id" # app-staging-db');
|
|
202
|
+
expect(getD1Id(out, 'staging', 'DB')).toBe('new-staging-id'); // round-trip
|
|
203
|
+
expect(getD1Id(out, 'staging', 'ANALYTICS')).toBeNull(); // sibling binding untouched
|
|
204
|
+
expect(getD1Id(out, 'production', 'DB')).toBe('prod-existing'); // other env untouched
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('tolerates database_name preceding database_id and preserves the name', () => {
|
|
208
|
+
const nameFirst =
|
|
209
|
+
'[[env.staging.d1_databases]]\nbinding = "DB"\ndatabase_name = "app-staging-db"\ndatabase_id = "old"\n';
|
|
210
|
+
const out = setD1Id(nameFirst, 'staging', 'DB', 'fresh', 'app-staging-db');
|
|
211
|
+
expect(getD1Id(out, 'staging', 'DB')).toBe('fresh');
|
|
212
|
+
expect(getD1Name(out, 'staging', 'DB')).toBe('app-staging-db'); // name line not clobbered
|
|
213
|
+
expect(() => assertToml(out)).not.toThrow();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('throws when the binding or the env is absent', () => {
|
|
217
|
+
expect(() => setD1Id(toml, 'staging', 'MISSING', 'x', 'n')).toThrow();
|
|
218
|
+
expect(() => setD1Id(toml, 'qa', 'DB', 'x', 'n')).toThrow();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('blankD1Ids', () => {
|
|
223
|
+
const toml =
|
|
224
|
+
'[[env.staging.d1_databases]]\nbinding = "DB"\ndatabase_id = "staging-db-id"\ndatabase_name = "app-staging-db"\n\n' +
|
|
225
|
+
'[[env.staging.d1_databases]]\nbinding = "ANALYTICS"\ndatabase_id = "staging-an-id"\ndatabase_name = "app-staging-analytics"\n\n' +
|
|
226
|
+
'[[env.production.d1_databases]]\nbinding = "DB"\ndatabase_id = "prod-db-id"\ndatabase_name = "app-db"\n';
|
|
227
|
+
|
|
228
|
+
it('blanks every d1 id under the env while names, bindings, and the other env survive', () => {
|
|
229
|
+
const out = blankD1Ids(toml, 'staging');
|
|
230
|
+
expect(getD1Id(out, 'staging', 'DB')).toBeNull();
|
|
231
|
+
expect(getD1Id(out, 'staging', 'ANALYTICS')).toBeNull();
|
|
232
|
+
// names survive the blanking
|
|
233
|
+
expect(getD1Name(out, 'staging', 'DB')).toBe('app-staging-db');
|
|
234
|
+
expect(getD1Name(out, 'staging', 'ANALYTICS')).toBe('app-staging-analytics');
|
|
235
|
+
// other env's id is untouched
|
|
236
|
+
expect(getD1Id(out, 'production', 'DB')).toBe('prod-db-id');
|
|
237
|
+
// bindings survived: setD1Id still resolves them and re-fills an id
|
|
238
|
+
const refilled = setD1Id(out, 'staging', 'DB', 're-provisioned', 'app-staging-db');
|
|
239
|
+
expect(getD1Id(refilled, 'staging', 'DB')).toBe('re-provisioned');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
164
243
|
describe('assertToml', () => {
|
|
165
244
|
it('returns for valid toml', () => {
|
|
166
245
|
expect(() => assertToml('name = "svc"\n[env.production]\n')).not.toThrow();
|
|
@@ -192,6 +271,7 @@ describe('readManifest', () => {
|
|
|
192
271
|
await writeFile(join(root, '.dev.vars.example'), '# secrets\nAPI_KEY=\nJWT_SECRET=""\n');
|
|
193
272
|
expect(readManifest(root, 'staging')).toEqual({
|
|
194
273
|
kvBindings: ['CACHE', 'SESSIONS'],
|
|
274
|
+
d1Bindings: [],
|
|
195
275
|
vars: ['PUBLIC_URL', 'FEATURE_FLAG'],
|
|
196
276
|
secrets: ['API_KEY', 'JWT_SECRET'],
|
|
197
277
|
});
|
|
@@ -212,6 +292,27 @@ describe('readManifest', () => {
|
|
|
212
292
|
await rm(root, { recursive: true, force: true });
|
|
213
293
|
}
|
|
214
294
|
});
|
|
295
|
+
|
|
296
|
+
it('reports d1 bindings alongside kv bindings and vars without dropping the existing fields', async () => {
|
|
297
|
+
const withD1 =
|
|
298
|
+
'name = "svc"\n\n' +
|
|
299
|
+
'[env.staging.vars]\nPUBLIC_URL = "https://staging"\n\n' +
|
|
300
|
+
'[[env.staging.kv_namespaces]]\nbinding = "CACHE"\nid = "abc"\n\n' +
|
|
301
|
+
'[[env.staging.d1_databases]]\nbinding = "DB"\ndatabase_id = "id-1"\ndatabase_name = "app-staging-db"\n\n' +
|
|
302
|
+
'[[env.staging.d1_databases]]\nbinding = "ANALYTICS"\ndatabase_id = ""\ndatabase_name = "app-analytics-db"\n';
|
|
303
|
+
const root = await mkdtemp(join(tmpdir(), 'smoo-prepare-env-'));
|
|
304
|
+
try {
|
|
305
|
+
await writeFile(join(root, 'wrangler.toml'), withD1);
|
|
306
|
+
expect(readManifest(root, 'staging')).toEqual({
|
|
307
|
+
kvBindings: ['CACHE'],
|
|
308
|
+
d1Bindings: ['DB', 'ANALYTICS'],
|
|
309
|
+
vars: ['PUBLIC_URL'],
|
|
310
|
+
secrets: [],
|
|
311
|
+
});
|
|
312
|
+
} finally {
|
|
313
|
+
await rm(root, { recursive: true, force: true });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
215
316
|
});
|
|
216
317
|
|
|
217
318
|
describe('isPem / readPemFile / looksLikeFileSecret', () => {
|
|
@@ -19,29 +19,78 @@
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { $ } from 'bun';
|
|
22
|
-
import {
|
|
22
|
+
import { type AST, getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
|
|
23
23
|
|
|
24
|
-
// ── runtime narrowing (wrangler JSON
|
|
24
|
+
// ── runtime narrowing (wrangler JSON is external data — no casts) ─────────────
|
|
25
25
|
|
|
26
26
|
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
27
27
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// ── wrangler.toml editors (CST: read via getStaticTOMLValue, write via
|
|
31
|
+
// node-range splice — comment/format-preserving by construction, no regex) ─────
|
|
32
|
+
|
|
33
|
+
/** All top-level tables (`[env.x]`, `[env.x.vars]`, `[[env.x.d1_databases]]`, …) — each carries `resolvedKey` (incl. array index). */
|
|
34
|
+
function tables(toml: string): AST.TOMLTable[] {
|
|
35
|
+
return parseTOML(toml).body[0].body.filter((n): n is AST.TOMLTable => n.type === 'TOMLTable');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whole doc as a JS object (comments/format dropped — reads only). */
|
|
39
|
+
function tomlValue(toml: string): Record<string, unknown> {
|
|
40
|
+
const value = getStaticTOMLValue(parseTOML(toml));
|
|
41
|
+
return isRecord(value) ? value : {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function envRecord(toml: string, env: string): Record<string, unknown> | null {
|
|
45
|
+
const envs = tomlValue(toml).env;
|
|
46
|
+
const block = isRecord(envs) ? envs[env] : undefined;
|
|
47
|
+
return isRecord(block) ? block : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sameKey(a: (string | number)[], b: (string | number)[]): boolean {
|
|
51
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The `key = value` node named `key` in a table (single-segment keys; matched by source text). */
|
|
55
|
+
function kvIn(toml: string, table: AST.TOMLTable, key: string): AST.TOMLKeyValue | null {
|
|
56
|
+
for (const kv of table.body) {
|
|
57
|
+
if (toml.slice(kv.key.range[0], kv.key.range[1]).trim() === key) return kv;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The `<field>` KV of the `[[env.<env>.<arrayKey>]]` table whose `binding` matches, or null. */
|
|
63
|
+
function arrayTableKv(toml: string, env: string, arrayKey: string, binding: string, field: string): AST.TOMLKeyValue | null {
|
|
64
|
+
const block = envRecord(toml, env);
|
|
65
|
+
const rows = isRecord(block) && Array.isArray(block[arrayKey]) ? block[arrayKey] : [];
|
|
66
|
+
const index = rows.findIndex((r) => isRecord(r) && r.binding === binding);
|
|
67
|
+
if (index < 0) return null;
|
|
68
|
+
const table = tables(toml).find((t) => t.kind === 'array' && sameKey(t.resolvedKey, ['env', env, arrayKey, index]));
|
|
69
|
+
return table ? kvIn(toml, table, field) : null;
|
|
32
70
|
}
|
|
33
71
|
|
|
34
|
-
|
|
72
|
+
function splice(toml: string, start: number, end: number, text: string): string {
|
|
73
|
+
return toml.slice(0, start) + text + toml.slice(end);
|
|
74
|
+
}
|
|
35
75
|
|
|
36
|
-
/**
|
|
76
|
+
/** End of the physical line containing `offset` (exclusive of the newline). */
|
|
77
|
+
function endOfLine(toml: string, offset: number): number {
|
|
78
|
+
const nl = toml.indexOf('\n', offset);
|
|
79
|
+
return nl === -1 ? toml.length : nl;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** First `[env.<name>]` (source order; also derived from a nested `[env.<name>.*]`), or null when no env declared. */
|
|
37
83
|
export function firstWranglerEnv(toml: string): string | null {
|
|
38
|
-
const
|
|
39
|
-
|
|
84
|
+
const envs = tomlValue(toml).env;
|
|
85
|
+
if (!isRecord(envs)) return null;
|
|
86
|
+
const [first] = Object.keys(envs);
|
|
87
|
+
return first ?? null;
|
|
40
88
|
}
|
|
41
89
|
|
|
42
|
-
/** True if the toml declares `[env.<env>]
|
|
90
|
+
/** True if the toml declares `[env.<env>]` (or any `[env.<env>.*]` sub-table). */
|
|
43
91
|
export function hasEnvBlock(toml: string, env: string): boolean {
|
|
44
|
-
|
|
92
|
+
const envs = tomlValue(toml).env;
|
|
93
|
+
return isRecord(envs) && env in envs;
|
|
45
94
|
}
|
|
46
95
|
|
|
47
96
|
/** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
|
|
@@ -52,13 +101,12 @@ export function getVar(toml: string, env: string, name: string): string | null {
|
|
|
52
101
|
return typeof value === 'string' && value ? value : null;
|
|
53
102
|
}
|
|
54
103
|
|
|
55
|
-
/** Set a `[env.<env>.vars]` value, preserving any trailing comment. Throws if the key
|
|
104
|
+
/** Set a `[env.<env>.vars]` value (JSON-encoded), preserving any trailing comment. Throws if the key is absent (scaffold first). */
|
|
56
105
|
export function setVar(toml: string, env: string, name: string, value: string): string {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return toml.replace(re, (_m: string, prefix: string) => `${prefix}${JSON.stringify(value)}`);
|
|
106
|
+
const table = tables(toml).find((t) => sameKey(t.resolvedKey, ['env', env, 'vars']));
|
|
107
|
+
const kv = table ? kvIn(toml, table, name) : null;
|
|
108
|
+
if (!kv) throw new Error(`vars key "${name}" not found under [env.${env}.vars]`);
|
|
109
|
+
return splice(toml, kv.value.range[0], kv.value.range[1], JSON.stringify(value));
|
|
62
110
|
}
|
|
63
111
|
|
|
64
112
|
/** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
|
|
@@ -66,75 +114,101 @@ export function getKvId(toml: string, env: string, binding: string): string | nu
|
|
|
66
114
|
const block = envRecord(toml, env);
|
|
67
115
|
const rows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
|
|
68
116
|
for (const row of rows) {
|
|
69
|
-
if (isRecord(row) && row.binding === binding && typeof row.id === 'string')
|
|
70
|
-
return row.id || null;
|
|
71
|
-
}
|
|
117
|
+
if (isRecord(row) && row.binding === binding && typeof row.id === 'string') return row.id || null;
|
|
72
118
|
}
|
|
73
119
|
return null;
|
|
74
120
|
}
|
|
75
121
|
|
|
76
|
-
/** Rewrite the `id = ...` line of the
|
|
122
|
+
/** Rewrite the `id = ...` line (value + trailing comment) of the kv_namespaces table whose binding matches. Throws if absent. */
|
|
77
123
|
export function setKvId(toml: string, env: string, binding: string, id: string, title: string): string {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
);
|
|
81
|
-
if (!re.test(toml)) {
|
|
82
|
-
throw new Error(`kv_namespaces binding "${binding}" not found under [env.${env}]`);
|
|
83
|
-
}
|
|
84
|
-
return toml.replace(re, `$1"${id}" # ${title}`);
|
|
124
|
+
const kv = arrayTableKv(toml, env, 'kv_namespaces', binding, 'id');
|
|
125
|
+
if (!kv) throw new Error(`kv_namespaces binding "${binding}" not found under [env.${env}]`);
|
|
126
|
+
return splice(toml, kv.value.range[0], endOfLine(toml, kv.value.range[1]), `${JSON.stringify(id)} # ${title}`);
|
|
85
127
|
}
|
|
86
128
|
|
|
87
129
|
/**
|
|
88
|
-
* Append a copy of
|
|
89
|
-
*
|
|
90
|
-
* toml. Throws if `<from>` is absent or `<to>` already exists (blank/fill instead).
|
|
130
|
+
* Append a copy of every `[env.<from>...]` table rewritten for `<to>` (headers
|
|
131
|
+
* only — values copied verbatim). Throws if `<from>` is absent or `<to>` exists.
|
|
91
132
|
*/
|
|
92
133
|
export function cloneEnvBlock(toml: string, from: string, to: string): string {
|
|
93
134
|
if (!hasEnvBlock(toml, from)) throw new Error(`source [env.${from}] not found`);
|
|
94
135
|
if (hasEnvBlock(toml, to)) throw new Error(`target [env.${to}] already exists — blank/fill it instead`);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
out.push(line);
|
|
104
|
-
} else if (capturing && isAnyEnvHeader(line) && !isFromHeader(line)) {
|
|
105
|
-
capturing = false; // a different env began — stop
|
|
106
|
-
} else if (capturing) {
|
|
107
|
-
out.push(line);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const cloned = out
|
|
111
|
-
.map((l) => l.replace(new RegExp(`(\\[+env\\.)${escapeRe(from)}(\\.|\\])`, 'g'), `$1${to}$2`))
|
|
112
|
-
.join('\n');
|
|
113
|
-
return `${toml.replace(/\s*$/, '')}\n\n${cloned}\n`;
|
|
136
|
+
const cloned = tables(toml)
|
|
137
|
+
.filter((t) => t.resolvedKey[0] === 'env' && t.resolvedKey[1] === from)
|
|
138
|
+
.map((t) => {
|
|
139
|
+
const seg = t.key.keys[1].range; // the `<from>` segment of this table's header
|
|
140
|
+
const text = toml.slice(t.range[0], t.range[1]);
|
|
141
|
+
return text.slice(0, seg[0] - t.range[0]) + to + text.slice(seg[1] - t.range[0]);
|
|
142
|
+
});
|
|
143
|
+
return `${toml.replace(/\s*$/, '')}\n\n${cloned.join('\n\n')}\n`;
|
|
114
144
|
}
|
|
115
145
|
|
|
116
|
-
/** Blank
|
|
146
|
+
/** Blank the named `[env.<env>.vars]` values to `""` (preserving comments), skipping absent keys. */
|
|
117
147
|
export function blankEnvVars(toml: string, env: string, keys: string[]): string {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
148
|
+
const table = tables(toml).find((t) => sameKey(t.resolvedKey, ['env', env, 'vars']));
|
|
149
|
+
if (!table) return toml;
|
|
150
|
+
const edits = keys
|
|
151
|
+
.map((key) => kvIn(toml, table, key))
|
|
152
|
+
.flatMap((kv) => (kv ? [{ start: kv.value.range[0], end: kv.value.range[1] }] : []));
|
|
153
|
+
return applyBlanks(toml, edits);
|
|
124
154
|
}
|
|
125
155
|
|
|
126
|
-
/** Blank every `id
|
|
156
|
+
/** Blank every kv_namespaces `id` under the env to `""` (drops trailing comment; binding/name survive). */
|
|
127
157
|
export function blankKvIds(toml: string, env: string): string {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
return blankArrayField(toml, env, 'kv_namespaces', 'id');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The database_id written for `[[env.<env>.d1_databases]]` binding, or null if absent/empty. */
|
|
162
|
+
export function getD1Id(toml: string, env: string, binding: string): string | null {
|
|
163
|
+
const block = envRecord(toml, env);
|
|
164
|
+
const rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (isRecord(row) && row.binding === binding && typeof row.database_id === 'string') return row.database_id || null;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The `database_name` for `[[env.<env>.d1_databases]]` binding, or null. Source of truth for which DB to create/reuse. */
|
|
172
|
+
export function getD1Name(toml: string, env: string, binding: string): string | null {
|
|
173
|
+
const block = envRecord(toml, env);
|
|
174
|
+
const rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
if (isRecord(row) && row.binding === binding && typeof row.database_name === 'string') return row.database_name || null;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Rewrite the `database_id = ...` line (value + trailing comment) of the d1 table whose binding matches; the name line is untouched. Throws if absent. */
|
|
182
|
+
export function setD1Id(toml: string, env: string, binding: string, id: string, name: string): string {
|
|
183
|
+
const kv = arrayTableKv(toml, env, 'd1_databases', binding, 'database_id');
|
|
184
|
+
if (!kv) throw new Error(`d1_databases binding "${binding}" not found under [env.${env}]`);
|
|
185
|
+
return splice(toml, kv.value.range[0], endOfLine(toml, kv.value.range[1]), `${JSON.stringify(id)} # ${name}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Blank every d1 `database_id` under the env to `""` (drops trailing comment; binding/name survive). */
|
|
189
|
+
export function blankD1Ids(toml: string, env: string): string {
|
|
190
|
+
return blankArrayField(toml, env, 'd1_databases', 'database_id');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Blank one field across every `[[env.<env>.<arrayKey>]]` table (value..EOL -> `""`). */
|
|
194
|
+
function blankArrayField(toml: string, env: string, arrayKey: string, field: string): string {
|
|
195
|
+
const edits = tables(toml)
|
|
196
|
+
.filter((t) => t.kind === 'array' && t.resolvedKey[0] === 'env' && t.resolvedKey[1] === env && t.resolvedKey[2] === arrayKey)
|
|
197
|
+
.map((t) => kvIn(toml, t, field))
|
|
198
|
+
.flatMap((kv) => (kv ? [{ start: kv.value.range[0], end: endOfLine(toml, kv.value.range[1]) }] : []));
|
|
199
|
+
return applyBlanks(toml, edits);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Apply `""` at each range, descending so earlier offsets stay valid. */
|
|
203
|
+
function applyBlanks(toml: string, edits: { start: number; end: number }[]): string {
|
|
204
|
+
let out = toml;
|
|
205
|
+
for (const e of [...edits].sort((a, b) => b.start - a.start)) out = splice(out, e.start, e.end, '""');
|
|
206
|
+
return out;
|
|
133
207
|
}
|
|
134
208
|
|
|
135
209
|
/** Parse-guard: throws if `toml` no longer parses, so a bad edit can't be written to disk. */
|
|
136
210
|
export function assertToml(toml: string): void {
|
|
137
|
-
|
|
211
|
+
parseTOML(toml);
|
|
138
212
|
}
|
|
139
213
|
|
|
140
214
|
/** Persist a toml string after confirming it still parses. */
|
|
@@ -143,18 +217,13 @@ export function saveToml(path: string, toml: string): void {
|
|
|
143
217
|
writeFileSync(path, toml);
|
|
144
218
|
}
|
|
145
219
|
|
|
146
|
-
function envRecord(toml: string, env: string): Record<string, unknown> | null {
|
|
147
|
-
const parsed = parse(toml);
|
|
148
|
-
const envs = isRecord(parsed) && isRecord(parsed.env) ? parsed.env : {};
|
|
149
|
-
const block = envs[env];
|
|
150
|
-
return isRecord(block) ? block : null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
220
|
// ── manifest: what the worker needs, split into vars vs secrets ──────────────
|
|
154
221
|
|
|
155
222
|
export interface Manifest {
|
|
156
223
|
/** KV binding names declared under `[[env.<env>.kv_namespaces]]` (resources to provision). */
|
|
157
224
|
kvBindings: string[];
|
|
225
|
+
/** D1 binding names declared under `[[env.<env>.d1_databases]]` (resources to provision). */
|
|
226
|
+
d1Bindings: string[];
|
|
158
227
|
/** Public var names declared in `[env.<env>.vars]` (committed config). */
|
|
159
228
|
vars: string[];
|
|
160
229
|
/** Secret names declared in `.dev.vars.example` (values live on the worker, never in the repo). */
|
|
@@ -185,9 +254,11 @@ export function readManifest(root: string, env: string): Manifest {
|
|
|
185
254
|
const vars = Object.keys(varsRec);
|
|
186
255
|
const kvRows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
|
|
187
256
|
const kvBindings = kvRows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
|
|
257
|
+
const d1Rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
|
|
258
|
+
const d1Bindings = d1Rows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
|
|
188
259
|
const examplePath = join(root, '.dev.vars.example');
|
|
189
260
|
const secrets = existsSync(examplePath) ? parseDevVarsExample(readFileSync(examplePath, 'utf8')) : [];
|
|
190
|
-
return { kvBindings, vars, secrets };
|
|
261
|
+
return { kvBindings, d1Bindings, vars, secrets };
|
|
191
262
|
}
|
|
192
263
|
|
|
193
264
|
// ── PEM (a standard, reusable) ───────────────────────────────────────────────
|
|
@@ -270,6 +341,51 @@ async function reList(logical: string, cwd: string): Promise<string | null> {
|
|
|
270
341
|
return (await listNamespaces(cwd)).find((n) => n.title === logical)?.id ?? null;
|
|
271
342
|
}
|
|
272
343
|
|
|
344
|
+
export interface D1Database {
|
|
345
|
+
uuid: string;
|
|
346
|
+
name: string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function asD1Databases(json: unknown): D1Database[] {
|
|
350
|
+
if (!Array.isArray(json)) return [];
|
|
351
|
+
const out: D1Database[] = [];
|
|
352
|
+
for (const row of json) {
|
|
353
|
+
if (isRecord(row) && typeof row.uuid === 'string' && typeof row.name === 'string') {
|
|
354
|
+
out.push({ uuid: row.uuid, name: row.name });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Live D1 databases on the account (`d1 list --json`); `[]` when offline/unauthenticated. */
|
|
361
|
+
export async function listD1Databases(cwd: string): Promise<D1Database[]> {
|
|
362
|
+
try {
|
|
363
|
+
return asD1Databases(await $`bunx wrangler d1 list --json`.cwd(cwd).quiet().json());
|
|
364
|
+
} catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Reuse the D1 database whose name matches, else create it. The uuid comes from
|
|
371
|
+
* the structured `list` JSON (create prints freeform text), so we re-list rather
|
|
372
|
+
* than scrape. Returns the uuid, or null on failure.
|
|
373
|
+
*/
|
|
374
|
+
export async function ensureD1Database(name: string, existing: D1Database[], cwd: string): Promise<string | null> {
|
|
375
|
+
const match = existing.find((d) => d.name === name);
|
|
376
|
+
if (match) return match.uuid;
|
|
377
|
+
try {
|
|
378
|
+
await $`bunx wrangler d1 create ${name}`.cwd(cwd).quiet();
|
|
379
|
+
} catch (err) {
|
|
380
|
+
return errText(err).includes('already') ? reListD1(name, cwd) : null;
|
|
381
|
+
}
|
|
382
|
+
return reListD1(name, cwd);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function reListD1(name: string, cwd: string): Promise<string | null> {
|
|
386
|
+
return (await listD1Databases(cwd)).find((d) => d.name === name)?.uuid ?? null;
|
|
387
|
+
}
|
|
388
|
+
|
|
273
389
|
/** Current secret names on the worker for `env`; `[]` when none/unreachable (Cloudflare allows setting pre-deploy). */
|
|
274
390
|
export async function listSecretNames(env: string, cwd: string): Promise<string[]> {
|
|
275
391
|
try {
|