aicodeman 1.20.1 → 1.22.0

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 (286) hide show
  1. package/README.md +19 -15
  2. package/README.zh-CN.md +6 -20
  3. package/dist/cli-style.d.ts +115 -0
  4. package/dist/cli-style.d.ts.map +1 -0
  5. package/dist/cli-style.js +245 -0
  6. package/dist/cli-style.js.map +1 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +237 -186
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config/dependency-registry.d.ts.map +1 -1
  11. package/dist/config/dependency-registry.js +24 -0
  12. package/dist/config/dependency-registry.js.map +1 -1
  13. package/dist/config/instance.d.ts +9 -0
  14. package/dist/config/instance.d.ts.map +1 -1
  15. package/dist/config/instance.js +14 -0
  16. package/dist/config/instance.js.map +1 -1
  17. package/dist/cron/cron-service.d.ts +2 -1
  18. package/dist/cron/cron-service.d.ts.map +1 -1
  19. package/dist/cron/cron-service.js +3 -2
  20. package/dist/cron/cron-service.js.map +1 -1
  21. package/dist/docker-hosts.d.ts.map +1 -1
  22. package/dist/docker-hosts.js +11 -0
  23. package/dist/docker-hosts.js.map +1 -1
  24. package/dist/mux-interface.d.ts +3 -1
  25. package/dist/mux-interface.d.ts.map +1 -1
  26. package/dist/remote-hosts.d.ts.map +1 -1
  27. package/dist/remote-hosts.js +1 -0
  28. package/dist/remote-hosts.js.map +1 -1
  29. package/dist/session.d.ts +9 -3
  30. package/dist/session.d.ts.map +1 -1
  31. package/dist/session.js +29 -4
  32. package/dist/session.js.map +1 -1
  33. package/dist/state-store.d.ts +22 -0
  34. package/dist/state-store.d.ts.map +1 -1
  35. package/dist/state-store.js +45 -13
  36. package/dist/state-store.js.map +1 -1
  37. package/dist/tab-layout-legacy-order.d.ts +12 -0
  38. package/dist/tab-layout-legacy-order.d.ts.map +1 -0
  39. package/dist/tab-layout-legacy-order.js +58 -0
  40. package/dist/tab-layout-legacy-order.js.map +1 -0
  41. package/dist/tab-layout-persistence.d.ts +43 -0
  42. package/dist/tab-layout-persistence.d.ts.map +1 -0
  43. package/dist/tab-layout-persistence.js +100 -0
  44. package/dist/tab-layout-persistence.js.map +1 -0
  45. package/dist/tab-layout-service.d.ts +96 -0
  46. package/dist/tab-layout-service.d.ts.map +1 -0
  47. package/dist/tab-layout-service.js +535 -0
  48. package/dist/tab-layout-service.js.map +1 -0
  49. package/dist/tab-layout.d.ts +83 -0
  50. package/dist/tab-layout.d.ts.map +1 -0
  51. package/dist/tab-layout.js +457 -0
  52. package/dist/tab-layout.js.map +1 -0
  53. package/dist/tmux-manager.d.ts +2 -1
  54. package/dist/tmux-manager.d.ts.map +1 -1
  55. package/dist/tmux-manager.js +60 -10
  56. package/dist/tmux-manager.js.map +1 -1
  57. package/dist/tui/tui-ansi.d.ts +71 -0
  58. package/dist/tui/tui-ansi.d.ts.map +1 -0
  59. package/dist/tui/tui-ansi.js +576 -0
  60. package/dist/tui/tui-ansi.js.map +1 -0
  61. package/dist/tui/tui-app.d.ts +424 -0
  62. package/dist/tui/tui-app.d.ts.map +1 -0
  63. package/dist/tui/tui-app.js +2424 -0
  64. package/dist/tui/tui-app.js.map +1 -0
  65. package/dist/tui/tui-approvals.d.ts +71 -0
  66. package/dist/tui/tui-approvals.d.ts.map +1 -0
  67. package/dist/tui/tui-approvals.js +114 -0
  68. package/dist/tui/tui-approvals.js.map +1 -0
  69. package/dist/tui/tui-client.d.ts +581 -0
  70. package/dist/tui/tui-client.d.ts.map +1 -0
  71. package/dist/tui/tui-client.js +1359 -0
  72. package/dist/tui/tui-client.js.map +1 -0
  73. package/dist/tui/tui-composer.d.ts +83 -0
  74. package/dist/tui/tui-composer.d.ts.map +1 -0
  75. package/dist/tui/tui-composer.js +174 -0
  76. package/dist/tui/tui-composer.js.map +1 -0
  77. package/dist/tui/tui-digest.d.ts +27 -0
  78. package/dist/tui/tui-digest.d.ts.map +1 -0
  79. package/dist/tui/tui-digest.js +79 -0
  80. package/dist/tui/tui-digest.js.map +1 -0
  81. package/dist/tui/tui-keys.d.ts +57 -0
  82. package/dist/tui/tui-keys.d.ts.map +1 -0
  83. package/dist/tui/tui-keys.js +204 -0
  84. package/dist/tui/tui-keys.js.map +1 -0
  85. package/dist/tui/tui-layout.d.ts +70 -0
  86. package/dist/tui/tui-layout.d.ts.map +1 -0
  87. package/dist/tui/tui-layout.js +87 -0
  88. package/dist/tui/tui-layout.js.map +1 -0
  89. package/dist/tui/tui-model.d.ts +178 -0
  90. package/dist/tui/tui-model.d.ts.map +1 -0
  91. package/dist/tui/tui-model.js +502 -0
  92. package/dist/tui/tui-model.js.map +1 -0
  93. package/dist/tui/tui-render.d.ts +150 -0
  94. package/dist/tui/tui-render.d.ts.map +1 -0
  95. package/dist/tui/tui-render.js +784 -0
  96. package/dist/tui/tui-render.js.map +1 -0
  97. package/dist/tui/tui-sse.d.ts +79 -0
  98. package/dist/tui/tui-sse.d.ts.map +1 -0
  99. package/dist/tui/tui-sse.js +200 -0
  100. package/dist/tui/tui-sse.js.map +1 -0
  101. package/dist/tui/tui-types.d.ts +190 -0
  102. package/dist/tui/tui-types.d.ts.map +1 -0
  103. package/dist/tui/tui-types.js +16 -0
  104. package/dist/tui/tui-types.js.map +1 -0
  105. package/dist/types/app-state.d.ts +3 -0
  106. package/dist/types/app-state.d.ts.map +1 -1
  107. package/dist/types/app-state.js.map +1 -1
  108. package/dist/types/session.d.ts +30 -4
  109. package/dist/types/session.d.ts.map +1 -1
  110. package/dist/types/session.js +2 -1
  111. package/dist/types/session.js.map +1 -1
  112. package/dist/utils/cli-executable-resolver.d.ts +2 -2
  113. package/dist/utils/cli-executable-resolver.js +2 -2
  114. package/dist/utils/dependency-report.d.ts +21 -3
  115. package/dist/utils/dependency-report.d.ts.map +1 -1
  116. package/dist/utils/dependency-report.js +29 -10
  117. package/dist/utils/dependency-report.js.map +1 -1
  118. package/dist/utils/grok-cli-resolver.d.ts +61 -0
  119. package/dist/utils/grok-cli-resolver.d.ts.map +1 -0
  120. package/dist/utils/grok-cli-resolver.js +130 -0
  121. package/dist/utils/grok-cli-resolver.js.map +1 -0
  122. package/dist/utils/index.d.ts +1 -0
  123. package/dist/utils/index.d.ts.map +1 -1
  124. package/dist/utils/index.js +1 -0
  125. package/dist/utils/index.js.map +1 -1
  126. package/dist/web/ports/index.d.ts +1 -0
  127. package/dist/web/ports/index.d.ts.map +1 -1
  128. package/dist/web/ports/session-port.d.ts +1 -1
  129. package/dist/web/ports/session-port.d.ts.map +1 -1
  130. package/dist/web/ports/tab-layout-port.d.ts +7 -0
  131. package/dist/web/ports/tab-layout-port.d.ts.map +1 -0
  132. package/dist/web/ports/tab-layout-port.js +2 -0
  133. package/dist/web/ports/tab-layout-port.js.map +1 -0
  134. package/dist/web/public/admin-ui.js.gz +0 -0
  135. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  136. package/dist/web/public/app.84bf8955.js +36 -0
  137. package/dist/web/public/app.84bf8955.js.br +0 -0
  138. package/dist/web/public/app.84bf8955.js.gz +0 -0
  139. package/dist/web/public/approvals-ui.js.gz +0 -0
  140. package/dist/web/public/{constants.5abbd823.js → constants.189c7bff.js} +82 -8
  141. package/dist/web/public/constants.189c7bff.js.br +0 -0
  142. package/dist/web/public/constants.189c7bff.js.gz +0 -0
  143. package/dist/web/public/cron-ui.js.gz +0 -0
  144. package/dist/web/public/entrance-animations.js.gz +0 -0
  145. package/dist/web/public/home-sessions.js +5 -2
  146. package/dist/web/public/home-sessions.js.br +0 -0
  147. package/dist/web/public/home-sessions.js.gz +0 -0
  148. package/dist/web/public/i18n.34016b89.js +1 -0
  149. package/dist/web/public/i18n.34016b89.js.br +0 -0
  150. package/dist/web/public/i18n.34016b89.js.gz +0 -0
  151. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  152. package/dist/web/public/index.html +74 -13
  153. package/dist/web/public/index.html.br +0 -0
  154. package/dist/web/public/index.html.gz +0 -0
  155. package/dist/web/public/input-cjk.87f14251.js.gz +0 -0
  156. package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
  157. package/dist/web/public/mobile-handlers.0f83a608.js.gz +0 -0
  158. package/dist/web/public/mobile-overview.js +1 -0
  159. package/dist/web/public/mobile-overview.js.br +0 -0
  160. package/dist/web/public/mobile-overview.js.gz +0 -0
  161. package/dist/web/public/mobile.983f064e.css +1 -0
  162. package/dist/web/public/mobile.983f064e.css.br +0 -0
  163. package/dist/web/public/mobile.983f064e.css.gz +0 -0
  164. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  165. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  166. package/dist/web/public/{panels-ui.5e97a36e.js → panels-ui.7c667123.js} +77 -77
  167. package/dist/web/public/panels-ui.7c667123.js.br +0 -0
  168. package/dist/web/public/panels-ui.7c667123.js.gz +0 -0
  169. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  170. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  171. package/dist/web/public/readmymind-ui.js.gz +0 -0
  172. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  173. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  174. package/dist/web/public/session-lineage.js +15 -5
  175. package/dist/web/public/session-lineage.js.br +0 -0
  176. package/dist/web/public/session-lineage.js.gz +0 -0
  177. package/dist/web/public/session-ui.acd79900.js +68 -0
  178. package/dist/web/public/session-ui.acd79900.js.br +0 -0
  179. package/dist/web/public/session-ui.acd79900.js.gz +0 -0
  180. package/dist/web/public/settings-ui.115a2d1e.js +67 -0
  181. package/dist/web/public/settings-ui.115a2d1e.js.br +0 -0
  182. package/dist/web/public/settings-ui.115a2d1e.js.gz +0 -0
  183. package/dist/web/public/styles.64369ebd.css +1 -0
  184. package/dist/web/public/styles.64369ebd.css.br +0 -0
  185. package/dist/web/public/styles.64369ebd.css.gz +0 -0
  186. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  187. package/dist/web/public/sw.js.gz +0 -0
  188. package/dist/web/public/tab-rail-resize.f3465ce1.js +1 -0
  189. package/dist/web/public/tab-rail-resize.f3465ce1.js.br +0 -0
  190. package/dist/web/public/tab-rail-resize.f3465ce1.js.gz +0 -0
  191. package/dist/web/public/{terminal-ui.f70d681f.js → terminal-ui.ef68a666.js} +1 -1
  192. package/dist/web/public/terminal-ui.ef68a666.js.br +0 -0
  193. package/dist/web/public/terminal-ui.ef68a666.js.gz +0 -0
  194. package/dist/web/public/ultracode-panel.js.gz +0 -0
  195. package/dist/web/public/ultracode-windows.js.gz +0 -0
  196. package/dist/web/public/upload.html.gz +0 -0
  197. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  198. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  199. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  200. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  201. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  202. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  203. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  204. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  205. package/dist/web/public/vendor/xterm.css.gz +0 -0
  206. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  207. package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
  208. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  209. package/dist/web/public/webview-tabs.js.gz +0 -0
  210. package/dist/web/response-viewer-transcript.js +1 -1
  211. package/dist/web/response-viewer-transcript.js.map +1 -1
  212. package/dist/web/routes/index.d.ts +1 -0
  213. package/dist/web/routes/index.d.ts.map +1 -1
  214. package/dist/web/routes/index.js +1 -0
  215. package/dist/web/routes/index.js.map +1 -1
  216. package/dist/web/routes/ralph-routes.js +1 -1
  217. package/dist/web/routes/ralph-routes.js.map +1 -1
  218. package/dist/web/routes/session-routes.d.ts +9 -4
  219. package/dist/web/routes/session-routes.d.ts.map +1 -1
  220. package/dist/web/routes/session-routes.js +68 -27
  221. package/dist/web/routes/session-routes.js.map +1 -1
  222. package/dist/web/routes/system-routes.d.ts +2 -2
  223. package/dist/web/routes/system-routes.d.ts.map +1 -1
  224. package/dist/web/routes/system-routes.js +15 -2
  225. package/dist/web/routes/system-routes.js.map +1 -1
  226. package/dist/web/routes/tab-layout-routes.d.ts +6 -0
  227. package/dist/web/routes/tab-layout-routes.d.ts.map +1 -0
  228. package/dist/web/routes/tab-layout-routes.js +45 -0
  229. package/dist/web/routes/tab-layout-routes.js.map +1 -0
  230. package/dist/web/routes/webview-routes.d.ts +2 -2
  231. package/dist/web/routes/webview-routes.d.ts.map +1 -1
  232. package/dist/web/routes/webview-routes.js +29 -1
  233. package/dist/web/routes/webview-routes.js.map +1 -1
  234. package/dist/web/schemas.d.ts +25 -1
  235. package/dist/web/schemas.d.ts.map +1 -1
  236. package/dist/web/schemas.js +47 -5
  237. package/dist/web/schemas.js.map +1 -1
  238. package/dist/web/server.d.ts +5 -0
  239. package/dist/web/server.d.ts.map +1 -1
  240. package/dist/web/server.js +101 -36
  241. package/dist/web/server.js.map +1 -1
  242. package/dist/web/session-order-sse.d.ts +7 -0
  243. package/dist/web/session-order-sse.d.ts.map +1 -0
  244. package/dist/web/session-order-sse.js +10 -0
  245. package/dist/web/session-order-sse.js.map +1 -0
  246. package/dist/web/sse-events.d.ts +5 -2
  247. package/dist/web/sse-events.d.ts.map +1 -1
  248. package/dist/web/sse-events.js +5 -2
  249. package/dist/web/sse-events.js.map +1 -1
  250. package/dist/web/sse-stream-manager.d.ts +9 -0
  251. package/dist/web/sse-stream-manager.d.ts.map +1 -1
  252. package/dist/web/sse-stream-manager.js +86 -27
  253. package/dist/web/sse-stream-manager.js.map +1 -1
  254. package/dist/web/tab-layout-sse.d.ts +4 -0
  255. package/dist/web/tab-layout-sse.d.ts.map +1 -0
  256. package/dist/web/tab-layout-sse.js +4 -0
  257. package/dist/web/tab-layout-sse.js.map +1 -0
  258. package/package.json +2 -1
  259. package/skills/codeman/reference/endpoints.md +12 -11
  260. package/skills/codeman/reference/messaging.md +2 -2
  261. package/skills/codeman/reference/recipes.md +1 -1
  262. package/skills/codeman/reference/verbs.md +3 -3
  263. package/dist/web/public/app.202140a2.js +0 -35
  264. package/dist/web/public/app.202140a2.js.br +0 -0
  265. package/dist/web/public/app.202140a2.js.gz +0 -0
  266. package/dist/web/public/constants.5abbd823.js.br +0 -0
  267. package/dist/web/public/constants.5abbd823.js.gz +0 -0
  268. package/dist/web/public/i18n.196a992e.js +0 -1
  269. package/dist/web/public/i18n.196a992e.js.br +0 -0
  270. package/dist/web/public/i18n.196a992e.js.gz +0 -0
  271. package/dist/web/public/mobile.49aeb17e.css +0 -1
  272. package/dist/web/public/mobile.49aeb17e.css.br +0 -0
  273. package/dist/web/public/mobile.49aeb17e.css.gz +0 -0
  274. package/dist/web/public/panels-ui.5e97a36e.js.br +0 -0
  275. package/dist/web/public/panels-ui.5e97a36e.js.gz +0 -0
  276. package/dist/web/public/session-ui.71a0ce9a.js +0 -68
  277. package/dist/web/public/session-ui.71a0ce9a.js.br +0 -0
  278. package/dist/web/public/session-ui.71a0ce9a.js.gz +0 -0
  279. package/dist/web/public/settings-ui.1a62e935.js +0 -67
  280. package/dist/web/public/settings-ui.1a62e935.js.br +0 -0
  281. package/dist/web/public/settings-ui.1a62e935.js.gz +0 -0
  282. package/dist/web/public/styles.68f9e118.css +0 -1
  283. package/dist/web/public/styles.68f9e118.css.br +0 -0
  284. package/dist/web/public/styles.68f9e118.css.gz +0 -0
  285. package/dist/web/public/terminal-ui.f70d681f.js.br +0 -0
  286. package/dist/web/public/terminal-ui.f70d681f.js.gz +0 -0
