@smoothbricks/cli 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +31 -23
  2. package/dist/bun/preload.d.ts +13 -0
  3. package/dist/bun/preload.d.ts.map +1 -0
  4. package/dist/bun/preload.js +12 -0
  5. package/dist/cli.js +36 -4
  6. package/dist/generate/index.d.ts.map +1 -1
  7. package/dist/generate/index.js +0 -6
  8. package/dist/github-ci/index.d.ts +17 -2
  9. package/dist/github-ci/index.d.ts.map +1 -1
  10. package/dist/github-ci/index.js +192 -26
  11. package/dist/github-ci/outputs.d.ts +22 -0
  12. package/dist/github-ci/outputs.d.ts.map +1 -0
  13. package/dist/github-ci/outputs.js +648 -0
  14. package/dist/lib/cli-package.js +2 -2
  15. package/dist/lib/json.d.ts +118 -11
  16. package/dist/lib/json.d.ts.map +1 -1
  17. package/dist/lib/json.js +317 -29
  18. package/dist/lib/workspace.d.ts +7 -14
  19. package/dist/lib/workspace.d.ts.map +1 -1
  20. package/dist/lib/workspace.js +76 -82
  21. package/dist/monorepo/ci-workflow.d.ts +2 -0
  22. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  23. package/dist/monorepo/ci-workflow.js +28 -2
  24. package/dist/monorepo/git-config.js +2 -2
  25. package/dist/monorepo/index.d.ts +3 -1
  26. package/dist/monorepo/index.d.ts.map +1 -1
  27. package/dist/monorepo/index.js +6 -2
  28. package/dist/monorepo/lockfile.d.ts +13 -1
  29. package/dist/monorepo/lockfile.d.ts.map +1 -1
  30. package/dist/monorepo/lockfile.js +33 -21
  31. package/dist/monorepo/managed-files.d.ts +12 -0
  32. package/dist/monorepo/managed-files.d.ts.map +1 -1
  33. package/dist/monorepo/managed-files.js +86 -28
  34. package/dist/monorepo/package-policy.d.ts.map +1 -1
  35. package/dist/monorepo/package-policy.js +78 -45
  36. package/dist/monorepo/packed-manifest.d.ts +5 -2
  37. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  38. package/dist/monorepo/packed-manifest.js +16 -47
  39. package/dist/monorepo/packed-package.d.ts.map +1 -1
  40. package/dist/monorepo/packed-package.js +28 -16
  41. package/dist/monorepo/packs/index.d.ts +3 -0
  42. package/dist/monorepo/packs/index.d.ts.map +1 -1
  43. package/dist/monorepo/packs/index.js +18 -6
  44. package/dist/monorepo/publish-workflow.d.ts +1 -0
  45. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  46. package/dist/monorepo/publish-workflow.js +242 -3
  47. package/dist/monorepo/runtime.d.ts +15 -0
  48. package/dist/monorepo/runtime.d.ts.map +1 -1
  49. package/dist/monorepo/runtime.js +66 -12
  50. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  51. package/dist/monorepo/tool-validation.js +32 -23
  52. package/dist/monorepo/wrangler.d.ts.map +1 -1
  53. package/dist/monorepo/wrangler.js +13 -13
  54. package/dist/nx/index.d.ts +12 -4
  55. package/dist/nx/index.d.ts.map +1 -1
  56. package/dist/nx/index.js +86 -19
  57. package/dist/pr/index.d.ts.map +1 -1
  58. package/dist/pr/index.js +28 -32
  59. package/dist/release/candidates.d.ts +3 -2
  60. package/dist/release/candidates.d.ts.map +1 -1
  61. package/dist/release/candidates.js +4 -5
  62. package/dist/release/github-release.d.ts +3 -0
  63. package/dist/release/github-release.d.ts.map +1 -1
  64. package/dist/release/github-release.js +11 -0
  65. package/dist/release/index.d.ts +20 -1
  66. package/dist/release/index.d.ts.map +1 -1
  67. package/dist/release/index.js +221 -89
  68. package/dist/release/orchestration.d.ts +8 -1
  69. package/dist/release/orchestration.d.ts.map +1 -1
  70. package/dist/release/orchestration.js +38 -1
  71. package/dist/wrangler/prepare-env.d.ts +0 -1
  72. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  73. package/dist/wrangler/prepare-env.js +100 -51
  74. package/dist/wrangler/scaffold.d.ts.map +1 -1
  75. package/dist/wrangler/scaffold.js +12 -12
  76. package/managed/raw/git-format-staged.yml +19 -1
  77. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +6 -0
  78. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  79. package/managed/raw/tooling/direnv/setup-environment.ts +187 -19
  80. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  81. package/managed/templates/github/actions/cache-nix-devenv/action.yml +12 -10
  82. package/managed/templates/github/actions/cache-node-modules/action.yml +6 -2
  83. package/managed/templates/github/actions/cache-nx/action.yml +2 -2
  84. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  85. package/managed/templates/github/actions/save-nix-devenv/action.yml +11 -3
  86. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  87. package/managed/templates/github/workflows/managed-files.yml +80 -0
  88. package/package.json +18 -4
  89. package/src/bun/preload.ts +12 -0
  90. package/src/cli.ts +49 -11
  91. package/src/generate/index.ts +0 -6
  92. package/src/github-ci/index.test.ts +486 -0
  93. package/src/github-ci/index.ts +220 -30
  94. package/src/github-ci/outputs.ts +505 -0
  95. package/src/lib/cli-package.ts +2 -2
  96. package/src/lib/json.ts +291 -32
  97. package/src/lib/workspace.ts +88 -100
  98. package/src/monorepo/__tests__/ci-workflow.test.ts +14 -3
  99. package/src/monorepo/__tests__/publish-workflow.test.ts +222 -1
  100. package/src/monorepo/ci-workflow.ts +28 -2
  101. package/src/monorepo/git-config.ts +2 -2
  102. package/src/monorepo/index.ts +6 -2
  103. package/src/monorepo/lockfile.test.ts +35 -36
  104. package/src/monorepo/lockfile.ts +48 -24
  105. package/src/monorepo/managed-files.test.ts +63 -0
  106. package/src/monorepo/managed-files.ts +94 -34
  107. package/src/monorepo/package-policy.test.ts +70 -51
  108. package/src/monorepo/package-policy.ts +104 -59
  109. package/src/monorepo/packed-manifest.ts +18 -52
  110. package/src/monorepo/packed-package.ts +43 -22
  111. package/src/monorepo/packs/index.test.ts +18 -1
  112. package/src/monorepo/packs/index.ts +20 -7
  113. package/src/monorepo/publish-workflow.ts +482 -3
  114. package/src/monorepo/runtime.test.ts +42 -3
  115. package/src/monorepo/runtime.ts +90 -12
  116. package/src/monorepo/tool-validation.ts +35 -25
  117. package/src/monorepo/wrangler.test.ts +11 -15
  118. package/src/monorepo/wrangler.ts +15 -15
  119. package/src/nx/index.test.ts +43 -0
  120. package/src/nx/index.ts +110 -23
  121. package/src/pr/index.ts +42 -36
  122. package/src/release/__tests__/candidates.test.ts +5 -4
  123. package/src/release/__tests__/fixture-repo.test.ts +18 -16
  124. package/src/release/__tests__/github-release.test.ts +11 -0
  125. package/src/release/__tests__/orchestration.test.ts +57 -2
  126. package/src/release/__tests__/trust-publisher.test.ts +98 -2
  127. package/src/release/candidates.ts +10 -17
  128. package/src/release/github-release.ts +12 -0
  129. package/src/release/index.ts +321 -112
  130. package/src/release/orchestration.ts +55 -3
  131. package/src/wrangler/prepare-env.ts +75 -54
  132. package/src/wrangler/scaffold.test.ts +9 -12
  133. package/src/wrangler/scaffold.ts +13 -13
