@iamsaroj/replicax 0.0.1 → 0.0.2

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.
Files changed (4) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +337 -190
  3. package/dist/index.js +805 -47
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -943,9 +943,763 @@ async function maybeWriteIgnoreFile(root) {
943
943
  }
944
944
  }
945
945
 
946
- // src/commands/create.ts
946
+ // src/commands/init-skill.ts
947
+ import path7 from "path";
948
+ import fs6 from "fs-extra";
949
+ import ora2 from "ora";
950
+
951
+ // src/config/ai-targets.ts
952
+ var SKILL_TARGETS = [
953
+ {
954
+ id: "claude",
955
+ label: "Claude Code",
956
+ entryPath: (slug2) => `.claude/skills/${slug2}/SKILL.md`,
957
+ note: "Claude Code loads skills from .claude/skills/<name>/SKILL.md"
958
+ },
959
+ {
960
+ id: "codex",
961
+ label: "OpenAI Codex CLI",
962
+ entryPath: (slug2) => `.codex/skills/${slug2}/SKILL.md`,
963
+ note: "Codex CLI loads project skills from .codex/skills/<name>/SKILL.md"
964
+ },
965
+ {
966
+ id: "antigravity",
967
+ label: "Google Antigravity",
968
+ entryPath: (slug2) => `.agents/skills/${slug2}.md`,
969
+ note: "Antigravity discovers skills under .agents/skills/"
970
+ }
971
+ ];
972
+ var SKILL_TARGET_BY_ID = new Map(SKILL_TARGETS.map((t) => [t.id, t]));
973
+ var SKILL_TARGET_IDS = SKILL_TARGETS.map((t) => t.id);
974
+
975
+ // src/core/skill-generator.ts
976
+ var FRAMEWORK_LABELS = {
977
+ next: "Next.js",
978
+ nuxt: "Nuxt",
979
+ remix: "Remix",
980
+ astro: "Astro",
981
+ angular: "Angular",
982
+ sveltekit: "SvelteKit",
983
+ nestjs: "NestJS",
984
+ expo: "Expo",
985
+ "react-native": "React Native",
986
+ vue: "Vue",
987
+ svelte: "Svelte",
988
+ solid: "SolidJS",
989
+ react: "React",
990
+ fastify: "Fastify",
991
+ koa: "Koa",
992
+ express: "Express",
993
+ node: "Node",
994
+ unknown: "unknown"
995
+ };
996
+ var PRIMARY_SCRIPTS = [
997
+ "dev",
998
+ "start",
999
+ "build",
1000
+ "test",
1001
+ "test:watch",
1002
+ "lint",
1003
+ "format",
1004
+ "format:check",
1005
+ "typecheck"
1006
+ ];
1007
+ function slugify(input) {
1008
+ const slug2 = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1009
+ return slug2 || "project";
1010
+ }
1011
+ function installCommand(pm) {
1012
+ switch (pm) {
1013
+ case "yarn":
1014
+ return "yarn";
1015
+ case "pnpm":
1016
+ return "pnpm install";
1017
+ case "bun":
1018
+ return "bun install";
1019
+ default:
1020
+ return "npm install";
1021
+ }
1022
+ }
1023
+ function runCommand(pm, script) {
1024
+ switch (pm) {
1025
+ case "yarn":
1026
+ return `yarn ${script}`;
1027
+ case "pnpm":
1028
+ return `pnpm ${script}`;
1029
+ case "bun":
1030
+ return `bun run ${script}`;
1031
+ default:
1032
+ return `npm run ${script}`;
1033
+ }
1034
+ }
1035
+ function orderedScripts(scripts) {
1036
+ const names = Object.keys(scripts);
1037
+ const primary = PRIMARY_SCRIPTS.filter((s) => names.includes(s));
1038
+ const rest = names.filter((s) => !PRIMARY_SCRIPTS.includes(s)).sort();
1039
+ return [...primary, ...rest];
1040
+ }
1041
+ function toolingByCategoryLabel(tooling) {
1042
+ const groups = /* @__PURE__ */ new Map();
1043
+ for (const file of tooling.files) {
1044
+ const label = CATEGORY_BY_ID.get(file.category)?.label ?? file.category;
1045
+ const list = groups.get(label) ?? [];
1046
+ list.push(file.path);
1047
+ groups.set(label, list);
1048
+ }
1049
+ return [...groups.entries()].map(([label, paths]) => [label, paths.sort()]).sort((a, b) => a[0].localeCompare(b[0]));
1050
+ }
1051
+ function topLevelDirectories(structure) {
1052
+ return structure.directories.filter((d) => !d.includes("/")).sort();
1053
+ }
1054
+ function yamlString(value) {
1055
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1056
+ }
1057
+ function testingTool(tooling, pkg) {
1058
+ const haystack = [
1059
+ ...tooling.files.map((f) => f.path),
1060
+ ...Object.keys(pkg?.devDependencies ?? {}),
1061
+ ...Object.keys(pkg?.dependencies ?? {})
1062
+ ];
1063
+ const has = (needle) => haystack.some((s) => s.includes(needle));
1064
+ if (has("vitest")) return "Vitest";
1065
+ if (has("jest")) return "Jest";
1066
+ if (has("playwright")) return "Playwright";
1067
+ if (has("cypress")) return "Cypress";
1068
+ return void 0;
1069
+ }
1070
+ function hasCategory(tooling, categoryId) {
1071
+ return tooling.files.some((f) => f.category === categoryId);
1072
+ }
1073
+ function conventionLines(args, scripts) {
1074
+ const { metadata, tooling } = args;
1075
+ const pm = metadata.packageManager;
1076
+ const has = (s) => s in scripts;
1077
+ const lines = [];
1078
+ if (metadata.language === "typescript") {
1079
+ lines.push(
1080
+ has("typecheck") ? `Written in TypeScript \u2014 run \`${runCommand(pm, "typecheck")}\` before committing.` : "Written in TypeScript \u2014 prefer typed APIs and keep the type checker clean."
1081
+ );
1082
+ }
1083
+ if (hasCategory(tooling, "prettier")) {
1084
+ lines.push(
1085
+ has("format") ? `Formatting is handled by Prettier \u2014 run \`${runCommand(pm, "format")}\`.` : "Formatting is handled by Prettier \u2014 match the configured style."
1086
+ );
1087
+ }
1088
+ if (hasCategory(tooling, "eslint")) {
1089
+ lines.push(
1090
+ has("lint") ? `Linting via ESLint \u2014 run \`${runCommand(pm, "lint")}\`.` : "Linting via ESLint \u2014 keep the rules satisfied."
1091
+ );
1092
+ }
1093
+ const tester = testingTool(tooling, args.pkg);
1094
+ if (tester) {
1095
+ lines.push(
1096
+ has("test") ? `Tests use ${tester} \u2014 run \`${runCommand(pm, "test")}\`.` : `Tests use ${tester}.`
1097
+ );
1098
+ }
1099
+ if (hasCategory(tooling, "docker")) {
1100
+ lines.push("Containerized with Docker (see the Dockerfile / compose files).");
1101
+ }
1102
+ if (hasCategory(tooling, "cicd")) {
1103
+ lines.push("CI is configured \u2014 keep the pipeline green before merging.");
1104
+ }
1105
+ if (hasCategory(tooling, "husky")) {
1106
+ lines.push("Git hooks (Husky) run on commit \u2014 do not bypass them.");
1107
+ }
1108
+ lines.push("Match the existing code style and the folder layout shown above.");
1109
+ return lines;
1110
+ }
1111
+ function buildSkill(args) {
1112
+ const { name, metadata, tooling, structure, pkg } = args;
1113
+ const slug2 = slugify(name);
1114
+ const pm = metadata.packageManager;
1115
+ const framework = FRAMEWORK_LABELS[metadata.framework] ?? metadata.framework;
1116
+ const language = metadata.language === "typescript" ? "TypeScript" : "JavaScript";
1117
+ const scripts = pkg?.scripts ?? {};
1118
+ const description = `${name} project: ${framework}/${language} setup, build/test commands, and tooling conventions. Use this skill when working in or scaffolding this codebase.`;
1119
+ const lines = [];
1120
+ lines.push("---");
1121
+ lines.push(`name: ${slug2}`);
1122
+ lines.push(`description: ${yamlString(description)}`);
1123
+ lines.push("---");
1124
+ lines.push("");
1125
+ lines.push(`# ${name}`);
1126
+ lines.push("");
1127
+ lines.push(`Setup, commands, and conventions for \`${name}\`, generated by ReplicaX.`);
1128
+ lines.push("");
1129
+ lines.push("## Tech stack");
1130
+ lines.push("");
1131
+ lines.push(`- **Language:** ${language}`);
1132
+ lines.push(`- **Framework:** ${framework}`);
1133
+ lines.push(`- **Package manager:** ${pm}`);
1134
+ lines.push(`- **Node version:** ${metadata.nodeVersion}`);
1135
+ lines.push("");
1136
+ lines.push("## Setup");
1137
+ lines.push("");
1138
+ lines.push("Install dependencies:");
1139
+ lines.push("");
1140
+ lines.push("```bash");
1141
+ lines.push(installCommand(pm));
1142
+ lines.push("```");
1143
+ lines.push("");
1144
+ const scriptNames = orderedScripts(scripts);
1145
+ if (scriptNames.length > 0) {
1146
+ lines.push("## Commands");
1147
+ lines.push("");
1148
+ for (const script of scriptNames) {
1149
+ lines.push(`- **${script}** \u2014 \`${runCommand(pm, script)}\``);
1150
+ }
1151
+ lines.push("");
1152
+ }
1153
+ const groups = toolingByCategoryLabel(tooling);
1154
+ if (groups.length > 0) {
1155
+ lines.push("## Tooling");
1156
+ lines.push("");
1157
+ for (const [label, paths] of groups) {
1158
+ lines.push(`- **${label}:** ${paths.join(", ")}`);
1159
+ }
1160
+ lines.push("");
1161
+ }
1162
+ const dirs = topLevelDirectories(structure);
1163
+ if (dirs.length > 0) {
1164
+ lines.push("## Project structure");
1165
+ lines.push("");
1166
+ lines.push("Top-level directories:");
1167
+ lines.push("");
1168
+ for (const dir of dirs) {
1169
+ lines.push(`- \`${dir}/\``);
1170
+ }
1171
+ lines.push("");
1172
+ }
1173
+ lines.push("## Conventions");
1174
+ lines.push("");
1175
+ for (const line of conventionLines(args, scripts)) {
1176
+ lines.push(`- ${line}`);
1177
+ }
1178
+ lines.push("");
1179
+ return { slug: slug2, content: lines.join("\n") };
1180
+ }
1181
+
1182
+ // src/core/ai/cli.ts
1183
+ import { spawn } from "child_process";
1184
+ async function commandExists(bin) {
1185
+ const onWindows = process.platform === "win32";
1186
+ const locator = onWindows ? "where" : "command";
1187
+ const args = onWindows ? [bin] : ["-v", bin];
1188
+ return new Promise((resolve) => {
1189
+ const child = spawn(locator, args, {
1190
+ // `command -v` is a POSIX shell builtin; `where` is a real Windows exe.
1191
+ shell: !onWindows,
1192
+ stdio: "ignore",
1193
+ windowsHide: true
1194
+ });
1195
+ child.on("error", () => resolve(false));
1196
+ child.on("close", (code) => resolve(code === 0));
1197
+ });
1198
+ }
1199
+ async function runWithStdin(bin, args, input, timeoutMs = 12e4) {
1200
+ return new Promise((resolve, reject) => {
1201
+ const child = spawn(bin, args, { shell: true, windowsHide: true });
1202
+ let stdout = "";
1203
+ let stderr = "";
1204
+ const timer = setTimeout(() => {
1205
+ child.kill();
1206
+ reject(new Error(`${bin} timed out after ${Math.round(timeoutMs / 1e3)}s`));
1207
+ }, timeoutMs);
1208
+ child.stdout.on("data", (d) => stdout += d.toString());
1209
+ child.stderr.on("data", (d) => stderr += d.toString());
1210
+ child.on("error", (err) => {
1211
+ clearTimeout(timer);
1212
+ reject(err);
1213
+ });
1214
+ child.on("close", (code) => {
1215
+ clearTimeout(timer);
1216
+ if (code !== 0 && !stdout.trim()) {
1217
+ reject(new Error(`${bin} exited with code ${code}: ${stderr.trim() || "no output"}`));
1218
+ } else {
1219
+ resolve({ stdout, code });
1220
+ }
1221
+ });
1222
+ child.stdin.end(input);
1223
+ });
1224
+ }
1225
+
1226
+ // src/core/ai/providers.ts
1227
+ async function postJson(url, headers, body) {
1228
+ const res = await fetch(url, {
1229
+ method: "POST",
1230
+ headers: { "content-type": "application/json", ...headers },
1231
+ body: JSON.stringify(body)
1232
+ });
1233
+ if (!res.ok) {
1234
+ const text = await res.text().catch(() => "");
1235
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 300) || res.statusText}`);
1236
+ }
1237
+ return res.json();
1238
+ }
1239
+ async function callAnthropic(prompt, apiKey, model) {
1240
+ const data = await postJson(
1241
+ "https://api.anthropic.com/v1/messages",
1242
+ { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
1243
+ { model, max_tokens: 8e3, messages: [{ role: "user", content: prompt }] }
1244
+ );
1245
+ return (data.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
1246
+ }
1247
+ async function callOpenAI(prompt, apiKey, model) {
1248
+ const data = await postJson(
1249
+ "https://api.openai.com/v1/chat/completions",
1250
+ { authorization: `Bearer ${apiKey}` },
1251
+ { model, messages: [{ role: "user", content: prompt }] }
1252
+ );
1253
+ return data.choices?.[0]?.message?.content ?? "";
1254
+ }
1255
+ async function callGemini(prompt, apiKey, model) {
1256
+ const data = await postJson(
1257
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`,
1258
+ { "x-goog-api-key": apiKey },
1259
+ { contents: [{ parts: [{ text: prompt }] }] }
1260
+ );
1261
+ return (data.candidates?.[0]?.content?.parts ?? []).map((p) => p.text ?? "").join("");
1262
+ }
1263
+ var PROVIDERS = [
1264
+ {
1265
+ id: "claude",
1266
+ label: "Claude Code",
1267
+ cliBin: "claude",
1268
+ cliArgs: ["-p"],
1269
+ apiEnvVars: ["ANTHROPIC_API_KEY"],
1270
+ modelEnvVar: "REPLICAX_ANTHROPIC_MODEL",
1271
+ defaultModel: "claude-opus-4-8",
1272
+ callApi: callAnthropic
1273
+ },
1274
+ {
1275
+ id: "openai",
1276
+ label: "OpenAI Codex",
1277
+ cliBin: "codex",
1278
+ cliArgs: ["exec"],
1279
+ apiEnvVars: ["OPENAI_API_KEY"],
1280
+ modelEnvVar: "REPLICAX_OPENAI_MODEL",
1281
+ defaultModel: "gpt-5.5",
1282
+ callApi: callOpenAI
1283
+ },
1284
+ {
1285
+ id: "gemini",
1286
+ label: "Gemini",
1287
+ cliBin: "gemini",
1288
+ cliArgs: [],
1289
+ apiEnvVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
1290
+ modelEnvVar: "REPLICAX_GEMINI_MODEL",
1291
+ defaultModel: "gemini-3.5-flash",
1292
+ callApi: callGemini
1293
+ }
1294
+ ];
1295
+ var PROVIDER_IDS = PROVIDERS.map((p) => p.id);
1296
+ function firstEnv(vars) {
1297
+ for (const v of vars) {
1298
+ const val = process.env[v];
1299
+ if (val && val.trim()) return val.trim();
1300
+ }
1301
+ return void 0;
1302
+ }
1303
+ function cliInvoker(def) {
1304
+ return {
1305
+ id: def.id,
1306
+ via: `${def.label} CLI`,
1307
+ run: async (prompt) => (await runWithStdin(def.cliBin, def.cliArgs, prompt)).stdout
1308
+ };
1309
+ }
1310
+ function apiInvoker(def, apiKey, modelOverride) {
1311
+ const model = modelOverride ?? process.env[def.modelEnvVar] ?? def.defaultModel;
1312
+ return {
1313
+ id: def.id,
1314
+ via: `${def.label} API (${model})`,
1315
+ run: (prompt) => def.callApi(prompt, apiKey, model)
1316
+ };
1317
+ }
1318
+ async function resolveProvider(preference, modelOverride) {
1319
+ if (preference) {
1320
+ const def = PROVIDERS.find((p) => p.id === preference);
1321
+ if (!def) {
1322
+ throw new ReplicaxError(`Unknown provider "${preference}".`, [
1323
+ `Valid providers: ${PROVIDER_IDS.join(", ")}.`
1324
+ ]);
1325
+ }
1326
+ if (await commandExists(def.cliBin)) return cliInvoker(def);
1327
+ const key = firstEnv(def.apiEnvVars);
1328
+ if (key) return apiInvoker(def, key, modelOverride);
1329
+ throw new ReplicaxError(`Provider "${preference}" is not available.`, [
1330
+ `Install its CLI (\`${def.cliBin}\`) or set ${def.apiEnvVars[0]}.`
1331
+ ]);
1332
+ }
1333
+ for (const def of PROVIDERS) {
1334
+ if (await commandExists(def.cliBin)) return cliInvoker(def);
1335
+ const key = firstEnv(def.apiEnvVars);
1336
+ if (key) return apiInvoker(def, key, modelOverride);
1337
+ }
1338
+ return null;
1339
+ }
1340
+
1341
+ // src/core/ai/prompt.ts
1342
+ function buildSkillPrompt(args) {
1343
+ const scripts = Object.entries(args.scripts).map(([name, cmd]) => ` ${name}: ${cmd}`).join("\n");
1344
+ const tooling = args.toolingPaths.map((p) => ` ${p}`).join("\n");
1345
+ return `You are an expert developer-tooling assistant. Generate a high-quality "skill" for an AI coding assistant: a document (plus optional supporting files) that teaches the assistant how to work productively in a specific software project.
1346
+
1347
+ STRICT RULES:
1348
+ - Base everything ONLY on the PROJECT ANALYSIS below. Do not invent tools, frameworks, commands, or files that are not present in the analysis.
1349
+ - Output ONLY a single JSON object \u2014 no prose, no markdown code fences, no commentary before or after.
1350
+
1351
+ OUTPUT SHAPE (exactly this JSON structure):
1352
+ {"files":[{"path":"<relative path>","content":"<file contents>"}]}
1353
+
1354
+ REQUIREMENTS:
1355
+ - Include exactly one entry file whose path is "${args.entryFile}". It MUST start with YAML frontmatter containing "name: ${args.slug}" and a concise single-line "description", then clear markdown covering: tech stack, setup/install, common commands, tooling, project structure, and conventions.
1356
+ - You MAY add a few supporting files under "references/" (e.g. "references/commands.md") when genuinely useful. Keep the bundle small and focused.
1357
+ - All paths must be relative, use forward slashes, and must NOT contain ".." or be absolute.
1358
+ - This skill targets ${args.target.label} and will be installed at ${args.entryPath}.
1359
+
1360
+ PROJECT ANALYSIS (ground truth \u2014 refine and expand this, do not contradict it):
1361
+ ${args.analysis}
1362
+
1363
+ CAPTURED CONFIG FILES:
1364
+ ${tooling || " (none)"}
1365
+
1366
+ PACKAGE SCRIPTS:
1367
+ ${scripts || " (none)"}
1368
+ `;
1369
+ }
1370
+
1371
+ // src/core/ai/bundle.ts
1372
+ import { z as z2 } from "zod";
1373
+ var SkillFileSchema = z2.object({
1374
+ path: z2.string().min(1),
1375
+ content: z2.string()
1376
+ });
1377
+ var SkillBundleSchema = z2.object({
1378
+ files: z2.array(SkillFileSchema).min(1)
1379
+ });
1380
+ function extractJson(raw) {
1381
+ const start = raw.indexOf("{");
1382
+ const end = raw.lastIndexOf("}");
1383
+ if (start === -1 || end === -1 || end < start) return null;
1384
+ return raw.slice(start, end + 1);
1385
+ }
1386
+ function parseSkillBundle(raw) {
1387
+ const json = extractJson(raw);
1388
+ if (!json) return null;
1389
+ let parsed;
1390
+ try {
1391
+ parsed = JSON.parse(json);
1392
+ } catch {
1393
+ return null;
1394
+ }
1395
+ const result = SkillBundleSchema.safeParse(parsed);
1396
+ if (!result.success) return null;
1397
+ const files = [];
1398
+ for (const file of result.data.files) {
1399
+ const safe = safeJoinable(file.path);
1400
+ if (!safe) return null;
1401
+ files.push({ path: safe, content: file.content });
1402
+ }
1403
+ return files;
1404
+ }
1405
+
1406
+ // src/commands/init-skill.ts
1407
+ async function initSkillCommand(options) {
1408
+ if (options.verbose) setVerbose(true);
1409
+ const targetId = options.target?.toLowerCase();
1410
+ if (!targetId) {
1411
+ throw new ReplicaxError("Missing --target.", [
1412
+ `Choose an AI assistant: ${SKILL_TARGET_IDS.join(", ")}.`,
1413
+ "Example: replicax init-skill --target codex"
1414
+ ]);
1415
+ }
1416
+ const target = SKILL_TARGET_BY_ID.get(targetId);
1417
+ if (!target) {
1418
+ throw new ReplicaxError(`Unknown target "${options.target}".`, [
1419
+ `Valid targets: ${SKILL_TARGET_IDS.join(", ")}.`
1420
+ ]);
1421
+ }
1422
+ const root = process.cwd();
1423
+ const spinner = ora2({ text: "Scanning project\u2026", isEnabled: !options.verbose }).start();
1424
+ const scan = await scanProject(root);
1425
+ spinner.succeed(
1426
+ `Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
1427
+ );
1428
+ const name = options.name ?? scan.structure.root;
1429
+ const seed = buildSkill({
1430
+ name,
1431
+ metadata: scan.metadata,
1432
+ tooling: scan.tooling,
1433
+ structure: scan.structure,
1434
+ pkg: scan.pkg
1435
+ });
1436
+ reportSkippedSecrets(scan.skippedSecrets);
1437
+ const entryRel = target.entryPath(seed.slug);
1438
+ const entryFile = entryRel.split("/").pop();
1439
+ const bundleRoot = entryRel.slice(0, entryRel.length - entryFile.length).replace(/\/$/, "");
1440
+ logger.newline();
1441
+ logger.info(`${target.label} skill \u2192 ${entryRel}`);
1442
+ if (options.dryRun) {
1443
+ logger.newline();
1444
+ logger.out(seed.content);
1445
+ logger.newline();
1446
+ logger.info("Dry run \u2014 no files were written and no AI provider was contacted.");
1447
+ return;
1448
+ }
1449
+ let files = null;
1450
+ let via = "built-in template";
1451
+ if (options.ai !== false) {
1452
+ const provider = await resolveProvider(options.provider, options.model);
1453
+ if (provider) {
1454
+ logger.info(`Generating with ${provider.via} (sending project setup only)\u2026`);
1455
+ const aiSpinner = ora2({ text: "Authoring skill\u2026", isEnabled: !options.verbose }).start();
1456
+ try {
1457
+ const prompt = buildSkillPrompt({
1458
+ slug: seed.slug,
1459
+ entryFile,
1460
+ entryPath: entryRel,
1461
+ target,
1462
+ analysis: seed.content,
1463
+ toolingPaths: scan.tooling.files.map((f) => f.path),
1464
+ scripts: scan.pkg?.scripts ?? {}
1465
+ });
1466
+ const raw = await provider.run(prompt);
1467
+ const parsed = parseSkillBundle(raw);
1468
+ if (parsed && parsed.some((f) => (f.path.split("/").pop() ?? "") === entryFile)) {
1469
+ files = parsed;
1470
+ via = provider.via;
1471
+ aiSpinner.succeed(`Authored ${parsed.length} file(s) with ${provider.via}`);
1472
+ } else {
1473
+ aiSpinner.fail("AI output was not a usable skill bundle");
1474
+ logger.warn("Falling back to the built-in template.");
1475
+ }
1476
+ } catch (err) {
1477
+ aiSpinner.fail("AI generation failed");
1478
+ logger.warn(`${err.message}. Falling back to the built-in template.`);
1479
+ }
1480
+ } else {
1481
+ logger.info("No configured AI provider found \u2014 using the built-in template.");
1482
+ logger.hint(
1483
+ "For AI-authored skills, install the Claude/Codex/Gemini CLI or set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY."
1484
+ );
1485
+ }
1486
+ }
1487
+ if (!files) {
1488
+ files = [{ path: entryFile, content: seed.content }];
1489
+ }
1490
+ const planned = files.map((f) => {
1491
+ const rel = bundleRoot ? `${bundleRoot}/${f.path}` : f.path;
1492
+ const safe = safeJoinable(rel);
1493
+ if (!safe) {
1494
+ throw new ReplicaxError(`Refusing to write unsafe skill path: ${f.path}`);
1495
+ }
1496
+ return { rel: safe, abs: path7.join(root, ...safe.split("/")), content: f.content };
1497
+ });
1498
+ const conflicts = [];
1499
+ for (const file of planned) {
1500
+ if (await fs6.pathExists(file.abs)) conflicts.push(file.rel);
1501
+ }
1502
+ if (conflicts.length > 0 && !options.force) {
1503
+ throw new ReplicaxError(
1504
+ `${conflicts.length} skill file(s) already exist: ${conflicts.join(", ")}.`,
1505
+ ["Re-run with --force to overwrite them."]
1506
+ );
1507
+ }
1508
+ for (const file of planned) {
1509
+ await fs6.ensureDir(path7.dirname(file.abs));
1510
+ await fs6.writeFile(file.abs, file.content, "utf8");
1511
+ logger.detail(`wrote: ${file.rel}`);
1512
+ }
1513
+ logger.newline();
1514
+ logger.success(`Skill "${seed.slug}" written (${planned.length} file(s), via ${via})`);
1515
+ logger.hint(
1516
+ `Location: ${relPosix(root, path7.join(root, ...(bundleRoot || entryFile).split("/")))}`
1517
+ );
1518
+ logger.hint(target.note);
1519
+ }
1520
+
1521
+ // src/commands/extract.ts
1522
+ import path9 from "path";
1523
+ import ora3 from "ora";
1524
+
1525
+ // src/core/github.ts
1526
+ import os from "os";
947
1527
  import path8 from "path";
948
1528
  import fs7 from "fs-extra";
1529
+ import { extract as tarExtract } from "tar";
1530
+ function parseGitHubRef(input) {
1531
+ const raw = input.trim();
1532
+ if (!raw) {
1533
+ throw new ReplicaxError("No repository specified.", [
1534
+ "Pass a repo, e.g. replicax extract owner/repo"
1535
+ ]);
1536
+ }
1537
+ let work = raw;
1538
+ let ref;
1539
+ const hash = work.indexOf("#");
1540
+ if (hash !== -1) {
1541
+ ref = work.slice(hash + 1).trim() || void 0;
1542
+ work = work.slice(0, hash);
1543
+ }
1544
+ work = work.replace(/^git@github\.com:/i, "").replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//i, "").replace(/^\/+/, "");
1545
+ const segments = work.split("/").filter(Boolean);
1546
+ const owner = segments[0];
1547
+ const repoSegment = segments[1];
1548
+ if (!owner || !repoSegment) {
1549
+ throw new ReplicaxError(`Could not parse a GitHub repository from "${input}".`, [
1550
+ "Use owner/repo, a full https://github.com/owner/repo URL, or add #branch."
1551
+ ]);
1552
+ }
1553
+ let repo = repoSegment.replace(/\.git$/i, "");
1554
+ if (!ref && repo.includes("@")) {
1555
+ const [name, atRef] = repo.split("@");
1556
+ repo = name ?? repo;
1557
+ if (atRef) ref = atRef;
1558
+ }
1559
+ const kind = segments[2];
1560
+ if (!ref && (kind === "tree" || kind === "commit" || kind === "blob")) {
1561
+ const rest = segments.slice(3).join("/");
1562
+ if (rest) ref = rest;
1563
+ }
1564
+ const valid = (s) => /^[\w.-]+$/.test(s) && s !== "." && s !== "..";
1565
+ if (!valid(owner) || !valid(repo)) {
1566
+ throw new ReplicaxError(`Invalid GitHub repository: "${owner}/${repo}".`, [
1567
+ 'Owner and repo may contain only letters, numbers, ".", "_", and "-".'
1568
+ ]);
1569
+ }
1570
+ return { owner, repo, ...ref ? { ref } : {} };
1571
+ }
1572
+ function refLabel(ref) {
1573
+ return `${ref.owner}/${ref.repo}${ref.ref ? `@${ref.ref}` : ""}`;
1574
+ }
1575
+ function tokenFromEnv() {
1576
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
1577
+ return token?.trim() || void 0;
1578
+ }
1579
+ function httpError(status, slug2, hasToken) {
1580
+ if (status === 404) {
1581
+ return new ReplicaxError(`Repository not found: ${slug2}.`, [
1582
+ "Check the owner/repo spelling and the branch/tag/commit name.",
1583
+ hasToken ? "If it is private, make sure your token can access it." : "If it is private, set GITHUB_TOKEN (or GH_TOKEN) with repo access."
1584
+ ]);
1585
+ }
1586
+ if (status === 401) {
1587
+ return new ReplicaxError(`GitHub rejected the credentials for ${slug2}.`, [
1588
+ "Check that GITHUB_TOKEN (or GH_TOKEN) is valid and not expired."
1589
+ ]);
1590
+ }
1591
+ if (status === 403 || status === 429) {
1592
+ return new ReplicaxError(`GitHub rate limit hit while fetching ${slug2}.`, [
1593
+ hasToken ? "Wait a moment and try again." : "Set GITHUB_TOKEN (or GH_TOKEN) to raise the limit, then retry."
1594
+ ]);
1595
+ }
1596
+ return new ReplicaxError(`GitHub returned HTTP ${status} for ${slug2}.`);
1597
+ }
1598
+ async function firstSubdir(dir) {
1599
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1600
+ for (const entry of entries) {
1601
+ if (entry.isDirectory()) return path8.join(dir, entry.name);
1602
+ }
1603
+ return null;
1604
+ }
1605
+ async function downloadRepo(ref) {
1606
+ const slug2 = `${ref.owner}/${ref.repo}`;
1607
+ const url = `https://api.github.com/repos/${ref.owner}/${ref.repo}/tarball` + (ref.ref ? `/${encodeURIComponent(ref.ref)}` : "");
1608
+ const token = tokenFromEnv();
1609
+ const headers = {
1610
+ "user-agent": "replicax",
1611
+ accept: "application/vnd.github+json"
1612
+ };
1613
+ if (token) headers.authorization = `Bearer ${token}`;
1614
+ let res;
1615
+ try {
1616
+ res = await fetch(url, { headers, redirect: "follow" });
1617
+ } catch (err) {
1618
+ throw new ReplicaxError(`Failed to reach GitHub: ${err.message}`, [
1619
+ "Check your internet connection and try again."
1620
+ ]);
1621
+ }
1622
+ if (!res.ok) {
1623
+ throw httpError(res.status, slug2, Boolean(token));
1624
+ }
1625
+ const tmpRoot = await fs7.mkdtemp(path8.join(os.tmpdir(), "replicax-extract-"));
1626
+ const cleanup = () => fs7.remove(tmpRoot);
1627
+ try {
1628
+ const tarPath = path8.join(tmpRoot, "repo.tar.gz");
1629
+ await fs7.writeFile(tarPath, Buffer.from(await res.arrayBuffer()));
1630
+ const extractDir = path8.join(tmpRoot, "src");
1631
+ await fs7.ensureDir(extractDir);
1632
+ await tarExtract({ file: tarPath, cwd: extractDir, strip: 0 });
1633
+ const repoRoot = await firstSubdir(extractDir);
1634
+ if (!repoRoot) {
1635
+ throw new ReplicaxError(`The downloaded archive for ${slug2} was empty.`);
1636
+ }
1637
+ return { dir: repoRoot, cleanup };
1638
+ } catch (err) {
1639
+ await cleanup();
1640
+ throw err;
1641
+ }
1642
+ }
1643
+
1644
+ // src/commands/extract.ts
1645
+ async function extractCommand(repo, options) {
1646
+ if (options.verbose) setVerbose(true);
1647
+ const parsed = parseGitHubRef(repo);
1648
+ const target = { owner: parsed.owner, repo: parsed.repo, ref: options.ref ?? parsed.ref };
1649
+ const label = refLabel(target);
1650
+ const fetchSpinner = ora3({
1651
+ text: `Fetching ${label} from GitHub\u2026`,
1652
+ isEnabled: !options.verbose
1653
+ }).start();
1654
+ let downloaded;
1655
+ try {
1656
+ downloaded = await downloadRepo(target);
1657
+ } catch (err) {
1658
+ fetchSpinner.fail(`Could not fetch ${label}`);
1659
+ throw err;
1660
+ }
1661
+ fetchSpinner.succeed(`Downloaded ${label}`);
1662
+ try {
1663
+ const name = options.name ?? target.repo;
1664
+ const scanSpinner = ora3({ text: "Scanning repository\u2026", isEnabled: !options.verbose }).start();
1665
+ const scan = await scanProject(downloaded.dir);
1666
+ scanSpinner.succeed(
1667
+ `Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
1668
+ );
1669
+ scan.structure.root = name;
1670
+ const bundle = buildBundle({
1671
+ name,
1672
+ tooling: scan.tooling,
1673
+ structure: scan.structure,
1674
+ metadata: scan.metadata
1675
+ });
1676
+ reportSkippedSecrets(scan.skippedSecrets);
1677
+ printScanSummary(bundle);
1678
+ logger.out(renderTree(bundle.structure.directories, bundle.structure.root));
1679
+ if (options.dryRun) {
1680
+ logger.newline();
1681
+ logger.info("Dry run \u2014 no files were written.");
1682
+ return;
1683
+ }
1684
+ const outRoot = options.out ? path9.resolve(options.out) : process.cwd();
1685
+ const dir = profileDir(outRoot);
1686
+ if (await profileExists(dir)) {
1687
+ logger.warn(
1688
+ `A ReplicaX profile already exists in ${relPosix(process.cwd(), dir)}/ \u2014 replacing it.`
1689
+ );
1690
+ }
1691
+ await saveBundle(dir, bundle);
1692
+ logger.newline();
1693
+ logger.success(`Profile "${name}" written to ${relPosix(process.cwd(), dir)}/`);
1694
+ logger.hint("Create a project from it with: replicax create <project-name>");
1695
+ } finally {
1696
+ await downloaded.cleanup();
1697
+ }
1698
+ }
1699
+
1700
+ // src/commands/create.ts
1701
+ import path11 from "path";
1702
+ import fs9 from "fs-extra";
949
1703
 
950
1704
  // src/core/conflict-resolver.ts
951
1705
  import { select } from "@inquirer/prompts";
@@ -987,8 +1741,8 @@ var ConflictResolver = class {
987
1741
  };
988
1742
 
989
1743
  // src/core/project-generator.ts
990
- import path7 from "path";
991
- import fs6 from "fs-extra";
1744
+ import path10 from "path";
1745
+ import fs8 from "fs-extra";
992
1746
  async function generateProject(options) {
993
1747
  const { bundle, targetDir, projectName, dryRun, conflict } = options;
994
1748
  const result = {
@@ -998,16 +1752,16 @@ async function generateProject(options) {
998
1752
  filesSkipped: 0,
999
1753
  unsafeSkipped: []
1000
1754
  };
1001
- if (!dryRun) await fs6.ensureDir(targetDir);
1755
+ if (!dryRun) await fs8.ensureDir(targetDir);
1002
1756
  for (const dir of bundle.structure.directories) {
1003
1757
  const safe = safeJoinable(dir);
1004
1758
  if (!safe) {
1005
1759
  result.unsafeSkipped.push(dir);
1006
1760
  continue;
1007
1761
  }
1008
- const full = path7.join(targetDir, safe);
1009
- const existed = await fs6.pathExists(full);
1010
- if (!dryRun) await fs6.ensureDir(full);
1762
+ const full = path10.join(targetDir, safe);
1763
+ const existed = await fs8.pathExists(full);
1764
+ if (!dryRun) await fs8.ensureDir(full);
1011
1765
  if (!existed) result.dirsCreated += 1;
1012
1766
  result.entries.push({ kind: "dir", path: safe, action: existed ? "skip" : "create" });
1013
1767
  }
@@ -1031,8 +1785,8 @@ async function writeFile(relPath, content, options, result) {
1031
1785
  logger.warn(`Refusing to write unsafe path from profile: ${relPath}`);
1032
1786
  return;
1033
1787
  }
1034
- const full = path7.join(options.targetDir, safe);
1035
- const exists = await fs6.pathExists(full);
1788
+ const full = path10.join(options.targetDir, safe);
1789
+ const exists = await fs8.pathExists(full);
1036
1790
  let action2 = exists ? "overwrite" : "create";
1037
1791
  if (exists) {
1038
1792
  const decision = await options.conflict.resolve(safe);
@@ -1045,8 +1799,8 @@ async function writeFile(relPath, content, options, result) {
1045
1799
  action2 = "overwrite";
1046
1800
  }
1047
1801
  if (!options.dryRun) {
1048
- await fs6.ensureDir(path7.dirname(full));
1049
- await fs6.writeFile(full, content, "utf8");
1802
+ await fs8.ensureDir(path10.dirname(full));
1803
+ await fs8.writeFile(full, content, "utf8");
1050
1804
  }
1051
1805
  result.filesWritten += 1;
1052
1806
  result.entries.push({ kind: "file", path: safe, action: action2 });
@@ -1054,7 +1808,7 @@ async function writeFile(relPath, content, options, result) {
1054
1808
  }
1055
1809
 
1056
1810
  // src/core/installer.ts
1057
- import { spawn } from "child_process";
1811
+ import { spawn as spawn2 } from "child_process";
1058
1812
  var COMMANDS = {
1059
1813
  npm: ["npm", "install"],
1060
1814
  pnpm: ["pnpm", "install"],
@@ -1065,7 +1819,7 @@ function installDependencies(cwd, manager) {
1065
1819
  if (manager === "unknown") return Promise.resolve(false);
1066
1820
  const [command, ...args] = COMMANDS[manager];
1067
1821
  return new Promise((resolve) => {
1068
- const child = spawn(command, args, {
1822
+ const child = spawn2(command, args, {
1069
1823
  cwd,
1070
1824
  stdio: "inherit",
1071
1825
  // npm/pnpm/yarn are .cmd shims on Windows; a shell resolves them.
@@ -1095,9 +1849,9 @@ async function createCommand(projectName, options) {
1095
1849
  logger.warn(`Profile integrity check found ${mismatches.length} issue(s); continuing anyway.`);
1096
1850
  logger.hint("Run `replicax validate` for details.");
1097
1851
  }
1098
- const targetDir = path8.resolve(process.cwd(), projectName);
1099
- const leafName = path8.basename(targetDir);
1100
- if (path8.resolve(process.cwd()) === targetDir) {
1852
+ const targetDir = path11.resolve(process.cwd(), projectName);
1853
+ const leafName = path11.basename(targetDir);
1854
+ if (path11.resolve(process.cwd()) === targetDir) {
1101
1855
  throw new ReplicaxError("Refusing to scaffold into the current directory.", [
1102
1856
  "Pass a new project name, e.g. `replicax create my-app`."
1103
1857
  ]);
@@ -1146,8 +1900,8 @@ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
1146
1900
  logger.hint("No package manager detected; run your install command manually.");
1147
1901
  return;
1148
1902
  }
1149
- const pkgPath = path8.join(targetDir, "package.json");
1150
- const pkg = await fs7.readJson(pkgPath).catch(() => null);
1903
+ const pkgPath = path11.join(targetDir, "package.json");
1904
+ const pkg = await fs9.readJson(pkgPath).catch(() => null);
1151
1905
  if (!pkg?.devDependencies || Object.keys(pkg.devDependencies).length === 0) {
1152
1906
  logger.hint("No dependencies to install.");
1153
1907
  return;
@@ -1160,7 +1914,7 @@ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
1160
1914
  }
1161
1915
 
1162
1916
  // src/commands/sync.ts
1163
- import ora2 from "ora";
1917
+ import ora4 from "ora";
1164
1918
 
1165
1919
  // src/core/diff.ts
1166
1920
  function diffChecksums(prev, next) {
@@ -1230,7 +1984,7 @@ async function syncCommand(options) {
1230
1984
  throw new ReplicaxError("No ReplicaX profile to sync.", ["Run `replicax init` first."]);
1231
1985
  }
1232
1986
  const existing = await loadBundle(dir);
1233
- const spinner = ora2({ text: "Re-scanning project\u2026", isEnabled: !options.verbose }).start();
1987
+ const spinner = ora4({ text: "Re-scanning project\u2026", isEnabled: !options.verbose }).start();
1234
1988
  const scan = await scanProject(root);
1235
1989
  spinner.succeed("Re-scan complete");
1236
1990
  const next = buildBundle({
@@ -1398,20 +2152,20 @@ async function validateCommand(options) {
1398
2152
  }
1399
2153
 
1400
2154
  // src/commands/export.ts
1401
- import path10 from "path";
1402
- import fs9 from "fs-extra";
1403
- import ora3 from "ora";
2155
+ import path13 from "path";
2156
+ import fs11 from "fs-extra";
2157
+ import ora5 from "ora";
1404
2158
 
1405
2159
  // src/core/archive.ts
1406
- import os from "os";
1407
- import path9 from "path";
1408
- import fs8 from "fs-extra";
1409
- import { create as tarCreate, extract as tarExtract } from "tar";
2160
+ import os2 from "os";
2161
+ import path12 from "path";
2162
+ import fs10 from "fs-extra";
2163
+ import { create as tarCreate, extract as tarExtract2 } from "tar";
1410
2164
  async function exportProfile(profileDirectory, outPath) {
1411
- const resolvedOut = path9.resolve(outPath);
1412
- await fs8.ensureDir(path9.dirname(resolvedOut));
1413
- const parent = path9.dirname(profileDirectory);
1414
- const base = path9.basename(profileDirectory);
2165
+ const resolvedOut = path12.resolve(outPath);
2166
+ await fs10.ensureDir(path12.dirname(resolvedOut));
2167
+ const parent = path12.dirname(profileDirectory);
2168
+ const base = path12.basename(profileDirectory);
1415
2169
  await tarCreate(
1416
2170
  {
1417
2171
  gzip: true,
@@ -1424,21 +2178,21 @@ async function exportProfile(profileDirectory, outPath) {
1424
2178
  );
1425
2179
  }
1426
2180
  async function extractToTemp(archivePath) {
1427
- const resolved = path9.resolve(archivePath);
1428
- if (!await fs8.pathExists(resolved)) {
2181
+ const resolved = path12.resolve(archivePath);
2182
+ if (!await fs10.pathExists(resolved)) {
1429
2183
  throw new Error(`Archive not found: ${archivePath}`);
1430
2184
  }
1431
- const tmp = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-import-"));
1432
- await tarExtract({ file: resolved, cwd: tmp, strip: 0 });
2185
+ const tmp = await fs10.mkdtemp(path12.join(os2.tmpdir(), "replicax-import-"));
2186
+ await tarExtract2({ file: resolved, cwd: tmp, strip: 0 });
1433
2187
  return tmp;
1434
2188
  }
1435
2189
  async function findProfileRoot(dir) {
1436
- const hasProfile = async (d) => fs8.pathExists(path9.join(d, PROFILE_FILES.profile));
2190
+ const hasProfile = async (d) => fs10.pathExists(path12.join(d, PROFILE_FILES.profile));
1437
2191
  if (await hasProfile(dir)) return dir;
1438
- const entries = await fs8.readdir(dir, { withFileTypes: true });
2192
+ const entries = await fs10.readdir(dir, { withFileTypes: true });
1439
2193
  for (const entry of entries) {
1440
2194
  if (entry.isDirectory()) {
1441
- const candidate = path9.join(dir, entry.name);
2195
+ const candidate = path12.join(dir, entry.name);
1442
2196
  if (await hasProfile(candidate)) return candidate;
1443
2197
  }
1444
2198
  }
@@ -1455,13 +2209,13 @@ async function exportCommand(options) {
1455
2209
  throw new ReplicaxError("No ReplicaX profile found to export.", ["Run `replicax init` first."]);
1456
2210
  }
1457
2211
  const bundle = await loadBundle(dir);
1458
- const outPath = path10.resolve(options.out ?? `${slug(bundle.profile.name)}.replicax.tar.gz`);
1459
- const spinner = ora3({ text: "Packaging profile\u2026" }).start();
2212
+ const outPath = path13.resolve(options.out ?? `${slug(bundle.profile.name)}.replicax.tar.gz`);
2213
+ const spinner = ora5({ text: "Packaging profile\u2026" }).start();
1460
2214
  await exportProfile(dir, outPath);
1461
2215
  spinner.stop();
1462
- const { size } = await fs9.stat(outPath);
2216
+ const { size } = await fs11.stat(outPath);
1463
2217
  logger.success(
1464
- `Exported "${bundle.profile.name}" \u2192 ${path10.relative(process.cwd(), outPath)} (${formatBytes2(size)})`
2218
+ `Exported "${bundle.profile.name}" \u2192 ${path13.relative(process.cwd(), outPath)} (${formatBytes2(size)})`
1465
2219
  );
1466
2220
  logger.hint("Share it, then `replicax import <file>` elsewhere.");
1467
2221
  }
@@ -1472,14 +2226,14 @@ function formatBytes2(bytes) {
1472
2226
  }
1473
2227
 
1474
2228
  // src/commands/import.ts
1475
- import fs10 from "fs-extra";
1476
- import ora4 from "ora";
2229
+ import fs12 from "fs-extra";
2230
+ import ora6 from "ora";
1477
2231
  import { confirm as confirm2 } from "@inquirer/prompts";
1478
2232
  async function importCommand(archivePath, options) {
1479
2233
  if (!archivePath) {
1480
2234
  throw new ReplicaxError("An archive path is required: replicax import <file>");
1481
2235
  }
1482
- const spinner = ora4({ text: "Extracting archive\u2026" }).start();
2236
+ const spinner = ora6({ text: "Extracting archive\u2026" }).start();
1483
2237
  const tmp = await extractToTemp(archivePath);
1484
2238
  try {
1485
2239
  const source = await findProfileRoot(tmp);
@@ -1500,7 +2254,7 @@ async function importCommand(archivePath, options) {
1500
2254
  "Re-run with --force to overwrite it."
1501
2255
  ]);
1502
2256
  }
1503
- await fs10.remove(dest);
2257
+ await fs12.remove(dest);
1504
2258
  }
1505
2259
  await saveBundle(dest, bundle);
1506
2260
  logger.newline();
@@ -1509,7 +2263,7 @@ async function importCommand(archivePath, options) {
1509
2263
  );
1510
2264
  logger.hint("Create a project with: replicax create <project-name>");
1511
2265
  } finally {
1512
- await fs10.remove(tmp).catch(() => void 0);
2266
+ await fs12.remove(tmp).catch(() => void 0);
1513
2267
  }
1514
2268
  }
1515
2269
 
@@ -1549,6 +2303,10 @@ function handleError(err) {
1549
2303
  var program = new Command();
1550
2304
  program.name("replicax").description("Copy the setup, not the code.").version(packageVersion(), "-v, --version", "Print the ReplicaX version").showHelpAfterError("(run `replicax --help` for usage)");
1551
2305
  program.command("init").description("Scan the current project and create a ReplicaX profile in .replicax/").option("--name <name>", "Name the profile").option("--dry-run", "Preview what would be captured without writing").option("--verbose", "Show every detected file").action(action(initCommand));
2306
+ program.command("init-skill").description(
2307
+ "Generate an AI assistant skill from the detected tech stack (uses your configured AI)"
2308
+ ).option("--target <ai>", `Target AI assistant: ${SKILL_TARGET_IDS.join("|")}`).option("--name <name>", "Name the skill (defaults to the project folder name)").option("--provider <ai>", "Force the AI provider: claude|openai|gemini (default: auto-detect)").option("--model <id>", "Override the API model id for the chosen provider").option("--no-ai", "Skip the AI provider and use the built-in deterministic template").option("--dry-run", "Preview the skill without writing (no AI call)").option("--force", "Overwrite existing skill files").option("--verbose", "Show every detected file").action(action(initSkillCommand));
2309
+ program.command("extract").argument("<repo>", "GitHub repo: owner/repo, a github.com URL, or owner/repo#branch").description("Extract a ReplicaX profile from a remote GitHub repository").option("--ref <ref>", "Branch, tag, or commit to fetch (default: the repo default branch)").option("--name <name>", "Name the profile (defaults to the repo name)").option("--out <dir>", "Directory to write the .replicax profile into (default: current dir)").option("--dry-run", "Preview what would be captured without writing").option("--verbose", "Show every detected file").action(action(extractCommand));
1552
2310
  program.command("create").argument("<project-name>", "Directory/name for the new project").description("Create a new project from a profile").option("--profile <path>", "Use a profile from a custom path").option("--skip-install", "Do not run the package manager install step").option("--dry-run", "Preview the output without writing").option("--force", "Overwrite conflicting files without prompting").option("--verbose", "Show every written file").action(action(createCommand));
1553
2311
  program.command("sync").description("Update the profile from the current project state").option("--diff", "Show a detailed list of what changed").option("--force", "Rewrite the profile even if nothing changed").option("--verbose", "Show every detected file").action(action(syncCommand));
1554
2312
  program.command("inspect").description("Display captured configuration and structure").option("--json", "Output as JSON").option("--section <section>", "Inspect one section: profile|tooling|structure|metadata").option("--profile <path>", "Inspect a profile at a custom path").action(action(inspectCommand));