superpowers-manager 0.2.0 → 0.4.0
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 +340 -66
- package/dist/adapter-result.js +204 -0
- package/dist/adapter.js +690 -0
- package/dist/atomic.js +142 -0
- package/dist/cli-arguments.js +32 -0
- package/dist/cli.js +313 -0
- package/dist/codex-harness.js +271 -0
- package/dist/codex-json.js +89 -0
- package/dist/codex-prepare.js +199 -0
- package/dist/codex-presentation.js +259 -0
- package/dist/codex-state.js +78 -0
- package/dist/commands/context.js +1 -0
- package/dist/commands/install.js +291 -0
- package/dist/commands/pin.js +79 -0
- package/dist/commands/prepare.js +420 -0
- package/dist/commands/probe.js +257 -0
- package/dist/commands/track-latest.js +44 -0
- package/dist/commands/uninstall.js +217 -0
- package/dist/commands/unpin.js +95 -0
- package/dist/commands/update.js +79 -0
- package/dist/domain/fingerprint.js +4 -0
- package/dist/domain/refs.js +37 -0
- package/dist/effective-selection.js +123 -0
- package/dist/generated-plugin.js +748 -0
- package/dist/git.js +38 -0
- package/dist/harness.js +1 -0
- package/dist/hooks.js +290 -0
- package/dist/lifecycle.js +135 -0
- package/dist/manifest-overlay.js +125 -0
- package/dist/provenance.js +111 -0
- package/dist/python-json-format.js +83 -0
- package/dist/python-json.js +29 -0
- package/dist/python-text.js +74 -0
- package/dist/safe-path.js +137 -0
- package/dist/safety-error.js +10 -0
- package/dist/selection-state-cli.js +85 -0
- package/dist/selection-store.js +172 -0
- package/dist/selection.js +200 -0
- package/dist/status.js +24 -0
- package/dist/strict-json.js +248 -0
- package/dist/upstream-cli.js +110 -0
- package/dist/upstream-version.js +40 -0
- package/dist/upstream.js +253 -0
- package/dist/validate-generated-plugin-cli.js +118 -0
- package/dist/validator.js +317 -0
- package/dist/workspace.js +139 -0
- package/package.json +25 -4
- package/bin/superpowers-manager.js +0 -290
- package/scripts/adapters/codex/adapter +0 -603
- package/scripts/adapters/codex/lib.sh +0 -449
- package/scripts/adapters/codex/validate-generated-plugin.py +0 -314
- package/scripts/core/adapter.sh +0 -103
- package/scripts/core/common.sh +0 -59
- package/scripts/core/lifecycle.sh +0 -141
- package/scripts/core/provenance.sh +0 -135
- package/scripts/core/selection-state.py +0 -414
- package/scripts/core/selection.sh +0 -168
- package/scripts/core/status.sh +0 -27
- package/scripts/core/upstream.sh +0 -280
- package/scripts/core/validate-adapter-response.py +0 -284
- package/scripts/install +0 -59
- package/scripts/pin +0 -51
- package/scripts/prepare +0 -117
- package/scripts/probe +0 -82
- package/scripts/track-latest +0 -30
- package/scripts/uninstall +0 -35
- package/scripts/unpin +0 -41
- package/scripts/update +0 -34
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# Sourced module; callers own set -eu.
|
|
3
|
-
|
|
4
|
-
SPW_PLUGIN_ID="superpowers@superpowers-manager"
|
|
5
|
-
SPW_MARKETPLACE_NAME="superpowers-manager"
|
|
6
|
-
SPW_LEGACY_PLUGIN_ID="superpowers@superpowers-wrapper"
|
|
7
|
-
SPW_LEGACY_MARKETPLACE_NAME="superpowers-wrapper"
|
|
8
|
-
|
|
9
|
-
# Given a JSON document as the FIRST ARGUMENT (a string), print "present" if any
|
|
10
|
-
# element of the top-level array <array_key> is an object whose <field> equals
|
|
11
|
-
# <value>, else print "absent" (exit 0). On unparseable JSON or invalid schema,
|
|
12
|
-
# print nothing and exit 2 so callers can fail closed rather than treat an
|
|
13
|
-
# unreadable listing as "absent". Every item in the inspected array must be an
|
|
14
|
-
# object with a non-empty string <field>; otherwise ownership is unprovable and
|
|
15
|
-
# the helper exits 2 instead of treating the malformed entry as unrelated.
|
|
16
|
-
# The JSON is passed as an argument (exactly as spw_json_get takes a file path),
|
|
17
|
-
# NOT on stdin: the here-doc below is Python's stdin (its program source), so a
|
|
18
|
-
# json.load(sys.stdin) here would read the program, not the caller's JSON.
|
|
19
|
-
spw_json_array_has() {
|
|
20
|
-
json="$1"
|
|
21
|
-
array_key="$2"
|
|
22
|
-
field="$3"
|
|
23
|
-
value="$4"
|
|
24
|
-
python3 - "$json" "$array_key" "$field" "$value" <<'PY'
|
|
25
|
-
import json, sys
|
|
26
|
-
raw, array_key, field, value = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
|
27
|
-
try:
|
|
28
|
-
data = json.loads(raw)
|
|
29
|
-
except (json.JSONDecodeError, RecursionError):
|
|
30
|
-
sys.exit(2)
|
|
31
|
-
if not isinstance(data, dict):
|
|
32
|
-
sys.exit(2)
|
|
33
|
-
items = data.get(array_key)
|
|
34
|
-
if not isinstance(items, list):
|
|
35
|
-
sys.exit(2)
|
|
36
|
-
for item in items:
|
|
37
|
-
if not isinstance(item, dict):
|
|
38
|
-
sys.exit(2)
|
|
39
|
-
field_value = item.get(field)
|
|
40
|
-
if not isinstance(field_value, str) or not field_value:
|
|
41
|
-
sys.exit(2)
|
|
42
|
-
found = any(item[field] == value for item in items)
|
|
43
|
-
print("present" if found else "absent")
|
|
44
|
-
PY
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
# Print the registered root of <marketplace_name> from a marketplace-list JSON
|
|
48
|
-
# document (as a string argument, like spw_json_array_has), or nothing if the
|
|
49
|
-
# marketplace is absent. Exit 2 on unparseable JSON, a non-object item, or an
|
|
50
|
-
# item without a non-empty string name: such an item cannot be proven unrelated
|
|
51
|
-
# and must not be mistaken for an absent manager marketplace. Validate root only
|
|
52
|
-
# on the matching item; unrelated marketplace roots are never read. A matching
|
|
53
|
-
# item without a non-empty string root also exits 2. Empty output is unambiguous:
|
|
54
|
-
# a valid registered root is always a non-empty path.
|
|
55
|
-
spw_marketplace_root_from_json() {
|
|
56
|
-
json="$1"
|
|
57
|
-
marketplace_name="$2"
|
|
58
|
-
python3 - "$json" "$marketplace_name" <<'PY'
|
|
59
|
-
import json, sys
|
|
60
|
-
raw, name = sys.argv[1], sys.argv[2]
|
|
61
|
-
try:
|
|
62
|
-
data = json.loads(raw)
|
|
63
|
-
except (json.JSONDecodeError, RecursionError):
|
|
64
|
-
sys.exit(2)
|
|
65
|
-
if not isinstance(data, dict):
|
|
66
|
-
sys.exit(2)
|
|
67
|
-
items = data.get("marketplaces")
|
|
68
|
-
if not isinstance(items, list):
|
|
69
|
-
sys.exit(2)
|
|
70
|
-
for item in items:
|
|
71
|
-
if not isinstance(item, dict):
|
|
72
|
-
sys.exit(2)
|
|
73
|
-
item_name = item.get("name")
|
|
74
|
-
if not isinstance(item_name, str) or not item_name:
|
|
75
|
-
sys.exit(2)
|
|
76
|
-
for item in items:
|
|
77
|
-
if item["name"] == name:
|
|
78
|
-
item_root = item.get("root")
|
|
79
|
-
if not isinstance(item_root, str) or not item_root:
|
|
80
|
-
sys.exit(2)
|
|
81
|
-
print(item_root)
|
|
82
|
-
sys.exit(0)
|
|
83
|
-
PY
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
# Print "same" if the two paths refer to the same physical location, else
|
|
87
|
-
# "different". Physical-path normalization via os.path.realpath on both
|
|
88
|
-
# sides (handles /var vs /private/var and other symlinked roots); if
|
|
89
|
-
# realpath fails, fall back to comparing the original strings.
|
|
90
|
-
spw_paths_equal() {
|
|
91
|
-
python3 - "$1" "$2" <<'PY'
|
|
92
|
-
import os, sys
|
|
93
|
-
a, b = sys.argv[1], sys.argv[2]
|
|
94
|
-
try:
|
|
95
|
-
na, nb = os.path.realpath(a), os.path.realpath(b)
|
|
96
|
-
except OSError:
|
|
97
|
-
na, nb = a, b
|
|
98
|
-
print("same" if na == nb else "different")
|
|
99
|
-
PY
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
spw_active_plugin_version_from_json() {
|
|
103
|
-
json="$1"
|
|
104
|
-
plugin_id="$2"
|
|
105
|
-
python3 - "$json" "$plugin_id" <<'PY'
|
|
106
|
-
import json
|
|
107
|
-
import sys
|
|
108
|
-
|
|
109
|
-
raw, plugin_id = sys.argv[1:]
|
|
110
|
-
|
|
111
|
-
def reject_constant(value):
|
|
112
|
-
raise ValueError(f"non-standard JSON constant: {value}")
|
|
113
|
-
|
|
114
|
-
def has_terminal_control(value):
|
|
115
|
-
return any(
|
|
116
|
-
ord(character) < 0x20
|
|
117
|
-
or 0x7F <= ord(character) <= 0x9F
|
|
118
|
-
or 0xD800 <= ord(character) <= 0xDFFF
|
|
119
|
-
for character in value
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
data = json.loads(raw, parse_constant=reject_constant)
|
|
124
|
-
except (json.JSONDecodeError, RecursionError, ValueError):
|
|
125
|
-
sys.exit(2)
|
|
126
|
-
|
|
127
|
-
if not isinstance(data, dict):
|
|
128
|
-
sys.exit(2)
|
|
129
|
-
items = data.get("installed")
|
|
130
|
-
if not isinstance(items, list):
|
|
131
|
-
sys.exit(2)
|
|
132
|
-
|
|
133
|
-
matches = []
|
|
134
|
-
for item in items:
|
|
135
|
-
if not isinstance(item, dict):
|
|
136
|
-
sys.exit(2)
|
|
137
|
-
item_plugin_id = item.get("pluginId")
|
|
138
|
-
if not isinstance(item_plugin_id, str) or not item_plugin_id:
|
|
139
|
-
sys.exit(2)
|
|
140
|
-
if item_plugin_id == plugin_id:
|
|
141
|
-
matches.append(item)
|
|
142
|
-
|
|
143
|
-
if len(matches) > 1:
|
|
144
|
-
sys.exit(2)
|
|
145
|
-
if not matches:
|
|
146
|
-
sys.exit(0)
|
|
147
|
-
|
|
148
|
-
version = matches[0].get("version")
|
|
149
|
-
if (
|
|
150
|
-
not isinstance(version, str)
|
|
151
|
-
or not version
|
|
152
|
-
or version in {".", ".."}
|
|
153
|
-
or "/" in version
|
|
154
|
-
or "\\" in version
|
|
155
|
-
or has_terminal_control(version)
|
|
156
|
-
):
|
|
157
|
-
sys.exit(2)
|
|
158
|
-
print(version)
|
|
159
|
-
PY
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
spw_installed_root_for_version() {
|
|
163
|
-
search_root="$1"
|
|
164
|
-
marketplace="$2"
|
|
165
|
-
plugin="$3"
|
|
166
|
-
version="$4"
|
|
167
|
-
case "$search_root" in
|
|
168
|
-
/) search_root= ;;
|
|
169
|
-
*/) search_root=${search_root%/} ;;
|
|
170
|
-
esac
|
|
171
|
-
printf '%s/plugins/cache/%s/%s/%s\n' \
|
|
172
|
-
"$search_root" "$marketplace" "$plugin" "$version"
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
spw_codex_json_get_or_empty() {
|
|
176
|
-
file="$1"
|
|
177
|
-
key="$2"
|
|
178
|
-
[ -f "$file" ] || return 1
|
|
179
|
-
python3 - "$file" "$key" <<'PY'
|
|
180
|
-
import json
|
|
181
|
-
import sys
|
|
182
|
-
|
|
183
|
-
path, dotted_key = sys.argv[1:]
|
|
184
|
-
MAX_JSON_NESTING = 256
|
|
185
|
-
|
|
186
|
-
def reject_constant(constant):
|
|
187
|
-
raise ValueError(f"non-standard numeric constant: {constant}")
|
|
188
|
-
|
|
189
|
-
def nesting_exceeds_limit(value):
|
|
190
|
-
stack = [(value, 0)]
|
|
191
|
-
while stack:
|
|
192
|
-
current, depth = stack.pop()
|
|
193
|
-
if isinstance(current, dict):
|
|
194
|
-
next_depth = depth + 1
|
|
195
|
-
if next_depth > MAX_JSON_NESTING:
|
|
196
|
-
return True
|
|
197
|
-
stack.extend((child, next_depth) for child in current.values())
|
|
198
|
-
elif isinstance(current, list):
|
|
199
|
-
next_depth = depth + 1
|
|
200
|
-
if next_depth > MAX_JSON_NESTING:
|
|
201
|
-
return True
|
|
202
|
-
stack.extend((child, next_depth) for child in current)
|
|
203
|
-
return False
|
|
204
|
-
|
|
205
|
-
try:
|
|
206
|
-
with open(path, "r", encoding="utf-8") as handle:
|
|
207
|
-
data = json.load(handle, parse_constant=reject_constant)
|
|
208
|
-
except (OSError, UnicodeError, json.JSONDecodeError, RecursionError, ValueError):
|
|
209
|
-
sys.exit(1)
|
|
210
|
-
|
|
211
|
-
if nesting_exceeds_limit(data) or not isinstance(data, dict):
|
|
212
|
-
sys.exit(1)
|
|
213
|
-
|
|
214
|
-
value = data
|
|
215
|
-
for part in dotted_key.split("."):
|
|
216
|
-
if not isinstance(value, dict):
|
|
217
|
-
value = ""
|
|
218
|
-
break
|
|
219
|
-
value = value.get(part, "")
|
|
220
|
-
|
|
221
|
-
if value is None:
|
|
222
|
-
value = ""
|
|
223
|
-
if not isinstance(value, str):
|
|
224
|
-
sys.exit(1)
|
|
225
|
-
print(value)
|
|
226
|
-
PY
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
spw_codex_metadata_commit_or_empty() {
|
|
230
|
-
file="$1"
|
|
231
|
-
if ! commit=$(spw_codex_json_get_or_empty "$file" "commit"); then
|
|
232
|
-
return 1
|
|
233
|
-
fi
|
|
234
|
-
case "$commit" in
|
|
235
|
-
""|*[!0-9a-fA-F]*) return 1 ;;
|
|
236
|
-
esac
|
|
237
|
-
case "${#commit}" in
|
|
238
|
-
7|40) printf '%s\n' "$commit" ;;
|
|
239
|
-
*) return 1 ;;
|
|
240
|
-
esac
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
spw_apply_manifest_overlay() {
|
|
244
|
-
manifest="$1"
|
|
245
|
-
version="$2"
|
|
246
|
-
python3 - "$manifest" "$version" <<'PY'
|
|
247
|
-
import json
|
|
248
|
-
import sys
|
|
249
|
-
|
|
250
|
-
path, version = sys.argv[1:]
|
|
251
|
-
MAX_JSON_NESTING = 256
|
|
252
|
-
|
|
253
|
-
def reject_constant(constant):
|
|
254
|
-
raise ValueError(f"non-standard numeric constant: {constant}")
|
|
255
|
-
|
|
256
|
-
def nesting_exceeds_limit(value):
|
|
257
|
-
stack = [(value, 0)]
|
|
258
|
-
while stack:
|
|
259
|
-
current, depth = stack.pop()
|
|
260
|
-
if isinstance(current, dict):
|
|
261
|
-
next_depth = depth + 1
|
|
262
|
-
if next_depth > MAX_JSON_NESTING:
|
|
263
|
-
return True
|
|
264
|
-
stack.extend((child, next_depth) for child in current.values())
|
|
265
|
-
elif isinstance(current, list):
|
|
266
|
-
next_depth = depth + 1
|
|
267
|
-
if next_depth > MAX_JSON_NESTING:
|
|
268
|
-
return True
|
|
269
|
-
stack.extend((child, next_depth) for child in current)
|
|
270
|
-
return False
|
|
271
|
-
|
|
272
|
-
try:
|
|
273
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
274
|
-
data = json.load(f, parse_constant=reject_constant)
|
|
275
|
-
except RecursionError:
|
|
276
|
-
sys.exit(f"JSON nesting exceeds limit in {path}")
|
|
277
|
-
except json.JSONDecodeError as exc:
|
|
278
|
-
sys.exit(
|
|
279
|
-
f"invalid manifest JSON in {path}: "
|
|
280
|
-
f"line {exc.lineno} column {exc.colno}: {exc.msg}"
|
|
281
|
-
)
|
|
282
|
-
except (OSError, UnicodeError) as exc:
|
|
283
|
-
sys.exit(f"cannot read manifest JSON in {path}: {exc}")
|
|
284
|
-
except ValueError as exc:
|
|
285
|
-
sys.exit(f"invalid manifest JSON in {path}: {exc}")
|
|
286
|
-
|
|
287
|
-
if nesting_exceeds_limit(data):
|
|
288
|
-
sys.exit(f"JSON nesting exceeds limit in {path}")
|
|
289
|
-
|
|
290
|
-
if not isinstance(data, dict):
|
|
291
|
-
sys.exit(f"manifest must be a JSON object: {path}")
|
|
292
|
-
|
|
293
|
-
data["version"] = version
|
|
294
|
-
data["skills"] = "./skills/"
|
|
295
|
-
data.pop("hooks", None)
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
299
|
-
json.dump(data, f, indent=2, allow_nan=False)
|
|
300
|
-
f.write("\n")
|
|
301
|
-
except RecursionError as exc:
|
|
302
|
-
sys.exit(f"manifest JSON nesting exceeds limit while writing {path}: {exc}")
|
|
303
|
-
except (OSError, UnicodeError, ValueError) as exc:
|
|
304
|
-
sys.exit(f"cannot write manifest JSON in {path}: {exc}")
|
|
305
|
-
PY
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
spw_manifest_short_sha_or_empty() {
|
|
309
|
-
file="$1"
|
|
310
|
-
if ! version=$(spw_codex_json_get_or_empty "$file" "version"); then
|
|
311
|
-
return 1
|
|
312
|
-
fi
|
|
313
|
-
case "$version" in
|
|
314
|
-
*+manager.*)
|
|
315
|
-
short="${version##*.}"
|
|
316
|
-
case "$short" in
|
|
317
|
-
[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])
|
|
318
|
-
printf '%s\n' "$short"
|
|
319
|
-
;;
|
|
320
|
-
esac
|
|
321
|
-
;;
|
|
322
|
-
esac
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
spw_installed_commit_from_root_or_empty() {
|
|
326
|
-
active_root="$1"
|
|
327
|
-
installed_metadata="$active_root/.superpowers-upstream.json"
|
|
328
|
-
installed_manifest="$active_root/.codex-plugin/plugin.json"
|
|
329
|
-
installed_commit=""
|
|
330
|
-
if [ -f "$installed_metadata" ]; then
|
|
331
|
-
installed_commit=$(spw_codex_metadata_commit_or_empty "$installed_metadata" || true)
|
|
332
|
-
fi
|
|
333
|
-
if [ -z "$installed_commit" ] && [ -f "$installed_manifest" ]; then
|
|
334
|
-
installed_commit=$(spw_manifest_short_sha_or_empty "$installed_manifest" || true)
|
|
335
|
-
fi
|
|
336
|
-
printf '%s\n' "$installed_commit"
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
spw_codex_emit() {
|
|
340
|
-
operation="$1"
|
|
341
|
-
ok="$2"
|
|
342
|
-
result_json="$3"
|
|
343
|
-
code="$4"
|
|
344
|
-
error_message="$5"
|
|
345
|
-
hints_file="$6"
|
|
346
|
-
messages_file="$7"
|
|
347
|
-
python3 - "$operation" "$ok" "$result_json" "$code" "$error_message" "$hints_file" "$messages_file" <<'PY'
|
|
348
|
-
import json
|
|
349
|
-
import sys
|
|
350
|
-
|
|
351
|
-
operation, ok_text, result_text, code, error_message, hints_path, messages_path = sys.argv[1:]
|
|
352
|
-
|
|
353
|
-
def has_terminal_control(value):
|
|
354
|
-
return any(
|
|
355
|
-
ord(char) < 0x20
|
|
356
|
-
or 0x7f <= ord(char) <= 0x9f
|
|
357
|
-
or 0xd800 <= ord(char) <= 0xdfff
|
|
358
|
-
for char in value
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
if any(has_terminal_control(value) for value in (operation, code, error_message)):
|
|
362
|
-
raise SystemExit("protocol strings must not contain terminal control characters")
|
|
363
|
-
|
|
364
|
-
messages = []
|
|
365
|
-
if messages_path:
|
|
366
|
-
with open(messages_path, encoding="utf-8") as handle:
|
|
367
|
-
for number, raw in enumerate(handle, 1):
|
|
368
|
-
line = raw.rstrip("\n")
|
|
369
|
-
if "\t" not in line:
|
|
370
|
-
raise SystemExit(f"invalid message record at line {number}")
|
|
371
|
-
channel, text = line.split("\t", 1)
|
|
372
|
-
if (
|
|
373
|
-
channel not in {"stdout", "stderr"}
|
|
374
|
-
or not text
|
|
375
|
-
or "\t" in text
|
|
376
|
-
or "\r" in text
|
|
377
|
-
or has_terminal_control(text)
|
|
378
|
-
):
|
|
379
|
-
raise SystemExit(f"invalid message record at line {number}")
|
|
380
|
-
messages.append({"channel": channel, "text": text})
|
|
381
|
-
|
|
382
|
-
ok = ok_text == "true"
|
|
383
|
-
if ok:
|
|
384
|
-
envelope = {
|
|
385
|
-
"protocol": 1,
|
|
386
|
-
"operation": operation,
|
|
387
|
-
"ok": True,
|
|
388
|
-
"messages": messages,
|
|
389
|
-
"result": json.loads(result_text),
|
|
390
|
-
"error": None,
|
|
391
|
-
}
|
|
392
|
-
else:
|
|
393
|
-
hints = []
|
|
394
|
-
if hints_path:
|
|
395
|
-
with open(hints_path, encoding="utf-8") as handle:
|
|
396
|
-
hints = [line.rstrip("\n") for line in handle if line.rstrip("\n")]
|
|
397
|
-
if any(has_terminal_control(hint) for hint in hints):
|
|
398
|
-
raise SystemExit("protocol hints must not contain terminal control characters")
|
|
399
|
-
envelope = {
|
|
400
|
-
"protocol": 1,
|
|
401
|
-
"operation": operation,
|
|
402
|
-
"ok": False,
|
|
403
|
-
"messages": messages,
|
|
404
|
-
"result": None,
|
|
405
|
-
"error": {"code": code, "message": error_message, "hints": hints},
|
|
406
|
-
}
|
|
407
|
-
json.dump(envelope, sys.stdout, allow_nan=False, separators=(",", ":"))
|
|
408
|
-
sys.stdout.write("\n")
|
|
409
|
-
PY
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
spw_codex_success() {
|
|
413
|
-
spw_codex_emit "$1" true "$2" "" "" "" "$3"
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
spw_codex_failure() {
|
|
417
|
-
spw_codex_emit "$1" false '{}' "$2" "$3" "$4" "$5"
|
|
418
|
-
exit 1
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
# Append command output as structured message records while keeping the
|
|
422
|
-
# tab-delimited spool format unambiguous. Each physical command-output line is
|
|
423
|
-
# escaped with Python's unicode_escape codec so literal backslashes remain
|
|
424
|
-
# distinguishable from rendered control characters like \t and \r.
|
|
425
|
-
spw_codex_append_messages() {
|
|
426
|
-
messages_file="$1"
|
|
427
|
-
channel="$2"
|
|
428
|
-
input_file="$3"
|
|
429
|
-
[ -f "$input_file" ] || return 0
|
|
430
|
-
python3 - "$messages_file" "$channel" "$input_file" <<'PY'
|
|
431
|
-
from pathlib import Path
|
|
432
|
-
import sys
|
|
433
|
-
|
|
434
|
-
messages_path = Path(sys.argv[1])
|
|
435
|
-
channel = sys.argv[2]
|
|
436
|
-
input_path = Path(sys.argv[3])
|
|
437
|
-
|
|
438
|
-
chunks = input_path.read_bytes().split(b"\n")
|
|
439
|
-
with messages_path.open("a", encoding="utf-8") as handle:
|
|
440
|
-
for chunk in chunks:
|
|
441
|
-
if not chunk:
|
|
442
|
-
continue
|
|
443
|
-
text = chunk.decode("utf-8", errors="backslashreplace")
|
|
444
|
-
escaped = text.encode("unicode_escape").decode("ascii")
|
|
445
|
-
if not escaped:
|
|
446
|
-
continue
|
|
447
|
-
handle.write(f"{channel}\t{escaped}\n")
|
|
448
|
-
PY
|
|
449
|
-
}
|