@webship.co/workspace 0.0.0-stage → 1.0.0-alpha1

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 (311) hide show
  1. package/PLUGINS.md +249 -0
  2. package/README.md +379 -2
  3. package/WORKSPACES.md +69 -0
  4. package/actions/ai-items.js +293 -0
  5. package/actions/audio.js +48 -0
  6. package/actions/build-order.js +79 -0
  7. package/actions/checks.js +95 -0
  8. package/actions/claude-session.js +104 -0
  9. package/actions/commands-center.js +258 -0
  10. package/actions/composer.js +267 -0
  11. package/actions/ddev-addons.js +90 -0
  12. package/actions/ddev-mail.js +80 -0
  13. package/actions/ddev.js +237 -0
  14. package/actions/folders.js +165 -0
  15. package/actions/graphs.js +107 -0
  16. package/actions/images-svg.js +33 -0
  17. package/actions/images.js +145 -0
  18. package/actions/navigation.js +190 -0
  19. package/actions/node.js +222 -0
  20. package/actions/notes.js +55 -0
  21. package/actions/obsidian.js +59 -0
  22. package/actions/plugins.js +36 -0
  23. package/actions/project-make.js +28 -0
  24. package/actions/rag.js +202 -0
  25. package/actions/row-actions.js +262 -0
  26. package/actions/screens.js +270 -0
  27. package/actions/testing.js +121 -0
  28. package/actions/video-edit.js +54 -0
  29. package/ai-inventory.js +182 -0
  30. package/ai-items.js +202 -0
  31. package/assistant-cast.js +176 -0
  32. package/assistant-suggestions.js +91 -0
  33. package/assistant.js +484 -0
  34. package/audio-voice.js +528 -0
  35. package/auth-google.js +260 -0
  36. package/build-ai.js +52 -0
  37. package/build-order-page.js +191 -0
  38. package/build-order.js +313 -0
  39. package/builder-args.js +117 -0
  40. package/changelog.js +302 -0
  41. package/chat-threads.js +134 -0
  42. package/checks.js +105 -0
  43. package/claude-args.js +282 -0
  44. package/claude-sessions.js +129 -0
  45. package/command-author.js +117 -0
  46. package/command-block.js +20 -0
  47. package/commands/defaults.yml +159 -0
  48. package/commands-page.js +460 -0
  49. package/commands.js +568 -0
  50. package/cucumber.js +43 -0
  51. package/ddev-addons-page.js +124 -0
  52. package/ddev-addons.js +154 -0
  53. package/ddev-custom-commands.js +96 -0
  54. package/ddev-domains.js +84 -0
  55. package/ddev-import.js +93 -0
  56. package/ddev-mail-page.js +167 -0
  57. package/ddev-mail.js +110 -0
  58. package/ddev-options.js +145 -0
  59. package/ddev-snapshots.js +126 -0
  60. package/ddev.js +145 -0
  61. package/docs/DEVELOPER-GUIDE.md +224 -0
  62. package/docs/README.md +252 -0
  63. package/docs/REFERENCE.md +684 -0
  64. package/docs/USER-GUIDE.md +697 -0
  65. package/docs/screenshots/assistant/avatar-board.png +0 -0
  66. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  67. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  68. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  69. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  70. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  71. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  72. package/docs/screenshots/assistant/avatar-status.png +0 -0
  73. package/docs/screenshots/create-card/create-card.png +0 -0
  74. package/docs/screenshots/create-card/start-ai.png +0 -0
  75. package/docs/screenshots/guide-clone-address.png +0 -0
  76. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  77. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  78. package/docs/screenshots/guide-ddev-menu.png +0 -0
  79. package/docs/screenshots/guide-domains.png +0 -0
  80. package/docs/screenshots/guide-home.png +0 -0
  81. package/docs/screenshots/guide-ide-menu.png +0 -0
  82. package/docs/screenshots/guide-remove-refusal.png +0 -0
  83. package/docs/screenshots/guide-rows-worktree.png +0 -0
  84. package/docs/screenshots/guide-screens.png +0 -0
  85. package/docs/screenshots/guide-settings.png +0 -0
  86. package/docs/screenshots/guide-workspace-page.png +0 -0
  87. package/docs/screenshots/home/card-settings.png +0 -0
  88. package/docs/screenshots/home/drag-reorder.png +0 -0
  89. package/docs/screenshots/home/edit-card.png +0 -0
  90. package/docs/screenshots/images/claude-code-image.png +0 -0
  91. package/docs/screenshots/images/edit-with-ai.png +0 -0
  92. package/docs/screenshots/images/gallery.png +0 -0
  93. package/docs/screenshots/images/generate-description.png +0 -0
  94. package/docs/screenshots/images/generate-plan.png +0 -0
  95. package/docs/screenshots/images/providers.png +0 -0
  96. package/docs/screenshots/images/svg-form.png +0 -0
  97. package/docs/screenshots/images/svg-result.png +0 -0
  98. package/docs/screenshots/images/upload.png +0 -0
  99. package/docs/screenshots/new-folder/2-form.png +0 -0
  100. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  101. package/docs/screenshots/new-folder/files-folder.png +0 -0
  102. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  103. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  104. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  105. package/docs/screenshots/readme/dashboard-light.png +0 -0
  106. package/docs/screenshots/readme/dev-dark.png +0 -0
  107. package/docs/screenshots/readme/dev-light.png +0 -0
  108. package/docs/screenshots/readme/docs-dark.png +0 -0
  109. package/docs/screenshots/readme/docs-light.png +0 -0
  110. package/docs/screenshots/readme/icons/ai.svg +4 -0
  111. package/docs/screenshots/readme/icons/code.svg +4 -0
  112. package/docs/screenshots/readme/icons/docs.svg +4 -0
  113. package/docs/screenshots/readme/icons/services.svg +4 -0
  114. package/docs/screenshots/readme/icons/sites.svg +4 -0
  115. package/docs/screenshots/readme/test-report-dark.png +0 -0
  116. package/docs/screenshots/readme/test-report-light.png +0 -0
  117. package/docs/screenshots/readme/test-scenarios.png +0 -0
  118. package/docs/screenshots/readme/test-steps.png +0 -0
  119. package/docs/screenshots/readme/videos-dark.png +0 -0
  120. package/docs/screenshots/readme/videos-light.png +0 -0
  121. package/docs/screenshots/screens-menu.png +0 -0
  122. package/docs/screenshots/screens-new.png +0 -0
  123. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  124. package/docs/screenshots/settings/delete-1-row.png +0 -0
  125. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  126. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  127. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  128. package/event-log.js +59 -0
  129. package/files-browser.js +247 -0
  130. package/folders.js +121 -0
  131. package/format.js +73 -0
  132. package/git-info.js +81 -0
  133. package/github-auth.js +62 -0
  134. package/graphs.js +104 -0
  135. package/html.js +10 -0
  136. package/icons.js +83 -0
  137. package/images-ai.js +447 -0
  138. package/images-create.js +226 -0
  139. package/images-page.js +417 -0
  140. package/images-records.js +39 -0
  141. package/images-render.js +36 -0
  142. package/images-svg-core.js +214 -0
  143. package/images-svg-sanitize.js +188 -0
  144. package/images-svg.js +241 -0
  145. package/images.js +270 -0
  146. package/jobs.js +430 -0
  147. package/linger.js +21 -0
  148. package/lists.js +292 -0
  149. package/media-info.js +136 -0
  150. package/navigation-page.js +264 -0
  151. package/navigation.js +394 -0
  152. package/notes.js +0 -0
  153. package/obsidian.js +405 -0
  154. package/package.json +47 -4
  155. package/palette-index.js +46 -0
  156. package/playwright.config.js +23 -0
  157. package/plugin-install.js +239 -0
  158. package/plugin-loader.js +448 -0
  159. package/plugin-manifest.js +83 -0
  160. package/plugin-settings.js +44 -0
  161. package/plugins/workspace-notes/index.js +59 -0
  162. package/plugins-page.js +103 -0
  163. package/project-make.js +241 -0
  164. package/project-name.js +210 -0
  165. package/public/assistant-avatar-moves.js +646 -0
  166. package/public/assistant-avatar-ui.js +1223 -0
  167. package/public/assistant-avatar.js +530 -0
  168. package/public/avatar/druplicon.vrm +0 -0
  169. package/public/css/base.css +260 -0
  170. package/public/css/components/ai-assistant.css +263 -0
  171. package/public/css/components/assistant-avatar.css +279 -0
  172. package/public/css/components/assistant-views.css +198 -0
  173. package/public/css/components/build-order.css +74 -0
  174. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  175. package/public/css/components/command-palette.css +61 -0
  176. package/public/css/components/commands-center.css +139 -0
  177. package/public/css/components/confirm-dialog.css +40 -0
  178. package/public/css/components/create-card.css +8 -0
  179. package/public/css/components/ddev-menu.css +23 -0
  180. package/public/css/components/deep-chat.css +17 -0
  181. package/public/css/components/density.css +11 -0
  182. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  183. package/public/css/components/folders.css +5 -0
  184. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  185. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  186. package/public/css/components/grid-and-list.css +203 -0
  187. package/public/css/components/help-page.css +36 -0
  188. package/public/css/components/hero.css +16 -0
  189. package/public/css/components/images.css +70 -0
  190. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  191. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  192. package/public/css/components/modal-fit.css +113 -0
  193. package/public/css/components/page-width.css +23 -0
  194. package/public/css/components/plugins-page.css +9 -0
  195. package/public/css/components/project-cards.css +256 -0
  196. package/public/css/components/project-rows.css +390 -0
  197. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  198. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  199. package/public/css/components/running-commands-as-toasts.css +327 -0
  200. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  201. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  202. package/public/css/components/sound-player.css +84 -0
  203. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  204. package/public/css/components/tab-modal.css +142 -0
  205. package/public/css/components/terminals.css +244 -0
  206. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  207. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  208. package/public/css/components/video-edit.css +6 -0
  209. package/public/css/components/web-ide-open-with.css +23 -0
  210. package/public/css/components/working-indicators.css +158 -0
  211. package/public/css/components/workspace-cards.css +79 -0
  212. package/public/favicon.ico +0 -0
  213. package/public/favicon.png +0 -0
  214. package/public/htmx.min.js +1 -0
  215. package/public/logo.png +0 -0
  216. package/public/media-player.js +215 -0
  217. package/public/mic.js +255 -0
  218. package/public/narration.js +110 -0
  219. package/public/palette.js +278 -0
  220. package/public/sortable.js +251 -0
  221. package/public/suggest.js +337 -0
  222. package/public/terminal.js +903 -0
  223. package/public/themes/default/theme.css +267 -0
  224. package/public/themes/ember/theme.css +180 -0
  225. package/public/themes/press/theme.css +290 -0
  226. package/public/themes/slate/theme.css +182 -0
  227. package/public/touch-icon.png +0 -0
  228. package/public/ui.js +2721 -0
  229. package/public/vendor/ace/LICENSE.txt +24 -0
  230. package/public/vendor/ace/ace.js +23 -0
  231. package/public/vendor/ace/ext-language_tools.js +8 -0
  232. package/public/vendor/ace/ext-searchbox.js +8 -0
  233. package/public/vendor/ace/mode-css.js +8 -0
  234. package/public/vendor/ace/mode-html.js +8 -0
  235. package/public/vendor/ace/mode-javascript.js +8 -0
  236. package/public/vendor/ace/mode-markdown.js +8 -0
  237. package/public/vendor/ace/mode-sh.js +8 -0
  238. package/public/vendor/ace/mode-yaml.js +8 -0
  239. package/public/vendor/ace/theme-textmate.js +8 -0
  240. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  241. package/public/vendor/ace/worker-yaml.js +1 -0
  242. package/public/vendor/deep-chat.LICENSE +21 -0
  243. package/public/vendor/deep-chat.bundle.js +1 -0
  244. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  245. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  246. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  248. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  249. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  251. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  252. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  253. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  254. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  255. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  256. package/public/vendor/tabler-LICENSE.txt +21 -0
  257. package/public/vendor/tabler-sprite.svg +1 -0
  258. package/public/vendor/three/LICENSE +21 -0
  259. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  260. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  261. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  262. package/public/vendor/three/three.core.min.js +6 -0
  263. package/public/vendor/three/three.module.min.js +6 -0
  264. package/public/vendor/three-vrm/LICENSE +22 -0
  265. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  266. package/public/vendor/uikit-icons.min.js +1 -0
  267. package/public/vendor/uikit.min.css +1 -0
  268. package/public/vendor/uikit.min.js +1 -0
  269. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  270. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  271. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  272. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  273. package/public/vendor/webship-icons/ui/play.svg +1 -0
  274. package/public/vendor/webship-icons/ui/share.svg +1 -0
  275. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  276. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  277. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  278. package/public/vendor/xterm/addon-fit.js +2 -0
  279. package/public/vendor/xterm/addon-unicode11.js +2 -0
  280. package/public/vendor/xterm/addon-web-links.js +2 -0
  281. package/public/vendor/xterm/xterm.css +218 -0
  282. package/public/vendor/xterm/xterm.js +2 -0
  283. package/rag.js +169 -0
  284. package/reader.js +132 -0
  285. package/repo-blobs.js +66 -0
  286. package/report-pdf.js +337 -0
  287. package/row-layout.js +1440 -0
  288. package/screenrc +37 -0
  289. package/screens.js +1063 -0
  290. package/scripts/code-server-start.sh +190 -0
  291. package/scripts/images-claude-finish.js +107 -0
  292. package/scripts/screen-host.sh +114 -0
  293. package/scripts/screen-runner.js +362 -0
  294. package/scripts/screen-then.js +47 -0
  295. package/scripts/update.sh +350 -0
  296. package/server.js +8140 -0
  297. package/settings-file.js +423 -0
  298. package/speech.js +297 -0
  299. package/tab-modal.js +75 -0
  300. package/test-reports.js +260 -0
  301. package/themes.js +381 -0
  302. package/transcripts.js +168 -0
  303. package/urls.js +13 -0
  304. package/validate.js +18 -0
  305. package/version-check.js +94 -0
  306. package/video-edit.js +588 -0
  307. package/waiting.js +85 -0
  308. package/workspace-card-tools.js +41 -0
  309. package/workspace-delete.js +139 -0
  310. package/workspace-types.js +85 -0
  311. package/workspaces.js +557 -0
