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

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 (307) 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/images-svg.js +33 -0
  16. package/actions/images.js +145 -0
  17. package/actions/navigation.js +190 -0
  18. package/actions/node.js +222 -0
  19. package/actions/notes.js +55 -0
  20. package/actions/plugins.js +36 -0
  21. package/actions/project-make.js +28 -0
  22. package/actions/rag.js +202 -0
  23. package/actions/row-actions.js +262 -0
  24. package/actions/screens.js +270 -0
  25. package/actions/testing.js +121 -0
  26. package/actions/video-edit.js +54 -0
  27. package/ai-inventory.js +182 -0
  28. package/ai-items.js +202 -0
  29. package/assistant-cast.js +176 -0
  30. package/assistant-suggestions.js +91 -0
  31. package/assistant.js +484 -0
  32. package/audio-voice.js +528 -0
  33. package/auth-google.js +260 -0
  34. package/build-ai.js +52 -0
  35. package/build-order-page.js +191 -0
  36. package/build-order.js +313 -0
  37. package/builder-args.js +117 -0
  38. package/changelog.js +302 -0
  39. package/chat-threads.js +134 -0
  40. package/checks.js +105 -0
  41. package/claude-args.js +282 -0
  42. package/claude-sessions.js +129 -0
  43. package/command-author.js +117 -0
  44. package/command-block.js +20 -0
  45. package/commands/defaults.yml +159 -0
  46. package/commands-page.js +460 -0
  47. package/commands.js +568 -0
  48. package/cucumber.js +43 -0
  49. package/ddev-addons-page.js +124 -0
  50. package/ddev-addons.js +154 -0
  51. package/ddev-custom-commands.js +96 -0
  52. package/ddev-domains.js +84 -0
  53. package/ddev-import.js +93 -0
  54. package/ddev-mail-page.js +167 -0
  55. package/ddev-mail.js +110 -0
  56. package/ddev-options.js +145 -0
  57. package/ddev-snapshots.js +126 -0
  58. package/ddev.js +145 -0
  59. package/docs/DEVELOPER-GUIDE.md +224 -0
  60. package/docs/README.md +252 -0
  61. package/docs/REFERENCE.md +684 -0
  62. package/docs/USER-GUIDE.md +697 -0
  63. package/docs/screenshots/assistant/avatar-board.png +0 -0
  64. package/docs/screenshots/assistant/avatar-coffee.png +0 -0
  65. package/docs/screenshots/assistant/avatar-colours.png +0 -0
  66. package/docs/screenshots/assistant/avatar-icons.png +0 -0
  67. package/docs/screenshots/assistant/avatar-moves.png +0 -0
  68. package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  69. package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
  70. package/docs/screenshots/assistant/avatar-status.png +0 -0
  71. package/docs/screenshots/create-card/create-card.png +0 -0
  72. package/docs/screenshots/create-card/start-ai.png +0 -0
  73. package/docs/screenshots/guide-clone-address.png +0 -0
  74. package/docs/screenshots/guide-clone-worktree-off.png +0 -0
  75. package/docs/screenshots/guide-clone-worktree-on.png +0 -0
  76. package/docs/screenshots/guide-ddev-menu.png +0 -0
  77. package/docs/screenshots/guide-domains.png +0 -0
  78. package/docs/screenshots/guide-home.png +0 -0
  79. package/docs/screenshots/guide-ide-menu.png +0 -0
  80. package/docs/screenshots/guide-remove-refusal.png +0 -0
  81. package/docs/screenshots/guide-rows-worktree.png +0 -0
  82. package/docs/screenshots/guide-screens.png +0 -0
  83. package/docs/screenshots/guide-settings.png +0 -0
  84. package/docs/screenshots/guide-workspace-page.png +0 -0
  85. package/docs/screenshots/home/card-settings.png +0 -0
  86. package/docs/screenshots/home/drag-reorder.png +0 -0
  87. package/docs/screenshots/home/edit-card.png +0 -0
  88. package/docs/screenshots/images/claude-code-image.png +0 -0
  89. package/docs/screenshots/images/edit-with-ai.png +0 -0
  90. package/docs/screenshots/images/gallery.png +0 -0
  91. package/docs/screenshots/images/generate-description.png +0 -0
  92. package/docs/screenshots/images/generate-plan.png +0 -0
  93. package/docs/screenshots/images/providers.png +0 -0
  94. package/docs/screenshots/images/svg-form.png +0 -0
  95. package/docs/screenshots/images/svg-result.png +0 -0
  96. package/docs/screenshots/images/upload.png +0 -0
  97. package/docs/screenshots/new-folder/2-form.png +0 -0
  98. package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
  99. package/docs/screenshots/new-folder/files-folder.png +0 -0
  100. package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
  101. package/docs/screenshots/projects/state-chip-stop.png +0 -0
  102. package/docs/screenshots/readme/dashboard-dark.png +0 -0
  103. package/docs/screenshots/readme/dashboard-light.png +0 -0
  104. package/docs/screenshots/readme/dev-dark.png +0 -0
  105. package/docs/screenshots/readme/dev-light.png +0 -0
  106. package/docs/screenshots/readme/docs-dark.png +0 -0
  107. package/docs/screenshots/readme/docs-light.png +0 -0
  108. package/docs/screenshots/readme/icons/ai.svg +4 -0
  109. package/docs/screenshots/readme/icons/code.svg +4 -0
  110. package/docs/screenshots/readme/icons/docs.svg +4 -0
  111. package/docs/screenshots/readme/icons/services.svg +4 -0
  112. package/docs/screenshots/readme/icons/sites.svg +4 -0
  113. package/docs/screenshots/readme/test-report-dark.png +0 -0
  114. package/docs/screenshots/readme/test-report-light.png +0 -0
  115. package/docs/screenshots/readme/test-scenarios.png +0 -0
  116. package/docs/screenshots/readme/test-steps.png +0 -0
  117. package/docs/screenshots/readme/videos-dark.png +0 -0
  118. package/docs/screenshots/readme/videos-light.png +0 -0
  119. package/docs/screenshots/screens-menu.png +0 -0
  120. package/docs/screenshots/screens-new.png +0 -0
  121. package/docs/screenshots/settings/auth-google-lists.png +0 -0
  122. package/docs/screenshots/settings/delete-1-row.png +0 -0
  123. package/docs/screenshots/settings/delete-2-confirm.png +0 -0
  124. package/docs/screenshots/settings/delete-3-type-name.png +0 -0
  125. package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
  126. package/event-log.js +59 -0
  127. package/files-browser.js +247 -0
  128. package/folders.js +121 -0
  129. package/format.js +73 -0
  130. package/git-info.js +81 -0
  131. package/github-auth.js +62 -0
  132. package/html.js +10 -0
  133. package/icons.js +83 -0
  134. package/images-ai.js +447 -0
  135. package/images-create.js +226 -0
  136. package/images-page.js +417 -0
  137. package/images-records.js +39 -0
  138. package/images-render.js +36 -0
  139. package/images-svg-core.js +214 -0
  140. package/images-svg-sanitize.js +188 -0
  141. package/images-svg.js +241 -0
  142. package/images.js +270 -0
  143. package/jobs.js +430 -0
  144. package/linger.js +21 -0
  145. package/lists.js +292 -0
  146. package/media-info.js +136 -0
  147. package/navigation-page.js +264 -0
  148. package/navigation.js +394 -0
  149. package/notes.js +0 -0
  150. package/package.json +47 -4
  151. package/palette-index.js +46 -0
  152. package/playwright.config.js +23 -0
  153. package/plugin-install.js +239 -0
  154. package/plugin-loader.js +466 -0
  155. package/plugin-manifest.js +83 -0
  156. package/plugin-settings.js +44 -0
  157. package/plugins/workspace-notes/index.js +59 -0
  158. package/plugins-page.js +103 -0
  159. package/project-make.js +241 -0
  160. package/project-name.js +210 -0
  161. package/public/assistant-avatar-moves.js +646 -0
  162. package/public/assistant-avatar-ui.js +1223 -0
  163. package/public/assistant-avatar.js +530 -0
  164. package/public/avatar/druplicon.vrm +0 -0
  165. package/public/css/base.css +260 -0
  166. package/public/css/components/ai-assistant.css +263 -0
  167. package/public/css/components/assistant-avatar.css +279 -0
  168. package/public/css/components/assistant-views.css +198 -0
  169. package/public/css/components/build-order.css +74 -0
  170. package/public/css/components/clone-as-a-git-worktree.css +30 -0
  171. package/public/css/components/command-palette.css +61 -0
  172. package/public/css/components/commands-center.css +139 -0
  173. package/public/css/components/confirm-dialog.css +40 -0
  174. package/public/css/components/create-card.css +8 -0
  175. package/public/css/components/ddev-menu.css +23 -0
  176. package/public/css/components/deep-chat.css +17 -0
  177. package/public/css/components/density.css +11 -0
  178. package/public/css/components/floating-assistant-workspace-pages.css +55 -0
  179. package/public/css/components/folders.css +5 -0
  180. package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
  181. package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
  182. package/public/css/components/grid-and-list.css +203 -0
  183. package/public/css/components/help-page.css +36 -0
  184. package/public/css/components/hero.css +16 -0
  185. package/public/css/components/images.css +70 -0
  186. package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
  187. package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
  188. package/public/css/components/modal-fit.css +113 -0
  189. package/public/css/components/page-width.css +23 -0
  190. package/public/css/components/plugins-page.css +9 -0
  191. package/public/css/components/project-cards.css +256 -0
  192. package/public/css/components/project-rows.css +390 -0
  193. package/public/css/components/reordering-the-workspace-cards.css +85 -0
  194. package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
  195. package/public/css/components/running-commands-as-toasts.css +327 -0
  196. package/public/css/components/settings-editor-core-config-yml.css +828 -0
  197. package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
  198. package/public/css/components/sound-player.css +84 -0
  199. package/public/css/components/suggest-agents-skills-commands.css +112 -0
  200. package/public/css/components/tab-modal.css +142 -0
  201. package/public/css/components/terminals.css +244 -0
  202. package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
  203. package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
  204. package/public/css/components/video-edit.css +6 -0
  205. package/public/css/components/web-ide-open-with.css +23 -0
  206. package/public/css/components/working-indicators.css +158 -0
  207. package/public/css/components/workspace-cards.css +79 -0
  208. package/public/favicon.ico +0 -0
  209. package/public/favicon.png +0 -0
  210. package/public/htmx.min.js +1 -0
  211. package/public/logo.png +0 -0
  212. package/public/media-player.js +215 -0
  213. package/public/mic.js +255 -0
  214. package/public/narration.js +110 -0
  215. package/public/palette.js +278 -0
  216. package/public/sortable.js +251 -0
  217. package/public/suggest.js +337 -0
  218. package/public/terminal.js +903 -0
  219. package/public/themes/default/theme.css +267 -0
  220. package/public/themes/ember/theme.css +180 -0
  221. package/public/themes/press/theme.css +290 -0
  222. package/public/themes/slate/theme.css +182 -0
  223. package/public/touch-icon.png +0 -0
  224. package/public/ui.js +2721 -0
  225. package/public/vendor/ace/LICENSE.txt +24 -0
  226. package/public/vendor/ace/ace.js +23 -0
  227. package/public/vendor/ace/ext-language_tools.js +8 -0
  228. package/public/vendor/ace/ext-searchbox.js +8 -0
  229. package/public/vendor/ace/mode-css.js +8 -0
  230. package/public/vendor/ace/mode-html.js +8 -0
  231. package/public/vendor/ace/mode-javascript.js +8 -0
  232. package/public/vendor/ace/mode-markdown.js +8 -0
  233. package/public/vendor/ace/mode-sh.js +8 -0
  234. package/public/vendor/ace/mode-yaml.js +8 -0
  235. package/public/vendor/ace/theme-textmate.js +8 -0
  236. package/public/vendor/ace/theme-tomorrow_night.js +8 -0
  237. package/public/vendor/ace/worker-yaml.js +1 -0
  238. package/public/vendor/deep-chat.LICENSE +21 -0
  239. package/public/vendor/deep-chat.bundle.js +1 -0
  240. package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
  241. package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  242. package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  243. package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
  244. package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  245. package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  246. package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  247. package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  248. package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
  249. package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  250. package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
  251. package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  252. package/public/vendor/tabler-LICENSE.txt +21 -0
  253. package/public/vendor/tabler-sprite.svg +1 -0
  254. package/public/vendor/three/LICENSE +21 -0
  255. package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
  256. package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
  257. package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
  258. package/public/vendor/three/three.core.min.js +6 -0
  259. package/public/vendor/three/three.module.min.js +6 -0
  260. package/public/vendor/three-vrm/LICENSE +22 -0
  261. package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
  262. package/public/vendor/uikit-icons.min.js +1 -0
  263. package/public/vendor/uikit.min.css +1 -0
  264. package/public/vendor/uikit.min.js +1 -0
  265. package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
  266. package/public/vendor/webship-icons/ui/headset.svg +1 -0
  267. package/public/vendor/webship-icons/ui/menu.svg +1 -0
  268. package/public/vendor/webship-icons/ui/pause.svg +1 -0
  269. package/public/vendor/webship-icons/ui/play.svg +1 -0
  270. package/public/vendor/webship-icons/ui/share.svg +1 -0
  271. package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
  272. package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
  273. package/public/vendor/webship-icons/ui/stop.svg +1 -0
  274. package/public/vendor/xterm/addon-fit.js +2 -0
  275. package/public/vendor/xterm/addon-unicode11.js +2 -0
  276. package/public/vendor/xterm/addon-web-links.js +2 -0
  277. package/public/vendor/xterm/xterm.css +218 -0
  278. package/public/vendor/xterm/xterm.js +2 -0
  279. package/rag.js +169 -0
  280. package/reader.js +132 -0
  281. package/repo-blobs.js +66 -0
  282. package/report-pdf.js +337 -0
  283. package/row-layout.js +1439 -0
  284. package/screenrc +37 -0
  285. package/screens.js +1063 -0
  286. package/scripts/code-server-start.sh +190 -0
  287. package/scripts/images-claude-finish.js +107 -0
  288. package/scripts/screen-host.sh +114 -0
  289. package/scripts/screen-runner.js +362 -0
  290. package/scripts/screen-then.js +47 -0
  291. package/scripts/update.sh +350 -0
  292. package/server.js +7923 -0
  293. package/settings-file.js +423 -0
  294. package/speech.js +297 -0
  295. package/tab-modal.js +75 -0
  296. package/test-reports.js +260 -0
  297. package/themes.js +381 -0
  298. package/transcripts.js +168 -0
  299. package/urls.js +13 -0
  300. package/validate.js +18 -0
  301. package/version-check.js +94 -0
  302. package/video-edit.js +588 -0
  303. package/waiting.js +85 -0
  304. package/workspace-card-tools.js +41 -0
  305. package/workspace-delete.js +139 -0
  306. package/workspace-types.js +85 -0
  307. package/workspaces.js +553 -0