package/dist/cli.js CHANGED
@@ -7,7 +7,6 @@
7
7
  * @module cli
8
8
  */
9
9
  import { Command } from 'commander';
10
- import chalk from 'chalk';
11
10
  import { createRequire } from 'module';
12
11
  import http from 'node:http';
13
12
  import https from 'node:https';
@@ -25,6 +24,7 @@ import { isSupportedAttachmentExtension } from './attachment-registry.js';
25
24
  import { daemonStatus, startDaemon, stopDaemon } from './daemon-control.js';
26
25
  import { installService, serviceStatus, uninstallService } from './service-installer.js';
27
26
  import { isLoopbackBindHost, isUnauthenticatedNetworkAcknowledged } from './web/network-auth-policy.js';
27
+ import { confirm, heading, isInteractive, kv, palette, rule, tint, withSpinner } from './cli-style.js';
28
28
  const require = createRequire(import.meta.url);
29
29
  const pkg = require('../package.json');
30
30
  const program = new Command();
@@ -97,13 +97,13 @@ program
97
97
  .action(async (filePath, options) => {
98
98
  const extension = String(filePath).split('.').pop()?.toLowerCase() || '';
99
99
  if (!isAbsolute(filePath) || !isSupportedAttachmentExtension(extension)) {
100
- console.error(chalk.red('✗ attach requires an absolute path to a png, pdf, docx, pptx, md, or txt file'));
100
+ console.error(palette.err('✗ attach requires an absolute path to a png, pdf, docx, pptx, md, or txt file'));
101
101
  process.exit(1);
102
102
  }
103
103
  const sessionId = options.session || process.env.CODEMAN_SESSION_ID;
104
104
  const apiUrl = options.url || process.env.CODEMAN_API_URL || 'https://127.0.0.1:3000';
105
105
  if (sessionId && (await postAttachment(apiUrl, sessionId, filePath))) {
106
- console.log(chalk.green('✓ Attachment card requested'));
106
+ console.log(palette.ok('✓ Attachment card requested'));
107
107
  return;
108
108
  }
109
109
  console.log(makeAttachmentMagicLink(filePath));
@@ -158,7 +158,7 @@ export function resolveSkillTargetPath(options) {
158
158
  function resolveSkillTarget(options) {
159
159
  const resolved = resolveSkillTargetPath(options);
160
160
  if (resolved.missingCase !== undefined) {
161
- console.error(chalk.red(`✗ Case not found: ${resolved.missingCase}`));
161
+ console.error(palette.err(`✗ Case not found: ${resolved.missingCase}`));
162
162
  process.exit(1);
163
163
  }
164
164
  return resolved.target;
@@ -182,10 +182,10 @@ function reportSkillResult(result, target) {
182
182
  };
183
183
  const message = messages[result];
184
184
  if (message.ok) {
185
- console.log(chalk.green(`✓ ${message.text}`));
185
+ console.log(palette.ok(`✓ ${message.text}`));
186
186
  }
187
187
  else {
188
- console.error(chalk.red(`✗ ${message.text}`));
188
+ console.error(palette.err(`✗ ${message.text}`));
189
189
  process.exit(1);
190
190
  }
191
191
  }
@@ -203,7 +203,7 @@ skillCmd
203
203
  reportSkillResult(await installAgentSkillInto(target), target);
204
204
  }
205
205
  catch (err) {
206
- console.error(chalk.red(`✗ Failed to install agent skill: ${getErrorMessage(err)}`));
206
+ console.error(palette.err(`✗ Failed to install agent skill: ${getErrorMessage(err)}`));
207
207
  process.exit(1);
208
208
  }
209
209
  });
