@zalom/plastic 1.0.0-alpha.9 → 1.0.0-beta.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.
- package/PLASTIC.md +163 -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 +687 -0
- package/scripts/doctor.rb +1054 -628
- 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 -529
- 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 +235 -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 +226 -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 +74 -36
- package/skills/creating-intent/evals/evals.json +56 -0
- package/skills/creating-intent/references/lifecycle.md +76 -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 +16 -7
- package/skills/linking-intents/references/zettelkasten.md +38 -0
- package/skills/managing-index/SKILL.md +5 -1
- package/skills/managing-index/references/zettelkasten-linking.md +1 -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,571 +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
|
-
|
|
112
|
-
def show_help
|
|
113
|
-
puts <<~HELP
|
|
114
|
-
|
|
115
|
-
plastic - Intent-driven idea development system
|
|
116
|
-
|
|
117
|
-
Usage:
|
|
118
|
-
npx @zalom/plastic@latest [options]
|
|
119
35
|
|
|
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
|
|
36
|
+
puts "\n\u{1f9e0} Plastic v#{version}\n\n"
|
|
128
37
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
npx @zalom/plastic
|
|
132
|
-
npx @zalom/plastic
|
|
133
|
-
|
|
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")
|
|
191
|
-
|
|
192
|
-
write_if_missing(File.join(PLASTIC_HOME, "INDEX.md"), <<~MD)
|
|
193
|
-
# Index
|
|
194
|
-
|
|
195
|
-
## Active
|
|
196
|
-
|
|
197
|
-
## Future
|
|
198
|
-
|
|
199
|
-
## Clusters
|
|
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"
|
|
200
53
|
|
|
201
|
-
|
|
54
|
+
distribute(mode)
|
|
55
|
+
bootstrap if fresh
|
|
202
56
|
|
|
203
|
-
|
|
204
|
-
MD
|
|
57
|
+
results = selected.map { |key| install_for_agent(key, force) }
|
|
205
58
|
|
|
206
|
-
|
|
207
|
-
|
|
59
|
+
action = ledger_action || (fresh ? "install" : "reinstall")
|
|
60
|
+
ledger_append(version, action)
|
|
208
61
|
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
def bootstrap_project_store(slug)
|
|
221
|
-
project_dir = File.join(PLASTIC_HOME, "projects", slug)
|
|
222
|
-
store_dir = File.join(project_dir, "store")
|
|
223
|
-
|
|
224
|
-
FileUtils.mkdir_p(store_dir)
|
|
225
|
-
|
|
226
|
-
template = File.join(PACKAGE_ROOT, "templates", "project.yml")
|
|
227
|
-
dest = File.join(project_dir, "project.yml")
|
|
228
|
-
write_if_missing(dest, File.read(template)) if File.exist?(template)
|
|
229
|
-
|
|
230
|
-
write_if_missing(File.join(project_dir, "INDEX.md"), <<~MD)
|
|
231
|
-
# Index
|
|
232
|
-
|
|
233
|
-
## Active
|
|
234
|
-
|
|
235
|
-
## Future
|
|
236
|
-
|
|
237
|
-
## Clusters
|
|
238
|
-
|
|
239
|
-
## Abandoned
|
|
240
|
-
|
|
241
|
-
## Completed
|
|
242
|
-
MD
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# --- Agent adapters ---
|
|
246
|
-
|
|
247
|
-
def install_for_agent(key, force)
|
|
248
|
-
config = agent_config(key)
|
|
249
|
-
return { agent: config[:name], success: false, reason: "Unknown agent" } unless config
|
|
250
|
-
|
|
251
|
-
unless File.directory?(config[:dir])
|
|
252
|
-
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
|
|
253
64
|
end
|
|
254
65
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
when "codex" then install_codex(config, force)
|
|
258
|
-
when "hermes" then install_hermes(config, force)
|
|
66
|
+
def installed?
|
|
67
|
+
File.exist?(File.join(plastic_home, "VERSION"))
|
|
259
68
|
end
|
|
260
|
-
end
|
|
261
69
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
plastic_dir = File.join(config[:dir], "plastic")
|
|
266
|
-
|
|
267
|
-
FileUtils.mkdir_p(hooks_dir)
|
|
268
|
-
FileUtils.mkdir_p(skills_dir)
|
|
269
|
-
FileUtils.mkdir_p(plastic_dir)
|
|
270
|
-
|
|
271
|
-
installed = []
|
|
272
|
-
|
|
273
|
-
# Copy hooks, rewriting script paths for the installed location
|
|
274
|
-
hook_source = File.join(PACKAGE_ROOT, "hooks")
|
|
275
|
-
Dir.glob(File.join(hook_source, "*")).each do |f|
|
|
276
|
-
next unless File.file?(f)
|
|
277
|
-
basename = File.basename(f)
|
|
278
|
-
next if %w[hooks.json run-hook].include?(basename)
|
|
279
|
-
dest_name = basename.start_with?("plastic-") ? basename : "plastic-#{basename}"
|
|
280
|
-
dest = File.join(hooks_dir, dest_name)
|
|
281
|
-
content = File.read(f)
|
|
282
|
-
content = content.gsub('$SCRIPT_DIR/../scripts/', '$HOME/.plastic/scripts/')
|
|
283
|
-
File.write(dest, content)
|
|
284
|
-
FileUtils.chmod(0o755, dest)
|
|
285
|
-
installed << dest
|
|
70
|
+
def installed_version
|
|
71
|
+
path = File.join(plastic_home, "VERSION")
|
|
72
|
+
File.exist?(path) ? File.read(path).strip : nil
|
|
286
73
|
end
|
|
287
74
|
|
|
288
|
-
|
|
289
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
290
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
291
|
-
|
|
292
|
-
# Write VERSION
|
|
293
|
-
version_file = File.join(plastic_dir, "VERSION")
|
|
294
|
-
File.write(version_file, "#{VERSION}\n")
|
|
295
|
-
installed << version_file
|
|
296
|
-
|
|
297
|
-
# Merge hooks into settings.json
|
|
298
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
299
|
-
merge_claude_hooks(settings_path)
|
|
300
|
-
|
|
301
|
-
# Write manifest
|
|
302
|
-
manifest_path = File.join(plastic_dir, "manifest.json")
|
|
303
|
-
write_manifest(installed, manifest_path)
|
|
304
|
-
|
|
305
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def install_codex(config, force)
|
|
309
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
310
|
-
FileUtils.mkdir_p(skills_dir)
|
|
311
|
-
|
|
312
|
-
installed = []
|
|
313
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
314
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
315
|
-
|
|
316
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
317
|
-
write_manifest(installed, manifest_path)
|
|
318
|
-
|
|
319
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def install_hermes(config, force)
|
|
323
|
-
skills_dir = File.join(config[:dir], "skills", "plastic")
|
|
324
|
-
FileUtils.mkdir_p(skills_dir)
|
|
325
|
-
|
|
326
|
-
installed = []
|
|
327
|
-
skills_source = File.join(PACKAGE_ROOT, "skills")
|
|
328
|
-
installed += copy_dir_recursive(skills_source, skills_dir) if File.directory?(skills_source)
|
|
329
|
-
|
|
330
|
-
manifest_path = File.join(config[:dir], "plastic-manifest.json")
|
|
331
|
-
write_manifest(installed, manifest_path)
|
|
332
|
-
|
|
333
|
-
{ agent: config[:name], success: true, files: installed.size }
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
# --- settings.json merge (read-modify-write, never clobber) ---
|
|
75
|
+
private
|
|
337
76
|
|
|
338
|
-
def
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
hooks = settings["hooks"] ||= {}
|
|
343
|
-
hook_dir = File.join(Dir.home, ".claude", "hooks")
|
|
344
|
-
|
|
345
|
-
purge_stale_plastic_hooks(hooks)
|
|
346
|
-
|
|
347
|
-
plastic_hooks = {
|
|
348
|
-
"SessionStart" => {
|
|
349
|
-
"matcher" => "",
|
|
350
|
-
"hooks" => [
|
|
351
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-session-start", "statusMessage" => "Loading Plastic context..." },
|
|
352
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-check-update", "statusMessage" => "" },
|
|
353
|
-
],
|
|
354
|
-
},
|
|
355
|
-
"PreCompact" => {
|
|
356
|
-
"matcher" => "",
|
|
357
|
-
"hooks" => [
|
|
358
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-savepoint", "statusMessage" => "Saving Plastic intent state..." },
|
|
359
|
-
],
|
|
360
|
-
},
|
|
361
|
-
"PostToolUse" => {
|
|
362
|
-
"matcher" => "Write|Edit",
|
|
363
|
-
"hooks" => [
|
|
364
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-gate-check", "statusMessage" => "Checking lifecycle gates..." },
|
|
365
|
-
],
|
|
366
|
-
},
|
|
367
|
-
"UserPromptSubmit" => {
|
|
368
|
-
"matcher" => "",
|
|
369
|
-
"hooks" => [
|
|
370
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-continue", "statusMessage" => "Checking for continue..." },
|
|
371
|
-
{ "type" => "command", "command" => "#{hook_dir}/plastic-future-intent-check", "statusMessage" => "Checking future intents..." },
|
|
372
|
-
],
|
|
373
|
-
},
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
plastic_hooks.each do |event, group|
|
|
377
|
-
hooks[event] ||= []
|
|
378
|
-
existing = hooks[event].find { |g| g.is_a?(Hash) && g["hooks"].is_a?(Array) && g["hooks"].any? { |h| h["command"].to_s.include?("plastic-") } }
|
|
379
|
-
|
|
380
|
-
if existing
|
|
381
|
-
existing["matcher"] = group["matcher"]
|
|
382
|
-
existing["hooks"] = group["hooks"]
|
|
383
|
-
else
|
|
384
|
-
hooks[event] << group
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
existing_status = settings["statusLine"]
|
|
389
|
-
if existing_status && !existing_status.dig("command").to_s.include?("plastic-")
|
|
390
|
-
cache_dir = File.join(PLASTIC_HOME, ".cache")
|
|
391
|
-
FileUtils.mkdir_p(cache_dir)
|
|
392
|
-
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]
|
|
393
81
|
end
|
|
394
82
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def purge_stale_plastic_hooks(hooks)
|
|
401
|
-
plastic_cmd = ->(cmd) { cmd.to_s.include?("plastic-") }
|
|
402
|
-
|
|
403
|
-
hooks.delete("statusLine")
|
|
404
|
-
|
|
405
|
-
hooks.each do |event, groups|
|
|
406
|
-
next unless groups.is_a?(Array)
|
|
407
|
-
|
|
408
|
-
hooks[event] = groups.map do |group|
|
|
409
|
-
if group.is_a?(Hash) && group["hooks"].is_a?(Array)
|
|
410
|
-
group["hooks"].reject! { |h| plastic_cmd.call(h["command"]) }
|
|
411
|
-
group unless group["hooks"].empty?
|
|
412
|
-
elsif group.is_a?(Hash) && group["command"]
|
|
413
|
-
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"
|
|
414
88
|
else
|
|
415
|
-
|
|
416
|
-
end
|
|
417
|
-
end.compact
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
# --- Uninstall ---
|
|
422
|
-
|
|
423
|
-
def handle_uninstall(agents)
|
|
424
|
-
agents.each do |key|
|
|
425
|
-
config = agent_config(key)
|
|
426
|
-
next unless config
|
|
427
|
-
|
|
428
|
-
result = uninstall_agent(key, config)
|
|
429
|
-
if result[:success]
|
|
430
|
-
puts " \u{2705} #{config[:name]}: uninstalled (#{result[:files]} files removed)"
|
|
431
|
-
else
|
|
432
|
-
puts " \u{26a0}\u{fe0f} #{config[:name]}: #{result[:reason]}"
|
|
433
|
-
end
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
puts "\n Note: ~/.plastic/ (your intent store) is preserved.\n\n"
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def uninstall_agent(key, config)
|
|
440
|
-
unless File.directory?(config[:dir])
|
|
441
|
-
return { success: false, reason: "#{config[:dir]} not found" }
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
manifest_path = case key
|
|
445
|
-
when "claude" then File.join(config[:dir], "plastic", "manifest.json")
|
|
446
|
-
else File.join(config[:dir], "plastic-manifest.json")
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
files_removed = 0
|
|
450
|
-
|
|
451
|
-
if File.exist?(manifest_path)
|
|
452
|
-
manifest = JSON.parse(File.read(manifest_path)) rescue {}
|
|
453
|
-
(manifest["files"] || {}).each_key do |f|
|
|
454
|
-
if File.exist?(f)
|
|
455
|
-
File.delete(f)
|
|
456
|
-
files_removed += 1
|
|
89
|
+
puts " \u{26a0}\u{fe0f} #{r[:agent]}: #{r[:reason]}"
|
|
457
90
|
end
|
|
458
91
|
end
|
|
459
|
-
File.delete(manifest_path)
|
|
460
|
-
files_removed += 1
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
# Clean known directories
|
|
464
|
-
dirs_to_clean = case key
|
|
465
|
-
when "claude" then [File.join(config[:dir], "plastic"), File.join(config[:dir], "skills", "plastic")]
|
|
466
|
-
else [File.join(config[:dir], "skills", "plastic")]
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
dirs_to_clean.each { |d| FileUtils.rm_rf(d) if File.directory?(d) }
|
|
470
|
-
|
|
471
|
-
# Clean hooks from settings.json (Claude Code only)
|
|
472
|
-
if key == "claude"
|
|
473
|
-
settings_path = File.join(config[:dir], "settings.json")
|
|
474
|
-
remove_claude_hooks(settings_path) if File.exist?(settings_path)
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
{ success: true, files: files_removed }
|
|
478
|
-
end
|
|
479
92
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return unless settings && settings["hooks"]
|
|
93
|
+
installed = results.select { |r| r[:success] }
|
|
94
|
+
return unless installed.any?
|
|
483
95
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if group.is_a?(Hash) && group["hooks"].is_a?(Array)
|
|
489
|
-
group["hooks"].reject! { |h| h["command"].to_s.include?("plastic-") }
|
|
490
|
-
group unless group["hooks"].empty?
|
|
491
|
-
elsif group.is_a?(Hash) && group["command"]
|
|
492
|
-
group["command"].to_s.include?("plastic-") ? nil : group
|
|
493
|
-
else
|
|
494
|
-
group
|
|
495
|
-
end
|
|
496
|
-
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"
|
|
497
100
|
end
|
|
498
101
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if settings.dig("statusLine", "command").to_s.include?("plastic-")
|
|
502
|
-
settings.delete("statusLine")
|
|
503
|
-
original_path = File.join(PLASTIC_HOME, ".cache", "original-statusline.json")
|
|
504
|
-
if File.exist?(original_path)
|
|
505
|
-
original = JSON.parse(File.read(original_path)) rescue nil
|
|
506
|
-
settings["statusLine"] = original if original
|
|
507
|
-
end
|
|
508
|
-
end
|
|
102
|
+
def show_help
|
|
103
|
+
puts <<~HELP
|
|
509
104
|
|
|
510
|
-
|
|
511
|
-
end
|
|
105
|
+
plastic install — install or re-sync Plastic for an agent
|
|
512
106
|
|
|
513
|
-
|
|
107
|
+
Usage:
|
|
108
|
+
npx @zalom/plastic install [options]
|
|
514
109
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
|
518
115
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
src_path = File.join(src, entry)
|
|
524
|
-
dest_path = File.join(dest, entry)
|
|
525
|
-
if File.directory?(src_path)
|
|
526
|
-
files += copy_dir_recursive(src_path, dest_path)
|
|
527
|
-
elsif File.file?(src_path)
|
|
528
|
-
FileUtils.cp(src_path, dest_path)
|
|
529
|
-
files << dest_path
|
|
530
|
-
end
|
|
531
|
-
end
|
|
532
|
-
files
|
|
533
|
-
end
|
|
116
|
+
Channel options:
|
|
117
|
+
--latest Stable channel (default)
|
|
118
|
+
--beta Beta channel
|
|
119
|
+
--alpha Alpha channel
|
|
534
120
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
# Try JSONC stripping (remove // comments and trailing commas)
|
|
540
|
-
content = File.read(path).gsub(%r{//[^\n]*}, "").gsub(/,(\s*[}\]])/, '\1')
|
|
541
|
-
JSON.parse(content)
|
|
542
|
-
rescue
|
|
543
|
-
nil
|
|
544
|
-
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
|
|
545
125
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
File.write(tmp, content)
|
|
550
|
-
File.rename(tmp, path)
|
|
551
|
-
rescue => e
|
|
552
|
-
File.delete(tmp) if tmp && File.exist?(tmp)
|
|
553
|
-
raise e
|
|
554
|
-
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`.
|
|
555
129
|
|
|
556
|
-
|
|
557
|
-
entries = {}
|
|
558
|
-
files.each do |f|
|
|
559
|
-
entries[f] = Digest::SHA256.file(f).hexdigest if File.exist?(f)
|
|
130
|
+
HELP
|
|
560
131
|
end
|
|
561
|
-
|
|
562
|
-
data = { "version" => "1", "created" => Time.now.utc.iso8601, "files" => entries }
|
|
563
|
-
File.write(manifest_path, JSON.pretty_generate(data) + "\n")
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
def write_if_missing(path, content)
|
|
567
|
-
File.write(path, content) unless File.exist?(path)
|
|
568
132
|
end
|
|
569
133
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
main
|
|
134
|
+
exit(Install.new(package_root: ENV["PLASTIC_PACKAGE_ROOT"] || File.expand_path("..", __dir__)).cli(ARGV)) if $PROGRAM_NAME == __FILE__
|