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.
Files changed (47) hide show
  1. package/cli.js +148 -0
  2. package/core/app/__init__.py +0 -0
  3. package/core/app/colab_cli/__init__.py +0 -0
  4. package/core/app/colab_cli/__pycache__/__init__.cpython-312.pyc +0 -0
  5. package/core/app/colab_cli/__pycache__/auth.cpython-312.pyc +0 -0
  6. package/core/app/colab_cli/__pycache__/auto_update.cpython-312.pyc +0 -0
  7. package/core/app/colab_cli/__pycache__/cli.cpython-312.pyc +0 -0
  8. package/core/app/colab_cli/__pycache__/client.cpython-312.pyc +0 -0
  9. package/core/app/colab_cli/__pycache__/common.cpython-312.pyc +0 -0
  10. package/core/app/colab_cli/__pycache__/console.cpython-312.pyc +0 -0
  11. package/core/app/colab_cli/__pycache__/contents.cpython-312.pyc +0 -0
  12. package/core/app/colab_cli/__pycache__/history.cpython-312.pyc +0 -0
  13. package/core/app/colab_cli/__pycache__/runtime.cpython-312.pyc +0 -0
  14. package/core/app/colab_cli/__pycache__/state.cpython-312.pyc +0 -0
  15. package/core/app/colab_cli/__pycache__/utils.cpython-312.pyc +0 -0
  16. package/core/app/colab_cli/auth.py +278 -0
  17. package/core/app/colab_cli/auto_update.py +248 -0
  18. package/core/app/colab_cli/cli.py +155 -0
  19. package/core/app/colab_cli/client.py +310 -0
  20. package/core/app/colab_cli/commands/__init__.py +14 -0
  21. package/core/app/colab_cli/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  22. package/core/app/colab_cli/commands/__pycache__/automation.cpython-312.pyc +0 -0
  23. package/core/app/colab_cli/commands/__pycache__/execution.cpython-312.pyc +0 -0
  24. package/core/app/colab_cli/commands/__pycache__/files.cpython-312.pyc +0 -0
  25. package/core/app/colab_cli/commands/__pycache__/run.cpython-312.pyc +0 -0
  26. package/core/app/colab_cli/commands/__pycache__/session.cpython-312.pyc +0 -0
  27. package/core/app/colab_cli/commands/__pycache__/utility.cpython-312.pyc +0 -0
  28. package/core/app/colab_cli/commands/automation.py +265 -0
  29. package/core/app/colab_cli/commands/execution.py +362 -0
  30. package/core/app/colab_cli/commands/files.py +204 -0
  31. package/core/app/colab_cli/commands/run.py +477 -0
  32. package/core/app/colab_cli/commands/session.py +519 -0
  33. package/core/app/colab_cli/commands/utility.py +436 -0
  34. package/core/app/colab_cli/common.py +185 -0
  35. package/core/app/colab_cli/console.py +172 -0
  36. package/core/app/colab_cli/contents.py +93 -0
  37. package/core/app/colab_cli/converter.py +184 -0
  38. package/core/app/colab_cli/history.py +65 -0
  39. package/core/app/colab_cli/oauth_config.json +11 -0
  40. package/core/app/colab_cli/repl.py +173 -0
  41. package/core/app/colab_cli/runtime.py +262 -0
  42. package/core/app/colab_cli/state.py +156 -0
  43. package/core/app/colab_cli/utils.py +85 -0
  44. package/core/colab/worker.py +679 -0
  45. package/core/daemon.py +184 -0
  46. package/core/requirements.txt +8 -0
  47. package/package.json +22 -0
