codebase-snapshot 0.9.1 → 0.9.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/CHANGELOG.md +181 -0
- package/README.md +141 -127
- package/dist/bin.js +2543 -506
- package/dist/chunk-EWZYK3VX.js +2577 -0
- package/dist/{chunk-HT7J3NWT.js → chunk-FCWD4LY7.js} +493 -362
- package/dist/cli.cjs +2450 -435
- package/dist/cli.d.cts +11 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +68 -750
- package/dist/index.cjs +481 -360
- package/dist/index.d.cts +111 -5
- package/dist/index.d.ts +111 -5
- package/dist/index.js +1 -1
- package/dist/mcp.cjs +9398 -0
- package/dist/mcp.d.cts +399 -0
- package/dist/mcp.d.ts +399 -0
- package/dist/mcp.js +4 -0
- package/docs/agent.md +36 -0
- package/docs/cli.md +85 -14
- package/docs/codebase-token-budget.md +98 -0
- package/docs/configuration.md +12 -2
- package/docs/how-to-feed-a-repo-to-claude.md +4 -2
- package/docs/install.md +39 -11
- package/docs/mcp.md +149 -0
- package/docs/stale-context-is-your-bug.md +118 -0
- package/docs/troubleshooting.md +13 -0
- package/docs/vs-repomix.md +4 -2
- package/package.json +29 -14
package/dist/bin.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawnSync } from "child_process";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs16 from "fs";
|
|
6
|
+
import * as path17 from "path";
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
8
|
|
|
9
9
|
// src/diff.ts
|
|
@@ -167,8 +167,8 @@ function workspacePackageDirs(cwd, patterns) {
|
|
|
167
167
|
function scanWorkspacePackages(cwd, patterns) {
|
|
168
168
|
const files = /* @__PURE__ */ new Set();
|
|
169
169
|
const dependencies = /* @__PURE__ */ new Set();
|
|
170
|
-
for (const
|
|
171
|
-
const full = path.join(cwd,
|
|
170
|
+
for (const relative11 of workspacePackageDirs(cwd, patterns)) {
|
|
171
|
+
const full = path.join(cwd, relative11);
|
|
172
172
|
try {
|
|
173
173
|
for (const entry of fs.readdirSync(full, { withFileTypes: true })) files.add(entry.name);
|
|
174
174
|
} catch {
|
|
@@ -245,25 +245,41 @@ function buildPresetContext(cwd) {
|
|
|
245
245
|
markdownCount: files.has("package.json") ? 0 : countMarkdownFiles(cwd)
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
|
|
249
|
+
// src/presets/evidence.ts
|
|
250
|
+
function fileEvidence(context, name) {
|
|
251
|
+
return context.files.has(name) ? name : null;
|
|
250
252
|
}
|
|
251
|
-
function
|
|
253
|
+
function fileStartingWithEvidence(context, prefix) {
|
|
252
254
|
for (const file of context.files) {
|
|
253
|
-
if (file.startsWith(prefix)) return
|
|
255
|
+
if (file.startsWith(prefix)) return file;
|
|
254
256
|
}
|
|
255
257
|
for (const file of context.workspaceFiles) {
|
|
256
|
-
if (file.startsWith(prefix)) return
|
|
258
|
+
if (file.startsWith(prefix)) return file;
|
|
257
259
|
}
|
|
258
|
-
return
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function dependencyEvidence(context, ...names) {
|
|
263
|
+
for (const name of names) {
|
|
264
|
+
if (context.dependencies.has(name)) return `depends on ${name}`;
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
259
267
|
}
|
|
260
|
-
function
|
|
268
|
+
function manifestEvidence(context, manifest, needle) {
|
|
261
269
|
const content = context.manifests[manifest];
|
|
262
|
-
if (!content) return
|
|
263
|
-
|
|
270
|
+
if (!content) return null;
|
|
271
|
+
if (!content.toLowerCase().includes(needle.toLowerCase())) return null;
|
|
272
|
+
return `${needle} in ${manifest}`;
|
|
273
|
+
}
|
|
274
|
+
function suffixEvidence(context, suffix) {
|
|
275
|
+
for (const file of context.files) {
|
|
276
|
+
if (file.endsWith(suffix)) return file;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
264
279
|
}
|
|
265
|
-
function
|
|
266
|
-
|
|
280
|
+
function markdownEvidence(context) {
|
|
281
|
+
if (context.markdownCount < 3 || context.files.has("package.json")) return null;
|
|
282
|
+
return `${context.markdownCount} markdown files, no package.json`;
|
|
267
283
|
}
|
|
268
284
|
|
|
269
285
|
// src/presets/helpers.ts
|
|
@@ -471,7 +487,7 @@ var JS_PRESETS = [
|
|
|
471
487
|
{
|
|
472
488
|
name: "nextjs",
|
|
473
489
|
priority: 100,
|
|
474
|
-
detect: (context) =>
|
|
490
|
+
detect: (context) => fileStartingWithEvidence(context, "next.config") ?? dependencyEvidence(context, "next"),
|
|
475
491
|
layers: NEXTJS_LAYERS,
|
|
476
492
|
kinds: NEXTJS_KINDS,
|
|
477
493
|
exclude: NEXT_LIKE_EXCLUDES,
|
|
@@ -480,7 +496,7 @@ var JS_PRESETS = [
|
|
|
480
496
|
{
|
|
481
497
|
name: "nuxt",
|
|
482
498
|
priority: 98,
|
|
483
|
-
detect: (context) =>
|
|
499
|
+
detect: (context) => fileStartingWithEvidence(context, "nuxt.config") ?? dependencyEvidence(context, "nuxt", "nuxt3"),
|
|
484
500
|
layers: [
|
|
485
501
|
layer("pages", "pages/", "src/pages/"),
|
|
486
502
|
layer("components", "components/", "src/components/"),
|
|
@@ -504,7 +520,7 @@ var JS_PRESETS = [
|
|
|
504
520
|
{
|
|
505
521
|
name: "sveltekit",
|
|
506
522
|
priority: 96,
|
|
507
|
-
detect: (context) =>
|
|
523
|
+
detect: (context) => fileStartingWithEvidence(context, "svelte.config") ?? dependencyEvidence(context, "@sveltejs/kit", "svelte"),
|
|
508
524
|
layers: [
|
|
509
525
|
layer("routes", "src/routes/", "routes/"),
|
|
510
526
|
layer("lib", "src/lib/", "lib/"),
|
|
@@ -525,7 +541,7 @@ var JS_PRESETS = [
|
|
|
525
541
|
{
|
|
526
542
|
name: "remix",
|
|
527
543
|
priority: 94,
|
|
528
|
-
detect: (context) =>
|
|
544
|
+
detect: (context) => dependencyEvidence(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
|
|
529
545
|
layers: [
|
|
530
546
|
layer("routes", "app/routes/", "routes/"),
|
|
531
547
|
layer("components", "app/components/"),
|
|
@@ -539,7 +555,7 @@ var JS_PRESETS = [
|
|
|
539
555
|
{
|
|
540
556
|
name: "astro",
|
|
541
557
|
priority: 92,
|
|
542
|
-
detect: (context) =>
|
|
558
|
+
detect: (context) => fileStartingWithEvidence(context, "astro.config") ?? dependencyEvidence(context, "astro"),
|
|
543
559
|
layers: [
|
|
544
560
|
layer("pages", "src/pages/"),
|
|
545
561
|
layer("layouts", "src/layouts/"),
|
|
@@ -554,7 +570,7 @@ var JS_PRESETS = [
|
|
|
554
570
|
{
|
|
555
571
|
name: "angular",
|
|
556
572
|
priority: 90,
|
|
557
|
-
detect: (context) =>
|
|
573
|
+
detect: (context) => fileEvidence(context, "angular.json") ?? dependencyEvidence(context, "@angular/core"),
|
|
558
574
|
layers: [
|
|
559
575
|
layer("app", "src/app/"),
|
|
560
576
|
layer("features", "src/app/features/"),
|
|
@@ -578,7 +594,7 @@ var JS_PRESETS = [
|
|
|
578
594
|
{
|
|
579
595
|
name: "solid",
|
|
580
596
|
priority: 88,
|
|
581
|
-
detect: (context) =>
|
|
597
|
+
detect: (context) => dependencyEvidence(context, "solid-js", "solid-start"),
|
|
582
598
|
layers: [
|
|
583
599
|
layer("pages", "src/routes/", "src/pages/"),
|
|
584
600
|
layer("components", "src/components/"),
|
|
@@ -591,7 +607,15 @@ var JS_PRESETS = [
|
|
|
591
607
|
{
|
|
592
608
|
name: "vue-vite",
|
|
593
609
|
priority: 86,
|
|
594
|
-
|
|
610
|
+
// `vue` is the decisive signal; `vite` (or its config file) only says the app is
|
|
611
|
+
// built with Vite rather than shipped pre-compiled, so it is required but not what
|
|
612
|
+
// gets reported.
|
|
613
|
+
detect: (context) => {
|
|
614
|
+
const framework = dependencyEvidence(context, "vue");
|
|
615
|
+
if (framework === null) return null;
|
|
616
|
+
const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
|
|
617
|
+
return build === null ? null : framework;
|
|
618
|
+
},
|
|
595
619
|
layers: [
|
|
596
620
|
layer("pages", "src/pages/", "src/views/"),
|
|
597
621
|
layer("components", "src/components/"),
|
|
@@ -616,7 +640,12 @@ var JS_PRESETS = [
|
|
|
616
640
|
{
|
|
617
641
|
name: "react-vite",
|
|
618
642
|
priority: 84,
|
|
619
|
-
detect: (context) =>
|
|
643
|
+
detect: (context) => {
|
|
644
|
+
const framework = dependencyEvidence(context, "react");
|
|
645
|
+
if (framework === null) return null;
|
|
646
|
+
const build = dependencyEvidence(context, "vite") ?? fileStartingWithEvidence(context, "vite.config");
|
|
647
|
+
return build === null ? null : framework;
|
|
648
|
+
},
|
|
620
649
|
layers: VITE_LAYERS,
|
|
621
650
|
kinds: VITE_KINDS,
|
|
622
651
|
exclude: [".vite", "dist"],
|
|
@@ -625,7 +654,7 @@ var JS_PRESETS = [
|
|
|
625
654
|
{
|
|
626
655
|
name: "nest",
|
|
627
656
|
priority: 80,
|
|
628
|
-
detect: (context) =>
|
|
657
|
+
detect: (context) => dependencyEvidence(context, "@nestjs/core"),
|
|
629
658
|
layers: [
|
|
630
659
|
layer("modules", "src/modules/", "modules/"),
|
|
631
660
|
layer("controllers", "src/controllers/", "controllers/"),
|
|
@@ -651,7 +680,7 @@ var JS_PRESETS = [
|
|
|
651
680
|
{
|
|
652
681
|
name: "node-express",
|
|
653
682
|
priority: 78,
|
|
654
|
-
detect: (context) =>
|
|
683
|
+
detect: (context) => dependencyEvidence(context, "express", "fastify", "koa", "@hapi/hapi"),
|
|
655
684
|
layers: NODE_LAYERS,
|
|
656
685
|
kinds: NODE_KINDS,
|
|
657
686
|
exclude: ["dist"],
|
|
@@ -662,7 +691,7 @@ var JS_PRESETS = [
|
|
|
662
691
|
// A bare package.json is often just tooling around a non-JS project, so it
|
|
663
692
|
// ranks below every definitive language manifest.
|
|
664
693
|
priority: 45,
|
|
665
|
-
detect: (context) =>
|
|
694
|
+
detect: (context) => fileEvidence(context, "package.json"),
|
|
666
695
|
layers: [
|
|
667
696
|
layer("src", "src/"),
|
|
668
697
|
layer("lib", "lib/"),
|
|
@@ -676,13 +705,12 @@ var JS_PRESETS = [
|
|
|
676
705
|
];
|
|
677
706
|
|
|
678
707
|
// src/presets/lang-presets.ts
|
|
679
|
-
var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
|
|
680
708
|
var DIR = (name) => `${name}/`;
|
|
681
709
|
var LANGUAGE_PRESETS = [
|
|
682
710
|
{
|
|
683
711
|
name: "python-django",
|
|
684
712
|
priority: 56,
|
|
685
|
-
detect: (context) =>
|
|
713
|
+
detect: (context) => fileEvidence(context, "manage.py") ?? manifestEvidence(context, "pyproject.toml", "django") ?? manifestEvidence(context, "requirements.txt", "django"),
|
|
686
714
|
layers: [
|
|
687
715
|
layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
|
|
688
716
|
layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
|
|
@@ -708,7 +736,7 @@ var LANGUAGE_PRESETS = [
|
|
|
708
736
|
{
|
|
709
737
|
name: "python-fastapi",
|
|
710
738
|
priority: 54,
|
|
711
|
-
detect: (context) =>
|
|
739
|
+
detect: (context) => manifestEvidence(context, "requirements.txt", "fastapi") ?? manifestEvidence(context, "requirements.txt", "flask") ?? manifestEvidence(context, "pyproject.toml", "fastapi") ?? manifestEvidence(context, "pyproject.toml", "flask"),
|
|
712
740
|
layers: [
|
|
713
741
|
layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
|
|
714
742
|
layer("models", DIR("models"), DIR("db")),
|
|
@@ -732,7 +760,7 @@ var LANGUAGE_PRESETS = [
|
|
|
732
760
|
name: "python-lib",
|
|
733
761
|
// Below node-lib: a Node repo with a stray .py file is still a Node repo.
|
|
734
762
|
priority: 30,
|
|
735
|
-
detect: (context) =>
|
|
763
|
+
detect: (context) => fileEvidence(context, "pyproject.toml") ?? fileEvidence(context, "setup.py") ?? fileEvidence(context, "requirements.txt") ?? suffixEvidence(context, ".py"),
|
|
736
764
|
layers: [
|
|
737
765
|
layer("package", DIR("src"), DIR("lib")),
|
|
738
766
|
layer("tests", DIR("tests"), DIR("test")),
|
|
@@ -745,7 +773,7 @@ var LANGUAGE_PRESETS = [
|
|
|
745
773
|
{
|
|
746
774
|
name: "go",
|
|
747
775
|
priority: 50,
|
|
748
|
-
detect: (context) =>
|
|
776
|
+
detect: (context) => fileEvidence(context, "go.mod"),
|
|
749
777
|
layers: [
|
|
750
778
|
layer("cmd", "cmd/"),
|
|
751
779
|
layer("internal", "internal/"),
|
|
@@ -769,7 +797,7 @@ var LANGUAGE_PRESETS = [
|
|
|
769
797
|
{
|
|
770
798
|
name: "rust",
|
|
771
799
|
priority: 48,
|
|
772
|
-
detect: (context) =>
|
|
800
|
+
detect: (context) => fileEvidence(context, "Cargo.toml"),
|
|
773
801
|
layers: [
|
|
774
802
|
layer("src", "src/"),
|
|
775
803
|
layer("crates", "crates/"),
|
|
@@ -785,7 +813,7 @@ var LANGUAGE_PRESETS = [
|
|
|
785
813
|
{
|
|
786
814
|
name: "java-spring",
|
|
787
815
|
priority: 46,
|
|
788
|
-
detect: (context) =>
|
|
816
|
+
detect: (context) => fileEvidence(context, "pom.xml") ?? fileEvidence(context, "build.gradle") ?? fileEvidence(context, "build.gradle.kts"),
|
|
789
817
|
layers: [
|
|
790
818
|
layer("api", "src/main/java/"),
|
|
791
819
|
layer("test", "src/test/java/"),
|
|
@@ -808,7 +836,7 @@ var LANGUAGE_PRESETS = [
|
|
|
808
836
|
{
|
|
809
837
|
name: "dotnet",
|
|
810
838
|
priority: 44,
|
|
811
|
-
detect: (context) =>
|
|
839
|
+
detect: (context) => suffixEvidence(context, ".csproj") ?? suffixEvidence(context, ".sln") ?? fileEvidence(context, "global.json"),
|
|
812
840
|
layers: [
|
|
813
841
|
layer("web", "src/", "Web/"),
|
|
814
842
|
layer("domain", "Domain/", "Core/"),
|
|
@@ -831,7 +859,7 @@ var LANGUAGE_PRESETS = [
|
|
|
831
859
|
{
|
|
832
860
|
name: "php-laravel",
|
|
833
861
|
priority: 42,
|
|
834
|
-
detect: (context) =>
|
|
862
|
+
detect: (context) => fileEvidence(context, "artisan") ?? manifestEvidence(context, "composer.json", "laravel") ?? fileEvidence(context, "composer.json"),
|
|
835
863
|
layers: [
|
|
836
864
|
layer("app", "app/"),
|
|
837
865
|
layer("routes", "routes/"),
|
|
@@ -857,7 +885,7 @@ var LANGUAGE_PRESETS = [
|
|
|
857
885
|
{
|
|
858
886
|
name: "ruby-rails",
|
|
859
887
|
priority: 40,
|
|
860
|
-
detect: (context) =>
|
|
888
|
+
detect: (context) => fileEvidence(context, "Gemfile") ?? fileEvidence(context, "config.ru") ?? fileEvidence(context, "Rakefile"),
|
|
861
889
|
layers: [
|
|
862
890
|
layer("app", "app/"),
|
|
863
891
|
layer("controllers", "app/controllers/"),
|
|
@@ -930,7 +958,7 @@ var SHAPE_PRESETS = [
|
|
|
930
958
|
{
|
|
931
959
|
name: "monorepo",
|
|
932
960
|
priority: 70,
|
|
933
|
-
detect: (context) =>
|
|
961
|
+
detect: (context) => fileEvidence(context, "pnpm-workspace.yaml") ?? fileEvidence(context, "nx.json") ?? fileEvidence(context, "turbo.json") ?? fileEvidence(context, "lerna.json") ?? manifestEvidence(context, "package.json", '"workspaces"'),
|
|
934
962
|
layers: [
|
|
935
963
|
layer("apps", "apps/"),
|
|
936
964
|
layer("packages", "packages/"),
|
|
@@ -944,7 +972,7 @@ var SHAPE_PRESETS = [
|
|
|
944
972
|
{
|
|
945
973
|
name: "infra",
|
|
946
974
|
priority: 24,
|
|
947
|
-
detect: (context) =>
|
|
975
|
+
detect: (context) => suffixEvidence(context, ".tf") ?? fileEvidence(context, "Chart.yaml") ?? fileEvidence(context, "kustomization.yaml") ?? fileEvidence(context, "docker-compose.yml") ?? fileEvidence(context, "docker-compose.yaml"),
|
|
948
976
|
layers: [
|
|
949
977
|
layer("modules", "modules/"),
|
|
950
978
|
layer("environments", "env/", "environments/"),
|
|
@@ -966,7 +994,7 @@ var SHAPE_PRESETS = [
|
|
|
966
994
|
// up to two directories down (bounded in the context builder). Counting
|
|
967
995
|
// only the root turned every real documentation repository — README.md plus
|
|
968
996
|
// the pages under `docs/` — into `generic`.
|
|
969
|
-
detect: (context) =>
|
|
997
|
+
detect: (context) => markdownEvidence(context),
|
|
970
998
|
layers: [
|
|
971
999
|
layer("content", "content/", "docs/"),
|
|
972
1000
|
layer("assets", "assets/", "static/", "public/"),
|
|
@@ -979,7 +1007,10 @@ var SHAPE_PRESETS = [
|
|
|
979
1007
|
{
|
|
980
1008
|
name: "generic",
|
|
981
1009
|
priority: 0,
|
|
982
|
-
|
|
1010
|
+
// No detector: this is the fallback, and `detectPresetFromContext` reaches it only
|
|
1011
|
+
// when every other preset reported no evidence. `fallback: true` is what lets the
|
|
1012
|
+
// wizard tell "I recognised this" apart from "I gave up".
|
|
1013
|
+
fallback: true,
|
|
983
1014
|
layers: DEFAULT_LAYERS,
|
|
984
1015
|
kinds: DEFAULT_KINDS,
|
|
985
1016
|
exclude: [],
|
|
@@ -1011,20 +1042,27 @@ function detectPresetFromContext(context, forced) {
|
|
|
1011
1042
|
if (forced !== void 0) {
|
|
1012
1043
|
const preset = BY_NAME.get(forced);
|
|
1013
1044
|
if (!preset) throw new Error(unknownPresetMessage(forced));
|
|
1014
|
-
return toDetected(preset);
|
|
1045
|
+
return toDetected(preset, null);
|
|
1015
1046
|
}
|
|
1016
1047
|
for (const preset of PRESETS) {
|
|
1017
|
-
|
|
1048
|
+
const reason = preset.detect?.(context) ?? null;
|
|
1049
|
+
if (reason !== null) return toDetected(preset, reason);
|
|
1050
|
+
}
|
|
1051
|
+
const fallback = PRESETS.find((preset) => preset.fallback === true);
|
|
1052
|
+
if (fallback === void 0) {
|
|
1053
|
+
throw new Error("No fallback preset is defined. Exactly one preset must set `fallback: true`.");
|
|
1018
1054
|
}
|
|
1019
|
-
return toDetected(
|
|
1055
|
+
return toDetected(fallback, null, true);
|
|
1020
1056
|
}
|
|
1021
|
-
function toDetected(preset) {
|
|
1057
|
+
function toDetected(preset, reason, fallback = false) {
|
|
1022
1058
|
return {
|
|
1023
1059
|
presetName: preset.name,
|
|
1024
1060
|
layers: preset.layers,
|
|
1025
1061
|
kinds: preset.kinds,
|
|
1026
1062
|
exclude: preset.exclude ? [...preset.exclude] : [],
|
|
1027
|
-
recommendedBudget: preset.recommendedBudget ?? null
|
|
1063
|
+
recommendedBudget: preset.recommendedBudget ?? null,
|
|
1064
|
+
reason,
|
|
1065
|
+
fallback
|
|
1028
1066
|
};
|
|
1029
1067
|
}
|
|
1030
1068
|
function detectPreset(cwd, forced) {
|
|
@@ -1470,8 +1508,8 @@ function readLinkTarget(linkPath, fallback) {
|
|
|
1470
1508
|
}
|
|
1471
1509
|
}
|
|
1472
1510
|
function isInside(root, candidate) {
|
|
1473
|
-
const
|
|
1474
|
-
return
|
|
1511
|
+
const relative11 = path2.relative(root, candidate);
|
|
1512
|
+
return relative11 === "" || !relative11.startsWith(`..${path2.sep}`) && relative11 !== ".." && !path2.isAbsolute(relative11);
|
|
1475
1513
|
}
|
|
1476
1514
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
1477
1515
|
try {
|
|
@@ -2153,6 +2191,20 @@ var CONFIG_FILENAMES = [
|
|
|
2153
2191
|
".snapshotrc.json",
|
|
2154
2192
|
".snapshotrc"
|
|
2155
2193
|
];
|
|
2194
|
+
function findConfigFile(cwd) {
|
|
2195
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
2196
|
+
const fullPath = path3.join(cwd, filename);
|
|
2197
|
+
if (fs3.existsSync(fullPath)) return fullPath;
|
|
2198
|
+
}
|
|
2199
|
+
const pkgJsonPath = path3.join(cwd, "package.json");
|
|
2200
|
+
if (!fs3.existsSync(pkgJsonPath)) return null;
|
|
2201
|
+
try {
|
|
2202
|
+
const parsed = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
|
|
2203
|
+
return parsed.snapshot === void 0 ? null : pkgJsonPath;
|
|
2204
|
+
} catch {
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2156
2208
|
var RESOLVED_CONFIG_BRAND = /* @__PURE__ */ Symbol.for(
|
|
2157
2209
|
"codebase-snapshot/resolved-config"
|
|
2158
2210
|
);
|
|
@@ -2608,11 +2660,11 @@ function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
|
2608
2660
|
const patterns = [];
|
|
2609
2661
|
for (const dir of [outDir, tmpDir]) {
|
|
2610
2662
|
const absolute = path3.resolve(cwd, dir);
|
|
2611
|
-
const
|
|
2612
|
-
if (!
|
|
2663
|
+
const relative11 = path3.relative(cwd, absolute);
|
|
2664
|
+
if (!relative11 || relative11 === ".." || relative11.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative11)) {
|
|
2613
2665
|
continue;
|
|
2614
2666
|
}
|
|
2615
|
-
patterns.push(`${
|
|
2667
|
+
patterns.push(`${relative11.replace(/\\/g, "/")}/**`);
|
|
2616
2668
|
}
|
|
2617
2669
|
return patterns;
|
|
2618
2670
|
}
|
|
@@ -4108,21 +4160,21 @@ function describeMatcher(matcher) {
|
|
|
4108
4160
|
}
|
|
4109
4161
|
function normalizeTarget(cwd, target) {
|
|
4110
4162
|
const absolute = path8.resolve(cwd, target);
|
|
4111
|
-
const
|
|
4112
|
-
if (
|
|
4113
|
-
return
|
|
4163
|
+
const relative11 = path8.relative(cwd, absolute);
|
|
4164
|
+
if (relative11.startsWith("..") || path8.isAbsolute(relative11)) return target;
|
|
4165
|
+
return relative11.split(path8.sep).join("/");
|
|
4114
4166
|
}
|
|
4115
4167
|
function findRuleMatch(rules, relativePath) {
|
|
4116
4168
|
return rules.find(
|
|
4117
4169
|
(rule) => matchRule(relativePath, rule.match)
|
|
4118
4170
|
);
|
|
4119
4171
|
}
|
|
4120
|
-
function findRecordedAncestor(skipped,
|
|
4172
|
+
function findRecordedAncestor(skipped, relative11, maskedPath2) {
|
|
4121
4173
|
let nearest;
|
|
4122
4174
|
for (const record of skipped) {
|
|
4123
4175
|
const ancestorPath = record.path;
|
|
4124
4176
|
if (ancestorPath.length === 0) continue;
|
|
4125
|
-
const isAncestor =
|
|
4177
|
+
const isAncestor = relative11.startsWith(`${ancestorPath}/`) || maskedPath2.startsWith(`${ancestorPath}/`);
|
|
4126
4178
|
if (!isAncestor) continue;
|
|
4127
4179
|
if (!nearest || ancestorPath.length > nearest.path.length) nearest = record;
|
|
4128
4180
|
}
|
|
@@ -4137,9 +4189,9 @@ function readManifest2(config) {
|
|
|
4137
4189
|
}
|
|
4138
4190
|
}
|
|
4139
4191
|
function explainPath(config, target) {
|
|
4140
|
-
const
|
|
4141
|
-
const absolute = path8.join(config.cwd,
|
|
4142
|
-
const maskedPath2 = findSecretInPath(
|
|
4192
|
+
const relative11 = normalizeTarget(config.cwd, target);
|
|
4193
|
+
const absolute = path8.join(config.cwd, relative11);
|
|
4194
|
+
const maskedPath2 = findSecretInPath(relative11, config.secretRules)?.masked ?? relative11;
|
|
4143
4195
|
const result = {
|
|
4144
4196
|
path: maskedPath2,
|
|
4145
4197
|
exists: fs7.existsSync(absolute),
|
|
@@ -4154,11 +4206,11 @@ function explainPath(config, target) {
|
|
|
4154
4206
|
// that is only missing because the walker dropped the tool's own output directory.
|
|
4155
4207
|
toolOwnedPaths: [config.outDir, config.tmpDir].map(
|
|
4156
4208
|
(directory) => path8.relative(config.cwd, path8.resolve(config.cwd, directory)).replace(/\\/g, "/")
|
|
4157
|
-
).filter((
|
|
4209
|
+
).filter((relative12) => relative12.length > 0 && !relative12.startsWith(".."))
|
|
4158
4210
|
});
|
|
4159
|
-
if (!collection.files.includes(
|
|
4211
|
+
if (!collection.files.includes(relative11)) {
|
|
4160
4212
|
const record = collection.skipped.find(
|
|
4161
|
-
(entry2) => entry2.path ===
|
|
4213
|
+
(entry2) => entry2.path === relative11 || entry2.path === maskedPath2
|
|
4162
4214
|
);
|
|
4163
4215
|
if (record) {
|
|
4164
4216
|
result.reason = record.reason;
|
|
@@ -4167,7 +4219,7 @@ function explainPath(config, target) {
|
|
|
4167
4219
|
result.reason = "missing";
|
|
4168
4220
|
result.detail = "no such file or directory, relative to the configured cwd";
|
|
4169
4221
|
} else {
|
|
4170
|
-
const ancestor = findRecordedAncestor(collection.skipped,
|
|
4222
|
+
const ancestor = findRecordedAncestor(collection.skipped, relative11, maskedPath2);
|
|
4171
4223
|
if (ancestor) {
|
|
4172
4224
|
result.reason = ancestor.reason;
|
|
4173
4225
|
result.detail = ancestor.detail ? `${ancestor.detail} \u2014 inherited from the skipped directory \`${ancestor.path}\`` : `inside the skipped directory \`${ancestor.path}\``;
|
|
@@ -4185,12 +4237,12 @@ function explainPath(config, target) {
|
|
|
4185
4237
|
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
4186
4238
|
return result;
|
|
4187
4239
|
}
|
|
4188
|
-
result.layer = classifyLayer(
|
|
4189
|
-
result.kind = classifyKind(
|
|
4190
|
-
result.language = detectLanguage(
|
|
4191
|
-
const layerRule = findRuleMatch(config.layers,
|
|
4240
|
+
result.layer = classifyLayer(relative11, config.layers);
|
|
4241
|
+
result.kind = classifyKind(relative11, config.kinds);
|
|
4242
|
+
result.language = detectLanguage(relative11);
|
|
4243
|
+
const layerRule = findRuleMatch(config.layers, relative11);
|
|
4192
4244
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
4193
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
4245
|
+
const kindRule = findRuleMatch(config.kinds, relative11);
|
|
4194
4246
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
4195
4247
|
if (fs7.existsSync(absolute) && fs7.statSync(absolute).isFile()) {
|
|
4196
4248
|
const buffer = fs7.readFileSync(absolute);
|
|
@@ -4205,12 +4257,12 @@ function explainPath(config, target) {
|
|
|
4205
4257
|
result.bytes = buffer.length;
|
|
4206
4258
|
result.lines = countLines(text2);
|
|
4207
4259
|
if (decoded.encoding !== "utf-8") result.encoding = decoded.encoding;
|
|
4208
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
4260
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative11);
|
|
4209
4261
|
result.secretCount = findings.length;
|
|
4210
4262
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rules: finding.rules, line: finding.line }));
|
|
4211
4263
|
}
|
|
4212
4264
|
const manifest = readManifest2(config);
|
|
4213
|
-
const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath ===
|
|
4265
|
+
const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative11) : void 0;
|
|
4214
4266
|
result.part = entry?.part ?? null;
|
|
4215
4267
|
result.inclusionMode = entry?.inclusionMode;
|
|
4216
4268
|
return result;
|
|
@@ -4294,7 +4346,7 @@ function gitignorePath(cwd, filePath) {
|
|
|
4294
4346
|
|
|
4295
4347
|
// src/version.ts
|
|
4296
4348
|
var GENERATOR_NAME = "codebase-snapshot";
|
|
4297
|
-
var GENERATOR_VERSION = "0.9.
|
|
4349
|
+
var GENERATOR_VERSION = "0.9.4";
|
|
4298
4350
|
var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
|
|
4299
4351
|
function isOurGeneratorName(name) {
|
|
4300
4352
|
if (typeof name !== "string") return false;
|
|
@@ -4506,7 +4558,7 @@ function keyReader(input) {
|
|
|
4506
4558
|
next() {
|
|
4507
4559
|
const queued = queue.shift();
|
|
4508
4560
|
if (queued) return Promise.resolve(queued);
|
|
4509
|
-
return new Promise((
|
|
4561
|
+
return new Promise((resolve9) => waiters.push(resolve9));
|
|
4510
4562
|
},
|
|
4511
4563
|
close() {
|
|
4512
4564
|
input.off("keypress", onKey);
|
|
@@ -4861,16 +4913,18 @@ function updatePackageJson(cwd, notes) {
|
|
|
4861
4913
|
return { scripts: added, dependency };
|
|
4862
4914
|
}
|
|
4863
4915
|
var GITIGNORE_ENTRIES = ["snapshot/", "snapshot.tmp/"];
|
|
4864
|
-
function resolvePresetChoice(
|
|
4916
|
+
function resolvePresetChoice(detectedName, presetName) {
|
|
4865
4917
|
return {
|
|
4866
4918
|
value: presetName,
|
|
4867
4919
|
label: presetName,
|
|
4868
|
-
hint: presetName ===
|
|
4920
|
+
hint: presetName === detectedName ? "detected" : void 0
|
|
4869
4921
|
};
|
|
4870
4922
|
}
|
|
4871
4923
|
async function runInit(options) {
|
|
4872
4924
|
const io = options.io ?? defaultIO();
|
|
4873
4925
|
const detected = detectPreset(options.cwd, options.preset);
|
|
4926
|
+
let reason = detected.reason;
|
|
4927
|
+
let fallback = detected.fallback;
|
|
4874
4928
|
const notes = [];
|
|
4875
4929
|
const answers = {
|
|
4876
4930
|
preset: detected.presetName,
|
|
@@ -4892,112 +4946,143 @@ async function runInit(options) {
|
|
|
4892
4946
|
if (interactive) {
|
|
4893
4947
|
io.output.write("\n codebase-snapshot\n");
|
|
4894
4948
|
io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
|
|
4895
|
-
|
|
4949
|
+
if (fallback) {
|
|
4950
|
+
io.output.write(" \u26A0 Could not tell what kind of project this is.\n");
|
|
4951
|
+
io.output.write(" No package.json, go.mod, Cargo.toml, pyproject.toml, Gemfile,\n");
|
|
4952
|
+
io.output.write(" composer.json, pom.xml, *.csproj, next.config.*, vite.config.*, *.tf\n");
|
|
4953
|
+
io.output.write(" or workspace marker was found here.\n\n");
|
|
4954
|
+
} else {
|
|
4955
|
+
io.output.write(` Detected ${detected.presetName}
|
|
4956
|
+
`);
|
|
4957
|
+
io.output.write(` Because ${reason}
|
|
4958
|
+
|
|
4896
4959
|
`);
|
|
4960
|
+
}
|
|
4897
4961
|
io.output.write(` Directory ${options.cwd}
|
|
4898
4962
|
|
|
4899
4963
|
`);
|
|
4900
|
-
const
|
|
4901
|
-
|
|
4964
|
+
const paceChoices = fallback ? [
|
|
4965
|
+
{ value: "choose", label: "Pick the closest preset", hint: "then generate with the defaults" },
|
|
4966
|
+
{ value: "recommended", label: "Use the defaults", hint: "generic \xB7 everything the scanner finds" },
|
|
4967
|
+
{ value: "walk", label: "Go through each setting", hint: "every question starts on its default" }
|
|
4968
|
+
] : [
|
|
4969
|
+
{
|
|
4970
|
+
value: "recommended",
|
|
4971
|
+
label: "Use the recommended setup",
|
|
4972
|
+
hint: `${detected.presetName} \xB7 config \xB7 npm scripts \xB7 install \xB7 generate`
|
|
4973
|
+
},
|
|
4974
|
+
{ value: "walk", label: "Go through each setting", hint: "every question starts on its default" },
|
|
4975
|
+
{ value: "choose", label: "Pick a different preset", hint: "everything else stays at the defaults" }
|
|
4976
|
+
];
|
|
4977
|
+
const pace = await select(io, "How should I set this up?", paceChoices, {
|
|
4978
|
+
defaultValue: fallback ? "choose" : "recommended"
|
|
4902
4979
|
});
|
|
4903
|
-
if (
|
|
4980
|
+
if (pace === "choose") {
|
|
4904
4981
|
answers.preset = await select(
|
|
4905
4982
|
io,
|
|
4906
4983
|
"What kind of project is this?",
|
|
4907
|
-
listPresetNames().map(
|
|
4984
|
+
listPresetNames().map(
|
|
4985
|
+
(name) => resolvePresetChoice(fallback ? null : detected.presetName, name)
|
|
4986
|
+
),
|
|
4987
|
+
{ defaultValue: answers.preset }
|
|
4908
4988
|
);
|
|
4909
4989
|
const chosen = detectPreset(options.cwd, answers.preset);
|
|
4910
4990
|
answers.preset = chosen.presetName;
|
|
4991
|
+
reason = null;
|
|
4992
|
+
fallback = false;
|
|
4911
4993
|
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4994
|
+
if (pace === "walk") {
|
|
4995
|
+
const scope = await select(
|
|
4996
|
+
io,
|
|
4997
|
+
"What should go into the snapshot?",
|
|
4998
|
+
[
|
|
4999
|
+
{ value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
|
|
5000
|
+
{ value: "folders", label: "Only specific folders\u2026" },
|
|
5001
|
+
{ value: "no-tests", label: "Everything, but skip tests" },
|
|
5002
|
+
{ value: "custom", label: "Let me write glob patterns" }
|
|
5003
|
+
],
|
|
5004
|
+
{ defaultValue: "all" }
|
|
5005
|
+
);
|
|
5006
|
+
if (scope === "folders") {
|
|
5007
|
+
const dirs = topLevelDirs(options.cwd).slice(0, 40);
|
|
5008
|
+
if (dirs.length === 0) {
|
|
5009
|
+
notes.push("No top-level directories found; the snapshot will include everything.");
|
|
5010
|
+
} else {
|
|
5011
|
+
const chosen = await multiSelect(
|
|
5012
|
+
io,
|
|
5013
|
+
"Select folders",
|
|
5014
|
+
dirs.map((name) => ({ value: name, label: `${name}/` })),
|
|
5015
|
+
{ defaults: dirs }
|
|
5016
|
+
);
|
|
5017
|
+
answers.include = chosen.map((name) => `${name}/**`);
|
|
5018
|
+
}
|
|
5019
|
+
} else if (scope === "no-tests") {
|
|
5020
|
+
answers.excludePatterns = [
|
|
5021
|
+
...answers.excludePatterns,
|
|
5022
|
+
"**/*.test.*",
|
|
5023
|
+
"**/*.spec.*",
|
|
5024
|
+
"tests",
|
|
5025
|
+
"test"
|
|
5026
|
+
];
|
|
5027
|
+
} else if (scope === "custom") {
|
|
5028
|
+
const raw = await text(io, "Glob patterns to include (comma separated)", {
|
|
5029
|
+
defaultValue: ""
|
|
5030
|
+
});
|
|
5031
|
+
answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
5032
|
+
}
|
|
5033
|
+
const currentBudget = String(answers.maxPartBytes);
|
|
5034
|
+
const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
|
|
5035
|
+
defaultValue: SIZE_CHOICES.some((choice) => choice.value === currentBudget) ? currentBudget : "120k"
|
|
5036
|
+
});
|
|
5037
|
+
if (sizeChoice === "custom") {
|
|
5038
|
+
const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
|
|
5039
|
+
defaultValue: String(answers.maxPartBytes),
|
|
5040
|
+
validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
|
|
5041
|
+
});
|
|
5042
|
+
answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
|
|
4927
5043
|
} else {
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
...answers.excludePatterns,
|
|
4939
|
-
"**/*.test.*",
|
|
4940
|
-
"**/*.spec.*",
|
|
4941
|
-
"tests",
|
|
4942
|
-
"test"
|
|
5044
|
+
answers.maxPartBytes = sizeChoice;
|
|
5045
|
+
}
|
|
5046
|
+
const artifactChoices = [
|
|
5047
|
+
{ value: "readme", label: "README.md", hint: "orientation page" },
|
|
5048
|
+
{ value: "manifest", label: "manifest.json", hint: "required by validate and report" },
|
|
5049
|
+
{ value: "parts", label: "parts/", hint: "the code payload" },
|
|
5050
|
+
{ value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
|
|
5051
|
+
{ value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
|
|
5052
|
+
{ value: "audit", label: "reports/audit.md", hint: "security and health findings" },
|
|
5053
|
+
{ value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
|
|
4943
5054
|
];
|
|
4944
|
-
|
|
4945
|
-
const
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
answers.maxPartBytes = sizeChoice;
|
|
4961
|
-
}
|
|
4962
|
-
const artifactChoices = [
|
|
4963
|
-
{ value: "readme", label: "README.md", hint: "orientation page" },
|
|
4964
|
-
{ value: "manifest", label: "manifest.json", hint: "required by validate and report" },
|
|
4965
|
-
{ value: "parts", label: "parts/", hint: "the code payload" },
|
|
4966
|
-
{ value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
|
|
4967
|
-
{ value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
|
|
4968
|
-
{ value: "audit", label: "reports/audit.md", hint: "security and health findings" },
|
|
4969
|
-
{ value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
|
|
4970
|
-
];
|
|
4971
|
-
const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
|
|
4972
|
-
const selectedArtifacts = await multiSelect(
|
|
4973
|
-
io,
|
|
4974
|
-
"Which artifacts should be generated?",
|
|
4975
|
-
artifactChoices,
|
|
4976
|
-
{ defaults: presetEnabled }
|
|
4977
|
-
);
|
|
4978
|
-
for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
|
|
4979
|
-
const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
|
|
4980
|
-
answers.includeSensitiveFiles = await confirm(
|
|
4981
|
-
io,
|
|
4982
|
-
"Include credential files (.env, *.pem, .npmrc, .ssh/)?",
|
|
4983
|
-
{ defaultValue: false }
|
|
4984
|
-
);
|
|
4985
|
-
io.output.write("\n Review\n\n");
|
|
4986
|
-
io.output.write(` project ${answers.preset}
|
|
5055
|
+
const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
|
|
5056
|
+
const selectedArtifacts = await multiSelect(
|
|
5057
|
+
io,
|
|
5058
|
+
"Which artifacts should be generated?",
|
|
5059
|
+
artifactChoices,
|
|
5060
|
+
{ defaults: presetEnabled }
|
|
5061
|
+
);
|
|
5062
|
+
for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
|
|
5063
|
+
const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
|
|
5064
|
+
answers.includeSensitiveFiles = await confirm(
|
|
5065
|
+
io,
|
|
5066
|
+
"Include credential files (.env, *.pem, .npmrc, .ssh/)?",
|
|
5067
|
+
{ defaultValue: false }
|
|
5068
|
+
);
|
|
5069
|
+
io.output.write("\n Review\n\n");
|
|
5070
|
+
io.output.write(` project ${answers.preset}
|
|
4987
5071
|
`);
|
|
4988
|
-
|
|
5072
|
+
io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
|
|
4989
5073
|
`);
|
|
4990
|
-
|
|
5074
|
+
io.output.write(` part size ${String(answers.maxPartBytes)}
|
|
4991
5075
|
`);
|
|
4992
|
-
|
|
5076
|
+
io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
|
|
4993
5077
|
`);
|
|
4994
|
-
|
|
5078
|
+
io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
|
|
4995
5079
|
|
|
4996
5080
|
`);
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5081
|
+
io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
|
|
5082
|
+
io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
|
|
5083
|
+
const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
|
|
5084
|
+
if (!proceed) throw new PromptCancelled();
|
|
5085
|
+
}
|
|
5001
5086
|
}
|
|
5002
5087
|
const configText = renderConfig(answers);
|
|
5003
5088
|
const preferredName = chooseConfigFileName(options.cwd);
|
|
@@ -5021,6 +5106,8 @@ async function runInit(options) {
|
|
|
5021
5106
|
written: false,
|
|
5022
5107
|
reused,
|
|
5023
5108
|
preset: answers.preset,
|
|
5109
|
+
reason,
|
|
5110
|
+
fallback,
|
|
5024
5111
|
scripts: [],
|
|
5025
5112
|
gitignoreEntries: [],
|
|
5026
5113
|
dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
|
|
@@ -5055,6 +5142,8 @@ async function runInit(options) {
|
|
|
5055
5142
|
written: true,
|
|
5056
5143
|
reused,
|
|
5057
5144
|
preset: answers.preset,
|
|
5145
|
+
reason,
|
|
5146
|
+
fallback,
|
|
5058
5147
|
scripts,
|
|
5059
5148
|
gitignoreEntries: ignore.added,
|
|
5060
5149
|
dependency,
|
|
@@ -5062,12 +5151,58 @@ async function runInit(options) {
|
|
|
5062
5151
|
};
|
|
5063
5152
|
}
|
|
5064
5153
|
|
|
5154
|
+
// src/wizard/autopilot.ts
|
|
5155
|
+
import * as fs10 from "fs";
|
|
5156
|
+
import * as path11 from "path";
|
|
5157
|
+
function describeTarget(cwd) {
|
|
5158
|
+
const relative11 = path11.relative(process.cwd(), cwd);
|
|
5159
|
+
if (relative11 === "") return "this directory";
|
|
5160
|
+
if (relative11.startsWith("..") || path11.isAbsolute(relative11)) return cwd;
|
|
5161
|
+
return relative11;
|
|
5162
|
+
}
|
|
5163
|
+
function firstRunMessage(reason, cwd) {
|
|
5164
|
+
const where = describeTarget(cwd);
|
|
5165
|
+
if (reason === "empty") {
|
|
5166
|
+
return `Nothing to snapshot: ${where} has no files. Run this inside the repository you want to snapshot, or pass --cwd.`;
|
|
5167
|
+
}
|
|
5168
|
+
return `No configuration found in ${where} and this session is not interactive, so nothing was generated.
|
|
5169
|
+
Choose one:
|
|
5170
|
+
${GENERATOR_NAME} --autopilot detect the project, set it up and generate, no questions
|
|
5171
|
+
${GENERATOR_NAME} init --yes write a config from the detected defaults, then generate
|
|
5172
|
+
${GENERATOR_NAME} init write a config interactively`;
|
|
5173
|
+
}
|
|
5174
|
+
function planFirstRun(facts) {
|
|
5175
|
+
if (facts.emptyTarget) return { action: "refuse", exitCode: 1, reason: "empty" };
|
|
5176
|
+
if (facts.autopilot) return { action: "setup", interactive: false };
|
|
5177
|
+
if (facts.hasConfig || facts.hasPriorSnapshot) return { action: "generate" };
|
|
5178
|
+
if (facts.interactive) return { action: "setup", interactive: true };
|
|
5179
|
+
return { action: "refuse", exitCode: 2, reason: "undecided" };
|
|
5180
|
+
}
|
|
5181
|
+
var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
|
|
5182
|
+
function isEffectivelyEmpty(cwd) {
|
|
5183
|
+
try {
|
|
5184
|
+
return fs10.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
|
|
5185
|
+
} catch {
|
|
5186
|
+
return false;
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
function hasPriorSnapshot(cwd, outDir) {
|
|
5190
|
+
let parsed;
|
|
5191
|
+
try {
|
|
5192
|
+
parsed = JSON.parse(fs10.readFileSync(path11.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
|
|
5193
|
+
} catch {
|
|
5194
|
+
return false;
|
|
5195
|
+
}
|
|
5196
|
+
if (parsed === null || typeof parsed !== "object") return false;
|
|
5197
|
+
return isOurGeneratorName(parsed.generator);
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5065
5200
|
// src/index.ts
|
|
5066
5201
|
import * as crypto3 from "crypto";
|
|
5067
5202
|
import { AsyncLocalStorage } from "async_hooks";
|
|
5068
|
-
import * as
|
|
5203
|
+
import * as fs11 from "fs";
|
|
5069
5204
|
import * as os from "os";
|
|
5070
|
-
import * as
|
|
5205
|
+
import * as path12 from "path";
|
|
5071
5206
|
|
|
5072
5207
|
// src/ast/profiles/shared.ts
|
|
5073
5208
|
var MAX_SUMMARY_CHARS = 1e6;
|
|
@@ -5291,9 +5426,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
|
|
|
5291
5426
|
var jvmProfile = {
|
|
5292
5427
|
id: "jvm",
|
|
5293
5428
|
extensions: [".java", ".kt", ".kts", ".scala"],
|
|
5294
|
-
summarize(
|
|
5429
|
+
summarize(path18, content) {
|
|
5295
5430
|
const summary = emptySummary();
|
|
5296
|
-
const lower =
|
|
5431
|
+
const lower = path18.toLowerCase();
|
|
5297
5432
|
const isJava = lower.endsWith(".java");
|
|
5298
5433
|
const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
|
|
5299
5434
|
eachLine(content, (line) => {
|
|
@@ -5749,16 +5884,16 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
5749
5884
|
// src/ast/profiles/sfc.ts
|
|
5750
5885
|
var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
5751
5886
|
var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
|
|
5752
|
-
function componentName(
|
|
5753
|
-
const base =
|
|
5887
|
+
function componentName(path18) {
|
|
5888
|
+
const base = path18.split("/").pop() ?? path18;
|
|
5754
5889
|
return base.replace(/\.[^.]+$/, "");
|
|
5755
5890
|
}
|
|
5756
5891
|
var sfcProfile = {
|
|
5757
5892
|
id: "sfc",
|
|
5758
5893
|
extensions: [".vue", ".svelte", ".astro"],
|
|
5759
|
-
summarize(
|
|
5894
|
+
summarize(path18, content) {
|
|
5760
5895
|
let script = "";
|
|
5761
|
-
if (
|
|
5896
|
+
if (path18.toLowerCase().endsWith(".astro")) {
|
|
5762
5897
|
const frontmatter = ASTRO_FRONTMATTER.exec(content);
|
|
5763
5898
|
if (frontmatter) script += `${frontmatter[1]}
|
|
5764
5899
|
`;
|
|
@@ -5767,7 +5902,7 @@ var sfcProfile = {
|
|
|
5767
5902
|
script += `${match[1]}
|
|
5768
5903
|
`;
|
|
5769
5904
|
}
|
|
5770
|
-
const name = componentName(
|
|
5905
|
+
const name = componentName(path18);
|
|
5771
5906
|
if (!script.trim()) {
|
|
5772
5907
|
const summary2 = emptySummary();
|
|
5773
5908
|
if (name) {
|
|
@@ -5801,15 +5936,15 @@ for (const profile of LANGUAGE_PROFILES) {
|
|
|
5801
5936
|
BY_EXTENSION.set(extension.toLowerCase(), profile);
|
|
5802
5937
|
}
|
|
5803
5938
|
}
|
|
5804
|
-
function profileForPath(
|
|
5805
|
-
const lower =
|
|
5939
|
+
function profileForPath(path18) {
|
|
5940
|
+
const lower = path18.toLowerCase();
|
|
5806
5941
|
const dot = lower.lastIndexOf(".");
|
|
5807
5942
|
if (dot === -1) return null;
|
|
5808
5943
|
return BY_EXTENSION.get(lower.slice(dot)) ?? null;
|
|
5809
5944
|
}
|
|
5810
|
-
function summarizeWithProfile(
|
|
5811
|
-
const profile = profileForPath(
|
|
5812
|
-
return profile ? profile.summarize(
|
|
5945
|
+
function summarizeWithProfile(path18, content) {
|
|
5946
|
+
const profile = profileForPath(path18);
|
|
5947
|
+
return profile ? profile.summarize(path18, content) : null;
|
|
5813
5948
|
}
|
|
5814
5949
|
|
|
5815
5950
|
// src/ast/structured.ts
|
|
@@ -6739,13 +6874,13 @@ function buildImportResolver(files) {
|
|
|
6739
6874
|
const relativePath = file.relativePath;
|
|
6740
6875
|
remember(byPath, relativePath, relativePath);
|
|
6741
6876
|
remember(byPathFolded, relativePath.toLowerCase(), relativePath);
|
|
6742
|
-
const extension =
|
|
6877
|
+
const extension = path12.posix.extname(relativePath);
|
|
6743
6878
|
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
|
6744
6879
|
const stem = relativePath.slice(0, -extension.length);
|
|
6745
6880
|
remember(byStem, stem, relativePath);
|
|
6746
6881
|
remember(byStemFolded, stem.toLowerCase(), relativePath);
|
|
6747
|
-
if (
|
|
6748
|
-
const directory =
|
|
6882
|
+
if (path12.posix.basename(stem) === "index") {
|
|
6883
|
+
const directory = path12.posix.dirname(stem);
|
|
6749
6884
|
remember(byStem, directory, relativePath);
|
|
6750
6885
|
remember(byStemFolded, directory.toLowerCase(), relativePath);
|
|
6751
6886
|
}
|
|
@@ -6754,8 +6889,8 @@ function buildImportResolver(files) {
|
|
|
6754
6889
|
}
|
|
6755
6890
|
function resolveSpecifier(fromFile, specifier, resolver) {
|
|
6756
6891
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
6757
|
-
const joined =
|
|
6758
|
-
|
|
6892
|
+
const joined = path12.posix.normalize(
|
|
6893
|
+
path12.posix.join(path12.posix.dirname(fromFile), specifier)
|
|
6759
6894
|
);
|
|
6760
6895
|
const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
|
|
6761
6896
|
const direct = exact(joined);
|
|
@@ -6848,11 +6983,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
|
|
|
6848
6983
|
return errors;
|
|
6849
6984
|
}
|
|
6850
6985
|
function resolveSafeOutputDirs(config) {
|
|
6851
|
-
const cwd =
|
|
6852
|
-
const outDir =
|
|
6853
|
-
const tmpDir =
|
|
6854
|
-
const root =
|
|
6855
|
-
const contains = (parent, child) => child === parent || child.startsWith(`${parent}${
|
|
6986
|
+
const cwd = path12.resolve(config.cwd);
|
|
6987
|
+
const outDir = path12.resolve(cwd, config.outDir);
|
|
6988
|
+
const tmpDir = path12.resolve(cwd, config.tmpDir);
|
|
6989
|
+
const root = path12.parse(cwd).root;
|
|
6990
|
+
const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path12.sep}`);
|
|
6856
6991
|
const reject = (reason) => {
|
|
6857
6992
|
throw new Error(
|
|
6858
6993
|
`Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
|
|
@@ -6908,14 +7043,14 @@ function listEntries(dir) {
|
|
|
6908
7043
|
const walk = (absolute, prefix) => {
|
|
6909
7044
|
let children;
|
|
6910
7045
|
try {
|
|
6911
|
-
children =
|
|
7046
|
+
children = fs11.readdirSync(absolute, { withFileTypes: true });
|
|
6912
7047
|
} catch {
|
|
6913
7048
|
return;
|
|
6914
7049
|
}
|
|
6915
7050
|
for (const child of children) {
|
|
6916
|
-
const
|
|
6917
|
-
entries.push(
|
|
6918
|
-
if (child.isDirectory()) walk(
|
|
7051
|
+
const relative11 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
7052
|
+
entries.push(relative11);
|
|
7053
|
+
if (child.isDirectory()) walk(path12.join(absolute, child.name), relative11);
|
|
6919
7054
|
}
|
|
6920
7055
|
};
|
|
6921
7056
|
walk(dir, "");
|
|
@@ -6932,28 +7067,28 @@ function markerPayload(dir, state, id, createdAt) {
|
|
|
6932
7067
|
pid: process.pid,
|
|
6933
7068
|
entries: listEntries(dir)
|
|
6934
7069
|
};
|
|
6935
|
-
const manifestPath =
|
|
6936
|
-
if (
|
|
6937
|
-
marker2.manifestSha256 = sha256Hex2(
|
|
7070
|
+
const manifestPath = path12.join(dir, "manifest.json");
|
|
7071
|
+
if (fs11.existsSync(manifestPath)) {
|
|
7072
|
+
marker2.manifestSha256 = sha256Hex2(fs11.readFileSync(manifestPath));
|
|
6938
7073
|
}
|
|
6939
7074
|
return marker2;
|
|
6940
7075
|
}
|
|
6941
7076
|
function writeOwnershipMarker(dir, marker2) {
|
|
6942
|
-
|
|
6943
|
-
|
|
7077
|
+
fs11.writeFileSync(
|
|
7078
|
+
path12.join(dir, OWNERSHIP_MARKER),
|
|
6944
7079
|
`${JSON.stringify(marker2, null, 2)}
|
|
6945
7080
|
`,
|
|
6946
7081
|
"utf8"
|
|
6947
7082
|
);
|
|
6948
7083
|
}
|
|
6949
7084
|
function readOwnershipMarker(dir) {
|
|
6950
|
-
const markerPath =
|
|
6951
|
-
if (!
|
|
7085
|
+
const markerPath = path12.join(dir, OWNERSHIP_MARKER);
|
|
7086
|
+
if (!fs11.existsSync(markerPath)) {
|
|
6952
7087
|
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
6953
7088
|
}
|
|
6954
7089
|
let raw;
|
|
6955
7090
|
try {
|
|
6956
|
-
raw =
|
|
7091
|
+
raw = fs11.readFileSync(markerPath, "utf8");
|
|
6957
7092
|
} catch {
|
|
6958
7093
|
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
6959
7094
|
}
|
|
@@ -7021,11 +7156,11 @@ function assessSnapshotDirectory(dir) {
|
|
|
7021
7156
|
marker: marker2
|
|
7022
7157
|
};
|
|
7023
7158
|
}
|
|
7024
|
-
const manifestPath =
|
|
7025
|
-
if (
|
|
7159
|
+
const manifestPath = path12.join(dir, "manifest.json");
|
|
7160
|
+
if (fs11.existsSync(manifestPath)) {
|
|
7026
7161
|
let raw;
|
|
7027
7162
|
try {
|
|
7028
|
-
raw =
|
|
7163
|
+
raw = fs11.readFileSync(manifestPath, "utf8");
|
|
7029
7164
|
} catch {
|
|
7030
7165
|
return {
|
|
7031
7166
|
owned: false,
|
|
@@ -7107,13 +7242,13 @@ function containsNoFiles(dir) {
|
|
|
7107
7242
|
const current = pending.pop();
|
|
7108
7243
|
let entries;
|
|
7109
7244
|
try {
|
|
7110
|
-
entries =
|
|
7245
|
+
entries = fs11.readdirSync(current, { withFileTypes: true });
|
|
7111
7246
|
} catch {
|
|
7112
7247
|
return false;
|
|
7113
7248
|
}
|
|
7114
7249
|
for (const entry of entries) {
|
|
7115
7250
|
if (entry.isDirectory()) {
|
|
7116
|
-
pending.push(
|
|
7251
|
+
pending.push(path12.join(current, entry.name));
|
|
7117
7252
|
} else if (!IGNORABLE_ENTRIES.has(entry.name)) {
|
|
7118
7253
|
return false;
|
|
7119
7254
|
}
|
|
@@ -7122,13 +7257,13 @@ function containsNoFiles(dir) {
|
|
|
7122
7257
|
return true;
|
|
7123
7258
|
}
|
|
7124
7259
|
function assertReplaceable(target, label, secretRules) {
|
|
7125
|
-
if (!
|
|
7126
|
-
if (!
|
|
7260
|
+
if (!fs11.existsSync(target)) return;
|
|
7261
|
+
if (!fs11.statSync(target).isDirectory()) {
|
|
7127
7262
|
throw new Error(
|
|
7128
7263
|
`Refusing to write into ${label} ${target}: it exists but is not a directory.`
|
|
7129
7264
|
);
|
|
7130
7265
|
}
|
|
7131
|
-
const entries =
|
|
7266
|
+
const entries = fs11.readdirSync(target);
|
|
7132
7267
|
if (entries.length === 0) return;
|
|
7133
7268
|
if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
|
|
7134
7269
|
if (containsNoFiles(target)) return;
|
|
@@ -7148,18 +7283,18 @@ function assertReplaceable(target, label, secretRules) {
|
|
|
7148
7283
|
);
|
|
7149
7284
|
}
|
|
7150
7285
|
function assertStagingRootUsable(tmpDir, secretRules) {
|
|
7151
|
-
if (!
|
|
7152
|
-
if (!
|
|
7286
|
+
if (!fs11.existsSync(tmpDir)) return;
|
|
7287
|
+
if (!fs11.statSync(tmpDir).isDirectory()) {
|
|
7153
7288
|
throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
|
|
7154
7289
|
}
|
|
7155
|
-
const entries =
|
|
7290
|
+
const entries = fs11.readdirSync(tmpDir);
|
|
7156
7291
|
if (entries.length === 0) return;
|
|
7157
7292
|
const foreign = entries.filter((name) => {
|
|
7158
|
-
const full =
|
|
7293
|
+
const full = path12.join(tmpDir, name);
|
|
7159
7294
|
if (!name.startsWith(RUN_DIR_PREFIX)) return true;
|
|
7160
7295
|
let stats;
|
|
7161
7296
|
try {
|
|
7162
|
-
stats =
|
|
7297
|
+
stats = fs11.statSync(full);
|
|
7163
7298
|
} catch {
|
|
7164
7299
|
return true;
|
|
7165
7300
|
}
|
|
@@ -7167,7 +7302,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
|
|
|
7167
7302
|
const { marker: marker2 } = readOwnershipMarker(full);
|
|
7168
7303
|
if (marker2) return false;
|
|
7169
7304
|
try {
|
|
7170
|
-
return
|
|
7305
|
+
return fs11.readdirSync(full).length > 0;
|
|
7171
7306
|
} catch {
|
|
7172
7307
|
return true;
|
|
7173
7308
|
}
|
|
@@ -7182,13 +7317,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
|
|
|
7182
7317
|
function cleanStaleRunDirs(tmpDir) {
|
|
7183
7318
|
let entries;
|
|
7184
7319
|
try {
|
|
7185
|
-
entries =
|
|
7320
|
+
entries = fs11.readdirSync(tmpDir);
|
|
7186
7321
|
} catch {
|
|
7187
7322
|
return;
|
|
7188
7323
|
}
|
|
7189
7324
|
for (const name of entries) {
|
|
7190
7325
|
if (!name.startsWith(RUN_DIR_PREFIX)) continue;
|
|
7191
|
-
const full =
|
|
7326
|
+
const full = path12.join(tmpDir, name);
|
|
7192
7327
|
const { marker: marker2 } = readOwnershipMarker(full);
|
|
7193
7328
|
if (marker2) {
|
|
7194
7329
|
const created = Date.parse(marker2.createdAt);
|
|
@@ -7196,33 +7331,33 @@ function cleanStaleRunDirs(tmpDir) {
|
|
|
7196
7331
|
const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
|
|
7197
7332
|
if (aged || abandoned) {
|
|
7198
7333
|
try {
|
|
7199
|
-
|
|
7334
|
+
fs11.rmSync(full, { recursive: true, force: true });
|
|
7200
7335
|
} catch {
|
|
7201
7336
|
}
|
|
7202
7337
|
}
|
|
7203
7338
|
continue;
|
|
7204
7339
|
}
|
|
7205
7340
|
try {
|
|
7206
|
-
if (
|
|
7341
|
+
if (fs11.readdirSync(full).length === 0) fs11.rmSync(full, { recursive: true, force: true });
|
|
7207
7342
|
} catch {
|
|
7208
7343
|
}
|
|
7209
7344
|
}
|
|
7210
7345
|
}
|
|
7211
7346
|
function removeIfEmpty(dir) {
|
|
7212
7347
|
try {
|
|
7213
|
-
if (
|
|
7348
|
+
if (fs11.readdirSync(dir).length === 0) fs11.rmSync(dir, { recursive: true, force: true });
|
|
7214
7349
|
} catch {
|
|
7215
7350
|
}
|
|
7216
7351
|
}
|
|
7217
7352
|
function ensureParentDirectory(target, label) {
|
|
7218
|
-
const parent =
|
|
7219
|
-
if (
|
|
7353
|
+
const parent = path12.dirname(target);
|
|
7354
|
+
if (fs11.existsSync(parent) && !fs11.statSync(parent).isDirectory()) {
|
|
7220
7355
|
throw new Error(
|
|
7221
7356
|
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
7222
7357
|
);
|
|
7223
7358
|
}
|
|
7224
7359
|
try {
|
|
7225
|
-
|
|
7360
|
+
fs11.mkdirSync(parent, { recursive: true });
|
|
7226
7361
|
} catch (err) {
|
|
7227
7362
|
throw new Error(
|
|
7228
7363
|
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
@@ -7231,7 +7366,7 @@ function ensureParentDirectory(target, label) {
|
|
|
7231
7366
|
}
|
|
7232
7367
|
function acquireRunLock(stagingRoot) {
|
|
7233
7368
|
const key = lockKeyFor(stagingRoot);
|
|
7234
|
-
const lockPath =
|
|
7369
|
+
const lockPath = path12.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
|
|
7235
7370
|
const payload = `${JSON.stringify({
|
|
7236
7371
|
pid: process.pid,
|
|
7237
7372
|
stagingRoot,
|
|
@@ -7240,22 +7375,22 @@ function acquireRunLock(stagingRoot) {
|
|
|
7240
7375
|
`;
|
|
7241
7376
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
7242
7377
|
try {
|
|
7243
|
-
const handle =
|
|
7378
|
+
const handle = fs11.openSync(lockPath, "wx");
|
|
7244
7379
|
try {
|
|
7245
|
-
|
|
7380
|
+
fs11.writeSync(handle, payload);
|
|
7246
7381
|
} finally {
|
|
7247
|
-
|
|
7382
|
+
fs11.closeSync(handle);
|
|
7248
7383
|
}
|
|
7249
7384
|
return () => {
|
|
7250
7385
|
try {
|
|
7251
|
-
|
|
7386
|
+
fs11.rmSync(lockPath, { force: true });
|
|
7252
7387
|
} catch {
|
|
7253
7388
|
}
|
|
7254
7389
|
};
|
|
7255
7390
|
} catch (err) {
|
|
7256
7391
|
if (err.code !== "EEXIST") throw err;
|
|
7257
7392
|
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
7258
|
-
|
|
7393
|
+
fs11.rmSync(lockPath, { force: true });
|
|
7259
7394
|
continue;
|
|
7260
7395
|
}
|
|
7261
7396
|
throw new Error(
|
|
@@ -7268,7 +7403,7 @@ function acquireRunLock(stagingRoot) {
|
|
|
7268
7403
|
function lockKeyFor(stagingRoot) {
|
|
7269
7404
|
let key = stagingRoot;
|
|
7270
7405
|
try {
|
|
7271
|
-
key =
|
|
7406
|
+
key = fs11.realpathSync(stagingRoot);
|
|
7272
7407
|
} catch {
|
|
7273
7408
|
}
|
|
7274
7409
|
if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
|
|
@@ -7277,7 +7412,7 @@ function lockKeyFor(stagingRoot) {
|
|
|
7277
7412
|
function lockOwnerIsGone(lockPath) {
|
|
7278
7413
|
let raw;
|
|
7279
7414
|
try {
|
|
7280
|
-
raw =
|
|
7415
|
+
raw = fs11.readFileSync(lockPath, "utf8");
|
|
7281
7416
|
} catch {
|
|
7282
7417
|
return false;
|
|
7283
7418
|
}
|
|
@@ -7297,12 +7432,12 @@ function lockOwnerIsGone(lockPath) {
|
|
|
7297
7432
|
}
|
|
7298
7433
|
function writeManifestAtomically(manifestPath, manifest) {
|
|
7299
7434
|
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
7300
|
-
|
|
7435
|
+
fs11.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
7301
7436
|
try {
|
|
7302
|
-
|
|
7437
|
+
fs11.renameSync(tempPath, manifestPath);
|
|
7303
7438
|
} catch (err) {
|
|
7304
7439
|
try {
|
|
7305
|
-
|
|
7440
|
+
fs11.rmSync(tempPath, { force: true });
|
|
7306
7441
|
} catch {
|
|
7307
7442
|
}
|
|
7308
7443
|
throw err;
|
|
@@ -7325,31 +7460,31 @@ function swapIntoPlace(stagingDir, outDir) {
|
|
|
7325
7460
|
}
|
|
7326
7461
|
function swapOnce(stagingDir, outDir) {
|
|
7327
7462
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
7328
|
-
const hadPrevious =
|
|
7329
|
-
if (hadPrevious)
|
|
7463
|
+
const hadPrevious = fs11.existsSync(outDir);
|
|
7464
|
+
if (hadPrevious) fs11.renameSync(outDir, backupDir);
|
|
7330
7465
|
try {
|
|
7331
7466
|
try {
|
|
7332
|
-
|
|
7467
|
+
fs11.renameSync(stagingDir, outDir);
|
|
7333
7468
|
} catch (err) {
|
|
7334
7469
|
if (err.code !== "EXDEV") throw err;
|
|
7335
7470
|
const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
|
|
7336
7471
|
try {
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7472
|
+
fs11.cpSync(stagingDir, partialDir, { recursive: true });
|
|
7473
|
+
fs11.renameSync(partialDir, outDir);
|
|
7474
|
+
fs11.rmSync(stagingDir, { recursive: true, force: true });
|
|
7340
7475
|
} catch (copyErr) {
|
|
7341
7476
|
try {
|
|
7342
|
-
|
|
7477
|
+
fs11.rmSync(partialDir, { recursive: true, force: true });
|
|
7343
7478
|
} catch {
|
|
7344
7479
|
}
|
|
7345
7480
|
throw copyErr;
|
|
7346
7481
|
}
|
|
7347
7482
|
}
|
|
7348
|
-
if (hadPrevious)
|
|
7483
|
+
if (hadPrevious) fs11.rmSync(backupDir, { recursive: true, force: true });
|
|
7349
7484
|
} catch (err) {
|
|
7350
|
-
if (hadPrevious &&
|
|
7351
|
-
if (
|
|
7352
|
-
|
|
7485
|
+
if (hadPrevious && fs11.existsSync(backupDir)) {
|
|
7486
|
+
if (fs11.existsSync(outDir)) fs11.rmSync(outDir, { recursive: true, force: true });
|
|
7487
|
+
fs11.renameSync(backupDir, outDir);
|
|
7353
7488
|
}
|
|
7354
7489
|
throw err;
|
|
7355
7490
|
}
|
|
@@ -7444,8 +7579,8 @@ function generateSnapshot(configInput, options) {
|
|
|
7444
7579
|
// first run cannot have marked them yet, so the name alone keeps a run from recording its own
|
|
7445
7580
|
// output — but only at these exact paths, not at every directory that shares the name.
|
|
7446
7581
|
toolOwnedPaths: [config.outDir, config.tmpDir].map(
|
|
7447
|
-
(directory) =>
|
|
7448
|
-
).filter((
|
|
7582
|
+
(directory) => path12.relative(config.cwd, path12.resolve(config.cwd, directory)).replace(/\\/g, "/")
|
|
7583
|
+
).filter((relative11) => relative11.length > 0 && !relative11.startsWith(".."))
|
|
7449
7584
|
});
|
|
7450
7585
|
const files = [];
|
|
7451
7586
|
const monolithBodies = /* @__PURE__ */ new Map();
|
|
@@ -7465,12 +7600,12 @@ function generateSnapshot(configInput, options) {
|
|
|
7465
7600
|
let totalSizeBytes = 0;
|
|
7466
7601
|
let largeFileCount = 0;
|
|
7467
7602
|
for (const relativePath of collection.files) {
|
|
7468
|
-
const absolutePath =
|
|
7603
|
+
const absolutePath = path12.join(config.cwd, relativePath);
|
|
7469
7604
|
let stats;
|
|
7470
7605
|
try {
|
|
7471
|
-
stats =
|
|
7606
|
+
stats = fs11.statSync(absolutePath);
|
|
7472
7607
|
if (!stats.isFile()) {
|
|
7473
|
-
const nestedRepository = stats.isDirectory() &&
|
|
7608
|
+
const nestedRepository = stats.isDirectory() && fs11.existsSync(path12.join(absolutePath, ".git"));
|
|
7474
7609
|
skipped.push(
|
|
7475
7610
|
nestedRepository ? {
|
|
7476
7611
|
path: relativePath,
|
|
@@ -7490,7 +7625,7 @@ function generateSnapshot(configInput, options) {
|
|
|
7490
7625
|
}
|
|
7491
7626
|
let buffer;
|
|
7492
7627
|
try {
|
|
7493
|
-
buffer =
|
|
7628
|
+
buffer = fs11.readFileSync(absolutePath);
|
|
7494
7629
|
} catch {
|
|
7495
7630
|
skipped.push({ path: relativePath, reason: "read-error" });
|
|
7496
7631
|
continue;
|
|
@@ -7820,12 +7955,12 @@ function generateSnapshot(configInput, options) {
|
|
|
7820
7955
|
let stagingDir = null;
|
|
7821
7956
|
try {
|
|
7822
7957
|
ensureParentDirectory(outDir, "outDir");
|
|
7823
|
-
|
|
7958
|
+
fs11.mkdirSync(tmpDir, { recursive: true });
|
|
7824
7959
|
cleanStaleRunDirs(tmpDir);
|
|
7825
7960
|
const snapshotId = crypto3.randomUUID();
|
|
7826
7961
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7827
|
-
stagingDir =
|
|
7828
|
-
|
|
7962
|
+
stagingDir = path12.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
7963
|
+
fs11.mkdirSync(stagingDir);
|
|
7829
7964
|
writeOwnershipMarker(stagingDir, {
|
|
7830
7965
|
generator: GENERATOR_NAME,
|
|
7831
7966
|
version: GENERATOR_VERSION,
|
|
@@ -7835,41 +7970,41 @@ function generateSnapshot(configInput, options) {
|
|
|
7835
7970
|
id: snapshotId,
|
|
7836
7971
|
pid: process.pid
|
|
7837
7972
|
});
|
|
7838
|
-
const partsDir =
|
|
7839
|
-
const reportsDir =
|
|
7840
|
-
if (artifacts.parts)
|
|
7973
|
+
const partsDir = path12.join(stagingDir, "parts");
|
|
7974
|
+
const reportsDir = path12.join(stagingDir, "reports");
|
|
7975
|
+
if (artifacts.parts) fs11.mkdirSync(partsDir, { recursive: true });
|
|
7841
7976
|
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
7842
|
-
|
|
7977
|
+
fs11.mkdirSync(reportsDir, { recursive: true });
|
|
7843
7978
|
}
|
|
7844
7979
|
if (artifacts.parts) {
|
|
7845
7980
|
for (const part of parts) {
|
|
7846
|
-
|
|
7981
|
+
fs11.writeFileSync(path12.join(partsDir, part.filename), part.markdown, "utf8");
|
|
7847
7982
|
}
|
|
7848
7983
|
}
|
|
7849
7984
|
if (projectTree !== null) {
|
|
7850
|
-
|
|
7985
|
+
fs11.writeFileSync(path12.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
7851
7986
|
}
|
|
7852
7987
|
if (artifacts.navigation) {
|
|
7853
|
-
|
|
7988
|
+
fs11.writeFileSync(path12.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
7854
7989
|
}
|
|
7855
7990
|
if (artifacts.audit) {
|
|
7856
|
-
|
|
7857
|
-
|
|
7991
|
+
fs11.writeFileSync(
|
|
7992
|
+
path12.join(reportsDir, "audit.md"),
|
|
7858
7993
|
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
7859
7994
|
"utf8"
|
|
7860
7995
|
);
|
|
7861
7996
|
}
|
|
7862
7997
|
if (artifacts.readme) {
|
|
7863
|
-
|
|
7998
|
+
fs11.writeFileSync(path12.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
|
|
7864
7999
|
}
|
|
7865
8000
|
if (artifacts.manifest) {
|
|
7866
|
-
|
|
8001
|
+
fs11.writeFileSync(path12.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
7867
8002
|
}
|
|
7868
8003
|
if (monolithJson !== null) {
|
|
7869
|
-
|
|
8004
|
+
fs11.writeFileSync(path12.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
7870
8005
|
}
|
|
7871
8006
|
if (monolithMarkdown !== null) {
|
|
7872
|
-
|
|
8007
|
+
fs11.writeFileSync(path12.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
7873
8008
|
}
|
|
7874
8009
|
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
7875
8010
|
swapIntoPlace(stagingDir, outDir);
|
|
@@ -7878,7 +8013,7 @@ function generateSnapshot(configInput, options) {
|
|
|
7878
8013
|
const pendingHook = runAfterGenerate(config, manifest);
|
|
7879
8014
|
const shipHookState = () => {
|
|
7880
8015
|
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
7881
|
-
writeManifestAtomically(
|
|
8016
|
+
writeManifestAtomically(path12.join(outDir, "manifest.json"), manifest);
|
|
7882
8017
|
}
|
|
7883
8018
|
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
7884
8019
|
};
|
|
@@ -7901,7 +8036,7 @@ function generateSnapshot(configInput, options) {
|
|
|
7901
8036
|
} catch (err) {
|
|
7902
8037
|
if (stagingDir !== null) {
|
|
7903
8038
|
try {
|
|
7904
|
-
|
|
8039
|
+
fs11.rmSync(stagingDir, { recursive: true, force: true });
|
|
7905
8040
|
} catch {
|
|
7906
8041
|
}
|
|
7907
8042
|
}
|
|
@@ -7912,177 +8047,1178 @@ function generateSnapshot(configInput, options) {
|
|
|
7912
8047
|
}
|
|
7913
8048
|
}
|
|
7914
8049
|
|
|
7915
|
-
// src/
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
"
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
"
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
"version"
|
|
7986
|
-
],
|
|
7987
|
-
diff: ["format", "mode", "no-config", "help", "version"],
|
|
7988
|
-
describe: ["format", "mode", "help", "version"],
|
|
7989
|
-
init: [
|
|
7990
|
-
"cwd",
|
|
7991
|
-
"out-dir",
|
|
7992
|
-
"max-part-bytes",
|
|
7993
|
-
"include-sensitive-files",
|
|
7994
|
-
"skip-artifacts",
|
|
7995
|
-
"only-artifacts",
|
|
7996
|
-
"preset",
|
|
7997
|
-
"include",
|
|
7998
|
-
"exclude",
|
|
7999
|
-
"yes",
|
|
8000
|
-
"force",
|
|
8001
|
-
"dry-run",
|
|
8002
|
-
"format",
|
|
8003
|
-
"mode",
|
|
8004
|
-
"help",
|
|
8005
|
-
"version"
|
|
8006
|
-
],
|
|
8007
|
-
skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
|
|
8008
|
-
setup: [
|
|
8009
|
-
"cwd",
|
|
8010
|
-
"out-dir",
|
|
8011
|
-
"max-part-bytes",
|
|
8012
|
-
"include-sensitive-files",
|
|
8013
|
-
"skip-artifacts",
|
|
8014
|
-
"only-artifacts",
|
|
8015
|
-
"preset",
|
|
8016
|
-
"include",
|
|
8017
|
-
"exclude",
|
|
8018
|
-
"yes",
|
|
8019
|
-
"force",
|
|
8020
|
-
"no-install",
|
|
8021
|
-
"dry-run",
|
|
8022
|
-
"format",
|
|
8023
|
-
"mode",
|
|
8024
|
-
"help",
|
|
8025
|
-
"version"
|
|
8026
|
-
]
|
|
8027
|
-
};
|
|
8028
|
-
var COMMAND_ORDER = [
|
|
8029
|
-
"generate",
|
|
8030
|
-
"validate",
|
|
8031
|
-
"report",
|
|
8032
|
-
"explain",
|
|
8033
|
-
"init",
|
|
8034
|
-
"setup",
|
|
8035
|
-
"diff",
|
|
8036
|
-
"describe",
|
|
8037
|
-
"skill"
|
|
8038
|
-
];
|
|
8039
|
-
function commandsForOption(option) {
|
|
8040
|
-
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
8050
|
+
// src/mcp/tools.ts
|
|
8051
|
+
import * as fs13 from "fs";
|
|
8052
|
+
import * as path14 from "path";
|
|
8053
|
+
|
|
8054
|
+
// src/mcp/part.ts
|
|
8055
|
+
var FILE_HEADING = "### FILE: ";
|
|
8056
|
+
var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
|
|
8057
|
+
function parsePartSections(partText) {
|
|
8058
|
+
const lines = partText.split("\n");
|
|
8059
|
+
const sections = [];
|
|
8060
|
+
let index = 0;
|
|
8061
|
+
while (index < lines.length) {
|
|
8062
|
+
if (!lines[index].startsWith(FILE_HEADING)) {
|
|
8063
|
+
index += 1;
|
|
8064
|
+
continue;
|
|
8065
|
+
}
|
|
8066
|
+
const heading = lines[index].slice(FILE_HEADING.length);
|
|
8067
|
+
let cursor = index + 1;
|
|
8068
|
+
let inclusion = null;
|
|
8069
|
+
let sha256 = null;
|
|
8070
|
+
let includedLines = null;
|
|
8071
|
+
while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
|
|
8072
|
+
const line = lines[cursor];
|
|
8073
|
+
if (line.startsWith(FILE_HEADING)) break;
|
|
8074
|
+
if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
|
|
8075
|
+
else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
|
|
8076
|
+
else if (line.startsWith("- Included Lines: ")) {
|
|
8077
|
+
const value = Number(line.slice("- Included Lines: ".length).trim());
|
|
8078
|
+
includedLines = Number.isFinite(value) ? value : null;
|
|
8079
|
+
}
|
|
8080
|
+
cursor += 1;
|
|
8081
|
+
}
|
|
8082
|
+
if (cursor >= lines.length || !CONTENT_MARKERS.has(lines[cursor])) {
|
|
8083
|
+
index = cursor + 1;
|
|
8084
|
+
continue;
|
|
8085
|
+
}
|
|
8086
|
+
let fenceLine = cursor + 1;
|
|
8087
|
+
while (fenceLine < lines.length && lines[fenceLine].trim().length === 0) fenceLine += 1;
|
|
8088
|
+
const fenceMatch = /^(`{4,})/.exec(lines[fenceLine] ?? "");
|
|
8089
|
+
if (!fenceMatch) {
|
|
8090
|
+
index = cursor + 1;
|
|
8091
|
+
continue;
|
|
8092
|
+
}
|
|
8093
|
+
const fence = fenceMatch[1];
|
|
8094
|
+
const body = [];
|
|
8095
|
+
let bodyLine = fenceLine + 1;
|
|
8096
|
+
while (bodyLine < lines.length && lines[bodyLine] !== fence) {
|
|
8097
|
+
body.push(lines[bodyLine]);
|
|
8098
|
+
bodyLine += 1;
|
|
8099
|
+
}
|
|
8100
|
+
sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
|
|
8101
|
+
index = bodyLine + 1;
|
|
8102
|
+
}
|
|
8103
|
+
return sections;
|
|
8104
|
+
}
|
|
8105
|
+
function findSection(sections, relativePath, sha256) {
|
|
8106
|
+
const exact = sections.find((section) => section.path === relativePath);
|
|
8107
|
+
if (exact) return exact;
|
|
8108
|
+
if (sha256) {
|
|
8109
|
+
const byHash = sections.filter((section) => section.sha256 === sha256);
|
|
8110
|
+
if (byHash.length === 1) return byHash[0];
|
|
8111
|
+
if (byHash.length > 1) {
|
|
8112
|
+
const byPath = byHash.find(
|
|
8113
|
+
(section) => section.path.toLowerCase() === relativePath.toLowerCase()
|
|
8114
|
+
);
|
|
8115
|
+
if (byPath) return byPath;
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
|
|
8119
|
+
return folded.length === 1 ? folded[0] : null;
|
|
8041
8120
|
}
|
|
8042
|
-
function
|
|
8043
|
-
|
|
8121
|
+
function windowText(body, fromLine, lineCount) {
|
|
8122
|
+
const lines = body.split("\n");
|
|
8123
|
+
const total = lines.length;
|
|
8124
|
+
const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
|
|
8125
|
+
const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
|
|
8126
|
+
const slice = lines.slice(start - 1, end);
|
|
8127
|
+
return {
|
|
8128
|
+
text: slice.join("\n"),
|
|
8129
|
+
fromLine: start,
|
|
8130
|
+
returnedLines: slice.length,
|
|
8131
|
+
totalLines: total,
|
|
8132
|
+
truncated: end < total,
|
|
8133
|
+
nextFromLine: end < total ? end + 1 : null
|
|
8134
|
+
};
|
|
8044
8135
|
}
|
|
8045
8136
|
|
|
8046
|
-
// src/
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
}
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
};
|
|
8068
|
-
|
|
8069
|
-
const
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8137
|
+
// src/mcp/workspace.ts
|
|
8138
|
+
import * as fs12 from "fs";
|
|
8139
|
+
import * as path13 from "path";
|
|
8140
|
+
function foldForComparison(value) {
|
|
8141
|
+
return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
|
|
8142
|
+
}
|
|
8143
|
+
function canonicalPath(value) {
|
|
8144
|
+
const absolute = path13.resolve(value);
|
|
8145
|
+
try {
|
|
8146
|
+
return fs12.realpathSync(absolute);
|
|
8147
|
+
} catch {
|
|
8148
|
+
return absolute;
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
function isInside2(parent, child) {
|
|
8152
|
+
const foldedParent = foldForComparison(parent);
|
|
8153
|
+
const foldedChild = foldForComparison(child);
|
|
8154
|
+
return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path13.sep}`);
|
|
8155
|
+
}
|
|
8156
|
+
function resolveRoot(requested, context) {
|
|
8157
|
+
if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
|
|
8158
|
+
return { ok: false, message: '"root" must be a non-empty string when provided.' };
|
|
8159
|
+
}
|
|
8160
|
+
const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
|
|
8161
|
+
const absolute = path13.resolve(context.cwd, wanted);
|
|
8162
|
+
let stats;
|
|
8163
|
+
try {
|
|
8164
|
+
stats = fs12.statSync(absolute);
|
|
8165
|
+
} catch {
|
|
8166
|
+
return { ok: false, message: `"${wanted}" does not exist, so it cannot be a repository root.` };
|
|
8167
|
+
}
|
|
8168
|
+
if (!stats.isDirectory()) {
|
|
8169
|
+
return { ok: false, message: `"${wanted}" is not a directory.` };
|
|
8170
|
+
}
|
|
8171
|
+
const resolved = canonicalPath(absolute);
|
|
8172
|
+
for (const allowed of context.roots) {
|
|
8173
|
+
if (isInside2(canonicalPath(allowed), resolved)) return { ok: true, root: resolved };
|
|
8174
|
+
}
|
|
8175
|
+
const shown = context.roots.join(", ");
|
|
8176
|
+
return {
|
|
8177
|
+
ok: false,
|
|
8178
|
+
message: `"${wanted}" resolves outside the directories this server may read (${shown}). Restart it with --root <path> to add a repository.`
|
|
8179
|
+
};
|
|
8180
|
+
}
|
|
8181
|
+
async function openWorkspace(root, overrides = {}) {
|
|
8182
|
+
const loaded = await loadConfigFileDetailed(root);
|
|
8183
|
+
if (loaded.errors.length > 0) {
|
|
8184
|
+
return {
|
|
8185
|
+
ok: false,
|
|
8186
|
+
message: `The config file could not be read: ${loaded.errors.join("; ")} Fix it, or delete it to use the defaults \u2014 this server will not silently ignore it.`
|
|
8187
|
+
};
|
|
8188
|
+
}
|
|
8189
|
+
const cliOptions = { cwd: root };
|
|
8190
|
+
if (overrides.maxPartBytes !== void 0) {
|
|
8191
|
+
const parsed = parseByteSize(overrides.maxPartBytes);
|
|
8192
|
+
if (parsed === void 0) {
|
|
8193
|
+
return {
|
|
8194
|
+
ok: false,
|
|
8195
|
+
message: '"maxPartBytes" must be a byte count, for example 307200 or "300k".'
|
|
8196
|
+
};
|
|
8197
|
+
}
|
|
8198
|
+
cliOptions.maxPartBytes = parsed;
|
|
8199
|
+
}
|
|
8200
|
+
if (overrides.outDir !== void 0) {
|
|
8201
|
+
if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
|
|
8202
|
+
return { ok: false, message: '"outDir" must be a non-empty string.' };
|
|
8203
|
+
}
|
|
8204
|
+
cliOptions.outDir = overrides.outDir.trim();
|
|
8205
|
+
}
|
|
8206
|
+
try {
|
|
8207
|
+
const config = resolveConfig(loaded.config, cliOptions, root);
|
|
8208
|
+
return { ok: true, workspace: { root, config, configFile: loaded.file } };
|
|
8209
|
+
} catch (error) {
|
|
8210
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
8211
|
+
}
|
|
8212
|
+
}
|
|
8213
|
+
var manifestCache = /* @__PURE__ */ new Map();
|
|
8214
|
+
function manifestPathFor(workspace) {
|
|
8215
|
+
return path13.resolve(workspace.root, workspace.config.outDir, "manifest.json");
|
|
8216
|
+
}
|
|
8217
|
+
function readManifest3(workspace) {
|
|
8218
|
+
const file = manifestPathFor(workspace);
|
|
8219
|
+
const shown = path13.relative(workspace.root, file).split(path13.sep).join("/") || file;
|
|
8220
|
+
let stats;
|
|
8221
|
+
try {
|
|
8222
|
+
stats = fs12.statSync(file);
|
|
8223
|
+
} catch {
|
|
8224
|
+
return {
|
|
8225
|
+
ok: false,
|
|
8226
|
+
missing: true,
|
|
8227
|
+
message: `There is no snapshot manifest at ${shown}. Call snapshot_plan to preview what would be included, or snapshot_generate to write it.`
|
|
8228
|
+
};
|
|
8229
|
+
}
|
|
8230
|
+
const cached = manifestCache.get(file);
|
|
8231
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
8232
|
+
return { ok: true, manifest: cached.manifest, file };
|
|
8233
|
+
}
|
|
8234
|
+
let raw;
|
|
8235
|
+
try {
|
|
8236
|
+
raw = fs12.readFileSync(file, "utf8");
|
|
8237
|
+
} catch (error) {
|
|
8238
|
+
return {
|
|
8239
|
+
ok: false,
|
|
8240
|
+
missing: false,
|
|
8241
|
+
message: `${shown} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`
|
|
8242
|
+
};
|
|
8243
|
+
}
|
|
8244
|
+
let parsed;
|
|
8245
|
+
try {
|
|
8246
|
+
parsed = JSON.parse(raw);
|
|
8247
|
+
} catch {
|
|
8248
|
+
return { ok: false, missing: false, message: `${shown} is not valid JSON.` };
|
|
8249
|
+
}
|
|
8250
|
+
const problem = describeManifestShapeProblem(parsed);
|
|
8251
|
+
if (problem) {
|
|
8252
|
+
return { ok: false, missing: false, message: `${shown} is not a snapshot manifest: ${problem}` };
|
|
8253
|
+
}
|
|
8254
|
+
const manifest = parsed;
|
|
8255
|
+
manifestCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, manifest });
|
|
8256
|
+
return { ok: true, manifest, file };
|
|
8257
|
+
}
|
|
8258
|
+
function forgetManifest(file) {
|
|
8259
|
+
manifestCache.delete(file);
|
|
8260
|
+
}
|
|
8261
|
+
|
|
8262
|
+
// src/mcp/tools.ts
|
|
8263
|
+
var READ_ONLY = {
|
|
8264
|
+
readOnlyHint: true,
|
|
8265
|
+
destructiveHint: false,
|
|
8266
|
+
idempotentHint: true,
|
|
8267
|
+
openWorldHint: false
|
|
8268
|
+
};
|
|
8269
|
+
var WRITES = {
|
|
8270
|
+
readOnlyHint: false,
|
|
8271
|
+
// It replaces the snapshot directory but never anything it did not write, so it is additive with
|
|
8272
|
+
// respect to the user's files.
|
|
8273
|
+
destructiveHint: false,
|
|
8274
|
+
idempotentHint: true,
|
|
8275
|
+
openWorldHint: false
|
|
8276
|
+
};
|
|
8277
|
+
var MAX_RESULT_BYTES = 64 * 1024;
|
|
8278
|
+
var ROOT_PROPERTY = {
|
|
8279
|
+
type: "string",
|
|
8280
|
+
description: "Repository root. Defaults to the directory this server was started in."
|
|
8281
|
+
};
|
|
8282
|
+
function failure(message) {
|
|
8283
|
+
return { text: message, isError: true };
|
|
8284
|
+
}
|
|
8285
|
+
function plural(count, word) {
|
|
8286
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
8287
|
+
}
|
|
8288
|
+
function formatBytes(bytes) {
|
|
8289
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
8290
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
8291
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
8292
|
+
}
|
|
8293
|
+
function relativeFor(workspace, absolute) {
|
|
8294
|
+
return path14.relative(workspace.root, absolute).split(path14.sep).join("/");
|
|
8295
|
+
}
|
|
8296
|
+
function capped(values, limit = 20) {
|
|
8297
|
+
return { count: values.length, sample: values.slice(0, limit) };
|
|
8298
|
+
}
|
|
8299
|
+
function summarizeDrift(drift) {
|
|
8300
|
+
return {
|
|
8301
|
+
missingInSnapshot: capped(drift.missingInSnapshot),
|
|
8302
|
+
missingOnDisk: capped(drift.missingOnDisk),
|
|
8303
|
+
unreadable: capped(drift.unreadable),
|
|
8304
|
+
hashMismatches: capped(drift.hashMismatches),
|
|
8305
|
+
partIssues: capped(drift.partIssues),
|
|
8306
|
+
totalsMismatches: capped(drift.totalsMismatches),
|
|
8307
|
+
configMismatches: capped(drift.configMismatches)
|
|
8308
|
+
};
|
|
8309
|
+
}
|
|
8310
|
+
function toRelativePath(root, value) {
|
|
8311
|
+
const cleaned = value.trim();
|
|
8312
|
+
if (cleaned.length === 0) return null;
|
|
8313
|
+
if (!path14.isAbsolute(cleaned)) {
|
|
8314
|
+
return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
8315
|
+
}
|
|
8316
|
+
const relative11 = path14.relative(root, cleaned);
|
|
8317
|
+
if (relative11.startsWith("..") || path14.isAbsolute(relative11)) return null;
|
|
8318
|
+
return relative11.split(path14.sep).join("/");
|
|
8319
|
+
}
|
|
8320
|
+
function symbolsOf(entry) {
|
|
8321
|
+
const summary = entry.summary;
|
|
8322
|
+
if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
|
|
8323
|
+
const source = summary;
|
|
8324
|
+
return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
|
|
8325
|
+
}
|
|
8326
|
+
async function prepare(args, context, overrides = {}) {
|
|
8327
|
+
const rootOutcome = resolveRoot(args.root, context);
|
|
8328
|
+
if (!rootOutcome.ok) return { ok: false, outcome: failure(rootOutcome.message) };
|
|
8329
|
+
const opened = await openWorkspace(rootOutcome.root, overrides);
|
|
8330
|
+
if (!opened.ok) return { ok: false, outcome: failure(opened.message) };
|
|
8331
|
+
return { ok: true, workspace: opened.workspace };
|
|
8332
|
+
}
|
|
8333
|
+
var statusTool = {
|
|
8334
|
+
name: "snapshot_status",
|
|
8335
|
+
title: "Snapshot status",
|
|
8336
|
+
description: "Report whether a snapshot exists for this repository, when it was built, and what it covers. Reads the manifest rather than the repository, so it is cheap. Call it before deciding whether a snapshot needs to be generated.",
|
|
8337
|
+
inputSchema: {
|
|
8338
|
+
type: "object",
|
|
8339
|
+
properties: { root: ROOT_PROPERTY },
|
|
8340
|
+
additionalProperties: false
|
|
8341
|
+
},
|
|
8342
|
+
outputSchema: {
|
|
8343
|
+
type: "object",
|
|
8344
|
+
properties: {
|
|
8345
|
+
present: { type: "boolean" },
|
|
8346
|
+
root: { type: "string" },
|
|
8347
|
+
outDir: { type: "string" },
|
|
8348
|
+
fingerprint: { type: "string" },
|
|
8349
|
+
generatedAt: { type: "string" },
|
|
8350
|
+
generatorVersion: { type: "string" },
|
|
8351
|
+
generatorVersionMatches: { type: "boolean" },
|
|
8352
|
+
includedFiles: { type: "integer" },
|
|
8353
|
+
skippedFiles: { type: "integer" },
|
|
8354
|
+
partCount: { type: "integer" },
|
|
8355
|
+
warningCount: { type: "integer" }
|
|
8356
|
+
}
|
|
8357
|
+
},
|
|
8358
|
+
annotations: READ_ONLY,
|
|
8359
|
+
async handler(args, context) {
|
|
8360
|
+
const prepared = await prepare(args, context);
|
|
8361
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8362
|
+
const workspace = prepared.workspace;
|
|
8363
|
+
const read = readManifest3(workspace);
|
|
8364
|
+
if (!read.ok) {
|
|
8365
|
+
return {
|
|
8366
|
+
text: read.message,
|
|
8367
|
+
structured: {
|
|
8368
|
+
present: false,
|
|
8369
|
+
root: workspace.root,
|
|
8370
|
+
outDir: workspace.config.outDir,
|
|
8371
|
+
message: read.message
|
|
8372
|
+
}
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
const manifest = read.manifest;
|
|
8376
|
+
const versionMatches = manifest.generator.version === GENERATOR_VERSION;
|
|
8377
|
+
const lines = [
|
|
8378
|
+
`Snapshot: ${workspace.config.outDir}/`,
|
|
8379
|
+
`Generated: ${manifest.generatedAt} by ${manifest.generator.name} ${manifest.generator.version}`,
|
|
8380
|
+
`Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
|
|
8381
|
+
`Content: ${formatBytes(manifest.counts.totalSizeBytes)} of source in ${formatBytes(manifest.totals.totalContentBytes)} emitted`,
|
|
8382
|
+
`Fingerprint: ${manifest.fingerprint}`,
|
|
8383
|
+
`Git: ${manifest.git.branch} @ ${manifest.git.commit}${manifest.git.isDirty ? " (dirty)" : " (clean)"}`
|
|
8384
|
+
];
|
|
8385
|
+
if (manifest.totals.warningCount > 0) lines.push(`Warnings: ${manifest.totals.warningCount}`);
|
|
8386
|
+
if (!versionMatches) {
|
|
8387
|
+
lines.push(
|
|
8388
|
+
`Note: written by ${manifest.generator.version}, this server is ${GENERATOR_VERSION}. Re-run snapshot_generate if the difference matters.`
|
|
8389
|
+
);
|
|
8390
|
+
}
|
|
8391
|
+
return {
|
|
8392
|
+
text: lines.join("\n"),
|
|
8393
|
+
structured: {
|
|
8394
|
+
present: true,
|
|
8395
|
+
root: workspace.root,
|
|
8396
|
+
outDir: workspace.config.outDir,
|
|
8397
|
+
fingerprint: manifest.fingerprint,
|
|
8398
|
+
generatedAt: manifest.generatedAt,
|
|
8399
|
+
generatorVersion: manifest.generator.version,
|
|
8400
|
+
generatorVersionMatches: versionMatches,
|
|
8401
|
+
includedFiles: manifest.counts.totalFiles,
|
|
8402
|
+
skippedFiles: manifest.counts.skippedCount,
|
|
8403
|
+
partCount: manifest.counts.partsCount,
|
|
8404
|
+
warningCount: manifest.totals.warningCount,
|
|
8405
|
+
preset: manifest.config.preset,
|
|
8406
|
+
maxPartBytes: manifest.budgets.maxPartBytes
|
|
8407
|
+
}
|
|
8408
|
+
};
|
|
8409
|
+
}
|
|
8410
|
+
};
|
|
8411
|
+
var planTool = {
|
|
8412
|
+
name: "snapshot_plan",
|
|
8413
|
+
title: "Preview a snapshot",
|
|
8414
|
+
description: "Preview a snapshot without writing anything: how many files and parts it would contain, and why every excluded file was excluded. Use it to choose a part budget before generating.",
|
|
8415
|
+
inputSchema: {
|
|
8416
|
+
type: "object",
|
|
8417
|
+
properties: {
|
|
8418
|
+
root: ROOT_PROPERTY,
|
|
8419
|
+
maxPartBytes: {
|
|
8420
|
+
type: "string",
|
|
8421
|
+
description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
|
|
8422
|
+
}
|
|
8423
|
+
},
|
|
8424
|
+
additionalProperties: false
|
|
8425
|
+
},
|
|
8426
|
+
outputSchema: {
|
|
8427
|
+
type: "object",
|
|
8428
|
+
properties: {
|
|
8429
|
+
files: { type: "integer" },
|
|
8430
|
+
skipped: { type: "integer" },
|
|
8431
|
+
parts: { type: "integer" },
|
|
8432
|
+
sourceBytes: { type: "integer" },
|
|
8433
|
+
emittedBytes: { type: "integer" },
|
|
8434
|
+
preset: { type: "string" }
|
|
8435
|
+
}
|
|
8436
|
+
},
|
|
8437
|
+
annotations: READ_ONLY,
|
|
8438
|
+
async handler(args, context) {
|
|
8439
|
+
const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
|
|
8440
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8441
|
+
const workspace = prepared.workspace;
|
|
8442
|
+
let manifest;
|
|
8443
|
+
try {
|
|
8444
|
+
manifest = generateSnapshot(workspace.config, { dryRun: true });
|
|
8445
|
+
} catch (error) {
|
|
8446
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8447
|
+
}
|
|
8448
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
8449
|
+
for (const record of manifest.skipped) {
|
|
8450
|
+
reasons.set(record.reason, (reasons.get(record.reason) ?? 0) + 1);
|
|
8451
|
+
}
|
|
8452
|
+
const topReasons = [...reasons.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 8).map(([reason, count]) => ({ reason, count }));
|
|
8453
|
+
const lines = [
|
|
8454
|
+
"Dry run \u2014 nothing was written.",
|
|
8455
|
+
`Preset: ${manifest.config.preset}`,
|
|
8456
|
+
`Would include ${plural(manifest.counts.totalFiles, "file")} in ${plural(manifest.counts.partsCount, "part")} (${formatBytes(manifest.counts.totalSizeBytes)} of source, ${formatBytes(manifest.totals.totalContentBytes)} emitted).`,
|
|
8457
|
+
`Would skip ${plural(manifest.counts.skippedCount, "file")}.`
|
|
8458
|
+
];
|
|
8459
|
+
if (topReasons.length > 0) {
|
|
8460
|
+
lines.push("Top skip reasons:");
|
|
8461
|
+
for (const entry of topReasons) lines.push(` - ${entry.reason} (${entry.count})`);
|
|
8462
|
+
}
|
|
8463
|
+
if (manifest.warnings.length > 0) {
|
|
8464
|
+
lines.push(`Warnings (${manifest.warnings.length}):`);
|
|
8465
|
+
for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
|
|
8466
|
+
}
|
|
8467
|
+
return {
|
|
8468
|
+
text: lines.join("\n"),
|
|
8469
|
+
structured: {
|
|
8470
|
+
files: manifest.counts.totalFiles,
|
|
8471
|
+
skipped: manifest.counts.skippedCount,
|
|
8472
|
+
parts: manifest.counts.partsCount,
|
|
8473
|
+
sourceBytes: manifest.counts.totalSizeBytes,
|
|
8474
|
+
emittedBytes: manifest.totals.totalContentBytes,
|
|
8475
|
+
preset: manifest.config.preset,
|
|
8476
|
+
maxPartBytes: manifest.budgets.maxPartBytes,
|
|
8477
|
+
layers: manifest.layers,
|
|
8478
|
+
skipReasons: topReasons,
|
|
8479
|
+
warnings: manifest.warnings.slice(0, 5),
|
|
8480
|
+
warningCount: manifest.warnings.length
|
|
8481
|
+
}
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8484
|
+
};
|
|
8485
|
+
var generateTool = {
|
|
8486
|
+
name: "snapshot_generate",
|
|
8487
|
+
title: "Generate the snapshot",
|
|
8488
|
+
description: "Write the snapshot to its output directory and return its fingerprint, parts and counts. Replaces the previous snapshot in that directory. A large repository can take several seconds.",
|
|
8489
|
+
inputSchema: {
|
|
8490
|
+
type: "object",
|
|
8491
|
+
properties: {
|
|
8492
|
+
root: ROOT_PROPERTY,
|
|
8493
|
+
maxPartBytes: {
|
|
8494
|
+
type: "string",
|
|
8495
|
+
description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
|
|
8496
|
+
},
|
|
8497
|
+
outDir: {
|
|
8498
|
+
type: "string",
|
|
8499
|
+
description: "Output directory relative to the root. Defaults to the project configuration."
|
|
8500
|
+
}
|
|
8501
|
+
},
|
|
8502
|
+
additionalProperties: false
|
|
8503
|
+
},
|
|
8504
|
+
outputSchema: {
|
|
8505
|
+
type: "object",
|
|
8506
|
+
properties: {
|
|
8507
|
+
fingerprint: { type: "string" },
|
|
8508
|
+
files: { type: "integer" },
|
|
8509
|
+
skipped: { type: "integer" },
|
|
8510
|
+
parts: { type: "integer" },
|
|
8511
|
+
emittedBytes: { type: "integer" }
|
|
8512
|
+
}
|
|
8513
|
+
},
|
|
8514
|
+
annotations: WRITES,
|
|
8515
|
+
async handler(args, context) {
|
|
8516
|
+
const prepared = await prepare(args, context, {
|
|
8517
|
+
maxPartBytes: args.maxPartBytes,
|
|
8518
|
+
outDir: args.outDir
|
|
8519
|
+
});
|
|
8520
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8521
|
+
const workspace = prepared.workspace;
|
|
8522
|
+
let manifest;
|
|
8523
|
+
try {
|
|
8524
|
+
manifest = generateSnapshot(workspace.config);
|
|
8525
|
+
} catch (error) {
|
|
8526
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8527
|
+
}
|
|
8528
|
+
forgetManifest(manifestPathFor(workspace));
|
|
8529
|
+
const written = relativeFor(workspace, path14.resolve(workspace.root, workspace.config.outDir));
|
|
8530
|
+
const lines = [
|
|
8531
|
+
`Wrote ${written}/`,
|
|
8532
|
+
`Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
|
|
8533
|
+
`Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
|
|
8534
|
+
`Fingerprint: ${manifest.fingerprint}`
|
|
8535
|
+
];
|
|
8536
|
+
if (manifest.warnings.length > 0) {
|
|
8537
|
+
lines.push(`Warnings (${manifest.warnings.length}):`);
|
|
8538
|
+
for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
|
|
8539
|
+
}
|
|
8540
|
+
return {
|
|
8541
|
+
text: lines.join("\n"),
|
|
8542
|
+
structured: {
|
|
8543
|
+
fingerprint: manifest.fingerprint,
|
|
8544
|
+
files: manifest.counts.totalFiles,
|
|
8545
|
+
skipped: manifest.counts.skippedCount,
|
|
8546
|
+
parts: manifest.counts.partsCount,
|
|
8547
|
+
emittedBytes: manifest.totals.totalContentBytes,
|
|
8548
|
+
outDir: written,
|
|
8549
|
+
partFiles: manifest.parts.map((part) => part.filename),
|
|
8550
|
+
warningCount: manifest.warnings.length
|
|
8551
|
+
}
|
|
8552
|
+
};
|
|
8553
|
+
}
|
|
8554
|
+
};
|
|
8555
|
+
var validateTool = {
|
|
8556
|
+
name: "snapshot_validate",
|
|
8557
|
+
title: "Validate the snapshot",
|
|
8558
|
+
description: "Re-read the repository and report whether the snapshot on disk still matches it. Use it before trusting context that came from an earlier session, or after changing the config.",
|
|
8559
|
+
inputSchema: {
|
|
8560
|
+
type: "object",
|
|
8561
|
+
properties: { root: ROOT_PROPERTY },
|
|
8562
|
+
additionalProperties: false
|
|
8563
|
+
},
|
|
8564
|
+
outputSchema: {
|
|
8565
|
+
type: "object",
|
|
8566
|
+
properties: {
|
|
8567
|
+
current: { type: "boolean" },
|
|
8568
|
+
manifestPresent: { type: "boolean" }
|
|
8569
|
+
}
|
|
8570
|
+
},
|
|
8571
|
+
annotations: READ_ONLY,
|
|
8572
|
+
async handler(args, context) {
|
|
8573
|
+
const prepared = await prepare(args, context);
|
|
8574
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8575
|
+
const workspace = prepared.workspace;
|
|
8576
|
+
let result;
|
|
8577
|
+
try {
|
|
8578
|
+
result = validateSnapshot(workspace.config);
|
|
8579
|
+
} catch (error) {
|
|
8580
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8581
|
+
}
|
|
8582
|
+
const lines = [];
|
|
8583
|
+
if (result.valid) {
|
|
8584
|
+
lines.push("The snapshot matches the repository.");
|
|
8585
|
+
} else {
|
|
8586
|
+
lines.push("The snapshot is out of date.");
|
|
8587
|
+
const drift = result.drift;
|
|
8588
|
+
const observations = [
|
|
8589
|
+
["file(s) changed on disk", drift.hashMismatches.length],
|
|
8590
|
+
["file(s) added since the snapshot", drift.missingInSnapshot.length],
|
|
8591
|
+
["file(s) gone since the snapshot", drift.missingOnDisk.length],
|
|
8592
|
+
["part file(s) damaged or missing", drift.partIssues.length],
|
|
8593
|
+
["setting(s) changed since the snapshot", drift.configMismatches.length],
|
|
8594
|
+
["internal count(s) disagreeing", drift.totalsMismatches.length]
|
|
8595
|
+
];
|
|
8596
|
+
for (const [label, count] of observations) {
|
|
8597
|
+
if (count > 0) lines.push(` - ${count} ${label}`);
|
|
8598
|
+
}
|
|
8599
|
+
for (const error of result.errors.slice(0, 5)) lines.push(` - ${error}`);
|
|
8600
|
+
lines.push("Run snapshot_generate to refresh it.");
|
|
8601
|
+
}
|
|
8602
|
+
for (const warning of result.warnings.slice(0, 5)) lines.push(`Note: ${warning}`);
|
|
8603
|
+
return {
|
|
8604
|
+
text: lines.join("\n"),
|
|
8605
|
+
structured: {
|
|
8606
|
+
current: result.valid,
|
|
8607
|
+
manifestPresent: result.manifestPresent,
|
|
8608
|
+
drift: summarizeDrift(result.drift),
|
|
8609
|
+
errors: result.errors.slice(0, 10),
|
|
8610
|
+
warnings: result.warnings.slice(0, 10)
|
|
8611
|
+
}
|
|
8612
|
+
// A stale snapshot is an answer, not a tool failure: the caller asked whether it is current.
|
|
8613
|
+
};
|
|
8614
|
+
}
|
|
8615
|
+
};
|
|
8616
|
+
var locateTool = {
|
|
8617
|
+
name: "snapshot_locate",
|
|
8618
|
+
title: "Locate code in the snapshot",
|
|
8619
|
+
description: "Find which part holds the code you need. Matches file paths, exported symbols, component names and routes from the snapshot's index, so one part can be read instead of all of them.",
|
|
8620
|
+
inputSchema: {
|
|
8621
|
+
type: "object",
|
|
8622
|
+
properties: {
|
|
8623
|
+
query: {
|
|
8624
|
+
type: "string",
|
|
8625
|
+
description: "Text to match, case-insensitively, against paths and exported symbols."
|
|
8626
|
+
},
|
|
8627
|
+
by: {
|
|
8628
|
+
type: "string",
|
|
8629
|
+
enum: ["auto", "path", "symbol", "route", "layer", "kind"],
|
|
8630
|
+
default: "auto",
|
|
8631
|
+
description: "Restrict what is matched. Defaults to every category."
|
|
8632
|
+
},
|
|
8633
|
+
limit: {
|
|
8634
|
+
type: "integer",
|
|
8635
|
+
minimum: 1,
|
|
8636
|
+
maximum: 100,
|
|
8637
|
+
default: 20,
|
|
8638
|
+
description: "Maximum matches to return."
|
|
8639
|
+
},
|
|
8640
|
+
root: ROOT_PROPERTY
|
|
8641
|
+
},
|
|
8642
|
+
required: ["query"],
|
|
8643
|
+
additionalProperties: false
|
|
8644
|
+
},
|
|
8645
|
+
outputSchema: {
|
|
8646
|
+
type: "object",
|
|
8647
|
+
properties: { total: { type: "integer" } }
|
|
8648
|
+
},
|
|
8649
|
+
annotations: READ_ONLY,
|
|
8650
|
+
async handler(args, context) {
|
|
8651
|
+
const prepared = await prepare(args, context);
|
|
8652
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8653
|
+
const workspace = prepared.workspace;
|
|
8654
|
+
const read = readManifest3(workspace);
|
|
8655
|
+
if (!read.ok) return failure(read.message);
|
|
8656
|
+
const query = String(args.query ?? "").trim();
|
|
8657
|
+
if (query.length === 0) return failure('"query" must not be empty.');
|
|
8658
|
+
const needle = query.toLowerCase();
|
|
8659
|
+
const by = typeof args.by === "string" ? args.by : "auto";
|
|
8660
|
+
const limit = typeof args.limit === "number" ? args.limit : 20;
|
|
8661
|
+
const manifest = read.manifest;
|
|
8662
|
+
const matches = [];
|
|
8663
|
+
const add = (entry, matchedBy, detail) => {
|
|
8664
|
+
matches.push({
|
|
8665
|
+
path: entry.relativePath,
|
|
8666
|
+
part: entry.part,
|
|
8667
|
+
layer: entry.layer,
|
|
8668
|
+
kind: entry.kind,
|
|
8669
|
+
matchedBy,
|
|
8670
|
+
...detail ? { detail } : {}
|
|
8671
|
+
});
|
|
8672
|
+
};
|
|
8673
|
+
const wants = (category) => by === "auto" || by === category;
|
|
8674
|
+
for (const entry of manifest.files) {
|
|
8675
|
+
if (wants("path") && entry.relativePath.toLowerCase().includes(needle)) {
|
|
8676
|
+
add(entry, "path");
|
|
8677
|
+
continue;
|
|
8678
|
+
}
|
|
8679
|
+
if (wants("symbol")) {
|
|
8680
|
+
const symbol = symbolsOf(entry).find((name) => name.toLowerCase().includes(needle));
|
|
8681
|
+
if (symbol) {
|
|
8682
|
+
add(entry, "symbol", symbol);
|
|
8683
|
+
continue;
|
|
8684
|
+
}
|
|
8685
|
+
}
|
|
8686
|
+
if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
|
|
8687
|
+
else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
|
|
8688
|
+
}
|
|
8689
|
+
if (wants("route")) {
|
|
8690
|
+
for (const route of manifest.indexes.routes) {
|
|
8691
|
+
if (!route.path.toLowerCase().includes(needle)) continue;
|
|
8692
|
+
if (matches.some((match) => match.path === route.path)) continue;
|
|
8693
|
+
matches.push({
|
|
8694
|
+
path: route.path,
|
|
8695
|
+
part: route.part,
|
|
8696
|
+
layer: "route",
|
|
8697
|
+
kind: "route",
|
|
8698
|
+
matchedBy: "route",
|
|
8699
|
+
detail: route.methods.join(", ")
|
|
8700
|
+
});
|
|
8701
|
+
}
|
|
8702
|
+
}
|
|
8703
|
+
if (matches.length === 0) {
|
|
8704
|
+
return {
|
|
8705
|
+
text: `Nothing in the snapshot matches "${query}". Try a shorter fragment, or snapshot_explain a path to see whether it is included at all.`,
|
|
8706
|
+
structured: { total: 0, matches: [] }
|
|
8707
|
+
};
|
|
8708
|
+
}
|
|
8709
|
+
const shown = matches.slice(0, limit);
|
|
8710
|
+
const lines = [
|
|
8711
|
+
`${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"` + (matches.length > shown.length ? ` (showing ${shown.length})` : "") + ":"
|
|
8712
|
+
];
|
|
8713
|
+
for (const match of shown) {
|
|
8714
|
+
const detail = match.detail ? ` \u2014 ${match.detail}` : "";
|
|
8715
|
+
lines.push(` ${match.path}${detail} [${match.part}, ${match.layer}/${match.kind}]`);
|
|
8716
|
+
}
|
|
8717
|
+
if (matches.length > shown.length) {
|
|
8718
|
+
lines.push("Raise `limit` to see more.");
|
|
8719
|
+
}
|
|
8720
|
+
return {
|
|
8721
|
+
text: lines.join("\n"),
|
|
8722
|
+
structured: { total: matches.length, returned: shown.length, matches: shown }
|
|
8723
|
+
};
|
|
8724
|
+
}
|
|
8725
|
+
};
|
|
8726
|
+
var readTool = {
|
|
8727
|
+
name: "snapshot_read",
|
|
8728
|
+
title: "Read a file from the snapshot",
|
|
8729
|
+
description: "Return a line window of one file as the snapshot contains it: redacted, and bounded to a preview when the source is large. Reports the next `fromLine` when more remains.",
|
|
8730
|
+
inputSchema: {
|
|
8731
|
+
type: "object",
|
|
8732
|
+
properties: {
|
|
8733
|
+
path: {
|
|
8734
|
+
type: "string",
|
|
8735
|
+
description: "File path relative to the repository root, as snapshot_locate reports it."
|
|
8736
|
+
},
|
|
8737
|
+
fromLine: {
|
|
8738
|
+
type: "integer",
|
|
8739
|
+
minimum: 1,
|
|
8740
|
+
default: 1,
|
|
8741
|
+
description: "First line to return, 1-based."
|
|
8742
|
+
},
|
|
8743
|
+
lines: {
|
|
8744
|
+
type: "integer",
|
|
8745
|
+
minimum: 1,
|
|
8746
|
+
maximum: 2e3,
|
|
8747
|
+
default: 200,
|
|
8748
|
+
description: "How many lines to return."
|
|
8749
|
+
},
|
|
8750
|
+
root: ROOT_PROPERTY
|
|
8751
|
+
},
|
|
8752
|
+
required: ["path"],
|
|
8753
|
+
additionalProperties: false
|
|
8754
|
+
},
|
|
8755
|
+
outputSchema: {
|
|
8756
|
+
type: "object",
|
|
8757
|
+
properties: {
|
|
8758
|
+
path: { type: "string" },
|
|
8759
|
+
part: { type: "string" },
|
|
8760
|
+
fromLine: { type: "integer" },
|
|
8761
|
+
returnedLines: { type: "integer" },
|
|
8762
|
+
totalLines: { type: "integer" },
|
|
8763
|
+
nextFromLine: { type: "integer" }
|
|
8764
|
+
}
|
|
8765
|
+
},
|
|
8766
|
+
annotations: READ_ONLY,
|
|
8767
|
+
async handler(args, context) {
|
|
8768
|
+
const prepared = await prepare(args, context);
|
|
8769
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8770
|
+
const workspace = prepared.workspace;
|
|
8771
|
+
if (typeof args.path !== "string") return failure('"path" must be a string.');
|
|
8772
|
+
const relativePath = toRelativePath(workspace.root, args.path);
|
|
8773
|
+
if (!relativePath) {
|
|
8774
|
+
return failure(
|
|
8775
|
+
`"${args.path}" is outside ${workspace.root}. Paths are read relative to the repository root.`
|
|
8776
|
+
);
|
|
8777
|
+
}
|
|
8778
|
+
const read = readManifest3(workspace);
|
|
8779
|
+
if (!read.ok) return failure(read.message);
|
|
8780
|
+
const manifest = read.manifest;
|
|
8781
|
+
const entry = manifest.files.find((file) => file.relativePath === relativePath) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === relativePath.toLowerCase());
|
|
8782
|
+
if (!entry) {
|
|
8783
|
+
const skipped = manifest.skipped.find((record) => record.path === relativePath);
|
|
8784
|
+
if (skipped) {
|
|
8785
|
+
const detail = skipped.detail ? ` (${skipped.detail})` : "";
|
|
8786
|
+
return failure(
|
|
8787
|
+
`${relativePath} is not in the snapshot: ${skipped.reason}${detail}. Sensitive and ignored files stay out unless they are included explicitly.`
|
|
8788
|
+
);
|
|
8789
|
+
}
|
|
8790
|
+
return failure(
|
|
8791
|
+
`${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
|
|
8792
|
+
);
|
|
8793
|
+
}
|
|
8794
|
+
const partPath = path14.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
|
|
8795
|
+
let partText;
|
|
8796
|
+
try {
|
|
8797
|
+
partText = fs13.readFileSync(partPath, "utf8");
|
|
8798
|
+
} catch {
|
|
8799
|
+
return failure(
|
|
8800
|
+
`${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
|
|
8801
|
+
);
|
|
8802
|
+
}
|
|
8803
|
+
const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
|
|
8804
|
+
if (!section) {
|
|
8805
|
+
return failure(
|
|
8806
|
+
`${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
|
|
8807
|
+
);
|
|
8808
|
+
}
|
|
8809
|
+
const fromLine = typeof args.fromLine === "number" ? args.fromLine : 1;
|
|
8810
|
+
const lineCount = typeof args.lines === "number" ? args.lines : 200;
|
|
8811
|
+
const window = windowText(section.body, fromLine, lineCount);
|
|
8812
|
+
let body = window.text;
|
|
8813
|
+
let truncatedByBytes = false;
|
|
8814
|
+
if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
|
|
8815
|
+
body = body.slice(0, MAX_RESULT_BYTES);
|
|
8816
|
+
const lastBreak = body.lastIndexOf("\n");
|
|
8817
|
+
body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
|
|
8818
|
+
truncatedByBytes = true;
|
|
8819
|
+
}
|
|
8820
|
+
const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
|
|
8821
|
+
const moreRemain = window.truncated || truncatedByBytes;
|
|
8822
|
+
const header = [
|
|
8823
|
+
`${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
|
|
8824
|
+
section.inclusion === "preview" ? "Bounded preview: this source file is large, so the snapshot holds only its head and tail." : `Lines ${window.fromLine}\u2013${window.fromLine + returnedLines - 1} of ${window.totalLines}.`
|
|
8825
|
+
];
|
|
8826
|
+
const lines = [...header, "", body];
|
|
8827
|
+
if (moreRemain) {
|
|
8828
|
+
const next = window.fromLine + returnedLines;
|
|
8829
|
+
lines.push("", `Truncated. Call snapshot_read with fromLine=${next} for the rest.`);
|
|
8830
|
+
}
|
|
8831
|
+
return {
|
|
8832
|
+
text: lines.join("\n"),
|
|
8833
|
+
structured: {
|
|
8834
|
+
path: entry.relativePath,
|
|
8835
|
+
part: entry.part,
|
|
8836
|
+
layer: entry.layer,
|
|
8837
|
+
kind: entry.kind,
|
|
8838
|
+
language: entry.language,
|
|
8839
|
+
inclusion: section.inclusion ?? entry.inclusionMode,
|
|
8840
|
+
fromLine: window.fromLine,
|
|
8841
|
+
returnedLines,
|
|
8842
|
+
totalLines: window.totalLines,
|
|
8843
|
+
truncated: moreRemain,
|
|
8844
|
+
nextFromLine: moreRemain ? window.fromLine + returnedLines : null,
|
|
8845
|
+
sha256: entry.hashSha256
|
|
8846
|
+
}
|
|
8847
|
+
};
|
|
8848
|
+
}
|
|
8849
|
+
};
|
|
8850
|
+
var explainTool = {
|
|
8851
|
+
name: "snapshot_explain",
|
|
8852
|
+
title: "Explain one path",
|
|
8853
|
+
description: 'Explain how the next snapshot would treat one path: its layer, kind, language and part, or the exact reason it is excluded. Answers "why is this file missing".',
|
|
8854
|
+
inputSchema: {
|
|
8855
|
+
type: "object",
|
|
8856
|
+
properties: {
|
|
8857
|
+
path: {
|
|
8858
|
+
type: "string",
|
|
8859
|
+
description: "Path relative to the repository root. Directories are explained as a whole."
|
|
8860
|
+
},
|
|
8861
|
+
root: ROOT_PROPERTY
|
|
8862
|
+
},
|
|
8863
|
+
required: ["path"],
|
|
8864
|
+
additionalProperties: false
|
|
8865
|
+
},
|
|
8866
|
+
outputSchema: {
|
|
8867
|
+
type: "object",
|
|
8868
|
+
properties: {
|
|
8869
|
+
path: { type: "string" },
|
|
8870
|
+
included: { type: "boolean" },
|
|
8871
|
+
reason: { type: "string" }
|
|
8872
|
+
}
|
|
8873
|
+
},
|
|
8874
|
+
annotations: READ_ONLY,
|
|
8875
|
+
async handler(args, context) {
|
|
8876
|
+
const prepared = await prepare(args, context);
|
|
8877
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8878
|
+
const workspace = prepared.workspace;
|
|
8879
|
+
if (typeof args.path !== "string") return failure('"path" must be a string.');
|
|
8880
|
+
const relativePath = toRelativePath(workspace.root, args.path);
|
|
8881
|
+
if (!relativePath) {
|
|
8882
|
+
return failure(`"${args.path}" is outside ${workspace.root}.`);
|
|
8883
|
+
}
|
|
8884
|
+
let result;
|
|
8885
|
+
try {
|
|
8886
|
+
result = explainPath(workspace.config, relativePath);
|
|
8887
|
+
} catch (error) {
|
|
8888
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8889
|
+
}
|
|
8890
|
+
return {
|
|
8891
|
+
text: renderExplainText(result),
|
|
8892
|
+
structured: {
|
|
8893
|
+
path: result.path,
|
|
8894
|
+
included: result.included,
|
|
8895
|
+
...result.reason ? { reason: result.reason } : {},
|
|
8896
|
+
...result.detail ? { detail: result.detail } : {},
|
|
8897
|
+
...result.layer ? { layer: result.layer } : {},
|
|
8898
|
+
...result.kind ? { kind: result.kind } : {},
|
|
8899
|
+
...result.language ? { language: result.language } : {},
|
|
8900
|
+
...result.part ? { part: result.part } : {},
|
|
8901
|
+
...result.inclusionMode ? { inclusion: result.inclusionMode } : {},
|
|
8902
|
+
...typeof result.lines === "number" ? { lines: result.lines } : {},
|
|
8903
|
+
...typeof result.bytes === "number" ? { bytes: result.bytes } : {},
|
|
8904
|
+
...typeof result.secretCount === "number" ? { secretCount: result.secretCount } : {}
|
|
8905
|
+
}
|
|
8906
|
+
};
|
|
8907
|
+
}
|
|
8908
|
+
};
|
|
8909
|
+
function buildTools() {
|
|
8910
|
+
return [
|
|
8911
|
+
statusTool,
|
|
8912
|
+
planTool,
|
|
8913
|
+
generateTool,
|
|
8914
|
+
validateTool,
|
|
8915
|
+
locateTool,
|
|
8916
|
+
readTool,
|
|
8917
|
+
explainTool
|
|
8918
|
+
];
|
|
8919
|
+
}
|
|
8920
|
+
|
|
8921
|
+
// src/mcp/prompts.ts
|
|
8922
|
+
var ORIENT = `You have a codebase-snapshot server for this repository. Build a map of it before reading any code.
|
|
8923
|
+
|
|
8924
|
+
1. Call snapshot_status. If no snapshot exists, call snapshot_plan to preview it, then snapshot_generate.
|
|
8925
|
+
2. Call snapshot_locate for the areas that matter. Search by exported symbol, route or path fragment rather than guessing filenames.
|
|
8926
|
+
3. Report back: the layers present, the entry points, and the files that matter here \u2014 cited as paths with their part.
|
|
8927
|
+
4. Read one file at a time with snapshot_read, one window at a time. Do not read a part end to end.`;
|
|
8928
|
+
var KEEP_CURRENT = `Keep the context you were given provably current while you work.
|
|
8929
|
+
|
|
8930
|
+
1. Before relying on a snapshot from an earlier session, call snapshot_validate.
|
|
8931
|
+
2. If it reports drift, call snapshot_generate once, then snapshot_validate again.
|
|
8932
|
+
3. After changes you make, call snapshot_validate and report what it said.
|
|
8933
|
+
4. Never call a snapshot current unless snapshot_validate said so in this session.`;
|
|
8934
|
+
function withFocus(base, focus) {
|
|
8935
|
+
return typeof focus === "string" && focus.trim().length > 0 ? `${base}
|
|
8936
|
+
|
|
8937
|
+
Focus for this pass: ${focus.trim()}` : base;
|
|
8938
|
+
}
|
|
8939
|
+
function buildPrompts() {
|
|
8940
|
+
return [
|
|
8941
|
+
{
|
|
8942
|
+
name: "orient",
|
|
8943
|
+
title: "Orient in this repository",
|
|
8944
|
+
description: "Map the repository from the snapshot before reading any of it.",
|
|
8945
|
+
arguments: [
|
|
8946
|
+
{
|
|
8947
|
+
name: "focus",
|
|
8948
|
+
description: 'Optional area to concentrate on, for example "authentication".',
|
|
8949
|
+
required: false
|
|
8950
|
+
}
|
|
8951
|
+
],
|
|
8952
|
+
build(args) {
|
|
8953
|
+
return {
|
|
8954
|
+
description: "Map the repository before reading it.",
|
|
8955
|
+
messages: [{ role: "user", content: { type: "text", text: withFocus(ORIENT, args.focus) } }]
|
|
8956
|
+
};
|
|
8957
|
+
}
|
|
8958
|
+
},
|
|
8959
|
+
{
|
|
8960
|
+
name: "keep-current",
|
|
8961
|
+
title: "Keep the snapshot current",
|
|
8962
|
+
description: "Validate before trusting earlier context, and again after making changes.",
|
|
8963
|
+
arguments: [],
|
|
8964
|
+
build() {
|
|
8965
|
+
return {
|
|
8966
|
+
description: "Keep the context current.",
|
|
8967
|
+
messages: [{ role: "user", content: { type: "text", text: KEEP_CURRENT } }]
|
|
8968
|
+
};
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
];
|
|
8972
|
+
}
|
|
8973
|
+
|
|
8974
|
+
// src/mcp/protocol.ts
|
|
8975
|
+
var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
|
|
8976
|
+
var MODERN_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
8977
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
8978
|
+
...MODERN_PROTOCOL_VERSIONS,
|
|
8979
|
+
...LEGACY_PROTOCOL_VERSIONS
|
|
8980
|
+
];
|
|
8981
|
+
var DEFAULT_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
|
|
8982
|
+
var ERROR_CODES = {
|
|
8983
|
+
parseError: -32700,
|
|
8984
|
+
invalidRequest: -32600,
|
|
8985
|
+
methodNotFound: -32601,
|
|
8986
|
+
invalidParams: -32602,
|
|
8987
|
+
internalError: -32603,
|
|
8988
|
+
unsupportedProtocolVersion: -32022
|
|
8989
|
+
};
|
|
8990
|
+
var METHOD3 = {
|
|
8991
|
+
initialize: "initialize",
|
|
8992
|
+
initialized: "notifications/initialized",
|
|
8993
|
+
discover: "server/discover",
|
|
8994
|
+
ping: "ping",
|
|
8995
|
+
toolsList: "tools/list",
|
|
8996
|
+
toolsCall: "tools/call",
|
|
8997
|
+
resourcesList: "resources/list",
|
|
8998
|
+
resourcesRead: "resources/read",
|
|
8999
|
+
resourceTemplatesList: "resources/templates/list",
|
|
9000
|
+
promptsList: "prompts/list",
|
|
9001
|
+
promptsGet: "prompts/get",
|
|
9002
|
+
cancelled: "notifications/cancelled"
|
|
9003
|
+
};
|
|
9004
|
+
var META_KEYS = {
|
|
9005
|
+
protocolVersion: "io.modelcontextprotocol/protocolVersion",
|
|
9006
|
+
clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
|
|
9007
|
+
clientInfo: "io.modelcontextprotocol/clientInfo",
|
|
9008
|
+
serverInfo: "io.modelcontextprotocol/serverInfo",
|
|
9009
|
+
progressToken: "progressToken"
|
|
9010
|
+
};
|
|
9011
|
+
function isJsonObject(value) {
|
|
9012
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9013
|
+
}
|
|
9014
|
+
function serverInfoMeta(info2) {
|
|
9015
|
+
const value = { name: info2.name, version: info2.version };
|
|
9016
|
+
if (info2.title) value.title = info2.title;
|
|
9017
|
+
return { [META_KEYS.serverInfo]: value };
|
|
9018
|
+
}
|
|
9019
|
+
function successResponse(id, result, modern, info2) {
|
|
9020
|
+
if (!modern) return { jsonrpc: "2.0", id, result };
|
|
9021
|
+
const withMeta = {
|
|
9022
|
+
...result,
|
|
9023
|
+
resultType: "complete",
|
|
9024
|
+
_meta: { ...isJsonObject(result._meta) ? result._meta : {}, ...serverInfoMeta(info2) }
|
|
9025
|
+
};
|
|
9026
|
+
return { jsonrpc: "2.0", id, result: withMeta };
|
|
9027
|
+
}
|
|
9028
|
+
function errorResponse(id, code, message, data) {
|
|
9029
|
+
const error = { code, message };
|
|
9030
|
+
if (data !== void 0) error.data = data;
|
|
9031
|
+
return { jsonrpc: "2.0", id, error };
|
|
9032
|
+
}
|
|
9033
|
+
|
|
9034
|
+
// src/mcp/uris.ts
|
|
9035
|
+
var STATIC_RESOURCE_URIS = [
|
|
9036
|
+
"snapshot://manifest",
|
|
9037
|
+
"snapshot://contract",
|
|
9038
|
+
"snapshot://skill",
|
|
9039
|
+
"snapshot://reports/audit",
|
|
9040
|
+
"snapshot://reports/tree",
|
|
9041
|
+
"snapshot://reports/navigation"
|
|
9042
|
+
];
|
|
9043
|
+
var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
|
|
9044
|
+
|
|
9045
|
+
// src/agent/option-scope.ts
|
|
9046
|
+
var VALIDATE_OPTIONS = [
|
|
9047
|
+
"cwd",
|
|
9048
|
+
"out-dir",
|
|
9049
|
+
"max-part-bytes",
|
|
9050
|
+
"large-file-bytes",
|
|
9051
|
+
"max-monolith-bytes",
|
|
9052
|
+
"include-monolith",
|
|
9053
|
+
"include-sensitive-files",
|
|
9054
|
+
"skip-artifacts",
|
|
9055
|
+
"only-artifacts",
|
|
9056
|
+
"no-config",
|
|
9057
|
+
"discovery",
|
|
9058
|
+
"format",
|
|
9059
|
+
"mode",
|
|
9060
|
+
"help",
|
|
9061
|
+
"version"
|
|
9062
|
+
];
|
|
9063
|
+
var COMMAND_OPTION_SCOPE = {
|
|
9064
|
+
generate: [
|
|
9065
|
+
"cwd",
|
|
9066
|
+
"out-dir",
|
|
9067
|
+
"max-part-bytes",
|
|
9068
|
+
"large-file-bytes",
|
|
9069
|
+
"max-monolith-bytes",
|
|
9070
|
+
"include-monolith",
|
|
9071
|
+
"include-sensitive-files",
|
|
9072
|
+
"skip-artifacts",
|
|
9073
|
+
"only-artifacts",
|
|
9074
|
+
"no-config",
|
|
9075
|
+
"discovery",
|
|
9076
|
+
"format",
|
|
9077
|
+
"mode",
|
|
9078
|
+
"dry-run",
|
|
9079
|
+
"watch",
|
|
9080
|
+
"autopilot",
|
|
9081
|
+
"no-install",
|
|
9082
|
+
"help",
|
|
9083
|
+
"version"
|
|
9084
|
+
],
|
|
9085
|
+
validate: VALIDATE_OPTIONS,
|
|
9086
|
+
report: [
|
|
9087
|
+
"cwd",
|
|
9088
|
+
"out-dir",
|
|
9089
|
+
"max-part-bytes",
|
|
9090
|
+
"large-file-bytes",
|
|
9091
|
+
"max-monolith-bytes",
|
|
9092
|
+
"include-monolith",
|
|
9093
|
+
"include-sensitive-files",
|
|
9094
|
+
"skip-artifacts",
|
|
9095
|
+
"only-artifacts",
|
|
9096
|
+
"no-config",
|
|
9097
|
+
"format",
|
|
9098
|
+
"mode",
|
|
9099
|
+
"help",
|
|
9100
|
+
"version"
|
|
9101
|
+
],
|
|
9102
|
+
explain: [
|
|
9103
|
+
"cwd",
|
|
9104
|
+
"out-dir",
|
|
9105
|
+
"max-part-bytes",
|
|
9106
|
+
"large-file-bytes",
|
|
9107
|
+
"max-monolith-bytes",
|
|
9108
|
+
"include-monolith",
|
|
9109
|
+
"include-sensitive-files",
|
|
9110
|
+
"skip-artifacts",
|
|
9111
|
+
"only-artifacts",
|
|
9112
|
+
"no-config",
|
|
9113
|
+
"discovery",
|
|
9114
|
+
"format",
|
|
9115
|
+
"mode",
|
|
9116
|
+
"help",
|
|
9117
|
+
"version"
|
|
9118
|
+
],
|
|
9119
|
+
diff: ["format", "mode", "no-config", "help", "version"],
|
|
9120
|
+
describe: ["format", "mode", "help", "version"],
|
|
9121
|
+
init: [
|
|
9122
|
+
"cwd",
|
|
9123
|
+
"out-dir",
|
|
9124
|
+
"max-part-bytes",
|
|
9125
|
+
"include-sensitive-files",
|
|
9126
|
+
"skip-artifacts",
|
|
9127
|
+
"only-artifacts",
|
|
9128
|
+
"preset",
|
|
9129
|
+
"include",
|
|
9130
|
+
"exclude",
|
|
9131
|
+
"yes",
|
|
9132
|
+
"force",
|
|
9133
|
+
"dry-run",
|
|
9134
|
+
"format",
|
|
9135
|
+
"mode",
|
|
9136
|
+
"help",
|
|
9137
|
+
"version"
|
|
9138
|
+
],
|
|
9139
|
+
skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
|
|
9140
|
+
// `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
|
|
9141
|
+
// messages, and there is no second shape to choose, so offering the flag would be a lie.
|
|
9142
|
+
mcp: ["cwd", "mode", "help", "version"],
|
|
9143
|
+
setup: [
|
|
9144
|
+
"cwd",
|
|
9145
|
+
"out-dir",
|
|
9146
|
+
"max-part-bytes",
|
|
9147
|
+
"include-sensitive-files",
|
|
9148
|
+
"skip-artifacts",
|
|
9149
|
+
"only-artifacts",
|
|
9150
|
+
"preset",
|
|
9151
|
+
"include",
|
|
9152
|
+
"exclude",
|
|
9153
|
+
"yes",
|
|
9154
|
+
"force",
|
|
9155
|
+
"no-install",
|
|
9156
|
+
"dry-run",
|
|
9157
|
+
"format",
|
|
9158
|
+
"mode",
|
|
9159
|
+
"help",
|
|
9160
|
+
"version"
|
|
9161
|
+
]
|
|
9162
|
+
};
|
|
9163
|
+
var COMMAND_ORDER = [
|
|
9164
|
+
"generate",
|
|
9165
|
+
"validate",
|
|
9166
|
+
"report",
|
|
9167
|
+
"explain",
|
|
9168
|
+
"init",
|
|
9169
|
+
"setup",
|
|
9170
|
+
"diff",
|
|
9171
|
+
"describe",
|
|
9172
|
+
"skill",
|
|
9173
|
+
"mcp"
|
|
9174
|
+
];
|
|
9175
|
+
function commandsForOption(option) {
|
|
9176
|
+
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
9177
|
+
}
|
|
9178
|
+
function optionAppliesTo(option) {
|
|
9179
|
+
return commandsForOption(option).join("/");
|
|
9180
|
+
}
|
|
9181
|
+
|
|
9182
|
+
// src/agent/contract.ts
|
|
9183
|
+
var CONTRACT_VERSION = "1.0.0";
|
|
9184
|
+
var PACKAGE_NAME = "codebase-snapshot";
|
|
9185
|
+
var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
|
|
9186
|
+
var ARTIFACT_SUMMARY = {
|
|
9187
|
+
readme: "README.md describing the snapshot at the output root.",
|
|
9188
|
+
manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
|
|
9189
|
+
parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
|
|
9190
|
+
navigation: "reports/navigation.md: links between parts and layers.",
|
|
9191
|
+
projectTree: "reports/project-tree.md: the directory tree of included files.",
|
|
9192
|
+
audit: "reports/audit.md: secret findings, large files, parse diagnostics, git state.",
|
|
9193
|
+
monolith: "full-codebase.json and full-codebase.md: the whole repository in one file (off by default)."
|
|
9194
|
+
};
|
|
9195
|
+
var ARTIFACT_DEFAULTS = {
|
|
9196
|
+
readme: true,
|
|
9197
|
+
manifest: true,
|
|
9198
|
+
parts: true,
|
|
9199
|
+
navigation: true,
|
|
9200
|
+
projectTree: true,
|
|
9201
|
+
audit: true,
|
|
9202
|
+
monolith: false
|
|
9203
|
+
};
|
|
9204
|
+
function renderContractText(contract) {
|
|
9205
|
+
const lines = [];
|
|
9206
|
+
lines.push(`${contract.package} v${contract.version}`);
|
|
9207
|
+
lines.push(contract.summary);
|
|
9208
|
+
lines.push("");
|
|
9209
|
+
lines.push(`Node : ${contract.requires.node}`);
|
|
9210
|
+
lines.push(`Schema : ${contract.schemaVersion} (contract ${contract.contractVersion})`);
|
|
9211
|
+
lines.push(`One-shot : ${contract.invocation.oneShot} <command>`);
|
|
9212
|
+
lines.push(`Installed : ${contract.invocation.installed} <command>`);
|
|
9213
|
+
lines.push("");
|
|
9214
|
+
lines.push("Workflow:");
|
|
9215
|
+
for (const step of contract.workflow) {
|
|
9216
|
+
lines.push(` ${step.step}. ${step.id}`);
|
|
9217
|
+
lines.push(` run : ${step.run}`);
|
|
9218
|
+
lines.push(` expect : ${step.expect}`);
|
|
9219
|
+
if (step.notes) lines.push(` note : ${step.notes}`);
|
|
9220
|
+
}
|
|
9221
|
+
lines.push("");
|
|
8086
9222
|
lines.push("Commands:");
|
|
8087
9223
|
for (const command of contract.commands) {
|
|
8088
9224
|
const args = command.positionals.length > 0 ? ` ${command.positionals.join(" ")}` : "";
|
|
@@ -8108,6 +9244,19 @@ function renderContractText(contract) {
|
|
|
8108
9244
|
lines.push(` ${"".padEnd(24)} ${option.summary}`);
|
|
8109
9245
|
}
|
|
8110
9246
|
lines.push("");
|
|
9247
|
+
lines.push("MCP:");
|
|
9248
|
+
lines.push(` command ${contract.mcp.command}`);
|
|
9249
|
+
lines.push(` transport ${contract.mcp.transport}`);
|
|
9250
|
+
lines.push(
|
|
9251
|
+
` versions modern ${contract.mcp.protocolVersions.modern.join(", ")} / legacy ${contract.mcp.protocolVersions.legacy.join(", ")}`
|
|
9252
|
+
);
|
|
9253
|
+
lines.push(
|
|
9254
|
+
` tools ${contract.mcp.tools.map((tool) => tool.readOnly ? tool.name : `${tool.name} (writes)`).join(", ")}`
|
|
9255
|
+
);
|
|
9256
|
+
lines.push(` prompts ${contract.mcp.prompts.map((prompt) => prompt.name).join(", ")}`);
|
|
9257
|
+
lines.push(` resources ${contract.mcp.resources.join(", ")}`);
|
|
9258
|
+
lines.push(` templates ${contract.mcp.resourceTemplates.join(", ")}`);
|
|
9259
|
+
lines.push("");
|
|
8111
9260
|
lines.push("Safety:");
|
|
8112
9261
|
for (const note of contract.safety) lines.push(` - ${note}`);
|
|
8113
9262
|
lines.push("");
|
|
@@ -8133,11 +9282,13 @@ function buildAgentContract() {
|
|
|
8133
9282
|
invocation: {
|
|
8134
9283
|
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
8135
9284
|
installed: "npx codebase-snapshot",
|
|
8136
|
-
bins: ["codebase-snapshot", "
|
|
9285
|
+
bins: ["codebase-snapshot", "snapshot"],
|
|
8137
9286
|
notes: [
|
|
8138
9287
|
`The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
|
|
8139
|
-
"
|
|
9288
|
+
"`codebase-snapshot` is the canonical name and the one to script against. `snapshot` is the short alias a global install provides; in a project that has the package installed, `npx snapshot` resolves the local bin first.",
|
|
9289
|
+
`Pin an exact version \u2014 for example \`npx ${PACKAGE_NAME}@${GENERATOR_VERSION} --version\` \u2014 when you need the same build on every machine. A major range is not enough before 1.0: npm's caret on a 0.x version pins the minor, and 0.x minors are breaking by convention.`,
|
|
8140
9290
|
"Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
|
|
9291
|
+
"A bare run in a project with no configuration asks once, sets the project up and generates \u2014 but only at a terminal. With no TTY it exits 2 and names the alternatives, so an unattended caller must say what it wants: `generate` after `init --yes`, or `--autopilot`.",
|
|
8141
9292
|
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON.",
|
|
8142
9293
|
"A failing run also writes pure JSON to stdout: { error: { message, exitCode } }. The exit code and the stderr line are unchanged."
|
|
8143
9294
|
]
|
|
@@ -8192,9 +9343,9 @@ function buildAgentContract() {
|
|
|
8192
9343
|
{
|
|
8193
9344
|
name: "generate",
|
|
8194
9345
|
aliases: ["default"],
|
|
8195
|
-
summary: "Write the snapshot. This is the command you normally want.",
|
|
9346
|
+
summary: "Write the snapshot. This is the command you normally want. In a project with no configuration, a terminal asks once and sets the project up first; without a terminal it exits 2 and names `--autopilot` and `init --yes`.",
|
|
8196
9347
|
positionals: [],
|
|
8197
|
-
exits: [0, 1]
|
|
9348
|
+
exits: [0, 1, 2]
|
|
8198
9349
|
},
|
|
8199
9350
|
{
|
|
8200
9351
|
name: "validate",
|
|
@@ -8251,6 +9402,13 @@ function buildAgentContract() {
|
|
|
8251
9402
|
summary: "Print the agent skill, or --install it into the tool registries this repository's agent reads (.claude/skills, .cursor/rules). Files it creates are added to .gitignore. It never writes AGENTS.md: that file belongs to the repository.",
|
|
8252
9403
|
positionals: [],
|
|
8253
9404
|
exits: [0, 1, 2]
|
|
9405
|
+
},
|
|
9406
|
+
{
|
|
9407
|
+
name: "mcp",
|
|
9408
|
+
aliases: [],
|
|
9409
|
+
summary: "Serve this repository to an MCP client over stdio. The tools, resources and protocol versions it offers are listed in this contract's `mcp` block.",
|
|
9410
|
+
positionals: [],
|
|
9411
|
+
exits: [0, 2]
|
|
8254
9412
|
}
|
|
8255
9413
|
],
|
|
8256
9414
|
exitCodes: {
|
|
@@ -8401,6 +9559,13 @@ function buildAgentContract() {
|
|
|
8401
9559
|
appliesTo: optionAppliesTo("force"),
|
|
8402
9560
|
summary: "Overwrite an existing config file."
|
|
8403
9561
|
},
|
|
9562
|
+
{
|
|
9563
|
+
flag: "--autopilot",
|
|
9564
|
+
type: "boolean",
|
|
9565
|
+
default: false,
|
|
9566
|
+
appliesTo: optionAppliesTo("autopilot"),
|
|
9567
|
+
summary: "Set the project up without asking, then generate: detect the preset, write the config and .gitignore, add the npm scripts and devDependency, install with the project's own package manager, generate. Never overwrites an existing config, and never prompts. Refuses an empty directory (exit 1); contradicts --no-config (exit 2)."
|
|
9568
|
+
},
|
|
8404
9569
|
{
|
|
8405
9570
|
flag: "--install",
|
|
8406
9571
|
type: "boolean",
|
|
@@ -8420,7 +9585,7 @@ function buildAgentContract() {
|
|
|
8420
9585
|
type: "boolean",
|
|
8421
9586
|
default: false,
|
|
8422
9587
|
appliesTo: optionAppliesTo("no-install"),
|
|
8423
|
-
summary: "
|
|
9588
|
+
summary: "Skip installing the dependency before generating: `setup`, or `generate --autopilot` (CI, offline, or a lockfile you do not want a package manager to touch)."
|
|
8424
9589
|
}
|
|
8425
9590
|
],
|
|
8426
9591
|
presets: PRESETS.map((preset) => ({
|
|
@@ -8492,6 +9657,36 @@ function buildAgentContract() {
|
|
|
8492
9657
|
"The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
|
|
8493
9658
|
"Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
|
|
8494
9659
|
],
|
|
9660
|
+
mcp: {
|
|
9661
|
+
command: `npx ${PACKAGE_NAME} mcp`,
|
|
9662
|
+
transport: "stdio",
|
|
9663
|
+
protocolVersions: {
|
|
9664
|
+
modern: [...MODERN_PROTOCOL_VERSIONS],
|
|
9665
|
+
legacy: [...LEGACY_PROTOCOL_VERSIONS],
|
|
9666
|
+
default: DEFAULT_LEGACY_PROTOCOL_VERSION
|
|
9667
|
+
},
|
|
9668
|
+
tools: buildTools().map((tool) => ({
|
|
9669
|
+
name: tool.name,
|
|
9670
|
+
title: tool.title,
|
|
9671
|
+
summary: tool.description,
|
|
9672
|
+
readOnly: tool.annotations.readOnlyHint,
|
|
9673
|
+
parameters: Object.keys(tool.inputSchema.properties),
|
|
9674
|
+
required: tool.inputSchema.required ?? []
|
|
9675
|
+
})),
|
|
9676
|
+
prompts: buildPrompts().map((prompt) => ({
|
|
9677
|
+
name: prompt.name,
|
|
9678
|
+
summary: prompt.description,
|
|
9679
|
+
arguments: prompt.arguments.map((argument) => argument.name)
|
|
9680
|
+
})),
|
|
9681
|
+
resources: [...STATIC_RESOURCE_URIS],
|
|
9682
|
+
resourceTemplates: [...RESOURCE_TEMPLATE_URIS],
|
|
9683
|
+
notes: [
|
|
9684
|
+
"stdout carries JSON-RPC messages only; every note and log line goes to stderr.",
|
|
9685
|
+
"The server speaks both protocol eras: the legacy `initialize` handshake and the stateless revision, which is detected from `server/discover` or from per-request `_meta`.",
|
|
9686
|
+
"Everything it can do is a subset of the CLI. It never installs packages and never writes a config file.",
|
|
9687
|
+
"A `root` argument may name a sub-directory of the server's --cwd, and nothing outside it is readable."
|
|
9688
|
+
]
|
|
9689
|
+
},
|
|
8495
9690
|
troubleshooting: [
|
|
8496
9691
|
{
|
|
8497
9692
|
symptom: "Command prints nothing and exits 0",
|
|
@@ -8523,8 +9718,8 @@ function buildAgentContract() {
|
|
|
8523
9718
|
}
|
|
8524
9719
|
|
|
8525
9720
|
// src/agent/install-skill.ts
|
|
8526
|
-
import * as
|
|
8527
|
-
import * as
|
|
9721
|
+
import * as fs14 from "fs";
|
|
9722
|
+
import * as path15 from "path";
|
|
8528
9723
|
|
|
8529
9724
|
// src/agent/skill.ts
|
|
8530
9725
|
var PACKAGE_NAME2 = "codebase-snapshot";
|
|
@@ -8553,10 +9748,11 @@ globally, and you do not need to clone anything.
|
|
|
8553
9748
|
|
|
8554
9749
|
**Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${GENERATOR_VERSION} <command>\` \u2014 when you
|
|
8555
9750
|
need the same version on every machine. The bare package name works too: this package ships
|
|
8556
|
-
one executable file, published under two names (\`codebase-snapshot\` and \`
|
|
8557
|
-
lets npm pick it.
|
|
8558
|
-
|
|
8559
|
-
|
|
9751
|
+
one executable file, published under two names (\`codebase-snapshot\` and \`snapshot\`), which is
|
|
9752
|
+
what lets npm pick it. A global install gives the short command \`snapshot\`; inside a project
|
|
9753
|
+
that has this package installed, \`npx snapshot\` resolves the local copy first. If a command
|
|
9754
|
+
exits 0 printing nothing, the executable was not resolved \u2014 clear the npm cache and retry;
|
|
9755
|
+
do not assume the repository is broken.
|
|
8560
9756
|
|
|
8561
9757
|
## Step by step
|
|
8562
9758
|
|
|
@@ -8581,8 +9777,11 @@ is broken.
|
|
|
8581
9777
|
does steps 4\u20136 in one go with defaults: it writes the config (an existing one is kept),
|
|
8582
9778
|
adds the npm scripts and the devDependency, installs with the project's package manager,
|
|
8583
9779
|
and generates. Use \`--no-install\` when the machine is offline or the user does not want
|
|
8584
|
-
their lockfile touched.
|
|
8585
|
-
|
|
9780
|
+
their lockfile touched. \`--autopilot\` does exactly the same thing from the default
|
|
9781
|
+
command (\`npx ${PACKAGE_NAME2}@latest --autopilot\`) and is the form to reach for when a
|
|
9782
|
+
single invocation must both set the project up and produce the snapshot. Prefer the
|
|
9783
|
+
explicit steps below when you need to control any of those, or when something fails and
|
|
9784
|
+
you want to see which part.
|
|
8586
9785
|
|
|
8587
9786
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
8588
9787
|
the project shape, then writes a commented config file listing **every** setting at its
|
|
@@ -8590,7 +9789,10 @@ is broken.
|
|
|
8590
9789
|
the running Node can strip types (Node >= 22.6) **and** the project is an ES module
|
|
8591
9790
|
(\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
|
|
8592
9791
|
\`--preset <name>\` only if detection is wrong; the preset names are in the contract.
|
|
8593
|
-
|
|
9792
|
+
Skipping this step is safe **at a terminal**: the first \`generate\` asks once, can write
|
|
9793
|
+
the config for you and then generates. Unattended, a \`generate\` with no config exits 2 \u2014
|
|
9794
|
+
run this step, or \`--autopilot\`, or pass \`--no-config\` to run deliberately on the
|
|
9795
|
+
built-in defaults.
|
|
8594
9796
|
|
|
8595
9797
|
5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
|
|
8596
9798
|
\`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
|
|
@@ -8638,6 +9840,10 @@ any of this to get a good first snapshot.
|
|
|
8638
9840
|
deleting anything.
|
|
8639
9841
|
- **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
|
|
8640
9842
|
before retrying.
|
|
9843
|
+
- **A first run refuses rather than guessing.** In a project with no config and no previous
|
|
9844
|
+
snapshot, \`generate\` without a terminal exits 2 instead of silently using defaults. That
|
|
9845
|
+
is deliberate: say which set-up you want (\`--autopilot\`, \`init --yes\`, or \`--no-config\`)
|
|
9846
|
+
rather than retrying the same command.
|
|
8641
9847
|
- **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
|
|
8642
9848
|
pipe into a JSON parser.
|
|
8643
9849
|
|
|
@@ -8653,6 +9859,14 @@ It never writes \`AGENTS.md\`. That file is the repository's own agent contract
|
|
|
8653
9859
|
commands, conventions, house rules \u2014 and this tool has no business editing it. If you want
|
|
8654
9860
|
this skill in there, run \`npx ${PACKAGE_NAME2}@latest skill\` and paste the text yourself: the
|
|
8655
9861
|
diff, and the commit, are then yours.
|
|
9862
|
+
|
|
9863
|
+
## When the client speaks MCP
|
|
9864
|
+
|
|
9865
|
+
If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
|
|
9866
|
+
as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
|
|
9867
|
+
the working tree, so excluded files and redacted values never reach the context. Use
|
|
9868
|
+
\`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
|
|
9869
|
+
from an earlier session.
|
|
8656
9870
|
`;
|
|
8657
9871
|
}
|
|
8658
9872
|
|
|
@@ -8663,17 +9877,17 @@ var LEGACY_AGENTS_MARKER = "<!-- codebase-snapshot:skill:start -->";
|
|
|
8663
9877
|
function targetPath(cwd, target) {
|
|
8664
9878
|
switch (target) {
|
|
8665
9879
|
case "claude":
|
|
8666
|
-
return
|
|
9880
|
+
return path15.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
|
|
8667
9881
|
case "cursor":
|
|
8668
|
-
return
|
|
9882
|
+
return path15.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
|
|
8669
9883
|
}
|
|
8670
9884
|
}
|
|
8671
9885
|
function assertWritableInsideRepository(cwd, filePath) {
|
|
8672
|
-
const root =
|
|
8673
|
-
const shown =
|
|
9886
|
+
const root = fs14.realpathSync(cwd);
|
|
9887
|
+
const shown = path15.relative(root, filePath) || filePath;
|
|
8674
9888
|
let stat = null;
|
|
8675
9889
|
try {
|
|
8676
|
-
stat =
|
|
9890
|
+
stat = fs14.lstatSync(filePath);
|
|
8677
9891
|
} catch {
|
|
8678
9892
|
stat = null;
|
|
8679
9893
|
}
|
|
@@ -8689,32 +9903,32 @@ function assertWritableInsideRepository(cwd, filePath) {
|
|
|
8689
9903
|
}
|
|
8690
9904
|
let existing = filePath;
|
|
8691
9905
|
const missing = [];
|
|
8692
|
-
while (!
|
|
8693
|
-
missing.unshift(
|
|
8694
|
-
const parent =
|
|
9906
|
+
while (!fs14.existsSync(existing)) {
|
|
9907
|
+
missing.unshift(path15.basename(existing));
|
|
9908
|
+
const parent = path15.dirname(existing);
|
|
8695
9909
|
if (parent === existing) break;
|
|
8696
9910
|
existing = parent;
|
|
8697
9911
|
}
|
|
8698
|
-
const realAncestor =
|
|
8699
|
-
const resolved = missing.length === 0 ? realAncestor :
|
|
8700
|
-
if (resolved !== root && !resolved.startsWith(`${root}${
|
|
9912
|
+
const realAncestor = fs14.realpathSync(existing);
|
|
9913
|
+
const resolved = missing.length === 0 ? realAncestor : path15.join(realAncestor, ...missing);
|
|
9914
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path15.sep}`)) {
|
|
8701
9915
|
throw new Error(
|
|
8702
9916
|
`${REFUSED}: ${shown} resolves outside the repository (${resolved}). Move it back inside ${root}, or remove the link, and run again.`
|
|
8703
9917
|
);
|
|
8704
9918
|
}
|
|
8705
9919
|
}
|
|
8706
9920
|
function writeIfChanged(target, filePath, content) {
|
|
8707
|
-
const existing =
|
|
9921
|
+
const existing = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf8") : null;
|
|
8708
9922
|
if (existing === content) return { target, path: filePath, action: "unchanged" };
|
|
8709
|
-
|
|
8710
|
-
|
|
9923
|
+
fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
|
|
9924
|
+
fs14.writeFileSync(filePath, content, "utf8");
|
|
8711
9925
|
return { target, path: filePath, action: existing === null ? "created" : "updated" };
|
|
8712
9926
|
}
|
|
8713
9927
|
function legacyAgentsNote(cwd) {
|
|
8714
|
-
const filePath =
|
|
9928
|
+
const filePath = path15.join(cwd, "AGENTS.md");
|
|
8715
9929
|
try {
|
|
8716
|
-
if (!
|
|
8717
|
-
if (!
|
|
9930
|
+
if (!fs14.existsSync(filePath)) return null;
|
|
9931
|
+
if (!fs14.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
|
|
8718
9932
|
} catch {
|
|
8719
9933
|
return null;
|
|
8720
9934
|
}
|
|
@@ -8726,9 +9940,9 @@ function installSkill(cwd, target = "all") {
|
|
|
8726
9940
|
const notes = [];
|
|
8727
9941
|
const wanted = target === "all" ? ["claude", "cursor"] : [target];
|
|
8728
9942
|
for (const item of wanted) assertWritableInsideRepository(cwd, targetPath(cwd, item));
|
|
8729
|
-
const createsSomething = wanted.some((item) => !
|
|
9943
|
+
const createsSomething = wanted.some((item) => !fs14.existsSync(targetPath(cwd, item)));
|
|
8730
9944
|
if (createsSomething) {
|
|
8731
|
-
assertWritableInsideRepository(cwd,
|
|
9945
|
+
assertWritableInsideRepository(cwd, path15.join(cwd, ".gitignore"));
|
|
8732
9946
|
}
|
|
8733
9947
|
for (const item of wanted) {
|
|
8734
9948
|
written.push(writeIfChanged(item, targetPath(cwd, item), skill));
|
|
@@ -8744,6 +9958,771 @@ function isSkillTarget(value) {
|
|
|
8744
9958
|
return SKILL_TARGETS.includes(value);
|
|
8745
9959
|
}
|
|
8746
9960
|
|
|
9961
|
+
// src/mcp/era.ts
|
|
9962
|
+
function createSession() {
|
|
9963
|
+
return { era: null, protocolVersion: null, ready: false };
|
|
9964
|
+
}
|
|
9965
|
+
function isLegacyVersion(version) {
|
|
9966
|
+
return LEGACY_PROTOCOL_VERSIONS.includes(version);
|
|
9967
|
+
}
|
|
9968
|
+
function isModernVersion(version) {
|
|
9969
|
+
return MODERN_PROTOCOL_VERSIONS.includes(version);
|
|
9970
|
+
}
|
|
9971
|
+
function inspectModernMeta(params) {
|
|
9972
|
+
const raw = params?._meta;
|
|
9973
|
+
if (!isJsonObject(raw)) return { kind: "absent" };
|
|
9974
|
+
const version = raw[META_KEYS.protocolVersion];
|
|
9975
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
9976
|
+
return { kind: "incomplete", missing: META_KEYS.protocolVersion };
|
|
9977
|
+
}
|
|
9978
|
+
const capabilities2 = raw[META_KEYS.clientCapabilities];
|
|
9979
|
+
if (!isJsonObject(capabilities2)) {
|
|
9980
|
+
return { kind: "incomplete", missing: META_KEYS.clientCapabilities };
|
|
9981
|
+
}
|
|
9982
|
+
const info2 = raw[META_KEYS.clientInfo];
|
|
9983
|
+
return {
|
|
9984
|
+
kind: "present",
|
|
9985
|
+
meta: {
|
|
9986
|
+
version,
|
|
9987
|
+
clientCapabilities: capabilities2,
|
|
9988
|
+
clientInfo: isJsonObject(info2) ? info2 : null
|
|
9989
|
+
}
|
|
9990
|
+
};
|
|
9991
|
+
}
|
|
9992
|
+
function unsupportedVersionData(requested) {
|
|
9993
|
+
return { requested, supported: [...SUPPORTED_PROTOCOL_VERSIONS] };
|
|
9994
|
+
}
|
|
9995
|
+
function classifyRequest(method, params, session) {
|
|
9996
|
+
const inspection = inspectModernMeta(params);
|
|
9997
|
+
if (session.era === "modern") {
|
|
9998
|
+
if (inspection.kind === "absent") {
|
|
9999
|
+
return {
|
|
10000
|
+
kind: "error",
|
|
10001
|
+
code: ERROR_CODES.invalidRequest,
|
|
10002
|
+
message: `This request has no _meta. The modern era requires "${META_KEYS.protocolVersion}" and "${META_KEYS.clientCapabilities}" on every request.`
|
|
10003
|
+
};
|
|
10004
|
+
}
|
|
10005
|
+
if (inspection.kind === "incomplete") {
|
|
10006
|
+
return {
|
|
10007
|
+
kind: "error",
|
|
10008
|
+
code: ERROR_CODES.invalidParams,
|
|
10009
|
+
message: `The request _meta is missing the required key "${inspection.missing}".`
|
|
10010
|
+
};
|
|
10011
|
+
}
|
|
10012
|
+
if (!isModernVersion(inspection.meta.version)) {
|
|
10013
|
+
return {
|
|
10014
|
+
kind: "error",
|
|
10015
|
+
code: ERROR_CODES.unsupportedProtocolVersion,
|
|
10016
|
+
message: `Unsupported protocol version "${inspection.meta.version}".`,
|
|
10017
|
+
data: unsupportedVersionData(inspection.meta.version)
|
|
10018
|
+
};
|
|
10019
|
+
}
|
|
10020
|
+
return { kind: "era", era: "modern", version: inspection.meta.version };
|
|
10021
|
+
}
|
|
10022
|
+
if (session.era === "legacy") {
|
|
10023
|
+
return { kind: "era", era: "legacy", version: session.protocolVersion };
|
|
10024
|
+
}
|
|
10025
|
+
if (method === METHOD3.initialize) {
|
|
10026
|
+
return { kind: "era", era: "legacy", version: null };
|
|
10027
|
+
}
|
|
10028
|
+
if (inspection.kind === "present") {
|
|
10029
|
+
if (!isModernVersion(inspection.meta.version)) {
|
|
10030
|
+
return {
|
|
10031
|
+
kind: "error",
|
|
10032
|
+
code: ERROR_CODES.unsupportedProtocolVersion,
|
|
10033
|
+
message: `Unsupported protocol version "${inspection.meta.version}".`,
|
|
10034
|
+
data: unsupportedVersionData(inspection.meta.version)
|
|
10035
|
+
};
|
|
10036
|
+
}
|
|
10037
|
+
return { kind: "era", era: "modern", version: inspection.meta.version };
|
|
10038
|
+
}
|
|
10039
|
+
if (inspection.kind === "incomplete") {
|
|
10040
|
+
return {
|
|
10041
|
+
kind: "error",
|
|
10042
|
+
code: ERROR_CODES.invalidParams,
|
|
10043
|
+
message: `The request _meta is missing the required key "${inspection.missing}".`
|
|
10044
|
+
};
|
|
10045
|
+
}
|
|
10046
|
+
if (method === METHOD3.discover) {
|
|
10047
|
+
return { kind: "era", era: "modern", version: MODERN_PROTOCOL_VERSIONS[0] };
|
|
10048
|
+
}
|
|
10049
|
+
return { kind: "era", era: "legacy", version: null };
|
|
10050
|
+
}
|
|
10051
|
+
function negotiateLegacyVersion(requested) {
|
|
10052
|
+
if (typeof requested === "string" && isLegacyVersion(requested)) {
|
|
10053
|
+
return { version: requested, matched: true };
|
|
10054
|
+
}
|
|
10055
|
+
return { version: DEFAULT_LEGACY_PROTOCOL_VERSION, matched: false };
|
|
10056
|
+
}
|
|
10057
|
+
|
|
10058
|
+
// src/mcp/schema.ts
|
|
10059
|
+
function validateArguments(schema, value) {
|
|
10060
|
+
if (value === void 0) value = {};
|
|
10061
|
+
if (!isJsonObject(value)) return ["arguments must be a JSON object"];
|
|
10062
|
+
const problems = [];
|
|
10063
|
+
const known = new Set(Object.keys(schema.properties));
|
|
10064
|
+
for (const key of Object.keys(value)) {
|
|
10065
|
+
if (!known.has(key) && schema.additionalProperties !== true) {
|
|
10066
|
+
const allowed = [...known].sort().join(", ");
|
|
10067
|
+
problems.push(`unknown argument "${key}" (accepted: ${allowed || "none"})`);
|
|
10068
|
+
}
|
|
10069
|
+
}
|
|
10070
|
+
for (const required of schema.required ?? []) {
|
|
10071
|
+
if (value[required] === void 0) problems.push(`missing required argument "${required}"`);
|
|
10072
|
+
}
|
|
10073
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
10074
|
+
const argument = value[key];
|
|
10075
|
+
if (argument === void 0) continue;
|
|
10076
|
+
problems.push(...checkValue(key, propertySchema, argument));
|
|
10077
|
+
}
|
|
10078
|
+
return problems;
|
|
10079
|
+
}
|
|
10080
|
+
function checkValue(path18, schema, value) {
|
|
10081
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
10082
|
+
return [`"${path18}" must be one of: ${schema.enum.join(", ")}`];
|
|
10083
|
+
}
|
|
10084
|
+
switch (schema.type) {
|
|
10085
|
+
case "string": {
|
|
10086
|
+
if (typeof value !== "string") return [`"${path18}" must be a string`];
|
|
10087
|
+
const problems = [];
|
|
10088
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
10089
|
+
problems.push(`"${path18}" must be at least ${schema.minLength} character(s)`);
|
|
10090
|
+
}
|
|
10091
|
+
if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
|
|
10092
|
+
problems.push(`"${path18}" must be at most ${schema.maxLength} character(s)`);
|
|
10093
|
+
}
|
|
10094
|
+
return problems;
|
|
10095
|
+
}
|
|
10096
|
+
case "number":
|
|
10097
|
+
case "integer": {
|
|
10098
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
10099
|
+
return [`"${path18}" must be a number`];
|
|
10100
|
+
}
|
|
10101
|
+
if (schema.type === "integer" && !Number.isInteger(value)) {
|
|
10102
|
+
return [`"${path18}" must be an integer`];
|
|
10103
|
+
}
|
|
10104
|
+
const problems = [];
|
|
10105
|
+
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
10106
|
+
problems.push(`"${path18}" must be >= ${schema.minimum}`);
|
|
10107
|
+
}
|
|
10108
|
+
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
10109
|
+
problems.push(`"${path18}" must be <= ${schema.maximum}`);
|
|
10110
|
+
}
|
|
10111
|
+
return problems;
|
|
10112
|
+
}
|
|
10113
|
+
case "boolean":
|
|
10114
|
+
return typeof value === "boolean" ? [] : [`"${path18}" must be a boolean`];
|
|
10115
|
+
case "array": {
|
|
10116
|
+
if (!Array.isArray(value)) return [`"${path18}" must be an array`];
|
|
10117
|
+
if (!schema.items) return [];
|
|
10118
|
+
const problems = [];
|
|
10119
|
+
value.forEach((item, index) => {
|
|
10120
|
+
problems.push(...checkValue(`${path18}[${index}]`, schema.items, item));
|
|
10121
|
+
});
|
|
10122
|
+
return problems;
|
|
10123
|
+
}
|
|
10124
|
+
case "object":
|
|
10125
|
+
return isJsonObject(value) ? [] : [`"${path18}" must be an object`];
|
|
10126
|
+
default:
|
|
10127
|
+
return [];
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
10130
|
+
function applyDefaults(schema, value) {
|
|
10131
|
+
const out = { ...value };
|
|
10132
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
10133
|
+
if (out[key] === void 0 && propertySchema.default !== void 0) {
|
|
10134
|
+
out[key] = propertySchema.default;
|
|
10135
|
+
}
|
|
10136
|
+
}
|
|
10137
|
+
return out;
|
|
10138
|
+
}
|
|
10139
|
+
|
|
10140
|
+
// src/mcp/server.ts
|
|
10141
|
+
function capabilities() {
|
|
10142
|
+
return {
|
|
10143
|
+
tools: { listChanged: false },
|
|
10144
|
+
resources: { subscribe: false, listChanged: false },
|
|
10145
|
+
prompts: { listChanged: false }
|
|
10146
|
+
};
|
|
10147
|
+
}
|
|
10148
|
+
function requestIdOf(message) {
|
|
10149
|
+
const raw = message.id;
|
|
10150
|
+
return typeof raw === "string" || typeof raw === "number" ? raw : null;
|
|
10151
|
+
}
|
|
10152
|
+
function createMcpServer(options) {
|
|
10153
|
+
const session = createSession();
|
|
10154
|
+
const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
10155
|
+
const promptsByName = new Map(options.prompts.map((prompt) => [prompt.name, prompt]));
|
|
10156
|
+
const identity = () => ({
|
|
10157
|
+
name: options.info.name,
|
|
10158
|
+
title: options.info.title,
|
|
10159
|
+
version: options.info.version,
|
|
10160
|
+
websiteUrl: options.info.websiteUrl
|
|
10161
|
+
});
|
|
10162
|
+
const ok = (id, result, modern) => successResponse(id, result, modern, options.info);
|
|
10163
|
+
function handleNotification(method) {
|
|
10164
|
+
if (method === METHOD3.initialized && session.era === "legacy") session.ready = true;
|
|
10165
|
+
}
|
|
10166
|
+
function initialize(id, params) {
|
|
10167
|
+
const negotiation = negotiateLegacyVersion(params?.protocolVersion);
|
|
10168
|
+
session.era = "legacy";
|
|
10169
|
+
session.protocolVersion = negotiation.version;
|
|
10170
|
+
session.ready = true;
|
|
10171
|
+
options.context.log(
|
|
10172
|
+
negotiation.matched ? `mcp: client initialized with protocol ${negotiation.version}` : `mcp: client asked for an unsupported version and was offered ${negotiation.version}`
|
|
10173
|
+
);
|
|
10174
|
+
return {
|
|
10175
|
+
jsonrpc: "2.0",
|
|
10176
|
+
id,
|
|
10177
|
+
result: {
|
|
10178
|
+
protocolVersion: negotiation.version,
|
|
10179
|
+
capabilities: capabilities(),
|
|
10180
|
+
serverInfo: identity(),
|
|
10181
|
+
instructions: options.instructions
|
|
10182
|
+
}
|
|
10183
|
+
};
|
|
10184
|
+
}
|
|
10185
|
+
function discover(id) {
|
|
10186
|
+
session.era = "modern";
|
|
10187
|
+
session.ready = true;
|
|
10188
|
+
return ok(
|
|
10189
|
+
id,
|
|
10190
|
+
{
|
|
10191
|
+
supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
10192
|
+
capabilities: capabilities(),
|
|
10193
|
+
instructions: options.instructions,
|
|
10194
|
+
// The capability set depends on which repository the server was started for, so the answer
|
|
10195
|
+
// may be cached only by the client that asked, and not at all after this response.
|
|
10196
|
+
cacheScope: "private",
|
|
10197
|
+
ttlMs: 0
|
|
10198
|
+
},
|
|
10199
|
+
true
|
|
10200
|
+
);
|
|
10201
|
+
}
|
|
10202
|
+
function listTools(id, modern) {
|
|
10203
|
+
const tools = options.tools.map((tool) => ({
|
|
10204
|
+
name: tool.name,
|
|
10205
|
+
title: tool.title,
|
|
10206
|
+
description: tool.description,
|
|
10207
|
+
inputSchema: tool.inputSchema,
|
|
10208
|
+
...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
|
|
10209
|
+
annotations: tool.annotations
|
|
10210
|
+
}));
|
|
10211
|
+
return ok(id, { tools }, modern);
|
|
10212
|
+
}
|
|
10213
|
+
async function callTool(id, modern, params) {
|
|
10214
|
+
const name = params?.name;
|
|
10215
|
+
if (typeof name !== "string") {
|
|
10216
|
+
return errorResponse(id, ERROR_CODES.invalidParams, 'tools/call requires a "name" string.');
|
|
10217
|
+
}
|
|
10218
|
+
const tool = toolsByName.get(name);
|
|
10219
|
+
if (!tool) {
|
|
10220
|
+
const known = [...toolsByName.keys()].sort().join(", ");
|
|
10221
|
+
return errorResponse(id, ERROR_CODES.invalidParams, `Unknown tool "${name}". Available: ${known}.`);
|
|
10222
|
+
}
|
|
10223
|
+
const rawArguments = params?.arguments ?? {};
|
|
10224
|
+
if (!isJsonObject(rawArguments)) {
|
|
10225
|
+
return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
|
|
10226
|
+
}
|
|
10227
|
+
const problems = validateArguments(tool.inputSchema, rawArguments);
|
|
10228
|
+
if (problems.length > 0) {
|
|
10229
|
+
return ok(
|
|
10230
|
+
id,
|
|
10231
|
+
{
|
|
10232
|
+
content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
|
|
10233
|
+
- ${problems.join("\n- ")}` }],
|
|
10234
|
+
isError: true
|
|
10235
|
+
},
|
|
10236
|
+
modern
|
|
10237
|
+
);
|
|
10238
|
+
}
|
|
10239
|
+
const args = applyDefaults(tool.inputSchema, rawArguments);
|
|
10240
|
+
let outcome;
|
|
10241
|
+
try {
|
|
10242
|
+
outcome = await tool.handler(args, options.context);
|
|
10243
|
+
} catch (error) {
|
|
10244
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10245
|
+
options.context.log(`mcp: tool ${tool.name} failed: ${detail}`);
|
|
10246
|
+
return ok(
|
|
10247
|
+
id,
|
|
10248
|
+
{ content: [{ type: "text", text: `${tool.name} failed: ${detail}` }], isError: true },
|
|
10249
|
+
modern
|
|
10250
|
+
);
|
|
10251
|
+
}
|
|
10252
|
+
return ok(
|
|
10253
|
+
id,
|
|
10254
|
+
{
|
|
10255
|
+
content: [{ type: "text", text: outcome.text }],
|
|
10256
|
+
...outcome.structured ? { structuredContent: outcome.structured } : {},
|
|
10257
|
+
...outcome.isError ? { isError: true } : {}
|
|
10258
|
+
},
|
|
10259
|
+
modern
|
|
10260
|
+
);
|
|
10261
|
+
}
|
|
10262
|
+
async function listResources(id, modern) {
|
|
10263
|
+
const resources = await options.resources.list(options.context);
|
|
10264
|
+
return ok(id, { resources }, modern);
|
|
10265
|
+
}
|
|
10266
|
+
async function readResource(id, modern, params) {
|
|
10267
|
+
const uri = params?.uri;
|
|
10268
|
+
if (typeof uri !== "string") {
|
|
10269
|
+
return errorResponse(id, ERROR_CODES.invalidParams, 'resources/read requires a "uri" string.');
|
|
10270
|
+
}
|
|
10271
|
+
const contents = await options.resources.read(uri, options.context);
|
|
10272
|
+
if (!contents) {
|
|
10273
|
+
return errorResponse(id, ERROR_CODES.invalidParams, `Unknown resource "${uri}".`);
|
|
10274
|
+
}
|
|
10275
|
+
return ok(id, { contents: [contents] }, modern);
|
|
10276
|
+
}
|
|
10277
|
+
function listPrompts(id, modern) {
|
|
10278
|
+
const prompts = options.prompts.map((prompt) => ({
|
|
10279
|
+
name: prompt.name,
|
|
10280
|
+
...prompt.title ? { title: prompt.title } : {},
|
|
10281
|
+
description: prompt.description,
|
|
10282
|
+
arguments: prompt.arguments
|
|
10283
|
+
}));
|
|
10284
|
+
return ok(id, { prompts }, modern);
|
|
10285
|
+
}
|
|
10286
|
+
function getPrompt(id, modern, params) {
|
|
10287
|
+
const name = params?.name;
|
|
10288
|
+
if (typeof name !== "string") {
|
|
10289
|
+
return errorResponse(id, ERROR_CODES.invalidParams, 'prompts/get requires a "name" string.');
|
|
10290
|
+
}
|
|
10291
|
+
const prompt = promptsByName.get(name);
|
|
10292
|
+
if (!prompt) {
|
|
10293
|
+
const known = [...promptsByName.keys()].sort().join(", ");
|
|
10294
|
+
return errorResponse(id, ERROR_CODES.invalidParams, `Unknown prompt "${name}". Available: ${known}.`);
|
|
10295
|
+
}
|
|
10296
|
+
const rawArguments = params?.arguments ?? {};
|
|
10297
|
+
if (!isJsonObject(rawArguments)) {
|
|
10298
|
+
return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
|
|
10299
|
+
}
|
|
10300
|
+
const built = prompt.build(rawArguments);
|
|
10301
|
+
const messages = built.messages.map((message) => ({
|
|
10302
|
+
role: message.role,
|
|
10303
|
+
content: { type: "text", text: message.content.text }
|
|
10304
|
+
}));
|
|
10305
|
+
return ok(id, { description: built.description, messages }, modern);
|
|
10306
|
+
}
|
|
10307
|
+
async function route(name, id, modern, params) {
|
|
10308
|
+
switch (name) {
|
|
10309
|
+
case METHOD3.initialize:
|
|
10310
|
+
return modern ? errorResponse(
|
|
10311
|
+
id,
|
|
10312
|
+
ERROR_CODES.methodNotFound,
|
|
10313
|
+
'"initialize" does not exist in this protocol revision; call "server/discover".'
|
|
10314
|
+
) : initialize(id, params);
|
|
10315
|
+
case METHOD3.discover:
|
|
10316
|
+
return discover(id);
|
|
10317
|
+
case METHOD3.ping:
|
|
10318
|
+
return modern ? errorResponse(
|
|
10319
|
+
id,
|
|
10320
|
+
ERROR_CODES.methodNotFound,
|
|
10321
|
+
'"ping" was removed in this protocol revision; call "server/discover".'
|
|
10322
|
+
) : ok(id, {}, modern);
|
|
10323
|
+
default:
|
|
10324
|
+
break;
|
|
10325
|
+
}
|
|
10326
|
+
if (!session.ready) {
|
|
10327
|
+
return errorResponse(
|
|
10328
|
+
id,
|
|
10329
|
+
ERROR_CODES.invalidRequest,
|
|
10330
|
+
'This connection has not been initialized. Send "initialize" first.'
|
|
10331
|
+
);
|
|
10332
|
+
}
|
|
10333
|
+
switch (name) {
|
|
10334
|
+
case METHOD3.toolsList:
|
|
10335
|
+
return listTools(id, modern);
|
|
10336
|
+
case METHOD3.toolsCall:
|
|
10337
|
+
return callTool(id, modern, params);
|
|
10338
|
+
case METHOD3.resourcesList:
|
|
10339
|
+
return listResources(id, modern);
|
|
10340
|
+
case METHOD3.resourcesRead:
|
|
10341
|
+
return readResource(id, modern, params);
|
|
10342
|
+
case METHOD3.resourceTemplatesList:
|
|
10343
|
+
return ok(id, { resourceTemplates: options.resources.templates() }, modern);
|
|
10344
|
+
case METHOD3.promptsList:
|
|
10345
|
+
return listPrompts(id, modern);
|
|
10346
|
+
case METHOD3.promptsGet:
|
|
10347
|
+
return getPrompt(id, modern, params);
|
|
10348
|
+
default:
|
|
10349
|
+
return errorResponse(id, ERROR_CODES.methodNotFound, `Unknown method "${name}".`);
|
|
10350
|
+
}
|
|
10351
|
+
}
|
|
10352
|
+
async function dispatch(message) {
|
|
10353
|
+
if (!isJsonObject(message)) {
|
|
10354
|
+
return errorResponse(null, ERROR_CODES.parseError, "Message is not a JSON object.");
|
|
10355
|
+
}
|
|
10356
|
+
const isRequest = Object.prototype.hasOwnProperty.call(message, "id");
|
|
10357
|
+
const method = message.method;
|
|
10358
|
+
const params = message.params;
|
|
10359
|
+
if (message.jsonrpc !== "2.0" && isRequest) {
|
|
10360
|
+
return errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry "jsonrpc": "2.0".');
|
|
10361
|
+
}
|
|
10362
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
10363
|
+
return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry a "method" string.') : null;
|
|
10364
|
+
}
|
|
10365
|
+
if (params !== void 0 && !isJsonObject(params)) {
|
|
10366
|
+
return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, '"params" must be a JSON object when present.') : null;
|
|
10367
|
+
}
|
|
10368
|
+
const named = params;
|
|
10369
|
+
const decision = classifyRequest(method, named, session);
|
|
10370
|
+
const modern = decision.kind === "era" ? decision.era === "modern" : session.era === "modern";
|
|
10371
|
+
if (!isRequest) {
|
|
10372
|
+
if (decision.kind === "era" && session.era === null) {
|
|
10373
|
+
session.era = decision.era;
|
|
10374
|
+
session.protocolVersion = decision.version;
|
|
10375
|
+
}
|
|
10376
|
+
handleNotification(method);
|
|
10377
|
+
return null;
|
|
10378
|
+
}
|
|
10379
|
+
const id = requestIdOf(message);
|
|
10380
|
+
if (id === null) {
|
|
10381
|
+
return errorResponse(null, ERROR_CODES.invalidRequest, 'A request "id" must be a string or a number.');
|
|
10382
|
+
}
|
|
10383
|
+
if (decision.kind === "error") {
|
|
10384
|
+
return errorResponse(id, decision.code, decision.message, decision.data);
|
|
10385
|
+
}
|
|
10386
|
+
if (session.era === null) {
|
|
10387
|
+
session.era = decision.era;
|
|
10388
|
+
session.protocolVersion = decision.version;
|
|
10389
|
+
}
|
|
10390
|
+
return route(method, id, modern, named);
|
|
10391
|
+
}
|
|
10392
|
+
return {
|
|
10393
|
+
session,
|
|
10394
|
+
async handle(message) {
|
|
10395
|
+
try {
|
|
10396
|
+
return await dispatch(message);
|
|
10397
|
+
} catch (error) {
|
|
10398
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10399
|
+
options.context.log(`mcp: internal error: ${detail}`);
|
|
10400
|
+
const id = isJsonObject(message) ? requestIdOf(message) : null;
|
|
10401
|
+
return errorResponse(id, ERROR_CODES.internalError, "Internal server error.");
|
|
10402
|
+
}
|
|
10403
|
+
}
|
|
10404
|
+
};
|
|
10405
|
+
}
|
|
10406
|
+
|
|
10407
|
+
// src/mcp/resources.ts
|
|
10408
|
+
import * as fs15 from "fs";
|
|
10409
|
+
import * as path16 from "path";
|
|
10410
|
+
var MAX_RESOURCE_BYTES = 512 * 1024;
|
|
10411
|
+
var PART_URI = /^snapshot:\/\/parts\/(\d+)$/;
|
|
10412
|
+
var REPORT_URIS = /* @__PURE__ */ new Map([
|
|
10413
|
+
["snapshot://reports/audit", { file: "audit.md", title: "Secret audit and risk findings" }],
|
|
10414
|
+
["snapshot://reports/tree", { file: "project-tree.md", title: "Project tree" }],
|
|
10415
|
+
["snapshot://reports/navigation", { file: "navigation.md", title: "Navigation index" }]
|
|
10416
|
+
]);
|
|
10417
|
+
function formatBytes2(bytes) {
|
|
10418
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
10419
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
10420
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
10421
|
+
}
|
|
10422
|
+
function truncationMarker(originalBytes) {
|
|
10423
|
+
return `
|
|
10424
|
+
|
|
10425
|
+
[truncated: ${formatBytes2(originalBytes)} total. Use the snapshot_read tool for a line window.]
|
|
10426
|
+
`;
|
|
10427
|
+
}
|
|
10428
|
+
function capUtf8(text2, limit) {
|
|
10429
|
+
const size2 = Buffer.byteLength(text2, "utf8");
|
|
10430
|
+
if (size2 <= limit) return { text: text2, truncated: false };
|
|
10431
|
+
const cut = Buffer.from(text2, "utf8").subarray(0, limit).toString("utf8").replace(/\uFFFD+$/, "");
|
|
10432
|
+
return { text: `${cut}${truncationMarker(size2)}`, truncated: true };
|
|
10433
|
+
}
|
|
10434
|
+
async function workspaceFor(context) {
|
|
10435
|
+
const root = resolveRoot(void 0, context);
|
|
10436
|
+
if (!root.ok) return { error: root.message };
|
|
10437
|
+
const opened = await openWorkspace(root.root);
|
|
10438
|
+
if (!opened.ok) return { error: opened.message };
|
|
10439
|
+
return opened.workspace;
|
|
10440
|
+
}
|
|
10441
|
+
function readFileIfPresent(file) {
|
|
10442
|
+
try {
|
|
10443
|
+
return fs15.readFileSync(file, "utf8");
|
|
10444
|
+
} catch {
|
|
10445
|
+
return null;
|
|
10446
|
+
}
|
|
10447
|
+
}
|
|
10448
|
+
function buildResourceProvider() {
|
|
10449
|
+
return {
|
|
10450
|
+
async list(context) {
|
|
10451
|
+
const resources = [
|
|
10452
|
+
{
|
|
10453
|
+
uri: "snapshot://manifest",
|
|
10454
|
+
name: "manifest",
|
|
10455
|
+
title: "Snapshot manifest",
|
|
10456
|
+
description: "Every file in the snapshot with its layer, kind, language, part and hash, plus the reason each excluded file was excluded.",
|
|
10457
|
+
mimeType: "application/json"
|
|
10458
|
+
},
|
|
10459
|
+
{
|
|
10460
|
+
uri: "snapshot://contract",
|
|
10461
|
+
name: "contract",
|
|
10462
|
+
title: "Tool contract",
|
|
10463
|
+
description: "The machine-readable description of the codebase-snapshot command line.",
|
|
10464
|
+
mimeType: "application/json"
|
|
10465
|
+
},
|
|
10466
|
+
{
|
|
10467
|
+
uri: "snapshot://skill",
|
|
10468
|
+
name: "skill",
|
|
10469
|
+
title: "Agent skill",
|
|
10470
|
+
description: "Usage guidance for an agent working with this tool.",
|
|
10471
|
+
mimeType: "text/markdown"
|
|
10472
|
+
}
|
|
10473
|
+
];
|
|
10474
|
+
for (const [uri, report] of REPORT_URIS) {
|
|
10475
|
+
resources.push({
|
|
10476
|
+
uri,
|
|
10477
|
+
name: report.file.replace(/\.md$/, ""),
|
|
10478
|
+
title: report.title,
|
|
10479
|
+
description: `${report.title} from the most recent snapshot.`,
|
|
10480
|
+
mimeType: "text/markdown"
|
|
10481
|
+
});
|
|
10482
|
+
}
|
|
10483
|
+
const workspace = await workspaceFor(context);
|
|
10484
|
+
if ("error" in workspace) return resources;
|
|
10485
|
+
const read = readManifest3(workspace);
|
|
10486
|
+
if (!read.ok) return resources;
|
|
10487
|
+
for (const part of read.manifest.parts) {
|
|
10488
|
+
resources.push({
|
|
10489
|
+
uri: `snapshot://parts/${part.index}`,
|
|
10490
|
+
name: `part-${String(part.index).padStart(3, "0")}`,
|
|
10491
|
+
title: `Part ${part.index} (${part.fileCount} files, ${formatBytes2(part.byteSize)})`,
|
|
10492
|
+
description: `Part ${part.index} of ${read.manifest.parts.length}: ${part.fileCount} file(s), ${part.lines} line(s), ${formatBytes2(part.byteSize)}.`,
|
|
10493
|
+
mimeType: "text/markdown"
|
|
10494
|
+
});
|
|
10495
|
+
}
|
|
10496
|
+
return resources;
|
|
10497
|
+
},
|
|
10498
|
+
templates() {
|
|
10499
|
+
return [
|
|
10500
|
+
{
|
|
10501
|
+
uriTemplate: "snapshot://parts/{index}",
|
|
10502
|
+
name: "part",
|
|
10503
|
+
title: "Snapshot part",
|
|
10504
|
+
description: "One budgeted part of the snapshot, by its 1-based index.",
|
|
10505
|
+
mimeType: "text/markdown"
|
|
10506
|
+
}
|
|
10507
|
+
];
|
|
10508
|
+
},
|
|
10509
|
+
async read(uri, context) {
|
|
10510
|
+
if (uri === "snapshot://contract") {
|
|
10511
|
+
const contract = buildAgentContract();
|
|
10512
|
+
return { uri, mimeType: "application/json", text: JSON.stringify(contract, null, 2) };
|
|
10513
|
+
}
|
|
10514
|
+
if (uri === "snapshot://skill") {
|
|
10515
|
+
return { uri, mimeType: "text/markdown", text: renderSkill() };
|
|
10516
|
+
}
|
|
10517
|
+
const workspace = await workspaceFor(context);
|
|
10518
|
+
if ("error" in workspace) return null;
|
|
10519
|
+
const report = REPORT_URIS.get(uri);
|
|
10520
|
+
if (report) {
|
|
10521
|
+
const text2 = readFileIfPresent(
|
|
10522
|
+
path16.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
|
|
10523
|
+
);
|
|
10524
|
+
if (text2 === null) return null;
|
|
10525
|
+
return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
|
|
10526
|
+
}
|
|
10527
|
+
if (uri === "snapshot://manifest") {
|
|
10528
|
+
const read = readManifest3(workspace);
|
|
10529
|
+
if (!read.ok) return null;
|
|
10530
|
+
const text2 = readFileIfPresent(read.file) ?? JSON.stringify(read.manifest, null, 2);
|
|
10531
|
+
return { uri, mimeType: "application/json", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
|
|
10532
|
+
}
|
|
10533
|
+
const partMatch = PART_URI.exec(uri);
|
|
10534
|
+
if (partMatch) {
|
|
10535
|
+
const read = readManifest3(workspace);
|
|
10536
|
+
if (!read.ok) return null;
|
|
10537
|
+
const index = Number(partMatch[1]);
|
|
10538
|
+
const part = read.manifest.parts.find((candidate) => candidate.index === index);
|
|
10539
|
+
if (!part) return null;
|
|
10540
|
+
const text2 = readFileIfPresent(
|
|
10541
|
+
path16.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
|
|
10542
|
+
);
|
|
10543
|
+
if (text2 === null) return null;
|
|
10544
|
+
return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
|
|
10545
|
+
}
|
|
10546
|
+
return null;
|
|
10547
|
+
}
|
|
10548
|
+
};
|
|
10549
|
+
}
|
|
10550
|
+
|
|
10551
|
+
// src/mcp/instructions.ts
|
|
10552
|
+
var SERVER_INSTRUCTIONS = [
|
|
10553
|
+
"codebase-snapshot serves this repository as a size-budgeted snapshot, not as a directory walk.",
|
|
10554
|
+
"",
|
|
10555
|
+
"Work in this order:",
|
|
10556
|
+
"1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
|
|
10557
|
+
"2. snapshot_locate \u2014 find which part holds the code you need.",
|
|
10558
|
+
"3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
|
|
10559
|
+
"4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
|
|
10560
|
+
"",
|
|
10561
|
+
"Rules that matter:",
|
|
10562
|
+
"- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
|
|
10563
|
+
"- A file absent from the snapshot has a recorded reason. Ask snapshot_explain, do not assume.",
|
|
10564
|
+
"- snapshot_generate is the only tool that writes. It never installs packages and never writes config.",
|
|
10565
|
+
"- Paths are relative to the repository root the server was started for, and nothing outside it is readable."
|
|
10566
|
+
].join("\n");
|
|
10567
|
+
|
|
10568
|
+
// src/mcp/stdio.ts
|
|
10569
|
+
var MAX_LINE_BYTES = 32 * 1024 * 1024;
|
|
10570
|
+
function createWriter(output) {
|
|
10571
|
+
return {
|
|
10572
|
+
write(value) {
|
|
10573
|
+
return new Promise((resolve9, reject) => {
|
|
10574
|
+
output.write(`${JSON.stringify(value)}
|
|
10575
|
+
`, (error) => {
|
|
10576
|
+
if (error) reject(error);
|
|
10577
|
+
else resolve9();
|
|
10578
|
+
});
|
|
10579
|
+
});
|
|
10580
|
+
}
|
|
10581
|
+
};
|
|
10582
|
+
}
|
|
10583
|
+
function createLineReader(input, options) {
|
|
10584
|
+
let buffer = "";
|
|
10585
|
+
let discarding = false;
|
|
10586
|
+
let discardedBytes = 0;
|
|
10587
|
+
const deliver = (line) => {
|
|
10588
|
+
if (discarding) {
|
|
10589
|
+
discarding = false;
|
|
10590
|
+
discardedBytes = 0;
|
|
10591
|
+
return;
|
|
10592
|
+
}
|
|
10593
|
+
const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
10594
|
+
if (trimmed.trim().length === 0) return;
|
|
10595
|
+
options.onLine(trimmed);
|
|
10596
|
+
};
|
|
10597
|
+
const drain = () => {
|
|
10598
|
+
let index = buffer.indexOf("\n");
|
|
10599
|
+
while (index >= 0) {
|
|
10600
|
+
const line = buffer.slice(0, index);
|
|
10601
|
+
buffer = buffer.slice(index + 1);
|
|
10602
|
+
deliver(line);
|
|
10603
|
+
index = buffer.indexOf("\n");
|
|
10604
|
+
}
|
|
10605
|
+
};
|
|
10606
|
+
const onData = (chunk) => {
|
|
10607
|
+
if (discarding) {
|
|
10608
|
+
const index = chunk.indexOf("\n");
|
|
10609
|
+
if (index < 0) {
|
|
10610
|
+
discardedBytes += chunk.length;
|
|
10611
|
+
return;
|
|
10612
|
+
}
|
|
10613
|
+
buffer = chunk.slice(index + 1);
|
|
10614
|
+
deliver("");
|
|
10615
|
+
drain();
|
|
10616
|
+
return;
|
|
10617
|
+
}
|
|
10618
|
+
buffer += chunk;
|
|
10619
|
+
if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
|
|
10620
|
+
discarding = true;
|
|
10621
|
+
discardedBytes = buffer.length;
|
|
10622
|
+
buffer = "";
|
|
10623
|
+
options.onOverflow?.(discardedBytes);
|
|
10624
|
+
return;
|
|
10625
|
+
}
|
|
10626
|
+
drain();
|
|
10627
|
+
};
|
|
10628
|
+
const onEnd = () => {
|
|
10629
|
+
if (buffer.length > 0) {
|
|
10630
|
+
const rest = buffer;
|
|
10631
|
+
buffer = "";
|
|
10632
|
+
deliver(rest);
|
|
10633
|
+
}
|
|
10634
|
+
};
|
|
10635
|
+
input.setEncoding("utf8");
|
|
10636
|
+
input.on("data", onData);
|
|
10637
|
+
input.on("end", onEnd);
|
|
10638
|
+
return () => {
|
|
10639
|
+
input.off("data", onData);
|
|
10640
|
+
input.off("end", onEnd);
|
|
10641
|
+
};
|
|
10642
|
+
}
|
|
10643
|
+
|
|
10644
|
+
// src/mcp/run.ts
|
|
10645
|
+
var PROJECT_URL = "https://ankurhalder.github.io/codebase-snapshot/";
|
|
10646
|
+
function createServer(options) {
|
|
10647
|
+
const cwd = options.cwd;
|
|
10648
|
+
const roots = options.roots && options.roots.length > 0 ? options.roots : [cwd];
|
|
10649
|
+
const context = {
|
|
10650
|
+
cwd,
|
|
10651
|
+
roots,
|
|
10652
|
+
log: options.log ?? ((line) => process.stderr.write(`${line}
|
|
10653
|
+
`))
|
|
10654
|
+
};
|
|
10655
|
+
return createMcpServer({
|
|
10656
|
+
info: {
|
|
10657
|
+
name: GENERATOR_NAME,
|
|
10658
|
+
title: "codebase-snapshot",
|
|
10659
|
+
version: GENERATOR_VERSION,
|
|
10660
|
+
websiteUrl: PROJECT_URL
|
|
10661
|
+
},
|
|
10662
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
10663
|
+
tools: buildTools(),
|
|
10664
|
+
resources: buildResourceProvider(),
|
|
10665
|
+
prompts: buildPrompts(),
|
|
10666
|
+
context
|
|
10667
|
+
});
|
|
10668
|
+
}
|
|
10669
|
+
async function runStdioServer(options) {
|
|
10670
|
+
const log = options.log ?? ((line) => process.stderr.write(`${line}
|
|
10671
|
+
`));
|
|
10672
|
+
const server = createServer(options);
|
|
10673
|
+
const writer = createWriter(process.stdout);
|
|
10674
|
+
let writeChain = Promise.resolve();
|
|
10675
|
+
const send = (message) => {
|
|
10676
|
+
writeChain = writeChain.then(() => writer.write(message)).catch((error) => {
|
|
10677
|
+
log(`mcp: could not write a response: ${error instanceof Error ? error.message : String(error)}`);
|
|
10678
|
+
});
|
|
10679
|
+
return writeChain;
|
|
10680
|
+
};
|
|
10681
|
+
let inFlight = 0;
|
|
10682
|
+
let inputEnded = false;
|
|
10683
|
+
let resolveDone = () => void 0;
|
|
10684
|
+
const finished = new Promise((resolve9) => {
|
|
10685
|
+
resolveDone = resolve9;
|
|
10686
|
+
});
|
|
10687
|
+
const finishIfIdle = () => {
|
|
10688
|
+
if (inputEnded && inFlight === 0) resolveDone(0);
|
|
10689
|
+
};
|
|
10690
|
+
const handleLine = async (line) => {
|
|
10691
|
+
let message;
|
|
10692
|
+
try {
|
|
10693
|
+
message = JSON.parse(line);
|
|
10694
|
+
} catch {
|
|
10695
|
+
await send(errorResponse(null, ERROR_CODES.parseError, "Message is not valid JSON."));
|
|
10696
|
+
return;
|
|
10697
|
+
}
|
|
10698
|
+
const response = await server.handle(message);
|
|
10699
|
+
if (response) await send(response);
|
|
10700
|
+
};
|
|
10701
|
+
const stopReading = createLineReader(process.stdin, {
|
|
10702
|
+
onOverflow: (bytes) => log(`mcp: discarded an oversized message (${bytes} bytes)`),
|
|
10703
|
+
onLine: (line) => {
|
|
10704
|
+
inFlight += 1;
|
|
10705
|
+
void handleLine(line).finally(() => {
|
|
10706
|
+
inFlight -= 1;
|
|
10707
|
+
finishIfIdle();
|
|
10708
|
+
});
|
|
10709
|
+
}
|
|
10710
|
+
});
|
|
10711
|
+
const onEnd = () => {
|
|
10712
|
+
inputEnded = true;
|
|
10713
|
+
finishIfIdle();
|
|
10714
|
+
};
|
|
10715
|
+
process.stdin.on("end", onEnd);
|
|
10716
|
+
process.stdin.on("close", onEnd);
|
|
10717
|
+
log(`mcp: serving ${options.cwd} over stdio`);
|
|
10718
|
+
const code = await finished;
|
|
10719
|
+
stopReading();
|
|
10720
|
+
process.stdin.off("end", onEnd);
|
|
10721
|
+
process.stdin.off("close", onEnd);
|
|
10722
|
+
await writeChain;
|
|
10723
|
+
return code;
|
|
10724
|
+
}
|
|
10725
|
+
|
|
8747
10726
|
// src/cli.ts
|
|
8748
10727
|
var VERSION = GENERATOR_VERSION;
|
|
8749
10728
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -8757,7 +10736,8 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
|
|
|
8757
10736
|
["explain", "explain"],
|
|
8758
10737
|
["diff", "diff"],
|
|
8759
10738
|
["describe", "describe"],
|
|
8760
|
-
["skill", "skill"]
|
|
10739
|
+
["skill", "skill"],
|
|
10740
|
+
["mcp", "mcp"]
|
|
8761
10741
|
]);
|
|
8762
10742
|
var COMMAND_ARITY = {
|
|
8763
10743
|
generate: 0,
|
|
@@ -8768,7 +10748,8 @@ var COMMAND_ARITY = {
|
|
|
8768
10748
|
explain: 1,
|
|
8769
10749
|
diff: 2,
|
|
8770
10750
|
describe: 0,
|
|
8771
|
-
skill: 0
|
|
10751
|
+
skill: 0,
|
|
10752
|
+
mcp: 0
|
|
8772
10753
|
};
|
|
8773
10754
|
var CLI_OPTIONS = {
|
|
8774
10755
|
cwd: { type: "string" },
|
|
@@ -8791,6 +10772,7 @@ var CLI_OPTIONS = {
|
|
|
8791
10772
|
exclude: { type: "string" },
|
|
8792
10773
|
yes: { type: "boolean" },
|
|
8793
10774
|
force: { type: "boolean" },
|
|
10775
|
+
autopilot: { type: "boolean" },
|
|
8794
10776
|
install: { type: "boolean" },
|
|
8795
10777
|
"no-install": { type: "boolean" },
|
|
8796
10778
|
target: { type: "string" },
|
|
@@ -8806,6 +10788,14 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
|
|
|
8806
10788
|
Usage:
|
|
8807
10789
|
codebase-snapshot [command] [options]
|
|
8808
10790
|
|
|
10791
|
+
First run:
|
|
10792
|
+
codebase-snapshot No config yet: asks once, sets the project up, generates.
|
|
10793
|
+
A configured project just generates. It never falls back
|
|
10794
|
+
to defaults it was not given.
|
|
10795
|
+
codebase-snapshot --autopilot The same without questions: detect the preset, write the
|
|
10796
|
+
config, add the npm scripts and devDependency, install,
|
|
10797
|
+
then generate.
|
|
10798
|
+
|
|
8809
10799
|
Commands:
|
|
8810
10800
|
setup Everything at once: config, scripts, dependency, install, generate
|
|
8811
10801
|
generate (default) Generate a complete partitioned codebase snapshot
|
|
@@ -8816,6 +10806,7 @@ Commands:
|
|
|
8816
10806
|
diff <a> <b> Compare two snapshots (directories or manifest.json files)
|
|
8817
10807
|
describe Print the machine-readable contract (--format json for agents)
|
|
8818
10808
|
skill Print the bundled agent skill; --install writes it into the repo
|
|
10809
|
+
mcp Serve this repository to an MCP client over stdio
|
|
8819
10810
|
|
|
8820
10811
|
Options:
|
|
8821
10812
|
--cwd <path> Target repository directory (default: current directory)
|
|
@@ -8838,6 +10829,12 @@ Options:
|
|
|
8838
10829
|
auto: git when usable. git: require git. walk: read
|
|
8839
10830
|
.gitignore directly (escape hatch for a broken .git)
|
|
8840
10831
|
|
|
10832
|
+
generate only:
|
|
10833
|
+
--autopilot Set the project up without asking, then generate: write the
|
|
10834
|
+
config and .gitignore, add the npm scripts and devDependency,
|
|
10835
|
+
and install with the project's own package manager.
|
|
10836
|
+
--no-install With --autopilot: skip the dependency install.
|
|
10837
|
+
|
|
8841
10838
|
init only:
|
|
8842
10839
|
--preset <name> Force a project preset instead of detecting one
|
|
8843
10840
|
--include <globs> Comma-separated allow-list
|
|
@@ -8917,10 +10914,10 @@ function parseCliArgs(argv) {
|
|
|
8917
10914
|
}
|
|
8918
10915
|
if (values.cwd !== void 0) {
|
|
8919
10916
|
const target = String(values.cwd);
|
|
8920
|
-
if (!
|
|
10917
|
+
if (!fs16.existsSync(target)) {
|
|
8921
10918
|
throw new UsageError(`--cwd directory does not exist: ${target}`);
|
|
8922
10919
|
}
|
|
8923
|
-
if (!
|
|
10920
|
+
if (!fs16.statSync(target).isDirectory()) {
|
|
8924
10921
|
throw new UsageError(`--cwd is not a directory: ${target}`);
|
|
8925
10922
|
}
|
|
8926
10923
|
}
|
|
@@ -9040,8 +11037,17 @@ function parseCliArgs(argv) {
|
|
|
9040
11037
|
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
9041
11038
|
);
|
|
9042
11039
|
}
|
|
11040
|
+
if (values["no-install"] === true && command === "generate" && values.autopilot !== true) {
|
|
11041
|
+
throw new UsageError("--no-install only applies to `setup`, or to `generate --autopilot`.");
|
|
11042
|
+
}
|
|
11043
|
+
if (values.autopilot === true && values["no-config"] === true) {
|
|
11044
|
+
throw new UsageError(
|
|
11045
|
+
"--autopilot sets a project up and writes a config; --no-config ignores config files. Pass one or the other."
|
|
11046
|
+
);
|
|
11047
|
+
}
|
|
9043
11048
|
return {
|
|
9044
11049
|
command,
|
|
11050
|
+
explicitCommand: firstPositional !== void 0 || explicitMode !== void 0,
|
|
9045
11051
|
args,
|
|
9046
11052
|
format: rawFormat,
|
|
9047
11053
|
dryRun: values["dry-run"] === true,
|
|
@@ -9053,10 +11059,11 @@ function parseCliArgs(argv) {
|
|
|
9053
11059
|
exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
|
|
9054
11060
|
yes: values.yes === true,
|
|
9055
11061
|
force: values.force === true,
|
|
11062
|
+
autopilot: values.autopilot === true,
|
|
9056
11063
|
install: values.install === true,
|
|
9057
11064
|
noInstall: values["no-install"] === true,
|
|
9058
11065
|
target: values.target === void 0 ? void 0 : String(values.target),
|
|
9059
|
-
cwd: values.cwd === void 0 ? process.cwd() :
|
|
11066
|
+
cwd: values.cwd === void 0 ? process.cwd() : path17.resolve(String(values.cwd)),
|
|
9060
11067
|
outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
|
|
9061
11068
|
includeMonolith: values["include-monolith"] === true || onlyNamesMonolith === true ? true : void 0,
|
|
9062
11069
|
includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
|
|
@@ -9113,26 +11120,29 @@ function splitList(value) {
|
|
|
9113
11120
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
9114
11121
|
}
|
|
9115
11122
|
function displayOutPath(cwd, outDir, ...segments) {
|
|
9116
|
-
const absolute =
|
|
9117
|
-
const
|
|
9118
|
-
if (
|
|
11123
|
+
const absolute = path17.resolve(cwd, outDir, ...segments);
|
|
11124
|
+
const relative11 = path17.relative(cwd, absolute);
|
|
11125
|
+
if (relative11 && !relative11.startsWith("..") && !path17.isAbsolute(relative11)) return relative11;
|
|
9119
11126
|
return absolute;
|
|
9120
11127
|
}
|
|
9121
11128
|
function nextStepHint(cwd, scriptsAddedNow) {
|
|
9122
11129
|
let hasScript = scriptsAddedNow;
|
|
9123
11130
|
if (!hasScript) {
|
|
9124
11131
|
try {
|
|
9125
|
-
const parsed = JSON.parse(
|
|
11132
|
+
const parsed = JSON.parse(fs16.readFileSync(path17.join(cwd, "package.json"), "utf8"));
|
|
9126
11133
|
hasScript = typeof parsed.scripts?.snapshot === "string";
|
|
9127
11134
|
} catch {
|
|
9128
11135
|
hasScript = false;
|
|
9129
11136
|
}
|
|
9130
11137
|
}
|
|
9131
11138
|
if (!hasScript) return `npx ${GENERATOR_NAME} generate`;
|
|
9132
|
-
const bin =
|
|
9133
|
-
const installed =
|
|
11139
|
+
const bin = path17.join(cwd, "node_modules", ".bin", "codebase-snapshot");
|
|
11140
|
+
const installed = fs16.existsSync(bin) || fs16.existsSync(`${bin}.cmd`);
|
|
9134
11141
|
return installed ? "npm run snapshot" : "npm install && npm run snapshot";
|
|
9135
11142
|
}
|
|
11143
|
+
function presetSummary(init) {
|
|
11144
|
+
return init.reason === null ? `preset: ${init.preset}` : `preset: ${init.preset}, because ${init.reason}`;
|
|
11145
|
+
}
|
|
9136
11146
|
async function runInitCommand(invocation) {
|
|
9137
11147
|
try {
|
|
9138
11148
|
const result = await runInit({
|
|
@@ -9157,7 +11167,7 @@ async function runInitCommand(invocation) {
|
|
|
9157
11167
|
info(result.configText);
|
|
9158
11168
|
return 0;
|
|
9159
11169
|
}
|
|
9160
|
-
info(`\u2705 Created ${
|
|
11170
|
+
info(`\u2705 Created ${path17.basename(result.configPath)} (${presetSummary(result)})`);
|
|
9161
11171
|
if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
|
|
9162
11172
|
if (result.dependency.added && result.dependency.range !== null) {
|
|
9163
11173
|
info(` Added devDependency: ${result.dependency.name} ${result.dependency.range}`);
|
|
@@ -9177,7 +11187,7 @@ async function runInitCommand(invocation) {
|
|
|
9177
11187
|
}
|
|
9178
11188
|
}
|
|
9179
11189
|
function detectPackageManager(cwd) {
|
|
9180
|
-
const has = (name) =>
|
|
11190
|
+
const has = (name) => fs16.existsSync(path17.join(cwd, name));
|
|
9181
11191
|
if (has("bun.lockb") || has("bun.lock")) return { name: "bun", command: "bun", args: ["install"] };
|
|
9182
11192
|
if (has("pnpm-lock.yaml")) return { name: "pnpm", command: "pnpm", args: ["install"] };
|
|
9183
11193
|
if (has("yarn.lock")) return { name: "yarn", command: "yarn", args: ["install"] };
|
|
@@ -9228,8 +11238,9 @@ async function captureStdout(fn) {
|
|
|
9228
11238
|
process.stdout.write = original;
|
|
9229
11239
|
}
|
|
9230
11240
|
}
|
|
9231
|
-
async function runSetupCommand(invocation) {
|
|
11241
|
+
async function runSetupCommand(invocation, options = {}) {
|
|
9232
11242
|
try {
|
|
11243
|
+
const interactive = options.interactive === true;
|
|
9233
11244
|
const init = await runInit({
|
|
9234
11245
|
cwd: invocation.cwd,
|
|
9235
11246
|
preset: invocation.preset,
|
|
@@ -9239,7 +11250,7 @@ async function runSetupCommand(invocation) {
|
|
|
9239
11250
|
exclude: splitList(invocation.exclude),
|
|
9240
11251
|
skipArtifacts: invocation.skipArtifacts,
|
|
9241
11252
|
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
9242
|
-
yes:
|
|
11253
|
+
yes: !interactive,
|
|
9243
11254
|
force: invocation.force,
|
|
9244
11255
|
reuseConfig: true,
|
|
9245
11256
|
dryRun: invocation.dryRun
|
|
@@ -9266,7 +11277,7 @@ async function runSetupCommand(invocation) {
|
|
|
9266
11277
|
}
|
|
9267
11278
|
info("\n\u{1F9EA} Dry run \u2014 setup would:");
|
|
9268
11279
|
info(
|
|
9269
|
-
` 1. ${init.reused ? `keep ${
|
|
11280
|
+
` 1. ${init.reused ? `keep ${path17.basename(init.configPath)}` : `write ${path17.basename(init.configPath)} (preset: ${init.preset})`}`
|
|
9270
11281
|
);
|
|
9271
11282
|
info(" 2. add the npm scripts, the devDependency and the .gitignore entries");
|
|
9272
11283
|
if (manager !== null) info(` 3. install with ${manager}`);
|
|
@@ -9276,11 +11287,11 @@ async function runSetupCommand(invocation) {
|
|
|
9276
11287
|
return 0;
|
|
9277
11288
|
}
|
|
9278
11289
|
if (invocation.format !== "json") {
|
|
9279
|
-
info(`\u{1F680} Setup for: ${invocation.cwd}`);
|
|
11290
|
+
if (!interactive) info(`\u{1F680} Setup for: ${invocation.cwd}`);
|
|
9280
11291
|
if (init.reused) {
|
|
9281
|
-
info(`\u267B Keeping ${
|
|
11292
|
+
info(`\u267B Keeping ${path17.basename(init.configPath)} (${presetSummary(init)})`);
|
|
9282
11293
|
} else {
|
|
9283
|
-
info(`\u2705 Created ${
|
|
11294
|
+
info(`\u2705 Created ${path17.basename(init.configPath)} (${presetSummary(init)})`);
|
|
9284
11295
|
}
|
|
9285
11296
|
if (init.scripts.length > 0) info(` Added npm scripts: ${init.scripts.join(", ")}`);
|
|
9286
11297
|
if (init.dependency.added && init.dependency.range !== null) {
|
|
@@ -9309,6 +11320,7 @@ async function runSetupCommand(invocation) {
|
|
|
9309
11320
|
if ((invocation.skipArtifacts ?? []).length > 0) {
|
|
9310
11321
|
generateArgs.push("--skip-artifacts", (invocation.skipArtifacts ?? []).join(","));
|
|
9311
11322
|
}
|
|
11323
|
+
if (invocation.watch) generateArgs.push("--watch");
|
|
9312
11324
|
if (invocation.format === "json") {
|
|
9313
11325
|
const generated = await captureStdout(() => main(generateArgs));
|
|
9314
11326
|
let manifest = null;
|
|
@@ -9348,6 +11360,24 @@ async function runSetupCommand(invocation) {
|
|
|
9348
11360
|
return fail(invocation.format, err.message);
|
|
9349
11361
|
}
|
|
9350
11362
|
}
|
|
11363
|
+
async function firstRun(invocation) {
|
|
11364
|
+
const plan = planFirstRun({
|
|
11365
|
+
hasConfig: findConfigFile(invocation.cwd) !== null,
|
|
11366
|
+
hasPriorSnapshot: hasPriorSnapshot(invocation.cwd, invocation.outDir ?? DEFAULT_ANSWERS.outDir),
|
|
11367
|
+
// A machine must never be asked a question, and `--format json` is how a machine says so.
|
|
11368
|
+
interactive: invocation.format === "text" && isInteractive(defaultIO()),
|
|
11369
|
+
autopilot: invocation.autopilot,
|
|
11370
|
+
emptyTarget: isEffectivelyEmpty(invocation.cwd)
|
|
11371
|
+
});
|
|
11372
|
+
if (plan.action === "generate") return null;
|
|
11373
|
+
if (plan.action === "refuse") {
|
|
11374
|
+
return fail(invocation.format, firstRunMessage(plan.reason, invocation.cwd), plan.exitCode);
|
|
11375
|
+
}
|
|
11376
|
+
info(
|
|
11377
|
+
plan.interactive ? "\u2699 This project has no snapshot configuration yet." : "\u2699 --autopilot: setting this project up before generating."
|
|
11378
|
+
);
|
|
11379
|
+
return await runSetupCommand(invocation, { interactive: plan.interactive });
|
|
11380
|
+
}
|
|
9351
11381
|
function runSkillCommand(invocation) {
|
|
9352
11382
|
if (!invocation.install) {
|
|
9353
11383
|
if (invocation.format === "json") {
|
|
@@ -9369,7 +11399,7 @@ function runSkillCommand(invocation) {
|
|
|
9369
11399
|
}
|
|
9370
11400
|
info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
|
|
9371
11401
|
for (const item of result.written) {
|
|
9372
|
-
const shown =
|
|
11402
|
+
const shown = path17.relative(invocation.cwd, item.path) || item.path;
|
|
9373
11403
|
info(` - ${item.action.padEnd(9)} ${shown}`);
|
|
9374
11404
|
}
|
|
9375
11405
|
if (result.gitignored.length > 0) {
|
|
@@ -9464,9 +11494,16 @@ ${USAGE}
|
|
|
9464
11494
|
if (invocation.command === "skill") {
|
|
9465
11495
|
return runSkillCommand(invocation);
|
|
9466
11496
|
}
|
|
11497
|
+
if (invocation.command === "mcp") {
|
|
11498
|
+
return await runStdioServer({ cwd: path17.resolve(invocation.cwd) });
|
|
11499
|
+
}
|
|
9467
11500
|
if (invocation.command === "setup") {
|
|
9468
11501
|
return runSetupCommand(invocation);
|
|
9469
11502
|
}
|
|
11503
|
+
if (invocation.command === "generate" && !invocation.noConfig && (!invocation.explicitCommand || invocation.autopilot)) {
|
|
11504
|
+
const decided = await firstRun(invocation);
|
|
11505
|
+
if (decided !== null) return decided;
|
|
11506
|
+
}
|
|
9470
11507
|
const skipConfig = invocation.noConfig || invocation.command === "diff";
|
|
9471
11508
|
const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
9472
11509
|
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
@@ -9475,7 +11512,7 @@ ${USAGE}
|
|
|
9475
11512
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
9476
11513
|
return 1;
|
|
9477
11514
|
}
|
|
9478
|
-
if (loaded.file) info(`\u2699 Using config ${
|
|
11515
|
+
if (loaded.file) info(`\u2699 Using config ${path17.relative(process.cwd(), loaded.file) || loaded.file}`);
|
|
9479
11516
|
const resolved = resolveConfig(
|
|
9480
11517
|
loaded.config,
|
|
9481
11518
|
{
|
|
@@ -9497,10 +11534,10 @@ ${USAGE}
|
|
|
9497
11534
|
if (invocation.command === "validate") {
|
|
9498
11535
|
const result = validateSnapshot(resolved);
|
|
9499
11536
|
if (result.valid) {
|
|
9500
|
-
const manifestPath =
|
|
11537
|
+
const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
9501
11538
|
let manifest;
|
|
9502
11539
|
try {
|
|
9503
|
-
manifest = JSON.parse(
|
|
11540
|
+
manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
|
|
9504
11541
|
} catch {
|
|
9505
11542
|
manifest = void 0;
|
|
9506
11543
|
}
|
|
@@ -9513,13 +11550,13 @@ ${USAGE}
|
|
|
9513
11550
|
return result.valid ? 0 : 1;
|
|
9514
11551
|
}
|
|
9515
11552
|
if (invocation.command === "report") {
|
|
9516
|
-
const manifestPath =
|
|
9517
|
-
if (!
|
|
11553
|
+
const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
11554
|
+
if (!fs16.existsSync(manifestPath)) {
|
|
9518
11555
|
return fail(invocation.format, `Manifest not found at ${manifestPath}. Run generate first.`);
|
|
9519
11556
|
}
|
|
9520
11557
|
let manifest;
|
|
9521
11558
|
try {
|
|
9522
|
-
manifest = JSON.parse(
|
|
11559
|
+
manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
|
|
9523
11560
|
} catch (err) {
|
|
9524
11561
|
return fail(invocation.format, `Could not read ${manifestPath}: ${err.message}`);
|
|
9525
11562
|
}
|
|
@@ -9683,20 +11720,20 @@ function finishWatchRun(state, now) {
|
|
|
9683
11720
|
};
|
|
9684
11721
|
}
|
|
9685
11722
|
function comparablePath(value) {
|
|
9686
|
-
const absolute =
|
|
11723
|
+
const absolute = path17.resolve(value);
|
|
9687
11724
|
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
9688
11725
|
}
|
|
9689
11726
|
function isSameOrInside(parent, candidate) {
|
|
9690
|
-
const
|
|
9691
|
-
return
|
|
11727
|
+
const relative11 = path17.relative(parent, candidate);
|
|
11728
|
+
return relative11 === "" || relative11 !== ".." && !relative11.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative11);
|
|
9692
11729
|
}
|
|
9693
11730
|
function isGeneratedWatchPath(candidate, paths) {
|
|
9694
|
-
const absolute = comparablePath(
|
|
9695
|
-
const outDir = comparablePath(
|
|
9696
|
-
const tmpDir = comparablePath(
|
|
11731
|
+
const absolute = comparablePath(path17.resolve(paths.cwd, candidate));
|
|
11732
|
+
const outDir = comparablePath(path17.resolve(paths.cwd, paths.outDir));
|
|
11733
|
+
const tmpDir = comparablePath(path17.resolve(paths.cwd, paths.tmpDir));
|
|
9697
11734
|
if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
|
|
9698
|
-
const parent =
|
|
9699
|
-
if (parent ===
|
|
11735
|
+
const parent = path17.dirname(absolute);
|
|
11736
|
+
if (parent === path17.dirname(outDir) && path17.basename(absolute).startsWith(`${path17.basename(outDir)}.backup-`)) {
|
|
9700
11737
|
return true;
|
|
9701
11738
|
}
|
|
9702
11739
|
return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
|
|
@@ -9713,8 +11750,8 @@ function decideWatchEvent(state, event, paths) {
|
|
|
9713
11750
|
return { state, action: "schedule" };
|
|
9714
11751
|
}
|
|
9715
11752
|
async function runWatchLoop(invocation, resolved) {
|
|
9716
|
-
const outDir =
|
|
9717
|
-
const tmpDir =
|
|
11753
|
+
const outDir = path17.resolve(resolved.cwd, resolved.outDir);
|
|
11754
|
+
const tmpDir = path17.resolve(resolved.cwd, resolved.tmpDir);
|
|
9718
11755
|
const paths = { cwd: resolved.cwd, outDir, tmpDir };
|
|
9719
11756
|
let state = initialWatchState();
|
|
9720
11757
|
let timer = null;
|
|
@@ -9779,11 +11816,11 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
9779
11816
|
};
|
|
9780
11817
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
9781
11818
|
void regenerate();
|
|
9782
|
-
const watcher =
|
|
11819
|
+
const watcher = fs16.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
9783
11820
|
const decision = decideWatchEvent(
|
|
9784
11821
|
state,
|
|
9785
11822
|
{
|
|
9786
|
-
path: filename ?
|
|
11823
|
+
path: filename ? path17.resolve(resolved.cwd, filename.toString()) : null,
|
|
9787
11824
|
now: performance.now()
|
|
9788
11825
|
},
|
|
9789
11826
|
paths
|
|
@@ -9798,12 +11835,12 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
9798
11835
|
}
|
|
9799
11836
|
if (decision.action === "schedule") schedule();
|
|
9800
11837
|
});
|
|
9801
|
-
await new Promise((
|
|
11838
|
+
await new Promise((resolve9) => {
|
|
9802
11839
|
const stop = () => {
|
|
9803
11840
|
stopped = true;
|
|
9804
11841
|
if (timer) clearTimeout(timer);
|
|
9805
11842
|
watcher.close();
|
|
9806
|
-
|
|
11843
|
+
resolve9();
|
|
9807
11844
|
};
|
|
9808
11845
|
process.once("SIGINT", stop);
|
|
9809
11846
|
process.once("SIGTERM", stop);
|
|
@@ -9811,7 +11848,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
9811
11848
|
return 0;
|
|
9812
11849
|
}
|
|
9813
11850
|
var entryFile = process.argv[1] ?? "";
|
|
9814
|
-
var entryName =
|
|
11851
|
+
var entryName = path17.basename(entryFile).toLowerCase();
|
|
9815
11852
|
var invokedDirectly = entryName === "cli.js" || entryName === "cli.cjs";
|
|
9816
11853
|
if (invokedDirectly) {
|
|
9817
11854
|
void main().then(
|