@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.
Files changed (91) hide show
  1. package/README.md +49 -1
  2. package/bin/cli.js +184 -87
  3. package/dev/cli.tsx +38 -0
  4. package/dist/config/openai-codex-models.json +205 -0
  5. package/dist/server/index.d.ts +4 -2
  6. package/dist/server/index.js +67 -8
  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/directory.d.ts +8 -0
  14. package/dist/server/routes/directory.js +84 -0
  15. package/dist/server/routes/init.d.ts +4 -3
  16. package/dist/server/routes/init.js +23 -9
  17. package/dist/server/routes/local-llm.d.ts +8 -0
  18. package/dist/server/routes/local-llm.js +255 -0
  19. package/dist/server/routes/logs.js +35 -11
  20. package/dist/server/routes/prompt.d.ts +16 -0
  21. package/dist/server/routes/prompt.js +173 -0
  22. package/dist/server/routes/session.d.ts +8 -0
  23. package/dist/server/routes/session.js +63 -0
  24. package/dist/server/routes/status.d.ts +9 -0
  25. package/dist/server/routes/status.js +54 -0
  26. package/dist/server/routes/usage.d.ts +12 -0
  27. package/dist/server/routes/usage.js +60 -0
  28. package/dist/server/routes/windows.js +4 -4
  29. package/dist/server/schema.d.ts +47 -16
  30. package/dist/server/schema.js +8 -1
  31. package/dist/server/services/database.d.ts +10 -1
  32. package/dist/server/services/database.js +19 -6
  33. package/dist/web/assets/{_baseUniq-BXqY9Mam.js → _baseUniq-BxVG561Z.js} +1 -1
  34. package/dist/web/assets/{arc-Bn6tUpO_.js → arc-B9TFF79T.js} +1 -1
  35. package/dist/web/assets/{architectureDiagram-VXUJARFQ-C7FAApUY.js → architectureDiagram-VXUJARFQ-BMRLMpf8.js} +1 -1
  36. package/dist/web/assets/{blockDiagram-VD42YOAC-C2fdaEWa.js → blockDiagram-VD42YOAC-DBQKFxeQ.js} +1 -1
  37. package/dist/web/assets/{c4Diagram-YG6GDRKO-FEVzhARQ.js → c4Diagram-YG6GDRKO-TiYEZrdu.js} +1 -1
  38. package/dist/web/assets/channel-aOIIaiSs.js +1 -0
  39. package/dist/web/assets/{chunk-4BX2VUAB-DLekcSAU.js → chunk-4BX2VUAB-CWCp0N17.js} +1 -1
  40. package/dist/web/assets/{chunk-55IACEB6-8hFRjyTP.js → chunk-55IACEB6-CeVCFKqv.js} +1 -1
  41. package/dist/web/assets/{chunk-B4BG7PRW-DULC9-MQ.js → chunk-B4BG7PRW-B-AoaHJt.js} +1 -1
  42. package/dist/web/assets/{chunk-DI55MBZ5-DuOE5RH1.js → chunk-DI55MBZ5-CCGkXnX-.js} +1 -1
  43. package/dist/web/assets/{chunk-FMBD7UC4-DaDNiCk7.js → chunk-FMBD7UC4-Bcupjeb_.js} +1 -1
  44. package/dist/web/assets/{chunk-QN33PNHL-CKshfIHj.js → chunk-QN33PNHL-DlyUQaTO.js} +1 -1
  45. package/dist/web/assets/{chunk-QZHKN3VN-D2Qy0tdi.js → chunk-QZHKN3VN-THN-at_3.js} +1 -1
  46. package/dist/web/assets/{chunk-TZMSLE5B-SPxkj-lp.js → chunk-TZMSLE5B-CtErOFJM.js} +1 -1
  47. package/dist/web/assets/classDiagram-2ON5EDUG-BFLMv18M.js +1 -0
  48. package/dist/web/assets/classDiagram-v2-WZHVMYZB-BFLMv18M.js +1 -0
  49. package/dist/web/assets/clone-BR_FHSwu.js +1 -0
  50. package/dist/web/assets/{code-block-QI2IAROF-BZdAQmZ2.js → code-block-QI2IAROF-CPI-88R6.js} +1 -1
  51. package/dist/web/assets/{cose-bilkent-S5V4N54A-DbasixUk.js → cose-bilkent-S5V4N54A-YrHmsLe4.js} +1 -1
  52. package/dist/web/assets/{dagre-6UL2VRFP-CStyjTc9.js → dagre-6UL2VRFP-Dcvw3qhj.js} +1 -1
  53. package/dist/web/assets/{diagram-PSM6KHXK-Crk93U8d.js → diagram-PSM6KHXK-B135EOe6.js} +1 -1
  54. package/dist/web/assets/{diagram-QEK2KX5R-DiW6RNbg.js → diagram-QEK2KX5R-w3KdB_-u.js} +1 -1
  55. package/dist/web/assets/{diagram-S2PKOQOG-CKksz_qL.js → diagram-S2PKOQOG-DYssvOTP.js} +1 -1
  56. package/dist/web/assets/{erDiagram-Q2GNP2WA-CisACqqq.js → erDiagram-Q2GNP2WA-DpnuE7B_.js} +1 -1
  57. package/dist/web/assets/{flowDiagram-NV44I4VS-BBp_5zAe.js → flowDiagram-NV44I4VS-BhcJ-8Yu.js} +1 -1
  58. package/dist/web/assets/{ganttDiagram-JELNMOA3-BKZ30gLA.js → ganttDiagram-JELNMOA3-ButVkRCz.js} +1 -1
  59. package/dist/web/assets/{gitGraphDiagram-NY62KEGX-ClizxUXq.js → gitGraphDiagram-NY62KEGX-ZLz8eoSo.js} +1 -1
  60. package/dist/web/assets/{graph-DqhaNOTU.js → graph-CKmCFGqF.js} +1 -1
  61. package/dist/web/assets/index-BHJDUcNL.js +1719 -0
  62. package/dist/web/assets/index-CcAJUkQw.css +1 -0
  63. package/dist/web/assets/index-DEiKajXR.js +1 -0
  64. package/dist/web/assets/{infoDiagram-WHAUD3N6-BQwNR0md.js → infoDiagram-WHAUD3N6-C_h94brE.js} +1 -1
  65. package/dist/web/assets/{journeyDiagram-XKPGCS4Q-YOqPPID4.js → journeyDiagram-XKPGCS4Q-u0bPRxxb.js} +1 -1
  66. package/dist/web/assets/{kanban-definition-3W4ZIXB7-Dtu8bvBx.js → kanban-definition-3W4ZIXB7-DkM-KD6Y.js} +1 -1
  67. package/dist/web/assets/{layout-Cc1ESzTe.js → layout-DGSU3MQw.js} +1 -1
  68. package/dist/web/assets/{linear-BwI2ANFG.js → linear-Dck9QCb9.js} +1 -1
  69. package/dist/web/assets/{mermaid.core-npIGP8NS.js → mermaid.core-DfB-jqaz.js} +5 -5
  70. package/dist/web/assets/{min--MKscDc6.js → min-CqEcl9J0.js} +1 -1
  71. package/dist/web/assets/{mindmap-definition-VGOIOE7T-Cr39Vhym.js → mindmap-definition-VGOIOE7T-D1KrSALz.js} +1 -1
  72. package/dist/web/assets/{pieDiagram-ADFJNKIX-Cv8ke00t.js → pieDiagram-ADFJNKIX-CZ-507Bd.js} +1 -1
  73. package/dist/web/assets/{quadrantDiagram-AYHSOK5B-BPhHaTg8.js → quadrantDiagram-AYHSOK5B-Jw0og6Ix.js} +1 -1
  74. package/dist/web/assets/{requirementDiagram-UZGBJVZJ-Cc42SoK0.js → requirementDiagram-UZGBJVZJ-5JWD7TEH.js} +1 -1
  75. package/dist/web/assets/{sankeyDiagram-TZEHDZUN-CtgBuq8T.js → sankeyDiagram-TZEHDZUN-DzlxPj37.js} +1 -1
  76. package/dist/web/assets/{sequenceDiagram-WL72ISMW-B9lNGN6V.js → sequenceDiagram-WL72ISMW-Cui1ykiA.js} +1 -1
  77. package/dist/web/assets/{stateDiagram-FKZM4ZOC-C3dRTOMb.js → stateDiagram-FKZM4ZOC-CCdGE_zt.js} +1 -1
  78. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-CsLg9bzy.js +1 -0
  79. package/dist/web/assets/{timeline-definition-IT6M3QCI-CXhSuTlt.js → timeline-definition-IT6M3QCI-CP2T8mHI.js} +1 -1
  80. package/dist/web/assets/{treemap-KMMF4GRG-Csy25Uov.js → treemap-KMMF4GRG-DGBVlHVf.js} +1 -1
  81. package/dist/web/assets/welcome-screen-test-DnIwI3hf.js +1 -0
  82. package/dist/web/assets/{xychartDiagram-PRI3JC2R-CxEERqse.js → xychartDiagram-PRI3JC2R-DDlMipkA.js} +1 -1
  83. package/dist/web/index.html +3 -3
  84. package/package.json +14 -5
  85. package/dist/web/assets/channel-CxjnQtV7.js +0 -1
  86. package/dist/web/assets/classDiagram-2ON5EDUG-CVG91-fs.js +0 -1
  87. package/dist/web/assets/classDiagram-v2-WZHVMYZB-CVG91-fs.js +0 -1
  88. package/dist/web/assets/clone-C7jxvixc.js +0 -1
  89. package/dist/web/assets/index-B0jWvqrS.css +0 -1
  90. package/dist/web/assets/index-Dnmavb3d.js +0 -1716
  91. package/dist/web/assets/stateDiagram-v2-4FDKWEC3-oHTO1yj_.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
 
