ttsd-colabcli 1.0.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/cli.js +148 -0
- package/core/app/__init__.py +0 -0
- package/core/app/colab_cli/__init__.py +0 -0
- package/core/app/colab_cli/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/auth.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/auto_update.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/cli.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/client.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/common.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/console.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/contents.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/history.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/runtime.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/state.cpython-312.pyc +0 -0
- package/core/app/colab_cli/__pycache__/utils.cpython-312.pyc +0 -0
- package/core/app/colab_cli/auth.py +278 -0
- package/core/app/colab_cli/auto_update.py +248 -0
- package/core/app/colab_cli/cli.py +155 -0
- package/core/app/colab_cli/client.py +310 -0
- package/core/app/colab_cli/commands/__init__.py +14 -0
- package/core/app/colab_cli/commands/__pycache__/__init__.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/__pycache__/automation.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/__pycache__/execution.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/__pycache__/files.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/__pycache__/run.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/__pycache__/session.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/__pycache__/utility.cpython-312.pyc +0 -0
- package/core/app/colab_cli/commands/automation.py +265 -0
- package/core/app/colab_cli/commands/execution.py +362 -0
- package/core/app/colab_cli/commands/files.py +204 -0
- package/core/app/colab_cli/commands/run.py +477 -0
- package/core/app/colab_cli/commands/session.py +519 -0
- package/core/app/colab_cli/commands/utility.py +436 -0
- package/core/app/colab_cli/common.py +185 -0
- package/core/app/colab_cli/console.py +172 -0
- package/core/app/colab_cli/contents.py +93 -0
- package/core/app/colab_cli/converter.py +184 -0
- package/core/app/colab_cli/history.py +65 -0
- package/core/app/colab_cli/oauth_config.json +11 -0
- package/core/app/colab_cli/repl.py +173 -0
- package/core/app/colab_cli/runtime.py +262 -0
- package/core/app/colab_cli/state.py +156 -0
- package/core/app/colab_cli/utils.py +85 -0
- package/core/colab/worker.py +679 -0
- package/core/daemon.py +184 -0
- package/core/requirements.txt +8 -0
- package/package.json +22 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import datetime
|
|
16
|
+
import nbformat
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import typer
|
|
21
|
+
import uuid
|
|
22
|
+
from nbformat.v4 import new_output
|
|
23
|
+
from typing import Optional
|
|
24
|
+
from typing_extensions import Annotated
|
|
25
|
+
|
|
26
|
+
from app.colab_cli.runtime import ColabRuntime
|
|
27
|
+
from app.colab_cli.utils import handle_image, is_terminal_error
|
|
28
|
+
from app.colab_cli.console import connect_console
|
|
29
|
+
|
|
30
|
+
TITLE_REGEX = re.compile(r"^\s*#\s*@title\s+(.*)", re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_stdin_tty():
|
|
34
|
+
return sys.stdin.isatty()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save_output(outputs, cell):
|
|
38
|
+
if cell is None:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if not hasattr(cell, "outputs"):
|
|
42
|
+
cell.outputs = []
|
|
43
|
+
else:
|
|
44
|
+
cell.outputs.clear()
|
|
45
|
+
|
|
46
|
+
for out in outputs:
|
|
47
|
+
if out.get("output_type") == "stream":
|
|
48
|
+
cell.outputs.append(
|
|
49
|
+
new_output(
|
|
50
|
+
output_type="stream",
|
|
51
|
+
name=out.get("name", "stdout"),
|
|
52
|
+
text=out.get("text", ""),
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
elif "data" in out:
|
|
56
|
+
output_type = out.get("output_type", "display_data")
|
|
57
|
+
cell.outputs.append(
|
|
58
|
+
new_output(
|
|
59
|
+
output_type=output_type,
|
|
60
|
+
data=out["data"],
|
|
61
|
+
metadata=out.get("metadata", {}),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
elif out.get("output_type") == "error":
|
|
65
|
+
cell.outputs.append(
|
|
66
|
+
new_output(
|
|
67
|
+
output_type="error",
|
|
68
|
+
ename=out.get("ename", "Error"),
|
|
69
|
+
evalue=out.get("evalue", ""),
|
|
70
|
+
traceback=out.get("traceback", []),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def display_output(out, output_image=None):
|
|
76
|
+
if out.get("output_type") == "stream":
|
|
77
|
+
stream = sys.stderr if out.get("name") == "stderr" else sys.stdout
|
|
78
|
+
stream.write(out.get("text", ""))
|
|
79
|
+
stream.flush()
|
|
80
|
+
elif "data" in out:
|
|
81
|
+
data = out["data"]
|
|
82
|
+
if text := data.get("text/plain"):
|
|
83
|
+
typer.echo(text)
|
|
84
|
+
if png := data.get("image/png"):
|
|
85
|
+
handle_image(png, "image/png", target_path=output_image)
|
|
86
|
+
elif jpeg := data.get("image/jpeg"):
|
|
87
|
+
handle_image(jpeg, "image/jpeg", target_path=output_image)
|
|
88
|
+
elif out.get("output_type") == "error":
|
|
89
|
+
tb = out.get("traceback", [])
|
|
90
|
+
if tb:
|
|
91
|
+
sys.stderr.write("".join(tb) + "\n")
|
|
92
|
+
else:
|
|
93
|
+
ename = out.get("ename", "Error")
|
|
94
|
+
evalue = out.get("evalue", "")
|
|
95
|
+
sys.stderr.write(f"{ename}: {evalue}\n")
|
|
96
|
+
else:
|
|
97
|
+
# Ignore silent outputs like metadata or clear_output for streaming
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def exec_command(
|
|
102
|
+
session: Annotated[
|
|
103
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
104
|
+
] = None,
|
|
105
|
+
file: Annotated[
|
|
106
|
+
Optional[str], typer.Option("-f", "--file", help="File to execute")
|
|
107
|
+
] = None,
|
|
108
|
+
output_image: Annotated[
|
|
109
|
+
Optional[str], typer.Option("--output-image", help="Path to save plot")
|
|
110
|
+
] = None,
|
|
111
|
+
timeout: Annotated[
|
|
112
|
+
Optional[float],
|
|
113
|
+
typer.Option("--timeout", help="Timeout in seconds for code execution"),
|
|
114
|
+
] = 30.0,
|
|
115
|
+
):
|
|
116
|
+
"""Execute code in a session"""
|
|
117
|
+
from app.colab_cli.common import state
|
|
118
|
+
|
|
119
|
+
name = state.resolve_session(session)
|
|
120
|
+
s = state.store.get(name)
|
|
121
|
+
if not s:
|
|
122
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
|
|
125
|
+
code_blocks = []
|
|
126
|
+
if file:
|
|
127
|
+
if file.endswith(".ipynb"):
|
|
128
|
+
typer.echo(f"[colab] Parsing notebook '{file}'...")
|
|
129
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
130
|
+
nb = nbformat.read(f, as_version=4)
|
|
131
|
+
for cell in nb.cells:
|
|
132
|
+
# nbformat v4.5+ requires 'id' at the top level
|
|
133
|
+
if not hasattr(cell, "id") or not cell.id:
|
|
134
|
+
cell.id = str(uuid.uuid4())
|
|
135
|
+
|
|
136
|
+
if cell.cell_type == "code":
|
|
137
|
+
code_blocks.append(
|
|
138
|
+
{"code": cell.source, "id": cell.id, "cell": cell}
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
with open(file, "r") as f:
|
|
142
|
+
code_blocks.append({"code": f.read(), "id": None})
|
|
143
|
+
else:
|
|
144
|
+
if is_stdin_tty():
|
|
145
|
+
typer.echo("[colab] Error: No input provided. Pipe code or provide a file.")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
code_blocks.append({"code": sys.stdin.read(), "id": None})
|
|
148
|
+
|
|
149
|
+
if not any(b["code"].strip() for b in code_blocks):
|
|
150
|
+
raise typer.Exit(0)
|
|
151
|
+
|
|
152
|
+
def on_started(kid):
|
|
153
|
+
s.kernel_id = kid
|
|
154
|
+
state.store.add(s)
|
|
155
|
+
|
|
156
|
+
def on_sess_started(sid):
|
|
157
|
+
s.session_id = sid
|
|
158
|
+
state.store.add(s)
|
|
159
|
+
|
|
160
|
+
runtime = ColabRuntime(
|
|
161
|
+
s.url,
|
|
162
|
+
s.token,
|
|
163
|
+
kernel_id=s.kernel_id,
|
|
164
|
+
session_id=s.session_id,
|
|
165
|
+
on_kernel_started=on_started,
|
|
166
|
+
on_session_started=on_sess_started,
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
# Ensure we are in /content which is the standard Colab working directory
|
|
170
|
+
runtime.execute_code(
|
|
171
|
+
"import os; os.makedirs('/content', exist_ok=True); os.chdir('/content')"
|
|
172
|
+
)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
if is_terminal_error(e):
|
|
175
|
+
typer.echo(
|
|
176
|
+
f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
|
|
177
|
+
)
|
|
178
|
+
state.prune_session(name)
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
raise e
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
is_nb = file and file.endswith(".ipynb")
|
|
184
|
+
s.running = f"exec({file or 'stdin'})"
|
|
185
|
+
state.store.add(s)
|
|
186
|
+
|
|
187
|
+
for i, block in enumerate(code_blocks):
|
|
188
|
+
code = block["code"]
|
|
189
|
+
identifier = None
|
|
190
|
+
if is_nb:
|
|
191
|
+
title_match = TITLE_REGEX.search(code)
|
|
192
|
+
if title_match:
|
|
193
|
+
identifier = title_match.group(1).strip()
|
|
194
|
+
elif block.get("id"):
|
|
195
|
+
identifier = block["id"]
|
|
196
|
+
else:
|
|
197
|
+
identifier = ""
|
|
198
|
+
|
|
199
|
+
identifier_str = f" - {identifier}" if identifier else ""
|
|
200
|
+
typer.echo(
|
|
201
|
+
f"[colab] Executing cell {i + 1}/{len(code_blocks)}{identifier_str}..."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
s.last_execution = (
|
|
205
|
+
file or "stdin",
|
|
206
|
+
identifier,
|
|
207
|
+
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
208
|
+
)
|
|
209
|
+
state.store.add(s)
|
|
210
|
+
|
|
211
|
+
outputs = runtime.execute_code(
|
|
212
|
+
code,
|
|
213
|
+
output_hook=lambda o: display_output(o, output_image),
|
|
214
|
+
timeout=timeout,
|
|
215
|
+
)
|
|
216
|
+
if "cell" in block:
|
|
217
|
+
save_output(outputs, block["cell"])
|
|
218
|
+
state.history.log_event(
|
|
219
|
+
name,
|
|
220
|
+
"execution",
|
|
221
|
+
{
|
|
222
|
+
"code": code,
|
|
223
|
+
"outputs": outputs,
|
|
224
|
+
"cell_index": i if len(code_blocks) > 1 else None,
|
|
225
|
+
"cell_id": block.get("id"),
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
finally:
|
|
229
|
+
s.running = None
|
|
230
|
+
state.store.add(s)
|
|
231
|
+
runtime.stop()
|
|
232
|
+
if file and file.endswith(".ipynb"):
|
|
233
|
+
output_file = os.path.splitext(file)[0] + "_output.ipynb"
|
|
234
|
+
typer.echo(f"[colab] Saving notebook with outputs to '{output_file}'...")
|
|
235
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
236
|
+
nbformat.write(nb, f)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def repl(
|
|
240
|
+
session: Annotated[
|
|
241
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
242
|
+
] = None,
|
|
243
|
+
output_image: Annotated[
|
|
244
|
+
Optional[str], typer.Option("--output-image", help="Path to save plot")
|
|
245
|
+
] = None,
|
|
246
|
+
):
|
|
247
|
+
"""Start an interactive REPL"""
|
|
248
|
+
from app.colab_cli.common import state
|
|
249
|
+
|
|
250
|
+
name = state.resolve_session(session)
|
|
251
|
+
s = state.store.get(name)
|
|
252
|
+
if not s:
|
|
253
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
254
|
+
raise typer.Exit(1)
|
|
255
|
+
|
|
256
|
+
def on_started(kid):
|
|
257
|
+
s.kernel_id = kid
|
|
258
|
+
state.store.add(s)
|
|
259
|
+
|
|
260
|
+
def on_sess_started(sid):
|
|
261
|
+
s.session_id = sid
|
|
262
|
+
state.store.add(s)
|
|
263
|
+
|
|
264
|
+
runtime = ColabRuntime(
|
|
265
|
+
s.url,
|
|
266
|
+
s.token,
|
|
267
|
+
kernel_id=s.kernel_id,
|
|
268
|
+
session_id=s.session_id,
|
|
269
|
+
on_kernel_started=on_started,
|
|
270
|
+
on_session_started=on_sess_started,
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
# Ensure we are in /content which is the standard Colab working directory
|
|
274
|
+
runtime.execute_code(
|
|
275
|
+
"import os; os.makedirs('/content', exist_ok=True); os.chdir('/content')"
|
|
276
|
+
)
|
|
277
|
+
except Exception as e:
|
|
278
|
+
if is_terminal_error(e):
|
|
279
|
+
typer.echo(
|
|
280
|
+
f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
|
|
281
|
+
)
|
|
282
|
+
state.prune_session(name)
|
|
283
|
+
raise typer.Exit(1)
|
|
284
|
+
raise e
|
|
285
|
+
|
|
286
|
+
if not is_stdin_tty():
|
|
287
|
+
code = sys.stdin.read()
|
|
288
|
+
if not code.strip():
|
|
289
|
+
raise typer.Exit(0)
|
|
290
|
+
|
|
291
|
+
s.last_execution = (
|
|
292
|
+
"stdin",
|
|
293
|
+
None,
|
|
294
|
+
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
295
|
+
)
|
|
296
|
+
s.running = "repl(stdin)"
|
|
297
|
+
state.store.add(s)
|
|
298
|
+
try:
|
|
299
|
+
outputs = runtime.execute_code(
|
|
300
|
+
code, output_hook=lambda o: display_output(o, output_image)
|
|
301
|
+
)
|
|
302
|
+
state.history.log_event(
|
|
303
|
+
name, "execution", {"code": code, "outputs": outputs, "source": "piped"}
|
|
304
|
+
)
|
|
305
|
+
finally:
|
|
306
|
+
s.running = None
|
|
307
|
+
state.store.add(s)
|
|
308
|
+
runtime.stop()
|
|
309
|
+
else:
|
|
310
|
+
from app.colab_cli.repl import ColabREPL
|
|
311
|
+
|
|
312
|
+
s.running = "repl"
|
|
313
|
+
state.store.add(s)
|
|
314
|
+
try:
|
|
315
|
+
repl_inst = ColabREPL(
|
|
316
|
+
runtime,
|
|
317
|
+
session_name=s.name,
|
|
318
|
+
history_logger=state.history,
|
|
319
|
+
output_image=output_image,
|
|
320
|
+
)
|
|
321
|
+
state.history.log_event(name, "repl_started", {})
|
|
322
|
+
repl_inst.run()
|
|
323
|
+
finally:
|
|
324
|
+
s.running = None
|
|
325
|
+
state.store.add(s)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def console(
|
|
329
|
+
session: Annotated[
|
|
330
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
331
|
+
] = None,
|
|
332
|
+
):
|
|
333
|
+
"""Connect to raw TTY console"""
|
|
334
|
+
from app.colab_cli.common import state
|
|
335
|
+
|
|
336
|
+
name = state.resolve_session(session)
|
|
337
|
+
s = state.store.get(name)
|
|
338
|
+
if not s:
|
|
339
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
340
|
+
raise typer.Exit(1)
|
|
341
|
+
state.history.log_event(s.name, "console_started", {})
|
|
342
|
+
s.running = "console"
|
|
343
|
+
state.store.add(s)
|
|
344
|
+
try:
|
|
345
|
+
connect_console(s)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
if is_terminal_error(e):
|
|
348
|
+
typer.echo(
|
|
349
|
+
f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
|
|
350
|
+
)
|
|
351
|
+
state.prune_session(name)
|
|
352
|
+
raise typer.Exit(1)
|
|
353
|
+
raise e
|
|
354
|
+
finally:
|
|
355
|
+
s.running = None
|
|
356
|
+
state.store.add(s)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def register(app: typer.Typer):
|
|
360
|
+
app.command(name="exec")(exec_command)
|
|
361
|
+
app.command()(repl)
|
|
362
|
+
app.command()(console)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import hashlib
|
|
17
|
+
import os
|
|
18
|
+
import tempfile
|
|
19
|
+
import typer
|
|
20
|
+
from typing import Optional
|
|
21
|
+
from typing_extensions import Annotated
|
|
22
|
+
|
|
23
|
+
from app.colab_cli.contents import ContentsClient
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def ls(
|
|
27
|
+
session: Annotated[
|
|
28
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
29
|
+
] = None,
|
|
30
|
+
path: Annotated[str, typer.Argument(help="Remote path to list")] = "content",
|
|
31
|
+
):
|
|
32
|
+
"""List files in a session"""
|
|
33
|
+
from app.colab_cli.common import state
|
|
34
|
+
|
|
35
|
+
name = state.resolve_session(session)
|
|
36
|
+
s = state.store.get(name)
|
|
37
|
+
if not s:
|
|
38
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
contents = ContentsClient(s)
|
|
41
|
+
try:
|
|
42
|
+
data = contents.list_dir(path)
|
|
43
|
+
state.history.log_event(name, "file_operation", {"op": "ls", "path": path})
|
|
44
|
+
if data.get("type") == "directory":
|
|
45
|
+
items = data.get("content", [])
|
|
46
|
+
for item in sorted(
|
|
47
|
+
items, key=lambda x: (x.get("type") != "directory", x.get("name"))
|
|
48
|
+
):
|
|
49
|
+
suffix = "/" if item.get("type") == "directory" else ""
|
|
50
|
+
typer.echo(f"{item.get('name')}{suffix}")
|
|
51
|
+
else:
|
|
52
|
+
typer.echo(data.get("name"))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
typer.echo(f"[colab] Error: {e}")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def rm(
|
|
59
|
+
session: Annotated[
|
|
60
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
61
|
+
] = None,
|
|
62
|
+
path: Annotated[str, typer.Argument(help="Remote path to remove")] = ...,
|
|
63
|
+
):
|
|
64
|
+
"""Remove a remote file"""
|
|
65
|
+
from app.colab_cli.common import state
|
|
66
|
+
|
|
67
|
+
name = state.resolve_session(session)
|
|
68
|
+
s = state.store.get(name)
|
|
69
|
+
if not s:
|
|
70
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
contents = ContentsClient(s)
|
|
73
|
+
try:
|
|
74
|
+
contents.rm(path)
|
|
75
|
+
state.history.log_event(name, "file_operation", {"op": "rm", "path": path})
|
|
76
|
+
typer.echo(f"[colab] Deleted {path}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
typer.echo(f"[colab] Error: {e}")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def upload(
|
|
83
|
+
session: Annotated[
|
|
84
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
85
|
+
] = None,
|
|
86
|
+
local_path: Annotated[str, typer.Argument(help="Local file to upload")] = ...,
|
|
87
|
+
remote_path: Annotated[str, typer.Argument(help="Remote path to upload to")] = ...,
|
|
88
|
+
):
|
|
89
|
+
"""Upload a file to a session"""
|
|
90
|
+
from app.colab_cli.common import state
|
|
91
|
+
|
|
92
|
+
name = state.resolve_session(session)
|
|
93
|
+
s = state.store.get(name)
|
|
94
|
+
if not s:
|
|
95
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
if not os.path.isfile(local_path):
|
|
98
|
+
typer.echo(f"[colab] Local file '{local_path}' not found.")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
contents = ContentsClient(s)
|
|
101
|
+
try:
|
|
102
|
+
contents.upload(local_path, remote_path)
|
|
103
|
+
state.history.log_event(
|
|
104
|
+
name,
|
|
105
|
+
"file_operation",
|
|
106
|
+
{"op": "upload", "local": local_path, "remote": remote_path},
|
|
107
|
+
)
|
|
108
|
+
typer.echo(f"[colab] Uploaded '{local_path}' to '{remote_path}'")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
typer.echo(f"[colab] Upload failed: {e}")
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def download(
|
|
115
|
+
session: Annotated[
|
|
116
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
117
|
+
] = None,
|
|
118
|
+
remote_path: Annotated[
|
|
119
|
+
str, typer.Argument(help="Remote path to download from")
|
|
120
|
+
] = ...,
|
|
121
|
+
local_path: Annotated[
|
|
122
|
+
str, typer.Argument(help="Local path to save the file")
|
|
123
|
+
] = ...,
|
|
124
|
+
):
|
|
125
|
+
"""Download a file from a session"""
|
|
126
|
+
from app.colab_cli.common import state
|
|
127
|
+
|
|
128
|
+
name = state.resolve_session(session)
|
|
129
|
+
s = state.store.get(name)
|
|
130
|
+
if not s:
|
|
131
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
contents = ContentsClient(s)
|
|
134
|
+
try:
|
|
135
|
+
contents.download(remote_path, local_path)
|
|
136
|
+
state.history.log_event(
|
|
137
|
+
name,
|
|
138
|
+
"file_operation",
|
|
139
|
+
{"op": "download", "remote": remote_path, "local": local_path},
|
|
140
|
+
)
|
|
141
|
+
typer.echo(f"[colab] Downloaded '{remote_path}' to '{local_path}'")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
typer.echo(f"[colab] Download failed: {e}")
|
|
144
|
+
raise typer.Exit(1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def edit(
|
|
148
|
+
session: Annotated[
|
|
149
|
+
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
150
|
+
] = None,
|
|
151
|
+
remote_path: Annotated[str, typer.Argument(help="Remote path to edit")] = ...,
|
|
152
|
+
):
|
|
153
|
+
"""Edit a file on a running Colab session"""
|
|
154
|
+
from app.colab_cli.common import state
|
|
155
|
+
|
|
156
|
+
name = state.resolve_session(session)
|
|
157
|
+
s = state.store.get(name)
|
|
158
|
+
if not s:
|
|
159
|
+
typer.echo(f"[colab] Session '{name}' not found.")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
contents = ContentsClient(s)
|
|
163
|
+
|
|
164
|
+
def get_file_hash(path):
|
|
165
|
+
if not os.path.exists(path):
|
|
166
|
+
return None
|
|
167
|
+
with open(path, "rb") as f:
|
|
168
|
+
return hashlib.file_digest(f, "sha256").hexdigest()
|
|
169
|
+
|
|
170
|
+
_, ext = os.path.splitext(remote_path)
|
|
171
|
+
|
|
172
|
+
with tempfile.NamedTemporaryFile(suffix=ext) as tf:
|
|
173
|
+
local_path = tf.name
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
contents.download(remote_path, local_path)
|
|
177
|
+
except Exception:
|
|
178
|
+
# If download fails, assume file doesn't exist and start empty
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
hash_before = get_file_hash(local_path)
|
|
182
|
+
|
|
183
|
+
click.edit(filename=local_path)
|
|
184
|
+
|
|
185
|
+
hash_after = get_file_hash(local_path)
|
|
186
|
+
|
|
187
|
+
if hash_after != hash_before:
|
|
188
|
+
contents.upload(local_path, remote_path)
|
|
189
|
+
state.history.log_event(
|
|
190
|
+
name,
|
|
191
|
+
"file_operation",
|
|
192
|
+
{"op": "edit", "remote": remote_path},
|
|
193
|
+
)
|
|
194
|
+
typer.echo(f"[colab] Edited and uploaded '{remote_path}'")
|
|
195
|
+
else:
|
|
196
|
+
typer.echo(f"[colab] No changes made to '{remote_path}'")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def register(app: typer.Typer):
|
|
200
|
+
app.command()(ls)
|
|
201
|
+
app.command()(rm)
|
|
202
|
+
app.command()(upload)
|
|
203
|
+
app.command()(download)
|
|
204
|
+
app.command()(edit)
|