@smoothbricks/cli 0.10.3 → 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 (111) hide show
  1. package/README.md +9 -13
  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 +12 -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.map +1 -1
  9. package/dist/github-ci/index.js +52 -16
  10. package/dist/github-ci/outputs.d.ts.map +1 -1
  11. package/dist/github-ci/outputs.js +1 -2
  12. package/dist/lib/cli-package.js +2 -2
  13. package/dist/lib/json.d.ts +118 -11
  14. package/dist/lib/json.d.ts.map +1 -1
  15. package/dist/lib/json.js +317 -29
  16. package/dist/lib/workspace.d.ts +7 -14
  17. package/dist/lib/workspace.d.ts.map +1 -1
  18. package/dist/lib/workspace.js +76 -82
  19. package/dist/monorepo/ci-workflow.d.ts +2 -0
  20. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  21. package/dist/monorepo/ci-workflow.js +27 -1
  22. package/dist/monorepo/git-config.js +2 -2
  23. package/dist/monorepo/index.d.ts +3 -1
  24. package/dist/monorepo/index.d.ts.map +1 -1
  25. package/dist/monorepo/index.js +6 -2
  26. package/dist/monorepo/lockfile.d.ts +13 -1
  27. package/dist/monorepo/lockfile.d.ts.map +1 -1
  28. package/dist/monorepo/lockfile.js +33 -21
  29. package/dist/monorepo/managed-files.d.ts +11 -0
  30. package/dist/monorepo/managed-files.d.ts.map +1 -1
  31. package/dist/monorepo/managed-files.js +52 -32
  32. package/dist/monorepo/package-policy.d.ts.map +1 -1
  33. package/dist/monorepo/package-policy.js +52 -45
  34. package/dist/monorepo/packed-manifest.d.ts +5 -2
  35. package/dist/monorepo/packed-manifest.d.ts.map +1 -1
  36. package/dist/monorepo/packed-manifest.js +16 -47
  37. package/dist/monorepo/packed-package.d.ts.map +1 -1
  38. package/dist/monorepo/packed-package.js +28 -16
  39. package/dist/monorepo/packs/index.d.ts.map +1 -1
  40. package/dist/monorepo/packs/index.js +13 -4
  41. package/dist/monorepo/publish-workflow.js +3 -1
  42. package/dist/monorepo/runtime.d.ts +15 -0
  43. package/dist/monorepo/runtime.d.ts.map +1 -1
  44. package/dist/monorepo/runtime.js +66 -12
  45. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  46. package/dist/monorepo/tool-validation.js +32 -23
  47. package/dist/monorepo/wrangler.d.ts.map +1 -1
  48. package/dist/monorepo/wrangler.js +13 -13
  49. package/dist/nx/index.d.ts +9 -7
  50. package/dist/nx/index.d.ts.map +1 -1
  51. package/dist/nx/index.js +48 -31
  52. package/dist/pr/index.d.ts.map +1 -1
  53. package/dist/pr/index.js +28 -32
  54. package/dist/release/candidates.d.ts +3 -2
  55. package/dist/release/candidates.d.ts.map +1 -1
  56. package/dist/release/candidates.js +4 -5
  57. package/dist/release/index.d.ts +1 -0
  58. package/dist/release/index.d.ts.map +1 -1
  59. package/dist/release/index.js +82 -59
  60. package/dist/wrangler/prepare-env.d.ts +0 -1
  61. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  62. package/dist/wrangler/prepare-env.js +100 -51
  63. package/dist/wrangler/scaffold.d.ts.map +1 -1
  64. package/dist/wrangler/scaffold.js +12 -12
  65. package/managed/raw/git-format-staged.yml +19 -1
  66. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +2 -1
  67. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  68. package/managed/raw/tooling/direnv/setup-environment.ts +150 -19
  69. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  70. package/managed/templates/github/actions/cache-node-modules/action.yml +5 -2
  71. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  72. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  73. package/managed/templates/github/workflows/managed-files.yml +80 -0
  74. package/package.json +15 -4
  75. package/src/bun/preload.ts +12 -0
  76. package/src/cli.ts +19 -7
  77. package/src/generate/index.ts +0 -6
  78. package/src/github-ci/index.ts +49 -18
  79. package/src/github-ci/outputs.ts +1 -2
  80. package/src/lib/cli-package.ts +2 -2
  81. package/src/lib/json.ts +291 -32
  82. package/src/lib/workspace.ts +88 -100
  83. package/src/monorepo/__tests__/ci-workflow.test.ts +3 -3
  84. package/src/monorepo/ci-workflow.ts +27 -1
  85. package/src/monorepo/git-config.ts +2 -2
  86. package/src/monorepo/index.ts +6 -2
  87. package/src/monorepo/lockfile.test.ts +35 -36
  88. package/src/monorepo/lockfile.ts +48 -24
  89. package/src/monorepo/managed-files.test.ts +1 -0
  90. package/src/monorepo/managed-files.ts +57 -38
  91. package/src/monorepo/package-policy.test.ts +20 -50
  92. package/src/monorepo/package-policy.ts +76 -59
  93. package/src/monorepo/packed-manifest.ts +18 -52
  94. package/src/monorepo/packed-package.ts +43 -22
  95. package/src/monorepo/packs/index.ts +13 -4
  96. package/src/monorepo/publish-workflow.ts +3 -1
  97. package/src/monorepo/runtime.test.ts +42 -3
  98. package/src/monorepo/runtime.ts +90 -12
  99. package/src/monorepo/tool-validation.ts +35 -25
  100. package/src/monorepo/wrangler.test.ts +11 -15
  101. package/src/monorepo/wrangler.ts +15 -15
  102. package/src/nx/index.test.ts +9 -5
  103. package/src/nx/index.ts +69 -42
  104. package/src/pr/index.ts +42 -36
  105. package/src/release/__tests__/candidates.test.ts +5 -4
  106. package/src/release/__tests__/trust-publisher.test.ts +7 -0
  107. package/src/release/candidates.ts +10 -17
  108. package/src/release/index.ts +104 -73
  109. package/src/wrangler/prepare-env.ts +75 -54
  110. package/src/wrangler/scaffold.test.ts +9 -12
  111. package/src/wrangler/scaffold.ts +13 -13
