silicon-cli 1.0.8__tar.gz → 1.0.11__tar.gz
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.
- {silicon_cli-1.0.8/silicon_cli.egg-info → silicon_cli-1.0.11}/PKG-INFO +13 -10
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/README.md +10 -8
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/pyproject.toml +1 -1
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/__init__.py +1 -1
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/cli.py +20 -4
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/config.py +25 -2
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/glassagent.py +1 -1
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/process.py +2 -2
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/registry.py +21 -12
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/stemcell.py +54 -5
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/sync.py +130 -9
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/update.py +6 -3
- {silicon_cli-1.0.8 → silicon_cli-1.0.11/silicon_cli.egg-info}/PKG-INFO +13 -10
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/LICENSE +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/setup.cfg +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/interface_cli.py +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli/ui.py +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/SOURCES.txt +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/dependency_links.txt +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/entry_points.txt +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/requires.txt +0 -0
- {silicon_cli-1.0.8 → silicon_cli-1.0.11}/silicon_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: silicon-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.11
|
|
4
4
|
Summary: Silicon CLI — create, run, and manage your silicon instances.
|
|
5
5
|
Author: Saket
|
|
6
6
|
Project-URL: Homepage, https://github.com/saket1225/silicon-cli
|
|
@@ -13,6 +13,7 @@ Requires-Python: >=3.9
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: certifi>=2024.2.2
|
|
16
|
+
Dynamic: license-file
|
|
16
17
|
|
|
17
18
|
# silicon-cli
|
|
18
19
|
|
|
@@ -40,12 +41,14 @@ pip install silicon-cli
|
|
|
40
41
|
silicon Show status or list instances
|
|
41
42
|
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
42
43
|
silicon new . Hydrate the current folder into a runnable silicon
|
|
43
|
-
silicon start <target> Start silicon(s). target = name,
|
|
44
|
-
silicon stop [--full] <target> Stop silicon(s)
|
|
45
|
-
silicon restart <target> Restart silicon(s)
|
|
44
|
+
silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
45
|
+
silicon stop [--full] <target> Stop silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
46
|
+
silicon restart <target> Restart silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
46
47
|
silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
|
|
47
48
|
silicon status [name] Show instance status
|
|
48
49
|
silicon browser [name] Open a headed browser for login
|
|
50
|
+
silicon browser-profile setup <token> Create a Steel browser profile through Glass
|
|
51
|
+
silicon browser-profile finish <token> <session_id> [before_ids_csv] Finish profile setup
|
|
49
52
|
silicon debug [name] Tail a running instance's logs
|
|
50
53
|
silicon attach [path] Register an existing silicon directory
|
|
51
54
|
silicon pull [api_token] Pull a Glass team or silicon into local folders
|
|
@@ -92,8 +95,8 @@ silicon pull sct_live_...
|
|
|
92
95
|
|
|
93
96
|
The command validates the token with Glass, mints one local silicon API key per
|
|
94
97
|
team silicon, creates one folder per silicon, hydrates the stemcell, writes
|
|
95
|
-
`.glass.json`, `.env`, and `env.py`, registers each instance,
|
|
96
|
-
Silicon Interface daemon.
|
|
98
|
+
`.glass.json`, `.env`, and `env.py`, registers each instance, starts each
|
|
99
|
+
Silicon Interface daemon, and starts each silicon process.
|
|
97
100
|
|
|
98
101
|
During team pull, setup asks for the default brain/fallback settings once. You
|
|
99
102
|
can apply those settings to every silicon, or select specific silicons that
|
|
@@ -102,9 +105,9 @@ Older per-silicon `scs_live_...` tokens still pull just that one silicon.
|
|
|
102
105
|
|
|
103
106
|
Provider API keys for voice, browser profiles, billing, and architecture
|
|
104
107
|
generation are configured on the Glass backend. After the silicon API token is
|
|
105
|
-
validated, `silicon pull` reads the team's provider-key metadata from Glass
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
validated, `silicon pull` reads the team's provider-key metadata from Glass and
|
|
109
|
+
automatically marks the pulled silicon(s) to use Glass-managed keys when every
|
|
110
|
+
provider is available. Plaintext secrets are never returned to the CLI. A
|
|
108
111
|
Glass-pulled silicon only stores its Glass API token plus a local marker that
|
|
109
112
|
provider keys come from Glass.
|
|
110
113
|
|
|
@@ -24,12 +24,14 @@ pip install silicon-cli
|
|
|
24
24
|
silicon Show status or list instances
|
|
25
25
|
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
26
26
|
silicon new . Hydrate the current folder into a runnable silicon
|
|
27
|
-
silicon start <target> Start silicon(s). target = name,
|
|
28
|
-
silicon stop [--full] <target> Stop silicon(s)
|
|
29
|
-
silicon restart <target> Restart silicon(s)
|
|
27
|
+
silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
28
|
+
silicon stop [--full] <target> Stop silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
29
|
+
silicon restart <target> Restart silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
30
30
|
silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
|
|
31
31
|
silicon status [name] Show instance status
|
|
32
32
|
silicon browser [name] Open a headed browser for login
|
|
33
|
+
silicon browser-profile setup <token> Create a Steel browser profile through Glass
|
|
34
|
+
silicon browser-profile finish <token> <session_id> [before_ids_csv] Finish profile setup
|
|
33
35
|
silicon debug [name] Tail a running instance's logs
|
|
34
36
|
silicon attach [path] Register an existing silicon directory
|
|
35
37
|
silicon pull [api_token] Pull a Glass team or silicon into local folders
|
|
@@ -76,8 +78,8 @@ silicon pull sct_live_...
|
|
|
76
78
|
|
|
77
79
|
The command validates the token with Glass, mints one local silicon API key per
|
|
78
80
|
team silicon, creates one folder per silicon, hydrates the stemcell, writes
|
|
79
|
-
`.glass.json`, `.env`, and `env.py`, registers each instance,
|
|
80
|
-
Silicon Interface daemon.
|
|
81
|
+
`.glass.json`, `.env`, and `env.py`, registers each instance, starts each
|
|
82
|
+
Silicon Interface daemon, and starts each silicon process.
|
|
81
83
|
|
|
82
84
|
During team pull, setup asks for the default brain/fallback settings once. You
|
|
83
85
|
can apply those settings to every silicon, or select specific silicons that
|
|
@@ -86,9 +88,9 @@ Older per-silicon `scs_live_...` tokens still pull just that one silicon.
|
|
|
86
88
|
|
|
87
89
|
Provider API keys for voice, browser profiles, billing, and architecture
|
|
88
90
|
generation are configured on the Glass backend. After the silicon API token is
|
|
89
|
-
validated, `silicon pull` reads the team's provider-key metadata from Glass
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
validated, `silicon pull` reads the team's provider-key metadata from Glass and
|
|
92
|
+
automatically marks the pulled silicon(s) to use Glass-managed keys when every
|
|
93
|
+
provider is available. Plaintext secrets are never returned to the CLI. A
|
|
92
94
|
Glass-pulled silicon only stores its Glass API token plus a local marker that
|
|
93
95
|
provider keys come from Glass.
|
|
94
96
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Silicon CLI — manage silicon instances. Python port of the original bash manager."""
|
|
2
|
-
__version__ = "1.0.
|
|
2
|
+
__version__ = "1.0.10"
|
|
@@ -10,7 +10,7 @@ from .config import python_run_cmd
|
|
|
10
10
|
|
|
11
11
|
COMMANDS = ["start", "stop", "restart", "status", "browser", "debug", "attach",
|
|
12
12
|
"pull", "push", "backup", "update", "update-check", "check-update",
|
|
13
|
-
"list", "install", "new", "help", "script", "agent"]
|
|
13
|
+
"browser-profile", "list", "install", "new", "help", "script", "agent"]
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
# ----------------------------------------------------------------- commands
|
|
@@ -61,7 +61,7 @@ def cmd_status(target: str | None) -> None:
|
|
|
61
61
|
def cmd_browser(target: str | None) -> None:
|
|
62
62
|
inst = registry.resolve_one(target)
|
|
63
63
|
ui.info(f"Opening browser for '{inst.name}'...")
|
|
64
|
-
subprocess.run([python_run_cmd(), "main.py", "browser"], cwd=inst.path)
|
|
64
|
+
subprocess.run([python_run_cmd(inst.path), "main.py", "browser"], cwd=inst.path)
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def cmd_debug(target: str | None) -> None:
|
|
@@ -156,13 +156,17 @@ def cmd_help() -> None:
|
|
|
156
156
|
silicon Show status or list instances
|
|
157
157
|
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
158
158
|
silicon new . Hydrate the current folder into a runnable silicon
|
|
159
|
-
silicon start <target> Start silicon(s). target = name,
|
|
159
|
+
silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
160
160
|
silicon stop <target> Stop silicon(s) (agent stays alive)
|
|
161
161
|
silicon stop --full <target> Stop silicon(s) and glass agent
|
|
162
162
|
silicon restart <target> Restart silicon(s)
|
|
163
163
|
silicon agent <start|stop|status> [name] Manage glass agent
|
|
164
164
|
silicon status [name] Show instance status
|
|
165
165
|
silicon browser [name] Open headed browser for login
|
|
166
|
+
silicon browser-profile setup <token>
|
|
167
|
+
Create a Steel browser profile through Glass
|
|
168
|
+
silicon browser-profile finish <token> <session_id> [before_ids_csv]
|
|
169
|
+
Finish a browser profile setup session
|
|
166
170
|
silicon debug [name] Attach to running instance (live logs)
|
|
167
171
|
silicon attach [path] Register an existing silicon instance
|
|
168
172
|
silicon pull [api_token] Pull a Glass team or silicon into local folders
|
|
@@ -170,7 +174,7 @@ def cmd_help() -> None:
|
|
|
170
174
|
silicon push [name] now Push a one-time backup to Glass
|
|
171
175
|
silicon push [name] stop Stop the daily backup loop
|
|
172
176
|
silicon backup [name] [now|stop] Alias for silicon push
|
|
173
|
-
silicon update <target> Update silicon(s) to latest. target = name,
|
|
177
|
+
silicon update <target> Update silicon(s) to latest. target = name, *, all, 1,2,4, or name,name
|
|
174
178
|
silicon update check [name] Trigger this silicon's system update check now
|
|
175
179
|
silicon update-check [name] Trigger this silicon's system update check now
|
|
176
180
|
silicon list List all instances
|
|
@@ -220,6 +224,18 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
220
224
|
cmd_status(a1)
|
|
221
225
|
elif cmd == "browser":
|
|
222
226
|
cmd_browser(a1)
|
|
227
|
+
elif cmd == "browser-profile":
|
|
228
|
+
if a1 == "setup":
|
|
229
|
+
sync.browser_profile_setup(a2)
|
|
230
|
+
elif a1 == "finish":
|
|
231
|
+
sync.browser_profile_finish(
|
|
232
|
+
a2,
|
|
233
|
+
argv[3] if len(argv) > 3 else None,
|
|
234
|
+
argv[4] if len(argv) > 4 else None,
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
ui.error("Usage: silicon browser-profile <setup|finish> ...")
|
|
238
|
+
sys.exit(1)
|
|
223
239
|
elif cmd == "debug":
|
|
224
240
|
cmd_debug(a1)
|
|
225
241
|
elif cmd == "attach":
|
|
@@ -41,6 +41,29 @@ SILICON_INTERFACE_DAEMON_SKIP = os.environ.get("SILICON_INTERFACE_DAEMON_SKIP",
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""The
|
|
44
|
+
def venv_python(path: str | os.PathLike) -> str | None:
|
|
45
|
+
"""The silicon's own .venv interpreter, if one exists."""
|
|
46
|
+
sub = "Scripts/python.exe" if os.name == "nt" else "bin/python"
|
|
47
|
+
cand = Path(path) / ".venv" / sub
|
|
48
|
+
return str(cand) if cand.exists() else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def base_python_cmd() -> str:
|
|
52
|
+
"""The interpreter used to CREATE a silicon's venv (not this CLI's venv)."""
|
|
46
53
|
return os.environ.get("SILICON_PYTHON") or shutil.which("python3") or shutil.which("python") or "python3"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def python_run_cmd(path: str | os.PathLike | None = None) -> str:
|
|
57
|
+
"""The interpreter used to RUN a silicon's code (not this CLI's venv).
|
|
58
|
+
|
|
59
|
+
SILICON_PYTHON always wins; otherwise prefer the silicon's own .venv —
|
|
60
|
+
system interpreters are often externally managed (PEP 668) and never
|
|
61
|
+
received the silicon's dependencies.
|
|
62
|
+
"""
|
|
63
|
+
if os.environ.get("SILICON_PYTHON"):
|
|
64
|
+
return os.environ["SILICON_PYTHON"]
|
|
65
|
+
if path:
|
|
66
|
+
venv = venv_python(path)
|
|
67
|
+
if venv:
|
|
68
|
+
return venv
|
|
69
|
+
return shutil.which("python3") or shutil.which("python") or "python3"
|
|
@@ -45,7 +45,7 @@ def start(path: str) -> None:
|
|
|
45
45
|
return
|
|
46
46
|
log = open(Path(path) / ".glass_agent.log", "a")
|
|
47
47
|
proc = subprocess.Popen(
|
|
48
|
-
[python_run_cmd(), "-u", "glass_agent.py"], cwd=path,
|
|
48
|
+
[python_run_cmd(path), "-u", "glass_agent.py"], cwd=path,
|
|
49
49
|
stdout=log, stderr=subprocess.STDOUT, start_new_session=True,
|
|
50
50
|
)
|
|
51
51
|
_pid_file(path).write_text(str(proc.pid))
|
|
@@ -92,7 +92,7 @@ def watchdog_loop(name: str, path: str, pid_file: str) -> None:
|
|
|
92
92
|
log_file = Path(path) / ".silicon.log"
|
|
93
93
|
main_py = str(Path(path) / "main.py")
|
|
94
94
|
stop_file = Path(path) / ".silicon.stop"
|
|
95
|
-
py = python_run_cmd()
|
|
95
|
+
py = python_run_cmd(path)
|
|
96
96
|
child: subprocess.Popen | None = None
|
|
97
97
|
|
|
98
98
|
def _terminate(signum=None, frame=None):
|
|
@@ -231,7 +231,7 @@ def _multi(target: str, verb: str, fn) -> bool:
|
|
|
231
231
|
if not names:
|
|
232
232
|
ui.error("No matching installations")
|
|
233
233
|
sys.exit(1)
|
|
234
|
-
if target
|
|
234
|
+
if target in {"all", "*"}:
|
|
235
235
|
joined = ", ".join(names)
|
|
236
236
|
if not ui.confirm(f"Are you sure you want to {verb} the following silicons: {joined}?"):
|
|
237
237
|
return True
|
|
@@ -82,26 +82,35 @@ def find(search: str | None = None) -> Install | None:
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def is_multi_target(s: str) -> bool:
|
|
85
|
-
if s
|
|
85
|
+
if s in {"all", "*"}:
|
|
86
86
|
return True
|
|
87
|
-
parts = s.split(",")
|
|
88
|
-
return all(p
|
|
87
|
+
parts = [p.strip() for p in s.split(",")]
|
|
88
|
+
return len(parts) > 1 and all(bool(p) for p in parts)
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def resolve_targets(selector: str) -> list[str]:
|
|
92
|
-
"""'all' | '1,2,4'
|
|
92
|
+
"""'all' | '*' | '1,2,4' | 'api-dev,copywriter' → install names."""
|
|
93
93
|
rows = installs()
|
|
94
|
-
if selector
|
|
94
|
+
if selector in {"all", "*"}:
|
|
95
95
|
return [i.name for i in rows]
|
|
96
|
+
by_name = {i.name: i.name for i in rows}
|
|
96
97
|
out = []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
seen = set()
|
|
99
|
+
for part in selector.split(","):
|
|
100
|
+
part = part.strip()
|
|
101
|
+
name = ""
|
|
102
|
+
if part.isdigit():
|
|
103
|
+
idx = int(part) - 1
|
|
104
|
+
for i in rows:
|
|
105
|
+
if i.index == idx:
|
|
106
|
+
name = i.name
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
name = by_name.get(part, "")
|
|
110
|
+
if not name or name in seen:
|
|
100
111
|
continue
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if i.index == idx:
|
|
104
|
-
out.append(i.name)
|
|
112
|
+
seen.add(name)
|
|
113
|
+
out.append(name)
|
|
105
114
|
return out
|
|
106
115
|
|
|
107
116
|
|
|
@@ -17,7 +17,7 @@ import tempfile
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
from . import interface_cli, registry, ui
|
|
20
|
-
from .config import STEMCELL_GIT_URL, STEMCELL_ZIP_URL, python_run_cmd
|
|
20
|
+
from .config import STEMCELL_GIT_URL, STEMCELL_ZIP_URL, base_python_cmd, python_run_cmd, venv_python
|
|
21
21
|
|
|
22
22
|
SKIP_NAMES = {".git", "__pycache__", ".DS_Store"}
|
|
23
23
|
PRESERVE_ROOT = {"env.py", "silicon.json", ".glass.json"}
|
|
@@ -90,6 +90,58 @@ def _choose_brain_order(primary: str) -> list[str]:
|
|
|
90
90
|
return [primary]
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def _ensure_venv(dst: Path) -> str | None:
|
|
94
|
+
"""Create <dst>/.venv if missing. Returns its interpreter, or None.
|
|
95
|
+
|
|
96
|
+
Returns None when the venv can't be created — e.g. Debian/Ubuntu without
|
|
97
|
+
python3-venv, where ensurepip is stripped from the system interpreter.
|
|
98
|
+
"""
|
|
99
|
+
existing = venv_python(dst)
|
|
100
|
+
if existing:
|
|
101
|
+
return existing
|
|
102
|
+
r = subprocess.run([base_python_cmd(), "-m", "venv", str(dst / ".venv")],
|
|
103
|
+
capture_output=True, text=True)
|
|
104
|
+
if r.returncode != 0:
|
|
105
|
+
shutil.rmtree(dst / ".venv", ignore_errors=True)
|
|
106
|
+
return None
|
|
107
|
+
return venv_python(dst)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def install_requirements(dst: Path, req: Path) -> None:
|
|
111
|
+
"""Install the silicon's requirements, preferring an instance-local venv.
|
|
112
|
+
|
|
113
|
+
System interpreters are commonly externally managed (PEP 668), so a plain
|
|
114
|
+
`pip install` is refused. Try, in order: the silicon's own .venv, plain
|
|
115
|
+
pip, `--user`, `--break-system-packages`. A failure here must abort the
|
|
116
|
+
hydration — a silicon without its dependencies starts but never completes
|
|
117
|
+
its Glass handshake.
|
|
118
|
+
"""
|
|
119
|
+
ui.info("Installing Python dependencies...")
|
|
120
|
+
vpy = _ensure_venv(dst)
|
|
121
|
+
if vpy:
|
|
122
|
+
r = subprocess.run([vpy, "-m", "pip", "install", "-r", str(req), "--quiet"])
|
|
123
|
+
if r.returncode == 0:
|
|
124
|
+
return
|
|
125
|
+
ui.warn("Install into the silicon's venv failed; falling back to the system interpreter.")
|
|
126
|
+
# A half-provisioned venv must not survive: python_run_cmd() would
|
|
127
|
+
# prefer it over the system interpreter that gets the deps below.
|
|
128
|
+
shutil.rmtree(dst / ".venv", ignore_errors=True)
|
|
129
|
+
py = base_python_cmd()
|
|
130
|
+
last = None
|
|
131
|
+
for extra in ([], ["--user"], ["--break-system-packages"]):
|
|
132
|
+
last = subprocess.run([py, "-m", "pip", "install", "-r", str(req), "--quiet", *extra],
|
|
133
|
+
capture_output=True, text=True)
|
|
134
|
+
if last.returncode == 0:
|
|
135
|
+
return
|
|
136
|
+
if last is not None and last.stderr:
|
|
137
|
+
sys.stderr.write(last.stderr)
|
|
138
|
+
ui.error("Could not install Python dependencies — this silicon would start but never "
|
|
139
|
+
"complete its Glass handshake, so hydration was aborted.")
|
|
140
|
+
ui.info("On Debian/Ubuntu, install venv support and retry: sudo apt install python3-venv")
|
|
141
|
+
ui.info("Or point SILICON_PYTHON at an interpreter that allows pip installs.")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
|
|
93
145
|
def hydrate(target: str, setup_config=None) -> None:
|
|
94
146
|
abs_target = str(Path(target).resolve())
|
|
95
147
|
os.makedirs(abs_target, exist_ok=True)
|
|
@@ -166,10 +218,7 @@ def hydrate(target: str, setup_config=None) -> None:
|
|
|
166
218
|
# Install dependencies
|
|
167
219
|
req = dst / "requirements.txt"
|
|
168
220
|
if req.exists():
|
|
169
|
-
|
|
170
|
-
r = subprocess.run([python_run_cmd(), "-m", "pip", "install", "-r", str(req), "--quiet"])
|
|
171
|
-
if r.returncode != 0:
|
|
172
|
-
subprocess.run([python_run_cmd(), "-m", "pip", "install", "-r", str(req), "--quiet", "--user"])
|
|
221
|
+
install_requirements(dst, req)
|
|
173
222
|
|
|
174
223
|
registry.register(name, abs_target)
|
|
175
224
|
interface_cli.setup(abs_target)
|
|
@@ -13,10 +13,11 @@ import time
|
|
|
13
13
|
import urllib.error
|
|
14
14
|
import urllib.parse
|
|
15
15
|
import urllib.request
|
|
16
|
+
import webbrowser
|
|
16
17
|
from datetime import datetime, timedelta, timezone
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
|
-
from . import registry, stemcell, ui
|
|
20
|
+
from . import process, registry, stemcell, ui
|
|
20
21
|
from .config import GLASS_CLI_REPO, GLASS_SERVER_URL
|
|
21
22
|
|
|
22
23
|
MANIFEST_NAME = ".backupsilicon"
|
|
@@ -226,6 +227,30 @@ def _post_json_with_team_key(url: str, team_key: str, body=None) -> tuple[int, d
|
|
|
226
227
|
return 0, {"detail": str(e)}
|
|
227
228
|
|
|
228
229
|
|
|
230
|
+
def _post_json(url: str, body=None, *, timeout: int = 60) -> tuple[int, dict]:
|
|
231
|
+
data = json.dumps(body or {}).encode("utf-8")
|
|
232
|
+
req = urllib.request.Request(
|
|
233
|
+
url,
|
|
234
|
+
data=data,
|
|
235
|
+
method="POST",
|
|
236
|
+
headers={
|
|
237
|
+
"Content-Type": "application/json",
|
|
238
|
+
"Accept": "application/json",
|
|
239
|
+
"User-Agent": "silicon-cli",
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
try:
|
|
243
|
+
with _urlopen(req, timeout=timeout) as resp:
|
|
244
|
+
return resp.status, json.loads(resp.read().decode() or "{}")
|
|
245
|
+
except urllib.error.HTTPError as e:
|
|
246
|
+
try:
|
|
247
|
+
return e.code, json.loads(e.read().decode() or "{}")
|
|
248
|
+
except Exception:
|
|
249
|
+
return e.code, {}
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return 0, {"detail": str(e)}
|
|
252
|
+
|
|
253
|
+
|
|
229
254
|
def _team_slug_from_silicon(silicon: dict) -> str:
|
|
230
255
|
return str(
|
|
231
256
|
silicon.get("team")
|
|
@@ -263,7 +288,12 @@ def _configured_team_key_names(rows: list[dict]) -> list[str]:
|
|
|
263
288
|
configured = []
|
|
264
289
|
for key_name, _label in PROVIDER_API_KEYS:
|
|
265
290
|
row = by_name.get(key_name, {})
|
|
266
|
-
if
|
|
291
|
+
if (
|
|
292
|
+
row.get("configured")
|
|
293
|
+
or row.get("team_configured")
|
|
294
|
+
or row.get("center_configured")
|
|
295
|
+
or row.get("server_fallback_configured")
|
|
296
|
+
):
|
|
267
297
|
configured.append(key_name)
|
|
268
298
|
return configured
|
|
269
299
|
|
|
@@ -273,8 +303,15 @@ def _display_team_api_keys(rows: list[dict]) -> None:
|
|
|
273
303
|
ui.info("Provider API token status from Glass (secrets are not returned):")
|
|
274
304
|
for key_name, label in PROVIDER_API_KEYS:
|
|
275
305
|
row = by_name.get(key_name, {})
|
|
276
|
-
|
|
277
|
-
|
|
306
|
+
source = str(row.get("source") or "").strip()
|
|
307
|
+
if row.get("team_configured") or source == "team":
|
|
308
|
+
status = "team override"
|
|
309
|
+
elif row.get("center_configured") or source == "center":
|
|
310
|
+
status = "center managed"
|
|
311
|
+
elif row.get("server_fallback_configured") or source == "server":
|
|
312
|
+
status = "server fallback"
|
|
313
|
+
elif row.get("configured"):
|
|
314
|
+
status = "Glass managed"
|
|
278
315
|
elif row.get("server_fallback_configured"):
|
|
279
316
|
status = "server fallback only"
|
|
280
317
|
else:
|
|
@@ -282,7 +319,7 @@ def _display_team_api_keys(rows: list[dict]) -> None:
|
|
|
282
319
|
ui.info(f" {label} ({key_name}): {status}")
|
|
283
320
|
|
|
284
321
|
|
|
285
|
-
def _provider_key_env_from_rows(team_slug: str, rows: list[dict], prompt: str) -> dict[str, str]:
|
|
322
|
+
def _provider_key_env_from_rows(team_slug: str, rows: list[dict], prompt: str = "") -> dict[str, str]:
|
|
286
323
|
if not rows:
|
|
287
324
|
ui.warn("Glass did not return provider API token metadata.")
|
|
288
325
|
return {}
|
|
@@ -295,10 +332,6 @@ def _provider_key_env_from_rows(team_slug: str, rows: list[dict], prompt: str) -
|
|
|
295
332
|
ui.info("Set them in Glass > API keys, then rerun silicon pull if these silicons need them.")
|
|
296
333
|
return {}
|
|
297
334
|
|
|
298
|
-
if not ui.confirm(prompt, default_yes=True):
|
|
299
|
-
ui.info("Skipping Glass-managed provider API token marker.")
|
|
300
|
-
return {}
|
|
301
|
-
|
|
302
335
|
return {
|
|
303
336
|
"SILICON_PROVIDER_KEYS_SOURCE": "glass",
|
|
304
337
|
"SILICON_PROVIDER_KEYS_TEAM": team_slug,
|
|
@@ -506,6 +539,89 @@ def _team_pull_url(server: str) -> str:
|
|
|
506
539
|
return f"{server}/api/v1/teams/setup-pull"
|
|
507
540
|
|
|
508
541
|
|
|
542
|
+
def _browser_profile_setup_start_url(server: str) -> str:
|
|
543
|
+
return f"{server}/api/v1/browser-profiles/setup/start"
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _browser_profile_setup_finish_url(server: str) -> str:
|
|
547
|
+
return f"{server}/api/v1/browser-profiles/setup/finish"
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _browser_profile_finish_command(token: str, session_id: str, before_ids: list[str]) -> str:
|
|
551
|
+
before = ",".join(before_ids)
|
|
552
|
+
return f"silicon browser-profile finish '{token}' '{session_id}' '{before}'"
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def browser_profile_finish(token: str | None, session_id: str | None, before_ids_csv: str | None = None) -> None:
|
|
556
|
+
token = (token or "").strip()
|
|
557
|
+
session_id = (session_id or "").strip()
|
|
558
|
+
if not token or not session_id:
|
|
559
|
+
ui.error("Usage: silicon browser-profile finish <setup_token> <session_id> [before_profile_ids_csv]")
|
|
560
|
+
sys.exit(1)
|
|
561
|
+
before_ids = [p.strip() for p in (before_ids_csv or "").split(",") if p.strip()]
|
|
562
|
+
server = GLASS_SERVER_URL.rstrip("/")
|
|
563
|
+
ui.info("Finishing browser profile setup with Glass...")
|
|
564
|
+
code, body = _post_json(
|
|
565
|
+
_browser_profile_setup_finish_url(server),
|
|
566
|
+
{"token": token, "session_id": session_id, "before_profile_ids": before_ids},
|
|
567
|
+
timeout=120,
|
|
568
|
+
)
|
|
569
|
+
if not (200 <= code < 300):
|
|
570
|
+
ui.error(body.get("detail") or body.get("error") or f"Glass could not finish the profile setup (HTTP {code}).")
|
|
571
|
+
sys.exit(1)
|
|
572
|
+
profile = body.get("profile") or {}
|
|
573
|
+
name = profile.get("name") or profile.get("id") or "browser profile"
|
|
574
|
+
assigned = body.get("assigned", 0)
|
|
575
|
+
ui.success(f"Saved Steel profile '{name}' and assigned it to {assigned} silicon(s).")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def browser_profile_setup(token: str | None) -> None:
|
|
579
|
+
token = (token or "").strip()
|
|
580
|
+
if not token:
|
|
581
|
+
ui.error("Usage: silicon browser-profile setup <setup_token>")
|
|
582
|
+
sys.exit(1)
|
|
583
|
+
server = GLASS_SERVER_URL.rstrip("/")
|
|
584
|
+
ui.info("Starting browser profile setup with Glass...")
|
|
585
|
+
code, body = _post_json(_browser_profile_setup_start_url(server), {"token": token}, timeout=120)
|
|
586
|
+
if not (200 <= code < 300):
|
|
587
|
+
ui.error(body.get("detail") or body.get("error") or f"Glass could not start the profile setup (HTTP {code}).")
|
|
588
|
+
sys.exit(1)
|
|
589
|
+
|
|
590
|
+
session_id = str(body.get("session_id") or "").strip()
|
|
591
|
+
viewer_url = str(body.get("viewer_url") or body.get("debug_url") or "").strip()
|
|
592
|
+
before_ids = [str(p) for p in (body.get("before_profile_ids") or []) if p]
|
|
593
|
+
if not session_id or not viewer_url:
|
|
594
|
+
ui.error("Glass returned an incomplete browser setup session.")
|
|
595
|
+
sys.exit(1)
|
|
596
|
+
|
|
597
|
+
ui.success("Steel setup session started.")
|
|
598
|
+
ui.info(f"Viewer URL: {viewer_url}")
|
|
599
|
+
try:
|
|
600
|
+
webbrowser.open(viewer_url)
|
|
601
|
+
except Exception:
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
finish_cmd = _browser_profile_finish_command(token, session_id, before_ids)
|
|
605
|
+
if not ui.interactive():
|
|
606
|
+
ui.info("When the browser is configured, finish with:")
|
|
607
|
+
print(f" {finish_cmd}")
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
print()
|
|
611
|
+
ui.info("Use the browser window to log in or configure the profile.")
|
|
612
|
+
ui.info("Press Enter here when you're done. Ctrl+C leaves the session open; finish later with:")
|
|
613
|
+
print(f" {finish_cmd}")
|
|
614
|
+
try:
|
|
615
|
+
input()
|
|
616
|
+
except KeyboardInterrupt:
|
|
617
|
+
print()
|
|
618
|
+
ui.warn("Setup session left open.")
|
|
619
|
+
ui.info(f"Finish later with: {finish_cmd}")
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
browser_profile_finish(token, session_id, ",".join(before_ids))
|
|
623
|
+
|
|
624
|
+
|
|
509
625
|
def _silicon_display(silicon: dict) -> str:
|
|
510
626
|
name = str(silicon.get("name") or "").strip()
|
|
511
627
|
sid = str(silicon.get("silicon_id") or "").strip()
|
|
@@ -622,6 +738,10 @@ def _pull_team(api_key: str, server: str) -> None:
|
|
|
622
738
|
for instance_name, target in pulled:
|
|
623
739
|
ui.info(f" {instance_name}: {target}")
|
|
624
740
|
|
|
741
|
+
ui.info("Starting pulled silicons...")
|
|
742
|
+
for instance_name, _target in pulled:
|
|
743
|
+
process.start_one(instance_name)
|
|
744
|
+
|
|
625
745
|
if ui.interactive() and ui.confirm("Enable daily 23:59 UTC backups for all pulled silicons?"):
|
|
626
746
|
for instance_name, target in pulled:
|
|
627
747
|
ui.info(f"Running initial backup for '{instance_name}'...")
|
|
@@ -679,6 +799,7 @@ def pull(api_token: str | None) -> None:
|
|
|
679
799
|
|
|
680
800
|
ui.success(f"Pulled Glass silicon '{silicon_name or silicon_id}' into {target}")
|
|
681
801
|
ui.info(f"Registered as '{instance_name}'.")
|
|
802
|
+
process.start_one(instance_name)
|
|
682
803
|
|
|
683
804
|
if ui.interactive() and ui.confirm("Enable daily 23:59 UTC backups for this silicon?"):
|
|
684
805
|
ui.info("Running initial backup...")
|
|
@@ -50,8 +50,11 @@ def _update_one(target: str | None, tmp_src: str, updater: str, multi: bool) ->
|
|
|
50
50
|
sys.exit(1)
|
|
51
51
|
|
|
52
52
|
ui.info(f"Updating '{inst.name}' safely...")
|
|
53
|
-
r = subprocess.run([python_run_cmd(), updater, "update", "--source", tmp_src, "--target", inst.path])
|
|
53
|
+
r = subprocess.run([python_run_cmd(inst.path), updater, "update", "--source", tmp_src, "--target", inst.path])
|
|
54
54
|
if r.returncode == 0:
|
|
55
|
+
req = Path(inst.path) / "requirements.txt"
|
|
56
|
+
if req.exists():
|
|
57
|
+
stemcell.install_requirements(Path(inst.path), req)
|
|
55
58
|
ui.success(f"'{inst.name}' updated successfully")
|
|
56
59
|
elif r.returncode == 2:
|
|
57
60
|
ui.error(f"Update aborted — merge conflicts detected in '{inst.name}'. No files overwritten.")
|
|
@@ -87,9 +90,9 @@ def trigger_update_check(target: str | None) -> None:
|
|
|
87
90
|
main_py = root / "main.py"
|
|
88
91
|
|
|
89
92
|
if updater.exists():
|
|
90
|
-
cmd = [python_run_cmd(), str(updater)]
|
|
93
|
+
cmd = [python_run_cmd(inst.path), str(updater)]
|
|
91
94
|
elif main_py.exists():
|
|
92
|
-
cmd = [python_run_cmd(), "main.py", "update-check"]
|
|
95
|
+
cmd = [python_run_cmd(inst.path), "main.py", "update-check"]
|
|
93
96
|
else:
|
|
94
97
|
ui.error(f"'{inst.name}' does not look like a stemcell with update.py or main.py.")
|
|
95
98
|
sys.exit(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: silicon-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.11
|
|
4
4
|
Summary: Silicon CLI — create, run, and manage your silicon instances.
|
|
5
5
|
Author: Saket
|
|
6
6
|
Project-URL: Homepage, https://github.com/saket1225/silicon-cli
|
|
@@ -13,6 +13,7 @@ Requires-Python: >=3.9
|
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: certifi>=2024.2.2
|
|
16
|
+
Dynamic: license-file
|
|
16
17
|
|
|
17
18
|
# silicon-cli
|
|
18
19
|
|
|
@@ -40,12 +41,14 @@ pip install silicon-cli
|
|
|
40
41
|
silicon Show status or list instances
|
|
41
42
|
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
42
43
|
silicon new . Hydrate the current folder into a runnable silicon
|
|
43
|
-
silicon start <target> Start silicon(s). target = name,
|
|
44
|
-
silicon stop [--full] <target> Stop silicon(s)
|
|
45
|
-
silicon restart <target> Restart silicon(s)
|
|
44
|
+
silicon start <target> Start silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
45
|
+
silicon stop [--full] <target> Stop silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
46
|
+
silicon restart <target> Restart silicon(s). target = name, *, all, 1,2,4, or name,name
|
|
46
47
|
silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
|
|
47
48
|
silicon status [name] Show instance status
|
|
48
49
|
silicon browser [name] Open a headed browser for login
|
|
50
|
+
silicon browser-profile setup <token> Create a Steel browser profile through Glass
|
|
51
|
+
silicon browser-profile finish <token> <session_id> [before_ids_csv] Finish profile setup
|
|
49
52
|
silicon debug [name] Tail a running instance's logs
|
|
50
53
|
silicon attach [path] Register an existing silicon directory
|
|
51
54
|
silicon pull [api_token] Pull a Glass team or silicon into local folders
|
|
@@ -92,8 +95,8 @@ silicon pull sct_live_...
|
|
|
92
95
|
|
|
93
96
|
The command validates the token with Glass, mints one local silicon API key per
|
|
94
97
|
team silicon, creates one folder per silicon, hydrates the stemcell, writes
|
|
95
|
-
`.glass.json`, `.env`, and `env.py`, registers each instance,
|
|
96
|
-
Silicon Interface daemon.
|
|
98
|
+
`.glass.json`, `.env`, and `env.py`, registers each instance, starts each
|
|
99
|
+
Silicon Interface daemon, and starts each silicon process.
|
|
97
100
|
|
|
98
101
|
During team pull, setup asks for the default brain/fallback settings once. You
|
|
99
102
|
can apply those settings to every silicon, or select specific silicons that
|
|
@@ -102,9 +105,9 @@ Older per-silicon `scs_live_...` tokens still pull just that one silicon.
|
|
|
102
105
|
|
|
103
106
|
Provider API keys for voice, browser profiles, billing, and architecture
|
|
104
107
|
generation are configured on the Glass backend. After the silicon API token is
|
|
105
|
-
validated, `silicon pull` reads the team's provider-key metadata from Glass
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
validated, `silicon pull` reads the team's provider-key metadata from Glass and
|
|
109
|
+
automatically marks the pulled silicon(s) to use Glass-managed keys when every
|
|
110
|
+
provider is available. Plaintext secrets are never returned to the CLI. A
|
|
108
111
|
Glass-pulled silicon only stores its Glass API token plus a local marker that
|
|
109
112
|
provider keys come from Glass.
|
|
110
113
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|