@bytespell/shella 0.1.2 → 0.1.4
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 +20 -7
- package/bin/cli.js +192 -101
- package/dev/cli.tsx +26 -0
- package/dist/config/openai-codex-models.json +205 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +89 -4
- 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/init.d.ts +16 -0
- package/dist/server/routes/init.js +39 -0
- 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-Dj1Rdun9.js → _baseUniq-6T01QAux.js} +1 -1
- package/dist/web/assets/{arc-BcAe_L9C.js → arc-BkH3TPJb.js} +1 -1
- package/dist/web/assets/{architectureDiagram-VXUJARFQ-CUydP-HB.js → architectureDiagram-VXUJARFQ-BSi6BLCC.js} +1 -1
- package/dist/web/assets/{blockDiagram-VD42YOAC-D7ENTm-e.js → blockDiagram-VD42YOAC-QSPUbinO.js} +1 -1
- package/dist/web/assets/{c4Diagram-YG6GDRKO-DE6z4ano.js → c4Diagram-YG6GDRKO-Cya_BihR.js} +1 -1
- package/dist/web/assets/channel-DGAtS-pa.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-f1AgJ5-4.js → chunk-4BX2VUAB-DIL6eizv.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-CRc_DE4C.js → chunk-55IACEB6-CgwejoZz.js} +1 -1
- package/dist/web/assets/{chunk-B4BG7PRW-BeFQ_8yC.js → chunk-B4BG7PRW-9mIPqoGe.js} +1 -1
- package/dist/web/assets/{chunk-DI55MBZ5-_SEo9TtQ.js → chunk-DI55MBZ5-BRbyRfgT.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-6M3K7zkj.js → chunk-FMBD7UC4-CVBT25Fj.js} +1 -1
- package/dist/web/assets/{chunk-QN33PNHL-BGGg7fLk.js → chunk-QN33PNHL-rTj-WT2G.js} +1 -1
- package/dist/web/assets/{chunk-QZHKN3VN-Q6HkNG6r.js → chunk-QZHKN3VN-BaUBiHya.js} +1 -1
- package/dist/web/assets/{chunk-TZMSLE5B-D3UMAT7-.js → chunk-TZMSLE5B-C4_O5TI-.js} +1 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-DLvlUUJq.js +1 -0
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-DLvlUUJq.js +1 -0
- package/dist/web/assets/clone-BZW2JABw.js +1 -0
- package/dist/web/assets/{code-block-QI2IAROF-BzpuLcBt.js → code-block-QI2IAROF-Bj_2OIYt.js} +1 -1
- package/dist/web/assets/{cose-bilkent-S5V4N54A-BIcssrhL.js → cose-bilkent-S5V4N54A-T7a1luWi.js} +1 -1
- package/dist/web/assets/{dagre-6UL2VRFP-YaA_gyjx.js → dagre-6UL2VRFP-CeH5ZsdW.js} +1 -1
- package/dist/web/assets/{diagram-PSM6KHXK-r6WVaeIu.js → diagram-PSM6KHXK-Cdod2Lna.js} +1 -1
- package/dist/web/assets/{diagram-QEK2KX5R-DtTKn621.js → diagram-QEK2KX5R-CYks2r54.js} +1 -1
- package/dist/web/assets/{diagram-S2PKOQOG-BINNPg42.js → diagram-S2PKOQOG-DCmy0g7p.js} +1 -1
- package/dist/web/assets/{erDiagram-Q2GNP2WA-DXIwYH9k.js → erDiagram-Q2GNP2WA-Dlz1bNvI.js} +1 -1
- package/dist/web/assets/{flowDiagram-NV44I4VS-D8Bad7wU.js → flowDiagram-NV44I4VS-Di5Iit1B.js} +1 -1
- package/dist/web/assets/{ganttDiagram-JELNMOA3-DSiZGBoG.js → ganttDiagram-JELNMOA3-9i1dugg-.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-NY62KEGX-CCxrAUDj.js → gitGraphDiagram-NY62KEGX-BORbMVri.js} +1 -1
- package/dist/web/assets/{graph-CcI3NtJu.js → graph-C0SCKxbQ.js} +1 -1
- package/dist/web/assets/index-CYVJT8rN.js +1 -0
- package/dist/web/assets/index-CcAJUkQw.css +1 -0
- package/dist/web/assets/index-CcDdxbB-.js +1719 -0
- package/dist/web/assets/{infoDiagram-WHAUD3N6-C3OGZjzs.js → infoDiagram-WHAUD3N6-7ohMQFLY.js} +1 -1
- package/dist/web/assets/{journeyDiagram-XKPGCS4Q-aT-Wsw7y.js → journeyDiagram-XKPGCS4Q-DZp7Z7wE.js} +1 -1
- package/dist/web/assets/{kanban-definition-3W4ZIXB7-DpUj9tx6.js → kanban-definition-3W4ZIXB7-BCNLCm54.js} +1 -1
- package/dist/web/assets/{layout-Ujef10t5.js → layout-AUnZuY21.js} +1 -1
- package/dist/web/assets/{linear-CLfbwX-c.js → linear-B0bfAqGt.js} +1 -1
- package/dist/web/assets/{mermaid.core-BAK_ixpL.js → mermaid.core-D5fXNCxA.js} +5 -5
- package/dist/web/assets/{min-D8VL4G-w.js → min-BZUFOEEw.js} +1 -1
- package/dist/web/assets/{mindmap-definition-VGOIOE7T-ChCIxiAQ.js → mindmap-definition-VGOIOE7T-hEGJLJ8N.js} +1 -1
- package/dist/web/assets/{pieDiagram-ADFJNKIX-jlQ1USe2.js → pieDiagram-ADFJNKIX-BRpCTJIO.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AYHSOK5B-Dj2wmV1N.js → quadrantDiagram-AYHSOK5B-m7jaiHQb.js} +1 -1
- package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Umq3-C8j.js → requirementDiagram-UZGBJVZJ-Coh9g9Sp.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-TZEHDZUN-Ce6gdeOT.js → sankeyDiagram-TZEHDZUN-CrD_kUGR.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WL72ISMW-DJGrGFRW.js → sequenceDiagram-WL72ISMW-C04yD1EI.js} +1 -1
- package/dist/web/assets/{stateDiagram-FKZM4ZOC-D-zz3TRT.js → stateDiagram-FKZM4ZOC-DhP-DMZW.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-DWi5vrD6.js +1 -0
- package/dist/web/assets/{timeline-definition-IT6M3QCI-C8M7N84Y.js → timeline-definition-IT6M3QCI-40iW2p_5.js} +1 -1
- package/dist/web/assets/{treemap-KMMF4GRG-B_8-FlJu.js → treemap-KMMF4GRG-BnxWQbzt.js} +1 -1
- package/dist/web/assets/welcome-screen-test-CLeWuIqq.js +1 -0
- package/dist/web/assets/{xychartDiagram-PRI3JC2R-DDPGTO3C.js → xychartDiagram-PRI3JC2R-D6lcJDCc.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +15 -5
- package/dist/web/assets/channel-C1Y873om.js +0 -1
- package/dist/web/assets/classDiagram-2ON5EDUG-DLBhGros.js +0 -1
- package/dist/web/assets/classDiagram-v2-WZHVMYZB-DLBhGros.js +0 -1
- package/dist/web/assets/clone-DVBZ10mH.js +0 -1
- package/dist/web/assets/index-B0jWvqrS.css +0 -1
- package/dist/web/assets/index-BCTWtQQB.js +0 -1716
- package/dist/web/assets/stateDiagram-v2-4FDKWEC3-CARvDj2s.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
|
|
|
@@ -6,29 +6,39 @@ Run multiple AI agents in parallel, persist sessions across devices, and manage
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Quick Start (npx)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
# From your project directory
|
|
13
12
|
cd ~/myproject
|
|
14
13
|
npx @bytespell/shella
|
|
15
14
|
```
|
|
16
15
|
|
|
17
16
|
Opens at `http://localhost:3067`. Access from your phone using the LAN IP shown.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
### Permanent Install
|
|
20
19
|
|
|
21
20
|
```bash
|
|
22
|
-
|
|
21
|
+
npm install -g @bytespell/shella
|
|
22
|
+
shella
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Now you can run `shella` from any directory.
|
|
26
|
+
|
|
27
|
+
### Multiple Projects
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
shella --projects-dir ~/code
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Registers all subdirectories of `~/code` as available projects.
|
|
34
|
+
|
|
35
|
+
### Docker
|
|
26
36
|
|
|
27
37
|
```yaml
|
|
28
38
|
# docker-compose.yml
|
|
29
39
|
services:
|
|
30
40
|
shella:
|
|
31
|
-
image: ghcr.io/bytespell/shella
|
|
41
|
+
image: ghcr.io/bytespell-oss/shella
|
|
32
42
|
ports:
|
|
33
43
|
- '3067:3067'
|
|
34
44
|
volumes:
|
|
@@ -70,6 +80,9 @@ shella --help # Show help
|
|
|
70
80
|
# Install dependencies
|
|
71
81
|
npm install
|
|
72
82
|
|
|
83
|
+
# Create .env.local with your projects directory
|
|
84
|
+
echo "SHELLA_PROJECTS_DIR=/path/to/your/projects" > .env.local
|
|
85
|
+
|
|
73
86
|
# Start dev servers (Vite + Express + OpenCode)
|
|
74
87
|
npm run start:dev
|
|
75
88
|
|
package/bin/cli.js
CHANGED
|
@@ -4,11 +4,26 @@ import { createOpencodeServer } from '@opencode-ai/sdk';
|
|
|
4
4
|
import { networkInterfaces } 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
|
*/
|
|
@@ -53,51 +68,42 @@ function getLanIp() {
|
|
|
53
68
|
return 'localhost';
|
|
54
69
|
}
|
|
55
70
|
/**
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
function printAccessBox(lanIp, port) {
|
|
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
|
-
+${border}+
|
|
66
|
-
|${pad('')}|
|
|
67
|
-
|${pad(' Access from any device:')}|
|
|
68
|
-
|${pad('')}|
|
|
69
|
-
|${pad(urlLine)}|
|
|
70
|
-
|${pad('')}|
|
|
71
|
-
+${border}+
|
|
72
|
-
`);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Register projects with OpenCode
|
|
71
|
+
* Register projects with OpenCode by creating a session in each directory.
|
|
72
|
+
* This triggers OpenCode to discover and register the project.
|
|
76
73
|
*/
|
|
77
74
|
async function registerProjects(opencodePort, projectsDir) {
|
|
78
75
|
const baseUrl = `http://localhost:${opencodePort}`;
|
|
76
|
+
const registered = [];
|
|
79
77
|
if (projectsDir) {
|
|
80
78
|
// Resolve to absolute path
|
|
81
79
|
const absDir = path.resolve(projectsDir);
|
|
82
80
|
if (!existsSync(absDir)) {
|
|
83
|
-
|
|
81
|
+
p.log.error(`projects directory not found: ${absDir}`);
|
|
84
82
|
process.exit(1);
|
|
85
83
|
}
|
|
86
|
-
// Register each subdirectory
|
|
84
|
+
// Register each subdirectory (OpenCode will determine if it's a git repo)
|
|
87
85
|
const entries = readdirSync(absDir, { withFileTypes: true });
|
|
88
86
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'));
|
|
89
87
|
if (dirs.length === 0) {
|
|
90
|
-
|
|
88
|
+
p.log.error(`no subdirectories found in ${absDir}`);
|
|
91
89
|
process.exit(1);
|
|
92
90
|
}
|
|
93
91
|
for (const entry of dirs) {
|
|
94
92
|
const dir = path.join(absDir, entry.name);
|
|
95
93
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// Create a session to trigger project registration
|
|
95
|
+
await fetch(`${baseUrl}/session`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
'X-OpenCode-Directory': dir,
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify({}),
|
|
102
|
+
});
|
|
103
|
+
registered.push(entry.name);
|
|
98
104
|
}
|
|
99
|
-
catch
|
|
100
|
-
|
|
105
|
+
catch {
|
|
106
|
+
// Silently skip failed registrations
|
|
101
107
|
}
|
|
102
108
|
}
|
|
103
109
|
}
|
|
@@ -105,72 +111,115 @@ async function registerProjects(opencodePort, projectsDir) {
|
|
|
105
111
|
// Register cwd
|
|
106
112
|
const dir = process.cwd();
|
|
107
113
|
try {
|
|
108
|
-
await fetch(`${baseUrl}/
|
|
109
|
-
|
|
114
|
+
await fetch(`${baseUrl}/session`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: {
|
|
117
|
+
'Content-Type': 'application/json',
|
|
118
|
+
'X-OpenCode-Directory': dir,
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify({}),
|
|
121
|
+
});
|
|
122
|
+
registered.push(path.basename(dir));
|
|
110
123
|
}
|
|
111
|
-
catch
|
|
112
|
-
|
|
124
|
+
catch {
|
|
125
|
+
// Silently skip
|
|
113
126
|
}
|
|
114
127
|
}
|
|
128
|
+
return { count: registered.length, names: registered };
|
|
115
129
|
}
|
|
116
130
|
let opencodeServer = null;
|
|
117
131
|
let expressProcess = null;
|
|
118
132
|
async function startCommand(options) {
|
|
119
133
|
const port = parseInt(options.port);
|
|
120
134
|
const opencodePort = parseInt(options.opencodePort);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
const verbose = options.verbose;
|
|
137
|
+
// Determine mode based on --projects-dir presence
|
|
138
|
+
const mode = options.projectsDir ? 'server' : 'cwd';
|
|
139
|
+
// Docker requires --projects-dir (server mode)
|
|
140
|
+
if (isDocker() && mode === 'cwd') {
|
|
141
|
+
p.log.error('--projects-dir is required when running in Docker');
|
|
142
|
+
p.log.message('example: docker run -v ~/code:/project shella --projects-dir /project');
|
|
143
|
+
p.log.message('or in docker-compose.yml: command: ["--projects-dir", "/project"]');
|
|
129
144
|
process.exit(1);
|
|
130
145
|
}
|
|
146
|
+
// Determine the directory to pass to the server
|
|
147
|
+
const projectsDir = options.projectsDir ? path.resolve(options.projectsDir) : cwd;
|
|
131
148
|
// Check if ports are available
|
|
132
149
|
const webPortAvailable = await isPortAvailable(port);
|
|
133
150
|
const ocPortAvailable = await isPortAvailable(opencodePort);
|
|
134
151
|
if (!webPortAvailable) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.error(` - Stop the process using port ${port}`);
|
|
138
|
-
console.error(` - Use a different port: shella --port 3070`);
|
|
152
|
+
p.log.error(`port ${port} is already in use`);
|
|
153
|
+
p.log.message('try: shella --port 3070');
|
|
139
154
|
process.exit(1);
|
|
140
155
|
}
|
|
141
156
|
if (!ocPortAvailable) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.error(`\nTry one of these:`);
|
|
145
|
-
console.error(` - Kill the existing OpenCode: pkill -f "opencode serve"`);
|
|
146
|
-
console.error(` - Use a different port: shella --opencode-port 4097`);
|
|
157
|
+
p.log.error(`port ${opencodePort} is already in use (opencode)`);
|
|
158
|
+
p.log.message('try: pkill -f "opencode serve" or shella --opencode-port 4097');
|
|
147
159
|
process.exit(1);
|
|
148
160
|
}
|
|
161
|
+
// Ensure opencode binary from node_modules/.bin is in PATH
|
|
162
|
+
// Required for: npm install -g @bytespell/shella
|
|
163
|
+
// No-op for: npx (already in PATH)
|
|
164
|
+
// Helps with: Direct execution (node bin/cli.js)
|
|
165
|
+
const binPath = path.join(__dirname, '..', 'node_modules', '.bin');
|
|
166
|
+
const pathSep = process.platform === 'win32' ? ';' : ':';
|
|
167
|
+
process.env.PATH = process.env.PATH ? `${binPath}${pathSep}${process.env.PATH}` : binPath;
|
|
168
|
+
// In verbose mode, use simple status messages instead of clack UI
|
|
169
|
+
// This prevents spinner interference with log streaming
|
|
170
|
+
const s = verbose ? null : p.spinner();
|
|
171
|
+
const log = (msg) => {
|
|
172
|
+
if (verbose) {
|
|
173
|
+
console.log(msg);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if (!verbose) {
|
|
177
|
+
p.intro(`${brand('shella')} ${dim(`v${VERSION}`)}`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
log(`${brand('shella')} ${dim(`v${VERSION}`)}`);
|
|
181
|
+
}
|
|
149
182
|
// Start OpenCode server using the bundled binary
|
|
150
|
-
|
|
183
|
+
if (s)
|
|
184
|
+
s.start('Starting OpenCode...');
|
|
185
|
+
else
|
|
186
|
+
log('starting opencode...');
|
|
151
187
|
try {
|
|
188
|
+
const config = getOpencodeConfig();
|
|
152
189
|
opencodeServer = await createOpencodeServer({
|
|
153
190
|
port: opencodePort,
|
|
154
191
|
timeout: 30000,
|
|
192
|
+
config,
|
|
155
193
|
});
|
|
156
|
-
console.log(` OpenCode ready (port ${opencodePort})`);
|
|
157
194
|
}
|
|
158
195
|
catch (err) {
|
|
159
196
|
const message = err instanceof Error ? err.message : String(err);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
console.error(` shella --opencode-port 4097`);
|
|
164
|
-
}
|
|
197
|
+
if (s)
|
|
198
|
+
s.stop('Failed to start OpenCode', 1);
|
|
199
|
+
p.log.error(message);
|
|
165
200
|
process.exit(1);
|
|
166
201
|
}
|
|
167
202
|
// Register projects with OpenCode
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
203
|
+
if (s)
|
|
204
|
+
s.message('Registering directories...');
|
|
205
|
+
else
|
|
206
|
+
log('registering directories...');
|
|
207
|
+
const { names } = await registerProjects(opencodePort, options.projectsDir);
|
|
208
|
+
const projectName = names.length === 1 ? names[0] : names.length > 1 ? `${names.length} projects` : undefined;
|
|
209
|
+
// Start Express server in production mode, passing mode and directory
|
|
210
|
+
if (s)
|
|
211
|
+
s.message('Starting server...');
|
|
212
|
+
else
|
|
213
|
+
log('starting server...');
|
|
172
214
|
const serverPath = path.join(__dirname, '..', 'dist', 'server', 'index.js');
|
|
173
|
-
|
|
215
|
+
if (verbose) {
|
|
216
|
+
console.log(`[shella] mode: ${mode}, directory: ${projectsDir}`);
|
|
217
|
+
}
|
|
218
|
+
// Pass mode and directory to server
|
|
219
|
+
const serverArgs = mode === 'server'
|
|
220
|
+
? ['--mode', 'server', '--projects-dir', projectsDir]
|
|
221
|
+
: ['--mode', 'cwd', '--directory', projectsDir];
|
|
222
|
+
expressProcess = spawn('node', [serverPath, ...serverArgs], {
|
|
174
223
|
env: {
|
|
175
224
|
...process.env,
|
|
176
225
|
NODE_ENV: 'production',
|
|
@@ -178,51 +227,82 @@ async function startCommand(options) {
|
|
|
178
227
|
},
|
|
179
228
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
180
229
|
});
|
|
230
|
+
// Track if server is ready (for verbose mode log streaming)
|
|
231
|
+
let serverReady = false;
|
|
181
232
|
// Wait for server to be ready
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
try {
|
|
234
|
+
await new Promise((resolve, reject) => {
|
|
235
|
+
const timeout = setTimeout(() => {
|
|
236
|
+
reject(new Error('Server startup timeout'));
|
|
237
|
+
}, 10000);
|
|
238
|
+
expressProcess.stdout?.on('data', (data) => {
|
|
239
|
+
const output = data.toString();
|
|
240
|
+
if (output.includes('Running on')) {
|
|
241
|
+
clearTimeout(timeout);
|
|
242
|
+
serverReady = true;
|
|
243
|
+
resolve();
|
|
244
|
+
}
|
|
245
|
+
// In verbose mode, stream logs after server is ready
|
|
246
|
+
if (verbose && serverReady) {
|
|
247
|
+
process.stdout.write(output);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
expressProcess.stderr?.on('data', (data) => {
|
|
251
|
+
// In verbose mode, stream stderr after server is ready
|
|
252
|
+
if (verbose && serverReady) {
|
|
253
|
+
process.stderr.write(data);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
expressProcess.on('error', (err) => {
|
|
204
257
|
clearTimeout(timeout);
|
|
205
|
-
reject(new Error(`
|
|
206
|
-
}
|
|
258
|
+
reject(new Error(`Failed to spawn server: ${err.message}`));
|
|
259
|
+
});
|
|
260
|
+
expressProcess.on('exit', (code) => {
|
|
261
|
+
if (code !== 0 && code !== null) {
|
|
262
|
+
clearTimeout(timeout);
|
|
263
|
+
reject(new Error(`Server exited with code ${code}`));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
207
266
|
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
270
|
+
if (s)
|
|
271
|
+
s.stop('Failed', 1);
|
|
272
|
+
p.log.error(message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
// Stop spinner and show success
|
|
276
|
+
if (s) {
|
|
277
|
+
s.stop('Ready');
|
|
278
|
+
}
|
|
279
|
+
// Show access URL
|
|
211
280
|
const lanIp = getLanIp();
|
|
212
|
-
|
|
213
|
-
|
|
281
|
+
const url = `http://${lanIp}:${port}`;
|
|
282
|
+
const title = projectName ? `${projectName}` : 'access from any device';
|
|
283
|
+
if (verbose) {
|
|
284
|
+
log(`\n${brand(url)}`);
|
|
285
|
+
log(dim(title));
|
|
286
|
+
log(`\n${dim('press ctrl+c to stop')}\n`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
p.log.success(`${brand(url)}`);
|
|
290
|
+
p.log.message(dim(title));
|
|
291
|
+
p.outro(dim('press ctrl+c to stop'));
|
|
292
|
+
}
|
|
214
293
|
// Open browser if requested
|
|
215
294
|
if (options.open) {
|
|
216
|
-
const
|
|
295
|
+
const localUrl = `http://localhost:${port}`;
|
|
217
296
|
const openCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
218
|
-
spawn(openCommand, [
|
|
297
|
+
spawn(openCommand, [localUrl], { stdio: 'ignore', detached: true }).unref();
|
|
219
298
|
}
|
|
220
299
|
}
|
|
221
300
|
/**
|
|
222
301
|
* Graceful shutdown handler
|
|
223
302
|
*/
|
|
224
303
|
function shutdown() {
|
|
225
|
-
console.log('
|
|
304
|
+
console.log(''); // newline after ^C
|
|
305
|
+
console.log(dim('stopping...'));
|
|
226
306
|
if (expressProcess) {
|
|
227
307
|
expressProcess.kill('SIGTERM');
|
|
228
308
|
expressProcess = null;
|
|
@@ -231,23 +311,34 @@ function shutdown() {
|
|
|
231
311
|
opencodeServer.close();
|
|
232
312
|
opencodeServer = null;
|
|
233
313
|
}
|
|
234
|
-
console.log('
|
|
314
|
+
console.log(`${brand('shella')} ${dim('stopped')}`);
|
|
235
315
|
process.exit(0);
|
|
236
316
|
}
|
|
237
317
|
// Handle shutdown signals
|
|
238
318
|
process.on('SIGINT', shutdown);
|
|
239
319
|
process.on('SIGTERM', shutdown);
|
|
240
320
|
// CLI definition
|
|
321
|
+
// Default verbose to true in Docker for easier debugging
|
|
322
|
+
const defaultVerbose = isDocker();
|
|
241
323
|
program
|
|
242
324
|
.name('shella')
|
|
243
|
-
.description('Self-hosted AI coding agents. Access from your phone.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
325
|
+
.description(`${brand('shella')} - Self-hosted AI coding agents. Access from your phone.
|
|
326
|
+
|
|
327
|
+
Modes:
|
|
328
|
+
${brand('shella')} Run in current directory (cwd mode)
|
|
329
|
+
${brand('shella')} --projects-dir . Register all subdirectories as projects (server mode)`)
|
|
330
|
+
.version(VERSION)
|
|
248
331
|
.option('-p, --port <port>', 'Web server port', '3067')
|
|
249
332
|
.option('--opencode-port <port>', 'OpenCode server port', '4096')
|
|
250
333
|
.option('--projects-dir <path>', 'Directory containing projects (each subdirectory becomes a project)')
|
|
251
334
|
.option('--no-open', "Don't open browser automatically")
|
|
252
|
-
.
|
|
335
|
+
.option('-v, --verbose', 'Show detailed logs during startup', defaultVerbose)
|
|
336
|
+
.option('--quiet', 'Suppress logs (opposite of --verbose)')
|
|
337
|
+
.action((options) => {
|
|
338
|
+
// --quiet overrides --verbose
|
|
339
|
+
if (options.quiet) {
|
|
340
|
+
options.verbose = false;
|
|
341
|
+
}
|
|
342
|
+
return startCommand(options);
|
|
343
|
+
});
|
|
253
344
|
program.parse();
|
package/dev/cli.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
if (args.length === 0) {
|
|
19
|
+
// Interactive mode - launch Ink TUI
|
|
20
|
+
const { runInteractive } = await import('./interactive.js');
|
|
21
|
+
await runInteractive();
|
|
22
|
+
} else {
|
|
23
|
+
// Non-interactive - run command and exit
|
|
24
|
+
const { runCommand } = await import('./core.js');
|
|
25
|
+
await runCommand(args);
|
|
26
|
+
}
|