@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.
- package/LICENSE +2 -2
- package/README.md +337 -190
- package/dist/index.js +805 -47
- 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/
|
|
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
|
|
991
|
-
import
|
|
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
|
|
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 =
|
|
1009
|
-
const existed = await
|
|
1010
|
-
if (!dryRun) await
|
|
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 =
|
|
1035
|
-
const exists = await
|
|
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
|
|
1049
|
-
await
|
|
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 =
|
|
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 =
|
|
1099
|
-
const leafName =
|
|
1100
|
-
if (
|
|
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 =
|
|
1150
|
-
const pkg = await
|
|
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
|
|
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 =
|
|
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
|
|
1402
|
-
import
|
|
1403
|
-
import
|
|
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
|
|
1407
|
-
import
|
|
1408
|
-
import
|
|
1409
|
-
import { create as tarCreate, extract as
|
|
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 =
|
|
1412
|
-
await
|
|
1413
|
-
const parent =
|
|
1414
|
-
const base =
|
|
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 =
|
|
1428
|
-
if (!await
|
|
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
|
|
1432
|
-
await
|
|
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) =>
|
|
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
|
|
2192
|
+
const entries = await fs10.readdir(dir, { withFileTypes: true });
|
|
1439
2193
|
for (const entry of entries) {
|
|
1440
2194
|
if (entry.isDirectory()) {
|
|
1441
|
-
const candidate =
|
|
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 =
|
|
1459
|
-
const spinner =
|
|
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
|
|
2216
|
+
const { size } = await fs11.stat(outPath);
|
|
1463
2217
|
logger.success(
|
|
1464
|
-
`Exported "${bundle.profile.name}" \u2192 ${
|
|
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
|
|
1476
|
-
import
|
|
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 =
|
|
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
|
|
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
|
|
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));
|