@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.
Files changed (89) hide show
  1. package/README.md +20 -7
  2. package/bin/cli.js +192 -101
  3. package/dev/cli.tsx +26 -0
  4. package/dist/config/openai-codex-models.json +205 -0
  5. package/dist/server/index.d.ts +4 -0
  6. package/dist/server/index.js +89 -4
  7. package/dist/server/lib/opencode-client.d.ts +14 -0
  8. package/dist/server/lib/opencode-client.js +17 -0
  9. package/dist/server/lib/opencode-config.d.ts +14 -0
  10. package/dist/server/lib/opencode-config.js +25 -0
  11. package/dist/server/routes/config.d.ts +12 -0
  12. package/dist/server/routes/config.js +207 -0
  13. package/dist/server/routes/init.d.ts +16 -0
  14. package/dist/server/routes/init.js +39 -0
  15. package/dist/server/routes/local-llm.d.ts +8 -0
  16. package/dist/server/routes/local-llm.js +255 -0
  17. package/dist/server/routes/logs.js +35 -11
  18. package/dist/server/routes/prompt.d.ts +16 -0
  19. package/dist/server/routes/prompt.js +173 -0
  20. package/dist/server/routes/session.d.ts +8 -0
  21. package/dist/server/routes/session.js +63 -0
  22. package/dist/server/routes/status.d.ts +9 -0
  23. package/dist/server/routes/status.js +54 -0
  24. package/dist/server/routes/usage.d.ts +12 -0
  25. package/dist/server/routes/usage.js +60 -0
  26. package/dist/server/routes/windows.js +4 -4
  27. package/dist/server/schema.d.ts +47 -16
  28. package/dist/server/schema.js +8 -1
  29. package/dist/server/services/database.d.ts +10 -1
  30. package/dist/server/services/database.js +19 -6
  31. package/dist/web/assets/{_baseUniq-Dj1Rdun9.js → _baseUniq-6T01QAux.js} +1 -1
  32. package/dist/web/assets/{arc-BcAe_L9C.js → arc-BkH3TPJb.js} +1 -1
  33. package/dist/web/assets/{architectureDiagram-VXUJARFQ-CUydP-HB.js → architectureDiagram-VXUJARFQ-BSi6BLCC.js} +1 -1
  34. package/dist/web/assets/{blockDiagram-VD42YOAC-D7ENTm-e.js → blockDiagram-VD42YOAC-QSPUbinO.js} +1 -1
  35. package/dist/web/assets/{c4Diagram-YG6GDRKO-DE6z4ano.js → c4Diagram-YG6GDRKO-Cya_BihR.js} +1 -1
  36. package/dist/web/assets/channel-DGAtS-pa.js +1 -0
  37. package/dist/web/assets/{chunk-4BX2VUAB-f1AgJ5-4.js → chunk-4BX2VUAB-DIL6eizv.js} +1 -1
  38. package/dist/web/assets/{chunk-55IACEB6-CRc_DE4C.js → chunk-55IACEB6-CgwejoZz.js} +1 -1
  39. package/dist/web/assets/{chunk-B4BG7PRW-BeFQ_8yC.js → chunk-B4BG7PRW-9mIPqoGe.js} +1 -1
  40. package/dist/web/assets/{chunk-DI55MBZ5-_SEo9TtQ.js → chunk-DI55MBZ5-BRbyRfgT.js} +1 -1
  41. package/dist/web/assets/{chunk-FMBD7UC4-6M3K7zkj.js → chunk-FMBD7UC4-CVBT25Fj.js} +1 -1
  42. package/dist/web/assets/{chunk-QN33PNHL-BGGg7fLk.js → chunk-QN33PNHL-rTj-WT2G.js} +1 -1
  43. package/dist/web/assets/{chunk-QZHKN3VN-Q6HkNG6r.js → chunk-QZHKN3VN-BaUBiHya.js} +1 -1
  44. package/dist/web/assets/{chunk-TZMSLE5B-D3UMAT7-.js → chunk-TZMSLE5B-C4_O5TI-.js} +1 -1
  45. package/dist/web/assets/classDiagram-2ON5EDUG-DLvlUUJq.js +1 -0
  46. package/dist/web/assets/classDiagram-v2-WZHVMYZB-DLvlUUJq.js +1 -0
  47. package/dist/web/assets/clone-BZW2JABw.js +1 -0
  48. package/dist/web/assets/{code-block-QI2IAROF-BzpuLcBt.js → code-block-QI2IAROF-Bj_2OIYt.js} +1 -1
  49. package/dist/web/assets/{cose-bilkent-S5V4N54A-BIcssrhL.js → cose-bilkent-S5V4N54A-T7a1luWi.js} +1 -1
  50. package/dist/web/assets/{dagre-6UL2VRFP-YaA_gyjx.js → dagre-6UL2VRFP-CeH5ZsdW.js} +1 -1
  51. package/dist/web/assets/{diagram-PSM6KHXK-r6WVaeIu.js → diagram-PSM6KHXK-Cdod2Lna.js} +1 -1
  52. package/dist/web/assets/{diagram-QEK2KX5R-DtTKn621.js → diagram-QEK2KX5R-CYks2r54.js} +1 -1
  53. package/dist/web/assets/{diagram-S2PKOQOG-BINNPg42.js → diagram-S2PKOQOG-DCmy0g7p.js} +1 -1
  54. package/dist/web/assets/{erDiagram-Q2GNP2WA-DXIwYH9k.js → erDiagram-Q2GNP2WA-Dlz1bNvI.js} +1 -1
  55. package/dist/web/assets/{flowDiagram-NV44I4VS-D8Bad7wU.js → flowDiagram-NV44I4VS-Di5Iit1B.js} +1 -1
  56. package/dist/web/assets/{ganttDiagram-JELNMOA3-DSiZGBoG.js → ganttDiagram-JELNMOA3-9i1dugg-.js} +1 -1
  57. package/dist/web/assets/{gitGraphDiagram-NY62KEGX-CCxrAUDj.js → gitGraphDiagram-NY62KEGX-BORbMVri.js} +1 -1
  58. package/dist/web/assets/{graph-CcI3NtJu.js → graph-C0SCKxbQ.js} +1 -1
  59. package/dist/web/assets/index-CYVJT8rN.js +1 -0
  60. package/dist/web/assets/index-CcAJUkQw.css +1 -0
  61. package/dist/web/assets/index-CcDdxbB-.js +1719 -0
  62. package/dist/web/assets/{infoDiagram-WHAUD3N6-C3OGZjzs.js → infoDiagram-WHAUD3N6-7ohMQFLY.js} +1 -1
  63. package/dist/web/assets/{journeyDiagram-XKPGCS4Q-aT-Wsw7y.js → journeyDiagram-XKPGCS4Q-DZp7Z7wE.js} +1 -1
  64. package/dist/web/assets/{kanban-definition-3W4ZIXB7-DpUj9tx6.js → kanban-definition-3W4ZIXB7-BCNLCm54.js} +1 -1
  65. package/dist/web/assets/{layout-Ujef10t5.js → layout-AUnZuY21.js} +1 -1
  66. package/dist/web/assets/{linear-CLfbwX-c.js → linear-B0bfAqGt.js} +1 -1
  67. package/dist/web/assets/{mermaid.core-BAK_ixpL.js → mermaid.core-D5fXNCxA.js} +5 -5
  68. package/dist/web/assets/{min-D8VL4G-w.js → min-BZUFOEEw.js} +1 -1
  69. package/dist/web/assets/{mindmap-definition-VGOIOE7T-ChCIxiAQ.js → mindmap-definition-VGOIOE7T-hEGJLJ8N.js} +1 -1
  70. package/dist/web/assets/{pieDiagram-ADFJNKIX-jlQ1USe2.js → pieDiagram-ADFJNKIX-BRpCTJIO.js} +1 -1
  71. package/dist/web/assets/{quadrantDiagram-AYHSOK5B-Dj2wmV1N.js → quadrantDiagram-AYHSOK5B-m7jaiHQb.js} +1 -1
  72. package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Umq3-C8j.js → requirementDiagram-UZGBJVZJ-Coh9g9Sp.js} +1 -1
  73. package/dist/web/assets/{sankeyDiagram-TZEHDZUN-Ce6gdeOT.js → sankeyDiagram-TZEHDZUN-CrD_kUGR.js} +1 -1
  74. package/dist/web/assets/{sequenceDiagram-WL72ISMW-DJGrGFRW.js → sequenceDiagram-WL72ISMW-C04yD1EI.js} +1 -1
  75. package/dist/web/assets/{stateDiagram-FKZM4ZOC-D-zz3TRT.js → stateDiagram-FKZM4ZOC-DhP-DMZW.js} +1 -1
  76. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-DWi5vrD6.js +1 -0
  77. package/dist/web/assets/{timeline-definition-IT6M3QCI-C8M7N84Y.js → timeline-definition-IT6M3QCI-40iW2p_5.js} +1 -1
  78. package/dist/web/assets/{treemap-KMMF4GRG-B_8-FlJu.js → treemap-KMMF4GRG-BnxWQbzt.js} +1 -1
  79. package/dist/web/assets/welcome-screen-test-CLeWuIqq.js +1 -0
  80. package/dist/web/assets/{xychartDiagram-PRI3JC2R-DDPGTO3C.js → xychartDiagram-PRI3JC2R-D6lcJDCc.js} +1 -1
  81. package/dist/web/index.html +3 -3
  82. package/package.json +15 -5
  83. package/dist/web/assets/channel-C1Y873om.js +0 -1
  84. package/dist/web/assets/classDiagram-2ON5EDUG-DLBhGros.js +0 -1
  85. package/dist/web/assets/classDiagram-v2-WZHVMYZB-DLBhGros.js +0 -1
  86. package/dist/web/assets/clone-DVBZ10mH.js +0 -1
  87. package/dist/web/assets/index-B0jWvqrS.css +0 -1
  88. package/dist/web/assets/index-BCTWtQQB.js +0 -1716
  89. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-CARvDj2s.js +0 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Shella
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
- ### npx (Developers)
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
- For multiple projects:
18
+ ### Permanent Install
20
19
 