@@ -30,14 +30,24 @@ export interface ReleaseNextShell<Package extends ReleasePackageInfo = ReleasePa
30
30
  bumpStablePackagesToNext(packages: Package[]): Promise<void>;
31
31
  }
32
32
 
33
- export interface ReleaseRepairShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
34
- extends ReleaseCompletionShell<Package> {
33
+ export interface ReleaseTargetCheckoutShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
35
34
  checkout(ref: string): Promise<void>;
36
35
  withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
37
36
  beforeRepairTarget?(target: ReleaseTarget<Package>): void;
38
37
  afterRepairTarget?(target: ReleaseTarget<Package>): void;
39
38
  }
40
39
 
40
+ export interface ReleaseRepairShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
41
+ extends ReleaseCompletionShell<Package>,
42
+ ReleaseTargetCheckoutShell<Package> {
43
+ prepareRepairTarget?(target: ReleaseTarget<Package>): Promise<void>;
44
+ }
45
+
46
+ export interface ReleaseRepairOutputsShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
47
+ extends ReleaseTargetCheckoutShell<Package> {
48
+ collectRepairTargetOutputs(target: ReleaseTarget<Package>): Promise<void>;
49
+ }
50
+
41
51
  export interface ReleaseVersionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
42
52
  releasePackagesAtHead(): Promise<Package[]>;
