@iamsaroj/replicax 0.0.2 → 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 +118 -12
- package/dist/index.js +1354 -175
- 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/",
|
|
@@ -129,15 +133,19 @@ var SECRET_GUARD_GLOBS = [
|
|
|
129
133
|
"**/*.key",
|
|
130
134
|
"**/*.p12",
|
|
131
135
|
"**/*.pfx",
|
|
136
|
+
"**/*.p8",
|
|
132
137
|
"**/*.cert",
|
|
133
138
|
"**/*.crt",
|
|
134
139
|
"**/*.keystore",
|
|
135
140
|
"**/*.jks",
|
|
141
|
+
"**/*.ppk",
|
|
136
142
|
"**/id_rsa*",
|
|
137
143
|
"**/id_dsa*",
|
|
138
144
|
"**/id_ecdsa*",
|
|
139
145
|
"**/id_ed25519*",
|
|
140
146
|
"**/.netrc",
|
|
147
|
+
"**/.pgpass",
|
|
148
|
+
"**/.htpasswd",
|
|
141
149
|
"**/secrets.*",
|
|
142
150
|
"**/*.secret",
|
|
143
151
|
"**/*.secrets"
|
|
@@ -177,9 +185,9 @@ yarn-error.log*
|
|
|
177
185
|
`;
|
|
178
186
|
|
|
179
187
|
// src/core/scanner.ts
|
|
180
|
-
import
|
|
181
|
-
import
|
|
182
|
-
import
|
|
188
|
+
import path5 from "path";
|
|
189
|
+
import fs4 from "fs-extra";
|
|
190
|
+
import fg2 from "fast-glob";
|
|
183
191
|
|
|
184
192
|
// src/config/supported-files.ts
|
|
185
193
|
var CONFIG_CATEGORIES = [
|
|
@@ -284,6 +292,38 @@ var CONFIG_CATEGORIES = [
|
|
|
284
292
|
label: "Git Hooks",
|
|
285
293
|
patterns: [".husky/*"]
|
|
286
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
|
+
},
|
|
287
327
|
{
|
|
288
328
|
id: "misc",
|
|
289
329
|
label: "Miscellaneous Tooling",
|
|
@@ -304,6 +344,13 @@ var CONFIG_CATEGORIES = [
|
|
|
304
344
|
];
|
|
305
345
|
var ALL_CONFIG_PATTERNS = CONFIG_CATEGORIES.flatMap((c) => c.patterns);
|
|
306
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
|
+
}
|
|
307
354
|
|
|
308
355
|
// src/utils/paths.ts
|
|
309
356
|
import path from "path";
|
|
@@ -349,6 +396,7 @@ import fs from "fs-extra";
|
|
|
349
396
|
import ignore from "ignore";
|
|
350
397
|
var IgnoreEngine = class _IgnoreEngine {
|
|
351
398
|
ig;
|
|
399
|
+
userIg;
|
|
352
400
|
secrets;
|
|
353
401
|
userPatterns;
|
|
354
402
|
constructor(userPatterns = []) {
|
|
@@ -357,6 +405,7 @@ var IgnoreEngine = class _IgnoreEngine {
|
|
|
357
405
|
return t.length > 0 && !t.startsWith("#");
|
|
358
406
|
});
|
|
359
407
|
this.ig = ignore().add(DEFAULT_IGNORE_PATTERNS).add(this.userPatterns);
|
|
408
|
+
this.userIg = ignore().add(this.userPatterns);
|
|
360
409
|
this.secrets = ignore().add(SECRET_GUARD_GLOBS);
|
|
361
410
|
}
|
|
362
411
|
/** Build an engine from a project's `.replicaxignore`, if present. */
|
|
@@ -373,6 +422,15 @@ var IgnoreEngine = class _IgnoreEngine {
|
|
|
373
422
|
if (!relPosixPath || relPosixPath === ".") return false;
|
|
374
423
|
return this.ig.ignores(relPosixPath);
|
|
375
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
|
+
}
|
|
376
434
|
/** Whether a path is a protected secret that must never be captured. */
|
|
377
435
|
isSecret(relPosixPath) {
|
|
378
436
|
if (!relPosixPath || relPosixPath === ".") return false;
|
|
@@ -435,6 +493,11 @@ async function detectLanguage(root, pkg) {
|
|
|
435
493
|
return file === "jsconfig.json" ? "javascript" : "typescript";
|
|
436
494
|
}
|
|
437
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
|
+
}
|
|
438
501
|
return "javascript";
|
|
439
502
|
}
|
|
440
503
|
function detectFramework(pkg) {
|
|
@@ -454,7 +517,7 @@ function detectFramework(pkg) {
|
|
|
454
517
|
[has("svelte"), "svelte"],
|
|
455
518
|
[has("solid-js"), "solid"],
|
|
456
519
|
[has("react"), "react"],
|
|
457
|
-
[has("
|
|
520
|
+
[has("fastify"), "fastify"],
|
|
458
521
|
[has("koa"), "koa"],
|
|
459
522
|
[has("express"), "express"]
|
|
460
523
|
];
|
|
@@ -550,6 +613,464 @@ function renderPackageJson(template, projectName) {
|
|
|
550
613
|
return JSON.stringify(ordered, null, 2) + "\n";
|
|
551
614
|
}
|
|
552
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
|
+
|
|
553
1074
|
// src/core/scanner.ts
|
|
554
1075
|
var FG_BASE_OPTIONS = {
|
|
555
1076
|
dot: true,
|
|
@@ -568,10 +1089,16 @@ function sanitizeNpmrc(content) {
|
|
|
568
1089
|
});
|
|
569
1090
|
return kept.join("\n");
|
|
570
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
|
+
}
|
|
571
1098
|
async function scanToolingFiles(root, ignore2) {
|
|
572
1099
|
const categoryOf = /* @__PURE__ */ new Map();
|
|
573
1100
|
for (const category of CONFIG_CATEGORIES) {
|
|
574
|
-
const found = await
|
|
1101
|
+
const found = await fg2(category.patterns, {
|
|
575
1102
|
cwd: root,
|
|
576
1103
|
onlyFiles: true,
|
|
577
1104
|
unique: true,
|
|
@@ -582,43 +1109,67 @@ async function scanToolingFiles(root, ignore2) {
|
|
|
582
1109
|
if (!categoryOf.has(norm)) categoryOf.set(norm, category.id);
|
|
583
1110
|
}
|
|
584
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));
|
|
585
1135
|
const files = [];
|
|
586
1136
|
const skippedSecrets = [];
|
|
587
|
-
for (const rel of
|
|
1137
|
+
for (const { rel, source, category } of candidates) {
|
|
588
1138
|
if (rel === "package.json") continue;
|
|
589
1139
|
if (ignore2.isSecret(rel)) {
|
|
590
1140
|
skippedSecrets.push(rel);
|
|
591
1141
|
logger.detail(`skipped (secret guard): ${rel}`);
|
|
592
1142
|
continue;
|
|
593
1143
|
}
|
|
594
|
-
|
|
1144
|
+
const excluded = source === "include" ? ignore2.isUserIgnored(rel) : ignore2.isIgnored(rel);
|
|
1145
|
+
if (excluded) {
|
|
595
1146
|
logger.detail(`skipped (.replicaxignore): ${rel}`);
|
|
596
1147
|
continue;
|
|
597
1148
|
}
|
|
598
|
-
const abs =
|
|
1149
|
+
const abs = path5.join(root, rel);
|
|
599
1150
|
let stat;
|
|
600
1151
|
try {
|
|
601
|
-
stat = await
|
|
1152
|
+
stat = await fs4.stat(abs);
|
|
602
1153
|
} catch {
|
|
603
1154
|
continue;
|
|
604
1155
|
}
|
|
605
1156
|
if (!stat.isFile()) continue;
|
|
606
|
-
let content = await
|
|
607
|
-
if (
|
|
1157
|
+
let content = await fs4.readFile(abs, "utf8");
|
|
1158
|
+
if (path5.basename(rel) === ".npmrc") content = sanitizeNpmrc(content);
|
|
608
1159
|
files.push({
|
|
609
1160
|
path: rel,
|
|
610
|
-
category
|
|
1161
|
+
category,
|
|
611
1162
|
variant: detectVariant(rel),
|
|
612
1163
|
encoding: "utf8",
|
|
613
1164
|
content,
|
|
614
1165
|
bytes: Buffer.byteLength(content, "utf8")
|
|
615
1166
|
});
|
|
616
|
-
logger.detail(`captured: ${rel}`);
|
|
1167
|
+
logger.detail(`captured${source === "include" ? " (include)" : ""}: ${rel}`);
|
|
617
1168
|
}
|
|
618
1169
|
return { files, skippedSecrets };
|
|
619
1170
|
}
|
|
620
1171
|
async function scanStructure(root, ignore2) {
|
|
621
|
-
const dirs = await
|
|
1172
|
+
const dirs = await fg2("**", {
|
|
622
1173
|
cwd: root,
|
|
623
1174
|
onlyDirectories: true,
|
|
624
1175
|
unique: true,
|
|
@@ -626,13 +1177,13 @@ async function scanStructure(root, ignore2) {
|
|
|
626
1177
|
});
|
|
627
1178
|
const directories = dirs.map(toPosix).filter((d) => d.length > 0 && d !== ".").filter((d) => !ignore2.isIgnored(d)).sort();
|
|
628
1179
|
return {
|
|
629
|
-
root:
|
|
1180
|
+
root: path5.basename(path5.resolve(root)) || "project",
|
|
630
1181
|
directories
|
|
631
1182
|
};
|
|
632
1183
|
}
|
|
633
1184
|
async function scanProject(root) {
|
|
634
|
-
const resolved =
|
|
635
|
-
if (!await
|
|
1185
|
+
const resolved = path5.resolve(root);
|
|
1186
|
+
if (!await fs4.pathExists(resolved)) {
|
|
636
1187
|
throw new Error(`Directory does not exist: ${resolved}`);
|
|
637
1188
|
}
|
|
638
1189
|
const ignore2 = await IgnoreEngine.fromProject(resolved);
|
|
@@ -642,11 +1193,13 @@ async function scanProject(root) {
|
|
|
642
1193
|
scanStructure(resolved, ignore2),
|
|
643
1194
|
detectMetadata(resolved, pkg)
|
|
644
1195
|
]);
|
|
1196
|
+
const detections = await detectStack(resolved, pkg, metadata);
|
|
1197
|
+
metadata.detections = detections;
|
|
645
1198
|
const tooling = {
|
|
646
1199
|
files,
|
|
647
1200
|
packageJson: buildPackageTemplate(pkg)
|
|
648
1201
|
};
|
|
649
|
-
return { tooling, structure, metadata, pkg, skippedSecrets };
|
|
1202
|
+
return { tooling, structure, metadata, pkg, detections, skippedSecrets };
|
|
650
1203
|
}
|
|
651
1204
|
|
|
652
1205
|
// src/core/checksum.ts
|
|
@@ -684,6 +1237,34 @@ function verifyChecksum(tooling, stored) {
|
|
|
684
1237
|
return mismatches;
|
|
685
1238
|
}
|
|
686
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
|
+
|
|
687
1268
|
// src/core/profile-generator.ts
|
|
688
1269
|
function buildBundle(args) {
|
|
689
1270
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -700,28 +1281,42 @@ function buildBundle(args) {
|
|
|
700
1281
|
replicaxVersion: REPLICAX_VERSION,
|
|
701
1282
|
...args.description ? { description: args.description } : {}
|
|
702
1283
|
};
|
|
1284
|
+
const checksum = computeChecksum(args.tooling);
|
|
703
1285
|
return {
|
|
704
1286
|
profile,
|
|
705
1287
|
tooling: args.tooling,
|
|
706
1288
|
structure: args.structure,
|
|
707
1289
|
metadata: args.metadata,
|
|
708
|
-
checksum
|
|
1290
|
+
checksum,
|
|
1291
|
+
manifest: buildManifest(args.tooling, checksum)
|
|
709
1292
|
};
|
|
710
1293
|
}
|
|
711
1294
|
|
|
712
1295
|
// src/core/profile-store.ts
|
|
713
|
-
import
|
|
714
|
-
import
|
|
1296
|
+
import path6 from "path";
|
|
1297
|
+
import fs5 from "fs-extra";
|
|
715
1298
|
|
|
716
1299
|
// src/schema.ts
|
|
717
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
|
+
});
|
|
718
1311
|
var ProfileSchema = z.object({
|
|
719
1312
|
name: z.string().min(1),
|
|
720
1313
|
version: z.string().min(1),
|
|
721
1314
|
createdAt: z.string().min(1),
|
|
722
1315
|
updatedAt: z.string().optional(),
|
|
723
1316
|
replicaxVersion: z.string().min(1),
|
|
724
|
-
description: z.string().optional()
|
|
1317
|
+
description: z.string().optional(),
|
|
1318
|
+
/** Optional registry metadata (future registry compatibility). */
|
|
1319
|
+
registry: RegistrySchema.optional()
|
|
725
1320
|
});
|
|
726
1321
|
var FileVariantSchema = z.enum(["ts", "js", "mjs", "cjs", "json", "yaml", "other"]);
|
|
727
1322
|
var ToolingFileSchema = z.object({
|
|
@@ -755,32 +1350,114 @@ var StructureSchema = z.object({
|
|
|
755
1350
|
root: z.string(),
|
|
756
1351
|
directories: z.array(z.string())
|
|
757
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
|
+
});
|
|
758
1382
|
var MetadataSchema = z.object({
|
|
759
1383
|
nodeVersion: z.string(),
|
|
760
1384
|
packageManager: z.enum(["npm", "yarn", "pnpm", "bun", "unknown"]),
|
|
761
1385
|
framework: z.string(),
|
|
762
|
-
language: z.enum(["typescript", "javascript"]),
|
|
763
|
-
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()
|
|
764
1390
|
});
|
|
765
1391
|
var ChecksumSchema = z.object({
|
|
766
1392
|
algorithm: z.literal("sha256"),
|
|
767
1393
|
files: z.record(z.string(), z.string())
|
|
768
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
|
+
}
|
|
769
1446
|
|
|
770
1447
|
// src/core/profile-store.ts
|
|
771
1448
|
function profileDir(root) {
|
|
772
|
-
return
|
|
1449
|
+
return path6.join(path6.resolve(root), REPLICAX_DIR);
|
|
773
1450
|
}
|
|
774
1451
|
async function profileExists(dir) {
|
|
775
|
-
return
|
|
1452
|
+
return fs5.pathExists(path6.join(dir, PROFILE_FILES.profile));
|
|
776
1453
|
}
|
|
777
1454
|
async function resolveProfileDir(input) {
|
|
778
|
-
const resolved =
|
|
779
|
-
if (!await
|
|
1455
|
+
const resolved = path6.resolve(input);
|
|
1456
|
+
if (!await fs5.pathExists(resolved)) {
|
|
780
1457
|
throw new ReplicaxError(`Profile path not found: ${input}`);
|
|
781
1458
|
}
|
|
782
1459
|
if (await profileExists(resolved)) return resolved;
|
|
783
|
-
const nested =
|
|
1460
|
+
const nested = path6.join(resolved, REPLICAX_DIR);
|
|
784
1461
|
if (await profileExists(nested)) return nested;
|
|
785
1462
|
throw new ReplicaxError(`No ReplicaX profile found at: ${input}`, [
|
|
786
1463
|
`Looked for ${PROFILE_FILES.profile} in ${resolved} and ${nested}.`,
|
|
@@ -788,26 +1465,29 @@ async function resolveProfileDir(input) {
|
|
|
788
1465
|
]);
|
|
789
1466
|
}
|
|
790
1467
|
async function saveBundle(dir, bundle) {
|
|
791
|
-
await
|
|
1468
|
+
await fs5.ensureDir(dir);
|
|
1469
|
+
const manifest = bundle.manifest ?? buildManifest(bundle.tooling, bundle.checksum);
|
|
792
1470
|
await Promise.all([
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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 })
|
|
798
1477
|
]);
|
|
799
1478
|
}
|
|
800
|
-
async function
|
|
801
|
-
const full =
|
|
802
|
-
if (!await
|
|
1479
|
+
async function readRawFile(dir, file) {
|
|
1480
|
+
const full = path6.join(dir, file);
|
|
1481
|
+
if (!await fs5.pathExists(full)) {
|
|
803
1482
|
throw new ReplicaxError(`Profile is missing ${file}`, [`Expected at ${full}.`]);
|
|
804
1483
|
}
|
|
805
|
-
let raw;
|
|
806
1484
|
try {
|
|
807
|
-
|
|
1485
|
+
return await fs5.readJson(full);
|
|
808
1486
|
} catch {
|
|
809
1487
|
throw new ReplicaxError(`Profile file ${file} is not valid JSON`, [`Path: ${full}`]);
|
|
810
1488
|
}
|
|
1489
|
+
}
|
|
1490
|
+
function parseFile(file, schema, raw) {
|
|
811
1491
|
const result = schema.safeParse(raw);
|
|
812
1492
|
if (!result.success) {
|
|
813
1493
|
const issues = result.error.issues.slice(0, 5).map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
@@ -821,17 +1501,35 @@ async function loadBundle(dir) {
|
|
|
821
1501
|
"Run `replicax init` to create one."
|
|
822
1502
|
]);
|
|
823
1503
|
}
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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 };
|
|
832
1525
|
}
|
|
833
1526
|
|
|
834
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
|
+
}
|
|
835
1533
|
function toolingByCategory(tooling) {
|
|
836
1534
|
const counts = /* @__PURE__ */ new Map();
|
|
837
1535
|
for (const file of tooling.files) {
|
|
@@ -840,7 +1538,7 @@ function toolingByCategory(tooling) {
|
|
|
840
1538
|
if (tooling.packageJson) {
|
|
841
1539
|
counts.set("package", (counts.get("package") ?? 0) + 1);
|
|
842
1540
|
}
|
|
843
|
-
return [...counts.entries()].map(([id, n]) => [
|
|
1541
|
+
return [...counts.entries()].map(([id, n]) => [categoryLabel(id), n]).sort((a, b) => a[0].localeCompare(b[0]));
|
|
844
1542
|
}
|
|
845
1543
|
function printScanSummary(bundle) {
|
|
846
1544
|
const { metadata, tooling, structure } = bundle;
|
|
@@ -850,6 +1548,7 @@ function printScanSummary(bundle) {
|
|
|
850
1548
|
logger.hint(`framework ${metadata.framework}`);
|
|
851
1549
|
logger.hint(`packageManager ${metadata.packageManager}`);
|
|
852
1550
|
logger.hint(`nodeVersion ${metadata.nodeVersion}`);
|
|
1551
|
+
printDetections(metadata.detections ?? []);
|
|
853
1552
|
logger.newline();
|
|
854
1553
|
logger.info(pc.bold(`Tooling (${tooling.files.length + (tooling.packageJson ? 1 : 0)} files)`));
|
|
855
1554
|
for (const [label, count] of toolingByCategory(tooling)) {
|
|
@@ -858,6 +1557,15 @@ function printScanSummary(bundle) {
|
|
|
858
1557
|
logger.newline();
|
|
859
1558
|
logger.info(pc.bold(`Structure (${structure.directories.length} directories)`));
|
|
860
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
|
+
}
|
|
861
1569
|
function reportSkippedSecrets(skipped) {
|
|
862
1570
|
if (skipped.length === 0) return;
|
|
863
1571
|
logger.warn(`Excluded ${skipped.length} protected file(s) from the profile:`);
|
|
@@ -906,7 +1614,7 @@ async function initCommand(options) {
|
|
|
906
1614
|
spinner.succeed(
|
|
907
1615
|
`Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
|
|
908
1616
|
);
|
|
909
|
-
const name = options.name ??
|
|
1617
|
+
const name = options.name ?? path7.basename(path7.resolve(root)) ?? "project";
|
|
910
1618
|
const bundle = buildBundle({
|
|
911
1619
|
name,
|
|
912
1620
|
tooling: scan.tooling,
|
|
@@ -931,21 +1639,21 @@ async function initCommand(options) {
|
|
|
931
1639
|
logger.hint("Create a project from it with: replicax create <project-name>");
|
|
932
1640
|
}
|
|
933
1641
|
async function maybeWriteIgnoreFile(root) {
|
|
934
|
-
const file =
|
|
935
|
-
if (await
|
|
1642
|
+
const file = path7.join(root, IGNORE_FILE);
|
|
1643
|
+
if (await fs6.pathExists(file)) return;
|
|
936
1644
|
const create = process.stdin.isTTY ? await confirm({
|
|
937
1645
|
message: `Create a starter ${IGNORE_FILE} to control what gets exported?`,
|
|
938
1646
|
default: true
|
|
939
1647
|
}) : false;
|
|
940
1648
|
if (create) {
|
|
941
|
-
await
|
|
1649
|
+
await fs6.writeFile(file, DEFAULT_IGNORE_FILE_CONTENTS, "utf8");
|
|
942
1650
|
logger.success(`Wrote ${IGNORE_FILE}`);
|
|
943
1651
|
}
|
|
944
1652
|
}
|
|
945
1653
|
|
|
946
1654
|
// src/commands/init-skill.ts
|
|
947
|
-
import
|
|
948
|
-
import
|
|
1655
|
+
import path8 from "path";
|
|
1656
|
+
import fs7 from "fs-extra";
|
|
949
1657
|
import ora2 from "ora";
|
|
950
1658
|
|
|
951
1659
|
// src/config/ai-targets.ts
|
|
@@ -953,27 +1661,33 @@ var SKILL_TARGETS = [
|
|
|
953
1661
|
{
|
|
954
1662
|
id: "claude",
|
|
955
1663
|
label: "Claude Code",
|
|
956
|
-
entryPath: (
|
|
1664
|
+
entryPath: (slug) => `.claude/skills/${slug}/SKILL.md`,
|
|
957
1665
|
note: "Claude Code loads skills from .claude/skills/<name>/SKILL.md"
|
|
958
1666
|
},
|
|
959
1667
|
{
|
|
960
1668
|
id: "codex",
|
|
961
1669
|
label: "OpenAI Codex CLI",
|
|
962
|
-
entryPath: (
|
|
1670
|
+
entryPath: (slug) => `.codex/skills/${slug}/SKILL.md`,
|
|
963
1671
|
note: "Codex CLI loads project skills from .codex/skills/<name>/SKILL.md"
|
|
964
1672
|
},
|
|
965
1673
|
{
|
|
966
1674
|
id: "antigravity",
|
|
967
1675
|
label: "Google Antigravity",
|
|
968
|
-
entryPath: (
|
|
1676
|
+
entryPath: (slug) => `.agents/skills/${slug}.md`,
|
|
969
1677
|
note: "Antigravity discovers skills under .agents/skills/"
|
|
970
1678
|
}
|
|
971
1679
|
];
|
|
972
1680
|
var SKILL_TARGET_BY_ID = new Map(SKILL_TARGETS.map((t) => [t.id, t]));
|
|
973
1681
|
var SKILL_TARGET_IDS = SKILL_TARGETS.map((t) => t.id);
|
|
974
1682
|
|
|
1683
|
+
// src/utils/slug.ts
|
|
1684
|
+
function slugify(input, fallback = "project") {
|
|
1685
|
+
const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1686
|
+
return slug || fallback;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
975
1689
|
// src/core/skill-generator.ts
|
|
976
|
-
var
|
|
1690
|
+
var FRAMEWORK_LABELS2 = {
|
|
977
1691
|
next: "Next.js",
|
|
978
1692
|
nuxt: "Nuxt",
|
|
979
1693
|
remix: "Remix",
|
|
@@ -1004,10 +1718,6 @@ var PRIMARY_SCRIPTS = [
|
|
|
1004
1718
|
"format:check",
|
|
1005
1719
|
"typecheck"
|
|
1006
1720
|
];
|
|
1007
|
-
function slugify(input) {
|
|
1008
|
-
const slug2 = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1009
|
-
return slug2 || "project";
|
|
1010
|
-
}
|
|
1011
1721
|
function installCommand(pm) {
|
|
1012
1722
|
switch (pm) {
|
|
1013
1723
|
case "yarn":
|
|
@@ -1041,7 +1751,7 @@ function orderedScripts(scripts) {
|
|
|
1041
1751
|
function toolingByCategoryLabel(tooling) {
|
|
1042
1752
|
const groups = /* @__PURE__ */ new Map();
|
|
1043
1753
|
for (const file of tooling.files) {
|
|
1044
|
-
const label =
|
|
1754
|
+
const label = categoryLabel(file.category);
|
|
1045
1755
|
const list = groups.get(label) ?? [];
|
|
1046
1756
|
list.push(file.path);
|
|
1047
1757
|
groups.set(label, list);
|
|
@@ -1110,15 +1820,15 @@ function conventionLines(args, scripts) {
|
|
|
1110
1820
|
}
|
|
1111
1821
|
function buildSkill(args) {
|
|
1112
1822
|
const { name, metadata, tooling, structure, pkg } = args;
|
|
1113
|
-
const
|
|
1823
|
+
const slug = slugify(name);
|
|
1114
1824
|
const pm = metadata.packageManager;
|
|
1115
|
-
const framework =
|
|
1825
|
+
const framework = FRAMEWORK_LABELS2[metadata.framework] ?? metadata.framework;
|
|
1116
1826
|
const language = metadata.language === "typescript" ? "TypeScript" : "JavaScript";
|
|
1117
1827
|
const scripts = pkg?.scripts ?? {};
|
|
1118
1828
|
const description = `${name} project: ${framework}/${language} setup, build/test commands, and tooling conventions. Use this skill when working in or scaffolding this codebase.`;
|
|
1119
1829
|
const lines = [];
|
|
1120
1830
|
lines.push("---");
|
|
1121
|
-
lines.push(`name: ${
|
|
1831
|
+
lines.push(`name: ${slug}`);
|
|
1122
1832
|
lines.push(`description: ${yamlString(description)}`);
|
|
1123
1833
|
lines.push("---");
|
|
1124
1834
|
lines.push("");
|
|
@@ -1176,10 +1886,13 @@ function buildSkill(args) {
|
|
|
1176
1886
|
lines.push(`- ${line}`);
|
|
1177
1887
|
}
|
|
1178
1888
|
lines.push("");
|
|
1179
|
-
return { slug
|
|
1889
|
+
return { slug, content: lines.join("\n") };
|
|
1180
1890
|
}
|
|
1181
1891
|
|
|
1182
1892
|
// src/core/ai/cli.ts
|
|
1893
|
+
import { spawn as spawn2 } from "child_process";
|
|
1894
|
+
|
|
1895
|
+
// src/core/process.ts
|
|
1183
1896
|
import { spawn } from "child_process";
|
|
1184
1897
|
async function commandExists(bin) {
|
|
1185
1898
|
const onWindows = process.platform === "win32";
|
|
@@ -1196,9 +1909,45 @@ async function commandExists(bin) {
|
|
|
1196
1909
|
child.on("close", (code) => resolve(code === 0));
|
|
1197
1910
|
});
|
|
1198
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
|
|
1199
1948
|
async function runWithStdin(bin, args, input, timeoutMs = 12e4) {
|
|
1200
1949
|
return new Promise((resolve, reject) => {
|
|
1201
|
-
const child =
|
|
1950
|
+
const child = spawn2(bin, args, { shell: true, windowsHide: true });
|
|
1202
1951
|
let stdout = "";
|
|
1203
1952
|
let stderr = "";
|
|
1204
1953
|
const timer = setTimeout(() => {
|
|
@@ -1342,6 +2091,12 @@ async function resolveProvider(preference, modelOverride) {
|
|
|
1342
2091
|
function buildSkillPrompt(args) {
|
|
1343
2092
|
const scripts = Object.entries(args.scripts).map(([name, cmd]) => ` ${name}: ${cmd}`).join("\n");
|
|
1344
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
|
+
` : "";
|
|
1345
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.
|
|
1346
2101
|
|
|
1347
2102
|
STRICT RULES:
|
|
@@ -1355,11 +2110,11 @@ REQUIREMENTS:
|
|
|
1355
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.
|
|
1356
2111
|
- You MAY add a few supporting files under "references/" (e.g. "references/commands.md") when genuinely useful. Keep the bundle small and focused.
|
|
1357
2112
|
- All paths must be relative, use forward slashes, and must NOT contain ".." or be absolute.
|
|
1358
|
-
- This skill targets ${args.target.label} and will be installed at ${args.entryPath}
|
|
2113
|
+
- This skill targets ${args.target.label} and will be installed at ${args.entryPath}.${templateRule}
|
|
1359
2114
|
|
|
1360
2115
|
PROJECT ANALYSIS (ground truth \u2014 refine and expand this, do not contradict it):
|
|
1361
2116
|
${args.analysis}
|
|
1362
|
-
|
|
2117
|
+
${templateSection}
|
|
1363
2118
|
CAPTURED CONFIG FILES:
|
|
1364
2119
|
${tooling || " (none)"}
|
|
1365
2120
|
|
|
@@ -1425,6 +2180,8 @@ async function initSkillCommand(options) {
|
|
|
1425
2180
|
spinner.succeed(
|
|
1426
2181
|
`Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
|
|
1427
2182
|
);
|
|
2183
|
+
const rootSkillPath = path8.join(root, ROOT_SKILL_FILE);
|
|
2184
|
+
const rootSkill = await fs7.pathExists(rootSkillPath) ? await fs7.readFile(rootSkillPath, "utf8") : void 0;
|
|
1428
2185
|
const name = options.name ?? scan.structure.root;
|
|
1429
2186
|
const seed = buildSkill({
|
|
1430
2187
|
name,
|
|
@@ -1452,6 +2209,7 @@ async function initSkillCommand(options) {
|
|
|
1452
2209
|
const provider = await resolveProvider(options.provider, options.model);
|
|
1453
2210
|
if (provider) {
|
|
1454
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.`);
|
|
1455
2213
|
const aiSpinner = ora2({ text: "Authoring skill\u2026", isEnabled: !options.verbose }).start();
|
|
1456
2214
|
try {
|
|
1457
2215
|
const prompt = buildSkillPrompt({
|
|
@@ -1461,7 +2219,8 @@ async function initSkillCommand(options) {
|
|
|
1461
2219
|
target,
|
|
1462
2220
|
analysis: seed.content,
|
|
1463
2221
|
toolingPaths: scan.tooling.files.map((f) => f.path),
|
|
1464
|
-
scripts: scan.pkg?.scripts ?? {}
|
|
2222
|
+
scripts: scan.pkg?.scripts ?? {},
|
|
2223
|
+
rootSkill
|
|
1465
2224
|
});
|
|
1466
2225
|
const raw = await provider.run(prompt);
|
|
1467
2226
|
const parsed = parseSkillBundle(raw);
|
|
@@ -1493,11 +2252,11 @@ async function initSkillCommand(options) {
|
|
|
1493
2252
|
if (!safe) {
|
|
1494
2253
|
throw new ReplicaxError(`Refusing to write unsafe skill path: ${f.path}`);
|
|
1495
2254
|
}
|
|
1496
|
-
return { rel: safe, abs:
|
|
2255
|
+
return { rel: safe, abs: path8.join(root, ...safe.split("/")), content: f.content };
|
|
1497
2256
|
});
|
|
1498
2257
|
const conflicts = [];
|
|
1499
2258
|
for (const file of planned) {
|
|
1500
|
-
if (await
|
|
2259
|
+
if (await fs7.pathExists(file.abs)) conflicts.push(file.rel);
|
|
1501
2260
|
}
|
|
1502
2261
|
if (conflicts.length > 0 && !options.force) {
|
|
1503
2262
|
throw new ReplicaxError(
|
|
@@ -1506,26 +2265,26 @@ async function initSkillCommand(options) {
|
|
|
1506
2265
|
);
|
|
1507
2266
|
}
|
|
1508
2267
|
for (const file of planned) {
|
|
1509
|
-
await
|
|
1510
|
-
await
|
|
2268
|
+
await fs7.ensureDir(path8.dirname(file.abs));
|
|
2269
|
+
await fs7.writeFile(file.abs, file.content, "utf8");
|
|
1511
2270
|
logger.detail(`wrote: ${file.rel}`);
|
|
1512
2271
|
}
|
|
1513
2272
|
logger.newline();
|
|
1514
2273
|
logger.success(`Skill "${seed.slug}" written (${planned.length} file(s), via ${via})`);
|
|
1515
2274
|
logger.hint(
|
|
1516
|
-
`Location: ${relPosix(root,
|
|
2275
|
+
`Location: ${relPosix(root, path8.join(root, ...(bundleRoot || entryFile).split("/")))}`
|
|
1517
2276
|
);
|
|
1518
2277
|
logger.hint(target.note);
|
|
1519
2278
|
}
|
|
1520
2279
|
|
|
1521
2280
|
// src/commands/extract.ts
|
|
1522
|
-
import
|
|
2281
|
+
import path10 from "path";
|
|
1523
2282
|
import ora3 from "ora";
|
|
1524
2283
|
|
|
1525
2284
|
// src/core/github.ts
|
|
1526
2285
|
import os from "os";
|
|
1527
|
-
import
|
|
1528
|
-
import
|
|
2286
|
+
import path9 from "path";
|
|
2287
|
+
import fs8 from "fs-extra";
|
|
1529
2288
|
import { extract as tarExtract } from "tar";
|
|
1530
2289
|
function parseGitHubRef(input) {
|
|
1531
2290
|
const raw = input.trim();
|
|
@@ -1576,34 +2335,34 @@ function tokenFromEnv() {
|
|
|
1576
2335
|
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
1577
2336
|
return token?.trim() || void 0;
|
|
1578
2337
|
}
|
|
1579
|
-
function httpError(status,
|
|
2338
|
+
function httpError(status, slug, hasToken) {
|
|
1580
2339
|
if (status === 404) {
|
|
1581
|
-
return new ReplicaxError(`Repository not found: ${
|
|
2340
|
+
return new ReplicaxError(`Repository not found: ${slug}.`, [
|
|
1582
2341
|
"Check the owner/repo spelling and the branch/tag/commit name.",
|
|
1583
2342
|
hasToken ? "If it is private, make sure your token can access it." : "If it is private, set GITHUB_TOKEN (or GH_TOKEN) with repo access."
|
|
1584
2343
|
]);
|
|
1585
2344
|
}
|
|
1586
2345
|
if (status === 401) {
|
|
1587
|
-
return new ReplicaxError(`GitHub rejected the credentials for ${
|
|
2346
|
+
return new ReplicaxError(`GitHub rejected the credentials for ${slug}.`, [
|
|
1588
2347
|
"Check that GITHUB_TOKEN (or GH_TOKEN) is valid and not expired."
|
|
1589
2348
|
]);
|
|
1590
2349
|
}
|
|
1591
2350
|
if (status === 403 || status === 429) {
|
|
1592
|
-
return new ReplicaxError(`GitHub rate limit hit while fetching ${
|
|
2351
|
+
return new ReplicaxError(`GitHub rate limit hit while fetching ${slug}.`, [
|
|
1593
2352
|
hasToken ? "Wait a moment and try again." : "Set GITHUB_TOKEN (or GH_TOKEN) to raise the limit, then retry."
|
|
1594
2353
|
]);
|
|
1595
2354
|
}
|
|
1596
|
-
return new ReplicaxError(`GitHub returned HTTP ${status} for ${
|
|
2355
|
+
return new ReplicaxError(`GitHub returned HTTP ${status} for ${slug}.`);
|
|
1597
2356
|
}
|
|
1598
2357
|
async function firstSubdir(dir) {
|
|
1599
|
-
const entries = await
|
|
2358
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1600
2359
|
for (const entry of entries) {
|
|
1601
|
-
if (entry.isDirectory()) return
|
|
2360
|
+
if (entry.isDirectory()) return path9.join(dir, entry.name);
|
|
1602
2361
|
}
|
|
1603
2362
|
return null;
|
|
1604
2363
|
}
|
|
1605
2364
|
async function downloadRepo(ref) {
|
|
1606
|
-
const
|
|
2365
|
+
const slug = `${ref.owner}/${ref.repo}`;
|
|
1607
2366
|
const url = `https://api.github.com/repos/${ref.owner}/${ref.repo}/tarball` + (ref.ref ? `/${encodeURIComponent(ref.ref)}` : "");
|
|
1608
2367
|
const token = tokenFromEnv();
|
|
1609
2368
|
const headers = {
|
|
@@ -1620,19 +2379,19 @@ async function downloadRepo(ref) {
|
|
|
1620
2379
|
]);
|
|
1621
2380
|
}
|
|
1622
2381
|
if (!res.ok) {
|
|
1623
|
-
throw httpError(res.status,
|
|
2382
|
+
throw httpError(res.status, slug, Boolean(token));
|
|
1624
2383
|
}
|
|
1625
|
-
const tmpRoot = await
|
|
1626
|
-
const cleanup = () =>
|
|
2384
|
+
const tmpRoot = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-extract-"));
|
|
2385
|
+
const cleanup = () => fs8.remove(tmpRoot);
|
|
1627
2386
|
try {
|
|
1628
|
-
const tarPath =
|
|
1629
|
-
await
|
|
1630
|
-
const extractDir =
|
|
1631
|
-
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);
|
|
1632
2391
|
await tarExtract({ file: tarPath, cwd: extractDir, strip: 0 });
|
|
1633
2392
|
const repoRoot = await firstSubdir(extractDir);
|
|
1634
2393
|
if (!repoRoot) {
|
|
1635
|
-
throw new ReplicaxError(`The downloaded archive for ${
|
|
2394
|
+
throw new ReplicaxError(`The downloaded archive for ${slug} was empty.`);
|
|
1636
2395
|
}
|
|
1637
2396
|
return { dir: repoRoot, cleanup };
|
|
1638
2397
|
} catch (err) {
|
|
@@ -1681,7 +2440,7 @@ async function extractCommand(repo, options) {
|
|
|
1681
2440
|
logger.info("Dry run \u2014 no files were written.");
|
|
1682
2441
|
return;
|
|
1683
2442
|
}
|
|
1684
|
-
const outRoot = options.out ?
|
|
2443
|
+
const outRoot = options.out ? path10.resolve(options.out) : process.cwd();
|
|
1685
2444
|
const dir = profileDir(outRoot);
|
|
1686
2445
|
if (await profileExists(dir)) {
|
|
1687
2446
|
logger.warn(
|
|
@@ -1698,8 +2457,8 @@ async function extractCommand(repo, options) {
|
|
|
1698
2457
|
}
|
|
1699
2458
|
|
|
1700
2459
|
// src/commands/create.ts
|
|
1701
|
-
import
|
|
1702
|
-
import
|
|
2460
|
+
import path12 from "path";
|
|
2461
|
+
import fs10 from "fs-extra";
|
|
1703
2462
|
|
|
1704
2463
|
// src/core/conflict-resolver.ts
|
|
1705
2464
|
import { select } from "@inquirer/prompts";
|
|
@@ -1741,8 +2500,8 @@ var ConflictResolver = class {
|
|
|
1741
2500
|
};
|
|
1742
2501
|
|
|
1743
2502
|
// src/core/project-generator.ts
|
|
1744
|
-
import
|
|
1745
|
-
import
|
|
2503
|
+
import path11 from "path";
|
|
2504
|
+
import fs9 from "fs-extra";
|
|
1746
2505
|
async function generateProject(options) {
|
|
1747
2506
|
const { bundle, targetDir, projectName, dryRun, conflict } = options;
|
|
1748
2507
|
const result = {
|
|
@@ -1752,16 +2511,16 @@ async function generateProject(options) {
|
|
|
1752
2511
|
filesSkipped: 0,
|
|
1753
2512
|
unsafeSkipped: []
|
|
1754
2513
|
};
|
|
1755
|
-
if (!dryRun) await
|
|
2514
|
+
if (!dryRun) await fs9.ensureDir(targetDir);
|
|
1756
2515
|
for (const dir of bundle.structure.directories) {
|
|
1757
2516
|
const safe = safeJoinable(dir);
|
|
1758
2517
|
if (!safe) {
|
|
1759
2518
|
result.unsafeSkipped.push(dir);
|
|
1760
2519
|
continue;
|
|
1761
2520
|
}
|
|
1762
|
-
const full =
|
|
1763
|
-
const existed = await
|
|
1764
|
-
if (!dryRun) await
|
|
2521
|
+
const full = path11.join(targetDir, safe);
|
|
2522
|
+
const existed = await fs9.pathExists(full);
|
|
2523
|
+
if (!dryRun) await fs9.ensureDir(full);
|
|
1765
2524
|
if (!existed) result.dirsCreated += 1;
|
|
1766
2525
|
result.entries.push({ kind: "dir", path: safe, action: existed ? "skip" : "create" });
|
|
1767
2526
|
}
|
|
@@ -1785,8 +2544,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1785
2544
|
logger.warn(`Refusing to write unsafe path from profile: ${relPath}`);
|
|
1786
2545
|
return;
|
|
1787
2546
|
}
|
|
1788
|
-
const full =
|
|
1789
|
-
const exists = await
|
|
2547
|
+
const full = path11.join(options.targetDir, safe);
|
|
2548
|
+
const exists = await fs9.pathExists(full);
|
|
1790
2549
|
let action2 = exists ? "overwrite" : "create";
|
|
1791
2550
|
if (exists) {
|
|
1792
2551
|
const decision = await options.conflict.resolve(safe);
|
|
@@ -1799,8 +2558,8 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1799
2558
|
action2 = "overwrite";
|
|
1800
2559
|
}
|
|
1801
2560
|
if (!options.dryRun) {
|
|
1802
|
-
await
|
|
1803
|
-
await
|
|
2561
|
+
await fs9.ensureDir(path11.dirname(full));
|
|
2562
|
+
await fs9.writeFile(full, content, "utf8");
|
|
1804
2563
|
}
|
|
1805
2564
|
result.filesWritten += 1;
|
|
1806
2565
|
result.entries.push({ kind: "file", path: safe, action: action2 });
|
|
@@ -1808,7 +2567,7 @@ async function writeFile(relPath, content, options, result) {
|
|
|
1808
2567
|
}
|
|
1809
2568
|
|
|
1810
2569
|
// src/core/installer.ts
|
|
1811
|
-
import { spawn as
|
|
2570
|
+
import { spawn as spawn3 } from "child_process";
|
|
1812
2571
|
var COMMANDS = {
|
|
1813
2572
|
npm: ["npm", "install"],
|
|
1814
2573
|
pnpm: ["pnpm", "install"],
|
|
@@ -1819,7 +2578,7 @@ function installDependencies(cwd, manager) {
|
|
|
1819
2578
|
if (manager === "unknown") return Promise.resolve(false);
|
|
1820
2579
|
const [command, ...args] = COMMANDS[manager];
|
|
1821
2580
|
return new Promise((resolve) => {
|
|
1822
|
-
const child =
|
|
2581
|
+
const child = spawn3(command, args, {
|
|
1823
2582
|
cwd,
|
|
1824
2583
|
stdio: "inherit",
|
|
1825
2584
|
// npm/pnpm/yarn are .cmd shims on Windows; a shell resolves them.
|
|
@@ -1849,9 +2608,9 @@ async function createCommand(projectName, options) {
|
|
|
1849
2608
|
logger.warn(`Profile integrity check found ${mismatches.length} issue(s); continuing anyway.`);
|
|
1850
2609
|
logger.hint("Run `replicax validate` for details.");
|
|
1851
2610
|
}
|
|
1852
|
-
const targetDir =
|
|
1853
|
-
const leafName =
|
|
1854
|
-
if (
|
|
2611
|
+
const targetDir = path12.resolve(process.cwd(), projectName);
|
|
2612
|
+
const leafName = path12.basename(targetDir);
|
|
2613
|
+
if (path12.resolve(process.cwd()) === targetDir) {
|
|
1855
2614
|
throw new ReplicaxError("Refusing to scaffold into the current directory.", [
|
|
1856
2615
|
"Pass a new project name, e.g. `replicax create my-app`."
|
|
1857
2616
|
]);
|
|
@@ -1900,8 +2659,8 @@ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
|
|
|
1900
2659
|
logger.hint("No package manager detected; run your install command manually.");
|
|
1901
2660
|
return;
|
|
1902
2661
|
}
|
|
1903
|
-
const pkgPath =
|
|
1904
|
-
const pkg = await
|
|
2662
|
+
const pkgPath = path12.join(targetDir, "package.json");
|
|
2663
|
+
const pkg = await fs10.readJson(pkgPath).catch(() => null);
|
|
1905
2664
|
if (!pkg?.devDependencies || Object.keys(pkg.devDependencies).length === 0) {
|
|
1906
2665
|
logger.hint("No dependencies to install.");
|
|
1907
2666
|
return;
|
|
@@ -1917,27 +2676,27 @@ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
|
|
|
1917
2676
|
import ora4 from "ora";
|
|
1918
2677
|
|
|
1919
2678
|
// src/core/diff.ts
|
|
1920
|
-
function
|
|
2679
|
+
function diffStringMaps(prev, next, options = {}) {
|
|
2680
|
+
const ignore2 = options.ignoreKeys;
|
|
1921
2681
|
const added = [];
|
|
1922
2682
|
const removed = [];
|
|
1923
2683
|
const changed = [];
|
|
1924
|
-
|
|
1925
|
-
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)]);
|
|
1926
2685
|
for (const key of keys) {
|
|
1927
|
-
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
if (before !== after) packageJsonChanged = true;
|
|
1931
|
-
continue;
|
|
1932
|
-
}
|
|
2686
|
+
if (ignore2?.has(key)) continue;
|
|
2687
|
+
const before = prev[key];
|
|
2688
|
+
const after = next[key];
|
|
1933
2689
|
if (before === void 0) added.push(key);
|
|
1934
2690
|
else if (after === void 0) removed.push(key);
|
|
1935
2691
|
else if (before !== after) changed.push(key);
|
|
1936
2692
|
}
|
|
1937
|
-
return {
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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 };
|
|
1941
2700
|
}
|
|
1942
2701
|
function diffStructure(prev, next) {
|
|
1943
2702
|
const before = new Set(prev.directories);
|
|
@@ -2038,7 +2797,16 @@ function printList(title, items, marker) {
|
|
|
2038
2797
|
|
|
2039
2798
|
// src/commands/inspect.ts
|
|
2040
2799
|
import Table from "cli-table3";
|
|
2041
|
-
|
|
2800
|
+
|
|
2801
|
+
// src/utils/format.ts
|
|
2802
|
+
function formatBytes(bytes) {
|
|
2803
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
2804
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2805
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// src/commands/inspect.ts
|
|
2809
|
+
var SECTIONS = ["profile", "tooling", "structure", "metadata", "detections"];
|
|
2042
2810
|
async function inspectCommand(options) {
|
|
2043
2811
|
const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
|
|
2044
2812
|
if (!await profileExists(dir)) {
|
|
@@ -2052,15 +2820,38 @@ async function inspectCommand(options) {
|
|
|
2052
2820
|
const bundle = await loadBundle(dir);
|
|
2053
2821
|
const section = options.section;
|
|
2054
2822
|
if (options.json) {
|
|
2055
|
-
|
|
2056
|
-
logger.out(JSON.stringify(payload, null, 2));
|
|
2823
|
+
logger.out(JSON.stringify(jsonPayload(bundle, section), null, 2));
|
|
2057
2824
|
return;
|
|
2058
2825
|
}
|
|
2059
2826
|
if (!section || section === "profile") printProfile(bundle);
|
|
2060
2827
|
if (!section || section === "metadata") printMetadata(bundle);
|
|
2828
|
+
if (!section || section === "detections") printDetectionsSection(bundle);
|
|
2061
2829
|
if (!section || section === "tooling") printTooling(bundle);
|
|
2062
2830
|
if (!section || section === "structure") printStructure(bundle);
|
|
2063
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
|
+
}
|
|
2064
2855
|
function printProfile(bundle) {
|
|
2065
2856
|
const p = bundle.profile;
|
|
2066
2857
|
logger.out(pc.bold("Profile"));
|
|
@@ -2094,12 +2885,7 @@ function printTooling(bundle) {
|
|
|
2094
2885
|
table.push(["Package Management & Monorepos", "package.json", "json", "template"]);
|
|
2095
2886
|
}
|
|
2096
2887
|
for (const file of [...tooling.files].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
2097
|
-
table.push([
|
|
2098
|
-
CATEGORY_BY_ID.get(file.category)?.label ?? file.category,
|
|
2099
|
-
file.path,
|
|
2100
|
-
file.variant,
|
|
2101
|
-
formatBytes(file.bytes)
|
|
2102
|
-
]);
|
|
2888
|
+
table.push([categoryLabel(file.category), file.path, file.variant, formatBytes(file.bytes)]);
|
|
2103
2889
|
}
|
|
2104
2890
|
logger.out(table.toString());
|
|
2105
2891
|
logger.out("");
|
|
@@ -2110,11 +2896,6 @@ function printStructure(bundle) {
|
|
|
2110
2896
|
logger.out(renderTree(structure.directories, structure.root));
|
|
2111
2897
|
logger.out("");
|
|
2112
2898
|
}
|
|
2113
|
-
function formatBytes(bytes) {
|
|
2114
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
2115
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2116
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2117
|
-
}
|
|
2118
2899
|
|
|
2119
2900
|
// src/commands/validate.ts
|
|
2120
2901
|
async function validateCommand(options) {
|
|
@@ -2152,20 +2933,20 @@ async function validateCommand(options) {
|
|
|
2152
2933
|
}
|
|
2153
2934
|
|
|
2154
2935
|
// src/commands/export.ts
|
|
2155
|
-
import
|
|
2156
|
-
import
|
|
2936
|
+
import path14 from "path";
|
|
2937
|
+
import fs12 from "fs-extra";
|
|
2157
2938
|
import ora5 from "ora";
|
|
2158
2939
|
|
|
2159
2940
|
// src/core/archive.ts
|
|
2160
2941
|
import os2 from "os";
|
|
2161
|
-
import
|
|
2162
|
-
import
|
|
2942
|
+
import path13 from "path";
|
|
2943
|
+
import fs11 from "fs-extra";
|
|
2163
2944
|
import { create as tarCreate, extract as tarExtract2 } from "tar";
|
|
2164
2945
|
async function exportProfile(profileDirectory, outPath) {
|
|
2165
|
-
const resolvedOut =
|
|
2166
|
-
await
|
|
2167
|
-
const parent =
|
|
2168
|
-
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);
|
|
2169
2950
|
await tarCreate(
|
|
2170
2951
|
{
|
|
2171
2952
|
gzip: true,
|
|
@@ -2178,21 +2959,21 @@ async function exportProfile(profileDirectory, outPath) {
|
|
|
2178
2959
|
);
|
|
2179
2960
|
}
|
|
2180
2961
|
async function extractToTemp(archivePath) {
|
|
2181
|
-
const resolved =
|
|
2182
|
-
if (!await
|
|
2962
|
+
const resolved = path13.resolve(archivePath);
|
|
2963
|
+
if (!await fs11.pathExists(resolved)) {
|
|
2183
2964
|
throw new Error(`Archive not found: ${archivePath}`);
|
|
2184
2965
|
}
|
|
2185
|
-
const tmp = await
|
|
2966
|
+
const tmp = await fs11.mkdtemp(path13.join(os2.tmpdir(), "replicax-import-"));
|
|
2186
2967
|
await tarExtract2({ file: resolved, cwd: tmp, strip: 0 });
|
|
2187
2968
|
return tmp;
|
|
2188
2969
|
}
|
|
2189
2970
|
async function findProfileRoot(dir) {
|
|
2190
|
-
const hasProfile = async (d) =>
|
|
2971
|
+
const hasProfile = async (d) => fs11.pathExists(path13.join(d, PROFILE_FILES.profile));
|
|
2191
2972
|
if (await hasProfile(dir)) return dir;
|
|
2192
|
-
const entries = await
|
|
2973
|
+
const entries = await fs11.readdir(dir, { withFileTypes: true });
|
|
2193
2974
|
for (const entry of entries) {
|
|
2194
2975
|
if (entry.isDirectory()) {
|
|
2195
|
-
const candidate =
|
|
2976
|
+
const candidate = path13.join(dir, entry.name);
|
|
2196
2977
|
if (await hasProfile(candidate)) return candidate;
|
|
2197
2978
|
}
|
|
2198
2979
|
}
|
|
@@ -2200,33 +2981,27 @@ async function findProfileRoot(dir) {
|
|
|
2200
2981
|
}
|
|
2201
2982
|
|
|
2202
2983
|
// src/commands/export.ts
|
|
2203
|
-
function slug(name) {
|
|
2204
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "profile";
|
|
2205
|
-
}
|
|
2206
2984
|
async function exportCommand(options) {
|
|
2207
2985
|
const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
|
|
2208
2986
|
if (!await profileExists(dir)) {
|
|
2209
2987
|
throw new ReplicaxError("No ReplicaX profile found to export.", ["Run `replicax init` first."]);
|
|
2210
2988
|
}
|
|
2211
2989
|
const bundle = await loadBundle(dir);
|
|
2212
|
-
const outPath =
|
|
2990
|
+
const outPath = path14.resolve(
|
|
2991
|
+
options.out ?? `${slugify(bundle.profile.name, "profile")}.replicax.tar.gz`
|
|
2992
|
+
);
|
|
2213
2993
|
const spinner = ora5({ text: "Packaging profile\u2026" }).start();
|
|
2214
2994
|
await exportProfile(dir, outPath);
|
|
2215
2995
|
spinner.stop();
|
|
2216
|
-
const { size } = await
|
|
2996
|
+
const { size } = await fs12.stat(outPath);
|
|
2217
2997
|
logger.success(
|
|
2218
|
-
`Exported "${bundle.profile.name}" \u2192 ${
|
|
2998
|
+
`Exported "${bundle.profile.name}" \u2192 ${path14.relative(process.cwd(), outPath)} (${formatBytes(size)})`
|
|
2219
2999
|
);
|
|
2220
3000
|
logger.hint("Share it, then `replicax import <file>` elsewhere.");
|
|
2221
3001
|
}
|
|
2222
|
-
function formatBytes2(bytes) {
|
|
2223
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
2224
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2225
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
2226
|
-
}
|
|
2227
3002
|
|
|
2228
3003
|
// src/commands/import.ts
|
|
2229
|
-
import
|
|
3004
|
+
import fs13 from "fs-extra";
|
|
2230
3005
|
import ora6 from "ora";
|
|
2231
3006
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
2232
3007
|
async function importCommand(archivePath, options) {
|
|
@@ -2254,7 +3029,7 @@ async function importCommand(archivePath, options) {
|
|
|
2254
3029
|
"Re-run with --force to overwrite it."
|
|
2255
3030
|
]);
|
|
2256
3031
|
}
|
|
2257
|
-
await
|
|
3032
|
+
await fs13.remove(dest);
|
|
2258
3033
|
}
|
|
2259
3034
|
await saveBundle(dest, bundle);
|
|
2260
3035
|
logger.newline();
|
|
@@ -2263,8 +3038,409 @@ async function importCommand(archivePath, options) {
|
|
|
2263
3038
|
);
|
|
2264
3039
|
logger.hint("Create a project with: replicax create <project-name>");
|
|
2265
3040
|
} finally {
|
|
2266
|
-
await
|
|
3041
|
+
await fs13.remove(tmp).catch(() => void 0);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
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
|
+
}
|
|
2267
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}`);
|
|
2268
3444
|
}
|
|
2269
3445
|
|
|
2270
3446
|
// src/index.ts
|
|
@@ -2313,6 +3489,9 @@ program.command("inspect").description("Display captured configuration and struc
|
|
|
2313
3489
|
program.command("validate").description("Check profile schema and integrity").option("--profile <path>", "Validate a profile at a custom path").action(action(validateCommand));
|
|
2314
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));
|
|
2315
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));
|
|
2316
3495
|
if (process.argv.slice(2).length === 0) {
|
|
2317
3496
|
program.outputHelp();
|
|
2318
3497
|
process.exit(0);
|