@iamsaroj/replicax 0.0.3 → 0.0.5
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/README.md +199 -73
- package/dist/index.js +1590 -205
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -60,21 +60,24 @@ var logger = {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// src/commands/init.ts
|
|
63
|
-
import
|
|
64
|
-
import
|
|
63
|
+
import path7 from "path";
|
|
64
|
+
import fs6 from "fs-extra";
|
|
65
65
|
import ora from "ora";
|
|
66
66
|
import { confirm } from "@inquirer/prompts";
|
|
67
67
|
|
|
68
68
|
// src/constants.ts
|
|
69
69
|
var REPLICAX_DIR = ".replicax";
|
|
70
70
|
var IGNORE_FILE = ".replicaxignore";
|
|
71
|
-
var
|
|
71
|
+
var INCLUDE_FILE = ".replicaxinclude";
|
|
72
|
+
var ROOT_SKILL_FILE = "SKILL.md";
|
|
73
|
+
var REPLICAX_VERSION = "2.2.0";
|
|
72
74
|
var PROFILE_FILES = {
|
|
73
75
|
profile: "profile.json",
|
|
74
76
|
tooling: "tooling.json",
|
|
75
77
|
structure: "structure.json",
|
|
76
78
|
metadata: "metadata.json",
|
|
77
|
-
checksum: "checksum.json"
|
|
79
|
+
checksum: "checksum.json",
|
|
80
|
+
manifest: "manifest.json"
|
|
78
81
|
};
|
|
79
82
|
var SCAN_PRUNE_GLOBS = [
|
|
80
83
|
"**/node_modules/**",
|
|
@@ -99,6 +102,7 @@ var SCAN_PRUNE_GLOBS = [
|
|
|
99
102
|
"**/.fleet/**",
|
|
100
103
|
"**/.zed/**"
|
|
101
104
|
];
|
|
105
|
+
var INCLUDE_PRUNE_GLOBS = ["**/node_modules/**", "**/.git/**", `**/${REPLICAX_DIR}/**`];
|
|
102
106
|
var DEFAULT_IGNORE_PATTERNS = [
|
|
103
107
|
"node_modules/",
|
|
104
108
|
".git/",
|
|
@@ -181,9 +185,9 @@ yarn-error.log*
|
|
|
181
185
|
`;
|
|
182
186
|
|
|
183
187
|
// src/core/scanner.ts
|
|
184
|
-
import
|
|
185
|
-
import
|
|
186
|
-
import
|
|
188
|
+
import path5 from "path";
|
|
189
|
+
import fs4 from "fs-extra";
|
|
190
|
+
import fg2 from "fast-glob";
|
|
187
191
|
|
|
188
192
|
// src/config/supported-files.ts
|
|
189
193
|
var CONFIG_CATEGORIES = [
|
|
@@ -288,6 +292,38 @@ var CONFIG_CATEGORIES = [
|
|
|
288
292
|
label: "Git Hooks",
|
|
289
293
|
patterns: [".husky/*"]
|
|
290
294
|
},
|
|
295
|
+
{
|
|
296
|
+
id: "jvm-build",
|
|
297
|
+
label: "JVM Build",
|
|
298
|
+
// The captured surface is language-agnostic: Maven/Gradle build files are
|
|
299
|
+
// setup, not application code. Globbed with `**/` so monorepos (a Spring
|
|
300
|
+
// backend beside a JS frontend) are captured too. The gradle wrapper JAR is
|
|
301
|
+
// binary and deliberately excluded — only its text `.properties` is kept.
|
|
302
|
+
patterns: [
|
|
303
|
+
"**/pom.xml",
|
|
304
|
+
"**/build.gradle",
|
|
305
|
+
"**/build.gradle.kts",
|
|
306
|
+
"**/settings.gradle",
|
|
307
|
+
"**/settings.gradle.kts",
|
|
308
|
+
"gradle.properties",
|
|
309
|
+
"mvnw",
|
|
310
|
+
"mvnw.cmd",
|
|
311
|
+
"gradlew",
|
|
312
|
+
"gradlew.bat",
|
|
313
|
+
"**/gradle/wrapper/gradle-wrapper.properties"
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: "jvm-config",
|
|
318
|
+
label: "JVM Config",
|
|
319
|
+
// Spring-style externalized config. Scoped to `src/main/resources/` so we
|
|
320
|
+
// capture application config without sweeping up unrelated `.properties`.
|
|
321
|
+
patterns: [
|
|
322
|
+
"**/src/main/resources/application*.yml",
|
|
323
|
+
"**/src/main/resources/application*.yaml",
|
|
324
|
+
"**/src/main/resources/application*.properties"
|
|
325
|
+
]
|
|
326
|
+
},
|
|
291
327
|
{
|
|
292
328
|
id: "misc",
|
|
293
329
|
label: "Miscellaneous Tooling",
|
|
@@ -308,6 +344,13 @@ var CONFIG_CATEGORIES = [
|
|
|
308
344
|
];
|
|
309
345
|
var ALL_CONFIG_PATTERNS = CONFIG_CATEGORIES.flatMap((c) => c.patterns);
|
|
310
346
|
var CATEGORY_BY_ID = new Map(CONFIG_CATEGORIES.map((c) => [c.id, c]));
|
|
347
|
+
var EXTRA_CATEGORY_LABELS = {
|
|
348
|
+
// Files pulled in explicitly via `.replicaxinclude`.
|
|
349
|
+
included: "Included files"
|
|
350
|
+
};
|
|
351
|
+
function categoryLabel(id) {
|
|
352
|
+
return CATEGORY_BY_ID.get(id)?.label ?? EXTRA_CATEGORY_LABELS[id] ?? id;
|
|
353
|
+
}
|
|
311
354
|
|
|
312
355
|
// src/utils/paths.ts
|
|
313
356
|
import path from "path";
|
|
@@ -353,6 +396,7 @@ import fs from "fs-extra";
|
|
|
353
396
|
import ignore from "ignore";
|
|
354
397
|
var IgnoreEngine = class _IgnoreEngine {
|
|
355
398
|
ig;
|
|
399
|
+
userIg;
|
|
356
400
|
secrets;
|
|
357
401
|
userPatterns;
|
|
358
402
|
constructor(userPatterns = []) {
|
|
@@ -361,6 +405,7 @@ var IgnoreEngine = class _IgnoreEngine {
|
|
|
361
405
|
return t.length > 0 && !t.startsWith("#");
|
|
362
406
|
});
|
|
363
407
|
this.ig = ignore().add(DEFAULT_IGNORE_PATTERNS).add(this.userPatterns);
|
|
408
|
+
this.userIg = ignore().add(this.userPatterns);
|
|
364
409
|
this.secrets = ignore().add(SECRET_GUARD_GLOBS);
|
|
365
410
|
}
|
|
366
411
|
/** Build an engine from a project's `.replicaxignore`, if present. */
|
|
@@ -377,6 +422,15 @@ var IgnoreEngine = class _IgnoreEngine {
|
|
|
377
422
|
if (!relPosixPath || relPosixPath === ".") return false;
|
|
378
423
|
return this.ig.ignores(relPosixPath);
|
|
379
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Whether a path is excluded by the user's `.replicaxignore` *only* (ignoring
|
|
427
|
+
* the built-in defaults). Used to apply `.replicaxignore`'s precedence over an
|
|
428
|
+
* explicit `.replicaxinclude` without the defaults vetoing the include.
|
|
429
|
+
*/
|
|
430
|
+
isUserIgnored(relPosixPath) {
|
|
431
|
+
if (!relPosixPath || relPosixPath === ".") return false;
|
|
432
|
+
return this.userIg.ignores(relPosixPath);
|
|
433
|
+
}
|
|
380
434
|
/** Whether a path is a protected secret that must never be captured. */
|
|
381
435
|
isSecret(relPosixPath) {
|
|
382
436
|
if (!relPosixPath || relPosixPath === ".") return false;
|
|
@@ -439,6 +493,11 @@ async function detectLanguage(root, pkg) {
|
|
|
439
493
|
return file === "jsconfig.json" ? "javascript" : "typescript";
|
|
440
494
|
}
|
|
441
495
|
}
|
|
496
|
+
if (!pkg) {
|
|
497
|
+
for (const file of ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"]) {
|
|
498
|
+
if (await fs2.pathExists(path3.join(root, file))) return "java";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
442
501
|
return "javascript";
|
|
443
502
|
}
|
|
444
503
|
function detectFramework(pkg) {
|
|
@@ -554,6 +613,464 @@ function renderPackageJson(template, projectName) {
|
|
|
554
613
|
return JSON.stringify(ordered, null, 2) + "\n";
|
|
555
614
|
}
|
|
556
615
|
|
|
616
|
+
// src/core/detection/context.ts
|
|
617
|
+
import path4 from "path";
|
|
618
|
+
import fs3 from "fs-extra";
|
|
619
|
+
import fg from "fast-glob";
|
|
620
|
+
var EXACT_CANDIDATES = [
|
|
621
|
+
// package managers / lockfiles
|
|
622
|
+
"package-lock.json",
|
|
623
|
+
"npm-shrinkwrap.json",
|
|
624
|
+
"yarn.lock",
|
|
625
|
+
"pnpm-lock.yaml",
|
|
626
|
+
"bun.lockb",
|
|
627
|
+
"bun.lock",
|
|
628
|
+
"pnpm-workspace.yaml",
|
|
629
|
+
// docker
|
|
630
|
+
"Dockerfile",
|
|
631
|
+
".dockerignore",
|
|
632
|
+
// ci
|
|
633
|
+
".gitlab-ci.yml",
|
|
634
|
+
".circleci/config.yml",
|
|
635
|
+
"Jenkinsfile",
|
|
636
|
+
"azure-pipelines.yml",
|
|
637
|
+
// monorepo / build
|
|
638
|
+
"turbo.json",
|
|
639
|
+
"nx.json",
|
|
640
|
+
"lerna.json",
|
|
641
|
+
// git hooks
|
|
642
|
+
".husky",
|
|
643
|
+
// editors / ai assistants
|
|
644
|
+
".vscode",
|
|
645
|
+
".cursor",
|
|
646
|
+
".cursorrules",
|
|
647
|
+
".claude",
|
|
648
|
+
"CLAUDE.md",
|
|
649
|
+
".windsurf",
|
|
650
|
+
".windsurfrules",
|
|
651
|
+
".devcontainer",
|
|
652
|
+
".devcontainer.json",
|
|
653
|
+
// lint / format (exact flavours)
|
|
654
|
+
".eslintrc",
|
|
655
|
+
".prettierrc",
|
|
656
|
+
// language
|
|
657
|
+
"tsconfig.json",
|
|
658
|
+
"jsconfig.json",
|
|
659
|
+
// jvm build
|
|
660
|
+
"pom.xml",
|
|
661
|
+
"mvnw",
|
|
662
|
+
"mvnw.cmd",
|
|
663
|
+
"build.gradle",
|
|
664
|
+
"build.gradle.kts",
|
|
665
|
+
"settings.gradle",
|
|
666
|
+
"settings.gradle.kts",
|
|
667
|
+
"gradlew",
|
|
668
|
+
"gradlew.bat"
|
|
669
|
+
];
|
|
670
|
+
var GLOB_CANDIDATES = [
|
|
671
|
+
".github/workflows/*.yml",
|
|
672
|
+
".github/workflows/*.yaml",
|
|
673
|
+
"Dockerfile.*",
|
|
674
|
+
"docker-compose*.yml",
|
|
675
|
+
"docker-compose*.yaml",
|
|
676
|
+
"compose.yml",
|
|
677
|
+
"compose.yaml",
|
|
678
|
+
"eslint.config.*",
|
|
679
|
+
".eslintrc.*",
|
|
680
|
+
"prettier.config.*",
|
|
681
|
+
".prettierrc.*",
|
|
682
|
+
"commitlint.config.*",
|
|
683
|
+
".commitlintrc",
|
|
684
|
+
".commitlintrc.*",
|
|
685
|
+
"lint-staged.config.*",
|
|
686
|
+
".lintstagedrc",
|
|
687
|
+
".lintstagedrc.*",
|
|
688
|
+
"vitest.config.*",
|
|
689
|
+
"jest.config.*",
|
|
690
|
+
"playwright.config.*",
|
|
691
|
+
"cypress.config.*",
|
|
692
|
+
".husky/*",
|
|
693
|
+
// JVM build files + Spring resources, globbed with `**/` so a backend nested
|
|
694
|
+
// inside a monorepo (e.g. fullstack JS + Spring) is still detected.
|
|
695
|
+
"**/pom.xml",
|
|
696
|
+
"**/build.gradle",
|
|
697
|
+
"**/build.gradle.kts",
|
|
698
|
+
"**/settings.gradle",
|
|
699
|
+
"**/settings.gradle.kts",
|
|
700
|
+
"**/src/main/resources/application*.yml",
|
|
701
|
+
"**/src/main/resources/application*.yaml",
|
|
702
|
+
"**/src/main/resources/application*.properties"
|
|
703
|
+
];
|
|
704
|
+
async function gatherContext(root, pkg) {
|
|
705
|
+
const present = /* @__PURE__ */ new Set();
|
|
706
|
+
await Promise.all(
|
|
707
|
+
EXACT_CANDIDATES.map(async (rel) => {
|
|
708
|
+
if (await fs3.pathExists(path4.join(root, rel))) present.add(rel);
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
const matches = await fg(GLOB_CANDIDATES, {
|
|
712
|
+
cwd: root,
|
|
713
|
+
dot: true,
|
|
714
|
+
onlyFiles: true,
|
|
715
|
+
unique: true,
|
|
716
|
+
suppressErrors: true,
|
|
717
|
+
followSymbolicLinks: false,
|
|
718
|
+
ignore: SCAN_PRUNE_GLOBS
|
|
719
|
+
});
|
|
720
|
+
for (const m of matches) present.add(toPosix(m));
|
|
721
|
+
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
722
|
+
return {
|
|
723
|
+
root,
|
|
724
|
+
pkg,
|
|
725
|
+
deps,
|
|
726
|
+
present,
|
|
727
|
+
has: (rel) => present.has(rel),
|
|
728
|
+
hasUnder: (prefix) => {
|
|
729
|
+
const p = prefix.replace(/\/+$/, "");
|
|
730
|
+
if (present.has(p)) return true;
|
|
731
|
+
const needle = `${p}/`;
|
|
732
|
+
for (const x of present) if (x.startsWith(needle)) return true;
|
|
733
|
+
return false;
|
|
734
|
+
},
|
|
735
|
+
hasDep: (name) => name in deps
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/core/detection/types.ts
|
|
740
|
+
var Confidence = {
|
|
741
|
+
/** A canonical, unambiguous artifact is present (e.g. a Dockerfile). */
|
|
742
|
+
Confirmed: 1,
|
|
743
|
+
/** Strong signal (e.g. a declared dependency, a pinned field). */
|
|
744
|
+
Strong: 0.9,
|
|
745
|
+
/** Secondary/heuristic evidence (e.g. a related config but not the tool itself). */
|
|
746
|
+
Likely: 0.7,
|
|
747
|
+
/** Weak hint. */
|
|
748
|
+
Possible: 0.5
|
|
749
|
+
};
|
|
750
|
+
function defineDetector(meta, fn) {
|
|
751
|
+
return {
|
|
752
|
+
...meta,
|
|
753
|
+
detect(ctx) {
|
|
754
|
+
const hit2 = fn(ctx);
|
|
755
|
+
return hit2 ? { ...meta, confidence: hit2.confidence, evidence: hit2.evidence } : null;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function hit(confidence, ...evidence) {
|
|
760
|
+
return { confidence, evidence };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/core/detection/detectors/languages.ts
|
|
764
|
+
var LANGUAGE_NAMES = {
|
|
765
|
+
typescript: "TypeScript",
|
|
766
|
+
javascript: "JavaScript",
|
|
767
|
+
java: "Java",
|
|
768
|
+
unknown: null
|
|
769
|
+
};
|
|
770
|
+
var FRAMEWORK_LABELS = {
|
|
771
|
+
next: "Next.js",
|
|
772
|
+
nuxt: "Nuxt",
|
|
773
|
+
remix: "Remix",
|
|
774
|
+
astro: "Astro",
|
|
775
|
+
angular: "Angular",
|
|
776
|
+
sveltekit: "SvelteKit",
|
|
777
|
+
nestjs: "NestJS",
|
|
778
|
+
expo: "Expo",
|
|
779
|
+
"react-native": "React Native",
|
|
780
|
+
vue: "Vue",
|
|
781
|
+
svelte: "Svelte",
|
|
782
|
+
solid: "SolidJS",
|
|
783
|
+
react: "React",
|
|
784
|
+
fastify: "Fastify",
|
|
785
|
+
koa: "Koa",
|
|
786
|
+
express: "Express"
|
|
787
|
+
};
|
|
788
|
+
var FRAMEWORK_SKIP = /* @__PURE__ */ new Set(["unknown", "node"]);
|
|
789
|
+
function metadataDetections(metadata) {
|
|
790
|
+
const out = [];
|
|
791
|
+
const languageName = LANGUAGE_NAMES[metadata.language];
|
|
792
|
+
if (languageName) {
|
|
793
|
+
out.push({
|
|
794
|
+
id: metadata.language,
|
|
795
|
+
name: languageName,
|
|
796
|
+
category: "language",
|
|
797
|
+
confidence: Confidence.Confirmed,
|
|
798
|
+
evidence: ["metadata.language"]
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
if (metadata.framework && !FRAMEWORK_SKIP.has(metadata.framework)) {
|
|
802
|
+
out.push({
|
|
803
|
+
id: metadata.framework,
|
|
804
|
+
name: FRAMEWORK_LABELS[metadata.framework] ?? metadata.framework,
|
|
805
|
+
category: "framework",
|
|
806
|
+
confidence: Confidence.Confirmed,
|
|
807
|
+
evidence: ["package.json"]
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return out;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/core/detection/detectors/packageManagers.ts
|
|
814
|
+
function pmField(ctx) {
|
|
815
|
+
const field = ctx.pkg?.packageManager;
|
|
816
|
+
if (typeof field !== "string") return null;
|
|
817
|
+
return field.split("@")[0]?.trim().toLowerCase() ?? null;
|
|
818
|
+
}
|
|
819
|
+
var packageManagerDetectors = [
|
|
820
|
+
defineDetector({ id: "npm", name: "npm", category: "package-manager" }, (ctx) => {
|
|
821
|
+
if (ctx.has("package-lock.json")) return hit(Confidence.Confirmed, "package-lock.json");
|
|
822
|
+
if (ctx.has("npm-shrinkwrap.json")) return hit(Confidence.Confirmed, "npm-shrinkwrap.json");
|
|
823
|
+
if (pmField(ctx) === "npm") return hit(Confidence.Strong, "package.json#packageManager");
|
|
824
|
+
return null;
|
|
825
|
+
}),
|
|
826
|
+
defineDetector({ id: "pnpm", name: "pnpm", category: "package-manager" }, (ctx) => {
|
|
827
|
+
if (ctx.has("pnpm-lock.yaml")) return hit(Confidence.Confirmed, "pnpm-lock.yaml");
|
|
828
|
+
if (ctx.has("pnpm-workspace.yaml")) return hit(Confidence.Strong, "pnpm-workspace.yaml");
|
|
829
|
+
if (pmField(ctx) === "pnpm") return hit(Confidence.Strong, "package.json#packageManager");
|
|
830
|
+
return null;
|
|
831
|
+
}),
|
|
832
|
+
defineDetector({ id: "yarn", name: "Yarn", category: "package-manager" }, (ctx) => {
|
|
833
|
+
if (ctx.has("yarn.lock")) return hit(Confidence.Confirmed, "yarn.lock");
|
|
834
|
+
if (pmField(ctx) === "yarn") return hit(Confidence.Strong, "package.json#packageManager");
|
|
835
|
+
return null;
|
|
836
|
+
}),
|
|
837
|
+
defineDetector({ id: "bun", name: "Bun", category: "package-manager" }, (ctx) => {
|
|
838
|
+
if (ctx.has("bun.lockb")) return hit(Confidence.Confirmed, "bun.lockb");
|
|
839
|
+
if (ctx.has("bun.lock")) return hit(Confidence.Confirmed, "bun.lock");
|
|
840
|
+
if (pmField(ctx) === "bun") return hit(Confidence.Strong, "package.json#packageManager");
|
|
841
|
+
return null;
|
|
842
|
+
})
|
|
843
|
+
];
|
|
844
|
+
|
|
845
|
+
// src/core/detection/detectors/tooling.ts
|
|
846
|
+
function match(ctx, re) {
|
|
847
|
+
for (const p of ctx.present) if (re.test(p)) return p;
|
|
848
|
+
return void 0;
|
|
849
|
+
}
|
|
850
|
+
function hasPkgKey(ctx, key) {
|
|
851
|
+
return Boolean(ctx.pkg && typeof ctx.pkg === "object" && key in ctx.pkg);
|
|
852
|
+
}
|
|
853
|
+
var toolingDetectors = [
|
|
854
|
+
// --- Containers ----------------------------------------------------------
|
|
855
|
+
defineDetector({ id: "docker", name: "Docker", category: "container" }, (ctx) => {
|
|
856
|
+
const dockerfile = ctx.has("Dockerfile") ? "Dockerfile" : match(ctx, /^Dockerfile(\.|$)/);
|
|
857
|
+
if (dockerfile) return hit(Confidence.Confirmed, dockerfile);
|
|
858
|
+
if (ctx.has(".dockerignore")) return hit(Confidence.Likely, ".dockerignore");
|
|
859
|
+
return null;
|
|
860
|
+
}),
|
|
861
|
+
defineDetector({ id: "docker-compose", name: "Docker Compose", category: "container" }, (ctx) => {
|
|
862
|
+
const compose = match(ctx, /^(docker-compose|compose).*\.ya?ml$/);
|
|
863
|
+
return compose ? hit(Confidence.Confirmed, compose) : null;
|
|
864
|
+
}),
|
|
865
|
+
// --- CI/CD ---------------------------------------------------------------
|
|
866
|
+
defineDetector({ id: "github-actions", name: "GitHub Actions", category: "ci" }, (ctx) => {
|
|
867
|
+
if (ctx.hasUnder(".github/workflows")) {
|
|
868
|
+
return hit(
|
|
869
|
+
Confidence.Confirmed,
|
|
870
|
+
match(ctx, /^\.github\/workflows\//) ?? ".github/workflows/"
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
return null;
|
|
874
|
+
}),
|
|
875
|
+
defineDetector(
|
|
876
|
+
{ id: "gitlab-ci", name: "GitLab CI", category: "ci" },
|
|
877
|
+
(ctx) => ctx.has(".gitlab-ci.yml") ? hit(Confidence.Confirmed, ".gitlab-ci.yml") : null
|
|
878
|
+
),
|
|
879
|
+
defineDetector(
|
|
880
|
+
{ id: "circleci", name: "CircleCI", category: "ci" },
|
|
881
|
+
(ctx) => ctx.has(".circleci/config.yml") ? hit(Confidence.Confirmed, ".circleci/config.yml") : null
|
|
882
|
+
),
|
|
883
|
+
defineDetector(
|
|
884
|
+
{ id: "jenkins", name: "Jenkins", category: "ci" },
|
|
885
|
+
(ctx) => ctx.has("Jenkinsfile") ? hit(Confidence.Confirmed, "Jenkinsfile") : null
|
|
886
|
+
),
|
|
887
|
+
defineDetector(
|
|
888
|
+
{ id: "azure-pipelines", name: "Azure Pipelines", category: "ci" },
|
|
889
|
+
(ctx) => ctx.has("azure-pipelines.yml") ? hit(Confidence.Confirmed, "azure-pipelines.yml") : null
|
|
890
|
+
),
|
|
891
|
+
// --- Monorepo ------------------------------------------------------------
|
|
892
|
+
defineDetector(
|
|
893
|
+
{ id: "turborepo", name: "Turborepo", category: "monorepo" },
|
|
894
|
+
(ctx) => ctx.has("turbo.json") ? hit(Confidence.Confirmed, "turbo.json") : null
|
|
895
|
+
),
|
|
896
|
+
defineDetector(
|
|
897
|
+
{ id: "nx", name: "Nx", category: "monorepo" },
|
|
898
|
+
(ctx) => ctx.has("nx.json") ? hit(Confidence.Confirmed, "nx.json") : null
|
|
899
|
+
),
|
|
900
|
+
// --- Git hooks / commit --------------------------------------------------
|
|
901
|
+
defineDetector({ id: "husky", name: "Husky", category: "git-hooks" }, (ctx) => {
|
|
902
|
+
if (ctx.hasUnder(".husky")) return hit(Confidence.Confirmed, ".husky/");
|
|
903
|
+
if (ctx.hasDep("husky")) return hit(Confidence.Strong, "package.json#husky");
|
|
904
|
+
return null;
|
|
905
|
+
}),
|
|
906
|
+
defineDetector({ id: "lint-staged", name: "lint-staged", category: "commit" }, (ctx) => {
|
|
907
|
+
const cfg = match(ctx, /^(lint-staged\.config\.|\.lintstagedrc)/);
|
|
908
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
909
|
+
if (hasPkgKey(ctx, "lint-staged") || hasPkgKey(ctx, "nano-staged")) {
|
|
910
|
+
return hit(Confidence.Confirmed, "package.json#lint-staged");
|
|
911
|
+
}
|
|
912
|
+
if (ctx.hasDep("lint-staged")) return hit(Confidence.Strong, "package.json#lint-staged");
|
|
913
|
+
return null;
|
|
914
|
+
}),
|
|
915
|
+
defineDetector({ id: "commitlint", name: "Commitlint", category: "commit" }, (ctx) => {
|
|
916
|
+
const cfg = match(ctx, /^(commitlint\.config\.|\.commitlintrc)/);
|
|
917
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
918
|
+
if (hasPkgKey(ctx, "commitlint")) return hit(Confidence.Confirmed, "package.json#commitlint");
|
|
919
|
+
if (ctx.hasDep("@commitlint/cli")) return hit(Confidence.Strong, "@commitlint/cli");
|
|
920
|
+
return null;
|
|
921
|
+
}),
|
|
922
|
+
// --- Lint / format -------------------------------------------------------
|
|
923
|
+
defineDetector({ id: "eslint", name: "ESLint", category: "lint" }, (ctx) => {
|
|
924
|
+
const cfg = ctx.has(".eslintrc") ? ".eslintrc" : match(ctx, /^(eslint\.config\.|\.eslintrc\.)/);
|
|
925
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
926
|
+
if (hasPkgKey(ctx, "eslintConfig"))
|
|
927
|
+
return hit(Confidence.Confirmed, "package.json#eslintConfig");
|
|
928
|
+
if (ctx.hasDep("eslint")) return hit(Confidence.Strong, "eslint");
|
|
929
|
+
return null;
|
|
930
|
+
}),
|
|
931
|
+
defineDetector({ id: "prettier", name: "Prettier", category: "format" }, (ctx) => {
|
|
932
|
+
const cfg = ctx.has(".prettierrc") ? ".prettierrc" : match(ctx, /^(prettier\.config\.|\.prettierrc\.)/);
|
|
933
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
934
|
+
if (hasPkgKey(ctx, "prettier")) return hit(Confidence.Confirmed, "package.json#prettier");
|
|
935
|
+
if (ctx.hasDep("prettier")) return hit(Confidence.Strong, "prettier");
|
|
936
|
+
return null;
|
|
937
|
+
}),
|
|
938
|
+
// --- Testing -------------------------------------------------------------
|
|
939
|
+
defineDetector({ id: "vitest", name: "Vitest", category: "test" }, (ctx) => {
|
|
940
|
+
const cfg = match(ctx, /^vitest\.config\./);
|
|
941
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
942
|
+
if (ctx.hasDep("vitest")) return hit(Confidence.Strong, "vitest");
|
|
943
|
+
return null;
|
|
944
|
+
}),
|
|
945
|
+
defineDetector({ id: "jest", name: "Jest", category: "test" }, (ctx) => {
|
|
946
|
+
const cfg = match(ctx, /^jest\.config\./);
|
|
947
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
948
|
+
if (hasPkgKey(ctx, "jest")) return hit(Confidence.Confirmed, "package.json#jest");
|
|
949
|
+
if (ctx.hasDep("jest")) return hit(Confidence.Strong, "jest");
|
|
950
|
+
return null;
|
|
951
|
+
}),
|
|
952
|
+
defineDetector({ id: "playwright", name: "Playwright", category: "test" }, (ctx) => {
|
|
953
|
+
const cfg = match(ctx, /^playwright\.config\./);
|
|
954
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
955
|
+
if (ctx.hasDep("@playwright/test") || ctx.hasDep("playwright")) {
|
|
956
|
+
return hit(Confidence.Strong, "playwright");
|
|
957
|
+
}
|
|
958
|
+
return null;
|
|
959
|
+
}),
|
|
960
|
+
defineDetector({ id: "cypress", name: "Cypress", category: "test" }, (ctx) => {
|
|
961
|
+
const cfg = match(ctx, /^cypress\.config\./);
|
|
962
|
+
if (cfg) return hit(Confidence.Confirmed, cfg);
|
|
963
|
+
if (ctx.hasDep("cypress")) return hit(Confidence.Strong, "cypress");
|
|
964
|
+
return null;
|
|
965
|
+
}),
|
|
966
|
+
// --- Dev containers ------------------------------------------------------
|
|
967
|
+
defineDetector({ id: "devcontainer", name: "Dev Container", category: "devcontainer" }, (ctx) => {
|
|
968
|
+
if (ctx.hasUnder(".devcontainer")) return hit(Confidence.Confirmed, ".devcontainer/");
|
|
969
|
+
if (ctx.has(".devcontainer.json")) return hit(Confidence.Confirmed, ".devcontainer.json");
|
|
970
|
+
return null;
|
|
971
|
+
})
|
|
972
|
+
];
|
|
973
|
+
|
|
974
|
+
// src/core/detection/detectors/editors.ts
|
|
975
|
+
var editorDetectors = [
|
|
976
|
+
defineDetector(
|
|
977
|
+
{ id: "vscode", name: "VS Code", category: "editor" },
|
|
978
|
+
(ctx) => ctx.hasUnder(".vscode") ? hit(Confidence.Confirmed, ".vscode/") : null
|
|
979
|
+
),
|
|
980
|
+
defineDetector({ id: "cursor", name: "Cursor", category: "ai" }, (ctx) => {
|
|
981
|
+
if (ctx.hasUnder(".cursor")) return hit(Confidence.Confirmed, ".cursor/");
|
|
982
|
+
if (ctx.has(".cursorrules")) return hit(Confidence.Confirmed, ".cursorrules");
|
|
983
|
+
return null;
|
|
984
|
+
}),
|
|
985
|
+
defineDetector({ id: "claude-code", name: "Claude Code", category: "ai" }, (ctx) => {
|
|
986
|
+
if (ctx.hasUnder(".claude")) return hit(Confidence.Confirmed, ".claude/");
|
|
987
|
+
if (ctx.has("CLAUDE.md")) return hit(Confidence.Confirmed, "CLAUDE.md");
|
|
988
|
+
return null;
|
|
989
|
+
}),
|
|
990
|
+
defineDetector({ id: "windsurf", name: "Windsurf", category: "ai" }, (ctx) => {
|
|
991
|
+
if (ctx.hasUnder(".windsurf")) return hit(Confidence.Confirmed, ".windsurf/");
|
|
992
|
+
if (ctx.has(".windsurfrules")) return hit(Confidence.Confirmed, ".windsurfrules");
|
|
993
|
+
return null;
|
|
994
|
+
})
|
|
995
|
+
];
|
|
996
|
+
|
|
997
|
+
// src/core/detection/detectors/jvm.ts
|
|
998
|
+
function match2(ctx, re) {
|
|
999
|
+
for (const p of ctx.present) if (re.test(p)) return p;
|
|
1000
|
+
return void 0;
|
|
1001
|
+
}
|
|
1002
|
+
var POM = /(^|\/)pom\.xml$/;
|
|
1003
|
+
var GRADLE = /(^|\/)(build|settings)\.gradle(\.kts)?$/;
|
|
1004
|
+
var APP_CONFIG = /(^|\/)src\/main\/resources\/application[^/]*\.(ya?ml|properties)$/;
|
|
1005
|
+
var jvmDetectors = [
|
|
1006
|
+
defineDetector({ id: "maven", name: "Maven", category: "jvm" }, (ctx) => {
|
|
1007
|
+
const pom = match2(ctx, POM);
|
|
1008
|
+
if (pom) return hit(Confidence.Confirmed, pom);
|
|
1009
|
+
if (ctx.has("mvnw") || ctx.has("mvnw.cmd")) return hit(Confidence.Strong, "mvnw");
|
|
1010
|
+
return null;
|
|
1011
|
+
}),
|
|
1012
|
+
defineDetector({ id: "gradle", name: "Gradle", category: "jvm" }, (ctx) => {
|
|
1013
|
+
const build = match2(ctx, GRADLE);
|
|
1014
|
+
if (build) return hit(Confidence.Confirmed, build);
|
|
1015
|
+
if (ctx.has("gradlew") || ctx.has("gradlew.bat")) return hit(Confidence.Strong, "gradlew");
|
|
1016
|
+
return null;
|
|
1017
|
+
}),
|
|
1018
|
+
defineDetector({ id: "spring-boot", name: "Spring Boot", category: "framework" }, (ctx) => {
|
|
1019
|
+
const appConfig = match2(ctx, APP_CONFIG);
|
|
1020
|
+
const hasBuild = Boolean(match2(ctx, POM) || match2(ctx, GRADLE));
|
|
1021
|
+
if (appConfig && hasBuild) return hit(Confidence.Strong, appConfig);
|
|
1022
|
+
if (appConfig) return hit(Confidence.Likely, appConfig);
|
|
1023
|
+
return null;
|
|
1024
|
+
})
|
|
1025
|
+
];
|
|
1026
|
+
|
|
1027
|
+
// src/core/detection/registry.ts
|
|
1028
|
+
var DETECTORS = [
|
|
1029
|
+
...packageManagerDetectors,
|
|
1030
|
+
...toolingDetectors,
|
|
1031
|
+
...editorDetectors,
|
|
1032
|
+
...jvmDetectors
|
|
1033
|
+
];
|
|
1034
|
+
var CATEGORY_ORDER = [
|
|
1035
|
+
"language",
|
|
1036
|
+
"framework",
|
|
1037
|
+
"jvm",
|
|
1038
|
+
"package-manager",
|
|
1039
|
+
"monorepo",
|
|
1040
|
+
"build",
|
|
1041
|
+
"lint",
|
|
1042
|
+
"format",
|
|
1043
|
+
"test",
|
|
1044
|
+
"container",
|
|
1045
|
+
"ci",
|
|
1046
|
+
"git-hooks",
|
|
1047
|
+
"commit",
|
|
1048
|
+
"devcontainer",
|
|
1049
|
+
"editor",
|
|
1050
|
+
"ai"
|
|
1051
|
+
];
|
|
1052
|
+
function sortDetections(list) {
|
|
1053
|
+
return [...list].sort((a, b) => {
|
|
1054
|
+
const ca = CATEGORY_ORDER.indexOf(a.category);
|
|
1055
|
+
const cb = CATEGORY_ORDER.indexOf(b.category);
|
|
1056
|
+
if (ca !== cb) return ca - cb;
|
|
1057
|
+
return a.name.localeCompare(b.name);
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
function runDetectors(ctx) {
|
|
1061
|
+
return DETECTORS.map((d) => d.detect(ctx)).filter((d) => d !== null);
|
|
1062
|
+
}
|
|
1063
|
+
async function detectStack(root, pkg, metadata) {
|
|
1064
|
+
const ctx = await gatherContext(root, pkg);
|
|
1065
|
+
const fromDetectors = runDetectors(ctx);
|
|
1066
|
+
const fromMetadata = metadata ? metadataDetections(metadata) : [];
|
|
1067
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const d of [...fromMetadata, ...fromDetectors]) {
|
|
1069
|
+
if (!byId.has(d.id)) byId.set(d.id, d);
|
|
1070
|
+
}
|
|
1071
|
+
return sortDetections([...byId.values()]);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
557
1074
|
// src/core/scanner.ts
|
|
558
1075
|
var FG_BASE_OPTIONS = {
|
|
559
1076
|
dot: true,
|
|
@@ -572,10 +1089,16 @@ function sanitizeNpmrc(content) {
|
|
|
572
1089
|
});
|
|
573
1090
|
return kept.join("\n");
|
|
574
1091
|
}
|
|
1092
|
+
async function readIncludePatterns(root) {
|
|
1093
|
+
const file = path5.join(root, INCLUDE_FILE);
|
|
1094
|
+
if (!await fs4.pathExists(file)) return [];
|
|
1095
|
+
const content = await fs4.readFile(file, "utf8");
|
|
1096
|
+
return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => line.endsWith("/") ? `${line}**` : line);
|
|
1097
|
+
}
|
|
575
1098
|
async function scanToolingFiles(root, ignore2) {
|
|
576
1099
|
const categoryOf = /* @__PURE__ */ new Map();
|
|
577
1100
|
for (const category of CONFIG_CATEGORIES) {
|
|
578
|
-
const found = await
|
|
1101
|
+
const found = await fg2(category.patterns, {
|
|
579
1102
|
cwd: root,
|
|
580
1103
|
onlyFiles: true,
|
|
581
1104
|
unique: true,
|
|
@@ -586,43 +1109,67 @@ async function scanToolingFiles(root, ignore2) {
|
|
|
586
1109
|
if (!categoryOf.has(norm)) categoryOf.set(norm, category.id);
|
|
587
1110
|
}
|
|
588
1111
|
}
|
|
1112
|
+
const includePatterns = await readIncludePatterns(root);
|
|
1113
|
+
const included = /* @__PURE__ */ new Set();
|
|
1114
|
+
if (includePatterns.length > 0) {
|
|
1115
|
+
const found = await fg2(includePatterns, {
|
|
1116
|
+
cwd: root,
|
|
1117
|
+
onlyFiles: true,
|
|
1118
|
+
unique: true,
|
|
1119
|
+
dot: true,
|
|
1120
|
+
followSymbolicLinks: false,
|
|
1121
|
+
suppressErrors: true,
|
|
1122
|
+
ignore: INCLUDE_PRUNE_GLOBS
|
|
1123
|
+
});
|
|
1124
|
+
for (const rel of found) {
|
|
1125
|
+
const norm = toPosix(rel);
|
|
1126
|
+
if (!categoryOf.has(norm)) included.add(norm);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const candidates = [
|
|
1130
|
+
...[...categoryOf.entries()].map(
|
|
1131
|
+
([rel, category]) => ({ rel, source: "catalogue", category })
|
|
1132
|
+
),
|
|
1133
|
+
...[...included].map((rel) => ({ rel, source: "include", category: "included" }))
|
|
1134
|
+
].sort((a, b) => a.rel.localeCompare(b.rel));
|
|
589
1135
|
const files = [];
|
|
590
1136
|
const skippedSecrets = [];
|
|
591
|
-
for (const rel of
|
|
1137
|
+
for (const { rel, source, category } of candidates) {
|
|
592
1138
|
if (rel === "package.json") continue;
|
|
593
1139
|
if (ignore2.isSecret(rel)) {
|
|
594
1140
|
skippedSecrets.push(rel);
|
|
595
1141
|
logger.detail(`skipped (secret guard): ${rel}`);
|
|
596
1142
|
continue;
|
|
597
1143
|
}
|
|
598
|
-
|
|
1144
|
+
const excluded = source === "include" ? ignore2.isUserIgnored(rel) : ignore2.isIgnored(rel);
|
|
1145
|
+
if (excluded) {
|
|
599
1146
|
logger.detail(`skipped (.replicaxignore): ${rel}`);
|
|
600
1147
|
continue;
|
|
601
1148
|
}
|
|
602
|
-
const abs =
|
|
1149
|
+
const abs = path5.join(root, rel);
|
|
603
1150
|
let stat;
|
|
604
1151
|
try {
|
|
605
|
-
stat = await
|
|
1152
|
+
stat = await fs4.stat(abs);
|
|
606
1153
|
} catch {
|
|
607
1154
|
continue;
|
|
608
1155
|
}
|
|
609
1156
|
if (!stat.isFile()) continue;
|
|
610
|
-
let content = await
|
|
611
|
-
if (
|
|
1157
|
+
let content = await fs4.readFile(abs, "utf8");
|
|
1158
|
+
if (path5.basename(rel) === ".npmrc") content = sanitizeNpmrc(content);
|
|
612
1159
|
files.push({
|
|
613
1160
|
path: rel,
|
|
614
|
-
category
|
|
1161
|
+
category,
|
|
615
1162
|
variant: detectVariant(rel),
|
|
616
1163
|
encoding: "utf8",
|
|
617
1164
|
content,
|
|
618
1165
|
bytes: Buffer.byteLength(content, "utf8")
|
|
619
1166
|
});
|
|
620
|
-
logger.detail(`captured: ${rel}`);
|
|
1167
|
+
logger.detail(`captured${source === "include" ? " (include)" : ""}: ${rel}`);
|
|
621
1168
|
}
|
|
622
1169
|
return { files, skippedSecrets };
|
|
623
1170
|
}
|
|
624
1171
|
async function scanStructure(root, ignore2) {
|
|
625
|
-
const dirs = await
|
|
1172
|
+
const dirs = await fg2("**", {
|
|
626
1173
|
cwd: root,
|
|
627
1174
|
onlyDirectories: true,
|
|
628
1175
|
unique: true,
|
|
@@ -630,13 +1177,13 @@ async function scanStructure(root, ignore2) {
|
|
|
630
1177
|
});
|
|
631
1178
|
const directories = dirs.map(toPosix).filter((d) => d.length > 0 && d !== ".").filter((d) => !ignore2.isIgnored(d)).sort();
|
|
632
1179
|
return {
|
|
633
|
-
root:
|
|
1180
|
+
root: path5.basename(path5.resolve(root)) || "project",
|
|
634
1181
|
directories
|
|
635
1182
|
};
|
|
636
1183
|
}
|
|
637
1184
|
async function scanProject(root) {
|
|
638
|
-
const resolved =
|
|
639
|
-
if (!await
|
|
1185
|
+
const resolved = path5.resolve(root);
|
|
1186
|
+
if (!await fs4.pathExists(resolved)) {
|
|
640
1187
|
throw new Error(`Directory does not exist: ${resolved}`);
|
|
641
1188
|
}
|
|
642
1189
|
const ignore2 = await IgnoreEngine.fromProject(resolved);
|
|
@@ -646,11 +1193,13 @@ async function scanProject(root) {
|
|
|
646
1193
|
scanStructure(resolved, ignore2),
|
|
647
1194
|
detectMetadata(resolved, pkg)
|
|
648
1195
|
]);
|
|
1196
|
+
const detections = await detectStack(resolved, pkg, metadata);
|
|
1197
|
+
metadata.detections = detections;
|
|
649
1198
|
const tooling = {
|
|
650
1199
|
files,
|
|
651
1200
|
packageJson: buildPackageTemplate(pkg)
|
|
652
1201
|
};
|
|
653
|
-
return { tooling, structure, metadata, pkg, skippedSecrets };
|
|
1202
|
+
return { tooling, structure, metadata, pkg, detections, skippedSecrets };
|
|
654
1203
|
}
|
|
655
1204
|
|
|
656
1205
|
// src/core/checksum.ts
|
|
@@ -688,6 +1237,34 @@ function verifyChecksum(tooling, stored) {
|
|
|
688
1237
|
return mismatches;
|
|
689
1238
|
}
|
|
690
1239
|
|
|
1240
|
+
// src/core/manifest.ts
|
|
1241
|
+
function buildManifest(tooling, checksum) {
|
|
1242
|
+
const entries = tooling.files.map((file) => ({
|
|
1243
|
+
path: file.path,
|
|
1244
|
+
category: file.category,
|
|
1245
|
+
variant: file.variant,
|
|
1246
|
+
bytes: file.bytes,
|
|
1247
|
+
sha256: checksum.files[file.path] ?? ""
|
|
1248
|
+
}));
|
|
1249
|
+
if (tooling.packageJson) {
|
|
1250
|
+
entries.push({
|
|
1251
|
+
path: PACKAGE_JSON_KEY,
|
|
1252
|
+
category: "package",
|
|
1253
|
+
variant: "json",
|
|
1254
|
+
// The curated template is a derived artifact, not a captured file on disk,
|
|
1255
|
+
// so byte size is not meaningful — recorded as 0.
|
|
1256
|
+
bytes: 0,
|
|
1257
|
+
sha256: checksum.files[PACKAGE_JSON_KEY] ?? ""
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
1261
|
+
return {
|
|
1262
|
+
schemaVersion: REPLICAX_VERSION,
|
|
1263
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1264
|
+
entries
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
691
1268
|
// src/core/profile-generator.ts
|
|
692
1269
|
function buildBundle(args) {
|
|
693
1270
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -696,36 +1273,56 @@ function buildBundle(args) {
|
|
|
696
1273
|
name: args.name,
|
|
697
1274
|
description: args.description ?? args.existing.description,
|
|
698
1275
|
replicaxVersion: REPLICAX_VERSION,
|
|
699
|
-
updatedAt: now
|
|
1276
|
+
updatedAt: now,
|
|
1277
|
+
// Preserve the original provenance on sync unless explicitly overridden.
|
|
1278
|
+
...args.source ?? args.existing.source ? { source: args.source ?? args.existing.source } : {}
|
|
700
1279
|
} : {
|
|
701
1280
|
name: args.name,
|
|
702
1281
|
version: "1.0.0",
|
|
703
1282
|
createdAt: now,
|
|
704
1283
|
replicaxVersion: REPLICAX_VERSION,
|
|
705
|
-
...args.description ? { description: args.description } : {}
|
|
1284
|
+
...args.description ? { description: args.description } : {},
|
|
1285
|
+
...args.source ? { source: args.source } : {}
|
|
706
1286
|
};
|
|
1287
|
+
const checksum = computeChecksum(args.tooling);
|
|
707
1288
|
return {
|
|
708
1289
|
profile,
|
|
709
1290
|
tooling: args.tooling,
|
|
710
1291
|
structure: args.structure,
|
|
711
1292
|
metadata: args.metadata,
|
|
712
|
-
checksum
|
|
1293
|
+
checksum,
|
|
1294
|
+
manifest: buildManifest(args.tooling, checksum)
|
|
713
1295
|
};
|
|
714
1296
|
}
|
|
715
1297
|
|
|
716
1298
|
// src/core/profile-store.ts
|
|
717
|
-
import
|
|
718
|
-
import
|
|
1299
|
+
import path6 from "path";
|
|
1300
|
+
import fs5 from "fs-extra";
|
|
719
1301
|
|
|
720
1302
|
// src/schema.ts
|
|
721
1303
|
import { z } from "zod";
|
|
1304
|
+
var RegistrySchema = z.object({
|
|
1305
|
+
/** Stable identifier within a registry, e.g. "acme/react-enterprise". */
|
|
1306
|
+
id: z.string().optional(),
|
|
1307
|
+
/** Owning namespace/org. */
|
|
1308
|
+
namespace: z.string().optional(),
|
|
1309
|
+
/** Intended visibility once published. */
|
|
1310
|
+
visibility: z.enum(["public", "private"]).optional(),
|
|
1311
|
+
/** Where the profile originated (URL, registry name, …). */
|
|
1312
|
+
source: z.string().optional()
|
|
1313
|
+
});
|
|
1314
|
+
var ProfileSourceSchema = z.enum(["local", "github", "import"]);
|
|
722
1315
|
var ProfileSchema = z.object({
|
|
723
1316
|
name: z.string().min(1),
|
|
724
1317
|
version: z.string().min(1),
|
|
725
1318
|
createdAt: z.string().min(1),
|
|
726
1319
|
updatedAt: z.string().optional(),
|
|
727
1320
|
replicaxVersion: z.string().min(1),
|
|
728
|
-
description: z.string().optional()
|
|
1321
|
+
description: z.string().optional(),
|
|
1322
|
+
/** Provenance of the captured setup (added in schema 2.2.0). */
|
|
1323
|
+
source: ProfileSourceSchema.optional(),
|
|
1324
|
+
/** Optional registry metadata (future registry compatibility). */
|
|
1325
|
+
registry: RegistrySchema.optional()
|
|
729
1326
|
});
|
|
730
1327
|
var FileVariantSchema = z.enum(["ts", "js", "mjs", "cjs", "json", "yaml", "other"]);
|
|
731
1328
|
var ToolingFileSchema = z.object({
|
|
@@ -759,32 +1356,121 @@ var StructureSchema = z.object({
|
|
|
759
1356
|
root: z.string(),
|
|
760
1357
|
directories: z.array(z.string())
|
|
761
1358
|
});
|
|
1359
|
+
var DetectionCategorySchema = z.enum([
|
|
1360
|
+
"language",
|
|
1361
|
+
"framework",
|
|
1362
|
+
"package-manager",
|
|
1363
|
+
"monorepo",
|
|
1364
|
+
"container",
|
|
1365
|
+
"ci",
|
|
1366
|
+
"git-hooks",
|
|
1367
|
+
"commit",
|
|
1368
|
+
"lint",
|
|
1369
|
+
"format",
|
|
1370
|
+
"test",
|
|
1371
|
+
"build",
|
|
1372
|
+
"editor",
|
|
1373
|
+
"ai",
|
|
1374
|
+
"devcontainer",
|
|
1375
|
+
"jvm"
|
|
1376
|
+
]);
|
|
1377
|
+
var DetectionSchema = z.object({
|
|
1378
|
+
/** Stable id, e.g. "docker", "github-actions". */
|
|
1379
|
+
id: z.string().min(1),
|
|
1380
|
+
/** Human-friendly label, e.g. "Docker". */
|
|
1381
|
+
name: z.string().min(1),
|
|
1382
|
+
category: DetectionCategorySchema,
|
|
1383
|
+
/** 0..1 — how sure we are this tool is in use. */
|
|
1384
|
+
confidence: z.number().min(0).max(1),
|
|
1385
|
+
/** Paths/fields that justify the detection (e.g. ["Dockerfile"]). */
|
|
1386
|
+
evidence: z.array(z.string()).default([])
|
|
1387
|
+
});
|
|
762
1388
|
var MetadataSchema = z.object({
|
|
763
1389
|
nodeVersion: z.string(),
|
|
764
1390
|
packageManager: z.enum(["npm", "yarn", "pnpm", "bun", "unknown"]),
|
|
765
1391
|
framework: z.string(),
|
|
766
|
-
language: z.enum(["typescript", "javascript"]),
|
|
767
|
-
platform: z.string()
|
|
1392
|
+
language: z.enum(["typescript", "javascript", "java", "unknown"]),
|
|
1393
|
+
platform: z.string(),
|
|
1394
|
+
/** Detected tools/technologies with confidence (added in schema 2.1.0). */
|
|
1395
|
+
detections: z.array(DetectionSchema).optional()
|
|
768
1396
|
});
|
|
769
1397
|
var ChecksumSchema = z.object({
|
|
770
1398
|
algorithm: z.literal("sha256"),
|
|
771
1399
|
files: z.record(z.string(), z.string())
|
|
772
1400
|
});
|
|
1401
|
+
var ManifestEntrySchema = z.object({
|
|
1402
|
+
path: z.string().min(1),
|
|
1403
|
+
category: z.string().min(1),
|
|
1404
|
+
variant: FileVariantSchema,
|
|
1405
|
+
bytes: z.number().int().nonnegative(),
|
|
1406
|
+
sha256: z.string()
|
|
1407
|
+
});
|
|
1408
|
+
var ManifestSchema = z.object({
|
|
1409
|
+
schemaVersion: z.string().min(1),
|
|
1410
|
+
generatedAt: z.string().min(1),
|
|
1411
|
+
entries: z.array(ManifestEntrySchema)
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
// src/core/migrations.ts
|
|
1415
|
+
var MIGRATIONS = [
|
|
1416
|
+
{
|
|
1417
|
+
from: "2.0.0",
|
|
1418
|
+
to: "2.1.0",
|
|
1419
|
+
apply(raw) {
|
|
1420
|
+
const metadata = raw.metadata;
|
|
1421
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata.detections)) {
|
|
1422
|
+
metadata.detections = [];
|
|
1423
|
+
}
|
|
1424
|
+
return raw;
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
from: "2.1.0",
|
|
1429
|
+
to: "2.2.0",
|
|
1430
|
+
apply(raw) {
|
|
1431
|
+
return raw;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
];
|
|
1435
|
+
var KNOWN_VERSIONS = /* @__PURE__ */ new Set([
|
|
1436
|
+
REPLICAX_VERSION,
|
|
1437
|
+
...MIGRATIONS.flatMap((m) => [m.from, m.to])
|
|
1438
|
+
]);
|
|
1439
|
+
function migrateRawBundle(raw, detectedVersion) {
|
|
1440
|
+
const steps = [];
|
|
1441
|
+
let current = detectedVersion;
|
|
1442
|
+
let data = raw;
|
|
1443
|
+
for (let guard = 0; guard < MIGRATIONS.length + 1; guard += 1) {
|
|
1444
|
+
if (current === REPLICAX_VERSION) break;
|
|
1445
|
+
const next = MIGRATIONS.find((m) => m.from === current);
|
|
1446
|
+
if (!next) break;
|
|
1447
|
+
data = next.apply(data);
|
|
1448
|
+
steps.push(`${next.from} \u2192 ${next.to}`);
|
|
1449
|
+
current = next.to;
|
|
1450
|
+
}
|
|
1451
|
+
return {
|
|
1452
|
+
raw: data,
|
|
1453
|
+
from: detectedVersion,
|
|
1454
|
+
to: current,
|
|
1455
|
+
migrated: steps.length > 0,
|
|
1456
|
+
steps
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
773
1459
|
|
|
774
1460
|
// src/core/profile-store.ts
|
|
775
1461
|
function profileDir(root) {
|
|
776
|
-
return
|
|
1462
|
+
return path6.join(path6.resolve(root), REPLICAX_DIR);
|
|
777
1463
|
}
|
|
778
1464
|
async function profileExists(dir) {
|
|
779
|
-
return
|
|
1465
|
+
return fs5.pathExists(path6.join(dir, PROFILE_FILES.profile));
|
|
780
1466
|
}
|
|
781
1467
|
async function resolveProfileDir(input) {
|
|
782
|
-
const resolved =
|
|
783
|
-
if (!await
|
|
1468
|
+
const resolved = path6.resolve(input);
|
|
1469
|
+
if (!await fs5.pathExists(resolved)) {
|
|
784
1470
|
throw new ReplicaxError(`Profile path not found: ${input}`);
|
|
785
1471
|
}
|
|
786
1472
|
if (await profileExists(resolved)) return resolved;
|
|
787
|
-
const nested =
|
|
1473
|
+
const nested = path6.join(resolved, REPLICAX_DIR);
|
|
788
1474
|
if (await profileExists(nested)) return nested;
|
|
789
1475
|
throw new ReplicaxError(`No ReplicaX profile found at: ${input}`, [
|
|
790
1476
|
`Looked for ${PROFILE_FILES.profile} in ${resolved} and ${nested}.`,
|
|
@@ -792,26 +1478,29 @@ async function resolveProfileDir(input) {
|
|
|
792
1478
|
]);
|
|
793
1479
|
}
|
|
794
1480
|
async function saveBundle(dir, bundle) {
|
|
795
|
-
await
|
|
1481
|
+
await fs5.ensureDir(dir);
|
|
1482
|
+
const manifest = bundle.manifest ?? buildManifest(bundle.tooling, bundle.checksum);
|
|
796
1483
|
await Promise.all([
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1484
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.profile), bundle.profile, { spaces: 2 }),
|
|
1485
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.tooling), bundle.tooling, { spaces: 2 }),
|
|
1486
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.structure), bundle.structure, { spaces: 2 }),
|
|
1487
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.metadata), bundle.metadata, { spaces: 2 }),
|
|
1488
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.checksum), bundle.checksum, { spaces: 2 }),
|
|
1489
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.manifest), manifest, { spaces: 2 })
|
|
802
1490
|
]);
|
|
803
1491
|
}
|
|
804
|
-
async function
|
|
805
|
-
const full =
|
|
806
|
-
if (!await
|
|
1492
|
+
async function readRawFile(dir, file) {
|
|
1493
|
+
const full = path6.join(dir, file);
|
|
1494
|
+
if (!await fs5.pathExists(full)) {
|
|
807
1495
|
throw new ReplicaxError(`Profile is missing ${file}`, [`Expected at ${full}.`]);
|
|
808
1496
|
}
|
|
809
|
-
let raw;
|
|
810
1497
|
try {
|
|
811
|
-
|
|
1498
|
+
return await fs5.readJson(full);
|
|
812
1499
|
} catch {
|
|
813
1500
|
throw new ReplicaxError(`Profile file ${file} is not valid JSON`, [`Path: ${full}`]);
|
|
814
1501
|
}
|
|
1502
|
+
}
|
|
1503
|
+
function parseFile(file, schema, raw) {
|
|
815
1504
|
const result = schema.safeParse(raw);
|
|
816
1505
|
if (!result.success) {
|
|
817
1506
|
const issues = result.error.issues.slice(0, 5).map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
@@ -825,17 +1514,35 @@ async function loadBundle(dir) {
|
|
|
825
1514
|
"Run `replicax init` to create one."
|
|
826
1515
|
]);
|
|
827
1516
|
}
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1517
|
+
const rawFiles = {
|
|
1518
|
+
profile: await readRawFile(dir, PROFILE_FILES.profile),
|
|
1519
|
+
tooling: await readRawFile(dir, PROFILE_FILES.tooling),
|
|
1520
|
+
structure: await readRawFile(dir, PROFILE_FILES.structure),
|
|
1521
|
+
metadata: await readRawFile(dir, PROFILE_FILES.metadata),
|
|
1522
|
+
checksum: await readRawFile(dir, PROFILE_FILES.checksum)
|
|
1523
|
+
};
|
|
1524
|
+
const detectedVersion = typeof rawFiles.profile.replicaxVersion === "string" ? rawFiles.profile.replicaxVersion : "2.0.0";
|
|
1525
|
+
const { raw } = migrateRawBundle(rawFiles, detectedVersion);
|
|
1526
|
+
const profile = parseFile(PROFILE_FILES.profile, ProfileSchema, raw.profile);
|
|
1527
|
+
const tooling = parseFile(PROFILE_FILES.tooling, ToolingSchema, raw.tooling);
|
|
1528
|
+
const structure = parseFile(PROFILE_FILES.structure, StructureSchema, raw.structure);
|
|
1529
|
+
const metadata = parseFile(PROFILE_FILES.metadata, MetadataSchema, raw.metadata);
|
|
1530
|
+
const checksum = parseFile(PROFILE_FILES.checksum, ChecksumSchema, raw.checksum);
|
|
1531
|
+
const manifestPath = path6.join(dir, PROFILE_FILES.manifest);
|
|
1532
|
+
const manifest = await fs5.pathExists(manifestPath) ? parseFile(
|
|
1533
|
+
PROFILE_FILES.manifest,
|
|
1534
|
+
ManifestSchema,
|
|
1535
|
+
await readRawFile(dir, PROFILE_FILES.manifest)
|
|
1536
|
+
) : buildManifest(tooling, checksum);
|
|
1537
|
+
return { profile, tooling, structure, metadata, checksum, manifest };
|
|
836
1538
|
}
|
|
837
1539
|
|
|
838
1540
|
// src/commands/report.ts
|
|
1541
|
+
function statusLine(ok, label, note) {
|
|
1542
|
+
const mark = ok ? pc.green("\u2713") : pc.red("\u2717");
|
|
1543
|
+
const text = ok ? label : pc.dim(label);
|
|
1544
|
+
return note ? `${mark} ${text} ${pc.dim(note)}` : `${mark} ${text}`;
|
|
1545
|
+
}
|
|
839
1546
|
function toolingByCategory(tooling) {
|
|
840
1547
|
const counts = /* @__PURE__ */ new Map();
|
|
841
1548
|
for (const file of tooling.files) {
|
|
@@ -844,7 +1551,7 @@ function toolingByCategory(tooling) {
|
|
|
844
1551
|
if (tooling.packageJson) {
|
|
845
1552
|
counts.set("package", (counts.get("package") ?? 0) + 1);
|
|
846
1553
|
}
|
|
847
|
-
return [...counts.entries()].map(([id, n]) => [
|
|
1554
|
+
return [...counts.entries()].map(([id, n]) => [categoryLabel(id), n]).sort((a, b) => a[0].localeCompare(b[0]));
|
|
848
1555
|
}
|
|
849
1556
|
function printScanSummary(bundle) {
|
|
850
1557
|
const { metadata, tooling, structure } = bundle;
|
|
@@ -854,6 +1561,7 @@ function printScanSummary(bundle) {
|
|
|
854
1561
|
logger.hint(`framework ${metadata.framework}`);
|
|
855
1562
|
logger.hint(`packageManager ${metadata.packageManager}`);
|
|
856
1563
|
logger.hint(`nodeVersion ${metadata.nodeVersion}`);
|
|
1564
|
+
printDetections(metadata.detections ?? []);
|
|
857
1565
|
logger.newline();
|
|
858
1566
|
logger.info(pc.bold(`Tooling (${tooling.files.length + (tooling.packageJson ? 1 : 0)} files)`));
|
|
859
1567
|
for (const [label, count] of toolingByCategory(tooling)) {
|
|
@@ -862,6 +1570,15 @@ function printScanSummary(bundle) {
|
|
|
862
1570
|
logger.newline();
|
|
863
1571
|
logger.info(pc.bold(`Structure (${structure.directories.length} directories)`));
|
|
864
1572
|
}
|
|
1573
|
+
function printDetections(detections) {
|
|
1574
|
+
if (detections.length === 0) return;
|
|
1575
|
+
logger.newline();
|
|
1576
|
+
logger.info(pc.bold(`Detected (${detections.length})`));
|
|
1577
|
+
for (const d of detections) {
|
|
1578
|
+
const pct = d.confidence < 1 ? pc.dim(` (${Math.round(d.confidence * 100)}%)`) : "";
|
|
1579
|
+
logger.hint(`${pc.green("\u2713")} ${d.name}${pct}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
865
1582
|
function reportSkippedSecrets(skipped) {
|
|
866
1583
|
if (skipped.length === 0) return;
|
|
867
1584
|
logger.warn(`Excluded ${skipped.length} protected file(s) from the profile:`);
|
|
@@ -910,12 +1627,13 @@ async function initCommand(options) {
|
|
|
910
1627
|
spinner.succeed(
|
|
911
1628
|
`Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
|
|
912
1629
|
);
|
|
913
|
-
const name = options.name ??
|
|
1630
|
+
const name = options.name ?? path7.basename(path7.resolve(root)) ?? "project";
|
|
914
1631
|
const bundle = buildBundle({
|
|
915
1632
|
name,
|
|
916
1633
|
tooling: scan.tooling,
|
|
917
1634
|
structure: scan.structure,
|
|
918
|
-
metadata: scan.metadata
|
|
1635
|
+
metadata: scan.metadata,
|
|
1636
|
+
source: "local"
|
|
919
1637
|
});
|
|
920
1638
|
reportSkippedSecrets(scan.skippedSecrets);
|
|
921
1639
|
printScanSummary(bundle);
|
|
@@ -935,21 +1653,21 @@ async function initCommand(options) {
|
|
|
935
1653
|
logger.hint("Create a project from it with: replicax create <project-name>");
|
|
936
1654
|
}
|
|
937
1655
|
async function maybeWriteIgnoreFile(root) {
|
|
938
|
-
const file =
|
|
939
|
-
if (await
|
|
1656
|
+
const file = path7.join(root, IGNORE_FILE);
|
|
1657
|
+
if (await fs6.pathExists(file)) return;
|
|
940
1658
|
const create = process.stdin.isTTY ? await confirm({
|
|
941
1659
|
message: `Create a starter ${IGNORE_FILE} to control what gets exported?`,
|
|
942
1660
|
default: true
|
|
943
1661
|
}) : false;
|
|
944
1662
|
if (create) {
|
|
945
|
-
await
|
|
1663
|
+
await fs6.writeFile(file, DEFAULT_IGNORE_FILE_CONTENTS, "utf8");
|
|
946
1664
|
logger.success(`Wrote ${IGNORE_FILE}`);
|
|
947
1665
|
}
|
|
948
1666
|
}
|
|
949
1667
|
|
|
950
1668
|
// src/commands/init-skill.ts
|
|
951
|
-
import
|
|
952
|
-
import
|
|
1669
|
+
import path8 from "path";
|
|
1670
|
+
import fs7 from "fs-extra";
|
|
953
1671
|
import ora2 from "ora";
|
|
954
1672
|
|
|
955
1673
|
// src/config/ai-targets.ts
|
|
@@ -983,7 +1701,7 @@ function slugify(input, fallback = "project") {
|
|
|
983
1701
|
}
|
|
984
1702
|
|
|
985
1703
|
// src/core/skill-generator.ts
|
|
986
|
-
var
|
|
1704
|
+
var FRAMEWORK_LABELS2 = {
|
|
987
1705
|
next: "Next.js",
|
|
988
1706
|
nuxt: "Nuxt",
|
|
989
1707
|
remix: "Remix",
|
|
@@ -1047,7 +1765,7 @@ function orderedScripts(scripts) {
|
|
|
1047
1765
|
function toolingByCategoryLabel(tooling) {
|
|
1048
1766
|
const groups = /* @__PURE__ */ new Map();
|
|
1049
1767
|
for (const file of tooling.files) {
|
|
1050
|
-
const label =
|
|
1768
|
+
const label = categoryLabel(file.category);
|
|
1051
1769
|
const list = groups.get(label) ?? [];
|
|
1052
1770
|
list.push(file.path);
|
|
1053
1771
|
groups.set(label, list);
|
|
@@ -1118,7 +1836,7 @@ function buildSkill(args) {
|
|
|
1118
1836
|
const { name, metadata, tooling, structure, pkg } = args;
|
|
1119
1837
|
const slug = slugify(name);
|
|
1120
1838
|
const pm = metadata.packageManager;
|
|
1121
|
-
const framework =
|
|
1839
|
+
const framework = FRAMEWORK_LABELS2[metadata.framework] ?? metadata.framework;
|
|
1122
1840
|
const language = metadata.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
1123
1841
|
const scripts = pkg?.scripts ?? {};
|
|
1124
1842
|
const description = `${name} project: ${framework}/${language} setup, build/test commands, and tooling conventions. Use this skill when working in or scaffolding this codebase.`;
|
|
@@ -1186,6 +1904,9 @@ function buildSkill(args) {
|
|
|
1186
1904
|
}
|
|
1187
1905
|
|
|
1188
1906
|
// src/core/ai/cli.ts
|
|
1907
|
+
import { spawn as spawn2 } from "child_process";
|
|
1908
|
+
|
|
1909
|
+
// src/core/process.ts
|
|
1189
1910
|
import { spawn } from "child_process";
|
|
1190
1911
|
async function commandExists(bin) {
|
|
1191
1912
|
const onWindows = process.platform === "win32";
|
|
@@ -1202,9 +1923,45 @@ async function commandExists(bin) {
|
|
|
1202
1923
|
child.on("close", (code) => resolve(code === 0));
|
|
1203
1924
|
});
|
|
1204
1925
|
}
|
|
1926
|
+
async function getCommandOutput(bin, args = [], options = {}) {
|
|
1927
|
+
const { timeoutMs = 5e3, shell = false } = options;
|
|
1928
|
+
return new Promise((resolve) => {
|
|
1929
|
+
let settled = false;
|
|
1930
|
+
const finish = (result) => {
|
|
1931
|
+
if (settled) return;
|
|
1932
|
+
settled = true;
|
|
1933
|
+
resolve(result);
|
|
1934
|
+
};
|
|
1935
|
+
let child;
|
|
1936
|
+
try {
|
|
1937
|
+
child = spawn(bin, args, { shell, windowsHide: true });
|
|
1938
|
+
} catch {
|
|
1939
|
+
finish({ ok: false, stdout: "", stderr: "", code: null });
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
let stdout = "";
|
|
1943
|
+
let stderr = "";
|
|
1944
|
+
const timer = setTimeout(() => {
|
|
1945
|
+
child.kill();
|
|
1946
|
+
finish({ ok: false, stdout, stderr, code: null });
|
|
1947
|
+
}, timeoutMs);
|
|
1948
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
1949
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
1950
|
+
child.on("error", () => {
|
|
1951
|
+
clearTimeout(timer);
|
|
1952
|
+
finish({ ok: false, stdout, stderr, code: null });
|
|
1953
|
+
});
|
|
1954
|
+
child.on("close", (code) => {
|
|
1955
|
+
clearTimeout(timer);
|
|
1956
|
+
finish({ ok: code === 0, stdout, stderr, code });
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// src/core/ai/cli.ts
|
|
1205
1962
|
async function runWithStdin(bin, args, input, timeoutMs = 12e4) {
|
|
1206
1963
|
return new Promise((resolve, reject) => {
|
|
1207
|
-
const child =
|
|
1964
|
+
const child = spawn2(bin, args, { shell: true, windowsHide: true });
|
|
1208
1965
|
let stdout = "";
|
|
1209
1966
|
let stderr = "";
|
|
1210
1967
|
const timer = setTimeout(() => {
|
|
@@ -1230,6 +1987,14 @@ async function runWithStdin(bin, args, input, timeoutMs = 12e4) {
|
|
|
1230
1987
|
}
|
|
1231
1988
|
|
|
1232
1989
|
// src/core/ai/providers.ts
|
|
1990
|
+
var ApiHttpError = class extends Error {
|
|
1991
|
+
constructor(status, message) {
|
|
1992
|
+
super(message);
|
|
1993
|
+
this.status = status;
|
|
1994
|
+
this.name = "ApiHttpError";
|
|
1995
|
+
}
|
|
1996
|
+
status;
|
|
1997
|
+
};
|
|
1233
1998
|
async function postJson(url, headers, body) {
|
|
1234
1999
|
const res = await fetch(url, {
|
|
1235
2000
|
method: "POST",
|
|
@@ -1238,10 +2003,33 @@ async function postJson(url, headers, body) {
|
|
|
1238
2003
|
});
|
|
1239
2004
|
if (!res.ok) {
|
|
1240
2005
|
const text = await res.text().catch(() => "");
|
|
1241
|
-
throw new
|
|
2006
|
+
throw new ApiHttpError(res.status, text.slice(0, 300) || res.statusText);
|
|
1242
2007
|
}
|
|
1243
2008
|
return res.json();
|
|
1244
2009
|
}
|
|
2010
|
+
function enrichProviderError(err, def, model) {
|
|
2011
|
+
const status = err instanceof ApiHttpError ? err.status : void 0;
|
|
2012
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2013
|
+
const overrideHint = `Override the model with --model <id> or ${def.modelEnvVar}=<id>.`;
|
|
2014
|
+
if (status === 404 || status === 400) {
|
|
2015
|
+
return new ReplicaxError(
|
|
2016
|
+
`${def.label} API rejected model "${model}" (HTTP ${status}). ${overrideHint}`,
|
|
2017
|
+
[`Provider response: ${detail}`]
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
if (status === 401 || status === 403) {
|
|
2021
|
+
return new ReplicaxError(`${def.label} API rejected the credentials (HTTP ${status}).`, [
|
|
2022
|
+
`Check ${def.apiEnvVars[0]} holds a valid API key.`,
|
|
2023
|
+
`Provider response: ${detail}`
|
|
2024
|
+
]);
|
|
2025
|
+
}
|
|
2026
|
+
if (status === 429) {
|
|
2027
|
+
return new ReplicaxError(`${def.label} API rate limit hit (HTTP 429).`, [
|
|
2028
|
+
"Wait a moment and try again."
|
|
2029
|
+
]);
|
|
2030
|
+
}
|
|
2031
|
+
return new ReplicaxError(`${def.label} API request failed: ${detail}`);
|
|
2032
|
+
}
|
|
1245
2033
|
async function callAnthropic(prompt, apiKey, model) {
|
|
1246
2034
|
const data = await postJson(
|
|
1247
2035
|
"https://api.anthropic.com/v1/messages",
|
|
@@ -1318,7 +2106,13 @@ function apiInvoker(def, apiKey, modelOverride) {
|
|
|
1318
2106
|
return {
|
|
1319
2107
|
id: def.id,
|
|
1320
2108
|
via: `${def.label} API (${model})`,
|
|
1321
|
-
run: (prompt) =>
|
|
2109
|
+
run: async (prompt) => {
|
|
2110
|
+
try {
|
|
2111
|
+
return await def.callApi(prompt, apiKey, model);
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
throw enrichProviderError(err, def, model);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1322
2116
|
};
|
|
1323
2117
|
}
|
|
1324
2118
|
async function resolveProvider(preference, modelOverride) {
|
|
@@ -1348,6 +2142,12 @@ async function resolveProvider(preference, modelOverride) {
|
|
|
1348
2142
|
function buildSkillPrompt(args) {
|
|
1349
2143
|
const scripts = Object.entries(args.scripts).map(([name, cmd]) => ` ${name}: ${cmd}`).join("\n");
|
|
1350
2144
|
const tooling = args.toolingPaths.map((p) => ` ${p}`).join("\n");
|
|
2145
|
+
const template = args.rootSkill?.trim();
|
|
2146
|
+
const templateRule = template ? "\n- A USER SKILL TEMPLATE (the project's root SKILL.md) is provided below. Use it as the BASE for the entry file: preserve its headings, structure, tone, and any explicit instructions, and refine/fill it in using the PROJECT ANALYSIS. Do not drop content the author put there; do not contradict it." : "";
|
|
2147
|
+
const templateSection = template ? `
|
|
2148
|
+
USER SKILL TEMPLATE (project root SKILL.md \u2014 use this as the base; preserve and refine, keep "name: ${args.slug}" in the frontmatter):
|
|
2149
|
+
${template}
|
|
2150
|
+
` : "";
|
|
1351
2151
|
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
2152
|
|
|
1353
2153
|
STRICT RULES:
|
|
@@ -1361,11 +2161,11 @@ REQUIREMENTS:
|
|
|
1361
2161
|
- 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
2162
|
- You MAY add a few supporting files under "references/" (e.g. "references/commands.md") when genuinely useful. Keep the bundle small and focused.
|
|
1363
2163
|
- 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}
|
|
2164
|
+
- This skill targets ${args.target.label} and will be installed at ${args.entryPath}.${templateRule}
|
|
1365
2165
|
|
|
1366
2166
|
PROJECT ANALYSIS (ground truth \u2014 refine and expand this, do not contradict it):
|
|
1367
2167
|
${args.analysis}
|
|
1368
|
-
|
|
2168
|
+
${templateSection}
|
|
1369
2169
|
CAPTURED CONFIG FILES:
|
|
1370
2170
|
${tooling || " (none)"}
|
|
1371
2171
|
|
|
@@ -1431,6 +2231,8 @@ async function initSkillCommand(options) {
|
|
|
1431
2231
|
spinner.succeed(
|
|
1432
2232
|
`Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
|
|
1433
2233
|
);
|
|
2234
|
+
const rootSkillPath = path8.join(root, ROOT_SKILL_FILE);
|
|
2235
|
+
const rootSkill = await fs7.pathExists(rootSkillPath) ? await fs7.readFile(rootSkillPath, "utf8") : void 0;
|
|
1434
2236
|
const name = options.name ?? scan.structure.root;
|
|
1435
2237
|
const seed = buildSkill({
|
|
1436
2238
|
name,
|
|
@@ -1458,6 +2260,7 @@ async function initSkillCommand(options) {
|
|
|
1458
2260
|
const provider = await resolveProvider(options.provider, options.model);
|
|
1459
2261
|
if (provider) {
|
|
1460
2262
|
logger.info(`Generating with ${provider.via} (sending project setup only)\u2026`);
|
|
2263
|
+
if (rootSkill?.trim()) logger.info(`Using ${ROOT_SKILL_FILE} as the skill template.`);
|
|
1461
2264
|
const aiSpinner = ora2({ text: "Authoring skill\u2026", isEnabled: !options.verbose }).start();
|
|
1462
2265
|
try {
|
|
1463
2266
|
const prompt = buildSkillPrompt({
|
|
@@ -1467,7 +2270,8 @@ async function initSkillCommand(options) {
|
|
|
1467
2270
|
target,
|
|
1468
2271
|
analysis: seed.content,
|
|
1469
2272
|
toolingPaths: scan.tooling.files.map((f) => f.path),
|
|
1470
|
-
scripts: scan.pkg?.scripts ?? {}
|
|
2273
|
+
scripts: scan.pkg?.scripts ?? {},
|
|
2274
|
+
rootSkill
|
|
1471
2275
|
});
|
|
1472
2276
|
const raw = await provider.run(prompt);
|
|
1473
2277
|
const parsed = parseSkillBundle(raw);
|
|
@@ -1481,7 +2285,9 @@ async function initSkillCommand(options) {
|
|
|
1481
2285
|
}
|
|
1482
2286
|
} catch (err) {
|
|
1483
2287
|
aiSpinner.fail("AI generation failed");
|
|
1484
|
-
logger.warn(`${err.message}
|
|
2288
|
+
logger.warn(`${err.message}`);
|
|
2289
|
+
if (err instanceof ReplicaxError) for (const hint of err.hints) logger.hint(hint);
|
|
2290
|
+
logger.warn("Falling back to the built-in template.");
|
|
1485
2291
|
}
|
|
1486
2292
|
} else {
|
|
1487
2293
|
logger.info("No configured AI provider found \u2014 using the built-in template.");
|
|
@@ -1499,11 +2305,11 @@ async function initSkillCommand(options) {
|
|
|
1499
2305
|
if (!safe) {
|
|
1500
2306
|
throw new ReplicaxError(`Refusing to write unsafe skill path: ${f.path}`);
|
|
1501
2307
|
}
|
|
1502
|
-
return { rel: safe, abs:
|
|
2308
|
+
return { rel: safe, abs: path8.join(root, ...safe.split("/")), content: f.content };
|
|
1503
2309
|
});
|
|
1504
2310
|
const conflicts = [];
|
|
1505
2311
|
for (const file of planned) {
|
|
1506
|
-
if (await
|
|
2312
|
+
if (await fs7.pathExists(file.abs)) conflicts.push(file.rel);
|
|
1507
2313
|
}
|
|
1508
2314
|
if (conflicts.length > 0 && !options.force) {
|
|
1509
2315
|
throw new ReplicaxError(
|
|
@@ -1512,27 +2318,193 @@ async function initSkillCommand(options) {
|
|
|
1512
2318
|
);
|
|
1513
2319
|
}
|
|
1514
2320
|
for (const file of planned) {
|
|
1515
|
-
await
|
|
1516
|
-
await
|
|
2321
|
+
await fs7.ensureDir(path8.dirname(file.abs));
|
|
2322
|
+
await fs7.writeFile(file.abs, file.content, "utf8");
|
|
1517
2323
|
logger.detail(`wrote: ${file.rel}`);
|
|
1518
2324
|
}
|
|
1519
2325
|
logger.newline();
|
|
1520
2326
|
logger.success(`Skill "${seed.slug}" written (${planned.length} file(s), via ${via})`);
|
|
1521
2327
|
logger.hint(
|
|
1522
|
-
`Location: ${relPosix(root,
|
|
2328
|
+
`Location: ${relPosix(root, path8.join(root, ...(bundleRoot || entryFile).split("/")))}`
|
|
1523
2329
|
);
|
|
1524
2330
|
logger.hint(target.note);
|
|
1525
2331
|
}
|
|
1526
2332
|
|
|
1527
2333
|
// src/commands/extract.ts
|
|
1528
|
-
import
|
|
2334
|
+
import path11 from "path";
|
|
1529
2335
|
import ora3 from "ora";
|
|
1530
2336
|
|
|
1531
2337
|
// src/core/github.ts
|
|
2338
|
+
import os2 from "os";
|
|
2339
|
+
import path10 from "path";
|
|
2340
|
+
import fs9 from "fs-extra";
|
|
2341
|
+
|
|
2342
|
+
// src/core/archive.ts
|
|
2343
|
+
import { createReadStream } from "fs";
|
|
1532
2344
|
import os from "os";
|
|
1533
|
-
import
|
|
1534
|
-
import
|
|
1535
|
-
import { extract as tarExtract } from "tar";
|
|
2345
|
+
import path9 from "path";
|
|
2346
|
+
import fs8 from "fs-extra";
|
|
2347
|
+
import { create as tarCreate, extract as tarExtract, Parser } from "tar";
|
|
2348
|
+
async function exportProfile(profileDirectory, outPath) {
|
|
2349
|
+
const resolvedOut = path9.resolve(outPath);
|
|
2350
|
+
await fs8.ensureDir(path9.dirname(resolvedOut));
|
|
2351
|
+
const parent = path9.dirname(profileDirectory);
|
|
2352
|
+
const base = path9.basename(profileDirectory);
|
|
2353
|
+
await tarCreate(
|
|
2354
|
+
{
|
|
2355
|
+
gzip: true,
|
|
2356
|
+
file: resolvedOut,
|
|
2357
|
+
cwd: parent,
|
|
2358
|
+
// tar strips leading "/" and ".." by default, so extraction stays scoped.
|
|
2359
|
+
portable: true
|
|
2360
|
+
},
|
|
2361
|
+
[base]
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
var PROFILE_ARCHIVE_LIMITS = {
|
|
2365
|
+
maxCompressedBytes: 50 * 1024 * 1024,
|
|
2366
|
+
// 50 MB
|
|
2367
|
+
maxTotalBytes: 200 * 1024 * 1024,
|
|
2368
|
+
// 200 MB uncompressed
|
|
2369
|
+
maxEntries: 2e4,
|
|
2370
|
+
maxEntryBytes: 50 * 1024 * 1024,
|
|
2371
|
+
// 50 MB per file
|
|
2372
|
+
allowSymlinks: false
|
|
2373
|
+
};
|
|
2374
|
+
var REPO_ARCHIVE_LIMITS = {
|
|
2375
|
+
maxCompressedBytes: 250 * 1024 * 1024,
|
|
2376
|
+
// 250 MB
|
|
2377
|
+
maxTotalBytes: 1024 * 1024 * 1024,
|
|
2378
|
+
// 1 GB uncompressed
|
|
2379
|
+
maxEntries: 2e5,
|
|
2380
|
+
maxEntryBytes: 200 * 1024 * 1024,
|
|
2381
|
+
// 200 MB per file
|
|
2382
|
+
allowSymlinks: true
|
|
2383
|
+
};
|
|
2384
|
+
function inspectEntry(entry, limits) {
|
|
2385
|
+
const type = String(entry.type);
|
|
2386
|
+
const entryPath = entry.path;
|
|
2387
|
+
if (type === "File" || type === "Directory" || type === "GNUDumpDir") {
|
|
2388
|
+
if (safeJoinable(entryPath) === null) {
|
|
2389
|
+
throw new ReplicaxError(`Refusing to extract unsafe path from archive: "${entryPath}".`, [
|
|
2390
|
+
"The archive may be malicious (path traversal)."
|
|
2391
|
+
]);
|
|
2392
|
+
}
|
|
2393
|
+
if ((entry.size ?? 0) > limits.maxEntryBytes) {
|
|
2394
|
+
throw new ReplicaxError(`Archive entry "${entryPath}" exceeds the per-file size limit.`);
|
|
2395
|
+
}
|
|
2396
|
+
return "extract";
|
|
2397
|
+
}
|
|
2398
|
+
if (type === "SymbolicLink" || type === "Link") {
|
|
2399
|
+
if (!limits.allowSymlinks) {
|
|
2400
|
+
throw new ReplicaxError(`Refusing to extract link entry from archive: "${entryPath}".`, [
|
|
2401
|
+
"Profile archives never contain symlinks; this one may be malicious."
|
|
2402
|
+
]);
|
|
2403
|
+
}
|
|
2404
|
+
return "skip";
|
|
2405
|
+
}
|
|
2406
|
+
throw new ReplicaxError(`Refusing to extract "${entryPath}" (unsupported tar entry: ${type}).`);
|
|
2407
|
+
}
|
|
2408
|
+
function validateArchive(resolved, limits) {
|
|
2409
|
+
return new Promise((resolve, reject) => {
|
|
2410
|
+
let entryCount = 0;
|
|
2411
|
+
let totalBytes = 0;
|
|
2412
|
+
let settled = false;
|
|
2413
|
+
const source = createReadStream(resolved);
|
|
2414
|
+
const parser = new Parser({});
|
|
2415
|
+
const fail = (err) => {
|
|
2416
|
+
if (settled) return;
|
|
2417
|
+
settled = true;
|
|
2418
|
+
source.destroy();
|
|
2419
|
+
reject(err);
|
|
2420
|
+
};
|
|
2421
|
+
const finish = () => {
|
|
2422
|
+
if (settled) return;
|
|
2423
|
+
settled = true;
|
|
2424
|
+
resolve();
|
|
2425
|
+
};
|
|
2426
|
+
parser.on("entry", (entry) => {
|
|
2427
|
+
if (settled) {
|
|
2428
|
+
entry.resume();
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
try {
|
|
2432
|
+
if (inspectEntry(entry, limits) === "extract") {
|
|
2433
|
+
entryCount += 1;
|
|
2434
|
+
if (entryCount > limits.maxEntries) {
|
|
2435
|
+
return fail(
|
|
2436
|
+
new ReplicaxError("Archive contains too many entries; refusing to extract.")
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
totalBytes += entry.size ?? 0;
|
|
2440
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
2441
|
+
return fail(
|
|
2442
|
+
new ReplicaxError("Archive is too large when uncompressed; refusing to extract.")
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
} catch (err) {
|
|
2447
|
+
return fail(err);
|
|
2448
|
+
}
|
|
2449
|
+
entry.resume();
|
|
2450
|
+
});
|
|
2451
|
+
parser.on("end", finish);
|
|
2452
|
+
parser.on("error", (err) => fail(err));
|
|
2453
|
+
source.on("error", (err) => fail(err));
|
|
2454
|
+
source.pipe(parser);
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
async function safeExtract(archivePath, destDir, limits) {
|
|
2458
|
+
const resolved = path9.resolve(archivePath);
|
|
2459
|
+
const stat = await fs8.stat(resolved).catch(() => null);
|
|
2460
|
+
if (!stat) {
|
|
2461
|
+
throw new ReplicaxError(`Archive not found: ${archivePath}`);
|
|
2462
|
+
}
|
|
2463
|
+
if (stat.size > limits.maxCompressedBytes) {
|
|
2464
|
+
throw new ReplicaxError("Archive file is too large; refusing to extract.");
|
|
2465
|
+
}
|
|
2466
|
+
await validateArchive(resolved, limits);
|
|
2467
|
+
await fs8.ensureDir(destDir);
|
|
2468
|
+
await tarExtract({
|
|
2469
|
+
file: resolved,
|
|
2470
|
+
cwd: destDir,
|
|
2471
|
+
strip: 0,
|
|
2472
|
+
filter: (_p, entry) => {
|
|
2473
|
+
try {
|
|
2474
|
+
return inspectEntry(entry, limits) === "extract";
|
|
2475
|
+
} catch {
|
|
2476
|
+
return false;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
async function extractToTemp(archivePath) {
|
|
2482
|
+
if (!await fs8.pathExists(path9.resolve(archivePath))) {
|
|
2483
|
+
throw new ReplicaxError(`Archive not found: ${archivePath}`);
|
|
2484
|
+
}
|
|
2485
|
+
const tmp = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-import-"));
|
|
2486
|
+
try {
|
|
2487
|
+
await safeExtract(archivePath, tmp, PROFILE_ARCHIVE_LIMITS);
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
await fs8.remove(tmp).catch(() => void 0);
|
|
2490
|
+
throw err;
|
|
2491
|
+
}
|
|
2492
|
+
return tmp;
|
|
2493
|
+
}
|
|
2494
|
+
async function findProfileRoot(dir) {
|
|
2495
|
+
const hasProfile = async (d) => fs8.pathExists(path9.join(d, PROFILE_FILES.profile));
|
|
2496
|
+
if (await hasProfile(dir)) return dir;
|
|
2497
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
2498
|
+
for (const entry of entries) {
|
|
2499
|
+
if (entry.isDirectory()) {
|
|
2500
|
+
const candidate = path9.join(dir, entry.name);
|
|
2501
|
+
if (await hasProfile(candidate)) return candidate;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// src/core/github.ts
|
|
1536
2508
|
function parseGitHubRef(input) {
|
|
1537
2509
|
const raw = input.trim();
|
|
1538
2510
|
if (!raw) {
|
|
@@ -1602,9 +2574,9 @@ function httpError(status, slug, hasToken) {
|
|
|
1602
2574
|
return new ReplicaxError(`GitHub returned HTTP ${status} for ${slug}.`);
|
|
1603
2575
|
}
|
|
1604
2576
|
async function firstSubdir(dir) {
|
|
1605
|
-
const entries = await
|
|
2577
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
1606
2578
|
for (const entry of entries) {
|
|
1607
|
-
if (entry.isDirectory()) return
|
|
2579
|
+
if (entry.isDirectory()) return path10.join(dir, entry.name);
|
|
1608
2580
|
}
|
|
1609
2581
|
return null;
|
|
1610
2582
|
}
|
|
@@ -1628,14 +2600,14 @@ async function downloadRepo(ref) {
|
|
|
1628
2600
|
if (!res.ok) {
|
|
1629
2601
|
throw httpError(res.status, slug, Boolean(token));
|
|
1630
2602
|
}
|
|
1631
|
-
const tmpRoot = await
|
|
1632
|
-
const cleanup = () =>
|
|
2603
|
+
const tmpRoot = await fs9.mkdtemp(path10.join(os2.tmpdir(), "replicax-extract-"));
|
|
2604
|
+
const cleanup = () => fs9.remove(tmpRoot);
|
|
1633
2605
|
try {
|
|
1634
|
-
const tarPath =
|
|
1635
|
-
await
|
|
1636
|
-
const extractDir =
|
|
1637
|
-
await
|
|
1638
|
-
await
|
|
2606
|
+
const tarPath = path10.join(tmpRoot, "repo.tar.gz");
|
|
2607
|
+
await fs9.writeFile(tarPath, Buffer.from(await res.arrayBuffer()));
|
|
2608
|
+
const extractDir = path10.join(tmpRoot, "src");
|
|
2609
|
+
await fs9.ensureDir(extractDir);
|
|
2610
|
+
await safeExtract(tarPath, extractDir, REPO_ARCHIVE_LIMITS);
|
|
1639
2611
|
const repoRoot = await firstSubdir(extractDir);
|
|
1640
2612
|
if (!repoRoot) {
|
|
1641
2613
|
throw new ReplicaxError(`The downloaded archive for ${slug} was empty.`);
|
|
@@ -1677,7 +2649,9 @@ async function extractCommand(repo, options) {
|
|
|
1677
2649
|
name,
|
|
1678
2650
|
tooling: scan.tooling,
|
|
1679
2651
|
structure: scan.structure,
|
|
1680
|
-
metadata: scan.metadata
|
|
2652
|
+
metadata: scan.metadata,
|
|
2653
|
+
// Captured from a remote repo we don't control — untrusted for auto-install.
|
|
2654
|
+
source: "github"
|
|
1681
2655
|
});
|
|
1682
2656
|
reportSkippedSecrets(scan.skippedSecrets);
|
|
1683
2657
|
printScanSummary(bundle);
|
|
@@ -1687,7 +2661,7 @@ async function extractCommand(repo, options) {
|
|
|
1687
2661
|
logger.info("Dry run \u2014 no files were written.");
|
|
1688
2662
|
return;
|
|
1689
2663
|
}
|
|
1690
|
-
const outRoot = options.out ?
|
|
2664
|
+
const outRoot = options.out ? path11.resolve(options.out) : process.cwd();
|
|
1691
2665
|
const dir = profileDir(outRoot);
|
|
1692
2666
|
if (await profileExists(dir)) {
|
|
1693
2667
|
logger.warn(
|
|
@@ -1704,8 +2678,7 @@ async function extractCommand(repo, options) {
|
|
|
1704
2678
|
}
|
|
1705
2679
|
|
|
1706
2680
|
// src/commands/create.ts
|
|
1707
|
-
import
|
|
1708
|
-
import fs9 from "fs-extra";
|
|
2681
|
+
import path13 from "path";
|
|
1709
2682
|
|
|
1710
2683
|
// src/core/conflict-resolver.ts
|
|
1711
2684
|
import { select } from "@inquirer/prompts";
|
|
@@ -1747,8 +2720,8 @@ var ConflictResolver = class {
|
|
|
1747
2720
|
};
|
|
1748
2721
|
|
|
1749
2722
|
// src/core/project-generator.ts
|
|
1750
|
-
import
|
|
1751
|
-
import
|
|
2723
|
+
import path12 from "path";
|
|
2724
|
+
import fs10 from "fs-extra";
|
|
1752
2725
|
async function generateProject(options) {
|
|
1753
2726
|
const { bundle, targetDir, projectName, dryRun, conflict } = options;
|
|
1754
2727
|
const result = {
|
|
@@ -1758,16 +2731,16 @@ async function generateProject(options) {
|
|
|
1758
2731
|
filesSkipped: 0,
|
|
1759
2732
|
unsafeSkipped: []
|
|
1760
2733
|
};
|
|
1761
|
-
if (!dryRun) await
|
|
2734
|
+
if (!dryRun) await fs10.ensureDir(targetDir);
|
|
1762
2735
|
for (const dir of bundle.structure.directories) {
|
|
1763
2736
|
const safe = safeJoinable(dir);
|
|
1764
2737
|
if (!safe) {
|
|
1765
2738
|
result.unsafeSkipped.push(dir);
|
|
1766
2739
|
continue;
|
|
1767
2740
|
}
|
|
1768
|
-
const full =
|
|
1769
|
-
const existed = await
|
|
1770
|
-
if (!dryRun) await
|
|
2741
|
+
const full = path12.join(targetDir, safe);
|
|
2742
|
+
const existed = await fs10.pathExists(full);
|
|
2743
|
+
if (!dryRun) await fs10.ensureDir(full);
|
|
1771
2744
|
if (!existed) result.dirsCreated += 1;
|
|
1772
2745
|
result.entries.push({ kind: "dir", path: safe, action: existed ? "skip" : "create" });
|
|
1773
2746
|
}
|
|
@@ -1791,8 +2764,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1791
2764
|
logger.warn(`Refusing to write unsafe path from profile: ${relPath}`);
|
|
1792
2765
|
return;
|
|
1793
2766
|
}
|
|
1794
|
-
const full =
|
|
1795
|
-
const exists = await
|
|
2767
|
+
const full = path12.join(options.targetDir, safe);
|
|
2768
|
+
const exists = await fs10.pathExists(full);
|
|
1796
2769
|
let action2 = exists ? "overwrite" : "create";
|
|
1797
2770
|
if (exists) {
|
|
1798
2771
|
const decision = await options.conflict.resolve(safe);
|
|
@@ -1805,8 +2778,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1805
2778
|
action2 = "overwrite";
|
|
1806
2779
|
}
|
|
1807
2780
|
if (!options.dryRun) {
|
|
1808
|
-
await
|
|
1809
|
-
await
|
|
2781
|
+
await fs10.ensureDir(path12.dirname(full));
|
|
2782
|
+
await fs10.writeFile(full, content, "utf8");
|
|
1810
2783
|
}
|
|
1811
2784
|
result.filesWritten += 1;
|
|
1812
2785
|
result.entries.push({ kind: "file", path: safe, action: action2 });
|
|
@@ -1814,7 +2787,7 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1814
2787
|
}
|
|
1815
2788
|
|
|
1816
2789
|
// src/core/installer.ts
|
|
1817
|
-
import { spawn as
|
|
2790
|
+
import { spawn as spawn3 } from "child_process";
|
|
1818
2791
|
var COMMANDS = {
|
|
1819
2792
|
npm: ["npm", "install"],
|
|
1820
2793
|
pnpm: ["pnpm", "install"],
|
|
@@ -1825,7 +2798,7 @@ function installDependencies(cwd, manager) {
|
|
|
1825
2798
|
if (manager === "unknown") return Promise.resolve(false);
|
|
1826
2799
|
const [command, ...args] = COMMANDS[manager];
|
|
1827
2800
|
return new Promise((resolve) => {
|
|
1828
|
-
const child =
|
|
2801
|
+
const child = spawn3(command, args, {
|
|
1829
2802
|
cwd,
|
|
1830
2803
|
stdio: "inherit",
|
|
1831
2804
|
// npm/pnpm/yarn are .cmd shims on Windows; a shell resolves them.
|
|
@@ -1855,9 +2828,9 @@ async function createCommand(projectName, options) {
|
|
|
1855
2828
|
logger.warn(`Profile integrity check found ${mismatches.length} issue(s); continuing anyway.`);
|
|
1856
2829
|
logger.hint("Run `replicax validate` for details.");
|
|
1857
2830
|
}
|
|
1858
|
-
const targetDir =
|
|
1859
|
-
const leafName =
|
|
1860
|
-
if (
|
|
2831
|
+
const targetDir = path13.resolve(process.cwd(), projectName);
|
|
2832
|
+
const leafName = path13.basename(targetDir);
|
|
2833
|
+
if (path13.resolve(process.cwd()) === targetDir) {
|
|
1861
2834
|
throw new ReplicaxError("Refusing to scaffold into the current directory.", [
|
|
1862
2835
|
"Pass a new project name, e.g. `replicax create my-app`."
|
|
1863
2836
|
]);
|
|
@@ -1887,63 +2860,95 @@ async function createCommand(projectName, options) {
|
|
|
1887
2860
|
return;
|
|
1888
2861
|
}
|
|
1889
2862
|
logger.hint(`Location: ${relPosix(process.cwd(), targetDir)}/`);
|
|
1890
|
-
await maybeInstall(
|
|
1891
|
-
bundle.metadata.packageManager,
|
|
1892
|
-
targetDir,
|
|
1893
|
-
options,
|
|
1894
|
-
Boolean(bundle.tooling.packageJson)
|
|
1895
|
-
);
|
|
2863
|
+
await maybeInstall(bundle, targetDir, options);
|
|
1896
2864
|
logger.newline();
|
|
1897
2865
|
logger.success(`Project ${pc.bold(leafName)} is ready.`);
|
|
1898
2866
|
}
|
|
1899
|
-
|
|
1900
|
-
if (options.skipInstall) {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
if (
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2867
|
+
function decideInstall(bundle, options) {
|
|
2868
|
+
if (options.skipInstall) return { kind: "skip-flag" };
|
|
2869
|
+
const devDeps = bundle.tooling.packageJson?.devDependencies ?? {};
|
|
2870
|
+
if (Object.keys(devDeps).length === 0) return { kind: "no-deps" };
|
|
2871
|
+
const manager = bundle.metadata.packageManager;
|
|
2872
|
+
if (manager === "unknown") return { kind: "no-manager" };
|
|
2873
|
+
const source = bundle.profile.source ?? "local";
|
|
2874
|
+
const trusted = source === "local";
|
|
2875
|
+
if (!trusted && !options.install) return { kind: "blocked", source, manager, devDeps };
|
|
2876
|
+
return { kind: "install", trusted, source, manager, devDeps };
|
|
2877
|
+
}
|
|
2878
|
+
function printDependencySummary(devDeps) {
|
|
2879
|
+
const names = Object.keys(devDeps).sort();
|
|
2880
|
+
for (const name of names.slice(0, 10)) logger.hint(` ${name} ${devDeps[name]}`);
|
|
2881
|
+
if (names.length > 10) logger.hint(` \u2026and ${names.length - 10} more`);
|
|
2882
|
+
}
|
|
2883
|
+
async function maybeInstall(bundle, targetDir, options) {
|
|
2884
|
+
const decision = decideInstall(bundle, options);
|
|
2885
|
+
switch (decision.kind) {
|
|
2886
|
+
case "skip-flag":
|
|
2887
|
+
logger.hint("Skipped dependency install (--skip-install).");
|
|
2888
|
+
return;
|
|
2889
|
+
case "no-deps":
|
|
2890
|
+
if (bundle.tooling.packageJson) logger.hint("No dependencies to install.");
|
|
2891
|
+
return;
|
|
2892
|
+
case "no-manager":
|
|
2893
|
+
logger.hint("No package manager detected; run your install command manually.");
|
|
2894
|
+
return;
|
|
2895
|
+
case "blocked":
|
|
2896
|
+
logger.newline();
|
|
2897
|
+
logger.warn(
|
|
2898
|
+
`Profile source is "${decision.source}" \u2014 skipping dependency install for safety.`
|
|
2899
|
+
);
|
|
2900
|
+
logger.hint(
|
|
2901
|
+
`Installing runs package lifecycle scripts. ${Object.keys(decision.devDeps).length} devDependencies would be added with ${decision.manager}:`
|
|
2902
|
+
);
|
|
2903
|
+
printDependencySummary(decision.devDeps);
|
|
2904
|
+
logger.hint(
|
|
2905
|
+
`Review them, then run \`${decision.manager} install\`, or re-run create with --install.`
|
|
2906
|
+
);
|
|
2907
|
+
return;
|
|
2908
|
+
case "install": {
|
|
2909
|
+
logger.newline();
|
|
2910
|
+
if (!decision.trusted) {
|
|
2911
|
+
logger.warn(
|
|
2912
|
+
`Installing for an untrusted ("${decision.source}") profile \u2014 package lifecycle scripts can execute code.`
|
|
2913
|
+
);
|
|
2914
|
+
}
|
|
2915
|
+
logger.info(
|
|
2916
|
+
`Installing ${Object.keys(decision.devDeps).length} devDependencies with ${decision.manager}\u2026`
|
|
2917
|
+
);
|
|
2918
|
+
printDependencySummary(decision.devDeps);
|
|
2919
|
+
const ok = await installDependencies(targetDir, decision.manager);
|
|
2920
|
+
if (ok) logger.success("Dependencies installed.");
|
|
2921
|
+
else logger.warn("Dependency install did not complete; run it manually.");
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
1914
2924
|
}
|
|
1915
|
-
logger.newline();
|
|
1916
|
-
logger.info(`Installing dependencies with ${manager}\u2026`);
|
|
1917
|
-
const ok = await installDependencies(targetDir, manager);
|
|
1918
|
-
if (ok) logger.success("Dependencies installed.");
|
|
1919
|
-
else logger.warn("Dependency install did not complete; run it manually.");
|
|
1920
2925
|
}
|
|
1921
2926
|
|
|
1922
2927
|
// src/commands/sync.ts
|
|
1923
2928
|
import ora4 from "ora";
|
|
1924
2929
|
|
|
1925
2930
|
// src/core/diff.ts
|
|
1926
|
-
function
|
|
2931
|
+
function diffStringMaps(prev, next, options = {}) {
|
|
2932
|
+
const ignore2 = options.ignoreKeys;
|
|
1927
2933
|
const added = [];
|
|
1928
2934
|
const removed = [];
|
|
1929
2935
|
const changed = [];
|
|
1930
|
-
|
|
1931
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(prev.files), ...Object.keys(next.files)]);
|
|
2936
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
1932
2937
|
for (const key of keys) {
|
|
1933
|
-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
if (before !== after) packageJsonChanged = true;
|
|
1937
|
-
continue;
|
|
1938
|
-
}
|
|
2938
|
+
if (ignore2?.has(key)) continue;
|
|
2939
|
+
const before = prev[key];
|
|
2940
|
+
const after = next[key];
|
|
1939
2941
|
if (before === void 0) added.push(key);
|
|
1940
2942
|
else if (after === void 0) removed.push(key);
|
|
1941
2943
|
else if (before !== after) changed.push(key);
|
|
1942
2944
|
}
|
|
1943
|
-
return {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2945
|
+
return { added: added.sort(), removed: removed.sort(), changed: changed.sort() };
|
|
2946
|
+
}
|
|
2947
|
+
var PACKAGE_JSON_KEYS = /* @__PURE__ */ new Set([PACKAGE_JSON_KEY]);
|
|
2948
|
+
function diffChecksums(prev, next) {
|
|
2949
|
+
const files = diffStringMaps(prev.files, next.files, { ignoreKeys: PACKAGE_JSON_KEYS });
|
|
2950
|
+
const packageJsonChanged = prev.files[PACKAGE_JSON_KEY] !== next.files[PACKAGE_JSON_KEY];
|
|
2951
|
+
return { files, packageJsonChanged };
|
|
1947
2952
|
}
|
|
1948
2953
|
function diffStructure(prev, next) {
|
|
1949
2954
|
const before = new Set(prev.directories);
|
|
@@ -1999,6 +3004,8 @@ async function syncCommand(options) {
|
|
|
1999
3004
|
tooling: scan.tooling,
|
|
2000
3005
|
structure: scan.structure,
|
|
2001
3006
|
metadata: scan.metadata,
|
|
3007
|
+
// A sync re-captures the local project, so the result is locally trusted.
|
|
3008
|
+
source: "local",
|
|
2002
3009
|
existing: existing.profile
|
|
2003
3010
|
});
|
|
2004
3011
|
const diff = diffBundles(existing, next);
|
|
@@ -2053,7 +3060,7 @@ function formatBytes(bytes) {
|
|
|
2053
3060
|
}
|
|
2054
3061
|
|
|
2055
3062
|
// src/commands/inspect.ts
|
|
2056
|
-
var SECTIONS = ["profile", "tooling", "structure", "metadata"];
|
|
3063
|
+
var SECTIONS = ["profile", "tooling", "structure", "metadata", "detections"];
|
|
2057
3064
|
async function inspectCommand(options) {
|
|
2058
3065
|
const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
|
|
2059
3066
|
if (!await profileExists(dir)) {
|
|
@@ -2067,15 +3074,38 @@ async function inspectCommand(options) {
|
|
|
2067
3074
|
const bundle = await loadBundle(dir);
|
|
2068
3075
|
const section = options.section;
|
|
2069
3076
|
if (options.json) {
|
|
2070
|
-
|
|
2071
|
-
logger.out(JSON.stringify(payload, null, 2));
|
|
3077
|
+
logger.out(JSON.stringify(jsonPayload(bundle, section), null, 2));
|
|
2072
3078
|
return;
|
|
2073
3079
|
}
|
|
2074
3080
|
if (!section || section === "profile") printProfile(bundle);
|
|
2075
3081
|
if (!section || section === "metadata") printMetadata(bundle);
|
|
3082
|
+
if (!section || section === "detections") printDetectionsSection(bundle);
|
|
2076
3083
|
if (!section || section === "tooling") printTooling(bundle);
|
|
2077
3084
|
if (!section || section === "structure") printStructure(bundle);
|
|
2078
3085
|
}
|
|
3086
|
+
function jsonPayload(bundle, section) {
|
|
3087
|
+
if (!section) return bundle;
|
|
3088
|
+
if (section === "detections") return { detections: bundle.metadata.detections ?? [] };
|
|
3089
|
+
return { [section]: bundle[section] };
|
|
3090
|
+
}
|
|
3091
|
+
function printDetectionsSection(bundle) {
|
|
3092
|
+
const detections = bundle.metadata.detections ?? [];
|
|
3093
|
+
logger.out(pc.bold(`Detections (${detections.length})`));
|
|
3094
|
+
if (detections.length === 0) {
|
|
3095
|
+
logger.out(" (none)");
|
|
3096
|
+
logger.out("");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const table = new Table({
|
|
3100
|
+
head: ["Category", "Tool", "Confidence", "Evidence"],
|
|
3101
|
+
style: { head: ["cyan"], border: ["dim"] }
|
|
3102
|
+
});
|
|
3103
|
+
for (const d of detections) {
|
|
3104
|
+
table.push([d.category, d.name, `${Math.round(d.confidence * 100)}%`, d.evidence.join(", ")]);
|
|
3105
|
+
}
|
|
3106
|
+
logger.out(table.toString());
|
|
3107
|
+
logger.out("");
|
|
3108
|
+
}
|
|
2079
3109
|
function printProfile(bundle) {
|
|
2080
3110
|
const p = bundle.profile;
|
|
2081
3111
|
logger.out(pc.bold("Profile"));
|
|
@@ -2109,12 +3139,7 @@ function printTooling(bundle) {
|
|
|
2109
3139
|
table.push(["Package Management & Monorepos", "package.json", "json", "template"]);
|
|
2110
3140
|
}
|
|
2111
3141
|
for (const file of [...tooling.files].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
2112
|
-
table.push([
|
|
2113
|
-
CATEGORY_BY_ID.get(file.category)?.label ?? file.category,
|
|
2114
|
-
file.path,
|
|
2115
|
-
file.variant,
|
|
2116
|
-
formatBytes(file.bytes)
|
|
2117
|
-
]);
|
|
3142
|
+
table.push([categoryLabel(file.category), file.path, file.variant, formatBytes(file.bytes)]);
|
|
2118
3143
|
}
|
|
2119
3144
|
logger.out(table.toString());
|
|
2120
3145
|
logger.out("");
|
|
@@ -2162,61 +3187,16 @@ async function validateCommand(options) {
|
|
|
2162
3187
|
}
|
|
2163
3188
|
|
|
2164
3189
|
// src/commands/export.ts
|
|
2165
|
-
import
|
|
3190
|
+
import path14 from "path";
|
|
2166
3191
|
import fs11 from "fs-extra";
|
|
2167
3192
|
import ora5 from "ora";
|
|
2168
|
-
|
|
2169
|
-
// src/core/archive.ts
|
|
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";
|
|
2174
|
-
async function exportProfile(profileDirectory, outPath) {
|
|
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);
|
|
2179
|
-
await tarCreate(
|
|
2180
|
-
{
|
|
2181
|
-
gzip: true,
|
|
2182
|
-
file: resolvedOut,
|
|
2183
|
-
cwd: parent,
|
|
2184
|
-
// tar strips leading "/" and ".." by default, so extraction stays scoped.
|
|
2185
|
-
portable: true
|
|
2186
|
-
},
|
|
2187
|
-
[base]
|
|
2188
|
-
);
|
|
2189
|
-
}
|
|
2190
|
-
async function extractToTemp(archivePath) {
|
|
2191
|
-
const resolved = path12.resolve(archivePath);
|
|
2192
|
-
if (!await fs10.pathExists(resolved)) {
|
|
2193
|
-
throw new Error(`Archive not found: ${archivePath}`);
|
|
2194
|
-
}
|
|
2195
|
-
const tmp = await fs10.mkdtemp(path12.join(os2.tmpdir(), "replicax-import-"));
|
|
2196
|
-
await tarExtract2({ file: resolved, cwd: tmp, strip: 0 });
|
|
2197
|
-
return tmp;
|
|
2198
|
-
}
|
|
2199
|
-
async function findProfileRoot(dir) {
|
|
2200
|
-
const hasProfile = async (d) => fs10.pathExists(path12.join(d, PROFILE_FILES.profile));
|
|
2201
|
-
if (await hasProfile(dir)) return dir;
|
|
2202
|
-
const entries = await fs10.readdir(dir, { withFileTypes: true });
|
|
2203
|
-
for (const entry of entries) {
|
|
2204
|
-
if (entry.isDirectory()) {
|
|
2205
|
-
const candidate = path12.join(dir, entry.name);
|
|
2206
|
-
if (await hasProfile(candidate)) return candidate;
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
return null;
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
// src/commands/export.ts
|
|
2213
3193
|
async function exportCommand(options) {
|
|
2214
3194
|
const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
|
|
2215
3195
|
if (!await profileExists(dir)) {
|
|
2216
3196
|
throw new ReplicaxError("No ReplicaX profile found to export.", ["Run `replicax init` first."]);
|
|
2217
3197
|
}
|
|
2218
3198
|
const bundle = await loadBundle(dir);
|
|
2219
|
-
const outPath =
|
|
3199
|
+
const outPath = path14.resolve(
|
|
2220
3200
|
options.out ?? `${slugify(bundle.profile.name, "profile")}.replicax.tar.gz`
|
|
2221
3201
|
);
|
|
2222
3202
|
const spinner = ora5({ text: "Packaging profile\u2026" }).start();
|
|
@@ -2224,7 +3204,7 @@ async function exportCommand(options) {
|
|
|
2224
3204
|
spinner.stop();
|
|
2225
3205
|
const { size } = await fs11.stat(outPath);
|
|
2226
3206
|
logger.success(
|
|
2227
|
-
`Exported "${bundle.profile.name}" \u2192 ${
|
|
3207
|
+
`Exported "${bundle.profile.name}" \u2192 ${path14.relative(process.cwd(), outPath)} (${formatBytes(size)})`
|
|
2228
3208
|
);
|
|
2229
3209
|
logger.hint("Share it, then `replicax import <file>` elsewhere.");
|
|
2230
3210
|
}
|
|
@@ -2246,6 +3226,7 @@ async function importCommand(archivePath, options) {
|
|
|
2246
3226
|
throw new ReplicaxError("The archive does not contain a ReplicaX profile.");
|
|
2247
3227
|
}
|
|
2248
3228
|
const bundle = await loadBundle(source);
|
|
3229
|
+
bundle.profile.source = "import";
|
|
2249
3230
|
spinner.succeed(`Validated profile "${bundle.profile.name}"`);
|
|
2250
3231
|
const dest = profileDir(process.cwd());
|
|
2251
3232
|
if (await profileExists(dest)) {
|
|
@@ -2271,6 +3252,407 @@ async function importCommand(archivePath, options) {
|
|
|
2271
3252
|
}
|
|
2272
3253
|
}
|
|
2273
3254
|
|
|
3255
|
+
// src/config/environment-tools.ts
|
|
3256
|
+
var ENVIRONMENT_TOOLS = [
|
|
3257
|
+
{ id: "node", name: "Node.js", bin: "node", versionArgs: ["--version"], kind: "runtime" },
|
|
3258
|
+
{ id: "git", name: "Git", bin: "git", versionArgs: ["--version"], kind: "vcs" },
|
|
3259
|
+
{ id: "npm", name: "npm", bin: "npm", versionArgs: ["--version"], kind: "package-manager" },
|
|
3260
|
+
{ id: "pnpm", name: "pnpm", bin: "pnpm", versionArgs: ["--version"], kind: "package-manager" },
|
|
3261
|
+
{ id: "yarn", name: "Yarn", bin: "yarn", versionArgs: ["--version"], kind: "package-manager" },
|
|
3262
|
+
{ id: "bun", name: "Bun", bin: "bun", versionArgs: ["--version"], kind: "package-manager" },
|
|
3263
|
+
{ id: "docker", name: "Docker", bin: "docker", versionArgs: ["--version"], kind: "container" },
|
|
3264
|
+
{
|
|
3265
|
+
id: "vscode",
|
|
3266
|
+
name: "VS Code",
|
|
3267
|
+
bin: "code",
|
|
3268
|
+
versionArgs: ["--version"],
|
|
3269
|
+
kind: "editor",
|
|
3270
|
+
// `code --version` prints version on the first line, then commit + arch.
|
|
3271
|
+
parseVersion: (raw) => raw.split(/\r?\n/)[0]?.trim() || void 0
|
|
3272
|
+
},
|
|
3273
|
+
{ id: "cursor", name: "Cursor", bin: "cursor", versionArgs: ["--version"], kind: "editor" },
|
|
3274
|
+
{
|
|
3275
|
+
id: "claude-code",
|
|
3276
|
+
name: "Claude Code",
|
|
3277
|
+
bin: "claude",
|
|
3278
|
+
versionArgs: ["--version"],
|
|
3279
|
+
kind: "editor"
|
|
3280
|
+
},
|
|
3281
|
+
{ id: "windsurf", name: "Windsurf", bin: "windsurf", versionArgs: ["--version"], kind: "editor" }
|
|
3282
|
+
];
|
|
3283
|
+
|
|
3284
|
+
// src/core/environment.ts
|
|
3285
|
+
function parseVersionDefault(raw) {
|
|
3286
|
+
const trimmed = raw.trim();
|
|
3287
|
+
const semver = trimmed.match(/\d+\.\d+\.\d+(?:[-+][\w.]+)?/);
|
|
3288
|
+
if (semver) return semver[0];
|
|
3289
|
+
const loose = trimmed.match(/\d+\.\d+/);
|
|
3290
|
+
return loose ? loose[0] : void 0;
|
|
3291
|
+
}
|
|
3292
|
+
var defaultProbe = async (tool) => {
|
|
3293
|
+
const out = await getCommandOutput(tool.bin, tool.versionArgs, { shell: true });
|
|
3294
|
+
if (out.ok) {
|
|
3295
|
+
const raw = out.stdout.trim() || out.stderr.trim();
|
|
3296
|
+
const parse = tool.parseVersion ?? parseVersionDefault;
|
|
3297
|
+
return { found: true, version: parse(raw) };
|
|
3298
|
+
}
|
|
3299
|
+
return { found: await commandExists(tool.bin) };
|
|
3300
|
+
};
|
|
3301
|
+
async function runEnvironmentChecks(tools = ENVIRONMENT_TOOLS, probe = defaultProbe) {
|
|
3302
|
+
return Promise.all(
|
|
3303
|
+
tools.map(async (tool) => {
|
|
3304
|
+
const result = await probe(tool);
|
|
3305
|
+
return {
|
|
3306
|
+
id: tool.id,
|
|
3307
|
+
name: tool.name,
|
|
3308
|
+
kind: tool.kind,
|
|
3309
|
+
found: result.found,
|
|
3310
|
+
version: result.version
|
|
3311
|
+
};
|
|
3312
|
+
})
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// src/commands/doctor.ts
|
|
3317
|
+
async function doctorCommand(options) {
|
|
3318
|
+
const checks = await runEnvironmentChecks();
|
|
3319
|
+
if (options.json) {
|
|
3320
|
+
logger.out(JSON.stringify({ checks }, null, 2));
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
logger.out(pc.bold("Developer environment"));
|
|
3324
|
+
logger.out("");
|
|
3325
|
+
for (const check of checks) {
|
|
3326
|
+
const note = check.found ? check.version : "not found";
|
|
3327
|
+
logger.out(statusLine(check.found, check.name, note));
|
|
3328
|
+
}
|
|
3329
|
+
const found = checks.filter((c) => c.found).length;
|
|
3330
|
+
logger.out("");
|
|
3331
|
+
logger.out(pc.dim(`${found}/${checks.length} tools found`));
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// src/commands/compare.ts
|
|
3335
|
+
import path15 from "path";
|
|
3336
|
+
import fs13 from "fs-extra";
|
|
3337
|
+
|
|
3338
|
+
// src/core/compare.ts
|
|
3339
|
+
function detectionsOf(bundle) {
|
|
3340
|
+
return bundle.metadata.detections ?? [];
|
|
3341
|
+
}
|
|
3342
|
+
var toolingComparator = {
|
|
3343
|
+
id: "tooling",
|
|
3344
|
+
title: "Tooling",
|
|
3345
|
+
compare(a, b) {
|
|
3346
|
+
const aById = new Map(detectionsOf(a).map((d) => [d.id, d]));
|
|
3347
|
+
const bById = new Map(detectionsOf(b).map((d) => [d.id, d]));
|
|
3348
|
+
const added = [];
|
|
3349
|
+
const removed = [];
|
|
3350
|
+
const changed = [];
|
|
3351
|
+
for (const [id, d] of bById) if (!aById.has(id)) added.push(d.name);
|
|
3352
|
+
for (const [id, d] of aById) if (!bById.has(id)) removed.push(d.name);
|
|
3353
|
+
for (const [id, d] of aById) {
|
|
3354
|
+
const other = bById.get(id);
|
|
3355
|
+
if (other && other.confidence !== d.confidence) changed.push(d.name);
|
|
3356
|
+
}
|
|
3357
|
+
return sortSection({ id: this.id, title: this.title, added, removed, changed });
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
var PACKAGE_JSON_KEYS2 = /* @__PURE__ */ new Set([PACKAGE_JSON_KEY]);
|
|
3361
|
+
var configFilesComparator = {
|
|
3362
|
+
id: "config-files",
|
|
3363
|
+
title: "Configuration files",
|
|
3364
|
+
compare(a, b) {
|
|
3365
|
+
const diff = diffStringMaps(a.checksum.files, b.checksum.files, {
|
|
3366
|
+
ignoreKeys: PACKAGE_JSON_KEYS2
|
|
3367
|
+
});
|
|
3368
|
+
return { id: this.id, title: this.title, ...diff };
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
var packageJsonComparator = {
|
|
3372
|
+
id: "package-json",
|
|
3373
|
+
title: "package.json",
|
|
3374
|
+
compare(a, b) {
|
|
3375
|
+
const flatten = (bundle) => {
|
|
3376
|
+
const pkg = bundle.tooling.packageJson;
|
|
3377
|
+
const out = {};
|
|
3378
|
+
for (const [name, cmd] of Object.entries(pkg?.scripts ?? {})) out[`script:${name}`] = cmd;
|
|
3379
|
+
for (const [name, ver] of Object.entries(pkg?.devDependencies ?? {})) {
|
|
3380
|
+
out[`devDependency:${name}`] = ver;
|
|
3381
|
+
}
|
|
3382
|
+
return out;
|
|
3383
|
+
};
|
|
3384
|
+
const diff = diffStringMaps(flatten(a), flatten(b));
|
|
3385
|
+
return { id: this.id, title: this.title, ...diff };
|
|
3386
|
+
}
|
|
3387
|
+
};
|
|
3388
|
+
var structureComparator = {
|
|
3389
|
+
id: "structure",
|
|
3390
|
+
title: "Structure",
|
|
3391
|
+
compare(a, b) {
|
|
3392
|
+
const before = new Set(a.structure.directories);
|
|
3393
|
+
const after = new Set(b.structure.directories);
|
|
3394
|
+
const added = b.structure.directories.filter((d) => !before.has(d));
|
|
3395
|
+
const removed = a.structure.directories.filter((d) => !after.has(d));
|
|
3396
|
+
return sortSection({ id: this.id, title: this.title, added, removed, changed: [] });
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
var metadataComparator = {
|
|
3400
|
+
id: "metadata",
|
|
3401
|
+
title: "Metadata",
|
|
3402
|
+
compare(a, b) {
|
|
3403
|
+
const fields = [
|
|
3404
|
+
"language",
|
|
3405
|
+
"framework",
|
|
3406
|
+
"packageManager",
|
|
3407
|
+
"nodeVersion"
|
|
3408
|
+
];
|
|
3409
|
+
const changed = [];
|
|
3410
|
+
for (const field of fields) {
|
|
3411
|
+
const from = String(a.metadata[field] ?? "");
|
|
3412
|
+
const to = String(b.metadata[field] ?? "");
|
|
3413
|
+
if (from !== to) changed.push(`${field}: ${from} \u2192 ${to}`);
|
|
3414
|
+
}
|
|
3415
|
+
return { id: this.id, title: this.title, added: [], removed: [], changed };
|
|
3416
|
+
}
|
|
3417
|
+
};
|
|
3418
|
+
var COMPARATORS = [
|
|
3419
|
+
toolingComparator,
|
|
3420
|
+
configFilesComparator,
|
|
3421
|
+
packageJsonComparator,
|
|
3422
|
+
structureComparator,
|
|
3423
|
+
metadataComparator
|
|
3424
|
+
];
|
|
3425
|
+
function sortSection(section) {
|
|
3426
|
+
return {
|
|
3427
|
+
...section,
|
|
3428
|
+
added: [...section.added].sort(),
|
|
3429
|
+
removed: [...section.removed].sort(),
|
|
3430
|
+
changed: [...section.changed].sort()
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
function compareBundles(a, b) {
|
|
3434
|
+
return { sections: COMPARATORS.map((c) => c.compare(a, b)) };
|
|
3435
|
+
}
|
|
3436
|
+
function sectionHasChanges(section) {
|
|
3437
|
+
return section.added.length > 0 || section.removed.length > 0 || section.changed.length > 0;
|
|
3438
|
+
}
|
|
3439
|
+
function comparisonHasChanges(comparison) {
|
|
3440
|
+
return comparison.sections.some(sectionHasChanges);
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
// src/commands/compare.ts
|
|
3444
|
+
async function resolveBundle(input) {
|
|
3445
|
+
const resolved = path15.resolve(input);
|
|
3446
|
+
if (!await fs13.pathExists(resolved)) {
|
|
3447
|
+
throw new ReplicaxError(`Path not found: ${input}`);
|
|
3448
|
+
}
|
|
3449
|
+
try {
|
|
3450
|
+
const dir = await resolveProfileDir(input);
|
|
3451
|
+
const bundle2 = await loadBundle(dir);
|
|
3452
|
+
return { bundle: bundle2, label: `${bundle2.profile.name} (profile)` };
|
|
3453
|
+
} catch {
|
|
3454
|
+
}
|
|
3455
|
+
const stat = await fs13.stat(resolved);
|
|
3456
|
+
if (!stat.isDirectory()) {
|
|
3457
|
+
throw new ReplicaxError(`Cannot compare "${input}": not a profile or a project directory.`, [
|
|
3458
|
+
"Pass a project folder or a directory containing a .replicax profile."
|
|
3459
|
+
]);
|
|
3460
|
+
}
|
|
3461
|
+
const scan = await scanProject(resolved);
|
|
3462
|
+
const bundle = buildBundle({
|
|
3463
|
+
name: path15.basename(resolved) || "project",
|
|
3464
|
+
tooling: scan.tooling,
|
|
3465
|
+
structure: scan.structure,
|
|
3466
|
+
metadata: scan.metadata
|
|
3467
|
+
});
|
|
3468
|
+
return { bundle, label: `${path15.basename(resolved)} (scanned)` };
|
|
3469
|
+
}
|
|
3470
|
+
async function compareCommand(source, target, options) {
|
|
3471
|
+
const [a, b] = await Promise.all([resolveBundle(source), resolveBundle(target)]);
|
|
3472
|
+
const comparison = compareBundles(a.bundle, b.bundle);
|
|
3473
|
+
if (options.json) {
|
|
3474
|
+
logger.out(JSON.stringify({ source: a.label, target: b.label, ...comparison }, null, 2));
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
logger.out(pc.bold(`Comparing ${a.label} \u2192 ${b.label}`));
|
|
3478
|
+
logger.out("");
|
|
3479
|
+
if (!comparisonHasChanges(comparison)) {
|
|
3480
|
+
logger.out("No differences.");
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
printGroup("Added", collect(comparison, "added"), pc.green("+"));
|
|
3484
|
+
printGroup("Removed", collect(comparison, "removed"), pc.red("-"));
|
|
3485
|
+
printGroup("Changed", collect(comparison, "changed"), pc.yellow("~"));
|
|
3486
|
+
}
|
|
3487
|
+
function collect(comparison, bucket) {
|
|
3488
|
+
const out = [];
|
|
3489
|
+
for (const section of comparison.sections) {
|
|
3490
|
+
for (const item of section[bucket]) {
|
|
3491
|
+
out.push(`${item} ${pc.dim(`(${section.title})`)}`);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
return out;
|
|
3495
|
+
}
|
|
3496
|
+
function printGroup(label, items, marker) {
|
|
3497
|
+
if (items.length === 0) return;
|
|
3498
|
+
logger.out(pc.bold(`${label}:`));
|
|
3499
|
+
for (const item of items) logger.out(` ${marker} ${item}`);
|
|
3500
|
+
logger.out("");
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
// src/commands/audit.ts
|
|
3504
|
+
import path16 from "path";
|
|
3505
|
+
|
|
3506
|
+
// src/core/audit/rules.ts
|
|
3507
|
+
function detected(ctx, ids) {
|
|
3508
|
+
const present = new Set(ctx.detections.map((d) => d.id));
|
|
3509
|
+
return ids.some((id) => present.has(id));
|
|
3510
|
+
}
|
|
3511
|
+
var AUDIT_RULES = [
|
|
3512
|
+
{
|
|
3513
|
+
id: "linting",
|
|
3514
|
+
title: "Linting",
|
|
3515
|
+
weight: 15,
|
|
3516
|
+
category: "quality",
|
|
3517
|
+
passes: (c) => detected(c, ["eslint", "biome"]),
|
|
3518
|
+
recommendation: "Add ESLint to catch problems with static analysis."
|
|
3519
|
+
},
|
|
3520
|
+
{
|
|
3521
|
+
id: "formatting",
|
|
3522
|
+
title: "Formatting",
|
|
3523
|
+
weight: 10,
|
|
3524
|
+
category: "quality",
|
|
3525
|
+
passes: (c) => detected(c, ["prettier", "biome"]),
|
|
3526
|
+
recommendation: "Add Prettier to keep formatting consistent."
|
|
3527
|
+
},
|
|
3528
|
+
{
|
|
3529
|
+
id: "testing",
|
|
3530
|
+
title: "Testing",
|
|
3531
|
+
weight: 20,
|
|
3532
|
+
category: "quality",
|
|
3533
|
+
passes: (c) => detected(c, ["vitest", "jest", "playwright", "cypress"]),
|
|
3534
|
+
recommendation: "Add a test runner such as Vitest or Jest."
|
|
3535
|
+
},
|
|
3536
|
+
{
|
|
3537
|
+
id: "git-hooks",
|
|
3538
|
+
title: "Git hooks",
|
|
3539
|
+
weight: 10,
|
|
3540
|
+
category: "quality",
|
|
3541
|
+
passes: (c) => detected(c, ["husky", "lefthook"]),
|
|
3542
|
+
recommendation: "Add Husky to run checks before each commit."
|
|
3543
|
+
},
|
|
3544
|
+
{
|
|
3545
|
+
id: "ci",
|
|
3546
|
+
title: "CI/CD",
|
|
3547
|
+
weight: 20,
|
|
3548
|
+
category: "delivery",
|
|
3549
|
+
passes: (c) => detected(c, ["github-actions", "gitlab-ci", "circleci", "jenkins", "azure-pipelines"]),
|
|
3550
|
+
recommendation: "Add a CI pipeline (e.g. GitHub Actions) to run checks on every push."
|
|
3551
|
+
},
|
|
3552
|
+
{
|
|
3553
|
+
id: "containerization",
|
|
3554
|
+
title: "Containerization",
|
|
3555
|
+
weight: 10,
|
|
3556
|
+
category: "delivery",
|
|
3557
|
+
passes: (c) => detected(c, ["docker", "docker-compose"]),
|
|
3558
|
+
recommendation: "Add a Dockerfile to containerize the application."
|
|
3559
|
+
},
|
|
3560
|
+
{
|
|
3561
|
+
id: "typescript",
|
|
3562
|
+
title: "TypeScript",
|
|
3563
|
+
weight: 10,
|
|
3564
|
+
category: "quality",
|
|
3565
|
+
passes: (c) => detected(c, ["typescript"]),
|
|
3566
|
+
recommendation: "Adopt TypeScript for type safety."
|
|
3567
|
+
},
|
|
3568
|
+
{
|
|
3569
|
+
id: "commit-linting",
|
|
3570
|
+
title: "Commit linting",
|
|
3571
|
+
weight: 3,
|
|
3572
|
+
category: "quality",
|
|
3573
|
+
passes: (c) => detected(c, ["commitlint"]),
|
|
3574
|
+
recommendation: "Add Commitlint to standardize commit messages."
|
|
3575
|
+
},
|
|
3576
|
+
{
|
|
3577
|
+
id: "staged-linting",
|
|
3578
|
+
title: "Staged-file linting",
|
|
3579
|
+
weight: 2,
|
|
3580
|
+
category: "quality",
|
|
3581
|
+
passes: (c) => detected(c, ["lint-staged"]),
|
|
3582
|
+
recommendation: "Add lint-staged to lint only changed files."
|
|
3583
|
+
}
|
|
3584
|
+
];
|
|
3585
|
+
|
|
3586
|
+
// src/core/audit/engine.ts
|
|
3587
|
+
function runAudit(ctx, rules = AUDIT_RULES) {
|
|
3588
|
+
const evaluated = rules.map((rule) => ({
|
|
3589
|
+
id: rule.id,
|
|
3590
|
+
title: rule.title,
|
|
3591
|
+
category: rule.category,
|
|
3592
|
+
weight: rule.weight,
|
|
3593
|
+
passed: rule.passes(ctx),
|
|
3594
|
+
recommendation: rule.recommendation
|
|
3595
|
+
}));
|
|
3596
|
+
const totalWeight = evaluated.reduce((sum, r) => sum + r.weight, 0);
|
|
3597
|
+
const passedWeight = evaluated.filter((r) => r.passed).reduce((sum, r) => sum + r.weight, 0);
|
|
3598
|
+
const score = totalWeight === 0 ? 100 : Math.round(passedWeight / totalWeight * 100);
|
|
3599
|
+
const failed = evaluated.filter((r) => !r.passed);
|
|
3600
|
+
return {
|
|
3601
|
+
score,
|
|
3602
|
+
maxScore: 100,
|
|
3603
|
+
rules: evaluated,
|
|
3604
|
+
missing: failed.map((r) => r.title),
|
|
3605
|
+
recommendations: failed.map((r) => r.recommendation)
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// src/commands/audit.ts
|
|
3610
|
+
async function buildContext(options) {
|
|
3611
|
+
if (options.profile) {
|
|
3612
|
+
const dir = await resolveProfileDir(options.profile);
|
|
3613
|
+
const bundle = await loadBundle(dir);
|
|
3614
|
+
return {
|
|
3615
|
+
ctx: {
|
|
3616
|
+
detections: bundle.metadata.detections ?? [],
|
|
3617
|
+
metadata: bundle.metadata,
|
|
3618
|
+
tooling: bundle.tooling
|
|
3619
|
+
},
|
|
3620
|
+
source: `profile "${bundle.profile.name}"`
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
const root = path16.resolve(options.path ?? process.cwd());
|
|
3624
|
+
const scan = await scanProject(root);
|
|
3625
|
+
return {
|
|
3626
|
+
ctx: { detections: scan.detections, metadata: scan.metadata, tooling: scan.tooling },
|
|
3627
|
+
source: path16.basename(root) || "project"
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
async function auditCommand(options) {
|
|
3631
|
+
const { ctx, source } = await buildContext(options);
|
|
3632
|
+
const result = runAudit(ctx);
|
|
3633
|
+
if (options.json) {
|
|
3634
|
+
logger.out(JSON.stringify(result, null, 2));
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
logger.out(pc.bold(`Project Score: ${result.score}/${result.maxScore}`));
|
|
3638
|
+
logger.out(pc.dim(`Audited ${source}`));
|
|
3639
|
+
logger.out("");
|
|
3640
|
+
for (const rule of result.rules) {
|
|
3641
|
+
logger.out(statusLine(rule.passed, rule.title));
|
|
3642
|
+
}
|
|
3643
|
+
if (result.missing.length === 0) {
|
|
3644
|
+
logger.out("");
|
|
3645
|
+
logger.out(pc.green("All checks passed."));
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
logger.out("");
|
|
3649
|
+
logger.out(pc.bold("Missing:"));
|
|
3650
|
+
for (const item of result.missing) logger.out(` - ${item}`);
|
|
3651
|
+
logger.out("");
|
|
3652
|
+
logger.out(pc.bold("Recommendations:"));
|
|
3653
|
+
for (const rec of result.recommendations) logger.out(` - ${rec}`);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
2274
3656
|
// src/index.ts
|
|
2275
3657
|
function packageVersion() {
|
|
2276
3658
|
try {
|
|
@@ -2311,12 +3693,15 @@ program.command("init-skill").description(
|
|
|
2311
3693
|
"Generate an AI assistant skill from the detected tech stack (uses your configured AI)"
|
|
2312
3694
|
).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
3695
|
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));
|
|
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));
|
|
3696
|
+
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("--install", "Install deps even for imported/remote (untrusted) profiles").option("--dry-run", "Preview the output without writing").option("--force", "Overwrite conflicting files without prompting").option("--verbose", "Show every written file").action(action(createCommand));
|
|
2315
3697
|
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));
|
|
2316
3698
|
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));
|
|
2317
3699
|
program.command("validate").description("Check profile schema and integrity").option("--profile <path>", "Validate a profile at a custom path").action(action(validateCommand));
|
|
2318
3700
|
program.command("export").description("Export the profile as a portable .tar.gz archive").option("--out <file>", "Output archive path").option("--profile <path>", "Export a profile from a custom path").action(action(exportCommand));
|
|
2319
3701
|
program.command("import").argument("<archive>", "Path to a .tar.gz profile archive").description("Import a portable profile archive into .replicax/").option("--force", "Overwrite an existing profile").action(action(importCommand));
|
|
3702
|
+
program.command("doctor").description("Check which developer tools are installed locally").option("--json", "Output as JSON").action(action(doctorCommand));
|
|
3703
|
+
program.command("compare").argument("<source>", "A profile path or project directory").argument("<target>", "A profile path or project directory").description("Compare two profiles (or projects): tooling, config, structure, metadata").option("--json", "Output as JSON").action(action(compareCommand));
|
|
3704
|
+
program.command("audit").description("Score a project setup against best practices and recommend improvements").option("--path <dir>", "Directory to audit (default: current dir)").option("--profile <path>", "Audit a stored profile instead of scanning").option("--json", "Output as JSON").action(action(auditCommand));
|
|
2320
3705
|
if (process.argv.slice(2).length === 0) {
|
|
2321
3706
|
program.outputHelp();
|
|
2322
3707
|
process.exit(0);
|