@shazhou/proman-core 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +18 -0
  3. package/dist/commands/bump.d.ts +13 -0
  4. package/dist/commands/bump.d.ts.map +1 -0
  5. package/dist/commands/bump.js +115 -0
  6. package/dist/commands/deploy.d.ts +9 -0
  7. package/dist/commands/deploy.d.ts.map +1 -0
  8. package/dist/commands/deploy.js +42 -0
  9. package/dist/commands/dev.d.ts +15 -0
  10. package/dist/commands/dev.d.ts.map +1 -0
  11. package/dist/commands/dev.js +175 -0
  12. package/dist/commands/index.d.ts +7 -0
  13. package/dist/commands/index.d.ts.map +1 -0
  14. package/dist/commands/index.js +7 -0
  15. package/dist/commands/init.d.ts +5 -0
  16. package/dist/commands/init.d.ts.map +1 -0
  17. package/dist/commands/init.js +262 -0
  18. package/dist/commands/link.d.ts +19 -0
  19. package/dist/commands/link.d.ts.map +1 -0
  20. package/dist/commands/link.js +155 -0
  21. package/dist/commands/publish.d.ts +18 -0
  22. package/dist/commands/publish.d.ts.map +1 -0
  23. package/dist/commands/publish.js +125 -0
  24. package/dist/config/index.d.ts +4 -0
  25. package/dist/config/index.d.ts.map +1 -0
  26. package/dist/config/index.js +2 -0
  27. package/dist/config/load-config.d.ts +6 -0
  28. package/dist/config/load-config.d.ts.map +1 -0
  29. package/dist/config/load-config.js +29 -0
  30. package/dist/config/types.d.ts +17 -0
  31. package/dist/config/types.d.ts.map +1 -0
  32. package/dist/config/types.js +1 -0
  33. package/dist/config/validate-config.d.ts +7 -0
  34. package/dist/config/validate-config.d.ts.map +1 -0
  35. package/dist/config/validate-config.js +72 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +6 -0
  39. package/dist/utils/changeset.d.ts +16 -0
  40. package/dist/utils/changeset.d.ts.map +1 -0
  41. package/dist/utils/changeset.js +80 -0
  42. package/dist/utils/fingerprint.d.ts +38 -0
  43. package/dist/utils/fingerprint.d.ts.map +1 -0
  44. package/dist/utils/fingerprint.js +182 -0
  45. package/dist/utils/git.d.ts +23 -0
  46. package/dist/utils/git.d.ts.map +1 -0
  47. package/dist/utils/git.js +105 -0
  48. package/dist/utils/index.d.ts +8 -0
  49. package/dist/utils/index.d.ts.map +1 -0
  50. package/dist/utils/index.js +8 -0
  51. package/dist/utils/npm.d.ts +30 -0
  52. package/dist/utils/npm.d.ts.map +1 -0
  53. package/dist/utils/npm.js +85 -0
  54. package/dist/utils/smoke-test.d.ts +7 -0
  55. package/dist/utils/smoke-test.d.ts.map +1 -0
  56. package/dist/utils/smoke-test.js +59 -0
  57. package/dist/utils/version.d.ts +5 -0
  58. package/dist/utils/version.d.ts.map +1 -0
  59. package/dist/utils/version.js +36 -0
  60. package/dist/utils/workspace.d.ts +21 -0
  61. package/dist/utils/workspace.d.ts.map +1 -0
  62. package/dist/utils/workspace.js +73 -0
  63. package/package.json +45 -0
  64. package/src/commands/bump.ts +131 -0
  65. package/src/commands/deploy.ts +52 -0
  66. package/src/commands/dev.ts +214 -0
  67. package/src/commands/index.ts +7 -0
  68. package/src/commands/init.integration.test.ts +59 -0
  69. package/src/commands/init.test.ts +179 -0
  70. package/src/commands/init.ts +290 -0
  71. package/src/commands/link.ts +195 -0
  72. package/src/commands/publish.ts +168 -0
  73. package/src/config/index.ts +8 -0
  74. package/src/config/load-config.ts +33 -0
  75. package/src/config/types.ts +19 -0
  76. package/src/config/validate-config.ts +81 -0
  77. package/src/index.ts +29 -0
  78. package/src/utils/changeset.ts +98 -0
  79. package/src/utils/fingerprint.ts +199 -0
  80. package/src/utils/git.ts +119 -0
  81. package/src/utils/index.ts +8 -0
  82. package/src/utils/npm.ts +110 -0
  83. package/src/utils/smoke-test.ts +79 -0
  84. package/src/utils/version.ts +41 -0
  85. package/src/utils/workspace.ts +94 -0
  86. package/tests/build-fingerprint-integration.test.ts +403 -0
  87. package/tests/bump.test.ts +261 -0
  88. package/tests/changeset.test.ts +147 -0
  89. package/tests/deploy.test.ts +98 -0
  90. package/tests/dev.test.ts +756 -0
  91. package/tests/fingerprint.test.ts +316 -0
  92. package/tests/fixtures/api-only/packages/api/.gitkeep +0 -0
  93. package/tests/fixtures/api-only/proman.yaml +4 -0
  94. package/tests/fixtures/bad-packages/proman.yaml +1 -0
  95. package/tests/fixtures/bun-project/packages/a/.gitkeep +0 -0
  96. package/tests/fixtures/bun-project/proman.yaml +4 -0
  97. package/tests/fixtures/defaults/proman.yaml +3 -0
  98. package/tests/fixtures/no-deployable/packages/core/.gitkeep +0 -0
  99. package/tests/fixtures/no-deployable/packages/mycli/.gitkeep +0 -0
  100. package/tests/fixtures/no-deployable/proman.yaml +7 -0
  101. package/tests/fixtures/node-runtime/packages/a/package.json +5 -0
  102. package/tests/fixtures/node-runtime/proman.yaml +3 -0
  103. package/tests/fixtures/pnpm-project/packages/a/package.json +1 -0
  104. package/tests/fixtures/pnpm-project/pnpm-lock.yaml +0 -0
  105. package/tests/fixtures/pnpm-project/proman.yaml +3 -0
  106. package/tests/fixtures/typed/packages/api/.gitkeep +0 -0
  107. package/tests/fixtures/typed/packages/core/.gitkeep +0 -0
  108. package/tests/fixtures/typed/packages/dashboard/.gitkeep +0 -0
  109. package/tests/fixtures/typed/packages/mycli/.gitkeep +0 -0
  110. package/tests/fixtures/typed/proman.yaml +13 -0
  111. package/tests/fixtures/valid/packages/cli/package.json +5 -0
  112. package/tests/fixtures/valid/packages/core/package.json +5 -0
  113. package/tests/fixtures/valid/packages/fs/package.json +5 -0
  114. package/tests/fixtures/valid/proman.yaml +13 -0
  115. package/tests/fixtures/webui-only/packages/dashboard/.gitkeep +0 -0
  116. package/tests/fixtures/webui-only/proman.yaml +4 -0
  117. package/tests/link.test.ts +419 -0
  118. package/tests/load-config.test.ts +44 -0
  119. package/tests/npm.test.ts +199 -0
  120. package/tests/publish.test.ts +599 -0
  121. package/tests/smoke-test.test.ts +211 -0
  122. package/tests/validate-config.test.ts +67 -0
  123. package/tests/version.test.ts +86 -0
  124. package/tests/workflow-schema.test.ts +72 -0
  125. package/tests/workspace.test.ts +160 -0
  126. package/tsconfig.build.json +14 -0
  127. package/tsconfig.json +8 -0
  128. package/tsconfig.tsbuildinfo +1 -0
  129. package/vitest.config.ts +8 -0
