cdx-manager 0.12.4 → 0.15.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 +214 -19
- package/changelogs/CHANGELOGS_0_14_0.md +146 -0
- package/changelogs/CHANGELOGS_0_15_0.md +139 -0
- package/checksums/release-archives.json +12 -0
- package/package.json +3 -3
- package/pyproject.toml +1 -1
- package/src/cli.py +64 -4
- package/src/cli_args.py +324 -39
- package/src/cli_commands.py +251 -3049
- package/src/cli_helpers.py +190 -2
- package/src/cli_render.py +17 -0
- package/src/commands/__init__.py +6 -0
- package/src/commands/auth.py +224 -0
- package/src/commands/backup.py +112 -0
- package/src/commands/context_memory.py +341 -0
- package/src/commands/launch.py +218 -0
- package/src/commands/lifecycle.py +154 -0
- package/src/commands/maintenance.py +723 -0
- package/src/commands/runs.py +849 -0
- package/src/commands/settings.py +107 -0
- package/src/commands/status.py +713 -0
- package/src/config.py +70 -0
- package/src/errors.py +17 -0
- package/src/health.py +151 -3
- package/src/notify.py +11 -5
- package/src/provider_background.py +263 -0
- package/src/provider_runtime.py +142 -15
- package/src/run_command.py +112 -4
- package/src/run_failover.py +221 -0
- package/src/run_registry.py +127 -2
- package/src/session_backup.py +343 -0
- package/src/session_helpers.py +179 -0
- package/src/session_ranking.py +259 -0
- package/src/session_service.py +698 -1426
- package/src/session_status.py +550 -0
- package/src/status_source.py +45 -0
- package/src/status_view.py +15 -34
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Context and memory domain commands: context, memory.
|
|
2
|
+
|
|
3
|
+
Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from ..cli_args import CONTEXT_USAGE, MEMORY_USAGE, _parse_json_flag
|
|
10
|
+
from ..cli_helpers import _format_bytes, _json_success, _write_json
|
|
11
|
+
from ..cli_render import _dim, _pad_table, _success
|
|
12
|
+
from ..context_store import (
|
|
13
|
+
append_context,
|
|
14
|
+
append_context_path,
|
|
15
|
+
clear_context,
|
|
16
|
+
clear_context_path,
|
|
17
|
+
edit_context,
|
|
18
|
+
edit_context_path,
|
|
19
|
+
get_context_path,
|
|
20
|
+
get_global_context_path,
|
|
21
|
+
get_named_project_context_path,
|
|
22
|
+
init_context,
|
|
23
|
+
init_context_path,
|
|
24
|
+
list_named_project_contexts,
|
|
25
|
+
read_context,
|
|
26
|
+
read_context_path,
|
|
27
|
+
write_context,
|
|
28
|
+
write_context_path,
|
|
29
|
+
)
|
|
30
|
+
from ..errors import CdxError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_context(rest, ctx):
|
|
34
|
+
json_flag, args = _parse_json_flag(rest)
|
|
35
|
+
if not args:
|
|
36
|
+
args = ["show"]
|
|
37
|
+
action = args[0]
|
|
38
|
+
base_dir = ctx["service"]["base_dir"]
|
|
39
|
+
cwd = ctx.get("cwd")
|
|
40
|
+
|
|
41
|
+
if action == "show":
|
|
42
|
+
if len(args) != 1:
|
|
43
|
+
raise CdxError(CONTEXT_USAGE)
|
|
44
|
+
content = read_context(base_dir, cwd)
|
|
45
|
+
payload = {
|
|
46
|
+
"path": get_context_path(base_dir, cwd),
|
|
47
|
+
"exists": bool(content.strip()),
|
|
48
|
+
"content": content,
|
|
49
|
+
}
|
|
50
|
+
if json_flag:
|
|
51
|
+
_write_json(ctx, _json_success("context.show", "Loaded shared context", context=payload))
|
|
52
|
+
return 0
|
|
53
|
+
if content.strip():
|
|
54
|
+
ctx["out"](content if content.endswith("\n") else f"{content}\n")
|
|
55
|
+
else:
|
|
56
|
+
ctx["out"](f"{_dim('No shared context for this workspace. Run: cdx context init or cdx context set <text>', ctx['use_color'])}\n")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
if action == "path":
|
|
60
|
+
if len(args) != 1:
|
|
61
|
+
raise CdxError(CONTEXT_USAGE)
|
|
62
|
+
path = get_context_path(base_dir, cwd)
|
|
63
|
+
if json_flag:
|
|
64
|
+
_write_json(ctx, _json_success("context.path", "Resolved shared context path", path=path))
|
|
65
|
+
return 0
|
|
66
|
+
ctx["out"](f"{path}\n")
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
if action == "init":
|
|
70
|
+
if len(args) != 1:
|
|
71
|
+
raise CdxError(CONTEXT_USAGE)
|
|
72
|
+
result = init_context(base_dir, cwd)
|
|
73
|
+
message = "Created shared context" if result.get("created") else "Shared context already exists"
|
|
74
|
+
if json_flag:
|
|
75
|
+
_write_json(ctx, _json_success("context.init", message, context=result))
|
|
76
|
+
return 0
|
|
77
|
+
text = f"{message}: {result['path']}"
|
|
78
|
+
ctx["out"](f"{_success(text, ctx['use_color'])}\n")
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
if action == "edit":
|
|
82
|
+
if len(args) != 1:
|
|
83
|
+
raise CdxError(CONTEXT_USAGE)
|
|
84
|
+
result = edit_context(
|
|
85
|
+
base_dir,
|
|
86
|
+
cwd,
|
|
87
|
+
env=ctx.get("env"),
|
|
88
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
89
|
+
)
|
|
90
|
+
if json_flag:
|
|
91
|
+
_write_json(ctx, _json_success("context.edit", "Edited shared context", context=result))
|
|
92
|
+
return 0
|
|
93
|
+
text = f"Edited shared context: {result['path']}"
|
|
94
|
+
ctx["out"](f"{_success(text, ctx['use_color'])}\n")
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
if action == "clear":
|
|
98
|
+
if len(args) != 1:
|
|
99
|
+
raise CdxError(CONTEXT_USAGE)
|
|
100
|
+
result = clear_context(base_dir, cwd)
|
|
101
|
+
message = "Cleared shared context" if result["removed"] else "No shared context to clear"
|
|
102
|
+
if json_flag:
|
|
103
|
+
_write_json(ctx, _json_success("context.clear", message, context=result))
|
|
104
|
+
return 0
|
|
105
|
+
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
if action == "set":
|
|
109
|
+
content_args = args[1:]
|
|
110
|
+
if content_args:
|
|
111
|
+
content = " ".join(content_args)
|
|
112
|
+
else:
|
|
113
|
+
stdin = ctx["options"].get("stdin_data")
|
|
114
|
+
if stdin is None and not ctx.get("stdin_is_tty"):
|
|
115
|
+
stdin = sys.stdin.read()
|
|
116
|
+
if stdin is None:
|
|
117
|
+
raise CdxError("Usage: cdx context set <text> [--json]")
|
|
118
|
+
content = stdin
|
|
119
|
+
result = write_context(base_dir, content, cwd)
|
|
120
|
+
if json_flag:
|
|
121
|
+
_write_json(ctx, _json_success("context.set", "Saved shared context", context=result))
|
|
122
|
+
return 0
|
|
123
|
+
text = f"Saved shared context: {result['path']}"
|
|
124
|
+
ctx["out"](f"{_success(text, ctx['use_color'])}\n")
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
if action == "append":
|
|
128
|
+
note = " ".join(args[1:])
|
|
129
|
+
if not note.strip():
|
|
130
|
+
raise CdxError("Usage: cdx context append <text> [--json]")
|
|
131
|
+
result = append_context(base_dir, note, cwd)
|
|
132
|
+
if json_flag:
|
|
133
|
+
_write_json(ctx, _json_success("context.append", "Appended shared context", context=result))
|
|
134
|
+
return 0
|
|
135
|
+
text = f"Appended shared context: {result['path']}"
|
|
136
|
+
ctx["out"](f"{_success(text, ctx['use_color'])}\n")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
raise CdxError(CONTEXT_USAGE)
|
|
140
|
+
|
|
141
|
+
def _parse_memory_args(rest):
|
|
142
|
+
json_flag, args = _parse_json_flag(rest)
|
|
143
|
+
scope = "current"
|
|
144
|
+
project = None
|
|
145
|
+
cleaned = []
|
|
146
|
+
index = 0
|
|
147
|
+
while index < len(args):
|
|
148
|
+
arg = args[index]
|
|
149
|
+
if arg == "--global":
|
|
150
|
+
if scope != "current":
|
|
151
|
+
raise CdxError(MEMORY_USAGE)
|
|
152
|
+
scope = "global"
|
|
153
|
+
index += 1
|
|
154
|
+
continue
|
|
155
|
+
if arg == "--project":
|
|
156
|
+
if scope != "current" or index + 1 >= len(args) or not args[index + 1].strip():
|
|
157
|
+
raise CdxError(MEMORY_USAGE)
|
|
158
|
+
scope = "project"
|
|
159
|
+
project = args[index + 1].strip()
|
|
160
|
+
index += 2
|
|
161
|
+
continue
|
|
162
|
+
if arg.startswith("--project="):
|
|
163
|
+
if scope != "current":
|
|
164
|
+
raise CdxError(MEMORY_USAGE)
|
|
165
|
+
project = arg.split("=", 1)[1].strip()
|
|
166
|
+
if not project:
|
|
167
|
+
raise CdxError(MEMORY_USAGE)
|
|
168
|
+
scope = "project"
|
|
169
|
+
index += 1
|
|
170
|
+
continue
|
|
171
|
+
cleaned.append(arg)
|
|
172
|
+
index += 1
|
|
173
|
+
if not cleaned:
|
|
174
|
+
cleaned = ["show"]
|
|
175
|
+
if cleaned[0] == "view":
|
|
176
|
+
cleaned[0] = "show"
|
|
177
|
+
return json_flag, scope, project, cleaned
|
|
178
|
+
|
|
179
|
+
def _memory_target(base_dir, cwd, scope, project):
|
|
180
|
+
if scope == "global":
|
|
181
|
+
return {"scope": "global", "path": get_global_context_path(base_dir)}
|
|
182
|
+
if scope == "project":
|
|
183
|
+
if os.path.exists(project):
|
|
184
|
+
return {
|
|
185
|
+
"scope": "project",
|
|
186
|
+
"project": project,
|
|
187
|
+
"project_kind": "path",
|
|
188
|
+
"path": get_context_path(base_dir, project),
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
"scope": "project",
|
|
192
|
+
"project": project,
|
|
193
|
+
"project_kind": "name",
|
|
194
|
+
"path": get_named_project_context_path(base_dir, project),
|
|
195
|
+
}
|
|
196
|
+
return {"scope": "current", "path": get_context_path(base_dir, cwd)}
|
|
197
|
+
|
|
198
|
+
def _memory_payload(target, content=None):
|
|
199
|
+
payload = {
|
|
200
|
+
"scope": target["scope"],
|
|
201
|
+
"path": target["path"],
|
|
202
|
+
"exists": os.path.isfile(target["path"]),
|
|
203
|
+
}
|
|
204
|
+
if target.get("project") is not None:
|
|
205
|
+
payload["project"] = target["project"]
|
|
206
|
+
payload["project_kind"] = target.get("project_kind")
|
|
207
|
+
if payload["exists"]:
|
|
208
|
+
payload["bytes"] = os.path.getsize(target["path"])
|
|
209
|
+
if content is not None:
|
|
210
|
+
payload["content"] = content
|
|
211
|
+
return payload
|
|
212
|
+
|
|
213
|
+
def _list_memory_entries(base_dir, cwd):
|
|
214
|
+
entries = []
|
|
215
|
+
global_path = get_global_context_path(base_dir)
|
|
216
|
+
if os.path.isfile(global_path):
|
|
217
|
+
entries.append({"scope": "global", "path": global_path, "bytes": os.path.getsize(global_path)})
|
|
218
|
+
entries.extend(list_named_project_contexts(base_dir))
|
|
219
|
+
current_path = get_context_path(base_dir, cwd)
|
|
220
|
+
if os.path.isfile(current_path):
|
|
221
|
+
entries.append({"scope": "current", "path": current_path, "bytes": os.path.getsize(current_path)})
|
|
222
|
+
return entries
|
|
223
|
+
|
|
224
|
+
def handle_memory(rest, ctx):
|
|
225
|
+
json_flag, scope, project, args = _parse_memory_args(rest)
|
|
226
|
+
action = args[0]
|
|
227
|
+
base_dir = ctx["service"]["base_dir"]
|
|
228
|
+
cwd = ctx.get("cwd")
|
|
229
|
+
|
|
230
|
+
if action == "list":
|
|
231
|
+
if len(args) != 1 or scope != "current":
|
|
232
|
+
raise CdxError(MEMORY_USAGE)
|
|
233
|
+
entries = _list_memory_entries(base_dir, cwd)
|
|
234
|
+
if json_flag:
|
|
235
|
+
_write_json(ctx, _json_success("memory.list", "Listed memory scopes", memories=entries))
|
|
236
|
+
return 0
|
|
237
|
+
if not entries:
|
|
238
|
+
ctx["out"](f"{_dim('No cdx memory files found.', ctx['use_color'])}\n")
|
|
239
|
+
return 0
|
|
240
|
+
rows = [["SCOPE", "PROJECT", "SIZE", "PATH"]]
|
|
241
|
+
for entry in entries:
|
|
242
|
+
rows.append([
|
|
243
|
+
entry["scope"],
|
|
244
|
+
entry.get("project") or "-",
|
|
245
|
+
_format_bytes(entry.get("bytes") or 0),
|
|
246
|
+
entry["path"],
|
|
247
|
+
])
|
|
248
|
+
ctx["out"](_pad_table(rows) + "\n")
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
target = _memory_target(base_dir, cwd, scope, project)
|
|
252
|
+
path = target["path"]
|
|
253
|
+
|
|
254
|
+
if action == "show":
|
|
255
|
+
if len(args) != 1:
|
|
256
|
+
raise CdxError(MEMORY_USAGE)
|
|
257
|
+
content = read_context_path(path)
|
|
258
|
+
payload = _memory_payload(target, content=content)
|
|
259
|
+
if json_flag:
|
|
260
|
+
_write_json(ctx, _json_success("memory.show", "Loaded memory", memory=payload))
|
|
261
|
+
return 0
|
|
262
|
+
if content.strip():
|
|
263
|
+
ctx["out"](content if content.endswith("\n") else f"{content}\n")
|
|
264
|
+
else:
|
|
265
|
+
ctx["out"](f"{_dim('No memory for this scope. Run: cdx memory append <text>', ctx['use_color'])}\n")
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
if action == "path":
|
|
269
|
+
if len(args) != 1:
|
|
270
|
+
raise CdxError(MEMORY_USAGE)
|
|
271
|
+
if json_flag:
|
|
272
|
+
_write_json(ctx, _json_success("memory.path", "Resolved memory path", memory=_memory_payload(target)))
|
|
273
|
+
return 0
|
|
274
|
+
ctx["out"](f"{path}\n")
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
if action == "init":
|
|
278
|
+
if len(args) != 1:
|
|
279
|
+
raise CdxError(MEMORY_USAGE)
|
|
280
|
+
result = init_context_path(path)
|
|
281
|
+
result = {**result, **{key: value for key, value in target.items() if key != "path"}}
|
|
282
|
+
message = "Created memory" if result.get("created") else "Memory already exists"
|
|
283
|
+
if json_flag:
|
|
284
|
+
_write_json(ctx, _json_success("memory.init", message, memory=result))
|
|
285
|
+
return 0
|
|
286
|
+
ctx["out"](f"{_success(f'{message}: {path}', ctx['use_color'])}\n")
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
if action == "edit":
|
|
290
|
+
if len(args) != 1:
|
|
291
|
+
raise CdxError(MEMORY_USAGE)
|
|
292
|
+
result = edit_context_path(path, env=ctx.get("env"), spawn_sync=ctx.get("spawn_sync"))
|
|
293
|
+
result = {**result, **{key: value for key, value in target.items() if key != "path"}}
|
|
294
|
+
if json_flag:
|
|
295
|
+
_write_json(ctx, _json_success("memory.edit", "Edited memory", memory=result))
|
|
296
|
+
return 0
|
|
297
|
+
ctx["out"](f"{_success(f'Edited memory: {path}', ctx['use_color'])}\n")
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
if action == "clear":
|
|
301
|
+
if len(args) != 1:
|
|
302
|
+
raise CdxError(MEMORY_USAGE)
|
|
303
|
+
result = clear_context_path(path)
|
|
304
|
+
result = {**result, **{key: value for key, value in target.items() if key != "path"}}
|
|
305
|
+
message = "Cleared memory" if result["removed"] else "No memory to clear"
|
|
306
|
+
if json_flag:
|
|
307
|
+
_write_json(ctx, _json_success("memory.clear", message, memory=result))
|
|
308
|
+
return 0
|
|
309
|
+
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
if action == "set":
|
|
313
|
+
content_args = args[1:]
|
|
314
|
+
if content_args:
|
|
315
|
+
content = " ".join(content_args)
|
|
316
|
+
else:
|
|
317
|
+
stdin = ctx["options"].get("stdin_data")
|
|
318
|
+
if stdin is None and not ctx.get("stdin_is_tty"):
|
|
319
|
+
stdin = sys.stdin.read()
|
|
320
|
+
if stdin is None:
|
|
321
|
+
raise CdxError("Usage: cdx memory set <text> [--json]")
|
|
322
|
+
content = stdin
|
|
323
|
+
result = write_context_path(path, content)
|
|
324
|
+
result = {**result, **{key: value for key, value in target.items() if key != "path"}}
|
|
325
|
+
if json_flag:
|
|
326
|
+
_write_json(ctx, _json_success("memory.set", "Saved memory", memory=result))
|
|
327
|
+
return 0
|
|
328
|
+
ctx["out"](f"{_success(f'Saved memory: {path}', ctx['use_color'])}\n")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
if action == "append":
|
|
332
|
+
note = " ".join(args[1:])
|
|
333
|
+
result = append_context_path(path, note)
|
|
334
|
+
result = {**result, **{key: value for key, value in target.items() if key != "path"}}
|
|
335
|
+
if json_flag:
|
|
336
|
+
_write_json(ctx, _json_success("memory.append", "Appended memory", memory=result))
|
|
337
|
+
return 0
|
|
338
|
+
ctx["out"](f"{_success(f'Appended memory: {path}', ctx['use_color'])}\n")
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
raise CdxError(MEMORY_USAGE)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Launch domain commands: launch, resume, can-resume, handoff.
|
|
2
|
+
|
|
3
|
+
Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
|
|
9
|
+
from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag
|
|
10
|
+
from ..cli_helpers import (
|
|
11
|
+
API_SCHEMA_VERSION,
|
|
12
|
+
_build_handoff_context,
|
|
13
|
+
_handoff_launch_prompt,
|
|
14
|
+
_json_success,
|
|
15
|
+
_latest_handoff_transcript_path,
|
|
16
|
+
_read_handoff_transcript,
|
|
17
|
+
_resume_capability_for_session,
|
|
18
|
+
_update_notice_warnings,
|
|
19
|
+
_warn_if_session_already_running,
|
|
20
|
+
_write_json,
|
|
21
|
+
_write_update_notice,
|
|
22
|
+
)
|
|
23
|
+
from ..cli_render import _dim, _info, _success, _warn
|
|
24
|
+
from ..context_store import install_context_for_session, write_context
|
|
25
|
+
from ..errors import CdxError
|
|
26
|
+
from ..provider_runtime import _ensure_session_authentication, _run_interactive_provider_command
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _format_resume_capability(capability, use_color=False):
|
|
30
|
+
name = capability["session"]
|
|
31
|
+
provider = capability["provider"]
|
|
32
|
+
if capability["resumable"]:
|
|
33
|
+
preview = shlex.join(capability.get("command_preview") or [])
|
|
34
|
+
detail = f"({provider}, {capability['strategy']})"
|
|
35
|
+
return (
|
|
36
|
+
f"{_success(f'{name} can resume', use_color)} "
|
|
37
|
+
f"{_dim(detail, use_color)}\n"
|
|
38
|
+
f"{_dim(preview, use_color)}"
|
|
39
|
+
)
|
|
40
|
+
reason = capability.get("reason") or "not_supported"
|
|
41
|
+
return (
|
|
42
|
+
f"{_warn(f'{name} cannot resume', use_color)} "
|
|
43
|
+
f"{_dim(f'({provider}, {reason})', use_color)}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def handle_can_resume(rest, ctx):
|
|
47
|
+
json_flag, args = _parse_json_flag(rest)
|
|
48
|
+
if len(args) != 1:
|
|
49
|
+
raise CdxError(CAN_RESUME_USAGE)
|
|
50
|
+
session = ctx["service"]["get_session"](args[0])
|
|
51
|
+
if not session:
|
|
52
|
+
raise CdxError(f"Unknown session: {args[0]}")
|
|
53
|
+
if session.get("enabled", True) is False:
|
|
54
|
+
capability = {
|
|
55
|
+
"session": session["name"],
|
|
56
|
+
"provider": session["provider"],
|
|
57
|
+
"resumable": False,
|
|
58
|
+
"strategy": "session_disabled",
|
|
59
|
+
"reason": "session_disabled",
|
|
60
|
+
"command_preview": [],
|
|
61
|
+
}
|
|
62
|
+
else:
|
|
63
|
+
capability = _resume_capability_for_session(session, ctx)
|
|
64
|
+
if json_flag:
|
|
65
|
+
_write_json(ctx, {
|
|
66
|
+
"schema_version": API_SCHEMA_VERSION,
|
|
67
|
+
"ok": True,
|
|
68
|
+
**capability,
|
|
69
|
+
})
|
|
70
|
+
return 0
|
|
71
|
+
ctx["out"](f"{_format_resume_capability(capability, ctx['use_color'])}\n")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
def handle_resume(rest, ctx):
|
|
75
|
+
json_flag, args = _parse_json_flag(rest)
|
|
76
|
+
if len(args) != 1:
|
|
77
|
+
raise CdxError(RESUME_USAGE)
|
|
78
|
+
return handle_launch(args[0], ctx, resume=True, force_json=json_flag)
|
|
79
|
+
|
|
80
|
+
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None):
|
|
81
|
+
json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
|
|
82
|
+
if force_json is not None:
|
|
83
|
+
json_flag = force_json
|
|
84
|
+
warnings = _update_notice_warnings(ctx)
|
|
85
|
+
_warn_if_session_already_running(command, ctx)
|
|
86
|
+
session = ctx["service"]["launch_session"](command)
|
|
87
|
+
capability = _resume_capability_for_session(session, ctx) if resume else None
|
|
88
|
+
if capability and not capability["resumable"]:
|
|
89
|
+
raise CdxError(
|
|
90
|
+
f"Provider {session['provider']} does not support native resume through cdx."
|
|
91
|
+
)
|
|
92
|
+
_ensure_session_authentication(
|
|
93
|
+
session,
|
|
94
|
+
ctx["service"],
|
|
95
|
+
spawn=ctx.get("spawn"),
|
|
96
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
97
|
+
env_override=ctx.get("env"),
|
|
98
|
+
stdin_is_tty=ctx["stdin_is_tty"],
|
|
99
|
+
behavior="launch",
|
|
100
|
+
signal_emitter=ctx.get("signal_emitter"),
|
|
101
|
+
trust_local_credentials=False,
|
|
102
|
+
)
|
|
103
|
+
message = (
|
|
104
|
+
f"Resuming {session['provider']} session {session['name']}"
|
|
105
|
+
if resume else
|
|
106
|
+
f"Launching {session['provider']} session {session['name']}"
|
|
107
|
+
)
|
|
108
|
+
if not json_flag:
|
|
109
|
+
ctx["out"](f"{_info(message, ctx['use_color'])}\n")
|
|
110
|
+
_write_update_notice(ctx)
|
|
111
|
+
cwd = ctx.get("cwd") or os.getcwd()
|
|
112
|
+
runtime_run_id = None
|
|
113
|
+
|
|
114
|
+
def runtime_lifecycle(event, info):
|
|
115
|
+
nonlocal runtime_run_id
|
|
116
|
+
if event == "started":
|
|
117
|
+
runtime = ctx["service"]["start_session_runtime"](session["name"], info)
|
|
118
|
+
runtime_run_id = runtime.get("runId")
|
|
119
|
+
elif event == "finished" and runtime_run_id:
|
|
120
|
+
ctx["service"]["finish_session_runtime"](
|
|
121
|
+
session["name"],
|
|
122
|
+
runtime_run_id,
|
|
123
|
+
{"status": "stopped", "returncode": info.get("returncode")},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
run_info = _run_interactive_provider_command(
|
|
128
|
+
session, "resume" if resume else "launch", spawn=ctx.get("spawn"), cwd=cwd, env_override=ctx.get("env"),
|
|
129
|
+
signal_emitter=ctx.get("signal_emitter"), initial_prompt=initial_prompt,
|
|
130
|
+
lifecycle_callback=runtime_lifecycle,
|
|
131
|
+
)
|
|
132
|
+
except CdxError as error:
|
|
133
|
+
run_info = getattr(error, "run_info", {}) or {}
|
|
134
|
+
if runtime_run_id:
|
|
135
|
+
ctx["service"]["finish_session_runtime"](
|
|
136
|
+
session["name"],
|
|
137
|
+
runtime_run_id,
|
|
138
|
+
{"status": "failed", "returncode": run_info.get("returncode")},
|
|
139
|
+
)
|
|
140
|
+
if run_info:
|
|
141
|
+
ctx["service"]["record_launch_history"](session["name"], {
|
|
142
|
+
"status": "failed",
|
|
143
|
+
"cwd": cwd,
|
|
144
|
+
"error": str(error),
|
|
145
|
+
"exit_code": error.exit_code,
|
|
146
|
+
**run_info,
|
|
147
|
+
})
|
|
148
|
+
raise
|
|
149
|
+
ctx["service"]["record_launch_history"](session["name"], {
|
|
150
|
+
"status": "success",
|
|
151
|
+
"action": "resume" if resume else "launch",
|
|
152
|
+
"cwd": cwd,
|
|
153
|
+
"exit_code": 0,
|
|
154
|
+
**run_info,
|
|
155
|
+
})
|
|
156
|
+
if json_flag:
|
|
157
|
+
extra = {"session": ctx["service"]["get_session"](session["name"])}
|
|
158
|
+
if capability:
|
|
159
|
+
extra["resume"] = capability
|
|
160
|
+
_write_json(ctx, _json_success("resume" if resume else "launch", message, warnings=warnings, **extra))
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
def handle_handoff(rest, ctx):
|
|
164
|
+
json_flag, args = _parse_json_flag(rest)
|
|
165
|
+
if len(args) not in (1, 2):
|
|
166
|
+
raise CdxError(HANDOFF_USAGE)
|
|
167
|
+
if len(args) == 1:
|
|
168
|
+
name = args[0]
|
|
169
|
+
session = ctx["service"]["get_session"](name)
|
|
170
|
+
if not session:
|
|
171
|
+
raise CdxError(f"Unknown session: {name}")
|
|
172
|
+
install = install_context_for_session(ctx["service"]["base_dir"], session, ctx.get("cwd"))
|
|
173
|
+
launch_prompt = _handoff_launch_prompt(session, install)
|
|
174
|
+
if json_flag:
|
|
175
|
+
_write_json(ctx, _json_success(
|
|
176
|
+
"handoff",
|
|
177
|
+
f"Installed shared context for {name}",
|
|
178
|
+
context=install,
|
|
179
|
+
launch_prompt=launch_prompt,
|
|
180
|
+
session=session,
|
|
181
|
+
))
|
|
182
|
+
return 0
|
|
183
|
+
text = f"Shared context installed for {name}: {install['target_path']}"
|
|
184
|
+
ctx["out"](f"{_info(text, ctx['use_color'])}\n")
|
|
185
|
+
return handle_launch(name, ctx, initial_prompt=launch_prompt)
|
|
186
|
+
|
|
187
|
+
source_name, target_name = args
|
|
188
|
+
if source_name == target_name:
|
|
189
|
+
raise CdxError("Source and target sessions must be different")
|
|
190
|
+
source = ctx["service"]["get_session"](source_name)
|
|
191
|
+
if not source:
|
|
192
|
+
raise CdxError(f"Unknown session: {source_name}")
|
|
193
|
+
target = ctx["service"]["get_session"](target_name)
|
|
194
|
+
if not target:
|
|
195
|
+
raise CdxError(f"Unknown session: {target_name}")
|
|
196
|
+
transcript_path = _latest_handoff_transcript_path(source)
|
|
197
|
+
if not transcript_path:
|
|
198
|
+
raise CdxError(f"No transcript found for session: {source_name}")
|
|
199
|
+
transcript, truncated = _read_handoff_transcript(transcript_path)
|
|
200
|
+
context = _build_handoff_context(source, target, transcript_path, transcript, truncated=truncated)
|
|
201
|
+
write_result = write_context(ctx["service"]["base_dir"], context, ctx.get("cwd"))
|
|
202
|
+
install = install_context_for_session(ctx["service"]["base_dir"], target, ctx.get("cwd"))
|
|
203
|
+
launch_prompt = _handoff_launch_prompt(target, install)
|
|
204
|
+
if json_flag:
|
|
205
|
+
_write_json(ctx, _json_success(
|
|
206
|
+
"handoff",
|
|
207
|
+
f"Prepared handoff from {source_name} to {target_name}",
|
|
208
|
+
context=install,
|
|
209
|
+
source_session=source,
|
|
210
|
+
target_session=target,
|
|
211
|
+
source_transcript=transcript_path,
|
|
212
|
+
shared_context=write_result,
|
|
213
|
+
launch_prompt=launch_prompt,
|
|
214
|
+
))
|
|
215
|
+
return 0
|
|
216
|
+
text = f"Handoff prepared from {source_name} to {target_name}: {install['target_path']}"
|
|
217
|
+
ctx["out"](f"{_info(text, ctx['use_color'])}\n")
|
|
218
|
+
return handle_launch(target_name, ctx, initial_prompt=launch_prompt)
|