@iamsaroj/replicax 0.0.1 → 0.0.3

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