@iamsaroj/replicax 0.0.3 → 0.0.4
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 +113 -7
- package/dist/index.js +1318 -143
- 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.1.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();
|
|
@@ -704,28 +1281,42 @@ function buildBundle(args) {
|
|
|
704
1281
|
replicaxVersion: REPLICAX_VERSION,
|
|
705
1282
|
...args.description ? { description: args.description } : {}
|
|
706
1283
|
};
|
|
1284
|
+
const checksum = computeChecksum(args.tooling);
|
|
707
1285
|
return {
|
|
708
1286
|
profile,
|
|
709
1287
|
tooling: args.tooling,
|
|
710
1288
|
structure: args.structure,
|
|
711
1289
|
metadata: args.metadata,
|
|
712
|
-
checksum
|
|
1290
|
+
checksum,
|
|
1291
|
+
manifest: buildManifest(args.tooling, checksum)
|
|
713
1292
|
};
|
|
714
1293
|
}
|
|
715
1294
|
|
|
716
1295
|
// src/core/profile-store.ts
|
|
717
|
-
import
|
|
718
|
-
import
|
|
1296
|
+
import path6 from "path";
|
|
1297
|
+
import fs5 from "fs-extra";
|
|
719
1298
|
|
|
720
1299
|
// src/schema.ts
|
|
721
1300
|
import { z } from "zod";
|
|
1301
|
+
var RegistrySchema = z.object({
|
|
1302
|
+
/** Stable identifier within a registry, e.g. "acme/react-enterprise". */
|
|
1303
|
+
id: z.string().optional(),
|
|
1304
|
+
/** Owning namespace/org. */
|
|
1305
|
+
namespace: z.string().optional(),
|
|
1306
|
+
/** Intended visibility once published. */
|
|
1307
|
+
visibility: z.enum(["public", "private"]).optional(),
|
|
1308
|
+
/** Where the profile originated (URL, registry name, …). */
|
|
1309
|
+
source: z.string().optional()
|
|
1310
|
+
});
|
|
722
1311
|
var ProfileSchema = z.object({
|
|
723
1312
|
name: z.string().min(1),
|
|
724
1313
|
version: z.string().min(1),
|
|
725
1314
|
createdAt: z.string().min(1),
|
|
726
1315
|
updatedAt: z.string().optional(),
|
|
727
1316
|
replicaxVersion: z.string().min(1),
|
|
728
|
-
description: z.string().optional()
|
|
1317
|
+
description: z.string().optional(),
|
|
1318
|
+
/** Optional registry metadata (future registry compatibility). */
|
|
1319
|
+
registry: RegistrySchema.optional()
|
|
729
1320
|
});
|
|
730
1321
|
var FileVariantSchema = z.enum(["ts", "js", "mjs", "cjs", "json", "yaml", "other"]);
|
|
731
1322
|
var ToolingFileSchema = z.object({
|
|
@@ -759,32 +1350,114 @@ var StructureSchema = z.object({
|
|
|
759
1350
|
root: z.string(),
|
|
760
1351
|
directories: z.array(z.string())
|
|
761
1352
|
});
|
|
1353
|
+
var DetectionCategorySchema = z.enum([
|
|
1354
|
+
"language",
|
|
1355
|
+
"framework",
|
|
1356
|
+
"package-manager",
|
|
1357
|
+
"monorepo",
|
|
1358
|
+
"container",
|
|
1359
|
+
"ci",
|
|
1360
|
+
"git-hooks",
|
|
1361
|
+
"commit",
|
|
1362
|
+
"lint",
|
|
1363
|
+
"format",
|
|
1364
|
+
"test",
|
|
1365
|
+
"build",
|
|
1366
|
+
"editor",
|
|
1367
|
+
"ai",
|
|
1368
|
+
"devcontainer",
|
|
1369
|
+
"jvm"
|
|
1370
|
+
]);
|
|
1371
|
+
var DetectionSchema = z.object({
|
|
1372
|
+
/** Stable id, e.g. "docker", "github-actions". */
|
|
1373
|
+
id: z.string().min(1),
|
|
1374
|
+
/** Human-friendly label, e.g. "Docker". */
|
|
1375
|
+
name: z.string().min(1),
|
|
1376
|
+
category: DetectionCategorySchema,
|
|
1377
|
+
/** 0..1 — how sure we are this tool is in use. */
|
|
1378
|
+
confidence: z.number().min(0).max(1),
|
|
1379
|
+
/** Paths/fields that justify the detection (e.g. ["Dockerfile"]). */
|
|
1380
|
+
evidence: z.array(z.string()).default([])
|
|
1381
|
+
});
|
|
762
1382
|
var MetadataSchema = z.object({
|
|
763
1383
|
nodeVersion: z.string(),
|
|
764
1384
|
packageManager: z.enum(["npm", "yarn", "pnpm", "bun", "unknown"]),
|
|
765
1385
|
framework: z.string(),
|
|
766
|
-
language: z.enum(["typescript", "javascript"]),
|
|
767
|
-
platform: z.string()
|
|
1386
|
+
language: z.enum(["typescript", "javascript", "java", "unknown"]),
|
|
1387
|
+
platform: z.string(),
|
|
1388
|
+
/** Detected tools/technologies with confidence (added in schema 2.1.0). */
|
|
1389
|
+
detections: z.array(DetectionSchema).optional()
|
|
768
1390
|
});
|
|
769
1391
|
var ChecksumSchema = z.object({
|
|
770
1392
|
algorithm: z.literal("sha256"),
|
|
771
1393
|
files: z.record(z.string(), z.string())
|
|
772
1394
|
});
|
|
1395
|
+
var ManifestEntrySchema = z.object({
|
|
1396
|
+
path: z.string().min(1),
|
|
1397
|
+
category: z.string().min(1),
|
|
1398
|
+
variant: FileVariantSchema,
|
|
1399
|
+
bytes: z.number().int().nonnegative(),
|
|
1400
|
+
sha256: z.string()
|
|
1401
|
+
});
|
|
1402
|
+
var ManifestSchema = z.object({
|
|
1403
|
+
schemaVersion: z.string().min(1),
|
|
1404
|
+
generatedAt: z.string().min(1),
|
|
1405
|
+
entries: z.array(ManifestEntrySchema)
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// src/core/migrations.ts
|
|
1409
|
+
var MIGRATIONS = [
|
|
1410
|
+
{
|
|
1411
|
+
from: "2.0.0",
|
|
1412
|
+
to: "2.1.0",
|
|
1413
|
+
apply(raw) {
|
|
1414
|
+
const metadata = raw.metadata;
|
|
1415
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata.detections)) {
|
|
1416
|
+
metadata.detections = [];
|
|
1417
|
+
}
|
|
1418
|
+
return raw;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
];
|
|
1422
|
+
var KNOWN_VERSIONS = /* @__PURE__ */ new Set([
|
|
1423
|
+
REPLICAX_VERSION,
|
|
1424
|
+
...MIGRATIONS.flatMap((m) => [m.from, m.to])
|
|
1425
|
+
]);
|
|
1426
|
+
function migrateRawBundle(raw, detectedVersion) {
|
|
1427
|
+
const steps = [];
|
|
1428
|
+
let current = detectedVersion;
|
|
1429
|
+
let data = raw;
|
|
1430
|
+
for (let guard = 0; guard < MIGRATIONS.length + 1; guard += 1) {
|
|
1431
|
+
if (current === REPLICAX_VERSION) break;
|
|
1432
|
+
const next = MIGRATIONS.find((m) => m.from === current);
|
|
1433
|
+
if (!next) break;
|
|
1434
|
+
data = next.apply(data);
|
|
1435
|
+
steps.push(`${next.from} \u2192 ${next.to}`);
|
|
1436
|
+
current = next.to;
|
|
1437
|
+
}
|
|
1438
|
+
return {
|
|
1439
|
+
raw: data,
|
|
1440
|
+
from: detectedVersion,
|
|
1441
|
+
to: current,
|
|
1442
|
+
migrated: steps.length > 0,
|
|
1443
|
+
steps
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
773
1446
|
|
|
774
1447
|
// src/core/profile-store.ts
|
|
775
1448
|
function profileDir(root) {
|
|
776
|
-
return
|
|
1449
|
+
return path6.join(path6.resolve(root), REPLICAX_DIR);
|
|
777
1450
|
}
|
|
778
1451
|
async function profileExists(dir) {
|
|
779
|
-
return
|
|
1452
|
+
return fs5.pathExists(path6.join(dir, PROFILE_FILES.profile));
|
|
780
1453
|
}
|
|
781
1454
|
async function resolveProfileDir(input) {
|
|
782
|
-
const resolved =
|
|
783
|
-
if (!await
|
|
1455
|
+
const resolved = path6.resolve(input);
|
|
1456
|
+
if (!await fs5.pathExists(resolved)) {
|
|
784
1457
|
throw new ReplicaxError(`Profile path not found: ${input}`);
|
|
785
1458
|
}
|
|
786
1459
|
if (await profileExists(resolved)) return resolved;
|
|
787
|
-
const nested =
|
|
1460
|
+
const nested = path6.join(resolved, REPLICAX_DIR);
|
|
788
1461
|
if (await profileExists(nested)) return nested;
|
|
789
1462
|
throw new ReplicaxError(`No ReplicaX profile found at: ${input}`, [
|
|
790
1463
|
`Looked for ${PROFILE_FILES.profile} in ${resolved} and ${nested}.`,
|
|
@@ -792,26 +1465,29 @@ async function resolveProfileDir(input) {
|
|
|
792
1465
|
]);
|
|
793
1466
|
}
|
|
794
1467
|
async function saveBundle(dir, bundle) {
|
|
795
|
-
await
|
|
1468
|
+
await fs5.ensureDir(dir);
|
|
1469
|
+
const manifest = bundle.manifest ?? buildManifest(bundle.tooling, bundle.checksum);
|
|
796
1470
|
await Promise.all([
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1471
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.profile), bundle.profile, { spaces: 2 }),
|
|
1472
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.tooling), bundle.tooling, { spaces: 2 }),
|
|
1473
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.structure), bundle.structure, { spaces: 2 }),
|
|
1474
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.metadata), bundle.metadata, { spaces: 2 }),
|
|
1475
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.checksum), bundle.checksum, { spaces: 2 }),
|
|
1476
|
+
fs5.writeJson(path6.join(dir, PROFILE_FILES.manifest), manifest, { spaces: 2 })
|
|
802
1477
|
]);
|
|
803
1478
|
}
|
|
804
|
-
async function
|
|
805
|
-
const full =
|
|
806
|
-
if (!await
|
|
1479
|
+
async function readRawFile(dir, file) {
|
|
1480
|
+
const full = path6.join(dir, file);
|
|
1481
|
+
if (!await fs5.pathExists(full)) {
|
|
807
1482
|
throw new ReplicaxError(`Profile is missing ${file}`, [`Expected at ${full}.`]);
|
|
808
1483
|
}
|
|
809
|
-
let raw;
|
|
810
1484
|
try {
|
|
811
|
-
|
|
1485
|
+
return await fs5.readJson(full);
|
|
812
1486
|
} catch {
|
|
813
1487
|
throw new ReplicaxError(`Profile file ${file} is not valid JSON`, [`Path: ${full}`]);
|
|
814
1488
|
}
|
|
1489
|
+
}
|
|
1490
|
+
function parseFile(file, schema, raw) {
|
|
815
1491
|
const result = schema.safeParse(raw);
|
|
816
1492
|
if (!result.success) {
|
|
817
1493
|
const issues = result.error.issues.slice(0, 5).map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
@@ -825,17 +1501,35 @@ async function loadBundle(dir) {
|
|
|
825
1501
|
"Run `replicax init` to create one."
|
|
826
1502
|
]);
|
|
827
1503
|
}
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1504
|
+
const rawFiles = {
|
|
1505
|
+
profile: await readRawFile(dir, PROFILE_FILES.profile),
|
|
1506
|
+
tooling: await readRawFile(dir, PROFILE_FILES.tooling),
|
|
1507
|
+
structure: await readRawFile(dir, PROFILE_FILES.structure),
|
|
1508
|
+
metadata: await readRawFile(dir, PROFILE_FILES.metadata),
|
|
1509
|
+
checksum: await readRawFile(dir, PROFILE_FILES.checksum)
|
|
1510
|
+
};
|
|
1511
|
+
const detectedVersion = typeof rawFiles.profile.replicaxVersion === "string" ? rawFiles.profile.replicaxVersion : "2.0.0";
|
|
1512
|
+
const { raw } = migrateRawBundle(rawFiles, detectedVersion);
|
|
1513
|
+
const profile = parseFile(PROFILE_FILES.profile, ProfileSchema, raw.profile);
|
|
1514
|
+
const tooling = parseFile(PROFILE_FILES.tooling, ToolingSchema, raw.tooling);
|
|
1515
|
+
const structure = parseFile(PROFILE_FILES.structure, StructureSchema, raw.structure);
|
|
1516
|
+
const metadata = parseFile(PROFILE_FILES.metadata, MetadataSchema, raw.metadata);
|
|
1517
|
+
const checksum = parseFile(PROFILE_FILES.checksum, ChecksumSchema, raw.checksum);
|
|
1518
|
+
const manifestPath = path6.join(dir, PROFILE_FILES.manifest);
|
|
1519
|
+
const manifest = await fs5.pathExists(manifestPath) ? parseFile(
|
|
1520
|
+
PROFILE_FILES.manifest,
|
|
1521
|
+
ManifestSchema,
|
|
1522
|
+
await readRawFile(dir, PROFILE_FILES.manifest)
|
|
1523
|
+
) : buildManifest(tooling, checksum);
|
|
1524
|
+
return { profile, tooling, structure, metadata, checksum, manifest };
|
|
836
1525
|
}
|
|
837
1526
|
|
|
838
1527
|
// src/commands/report.ts
|
|
1528
|
+
function statusLine(ok, label, note) {
|
|
1529
|
+
const mark = ok ? pc.green("\u2713") : pc.red("\u2717");
|
|
1530
|
+
const text = ok ? label : pc.dim(label);
|
|
1531
|
+
return note ? `${mark} ${text} ${pc.dim(note)}` : `${mark} ${text}`;
|
|
1532
|
+
}
|
|
839
1533
|
function toolingByCategory(tooling) {
|
|
840
1534
|
const counts = /* @__PURE__ */ new Map();
|
|
841
1535
|
for (const file of tooling.files) {
|
|
@@ -844,7 +1538,7 @@ function toolingByCategory(tooling) {
|
|
|
844
1538
|
if (tooling.packageJson) {
|
|
845
1539
|
counts.set("package", (counts.get("package") ?? 0) + 1);
|
|
846
1540
|
}
|
|
847
|
-
return [...counts.entries()].map(([id, n]) => [
|
|
1541
|
+
return [...counts.entries()].map(([id, n]) => [categoryLabel(id), n]).sort((a, b) => a[0].localeCompare(b[0]));
|
|
848
1542
|
}
|
|
849
1543
|
function printScanSummary(bundle) {
|
|
850
1544
|
const { metadata, tooling, structure } = bundle;
|
|
@@ -854,6 +1548,7 @@ function printScanSummary(bundle) {
|
|
|
854
1548
|
logger.hint(`framework ${metadata.framework}`);
|
|
855
1549
|
logger.hint(`packageManager ${metadata.packageManager}`);
|
|
856
1550
|
logger.hint(`nodeVersion ${metadata.nodeVersion}`);
|
|
1551
|
+
printDetections(metadata.detections ?? []);
|
|
857
1552
|
logger.newline();
|
|
858
1553
|
logger.info(pc.bold(`Tooling (${tooling.files.length + (tooling.packageJson ? 1 : 0)} files)`));
|
|
859
1554
|
for (const [label, count] of toolingByCategory(tooling)) {
|
|
@@ -862,6 +1557,15 @@ function printScanSummary(bundle) {
|
|
|
862
1557
|
logger.newline();
|
|
863
1558
|
logger.info(pc.bold(`Structure (${structure.directories.length} directories)`));
|
|
864
1559
|
}
|
|
1560
|
+
function printDetections(detections) {
|
|
1561
|
+
if (detections.length === 0) return;
|
|
1562
|
+
logger.newline();
|
|
1563
|
+
logger.info(pc.bold(`Detected (${detections.length})`));
|
|
1564
|
+
for (const d of detections) {
|
|
1565
|
+
const pct = d.confidence < 1 ? pc.dim(` (${Math.round(d.confidence * 100)}%)`) : "";
|
|
1566
|
+
logger.hint(`${pc.green("\u2713")} ${d.name}${pct}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
865
1569
|
function reportSkippedSecrets(skipped) {
|
|
866
1570
|
if (skipped.length === 0) return;
|
|
867
1571
|
logger.warn(`Excluded ${skipped.length} protected file(s) from the profile:`);
|
|
@@ -910,7 +1614,7 @@ async function initCommand(options) {
|
|
|
910
1614
|
spinner.succeed(
|
|
911
1615
|
`Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
|
|
912
1616
|
);
|
|
913
|
-
const name = options.name ??
|
|
1617
|
+
const name = options.name ?? path7.basename(path7.resolve(root)) ?? "project";
|
|
914
1618
|
const bundle = buildBundle({
|
|
915
1619
|
name,
|
|
916
1620
|
tooling: scan.tooling,
|
|
@@ -935,21 +1639,21 @@ async function initCommand(options) {
|
|
|
935
1639
|
logger.hint("Create a project from it with: replicax create <project-name>");
|
|
936
1640
|
}
|
|
937
1641
|
async function maybeWriteIgnoreFile(root) {
|
|
938
|
-
const file =
|
|
939
|
-
if (await
|
|
1642
|
+
const file = path7.join(root, IGNORE_FILE);
|
|
1643
|
+
if (await fs6.pathExists(file)) return;
|
|
940
1644
|
const create = process.stdin.isTTY ? await confirm({
|
|
941
1645
|
message: `Create a starter ${IGNORE_FILE} to control what gets exported?`,
|
|
942
1646
|
default: true
|
|
943
1647
|
}) : false;
|
|
944
1648
|
if (create) {
|
|
945
|
-
await
|
|
1649
|
+
await fs6.writeFile(file, DEFAULT_IGNORE_FILE_CONTENTS, "utf8");
|
|
946
1650
|
logger.success(`Wrote ${IGNORE_FILE}`);
|
|
947
1651
|
}
|
|
948
1652
|
}
|
|
949
1653
|
|
|
950
1654
|
// src/commands/init-skill.ts
|
|
951
|
-
import
|
|
952
|
-
import
|
|
1655
|
+
import path8 from "path";
|
|
1656
|
+
import fs7 from "fs-extra";
|
|
953
1657
|
import ora2 from "ora";
|
|
954
1658
|
|
|
955
1659
|
// src/config/ai-targets.ts
|
|
@@ -983,7 +1687,7 @@ function slugify(input, fallback = "project") {
|
|
|
983
1687
|
}
|
|
984
1688
|
|
|
985
1689
|
// src/core/skill-generator.ts
|
|
986
|
-
var
|
|
1690
|
+
var FRAMEWORK_LABELS2 = {
|
|
987
1691
|
next: "Next.js",
|
|
988
1692
|
nuxt: "Nuxt",
|
|
989
1693
|
remix: "Remix",
|
|
@@ -1047,7 +1751,7 @@ function orderedScripts(scripts) {
|
|
|
1047
1751
|
function toolingByCategoryLabel(tooling) {
|
|
1048
1752
|
const groups = /* @__PURE__ */ new Map();
|
|
1049
1753
|
for (const file of tooling.files) {
|
|
1050
|
-
const label =
|
|
1754
|
+
const label = categoryLabel(file.category);
|
|
1051
1755
|
const list = groups.get(label) ?? [];
|
|
1052
1756
|
list.push(file.path);
|
|
1053
1757
|
groups.set(label, list);
|
|
@@ -1118,7 +1822,7 @@ function buildSkill(args) {
|
|
|
1118
1822
|
const { name, metadata, tooling, structure, pkg } = args;
|
|
1119
1823
|
const slug = slugify(name);
|
|
1120
1824
|
const pm = metadata.packageManager;
|
|
1121
|
-
const framework =
|
|
1825
|
+
const framework = FRAMEWORK_LABELS2[metadata.framework] ?? metadata.framework;
|
|
1122
1826
|
const language = metadata.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
1123
1827
|
const scripts = pkg?.scripts ?? {};
|
|
1124
1828
|
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 +1890,9 @@ function buildSkill(args) {
|
|
|
1186
1890
|
}
|
|
1187
1891
|
|
|
1188
1892
|
// src/core/ai/cli.ts
|
|
1893
|
+
import { spawn as spawn2 } from "child_process";
|
|
1894
|
+
|
|
1895
|
+
// src/core/process.ts
|
|
1189
1896
|
import { spawn } from "child_process";
|
|
1190
1897
|
async function commandExists(bin) {
|
|
1191
1898
|
const onWindows = process.platform === "win32";
|
|
@@ -1202,9 +1909,45 @@ async function commandExists(bin) {
|
|
|
1202
1909
|
child.on("close", (code) => resolve(code === 0));
|
|
1203
1910
|
});
|
|
1204
1911
|
}
|
|
1912
|
+
async function getCommandOutput(bin, args = [], options = {}) {
|
|
1913
|
+
const { timeoutMs = 5e3, shell = false } = options;
|
|
1914
|
+
return new Promise((resolve) => {
|
|
1915
|
+
let settled = false;
|
|
1916
|
+
const finish = (result) => {
|
|
1917
|
+
if (settled) return;
|
|
1918
|
+
settled = true;
|
|
1919
|
+
resolve(result);
|
|
1920
|
+
};
|
|
1921
|
+
let child;
|
|
1922
|
+
try {
|
|
1923
|
+
child = spawn(bin, args, { shell, windowsHide: true });
|
|
1924
|
+
} catch {
|
|
1925
|
+
finish({ ok: false, stdout: "", stderr: "", code: null });
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
let stdout = "";
|
|
1929
|
+
let stderr = "";
|
|
1930
|
+
const timer = setTimeout(() => {
|
|
1931
|
+
child.kill();
|
|
1932
|
+
finish({ ok: false, stdout, stderr, code: null });
|
|
1933
|
+
}, timeoutMs);
|
|
1934
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
1935
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
1936
|
+
child.on("error", () => {
|
|
1937
|
+
clearTimeout(timer);
|
|
1938
|
+
finish({ ok: false, stdout, stderr, code: null });
|
|
1939
|
+
});
|
|
1940
|
+
child.on("close", (code) => {
|
|
1941
|
+
clearTimeout(timer);
|
|
1942
|
+
finish({ ok: code === 0, stdout, stderr, code });
|
|
1943
|
+
});
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// src/core/ai/cli.ts
|
|
1205
1948
|
async function runWithStdin(bin, args, input, timeoutMs = 12e4) {
|
|
1206
1949
|
return new Promise((resolve, reject) => {
|
|
1207
|
-
const child =
|
|
1950
|
+
const child = spawn2(bin, args, { shell: true, windowsHide: true });
|
|
1208
1951
|
let stdout = "";
|
|
1209
1952
|
let stderr = "";
|
|
1210
1953
|
const timer = setTimeout(() => {
|
|
@@ -1348,6 +2091,12 @@ async function resolveProvider(preference, modelOverride) {
|
|
|
1348
2091
|
function buildSkillPrompt(args) {
|
|
1349
2092
|
const scripts = Object.entries(args.scripts).map(([name, cmd]) => ` ${name}: ${cmd}`).join("\n");
|
|
1350
2093
|
const tooling = args.toolingPaths.map((p) => ` ${p}`).join("\n");
|
|
2094
|
+
const template = args.rootSkill?.trim();
|
|
2095
|
+
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." : "";
|
|
2096
|
+
const templateSection = template ? `
|
|
2097
|
+
USER SKILL TEMPLATE (project root SKILL.md \u2014 use this as the base; preserve and refine, keep "name: ${args.slug}" in the frontmatter):
|
|
2098
|
+
${template}
|
|
2099
|
+
` : "";
|
|
1351
2100
|
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
2101
|
|
|
1353
2102
|
STRICT RULES:
|
|
@@ -1361,11 +2110,11 @@ REQUIREMENTS:
|
|
|
1361
2110
|
- 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
2111
|
- You MAY add a few supporting files under "references/" (e.g. "references/commands.md") when genuinely useful. Keep the bundle small and focused.
|
|
1363
2112
|
- 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}
|
|
2113
|
+
- This skill targets ${args.target.label} and will be installed at ${args.entryPath}.${templateRule}
|
|
1365
2114
|
|
|
1366
2115
|
PROJECT ANALYSIS (ground truth \u2014 refine and expand this, do not contradict it):
|
|
1367
2116
|
${args.analysis}
|
|
1368
|
-
|
|
2117
|
+
${templateSection}
|
|
1369
2118
|
CAPTURED CONFIG FILES:
|
|
1370
2119
|
${tooling || " (none)"}
|
|
1371
2120
|
|
|
@@ -1431,6 +2180,8 @@ async function initSkillCommand(options) {
|
|
|
1431
2180
|
spinner.succeed(
|
|
1432
2181
|
`Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
|
|
1433
2182
|
);
|
|
2183
|
+
const rootSkillPath = path8.join(root, ROOT_SKILL_FILE);
|
|
2184
|
+
const rootSkill = await fs7.pathExists(rootSkillPath) ? await fs7.readFile(rootSkillPath, "utf8") : void 0;
|
|
1434
2185
|
const name = options.name ?? scan.structure.root;
|
|
1435
2186
|
const seed = buildSkill({
|
|
1436
2187
|
name,
|
|
@@ -1458,6 +2209,7 @@ async function initSkillCommand(options) {
|
|
|
1458
2209
|
const provider = await resolveProvider(options.provider, options.model);
|
|
1459
2210
|
if (provider) {
|
|
1460
2211
|
logger.info(`Generating with ${provider.via} (sending project setup only)\u2026`);
|
|
2212
|
+
if (rootSkill?.trim()) logger.info(`Using ${ROOT_SKILL_FILE} as the skill template.`);
|
|
1461
2213
|
const aiSpinner = ora2({ text: "Authoring skill\u2026", isEnabled: !options.verbose }).start();
|
|
1462
2214
|
try {
|
|
1463
2215
|
const prompt = buildSkillPrompt({
|
|
@@ -1467,7 +2219,8 @@ async function initSkillCommand(options) {
|
|
|
1467
2219
|
target,
|
|
1468
2220
|
analysis: seed.content,
|
|
1469
2221
|
toolingPaths: scan.tooling.files.map((f) => f.path),
|
|
1470
|
-
scripts: scan.pkg?.scripts ?? {}
|
|
2222
|
+
scripts: scan.pkg?.scripts ?? {},
|
|
2223
|
+
rootSkill
|
|
1471
2224
|
});
|
|
1472
2225
|
const raw = await provider.run(prompt);
|
|
1473
2226
|
const parsed = parseSkillBundle(raw);
|
|
@@ -1499,11 +2252,11 @@ async function initSkillCommand(options) {
|
|
|
1499
2252
|
if (!safe) {
|
|
1500
2253
|
throw new ReplicaxError(`Refusing to write unsafe skill path: ${f.path}`);
|
|
1501
2254
|
}
|
|
1502
|
-
return { rel: safe, abs:
|
|
2255
|
+
return { rel: safe, abs: path8.join(root, ...safe.split("/")), content: f.content };
|
|
1503
2256
|
});
|
|
1504
2257
|
const conflicts = [];
|
|
1505
2258
|
for (const file of planned) {
|
|
1506
|
-
if (await
|
|
2259
|
+
if (await fs7.pathExists(file.abs)) conflicts.push(file.rel);
|
|
1507
2260
|
}
|
|
1508
2261
|
if (conflicts.length > 0 && !options.force) {
|
|
1509
2262
|
throw new ReplicaxError(
|
|
@@ -1512,26 +2265,26 @@ async function initSkillCommand(options) {
|
|
|
1512
2265
|
);
|
|
1513
2266
|
}
|
|
1514
2267
|
for (const file of planned) {
|
|
1515
|
-
await
|
|
1516
|
-
await
|
|
2268
|
+
await fs7.ensureDir(path8.dirname(file.abs));
|
|
2269
|
+
await fs7.writeFile(file.abs, file.content, "utf8");
|
|
1517
2270
|
logger.detail(`wrote: ${file.rel}`);
|
|
1518
2271
|
}
|
|
1519
2272
|
logger.newline();
|
|
1520
2273
|
logger.success(`Skill "${seed.slug}" written (${planned.length} file(s), via ${via})`);
|
|
1521
2274
|
logger.hint(
|
|
1522
|
-
`Location: ${relPosix(root,
|
|
2275
|
+
`Location: ${relPosix(root, path8.join(root, ...(bundleRoot || entryFile).split("/")))}`
|
|
1523
2276
|
);
|
|
1524
2277
|
logger.hint(target.note);
|
|
1525
2278
|
}
|
|
1526
2279
|
|
|
1527
2280
|
// src/commands/extract.ts
|
|
1528
|
-
import
|
|
2281
|
+
import path10 from "path";
|
|
1529
2282
|
import ora3 from "ora";
|
|
1530
2283
|
|
|
1531
2284
|
// src/core/github.ts
|
|
1532
2285
|
import os from "os";
|
|
1533
|
-
import
|
|
1534
|
-
import
|
|
2286
|
+
import path9 from "path";
|
|
2287
|
+
import fs8 from "fs-extra";
|
|
1535
2288
|
import { extract as tarExtract } from "tar";
|
|
1536
2289
|
function parseGitHubRef(input) {
|
|
1537
2290
|
const raw = input.trim();
|
|
@@ -1602,9 +2355,9 @@ function httpError(status, slug, hasToken) {
|
|
|
1602
2355
|
return new ReplicaxError(`GitHub returned HTTP ${status} for ${slug}.`);
|
|
1603
2356
|
}
|
|
1604
2357
|
async function firstSubdir(dir) {
|
|
1605
|
-
const entries = await
|
|
2358
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1606
2359
|
for (const entry of entries) {
|
|
1607
|
-
if (entry.isDirectory()) return
|
|
2360
|
+
if (entry.isDirectory()) return path9.join(dir, entry.name);
|
|
1608
2361
|
}
|
|
1609
2362
|
return null;
|
|
1610
2363
|
}
|
|
@@ -1628,13 +2381,13 @@ async function downloadRepo(ref) {
|
|
|
1628
2381
|
if (!res.ok) {
|
|
1629
2382
|
throw httpError(res.status, slug, Boolean(token));
|
|
1630
2383
|
}
|
|
1631
|
-
const tmpRoot = await
|
|
1632
|
-
const cleanup = () =>
|
|
2384
|
+
const tmpRoot = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-extract-"));
|
|
2385
|
+
const cleanup = () => fs8.remove(tmpRoot);
|
|
1633
2386
|
try {
|
|
1634
|
-
const tarPath =
|
|
1635
|
-
await
|
|
1636
|
-
const extractDir =
|
|
1637
|
-
await
|
|
2387
|
+
const tarPath = path9.join(tmpRoot, "repo.tar.gz");
|
|
2388
|
+
await fs8.writeFile(tarPath, Buffer.from(await res.arrayBuffer()));
|
|
2389
|
+
const extractDir = path9.join(tmpRoot, "src");
|
|
2390
|
+
await fs8.ensureDir(extractDir);
|
|
1638
2391
|
await tarExtract({ file: tarPath, cwd: extractDir, strip: 0 });
|
|
1639
2392
|
const repoRoot = await firstSubdir(extractDir);
|
|
1640
2393
|
if (!repoRoot) {
|
|
@@ -1687,7 +2440,7 @@ async function extractCommand(repo, options) {
|
|
|
1687
2440
|
logger.info("Dry run \u2014 no files were written.");
|
|
1688
2441
|
return;
|
|
1689
2442
|
}
|
|
1690
|
-
const outRoot = options.out ?
|
|
2443
|
+
const outRoot = options.out ? path10.resolve(options.out) : process.cwd();
|
|
1691
2444
|
const dir = profileDir(outRoot);
|
|
1692
2445
|
if (await profileExists(dir)) {
|
|
1693
2446
|
logger.warn(
|
|
@@ -1704,8 +2457,8 @@ async function extractCommand(repo, options) {
|
|
|
1704
2457
|
}
|
|
1705
2458
|
|
|
1706
2459
|
// src/commands/create.ts
|
|
1707
|
-
import
|
|
1708
|
-
import
|
|
2460
|
+
import path12 from "path";
|
|
2461
|
+
import fs10 from "fs-extra";
|
|
1709
2462
|
|
|
1710
2463
|
// src/core/conflict-resolver.ts
|
|
1711
2464
|
import { select } from "@inquirer/prompts";
|
|
@@ -1747,8 +2500,8 @@ var ConflictResolver = class {
|
|
|
1747
2500
|
};
|
|
1748
2501
|
|
|
1749
2502
|
// src/core/project-generator.ts
|
|
1750
|
-
import
|
|
1751
|
-
import
|
|
2503
|
+
import path11 from "path";
|
|
2504
|
+
import fs9 from "fs-extra";
|
|
1752
2505
|
async function generateProject(options) {
|
|
1753
2506
|
const { bundle, targetDir, projectName, dryRun, conflict } = options;
|
|
1754
2507
|
const result = {
|
|
@@ -1758,16 +2511,16 @@ async function generateProject(options) {
|
|
|
1758
2511
|
filesSkipped: 0,
|
|
1759
2512
|
unsafeSkipped: []
|
|
1760
2513
|
};
|
|
1761
|
-
if (!dryRun) await
|
|
2514
|
+
if (!dryRun) await fs9.ensureDir(targetDir);
|
|
1762
2515
|
for (const dir of bundle.structure.directories) {
|
|
1763
2516
|
const safe = safeJoinable(dir);
|
|
1764
2517
|
if (!safe) {
|
|
1765
2518
|
result.unsafeSkipped.push(dir);
|
|
1766
2519
|
continue;
|
|
1767
2520
|
}
|
|
1768
|
-
const full =
|
|
1769
|
-
const existed = await
|
|
1770
|
-
if (!dryRun) await
|
|
2521
|
+
const full = path11.join(targetDir, safe);
|
|
2522
|
+
const existed = await fs9.pathExists(full);
|
|
2523
|
+
if (!dryRun) await fs9.ensureDir(full);
|
|
1771
2524
|
if (!existed) result.dirsCreated += 1;
|
|
1772
2525
|
result.entries.push({ kind: "dir", path: safe, action: existed ? "skip" : "create" });
|
|
1773
2526
|
}
|
|
@@ -1791,8 +2544,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1791
2544
|
logger.warn(`Refusing to write unsafe path from profile: ${relPath}`);
|
|
1792
2545
|
return;
|
|
1793
2546
|
}
|
|
1794
|
-
const full =
|
|
1795
|
-
const exists = await
|
|
2547
|
+
const full = path11.join(options.targetDir, safe);
|
|
2548
|
+
const exists = await fs9.pathExists(full);
|
|
1796
2549
|
let action2 = exists ? "overwrite" : "create";
|
|
1797
2550
|
if (exists) {
|
|
1798
2551
|
const decision = await options.conflict.resolve(safe);
|
|
@@ -1805,8 +2558,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1805
2558
|
action2 = "overwrite";
|
|
1806
2559
|
}
|
|
1807
2560
|
if (!options.dryRun) {
|
|
1808
|
-
await
|
|
1809
|
-
await
|
|
2561
|
+
await fs9.ensureDir(path11.dirname(full));
|
|
2562
|
+
await fs9.writeFile(full, content, "utf8");
|
|
1810
2563
|
}
|
|
1811
2564
|
result.filesWritten += 1;
|
|
1812
2565
|
result.entries.push({ kind: "file", path: safe, action: action2 });
|
|
@@ -1814,7 +2567,7 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1814
2567
|
}
|
|
1815
2568
|
|
|
1816
2569
|
// src/core/installer.ts
|
|
1817
|
-
import { spawn as
|
|
2570
|
+
import { spawn as spawn3 } from "child_process";
|
|
1818
2571
|
var COMMANDS = {
|
|
1819
2572
|
npm: ["npm", "install"],
|
|
1820
2573
|
pnpm: ["pnpm", "install"],
|
|
@@ -1825,7 +2578,7 @@ function installDependencies(cwd, manager) {
|
|
|
1825
2578
|
if (manager === "unknown") return Promise.resolve(false);
|
|
1826
2579
|
const [command, ...args] = COMMANDS[manager];
|
|
1827
2580
|
return new Promise((resolve) => {
|
|
1828
|
-
const child =
|
|
2581
|
+
const child = spawn3(command, args, {
|
|
1829
2582
|
cwd,
|
|
1830
2583
|
stdio: "inherit",
|
|
1831
2584
|
// npm/pnpm/yarn are .cmd shims on Windows; a shell resolves them.
|
|
@@ -1855,9 +2608,9 @@ async function createCommand(projectName, options) {
|
|
|
1855
2608
|
logger.warn(`Profile integrity check found ${mismatches.length} issue(s); continuing anyway.`);
|
|
1856
2609
|
logger.hint("Run `replicax validate` for details.");
|
|
1857
2610
|
}
|
|
1858
|
-
const targetDir =
|
|
1859
|
-
const leafName =
|
|
1860
|
-
if (
|
|
2611
|
+
const targetDir = path12.resolve(process.cwd(), projectName);
|
|
2612
|
+
const leafName = path12.basename(targetDir);
|
|
2613
|
+
if (path12.resolve(process.cwd()) === targetDir) {
|
|
1861
2614
|
throw new ReplicaxError("Refusing to scaffold into the current directory.", [
|
|
1862
2615
|
"Pass a new project name, e.g. `replicax create my-app`."
|
|
1863
2616
|
]);
|
|
@@ -1906,8 +2659,8 @@ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
|
|
|
1906
2659
|
logger.hint("No package manager detected; run your install command manually.");
|
|
1907
2660
|
return;
|
|
1908
2661
|
}
|
|
1909
|
-
const pkgPath =
|
|
1910
|
-
const pkg = await
|
|
2662
|
+
const pkgPath = path12.join(targetDir, "package.json");
|
|
2663
|
+
const pkg = await fs10.readJson(pkgPath).catch(() => null);
|
|
1911
2664
|
if (!pkg?.devDependencies || Object.keys(pkg.devDependencies).length === 0) {
|
|
1912
2665
|
logger.hint("No dependencies to install.");
|
|
1913
2666
|
return;
|
|
@@ -1923,27 +2676,27 @@ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
|
|
|
1923
2676
|
import ora4 from "ora";
|
|
1924
2677
|
|
|
1925
2678
|
// src/core/diff.ts
|
|
1926
|
-
function
|
|
2679
|
+
function diffStringMaps(prev, next, options = {}) {
|
|
2680
|
+
const ignore2 = options.ignoreKeys;
|
|
1927
2681
|
const added = [];
|
|
1928
2682
|
const removed = [];
|
|
1929
2683
|
const changed = [];
|
|
1930
|
-
|
|
1931
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(prev.files), ...Object.keys(next.files)]);
|
|
2684
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
1932
2685
|
for (const key of keys) {
|
|
1933
|
-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
if (before !== after) packageJsonChanged = true;
|
|
1937
|
-
continue;
|
|
1938
|
-
}
|
|
2686
|
+
if (ignore2?.has(key)) continue;
|
|
2687
|
+
const before = prev[key];
|
|
2688
|
+
const after = next[key];
|
|
1939
2689
|
if (before === void 0) added.push(key);
|
|
1940
2690
|
else if (after === void 0) removed.push(key);
|
|
1941
2691
|
else if (before !== after) changed.push(key);
|
|
1942
2692
|
}
|
|
1943
|
-
return {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2693
|
+
return { added: added.sort(), removed: removed.sort(), changed: changed.sort() };
|
|
2694
|
+
}
|
|
2695
|
+
var PACKAGE_JSON_KEYS = /* @__PURE__ */ new Set([PACKAGE_JSON_KEY]);
|
|
2696
|
+
function diffChecksums(prev, next) {
|
|
2697
|
+
const files = diffStringMaps(prev.files, next.files, { ignoreKeys: PACKAGE_JSON_KEYS });
|
|
2698
|
+
const packageJsonChanged = prev.files[PACKAGE_JSON_KEY] !== next.files[PACKAGE_JSON_KEY];
|
|
2699
|
+
return { files, packageJsonChanged };
|
|
1947
2700
|
}
|
|
1948
2701
|
function diffStructure(prev, next) {
|
|
1949
2702
|
const before = new Set(prev.directories);
|
|
@@ -2053,7 +2806,7 @@ function formatBytes(bytes) {
|
|
|
2053
2806
|
}
|
|
2054
2807
|
|
|
2055
2808
|
// src/commands/inspect.ts
|
|
2056
|
-
var SECTIONS = ["profile", "tooling", "structure", "metadata"];
|
|
2809
|
+
var SECTIONS = ["profile", "tooling", "structure", "metadata", "detections"];
|
|
2057
2810
|
async function inspectCommand(options) {
|
|
2058
2811
|
const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
|
|
2059
2812
|
if (!await profileExists(dir)) {
|
|
@@ -2067,15 +2820,38 @@ async function inspectCommand(options) {
|
|
|
2067
2820
|
const bundle = await loadBundle(dir);
|
|
2068
2821
|
const section = options.section;
|
|
2069
2822
|
if (options.json) {
|
|
2070
|
-
|
|
2071
|
-
logger.out(JSON.stringify(payload, null, 2));
|
|
2823
|
+
logger.out(JSON.stringify(jsonPayload(bundle, section), null, 2));
|
|
2072
2824
|
return;
|
|
2073
2825
|
}
|
|
2074
2826
|
if (!section || section === "profile") printProfile(bundle);
|
|
2075
2827
|
if (!section || section === "metadata") printMetadata(bundle);
|
|
2828
|
+
if (!section || section === "detections") printDetectionsSection(bundle);
|
|
2076
2829
|
if (!section || section === "tooling") printTooling(bundle);
|
|
2077
2830
|
if (!section || section === "structure") printStructure(bundle);
|
|
2078
2831
|
}
|
|
2832
|
+
function jsonPayload(bundle, section) {
|
|
2833
|
+
if (!section) return bundle;
|
|
2834
|
+
if (section === "detections") return { detections: bundle.metadata.detections ?? [] };
|
|
2835
|
+
return { [section]: bundle[section] };
|
|
2836
|
+
}
|
|
2837
|
+
function printDetectionsSection(bundle) {
|
|
2838
|
+
const detections = bundle.metadata.detections ?? [];
|
|
2839
|
+
logger.out(pc.bold(`Detections (${detections.length})`));
|
|
2840
|
+
if (detections.length === 0) {
|
|
2841
|
+
logger.out(" (none)");
|
|
2842
|
+
logger.out("");
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
const table = new Table({
|
|
2846
|
+
head: ["Category", "Tool", "Confidence", "Evidence"],
|
|
2847
|
+
style: { head: ["cyan"], border: ["dim"] }
|
|
2848
|
+
});
|
|
2849
|
+
for (const d of detections) {
|
|
2850
|
+
table.push([d.category, d.name, `${Math.round(d.confidence * 100)}%`, d.evidence.join(", ")]);
|
|
2851
|
+
}
|
|
2852
|
+
logger.out(table.toString());
|
|
2853
|
+
logger.out("");
|
|
2854
|
+
}
|
|
2079
2855
|
function printProfile(bundle) {
|
|
2080
2856
|
const p = bundle.profile;
|
|
2081
2857
|
logger.out(pc.bold("Profile"));
|
|
@@ -2109,12 +2885,7 @@ function printTooling(bundle) {
|
|
|
2109
2885
|
table.push(["Package Management & Monorepos", "package.json", "json", "template"]);
|
|
2110
2886
|
}
|
|
2111
2887
|
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
|
-
]);
|
|
2888
|
+
table.push([categoryLabel(file.category), file.path, file.variant, formatBytes(file.bytes)]);
|
|
2118
2889
|
}
|
|
2119
2890
|
logger.out(table.toString());
|
|
2120
2891
|
logger.out("");
|
|
@@ -2162,20 +2933,20 @@ async function validateCommand(options) {
|
|
|
2162
2933
|
}
|
|
2163
2934
|
|
|
2164
2935
|
// src/commands/export.ts
|
|
2165
|
-
import
|
|
2166
|
-
import
|
|
2936
|
+
import path14 from "path";
|
|
2937
|
+
import fs12 from "fs-extra";
|
|
2167
2938
|
import ora5 from "ora";
|
|
2168
2939
|
|
|
2169
2940
|
// src/core/archive.ts
|
|
2170
2941
|
import os2 from "os";
|
|
2171
|
-
import
|
|
2172
|
-
import
|
|
2942
|
+
import path13 from "path";
|
|
2943
|
+
import fs11 from "fs-extra";
|
|
2173
2944
|
import { create as tarCreate, extract as tarExtract2 } from "tar";
|
|
2174
2945
|
async function exportProfile(profileDirectory, outPath) {
|
|
2175
|
-
const resolvedOut =
|
|
2176
|
-
await
|
|
2177
|
-
const parent =
|
|
2178
|
-
const base =
|
|
2946
|
+
const resolvedOut = path13.resolve(outPath);
|
|
2947
|
+
await fs11.ensureDir(path13.dirname(resolvedOut));
|
|
2948
|
+
const parent = path13.dirname(profileDirectory);
|
|
2949
|
+
const base = path13.basename(profileDirectory);
|
|
2179
2950
|
await tarCreate(
|
|
2180
2951
|
{
|
|
2181
2952
|
gzip: true,
|
|
@@ -2188,21 +2959,21 @@ async function exportProfile(profileDirectory, outPath) {
|
|
|
2188
2959
|
);
|
|
2189
2960
|
}
|
|
2190
2961
|
async function extractToTemp(archivePath) {
|
|
2191
|
-
const resolved =
|
|
2192
|
-
if (!await
|
|
2962
|
+
const resolved = path13.resolve(archivePath);
|
|
2963
|
+
if (!await fs11.pathExists(resolved)) {
|
|
2193
2964
|
throw new Error(`Archive not found: ${archivePath}`);
|
|
2194
2965
|
}
|
|
2195
|
-
const tmp = await
|
|
2966
|
+
const tmp = await fs11.mkdtemp(path13.join(os2.tmpdir(), "replicax-import-"));
|
|
2196
2967
|
await tarExtract2({ file: resolved, cwd: tmp, strip: 0 });
|
|
2197
2968
|
return tmp;
|
|
2198
2969
|
}
|
|
2199
2970
|
async function findProfileRoot(dir) {
|
|
2200
|
-
const hasProfile = async (d) =>
|
|
2971
|
+
const hasProfile = async (d) => fs11.pathExists(path13.join(d, PROFILE_FILES.profile));
|
|
2201
2972
|
if (await hasProfile(dir)) return dir;
|
|
2202
|
-
const entries = await
|
|
2973
|
+
const entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
2203
2974
|
for (const entry of entries) {
|
|
2204
2975
|
if (entry.isDirectory()) {
|
|
2205
|
-
const candidate =
|
|
2976
|
+
const candidate = path13.join(dir, entry.name);
|
|
2206
2977
|
if (await hasProfile(candidate)) return candidate;
|
|
2207
2978
|
}
|
|
2208
2979
|
}
|
|
@@ -2216,21 +2987,21 @@ async function exportCommand(options) {
|
|
|
2216
2987
|
throw new ReplicaxError("No ReplicaX profile found to export.", ["Run `replicax init` first."]);
|
|
2217
2988
|
}
|
|
2218
2989
|
const bundle = await loadBundle(dir);
|
|
2219
|
-
const outPath =
|
|
2990
|
+
const outPath = path14.resolve(
|
|
2220
2991
|
options.out ?? `${slugify(bundle.profile.name, "profile")}.replicax.tar.gz`
|
|
2221
2992
|
);
|
|
2222
2993
|
const spinner = ora5({ text: "Packaging profile\u2026" }).start();
|
|
2223
2994
|
await exportProfile(dir, outPath);
|
|
2224
2995
|
spinner.stop();
|
|
2225
|
-
const { size } = await
|
|
2996
|
+
const { size } = await fs12.stat(outPath);
|
|
2226
2997
|
logger.success(
|
|
2227
|
-
`Exported "${bundle.profile.name}" \u2192 ${
|
|
2998
|
+
`Exported "${bundle.profile.name}" \u2192 ${path14.relative(process.cwd(), outPath)} (${formatBytes(size)})`
|
|
2228
2999
|
);
|
|
2229
3000
|
logger.hint("Share it, then `replicax import <file>` elsewhere.");
|
|
2230
3001
|
}
|
|
2231
3002
|
|
|
2232
3003
|
// src/commands/import.ts
|
|
2233
|
-
import
|
|
3004
|
+
import fs13 from "fs-extra";
|
|
2234
3005
|
import ora6 from "ora";
|
|
2235
3006
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
2236
3007
|
async function importCommand(archivePath, options) {
|
|
@@ -2258,7 +3029,7 @@ async function importCommand(archivePath, options) {
|
|
|
2258
3029
|
"Re-run with --force to overwrite it."
|
|
2259
3030
|
]);
|
|
2260
3031
|
}
|
|
2261
|
-
await
|
|
3032
|
+
await fs13.remove(dest);
|
|
2262
3033
|
}
|
|
2263
3034
|
await saveBundle(dest, bundle);
|
|
2264
3035
|
logger.newline();
|
|
@@ -2267,10 +3038,411 @@ async function importCommand(archivePath, options) {
|
|
|
2267
3038
|
);
|
|
2268
3039
|
logger.hint("Create a project with: replicax create <project-name>");
|
|
2269
3040
|
} finally {
|
|
2270
|
-
await
|
|
3041
|
+
await fs13.remove(tmp).catch(() => void 0);
|
|
2271
3042
|
}
|
|
2272
3043
|
}
|
|
2273
3044
|
|
|
3045
|
+
// src/config/environment-tools.ts
|
|
3046
|
+
var ENVIRONMENT_TOOLS = [
|
|
3047
|
+
{ id: "node", name: "Node.js", bin: "node", versionArgs: ["--version"], kind: "runtime" },
|
|
3048
|
+
{ id: "git", name: "Git", bin: "git", versionArgs: ["--version"], kind: "vcs" },
|
|
3049
|
+
{ id: "npm", name: "npm", bin: "npm", versionArgs: ["--version"], kind: "package-manager" },
|
|
3050
|
+
{ id: "pnpm", name: "pnpm", bin: "pnpm", versionArgs: ["--version"], kind: "package-manager" },
|
|
3051
|
+
{ id: "yarn", name: "Yarn", bin: "yarn", versionArgs: ["--version"], kind: "package-manager" },
|
|
3052
|
+
{ id: "bun", name: "Bun", bin: "bun", versionArgs: ["--version"], kind: "package-manager" },
|
|
3053
|
+
{ id: "docker", name: "Docker", bin: "docker", versionArgs: ["--version"], kind: "container" },
|
|
3054
|
+
{
|
|
3055
|
+
id: "vscode",
|
|
3056
|
+
name: "VS Code",
|
|
3057
|
+
bin: "code",
|
|
3058
|
+
versionArgs: ["--version"],
|
|
3059
|
+
kind: "editor",
|
|
3060
|
+
// `code --version` prints version on the first line, then commit + arch.
|
|
3061
|
+
parseVersion: (raw) => raw.split(/\r?\n/)[0]?.trim() || void 0
|
|
3062
|
+
},
|
|
3063
|
+
{ id: "cursor", name: "Cursor", bin: "cursor", versionArgs: ["--version"], kind: "editor" },
|
|
3064
|
+
{
|
|
3065
|
+
id: "claude-code",
|
|
3066
|
+
name: "Claude Code",
|
|
3067
|
+
bin: "claude",
|
|
3068
|
+
versionArgs: ["--version"],
|
|
3069
|
+
kind: "editor"
|
|
3070
|
+
},
|
|
3071
|
+
{ id: "windsurf", name: "Windsurf", bin: "windsurf", versionArgs: ["--version"], kind: "editor" }
|
|
3072
|
+
];
|
|
3073
|
+
|
|
3074
|
+
// src/core/environment.ts
|
|
3075
|
+
function parseVersionDefault(raw) {
|
|
3076
|
+
const trimmed = raw.trim();
|
|
3077
|
+
const semver = trimmed.match(/\d+\.\d+\.\d+(?:[-+][\w.]+)?/);
|
|
3078
|
+
if (semver) return semver[0];
|
|
3079
|
+
const loose = trimmed.match(/\d+\.\d+/);
|
|
3080
|
+
return loose ? loose[0] : void 0;
|
|
3081
|
+
}
|
|
3082
|
+
var defaultProbe = async (tool) => {
|
|
3083
|
+
const out = await getCommandOutput(tool.bin, tool.versionArgs, { shell: true });
|
|
3084
|
+
if (out.ok) {
|
|
3085
|
+
const raw = out.stdout.trim() || out.stderr.trim();
|
|
3086
|
+
const parse = tool.parseVersion ?? parseVersionDefault;
|
|
3087
|
+
return { found: true, version: parse(raw) };
|
|
3088
|
+
}
|
|
3089
|
+
return { found: await commandExists(tool.bin) };
|
|
3090
|
+
};
|
|
3091
|
+
async function runEnvironmentChecks(tools = ENVIRONMENT_TOOLS, probe = defaultProbe) {
|
|
3092
|
+
return Promise.all(
|
|
3093
|
+
tools.map(async (tool) => {
|
|
3094
|
+
const result = await probe(tool);
|
|
3095
|
+
return {
|
|
3096
|
+
id: tool.id,
|
|
3097
|
+
name: tool.name,
|
|
3098
|
+
kind: tool.kind,
|
|
3099
|
+
found: result.found,
|
|
3100
|
+
version: result.version
|
|
3101
|
+
};
|
|
3102
|
+
})
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
// src/commands/doctor.ts
|
|
3107
|
+
async function doctorCommand(options) {
|
|
3108
|
+
const checks = await runEnvironmentChecks();
|
|
3109
|
+
if (options.json) {
|
|
3110
|
+
logger.out(JSON.stringify({ checks }, null, 2));
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
logger.out(pc.bold("Developer environment"));
|
|
3114
|
+
logger.out("");
|
|
3115
|
+
for (const check of checks) {
|
|
3116
|
+
const note = check.found ? check.version : "not found";
|
|
3117
|
+
logger.out(statusLine(check.found, check.name, note));
|
|
3118
|
+
}
|
|
3119
|
+
const found = checks.filter((c) => c.found).length;
|
|
3120
|
+
logger.out("");
|
|
3121
|
+
logger.out(pc.dim(`${found}/${checks.length} tools found`));
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/commands/compare.ts
|
|
3125
|
+
import path15 from "path";
|
|
3126
|
+
import fs14 from "fs-extra";
|
|
3127
|
+
|
|
3128
|
+
// src/core/compare.ts
|
|
3129
|
+
function detectionsOf(bundle) {
|
|
3130
|
+
return bundle.metadata.detections ?? [];
|
|
3131
|
+
}
|
|
3132
|
+
var toolingComparator = {
|
|
3133
|
+
id: "tooling",
|
|
3134
|
+
title: "Tooling",
|
|
3135
|
+
compare(a, b) {
|
|
3136
|
+
const aById = new Map(detectionsOf(a).map((d) => [d.id, d]));
|
|
3137
|
+
const bById = new Map(detectionsOf(b).map((d) => [d.id, d]));
|
|
3138
|
+
const added = [];
|
|
3139
|
+
const removed = [];
|
|
3140
|
+
const changed = [];
|
|
3141
|
+
for (const [id, d] of bById) if (!aById.has(id)) added.push(d.name);
|
|
3142
|
+
for (const [id, d] of aById) if (!bById.has(id)) removed.push(d.name);
|
|
3143
|
+
for (const [id, d] of aById) {
|
|
3144
|
+
const other = bById.get(id);
|
|
3145
|
+
if (other && other.confidence !== d.confidence) changed.push(d.name);
|
|
3146
|
+
}
|
|
3147
|
+
return sortSection({ id: this.id, title: this.title, added, removed, changed });
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
var PACKAGE_JSON_KEYS2 = /* @__PURE__ */ new Set([PACKAGE_JSON_KEY]);
|
|
3151
|
+
var configFilesComparator = {
|
|
3152
|
+
id: "config-files",
|
|
3153
|
+
title: "Configuration files",
|
|
3154
|
+
compare(a, b) {
|
|
3155
|
+
const diff = diffStringMaps(a.checksum.files, b.checksum.files, {
|
|
3156
|
+
ignoreKeys: PACKAGE_JSON_KEYS2
|
|
3157
|
+
});
|
|
3158
|
+
return { id: this.id, title: this.title, ...diff };
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
var packageJsonComparator = {
|
|
3162
|
+
id: "package-json",
|
|
3163
|
+
title: "package.json",
|
|
3164
|
+
compare(a, b) {
|
|
3165
|
+
const flatten = (bundle) => {
|
|
3166
|
+
const pkg = bundle.tooling.packageJson;
|
|
3167
|
+
const out = {};
|
|
3168
|
+
for (const [name, cmd] of Object.entries(pkg?.scripts ?? {})) out[`script:${name}`] = cmd;
|
|
3169
|
+
for (const [name, ver] of Object.entries(pkg?.devDependencies ?? {})) {
|
|
3170
|
+
out[`devDependency:${name}`] = ver;
|
|
3171
|
+
}
|
|
3172
|
+
return out;
|
|
3173
|
+
};
|
|
3174
|
+
const diff = diffStringMaps(flatten(a), flatten(b));
|
|
3175
|
+
return { id: this.id, title: this.title, ...diff };
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
var structureComparator = {
|
|
3179
|
+
id: "structure",
|
|
3180
|
+
title: "Structure",
|
|
3181
|
+
compare(a, b) {
|
|
3182
|
+
const before = new Set(a.structure.directories);
|
|
3183
|
+
const after = new Set(b.structure.directories);
|
|
3184
|
+
const added = b.structure.directories.filter((d) => !before.has(d));
|
|
3185
|
+
const removed = a.structure.directories.filter((d) => !after.has(d));
|
|
3186
|
+
return sortSection({ id: this.id, title: this.title, added, removed, changed: [] });
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
var metadataComparator = {
|
|
3190
|
+
id: "metadata",
|
|
3191
|
+
title: "Metadata",
|
|
3192
|
+
compare(a, b) {
|
|
3193
|
+
const fields = [
|
|
3194
|
+
"language",
|
|
3195
|
+
"framework",
|
|
3196
|
+
"packageManager",
|
|
3197
|
+
"nodeVersion"
|
|
3198
|
+
];
|
|
3199
|
+
const changed = [];
|
|
3200
|
+
for (const field of fields) {
|
|
3201
|
+
const from = String(a.metadata[field] ?? "");
|
|
3202
|
+
const to = String(b.metadata[field] ?? "");
|
|
3203
|
+
if (from !== to) changed.push(`${field}: ${from} \u2192 ${to}`);
|
|
3204
|
+
}
|
|
3205
|
+
return { id: this.id, title: this.title, added: [], removed: [], changed };
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
3208
|
+
var COMPARATORS = [
|
|
3209
|
+
toolingComparator,
|
|
3210
|
+
configFilesComparator,
|
|
3211
|
+
packageJsonComparator,
|
|
3212
|
+
structureComparator,
|
|
3213
|
+
metadataComparator
|
|
3214
|
+
];
|
|
3215
|
+
function sortSection(section) {
|
|
3216
|
+
return {
|
|
3217
|
+
...section,
|
|
3218
|
+
added: [...section.added].sort(),
|
|
3219
|
+
removed: [...section.removed].sort(),
|
|
3220
|
+
changed: [...section.changed].sort()
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function compareBundles(a, b) {
|
|
3224
|
+
return { sections: COMPARATORS.map((c) => c.compare(a, b)) };
|
|
3225
|
+
}
|
|
3226
|
+
function sectionHasChanges(section) {
|
|
3227
|
+
return section.added.length > 0 || section.removed.length > 0 || section.changed.length > 0;
|
|
3228
|
+
}
|
|
3229
|
+
function comparisonHasChanges(comparison) {
|
|
3230
|
+
return comparison.sections.some(sectionHasChanges);
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// src/commands/compare.ts
|
|
3234
|
+
async function resolveBundle(input) {
|
|
3235
|
+
const resolved = path15.resolve(input);
|
|
3236
|
+
if (!await fs14.pathExists(resolved)) {
|
|
3237
|
+
throw new ReplicaxError(`Path not found: ${input}`);
|
|
3238
|
+
}
|
|
3239
|
+
try {
|
|
3240
|
+
const dir = await resolveProfileDir(input);
|
|
3241
|
+
const bundle2 = await loadBundle(dir);
|
|
3242
|
+
return { bundle: bundle2, label: `${bundle2.profile.name} (profile)` };
|
|
3243
|
+
} catch {
|
|
3244
|
+
}
|
|
3245
|
+
const stat = await fs14.stat(resolved);
|
|
3246
|
+
if (!stat.isDirectory()) {
|
|
3247
|
+
throw new ReplicaxError(`Cannot compare "${input}": not a profile or a project directory.`, [
|
|
3248
|
+
"Pass a project folder or a directory containing a .replicax profile."
|
|
3249
|
+
]);
|
|
3250
|
+
}
|
|
3251
|
+
const scan = await scanProject(resolved);
|
|
3252
|
+
const bundle = buildBundle({
|
|
3253
|
+
name: path15.basename(resolved) || "project",
|
|
3254
|
+
tooling: scan.tooling,
|
|
3255
|
+
structure: scan.structure,
|
|
3256
|
+
metadata: scan.metadata
|
|
3257
|
+
});
|
|
3258
|
+
return { bundle, label: `${path15.basename(resolved)} (scanned)` };
|
|
3259
|
+
}
|
|
3260
|
+
async function compareCommand(source, target, options) {
|
|
3261
|
+
const [a, b] = await Promise.all([resolveBundle(source), resolveBundle(target)]);
|
|
3262
|
+
const comparison = compareBundles(a.bundle, b.bundle);
|
|
3263
|
+
if (options.json) {
|
|
3264
|
+
logger.out(JSON.stringify({ source: a.label, target: b.label, ...comparison }, null, 2));
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
logger.out(pc.bold(`Comparing ${a.label} \u2192 ${b.label}`));
|
|
3268
|
+
logger.out("");
|
|
3269
|
+
if (!comparisonHasChanges(comparison)) {
|
|
3270
|
+
logger.out("No differences.");
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
printGroup("Added", collect(comparison, "added"), pc.green("+"));
|
|
3274
|
+
printGroup("Removed", collect(comparison, "removed"), pc.red("-"));
|
|
3275
|
+
printGroup("Changed", collect(comparison, "changed"), pc.yellow("~"));
|
|
3276
|
+
}
|
|
3277
|
+
function collect(comparison, bucket) {
|
|
3278
|
+
const out = [];
|
|
3279
|
+
for (const section of comparison.sections) {
|
|
3280
|
+
for (const item of section[bucket]) {
|
|
3281
|
+
out.push(`${item} ${pc.dim(`(${section.title})`)}`);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return out;
|
|
3285
|
+
}
|
|
3286
|
+
function printGroup(label, items, marker) {
|
|
3287
|
+
if (items.length === 0) return;
|
|
3288
|
+
logger.out(pc.bold(`${label}:`));
|
|
3289
|
+
for (const item of items) logger.out(` ${marker} ${item}`);
|
|
3290
|
+
logger.out("");
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
// src/commands/audit.ts
|
|
3294
|
+
import path16 from "path";
|
|
3295
|
+
|
|
3296
|
+
// src/core/audit/rules.ts
|
|
3297
|
+
function detected(ctx, ids) {
|
|
3298
|
+
const present = new Set(ctx.detections.map((d) => d.id));
|
|
3299
|
+
return ids.some((id) => present.has(id));
|
|
3300
|
+
}
|
|
3301
|
+
var AUDIT_RULES = [
|
|
3302
|
+
{
|
|
3303
|
+
id: "linting",
|
|
3304
|
+
title: "Linting",
|
|
3305
|
+
weight: 15,
|
|
3306
|
+
category: "quality",
|
|
3307
|
+
passes: (c) => detected(c, ["eslint", "biome"]),
|
|
3308
|
+
recommendation: "Add ESLint to catch problems with static analysis."
|
|
3309
|
+
},
|
|
3310
|
+
{
|
|
3311
|
+
id: "formatting",
|
|
3312
|
+
title: "Formatting",
|
|
3313
|
+
weight: 10,
|
|
3314
|
+
category: "quality",
|
|
3315
|
+
passes: (c) => detected(c, ["prettier", "biome"]),
|
|
3316
|
+
recommendation: "Add Prettier to keep formatting consistent."
|
|
3317
|
+
},
|
|
3318
|
+
{
|
|
3319
|
+
id: "testing",
|
|
3320
|
+
title: "Testing",
|
|
3321
|
+
weight: 20,
|
|
3322
|
+
category: "quality",
|
|
3323
|
+
passes: (c) => detected(c, ["vitest", "jest", "playwright", "cypress"]),
|
|
3324
|
+
recommendation: "Add a test runner such as Vitest or Jest."
|
|
3325
|
+
},
|
|
3326
|
+
{
|
|
3327
|
+
id: "git-hooks",
|
|
3328
|
+
title: "Git hooks",
|
|
3329
|
+
weight: 10,
|
|
3330
|
+
category: "quality",
|
|
3331
|
+
passes: (c) => detected(c, ["husky", "lefthook"]),
|
|
3332
|
+
recommendation: "Add Husky to run checks before each commit."
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
id: "ci",
|
|
3336
|
+
title: "CI/CD",
|
|
3337
|
+
weight: 20,
|
|
3338
|
+
category: "delivery",
|
|
3339
|
+
passes: (c) => detected(c, ["github-actions", "gitlab-ci", "circleci", "jenkins", "azure-pipelines"]),
|
|
3340
|
+
recommendation: "Add a CI pipeline (e.g. GitHub Actions) to run checks on every push."
|
|
3341
|
+
},
|
|
3342
|
+
{
|
|
3343
|
+
id: "containerization",
|
|
3344
|
+
title: "Containerization",
|
|
3345
|
+
weight: 10,
|
|
3346
|
+
category: "delivery",
|
|
3347
|
+
passes: (c) => detected(c, ["docker", "docker-compose"]),
|
|
3348
|
+
recommendation: "Add a Dockerfile to containerize the application."
|
|
3349
|
+
},
|
|
3350
|
+
{
|
|
3351
|
+
id: "typescript",
|
|
3352
|
+
title: "TypeScript",
|
|
3353
|
+
weight: 10,
|
|
3354
|
+
category: "quality",
|
|
3355
|
+
passes: (c) => detected(c, ["typescript"]),
|
|
3356
|
+
recommendation: "Adopt TypeScript for type safety."
|
|
3357
|
+
},
|
|
3358
|
+
{
|
|
3359
|
+
id: "commit-linting",
|
|
3360
|
+
title: "Commit linting",
|
|
3361
|
+
weight: 3,
|
|
3362
|
+
category: "quality",
|
|
3363
|
+
passes: (c) => detected(c, ["commitlint"]),
|
|
3364
|
+
recommendation: "Add Commitlint to standardize commit messages."
|
|
3365
|
+
},
|
|
3366
|
+
{
|
|
3367
|
+
id: "staged-linting",
|
|
3368
|
+
title: "Staged-file linting",
|
|
3369
|
+
weight: 2,
|
|
3370
|
+
category: "quality",
|
|
3371
|
+
passes: (c) => detected(c, ["lint-staged"]),
|
|
3372
|
+
recommendation: "Add lint-staged to lint only changed files."
|
|
3373
|
+
}
|
|
3374
|
+
];
|
|
3375
|
+
|
|
3376
|
+
// src/core/audit/engine.ts
|
|
3377
|
+
function runAudit(ctx, rules = AUDIT_RULES) {
|
|
3378
|
+
const evaluated = rules.map((rule) => ({
|
|
3379
|
+
id: rule.id,
|
|
3380
|
+
title: rule.title,
|
|
3381
|
+
category: rule.category,
|
|
3382
|
+
weight: rule.weight,
|
|
3383
|
+
passed: rule.passes(ctx),
|
|
3384
|
+
recommendation: rule.recommendation
|
|
3385
|
+
}));
|
|
3386
|
+
const totalWeight = evaluated.reduce((sum, r) => sum + r.weight, 0);
|
|
3387
|
+
const passedWeight = evaluated.filter((r) => r.passed).reduce((sum, r) => sum + r.weight, 0);
|
|
3388
|
+
const score = totalWeight === 0 ? 100 : Math.round(passedWeight / totalWeight * 100);
|
|
3389
|
+
const failed = evaluated.filter((r) => !r.passed);
|
|
3390
|
+
return {
|
|
3391
|
+
score,
|
|
3392
|
+
maxScore: 100,
|
|
3393
|
+
rules: evaluated,
|
|
3394
|
+
missing: failed.map((r) => r.title),
|
|
3395
|
+
recommendations: failed.map((r) => r.recommendation)
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/commands/audit.ts
|
|
3400
|
+
async function buildContext(options) {
|
|
3401
|
+
if (options.profile) {
|
|
3402
|
+
const dir = await resolveProfileDir(options.profile);
|
|
3403
|
+
const bundle = await loadBundle(dir);
|
|
3404
|
+
return {
|
|
3405
|
+
ctx: {
|
|
3406
|
+
detections: bundle.metadata.detections ?? [],
|
|
3407
|
+
metadata: bundle.metadata,
|
|
3408
|
+
tooling: bundle.tooling
|
|
3409
|
+
},
|
|
3410
|
+
source: `profile "${bundle.profile.name}"`
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
const root = path16.resolve(options.path ?? process.cwd());
|
|
3414
|
+
const scan = await scanProject(root);
|
|
3415
|
+
return {
|
|
3416
|
+
ctx: { detections: scan.detections, metadata: scan.metadata, tooling: scan.tooling },
|
|
3417
|
+
source: path16.basename(root) || "project"
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3420
|
+
async function auditCommand(options) {
|
|
3421
|
+
const { ctx, source } = await buildContext(options);
|
|
3422
|
+
const result = runAudit(ctx);
|
|
3423
|
+
if (options.json) {
|
|
3424
|
+
logger.out(JSON.stringify(result, null, 2));
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
logger.out(pc.bold(`Project Score: ${result.score}/${result.maxScore}`));
|
|
3428
|
+
logger.out(pc.dim(`Audited ${source}`));
|
|
3429
|
+
logger.out("");
|
|
3430
|
+
for (const rule of result.rules) {
|
|
3431
|
+
logger.out(statusLine(rule.passed, rule.title));
|
|
3432
|
+
}
|
|
3433
|
+
if (result.missing.length === 0) {
|
|
3434
|
+
logger.out("");
|
|
3435
|
+
logger.out(pc.green("All checks passed."));
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
logger.out("");
|
|
3439
|
+
logger.out(pc.bold("Missing:"));
|
|
3440
|
+
for (const item of result.missing) logger.out(` - ${item}`);
|
|
3441
|
+
logger.out("");
|
|
3442
|
+
logger.out(pc.bold("Recommendations:"));
|
|
3443
|
+
for (const rec of result.recommendations) logger.out(` - ${rec}`);
|
|
3444
|
+
}
|
|
3445
|
+
|
|
2274
3446
|
// src/index.ts
|
|
2275
3447
|
function packageVersion() {
|
|
2276
3448
|
try {
|
|
@@ -2317,6 +3489,9 @@ program.command("inspect").description("Display captured configuration and struc
|
|
|
2317
3489
|
program.command("validate").description("Check profile schema and integrity").option("--profile <path>", "Validate a profile at a custom path").action(action(validateCommand));
|
|
2318
3490
|
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
3491
|
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));
|
|
3492
|
+
program.command("doctor").description("Check which developer tools are installed locally").option("--json", "Output as JSON").action(action(doctorCommand));
|
|
3493
|
+
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));
|
|
3494
|
+
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
3495
|
if (process.argv.slice(2).length === 0) {
|
|
2321
3496
|
program.outputHelp();
|
|
2322
3497
|
process.exit(0);
|