@@ -20,10 +20,78 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { $ } from 'bun';
22
22
  import { getStaticTOMLValue, parseTOML } from 'toml-eslint-parser';
23
- // ── runtime narrowing (wrangler JSON is external data — no casts) ─────────────
24
- export function isRecord(v) {
25
- return typeof v === 'object' && v !== null && !Array.isArray(v);
26
- }
23
+ import typia from 'typia';
24
+ const isWranglerTomlRoot = (() => {
25
+ const _ip0 = input => undefined === input["binding"] || "string" === typeof input["binding"];
26
+ const _io0 = input => undefined === input.env || "object" === typeof input.env && null !== input.env && false === Array.isArray(input.env) && _io1(input.env);
27
+ const _io1 = input => Object.keys(input).every(key => {
28
+ const value = input[key];
29
+ if (undefined === value)
30
+ return true;
31
+ return undefined === value || "object" === typeof value && null !== value && false === Array.isArray(value) && _io2(value);
32
+ });
33
+ const _io2 = input => (undefined === input.vars || "object" === typeof input.vars && null !== input.vars && false === Array.isArray(input.vars) && _io3(input.vars)) && (undefined === input.kv_namespaces || Array.isArray(input.kv_namespaces) && input.kv_namespaces.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io4(elem))) && (undefined === input.d1_databases || Array.isArray(input.d1_databases) && input.d1_databases.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io5(elem))) && Object.keys(input).every(key => {
34
+ if (["vars", "kv_namespaces", "d1_databases"].some(prop => key === prop))
35
+ return true;
36
+ const value = input[key];
37
+ if (undefined === value)
38
+ return true;
39
+ return true;
40
+ });
41
+ const _io3 = input => Object.keys(input).every(key => {
42
+ const value = input[key];
43
+ if (undefined === value)
44
+ return true;
45
+ return true;
46
+ });
47
+ const _io4 = input => _ip0(input) && (undefined === input.id || "string" === typeof input.id);
48
+ const _io5 = input => _ip0(input) && (undefined === input.database_id || "string" === typeof input.database_id) && (undefined === input.database_name || "string" === typeof input.database_name);
49
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
50
+ })();
51
+ const isWranglerEnvBlock = (() => {
52
+ const _ip0 = input => undefined === input["binding"] || "string" === typeof input["binding"];
53
+ const _io0 = input => (undefined === input.vars || "object" === typeof input.vars && null !== input.vars && false === Array.isArray(input.vars) && _io1(input.vars)) && (undefined === input.kv_namespaces || Array.isArray(input.kv_namespaces) && input.kv_namespaces.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io2(elem))) && (undefined === input.d1_databases || Array.isArray(input.d1_databases) && input.d1_databases.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io3(elem))) && Object.keys(input).every(key => {
54
+ if (["vars", "kv_namespaces", "d1_databases"].some(prop => key === prop))
55
+ return true;
56
+ const value = input[key];
57
+ if (undefined === value)
58
+ return true;
59
+ return true;
60
+ });
61
+ const _io1 = input => Object.keys(input).every(key => {
62
+ const value = input[key];
63
+ if (undefined === value)
64
+ return true;
65
+ return true;
66
+ });
67
+ const _io2 = input => _ip0(input) && (undefined === input.id || "string" === typeof input.id);
68
+ const _io3 = input => _ip0(input) && (undefined === input.database_id || "string" === typeof input.database_id) && (undefined === input.database_name || "string" === typeof input.database_name);
69
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
70
+ })();
71
+ const isWranglerKvRow = (() => {
72
+ const _io0 = input => (undefined === input.binding || "string" === typeof input.binding) && (undefined === input.id || "string" === typeof input.id);
73
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
74
+ })();
75
+ const isWranglerD1Row = (() => {
76
+ const _io0 = input => (undefined === input.binding || "string" === typeof input.binding) && (undefined === input.database_id || "string" === typeof input.database_id) && (undefined === input.database_name || "string" === typeof input.database_name);
77
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
78
+ })();
79
+ const isWranglerSecretRow = (() => {
80
+ const _io0 = input => undefined === input.name || "string" === typeof input.name;
81
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
82
+ })();
83
+ const isKvNamespaceRow = (() => {
84
+ const _io0 = input => "string" === typeof input.id && "string" === typeof input.title;
85
+ return input => "object" === typeof input && null !== input && _io0(input);
86
+ })();
87
+ const isD1DatabaseRow = (() => {
88
+ const _io0 = input => "string" === typeof input.uuid && "string" === typeof input.name;
89
+ return input => "object" === typeof input && null !== input && _io0(input);
90
+ })();
91
+ const isShellErrorShape = (() => {
92
+ const _io0 = input => true && true;
93
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
94
+ })();
27
95
  // ── wrangler.toml editors (CST: read via getStaticTOMLValue, write via
