@smoothbricks/cli 0.10.3 → 0.10.5

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 (115) 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 +13 -5
  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 +120 -11
  14. package/dist/lib/json.d.ts.map +1 -1
  15. package/dist/lib/json.js +319 -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 +4 -2
  24. package/dist/monorepo/index.d.ts.map +1 -1
  25. package/dist/monorepo/index.js +19 -3
  26. package/dist/monorepo/lockfile.d.ts +14 -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 +57 -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 +14 -5
  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 +2 -2
  46. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  47. package/dist/monorepo/tool-validation.js +141 -38
  48. package/dist/monorepo/wrangler.d.ts.map +1 -1
  49. package/dist/monorepo/wrangler.js +13 -13
  50. package/dist/nx/index.d.ts +9 -7
  51. package/dist/nx/index.d.ts.map +1 -1
  52. package/dist/nx/index.js +48 -31
  53. package/dist/pr/index.d.ts.map +1 -1
  54. package/dist/pr/index.js +28 -32
  55. package/dist/release/candidates.d.ts +3 -2
  56. package/dist/release/candidates.d.ts.map +1 -1
  57. package/dist/release/candidates.js +4 -5
  58. package/dist/release/index.d.ts +1 -0
  59. package/dist/release/index.d.ts.map +1 -1
  60. package/dist/release/index.js +88 -59
  61. package/dist/wrangler/prepare-env.d.ts +0 -1
  62. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  63. package/dist/wrangler/prepare-env.js +100 -51
  64. package/dist/wrangler/scaffold.d.ts.map +1 -1
  65. package/dist/wrangler/scaffold.js +12 -12
  66. package/managed/raw/envrc +5 -3
  67. package/managed/raw/git-format-staged.yml +19 -1
  68. package/managed/raw/patches/ttsc@0.19.3.patch +67 -0
  69. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +2 -1
  70. package/managed/raw/tooling/direnv/merge-newer-pins.sh +3 -3
  71. package/managed/raw/tooling/direnv/setup-environment.ts +170 -19
  72. package/managed/raw/tooling/git-hooks/pre-commit.sh +0 -6
  73. package/managed/templates/github/actions/cache-node-modules/action.yml +5 -2
  74. package/managed/templates/github/actions/cache-ttsc-plugins/action.yml +19 -0
  75. package/managed/templates/github/actions/setup-devenv/action.yml +5 -0
  76. package/managed/templates/github/workflows/managed-files.yml +80 -0
  77. package/package.json +15 -4
  78. package/src/bun/preload.ts +12 -0
  79. package/src/cli.ts +20 -8
  80. package/src/generate/index.ts +0 -6
  81. package/src/github-ci/index.ts +49 -18
  82. package/src/github-ci/outputs.ts +1 -2
  83. package/src/lib/cli-package.ts +2 -2
  84. package/src/lib/json.ts +293 -32
  85. package/src/lib/workspace.ts +88 -100
  86. package/src/monorepo/__tests__/ci-workflow.test.ts +3 -3
  87. package/src/monorepo/ci-workflow.ts +27 -1
  88. package/src/monorepo/git-config.ts +2 -2
  89. package/src/monorepo/index.ts +19 -3
  90. package/src/monorepo/lockfile.test.ts +35 -36
  91. package/src/monorepo/lockfile.ts +49 -24
  92. package/src/monorepo/managed-files.test.ts +1 -0
  93. package/src/monorepo/managed-files.ts +62 -38
  94. package/src/monorepo/package-policy.test.ts +20 -50
  95. package/src/monorepo/package-policy.ts +76 -59
  96. package/src/monorepo/packed-manifest.ts +18 -52
  97. package/src/monorepo/packed-package.ts +43 -22
  98. package/src/monorepo/packs/index.ts +14 -5
  99. package/src/monorepo/publish-workflow.ts +3 -1
  100. package/src/monorepo/runtime.test.ts +42 -3
  101. package/src/monorepo/runtime.ts +90 -12
  102. package/src/monorepo/tool-validation.test.ts +38 -16
  103. package/src/monorepo/tool-validation.ts +149 -41
  104. package/src/monorepo/wrangler.test.ts +11 -15
  105. package/src/monorepo/wrangler.ts +15 -15
  106. package/src/nx/index.test.ts +9 -5
  107. package/src/nx/index.ts +69 -42
  108. package/src/pr/index.ts +42 -36
  109. package/src/release/__tests__/candidates.test.ts +5 -4
  110. package/src/release/__tests__/trust-publisher.test.ts +7 -0
  111. package/src/release/candidates.ts +10 -17
  112. package/src/release/index.ts +110 -73
  113. package/src/wrangler/prepare-env.ts +75 -54
  114. package/src/wrangler/scaffold.test.ts +9 -12
  115. package/src/wrangler/scaffold.ts +13 -13
