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

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 +84 -28
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.3",
4
4
  "description": "Intent-driven idea development system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -141,13 +141,18 @@ def distribute(mode)
141
141
  puts " \u{1f4e6} #{mode == :update ? "Updating" : "Installing"} core files to #{PLASTIC_HOME}"
142
142
 
143
143
  FileUtils.mkdir_p(PLASTIC_HOME)
144
- FileUtils.mkdir_p(File.join(PLASTIC_HOME, "scripts"))
144
+ FileUtils.mkdir_p(File.join(PLASTIC_HOME, "scripts", "lib"))
145
145
 
146
146
  core_files = {
147
147
  "PLASTIC.md" => "PLASTIC.md",
148
148
  "deprecations.yml" => "deprecations.yml",
149
149
  "scripts/folgezettel-id" => "scripts/folgezettel-id",
150
150
  "scripts/read-config" => "scripts/read-config",
151
+ "scripts/hook-session-start" => "scripts/hook-session-start",
152
+ "scripts/hook-continue" => "scripts/hook-continue",
153
+ "scripts/hook-future-intent-check" => "scripts/hook-future-intent-check",
154
+ "scripts/hook-gate-check" => "scripts/hook-gate-check",
155
+ "scripts/lib/bridge.rb" => "scripts/lib/bridge.rb",
151
156
  }
152
157
 
153
158
  core_files.each do |src, dest|
@@ -239,7 +244,7 @@ def install_claude(config, force)
239
244
 
240
245
  installed = []
241
246
 
242
- # Copy hooks
247
+ # Copy hooks, rewriting script paths for the installed location
243
248
  hook_source = File.join(PACKAGE_ROOT, "hooks")
244
249
  Dir.glob(File.join(hook_source, "*")).each do |f|
245
250
  next unless File.file?(f)
@@ -247,7 +252,9 @@ def install_claude(config, force)
247
252
  next if %w[hooks.json run-hook].include?(basename)
248
253
  dest_name = basename.start_with?("plastic-") ? basename : "plastic-#{basename}"
249
254
  dest = File.join(hooks_dir, dest_name)
250
- FileUtils.cp(f, dest)
255
+ content = File.read(f)
256
+ content = content.gsub('$SCRIPT_DIR/../scripts/', '$HOME/.plastic/scripts/')
257
+ File.write(dest, content)
251
258
  FileUtils.chmod(0o755, dest)
252
259
  installed << dest
253
260
  end
@@ -304,42 +311,78 @@ end
304
311
 
305
312
  def merge_claude_hooks(settings_path)
306
313
  settings = read_json_safe(settings_path) || {}
307
- return if settings.nil? # unparseable — refuse to modify
314
+ return if settings.nil?
308
315
 
309
316
  hooks = settings["hooks"] ||= {}
310
317
  hook_dir = File.join(Dir.home, ".claude", "hooks")
311
318
 
319
+ purge_stale_plastic_hooks(hooks)
320
+
312
321
  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
- ],
322
+ "SessionStart" => {
323
+ "matcher" => "",
324
+ "hooks" => [
325
+ { "type" => "command", "command" => "#{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
326
+ { "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
327
+ ],
328
+ },
329
+ "PreCompact" => {
330
+ "matcher" => "",
331
+ "hooks" => [
332
+ { "type" => "command", "command" => "#{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
333
+ ],
334
+ },
335
+ "PostToolUse" => {
336
+ "matcher" => "Write|Edit",
337
+ "hooks" => [
338
+ { "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
339
+ ],
340
+ },
341
+ "UserPromptSubmit" => {
342
+ "matcher" => "",
343
+ "hooks" => [
344
+ { "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
345
+ { "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
346
+ ],
347
+ },
330
348
  }
331
349
 
332
- plastic_hooks.each do |event, entries|
350
+ plastic_hooks.each do |event, group|
333
351
  hooks[event] ||= []
334
- entries.each do |entry|
335
- already = hooks[event].any? { |h| h["command"] == entry["command"] }
336
- hooks[event] << entry unless already
352
+ existing = hooks[event].find { |g| g.is_a?(Hash) && g["hooks"].is_a?(Array) && g["hooks"].any? { |h| h["command"].to_s.include?("plastic-") } }
353
+
354
+ if existing
355
+ existing["matcher"] = group["matcher"]
356
+ existing["hooks"] = group["hooks"]
357
+ else
358
+ hooks[event] << group
337
359
  end
338
360
  end
339
361
 
362
+ settings["statusLine"] = { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" }
363
+
340
364
  write_json_atomic(settings_path, settings)
341
365
  end
342
366
 
367
+ def purge_stale_plastic_hooks(hooks)
368
+ stale = ->(cmd) { cmd.to_s.match?(/ruby\s+.*plastic-/) || cmd.to_s.match?(/plastic-[a-z-]+\.rb/) }
369
+
370
+ hooks.each do |event, groups|
371
+ next unless groups.is_a?(Array)
372
+
373
+ hooks[event] = groups.map do |group|
374
+ if group.is_a?(Hash) && group["hooks"].is_a?(Array)
375
+ group["hooks"].reject! { |h| stale.call(h["command"]) }
376
+ group unless group["hooks"].empty?
377
+ elsif group.is_a?(Hash) && group["command"]
378
+ stale.call(group["command"]) ? nil : group
379
+ else
380
+ group
381
+ end
382
+ end.compact
383
+ end
384
+ end
385
+
343
386
  # --- Uninstall ---
344
387
 
345
388
  def handle_uninstall(agents)
@@ -403,11 +446,24 @@ def remove_claude_hooks(settings_path)
403
446
  settings = read_json_safe(settings_path)
404
447
  return unless settings && settings["hooks"]
405
448
 
406
- settings["hooks"].each do |event, entries|
407
- settings["hooks"][event] = entries.reject { |h| (h["command"] || "").include?("plastic-") }
449
+ settings["hooks"].each do |event, groups|
450
+ next unless groups.is_a?(Array)
451
+
452
+ settings["hooks"][event] = groups.map do |group|
453
+ if group.is_a?(Hash) && group["hooks"].is_a?(Array)
454
+ group["hooks"].reject! { |h| h["command"].to_s.include?("plastic-") }
455
+ group unless group["hooks"].empty?
456
+ elsif group.is_a?(Hash) && group["command"]
457
+ group["command"].to_s.include?("plastic-") ? nil : group
458
+ else
459
+ group
460
+ end
461
+ end.compact
408
462
  end
409
- settings["hooks"].delete_if { |_, v| v.empty? }
463
+
464
+ settings["hooks"].delete_if { |_, v| v.is_a?(Array) && v.empty? }
410
465
  settings.delete("hooks") if settings["hooks"]&.empty?
466
+ settings.delete("statusLine") if settings.dig("statusLine", "command").to_s.include?("plastic-")
411
467
 
412
468
  write_json_atomic(settings_path, settings)
413
469
  end