43
53
  releaseVersionPackages(bump: string): Promise<Package[]>;
@@ -151,17 +161,59 @@ export async function repairPendingTargets<Package extends ReleasePackageInfo>(
151
161
  }
152
162
  shell.beforeRepairTarget?.(target);
153
163
  try {
154
- summaries.push(await shell.withDevenvEnv(() => completeRepairTargetAtHead(shell, target, dryRun)));
164
+ summaries.push(
165
+ await shell.withDevenvEnv(async () => {
166
+ await shell.prepareRepairTarget?.(target);
167
+ return completeRepairTargetAtHead(shell, target, dryRun);
168
+ }),
169
+ );
155
170
  } finally {
156
171
  shell.afterRepairTarget?.(target);
157
172
  }
158
173
  }
159
174
  } finally {
160
175
  await shell.checkout(restoreRef);
176
+ if (targets.length > 0) {
177
+ // Loading devenv at a historical release can rebuild ignored tool outputs
178
+ // from that checkout. Refresh after restoring HEAD so later release phases
179
+ // cannot combine the current CLI with an older Nx plugin build.
180
+ await shell.withDevenvEnv(async () => undefined);
181
+ }
161
182
  }
162
183
  return summaries;
163
184
  }
164
185
 
186
+ export async function collectRepairPlatformOutputs<Package extends ReleasePackageInfo>(
187
+ shell: ReleaseRepairOutputsShell<Package>,
188
+ targets: Array<ReleaseTarget<Package>>,
189
+ restoreRef: string,
190
+ ): Promise<void> {
191
+ const outputTargets = targets.filter((target) => target.npmPackages.length > 0);
192
+ if (outputTargets.length === 0) {
193
+ return;
194
+ }
195
+ try {
196
+ for (const target of outputTargets) {
197
+ await shell.checkout(target.sha);
198
+ if (target.packages.length === 0) {
199
+ // invariant throw: release target discovery must never yield a package-free target.
200
+ throw new Error(`Release commit ${target.sha} has no release packages after checkout.`);
201
+ }
202
+ shell.beforeRepairTarget?.(target);
203
+ try {
204
+ await shell.withDevenvEnv(() => shell.collectRepairTargetOutputs(target));
205
+ } finally {
206
+ shell.afterRepairTarget?.(target);
207
+ }
208
+ }
209
+ } finally {
210
+ await shell.checkout(restoreRef);
211
+ // Historical devenv activation may rebuild ignored tool outputs. Restore the
212
+ // dispatch environment before the macOS job performs any further work.
213
+ await shell.withDevenvEnv(async () => undefined);
214
+ }
215
+ }
216
+
165
217
  function repairDryRunSummary<Package extends ReleasePackageInfo>(
166
218
  target: ReleaseTarget<Package>,
167
219
  ): ReleaseSummary<Package> {
@@ -20,13 +20,50 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { $ } from 'bun';
22
22
  import { type AST, getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
23
+ import typia from 'typia';
23
24
 
24
- // ── runtime narrowing (wrangler JSON is external data — no casts) ─────────────
25
+ // ── runtime narrowing (wrangler TOML/JSON is external data — no casts) ───────
25
26
 
26
- export function isRecord(v: unknown): v is Record<string, unknown> {
27
- return typeof v === 'object' && v !== null && !Array.isArray(v);
27
+ interface WranglerTomlRoot {
28
+ env?: Record<string, WranglerEnvBlock | undefined>;
28
29
  }
29
30
 
31
+ interface WranglerEnvBlock {
32
+ vars?: Record<string, unknown>;
33
+ kv_namespaces?: WranglerKvRow[];
34
+ d1_databases?: WranglerD1Row[];
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ interface WranglerKvRow {
39
+ binding?: string;
40
+ id?: string;
41
+ }
42
+
43
+ interface WranglerD1Row {
44
+ binding?: string;
45
+ database_id?: string;
46
+ database_name?: string;
47
+ }
48
+
49
+ interface WranglerSecretRow {
50
+ name?: string;
51
+ }
52
+
53
+ interface ShellErrorShape {
54
+ stderr?: unknown;
55
+ exitCode?: unknown;
56
+ }
57
+
58
+ const isWranglerTomlRoot = typia.createIs<WranglerTomlRoot>();
59
+ const isWranglerEnvBlock = typia.createIs<WranglerEnvBlock>();
60
+ const isWranglerKvRow = typia.createIs<WranglerKvRow>();
61
+ const isWranglerD1Row = typia.createIs<WranglerD1Row>();
62
+ const isWranglerSecretRow = typia.createIs<WranglerSecretRow>();
63
+ const isKvNamespaceRow = typia.createIs<{ id: string; title: string }>();
64
+ const isD1DatabaseRow = typia.createIs<{ uuid: string; name: string }>();
65
+ const isShellErrorShape = typia.createIs<ShellErrorShape>();
66
+
30
67
  // ── wrangler.toml editors (CST: read via getStaticTOMLValue, write via
31
68
  // node-range splice — comment/format-preserving by construction, no regex) ─────
32
69
 
@@ -36,15 +73,14 @@ function tables(toml: string): AST.TOMLTable[] {
36
73
  }
37
74
 
38
75
  /** 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 : {};
76
+ function tomlValue(toml: string): WranglerTomlRoot {
77
+ const value: unknown = getStaticTOMLValue(parseTOML(toml));
78
+ return isWranglerTomlRoot(value) ? value : {};
42
79
  }
43
80
 
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;
81
+ function envRecord(toml: string, env: string): WranglerEnvBlock | null {
82
+ const block = tomlValue(toml).env?.[env];
83
+ return isWranglerEnvBlock(block) ? block : null;
48
84
  }
49
85
 
50
86
  function sameKey(a: (string | number)[], b: (string | number)[]): boolean {
@@ -68,8 +104,13 @@ function arrayTableKv(
68
104
  field: string,
69
105
  ): AST.TOMLKeyValue | null {
70
106
  const block = envRecord(toml, env);
71
- const rows = isRecord(block) && Array.isArray(block[arrayKey]) ? block[arrayKey] : [];
72
- const index = rows.findIndex((r) => isRecord(r) && r.binding === binding);
107
+ const rows = Array.isArray(block?.[arrayKey]) ? block[arrayKey] : [];
108
+ const index = rows.findIndex((row) => {
109
+ if (arrayKey === 'd1_databases') {
110
+ return isWranglerD1Row(row) && row.binding === binding;
111
+ }
112
+ return isWranglerKvRow(row) && row.binding === binding;
113
+ });
73
114
  if (index < 0) return null;
74
115
  const table = tables(toml).find((t) => t.kind === 'array' && sameKey(t.resolvedKey, ['env', env, arrayKey, index]));
75
116
  return table ? kvIn(toml, table, field) : null;
@@ -88,7 +129,7 @@ function endOfLine(toml: string, offset: number): number {
88
129
  /** First `[env.<name>]` (source order; also derived from a nested `[env.<name>.*]`), or null when no env declared. */
89
130
  export function firstWranglerEnv(toml: string): string | null {
90
131
  const envs = tomlValue(toml).env;
91
- if (!isRecord(envs)) return null;
132
+ if (!envs) return null;
92
133
  const [first] = Object.keys(envs);
93
134
  return first ?? null;
94
135
  }
@@ -96,14 +137,12 @@ export function firstWranglerEnv(toml: string): string | null {
96
137
  /** True if the toml declares `[env.<env>]` (or any `[env.<env>.*]` sub-table). */
97
138
  export function hasEnvBlock(toml: string, env: string): boolean {
98
139
  const envs = tomlValue(toml).env;
99
- return isRecord(envs) && env in envs;
140
+ return !!envs && env in envs;
100
141
  }
101
142
 
102
143
  /** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
103
144
  export function getVar(toml: string, env: string, name: string): string | null {
104
- const block = envRecord(toml, env);
105
- const vars = isRecord(block?.vars) ? block.vars : {};
106
- const value = vars[name];
145
+ const value = envRecord(toml, env)?.vars?.[name];
107
146
  return typeof value === 'string' && value ? value : null;
108
147
  }
109
148
 
@@ -117,10 +156,8 @@ export function setVar(toml: string, env: string, name: string, value: string):
117
156
 
118
157
  /** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
119
158
  export function getKvId(toml: string, env: string, binding: string): string | null {
120
- const block = envRecord(toml, env);
121
- const rows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
122
- for (const row of rows) {
123
- if (isRecord(row) && row.binding === binding && typeof row.id === 'string') return row.id || null;
159
+ for (const row of envRecord(toml, env)?.kv_namespaces ?? []) {
160
+ if (row.binding === binding && typeof row.id === 'string') return row.id || null;
124
161
  }
125
162
  return null;
126
163
  }
@@ -166,21 +203,16 @@ export function blankKvIds(toml: string, env: string): string {
166
203
 
167
204
  /** The database_id written for `[[env.<env>.d1_databases]]` binding, or null if absent/empty. */
168
205
  export function getD1Id(toml: string, env: string, binding: string): string | null {
169
- const block = envRecord(toml, env);
170
- const rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
171
- for (const row of rows) {
172
- if (isRecord(row) && row.binding === binding && typeof row.database_id === 'string') return row.database_id || null;
206
+ for (const row of envRecord(toml, env)?.d1_databases ?? []) {
207
+ if (row.binding === binding && typeof row.database_id === 'string') return row.database_id || null;
173
208
  }
174
209
  return null;
175
210
  }
176
211
 
177
212
  /** The `database_name` for `[[env.<env>.d1_databases]]` binding, or null. Source of truth for which DB to create/reuse. */
178
213
  export function getD1Name(toml: string, env: string, binding: string): string | null {
179
- const block = envRecord(toml, env);
180
- const rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
181
- for (const row of rows) {
182
- if (isRecord(row) && row.binding === binding && typeof row.database_name === 'string')
183
- return row.database_name || null;
214
+ for (const row of envRecord(toml, env)?.d1_databases ?? []) {
215
+ if (row.binding === binding && typeof row.database_name === 'string') return row.database_name || null;
184
216
  }
185
217
  return null;
186
218
  }
@@ -260,12 +292,13 @@ export function parseDevVarsExample(text: string): string[] {
260
292
  export function readManifest(root: string, env: string): Manifest {
261
293
  const toml = readFileSync(join(root, 'wrangler.toml'), 'utf8');
262
294
  const block = envRecord(toml, env);
263
- const varsRec = isRecord(block?.vars) ? block.vars : {};
264
- const vars = Object.keys(varsRec);
265
- const kvRows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
266
- const kvBindings = kvRows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
267
- const d1Rows = isRecord(block) && Array.isArray(block.d1_databases) ? block.d1_databases : [];
268
- const d1Bindings = d1Rows.flatMap((row) => (isRecord(row) && typeof row.binding === 'string' ? [row.binding] : []));
295
+ const vars = Object.keys(block?.vars ?? {});
296
+ const kvBindings = (block?.kv_namespaces ?? []).flatMap((row) =>
297
+ typeof row.binding === 'string' ? [row.binding] : [],
298
+ );
299
+ const d1Bindings = (block?.d1_databases ?? []).flatMap((row) =>
300
+ typeof row.binding === 'string' ? [row.binding] : [],
301
+ );
269
302
  const examplePath = join(root, '.dev.vars.example');
270
303
  const secrets = existsSync(examplePath) ? parseDevVarsExample(readFileSync(examplePath, 'utf8')) : [];
271
304
  return { kvBindings, d1Bindings, vars, secrets };
@@ -300,25 +333,19 @@ export interface KvNamespace {
300
333
 
301
334
  function asKvNamespaces(json: unknown): KvNamespace[] {
302
335
  if (!Array.isArray(json)) return [];
303
- const out: KvNamespace[] = [];
304
- for (const row of json) {
305
- if (isRecord(row) && typeof row.id === 'string' && typeof row.title === 'string') {
306
- out.push({ id: row.id, title: row.title });
307
- }
308
- }
309
- return out;
336
+ return json.filter(isKvNamespaceRow);
310
337
  }
311
338
 
312
339
  function asSecretNames(json: unknown): string[] {
313
340
  if (!Array.isArray(json)) return [];
314
- return json.flatMap((row) => (isRecord(row) && typeof row.name === 'string' ? [row.name] : []));
341
+ return json.flatMap((row) => (isWranglerSecretRow(row) && typeof row.name === 'string' ? [row.name] : []));
315
342
  }
316
343
 
317
344
  /** Best-effort ERROR line from a Bun ShellError (thrown on non-zero exit). */
318
345
  export function errText(err: unknown): string {
319
- if (!isRecord(err)) return 'unknown error';
320
- const stderr = 'stderr' in err && err.stderr != null ? String(err.stderr) : '';
321
- const exit = 'exitCode' in err ? String(err.exitCode) : '?';
346
+ if (!isShellErrorShape(err)) return 'unknown error';
347
+ const stderr = err.stderr != null ? String(err.stderr) : '';
348
+ const exit = err.exitCode != null ? String(err.exitCode) : '?';
322
349
  return stderr.split('\n').find((l) => l.includes('ERROR')) ?? `exit ${exit}`;
323
350
  }
324
351
 
@@ -358,13 +385,7 @@ export interface D1Database {
358
385
 
359
386
  function asD1Databases(json: unknown): D1Database[] {
360
387
  if (!Array.isArray(json)) return [];
361
- const out: D1Database[] = [];
362
- for (const row of json) {
363
- if (isRecord(row) && typeof row.uuid === 'string' && typeof row.name === 'string') {
364
- out.push({ uuid: row.uuid, name: row.name });
365
- }
366
- }
367
- return out;
388
+ return json.filter(isD1DatabaseRow);
368
389
  }
369
390
 
370
391
  /** Live D1 databases on the account (`d1 list --json`); `[]` when offline/unauthenticated. */
@@ -2,6 +2,7 @@ import { describe, expect, it, spyOn } from 'bun:test';
2
2
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
+ import { type PackageJson, parsePackageJsonText } from '../lib/json.js';
5
6
  import { scaffold } from './scaffold.js';
6
7
 
7
8
  describe('scaffold', () => {
@@ -164,27 +165,23 @@ async function createWorkspace(
164
165
  return root;
165
166
  }
166
167
 
167
- function nxTargets(pkg: Record<string, unknown>): Record<string, unknown> {
168
- const nx = pkg.nx;
169
- if (!isRecord(nx) || !isRecord(nx.targets)) {
168
+ function nxTargets(pkg: PackageJson): NonNullable<NonNullable<PackageJson['nx']>['targets']> {
169
+ const targets = pkg.nx?.targets;
170
+ if (!targets) {
170
171
  throw new Error('nx.targets not found');
171
172
  }
172
- return nx.targets;
173
+ return targets;
173
174
  }
174
175
 
175
- function isRecord(value: unknown): value is Record<string, unknown> {
176
- return typeof value === 'object' && value !== null && !Array.isArray(value);
177
- }
178
-
179
- async function readJson(path: string): Promise<Record<string, unknown>> {
180
- const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
181
- if (!isRecord(parsed)) {
176
+ async function readJson(path: string): Promise<PackageJson> {
177
+ const parsed = parsePackageJsonText(await readFile(path, 'utf8'));
178
+ if (!parsed) {
182
179
  throw new Error('expected JSON object');
183
180
  }
184
181
  return parsed;
185
182
  }
186
183
 
187
- async function writeJson(path: string, value: Record<string, unknown>): Promise<void> {
184
+ async function writeJson(path: string, value: PackageJson): Promise<void> {
188
185
  await mkdir(join(path, '..'), { recursive: true });
189
186
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
190
187
  }
@@ -6,8 +6,8 @@
6
6
  * .dev.vars.example), so it never drifts from what the worker declares.
7
7
  */
8
8
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
9
- import { join, relative } from 'node:path';
10
- import { getOrCreateRecord, recordProperty, writeJsonObject } from '../lib/json.js';
9
+ import { join } from 'node:path';
10
+ import { ensureNx, ensureNxTargets, type NxTargetConfig, type PackageJson, writeJsonObject } from '../lib/json.js';
11
11
  import { getWorkspacePackageManifests } from '../lib/workspace.js';
12
12
 
13
13
  export interface ScaffoldOptions {
@@ -19,22 +19,21 @@ export interface ScaffoldOptions {
19
19
  function resolveProject(
20
20
  root: string,
21
21
  project: string,
22
- ): { dir: string; label: string; packageJsonPath: string; json: Record<string, unknown> } {
22
+ ): { dir: string; label: string; packageJsonPath: string; json: PackageJson } {
23
23
  const manifests = getWorkspacePackageManifests(root);
24
24
  const match = manifests.find((m) => {
25
- const nx = recordProperty(m.json, 'nx');
26
- const nxName = nx && typeof nx.name === 'string' ? nx.name : null;
27
- return nxName === project || m.name === project || relative(root, m.path) === project;
25
+ const nxName = m.json.nx?.name;
26
+ return nxName === project || m.name === project || m.path === project;
28
27
  });
29
28
  if (!match) {
30
- const known = manifests.map((m) => recordProperty(m.json, 'nx')?.name ?? m.name).join(', ');
29
+ const known = manifests.map((m) => m.json.nx?.name ?? m.name).join(', ');
31
30
  throw new Error(`project "${project}" not found. Known: ${known}`);
32
31
  }
33
- const dir = match.path; // WorkspacePackageManifest.path is absolute
32
+ const dir = join(root, match.path);
34
33
  if (!existsSync(join(dir, 'wrangler.toml'))) {
35
34
  throw new Error(`"${project}" has no wrangler.toml — not a wrangler project`);
36
35
  }
37
- return { dir, label: relative(root, dir), packageJsonPath: match.packageJsonPath, json: match.json };
36
+ return { dir, label: match.path, packageJsonPath: match.packageJsonPath, json: match.json };
38
37
  }
39
38
 
40
39
  /** The generated starter script — generic, manifest-driven, prompt-batteries included. */
@@ -226,13 +225,14 @@ export function scaffold(root: string, project: string, options: ScaffoldOptions
226
225
  console.log(`${existed ? 'overwrote' : 'created'} ${label}/scripts/prepare-env.ts`);
227
226
 
228
227
  // Wire the nx target (idempotent).
229
- const nx = getOrCreateRecord(json, 'nx');
230
- const targets = getOrCreateRecord(nx, 'targets');
231
- if (!recordProperty(targets, 'prepare-env')) {
232
- targets['prepare-env'] = {
228
+ const nx = ensureNx(json);
229
+ const targets = ensureNxTargets(nx);
230
+ if (!targets['prepare-env']) {
231
+ const prepareEnvTarget: NxTargetConfig = {
233
232
  executor: 'nx:run-commands',
234
233
  options: { command: 'bun scripts/prepare-env.ts', cwd: '{projectRoot}' },
235
234
  };
235
+ targets['prepare-env'] = prepareEnvTarget;
236
236
  writeJsonObject(packageJsonPath, json);
237
237
  console.log(`updated ${label}/package.json prepare-env target`);
238
238
  }