@bytespell/shella 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -1
- package/bin/cli.js +184 -87
- package/dev/cli.tsx +38 -0
- package/dist/config/openai-codex-models.json +205 -0
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.js +67 -8
- package/dist/server/lib/opencode-client.d.ts +14 -0
- package/dist/server/lib/opencode-client.js +17 -0
- package/dist/server/lib/opencode-config.d.ts +14 -0
- package/dist/server/lib/opencode-config.js +25 -0
- package/dist/server/routes/config.d.ts +12 -0
- package/dist/server/routes/config.js +207 -0
- package/dist/server/routes/directory.d.ts +8 -0
- package/dist/server/routes/directory.js +84 -0
- package/dist/server/routes/init.d.ts +4 -3
- package/dist/server/routes/init.js +23 -9
- package/dist/server/routes/local-llm.d.ts +8 -0
- package/dist/server/routes/local-llm.js +255 -0
- package/dist/server/routes/logs.js +35 -11
- package/dist/server/routes/prompt.d.ts +16 -0
- package/dist/server/routes/prompt.js +173 -0
- package/dist/server/routes/session.d.ts +8 -0
- package/dist/server/routes/session.js +63 -0
- package/dist/server/routes/status.d.ts +9 -0
- package/dist/server/routes/status.js +54 -0
- package/dist/server/routes/usage.d.ts +12 -0
- package/dist/server/routes/usage.js +60 -0
- package/dist/server/routes/windows.js +4 -4
- package/dist/server/schema.d.ts +47 -16
- package/dist/server/schema.js +8 -1
- package/dist/server/services/database.d.ts +10 -1
- package/dist/server/services/database.js +19 -6
- package/dist/web/assets/{_baseUniq-BXqY9Mam.js → _baseUniq-BxVG561Z.js} +1 -1
- package/dist/web/assets/{arc-Bn6tUpO_.js → arc-B9TFF79T.js} +1 -1
- package/dist/web/assets/{architectureDiagram-VXUJARFQ-C7FAApUY.js → architectureDiagram-VXUJARFQ-BMRLMpf8.js} +1 -1
- package/dist/web/assets/{blockDiagram-VD42YOAC-C2fdaEWa.js → blockDiagram-VD42YOAC-DBQKFxeQ.js} +1 -1
- package/dist/web/assets/{c4Diagram-YG6GDRKO-FEVzhARQ.js → c4Diagram-YG6GDRKO-TiYEZrdu.js} +1 -1
- package/dist/web/assets/channel-aOIIaiSs.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-DLekcSAU.js → chunk-4BX2VUAB-CWCp0N17.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-8hFRjyTP.js → chunk-55IACEB6-CeVCFKqv.js} +1 -1
- package/dist/web/assets/{chunk-B4BG7PRW-DULC9-MQ.js → chunk-B4BG7PRW-B-AoaHJt.js} +1 -1
- package/dist/web/assets/{chunk-DI55MBZ5-DuOE5RH1.js → chunk-DI55MBZ5-CCGkXnX-.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DaDNiCk7.js → chunk-FMBD7UC4-Bcupjeb_.js} +1 -1
- package/dist/web/assets/{chunk-QN33PNHL-CKshfIHj.js → chunk-QN33PNHL-DlyUQaTO.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-D2Qy0tdi.js → chunk-QZHKN3VN-THN-at_3.js} +1 -1
- package/dist/web/assets/{chunk-TZMSLE5B-SPxkj-lp.js → chunk-TZMSLE5B-CtErOFJM.js} +1 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-BFLMv18M.js +1 -0
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-BFLMv18M.js +1 -0
- package/dist/web/assets/clone-BR_FHSwu.js +1 -0
- package/dist/web/assets/{code-block-QI2IAROF-BZdAQmZ2.js → code-block-QI2IAROF-CPI-88R6.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DbasixUk.js → cose-bilkent-S5V4N54A-YrHmsLe4.js} +1 -1
- package/dist/web/assets/{dagre-6UL2VRFP-CStyjTc9.js → dagre-6UL2VRFP-Dcvw3qhj.js} +1 -1
- package/dist/web/assets/{diagram-PSM6KHXK-Crk93U8d.js → diagram-PSM6KHXK-B135EOe6.js} +1 -1
- package/dist/web/assets/{diagram-QEK2KX5R-DiW6RNbg.js → diagram-QEK2KX5R-w3KdB_-u.js} +1 -1
- package/dist/web/assets/{diagram-S2PKOQOG-CKksz_qL.js → diagram-S2PKOQOG-DYssvOTP.js} +1 -1
- package/dist/web/assets/{erDiagram-Q2GNP2WA-CisACqqq.js → erDiagram-Q2GNP2WA-DpnuE7B_.js} +1 -1
- package/dist/web/assets/{flowDiagram-NV44I4VS-BBp_5zAe.js → flowDiagram-NV44I4VS-BhcJ-8Yu.js} +1 -1
- package/dist/web/assets/{ganttDiagram-JELNMOA3-BKZ30gLA.js → ganttDiagram-JELNMOA3-ButVkRCz.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-NY62KEGX-ClizxUXq.js → gitGraphDiagram-NY62KEGX-ZLz8eoSo.js} +1 -1
- package/dist/web/assets/{graph-DqhaNOTU.js → graph-CKmCFGqF.js} +1 -1
- package/dist/web/assets/index-BHJDUcNL.js +1719 -0
- package/dist/web/assets/index-CcAJUkQw.css +1 -0
- package/dist/web/assets/index-DEiKajXR.js +1 -0
- package/dist/web/assets/{infoDiagram-WHAUD3N6-BQwNR0md.js → infoDiagram-WHAUD3N6-C_h94brE.js} +1 -1
- package/dist/web/assets/{journeyDiagram-XKPGCS4Q-YOqPPID4.js → journeyDiagram-XKPGCS4Q-u0bPRxxb.js} +1 -1
- package/dist/web/assets/{kanban-definition-3W4ZIXB7-Dtu8bvBx.js → kanban-definition-3W4ZIXB7-DkM-KD6Y.js} +1 -1
- package/dist/web/assets/{layout-Cc1ESzTe.js → layout-DGSU3MQw.js} +1 -1
- package/dist/web/assets/{linear-BwI2ANFG.js → linear-Dck9QCb9.js} +1 -1
- package/dist/web/assets/{mermaid.core-npIGP8NS.js → mermaid.core-DfB-jqaz.js} +5 -5
- package/dist/web/assets/{min--MKscDc6.js → min-CqEcl9J0.js} +1 -1
- package/dist/web/assets/{mindmap-definition-VGOIOE7T-Cr39Vhym.js → mindmap-definition-VGOIOE7T-D1KrSALz.js} +1 -1
- package/dist/web/assets/{pieDiagram-ADFJNKIX-Cv8ke00t.js → pieDiagram-ADFJNKIX-CZ-507Bd.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AYHSOK5B-BPhHaTg8.js → quadrantDiagram-AYHSOK5B-Jw0og6Ix.js} +1 -1
- package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Cc42SoK0.js → requirementDiagram-UZGBJVZJ-5JWD7TEH.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-TZEHDZUN-CtgBuq8T.js → sankeyDiagram-TZEHDZUN-DzlxPj37.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WL72ISMW-B9lNGN6V.js → sequenceDiagram-WL72ISMW-Cui1ykiA.js} +1 -1
- package/dist/web/assets/{stateDiagram-FKZM4ZOC-C3dRTOMb.js → stateDiagram-FKZM4ZOC-CCdGE_zt.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-CsLg9bzy.js +1 -0
- package/dist/web/assets/{timeline-definition-IT6M3QCI-CXhSuTlt.js → timeline-definition-IT6M3QCI-CP2T8mHI.js} +1 -1
- package/dist/web/assets/{treemap-KMMF4GRG-Csy25Uov.js → treemap-KMMF4GRG-DGBVlHVf.js} +1 -1
- package/dist/web/assets/welcome-screen-test-DnIwI3hf.js +1 -0
- package/dist/web/assets/{xychartDiagram-PRI3JC2R-CxEERqse.js → xychartDiagram-PRI3JC2R-DDlMipkA.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +14 -5
- package/dist/web/assets/channel-CxjnQtV7.js +0 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-CVG91-fs.js +0 -1
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-CVG91-fs.js +0 -1
- package/dist/web/assets/clone-C7jxvixc.js +0 -1
- package/dist/web/assets/index-B0jWvqrS.css +0 -1
- package/dist/web/assets/index-Dnmavb3d.js +0 -1716
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-oHTO1yj_.js +0 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# shella
|
|
2
2
|
|
|
3
3
|
Self-hosted AI coding agents. Access from your phone.
|
|
4
4
|
|
|
@@ -56,6 +56,54 @@ docker compose up -d
|
|
|
56
56
|
# Access at http://<server-ip>:3067
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## GitHub Authentication
|
|
60
|
+
|
|
61
|
+
AI agents may need to push code changes to GitHub. Shella uses the GitHub CLI (`gh`) for authentication.
|
|
62
|
+
|
|
63
|
+
### Docker Setup
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Start shella
|
|
67
|
+
docker compose up -d
|
|
68
|
+
|
|
69
|
+
# 2. Authenticate with GitHub (one-time setup)
|
|
70
|
+
docker exec -it shella gh auth login
|
|
71
|
+
|
|
72
|
+
# 3. Follow the device flow prompts
|
|
73
|
+
# - Choose "GitHub.com"
|
|
74
|
+
# - Choose "HTTPS"
|
|
75
|
+
# - Authenticate via browser
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Your authentication is stored in `/projects/.shella/gh/` and persists across container restarts and rebuilds.
|
|
79
|
+
|
|
80
|
+
### npx Usage
|
|
81
|
+
|
|
82
|
+
If running via `npx @bytespell/shella`, you need `gh` installed on your system:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Install gh CLI (macOS)
|
|
86
|
+
brew install gh
|
|
87
|
+
|
|
88
|
+
# Install gh CLI (Linux)
|
|
89
|
+
# See: https://github.com/cli/cli/blob/trunk/docs/install_linux.md
|
|
90
|
+
|
|
91
|
+
# Authenticate
|
|
92
|
+
gh auth login
|
|
93
|
+
|
|
94
|
+
# Run shella
|
|
95
|
+
npx @bytespell/shella
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Verify Authentication
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Check auth status
|
|
102
|
+
docker exec shella gh auth status
|
|
103
|
+
|
|
104
|
+
# Should show: "Logged in to github.com as <username>"
|
|
105
|
+
```
|
|
106
|
+
|
|
59
107
|
## Features
|
|
60
108
|
|
|
61
109
|
- **Parallel agents** - Run multiple agents on different tasks simultaneously
|
package/bin/cli.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import { createOpencodeServer } from '@opencode-ai/sdk';
|
|
4
|
-
import { networkInterfaces } from 'os';
|
|
4
|
+
import { networkInterfaces, homedir } from 'os';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { createConnection } from 'net';
|
|
7
|
-
import { existsSync, readdirSync } from 'fs';
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
+
import * as p from '@clack/prompts';
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const VERSION = '0.1.
|
|
12
|
+
const VERSION = '0.1.4';
|
|
13
|
+
// Brand color (indigo) for terminal output
|
|
14
|
+
const BRAND = '\x1b[38;2;99;102;241m'; // rgb(99, 102, 241) - matches --primary
|
|
15
|
+
const RESET = '\x1b[0m';
|
|
16
|
+
const DIM = '\x1b[2m';
|
|
17
|
+
const brand = (text) => `${BRAND}${text}${RESET}`;
|
|
18
|
+
const dim = (text) => `${DIM}${text}${RESET}`;
|
|
19
|
+
/**
|
|
20
|
+
* Load OpenCode config with plugin and model definitions.
|
|
21
|
+
*/
|
|
22
|
+
function getOpencodeConfig() {
|
|
23
|
+
const configPath = path.join(__dirname, '..', 'dist', 'config', 'openai-codex-models.json');
|
|
24
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Check if running inside Docker container
|
|
14
29
|
*/
|
|
@@ -35,6 +50,17 @@ async function isPortAvailable(port) {
|
|
|
35
50
|
});
|
|
36
51
|
});
|
|
37
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Find the next available port starting from a given port
|
|
55
|
+
*/
|
|
56
|
+
async function findAvailablePort(start, maxAttempts = 100) {
|
|
57
|
+
for (let port = start; port < start + maxAttempts; port++) {
|
|
58
|
+
if (await isPortAvailable(port)) {
|
|
59
|
+
return port;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
38
64
|
/**
|
|
39
65
|
* Get the LAN IP address for mobile access
|
|
40
66
|
*/
|
|
@@ -52,31 +78,6 @@ function getLanIp() {
|
|
|
52
78
|
}
|
|
53
79
|
return 'localhost';
|
|
54
80
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Print a nice boxed message with the access URL
|
|
57
|
-
*/
|
|
58
|
-
function printAccessBox(lanIp, port, projectName) {
|
|
59
|
-
const url = `http://${lanIp}:${port}`;
|
|
60
|
-
const urlLine = ` ${url}`;
|
|
61
|
-
const width = Math.max(44, urlLine.length + 4);
|
|
62
|
-
const border = '-'.repeat(width);
|
|
63
|
-
const pad = (s) => s + ' '.repeat(width - s.length);
|
|
64
|
-
console.log('');
|
|
65
|
-
console.log(`+${border}+`);
|
|
66
|
-
console.log(`|${pad('')}|`);
|
|
67
|
-
if (projectName) {
|
|
68
|
-
console.log(`|${pad(` Shella v${VERSION} - ${projectName}`)}|`);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
console.log(`|${pad(` Shella v${VERSION}`)}|`);
|
|
72
|
-
}
|
|
73
|
-
console.log(`|${pad('')}|`);
|
|
74
|
-
console.log(`|${pad(' Access from any device:')}|`);
|
|
75
|
-
console.log(`|${pad(urlLine)}|`);
|
|
76
|
-
console.log(`|${pad('')}|`);
|
|
77
|
-
console.log(`+${border}+`);
|
|
78
|
-
console.log('');
|
|
79
|
-
}
|
|
80
81
|
/**
|
|
81
82
|
* Register projects with OpenCode by creating a session in each directory.
|
|
82
83
|
* This triggers OpenCode to discover and register the project.
|
|
@@ -88,14 +89,14 @@ async function registerProjects(opencodePort, projectsDir) {
|
|
|
88
89
|
// Resolve to absolute path
|
|
89
90
|
const absDir = path.resolve(projectsDir);
|
|
90
91
|
if (!existsSync(absDir)) {
|
|
91
|
-
|
|
92
|
+
p.log.error(`projects directory not found: ${absDir}`);
|
|
92
93
|
process.exit(1);
|
|
93
94
|
}
|
|
94
95
|
// Register each subdirectory (OpenCode will determine if it's a git repo)
|
|
95
96
|
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
96
97
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'));
|
|
97
98
|
if (dirs.length === 0) {
|
|
98
|
-
|
|
99
|
+
p.log.error(`no subdirectories found in ${absDir}`);
|
|
99
100
|
process.exit(1);
|
|
100
101
|
}
|
|
101
102
|
for (const entry of dirs) {
|
|
@@ -143,29 +144,37 @@ async function startCommand(options) {
|
|
|
143
144
|
const port = parseInt(options.port);
|
|
144
145
|
const opencodePort = parseInt(options.opencodePort);
|
|
145
146
|
const cwd = process.cwd();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const verbose = options.verbose;
|
|
148
|
+
// Determine mode based on --projects-dir presence
|
|
149
|
+
const mode = options.projectsDir ? 'server' : 'cwd';
|
|
150
|
+
// Docker requires --projects-dir (server mode)
|
|
151
|
+
if (isDocker() && mode === 'cwd') {
|
|
152
|
+
p.log.error('--projects-dir is required when running in Docker');
|
|
153
|
+
p.log.message('example: docker run -v ~/code:/project shella --projects-dir /project');
|
|
154
|
+
p.log.message('or in docker-compose.yml: command: ["--projects-dir", "/project"]');
|
|
153
155
|
process.exit(1);
|
|
154
156
|
}
|
|
155
|
-
// Determine the
|
|
157
|
+
// Determine the directory to pass to the server
|
|
156
158
|
const projectsDir = options.projectsDir ? path.resolve(options.projectsDir) : cwd;
|
|
157
159
|
// Check if ports are available
|
|
158
160
|
const webPortAvailable = await isPortAvailable(port);
|
|
159
|
-
const ocPortAvailable = await isPortAvailable(opencodePort);
|
|
160
161
|
if (!webPortAvailable) {
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
p.log.error(`port ${port} is already in use`);
|
|
163
|
+
p.log.message('try: shella --port 3070');
|
|
163
164
|
process.exit(1);
|
|
164
165
|
}
|
|
166
|
+
// Check OpenCode port - use fallback if in use (likely another OpenCode instance)
|
|
167
|
+
let actualOpencodePort = opencodePort;
|
|
168
|
+
const ocPortAvailable = await isPortAvailable(opencodePort);
|
|
165
169
|
if (!ocPortAvailable) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
const fallbackPort = await findAvailablePort(opencodePort + 1);
|
|
171
|
+
if (!fallbackPort) {
|
|
172
|
+
p.log.error(`port ${opencodePort} is in use and no fallback ports available`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
actualOpencodePort = fallbackPort;
|
|
176
|
+
p.log.warn(`port ${opencodePort} is in use (possibly another OpenCode instance)`);
|
|
177
|
+
p.log.message(`using port ${actualOpencodePort} instead`);
|
|
169
178
|
}
|
|
170
179
|
// Ensure opencode binary from node_modules/.bin is in PATH
|
|
171
180
|
// Required for: npm install -g @bytespell/shella
|
|
@@ -174,76 +183,152 @@ async function startCommand(options) {
|
|
|
174
183
|
const binPath = path.join(__dirname, '..', 'node_modules', '.bin');
|
|
175
184
|
const pathSep = process.platform === 'win32' ? ';' : ':';
|
|
176
185
|
process.env.PATH = process.env.PATH ? `${binPath}${pathSep}${process.env.PATH}` : binPath;
|
|
177
|
-
//
|
|
178
|
-
|
|
186
|
+
// In verbose mode, use simple status messages instead of clack UI
|
|
187
|
+
// This prevents spinner interference with log streaming
|
|
188
|
+
const s = verbose ? null : p.spinner();
|
|
189
|
+
const log = (msg) => {
|
|
190
|
+
if (verbose) {
|
|
191
|
+
console.log(msg);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
if (!verbose) {
|
|
195
|
+
p.intro(`${brand('shella')} ${dim(`v${VERSION}`)}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
log(`${brand('shella')} ${dim(`v${VERSION}`)}`);
|
|
199
|
+
}
|
|
179
200
|
// Start OpenCode server using the bundled binary
|
|
201
|
+
if (s)
|
|
202
|
+
s.start('Starting OpenCode...');
|
|
203
|
+
else
|
|
204
|
+
log('starting opencode...');
|
|
205
|
+
// Set XDG env vars to isolate OpenCode data to shella directories
|
|
206
|
+
// This prevents shella from polluting user's existing OpenCode installation
|
|
207
|
+
const home = homedir();
|
|
208
|
+
process.env.XDG_DATA_HOME = path.join(home, '.local', 'share', 'shella');
|
|
209
|
+
process.env.XDG_CACHE_HOME = path.join(home, '.cache', 'shella');
|
|
210
|
+
process.env.XDG_CONFIG_HOME = path.join(home, '.config', 'shella');
|
|
211
|
+
process.env.XDG_STATE_HOME = path.join(home, '.local', 'state', 'shella');
|
|
180
212
|
try {
|
|
213
|
+
const config = getOpencodeConfig();
|
|
181
214
|
opencodeServer = await createOpencodeServer({
|
|
182
|
-
port:
|
|
215
|
+
port: actualOpencodePort,
|
|
183
216
|
timeout: 30000,
|
|
217
|
+
config,
|
|
184
218
|
});
|
|
185
219
|
}
|
|
186
220
|
catch (err) {
|
|
187
221
|
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
-
|
|
222
|
+
if (s)
|
|
223
|
+
s.stop('Failed to start OpenCode', 1);
|
|
224
|
+
p.log.error(message);
|
|
189
225
|
process.exit(1);
|
|
190
226
|
}
|
|
191
227
|
// Register projects with OpenCode
|
|
192
|
-
|
|
228
|
+
if (s)
|
|
229
|
+
s.message('Registering directories...');
|
|
230
|
+
else
|
|
231
|
+
log('registering directories...');
|
|
232
|
+
const { names } = await registerProjects(actualOpencodePort, options.projectsDir);
|
|
193
233
|
const projectName = names.length === 1 ? names[0] : names.length > 1 ? `${names.length} projects` : undefined;
|
|
194
|
-
// Start Express server in production mode, passing
|
|
234
|
+
// Start Express server in production mode, passing mode and directory
|
|
235
|
+
if (s)
|
|
236
|
+
s.message('Starting server...');
|
|
237
|
+
else
|
|
238
|
+
log('starting server...');
|
|
195
239
|
const serverPath = path.join(__dirname, '..', 'dist', 'server', 'index.js');
|
|
196
|
-
|
|
240
|
+
if (verbose) {
|
|
241
|
+
console.log(`[shella] mode: ${mode}, directory: ${projectsDir}`);
|
|
242
|
+
}
|
|
243
|
+
// Pass mode and directory to server
|
|
244
|
+
const serverArgs = mode === 'server'
|
|
245
|
+
? ['--mode', 'server', '--projects-dir', projectsDir]
|
|
246
|
+
: ['--mode', 'cwd', '--directory', projectsDir];
|
|
247
|
+
expressProcess = spawn('node', [serverPath, ...serverArgs], {
|
|
197
248
|
env: {
|
|
198
249
|
...process.env,
|
|
199
250
|
NODE_ENV: 'production',
|
|
200
251
|
PORT: String(port),
|
|
252
|
+
OPENCODE_URL: `http://localhost:${actualOpencodePort}`,
|
|
201
253
|
},
|
|
202
254
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
203
255
|
});
|
|
256
|
+
// Track if server is ready (for verbose mode log streaming)
|
|
257
|
+
let serverReady = false;
|
|
204
258
|
// Wait for server to be ready
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
259
|
+
try {
|
|
260
|
+
await new Promise((resolve, reject) => {
|
|
261
|
+
const timeout = setTimeout(() => {
|
|
262
|
+
reject(new Error('Server startup timeout'));
|
|
263
|
+
}, 10000);
|
|
264
|
+
expressProcess.stdout?.on('data', (data) => {
|
|
265
|
+
const output = data.toString();
|
|
266
|
+
if (output.includes('Running on')) {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
serverReady = true;
|
|
269
|
+
resolve();
|
|
270
|
+
}
|
|
271
|
+
// In verbose mode, stream logs after server is ready
|
|
272
|
+
if (verbose && serverReady) {
|
|
273
|
+
process.stdout.write(output);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
expressProcess.stderr?.on('data', (data) => {
|
|
277
|
+
// In verbose mode, stream stderr after server is ready
|
|
278
|
+
if (verbose && serverReady) {
|
|
279
|
+
process.stderr.write(data);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
expressProcess.on('error', (err) => {
|
|
226
283
|
clearTimeout(timeout);
|
|
227
|
-
reject(new Error(`
|
|
228
|
-
}
|
|
284
|
+
reject(new Error(`Failed to spawn server: ${err.message}`));
|
|
285
|
+
});
|
|
286
|
+
expressProcess.on('exit', (code) => {
|
|
287
|
+
if (code !== 0 && code !== null) {
|
|
288
|
+
clearTimeout(timeout);
|
|
289
|
+
reject(new Error(`Server exited with code ${code}`));
|
|
290
|
+
}
|
|
291
|
+
});
|
|
229
292
|
});
|
|
230
|
-
}
|
|
231
|
-
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
296
|
+
if (s)
|
|
297
|
+
s.stop('Failed', 1);
|
|
298
|
+
p.log.error(message);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
// Stop spinner and show success
|
|
302
|
+
if (s) {
|
|
303
|
+
s.stop('Ready');
|
|
304
|
+
}
|
|
305
|
+
// Show access URL
|
|
232
306
|
const lanIp = getLanIp();
|
|
233
|
-
|
|
234
|
-
|
|
307
|
+
const url = `http://${lanIp}:${port}`;
|
|
308
|
+
const title = projectName ? `${projectName}` : 'access from any device';
|
|
309
|
+
if (verbose) {
|
|
310
|
+
log(`\n${brand(url)}`);
|
|
311
|
+
log(dim(title));
|
|
312
|
+
log(`\n${dim('press ctrl+c to stop')}\n`);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
p.log.success(`${brand(url)}`);
|
|
316
|
+
p.log.message(dim(title));
|
|
317
|
+
p.outro(dim('press ctrl+c to stop'));
|
|
318
|
+
}
|
|
235
319
|
// Open browser if requested
|
|
236
320
|
if (options.open) {
|
|
237
|
-
const
|
|
321
|
+
const localUrl = `http://localhost:${port}`;
|
|
238
322
|
const openCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
239
|
-
spawn(openCommand, [
|
|
323
|
+
spawn(openCommand, [localUrl], { stdio: 'ignore', detached: true }).unref();
|
|
240
324
|
}
|
|
241
325
|
}
|
|
242
326
|
/**
|
|
243
327
|
* Graceful shutdown handler
|
|
244
328
|
*/
|
|
245
329
|
function shutdown() {
|
|
246
|
-
console.log('
|
|
330
|
+
console.log(''); // newline after ^C
|
|
331
|
+
console.log(dim('stopping...'));
|
|
247
332
|
if (expressProcess) {
|
|
248
333
|
expressProcess.kill('SIGTERM');
|
|
249
334
|
expressProcess = null;
|
|
@@ -252,22 +337,34 @@ function shutdown() {
|
|
|
252
337
|
opencodeServer.close();
|
|
253
338
|
opencodeServer = null;
|
|
254
339
|
}
|
|
340
|
+
console.log(`${brand('shella')} ${dim('stopped')}`);
|
|
255
341
|
process.exit(0);
|
|
256
342
|
}
|
|
257
343
|
// Handle shutdown signals
|
|
258
344
|
process.on('SIGINT', shutdown);
|
|
259
345
|
process.on('SIGTERM', shutdown);
|
|
260
346
|
// CLI definition
|
|
347
|
+
// Default verbose to true in Docker for easier debugging
|
|
348
|
+
const defaultVerbose = isDocker();
|
|
261
349
|
program
|
|
262
350
|
.name('shella')
|
|
263
|
-
.description('Self-hosted AI coding agents. Access from your phone.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
351
|
+
.description(`${brand('shella')} - Self-hosted AI coding agents. Access from your phone.
|
|
352
|
+
|
|
353
|
+
Modes:
|
|
354
|
+
${brand('shella')} Run in current directory (cwd mode)
|
|
355
|
+
${brand('shella')} --projects-dir . Register all subdirectories as projects (server mode)`)
|
|
356
|
+
.version(VERSION)
|
|
268
357
|
.option('-p, --port <port>', 'Web server port', '3067')
|
|
269
358
|
.option('--opencode-port <port>', 'OpenCode server port', '4096')
|
|
270
359
|
.option('--projects-dir <path>', 'Directory containing projects (each subdirectory becomes a project)')
|
|
271
360
|
.option('--no-open', "Don't open browser automatically")
|
|
272
|
-
.
|
|
361
|
+
.option('-v, --verbose', 'Show detailed logs during startup', defaultVerbose)
|
|
362
|
+
.option('--quiet', 'Suppress logs (opposite of --verbose)')
|
|
363
|
+
.action((options) => {
|
|
364
|
+
// --quiet overrides --verbose
|
|
365
|
+
if (options.quiet) {
|
|
366
|
+
options.verbose = false;
|
|
367
|
+
}
|
|
368
|
+
return startCommand(options);
|
|
369
|
+
});
|
|
273
370
|
program.parse();
|
package/dev/cli.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* shella dev CLI - Ink-based development server orchestrator
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx devi Start interactive TUI (default)
|
|
7
|
+
* npx devi [command] Run command and exit
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* status, start, stop, logs, errors, clear
|
|
11
|
+
* docker:up, docker:down, docker:status, etc.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export {}; // Make this a module for top-level await
|
|
15
|
+
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
|
|
18
|
+
// Check if running inside a container - this tool won't work there
|
|
19
|
+
if (fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv')) {
|
|
20
|
+
console.error('\x1b[31m✗ Error: Running inside a container\x1b[0m');
|
|
21
|
+
console.error(
|
|
22
|
+
'\x1b[33m The dev tool manages host-based servers and cannot be used from containers.\x1b[0m',
|
|
23
|
+
);
|
|
24
|
+
console.error('\x1b[33m Please run this command from your host machine instead.\x1b[0m');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
|
|
30
|
+
if (args.length === 0) {
|
|
31
|
+
// Interactive mode - launch Ink TUI
|
|
32
|
+
const { runInteractive } = await import('./interactive.js');
|
|
33
|
+
await runInteractive();
|
|
34
|
+
} else {
|
|
35
|
+
// Non-interactive - run command and exit
|
|
36
|
+
const { runCommand } = await import('./core.js');
|
|
37
|
+
await runCommand(args);
|
|
38
|
+
}
|