@zalom/plastic 1.0.0-alpha.18 → 1.0.0-alpha.19
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/README.md +48 -6
- package/bin/plastic.js +57 -0
- package/bin/test +28 -0
- package/hooks/bash-gate +3 -0
- package/hooks/hooks.json +10 -0
- package/package.json +2 -2
- package/scripts/doctor.rb +741 -729
- package/scripts/hook-bash-gate +53 -0
- package/scripts/hook-session-start +42 -34
- package/scripts/install.rb +90 -695
- package/scripts/lib/bridge.rb +103 -0
- package/scripts/lib/installer_core.rb +760 -0
- package/scripts/uninstall.rb +53 -0
- package/scripts/update.rb +142 -0
- package/scripts/versions.rb +141 -0
- package/skills/brainstorming/SKILL.md +1 -1
- package/skills/executing-plan/SKILL.md +2 -2
- package/skills/install/SKILL.md +5 -5
- package/skills/uninstall/SKILL.md +2 -2
- package/skills/update/SKILL.md +35 -91
- package/skills/versions/SKILL.md +65 -0
- package/templates/outcome.md +13 -0
- package/templates/spec.md +25 -0
- package/bin/install.js +0 -29
package/scripts/lib/bridge.rb
CHANGED
|
@@ -215,6 +215,109 @@ module Bridge
|
|
|
215
215
|
"(blocked edit: #{file_abs})"
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
+
# --- Bash-edit gate (intent 27a) ---
|
|
219
|
+
|
|
220
|
+
# Extract the set of file paths a Bash command writes to. Conservative by
|
|
221
|
+
# design: it is acceptable to miss exotic forms, but it must NOT flag reads
|
|
222
|
+
# or /dev/null. Returns an Array of path strings (possibly relative).
|
|
223
|
+
#
|
|
224
|
+
# Covered write vectors: redirection (>, >>, including heredoc `cat > f <<EOF`),
|
|
225
|
+
# tee / tee -a, sed -i / sed -i.bak, cp/mv (last non-flag arg), dd of=.
|
|
226
|
+
def self.bash_write_targets(command)
|
|
227
|
+
return [] unless command.is_a?(String)
|
|
228
|
+
|
|
229
|
+
targets = []
|
|
230
|
+
targets.concat(bash_redirect_targets(command))
|
|
231
|
+
# Split on command separators for per-segment utility parsing.
|
|
232
|
+
command.split(/[;\n]|&&|\|\||\|/).each do |segment|
|
|
233
|
+
targets.concat(bash_utility_targets(segment))
|
|
234
|
+
end
|
|
235
|
+
targets.uniq
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Redirections: `> path` / `>> path`, but not fd dups (`2>&1`) or /dev/null.
|
|
239
|
+
# A leading digit (fd number) before > is fine; `>&` is a dup and excluded.
|
|
240
|
+
def self.bash_redirect_targets(command)
|
|
241
|
+
targets = []
|
|
242
|
+
# Match optional leading fd digits, then > or >>, not followed by & , then path.
|
|
243
|
+
command.scan(/\d*>>?(?!&)\s*([^\s;|&<>]+)/) do |m|
|
|
244
|
+
path = m[0]
|
|
245
|
+
next if path.nil? || path.empty?
|
|
246
|
+
next if dev_null?(path)
|
|
247
|
+
targets << path
|
|
248
|
+
end
|
|
249
|
+
targets
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.bash_utility_targets(segment)
|
|
253
|
+
tokens = segment.strip.split(/\s+/)
|
|
254
|
+
return [] if tokens.empty?
|
|
255
|
+
|
|
256
|
+
# Find the utility name, skipping env-style assignments.
|
|
257
|
+
idx = 0
|
|
258
|
+
idx += 1 while tokens[idx] && tokens[idx].include?("=") && tokens[idx] !~ /^-/ && !tokens[idx].start_with?("of=")
|
|
259
|
+
util = File.basename(tokens[idx].to_s)
|
|
260
|
+
args = tokens[(idx + 1)..] || []
|
|
261
|
+
|
|
262
|
+
case util
|
|
263
|
+
when "tee"
|
|
264
|
+
tee_targets(args)
|
|
265
|
+
when "sed"
|
|
266
|
+
sed_targets(args)
|
|
267
|
+
when "cp", "mv"
|
|
268
|
+
copy_move_targets(args)
|
|
269
|
+
when "dd"
|
|
270
|
+
dd_targets(tokens)
|
|
271
|
+
else
|
|
272
|
+
[]
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.tee_targets(args)
|
|
277
|
+
args.reject { |a| a.start_with?("-") || dev_null?(a) }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def self.sed_targets(args)
|
|
281
|
+
# In-place only: -i or -i.bak (suffix attached). Otherwise sed reads.
|
|
282
|
+
inplace = args.any? { |a| a == "-i" || a.start_with?("-i") }
|
|
283
|
+
return [] unless inplace
|
|
284
|
+
files = args.reject { |a| a.start_with?("-") }
|
|
285
|
+
# sed args: script then file(s). First non-flag is the script expression
|
|
286
|
+
# unless an -e/-f was used; conservatively treat the LAST non-flag as file.
|
|
287
|
+
files.empty? ? [] : [files.last].reject { |f| dev_null?(f) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def self.copy_move_targets(args)
|
|
291
|
+
files = args.reject { |a| a.start_with?("-") }
|
|
292
|
+
return [] if files.length < 2
|
|
293
|
+
dest = files.last
|
|
294
|
+
dev_null?(dest) ? [] : [dest]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.dd_targets(tokens)
|
|
298
|
+
tokens.each_with_object([]) do |t, acc|
|
|
299
|
+
next unless t.start_with?("of=")
|
|
300
|
+
path = t.sub("of=", "")
|
|
301
|
+
acc << path unless path.empty? || dev_null?(path)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def self.dev_null?(path)
|
|
306
|
+
path == "/dev/null" || path.start_with?("/dev/")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Decide whether a Bash command should be blocked under the auto-mode code
|
|
310
|
+
# gate. Resolves each write target against cwd and applies the SAME policy as
|
|
311
|
+
# code_gate_decision. Returns the first block reason, or nil to allow.
|
|
312
|
+
def self.bash_gate_decision(bridge_data, command, cwd:, home: Dir.home)
|
|
313
|
+
bash_write_targets(command).each do |target|
|
|
314
|
+
abs = File.absolute_path?(target) ? target : File.join(cwd, target)
|
|
315
|
+
reason = code_gate_decision(bridge_data, abs, home: home)
|
|
316
|
+
return reason if reason
|
|
317
|
+
end
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
|
|
218
321
|
def self.deep_merge(base, overlay)
|
|
219
322
|
result = base.dup
|
|
220
323
|
overlay.each do |key, value|
|