mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(release): npmjs.com shows the English README, and the package metadata names what Codeman is
npm takes the package's readme from an unsorted {README,README.*} glob at
publish time, and with README.zh-CN.md beside README.md it kept picking the
Chinese one, so npmjs.com rendered it. `npm run release` now runs
scripts/npm-release.mjs, which moves README.zh-CN.md to a dot-name for the
length of `changeset publish` and always puts it back (also on a failed or
thrown publish, and after an interrupted earlier run). It lives in the publish
command, not in release.yml, because changesets/action commits the working
tree into its version PR. Verified with npm's own normalize: README.zh-CN.md
before, README.md with the file aside, and the aside copy is never packed.
package.json: the description names the CLIs and says self-hosted (the old
"run 20 autonomous agents" was an internal performance target), and homepage
points at getcodeman.com instead of repeating the repository link.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "aicodeman",
|
||||
"version": "1.40.0",
|
||||
"description": "Mission control for AI coding agents - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"description": "Self-hosted mission control for AI coding agents: run Claude Code, Codex, OpenCode, Gemini, DeepSeek, Grok and more 24/7 in tmux, from any device.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -42,7 +42,7 @@
|
||||
"check:lockfile": "node scripts/check-lockfile-sync.mjs",
|
||||
"check:plugin": "node scripts/sync-plugin.mjs --check && claude plugin validate --strict plugins/codeman && claude plugin validate --strict .claude-plugin/marketplace.json",
|
||||
"knip": "npx --yes knip@latest --config config/knip.json",
|
||||
"release": "changeset publish"
|
||||
"release": "node scripts/npm-release.mjs"
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
@@ -171,7 +171,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/Ark0N/Codeman/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Ark0N/Codeman#readme",
|
||||
"homepage": "https://getcodeman.com",
|
||||
"files": [
|
||||
"dist",
|
||||
"scripts/postinstall.js",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @fileoverview `npm run release`: `changeset publish` with `README.zh-CN.md` moved aside.
|
||||
*
|
||||
* npmjs.com renders the package's top-level `readme`, and npm picks it at publish time:
|
||||
* @npmcli/package-json's normalize globs `{README,README.*}` in the package root UNSORTED and
|
||||
* keeps the first `.md` it sees. With `README.zh-CN.md` next to `README.md` that was the
|
||||
* Chinese one on this machine and on CI, so npmjs.com showed the Chinese README for months.
|
||||
* `files` cannot help: npm-packlist always includes every root `README.*`.
|
||||
*
|
||||
* Renaming the file would break every link to it, so for the length of the publish only it
|
||||
* moves to a name npm does not treat as a readme (a leading dot), and is put back afterwards,
|
||||
* whatever the publish did. The move happens HERE, inside the publish command, never as a
|
||||
* step before `changesets/action` in release.yml: that action also runs the version path and
|
||||
* commits the working tree into its version PR, which would commit the deletion.
|
||||
*
|
||||
* A previous run killed between the move and the restore leaves the aside copy behind; the
|
||||
* next run puts it back first. `xterm-zerolag-input` (packages/) has only a README.md.
|
||||
*
|
||||
* node scripts/npm-release.mjs what the Release workflow runs (via `npm run release`)
|
||||
*/
|
||||
import { existsSync, renameSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const HIDDEN_README = 'README.zh-CN.md';
|
||||
export const ASIDE_NAME = '.README.zh-CN.md.release-aside';
|
||||
|
||||
/**
|
||||
* Runs `publish` with `HIDDEN_README` moved aside in `root`, restoring it afterwards, also
|
||||
* when `publish` fails or throws. Returns the exit code `publish` returned.
|
||||
*
|
||||
* @param {{ root: string, publish: () => number, log?: (msg: string) => void }} opts
|
||||
* @returns {number}
|
||||
*/
|
||||
export function publishWithReadmeAside({ root, publish, log = (msg) => console.log(msg) }) {
|
||||
const original = join(root, HIDDEN_README);
|
||||
const aside = join(root, ASIDE_NAME);
|
||||
if (existsSync(aside) && !existsSync(original)) {
|
||||
renameSync(aside, original);
|
||||
log(`npm-release: restored ${HIDDEN_README} left aside by an earlier run`);
|
||||
}
|
||||
const moved = existsSync(original);
|
||||
if (moved) {
|
||||
renameSync(original, aside);
|
||||
log(`npm-release: ${HIDDEN_README} moved aside so npm picks README.md as the readme`);
|
||||
}
|
||||
try {
|
||||
return publish();
|
||||
} finally {
|
||||
if (moved) {
|
||||
renameSync(aside, original);
|
||||
log(`npm-release: ${HIDDEN_README} restored`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runChangesetPublish() {
|
||||
const result = spawnSync('changeset', ['publish'], { stdio: 'inherit', shell: process.platform === 'win32' });
|
||||
if (result.error) {
|
||||
console.error(`npm-release: could not run changeset publish: ${result.error.message}`);
|
||||
return 1;
|
||||
}
|
||||
return result.status ?? 1;
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
||||
process.exitCode = publishWithReadmeAside({ root, publish: runChangesetPublish });
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @fileoverview `npm run release` moves README.zh-CN.md aside for the publish, so npmjs.com
|
||||
* renders README.md, and always puts it back (scripts/npm-release.mjs).
|
||||
*
|
||||
* Port: N/A (no server).
|
||||
*/
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { ASIDE_NAME, HIDDEN_README, publishWithReadmeAside } from '../scripts/npm-release.mjs';
|
||||
|
||||
const roots: string[] = [];
|
||||
function fixture(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'npm-release-'));
|
||||
roots.push(root);
|
||||
writeFileSync(join(root, 'README.md'), 'english\n');
|
||||
writeFileSync(join(root, HIDDEN_README), 'chinese\n');
|
||||
return root;
|
||||
}
|
||||
const quiet = () => {};
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('publishWithReadmeAside', () => {
|
||||
it('hides the Chinese README during the publish and restores it after a success', () => {
|
||||
const root = fixture();
|
||||
let seen: { zh: boolean; en: boolean } | null = null;
|
||||
const code = publishWithReadmeAside({
|
||||
root,
|
||||
log: quiet,
|
||||
publish: () => {
|
||||
seen = { zh: existsSync(join(root, HIDDEN_README)), en: existsSync(join(root, 'README.md')) };
|
||||
return 0;
|
||||
},
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
expect(seen).toEqual({ zh: false, en: true });
|
||||
expect(readFileSync(join(root, HIDDEN_README), 'utf8')).toBe('chinese\n');
|
||||
expect(existsSync(join(root, ASIDE_NAME))).toBe(false);
|
||||
});
|
||||
|
||||
it('the aside name is not one npm treats as a readme', () => {
|
||||
// npm-packlist keeps every root /readme{,.*}/i and normalize globs {README,README.*}.
|
||||
expect(/^readme/i.test(ASIDE_NAME)).toBe(false);
|
||||
});
|
||||
|
||||
it('restores it and passes the exit code through when the publish fails', () => {
|
||||
const root = fixture();
|
||||
const code = publishWithReadmeAside({ root, log: quiet, publish: () => 1 });
|
||||
expect(code).toBe(1);
|
||||
expect(readFileSync(join(root, HIDDEN_README), 'utf8')).toBe('chinese\n');
|
||||
expect(existsSync(join(root, ASIDE_NAME))).toBe(false);
|
||||
});
|
||||
|
||||
it('restores it when the publish throws', () => {
|
||||
const root = fixture();
|
||||
expect(() =>
|
||||
publishWithReadmeAside({
|
||||
root,
|
||||
log: quiet,
|
||||
publish: () => {
|
||||
throw new Error('boom');
|
||||
},
|
||||
})
|
||||
).toThrow('boom');
|
||||
expect(readFileSync(join(root, HIDDEN_README), 'utf8')).toBe('chinese\n');
|
||||
expect(existsSync(join(root, ASIDE_NAME))).toBe(false);
|
||||
});
|
||||
|
||||
it('first puts back a copy an interrupted earlier run left aside', () => {
|
||||
const root = fixture();
|
||||
rmSync(join(root, HIDDEN_README));
|
||||
writeFileSync(join(root, ASIDE_NAME), 'chinese\n');
|
||||
let zhDuring = true;
|
||||
publishWithReadmeAside({
|
||||
root,
|
||||
log: quiet,
|
||||
publish: () => {
|
||||
zhDuring = existsSync(join(root, HIDDEN_README));
|
||||
return 0;
|
||||
},
|
||||
});
|
||||
expect(zhDuring).toBe(false);
|
||||
expect(readFileSync(join(root, HIDDEN_README), 'utf8')).toBe('chinese\n');
|
||||
expect(existsSync(join(root, ASIDE_NAME))).toBe(false);
|
||||
});
|
||||
|
||||
it('publishes normally when there is no Chinese README', () => {
|
||||
const root = fixture();
|
||||
rmSync(join(root, HIDDEN_README));
|
||||
const code = publishWithReadmeAside({ root, log: quiet, publish: () => 0 });
|
||||
expect(code).toBe(0);
|
||||
expect(existsSync(join(root, HIDDEN_README))).toBe(false);
|
||||
});
|
||||
|
||||
it('package.json routes npm run release through the wrapper', () => {
|
||||
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf8'));
|
||||
expect(pkg.scripts.release).toBe('node scripts/npm-release.mjs');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user