acstack 0.49.0 → 0.49.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.
Files changed (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/assets/terminal-workspace/helpers/acstack-bridge +185 -0
  3. package/assets/terminal-workspace/helpers/acstack-clip +88 -0
  4. package/assets/terminal-workspace/helpers/acstack-dash +25 -0
  5. package/assets/terminal-workspace/helpers/acstack-db +86 -0
  6. package/assets/terminal-workspace/helpers/acstack-diff +268 -0
  7. package/assets/terminal-workspace/helpers/acstack-edit +25 -0
  8. package/assets/terminal-workspace/helpers/acstack-gh +47 -0
  9. package/assets/terminal-workspace/helpers/acstack-herdr-renumber +93 -0
  10. package/assets/terminal-workspace/helpers/acstack-herdr-tab +49 -0
  11. package/assets/terminal-workspace/helpers/acstack-md +401 -0
  12. package/assets/terminal-workspace/helpers/acstack-md-lib +219 -0
  13. package/assets/terminal-workspace/helpers/acstack-open +217 -0
  14. package/assets/terminal-workspace/helpers/acstack-popup +13 -0
  15. package/assets/terminal-workspace/helpers/acstack-view +33 -0
  16. package/assets/terminal-workspace/helpers/xclip +31 -0
  17. package/assets/terminal-workspace/lib/apply-ghdash.sh +78 -0
  18. package/assets/terminal-workspace/lib/apply-herdr-numbers.sh +49 -0
  19. package/assets/terminal-workspace/lib/apply-herdr.sh +308 -0
  20. package/assets/terminal-workspace/lib/apply-tools.sh +293 -0
  21. package/assets/terminal-workspace/lib/apply-tuicr.sh +133 -0
  22. package/assets/terminal-workspace/lib/apply-yazi.sh +389 -0
  23. package/assets/terminal-workspace/lib/commands.sh +296 -0
  24. package/assets/terminal-workspace/lib/core.sh +184 -0
  25. package/assets/terminal-workspace/lib/helper-install.sh +35 -0
  26. package/assets/terminal-workspace/lib/pi-packages.sh +331 -0
  27. package/assets/terminal-workspace/lib/pi-state.sh +479 -0
  28. package/assets/terminal-workspace/lib/versions.sh +254 -0
  29. package/assets/terminal-workspace/setup.sh +24 -4637
  30. package/cli/package.json +1 -1
  31. package/package.json +1 -1
  32. package/skills/handoff/SKILL.md +14 -14
  33. package/skills/handoff/launch-herdr.md +15 -4
  34. package/skills/implement/SKILL.md +5 -0
@@ -0,0 +1,479 @@
1
+ pi_state() {
2
+ ACSTACK_PI_OWNED="$PI_OWNED" ACSTACK_PI_SETTINGS="$PI_SETTINGS" \
3
+ ACSTACK_PI_DIR="$PI_DIR" ACSTACK_PI_MD="$PI_MD_BEGIN|$PI_MD_END|$PI_DELEGATION" \
4
+ py "$@" <<'PY'
5
+ import json, os, re, sys
6
+
7
+ OWNED = os.environ["ACSTACK_PI_OWNED"]
8
+ MISSING = object()
9
+
10
+ def load_owned():
11
+ try:
12
+ d = json.load(open(OWNED))
13
+ except (OSError, ValueError):
14
+ d = {}
15
+ return d if isinstance(d, dict) else {}
16
+
17
+ def save_owned(d):
18
+ d = {k: v for k, v in d.items() if v}
19
+ if not d:
20
+ if os.path.exists(OWNED):
21
+ os.remove(OWNED)
22
+ return
23
+ os.makedirs(os.path.dirname(OWNED), exist_ok=True)
24
+ json.dump(d, open(OWNED, "w"), indent=2)
25
+
26
+ def load_json(path):
27
+ if not os.path.exists(path):
28
+ return {}
29
+ d = json.load(open(path))
30
+ if not isinstance(d, dict):
31
+ raise ValueError("not an object")
32
+ return d
33
+
34
+ def identity(src):
35
+ # pi names a package by its npm name or its repository without the ref, so
36
+ # two pins of one repository are one package.
37
+ if isinstance(src, dict):
38
+ src = src.get("source", "")
39
+ if src.startswith("npm:"):
40
+ spec = src[4:]
41
+ at = spec.rfind("@")
42
+ return "npm:" + (spec[:at] if at > 0 else spec)
43
+ m = re.match(r"^(?:git:|https?://|ssh://)(?:git@)?(.+?)(?:\.git)?(?:@[^/@]+)?$", src)
44
+ if m:
45
+ return "git:" + m.group(1).replace(":", "/")
46
+ return src
47
+
48
+ cmd, args = sys.argv[1], sys.argv[2:]
49
+
50
+ if cmd == "pkg":
51
+ try:
52
+ pkgs = load_json(os.environ["ACSTACK_PI_SETTINGS"]).get("packages") or []
53
+ except ValueError:
54
+ raise SystemExit(1)
55
+ for p in pkgs:
56
+ if identity(p) == args[0]:
57
+ print(p.get("source") if isinstance(p, dict) else p)
58
+ raise SystemExit
59
+ raise SystemExit(1)
60
+
61
+ if cmd in ("own", "disown", "owns"):
62
+ kind, name = args
63
+ slot = {"pkg": "packages", "file": "files"}[kind]
64
+ d = load_owned()
65
+ have = d.get(slot, [])
66
+ if cmd == "owns":
67
+ raise SystemExit(0 if name in have else 1)
68
+ have = [x for x in have if x != name] + ([name] if cmd == "own" else [])
69
+ d[slot] = have
70
+ save_owned(d)
71
+ raise SystemExit
72
+
73
+ if cmd in ("pin", "pin-get", "pin-forget"):
74
+ d = load_owned()
75
+ pins = d.setdefault("pins", {})
76
+ if cmd == "pin":
77
+ ident, was, ours = args
78
+ pins[ident] = {"prior": pins.get(ident, {}).get("prior", was), "ours": ours}
79
+ elif cmd == "pin-forget":
80
+ pins.pop(args[0], None)
81
+ else:
82
+ rec = pins.get(args[0])
83
+ if not rec:
84
+ raise SystemExit(1)
85
+ print("%s\t%s" % (rec["prior"], rec["ours"]))
86
+ raise SystemExit
87
+ save_owned(d)
88
+ raise SystemExit
89
+
90
+ if cmd == "parses":
91
+ try:
92
+ load_json(args[0])
93
+ except ValueError:
94
+ raise SystemExit(1)
95
+ raise SystemExit
96
+
97
+ if cmd == "merge":
98
+ path, slot, want = args[0], args[1], json.loads(args[2])
99
+ try:
100
+ d = load_json(path)
101
+ except ValueError:
102
+ print(" pi: %s does not parse, so it was left alone" % path)
103
+ raise SystemExit(3)
104
+ owned = load_owned()
105
+ mine = owned.setdefault(slot, {})
106
+ # What a claimed key held before apply first changed it, so release can
107
+ # put the user's value back rather than delete it.
108
+ prior = owned.setdefault(slot + "_prior", {})
109
+ wrote, dropped = [], []
110
+ for key, value in want.items():
111
+ parents, leaf = ([], key) if slot == "keybindings" else (key.split(".")[:-1], key.split(".")[-1])
112
+ node = d
113
+ for p in parents:
114
+ if node.get(p, MISSING) is MISSING and value is not None:
115
+ node[p] = {}
116
+ node = node.get(p)
117
+ if not isinstance(node, dict):
118
+ break
119
+ if not isinstance(node, dict):
120
+ # A parent the user set to something other than an object, or
121
+ # deleted. Writing through it would replace their value, so
122
+ # concede this key. On release there is nothing left of ours.
123
+ if value is not None:
124
+ print(" pi: %s in %s is not an object, %s left alone" % (".".join(parents), path, key))
125
+ else:
126
+ mine.pop(key, None)
127
+ prior.pop(key, None)
128
+ continue
129
+ have = node.get(leaf, MISSING)
130
+ if value is None:
131
+ # Released. Delete only a value this setup wrote and nobody has
132
+ # changed since; anything else is the user's.
133
+ if key in mine and have == mine[key]:
134
+ if key in prior:
135
+ node[leaf] = prior[key]
136
+ else:
137
+ del node[leaf]
138
+ cur = d
139
+ for p in parents[:-1]:
140
+ cur = cur[p]
141
+ if parents and not node:
142
+ del cur[parents[-1]]
143
+ dropped.append(key)
144
+ mine.pop(key, None)
145
+ prior.pop(key, None)
146
+ elif have != value:
147
+ if key not in mine and have is not MISSING:
148
+ prior[key] = have
149
+ node[leaf] = value
150
+ mine[key] = value
151
+ wrote.append(key)
152
+ save_owned(owned)
153
+ if wrote or dropped:
154
+ if d or not os.path.exists(path):
155
+ os.makedirs(os.path.dirname(path), exist_ok=True)
156
+ with open(path, "w") as f:
157
+ # pi writes its own settings unescaped, so a value in any
158
+ # script round-trips as the user typed it.
159
+ json.dump(d, f, indent=2, ensure_ascii=False)
160
+ f.write("\n")
161
+ else:
162
+ # Everything in it was ours, and an empty file is no setting at all.
163
+ os.remove(path)
164
+ if wrote:
165
+ print(" pi: set %s in %s" % (", ".join(wrote), path))
166
+ if dropped:
167
+ print(" pi: released %s in %s" % (", ".join(dropped), path))
168
+ raise SystemExit
169
+
170
+ if cmd == "md":
171
+ begin, end, line = os.environ["ACSTACK_PI_MD"].split("|")
172
+ agent_dir = os.environ["ACSTACK_PI_DIR"]
173
+ # pi reads one context file from its agent directory, the first of these
174
+ # that exists. A block in any other one would never reach the model.
175
+ names = ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]
176
+ paths = [os.path.join(agent_dir, n) for n in names]
177
+ target = next((p for p in paths if os.path.isfile(p)), paths[1])
178
+ block = re.compile(re.escape(begin) + r".*?" + re.escape(end) + r"\n?", re.S)
179
+ # The blank line write puts before the block goes with it, so stripping
180
+ # hands back the user's text exactly as it was.
181
+ gap = re.compile(r"\n?" + block.pattern, re.S)
182
+ state = load_owned()
183
+ owned_md = state.get("md", {})
184
+ for p in paths:
185
+ if not os.path.isfile(p) or (args[0] == "write" and p == target):
186
+ continue
187
+ t = open(p).read()
188
+ # write records the exact separator it added. Removing that and no
189
+ # more hands back the user's file byte for byte, final newline or not.
190
+ sep = owned_md.get("sep")
191
+ cut = re.compile(re.escape(sep) + block.pattern, re.S) if sep is not None else gap
192
+ stripped = cut.sub("", t)
193
+ if stripped != t:
194
+ if stripped.strip():
195
+ open(p, "w").write(stripped)
196
+ else:
197
+ os.remove(p)
198
+ print(" pi: delegation line removed from %s" % p)
199
+ if args[0] == "strip":
200
+ state.pop("md", None)
201
+ save_owned(state)
202
+ if args[0] == "write":
203
+ t = open(target).read() if os.path.isfile(target) else ""
204
+ mine = "%s\n%s\n%s\n" % (begin, line, end)
205
+ if block.search(t):
206
+ new = block.sub(lambda m: mine, t, count=1)
207
+ else:
208
+ sep = ("\n" if t and not t.endswith("\n") else "") + ("\n" if t else "")
209
+ new = t + sep + mine
210
+ state["md"] = {"sep": sep}
211
+ save_owned(state)
212
+ if new != t:
213
+ os.makedirs(agent_dir, exist_ok=True)
214
+ open(target, "w").write(new)
215
+ print(" pi: delegation line written to %s" % target)
216
+ raise SystemExit
217
+ PY
218
+ }
219
+
220
+ # The four personal settings, from the config, as the JSON merge reads them.
221
+ # An empty value releases the key rather than writing an empty one.
222
+ pi_settings_wanted() {
223
+ ACSTACK_CFG="$CFG" py <<'PY'
224
+ import json, os, sys
225
+ import yaml
226
+ p = os.environ["ACSTACK_CFG"]
227
+ c = (yaml.safe_load(open(p)) or {}) if os.path.isfile(p) else {}
228
+ s = ((c.get("pi") or {}).get("settings") or {})
229
+ # config key -> (pi's dotted key, the default config.example.yaml ships, the
230
+ # values pi accepts). YAML reads a bare off as false, so mermaid maps it back.
231
+ KEYS = {"hide_thinking_block": ("hideThinkingBlock", True, (True, False)),
232
+ "mermaid": ("markdown.mermaid", "streaming", ("off", "final", "streaming")),
233
+ "install_telemetry": ("enableInstallTelemetry", False, (True, False)),
234
+ "theme": ("theme", "alex-mid-grey", None)}
235
+ out = {}
236
+ for k, (key, default, allowed) in KEYS.items():
237
+ v = s.get(k, default)
238
+ if v in ("", None):
239
+ out[key] = None
240
+ continue
241
+ if k == "mermaid" and v is False:
242
+ v = "off"
243
+ ok = (v in allowed and type(v) is type(allowed[0])) if allowed else isinstance(v, str)
244
+ if not ok:
245
+ # Left out entirely, so pi keeps whatever it holds now.
246
+ print(" pi: settings.%s cannot be %r, left alone" % (k, v), file=sys.stderr)
247
+ continue
248
+ out[key] = v
249
+ print(json.dumps(out))
250
+ PY
251
+ }
252
+
253
+ pi_zentui_wanted() {
254
+ cat <<'JSON'
255
+ {
256
+ "components.editor.enabled": true,
257
+ "components.editor.style": "accent-rail",
258
+ "components.editor.styles.accent-rail.transparent": false,
259
+ "components.userMessages.enabled": false,
260
+ "components.footer.style": "starship",
261
+ "components.footer.modelLabel": "id",
262
+ "components.footer.styles.starship.segments.modelInfo": true,
263
+ "components.footer.styles.starship.extensionStatuses.defaultPlacement": "off",
264
+ "components.extensionStatuses.defaultVisibility": "hide",
265
+ "components.extensionStatuses.visibility.pi-lens": "hide",
266
+ "components.extensionStatuses.visibility.pi-lens-lsp": "hide",
267
+ "components.extensionStatuses.visibility.ponytail": "hide",
268
+ "components.footer.styles.starship.responsive": true,
269
+ "components.footer.styles.starship.format": "$cwd( in $session_name)( on $git_branch)( $git_status)( via $runtime)$fill$model($sep$provider)($sep$thinkingLevel)($sep$context)( $auto_compaction)($sep$tokens)( $cache_read)( $cache_write)($sep$cost)( $subscription)",
270
+ "components.footer.styles.starship.compactFormat": "$cwd$wrap(in $session_name)$wrap(on $git_branch) $git_status$fill$model$wrap_sep($provider)$wrap_sep($thinkingLevel)$wrap_sep$context$wrap_sep$tokens"
271
+ }
272
+ JSON
273
+ }
274
+
275
+ pi_zentui_release() {
276
+ cat <<'JSON'
277
+ {
278
+ "components.editor.enabled": null,
279
+ "components.editor.style": null,
280
+ "components.editor.styles.accent-rail.transparent": null,
281
+ "components.userMessages.enabled": null,
282
+ "components.footer.style": null,
283
+ "components.footer.modelLabel": null,
284
+ "components.footer.styles.starship.segments.modelInfo": null,
285
+ "components.footer.styles.starship.extensionStatuses.defaultPlacement": null,
286
+ "components.extensionStatuses.defaultVisibility": null,
287
+ "components.extensionStatuses.visibility.pi-lens": null,
288
+ "components.extensionStatuses.visibility.pi-lens-lsp": null,
289
+ "components.extensionStatuses.visibility.ponytail": null,
290
+ "components.footer.styles.starship.responsive": null,
291
+ "components.footer.styles.starship.format": null,
292
+ "components.footer.styles.starship.compactFormat": null
293
+ }
294
+ JSON
295
+ }
296
+
297
+ pi_write_theme() {
298
+ if [ -e "$PI_THEME" ] && ! pi_state owns file "$PI_THEME"; then
299
+ say "pi: $PI_THEME already exists, so the ACStack theme was left alone"
300
+ return 0
301
+ fi
302
+ ACSTACK_PI_THEME="$PI_THEME" py <<'PY'
303
+ import json, os
304
+
305
+ target = os.environ["ACSTACK_PI_THEME"]
306
+ theme = {
307
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
308
+ "name": "alex-mid-grey",
309
+ "vars": {
310
+ "surfaceMuted": "#21262D",
311
+ "border": "#30363D",
312
+ "borderSubtle": "#21262D",
313
+ "text": "#C9D1D9",
314
+ "textBright": "#E6EDF3",
315
+ "muted": "#8B949E",
316
+ "dim": "#6E7681",
317
+ "toolStatus": "#59616B",
318
+ "blue": "#58A6FF",
319
+ "blueSoft": "#79C0FF",
320
+ "inlineCode": "#D2A8FF",
321
+ "green": "#3FB950",
322
+ "red": "#F85149",
323
+ "yellow": "#E0A052",
324
+ "orange": "#FFA657",
325
+ "purple": "#D2A8FF",
326
+ "toolAccent": "#95A7FF",
327
+ "toolName": "#8B85D6",
328
+ "toolText": "#98A1AC",
329
+ "pink": "#FF7B72",
330
+ "userPromptBg": "#24272e",
331
+ },
332
+ "colors": {
333
+ "accent": "toolAccent",
334
+ "border": "border",
335
+ "borderAccent": "toolStatus",
336
+ "borderMuted": "borderSubtle",
337
+ "success": "green",
338
+ "error": "red",
339
+ "warning": "yellow",
340
+ "muted": "muted",
341
+ "dim": "dim",
342
+ "text": "text",
343
+ "thinkingText": "toolText",
344
+ "selectedBg": "surfaceMuted",
345
+ "searchMatchBg": "#3B2E10",
346
+ "searchMatchText": "textBright",
347
+ "userMessageBg": "userPromptBg",
348
+ "userMessageText": "textBright",
349
+ "customMessageBg": "",
350
+ "customMessageText": "text",
351
+ "customMessageLabel": "muted",
352
+ "toolPendingBg": "",
353
+ "toolSuccessBg": "",
354
+ "toolErrorBg": "#211414",
355
+ "toolTitle": "toolName",
356
+ "toolOutput": "toolText",
357
+ "scrollbarTrack": "borderSubtle",
358
+ "scrollbarThumb": "border",
359
+ "mdHeading": "textBright",
360
+ "mdLink": "blue",
361
+ "mdLinkUrl": "dim",
362
+ "mdCode": "inlineCode",
363
+ "mdCodeBlock": "text",
364
+ "mdCodeBlockBorder": "#0D1117",
365
+ "mdQuote": "toolText",
366
+ "mdQuoteBorder": "border",
367
+ "mdHr": "borderSubtle",
368
+ "mdListBullet": "muted",
369
+ "toolDiffAdded": "#45B85A",
370
+ "toolDiffRemoved": "#E45D57",
371
+ "toolDiffContext": "dim",
372
+ "syntaxComment": "dim",
373
+ "syntaxKeyword": "pink",
374
+ "syntaxFunction": "purple",
375
+ "syntaxVariable": "text",
376
+ "syntaxString": "blueSoft",
377
+ "syntaxNumber": "orange",
378
+ "syntaxType": "orange",
379
+ "syntaxOperator": "muted",
380
+ "syntaxPunctuation": "muted",
381
+ "thinkingOff": "borderSubtle",
382
+ "thinkingMinimal": "border",
383
+ "thinkingLow": "dim",
384
+ "thinkingMedium": "muted",
385
+ "thinkingHigh": "blue",
386
+ "thinkingXhigh": "blueSoft",
387
+ "thinkingMax": "#9FCFF8",
388
+ "bashMode": "green",
389
+ },
390
+ "export": {"pageBg": "#0D1117", "cardBg": "#161B22", "infoBg": "#1C2128"},
391
+ }
392
+ os.makedirs(os.path.dirname(target), exist_ok=True)
393
+ json.dump(theme, open(target, "w"), indent=2, ensure_ascii=False)
394
+ open(target, "a").write("\n")
395
+ PY
396
+ pi_state own file "$PI_THEME"
397
+ say "pi: ACStack theme written to $PI_THEME"
398
+ }
399
+
400
+ # npm installs pi into its global bin, which a fresh shell may not have on
401
+ # PATH. Asking npm costs a process, so only when pi is not already found.
402
+ find_pi() {
403
+ have pi || ! have npm || PATH="$PATH:$(npm prefix -g 2>/dev/null)/bin"
404
+ }
405
+
406
+
407
+ # Pi's bundled TUI does not always receive Windows Terminal paste as plain
408
+ # text. Through Herdr and ConPTY it can arrive as Win32 input records such as
409
+ # ESC[13;28;13;1;0;1_. Patch the installed bundle after install or update.
410
+ patch_pi_win32_paste() {
411
+ have pi || return 0
412
+ local pi_bin pi_real root chunks
413
+ pi_bin=$(command -v pi) || return 0
414
+ pi_real=$(readlink -f "$pi_bin" 2>/dev/null || printf '%s' "$pi_bin")
415
+ case "$pi_real" in
416
+ */dist/bundle/cli.js) root=${pi_real%/dist/bundle/cli.js} ;;
417
+ *)
418
+ root="${pi_real%/bin/pi}/lib/node_modules/@earendil-works/pi-coding-agent"
419
+ if [ ! -d "$root/dist/bundle/chunks" ]; then
420
+ have npm || { say "pi: npm not on PATH, so Win32 paste patch was skipped"; return 0; }
421
+ root="$(npm root -g 2>/dev/null)/@earendil-works/pi-coding-agent"
422
+ fi
423
+ ;;
424
+ esac
425
+ chunks="$root/dist/bundle/chunks"
426
+ [ -d "$chunks" ] || { say "pi: bundle chunks not found, so Win32 paste patch was skipped"; return 0; }
427
+ SPECHUB_PI_CHUNKS="$chunks" py <<'PI_PATCH_PY'
428
+ import os, pathlib
429
+ chunks = pathlib.Path(os.environ["SPECHUB_PI_CHUNKS"])
430
+ target = None
431
+ for p in chunks.glob("*.js"):
432
+ try:
433
+ s = p.read_text()
434
+ except (OSError, UnicodeDecodeError):
435
+ continue
436
+ if "function decodePrintableKey(data)" in s and "handlePaste(pastedText)" in s:
437
+ target = p
438
+ break
439
+ if target is None:
440
+ print(" pi: TUI bundle not found, so Win32 paste patch was skipped")
441
+ raise SystemExit
442
+ s = target.read_text()
443
+ changed = False
444
+ repls = [
445
+ ('function isKeyRelease(data){return data.includes("\\x1B[200~")?!1:!!(data.includes(":3u")||data.includes(":3~")||data.includes(":3A")||data.includes(":3B")||data.includes(":3C")||data.includes(":3D")||data.includes(":3H")||data.includes(":3F"))}',
446
+ 'function isKeyRelease(data){let win32=parseWin32InputRecord(data);if(win32)return!win32.keyDown;return data.includes("\\x1B[200~")?!1:!!(data.includes(":3u")||data.includes(":3~")||data.includes(":3A")||data.includes(":3B")||data.includes(":3C")||data.includes(":3D")||data.includes(":3H")||data.includes(":3F"))}'),
447
+ ('function parseModifyOtherKeysSequence(data){let match=data.match(/^\\x1b\\[27;(\\d+);(\\d+)~$/);if(!match)return null;let modValue=parseInt(match[1],10);return{codepoint:parseInt(match[2],10),modifier:modValue-1}}',
448
+ 'function parseModifyOtherKeysSequence(data){let match=data.match(/^\\x1b\\[27;(\\d+);(\\d+)~$/);if(!match)return null;let modValue=parseInt(match[1],10);return{codepoint:parseInt(match[2],10),modifier:modValue-1}}function parseWin32InputRecord(data){let match=data.match(/^\\x1b\\[(\\d+);(\\d+);(-?\\d+);([01]);(\\d+);(\\d+)_$/);if(!match)return null;let virtualKey=Number.parseInt(match[1],10),scanCode=Number.parseInt(match[2],10),codepoint=Number.parseInt(match[3],10),keyDown=match[4]==="1",controlState=Number.parseInt(match[5],10),modifier=0;if((controlState&16)!==0)modifier|=MODIFIERS.shift;if((controlState&(1|2))!==0)modifier|=MODIFIERS.alt;if((controlState&(4|8))!==0)modifier|=MODIFIERS.ctrl;if((controlState&32)!==0)modifier|=128;if((controlState&128)!==0)modifier|=64;return{virtualKey,scanCode,codepoint,keyDown,modifier}}'),
449
+ ('function parseKey(data){let kitty=parseKittySequence(data);',
450
+ 'function parseKey(data){let win32=parseWin32InputRecord(data);if(win32){_lastEventType=win32.keyDown?"press":"release";if(!win32.keyDown)return;let codepoint=win32.codepoint;if(win32.virtualKey===46)codepoint=FUNCTIONAL_CODEPOINTS.delete;else if(win32.virtualKey===45)codepoint=FUNCTIONAL_CODEPOINTS.insert;else if(win32.virtualKey===33)codepoint=FUNCTIONAL_CODEPOINTS.pageUp;else if(win32.virtualKey===34)codepoint=FUNCTIONAL_CODEPOINTS.pageDown;else if(win32.virtualKey===35)codepoint=FUNCTIONAL_CODEPOINTS.end;else if(win32.virtualKey===36)codepoint=FUNCTIONAL_CODEPOINTS.home;else if(win32.virtualKey===37)codepoint=ARROW_CODEPOINTS.left;else if(win32.virtualKey===38)codepoint=ARROW_CODEPOINTS.up;else if(win32.virtualKey===39)codepoint=ARROW_CODEPOINTS.right;else if(win32.virtualKey===40)codepoint=ARROW_CODEPOINTS.down;else if(win32.virtualKey===8)codepoint=CODEPOINTS.backspace;else if(win32.virtualKey===9)codepoint=CODEPOINTS.tab;else if(win32.virtualKey===13)codepoint=CODEPOINTS.enter;else if(win32.virtualKey===27)codepoint=CODEPOINTS.escape;return formatParsedKey(codepoint,win32.modifier)}let kitty=parseKittySequence(data);'),
451
+ ('function decodeModifyOtherKeysPrintable(data){let parsed=parseModifyOtherKeysSequence(data);if(!(!parsed||(parsed.modifier&~LOCK_MASK&~MODIFIERS.shift)!==0)&&!(!Number.isFinite(parsed.codepoint)||parsed.codepoint<32))try{return String.fromCodePoint(parsed.codepoint)}catch{return}}function decodePrintableKey(data){return decodeKittyPrintable(data)??decodeModifyOtherKeysPrintable(data)}',
452
+ 'function decodeModifyOtherKeysPrintable(data){let parsed=parseModifyOtherKeysSequence(data);if(!(!parsed||(parsed.modifier&~LOCK_MASK&~MODIFIERS.shift)!==0)&&!(!Number.isFinite(parsed.codepoint)||parsed.codepoint<32))try{return String.fromCodePoint(parsed.codepoint)}catch{return}}function decodeWin32Printable(data){let parsed=parseWin32InputRecord(data);if(!parsed||!parsed.keyDown)return;let modifier=parsed.modifier&~LOCK_MASK;if((modifier&(MODIFIERS.ctrl|MODIFIERS.alt|MODIFIERS.super))!==0)return;if(!Number.isFinite(parsed.codepoint)||parsed.codepoint<32)return;try{return String.fromCodePoint(parsed.codepoint)}catch{return}}function decodePrintableKey(data){return decodeKittyPrintable(data)??decodeModifyOtherKeysPrintable(data)??decodeWin32Printable(data)}'),
453
+ ('let decodedText=pastedText.replace(/\\x1b\\[(\\d+);5u/g,(match,code)=>{let cp=Number(code);return cp>=97&&cp<=122?String.fromCharCode(cp-96):cp>=65&&cp<=90?String.fromCharCode(cp-64):match}),filteredText=this.normalizeText(decodedText).split("").filter(char=>char===`\n`||char.charCodeAt(0)>=32).join("");',
454
+ 'let decodedText=pastedText.replace(/\\x1b\\[(\\d+);(\\d+);(-?\\d+);([01]);(\\d+);(\\d+)_/g,(match,vk,sc,uc,kd)=>{if(kd!=="1")return "";let cp=Number(uc),key=Number(vk);if(key===13||cp===13)return `\n`;if(cp>=32)try{return String.fromCodePoint(cp)}catch{return ""}return ""}).replace(/\\x1b\\[(\\d+);5u/g,(match,code)=>{let cp=Number(code);return cp>=97&&cp<=122?String.fromCharCode(cp-96):cp>=65&&cp<=90?String.fromCharCode(cp-64):match}),filteredText=this.normalizeText(decodedText).split("").filter(char=>char===`\n`||char.charCodeAt(0)>=32).join("");')]
455
+ missing = []
456
+ for old, new in repls:
457
+ if new in s:
458
+ continue
459
+ if old not in s:
460
+ missing.append(old[:80])
461
+ continue
462
+ s = s.replace(old, new, 1)
463
+ changed = True
464
+ if missing:
465
+ print(" pi: Win32 paste patch did not match this Pi bundle")
466
+ raise SystemExit
467
+ if changed:
468
+ try:
469
+ target.write_text(s)
470
+ except OSError as exc:
471
+ print(f" pi: could not patch {target} ({exc})")
472
+ raise SystemExit
473
+ print(" pi: Win32 paste patch applied")
474
+ else:
475
+ print(" pi: Win32 paste patch already applied")
476
+ PI_PATCH_PY
477
+ }
478
+
479
+ # The ACStack version running this script, which is the one pi should pin.