@@ -0,0 +1,85 @@
1
+ const RELEASE_BRANCH_RE = /^release\/(.+)$/;
2
+ export function parseReleaseBranch(branch) {
3
+ const m = branch.match(RELEASE_BRANCH_RE);
4
+ if (!m)
5
+ throw new Error(`not a release branch: '${branch}'`);
6
+ const v = m[1].trim();
7
+ if (!v)
8
+ throw new Error(`malformed release branch: '${branch}'`);
9
+ return v;
10
+ }
11
+ export function nextRcNumber(opts) {
12
+ const { baseVersion, existing } = opts;
13
+ const prefix = `${baseVersion}-rc.`;
14
+ let max = 0;
15
+ for (const v of existing) {
16
+ if (!v.startsWith(prefix))
17
+ continue;
18
+ const tail = v.slice(prefix.length);
19
+ const n = Number.parseInt(tail, 10);
20
+ if (Number.isFinite(n) && n > max)
21
+ max = n;
22
+ }
23
+ return max + 1;
24
+ }
25
+ export function formatRcVersion(baseVersion, n) {
26
+ return `${baseVersion}-rc.${n}`;
27
+ }
28
+ export const defaultRegistryFetch = async (pkg) => {
29
+ const res = await fetch(`https://registry.npmjs.org/${pkg}`);
30
+ if (!res.ok) {
31
+ if (res.status === 404)
32
+ return [];
33
+ throw new Error(`registry fetch failed for ${pkg}: ${res.status} ${res.statusText}`);
34
+ }
35
+ const json = (await res.json());
36
+ return Object.keys(json.versions ?? {});
37
+ };
38
+ export const defaultSpawn = async (argv, cwd) => {
39
+ const { spawnSync } = await import('node:child_process');
40
+ const result = spawnSync(argv[0], argv.slice(1), {
41
+ cwd,
42
+ stdio: 'pipe',
43
+ });
44
+ const stdout = result.stdout?.toString() ?? '';
45
+ const stderr = result.stderr?.toString() ?? '';
46
+ // Forward captured output to terminal so users still see progress
47
+ if (stdout)
48
+ process.stdout.write(stdout);
49
+ if (stderr)
50
+ process.stderr.write(stderr);
51
+ return {
52
+ code: result.status ?? 1,
53
+ stdout,
54
+ stderr,
55
+ };
56
+ };
57
+ export async function runOrThrow(spawn, argv, cwd) {
58
+ const { code, stdout, stderr } = await spawn(argv, cwd);
59
+ if (code !== 0) {
60
+ const detail = stderr.trim() || stdout.trim();
61
+ throw new Error(detail ? `${argv.join(' ')} failed: ${detail}` : `${argv.join(' ')} failed`);
62
+ }
63
+ }
64
+ export function createNpmRunner(cwd, spawn = defaultSpawn) {
65
+ const runScript = (script) => async () => {
66
+ await runOrThrow(spawn, ['pnpm', 'run', script], cwd);
67
+ };
68
+ return {
69
+ install: async () => {
70
+ await runOrThrow(spawn, ['pnpm', 'install'], cwd);
71
+ },
72
+ build: runScript('build'),
73
+ test: async () => {
74
+ await runOrThrow(spawn, ['pnpm', 'exec', 'vitest', 'run'], cwd);
75
+ },
76
+ check: runScript('check'),
77
+ format: runScript('format'),
78
+ publish: async (pkgDir, opts) => {
79
+ const args = ['pnpm', 'publish', '--tag', opts.tag, '--no-git-checks'];
80
+ if (opts.access)
81
+ args.push('--access', opts.access);
82
+ await runOrThrow(spawn, args, pkgDir);
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,7 @@
1
+ import type { SpawnFn } from './npm.js';
2
+ /**
3
+ * Smoke test a package tarball by extracting it and running bin commands.
4
+ * Validates that the packaged artifact actually works before publishing.
5
+ */
6
+ export declare function smokeTestTarball(pkgDir: string, spawn: SpawnFn): Promise<void>;
7
+ //# sourceMappingURL=smoke-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke-test.d.ts","sourceRoot":"","sources":["../../src/utils/smoke-test.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAQvC;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DpF"}
@@ -0,0 +1,59 @@
1
+ import { readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Smoke test a package tarball by extracting it and running bin commands.
6
+ * Validates that the packaged artifact actually works before publishing.
7
+ */
8
+ export async function smokeTestTarball(pkgDir, spawn) {
9
+ // Read package.json to check for bin entries
10
+ const pkgJsonPath = join(pkgDir, 'package.json');
11
+ const pkgJsonText = await readFile(pkgJsonPath, 'utf8');
12
+ const pkgJson = JSON.parse(pkgJsonText);
13
+ // Skip if no bin entry
14
+ if (!pkgJson.bin) {
15
+ return;
16
+ }
17
+ // Normalize bin to Record format
18
+ const binEntries = typeof pkgJson.bin === 'string' ? { [pkgJson.name]: pkgJson.bin } : pkgJson.bin;
19
+ // Skip if bin is empty
20
+ if (Object.keys(binEntries).length === 0) {
21
+ return;
22
+ }
23
+ // Step 1: Create tarball with pnpm pack
24
+ const packResult = await spawn(['pnpm', 'pack'], pkgDir);
25
+ if (packResult.code !== 0) {
26
+ throw new Error(`pnpm pack failed: ${packResult.stderr || packResult.stdout}`);
27
+ }
28
+ const tarballName = packResult.stdout.trim();
29
+ if (!tarballName) {
30
+ throw new Error('pnpm pack did not return tarball filename');
31
+ }
32
+ // Step 2: Extract tarball to temp directory
33
+ const { mkdtemp } = await import('node:fs/promises');
34
+ const testDir = await mkdtemp(join(tmpdir(), 'proman-smoke-'));
35
+ try {
36
+ // Extract tarball
37
+ const tarballPath = join(pkgDir, tarballName);
38
+ const extractResult = await spawn(['tar', '-xzf', tarballPath, '-C', testDir], pkgDir);
39
+ if (extractResult.code !== 0) {
40
+ throw new Error(`tar extract failed: ${extractResult.stderr}`);
41
+ }
42
+ // pnpm pack creates a 'package/' directory inside the tarball
43
+ const extractedPkgDir = join(testDir, 'package');
44
+ // Step 3: Test each bin entry
45
+ for (const [binName, binPath] of Object.entries(binEntries)) {
46
+ const binFullPath = join(extractedPkgDir, binPath);
47
+ const binTestResult = await spawn(['node', binFullPath, '--version'], extractedPkgDir);
48
+ if (binTestResult.code !== 0) {
49
+ const errorMsg = binTestResult.stderr.trim() || binTestResult.stdout.trim();
50
+ throw new Error(`smoke test failed for bin '${binName}': ${errorMsg || 'non-zero exit code'}`);
51
+ }
52
+ }
53
+ }
54
+ finally {
55
+ // Step 4: Always clean up temp directory and tarball
56
+ await rm(testDir, { recursive: true, force: true });
57
+ await rm(join(pkgDir, tarballName), { force: true });
58
+ }
59
+ }
@@ -0,0 +1,5 @@
1
+ import type { Bump, Changeset } from './changeset.js';
2
+ export declare function bumpVersion(current: string, bump: Bump): string;
3
+ export declare function inferBump(changesets: Changeset[]): Record<string, Bump>;
4
+ export declare function parseTagVersion(tag: string): string;
5
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/utils/version.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAIrD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAW/D;AAID,wBAAgB,SAAS,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAWvE;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMnD"}
@@ -0,0 +1,36 @@
1
+ const VERSION_CORE_RE = /^(\d+)\.(\d+)\.(\d+)(?:-[\w.+-]+)?$/;
2
+ export function bumpVersion(current, bump) {
3
+ const m = current.match(VERSION_CORE_RE);
4
+ if (!m) {
5
+ throw new Error(`invalid version: '${current}'`);
6
+ }
7
+ const major = Number(m[1]);
8
+ const minor = Number(m[2]);
9
+ const patch = Number(m[3]);
10
+ if (bump === 'major')
11
+ return `${major + 1}.0.0`;
12
+ if (bump === 'minor')
13
+ return `${major}.${minor + 1}.0`;
14
+ return `${major}.${minor}.${patch + 1}`;
15
+ }
16
+ const ORDER = { patch: 1, minor: 2, major: 3 };
17
+ export function inferBump(changesets) {
18
+ const result = {};
19
+ for (const c of changesets) {
20
+ for (const [pkg, bump] of Object.entries(c.packages)) {
21
+ const current = result[pkg];
22
+ if (current === undefined || ORDER[bump] > ORDER[current]) {
23
+ result[pkg] = bump;
24
+ }
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+ const TAG_RE = /^v?(\d+\.\d+\.\d+(?:-[\w.+-]+)?)$/;
30
+ export function parseTagVersion(tag) {
31
+ const m = tag.match(TAG_RE);
32
+ if (!m) {
33
+ throw new Error(`invalid tag: '${tag}'`);
34
+ }
35
+ return m[1];
36
+ }
@@ -0,0 +1,21 @@
1
+ export type PkgManifest = {
2
+ name: string;
3
+ version: string;
4
+ dependencies?: Record<string, string>;
5
+ devDependencies?: Record<string, string>;
6
+ [k: string]: unknown;
7
+ };
8
+ type Unresolved = {
9
+ pkg: string;
10
+ dep: string;
11
+ };
12
+ export declare function rewriteWorkspaceDeps(manifests: PkgManifest[]): {
13
+ rewritten: PkgManifest[];
14
+ unresolved: Unresolved[];
15
+ };
16
+ export declare function applyWorkspaceRewrites(rootDir: string, packages: {
17
+ name: string;
18
+ path: string;
19
+ }[]): Promise<string[]>;
20
+ export {};
21
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/utils/workspace.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB,CAAA;AAED,KAAK,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AA8B9C,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG;IAC9D,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,UAAU,EAAE,UAAU,EAAE,CAAA;CACzB,CAiBA;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GACzC,OAAO,CAAC,MAAM,EAAE,CAAC,CA2BnB"}
@@ -0,0 +1,73 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ function rewriteDepsField(pkgName, field, versions, unresolved) {
4
+ if (!field)
5
+ return undefined;
6
+ const out = {};
7
+ let changed = false;
8
+ for (const [dep, val] of Object.entries(field)) {
9
+ if (val === 'workspace:*') {
10
+ const v = versions.get(dep);
11
+ if (v) {
12
+ out[dep] = v;
13
+ changed = true;
14
+ }
15
+ else {
16
+ out[dep] = val;
17
+ unresolved.push({ pkg: pkgName, dep });
18
+ }
19
+ }
20
+ else {
21
+ out[dep] = val;
22
+ }
23
+ }
24
+ // Mark via reference identity if changed; caller can compare to original
25
+ void changed;
26
+ return out;
27
+ }
28
+ export function rewriteWorkspaceDeps(manifests) {
29
+ const versions = new Map();
30
+ for (const m of manifests) {
31
+ versions.set(m.name, m.version);
32
+ }
33
+ const unresolved = [];
34
+ const rewritten = manifests.map((m) => {
35
+ const copy = { ...m };
36
+ if (m.dependencies) {
37
+ copy.dependencies = rewriteDepsField(m.name, m.dependencies, versions, unresolved);
38
+ }
39
+ if (m.devDependencies) {
40
+ copy.devDependencies = rewriteDepsField(m.name, m.devDependencies, versions, unresolved);
41
+ }
42
+ return copy;
43
+ });
44
+ return { rewritten, unresolved };
45
+ }
46
+ export async function applyWorkspaceRewrites(rootDir, packages) {
47
+ const paths = packages.map((p) => resolve(rootDir, p.path, 'package.json'));
48
+ const manifests = [];
49
+ for (let i = 0; i < paths.length; i++) {
50
+ const p = paths[i];
51
+ let text;
52
+ try {
53
+ text = await readFile(p, 'utf8');
54
+ }
55
+ catch (err) {
56
+ throw new Error(`package.json not found at ${p}: ${err.message}`);
57
+ }
58
+ manifests.push(JSON.parse(text));
59
+ }
60
+ const { rewritten } = rewriteWorkspaceDeps(manifests);
61
+ const changed = [];
62
+ for (let i = 0; i < paths.length; i++) {
63
+ const before = JSON.stringify(manifests[i]);
64
+ const after = JSON.stringify(rewritten[i]);
65
+ if (before !== after) {
66
+ const p = paths[i];
67
+ const newText = `${JSON.stringify(rewritten[i], null, 2)}\n`;
68
+ await writeFile(p, newText);
69
+ changed.push(p);
70
+ }
71
+ }
72
+ return changed;
73
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@shazhou/proman-core",
3
+ "version": "0.9.0",
4
+ "type": "module",
5
+ "description": "Core library for TypeScript monorepo management",
6
+ "keywords": [
7
+ "typescript",
8
+ "monorepo",
9
+ "build",
10
+ "library"
11
+ ],
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./commands": "./dist/commands/index.js",
15
+ "./utils": "./dist/utils/index.js",
16
+ "./config": "./dist/config/index.js"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^5.0.0",
21
+ "vitest": "^4.0.0"
22
+ },
23
+ "dependencies": {
24
+ "yaml": "^2.9.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@biomejs/biome": "^2.0.0",
28
+ "typescript": "^5.0.0",
29
+ "vite": "^6.0.0",
30
+ "vitest": "^4.0.0",
31
+ "wrangler": "^3.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "vite": {
35
+ "optional": true
36
+ },
37
+ "wrangler": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "scripts": {
42
+ "test": "vitest run --passWithNoTests",
43
+ "build": "tsc --project tsconfig.build.json"
44
+ }
45
+ }
@@ -0,0 +1,131 @@
1
+ import { readFile, stat, unlink, writeFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import { loadConfig } from '../config/load-config.js'
4
+ import {
5
+ buildChangelogEntry,
6
+ type Changeset,
7
+ prependChangelog,
8
+ readChangesets,
9
+ } from '../utils/changeset.js'
10
+ import { bumpVersion, inferBump } from '../utils/version.js'
11
+
12
+ export type BumpOptions = {
13
+ type?: 'major' | 'minor' | 'patch'
14
+ cwd?: string
15
+ now?: () => Date
16
+ }
17
+
18
+ async function readJson(path: string): Promise<Record<string, unknown>> {
19
+ const text = await readFile(path, 'utf8')
20
+ return JSON.parse(text) as Record<string, unknown>
21
+ }
22
+
23
+ async function writeJson(path: string, data: Record<string, unknown>): Promise<void> {
24
+ await writeFile(path, `${JSON.stringify(data, null, 2)}\n`)
25
+ }
26
+
27
+ function formatDate(d: Date): string {
28
+ const y = d.getUTCFullYear()
29
+ const m = String(d.getUTCMonth() + 1).padStart(2, '0')
30
+ const day = String(d.getUTCDate()).padStart(2, '0')
31
+ return `${y}-${m}-${day}`
32
+ }
33
+
34
+ async function fileExists(path: string): Promise<boolean> {
35
+ try {
36
+ await stat(path)
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Bump package versions independently.
45
+ * --type: bump all packages with the given type.
46
+ * No --type: infer per-package bumps from changesets; only bump packages mentioned in changesets.
47
+ * Returns a map of package name → new version.
48
+ */
49
+ export async function bump(opts: BumpOptions = {}): Promise<Record<string, string>> {
50
+ const cwd = opts.cwd ?? process.cwd()
51
+ const now = opts.now ?? (() => new Date())
52
+ const cfg = loadConfig(cwd)
53
+
54
+ const bumped: Record<string, string> = {}
55
+
56
+ if (opts.type) {
57
+ // Explicit --type: bump all packages
58
+ for (const pkg of cfg.packages) {
59
+ const pkgPath = resolve(cwd, pkg.path, 'package.json')
60
+ const json = await readJson(pkgPath)
61
+ const current = json.version as string
62
+ if (!current) throw new Error(`missing version in ${pkgPath}`)
63
+ const version = bumpVersion(current, opts.type)
64
+ json.version = version
65
+ await writeJson(pkgPath, json)
66
+ bumped[pkg.name] = version
67
+ }
68
+ } else {
69
+ // Infer from changesets: per-package independent bump
70
+ const changesets = await readChangesets(cwd)
71
+ if (changesets.length === 0) {
72
+ throw new Error('no --type specified and no pending changesets found')
73
+ }
74
+ const bumpMap = inferBump(changesets)
75
+ if (Object.keys(bumpMap).length === 0) {
76
+ throw new Error('no inferable bump from changeset entries')
77
+ }
78
+
79
+ const pkgByName = new Map(cfg.packages.map((p) => [p.name, p]))
80
+
81
+ for (const [pkgName, bumpType] of Object.entries(bumpMap)) {
82
+ const pkg = pkgByName.get(pkgName)
83
+ if (!pkg) continue // changeset mentions unknown package, skip
84
+ const pkgPath = resolve(cwd, pkg.path, 'package.json')
85
+ const json = await readJson(pkgPath)
86
+ const current = json.version as string
87
+ if (!current) throw new Error(`missing version in ${pkgPath}`)
88
+ const version = bumpVersion(current, bumpType)
89
+ json.version = version
90
+ await writeJson(pkgPath, json)
91
+ bumped[pkgName] = version
92
+ }
93
+
94
+ // Generate CHANGELOG.md per bumped package
95
+ const date = formatDate(now())
96
+ const byPackage: Record<string, Changeset[]> = {}
97
+ for (const cs of changesets) {
98
+ for (const pkg of Object.keys(cs.packages)) {
99
+ if (!pkgByName.has(pkg)) continue
100
+ const arr = byPackage[pkg] ?? []
101
+ arr.push(cs)
102
+ byPackage[pkg] = arr
103
+ }
104
+ }
105
+
106
+ for (const [pkgName, version] of Object.entries(bumped)) {
107
+ const list = byPackage[pkgName]
108
+ if (!list || list.length === 0) continue
109
+ const pkg = pkgByName.get(pkgName)
110
+ if (!pkg) continue
111
+ const entry = buildChangelogEntry({
112
+ version,
113
+ date,
114
+ bodies: list.map((c) => c.body),
115
+ })
116
+ const path = resolve(cwd, pkg.path, 'CHANGELOG.md')
117
+ let existing: string | null = null
118
+ if (await fileExists(path)) {
119
+ existing = await readFile(path, 'utf8')
120
+ }
121
+ await writeFile(path, prependChangelog(existing, entry))
122
+ }
123
+
124
+ // Delete consumed changeset files
125
+ for (const cs of changesets) {
126
+ await unlink(cs.file)
127
+ }
128
+ }
129
+
130
+ return bumped
131
+ }
@@ -0,0 +1,52 @@
1
+ import { resolve } from 'node:path'
2
+ import { loadConfig } from '../config/index.js'
3
+ import { defaultSpawn, runOrThrow, type SpawnFn } from '../utils/npm.js'
4
+
5
+ export type DeployCommandOptions = {
6
+ cwd: string
7
+ pkg?: string
8
+ env?: string
9
+ spawn?: SpawnFn
10
+ }
11
+
12
+ function pnpmExec(bin: string, ...args: string[]): string[] {
13
+ return ['pnpm', 'exec', bin, ...args]
14
+ }
15
+
16
+ export async function deploy(opts: DeployCommandOptions): Promise<void> {
17
+ const spawn = opts.spawn ?? defaultSpawn
18
+ const cwd = resolve(opts.cwd)
19
+ const cfg = loadConfig(cwd)
20
+
21
+ let targets = cfg.packages
22
+ if (opts.pkg !== undefined) {
23
+ const match = cfg.packages.find((p) => p.name === opts.pkg)
24
+ if (!match) {
25
+ throw new Error(`package not found: ${opts.pkg}`)
26
+ }
27
+ if (match.type !== 'webui' && match.type !== 'api') {
28
+ throw new Error(
29
+ `package ${opts.pkg} (type=${match.type}) is not deployable; cannot deploy non-webui/api packages`,
30
+ )
31
+ }
32
+ targets = [match]
33
+ } else {
34
+ targets = cfg.packages.filter((p) => p.type === 'webui' || p.type === 'api')
35
+ }
36
+
37
+ for (const pkg of targets) {
38
+ const pkgDir = resolve(cwd, pkg.path)
39
+ let argv: string[]
40
+ if (pkg.type === 'webui') {
41
+ argv = pnpmExec('wrangler', 'pages', 'deploy', 'dist')
42
+ } else if (pkg.type === 'api') {
43
+ argv = pnpmExec('wrangler', 'deploy')
44
+ } else {
45
+ continue
46
+ }
47
+ if (opts.env !== undefined) {
48
+ argv.push('--env', opts.env)
49
+ }
50
+ await runOrThrow(spawn, argv, pkgDir)
51
+ }
52
+ }