@@ -218,7 +218,7 @@ skillCmd
218
218
  reportSkillResult(await removeAgentSkillFrom(target), target);
219
219
  }
220
220
  catch (err) {
221
- console.error(chalk.red(`✗ Failed to remove agent skill: ${getErrorMessage(err)}`));
221
+ console.error(palette.err(`✗ Failed to remove agent skill: ${getErrorMessage(err)}`));
222
222
  process.exit(1);
223
223
  }
224
224
  });
@@ -232,12 +232,12 @@ sessionCmd
232
232
  try {
233
233
  const manager = getSessionManager();
234
234
  const session = await manager.createSession(options.dir);
235
- console.log(chalk.green(`✓ Session started: ${session.id}`));
235
+ console.log(palette.ok(`✓ Session started: ${session.id}`));
236
236
  console.log(` Working directory: ${session.workingDir}`);
237
237
  console.log(` PID: ${session.pid}`);
238
238
  }
239
239
  catch (err) {
240
- console.error(chalk.red(`✗ Failed to start session: ${getErrorMessage(err)}`));
240
+ console.error(palette.err(`✗ Failed to start session: ${getErrorMessage(err)}`));
241
241
  process.exit(1);
242
242
  }
243
243
  });
@@ -248,65 +248,72 @@ sessionCmd
248
248
  try {
249
249
  const manager = getSessionManager();
250
250
  await manager.stopSession(id);
251
- console.log(chalk.green(`✓ Session stopped: ${id}`));
251
+ console.log(palette.ok(`✓ Session stopped: ${id}`));
252
252
  }
253
253
  catch (err) {
254
- console.error(chalk.red(`✗ Failed to stop session: ${getErrorMessage(err)}`));
254
+ console.error(palette.err(`✗ Failed to stop session: ${getErrorMessage(err)}`));
255
255
  process.exit(1);
256
256
  }
257
257
  });
