silicon-cli 1.0.0__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.0/LICENSE +21 -0
- silicon_cli-1.0.0/PKG-INFO +74 -0
- silicon_cli-1.0.0/README.md +58 -0
- silicon_cli-1.0.0/pyproject.toml +28 -0
- silicon_cli-1.0.0/setup.cfg +4 -0
- silicon_cli-1.0.0/silicon_cli/__init__.py +2 -0
- silicon_cli-1.0.0/silicon_cli/cli.py +250 -0
- silicon_cli-1.0.0/silicon_cli/config.py +29 -0
- silicon_cli-1.0.0/silicon_cli/glassagent.py +66 -0
- silicon_cli-1.0.0/silicon_cli/process.py +258 -0
- silicon_cli-1.0.0/silicon_cli/registry.py +145 -0
- silicon_cli-1.0.0/silicon_cli/stemcell.py +226 -0
- silicon_cli-1.0.0/silicon_cli/sync.py +216 -0
- silicon_cli-1.0.0/silicon_cli/ui.py +61 -0
- silicon_cli-1.0.0/silicon_cli/update.py +80 -0
- silicon_cli-1.0.0/silicon_cli.egg-info/PKG-INFO +74 -0
- silicon_cli-1.0.0/silicon_cli.egg-info/SOURCES.txt +18 -0
- silicon_cli-1.0.0/silicon_cli.egg-info/dependency_links.txt +1 -0
- silicon_cli-1.0.0/silicon_cli.egg-info/entry_points.txt +2 -0
- silicon_cli-1.0.0/silicon_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Saket
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: silicon-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Silicon CLI — create, run, and manage your silicon instances.
|
|
5
|
+
Author: Saket
|
|
6
|
+
Project-URL: Homepage, https://github.com/saket1225/silicon-cli
|
|
7
|
+
Project-URL: Repository, https://github.com/saket1225/silicon-cli
|
|
8
|
+
Keywords: silicon,cli,agents
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# silicon-cli
|
|
18
|
+
|
|
19
|
+
Our own **`silicon`** CLI — a Python (pip-installable) port of the original bash
|
|
20
|
+
silicon manager. Manages silicon instances on a machine: create them from the
|
|
21
|
+
[silicon-stemcell](https://github.com/unlikefraction/silicon-stemcell) base,
|
|
22
|
+
start/stop them under an auto-restart watchdog, stream logs, and back them up to
|
|
23
|
+
Glass. It reads the same `~/.silicon/registry.json`, so existing installs carry
|
|
24
|
+
over unchanged.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install silicon-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
(Zero runtime dependencies — stdlib only.)
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
silicon Show status or list instances
|
|
38
|
+
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
39
|
+
silicon new . Hydrate the current folder into a runnable silicon
|
|
40
|
+
silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
|
|
41
|
+
silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
|
|
42
|
+
silicon restart <target> Restart silicon(s)
|
|
43
|
+
silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
|
|
44
|
+
silicon status [name] Show instance status
|
|
45
|
+
silicon browser [name] Open a headed browser for login
|
|
46
|
+
silicon debug [name] Tail a running instance's logs
|
|
47
|
+
silicon attach [path] Register an existing silicon directory
|
|
48
|
+
silicon pull <username> Pull a silicon from Glass into a new folder
|
|
49
|
+
silicon push [name] [now|stop] Hourly backups to Glass (now = one-shot, stop = end loop)
|
|
50
|
+
silicon update <target> Update silicon(s) from the latest stemcell
|
|
51
|
+
silicon list List all instances
|
|
52
|
+
silicon script update Update this CLI itself
|
|
53
|
+
silicon help Show help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration (env vars)
|
|
57
|
+
|
|
58
|
+
| Var | Default | Purpose |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `SILICON_HOME` | `~/.silicon` | registry + CLI state |
|
|
61
|
+
| `GLASS_SERVER_URL` | `https://glass.unlikefraction.com` | Glass sync server (pull/push) |
|
|
62
|
+
| `SILICON_STEMCELL_REPO` | `unlikefraction/silicon-stemcell` | base for `new` |
|
|
63
|
+
| `SILICON_GLASS_CLI_REPO` | `unlikefraction/glass` | glass backup CLI |
|
|
64
|
+
| `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
|
|
65
|
+
|
|
66
|
+
## How it differs from the bash version
|
|
67
|
+
|
|
68
|
+
- Pure Python package with a `silicon` console entry point (installed in an
|
|
69
|
+
isolated venv), instead of a single bash script.
|
|
70
|
+
- The auto-restart watchdog runs as `silicon _watchdog` (a detached supervisor
|
|
71
|
+
process) rather than a backgrounded bash function — same crash-loop detection,
|
|
72
|
+
`.silicon.stop` sentinel, and `.silicon.pid`/`.silicon.log` behavior.
|
|
73
|
+
- `silicon script update` reinstalls the package via pip from its recorded source.
|
|
74
|
+
- Everything (server, stemcell repo) is env-overridable.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# silicon-cli
|
|
2
|
+
|
|
3
|
+
Our own **`silicon`** CLI — a Python (pip-installable) port of the original bash
|
|
4
|
+
silicon manager. Manages silicon instances on a machine: create them from the
|
|
5
|
+
[silicon-stemcell](https://github.com/unlikefraction/silicon-stemcell) base,
|
|
6
|
+
start/stop them under an auto-restart watchdog, stream logs, and back them up to
|
|
7
|
+
Glass. It reads the same `~/.silicon/registry.json`, so existing installs carry
|
|
8
|
+
over unchanged.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install silicon-cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(Zero runtime dependencies — stdlib only.)
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
silicon Show status or list instances
|
|
22
|
+
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
23
|
+
silicon new . Hydrate the current folder into a runnable silicon
|
|
24
|
+
silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
|
|
25
|
+
silicon stop [--full] <target> Stop silicon(s) (--full also stops the glass agent)
|
|
26
|
+
silicon restart <target> Restart silicon(s)
|
|
27
|
+
silicon agent <start|stop|status> [name] Manage the per-silicon glass agent
|
|
28
|
+
silicon status [name] Show instance status
|
|
29
|
+
silicon browser [name] Open a headed browser for login
|
|
30
|
+
silicon debug [name] Tail a running instance's logs
|
|
31
|
+
silicon attach [path] Register an existing silicon directory
|
|
32
|
+
silicon pull <username> Pull a silicon from Glass into a new folder
|
|
33
|
+
silicon push [name] [now|stop] Hourly backups to Glass (now = one-shot, stop = end loop)
|
|
34
|
+
silicon update <target> Update silicon(s) from the latest stemcell
|
|
35
|
+
silicon list List all instances
|
|
36
|
+
silicon script update Update this CLI itself
|
|
37
|
+
silicon help Show help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration (env vars)
|
|
41
|
+
|
|
42
|
+
| Var | Default | Purpose |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `SILICON_HOME` | `~/.silicon` | registry + CLI state |
|
|
45
|
+
| `GLASS_SERVER_URL` | `https://glass.unlikefraction.com` | Glass sync server (pull/push) |
|
|
46
|
+
| `SILICON_STEMCELL_REPO` | `unlikefraction/silicon-stemcell` | base for `new` |
|
|
47
|
+
| `SILICON_GLASS_CLI_REPO` | `unlikefraction/glass` | glass backup CLI |
|
|
48
|
+
| `SILICON_PYTHON` | `python3` | interpreter used to run a silicon's `main.py` |
|
|
49
|
+
|
|
50
|
+
## How it differs from the bash version
|
|
51
|
+
|
|
52
|
+
- Pure Python package with a `silicon` console entry point (installed in an
|
|
53
|
+
isolated venv), instead of a single bash script.
|
|
54
|
+
- The auto-restart watchdog runs as `silicon _watchdog` (a detached supervisor
|
|
55
|
+
process) rather than a backgrounded bash function — same crash-loop detection,
|
|
56
|
+
`.silicon.stop` sentinel, and `.silicon.pid`/`.silicon.log` behavior.
|
|
57
|
+
- `silicon script update` reinstalls the package via pip from its recorded source.
|
|
58
|
+
- Everything (server, stemcell repo) is env-overridable.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "silicon-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Silicon CLI — create, run, and manage your silicon instances."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [{ name = "Saket" }]
|
|
12
|
+
keywords = ["silicon", "cli", "agents"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [] # stdlib only — keeps the install bulletproof
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/saket1225/silicon-cli"
|
|
22
|
+
Repository = "https://github.com/saket1225/silicon-cli"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
silicon = "silicon_cli.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
packages = ["silicon_cli"]
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""silicon — manage your silicon instances. Dispatch mirrors the original bash CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import glassagent, process, registry, stemcell, sync, ui, update
|
|
9
|
+
from .config import python_run_cmd
|
|
10
|
+
|
|
11
|
+
COMMANDS = ["start", "stop", "restart", "status", "browser", "debug", "attach",
|
|
12
|
+
"pull", "push", "update", "list", "install", "new", "help", "script", "agent"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ----------------------------------------------------------------- commands
|
|
16
|
+
def cmd_list() -> None:
|
|
17
|
+
rows = registry.installs()
|
|
18
|
+
if not rows:
|
|
19
|
+
ui.info("No silicon installations found.")
|
|
20
|
+
ui.info("Run 'silicon install' to set up a new instance.")
|
|
21
|
+
return
|
|
22
|
+
print(f"\n{ui.BOLD}{ui.CYAN}Silicon Installations{ui.RESET}\n")
|
|
23
|
+
print(f" {ui.DIM}{'#':<4}{'NAME':<22}{'STATUS':<10}PATH{ui.RESET}")
|
|
24
|
+
print(f" {ui.DIM}{'---':<4}{'----':<22}{'------':<10}----{ui.RESET}")
|
|
25
|
+
for i in rows:
|
|
26
|
+
if process.is_running(i.pid_file):
|
|
27
|
+
pid = process.get_pid(i.pid_file)
|
|
28
|
+
status = f"{ui.GREEN}● running{ui.RESET}"
|
|
29
|
+
extra = f" {ui.DIM}(PID {pid}){ui.RESET}"
|
|
30
|
+
else:
|
|
31
|
+
status, extra = f"{ui.DIM}○ stopped{ui.RESET}", ""
|
|
32
|
+
print(f" {i.index + 1:<4}{i.name:<22}{status}{extra} {ui.DIM}{i.path}{ui.RESET}")
|
|
33
|
+
print()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _print_status(inst) -> None:
|
|
37
|
+
if process.is_running(inst.pid_file):
|
|
38
|
+
pid = process.get_pid(inst.pid_file)
|
|
39
|
+
print(f"\n{ui.BOLD}{inst.name}{ui.RESET} {ui.GREEN}● running{ui.RESET} (PID {pid})")
|
|
40
|
+
else:
|
|
41
|
+
print(f"\n{ui.BOLD}{inst.name}{ui.RESET} {ui.DIM}○ stopped{ui.RESET}")
|
|
42
|
+
print(f"{ui.DIM} Path: {inst.path}{ui.RESET}\n")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_status(target: str | None) -> None:
|
|
46
|
+
if target:
|
|
47
|
+
inst = registry.find(target)
|
|
48
|
+
if not inst:
|
|
49
|
+
cmd_list()
|
|
50
|
+
return
|
|
51
|
+
_print_status(inst)
|
|
52
|
+
return
|
|
53
|
+
inst = registry.find()
|
|
54
|
+
if inst:
|
|
55
|
+
_print_status(inst)
|
|
56
|
+
else:
|
|
57
|
+
cmd_list()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def cmd_browser(target: str | None) -> None:
|
|
61
|
+
inst = registry.resolve_one(target)
|
|
62
|
+
ui.info(f"Opening browser for '{inst.name}'...")
|
|
63
|
+
subprocess.run([python_run_cmd(), "main.py", "browser"], cwd=inst.path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def cmd_debug(target: str | None) -> None:
|
|
67
|
+
inst = registry.resolve_one(target)
|
|
68
|
+
if not process.is_running(inst.pid_file):
|
|
69
|
+
ui.error(f"'{inst.name}' is not running. Start it first with: silicon start {inst.name}")
|
|
70
|
+
sys.exit(1)
|
|
71
|
+
log_file = Path(inst.path) / ".silicon.log"
|
|
72
|
+
if not log_file.exists():
|
|
73
|
+
ui.error(f"No log file found at {log_file}")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
pid = process.get_pid(inst.pid_file)
|
|
76
|
+
print(f"\n{ui.BOLD}{ui.CYAN}Debugging '{inst.name}'{ui.RESET} (PID {pid})")
|
|
77
|
+
print(f"{ui.DIM} Log: {log_file}{ui.RESET}")
|
|
78
|
+
print(f"{ui.DIM} Press Ctrl+C to detach{ui.RESET}\n")
|
|
79
|
+
try:
|
|
80
|
+
subprocess.run(["tail", "-f", str(log_file)])
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_attach(target_dir: str | None) -> None:
|
|
86
|
+
target = Path(target_dir or ".").resolve()
|
|
87
|
+
if not (target / "main.py").exists() or not (target / "config.py").exists():
|
|
88
|
+
ui.error("This doesn't look like a silicon directory.")
|
|
89
|
+
ui.info(f"Expected main.py and config.py in: {target}")
|
|
90
|
+
ui.info(" silicon attach /path/to/silicon")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
if not (target / "prompts").is_dir() or not (target / "core").is_dir():
|
|
93
|
+
ui.error("Missing prompts/ or core/ directory. Not a valid silicon.")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
for i in registry.installs():
|
|
97
|
+
if i.path == str(target):
|
|
98
|
+
ui.warn(f"This silicon is already registered as '{i.name}'")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
ui.success(f"Found a silicon at: {target}")
|
|
102
|
+
name = ui.ask("Instance name", target.name)
|
|
103
|
+
if registry.name_taken(name):
|
|
104
|
+
ui.error(f"Name '{name}' is already taken. Pick a different one.")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
pid_file = target / ".silicon.pid"
|
|
108
|
+
running = process.is_running(str(pid_file))
|
|
109
|
+
registry.register(name, str(target), str(pid_file))
|
|
110
|
+
ui.success(f"Attached '{name}' at {target}")
|
|
111
|
+
if running:
|
|
112
|
+
print(f"\n {ui.BOLD}{name}{ui.RESET} {ui.GREEN}● running{ui.RESET} (PID {process.get_pid(str(pid_file))})\n")
|
|
113
|
+
else:
|
|
114
|
+
print(f"\n {ui.BOLD}{name}{ui.RESET} {ui.DIM}○ stopped{ui.RESET}")
|
|
115
|
+
print(f" Start it with: {ui.BOLD}silicon start {name}{ui.RESET}\n")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_agent(subcmd: str | None, target: str | None) -> None:
|
|
119
|
+
if not subcmd:
|
|
120
|
+
ui.error("Usage: silicon agent <start|stop|status> [name]")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
inst = registry.resolve_one(target)
|
|
123
|
+
if subcmd == "start":
|
|
124
|
+
glassagent.start(inst.path)
|
|
125
|
+
elif subcmd == "stop":
|
|
126
|
+
glassagent.stop(inst.path)
|
|
127
|
+
elif subcmd == "status":
|
|
128
|
+
if glassagent.status(inst.path):
|
|
129
|
+
pid = (Path(inst.path) / ".glass_agent.pid").read_text().strip()
|
|
130
|
+
print(f"{ui.GREEN}●{ui.RESET} Glass agent running (PID {pid})")
|
|
131
|
+
else:
|
|
132
|
+
print(f"{ui.DIM}○{ui.RESET} Glass agent stopped")
|
|
133
|
+
else:
|
|
134
|
+
ui.error(f"Unknown agent command: {subcmd}. Use start, stop, or status.")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_new(target: str | None) -> None:
|
|
139
|
+
if target:
|
|
140
|
+
stemcell.hydrate(target)
|
|
141
|
+
return
|
|
142
|
+
# No target: ask for a folder to create (Python-native installer).
|
|
143
|
+
name = ui.ask("New silicon folder name", "silicon")
|
|
144
|
+
if not name:
|
|
145
|
+
ui.error("A folder name is required.")
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
stemcell.hydrate(str(Path.cwd() / name))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_help() -> None:
|
|
151
|
+
print(f"""
|
|
152
|
+
{ui.BOLD}{ui.CYAN}silicon{ui.RESET} – manage your silicon instances
|
|
153
|
+
|
|
154
|
+
{ui.BOLD}Usage:{ui.RESET}
|
|
155
|
+
silicon Show status or list instances
|
|
156
|
+
silicon new [dir] Create a new Silicon (hydrate from stemcell)
|
|
157
|
+
silicon new . Hydrate the current folder into a runnable silicon
|
|
158
|
+
silicon start <target> Start silicon(s). target = name, index, 1,2,4, or all
|
|
159
|
+
silicon stop <target> Stop silicon(s) (agent stays alive)
|
|
160
|
+
silicon stop --full <target> Stop silicon(s) and glass agent
|
|
161
|
+
silicon restart <target> Restart silicon(s)
|
|
162
|
+
silicon agent <start|stop|status> [name] Manage glass agent
|
|
163
|
+
silicon status [name] Show instance status
|
|
164
|
+
silicon browser [name] Open headed browser for login
|
|
165
|
+
silicon debug [name] Attach to running instance (live logs)
|
|
166
|
+
silicon attach [path] Register an existing silicon instance
|
|
167
|
+
silicon pull <username> Pull a silicon from Glass into a new folder
|
|
168
|
+
silicon push [name] Start hourly backup loop to Glass
|
|
169
|
+
silicon push [name] now Push a one-time backup to Glass
|
|
170
|
+
silicon push [name] stop Stop the hourly backup loop
|
|
171
|
+
silicon update <target> Update silicon(s) to latest. target = name, index, 1,2,4, or all
|
|
172
|
+
silicon list List all instances
|
|
173
|
+
silicon script update Update the silicon CLI itself
|
|
174
|
+
silicon install Install a new instance
|
|
175
|
+
silicon help Show this help
|
|
176
|
+
""")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def suggest_command(inp: str) -> None:
|
|
180
|
+
def score(cmd: str) -> int:
|
|
181
|
+
ld = abs(len(inp) - len(cmd))
|
|
182
|
+
if cmd.startswith(inp) or inp.startswith(cmd):
|
|
183
|
+
return ld
|
|
184
|
+
common = sum(1 for a, b in zip(inp, cmd) if a == b)
|
|
185
|
+
return max(len(cmd), len(inp)) - common + ld
|
|
186
|
+
best = min(COMMANDS + ["ls"], key=score)
|
|
187
|
+
if score(best) <= 3:
|
|
188
|
+
print(f"\n{ui.YELLOW}Did you mean?{ui.RESET}\n silicon {best}\n")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ----------------------------------------------------------------- dispatch
|
|
192
|
+
def main(argv: list[str] | None = None) -> None:
|
|
193
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
194
|
+
cmd = argv[0] if argv else ""
|
|
195
|
+
a1 = argv[1] if len(argv) > 1 else None
|
|
196
|
+
a2 = argv[2] if len(argv) > 2 else None
|
|
197
|
+
|
|
198
|
+
if cmd == "_watchdog": # internal: the supervised auto-restart loop
|
|
199
|
+
process.watchdog_loop(name=a2 or "silicon", path=a1, pid_file=argv[3] if len(argv) > 3 else "")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if cmd == "start":
|
|
203
|
+
process.start(a1)
|
|
204
|
+
elif cmd == "stop":
|
|
205
|
+
if a1 == "--full":
|
|
206
|
+
process.stop(a2, full=True)
|
|
207
|
+
else:
|
|
208
|
+
process.stop(a1, full=(a2 == "--full"))
|
|
209
|
+
elif cmd == "restart":
|
|
210
|
+
process.restart(a1)
|
|
211
|
+
elif cmd == "status":
|
|
212
|
+
cmd_status(a1)
|
|
213
|
+
elif cmd == "browser":
|
|
214
|
+
cmd_browser(a1)
|
|
215
|
+
elif cmd == "debug":
|
|
216
|
+
cmd_debug(a1)
|
|
217
|
+
elif cmd == "attach":
|
|
218
|
+
cmd_attach(a1)
|
|
219
|
+
elif cmd == "pull":
|
|
220
|
+
sync.pull(a1)
|
|
221
|
+
elif cmd == "push":
|
|
222
|
+
sync.push(a1, a2)
|
|
223
|
+
elif cmd == "update":
|
|
224
|
+
update.update_instance(a1)
|
|
225
|
+
elif cmd in ("list", "ls"):
|
|
226
|
+
cmd_list()
|
|
227
|
+
elif cmd == "agent":
|
|
228
|
+
cmd_agent(a1, a2)
|
|
229
|
+
elif cmd == "script":
|
|
230
|
+
if a1 == "update":
|
|
231
|
+
update.update_cli()
|
|
232
|
+
else:
|
|
233
|
+
ui.error(f"Unknown script command: {a1}. Did you mean: silicon script update?")
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
elif cmd == "new":
|
|
236
|
+
cmd_new(a1)
|
|
237
|
+
elif cmd == "install":
|
|
238
|
+
cmd_new(None)
|
|
239
|
+
elif cmd in ("help", "-h", "--help"):
|
|
240
|
+
cmd_help()
|
|
241
|
+
elif cmd == "":
|
|
242
|
+
cmd_status(None)
|
|
243
|
+
else:
|
|
244
|
+
ui.error(f"Unknown command: {cmd}")
|
|
245
|
+
suggest_command(cmd)
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Paths + endpoints. Everything is env-overridable so this CLI can point at
|
|
2
|
+
either the original Glass or your own."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
HOME = Path.home()
|
|
10
|
+
REGISTRY_DIR = Path(os.environ.get("SILICON_HOME", HOME / ".silicon"))
|
|
11
|
+
REGISTRY_FILE = REGISTRY_DIR / "registry.json"
|
|
12
|
+
CLI_SOURCE_FILE = REGISTRY_DIR / "cli-source" # where `silicon script update` reinstalls from
|
|
13
|
+
|
|
14
|
+
# Glass sync server (pull/push). Kept as the original default for compatibility;
|
|
15
|
+
# override with GLASS_SERVER_URL to point at your own.
|
|
16
|
+
GLASS_SERVER_URL = os.environ.get("GLASS_SERVER_URL", "https://glass.unlikefraction.com").rstrip("/")
|
|
17
|
+
|
|
18
|
+
# Stemcell — the base every new silicon is hydrated from.
|
|
19
|
+
STEMCELL_REPO = os.environ.get("SILICON_STEMCELL_REPO", "unlikefraction/silicon-stemcell")
|
|
20
|
+
STEMCELL_GIT_URL = f"https://github.com/{STEMCELL_REPO}.git"
|
|
21
|
+
STEMCELL_ZIP_URL = f"https://github.com/{STEMCELL_REPO}/archive/refs/heads/main.zip"
|
|
22
|
+
|
|
23
|
+
# Glass CLI (used by pull/push for backups).
|
|
24
|
+
GLASS_CLI_REPO = os.environ.get("SILICON_GLASS_CLI_REPO", "unlikefraction/glass")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def python_run_cmd() -> str:
|
|
28
|
+
"""The interpreter used to RUN a silicon's main.py (not this CLI's venv)."""
|
|
29
|
+
return os.environ.get("SILICON_PYTHON") or shutil.which("python3") or shutil.which("python") or "python3"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""The per-silicon Glass agent (glass_agent.py) — remote control / backups.
|
|
2
|
+
|
|
3
|
+
Only relevant when the silicon dir has a .glass.json. Tracked via .glass_agent.pid.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from . import ui
|
|
14
|
+
from .config import python_run_cmd
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _pid_file(path: str) -> Path:
|
|
18
|
+
return Path(path) / ".glass_agent.pid"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read_pid(path: str) -> int | None:
|
|
22
|
+
try:
|
|
23
|
+
return int(_pid_file(path).read_text().strip())
|
|
24
|
+
except Exception:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _alive(pid: int) -> bool:
|
|
29
|
+
try:
|
|
30
|
+
os.kill(pid, 0)
|
|
31
|
+
return True
|
|
32
|
+
except (OSError, ProcessLookupError):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def status(path: str) -> bool:
|
|
37
|
+
pid = _read_pid(path)
|
|
38
|
+
return bool(pid and _alive(pid))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def start(path: str) -> None:
|
|
42
|
+
if not (Path(path) / ".glass.json").exists():
|
|
43
|
+
return
|
|
44
|
+
if status(path):
|
|
45
|
+
return
|
|
46
|
+
log = open(Path(path) / ".glass_agent.log", "a")
|
|
47
|
+
proc = subprocess.Popen(
|
|
48
|
+
[python_run_cmd(), "-u", "glass_agent.py"], cwd=path,
|
|
49
|
+
stdout=log, stderr=subprocess.STDOUT, start_new_session=True,
|
|
50
|
+
)
|
|
51
|
+
_pid_file(path).write_text(str(proc.pid))
|
|
52
|
+
ui.info(f"Glass agent started (PID {proc.pid})")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def stop(path: str) -> None:
|
|
56
|
+
pid = _read_pid(path)
|
|
57
|
+
if pid and _alive(pid):
|
|
58
|
+
try:
|
|
59
|
+
os.kill(pid, signal.SIGTERM)
|
|
60
|
+
time.sleep(1)
|
|
61
|
+
if _alive(pid):
|
|
62
|
+
os.kill(pid, signal.SIGKILL)
|
|
63
|
+
except (OSError, ProcessLookupError):
|
|
64
|
+
pass
|
|
65
|
+
ui.info("Glass agent stopped")
|
|
66
|
+
_pid_file(path).unlink(missing_ok=True)
|