@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/changelog.js ADDED
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Reading the CHANGELOG of anything in the tree that keeps one.
3
+ *
4
+ * Not only `products/`. A changelog turns up wherever work does — a built site in `dev/` carries
5
+ * the one Drupal CMS ships, a module checkout in `modules/` carries its own, a recipe in
6
+ * `recipes/` carries the one its maintainer writes. 68 of them across the registered workspaces
7
+ * today, and the number is not the point: the point is that somebody adding a checkout tomorrow
8
+ * gets it read without anybody wiring it up.
9
+ *
10
+ * Answering "what shipped in Haven last week" meant opening a file and scrolling, which is
11
+ * why nobody did it — and a changelog nobody reads is a changelog that stops being written.
12
+ *
13
+ * So the files are parsed into releases and entries and rendered as a page. What this module does
14
+ * NOT do is rewrite anything: a changelog is the product's own record, edited in its own repository
15
+ * by whoever cut the release. This only reads.
16
+ *
17
+ * Three formats are in the tree and all three are parsed, because the alternative is a reader that
18
+ * works for the products somebody remembered:
19
+ *
20
+ * 1. **Keep a Changelog** — `## [1.0.3] - 2026-09-14`, with `### Added` / `### Fixed` sections.
21
+ * Most of the recipes and the site templates.
22
+ * 2. **The Webship suite's own** — `# 11.0.0` as a top-level heading, with prose sections under it
23
+ * and entries prefixed `feat:` / `fix:` / `task:`.
24
+ * 3. **The old Drupal .txt** — `Menu Position 7.x-1.1, 2012-02-22` underlined with dashes. Two
25
+ * products still carry one, and they are the oldest things here.
26
+ *
27
+ * A release nobody can date is still a release, so a missing date is left empty rather than guessed.
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+
35
+ const { ROOT, loadWorkspaces, isValidWorkspace, workspaceDir } = require('./workspaces');
36
+
37
+ /** The file names a product might keep its changelog in, in the order they are preferred. */
38
+ const CHANGELOG_NAMES = ['CHANGELOG.md', 'CHANGELOG.txt', 'changelog.md', 'changelog.txt'];
39
+
40
+ /** Nothing bigger than this is parsed: a changelog is a document, not a log file. */
41
+ const MAX_BYTES = 512 * 1024;
42
+
43
+ /**
44
+ * The kinds of change, and how each is shown.
45
+ *
46
+ * Keep a Changelog names six; our own entries are prefixed in the Drupal commit-type style
47
+ * (`feat:`, `fix:`, `task:`), and drupal.org adds a few more. Both vocabularies map onto the same
48
+ * small set, because a reader should not have to know which convention a given product follows.
49
+ *
50
+ * `tone` is a UIkit label modifier, so the badge is coloured by the component rather than by a rule
51
+ * of ours — success, warning, danger are UIkit's own.
52
+ */
53
+ const KINDS = {
54
+ added: { label: 'Added', tone: 'success' },
55
+ feat: { label: 'Added', tone: 'success' },
56
+ feature: { label: 'Added', tone: 'success' },
57
+ fixed: { label: 'Fixed', tone: 'danger' },
58
+ fix: { label: 'Fixed', tone: 'danger' },
59
+ bug: { label: 'Fixed', tone: 'danger' },
60
+ changed: { label: 'Changed', tone: '' },
61
+ change: { label: 'Changed', tone: '' },
62
+ update: { label: 'Changed', tone: '' },
63
+ task: { label: 'Task', tone: '' },
64
+ chore: { label: 'Task', tone: '' },
65
+ docs: { label: 'Docs', tone: '' },
66
+ removed: { label: 'Removed', tone: 'warning' },
67
+ remove: { label: 'Removed', tone: 'warning' },
68
+ deprecated: { label: 'Deprecated', tone: 'warning' },
69
+ security: { label: 'Security', tone: 'danger' },
70
+ };
71
+
72
+ /** What a section heading or an entry prefix means, or null when it means nothing in particular. */
73
+ function kindOf(word) {
74
+ if (!word) return null;
75
+ const key = String(word).trim().toLowerCase().replace(/[^a-z]/g, '');
76
+ return KINDS[key] || null;
77
+ }
78
+
79
+ /**
80
+ * Everything in the tree that keeps a changelog, with the file it keeps it in.
81
+ *
82
+ * Every registered workspace is walked, and in each one: the folder's own changelog, if it has one,
83
+ * and then every item inside it. Discovery rather than a list, for the same reason the builders are
84
+ * found by filename — a checkout added tomorrow should appear without anybody editing this file.
85
+ *
86
+ * Memoised for two seconds, the same window `loadWorkspaces` uses. 68 files is a cheap walk, but
87
+ * the index parses every one of them and a page can ask for the list more than once.
88
+ */
89
+ let _cache = null;
90
+ let _cacheAt = 0;
91
+
92
+ function listChangelogs() {
93
+ const now = Date.now();
94
+ if (_cache && now - _cacheAt < 2000) return _cache;
95
+
96
+ const out = [];
97
+ const fileIn = (dir) => {
98
+ for (const name of CHANGELOG_NAMES) {
99
+ const full = path.join(dir, name);
100
+ let stat;
101
+ try { stat = fs.statSync(full); } catch (_) { continue; }
102
+ if (!stat.isFile() || !stat.size || stat.size > MAX_BYTES) continue;
103
+ return { file: name, path: full, bytes: stat.size, changed: stat.mtimeMs };
104
+ }
105
+ return null;
106
+ };
107
+
108
+ for (const ws of Object.values(loadWorkspaces())) {
109
+ if (!ws.dir) continue;
110
+ // The workspace folder's own, if it keeps one.
111
+ const own = fileIn(ws.dir);
112
+ if (own) out.push({ workspace: ws.key, item: '', label: ws.label || ws.key, ...own });
113
+
114
+ let entries = [];
115
+ try {
116
+ entries = fs.readdirSync(ws.dir, { withFileTypes: true })
117
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
118
+ .map((e) => e.name)
119
+ .sort();
120
+ } catch (_) { continue; }
121
+
122
+ for (const item of entries) {
123
+ const found = fileIn(path.join(ws.dir, item));
124
+ if (found) out.push({ workspace: ws.key, item, label: item, ...found });
125
+ }
126
+ }
127
+
128
+ _cache = out;
129
+ _cacheAt = now;
130
+ return out;
131
+ }
132
+
133
+ /** Forget the walk — for a test that writes a changelog and expects to see it. */
134
+ function invalidate() { _cache = null; _cacheAt = 0; }
135
+
136
+ /** An item name that cannot climb out of its workspace. The folder must exist to be used anyway. */
137
+ const isItem = (name) => name === '' || /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(String(name || ''));
138
+
139
+ /** The changelog of one item, or null when it keeps none. */
140
+ function changelogFor(workspace, item = '') {
141
+ if (!isValidWorkspace(workspace) || !isItem(item)) return null;
142
+ return listChangelogs().find((c) => c.workspace === workspace && c.item === (item || '')) || null;
143
+ }
144
+
145
+ /* ---------------- parsing ---------------- */
146
+
147
+ // `## [1.0.3] - 2026-09-14`, `## [Unreleased]`, `## 1.0.3 — 2026-09-14`, `# 11.0.0`.
148
+ //
149
+ // The separator before the date must be SURROUNDED by whitespace. Without that, `# 11.0.0-rc1`
150
+ // parses as version `11.0.0` with `rc1` where the date goes — and the file then shows three
151
+ // releases called 11.0.0, which is how this was found.
152
+ const MD_HEADING = /^(#{1,4})\s+\[?([^\]\n]+?)\]?(?:\s+[-—–]\s+(.+?))?\s*$/;
153
+ // `Menu Position 7.x-1.1, 2012-02-22` followed by a line of dashes.
154
+ const TXT_HEADING = /^(.+?),\s*([0-9x]{4}-[0-9x]{2}-[0-9x]{2}|\d{2}-\d{2}-\d{4})\s*(\(.*\))?\s*$/;
155
+ // A date on its own, in any of the shapes these files use.
156
+ const DATE_ONLY = /^(\d{4}-\d{2}-\d{2}|\d{2}-\d{2}-\d{4}|[0-9x]{4}-[0-9x]{2}-[0-9x]{2})$/;
157
+ // `- feat: [#3622833](…) Add a Newsletter Canvas page`
158
+ const ENTRY_PREFIX = /^([A-Za-z]+)\s*:\s*(.+)$/s;
159
+
160
+ /** Is this heading text a version rather than prose? */
161
+ function looksLikeVersion(text) {
162
+ const t = String(text).trim();
163
+ if (/^unreleased$/i.test(t)) return true;
164
+ // 1.0.3, 11.0.0-rc1, 8.x-1.1, v2.3
165
+ return /^v?\d/.test(t) && /[\d.x]/.test(t) && t.length < 40 && !/\s{2}/.test(t);
166
+ }
167
+
168
+ /**
169
+ * One changelog, as releases.
170
+ *
171
+ * Entries keep their Markdown: the page renders it, so a `[#3622833](https://…)` stays a link and
172
+ * `` `config/optional` `` stays code. Stripping it here would mean the reader shows less than the
173
+ * file does, which is the one thing a reader must not do.
174
+ */
175
+ function parseChangelog(text) {
176
+ const lines = String(text).replace(/\r\n?/g, '\n').split('\n');
177
+ const releases = [];
178
+ let current = null;
179
+ let section = null; // the `### Added` a bullet sits under
180
+ let group = null; // the `#### Website Starter 1.0.1` it belongs to
181
+ let pending = null; // an entry being continued on the next line
182
+
183
+ const flush = () => {
184
+ if (pending && current) current.entries.push(pending);
185
+ pending = null;
186
+ };
187
+ const open = (version, date) => {
188
+ flush();
189
+ section = null;
190
+ group = null;
191
+ current = { version: String(version).trim(), date: (date || '').trim(), entries: [], notes: [] };
192
+ releases.push(current);
193
+ };
194
+
195
+ for (let i = 0; i < lines.length; i++) {
196
+ const raw = lines[i];
197
+ const line = raw.trim();
198
+
199
+ // The old .txt shape: a title line, underlined with dashes.
200
+ const under = lines[i + 1] && /^-{3,}$/.test(lines[i + 1].trim());
201
+ if (under) {
202
+ const m = TXT_HEADING.exec(line);
203
+ if (m) { open(m[1], m[2]); i++; continue; }
204
+ if (line) { open(line, ''); i++; continue; }
205
+ }
206
+
207
+ const h = MD_HEADING.exec(line);
208
+ if (h) {
209
+ const [, hashes, title, rest] = h;
210
+ if (looksLikeVersion(title)) {
211
+ // `## [1.0.3] - 2026-09-14` — the date is what follows the dash, when it is a date.
212
+ const date = rest && DATE_ONLY.test(rest.trim()) ? rest.trim() : '';
213
+ // A `## 1.0.3 - some words` heading keeps its version and simply has no date, rather than
214
+ // showing "some words" where a date belongs.
215
+ open(title, date);
216
+ continue;
217
+ }
218
+ // `#### Website Starter 1.0.1` inside a release: the package a bullet belongs to. The Webship
219
+ // suite writes one release as a stack of per-package sections, and 239 entries in a flat list
220
+ // is a list nobody reads — the group is what makes it navigable.
221
+ if (current && hashes.length === 4) {
222
+ flush();
223
+ group = title.trim();
224
+ section = null;
225
+ continue;
226
+ }
227
+ // `### Added` under a release: the kind every bullet below it carries.
228
+ if (current && hashes.length === 3) {
229
+ flush();
230
+ const k = kindOf(title);
231
+ if (k) { section = k; } else { section = null; group = title.trim(); }
232
+ continue;
233
+ }
234
+ // Any other heading before the first release is the file's own preamble.
235
+ continue;
236
+ }
237
+
238
+ if (!current) continue;
239
+
240
+ const bullet = /^[-*+]\s+(.*)$/.exec(line);
241
+ if (bullet) {
242
+ flush();
243
+ let body = bullet[1];
244
+ let kind = section;
245
+ const pre = ENTRY_PREFIX.exec(body);
246
+ // A `feat:` prefix wins over the section it sits under: it is the more specific statement,
247
+ // and the two disagree in practice — a `fix:` often lives under `### Added`.
248
+ if (pre && kindOf(pre[1])) { kind = kindOf(pre[1]); body = pre[2]; }
249
+ pending = { kind: kind ? kind.label : '', tone: kind ? kind.tone : '', group: group || '', text: body.trim() };
250
+ continue;
251
+ }
252
+
253
+ // A wrapped bullet: these files hard-wrap at 100 columns, so most entries are several lines.
254
+ if (pending && line) { pending.text += ` ${line}`; continue; }
255
+ if (pending && !line) { flush(); continue; }
256
+
257
+ // Prose inside a release that is not a bullet — Webship's "At a glance" paragraphs.
258
+ if (line) current.notes.push(line);
259
+ }
260
+ flush();
261
+
262
+ return releases.filter((r) => r.entries.length || r.notes.length);
263
+ }
264
+
265
+ /** An item's releases, parsed. An unreadable file is no releases rather than an error page. */
266
+ function releasesFor(workspace, item = '') {
267
+ const found = changelogFor(workspace, item);
268
+ if (!found) return null;
269
+ let text = '';
270
+ try { text = fs.readFileSync(found.path, 'utf8'); } catch (_) { return { ...found, releases: [] }; }
271
+ return { ...found, releases: parseChangelog(text) };
272
+ }
273
+
274
+ /**
275
+ * Every changelog in the tree, summarised for the index: how many releases, and the newest one.
276
+ *
277
+ * The newest release is the FIRST in the file, not the highest version number: every one of these
278
+ * files is written newest-first, and comparing versions across three numbering schemes (1.0.3,
279
+ * 8.x-1.1, 11.0.0-rc1) would be a guess dressed as a sort.
280
+ */
281
+ function changelogIndex() {
282
+ return listChangelogs().map((c) => {
283
+ let releases = [];
284
+ try { releases = parseChangelog(fs.readFileSync(c.path, 'utf8')); } catch (_) { /* unreadable */ }
285
+ const newest = releases.find((r) => !/^unreleased$/i.test(r.version)) || releases[0] || null;
286
+ const unreleased = releases.find((r) => /^unreleased$/i.test(r.version));
287
+ return {
288
+ ...c,
289
+ releases: releases.length,
290
+ entries: releases.reduce((n, r) => n + r.entries.length, 0),
291
+ latest: newest ? newest.version : '',
292
+ latestDate: newest ? newest.date : '',
293
+ unreleasedEntries: unreleased ? unreleased.entries.length : 0,
294
+ };
295
+ });
296
+ }
297
+
298
+ module.exports = {
299
+ CHANGELOG_NAMES, KINDS,
300
+ listChangelogs, changelogFor, releasesFor, changelogIndex, invalidate,
301
+ parseChangelog, kindOf, looksLikeVersion,
302
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The assistant's conversation, kept so it survives the next page.
3
+ *
4
+ * Every ask used to be a one-shot `claude -p --no-session-persistence`: the assistant remembered
5
+ * nothing between turns, and deep-chat's transcript lived only in the page that drew it — so moving
6
+ * from /dev to /test, or following the assistant's own NAVIGATE, threw the conversation away. On a
7
+ * dashboard whose whole point is moving between workspaces, that is most of the conversation.
8
+ *
9
+ * Two halves, and both are needed:
10
+ *
11
+ * - the TRANSCRIPT, so the next page can draw what was said (deep-chat's `history`);
12
+ * - the claude SESSION id, so the assistant itself still remembers — a redrawn transcript in
13
+ * front of an agent with no memory is worse than none, because it reads as memory.
14
+ *
15
+ * The id travels in a COOKIE, not localStorage, and that is the crux: the dashboard spans
16
+ * <workspace>.<hub> subdomains and localStorage is per-origin, so a thread kept there would be a
17
+ * different thread on every workspace page — exactly the bug this fixes. The lists' page-size cookie
18
+ * already had to be scoped to the hub domain for the same reason.
19
+ *
20
+ * The file is this machine's chat log. It is gitignored, never synced, and holds whatever was typed,
21
+ * so it is treated like the rest of the runtime state: capped, pruned, and never committed.
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const crypto = require('crypto');
27
+
28
+ const { hubDomain } = require('./workspaces');
29
+
30
+ const DIR = path.join(__dirname, '.chats');
31
+
32
+ // What one thread keeps. A long conversation is trimmed from the front rather than refused: the
33
+ // newest turns are the ones the next page has to draw, and claude's own session holds the rest.
34
+ const MAX_TURNS = 60;
35
+ // One turn's rendered HTML can be a whole report; past this it is stored as its text alone.
36
+ const MAX_TURN_CHARS = 20000;
37
+ // A thread nobody has touched in a month is not a conversation any more.
38
+ const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
39
+
40
+ const isId = (id) => /^[a-f0-9]{32}$/.test(String(id || ''));
41
+ const newId = () => crypto.randomBytes(16).toString('hex');
42
+ const fileFor = (id) => path.join(DIR, `${id}.json`);
43
+
44
+ /** The thread id this request carries, or null. Only our own shape is accepted — the id names a file. */
45
+ function threadIdFrom(req) {
46
+ const raw = String((req && req.headers && req.headers.cookie) || '');
47
+ const hit = raw.split(';').map((c) => c.trim().split('=')).find((p) => p[0] === 'ws-chat');
48
+ return hit && isId(hit[1]) ? hit[1] : null;
49
+ }
50
+
51
+ /**
52
+ * The Set-Cookie that keeps one thread across the workspace subdomains. No Max-Age: the conversation
53
+ * belongs to this browsing session, and a chat log that outlives the browser being closed is a
54
+ * surprise, not a feature. The store prunes the file either way.
55
+ */
56
+ function threadCookie(id) {
57
+ return `ws-chat=${id}; Path=/; Domain=.${hubDomain()}; SameSite=Lax`;
58
+ }
59
+
60
+ function readThread(id) {
61
+ if (!isId(id)) return null;
62
+ try {
63
+ const t = JSON.parse(fs.readFileSync(fileFor(id), 'utf8'));
64
+ return t && Array.isArray(t.turns) ? t : null;
65
+ } catch (_) {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /** The thread this request is in, created only when something is written to it. */
71
+ function loadThread(req) {
72
+ const id = threadIdFrom(req);
73
+ return (id && readThread(id)) || { id: id || newId(), sessionId: null, turns: [], updated: 0 };
74
+ }
75
+
76
+ function writeThread(t) {
77
+ const thread = { ...t, updated: Date.now(), turns: t.turns.slice(-MAX_TURNS) };
78
+ try {
79
+ fs.mkdirSync(DIR, { recursive: true });
80
+ // 0600: it is a chat log. The dashboard runs as one user, but the file says so on disk too.
81
+ fs.writeFileSync(fileFor(thread.id), JSON.stringify(thread), { mode: 0o600 });
82
+ } catch (_) { /* a thread that cannot be written is a conversation without memory, not an error */ }
83
+ return thread;
84
+ }
85
+
86
+ /** One turn. `html` is what deep-chat draws for the assistant; the user's turn is plain text. */
87
+ function appendTurn(thread, turn) {
88
+ const t = { ...turn, at: Date.now() };
89
+ if (t.html && t.html.length > MAX_TURN_CHARS) t.html = null;
90
+ if (t.text && t.text.length > MAX_TURN_CHARS) t.text = `${t.text.slice(0, MAX_TURN_CHARS)}…`;
91
+ return writeThread({ ...thread, turns: [...thread.turns, t] });
92
+ }
93
+
94
+ /**
95
+ * The transcript as deep-chat's `history`. Its own shape: `role` is 'user' or 'ai', and a message
96
+ * carries either `text` or `html` — the assistant's replies are already rendered markdown, so they
97
+ * are handed back as html rather than re-escaped into text.
98
+ */
99
+ function historyFor(thread) {
100
+ return (thread.turns || [])
101
+ .map((t) => (t.role === 'ai'
102
+ ? (t.html ? { role: 'ai', html: t.html } : { role: 'ai', text: t.text || '' })
103
+ : { role: 'user', text: t.text || '' }))
104
+ .filter((m) => m.html || m.text);
105
+ }
106
+
107
+ /** Forget this conversation: the file, and with it the claude session it resumed. */
108
+ function clearThread(id) {
109
+ if (!isId(id)) return;
110
+ try { fs.unlinkSync(fileFor(id)); } catch (_) { /* already gone */ }
111
+ }
112
+
113
+ /**
114
+ * Drop threads nobody has touched in a month. Called when one is written rather than on a timer:
115
+ * there is no scheduler in this app, and the moment a thread is used is a fine moment to sweep.
116
+ */
117
+ function pruneThreads(now = Date.now()) {
118
+ let removed = 0;
119
+ let files = [];
120
+ try { files = fs.readdirSync(DIR); } catch (_) { return 0; }
121
+ for (const f of files) {
122
+ if (!/^[a-f0-9]{32}\.json$/.test(f)) continue;
123
+ try {
124
+ if (now - fs.statSync(path.join(DIR, f)).mtimeMs > MAX_AGE_MS) { fs.unlinkSync(path.join(DIR, f)); removed += 1; }
125
+ } catch (_) { /* raced with another write */ }
126
+ }
127
+ return removed;
128
+ }
129
+
130
+ module.exports = {
131
+ DIR, MAX_TURNS, MAX_AGE_MS,
132
+ isId, newId, threadIdFrom, threadCookie,
133
+ loadThread, readThread, writeThread, appendTurn, historyFor, clearThread, pruneThreads,
134
+ };
package/checks.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * What the Checks button knows before you press it.
3
+ *
4
+ * Two questions, both answered without running anything: how many checks this project actually
5
+ * declares, and how the last run went. The first comes from the SAME resolver the command uses
6
+ * (core/scripts/libs/checks_plan.js), so the caption and the command can never disagree about what a
7
+ * package offers. The second comes from the marker the command writes outside the package.
8
+ *
9
+ * Memoised for two seconds: a workspace page renders thirty rows, and each one would otherwise read
10
+ * a package.json and stat a directory.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const { ROOT } = require('./workspaces');
17
+
18
+ // The resolver lives with the shell tooling, not with the dashboard — it is the shell verb's own
19
+ // library, and the dashboard is the second caller rather than the owner.
20
+ const { resolvePlan } = require(path.join(ROOT, 'core', 'scripts', 'libs', 'checks_plan'));
21
+
22
+ const STATE_DIR = path.join(ROOT, 'core', 'state', 'checks');
23
+
24
+ let _cache = new Map();
25
+ let _cacheAt = 0;
26
+
27
+ const fresh = () => {
28
+ const now = Date.now();
29
+ if (now - _cacheAt > 2000) { _cache = new Map(); _cacheAt = now; }
30
+ return _cache;
31
+ };
32
+
33
+ /**
34
+ * How many checks a project declares, and how many of those are runnable right now. A package with
35
+ * six declared checks and no node_modules is worth saying out loud: the button is not broken, the
36
+ * dependencies are missing.
37
+ */
38
+ function checksDeclaredOf(dir, project) {
39
+ const cache = fresh();
40
+ const key = `d:${dir}/${project}`;
41
+ if (cache.has(key)) return cache.get(key);
42
+ let out = { declared: 0, ready: 0, blocked: 0, hasPackageJson: false };
43
+ try {
44
+ const plan = resolvePlan(path.join(dir, project));
45
+ const counted = plan.rows.filter((r) => r.status !== 'none');
46
+ out = {
47
+ declared: counted.length,
48
+ ready: counted.filter((r) => r.status === 'ready').length,
49
+ blocked: counted.filter((r) => ['deps-missing', 'unresolved-runner'].includes(r.status)).length,
50
+ hasPackageJson: plan.hasPackageJson,
51
+ };
52
+ } catch (_) { /* an unreadable package.json is a project with nothing to run */ }
53
+ cache.set(key, out);
54
+ return out;
55
+ }
56
+
57
+ /**
58
+ * The rows themselves, for the menu that lists them: which canonical check, which script the package
59
+ * declared for it, and whether it can run right now. The menu shows the ALIAS beside the real script
60
+ * name — `js → lint:js` — because the whole point of the verb is that a package's own vocabulary is
61
+ * what runs, and seeing the pair is how you know the mapping is right.
62
+ */
63
+ function checksRowsOf(dir, project) {
64
+ const cache = fresh();
65
+ const key = `r:${dir}/${project}`;
66
+ if (cache.has(key)) return cache.get(key);
67
+ let rows = [];
68
+ try {
69
+ rows = resolvePlan(path.join(dir, project)).rows.filter((r) => r.status !== 'none');
70
+ } catch (_) { /* nothing declared */ }
71
+ cache.set(key, rows);
72
+ return rows;
73
+ }
74
+
75
+ /** The last run, from the marker the command wrote. Null when it has never been run. */
76
+ function checksStateOf(workspace, project) {
77
+ const cache = fresh();
78
+ const key = `s:${workspace}/${project}`;
79
+ if (cache.has(key)) return cache.get(key);
80
+ let out = null;
81
+ try {
82
+ out = JSON.parse(fs.readFileSync(path.join(STATE_DIR, workspace, `${project}.json`), 'utf8'));
83
+ } catch (_) { /* never run */ }
84
+ cache.set(key, out);
85
+ return out;
86
+ }
87
+
88
+ /**
89
+ * The word under the button. It answers "is there anything to look at yet" in one glance:
90
+ * a failure count when there is one, "passed" when the last run was clean, how many checks are
91
+ * declared when it has never run, and why nothing can run when that is the answer.
92
+ */
93
+ function checksCaption(workspace, dir, project) {
94
+ const state = checksStateOf(workspace, project);
95
+ if (state) {
96
+ if (state.failed) return `${state.failed} failed`;
97
+ if (state.ran) return `${state.ran} passed`;
98
+ }
99
+ const d = checksDeclaredOf(dir, project);
100
+ if (!d.declared) return 'none';
101
+ if (d.blocked && !d.ready) return 'deps missing';
102
+ return `${d.declared} declared`;
103
+ }
104
+
105
+ module.exports = { checksDeclaredOf, checksRowsOf, checksStateOf, checksCaption };