258
- sessionCmd
259
- .command('list')
260
- .alias('ls')
261
- .description('List all sessions')
262
- .action(() => {
258
+ /** Session status in the shared vocabulary: idle is fine, busy is working, anything else is a problem. */
259
+ function sessionStatusLabel(status) {
260
+ if (status === 'idle')
261
+ return palette.ok('idle');
262
+ if (status === 'busy')
263
+ return palette.warn('busy');
264
+ return palette.err(status);
265
+ }
266
+ /**
267
+ * The one session listing. `codeman list` used to be a copy of this that had
268
+ * drifted (it lost the stopped and web-server sections), so it now calls the
269
+ * same renderer and only opts out of those two sections.
270
+ */
271
+ function printSessionList(options) {
263
272
  const manager = getSessionManager();
264
273
  const sessions = manager.getAllSessions();
265
274
  const stored = manager.getStoredSessions();
266
275
  if (sessions.length === 0 && Object.keys(stored).length === 0) {
267
- console.log(chalk.yellow('No sessions found'));
276
+ console.log(palette.warn('No sessions found'));
268
277
  return;
269
278
  }
270
- console.log(chalk.bold('\nActive Sessions:'));
279
+ console.log(heading('Active Sessions:'));
271
280
  if (sessions.length === 0) {
272
281
  console.log(' (none)');
273
282
  }
274
283
  else {
275
284
  for (const session of sessions) {
276
- const status = session.status === 'idle'
277
- ? chalk.green('idle')
278
- : session.status === 'busy'
279
- ? chalk.yellow('busy')
280
- : chalk.red(session.status);
281
- console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status} ${session.workingDir}`);
285
+ console.log(` ${palette.info(session.id.slice(0, 8))} ${sessionStatusLabel(session.status)} ${session.workingDir}`);
282
286
  }
283
287
  }
284
- const stoppedSessions = Object.values(stored).filter((s) => s.status === 'stopped');
285
- if (stoppedSessions.length > 0) {
286
- console.log(chalk.bold('\nStopped Sessions:'));
287
- for (const session of stoppedSessions) {
288
- const name = session.name ? ` (${session.name})` : '';
289
- console.log(` ${chalk.gray(session.id.slice(0, 8))} ${chalk.gray('stopped')}${name} ${session.workingDir}`);
288
+ if (options.includeStored) {
289
+ const stoppedSessions = Object.values(stored).filter((s) => s.status === 'stopped');
290
+ if (stoppedSessions.length > 0) {
291
+ console.log(heading('Stopped Sessions:'));
292
+ for (const session of stoppedSessions) {
293
+ const name = session.name ? ` (${session.name})` : '';
294
+ console.log(` ${palette.muted(session.id.slice(0, 8))} ${palette.muted('stopped')}${name} ${session.workingDir}`);
295
+ }
290
296
  }
291
- }
292
- // Show active sessions from state (when web server manages them)
293
- const activeSessions = Object.values(stored).filter((s) => s.status !== 'stopped');
294
- if (sessions.length === 0 && activeSessions.length > 0) {
295
- console.log(chalk.bold('\nActive Sessions (from web server):'));
296
- for (const session of activeSessions) {
297
- const status = session.status === 'idle'
298
- ? chalk.green('idle')
299
- : session.status === 'busy'
300
- ? chalk.yellow('busy')
301
- : chalk.red(session.status);
302
- const name = session.name ? ` (${session.name})` : '';
303
- const mode = session.mode === 'shell' ? chalk.gray(' [shell]') : '';
304
- const cost = session.totalCost ? chalk.gray(` $${session.totalCost.toFixed(4)}`) : '';
305
- console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status}${name}${mode}${cost} ${session.workingDir}`);
297
+ // Sessions the web server owns: this process has no PTY for them, so they
298
+ // only exist in the shared state file.
299
+ const activeSessions = Object.values(stored).filter((s) => s.status !== 'stopped');
300
+ if (sessions.length === 0 && activeSessions.length > 0) {
301
+ console.log(heading('Active Sessions (from web server):'));
302
+ for (const session of activeSessions) {
303
+ const name = session.name ? ` (${session.name})` : '';
304
+ const mode = session.mode === 'shell' ? palette.muted(' [shell]') : '';
305
+ const cost = session.totalCost ? palette.muted(` $${session.totalCost.toFixed(4)}`) : '';
306
+ console.log(` ${palette.info(session.id.slice(0, 8))} ${sessionStatusLabel(session.status)}${name}${mode}${cost} ${session.workingDir}`);
307
+ }
306
308
  }
307
309
  }
308
310
  console.log('');
309
- });
311
+ }
312
+ sessionCmd
313
+ .command('list')
314
+ .alias('ls')
315
+ .description('List all sessions')
316
+ .action(() => printSessionList({ includeStored: true }));
310
317
  sessionCmd
311
318
  .command('logs <id>')
312
319
  .description('View session output')
@@ -315,11 +322,11 @@ sessionCmd
315
322
  const manager = getSessionManager();
316
323
  const output = options.errors ? manager.getSessionError(id) : manager.getSessionOutput(id);
317
324
  if (output === null) {
318
- console.log(chalk.yellow(`Session ${id} not found or not active`));
325
+ console.log(palette.warn(`Session ${id} not found or not active`));
319
326
  return;
320
327
  }
321
328
  if (output === '') {
322
- console.log(chalk.gray('(no output)'));
329
+ console.log(palette.muted('(no output)'));
323
330
  return;
324
331
  }
325
332
  console.log(output);
@@ -342,7 +349,7 @@ taskCmd
342
349
  completionPhrase: options.completion,
343
350
  timeoutMs: options.timeout ? parseInt(options.timeout, 10) : undefined,
344
351
  });
345
- console.log(chalk.green(`✓ Task added: ${task.id}`));
352
+ console.log(palette.ok(`✓ Task added: ${task.id}`));
346
353
  console.log(` Prompt: ${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}`);
347
354
  console.log(` Priority: ${task.priority}`);
348
355
  });
@@ -358,23 +365,23 @@ taskCmd
358
365
  tasks = tasks.filter((t) => t.status === options.status);
359
366
  }
360
367
  if (tasks.length === 0) {
361
- console.log(chalk.yellow('No tasks found'));
368
+ console.log(palette.warn('No tasks found'));
362
369
  return;
363
370
  }
364
371
  const statusColors = {
365
- pending: chalk.gray,
366
- running: chalk.yellow,
367
- completed: chalk.green,
368
- failed: chalk.red,
372
+ pending: palette.muted,
373
+ running: palette.warn,
374
+ completed: palette.ok,
375
+ failed: palette.err,
369
376
  };
370
- console.log(chalk.bold('\nTasks:'));
377
+ console.log(palette.emph('\nTasks:'));
371
378
  for (const task of tasks) {
372
379
  const color = statusColors[task.status];
373
380
  const prompt = task.prompt.slice(0, 40) + (task.prompt.length > 40 ? '...' : '');
374
- console.log(` ${chalk.cyan(task.id.slice(0, 8))} ${color(task.status.padEnd(10))} [${task.priority}] ${prompt}`);
381
+ console.log(` ${palette.info(task.id.slice(0, 8))} ${color(task.status.padEnd(10))} [${task.priority}] ${prompt}`);
375
382
  }
376
383
  const counts = queue.getCount();
377
- console.log(chalk.bold('\nSummary:'));
384
+ console.log(palette.emph('\nSummary:'));
378
385
  console.log(` Pending: ${counts.pending}, Running: ${counts.running}, Completed: ${counts.completed}, Failed: ${counts.failed}`);
379
386
  console.log('');
380
387
  });
@@ -385,10 +392,10 @@ taskCmd
385
392
  const queue = getTaskQueue();
386
393
  const task = queue.getTask(id);
387
394
  if (!task) {
388
- console.log(chalk.red(`Task ${id} not found`));
395
+ console.log(palette.err(`Task ${id} not found`));
389
396
  return;
390
397
  }
391
- console.log(chalk.bold('\nTask Details:'));
398
+ console.log(palette.emph('\nTask Details:'));
392
399
  console.log(` ID: ${task.id}`);
393
400
  console.log(` Status: ${task.status}`);
394
401
  console.log(` Priority: ${task.priority}`);
@@ -398,10 +405,10 @@ taskCmd
398
405
  console.log(` Session: ${task.assignedSessionId}`);
399
406
  }
400
407
  if (task.error) {
401
- console.log(` Error: ${chalk.red(task.error)}`);
408
+ console.log(` Error: ${palette.err(task.error)}`);
402
409
  }
403
410
  if (task.output) {
404
- console.log(chalk.bold('\nOutput:'));
411
+ console.log(palette.emph('\nOutput:'));
405
412
  console.log(task.output.slice(0, 500) + (task.output.length > 500 ? '...' : ''));
406
413
  }
407
414
  console.log('');
@@ -413,10 +420,10 @@ taskCmd
413
420
  .action((id) => {
414
421
  const queue = getTaskQueue();
415
422
  if (queue.removeTask(id)) {
416
- console.log(chalk.green(`✓ Task removed: ${id}`));
423
+ console.log(palette.ok(`✓ Task removed: ${id}`));
417
424
  }
418
425
  else {
419
- console.log(chalk.red(`Task ${id} not found`));
426
+ console.log(palette.err(`Task ${id} not found`));
420
427
  }
421
428
  });
422
429
  taskCmd
@@ -429,15 +436,15 @@ taskCmd
429
436
  let count;
430
437
  if (options.all) {
431
438
  count = queue.clearAll();
432
- console.log(chalk.green(`✓ Cleared ${count} tasks`));
439
+ console.log(palette.ok(`✓ Cleared ${count} tasks`));
433
440
  }
