@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,262 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { basename, join, resolve } from 'node:path';
4
+ function jsonStringify(obj) {
5
+ return JSON.stringify(obj, null, 2);
6
+ }
7
+ /**
8
+ * Sanitize a directory name into a valid npm package name segment.
9
+ *
10
+ * Rules applied (per https://github.com/npm/validate-npm-package-name):
11
+ * - lowercase only
12
+ * - strip characters not in `[a-z0-9._-]` (replaces with hyphens)
13
+ * - strip leading `.`, `_`, or `-`
14
+ * - collapse consecutive hyphens
15
+ * - enforce 214-character npm limit
16
+ * - fall back to `'my-project'` if nothing remains
17
+ */
18
+ function toPackageName(dirName) {
19
+ return (dirName
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9._-]/g, '-') // replace invalid chars
22
+ .replace(/^[._-]+/, '') // strip leading dots/hyphens/underscores
23
+ .replace(/-+/g, '-') // collapse consecutive hyphens
24
+ .slice(0, 214) || // npm name length limit
25
+ 'my-project'); // fallback if everything was stripped
26
+ }
27
+ export async function init(opts) {
28
+ const targetDir = resolve(opts.targetDir);
29
+ const projectName = toPackageName(basename(targetDir));
30
+ // Check if directory is empty
31
+ if (existsSync(targetDir)) {
32
+ const entries = readdirSync(targetDir);
33
+ if (entries.length > 0) {
34
+ throw new Error(`Directory is not empty: ${targetDir}`);
35
+ }
36
+ }
37
+ else {
38
+ mkdirSync(targetDir, { recursive: true });
39
+ }
40
+ // Create root files
41
+ createRootPackageJson(targetDir, projectName);
42
+ createPromanYaml(targetDir, projectName);
43
+ createPnpmWorkspace(targetDir);
44
+ createBiomeJson(targetDir);
45
+ createTsConfig(targetDir);
46
+ createGitignore(targetDir);
47
+ // Create packages
48
+ createCorePackage(targetDir, projectName);
49
+ createCliPackage(targetDir, projectName);
50
+ // Format all JSON files with biome
51
+ try {
52
+ execSync('pnpm exec biome format --write .', { cwd: targetDir, stdio: 'ignore' });
53
+ }
54
+ catch {
55
+ // Ignore biome formatting errors during init - user can run format later
56
+ }
57
+ // Print post-init message
58
+ console.log(`✓ Created monorepo in ${targetDir}`);
59
+ console.log('');
60
+ console.log('Next steps:');
61
+ if (targetDir !== process.cwd()) {
62
+ console.log(` cd ${projectName}`);
63
+ }
64
+ console.log(' pnpm install');
65
+ console.log(' proman build');
66
+ }
67
+ function createRootPackageJson(targetDir, projectName) {
68
+ const content = {
69
+ name: projectName,
70
+ private: true,
71
+ type: 'module',
72
+ scripts: {
73
+ build: 'proman build',
74
+ test: 'proman test',
75
+ check: 'proman check',
76
+ format: 'proman format',
77
+ },
78
+ devDependencies: {
79
+ '@biomejs/biome': '^2.4.16',
80
+ '@shazhou/proman': '^0.7.0',
81
+ '@types/node': '^22.0.0',
82
+ typescript: '^5.9.3',
83
+ vitest: '^4.1.8',
84
+ },
85
+ };
86
+ writeFileSync(join(targetDir, 'package.json'), `${jsonStringify(content)}\n`);
87
+ }
88
+ function createPromanYaml(targetDir, projectName) {
89
+ const content = `packages:
90
+ - name: '@${projectName}/core'
91
+ path: packages/core
92
+ type: lib
93
+ - name: '@${projectName}/cli'
94
+ path: packages/cli
95
+ type: cli
96
+ `;
97
+ writeFileSync(join(targetDir, 'proman.yaml'), content);
98
+ }
99
+ function createPnpmWorkspace(targetDir) {
100
+ const content = `packages:
101
+ - 'packages/*'
102
+
103
+ allowBuilds:
104
+ esbuild: true
105
+ `;
106
+ writeFileSync(join(targetDir, 'pnpm-workspace.yaml'), content);
107
+ }
108
+ function createBiomeJson(targetDir) {
109
+ const content = {
110
+ $schema: 'https://biomejs.dev/schemas/2.4.16/schema.json',
111
+ assist: { actions: { source: { organizeImports: 'on' } } },
112
+ linter: {
113
+ enabled: true,
114
+ rules: { recommended: true },
115
+ },
116
+ formatter: {
117
+ enabled: true,
118
+ indentStyle: 'space',
119
+ indentWidth: 2,
120
+ lineWidth: 100,
121
+ },
122
+ javascript: {
123
+ formatter: {
124
+ quoteStyle: 'single',
125
+ semicolons: 'asNeeded',
126
+ trailingCommas: 'all',
127
+ },
128
+ },
129
+ files: {
130
+ includes: ['**', '!**/dist', '!**/node_modules', '!**/tests/fixtures', '!.worktrees'],
131
+ },
132
+ };
133
+ writeFileSync(join(targetDir, 'biome.json'), `${jsonStringify(content)}\n`);
134
+ }
135
+ function createTsConfig(targetDir) {
136
+ const content = {
137
+ compilerOptions: {
138
+ target: 'ESNext',
139
+ module: 'ESNext',
140
+ moduleResolution: 'bundler',
141
+ strict: true,
142
+ skipLibCheck: true,
143
+ verbatimModuleSyntax: true,
144
+ esModuleInterop: true,
145
+ resolveJsonModule: true,
146
+ types: ['node'],
147
+ lib: ['ESNext'],
148
+ },
149
+ references: [{ path: './packages/core' }, { path: './packages/cli' }],
150
+ };
151
+ writeFileSync(join(targetDir, 'tsconfig.json'), `${jsonStringify(content)}\n`);
152
+ }
153
+ function createGitignore(targetDir) {
154
+ const content = `node_modules
155
+ dist
156
+ .proman
157
+ *.tsbuildinfo
158
+ `;
159
+ writeFileSync(join(targetDir, '.gitignore'), content);
160
+ }
161
+ function createCorePackage(targetDir, projectName) {
162
+ const pkgDir = join(targetDir, 'packages', 'core');
163
+ mkdirSync(join(pkgDir, 'src'), { recursive: true });
164
+ // package.json
165
+ const packageJson = {
166
+ name: `@${projectName}/core`,
167
+ version: '0.0.1',
168
+ type: 'module',
169
+ exports: {
170
+ '.': {
171
+ types: './dist/index.d.ts',
172
+ default: './dist/index.js',
173
+ },
174
+ },
175
+ files: ['dist'],
176
+ scripts: {
177
+ build: 'tsc --build',
178
+ },
179
+ };
180
+ writeFileSync(join(pkgDir, 'package.json'), `${jsonStringify(packageJson)}\n`);
181
+ // tsconfig.json
182
+ const tsConfig = {
183
+ extends: '../../tsconfig.json',
184
+ compilerOptions: {
185
+ composite: true,
186
+ outDir: 'dist',
187
+ rootDir: 'src',
188
+ noEmit: false,
189
+ declaration: true,
190
+ },
191
+ include: ['src/**/*'],
192
+ };
193
+ writeFileSync(join(pkgDir, 'tsconfig.json'), `${jsonStringify(tsConfig)}\n`);
194
+ // src/index.ts
195
+ const indexTs = `export function hello(): string {
196
+ return 'hello'
197
+ }
198
+ `;
199
+ writeFileSync(join(pkgDir, 'src', 'index.ts'), indexTs);
200
+ // src/index.test.ts
201
+ const testTs = `import { describe, expect, test } from 'vitest'
202
+ import { hello } from './index.js'
203
+
204
+ describe('hello', () => {
205
+ test('returns hello', () => {
206
+ expect(hello()).toBe('hello')
207
+ })
208
+ })
209
+ `;
210
+ writeFileSync(join(pkgDir, 'src', 'index.test.ts'), testTs);
211
+ }
212
+ function createCliPackage(targetDir, projectName) {
213
+ const pkgDir = join(targetDir, 'packages', 'cli');
214
+ mkdirSync(join(pkgDir, 'src'), { recursive: true });
215
+ // package.json
216
+ const packageJson = {
217
+ name: `@${projectName}/cli`,
218
+ version: '0.0.1',
219
+ type: 'module',
220
+ bin: {
221
+ [projectName]: 'dist/cli.js',
222
+ },
223
+ files: ['dist'],
224
+ scripts: {
225
+ build: 'tsc --build',
226
+ },
227
+ dependencies: {
228
+ [`@${projectName}/core`]: 'workspace:*',
229
+ },
230
+ };
231
+ writeFileSync(join(pkgDir, 'package.json'), `${jsonStringify(packageJson)}\n`);
232
+ // tsconfig.json
233
+ const tsConfig = {
234
+ extends: '../../tsconfig.json',
235
+ compilerOptions: {
236
+ composite: true,
237
+ outDir: 'dist',
238
+ rootDir: 'src',
239
+ noEmit: false,
240
+ },
241
+ include: ['src/**/*'],
242
+ references: [{ path: '../core' }],
243
+ };
244
+ writeFileSync(join(pkgDir, 'tsconfig.json'), `${jsonStringify(tsConfig)}\n`);
245
+ // src/cli.ts
246
+ const cliTs = `#!/usr/bin/env node
247
+ import { hello } from '@${projectName}/core'
248
+
249
+ console.log(hello())
250
+ `;
251
+ writeFileSync(join(pkgDir, 'src', 'cli.ts'), cliTs);
252
+ // src/cli.test.ts
253
+ const testTs = `import { describe, expect, test } from 'vitest'
254
+
255
+ describe('cli', () => {
256
+ test('placeholder test', () => {
257
+ expect(true).toBe(true)
258
+ })
259
+ })
260
+ `;
261
+ writeFileSync(join(pkgDir, 'src', 'cli.test.ts'), testTs);
262
+ }
@@ -0,0 +1,19 @@
1
+ import { type SpawnFn } from '../utils/npm.js';
2
+ export type LinkCommandOptions = {
3
+ cwd: string;
4
+ packageName?: string;
5
+ spawn?: SpawnFn;
6
+ };
7
+ /**
8
+ * Link a package globally (provider mode) or link from global registry (consumer mode)
9
+ */
10
+ export declare function link(opts: LinkCommandOptions): Promise<void>;
11
+ /**
12
+ * Show currently linked packages
13
+ */
14
+ export declare function linkStatus(opts: Omit<LinkCommandOptions, 'packageName'>): Promise<string>;
15
+ /**
16
+ * Unlink packages (all or specific)
17
+ */
18
+ export declare function unlink(opts: LinkCommandOptions): Promise<void>;
19
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/commands/link.ts"],"names":[],"mappings":"AAEA,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAExE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AA0BD;;GAEG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BlE;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,aAAa,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAoD/F;AAED;;GAEG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgEpE"}
@@ -0,0 +1,155 @@
1
+ import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { defaultSpawn, runOrThrow } from '../utils/npm.js';
4
+ function readPackageJson(cwd) {
5
+ const pkgPath = join(cwd, 'package.json');
6
+ if (!existsSync(pkgPath)) {
7
+ throw new Error(`Missing package.json in ${cwd}`);
8
+ }
9
+ const json = JSON.parse(readFileSync(pkgPath, 'utf-8'));
10
+ // Runtime validation: ensure parsed value is an object (not array, primitive, or null)
11
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) {
12
+ throw new Error(`Invalid package.json at ${pkgPath}: expected a JSON object`);
13
+ }
14
+ return json;
15
+ }
16
+ function hasDistFolder(cwd) {
17
+ const distPath = join(cwd, 'dist');
18
+ return existsSync(distPath);
19
+ }
20
+ /**
21
+ * Link a package globally (provider mode) or link from global registry (consumer mode)
22
+ */
23
+ export async function link(opts) {
24
+ const spawn = opts.spawn ?? defaultSpawn;
25
+ const cwd = resolve(opts.cwd);
26
+ // Consumer mode: link specific package from global registry
27
+ if (opts.packageName) {
28
+ const pkg = readPackageJson(cwd);
29
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
30
+ if (!allDeps[opts.packageName]) {
31
+ throw new Error(`Package "${opts.packageName}" not found in dependencies or devDependencies of ${cwd}`);
32
+ }
33
+ await runOrThrow(spawn, ['pnpm', 'link', '--global', opts.packageName], cwd);
34
+ console.log(`✓ Linked ${opts.packageName} from global registry`);
35
+ return;
36
+ }
37
+ // Provider mode: link current package globally
38
+ const pkg = readPackageJson(cwd);
39
+ if (!pkg.name) {
40
+ throw new Error(`package.json in ${cwd} is missing a "name" field`);
41
+ }
42
+ if (!hasDistFolder(cwd)) {
43
+ throw new Error(`No dist/ folder in ${cwd}. Run \`proman build\` first.`);
44
+ }
45
+ await runOrThrow(spawn, ['pnpm', 'link', '--global'], cwd);
46
+ console.log(`✓ Linked ${pkg.name} globally`);
47
+ }
48
+ /**
49
+ * Show currently linked packages
50
+ */
51
+ export async function linkStatus(opts) {
52
+ const cwd = resolve(opts.cwd);
53
+ const nodeModulesDir = join(cwd, 'node_modules');
54
+ if (!existsSync(nodeModulesDir)) {
55
+ return 'No linked packages found';
56
+ }
57
+ const linkedPackages = [];
58
+ // Scan node_modules for symlinks
59
+ function scanDir(dir, prefix = '') {
60
+ if (!existsSync(dir))
61
+ return;
62
+ for (const entry of readdirSync(dir)) {
63
+ const fullPath = join(dir, entry);
64
+ const stat = lstatSync(fullPath);
65
+ // Check if it's a symlink
66
+ if (stat.isSymbolicLink()) {
67
+ const target = readlinkSync(fullPath);
68
+ const resolvedTarget = resolve(dir, target);
69
+ const pkgJsonPath = join(fullPath, 'package.json');
70
+ if (existsSync(pkgJsonPath)) {
71
+ try {
72
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
73
+ const name = pkgJson.name || `${prefix}${entry}`;
74
+ linkedPackages.push({ name, target: resolvedTarget });
75
+ }
76
+ catch {
77
+ // Invalid package.json, skip
78
+ }
79
+ }
80
+ }
81
+ else if (stat.isDirectory() && entry.startsWith('@')) {
82
+ // Scan scoped packages
83
+ scanDir(fullPath, `${entry}/`);
84
+ }
85
+ }
86
+ }
87
+ scanDir(nodeModulesDir);
88
+ if (linkedPackages.length === 0) {
89
+ return 'No linked packages found';
90
+ }
91
+ const lines = ['Linked packages:'];
92
+ for (const { name, target } of linkedPackages) {
93
+ lines.push(`• ${name} → ${target}`);
94
+ }
95
+ return lines.join('\n');
96
+ }
97
+ /**
98
+ * Unlink packages (all or specific)
99
+ */
100
+ export async function unlink(opts) {
101
+ const spawn = opts.spawn ?? defaultSpawn;
102
+ const cwd = resolve(opts.cwd);
103
+ // Unlink specific package
104
+ if (opts.packageName) {
105
+ await runOrThrow(spawn, ['pnpm', 'unlink', opts.packageName], cwd);
106
+ await runOrThrow(spawn, ['pnpm', 'install', opts.packageName], cwd);
107
+ console.log(`✓ Unlinked ${opts.packageName} and restored from registry`);
108
+ return;
109
+ }
110
+ // Unlink all packages
111
+ const nodeModulesDir = join(cwd, 'node_modules');
112
+ if (!existsSync(nodeModulesDir)) {
113
+ console.log('No linked packages to unlink');
114
+ return;
115
+ }
116
+ const linkedPackages = [];
117
+ // Find all symlinked packages
118
+ function scanDir(dir) {
119
+ if (!existsSync(dir))
120
+ return;
121
+ for (const entry of readdirSync(dir)) {
122
+ const fullPath = join(dir, entry);
123
+ const stat = lstatSync(fullPath);
124
+ if (stat.isSymbolicLink()) {
125
+ const pkgJsonPath = join(fullPath, 'package.json');
126
+ if (existsSync(pkgJsonPath)) {
127
+ try {
128
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
129
+ if (pkgJson.name) {
130
+ linkedPackages.push(pkgJson.name);
131
+ }
132
+ }
133
+ catch {
134
+ // Invalid package.json, skip
135
+ }
136
+ }
137
+ }
138
+ else if (stat.isDirectory() && entry.startsWith('@')) {
139
+ scanDir(fullPath);
140
+ }
141
+ }
142
+ }
143
+ scanDir(nodeModulesDir);
144
+ if (linkedPackages.length === 0) {
145
+ console.log('No linked packages to unlink');
146
+ return;
147
+ }
148
+ // Unlink each package
149
+ for (const pkgName of linkedPackages) {
150
+ await runOrThrow(spawn, ['pnpm', 'unlink', pkgName], cwd);
151
+ }
152
+ // Restore all packages
153
+ await runOrThrow(spawn, ['pnpm', 'install'], cwd);
154
+ console.log(`✓ Unlinked ${linkedPackages.length} package(s) and restored from registry`);
155
+ }
@@ -0,0 +1,18 @@
1
+ import { type GitOps } from '../utils/git.js';
2
+ import { type NpmRegistryFetch, type NpmRunner, type SpawnFn } from '../utils/npm.js';
3
+ export type { GitOps } from '../utils/git.js';
4
+ export type { NpmRunner } from '../utils/npm.js';
5
+ export type PublishOptions = {
6
+ skipTests?: boolean;
7
+ cwd?: string;
8
+ git?: GitOps;
9
+ npm?: NpmRunner;
10
+ registryFetch?: NpmRegistryFetch;
11
+ spawn?: SpawnFn;
12
+ };
13
+ /**
14
+ * Publish all packages. Reads each package's version from its own package.json.
15
+ * build → test → check → smoke test tarball → publish → commit → tag → push
16
+ */
17
+ export declare function publish(opts?: PublishOptions): Promise<void>;
18
+ //# sourceMappingURL=publish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,OAAO,EACb,MAAM,iBAAiB,CAAA;AAGxB,YAAY,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAC7C,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,SAAS,CAAA;IACf,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAoBD;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuHtE"}
@@ -0,0 +1,125 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { loadConfig } from '../config/load-config.js';
4
+ import { createGitOps } from '../utils/git.js';
5
+ import { createNpmRunner, defaultRegistryFetch, defaultSpawn, } from '../utils/npm.js';
6
+ import { smokeTestTarball } from '../utils/smoke-test.js';
7
+ const AUTHOR = '小橘 <xiaoju@shazhou.work>';
8
+ async function readJson(path) {
9
+ const text = await readFile(path, 'utf8');
10
+ return JSON.parse(text);
11
+ }
12
+ function isRcVersion(version) {
13
+ return /-rc\.\d+$/.test(version);
14
+ }
15
+ const ALREADY_PUBLISHED_RE = /cannot publish over the previously published versions|you cannot publish over the previously published version/i;
16
+ function isAlreadyPublished(message) {
17
+ return ALREADY_PUBLISHED_RE.test(message);
18
+ }
19
+ /**
20
+ * Publish all packages. Reads each package's version from its own package.json.
21
+ * build → test → check → smoke test tarball → publish → commit → tag → push
22
+ */
23
+ export async function publish(opts = {}) {
24
+ const { skipTests = false } = opts;
25
+ const cwd = opts.cwd ?? process.cwd();
26
+ const git = opts.git ?? createGitOps(cwd);
27
+ const fetchVersions = opts.registryFetch ?? defaultRegistryFetch;
28
+ const spawn = opts.spawn ?? defaultSpawn;
29
+ const cfg = loadConfig(cwd);
30
+ const npm = opts.npm ?? createNpmRunner(cwd);
31
+ const pkgJsonMap = {};
32
+ for (const pkg of cfg.packages) {
33
+ const pkgPath = resolve(cwd, pkg.path, 'package.json');
34
+ const json = await readJson(pkgPath);
35
+ const version = json.version;
36
+ if (!version)
37
+ throw new Error(`missing version in ${pkgPath}`);
38
+ pkgJsonMap[pkg.name] = { version, private: json.private === true };
39
+ }
40
+ const publishablePackages = cfg.packages.filter((pkg) => pkg.private !== true && pkgJsonMap[pkg.name]?.private !== true);
41
+ // Read each publishable package's version
42
+ const versions = {};
43
+ for (const pkg of publishablePackages) {
44
+ versions[pkg.name] = pkgJsonMap[pkg.name]?.version;
45
+ }
46
+ // Build + test + check
47
+ await npm.install();
48
+ await npm.build();
49
+ console.log('✓ build');
50
+ if (!skipTests) {
51
+ await npm.test();
52
+ console.log('✓ test');
53
+ }
54
+ await npm.check();
55
+ console.log('✓ check');
56
+ // Log skipped private packages
57
+ for (const pkg of cfg.packages) {
58
+ if (pkg.private === true || pkgJsonMap[pkg.name]?.private === true) {
59
+ console.log(`⏭ skipped ${pkg.name} (private)`);
60
+ }
61
+ }
62
+ // Publish each publishable package
63
+ const access = cfg.release?.access;
64
+ for (let i = 0; i < publishablePackages.length; i++) {
65
+ const entry = publishablePackages[i];
66
+ const version = versions[entry.name];
67
+ const isRc = isRcVersion(version);
68
+ const publishTag = isRc ? 'rc' : 'latest';
69
+ const pkgDir = resolve(cwd, entry.path);
70
+ // Pre-check: skip if already published on registry
71
+ const existingVersions = await fetchVersions(entry.name);
72
+ if (existingVersions.includes(version)) {
73
+ console.log(`⏭ skipped ${entry.name}@${version} (already published)`);
74
+ continue;
75
+ }
76
+ // Smoke test: validate tarball before publishing
77
+ try {
78
+ await smokeTestTarball(pkgDir, spawn);
79
+ }
80
+ catch (err) {
81
+ const message = err.message;
82
+ const published = publishablePackages.slice(0, i).map((p) => p.name);
83
+ const remaining = publishablePackages.slice(i + 1).map((p) => p.name);
84
+ const msg = `smoke test failed for ${entry.name}: ${message}\n` +
85
+ ` published: ${published.join(', ') || '(none)'}\n` +
86
+ ` unpublished: ${[entry.name, ...remaining].join(', ')}`;
87
+ throw new Error(msg);
88
+ }
89
+ try {
90
+ await npm.publish(pkgDir, { tag: publishTag, ...(access ? { access } : {}) });
91
+ console.log(`✓ published ${entry.name}@${version}`);
92
+ }
93
+ catch (err) {
94
+ const message = err.message;
95
+ // Fallback: catch the error in case of race condition
96
+ if (isAlreadyPublished(message)) {
97
+ console.log(`⏭ skipped ${entry.name}@${version} (already published)`);
98
+ continue;
99
+ }
100
+ const published = publishablePackages.slice(0, i).map((p) => p.name);
101
+ const remaining = publishablePackages.slice(i + 1).map((p) => p.name);
102
+ const msg = `publish failed for ${entry.name}: ${message}\n` +
103
+ ` published: ${published.join(', ') || '(none)'}\n` +
104
+ ` unpublished: ${[entry.name, ...remaining].join(', ')}`;
105
+ throw new Error(msg);
106
+ }
107
+ }
108
+ // Commit + tag + push all publishable packages
109
+ // Changelog generation and changeset cleanup are now bump's responsibility (issue #74)
110
+ await git.addAll();
111
+ const tagPrefix = cfg.release?.gitTagPrefix ?? 'v';
112
+ const bumpedVersions = Object.entries(versions);
113
+ const commitVersion = bumpedVersions[0]?.[1] ?? 'unknown';
114
+ await git.commit(`release: v${commitVersion}`, AUTHOR);
115
+ for (const [pkgName, version] of bumpedVersions) {
116
+ const tagName = `${pkgName}@${tagPrefix}${version}`;
117
+ await git.tag(tagName, `Release ${pkgName}@${version}`);
118
+ }
119
+ await git.pushTags();
120
+ await git.push('main');
121
+ for (const [pkgName, version] of bumpedVersions) {
122
+ console.log(`✓ tagged ${pkgName}@${tagPrefix}${version}`);
123
+ }
124
+ console.log(`✓ pushed`);
125
+ }
@@ -0,0 +1,4 @@
1
+ export { loadConfig } from './load-config.js';
2
+ export type { PackageEntry, PackageType, PromanConfig, ReleaseConfig, } from './types.js';
3
+ export { validateConfig } from './validate-config.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { loadConfig } from './load-config.js';
2
+ export { validateConfig } from './validate-config.js';
@@ -0,0 +1,6 @@
1
+ import type { PromanConfig } from './types.js';
2
+ /**
3
+ * Loads `proman.yaml` from the given cwd (or `process.cwd()`).
4
+ */
5
+ export declare function loadConfig(cwd?: string): PromanConfig;
6
+ //# sourceMappingURL=load-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../../src/config/load-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,YAAY,CAAA;AAiB7D;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,YAAY,CASpE"}
@@ -0,0 +1,29 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import { validateConfig } from './validate-config.js';
5
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
6
+ const DEFAULT_GIT_TAG_PREFIX = 'v';
7
+ function applyDefaults(config) {
8
+ const release = {
9
+ registry: config.release?.registry ?? DEFAULT_REGISTRY,
10
+ gitTagPrefix: config.release?.gitTagPrefix ?? DEFAULT_GIT_TAG_PREFIX,
11
+ };
12
+ if (config.release?.access !== undefined) {
13
+ release.access = config.release.access;
14
+ }
15
+ return { ...config, release };
16
+ }
17
+ /**
18
+ * Loads `proman.yaml` from the given cwd (or `process.cwd()`).
19
+ */
20
+ export function loadConfig(cwd = process.cwd()) {
21
+ const absPath = resolve(cwd, 'proman.yaml');
22
+ if (!existsSync(absPath)) {
23
+ throw new Error(`proman.yaml not found at ${absPath}`);
24
+ }
25
+ const text = readFileSync(absPath, 'utf8');
26
+ const raw = parse(text);
27
+ const validated = validateConfig(raw);
28
+ return applyDefaults(validated);
29
+ }
@@ -0,0 +1,17 @@
1
+ export type PackageType = 'lib' | 'cli' | 'webui' | 'api';
2
+ export type PackageEntry = {
3
+ name: string;
4
+ path: string;
5
+ type: PackageType;
6
+ private?: boolean;
7
+ };
8
+ export type ReleaseConfig = {
9
+ registry?: string;
10
+ access?: 'public' | 'restricted';
11
+ gitTagPrefix?: string;
12
+ };
13
+ export type PromanConfig = {
14
+ packages: PackageEntry[];
15
+ release?: ReleaseConfig;
16
+ };
17
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,GAAG,KAAK,CAAA;AAEzD,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAA;IAChC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,OAAO,CAAC,EAAE,aAAa,CAAA;CACxB,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { PromanConfig } from './types.js';
2
+ /**
3
+ * Pure validator. Throws Error with descriptive message on failure.
4
+ * Returns a typed PromanConfig (does not mutate input, does not apply defaults).
5
+ */
6
+ export declare function validateConfig(value: unknown): PromanConfig;
7
+ //# sourceMappingURL=validate-config.d.ts.map