package/format.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * How a size, a time and a video are said, in one place.
3
+ *
4
+ * Small, and imported by nearly everything that renders — which is exactly why it is not left
5
+ * in the middle of the server.
6
+ */
7
+
8
+ // What counts as a video, in one place: the listing, the play fragment and the row icon all have
9
+ // to agree, or a file gets a Play button that 404s.
10
+ const VIDEO_RE = /\.(mp4|m4v|webm|ogv|mov)$/i;
11
+ // Sound the browser plays in an <audio> element; the listing, the player and the file route share it.
12
+ const AUDIO_RE = /\.(mp3|wav|ogg|oga|opus|m4a|aac|flac|weba)$/i;
13
+ // Rendered document formats a docs row can carry beside its markdown.
14
+ const DOC_ARTIFACT_RE = /\.(pdf|html?|docx?|xlsx?|pptx?|odt|ods|odp|rtf|csv)$/i;
15
+
16
+ function humanTime(d) {
17
+ if (!(d instanceof Date) || !d.getTime()) return '';
18
+ const p = new Intl.DateTimeFormat('en-GB', { day: '2-digit', month: 'short', year: 'numeric',
19
+ hour: '2-digit', minute: '2-digit', hour12: false }).formatToParts(d)
20
+ .reduce((o, x) => (o[x.type] = x.value, o), {});
21
+ const off = -d.getTimezoneOffset();
22
+ const sign = off < 0 ? '-' : '+';
23
+ const oh = String(Math.floor(Math.abs(off) / 60)).padStart(2, '0');
24
+ const om = String(Math.abs(off) % 60).padStart(2, '0');
25
+ return `${p.day} ${p.month} ${p.year} ${p.hour}:${p.minute} ${sign}${oh}:${om}`;
26
+ }
27
+
28
+ function humanSize(bytes) {
29
+ if (bytes > 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
30
+ if (bytes > 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
31
+ return Math.max(1, Math.round(bytes / 1024)) + ' KB';
32
+ }
33
+
34
+
35
+ /**
36
+ * A path as a person writes it: `~/workspace/dev/site1`, not
37
+ * `/home/rajab/workspace/dev/site1`. Every path the dashboard SHOWS goes
38
+ * through this — the home prefix is noise in a UI, and it is the one part of a
39
+ * path that differs between machines, so a command someone copies from here
40
+ * should paste cleanly on theirs.
41
+ */
42
+ function tildePath(p) {
43
+ const home = process.env.HOME || '';
44
+ const s = String(p || '');
45
+ if (!home || !s.startsWith(home)) return s;
46
+ const rest = s.slice(home.length);
47
+ return rest === '' ? '~' : (rest.startsWith('/') ? `~${rest}` : s);
48
+ }
49
+
50
+ // A CLI's error, trimmed for a person to read.
51
+ //
52
+ // Tail-slicing is right for a build log — the end is where the failure is — and wrong
53
+ // for a command's error: these CLIs print the REASON first and then a usage synopsis,
54
+ // so `.slice(-300)` showed a list of flags and threw the reason away. An unreadable
55
+ // error is worse than a long one: the Upsun token failure read as a syntax problem for
56
+ // as long as it was displayed that way.
57
+ function cliError(text, max = 400) {
58
+ const raw = String(text || '').replace(/\r/g, '');
59
+ const kept = raw.split('\n').filter((l) => {
60
+ const t = l.trim();
61
+ if (!t) return false;
62
+ // the usage synopsis: "upsun environments [-I|--no-inactive] … [--] <command>"
63
+ if (/\[-{1,2}[A-Za-z]/.test(t) && /\[--\]|\[-h\|--help\]/.test(t)) return false;
64
+ if (/^Usage:$/i.test(t) || /^\s*[a-z][\w:.-]*( \[[^\]]+\])+\s*$/.test(t)) return false;
65
+ return true;
66
+ }).join('\n').trim();
67
+ const out = kept || raw.trim();
68
+ return out.length > max ? out.slice(0, max) + '…' : out;
69
+ }
70
+
71
+ module.exports = {
72
+ tildePath, cliError,
73
+ DOC_ARTIFACT_RE, VIDEO_RE, AUDIO_RE, humanTime, humanSize };
package/git-info.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * What the dashboard asks git about a folder: whether it is a repository, which worktree role it has,
3
+ * what a remove would destroy, which branch it is on. Used by the clone and remove forms, the IDE
4
+ * caption and the row context, and handed to plugins through api.lib (the GIT plugin draws its menu
5
+ * from these answers).
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ /**
13
+ * Is this project directory a git checkout?
14
+ *
15
+ * `rev-parse --git-dir` rather than a test for a `.git` directory, because a
16
+ * project that is ITSELF a worktree carries `.git` as a FILE pointing at the real
17
+ * one — and a worktree of a worktree is both legal and useful. Asking git is the
18
+ * only answer that is right for every shape, including a submodule.
19
+ */
20
+ function projectHasGit(dir) {
21
+ try {
22
+ return spawnSync('git', ['-C', dir, 'rev-parse', '--git-dir'],
23
+ { stdio: ['ignore', 'ignore', 'ignore'], timeout: 5000 }).status === 0;
24
+ } catch (_) { return false; }
25
+ }
26
+
27
+ /** Does this folder have its own `.git`? A directory for a clone, a file for a worktree. */
28
+ function hasOwnGit(dir) {
29
+ try { return fs.existsSync(path.join(dir, '.git')); } catch (_) { return false; }
30
+ }
31
+
32
+ /**
33
+ * What a project folder IS to git: 'none', 'plain', 'primary' (other worktrees
34
+ * live off its .git) or 'linked' (it is a worktree). The destructive difference:
35
+ * deleting a PRIMARY takes the only copy of the repository with it and leaves
36
+ * every linked worktree a folder of files git can say nothing about.
37
+ *
38
+ * Deliberately the same questions fun-git-worktrees.sh asks, so the dashboard and
39
+ * the command cannot disagree about what is safe.
40
+ */
41
+ function gitWorktreeInfo(dir) {
42
+ const out = {
43
+ role: 'none', branch: '', others: [], primary: '', unpushed: 0, dirty: 0,
44
+ };
45
+ if (!projectHasGit(dir)) return out;
46
+ const git = (args) => {
47
+ const r = spawnSync('git', ['-C', dir, ...args], { encoding: 'utf8', timeout: 5000 });
48
+ return r.status === 0 ? String(r.stdout || '').trim() : '';
49
+ };
50
+ const gitdir = git(['rev-parse', '--absolute-git-dir']);
51
+ const common = git(['rev-parse', '--path-format=absolute', '--git-common-dir']);
52
+ const list = git(['worktree', 'list', '--porcelain'])
53
+ .split('\n').filter((l) => l.startsWith('worktree ')).map((l) => l.slice(9));
54
+ const self = fs.realpathSync.native ? (() => { try { return fs.realpathSync(dir); } catch (_) { return dir; } })() : dir;
55
+ out.primary = list[0] || '';
56
+ out.others = list.filter((p) => { try { return fs.realpathSync(p) !== self; } catch (_) { return p !== self; } });
57
+ // A repository with no commits yet has a branch that EXISTS but points at nothing, so
58
+ // `rev-parse --abbrev-ref HEAD` fails and the menus read "detached HEAD" for a folder that was
59
+ // `git init`-ed a moment ago. `symbolic-ref` still names the branch; only when that fails too is
60
+ // HEAD really detached.
61
+ const b = git(['rev-parse', '--abbrev-ref', 'HEAD']);
62
+ const named = git(['symbolic-ref', '--short', '-q', 'HEAD']);
63
+ out.branch = (b && b !== 'HEAD') ? b : named;
64
+ out.unborn = !git(['rev-parse', '--verify', '-q', 'HEAD']) && !!named;
65
+ out.unpushed = parseInt(git(['rev-list', '--count', 'HEAD', '--not', '--remotes']) || '0', 10) || 0;
66
+ out.dirty = git(['status', '--porcelain', '--untracked-files=normal']).split('\n').filter(Boolean).length;
67
+ if (gitdir && common && gitdir !== common) out.role = 'linked';
68
+ else out.role = out.others.length ? 'primary' : 'plain';
69
+ return out;
70
+ }
71
+
72
+ /** The branch a checkout is on, or '' — used only to label the worktree control. */
73
+ function gitBranchOf(dir) {
74
+ try {
75
+ const r = spawnSync('git', ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'],
76
+ { encoding: 'utf8', timeout: 5000 });
77
+ return r.status === 0 ? String(r.stdout || '').trim() : '';
78
+ } catch (_) { return ''; }
79
+ }
80
+
81
+ module.exports = { projectHasGit, hasOwnGit, gitWorktreeInfo, gitBranchOf };
package/github-auth.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * How this app authenticates GitHub work: gh's own login when it stands on its
3
+ * own, otherwise the token from settings.yml's git.github block — probed once
4
+ * and cached, never logged.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const { run } = require('./jobs');
11
+ const { loadSettings } = require('./workspaces');
12
+
13
+ // Whether the `gh` CLI can authenticate on its own, and as whom. Cached for 5 minutes: the
14
+ // probe is a network call. `hosts.yml` names the account even when the token is unreachable —
15
+ // that is exactly the container's situation — so the name alone proves nothing and the probe
16
+ // is what decides.
17
+ let _ghAuth = null;
18
+ let _ghAuthAt = 0;
19
+ async function ghCliAuth() {
20
+ if (_ghAuth && Date.now() - _ghAuthAt < 5 * 60 * 1000) return _ghAuth;
21
+ const out = { binary: null, named: '', login: '' };
22
+ for (const dir of (process.env.PATH || '').split(':')) {
23
+ const candidate = path.join(dir, 'gh');
24
+ try { fs.accessSync(candidate, fs.constants.X_OK); out.binary = candidate; break; } catch (_) { /* keep looking */ }
25
+ }
26
+ try {
27
+ const hosts = fs.readFileSync(path.join(process.env.HOME, '.config', 'gh', 'hosts.yml'), 'utf8');
28
+ out.named = (hosts.match(/^\s*user:\s*(\S+)/m) || [])[1] || '';
29
+ } catch (_) { /* gh never signed in here */ }
30
+ if (out.binary) {
31
+ // No settings token in this environment: the question is whether gh stands on its own.
32
+ const env = { ...process.env };
33
+ delete env.GH_TOKEN; delete env.GITHUB_TOKEN;
34
+ const res = await run('gh', ['api', 'user', '--jq', '.login'], process.env.HOME, { timeoutMs: 20000, env });
35
+ if (res.ok) out.login = res.stdout.trim();
36
+ }
37
+ _ghAuth = out; _ghAuthAt = Date.now();
38
+ return out;
39
+ }
40
+
41
+
42
+ // GitHub auth for the child processes that need it. The token comes from settings.yml's
43
+ // `git.github_token`, which is how this container authenticates at all: its gh config is
44
+ // mounted from the host but the token lives in the host keyring, which it cannot read. Passed
45
+ // per child rather than assigned into process.env, so it is not handed to every job the
46
+ // dashboard runs. Never logged.
47
+ function githubEnv() {
48
+ if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN) return process.env;
49
+ // A forge plugin (@webship.co/workspace-github) owns the credential when it is active.
50
+ try {
51
+ const forge = require('./plugin-loader').pluginForge('github.com');
52
+ if (forge && typeof forge.env === 'function') return forge.env() || process.env;
53
+ } catch (_) { /* the loader is not up yet: the legacy block below answers */ }
54
+ const github = (() => { try { return (loadSettings().git || {}).github || {}; } catch (_) { return {}; } })();
55
+ // gh's own login is preferred when it works; the cached probe answers without waiting.
56
+ if (String(github.use_gh_cli) !== 'false' && _ghAuth && _ghAuth.login) return process.env;
57
+ const token = github.token || '';
58
+ if (!token || token === 'CHANGE_ME') return process.env;
59
+ return { ...process.env, GH_TOKEN: token };
60
+ }
61
+
62
+ module.exports = { ghCliAuth, githubEnv };
package/html.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The one escaper every fragment goes through.
3
+ *
4
+ * Its own module because everything that renders HTML needs it and nothing else in this file is a
5
+ * dependency of it — so a module can import the escaper without importing the server.
6
+ */
7
+
8
+ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
9
+
10
+ module.exports = { esc };
package/icons.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The icon libraries a workspace card can draw from.
3
+ *
4
+ * Two of them: UIKit's own set, and Tabler's 4,700 as a vendored sprite. A name is `tabler:x` or a
5
+ * bare UIKit name, and an unknown one falls back rather than rendering an empty box.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const PUBLIC_DIR = path.join(__dirname, 'public');
12
+
13
+ const { esc } = require('./html');
14
+
15
+ // Read once: the Tabler sprite is 4,736 icons, and a workspace page renders twenty of them.
16
+ let _tablerCache = null;
17
+ let _iconCache = null;
18
+ // Icon and description for a workspace, stored in its own settings file so a
19
+ // workspace added from the dashboard is not stuck with the default folder icon.
20
+ //
21
+ // The icon list is PARSED OUT OF the vendored uikit-icons.min.js rather than typed
22
+ // here, so it cannot drift from what will actually render. That drift was real: the
23
+ // commands workspace shipped with `terminal`, which UIKit has no icon for, so its
24
+ // card and rail rendered an empty box. Anything not in this set is refused.
25
+ // Brand and social marks. They are logos, not interface icons — a workspace card
26
+ // showing the Facebook mark says nothing about the workspace — so they are kept out
27
+ // of the chooser. UIKit ships about forty of them.
28
+ const BRAND_ICONS = new Set([
29
+
30
+ '500px', 'android', 'android-robot', 'apple', 'behance', 'bluesky', 'discord', 'dribbble',
31
+ 'etsy', 'facebook', 'flickr', 'foursquare', 'github', 'github-alt', 'gitter', 'google',
32
+ 'instagram', 'joomla', 'linkedin', 'mastodon', 'microsoft', 'pinterest', 'reddit',
33
+ 'soundcloud', 'telegram', 'threads', 'tiktok', 'tripadvisor', 'tumblr', 'twitch',
34
+ 'twitter', 'uikit', 'vimeo', 'whatsapp', 'wordpress', 'x', 'xing', 'yelp', 'yootheme',
35
+ 'youtube',
36
+ ]);
37
+
38
+ function uikitIcons() {
39
+ if (_iconCache) return _iconCache;
40
+ try {
41
+ const src = fs.readFileSync(path.join(PUBLIC_DIR, 'vendor', 'uikit-icons.min.js'), 'utf8');
42
+ const names = [...src.matchAll(/(?:"([a-zA-Z0-9-]+)"|([a-zA-Z0-9-]+))\s*:\s*'<svg/g)]
43
+ .map((m) => m[1] || m[2]);
44
+ _iconCache = [...new Set(names)].filter((n) => !BRAND_ICONS.has(n)).sort();
45
+ } catch (_) {
46
+ _iconCache = ['folder'];
47
+ }
48
+ return _iconCache;
49
+ }
50
+
51
+ function tablerIcons() {
52
+ if (_tablerCache) return _tablerCache;
53
+ try {
54
+ const src = fs.readFileSync(path.join(PUBLIC_DIR, 'vendor', 'tabler-sprite.svg'), 'utf8');
55
+ _tablerCache = [...new Set([...src.matchAll(/<symbol id="([a-z0-9-]+)"/g)].map((m) => m[1]))]
56
+ .filter((n) => !BRAND_ICONS.has(n))
57
+ .sort();
58
+ } catch (_) {
59
+ _tablerCache = [];
60
+ }
61
+ return _tablerCache;
62
+ }
63
+
64
+ // One icon, from either set. A bare name is UIKit, so every icon stored before
65
+ // Tabler existed keeps working; `tabler:<name>` comes from the sprite.
66
+ function iconHtml(name, ratio = 1) {
67
+ const n = String(name || 'folder');
68
+ if (n.startsWith('tabler:')) {
69
+ const id = n.slice(7);
70
+ if (!tablerIcons().includes(id)) return `<span uk-icon="icon: folder; ratio: ${ratio}"></span>`;
71
+ const px = Math.round(20 * ratio);
72
+ return `<svg class="tabler-icon" width="${px}" height="${px}" aria-hidden="true">`
73
+ + `<use href="/vendor/tabler-sprite.svg#${esc(id)}"></use></svg>`;
74
+ }
75
+ return `<span uk-icon="icon: ${esc(n)}; ratio: ${ratio}"></span>`;
76
+ }
77
+
78
+ function isKnownIcon(name) {
79
+ const n = String(name || '');
80
+ return n.startsWith('tabler:') ? tablerIcons().includes(n.slice(7)) : uikitIcons().includes(n);
81
+ }
82
+
83
+ module.exports = { uikitIcons, tablerIcons, iconHtml, isKnownIcon };