package/cli.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn, execSync } = require('child_process');
6
+
7
+ const CMD = process.argv[2] || 'status';
8
+ const ENV_PATH = path.join(__dirname, '.env');
9
+ const CORE_DIR = path.join(__dirname, 'core');
10
+ const PID_FILE = path.join(__dirname, '.daemon.pid');
11
+
12
+ async function promptConfig() {
13
+ const inquirer = require('inquirer');
14
+ console.log('=====================================');
15
+ console.log(' SATELLITE NODE INITIAL SETUP');
16
+ console.log('=====================================\n');
17
+ const answers = await inquirer.prompt([
18
+ {
19
+ type: 'input',
20
+ name: 'masterUrl',
21
+ message: 'Enter MASTER_URL (e.g., http://api.yourdomain.com):',
22
+ validate: input => input ? true : 'Required!'
23
+ },
24
+ {
25
+ type: 'input',
26
+ name: 'nodeApiKey',
27
+ message: 'Enter NODE_API_KEY (e.g., satellite-secret-key-123):',
28
+ validate: input => input ? true : 'Required!'
29
+ }
30
+ ]);
31
+
32
+ const envContent = `MASTER_URL=${answers.masterUrl}\nNODE_API_KEY=${answers.nodeApiKey}\n`;
33
+ fs.writeFileSync(ENV_PATH, envContent);
34
+ console.log('Configuration saved to .env file.\n');
35
+ }
36
+
37
+ function setupPythonEnv() {
38
+ const venvDir = path.join(CORE_DIR, 'venv');
39
+ if (!fs.existsSync(venvDir)) {
40
+ console.log('Creating Python virtual environment...');
41
+ try {
42
+ execSync(`python3 -m venv "${venvDir}"`, { stdio: 'inherit' });
43
+ } catch (e) {
44
+ console.log('python3 not found, trying python...');
45
+ execSync(`python -m venv "${venvDir}"`, { stdio: 'inherit' });
46
+ }
47
+ }
48
+
49
+ console.log('Installing dependencies...');
50
+ const pip = process.platform === 'win32'
51
+ ? path.join(venvDir, 'Scripts', 'pip')
52
+ : path.join(venvDir, 'bin', 'pip');
53
+ execSync(`"${pip}" install -r "${path.join(CORE_DIR, 'requirements.txt')}" -q`, { stdio: 'inherit' });
54
+ }
55
+
56
+ async function start() {
57
+ if (fs.existsSync(PID_FILE)) {
58
+ const oldPid = fs.readFileSync(PID_FILE, 'utf8').trim();
59
+ try {
60
+ process.kill(parseInt(oldPid), 0);
61
+ console.log(`Daemon is already running with PID ${oldPid}.`);
62
+ return;
63
+ } catch (e) {
64
+ // Process not running, clean up PID file
65
+ fs.unlinkSync(PID_FILE);
66
+ }
67
+ }
68
+
69
+ if (!fs.existsSync(ENV_PATH)) {
70
+ await promptConfig();
71
+ }
72
+
73
+ setupPythonEnv();
74
+
75
+ console.log('Starting Satellite Daemon in background...');
76
+
77
+ const pythonBin = process.platform === 'win32'
78
+ ? path.join(CORE_DIR, 'venv', 'Scripts', 'python')
79
+ : path.join(CORE_DIR, 'venv', 'bin', 'python');
80
+
81
+ // Copy .env to core dir so daemon.py can load it
82
+ fs.copyFileSync(ENV_PATH, path.join(CORE_DIR, '.env'));
83
+
84
+ const out = fs.openSync(path.join(__dirname, 'daemon.log'), 'a');
85
+ const err = fs.openSync(path.join(__dirname, 'daemon.error.log'), 'a');
86
+
87
+ const child = spawn(pythonBin, ['daemon.py'], {
88
+ cwd: CORE_DIR,
89
+ detached: true,
90
+ stdio: ['ignore', out, err]
91
+ });
92
+
93
+ child.unref();
94
+
95
+ fs.writeFileSync(PID_FILE, child.pid.toString());
96
+ console.log(`Daemon started successfully (PID: ${child.pid}).`);
97
+ console.log('You can now close this terminal. The node will keep running in the background.');
98
+ console.log('Run "colabcli status" to check its status or "colabcli stop" to stop it.');
99
+ }
100
+
101
+ function stop() {
102
+ if (!fs.existsSync(PID_FILE)) {
103
+ console.log('No daemon is currently running.');
104
+ return;
105
+ }
106
+
107
+ const pid = fs.readFileSync(PID_FILE, 'utf8').trim();
108
+ try {
109
+ process.kill(parseInt(pid), 'SIGTERM');
110
+ console.log(`Daemon (PID ${pid}) stopped successfully.`);
111
+ } catch (e) {
112
+ console.log(`Could not stop process ${pid}. It might have already stopped.`);
113
+ }
114
+
115
+ fs.unlinkSync(PID_FILE);
116
+ }
117
+
118
+ function status() {
119
+ if (!fs.existsSync(PID_FILE)) {
120
+ console.log('Satellite Daemon is NOT running.');
121
+ return;
122
+ }
123
+
124
+ const pid = fs.readFileSync(PID_FILE, 'utf8').trim();
125
+ try {
126
+ process.kill(parseInt(pid), 0);
127
+ console.log(`Satellite Daemon is RUNNING (PID ${pid}).`);
128
+ } catch (e) {
129
+ console.log('Satellite Daemon is NOT running (stale PID file found).');
130
+ fs.unlinkSync(PID_FILE);
131
+ }
132
+ }
133
+
134
+ async function main() {
135
+ if (CMD === 'start') {
136
+ await start();
137
+ } else if (CMD === 'stop') {
138
+ stop();
139
+ } else if (CMD === 'status') {
140
+ status();
141
+ } else {
142
+ console.log('Usage: colabcli <start|stop|status>');
143
+ }
144
+ }
145
+
146
+ main().catch(err => {
147
+ console.error('Error:', err);
148
+ });
File without changes
File without changes
@@ -0,0 +1,278 @@
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 enum
16
+ import json
17
+ import logging
18
+ from importlib import resources
19
+ import os
20
+ import sys
21
+ import warnings
22
+ from typing import Optional
23
+
24
+ import google.auth
25
+ import typer
26
+ from google.auth.transport import requests
27
+ from google.auth.transport.requests import Request
28
+ from google.oauth2.credentials import Credentials
29
+ from google_auth_oauthlib.flow import InstalledAppFlow
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Set by worker to skip interactive OAuth prompt
34
+ _DISABLE_INTERACTIVE_AUTH = False
35
+
36
+
37
+ class AuthProvider(str, enum.Enum):
38
+ """Authentication strategy for talking to the Colab backend.
39
+
40
+ Values are the lowercase strings accepted by the global ``--auth`` flag.
41
+ """
42
+
43
+ OAUTH2 = "oauth2"
44
+ ADC = "adc"
45
+
46
+
47
+ # Standard Scopes for Colab and Drive (Public Auth)
48
+ PUBLIC_SCOPES = [
49
+ "openid",
50
+ "https://www.googleapis.com/auth/userinfo.profile",
51
+ "https://www.googleapis.com/auth/userinfo.email",
52
+ "https://www.googleapis.com/auth/cloud-platform",
53
+ "https://www.googleapis.com/auth/colaboratory",
54
+ "https://www.googleapis.com/auth/drive.file",
55
+ ]
56
+
57
+
58
+ TOKEN_CONFIG_PATH = os.getenv("COLAB_CLI_TOKEN_PATH") or os.path.expanduser("~/.config/colab-cli/token.json")
59
+
60
+ # Remote copy-paste OAuth flow.
61
+ #
62
+ # We deliberately do NOT use a localhost redirect (`run_local_server`) or the
63
+ # out-of-band (OOB) redirect `urn:ietf:wg:oauth:2.0:oob`. OOB was blocked by
64
+ # Google in 2022 ("The out-of-band (OOB) flow has been blocked in order to
65
+ # keep users secure") and a localhost server is environment-dependent (fails
66
+ # on headless/remote/container hosts, requires an auto-openable browser, etc.).
67
+ #
68
+ # Instead we use the same mechanism `gcloud auth application-default login`
69
+ # uses: a real registered HTTPS landing page that displays the authorization
70
+ # code for the user to copy & paste, combined with the `token_usage=remote`
71
+ # consent parameter. This works identically in local and remote environments.
72
+ #
73
+ # The landing page below is registered to Google's cloud-SDK OAuth client
74
+ # (`764086051850-...`), which is also the client shipped in
75
+ # `colab_cli/oauth_config.json`; reusing another client id with this redirect
76
+ # yields `redirect_uri_mismatch`.
77
+ REMOTE_REDIRECT_URI = "https://sdk.cloud.google.com/applicationdefaultauthcode.html"
78
+
79
+
80
+ def _run_remote_flow(client_config: dict) -> Credentials:
81
+ """Run the remote copy-paste OAuth2 flow.
82
+
83
+ Interactive prompt when a TTY is available. No TTY → raises error.
84
+ Pre-configure tokens via `refresh_tokens.py` to avoid this flow.
85
+ """
86
+ flow = InstalledAppFlow.from_client_config(client_config, PUBLIC_SCOPES)
87
+ flow.redirect_uri = REMOTE_REDIRECT_URI
88
+ auth_url, _ = flow.authorization_url(prompt="consent", token_usage="remote")
89
+
90
+ if _DISABLE_INTERACTIVE_AUTH or not sys.stdin.isatty():
91
+ raise RuntimeError(
92
+ f"No saved token and interactive auth disabled. "
93
+ f"Run `refresh_tokens.py` first, or open URL manually: {auth_url}"
94
+ )
95
+
96
+ typer.echo("\nTo authorize colab-cli, visit this URL in any browser:\n", err=True)
97
+ typer.echo(" " + auth_url + "\n", err=True)
98
+ typer.echo("After approving, Google will display an authorization code.", err=True)
99
+ try:
100
+ code = input("Enter the authorization code: ").strip()
101
+ except (EOFError, OSError) as e:
102
+ raise RuntimeError(f"Interactive input failed: {e}")
103
+
104
+ if not code:
105
+ raise RuntimeError("No authorization code provided.")
106
+
107
+ flow.fetch_token(code=code)
108
+ return flow.credentials
109
+
110
+
111
+ def _get_google_auth_credentials(config_path: str) -> Credentials:
112
+ """
113
+ Retrieves credentials using standard public OAuth2 flow.
114
+ """
115
+ client_config = None
116
+ if os.path.exists(config_path):
117
+ with open(config_path, "r") as f:
118
+ client_config = json.load(f)
119
+ else:
120
+ # Try inlined config — check multiple possible package paths
121
+ for pkg in ("colab_cli", "app.colab_cli"):
122
+ try:
123
+ config_resource = resources.files(pkg).joinpath("oauth_config.json")
124
+ if config_resource.is_file():
125
+ client_config = json.loads(config_resource.read_text())
126
+ break
127
+ except (Exception, ModuleNotFoundError):
128
+ continue
129
+ # Fallback: direct file path
130
+ if not client_config:
131
+ bundled = os.path.join(os.path.dirname(__file__), "oauth_config.json")
132
+ if os.path.exists(bundled):
133
+ with open(bundled) as f:
134
+ client_config = json.load(f)
135
+
136
+ if not client_config:
137
+ raise FileNotFoundError(
138
+ f"Client OAuth config not found at {config_path} and no inlined config available. "
139
+ "Please provide a valid path via -c/--client-oauth-config."
140
+ )
141
+
142
+ creds = None
143
+
144
+ # Ensure config directory exists for the token file
145
+ os.makedirs(os.path.dirname(TOKEN_CONFIG_PATH), exist_ok=True)
146
+
147
+ if os.path.exists(TOKEN_CONFIG_PATH):
148
+ try:
149
+ creds = Credentials.from_authorized_user_file(
150
+ TOKEN_CONFIG_PATH, PUBLIC_SCOPES
151
+ )
152
+ except Exception as e:
153
+ logger.warning(f"Failed to load token from {TOKEN_CONFIG_PATH}: {e}")
154
+
155
+ if not creds or not creds.valid:
156
+ if creds and creds.expired and creds.refresh_token:
157
+ try:
158
+ creds.refresh(Request())
159
+ except Exception as e:
160
+ logger.warning(f"Failed to refresh token: {e}")
161
+ creds = None
162
+
163
+ if not creds:
164
+ creds = _run_remote_flow(client_config)
165
+
166
+ # Save the credentials for the next run
167
+ try:
168
+ with open(TOKEN_CONFIG_PATH, "w") as token_file:
169
+ token_file.write(creds.to_json())
170
+ except Exception as e:
171
+ logger.error(f"Failed to save token to {TOKEN_CONFIG_PATH}: {e}")
172
+
173
+ return creds
174
+
175
+
176
+ def _get_adc_credentials() -> Credentials:
177
+ """Retrieves credentials using Google Application Default Credentials.
178
+
179
+ Honors the standard ADC discovery chain (``GOOGLE_APPLICATION_CREDENTIALS``,
180
+ ``gcloud auth application-default login``, GCE/GKE metadata server, etc.).
181
+
182
+ The RuntimeService at colab.pa.googleapis.com requires the
183
+ `colaboratory` scope (otherwise keep-alive returns 403 SCOPE_NOT_PERMITTED).
184
+ Most ADC credential types (service accounts, GCE/GKE, impersonated)
185
+ support `with_scopes`; user credentials minted by
186
+ `gcloud auth application-default login` do not. For the latter, the user
187
+ must re-run `gcloud auth application-default login` with
188
+ `--scopes=openid,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/colaboratory`
189
+ (`openid` and `cloud-platform` are required by `gcloud` itself; `userinfo.email`
190
+ is required by the session backend; `colaboratory` is required by this RPC).
191
+ """
192
+ # `google.auth._default` emits a UserWarning when ADC user credentials
193
+ # don't have a quota project pinned ("Your application has authenticated
194
+ # using end user credentials from Google Cloud SDK without a quota
195
+ # project. You might receive a 'quota exceeded' or 'API not enabled'
196
+ # error.").
197
+ #
198
+ # That heuristic does not apply to this CLI: every call we make to
199
+ # `colab.pa.googleapis.com` carries `X-Goog-User-Project: 1014160490159`
200
+ # (Colab's project id) — see AGENTS.md item 18 — so the user's
201
+ # quota-project setting is irrelevant. The warning shows up on every
202
+ # single `colab` invocation under ADC, which is pure noise. Filter it,
203
+ # but keep the scope as tight as possible: only this exact message,
204
+ # only during this one call.
205
+ with warnings.catch_warnings():
206
+ warnings.filterwarnings(
207
+ "ignore",
208
+ message=r"Your application has authenticated using end user credentials.*",
209
+ category=UserWarning,
210
+ )
211
+ creds, _ = google.auth.default(scopes=list(PUBLIC_SCOPES))
212
+
213
+ if not creds.valid:
214
+ from google.auth import compute_engine
215
+
216
+ if isinstance(creds, compute_engine.Credentials):
217
+ creds = None
218
+ else:
219
+ logger.warning("Failed to obtain valid ADC credentials.")
220
+ try:
221
+ logger.warning("Trying to refresh ADC credentials")
222
+ creds.refresh(Request())
223
+ except Exception as e:
224
+ logger.warning(f"Failed to refresh token: {e}")
225
+ creds = None
226
+
227
+ if not creds:
228
+ raise RuntimeError(
229
+ "No valid ADC credentials found. "
230
+ "Set GOOGLE_APPLICATION_CREDENTIALS or run:\n"
231
+ " gcloud auth application-default login \\\n"
232
+ " --scopes=openid,"
233
+ "https://www.googleapis.com/auth/cloud-platform,"
234
+ "https://www.googleapis.com/auth/userinfo.email,"
235
+ "https://www.googleapis.com/auth/colaboratory"
236
+ )
237
+
238
+ # Some credential subclasses ignore the `scopes=` kwarg in `default()`
239
+ # (e.g. user creds), so re-apply via `with_scopes` when supported.
240
+ if getattr(creds, "requires_scopes", False):
241
+ try:
242
+ creds = creds.with_scopes(list(PUBLIC_SCOPES))
243
+ except Exception as e: # NotImplementedError for non-scopable creds.
244
+ logger.debug(f"Could not augment ADC scopes via with_scopes: {e}")
245
+ return creds
246
+
247
+
248
+ def get_credentials(
249
+ config_path: Optional[str] = None,
250
+ provider: AuthProvider = AuthProvider.OAUTH2,
251
+ token_path: Optional[str] = None,
252
+ ) -> requests.AuthorizedSession:
253
+ """Unified entry point for retrieving an authorized session.
254
+
255
+ Args:
256
+ config_path: Path to the OAuth2 client config JSON.
257
+ provider: Authentication strategy.
258
+ token_path: Path to the saved token JSON file. If provided, overrides
259
+ the global TOKEN_CONFIG_PATH for this call.
260
+ """
261
+ if provider == AuthProvider.OAUTH2:
262
+ if not config_path:
263
+ config_path = os.path.expanduser("~/.colab-cli-oauth-config.json")
264
+ if token_path:
265
+ orig_token_path = TOKEN_CONFIG_PATH
266
+ try:
267
+ globals()["TOKEN_CONFIG_PATH"] = token_path
268
+ creds = _get_google_auth_credentials(config_path)
269
+ finally:
270
+ globals()["TOKEN_CONFIG_PATH"] = orig_token_path
271
+ else:
272
+ creds = _get_google_auth_credentials(config_path)
273
+ elif provider == AuthProvider.ADC:
274
+ creds = _get_adc_credentials()
275
+ else:
276
+ raise ValueError(f"Unknown auth provider: {provider!r}")
277
+
278
+ return requests.AuthorizedSession(creds)