diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e1cad8ef..a72750d0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,6 +13,7 @@ on: - workload-router - harness-kit - cli + - agentworkforce version: description: 'Version bump type (ignored if custom_version is set)' required: true @@ -98,10 +99,11 @@ jobs: run: | case "${{ github.event.inputs.package }}" in all) - # Must be in dependency order: router → harness-kit → cli. - echo "packages=workload-router harness-kit cli" >> "$GITHUB_OUTPUT" + # Must be in dependency order: router → harness-kit → cli → + # agentworkforce (the wrapper depends on cli). + echo "packages=workload-router harness-kit cli agentworkforce" >> "$GITHUB_OUTPUT" ;; - workload-router|harness-kit|cli) + workload-router|harness-kit|cli|agentworkforce) echo "packages=${{ github.event.inputs.package }}" >> "$GITHUB_OUTPUT" ;; *) @@ -125,8 +127,9 @@ jobs: run: | set -euo pipefail for pkg in ${{ steps.targets.outputs.packages }}; do + NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name") LOCAL=$(node -p "require('./packages/$pkg/package.json').version") - REMOTE=$(npm view "@agentworkforce/$pkg" versions --json 2>/dev/null \ + REMOTE=$(npm view "$NPM_NAME" versions --json 2>/dev/null \ | node -e ' const raw = require("fs").readFileSync(0, "utf8").trim() || "[]"; const parsed = JSON.parse(raw); @@ -143,7 +146,7 @@ jobs: process.stdout.write(stable.length ? stable[stable.length - 1] : ""); ' || echo "") if [ -z "$REMOTE" ]; then - echo "@agentworkforce/$pkg: no stable releases on npm yet — OK" + echo "$NPM_NAME: no stable releases on npm yet — OK" continue fi CMP=$(node -e ' @@ -158,10 +161,10 @@ jobs: console.log(0); ' "$LOCAL" "$REMOTE") if [ "$CMP" = "-1" ]; then - echo "::error title=npm/git version drift::@agentworkforce/$pkg: package.json is at $LOCAL but npm has $REMOTE published. A previous publish run likely succeeded on npm but failed before tagging. Bump packages/$pkg/package.json to >= $REMOTE on a branch, push, then re-run this workflow. (You may also want to tag the prior release commit as $pkg-v$REMOTE so the changelog generator picks the right baseline.)" + echo "::error title=npm/git version drift::$NPM_NAME: package.json is at $LOCAL but npm has $REMOTE published. A previous publish run likely succeeded on npm but failed before tagging. Bump packages/$pkg/package.json to >= $REMOTE on a branch, push, then re-run this workflow. (You may also want to tag the prior release commit as $pkg-v$REMOTE so the changelog generator picks the right baseline.)" exit 1 fi - echo "@agentworkforce/$pkg: local=$LOCAL, npm=$REMOTE — OK" + echo "$NPM_NAME: local=$LOCAL, npm=$REMOTE — OK" done - name: Bump versions @@ -199,12 +202,13 @@ jobs: for entry in ${{ steps.bump.outputs.versions }}; do pkg="${entry%%:*}" ver="${entry##*:}" - EXISTS=$(npm view "@agentworkforce/$pkg@$ver" version 2>/dev/null || true) + NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name") + EXISTS=$(npm view "$NPM_NAME@$ver" version 2>/dev/null || true) if [ -n "$EXISTS" ]; then - echo "::error title=Version already published::@agentworkforce/$pkg@$ver is already on npm. Pick a different bump type or set custom_version to a higher version." + echo "::error title=Version already published::$NPM_NAME@$ver is already on npm. Pick a different bump type or set custom_version to a higher version." exit 1 fi - echo "@agentworkforce/$pkg@$ver: unpublished — OK" + echo "$NPM_NAME@$ver: unpublished — OK" done # Per-package CHANGELOG.md generation. For each package being published, @@ -222,6 +226,7 @@ jobs: const [,, pkg, newVersion, today] = process.argv; const path = `packages/${pkg}/CHANGELOG.md`; + const npmName = JSON.parse(readFileSync(`packages/${pkg}/package.json`, 'utf-8')).name; if (newVersion.includes('-')) { console.log(`prerelease ${pkg}@${newVersion}: skipping`); @@ -325,7 +330,7 @@ jobs: const newEntry = lines.join('\n'); if (!existing) { - const header = `# Changelog\n\nAll notable changes to \`@agentworkforce/${pkg}\` will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n`; + const header = `# Changelog\n\nAll notable changes to \`${npmName}\` will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n`; writeFileSync(path, header + newEntry + '\n'); } else { // Insert after the [Unreleased] block, before the first versioned entry. @@ -359,7 +364,8 @@ jobs: for entry in ${{ steps.bump.outputs.versions }}; do pkg="${entry%%:*}" version="${entry##*:}" - MSG+=" @agentworkforce/$pkg@$version" + NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name") + MSG+=" $NPM_NAME@$version" done git commit -m "$MSG" fi @@ -393,11 +399,17 @@ jobs: fi for pkg in ${{ steps.targets.outputs.packages }}; do - echo "==> Packing @agentworkforce/$pkg" - pnpm --filter "@agentworkforce/$pkg" pack --pack-destination "$PACK_DIR" - TARBALL=$(ls -1t "$PACK_DIR"/agentworkforce-$pkg-*.tgz | head -n1) - if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then - echo "::error::could not find packed tarball for $pkg in $PACK_DIR" >&2 + NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name") + VERSION=$(node -p "require('./packages/$pkg/package.json').version") + # `pnpm pack` writes -.tgz with `/` → `-`. + # @agentworkforce/cli@0.4.0 → agentworkforce-cli-0.4.0.tgz + # agentworkforce@0.4.0 → agentworkforce-0.4.0.tgz + TARBALL_BASENAME="$(echo "${NPM_NAME#@}" | tr '/' '-')-${VERSION}.tgz" + echo "==> Packing $NPM_NAME" + pnpm --filter "$NPM_NAME" pack --pack-destination "$PACK_DIR" + TARBALL="$PACK_DIR/$TARBALL_BASENAME" + if [ ! -f "$TARBALL" ]; then + echo "::error::could not find packed tarball $TARBALL_BASENAME in $PACK_DIR" >&2 ls -la "$PACK_DIR" >&2 || true exit 1 fi @@ -413,7 +425,8 @@ jobs: for entry in ${{ steps.bump.outputs.versions }}; do pkg="${entry%%:*}" version="${entry##*:}" - git tag -a "$pkg-v$version" -m "@agentworkforce/$pkg@$version" + NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name") + git tag -a "$pkg-v$version" -m "$NPM_NAME@$version" done git push origin HEAD --follow-tags @@ -425,7 +438,8 @@ jobs: for entry in ${{ steps.bump.outputs.versions }}; do pkg="${entry%%:*}" version="${entry##*:}" - echo "- \`@agentworkforce/$pkg@$version\`" + NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name") + echo "- \`$NPM_NAME@$version\`" done echo "" echo "- **dist-tag**: \`${{ github.event.inputs.tag }}\`" @@ -436,94 +450,158 @@ jobs: fi } >> "$GITHUB_STEP_SUMMARY" - # One GitHub Release per package tag. Matches the relay pattern - # (softprops/action-gh-release@v2 + generate_release_notes). Runs in parallel - # across packages; each cell is gated on whether its package was published. + # One GitHub Release per publish run. Per-package git tags are still pushed + # above (so the next publish's changelog generator can find them), but the + # public GitHub Release is anchored to a single canonical tag — `agentworkforce` + # if the wrapper was bumped, otherwise the first package in the run. The + # release body lists every published package and inlines each one's + # CHANGELOG block, so the releases page has one item per version instead of + # 4 near-duplicate entries per `all` publish. create-release: - name: Release @agentworkforce/${{ matrix.package }} + name: Create GitHub Release needs: publish if: ${{ github.event.inputs.dry_run != 'true' && (github.event.inputs.version != 'none' || github.event.inputs.custom_version != '') }} runs-on: ubuntu-latest permissions: contents: write - strategy: - fail-fast: false - matrix: - package: [workload-router, harness-kit, cli] steps: - - name: Check if this package was published - id: check + - name: Resolve canonical release + id: release run: | + set -euo pipefail + VERSIONS='${{ needs.publish.outputs.versions }}' + canonical="" for entry in $VERSIONS; do pkg="${entry%%:*}" - ver="${entry##*:}" - if [ "$pkg" = "${{ matrix.package }}" ]; then - echo "published=true" >> "$GITHUB_OUTPUT" - echo "version=$ver" >> "$GITHUB_OUTPUT" - if [[ "$ver" == *-* ]]; then - echo "prerelease=true" >> "$GITHUB_OUTPUT" - else - echo "prerelease=false" >> "$GITHUB_OUTPUT" - fi - exit 0 + if [ -z "$canonical" ]; then + canonical="$entry" + fi + if [ "$pkg" = "agentworkforce" ]; then + canonical="$entry" + break fi done - echo "published=false" >> "$GITHUB_OUTPUT" + + if [ -z "$canonical" ]; then + echo "::error title=Missing release target::publish job did not report any package versions" + exit 1 + fi + + pkg="${canonical%%:*}" + ver="${canonical##*:}" + echo "canonical_pkg=$pkg" >> "$GITHUB_OUTPUT" + echo "version=$ver" >> "$GITHUB_OUTPUT" + echo "tag_name=$pkg-v$ver" >> "$GITHUB_OUTPUT" + if [[ "$ver" == *-* ]]; then + echo "prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "prerelease=false" >> "$GITHUB_OUTPUT" + fi - name: Checkout - if: steps.check.outputs.published == 'true' uses: actions/checkout@v4 with: - # Need the tag that the publish job just pushed. - ref: ${{ matrix.package }}-v${{ steps.check.outputs.version }} - - # Prefer the per-package CHANGELOG section that was generated and - # committed during publish. Falls back to auto-generated notes for - # prereleases and first publishes (where no CHANGELOG entry exists). - - name: Extract changelog notes - if: steps.check.outputs.published == 'true' + # Need the canonical tag that the publish job just pushed — it + # points at the chore(release) commit with all bumped CHANGELOGs. + ref: ${{ steps.release.outputs.tag_name }} + + - name: Build combined release notes id: notes run: | - pkg="${{ matrix.package }}" - ver="${{ steps.check.outputs.version }}" - file="packages/$pkg/CHANGELOG.md" - out=/tmp/release-notes.md - : > "$out" - if [ -f "$file" ]; then - awk -v prefix="## [$ver]" ' - index($0, prefix) == 1 { flag=1; print; next } - /^## \[/ && flag { exit } - flag { print } - ' "$file" > "$out" - fi - if [ -s "$out" ]; then - echo "has_notes=true" >> "$GITHUB_OUTPUT" - else - echo "has_notes=false" >> "$GITHUB_OUTPUT" - fi + cat > /tmp/build-release-notes.mjs << 'GENEOF' + import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; - - name: Create GitHub Release (stable, with changelog) - if: steps.check.outputs.published == 'true' && steps.check.outputs.prerelease != 'true' && steps.notes.outputs.has_notes == 'true' - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ matrix.package }}-v${{ steps.check.outputs.version }} - name: "@agentworkforce/${{ matrix.package }}@${{ steps.check.outputs.version }}" - body_path: /tmp/release-notes.md + const versionsRaw = process.env.VERSIONS || ''; + const canonicalPkg = process.env.CANONICAL_PKG; + const canonicalVersion = process.env.CANONICAL_VERSION; - - name: Create GitHub Release (stable, auto notes) - if: steps.check.outputs.published == 'true' && steps.check.outputs.prerelease != 'true' && steps.notes.outputs.has_notes != 'true' - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ matrix.package }}-v${{ steps.check.outputs.version }} - name: "@agentworkforce/${{ matrix.package }}@${{ steps.check.outputs.version }}" - generate_release_notes: true + const packageOrder = ['workload-router', 'harness-kit', 'cli', 'agentworkforce']; + const entries = versionsRaw.trim().split(/\s+/).filter(Boolean).map((entry) => { + const idx = entry.indexOf(':'); + return { pkg: entry.slice(0, idx), ver: entry.slice(idx + 1) }; + }).sort((a, b) => packageOrder.indexOf(a.pkg) - packageOrder.indexOf(b.pkg)); + + if (entries.length === 0) { + throw new Error('publish job did not report any package versions'); + } + + const packageInfo = entries.map(({ pkg, ver }) => { + const pkgJson = JSON.parse(readFileSync(`packages/${pkg}/package.json`, 'utf8')); + return { + pkg, + ver, + npmName: pkgJson.name, + tag: `${pkg}-v${ver}`, + }; + }); + + const canonical = + packageInfo.find((entry) => entry.pkg === canonicalPkg && entry.ver === canonicalVersion) || + packageInfo[0]; + + function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } - - name: Create GitHub Release (prerelease) - if: steps.check.outputs.published == 'true' && steps.check.outputs.prerelease == 'true' + function extractChangelogBody(path, version) { + if (!existsSync(path)) return ''; + const raw = readFileSync(path, 'utf8'); + const headerRe = new RegExp(`^## \\[${escapeRegExp(version)}\\][^\\n]*\\n`, 'm'); + const header = raw.match(headerRe); + if (!header || header.index === undefined) return ''; + const start = header.index + header[0].length; + const tail = raw.slice(start); + const next = tail.match(/^## \[/m); + const end = next && next.index !== undefined ? start + next.index : raw.length; + return raw.slice(start, end).trim(); + } + + // Per-package changelogs use ### headings; nest them one level deeper + // so they live under the ### heading we add below. + function nestChangelogHeadings(notes) { + return notes.replace(/^(#{3,5}) /gm, '#$1 '); + } + + const lines = ['## Packages', '']; + for (const entry of packageInfo) { + lines.push(`- \`${entry.npmName}@${entry.ver}\` (tag: \`${entry.tag}\`)`); + } + + const packageNotes = packageInfo + .map((entry) => ({ + ...entry, + notes: extractChangelogBody(`packages/${entry.pkg}/CHANGELOG.md`, entry.ver), + })) + .filter((entry) => entry.notes.length > 0); + + if (packageNotes.length > 0) { + lines.push('', '## Package Changelogs', ''); + for (const entry of packageNotes) { + lines.push(`### ${entry.npmName}`, '', nestChangelogHeadings(entry.notes), ''); + } + } else { + lines.push('', '## Release Notes', '', '_No changelog entries were generated for this release._'); + } + + writeFileSync('/tmp/release-notes.md', `${lines.join('\n').trimEnd()}\n`); + + // Display name on the releases page. Always anchor to the canonical + // package so consumers see e.g. `agentworkforce@0.5.0` rather than + // a bare scoped package version. + const releaseName = `${canonical.npmName}@${canonical.ver}`; + appendFileSync(process.env.GITHUB_OUTPUT, `release_name=${releaseName}\n`); + GENEOF + + VERSIONS='${{ needs.publish.outputs.versions }}' \ + CANONICAL_PKG='${{ steps.release.outputs.canonical_pkg }}' \ + CANONICAL_VERSION='${{ steps.release.outputs.version }}' \ + node /tmp/build-release-notes.mjs + + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ matrix.package }}-v${{ steps.check.outputs.version }} - name: "@agentworkforce/${{ matrix.package }}@${{ steps.check.outputs.version }} (prerelease)" - prerelease: true - generate_release_notes: true + tag_name: ${{ steps.release.outputs.tag_name }} + name: ${{ steps.notes.outputs.release_name }} + body_path: /tmp/release-notes.md + prerelease: ${{ steps.release.outputs.prerelease }} diff --git a/.github/workflows/verify-publish.yml b/.github/workflows/verify-publish.yml index c966145c..032e7d1d 100644 --- a/.github/workflows/verify-publish.yml +++ b/.github/workflows/verify-publish.yml @@ -15,6 +15,7 @@ on: type: choice options: - '@agentworkforce/cli' + - 'agentworkforce' - '@agentworkforce/harness-kit' - '@agentworkforce/workload-router' version: @@ -54,31 +55,41 @@ jobs: echo "Verifying ${{ github.event.inputs.package }}@$RESOLVED" - name: CLI smoke test - if: ${{ github.event.inputs.package == '@agentworkforce/cli' }} + if: ${{ github.event.inputs.package == '@agentworkforce/cli' || github.event.inputs.package == 'agentworkforce' }} run: | set -euo pipefail + # The two CLI packages install different bin names: the scoped + # @agentworkforce/cli ships `agent-workforce`; the top-level + # `agentworkforce` package ships `agentworkforce`. The CLI itself + # derives its USAGE bin name from process.argv[1], so we expect + # the help block to use whichever name is on PATH. + if [ "${{ github.event.inputs.package }}" = "agentworkforce" ]; then + BIN="agentworkforce" + else + BIN="agent-workforce" + fi npm install -g "${{ github.event.inputs.package }}@${{ steps.resolve.outputs.version }}" # --help should exit 0 and print the usage block. - output=$(agent-workforce --help) + output=$("$BIN" --help) echo "$output" - echo "$output" | grep -q "Usage: agent-workforce agent" || { - echo "::error::--help output did not include expected usage block" >&2 + echo "$output" | grep -q "Usage: $BIN " || { + echo "::error::--help output did not include expected usage block (looking for 'Usage: $BIN ')" >&2 exit 1 } # Bare invocation should exit non-zero with the usage text. - if agent-workforce > /dev/null 2>&1; then - echo "::error::bare agent-workforce should exit non-zero (missing subcommand)" >&2 + if "$BIN" > /dev/null 2>&1; then + echo "::error::bare $BIN should exit non-zero (missing subcommand)" >&2 exit 1 fi # Unknown persona should exit non-zero and list the catalog. - if agent-workforce agent __definitely_not_a_persona__ > /dev/null 2>&1; then + if "$BIN" agent __definitely_not_a_persona__ > /dev/null 2>&1; then echo "::error::unknown persona should exit non-zero" >&2 exit 1 fi echo "CLI smoke test passed." - name: Library smoke test (ESM import + exports surface) - if: ${{ github.event.inputs.package != '@agentworkforce/cli' }} + if: ${{ github.event.inputs.package != '@agentworkforce/cli' && github.event.inputs.package != 'agentworkforce' }} run: | set -euo pipefail WORKDIR=$(mktemp -d) diff --git a/README.md b/README.md index 38b08a16..c42e9fdb 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,25 @@ model, system prompt, env vars, MCP servers, and permissions wired up. ### Install +From npm (recommended): + +```bash +npm i -g agentworkforce +``` + +That puts the `agentworkforce` command on your PATH. (The same CLI is also +published as `@agentworkforce/cli`, which exposes the same code under the +historical `agent-workforce` bin name — pick whichever name you prefer.) + From the monorepo checkout: ```bash corepack pnpm -r build corepack pnpm --filter @agentworkforce/cli link --global +# or: corepack pnpm --filter agentworkforce link --global ``` -`agent-workforce` is now on your PATH. (Or run the built bin directly: -`./packages/cli/dist/cli.js …`.) +(Or run the built bin directly: `./packages/cli/dist/cli.js …`.) ### Usage diff --git a/packages/agentworkforce/CHANGELOG.md b/packages/agentworkforce/CHANGELOG.md new file mode 100644 index 00000000..080a5c3a --- /dev/null +++ b/packages/agentworkforce/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to `agentworkforce` will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.4.0] - 2026-04-29 + +### Added + +- Initial release. This package installs the same CLI as `@agentworkforce/cli` + under the unscoped `agentworkforce` name, so `npm i -g agentworkforce` + provides the `agentworkforce` command on your PATH. diff --git a/packages/agentworkforce/README.md b/packages/agentworkforce/README.md new file mode 100644 index 00000000..ba2eb7ca --- /dev/null +++ b/packages/agentworkforce/README.md @@ -0,0 +1,25 @@ +# agentworkforce + +Top-level installer for the Agent Workforce CLI. + +```sh +npm i -g agentworkforce +``` + +Then: + +```sh +agentworkforce agent [@] +agentworkforce list [flags] +agentworkforce show +agentworkforce harness check +``` + +This package is a thin wrapper around [`@agentworkforce/cli`](https://www.npmjs.com/package/@agentworkforce/cli). +It exists so the global install command and the binary name match the +project name. Functionally identical to installing `@agentworkforce/cli` +and invoking `agent-workforce` — both bins call into the same code path. + +See the [main README](https://github.com/AgentWorkforce/workforce#readme) +for the full feature tour, and [`packages/cli/README.md`](https://github.com/AgentWorkforce/workforce/blob/main/packages/cli/README.md) +for command reference. diff --git a/packages/agentworkforce/bin/agentworkforce.js b/packages/agentworkforce/bin/agentworkforce.js new file mode 100755 index 00000000..2a5b8c3c --- /dev/null +++ b/packages/agentworkforce/bin/agentworkforce.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node +// Top-level wrapper. Re-exports the @agentworkforce/cli entry point under the +// shorter `agentworkforce` bin name so users can `npm i -g agentworkforce` and +// run `agentworkforce agent `. The CLI derives its help-text bin name +// from process.argv[1], so this file's basename (sans extension) is what +// shows up in usage strings. +import { main } from '@agentworkforce/cli/dist/cli.js'; + +main().catch((err) => { + process.stderr.write(`${err?.stack ?? String(err)}\n`); + process.exit(1); +}); diff --git a/packages/agentworkforce/package.json b/packages/agentworkforce/package.json new file mode 100644 index 00000000..b304e3ba --- /dev/null +++ b/packages/agentworkforce/package.json @@ -0,0 +1,37 @@ +{ + "name": "agentworkforce", + "version": "0.4.0", + "private": false, + "description": "Top-level installer for the Agent Workforce CLI (installs the `agentworkforce` command). Wraps @agentworkforce/cli.", + "type": "module", + "bin": { + "agentworkforce": "./bin/agentworkforce.js" + }, + "files": [ + "bin", + "README.md", + "CHANGELOG.md", + "package.json" + ], + "scripts": { + "build": "chmod +x bin/agentworkforce.js", + "dev": "chmod +x bin/agentworkforce.js", + "typecheck": "node --check bin/agentworkforce.js", + "lint": "node --check bin/agentworkforce.js", + "test": "node --check bin/agentworkforce.js" + }, + "engines": { + "node": ">=22" + }, + "dependencies": { + "@agentworkforce/cli": "workspace:*" + }, + "repository": { + "type": "git", + "url": "https://github.com/AgentWorkforce/workforce", + "directory": "packages/agentworkforce" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cli/README.md b/packages/cli/README.md index a3b9ceed..0a18d5fc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -20,17 +20,33 @@ agent-workforce harness check ## Install -The CLI ships as a `bin` in `@agentworkforce/cli` (which depends on -`@agentworkforce/workload-router` via the pnpm workspace). From the repo -checkout: +The CLI ships under two npm names that point at the same code: + +- **`@agentworkforce/cli`** — the scoped package; installs the + `agent-workforce` bin. +- **`agentworkforce`** — a thin top-level wrapper; installs the + `agentworkforce` bin so the global install command and command name match + (`npm i -g agentworkforce`). + +Both depend on `@agentworkforce/workload-router` and `@agentworkforce/harness-kit` +via the pnpm workspace. The CLI derives its help-text bin name from +`process.argv[1]`, so `--help` shows whichever name you invoked. + +From npm: + +```sh +npm i -g agentworkforce # provides `agentworkforce` +# or +npm i -g @agentworkforce/cli # provides `agent-workforce` +``` + +From the repo checkout: ```sh corepack pnpm -r build corepack pnpm --filter @agentworkforce/cli link --global ``` -That puts `agent-workforce` on your PATH. - ## Selectors ``` diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d85f8dd1..f6ca9f32 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -33,7 +33,22 @@ import { launchOnMount } from '@relayfile/local-mount'; import ora, { type Ora } from 'ora'; import { loadLocalPersonas, type PersonaSource } from './local-personas.js'; -const USAGE = `Usage: agent-workforce [args...] +// Derived from the binary the user actually invoked. Lets the same dist file +// drive multiple npm bins (`agent-workforce` from @agentworkforce/cli, +// `agentworkforce` from the top-level wrapper package) without baking either +// name into the help text. Falls back to `agent-workforce` for the bare +// `node dist/cli.js` case so the test suite, which spawns cli.js directly, +// still sees the historical name. +const BIN_NAME = (() => { + const arg1 = process.argv[1]; + if (!arg1) return 'agent-workforce'; + const base = arg1.split(/[/\\]/).pop() ?? ''; + const stripped = base.replace(/\.(m?js|cjs)$/, ''); + if (!stripped || stripped === 'cli') return 'agent-workforce'; + return stripped; +})(); + +const USAGE = `Usage: ${BIN_NAME} [args...] Commands: agent [flags] [@] [task...] @@ -98,12 +113,12 @@ loader implicitly inherits from the same-id persona below. (Override the home layer path via AGENT_WORKFORCE_CONFIG_DIR.) Examples: - agent-workforce agent npm-provenance-publisher@best - agent-workforce agent my-posthog@best - agent-workforce agent review@best-value "look at the diff on this branch" - agent-workforce list - agent-workforce show posthog - agent-workforce harness check + ${BIN_NAME} agent npm-provenance-publisher@best + ${BIN_NAME} agent my-posthog@best + ${BIN_NAME} agent review@best-value "look at the diff on this branch" + ${BIN_NAME} list + ${BIN_NAME} show posthog + ${BIN_NAME} harness check `; function die(msg: string, withUsage = true): never { @@ -984,7 +999,7 @@ function parseListArgs(args: readonly string[]): { json = true; } else if (arg === '-h' || arg === '--help') { process.stdout.write( - 'Usage: agent-workforce list [--all] [--json] [--filter-rating ] [--filter-harness ] [--filter-tag ] [--no-display-description]\n' + `Usage: ${BIN_NAME} list [--all] [--json] [--filter-rating ] [--filter-harness ] [--filter-tag ] [--no-display-description]\n` ); process.exit(0); } else if (arg === '--all' || arg === '--no-recommended') { @@ -1064,7 +1079,7 @@ function parseShowArgs(args: readonly string[]): { } else if (arg === '--all') { all = true; } else if (arg === '-h' || arg === '--help') { - process.stdout.write('Usage: agent-workforce show [@] [--all] [--json]\n'); + process.stdout.write(`Usage: ${BIN_NAME} show [@] [--all] [--json]\n`); process.exit(0); } else if (arg.startsWith('--')) { die(`show: unexpected flag "${arg}".`); @@ -1253,7 +1268,7 @@ function runHarnessCheck(): never { process.exit(0); } -async function main(): Promise { +export async function main(): Promise { const argv = process.argv.slice(2); const [subcommand, ...rest] = argv; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64cf36cb..c2ba2951 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,12 @@ importers: specifier: ^5.9.2 version: 5.9.3 + packages/agentworkforce: + dependencies: + '@agentworkforce/cli': + specifier: workspace:* + version: link:../cli + packages/cli: dependencies: '@agentworkforce/harness-kit':