@@ -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.3';
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
- console.error(`Error: Projects directory not found: ${absDir}`);
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
- console.error(`Error: No subdirectories found in ${absDir}`);
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
- // Docker requires --projects-dir
147
- if (isDocker() && !options.projectsDir) {
148
- console.error('Error: --projects-dir is required when running in Docker.\n');
149
- console.error('Example:');
150
- console.error(' docker run -v ~/code:/project shella --projects-dir /project\n');
151
- console.error('Or in docker-compose.yml:');
152
- console.error(' command: ["--projects-dir", "/project"]');
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 projects directory to pass to the server
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
- console.error(`Error: Port ${port} is already in use.`);
162
- console.error(`\nTry: shella --port 3070`);
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
- console.error(`Error: Port ${opencodePort} is already in use (OpenCode).`);
167
- console.error(`\nTry: pkill -f "opencode serve" or shella --opencode-port 4097`);
168
- process.exit(1);
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
- // Show minimal startup message
178
- console.log('Starting Shella...');
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: opencodePort,
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
- console.error(`Failed to start OpenCode: ${message}`);
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
- const { names } = await registerProjects(opencodePort, options.projectsDir);
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 projectsDir
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
- expressProcess = spawn('node', [serverPath, '--projects-dir', projectsDir], {
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
- await new Promise((resolve, reject) => {
206
- const timeout = setTimeout(() => {
207
- reject(new Error('Server startup timeout'));
208
- }, 10000);
209
- expressProcess.stdout?.on('data', (data) => {
210
- const output = data.toString();
211
- if (output.includes('Running on')) {
212
- clearTimeout(timeout);
213
- resolve();
214
- }
215
- // Suppress all server output
216
- });
217
- expressProcess.stderr?.on('data', () => {
218
- // Suppress stderr too
219
- });
220
- expressProcess.on('error', (err) => {
221
- clearTimeout(timeout);
222
- reject(new Error(`Failed to spawn server: ${err.message}`));
223
- });
224
- expressProcess.on('exit', (code) => {
225
- if (code !== 0 && code !== null) {
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(`Server exited with code ${code}`));
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
- // Print access box
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
- printAccessBox(lanIp, port, projectName);
234
- console.log('Press Ctrl+C to stop');
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 url = `http://localhost:${port}`;
321
+ const localUrl = `http://localhost:${port}`;
238
322
  const openCommand = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
239
- spawn(openCommand, [url], { stdio: 'ignore', detached: true }).unref();
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('\nStopping...');
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
- .version(VERSION);
265
- program
266
- .command('start', { isDefault: true })
267
- .description('Start Shella')
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
- .action(startCommand);
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
+ }