434
441
  else if (options.failed) {
435
442
  count = queue.clearFailed();
436
- console.log(chalk.green(`✓ Cleared ${count} failed tasks`));
443
+ console.log(palette.ok(`✓ Cleared ${count} failed tasks`));
437
444
  }
438
445
  else {
439
446
  count = queue.clearCompleted();
440
- console.log(chalk.green(`✓ Cleared ${count} completed tasks`));
447
+ console.log(palette.ok(`✓ Cleared ${count} completed tasks`));
441
448
  }
442
449
  });
443
450
  // ============ Ralph Loop Commands ============
@@ -455,32 +462,32 @@ ralphCmd
455
462
  loop.setMinDuration(parseFloat(options.minHours));
456
463
  }
457
464
  if (loop.isRunning()) {
458
- console.log(chalk.yellow('Ralph loop is already running'));
465
+ console.log(palette.warn('Ralph loop is already running'));
459
466
  return;
460
467
  }
461
468
  loop.on('taskAssigned', (taskId, sessionId) => {
462
- console.log(chalk.cyan(`→ Task ${taskId.slice(0, 8)} assigned to session ${sessionId.slice(0, 8)}`));
469
+ console.log(palette.info(`→ Task ${taskId.slice(0, 8)} assigned to session ${sessionId.slice(0, 8)}`));
463
470
  });
464
471
  loop.on('taskCompleted', (taskId) => {
465
- console.log(chalk.green(`✓ Task ${taskId.slice(0, 8)} completed`));
472
+ console.log(palette.ok(`✓ Task ${taskId.slice(0, 8)} completed`));
466
473
  });
467
474
  loop.on('taskFailed', (taskId, error) => {
468
- console.log(chalk.red(`✗ Task ${taskId.slice(0, 8)} failed: ${error}`));
475
+ console.log(palette.err(`✗ Task ${taskId.slice(0, 8)} failed: ${error}`));
469
476
  });
470
477
  loop.on('stopped', () => {
471
- console.log(chalk.yellow('\nRalph loop stopped'));
478
+ console.log(palette.warn('\nRalph loop stopped'));
472
479
  printStats(loop.getStats());
473
480
  process.exit(0);
474
481
  });
475
482
  await loop.start();
476
- console.log(chalk.green('✓ Ralph loop started'));
483
+ console.log(palette.ok('✓ Ralph loop started'));
477
484
  if (options.minHours) {
478
485
  console.log(` Minimum duration: ${options.minHours} hours`);
479
486
  }
480
- console.log(chalk.gray(' Press Ctrl+C to stop\n'));
487
+ console.log(palette.muted(' Press Ctrl+C to stop\n'));
481
488
  // Keep process running
482
489
  process.on('SIGINT', () => {
483
- console.log(chalk.yellow('\nStopping Ralph loop...'));
490
+ console.log(palette.warn('\nStopping Ralph loop...'));
484
491
  loop.stop();
485
492
  });
486
493
  });
@@ -490,11 +497,11 @@ ralphCmd
490
497
  .action(() => {
491
498
  const loop = getRalphLoop();
492
499
  if (!loop.isRunning()) {
493
- console.log(chalk.yellow('Ralph loop is not running'));
500
+ console.log(palette.warn('Ralph loop is not running'));
494
501
  return;
495
502
  }
496
503
  loop.stop();
497
- console.log(chalk.green('✓ Ralph loop stopped'));
504
+ console.log(palette.ok('✓ Ralph loop stopped'));
498
505
  });
499
506
  ralphCmd
500
507
  .command('status')
@@ -505,21 +512,21 @@ ralphCmd
505
512
  printStats(stats);
506
513
  });
507
514
  function printStats(stats) {
508
- const statusColor = stats.status === 'running' ? chalk.green : stats.status === 'paused' ? chalk.yellow : chalk.gray;
509
- console.log(chalk.bold('\nRalph Loop Status:'));
515
+ const statusColor = stats.status === 'running' ? palette.ok : stats.status === 'paused' ? palette.warn : palette.muted;
516
+ console.log(palette.emph('\nRalph Loop Status:'));
510
517
  console.log(` Status: ${statusColor(stats.status)}`);
511
518
  console.log(` Elapsed: ${stats.elapsedHours.toFixed(2)} hours`);
512
519
  if (stats.minDurationMs) {
513
520
  const minHours = stats.minDurationMs / (1000 * 60 * 60);
514
521
  console.log(` Min Duration: ${minHours.toFixed(2)} hours (${stats.minDurationReached ? 'reached' : 'not reached'})`);
515
522
  }
516
- console.log(chalk.bold('\nTasks:'));
523
+ console.log(palette.emph('\nTasks:'));
517
524
  console.log(` Pending: ${stats.pending}`);
518
525
  console.log(` Running: ${stats.running}`);
519
526
  console.log(` Completed: ${stats.completed} (${stats.tasksCompleted} this session)`);
520
527
  console.log(` Failed: ${stats.failed}`);
521
528
  console.log(` Generated: ${stats.tasksGenerated}`);
522
- console.log(chalk.bold('\nSessions:'));
529
+ console.log(palette.emph('\nSessions:'));
523
530
  console.log(` Active: ${stats.activeSessions}`);
524
531
  console.log(` Idle: ${stats.idleSessions}`);
525
532
  console.log(` Busy: ${stats.busySessions}`);
@@ -615,44 +622,44 @@ program
615
622
  break;
616
623
  }
617
624
  }
618
- console.log(chalk.bold('\nCodeman Status'));
619
- console.log('─'.repeat(40));
620
- console.log(chalk.bold('\nWeb Server:'));
625
+ console.log(heading('Codeman Status'));
626
+ console.log(rule(40));
627
+ console.log(heading('Web Server:'));
621
628
  if (probe.reachable) {
622
629
  const version = probe.version ? ` (v${probe.version})` : '';
623
- console.log(` Status: ${chalk.green('running')}${version} at ${probe.url}`);
630
+ console.log(kv('Status', `${palette.ok('running')}${version} at ${probe.url}`));
624
631
  if (probe.authRequired) {
625
- console.log(chalk.gray(' (answers 401: set CODEMAN_PASSWORD/CODEMAN_USERNAME to see session details)'));
632
+ console.log(palette.muted(' (answers 401: set CODEMAN_PASSWORD/CODEMAN_USERNAME to see session details)'));
626
633
  }
627
634
  }
628
635
  else {
629
- console.log(` Status: ${chalk.red('not reachable')} at ${candidates.join(' or ')}`);
630
- console.log(chalk.gray(' (start it with `codeman web`, or check your service: systemctl --user status codeman-web)'));
636
+ console.log(kv('Status', `${palette.err('not reachable')} at ${candidates.join(' or ')}`));
637
+ console.log(palette.muted(' (start it with `codeman web`, or check your service: systemctl --user status codeman-web)'));
631
638
  }
632
639
  // Prefer the server's live view; fall back to the shared saved state, labeled
633
640
  // as such, so the numbers are never silently a different thing.
634
641
  if (probe.sessions) {
635
642
  const live = probe.sessions;
636
- console.log(chalk.bold('\nSessions (live, from the server):'));
637
- console.log(` Total: ${live.length}`);
638
- console.log(` Idle: ${live.filter((s) => s.status === 'idle').length}`);
639
- console.log(` Busy: ${live.filter((s) => s.status === 'busy').length}`);
643
+ console.log(heading('Sessions (live, from the server):'));
644
+ console.log(kv('Total', String(live.length)));
645
+ console.log(kv('Idle', String(live.filter((s) => s.status === 'idle').length)));
646
+ console.log(kv('Busy', String(live.filter((s) => s.status === 'busy').length)));
640
647
  }
641
648
  else {
642
649
  const manager = getSessionManager();
643
650
  const storedValues = Object.values(manager.getStoredSessions());
644
- console.log(chalk.bold('\nSessions (from saved state):'));
645
- console.log(` Active: ${storedValues.filter((s) => s.status !== 'stopped').length}`);
646
- console.log(` Idle: ${storedValues.filter((s) => s.status === 'idle').length}`);
647
- console.log(` Busy: ${storedValues.filter((s) => s.status === 'busy').length}`);
651
+ console.log(heading('Sessions (from saved state):'));
652
+ console.log(kv('Active', String(storedValues.filter((s) => s.status !== 'stopped').length)));
653
+ console.log(kv('Idle', String(storedValues.filter((s) => s.status === 'idle').length)));
654
+ console.log(kv('Busy', String(storedValues.filter((s) => s.status === 'busy').length)));
648
655
  }
