codebase-snapshot 0.9.1 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +219 -0
- package/README.md +141 -127
- package/dist/bin.js +2617 -492
- package/dist/chunk-4AHHSNGG.js +2577 -0
- package/dist/{chunk-HT7J3NWT.js → chunk-B336MMNC.js} +599 -379
- package/dist/cli.cjs +2703 -600
- package/dist/cli.d.cts +11 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +71 -754
- package/dist/index.cjs +587 -377
- 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 +95 -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.5";
|
|
4298
4350
|
var LEGACY_GENERATOR_NAMES = ["@ankurhalder/codebase-snapshot"];
|
|
4299
4351
|
function isOurGeneratorName(name) {
|
|
4300
4352
|
if (typeof name !== "string") return false;
|
|
@@ -4435,6 +4487,12 @@ var PromptCancelled = class extends Error {
|
|
|
4435
4487
|
this.name = "PromptCancelled";
|
|
4436
4488
|
}
|
|
4437
4489
|
};
|
|
4490
|
+
var PromptBack = class extends Error {
|
|
4491
|
+
constructor() {
|
|
4492
|
+
super("Prompt back");
|
|
4493
|
+
this.name = "PromptBack";
|
|
4494
|
+
}
|
|
4495
|
+
};
|
|
4438
4496
|
function defaultIO() {
|
|
4439
4497
|
return { input: process.stdin, output: process.stdout };
|
|
4440
4498
|
}
|
|
@@ -4458,35 +4516,40 @@ function choiceLine(choice, active) {
|
|
|
4458
4516
|
const hint = choice.hint ? ` ${choice.hint}` : "";
|
|
4459
4517
|
return ` ${marker(active)} ${choice.label}${hint}`;
|
|
4460
4518
|
}
|
|
4461
|
-
function
|
|
4519
|
+
function listFooter(base, allowBack) {
|
|
4520
|
+
return allowBack ? `${base} \xB7 \u2190 back` : base;
|
|
4521
|
+
}
|
|
4522
|
+
function renderSelect(message, choices, index, allowBack = false) {
|
|
4462
4523
|
const lines = [`\u25C6 ${message}`, ""];
|
|
4463
4524
|
choices.forEach((choice, position) => lines.push(choiceLine(choice, position === index)));
|
|
4464
|
-
lines.push("", "
|
|
4525
|
+
lines.push("", ` ${listFooter("\u2191\u2193 move \xB7 enter select", allowBack)}`);
|
|
4465
4526
|
return lines.join("\n");
|
|
4466
4527
|
}
|
|
4467
|
-
function renderMultiSelect(message, choices, index, selected) {
|
|
4528
|
+
function renderMultiSelect(message, choices, index, selected, allowBack = false) {
|
|
4468
4529
|
const lines = [`\u25C6 ${message}`, ""];
|
|
4469
4530
|
choices.forEach((choice, position) => {
|
|
4470
4531
|
const box = selected.has(position) ? "\u25FC" : "\u25FB";
|
|
4471
4532
|
const hint = choice.hint ? ` ${choice.hint}` : "";
|
|
4472
4533
|
lines.push(` ${marker(position === index)} ${box} ${choice.label}${hint}`);
|
|
4473
4534
|
});
|
|
4474
|
-
lines.push("", "
|
|
4535
|
+
lines.push("", ` ${listFooter("\u2191\u2193 move \xB7 space toggle \xB7 enter confirm", allowBack)}`);
|
|
4475
4536
|
return lines.join("\n");
|
|
4476
4537
|
}
|
|
4477
|
-
function renderConfirm(message, value) {
|
|
4538
|
+
function renderConfirm(message, value, allowBack = false) {
|
|
4539
|
+
const footer = allowBack ? "\u2190\u2192 or y/n \xB7 enter confirm \xB7 b back" : "\u2190\u2192 or y/n \xB7 enter confirm";
|
|
4478
4540
|
return [
|
|
4479
4541
|
`\u25C6 ${message}`,
|
|
4480
4542
|
"",
|
|
4481
4543
|
` ${marker(value)} Yes`,
|
|
4482
4544
|
` ${marker(!value)} No`,
|
|
4483
4545
|
"",
|
|
4484
|
-
|
|
4546
|
+
` ${footer}`
|
|
4485
4547
|
].join("\n");
|
|
4486
4548
|
}
|
|
4487
|
-
function renderText(message, value, error) {
|
|
4549
|
+
function renderText(message, value, error, allowBack = false) {
|
|
4488
4550
|
const lines = [`\u25C6 ${message}`, "", ` ${value || "\u258F"}`];
|
|
4489
4551
|
if (error) lines.push("", ` \u2716 ${error}`);
|
|
4552
|
+
if (allowBack) lines.push("", " enter confirm \xB7 \u2190 back");
|
|
4490
4553
|
return lines.join("\n");
|
|
4491
4554
|
}
|
|
4492
4555
|
function keyReader(input) {
|
|
@@ -4506,7 +4569,7 @@ function keyReader(input) {
|
|
|
4506
4569
|
next() {
|
|
4507
4570
|
const queued = queue.shift();
|
|
4508
4571
|
if (queued) return Promise.resolve(queued);
|
|
4509
|
-
return new Promise((
|
|
4572
|
+
return new Promise((resolve9) => waiters.push(resolve9));
|
|
4510
4573
|
},
|
|
4511
4574
|
close() {
|
|
4512
4575
|
input.off("keypress", onKey);
|
|
@@ -4535,14 +4598,18 @@ var Frame = class {
|
|
|
4535
4598
|
function isCancel(key) {
|
|
4536
4599
|
return key.ctrl === true && key.name === "c" || key.ctrl === true && key.name === "d";
|
|
4537
4600
|
}
|
|
4601
|
+
function isListBack(key, allowBack) {
|
|
4602
|
+
return allowBack && (key.name === "left" || key.name === "b");
|
|
4603
|
+
}
|
|
4538
4604
|
async function select(io, message, choices, options = {}) {
|
|
4539
4605
|
if (choices.length === 0) throw new Error("select() requires at least one choice.");
|
|
4606
|
+
const allowBack = options.allowBack === true;
|
|
4540
4607
|
const fallbackIndex = Math.max(
|
|
4541
4608
|
0,
|
|
4542
4609
|
choices.findIndex((choice) => choice.value === options.defaultValue)
|
|
4543
4610
|
);
|
|
4544
4611
|
if (!isInteractive(io)) {
|
|
4545
|
-
io.output.write(`${renderSelect(message, choices, fallbackIndex)}
|
|
4612
|
+
io.output.write(`${renderSelect(message, choices, fallbackIndex, allowBack)}
|
|
4546
4613
|
`);
|
|
4547
4614
|
return choices[fallbackIndex].value;
|
|
4548
4615
|
}
|
|
@@ -4551,9 +4618,10 @@ async function select(io, message, choices, options = {}) {
|
|
|
4551
4618
|
let index = fallbackIndex;
|
|
4552
4619
|
try {
|
|
4553
4620
|
for (; ; ) {
|
|
4554
|
-
frame.draw(renderSelect(message, choices, index));
|
|
4621
|
+
frame.draw(renderSelect(message, choices, index, allowBack));
|
|
4555
4622
|
const key = await reader.next();
|
|
4556
4623
|
if (isCancel(key)) throw new PromptCancelled();
|
|
4624
|
+
if (isListBack(key, allowBack)) throw new PromptBack();
|
|
4557
4625
|
if (key.name === "up") index = moveSelection(index, -1, choices.length);
|
|
4558
4626
|
else if (key.name === "down") index = moveSelection(index, 1, choices.length);
|
|
4559
4627
|
else if (key.name === "return" || key.name === "enter") {
|
|
@@ -4562,18 +4630,22 @@ async function select(io, message, choices, options = {}) {
|
|
|
4562
4630
|
} else if (key.name === "j") index = moveSelection(index, 1, choices.length);
|
|
4563
4631
|
else if (key.name === "k") index = moveSelection(index, -1, choices.length);
|
|
4564
4632
|
}
|
|
4633
|
+
} catch (error) {
|
|
4634
|
+
frame.erase();
|
|
4635
|
+
throw error;
|
|
4565
4636
|
} finally {
|
|
4566
4637
|
reader.close();
|
|
4567
4638
|
}
|
|
4568
4639
|
}
|
|
4569
4640
|
async function multiSelect(io, message, choices, options = {}) {
|
|
4641
|
+
const allowBack = options.allowBack === true;
|
|
4570
4642
|
const selected = /* @__PURE__ */ new Set();
|
|
4571
4643
|
choices.forEach((choice, position) => {
|
|
4572
4644
|
if (options.defaults?.includes(choice.value)) selected.add(position);
|
|
4573
4645
|
});
|
|
4574
4646
|
const result = () => choices.filter((_, position) => selected.has(position)).map((choice) => choice.value);
|
|
4575
4647
|
if (!isInteractive(io)) {
|
|
4576
|
-
io.output.write(`${renderMultiSelect(message, choices, 0, selected)}
|
|
4648
|
+
io.output.write(`${renderMultiSelect(message, choices, 0, selected, allowBack)}
|
|
4577
4649
|
`);
|
|
4578
4650
|
return result();
|
|
4579
4651
|
}
|
|
@@ -4582,9 +4654,10 @@ async function multiSelect(io, message, choices, options = {}) {
|
|
|
4582
4654
|
let index = 0;
|
|
4583
4655
|
try {
|
|
4584
4656
|
for (; ; ) {
|
|
4585
|
-
frame.draw(renderMultiSelect(message, choices, index, selected));
|
|
4657
|
+
frame.draw(renderMultiSelect(message, choices, index, selected, allowBack));
|
|
4586
4658
|
const key = await reader.next();
|
|
4587
4659
|
if (isCancel(key)) throw new PromptCancelled();
|
|
4660
|
+
if (isListBack(key, allowBack)) throw new PromptBack();
|
|
4588
4661
|
if (key.name === "up") index = moveSelection(index, -1, choices.length);
|
|
4589
4662
|
else if (key.name === "down") index = moveSelection(index, 1, choices.length);
|
|
4590
4663
|
else if (key.name === "space") {
|
|
@@ -4599,14 +4672,18 @@ async function multiSelect(io, message, choices, options = {}) {
|
|
|
4599
4672
|
return result();
|
|
4600
4673
|
}
|
|
4601
4674
|
}
|
|
4675
|
+
} catch (error) {
|
|
4676
|
+
frame.erase();
|
|
4677
|
+
throw error;
|
|
4602
4678
|
} finally {
|
|
4603
4679
|
reader.close();
|
|
4604
4680
|
}
|
|
4605
4681
|
}
|
|
4606
4682
|
async function confirm(io, message, options = {}) {
|
|
4683
|
+
const allowBack = options.allowBack === true;
|
|
4607
4684
|
let value = options.defaultValue ?? true;
|
|
4608
4685
|
if (!isInteractive(io)) {
|
|
4609
|
-
io.output.write(`${renderConfirm(message, value)}
|
|
4686
|
+
io.output.write(`${renderConfirm(message, value, allowBack)}
|
|
4610
4687
|
`);
|
|
4611
4688
|
return value;
|
|
4612
4689
|
}
|
|
@@ -4614,9 +4691,10 @@ async function confirm(io, message, options = {}) {
|
|
|
4614
4691
|
const reader = keyReader(io.input);
|
|
4615
4692
|
try {
|
|
4616
4693
|
for (; ; ) {
|
|
4617
|
-
frame.draw(renderConfirm(message, value));
|
|
4694
|
+
frame.draw(renderConfirm(message, value, allowBack));
|
|
4618
4695
|
const key = await reader.next();
|
|
4619
4696
|
if (isCancel(key)) throw new PromptCancelled();
|
|
4697
|
+
if (allowBack && key.name === "b") throw new PromptBack();
|
|
4620
4698
|
if (key.name === "up" || key.name === "left") value = true;
|
|
4621
4699
|
else if (key.name === "down" || key.name === "right") value = false;
|
|
4622
4700
|
else if (key.name === "y") value = true;
|
|
@@ -4626,14 +4704,18 @@ async function confirm(io, message, options = {}) {
|
|
|
4626
4704
|
return value;
|
|
4627
4705
|
}
|
|
4628
4706
|
}
|
|
4707
|
+
} catch (error) {
|
|
4708
|
+
frame.erase();
|
|
4709
|
+
throw error;
|
|
4629
4710
|
} finally {
|
|
4630
4711
|
reader.close();
|
|
4631
4712
|
}
|
|
4632
4713
|
}
|
|
4633
4714
|
async function text(io, message, options = {}) {
|
|
4715
|
+
const allowBack = options.allowBack === true;
|
|
4634
4716
|
const fallback = options.defaultValue ?? "";
|
|
4635
4717
|
if (!isInteractive(io)) {
|
|
4636
|
-
io.output.write(`${renderText(message, fallback)}
|
|
4718
|
+
io.output.write(`${renderText(message, fallback, void 0, allowBack)}
|
|
4637
4719
|
`);
|
|
4638
4720
|
return fallback;
|
|
4639
4721
|
}
|
|
@@ -4643,9 +4725,10 @@ async function text(io, message, options = {}) {
|
|
|
4643
4725
|
let error;
|
|
4644
4726
|
try {
|
|
4645
4727
|
for (; ; ) {
|
|
4646
|
-
frame.draw(renderText(message, value, error));
|
|
4728
|
+
frame.draw(renderText(message, value, error, allowBack));
|
|
4647
4729
|
const key = await reader.next();
|
|
4648
4730
|
if (isCancel(key)) throw new PromptCancelled();
|
|
4731
|
+
if (allowBack && key.name === "left") throw new PromptBack();
|
|
4649
4732
|
error = void 0;
|
|
4650
4733
|
if (key.name === "return" || key.name === "enter") {
|
|
4651
4734
|
const problem = options.validate ? options.validate(value) : null;
|
|
@@ -4666,6 +4749,9 @@ async function text(io, message, options = {}) {
|
|
|
4666
4749
|
}
|
|
4667
4750
|
if (_isPrintable(key)) value += _keyText(key);
|
|
4668
4751
|
}
|
|
4752
|
+
} catch (error_) {
|
|
4753
|
+
frame.erase();
|
|
4754
|
+
throw error_;
|
|
4669
4755
|
} finally {
|
|
4670
4756
|
reader.close();
|
|
4671
4757
|
}
|
|
@@ -4861,16 +4947,18 @@ function updatePackageJson(cwd, notes) {
|
|
|
4861
4947
|
return { scripts: added, dependency };
|
|
4862
4948
|
}
|
|
4863
4949
|
var GITIGNORE_ENTRIES = ["snapshot/", "snapshot.tmp/"];
|
|
4864
|
-
function resolvePresetChoice(
|
|
4950
|
+
function resolvePresetChoice(detectedName, presetName) {
|
|
4865
4951
|
return {
|
|
4866
4952
|
value: presetName,
|
|
4867
4953
|
label: presetName,
|
|
4868
|
-
hint: presetName ===
|
|
4954
|
+
hint: presetName === detectedName ? "detected" : void 0
|
|
4869
4955
|
};
|
|
4870
4956
|
}
|
|
4871
4957
|
async function runInit(options) {
|
|
4872
4958
|
const io = options.io ?? defaultIO();
|
|
4873
4959
|
const detected = detectPreset(options.cwd, options.preset);
|
|
4960
|
+
let reason = detected.reason;
|
|
4961
|
+
let fallback = detected.fallback;
|
|
4874
4962
|
const notes = [];
|
|
4875
4963
|
const answers = {
|
|
4876
4964
|
preset: detected.presetName,
|
|
@@ -4892,73 +4980,37 @@ async function runInit(options) {
|
|
|
4892
4980
|
if (interactive) {
|
|
4893
4981
|
io.output.write("\n codebase-snapshot\n");
|
|
4894
4982
|
io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
|
|
4895
|
-
|
|
4983
|
+
if (fallback) {
|
|
4984
|
+
io.output.write(" \u26A0 Could not tell what kind of project this is.\n");
|
|
4985
|
+
io.output.write(" No package.json, go.mod, Cargo.toml, pyproject.toml, Gemfile,\n");
|
|
4986
|
+
io.output.write(" composer.json, pom.xml, *.csproj, next.config.*, vite.config.*, *.tf\n");
|
|
4987
|
+
io.output.write(" or workspace marker was found here.\n\n");
|
|
4988
|
+
} else {
|
|
4989
|
+
io.output.write(` Detected ${detected.presetName}
|
|
4896
4990
|
`);
|
|
4897
|
-
|
|
4991
|
+
io.output.write(` Because ${reason}
|
|
4898
4992
|
|
|
4899
4993
|
`);
|
|
4900
|
-
const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
|
|
4901
|
-
defaultValue: true
|
|
4902
|
-
});
|
|
4903
|
-
if (!useDetected) {
|
|
4904
|
-
answers.preset = await select(
|
|
4905
|
-
io,
|
|
4906
|
-
"What kind of project is this?",
|
|
4907
|
-
listPresetNames().map((name) => resolvePresetChoice(answers, name))
|
|
4908
|
-
);
|
|
4909
|
-
const chosen = detectPreset(options.cwd, answers.preset);
|
|
4910
|
-
answers.preset = chosen.presetName;
|
|
4911
|
-
}
|
|
4912
|
-
const scope = await select(
|
|
4913
|
-
io,
|
|
4914
|
-
"What should go into the snapshot?",
|
|
4915
|
-
[
|
|
4916
|
-
{ value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
|
|
4917
|
-
{ value: "folders", label: "Only specific folders\u2026" },
|
|
4918
|
-
{ value: "no-tests", label: "Everything, but skip tests" },
|
|
4919
|
-
{ value: "custom", label: "Let me write glob patterns" }
|
|
4920
|
-
],
|
|
4921
|
-
{ defaultValue: "all" }
|
|
4922
|
-
);
|
|
4923
|
-
if (scope === "folders") {
|
|
4924
|
-
const dirs = topLevelDirs(options.cwd).slice(0, 40);
|
|
4925
|
-
if (dirs.length === 0) {
|
|
4926
|
-
notes.push("No top-level directories found; the snapshot will include everything.");
|
|
4927
|
-
} else {
|
|
4928
|
-
const chosen = await multiSelect(
|
|
4929
|
-
io,
|
|
4930
|
-
"Select folders",
|
|
4931
|
-
dirs.map((name) => ({ value: name, label: `${name}/` })),
|
|
4932
|
-
{ defaults: dirs }
|
|
4933
|
-
);
|
|
4934
|
-
answers.include = chosen.map((name) => `${name}/**`);
|
|
4935
|
-
}
|
|
4936
|
-
} else if (scope === "no-tests") {
|
|
4937
|
-
answers.excludePatterns = [
|
|
4938
|
-
...answers.excludePatterns,
|
|
4939
|
-
"**/*.test.*",
|
|
4940
|
-
"**/*.spec.*",
|
|
4941
|
-
"tests",
|
|
4942
|
-
"test"
|
|
4943
|
-
];
|
|
4944
|
-
} else if (scope === "custom") {
|
|
4945
|
-
const raw = await text(io, "Glob patterns to include (comma separated)", {
|
|
4946
|
-
defaultValue: ""
|
|
4947
|
-
});
|
|
4948
|
-
answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
4949
|
-
}
|
|
4950
|
-
const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
|
|
4951
|
-
defaultValue: "120k"
|
|
4952
|
-
});
|
|
4953
|
-
if (sizeChoice === "custom") {
|
|
4954
|
-
const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
|
|
4955
|
-
defaultValue: String(answers.maxPartBytes),
|
|
4956
|
-
validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
|
|
4957
|
-
});
|
|
4958
|
-
answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
|
|
4959
|
-
} else {
|
|
4960
|
-
answers.maxPartBytes = sizeChoice;
|
|
4961
4994
|
}
|
|
4995
|
+
io.output.write(` Directory ${options.cwd}
|
|
4996
|
+
|
|
4997
|
+
`);
|
|
4998
|
+
const paceChoices = fallback ? [
|
|
4999
|
+
{ value: "choose", label: "Pick the closest preset", hint: "then generate with the defaults" },
|
|
5000
|
+
{ value: "recommended", label: "Use the defaults", hint: "generic \xB7 everything the scanner finds" },
|
|
5001
|
+
{ value: "walk", label: "Go through each setting", hint: "every question starts on its default" }
|
|
5002
|
+
] : [
|
|
5003
|
+
{
|
|
5004
|
+
value: "recommended",
|
|
5005
|
+
label: "Use the recommended setup",
|
|
5006
|
+
hint: `${detected.presetName} \xB7 config \xB7 npm scripts \xB7 install \xB7 generate`
|
|
5007
|
+
},
|
|
5008
|
+
{ value: "walk", label: "Go through each setting", hint: "every question starts on its default" },
|
|
5009
|
+
{ value: "choose", label: "Pick a different preset", hint: "everything else stays at the defaults" }
|
|
5010
|
+
];
|
|
5011
|
+
const baseInclude = [...answers.include];
|
|
5012
|
+
const baseExclude = [...answers.excludePatterns];
|
|
5013
|
+
let lastScope = "all";
|
|
4962
5014
|
const artifactChoices = [
|
|
4963
5015
|
{ value: "readme", label: "README.md", hint: "orientation page" },
|
|
4964
5016
|
{ value: "manifest", label: "manifest.json", hint: "required by validate and report" },
|
|
@@ -4968,36 +5020,158 @@ async function runInit(options) {
|
|
|
4968
5020
|
{ value: "audit", label: "reports/audit.md", hint: "security and health findings" },
|
|
4969
5021
|
{ value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
|
|
4970
5022
|
];
|
|
4971
|
-
const
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5023
|
+
const scopeStep = async () => {
|
|
5024
|
+
for (; ; ) {
|
|
5025
|
+
const scope = await select(
|
|
5026
|
+
io,
|
|
5027
|
+
"What should go into the snapshot?",
|
|
5028
|
+
[
|
|
5029
|
+
{ value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
|
|
5030
|
+
{ value: "folders", label: "Only specific folders\u2026" },
|
|
5031
|
+
{ value: "no-tests", label: "Everything, but skip tests" },
|
|
5032
|
+
{ value: "custom", label: "Let me write glob patterns" }
|
|
5033
|
+
],
|
|
5034
|
+
{ defaultValue: lastScope, allowBack: true }
|
|
5035
|
+
);
|
|
5036
|
+
lastScope = scope;
|
|
5037
|
+
answers.include = [...baseInclude];
|
|
5038
|
+
answers.excludePatterns = [...baseExclude];
|
|
5039
|
+
try {
|
|
5040
|
+
if (scope === "folders") {
|
|
5041
|
+
const dirs = topLevelDirs(options.cwd).slice(0, 40);
|
|
5042
|
+
if (dirs.length === 0) {
|
|
5043
|
+
notes.push("No top-level directories found; the snapshot will include everything.");
|
|
5044
|
+
} else {
|
|
5045
|
+
const chosen = await multiSelect(
|
|
5046
|
+
io,
|
|
5047
|
+
"Select folders",
|
|
5048
|
+
dirs.map((name) => ({ value: name, label: `${name}/` })),
|
|
5049
|
+
{ defaults: dirs, allowBack: true }
|
|
5050
|
+
);
|
|
5051
|
+
answers.include = chosen.map((name) => `${name}/**`);
|
|
5052
|
+
}
|
|
5053
|
+
} else if (scope === "no-tests") {
|
|
5054
|
+
answers.excludePatterns = [...baseExclude, "**/*.test.*", "**/*.spec.*", "tests", "test"];
|
|
5055
|
+
} else if (scope === "custom") {
|
|
5056
|
+
const raw = await text(io, "Glob patterns to include (comma separated)", {
|
|
5057
|
+
defaultValue: baseInclude.join(", "),
|
|
5058
|
+
allowBack: true
|
|
5059
|
+
});
|
|
5060
|
+
answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
5061
|
+
}
|
|
5062
|
+
return;
|
|
5063
|
+
} catch (error) {
|
|
5064
|
+
if (error instanceof PromptBack) continue;
|
|
5065
|
+
throw error;
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
};
|
|
5069
|
+
const sizeStep = async () => {
|
|
5070
|
+
for (; ; ) {
|
|
5071
|
+
const currentBudget = String(answers.maxPartBytes);
|
|
5072
|
+
const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
|
|
5073
|
+
defaultValue: SIZE_CHOICES.some((choice) => choice.value === currentBudget) ? currentBudget : "custom",
|
|
5074
|
+
allowBack: true
|
|
5075
|
+
});
|
|
5076
|
+
if (sizeChoice !== "custom") {
|
|
5077
|
+
answers.maxPartBytes = sizeChoice;
|
|
5078
|
+
return;
|
|
5079
|
+
}
|
|
5080
|
+
try {
|
|
5081
|
+
const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
|
|
5082
|
+
defaultValue: currentBudget,
|
|
5083
|
+
validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null,
|
|
5084
|
+
allowBack: true
|
|
5085
|
+
});
|
|
5086
|
+
answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
|
|
5087
|
+
return;
|
|
5088
|
+
} catch (error) {
|
|
5089
|
+
if (error instanceof PromptBack) continue;
|
|
5090
|
+
throw error;
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
};
|
|
5094
|
+
const artifactsStep = async () => {
|
|
5095
|
+
const enabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
|
|
5096
|
+
const selectedArtifacts = await multiSelect(
|
|
5097
|
+
io,
|
|
5098
|
+
"Which artifacts should be generated?",
|
|
5099
|
+
artifactChoices,
|
|
5100
|
+
{ defaults: enabled, allowBack: true }
|
|
5101
|
+
);
|
|
5102
|
+
for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
|
|
5103
|
+
};
|
|
5104
|
+
const credentialsStep = async () => {
|
|
5105
|
+
answers.includeSensitiveFiles = await confirm(
|
|
5106
|
+
io,
|
|
5107
|
+
"Include credential files (.env, *.pem, *.npmrc, *.ssh/)?",
|
|
5108
|
+
{ defaultValue: answers.includeSensitiveFiles, allowBack: true }
|
|
5109
|
+
);
|
|
5110
|
+
};
|
|
5111
|
+
const reviewStep = async () => {
|
|
5112
|
+
const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
|
|
5113
|
+
io.output.write("\n Review\n\n");
|
|
5114
|
+
io.output.write(` project ${answers.preset}
|
|
4987
5115
|
`);
|
|
4988
|
-
|
|
5116
|
+
io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
|
|
4989
5117
|
`);
|
|
4990
|
-
|
|
5118
|
+
io.output.write(` part size ${String(answers.maxPartBytes)}
|
|
4991
5119
|
`);
|
|
4992
|
-
|
|
5120
|
+
io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
|
|
4993
5121
|
`);
|
|
4994
|
-
|
|
5122
|
+
io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
|
|
4995
5123
|
|
|
4996
5124
|
`);
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5125
|
+
io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
|
|
5126
|
+
io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
|
|
5127
|
+
const proceed = await confirm(io, "Write this and continue?", {
|
|
5128
|
+
defaultValue: true,
|
|
5129
|
+
allowBack: true
|
|
5130
|
+
});
|
|
5131
|
+
if (!proceed) throw new PromptBack();
|
|
5132
|
+
};
|
|
5133
|
+
const walk = async () => {
|
|
5134
|
+
const steps = [scopeStep, sizeStep, artifactsStep, credentialsStep, reviewStep];
|
|
5135
|
+
let index = 0;
|
|
5136
|
+
while (index < steps.length) {
|
|
5137
|
+
try {
|
|
5138
|
+
await steps[index]();
|
|
5139
|
+
index += 1;
|
|
5140
|
+
} catch (error) {
|
|
5141
|
+
if (!(error instanceof PromptBack)) throw error;
|
|
5142
|
+
if (index === 0) return false;
|
|
5143
|
+
index -= 1;
|
|
5144
|
+
io.output.write("\n");
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
return true;
|
|
5148
|
+
};
|
|
5149
|
+
for (; ; ) {
|
|
5150
|
+
const pace = await select(io, "How should I set this up?", paceChoices, {
|
|
5151
|
+
defaultValue: fallback ? "choose" : "recommended"
|
|
5152
|
+
});
|
|
5153
|
+
if (pace === "choose") {
|
|
5154
|
+
try {
|
|
5155
|
+
answers.preset = await select(
|
|
5156
|
+
io,
|
|
5157
|
+
"What kind of project is this?",
|
|
5158
|
+
listPresetNames().map(
|
|
5159
|
+
(name) => resolvePresetChoice(fallback ? null : detected.presetName, name)
|
|
5160
|
+
),
|
|
5161
|
+
{ defaultValue: answers.preset, allowBack: true }
|
|
5162
|
+
);
|
|
5163
|
+
} catch (error) {
|
|
5164
|
+
if (error instanceof PromptBack) continue;
|
|
5165
|
+
throw error;
|
|
5166
|
+
}
|
|
5167
|
+
const chosen = detectPreset(options.cwd, answers.preset);
|
|
5168
|
+
answers.preset = chosen.presetName;
|
|
5169
|
+
reason = null;
|
|
5170
|
+
fallback = false;
|
|
5171
|
+
}
|
|
5172
|
+
if (pace === "walk" && !await walk()) continue;
|
|
5173
|
+
break;
|
|
5174
|
+
}
|
|
5001
5175
|
}
|
|
5002
5176
|
const configText = renderConfig(answers);
|
|
5003
5177
|
const preferredName = chooseConfigFileName(options.cwd);
|
|
@@ -5021,6 +5195,8 @@ async function runInit(options) {
|
|
|
5021
5195
|
written: false,
|
|
5022
5196
|
reused,
|
|
5023
5197
|
preset: answers.preset,
|
|
5198
|
+
reason,
|
|
5199
|
+
fallback,
|
|
5024
5200
|
scripts: [],
|
|
5025
5201
|
gitignoreEntries: [],
|
|
5026
5202
|
dependency: { name: GENERATOR_NAME, range: `^${GENERATOR_VERSION}`, added: false },
|
|
@@ -5055,6 +5231,8 @@ async function runInit(options) {
|
|
|
5055
5231
|
written: true,
|
|
5056
5232
|
reused,
|
|
5057
5233
|
preset: answers.preset,
|
|
5234
|
+
reason,
|
|
5235
|
+
fallback,
|
|
5058
5236
|
scripts,
|
|
5059
5237
|
gitignoreEntries: ignore.added,
|
|
5060
5238
|
dependency,
|
|
@@ -5062,12 +5240,58 @@ async function runInit(options) {
|
|
|
5062
5240
|
};
|
|
5063
5241
|
}
|
|
5064
5242
|
|
|
5243
|
+
// src/wizard/autopilot.ts
|
|
5244
|
+
import * as fs10 from "fs";
|
|
5245
|
+
import * as path11 from "path";
|
|
5246
|
+
function describeTarget(cwd) {
|
|
5247
|
+
const relative11 = path11.relative(process.cwd(), cwd);
|
|
5248
|
+
if (relative11 === "") return "this directory";
|
|
5249
|
+
if (relative11.startsWith("..") || path11.isAbsolute(relative11)) return cwd;
|
|
5250
|
+
return relative11;
|
|
5251
|
+
}
|
|
5252
|
+
function firstRunMessage(reason, cwd) {
|
|
5253
|
+
const where = describeTarget(cwd);
|
|
5254
|
+
if (reason === "empty") {
|
|
5255
|
+
return `Nothing to snapshot: ${where} has no files. Run this inside the repository you want to snapshot, or pass --cwd.`;
|
|
5256
|
+
}
|
|
5257
|
+
return `No configuration found in ${where} and this session is not interactive, so nothing was generated.
|
|
5258
|
+
Choose one:
|
|
5259
|
+
${GENERATOR_NAME} --autopilot detect the project, set it up and generate, no questions
|
|
5260
|
+
${GENERATOR_NAME} init --yes write a config from the detected defaults, then generate
|
|
5261
|
+
${GENERATOR_NAME} init write a config interactively`;
|
|
5262
|
+
}
|
|
5263
|
+
function planFirstRun(facts) {
|
|
5264
|
+
if (facts.emptyTarget) return { action: "refuse", exitCode: 1, reason: "empty" };
|
|
5265
|
+
if (facts.autopilot) return { action: "setup", interactive: false };
|
|
5266
|
+
if (facts.hasConfig || facts.hasPriorSnapshot) return { action: "generate" };
|
|
5267
|
+
if (facts.interactive) return { action: "setup", interactive: true };
|
|
5268
|
+
return { action: "refuse", exitCode: 2, reason: "undecided" };
|
|
5269
|
+
}
|
|
5270
|
+
var NOT_CONTENT = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr", ".DS_Store", "Thumbs.db"]);
|
|
5271
|
+
function isEffectivelyEmpty(cwd) {
|
|
5272
|
+
try {
|
|
5273
|
+
return fs10.readdirSync(cwd).every((entry) => NOT_CONTENT.has(entry));
|
|
5274
|
+
} catch {
|
|
5275
|
+
return false;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
function hasPriorSnapshot(cwd, outDir) {
|
|
5279
|
+
let parsed;
|
|
5280
|
+
try {
|
|
5281
|
+
parsed = JSON.parse(fs10.readFileSync(path11.join(cwd, outDir, ".codebase-snapshot"), "utf8"));
|
|
5282
|
+
} catch {
|
|
5283
|
+
return false;
|
|
5284
|
+
}
|
|
5285
|
+
if (parsed === null || typeof parsed !== "object") return false;
|
|
5286
|
+
return isOurGeneratorName(parsed.generator);
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5065
5289
|
// src/index.ts
|
|
5066
5290
|
import * as crypto3 from "crypto";
|
|
5067
5291
|
import { AsyncLocalStorage } from "async_hooks";
|
|
5068
|
-
import * as
|
|
5292
|
+
import * as fs11 from "fs";
|
|
5069
5293
|
import * as os from "os";
|
|
5070
|
-
import * as
|
|
5294
|
+
import * as path12 from "path";
|
|
5071
5295
|
|
|
5072
5296
|
// src/ast/profiles/shared.ts
|
|
5073
5297
|
var MAX_SUMMARY_CHARS = 1e6;
|
|
@@ -5291,9 +5515,9 @@ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)
|
|
|
5291
5515
|
var jvmProfile = {
|
|
5292
5516
|
id: "jvm",
|
|
5293
5517
|
extensions: [".java", ".kt", ".kts", ".scala"],
|
|
5294
|
-
summarize(
|
|
5518
|
+
summarize(path18, content) {
|
|
5295
5519
|
const summary = emptySummary();
|
|
5296
|
-
const lower =
|
|
5520
|
+
const lower = path18.toLowerCase();
|
|
5297
5521
|
const isJava = lower.endsWith(".java");
|
|
5298
5522
|
const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
|
|
5299
5523
|
eachLine(content, (line) => {
|
|
@@ -5749,16 +5973,16 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
5749
5973
|
// src/ast/profiles/sfc.ts
|
|
5750
5974
|
var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
5751
5975
|
var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
|
|
5752
|
-
function componentName(
|
|
5753
|
-
const base =
|
|
5976
|
+
function componentName(path18) {
|
|
5977
|
+
const base = path18.split("/").pop() ?? path18;
|
|
5754
5978
|
return base.replace(/\.[^.]+$/, "");
|
|
5755
5979
|
}
|
|
5756
5980
|
var sfcProfile = {
|
|
5757
5981
|
id: "sfc",
|
|
5758
5982
|
extensions: [".vue", ".svelte", ".astro"],
|
|
5759
|
-
summarize(
|
|
5983
|
+
summarize(path18, content) {
|
|
5760
5984
|
let script = "";
|
|
5761
|
-
if (
|
|
5985
|
+
if (path18.toLowerCase().endsWith(".astro")) {
|
|
5762
5986
|
const frontmatter = ASTRO_FRONTMATTER.exec(content);
|
|
5763
5987
|
if (frontmatter) script += `${frontmatter[1]}
|
|
5764
5988
|
`;
|
|
@@ -5767,7 +5991,7 @@ var sfcProfile = {
|
|
|
5767
5991
|
script += `${match[1]}
|
|
5768
5992
|
`;
|
|
5769
5993
|
}
|
|
5770
|
-
const name = componentName(
|
|
5994
|
+
const name = componentName(path18);
|
|
5771
5995
|
if (!script.trim()) {
|
|
5772
5996
|
const summary2 = emptySummary();
|
|
5773
5997
|
if (name) {
|
|
@@ -5801,15 +6025,15 @@ for (const profile of LANGUAGE_PROFILES) {
|
|
|
5801
6025
|
BY_EXTENSION.set(extension.toLowerCase(), profile);
|
|
5802
6026
|
}
|
|
5803
6027
|
}
|
|
5804
|
-
function profileForPath(
|
|
5805
|
-
const lower =
|
|
6028
|
+
function profileForPath(path18) {
|
|
6029
|
+
const lower = path18.toLowerCase();
|
|
5806
6030
|
const dot = lower.lastIndexOf(".");
|
|
5807
6031
|
if (dot === -1) return null;
|
|
5808
6032
|
return BY_EXTENSION.get(lower.slice(dot)) ?? null;
|
|
5809
6033
|
}
|
|
5810
|
-
function summarizeWithProfile(
|
|
5811
|
-
const profile = profileForPath(
|
|
5812
|
-
return profile ? profile.summarize(
|
|
6034
|
+
function summarizeWithProfile(path18, content) {
|
|
6035
|
+
const profile = profileForPath(path18);
|
|
6036
|
+
return profile ? profile.summarize(path18, content) : null;
|
|
5813
6037
|
}
|
|
5814
6038
|
|
|
5815
6039
|
// src/ast/structured.ts
|
|
@@ -6739,13 +6963,13 @@ function buildImportResolver(files) {
|
|
|
6739
6963
|
const relativePath = file.relativePath;
|
|
6740
6964
|
remember(byPath, relativePath, relativePath);
|
|
6741
6965
|
remember(byPathFolded, relativePath.toLowerCase(), relativePath);
|
|
6742
|
-
const extension =
|
|
6966
|
+
const extension = path12.posix.extname(relativePath);
|
|
6743
6967
|
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
|
6744
6968
|
const stem = relativePath.slice(0, -extension.length);
|
|
6745
6969
|
remember(byStem, stem, relativePath);
|
|
6746
6970
|
remember(byStemFolded, stem.toLowerCase(), relativePath);
|
|
6747
|
-
if (
|
|
6748
|
-
const directory =
|
|
6971
|
+
if (path12.posix.basename(stem) === "index") {
|
|
6972
|
+
const directory = path12.posix.dirname(stem);
|
|
6749
6973
|
remember(byStem, directory, relativePath);
|
|
6750
6974
|
remember(byStemFolded, directory.toLowerCase(), relativePath);
|
|
6751
6975
|
}
|
|
@@ -6754,8 +6978,8 @@ function buildImportResolver(files) {
|
|
|
6754
6978
|
}
|
|
6755
6979
|
function resolveSpecifier(fromFile, specifier, resolver) {
|
|
6756
6980
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
6757
|
-
const joined =
|
|
6758
|
-
|
|
6981
|
+
const joined = path12.posix.normalize(
|
|
6982
|
+
path12.posix.join(path12.posix.dirname(fromFile), specifier)
|
|
6759
6983
|
);
|
|
6760
6984
|
const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
|
|
6761
6985
|
const direct = exact(joined);
|
|
@@ -6848,11 +7072,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
|
|
|
6848
7072
|
return errors;
|
|
6849
7073
|
}
|
|
6850
7074
|
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}${
|
|
7075
|
+
const cwd = path12.resolve(config.cwd);
|
|
7076
|
+
const outDir = path12.resolve(cwd, config.outDir);
|
|
7077
|
+
const tmpDir = path12.resolve(cwd, config.tmpDir);
|
|
7078
|
+
const root = path12.parse(cwd).root;
|
|
7079
|
+
const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path12.sep}`);
|
|
6856
7080
|
const reject = (reason) => {
|
|
6857
7081
|
throw new Error(
|
|
6858
7082
|
`Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
|
|
@@ -6908,14 +7132,14 @@ function listEntries(dir) {
|
|
|
6908
7132
|
const walk = (absolute, prefix) => {
|
|
6909
7133
|
let children;
|
|
6910
7134
|
try {
|
|
6911
|
-
children =
|
|
7135
|
+
children = fs11.readdirSync(absolute, { withFileTypes: true });
|
|
6912
7136
|
} catch {
|
|
6913
7137
|
return;
|
|
6914
7138
|
}
|
|
6915
7139
|
for (const child of children) {
|
|
6916
|
-
const
|
|
6917
|
-
entries.push(
|
|
6918
|
-
if (child.isDirectory()) walk(
|
|
7140
|
+
const relative11 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
7141
|
+
entries.push(relative11);
|
|
7142
|
+
if (child.isDirectory()) walk(path12.join(absolute, child.name), relative11);
|
|
6919
7143
|
}
|
|
6920
7144
|
};
|
|
6921
7145
|
walk(dir, "");
|
|
@@ -6932,28 +7156,28 @@ function markerPayload(dir, state, id, createdAt) {
|
|
|
6932
7156
|
pid: process.pid,
|
|
6933
7157
|
entries: listEntries(dir)
|
|
6934
7158
|
};
|
|
6935
|
-
const manifestPath =
|
|
6936
|
-
if (
|
|
6937
|
-
marker2.manifestSha256 = sha256Hex2(
|
|
7159
|
+
const manifestPath = path12.join(dir, "manifest.json");
|
|
7160
|
+
if (fs11.existsSync(manifestPath)) {
|
|
7161
|
+
marker2.manifestSha256 = sha256Hex2(fs11.readFileSync(manifestPath));
|
|
6938
7162
|
}
|
|
6939
7163
|
return marker2;
|
|
6940
7164
|
}
|
|
6941
7165
|
function writeOwnershipMarker(dir, marker2) {
|
|
6942
|
-
|
|
6943
|
-
|
|
7166
|
+
fs11.writeFileSync(
|
|
7167
|
+
path12.join(dir, OWNERSHIP_MARKER),
|
|
6944
7168
|
`${JSON.stringify(marker2, null, 2)}
|
|
6945
7169
|
`,
|
|
6946
7170
|
"utf8"
|
|
6947
7171
|
);
|
|
6948
7172
|
}
|
|
6949
7173
|
function readOwnershipMarker(dir) {
|
|
6950
|
-
const markerPath =
|
|
6951
|
-
if (!
|
|
7174
|
+
const markerPath = path12.join(dir, OWNERSHIP_MARKER);
|
|
7175
|
+
if (!fs11.existsSync(markerPath)) {
|
|
6952
7176
|
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
6953
7177
|
}
|
|
6954
7178
|
let raw;
|
|
6955
7179
|
try {
|
|
6956
|
-
raw =
|
|
7180
|
+
raw = fs11.readFileSync(markerPath, "utf8");
|
|
6957
7181
|
} catch {
|
|
6958
7182
|
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
6959
7183
|
}
|
|
@@ -7021,11 +7245,11 @@ function assessSnapshotDirectory(dir) {
|
|
|
7021
7245
|
marker: marker2
|
|
7022
7246
|
};
|
|
7023
7247
|
}
|
|
7024
|
-
const manifestPath =
|
|
7025
|
-
if (
|
|
7248
|
+
const manifestPath = path12.join(dir, "manifest.json");
|
|
7249
|
+
if (fs11.existsSync(manifestPath)) {
|
|
7026
7250
|
let raw;
|
|
7027
7251
|
try {
|
|
7028
|
-
raw =
|
|
7252
|
+
raw = fs11.readFileSync(manifestPath, "utf8");
|
|
7029
7253
|
} catch {
|
|
7030
7254
|
return {
|
|
7031
7255
|
owned: false,
|
|
@@ -7107,13 +7331,13 @@ function containsNoFiles(dir) {
|
|
|
7107
7331
|
const current = pending.pop();
|
|
7108
7332
|
let entries;
|
|
7109
7333
|
try {
|
|
7110
|
-
entries =
|
|
7334
|
+
entries = fs11.readdirSync(current, { withFileTypes: true });
|
|
7111
7335
|
} catch {
|
|
7112
7336
|
return false;
|
|
7113
7337
|
}
|
|
7114
7338
|
for (const entry of entries) {
|
|
7115
7339
|
if (entry.isDirectory()) {
|
|
7116
|
-
pending.push(
|
|
7340
|
+
pending.push(path12.join(current, entry.name));
|
|
7117
7341
|
} else if (!IGNORABLE_ENTRIES.has(entry.name)) {
|
|
7118
7342
|
return false;
|
|
7119
7343
|
}
|
|
@@ -7122,13 +7346,13 @@ function containsNoFiles(dir) {
|
|
|
7122
7346
|
return true;
|
|
7123
7347
|
}
|
|
7124
7348
|
function assertReplaceable(target, label, secretRules) {
|
|
7125
|
-
if (!
|
|
7126
|
-
if (!
|
|
7349
|
+
if (!fs11.existsSync(target)) return;
|
|
7350
|
+
if (!fs11.statSync(target).isDirectory()) {
|
|
7127
7351
|
throw new Error(
|
|
7128
7352
|
`Refusing to write into ${label} ${target}: it exists but is not a directory.`
|
|
7129
7353
|
);
|
|
7130
7354
|
}
|
|
7131
|
-
const entries =
|
|
7355
|
+
const entries = fs11.readdirSync(target);
|
|
7132
7356
|
if (entries.length === 0) return;
|
|
7133
7357
|
if (entries.every((name) => IGNORABLE_ENTRIES.has(name))) return;
|
|
7134
7358
|
if (containsNoFiles(target)) return;
|
|
@@ -7148,18 +7372,18 @@ function assertReplaceable(target, label, secretRules) {
|
|
|
7148
7372
|
);
|
|
7149
7373
|
}
|
|
7150
7374
|
function assertStagingRootUsable(tmpDir, secretRules) {
|
|
7151
|
-
if (!
|
|
7152
|
-
if (!
|
|
7375
|
+
if (!fs11.existsSync(tmpDir)) return;
|
|
7376
|
+
if (!fs11.statSync(tmpDir).isDirectory()) {
|
|
7153
7377
|
throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
|
|
7154
7378
|
}
|
|
7155
|
-
const entries =
|
|
7379
|
+
const entries = fs11.readdirSync(tmpDir);
|
|
7156
7380
|
if (entries.length === 0) return;
|
|
7157
7381
|
const foreign = entries.filter((name) => {
|
|
7158
|
-
const full =
|
|
7382
|
+
const full = path12.join(tmpDir, name);
|
|
7159
7383
|
if (!name.startsWith(RUN_DIR_PREFIX)) return true;
|
|
7160
7384
|
let stats;
|
|
7161
7385
|
try {
|
|
7162
|
-
stats =
|
|
7386
|
+
stats = fs11.statSync(full);
|
|
7163
7387
|
} catch {
|
|
7164
7388
|
return true;
|
|
7165
7389
|
}
|
|
@@ -7167,7 +7391,7 @@ function assertStagingRootUsable(tmpDir, secretRules) {
|
|
|
7167
7391
|
const { marker: marker2 } = readOwnershipMarker(full);
|
|
7168
7392
|
if (marker2) return false;
|
|
7169
7393
|
try {
|
|
7170
|
-
return
|
|
7394
|
+
return fs11.readdirSync(full).length > 0;
|
|
7171
7395
|
} catch {
|
|
7172
7396
|
return true;
|
|
7173
7397
|
}
|
|
@@ -7182,13 +7406,13 @@ function assertStagingRootUsable(tmpDir, secretRules) {
|
|
|
7182
7406
|
function cleanStaleRunDirs(tmpDir) {
|
|
7183
7407
|
let entries;
|
|
7184
7408
|
try {
|
|
7185
|
-
entries =
|
|
7409
|
+
entries = fs11.readdirSync(tmpDir);
|
|
7186
7410
|
} catch {
|
|
7187
7411
|
return;
|
|
7188
7412
|
}
|
|
7189
7413
|
for (const name of entries) {
|
|
7190
7414
|
if (!name.startsWith(RUN_DIR_PREFIX)) continue;
|
|
7191
|
-
const full =
|
|
7415
|
+
const full = path12.join(tmpDir, name);
|
|
7192
7416
|
const { marker: marker2 } = readOwnershipMarker(full);
|
|
7193
7417
|
if (marker2) {
|
|
7194
7418
|
const created = Date.parse(marker2.createdAt);
|
|
@@ -7196,33 +7420,33 @@ function cleanStaleRunDirs(tmpDir) {
|
|
|
7196
7420
|
const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
|
|
7197
7421
|
if (aged || abandoned) {
|
|
7198
7422
|
try {
|
|
7199
|
-
|
|
7423
|
+
fs11.rmSync(full, { recursive: true, force: true });
|
|
7200
7424
|
} catch {
|
|
7201
7425
|
}
|
|
7202
7426
|
}
|
|
7203
7427
|
continue;
|
|
7204
7428
|
}
|
|
7205
7429
|
try {
|
|
7206
|
-
if (
|
|
7430
|
+
if (fs11.readdirSync(full).length === 0) fs11.rmSync(full, { recursive: true, force: true });
|
|
7207
7431
|
} catch {
|
|
7208
7432
|
}
|
|
7209
7433
|
}
|
|
7210
7434
|
}
|
|
7211
7435
|
function removeIfEmpty(dir) {
|
|
7212
7436
|
try {
|
|
7213
|
-
if (
|
|
7437
|
+
if (fs11.readdirSync(dir).length === 0) fs11.rmSync(dir, { recursive: true, force: true });
|
|
7214
7438
|
} catch {
|
|
7215
7439
|
}
|
|
7216
7440
|
}
|
|
7217
7441
|
function ensureParentDirectory(target, label) {
|
|
7218
|
-
const parent =
|
|
7219
|
-
if (
|
|
7442
|
+
const parent = path12.dirname(target);
|
|
7443
|
+
if (fs11.existsSync(parent) && !fs11.statSync(parent).isDirectory()) {
|
|
7220
7444
|
throw new Error(
|
|
7221
7445
|
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
7222
7446
|
);
|
|
7223
7447
|
}
|
|
7224
7448
|
try {
|
|
7225
|
-
|
|
7449
|
+
fs11.mkdirSync(parent, { recursive: true });
|
|
7226
7450
|
} catch (err) {
|
|
7227
7451
|
throw new Error(
|
|
7228
7452
|
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
@@ -7231,7 +7455,7 @@ function ensureParentDirectory(target, label) {
|
|
|
7231
7455
|
}
|
|
7232
7456
|
function acquireRunLock(stagingRoot) {
|
|
7233
7457
|
const key = lockKeyFor(stagingRoot);
|
|
7234
|
-
const lockPath =
|
|
7458
|
+
const lockPath = path12.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
|
|
7235
7459
|
const payload = `${JSON.stringify({
|
|
7236
7460
|
pid: process.pid,
|
|
7237
7461
|
stagingRoot,
|
|
@@ -7240,22 +7464,22 @@ function acquireRunLock(stagingRoot) {
|
|
|
7240
7464
|
`;
|
|
7241
7465
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
7242
7466
|
try {
|
|
7243
|
-
const handle =
|
|
7467
|
+
const handle = fs11.openSync(lockPath, "wx");
|
|
7244
7468
|
try {
|
|
7245
|
-
|
|
7469
|
+
fs11.writeSync(handle, payload);
|
|
7246
7470
|
} finally {
|
|
7247
|
-
|
|
7471
|
+
fs11.closeSync(handle);
|
|
7248
7472
|
}
|
|
7249
7473
|
return () => {
|
|
7250
7474
|
try {
|
|
7251
|
-
|
|
7475
|
+
fs11.rmSync(lockPath, { force: true });
|
|
7252
7476
|
} catch {
|
|
7253
7477
|
}
|
|
7254
7478
|
};
|
|
7255
7479
|
} catch (err) {
|
|
7256
7480
|
if (err.code !== "EEXIST") throw err;
|
|
7257
7481
|
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
7258
|
-
|
|
7482
|
+
fs11.rmSync(lockPath, { force: true });
|
|
7259
7483
|
continue;
|
|
7260
7484
|
}
|
|
7261
7485
|
throw new Error(
|
|
@@ -7268,7 +7492,7 @@ function acquireRunLock(stagingRoot) {
|
|
|
7268
7492
|
function lockKeyFor(stagingRoot) {
|
|
7269
7493
|
let key = stagingRoot;
|
|
7270
7494
|
try {
|
|
7271
|
-
key =
|
|
7495
|
+
key = fs11.realpathSync(stagingRoot);
|
|
7272
7496
|
} catch {
|
|
7273
7497
|
}
|
|
7274
7498
|
if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
|
|
@@ -7277,7 +7501,7 @@ function lockKeyFor(stagingRoot) {
|
|
|
7277
7501
|
function lockOwnerIsGone(lockPath) {
|
|
7278
7502
|
let raw;
|
|
7279
7503
|
try {
|
|
7280
|
-
raw =
|
|
7504
|
+
raw = fs11.readFileSync(lockPath, "utf8");
|
|
7281
7505
|
} catch {
|
|
7282
7506
|
return false;
|
|
7283
7507
|
}
|
|
@@ -7297,12 +7521,12 @@ function lockOwnerIsGone(lockPath) {
|
|
|
7297
7521
|
}
|
|
7298
7522
|
function writeManifestAtomically(manifestPath, manifest) {
|
|
7299
7523
|
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
7300
|
-
|
|
7524
|
+
fs11.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
7301
7525
|
try {
|
|
7302
|
-
|
|
7526
|
+
fs11.renameSync(tempPath, manifestPath);
|
|
7303
7527
|
} catch (err) {
|
|
7304
7528
|
try {
|
|
7305
|
-
|
|
7529
|
+
fs11.rmSync(tempPath, { force: true });
|
|
7306
7530
|
} catch {
|
|
7307
7531
|
}
|
|
7308
7532
|
throw err;
|
|
@@ -7325,31 +7549,31 @@ function swapIntoPlace(stagingDir, outDir) {
|
|
|
7325
7549
|
}
|
|
7326
7550
|
function swapOnce(stagingDir, outDir) {
|
|
7327
7551
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
7328
|
-
const hadPrevious =
|
|
7329
|
-
if (hadPrevious)
|
|
7552
|
+
const hadPrevious = fs11.existsSync(outDir);
|
|
7553
|
+
if (hadPrevious) fs11.renameSync(outDir, backupDir);
|
|
7330
7554
|
try {
|
|
7331
7555
|
try {
|
|
7332
|
-
|
|
7556
|
+
fs11.renameSync(stagingDir, outDir);
|
|
7333
7557
|
} catch (err) {
|
|
7334
7558
|
if (err.code !== "EXDEV") throw err;
|
|
7335
7559
|
const partialDir = `${outDir}.partial-${process.pid}-${Date.now()}`;
|
|
7336
7560
|
try {
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7561
|
+
fs11.cpSync(stagingDir, partialDir, { recursive: true });
|
|
7562
|
+
fs11.renameSync(partialDir, outDir);
|
|
7563
|
+
fs11.rmSync(stagingDir, { recursive: true, force: true });
|
|
7340
7564
|
} catch (copyErr) {
|
|
7341
7565
|
try {
|
|
7342
|
-
|
|
7566
|
+
fs11.rmSync(partialDir, { recursive: true, force: true });
|
|
7343
7567
|
} catch {
|
|
7344
7568
|
}
|
|
7345
7569
|
throw copyErr;
|
|
7346
7570
|
}
|
|
7347
7571
|
}
|
|
7348
|
-
if (hadPrevious)
|
|
7572
|
+
if (hadPrevious) fs11.rmSync(backupDir, { recursive: true, force: true });
|
|
7349
7573
|
} catch (err) {
|
|
7350
|
-
if (hadPrevious &&
|
|
7351
|
-
if (
|
|
7352
|
-
|
|
7574
|
+
if (hadPrevious && fs11.existsSync(backupDir)) {
|
|
7575
|
+
if (fs11.existsSync(outDir)) fs11.rmSync(outDir, { recursive: true, force: true });
|
|
7576
|
+
fs11.renameSync(backupDir, outDir);
|
|
7353
7577
|
}
|
|
7354
7578
|
throw err;
|
|
7355
7579
|
}
|
|
@@ -7444,8 +7668,8 @@ function generateSnapshot(configInput, options) {
|
|
|
7444
7668
|
// first run cannot have marked them yet, so the name alone keeps a run from recording its own
|
|
7445
7669
|
// output — but only at these exact paths, not at every directory that shares the name.
|
|
7446
7670
|
toolOwnedPaths: [config.outDir, config.tmpDir].map(
|
|
7447
|
-
(directory) =>
|
|
7448
|
-
).filter((
|
|
7671
|
+
(directory) => path12.relative(config.cwd, path12.resolve(config.cwd, directory)).replace(/\\/g, "/")
|
|
7672
|
+
).filter((relative11) => relative11.length > 0 && !relative11.startsWith(".."))
|
|
7449
7673
|
});
|
|
7450
7674
|
const files = [];
|
|
7451
7675
|
const monolithBodies = /* @__PURE__ */ new Map();
|
|
@@ -7465,12 +7689,12 @@ function generateSnapshot(configInput, options) {
|
|
|
7465
7689
|
let totalSizeBytes = 0;
|
|
7466
7690
|
let largeFileCount = 0;
|
|
7467
7691
|
for (const relativePath of collection.files) {
|
|
7468
|
-
const absolutePath =
|
|
7692
|
+
const absolutePath = path12.join(config.cwd, relativePath);
|
|
7469
7693
|
let stats;
|
|
7470
7694
|
try {
|
|
7471
|
-
stats =
|
|
7695
|
+
stats = fs11.statSync(absolutePath);
|
|
7472
7696
|
if (!stats.isFile()) {
|
|
7473
|
-
const nestedRepository = stats.isDirectory() &&
|
|
7697
|
+
const nestedRepository = stats.isDirectory() && fs11.existsSync(path12.join(absolutePath, ".git"));
|
|
7474
7698
|
skipped.push(
|
|
7475
7699
|
nestedRepository ? {
|
|
7476
7700
|
path: relativePath,
|
|
@@ -7490,7 +7714,7 @@ function generateSnapshot(configInput, options) {
|
|
|
7490
7714
|
}
|
|
7491
7715
|
let buffer;
|
|
7492
7716
|
try {
|
|
7493
|
-
buffer =
|
|
7717
|
+
buffer = fs11.readFileSync(absolutePath);
|
|
7494
7718
|
} catch {
|
|
7495
7719
|
skipped.push({ path: relativePath, reason: "read-error" });
|
|
7496
7720
|
continue;
|
|
@@ -7820,12 +8044,12 @@ function generateSnapshot(configInput, options) {
|
|
|
7820
8044
|
let stagingDir = null;
|
|
7821
8045
|
try {
|
|
7822
8046
|
ensureParentDirectory(outDir, "outDir");
|
|
7823
|
-
|
|
8047
|
+
fs11.mkdirSync(tmpDir, { recursive: true });
|
|
7824
8048
|
cleanStaleRunDirs(tmpDir);
|
|
7825
8049
|
const snapshotId = crypto3.randomUUID();
|
|
7826
8050
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7827
|
-
stagingDir =
|
|
7828
|
-
|
|
8051
|
+
stagingDir = path12.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
8052
|
+
fs11.mkdirSync(stagingDir);
|
|
7829
8053
|
writeOwnershipMarker(stagingDir, {
|
|
7830
8054
|
generator: GENERATOR_NAME,
|
|
7831
8055
|
version: GENERATOR_VERSION,
|
|
@@ -7835,41 +8059,41 @@ function generateSnapshot(configInput, options) {
|
|
|
7835
8059
|
id: snapshotId,
|
|
7836
8060
|
pid: process.pid
|
|
7837
8061
|
});
|
|
7838
|
-
const partsDir =
|
|
7839
|
-
const reportsDir =
|
|
7840
|
-
if (artifacts.parts)
|
|
8062
|
+
const partsDir = path12.join(stagingDir, "parts");
|
|
8063
|
+
const reportsDir = path12.join(stagingDir, "reports");
|
|
8064
|
+
if (artifacts.parts) fs11.mkdirSync(partsDir, { recursive: true });
|
|
7841
8065
|
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
7842
|
-
|
|
8066
|
+
fs11.mkdirSync(reportsDir, { recursive: true });
|
|
7843
8067
|
}
|
|
7844
8068
|
if (artifacts.parts) {
|
|
7845
8069
|
for (const part of parts) {
|
|
7846
|
-
|
|
8070
|
+
fs11.writeFileSync(path12.join(partsDir, part.filename), part.markdown, "utf8");
|
|
7847
8071
|
}
|
|
7848
8072
|
}
|
|
7849
8073
|
if (projectTree !== null) {
|
|
7850
|
-
|
|
8074
|
+
fs11.writeFileSync(path12.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
7851
8075
|
}
|
|
7852
8076
|
if (artifacts.navigation) {
|
|
7853
|
-
|
|
8077
|
+
fs11.writeFileSync(path12.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
7854
8078
|
}
|
|
7855
8079
|
if (artifacts.audit) {
|
|
7856
|
-
|
|
7857
|
-
|
|
8080
|
+
fs11.writeFileSync(
|
|
8081
|
+
path12.join(reportsDir, "audit.md"),
|
|
7858
8082
|
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
7859
8083
|
"utf8"
|
|
7860
8084
|
);
|
|
7861
8085
|
}
|
|
7862
8086
|
if (artifacts.readme) {
|
|
7863
|
-
|
|
8087
|
+
fs11.writeFileSync(path12.join(stagingDir, "README.md"), generateReadme(manifest, artifacts), "utf8");
|
|
7864
8088
|
}
|
|
7865
8089
|
if (artifacts.manifest) {
|
|
7866
|
-
|
|
8090
|
+
fs11.writeFileSync(path12.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
7867
8091
|
}
|
|
7868
8092
|
if (monolithJson !== null) {
|
|
7869
|
-
|
|
8093
|
+
fs11.writeFileSync(path12.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
7870
8094
|
}
|
|
7871
8095
|
if (monolithMarkdown !== null) {
|
|
7872
|
-
|
|
8096
|
+
fs11.writeFileSync(path12.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
7873
8097
|
}
|
|
7874
8098
|
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
7875
8099
|
swapIntoPlace(stagingDir, outDir);
|
|
@@ -7878,7 +8102,7 @@ function generateSnapshot(configInput, options) {
|
|
|
7878
8102
|
const pendingHook = runAfterGenerate(config, manifest);
|
|
7879
8103
|
const shipHookState = () => {
|
|
7880
8104
|
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
7881
|
-
writeManifestAtomically(
|
|
8105
|
+
writeManifestAtomically(path12.join(outDir, "manifest.json"), manifest);
|
|
7882
8106
|
}
|
|
7883
8107
|
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
7884
8108
|
};
|
|
@@ -7901,7 +8125,7 @@ function generateSnapshot(configInput, options) {
|
|
|
7901
8125
|
} catch (err) {
|
|
7902
8126
|
if (stagingDir !== null) {
|
|
7903
8127
|
try {
|
|
7904
|
-
|
|
8128
|
+
fs11.rmSync(stagingDir, { recursive: true, force: true });
|
|
7905
8129
|
} catch {
|
|
7906
8130
|
}
|
|
7907
8131
|
}
|
|
@@ -7912,143 +8136,1144 @@ function generateSnapshot(configInput, options) {
|
|
|
7912
8136
|
}
|
|
7913
8137
|
}
|
|
7914
8138
|
|
|
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));
|
|
8139
|
+
// src/mcp/tools.ts
|
|
8140
|
+
import * as fs13 from "fs";
|
|
8141
|
+
import * as path14 from "path";
|
|
8142
|
+
|
|
8143
|
+
// src/mcp/part.ts
|
|
8144
|
+
var FILE_HEADING = "### FILE: ";
|
|
8145
|
+
var CONTENT_MARKERS = /* @__PURE__ */ new Set(["#### Content", "#### Bounded Preview"]);
|
|
8146
|
+
function parsePartSections(partText) {
|
|
8147
|
+
const lines = partText.split("\n");
|
|
8148
|
+
const sections = [];
|
|
8149
|
+
let index = 0;
|
|
8150
|
+
while (index < lines.length) {
|
|
8151
|
+
if (!lines[index].startsWith(FILE_HEADING)) {
|
|
8152
|
+
index += 1;
|
|
8153
|
+
continue;
|
|
8154
|
+
}
|
|
8155
|
+
const heading = lines[index].slice(FILE_HEADING.length);
|
|
8156
|
+
let cursor = index + 1;
|
|
8157
|
+
let inclusion = null;
|
|
8158
|
+
let sha256 = null;
|
|
8159
|
+
let includedLines = null;
|
|
8160
|
+
while (cursor < lines.length && !CONTENT_MARKERS.has(lines[cursor])) {
|
|
8161
|
+
const line = lines[cursor];
|
|
8162
|
+
if (line.startsWith(FILE_HEADING)) break;
|
|
8163
|
+
if (line.startsWith("- Inclusion: ")) inclusion = line.slice("- Inclusion: ".length).trim();
|
|
8164
|
+
else if (line.startsWith("- SHA-256: ")) sha256 = line.slice("- SHA-256: ".length).trim();
|
|
8165
|
+
else if (line.startsWith("- Included Lines: ")) {
|
|
8166
|
+
const value = Number(line.slice("- Included Lines: ".length).trim());
|
|
8167
|
+
includedLines = Number.isFinite(value) ? value : null;
|
|
8168
|
+
}
|
|
8169
|
+
cursor += 1;
|
|
8170
|
+
}
|
|
8171
|
+
if (cursor >= lines.length || !CONTENT_MARKERS.has(lines[cursor])) {
|
|
8172
|
+
index = cursor + 1;
|
|
8173
|
+
continue;
|
|
8174
|
+
}
|
|
8175
|
+
let fenceLine = cursor + 1;
|
|
8176
|
+
while (fenceLine < lines.length && lines[fenceLine].trim().length === 0) fenceLine += 1;
|
|
8177
|
+
const fenceMatch = /^(`{4,})/.exec(lines[fenceLine] ?? "");
|
|
8178
|
+
if (!fenceMatch) {
|
|
8179
|
+
index = cursor + 1;
|
|
8180
|
+
continue;
|
|
8181
|
+
}
|
|
8182
|
+
const fence = fenceMatch[1];
|
|
8183
|
+
const body = [];
|
|
8184
|
+
let bodyLine = fenceLine + 1;
|
|
8185
|
+
while (bodyLine < lines.length && lines[bodyLine] !== fence) {
|
|
8186
|
+
body.push(lines[bodyLine]);
|
|
8187
|
+
bodyLine += 1;
|
|
8188
|
+
}
|
|
8189
|
+
sections.push({ path: heading, body: body.join("\n"), inclusion, sha256, includedLines });
|
|
8190
|
+
index = bodyLine + 1;
|
|
8191
|
+
}
|
|
8192
|
+
return sections;
|
|
8193
|
+
}
|
|
8194
|
+
function findSection(sections, relativePath, sha256) {
|
|
8195
|
+
const exact = sections.find((section) => section.path === relativePath);
|
|
8196
|
+
if (exact) return exact;
|
|
8197
|
+
if (sha256) {
|
|
8198
|
+
const byHash = sections.filter((section) => section.sha256 === sha256);
|
|
8199
|
+
if (byHash.length === 1) return byHash[0];
|
|
8200
|
+
if (byHash.length > 1) {
|
|
8201
|
+
const byPath = byHash.find(
|
|
8202
|
+
(section) => section.path.toLowerCase() === relativePath.toLowerCase()
|
|
8203
|
+
);
|
|
8204
|
+
if (byPath) return byPath;
|
|
8205
|
+
}
|
|
8206
|
+
}
|
|
8207
|
+
const folded = sections.filter((section) => section.path.toLowerCase() === relativePath.toLowerCase());
|
|
8208
|
+
return folded.length === 1 ? folded[0] : null;
|
|
8041
8209
|
}
|
|
8042
|
-
function
|
|
8043
|
-
|
|
8210
|
+
function windowText(body, fromLine, lineCount) {
|
|
8211
|
+
const lines = body.split("\n");
|
|
8212
|
+
const total = lines.length;
|
|
8213
|
+
const start = Math.min(Math.max(1, Math.trunc(fromLine)), Math.max(1, total));
|
|
8214
|
+
const end = Math.min(total, start + Math.max(1, Math.trunc(lineCount)) - 1);
|
|
8215
|
+
const slice = lines.slice(start - 1, end);
|
|
8216
|
+
return {
|
|
8217
|
+
text: slice.join("\n"),
|
|
8218
|
+
fromLine: start,
|
|
8219
|
+
returnedLines: slice.length,
|
|
8220
|
+
totalLines: total,
|
|
8221
|
+
truncated: end < total,
|
|
8222
|
+
nextFromLine: end < total ? end + 1 : null
|
|
8223
|
+
};
|
|
8044
8224
|
}
|
|
8045
8225
|
|
|
8046
|
-
// src/
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8226
|
+
// src/mcp/workspace.ts
|
|
8227
|
+
import * as fs12 from "fs";
|
|
8228
|
+
import * as path13 from "path";
|
|
8229
|
+
function foldForComparison(value) {
|
|
8230
|
+
return process.platform === "win32" || process.platform === "darwin" ? value.toLowerCase() : value;
|
|
8231
|
+
}
|
|
8232
|
+
function canonicalPath(value) {
|
|
8233
|
+
const absolute = path13.resolve(value);
|
|
8234
|
+
try {
|
|
8235
|
+
return fs12.realpathSync(absolute);
|
|
8236
|
+
} catch {
|
|
8237
|
+
return absolute;
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
function isInside2(parent, child) {
|
|
8241
|
+
const foldedParent = foldForComparison(parent);
|
|
8242
|
+
const foldedChild = foldForComparison(child);
|
|
8243
|
+
return foldedChild === foldedParent || foldedChild.startsWith(`${foldedParent}${path13.sep}`);
|
|
8244
|
+
}
|
|
8245
|
+
function resolveRoot(requested, context) {
|
|
8246
|
+
if (requested !== void 0 && (typeof requested !== "string" || requested.trim().length === 0)) {
|
|
8247
|
+
return { ok: false, message: '"root" must be a non-empty string when provided.' };
|
|
8248
|
+
}
|
|
8249
|
+
const wanted = typeof requested === "string" ? requested.trim() : context.cwd;
|
|
8250
|
+
const absolute = path13.resolve(context.cwd, wanted);
|
|
8251
|
+
let stats;
|
|
8252
|
+
try {
|
|
8253
|
+
stats = fs12.statSync(absolute);
|
|
8254
|
+
} catch {
|
|
8255
|
+
return { ok: false, message: `"${wanted}" does not exist, so it cannot be a repository root.` };
|
|
8256
|
+
}
|
|
8257
|
+
if (!stats.isDirectory()) {
|
|
8258
|
+
return { ok: false, message: `"${wanted}" is not a directory.` };
|
|
8259
|
+
}
|
|
8260
|
+
const resolved = canonicalPath(absolute);
|
|
8261
|
+
for (const allowed of context.roots) {
|
|
8262
|
+
if (isInside2(canonicalPath(allowed), resolved)) return { ok: true, root: resolved };
|
|
8263
|
+
}
|
|
8264
|
+
const shown = context.roots.join(", ");
|
|
8265
|
+
return {
|
|
8266
|
+
ok: false,
|
|
8267
|
+
message: `"${wanted}" resolves outside the directories this server may read (${shown}). Restart it with --root <path> to add a repository.`
|
|
8268
|
+
};
|
|
8269
|
+
}
|
|
8270
|
+
async function openWorkspace(root, overrides = {}) {
|
|
8271
|
+
const loaded = await loadConfigFileDetailed(root);
|
|
8272
|
+
if (loaded.errors.length > 0) {
|
|
8273
|
+
return {
|
|
8274
|
+
ok: false,
|
|
8275
|
+
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.`
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8278
|
+
const cliOptions = { cwd: root };
|
|
8279
|
+
if (overrides.maxPartBytes !== void 0) {
|
|
8280
|
+
const parsed = parseByteSize(overrides.maxPartBytes);
|
|
8281
|
+
if (parsed === void 0) {
|
|
8282
|
+
return {
|
|
8283
|
+
ok: false,
|
|
8284
|
+
message: '"maxPartBytes" must be a byte count, for example 307200 or "300k".'
|
|
8285
|
+
};
|
|
8286
|
+
}
|
|
8287
|
+
cliOptions.maxPartBytes = parsed;
|
|
8288
|
+
}
|
|
8289
|
+
if (overrides.outDir !== void 0) {
|
|
8290
|
+
if (typeof overrides.outDir !== "string" || overrides.outDir.trim().length === 0) {
|
|
8291
|
+
return { ok: false, message: '"outDir" must be a non-empty string.' };
|
|
8292
|
+
}
|
|
8293
|
+
cliOptions.outDir = overrides.outDir.trim();
|
|
8294
|
+
}
|
|
8295
|
+
try {
|
|
8296
|
+
const config = resolveConfig(loaded.config, cliOptions, root);
|
|
8297
|
+
return { ok: true, workspace: { root, config, configFile: loaded.file } };
|
|
8298
|
+
} catch (error) {
|
|
8299
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8302
|
+
var manifestCache = /* @__PURE__ */ new Map();
|
|
8303
|
+
function manifestPathFor(workspace) {
|
|
8304
|
+
return path13.resolve(workspace.root, workspace.config.outDir, "manifest.json");
|
|
8305
|
+
}
|
|
8306
|
+
function readManifest3(workspace) {
|
|
8307
|
+
const file = manifestPathFor(workspace);
|
|
8308
|
+
const shown = path13.relative(workspace.root, file).split(path13.sep).join("/") || file;
|
|
8309
|
+
let stats;
|
|
8310
|
+
try {
|
|
8311
|
+
stats = fs12.statSync(file);
|
|
8312
|
+
} catch {
|
|
8313
|
+
return {
|
|
8314
|
+
ok: false,
|
|
8315
|
+
missing: true,
|
|
8316
|
+
message: `There is no snapshot manifest at ${shown}. Call snapshot_plan to preview what would be included, or snapshot_generate to write it.`
|
|
8317
|
+
};
|
|
8318
|
+
}
|
|
8319
|
+
const cached = manifestCache.get(file);
|
|
8320
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
8321
|
+
return { ok: true, manifest: cached.manifest, file };
|
|
8322
|
+
}
|
|
8323
|
+
let raw;
|
|
8324
|
+
try {
|
|
8325
|
+
raw = fs12.readFileSync(file, "utf8");
|
|
8326
|
+
} catch (error) {
|
|
8327
|
+
return {
|
|
8328
|
+
ok: false,
|
|
8329
|
+
missing: false,
|
|
8330
|
+
message: `${shown} exists but could not be read: ${error instanceof Error ? error.message : String(error)}`
|
|
8331
|
+
};
|
|
8332
|
+
}
|
|
8333
|
+
let parsed;
|
|
8334
|
+
try {
|
|
8335
|
+
parsed = JSON.parse(raw);
|
|
8336
|
+
} catch {
|
|
8337
|
+
return { ok: false, missing: false, message: `${shown} is not valid JSON.` };
|
|
8338
|
+
}
|
|
8339
|
+
const problem = describeManifestShapeProblem(parsed);
|
|
8340
|
+
if (problem) {
|
|
8341
|
+
return { ok: false, missing: false, message: `${shown} is not a snapshot manifest: ${problem}` };
|
|
8342
|
+
}
|
|
8343
|
+
const manifest = parsed;
|
|
8344
|
+
manifestCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, manifest });
|
|
8345
|
+
return { ok: true, manifest, file };
|
|
8346
|
+
}
|
|
8347
|
+
function forgetManifest(file) {
|
|
8348
|
+
manifestCache.delete(file);
|
|
8349
|
+
}
|
|
8350
|
+
|
|
8351
|
+
// src/mcp/tools.ts
|
|
8352
|
+
var READ_ONLY = {
|
|
8353
|
+
readOnlyHint: true,
|
|
8354
|
+
destructiveHint: false,
|
|
8355
|
+
idempotentHint: true,
|
|
8356
|
+
openWorldHint: false
|
|
8357
|
+
};
|
|
8358
|
+
var WRITES = {
|
|
8359
|
+
readOnlyHint: false,
|
|
8360
|
+
// It replaces the snapshot directory but never anything it did not write, so it is additive with
|
|
8361
|
+
// respect to the user's files.
|
|
8362
|
+
destructiveHint: false,
|
|
8363
|
+
idempotentHint: true,
|
|
8364
|
+
openWorldHint: false
|
|
8365
|
+
};
|
|
8366
|
+
var MAX_RESULT_BYTES = 64 * 1024;
|
|
8367
|
+
var ROOT_PROPERTY = {
|
|
8368
|
+
type: "string",
|
|
8369
|
+
description: "Repository root. Defaults to the directory this server was started in."
|
|
8370
|
+
};
|
|
8371
|
+
function failure(message) {
|
|
8372
|
+
return { text: message, isError: true };
|
|
8373
|
+
}
|
|
8374
|
+
function plural(count, word) {
|
|
8375
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
8376
|
+
}
|
|
8377
|
+
function formatBytes(bytes) {
|
|
8378
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
8379
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
8380
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
8381
|
+
}
|
|
8382
|
+
function relativeFor(workspace, absolute) {
|
|
8383
|
+
return path14.relative(workspace.root, absolute).split(path14.sep).join("/");
|
|
8384
|
+
}
|
|
8385
|
+
function capped(values, limit = 20) {
|
|
8386
|
+
return { count: values.length, sample: values.slice(0, limit) };
|
|
8387
|
+
}
|
|
8388
|
+
function summarizeDrift(drift) {
|
|
8389
|
+
return {
|
|
8390
|
+
missingInSnapshot: capped(drift.missingInSnapshot),
|
|
8391
|
+
missingOnDisk: capped(drift.missingOnDisk),
|
|
8392
|
+
unreadable: capped(drift.unreadable),
|
|
8393
|
+
hashMismatches: capped(drift.hashMismatches),
|
|
8394
|
+
partIssues: capped(drift.partIssues),
|
|
8395
|
+
totalsMismatches: capped(drift.totalsMismatches),
|
|
8396
|
+
configMismatches: capped(drift.configMismatches)
|
|
8397
|
+
};
|
|
8398
|
+
}
|
|
8399
|
+
function toRelativePath(root, value) {
|
|
8400
|
+
const cleaned = value.trim();
|
|
8401
|
+
if (cleaned.length === 0) return null;
|
|
8402
|
+
if (!path14.isAbsolute(cleaned)) {
|
|
8403
|
+
return cleaned.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
8404
|
+
}
|
|
8405
|
+
const relative11 = path14.relative(root, cleaned);
|
|
8406
|
+
if (relative11.startsWith("..") || path14.isAbsolute(relative11)) return null;
|
|
8407
|
+
return relative11.split(path14.sep).join("/");
|
|
8408
|
+
}
|
|
8409
|
+
function symbolsOf(entry) {
|
|
8410
|
+
const summary = entry.summary;
|
|
8411
|
+
if (!summary || typeof summary !== "object" || !("exports" in summary)) return [];
|
|
8412
|
+
const source = summary;
|
|
8413
|
+
return [...source.exports, ...source.reactComponents, ...source.routeHandlers];
|
|
8414
|
+
}
|
|
8415
|
+
async function prepare(args, context, overrides = {}) {
|
|
8416
|
+
const rootOutcome = resolveRoot(args.root, context);
|
|
8417
|
+
if (!rootOutcome.ok) return { ok: false, outcome: failure(rootOutcome.message) };
|
|
8418
|
+
const opened = await openWorkspace(rootOutcome.root, overrides);
|
|
8419
|
+
if (!opened.ok) return { ok: false, outcome: failure(opened.message) };
|
|
8420
|
+
return { ok: true, workspace: opened.workspace };
|
|
8421
|
+
}
|
|
8422
|
+
var statusTool = {
|
|
8423
|
+
name: "snapshot_status",
|
|
8424
|
+
title: "Snapshot status",
|
|
8425
|
+
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.",
|
|
8426
|
+
inputSchema: {
|
|
8427
|
+
type: "object",
|
|
8428
|
+
properties: { root: ROOT_PROPERTY },
|
|
8429
|
+
additionalProperties: false
|
|
8430
|
+
},
|
|
8431
|
+
outputSchema: {
|
|
8432
|
+
type: "object",
|
|
8433
|
+
properties: {
|
|
8434
|
+
present: { type: "boolean" },
|
|
8435
|
+
root: { type: "string" },
|
|
8436
|
+
outDir: { type: "string" },
|
|
8437
|
+
fingerprint: { type: "string" },
|
|
8438
|
+
generatedAt: { type: "string" },
|
|
8439
|
+
generatorVersion: { type: "string" },
|
|
8440
|
+
generatorVersionMatches: { type: "boolean" },
|
|
8441
|
+
includedFiles: { type: "integer" },
|
|
8442
|
+
skippedFiles: { type: "integer" },
|
|
8443
|
+
partCount: { type: "integer" },
|
|
8444
|
+
warningCount: { type: "integer" }
|
|
8445
|
+
}
|
|
8446
|
+
},
|
|
8447
|
+
annotations: READ_ONLY,
|
|
8448
|
+
async handler(args, context) {
|
|
8449
|
+
const prepared = await prepare(args, context);
|
|
8450
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8451
|
+
const workspace = prepared.workspace;
|
|
8452
|
+
const read = readManifest3(workspace);
|
|
8453
|
+
if (!read.ok) {
|
|
8454
|
+
return {
|
|
8455
|
+
text: read.message,
|
|
8456
|
+
structured: {
|
|
8457
|
+
present: false,
|
|
8458
|
+
root: workspace.root,
|
|
8459
|
+
outDir: workspace.config.outDir,
|
|
8460
|
+
message: read.message
|
|
8461
|
+
}
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
const manifest = read.manifest;
|
|
8465
|
+
const versionMatches = manifest.generator.version === GENERATOR_VERSION;
|
|
8466
|
+
const lines = [
|
|
8467
|
+
`Snapshot: ${workspace.config.outDir}/`,
|
|
8468
|
+
`Generated: ${manifest.generatedAt} by ${manifest.generator.name} ${manifest.generator.version}`,
|
|
8469
|
+
`Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
|
|
8470
|
+
`Content: ${formatBytes(manifest.counts.totalSizeBytes)} of source in ${formatBytes(manifest.totals.totalContentBytes)} emitted`,
|
|
8471
|
+
`Fingerprint: ${manifest.fingerprint}`,
|
|
8472
|
+
`Git: ${manifest.git.branch} @ ${manifest.git.commit}${manifest.git.isDirty ? " (dirty)" : " (clean)"}`
|
|
8473
|
+
];
|
|
8474
|
+
if (manifest.totals.warningCount > 0) lines.push(`Warnings: ${manifest.totals.warningCount}`);
|
|
8475
|
+
if (!versionMatches) {
|
|
8476
|
+
lines.push(
|
|
8477
|
+
`Note: written by ${manifest.generator.version}, this server is ${GENERATOR_VERSION}. Re-run snapshot_generate if the difference matters.`
|
|
8478
|
+
);
|
|
8479
|
+
}
|
|
8480
|
+
return {
|
|
8481
|
+
text: lines.join("\n"),
|
|
8482
|
+
structured: {
|
|
8483
|
+
present: true,
|
|
8484
|
+
root: workspace.root,
|
|
8485
|
+
outDir: workspace.config.outDir,
|
|
8486
|
+
fingerprint: manifest.fingerprint,
|
|
8487
|
+
generatedAt: manifest.generatedAt,
|
|
8488
|
+
generatorVersion: manifest.generator.version,
|
|
8489
|
+
generatorVersionMatches: versionMatches,
|
|
8490
|
+
includedFiles: manifest.counts.totalFiles,
|
|
8491
|
+
skippedFiles: manifest.counts.skippedCount,
|
|
8492
|
+
partCount: manifest.counts.partsCount,
|
|
8493
|
+
warningCount: manifest.totals.warningCount,
|
|
8494
|
+
preset: manifest.config.preset,
|
|
8495
|
+
maxPartBytes: manifest.budgets.maxPartBytes
|
|
8496
|
+
}
|
|
8497
|
+
};
|
|
8498
|
+
}
|
|
8499
|
+
};
|
|
8500
|
+
var planTool = {
|
|
8501
|
+
name: "snapshot_plan",
|
|
8502
|
+
title: "Preview a snapshot",
|
|
8503
|
+
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.",
|
|
8504
|
+
inputSchema: {
|
|
8505
|
+
type: "object",
|
|
8506
|
+
properties: {
|
|
8507
|
+
root: ROOT_PROPERTY,
|
|
8508
|
+
maxPartBytes: {
|
|
8509
|
+
type: "string",
|
|
8510
|
+
description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
|
|
8511
|
+
}
|
|
8512
|
+
},
|
|
8513
|
+
additionalProperties: false
|
|
8514
|
+
},
|
|
8515
|
+
outputSchema: {
|
|
8516
|
+
type: "object",
|
|
8517
|
+
properties: {
|
|
8518
|
+
files: { type: "integer" },
|
|
8519
|
+
skipped: { type: "integer" },
|
|
8520
|
+
parts: { type: "integer" },
|
|
8521
|
+
sourceBytes: { type: "integer" },
|
|
8522
|
+
emittedBytes: { type: "integer" },
|
|
8523
|
+
preset: { type: "string" }
|
|
8524
|
+
}
|
|
8525
|
+
},
|
|
8526
|
+
annotations: READ_ONLY,
|
|
8527
|
+
async handler(args, context) {
|
|
8528
|
+
const prepared = await prepare(args, context, { maxPartBytes: args.maxPartBytes });
|
|
8529
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8530
|
+
const workspace = prepared.workspace;
|
|
8531
|
+
let manifest;
|
|
8532
|
+
try {
|
|
8533
|
+
manifest = generateSnapshot(workspace.config, { dryRun: true });
|
|
8534
|
+
} catch (error) {
|
|
8535
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8536
|
+
}
|
|
8537
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
8538
|
+
for (const record of manifest.skipped) {
|
|
8539
|
+
reasons.set(record.reason, (reasons.get(record.reason) ?? 0) + 1);
|
|
8540
|
+
}
|
|
8541
|
+
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 }));
|
|
8542
|
+
const lines = [
|
|
8543
|
+
"Dry run \u2014 nothing was written.",
|
|
8544
|
+
`Preset: ${manifest.config.preset}`,
|
|
8545
|
+
`Would include ${plural(manifest.counts.totalFiles, "file")} in ${plural(manifest.counts.partsCount, "part")} (${formatBytes(manifest.counts.totalSizeBytes)} of source, ${formatBytes(manifest.totals.totalContentBytes)} emitted).`,
|
|
8546
|
+
`Would skip ${plural(manifest.counts.skippedCount, "file")}.`
|
|
8547
|
+
];
|
|
8548
|
+
if (topReasons.length > 0) {
|
|
8549
|
+
lines.push("Top skip reasons:");
|
|
8550
|
+
for (const entry of topReasons) lines.push(` - ${entry.reason} (${entry.count})`);
|
|
8551
|
+
}
|
|
8552
|
+
if (manifest.warnings.length > 0) {
|
|
8553
|
+
lines.push(`Warnings (${manifest.warnings.length}):`);
|
|
8554
|
+
for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
|
|
8555
|
+
}
|
|
8556
|
+
return {
|
|
8557
|
+
text: lines.join("\n"),
|
|
8558
|
+
structured: {
|
|
8559
|
+
files: manifest.counts.totalFiles,
|
|
8560
|
+
skipped: manifest.counts.skippedCount,
|
|
8561
|
+
parts: manifest.counts.partsCount,
|
|
8562
|
+
sourceBytes: manifest.counts.totalSizeBytes,
|
|
8563
|
+
emittedBytes: manifest.totals.totalContentBytes,
|
|
8564
|
+
preset: manifest.config.preset,
|
|
8565
|
+
maxPartBytes: manifest.budgets.maxPartBytes,
|
|
8566
|
+
layers: manifest.layers,
|
|
8567
|
+
skipReasons: topReasons,
|
|
8568
|
+
warnings: manifest.warnings.slice(0, 5),
|
|
8569
|
+
warningCount: manifest.warnings.length
|
|
8570
|
+
}
|
|
8571
|
+
};
|
|
8572
|
+
}
|
|
8573
|
+
};
|
|
8574
|
+
var generateTool = {
|
|
8575
|
+
name: "snapshot_generate",
|
|
8576
|
+
title: "Generate the snapshot",
|
|
8577
|
+
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.",
|
|
8578
|
+
inputSchema: {
|
|
8579
|
+
type: "object",
|
|
8580
|
+
properties: {
|
|
8581
|
+
root: ROOT_PROPERTY,
|
|
8582
|
+
maxPartBytes: {
|
|
8583
|
+
type: "string",
|
|
8584
|
+
description: 'Byte budget per part, for example 307200 or "300k". Defaults to the project configuration.'
|
|
8585
|
+
},
|
|
8586
|
+
outDir: {
|
|
8587
|
+
type: "string",
|
|
8588
|
+
description: "Output directory relative to the root. Defaults to the project configuration."
|
|
8589
|
+
}
|
|
8590
|
+
},
|
|
8591
|
+
additionalProperties: false
|
|
8592
|
+
},
|
|
8593
|
+
outputSchema: {
|
|
8594
|
+
type: "object",
|
|
8595
|
+
properties: {
|
|
8596
|
+
fingerprint: { type: "string" },
|
|
8597
|
+
files: { type: "integer" },
|
|
8598
|
+
skipped: { type: "integer" },
|
|
8599
|
+
parts: { type: "integer" },
|
|
8600
|
+
emittedBytes: { type: "integer" }
|
|
8601
|
+
}
|
|
8602
|
+
},
|
|
8603
|
+
annotations: WRITES,
|
|
8604
|
+
async handler(args, context) {
|
|
8605
|
+
const prepared = await prepare(args, context, {
|
|
8606
|
+
maxPartBytes: args.maxPartBytes,
|
|
8607
|
+
outDir: args.outDir
|
|
8608
|
+
});
|
|
8609
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8610
|
+
const workspace = prepared.workspace;
|
|
8611
|
+
let manifest;
|
|
8612
|
+
try {
|
|
8613
|
+
manifest = generateSnapshot(workspace.config);
|
|
8614
|
+
} catch (error) {
|
|
8615
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8616
|
+
}
|
|
8617
|
+
forgetManifest(manifestPathFor(workspace));
|
|
8618
|
+
const written = relativeFor(workspace, path14.resolve(workspace.root, workspace.config.outDir));
|
|
8619
|
+
const lines = [
|
|
8620
|
+
`Wrote ${written}/`,
|
|
8621
|
+
`Files: ${manifest.counts.totalFiles} included, ${manifest.counts.skippedCount} skipped, ${plural(manifest.counts.partsCount, "part")}`,
|
|
8622
|
+
`Parts: ${manifest.parts.map((part) => `${part.filename} (${formatBytes(part.byteSize)})`).join(", ")}`,
|
|
8623
|
+
`Fingerprint: ${manifest.fingerprint}`
|
|
8624
|
+
];
|
|
8625
|
+
if (manifest.warnings.length > 0) {
|
|
8626
|
+
lines.push(`Warnings (${manifest.warnings.length}):`);
|
|
8627
|
+
for (const warning of manifest.warnings.slice(0, 5)) lines.push(` - ${warning}`);
|
|
8628
|
+
}
|
|
8629
|
+
return {
|
|
8630
|
+
text: lines.join("\n"),
|
|
8631
|
+
structured: {
|
|
8632
|
+
fingerprint: manifest.fingerprint,
|
|
8633
|
+
files: manifest.counts.totalFiles,
|
|
8634
|
+
skipped: manifest.counts.skippedCount,
|
|
8635
|
+
parts: manifest.counts.partsCount,
|
|
8636
|
+
emittedBytes: manifest.totals.totalContentBytes,
|
|
8637
|
+
outDir: written,
|
|
8638
|
+
partFiles: manifest.parts.map((part) => part.filename),
|
|
8639
|
+
warningCount: manifest.warnings.length
|
|
8640
|
+
}
|
|
8641
|
+
};
|
|
8642
|
+
}
|
|
8643
|
+
};
|
|
8644
|
+
var validateTool = {
|
|
8645
|
+
name: "snapshot_validate",
|
|
8646
|
+
title: "Validate the snapshot",
|
|
8647
|
+
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.",
|
|
8648
|
+
inputSchema: {
|
|
8649
|
+
type: "object",
|
|
8650
|
+
properties: { root: ROOT_PROPERTY },
|
|
8651
|
+
additionalProperties: false
|
|
8652
|
+
},
|
|
8653
|
+
outputSchema: {
|
|
8654
|
+
type: "object",
|
|
8655
|
+
properties: {
|
|
8656
|
+
current: { type: "boolean" },
|
|
8657
|
+
manifestPresent: { type: "boolean" }
|
|
8658
|
+
}
|
|
8659
|
+
},
|
|
8660
|
+
annotations: READ_ONLY,
|
|
8661
|
+
async handler(args, context) {
|
|
8662
|
+
const prepared = await prepare(args, context);
|
|
8663
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8664
|
+
const workspace = prepared.workspace;
|
|
8665
|
+
let result;
|
|
8666
|
+
try {
|
|
8667
|
+
result = validateSnapshot(workspace.config);
|
|
8668
|
+
} catch (error) {
|
|
8669
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8670
|
+
}
|
|
8671
|
+
const lines = [];
|
|
8672
|
+
if (result.valid) {
|
|
8673
|
+
lines.push("The snapshot matches the repository.");
|
|
8674
|
+
} else {
|
|
8675
|
+
lines.push("The snapshot is out of date.");
|
|
8676
|
+
const drift = result.drift;
|
|
8677
|
+
const observations = [
|
|
8678
|
+
["file(s) changed on disk", drift.hashMismatches.length],
|
|
8679
|
+
["file(s) added since the snapshot", drift.missingInSnapshot.length],
|
|
8680
|
+
["file(s) gone since the snapshot", drift.missingOnDisk.length],
|
|
8681
|
+
["part file(s) damaged or missing", drift.partIssues.length],
|
|
8682
|
+
["setting(s) changed since the snapshot", drift.configMismatches.length],
|
|
8683
|
+
["internal count(s) disagreeing", drift.totalsMismatches.length]
|
|
8684
|
+
];
|
|
8685
|
+
for (const [label, count] of observations) {
|
|
8686
|
+
if (count > 0) lines.push(` - ${count} ${label}`);
|
|
8687
|
+
}
|
|
8688
|
+
for (const error of result.errors.slice(0, 5)) lines.push(` - ${error}`);
|
|
8689
|
+
lines.push("Run snapshot_generate to refresh it.");
|
|
8690
|
+
}
|
|
8691
|
+
for (const warning of result.warnings.slice(0, 5)) lines.push(`Note: ${warning}`);
|
|
8692
|
+
return {
|
|
8693
|
+
text: lines.join("\n"),
|
|
8694
|
+
structured: {
|
|
8695
|
+
current: result.valid,
|
|
8696
|
+
manifestPresent: result.manifestPresent,
|
|
8697
|
+
drift: summarizeDrift(result.drift),
|
|
8698
|
+
errors: result.errors.slice(0, 10),
|
|
8699
|
+
warnings: result.warnings.slice(0, 10)
|
|
8700
|
+
}
|
|
8701
|
+
// A stale snapshot is an answer, not a tool failure: the caller asked whether it is current.
|
|
8702
|
+
};
|
|
8703
|
+
}
|
|
8704
|
+
};
|
|
8705
|
+
var locateTool = {
|
|
8706
|
+
name: "snapshot_locate",
|
|
8707
|
+
title: "Locate code in the snapshot",
|
|
8708
|
+
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.",
|
|
8709
|
+
inputSchema: {
|
|
8710
|
+
type: "object",
|
|
8711
|
+
properties: {
|
|
8712
|
+
query: {
|
|
8713
|
+
type: "string",
|
|
8714
|
+
description: "Text to match, case-insensitively, against paths and exported symbols."
|
|
8715
|
+
},
|
|
8716
|
+
by: {
|
|
8717
|
+
type: "string",
|
|
8718
|
+
enum: ["auto", "path", "symbol", "route", "layer", "kind"],
|
|
8719
|
+
default: "auto",
|
|
8720
|
+
description: "Restrict what is matched. Defaults to every category."
|
|
8721
|
+
},
|
|
8722
|
+
limit: {
|
|
8723
|
+
type: "integer",
|
|
8724
|
+
minimum: 1,
|
|
8725
|
+
maximum: 100,
|
|
8726
|
+
default: 20,
|
|
8727
|
+
description: "Maximum matches to return."
|
|
8728
|
+
},
|
|
8729
|
+
root: ROOT_PROPERTY
|
|
8730
|
+
},
|
|
8731
|
+
required: ["query"],
|
|
8732
|
+
additionalProperties: false
|
|
8733
|
+
},
|
|
8734
|
+
outputSchema: {
|
|
8735
|
+
type: "object",
|
|
8736
|
+
properties: { total: { type: "integer" } }
|
|
8737
|
+
},
|
|
8738
|
+
annotations: READ_ONLY,
|
|
8739
|
+
async handler(args, context) {
|
|
8740
|
+
const prepared = await prepare(args, context);
|
|
8741
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8742
|
+
const workspace = prepared.workspace;
|
|
8743
|
+
const read = readManifest3(workspace);
|
|
8744
|
+
if (!read.ok) return failure(read.message);
|
|
8745
|
+
const query = String(args.query ?? "").trim();
|
|
8746
|
+
if (query.length === 0) return failure('"query" must not be empty.');
|
|
8747
|
+
const needle = query.toLowerCase();
|
|
8748
|
+
const by = typeof args.by === "string" ? args.by : "auto";
|
|
8749
|
+
const limit = typeof args.limit === "number" ? args.limit : 20;
|
|
8750
|
+
const manifest = read.manifest;
|
|
8751
|
+
const matches = [];
|
|
8752
|
+
const add = (entry, matchedBy, detail) => {
|
|
8753
|
+
matches.push({
|
|
8754
|
+
path: entry.relativePath,
|
|
8755
|
+
part: entry.part,
|
|
8756
|
+
layer: entry.layer,
|
|
8757
|
+
kind: entry.kind,
|
|
8758
|
+
matchedBy,
|
|
8759
|
+
...detail ? { detail } : {}
|
|
8760
|
+
});
|
|
8761
|
+
};
|
|
8762
|
+
const wants = (category) => by === "auto" || by === category;
|
|
8763
|
+
for (const entry of manifest.files) {
|
|
8764
|
+
if (wants("path") && entry.relativePath.toLowerCase().includes(needle)) {
|
|
8765
|
+
add(entry, "path");
|
|
8766
|
+
continue;
|
|
8767
|
+
}
|
|
8768
|
+
if (wants("symbol")) {
|
|
8769
|
+
const symbol = symbolsOf(entry).find((name) => name.toLowerCase().includes(needle));
|
|
8770
|
+
if (symbol) {
|
|
8771
|
+
add(entry, "symbol", symbol);
|
|
8772
|
+
continue;
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8775
|
+
if (wants("layer") && entry.layer.toLowerCase() === needle) add(entry, "layer");
|
|
8776
|
+
else if (wants("kind") && entry.kind.toLowerCase() === needle) add(entry, "kind");
|
|
8777
|
+
}
|
|
8778
|
+
if (wants("route")) {
|
|
8779
|
+
for (const route of manifest.indexes.routes) {
|
|
8780
|
+
if (!route.path.toLowerCase().includes(needle)) continue;
|
|
8781
|
+
if (matches.some((match) => match.path === route.path)) continue;
|
|
8782
|
+
matches.push({
|
|
8783
|
+
path: route.path,
|
|
8784
|
+
part: route.part,
|
|
8785
|
+
layer: "route",
|
|
8786
|
+
kind: "route",
|
|
8787
|
+
matchedBy: "route",
|
|
8788
|
+
detail: route.methods.join(", ")
|
|
8789
|
+
});
|
|
8790
|
+
}
|
|
8791
|
+
}
|
|
8792
|
+
if (matches.length === 0) {
|
|
8793
|
+
return {
|
|
8794
|
+
text: `Nothing in the snapshot matches "${query}". Try a shorter fragment, or snapshot_explain a path to see whether it is included at all.`,
|
|
8795
|
+
structured: { total: 0, matches: [] }
|
|
8796
|
+
};
|
|
8797
|
+
}
|
|
8798
|
+
const shown = matches.slice(0, limit);
|
|
8799
|
+
const lines = [
|
|
8800
|
+
`${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"` + (matches.length > shown.length ? ` (showing ${shown.length})` : "") + ":"
|
|
8801
|
+
];
|
|
8802
|
+
for (const match of shown) {
|
|
8803
|
+
const detail = match.detail ? ` \u2014 ${match.detail}` : "";
|
|
8804
|
+
lines.push(` ${match.path}${detail} [${match.part}, ${match.layer}/${match.kind}]`);
|
|
8805
|
+
}
|
|
8806
|
+
if (matches.length > shown.length) {
|
|
8807
|
+
lines.push("Raise `limit` to see more.");
|
|
8808
|
+
}
|
|
8809
|
+
return {
|
|
8810
|
+
text: lines.join("\n"),
|
|
8811
|
+
structured: { total: matches.length, returned: shown.length, matches: shown }
|
|
8812
|
+
};
|
|
8813
|
+
}
|
|
8814
|
+
};
|
|
8815
|
+
var readTool = {
|
|
8816
|
+
name: "snapshot_read",
|
|
8817
|
+
title: "Read a file from the snapshot",
|
|
8818
|
+
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.",
|
|
8819
|
+
inputSchema: {
|
|
8820
|
+
type: "object",
|
|
8821
|
+
properties: {
|
|
8822
|
+
path: {
|
|
8823
|
+
type: "string",
|
|
8824
|
+
description: "File path relative to the repository root, as snapshot_locate reports it."
|
|
8825
|
+
},
|
|
8826
|
+
fromLine: {
|
|
8827
|
+
type: "integer",
|
|
8828
|
+
minimum: 1,
|
|
8829
|
+
default: 1,
|
|
8830
|
+
description: "First line to return, 1-based."
|
|
8831
|
+
},
|
|
8832
|
+
lines: {
|
|
8833
|
+
type: "integer",
|
|
8834
|
+
minimum: 1,
|
|
8835
|
+
maximum: 2e3,
|
|
8836
|
+
default: 200,
|
|
8837
|
+
description: "How many lines to return."
|
|
8838
|
+
},
|
|
8839
|
+
root: ROOT_PROPERTY
|
|
8840
|
+
},
|
|
8841
|
+
required: ["path"],
|
|
8842
|
+
additionalProperties: false
|
|
8843
|
+
},
|
|
8844
|
+
outputSchema: {
|
|
8845
|
+
type: "object",
|
|
8846
|
+
properties: {
|
|
8847
|
+
path: { type: "string" },
|
|
8848
|
+
part: { type: "string" },
|
|
8849
|
+
fromLine: { type: "integer" },
|
|
8850
|
+
returnedLines: { type: "integer" },
|
|
8851
|
+
totalLines: { type: "integer" },
|
|
8852
|
+
nextFromLine: { type: "integer" }
|
|
8853
|
+
}
|
|
8854
|
+
},
|
|
8855
|
+
annotations: READ_ONLY,
|
|
8856
|
+
async handler(args, context) {
|
|
8857
|
+
const prepared = await prepare(args, context);
|
|
8858
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8859
|
+
const workspace = prepared.workspace;
|
|
8860
|
+
if (typeof args.path !== "string") return failure('"path" must be a string.');
|
|
8861
|
+
const relativePath = toRelativePath(workspace.root, args.path);
|
|
8862
|
+
if (!relativePath) {
|
|
8863
|
+
return failure(
|
|
8864
|
+
`"${args.path}" is outside ${workspace.root}. Paths are read relative to the repository root.`
|
|
8865
|
+
);
|
|
8866
|
+
}
|
|
8867
|
+
const read = readManifest3(workspace);
|
|
8868
|
+
if (!read.ok) return failure(read.message);
|
|
8869
|
+
const manifest = read.manifest;
|
|
8870
|
+
const entry = manifest.files.find((file) => file.relativePath === relativePath) ?? manifest.files.find((file) => file.relativePath.toLowerCase() === relativePath.toLowerCase());
|
|
8871
|
+
if (!entry) {
|
|
8872
|
+
const skipped = manifest.skipped.find((record) => record.path === relativePath);
|
|
8873
|
+
if (skipped) {
|
|
8874
|
+
const detail = skipped.detail ? ` (${skipped.detail})` : "";
|
|
8875
|
+
return failure(
|
|
8876
|
+
`${relativePath} is not in the snapshot: ${skipped.reason}${detail}. Sensitive and ignored files stay out unless they are included explicitly.`
|
|
8877
|
+
);
|
|
8878
|
+
}
|
|
8879
|
+
return failure(
|
|
8880
|
+
`${relativePath} is not in the snapshot. Call snapshot_locate to find the path, or snapshot_explain to learn why it was excluded.`
|
|
8881
|
+
);
|
|
8882
|
+
}
|
|
8883
|
+
const partPath = path14.resolve(workspace.root, workspace.config.outDir, "parts", entry.part);
|
|
8884
|
+
let partText;
|
|
8885
|
+
try {
|
|
8886
|
+
partText = fs13.readFileSync(partPath, "utf8");
|
|
8887
|
+
} catch {
|
|
8888
|
+
return failure(
|
|
8889
|
+
`${entry.part} could not be read, so the snapshot is damaged. Run snapshot_generate to rebuild it.`
|
|
8890
|
+
);
|
|
8891
|
+
}
|
|
8892
|
+
const section = findSection(parsePartSections(partText), entry.relativePath, entry.hashSha256);
|
|
8893
|
+
if (!section) {
|
|
8894
|
+
return failure(
|
|
8895
|
+
`${relativePath} is recorded in ${entry.part} but its section could not be located there. Run snapshot_generate to rebuild the snapshot.`
|
|
8896
|
+
);
|
|
8897
|
+
}
|
|
8898
|
+
const fromLine = typeof args.fromLine === "number" ? args.fromLine : 1;
|
|
8899
|
+
const lineCount = typeof args.lines === "number" ? args.lines : 200;
|
|
8900
|
+
const window = windowText(section.body, fromLine, lineCount);
|
|
8901
|
+
let body = window.text;
|
|
8902
|
+
let truncatedByBytes = false;
|
|
8903
|
+
if (Buffer.byteLength(body, "utf8") > MAX_RESULT_BYTES) {
|
|
8904
|
+
body = body.slice(0, MAX_RESULT_BYTES);
|
|
8905
|
+
const lastBreak = body.lastIndexOf("\n");
|
|
8906
|
+
body = lastBreak > 0 ? body.slice(0, lastBreak) : body;
|
|
8907
|
+
truncatedByBytes = true;
|
|
8908
|
+
}
|
|
8909
|
+
const returnedLines = body.length === 0 ? 0 : body.split("\n").length;
|
|
8910
|
+
const moreRemain = window.truncated || truncatedByBytes;
|
|
8911
|
+
const header = [
|
|
8912
|
+
`${entry.relativePath} \u2014 ${entry.part}, ${entry.layer}/${entry.kind}, ${entry.language}`,
|
|
8913
|
+
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}.`
|
|
8914
|
+
];
|
|
8915
|
+
const lines = [...header, "", body];
|
|
8916
|
+
if (moreRemain) {
|
|
8917
|
+
const next = window.fromLine + returnedLines;
|
|
8918
|
+
lines.push("", `Truncated. Call snapshot_read with fromLine=${next} for the rest.`);
|
|
8919
|
+
}
|
|
8920
|
+
return {
|
|
8921
|
+
text: lines.join("\n"),
|
|
8922
|
+
structured: {
|
|
8923
|
+
path: entry.relativePath,
|
|
8924
|
+
part: entry.part,
|
|
8925
|
+
layer: entry.layer,
|
|
8926
|
+
kind: entry.kind,
|
|
8927
|
+
language: entry.language,
|
|
8928
|
+
inclusion: section.inclusion ?? entry.inclusionMode,
|
|
8929
|
+
fromLine: window.fromLine,
|
|
8930
|
+
returnedLines,
|
|
8931
|
+
totalLines: window.totalLines,
|
|
8932
|
+
truncated: moreRemain,
|
|
8933
|
+
nextFromLine: moreRemain ? window.fromLine + returnedLines : null,
|
|
8934
|
+
sha256: entry.hashSha256
|
|
8935
|
+
}
|
|
8936
|
+
};
|
|
8937
|
+
}
|
|
8938
|
+
};
|
|
8939
|
+
var explainTool = {
|
|
8940
|
+
name: "snapshot_explain",
|
|
8941
|
+
title: "Explain one path",
|
|
8942
|
+
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".',
|
|
8943
|
+
inputSchema: {
|
|
8944
|
+
type: "object",
|
|
8945
|
+
properties: {
|
|
8946
|
+
path: {
|
|
8947
|
+
type: "string",
|
|
8948
|
+
description: "Path relative to the repository root. Directories are explained as a whole."
|
|
8949
|
+
},
|
|
8950
|
+
root: ROOT_PROPERTY
|
|
8951
|
+
},
|
|
8952
|
+
required: ["path"],
|
|
8953
|
+
additionalProperties: false
|
|
8954
|
+
},
|
|
8955
|
+
outputSchema: {
|
|
8956
|
+
type: "object",
|
|
8957
|
+
properties: {
|
|
8958
|
+
path: { type: "string" },
|
|
8959
|
+
included: { type: "boolean" },
|
|
8960
|
+
reason: { type: "string" }
|
|
8961
|
+
}
|
|
8962
|
+
},
|
|
8963
|
+
annotations: READ_ONLY,
|
|
8964
|
+
async handler(args, context) {
|
|
8965
|
+
const prepared = await prepare(args, context);
|
|
8966
|
+
if (!prepared.ok) return prepared.outcome;
|
|
8967
|
+
const workspace = prepared.workspace;
|
|
8968
|
+
if (typeof args.path !== "string") return failure('"path" must be a string.');
|
|
8969
|
+
const relativePath = toRelativePath(workspace.root, args.path);
|
|
8970
|
+
if (!relativePath) {
|
|
8971
|
+
return failure(`"${args.path}" is outside ${workspace.root}.`);
|
|
8972
|
+
}
|
|
8973
|
+
let result;
|
|
8974
|
+
try {
|
|
8975
|
+
result = explainPath(workspace.config, relativePath);
|
|
8976
|
+
} catch (error) {
|
|
8977
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
8978
|
+
}
|
|
8979
|
+
return {
|
|
8980
|
+
text: renderExplainText(result),
|
|
8981
|
+
structured: {
|
|
8982
|
+
path: result.path,
|
|
8983
|
+
included: result.included,
|
|
8984
|
+
...result.reason ? { reason: result.reason } : {},
|
|
8985
|
+
...result.detail ? { detail: result.detail } : {},
|
|
8986
|
+
...result.layer ? { layer: result.layer } : {},
|
|
8987
|
+
...result.kind ? { kind: result.kind } : {},
|
|
8988
|
+
...result.language ? { language: result.language } : {},
|
|
8989
|
+
...result.part ? { part: result.part } : {},
|
|
8990
|
+
...result.inclusionMode ? { inclusion: result.inclusionMode } : {},
|
|
8991
|
+
...typeof result.lines === "number" ? { lines: result.lines } : {},
|
|
8992
|
+
...typeof result.bytes === "number" ? { bytes: result.bytes } : {},
|
|
8993
|
+
...typeof result.secretCount === "number" ? { secretCount: result.secretCount } : {}
|
|
8994
|
+
}
|
|
8995
|
+
};
|
|
8996
|
+
}
|
|
8997
|
+
};
|
|
8998
|
+
function buildTools() {
|
|
8999
|
+
return [
|
|
9000
|
+
statusTool,
|
|
9001
|
+
planTool,
|
|
9002
|
+
generateTool,
|
|
9003
|
+
validateTool,
|
|
9004
|
+
locateTool,
|
|
9005
|
+
readTool,
|
|
9006
|
+
explainTool
|
|
9007
|
+
];
|
|
9008
|
+
}
|
|
9009
|
+
|
|
9010
|
+
// src/mcp/prompts.ts
|
|
9011
|
+
var ORIENT = `You have a codebase-snapshot server for this repository. Build a map of it before reading any code.
|
|
9012
|
+
|
|
9013
|
+
1. Call snapshot_status. If no snapshot exists, call snapshot_plan to preview it, then snapshot_generate.
|
|
9014
|
+
2. Call snapshot_locate for the areas that matter. Search by exported symbol, route or path fragment rather than guessing filenames.
|
|
9015
|
+
3. Report back: the layers present, the entry points, and the files that matter here \u2014 cited as paths with their part.
|
|
9016
|
+
4. Read one file at a time with snapshot_read, one window at a time. Do not read a part end to end.`;
|
|
9017
|
+
var KEEP_CURRENT = `Keep the context you were given provably current while you work.
|
|
9018
|
+
|
|
9019
|
+
1. Before relying on a snapshot from an earlier session, call snapshot_validate.
|
|
9020
|
+
2. If it reports drift, call snapshot_generate once, then snapshot_validate again.
|
|
9021
|
+
3. After changes you make, call snapshot_validate and report what it said.
|
|
9022
|
+
4. Never call a snapshot current unless snapshot_validate said so in this session.`;
|
|
9023
|
+
function withFocus(base, focus) {
|
|
9024
|
+
return typeof focus === "string" && focus.trim().length > 0 ? `${base}
|
|
9025
|
+
|
|
9026
|
+
Focus for this pass: ${focus.trim()}` : base;
|
|
9027
|
+
}
|
|
9028
|
+
function buildPrompts() {
|
|
9029
|
+
return [
|
|
9030
|
+
{
|
|
9031
|
+
name: "orient",
|
|
9032
|
+
title: "Orient in this repository",
|
|
9033
|
+
description: "Map the repository from the snapshot before reading any of it.",
|
|
9034
|
+
arguments: [
|
|
9035
|
+
{
|
|
9036
|
+
name: "focus",
|
|
9037
|
+
description: 'Optional area to concentrate on, for example "authentication".',
|
|
9038
|
+
required: false
|
|
9039
|
+
}
|
|
9040
|
+
],
|
|
9041
|
+
build(args) {
|
|
9042
|
+
return {
|
|
9043
|
+
description: "Map the repository before reading it.",
|
|
9044
|
+
messages: [{ role: "user", content: { type: "text", text: withFocus(ORIENT, args.focus) } }]
|
|
9045
|
+
};
|
|
9046
|
+
}
|
|
9047
|
+
},
|
|
9048
|
+
{
|
|
9049
|
+
name: "keep-current",
|
|
9050
|
+
title: "Keep the snapshot current",
|
|
9051
|
+
description: "Validate before trusting earlier context, and again after making changes.",
|
|
9052
|
+
arguments: [],
|
|
9053
|
+
build() {
|
|
9054
|
+
return {
|
|
9055
|
+
description: "Keep the context current.",
|
|
9056
|
+
messages: [{ role: "user", content: { type: "text", text: KEEP_CURRENT } }]
|
|
9057
|
+
};
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
];
|
|
9061
|
+
}
|
|
9062
|
+
|
|
9063
|
+
// src/mcp/protocol.ts
|
|
9064
|
+
var LEGACY_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
|
|
9065
|
+
var MODERN_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
9066
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
9067
|
+
...MODERN_PROTOCOL_VERSIONS,
|
|
9068
|
+
...LEGACY_PROTOCOL_VERSIONS
|
|
9069
|
+
];
|
|
9070
|
+
var DEFAULT_LEGACY_PROTOCOL_VERSION = LEGACY_PROTOCOL_VERSIONS[0];
|
|
9071
|
+
var ERROR_CODES = {
|
|
9072
|
+
parseError: -32700,
|
|
9073
|
+
invalidRequest: -32600,
|
|
9074
|
+
methodNotFound: -32601,
|
|
9075
|
+
invalidParams: -32602,
|
|
9076
|
+
internalError: -32603,
|
|
9077
|
+
unsupportedProtocolVersion: -32022
|
|
9078
|
+
};
|
|
9079
|
+
var METHOD3 = {
|
|
9080
|
+
initialize: "initialize",
|
|
9081
|
+
initialized: "notifications/initialized",
|
|
9082
|
+
discover: "server/discover",
|
|
9083
|
+
ping: "ping",
|
|
9084
|
+
toolsList: "tools/list",
|
|
9085
|
+
toolsCall: "tools/call",
|
|
9086
|
+
resourcesList: "resources/list",
|
|
9087
|
+
resourcesRead: "resources/read",
|
|
9088
|
+
resourceTemplatesList: "resources/templates/list",
|
|
9089
|
+
promptsList: "prompts/list",
|
|
9090
|
+
promptsGet: "prompts/get",
|
|
9091
|
+
cancelled: "notifications/cancelled"
|
|
9092
|
+
};
|
|
9093
|
+
var META_KEYS = {
|
|
9094
|
+
protocolVersion: "io.modelcontextprotocol/protocolVersion",
|
|
9095
|
+
clientCapabilities: "io.modelcontextprotocol/clientCapabilities",
|
|
9096
|
+
clientInfo: "io.modelcontextprotocol/clientInfo",
|
|
9097
|
+
serverInfo: "io.modelcontextprotocol/serverInfo",
|
|
9098
|
+
progressToken: "progressToken"
|
|
9099
|
+
};
|
|
9100
|
+
function isJsonObject(value) {
|
|
9101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9102
|
+
}
|
|
9103
|
+
function serverInfoMeta(info2) {
|
|
9104
|
+
const value = { name: info2.name, version: info2.version };
|
|
9105
|
+
if (info2.title) value.title = info2.title;
|
|
9106
|
+
return { [META_KEYS.serverInfo]: value };
|
|
9107
|
+
}
|
|
9108
|
+
function successResponse(id, result, modern, info2) {
|
|
9109
|
+
if (!modern) return { jsonrpc: "2.0", id, result };
|
|
9110
|
+
const withMeta = {
|
|
9111
|
+
...result,
|
|
9112
|
+
resultType: "complete",
|
|
9113
|
+
_meta: { ...isJsonObject(result._meta) ? result._meta : {}, ...serverInfoMeta(info2) }
|
|
9114
|
+
};
|
|
9115
|
+
return { jsonrpc: "2.0", id, result: withMeta };
|
|
9116
|
+
}
|
|
9117
|
+
function errorResponse(id, code, message, data) {
|
|
9118
|
+
const error = { code, message };
|
|
9119
|
+
if (data !== void 0) error.data = data;
|
|
9120
|
+
return { jsonrpc: "2.0", id, error };
|
|
9121
|
+
}
|
|
9122
|
+
|
|
9123
|
+
// src/mcp/uris.ts
|
|
9124
|
+
var STATIC_RESOURCE_URIS = [
|
|
9125
|
+
"snapshot://manifest",
|
|
9126
|
+
"snapshot://contract",
|
|
9127
|
+
"snapshot://skill",
|
|
9128
|
+
"snapshot://reports/audit",
|
|
9129
|
+
"snapshot://reports/tree",
|
|
9130
|
+
"snapshot://reports/navigation"
|
|
9131
|
+
];
|
|
9132
|
+
var RESOURCE_TEMPLATE_URIS = ["snapshot://parts/{index}"];
|
|
9133
|
+
|
|
9134
|
+
// src/agent/option-scope.ts
|
|
9135
|
+
var VALIDATE_OPTIONS = [
|
|
9136
|
+
"cwd",
|
|
9137
|
+
"out-dir",
|
|
9138
|
+
"max-part-bytes",
|
|
9139
|
+
"large-file-bytes",
|
|
9140
|
+
"max-monolith-bytes",
|
|
9141
|
+
"include-monolith",
|
|
9142
|
+
"include-sensitive-files",
|
|
9143
|
+
"skip-artifacts",
|
|
9144
|
+
"only-artifacts",
|
|
9145
|
+
"no-config",
|
|
9146
|
+
"discovery",
|
|
9147
|
+
"format",
|
|
9148
|
+
"mode",
|
|
9149
|
+
"help",
|
|
9150
|
+
"version"
|
|
9151
|
+
];
|
|
9152
|
+
var COMMAND_OPTION_SCOPE = {
|
|
9153
|
+
generate: [
|
|
9154
|
+
"cwd",
|
|
9155
|
+
"out-dir",
|
|
9156
|
+
"max-part-bytes",
|
|
9157
|
+
"large-file-bytes",
|
|
9158
|
+
"max-monolith-bytes",
|
|
9159
|
+
"include-monolith",
|
|
9160
|
+
"include-sensitive-files",
|
|
9161
|
+
"skip-artifacts",
|
|
9162
|
+
"only-artifacts",
|
|
9163
|
+
"no-config",
|
|
9164
|
+
"discovery",
|
|
9165
|
+
"format",
|
|
9166
|
+
"mode",
|
|
9167
|
+
"dry-run",
|
|
9168
|
+
"watch",
|
|
9169
|
+
"autopilot",
|
|
9170
|
+
"no-install",
|
|
9171
|
+
"help",
|
|
9172
|
+
"version"
|
|
9173
|
+
],
|
|
9174
|
+
validate: VALIDATE_OPTIONS,
|
|
9175
|
+
report: [
|
|
9176
|
+
"cwd",
|
|
9177
|
+
"out-dir",
|
|
9178
|
+
"max-part-bytes",
|
|
9179
|
+
"large-file-bytes",
|
|
9180
|
+
"max-monolith-bytes",
|
|
9181
|
+
"include-monolith",
|
|
9182
|
+
"include-sensitive-files",
|
|
9183
|
+
"skip-artifacts",
|
|
9184
|
+
"only-artifacts",
|
|
9185
|
+
"no-config",
|
|
9186
|
+
"format",
|
|
9187
|
+
"mode",
|
|
9188
|
+
"help",
|
|
9189
|
+
"version"
|
|
9190
|
+
],
|
|
9191
|
+
explain: [
|
|
9192
|
+
"cwd",
|
|
9193
|
+
"out-dir",
|
|
9194
|
+
"max-part-bytes",
|
|
9195
|
+
"large-file-bytes",
|
|
9196
|
+
"max-monolith-bytes",
|
|
9197
|
+
"include-monolith",
|
|
9198
|
+
"include-sensitive-files",
|
|
9199
|
+
"skip-artifacts",
|
|
9200
|
+
"only-artifacts",
|
|
9201
|
+
"no-config",
|
|
9202
|
+
"discovery",
|
|
9203
|
+
"format",
|
|
9204
|
+
"mode",
|
|
9205
|
+
"help",
|
|
9206
|
+
"version"
|
|
9207
|
+
],
|
|
9208
|
+
diff: ["format", "mode", "no-config", "help", "version"],
|
|
9209
|
+
describe: ["format", "mode", "help", "version"],
|
|
9210
|
+
init: [
|
|
9211
|
+
"cwd",
|
|
9212
|
+
"out-dir",
|
|
9213
|
+
"max-part-bytes",
|
|
9214
|
+
"include-sensitive-files",
|
|
9215
|
+
"skip-artifacts",
|
|
9216
|
+
"only-artifacts",
|
|
9217
|
+
"preset",
|
|
9218
|
+
"include",
|
|
9219
|
+
"exclude",
|
|
9220
|
+
"yes",
|
|
9221
|
+
"force",
|
|
9222
|
+
"dry-run",
|
|
9223
|
+
"format",
|
|
9224
|
+
"mode",
|
|
9225
|
+
"help",
|
|
9226
|
+
"version"
|
|
9227
|
+
],
|
|
9228
|
+
skill: ["cwd", "format", "mode", "install", "target", "help", "version"],
|
|
9229
|
+
// `mcp` reads the repository, so it takes --cwd. It has no --format: stdout carries JSON-RPC
|
|
9230
|
+
// messages, and there is no second shape to choose, so offering the flag would be a lie.
|
|
9231
|
+
mcp: ["cwd", "mode", "help", "version"],
|
|
9232
|
+
setup: [
|
|
9233
|
+
"cwd",
|
|
9234
|
+
"out-dir",
|
|
9235
|
+
"max-part-bytes",
|
|
9236
|
+
"include-sensitive-files",
|
|
9237
|
+
"skip-artifacts",
|
|
9238
|
+
"only-artifacts",
|
|
9239
|
+
"preset",
|
|
9240
|
+
"include",
|
|
9241
|
+
"exclude",
|
|
9242
|
+
"yes",
|
|
9243
|
+
"force",
|
|
9244
|
+
"no-install",
|
|
9245
|
+
"dry-run",
|
|
9246
|
+
"format",
|
|
9247
|
+
"mode",
|
|
9248
|
+
"help",
|
|
9249
|
+
"version"
|
|
9250
|
+
]
|
|
9251
|
+
};
|
|
9252
|
+
var COMMAND_ORDER = [
|
|
9253
|
+
"generate",
|
|
9254
|
+
"validate",
|
|
9255
|
+
"report",
|
|
9256
|
+
"explain",
|
|
9257
|
+
"init",
|
|
9258
|
+
"setup",
|
|
9259
|
+
"diff",
|
|
9260
|
+
"describe",
|
|
9261
|
+
"skill",
|
|
9262
|
+
"mcp"
|
|
9263
|
+
];
|
|
9264
|
+
function commandsForOption(option) {
|
|
9265
|
+
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
9266
|
+
}
|
|
9267
|
+
function optionAppliesTo(option) {
|
|
9268
|
+
return commandsForOption(option).join("/");
|
|
9269
|
+
}
|
|
9270
|
+
|
|
9271
|
+
// src/agent/contract.ts
|
|
9272
|
+
var CONTRACT_VERSION = "1.0.0";
|
|
9273
|
+
var PACKAGE_NAME = "codebase-snapshot";
|
|
9274
|
+
var MAJOR_VERSION = GENERATOR_VERSION.split(".")[0];
|
|
9275
|
+
var ARTIFACT_SUMMARY = {
|
|
9276
|
+
readme: "README.md describing the snapshot at the output root.",
|
|
8052
9277
|
manifest: "manifest.json: machine-readable index of every file, part, hash and config.",
|
|
8053
9278
|
parts: "parts/*.md: the packed, budget-limited markdown parts that carry the code.",
|
|
8054
9279
|
navigation: "reports/navigation.md: links between parts and layers.",
|
|
@@ -8108,6 +9333,19 @@ function renderContractText(contract) {
|
|
|
8108
9333
|
lines.push(` ${"".padEnd(24)} ${option.summary}`);
|
|
8109
9334
|
}
|
|
8110
9335
|
lines.push("");
|
|
9336
|
+
lines.push("MCP:");
|
|
9337
|
+
lines.push(` command ${contract.mcp.command}`);
|
|
9338
|
+
lines.push(` transport ${contract.mcp.transport}`);
|
|
9339
|
+
lines.push(
|
|
9340
|
+
` versions modern ${contract.mcp.protocolVersions.modern.join(", ")} / legacy ${contract.mcp.protocolVersions.legacy.join(", ")}`
|
|
9341
|
+
);
|
|
9342
|
+
lines.push(
|
|
9343
|
+
` tools ${contract.mcp.tools.map((tool) => tool.readOnly ? tool.name : `${tool.name} (writes)`).join(", ")}`
|
|
9344
|
+
);
|
|
9345
|
+
lines.push(` prompts ${contract.mcp.prompts.map((prompt) => prompt.name).join(", ")}`);
|
|
9346
|
+
lines.push(` resources ${contract.mcp.resources.join(", ")}`);
|
|
9347
|
+
lines.push(` templates ${contract.mcp.resourceTemplates.join(", ")}`);
|
|
9348
|
+
lines.push("");
|
|
8111
9349
|
lines.push("Safety:");
|
|
8112
9350
|
for (const note of contract.safety) lines.push(` - ${note}`);
|
|
8113
9351
|
lines.push("");
|
|
@@ -8133,11 +9371,13 @@ function buildAgentContract() {
|
|
|
8133
9371
|
invocation: {
|
|
8134
9372
|
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
8135
9373
|
installed: "npx codebase-snapshot",
|
|
8136
|
-
bins: ["codebase-snapshot", "
|
|
9374
|
+
bins: ["codebase-snapshot", "snapshot"],
|
|
8137
9375
|
notes: [
|
|
8138
9376
|
`The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
|
|
8139
|
-
"
|
|
9377
|
+
"`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.",
|
|
9378
|
+
`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
9379
|
"Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
|
|
9380
|
+
"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
9381
|
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON.",
|
|
8142
9382
|
"A failing run also writes pure JSON to stdout: { error: { message, exitCode } }. The exit code and the stderr line are unchanged."
|
|
8143
9383
|
]
|
|
@@ -8192,9 +9432,9 @@ function buildAgentContract() {
|
|
|
8192
9432
|
{
|
|
8193
9433
|
name: "generate",
|
|
8194
9434
|
aliases: ["default"],
|
|
8195
|
-
summary: "Write the snapshot. This is the command you normally want.",
|
|
9435
|
+
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
9436
|
positionals: [],
|
|
8197
|
-
exits: [0, 1]
|
|
9437
|
+
exits: [0, 1, 2]
|
|
8198
9438
|
},
|
|
8199
9439
|
{
|
|
8200
9440
|
name: "validate",
|
|
@@ -8251,6 +9491,13 @@ function buildAgentContract() {
|
|
|
8251
9491
|
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
9492
|
positionals: [],
|
|
8253
9493
|
exits: [0, 1, 2]
|
|
9494
|
+
},
|
|
9495
|
+
{
|
|
9496
|
+
name: "mcp",
|
|
9497
|
+
aliases: [],
|
|
9498
|
+
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.",
|
|
9499
|
+
positionals: [],
|
|
9500
|
+
exits: [0, 2]
|
|
8254
9501
|
}
|
|
8255
9502
|
],
|
|
8256
9503
|
exitCodes: {
|
|
@@ -8401,6 +9648,13 @@ function buildAgentContract() {
|
|
|
8401
9648
|
appliesTo: optionAppliesTo("force"),
|
|
8402
9649
|
summary: "Overwrite an existing config file."
|
|
8403
9650
|
},
|
|
9651
|
+
{
|
|
9652
|
+
flag: "--autopilot",
|
|
9653
|
+
type: "boolean",
|
|
9654
|
+
default: false,
|
|
9655
|
+
appliesTo: optionAppliesTo("autopilot"),
|
|
9656
|
+
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)."
|
|
9657
|
+
},
|
|
8404
9658
|
{
|
|
8405
9659
|
flag: "--install",
|
|
8406
9660
|
type: "boolean",
|
|
@@ -8420,7 +9674,7 @@ function buildAgentContract() {
|
|
|
8420
9674
|
type: "boolean",
|
|
8421
9675
|
default: false,
|
|
8422
9676
|
appliesTo: optionAppliesTo("no-install"),
|
|
8423
|
-
summary: "
|
|
9677
|
+
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
9678
|
}
|
|
8425
9679
|
],
|
|
8426
9680
|
presets: PRESETS.map((preset) => ({
|
|
@@ -8492,6 +9746,36 @@ function buildAgentContract() {
|
|
|
8492
9746
|
"The tool refuses to replace an outDir that it did not create, so a mistyped outDir cannot delete source code.",
|
|
8493
9747
|
"Nothing is ever written above the repository root; --out-dir may not be the root or an ancestor."
|
|
8494
9748
|
],
|
|
9749
|
+
mcp: {
|
|
9750
|
+
command: `npx ${PACKAGE_NAME} mcp`,
|
|
9751
|
+
transport: "stdio",
|
|
9752
|
+
protocolVersions: {
|
|
9753
|
+
modern: [...MODERN_PROTOCOL_VERSIONS],
|
|
9754
|
+
legacy: [...LEGACY_PROTOCOL_VERSIONS],
|
|
9755
|
+
default: DEFAULT_LEGACY_PROTOCOL_VERSION
|
|
9756
|
+
},
|
|
9757
|
+
tools: buildTools().map((tool) => ({
|
|
9758
|
+
name: tool.name,
|
|
9759
|
+
title: tool.title,
|
|
9760
|
+
summary: tool.description,
|
|
9761
|
+
readOnly: tool.annotations.readOnlyHint,
|
|
9762
|
+
parameters: Object.keys(tool.inputSchema.properties),
|
|
9763
|
+
required: tool.inputSchema.required ?? []
|
|
9764
|
+
})),
|
|
9765
|
+
prompts: buildPrompts().map((prompt) => ({
|
|
9766
|
+
name: prompt.name,
|
|
9767
|
+
summary: prompt.description,
|
|
9768
|
+
arguments: prompt.arguments.map((argument) => argument.name)
|
|
9769
|
+
})),
|
|
9770
|
+
resources: [...STATIC_RESOURCE_URIS],
|
|
9771
|
+
resourceTemplates: [...RESOURCE_TEMPLATE_URIS],
|
|
9772
|
+
notes: [
|
|
9773
|
+
"stdout carries JSON-RPC messages only; every note and log line goes to stderr.",
|
|
9774
|
+
"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`.",
|
|
9775
|
+
"Everything it can do is a subset of the CLI. It never installs packages and never writes a config file.",
|
|
9776
|
+
"A `root` argument may name a sub-directory of the server's --cwd, and nothing outside it is readable."
|
|
9777
|
+
]
|
|
9778
|
+
},
|
|
8495
9779
|
troubleshooting: [
|
|
8496
9780
|
{
|
|
8497
9781
|
symptom: "Command prints nothing and exits 0",
|
|
@@ -8523,8 +9807,8 @@ function buildAgentContract() {
|
|
|
8523
9807
|
}
|
|
8524
9808
|
|
|
8525
9809
|
// src/agent/install-skill.ts
|
|
8526
|
-
import * as
|
|
8527
|
-
import * as
|
|
9810
|
+
import * as fs14 from "fs";
|
|
9811
|
+
import * as path15 from "path";
|
|
8528
9812
|
|
|
8529
9813
|
// src/agent/skill.ts
|
|
8530
9814
|
var PACKAGE_NAME2 = "codebase-snapshot";
|
|
@@ -8553,10 +9837,11 @@ globally, and you do not need to clone anything.
|
|
|
8553
9837
|
|
|
8554
9838
|
**Pin a version** \u2014 \`npx ${PACKAGE_NAME2}@${GENERATOR_VERSION} <command>\` \u2014 when you
|
|
8555
9839
|
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
|
-
|
|
9840
|
+
one executable file, published under two names (\`codebase-snapshot\` and \`snapshot\`), which is
|
|
9841
|
+
what lets npm pick it. A global install gives the short command \`snapshot\`; inside a project
|
|
9842
|
+
that has this package installed, \`npx snapshot\` resolves the local copy first. If a command
|
|
9843
|
+
exits 0 printing nothing, the executable was not resolved \u2014 clear the npm cache and retry;
|
|
9844
|
+
do not assume the repository is broken.
|
|
8560
9845
|
|
|
8561
9846
|
## Step by step
|
|
8562
9847
|
|
|
@@ -8581,8 +9866,11 @@ is broken.
|
|
|
8581
9866
|
does steps 4\u20136 in one go with defaults: it writes the config (an existing one is kept),
|
|
8582
9867
|
adds the npm scripts and the devDependency, installs with the project's package manager,
|
|
8583
9868
|
and generates. Use \`--no-install\` when the machine is offline or the user does not want
|
|
8584
|
-
their lockfile touched.
|
|
8585
|
-
|
|
9869
|
+
their lockfile touched. \`--autopilot\` does exactly the same thing from the default
|
|
9870
|
+
command (\`npx ${PACKAGE_NAME2}@latest --autopilot\`) and is the form to reach for when a
|
|
9871
|
+
single invocation must both set the project up and produce the snapshot. Prefer the
|
|
9872
|
+
explicit steps below when you need to control any of those, or when something fails and
|
|
9873
|
+
you want to see which part.
|
|
8586
9874
|
|
|
8587
9875
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
8588
9876
|
the project shape, then writes a commented config file listing **every** setting at its
|
|
@@ -8590,7 +9878,10 @@ is broken.
|
|
|
8590
9878
|
the running Node can strip types (Node >= 22.6) **and** the project is an ES module
|
|
8591
9879
|
(\`"type": "module"\` in package.json); otherwise \`snapshot.config.mjs\`. Pass
|
|
8592
9880
|
\`--preset <name>\` only if detection is wrong; the preset names are in the contract.
|
|
8593
|
-
|
|
9881
|
+
Skipping this step is safe **at a terminal**: the first \`generate\` asks once, can write
|
|
9882
|
+
the config for you and then generates. Unattended, a \`generate\` with no config exits 2 \u2014
|
|
9883
|
+
run this step, or \`--autopilot\`, or pass \`--no-config\` to run deliberately on the
|
|
9884
|
+
built-in defaults.
|
|
8594
9885
|
|
|
8595
9886
|
5. **Generate.** Run \`npx ${PACKAGE_NAME2}@latest generate\`. Expect exit 0 and a
|
|
8596
9887
|
\`./snapshot/\` directory. If you need a different location use \`--out-dir <path>\`; if
|
|
@@ -8638,6 +9929,10 @@ any of this to get a good first snapshot.
|
|
|
8638
9929
|
deleting anything.
|
|
8639
9930
|
- **Exit 2 is a usage error**, not a failure of the repository. Re-read the contract
|
|
8640
9931
|
before retrying.
|
|
9932
|
+
- **A first run refuses rather than guessing.** In a project with no config and no previous
|
|
9933
|
+
snapshot, \`generate\` without a terminal exits 2 instead of silently using defaults. That
|
|
9934
|
+
is deliberate: say which set-up you want (\`--autopilot\`, \`init --yes\`, or \`--no-config\`)
|
|
9935
|
+
rather than retrying the same command.
|
|
8641
9936
|
- **\`--format json\` keeps stdout pure.** Progress goes to stderr, so \`stdout\` is safe to
|
|
8642
9937
|
pipe into a JSON parser.
|
|
8643
9938
|
|
|
@@ -8653,6 +9948,14 @@ It never writes \`AGENTS.md\`. That file is the repository's own agent contract
|
|
|
8653
9948
|
commands, conventions, house rules \u2014 and this tool has no business editing it. If you want
|
|
8654
9949
|
this skill in there, run \`npx ${PACKAGE_NAME2}@latest skill\` and paste the text yourself: the
|
|
8655
9950
|
diff, and the commit, are then yours.
|
|
9951
|
+
|
|
9952
|
+
## When the client speaks MCP
|
|
9953
|
+
|
|
9954
|
+
If the agent runs inside an MCP client, \`npx ${PACKAGE_NAME2}@latest mcp\` serves this repository
|
|
9955
|
+
as seven tools over stdio instead of shelling out. Its reads come from the snapshot rather than
|
|
9956
|
+
the working tree, so excluded files and redacted values never reach the context. Use
|
|
9957
|
+
\`snapshot_locate\` before \`snapshot_read\`, and \`snapshot_validate\` before trusting a snapshot
|
|
9958
|
+
from an earlier session.
|
|
8656
9959
|
`;
|
|
8657
9960
|
}
|
|
8658
9961
|
|
|
@@ -8663,17 +9966,17 @@ var LEGACY_AGENTS_MARKER = "<!-- codebase-snapshot:skill:start -->";
|
|
|
8663
9966
|
function targetPath(cwd, target) {
|
|
8664
9967
|
switch (target) {
|
|
8665
9968
|
case "claude":
|
|
8666
|
-
return
|
|
9969
|
+
return path15.join(cwd, ".claude", "skills", SKILL_NAME, "SKILL.md");
|
|
8667
9970
|
case "cursor":
|
|
8668
|
-
return
|
|
9971
|
+
return path15.join(cwd, ".cursor", "rules", `${SKILL_NAME}.mdc`);
|
|
8669
9972
|
}
|
|
8670
9973
|
}
|
|
8671
9974
|
function assertWritableInsideRepository(cwd, filePath) {
|
|
8672
|
-
const root =
|
|
8673
|
-
const shown =
|
|
9975
|
+
const root = fs14.realpathSync(cwd);
|
|
9976
|
+
const shown = path15.relative(root, filePath) || filePath;
|
|
8674
9977
|
let stat = null;
|
|
8675
9978
|
try {
|
|
8676
|
-
stat =
|
|
9979
|
+
stat = fs14.lstatSync(filePath);
|
|
8677
9980
|
} catch {
|
|
8678
9981
|
stat = null;
|
|
8679
9982
|
}
|
|
@@ -8689,32 +9992,32 @@ function assertWritableInsideRepository(cwd, filePath) {
|
|
|
8689
9992
|
}
|
|
8690
9993
|
let existing = filePath;
|
|
8691
9994
|
const missing = [];
|
|
8692
|
-
while (!
|
|
8693
|
-
missing.unshift(
|
|
8694
|
-
const parent =
|
|
9995
|
+
while (!fs14.existsSync(existing)) {
|
|
9996
|
+
missing.unshift(path15.basename(existing));
|
|
9997
|
+
const parent = path15.dirname(existing);
|
|
8695
9998
|
if (parent === existing) break;
|
|
8696
9999
|
existing = parent;
|
|
8697
10000
|
}
|
|
8698
|
-
const realAncestor =
|
|
8699
|
-
const resolved = missing.length === 0 ? realAncestor :
|
|
8700
|
-
if (resolved !== root && !resolved.startsWith(`${root}${
|
|
10001
|
+
const realAncestor = fs14.realpathSync(existing);
|
|
10002
|
+
const resolved = missing.length === 0 ? realAncestor : path15.join(realAncestor, ...missing);
|
|
10003
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path15.sep}`)) {
|
|
8701
10004
|
throw new Error(
|
|
8702
10005
|
`${REFUSED}: ${shown} resolves outside the repository (${resolved}). Move it back inside ${root}, or remove the link, and run again.`
|
|
8703
10006
|
);
|
|
8704
10007
|
}
|
|
8705
10008
|
}
|
|
8706
10009
|
function writeIfChanged(target, filePath, content) {
|
|
8707
|
-
const existing =
|
|
10010
|
+
const existing = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf8") : null;
|
|
8708
10011
|
if (existing === content) return { target, path: filePath, action: "unchanged" };
|
|
8709
|
-
|
|
8710
|
-
|
|
10012
|
+
fs14.mkdirSync(path15.dirname(filePath), { recursive: true });
|
|
10013
|
+
fs14.writeFileSync(filePath, content, "utf8");
|
|
8711
10014
|
return { target, path: filePath, action: existing === null ? "created" : "updated" };
|
|
8712
10015
|
}
|
|
8713
10016
|
function legacyAgentsNote(cwd) {
|
|
8714
|
-
const filePath =
|
|
10017
|
+
const filePath = path15.join(cwd, "AGENTS.md");
|
|
8715
10018
|
try {
|
|
8716
|
-
if (!
|
|
8717
|
-
if (!
|
|
10019
|
+
if (!fs14.existsSync(filePath)) return null;
|
|
10020
|
+
if (!fs14.readFileSync(filePath, "utf8").includes(LEGACY_AGENTS_MARKER)) return null;
|
|
8718
10021
|
} catch {
|
|
8719
10022
|
return null;
|
|
8720
10023
|
}
|
|
@@ -8726,9 +10029,9 @@ function installSkill(cwd, target = "all") {
|
|
|
8726
10029
|
const notes = [];
|
|
8727
10030
|
const wanted = target === "all" ? ["claude", "cursor"] : [target];
|
|
8728
10031
|
for (const item of wanted) assertWritableInsideRepository(cwd, targetPath(cwd, item));
|
|
8729
|
-
const createsSomething = wanted.some((item) => !
|
|
10032
|
+
const createsSomething = wanted.some((item) => !fs14.existsSync(targetPath(cwd, item)));
|
|
8730
10033
|
if (createsSomething) {
|
|
8731
|
-
assertWritableInsideRepository(cwd,
|
|
10034
|
+
assertWritableInsideRepository(cwd, path15.join(cwd, ".gitignore"));
|
|
8732
10035
|
}
|
|
8733
10036
|
for (const item of wanted) {
|
|
8734
10037
|
written.push(writeIfChanged(item, targetPath(cwd, item), skill));
|
|
@@ -8744,6 +10047,771 @@ function isSkillTarget(value) {
|
|
|
8744
10047
|
return SKILL_TARGETS.includes(value);
|
|
8745
10048
|
}
|
|
8746
10049
|
|
|
10050
|
+
// src/mcp/era.ts
|
|
10051
|
+
function createSession() {
|
|
10052
|
+
return { era: null, protocolVersion: null, ready: false };
|
|
10053
|
+
}
|
|
10054
|
+
function isLegacyVersion(version) {
|
|
10055
|
+
return LEGACY_PROTOCOL_VERSIONS.includes(version);
|
|
10056
|
+
}
|
|
10057
|
+
function isModernVersion(version) {
|
|
10058
|
+
return MODERN_PROTOCOL_VERSIONS.includes(version);
|
|
10059
|
+
}
|
|
10060
|
+
function inspectModernMeta(params) {
|
|
10061
|
+
const raw = params?._meta;
|
|
10062
|
+
if (!isJsonObject(raw)) return { kind: "absent" };
|
|
10063
|
+
const version = raw[META_KEYS.protocolVersion];
|
|
10064
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
10065
|
+
return { kind: "incomplete", missing: META_KEYS.protocolVersion };
|
|
10066
|
+
}
|
|
10067
|
+
const capabilities2 = raw[META_KEYS.clientCapabilities];
|
|
10068
|
+
if (!isJsonObject(capabilities2)) {
|
|
10069
|
+
return { kind: "incomplete", missing: META_KEYS.clientCapabilities };
|
|
10070
|
+
}
|
|
10071
|
+
const info2 = raw[META_KEYS.clientInfo];
|
|
10072
|
+
return {
|
|
10073
|
+
kind: "present",
|
|
10074
|
+
meta: {
|
|
10075
|
+
version,
|
|
10076
|
+
clientCapabilities: capabilities2,
|
|
10077
|
+
clientInfo: isJsonObject(info2) ? info2 : null
|
|
10078
|
+
}
|
|
10079
|
+
};
|
|
10080
|
+
}
|
|
10081
|
+
function unsupportedVersionData(requested) {
|
|
10082
|
+
return { requested, supported: [...SUPPORTED_PROTOCOL_VERSIONS] };
|
|
10083
|
+
}
|
|
10084
|
+
function classifyRequest(method, params, session) {
|
|
10085
|
+
const inspection = inspectModernMeta(params);
|
|
10086
|
+
if (session.era === "modern") {
|
|
10087
|
+
if (inspection.kind === "absent") {
|
|
10088
|
+
return {
|
|
10089
|
+
kind: "error",
|
|
10090
|
+
code: ERROR_CODES.invalidRequest,
|
|
10091
|
+
message: `This request has no _meta. The modern era requires "${META_KEYS.protocolVersion}" and "${META_KEYS.clientCapabilities}" on every request.`
|
|
10092
|
+
};
|
|
10093
|
+
}
|
|
10094
|
+
if (inspection.kind === "incomplete") {
|
|
10095
|
+
return {
|
|
10096
|
+
kind: "error",
|
|
10097
|
+
code: ERROR_CODES.invalidParams,
|
|
10098
|
+
message: `The request _meta is missing the required key "${inspection.missing}".`
|
|
10099
|
+
};
|
|
10100
|
+
}
|
|
10101
|
+
if (!isModernVersion(inspection.meta.version)) {
|
|
10102
|
+
return {
|
|
10103
|
+
kind: "error",
|
|
10104
|
+
code: ERROR_CODES.unsupportedProtocolVersion,
|
|
10105
|
+
message: `Unsupported protocol version "${inspection.meta.version}".`,
|
|
10106
|
+
data: unsupportedVersionData(inspection.meta.version)
|
|
10107
|
+
};
|
|
10108
|
+
}
|
|
10109
|
+
return { kind: "era", era: "modern", version: inspection.meta.version };
|
|
10110
|
+
}
|
|
10111
|
+
if (session.era === "legacy") {
|
|
10112
|
+
return { kind: "era", era: "legacy", version: session.protocolVersion };
|
|
10113
|
+
}
|
|
10114
|
+
if (method === METHOD3.initialize) {
|
|
10115
|
+
return { kind: "era", era: "legacy", version: null };
|
|
10116
|
+
}
|
|
10117
|
+
if (inspection.kind === "present") {
|
|
10118
|
+
if (!isModernVersion(inspection.meta.version)) {
|
|
10119
|
+
return {
|
|
10120
|
+
kind: "error",
|
|
10121
|
+
code: ERROR_CODES.unsupportedProtocolVersion,
|
|
10122
|
+
message: `Unsupported protocol version "${inspection.meta.version}".`,
|
|
10123
|
+
data: unsupportedVersionData(inspection.meta.version)
|
|
10124
|
+
};
|
|
10125
|
+
}
|
|
10126
|
+
return { kind: "era", era: "modern", version: inspection.meta.version };
|
|
10127
|
+
}
|
|
10128
|
+
if (inspection.kind === "incomplete") {
|
|
10129
|
+
return {
|
|
10130
|
+
kind: "error",
|
|
10131
|
+
code: ERROR_CODES.invalidParams,
|
|
10132
|
+
message: `The request _meta is missing the required key "${inspection.missing}".`
|
|
10133
|
+
};
|
|
10134
|
+
}
|
|
10135
|
+
if (method === METHOD3.discover) {
|
|
10136
|
+
return { kind: "era", era: "modern", version: MODERN_PROTOCOL_VERSIONS[0] };
|
|
10137
|
+
}
|
|
10138
|
+
return { kind: "era", era: "legacy", version: null };
|
|
10139
|
+
}
|
|
10140
|
+
function negotiateLegacyVersion(requested) {
|
|
10141
|
+
if (typeof requested === "string" && isLegacyVersion(requested)) {
|
|
10142
|
+
return { version: requested, matched: true };
|
|
10143
|
+
}
|
|
10144
|
+
return { version: DEFAULT_LEGACY_PROTOCOL_VERSION, matched: false };
|
|
10145
|
+
}
|
|
10146
|
+
|
|
10147
|
+
// src/mcp/schema.ts
|
|
10148
|
+
function validateArguments(schema, value) {
|
|
10149
|
+
if (value === void 0) value = {};
|
|
10150
|
+
if (!isJsonObject(value)) return ["arguments must be a JSON object"];
|
|
10151
|
+
const problems = [];
|
|
10152
|
+
const known = new Set(Object.keys(schema.properties));
|
|
10153
|
+
for (const key of Object.keys(value)) {
|
|
10154
|
+
if (!known.has(key) && schema.additionalProperties !== true) {
|
|
10155
|
+
const allowed = [...known].sort().join(", ");
|
|
10156
|
+
problems.push(`unknown argument "${key}" (accepted: ${allowed || "none"})`);
|
|
10157
|
+
}
|
|
10158
|
+
}
|
|
10159
|
+
for (const required of schema.required ?? []) {
|
|
10160
|
+
if (value[required] === void 0) problems.push(`missing required argument "${required}"`);
|
|
10161
|
+
}
|
|
10162
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
10163
|
+
const argument = value[key];
|
|
10164
|
+
if (argument === void 0) continue;
|
|
10165
|
+
problems.push(...checkValue(key, propertySchema, argument));
|
|
10166
|
+
}
|
|
10167
|
+
return problems;
|
|
10168
|
+
}
|
|
10169
|
+
function checkValue(path18, schema, value) {
|
|
10170
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
10171
|
+
return [`"${path18}" must be one of: ${schema.enum.join(", ")}`];
|
|
10172
|
+
}
|
|
10173
|
+
switch (schema.type) {
|
|
10174
|
+
case "string": {
|
|
10175
|
+
if (typeof value !== "string") return [`"${path18}" must be a string`];
|
|
10176
|
+
const problems = [];
|
|
10177
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
10178
|
+
problems.push(`"${path18}" must be at least ${schema.minLength} character(s)`);
|
|
10179
|
+
}
|
|
10180
|
+
if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
|
|
10181
|
+
problems.push(`"${path18}" must be at most ${schema.maxLength} character(s)`);
|
|
10182
|
+
}
|
|
10183
|
+
return problems;
|
|
10184
|
+
}
|
|
10185
|
+
case "number":
|
|
10186
|
+
case "integer": {
|
|
10187
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
10188
|
+
return [`"${path18}" must be a number`];
|
|
10189
|
+
}
|
|
10190
|
+
if (schema.type === "integer" && !Number.isInteger(value)) {
|
|
10191
|
+
return [`"${path18}" must be an integer`];
|
|
10192
|
+
}
|
|
10193
|
+
const problems = [];
|
|
10194
|
+
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
10195
|
+
problems.push(`"${path18}" must be >= ${schema.minimum}`);
|
|
10196
|
+
}
|
|
10197
|
+
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
10198
|
+
problems.push(`"${path18}" must be <= ${schema.maximum}`);
|
|
10199
|
+
}
|
|
10200
|
+
return problems;
|
|
10201
|
+
}
|
|
10202
|
+
case "boolean":
|
|
10203
|
+
return typeof value === "boolean" ? [] : [`"${path18}" must be a boolean`];
|
|
10204
|
+
case "array": {
|
|
10205
|
+
if (!Array.isArray(value)) return [`"${path18}" must be an array`];
|
|
10206
|
+
if (!schema.items) return [];
|
|
10207
|
+
const problems = [];
|
|
10208
|
+
value.forEach((item, index) => {
|
|
10209
|
+
problems.push(...checkValue(`${path18}[${index}]`, schema.items, item));
|
|
10210
|
+
});
|
|
10211
|
+
return problems;
|
|
10212
|
+
}
|
|
10213
|
+
case "object":
|
|
10214
|
+
return isJsonObject(value) ? [] : [`"${path18}" must be an object`];
|
|
10215
|
+
default:
|
|
10216
|
+
return [];
|
|
10217
|
+
}
|
|
10218
|
+
}
|
|
10219
|
+
function applyDefaults(schema, value) {
|
|
10220
|
+
const out = { ...value };
|
|
10221
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
10222
|
+
if (out[key] === void 0 && propertySchema.default !== void 0) {
|
|
10223
|
+
out[key] = propertySchema.default;
|
|
10224
|
+
}
|
|
10225
|
+
}
|
|
10226
|
+
return out;
|
|
10227
|
+
}
|
|
10228
|
+
|
|
10229
|
+
// src/mcp/server.ts
|
|
10230
|
+
function capabilities() {
|
|
10231
|
+
return {
|
|
10232
|
+
tools: { listChanged: false },
|
|
10233
|
+
resources: { subscribe: false, listChanged: false },
|
|
10234
|
+
prompts: { listChanged: false }
|
|
10235
|
+
};
|
|
10236
|
+
}
|
|
10237
|
+
function requestIdOf(message) {
|
|
10238
|
+
const raw = message.id;
|
|
10239
|
+
return typeof raw === "string" || typeof raw === "number" ? raw : null;
|
|
10240
|
+
}
|
|
10241
|
+
function createMcpServer(options) {
|
|
10242
|
+
const session = createSession();
|
|
10243
|
+
const toolsByName = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
10244
|
+
const promptsByName = new Map(options.prompts.map((prompt) => [prompt.name, prompt]));
|
|
10245
|
+
const identity = () => ({
|
|
10246
|
+
name: options.info.name,
|
|
10247
|
+
title: options.info.title,
|
|
10248
|
+
version: options.info.version,
|
|
10249
|
+
websiteUrl: options.info.websiteUrl
|
|
10250
|
+
});
|
|
10251
|
+
const ok = (id, result, modern) => successResponse(id, result, modern, options.info);
|
|
10252
|
+
function handleNotification(method) {
|
|
10253
|
+
if (method === METHOD3.initialized && session.era === "legacy") session.ready = true;
|
|
10254
|
+
}
|
|
10255
|
+
function initialize(id, params) {
|
|
10256
|
+
const negotiation = negotiateLegacyVersion(params?.protocolVersion);
|
|
10257
|
+
session.era = "legacy";
|
|
10258
|
+
session.protocolVersion = negotiation.version;
|
|
10259
|
+
session.ready = true;
|
|
10260
|
+
options.context.log(
|
|
10261
|
+
negotiation.matched ? `mcp: client initialized with protocol ${negotiation.version}` : `mcp: client asked for an unsupported version and was offered ${negotiation.version}`
|
|
10262
|
+
);
|
|
10263
|
+
return {
|
|
10264
|
+
jsonrpc: "2.0",
|
|
10265
|
+
id,
|
|
10266
|
+
result: {
|
|
10267
|
+
protocolVersion: negotiation.version,
|
|
10268
|
+
capabilities: capabilities(),
|
|
10269
|
+
serverInfo: identity(),
|
|
10270
|
+
instructions: options.instructions
|
|
10271
|
+
}
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
function discover(id) {
|
|
10275
|
+
session.era = "modern";
|
|
10276
|
+
session.ready = true;
|
|
10277
|
+
return ok(
|
|
10278
|
+
id,
|
|
10279
|
+
{
|
|
10280
|
+
supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
10281
|
+
capabilities: capabilities(),
|
|
10282
|
+
instructions: options.instructions,
|
|
10283
|
+
// The capability set depends on which repository the server was started for, so the answer
|
|
10284
|
+
// may be cached only by the client that asked, and not at all after this response.
|
|
10285
|
+
cacheScope: "private",
|
|
10286
|
+
ttlMs: 0
|
|
10287
|
+
},
|
|
10288
|
+
true
|
|
10289
|
+
);
|
|
10290
|
+
}
|
|
10291
|
+
function listTools(id, modern) {
|
|
10292
|
+
const tools = options.tools.map((tool) => ({
|
|
10293
|
+
name: tool.name,
|
|
10294
|
+
title: tool.title,
|
|
10295
|
+
description: tool.description,
|
|
10296
|
+
inputSchema: tool.inputSchema,
|
|
10297
|
+
...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
|
|
10298
|
+
annotations: tool.annotations
|
|
10299
|
+
}));
|
|
10300
|
+
return ok(id, { tools }, modern);
|
|
10301
|
+
}
|
|
10302
|
+
async function callTool(id, modern, params) {
|
|
10303
|
+
const name = params?.name;
|
|
10304
|
+
if (typeof name !== "string") {
|
|
10305
|
+
return errorResponse(id, ERROR_CODES.invalidParams, 'tools/call requires a "name" string.');
|
|
10306
|
+
}
|
|
10307
|
+
const tool = toolsByName.get(name);
|
|
10308
|
+
if (!tool) {
|
|
10309
|
+
const known = [...toolsByName.keys()].sort().join(", ");
|
|
10310
|
+
return errorResponse(id, ERROR_CODES.invalidParams, `Unknown tool "${name}". Available: ${known}.`);
|
|
10311
|
+
}
|
|
10312
|
+
const rawArguments = params?.arguments ?? {};
|
|
10313
|
+
if (!isJsonObject(rawArguments)) {
|
|
10314
|
+
return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
|
|
10315
|
+
}
|
|
10316
|
+
const problems = validateArguments(tool.inputSchema, rawArguments);
|
|
10317
|
+
if (problems.length > 0) {
|
|
10318
|
+
return ok(
|
|
10319
|
+
id,
|
|
10320
|
+
{
|
|
10321
|
+
content: [{ type: "text", text: `Invalid arguments for ${tool.name}:
|
|
10322
|
+
- ${problems.join("\n- ")}` }],
|
|
10323
|
+
isError: true
|
|
10324
|
+
},
|
|
10325
|
+
modern
|
|
10326
|
+
);
|
|
10327
|
+
}
|
|
10328
|
+
const args = applyDefaults(tool.inputSchema, rawArguments);
|
|
10329
|
+
let outcome;
|
|
10330
|
+
try {
|
|
10331
|
+
outcome = await tool.handler(args, options.context);
|
|
10332
|
+
} catch (error) {
|
|
10333
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10334
|
+
options.context.log(`mcp: tool ${tool.name} failed: ${detail}`);
|
|
10335
|
+
return ok(
|
|
10336
|
+
id,
|
|
10337
|
+
{ content: [{ type: "text", text: `${tool.name} failed: ${detail}` }], isError: true },
|
|
10338
|
+
modern
|
|
10339
|
+
);
|
|
10340
|
+
}
|
|
10341
|
+
return ok(
|
|
10342
|
+
id,
|
|
10343
|
+
{
|
|
10344
|
+
content: [{ type: "text", text: outcome.text }],
|
|
10345
|
+
...outcome.structured ? { structuredContent: outcome.structured } : {},
|
|
10346
|
+
...outcome.isError ? { isError: true } : {}
|
|
10347
|
+
},
|
|
10348
|
+
modern
|
|
10349
|
+
);
|
|
10350
|
+
}
|
|
10351
|
+
async function listResources(id, modern) {
|
|
10352
|
+
const resources = await options.resources.list(options.context);
|
|
10353
|
+
return ok(id, { resources }, modern);
|
|
10354
|
+
}
|
|
10355
|
+
async function readResource(id, modern, params) {
|
|
10356
|
+
const uri = params?.uri;
|
|
10357
|
+
if (typeof uri !== "string") {
|
|
10358
|
+
return errorResponse(id, ERROR_CODES.invalidParams, 'resources/read requires a "uri" string.');
|
|
10359
|
+
}
|
|
10360
|
+
const contents = await options.resources.read(uri, options.context);
|
|
10361
|
+
if (!contents) {
|
|
10362
|
+
return errorResponse(id, ERROR_CODES.invalidParams, `Unknown resource "${uri}".`);
|
|
10363
|
+
}
|
|
10364
|
+
return ok(id, { contents: [contents] }, modern);
|
|
10365
|
+
}
|
|
10366
|
+
function listPrompts(id, modern) {
|
|
10367
|
+
const prompts = options.prompts.map((prompt) => ({
|
|
10368
|
+
name: prompt.name,
|
|
10369
|
+
...prompt.title ? { title: prompt.title } : {},
|
|
10370
|
+
description: prompt.description,
|
|
10371
|
+
arguments: prompt.arguments
|
|
10372
|
+
}));
|
|
10373
|
+
return ok(id, { prompts }, modern);
|
|
10374
|
+
}
|
|
10375
|
+
function getPrompt(id, modern, params) {
|
|
10376
|
+
const name = params?.name;
|
|
10377
|
+
if (typeof name !== "string") {
|
|
10378
|
+
return errorResponse(id, ERROR_CODES.invalidParams, 'prompts/get requires a "name" string.');
|
|
10379
|
+
}
|
|
10380
|
+
const prompt = promptsByName.get(name);
|
|
10381
|
+
if (!prompt) {
|
|
10382
|
+
const known = [...promptsByName.keys()].sort().join(", ");
|
|
10383
|
+
return errorResponse(id, ERROR_CODES.invalidParams, `Unknown prompt "${name}". Available: ${known}.`);
|
|
10384
|
+
}
|
|
10385
|
+
const rawArguments = params?.arguments ?? {};
|
|
10386
|
+
if (!isJsonObject(rawArguments)) {
|
|
10387
|
+
return errorResponse(id, ERROR_CODES.invalidParams, '"arguments" must be a JSON object.');
|
|
10388
|
+
}
|
|
10389
|
+
const built = prompt.build(rawArguments);
|
|
10390
|
+
const messages = built.messages.map((message) => ({
|
|
10391
|
+
role: message.role,
|
|
10392
|
+
content: { type: "text", text: message.content.text }
|
|
10393
|
+
}));
|
|
10394
|
+
return ok(id, { description: built.description, messages }, modern);
|
|
10395
|
+
}
|
|
10396
|
+
async function route(name, id, modern, params) {
|
|
10397
|
+
switch (name) {
|
|
10398
|
+
case METHOD3.initialize:
|
|
10399
|
+
return modern ? errorResponse(
|
|
10400
|
+
id,
|
|
10401
|
+
ERROR_CODES.methodNotFound,
|
|
10402
|
+
'"initialize" does not exist in this protocol revision; call "server/discover".'
|
|
10403
|
+
) : initialize(id, params);
|
|
10404
|
+
case METHOD3.discover:
|
|
10405
|
+
return discover(id);
|
|
10406
|
+
case METHOD3.ping:
|
|
10407
|
+
return modern ? errorResponse(
|
|
10408
|
+
id,
|
|
10409
|
+
ERROR_CODES.methodNotFound,
|
|
10410
|
+
'"ping" was removed in this protocol revision; call "server/discover".'
|
|
10411
|
+
) : ok(id, {}, modern);
|
|
10412
|
+
default:
|
|
10413
|
+
break;
|
|
10414
|
+
}
|
|
10415
|
+
if (!session.ready) {
|
|
10416
|
+
return errorResponse(
|
|
10417
|
+
id,
|
|
10418
|
+
ERROR_CODES.invalidRequest,
|
|
10419
|
+
'This connection has not been initialized. Send "initialize" first.'
|
|
10420
|
+
);
|
|
10421
|
+
}
|
|
10422
|
+
switch (name) {
|
|
10423
|
+
case METHOD3.toolsList:
|
|
10424
|
+
return listTools(id, modern);
|
|
10425
|
+
case METHOD3.toolsCall:
|
|
10426
|
+
return callTool(id, modern, params);
|
|
10427
|
+
case METHOD3.resourcesList:
|
|
10428
|
+
return listResources(id, modern);
|
|
10429
|
+
case METHOD3.resourcesRead:
|
|
10430
|
+
return readResource(id, modern, params);
|
|
10431
|
+
case METHOD3.resourceTemplatesList:
|
|
10432
|
+
return ok(id, { resourceTemplates: options.resources.templates() }, modern);
|
|
10433
|
+
case METHOD3.promptsList:
|
|
10434
|
+
return listPrompts(id, modern);
|
|
10435
|
+
case METHOD3.promptsGet:
|
|
10436
|
+
return getPrompt(id, modern, params);
|
|
10437
|
+
default:
|
|
10438
|
+
return errorResponse(id, ERROR_CODES.methodNotFound, `Unknown method "${name}".`);
|
|
10439
|
+
}
|
|
10440
|
+
}
|
|
10441
|
+
async function dispatch(message) {
|
|
10442
|
+
if (!isJsonObject(message)) {
|
|
10443
|
+
return errorResponse(null, ERROR_CODES.parseError, "Message is not a JSON object.");
|
|
10444
|
+
}
|
|
10445
|
+
const isRequest = Object.prototype.hasOwnProperty.call(message, "id");
|
|
10446
|
+
const method = message.method;
|
|
10447
|
+
const params = message.params;
|
|
10448
|
+
if (message.jsonrpc !== "2.0" && isRequest) {
|
|
10449
|
+
return errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry "jsonrpc": "2.0".');
|
|
10450
|
+
}
|
|
10451
|
+
if (typeof method !== "string" || method.length === 0) {
|
|
10452
|
+
return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, 'Message must carry a "method" string.') : null;
|
|
10453
|
+
}
|
|
10454
|
+
if (params !== void 0 && !isJsonObject(params)) {
|
|
10455
|
+
return isRequest ? errorResponse(requestIdOf(message), ERROR_CODES.invalidRequest, '"params" must be a JSON object when present.') : null;
|
|
10456
|
+
}
|
|
10457
|
+
const named = params;
|
|
10458
|
+
const decision = classifyRequest(method, named, session);
|
|
10459
|
+
const modern = decision.kind === "era" ? decision.era === "modern" : session.era === "modern";
|
|
10460
|
+
if (!isRequest) {
|
|
10461
|
+
if (decision.kind === "era" && session.era === null) {
|
|
10462
|
+
session.era = decision.era;
|
|
10463
|
+
session.protocolVersion = decision.version;
|
|
10464
|
+
}
|
|
10465
|
+
handleNotification(method);
|
|
10466
|
+
return null;
|
|
10467
|
+
}
|
|
10468
|
+
const id = requestIdOf(message);
|
|
10469
|
+
if (id === null) {
|
|
10470
|
+
return errorResponse(null, ERROR_CODES.invalidRequest, 'A request "id" must be a string or a number.');
|
|
10471
|
+
}
|
|
10472
|
+
if (decision.kind === "error") {
|
|
10473
|
+
return errorResponse(id, decision.code, decision.message, decision.data);
|
|
10474
|
+
}
|
|
10475
|
+
if (session.era === null) {
|
|
10476
|
+
session.era = decision.era;
|
|
10477
|
+
session.protocolVersion = decision.version;
|
|
10478
|
+
}
|
|
10479
|
+
return route(method, id, modern, named);
|
|
10480
|
+
}
|
|
10481
|
+
return {
|
|
10482
|
+
session,
|
|
10483
|
+
async handle(message) {
|
|
10484
|
+
try {
|
|
10485
|
+
return await dispatch(message);
|
|
10486
|
+
} catch (error) {
|
|
10487
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
10488
|
+
options.context.log(`mcp: internal error: ${detail}`);
|
|
10489
|
+
const id = isJsonObject(message) ? requestIdOf(message) : null;
|
|
10490
|
+
return errorResponse(id, ERROR_CODES.internalError, "Internal server error.");
|
|
10491
|
+
}
|
|
10492
|
+
}
|
|
10493
|
+
};
|
|
10494
|
+
}
|
|
10495
|
+
|
|
10496
|
+
// src/mcp/resources.ts
|
|
10497
|
+
import * as fs15 from "fs";
|
|
10498
|
+
import * as path16 from "path";
|
|
10499
|
+
var MAX_RESOURCE_BYTES = 512 * 1024;
|
|
10500
|
+
var PART_URI = /^snapshot:\/\/parts\/(\d+)$/;
|
|
10501
|
+
var REPORT_URIS = /* @__PURE__ */ new Map([
|
|
10502
|
+
["snapshot://reports/audit", { file: "audit.md", title: "Secret audit and risk findings" }],
|
|
10503
|
+
["snapshot://reports/tree", { file: "project-tree.md", title: "Project tree" }],
|
|
10504
|
+
["snapshot://reports/navigation", { file: "navigation.md", title: "Navigation index" }]
|
|
10505
|
+
]);
|
|
10506
|
+
function formatBytes2(bytes) {
|
|
10507
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
10508
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
|
|
10509
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
10510
|
+
}
|
|
10511
|
+
function truncationMarker(originalBytes) {
|
|
10512
|
+
return `
|
|
10513
|
+
|
|
10514
|
+
[truncated: ${formatBytes2(originalBytes)} total. Use the snapshot_read tool for a line window.]
|
|
10515
|
+
`;
|
|
10516
|
+
}
|
|
10517
|
+
function capUtf8(text2, limit) {
|
|
10518
|
+
const size2 = Buffer.byteLength(text2, "utf8");
|
|
10519
|
+
if (size2 <= limit) return { text: text2, truncated: false };
|
|
10520
|
+
const cut = Buffer.from(text2, "utf8").subarray(0, limit).toString("utf8").replace(/\uFFFD+$/, "");
|
|
10521
|
+
return { text: `${cut}${truncationMarker(size2)}`, truncated: true };
|
|
10522
|
+
}
|
|
10523
|
+
async function workspaceFor(context) {
|
|
10524
|
+
const root = resolveRoot(void 0, context);
|
|
10525
|
+
if (!root.ok) return { error: root.message };
|
|
10526
|
+
const opened = await openWorkspace(root.root);
|
|
10527
|
+
if (!opened.ok) return { error: opened.message };
|
|
10528
|
+
return opened.workspace;
|
|
10529
|
+
}
|
|
10530
|
+
function readFileIfPresent(file) {
|
|
10531
|
+
try {
|
|
10532
|
+
return fs15.readFileSync(file, "utf8");
|
|
10533
|
+
} catch {
|
|
10534
|
+
return null;
|
|
10535
|
+
}
|
|
10536
|
+
}
|
|
10537
|
+
function buildResourceProvider() {
|
|
10538
|
+
return {
|
|
10539
|
+
async list(context) {
|
|
10540
|
+
const resources = [
|
|
10541
|
+
{
|
|
10542
|
+
uri: "snapshot://manifest",
|
|
10543
|
+
name: "manifest",
|
|
10544
|
+
title: "Snapshot manifest",
|
|
10545
|
+
description: "Every file in the snapshot with its layer, kind, language, part and hash, plus the reason each excluded file was excluded.",
|
|
10546
|
+
mimeType: "application/json"
|
|
10547
|
+
},
|
|
10548
|
+
{
|
|
10549
|
+
uri: "snapshot://contract",
|
|
10550
|
+
name: "contract",
|
|
10551
|
+
title: "Tool contract",
|
|
10552
|
+
description: "The machine-readable description of the codebase-snapshot command line.",
|
|
10553
|
+
mimeType: "application/json"
|
|
10554
|
+
},
|
|
10555
|
+
{
|
|
10556
|
+
uri: "snapshot://skill",
|
|
10557
|
+
name: "skill",
|
|
10558
|
+
title: "Agent skill",
|
|
10559
|
+
description: "Usage guidance for an agent working with this tool.",
|
|
10560
|
+
mimeType: "text/markdown"
|
|
10561
|
+
}
|
|
10562
|
+
];
|
|
10563
|
+
for (const [uri, report] of REPORT_URIS) {
|
|
10564
|
+
resources.push({
|
|
10565
|
+
uri,
|
|
10566
|
+
name: report.file.replace(/\.md$/, ""),
|
|
10567
|
+
title: report.title,
|
|
10568
|
+
description: `${report.title} from the most recent snapshot.`,
|
|
10569
|
+
mimeType: "text/markdown"
|
|
10570
|
+
});
|
|
10571
|
+
}
|
|
10572
|
+
const workspace = await workspaceFor(context);
|
|
10573
|
+
if ("error" in workspace) return resources;
|
|
10574
|
+
const read = readManifest3(workspace);
|
|
10575
|
+
if (!read.ok) return resources;
|
|
10576
|
+
for (const part of read.manifest.parts) {
|
|
10577
|
+
resources.push({
|
|
10578
|
+
uri: `snapshot://parts/${part.index}`,
|
|
10579
|
+
name: `part-${String(part.index).padStart(3, "0")}`,
|
|
10580
|
+
title: `Part ${part.index} (${part.fileCount} files, ${formatBytes2(part.byteSize)})`,
|
|
10581
|
+
description: `Part ${part.index} of ${read.manifest.parts.length}: ${part.fileCount} file(s), ${part.lines} line(s), ${formatBytes2(part.byteSize)}.`,
|
|
10582
|
+
mimeType: "text/markdown"
|
|
10583
|
+
});
|
|
10584
|
+
}
|
|
10585
|
+
return resources;
|
|
10586
|
+
},
|
|
10587
|
+
templates() {
|
|
10588
|
+
return [
|
|
10589
|
+
{
|
|
10590
|
+
uriTemplate: "snapshot://parts/{index}",
|
|
10591
|
+
name: "part",
|
|
10592
|
+
title: "Snapshot part",
|
|
10593
|
+
description: "One budgeted part of the snapshot, by its 1-based index.",
|
|
10594
|
+
mimeType: "text/markdown"
|
|
10595
|
+
}
|
|
10596
|
+
];
|
|
10597
|
+
},
|
|
10598
|
+
async read(uri, context) {
|
|
10599
|
+
if (uri === "snapshot://contract") {
|
|
10600
|
+
const contract = buildAgentContract();
|
|
10601
|
+
return { uri, mimeType: "application/json", text: JSON.stringify(contract, null, 2) };
|
|
10602
|
+
}
|
|
10603
|
+
if (uri === "snapshot://skill") {
|
|
10604
|
+
return { uri, mimeType: "text/markdown", text: renderSkill() };
|
|
10605
|
+
}
|
|
10606
|
+
const workspace = await workspaceFor(context);
|
|
10607
|
+
if ("error" in workspace) return null;
|
|
10608
|
+
const report = REPORT_URIS.get(uri);
|
|
10609
|
+
if (report) {
|
|
10610
|
+
const text2 = readFileIfPresent(
|
|
10611
|
+
path16.resolve(workspace.root, workspace.config.outDir, "reports", report.file)
|
|
10612
|
+
);
|
|
10613
|
+
if (text2 === null) return null;
|
|
10614
|
+
return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
|
|
10615
|
+
}
|
|
10616
|
+
if (uri === "snapshot://manifest") {
|
|
10617
|
+
const read = readManifest3(workspace);
|
|
10618
|
+
if (!read.ok) return null;
|
|
10619
|
+
const text2 = readFileIfPresent(read.file) ?? JSON.stringify(read.manifest, null, 2);
|
|
10620
|
+
return { uri, mimeType: "application/json", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
|
|
10621
|
+
}
|
|
10622
|
+
const partMatch = PART_URI.exec(uri);
|
|
10623
|
+
if (partMatch) {
|
|
10624
|
+
const read = readManifest3(workspace);
|
|
10625
|
+
if (!read.ok) return null;
|
|
10626
|
+
const index = Number(partMatch[1]);
|
|
10627
|
+
const part = read.manifest.parts.find((candidate) => candidate.index === index);
|
|
10628
|
+
if (!part) return null;
|
|
10629
|
+
const text2 = readFileIfPresent(
|
|
10630
|
+
path16.resolve(workspace.root, workspace.config.outDir, "parts", part.filename)
|
|
10631
|
+
);
|
|
10632
|
+
if (text2 === null) return null;
|
|
10633
|
+
return { uri, mimeType: "text/markdown", text: capUtf8(text2, MAX_RESOURCE_BYTES).text };
|
|
10634
|
+
}
|
|
10635
|
+
return null;
|
|
10636
|
+
}
|
|
10637
|
+
};
|
|
10638
|
+
}
|
|
10639
|
+
|
|
10640
|
+
// src/mcp/instructions.ts
|
|
10641
|
+
var SERVER_INSTRUCTIONS = [
|
|
10642
|
+
"codebase-snapshot serves this repository as a size-budgeted snapshot, not as a directory walk.",
|
|
10643
|
+
"",
|
|
10644
|
+
"Work in this order:",
|
|
10645
|
+
"1. snapshot_status \u2014 is there a snapshot here, and has the tree moved since it was built?",
|
|
10646
|
+
"2. snapshot_locate \u2014 find which part holds the code you need.",
|
|
10647
|
+
"3. snapshot_read \u2014 read a window of one file. Never read a part end to end.",
|
|
10648
|
+
"4. snapshot_validate \u2014 before trusting context that came from an earlier conversation.",
|
|
10649
|
+
"",
|
|
10650
|
+
"Rules that matter:",
|
|
10651
|
+
"- Parts are budgeted, not small: a 12-part snapshot is hundreds of kilobytes. Use snapshot_locate first.",
|
|
10652
|
+
"- A file absent from the snapshot has a recorded reason. Ask snapshot_explain, do not assume.",
|
|
10653
|
+
"- snapshot_generate is the only tool that writes. It never installs packages and never writes config.",
|
|
10654
|
+
"- Paths are relative to the repository root the server was started for, and nothing outside it is readable."
|
|
10655
|
+
].join("\n");
|
|
10656
|
+
|
|
10657
|
+
// src/mcp/stdio.ts
|
|
10658
|
+
var MAX_LINE_BYTES = 32 * 1024 * 1024;
|
|
10659
|
+
function createWriter(output) {
|
|
10660
|
+
return {
|
|
10661
|
+
write(value) {
|
|
10662
|
+
return new Promise((resolve9, reject) => {
|
|
10663
|
+
output.write(`${JSON.stringify(value)}
|
|
10664
|
+
`, (error) => {
|
|
10665
|
+
if (error) reject(error);
|
|
10666
|
+
else resolve9();
|
|
10667
|
+
});
|
|
10668
|
+
});
|
|
10669
|
+
}
|
|
10670
|
+
};
|
|
10671
|
+
}
|
|
10672
|
+
function createLineReader(input, options) {
|
|
10673
|
+
let buffer = "";
|
|
10674
|
+
let discarding = false;
|
|
10675
|
+
let discardedBytes = 0;
|
|
10676
|
+
const deliver = (line) => {
|
|
10677
|
+
if (discarding) {
|
|
10678
|
+
discarding = false;
|
|
10679
|
+
discardedBytes = 0;
|
|
10680
|
+
return;
|
|
10681
|
+
}
|
|
10682
|
+
const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
10683
|
+
if (trimmed.trim().length === 0) return;
|
|
10684
|
+
options.onLine(trimmed);
|
|
10685
|
+
};
|
|
10686
|
+
const drain = () => {
|
|
10687
|
+
let index = buffer.indexOf("\n");
|
|
10688
|
+
while (index >= 0) {
|
|
10689
|
+
const line = buffer.slice(0, index);
|
|
10690
|
+
buffer = buffer.slice(index + 1);
|
|
10691
|
+
deliver(line);
|
|
10692
|
+
index = buffer.indexOf("\n");
|
|
10693
|
+
}
|
|
10694
|
+
};
|
|
10695
|
+
const onData = (chunk) => {
|
|
10696
|
+
if (discarding) {
|
|
10697
|
+
const index = chunk.indexOf("\n");
|
|
10698
|
+
if (index < 0) {
|
|
10699
|
+
discardedBytes += chunk.length;
|
|
10700
|
+
return;
|
|
10701
|
+
}
|
|
10702
|
+
buffer = chunk.slice(index + 1);
|
|
10703
|
+
deliver("");
|
|
10704
|
+
drain();
|
|
10705
|
+
return;
|
|
10706
|
+
}
|
|
10707
|
+
buffer += chunk;
|
|
10708
|
+
if (buffer.length > MAX_LINE_BYTES && buffer.indexOf("\n") < 0) {
|
|
10709
|
+
discarding = true;
|
|
10710
|
+
discardedBytes = buffer.length;
|
|
10711
|
+
buffer = "";
|
|
10712
|
+
options.onOverflow?.(discardedBytes);
|
|
10713
|
+
return;
|
|
10714
|
+
}
|
|
10715
|
+
drain();
|
|
10716
|
+
};
|
|
10717
|
+
const onEnd = () => {
|
|
10718
|
+
if (buffer.length > 0) {
|
|
10719
|
+
const rest = buffer;
|
|
10720
|
+
buffer = "";
|
|
10721
|
+
deliver(rest);
|
|
10722
|
+
}
|
|
10723
|
+
};
|
|
10724
|
+
input.setEncoding("utf8");
|
|
10725
|
+
input.on("data", onData);
|
|
10726
|
+
input.on("end", onEnd);
|
|
10727
|
+
return () => {
|
|
10728
|
+
input.off("data", onData);
|
|
10729
|
+
input.off("end", onEnd);
|
|
10730
|
+
};
|
|
10731
|
+
}
|
|
10732
|
+
|
|
10733
|
+
// src/mcp/run.ts
|
|
10734
|
+
var PROJECT_URL = "https://ankurhalder.github.io/codebase-snapshot/";
|
|
10735
|
+
function createServer(options) {
|
|
10736
|
+
const cwd = options.cwd;
|
|
10737
|
+
const roots = options.roots && options.roots.length > 0 ? options.roots : [cwd];
|
|
10738
|
+
const context = {
|
|
10739
|
+
cwd,
|
|
10740
|
+
roots,
|
|
10741
|
+
log: options.log ?? ((line) => process.stderr.write(`${line}
|
|
10742
|
+
`))
|
|
10743
|
+
};
|
|
10744
|
+
return createMcpServer({
|
|
10745
|
+
info: {
|
|
10746
|
+
name: GENERATOR_NAME,
|
|
10747
|
+
title: "codebase-snapshot",
|
|
10748
|
+
version: GENERATOR_VERSION,
|
|
10749
|
+
websiteUrl: PROJECT_URL
|
|
10750
|
+
},
|
|
10751
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
10752
|
+
tools: buildTools(),
|
|
10753
|
+
resources: buildResourceProvider(),
|
|
10754
|
+
prompts: buildPrompts(),
|
|
10755
|
+
context
|
|
10756
|
+
});
|
|
10757
|
+
}
|
|
10758
|
+
async function runStdioServer(options) {
|
|
10759
|
+
const log = options.log ?? ((line) => process.stderr.write(`${line}
|
|
10760
|
+
`));
|
|
10761
|
+
const server = createServer(options);
|
|
10762
|
+
const writer = createWriter(process.stdout);
|
|
10763
|
+
let writeChain = Promise.resolve();
|
|
10764
|
+
const send = (message) => {
|
|
10765
|
+
writeChain = writeChain.then(() => writer.write(message)).catch((error) => {
|
|
10766
|
+
log(`mcp: could not write a response: ${error instanceof Error ? error.message : String(error)}`);
|
|
10767
|
+
});
|
|
10768
|
+
return writeChain;
|
|
10769
|
+
};
|
|
10770
|
+
let inFlight = 0;
|
|
10771
|
+
let inputEnded = false;
|
|
10772
|
+
let resolveDone = () => void 0;
|
|
10773
|
+
const finished = new Promise((resolve9) => {
|
|
10774
|
+
resolveDone = resolve9;
|
|
10775
|
+
});
|
|
10776
|
+
const finishIfIdle = () => {
|
|
10777
|
+
if (inputEnded && inFlight === 0) resolveDone(0);
|
|
10778
|
+
};
|
|
10779
|
+
const handleLine = async (line) => {
|
|
10780
|
+
let message;
|
|
10781
|
+
try {
|
|
10782
|
+
message = JSON.parse(line);
|
|
10783
|
+
} catch {
|
|
10784
|
+
await send(errorResponse(null, ERROR_CODES.parseError, "Message is not valid JSON."));
|
|
10785
|
+
return;
|
|
10786
|
+
}
|
|
10787
|
+
const response = await server.handle(message);
|
|
10788
|
+
if (response) await send(response);
|
|
10789
|
+
};
|
|
10790
|
+
const stopReading = createLineReader(process.stdin, {
|
|
10791
|
+
onOverflow: (bytes) => log(`mcp: discarded an oversized message (${bytes} bytes)`),
|
|
10792
|
+
onLine: (line) => {
|
|
10793
|
+
inFlight += 1;
|
|
10794
|
+
void handleLine(line).finally(() => {
|
|
10795
|
+
inFlight -= 1;
|
|
10796
|
+
finishIfIdle();
|
|
10797
|
+
});
|
|
10798
|
+
}
|
|
10799
|
+
});
|
|
10800
|
+
const onEnd = () => {
|
|
10801
|
+
inputEnded = true;
|
|
10802
|
+
finishIfIdle();
|
|
10803
|
+
};
|
|
10804
|
+
process.stdin.on("end", onEnd);
|
|
10805
|
+
process.stdin.on("close", onEnd);
|
|
10806
|
+
log(`mcp: serving ${options.cwd} over stdio`);
|
|
10807
|
+
const code = await finished;
|
|
10808
|
+
stopReading();
|
|
10809
|
+
process.stdin.off("end", onEnd);
|
|
10810
|
+
process.stdin.off("close", onEnd);
|
|
10811
|
+
await writeChain;
|
|
10812
|
+
return code;
|
|
10813
|
+
}
|
|
10814
|
+
|
|
8747
10815
|
// src/cli.ts
|
|
8748
10816
|
var VERSION = GENERATOR_VERSION;
|
|
8749
10817
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -8757,7 +10825,8 @@ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
|
|
|
8757
10825
|
["explain", "explain"],
|
|
8758
10826
|
["diff", "diff"],
|
|
8759
10827
|
["describe", "describe"],
|
|
8760
|
-
["skill", "skill"]
|
|
10828
|
+
["skill", "skill"],
|
|
10829
|
+
["mcp", "mcp"]
|
|
8761
10830
|
]);
|
|
8762
10831
|
var COMMAND_ARITY = {
|
|
8763
10832
|
generate: 0,
|
|
@@ -8768,7 +10837,8 @@ var COMMAND_ARITY = {
|
|
|
8768
10837
|
explain: 1,
|
|
8769
10838
|
diff: 2,
|
|
8770
10839
|
describe: 0,
|
|
8771
|
-
skill: 0
|
|
10840
|
+
skill: 0,
|
|
10841
|
+
mcp: 0
|
|
8772
10842
|
};
|
|
8773
10843
|
var CLI_OPTIONS = {
|
|
8774
10844
|
cwd: { type: "string" },
|
|
@@ -8791,6 +10861,7 @@ var CLI_OPTIONS = {
|
|
|
8791
10861
|
exclude: { type: "string" },
|
|
8792
10862
|
yes: { type: "boolean" },
|
|
8793
10863
|
force: { type: "boolean" },
|
|
10864
|
+
autopilot: { type: "boolean" },
|
|
8794
10865
|
install: { type: "boolean" },
|
|
8795
10866
|
"no-install": { type: "boolean" },
|
|
8796
10867
|
target: { type: "string" },
|
|
@@ -8806,6 +10877,14 @@ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
|
|
|
8806
10877
|
Usage:
|
|
8807
10878
|
codebase-snapshot [command] [options]
|
|
8808
10879
|
|
|
10880
|
+
First run:
|
|
10881
|
+
codebase-snapshot No config yet: asks once, sets the project up, generates.
|
|
10882
|
+
A configured project just generates. It never falls back
|
|
10883
|
+
to defaults it was not given.
|
|
10884
|
+
codebase-snapshot --autopilot The same without questions: detect the preset, write the
|
|
10885
|
+
config, add the npm scripts and devDependency, install,
|
|
10886
|
+
then generate.
|
|
10887
|
+
|
|
8809
10888
|
Commands:
|
|
8810
10889
|
setup Everything at once: config, scripts, dependency, install, generate
|
|
8811
10890
|
generate (default) Generate a complete partitioned codebase snapshot
|
|
@@ -8816,6 +10895,7 @@ Commands:
|
|
|
8816
10895
|
diff <a> <b> Compare two snapshots (directories or manifest.json files)
|
|
8817
10896
|
describe Print the machine-readable contract (--format json for agents)
|
|
8818
10897
|
skill Print the bundled agent skill; --install writes it into the repo
|
|
10898
|
+
mcp Serve this repository to an MCP client over stdio
|
|
8819
10899
|
|
|
8820
10900
|
Options:
|
|
8821
10901
|
--cwd <path> Target repository directory (default: current directory)
|
|
@@ -8838,6 +10918,12 @@ Options:
|
|
|
8838
10918
|
auto: git when usable. git: require git. walk: read
|
|
8839
10919
|
.gitignore directly (escape hatch for a broken .git)
|
|
8840
10920
|
|
|
10921
|
+
generate only:
|
|
10922
|
+
--autopilot Set the project up without asking, then generate: write the
|
|
10923
|
+
config and .gitignore, add the npm scripts and devDependency,
|
|
10924
|
+
and install with the project's own package manager.
|
|
10925
|
+
--no-install With --autopilot: skip the dependency install.
|
|
10926
|
+
|
|
8841
10927
|
init only:
|
|
8842
10928
|
--preset <name> Force a project preset instead of detecting one
|
|
8843
10929
|
--include <globs> Comma-separated allow-list
|
|
@@ -8917,10 +11003,10 @@ function parseCliArgs(argv) {
|
|
|
8917
11003
|
}
|
|
8918
11004
|
if (values.cwd !== void 0) {
|
|
8919
11005
|
const target = String(values.cwd);
|
|
8920
|
-
if (!
|
|
11006
|
+
if (!fs16.existsSync(target)) {
|
|
8921
11007
|
throw new UsageError(`--cwd directory does not exist: ${target}`);
|
|
8922
11008
|
}
|
|
8923
|
-
if (!
|
|
11009
|
+
if (!fs16.statSync(target).isDirectory()) {
|
|
8924
11010
|
throw new UsageError(`--cwd is not a directory: ${target}`);
|
|
8925
11011
|
}
|
|
8926
11012
|
}
|
|
@@ -9040,8 +11126,17 @@ function parseCliArgs(argv) {
|
|
|
9040
11126
|
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
9041
11127
|
);
|
|
9042
11128
|
}
|
|
11129
|
+
if (values["no-install"] === true && command === "generate" && values.autopilot !== true) {
|
|
11130
|
+
throw new UsageError("--no-install only applies to `setup`, or to `generate --autopilot`.");
|
|
11131
|
+
}
|
|
11132
|
+
if (values.autopilot === true && values["no-config"] === true) {
|
|
11133
|
+
throw new UsageError(
|
|
11134
|
+
"--autopilot sets a project up and writes a config; --no-config ignores config files. Pass one or the other."
|
|
11135
|
+
);
|
|
11136
|
+
}
|
|
9043
11137
|
return {
|
|
9044
11138
|
command,
|
|
11139
|
+
explicitCommand: firstPositional !== void 0 || explicitMode !== void 0,
|
|
9045
11140
|
args,
|
|
9046
11141
|
format: rawFormat,
|
|
9047
11142
|
dryRun: values["dry-run"] === true,
|
|
@@ -9053,10 +11148,11 @@ function parseCliArgs(argv) {
|
|
|
9053
11148
|
exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
|
|
9054
11149
|
yes: values.yes === true,
|
|
9055
11150
|
force: values.force === true,
|
|
11151
|
+
autopilot: values.autopilot === true,
|
|
9056
11152
|
install: values.install === true,
|
|
9057
11153
|
noInstall: values["no-install"] === true,
|
|
9058
11154
|
target: values.target === void 0 ? void 0 : String(values.target),
|
|
9059
|
-
cwd: values.cwd === void 0 ? process.cwd() :
|
|
11155
|
+
cwd: values.cwd === void 0 ? process.cwd() : path17.resolve(String(values.cwd)),
|
|
9060
11156
|
outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
|
|
9061
11157
|
includeMonolith: values["include-monolith"] === true || onlyNamesMonolith === true ? true : void 0,
|
|
9062
11158
|
includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
|
|
@@ -9113,26 +11209,29 @@ function splitList(value) {
|
|
|
9113
11209
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
9114
11210
|
}
|
|
9115
11211
|
function displayOutPath(cwd, outDir, ...segments) {
|
|
9116
|
-
const absolute =
|
|
9117
|
-
const
|
|
9118
|
-
if (
|
|
11212
|
+
const absolute = path17.resolve(cwd, outDir, ...segments);
|
|
11213
|
+
const relative11 = path17.relative(cwd, absolute);
|
|
11214
|
+
if (relative11 && !relative11.startsWith("..") && !path17.isAbsolute(relative11)) return relative11;
|
|
9119
11215
|
return absolute;
|
|
9120
11216
|
}
|
|
9121
11217
|
function nextStepHint(cwd, scriptsAddedNow) {
|
|
9122
11218
|
let hasScript = scriptsAddedNow;
|
|
9123
11219
|
if (!hasScript) {
|
|
9124
11220
|
try {
|
|
9125
|
-
const parsed = JSON.parse(
|
|
11221
|
+
const parsed = JSON.parse(fs16.readFileSync(path17.join(cwd, "package.json"), "utf8"));
|
|
9126
11222
|
hasScript = typeof parsed.scripts?.snapshot === "string";
|
|
9127
11223
|
} catch {
|
|
9128
11224
|
hasScript = false;
|
|
9129
11225
|
}
|
|
9130
11226
|
}
|
|
9131
11227
|
if (!hasScript) return `npx ${GENERATOR_NAME} generate`;
|
|
9132
|
-
const bin =
|
|
9133
|
-
const installed =
|
|
11228
|
+
const bin = path17.join(cwd, "node_modules", ".bin", "codebase-snapshot");
|
|
11229
|
+
const installed = fs16.existsSync(bin) || fs16.existsSync(`${bin}.cmd`);
|
|
9134
11230
|
return installed ? "npm run snapshot" : "npm install && npm run snapshot";
|
|
9135
11231
|
}
|
|
11232
|
+
function presetSummary(init) {
|
|
11233
|
+
return init.reason === null ? `preset: ${init.preset}` : `preset: ${init.preset}, because ${init.reason}`;
|
|
11234
|
+
}
|
|
9136
11235
|
async function runInitCommand(invocation) {
|
|
9137
11236
|
try {
|
|
9138
11237
|
const result = await runInit({
|
|
@@ -9157,7 +11256,7 @@ async function runInitCommand(invocation) {
|
|
|
9157
11256
|
info(result.configText);
|
|
9158
11257
|
return 0;
|
|
9159
11258
|
}
|
|
9160
|
-
info(`\u2705 Created ${
|
|
11259
|
+
info(`\u2705 Created ${path17.basename(result.configPath)} (${presetSummary(result)})`);
|
|
9161
11260
|
if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
|
|
9162
11261
|
if (result.dependency.added && result.dependency.range !== null) {
|
|
9163
11262
|
info(` Added devDependency: ${result.dependency.name} ${result.dependency.range}`);
|
|
@@ -9177,7 +11276,7 @@ async function runInitCommand(invocation) {
|
|
|
9177
11276
|
}
|
|
9178
11277
|
}
|
|
9179
11278
|
function detectPackageManager(cwd) {
|
|
9180
|
-
const has = (name) =>
|
|
11279
|
+
const has = (name) => fs16.existsSync(path17.join(cwd, name));
|
|
9181
11280
|
if (has("bun.lockb") || has("bun.lock")) return { name: "bun", command: "bun", args: ["install"] };
|
|
9182
11281
|
if (has("pnpm-lock.yaml")) return { name: "pnpm", command: "pnpm", args: ["install"] };
|
|
9183
11282
|
if (has("yarn.lock")) return { name: "yarn", command: "yarn", args: ["install"] };
|
|
@@ -9196,14 +11295,13 @@ function installDependencies(cwd) {
|
|
|
9196
11295
|
const manager = detectPackageManager(cwd);
|
|
9197
11296
|
const command = `${manager.command} ${manager.args.join(" ")}`;
|
|
9198
11297
|
const started = performance.now();
|
|
9199
|
-
const
|
|
11298
|
+
const spawnOptions = {
|
|
9200
11299
|
cwd,
|
|
9201
11300
|
encoding: "utf8",
|
|
9202
|
-
// npm, pnpm and yarn ship as `.cmd` shims on Windows, which spawnSync cannot exec directly.
|
|
9203
|
-
shell: process.platform === "win32",
|
|
9204
11301
|
timeout: INSTALL_TIMEOUT_MS,
|
|
9205
11302
|
env: cleanInstallEnv()
|
|
9206
|
-
}
|
|
11303
|
+
};
|
|
11304
|
+
const result = process.platform === "win32" ? spawnSync(command, { ...spawnOptions, shell: true }) : spawnSync(manager.command, manager.args, spawnOptions);
|
|
9207
11305
|
const seconds = `${((performance.now() - started) / 1e3).toFixed(1)}s`;
|
|
9208
11306
|
if (result.error) {
|
|
9209
11307
|
return { manager: manager.name, command, ok: false, detail: `${result.error.message} (${seconds})` };
|
|
@@ -9228,8 +11326,9 @@ async function captureStdout(fn) {
|
|
|
9228
11326
|
process.stdout.write = original;
|
|
9229
11327
|
}
|
|
9230
11328
|
}
|
|
9231
|
-
async function runSetupCommand(invocation) {
|
|
11329
|
+
async function runSetupCommand(invocation, options = {}) {
|
|
9232
11330
|
try {
|
|
11331
|
+
const interactive = options.interactive === true;
|
|
9233
11332
|
const init = await runInit({
|
|
9234
11333
|
cwd: invocation.cwd,
|
|
9235
11334
|
preset: invocation.preset,
|
|
@@ -9239,7 +11338,7 @@ async function runSetupCommand(invocation) {
|
|
|
9239
11338
|
exclude: splitList(invocation.exclude),
|
|
9240
11339
|
skipArtifacts: invocation.skipArtifacts,
|
|
9241
11340
|
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
9242
|
-
yes:
|
|
11341
|
+
yes: !interactive,
|
|
9243
11342
|
force: invocation.force,
|
|
9244
11343
|
reuseConfig: true,
|
|
9245
11344
|
dryRun: invocation.dryRun
|
|
@@ -9266,7 +11365,7 @@ async function runSetupCommand(invocation) {
|
|
|
9266
11365
|
}
|
|
9267
11366
|
info("\n\u{1F9EA} Dry run \u2014 setup would:");
|
|
9268
11367
|
info(
|
|
9269
|
-
` 1. ${init.reused ? `keep ${
|
|
11368
|
+
` 1. ${init.reused ? `keep ${path17.basename(init.configPath)}` : `write ${path17.basename(init.configPath)} (preset: ${init.preset})`}`
|
|
9270
11369
|
);
|
|
9271
11370
|
info(" 2. add the npm scripts, the devDependency and the .gitignore entries");
|
|
9272
11371
|
if (manager !== null) info(` 3. install with ${manager}`);
|
|
@@ -9276,11 +11375,11 @@ async function runSetupCommand(invocation) {
|
|
|
9276
11375
|
return 0;
|
|
9277
11376
|
}
|
|
9278
11377
|
if (invocation.format !== "json") {
|
|
9279
|
-
info(`\u{1F680} Setup for: ${invocation.cwd}`);
|
|
11378
|
+
if (!interactive) info(`\u{1F680} Setup for: ${invocation.cwd}`);
|
|
9280
11379
|
if (init.reused) {
|
|
9281
|
-
info(`\u267B Keeping ${
|
|
11380
|
+
info(`\u267B Keeping ${path17.basename(init.configPath)} (${presetSummary(init)})`);
|
|
9282
11381
|
} else {
|
|
9283
|
-
info(`\u2705 Created ${
|
|
11382
|
+
info(`\u2705 Created ${path17.basename(init.configPath)} (${presetSummary(init)})`);
|
|
9284
11383
|
}
|
|
9285
11384
|
if (init.scripts.length > 0) info(` Added npm scripts: ${init.scripts.join(", ")}`);
|
|
9286
11385
|
if (init.dependency.added && init.dependency.range !== null) {
|
|
@@ -9309,6 +11408,7 @@ async function runSetupCommand(invocation) {
|
|
|
9309
11408
|
if ((invocation.skipArtifacts ?? []).length > 0) {
|
|
9310
11409
|
generateArgs.push("--skip-artifacts", (invocation.skipArtifacts ?? []).join(","));
|
|
9311
11410
|
}
|
|
11411
|
+
if (invocation.watch) generateArgs.push("--watch");
|
|
9312
11412
|
if (invocation.format === "json") {
|
|
9313
11413
|
const generated = await captureStdout(() => main(generateArgs));
|
|
9314
11414
|
let manifest = null;
|
|
@@ -9348,6 +11448,24 @@ async function runSetupCommand(invocation) {
|
|
|
9348
11448
|
return fail(invocation.format, err.message);
|
|
9349
11449
|
}
|
|
9350
11450
|
}
|
|
11451
|
+
async function firstRun(invocation) {
|
|
11452
|
+
const plan = planFirstRun({
|
|
11453
|
+
hasConfig: findConfigFile(invocation.cwd) !== null,
|
|
11454
|
+
hasPriorSnapshot: hasPriorSnapshot(invocation.cwd, invocation.outDir ?? DEFAULT_ANSWERS.outDir),
|
|
11455
|
+
// A machine must never be asked a question, and `--format json` is how a machine says so.
|
|
11456
|
+
interactive: invocation.format === "text" && isInteractive(defaultIO()),
|
|
11457
|
+
autopilot: invocation.autopilot,
|
|
11458
|
+
emptyTarget: isEffectivelyEmpty(invocation.cwd)
|
|
11459
|
+
});
|
|
11460
|
+
if (plan.action === "generate") return null;
|
|
11461
|
+
if (plan.action === "refuse") {
|
|
11462
|
+
return fail(invocation.format, firstRunMessage(plan.reason, invocation.cwd), plan.exitCode);
|
|
11463
|
+
}
|
|
11464
|
+
info(
|
|
11465
|
+
plan.interactive ? "\u2699 This project has no snapshot configuration yet." : "\u2699 --autopilot: setting this project up before generating."
|
|
11466
|
+
);
|
|
11467
|
+
return await runSetupCommand(invocation, { interactive: plan.interactive });
|
|
11468
|
+
}
|
|
9351
11469
|
function runSkillCommand(invocation) {
|
|
9352
11470
|
if (!invocation.install) {
|
|
9353
11471
|
if (invocation.format === "json") {
|
|
@@ -9369,7 +11487,7 @@ function runSkillCommand(invocation) {
|
|
|
9369
11487
|
}
|
|
9370
11488
|
info(`\u2705 Agent skill installed (${SKILL_NAME}):`);
|
|
9371
11489
|
for (const item of result.written) {
|
|
9372
|
-
const shown =
|
|
11490
|
+
const shown = path17.relative(invocation.cwd, item.path) || item.path;
|
|
9373
11491
|
info(` - ${item.action.padEnd(9)} ${shown}`);
|
|
9374
11492
|
}
|
|
9375
11493
|
if (result.gitignored.length > 0) {
|
|
@@ -9464,9 +11582,16 @@ ${USAGE}
|
|
|
9464
11582
|
if (invocation.command === "skill") {
|
|
9465
11583
|
return runSkillCommand(invocation);
|
|
9466
11584
|
}
|
|
11585
|
+
if (invocation.command === "mcp") {
|
|
11586
|
+
return await runStdioServer({ cwd: path17.resolve(invocation.cwd) });
|
|
11587
|
+
}
|
|
9467
11588
|
if (invocation.command === "setup") {
|
|
9468
11589
|
return runSetupCommand(invocation);
|
|
9469
11590
|
}
|
|
11591
|
+
if (invocation.command === "generate" && !invocation.noConfig && (!invocation.explicitCommand || invocation.autopilot)) {
|
|
11592
|
+
const decided = await firstRun(invocation);
|
|
11593
|
+
if (decided !== null) return decided;
|
|
11594
|
+
}
|
|
9470
11595
|
const skipConfig = invocation.noConfig || invocation.command === "diff";
|
|
9471
11596
|
const loaded = skipConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
9472
11597
|
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
@@ -9475,7 +11600,7 @@ ${USAGE}
|
|
|
9475
11600
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
9476
11601
|
return 1;
|
|
9477
11602
|
}
|
|
9478
|
-
if (loaded.file) info(`\u2699 Using config ${
|
|
11603
|
+
if (loaded.file) info(`\u2699 Using config ${path17.relative(process.cwd(), loaded.file) || loaded.file}`);
|
|
9479
11604
|
const resolved = resolveConfig(
|
|
9480
11605
|
loaded.config,
|
|
9481
11606
|
{
|
|
@@ -9497,10 +11622,10 @@ ${USAGE}
|
|
|
9497
11622
|
if (invocation.command === "validate") {
|
|
9498
11623
|
const result = validateSnapshot(resolved);
|
|
9499
11624
|
if (result.valid) {
|
|
9500
|
-
const manifestPath =
|
|
11625
|
+
const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
9501
11626
|
let manifest;
|
|
9502
11627
|
try {
|
|
9503
|
-
manifest = JSON.parse(
|
|
11628
|
+
manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
|
|
9504
11629
|
} catch {
|
|
9505
11630
|
manifest = void 0;
|
|
9506
11631
|
}
|
|
@@ -9513,13 +11638,13 @@ ${USAGE}
|
|
|
9513
11638
|
return result.valid ? 0 : 1;
|
|
9514
11639
|
}
|
|
9515
11640
|
if (invocation.command === "report") {
|
|
9516
|
-
const manifestPath =
|
|
9517
|
-
if (!
|
|
11641
|
+
const manifestPath = path17.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
11642
|
+
if (!fs16.existsSync(manifestPath)) {
|
|
9518
11643
|
return fail(invocation.format, `Manifest not found at ${manifestPath}. Run generate first.`);
|
|
9519
11644
|
}
|
|
9520
11645
|
let manifest;
|
|
9521
11646
|
try {
|
|
9522
|
-
manifest = JSON.parse(
|
|
11647
|
+
manifest = JSON.parse(fs16.readFileSync(manifestPath, "utf8"));
|
|
9523
11648
|
} catch (err) {
|
|
9524
11649
|
return fail(invocation.format, `Could not read ${manifestPath}: ${err.message}`);
|
|
9525
11650
|
}
|
|
@@ -9683,20 +11808,20 @@ function finishWatchRun(state, now) {
|
|
|
9683
11808
|
};
|
|
9684
11809
|
}
|
|
9685
11810
|
function comparablePath(value) {
|
|
9686
|
-
const absolute =
|
|
11811
|
+
const absolute = path17.resolve(value);
|
|
9687
11812
|
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
9688
11813
|
}
|
|
9689
11814
|
function isSameOrInside(parent, candidate) {
|
|
9690
|
-
const
|
|
9691
|
-
return
|
|
11815
|
+
const relative11 = path17.relative(parent, candidate);
|
|
11816
|
+
return relative11 === "" || relative11 !== ".." && !relative11.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative11);
|
|
9692
11817
|
}
|
|
9693
11818
|
function isGeneratedWatchPath(candidate, paths) {
|
|
9694
|
-
const absolute = comparablePath(
|
|
9695
|
-
const outDir = comparablePath(
|
|
9696
|
-
const tmpDir = comparablePath(
|
|
11819
|
+
const absolute = comparablePath(path17.resolve(paths.cwd, candidate));
|
|
11820
|
+
const outDir = comparablePath(path17.resolve(paths.cwd, paths.outDir));
|
|
11821
|
+
const tmpDir = comparablePath(path17.resolve(paths.cwd, paths.tmpDir));
|
|
9697
11822
|
if (isSameOrInside(outDir, absolute) || isSameOrInside(tmpDir, absolute)) return true;
|
|
9698
|
-
const parent =
|
|
9699
|
-
if (parent ===
|
|
11823
|
+
const parent = path17.dirname(absolute);
|
|
11824
|
+
if (parent === path17.dirname(outDir) && path17.basename(absolute).startsWith(`${path17.basename(outDir)}.backup-`)) {
|
|
9700
11825
|
return true;
|
|
9701
11826
|
}
|
|
9702
11827
|
return absolute.split(/[\\/]+/).some((segment) => VCS_SEGMENTS.has(segment));
|
|
@@ -9713,8 +11838,8 @@ function decideWatchEvent(state, event, paths) {
|
|
|
9713
11838
|
return { state, action: "schedule" };
|
|
9714
11839
|
}
|
|
9715
11840
|
async function runWatchLoop(invocation, resolved) {
|
|
9716
|
-
const outDir =
|
|
9717
|
-
const tmpDir =
|
|
11841
|
+
const outDir = path17.resolve(resolved.cwd, resolved.outDir);
|
|
11842
|
+
const tmpDir = path17.resolve(resolved.cwd, resolved.tmpDir);
|
|
9718
11843
|
const paths = { cwd: resolved.cwd, outDir, tmpDir };
|
|
9719
11844
|
let state = initialWatchState();
|
|
9720
11845
|
let timer = null;
|
|
@@ -9779,11 +11904,11 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
9779
11904
|
};
|
|
9780
11905
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
9781
11906
|
void regenerate();
|
|
9782
|
-
const watcher =
|
|
11907
|
+
const watcher = fs16.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
9783
11908
|
const decision = decideWatchEvent(
|
|
9784
11909
|
state,
|
|
9785
11910
|
{
|
|
9786
|
-
path: filename ?
|
|
11911
|
+
path: filename ? path17.resolve(resolved.cwd, filename.toString()) : null,
|
|
9787
11912
|
now: performance.now()
|
|
9788
11913
|
},
|
|
9789
11914
|
paths
|
|
@@ -9798,12 +11923,12 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
9798
11923
|
}
|
|
9799
11924
|
if (decision.action === "schedule") schedule();
|
|
9800
11925
|
});
|
|
9801
|
-
await new Promise((
|
|
11926
|
+
await new Promise((resolve9) => {
|
|
9802
11927
|
const stop = () => {
|
|
9803
11928
|
stopped = true;
|
|
9804
11929
|
if (timer) clearTimeout(timer);
|
|
9805
11930
|
watcher.close();
|
|
9806
|
-
|
|
11931
|
+
resolve9();
|
|
9807
11932
|
};
|
|
9808
11933
|
process.once("SIGINT", stop);
|
|
9809
11934
|
process.once("SIGTERM", stop);
|
|
@@ -9811,7 +11936,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
9811
11936
|
return 0;
|
|
9812
11937
|
}
|
|
9813
11938
|
var entryFile = process.argv[1] ?? "";
|
|
9814
|
-
var entryName =
|
|
11939
|
+
var entryName = path17.basename(entryFile).toLowerCase();
|
|
9815
11940
|
var invokedDirectly = entryName === "cli.js" || entryName === "cli.cjs";
|
|
9816
11941
|
if (invokedDirectly) {
|
|
9817
11942
|
void main().then(
|