| import { spawn } from "node:child_process"; | ||
| export const ASHBY_API_KEYS_URL = "https://app.ashbyhq.com/admin/api/keys"; | ||
| export const RECOMMENDED_PERMISSIONS = [ | ||
| "Jobs: read", | ||
| "Candidates: read + write", | ||
| "Interviews: read + write", | ||
| "Hiring Process: read", | ||
| "API Keys: read", | ||
| ]; | ||
| export function buildAuthSetupInstructions() { | ||
| return [ | ||
| "Ashby uses API keys, not OAuth.", | ||
| `Open this page: ${ASHBY_API_KEYS_URL}`, | ||
| "Create an API key with these permissions:", | ||
| ...RECOMMENDED_PERMISSIONS.map((permission) => `- ${permission}`), | ||
| "Optional toggles if you need them:", | ||
| "- Allow access to confidential jobs and projects", | ||
| "- Allow access to non-offer private fields", | ||
| "Then paste the key here so ashby-cli can save and validate it.", | ||
| ].join("\n"); | ||
| } | ||
| export function getBrowserOpenCommand(url, platform = process.platform) { | ||
| if (platform === "darwin") { | ||
| return { command: "open", args: [url] }; | ||
| } | ||
| if (platform === "win32") { | ||
| return { command: "cmd", args: ["/c", "start", "", url] }; | ||
| } | ||
| return { command: "xdg-open", args: [url] }; | ||
| } | ||
| export async function openBrowser(url, platform = process.platform) { | ||
| const { command, args } = getBrowserOpenCommand(url, platform); | ||
| return await new Promise((resolve) => { | ||
| const child = spawn(command, args, { stdio: "ignore" }); | ||
| child.on("error", (error) => { | ||
| resolve({ ok: false, command, error: error.message }); | ||
| }); | ||
| child.on("spawn", () => { | ||
| resolve({ ok: true, command }); | ||
| }); | ||
| }); | ||
| } |
+83
-1
| #!/usr/bin/env node | ||
| import { createRequire } from "node:module"; | ||
| import { createInterface } from "node:readline/promises"; | ||
| import { Command } from "commander"; | ||
| import { stdin as input, stderr as output } from "node:process"; | ||
| import { saveAndValidateApiKey, validateApiKey } from "./auth.js"; | ||
| import { ASHBY_API_KEYS_URL, buildAuthSetupInstructions, openBrowser } from "./auth-setup.js"; | ||
| import { AshbyApiClient, AshbyApiError } from "./ashby-api.js"; | ||
@@ -33,3 +36,3 @@ import { clearConfig, readConfig, redactApiKey, resolveApiKey } from "./config.js"; | ||
| else | ||
| process.stderr.write("No API key. Use `ashby auth set --stdin` or export `ASHBY_API_KEY`.\n"); | ||
| process.stderr.write("No API key. Use `ashby auth setup`, `ashby auth set --stdin`, or export `ASHBY_API_KEY`.\n"); | ||
| process.exitCode = 2; | ||
@@ -80,2 +83,81 @@ return ""; | ||
| auth | ||
| .command("setup") | ||
| .description("Open the Ashby API key page and save a pasted API key") | ||
| .option("--stdin", "Read the API key from stdin instead of prompting") | ||
| .option("--no-open", "Do not open the Ashby admin page in a browser") | ||
| .option("--open-only", "Only open the Ashby admin page and print setup guidance") | ||
| .option("--json", "Emit JSON output") | ||
| .action(async (opts) => { | ||
| const resolved = await resolveApiKey(); | ||
| const existingValidation = resolved ? await validateApiKey(resolved) : undefined; | ||
| const instructions = buildAuthSetupInstructions(); | ||
| const browser = opts.open === false | ||
| ? { attempted: false, ok: false, command: null, error: undefined } | ||
| : await openBrowser(ASHBY_API_KEYS_URL); | ||
| const browserMeta = { | ||
| attempted: opts.open !== false, | ||
| ok: browser.ok, | ||
| command: browser.command, | ||
| url: ASHBY_API_KEYS_URL, | ||
| error: browser.ok ? undefined : browser.error, | ||
| }; | ||
| if (opts.openOnly) { | ||
| if (opts.json) { | ||
| printJson(ok({ | ||
| alreadyConfigured: Boolean(resolved), | ||
| existingValidation, | ||
| browser: browserMeta, | ||
| instructions, | ||
| })); | ||
| } | ||
| else { | ||
| process.stderr.write(`${instructions}\n`); | ||
| } | ||
| return; | ||
| } | ||
| let apiKey = ""; | ||
| if (opts.stdin) { | ||
| apiKey = await readStdin(); | ||
| } | ||
| else if (input.isTTY) { | ||
| const rl = createInterface({ input, output }); | ||
| process.stderr.write(`${instructions}\n`); | ||
| apiKey = await rl.question("Paste Ashby API key: "); | ||
| rl.close(); | ||
| } | ||
| else { | ||
| const error = makeError(null, { | ||
| code: "AUTH_MISSING", | ||
| message: "No interactive terminal. Run `ashby auth setup --stdin`, `ashby auth set --stdin`, or export `ASHBY_API_KEY`.", | ||
| }); | ||
| if (opts.json) { | ||
| printJson(fail(error, { browser: browserMeta, instructions })); | ||
| } | ||
| else { | ||
| process.stderr.write(`${instructions}\n${error.message}\n`); | ||
| } | ||
| process.exitCode = 2; | ||
| return; | ||
| } | ||
| if (!apiKey.trim()) { | ||
| const error = makeError(null, { code: "VALIDATION", message: "No API key provided." }); | ||
| if (opts.json) | ||
| printJson(fail(error, { browser: browserMeta })); | ||
| else | ||
| process.stderr.write("No API key provided.\n"); | ||
| process.exitCode = 2; | ||
| return; | ||
| } | ||
| const result = await saveAndValidateApiKey(apiKey); | ||
| const payload = { | ||
| apiKeyRedacted: redactApiKey(result.apiKey), | ||
| validation: result.validation, | ||
| browser: browserMeta, | ||
| }; | ||
| if (opts.json) | ||
| printJson(ok(payload)); | ||
| else | ||
| console.log(`Saved API key ${payload.apiKeyRedacted}`); | ||
| }); | ||
| auth | ||
| .command("set") | ||
@@ -82,0 +164,0 @@ .description("Store an Ashby API key from stdin") |
+1
-1
| { | ||
| "name": "ashby-cli", | ||
| "version": "0.1.1", | ||
| "version": "0.1.2", | ||
| "description": "Agent-first CLI for Ashby's official API", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+34
-0
@@ -16,2 +16,5 @@ # ashby-cli | ||
| Package name: `ashby-cli` | ||
| Binary name: `ashby` | ||
| ```bash | ||
@@ -21,2 +24,8 @@ npm install -g ashby-cli | ||
| If you do not want a global install, invoke the published package directly: | ||
| ```bash | ||
| npx -y ashby-cli doctor --json | ||
| ``` | ||
| Or from source: | ||
@@ -35,2 +44,4 @@ | ||
| Ashby does not expose OAuth for this API. The easiest human setup path is browser-assisted API key creation. | ||
| You can either: | ||
@@ -42,2 +53,3 @@ | ||
| ```bash | ||
| ashby auth setup | ||
| ashby auth set --stdin | ||
@@ -48,2 +60,24 @@ ashby auth status | ||
| If you are using `npx`, remember it will not load `.env.local` automatically. Export `ASHBY_API_KEY` first or source your env file in the shell. | ||
| ### Recommended first-time setup | ||
| ```bash | ||
| ashby auth setup | ||
| ``` | ||
| This will: | ||
| - open the Ashby API key admin page | ||
| - remind you which permissions to enable | ||
| - accept a pasted API key | ||
| - save it locally | ||
| - validate it immediately | ||
| If you prefer not to install globally: | ||
| ```bash | ||
| npx -y ashby-cli auth setup | ||
| ``` | ||
| ## Commands | ||
@@ -50,0 +84,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
32893
19.67%11
10%712
21.09%135
33.66%2
100%