649
656
  const taskCounts = getTaskQueue().getCount();
650
- console.log(chalk.bold('\nTasks:'));
651
- console.log(` Total: ${taskCounts.total}`);
652
- console.log(` Pending: ${taskCounts.pending}`);
653
- console.log(` Running: ${taskCounts.running}`);
654
- console.log(` Completed: ${taskCounts.completed}`);
655
- console.log(` Failed: ${taskCounts.failed}`);
657
+ console.log(heading('Tasks:'));
658
+ console.log(kv('Total', String(taskCounts.total)));
659
+ console.log(kv('Pending', String(taskCounts.pending)));
660
+ console.log(kv('Running', String(taskCounts.running)));
661
+ console.log(kv('Completed', String(taskCounts.completed)));
662
+ console.log(kv('Failed', String(taskCounts.failed)));
656
663
  console.log('');
657
664
  });
658
665
  program
@@ -661,15 +668,23 @@ program
661
668
  .option('-f, --force', 'Skip confirmation')
662
669
  .action(async (options) => {
663
670
  if (!options.force) {
664
- console.log(chalk.yellow('This will stop all sessions and clear all state.'));
665
- console.log(chalk.yellow('Use --force to confirm.'));
666
- return;
671
+ console.log(palette.warn('This will stop all sessions and clear all state.'));
672
+ // Non-interactive callers keep the old refusal: a script piping into the
673
+ // CLI must never be able to reset state by hanging on an unseen question.
674
+ if (!isInteractive()) {
675
+ console.log(palette.warn('Use --force to confirm.'));
676
+ return;
677
+ }
678
+ if (!(await confirm('Reset all Codeman state?'))) {
679
+ console.log(palette.muted('○ Cancelled, nothing was changed'));
680
+ return;
681
+ }
667
682
  }
668
683
  const manager = getSessionManager();
669
684
  const store = getStore();
670
685
  await manager.stopAllSessions();
671
686
  store.reset();
672
- console.log(chalk.green('✓ All state reset'));
687
+ console.log(palette.ok('✓ All state reset'));
673
688
  });
674
689
  // Shorthand commands at root level
675
690
  program
@@ -679,35 +694,41 @@ program
679
694
  .action(async (options) => {
680
695
  const manager = getSessionManager();
681
696
  const session = await manager.createSession(options.dir);
682
- console.log(chalk.green(`✓ Session started: ${session.id}`));
697
+ console.log(palette.ok(`✓ Session started: ${session.id}`));
683
698
  });
684
699
  program
685
700
  .command('list')
686
701
  .alias('ls')
