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
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 14 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand Down Expand Up @@ -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
```
Expand Down Expand Up @@ -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`
Expand All @@ -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`
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <model-key>` and `--model=<model-key>` support for curated non-secret model selection.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ You can also skip the interactive model prompt with:
npx @gonkagate/claude-code --model <model-key>
```

The setup-style alias accepts the same options:

```bash
npx @gonkagate/claude-code-setup --model <model-key>
```

Only current curated keys are accepted.

## Corrupted settings file
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"type": "module",
"bin": {
"claude-code-setup": "bin/gonkagate-claude-code.js",
"gonkagate-claude-code": "bin/gonkagate-claude-code.js"
},
"files": [
Expand All @@ -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"
},
Expand All @@ -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",
Expand Down
108 changes: 108 additions & 0 deletions scripts/publish-alias-package.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}
25 changes: 18 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand All @@ -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 <model-key>", "Skip the model prompt with a curated supported model.").choices(SUPPORTED_MODEL_KEYS)
Expand All @@ -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

Expand All @@ -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<ParsedProgramOptions>();
return {
Expand All @@ -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.");
Expand Down Expand Up @@ -158,8 +169,8 @@ export async function resolveSettingsTarget(
}
}

export async function run(argv = process.argv.slice(2)): Promise<void> {
const options = parseCliOptions(argv);
export async function run(argv = process.argv.slice(2), commandName = getCommandNameFromArgv()): Promise<void> {
const options = parseCliOptions(argv, undefined, commandName);

printIntro();

Expand Down
Loading