@zalom/plastic 1.0.0-alpha.2 → 1.0.0-alpha.21
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/PLASTIC.md +128 -473
- package/README.md +90 -58
- package/agents/future-intent-researcher.md +1 -1
- package/agents/intent-curator.md +1 -1
- package/bin/plastic.js +57 -0
- package/bin/test +28 -0
- package/deprecations.yml +7 -6
- package/hooks/auto-arm +5 -0
- package/hooks/bash-gate +3 -0
- package/hooks/check-update +12 -8
- package/hooks/code-gate +10 -0
- package/hooks/hooks.json +25 -4
- package/hooks/statusline +50 -10
- package/package.json +2 -2
- package/scripts/dashboard.rb +480 -0
- package/scripts/doctor.rb +973 -0
- package/scripts/hook-auto-arm +52 -0
- package/scripts/hook-bash-gate +53 -0
- package/scripts/hook-code-gate +39 -0
- package/scripts/hook-continue +15 -114
- package/scripts/hook-gate-check +19 -4
- package/scripts/hook-session-start +76 -31
- package/scripts/install.rb +91 -480
- package/scripts/lib/bridge.rb +255 -0
- package/scripts/lib/installer_core.rb +760 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/select-update-target +93 -0
- package/scripts/uninstall.rb +53 -0
- package/scripts/update.rb +142 -0
- package/scripts/versions.rb +141 -0
- package/skills/_active-intent-gate.md +26 -0
- package/skills/auto/SKILL.md +62 -9
- package/skills/auto/evals/evals.json +92 -0
- package/skills/auto/references/agent-architecture.md +60 -0
- package/skills/brainstorming/SKILL.md +143 -0
- package/skills/brainstorming-grill-me/SKILL.md +5 -5
- package/skills/continuing/SKILL.md +102 -77
- package/skills/continuing/evals/evals.json +136 -0
- package/skills/continuing/references/context-management.md +32 -0
- package/skills/creating-intent/SKILL.md +16 -1
- package/skills/creating-intent/references/lifecycle.md +74 -0
- package/skills/creating-intent/references/wikilinks.md +8 -0
- package/skills/creating-project/SKILL.md +8 -4
- package/skills/creating-project/references/hubs-projects.md +55 -0
- package/skills/dashboard/SKILL.md +92 -0
- package/skills/doctor/SKILL.md +116 -0
- package/skills/doctor/references/gates-stuck-detection.md +38 -0
- package/skills/doctor/report.md +96 -0
- package/skills/evaluating-skills/SKILL.md +140 -0
- package/skills/evaluating-skills/assets/eval-template.json +12 -0
- package/skills/evaluating-skills/evals/evals.json +75 -0
- package/skills/evaluating-skills/references/convention-checks.md +76 -0
- package/skills/evaluating-skills/references/eval-methodology.md +154 -0
- package/skills/executing-plan/SKILL.md +3 -3
- package/skills/install/SKILL.md +56 -8
- package/skills/intent-curator/SKILL.md +3 -3
- package/skills/linking-intents/SKILL.md +5 -1
- package/skills/linking-intents/references/zettelkasten.md +33 -0
- package/skills/managing-index/SKILL.md +5 -1
- package/skills/releasing/SKILL.md +119 -18
- package/skills/releasing/references/deprecations.md +44 -0
- package/skills/research/SKILL.md +114 -0
- package/skills/savepoint/SKILL.md +46 -37
- package/skills/savepoint/references/context-management.md +32 -0
- package/skills/uninstall/SKILL.md +39 -28
- package/skills/update/SKILL.md +41 -36
- package/skills/versions/SKILL.md +65 -0
- package/skills/writing-instructions/SKILL.md +159 -0
- package/skills/writing-instructions/references/agentskills-spec.md +135 -0
- package/skills/writing-plans/SKILL.md +183 -0
- package/templates/agents.md +16 -0
- package/templates/outcome.md +13 -0
- package/templates/project.yml +5 -0
- package/templates/savepoint.md +14 -13
- package/templates/spec.md +25 -0
- package/bin/install.js +0 -29
package/scripts/install.rb
CHANGED
|
@@ -2,522 +2,133 @@
|
|
|
2
2
|
# encoding: UTF-8
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
# Plastic
|
|
6
|
-
# Usage: ruby scripts/install.rb [--claude
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def main
|
|
24
|
-
flags = parse_flags(ARGV)
|
|
25
|
-
|
|
26
|
-
if flags[:help]
|
|
27
|
-
show_help
|
|
28
|
-
return
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
puts "\n\u{1f9e0} Plastic v#{VERSION}\n\n"
|
|
32
|
-
|
|
33
|
-
if flags[:uninstall]
|
|
34
|
-
handle_uninstall(flags[:agents].empty? ? ["claude"] : flags[:agents])
|
|
35
|
-
return
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
agents = flags[:agents].empty? ? prompt_agents : flags[:agents]
|
|
39
|
-
|
|
40
|
-
if agents.empty?
|
|
41
|
-
puts "No agents selected. Nothing to do."
|
|
42
|
-
return
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
mode = File.exist?(File.join(PLASTIC_HOME, "INDEX.md")) ? :update : :install
|
|
46
|
-
puts "Mode: #{mode}"
|
|
47
|
-
puts "Agents: #{agents.map { |k| agent_config(k)[:name] }.join(", ")}\n\n"
|
|
48
|
-
|
|
49
|
-
distribute(mode)
|
|
50
|
-
bootstrap if mode == :install
|
|
51
|
-
|
|
52
|
-
results = agents.map { |key| install_for_agent(key, flags[:force]) }
|
|
53
|
-
|
|
54
|
-
puts "\n\u{2014} Results \u{2014}\n\n"
|
|
55
|
-
results.each do |r|
|
|
56
|
-
if r[:success]
|
|
57
|
-
puts " \u{2705} #{r[:agent]}: #{r[:files]} files installed"
|
|
58
|
-
else
|
|
59
|
-
puts " \u{26a0}\u{fe0f} #{r[:agent]}: #{r[:reason]}"
|
|
5
|
+
# Plastic — `install` verb. Runs via `npx @zalom/plastic install` (bin/plastic.js) or directly.
|
|
6
|
+
# Usage: ruby scripts/install.rb [--claude|--codex|--hermes|--all] [--alpha|--beta|--latest] [--reinstall] [--force] [--help]
|
|
7
|
+
#
|
|
8
|
+
# One-shot by design:
|
|
9
|
+
# - no install present -> fresh install + bootstrap store
|
|
10
|
+
# - already installed -> refuse (point at `update` / `--reinstall`)
|
|
11
|
+
# - --reinstall -> re-sync core files for the installed version (repair); store untouched
|
|
12
|
+
#
|
|
13
|
+
# `update` and `versions` delegate their file-sync here via `--reinstall --ledger-action <action>`,
|
|
14
|
+
# so install.rb is the single file-syncer; the ledger action is contextual.
|
|
15
|
+
|
|
16
|
+
require_relative "lib/installer_core"
|
|
17
|
+
|
|
18
|
+
class Install < InstallerCore
|
|
19
|
+
def cli(argv = ARGV)
|
|
20
|
+
if argv.include?("--help") || argv.include?("-h")
|
|
21
|
+
show_help
|
|
22
|
+
return 0
|
|
60
23
|
end
|
|
61
|
-
end
|
|
62
24
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
puts " Registered for: #{installed.map { |r| r[:agent] }.join(", ")}"
|
|
67
|
-
puts " Run /clear (or restart your agent) to pick up new conventions.\n\n"
|
|
68
|
-
end
|
|
69
|
-
end
|
|
25
|
+
force = argv.include?("--force")
|
|
26
|
+
reinstall = argv.include?("--reinstall")
|
|
27
|
+
ledger_action = flag_value(argv, "--ledger-action")
|
|
70
28
|
|
|
71
|
-
|
|
29
|
+
selected = agent_keys_from(argv)
|
|
30
|
+
selected = prompt_agents if selected.empty?
|
|
31
|
+
if selected.empty?
|
|
32
|
+
puts "No agents selected. Nothing to do."
|
|
33
|
+
return 0
|
|
34
|
+
end
|
|
72
35
|
|
|
73
|
-
|
|
74
|
-
flags = { agents: [], force: false, uninstall: false, help: false }
|
|
36
|
+
puts "\n\u{1f9e0} Plastic v#{version}\n\n"
|
|
75
37
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
when "--help", "-h" then flags[:help] = true
|
|
82
|
-
else
|
|
83
|
-
agent = AGENTS.find { |a| a[:flag] == arg }
|
|
84
|
-
flags[:agents] << agent[:key] if agent
|
|
38
|
+
if installed? && !reinstall
|
|
39
|
+
warn "Plastic v#{installed_version} is already installed."
|
|
40
|
+
warn " - To upgrade: npx @zalom/plastic update"
|
|
41
|
+
warn " - To re-sync files: npx @zalom/plastic install --reinstall"
|
|
42
|
+
return 1
|
|
85
43
|
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
flags
|
|
89
|
-
end
|
|
90
44
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return ["claude"]
|
|
45
|
+
run(selected: selected, force: force, reinstall: reinstall, ledger_action: ledger_action)
|
|
46
|
+
0
|
|
94
47
|
end
|
|
95
48
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
print "Select (comma-separated numbers, or Enter for Claude Code): "
|
|
102
|
-
answer = $stdin.gets&.strip || ""
|
|
103
|
-
|
|
104
|
-
return ["claude"] if answer.empty?
|
|
105
|
-
|
|
106
|
-
nums = answer.split(",").map { |n| n.strip.to_i }
|
|
107
|
-
return AGENTS.map { |a| a[:key] } if nums.include?(AGENTS.size + 1)
|
|
108
|
-
|
|
109
|
-
nums.select { |n| n >= 1 && n <= AGENTS.size }.map { |n| AGENTS[n - 1][:key] }
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def show_help
|
|
113
|
-
puts <<~HELP
|
|
114
|
-
|
|
115
|
-
plastic - Intent-driven idea development system
|
|
116
|
-
|
|
117
|
-
Usage:
|
|
118
|
-
npx @zalom/plastic@latest [options]
|
|
49
|
+
# Hermetic entrypoint (no prompting / no exit). Returns the per-agent results array.
|
|
50
|
+
def run(selected:, force: false, reinstall: false, ledger_action: nil)
|
|
51
|
+
fresh = !installed?
|
|
52
|
+
mode = fresh ? :install : :update # :update here means "re-sync, skip bootstrap"
|
|
119
53
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
--codex Install for Codex CLI
|
|
123
|
-
--hermes Install for Hermes
|
|
124
|
-
--all Install for all supported agents
|
|
125
|
-
--force Overwrite existing files without prompting
|
|
126
|
-
--uninstall Remove Plastic from agent directories
|
|
127
|
-
-h, --help Show this help
|
|
54
|
+
distribute(mode)
|
|
55
|
+
bootstrap if fresh
|
|
128
56
|
|
|
129
|
-
|
|
130
|
-
npx @zalom/plastic@latest Interactive agent selection
|
|
131
|
-
npx @zalom/plastic@latest --claude Install for Claude Code only
|
|
132
|
-
npx @zalom/plastic@latest --all Install for all agents
|
|
133
|
-
npx @zalom/plastic@latest --uninstall Remove from agent directories
|
|
57
|
+
results = selected.map { |key| install_for_agent(key, force) }
|
|
134
58
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# --- Distribution phase ---
|
|
139
|
-
|
|
140
|
-
def distribute(mode)
|
|
141
|
-
puts " \u{1f4e6} #{mode == :update ? "Updating" : "Installing"} core files to #{PLASTIC_HOME}"
|
|
142
|
-
|
|
143
|
-
FileUtils.mkdir_p(PLASTIC_HOME)
|
|
144
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "scripts"))
|
|
145
|
-
|
|
146
|
-
core_files = {
|
|
147
|
-
"PLASTIC.md" => "PLASTIC.md",
|
|
148
|
-
"deprecations.yml" => "deprecations.yml",
|
|
149
|
-
"scripts/folgezettel-id" => "scripts/folgezettel-id",
|
|
150
|
-
"scripts/read-config" => "scripts/read-config",
|
|
151
|
-
}
|
|
59
|
+
action = ledger_action || (fresh ? "install" : "reinstall")
|
|
60
|
+
ledger_append(version, action)
|
|
152
61
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
dest_path = File.join(PLASTIC_HOME, dest)
|
|
156
|
-
FileUtils.cp(src_path, dest_path) if File.exist?(src_path)
|
|
62
|
+
print_results(results, fresh ? :install : :reinstall)
|
|
63
|
+
results
|
|
157
64
|
end
|
|
158
65
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Dir.glob(File.join(PLASTIC_HOME, "scripts", "*")).each { |f| FileUtils.chmod(0o755, f) }
|
|
162
|
-
|
|
163
|
-
puts " \u{2705} Core files synced (v#{VERSION})"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def bootstrap
|
|
167
|
-
puts " \u{1f331} First install \u{2014} bootstrapping store..."
|
|
168
|
-
|
|
169
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "store"))
|
|
170
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "projects"))
|
|
171
|
-
|
|
172
|
-
write_if_missing(File.join(PLASTIC_HOME, "config.yml"), <<~YAML)
|
|
173
|
-
version: 3
|
|
174
|
-
execution_mode: subagent-driven
|
|
175
|
-
stale_threshold_days: 3
|
|
176
|
-
hash_length: 6
|
|
177
|
-
hash_algorithm: sha256-base36
|
|
178
|
-
max_slug_words: 5
|
|
179
|
-
agent:
|
|
180
|
-
type: claude-code
|
|
181
|
-
parallel_mode: agent-teams
|
|
182
|
-
YAML
|
|
183
|
-
|
|
184
|
-
write_if_missing(File.join(PLASTIC_HOME, "projects.yml"), "---\nprojects: {}\n")
|
|
185
|
-
|
|
186
|
-
write_if_missing(File.join(PLASTIC_HOME, "INDEX.md"), <<~MD)
|
|
187
|
-
# Index
|
|
188
|
-
|
|
189
|
-
## Active
|
|
190
|
-
|
|
191
|
-
## Future
|
|
192
|
-
|
|
193
|
-
## Clusters
|
|
194
|
-
|
|
195
|
-
## Abandoned
|
|
196
|
-
|
|
197
|
-
## Completed
|
|
198
|
-
MD
|
|
199
|
-
|
|
200
|
-
write_if_missing(File.join(PLASTIC_HOME, "AGENTS.md"), <<~MD)
|
|
201
|
-
# Plastic \u{2014} Agent Instructions
|
|
202
|
-
|
|
203
|
-
Read `PLASTIC.md` in this directory. It contains all Plastic conventions.
|
|
204
|
-
Follow it exactly. Never modify it \u{2014} it is overwritten on plugin updates.
|
|
205
|
-
|
|
206
|
-
This file (`AGENTS.md`) is where project-specific rules live.
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
MD
|
|
210
|
-
|
|
211
|
-
puts " \u{2705} Store bootstrapped"
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# --- Agent adapters ---
|
|
215
|
-
|
|
216
|
-
def install_for_agent(key, force)
|
|
217
|
-
config = agent_config(key)
|
|
218
|
-
return { agent: config[:name], success: false, reason: "Unknown agent" } unless config
|
|
219
|
-
|
|
220
|
-
unless File.directory?(config[:dir])
|
|
221
|
-
return { agent: config[:name], success: false, reason: "#{config[:dir]} not found \u{2014} #{config[:name]} not installed?" }
|
|
66
|
+
def installed?
|
|
67
|
+
File.exist?(File.join(plastic_home, "VERSION"))
|
|
222
68
|
end
|
|
223
69
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
when "hermes" then install_hermes(config, force)
|
|
70
|
+
def installed_version
|
|
71
|
+
path = File.join(plastic_home, "VERSION")
|
|
72
|
+
File.exist?(path) ? File.read(path).strip : nil
|
|
228
73
|
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def install_claude(config, force)
|
|
232
|
-
hooks_dir = File.join(config[:dir], "hooks")
|
|
233
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
234
|
-
plastic_dir = File.join(config[:dir], "plastic")
|
|
235
74
|
|
|
236
|
-
|
|
237
|
-
FileUtils.mkdir_p(skills_dir)
|
|
238
|
-
FileUtils.mkdir_p(plastic_dir)
|
|
75
|
+
private
|
|
239
76
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
Dir.glob(File.join(hook_source, "*")).each do |f|
|
|
245
|
-
next unless File.file?(f)
|
|
246
|
-
basename = File.basename(f)
|
|
247
|
-
next if %w[hooks.json run-hook].include?(basename)
|
|
248
|
-
dest_name = basename.start_with?("plastic-") ? basename : "plastic-#{basename}"
|
|
249
|
-
dest = File.join(hooks_dir, dest_name)
|
|
250
|
-
FileUtils.cp(f, dest)
|
|
251
|
-
FileUtils.chmod(0o755, dest)
|
|
252
|
-
installed << dest
|
|
77
|
+
def flag_value(argv, name)
|
|
78
|
+
i = argv.index(name)
|
|
79
|
+
return nil unless i && argv[i + 1]
|
|
80
|
+
argv[i + 1]
|
|
253
81
|
end
|
|
254
82
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
#
|
|
260
|
-
version_file = File.join(plastic_dir, "VERSION")
|
|
261
|
-
File.write(version_file, "#{VERSION}\n")
|
|
262
|
-
installed << version_file
|
|
263
|
-
|
|
264
|
-
# Merge hooks into settings.json
|
|
265
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
266
|
-
merge_claude_hooks(settings_path)
|
|
267
|
-
|
|
268
|
-
# Write manifest
|
|
269
|
-
manifest_path = File.join(plastic_dir, "manifest.json")
|
|
270
|
-
write_manifest(installed, manifest_path)
|
|
271
|
-
|
|
272
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def install_codex(config, force)
|
|
276
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
277
|
-
FileUtils.mkdir_p(skills_dir)
|
|
278
|
-
|
|
279
|
-
installed = []
|
|
280
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
281
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
282
|
-
|
|
283
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
284
|
-
write_manifest(installed, manifest_path)
|
|
285
|
-
|
|
286
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def install_hermes(config, force)
|
|
290
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
291
|
-
FileUtils.mkdir_p(skills_dir)
|
|
292
|
-
|
|
293
|
-
installed = []
|
|
294
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
295
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
296
|
-
|
|
297
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
298
|
-
write_manifest(installed, manifest_path)
|
|
299
|
-
|
|
300
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
# --- settings.json merge (read-modify-write, never clobber) ---
|
|
304
|
-
|
|
305
|
-
def merge_claude_hooks(settings_path)
|
|
306
|
-
settings = read_json_safe(settings_path) || {}
|
|
307
|
-
return if settings.nil?
|
|
308
|
-
|
|
309
|
-
hooks = settings["hooks"] ||= {}
|
|
310
|
-
hook_dir = File.join(Dir.home, ".claude", "hooks")
|
|
311
|
-
|
|
312
|
-
purge_stale_plastic_hooks(hooks)
|
|
313
|
-
|
|
314
|
-
plastic_hooks = {
|
|
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
|
-
},
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
plastic_hooks.each do |event, group|
|
|
344
|
-
hooks[event] ||= []
|
|
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
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
settings["statusLine"] = { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" }
|
|
356
|
-
|
|
357
|
-
write_json_atomic(settings_path, settings)
|
|
358
|
-
end
|
|
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
|
|
83
|
+
def print_results(results, mode)
|
|
84
|
+
puts "\n\u{2014} Results \u{2014}\n\n"
|
|
85
|
+
results.each do |r|
|
|
86
|
+
if r[:success]
|
|
87
|
+
puts " \u{2705} #{r[:agent]}: #{r[:files]} files installed"
|
|
372
88
|
else
|
|
373
|
-
|
|
89
|
+
puts " \u{26a0}\u{fe0f} #{r[:agent]}: #{r[:reason]}"
|
|
374
90
|
end
|
|
375
|
-
end.compact
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
# --- Uninstall ---
|
|
380
|
-
|
|
381
|
-
def handle_uninstall(agents)
|
|
382
|
-
agents.each do |key|
|
|
383
|
-
config = agent_config(key)
|
|
384
|
-
next unless config
|
|
385
|
-
|
|
386
|
-
result = uninstall_agent(key, config)
|
|
387
|
-
if result[:success]
|
|
388
|
-
puts " \u{2705} #{config[:name]}: uninstalled (#{result[:files]} files removed)"
|
|
389
|
-
else
|
|
390
|
-
puts " \u{26a0}\u{fe0f} #{config[:name]}: #{result[:reason]}"
|
|
391
91
|
end
|
|
392
|
-
end
|
|
393
92
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def uninstall_agent(key, config)
|
|
398
|
-
unless File.directory?(config[:dir])
|
|
399
|
-
return { success: false, reason: "#{config[:dir]} not found" }
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
manifest_path = case key
|
|
403
|
-
when "claude" then File.join(config[:dir], "plastic", "manifest.json")
|
|
404
|
-
else File.join(config[:dir], "plastic-manifest.json")
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
files_removed = 0
|
|
408
|
-
|
|
409
|
-
if File.exist?(manifest_path)
|
|
410
|
-
manifest = JSON.parse(File.read(manifest_path)) rescue {}
|
|
411
|
-
(manifest["files"] || {}).each_key do |f|
|
|
412
|
-
if File.exist?(f)
|
|
413
|
-
File.delete(f)
|
|
414
|
-
files_removed += 1
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
File.delete(manifest_path)
|
|
418
|
-
files_removed += 1
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
# Clean known directories
|
|
422
|
-
dirs_to_clean = case key
|
|
423
|
-
when "claude" then [File.join(config[:dir], "plastic"), File.join(config[:dir], "skills", "plastic")]
|
|
424
|
-
else [File.join(config[:dir], "skills", "plastic")]
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
dirs_to_clean.each { |d| FileUtils.rm_rf(d) if File.directory?(d) }
|
|
428
|
-
|
|
429
|
-
# Clean hooks from settings.json (Claude Code only)
|
|
430
|
-
if key == "claude"
|
|
431
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
432
|
-
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
{ success: true, files: files_removed }
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def remove_claude_hooks(settings_path)
|
|
439
|
-
settings = read_json_safe(settings_path)
|
|
440
|
-
return unless settings && settings["hooks"]
|
|
93
|
+
installed = results.select { |r| r[:success] }
|
|
94
|
+
return unless installed.any?
|
|
441
95
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
96
|
+
verb = mode == :reinstall ? "re-synced" : "installed"
|
|
97
|
+
puts "\n\u{2705} Plastic v#{version} #{verb}."
|
|
98
|
+
puts " Registered for: #{installed.map { |r| r[:agent] }.join(", ")}"
|
|
99
|
+
puts " Run /clear (or restart your agent) to pick up new conventions.\n\n"
|
|
455
100
|
end
|
|
456
101
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
settings.delete("statusLine") if settings.dig("statusLine", "command").to_s.include?("plastic-")
|
|
102
|
+
def show_help
|
|
103
|
+
puts <<~HELP
|
|
460
104
|
|
|
461
|
-
|
|
462
|
-
end
|
|
105
|
+
plastic install — install or re-sync Plastic for an agent
|
|
463
106
|
|
|
464
|
-
|
|
107
|
+
Usage:
|
|
108
|
+
npx @zalom/plastic install [options]
|
|
465
109
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
110
|
+
Agent options:
|
|
111
|
+
--claude Install for Claude Code
|
|
112
|
+
--codex Install for Codex CLI
|
|
113
|
+
--hermes Install for Hermes
|
|
114
|
+
--all Install for all supported agents
|
|
469
115
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
src_path = File.join(src, entry)
|
|
475
|
-
dest_path = File.join(dest, entry)
|
|
476
|
-
if File.directory?(src_path)
|
|
477
|
-
files += copy_dir_recursive(src_path, dest_path)
|
|
478
|
-
elsif File.file?(src_path)
|
|
479
|
-
FileUtils.cp(src_path, dest_path)
|
|
480
|
-
files << dest_path
|
|
481
|
-
end
|
|
482
|
-
end
|
|
483
|
-
files
|
|
484
|
-
end
|
|
116
|
+
Channel options:
|
|
117
|
+
--latest Stable channel (default)
|
|
118
|
+
--beta Beta channel
|
|
119
|
+
--alpha Alpha channel
|
|
485
120
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
# Try JSONC stripping (remove // comments and trailing commas)
|
|
491
|
-
content = File.read(path).gsub(%r{//[^\n]*}, "").gsub(/,(\s*[}\]])/, '\1')
|
|
492
|
-
JSON.parse(content)
|
|
493
|
-
rescue
|
|
494
|
-
nil
|
|
495
|
-
end
|
|
121
|
+
Other options:
|
|
122
|
+
--reinstall Re-sync core files for the installed version (repair). Store untouched.
|
|
123
|
+
--force Overwrite existing files without prompting
|
|
124
|
+
-h, --help Show this help
|
|
496
125
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
File.write(tmp, content)
|
|
501
|
-
File.rename(tmp, path)
|
|
502
|
-
rescue => e
|
|
503
|
-
File.delete(tmp) if tmp && File.exist?(tmp)
|
|
504
|
-
raise e
|
|
505
|
-
end
|
|
126
|
+
Notes:
|
|
127
|
+
Install is one-shot. If Plastic is already installed, use `update` to upgrade or
|
|
128
|
+
`install --reinstall` to repair. To change versions, use `update` / `versions`.
|
|
506
129
|
|
|
507
|
-
|
|
508
|
-
entries = {}
|
|
509
|
-
files.each do |f|
|
|
510
|
-
entries[f] = Digest::SHA256.file(f).hexdigest if File.exist?(f)
|
|
130
|
+
HELP
|
|
511
131
|
end
|
|
512
|
-
|
|
513
|
-
data = { "version" => "1", "created" => Time.now.utc.iso8601, "files" => entries }
|
|
514
|
-
File.write(manifest_path, JSON.pretty_generate(data) + "\n")
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
def write_if_missing(path, content)
|
|
518
|
-
File.write(path, content) unless File.exist?(path)
|
|
519
132
|
end
|
|
520
133
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
main
|
|
134
|
+
exit(Install.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__)).cli(ARGV)) if $PROGRAM_NAME == __FILE__
|