glab-setup-git-identity 0.6.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 (66) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +372 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +143 -0
  9. package/LICENSE +24 -0
  10. package/README.md +455 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/docs/case-studies/issue-13/README.md +195 -0
  14. package/docs/case-studies/issue-13/hive-mind-issue-960.json +23 -0
  15. package/docs/case-studies/issue-13/hive-mind-pr-961-diff.txt +773 -0
  16. package/docs/case-studies/issue-13/hive-mind-pr-961.json +126 -0
  17. package/docs/case-studies/issue-21/README.md +384 -0
  18. package/docs/case-studies/issue-21/ci-logs/run-20803315337.txt +1188 -0
  19. package/docs/case-studies/issue-21/ci-logs/run-20885464993.txt +1310 -0
  20. package/docs/case-studies/issue-21/issue-111-data.txt +15 -0
  21. package/docs/case-studies/issue-21/issue-113-data.txt +15 -0
  22. package/docs/case-studies/issue-21/pr-112-data.json +109 -0
  23. package/docs/case-studies/issue-21/pr-112-diff.patch +1336 -0
  24. package/docs/case-studies/issue-21/pr-114-data.json +126 -0
  25. package/docs/case-studies/issue-21/pr-114-diff.patch +879 -0
  26. package/docs/case-studies/issue-3/README.md +338 -0
  27. package/docs/case-studies/issue-3/created-issues.md +32 -0
  28. package/docs/case-studies/issue-3/issue-data.json +29 -0
  29. package/docs/case-studies/issue-3/original-format-release-notes.mjs +212 -0
  30. package/docs/case-studies/issue-3/reference-pr-59-diff.txt +614 -0
  31. package/docs/case-studies/issue-3/reference-pr-59.json +109 -0
  32. package/docs/case-studies/issue-3/release-v0.1.0.json +9 -0
  33. package/docs/case-studies/issue-3/repositories-with-same-script.json +22 -0
  34. package/docs/case-studies/issue-3/research-notes.md +33 -0
  35. package/docs/case-studies/issue-7/BEST-PRACTICES-COMPARISON.md +334 -0
  36. package/docs/case-studies/issue-7/FORMATTER-COMPARISON.md +649 -0
  37. package/docs/case-studies/issue-7/current-repository-analysis.json +70 -0
  38. package/docs/case-studies/issue-7/effect-template-analysis.json +178 -0
  39. package/eslint.config.js +91 -0
  40. package/examples/basic-usage.js +64 -0
  41. package/experiments/test-changeset-scripts.mjs +303 -0
  42. package/experiments/test-failure-detection.mjs +143 -0
  43. package/experiments/test-format-major-changes.mjs +49 -0
  44. package/experiments/test-format-minor-changes.mjs +52 -0
  45. package/experiments/test-format-no-hash.mjs +43 -0
  46. package/experiments/test-format-patch-changes.mjs +46 -0
  47. package/package.json +80 -0
  48. package/scripts/changeset-version.mjs +75 -0
  49. package/scripts/check-changesets.mjs +67 -0
  50. package/scripts/check-version.mjs +129 -0
  51. package/scripts/create-github-release.mjs +93 -0
  52. package/scripts/create-manual-changeset.mjs +89 -0
  53. package/scripts/detect-code-changes.mjs +194 -0
  54. package/scripts/format-github-release.mjs +83 -0
  55. package/scripts/format-release-notes.mjs +219 -0
  56. package/scripts/instant-version-bump.mjs +172 -0
  57. package/scripts/js-paths.mjs +177 -0
  58. package/scripts/merge-changesets.mjs +263 -0
  59. package/scripts/publish-to-npm.mjs +302 -0
  60. package/scripts/setup-npm.mjs +37 -0
  61. package/scripts/validate-changeset.mjs +265 -0
  62. package/scripts/version-and-commit.mjs +284 -0
  63. package/src/cli.js +386 -0
  64. package/src/index.d.ts +255 -0
  65. package/src/index.js +563 -0
  66. package/tests/index.test.js +137 -0