687
- .description('List all sessions (shorthand)')
688
- .action(() => {
689
- const manager = getSessionManager();
690
- const sessions = manager.getAllSessions();
691
- const stored = manager.getStoredSessions();
692
- if (sessions.length === 0 && Object.keys(stored).length === 0) {
693
- console.log(chalk.yellow('No sessions found'));
702
+ .description('List active sessions (shorthand; `codeman session list` also shows stopped ones)')
703
+ .action(() => printSessionList({ includeStored: false }));
704
+ // ============ TUI ============
705
+ program
706
+ .command('tui')
707
+ .argument('[n]', 'attach straight to the nth session of `codeman tui --list`')
708
+ .description('Terminal dashboard for your sessions (the web UI remains the primary surface)')
709
+ .option('-l, --list', 'Print the numbered session list and exit, instead of opening the dashboard')
710
+ .action(async (position, options) => {
711
+ // Imported here, not at the top: the dashboard pulls in the whole TUI core,
712
+ // and every other command would pay for it at startup.
713
+ const { runTui, runTuiAttach, runTuiList } = await import('./tui/tui-app.js');
714
+ if (options.list) {
715
+ process.exitCode = await runTuiList();
694
716
  return;
695
717
  }
696
- console.log(chalk.bold('\nActive Sessions:'));
697
- if (sessions.length === 0) {
698
- console.log(' (none)');
699
- }
700
- else {
701
- for (const session of sessions) {
702
- const status = session.status === 'idle'
703
- ? chalk.green('idle')
704
- : session.status === 'busy'
705
- ? chalk.yellow('busy')
706
- : chalk.red(session.status);
707
- console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status} ${session.workingDir}`);
718
+ if (position !== undefined) {
719
+ const n = Number.parseInt(position, 10);
720
+ if (!Number.isSafeInteger(n) || n < 1) {
721
+ console.error(palette.err(`"${position}" is not a session number.`));
722
+ console.error(`Run ${palette.info('codeman tui --list')} to see them.`);
723
+ process.exitCode = 1;
724
+ return;
708
725
  }
726
+ process.exitCode = await runTuiAttach(n);
727
+ return;
709
728
  }
710
- console.log('');
729
+ // The dashboard owns the terminal until it quits; exiting explicitly keeps a
730
+ // stray handle (a socket mid-close) from stranding the user's shell.
731
+ process.exit(await runTui());
711
732
  });
712
733
  // ============ Web / daemon / service Commands ============
713
734
  /** Shared option set for the commands that can launch a web server. */
@@ -724,7 +745,7 @@ function addWebLaunchOptions(cmd) {
724
745
  function toWebLaunchOptions(options) {
725
746
  const port = parseInt(options.port, 10);
726
747
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
727
- console.error(chalk.red(`✗ Invalid port: ${options.port}`));
748
+ console.error(palette.err(`✗ Invalid port: ${options.port}`));
728
749
  process.exit(1);
729
750
  }
730
751
  return {
@@ -745,8 +766,8 @@ function warnIfUnauthenticatedNetwork(launch) {
745
766
  return;
746
767
  if (isUnauthenticatedNetworkAcknowledged(launch.allowUnauthenticatedNetwork))
747
768
  return;
748
- console.log(chalk.yellow(`⚠ Binding ${launch.host} without CODEMAN_PASSWORD: anyone who can reach this port gets terminal control.`));
749
- console.log(chalk.yellow(' Set CODEMAN_PASSWORD, or bind 127.0.0.1 and front it with tailscale serve.'));
769
+ console.log(palette.warn(`⚠ Binding ${launch.host} without CODEMAN_PASSWORD: anyone who can reach this port gets terminal control.`));
770
+ console.log(palette.warn(' Set CODEMAN_PASSWORD, or bind 127.0.0.1 and front it with tailscale serve.'));
750
771
  }
751
772
  // Web interface command
752
773
  const webCmd = addWebLaunchOptions(program.command('web').description('Start the web interface'))
@@ -760,48 +781,49 @@ webCmd.action(async (options) => {
760
781
  process.env.CODEMAN_MULTIUSER = '1';
761
782
  const launch = toWebLaunchOptions(options);
762
783
  if (options.stop) {
763
- const result = await stopDaemon(launch);
784
+ // stopDaemon waits for the process to actually exit (up to 15s).
785
+ const result = await withSpinner('Stopping Codeman...', () => stopDaemon(launch));
764
786
  if (result.ok && result.reason === 'not-running') {
765
- console.log(chalk.gray(`○ ${result.message}`));
787
+ console.log(palette.muted(`○ ${result.message}`));
766
788
  return;
767
789
  }
768
790
  if (result.ok) {
769
- console.log(chalk.green(`✓ ${result.message ?? `Stopped Codeman (pid ${result.pid})`}`));
770
- console.log(chalk.gray(' Your agents keep running in tmux.'));
791
+ console.log(palette.ok(`✓ ${result.message ?? `Stopped Codeman (pid ${result.pid})`}`));
792
+ console.log(palette.muted(' Your agents keep running in tmux.'));
771
793
  return;
772
794
  }
773
- console.error(chalk.red(`✗ ${result.message ?? 'Could not stop the server'}`));
795
+ console.error(palette.err(`✗ ${result.message ?? 'Could not stop the server'}`));
774
796
  process.exit(1);
775
797
  }
776
798
  if (options.status) {
777
799
  const status = await daemonStatus(launch);
778
800
  if (status.responding) {
779
801
  const version = status.version ? ` (v${status.version})` : '';
780
- console.log(chalk.green(`✓ Responding at ${status.url}${version}`));
802
+ console.log(palette.ok(`✓ Responding at ${status.url}${version}`));
781
803
  }
782
804
  else {
783
- console.log(chalk.yellow(`○ Nothing answering at ${status.url}`));
805
+ console.log(palette.warn(`○ Nothing answering at ${status.url}`));
784
806
  }
785
- console.log(` Daemon pid: ${status.running ? chalk.green(String(status.pid)) : chalk.gray('not running')}`);
786
- console.log(chalk.gray(` Pidfile: ${status.pidFile}`));
787
- console.log(chalk.gray(` Log: ${status.logPath}`));
807
+ console.log(kv('Daemon pid', status.running ? palette.ok(String(status.pid)) : palette.muted('not running'), 11));
808
+ console.log(palette.muted(kv('Pidfile', status.pidFile, 11)));
809
+ console.log(palette.muted(kv('Log', status.logPath, 11)));
788
810
  if (!status.running && status.responding) {
789
- console.log(chalk.gray(' (running, but not started with --daemon: probably a service or a foreground run)'));
811
+ console.log(palette.muted(' (running, but not started with --daemon: probably a service or a foreground run)'));
790
812
  }
791
813
  return;
792
814
  }
793
815
  if (options.daemon) {
794
816
  warnIfUnauthenticatedNetwork(launch);
795
- console.log(chalk.cyan('Starting Codeman in the background...'));
796
- const result = await startDaemon(launch);
817
+ // The start polls /api/status for up to 30s; without this the shell just sits there.
818
+ const result = await withSpinner('Starting Codeman in the background, waiting for it to answer...', () => startDaemon(launch));
797
819
  if (result.ok) {
798
- console.log(chalk.green(`\n✓ Codeman is running at ${result.url} (pid ${result.pid})`));
799
- console.log(chalk.gray(` Logs: ${result.logPath}`));
800
- console.log(chalk.gray(' Stop it with: codeman web --stop'));
801
- console.log(chalk.gray(' Want it back after a reboot? codeman service install'));
820
+ console.log(palette.ok(`\n✓ Codeman is running at ${result.url} (pid ${result.pid})`));
821
+ console.log(palette.muted(` Logs: ${result.logPath}`));
822
+ console.log(palette.muted(' Stop it with: codeman web --stop'));
823
+ console.log(palette.muted(' Want it back after a reboot? codeman service install'));
802
824
  return;
803
825
  }
804
- console.error(chalk.red(`\n✗ ${result.message ?? 'Failed to start'}`));
826
+ console.error(palette.err(`\n✗ ${result.message ?? 'Failed to start'}`));
805
827
  process.exit(1);
806
828
  }
807
829
  const { startWebServer } = await import('./web/server.js');
@@ -810,28 +832,28 @@ webCmd.action(async (options) => {
810
832
  const https = launch.https;
811
833
  const titleHostname = options.titleHostname;
812
834
  const allowUnauthenticatedNetwork = launch.allowUnauthenticatedNetwork ?? false;
813
- const protocol = https ? 'https' : 'http';
814
835
  const displayHost = host === '0.0.0.0' ? 'localhost' : host;
815
- console.log(chalk.cyan(`Starting Codeman web interface on ${displayHost}:${port}${https ? ' (HTTPS)' : ''}...`));
836
+ console.log(palette.info(`Starting Codeman web interface on ${displayHost}:${port}${https ? ' (HTTPS)' : ''}...`));
816
837
  try {
838
+ // The server prints its own "running at" line (it also covers the daemon and
839
+ // service launch paths), so this one used to be a duplicate of it.
817
840
  const server = await startWebServer(port, https, false, host, titleHostname, allowUnauthenticatedNetwork);
818
- console.log(chalk.green(`\n✓ Web interface running at ${protocol}://${displayHost}:${port}`));
819
841
  if (https) {
820
- console.log(chalk.yellow(' Note: Accept the self-signed certificate in your browser on first visit'));
842
+ console.log(palette.warn(' Note: Accept the self-signed certificate in your browser on first visit'));
821
843
  }
822
- console.log(chalk.gray(' Press Ctrl+C to stop\n'));
844
+ console.log(palette.muted(' Press Ctrl+C to stop\n'));
823
845
  // Graceful shutdown handler — flush state and clean up on SIGTERM/SIGINT
824
846
  let shuttingDown = false;
825
847
  const shutdown = async (signal) => {
826
848
  if (shuttingDown)
827
849
  return;
828
850
  shuttingDown = true;
829
- console.log(chalk.yellow(`\n${signal} received, shutting down gracefully...`));
851
+ console.log(palette.warn(`\n${signal} received, shutting down gracefully...`));
830
852
  try {
831
853
  await server.stop();
832
854
  }
833
855
  catch (err) {
834
- console.error(chalk.red(`Error during shutdown: ${getErrorMessage(err)}`));
856
+ console.error(palette.err(`Error during shutdown: ${getErrorMessage(err)}`));
835
857
  }
836
858
  process.exit(0);
837
859
  };
@@ -840,7 +862,7 @@ webCmd.action(async (options) => {
840
862
  process.on('SIGHUP', () => shutdown('SIGHUP'));
841
863
  }
842
864
  catch (err) {
843
- console.error(chalk.red(`✗ Failed to start web server: ${getErrorMessage(err)}`));
865
+ console.error(palette.err(`✗ Failed to start web server: ${getErrorMessage(err)}`));
844
866
  process.exit(1);
845
867
  }
846
868
  });
@@ -852,18 +874,19 @@ const serviceCmd = program
852
874
  addWebLaunchOptions(serviceCmd.command('install').description('Install and start the service, then verify it answers')).action(async (options) => {
853
875
  const launch = toWebLaunchOptions(options);
854
876
  warnIfUnauthenticatedNetwork(launch);
855
- console.log(chalk.cyan('Installing the Codeman service...'));
856
- const result = await installService(launch);
877
+ // Install polls the new unit's /api/status for up to 30s before it can honestly
878
+ // report success, so the wait needs a visible heartbeat.
879
+ const result = await withSpinner('Installing the Codeman service, waiting for it to answer...', () => installService(launch));
857
880
  for (const warning of result.warnings ?? [])
858
- console.log(chalk.yellow(`⚠ ${warning}`));
881
+ console.log(palette.warn(`⚠ ${warning}`));
859
882
  if (!result.ok) {
860
- console.error(chalk.red(`✗ ${result.message}`));
883
+ console.error(palette.err(`✗ ${result.message}`));
861
884
  process.exit(1);
862
885
  }
863
- console.log(chalk.green(`✓ ${result.message}`));
864
- console.log(chalk.gray(` Unit: ${result.unitPath}`));
886
+ console.log(palette.ok(`✓ ${result.message}`));
887
+ console.log(palette.muted(` Unit: ${result.unitPath}`));
865
888
  if (process.env.CODEMAN_PASSWORD) {
866
- console.log(chalk.yellow(' Note: CODEMAN_PASSWORD was NOT copied into the unit file. Add it there yourself if the service needs auth.'));
889
+ console.log(palette.warn(' Note: CODEMAN_PASSWORD was NOT copied into the unit file. Add it there yourself if the service needs auth.'));
867
890
  }
868
891
  });
869
892
  serviceCmd
@@ -872,22 +895,22 @@ serviceCmd
872
895
  .action(() => {
873
896
  const result = uninstallService();
874
897
  if (!result.ok) {
875
- console.error(chalk.red(`✗ ${result.message}`));
898
+ console.error(palette.err(`✗ ${result.message}`));
876
899
  process.exit(1);
877
900
  }
878
- console.log(chalk.green(`✓ ${result.message}`));
901
+ console.log(palette.ok(`✓ ${result.message}`));
879
902
  });
880
903
  addWebLaunchOptions(serviceCmd.command('status').description('Show whether the service is installed and running')).action(async (options) => {
881
904
  const status = await serviceStatus(toWebLaunchOptions(options));
882
905
  if (!status.kind) {
883
- console.log(chalk.yellow(`No supported supervisor on ${process.platform}. Use \`codeman web -d\` instead.`));
906
+ console.log(palette.warn(`No supported supervisor on ${process.platform}. Use \`codeman web -d\` instead.`));
884
907
  return;
885
908
  }
886
909
  console.log(` Supervisor: ${status.kind} (${status.name})`);
887
- console.log(` Unit file: ${status.installed ? chalk.green(status.unitPath) : chalk.gray('not installed')}`);
888
- console.log(` Loaded: ${status.loaded ? chalk.green('yes') : chalk.gray('no')}`);
910
+ console.log(` Unit file: ${status.installed ? palette.ok(status.unitPath) : palette.muted('not installed')}`);
911
+ console.log(` Loaded: ${status.loaded ? palette.ok('yes') : palette.muted('no')}`);
889
912
  const version = status.version ? ` (v${status.version})` : '';
890
- console.log(` Responding: ${status.responding ? chalk.green(`yes at ${status.url}${version}`) : chalk.gray(`no at ${status.url}`)}`);
913
+ console.log(` Responding: ${status.responding ? palette.ok(`yes at ${status.url}${version}`) : palette.muted(`no at ${status.url}`)}`);
891
914
  });
892
915
  // ============ Multi-user Commands ============
893
916
  //
@@ -954,7 +977,7 @@ usersCmd
954
977
  .action(async (name, options) => {
955
978
  const { createUser, isValidUsername } = await import('./user-store.js');
956
979
  if (!isValidUsername(name)) {
957
- console.error(chalk.red('✗ Username must be lowercase, start alphanumeric, 2-32 chars ([a-z0-9_-])'));
980
+ console.error(palette.err('✗ Username must be lowercase, start alphanumeric, 2-32 chars ([a-z0-9_-])'));
958
981
  process.exit(1);
959
982
  }
960
983
  try {
@@ -966,19 +989,19 @@ usersCmd
966
989
  password = await promptHiddenPassword('New password: ');
967
990
  const confirm = await promptHiddenPassword('Confirm password: ');
968
991
  if (password !== confirm) {
969
- console.error(chalk.red('✗ Passwords do not match'));
992
+ console.error(palette.err('✗ Passwords do not match'));
970
993
  process.exit(1);
971
994
  }
972
995
  }
973
996
  if (!password || password.length < 8) {
974
- console.error(chalk.red('✗ Password must be at least 8 characters'));
997
+ console.error(palette.err('✗ Password must be at least 8 characters'));
975
998
  process.exit(1);
976
999
  }
977
1000
  const user = await createUser({ username: name, role: options.admin ? 'admin' : 'user', password });
978
- console.log(chalk.green(`✓ Created ${user.role} "${user.username}"`));
1001
+ console.log(palette.ok(`✓ Created ${user.role} "${user.username}"`));
979
1002
  }
980
1003
  catch (err) {
981
- console.error(chalk.red(`✗ ${getErrorMessage(err)}`));
1004
+ console.error(palette.err(`✗ ${getErrorMessage(err)}`));
982
1005
  process.exit(1);
983
1006
  }
984
1007
  });
@@ -997,15 +1020,15 @@ usersCmd
997
1020
  password = await promptHiddenPassword('New password: ');
998
1021
  const confirm = await promptHiddenPassword('Confirm password: ');
999
1022
  if (password !== confirm) {
1000
- console.error(chalk.red('✗ Passwords do not match'));
1023
+ console.error(palette.err('✗ Passwords do not match'));
1001
1024
  process.exit(1);
1002
1025
  }
1003
1026
  }
1004
1027
  await setPassword(name, password, { mustChangePassword: false });
1005
- console.log(chalk.green(`✓ Password updated for "${name}"`));
1028
+ console.log(palette.ok(`✓ Password updated for "${name}"`));
1006
1029
  }
1007
1030
  catch (err) {
1008
- console.error(chalk.red(`✗ ${getErrorMessage(err)}`));
1031
+ console.error(palette.err(`✗ ${getErrorMessage(err)}`));
1009
1032
  process.exit(1);
1010
1033
  }
1011
1034
  });
