@zalom/plastic 1.0.0-alpha.3 → 1.0.0-alpha.31
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 +134 -469
- package/README.md +95 -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 +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/gate-check +3 -1
- package/hooks/hooks.json +25 -4
- package/hooks/statusline +50 -10
- package/package.json +2 -2
- package/scripts/dashboard.rb +680 -0
- package/scripts/doctor.rb +1227 -0
- 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-gate-check +30 -34
- package/scripts/hook-session-start +121 -31
- package/scripts/install.rb +91 -487
- package/scripts/lib/boot_banner.rb +28 -0
- package/scripts/lib/bridge.rb +358 -10
- package/scripts/lib/installer_core.rb +747 -0
- package/scripts/lib/intent_validator.rb +90 -0
- package/scripts/lib/qmd_sync.rb +159 -0
- package/scripts/lib/store_provisioning.rb +100 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/provision-project-store +53 -0
- package/scripts/qmd-sync +92 -0
- package/scripts/select-update-target +93 -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 +26 -0
- package/skills/add-project-store/SKILL.md +54 -0
- package/skills/auto/SKILL.md +69 -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 +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 +38 -9
- package/skills/creating-intent/references/lifecycle.md +74 -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 +159 -0
- package/skills/doctor/references/gates-stuck-detection.md +38 -0
- package/skills/doctor/report.md +100 -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 +119 -18
- package/skills/releasing/references/deprecations.md +60 -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,529 +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", "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
|
-
}
|
|
59
|
+
action = ledger_action || (fresh ? "install" : "reinstall")
|
|
60
|
+
ledger_append(version, action)
|
|
157
61
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
dest_path = File.join(PLASTIC_HOME, dest)
|
|
161
|
-
FileUtils.cp(src_path, dest_path) if File.exist?(src_path)
|
|
62
|
+
print_results(results, fresh ? :install : :reinstall)
|
|
63
|
+
results
|
|
162
64
|
end
|
|
163
65
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
Dir.glob(File.join(PLASTIC_HOME, "scripts", "*")).each { |f| FileUtils.chmod(0o755, f) }
|
|
167
|
-
|
|
168
|
-
puts " \u{2705} Core files synced (v#{VERSION})"
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def bootstrap
|
|
172
|
-
puts " \u{1f331} First install \u{2014} bootstrapping store..."
|
|
173
|
-
|
|
174
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "store"))
|
|
175
|
-
FileUtils.mkdir_p(File.join(PLASTIC_HOME, "projects"))
|
|
176
|
-
|
|
177
|
-
write_if_missing(File.join(PLASTIC_HOME, "config.yml"), <<~YAML)
|
|
178
|
-
version: 3
|
|
179
|
-
execution_mode: subagent-driven
|
|
180
|
-
stale_threshold_days: 3
|
|
181
|
-
hash_length: 6
|
|
182
|
-
hash_algorithm: sha256-base36
|
|
183
|
-
max_slug_words: 5
|
|
184
|
-
agent:
|
|
185
|
-
type: claude-code
|
|
186
|
-
parallel_mode: agent-teams
|
|
187
|
-
YAML
|
|
188
|
-
|
|
189
|
-
write_if_missing(File.join(PLASTIC_HOME, "projects.yml"), "---\nprojects: {}\n")
|
|
190
|
-
|
|
191
|
-
write_if_missing(File.join(PLASTIC_HOME, "INDEX.md"), <<~MD)
|
|
192
|
-
# Index
|
|
193
|
-
|
|
194
|
-
## Active
|
|
195
|
-
|
|
196
|
-
## Future
|
|
197
|
-
|
|
198
|
-
## Clusters
|
|
199
|
-
|
|
200
|
-
## Abandoned
|
|
201
|
-
|
|
202
|
-
## Completed
|
|
203
|
-
MD
|
|
204
|
-
|
|
205
|
-
write_if_missing(File.join(PLASTIC_HOME, "AGENTS.md"), <<~MD)
|
|
206
|
-
# Plastic \u{2014} Agent Instructions
|
|
207
|
-
|
|
208
|
-
Read `PLASTIC.md` in this directory. It contains all Plastic conventions.
|
|
209
|
-
Follow it exactly. Never modify it \u{2014} it is overwritten on plugin updates.
|
|
210
|
-
|
|
211
|
-
This file (`AGENTS.md`) is where project-specific rules live.
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
MD
|
|
215
|
-
|
|
216
|
-
puts " \u{2705} Store bootstrapped"
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
# --- Agent adapters ---
|
|
220
|
-
|
|
221
|
-
def install_for_agent(key, force)
|
|
222
|
-
config = agent_config(key)
|
|
223
|
-
return { agent: config[:name], success: false, reason: "Unknown agent" } unless config
|
|
224
|
-
|
|
225
|
-
unless File.directory?(config[:dir])
|
|
226
|
-
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"))
|
|
227
68
|
end
|
|
228
69
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
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
|
|
233
73
|
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def install_claude(config, force)
|
|
237
|
-
hooks_dir = File.join(config[:dir], "hooks")
|
|
238
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
239
|
-
plastic_dir = File.join(config[:dir], "plastic")
|
|
240
74
|
|
|
241
|
-
|
|
242
|
-
FileUtils.mkdir_p(skills_dir)
|
|
243
|
-
FileUtils.mkdir_p(plastic_dir)
|
|
75
|
+
private
|
|
244
76
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Dir.glob(File.join(hook_source, "*")).each do |f|
|
|
250
|
-
next unless File.file?(f)
|
|
251
|
-
basename = File.basename(f)
|
|
252
|
-
next if %w[hooks.json run-hook].include?(basename)
|
|
253
|
-
dest_name = basename.start_with?("plastic-") ? basename : "plastic-#{basename}"
|
|
254
|
-
dest = File.join(hooks_dir, dest_name)
|
|
255
|
-
content = File.read(f)
|
|
256
|
-
content = content.gsub('$SCRIPT_DIR/../scripts/', '$HOME/.plastic/scripts/')
|
|
257
|
-
File.write(dest, content)
|
|
258
|
-
FileUtils.chmod(0o755, dest)
|
|
259
|
-
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]
|
|
260
81
|
end
|
|
261
82
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
#
|
|
267
|
-
version_file = File.join(plastic_dir, "VERSION")
|
|
268
|
-
File.write(version_file, "#{VERSION}\n")
|
|
269
|
-
installed << version_file
|
|
270
|
-
|
|
271
|
-
# Merge hooks into settings.json
|
|
272
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
273
|
-
merge_claude_hooks(settings_path)
|
|
274
|
-
|
|
275
|
-
# Write manifest
|
|
276
|
-
manifest_path = File.join(plastic_dir, "manifest.json")
|
|
277
|
-
write_manifest(installed, manifest_path)
|
|
278
|
-
|
|
279
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def install_codex(config, force)
|
|
283
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
284
|
-
FileUtils.mkdir_p(skills_dir)
|
|
285
|
-
|
|
286
|
-
installed = []
|
|
287
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
288
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
289
|
-
|
|
290
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
291
|
-
write_manifest(installed, manifest_path)
|
|
292
|
-
|
|
293
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def install_hermes(config, force)
|
|
297
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
298
|
-
FileUtils.mkdir_p(skills_dir)
|
|
299
|
-
|
|
300
|
-
installed = []
|
|
301
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
302
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
303
|
-
|
|
304
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
305
|
-
write_manifest(installed, manifest_path)
|
|
306
|
-
|
|
307
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# --- settings.json merge (read-modify-write, never clobber) ---
|
|
311
|
-
|
|
312
|
-
def merge_claude_hooks(settings_path)
|
|
313
|
-
settings = read_json_safe(settings_path) || {}
|
|
314
|
-
return if settings.nil?
|
|
315
|
-
|
|
316
|
-
hooks = settings["hooks"] ||= {}
|
|
317
|
-
hook_dir = File.join(Dir.home, ".claude", "hooks")
|
|
318
|
-
|
|
319
|
-
purge_stale_plastic_hooks(hooks)
|
|
320
|
-
|
|
321
|
-
plastic_hooks = {
|
|
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
|
-
},
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
plastic_hooks.each do |event, group|
|
|
351
|
-
hooks[event] ||= []
|
|
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
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
settings["statusLine"] = { "type" => "command", "command" => "#{hook_dir}/plastic-statusline" }
|
|
363
|
-
|
|
364
|
-
write_json_atomic(settings_path, settings)
|
|
365
|
-
end
|
|
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
|
|
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"
|
|
379
88
|
else
|
|
380
|
-
|
|
89
|
+
puts " \u{26a0}\u{fe0f} #{r[:agent]}: #{r[:reason]}"
|
|
381
90
|
end
|
|
382
|
-
end.compact
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
# --- Uninstall ---
|
|
387
|
-
|
|
388
|
-
def handle_uninstall(agents)
|
|
389
|
-
agents.each do |key|
|
|
390
|
-
config = agent_config(key)
|
|
391
|
-
next unless config
|
|
392
|
-
|
|
393
|
-
result = uninstall_agent(key, config)
|
|
394
|
-
if result[:success]
|
|
395
|
-
puts " \u{2705} #{config[:name]}: uninstalled (#{result[:files]} files removed)"
|
|
396
|
-
else
|
|
397
|
-
puts " \u{26a0}\u{fe0f} #{config[:name]}: #{result[:reason]}"
|
|
398
91
|
end
|
|
399
|
-
end
|
|
400
92
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def uninstall_agent(key, config)
|
|
405
|
-
unless File.directory?(config[:dir])
|
|
406
|
-
return { success: false, reason: "#{config[:dir]} not found" }
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
manifest_path = case key
|
|
410
|
-
when "claude" then File.join(config[:dir], "plastic", "manifest.json")
|
|
411
|
-
else File.join(config[:dir], "plastic-manifest.json")
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
files_removed = 0
|
|
415
|
-
|
|
416
|
-
if File.exist?(manifest_path)
|
|
417
|
-
manifest = JSON.parse(File.read(manifest_path)) rescue {}
|
|
418
|
-
(manifest["files"] || {}).each_key do |f|
|
|
419
|
-
if File.exist?(f)
|
|
420
|
-
File.delete(f)
|
|
421
|
-
files_removed += 1
|
|
422
|
-
end
|
|
423
|
-
end
|
|
424
|
-
File.delete(manifest_path)
|
|
425
|
-
files_removed += 1
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
# Clean known directories
|
|
429
|
-
dirs_to_clean = case key
|
|
430
|
-
when "claude" then [File.join(config[:dir], "plastic"), File.join(config[:dir], "skills", "plastic")]
|
|
431
|
-
else [File.join(config[:dir], "skills", "plastic")]
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
dirs_to_clean.each { |d| FileUtils.rm_rf(d) if File.directory?(d) }
|
|
435
|
-
|
|
436
|
-
# Clean hooks from settings.json (Claude Code only)
|
|
437
|
-
if key == "claude"
|
|
438
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
439
|
-
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
{ success: true, files: files_removed }
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def remove_claude_hooks(settings_path)
|
|
446
|
-
settings = read_json_safe(settings_path)
|
|
447
|
-
return unless settings && settings["hooks"]
|
|
93
|
+
installed = results.select { |r| r[:success] }
|
|
94
|
+
return unless installed.any?
|
|
448
95
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
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"
|
|
462
100
|
end
|
|
463
101
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
settings.delete("statusLine") if settings.dig("statusLine", "command").to_s.include?("plastic-")
|
|
102
|
+
def show_help
|
|
103
|
+
puts <<~HELP
|
|
467
104
|
|
|
468
|
-
|
|
469
|
-
end
|
|
105
|
+
plastic install — install or re-sync Plastic for an agent
|
|
470
106
|
|
|
471
|
-
|
|
107
|
+
Usage:
|
|
108
|
+
npx @zalom/plastic install [options]
|
|
472
109
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
476
115
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
src_path = File.join(src, entry)
|
|
482
|
-
dest_path = File.join(dest, entry)
|
|
483
|
-
if File.directory?(src_path)
|
|
484
|
-
files += copy_dir_recursive(src_path, dest_path)
|
|
485
|
-
elsif File.file?(src_path)
|
|
486
|
-
FileUtils.cp(src_path, dest_path)
|
|
487
|
-
files << dest_path
|
|
488
|
-
end
|
|
489
|
-
end
|
|
490
|
-
files
|
|
491
|
-
end
|
|
116
|
+
Channel options:
|
|
117
|
+
--latest Stable channel (default)
|
|
118
|
+
--beta Beta channel
|
|
119
|
+
--alpha Alpha channel
|
|
492
120
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
# Try JSONC stripping (remove // comments and trailing commas)
|
|
498
|
-
content = File.read(path).gsub(%r{//[^\n]*}, "").gsub(/,(\s*[}\]])/, '\1')
|
|
499
|
-
JSON.parse(content)
|
|
500
|
-
rescue
|
|
501
|
-
nil
|
|
502
|
-
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
|
|
503
125
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
File.write(tmp, content)
|
|
508
|
-
File.rename(tmp, path)
|
|
509
|
-
rescue => e
|
|
510
|
-
File.delete(tmp) if tmp && File.exist?(tmp)
|
|
511
|
-
raise e
|
|
512
|
-
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`.
|
|
513
129
|
|
|
514
|
-
|
|
515
|
-
entries = {}
|
|
516
|
-
files.each do |f|
|
|
517
|
-
entries[f] = Digest::SHA256.file(f).hexdigest if File.exist?(f)
|
|
130
|
+
HELP
|
|
518
131
|
end
|
|
519
|
-
|
|
520
|
-
data = { "version" => "1", "created" => Time.now.utc.iso8601, "files" => entries }
|
|
521
|
-
File.write(manifest_path, JSON.pretty_generate(data) + "\n")
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def write_if_missing(path, content)
|
|
525
|
-
File.write(path, content) unless File.exist?(path)
|
|
526
132
|
end
|
|
527
133
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
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
|