@@ -0,0 +1,879 @@
1
+ diff --git a/docs/case-studies/issue-113/README.md b/docs/case-studies/issue-113/README.md
2
+ new file mode 100644
3
+ index 0000000..bd453f9
4
+ --- /dev/null
5
+ +++ b/docs/case-studies/issue-113/README.md
6
+ @@ -0,0 +1,111 @@
7
+ +# Case Study: Issue #113 - JavaScript Publish Does Not Work
8
+ +
9
+ +## Summary
10
+ +
11
+ +The JavaScript CI/CD pipeline was failing during the release step due to a subtle bug related to how the `command-stream` library handles the `cd` command.
12
+ +
13
+ +## Timeline of Events
14
+ +
15
+ +1. **CI Run Triggered**: Push to main branch triggered the JS CI/CD Pipeline (run #20885464993)
16
+ +2. **Tests Passed**: Lint, format check, and unit tests all passed successfully
17
+ +3. **Release Job Started**: The release job started and began processing changesets
18
+ +4. **Version Bump Executed**: The `version-and-commit.mjs` script ran `cd js && npm run changeset:version`
19
+ +5. **Failure**: After the version bump completed, the script failed with:
20
+ + ```
21
+ + Error: ENOENT: no such file or directory, open './js/package.json'
22
+ + ```
23
+ +
24
+ +## Root Cause Analysis
25
+ +
26
+ +### The Bug
27
+ +
28
+ +The root cause was a subtle interaction between the `command-stream` library and Node.js's process working directory:
29
+ +
30
+ +1. **command-stream's Virtual `cd` Command**: The `command-stream` library implements `cd` as a **virtual command** that calls `process.chdir()` on the Node.js process itself, rather than just affecting the subprocess.
31
+ +
32
+ +2. **Working Directory Persistence**: When the script executed:
33
+ + ```javascript
34
+ + await $`cd js && npm run changeset:version`;
35
+ + ```
36
+ + The `cd js` command permanently changed the Node.js process's working directory from the repository root to the `js/` subdirectory.
37
+ +
38
+ +3. **Subsequent File Access Failure**: After the command returned, when the script tried to read `./js/package.json`, it was looking for the file relative to the **new** working directory (`js/`), which would resolve to `js/js/package.json` - a path that doesn't exist.
39
+ +
40
+ +### Code Flow
41
+ +
42
+ +```
43
+ +Repository Root (/)
44
+ +├── js/
45
+ +│ └── package.json <- This is what we want to read
46
+ +└── scripts/
47
+ + └── version-and-commit.mjs
48
+ +
49
+ +1. Script starts with cwd = /
50
+ +2. Script runs: await $`cd js && npm run changeset:version`
51
+ +3. command-stream's cd command calls: process.chdir('js')
52
+ +4. cwd is now /js/
53
+ +5. Script tries to read: readFileSync('./js/package.json')
54
+ +6. This resolves to: /js/js/package.json <- DOES NOT EXIST!
55
+ +7. Error: ENOENT
56
+ +```
57
+ +
58
+ +### Why This Was Hard to Detect
59
+ +
60
+ +- The `cd` command in most shell scripts only affects the subprocess, not the parent process
61
+ +- Developers familiar with Unix shells would not expect `cd` to affect the Node.js process
62
+ +- The error message didn't clearly indicate that the working directory had changed
63
+ +- The `command-stream` library documentation doesn't prominently warn about this behavior
64
+ +
65
+ +## Solution
66
+ +
67
+ +The fix involves saving the original working directory and restoring it after any command that uses `cd`:
68
+ +
69
+ +```javascript
70
+ +// Store the original working directory
71
+ +const originalCwd = process.cwd();
72
+ +
73
+ +try {
74
+ + // ... code that uses cd ...
75
+ + await $`cd js && npm run changeset:version`;
76
+ +
77
+ + // Restore the original working directory
78
+ + process.chdir(originalCwd);
79
+ +
80
+ + // Now file operations work correctly
81
+ + const packageJson = JSON.parse(readFileSync('./js/package.json', 'utf8'));
82
+ +} catch (error) {
83
+ + // Handle error
84
+ +}
85
+ +```
86
+ +
87
+ +### Files Modified
88
+ +
89
+ +1. **scripts/version-and-commit.mjs**: Added cwd preservation and restoration after `cd js && npm run changeset:version`
90
+ +
91
+ +2. **scripts/instant-version-bump.mjs**: Added cwd preservation and restoration after:
92
+ + - `cd js && npm version ${bumpType} --no-git-tag-version`
93
+ + - `cd js && npm install --package-lock-only --legacy-peer-deps`
94
+ +
95
+ +3. **scripts/publish-to-npm.mjs**: Added cwd preservation and restoration after `cd js && npm run changeset:publish`, including proper handling in the retry loop error path
96
+ +
97
+ +## Lessons Learned
98
+ +
99
+ +1. **Understand Library Internals**: Third-party libraries may have non-obvious behaviors. The `command-stream` library's virtual `cd` command is a powerful feature for maintaining working directory state, but it can cause issues if not handled properly.
100
+ +
101
+ +2. **Test Edge Cases**: The CI environment differs from local development. File path handling can behave differently depending on the working directory context.
102
+ +
103
+ +3. **Add Defensive Code**: When using commands that modify process state, always save and restore the original state.
104
+ +
105
+ +4. **Document Non-Obvious Behaviors**: The fix includes detailed comments explaining why the `process.chdir()` restoration is necessary.
106
+ +
107
+ +## CI Logs
108
+ +
109
+ +The full CI logs are preserved in:
110
+ +- `ci-logs/full-run-20885464993.log` - Complete run log
111
+ +- `ci-logs/release-job-60008012717.log` - Detailed release job log
112
+ +
113
+ +## References
114
+ +
115
+ +- [GitHub Issue #113](https://github.com/link-assistant/agent/issues/113)
116
+ +- [CI Run #20885464993](https://github.com/link-assistant/agent/actions/runs/20885464993)
117
+ +- [command-stream npm package](https://www.npmjs.com/package/command-stream)
118
+ diff --git a/scripts/instant-version-bump.mjs b/scripts/instant-version-bump.mjs
119
+ index c1a34dd..7673338 100644
120
+ --- a/scripts/instant-version-bump.mjs
121
+ +++ b/scripts/instant-version-bump.mjs
122
+ @@ -14,6 +14,13 @@
123
+
124
+ import { readFileSync, writeFileSync } from 'fs';
125
+
126
+ +import {
127
+ + getJsRoot,
128
+ + getPackageJsonPath,
129
+ + needsCd,
130
+ + parseJsRootConfig,
131
+ +} from './js-paths.mjs';
132
+ +
133
+ // Load use-m dynamically
134
+ const { use } = eval(
135
+ await (await fetch('https://unpkg.com/use-m/use.js')).text()
136
+ @@ -37,11 +44,24 @@ const config = makeConfig({
137
+ type: 'string',
138
+ default: getenv('DESCRIPTION', ''),
139
+ describe: 'Description for the version bump',
140
+ + })
141
+ + .option('js-root', {
142
+ + type: 'string',
143
+ + default: getenv('JS_ROOT', ''),
144
+ + describe: 'JavaScript package root directory (auto-detected if not specified)',
145
+ }),
146
+ });
147
+
148
+ +// Store the original working directory to restore after cd commands
149
+ +// IMPORTANT: command-stream's cd is a virtual command that calls process.chdir()
150
+ +const originalCwd = process.cwd();
151
+ +
152
+ try {
153
+ - const { bumpType, description } = config;
154
+ + const { bumpType, description, jsRoot: jsRootArg } = config;
155
+ +
156
+ + // Get JavaScript package root (auto-detect or use explicit config)
157
+ + const jsRootConfig = jsRootArg || parseJsRootConfig();
158
+ + const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true });
159
+ const finalDescription = description || `Manual ${bumpType} release`;
160
+
161
+ if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) {
162
+ @@ -54,15 +74,22 @@ try {
163
+ console.log(`\nBumping version (${bumpType})...`);
164
+
165
+ // Get current version
166
+ - const packageJson = JSON.parse(readFileSync('js/package.json', 'utf-8'));
167
+ + const packageJsonPath = getPackageJsonPath({ jsRoot });
168
+ + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
169
+ const oldVersion = packageJson.version;
170
+ console.log(`Current version: ${oldVersion}`);
171
+
172
+ // Bump version using npm version (doesn't create git tag)
173
+ - await $`cd js && npm version ${bumpType} --no-git-tag-version`;
174
+ + // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after
175
+ + if (needsCd({ jsRoot })) {
176
+ + await $`cd ${jsRoot} && npm version ${bumpType} --no-git-tag-version`;
177
+ + process.chdir(originalCwd);
178
+ + } else {
179
+ + await $`npm version ${bumpType} --no-git-tag-version`;
180
+ + }
181
+
182
+ // Get new version
183
+ - const updatedPackageJson = JSON.parse(readFileSync('js/package.json', 'utf-8'));
184
+ + const updatedPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
185
+ const newVersion = updatedPackageJson.version;
186
+ console.log(`New version: ${newVersion}`);
187
+
188
+ @@ -108,7 +135,13 @@ try {
189
+
190
+ // Synchronize package-lock.json
191
+ console.log('\nSynchronizing package-lock.json...');
192
+ - await $`cd js && npm install --package-lock-only --legacy-peer-deps`;
193
+ + // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after
194
+ + if (needsCd({ jsRoot })) {
195
+ + await $`cd ${jsRoot} && npm install --package-lock-only --legacy-peer-deps`;
196
+ + process.chdir(originalCwd);
197
+ + } else {
198
+ + await $`npm install --package-lock-only --legacy-peer-deps`;
199
+ + }
200
+
201
+ console.log('\n✅ Instant version bump complete');
202
+ console.log(`Version: ${oldVersion} → ${newVersion}`);
203
+ diff --git a/scripts/js-paths.mjs b/scripts/js-paths.mjs
204
+ new file mode 100644
205
+ index 0000000..810d56b
206
+ --- /dev/null
207
+ +++ b/scripts/js-paths.mjs
208
+ @@ -0,0 +1,159 @@
209
+ +#!/usr/bin/env node
210
+ +
211
+ +/**
212
+ + * JavaScript package path detection utility
213
+ + *
214
+ + * Automatically detects the JavaScript package root for both:
215
+ + * - Single-language repositories (package.json in root)
216
+ + * - Multi-language repositories (package.json in js/ subfolder)
217
+ + *
218
+ + * Usage:
219
+ + * import { getJsRoot, getPackageJsonPath, getChangesetDir } from './js-paths.mjs';
220
+ + *
221
+ + * const jsRoot = getJsRoot(); // Returns 'js' or '.'
222
+ + * const pkgPath = getPackageJsonPath(); // Returns 'js/package.json' or './package.json'
223
+ + */
224
+ +
225
+ +import { existsSync } from 'fs';
226
+ +import { join } from 'path';
227
+ +
228
+ +// Cache for detected paths (computed once per process)
229
+ +let cachedJsRoot = null;
230
+ +
231
+ +/**
232
+ + * Detect JavaScript package root directory
233
+ + * Checks in order:
234
+ + * 1. ./package.json (single-language repo)
235
+ + * 2. ./js/package.json (multi-language repo)
236
+ + *
237
+ + * @param {Object} options - Configuration options
238
+ + * @param {string} [options.jsRoot] - Explicitly set JavaScript root (overrides auto-detection)
239
+ + * @param {boolean} [options.verbose=false] - Log detection details
240
+ + * @returns {string} The JavaScript root directory ('.' or 'js')
241
+ + * @throws {Error} If no package.json is found in expected locations
242
+ + */
243
+ +export function getJsRoot(options = {}) {
244
+ + const { jsRoot: explicitRoot, verbose = false } = options;
245
+ +
246
+ + // If explicitly configured, use that
247
+ + if (explicitRoot !== undefined) {
248
+ + if (verbose) {
249
+ + console.log(`Using explicitly configured JavaScript root: ${explicitRoot}`);
250
+ + }
251
+ + return explicitRoot;
252
+ + }
253
+ +
254
+ + // Return cached value if already computed
255
+ + if (cachedJsRoot !== null) {
256
+ + return cachedJsRoot;
257
+ + }
258
+ +
259
+ + // Check for single-language repo (package.json in root)
260
+ + if (existsSync('./package.json')) {
261
+ + if (verbose) {
262
+ + console.log('Detected single-language repository (package.json in root)');
263
+ + }
264
+ + cachedJsRoot = '.';
265
+ + return cachedJsRoot;
266
+ + }
267
+ +
268
+ + // Check for multi-language repo (package.json in js/ subfolder)
269
+ + if (existsSync('./js/package.json')) {
270
+ + if (verbose) {
271
+ + console.log('Detected multi-language repository (package.json in js/)');
272
+ + }
273
+ + cachedJsRoot = 'js';
274
+ + return cachedJsRoot;
275
+ + }
276
+ +
277
+ + // No package.json found
278
+ + throw new Error(
279
+ + 'Could not find package.json in expected locations.\n' +
280
+ + 'Searched in:\n' +
281
+ + ' - ./package.json (single-language repository)\n' +
282
+ + ' - ./js/package.json (multi-language repository)\n\n' +
283
+ + 'To fix this, either:\n' +
284
+ + ' 1. Run the script from the repository root\n' +
285
+ + ' 2. Explicitly configure the JavaScript root using --js-root option\n' +
286
+ + ' 3. Set the JS_ROOT environment variable'
287
+ + );
288
+ +}
289
+ +
290
+ +/**
291
+ + * Get the path to package.json
292
+ + * @param {Object} options - Configuration options (passed to getJsRoot)
293
+ + * @returns {string} Path to package.json
294
+ + */
295
+ +export function getPackageJsonPath(options = {}) {
296
+ + const jsRoot = getJsRoot(options);
297
+ + return jsRoot === '.' ? './package.json' : join(jsRoot, 'package.json');
298
+ +}
299
+ +
300
+ +/**
301
+ + * Get the path to package-lock.json
302
+ + * @param {Object} options - Configuration options (passed to getJsRoot)
303
+ + * @returns {string} Path to package-lock.json
304
+ + */
305
+ +export function getPackageLockPath(options = {}) {
306
+ + const jsRoot = getJsRoot(options);
307
+ + return jsRoot === '.' ? './package-lock.json' : join(jsRoot, 'package-lock.json');
308
+ +}
309
+ +
310
+ +/**
311
+ + * Get the path to .changeset directory
312
+ + * @param {Object} options - Configuration options (passed to getJsRoot)
313
+ + * @returns {string} Path to .changeset directory
314
+ + */
315
+ +export function getChangesetDir(options = {}) {
316
+ + const jsRoot = getJsRoot(options);
317
+ + return jsRoot === '.' ? './.changeset' : join(jsRoot, '.changeset');
318
+ +}
319
+ +
320
+ +/**
321
+ + * Get the cd command prefix for running npm commands
322
+ + * Returns empty string for single-language repos, 'cd js && ' for multi-language repos
323
+ + * @param {Object} options - Configuration options (passed to getJsRoot)
324
+ + * @returns {string} CD prefix for shell commands
325
+ + */
326
+ +export function getCdPrefix(options = {}) {
327
+ + const jsRoot = getJsRoot(options);
328
+ + return jsRoot === '.' ? '' : `cd ${jsRoot} && `;
329
+ +}
330
+ +
331
+ +/**
332
+ + * Check if we need to change directory before running npm commands
333
+ + * @param {Object} options - Configuration options (passed to getJsRoot)
334
+ + * @returns {boolean} True if cd is needed
335
+ + */
336
+ +export function needsCd(options = {}) {
337
+ + const jsRoot = getJsRoot(options);
338
+ + return jsRoot !== '.';
339
+ +}
340
+ +
341
+ +/**
342
+ + * Reset the cached JavaScript root (useful for testing)
343
+ + */
344
+ +export function resetCache() {
345
+ + cachedJsRoot = null;
346
+ +}
347
+ +
348
+ +/**
349
+ + * Parse JavaScript root from CLI arguments or environment
350
+ + * Supports --js-root argument and JS_ROOT environment variable
351
+ + * @returns {string|undefined} Configured JavaScript root or undefined for auto-detection
352
+ + */
353
+ +export function parseJsRootConfig() {
354
+ + // Check CLI arguments
355
+ + const args = process.argv.slice(2);
356
+ + const jsRootIndex = args.indexOf('--js-root');
357
+ + if (jsRootIndex >= 0 && args[jsRootIndex + 1]) {
358
+ + return args[jsRootIndex + 1];
359
+ + }
360
+ +
361
+ + // Check environment variable
362
+ + if (process.env.JS_ROOT) {
363
+ + return process.env.JS_ROOT;
364
+ + }
365
+ +
366
+ + return undefined;
367
+ +}
368
+ diff --git a/scripts/publish-to-npm.mjs b/scripts/publish-to-npm.mjs
369
+ index 450af67..9b41bc4 100644
370
+ --- a/scripts/publish-to-npm.mjs
371
+ +++ b/scripts/publish-to-npm.mjs
372
+ @@ -15,6 +15,13 @@
373
+
374
+ import { readFileSync, appendFileSync } from 'fs';
375
+
376
+ +import {
377
+ + getJsRoot,
378
+ + getPackageJsonPath,
379
+ + needsCd,
380
+ + parseJsRootConfig,
381
+ +} from './js-paths.mjs';
382
+ +
383
+ // Package name from package.json
384
+ const PACKAGE_NAME = '@link-assistant/agent';
385
+
386
+ @@ -30,14 +37,24 @@ const { makeConfig } = await use('lino-arguments');
387
+ // Parse CLI arguments using lino-arguments
388
+ const config = makeConfig({
389
+ yargs: ({ yargs, getenv }) =>
390
+ - yargs.option('should-pull', {
391
+ - type: 'boolean',
392
+ - default: getenv('SHOULD_PULL', false),
393
+ - describe: 'Pull latest changes before publishing',
394
+ - }),
395
+ + yargs
396
+ + .option('should-pull', {
397
+ + type: 'boolean',
398
+ + default: getenv('SHOULD_PULL', false),
399
+ + describe: 'Pull latest changes before publishing',
400
+ + })
401
+ + .option('js-root', {
402
+ + type: 'string',
403
+ + default: getenv('JS_ROOT', ''),
404
+ + describe: 'JavaScript package root directory (auto-detected if not specified)',
405
+ + }),
406
+ });
407
+
408
+ -const { shouldPull } = config;
409
+ +const { shouldPull, jsRoot: jsRootArg } = config;
410
+ +
411
+ +// Get JavaScript package root (auto-detect or use explicit config)
412
+ +const jsRootConfig = jsRootArg || parseJsRootConfig();
413
+ +const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true });
414
+ const MAX_RETRIES = 3;
415
+ const RETRY_DELAY = 10000; // 10 seconds
416
+
417
+ @@ -62,6 +79,10 @@ function setOutput(key, value) {
418
+ }
419
+
420
+ async function main() {
421
+ + // Store the original working directory to restore after cd commands
422
+ + // IMPORTANT: command-stream's cd is a virtual command that calls process.chdir()
423
+ + const originalCwd = process.cwd();
424
+ +
425
+ try {
426
+ if (shouldPull) {
427
+ // Pull the latest changes we just pushed
428
+ @@ -69,7 +90,8 @@ async function main() {
429
+ }
430
+
431
+ // Get current version
432
+ - const packageJson = JSON.parse(readFileSync('./js/package.json', 'utf8'));
433
+ + const packageJsonPath = getPackageJsonPath({ jsRoot });
434
+ + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
435
+ const currentVersion = packageJson.version;
436
+ console.log(`Current version to publish: ${currentVersion}`);
437
+
438
+ @@ -101,7 +123,14 @@ async function main() {
439
+ for (let i = 1; i <= MAX_RETRIES; i++) {
440
+ console.log(`Publish attempt ${i} of ${MAX_RETRIES}...`);
441
+ try {
442
+ - await $`npm run changeset:publish`;
443
+ + // Run changeset:publish from the js directory where package.json with this script exists
444
+ + // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after
445
+ + if (needsCd({ jsRoot })) {
446
+ + await $`cd ${jsRoot} && npm run changeset:publish`;
447
+ + process.chdir(originalCwd);
448
+ + } else {
449
+ + await $`npm run changeset:publish`;
450
+ + }
451
+ setOutput('published', 'true');
452
+ setOutput('published_version', currentVersion);
453
+ console.log(
454
+ @@ -109,6 +138,10 @@ async function main() {
455
+ );
456
+ return;
457
+ } catch (_error) {
458
+ + // Restore cwd on error before retry
459
+ + if (needsCd({ jsRoot })) {
460
+ + process.chdir(originalCwd);
461
+ + }
462
+ if (i < MAX_RETRIES) {
463
+ console.log(
464
+ `Publish failed, waiting ${RETRY_DELAY / 1000}s before retry...`
465
+ diff --git a/scripts/rust-collect-changelog.mjs b/scripts/rust-collect-changelog.mjs
466
+ index dc4f385..dd6712c 100644
467
+ --- a/scripts/rust-collect-changelog.mjs
468
+ +++ b/scripts/rust-collect-changelog.mjs
469
+ @@ -15,8 +15,29 @@ import {
470
+ } from 'fs';
471
+ import { join } from 'path';
472
+
473
+ -const CHANGELOG_DIR = 'rust/changelog.d';
474
+ -const CHANGELOG_FILE = 'rust/CHANGELOG.md';
475
+ +import {
476
+ + getRustRoot,
477
+ + getCargoTomlPath,
478
+ + getChangelogDir,
479
+ + getChangelogPath,
480
+ + parseRustRootConfig,
481
+ +} from './rust-paths.mjs';
482
+ +
483
+ +// Simple CLI argument parsing
484
+ +const args = process.argv.slice(2);
485
+ +const getArg = (name, defaultValue) => {
486
+ + const index = args.indexOf(`--${name}`);
487
+ + return index >= 0 && args[index + 1] ? args[index + 1] : defaultValue;
488
+ +};
489
+ +
490
+ +// Get Rust package root (auto-detect or use explicit config)
491
+ +const rustRootConfig = getArg('rust-root', '') || parseRustRootConfig();
492
+ +const rustRoot = getRustRoot({ rustRoot: rustRootConfig || undefined, verbose: true });
493
+ +
494
+ +// Get paths based on detected/configured rust root
495
+ +const CARGO_TOML = getCargoTomlPath({ rustRoot });
496
+ +const CHANGELOG_DIR = getChangelogDir({ rustRoot });
497
+ +const CHANGELOG_FILE = getChangelogPath({ rustRoot });
498
+ const INSERT_MARKER = '<!-- changelog-insert-here -->';
499
+
500
+ /**
501
+ @@ -24,11 +45,11 @@ const INSERT_MARKER = '<!-- changelog-insert-here -->';
502
+ * @returns {string}
503
+ */
504
+ function getVersionFromCargo() {
505
+ - const cargoToml = readFileSync('rust/Cargo.toml', 'utf-8');
506
+ + const cargoToml = readFileSync(CARGO_TOML, 'utf-8');
507
+ const match = cargoToml.match(/^version\s*=\s*"([^"]+)"/m);
508
+
509
+ if (!match) {
510
+ - console.error('Error: Could not find version in rust/Cargo.toml');
511
+ + console.error(`Error: Could not find version in ${CARGO_TOML}`);
512
+ process.exit(1);
513
+ }
514
+
515
+ diff --git a/scripts/rust-get-bump-type.mjs b/scripts/rust-get-bump-type.mjs
516
+ index 31b492e..0a608a9 100644
517
+ --- a/scripts/rust-get-bump-type.mjs
518
+ +++ b/scripts/rust-get-bump-type.mjs
519
+ @@ -20,6 +20,12 @@
520
+ import { readFileSync, readdirSync, existsSync, appendFileSync } from 'fs';
521
+ import { join } from 'path';
522
+
523
+ +import {
524
+ + getRustRoot,
525
+ + getChangelogDir,
526
+ + parseRustRootConfig,
527
+ +} from './rust-paths.mjs';
528
+ +
529
+ // Simple CLI argument parsing
530
+ const args = process.argv.slice(2);
531
+ const getArg = (name, defaultValue) => {
532
+ @@ -29,7 +35,12 @@ const getArg = (name, defaultValue) => {
533
+
534
+ const defaultBump = getArg('default', process.env.DEFAULT_BUMP || 'patch');
535
+
536
+ -const CHANGELOG_DIR = 'rust/changelog.d';
537
+ +// Get Rust package root (auto-detect or use explicit config)
538
+ +const rustRootConfig = getArg('rust-root', '') || parseRustRootConfig();
539
+ +const rustRoot = getRustRoot({ rustRoot: rustRootConfig || undefined, verbose: true });
540
+ +
541
+ +// Get paths based on detected/configured rust root
542
+ +const CHANGELOG_DIR = getChangelogDir({ rustRoot });
543
+
544
+ // Bump type priority (higher = more significant)
545
+ const BUMP_PRIORITY = {
546
+ diff --git a/scripts/rust-paths.mjs b/scripts/rust-paths.mjs
547
+ new file mode 100644
548
+ index 0000000..4f4636a
549
+ --- /dev/null
550
+ +++ b/scripts/rust-paths.mjs
551
+ @@ -0,0 +1,169 @@
552
+ +#!/usr/bin/env node
553
+ +
554
+ +/**
555
+ + * Rust package path detection utility
556
+ + *
557
+ + * Automatically detects the Rust package root for both:
558
+ + * - Single-language repositories (Cargo.toml in root)
559
+ + * - Multi-language repositories (Cargo.toml in rust/ subfolder)
560
+ + *
561
+ + * Usage:
562
+ + * import { getRustRoot, getCargoTomlPath, getChangelogDir } from './rust-paths.mjs';
563
+ + *
564
+ + * const rustRoot = getRustRoot(); // Returns 'rust' or '.'
565
+ + * const cargoPath = getCargoTomlPath(); // Returns 'rust/Cargo.toml' or './Cargo.toml'
566
+ + */
567
+ +
568
+ +import { existsSync } from 'fs';
569
+ +import { join } from 'path';
570
+ +
571
+ +// Cache for detected paths (computed once per process)
572
+ +let cachedRustRoot = null;
573
+ +
574
+ +/**
575
+ + * Detect Rust package root directory
576
+ + * Checks in order:
577
+ + * 1. ./Cargo.toml (single-language repo)
578
+ + * 2. ./rust/Cargo.toml (multi-language repo)
579
+ + *
580
+ + * @param {Object} options - Configuration options
581
+ + * @param {string} [options.rustRoot] - Explicitly set Rust root (overrides auto-detection)
582
+ + * @param {boolean} [options.verbose=false] - Log detection details
583
+ + * @returns {string} The Rust root directory ('.' or 'rust')
584
+ + * @throws {Error} If no Cargo.toml is found in expected locations
585
+ + */
586
+ +export function getRustRoot(options = {}) {
587
+ + const { rustRoot: explicitRoot, verbose = false } = options;
588
+ +
589
+ + // If explicitly configured, use that
590
+ + if (explicitRoot !== undefined) {
591
+ + if (verbose) {
592
+ + console.log(`Using explicitly configured Rust root: ${explicitRoot}`);
593
+ + }
594
+ + return explicitRoot;
595
+ + }
596
+ +
597
+ + // Return cached value if already computed
598
+ + if (cachedRustRoot !== null) {
599
+ + return cachedRustRoot;
600
+ + }
601
+ +
602
+ + // Check for single-language repo (Cargo.toml in root)
603
+ + if (existsSync('./Cargo.toml')) {
604
+ + if (verbose) {
605
+ + console.log('Detected single-language repository (Cargo.toml in root)');
606
+ + }
607
+ + cachedRustRoot = '.';
608
+ + return cachedRustRoot;
609
+ + }
610
+ +
611
+ + // Check for multi-language repo (Cargo.toml in rust/ subfolder)
612
+ + if (existsSync('./rust/Cargo.toml')) {
613
+ + if (verbose) {
614
+ + console.log('Detected multi-language repository (Cargo.toml in rust/)');
615
+ + }
616
+ + cachedRustRoot = 'rust';
617
+ + return cachedRustRoot;
618
+ + }
619
+ +
620
+ + // No Cargo.toml found
621
+ + throw new Error(
622
+ + 'Could not find Cargo.toml in expected locations.\n' +
623
+ + 'Searched in:\n' +
624
+ + ' - ./Cargo.toml (single-language repository)\n' +
625
+ + ' - ./rust/Cargo.toml (multi-language repository)\n\n' +
626
+ + 'To fix this, either:\n' +
627
+ + ' 1. Run the script from the repository root\n' +
628
+ + ' 2. Explicitly configure the Rust root using --rust-root option\n' +
629
+ + ' 3. Set the RUST_ROOT environment variable'
630
+ + );
631
+ +}
632
+ +
633
+ +/**
634
+ + * Get the path to Cargo.toml
635
+ + * @param {Object} options - Configuration options (passed to getRustRoot)
636
+ + * @returns {string} Path to Cargo.toml
637
+ + */
638
+ +export function getCargoTomlPath(options = {}) {
639
+ + const rustRoot = getRustRoot(options);
640
+ + return rustRoot === '.' ? './Cargo.toml' : join(rustRoot, 'Cargo.toml');
641
+ +}
642
+ +
643
+ +/**
644
+ + * Get the path to Cargo.lock
645
+ + * @param {Object} options - Configuration options (passed to getRustRoot)
646
+ + * @returns {string} Path to Cargo.lock
647
+ + */
648
+ +export function getCargoLockPath(options = {}) {
649
+ + const rustRoot = getRustRoot(options);
650
+ + return rustRoot === '.' ? './Cargo.lock' : join(rustRoot, 'Cargo.lock');
651
+ +}
652
+ +
653
+ +/**
654
+ + * Get the path to changelog.d directory
655
+ + * @param {Object} options - Configuration options (passed to getRustRoot)
656
+ + * @returns {string} Path to changelog.d directory
657
+ + */
658
+ +export function getChangelogDir(options = {}) {
659
+ + const rustRoot = getRustRoot(options);
660
+ + return rustRoot === '.' ? './changelog.d' : join(rustRoot, 'changelog.d');
661
+ +}
662
+ +
663
+ +/**
664
+ + * Get the path to CHANGELOG.md
665
+ + * @param {Object} options - Configuration options (passed to getRustRoot)
666
+ + * @returns {string} Path to CHANGELOG.md
667
+ + */
668
+ +export function getChangelogPath(options = {}) {
669
+ + const rustRoot = getRustRoot(options);
670
+ + return rustRoot === '.' ? './CHANGELOG.md' : join(rustRoot, 'CHANGELOG.md');
671
+ +}
672
+ +
673
+ +/**
674
+ + * Get the cd command prefix for running cargo commands
675
+ + * Returns empty string for single-language repos, 'cd rust && ' for multi-language repos
676
+ + * @param {Object} options - Configuration options (passed to getRustRoot)
677
+ + * @returns {string} CD prefix for shell commands
678
+ + */
679
+ +export function getCdPrefix(options = {}) {
680
+ + const rustRoot = getRustRoot(options);
681
+ + return rustRoot === '.' ? '' : `cd ${rustRoot} && `;
682
+ +}
683
+ +
684
+ +/**
685
+ + * Check if we need to change directory before running cargo commands
686
+ + * @param {Object} options - Configuration options (passed to getRustRoot)
687
+ + * @returns {boolean} True if cd is needed
688
+ + */
689
+ +export function needsCd(options = {}) {
690
+ + const rustRoot = getRustRoot(options);
691
+ + return rustRoot !== '.';
692
+ +}
693
+ +
694
+ +/**
695
+ + * Reset the cached Rust root (useful for testing)
696
+ + */
697
+ +export function resetCache() {
698
+ + cachedRustRoot = null;
699
+ +}
700
+ +
701
+ +/**
702
+ + * Parse Rust root from CLI arguments or environment
703
+ + * Supports --rust-root argument and RUST_ROOT environment variable
704
+ + * @returns {string|undefined} Configured Rust root or undefined for auto-detection
705
+ + */
706
+ +export function parseRustRootConfig() {
707
+ + // Check CLI arguments
708
+ + const args = process.argv.slice(2);
709
+ + const rustRootIndex = args.indexOf('--rust-root');
710
+ + if (rustRootIndex >= 0 && args[rustRootIndex + 1]) {
711
+ + return args[rustRootIndex + 1];
712
+ + }
713
+ +
714
+ + // Check environment variable
715
+ + if (process.env.RUST_ROOT) {
716
+ + return process.env.RUST_ROOT;
717
+ + }
718
+ +
719
+ + return undefined;
720
+ +}
721
+ diff --git a/scripts/rust-version-and-commit.mjs b/scripts/rust-version-and-commit.mjs
722
+ index f90bbd7..7b9117c 100644
723
+ --- a/scripts/rust-version-and-commit.mjs
724
+ +++ b/scripts/rust-version-and-commit.mjs
725
+ @@ -18,6 +18,14 @@ import {
726
+ import { join } from 'path';
727
+ import { execSync } from 'child_process';
728
+
729
+ +import {
730
+ + getRustRoot,
731
+ + getCargoTomlPath,
732
+ + getChangelogDir,
733
+ + getChangelogPath,
734
+ + parseRustRootConfig,
735
+ +} from './rust-paths.mjs';
736
+ +
737
+ // Simple CLI argument parsing
738
+ const args = process.argv.slice(2);
739
+ const getArg = (name, defaultValue) => {
740
+ @@ -28,16 +36,21 @@ const getArg = (name, defaultValue) => {
741
+ const bumpType = getArg('bump-type', process.env.BUMP_TYPE || '');
742
+ const description = getArg('description', process.env.DESCRIPTION || '');
743
+
744
+ +// Get Rust package root (auto-detect or use explicit config)
745
+ +const rustRootConfig = getArg('rust-root', '') || parseRustRootConfig();
746
+ +const rustRoot = getRustRoot({ rustRoot: rustRootConfig || undefined, verbose: true });
747
+ +
748
+ if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) {
749
+ console.error(
750
+ - 'Usage: node scripts/rust-version-and-commit.mjs --bump-type <major|minor|patch> [--description <desc>]'
751
+ + 'Usage: node scripts/rust-version-and-commit.mjs --bump-type <major|minor|patch> [--description <desc>] [--rust-root <path>]'
752
+ );
753
+ process.exit(1);
754
+ }
755
+
756
+ -const CARGO_TOML = 'rust/Cargo.toml';
757
+ -const CHANGELOG_DIR = 'rust/changelog.d';
758
+ -const CHANGELOG_FILE = 'rust/CHANGELOG.md';
759
+ +// Get paths based on detected/configured rust root
760
+ +const CARGO_TOML = getCargoTomlPath({ rustRoot });
761
+ +const CHANGELOG_DIR = getChangelogDir({ rustRoot });
762
+ +const CHANGELOG_FILE = getChangelogPath({ rustRoot });
763
+
764
+ /**
765
+ * Append to GitHub Actions output file
766
+ diff --git a/scripts/version-and-commit.mjs b/scripts/version-and-commit.mjs
767
+ index 7235407..28d21dc 100644
768
+ --- a/scripts/version-and-commit.mjs
769
+ +++ b/scripts/version-and-commit.mjs
770
+ @@ -14,6 +14,14 @@
771
+
772
+ import { readFileSync, appendFileSync, readdirSync } from 'fs';
773
+
774
+ +import {
775
+ + getJsRoot,
776
+ + getPackageJsonPath,
777
+ + getChangesetDir,
778
+ + needsCd,
779
+ + parseJsRootConfig,
780
+ +} from './js-paths.mjs';
781
+ +
782
+ // Load use-m dynamically
783
+ const { use } = eval(
784
+ await (await fetch('https://unpkg.com/use-m/use.js')).text()
785
+ @@ -42,16 +50,26 @@ const config = makeConfig({
786
+ type: 'string',
787
+ default: getenv('DESCRIPTION', ''),
788
+ describe: 'Description for instant version bump',
789
+ + })
790
+ + .option('js-root', {
791
+ + type: 'string',
792
+ + default: getenv('JS_ROOT', ''),
793
+ + describe: 'JavaScript package root directory (auto-detected if not specified)',
794
+ }),
795
+ });
796
+
797
+ -const { mode, bumpType, description } = config;
798
+ +const { mode, bumpType, description, jsRoot: jsRootArg } = config;
799
+ +
800
+ +// Get JavaScript package root (auto-detect or use explicit config)
801
+ +const jsRootConfig = jsRootArg || parseJsRootConfig();
802
+ +const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true });
803
+
804
+ // Debug: Log parsed configuration
805
+ console.log('Parsed configuration:', {
806
+ mode,
807
+ bumpType,
808
+ description: description || '(none)',
809
+ + jsRoot,
810
+ });
811
+
812
+ // Detect if positional arguments were used (common mistake)
813
+ @@ -112,7 +130,7 @@ function setOutput(key, value) {
814
+ */
815
+ function countChangesets() {
816
+ try {
817
+ - const changesetDir = 'js/.changeset';
818
+ + const changesetDir = getChangesetDir({ jsRoot });
819
+ const files = readdirSync(changesetDir);
820
+ return files.filter((f) => f.endsWith('.md') && f !== 'README.md').length;
821
+ } catch {
822
+ @@ -125,16 +143,24 @@ function countChangesets() {
823
+ * @param {string} source - 'local' or 'remote'
824
+ */
825
+ async function getVersion(source = 'local') {
826
+ + const packageJsonPath = getPackageJsonPath({ jsRoot });
827
+ if (source === 'remote') {
828
+ - const result = await $`git show origin/main:js/package.json`.run({
829
+ + // For remote, we need the path relative to repo root (without ./ prefix)
830
+ + const remotePath = packageJsonPath.replace(/^\.\//, '');
831
+ + const result = await $`git show origin/main:${remotePath}`.run({
832
+ capture: true,
833
+ });
834
+ return JSON.parse(result.stdout).version;
835
+ }
836
+ - return JSON.parse(readFileSync('./js/package.json', 'utf8')).version;
837
+ + return JSON.parse(readFileSync(packageJsonPath, 'utf8')).version;
838
+ }
839
+
840
+ async function main() {
841
+ + // Store the original working directory to restore after cd commands
842
+ + // IMPORTANT: command-stream's cd is a virtual command that calls process.chdir()
843
+ + // This means `cd js` actually changes the Node.js process's working directory
844
+ + const originalCwd = process.cwd();
845
+ +
846
+ try {
847
+ // Configure git
848
+ await $`git config user.name "github-actions[bot]"`;
849
+ @@ -186,17 +212,26 @@ async function main() {
850
+
851
+ if (mode === 'instant') {
852
+ console.log('Running instant version bump...');
853
+ - // Run instant version bump script
854
+ + // Run instant version bump script, passing js-root for consistent path handling
855
+ // Rely on command-stream's auto-quoting for proper argument handling
856
+ if (description) {
857
+ - await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --description ${description}`;
858
+ + await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --description ${description} --js-root ${jsRoot}`;
859
+ } else {
860
+ - await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType}`;
861
+ + await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --js-root ${jsRoot}`;
862
+ }
863
+ } else {
864
+ console.log('Running changeset version...');
865
+ // Run changeset version to bump versions and update CHANGELOG
866
+ - await $`cd js && npm run changeset:version`;
867
+ + // IMPORTANT: cd is a virtual command in command-stream that calls process.chdir()
868
+ + // We need to restore the original directory after this command
869
+ + if (needsCd({ jsRoot })) {
870
+ + await $`cd ${jsRoot} && npm run changeset:version`;
871
+ + // Restore the original working directory
872
+ + process.chdir(originalCwd);
873
+ + } else {
874
+ + // Single-language repo - run in current directory
875
+ + await $`npm run changeset:version`;
876
+ + }
877
+ }
878
+
879
+ // Get new version after bump