@smoothbricks/cli 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -67,9 +67,12 @@ function buildProgram() {
67
67
  const { validateCommitMessageFile } = await import('./monorepo/index.js');
68
68
  validateCommitMessageFile(commitMsgFile, options, await findRepoRoot());
69
69
  });
70
- monorepo.command('sync-bun-lockfile-versions').action(async () => {
70
+ monorepo
71
+ .command('sync-bun-lockfile-versions')
72
+ .option('--stage', 'stage bun.lock when versions were resynced (pre-commit self-heal); quiet when clean')
73
+ .action(async (options) => {
71
74
  const { syncBunLockfileVersions } = await import('./monorepo/index.js');
72
- syncBunLockfileVersions(await findRepoRoot());
75
+ syncBunLockfileVersions(await findRepoRoot(), options.stage ? { log: false, stage: true } : {});
73
76
  });
74
77
  monorepo
75
78
  .command('list-release-packages')
@@ -1,5 +1,7 @@
1
1
  export interface SyncBunLockfileVersionsOptions {
2
2
  log?: boolean;
3
+ /** `git add` bun.lock after rewriting it, for the pre-commit self-heal path. */
4
+ stage?: boolean;
3
5
  }
4
6
  export declare function syncBunLockfileVersions(root: string, options?: SyncBunLockfileVersionsOptions): number;
5
7
  export declare function validateBunLockfileVersions(root: string): number;
@@ -1 +1 @@
1
- {"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,8BAA8B;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAWD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,8BAAmC,GAAG,MAAM,CAkD1G;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiChE"}
1
+ {"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,8BAA8B;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiBD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,8BAAmC,GAAG,MAAM,CAuD1G;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiChE"}
@@ -11,6 +11,12 @@ import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
11
11
  // - https://github.com/oven-sh/bun/issues/18906
12
12
  // - https://github.com/oven-sh/bun/issues/20477
13
13
  // - https://github.com/oven-sh/bun/issues/20829
14
+ //
15
+ // Any `bun install` rewrites the workspace versions from package.json, reverting
16
+ // this sync between releases. The pre-commit hook therefore runs
17
+ // `smoo monorepo sync-bun-lockfile-versions --stage` on every commit so drift is
18
+ // healed at the commit that would otherwise carry it — validation must never
19
+ // fail a committer for drift a routine `bun install` introduced.
14
20
  export function syncBunLockfileVersions(root, options = {}) {
15
21
  const log = options.log ?? true;
16
22
  const lockfilePath = join(root, 'bun.lock');
@@ -53,9 +59,14 @@ export function syncBunLockfileVersions(root, options = {}) {
53
59
  }
54
60
  if (updated > 0) {
55
61
  writeFileSync(lockfilePath, lockfile);
62
+ if (options.stage) {
63
+ execSync('git add bun.lock', { cwd: root, stdio: ['pipe', 'pipe', 'pipe'] });
64
+ }
56
65
  }
57
- if (log) {
58
- console.log(updated > 0 ? `Updated ${updated} workspace version(s) in bun.lock` : 'All workspace versions already in sync.');
66
+ if (log || updated > 0) {
67
+ console.log(updated > 0
68
+ ? `Updated ${updated} workspace version(s) in bun.lock${options.stage ? ' (staged)' : ''}`
69
+ : 'All workspace versions already in sync.');
59
70
  }
60
71
  return updated;
61
72
  }
@@ -1,26 +1,33 @@
1
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). */
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 isn't present (scaffold first). */
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 `[[env.<env>.kv_namespaces]]` table whose binding matches. Throws if absent. */
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 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).
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 every `NAME = "..."` under `[env.<env>.vars]` whose key is in `keys` (env-specific values to re-fill). */
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 = ...` line under the env's `[[env.<env>.kv_namespaces]]` tables (per-env resource ids). */
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;AAQD,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5D;AAED,+CAA+C;AAC/C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,yFAAyF;AACzF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7E;AAED,yHAAyH;AACzH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMrF;AAED,yFAAyF;AACzF,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASjF;AAED,sHAAsH;AACtH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAQrG;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAsB5E;AAED,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAO9E;AAED,+GAA+G;AAC/G,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAM5D;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,8DAA8D;AAC9D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAGzD;AAWD,MAAM,WAAW,QAAQ;IACvB,8FAA8F;IAC9F,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,mGAAmG;IACnG,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAS1D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAUhE;AAID,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,kGAAkG;AAClG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,kIAAkI;AAClI,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAID,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAkBD,8EAA8E;AAC9E,wBAAgB,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAK5C;AAED,0HAA0H;AAC1H,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAMxE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASnH;AAMD,uHAAuH;AACvH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAMjF;AAED,2GAA2G;AAC3G,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOvG"}
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 { parse } from 'smol-toml';
23
- // ── runtime narrowing (wrangler JSON + parsed toml are external data — no casts)
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
- function escapeRe(s) {
28
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
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
- // ── pure wrangler.toml editors (no I/Ounit-testable) ──────────────────────
31
- /** First `[env.<name>]` header, or `null` when the config declares no envs (top-level bindings only). */
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 match = toml.match(/^\s*\[env\.([A-Za-z0-9_-]+)/m);
34
- return match ? match[1] : null;
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
- return new RegExp(`^\\[env\\.${escapeRe(env)}\\]`, 'm').test(toml);
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 isn't present (scaffold first). */
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 re = new RegExp(`(\\[env\\.${escapeRe(env)}\\.vars\\][\\s\\S]*?\\n${escapeRe(name)} = )"[^"]*"`);
50
- if (!re.test(toml)) {
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 `[[env.<env>.kv_namespaces]]` table whose binding matches. Throws if absent. */
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 re = new RegExp(`(\\[\\[env\\.${escapeRe(env)}\\.kv_namespaces\\]\\]\\s*\\n\\s*binding = "${escapeRe(binding)}"\\s*\\n\\s*id = )[^\\n]*`);
69
- if (!re.test(toml)) {
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 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).
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 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`;
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 every `NAME = "..."` under `[env.<env>.vars]` whose key is in `keys` (env-specific values to re-fill). */
135
+ /** Blank the named `[env.<env>.vars]` values to `""` (preserving comments), skipping absent keys. */
107
136
  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;
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 = ...` line under the env's `[[env.<env>.kv_namespaces]]` tables (per-env resource ids). */
145
+ /** Blank every kv_namespaces `id` under the env to `""` (drops trailing comment; binding/name survive). */
117
146
  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""');
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
- parse(toml);
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 {
@@ -9,6 +9,12 @@ set -e -o pipefail
9
9
  # Format exactly what is staged so the commit includes the formatter output.
10
10
  git-format-staged
11
11
 
12
+ # Any `bun install` rewrites bun.lock workspace versions from package.json,
13
+ # reverting the stable-tag versions the release flow syncs (see
14
+ # packages/cli/src/monorepo/lockfile.ts). Self-heal here instead of failing a
15
+ # later commit on drift it didn't cause.
16
+ smoo monorepo sync-bun-lockfile-versions --stage
17
+
12
18
  # Adding a workspace package is rare and easy to leave half-wired. Run the
13
19
  # full monorepo validator only for newly staged package manifests.
14
20
  smoo monorepo validate --fail-fast --only-if-new-workspace-package
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -52,13 +52,13 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@arethetypeswrong/core": "^0.18.2",
55
- "@smoothbricks/nx-plugin": "0.2.5",
55
+ "@smoothbricks/nx-plugin": "0.3.0",
56
56
  "@smoothbricks/validation": "0.1.1",
57
57
  "commander": "^14.0.3",
58
58
  "publint": "^0.3.18",
59
59
  "semver": "^7.7.4",
60
60
  "sherif": "^1.11.1",
61
- "smol-toml": "^1.6.1",
61
+ "toml-eslint-parser": "^1.0.3",
62
62
  "tslib": "^2.8.1"
63
63
  },
64
64
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -76,10 +76,13 @@ function buildProgram(): Command {
76
76
  const { validateCommitMessageFile } = await import('./monorepo/index.js');
77
77
  validateCommitMessageFile(commitMsgFile, options, await findRepoRoot());
78
78
  });
79
- monorepo.command('sync-bun-lockfile-versions').action(async () => {
80
- const { syncBunLockfileVersions } = await import('./monorepo/index.js');
81
- syncBunLockfileVersions(await findRepoRoot());
82
- });
79
+ monorepo
80
+ .command('sync-bun-lockfile-versions')
81
+ .option('--stage', 'stage bun.lock when versions were resynced (pre-commit self-heal); quiet when clean')
82
+ .action(async (options: { stage?: boolean }) => {
83
+ const { syncBunLockfileVersions } = await import('./monorepo/index.js');
84
+ syncBunLockfileVersions(await findRepoRoot(), options.stage ? { log: false, stage: true } : {});
85
+ });
83
86
  monorepo
84
87
  .command('list-release-packages')
85
88
  .option('--fail-empty', 'fail when no owned release packages are found')