@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.
- package/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +466 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1439 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +7923 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 };
|