@sniper.ai/cli 3.0.0 → 3.1.1

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 CHANGED
@@ -7,7 +7,7 @@ CLI tool for scaffolding and managing [SNIPER](https://sniperai.dev/)-enabled pr
7
7
 
8
8
  ## What is SNIPER?
9
9
 
10
- SNIPER (**S**pawn, **N**avigate, **I**mplement, **P**arallelize, **E**valuate, **R**elease) is an AI-powered project lifecycle framework that orchestrates Claude Code agent teams through structured phases -- from discovery and planning through implementation and release. Each phase spawns coordinated teams of specialized agents composed from layered personas.
10
+ SNIPER is an AI-powered project lifecycle framework that orchestrates Claude Code agent teams through structured phases -- from discovery and planning through implementation and release. Each phase spawns coordinated teams of specialized agents composed from layered personas.
11
11
 
12
12
  ## Quick Start
13
13
 
package/dist/index.js CHANGED
@@ -152,6 +152,12 @@ async function composeMixin(basePath, mixinPaths) {
152
152
  }
153
153
  return content;
154
154
  }
155
+ function stableStringify(obj) {
156
+ if (obj === null || obj === void 0 || typeof obj !== "object") return JSON.stringify(obj ?? null);
157
+ if (Array.isArray(obj)) return "[" + obj.map(stableStringify).join(",") + "]";
158
+ const sorted = Object.keys(obj).sort();
159
+ return "{" + sorted.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
160
+ }
155
161
  function mergeHooks(base, ...sources) {
156
162
  const result = { ...base };
157
163
  if (!result.hooks || typeof result.hooks !== "object") {
@@ -164,11 +170,22 @@ function mergeHooks(base, ...sources) {
164
170
  if (!Array.isArray(entries)) continue;
165
171
  if (!hooks[event]) hooks[event] = [];
166
172
  for (const entry of entries) {
167
- const desc = entry.description;
173
+ const typedEntry = entry;
174
+ const matcherKey = stableStringify(typedEntry.matcher || {});
168
175
  const existing = hooks[event].find(
169
- (h) => h.description === desc
176
+ (h) => stableStringify(h.matcher || {}) === matcherKey
170
177
  );
171
- if (!existing) {
178
+ if (existing) {
179
+ const existingHooks = existing.hooks || [];
180
+ const newHooks = typedEntry.hooks || [];
181
+ for (const hook of newHooks) {
182
+ const alreadyExists = existingHooks.some((h) => h.description === hook.description);
183
+ if (!alreadyExists) {
184
+ existingHooks.push(hook);
185
+ }
186
+ }
187
+ existing.hooks = existingHooks;
188
+ } else {
172
189
  hooks[event].push(entry);
173
190
  }
174
191
  }
@@ -258,6 +275,43 @@ async function scaffoldProject(cwd, config, options = {}) {
258
275
  const coreHooks = JSON.parse(await readFile2(coreHooksPath, "utf-8"));
259
276
  settings = mergeHooks(settings, coreHooks);
260
277
  }
278
+ const signalHooksPath = join2(corePath, "hooks", "signal-hooks.json");
279
+ if (await fileExists(signalHooksPath)) {
280
+ const signalHooks = JSON.parse(await readFile2(signalHooksPath, "utf-8"));
281
+ settings = mergeHooks(settings, signalHooks);
282
+ }
283
+ if (config.plugins) {
284
+ for (const plugin of config.plugins) {
285
+ const pluginName = plugin.name;
286
+ const pluginYamlPath = join2(corePath, "..", "plugins", `plugin-${pluginName}`, "plugin.yaml");
287
+ if (!await fileExists(pluginYamlPath)) {
288
+ log14.push(`Warning: plugin "${pluginName}" not found at ${pluginYamlPath}`);
289
+ continue;
290
+ }
291
+ const pluginContent = YAML2.parse(await readFile2(pluginYamlPath, "utf-8"));
292
+ if (pluginContent?.hooks) {
293
+ const pluginHooksFormatted = {};
294
+ for (const [event, entries] of Object.entries(pluginContent.hooks)) {
295
+ if (!Array.isArray(entries)) continue;
296
+ pluginHooksFormatted[event] = entries.map((entry) => {
297
+ if (typeof entry === "object" && entry !== null && "matcher" in entry && typeof entry.matcher === "object" && "hooks" in entry && Array.isArray(entry.hooks)) {
298
+ return entry;
299
+ }
300
+ const cmd = String(entry);
301
+ return {
302
+ matcher: {},
303
+ hooks: [{
304
+ type: "command",
305
+ description: `${pluginName} plugin: ${cmd.split(" ")[0]}`,
306
+ command: cmd
307
+ }]
308
+ };
309
+ });
310
+ }
311
+ settings = mergeHooks(settings, { hooks: pluginHooksFormatted });
312
+ }
313
+ }
314
+ }
261
315
  if (!settings.env || typeof settings.env !== "object") {
262
316
  settings.env = {};
263
317
  }
@@ -276,8 +330,13 @@ async function scaffoldProject(cwd, config, options = {}) {
276
330
  } else {
277
331
  log14.push("Skipped CLAUDE.md (preserved user customizations)");
278
332
  }
279
- if (!isUpdate) {
280
- await ensureDir(join2(cwd, "docs"));
333
+ await ensureDir(join2(cwd, "docs"));
334
+ const registryTemplate = join2(corePath, "templates", "registry.md");
335
+ const registryDest = join2(cwd, "docs", "registry.md");
336
+ if (await fileExists(registryTemplate) && !await fileExists(registryDest)) {
337
+ await cp(registryTemplate, registryDest);
338
+ log14.push(isUpdate ? "Created missing docs/registry.md" : "Created docs/ with registry.md");
339
+ } else if (!isUpdate) {
281
340
  log14.push("Created docs/");
282
341
  }
283
342
  return log14;