init
Release Please / release-please (push) Failing after 4m2s
Web CI / web typecheck + build (push) Successful in 3m39s
CodeQL Analysis / Analyze (javascript-typescript) (push) Failing after 1h1m49s
CodeQL Analysis / Analyze (go) (push) Failing after 1h1m49s

This commit is contained in:
redlineo committed 2026-07-13 10:14:00 +03:00
commit d295c7f139
1527 files changed
+270726

No files matched your search

+33
View File
@@ -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)
+155
View File
@@ -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)));
+116
View File
@@ -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;
}
+37
View File
@@ -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": {}
}