@@ -1,15 +1,26 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
1
2
  import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
3
  import { tmpdir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
4
5
  import { createInterface } from 'node:readline/promises';
5
6
  import { Writable } from 'node:stream';
6
7
  import { $ } from 'bun';
8
+ import typia from 'typia';
7
9
  import { githubCiApplyOutputs, githubCiNxRunMany } from '../github-ci/index.js';
8
10
  import { assertNoConflictMarkers } from '../lib/conflict-markers.js';
9
11
  import { withDevenvEnv } from '../lib/devenv.js';
10
- import { isRecord, readJsonObject, stringProperty } from '../lib/json.js';
12
+ import {
13
+ type NxJson,
14
+ type NxProjectJson,
15
+ type NxTargetConfig,
16
+ type PackageJson,
17
+ parseNxJsonText,
18
+ parseNxProjectJsonText,
19
+ parsePackageJsonText,
20
+ } from '../lib/json.js';
11
21
  import { decode, run, runInteractiveStatus, runResult } from '../lib/run.js';
12
22
  import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
23
+ import { syncBunLockfileVersions } from '../monorepo/lockfile.js';
13
24
  import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
14
25
  import {
15
26
  type BootstrapNpmPackagesOptions,
@@ -519,6 +530,10 @@ async function listUnpublishedPackages(root: string, packages: ReleasePackage[])
519
530
  async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: string, dryRun: boolean): Promise<void> {
520
531
  const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
521
532
  const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
533
+ // bun pm pack resolves workspace:* from bun.lock. Day-to-day lock keeps -next
534
+ // (install/CI). For publish, temporarily rewrite unpublished -next entries to
535
+ // the last stable tag so the tarball embeds installable versions, then restore.
536
+ syncBunLockfileVersions(root, { mode: 'publish', log: true });
522
537
  try {
523
538
  console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
524
539
  await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
@@ -550,6 +565,7 @@ async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: stri
550
565
  },
551
566
  );
552
567
  } finally {
568
+ syncBunLockfileVersions(root, { mode: 'install', log: true });
553
569
  await rm(tempDir, { recursive: true, force: true });
554
570
  }
555
571
  }
@@ -572,7 +588,7 @@ async function publishPlaceholderPackage(
572
588
  name: pkg.name,
573
589
  version: NPM_BOOTSTRAP_VERSION,
574
590
  description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
575
- license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
591
+ license: pkg.json.license ?? 'UNLICENSED',
576
592
  repository: pkg.json.repository,
577
593
  publishConfig: { access: 'public' },
578
594
  },