@@ -1017,17 +1040,17 @@ usersCmd
1017
1040
  const { readUsers } = await import('./user-store.js');
1018
1041
  const users = await readUsers(true);
1019
1042
  if (users.length === 0) {
1020
- console.log(chalk.yellow('No users defined (run: codeman users add <name> --admin)'));
1043
+ console.log(palette.warn('No users defined (run: codeman users add <name> --admin)'));
1021
1044
  return;
1022
1045
  }
1023
- console.log(chalk.bold('\nUsers:'));
1046
+ console.log(palette.emph('\nUsers:'));
1024
1047
  for (const u of users) {
1025
- const role = u.role === 'admin' ? chalk.magenta('admin') : chalk.cyan('user ');
1026
- const state = u.disabled ? chalk.red('disabled') : chalk.green('enabled ');
1048
+ const role = u.role === 'admin' ? palette.accent('admin') : palette.info('user ');
1049
+ const state = u.disabled ? palette.err('disabled') : palette.ok('enabled ');
1027
1050
  const flags = [u.mustChangePassword ? 'must-change-pw' : '', u.canBypassPermissions ? 'can-bypass' : '']
1028
1051
  .filter(Boolean)
1029
1052
  .join(' ');
1030
- console.log(` ${role} ${state} ${u.username}${flags ? chalk.gray(` [${flags}]`) : ''}`);
1053
+ console.log(` ${role} ${state} ${u.username}${flags ? palette.muted(` [${flags}]`) : ''}`);
1031
1054
  }
1032
1055
  console.log('');
1033
1056
  });
@@ -1041,17 +1064,44 @@ usersCmd
1041
1064
  await deleteUser(name);
1042
1065
  if (options.deleteSpace) {
1043
1066
  await deleteUserSpace(name);
1044
- console.log(chalk.green(`✓ Deleted user "${name}" and their space`));
1067
+ console.log(palette.ok(`✓ Deleted user "${name}" and their space`));
1045
1068
  }
1046
1069
  else {
1047
- console.log(chalk.green(`✓ Deleted user "${name}" (space left on disk)`));
1070
+ console.log(palette.ok(`✓ Deleted user "${name}" (space left on disk)`));
1048
1071
  }
1049
1072
  }
1050
1073
  catch (err) {
1051
- console.error(chalk.red(`✗ ${getErrorMessage(err)}`));
1074
+ console.error(palette.err(`✗ ${getErrorMessage(err)}`));
1052
1075
  process.exit(1);
1053
1076
  }
1054
1077
  });
1078
+ /**
1079
+ * Missing REQUIRED tools are failures; a missing optional one or a skipped check
1080
+ * is just absence, so it stays muted rather than shouting red at everyone
1081
+ * without LibreOffice installed.
1082
+ */
1083
+ function dependencyTone(result) {
1084
+ if (result.status === 'ok')
1085
+ return 'ok';
1086
+ if (result.status === 'skipped')
1087
+ return 'idle';
1088
+ return result.required ? 'err' : 'idle';
1089
+ }
1090
+ /**
1091
+ * The colorize hook `dependency-report.ts` was written for. Versions stay in the
1092
+ * default color (they are data, not a verdict); everything that IS a verdict is
1093
+ * painted, and the supporting detail is muted so the glyph column reads first.
1094
+ */
1095
+ const DOCTOR_STYLE = {
1096
+ title: (text) => palette.emph(text),
1097
+ heading: (text) => palette.emph(palette.info(text)),
1098
+ glyph: (result, glyph) => tint(dependencyTone(result), glyph),
1099
+ label: (text) => text,
1100
+ status: (result, text) => (result.status === 'ok' ? text : tint(dependencyTone(result), text)),
1101
+ path: (text) => palette.muted(text),
1102
+ meta: (text) => palette.muted(text),
1103
+ summary: (text) => palette.emph(text),
1104
+ };
1055
1105
  program
1056
1106
  .command('doctor')
1057
1107
  .alias('check-deps')
@@ -1072,10 +1122,11 @@ program
1072
1122
  : DEPENDENCY_REGISTRY;
1073
1123
  const results = checkAll(registry, host);
1074
1124
  if (options.json) {
1125
+ // Raw JSON, never styled: this output is parsed, not read.
1075
1126
  console.log(JSON.stringify(renderJson(results, host.environment), null, 2));
1076
1127
  }
1077
1128
  else {
1078
- console.log(renderTable(results, host.environment));
1129
+ console.log(renderTable(results, host.environment, DOCTOR_STYLE));
1079
1130
  }
1080
1131
  process.exit(computeExitCode(results));
1081
1132
  });