Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .changeset/adopt-agents-audit-version-line.md

This file was deleted.

33 changes: 26 additions & 7 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
# Changelog — `@workspacejson/cli`

## [0.5.0] - Unreleased
## 0.5.1

### Patch Changes

- **Fixed:** `workspacejson --help` and `workspacejson --version` exited `1`
instead of `0`.

The CLI uses commander's `.exitOverride()`, which throws rather than exiting —
including on the success paths, where `--help` and `--version` raise a
`CommanderError` carrying `exitCode: 0`. The handler read that with
`Number(error.exitCode) || 1`, and `0 || 1` is `1`, so both commands reported
failure to every shell, script and CI job that checked their status. The output
was always correct; only the exit code was wrong.

Inherited from `agents-audit`, where the same expression is present and, since
that package is frozen at `0.4.4`, will remain. Regression tests assert the exit
code for `--help`, `--version` and the `version` subcommand, plus a guard that an
unknown option still exits non-zero — verified red against the pre-fix source.

## [0.5.0] - 2026-07-27

First release under the `workspacejson` name, and the continuation of
`agents-audit`'s version line. That package is frozen at `0.4.4` — locked for
hackathon judging — and all forward development happens here, so the numbering
carries over rather than restarting.

**Why `0.5.0` and not `0.4.5`.** Measured against `agents-audit@0.4.4`, this
binary *removes* surface: there is no `scan` command, no config file (see
binary _removes_ surface: there is no `scan` command, no config file (see
Notes), and the binary itself is renamed from `agents-audit` to `workspacejson`.
A patch bump would promise a drop-in successor, and the first thing a migrating
user would hit is a missing command. Under 0.x, breaking changes go in the minor
slot.

Migrating from `agents-audit@0.4.4`:

| Before | After |
| -- | -- |
| `npx agents-audit generate` | `npx workspacejson generate` |
| `npx agents-audit scan` | no equivalent — the producer does not audit |
| `.agentsauditrc` | not read (see Notes) |
| Before | After |
| --------------------------- | ------------------------------------------- |
| `npx agents-audit generate` | `npx workspacejson generate` |
| `npx agents-audit scan` | no equivalent — the producer does not audit |
| `.agentsauditrc` | not read (see Notes) |

### Changed

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workspacejson/cli",
"version": "0.5.0",
"version": "0.5.1",
"description": "The workspace.json producer — scans a repository and generates .agents/workspace.json deterministically, preserving human-authored manual evidence.",
"license": "Apache-2.0",
"author": "workspace.json contributors",
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/cli.exit-code.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from 'vitest';
import { runCli } from './cli.js';

// Regression cover for the 0.5.0 defect: `.exitOverride()` makes commander throw
// on its *success* paths too — `--help` and `--version` raise a CommanderError
// carrying `exitCode: 0`. The handler read that with `Number(...) || 1`, so the
// legitimate zero became 1 and `workspacejson --help` reported failure to every
// shell and CI job that checked it. These assert the exit code, not the output,
// because the output was always correct — only the status was wrong.
const ARGV = (...args: string[]) => ['node', 'workspacejson', ...args];

describe('runCli exit codes', () => {
it('exits 0 for --help', async () => {
const stdout = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
try {
await expect(runCli(ARGV('--help'))).resolves.toBe(0);
} finally {
stdout.mockRestore();
}
});

it('exits 0 for --version', async () => {
const stdout = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
try {
await expect(runCli(ARGV('--version'))).resolves.toBe(0);
} finally {
stdout.mockRestore();
}
});

it('exits 0 for the version subcommand', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
try {
await expect(runCli(ARGV('version'))).resolves.toBe(0);
} finally {
log.mockRestore();
}
});

// The fix must not turn every failure into a success: an unparseable option
// still has to report non-zero.
it('exits non-zero for an unknown option', async () => {
const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
try {
await expect(runCli(ARGV('--no-such-flag'))).resolves.not.toBe(0);
} finally {
stderr.mockRestore();
}
});
});
12 changes: 11 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ export async function runCli(argv: string[] = process.argv): Promise<number> {
try {
await program.parseAsync(argv);
} catch (error) {
exitCode = typeof error === 'object' && error && 'exitCode' in error ? Number((error as { exitCode?: number }).exitCode) || 1 : 1;
// `.exitOverride()` makes commander throw instead of exiting, including on
// the success paths: `--help` and `--version` raise a CommanderError whose
// exitCode is 0. The previous `Number(...) || 1` collapsed that legitimate
// zero into 1, so `workspacejson --help` reported failure to every shell and
// CI job that checked it. Distinguish "no usable exitCode" from "exitCode is
// zero" instead of leaning on falsiness.
const reported =
typeof error === 'object' && error !== null && 'exitCode' in error
? Number((error as { exitCode?: number }).exitCode)
: Number.NaN;
exitCode = Number.isInteger(reported) ? reported : 1;
}

return exitCode;
Expand Down
31 changes: 26 additions & 5 deletions scripts/verify-published.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ import { spawnSync } from "node:child_process";
// `cli-v*` release must not be reported green because a previously published
// sibling still installs. With no argument it verifies every publishable
// package, which is only meaningful when their versions genuinely agree.
// The health check runs `version`, not `--help`. Both CLIs use commander's
// `.exitOverride()`, which throws a CommanderError with exitCode 0 on the
// `--help` success path; until the fix landing alongside this, that zero was
// coerced to 1, so `--help` reported failure and this verifier could never pass
// for either package. `version` is a normal action that returns 0, and it proves
// more anyway — it executes the installed entry point and prints the version the
// artifact actually carries, rather than text commander produces before any of
// the package's own code runs.
//
// `agents-audit` is frozen at 0.4.4, so its `--help` keeps the old exit code
// permanently. That alone makes `version` the only check that can work for both.
const RELEASABLE = {
"@workspacejson/cli": {
manifest: "../packages/cli/package.json",
check: ["npx", "--no-install", "workspacejson", "--help"],
check: ["npx", "--no-install", "workspacejson", "version"],
},
"agents-audit": {
manifest: "../packages/agents-audit-compat/package.json",
check: ["npx", "--no-install", "agents-audit", "--help"],
check: ["npx", "--no-install", "agents-audit", "version"],
},
};

Expand Down Expand Up @@ -57,8 +68,18 @@ if (requested.length !== 1 && process.env.WORKSPACEJSON_RELEASE_VERSION) {
// immediate post-publish check has no way to tell "not actually published"
// apart from "not visible here yet" and fails the Release workflow either
// way — training everyone to ignore red, which is worse than no gate at all.
const REGISTRY_PROPAGATION_RETRIES = 6;
const REGISTRY_PROPAGATION_BASE_DELAY_MS = 5000;
//
// Measured on the 0.5.0 release (2026-07-27): the version endpoint answered
// almost immediately, but the aggregated packument — which is what `npm install`
// resolves against — took ~4m30s to appear. The previous window summed to 75s,
// so it reported a red failure for a release that had in fact published
// correctly, provenance and all. A first-ever publish is the slow case: there is
// no existing packument to update, so budget for minutes, not seconds.
//
// Linear backoff with a ceiling: 10+20+30+40+50+60+60+60+60 ≈ 6m30s total.
const REGISTRY_PROPAGATION_RETRIES = 10;
const REGISTRY_PROPAGATION_BASE_DELAY_MS = 10_000;
const REGISTRY_PROPAGATION_MAX_DELAY_MS = 60_000;
const isRegistryPropagationLag = (stderr) => /\bE(TARGET|404)\b|No matching version found/.test(stderr ?? "");

for (const pkg of packages) {
Expand Down Expand Up @@ -90,7 +111,7 @@ async function installWithRetry(pkg, directory) {
process.stderr.write(result.stderr);
process.exit(result.status ?? 1);
}
const delayMs = REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt;
const delayMs = Math.min(REGISTRY_PROPAGATION_BASE_DELAY_MS * attempt, REGISTRY_PROPAGATION_MAX_DELAY_MS);
console.log(`${pkg.name}@${pkg.version} not yet visible on the registry (attempt ${attempt}/${REGISTRY_PROPAGATION_RETRIES}) — retrying in ${delayMs}ms`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
Expand Down
Loading