@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.
Files changed (3) hide show
  1. package/README.md +113 -7
  2. package/dist/index.js +1318 -143
  3. 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 path6 from "path";
64
- import fs5 from "fs-extra";
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 REPLICAX_VERSION = "2.0.0";
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 path4 from "path";
185
- import fs3 from "fs-extra";
186
- import fg from "fast-glob";
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 fg(category.patterns, {
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 [...categoryOf.keys()].sort()) {
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
- if (ignore2.isIgnored(rel)) {
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 = path4.join(root, rel);
1149
+ const abs = path5.join(root, rel);
603
1150
  let stat;
604
1151
  try {
605
- stat = await fs3.stat(abs);
1152
+ stat = await fs4.stat(abs);
606
1153
  } catch {
607
1154
  continue;
608
1155
  }
609
1156
  if (!stat.isFile()) continue;
610
- let content = await fs3.readFile(abs, "utf8");
611
- if (path4.basename(rel) === ".npmrc") content = sanitizeNpmrc(content);
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: categoryOf.get(rel) ?? "misc",
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 fg("**", {
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: path4.basename(path4.resolve(root)) || "project",
1180
+ root: path5.basename(path5.resolve(root)) || "project",
634
1181
  directories
635
1182
  };
636
1183
  }
637
1184
  async function scanProject(root) {
638
- const resolved = path4.resolve(root);
639
- if (!await fs3.pathExists(resolved)) {
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: computeChecksum(args.tooling)
1290
+ checksum,
1291
+ manifest: buildManifest(args.tooling, checksum)
713
1292
  };
714
1293
  }
715
1294
 
716
1295
  // src/core/profile-store.ts
717
- import path5 from "path";
718
- import fs4 from "fs-extra";
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 path5.join(path5.resolve(root), REPLICAX_DIR);
1449
+ return path6.join(path6.resolve(root), REPLICAX_DIR);
777
1450
  }
778
1451
  async function profileExists(dir) {
779
- return fs4.pathExists(path5.join(dir, PROFILE_FILES.profile));
1452
+ return fs5.pathExists(path6.join(dir, PROFILE_FILES.profile));
780
1453
  }
781
1454
  async function resolveProfileDir(input) {
782
- const resolved = path5.resolve(input);
783
- if (!await fs4.pathExists(resolved)) {
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 = path5.join(resolved, REPLICAX_DIR);
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 fs4.ensureDir(dir);
1468
+ await fs5.ensureDir(dir);
1469
+ const manifest = bundle.manifest ?? buildManifest(bundle.tooling, bundle.checksum);
796
1470
  await Promise.all([
797
- fs4.writeJson(path5.join(dir, PROFILE_FILES.profile), bundle.profile, { spaces: 2 }),
798
- fs4.writeJson(path5.join(dir, PROFILE_FILES.tooling), bundle.tooling, { spaces: 2 }),
799
- fs4.writeJson(path5.join(dir, PROFILE_FILES.structure), bundle.structure, { spaces: 2 }),
800
- fs4.writeJson(path5.join(dir, PROFILE_FILES.metadata), bundle.metadata, { spaces: 2 }),
801
- fs4.writeJson(path5.join(dir, PROFILE_FILES.checksum), bundle.checksum, { spaces: 2 })
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 readAndParse(dir, file, schema) {
805
- const full = path5.join(dir, file);
806
- if (!await fs4.pathExists(full)) {
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
- raw = await fs4.readJson(full);
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 [profile, tooling, structure, metadata, checksum] = await Promise.all([
829
- readAndParse(dir, PROFILE_FILES.profile, ProfileSchema),
830
- readAndParse(dir, PROFILE_FILES.tooling, ToolingSchema),
831
- readAndParse(dir, PROFILE_FILES.structure, StructureSchema),
832
- readAndParse(dir, PROFILE_FILES.metadata, MetadataSchema),
833
- readAndParse(dir, PROFILE_FILES.checksum, ChecksumSchema)
834
- ]);
835
- return { profile, tooling, structure, metadata, checksum };
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]) => [CATEGORY_BY_ID.get(id)?.label ?? id, n]).sort((a, b) => a[0].localeCompare(b[0]));
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 ?? path6.basename(path6.resolve(root)) ?? "project";
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 = path6.join(root, IGNORE_FILE);
939
- if (await fs5.pathExists(file)) return;
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 fs5.writeFile(file, DEFAULT_IGNORE_FILE_CONTENTS, "utf8");
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 path7 from "path";
952
- import fs6 from "fs-extra";
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 FRAMEWORK_LABELS = {
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 = CATEGORY_BY_ID.get(file.category)?.label ?? file.category;
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 = FRAMEWORK_LABELS[metadata.framework] ?? metadata.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 = spawn(bin, args, { shell: true, windowsHide: true });
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: path7.join(root, ...safe.split("/")), content: f.content };
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 fs6.pathExists(file.abs)) conflicts.push(file.rel);
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 fs6.ensureDir(path7.dirname(file.abs));
1516
- await fs6.writeFile(file.abs, file.content, "utf8");
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, path7.join(root, ...(bundleRoot || entryFile).split("/")))}`
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 path9 from "path";
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 path8 from "path";
1534
- import fs7 from "fs-extra";
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 fs7.readdir(dir, { withFileTypes: true });
2358
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1606
2359
  for (const entry of entries) {
1607
- if (entry.isDirectory()) return path8.join(dir, entry.name);
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 fs7.mkdtemp(path8.join(os.tmpdir(), "replicax-extract-"));
1632
- const cleanup = () => fs7.remove(tmpRoot);
2384
+ const tmpRoot = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-extract-"));
2385
+ const cleanup = () => fs8.remove(tmpRoot);
1633
2386
  try {
1634
- const tarPath = path8.join(tmpRoot, "repo.tar.gz");
1635
- await fs7.writeFile(tarPath, Buffer.from(await res.arrayBuffer()));
1636
- const extractDir = path8.join(tmpRoot, "src");
1637
- await fs7.ensureDir(extractDir);
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 ? path9.resolve(options.out) : process.cwd();
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 path11 from "path";
1708
- import fs9 from "fs-extra";
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 path10 from "path";
1751
- import fs8 from "fs-extra";
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 fs8.ensureDir(targetDir);
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 = path10.join(targetDir, safe);
1769
- const existed = await fs8.pathExists(full);
1770
- if (!dryRun) await fs8.ensureDir(full);
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 = path10.join(options.targetDir, safe);
1795
- const exists = await fs8.pathExists(full);
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 fs8.ensureDir(path10.dirname(full));
1809
- await fs8.writeFile(full, content, "utf8");
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 spawn2 } from "child_process";
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 = spawn2(command, args, {
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 = path11.resolve(process.cwd(), projectName);
1859
- const leafName = path11.basename(targetDir);
1860
- if (path11.resolve(process.cwd()) === targetDir) {
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 = path11.join(targetDir, "package.json");
1910
- const pkg = await fs9.readJson(pkgPath).catch(() => null);
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 diffChecksums(prev, next) {
2679
+ function diffStringMaps(prev, next, options = {}) {
2680
+ const ignore2 = options.ignoreKeys;
1927
2681
  const added = [];
1928
2682
  const removed = [];
1929
2683
  const changed = [];
1930
- let packageJsonChanged = false;
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
- const before = prev.files[key];
1934
- const after = next.files[key];
1935
- if (key === PACKAGE_JSON_KEY) {
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
- files: { added: added.sort(), removed: removed.sort(), changed: changed.sort() },
1945
- packageJsonChanged
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
- const payload = section ? { [section]: bundle[section] } : bundle;
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 path13 from "path";
2166
- import fs11 from "fs-extra";
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 path12 from "path";
2172
- import fs10 from "fs-extra";
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 = path12.resolve(outPath);
2176
- await fs10.ensureDir(path12.dirname(resolvedOut));
2177
- const parent = path12.dirname(profileDirectory);
2178
- const base = path12.basename(profileDirectory);
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 = path12.resolve(archivePath);
2192
- if (!await fs10.pathExists(resolved)) {
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 fs10.mkdtemp(path12.join(os2.tmpdir(), "replicax-import-"));
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) => fs10.pathExists(path12.join(d, PROFILE_FILES.profile));
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 fs10.readdir(dir, { withFileTypes: true });
2973
+ const entries = await fs11.readdir(dir, { withFileTypes: true });
2203
2974
  for (const entry of entries) {
2204
2975
  if (entry.isDirectory()) {
2205
- const candidate = path12.join(dir, entry.name);
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 = path13.resolve(
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 fs11.stat(outPath);
2996
+ const { size } = await fs12.stat(outPath);
2226
2997
  logger.success(
2227
- `Exported "${bundle.profile.name}" \u2192 ${path13.relative(process.cwd(), outPath)} (${formatBytes(size)})`
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 fs12 from "fs-extra";
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 fs12.remove(dest);
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 fs12.remove(tmp).catch(() => void 0);
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);