@smoothbricks/cli 0.8.0 → 0.9.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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -0
- package/dist/wrangler/prepare-env.d.ts +68 -0
- package/dist/wrangler/prepare-env.d.ts.map +1 -0
- package/dist/wrangler/prepare-env.js +253 -0
- package/dist/wrangler/scaffold.d.ts +7 -0
- package/dist/wrangler/scaffold.d.ts.map +1 -0
- package/dist/wrangler/scaffold.js +228 -0
- package/package.json +9 -1
- package/src/cli.ts +10 -0
- package/src/release/__tests__/helpers/fixture-repo.ts +19 -1
- package/src/wrangler/prepare-env.test.ts +246 -0
- package/src/wrangler/prepare-env.ts +290 -0
- package/src/wrangler/scaffold.test.ts +200 -0
- package/src/wrangler/scaffold.ts +239 -0
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAOA,wBAAsB,MAAM,CAAC,IAAI,WAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAexE"}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { variants } from './generate/index.js';
|
|
|
3
3
|
import { cliPackageVersion } from './lib/cli-package.js';
|
|
4
4
|
import { findRepoRoot } from './lib/run.js';
|
|
5
5
|
import { resolvePrConflicts } from './pr/index.js';
|
|
6
|
+
import { scaffold } from './wrangler/scaffold.js';
|
|
6
7
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
7
8
|
const program = buildProgram();
|
|
8
9
|
try {
|
|
@@ -290,6 +291,14 @@ function buildProgram() {
|
|
|
290
291
|
process.exitCode = exitCode;
|
|
291
292
|
}
|
|
292
293
|
});
|
|
294
|
+
const wrangler = program.command('wrangler').description('Cloudflare wrangler project helpers');
|
|
295
|
+
wrangler
|
|
296
|
+
.command('scaffold <project>')
|
|
297
|
+
.description('Write a starter scripts/prepare-env.ts (manifest-driven) and wire its nx target')
|
|
298
|
+
.option('--force', 'overwrite an existing scripts/prepare-env.ts')
|
|
299
|
+
.action(async (project, options) => {
|
|
300
|
+
scaffold(await findRepoRoot(), project, { force: options.force });
|
|
301
|
+
});
|
|
293
302
|
return program;
|
|
294
303
|
}
|
|
295
304
|
function booleanOption(value) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
2
|
+
/** First `[env.<name>]` header, or `null` when the config declares no envs (top-level bindings only). */
|
|
3
|
+
export declare function firstWranglerEnv(toml: string): string | null;
|
|
4
|
+
/** True if the toml declares `[env.<env>]`. */
|
|
5
|
+
export declare function hasEnvBlock(toml: string, env: string): boolean;
|
|
6
|
+
/** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
|
|
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 isn't present (scaffold first). */
|
|
9
|
+
export declare function setVar(toml: string, env: string, name: string, value: string): string;
|
|
10
|
+
/** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
|
|
11
|
+
export declare function getKvId(toml: string, env: string, binding: string): string | null;
|
|
12
|
+
/** Rewrite the `id = ...` line of the `[[env.<env>.kv_namespaces]]` table whose binding matches. Throws if absent. */
|
|
13
|
+
export declare function setKvId(toml: string, env: string, binding: string, id: string, title: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Append a copy of the whole `[env.<from>]` … block-group (every `[env.<from>...]`
|
|
16
|
+
* table up to the next non-`from` env or EOF) rewritten for `<to>`. Returns the new
|
|
17
|
+
* toml. Throws if `<from>` is absent or `<to>` already exists (blank/fill instead).
|
|
18
|
+
*/
|
|
19
|
+
export declare function cloneEnvBlock(toml: string, from: string, to: string): string;
|
|
20
|
+
/** Blank every `NAME = "..."` under `[env.<env>.vars]` whose key is in `keys` (env-specific values to re-fill). */
|
|
21
|
+
export declare function blankEnvVars(toml: string, env: string, keys: string[]): string;
|
|
22
|
+
/** Blank every `id = ...` line under the env's `[[env.<env>.kv_namespaces]]` tables (per-env resource ids). */
|
|
23
|
+
export declare function blankKvIds(toml: string, env: string): string;
|
|
24
|
+
/** Parse-guard: throws if `toml` no longer parses, so a bad edit can't be written to disk. */
|
|
25
|
+
export declare function assertToml(toml: string): void;
|
|
26
|
+
/** Persist a toml string after confirming it still parses. */
|
|
27
|
+
export declare function saveToml(path: string, toml: string): void;
|
|
28
|
+
export interface Manifest {
|
|
29
|
+
/** KV binding names declared under `[[env.<env>.kv_namespaces]]` (resources to provision). */
|
|
30
|
+
kvBindings: string[];
|
|
31
|
+
/** Public var names declared in `[env.<env>.vars]` (committed config). */
|
|
32
|
+
vars: string[];
|
|
33
|
+
/** Secret names declared in `.dev.vars.example` (values live on the worker, never in the repo). */
|
|
34
|
+
secrets: string[];
|
|
35
|
+
}
|
|
36
|
+
/** Secret NAMES from a `.dev.vars.example` file (KEY=... / KEY="..."), ignoring comments/blanks. */
|
|
37
|
+
export declare function parseDevVarsExample(text: string): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Read the vars/secrets split for `env` from `<root>/wrangler.toml` (public var
|
|
40
|
+
* keys) + `<root>/.dev.vars.example` (secret names). This is the "what to prompt
|
|
41
|
+
* for" manifest a prepare-env script derives its work from.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readManifest(root: string, env: string): Manifest;
|
|
44
|
+
/** True if `value` looks like a PEM block (any `-----BEGIN ...-----`). */
|
|
45
|
+
export declare function isPem(value: string): boolean;
|
|
46
|
+
/** Read a `.pem` file (expanding a leading `~`) and assert it's a PEM block. Throws otherwise. */
|
|
47
|
+
export declare function readPemFile(path: string): string;
|
|
48
|
+
/** Heuristic: does this secret name suggest file input (PEM/key)? Used to preselect a file prompt — never to change semantics. */
|
|
49
|
+
export declare function looksLikeFileSecret(name: string): boolean;
|
|
50
|
+
export interface KvNamespace {
|
|
51
|
+
id: string;
|
|
52
|
+
title: string;
|
|
53
|
+
}
|
|
54
|
+
/** Best-effort ERROR line from a Bun ShellError (thrown on non-zero exit). */
|
|
55
|
+
export declare function errText(err: unknown): string;
|
|
56
|
+
/** Live KV namespaces on the account (`kv namespace list` emits a pure JSON array); `[]` when offline/unauthenticated. */
|
|
57
|
+
export declare function listNamespaces(cwd: string): Promise<KvNamespace[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Reuse the namespace whose title exactly matches `logical`, else create it. The
|
|
60
|
+
* id always comes from the structured `list` JSON (create prints freeform text),
|
|
61
|
+
* so we re-list rather than scrape. Returns the id, or null on failure.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ensureNamespace(logical: string, existing: KvNamespace[], cwd: string): Promise<string | null>;
|
|
64
|
+
/** Current secret names on the worker for `env`; `[]` when none/unreachable (Cloudflare allows setting pre-deploy). */
|
|
65
|
+
export declare function listSecretNames(env: string, cwd: string): Promise<string[]>;
|
|
66
|
+
/** `wrangler secret put <name> --env <env>` piping `value` on stdin (never a CLI arg). Returns success. */
|
|
67
|
+
export declare function putSecret(name: string, value: string, env: string, cwd: string): Promise<boolean>;
|
|
68
|
+
//# sourceMappingURL=prepare-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepare-env.d.ts","sourceRoot":"","sources":["../../src/wrangler/prepare-env.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable primitives for writing a project's own `prepare-env` script — the
|
|
3
|
+
* provisioning half of the wrangler setup flow (the `wrangler` monorepo pack owns
|
|
4
|
+
* the build-time half: the `wrangler-types` target + `.dev.vars.example`).
|
|
5
|
+
*
|
|
6
|
+
* `wrangler types --env-file .dev.vars.example` emits a machine-readable manifest
|
|
7
|
+
* that distinguishes committed public **vars** (literal-typed in `[env.<env>.vars]`)
|
|
8
|
+
* from **secrets** (string-typed, names in `.dev.vars.example`, values never in the
|
|
9
|
+
* repo). `readManifest` reads that split back so a script knows what to prompt for;
|
|
10
|
+
* the toml editors clone/blank/fill env blocks; the wrangler shell-outs provision.
|
|
11
|
+
*
|
|
12
|
+
* Import from a target's `scripts/prepare-env.ts`:
|
|
13
|
+
* import * as pe from '@smoothbricks/cli/wrangler/prepare-env';
|
|
14
|
+
* Scaffold a starter with: smoo wrangler scaffold <project>
|
|
15
|
+
*
|
|
16
|
+
* Prompt-agnostic by design: the module does the toml + wrangler work; the script
|
|
17
|
+
* brings its own prompts (e.g. @clack/prompts). Bun-only (uses `bun`'s `$`).
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { $ } from 'bun';
|
|
22
|
+
import { parse } from 'smol-toml';
|
|
23
|
+
// ── runtime narrowing (wrangler JSON + parsed toml are external data — no casts) ─
|
|
24
|
+
export function isRecord(v) {
|
|
25
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
26
|
+
}
|
|
27
|
+
function escapeRe(s) {
|
|
28
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
// ── pure wrangler.toml editors (no I/O — unit-testable) ──────────────────────
|
|
31
|
+
/** First `[env.<name>]` header, or `null` when the config declares no envs (top-level bindings only). */
|
|
32
|
+
export function firstWranglerEnv(toml) {
|
|
33
|
+
const match = toml.match(/^\s*\[env\.([A-Za-z0-9_-]+)/m);
|
|
34
|
+
return match ? match[1] : null;
|
|
35
|
+
}
|
|
36
|
+
/** True if the toml declares `[env.<env>]`. */
|
|
37
|
+
export function hasEnvBlock(toml, env) {
|
|
38
|
+
return new RegExp(`^\\[env\\.${escapeRe(env)}\\]`, 'm').test(toml);
|
|
39
|
+
}
|
|
40
|
+
/** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
|
|
41
|
+
export function getVar(toml, env, name) {
|
|
42
|
+
const block = envRecord(toml, env);
|
|
43
|
+
const vars = isRecord(block?.vars) ? block.vars : {};
|
|
44
|
+
const value = vars[name];
|
|
45
|
+
return typeof value === 'string' && value ? value : null;
|
|
46
|
+
}
|
|
47
|
+
/** Set a `[env.<env>.vars]` value, preserving any trailing comment. Throws if the key isn't present (scaffold first). */
|
|
48
|
+
export function setVar(toml, env, name, value) {
|
|
49
|
+
const re = new RegExp(`(\\[env\\.${escapeRe(env)}\\.vars\\][\\s\\S]*?\\n${escapeRe(name)} = )"[^"]*"`);
|
|
50
|
+
if (!re.test(toml)) {
|
|
51
|
+
throw new Error(`vars key "${name}" not found under [env.${env}.vars]`);
|
|
52
|
+
}
|
|
53
|
+
return toml.replace(re, (_m, prefix) => `${prefix}${JSON.stringify(value)}`);
|
|
54
|
+
}
|
|
55
|
+
/** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
|
|
56
|
+
export function getKvId(toml, env, binding) {
|
|
57
|
+
const block = envRecord(toml, env);
|
|
58
|
+
const rows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
if (isRecord(row) && row.binding === binding && typeof row.id === 'string') {
|
|
61
|
+
return row.id || null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** Rewrite the `id = ...` line of the `[[env.<env>.kv_namespaces]]` table whose binding matches. Throws if absent. */
|
|
67
|
+
export function setKvId(toml, env, binding, id, title) {
|
|
68
|
+
const re = new RegExp(`(\\[\\[env\\.${escapeRe(env)}\\.kv_namespaces\\]\\]\\s*\\n\\s*binding = "${escapeRe(binding)}"\\s*\\n\\s*id = )[^\\n]*`);
|
|
69
|
+
if (!re.test(toml)) {
|
|
70
|
+
throw new Error(`kv_namespaces binding "${binding}" not found under [env.${env}]`);
|
|
71
|
+
}
|
|
72
|
+
return toml.replace(re, `$1"${id}" # ${title}`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Append a copy of the whole `[env.<from>]` … block-group (every `[env.<from>...]`
|
|
76
|
+
* table up to the next non-`from` env or EOF) rewritten for `<to>`. Returns the new
|
|
77
|
+
* toml. Throws if `<from>` is absent or `<to>` already exists (blank/fill instead).
|
|
78
|
+
*/
|
|
79
|
+
export function cloneEnvBlock(toml, from, to) {
|
|
80
|
+
if (!hasEnvBlock(toml, from))
|
|
81
|
+
throw new Error(`source [env.${from}] not found`);
|
|
82
|
+
if (hasEnvBlock(toml, to))
|
|
83
|
+
throw new Error(`target [env.${to}] already exists — blank/fill it instead`);
|
|
84
|
+
const lines = toml.split('\n');
|
|
85
|
+
const isFromHeader = (l) => new RegExp(`^\\[+env\\.${escapeRe(from)}(\\.|\\])`).test(l);
|
|
86
|
+
const isAnyEnvHeader = (l) => /^\[+env\.[A-Za-z0-9_-]+(\.|\])/.test(l);
|
|
87
|
+
const out = [];
|
|
88
|
+
let capturing = false;
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (isFromHeader(line)) {
|
|
91
|
+
capturing = true;
|
|
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`;
|
|
105
|
+
}
|
|
106
|
+
/** Blank every `NAME = "..."` under `[env.<env>.vars]` whose key is in `keys` (env-specific values to re-fill). */
|
|
107
|
+
export function blankEnvVars(toml, env, keys) {
|
|
108
|
+
let out = toml;
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
const re = new RegExp(`(\\[env\\.${escapeRe(env)}\\.vars\\][\\s\\S]*?\\n${escapeRe(key)} = )"[^"]*"`);
|
|
111
|
+
if (re.test(out))
|
|
112
|
+
out = out.replace(re, '$1""');
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/** Blank every `id = ...` line under the env's `[[env.<env>.kv_namespaces]]` tables (per-env resource ids). */
|
|
117
|
+
export function blankKvIds(toml, env) {
|
|
118
|
+
const re = new RegExp(`(\\[\\[env\\.${escapeRe(env)}\\.kv_namespaces\\]\\]\\s*\\n\\s*binding = "[^"]*"\\s*\\n\\s*id = )[^\\n]*`, 'g');
|
|
119
|
+
return toml.replace(re, '$1""');
|
|
120
|
+
}
|
|
121
|
+
/** Parse-guard: throws if `toml` no longer parses, so a bad edit can't be written to disk. */
|
|
122
|
+
export function assertToml(toml) {
|
|
123
|
+
parse(toml);
|
|
124
|
+
}
|
|
125
|
+
/** Persist a toml string after confirming it still parses. */
|
|
126
|
+
export function saveToml(path, toml) {
|
|
127
|
+
assertToml(toml);
|
|
128
|
+
writeFileSync(path, toml);
|
|
129
|
+
}
|
|
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
|
+
/** Secret NAMES from a `.dev.vars.example` file (KEY=... / KEY="..."), ignoring comments/blanks. */
|
|
137
|
+
export function parseDevVarsExample(text) {
|
|
138
|
+
const names = [];
|
|
139
|
+
for (const raw of text.split('\n')) {
|
|
140
|
+
const line = raw.trim();
|
|
141
|
+
if (!line || line.startsWith('#'))
|
|
142
|
+
continue;
|
|
143
|
+
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
144
|
+
if (m)
|
|
145
|
+
names.push(m[1]);
|
|
146
|
+
}
|
|
147
|
+
return names;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Read the vars/secrets split for `env` from `<root>/wrangler.toml` (public var
|
|
151
|
+
* keys) + `<root>/.dev.vars.example` (secret names). This is the "what to prompt
|
|
152
|
+
* for" manifest a prepare-env script derives its work from.
|
|
153
|
+
*/
|
|
154
|
+
export function readManifest(root, env) {
|
|
155
|
+
const toml = readFileSync(join(root, 'wrangler.toml'), 'utf8');
|
|
156
|
+
const block = envRecord(toml, env);
|
|
157
|
+
const varsRec = isRecord(block?.vars) ? block.vars : {};
|
|
158
|
+
const vars = Object.keys(varsRec);
|
|
159
|
+
const kvRows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
|
|
160
|
+
const kvBindings = kvRows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
|
|
161
|
+
const examplePath = join(root, '.dev.vars.example');
|
|
162
|
+
const secrets = existsSync(examplePath) ? parseDevVarsExample(readFileSync(examplePath, 'utf8')) : [];
|
|
163
|
+
return { kvBindings, vars, secrets };
|
|
164
|
+
}
|
|
165
|
+
// ── PEM (a standard, reusable) ───────────────────────────────────────────────
|
|
166
|
+
/** True if `value` looks like a PEM block (any `-----BEGIN ...-----`). */
|
|
167
|
+
export function isPem(value) {
|
|
168
|
+
return /-----BEGIN [A-Z ]+-----/.test(value);
|
|
169
|
+
}
|
|
170
|
+
/** Read a `.pem` file (expanding a leading `~`) and assert it's a PEM block. Throws otherwise. */
|
|
171
|
+
export function readPemFile(path) {
|
|
172
|
+
const resolved = path.replace(/^~/, process.env.HOME ?? '~');
|
|
173
|
+
const content = readFileSync(resolved, 'utf8');
|
|
174
|
+
if (!isPem(content))
|
|
175
|
+
throw new Error(`${path} is not a PEM (no -----BEGIN block)`);
|
|
176
|
+
return content;
|
|
177
|
+
}
|
|
178
|
+
/** Heuristic: does this secret name suggest file input (PEM/key)? Used to preselect a file prompt — never to change semantics. */
|
|
179
|
+
export function looksLikeFileSecret(name) {
|
|
180
|
+
return /_PEM$|_KEY$|_CERT$/.test(name);
|
|
181
|
+
}
|
|
182
|
+
function asKvNamespaces(json) {
|
|
183
|
+
if (!Array.isArray(json))
|
|
184
|
+
return [];
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const row of json) {
|
|
187
|
+
if (isRecord(row) && typeof row.id === 'string' && typeof row.title === 'string') {
|
|
188
|
+
out.push({ id: row.id, title: row.title });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
function asSecretNames(json) {
|
|
194
|
+
if (!Array.isArray(json))
|
|
195
|
+
return [];
|
|
196
|
+
return json.flatMap((row) => (isRecord(row) && typeof row.name === 'string' ? [row.name] : []));
|
|
197
|
+
}
|
|
198
|
+
/** Best-effort ERROR line from a Bun ShellError (thrown on non-zero exit). */
|
|
199
|
+
export function errText(err) {
|
|
200
|
+
if (!isRecord(err))
|
|
201
|
+
return 'unknown error';
|
|
202
|
+
const stderr = 'stderr' in err && err.stderr != null ? String(err.stderr) : '';
|
|
203
|
+
const exit = 'exitCode' in err ? String(err.exitCode) : '?';
|
|
204
|
+
return stderr.split('\n').find((l) => l.includes('ERROR')) ?? `exit ${exit}`;
|
|
205
|
+
}
|
|
206
|
+
/** Live KV namespaces on the account (`kv namespace list` emits a pure JSON array); `[]` when offline/unauthenticated. */
|
|
207
|
+
export async function listNamespaces(cwd) {
|
|
208
|
+
try {
|
|
209
|
+
return asKvNamespaces(await $ `bunx wrangler kv namespace list`.cwd(cwd).quiet().json());
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Reuse the namespace whose title exactly matches `logical`, else create it. The
|
|
217
|
+
* id always comes from the structured `list` JSON (create prints freeform text),
|
|
218
|
+
* so we re-list rather than scrape. Returns the id, or null on failure.
|
|
219
|
+
*/
|
|
220
|
+
export async function ensureNamespace(logical, existing, cwd) {
|
|
221
|
+
const match = existing.find((n) => n.title === logical);
|
|
222
|
+
if (match)
|
|
223
|
+
return match.id;
|
|
224
|
+
try {
|
|
225
|
+
await $ `bunx wrangler kv namespace create ${logical}`.cwd(cwd).quiet();
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
return errText(err).includes('already') ? (await reList(logical, cwd)) : null;
|
|
229
|
+
}
|
|
230
|
+
return reList(logical, cwd);
|
|
231
|
+
}
|
|
232
|
+
async function reList(logical, cwd) {
|
|
233
|
+
return (await listNamespaces(cwd)).find((n) => n.title === logical)?.id ?? null;
|
|
234
|
+
}
|
|
235
|
+
/** Current secret names on the worker for `env`; `[]` when none/unreachable (Cloudflare allows setting pre-deploy). */
|
|
236
|
+
export async function listSecretNames(env, cwd) {
|
|
237
|
+
try {
|
|
238
|
+
return asSecretNames(await $ `bunx wrangler secret list --format json --env ${env}`.cwd(cwd).quiet().json());
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** `wrangler secret put <name> --env <env>` piping `value` on stdin (never a CLI arg). Returns success. */
|
|
245
|
+
export async function putSecret(name, value, env, cwd) {
|
|
246
|
+
try {
|
|
247
|
+
await $ `bunx wrangler secret put ${name} --env ${env} < ${Buffer.from(value)}`.cwd(cwd).quiet();
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ScaffoldOptions {
|
|
2
|
+
/** Overwrite an existing scripts/prepare-env.ts. */
|
|
3
|
+
force?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/** Write the starter script + wire the `prepare-env` nx target. Returns the script path. */
|
|
6
|
+
export declare function scaffold(root: string, project: string, options?: ScaffoldOptions): string;
|
|
7
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/wrangler/scaffold.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAmMD,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CA2B7F"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `smoo wrangler scaffold <project>` — write a starter `scripts/prepare-env.ts`
|
|
3
|
+
* into a wrangler project and wire its `prepare-env` nx target. The generated
|
|
4
|
+
* script imports the `@smoothbricks/cli/wrangler/prepare-env` primitives and
|
|
5
|
+
* derives its work at runtime from the project's manifest (wrangler.toml [vars] +
|
|
6
|
+
* .dev.vars.example), so it never drifts from what the worker declares.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join, relative } from 'node:path';
|
|
10
|
+
import { getOrCreateRecord, recordProperty, writeJsonObject } from '../lib/json.js';
|
|
11
|
+
import { getWorkspacePackageManifests } from '../lib/workspace.js';
|
|
12
|
+
/** Locate a workspace package by nx name, package name, or relative path; must have a wrangler.toml. */
|
|
13
|
+
function resolveProject(root, project) {
|
|
14
|
+
const manifests = getWorkspacePackageManifests(root);
|
|
15
|
+
const match = manifests.find((m) => {
|
|
16
|
+
const nx = recordProperty(m.json, 'nx');
|
|
17
|
+
const nxName = nx && typeof nx.name === 'string' ? nx.name : null;
|
|
18
|
+
return nxName === project || m.name === project || relative(root, m.path) === project;
|
|
19
|
+
});
|
|
20
|
+
if (!match) {
|
|
21
|
+
const known = manifests.map((m) => recordProperty(m.json, 'nx')?.name ?? m.name).join(', ');
|
|
22
|
+
throw new Error(`project "${project}" not found. Known: ${known}`);
|
|
23
|
+
}
|
|
24
|
+
const dir = match.path; // WorkspacePackageManifest.path is absolute
|
|
25
|
+
if (!existsSync(join(dir, 'wrangler.toml'))) {
|
|
26
|
+
throw new Error(`"${project}" has no wrangler.toml — not a wrangler project`);
|
|
27
|
+
}
|
|
28
|
+
return { dir, label: relative(root, dir), packageJsonPath: match.packageJsonPath, json: match.json };
|
|
29
|
+
}
|
|
30
|
+
/** The generated starter script — generic, manifest-driven, prompt-batteries included. */
|
|
31
|
+
function renderScript(projectName) {
|
|
32
|
+
return `#!/usr/bin/env bun
|
|
33
|
+
/**
|
|
34
|
+
* Prepare a deploy environment for ${projectName}. Generated by \`smoo wrangler scaffold\`.
|
|
35
|
+
*
|
|
36
|
+
* Provisions what \`wrangler deploy\` assumes already exists, idempotently:
|
|
37
|
+
* - KV namespaces the worker binds (ids written into wrangler.toml)
|
|
38
|
+
* - public [env.<env>.vars] (committed config the deploy applies)
|
|
39
|
+
* - secrets (wrangler secret put; values never touch the repo)
|
|
40
|
+
*
|
|
41
|
+
* Work is derived from the manifest (wrangler.toml [vars] + .dev.vars.example),
|
|
42
|
+
* so it stays in sync with what the worker declares. Customize the app-specific
|
|
43
|
+
* bits below (KV title convention; any secret that feeds >1 binding).
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
* bun scripts/prepare-env.ts <env> # provision (staging | production | …)
|
|
47
|
+
* bun scripts/prepare-env.ts <env> --check # report state, no writes
|
|
48
|
+
*/
|
|
49
|
+
import { readFileSync } from 'node:fs';
|
|
50
|
+
import { resolve } from 'node:path';
|
|
51
|
+
import * as p from '@clack/prompts';
|
|
52
|
+
import * as pe from '@smoothbricks/cli/wrangler/prepare-env';
|
|
53
|
+
|
|
54
|
+
const ROOT = resolve(import.meta.dir, '..');
|
|
55
|
+
const WRANGLER = resolve(ROOT, 'wrangler.toml');
|
|
56
|
+
const readToml = () => readFileSync(WRANGLER, 'utf8');
|
|
57
|
+
|
|
58
|
+
// App-specific: the account-level logical title for a KV binding. Default is
|
|
59
|
+
// lowercase + underscores→dashes; edit if your namespaces are named differently.
|
|
60
|
+
function kvTitle(binding: string, env: string): string {
|
|
61
|
+
return \`\${binding.toLowerCase().replace(/_/g, '-')}-\${env}\`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function bail<T>(v: T | symbol): asserts v is T {
|
|
65
|
+
if (p.isCancel(v)) {
|
|
66
|
+
p.cancel('Aborted — anything already provisioned stays; re-run to continue (idempotent).');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function checkMode(env: string): Promise<void> {
|
|
72
|
+
const toml = readToml();
|
|
73
|
+
const { kvBindings, vars, secrets } = pe.readManifest(ROOT, env);
|
|
74
|
+
p.log.info(\`env block [env.\${env}]: \${pe.hasEnvBlock(toml, env) ? '✓ present' : '✗ missing'}\`);
|
|
75
|
+
const live = await pe.listNamespaces(ROOT);
|
|
76
|
+
for (const binding of kvBindings) {
|
|
77
|
+
const inToml = pe.getKvId(toml, env, binding);
|
|
78
|
+
const liveId = live.find((n) => n.title === kvTitle(binding, env))?.id ?? null;
|
|
79
|
+
p.log.message(\` KV \${binding}: \${!liveId ? '✗ not created' : inToml === liveId ? \`✓ \${liveId}\` : \`⚠ drift (live \${liveId}, toml \${inToml ?? 'unset'})\`}\`);
|
|
80
|
+
}
|
|
81
|
+
for (const name of vars) {
|
|
82
|
+
p.log.message(\` var \${name}: \${pe.getVar(toml, env, name) ? '✓ set' : '✗ empty'}\`);
|
|
83
|
+
}
|
|
84
|
+
const set = await pe.listSecretNames(env, ROOT);
|
|
85
|
+
for (const name of secrets) {
|
|
86
|
+
p.log.message(\` secret \${name}: \${set.includes(name) ? '✓ set' : '✗ missing'}\`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main(): Promise<void> {
|
|
91
|
+
const env = process.argv.slice(2).find((a) => !a.startsWith('-'));
|
|
92
|
+
if (!env) {
|
|
93
|
+
console.error('usage: bun scripts/prepare-env.ts <env> [--check]');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const check = process.argv.includes('--check');
|
|
97
|
+
p.intro(\`${projectName} · prepare \${env}\${check ? ' (check)' : ''}\`);
|
|
98
|
+
|
|
99
|
+
if (check) {
|
|
100
|
+
await checkMode(env);
|
|
101
|
+
p.outro(\`check complete for "\${env}" — nothing changed\`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let toml = readToml();
|
|
106
|
+
|
|
107
|
+
// 0. Env block. If missing, offer to clone an existing env and blank its
|
|
108
|
+
// per-env values (KV ids), then fill below.
|
|
109
|
+
if (!pe.hasEnvBlock(toml, env)) {
|
|
110
|
+
const from = pe.firstWranglerEnv(toml);
|
|
111
|
+
if (from && (await p.confirm({ message: \`No [env.\${env}] — clone from [env.\${from}]?\` })) === true) {
|
|
112
|
+
toml = pe.blankKvIds(pe.cloneEnvBlock(toml, from, env), env);
|
|
113
|
+
pe.saveToml(WRANGLER, toml);
|
|
114
|
+
p.log.success(\`cloned [env.\${from}] → [env.\${env}] (KV ids blanked; edit domain-specific vars below)\`);
|
|
115
|
+
} else {
|
|
116
|
+
p.log.warn(\`Add an [env.\${env}] block to wrangler.toml first, then re-run.\`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { kvBindings, vars, secrets } = pe.readManifest(ROOT, env);
|
|
122
|
+
|
|
123
|
+
// 1. KV namespaces → ids into the toml.
|
|
124
|
+
const live = await pe.listNamespaces(ROOT);
|
|
125
|
+
for (const binding of kvBindings) {
|
|
126
|
+
const s = p.spinner();
|
|
127
|
+
s.start(\`KV \${binding}\`);
|
|
128
|
+
const id = await pe.ensureNamespace(kvTitle(binding, env), live, ROOT);
|
|
129
|
+
if (!id) { s.stop(\`✗ \${binding}\`); continue; }
|
|
130
|
+
toml = pe.setKvId(toml, env, binding, id, kvTitle(binding, env));
|
|
131
|
+
pe.saveToml(WRANGLER, toml);
|
|
132
|
+
s.stop(\`✓ \${binding} → \${id}\`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2. Public vars → [env.<env>.vars] (committed, applied by deploy). Enter keeps
|
|
136
|
+
// current; a same-named env var overrides (CI).
|
|
137
|
+
if ((await p.confirm({ message: \`Set public vars for "\${env}"?\` })) === true) {
|
|
138
|
+
for (const name of vars) {
|
|
139
|
+
const current = pe.getVar(toml, env, name);
|
|
140
|
+
const preset = process.env[name]?.trim();
|
|
141
|
+
let value: string;
|
|
142
|
+
if (preset) {
|
|
143
|
+
value = preset;
|
|
144
|
+
p.log.info(\`\${name}: from environment\`);
|
|
145
|
+
} else {
|
|
146
|
+
const raw = await p.text({ message: \`\${name}:\`, placeholder: current ? 'Enter to keep current' : 'Enter to skip', initialValue: current ?? '' });
|
|
147
|
+
bail(raw);
|
|
148
|
+
value = raw.trim();
|
|
149
|
+
}
|
|
150
|
+
if (!value || value === current) continue;
|
|
151
|
+
toml = pe.setVar(toml, env, name, value);
|
|
152
|
+
pe.saveToml(WRANGLER, toml);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Secrets → wrangler secret put (values never in the repo). File-or-paste;
|
|
157
|
+
// PEM validated by content. Skip-if-set unless you choose to replace.
|
|
158
|
+
const alreadySet = await pe.listSecretNames(env, ROOT);
|
|
159
|
+
if ((await p.confirm({ message: \`Set secrets for "\${env}"?\` })) === true) {
|
|
160
|
+
for (const name of secrets) {
|
|
161
|
+
const preset = process.env[name]?.trim();
|
|
162
|
+
if (alreadySet.includes(name) && !preset) {
|
|
163
|
+
const replace = await p.confirm({ message: \`\${name} already set — replace?\`, initialValue: false });
|
|
164
|
+
bail(replace);
|
|
165
|
+
if (!replace) { p.log.info(\`kept \${name}\`); continue; }
|
|
166
|
+
}
|
|
167
|
+
let value: string;
|
|
168
|
+
if (preset) {
|
|
169
|
+
value = preset;
|
|
170
|
+
} else if (pe.looksLikeFileSecret(name)) {
|
|
171
|
+
const path = await p.text({ message: \`\${name} — path to file:\`, placeholder: 'Enter to skip' });
|
|
172
|
+
bail(path);
|
|
173
|
+
if (!path.trim()) continue;
|
|
174
|
+
value = pe.readPemFile(path.trim());
|
|
175
|
+
} else {
|
|
176
|
+
const raw = await p.password({ message: \`\${name}:\` });
|
|
177
|
+
bail(raw);
|
|
178
|
+
value = raw.trim();
|
|
179
|
+
if (!value) continue;
|
|
180
|
+
if (pe.isPem(value) === false && /KEY|PEM|CERT/.test(name) && value.startsWith('~')) {
|
|
181
|
+
value = pe.readPemFile(value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const s = p.spinner();
|
|
185
|
+
s.start(\`secret \${name}\`);
|
|
186
|
+
const ok = await pe.putSecret(name, value, env, ROOT);
|
|
187
|
+
s.stop(\`\${ok ? '✓' : '✗'} \${name}\`);
|
|
188
|
+
}
|
|
189
|
+
// Verify: re-list and flag any we set that didn't land.
|
|
190
|
+
const after = await pe.listSecretNames(env, ROOT);
|
|
191
|
+
const missing = secrets.filter((n) => !after.includes(n));
|
|
192
|
+
if (missing.length) p.log.warn(\`not listed afterwards — verify manually: \${missing.join(', ')}\`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
p.outro(\`"\${env}" prepared — re-run anytime (idempotent) · inspect with --check · then deploy\`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (import.meta.main) {
|
|
199
|
+
await main();
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
/** Write the starter script + wire the `prepare-env` nx target. Returns the script path. */
|
|
204
|
+
export function scaffold(root, project, options = {}) {
|
|
205
|
+
const { dir, label, packageJsonPath, json } = resolveProject(root, project);
|
|
206
|
+
const scriptsDir = join(dir, 'scripts');
|
|
207
|
+
const scriptPath = join(scriptsDir, 'prepare-env.ts');
|
|
208
|
+
const existed = existsSync(scriptPath);
|
|
209
|
+
if (existed && !options.force) {
|
|
210
|
+
throw new Error(`${label}/scripts/prepare-env.ts already exists — pass --force to overwrite`);
|
|
211
|
+
}
|
|
212
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
213
|
+
writeFileSync(scriptPath, renderScript(project));
|
|
214
|
+
console.log(`${existed ? 'overwrote' : 'created'} ${label}/scripts/prepare-env.ts`);
|
|
215
|
+
// Wire the nx target (idempotent).
|
|
216
|
+
const nx = getOrCreateRecord(json, 'nx');
|
|
217
|
+
const targets = getOrCreateRecord(nx, 'targets');
|
|
218
|
+
if (!recordProperty(targets, 'prepare-env')) {
|
|
219
|
+
targets['prepare-env'] = {
|
|
220
|
+
executor: 'nx:run-commands',
|
|
221
|
+
options: { command: 'bun scripts/prepare-env.ts', cwd: '{projectRoot}' },
|
|
222
|
+
};
|
|
223
|
+
writeJsonObject(packageJsonPath, json);
|
|
224
|
+
console.log(`updated ${label}/package.json prepare-env target`);
|
|
225
|
+
}
|
|
226
|
+
console.log(`\nnext: fill secret NAMES in ${label}/.dev.vars.example, then\n bunx nx run ${project}:prepare-env -- <env>`);
|
|
227
|
+
return scriptPath;
|
|
228
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smoothbricks/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SmoothBricks monorepo automation CLI",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
"development": "./src/monorepo/git-config.ts",
|
|
34
34
|
"import": "./dist/monorepo/git-config.js",
|
|
35
35
|
"default": "./dist/monorepo/git-config.js"
|
|
36
|
+
},
|
|
37
|
+
"./wrangler/prepare-env": {
|
|
38
|
+
"types": "./dist/wrangler/prepare-env.d.ts",
|
|
39
|
+
"bun": "./src/wrangler/prepare-env.ts",
|
|
40
|
+
"development": "./src/wrangler/prepare-env.ts",
|
|
41
|
+
"import": "./dist/wrangler/prepare-env.js",
|
|
42
|
+
"default": "./dist/wrangler/prepare-env.js"
|
|
36
43
|
}
|
|
37
44
|
},
|
|
38
45
|
"files": [
|
|
@@ -51,6 +58,7 @@
|
|
|
51
58
|
"publint": "^0.3.18",
|
|
52
59
|
"semver": "^7.7.4",
|
|
53
60
|
"sherif": "^1.11.1",
|
|
61
|
+
"smol-toml": "^1.6.1",
|
|
54
62
|
"tslib": "^2.8.1"
|
|
55
63
|
},
|
|
56
64
|
"devDependencies": {
|
package/src/cli.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { variants } from './generate/index.js';
|
|
|
3
3
|
import { cliPackageVersion } from './lib/cli-package.js';
|
|
4
4
|
import { findRepoRoot } from './lib/run.js';
|
|
5
5
|
import { resolvePrConflicts } from './pr/index.js';
|
|
6
|
+
import { scaffold } from './wrangler/scaffold.js';
|
|
6
7
|
|
|
7
8
|
export async function runCli(argv = process.argv.slice(2)): Promise<void> {
|
|
8
9
|
const program = buildProgram();
|
|
@@ -354,6 +355,15 @@ function buildProgram(): Command {
|
|
|
354
355
|
}
|
|
355
356
|
});
|
|
356
357
|
|
|
358
|
+
const wrangler = program.command('wrangler').description('Cloudflare wrangler project helpers');
|
|
359
|
+
wrangler
|
|
360
|
+
.command('scaffold <project>')
|
|
361
|
+
.description('Write a starter scripts/prepare-env.ts (manifest-driven) and wire its nx target')
|
|
362
|
+
.option('--force', 'overwrite an existing scripts/prepare-env.ts')
|
|
363
|
+
.action(async (project: string, options: { force?: boolean }) => {
|
|
364
|
+
scaffold(await findRepoRoot(), project, { force: options.force });
|
|
365
|
+
});
|
|
366
|
+
|
|
357
367
|
return program;
|
|
358
368
|
}
|
|
359
369
|
|