diff --git a/cli/src/__tests__/companies-sh.test.ts b/cli/src/__tests__/companies-sh.test.ts new file mode 100644 index 00000000..d0d5e203 --- /dev/null +++ b/cli/src/__tests__/companies-sh.test.ts @@ -0,0 +1,75 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const scriptPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../companies.sh"); + +function runEcho(args: string[]) { + return execFileSync("bash", [scriptPath, ...args], { + cwd: path.dirname(scriptPath), + env: { + ...process.env, + COMPANIES_SH_ECHO: "1", + }, + encoding: "utf8", + }).trim(); +} + +describe("companies.sh", () => { + it("passes through positional source imports with current company import ergonomics", () => { + expect(runEcho([ + "paperclipai/companies/engineering", + "--target", "existing", + "-C", "company-123", + "--dry-run", + ])).toBe( + "pnpm paperclipai company import paperclipai/companies/engineering --target existing -C company-123 --dry-run", + ); + }); + + it("accepts the optional import verb", () => { + expect(runEcho([ + "import", + "./exports/acme", + "--include", "agents,skills", + "--collision", "rename", + ])).toBe( + "pnpm paperclipai company import ./exports/acme --include agents\\,skills --collision rename", + ); + }); + + it("normalizes legacy --from usage into the positional source argument", () => { + expect(runEcho([ + "--from", "https://github.com/org/repo/tree/main/acme", + "--ref", "release/2026-03-23", + "--yes", + ])).toBe( + "pnpm paperclipai company import https://github.com/org/repo/tree/main/acme --ref release/2026-03-23 --yes", + ); + }); + + it("supports --from=value compatibility", () => { + expect(runEcho([ + "--from=org/repo/company-template", + "--paperclip-url", "http://localhost:3100", + "--json", + ])).toBe( + "pnpm paperclipai company import org/repo/company-template --paperclip-url http://localhost:3100 --json", + ); + }); + + it("fails when no source path or URL is provided", () => { + const result = spawnSync("bash", [scriptPath, "--dry-run"], { + cwd: path.dirname(scriptPath), + env: { + ...process.env, + COMPANIES_SH_ECHO: "1", + }, + encoding: "utf8", + }); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("source path or URL is required"); + }); +}); diff --git a/companies.sh b/companies.sh new file mode 100755 index 00000000..da1aa6ff --- /dev/null +++ b/companies.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +usage() { + cat <<'EOF' +Usage: + ./companies.sh [paperclipai company import flags...] + ./companies.sh import [paperclipai company import flags...] + ./companies.sh --from [paperclipai company import flags...] + +Thin wrapper around: + pnpm paperclipai company import ... + +Notes: + - Accepts the source as the first positional argument, like `paperclipai company import` + - Still accepts legacy `--from ` for compatibility + - Runs from the repo root so it can be invoked from anywhere + +Examples: + ./companies.sh org/repo/company-template --dry-run + ./companies.sh import ./exports/acme --target existing -C company-123 + ./companies.sh --from https://github.com/org/repo/tree/main/acme --ref main +EOF +} + +fail() { + printf 'companies.sh: %s\n' "$*" >&2 + exit 1 +} + +source_arg="" +expect_legacy_source=0 +pass_through=() + +if [[ $# -gt 0 && "$1" == "import" ]]; then + shift +fi + +while [[ $# -gt 0 ]]; do + arg="$1" + shift + + if [[ "$expect_legacy_source" -eq 1 ]]; then + [[ -n "$arg" ]] || fail "--from requires a value" + [[ -z "$source_arg" ]] || fail "source path or URL was provided more than once" + source_arg="$arg" + expect_legacy_source=0 + continue + fi + + case "$arg" in + help|-h|--help) + usage + exit 0 + ;; + --from) + expect_legacy_source=1 + ;; + --from=*) + value="${arg#--from=}" + [[ -n "$value" ]] || fail "--from requires a value" + [[ -z "$source_arg" ]] || fail "source path or URL was provided more than once" + source_arg="$value" + ;; + --include|--target|-C|--company-id|--new-company-name|--agents|--collision|--ref|--paperclip-url|--api-base) + [[ $# -gt 0 ]] || fail "$arg requires a value" + pass_through+=("$arg" "$1") + shift + ;; + --yes|--dry-run|--json) + pass_through+=("$arg") + ;; + --) + if [[ $# -gt 0 ]]; then + if [[ -z "$source_arg" ]]; then + source_arg="$1" + shift + else + fail "unexpected extra positional argument: $1" + fi + fi + while [[ $# -gt 0 ]]; do + pass_through+=("$1") + shift + done + ;; + -*) + pass_through+=("$arg") + ;; + *) + if [[ -z "$source_arg" ]]; then + source_arg="$arg" + else + fail "unexpected extra positional argument: $arg" + fi + ;; + esac +done + +[[ "$expect_legacy_source" -eq 0 ]] || fail "--from requires a value" +[[ -n "$source_arg" ]] || fail "source path or URL is required" + +cmd=(pnpm paperclipai company import "$source_arg") +if [[ "${#pass_through[@]}" -gt 0 ]]; then + cmd+=("${pass_through[@]}") +fi + +if [[ "${COMPANIES_SH_ECHO:-}" == "1" ]]; then + printf '%q ' "${cmd[@]}" + printf '\n' + exit 0 +fi + +cd "$repo_root" +exec "${cmd[@]}" diff --git a/docs/cli/control-plane-commands.md b/docs/cli/control-plane-commands.md index 80eb0edb..cdb52f5e 100644 --- a/docs/cli/control-plane-commands.md +++ b/docs/cli/control-plane-commands.md @@ -54,6 +54,9 @@ pnpm paperclipai company import \ --target new \ --new-company-name "Acme Imported" \ --include company,agents + +# Repo helper wrapper with the same source-first ergonomics +./companies.sh org/repo/company-template --target new --dry-run ``` ## Agent Commands diff --git a/docs/guides/board-operator/importing-and-exporting.md b/docs/guides/board-operator/importing-and-exporting.md index ae4f36a6..0bd468b8 100644 --- a/docs/guides/board-operator/importing-and-exporting.md +++ b/docs/guides/board-operator/importing-and-exporting.md @@ -92,6 +92,13 @@ paperclipai company import org/repo paperclipai company import org/repo/companies/acme ``` +If you are working inside the Paperclip repo, `./companies.sh` is a thin wrapper around `paperclipai company import`. It accepts the same source-first form and still supports legacy `--from ` compatibility: + +```sh +./companies.sh org/repo/companies/acme --dry-run +./companies.sh --from ./my-export --target existing --company-id abc123 +``` + ### Options | Option | Description | Default |