package/claude-args.js ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * The Claude Code CLI's own arguments, read from `claude --help`.
3
+ *
4
+ * The New screen form used to offer a permission-mode select and one free-text box labelled "more
5
+ * claude options" — which means knowing the flags by heart, and typing them without a single check
6
+ * until the screen either starts or dies. The CLI already documents every one of them, so this parses
7
+ * what it prints rather than keeping a hand-written list that drifts the next time it gains a flag.
8
+ *
9
+ * Two things fall out of that:
10
+ *
11
+ * - every option is offered with the control its own shape asks for — a checkbox for a flag, a select
12
+ * for one with `(choices: …)`, a field for one that takes a value;
13
+ * - a value is checked against that shape before it reaches a shell line, instead of a charset test
14
+ * over one long string.
15
+ *
16
+ * **Not every flag belongs on a screen.** A screen is an interactive terminal in a directory — that is
17
+ * the whole point of starting one — so the flags that only work with `--print`, the ones that start the
18
+ * session somewhere else (the cloud, the background, a remote control), and the ones this form already
19
+ * asks about itself (the resume row, the prompt field) are held back, each with the reason. Everything
20
+ * else is offered, including flags that did not exist when this was written.
21
+ */
22
+
23
+ const { execFile } = require('child_process');
24
+
25
+ /* ------------------------------------------------------------------ *
26
+ * Parsing `claude --help`
27
+ * ------------------------------------------------------------------ */
28
+
29
+ /**
30
+ * One option of the Options: block. The flag line is ` -p, --print`, ` --add-dir <directories...>`,
31
+ * ` --autocompact <auto|tokens>` or ` --cloud [description|session_id|url]`, and its description runs
32
+ * on until the next one — wrapped, and sometimes starting on the line below when the flags are long.
33
+ */
34
+ function parseOptions(help) {
35
+ const body = help.slice(help.indexOf('\nOptions:'));
36
+ const end = body.indexOf('\nCommands:');
37
+ const lines = (end < 0 ? body : body.slice(0, end)).split('\n').slice(1);
38
+
39
+ const out = [];
40
+ let current = null;
41
+ for (const line of lines) {
42
+ const start = line.match(/^ {2}(-[^\s].*)$/);
43
+ if (start) {
44
+ if (current) out.push(current);
45
+ // Two columns: the flags, then a run of 2+ spaces, then the description — which wraps, and which
46
+ // starts on the NEXT line when the flags are too long for the column. Splitting on the first wide
47
+ // gap is what tells the two apart; without it the description was read as part of the flag name.
48
+ const both = start[1].match(/^(.*?)(?:\s{2,}(.*))?$/);
49
+ current = { spec: both[1].trim(), text: (both[2] || '').trim() };
50
+ continue;
51
+ }
52
+ if (!current) continue;
53
+ const cont = line.match(/^ {4,}(\S.*)$/);
54
+ if (cont) current.text += `${current.text ? ' ' : ''}${cont[1].trim()}`;
55
+ }
56
+ if (current) out.push(current);
57
+
58
+ return out.map(shape).filter(Boolean);
59
+ }
60
+
61
+ /**
62
+ * A raw entry as something to render and to check. The flags and the value come off the front of the
63
+ * spec; anything left on it is the first line of a wrapped description.
64
+ */
65
+ function shape({ spec, text }) {
66
+ // The value is the LAST bracketed group of the flag part: `--allowedTools, --allowed-tools <tools...>`.
67
+ const m = spec.match(/^([^<[]+?)(?:\s+([<[][^\])>]*[\])>]))?\s*$/);
68
+ if (!m) return null;
69
+ const flagPart = m[1].trim().replace(/,$/, '');
70
+ const arg = m[2] || '';
71
+ const flags = flagPart.split(/,\s*/).map((f) => f.trim()).filter((f) => /^--?[A-Za-z]/.test(f));
72
+ if (!flags.length) return null;
73
+ const name = flags.filter((f) => f.startsWith('--')).pop() || flags[0];
74
+
75
+ const description = text.replace(/\s+/g, ' ').trim();
76
+ // Only what the CLI calls a CHOICE is one. `<auto|tokens>` in the flag line is a hint at the shape of
77
+ // the value ("auto, or 100k-1M tokens"), and rendering that as a select would refuse 200k. The
78
+ // parenthesis also carries `default:` and `preset:` inside the same list, which are not choices.
79
+ const choices = (description.match(/\(choices: ([^)]*)\)/) || [])[1];
80
+
81
+ return {
82
+ name,
83
+ flags,
84
+ // `<x>` needs a value, `[x]` may take one, neither is a plain flag. `...` means it repeats.
85
+ takes: arg.startsWith('<') ? 'required' : arg.startsWith('[') ? 'optional' : 'none',
86
+ repeatable: arg.includes('...'),
87
+ placeholder: arg.replace(/^[<[]|[\])>]$/g, '').replace(/\.\.\.$/, ''),
88
+ choices: choices
89
+ ? choices.split(',').map((c) => c.trim()).filter((c) => c && !c.includes(':'))
90
+ .map((c) => c.replace(/^"|"$/g, ''))
91
+ // Some options enumerate their values in PROSE rather than in the `(choices: …)` the help
92
+ // generator emits — `--effort <level> Effort level for the current session (low, medium, high,
93
+ // xhigh, max)`. That is a closed list said in a sentence, and reading it is better than leaving
94
+ // a field where five known answers have to be typed from memory. Bare lowercase words only, and
95
+ // three or more of them: `(default: sonnet)` and `(e.g. 'fable', 'opus')` do not match, and
96
+ // neither does the `<auto|tokens>` shape hint the comment above warns about.
97
+ : (((description.match(/\((([a-z][a-z0-9-]*)(, [a-z][a-z0-9-]*){2,})\)/) || [])[1] || '')
98
+ .split(',').map((c) => c.trim()).filter(Boolean)),
99
+ default: (description.match(/\(default: ([^),]*)\)/) || [])[1] || '',
100
+ // What the help NAMES as examples, which is not the same as the whole set. `--model` says "an alias
101
+ // for the latest model (e.g. 'fable', 'opus', or 'sonnet') or a model's full name (e.g.
102
+ // 'claude-fable-5')" — so those are worth offering, and typing another one has to stay possible.
103
+ // A closed select here would refuse every model released after this help text was read.
104
+ suggestions: [...description.matchAll(/'([a-z][a-z0-9.-]{1,40})'/g)].map((m) => m[1]),
105
+ description,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * The agents `--agent` can actually resolve on this machine.
111
+ *
112
+ * Read from the files rather than from a list kept here: the CLI dispatches on the frontmatter `name`,
113
+ * which is what a file may have been cloned and renamed to, so the name in the file wins over the name
114
+ * of the file. A project's own `.claude/agents` counts too, which is why the directory is a parameter.
115
+ *
116
+ * Suggestions, not a closed list: an agent can also come from a plugin or a directory this does not
117
+ * know about, and refusing a name the CLI would have accepted is worse than offering an incomplete list.
118
+ */
119
+ function installedAgents(projectDir) {
120
+ const fs = require('fs');
121
+ const path = require('path');
122
+ const homes = [path.join(process.env.HOME || '', '.claude', 'agents')];
123
+ if (projectDir) homes.push(path.join(projectDir, '.claude', 'agents'));
124
+ const names = new Set();
125
+ for (const dir of homes) {
126
+ let files = [];
127
+ try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')); } catch (_) { continue; }
128
+ for (const f of files) {
129
+ let head = '';
130
+ try { head = fs.readFileSync(path.join(dir, f), 'utf8').slice(0, 800); } catch (_) { continue; }
131
+ // An agent is a file with FRONTMATTER naming it — that is what the CLI dispatches on, and it is
132
+ // also what tells an agent apart from the prose that lives in the same folder: RULES.md there is
133
+ // the standing working rules, not something `--agent RULES` could ever resolve. Blocklisting the
134
+ // names of the docs would work until somebody added another one.
135
+ if (!/^---\s*$/m.test(head.split('\n')[0] || '')) continue;
136
+ const m = head.match(/^name:\s*["']?([a-zA-Z0-9][a-zA-Z0-9_-]{0,60})["']?\s*$/m);
137
+ if (!m) continue;
138
+ names.add(m[1]);
139
+ }
140
+ }
141
+ return [...names].sort();
142
+ }
143
+
144
+ /* ------------------------------------------------------------------ *
145
+ * What belongs on a screen
146
+ * ------------------------------------------------------------------ */
147
+
148
+ /**
149
+ * Held back, each with the reason it is not offered. Stated rather than filtered by a clever rule: a
150
+ * person reading this should be able to see why a flag they know is missing, and disagree with it.
151
+ */
152
+ const WITHHELD = {
153
+ '--help': 'prints the help and exits',
154
+ '--version': 'prints the version and exits',
155
+ '--print': 'a screen IS the interactive session; --print exits as soon as it has answered',
156
+ '--resume': 'the Resume row above asks this, and checks the id against the sessions that directory has',
157
+ '--continue': 'takes the most recent session for the directory, which after a day of parallel work is often not the one meant — resume by id instead',
158
+ '--from-pr': 'opens its own picker over the session this form is starting',
159
+ '--session-id': 'a new session gets its own id; naming one that exists resumes it, which the Resume row does',
160
+ '--background': 'returns immediately, so the screen would hold nothing',
161
+ '--bg': 'returns immediately, so the screen would hold nothing',
162
+ '--cloud': 'runs the session somewhere else, so a screen on this machine is not where it is',
163
+ '--environment': 'belongs to a cloud session',
164
+ '--teleport': 'resumes a teleport session rather than starting one here',
165
+ '--remote-control': 'a screen is already the way back to this session, through screen -r',
166
+ '--remote-control-session-name-prefix': 'belongs to Remote Control',
167
+ '--tmux': 'a tmux inside a screen is two multiplexers over one session',
168
+ '--ide': 'connects to an IDE on this machine, which a detached session has no window in',
169
+ '--chrome': 'drives a browser on the desktop, which a detached session cannot show',
170
+ '--no-chrome': 'the integration is off unless --chrome asks for it',
171
+ '--file': 'downloads resources by id, which belongs to a hosted session',
172
+ '--permission-prompts': 'answers prompts for --print; on a screen a person is sitting in front of it',
173
+ '--json-schema': 'validates structured output, which needs --print to have any output to validate',
174
+ };
175
+
176
+ /** Only works when the CLI is printing rather than running a session. */
177
+ const PRINT_ONLY = /only works with (--print|--input-format|--output-format)|\(only works with --print\)/i;
178
+
179
+ function usableOnAScreen(opt) {
180
+ if (WITHHELD[opt.name]) return false;
181
+ if (opt.flags.some((f) => WITHHELD[f])) return false;
182
+ return !PRINT_ONLY.test(opt.description);
183
+ }
184
+
185
+ /* ------------------------------------------------------------------ *
186
+ * Reading it, once
187
+ * ------------------------------------------------------------------ */
188
+
189
+ // The help text changes only when the CLI is updated, and the form is drawn on every modal open — so it
190
+ // is read once and kept. A failure is kept too, briefly: a machine with no claude on PATH must not shell
191
+ // out on every keystroke to be told so again.
192
+ let memo = { at: 0, options: null, error: '' };
193
+ const TTL = 10 * 60 * 1000;
194
+ const FAIL_TTL = 30 * 1000;
195
+
196
+ function helpText() {
197
+ return new Promise((resolve) => {
198
+ execFile('claude', ['--help'], { timeout: 15000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
199
+ // Commander prints its help on stdout; a version that uses stderr is still the help.
200
+ const text = `${stdout || ''}${stderr || ''}`;
201
+ if (text.includes('\nOptions:')) return resolve({ text });
202
+ resolve({ error: (err && err.message) || 'claude --help printed no options' });
203
+ });
204
+ });
205
+ }
206
+
207
+ /** Every option a screen can be started with, newest help text, cached. */
208
+ async function claudeOptions() {
209
+ const now = Date.now();
210
+ const ttl = memo.error ? FAIL_TTL : TTL;
211
+ if (memo.options && now - memo.at < ttl) return memo;
212
+ const { text, error } = await helpText();
213
+ memo = error
214
+ ? { at: now, options: [], error }
215
+ : { at: now, options: parseOptions(text).filter(usableOnAScreen), error: '' };
216
+ return memo;
217
+ }
218
+
219
+ /** Forget what was read — for a test, and for a machine that has just updated the CLI. */
220
+ function forget() { memo = { at: 0, options: null, error: '' }; }
221
+
222
+ /* ------------------------------------------------------------------ *
223
+ * How the form shows them
224
+ * ------------------------------------------------------------------ */
225
+
226
+ /**
227
+ * The handful that are asked about every time, in the order someone reaches for them. Everything else is
228
+ * behind "All options", so the form opens as four controls rather than fifty.
229
+ */
230
+ const COMMON = ['--model', '--permission-mode', '--effort', '--agent'];
231
+
232
+ /** A field name that cannot collide with the form's own: `--add-dir` posts as `arg_add_dir`. */
233
+ const fieldName = (name) => `arg_${name.replace(/^--/, '').replace(/[^A-Za-z0-9]+/g, '_')}`;
234
+
235
+ /** The option a posted field name belongs to. */
236
+ function optionFor(options, field) {
237
+ return options.find((o) => fieldName(o.name) === field) || null;
238
+ }
239
+
240
+ /**
241
+ * A value on its way to a shell line. The line is handed to an outer shell, so a value may hold no
242
+ * quote, no backtick, no `$` and no newline — the same rule every command in this dashboard follows.
243
+ * A value that breaks it is REFUSED by name rather than dropped, because a flag that silently did
244
+ * nothing is worse than one that says why.
245
+ */
246
+ const SAFE_VALUE = /^[A-Za-z0-9 @=+.,:/_-]*$/;
247
+
248
+ /**
249
+ * The flags one submitted form asks for, as an array of shell words, or an error naming the field.
250
+ * Every flag has to be one the CLI printed, and every value has to fit the shape that flag documented.
251
+ */
252
+ function flagsFromForm(options, form) {
253
+ const words = [];
254
+ const problems = [];
255
+ for (const opt of options) {
256
+ const raw = form[fieldName(opt.name)];
257
+ if (raw === undefined) continue;
258
+ const value = String(Array.isArray(raw) ? raw[raw.length - 1] : raw).trim();
259
+ if (!value) continue;
260
+ if (opt.takes === 'none') {
261
+ // A checkbox posts its value only when it is ticked.
262
+ words.push(opt.name);
263
+ continue;
264
+ }
265
+ if (opt.choices.length && !opt.choices.includes(value)) {
266
+ problems.push(`${opt.name} does not take “${value}”`);
267
+ continue;
268
+ }
269
+ if (!SAFE_VALUE.test(value)) {
270
+ problems.push(`${opt.name} may hold only letters, digits and @ = + . , : / _ -`);
271
+ continue;
272
+ }
273
+ words.push(opt.name, ...(opt.repeatable ? value.split(/\s+/) : [value]));
274
+ }
275
+ return { words, problems };
276
+ }
277
+
278
+ module.exports = {
279
+ installedAgents,
280
+ parseOptions, shape, usableOnAScreen, WITHHELD, PRINT_ONLY, COMMON,
281
+ claudeOptions, forget, fieldName, optionFor, flagsFromForm, SAFE_VALUE,
282
+ };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The Claude Code sessions a directory already has, so one can be RESUMED rather than started over.
3
+ *
4
+ * A screen session that dies — the machine reboots, the terminal closes, `screen -wipe` takes a dead
5
+ * one — does not take the conversation with it: the Claude Code CLI keeps every session's transcript
6
+ * under `~/.claude/projects/<the directory, slashes turned into dashes>/<session id>.jsonl`. Resuming
7
+ * by id is `claude --resume <id>` from that same directory.
8
+ *
9
+ * By ID, deliberately, and not `--continue`: continue takes the most recent session for the directory,
10
+ * which after a day of parallel work is often not the one meant. The id is exact.
11
+ *
12
+ * What is on disk is two different things, and only one of them can be resumed:
13
+ * <id>.jsonl the transcript. This is what --resume reads, so this is what makes a session resumable.
14
+ * <id>/ that session's tool results and subagents. It exists on its own for a session whose
15
+ * transcript has not been written — and `claude --resume` on such an id answers
16
+ * "No conversation found with session ID", which is exactly what happened when this
17
+ * listed them: the screen started and died in the same second.
18
+ * So `resumable` is the transcript, and a directory-only id is reported rather than offered.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const os = require('os');
24
+
25
+ const PROJECTS = path.join(os.homedir(), '.claude', 'projects');
26
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
+
28
+ /** Is this a session id we would hand to `claude --resume`? */
29
+ const isSessionId = (v) => UUID_RE.test(String(v || ''));
30
+
31
+ /**
32
+ * The CLI's own name for a directory: the absolute path with every slash turned into a dash. So
33
+ * /home/rajab/workspace/dev/ha1x00b1 is -home-rajab-workspace-dev-ha1x00b1.
34
+ */
35
+ function projectSlug(dir) {
36
+ return String(path.resolve(dir)).replace(/\//g, '-');
37
+ }
38
+
39
+ function projectDirFor(dir) {
40
+ return path.join(PROJECTS, projectSlug(dir));
41
+ }
42
+
43
+ /**
44
+ * The first thing the person typed, as the session's title. Read from the head of the file rather than
45
+ * all of it: a long transcript is tens of megabytes and this runs while a form is being drawn.
46
+ */
47
+ function titleOf(file) {
48
+ let head = '';
49
+ try {
50
+ const fd = fs.openSync(file, 'r');
51
+ const buf = Buffer.alloc(256 * 1024);
52
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
53
+ fs.closeSync(fd);
54
+ head = buf.slice(0, n).toString('utf8');
55
+ } catch (_) {
56
+ return '';
57
+ }
58
+ for (const line of head.split('\n')) {
59
+ if (!line.startsWith('{') || !line.includes('"type":"user"')) continue;
60
+ try {
61
+ const d = JSON.parse(line);
62
+ const content = (d.message || {}).content;
63
+ const text = typeof content === 'string'
64
+ ? content
65
+ : Array.isArray(content)
66
+ ? content.filter((p) => p && p.type === 'text').map((p) => p.text).join(' ')
67
+ : '';
68
+ const clean = String(text).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
69
+ if (clean) return clean.slice(0, 90);
70
+ } catch (_) { /* a half-written line at the head is not a title */ }
71
+ }
72
+ return '';
73
+ }
74
+
75
+ /**
76
+ * Every resumable session for one directory, newest first.
77
+ *
78
+ * `at` is when it was last written — which is what "the one I was just in" means — and `title` is the
79
+ * first thing typed in it, so two sessions in one project can be told apart without opening either.
80
+ */
81
+ function sessionsFor(dir, { limit = 25, resumableOnly = false } = {}) {
82
+ const projectDir = projectDirFor(dir);
83
+ let names = [];
84
+ try { names = fs.readdirSync(projectDir); } catch (_) { return []; }
85
+
86
+ const found = new Map();
87
+ const note = (id, file, isTranscript) => {
88
+ let st;
89
+ try { st = fs.statSync(file); } catch (_) { return; }
90
+ const prev = found.get(id);
91
+ const entry = prev || { id, at: 0, size: 0, title: '', transcript: false, resumable: false };
92
+ entry.at = Math.max(entry.at, st.mtimeMs);
93
+ if (isTranscript) {
94
+ entry.size = st.size;
95
+ entry.transcript = true;
96
+ // An empty transcript is a session that never got going; --resume finds nothing in it either.
97
+ entry.resumable = st.size > 0;
98
+ entry.title = entry.title || titleOf(file);
99
+ }
100
+ found.set(id, entry);
101
+ };
102
+
103
+ for (const name of names) {
104
+ if (name === 'memory') continue;
105
+ if (name.endsWith('.jsonl')) {
106
+ const id = name.slice(0, -'.jsonl'.length);
107
+ if (isSessionId(id)) note(id, path.join(projectDir, name), true);
108
+ } else if (isSessionId(name)) {
109
+ // A session whose transcript has not been written yet — still running, or cut off by a reboot.
110
+ note(name, path.join(projectDir, name), false);
111
+ }
112
+ }
113
+
114
+ return [...found.values()]
115
+ .filter((x) => !resumableOnly || x.resumable)
116
+ .sort((a, b) => b.at - a.at)
117
+ .slice(0, limit);
118
+ }
119
+
120
+ /** Does this directory have any session to resume? Cheap enough to ask while drawing a list. */
121
+ function hasSessions(dir) {
122
+ try {
123
+ return fs.readdirSync(projectDirFor(dir)).some((n) => isSessionId(n) || (n.endsWith('.jsonl') && isSessionId(n.slice(0, -6))));
124
+ } catch (_) {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ module.exports = { PROJECTS, isSessionId, projectSlug, projectDirFor, sessionsFor, hasSessions, titleOf };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * What the assistant is told when it writes or changes a `cmd-*.sh`.
3
+ *
4
+ * A workspace command is not a free-form script. It has a fixed shape — locate the tooling from its
5
+ * own path, source the bootstrap, parse the workspace's settings file, declare its arguments through
6
+ * the argparse heredoc, then call one function — and a set of rules that are not negotiable, because
7
+ * breaking them breaks the machine rather than the script: DDEV only, no `$databases` block, no
8
+ * masked failures. An agent that has not been told these writes plausible bash that this workspace
9
+ * cannot use.
10
+ *
11
+ * So the prompt carries the conventions, and it points at the real files rather than describing
12
+ * them: the surest way to write a command like the others is to read the others. The agent is given
13
+ * read tools ONLY — it never writes the file itself. Its answer is captured, checked with `bash -n`,
14
+ * and only then does it become a command.
15
+ *
16
+ * One module, two callers: creating a command with a purpose, and asking for a change to one that
17
+ * exists. They share every rule; only the last paragraph differs.
18
+ */
19
+
20
+ const path = require('path');
21
+
22
+ const { ROOT } = require('./workspaces');
23
+
24
+ // The conventions, in the order they appear in a real command. Written as instructions rather than
25
+ // as prose about instructions, because this text is read by an agent and not by a person.
26
+ function conventions(workspace) {
27
+ return `THE SHAPE OF A WORKSPACE COMMAND — follow it exactly:
28
+
29
+ 1. \`#!/usr/bin/env bash\` and, for a builder, a \`# workspace-name: <Human Name>\` header on line 3.
30
+ The smoke test at core/scripts/tests/cmd-smoke-test.sh enforces the header on cmd-*-project.sh.
31
+ 2. The bootstrap, verbatim, so a fresh clone needs no setup and no environment variable:
32
+ WORKSPACE_SCRIPTS="\${WORKSPACE_SCRIPTS:-$(cd "$(dirname "\${BASH_SOURCE[0]}")/../core/scripts" && pwd)}";
33
+ source \${WORKSPACE_SCRIPTS}/bootstrap.sh || exit 1 ;
34
+ 3. \`eval $(parse_yaml \${WORKSPACE_CONFIG}/workspace.${workspace}.settings.yml);\` — this is where
35
+ \`doc_name\` comes from, and every function below uses it.
36
+ 4. \`ARGPARSE_DESCRIPTION="…"\` then \`source \${WORKSPACE_SCRIPTS}/args/arg-<name>.sh || exit 1 ;\`
37
+ 5. \`shift $#;\` then ONE call to the function that does the work.
38
+
39
+ The logic goes in \`core/scripts/functions/fun-<name>.sh\`, sourced from
40
+ core/scripts/bootstrap-functions.sh. A cmd-*.sh that contains its own logic is the wrong shape: the
41
+ command is the entry point, the function is the program.
42
+
43
+ ARGUMENTS are declared in an argparse heredoc in core/scripts/args/arg-<name>.sh. Optional string
44
+ flags default to the string \`_none_\` and are tested \`[ ! "\${FLAG}" == '_none_' ]\`. Booleans use
45
+ \`action='store_true'\` and arrive as \`yes\` or empty. Do NOT use \`nargs='+'\`: this argparse hands
46
+ arrays back as a scalar and the extra values are lost silently.
47
+
48
+ HARD RULES — these are not style:
49
+ * DDEV only. \`ddev composer\`, \`ddev drush\`, \`ddev exec\`, \`ddev export-db\`. Never raw composer,
50
+ drush, npm or mysql on the host.
51
+ * Never write a \`$databases\` block into settings.php — DDEV's settings.ddev.php owns it.
52
+ * \`ddev start\` accepts \`-y\`; \`ddev stop\` does NOT.
53
+ * Never mask a failure with \`|| true\` on a command whose result matters, and never \`rm -rf\` a
54
+ path built from an unvalidated argument.
55
+ * Anything the dashboard runs has no one to answer a prompt, so never wait for input.
56
+ * A command that CREATES something for its own checking removes it again, or reports it and stops —
57
+ never leaves a scratch project behind. Remove one with cmd-tools-remove.sh <project>, or
58
+ ddev delete -y -O inside it; never rm -rf. The folder is not the project: deleting it alone
59
+ strands the DDEV registry entry, the database volume, the snapshots volume and the built images.
60
+
61
+ READ THESE FIRST with your tools — they are the pattern, and copying it is the point:
62
+ ${path.join(ROOT, workspace)}/cmd-*.sh (the commands this workspace already has)
63
+ ${path.join(ROOT, 'core', 'scripts', 'functions')}/ (every fun-*.sh — the functions that already exist)
64
+ ${path.join(ROOT, 'core', 'scripts', 'args')}/ (every arg-*.sh — the argparse blocks that already exist)
65
+ ${path.join(ROOT, 'core', 'scripts', 'bootstrap.sh')} (what is exported for you)
66
+
67
+ REUSE BEFORE INVENTING. List both directories and read what is close to the task: the best command
68
+ is usually ONE call to a function somebody already wrote, behind an arg file that already fits
69
+ (arg-distribution, arg-webship-site-template, arg-checks, arg-testing …). Write new logic inline
70
+ only when no existing function covers it — and then still declare the arguments through an existing
71
+ arg file when one matches, so the same flags mean the same things across the workspace.`;
72
+ }
73
+
74
+ /** Writing a new command's body into the scaffold that already exists. */
75
+ function commandAuthorPrompt({ workspace, file, purpose, scaffoldPath }) {
76
+ return `You are writing a command for the Webship Workspace, a DDEV-only harness rooted at ${ROOT}.
77
+
78
+ The file ${scaffoldPath} has just been scaffolded and already carries the correct bootstrap chain.
79
+ Read it first. Rewrite it so it does what is asked below, keeping every line of the bootstrap exactly
80
+ as it is.
81
+
82
+ WHAT IT SHOULD DO:
83
+ ${purpose}
84
+
85
+ ${conventions(workspace)}
86
+
87
+ Also state, in a comment block at the top of the file, what this command is for and what it will NOT
88
+ do — the destructive edges it deliberately leaves alone. That comment is the first thing the next
89
+ person reads.
90
+
91
+ If the work needs a new function file or a new arg file, say so in a comment at the point it would be
92
+ called, naming the exact path — you cannot create it, and a command that calls a function nobody
93
+ wrote fails loudly on the first run, which is better than a command that silently does half of it.
94
+
95
+ Output ONLY the complete contents of ${file}. No commentary, no code fences, no explanation.`;
96
+ }
97
+
98
+ /** Changing a command that already exists, to an instruction. */
99
+ function commandEditPrompt({ workspace, file, instruction, fullPath }) {
100
+ return `You are changing a command in the Webship Workspace, a DDEV-only harness rooted at ${ROOT}.
101
+
102
+ The command is ${fullPath}. Read it first, and read the commands beside it.
103
+
104
+ WHAT TO CHANGE:
105
+ ${instruction}
106
+
107
+ Change only what that asks for. Everything else — the bootstrap chain, the argparse source line, the
108
+ comments explaining decisions somebody already made — stays exactly as it is. If the instruction
109
+ cannot be carried out without breaking one of the rules below, do not carry it out: leave the file as
110
+ it is and put a single comment at the top saying which rule stopped you.
111
+
112
+ ${conventions(workspace)}
113
+
114
+ Output ONLY the complete new contents of ${file}. No commentary, no code fences, no explanation.`;
115
+ }
116
+
117
+ module.exports = { commandAuthorPrompt, commandEditPrompt, conventions };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A command the dashboard shows rather than runs, with a copy button on each line.
3
+ *
4
+ * `claude mcp add` writes ~/.claude.json — the CLI configuration of the person at the keyboard, not
5
+ * of this web request. The Upsun pulls need cloud credentials this container does not have, and
6
+ * they replace local data. Both are worth reading before they happen.
7
+ */
8
+
9
+ const { esc } = require('./html');
10
+
11
+ const commandBlock = (title, lines, note) => `<div class="msg assistant">
12
+ <strong>${esc(title)}</strong>
13
+ ${lines.map((l) => `<div class="uk-margin-small-top uk-flex uk-flex-middle" style="gap:.5rem">
14
+ <code style="flex:1;overflow-x:auto;white-space:pre">${esc(l)}</code>
15
+ <button class="uk-button uk-button-default uk-button-small copy-cmd" data-copy="${esc(l)}"><span uk-icon="icon: copy; ratio: .7"></span> Copy</button>
16
+ </div>`).join('')}
17
+ <p class="uk-text-meta uk-margin-small-top">${note}</p>
18
+ </div>`;
19
+
20
+ module.exports = { commandBlock };