@zalom/plastic 1.0.0-alpha.1 → 1.0.0-alpha.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/install.rb +74 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalom/plastic",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -304,42 +304,78 @@ end
304
304
 
305
305
  def merge_claude_hooks(settings_path)
306
306
  settings = read_json_safe(settings_path) || {}
307
- return if settings.nil? # unparseable — refuse to modify
307
+ return if settings.nil?
308
308
 
309
309
  hooks = settings["hooks"] ||= {}
310
310
  hook_dir = File.join(Dir.home, ".claude", "hooks")
311
311
 
312
+ purge_stale_plastic_hooks(hooks)
313
+
312
314
  plastic_hooks = {
313
- "SessionStart" => [
314
- { "type" => "command", "command" => "ruby #{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
315
- { "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
316
- ],
317
- "PreCompact" => [
318
- { "type" => "command", "command" => "ruby #{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
319
- ],
320
- "PostToolUse" => [
321
- { "matcher" => "Write|Edit", "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
322
- ],
323
- "UserPromptSubmit" => [
324
- { "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
325
- { "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
326
- ],
327
- "statusLine" => [
328
- { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" },
329
- ],
315
+ "SessionStart" => {
316
+ "matcher" => "",
317
+ "hooks" => [
318
+ { "type" => "command", "command" => "#{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
319
+ { "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
320
+ ],
321
+ },
322
+ "PreCompact" => {
323
+ "matcher" => "",
324
+ "hooks" => [
325
+ { "type" => "command", "command" => "#{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
326
+ ],
327
+ },
328
+ "PostToolUse" => {
329
+ "matcher" => "Write|Edit",
330
+ "hooks" => [
331
+ { "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
332
+ ],
333
+ },
334
+ "UserPromptSubmit" => {
335
+ "matcher" => "",
336
+ "hooks" => [
337
+ { "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
338
+ { "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
339
+ ],
340
+ },
330
341
  }
331
342
 
332
- plastic_hooks.each do |event, entries|
343
+ plastic_hooks.each do |event, group|
333
344
  hooks[event] ||= []
334
- entries.each do |entry|
335
- already = hooks[event].any? { |h| h["command"] == entry["command"] }
336
- hooks[event] << entry unless already
345
+ existing = hooks[event].find { |g| g.is_a?(Hash) && g["hooks"].is_a?(Array) && g["hooks"].any? { |h| h["command"].to_s.include?("plastic-") } }
346
+
347
+ if existing
348
+ existing["matcher"] = group["matcher"]
349
+ existing["hooks"] = group["hooks"]
350
+ else
351
+ hooks[event] << group
337
352
  end
338
353
  end
339
354
 
355
+ settings["statusLine"] = { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" }
356
+
340
357
  write_json_atomic(settings_path, settings)
341
358
  end
342
359
 
360
+ def purge_stale_plastic_hooks(hooks)
361
+ stale = ->(cmd) { cmd.to_s.match?(/ruby\s+.*plastic-/) || cmd.to_s.match?(/plastic-[a-z-]+\.rb/) }
362
+
363
+ hooks.each do |event, groups|
364
+ next unless groups.is_a?(Array)
365
+
366
+ hooks[event] = groups.map do |group|
367
+ if group.is_a?(Hash) && group["hooks"].is_a?(Array)
368
+ group["hooks"].reject! { |h| stale.call(h["command"]) }
369
+ group unless group["hooks"].empty?
370
+ elsif group.is_a?(Hash) && group["command"]
371
+ stale.call(group["command"]) ? nil : group
372
+ else
373
+ group
374
+ end
375
+ end.compact
376
+ end
377
+ end
378
+
343
379
  # --- Uninstall ---
344
380
 
345
381
  def handle_uninstall(agents)
@@ -403,11 +439,24 @@ def remove_claude_hooks(settings_path)
403
439
  settings = read_json_safe(settings_path)
404
440
  return unless settings && settings["hooks"]
405
441
 
406
- settings["hooks"].each do |event, entries|
407
- settings["hooks"][event] = entries.reject { |h| (h["command"] || "").include?("plastic-") }
442
+ settings["hooks"].each do |event, groups|
443
+ next unless groups.is_a?(Array)
444
+
445
+ settings["hooks"][event] = groups.map do |group|
446
+ if group.is_a?(Hash) && group["hooks"].is_a?(Array)
447
+ group["hooks"].reject! { |h| h["command"].to_s.include?("plastic-") }
448
+ group unless group["hooks"].empty?
449
+ elsif group.is_a?(Hash) && group["command"]
450
+ group["command"].to_s.include?("plastic-") ? nil : group
451
+ else
452
+ group
453
+ end
454
+ end.compact
408
455
  end
409
- settings["hooks"].delete_if { |_, v| v.empty? }
456
+
457
+ settings["hooks"].delete_if { |_, v| v.is_a?(Array) && v.empty? }
410
458
  settings.delete("hooks") if settings["hooks"]&.empty?
459
+ settings.delete("statusLine") if settings.dig("statusLine", "command").to_s.include?("plastic-")
411
460
 
412
461
  write_json_atomic(settings_path, settings)
413
462
  end