@aiworker/web 1.22.2 → 1.23.1
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/bin/cli.js +7 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
- package/dist/assets/DiffView-DC9RX3MF.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
- package/dist/assets/FilesView-BFZkuu4O.js +13 -0
- package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
- package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
- package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
- package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
- package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
- package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
- package/dist/assets/PlanView-BmQUYT_z.js +5 -0
- package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
- package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
- package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
- package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DJsQr-p7.js +10 -0
- package/dist/assets/es-BuYMlA9g.js +16 -0
- package/dist/assets/fr-ePDay33C.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-BD8ILbxF.js +40 -0
- package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
- package/dist/assets/index-zhgg-SDz.css +1 -0
- package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
- package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
- package/dist/assets/ja-BzR9xOY_.js +16 -0
- package/dist/assets/ko-BEjHAm14.js +16 -0
- package/dist/assets/main-BydQ1tr_.js +930 -0
- package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
- package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
- package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
- package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
- package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
- package/dist/assets/pl-rQmpxNIF.js +16 -0
- package/dist/assets/pt-BR-CorbsSW-.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
- package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
- package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
- package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
- package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
- package/dist/assets/surface-COKb77Ua.js +1 -0
- package/dist/assets/tr-CoAMXPtI.js +7 -0
- package/dist/assets/uk-YlAX0rRU.js +16 -0
- package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
- package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
- package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
- package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/vendor-zod-B6551dwG.js +39 -0
- package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
- package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
- package/dist/assets/zh-TW-CQKaCayr.js +16 -0
- package/dist/index.html +5 -43
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +2 -3
- package/server/index.d.ts +15 -0
- package/server/index.js +48 -22
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -0
- package/server/lib/event-stream/global-hub.js +29 -4
- package/server/lib/event-stream/global-hub.test.js +22 -0
- package/server/lib/event-stream/global-ws-bridge.js +15 -18
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +72 -12
- package/server/lib/fs/DOCUMENTATION.md +3 -0
- package/server/lib/fs/routes.js +31 -0
- package/server/lib/fs/routes.test.js +110 -1
- package/server/lib/git/DOCUMENTATION.md +39 -6
- package/server/lib/git/routes.js +45 -12
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/service.js +343 -202
- package/server/lib/git/service.test.js +496 -25
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/message-queue/DOCUMENTATION.md +11 -1
- package/server/lib/message-queue/runtime.js +13 -0
- package/server/lib/message-queue/runtime.test.js +31 -1
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/opencode/DOCUMENTATION.md +35 -5
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +17 -1
- package/server/lib/opencode/lifecycle.js +4 -3
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +128 -1
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/routes.js +5 -3
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +88 -41
- package/server/lib/opencode/settings-helpers.test.js +176 -1
- package/server/lib/opencode/settings-registry.json +750 -0
- package/server/lib/opencode/settings-runtime.js +175 -27
- package/server/lib/opencode/settings-runtime.test.js +321 -7
- package/server/lib/project-context/DOCUMENTATION.md +38 -4
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +15 -0
- package/server/lib/project-context/runtime.js +256 -25
- package/server/lib/project-context/runtime.test.js +160 -3
- package/server/lib/projects/DOCUMENTATION.md +77 -0
- package/server/lib/projects/project-config.js +179 -17
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +43 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +31 -1
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/index.js +7 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +31 -0
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +1 -1
- package/server/lib/quota/providers/opencode-go.test.js +26 -1
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/relay/DOCUMENTATION.md +75 -0
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +44 -8
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +66 -17
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +12 -2
- package/server/lib/scheduled-tasks/runtime.test.js +53 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +117 -267
- package/server/lib/session-assist/runtime.test.js +203 -0
- package/server/lib/session-goal/runtime.js +4 -9
- package/server/lib/small-model/DOCUMENTATION.md +3 -2
- package/server/lib/small-model/call.js +9 -4
- package/server/lib/small-model/call.test.js +29 -0
- package/server/lib/small-model/index.js +5 -10
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/terminal/DOCUMENTATION.md +4 -4
- package/server/lib/terminal/runtime.js +11 -6
- package/server/lib/terminal/runtime.test.js +20 -13
- package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +36 -4
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-hDBKesA_.js +0 -4
- package/dist/assets/FilesView-CdWzeOp9.js +0 -17
- package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
- package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
- package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
- package/dist/assets/PlanView-DyDt_nZ_.js +0 -5
- package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
- package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
- package/dist/assets/de-B5JaA3SA.js +0 -10
- package/dist/assets/es-EPIH88ii.js +0 -16
- package/dist/assets/fr-CAMfxUdZ.js +0 -14
- package/dist/assets/index-DF5kMP-i.js +0 -39
- package/dist/assets/index-Dl3xbAB_.css +0 -1
- package/dist/assets/ja-CifOE1VN.js +0 -16
- package/dist/assets/ko-BBDyAyYQ.js +0 -16
- package/dist/assets/main-MhSaOZoB.js +0 -930
- package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
- package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
- package/dist/assets/pl-DgEHYfFM.js +0 -16
- package/dist/assets/pt-BR-DuWHojUw.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
- package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
- package/dist/assets/tr-D5m2yIWL.js +0 -7
- package/dist/assets/uk-D_q60oog.js +0 -16
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
- package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
- package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
- package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
- package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// A project's setup — worktree setup commands, project actions, pinned draft
|
|
2
|
+
// starters — comes from two files:
|
|
3
|
+
//
|
|
4
|
+
// - the personal file `~/.config/openchamber/projects/<projectId>.json`
|
|
5
|
+
// (client-owned keys; server-owned `version` / `scheduledTasks` live beside
|
|
6
|
+
// them and are never touched here), and
|
|
7
|
+
// - the shared file `<repo>/.aiworker/project.json`, committed by a team so
|
|
8
|
+
// a teammate who pulls the code gets the setup without configuring anything.
|
|
9
|
+
//
|
|
10
|
+
// This module knows both shapes and the one merge rule per field. The route
|
|
11
|
+
// and the VS Code bridge (`packages/vscode/src/project-setup.ts`, a mirror of
|
|
12
|
+
// this file) use the same code paths so a value reads back the same on every
|
|
13
|
+
// surface.
|
|
14
|
+
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const ACTION_NAME_MAX_LENGTH = 80;
|
|
18
|
+
const ACTION_COMMAND_MAX_LENGTH = 4000;
|
|
19
|
+
const ACTION_OPEN_URL_MAX_LENGTH = 2000;
|
|
20
|
+
const ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
|
|
21
|
+
const SETUP_COMMAND_MAX_LENGTH = 4000;
|
|
22
|
+
const SETUP_COMMANDS_MAX = 50;
|
|
23
|
+
|
|
24
|
+
const ACTION_PLATFORMS = new Set(['macos', 'linux', 'windows']);
|
|
25
|
+
const SETUP_WORKTREE_MODES = new Set(['append', 'replace']);
|
|
26
|
+
|
|
27
|
+
const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
const clamp = (value, maxLength) => (value.length > maxLength ? value.slice(0, maxLength) : value);
|
|
30
|
+
|
|
31
|
+
const trimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
32
|
+
|
|
33
|
+
/** Setup commands: non-empty trimmed strings, capped in count and length. */
|
|
34
|
+
export const sanitizeSetupCommands = (value) => {
|
|
35
|
+
if (!Array.isArray(value)) return [];
|
|
36
|
+
const commands = [];
|
|
37
|
+
for (const entry of value) {
|
|
38
|
+
const command = clamp(trimmedString(entry), SETUP_COMMAND_MAX_LENGTH);
|
|
39
|
+
if (!command) continue;
|
|
40
|
+
commands.push(command);
|
|
41
|
+
if (commands.length >= SETUP_COMMANDS_MAX) break;
|
|
42
|
+
}
|
|
43
|
+
return commands;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sanitizeActionPlatforms = (value) => {
|
|
47
|
+
if (!Array.isArray(value)) return [];
|
|
48
|
+
const platforms = [];
|
|
49
|
+
for (const entry of value) {
|
|
50
|
+
const platform = trimmedString(entry).toLowerCase();
|
|
51
|
+
if (ACTION_PLATFORMS.has(platform) && !platforms.includes(platform)) platforms.push(platform);
|
|
52
|
+
}
|
|
53
|
+
return platforms;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Project actions: `id`, `name`, and `command` are required and ids are
|
|
58
|
+
* unique; every optional field is dropped when empty so the stored record
|
|
59
|
+
* carries only what the user set. `runIn` keeps only the one value the UI
|
|
60
|
+
* understands (`parent`); anything else means "run in the worktree".
|
|
61
|
+
*/
|
|
62
|
+
export const sanitizeProjectActions = (value) => {
|
|
63
|
+
if (!Array.isArray(value)) return [];
|
|
64
|
+
const actions = [];
|
|
65
|
+
const seenIds = new Set();
|
|
66
|
+
for (const entry of value) {
|
|
67
|
+
if (!isObjectRecord(entry)) continue;
|
|
68
|
+
const id = trimmedString(entry.id);
|
|
69
|
+
const name = clamp(trimmedString(entry.name), ACTION_NAME_MAX_LENGTH);
|
|
70
|
+
const command = clamp(trimmedString(entry.command), ACTION_COMMAND_MAX_LENGTH);
|
|
71
|
+
if (!id || !name || !command || seenIds.has(id)) continue;
|
|
72
|
+
seenIds.add(id);
|
|
73
|
+
|
|
74
|
+
const icon = trimmedString(entry.icon);
|
|
75
|
+
const platforms = sanitizeActionPlatforms(entry.platforms);
|
|
76
|
+
const openUrl = clamp(trimmedString(entry.openUrl), ACTION_OPEN_URL_MAX_LENGTH);
|
|
77
|
+
const desktopOpenSshForward = clamp(trimmedString(entry.desktopOpenSshForward), ACTION_DESKTOP_FORWARD_MAX_LENGTH);
|
|
78
|
+
|
|
79
|
+
const action = { id, name, command, icon: icon || null };
|
|
80
|
+
if (entry.autoOpenUrl === true) action.autoOpenUrl = true;
|
|
81
|
+
if (openUrl) action.openUrl = openUrl;
|
|
82
|
+
if (desktopOpenSshForward) action.desktopOpenSshForward = desktopOpenSshForward;
|
|
83
|
+
if (platforms.length > 0) action.platforms = platforms;
|
|
84
|
+
if (entry.runIn === 'parent') action.runIn = 'parent';
|
|
85
|
+
actions.push(action);
|
|
86
|
+
}
|
|
87
|
+
return actions;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Draft starters: `{ type: 'command' | 'skill', name }`, unique by `type:name`. */
|
|
91
|
+
export const sanitizeDraftStarters = (value) => {
|
|
92
|
+
if (!Array.isArray(value)) return [];
|
|
93
|
+
const starters = [];
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const entry of value) {
|
|
96
|
+
if (!isObjectRecord(entry)) continue;
|
|
97
|
+
const type = entry.type === 'command' || entry.type === 'skill' ? entry.type : null;
|
|
98
|
+
const name = trimmedString(entry.name);
|
|
99
|
+
if (!type || !name) continue;
|
|
100
|
+
const key = `${type}:${name}`;
|
|
101
|
+
if (seen.has(key)) continue;
|
|
102
|
+
seen.add(key);
|
|
103
|
+
starters.push({ type, name });
|
|
104
|
+
}
|
|
105
|
+
return starters;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The client-facing view of a raw config document. A primary action id that
|
|
110
|
+
* names no action is reported as `null`.
|
|
111
|
+
*/
|
|
112
|
+
const sanitizeIdList = (value) => {
|
|
113
|
+
if (!Array.isArray(value)) return [];
|
|
114
|
+
const ids = [];
|
|
115
|
+
for (const entry of value) {
|
|
116
|
+
const id = trimmedString(entry);
|
|
117
|
+
if (id && !ids.includes(id)) ids.push(id);
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The personal part of the view, straight from the personal file. The wait
|
|
124
|
+
* flag is `null` when the file does not set it, so the merge can let a
|
|
125
|
+
* shared value through; `hiddenSharedActionIds` and `setupWorktreeMode` only
|
|
126
|
+
* matter when a shared file exists.
|
|
127
|
+
*/
|
|
128
|
+
export const projectSetupViewOf = (raw) => {
|
|
129
|
+
const document = isObjectRecord(raw) ? raw : {};
|
|
130
|
+
const projectActions = sanitizeProjectActions(document.projectActions);
|
|
131
|
+
const primaryRaw = trimmedString(document.projectActionsPrimaryId);
|
|
132
|
+
return {
|
|
133
|
+
setupWorktree: sanitizeSetupCommands(document['setup-worktree']),
|
|
134
|
+
setupWorktreeWait: typeof document['setup-worktree-wait'] === 'boolean' ? document['setup-worktree-wait'] : null,
|
|
135
|
+
setupWorktreeMode: SETUP_WORKTREE_MODES.has(document.setupWorktreeMode) ? document.setupWorktreeMode : 'append',
|
|
136
|
+
projectActions,
|
|
137
|
+
projectActionsPrimaryId: primaryRaw && projectActions.some((action) => action.id === primaryRaw) ? primaryRaw : null,
|
|
138
|
+
draftStarters: sanitizeDraftStarters(document.draftStarters),
|
|
139
|
+
hiddenSharedActionIds: sanitizeIdList(document.hiddenSharedActionIds),
|
|
140
|
+
sharedTrust: sharedTrustOf(document.sharedTrust),
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** The recorded answer to the trust prompt: which shared commands were trusted, and when. */
|
|
145
|
+
const sharedTrustOf = (value) => {
|
|
146
|
+
if (!isObjectRecord(value)) return null;
|
|
147
|
+
const hash = trimmedString(value.hash);
|
|
148
|
+
if (!hash) return null;
|
|
149
|
+
return { hash, trustedAt: Number.isFinite(value.trustedAt) ? value.trustedAt : 0 };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Turn a client patch (view keys) into the on-disk keys it changes. Only the
|
|
154
|
+
* keys present in the patch are returned, so a caller can merge the result
|
|
155
|
+
* over the raw document without clearing what the patch did not mention.
|
|
156
|
+
* A key with the wrong shape is a validation error, never silently dropped.
|
|
157
|
+
*/
|
|
158
|
+
export const projectSetupPatchToStored = (patch) => {
|
|
159
|
+
if (!isObjectRecord(patch)) {
|
|
160
|
+
throw new Error('patch must be an object');
|
|
161
|
+
}
|
|
162
|
+
const stored = {};
|
|
163
|
+
if ('setupWorktree' in patch) {
|
|
164
|
+
if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
|
|
165
|
+
stored['setup-worktree'] = sanitizeSetupCommands(patch.setupWorktree);
|
|
166
|
+
}
|
|
167
|
+
if ('setupWorktreeWait' in patch) {
|
|
168
|
+
if (typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean');
|
|
169
|
+
stored['setup-worktree-wait'] = patch.setupWorktreeWait;
|
|
170
|
+
}
|
|
171
|
+
if ('projectActions' in patch) {
|
|
172
|
+
if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
|
|
173
|
+
stored.projectActions = sanitizeProjectActions(patch.projectActions);
|
|
174
|
+
}
|
|
175
|
+
if ('projectActionsPrimaryId' in patch) {
|
|
176
|
+
const primary = patch.projectActionsPrimaryId;
|
|
177
|
+
if (primary !== null && typeof primary !== 'string') throw new Error('projectActionsPrimaryId must be a string or null');
|
|
178
|
+
stored.projectActionsPrimaryId = trimmedString(primary) || undefined;
|
|
179
|
+
}
|
|
180
|
+
if ('draftStarters' in patch) {
|
|
181
|
+
if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
|
|
182
|
+
stored.draftStarters = sanitizeDraftStarters(patch.draftStarters);
|
|
183
|
+
}
|
|
184
|
+
if ('hiddenSharedActionIds' in patch) {
|
|
185
|
+
if (!Array.isArray(patch.hiddenSharedActionIds)) throw new Error('hiddenSharedActionIds must be an array');
|
|
186
|
+
stored.hiddenSharedActionIds = sanitizeIdList(patch.hiddenSharedActionIds);
|
|
187
|
+
}
|
|
188
|
+
if ('setupWorktreeMode' in patch) {
|
|
189
|
+
if (!SETUP_WORKTREE_MODES.has(patch.setupWorktreeMode)) throw new Error('setupWorktreeMode must be "append" or "replace"');
|
|
190
|
+
stored.setupWorktreeMode = patch.setupWorktreeMode;
|
|
191
|
+
}
|
|
192
|
+
if ('sharedTrustHash' in patch) {
|
|
193
|
+
const hash = patch.sharedTrustHash;
|
|
194
|
+
if (hash !== null && (typeof hash !== 'string' || !hash.trim())) throw new Error('sharedTrustHash must be a non-empty string or null');
|
|
195
|
+
stored.sharedTrust = hash === null ? undefined : { hash: hash.trim(), trustedAt: Date.now() };
|
|
196
|
+
}
|
|
197
|
+
if ('projectPath' in patch) {
|
|
198
|
+
if (typeof patch.projectPath !== 'string') throw new Error('projectPath must be a string');
|
|
199
|
+
const projectPath = patch.projectPath.trim();
|
|
200
|
+
if (projectPath) stored.projectPath = projectPath;
|
|
201
|
+
}
|
|
202
|
+
return stored;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ── Shared file ──
|
|
206
|
+
|
|
207
|
+
export const SHARED_CONFIG_RELATIVE_PATH = '.aiworker/project.json';
|
|
208
|
+
/** Where repository plans live unless the shared file's `plansDir` says otherwise. */
|
|
209
|
+
export const DEFAULT_PLANS_DIR = '.aiworker/plans';
|
|
210
|
+
const SHARED_CONFIG_VERSION = 1;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A `plansDir` is a relative path inside the repo: no absolute paths, no
|
|
214
|
+
* drive letters, no `..` segments, forward slashes. Returns the normalized
|
|
215
|
+
* value or `null` when the value is not acceptable.
|
|
216
|
+
*/
|
|
217
|
+
export const normalizePlansDir = (value) => {
|
|
218
|
+
const raw = trimmedString(value).replace(/\\/g, '/');
|
|
219
|
+
if (!raw) return null;
|
|
220
|
+
if (raw.startsWith('/') || /^[A-Za-z]:/.test(raw)) return null;
|
|
221
|
+
const segments = raw.split('/').filter((segment) => segment.length > 0 && segment !== '.');
|
|
222
|
+
if (segments.length === 0 || segments.some((segment) => segment === '..')) return null;
|
|
223
|
+
return segments.join('/');
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const EMPTY_SHARED = Object.freeze({
|
|
227
|
+
setupWorktree: [],
|
|
228
|
+
setupWorktreeWait: null,
|
|
229
|
+
projectActions: [],
|
|
230
|
+
draftStarters: [],
|
|
231
|
+
plansDir: null,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Parse the text of a shared file. Anything that is not a version-1 object
|
|
236
|
+
* is `invalid` with a reason (never an empty config: a teammate must see that
|
|
237
|
+
* the file is broken, not that the project has no shared setup). A `plansDir`
|
|
238
|
+
* that points outside the repo is invalid for the same reason.
|
|
239
|
+
*/
|
|
240
|
+
export const parseSharedProjectConfig = (raw) => {
|
|
241
|
+
let parsed;
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(raw);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return { status: 'invalid', reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
246
|
+
}
|
|
247
|
+
if (!isObjectRecord(parsed)) return { status: 'invalid', reason: 'not an object' };
|
|
248
|
+
if (parsed.version !== SHARED_CONFIG_VERSION) return { status: 'invalid', reason: `unsupported version ${JSON.stringify(parsed.version)}` };
|
|
249
|
+
if ('setupWorktree' in parsed && !Array.isArray(parsed.setupWorktree)) return { status: 'invalid', reason: 'setupWorktree must be an array' };
|
|
250
|
+
if ('setupWorktreeWait' in parsed && typeof parsed.setupWorktreeWait !== 'boolean') return { status: 'invalid', reason: 'setupWorktreeWait must be a boolean' };
|
|
251
|
+
if ('projectActions' in parsed && !Array.isArray(parsed.projectActions)) return { status: 'invalid', reason: 'projectActions must be an array' };
|
|
252
|
+
if ('draftStarters' in parsed && !Array.isArray(parsed.draftStarters)) return { status: 'invalid', reason: 'draftStarters must be an array' };
|
|
253
|
+
let plansDir = null;
|
|
254
|
+
if ('plansDir' in parsed && parsed.plansDir !== null) {
|
|
255
|
+
plansDir = normalizePlansDir(parsed.plansDir);
|
|
256
|
+
if (!plansDir) return { status: 'invalid', reason: 'plansDir must be a relative path inside the repository' };
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
status: 'ok',
|
|
260
|
+
config: {
|
|
261
|
+
setupWorktree: sanitizeSetupCommands(parsed.setupWorktree),
|
|
262
|
+
setupWorktreeWait: typeof parsed.setupWorktreeWait === 'boolean' ? parsed.setupWorktreeWait : null,
|
|
263
|
+
projectActions: sanitizeProjectActions(parsed.projectActions),
|
|
264
|
+
draftStarters: sanitizeDraftStarters(parsed.draftStarters),
|
|
265
|
+
plansDir,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const withSource = (entries, source) => entries.map((entry) => ({ ...entry, source }));
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* What a trust answer covers: the shared setup commands and the shared
|
|
274
|
+
* actions' commands, in a canonical order, hashed. A pull that changes any
|
|
275
|
+
* of them changes the hash, so the prompt returns for the new commands.
|
|
276
|
+
* `null` when the shared config has nothing that executes.
|
|
277
|
+
*/
|
|
278
|
+
export const sharedTrustHashOf = (shared) => {
|
|
279
|
+
const commands = shared.setupWorktree;
|
|
280
|
+
const actions = shared.projectActions
|
|
281
|
+
.map((action) => {
|
|
282
|
+
const executable = { id: action.id, command: action.command };
|
|
283
|
+
if (action.runIn) executable.runIn = action.runIn;
|
|
284
|
+
return executable;
|
|
285
|
+
})
|
|
286
|
+
.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
287
|
+
if (commands.length === 0 && actions.length === 0) return null;
|
|
288
|
+
const digest = crypto.createHash('sha256').update(JSON.stringify({ setupWorktree: commands, projectActions: actions })).digest('hex');
|
|
289
|
+
return `sha256:${digest}`;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* One merged view from the personal view and the shared read. Rules:
|
|
294
|
+
* - setup commands: shared first, then personal; personal `setupWorktreeMode`
|
|
295
|
+
* `replace` uses only the personal list;
|
|
296
|
+
* - wait flag: personal when the personal file sets it, else shared, else off;
|
|
297
|
+
* - actions: union by id, a personal action replaces the shared one with the
|
|
298
|
+
* same id, hidden shared ids are dropped, primary is personal only;
|
|
299
|
+
* - draft starters: union by `type:name`, shared first.
|
|
300
|
+
* Every merged action and starter carries `source`. The `shared` and
|
|
301
|
+
* `personal` blocks are returned too so a page can edit one without guessing
|
|
302
|
+
* which entries came from where.
|
|
303
|
+
*/
|
|
304
|
+
export const mergeProjectSetup = (personal, sharedRead) => {
|
|
305
|
+
const shared = sharedRead.status === 'ok' ? sharedRead.config : EMPTY_SHARED;
|
|
306
|
+
const hidden = new Set(personal.hiddenSharedActionIds);
|
|
307
|
+
const personalIds = new Set(personal.projectActions.map((action) => action.id));
|
|
308
|
+
const sharedActions = shared.projectActions.filter((action) => !hidden.has(action.id) && !personalIds.has(action.id));
|
|
309
|
+
const starterKeys = new Set(shared.draftStarters.map((starter) => `${starter.type}:${starter.name}`));
|
|
310
|
+
const personalStarters = personal.draftStarters.filter((starter) => !starterKeys.has(`${starter.type}:${starter.name}`));
|
|
311
|
+
const trustHash = sharedTrustHashOf(shared);
|
|
312
|
+
return {
|
|
313
|
+
// Nothing executable in the shared file means nothing to trust; otherwise
|
|
314
|
+
// the recorded answer must match the current commands exactly.
|
|
315
|
+
trust: { hash: trustHash, trusted: trustHash === null || personal.sharedTrust?.hash === trustHash },
|
|
316
|
+
setupWorktree: personal.setupWorktreeMode === 'replace'
|
|
317
|
+
? personal.setupWorktree
|
|
318
|
+
: [...shared.setupWorktree, ...personal.setupWorktree],
|
|
319
|
+
setupWorktreeWait: personal.setupWorktreeWait !== null
|
|
320
|
+
? personal.setupWorktreeWait
|
|
321
|
+
: shared.setupWorktreeWait === true,
|
|
322
|
+
projectActions: [...withSource(sharedActions, 'shared'), ...withSource(personal.projectActions, 'personal')],
|
|
323
|
+
projectActionsPrimaryId: personal.projectActionsPrimaryId,
|
|
324
|
+
draftStarters: [...withSource(shared.draftStarters, 'shared'), ...withSource(personalStarters, 'personal')],
|
|
325
|
+
shared: sharedBlockOf(sharedRead, shared),
|
|
326
|
+
personal,
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const sharedBlockOf = (sharedRead, shared) => {
|
|
331
|
+
const block = { status: sharedRead.status, path: SHARED_CONFIG_RELATIVE_PATH, ...shared };
|
|
332
|
+
if (sharedRead.status === 'invalid') block.reason = sharedRead.reason;
|
|
333
|
+
return block;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** An action without an icon is written without the key; readers fall back to the play icon. */
|
|
337
|
+
const withoutEmptyIcon = (action) => {
|
|
338
|
+
if (action.icon !== null) return action;
|
|
339
|
+
const { icon: _emptyIcon, ...rest } = action;
|
|
340
|
+
return rest;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/** True when the shared config carries nothing: the file should not exist. */
|
|
344
|
+
export const isSharedProjectConfigEmpty = (config) => (
|
|
345
|
+
config.setupWorktree.length === 0
|
|
346
|
+
&& config.setupWorktreeWait === null
|
|
347
|
+
&& config.projectActions.length === 0
|
|
348
|
+
&& config.draftStarters.length === 0
|
|
349
|
+
&& config.plansDir === null
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The bytes of a shared file: version first, then only the keys that carry
|
|
354
|
+
* something, in a fixed order, pretty-printed — the file is committed and
|
|
355
|
+
* reviewed, so its diffs must stay readable. Actions lose their `source`
|
|
356
|
+
* mark and keep only the fields the user set.
|
|
357
|
+
*/
|
|
358
|
+
export const serializeSharedProjectConfig = (config) => {
|
|
359
|
+
const document = { version: SHARED_CONFIG_VERSION };
|
|
360
|
+
if (config.setupWorktree.length > 0) document.setupWorktree = config.setupWorktree;
|
|
361
|
+
if (config.setupWorktreeWait !== null) document.setupWorktreeWait = config.setupWorktreeWait;
|
|
362
|
+
if (config.projectActions.length > 0) document.projectActions = sanitizeProjectActions(config.projectActions).map(withoutEmptyIcon);
|
|
363
|
+
if (config.draftStarters.length > 0) document.draftStarters = config.draftStarters;
|
|
364
|
+
if (config.plansDir !== null) document.plansDir = config.plansDir;
|
|
365
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The next shared config after a client patch over the current one. Every
|
|
370
|
+
* named key replaces the current value; a wrongly shaped key is a validation
|
|
371
|
+
* error, and a `plansDir` outside the repo is refused rather than stored.
|
|
372
|
+
*/
|
|
373
|
+
export const applySharedProjectSetupPatch = (current, patch) => {
|
|
374
|
+
if (!isObjectRecord(patch)) throw new Error('patch must be an object');
|
|
375
|
+
const next = { ...current };
|
|
376
|
+
if ('setupWorktree' in patch) {
|
|
377
|
+
if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
|
|
378
|
+
next.setupWorktree = sanitizeSetupCommands(patch.setupWorktree);
|
|
379
|
+
}
|
|
380
|
+
if ('setupWorktreeWait' in patch) {
|
|
381
|
+
if (patch.setupWorktreeWait !== null && typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean or null');
|
|
382
|
+
next.setupWorktreeWait = patch.setupWorktreeWait;
|
|
383
|
+
}
|
|
384
|
+
if ('projectActions' in patch) {
|
|
385
|
+
if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
|
|
386
|
+
next.projectActions = sanitizeProjectActions(patch.projectActions);
|
|
387
|
+
}
|
|
388
|
+
if ('draftStarters' in patch) {
|
|
389
|
+
if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
|
|
390
|
+
next.draftStarters = sanitizeDraftStarters(patch.draftStarters);
|
|
391
|
+
}
|
|
392
|
+
if ('plansDir' in patch) {
|
|
393
|
+
if (patch.plansDir === null || (typeof patch.plansDir === 'string' && !patch.plansDir.trim())) {
|
|
394
|
+
next.plansDir = null;
|
|
395
|
+
} else {
|
|
396
|
+
const plansDir = normalizePlansDir(patch.plansDir);
|
|
397
|
+
if (!plansDir) throw new Error('plansDir must be a relative path inside the repository');
|
|
398
|
+
next.plansDir = plansDir;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return next;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export const EMPTY_SHARED_PROJECT_CONFIG = EMPTY_SHARED;
|
|
405
|
+
|
|
406
|
+
export const isProjectSetupValidationError = (error) => {
|
|
407
|
+
const message = error instanceof Error ? error.message : '';
|
|
408
|
+
return message.includes('must be') || message.includes('is required') || message.includes('unsupported characters') || message.includes('not found');
|
|
409
|
+
};
|