@sniper.ai/cli 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -2
- package/dist/index.js +957 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire as createRequire2 } from "module";
|
|
5
|
-
import { defineCommand as
|
|
5
|
+
import { defineCommand as defineCommand9, runMain } from "citty";
|
|
6
6
|
|
|
7
7
|
// src/commands/init.ts
|
|
8
8
|
import { defineCommand } from "citty";
|
|
@@ -106,9 +106,9 @@ var FRAMEWORK_DIRS = [
|
|
|
106
106
|
async function ensureDir(dir) {
|
|
107
107
|
await mkdir(dir, { recursive: true });
|
|
108
108
|
}
|
|
109
|
-
async function fileExists(
|
|
109
|
+
async function fileExists(p9) {
|
|
110
110
|
try {
|
|
111
|
-
await access2(
|
|
111
|
+
await access2(p9);
|
|
112
112
|
return true;
|
|
113
113
|
} catch {
|
|
114
114
|
return false;
|
|
@@ -117,20 +117,36 @@ async function fileExists(p7) {
|
|
|
117
117
|
async function scaffoldProject(cwd, config, options = {}) {
|
|
118
118
|
const corePath = getCorePath();
|
|
119
119
|
const sniperDir = join2(cwd, ".sniper");
|
|
120
|
-
const
|
|
120
|
+
const log9 = [];
|
|
121
121
|
const isUpdate = options.update === true;
|
|
122
122
|
await ensureDir(sniperDir);
|
|
123
123
|
for (const dir of FRAMEWORK_DIRS) {
|
|
124
124
|
const src = join2(corePath, dir);
|
|
125
125
|
const dest = join2(sniperDir, dir);
|
|
126
126
|
await cp(src, dest, { recursive: true, force: true });
|
|
127
|
-
|
|
127
|
+
log9.push(`Copied ${dir}/`);
|
|
128
128
|
}
|
|
129
129
|
await ensureDir(join2(sniperDir, "domain-packs"));
|
|
130
|
+
const memoryDir = join2(sniperDir, "memory");
|
|
131
|
+
await ensureDir(memoryDir);
|
|
132
|
+
await ensureDir(join2(memoryDir, "retros"));
|
|
133
|
+
const memoryFiles = {
|
|
134
|
+
"conventions.yaml": "conventions: []\n",
|
|
135
|
+
"anti-patterns.yaml": "anti_patterns: []\n",
|
|
136
|
+
"decisions.yaml": "decisions: []\n",
|
|
137
|
+
"estimates.yaml": "calibration:\n velocity_factor: 1.0\n common_underestimates: []\n last_updated: null\n sprints_analyzed: 0\n"
|
|
138
|
+
};
|
|
139
|
+
for (const [filename, content] of Object.entries(memoryFiles)) {
|
|
140
|
+
const filePath = join2(memoryDir, filename);
|
|
141
|
+
if (!isUpdate || !await fileExists(filePath)) {
|
|
142
|
+
await writeFile2(filePath, content, "utf-8");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
log9.push("Created memory/ directory");
|
|
130
146
|
if (!isUpdate) {
|
|
131
147
|
const configContent = YAML2.stringify(config, { lineWidth: 0 });
|
|
132
148
|
await writeFile2(join2(sniperDir, "config.yaml"), configContent, "utf-8");
|
|
133
|
-
|
|
149
|
+
log9.push("Created config.yaml");
|
|
134
150
|
}
|
|
135
151
|
if (!isUpdate || !await fileExists(join2(cwd, "CLAUDE.md"))) {
|
|
136
152
|
const claudeTemplate = await readFile2(
|
|
@@ -138,9 +154,9 @@ async function scaffoldProject(cwd, config, options = {}) {
|
|
|
138
154
|
"utf-8"
|
|
139
155
|
);
|
|
140
156
|
await writeFile2(join2(cwd, "CLAUDE.md"), claudeTemplate, "utf-8");
|
|
141
|
-
|
|
157
|
+
log9.push("Created CLAUDE.md");
|
|
142
158
|
} else {
|
|
143
|
-
|
|
159
|
+
log9.push("Skipped CLAUDE.md (preserved user customizations)");
|
|
144
160
|
}
|
|
145
161
|
const settingsDir = join2(cwd, ".claude");
|
|
146
162
|
await ensureDir(settingsDir);
|
|
@@ -154,14 +170,14 @@ async function scaffoldProject(cwd, config, options = {}) {
|
|
|
154
170
|
settingsTemplate,
|
|
155
171
|
"utf-8"
|
|
156
172
|
);
|
|
157
|
-
|
|
173
|
+
log9.push("Created .claude/settings.json");
|
|
158
174
|
} else {
|
|
159
|
-
|
|
175
|
+
log9.push("Skipped .claude/settings.json (preserved user customizations)");
|
|
160
176
|
}
|
|
161
177
|
const commandsSrc = join2(corePath, "commands");
|
|
162
178
|
const commandsDest = join2(settingsDir, "commands");
|
|
163
179
|
await cp(commandsSrc, commandsDest, { recursive: true, force: true });
|
|
164
|
-
|
|
180
|
+
log9.push("Copied skills to .claude/commands/");
|
|
165
181
|
if (!isUpdate) {
|
|
166
182
|
for (const sub of ["epics", "stories", "reviews"]) {
|
|
167
183
|
const dir = join2(cwd, "docs", sub);
|
|
@@ -175,9 +191,9 @@ async function scaffoldProject(cwd, config, options = {}) {
|
|
|
175
191
|
await writeFile2(join2(dir, ".gitkeep"), "", "utf-8");
|
|
176
192
|
}
|
|
177
193
|
}
|
|
178
|
-
|
|
194
|
+
log9.push("Created docs/ directory");
|
|
179
195
|
}
|
|
180
|
-
return
|
|
196
|
+
return log9;
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
// src/commands/init.ts
|
|
@@ -391,9 +407,9 @@ var initCommand = defineCommand({
|
|
|
391
407
|
const s = p.spinner();
|
|
392
408
|
s.start("Scaffolding SNIPER project...");
|
|
393
409
|
try {
|
|
394
|
-
const
|
|
410
|
+
const log9 = await scaffoldProject(cwd, config);
|
|
395
411
|
s.stop("Done!");
|
|
396
|
-
for (const entry of
|
|
412
|
+
for (const entry of log9) {
|
|
397
413
|
p.log.success(entry);
|
|
398
414
|
}
|
|
399
415
|
p.outro(
|
|
@@ -490,16 +506,16 @@ function assertSafePath(base, untrusted) {
|
|
|
490
506
|
}
|
|
491
507
|
return full;
|
|
492
508
|
}
|
|
493
|
-
async function pathExists(
|
|
509
|
+
async function pathExists(p9) {
|
|
494
510
|
try {
|
|
495
|
-
await access3(
|
|
511
|
+
await access3(p9);
|
|
496
512
|
return true;
|
|
497
513
|
} catch {
|
|
498
514
|
return false;
|
|
499
515
|
}
|
|
500
516
|
}
|
|
501
|
-
async function readJson(
|
|
502
|
-
const raw = await readFile3(
|
|
517
|
+
async function readJson(p9) {
|
|
518
|
+
const raw = await readFile3(p9, "utf-8");
|
|
503
519
|
return JSON.parse(raw);
|
|
504
520
|
}
|
|
505
521
|
function getPackDir(pkgName, cwd) {
|
|
@@ -530,7 +546,7 @@ async function installPack(packageName, cwd) {
|
|
|
530
546
|
}
|
|
531
547
|
const config = await readConfig(cwd);
|
|
532
548
|
if (!config.domain_packs) config.domain_packs = [];
|
|
533
|
-
if (!config.domain_packs.some((
|
|
549
|
+
if (!config.domain_packs.some((p9) => p9.name === shortName)) {
|
|
534
550
|
config.domain_packs.push({ name: shortName, package: packageName });
|
|
535
551
|
}
|
|
536
552
|
await writeConfig(cwd, config);
|
|
@@ -544,7 +560,7 @@ async function installPack(packageName, cwd) {
|
|
|
544
560
|
async function removePack(packName, cwd) {
|
|
545
561
|
const config = await readConfig(cwd);
|
|
546
562
|
const packEntry = (config.domain_packs || []).find(
|
|
547
|
-
(
|
|
563
|
+
(p9) => p9.name === packName
|
|
548
564
|
);
|
|
549
565
|
const packageName = packEntry?.package || `@sniper.ai/pack-${packName}`;
|
|
550
566
|
const domainPacksDir = join3(cwd, ".sniper", "domain-packs");
|
|
@@ -557,7 +573,7 @@ async function removePack(packName, cwd) {
|
|
|
557
573
|
} catch {
|
|
558
574
|
}
|
|
559
575
|
config.domain_packs = (config.domain_packs || []).filter(
|
|
560
|
-
(
|
|
576
|
+
(p9) => p9.name !== packName
|
|
561
577
|
);
|
|
562
578
|
await writeConfig(cwd, config);
|
|
563
579
|
}
|
|
@@ -761,9 +777,9 @@ var updateCommand = defineCommand6({
|
|
|
761
777
|
const s = p6.spinner();
|
|
762
778
|
s.start("Updating framework files...");
|
|
763
779
|
try {
|
|
764
|
-
const
|
|
780
|
+
const log9 = await scaffoldProject(cwd, currentConfig, { update: true });
|
|
765
781
|
s.stop("Done!");
|
|
766
|
-
for (const entry of
|
|
782
|
+
for (const entry of log9) {
|
|
767
783
|
p6.log.success(entry);
|
|
768
784
|
}
|
|
769
785
|
p6.outro("SNIPER updated successfully.");
|
|
@@ -775,10 +791,923 @@ var updateCommand = defineCommand6({
|
|
|
775
791
|
}
|
|
776
792
|
});
|
|
777
793
|
|
|
794
|
+
// src/commands/memory.ts
|
|
795
|
+
import { readFile as readFile4, writeFile as writeFile3, readdir as readdir3 } from "fs/promises";
|
|
796
|
+
import { join as join4 } from "path";
|
|
797
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
798
|
+
import * as p7 from "@clack/prompts";
|
|
799
|
+
import YAML4 from "yaml";
|
|
800
|
+
|
|
801
|
+
// src/fs-utils.ts
|
|
802
|
+
import { mkdir as mkdir3, access as access4 } from "fs/promises";
|
|
803
|
+
async function ensureDir2(dir) {
|
|
804
|
+
await mkdir3(dir, { recursive: true });
|
|
805
|
+
}
|
|
806
|
+
async function pathExists2(path) {
|
|
807
|
+
try {
|
|
808
|
+
await access4(path);
|
|
809
|
+
return true;
|
|
810
|
+
} catch {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/commands/memory.ts
|
|
816
|
+
async function readYamlArray(filePath, key) {
|
|
817
|
+
if (!await pathExists2(filePath)) return [];
|
|
818
|
+
try {
|
|
819
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
820
|
+
const parsed = YAML4.parse(raw);
|
|
821
|
+
return Array.isArray(parsed?.[key]) ? parsed[key] : [];
|
|
822
|
+
} catch {
|
|
823
|
+
return [];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
async function writeYamlArray(filePath, key, entries) {
|
|
827
|
+
const content = YAML4.stringify({ [key]: entries }, { lineWidth: 0 });
|
|
828
|
+
await writeFile3(filePath, content, "utf-8");
|
|
829
|
+
}
|
|
830
|
+
function nextId(entries, prefix) {
|
|
831
|
+
let max = 0;
|
|
832
|
+
for (const entry of entries) {
|
|
833
|
+
const match = entry.id?.match(new RegExp(`^${prefix}-(\\d+)$`));
|
|
834
|
+
if (match) {
|
|
835
|
+
const num = parseInt(match[1], 10);
|
|
836
|
+
if (num > max) max = num;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return `${prefix}-${String(max + 1).padStart(3, "0")}`;
|
|
840
|
+
}
|
|
841
|
+
var memoryCommand = defineCommand7({
|
|
842
|
+
meta: {
|
|
843
|
+
name: "memory",
|
|
844
|
+
description: "Manage agent memory (conventions, anti-patterns, decisions)"
|
|
845
|
+
},
|
|
846
|
+
args: {
|
|
847
|
+
action: {
|
|
848
|
+
type: "positional",
|
|
849
|
+
description: "Action to perform: list, add, remove, promote, export, import",
|
|
850
|
+
required: false
|
|
851
|
+
},
|
|
852
|
+
type: {
|
|
853
|
+
type: "positional",
|
|
854
|
+
description: "Memory type: convention, anti-pattern, decision (for add/remove/promote)",
|
|
855
|
+
required: false
|
|
856
|
+
},
|
|
857
|
+
value: {
|
|
858
|
+
type: "positional",
|
|
859
|
+
description: "Value for the action (rule text, ID, or file path)",
|
|
860
|
+
required: false
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
run: async ({ args }) => {
|
|
864
|
+
const cwd = process.cwd();
|
|
865
|
+
if (!await sniperConfigExists(cwd)) {
|
|
866
|
+
p7.log.error(
|
|
867
|
+
'SNIPER is not initialized in this directory. Run "sniper init" first.'
|
|
868
|
+
);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
const memoryDir = join4(cwd, ".sniper", "memory");
|
|
872
|
+
if (!await pathExists2(memoryDir)) {
|
|
873
|
+
await ensureDir2(memoryDir);
|
|
874
|
+
await ensureDir2(join4(memoryDir, "retros"));
|
|
875
|
+
await writeFile3(
|
|
876
|
+
join4(memoryDir, "conventions.yaml"),
|
|
877
|
+
"conventions: []\n",
|
|
878
|
+
"utf-8"
|
|
879
|
+
);
|
|
880
|
+
await writeFile3(
|
|
881
|
+
join4(memoryDir, "anti-patterns.yaml"),
|
|
882
|
+
"anti_patterns: []\n",
|
|
883
|
+
"utf-8"
|
|
884
|
+
);
|
|
885
|
+
await writeFile3(
|
|
886
|
+
join4(memoryDir, "decisions.yaml"),
|
|
887
|
+
"decisions: []\n",
|
|
888
|
+
"utf-8"
|
|
889
|
+
);
|
|
890
|
+
await writeFile3(
|
|
891
|
+
join4(memoryDir, "estimates.yaml"),
|
|
892
|
+
"calibration:\n velocity_factor: 1.0\n common_underestimates: []\n last_updated: null\n sprints_analyzed: 0\n",
|
|
893
|
+
"utf-8"
|
|
894
|
+
);
|
|
895
|
+
p7.log.info("Initialized .sniper/memory/ directory");
|
|
896
|
+
}
|
|
897
|
+
const conventions = await readYamlArray(
|
|
898
|
+
join4(memoryDir, "conventions.yaml"),
|
|
899
|
+
"conventions"
|
|
900
|
+
);
|
|
901
|
+
const antiPatterns = await readYamlArray(
|
|
902
|
+
join4(memoryDir, "anti-patterns.yaml"),
|
|
903
|
+
"anti_patterns"
|
|
904
|
+
);
|
|
905
|
+
const decisions = await readYamlArray(
|
|
906
|
+
join4(memoryDir, "decisions.yaml"),
|
|
907
|
+
"decisions"
|
|
908
|
+
);
|
|
909
|
+
let retroCount = 0;
|
|
910
|
+
const retrosDir = join4(memoryDir, "retros");
|
|
911
|
+
if (await pathExists2(retrosDir)) {
|
|
912
|
+
const files = await readdir3(retrosDir);
|
|
913
|
+
retroCount = files.filter((f) => f.endsWith(".yaml")).length;
|
|
914
|
+
}
|
|
915
|
+
const action = args.action;
|
|
916
|
+
if (!action || action === "list") {
|
|
917
|
+
p7.intro("SNIPER Memory");
|
|
918
|
+
const confirmedConv = conventions.filter(
|
|
919
|
+
(c) => c.status !== "candidate"
|
|
920
|
+
).length;
|
|
921
|
+
const candidateConv = conventions.filter(
|
|
922
|
+
(c) => c.status === "candidate"
|
|
923
|
+
).length;
|
|
924
|
+
const confirmedAp = antiPatterns.filter(
|
|
925
|
+
(a) => a.status !== "candidate"
|
|
926
|
+
).length;
|
|
927
|
+
const candidateAp = antiPatterns.filter(
|
|
928
|
+
(a) => a.status === "candidate"
|
|
929
|
+
).length;
|
|
930
|
+
const activeDecisions = decisions.filter(
|
|
931
|
+
(d) => d.status === "active" || !d.status
|
|
932
|
+
).length;
|
|
933
|
+
const supersededDecisions = decisions.filter(
|
|
934
|
+
(d) => d.status === "superseded"
|
|
935
|
+
).length;
|
|
936
|
+
p7.log.info(
|
|
937
|
+
`Conventions: ${confirmedConv} confirmed, ${candidateConv} candidates`
|
|
938
|
+
);
|
|
939
|
+
p7.log.info(
|
|
940
|
+
`Anti-Patterns: ${confirmedAp} confirmed, ${candidateAp} candidates`
|
|
941
|
+
);
|
|
942
|
+
p7.log.info(
|
|
943
|
+
`Decisions: ${activeDecisions} active, ${supersededDecisions} superseded`
|
|
944
|
+
);
|
|
945
|
+
p7.log.info(`Retrospectives: ${retroCount}`);
|
|
946
|
+
const config = await readConfig(cwd);
|
|
947
|
+
if (config.workspace?.enabled && config.workspace.workspace_path) {
|
|
948
|
+
const wsMemory = join4(
|
|
949
|
+
cwd,
|
|
950
|
+
config.workspace.workspace_path,
|
|
951
|
+
"memory"
|
|
952
|
+
);
|
|
953
|
+
if (await pathExists2(wsMemory)) {
|
|
954
|
+
const wsConv = await readYamlArray(
|
|
955
|
+
join4(wsMemory, "conventions.yaml"),
|
|
956
|
+
"conventions"
|
|
957
|
+
);
|
|
958
|
+
const wsAp = await readYamlArray(
|
|
959
|
+
join4(wsMemory, "anti-patterns.yaml"),
|
|
960
|
+
"anti_patterns"
|
|
961
|
+
);
|
|
962
|
+
const wsDec = await readYamlArray(
|
|
963
|
+
join4(wsMemory, "decisions.yaml"),
|
|
964
|
+
"decisions"
|
|
965
|
+
);
|
|
966
|
+
p7.log.step("Workspace Memory:");
|
|
967
|
+
p7.log.info(` Conventions: ${wsConv.length}`);
|
|
968
|
+
p7.log.info(` Anti-Patterns: ${wsAp.length}`);
|
|
969
|
+
p7.log.info(` Decisions: ${wsDec.length}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
p7.outro("");
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (action === "add") {
|
|
976
|
+
const type = args.type;
|
|
977
|
+
const value = args.value;
|
|
978
|
+
if (!type || !value) {
|
|
979
|
+
p7.log.error(
|
|
980
|
+
'Usage: sniper memory add <convention|anti-pattern|decision> "<text>"'
|
|
981
|
+
);
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
985
|
+
if (type === "convention") {
|
|
986
|
+
const id = nextId(conventions, "conv");
|
|
987
|
+
conventions.push({
|
|
988
|
+
id,
|
|
989
|
+
rule: value,
|
|
990
|
+
rationale: "",
|
|
991
|
+
source: { type: "manual", ref: "user-added", date: today },
|
|
992
|
+
applies_to: [],
|
|
993
|
+
enforcement: "both",
|
|
994
|
+
scope: "project",
|
|
995
|
+
status: "confirmed",
|
|
996
|
+
examples: { positive: "", negative: "" }
|
|
997
|
+
});
|
|
998
|
+
await writeYamlArray(
|
|
999
|
+
join4(memoryDir, "conventions.yaml"),
|
|
1000
|
+
"conventions",
|
|
1001
|
+
conventions
|
|
1002
|
+
);
|
|
1003
|
+
p7.log.success(`Added convention ${id}: ${value}`);
|
|
1004
|
+
} else if (type === "anti-pattern") {
|
|
1005
|
+
const id = nextId(antiPatterns, "ap");
|
|
1006
|
+
antiPatterns.push({
|
|
1007
|
+
id,
|
|
1008
|
+
description: value,
|
|
1009
|
+
why_bad: "",
|
|
1010
|
+
fix_pattern: "",
|
|
1011
|
+
source: { type: "manual", ref: "user-added", date: today },
|
|
1012
|
+
detection_hint: "",
|
|
1013
|
+
applies_to: [],
|
|
1014
|
+
severity: "medium",
|
|
1015
|
+
status: "confirmed"
|
|
1016
|
+
});
|
|
1017
|
+
await writeYamlArray(
|
|
1018
|
+
join4(memoryDir, "anti-patterns.yaml"),
|
|
1019
|
+
"anti_patterns",
|
|
1020
|
+
antiPatterns
|
|
1021
|
+
);
|
|
1022
|
+
p7.log.success(`Added anti-pattern ${id}: ${value}`);
|
|
1023
|
+
} else if (type === "decision") {
|
|
1024
|
+
const id = nextId(decisions, "dec");
|
|
1025
|
+
decisions.push({
|
|
1026
|
+
id,
|
|
1027
|
+
title: value,
|
|
1028
|
+
context: "",
|
|
1029
|
+
decision: value,
|
|
1030
|
+
alternatives_considered: [],
|
|
1031
|
+
source: { type: "manual", ref: "user-added", date: today },
|
|
1032
|
+
applies_to: [],
|
|
1033
|
+
status: "active",
|
|
1034
|
+
superseded_by: null
|
|
1035
|
+
});
|
|
1036
|
+
await writeYamlArray(
|
|
1037
|
+
join4(memoryDir, "decisions.yaml"),
|
|
1038
|
+
"decisions",
|
|
1039
|
+
decisions
|
|
1040
|
+
);
|
|
1041
|
+
p7.log.success(`Added decision ${id}: ${value}`);
|
|
1042
|
+
} else {
|
|
1043
|
+
p7.log.error(
|
|
1044
|
+
`Unknown memory type "${type}". Use: convention, anti-pattern, decision`
|
|
1045
|
+
);
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (action === "remove") {
|
|
1051
|
+
const id = args.type;
|
|
1052
|
+
if (!id) {
|
|
1053
|
+
p7.log.error("Usage: sniper memory remove <id>");
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
let found = false;
|
|
1057
|
+
if (id.startsWith("conv-")) {
|
|
1058
|
+
const idx = conventions.findIndex((c) => c.id === id);
|
|
1059
|
+
if (idx >= 0) {
|
|
1060
|
+
conventions.splice(idx, 1);
|
|
1061
|
+
await writeYamlArray(
|
|
1062
|
+
join4(memoryDir, "conventions.yaml"),
|
|
1063
|
+
"conventions",
|
|
1064
|
+
conventions
|
|
1065
|
+
);
|
|
1066
|
+
found = true;
|
|
1067
|
+
}
|
|
1068
|
+
} else if (id.startsWith("ap-")) {
|
|
1069
|
+
const idx = antiPatterns.findIndex((a) => a.id === id);
|
|
1070
|
+
if (idx >= 0) {
|
|
1071
|
+
antiPatterns.splice(idx, 1);
|
|
1072
|
+
await writeYamlArray(
|
|
1073
|
+
join4(memoryDir, "anti-patterns.yaml"),
|
|
1074
|
+
"anti_patterns",
|
|
1075
|
+
antiPatterns
|
|
1076
|
+
);
|
|
1077
|
+
found = true;
|
|
1078
|
+
}
|
|
1079
|
+
} else if (id.startsWith("dec-")) {
|
|
1080
|
+
const idx = decisions.findIndex((d) => d.id === id);
|
|
1081
|
+
if (idx >= 0) {
|
|
1082
|
+
decisions.splice(idx, 1);
|
|
1083
|
+
await writeYamlArray(
|
|
1084
|
+
join4(memoryDir, "decisions.yaml"),
|
|
1085
|
+
"decisions",
|
|
1086
|
+
decisions
|
|
1087
|
+
);
|
|
1088
|
+
found = true;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (found) {
|
|
1092
|
+
p7.log.success(`Removed ${id}`);
|
|
1093
|
+
} else {
|
|
1094
|
+
p7.log.error(`Entry ${id} not found in memory.`);
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (action === "promote") {
|
|
1100
|
+
const id = args.type;
|
|
1101
|
+
if (!id) {
|
|
1102
|
+
p7.log.error("Usage: sniper memory promote <id>");
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
let found = false;
|
|
1106
|
+
if (id.startsWith("conv-")) {
|
|
1107
|
+
const entry = conventions.find((c) => c.id === id);
|
|
1108
|
+
if (entry && entry.status === "candidate") {
|
|
1109
|
+
entry.status = "confirmed";
|
|
1110
|
+
await writeYamlArray(
|
|
1111
|
+
join4(memoryDir, "conventions.yaml"),
|
|
1112
|
+
"conventions",
|
|
1113
|
+
conventions
|
|
1114
|
+
);
|
|
1115
|
+
found = true;
|
|
1116
|
+
}
|
|
1117
|
+
} else if (id.startsWith("ap-")) {
|
|
1118
|
+
const entry = antiPatterns.find((a) => a.id === id);
|
|
1119
|
+
if (entry && entry.status === "candidate") {
|
|
1120
|
+
entry.status = "confirmed";
|
|
1121
|
+
await writeYamlArray(
|
|
1122
|
+
join4(memoryDir, "anti-patterns.yaml"),
|
|
1123
|
+
"anti_patterns",
|
|
1124
|
+
antiPatterns
|
|
1125
|
+
);
|
|
1126
|
+
found = true;
|
|
1127
|
+
}
|
|
1128
|
+
} else if (id.startsWith("dec-")) {
|
|
1129
|
+
const entry = decisions.find((d) => d.id === id);
|
|
1130
|
+
if (entry && entry.status === "candidate") {
|
|
1131
|
+
entry.status = "active";
|
|
1132
|
+
await writeYamlArray(
|
|
1133
|
+
join4(memoryDir, "decisions.yaml"),
|
|
1134
|
+
"decisions",
|
|
1135
|
+
decisions
|
|
1136
|
+
);
|
|
1137
|
+
found = true;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (found) {
|
|
1141
|
+
p7.log.success(`Promoted ${id} to confirmed/active`);
|
|
1142
|
+
} else {
|
|
1143
|
+
p7.log.error(
|
|
1144
|
+
`Entry ${id} not found or is not a candidate.`
|
|
1145
|
+
);
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (action === "export") {
|
|
1151
|
+
const exportData = {
|
|
1152
|
+
exported_from: (await readConfig(cwd)).project.name,
|
|
1153
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1154
|
+
version: "1.0",
|
|
1155
|
+
conventions: conventions.map(({ id: _id, source: _src, ...rest }) => rest),
|
|
1156
|
+
anti_patterns: antiPatterns.map(
|
|
1157
|
+
({ id: _id, source: _src, ...rest }) => rest
|
|
1158
|
+
),
|
|
1159
|
+
decisions: decisions.map(({ id: _id, source: _src, ...rest }) => rest)
|
|
1160
|
+
};
|
|
1161
|
+
const exportPath = join4(cwd, "sniper-memory-export.yaml");
|
|
1162
|
+
await writeFile3(
|
|
1163
|
+
exportPath,
|
|
1164
|
+
YAML4.stringify(exportData, { lineWidth: 0 }),
|
|
1165
|
+
"utf-8"
|
|
1166
|
+
);
|
|
1167
|
+
p7.log.success(
|
|
1168
|
+
`Exported ${conventions.length} conventions, ${antiPatterns.length} anti-patterns, ${decisions.length} decisions to sniper-memory-export.yaml`
|
|
1169
|
+
);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (action === "import") {
|
|
1173
|
+
const filePath = args.type;
|
|
1174
|
+
if (!filePath) {
|
|
1175
|
+
p7.log.error("Usage: sniper memory import <file>");
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
const raw = await readFile4(join4(cwd, filePath), "utf-8");
|
|
1179
|
+
const imported = YAML4.parse(raw);
|
|
1180
|
+
let addedConv = 0;
|
|
1181
|
+
let addedAp = 0;
|
|
1182
|
+
let skipped = 0;
|
|
1183
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1184
|
+
if (Array.isArray(imported.conventions)) {
|
|
1185
|
+
for (const conv of imported.conventions) {
|
|
1186
|
+
const exists = conventions.some(
|
|
1187
|
+
(c) => c.rule === conv.rule
|
|
1188
|
+
);
|
|
1189
|
+
if (exists) {
|
|
1190
|
+
skipped++;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
conventions.push({
|
|
1194
|
+
...conv,
|
|
1195
|
+
id: nextId(conventions, "conv"),
|
|
1196
|
+
source: { type: "imported", ref: filePath, date: today },
|
|
1197
|
+
status: "candidate"
|
|
1198
|
+
});
|
|
1199
|
+
addedConv++;
|
|
1200
|
+
}
|
|
1201
|
+
await writeYamlArray(
|
|
1202
|
+
join4(memoryDir, "conventions.yaml"),
|
|
1203
|
+
"conventions",
|
|
1204
|
+
conventions
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
if (Array.isArray(imported.anti_patterns)) {
|
|
1208
|
+
for (const ap of imported.anti_patterns) {
|
|
1209
|
+
const exists = antiPatterns.some(
|
|
1210
|
+
(a) => a.description === ap.description
|
|
1211
|
+
);
|
|
1212
|
+
if (exists) {
|
|
1213
|
+
skipped++;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
antiPatterns.push({
|
|
1217
|
+
...ap,
|
|
1218
|
+
id: nextId(antiPatterns, "ap"),
|
|
1219
|
+
source: { type: "imported", ref: filePath, date: today },
|
|
1220
|
+
status: "candidate"
|
|
1221
|
+
});
|
|
1222
|
+
addedAp++;
|
|
1223
|
+
}
|
|
1224
|
+
await writeYamlArray(
|
|
1225
|
+
join4(memoryDir, "anti-patterns.yaml"),
|
|
1226
|
+
"anti_patterns",
|
|
1227
|
+
antiPatterns
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
p7.log.success(
|
|
1231
|
+
`Imported ${addedConv} conventions, ${addedAp} anti-patterns (${skipped} skipped as duplicates)`
|
|
1232
|
+
);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
p7.log.error(
|
|
1236
|
+
`Unknown action "${action}". Use: list, add, remove, promote, export, import`
|
|
1237
|
+
);
|
|
1238
|
+
process.exit(1);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
// src/commands/workspace.ts
|
|
1243
|
+
import {
|
|
1244
|
+
readFile as readFile5,
|
|
1245
|
+
writeFile as writeFile4,
|
|
1246
|
+
readdir as readdir4,
|
|
1247
|
+
stat as stat2,
|
|
1248
|
+
symlink
|
|
1249
|
+
} from "fs/promises";
|
|
1250
|
+
import { join as join5, relative, resolve as resolve2 } from "path";
|
|
1251
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
1252
|
+
import * as p8 from "@clack/prompts";
|
|
1253
|
+
import YAML5 from "yaml";
|
|
1254
|
+
var initSubCommand = defineCommand8({
|
|
1255
|
+
meta: {
|
|
1256
|
+
name: "init",
|
|
1257
|
+
description: "Initialize a SNIPER workspace"
|
|
1258
|
+
},
|
|
1259
|
+
run: async () => {
|
|
1260
|
+
const cwd = process.cwd();
|
|
1261
|
+
if (await pathExists2(join5(cwd, "workspace.yaml"))) {
|
|
1262
|
+
const raw = await readFile5(join5(cwd, "workspace.yaml"), "utf-8");
|
|
1263
|
+
const ws = YAML5.parse(raw);
|
|
1264
|
+
p8.log.warn(
|
|
1265
|
+
`A workspace already exists: ${ws.name} (${ws.repositories.length} repos)`
|
|
1266
|
+
);
|
|
1267
|
+
p8.log.info("Use /sniper-workspace status to view details.");
|
|
1268
|
+
process.exit(0);
|
|
1269
|
+
}
|
|
1270
|
+
p8.intro("Initialize SNIPER Workspace");
|
|
1271
|
+
const name = await p8.text({
|
|
1272
|
+
message: "Workspace name:",
|
|
1273
|
+
placeholder: "my-saas-platform"
|
|
1274
|
+
});
|
|
1275
|
+
if (p8.isCancel(name)) {
|
|
1276
|
+
p8.cancel("Aborted.");
|
|
1277
|
+
process.exit(0);
|
|
1278
|
+
}
|
|
1279
|
+
const description = await p8.text({
|
|
1280
|
+
message: "Description:",
|
|
1281
|
+
placeholder: "Multi-service SaaS platform"
|
|
1282
|
+
});
|
|
1283
|
+
if (p8.isCancel(description)) {
|
|
1284
|
+
p8.cancel("Aborted.");
|
|
1285
|
+
process.exit(0);
|
|
1286
|
+
}
|
|
1287
|
+
const s = p8.spinner();
|
|
1288
|
+
s.start("Scanning for SNIPER-enabled repositories...");
|
|
1289
|
+
const parentDir = resolve2(cwd, "..");
|
|
1290
|
+
const repos = [];
|
|
1291
|
+
try {
|
|
1292
|
+
const siblings = await readdir4(parentDir);
|
|
1293
|
+
for (const entry of siblings) {
|
|
1294
|
+
const entryPath = join5(parentDir, entry);
|
|
1295
|
+
const entryStat = await stat2(entryPath);
|
|
1296
|
+
if (!entryStat.isDirectory()) continue;
|
|
1297
|
+
if (resolve2(entryPath) === resolve2(cwd)) continue;
|
|
1298
|
+
const configPath = join5(entryPath, ".sniper", "config.yaml");
|
|
1299
|
+
if (await pathExists2(configPath)) {
|
|
1300
|
+
try {
|
|
1301
|
+
const raw = await readFile5(configPath, "utf-8");
|
|
1302
|
+
const config = YAML5.parse(raw);
|
|
1303
|
+
repos.push({
|
|
1304
|
+
name: config.project?.name || entry,
|
|
1305
|
+
path: relative(cwd, entryPath),
|
|
1306
|
+
role: inferRole(config.project?.type),
|
|
1307
|
+
language: config.stack?.language || "unknown",
|
|
1308
|
+
sniper_enabled: true,
|
|
1309
|
+
exposes: [],
|
|
1310
|
+
consumes: []
|
|
1311
|
+
});
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
s.stop(`Found ${repos.length} SNIPER-enabled repositories`);
|
|
1319
|
+
if (repos.length === 0) {
|
|
1320
|
+
p8.log.warn(
|
|
1321
|
+
"No SNIPER-enabled repositories found in sibling directories."
|
|
1322
|
+
);
|
|
1323
|
+
p8.log.info(
|
|
1324
|
+
'Initialize SNIPER in your repos first with "sniper init", or add repos manually later.'
|
|
1325
|
+
);
|
|
1326
|
+
} else {
|
|
1327
|
+
for (const repo of repos) {
|
|
1328
|
+
p8.log.info(` ${repo.name} (${repo.role}, ${repo.language}) ${repo.path}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const depGraph = {};
|
|
1332
|
+
for (const repo of repos) {
|
|
1333
|
+
depGraph[repo.name] = [];
|
|
1334
|
+
}
|
|
1335
|
+
const workspace = {
|
|
1336
|
+
name,
|
|
1337
|
+
description,
|
|
1338
|
+
version: "1.0",
|
|
1339
|
+
repositories: repos,
|
|
1340
|
+
dependency_graph: depGraph,
|
|
1341
|
+
config: {
|
|
1342
|
+
contract_format: "yaml",
|
|
1343
|
+
integration_validation: true,
|
|
1344
|
+
shared_domain_packs: [],
|
|
1345
|
+
memory: {
|
|
1346
|
+
workspace_conventions: true,
|
|
1347
|
+
auto_promote: false
|
|
1348
|
+
}
|
|
1349
|
+
},
|
|
1350
|
+
state: {
|
|
1351
|
+
feature_counter: 1,
|
|
1352
|
+
features: []
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
await writeFile4(
|
|
1356
|
+
join5(cwd, "workspace.yaml"),
|
|
1357
|
+
YAML5.stringify(workspace, { lineWidth: 0 }),
|
|
1358
|
+
"utf-8"
|
|
1359
|
+
);
|
|
1360
|
+
await ensureDir2(join5(cwd, "memory"));
|
|
1361
|
+
await writeFile4(
|
|
1362
|
+
join5(cwd, "memory", "conventions.yaml"),
|
|
1363
|
+
"conventions: []\n",
|
|
1364
|
+
"utf-8"
|
|
1365
|
+
);
|
|
1366
|
+
await writeFile4(
|
|
1367
|
+
join5(cwd, "memory", "anti-patterns.yaml"),
|
|
1368
|
+
"anti_patterns: []\n",
|
|
1369
|
+
"utf-8"
|
|
1370
|
+
);
|
|
1371
|
+
await writeFile4(
|
|
1372
|
+
join5(cwd, "memory", "decisions.yaml"),
|
|
1373
|
+
"decisions: []\n",
|
|
1374
|
+
"utf-8"
|
|
1375
|
+
);
|
|
1376
|
+
await ensureDir2(join5(cwd, "contracts"));
|
|
1377
|
+
await writeFile4(join5(cwd, "contracts", ".gitkeep"), "", "utf-8");
|
|
1378
|
+
await ensureDir2(join5(cwd, "features"));
|
|
1379
|
+
await writeFile4(join5(cwd, "features", ".gitkeep"), "", "utf-8");
|
|
1380
|
+
if (repos.length > 0) {
|
|
1381
|
+
await ensureDir2(join5(cwd, "repositories"));
|
|
1382
|
+
for (const repo of repos) {
|
|
1383
|
+
const linkPath = join5(cwd, "repositories", repo.name);
|
|
1384
|
+
const targetPath = resolve2(cwd, repo.path);
|
|
1385
|
+
if (!await pathExists2(linkPath)) {
|
|
1386
|
+
try {
|
|
1387
|
+
await symlink(targetPath, linkPath);
|
|
1388
|
+
} catch {
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
for (const repo of repos) {
|
|
1394
|
+
const repoDir = resolve2(cwd, repo.path);
|
|
1395
|
+
try {
|
|
1396
|
+
const config = await readConfig(repoDir);
|
|
1397
|
+
config.workspace = {
|
|
1398
|
+
enabled: true,
|
|
1399
|
+
workspace_path: relative(repoDir, cwd),
|
|
1400
|
+
repo_name: repo.name
|
|
1401
|
+
};
|
|
1402
|
+
await writeConfig(repoDir, config);
|
|
1403
|
+
} catch {
|
|
1404
|
+
p8.log.warn(`Could not update config for ${repo.name}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
p8.log.success("Workspace initialized!");
|
|
1408
|
+
p8.log.info(` Location: ${cwd}`);
|
|
1409
|
+
p8.log.info(` Repos: ${repos.length}`);
|
|
1410
|
+
p8.log.info("");
|
|
1411
|
+
p8.log.info("Next steps:");
|
|
1412
|
+
p8.log.info(
|
|
1413
|
+
' /sniper-workspace feature "description" \u2014 Plan a cross-repo feature'
|
|
1414
|
+
);
|
|
1415
|
+
p8.log.info(
|
|
1416
|
+
" /sniper-workspace status \u2014 View workspace status"
|
|
1417
|
+
);
|
|
1418
|
+
p8.outro("");
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
var statusSubCommand = defineCommand8({
|
|
1422
|
+
meta: {
|
|
1423
|
+
name: "status",
|
|
1424
|
+
description: "Show workspace status"
|
|
1425
|
+
},
|
|
1426
|
+
run: async () => {
|
|
1427
|
+
const cwd = process.cwd();
|
|
1428
|
+
const wsPath = join5(cwd, "workspace.yaml");
|
|
1429
|
+
if (!await pathExists2(wsPath)) {
|
|
1430
|
+
p8.log.error(
|
|
1431
|
+
"No workspace found. Run /sniper-workspace init to create one."
|
|
1432
|
+
);
|
|
1433
|
+
process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
const raw = await readFile5(wsPath, "utf-8");
|
|
1436
|
+
const ws = YAML5.parse(raw);
|
|
1437
|
+
p8.intro(`Workspace: ${ws.name}`);
|
|
1438
|
+
p8.log.info(ws.description);
|
|
1439
|
+
p8.log.step("Repositories:");
|
|
1440
|
+
for (const repo of ws.repositories) {
|
|
1441
|
+
const repoPath = resolve2(cwd, repo.path);
|
|
1442
|
+
const accessible = await pathExists2(repoPath);
|
|
1443
|
+
const icon = accessible ? "\u2713" : "\u2717";
|
|
1444
|
+
p8.log.info(
|
|
1445
|
+
` ${icon} ${repo.name.padEnd(20)} ${repo.role.padEnd(12)} ${repo.language}`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const activeFeatures = ws.state.features.filter(
|
|
1449
|
+
(f) => f.phase !== "complete"
|
|
1450
|
+
);
|
|
1451
|
+
if (activeFeatures.length > 0) {
|
|
1452
|
+
p8.log.step("Active Features:");
|
|
1453
|
+
for (const f of activeFeatures) {
|
|
1454
|
+
p8.log.info(
|
|
1455
|
+
` ${f.id} "${f.title}" Phase: ${f.phase}${f.sprint_wave ? ` Wave: ${f.sprint_wave}` : ""}`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
p8.log.step("No active workspace features.");
|
|
1460
|
+
}
|
|
1461
|
+
const contractsDir = join5(cwd, "contracts");
|
|
1462
|
+
if (await pathExists2(contractsDir)) {
|
|
1463
|
+
const files = (await readdir4(contractsDir)).filter(
|
|
1464
|
+
(f) => f.endsWith(".contract.yaml")
|
|
1465
|
+
);
|
|
1466
|
+
if (files.length > 0) {
|
|
1467
|
+
p8.log.step("Contracts:");
|
|
1468
|
+
for (const file of files) {
|
|
1469
|
+
try {
|
|
1470
|
+
const cRaw = await readFile5(join5(contractsDir, file), "utf-8");
|
|
1471
|
+
const contract = YAML5.parse(cRaw);
|
|
1472
|
+
const name = contract.contract?.name || file;
|
|
1473
|
+
const version2 = contract.contract?.version || "?";
|
|
1474
|
+
const between = contract.contract?.between?.join(" \u2194 ") || "?";
|
|
1475
|
+
p8.log.info(` ${name} v${version2} ${between}`);
|
|
1476
|
+
} catch {
|
|
1477
|
+
p8.log.info(` ${file} (parse error)`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
} else {
|
|
1481
|
+
p8.log.step("No contracts defined.");
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const memDir = join5(cwd, "memory");
|
|
1485
|
+
if (await pathExists2(memDir)) {
|
|
1486
|
+
const convFile = join5(memDir, "conventions.yaml");
|
|
1487
|
+
const apFile = join5(memDir, "anti-patterns.yaml");
|
|
1488
|
+
const decFile = join5(memDir, "decisions.yaml");
|
|
1489
|
+
let convCount = 0;
|
|
1490
|
+
let apCount = 0;
|
|
1491
|
+
let decCount = 0;
|
|
1492
|
+
if (await pathExists2(convFile)) {
|
|
1493
|
+
try {
|
|
1494
|
+
const parsed = YAML5.parse(await readFile5(convFile, "utf-8"));
|
|
1495
|
+
convCount = Array.isArray(parsed?.conventions) ? parsed.conventions.length : 0;
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (await pathExists2(apFile)) {
|
|
1500
|
+
try {
|
|
1501
|
+
const parsed = YAML5.parse(await readFile5(apFile, "utf-8"));
|
|
1502
|
+
apCount = Array.isArray(parsed?.anti_patterns) ? parsed.anti_patterns.length : 0;
|
|
1503
|
+
} catch {
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
if (await pathExists2(decFile)) {
|
|
1507
|
+
try {
|
|
1508
|
+
const parsed = YAML5.parse(await readFile5(decFile, "utf-8"));
|
|
1509
|
+
decCount = Array.isArray(parsed?.decisions) ? parsed.decisions.length : 0;
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
p8.log.step("Workspace Memory:");
|
|
1514
|
+
p8.log.info(` Conventions: ${convCount}`);
|
|
1515
|
+
p8.log.info(` Anti-Patterns: ${apCount}`);
|
|
1516
|
+
p8.log.info(` Decisions: ${decCount}`);
|
|
1517
|
+
}
|
|
1518
|
+
p8.outro("");
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
var addRepoSubCommand = defineCommand8({
|
|
1522
|
+
meta: {
|
|
1523
|
+
name: "add-repo",
|
|
1524
|
+
description: "Add a repository to the workspace"
|
|
1525
|
+
},
|
|
1526
|
+
args: {
|
|
1527
|
+
path: {
|
|
1528
|
+
type: "positional",
|
|
1529
|
+
description: "Path to the repository",
|
|
1530
|
+
required: true
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
run: async ({ args }) => {
|
|
1534
|
+
const cwd = process.cwd();
|
|
1535
|
+
const wsPath = join5(cwd, "workspace.yaml");
|
|
1536
|
+
if (!await pathExists2(wsPath)) {
|
|
1537
|
+
p8.log.error("No workspace found. Run /sniper-workspace init first.");
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
const repoPath = resolve2(cwd, args.path);
|
|
1541
|
+
if (!await sniperConfigExists(repoPath)) {
|
|
1542
|
+
p8.log.error(
|
|
1543
|
+
`${repoPath} is not a SNIPER-enabled project. Run "sniper init" in that directory first.`
|
|
1544
|
+
);
|
|
1545
|
+
process.exit(1);
|
|
1546
|
+
}
|
|
1547
|
+
const repoConfig = await readConfig(repoPath);
|
|
1548
|
+
const raw = await readFile5(wsPath, "utf-8");
|
|
1549
|
+
const ws = YAML5.parse(raw);
|
|
1550
|
+
const repoName = repoConfig.project.name;
|
|
1551
|
+
if (ws.repositories.some((r) => r.name === repoName)) {
|
|
1552
|
+
p8.log.warn(`Repository "${repoName}" is already in the workspace.`);
|
|
1553
|
+
process.exit(0);
|
|
1554
|
+
}
|
|
1555
|
+
ws.repositories.push({
|
|
1556
|
+
name: repoName,
|
|
1557
|
+
path: relative(cwd, repoPath),
|
|
1558
|
+
role: inferRole(repoConfig.project.type),
|
|
1559
|
+
language: repoConfig.stack.language,
|
|
1560
|
+
sniper_enabled: true,
|
|
1561
|
+
exposes: [],
|
|
1562
|
+
consumes: []
|
|
1563
|
+
});
|
|
1564
|
+
ws.dependency_graph[repoName] = [];
|
|
1565
|
+
await writeFile4(wsPath, YAML5.stringify(ws, { lineWidth: 0 }), "utf-8");
|
|
1566
|
+
repoConfig.workspace = {
|
|
1567
|
+
enabled: true,
|
|
1568
|
+
workspace_path: relative(repoPath, cwd),
|
|
1569
|
+
repo_name: repoName
|
|
1570
|
+
};
|
|
1571
|
+
await writeConfig(repoPath, repoConfig);
|
|
1572
|
+
p8.log.success(
|
|
1573
|
+
`Added ${repoName} (${repoConfig.project.type}, ${repoConfig.stack.language})`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
var removeRepoSubCommand = defineCommand8({
|
|
1578
|
+
meta: {
|
|
1579
|
+
name: "remove-repo",
|
|
1580
|
+
description: "Remove a repository from the workspace"
|
|
1581
|
+
},
|
|
1582
|
+
args: {
|
|
1583
|
+
name: {
|
|
1584
|
+
type: "positional",
|
|
1585
|
+
description: "Repository name",
|
|
1586
|
+
required: true
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
run: async ({ args }) => {
|
|
1590
|
+
const cwd = process.cwd();
|
|
1591
|
+
const wsPath = join5(cwd, "workspace.yaml");
|
|
1592
|
+
if (!await pathExists2(wsPath)) {
|
|
1593
|
+
p8.log.error("No workspace found.");
|
|
1594
|
+
process.exit(1);
|
|
1595
|
+
}
|
|
1596
|
+
const raw = await readFile5(wsPath, "utf-8");
|
|
1597
|
+
const ws = YAML5.parse(raw);
|
|
1598
|
+
const repoName = args.name;
|
|
1599
|
+
const idx = ws.repositories.findIndex((r) => r.name === repoName);
|
|
1600
|
+
if (idx < 0) {
|
|
1601
|
+
p8.log.error(`Repository "${repoName}" not found in workspace.`);
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
}
|
|
1604
|
+
const repo = ws.repositories[idx];
|
|
1605
|
+
ws.repositories.splice(idx, 1);
|
|
1606
|
+
delete ws.dependency_graph[repoName];
|
|
1607
|
+
for (const deps of Object.values(ws.dependency_graph)) {
|
|
1608
|
+
const depIdx = deps.indexOf(repoName);
|
|
1609
|
+
if (depIdx >= 0) deps.splice(depIdx, 1);
|
|
1610
|
+
}
|
|
1611
|
+
await writeFile4(wsPath, YAML5.stringify(ws, { lineWidth: 0 }), "utf-8");
|
|
1612
|
+
const repoPath = resolve2(cwd, repo.path);
|
|
1613
|
+
try {
|
|
1614
|
+
const repoConfig = await readConfig(repoPath);
|
|
1615
|
+
repoConfig.workspace = {
|
|
1616
|
+
enabled: false,
|
|
1617
|
+
workspace_path: null,
|
|
1618
|
+
repo_name: null
|
|
1619
|
+
};
|
|
1620
|
+
await writeConfig(repoPath, repoConfig);
|
|
1621
|
+
} catch {
|
|
1622
|
+
}
|
|
1623
|
+
p8.log.success(`Removed ${repoName} from workspace.`);
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
var validateSubCommand = defineCommand8({
|
|
1627
|
+
meta: {
|
|
1628
|
+
name: "validate",
|
|
1629
|
+
description: "Validate interface contracts against implementations"
|
|
1630
|
+
},
|
|
1631
|
+
run: async () => {
|
|
1632
|
+
const cwd = process.cwd();
|
|
1633
|
+
const wsPath = join5(cwd, "workspace.yaml");
|
|
1634
|
+
if (!await pathExists2(wsPath)) {
|
|
1635
|
+
p8.log.error("No workspace found.");
|
|
1636
|
+
process.exit(1);
|
|
1637
|
+
}
|
|
1638
|
+
const contractsDir = join5(cwd, "contracts");
|
|
1639
|
+
if (!await pathExists2(contractsDir)) {
|
|
1640
|
+
p8.log.error("No contracts/ directory found.");
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
const files = (await readdir4(contractsDir)).filter(
|
|
1644
|
+
(f) => f.endsWith(".contract.yaml")
|
|
1645
|
+
);
|
|
1646
|
+
if (files.length === 0) {
|
|
1647
|
+
p8.log.info("No contracts found. Create them with /sniper-workspace feature.");
|
|
1648
|
+
process.exit(0);
|
|
1649
|
+
}
|
|
1650
|
+
p8.intro("Contract Validation");
|
|
1651
|
+
for (const file of files) {
|
|
1652
|
+
try {
|
|
1653
|
+
const raw = await readFile5(join5(contractsDir, file), "utf-8");
|
|
1654
|
+
const contract = YAML5.parse(raw);
|
|
1655
|
+
const name = contract.contract?.name || file;
|
|
1656
|
+
const version2 = contract.contract?.version || "?";
|
|
1657
|
+
const endpoints = contract.endpoints?.length || 0;
|
|
1658
|
+
const types = contract.shared_types?.length || 0;
|
|
1659
|
+
const events = contract.events?.length || 0;
|
|
1660
|
+
p8.log.info(
|
|
1661
|
+
`${name} v${version2}: ${endpoints} endpoints, ${types} types, ${events} events`
|
|
1662
|
+
);
|
|
1663
|
+
p8.log.info(
|
|
1664
|
+
" (Structural validation requires running /sniper-workspace validate as a slash command)"
|
|
1665
|
+
);
|
|
1666
|
+
} catch {
|
|
1667
|
+
p8.log.warn(` ${file}: parse error`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
p8.log.info(
|
|
1671
|
+
"\nFull validation (endpoint/type/event checking) runs via the /sniper-workspace validate slash command."
|
|
1672
|
+
);
|
|
1673
|
+
p8.outro("");
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
function inferRole(projectType) {
|
|
1677
|
+
switch (projectType) {
|
|
1678
|
+
case "saas":
|
|
1679
|
+
case "web":
|
|
1680
|
+
case "mobile":
|
|
1681
|
+
return "frontend";
|
|
1682
|
+
case "api":
|
|
1683
|
+
return "backend";
|
|
1684
|
+
case "library":
|
|
1685
|
+
return "library";
|
|
1686
|
+
case "cli":
|
|
1687
|
+
case "monorepo":
|
|
1688
|
+
return "service";
|
|
1689
|
+
default:
|
|
1690
|
+
return "service";
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
var workspaceCommand = defineCommand8({
|
|
1694
|
+
meta: {
|
|
1695
|
+
name: "workspace",
|
|
1696
|
+
description: "Manage SNIPER workspaces for multi-project orchestration"
|
|
1697
|
+
},
|
|
1698
|
+
subCommands: {
|
|
1699
|
+
init: initSubCommand,
|
|
1700
|
+
status: statusSubCommand,
|
|
1701
|
+
"add-repo": addRepoSubCommand,
|
|
1702
|
+
"remove-repo": removeRepoSubCommand,
|
|
1703
|
+
validate: validateSubCommand
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
|
|
778
1707
|
// src/index.ts
|
|
779
1708
|
var require2 = createRequire2(import.meta.url);
|
|
780
1709
|
var { version } = require2("../package.json");
|
|
781
|
-
var main =
|
|
1710
|
+
var main = defineCommand9({
|
|
782
1711
|
meta: {
|
|
783
1712
|
name: "sniper",
|
|
784
1713
|
version,
|
|
@@ -790,7 +1719,9 @@ var main = defineCommand7({
|
|
|
790
1719
|
"add-pack": addPackCommand,
|
|
791
1720
|
"remove-pack": removePackCommand,
|
|
792
1721
|
"list-packs": listPacksCommand,
|
|
793
|
-
update: updateCommand
|
|
1722
|
+
update: updateCommand,
|
|
1723
|
+
memory: memoryCommand,
|
|
1724
|
+
workspace: workspaceCommand
|
|
794
1725
|
}
|
|
795
1726
|
});
|
|
796
1727
|
runMain(main);
|