diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 051bc09..dc6bff7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -84,3 +84,7 @@ jobs: - name: Publish if: (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'publish')) && steps.publish_check.outputs.should_publish == 'true' run: npm publish --provenance --access public + + - name: Publish setup-style alias package + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'publish') + run: npm run publish:alias diff --git a/AGENTS.md b/AGENTS.md index c2532d7..dd68863 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,12 @@ Recommended public flow: npx @gonkagate/claude-code ``` +Setup-style alias for consistency with other GonkaGate agent installers: + +```bash +npx @gonkagate/claude-code-setup +``` + The happy-path installer interactively prompts only for: - a `gp-...` API key @@ -43,6 +49,7 @@ These decisions are part of the repo contract. Changing them is not a small refa - the default auth path is `ANTHROPIC_AUTH_TOKEN` - model choice comes only from a code-owned curated registry - the primary UX is `npx @gonkagate/claude-code` +- the setup-style alias is `npx @gonkagate/claude-code-setup` - API key entry must remain interactive and hidden - the installer writes Claude Code settings, not shell env and not shell rc files - default scope is `user` @@ -114,7 +121,8 @@ This repo does not do: │ └── troubleshooting.md ├── scripts/ │ ├── install.ps1 -│ └── install.sh +│ ├── install.sh +│ └── publish-alias-package.mjs └── test/ └── install.test.ts ``` @@ -236,6 +244,10 @@ Fallback entrypoints: - `install.sh` - `install.ps1` +Alias publishing: + +- `publish-alias-package.mjs` builds the `@gonkagate/claude-code-setup` alias package from the current root build output + They must not replace `npx` as the primary public UX. ### `test/install.test.ts` @@ -250,7 +262,7 @@ Baseline tests cover: ## Installer Happy Path -1. The user runs `npx @gonkagate/claude-code` +1. The user runs `npx @gonkagate/claude-code` or the setup-style alias `npx @gonkagate/claude-code-setup` 2. The installer securely prompts for a `gp-...` API key 3. The installer shows the curated model picker 4. The installer asks for scope: `user` or `local` diff --git a/CHANGELOG.md b/CHANGELOG.md index 430c5e7..1e77768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Added `@gonkagate/claude-code-setup` as a setup-style alias for the existing Claude Code installer. - Improved npm package metadata and README copy for better package-page clarity, discovery, and onboarding. - Added a curated model registry and model picker to the public installer flow. - Added `--model ` and `--model=` support for curated non-secret model selection. diff --git a/README.md b/README.md index 0f4fab5..9459e00 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ It does not install `Claude Code` itself. It configures an existing local Claude npx @gonkagate/claude-code ``` +Setup-style alias for consistency with the other GonkaGate agent installers: + +```bash +npx @gonkagate/claude-code-setup +``` + ## See It In Action From API key to a working Claude Code setup in one short walkthrough: diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 1f1c280..0860f27 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -2,6 +2,8 @@ `gonkagate-claude-code` is a small Node-based installer for local `Claude Code`. +The primary package remains `@gonkagate/claude-code`. The setup-style alias package `@gonkagate/claude-code-setup` runs the same installer for consistency with the other GonkaGate agent setup packages. + ## Fixed product contract These values are intentionally fixed by the installer: diff --git a/docs/security.md b/docs/security.md index e2c391f..50d51a0 100644 --- a/docs/security.md +++ b/docs/security.md @@ -8,6 +8,8 @@ The recommended install flow is interactive: npx @gonkagate/claude-code ``` +The setup-style alias `npx @gonkagate/claude-code-setup` runs the same interactive installer. + The installer asks for the API key through a hidden prompt. It intentionally rejects `--api-key ...` arguments so secrets do not end up in shell history or process lists. ## Where the key is stored diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ba971f5..9f5683c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -39,6 +39,12 @@ You can also skip the interactive model prompt with: npx @gonkagate/claude-code --model ``` +The setup-style alias accepts the same options: + +```bash +npx @gonkagate/claude-code-setup --model +``` + Only current curated keys are accepted. ## Corrupted settings file diff --git a/package-lock.json b/package-lock.json index f7bc4bc..0167e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "write-file-atomic": "^5.0.1" }, "bin": { + "claude-code-setup": "bin/gonkagate-claude-code.js", "gonkagate-claude-code": "bin/gonkagate-claude-code.js" }, "devDependencies": { diff --git a/package.json b/package.json index 90f268f..4367d44 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "type": "module", "bin": { + "claude-code-setup": "bin/gonkagate-claude-code.js", "gonkagate-claude-code": "bin/gonkagate-claude-code.js" }, "files": [ @@ -27,6 +28,7 @@ "build": "tsc -p tsconfig.json", "dev": "tsx src/cli.ts", "prepack": "npm run build", + "publish:alias": "node scripts/publish-alias-package.mjs", "test": "node scripts/run-tests.mjs", "ci": "npm test && npm run build" }, @@ -42,7 +44,9 @@ "gonka-api", "gonka gateway", "claude-code", - "claude code" + "claude code", + "claude-code-setup", + "setup" ], "license": "Apache-2.0", "packageManager": "npm@11.11.1", diff --git a/scripts/publish-alias-package.mjs b/scripts/publish-alias-package.mjs new file mode 100644 index 0000000..8ed73b6 --- /dev/null +++ b/scripts/publish-alias-package.mjs @@ -0,0 +1,108 @@ +import { spawnSync } from "node:child_process"; +import { constants as fsConstants } from "node:fs"; +import { access, cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ALIAS_PACKAGE_NAME = "@gonkagate/claude-code-setup"; +const ALIAS_BIN_NAME = "claude-code-setup"; +const dryRun = process.argv.includes("--dry-run"); + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(scriptDir, ".."); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: repoRoot, + stdio: "inherit", + ...options + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function isPublished(packageName, version) { + const result = spawnSync("npm", ["view", `${packageName}@${version}`, "version"], { + cwd: repoRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"] + }); + + if (result.status === 0) { + return true; + } + + const output = `${result.stdout}\n${result.stderr}`; + if (output.includes("E404") || output.includes("404 Not Found")) { + return false; + } + + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); +} + +async function assertExists(path) { + await access(path, fsConstants.R_OK); +} + +const rootPackage = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf8")); +const packageVersion = rootPackage.version; + +if (!dryRun && isPublished(ALIAS_PACKAGE_NAME, packageVersion)) { + console.log(`${ALIAS_PACKAGE_NAME}@${packageVersion} is already published; skipping alias publish.`); + process.exit(0); +} + +for (const path of ["bin", "dist", "docs", "README.md", "CHANGELOG.md", "LICENSE"]) { + await assertExists(join(repoRoot, path)); +} + +const tempRoot = await mkdtemp(join(tmpdir(), "gonkagate-claude-code-setup-")); +const packageRoot = join(tempRoot, "package"); + +try { + await mkdir(packageRoot, { recursive: true }); + + for (const path of ["bin", "dist", "docs", "README.md", "CHANGELOG.md", "LICENSE"]) { + await cp(join(repoRoot, path), join(packageRoot, path), { recursive: true }); + } + + const aliasPackage = { + name: ALIAS_PACKAGE_NAME, + version: packageVersion, + description: "Setup-style alias for the GonkaGate Claude Code installer.", + homepage: rootPackage.homepage, + bugs: rootPackage.bugs, + repository: rootPackage.repository, + type: rootPackage.type, + bin: { + [ALIAS_BIN_NAME]: "bin/gonkagate-claude-code.js" + }, + files: ["bin", "dist", "docs", "README.md", "CHANGELOG.md", "LICENSE"], + engines: rootPackage.engines, + keywords: Array.from(new Set([...rootPackage.keywords, ALIAS_BIN_NAME, "setup"])), + license: rootPackage.license, + dependencies: rootPackage.dependencies + }; + + await writeFile(join(packageRoot, "package.json"), `${JSON.stringify(aliasPackage, null, 2)}\n`, "utf8"); + + const publishArgs = ["publish", "--access", "public"]; + if (dryRun) { + publishArgs.push("--dry-run"); + } else { + publishArgs.push("--provenance"); + } + + run("npm", publishArgs, { cwd: packageRoot }); +} finally { + await rm(tempRoot, { recursive: true, force: true }); +} diff --git a/src/cli.ts b/src/cli.ts index 18daa8b..3469706 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,5 @@ import process from "node:process"; +import { basename } from "node:path"; import { pathToFileURL } from "node:url"; import { Command, CommanderError, Option } from "commander"; import { DEFAULT_MODEL_KEY, SUPPORTED_MODELS, SUPPORTED_MODEL_KEYS, requireSupportedModel } from "./constants/models.js"; @@ -14,6 +15,8 @@ import type { InstallScope, SettingsTarget } from "./types/settings.js"; import type { SupportedModel, SupportedModelKey } from "./constants/models.js"; import type { TrackedLocalSettingsAction } from "./install/prompts.js"; +const DEFAULT_COMMAND_NAME = "gonkagate-claude-code"; + interface CliOptions { help: boolean; version: boolean; @@ -37,14 +40,14 @@ function rejectApiKeyArgs(argv: string[]): void { } } -function createProgram(output?: ProgramOutput): Command { +function createProgram(output?: ProgramOutput, commandName = DEFAULT_COMMAND_NAME): Command { const supportedModelLines = SUPPORTED_MODELS.map((model) => { const defaultSuffix = model.key === DEFAULT_MODEL_KEY ? " (default)" : ""; return ` ${model.key} ${model.displayName}${defaultSuffix}`; }).join("\n"); const program = new Command() - .name("gonkagate-claude-code") + .name(commandName) .description("GonkaGate Claude Code installer") .addOption( new Option("--model ", "Skip the model prompt with a curated supported model.").choices(SUPPORTED_MODEL_KEYS) @@ -57,6 +60,7 @@ function createProgram(output?: ProgramOutput): Command { ` Examples: npx @gonkagate/claude-code + npx @gonkagate/claude-code-setup npx @gonkagate/claude-code --model ${DEFAULT_MODEL_KEY} npx @gonkagate/claude-code --scope local @@ -73,11 +77,11 @@ ${supportedModelLines} return program; } -export function parseCliOptions(argv: string[], output?: ProgramOutput): CliOptions { +export function parseCliOptions(argv: string[], output?: ProgramOutput, commandName = DEFAULT_COMMAND_NAME): CliOptions { rejectApiKeyArgs(argv); - const program = createProgram(output); - program.parse(["node", "gonkagate-claude-code", ...argv]); + const program = createProgram(output, commandName); + program.parse(["node", commandName, ...argv]); const options = program.opts(); return { @@ -88,6 +92,13 @@ export function parseCliOptions(argv: string[], output?: ProgramOutput): CliOpti }; } +function getCommandNameFromArgv(): string { + const commandName = process.argv[1] === undefined + ? DEFAULT_COMMAND_NAME + : basename(process.argv[1]).replace(/\.(?:js|ts)$/, ""); + return commandName === "cli" ? DEFAULT_COMMAND_NAME : commandName; +} + function printIntro(): void { console.log("Connect Claude Code to GonkaGate in one step.\n"); console.log("This installer writes Claude Code settings for GonkaGate's public gateway."); @@ -158,8 +169,8 @@ export async function resolveSettingsTarget( } } -export async function run(argv = process.argv.slice(2)): Promise { - const options = parseCliOptions(argv); +export async function run(argv = process.argv.slice(2), commandName = getCommandNameFromArgv()): Promise { + const options = parseCliOptions(argv, undefined, commandName); printIntro();