@@ -596,7 +612,7 @@ function safeTarballPrefix(name: string): string {
596
612
 
597
613
  async function assertPackedWorkspaceDependencies(root: string, tarball: string, pkg: ReleasePackage): Promise<void> {
598
614
  const manifest = await readPackedPackageJson(root, tarball, pkg.name);
599
- const failures = validatePackedWorkspaceDependencies(root, pkg, manifest);
615
+ const failures = validatePackedWorkspaceDependencies(root, pkg, manifest, { mode: 'publish' });
600
616
  if (failures.length > 0) {
601
617
  throw new Error(failures.join('\n'));
602
618
  }
@@ -783,8 +799,7 @@ async function packageVersionAtRef(root: string, packagePath: string, ref: strin
783
799
  return null;
784
800
  }
785
801
  try {
786
- const parsed = JSON.parse(decode(result.stdout));
787
- return isRecord(parsed) ? stringProperty(parsed, 'version') : null;
802
+ return parsePackageJsonText(decode(result.stdout))?.version ?? null;
788
803
  } catch {
789
804
  return null;
790
805
  }
@@ -1264,74 +1279,83 @@ async function packageChangedFilesSince(root: string, ref: string, packagePath:
1264
1279
  .map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
1265
1280
  }
1266
1281
 
1267
- async function packageJsonAtRef(
1268
- root: string,
1269
- ref: string,
1270
- packagePath: string,
1271
- ): Promise<Record<string, unknown> | null> {
1282
+ async function packageJsonAtRef(root: string, ref: string, packagePath: string): Promise<PackageJson | null> {
1272
1283
  const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
1273
1284
  if (result.exitCode !== 0) {
1274
1285
  return null;
1275
1286
  }
1276
1287
  try {
1277
- const parsed = JSON.parse(decode(result.stdout));
1278
- return isRecord(parsed) ? parsed : null;
1288
+ return parsePackageJsonText(decode(result.stdout));
1279
1289
  } catch {
1280
1290
  return null;
1281
1291
  }
1282
1292
  }
1283
1293
 
1284
- async function currentPackageJson(root: string, packagePath: string): Promise<Record<string, unknown> | null> {
1294
+ async function currentPackageJson(root: string, packagePath: string): Promise<PackageJson | null> {
1285
1295
  return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
1286
1296
  }
1287
1297
 
1288
1298
  async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
1289
1299
  const project = await nxProjectJson(root, projectName);
1290
- const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
1291
- return resolveBuildInputPatterns(project, nxJson);
1300
+ return resolveBuildInputPatterns(project, readNxJson(join(root, 'nx.json')));
1292
1301
  }
1293
1302
 
1294
- async function nxProjectJson(root: string, projectName: string): Promise<Record<string, unknown>> {
1303
+ async function nxProjectJson(root: string, projectName: string): Promise<NxProjectJson> {
1295
1304
  const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
1296
- const parsed = JSON.parse(decode(result.stdout));
1297
- if (!isRecord(parsed)) {
1305
+ const parsed = parseNxProjectJsonText(decode(result.stdout));
1306
+ if (!parsed) {
1298
1307
  throw new Error(`Unable to inspect Nx project ${projectName}.`);
1299
1308
  }
1300
1309
  return parsed;
1301
1310
  }
1302
1311
 
1303
- function resolveBuildInputPatterns(project: Record<string, unknown>, nxJson: Record<string, unknown>): string[] {
1304
- const targets = recordProperty(project, 'targets');
1312
+ function readNxJson(path: string): NxJson {
1313
+ if (!existsSync(path)) {
1314
+ return {};
1315
+ }
1316
+ try {
1317
+ return parseNxJsonText(readFileSync(path, 'utf8')) ?? {};
1318
+ } catch {
1319
+ return {};
1320
+ }
1321
+ }
1322
+
1323
+ function resolveBuildInputPatterns(project: NxProjectJson, nxJson: NxJson): string[] {
1324
+ const targets = project.targets;
1305
1325
  if (!targets) {
1306
1326
  return [];
1307
1327
  }
1308
1328
  return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
1309
1329
  }
1310
1330
 
1311
- function collectBuildInputs(targets: Record<string, unknown>): string[] {
1312
- const build = recordProperty(targets, 'build');
1331
+ function collectBuildInputs(targets: Record<string, NxTargetConfig>): string[] {
1332
+ const build = targets.build;
1313
1333
  if (!build) {
1314
1334
  return ['production'];
1315
1335
  }
1316
- const directInputs = stringArrayProperty(build, 'inputs');
1336
+ const directInputs = stringInputs(build.inputs);
1317
1337
  if (directInputs.length > 0) {
1318
1338
  return directInputs;
1319
1339
  }
1320
1340
  const inputs: string[] = [];
1321
- for (const dependency of stringArrayProperty(build, 'dependsOn')) {
1322
- if (dependency.startsWith('^')) {
1341
+ for (const dependency of build.dependsOn ?? []) {
1342
+ if (typeof dependency !== 'string' || dependency.startsWith('^')) {
1323
1343
  continue;
1324
1344
  }
1325
1345
  const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
1326
1346
  if (!targetName) {
1327
1347
  continue;
1328
1348
  }
1329
- inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
1349
+ inputs.push(...stringInputs(targets[targetName]?.inputs));
1330
1350
  }
1331
1351
  return inputs.length > 0 ? inputs : ['production'];
1332
1352
  }
1333
1353
 
1334
- function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown>): string[] {
1354
+ function stringInputs(inputs: NxTargetConfig['inputs'] | undefined): string[] {
1355
+ return Array.isArray(inputs) ? inputs.filter((entry): entry is string => typeof entry === 'string') : [];
1356
+ }
1357
+
1358
+ function normalizeInputPatterns(inputs: string[], nxJson: NxJson): string[] {
1335
1359
  const patterns: string[] = [];
1336
1360
  const seen = new Set<string>();
1337
1361
  for (const input of inputs) {
@@ -1342,14 +1366,13 @@ function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown
1342
1366
  return patterns;
1343
1367
  }
1344
1368
 
1345
- function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen: Set<string>): string[] {
1369
+ function expandInputPattern(input: string, nxJson: NxJson, seen: Set<string>): string[] {
1346
1370
  if (seen.has(input)) {
1347
1371
  return [];
1348
1372
  }
1349
1373
  seen.add(input);
1350
1374
  if (!input.includes('{')) {
1351
- const namedInputs = recordProperty(nxJson, 'namedInputs');
1352
- const namedInput = namedInputs?.[input];
1375
+ const namedInput = nxJson.namedInputs?.[input];
1353
1376
  if (Array.isArray(namedInput)) {
1354
1377
  return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
1355
1378
  }
@@ -1364,19 +1387,6 @@ function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen
1364
1387
  return [`${excluded ? '!' : ''}${packageRelative}`];
1365
1388
  }
1366
1389
 
1367
- function recordProperty(record: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
1368
- if (!record) {
1369
- return null;
1370
- }
1371
- const value = record[key];
1372
- return isRecord(value) ? value : null;
1373
- }
1374
-
1375
- function stringArrayProperty(record: Record<string, unknown> | null, key: string): string[] {
1376
- const value = record?.[key];
1377
- return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
1378
- }
1379
-
1380
1390
  async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
1381
1391
  const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
1382
1392
  if (result.exitCode !== 0) {
@@ -1615,7 +1625,7 @@ async function runLatestNpmTrust(
1615
1625
  if (status === 0) {
1616
1626
  return 'configured';
1617
1627
  }
1618
- throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
1628
+ throw new Error(npmCommandFailedMessage(npmArgs, status));
1619
1629
  }
1620
1630
 
1621
1631
  async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisherLookup> {
@@ -1624,7 +1634,7 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
1624
1634
  if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
1625
1635
  const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1626
1636
  if (status !== 0) {
1627
- throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${status}`);
1637
+ throw new Error(npmCommandFailedMessage(args, status));
1628
1638
  }
1629
1639
  result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
1630
1640
  }
@@ -1632,15 +1642,47 @@ async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'na
1632
1642
  return npmTrustAccessDetails(root, pkg.name);
1633
1643
  }
1634
1644
  if (result.exitCode !== 0) {
1635
- throw new Error(
1636
- `nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`,
1637
- );
1645
+ throw new Error(npmCommandFailedMessage(args, result.exitCode, result.stdout, result.stderr));
1638
1646
  }
1639
1647
  return parseTrustedPublishers(result.stdout, pkg.name);
1640
1648
  }
1641
1649
 
1650
+ /** npm trust list/setup requires an authenticated package owner. */
1642
1651
  export function npmTrustListAccessDenied(stdout: string, stderr: string): boolean {
1643
- return /\bE403\b/.test(`${stdout}\n${stderr}`);
1652
+ const output = `${stdout}\n${stderr}`;
1653
+ // E403: authenticated but not an owner. E401 / login-required: no usable session.
1654
+ return (
1655
+ /\bE403\b/.test(output) ||
1656
+ /\bE401\b/.test(output) ||
1657
+ /You must be logged in to publish packages/i.test(output) ||
1658
+ /npm error 401 Unauthorized/i.test(output)
1659
+ );
1660
+ }
1661
+
1662
+ function npmCommandFailedMessage(npmArgs: string[], exitCode: number, stdout = '', stderr = ''): string {
1663
+ const command = `nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')}`;
1664
+ const detail = npmFailureDetail(stdout, stderr);
1665
+ return detail.length > 0
1666
+ ? `${command} failed with exit code ${exitCode}\n${detail}`
1667
+ : `${command} failed with exit code ${exitCode}`;
1668
+ }
1669
+
1670
+ function npmFailureDetail(stdout: string, stderr: string): string {
1671
+ const chunks = [stderr.trim(), stdout.trim()].filter((chunk) => chunk.length > 0);
1672
+ if (chunks.length === 0) {
1673
+ return '';
1674
+ }
1675
+ // Prefer the actionable npm error line over notice spam when present.
1676
+ const joined = chunks.join('\n');
1677
+ const errorLines = joined
1678
+ .split('\n')
1679
+ .map((line) => line.trimEnd())
1680
+ .filter((line) => /npm error|error code|401 Unauthorized|403 Forbidden|E401|E403|logged in/i.test(line));
1681
+ if (errorLines.length > 0) {
1682
+ return errorLines.slice(0, 8).join('\n');
1683
+ }
1684
+ // Fall back to a short tail of combined output.
1685
+ return joined.split('\n').slice(-12).join('\n');
1644
1686
  }
1645
1687
 
1646
1688
  async function npmTrustAccessDetails(root: string, packageName: string): Promise<NpmTrustAccessDenied> {
@@ -1665,36 +1707,31 @@ export function parseTrustedPublishers(stdout: string, packageName: string): Tru
1665
1707
  if (stdout.trim().length === 0) {
1666
1708
  return [];
1667
1709
  }
1668
- const parsed = JSON.parse(stdout) as unknown;
1710
+ // createIsParse returns null for non-matching shapes and bare `null`.
1711
+ const parsed = parseTrustedPublisherJson(stdout);
1669
1712
  if (parsed === null) {
1670
- return [];
1671
- }
1672
- if (Array.isArray(parsed)) {
1673
- return parsed.map((value) => parseTrustedPublisher(value, packageName));
1674
- }
1675
- if (isRecord(parsed)) {
1676
- return [parseTrustedPublisher(parsed, packageName)];
1713
+ if (stdout.trim() === 'null') {
1714
+ return [];
1715
+ }
1716
+ throw new Error(`${packageName}: npm trust list returned invalid trusted publisher JSON.`);
1677
1717
  }
1678
- throw new Error(`${packageName}: npm trust list returned invalid JSON.`);
1718
+ return (Array.isArray(parsed) ? parsed : [parsed]).map((value) => ({
1719
+ id: value.id,
1720
+ type: value.type,
1721
+ file: value.file,
1722
+ repository: value.repository,
1723
+ }));
1679
1724
  }
1680
1725
 
1681
- function parseTrustedPublisher(value: unknown, packageName: string): TrustedPublisher {
1682
- if (!isRecord(value)) {
1683
- throw new Error(`${packageName}: npm trust list returned invalid trusted publisher entry.`);
1684
- }
1685
- const id = stringProperty(value, 'id');
1686
- const type = stringProperty(value, 'type');
1687
- if (!id || !type) {
1688
- throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
1689
- }
1690
- return {
1691
- id,
1692
- type,
1693
- file: stringProperty(value, 'file') ?? undefined,
1694
- repository: stringProperty(value, 'repository') ?? undefined,
1695
- };
1726
+ interface TrustedPublisherJson {
1727
+ id: string;
1728
+ type: string;
1729
+ file?: string;
1730
+ repository?: string;
1696
1731
  }
1697
1732
 
1733
+ const parseTrustedPublisherJson = typia.json.createIsParse<TrustedPublisherJson | TrustedPublisherJson[]>();
1734
+
1698
1735
  async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
1699
1736
  await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
1700
1737
  }
@@ -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
  }