28
96
  // node-range splice — comment/format-preserving by construction, no regex) ─────
29
97
  /** All top-level tables (`[env.x]`, `[env.x.vars]`, `[[env.x.d1_databases]]`, …) — each carries `resolvedKey` (incl. array index). */
@@ -33,12 +101,11 @@ function tables(toml) {
33
101
  /** Whole doc as a JS object (comments/format dropped — reads only). */
34
102
  function tomlValue(toml) {
35
103
  const value = getStaticTOMLValue(parseTOML(toml));
36
- return isRecord(value) ? value : {};
104
+ return isWranglerTomlRoot(value) ? value : {};
37
105
  }
38
106
  function envRecord(toml, env) {
39
- const envs = tomlValue(toml).env;
40
- const block = isRecord(envs) ? envs[env] : undefined;
41
- return isRecord(block) ? block : null;
107
+ const block = tomlValue(toml).env?.[env];
108
+ return isWranglerEnvBlock(block) ? block : null;
42
109
  }
43
110
  function sameKey(a, b) {
44
111
  return a.length === b.length && a.every((x, i) => x === b[i]);
@@ -54,8 +121,13 @@ function kvIn(toml, table, key) {
54
121
  /** The `<field>` KV of the `[[env.<env>.<arrayKey>]]` table whose `binding` matches, or null. */
55
122
  function arrayTableKv(toml, env, arrayKey, binding, field) {
56
123
  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);
124
+ const rows = Array.isArray(block?.[arrayKey]) ? block[arrayKey] : [];
125
+ const index = rows.findIndex((row) => {
126
+ if (arrayKey === 'd1_databases') {
127
+ return isWranglerD1Row(row) && row.binding === binding;
128
+ }
129
+ return isWranglerKvRow(row) && row.binding === binding;
130
+ });
59
131
  if (index < 0)
60
132
  return null;
61
133
  const table = tables(toml).find((t) => t.kind === 'array' && sameKey(t.resolvedKey, ['env', env, arrayKey, index]));
@@ -72,7 +144,7 @@ function endOfLine(toml, offset) {
72
144
  /** First `[env.<name>]` (source order; also derived from a nested `[env.<name>.*]`), or null when no env declared. */
73
145
  export function firstWranglerEnv(toml) {
74
146
  const envs = tomlValue(toml).env;
75
- if (!isRecord(envs))
147
+ if (!envs)
76
148
  return null;
77
149
  const [first] = Object.keys(envs);
78
150
  return first ?? null;
@@ -80,13 +152,11 @@ export function firstWranglerEnv(toml) {
80
152
  /** True if the toml declares `[env.<env>]` (or any `[env.<env>.*]` sub-table). */
81
153
  export function hasEnvBlock(toml, env) {
82
154
  const envs = tomlValue(toml).env;
83
- return isRecord(envs) && env in envs;
155
+ return !!envs && env in envs;
84
156
  }
85
157
  /** A `[env.<env>.vars]` value, or null if unset/empty. Reads committed public config. */
86
158
  export function getVar(toml, env, name) {
87
- const block = envRecord(toml, env);
88
- const vars = isRecord(block?.vars) ? block.vars : {};
89
- const value = vars[name];
159
+ const value = envRecord(toml, env)?.vars?.[name];
90
160
  return typeof value === 'string' && value ? value : null;
91
161
  }
92
162
  /** Set a `[env.<env>.vars]` value (JSON-encoded), preserving any trailing comment. Throws if the key is absent (scaffold first). */
@@ -99,10 +169,8 @@ export function setVar(toml, env, name, value) {
99
169
  }
100
170
  /** The id written for `[[env.<env>.kv_namespaces]]` binding, or null if absent/empty. */
101
171
  export function getKvId(toml, env, binding) {
102
- const block = envRecord(toml, env);
103
- const rows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
104
- for (const row of rows) {
105
- if (isRecord(row) && row.binding === binding && typeof row.id === 'string')
172
+ for (const row of envRecord(toml, env)?.kv_namespaces ?? []) {
173
+ if (row.binding === binding && typeof row.id === 'string')
106
174
  return row.id || null;
107
175
  }
108
176
  return null;
@@ -148,20 +216,16 @@ export function blankKvIds(toml, env) {
148
216
  }
149
217
  /** The database_id written for `[[env.<env>.d1_databases]]` binding, or null if absent/empty. */
150
218
  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')
219
+ for (const row of envRecord(toml, env)?.d1_databases ?? []) {
220
+ if (row.binding === binding && typeof row.database_id === 'string')
155
221
  return row.database_id || null;
156
222
  }
157
223
  return null;
158
224
  }
159
225
  /** The `database_name` for `[[env.<env>.d1_databases]]` binding, or null. Source of truth for which DB to create/reuse. */
160
226
  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')
227
+ for (const row of envRecord(toml, env)?.d1_databases ?? []) {
228
+ if (row.binding === binding && typeof row.database_name === 'string')
165
229
  return row.database_name || null;
166
230
  }
167
231
  return null;
@@ -222,12 +286,9 @@ export function parseDevVarsExample(text) {
222
286
  export function readManifest(root, env) {
223
287
  const toml = readFileSync(join(root, 'wrangler.toml'), 'utf8');
224
288
  const block = envRecord(toml, env);
225
- const varsRec = isRecord(block?.vars) ? block.vars : {};
226
- const vars = Object.keys(varsRec);
227
- const kvRows = isRecord(block) && Array.isArray(block.kv_namespaces) ? block.kv_namespaces : [];
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] : []));
289
+ const vars = Object.keys(block?.vars ?? {});
290
+ const kvBindings = (block?.kv_namespaces ?? []).flatMap((row) => typeof row.binding === 'string' ? [row.binding] : []);
291
+ const d1Bindings = (block?.d1_databases ?? []).flatMap((row) => typeof row.binding === 'string' ? [row.binding] : []);
231
292
  const examplePath = join(root, '.dev.vars.example');
232
293
  const secrets = existsSync(examplePath) ? parseDevVarsExample(readFileSync(examplePath, 'utf8')) : [];
233
294
  return { kvBindings, d1Bindings, vars, secrets };
@@ -252,25 +313,19 @@ export function looksLikeFileSecret(name) {
252
313
  function asKvNamespaces(json) {
253
314
  if (!Array.isArray(json))
254
315
  return [];
255
- const out = [];
256
- for (const row of json) {
257
- if (isRecord(row) && typeof row.id === 'string' && typeof row.title === 'string') {
258
- out.push({ id: row.id, title: row.title });
259
- }
260
- }
261
- return out;
316
+ return json.filter(isKvNamespaceRow);
262
317
  }
263
318
  function asSecretNames(json) {
264
319
  if (!Array.isArray(json))
265
320
  return [];
266
- return json.flatMap((row) => (isRecord(row) && typeof row.name === 'string' ? [row.name] : []));
321
+ return json.flatMap((row) => (isWranglerSecretRow(row) && typeof row.name === 'string' ? [row.name] : []));
267
322
  }
268
323
  /** Best-effort ERROR line from a Bun ShellError (thrown on non-zero exit). */
269
324
  export function errText(err) {
270
- if (!isRecord(err))
325
+ if (!isShellErrorShape(err))
271
326
  return 'unknown error';
272
- const stderr = 'stderr' in err && err.stderr != null ? String(err.stderr) : '';
273
- const exit = 'exitCode' in err ? String(err.exitCode) : '?';
327
+ const stderr = err.stderr != null ? String(err.stderr) : '';
328
+ const exit = err.exitCode != null ? String(err.exitCode) : '?';
274
329
  return stderr.split('\n').find((l) => l.includes('ERROR')) ?? `exit ${exit}`;
275
330
  }
276
331
  /** Live KV namespaces on the account (`kv namespace list` emits a pure JSON array); `[]` when offline/unauthenticated. */
@@ -305,13 +360,7 @@ async function reList(logical, cwd) {
305
360
  function asD1Databases(json) {
306
361
  if (!Array.isArray(json))
307
362
  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;
363
+ return json.filter(isD1DatabaseRow);
315
364
  }
316
365
  /** Live D1 databases on the account (`d1 list --json`); `[]` when offline/unauthenticated. */
317
366
  export async function listD1Databases(cwd) {
@@ -1 +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;AAsMD,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CA6B7F"}
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;AAqMD,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CA8B7F"}
@@ -6,26 +6,25 @@
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, writeJsonObject } from '../lib/json.js';
11
11
  import { getWorkspacePackageManifests } from '../lib/workspace.js';
12
12
  /** Locate a workspace package by nx name, package name, or relative path; must have a wrangler.toml. */
13
13
  function resolveProject(root, project) {
14
14
  const manifests = getWorkspacePackageManifests(root);
15
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;
16
+ const nxName = m.json.nx?.name;
17
+ return nxName === project || m.name === project || m.path === project;
19
18
  });
20
19
  if (!match) {
21
- const known = manifests.map((m) => recordProperty(m.json, 'nx')?.name ?? m.name).join(', ');
20
+ const known = manifests.map((m) => m.json.nx?.name ?? m.name).join(', ');
22
21
  throw new Error(`project "${project}" not found. Known: ${known}`);
23
22
  }
24
- const dir = match.path; // WorkspacePackageManifest.path is absolute
23
+ const dir = join(root, match.path);
25
24
  if (!existsSync(join(dir, 'wrangler.toml'))) {
26
25
  throw new Error(`"${project}" has no wrangler.toml — not a wrangler project`);
27
26
  }
28
- return { dir, label: relative(root, dir), packageJsonPath: match.packageJsonPath, json: match.json };
27
+ return { dir, label: match.path, packageJsonPath: match.packageJsonPath, json: match.json };
29
28
  }
30
29
  /** The generated starter script — generic, manifest-driven, prompt-batteries included. */
31
30
  function renderScript(projectName) {
@@ -213,13 +212,14 @@ export function scaffold(root, project, options = {}) {
213
212
  writeFileSync(scriptPath, renderScript(project));
214
213
  console.log(`${existed ? 'overwrote' : 'created'} ${label}/scripts/prepare-env.ts`);
215
214
  // 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'] = {
215
+ const nx = ensureNx(json);
216
+ const targets = ensureNxTargets(nx);
217
+ if (!targets['prepare-env']) {
218
+ const prepareEnvTarget = {
220
219
  executor: 'nx:run-commands',
221
220
  options: { command: 'bun scripts/prepare-env.ts', cwd: '{projectRoot}' },
222
221
  };
222
+ targets['prepare-env'] = prepareEnvTarget;
223
223
  writeJsonObject(packageJsonPath, json);
224
224
  console.log(`updated ${label}/package.json prepare-env target`);
225
225
  }
@@ -8,12 +8,16 @@ formatters:
8
8
  - '*.astro'
9
9
  - '!.*' # Exclude hidden files
10
10
 
11
- # Biome for JS/TS/JSX/TSX/JSON/HTML/CSS/GraphQL files
11
+ # Biome for JS/TS family (including CommonJS/CTS and ESM .mjs/.mts)
12
12
  biome:
13
13
  command: "biome check --files-ignore-unknown=true --use-editorconfig=true '--stdin-file-path={}' --fix"
14
14
  patterns:
15
15
  - '*.js'
16
+ - '*.cjs'
17
+ - '*.mjs'
16
18
  - '*.ts'
19
+ - '*.cts'
20
+ - '*.mts'
17
21
  - '*.jsx'
18
22
  - '*.tsx'
19
23
  - '*.json'
@@ -30,7 +34,11 @@ formatters:
30
34
  - '*'
31
35
  # Exclude files handled by Biome
32
36
  - '!*.js'
37
+ - '!*.cjs'
38
+ - '!*.mjs'
33
39
  - '!*.ts'
40
+ - '!*.cts'
41
+ - '!*.mts'
34
42
  - '!*.jsx'
35
43
  - '!*.tsx'
36
44
  - '!*.json'
@@ -38,15 +46,25 @@ formatters:
38
46
  - '!*.html'
39
47
  - '!*.css'
40
48
  - '!*.graphql'
49
+ # Exclude files handled by rustfmt
50
+ - '!*.rs'
41
51
  # Exclude files handled by Alejandra
42
52
  - '!*.nix'
43
53
  # Bun lockfiles are generated and should not be rewritten by Prettier
44
54
  - '!bun.lock'
55
+ # Cargo lockfiles are generated and should not be rewritten by Prettier
56
+ - '!Cargo.lock'
45
57
  # Binary artifacts: Prettier reads them as UTF-8 and can corrupt them
46
58
  # by replacing invalid byte sequences.
47
59
  - '!*.wasm'
48
60
  - '!*.wasm.sha256'
49
61
 
62
+ # rustfmt for Rust files
63
+ rustfmt:
64
+ command: 'rustfmt --edition 2024'
65
+ patterns:
66
+ - '*.rs'
67
+
50
68
  # Alejandra for Nix files
51
69
  alejandra:
52
70
  command: 'alejandra'
@@ -46,10 +46,11 @@ install_devenv() {
46
46
 
47
47
  build_devenv_shell() {
48
48
  devenv shell --verbose -- date
49
- # enterShell exports this for the bootstrap itself; persist it for the
49
+ # enterShell exports these for the bootstrap itself; persist them for the
50
50
  # independent workflow step shells that run after this composite action.
51
51
  if [ -n "${GITHUB_ENV:-}" ]; then
52
52
  echo "TTSC_TSGO_BINARY=$repo_root/node_modules/@typescript/native/bin/tsc" >> "$GITHUB_ENV"
53
+ echo "TTSC_CACHE_DIR=$repo_root/.cache/ttsc" >> "$GITHUB_ENV"
53
54
  fi
54
55
  # Add repo-local tools only after the shell exists; cleanup steps use an
55
56
  # explicit PATH because failures before this point must still refresh caches.
@@ -4,9 +4,9 @@
4
4
  # Runtime version pins (nvfetcher overlay _sources/generated.{json,nix}) and the
5
5
  # devenv/flake lock (devenv.lock) frequently differ across branches or across a
6
6
  # mirror sync's `git am --3way`. Instead of stalling on a conflict, keep the
7
- # higher pin wholesale so the tree always converges to the newest runtime; the
8
- # next `devenv shell` then regenerates package.json engine/packageManager from
9
- # it (see monorepo/runtime.ts syncRootRuntimeVersions).
7
+ # higher pin wholesale so the tree always converges to the newest runtime.
8
+ # Repair package.json engine/packageManager pins explicitly with
9
+ # `smoo monorepo init --runtime-only` when needed.
10
10
  #
11
11
  # Wired by `smoo` (managed .gitattributes -> merge=smoo-newer-pins, and
12
12
  # `git config merge.smoo-newer-pins.driver` installed by applyWorkspaceGitConfig).
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
3
3
  import { mkdir, rmdir, stat } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
4
5
  import path from 'node:path';
5
6
  import { $ } from 'bun';
6
7
 
7
8
  const devenvRoot = process.env.DEVENV_ROOT;
8
9
  const projectRoot = path.resolve(`${devenvRoot}/../..`);
9
- const startedWithoutNodeModules = !existsSync(path.join(projectRoot, 'node_modules'));
10
10
 
11
11
  // A legitimate concurrent setup finishes well within this; anything older is a
12
12
  // leftover from an interrupted run (CTRL-C/kill before the finally-cleanup) and
@@ -14,6 +14,13 @@ const startedWithoutNodeModules = !existsSync(path.join(projectRoot, 'node_modul
14
14
  // NOTE: must be declared ABOVE the top-level setup block below — module consts
15
15
  // are not hoisted, and the lock loop runs during that block.
16
16
  const STALE_LOCK_MS = 10 * 60_000;
17
+ // Unscoped require("typescript") must expose the TS6 compiler API for Nx.
18
+ // @typescript/native installs another package also named "typescript" (TS7). Bun's
19
+ // isolated linker often points node_modules/.bun/node_modules/typescript at TS7,
20
+ // so packages resolved from the .bun store (nx) get version.cjs without readConfigFile.
21
+ // After every install, force both unscoped typescript slots onto typescript@6.0.3.
22
+ // Keep @typescript/native for ttsc via TTSC_TSGO_BINARY. See https://github.com/oven-sh/bun/issues/33834.
23
+ const TYPESCRIPT_API_VERSION = '6.0.3';
17
24
 
18
25
  class CapturedCommandError extends Error {
19
26
  constructor(
@@ -30,7 +37,9 @@ class CapturedCommandError extends Error {
30
37
  process.chdir(projectRoot);
31
38
 
32
39
  try {
33
- // Install dependencies first so node_modules/.bin tools are available
40
+ // Bootstrap only: install deps + wire local git hooks/config.
41
+ // Do not import workspace packages here — this script is what installs them,
42
+ // and package resolution/Typia transforms are not available yet.
34
43
  if (process.env.CI) {
35
44
  try {
36
45
  await runSetupCommand('bun install --frozen-lockfile', $`bun install --frozen-lockfile`, { quiet: false });
@@ -46,20 +55,9 @@ try {
46
55
  await installLocalDependencies();
47
56
  }
48
57
 
49
- // Bun selects its resolver mode at process startup. If this process started
50
- // before node_modules existed, imports below can stay in auto-install mode
51
- // even after bun install succeeds, so restart once into the real workspace.
52
- if (startedWithoutNodeModules) {
53
- await runSetupCommand('restart setup-environment.ts', $`bun --no-install ${import.meta.path}`, { quiet: false });
54
- process.exit(0);
55
- }
56
-
57
- if (!process.env.CI) {
58
- const { syncRootRuntimeVersions } = await import('@smoothbricks/cli/monorepo/runtime');
59
- await syncRootRuntimeVersions(projectRoot);
60
- }
58
+ // Pin unscoped typescript API 6 for root and Bun's shared .bun hoist (Nx).
59
+ ensureTypeScriptApiPackage(projectRoot);
61
60
 
62
- const { applyWorkspaceGitConfig } = await import('@smoothbricks/cli/monorepo/git-config');
63
61
  await applyWorkspaceGitConfig(projectRoot);
64
62
  } catch (error) {
65
63
  if (error instanceof CapturedCommandError) {
@@ -74,14 +72,147 @@ try {
74
72
  }
75
73
 
76
74
  async function installLocalDependencies(): Promise<void> {
77
- // bun install runs the root prepare script, which patches TypeScript with
78
- // ts-patch. Multiple concurrent direnv activations can otherwise race while
79
- // mutating the same files under node_modules.
75
+ // bun install runs the root prepare script. Multiple concurrent direnv
76
+ // activations can otherwise race while mutating the same files under
77
+ // node_modules.
80
78
  await withSetupLock(async () => {
81
79
  await runSetupCommand('bun install --no-summary', $`bun install --no-summary`);
82
80
  });
83
81
  }
84
82
 
83
+ function ensureTypeScriptApiPackage(root: string): void {
84
+ const apiPackageRoot = findInstalledTypeScriptApiPackage(root);
85
+ if (!apiPackageRoot) {
86
+ throw new Error(
87
+ `typescript@${TYPESCRIPT_API_VERSION} was not installed under node_modules/.bun. ` +
88
+ `Keep root devDependency typescript@^${TYPESCRIPT_API_VERSION} (compiler API for Nx).`,
89
+ );
90
+ }
91
+
92
+ // Root bare require("typescript") and Bun store-local requires (nx lives under
93
+ // node_modules/.bun/...) must both see the API package — not @typescript/native's TS7.
94
+ forceSymlink(path.join(root, 'node_modules', 'typescript'), apiPackageRoot);
95
+ forceSymlink(path.join(root, 'node_modules', '.bun', 'node_modules', 'typescript'), apiPackageRoot);
96
+
97
+ assertTypescriptApiAt(path.join(root, 'node_modules', 'typescript'), apiPackageRoot);
98
+ assertTypescriptApiAt(path.join(root, 'node_modules', '.bun', 'node_modules', 'typescript'), apiPackageRoot);
99
+
100
+ const nativeBin = path.join(root, 'node_modules', '@typescript', 'native', 'bin', 'tsc');
101
+ if (!existsSync(nativeBin)) {
102
+ throw new Error(`Missing ${nativeBin}. @typescript/native must remain the TypeScript 7 native compiler for ttsc.`);
103
+ }
104
+ }
105
+
106
+ function findInstalledTypeScriptApiPackage(root: string): string | null {
107
+ const bunStore = path.join(root, 'node_modules', '.bun');
108
+ if (!existsSync(bunStore)) {
109
+ return null;
110
+ }
111
+ const exact = `typescript@${TYPESCRIPT_API_VERSION}`;
112
+ const names = readdirSync(bunStore)
113
+ .filter((name) => name === exact || name.startsWith(`${exact}+`) || name.startsWith('typescript@6.'))
114
+ .sort((a, b) => {
115
+ const aExact = a === exact || a.startsWith(`${exact}+`) ? 0 : 1;
116
+ const bExact = b === exact || b.startsWith(`${exact}+`) ? 0 : 1;
117
+ return aExact - bExact || a.localeCompare(b);
118
+ });
119
+ for (const name of names) {
120
+ const candidate = path.join(bunStore, name, 'node_modules', 'typescript');
121
+ if (existsSync(path.join(candidate, 'package.json'))) {
122
+ return candidate;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function forceSymlink(linkPath: string, targetPath: string): void {
129
+ mkdirSync(path.dirname(linkPath), { recursive: true });
130
+ const relativeTarget = path.relative(path.dirname(linkPath), targetPath);
131
+ let current: string | null = null;
132
+ try {
133
+ current = readlinkSync(linkPath);
134
+ } catch {
135
+ current = null;
136
+ }
137
+ if (
138
+ current === relativeTarget ||
139
+ (current !== null && path.resolve(path.dirname(linkPath), current) === targetPath)
140
+ ) {
141
+ return;
142
+ }
143
+ rmSync(linkPath, { recursive: true, force: true });
144
+ symlinkSync(relativeTarget, linkPath);
145
+ }
146
+
147
+ function assertTypescriptApiAt(typescriptRoot: string, expectedTarget: string): void {
148
+ if (!existsSync(typescriptRoot)) {
149
+ throw new Error(`Missing ${typescriptRoot} after linking TypeScript ${TYPESCRIPT_API_VERSION} API package`);
150
+ }
151
+ const requireFromInstall = createRequire(path.join(typescriptRoot, 'package.json'));
152
+ const typed = requireFromInstall(typescriptRoot) as { version?: string; readConfigFile?: unknown };
153
+ if (typeof typed.readConfigFile !== 'function' || typed.version !== TYPESCRIPT_API_VERSION) {
154
+ throw new Error(
155
+ `${typescriptRoot} must export TypeScript ${TYPESCRIPT_API_VERSION} compiler API (readConfigFile). ` +
156
+ `Resolved version ${typed.version ?? 'unknown'} (expected link target ${expectedTarget}). ` +
157
+ 'Bun isolated linking often points .bun/node_modules/typescript at @typescript/native (TS7). ' +
158
+ 'See https://github.com/oven-sh/bun/issues/33834.',
159
+ );
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Keep git hook wiring local to bootstrap. Runtime pin sync
165
+ * (`syncRootRuntimeVersions`) belongs to explicit monorepo tooling after the
166
+ * package graph exists — not the installer.
167
+ */
168
+ async function applyWorkspaceGitConfig(root: string): Promise<void> {
169
+ const gitDirResult = await $`git rev-parse --git-dir`.cwd(root).quiet().nothrow();
170
+ if (gitDirResult.exitCode !== 0) {
171
+ throw new Error('Not in a git repository');
172
+ }
173
+
174
+ const gitDir = path.resolve(root, new TextDecoder().decode(gitDirResult.stdout).trim());
175
+ const tooling = path.join(root, 'tooling');
176
+
177
+ await $`git config --local include.path ${path.join(tooling, 'workspace.gitconfig')}`.cwd(root);
178
+
179
+ // Keep the newer runtime version pins on any merge (nvfetcher overlay +
180
+ // devenv.lock) so a mirror sync's `git am --3way` never stalls on a version
181
+ // conflict. Mapped by the managed .gitattributes (merge=smoo-newer-pins);
182
+ // implemented in tooling/direnv/merge-newer-pins.sh.
183
+ await $`git config --local merge.smoo-newer-pins.name ${'keep the newer devenv/nvfetcher runtime pins'}`.cwd(root);
184
+ await $`git config --local merge.smoo-newer-pins.driver ${'bash tooling/direnv/merge-newer-pins.sh %O %A %B %P'}`.cwd(
185
+ root,
186
+ );
187
+ linkHook(gitDir, tooling, 'pre-commit');
188
+ linkHook(gitDir, tooling, 'commit-msg');
189
+ }
190
+
191
+ function linkHook(gitDir: string, tooling: string, name: string): void {
192
+ const source = path.join(tooling, 'git-hooks', `${name}.sh`);
193
+ if (!existsSync(source)) {
194
+ throw new Error(`Missing ${name} hook source: ${source}`);
195
+ }
196
+
197
+ const target = path.join(gitDir, 'hooks', name);
198
+ if (readLinkOrNull(target) === source) {
199
+ return;
200
+ }
201
+
202
+ console.log(`[!] Linking ${name} hook in ${gitDir}`);
203
+ mkdirSync(path.dirname(target), { recursive: true });
204
+ rmSync(target, { force: true });
205
+ symlinkSync(source, target);
206
+ }
207
+
208
+ function readLinkOrNull(hookPath: string): string | null {
209
+ try {
210
+ return readlinkSync(hookPath);
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
85
216
  async function runSetupCommand(
86
217
  command: string,
87
218
  shell: ReturnType<typeof $>,
@@ -9,12 +9,6 @@ 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
-
18
12
  # Adding a workspace package is rare and easy to leave half-wired. Run the
19
13
  # full monorepo validator only for newly staged package manifests.
20
14
  smoo monorepo validate --fail-fast --only-if-new-workspace-package
@@ -7,8 +7,11 @@ runs:
7
7
  - name: Cache node_modules
8
8
  uses: actions/cache@v5.0.5
9
9
  with:
10
+ # Dependencies only. ttsc plugin/GOCACHE live under $TTSC_CACHE_DIR
11
+ # (.cache/ttsc), not inside node_modules.
12
+ # v2 isolates restore-keys from older Darwin caches with a broken TS dual-package hoist.
10
13
  path: node_modules
11
14
  # prettier-ignore
12
- key: ${{ runner.os }}-${{ runner.arch }}-node-{{NODE_MODULES_CACHE_KEY}}
15
+ key: ${{ runner.os }}-${{ runner.arch }}-node-v2-{{NODE_MODULES_CACHE_KEY}}
13
16
  restore-keys: |
14
- ${{ runner.os }}-${{ runner.arch }}-node-
17
+ ${{ runner.os }}-${{ runner.arch }}-node-v2-
@@ -0,0 +1,19 @@
1
+ name: Cache ttsc plugins
2
+ description: >
3
+ Restore/save compiled ttsc native plugin binaries (typia, etc.). These are content-keyed under TTSC_CACHE_DIR/plugins
4
+ and skip the multi-minute `go build` when the cache key matches. The Go object cache (go-build/) is NOT cached — it is
5
+ multi-GB and only helps when the plugin key misses.
6
+
7
+ runs:
8
+ using: composite
9
+ steps:
10
+ - name: Cache ttsc plugin binaries
11
+ uses: actions/cache@v5.0.5
12
+ with:
13
+ # Only the compiled plugin binaries (~30MB each). Path is relative to the
14
+ # workspace; devenv sets TTSC_CACHE_DIR to <repo>/.cache/ttsc.
15
+ path: .cache/ttsc/plugins
16
+ # prettier-ignore
17
+ key: ${{ runner.os }}-${{ runner.arch }}-ttsc-plugins-v1-${{ hashFiles('bun.lock', 'package.json', 'patches/ttsc@*.patch', 'tooling/direnv/devenv.lock') }}
18
+ restore-keys: |
19
+ ${{ runner.os }}-${{ runner.arch }}-ttsc-plugins-v1-