156 lines
5.9 KiB
JavaScript
156 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// career-ops scaffolder — one-command install.
|
|
// Clones the repo at the latest release tag and installs dependencies.
|
|
// It deliberately does NOT create cv.md / config/profile.yml / portals.yml:
|
|
// the agent runs a conversational onboarding on first launch (see AGENTS.md
|
|
// "First Run — Onboarding"), which is triggered precisely by those files
|
|
// being absent. Pre-creating them from the examples would suppress that
|
|
// onboarding and leave the user with placeholder data.
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, readdirSync } from "node:fs";
|
|
import { join, delimiter } from "node:path";
|
|
import { ensureSkillEntrypoints } from "./skill-entrypoints.mjs";
|
|
|
|
const REPO = "https://github.com/santifer/career-ops.git";
|
|
const LATEST_RELEASE = "https://api.github.com/repos/santifer/career-ops/releases/latest";
|
|
const NPM = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
|
|
// career-ops is AI-agnostic: every one of these CLIs reads AGENTS.md and works
|
|
// out of the box. We only detect them to tailor the final message — we never
|
|
// install, configure, or remove anything per-CLI.
|
|
const SUPPORTED_CLIS = [
|
|
{ name: "Claude Code", cmd: "claude" },
|
|
{ name: "Gemini CLI", cmd: "gemini" },
|
|
{ name: "Codex", cmd: "codex" },
|
|
{ name: "Qwen Code", cmd: "qwen" },
|
|
{ name: "OpenCode", cmd: "opencode" },
|
|
{ name: "GitHub Copilot CLI", cmd: "copilot" },
|
|
{ name: "Antigravity CLI", cmd: "agy" },
|
|
{ name: "Grok Build CLI", cmd: "grok" },
|
|
];
|
|
|
|
const USAGE = `career-ops — set up an AI job search workspace.
|
|
|
|
Usage:
|
|
npx career-ops init [folder] Create a new workspace (default: ./career-ops)
|
|
|
|
After setup, open your AI coding tool inside the folder and paste a job offer.
|
|
Docs: https://github.com/santifer/career-ops`;
|
|
|
|
function die(msg) {
|
|
console.error(`\n✗ ${msg}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function has(cmd, arg = "--version") {
|
|
try {
|
|
execFileSync(cmd, [arg], { stdio: "ignore" });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Is `cmd` resolvable on PATH? Manual lookup so it works cross-platform
|
|
// without spawning which/where (and without any dependency).
|
|
function onPath(cmd) {
|
|
const exts = process.platform === "win32" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
for (const dir of (process.env.PATH || "").split(delimiter)) {
|
|
if (!dir) continue;
|
|
for (const ext of exts) {
|
|
try {
|
|
if (existsSync(join(dir, cmd + ext))) return true;
|
|
} catch {}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function detectClis() {
|
|
return SUPPORTED_CLIS.filter((c) => onPath(c.cmd));
|
|
}
|
|
|
|
async function latestTag() {
|
|
try {
|
|
const res = await fetch(LATEST_RELEASE, {
|
|
headers: { "User-Agent": "career-ops-cli", Accept: "application/vnd.github+json" },
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
return data.tag_name || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const [cmd, dirArg] = process.argv.slice(2);
|
|
|
|
if (!cmd || cmd === "-h" || cmd === "--help") {
|
|
console.log(USAGE);
|
|
process.exit(cmd ? 0 : 1);
|
|
}
|
|
if (cmd !== "init") die(`Unknown command "${cmd}".\n${USAGE}`);
|
|
|
|
const target = dirArg || "career-ops";
|
|
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
die(`Target folder "${target}" already exists and is not empty. Pick another name.`);
|
|
}
|
|
if (!has("git")) die("git is required but was not found on PATH. Install git and try again.");
|
|
|
|
// Pretty path for messages: "./career-ops" for relative, as-is for absolute.
|
|
const isAbsolute = target.startsWith("/") || /^[A-Za-z]:/.test(target);
|
|
const display = isAbsolute ? target : `./${target}`;
|
|
|
|
// 1. Clone at the latest stable release (fall back to the default branch).
|
|
const tag = await latestTag();
|
|
console.log(`\n→ Cloning career-ops${tag ? ` @ ${tag}` : ""} into ${display} ...`);
|
|
const cloneArgs = ["clone", "--depth=1"];
|
|
if (tag) cloneArgs.push("--branch", tag);
|
|
cloneArgs.push(REPO, target);
|
|
try {
|
|
execFileSync("git", cloneArgs, { stdio: "inherit" });
|
|
} catch {
|
|
die("git clone failed. Check your network connection and try again.");
|
|
}
|
|
|
|
// 2. Install dependencies.
|
|
console.log("\n→ Installing dependencies (npm install) ...");
|
|
try {
|
|
execFileSync(NPM, ["install"], { cwd: target, stdio: "inherit" });
|
|
} catch {
|
|
console.warn('\n! npm install failed — you can re-run it manually later with "npm install".');
|
|
}
|
|
|
|
// 2b. Bootstrap CLI skill entrypoints (covers CLIs added after the cloned release).
|
|
const bootstrapped = ensureSkillEntrypoints(target);
|
|
if (bootstrapped.length > 0) {
|
|
console.log(`\n→ Bootstrapped ${bootstrapped.length} CLI skill entrypoint(s) for this workspace`);
|
|
}
|
|
|
|
// 3. Next steps. We do NOT scaffold cv.md / profile.yml / portals.yml here:
|
|
// their absence is what triggers the agent's conversational onboarding on
|
|
// first launch, which sets them up far better than copying placeholders.
|
|
console.log(`\n✓ career-ops is ready in ${display}\n`);
|
|
console.log("Next steps:");
|
|
console.log(` 1. cd ${target}`);
|
|
|
|
// Tailor the "open your AI tool" line to whatever CLI is installed.
|
|
const detected = detectClis();
|
|
if (detected.length === 1) {
|
|
console.log(` 2. Open your workspace: ${detected[0].cmd} (${detected[0].name} detected)`);
|
|
} else if (detected.length > 1) {
|
|
console.log(` 2. Open your workspace with any of: ${detected.map((c) => c.cmd).join(", ")} (detected)`);
|
|
} else {
|
|
console.log(` 2. Open your AI coding tool here, e.g.: ${SUPPORTED_CLIS.map((c) => c.cmd).join(", ")}`);
|
|
}
|
|
|
|
console.log("\nOn first launch it walks you through setup — your CV, profile and target");
|
|
console.log("roles — just by chatting. Nothing to configure by hand.");
|
|
console.log("\ncareer-ops is AI-agnostic — Claude Code, Codex, Qwen, OpenCode, Copilot, Antigravity and Grok all work.");
|
|
console.log("\nOptional (for PDF generation):");
|
|
console.log(" npx playwright install chromium\n");
|
|
}
|
|
|
|
main().catch((err) => die(err?.message || String(err)));
|