@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/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 defineCommand7, runMain } from "citty";
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(p7) {
109
+ async function fileExists(p9) {
110
110
  try {
111
- await access2(p7);
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 log7 = [];
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
- log7.push(`Copied ${dir}/`);
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
- log7.push("Created config.yaml");
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
- log7.push("Created CLAUDE.md");
157
+ log9.push("Created CLAUDE.md");
142
158
  } else {
143
- log7.push("Skipped CLAUDE.md (preserved user customizations)");
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
- log7.push("Created .claude/settings.json");
173
+ log9.push("Created .claude/settings.json");
158
174
  } else {
159
- log7.push("Skipped .claude/settings.json (preserved user customizations)");
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
- log7.push("Copied skills to .claude/commands/");
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
- log7.push("Created docs/ directory");
194
+ log9.push("Created docs/ directory");
179
195
  }
180
- return log7;
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 log7 = await scaffoldProject(cwd, config);
410
+ const log9 = await scaffoldProject(cwd, config);
395
411
  s.stop("Done!");
396
- for (const entry of log7) {
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(p7) {
509
+ async function pathExists(p9) {
494
510
  try {
495
- await access3(p7);
511
+ await access3(p9);
496
512
  return true;
497
513
  } catch {
498
514
  return false;
499
515
  }
500
516
  }
501
- async function readJson(p7) {
502
- const raw = await readFile3(p7, "utf-8");
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((p7) => p7.name === shortName)) {
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
- (p7) => p7.name === packName
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
- (p7) => p7.name !== packName
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 log7 = await scaffoldProject(cwd, currentConfig, { update: true });
780
+ const log9 = await scaffoldProject(cwd, currentConfig, { update: true });
765
781
  s.stop("Done!");
766
- for (const entry of log7) {
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 = defineCommand7({
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);