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.
- package/.claude-plugin/plugin.json +1 -1
- package/assets/terminal-workspace/helpers/acstack-bridge +185 -0
- package/assets/terminal-workspace/helpers/acstack-clip +88 -0
- package/assets/terminal-workspace/helpers/acstack-dash +25 -0
- package/assets/terminal-workspace/helpers/acstack-db +86 -0
- package/assets/terminal-workspace/helpers/acstack-diff +268 -0
- package/assets/terminal-workspace/helpers/acstack-edit +25 -0
- package/assets/terminal-workspace/helpers/acstack-gh +47 -0
- package/assets/terminal-workspace/helpers/acstack-herdr-renumber +93 -0
- package/assets/terminal-workspace/helpers/acstack-herdr-tab +49 -0
- package/assets/terminal-workspace/helpers/acstack-md +401 -0
- package/assets/terminal-workspace/helpers/acstack-md-lib +219 -0
- package/assets/terminal-workspace/helpers/acstack-open +217 -0
- package/assets/terminal-workspace/helpers/acstack-popup +13 -0
- package/assets/terminal-workspace/helpers/acstack-view +33 -0
- package/assets/terminal-workspace/helpers/xclip +31 -0
- package/assets/terminal-workspace/lib/apply-ghdash.sh +78 -0
- package/assets/terminal-workspace/lib/apply-herdr-numbers.sh +49 -0
- package/assets/terminal-workspace/lib/apply-herdr.sh +308 -0
- package/assets/terminal-workspace/lib/apply-tools.sh +293 -0
- package/assets/terminal-workspace/lib/apply-tuicr.sh +133 -0
- package/assets/terminal-workspace/lib/apply-yazi.sh +389 -0
- package/assets/terminal-workspace/lib/commands.sh +296 -0
- package/assets/terminal-workspace/lib/core.sh +184 -0
- package/assets/terminal-workspace/lib/helper-install.sh +35 -0
- package/assets/terminal-workspace/lib/pi-packages.sh +331 -0
- package/assets/terminal-workspace/lib/pi-state.sh +479 -0
- package/assets/terminal-workspace/lib/versions.sh +254 -0
- package/assets/terminal-workspace/setup.sh +24 -4637
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/handoff/SKILL.md +14 -14
- package/skills/handoff/launch-herdr.md +15 -4
- 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.
|