21
20
  ```bash
22
- npx @bytespell/shella --projects-dir ~/code
21
+ npm install -g @bytespell/shella
22
+ shella
23
23
  ```
24
24
 
25
- ### Docker (Homelab)
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.2';
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
- * Print a nice boxed message with the access URL
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
- console.error(`Error: Projects directory not found: ${absDir}`);
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
- console.error(`Error: No project directories found in ${absDir}`);
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
- await fetch(`${baseUrl}/project?directory=${encodeURIComponent(dir)}`);
97
- console.log(` Registered: ${entry.name}`);
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 (err) {
100
- console.error(` Failed to register ${entry.name}: ${err}`);
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}/project?directory=${encodeURIComponent(dir)}`);
109
- console.log(` Registered: ${path.basename(dir)}`);
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 (err) {
112
- console.error(` Failed to register ${path.basename(dir)}: ${err}`);
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
- console.log(`Shella v${VERSION}\n`);
122
- // Docker requires --projects-dir
123
- if (isDocker() && !options.projectsDir) {
124
- console.error('Error: --projects-dir is required when running in Docker.\n');
125
- console.error('Example:');
126
- console.error(' docker run -v ~/code:/project shella --projects-dir /project\n');
127
- console.error('Or in docker-compose.yml:');
128
- console.error(' command: ["--projects-dir", "/project"]');
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
- console.error(`Error: Port ${port} is already in use.`);
136
- console.error(`\nTry one of these:`);
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
- console.error(`Error: Port ${opencodePort} is already in use (OpenCode port).`);
143
- console.error(`\nThis usually means OpenCode is already running.`);
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
- console.log('Starting OpenCode...');
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
- console.error(`\nFailed to start OpenCode: ${message}`);
161
- if (message.includes('port')) {
162
- console.error(`\nThe port ${opencodePort} may be in use. Try:`);
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
- console.log('Registering projects...');
169
- await registerProjects(opencodePort, options.projectsDir);
170
- // Start Express server in production mode
171
- console.log('Starting Shella server...');
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
- expressProcess = spawn('node', [serverPath], {
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
- await new Promise((resolve, reject) => {
183
- const timeout = setTimeout(() => {
184
- reject(new Error('Server startup timeout (10s). Check logs for errors.'));
185
- }, 10000);
186
- expressProcess.stdout?.on('data', (data) => {
187
- const output = data.toString();
188
- if (output.includes('Running on')) {
189
- clearTimeout(timeout);
190
- resolve();
191
- }
192
- // Forward output but strip the prefix since we add our own
193
- process.stdout.write(output.replace(/\[shella-server\] /g, ' '));
194
- });
195
- expressProcess.stderr?.on('data', (data) => {
196
- process.stderr.write(data);
197
- });
198
- expressProcess.on('error', (err) => {
199
- clearTimeout(timeout);
200
- reject(new Error(`Failed to spawn server: ${err.message}`));
201
- });
202
- expressProcess.on('exit', (code) => {
203
- if (code !== 0 && code !== null) {
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(`Server exited with code ${code}`));
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
- console.log(` Shella ready (port ${port})`);
210
- // Print access box
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
- printAccessBox(lanIp, port);
213
- console.log('Press Ctrl+C to stop\n');
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 url = `http://localhost:${port}`;
295
+ const localUrl = `http://localhost:${port}`;
217
296
  const openCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
218
- spawn(openCommand, [url], { stdio: 'ignore', detached: true }).unref();
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('\nShutting down...');
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('Goodbye!');
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
- .version(VERSION);
245
- program
246
- .command('start', { isDefault: true })
247
- .description('Start Shella')
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
- .action(startCommand);
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
+ }