init
This commit is contained in:
commit
d295c7f139
1527 files changed
+270726
No files matched your search
@@ -0,0 +1,33 @@
|
||||
# career-ops
|
||||
|
||||
One-command installer for [**career-ops**](https://github.com/santifer/career-ops) — the AI-powered job search pipeline built on Claude Code.
|
||||
|
||||
```bash
|
||||
npx @santifer/career-ops init
|
||||
```
|
||||
|
||||
This sets up a ready-to-use workspace:
|
||||
|
||||
1. Clones career-ops at the latest stable release
|
||||
2. Installs dependencies
|
||||
|
||||
Then open your AI coding tool in the folder. **On first launch the agent walks you through setup — your CV, profile and target roles — just by chatting.** Nothing to configure by hand. career-ops is AI-agnostic — Claude Code, Gemini, Codex, Qwen, OpenCode, GitHub Copilot CLI, Antigravity CLI, and Grok Build CLI all work.
|
||||
|
||||
The installer bootstraps CLI skill entrypoints after clone, so new CLIs (e.g. Grok) work even when `npx` pulled an older release tag.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npx @santifer/career-ops init [folder] # default folder: ./career-ops
|
||||
```
|
||||
|
||||
Prefer the manual route? `git clone` still works exactly as before — see the [setup guide](https://github.com/santifer/career-ops/blob/main/docs/SETUP.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- git
|
||||
|
||||
## License
|
||||
|
||||
MIT © [Santiago Fernández de Valderrama](https://santifer.io)
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/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)));
|
||||
@@ -0,0 +1,116 @@
|
||||
// Shared CLI skill entrypoint bootstrap — used by npx init and update-system.
|
||||
// Ensures every supported CLI gets .*/skills/career-ops/SKILL.md even when the
|
||||
// cloned release predates a CLI (e.g. Grok on v1.13.0). Materializes pointer
|
||||
// files to canonical content on filesystems without symlink support.
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
|
||||
export const CANONICAL_SKILL_PATH = '.agents/skills/career-ops/SKILL.md';
|
||||
|
||||
export const SKILL_ENTRYPOINTS = [
|
||||
{
|
||||
path: '.claude/skills/career-ops/SKILL.md',
|
||||
pointer: '../../../.agents/skills/career-ops/SKILL.md',
|
||||
},
|
||||
{
|
||||
path: '.opencode/skills/career-ops/SKILL.md',
|
||||
pointer: '../../../.agents/skills/career-ops/SKILL.md',
|
||||
},
|
||||
{
|
||||
path: '.qwen/skills/career-ops/SKILL.md',
|
||||
pointer: '../../../.agents/skills/career-ops/SKILL.md',
|
||||
},
|
||||
{
|
||||
path: '.antigravitycli/skills/career-ops/SKILL.md',
|
||||
pointer: '../../../.agents/skills/career-ops/SKILL.md',
|
||||
},
|
||||
{
|
||||
path: '.grok/skills/career-ops/SKILL.md',
|
||||
pointer: '../../../.agents/skills/career-ops/SKILL.md',
|
||||
},
|
||||
];
|
||||
|
||||
function repoPath(root, path) {
|
||||
return join(root, ...path.split('/'));
|
||||
}
|
||||
|
||||
function readCanonical(root) {
|
||||
const canonicalPath = repoPath(root, CANONICAL_SKILL_PATH);
|
||||
if (!existsSync(canonicalPath)) return null;
|
||||
try {
|
||||
return readFileSync(canonicalPath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function materializeSkillEntrypoints(root) {
|
||||
const canonicalContent = readCanonical(root);
|
||||
if (canonicalContent === null) return [];
|
||||
|
||||
const materialized = [];
|
||||
for (const entry of SKILL_ENTRYPOINTS) {
|
||||
const entryPath = repoPath(root, entry.path);
|
||||
if (!existsSync(entryPath)) continue;
|
||||
|
||||
let stat = null;
|
||||
try {
|
||||
stat = lstatSync(entryPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (stat.isSymbolicLink()) continue;
|
||||
if (!stat.isFile()) continue;
|
||||
|
||||
try {
|
||||
const content = readFileSync(entryPath, 'utf-8').trim();
|
||||
if (content !== entry.pointer) continue;
|
||||
writeFileSync(entryPath, canonicalContent);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
materialized.push(entry.path);
|
||||
}
|
||||
|
||||
return materialized;
|
||||
}
|
||||
|
||||
export function ensureSkillEntrypoints(root) {
|
||||
const canonicalContent = readCanonical(root);
|
||||
if (canonicalContent === null) return [];
|
||||
|
||||
const touched = [];
|
||||
for (const entry of SKILL_ENTRYPOINTS) {
|
||||
const entryPath = repoPath(root, entry.path);
|
||||
|
||||
if (!existsSync(entryPath)) {
|
||||
try {
|
||||
mkdirSync(dirname(entryPath), { recursive: true });
|
||||
writeFileSync(entryPath, entry.pointer);
|
||||
touched.push(entry.path);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let stat = null;
|
||||
try {
|
||||
stat = lstatSync(entryPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (stat.isSymbolicLink()) continue;
|
||||
if (!stat.isFile()) continue;
|
||||
|
||||
try {
|
||||
const content = readFileSync(entryPath, 'utf-8').trim();
|
||||
if (content !== entry.pointer) continue;
|
||||
writeFileSync(entryPath, canonicalContent);
|
||||
if (!touched.includes(entry.path)) touched.push(entry.path);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return touched;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@santifer/career-ops",
|
||||
"version": "1.17.0",
|
||||
"description": "One-command installer for career-ops — the AI-powered job search pipeline built on Claude Code.",
|
||||
"bin": {
|
||||
"career-ops": "bin/cli.mjs"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"bin/cli.mjs",
|
||||
"bin/skill-entrypoints.mjs",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"keywords": [
|
||||
"career-ops",
|
||||
"claude-code",
|
||||
"job-search",
|
||||
"scaffold",
|
||||
"create",
|
||||
"cli"
|
||||
],
|
||||
"author": "Santiago Fernández de Valderrama <hi@santifer.io> (https://santifer.io)",
|
||||
"homepage": "https://github.com/santifer/career-ops#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/santifer/career-ops.git",
|
||||
"directory": "scaffolder"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/santifer/career-ops/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {}
|
||||
}
|
||||
Reference in new issue
Block a user