api2cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "api2cli",
3
+ "version": "0.1.0",
4
+ "description": "Turn any REST API into a standardized, agent-ready CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "api2cli": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build src/index.ts --outfile dist/index.js --target bun",
11
+ "dev": "bun run src/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "commander": "^13.0.0",
15
+ "picocolors": "^1.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "latest",
19
+ "typescript": "^5.7.0"
20
+ },
21
+ "license": "MIT",
22
+ "keywords": ["cli", "api", "rest", "agent", "skill", "mcp", "agentskills"]
23
+ }
@@ -0,0 +1,68 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import pc from "picocolors";
5
+ import { getCliDir, getDistDir, CLI_ROOT } from "../lib/config.js";
6
+ import { readdirSync } from "fs";
7
+
8
+ export const bundleCommand = new Command("bundle")
9
+ .description("Build/rebuild a CLI from source")
10
+ .argument("[app]", "CLI to build (omit with --all)")
11
+ .option("--compile", "Create standalone binary (~50MB, no runtime needed)")
12
+ .option("--all", "Build all installed CLIs")
13
+ .addHelpText(
14
+ "after",
15
+ "\nExamples:\n api2cli bundle typefully\n api2cli bundle typefully --compile\n api2cli bundle --all",
16
+ )
17
+ .action(async (app: string | undefined, opts) => {
18
+ if (opts.all) {
19
+ if (!existsSync(CLI_ROOT)) {
20
+ console.log("No CLIs installed.");
21
+ return;
22
+ }
23
+ const dirs = readdirSync(CLI_ROOT).filter((d) => d.endsWith("-cli"));
24
+ for (const d of dirs) {
25
+ await buildCli(d.replace(/-cli$/, ""), opts.compile);
26
+ }
27
+ return;
28
+ }
29
+
30
+ if (!app) {
31
+ console.error("Specify an app name or use --all");
32
+ process.exit(2);
33
+ }
34
+
35
+ await buildCli(app, opts.compile);
36
+ });
37
+
38
+ async function buildCli(app: string, compile?: boolean): Promise<void> {
39
+ const cliDir = getCliDir(app);
40
+ if (!existsSync(cliDir)) {
41
+ console.error(`${pc.red("✗")} ${app}-cli not found. Run: ${pc.cyan(`api2cli create ${app}`)}`);
42
+ return;
43
+ }
44
+
45
+ const distDir = getDistDir(app);
46
+ mkdirSync(distDir, { recursive: true });
47
+
48
+ console.log(`Building ${pc.bold(`${app}-cli`)}...`);
49
+
50
+ const entry = join(cliDir, "src", "index.ts");
51
+ const outfile = join(distDir, compile ? `${app}-cli` : `${app}-cli.js`);
52
+ const args = ["bun", "build", entry, "--outfile", outfile, "--target", "bun"];
53
+ if (compile) args.push("--compile");
54
+
55
+ const proc = Bun.spawn(args, { cwd: cliDir, stdout: "pipe", stderr: "pipe" });
56
+ const code = await proc.exited;
57
+
58
+ if (code === 0) {
59
+ const size = Bun.file(outfile).size;
60
+ const sizeStr = size > 1024 * 1024
61
+ ? `${(size / 1024 / 1024).toFixed(1)}MB`
62
+ : `${(size / 1024).toFixed(1)}KB`;
63
+ console.log(`${pc.green("✓")} Built ${pc.bold(`${app}-cli`)} (${sizeStr})`);
64
+ } else {
65
+ const stderr = await new Response(proc.stderr).text();
66
+ console.error(`${pc.red("✗")} Build failed: ${stderr}`);
67
+ }
68
+ }
@@ -0,0 +1,78 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import pc from "picocolors";
5
+ import { getCliDir } from "../lib/config.js";
6
+ import { copyTemplate, replacePlaceholders } from "../lib/template.js";
7
+
8
+ export const createCommand = new Command("create")
9
+ .description("Generate a new CLI from API documentation")
10
+ .argument("<app>", "API/app name (e.g. typefully, dub, mercury)")
11
+ .option("--docs <url>", "URL to API documentation")
12
+ .option("--openapi <url>", "URL to OpenAPI/Swagger spec")
13
+ .option("--base-url <url>", "API base URL", "https://api.example.com")
14
+ .option("--auth-type <type>", "Auth type: bearer, api-key, basic, custom", "bearer")
15
+ .option("--auth-header <name>", "Auth header name", "Authorization")
16
+ .option("--force", "Overwrite existing CLI", false)
17
+ .addHelpText(
18
+ "after",
19
+ `
20
+ Examples:
21
+ api2cli create typefully --base-url https://api.typefully.com --auth-type bearer
22
+ api2cli create dub --openapi https://api.dub.co/openapi.json
23
+ api2cli create my-api --docs https://docs.example.com/api`,
24
+ )
25
+ .action(async (app: string, opts) => {
26
+ const cliDir = getCliDir(app);
27
+
28
+ if (existsSync(cliDir) && !opts.force) {
29
+ console.error(`${pc.red("✗")} ${app}-cli already exists at ${cliDir}`);
30
+ console.error(` Use ${pc.cyan("--force")} to overwrite.`);
31
+ process.exit(1);
32
+ }
33
+
34
+ console.log(`\n${pc.bold("Creating")} ${pc.cyan(`${app}-cli`)}...\n`);
35
+
36
+ // 1. Create target directory
37
+ mkdirSync(cliDir, { recursive: true });
38
+ console.log(` ${pc.green("+")} Created ${pc.dim(cliDir)}`);
39
+
40
+ // 2. Copy template
41
+ copyTemplate(cliDir);
42
+ console.log(` ${pc.green("+")} Copied template scaffold`);
43
+
44
+ // 3. Replace placeholders
45
+ replacePlaceholders(cliDir, {
46
+ appName: app,
47
+ appCli: `${app}-cli`,
48
+ baseUrl: opts.baseUrl,
49
+ authType: opts.authType,
50
+ authHeader: opts.authHeader,
51
+ });
52
+ console.log(` ${pc.green("+")} Configured for ${pc.bold(app)}`);
53
+
54
+ // 4. Install dependencies
55
+ console.log(` ${pc.dim("Installing dependencies...")}`);
56
+ const install = Bun.spawn(["bun", "install"], {
57
+ cwd: cliDir,
58
+ stdout: "ignore",
59
+ stderr: "pipe",
60
+ });
61
+ await install.exited;
62
+ console.log(` ${pc.green("+")} Dependencies installed`);
63
+
64
+ // 5. Rename SKILL.md.template
65
+ const skillTemplate = join(cliDir, "SKILL.md.template");
66
+ const skillTarget = join(cliDir, "SKILL.md");
67
+ if (existsSync(skillTemplate)) {
68
+ const { renameSync } = require("fs");
69
+ renameSync(skillTemplate, skillTarget);
70
+ }
71
+
72
+ console.log(`\n${pc.green("✓")} Created ${pc.bold(`${app}-cli`)} at ${pc.dim(cliDir)}`);
73
+ console.log(`\n${pc.bold("Next steps:")}`);
74
+ console.log(` 1. Edit resources in ${pc.dim(`${cliDir}/src/resources/`)}`);
75
+ console.log(` 2. Build: ${pc.cyan(`api2cli bundle ${app}`)}`);
76
+ console.log(` 3. Link: ${pc.cyan(`api2cli link ${app}`)}`);
77
+ console.log(` 4. Auth: ${pc.cyan(`${app}-cli auth set "your-token"`)}`);
78
+ });
@@ -0,0 +1,48 @@
1
+ import { Command } from "commander";
2
+ import { existsSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { CLI_ROOT, TOKENS_DIR, TEMPLATE_DIR } from "../lib/config.js";
5
+
6
+ export const doctorCommand = new Command("doctor")
7
+ .description("Check system requirements and configuration")
8
+ .addHelpText("after", "\nExample:\n api2cli doctor")
9
+ .action(async () => {
10
+ console.log(`\n${pc.bold("api2cli doctor")}\n`);
11
+ let issues = 0;
12
+
13
+ // Bun
14
+ try {
15
+ const proc = Bun.spawn(["bun", "--version"], { stdout: "pipe", stderr: "pipe" });
16
+ const version = (await new Response(proc.stdout).text()).trim();
17
+ console.log(` ${pc.green("✓")} Bun ${version}`);
18
+ } catch {
19
+ console.log(` ${pc.red("✗")} Bun not found. Install: ${pc.cyan("https://bun.sh")}`);
20
+ issues++;
21
+ }
22
+
23
+ // CLI root
24
+ if (existsSync(CLI_ROOT)) {
25
+ console.log(` ${pc.green("✓")} CLI root: ${pc.dim(CLI_ROOT)}`);
26
+ } else {
27
+ console.log(` ${pc.yellow("~")} CLI root not yet created: ${pc.dim(CLI_ROOT)}`);
28
+ }
29
+
30
+ // Tokens dir
31
+ if (existsSync(TOKENS_DIR)) {
32
+ console.log(` ${pc.green("✓")} Tokens dir: ${pc.dim(TOKENS_DIR)}`);
33
+ } else {
34
+ console.log(` ${pc.yellow("~")} Tokens dir not yet created: ${pc.dim(TOKENS_DIR)}`);
35
+ }
36
+
37
+ // Template
38
+ if (existsSync(TEMPLATE_DIR)) {
39
+ console.log(` ${pc.green("✓")} Template: ${pc.dim(TEMPLATE_DIR)}`);
40
+ } else {
41
+ console.log(` ${pc.red("✗")} Template not found: ${pc.dim(TEMPLATE_DIR)}`);
42
+ issues++;
43
+ }
44
+
45
+ console.log(
46
+ issues === 0 ? `\n${pc.green("All good!")} 🎉\n` : `\n${pc.red(`${issues} issue(s) found.`)}\n`,
47
+ );
48
+ });
@@ -0,0 +1,29 @@
1
+ import { Command } from "commander";
2
+ import { existsSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { getCliDir } from "../lib/config.js";
5
+
6
+ export const installCommand = new Command("install")
7
+ .description("Install a pre-built CLI from the registry")
8
+ .argument("<app>", "CLI to install (e.g. typefully, dub)")
9
+ .option("--force", "Overwrite existing CLI", false)
10
+ .addHelpText(
11
+ "after",
12
+ "\nExamples:\n api2cli install typefully\n api2cli install dub --force",
13
+ )
14
+ .action(async (app: string, opts) => {
15
+ const cliDir = getCliDir(app);
16
+
17
+ if (existsSync(cliDir) && !opts.force) {
18
+ console.error(`${pc.red("✗")} ${app}-cli already installed. Use ${pc.cyan("--force")} to reinstall.`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(`Installing ${pc.bold(`${app}-cli`)} from registry...`);
23
+
24
+ // TODO: Fetch from npm @api2cli/<app> or api2cli.dev API
25
+ // Download resources + config -> merge with template -> build -> link
26
+ console.log(`\n${pc.yellow("🚧")} Registry not yet available.`);
27
+ console.log(`\nCreate it manually instead:`);
28
+ console.log(` ${pc.cyan(`api2cli create ${app} --docs <api-docs-url>`)}`);
29
+ });
@@ -0,0 +1,32 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, readdirSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { getCliDir, getDistDir, CLI_ROOT } from "../lib/config.js";
5
+ import { addToPath } from "../lib/shell.js";
6
+
7
+ export const linkCommand = new Command("link")
8
+ .description("Add a CLI to your PATH")
9
+ .argument("[app]", "CLI to link (omit with --all)")
10
+ .option("--all", "Link all installed CLIs")
11
+ .addHelpText("after", "\nExamples:\n api2cli link typefully\n api2cli link --all")
12
+ .action((app: string | undefined, opts) => {
13
+ if (opts.all || !app) {
14
+ if (!existsSync(CLI_ROOT)) {
15
+ console.log("No CLIs installed.");
16
+ return;
17
+ }
18
+ const dirs = readdirSync(CLI_ROOT).filter((d) => d.endsWith("-cli"));
19
+ for (const d of dirs) {
20
+ const name = d.replace(/-cli$/, "");
21
+ addToPath(name, getDistDir(name));
22
+ }
23
+ return;
24
+ }
25
+
26
+ if (!existsSync(getCliDir(app))) {
27
+ console.error(`${pc.red("✗")} ${app}-cli not found. Run: ${pc.cyan(`api2cli create ${app}`)}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ addToPath(app, getDistDir(app));
32
+ });
@@ -0,0 +1,52 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, readdirSync, statSync } from "fs";
3
+ import { join } from "path";
4
+ import pc from "picocolors";
5
+ import { CLI_ROOT, TOKENS_DIR } from "../lib/config.js";
6
+
7
+ export const listCommand = new Command("list")
8
+ .description("List all installed CLIs")
9
+ .option("--json", "Output as JSON")
10
+ .addHelpText("after", "\nExamples:\n api2cli list\n api2cli list --json")
11
+ .action((opts) => {
12
+ if (!existsSync(CLI_ROOT)) {
13
+ console.log("No CLIs installed. Run: api2cli create <app>");
14
+ return;
15
+ }
16
+
17
+ const dirs = readdirSync(CLI_ROOT).filter((d) => {
18
+ return statSync(join(CLI_ROOT, d)).isDirectory() && d.endsWith("-cli");
19
+ });
20
+
21
+ if (dirs.length === 0) {
22
+ console.log("No CLIs installed.");
23
+ return;
24
+ }
25
+
26
+ if (opts.json) {
27
+ const data = dirs.map((d) => {
28
+ const name = d.replace(/-cli$/, "");
29
+ return {
30
+ name,
31
+ built: existsSync(join(CLI_ROOT, d, "dist")),
32
+ hasToken: existsSync(join(TOKENS_DIR, `${d}.txt`)),
33
+ path: join(CLI_ROOT, d),
34
+ };
35
+ });
36
+ console.log(JSON.stringify({ ok: true, data }, null, 2));
37
+ return;
38
+ }
39
+
40
+ console.log(`\n${pc.bold("Installed CLIs:")}\n`);
41
+ for (const d of dirs) {
42
+ const name = d.replace(/-cli$/, "");
43
+ const built = existsSync(join(CLI_ROOT, d, "dist"));
44
+ const hasToken = existsSync(join(TOKENS_DIR, `${d}.txt`));
45
+ const status = [
46
+ built ? pc.green("built") : pc.yellow("not built"),
47
+ hasToken ? pc.green("auth") : pc.dim("no auth"),
48
+ ].join(pc.dim(" | "));
49
+ console.log(` ${pc.bold(name.padEnd(20))} ${status}`);
50
+ }
51
+ console.log();
52
+ });
@@ -0,0 +1,22 @@
1
+ import { Command } from "commander";
2
+ import { existsSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { getCliDir } from "../lib/config.js";
5
+
6
+ export const publishCommand = new Command("publish")
7
+ .description("Publish a CLI to the api2cli registry")
8
+ .argument("<app>", "CLI to publish")
9
+ .option("--scope <scope>", "npm scope", "@api2cli")
10
+ .addHelpText("after", "\nExample:\n api2cli publish typefully")
11
+ .action(async (app: string, opts) => {
12
+ const cliDir = getCliDir(app);
13
+
14
+ if (!existsSync(cliDir)) {
15
+ console.error(`${pc.red("✗")} ${app}-cli not found.`);
16
+ process.exit(1);
17
+ }
18
+
19
+ // TODO: Package resources + config, publish to npm + api2cli.dev
20
+ console.log(`Publishing ${pc.bold(`${app}-cli`)} as ${pc.cyan(`${opts.scope}/${app}`)}...`);
21
+ console.log(`\n${pc.yellow("🚧")} Publishing not yet implemented.`);
22
+ });
@@ -0,0 +1,35 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, rmSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { getCliDir, getTokenFile, getDistDir } from "../lib/config.js";
5
+ import { removeFromPath } from "../lib/shell.js";
6
+
7
+ export const removeCommand = new Command("remove")
8
+ .description("Remove a CLI entirely")
9
+ .argument("<app>", "CLI to remove")
10
+ .option("--keep-token", "Keep the auth token")
11
+ .addHelpText("after", "\nExamples:\n api2cli remove typefully\n api2cli remove typefully --keep-token")
12
+ .action((app: string, opts) => {
13
+ const cliDir = getCliDir(app);
14
+
15
+ if (!existsSync(cliDir)) {
16
+ console.error(`${pc.red("✗")} ${app}-cli not found.`);
17
+ process.exit(1);
18
+ }
19
+
20
+ // Remove from PATH
21
+ removeFromPath(app, getDistDir(app));
22
+
23
+ // Remove directory
24
+ rmSync(cliDir, { recursive: true, force: true });
25
+ console.log(`${pc.green("✓")} Removed ${pc.bold(`${app}-cli`)}`);
26
+
27
+ // Remove token unless --keep-token
28
+ if (!opts.keepToken) {
29
+ const tokenFile = getTokenFile(app);
30
+ if (existsSync(tokenFile)) {
31
+ rmSync(tokenFile);
32
+ console.log(`${pc.green("✓")} Removed token`);
33
+ }
34
+ }
35
+ });
@@ -0,0 +1,35 @@
1
+ import { Command } from "commander";
2
+ import { existsSync, readdirSync, readFileSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { TOKENS_DIR } from "../lib/config.js";
5
+ import { join } from "path";
6
+
7
+ export const tokensCommand = new Command("tokens")
8
+ .description("List all configured API tokens")
9
+ .option("--show", "Show full unmasked tokens")
10
+ .addHelpText("after", "\nExamples:\n api2cli tokens\n api2cli tokens --show")
11
+ .action((opts) => {
12
+ if (!existsSync(TOKENS_DIR)) {
13
+ console.log("No tokens configured yet.");
14
+ return;
15
+ }
16
+
17
+ const files = readdirSync(TOKENS_DIR).filter((f) => f.endsWith(".txt"));
18
+ if (files.length === 0) {
19
+ console.log("No tokens configured yet.");
20
+ return;
21
+ }
22
+
23
+ console.log(`\n${pc.bold("Configured tokens:")}\n`);
24
+ for (const f of files) {
25
+ const name = f.replace(".txt", "");
26
+ const token = readFileSync(join(TOKENS_DIR, f), "utf-8").trim();
27
+ const display = opts.show
28
+ ? token
29
+ : token.length > 8
30
+ ? `${token.slice(0, 4)}${pc.dim("...")}${token.slice(-4)}`
31
+ : pc.dim("****");
32
+ console.log(` ${pc.bold(name.padEnd(25))} ${display}`);
33
+ }
34
+ console.log();
35
+ });
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ import { getDistDir } from "../lib/config.js";
3
+ import { removeFromPath } from "../lib/shell.js";
4
+
5
+ export const unlinkCommand = new Command("unlink")
6
+ .description("Remove a CLI from your PATH")
7
+ .argument("<app>", "CLI to unlink")
8
+ .addHelpText("after", "\nExample:\n api2cli unlink typefully")
9
+ .action((app: string) => {
10
+ removeFromPath(app, getDistDir(app));
11
+ });
@@ -0,0 +1,26 @@
1
+ import { Command } from "commander";
2
+ import { existsSync } from "fs";
3
+ import pc from "picocolors";
4
+ import { getCliDir } from "../lib/config.js";
5
+
6
+ export const updateCommand = new Command("update")
7
+ .description("Re-sync a CLI when the upstream API changes")
8
+ .argument("<app>", "CLI to update")
9
+ .option("--docs <url>", "Updated API documentation URL")
10
+ .option("--openapi <url>", "Updated OpenAPI spec URL")
11
+ .addHelpText("after", "\nExample:\n api2cli update typefully --docs https://docs.typefully.com")
12
+ .action(async (app: string) => {
13
+ const cliDir = getCliDir(app);
14
+
15
+ if (!existsSync(cliDir)) {
16
+ console.error(`${pc.red("✗")} ${app}-cli not found. Run: ${pc.cyan(`api2cli create ${app}`)}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ // TODO: Agent-driven update flow
21
+ // Re-read API docs -> diff endpoints -> add/update resources -> rebuild
22
+ console.log(`${pc.yellow("🚧")} Update is agent-driven.`);
23
+ console.log(`\nUse your AI agent to update resources in:`);
24
+ console.log(` ${pc.dim(`${cliDir}/src/resources/`)}`);
25
+ console.log(`\nThen rebuild: ${pc.cyan(`api2cli bundle ${app}`)}`);
26
+ });
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import { createCommand } from "./commands/create.js";
4
+ import { installCommand } from "./commands/install.js";
5
+ import { listCommand } from "./commands/list.js";
6
+ import { bundleCommand } from "./commands/bundle.js";
7
+ import { linkCommand } from "./commands/link.js";
8
+ import { unlinkCommand } from "./commands/unlink.js";
9
+ import { tokensCommand } from "./commands/tokens.js";
10
+ import { removeCommand } from "./commands/remove.js";
11
+ import { doctorCommand } from "./commands/doctor.js";
12
+ import { updateCommand } from "./commands/update.js";
13
+ import { publishCommand } from "./commands/publish.js";
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name("api2cli")
19
+ .description("Turn any REST API into a standardized, agent-ready CLI")
20
+ .version("0.1.0");
21
+
22
+ // Core
23
+ program.addCommand(createCommand);
24
+ program.addCommand(bundleCommand);
25
+ program.addCommand(linkCommand);
26
+ program.addCommand(unlinkCommand);
27
+ program.addCommand(listCommand);
28
+
29
+ // Auth
30
+ program.addCommand(tokensCommand);
31
+
32
+ // Lifecycle
33
+ program.addCommand(removeCommand);
34
+ program.addCommand(doctorCommand);
35
+ program.addCommand(updateCommand);
36
+
37
+ // Registry
38
+ program.addCommand(installCommand);
39
+ program.addCommand(publishCommand);
40
+
41
+ program.parse();
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync, appendFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ const MARKER_START = "# >>> api2cli >>>";
6
+ const MARKER_END = "# <<< api2cli <<<";
7
+
8
+ function getShellRc(): string {
9
+ const zshrc = join(homedir(), ".zshrc");
10
+ const bashrc = join(homedir(), ".bashrc");
11
+ if (existsSync(zshrc)) return zshrc;
12
+ return bashrc;
13
+ }
14
+
15
+ export function addToPath(appName: string, binDir: string): void {
16
+ const rcFile = getShellRc();
17
+ const content = existsSync(rcFile) ? readFileSync(rcFile, "utf-8") : "";
18
+
19
+ const exportLine = `export PATH="${binDir}:$PATH"`;
20
+
21
+ // Check if already linked
22
+ if (content.includes(exportLine)) {
23
+ console.log(`${appName} already in PATH`);
24
+ return;
25
+ }
26
+
27
+ // Find or create api2cli block
28
+ if (content.includes(MARKER_START)) {
29
+ // Insert before the end marker
30
+ const updated = content.replace(
31
+ MARKER_END,
32
+ `${exportLine}\n${MARKER_END}`
33
+ );
34
+ const { writeFileSync } = require("fs");
35
+ writeFileSync(rcFile, updated);
36
+ } else {
37
+ appendFileSync(rcFile, `\n${MARKER_START}\n${exportLine}\n${MARKER_END}\n`);
38
+ }
39
+
40
+ console.log(`Added ${appName} to PATH in ${rcFile}`);
41
+ console.log(`Run: source ${rcFile}`);
42
+ }
43
+
44
+ export function removeFromPath(appName: string, binDir: string): void {
45
+ const rcFile = getShellRc();
46
+ if (!existsSync(rcFile)) return;
47
+
48
+ const content = readFileSync(rcFile, "utf-8");
49
+ const exportLine = `export PATH="${binDir}:$PATH"`;
50
+ const updated = content.replace(`${exportLine}\n`, "");
51
+
52
+ const { writeFileSync } = require("fs");
53
+ writeFileSync(rcFile, updated);
54
+ console.log(`Removed ${appName} from PATH`);
55
+ }
@@ -0,0 +1,35 @@
1
+ import { homedir } from "os";
2
+ import { join, resolve } from "path";
3
+
4
+ /** Root directory for all generated CLIs */
5
+ export const CLI_ROOT = join(homedir(), ".cli");
6
+
7
+ /** Centralized token storage directory */
8
+ export const TOKENS_DIR = join(homedir(), ".config", "tokens");
9
+
10
+ /** Template directory (relative to this package in the monorepo) */
11
+ export const TEMPLATE_DIR = resolve(import.meta.dir, "..", "..", "..", "template");
12
+
13
+ /** Placeholders used in the template that get replaced during create */
14
+ export const PLACEHOLDERS = [
15
+ "{{APP_NAME}}",
16
+ "{{APP_CLI}}",
17
+ "{{BASE_URL}}",
18
+ "{{AUTH_TYPE}}",
19
+ "{{AUTH_HEADER}}",
20
+ ] as const;
21
+
22
+ /** Get the installation directory for a CLI */
23
+ export function getCliDir(app: string): string {
24
+ return join(CLI_ROOT, `${app}-cli`);
25
+ }
26
+
27
+ /** Get the token file path for a CLI */
28
+ export function getTokenFile(app: string): string {
29
+ return join(TOKENS_DIR, `${app}-cli.txt`);
30
+ }
31
+
32
+ /** Get the dist directory for a CLI */
33
+ export function getDistDir(app: string): string {
34
+ return join(getCliDir(app), "dist");
35
+ }
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync, writeFileSync, appendFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import pc from "picocolors";
5
+
6
+ const MARKER_START = "# >>> api2cli >>>";
7
+ const MARKER_END = "# <<< api2cli <<<";
8
+
9
+ /** Detect the user's shell rc file */
10
+ function getShellRc(): string {
11
+ const shell = process.env.SHELL ?? "";
12
+ if (shell.includes("zsh")) return join(homedir(), ".zshrc");
13
+ if (shell.includes("fish")) return join(homedir(), ".config", "fish", "config.fish");
14
+ // Check if .zshrc exists even if SHELL isn't set
15
+ const zshrc = join(homedir(), ".zshrc");
16
+ if (existsSync(zshrc)) return zshrc;
17
+ return join(homedir(), ".bashrc");
18
+ }
19
+
20
+ /** Add a CLI's dist directory to PATH via the shell rc file. Idempotent. */
21
+ export function addToPath(app: string, binDir: string): void {
22
+ const rcFile = getShellRc();
23
+ const content = existsSync(rcFile) ? readFileSync(rcFile, "utf-8") : "";
24
+ const exportLine = `export PATH="${binDir}:$PATH"`;
25
+
26
+ if (content.includes(exportLine)) {
27
+ console.log(`${pc.dim(app)} already in PATH`);
28
+ return;
29
+ }
30
+
31
+ if (content.includes(MARKER_START)) {
32
+ const updated = content.replace(MARKER_END, `${exportLine}\n${MARKER_END}`);
33
+ writeFileSync(rcFile, updated);
34
+ } else {
35
+ appendFileSync(rcFile, `\n${MARKER_START}\n${exportLine}\n${MARKER_END}\n`);
36
+ }
37
+
38
+ console.log(`${pc.green("+")} Added ${pc.bold(app)} to PATH in ${pc.dim(rcFile)}`);
39
+ console.log(` Run: ${pc.cyan(`source ${rcFile}`)}`);
40
+ }
41
+
42
+ /** Remove a CLI from PATH in the shell rc file */
43
+ export function removeFromPath(app: string, binDir: string): void {
44
+ const rcFile = getShellRc();
45
+ if (!existsSync(rcFile)) return;
46
+
47
+ const content = readFileSync(rcFile, "utf-8");
48
+ const exportLine = `export PATH="${binDir}:$PATH"\n`;
49
+
50
+ if (!content.includes(exportLine)) {
51
+ console.log(`${pc.dim(app)} not in PATH`);
52
+ return;
53
+ }
54
+
55
+ writeFileSync(rcFile, content.replace(exportLine, ""));
56
+ console.log(`${pc.red("-")} Removed ${pc.bold(app)} from PATH`);
57
+ }