packwright 0.1.0__py3-none-any.whl
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.
- packwright/__init__.py +3 -0
- packwright/__main__.py +5 -0
- packwright/adapters/__init__.py +10 -0
- packwright/adapters/claude_code.py +377 -0
- packwright/adapters/codex.py +311 -0
- packwright/adapters/cursor.py +375 -0
- packwright/checker/__init__.py +3 -0
- packwright/checker/scoring.py +924 -0
- packwright/cli.py +971 -0
- packwright/core/__init__.py +57 -0
- packwright/core/adapter_layout.py +35 -0
- packwright/core/adopt.py +199 -0
- packwright/core/character_intake.py +1649 -0
- packwright/core/emotion_engine_contract.py +147 -0
- packwright/core/errors.py +13 -0
- packwright/core/handoff.py +531 -0
- packwright/core/install.py +2114 -0
- packwright/core/intake_contract.py +105 -0
- packwright/core/knowledge_contract.py +212 -0
- packwright/core/loader.py +35 -0
- packwright/core/memory_projection.py +126 -0
- packwright/core/naming.py +98 -0
- packwright/core/pack_metadata.py +100 -0
- packwright/core/path_safety.py +70 -0
- packwright/core/resolver.py +66 -0
- packwright/core/validation.py +645 -0
- packwright/core/workspace_contract.py +102 -0
- packwright-0.1.0.dist-info/METADATA +213 -0
- packwright-0.1.0.dist-info/RECORD +33 -0
- packwright-0.1.0.dist-info/WHEEL +5 -0
- packwright-0.1.0.dist-info/entry_points.txt +2 -0
- packwright-0.1.0.dist-info/licenses/LICENSE +21 -0
- packwright-0.1.0.dist-info/top_level.txt +1 -0
packwright/cli.py
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from packwright import __version__
|
|
11
|
+
from packwright.adapters import compile_to_claude_code_pack, compile_to_codex_pack, compile_to_cursor_pack
|
|
12
|
+
from packwright.checker import score_mechanism
|
|
13
|
+
from packwright.core.pack_metadata import SPEC_PATH, embed_pack_metadata, load_embedded_spec
|
|
14
|
+
from packwright.core import (
|
|
15
|
+
PackwrightError,
|
|
16
|
+
adopt_existing,
|
|
17
|
+
apply_migration,
|
|
18
|
+
create_handoff,
|
|
19
|
+
doctor_target,
|
|
20
|
+
generate_character_source,
|
|
21
|
+
generate_character_source_from_data,
|
|
22
|
+
install_pack,
|
|
23
|
+
load_mechanism,
|
|
24
|
+
plan_migration,
|
|
25
|
+
refresh_emotion_engine_codex,
|
|
26
|
+
render_interviewer_prompt,
|
|
27
|
+
resolve_mechanism,
|
|
28
|
+
starter_character_intake,
|
|
29
|
+
starter_character_preset_names,
|
|
30
|
+
validate_mechanism,
|
|
31
|
+
write_interviewer_prompt,
|
|
32
|
+
)
|
|
33
|
+
from packwright.core.emotion_engine_contract import EMOTION_ENGINE_CODEX_ARTIFACTS
|
|
34
|
+
from packwright.core.errors import PackwrightValidationError
|
|
35
|
+
from packwright.core.naming import normalize_slug
|
|
36
|
+
from packwright.core.path_safety import resolve_destination_path, resolve_source_path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv=None):
|
|
40
|
+
parser = _build_parser()
|
|
41
|
+
args = parser.parse_args(argv)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
if args.command == "validate":
|
|
45
|
+
return _cmd_validate(args)
|
|
46
|
+
if args.command == "resolve":
|
|
47
|
+
return _cmd_resolve(args)
|
|
48
|
+
if args.command == "compile":
|
|
49
|
+
return _cmd_compile(args)
|
|
50
|
+
if args.command == "score":
|
|
51
|
+
return _cmd_score(args)
|
|
52
|
+
if args.command == "install":
|
|
53
|
+
return _cmd_install(args)
|
|
54
|
+
if args.command in {"migrate", "migrate-target"}:
|
|
55
|
+
return _cmd_migrate_target(args)
|
|
56
|
+
if args.command == "handoff-export":
|
|
57
|
+
return _cmd_handoff_export(args)
|
|
58
|
+
if args.command == "adopt":
|
|
59
|
+
return _cmd_adopt(args)
|
|
60
|
+
if args.command == "refresh-emotion-engine-codex":
|
|
61
|
+
return _cmd_refresh_emotion_engine_codex(args)
|
|
62
|
+
if args.command == "doctor":
|
|
63
|
+
return _cmd_doctor(args)
|
|
64
|
+
if args.command == "draft-character":
|
|
65
|
+
return _cmd_draft_character(args)
|
|
66
|
+
if args.command in {"init", "init-character"}:
|
|
67
|
+
return _cmd_init_character(args)
|
|
68
|
+
if args.command in {"build", "run"}:
|
|
69
|
+
return _cmd_run(args)
|
|
70
|
+
except PackwrightError as exc:
|
|
71
|
+
print(str(exc), file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_parser():
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
prog="packwright",
|
|
79
|
+
description="Compile, install, migrate, check, and repair portable agent packs.",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
82
|
+
subparsers = parser.add_subparsers(
|
|
83
|
+
dest="command",
|
|
84
|
+
required=True,
|
|
85
|
+
metavar="{init,draft-character,adopt,build,install,migrate,doctor,score}",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
init_cmd = subparsers.add_parser(
|
|
89
|
+
"init",
|
|
90
|
+
help="create editable agent source from your intake or a nameless starter preset",
|
|
91
|
+
)
|
|
92
|
+
_add_init_arguments(init_cmd)
|
|
93
|
+
|
|
94
|
+
build = subparsers.add_parser("build", help="validate, compile, and score an adapter pack")
|
|
95
|
+
_add_build_arguments(build)
|
|
96
|
+
|
|
97
|
+
validate = subparsers.add_parser("validate", description="validate a character mechanism spec")
|
|
98
|
+
validate.add_argument("mechanism")
|
|
99
|
+
|
|
100
|
+
resolve = subparsers.add_parser("resolve", description="resolve a character mechanism spec to JSON")
|
|
101
|
+
resolve.add_argument("mechanism")
|
|
102
|
+
resolve.add_argument("--set", action="append", default=[], dest="sets", help="parameter override as key=value")
|
|
103
|
+
resolve.add_argument("--out", help="output JSON path")
|
|
104
|
+
|
|
105
|
+
compile_cmd = subparsers.add_parser("compile", description="compile an adapter pack")
|
|
106
|
+
compile_cmd.add_argument("mechanism")
|
|
107
|
+
compile_cmd.add_argument("--adapter", default="codex", choices=["codex", "claude-code", "cursor"])
|
|
108
|
+
compile_cmd.add_argument("--set", action="append", default=[], dest="sets", help="parameter override as key=value")
|
|
109
|
+
compile_cmd.add_argument("--out-dir", default="build/codex", help="adapter pack output directory")
|
|
110
|
+
compile_cmd.add_argument("--force", action="store_true", help="overwrite existing pack artifacts")
|
|
111
|
+
|
|
112
|
+
install = subparsers.add_parser("install", help="install an adapter pack into a local runtime directory")
|
|
113
|
+
install.add_argument("pack_dir_positional", nargs="?", metavar="PACK_DIR", help="adapter pack directory")
|
|
114
|
+
install.add_argument("--adapter", default="codex", choices=["codex", "claude-code", "cursor"])
|
|
115
|
+
install.add_argument(
|
|
116
|
+
"--pack-dir",
|
|
117
|
+
dest="pack_dir_option",
|
|
118
|
+
metavar="PACK_DIR",
|
|
119
|
+
help="existing adapter pack directory",
|
|
120
|
+
)
|
|
121
|
+
install.add_argument(
|
|
122
|
+
"--target-dir",
|
|
123
|
+
"--target",
|
|
124
|
+
required=True,
|
|
125
|
+
dest="target_dir",
|
|
126
|
+
metavar="TARGET",
|
|
127
|
+
help="local runtime working directory",
|
|
128
|
+
)
|
|
129
|
+
install.add_argument(
|
|
130
|
+
"--force",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="overwrite managed target artifacts while preserving portable state",
|
|
133
|
+
)
|
|
134
|
+
install.add_argument(
|
|
135
|
+
"--include-emotion-engine-codex",
|
|
136
|
+
action="store_true",
|
|
137
|
+
default=None,
|
|
138
|
+
help="include the optional Emotion Engine Codex sidecar; requires --emotion-engine-codex-source or PACKWRIGHT_EMOTION_ENGINE_CODEX_DIR",
|
|
139
|
+
)
|
|
140
|
+
install.add_argument(
|
|
141
|
+
"--emotion-engine-codex-source",
|
|
142
|
+
help="source directory for the emotion-engine-codex sidecar skill",
|
|
143
|
+
)
|
|
144
|
+
install.add_argument(
|
|
145
|
+
"--emotion-style",
|
|
146
|
+
help="style description for the initialized Emotion Engine state",
|
|
147
|
+
)
|
|
148
|
+
install.add_argument(
|
|
149
|
+
"--emotion-engine-mode",
|
|
150
|
+
default=None,
|
|
151
|
+
choices=["light", "always", "paused"],
|
|
152
|
+
help="override the pack's recommended Emotion Engine runtime mode for Codex installs",
|
|
153
|
+
)
|
|
154
|
+
install.add_argument("--out", help="output install JSON path")
|
|
155
|
+
|
|
156
|
+
migrate = subparsers.add_parser("migrate", help="migrate an installed target into another adapter")
|
|
157
|
+
_add_migrate_arguments(migrate)
|
|
158
|
+
|
|
159
|
+
migrate_target = subparsers.add_parser(
|
|
160
|
+
"migrate-target",
|
|
161
|
+
description="legacy alias for migrate",
|
|
162
|
+
)
|
|
163
|
+
_add_migrate_arguments(migrate_target)
|
|
164
|
+
|
|
165
|
+
handoff = subparsers.add_parser(
|
|
166
|
+
"handoff-export",
|
|
167
|
+
description="write a reviewable handoff file for another agent",
|
|
168
|
+
)
|
|
169
|
+
handoff.add_argument("--source-target-dir", required=True, help="installed source target directory")
|
|
170
|
+
handoff.add_argument("--out", required=True, help="handoff Markdown output path")
|
|
171
|
+
handoff.add_argument("--summary", help="human handoff summary written by the source agent")
|
|
172
|
+
handoff.add_argument("--changed", action="append", default=[], help="relative source target path changed in this handoff")
|
|
173
|
+
handoff.add_argument("--read", action="append", default=[], dest="reads", help="relative source target path the receiver should read")
|
|
174
|
+
handoff.add_argument("--next-step", action="append", default=[], help="next step for the receiving agent")
|
|
175
|
+
handoff.add_argument(
|
|
176
|
+
"--include-inventory",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="include a portable memory/workspace file inventory for review; does not copy files",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
adopt = subparsers.add_parser(
|
|
182
|
+
"adopt",
|
|
183
|
+
help="inventory an existing agent for reviewable adoption",
|
|
184
|
+
description="inventory an existing local agent/workspace for reviewable Packwright adoption",
|
|
185
|
+
)
|
|
186
|
+
adopt.add_argument("--from", required=True, dest="source_dir", help="existing local instance directory")
|
|
187
|
+
adopt.add_argument("--target-dir", help="directory where adoption report and scaffold should be written")
|
|
188
|
+
adopt.add_argument("--dry-run", action="store_true", help="only print inventory and review queues")
|
|
189
|
+
adopt.add_argument("--force", action="store_true", help="overwrite existing adoption report files")
|
|
190
|
+
adopt.add_argument("--out", help="output adopt JSON path")
|
|
191
|
+
|
|
192
|
+
refresh_emotion = subparsers.add_parser(
|
|
193
|
+
"refresh-emotion-engine-codex",
|
|
194
|
+
description="refresh an installed Codex Emotion Engine sidecar without resetting runtime state",
|
|
195
|
+
)
|
|
196
|
+
refresh_emotion.add_argument("--target-dir", required=True, help="installed Codex target directory")
|
|
197
|
+
refresh_emotion.add_argument(
|
|
198
|
+
"--emotion-engine-codex-source",
|
|
199
|
+
help="source directory for the emotion-engine-codex sidecar skill",
|
|
200
|
+
)
|
|
201
|
+
refresh_emotion.add_argument(
|
|
202
|
+
"--emotion-style",
|
|
203
|
+
help="style description used only if the target state file must be initialized",
|
|
204
|
+
)
|
|
205
|
+
refresh_emotion.add_argument(
|
|
206
|
+
"--emotion-engine-mode",
|
|
207
|
+
default=None,
|
|
208
|
+
choices=["light", "always", "paused"],
|
|
209
|
+
help="override the target manifest's Emotion Engine runtime mode",
|
|
210
|
+
)
|
|
211
|
+
refresh_emotion.add_argument("--out", help="output refresh JSON path")
|
|
212
|
+
|
|
213
|
+
doctor = subparsers.add_parser("doctor", help="inspect and optionally repair an installed target")
|
|
214
|
+
doctor.add_argument("target_dir_positional", nargs="?", metavar="TARGET", help="installed target directory")
|
|
215
|
+
doctor.add_argument(
|
|
216
|
+
"--target-dir",
|
|
217
|
+
"--target",
|
|
218
|
+
dest="target_dir_option",
|
|
219
|
+
metavar="TARGET",
|
|
220
|
+
help="installed target directory",
|
|
221
|
+
)
|
|
222
|
+
doctor.add_argument("--fix", action="store_true", help="apply deterministic repairs for detected drift")
|
|
223
|
+
doctor.add_argument(
|
|
224
|
+
"--emotion-engine-codex-source",
|
|
225
|
+
help="source directory for the emotion-engine-codex sidecar skill",
|
|
226
|
+
)
|
|
227
|
+
doctor.add_argument(
|
|
228
|
+
"--emotion-style",
|
|
229
|
+
help="style description used only if the target state file must be initialized",
|
|
230
|
+
)
|
|
231
|
+
doctor.add_argument(
|
|
232
|
+
"--emotion-engine-mode",
|
|
233
|
+
default=None,
|
|
234
|
+
choices=["light", "always", "paused"],
|
|
235
|
+
help="override the target manifest's Emotion Engine runtime mode when fixing",
|
|
236
|
+
)
|
|
237
|
+
doctor.add_argument("--out", help="output doctor JSON path")
|
|
238
|
+
|
|
239
|
+
score = subparsers.add_parser("score", help="score a resolved mechanism and adapter pack")
|
|
240
|
+
score.add_argument("mechanism", help="mechanism source, pack, or installed target")
|
|
241
|
+
score.add_argument("--adapter", default="codex", choices=["codex", "claude-code", "cursor"])
|
|
242
|
+
score.add_argument("--pack-dir", help="existing adapter pack directory; compiles in memory when omitted")
|
|
243
|
+
score.add_argument("--threshold", type=int, help="score threshold override")
|
|
244
|
+
score.add_argument("--set", action="append", default=[], dest="sets", help="parameter override as key=value")
|
|
245
|
+
score.add_argument("--out", help="output score JSON path")
|
|
246
|
+
|
|
247
|
+
draft_character = subparsers.add_parser(
|
|
248
|
+
"draft-character",
|
|
249
|
+
help="draft a custom character intake through your coding agent",
|
|
250
|
+
description="print the LLM interviewer contract for drafting canonical character_intake.yaml",
|
|
251
|
+
)
|
|
252
|
+
draft_character.add_argument("--user-name", default="the user", help="default user name in the interview contract")
|
|
253
|
+
draft_character.add_argument("--prompt-out", help="write the interviewer prompt to this Markdown file")
|
|
254
|
+
|
|
255
|
+
init_character = subparsers.add_parser(
|
|
256
|
+
"init-character",
|
|
257
|
+
description="legacy alias for init",
|
|
258
|
+
)
|
|
259
|
+
_add_init_arguments(init_character)
|
|
260
|
+
|
|
261
|
+
run = subparsers.add_parser("run", description="legacy alias for build")
|
|
262
|
+
_add_build_arguments(run)
|
|
263
|
+
|
|
264
|
+
return parser
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _add_migrate_arguments(parser):
|
|
268
|
+
parser.add_argument(
|
|
269
|
+
"source_target_dir_positional",
|
|
270
|
+
nargs="?",
|
|
271
|
+
metavar="SOURCE_TARGET",
|
|
272
|
+
help="installed source target directory",
|
|
273
|
+
)
|
|
274
|
+
parser.add_argument(
|
|
275
|
+
"--source-target-dir",
|
|
276
|
+
dest="source_target_dir_option",
|
|
277
|
+
metavar="SOURCE_TARGET",
|
|
278
|
+
help="installed source target directory",
|
|
279
|
+
)
|
|
280
|
+
parser.add_argument(
|
|
281
|
+
"--target-dir",
|
|
282
|
+
"--target",
|
|
283
|
+
required=True,
|
|
284
|
+
dest="target_dir",
|
|
285
|
+
metavar="TARGET",
|
|
286
|
+
help="destination runtime working directory",
|
|
287
|
+
)
|
|
288
|
+
parser.add_argument(
|
|
289
|
+
"--to",
|
|
290
|
+
"--to-adapter",
|
|
291
|
+
required=True,
|
|
292
|
+
dest="to_adapter",
|
|
293
|
+
choices=["codex", "claude-code", "cursor"],
|
|
294
|
+
)
|
|
295
|
+
parser.add_argument("--mechanism", help="source mechanism path; defaults to source manifest source_mechanism")
|
|
296
|
+
parser.add_argument("--set", action="append", default=[], dest="sets", help="parameter override as key=value")
|
|
297
|
+
parser.add_argument("--pack-dir", help="optional destination adapter pack output directory")
|
|
298
|
+
parser.add_argument("--force", action="store_true", help="overwrite existing pack/target/migration state")
|
|
299
|
+
execution = parser.add_mutually_exclusive_group()
|
|
300
|
+
execution.add_argument(
|
|
301
|
+
"--dry-run",
|
|
302
|
+
action="store_true",
|
|
303
|
+
help="print the complete migration plan without writing target or pack files",
|
|
304
|
+
)
|
|
305
|
+
execution.add_argument(
|
|
306
|
+
"--yes",
|
|
307
|
+
action="store_true",
|
|
308
|
+
help="apply without an interactive confirmation prompt",
|
|
309
|
+
)
|
|
310
|
+
parser.add_argument("--json", action="store_true", help="emit the complete path-level migration receipt as JSON")
|
|
311
|
+
parser.add_argument("--slug", help="explicit character slug for generated destination artifacts")
|
|
312
|
+
parser.add_argument(
|
|
313
|
+
"--no-upgrade-adapter-support",
|
|
314
|
+
action="store_false",
|
|
315
|
+
dest="upgrade_adapter_support",
|
|
316
|
+
help="do not synthesize missing current adapter declarations in old mechanism specs",
|
|
317
|
+
)
|
|
318
|
+
parser.add_argument(
|
|
319
|
+
"--no-emotion-state",
|
|
320
|
+
action="store_false",
|
|
321
|
+
dest="include_emotion_state",
|
|
322
|
+
help="do not copy .emotion-engine/codex-state.json as a migration snapshot",
|
|
323
|
+
)
|
|
324
|
+
parser.add_argument(
|
|
325
|
+
"--emotion-engine-codex-source",
|
|
326
|
+
help="source directory for the emotion-engine-codex sidecar skill when migrating to Codex",
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument(
|
|
329
|
+
"--emotion-style",
|
|
330
|
+
help="style description used only if a Codex target state file must be initialized",
|
|
331
|
+
)
|
|
332
|
+
parser.add_argument(
|
|
333
|
+
"--emotion-engine-mode",
|
|
334
|
+
default=None,
|
|
335
|
+
choices=["light", "always", "paused"],
|
|
336
|
+
help="override the destination Codex target Emotion Engine runtime mode",
|
|
337
|
+
)
|
|
338
|
+
parser.add_argument("--out", help="output migration JSON path")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _add_init_arguments(parser):
|
|
342
|
+
parser.add_argument("intake", nargs="?", help="character_intake.yaml path; omit with --template or --interactive")
|
|
343
|
+
parser.add_argument(
|
|
344
|
+
"--interactive",
|
|
345
|
+
action="store_true",
|
|
346
|
+
help="basic fallback prompts only; prefer draft-character with an LLM for semantic normalization",
|
|
347
|
+
)
|
|
348
|
+
parser.add_argument(
|
|
349
|
+
"--template",
|
|
350
|
+
choices=starter_character_preset_names(),
|
|
351
|
+
help="nameless starter preset: code, work, or companion; requires --name",
|
|
352
|
+
)
|
|
353
|
+
parser.add_argument("--name", help="character name chosen by the user; required with --template")
|
|
354
|
+
parser.add_argument("--user-name", help="how the character should refer to the user in generated files")
|
|
355
|
+
parser.add_argument("--slug", help="explicit lowercase ASCII character slug, useful for non-Latin names")
|
|
356
|
+
parser.add_argument("--save-intake", help="write the generated or answered intake YAML to this path")
|
|
357
|
+
parser.add_argument(
|
|
358
|
+
"-o",
|
|
359
|
+
"--out-dir",
|
|
360
|
+
dest="out_dir",
|
|
361
|
+
help="output source directory; defaults to work/<character>",
|
|
362
|
+
)
|
|
363
|
+
parser.add_argument("--force", action="store_true", help="overwrite generated character files")
|
|
364
|
+
parser.add_argument("--out", help="output generation JSON path")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _add_build_arguments(parser):
|
|
368
|
+
parser.add_argument("mechanism")
|
|
369
|
+
parser.add_argument("--adapter", default="codex", choices=["codex", "claude-code", "cursor"])
|
|
370
|
+
parser.add_argument("--set", action="append", default=[], dest="sets", help="parameter override as key=value")
|
|
371
|
+
parser.add_argument(
|
|
372
|
+
"-o",
|
|
373
|
+
"--out-dir",
|
|
374
|
+
"--build-dir",
|
|
375
|
+
default="build/codex",
|
|
376
|
+
dest="build_dir",
|
|
377
|
+
help="adapter pack output directory",
|
|
378
|
+
)
|
|
379
|
+
parser.add_argument("--threshold", type=int, help="score threshold override")
|
|
380
|
+
parser.add_argument("--force", action="store_true", help="overwrite existing pack artifacts")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _cmd_validate(args):
|
|
384
|
+
data = load_mechanism(args.mechanism)
|
|
385
|
+
validate_mechanism(data)
|
|
386
|
+
print("ok")
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _cmd_resolve(args):
|
|
391
|
+
data = load_mechanism(args.mechanism)
|
|
392
|
+
resolved = resolve_mechanism(data, _parse_sets(args.sets))
|
|
393
|
+
_write_json_or_print(resolved, args.out)
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _cmd_compile(args):
|
|
398
|
+
data = load_mechanism(args.mechanism)
|
|
399
|
+
resolved = resolve_mechanism(data, _parse_sets(args.sets))
|
|
400
|
+
pack = _compile_pack(args.adapter, resolved, {"source_mechanism": args.mechanism})
|
|
401
|
+
receipt = score_mechanism(resolved, pack, adapter=args.adapter)
|
|
402
|
+
pack = embed_pack_metadata(pack, resolved, receipt)
|
|
403
|
+
out_dir = Path(args.out_dir)
|
|
404
|
+
_write_pack(pack, out_dir, force=args.force)
|
|
405
|
+
print(
|
|
406
|
+
json.dumps(
|
|
407
|
+
{
|
|
408
|
+
"adapter_pack": str(out_dir),
|
|
409
|
+
"artifacts": sorted(pack.keys()),
|
|
410
|
+
},
|
|
411
|
+
indent=2,
|
|
412
|
+
sort_keys=True,
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _cmd_score(args):
|
|
419
|
+
input_path = Path(args.mechanism)
|
|
420
|
+
embedded_spec = input_path / SPEC_PATH
|
|
421
|
+
is_embedded = input_path.is_dir() and embedded_spec.is_file()
|
|
422
|
+
if is_embedded:
|
|
423
|
+
data = load_embedded_spec(input_path)
|
|
424
|
+
if not args.pack_dir:
|
|
425
|
+
args.pack_dir = str(input_path)
|
|
426
|
+
manifest = _load_pack_manifest(input_path)
|
|
427
|
+
args.adapter = manifest.get("adapter", args.adapter)
|
|
428
|
+
else:
|
|
429
|
+
data = load_mechanism(args.mechanism)
|
|
430
|
+
params = _parse_sets(args.sets)
|
|
431
|
+
if args.pack_dir:
|
|
432
|
+
pack = _read_pack(Path(args.pack_dir))
|
|
433
|
+
if not params:
|
|
434
|
+
params = _pack_resolved_parameters(pack)
|
|
435
|
+
else:
|
|
436
|
+
pack = None
|
|
437
|
+
resolved = data if is_embedded and not params else resolve_mechanism(data, params)
|
|
438
|
+
if pack is None:
|
|
439
|
+
pack = _compile_pack(args.adapter, resolved, {"source_mechanism": args.mechanism})
|
|
440
|
+
result = score_mechanism(resolved, pack, adapter=args.adapter, threshold=args.threshold)
|
|
441
|
+
_write_json_or_print(result, args.out)
|
|
442
|
+
return 0 if result["passed"] else 1
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _cmd_install(args):
|
|
446
|
+
pack_dir = _required_path_argument(
|
|
447
|
+
args.pack_dir_positional,
|
|
448
|
+
args.pack_dir_option,
|
|
449
|
+
"pack directory",
|
|
450
|
+
"PACK_DIR or --pack-dir",
|
|
451
|
+
)
|
|
452
|
+
result = install_pack(
|
|
453
|
+
pack_dir,
|
|
454
|
+
args.target_dir,
|
|
455
|
+
adapter=args.adapter,
|
|
456
|
+
force=args.force,
|
|
457
|
+
include_emotion_engine_codex=args.include_emotion_engine_codex,
|
|
458
|
+
emotion_engine_codex_source=args.emotion_engine_codex_source,
|
|
459
|
+
emotion_style=args.emotion_style,
|
|
460
|
+
emotion_engine_mode=args.emotion_engine_mode,
|
|
461
|
+
)
|
|
462
|
+
_write_json_or_print(result, args.out)
|
|
463
|
+
return 0
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _cmd_migrate_target(args):
|
|
467
|
+
source_target_dir = _required_path_argument(
|
|
468
|
+
args.source_target_dir_positional,
|
|
469
|
+
args.source_target_dir_option,
|
|
470
|
+
"source target",
|
|
471
|
+
"SOURCE_TARGET or --source-target-dir",
|
|
472
|
+
)
|
|
473
|
+
plan = plan_migration(
|
|
474
|
+
source_target_dir,
|
|
475
|
+
args.target_dir,
|
|
476
|
+
to_adapter=args.to_adapter,
|
|
477
|
+
mechanism_path=args.mechanism,
|
|
478
|
+
parameters=_parse_sets(args.sets),
|
|
479
|
+
pack_dir=args.pack_dir,
|
|
480
|
+
force=args.force,
|
|
481
|
+
include_emotion_state=args.include_emotion_state,
|
|
482
|
+
slug=args.slug,
|
|
483
|
+
upgrade_adapter_support=args.upgrade_adapter_support,
|
|
484
|
+
emotion_engine_codex_source=args.emotion_engine_codex_source,
|
|
485
|
+
emotion_style=args.emotion_style,
|
|
486
|
+
emotion_engine_mode=args.emotion_engine_mode,
|
|
487
|
+
)
|
|
488
|
+
report = plan.to_dict()
|
|
489
|
+
if args.dry_run:
|
|
490
|
+
report["dry_run"] = True
|
|
491
|
+
_emit_migration_report(report, args)
|
|
492
|
+
return 0 if report["ready"] else 1
|
|
493
|
+
|
|
494
|
+
if not report["ready"]:
|
|
495
|
+
report["dry_run"] = True
|
|
496
|
+
_emit_migration_report(report, args)
|
|
497
|
+
return 1
|
|
498
|
+
|
|
499
|
+
if not args.yes:
|
|
500
|
+
if args.json or not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
501
|
+
report["status"] = "confirmation_required"
|
|
502
|
+
report["dry_run"] = True
|
|
503
|
+
_emit_migration_report(report, args)
|
|
504
|
+
print(
|
|
505
|
+
"migration not applied: use --dry-run to preview or --yes after review",
|
|
506
|
+
file=sys.stderr,
|
|
507
|
+
)
|
|
508
|
+
return 2
|
|
509
|
+
_print_migration_report(report)
|
|
510
|
+
if not _confirm_migration():
|
|
511
|
+
print("Migration cancelled. No files written.")
|
|
512
|
+
return 1
|
|
513
|
+
result = apply_migration(plan)
|
|
514
|
+
result["dry_run"] = False
|
|
515
|
+
_emit_migration_report(result, args)
|
|
516
|
+
return 0 if result["ok"] else 1
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _cmd_handoff_export(args):
|
|
520
|
+
result = create_handoff(
|
|
521
|
+
args.source_target_dir,
|
|
522
|
+
args.out,
|
|
523
|
+
summary=args.summary,
|
|
524
|
+
changed_paths=args.changed,
|
|
525
|
+
recommended_reads=args.reads or None,
|
|
526
|
+
next_steps=args.next_step,
|
|
527
|
+
include_inventory=args.include_inventory,
|
|
528
|
+
)
|
|
529
|
+
_write_json_or_print(result, None)
|
|
530
|
+
return 0
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _cmd_adopt(args):
|
|
534
|
+
dry_run = args.dry_run or not args.target_dir
|
|
535
|
+
result = adopt_existing(
|
|
536
|
+
args.source_dir,
|
|
537
|
+
target_dir=args.target_dir,
|
|
538
|
+
dry_run=dry_run,
|
|
539
|
+
force=args.force,
|
|
540
|
+
)
|
|
541
|
+
_write_json_or_print(result, args.out)
|
|
542
|
+
return 0
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _cmd_refresh_emotion_engine_codex(args):
|
|
546
|
+
result = refresh_emotion_engine_codex(
|
|
547
|
+
args.target_dir,
|
|
548
|
+
emotion_engine_codex_source=args.emotion_engine_codex_source,
|
|
549
|
+
emotion_style=args.emotion_style,
|
|
550
|
+
emotion_engine_mode=args.emotion_engine_mode,
|
|
551
|
+
)
|
|
552
|
+
_write_json_or_print(result, args.out)
|
|
553
|
+
return 0
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _cmd_doctor(args):
|
|
557
|
+
target_dir = _required_path_argument(
|
|
558
|
+
args.target_dir_positional,
|
|
559
|
+
args.target_dir_option,
|
|
560
|
+
"target",
|
|
561
|
+
"TARGET or --target-dir",
|
|
562
|
+
)
|
|
563
|
+
result = doctor_target(
|
|
564
|
+
target_dir,
|
|
565
|
+
fix=args.fix,
|
|
566
|
+
emotion_engine_codex_source=args.emotion_engine_codex_source,
|
|
567
|
+
emotion_style=args.emotion_style,
|
|
568
|
+
emotion_engine_mode=args.emotion_engine_mode,
|
|
569
|
+
)
|
|
570
|
+
_write_json_or_print(result, args.out)
|
|
571
|
+
return 0 if result.get("ok") else 1
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _cmd_draft_character(args):
|
|
575
|
+
if args.prompt_out:
|
|
576
|
+
path = write_interviewer_prompt(args.prompt_out, user_name=args.user_name)
|
|
577
|
+
print(json.dumps({"interviewer_prompt": str(path)}, indent=2, sort_keys=True))
|
|
578
|
+
else:
|
|
579
|
+
print(render_interviewer_prompt(user_name=args.user_name))
|
|
580
|
+
return 0
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _cmd_init_character(args):
|
|
584
|
+
if args.template:
|
|
585
|
+
if args.intake:
|
|
586
|
+
raise PackwrightError("init accepts either an intake path or --template, not both")
|
|
587
|
+
if args.interactive:
|
|
588
|
+
raise PackwrightError("init accepts either --interactive or --template, not both")
|
|
589
|
+
if not args.name:
|
|
590
|
+
raise PackwrightError("starter presets are nameless; provide the character name with --name")
|
|
591
|
+
intake = starter_character_intake(
|
|
592
|
+
args.template,
|
|
593
|
+
name=args.name,
|
|
594
|
+
user_name=args.user_name,
|
|
595
|
+
slug=args.slug,
|
|
596
|
+
)
|
|
597
|
+
if args.save_intake:
|
|
598
|
+
_write_yaml(intake, Path(args.save_intake))
|
|
599
|
+
result = generate_character_source_from_data(intake, out_dir=args.out_dir, force=args.force)
|
|
600
|
+
result["intake"] = args.save_intake or f"template:{args.template}"
|
|
601
|
+
elif args.interactive:
|
|
602
|
+
if args.name:
|
|
603
|
+
raise PackwrightError("--name is only accepted with --template; interactive mode asks for a name")
|
|
604
|
+
print(
|
|
605
|
+
"warning: --interactive is a basic fallback and does not semantically normalize answers; "
|
|
606
|
+
"prefer `packwright draft-character` with an LLM-produced intake YAML.",
|
|
607
|
+
file=sys.stderr,
|
|
608
|
+
)
|
|
609
|
+
intake = _prompt_character_intake(args.user_name, slug=args.slug)
|
|
610
|
+
if args.save_intake:
|
|
611
|
+
_write_yaml(intake, Path(args.save_intake))
|
|
612
|
+
result = generate_character_source_from_data(intake, out_dir=args.out_dir, force=args.force)
|
|
613
|
+
result["intake"] = args.save_intake or "interactive"
|
|
614
|
+
else:
|
|
615
|
+
if args.name:
|
|
616
|
+
raise PackwrightError("--name is only accepted with --template; intake files already contain a name")
|
|
617
|
+
if not args.intake:
|
|
618
|
+
raise PackwrightError("init requires an intake path unless --template or --interactive is used")
|
|
619
|
+
result = generate_character_source(args.intake, out_dir=args.out_dir, force=args.force)
|
|
620
|
+
_write_json_or_print(result, args.out)
|
|
621
|
+
return 0
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _cmd_run(args):
|
|
625
|
+
data = load_mechanism(args.mechanism)
|
|
626
|
+
validate_mechanism(data)
|
|
627
|
+
resolved = resolve_mechanism(data, _parse_sets(args.sets))
|
|
628
|
+
|
|
629
|
+
build_dir = Path(args.build_dir)
|
|
630
|
+
resolved_path = build_dir / "resolved.json"
|
|
631
|
+
score_path = build_dir / "score.json"
|
|
632
|
+
|
|
633
|
+
pack = _compile_pack(
|
|
634
|
+
args.adapter,
|
|
635
|
+
resolved,
|
|
636
|
+
{
|
|
637
|
+
"source_mechanism": args.mechanism,
|
|
638
|
+
"resolved_mechanism": str(resolved_path),
|
|
639
|
+
"checker_score": str(score_path),
|
|
640
|
+
},
|
|
641
|
+
)
|
|
642
|
+
result = score_mechanism(resolved, pack, adapter=args.adapter, threshold=args.threshold)
|
|
643
|
+
|
|
644
|
+
pack = embed_pack_metadata(pack, resolved, result)
|
|
645
|
+
|
|
646
|
+
outputs = dict(pack)
|
|
647
|
+
outputs["resolved.json"] = pack[SPEC_PATH]
|
|
648
|
+
outputs["score.json"] = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
|
649
|
+
_write_pack(outputs, build_dir, force=args.force)
|
|
650
|
+
|
|
651
|
+
manifest = {
|
|
652
|
+
"adapter_pack": str(build_dir),
|
|
653
|
+
"resolved_mechanism": str(resolved_path),
|
|
654
|
+
"checker_score": str(score_path),
|
|
655
|
+
"passed": result["passed"],
|
|
656
|
+
"score": result["score"],
|
|
657
|
+
}
|
|
658
|
+
print(json.dumps(manifest, indent=2, sort_keys=True))
|
|
659
|
+
return 0 if result["passed"] else 1
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _parse_sets(items):
|
|
663
|
+
parsed = {}
|
|
664
|
+
for item in items:
|
|
665
|
+
if "=" not in item:
|
|
666
|
+
raise PackwrightError(f"--set value must use key=value format: {item}")
|
|
667
|
+
key, value = item.split("=", 1)
|
|
668
|
+
key = key.strip()
|
|
669
|
+
if not key:
|
|
670
|
+
raise PackwrightError("--set key cannot be empty")
|
|
671
|
+
parsed[key] = value
|
|
672
|
+
return parsed
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _required_path_argument(positional, option, label, usage):
|
|
676
|
+
if positional and option:
|
|
677
|
+
raise PackwrightError(f"pass {label} once, using {usage}")
|
|
678
|
+
value = option or positional
|
|
679
|
+
if not value:
|
|
680
|
+
raise PackwrightError(f"{label} is required; pass {usage}")
|
|
681
|
+
return value
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _prompt_character_intake(user_name, slug=None):
|
|
685
|
+
print("Packwright character intake")
|
|
686
|
+
print("Basic fallback mode: fixed questions, no LLM normalization.")
|
|
687
|
+
name = _prompt_required("1. What should the character be called?")
|
|
688
|
+
resolved_slug = normalize_slug(slug, default="") if slug else _prompt_optional_slug(name)
|
|
689
|
+
relationship = _prompt_required(
|
|
690
|
+
"2. What relationship should they have with you? "
|
|
691
|
+
"For example: work partner, secretary, coach, friendly companion, or researcher."
|
|
692
|
+
)
|
|
693
|
+
primary_work = _prompt_list(
|
|
694
|
+
"3. What should they mainly help you do? Separate multiple items with commas or semicolons."
|
|
695
|
+
)
|
|
696
|
+
voice = _prompt_required(
|
|
697
|
+
"4. How should they sound? You can also describe tones or habits they should avoid."
|
|
698
|
+
)
|
|
699
|
+
continuity = _prompt_relationship_continuity()
|
|
700
|
+
resolved_user_name = (
|
|
701
|
+
user_name
|
|
702
|
+
or os.environ.get("PACKWRIGHT_USER_NAME")
|
|
703
|
+
or "the user"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
"version": "0.1",
|
|
708
|
+
"kind": "CharacterIntake",
|
|
709
|
+
"character": {
|
|
710
|
+
"name": name,
|
|
711
|
+
"slug": resolved_slug or normalize_slug(name),
|
|
712
|
+
"user_name": resolved_user_name,
|
|
713
|
+
"relationship": relationship,
|
|
714
|
+
"role": _role_from_answers(resolved_user_name, relationship, primary_work),
|
|
715
|
+
"voice": voice,
|
|
716
|
+
"avoid": _avoid_from_voice(voice),
|
|
717
|
+
"primary_work": primary_work,
|
|
718
|
+
"relationship_continuity": continuity,
|
|
719
|
+
"traits": _traits_from_voice(voice),
|
|
720
|
+
"direct_emotional_interaction": _direct_emotional_interaction_from_continuity(continuity),
|
|
721
|
+
},
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _prompt_optional_slug(name):
|
|
726
|
+
value = input(
|
|
727
|
+
"1b. Choose an English or romanized slug for filenames, such as nova. "
|
|
728
|
+
"Leave blank to generate one automatically.\n> "
|
|
729
|
+
).strip()
|
|
730
|
+
if value:
|
|
731
|
+
return normalize_slug(value)
|
|
732
|
+
return normalize_slug(name)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _prompt_required(prompt):
|
|
736
|
+
while True:
|
|
737
|
+
value = input(prompt + "\n> ").strip()
|
|
738
|
+
if value:
|
|
739
|
+
return value
|
|
740
|
+
print("This field is required.")
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _prompt_list(prompt):
|
|
744
|
+
while True:
|
|
745
|
+
value = _prompt_required(prompt)
|
|
746
|
+
items = [item.strip() for item in re.split(r"[,,;;\n]+", value) if item.strip()]
|
|
747
|
+
if items:
|
|
748
|
+
return items
|
|
749
|
+
print("Enter at least one item.")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _prompt_relationship_continuity():
|
|
753
|
+
prompt = (
|
|
754
|
+
"5. How much relationship continuity should this character maintain?\n"
|
|
755
|
+
" A = Task-only, with no emotional relationship continuity\n"
|
|
756
|
+
" B = Warm, but remembers only important preferences\n"
|
|
757
|
+
" C = Close, long-term continuity that remembers interaction details"
|
|
758
|
+
)
|
|
759
|
+
choices = {
|
|
760
|
+
"a": "task_only",
|
|
761
|
+
"1": "task_only",
|
|
762
|
+
"只做事": "task_only",
|
|
763
|
+
"task_only": "task_only",
|
|
764
|
+
"b": "warm_selective",
|
|
765
|
+
"2": "warm_selective",
|
|
766
|
+
"有温度": "warm_selective",
|
|
767
|
+
"重要偏好": "warm_selective",
|
|
768
|
+
"warm_selective": "warm_selective",
|
|
769
|
+
"c": "close_continuous",
|
|
770
|
+
"3": "close_continuous",
|
|
771
|
+
"长期陪伴": "close_continuous",
|
|
772
|
+
"持续记住": "close_continuous",
|
|
773
|
+
"close_continuous": "close_continuous",
|
|
774
|
+
}
|
|
775
|
+
while True:
|
|
776
|
+
value = input(prompt + "\n> ").strip().lower()
|
|
777
|
+
if value in choices:
|
|
778
|
+
return choices[value]
|
|
779
|
+
print("Enter A, B, or C, or use task_only, warm_selective, or close_continuous.")
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _direct_emotional_interaction_from_continuity(continuity):
|
|
783
|
+
return {
|
|
784
|
+
"task_only": "work_only",
|
|
785
|
+
"warm_selective": "some_direct_emotional_interaction",
|
|
786
|
+
"close_continuous": "some_direct_emotional_interaction",
|
|
787
|
+
}[continuity]
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _role_from_answers(user_name, relationship, primary_work):
|
|
791
|
+
work = primary_work[0].rstrip(".")
|
|
792
|
+
owner = "the user's" if user_name == "the user" else f"{user_name}'s"
|
|
793
|
+
return f"{owner} {relationship} for {work}."
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _avoid_from_voice(voice):
|
|
797
|
+
avoid = []
|
|
798
|
+
lowered = voice.lower()
|
|
799
|
+
for marker in ("讨厌", "不要", "not ", "avoid "):
|
|
800
|
+
if marker in lowered:
|
|
801
|
+
avoid.append(voice)
|
|
802
|
+
break
|
|
803
|
+
if not avoid:
|
|
804
|
+
avoid = ["mechanical audit-log replies", "decorative warmth", "over-compliance"]
|
|
805
|
+
return avoid
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _traits_from_voice(voice):
|
|
809
|
+
traits = []
|
|
810
|
+
for item in re.split(r"[,,;;、]+", voice):
|
|
811
|
+
item = item.strip().lower().strip(".")
|
|
812
|
+
if item and len(item) <= 32 and item not in traits:
|
|
813
|
+
traits.append(item)
|
|
814
|
+
if len(traits) == 4:
|
|
815
|
+
break
|
|
816
|
+
return traits or ["steady", "practical", "scope-preserving"]
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _compile_pack(adapter, resolved, references):
|
|
820
|
+
if adapter == "codex":
|
|
821
|
+
return compile_to_codex_pack(resolved, references=references)
|
|
822
|
+
if adapter == "claude-code":
|
|
823
|
+
return compile_to_claude_code_pack(resolved, references=references)
|
|
824
|
+
if adapter == "cursor":
|
|
825
|
+
return compile_to_cursor_pack(resolved, references=references)
|
|
826
|
+
raise PackwrightError(f"unsupported adapter: {adapter}")
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _read_pack(pack_dir):
|
|
830
|
+
manifest = _load_pack_manifest(pack_dir)
|
|
831
|
+
artifacts = manifest.get("artifacts")
|
|
832
|
+
if not isinstance(artifacts, list) or not artifacts:
|
|
833
|
+
raise PackwrightError("adapter pack manifest must contain a non-empty artifacts list")
|
|
834
|
+
pack = {}
|
|
835
|
+
for artifact in artifacts:
|
|
836
|
+
path = resolve_source_path(pack_dir, artifact, "adapter pack artifact")
|
|
837
|
+
pack[artifact] = path.read_text(encoding="utf-8")
|
|
838
|
+
for artifact in _optional_installed_artifacts():
|
|
839
|
+
path = pack_dir / artifact
|
|
840
|
+
if path.exists():
|
|
841
|
+
path = resolve_source_path(pack_dir, artifact, "optional installed artifact")
|
|
842
|
+
pack[artifact] = path.read_text(encoding="utf-8")
|
|
843
|
+
return pack
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _load_pack_manifest(pack_dir):
|
|
847
|
+
try:
|
|
848
|
+
manifest_path = resolve_source_path(pack_dir, "manifest.json", "adapter pack manifest")
|
|
849
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
850
|
+
except OSError as exc:
|
|
851
|
+
raise PackwrightError(f"cannot read adapter pack manifest {pack_dir / 'manifest.json'}: {exc}")
|
|
852
|
+
except json.JSONDecodeError as exc:
|
|
853
|
+
raise PackwrightError(f"invalid adapter pack manifest {pack_dir / 'manifest.json'}: {exc}")
|
|
854
|
+
if not isinstance(manifest, dict):
|
|
855
|
+
raise PackwrightError(f"adapter pack manifest must be a mapping: {pack_dir / 'manifest.json'}")
|
|
856
|
+
return manifest
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _optional_installed_artifacts():
|
|
860
|
+
return EMOTION_ENGINE_CODEX_ARTIFACTS
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _pack_resolved_parameters(pack):
|
|
864
|
+
try:
|
|
865
|
+
manifest = json.loads(pack.get("manifest.json", "{}"))
|
|
866
|
+
except json.JSONDecodeError:
|
|
867
|
+
return {}
|
|
868
|
+
params = manifest.get("resolved_parameters", {})
|
|
869
|
+
return params if isinstance(params, dict) else {}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _write_pack(pack, out_dir, force=False):
|
|
873
|
+
destinations = {
|
|
874
|
+
rel_path: resolve_destination_path(out_dir, rel_path, "pack artifact destination")
|
|
875
|
+
for rel_path in pack
|
|
876
|
+
}
|
|
877
|
+
existing = [rel_path for rel_path, path in destinations.items() if path.exists()]
|
|
878
|
+
if existing and not force:
|
|
879
|
+
raise PackwrightValidationError([
|
|
880
|
+
"pack directory already contains files that would be overwritten; rerun with --force after reviewing them",
|
|
881
|
+
*[f"existing pack artifact: {artifact}" for artifact in sorted(existing)],
|
|
882
|
+
])
|
|
883
|
+
for rel_path, content in pack.items():
|
|
884
|
+
path = destinations[rel_path]
|
|
885
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
886
|
+
path.write_text(content, encoding="utf-8")
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _emit_migration_report(report, args):
|
|
890
|
+
if args.out:
|
|
891
|
+
_write_json(report, Path(args.out))
|
|
892
|
+
return
|
|
893
|
+
if args.json or not sys.stdout.isatty():
|
|
894
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
895
|
+
return
|
|
896
|
+
_print_migration_report(report)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _print_migration_report(report):
|
|
900
|
+
source = report["source"]
|
|
901
|
+
destination = report["destination"]
|
|
902
|
+
print(f"Packwright migration {report['status']}: {source['adapter']} -> {destination['adapter']}")
|
|
903
|
+
print(f" {source['target_dir']} -> {destination['target_dir']}")
|
|
904
|
+
for name in ("generated", "carried", "rewritten", "excluded"):
|
|
905
|
+
items = report["changes"][name]
|
|
906
|
+
print(f" {name}: {len(items)} | {_migration_path_summary(items, exact=name == 'rewritten')}")
|
|
907
|
+
planned = report["score"]["planned"]
|
|
908
|
+
score_line = f" score: planned {planned['score']:.1f} ({'pass' if planned['passed'] else 'fail'})"
|
|
909
|
+
installed = report["score"].get("installed")
|
|
910
|
+
if installed:
|
|
911
|
+
score_line += f" | installed {installed['score']:.1f} ({'pass' if installed['passed'] else 'fail'})"
|
|
912
|
+
integrity = report.get("integrity")
|
|
913
|
+
if integrity:
|
|
914
|
+
score_line += f" | hashes {integrity['checked']} ({'pass' if integrity['passed'] else 'fail'})"
|
|
915
|
+
print(score_line)
|
|
916
|
+
if report.get("conflicts"):
|
|
917
|
+
print(f" conflicts: {_migration_conflict_summary(report['conflicts'])}")
|
|
918
|
+
if report["status"] != "applied":
|
|
919
|
+
print("No files written. Use --json for the complete path-level receipt.")
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def _migration_path_summary(items, exact=False):
|
|
923
|
+
if not items:
|
|
924
|
+
return "none"
|
|
925
|
+
if exact:
|
|
926
|
+
return ", ".join(item["path"] for item in items)
|
|
927
|
+
groups = {}
|
|
928
|
+
for item in items:
|
|
929
|
+
path = item["path"]
|
|
930
|
+
parts = Path(path).parts
|
|
931
|
+
label = f"{parts[0]}/**" if len(parts) > 1 else path
|
|
932
|
+
groups[label] = groups.get(label, 0) + 1
|
|
933
|
+
return " | ".join(
|
|
934
|
+
f"{label} ({count} files)" if label.endswith("/**") else label
|
|
935
|
+
for label, count in sorted(groups.items())
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _migration_conflict_summary(conflicts):
|
|
940
|
+
groups = {}
|
|
941
|
+
for item in conflicts:
|
|
942
|
+
groups.setdefault(item["location"], []).append(item["path"])
|
|
943
|
+
return " | ".join(
|
|
944
|
+
f"{location}: {', '.join(paths)}"
|
|
945
|
+
for location, paths in sorted(groups.items())
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _confirm_migration():
|
|
950
|
+
try:
|
|
951
|
+
answer = input("Apply this migration? [y/N] ").strip().lower()
|
|
952
|
+
except EOFError:
|
|
953
|
+
return False
|
|
954
|
+
return answer in {"y", "yes"}
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _write_json_or_print(data, out):
|
|
958
|
+
if out:
|
|
959
|
+
_write_json(data, Path(out))
|
|
960
|
+
else:
|
|
961
|
+
print(json.dumps(data, indent=2, sort_keys=True))
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _write_json(data, path):
|
|
965
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
966
|
+
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def _write_yaml(data, path):
|
|
970
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
971
|
+
path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
|