@zalom/plastic 1.0.0-alpha.8 → 1.0.0-beta.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/PLASTIC.md +154 -469
- package/README.md +95 -58
- package/agents/plastic-brainstorming.md +37 -0
- package/agents/plastic-enforcer.md +36 -0
- package/agents/plastic-executor.md +37 -0
- package/agents/{future-intent-researcher.md → plastic-future-intent-researcher.md} +1 -1
- package/agents/{intent-curator.md → plastic-intent-curator.md} +1 -1
- package/agents/plastic-planner.md +37 -0
- package/agents/plastic-spec-specialist.md +37 -0
- package/bin/plastic.js +57 -0
- package/bin/test +28 -0
- package/deprecations.yml +1 -10
- package/hooks/auto-arm +5 -0
- package/hooks/bash-gate +3 -0
- package/hooks/check-update +12 -8
- package/hooks/code-gate +12 -0
- package/hooks/create-gate +3 -0
- package/hooks/gate-check +3 -1
- package/hooks/hooks.json +52 -0
- package/hooks/qmd-search +8 -0
- package/package.json +2 -2
- package/scripts/dashboard.rb +685 -0
- package/scripts/doctor.rb +987 -632
- package/scripts/hook-auto-arm +51 -0
- package/scripts/hook-bash-gate +41 -0
- package/scripts/hook-code-gate +27 -0
- package/scripts/hook-continue +15 -114
- package/scripts/hook-create-gate +59 -0
- package/scripts/hook-gate-check +47 -32
- package/scripts/hook-qmd-search +44 -0
- package/scripts/hook-session-start +106 -38
- package/scripts/install.rb +91 -504
- package/scripts/lib/boot_banner.rb +28 -0
- package/scripts/lib/bridge.rb +404 -19
- package/scripts/lib/installer_core.rb +807 -0
- package/scripts/lib/intent_validator.rb +156 -0
- package/scripts/lib/qmd_hook.rb +44 -0
- package/scripts/lib/qmd_sync.rb +209 -0
- package/scripts/lib/store_provisioning.rb +100 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +177 -0
- package/scripts/provision-project-store +53 -0
- package/scripts/qmd-sync +92 -0
- package/scripts/select-update-target +93 -0
- package/scripts/spawn-preamble +121 -0
- package/scripts/uninstall.rb +53 -0
- package/scripts/update.rb +164 -0
- package/scripts/validate-intent +54 -0
- package/scripts/versions.rb +141 -0
- package/skills/_active-intent-gate.md +1 -1
- package/skills/add-project-store/SKILL.md +54 -0
- package/skills/auto/SKILL.md +77 -7
- package/skills/auto/evals/evals.json +207 -0
- package/skills/auto/references/agent-architecture.md +135 -0
- package/skills/brainstorming/SKILL.md +9 -9
- package/skills/brainstorming-grill-me/SKILL.md +6 -6
- package/skills/continuing/SKILL.md +92 -82
- package/skills/continuing/evals/evals.json +136 -0
- package/skills/continuing/references/context-management.md +32 -0
- package/skills/creating-intent/SKILL.md +59 -33
- package/skills/creating-intent/references/lifecycle.md +73 -0
- package/skills/creating-intent/references/wikilinks.md +8 -0
- package/skills/creating-project/SKILL.md +40 -8
- package/skills/creating-project/references/hubs-projects.md +55 -0
- package/skills/dashboard/SKILL.md +121 -0
- package/skills/dashboard/templates/dashboard-global.md +31 -0
- package/skills/dashboard/templates/dashboard-project.md +40 -0
- package/skills/doctor/SKILL.md +51 -4
- package/skills/doctor/references/gates-stuck-detection.md +38 -0
- package/skills/doctor/report.md +4 -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 +5 -3
- package/skills/install/SKILL.md +69 -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 +80 -23
- package/skills/releasing/references/deprecations.md +60 -0
- package/skills/research/SKILL.md +2 -2
- 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 -44
- 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 +5 -5
- package/templates/agents.md +7 -7
- package/templates/outcome.md +13 -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,546 +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
|
|
70
|
-
|
|
71
|
-
# --- Flag parsing ---
|
|
25
|
+
force = argv.include?("--force")
|
|
26
|
+
reinstall = argv.include?("--reinstall")
|
|
27
|
+
ledger_action = flag_value(argv, "--ledger-action")
|
|
72
28
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
when "--all" then flags[:agents] = AGENTS.map { |a| a[:key] }
|
|
79
|
-
when "--force" then flags[:force] = true
|
|
80
|
-
when "--uninstall" then flags[:uninstall] = true
|
|
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
|
|
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
|
|
85
34
|
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
flags
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def prompt_agents
|
|
92
|
-
unless $stdin.tty?
|
|
93
|
-
return ["claude"]
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
puts "Which agents should Plastic register for?\n\n"
|
|
97
|
-
AGENTS.each_with_index { |a, i| puts " #{i + 1}. #{a[:name]} (#{a[:dir]})" }
|
|
98
|
-
puts " #{AGENTS.size + 1}. All"
|
|
99
|
-
puts
|
|
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
35
|
|
|
112
|
-
|
|
113
|
-
puts <<~HELP
|
|
36
|
+
puts "\n\u{1f9e0} Plastic v#{version}\n\n"
|
|
114
37
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
npx @zalom/plastic
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
--claude Install for Claude Code
|
|
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
|
|
128
|
-
|
|
129
|
-
Examples:
|
|
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
|
|
134
|
-
|
|
135
|
-
HELP
|
|
136
|
-
end
|
|
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", "lib"))
|
|
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
|
-
"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",
|
|
156
|
-
"scripts/doctor.rb" => "scripts/doctor.rb",
|
|
157
|
-
}
|
|
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
|
|
43
|
+
end
|
|
158
44
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
dest_path = File.join(PLASTIC_HOME, dest)
|
|
162
|
-
FileUtils.cp(src_path, dest_path) if File.exist?(src_path)
|
|
45
|
+
run(selected: selected, force: force, reinstall: reinstall, ledger_action: ledger_action)
|
|
46
|
+
0
|
|
163
47
|
end
|
|
164
48
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
puts " \u{2705} Core files synced (v#{VERSION})"
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def bootstrap
|
|
173
|
-
puts " \u{1f331} First install \u{2014} bootstrapping store..."
|
|
174
|
-
|
|
175
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "store"))
|
|
176
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "projects"))
|
|
177
|
-
|
|
178
|
-
write_if_missing(File.join(PLASTIC_HOME, "config.yml"), <<~YAML)
|
|
179
|
-
version: 3
|
|
180
|
-
execution_mode: subagent-driven
|
|
181
|
-
stale_threshold_days: 3
|
|
182
|
-
hash_length: 6
|
|
183
|
-
hash_algorithm: sha256-base36
|
|
184
|
-
max_slug_words: 5
|
|
185
|
-
agent:
|
|
186
|
-
type: claude-code
|
|
187
|
-
parallel_mode: agent-teams
|
|
188
|
-
YAML
|
|
189
|
-
|
|
190
|
-
write_if_missing(File.join(PLASTIC_HOME, "projects.yml"), "---\nprojects: {}\n")
|
|
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"
|
|
191
53
|
|
|
192
|
-
|
|
193
|
-
|
|
54
|
+
distribute(mode)
|
|
55
|
+
bootstrap if fresh
|
|
194
56
|
|
|
195
|
-
|
|
57
|
+
results = selected.map { |key| install_for_agent(key, force) }
|
|
196
58
|
|
|
197
|
-
|
|
59
|
+
action = ledger_action || (fresh ? "install" : "reinstall")
|
|
60
|
+
ledger_append(version, action)
|
|
198
61
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
## Abandoned
|
|
202
|
-
|
|
203
|
-
## Completed
|
|
204
|
-
MD
|
|
205
|
-
|
|
206
|
-
write_if_missing(File.join(PLASTIC_HOME, "AGENTS.md"), <<~MD)
|
|
207
|
-
# Plastic \u{2014} Agent Instructions
|
|
208
|
-
|
|
209
|
-
Read `PLASTIC.md` in this directory. It contains all Plastic conventions.
|
|
210
|
-
Follow it exactly. Never modify it \u{2014} it is overwritten on plugin updates.
|
|
211
|
-
|
|
212
|
-
This file (`AGENTS.md`) is where project-specific rules live.
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
MD
|
|
216
|
-
|
|
217
|
-
puts " \u{2705} Store bootstrapped"
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# --- Agent adapters ---
|
|
221
|
-
|
|
222
|
-
def install_for_agent(key, force)
|
|
223
|
-
config = agent_config(key)
|
|
224
|
-
return { agent: config[:name], success: false, reason: "Unknown agent" } unless config
|
|
225
|
-
|
|
226
|
-
unless File.directory?(config[:dir])
|
|
227
|
-
return { agent: config[:name], success: false, reason: "#{config[:dir]} not found \u{2014} #{config[:name]} not installed?" }
|
|
62
|
+
print_results(results, fresh ? :install : :reinstall)
|
|
63
|
+
results
|
|
228
64
|
end
|
|
229
65
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
when "codex" then install_codex(config, force)
|
|
233
|
-
when "hermes" then install_hermes(config, force)
|
|
66
|
+
def installed?
|
|
67
|
+
File.exist?(File.join(plastic_home, "VERSION"))
|
|
234
68
|
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def install_claude(config, force)
|
|
238
|
-
hooks_dir = File.join(config[:dir], "hooks")
|
|
239
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
240
|
-
plastic_dir = File.join(config[:dir], "plastic")
|
|
241
69
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
installed = []
|
|
247
|
-
|
|
248
|
-
# Copy hooks, rewriting script paths for the installed location
|
|
249
|
-
hook_source = File.join(PACKAGE_ROOT, "hooks")
|
|
250
|
-
Dir.glob(File.join(hook_source, "*")).each do |f|
|
|
251
|
-
next unless File.file?(f)
|
|
252
|
-
basename = File.basename(f)
|
|
253
|
-
next if %w[hooks.json run-hook].include?(basename)
|
|
254
|
-
dest_name = basename.start_with?("plastic-") ? basename : "plastic-#{basename}"
|
|
255
|
-
dest = File.join(hooks_dir, dest_name)
|
|
256
|
-
content = File.read(f)
|
|
257
|
-
content = content.gsub('$SCRIPT_DIR/../scripts/', '$HOME/.plastic/scripts/')
|
|
258
|
-
File.write(dest, content)
|
|
259
|
-
FileUtils.chmod(0o755, dest)
|
|
260
|
-
installed << dest
|
|
70
|
+
def installed_version
|
|
71
|
+
path = File.join(plastic_home, "VERSION")
|
|
72
|
+
File.exist?(path) ? File.read(path).strip : nil
|
|
261
73
|
end
|
|
262
74
|
|
|
263
|
-
|
|
264
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
265
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
266
|
-
|
|
267
|
-
# Write VERSION
|
|
268
|
-
version_file = File.join(plastic_dir, "VERSION")
|
|
269
|
-
File.write(version_file, "#{VERSION}\n")
|
|
270
|
-
installed << version_file
|
|
271
|
-
|
|
272
|
-
# Merge hooks into settings.json
|
|
273
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
274
|
-
merge_claude_hooks(settings_path)
|
|
75
|
+
private
|
|
275
76
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def install_codex(config, force)
|
|
284
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
285
|
-
FileUtils.mkdir_p(skills_dir)
|
|
286
|
-
|
|
287
|
-
installed = []
|
|
288
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
289
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
290
|
-
|
|
291
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
292
|
-
write_manifest(installed, manifest_path)
|
|
293
|
-
|
|
294
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def install_hermes(config, force)
|
|
298
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
299
|
-
FileUtils.mkdir_p(skills_dir)
|
|
300
|
-
|
|
301
|
-
installed = []
|
|
302
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
303
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
304
|
-
|
|
305
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
306
|
-
write_manifest(installed, manifest_path)
|
|
307
|
-
|
|
308
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
# --- settings.json merge (read-modify-write, never clobber) ---
|
|
312
|
-
|
|
313
|
-
def merge_claude_hooks(settings_path)
|
|
314
|
-
settings = read_json_safe(settings_path) || {}
|
|
315
|
-
return if settings.nil?
|
|
316
|
-
|
|
317
|
-
hooks = settings["hooks"] ||= {}
|
|
318
|
-
hook_dir = File.join(Dir.home, ".claude", "hooks")
|
|
319
|
-
|
|
320
|
-
purge_stale_plastic_hooks(hooks)
|
|
321
|
-
|
|
322
|
-
plastic_hooks = {
|
|
323
|
-
"SessionStart" => {
|
|
324
|
-
"matcher" => "",
|
|
325
|
-
"hooks" => [
|
|
326
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
|
|
327
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
|
|
328
|
-
],
|
|
329
|
-
},
|
|
330
|
-
"PreCompact" => {
|
|
331
|
-
"matcher" => "",
|
|
332
|
-
"hooks" => [
|
|
333
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
|
|
334
|
-
],
|
|
335
|
-
},
|
|
336
|
-
"PostToolUse" => {
|
|
337
|
-
"matcher" => "Write|Edit",
|
|
338
|
-
"hooks" => [
|
|
339
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
|
|
340
|
-
],
|
|
341
|
-
},
|
|
342
|
-
"UserPromptSubmit" => {
|
|
343
|
-
"matcher" => "",
|
|
344
|
-
"hooks" => [
|
|
345
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
|
|
346
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
|
|
347
|
-
],
|
|
348
|
-
},
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
plastic_hooks.each do |event, group|
|
|
352
|
-
hooks[event] ||= []
|
|
353
|
-
existing = hooks[event].find { |g| g.is_a?(Hash) && g["hooks"].is_a?(Array) && g["hooks"].any? { |h| h["command"].to_s.include?("plastic-") } }
|
|
354
|
-
|
|
355
|
-
if existing
|
|
356
|
-
existing["matcher"] = group["matcher"]
|
|
357
|
-
existing["hooks"] = group["hooks"]
|
|
358
|
-
else
|
|
359
|
-
hooks[event] << group
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
existing_status = settings["statusLine"]
|
|
364
|
-
if existing_status && !existing_status.dig("command").to_s.include?("plastic-")
|
|
365
|
-
cache_dir = File.join(PLASTIC_HOME, ".cache")
|
|
366
|
-
FileUtils.mkdir_p(cache_dir)
|
|
367
|
-
File.write(File.join(cache_dir, "original-statusline.json"), JSON.pretty_generate(existing_status))
|
|
77
|
+
def flag_value(argv, name)
|
|
78
|
+
i = argv.index(name)
|
|
79
|
+
return nil unless i && argv[i + 1]
|
|
80
|
+
argv[i + 1]
|
|
368
81
|
end
|
|
369
82
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
def purge_stale_plastic_hooks(hooks)
|
|
376
|
-
plastic_cmd = ->(cmd) { cmd.to_s.include?("plastic-") }
|
|
377
|
-
|
|
378
|
-
hooks.delete("statusLine")
|
|
379
|
-
|
|
380
|
-
hooks.each do |event, groups|
|
|
381
|
-
next unless groups.is_a?(Array)
|
|
382
|
-
|
|
383
|
-
hooks[event] = groups.map do |group|
|
|
384
|
-
if group.is_a?(Hash) && group["hooks"].is_a?(Array)
|
|
385
|
-
group["hooks"].reject! { |h| plastic_cmd.call(h["command"]) }
|
|
386
|
-
group unless group["hooks"].empty?
|
|
387
|
-
elsif group.is_a?(Hash) && group["command"]
|
|
388
|
-
plastic_cmd.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"
|
|
389
88
|
else
|
|
390
|
-
|
|
391
|
-
end
|
|
392
|
-
end.compact
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# --- Uninstall ---
|
|
397
|
-
|
|
398
|
-
def handle_uninstall(agents)
|
|
399
|
-
agents.each do |key|
|
|
400
|
-
config = agent_config(key)
|
|
401
|
-
next unless config
|
|
402
|
-
|
|
403
|
-
result = uninstall_agent(key, config)
|
|
404
|
-
if result[:success]
|
|
405
|
-
puts " \u{2705} #{config[:name]}: uninstalled (#{result[:files]} files removed)"
|
|
406
|
-
else
|
|
407
|
-
puts " \u{26a0}\u{fe0f} #{config[:name]}: #{result[:reason]}"
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
puts "\n Note: ~/.plastic/ (your intent store) is preserved.\n\n"
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def uninstall_agent(key, config)
|
|
415
|
-
unless File.directory?(config[:dir])
|
|
416
|
-
return { success: false, reason: "#{config[:dir]} not found" }
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
manifest_path = case key
|
|
420
|
-
when "claude" then File.join(config[:dir], "plastic", "manifest.json")
|
|
421
|
-
else File.join(config[:dir], "plastic-manifest.json")
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
files_removed = 0
|
|
425
|
-
|
|
426
|
-
if File.exist?(manifest_path)
|
|
427
|
-
manifest = JSON.parse(File.read(manifest_path)) rescue {}
|
|
428
|
-
(manifest["files"] || {}).each_key do |f|
|
|
429
|
-
if File.exist?(f)
|
|
430
|
-
File.delete(f)
|
|
431
|
-
files_removed += 1
|
|
89
|
+
puts " \u{26a0}\u{fe0f} #{r[:agent]}: #{r[:reason]}"
|
|
432
90
|
end
|
|
433
91
|
end
|
|
434
|
-
File.delete(manifest_path)
|
|
435
|
-
files_removed += 1
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# Clean known directories
|
|
439
|
-
dirs_to_clean = case key
|
|
440
|
-
when "claude" then [File.join(config[:dir], "plastic"), File.join(config[:dir], "skills", "plastic")]
|
|
441
|
-
else [File.join(config[:dir], "skills", "plastic")]
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
dirs_to_clean.each { |d| FileUtils.rm_rf(d) if File.directory?(d) }
|
|
445
|
-
|
|
446
|
-
# Clean hooks from settings.json (Claude Code only)
|
|
447
|
-
if key == "claude"
|
|
448
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
449
|
-
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
{ success: true, files: files_removed }
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def remove_claude_hooks(settings_path)
|
|
456
|
-
settings = read_json_safe(settings_path)
|
|
457
|
-
return unless settings && settings["hooks"]
|
|
458
92
|
|
|
459
|
-
|
|
460
|
-
|
|
93
|
+
installed = results.select { |r| r[:success] }
|
|
94
|
+
return unless installed.any?
|
|
461
95
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
elsif group.is_a?(Hash) && group["command"]
|
|
467
|
-
group["command"].to_s.include?("plastic-") ? nil : group
|
|
468
|
-
else
|
|
469
|
-
group
|
|
470
|
-
end
|
|
471
|
-
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"
|
|
472
100
|
end
|
|
473
101
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if settings.dig("statusLine", "command").to_s.include?("plastic-")
|
|
477
|
-
settings.delete("statusLine")
|
|
478
|
-
original_path = File.join(PLASTIC_HOME, ".cache", "original-statusline.json")
|
|
479
|
-
if File.exist?(original_path)
|
|
480
|
-
original = JSON.parse(File.read(original_path)) rescue nil
|
|
481
|
-
settings["statusLine"] = original if original
|
|
482
|
-
end
|
|
483
|
-
end
|
|
102
|
+
def show_help
|
|
103
|
+
puts <<~HELP
|
|
484
104
|
|
|
485
|
-
|
|
486
|
-
end
|
|
105
|
+
plastic install — install or re-sync Plastic for an agent
|
|
487
106
|
|
|
488
|
-
|
|
107
|
+
Usage:
|
|
108
|
+
npx @zalom/plastic install [options]
|
|
489
109
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
493
115
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
src_path = File.join(src, entry)
|
|
499
|
-
dest_path = File.join(dest, entry)
|
|
500
|
-
if File.directory?(src_path)
|
|
501
|
-
files += copy_dir_recursive(src_path, dest_path)
|
|
502
|
-
elsif File.file?(src_path)
|
|
503
|
-
FileUtils.cp(src_path, dest_path)
|
|
504
|
-
files << dest_path
|
|
505
|
-
end
|
|
506
|
-
end
|
|
507
|
-
files
|
|
508
|
-
end
|
|
116
|
+
Channel options:
|
|
117
|
+
--latest Stable channel (default)
|
|
118
|
+
--beta Beta channel
|
|
119
|
+
--alpha Alpha channel
|
|
509
120
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
# Try JSONC stripping (remove // comments and trailing commas)
|
|
515
|
-
content = File.read(path).gsub(%r{//[^\n]*}, "").gsub(/,(\s*[}\]])/, '\1')
|
|
516
|
-
JSON.parse(content)
|
|
517
|
-
rescue
|
|
518
|
-
nil
|
|
519
|
-
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
|
|
520
125
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
File.write(tmp, content)
|
|
525
|
-
File.rename(tmp, path)
|
|
526
|
-
rescue => e
|
|
527
|
-
File.delete(tmp) if tmp && File.exist?(tmp)
|
|
528
|
-
raise e
|
|
529
|
-
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`.
|
|
530
129
|
|
|
531
|
-
|
|
532
|
-
entries = {}
|
|
533
|
-
files.each do |f|
|
|
534
|
-
entries[f] = Digest::SHA256.file(f).hexdigest if File.exist?(f)
|
|
130
|
+
HELP
|
|
535
131
|
end
|
|
536
|
-
|
|
537
|
-
data = { "version" => "1", "created" => Time.now.utc.iso8601, "files" => entries }
|
|
538
|
-
File.write(manifest_path, JSON.pretty_generate(data) + "\n")
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def write_if_missing(path, content)
|
|
542
|
-
File.write(path, content) unless File.exist?(path)
|
|
543
132
|
end
|
|
544
133
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
main
|
|
134
|
+
exit(Install.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__)).cli(ARGV)) if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Renders the SessionStart boot banner from a core-health result (intent 36a).
|
|
5
|
+
#
|
|
6
|
+
# Pure and dependency-injected: it takes the health hash (the return value of
|
|
7
|
+
# Doctor#run_core_checks) and the version string, and returns a single banner
|
|
8
|
+
# line. No I/O, no globals, no ENV — so it is unit-testable in isolation while
|
|
9
|
+
# the hook feeds it real data.
|
|
10
|
+
module BootBanner
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# health: the Hash returned by Doctor#run_core_checks, or nil if the check
|
|
14
|
+
# itself raised (degraded to an error banner).
|
|
15
|
+
# version: the installed Plastic version string, or nil.
|
|
16
|
+
#
|
|
17
|
+
# Returns one of two binary lines:
|
|
18
|
+
# "Plastic Core loaded — v{VER} | doctor --core run: success"
|
|
19
|
+
# "Plastic Core loaded — v{VER} | doctor --core run: error — run /plastic-doctor"
|
|
20
|
+
def render(health:, version:)
|
|
21
|
+
ver = version || "unknown"
|
|
22
|
+
if !health.nil? && health[:status] == "pass"
|
|
23
|
+
"Plastic Core loaded — v#{ver} | doctor --core run: success"
|
|
24
|
+
else
|
|
25
|
+
"Plastic Core loaded — v#{ver} | doctor --core run: error — run /plastic-doctor"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|