@dsh-workbench/core 0.1.9 → 0.1.11

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 (42) hide show
  1. package/bin/dsh-workbench +23 -0
  2. package/client.js +609 -84
  3. package/cordis.patch.yml +11 -3
  4. package/dist/client/client.body.js +609 -84
  5. package/dist/client/index.js +610 -85
  6. package/dist/host/index.js +406 -276
  7. package/dist/types/app/client.d.ts +140 -6
  8. package/dist/types/app/layouts/rightbar/BoardEmpty.d.ts +2 -1
  9. package/dist/types/app/layouts/rightbar/Rightbar.d.ts +1 -0
  10. package/dist/types/app/layouts/rightbar/board-plan.d.ts +2 -0
  11. package/dist/types/app/layouts/sidebar/ProjectSwitcher.d.ts +2 -2
  12. package/dist/types/app/layouts/sidebar/Sidebar.d.ts +1 -1
  13. package/dist/types/app/layouts/sidebar/WorkspaceRenameDialog.d.ts +8 -0
  14. package/dist/types/app/layouts/sidebar/WorkspaceRowAction.d.ts +7 -0
  15. package/dist/types/app/layouts/sidebar/WorkspaceRowLabel.d.ts +8 -0
  16. package/dist/types/app/layouts/sidebar/WorkspaceSearchField.d.ts +9 -0
  17. package/dist/types/app/layouts/sidebar/__tests__/workspace-name.test.d.ts +1 -0
  18. package/dist/types/app/layouts/sidebar/workspace-items.d.ts +11 -0
  19. package/dist/types/app/layouts/sidebar/workspace-name.d.ts +16 -0
  20. package/dist/types/app/units/__tests__/registry.test.d.ts +1 -0
  21. package/dist/types/app/units/fixture-unit-mock.d.ts +3 -1
  22. package/dist/types/modules/panels/__tests__/approval.test.d.ts +1 -0
  23. package/dist/types/modules/panels/model/approval.d.ts +2 -0
  24. package/dist/types/shared/api/diy.d.ts +3 -0
  25. package/dist/types/shared/api/diy.test.d.ts +1 -0
  26. package/dist/types/shared/api/host/__tests__/approval-legacy.test.d.ts +1 -0
  27. package/dist/types/shared/api/host/approval-legacy.d.ts +2 -0
  28. package/dist/types/shared/api/host/diy.d.ts +3 -0
  29. package/dist/types/shared/api/host/diy.test.d.ts +1 -0
  30. package/dist/types/shared/api/host/mount-units.d.ts +13 -6
  31. package/dist/types/shared/hooks/mock-mode.d.ts +1 -0
  32. package/package.json +13 -5
  33. package/preset/diy.patch.yml +156 -0
  34. package/scripts/seed-profile.sh +116 -0
  35. package/skills/workbench-module-diy/BUILD.md +179 -0
  36. package/skills/workbench-module-diy/DESIGN.md +81 -0
  37. package/skills/workbench-module-diy/LINKAGE.md +113 -0
  38. package/skills/workbench-module-diy/PANEL.md +124 -0
  39. package/skills/workbench-module-diy/REVIEW.md +31 -0
  40. package/skills/workbench-module-diy/SKILL.md +54 -0
  41. package/skills/workbench-module-diy/STEPS.md +93 -0
  42. package/skills/workbench-module-diy/TOOLS.md +112 -0
@@ -2,9 +2,9 @@ import type { Translate, WorkspaceView } from '@dsh-workbench/shared';
2
2
  export interface ProjectSwitcherProps {
3
3
  items: readonly WorkspaceView[];
4
4
  current: WorkspaceView | null;
5
- workspaceTitle: (title: string) => string;
6
5
  onPick: (workspaceId: string) => void;
7
6
  onAdd: () => void;
7
+ onRename: (workspaceId: string, title: string) => Promise<boolean>;
8
8
  t: Translate;
9
9
  }
10
- export declare function ProjectSwitcher({ items, current, workspaceTitle, onPick, onAdd, t, }: ProjectSwitcherProps): JSX.Element;
10
+ export declare function ProjectSwitcher({ items, current, onPick, onAdd, onRename, t }: ProjectSwitcherProps): JSX.Element;
@@ -2,7 +2,7 @@ import type { PanelEntry, PanelInfo, RenderSlot, SessionStatusSnapshot, Sessions
2
2
  export interface SidebarActions {
3
3
  listPanels: () => PanelEntry[];
4
4
  subscribePanels: (callback: () => void) => () => void;
5
- workspaceDisplayTitle: (title: string) => string;
5
+ renameWorkspace: (workspaceId: string, title: string) => Promise<boolean>;
6
6
  openWorkspace: (workspaceId: string) => void;
7
7
  createWorkspace: (path: string) => Promise<{
8
8
  workspaceId?: string;
@@ -0,0 +1,8 @@
1
+ import type { Translate, WorkspaceView } from '@dsh-workbench/shared';
2
+ export interface WorkspaceRenameDialogProps {
3
+ readonly workspace: WorkspaceView;
4
+ readonly t: Translate;
5
+ readonly onClose: () => void;
6
+ readonly onSubmit: (title: string) => Promise<boolean>;
7
+ }
8
+ export declare function WorkspaceRenameDialog({ workspace, t, onClose, onSubmit }: WorkspaceRenameDialogProps): JSX.Element;
@@ -0,0 +1,7 @@
1
+ import type { Translate, WorkspaceView } from '@dsh-workbench/shared';
2
+ export interface WorkspaceRowActionProps {
3
+ readonly workspace: WorkspaceView;
4
+ readonly t: Translate;
5
+ readonly onEdit: () => void;
6
+ }
7
+ export declare function WorkspaceRowAction({ workspace, t, onEdit }: WorkspaceRowActionProps): JSX.Element;
@@ -0,0 +1,8 @@
1
+ import type { ReactNode, WorkspaceView } from '@dsh-workbench/shared';
2
+ export interface WorkspaceRowLabelProps {
3
+ readonly workspace: WorkspaceView;
4
+ readonly current: boolean;
5
+ readonly action?: ReactNode;
6
+ readonly onHover?: (hovering: boolean) => void;
7
+ }
8
+ export declare function WorkspaceRowLabel({ workspace, current, action, onHover }: WorkspaceRowLabelProps): JSX.Element;
@@ -0,0 +1,9 @@
1
+ import type { Translate } from '@dsh-workbench/shared';
2
+ export interface WorkspaceSearchFieldProps {
3
+ readonly t: Translate;
4
+ readonly query: string;
5
+ readonly onQuery: (query: string) => void;
6
+ readonly onCursor: (step: 1 | -1) => void;
7
+ readonly onSubmit: () => void;
8
+ }
9
+ export declare function WorkspaceSearchField({ t, query, onQuery, onCursor, onSubmit, }: WorkspaceSearchFieldProps): JSX.Element;
@@ -0,0 +1,11 @@
1
+ import type { Translate, WorkspaceView } from '@dsh-workbench/shared';
2
+ import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ export interface WorkspaceItemsOptions {
4
+ readonly results: readonly WorkspaceView[];
5
+ readonly currentId: string | null;
6
+ readonly hoverId: string | null;
7
+ readonly t: Translate;
8
+ readonly onHover: (workspaceId: string | null) => void;
9
+ readonly onEdit: (workspace: WorkspaceView) => void;
10
+ }
11
+ export declare function workspaceMenuItems(options: WorkspaceItemsOptions): MenuEntry[];
@@ -0,0 +1,16 @@
1
+ export declare const WORKSPACE_NAME_MAX = 20;
2
+ export declare const WORKSPACE_NAME_INPUT_MAX: number;
3
+ export interface WorkspaceDisplayName {
4
+ readonly dir: string;
5
+ readonly name: string;
6
+ }
7
+ interface Named {
8
+ readonly path: string;
9
+ readonly title: string;
10
+ }
11
+ export declare function basenameOf(value: string): string;
12
+ export declare function displayNameOf(workspace: Named): WorkspaceDisplayName;
13
+ export declare function matchWorkspaces<T extends Named>(items: readonly T[], query: string): T[];
14
+ export declare function titleFor(workspace: Named, draft: string): string;
15
+ export declare function nameTooLong(draft: string): boolean;
16
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,7 @@
1
1
  import type { BoardModule, UnitClientFace, UnitManifest } from '@dsh-workbench/shared';
2
2
  export declare const FIXTURE_UNIT_ID = "wb-fixture";
3
+ export declare const CRASH_UNIT_ID = "wb-fixture-crash";
3
4
  export declare const FIXTURE_FACE: UnitClientFace;
5
+ export declare const CRASH_FACE: UnitClientFace;
4
6
  export declare function fixtureRoster(roster: readonly UnitManifest[]): readonly UnitManifest[];
5
- export declare function fixtureSeed(roster: readonly UnitManifest[], all: readonly BoardModule[]): readonly BoardModule[];
7
+ export declare function fixtureSeed(roster: readonly UnitManifest[], all: readonly BoardModule[], crash?: boolean): readonly BoardModule[];
@@ -0,0 +1,2 @@
1
+ import type { ApprovalPolicy } from '@dsh-workbench/shared/host';
2
+ export declare const panelsApproval: ApprovalPolicy;
@@ -0,0 +1,3 @@
1
+ export declare const DIY_PRESET_ID = "workbench-diy";
2
+ export declare const WORKSHOP_TITLE = "\u5DE5\u4F5C\u53F0DIY";
3
+ export declare const WORKSHOP_PAST_TITLES: readonly string[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ApprovalPolicy } from '@dsh-workbench/shared/host';
2
+ export declare const legacyApproval: ApprovalPolicy;
@@ -0,0 +1,3 @@
1
+ import type { DshHostContext } from '@dsh-workbench/shared';
2
+ export declare const WORKSHOP_RELATIVE = "workbench/diy";
3
+ export declare function workshopPathOf(ctx: DshHostContext): string | null;
@@ -0,0 +1 @@
1
+ export {};
@@ -3,6 +3,10 @@ import type { UnitManifest } from '@dsh-workbench/shared/host';
3
3
  import { type RouteTable } from '@dsh-workbench/shared/host';
4
4
  import type { ContextRenderer, UnitHostFace, UnitMountDeps, UnitMountResult, UnitCall } from '@dsh-workbench/shared/host';
5
5
  import { type UnitReadiness } from './unit-readiness';
6
+ export interface UnitFailure {
7
+ readonly id: string;
8
+ readonly reason: string;
9
+ }
6
10
  export interface MountedUnit {
7
11
  readonly face: UnitHostFace;
8
12
  readonly result: UnitMountResult;
@@ -12,15 +16,11 @@ export interface UnitsPlan {
12
16
  readonly mounted: readonly MountedUnit[];
13
17
  readonly routes: RouteTable;
14
18
  readonly gate: UnitMountResult['gate'];
15
- readonly prefs: ReadonlyArray<{
16
- readonly resource: string;
19
+ readonly prefs: ReadonlyArray<NamedEntry & {
17
20
  readonly unit: string;
18
- handle(payload: unknown): unknown;
19
21
  }>;
20
- readonly actions: ReadonlyArray<{
21
- readonly resource: string;
22
+ readonly actions: ReadonlyArray<NamedEntry & {
22
23
  readonly unit: string;
23
- handle(payload: unknown): unknown;
24
24
  }>;
25
25
  readonly provision: UnitMountResult['provision'];
26
26
  readonly contextRenderers: ReadonlyMap<string, ContextRenderer>;
@@ -31,9 +31,15 @@ export interface UnitsPlan {
31
31
  readonly id: string;
32
32
  readonly missing: readonly string[];
33
33
  }>;
34
+ readonly failed: readonly UnitFailure[];
34
35
  readonly warmReads: () => readonly (readonly string[])[];
35
36
  readonly disposeLongTasks: () => void;
36
37
  }
38
+ interface NamedEntry {
39
+ readonly resource: string;
40
+ handle(payload: unknown): unknown;
41
+ }
42
+ export declare function failureReasonOf(error: unknown): string;
37
43
  export declare function mountUnits(faces: readonly UnitHostFace[], deps: {
38
44
  ctx: DshHostContext;
39
45
  logger?: {
@@ -50,3 +56,4 @@ export declare function mountUnits(faces: readonly UnitHostFace[], deps: {
50
56
  readonly bump: (facts: readonly string[]) => void;
51
57
  } | undefined;
52
58
  }): UnitsPlan;
59
+ export {};
@@ -1 +1,2 @@
1
1
  export declare function mockModeOn(search: string): boolean;
2
+ export declare function crashModeOn(search: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -22,7 +22,7 @@
22
22
  "bin": {
23
23
  "dsh-workbench": "./bin/dsh-workbench"
24
24
  },
25
- "//files": "npm 的兜底是 .gitignore —— 而 dist/ 在 .gitignore 里,不显式列出就会**漏掉宿主入口**(exports['.'] 指向它)。发出去之前用 npm pack --dry-run 数一遍 dist/ 有几条。",
25
+ "//files": "npm 的兜底是 .gitignore —— 而 dist/ 在 .gitignore 里,不显式列出就会**漏掉宿主入口**(exports['.'] 指向它)。发出去之前用 npm pack --dry-run 数一遍 dist/ 有几条。skills/ 是 DIY 模式挂给 agent 的指引(经 skill-filesystem 的 customSkillDirs 挂载),漏了不报错、只是 DIY 的 agent 什么都不知道 —— selfcheck 有一条判据盯着它。",
26
26
  "files": [
27
27
  "dist",
28
28
  "client.js",
@@ -30,6 +30,8 @@
30
30
  "icon.svg",
31
31
  "bin",
32
32
  "profile",
33
+ "skills",
34
+ "preset",
33
35
  "scripts/seed-profile.sh",
34
36
  "README.md",
35
37
  "docs/HANDOFF.md",
@@ -40,16 +42,22 @@
40
42
  "description": "个人工作台"
41
43
  },
42
44
  "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。prepublishOnly 走 -w 跑根那一整套。",
45
+ "//dsh.bundle.patch": "一份 preset 就是一个补丁层:它 insert 一行 @deepseek-ai/dsh-agent-preset,config.plugins 是整表插件清单。diy.patch.yml 是**生成的**(scripts/gen-diy-preset.mjs,从 dsh 出厂那份 standard.patch.yml 派生),selfcheck 有一条判据盯着它有没有过期。",
43
46
  "dsh": {
44
47
  "bundle": {
45
- "patch": "./cordis.patch.yml"
48
+ "patch": [
49
+ "./cordis.patch.yml",
50
+ "./preset/diy.patch.yml"
51
+ ]
46
52
  },
47
53
  "client": {
48
54
  "platform": "web",
49
55
  "immediately": true,
50
56
  "inject": [
51
57
  "@deepseek-ai/dsh-client-ui-slots",
52
- "@deepseek-ai/dsh-client-locale"
58
+ "@deepseek-ai/dsh-client-locale",
59
+ "@deepseek-ai/dsh-api-gateway",
60
+ "@deepseek-ai/dsh-api-session-controller"
53
61
  ]
54
62
  }
55
63
  },
@@ -78,7 +86,7 @@
78
86
  "access": "public"
79
87
  },
80
88
  "dependencies": {
81
- "@dsh-workbench/shared": "0.1.9"
89
+ "@dsh-workbench/shared": "0.1.11"
82
90
  },
83
91
  "scripts": {
84
92
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -0,0 +1,156 @@
1
+ # Agent preset workbench-diy:DIY 模式。**这个文件是生成的**,别手改 ——
2
+ # 改 packages/workbench/scripts/gen-diy-preset.mjs 再重跑它。
3
+ #
4
+ # 它从 dsh 出厂那份 standard.patch.yml 派生(整表 plugins 归 dsh 管,手抄会漂),
5
+ # 只叠加三处差分:DIY skill 目录、tool-cordis、plugin-manager 打开。
6
+ # 派生逻辑与差分理由写在生成器开头。
7
+ - insert:
8
+ - id: preset-workbench-diy
9
+ name: '@deepseek-ai/dsh-agent-preset'
10
+ config:
11
+ id: workbench-diy
12
+ name: 工作台 DIY
13
+ description: 用大白话说你要在面板上看到什么、数据从哪来 —— agent 在工作台的工坊里把模块造出来,装上就上板。不用写代码。
14
+ order: 9
15
+ plugins:
16
+ - id: persona
17
+ name: '@deepseek-ai/dsh-persona'
18
+ config:
19
+ suffix: Your working directory is {{cwd}}.
20
+ prefix: You are a coding agent powered by the {{model}} model.
21
+ - id: agent-instructions
22
+ name: '@deepseek-ai/dsh-agent-instructions'
23
+ config:
24
+ maxBytes: 65536
25
+ - id: tool-bash
26
+ name: '@deepseek-ai/dsh-tool-bash'
27
+ disabled: !!js process.platform === 'win32'
28
+ - id: tool-pwsh
29
+ name: '@deepseek-ai/dsh-tool-pwsh'
30
+ disabled: !!js process.platform !== 'win32'
31
+ - id: tool-fs
32
+ name: '@deepseek-ai/dsh-tool-fs'
33
+ - id: tool-fs-search
34
+ name: '@deepseek-ai/dsh-tool-fs-search'
35
+ config:
36
+ sampleOverCapGlobResults: false
37
+ - id: tool-jobs
38
+ name: '@deepseek-ai/dsh-tool-jobs'
39
+ - id: command-goal
40
+ name: '@deepseek-ai/dsh-command-goal'
41
+ - id: tool-goal
42
+ name: '@deepseek-ai/dsh-tool-goal'
43
+ - id: planning
44
+ name: cordis:group
45
+ group: true
46
+ isolate:
47
+ planMode: true
48
+ config:
49
+ - id: plan-mode
50
+ name: '@deepseek-ai/dsh-plan-mode'
51
+ config:
52
+ section: |
53
+ You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
54
+
55
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
56
+
57
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
58
+
59
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
60
+
61
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
62
+
63
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
64
+ - id: compaction
65
+ name: cordis:group
66
+ group: true
67
+ isolate:
68
+ compaction: true
69
+ toolResultPruner: true
70
+ config:
71
+ - id: compaction-basic
72
+ name: '@deepseek-ai/dsh-compaction-basic'
73
+ - id: command-compact
74
+ name: '@deepseek-ai/dsh-command-compact'
75
+ - id: tool-result-pruner
76
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
77
+ config:
78
+ thresholdChars: 8192
79
+ headChars: 4096
80
+ tailChars: 1024
81
+ - id: delegation
82
+ name: cordis:group
83
+ group: true
84
+ isolate:
85
+ workflowEngine: true
86
+ config:
87
+ - id: tool-subagent-control
88
+ name: '@deepseek-ai/dsh-tool-subagent-control'
89
+ - id: tool-subagent-list-agents
90
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
91
+ - id: tool-subagent
92
+ name: '@deepseek-ai/dsh-tool-subagent'
93
+ config:
94
+ provider: spawn
95
+ toolName: subagent
96
+ modelSelectionSettings: true
97
+ backgroundMode: continuable
98
+ - id: tool-subagent-fork
99
+ name: '@deepseek-ai/dsh-tool-subagent'
100
+ config:
101
+ provider: fork
102
+ toolName: subagent_fork
103
+ backgroundMode: continuable
104
+ - id: tool-subagent-codex
105
+ name: '@deepseek-ai/dsh-tool-subagent'
106
+ disabled: true
107
+ config:
108
+ provider: codex
109
+ toolName: subagent_codex
110
+ backgroundMode: one-shot
111
+ maxDepth: provider-managed
112
+ - id: tool-subagent-claude-code
113
+ name: '@deepseek-ai/dsh-tool-subagent'
114
+ disabled: true
115
+ config:
116
+ provider: claude-code
117
+ toolName: subagent_claude_code
118
+ backgroundMode: one-shot
119
+ maxDepth: provider-managed
120
+ - id: workflow-ptc
121
+ name: '@deepseek-ai/dsh-workflow-ptc'
122
+ config:
123
+ provider: spawn
124
+ - id: tool-workflow
125
+ name: '@deepseek-ai/dsh-tool-workflow'
126
+ - id: tool-ralph
127
+ name: '@deepseek-ai/dsh-tool-ralph'
128
+ disabled: true
129
+ config:
130
+ subagentProvider: spawn
131
+ maxRounds: 64
132
+ - id: tool-ask-user
133
+ name: '@deepseek-ai/dsh-tool-ask-user'
134
+ - id: tool-todo
135
+ name: '@deepseek-ai/dsh-tool-todo'
136
+ config:
137
+ allowParallelInProgress: true
138
+ - id: tool-web
139
+ name: '@deepseek-ai/dsh-tool-web'
140
+ config:
141
+ fetch: true
142
+ searchTimeoutMs: 60000
143
+ - id: present
144
+ name: '@deepseek-ai/dsh-tool-present'
145
+ - id: tool-plugin-manager
146
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
147
+ disabled: !!js "!ctx.get('profileContext')"
148
+ - id: tool-cordis
149
+ name: '@deepseek-ai/dsh-tool-cordis'
150
+ - id: skill-filesystem
151
+ name: '@deepseek-ai/dsh-skill-filesystem'
152
+ config:
153
+ customSkillDirs:
154
+ - !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@dsh-workbench/core/package.json')), 'skills')
155
+ - id: tool-skill
156
+ name: '@deepseek-ai/dsh-tool-skill'
@@ -168,6 +168,122 @@ else
168
168
  echo "→ 依赖已齐备,跳过安装"
169
169
  fi
170
170
 
171
+ # ── 第 6 步:建 DIY 工坊 ────────────────────────────────────────────────
172
+ #
173
+ # 工坊是 DIY 模式的落点:agent 在那里造模块(一个模块一个目录),
174
+ # 而 **DIY 会话的 cwd 就是它** —— dsh 的沙箱把"会话 cwd"当作 workspace-write 的边界,
175
+ # 所以源码写在工坊里天然放行,不需要给用户开 danger-full-access。
176
+ #
177
+ # 为什么它是一个 pnpm workspace 而不是随便一个目录:模块要构建就得有
178
+ # module-tools / core / shared / react / typescript / vitest;把它做成 workspace 根,
179
+ # 依赖只装一次,每个模块只是 packages/ 下多一个目录。而且 module-tools 的发现逻辑
180
+ # 认 `pnpm-workspace.yaml`,于是模块单独跑 check 时能找到同仓的外壳(kit / standalone 两条判据
181
+ # 才不会静默跳过 —— 跳过等于没验)。
182
+ #
183
+ # 两个身份同 bin/dsh-workbench:**仓库副本**把三个 @dsh-workbench/* 用 link 指进本仓
184
+ # (改工具链立刻生效),**安装包副本**从源上按版本装。
185
+ DIY_DIR="$DSH_HOME/workbench/diy"
186
+ mkdir -p "$DIY_DIR/packages"
187
+
188
+ if [ -n "$GIT_TOP" ]; then
189
+ CORE_SPEC="link:$REPO_DIR"
190
+ TOOLS_SPEC="link:$(cd "$REPO_DIR/../module-tools" && pwd)"
191
+ SHARED_SPEC="link:$(cd "$REPO_DIR/../shared" && pwd)"
192
+ else
193
+ CORE_SPEC="@dsh-workbench/core@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
194
+ TOOLS_SPEC="@dsh-workbench/module-tools@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
195
+ SHARED_SPEC="@dsh-workbench/shared@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
196
+ fi
197
+
198
+ # 工坊必须**钉住自己那一代 pnpm**(与仓库根钉的同一个)。
199
+ #
200
+ # 不钉的话它用用户机器上那个 pnpm —— 实测这台是 11.21.0,而仓库钉的是 10.32.1,
201
+ # 而**两代的设置住址与键名都不一样**:`store-dir` 在 pnpm 10 从 `.npmrc` 读、
202
+ # 在 pnpm 11 只认 `pnpm-workspace.yaml` 里的 `storeDir`;放行依赖构建的键
203
+ # 在 pnpm 10 是 `onlyBuiltDependencies`、11 是 `allowBuilds`(11 还会往
204
+ # pnpm-workspace.yaml 里写一行 `esbuild: set this to true or false` 占位)。
205
+ # 结果是同一份工坊配置在这台机器上对、在另一台上错,而且错法都是静默或半静默的。
206
+ # 钉了之后两边一致,且这一整轮验证过的就是这一代。
207
+ PNPM_PIN=""
208
+ if [ -n "$GIT_TOP" ] && [ -f "$GIT_TOP/package.json" ]; then
209
+ PNPM_PIN="$(node -p "require('$GIT_TOP/package.json').packageManager || ''" 2>/dev/null || true)"
210
+ fi
211
+ PNPM_PIN="${PNPM_PIN:-pnpm@10.32.1}"
212
+
213
+ if [ -f "$DIY_DIR/package.json" ]; then
214
+ echo "→ DIY 工坊已存在,跳过建骨架(要重来就先删掉 $DIY_DIR)"
215
+ else
216
+ echo "→ 建 DIY 工坊 $DIY_DIR"
217
+ cat >"$DIY_DIR/package.json" <<EOF
218
+ {
219
+ "name": "dsh-workbench-diy",
220
+ "version": "0.0.0",
221
+ "private": true,
222
+ "description": "DIY 工坊:工作台模块在这里造。这个目录由 dsh-workbench 的 setup 生成,手改会被覆盖。",
223
+ "packageManager": "$PNPM_PIN",
224
+ "devDependencies": {
225
+ "@dsh-workbench/core": "$CORE_SPEC",
226
+ "@dsh-workbench/module-tools": "$TOOLS_SPEC",
227
+ "@dsh-workbench/shared": "$SHARED_SPEC",
228
+ "@types/node": "^24.19.0",
229
+ "@types/react": "^19.3.0",
230
+ "esbuild": "^0.28.2",
231
+ "react": "^19.3.0",
232
+ "typescript": "5.9",
233
+ "vitest": "^5.0.2"
234
+ }
235
+ }
236
+ EOF
237
+ cat >"$DIY_DIR/pnpm-workspace.yaml" <<'EOF'
238
+ packages:
239
+ - "packages/*"
240
+
241
+ # esbuild 的构建脚本要放行:pnpm 默认不跑依赖的 postinstall,而模块的构建就是 esbuild。
242
+ # 不放行的话每次 install 都会打一行
243
+ # `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.2` —— 看着像失败,
244
+ # 其实退出码是 0(实测踩过)。
245
+ # 钉在 10.32.1 之后这个键名是对的(同一代 pnpm 也从 package.json 的 `pnpm` 段读它,
246
+ # 仓库根就是这么放的);换 pnpm 11 会变成 `allowBuilds`,所以上面那句「钉住 pnpm」是前提。
247
+ onlyBuiltDependencies:
248
+ - esbuild
249
+ EOF
250
+ cat >"$DIY_DIR/.npmrc" <<EOF
251
+ # 工坊自己的 store 与 cache。
252
+ #
253
+ # 为什么必须指进来:agent 的 shell 跑在**会话沙箱**里,可写根就是会话 cwd(= 这个目录)。
254
+ # 而 pnpm 默认的 store(例如 ~/Library/pnpm/store/v10)与 npm 默认的 cache(~/.npm)
255
+ # 都在可写根**之外** —— 不指进来的话,agent 在会话里跑一次 pnpm install 会被沙箱拒(EPERM),
256
+ # 而"给模块加一个依赖"只有这一条路。setup 时的安装由人跑,不受沙箱限制,但用的也是这份配置,
257
+ # 于是装一次之后 agent 那边也能继续装。
258
+ store-dir=$DIY_DIR/.pnpm-store
259
+ cache-dir=$DIY_DIR/.npm-cache
260
+ EOF
261
+ cat >"$DIY_DIR/README.md" <<'EOF'
262
+ # DIY 工坊
263
+
264
+ 工作台模块在这里造。**一个模块一个目录**,都住在 `packages/` 下。
265
+
266
+ - 这个目录是「DIY 会话」的工作目录 —— 在对话里让 agent 造模块,它就写在这里。
267
+ - 依赖已经装好(`@dsh-workbench/core` / `module-tools` / `shared` / react / typescript / vitest)。
268
+ - 造一个新模块:`dsh-wb-module init <名字> --dir packages/module-<名字>`
269
+ - 每个模块自己的四件事:`pnpm build` / `pnpm typecheck` / `pnpm test` / `pnpm selfcheck`,
270
+ 外加 `dsh-wb-module check` 的十条判据。
271
+
272
+ 不要手改这个目录下的 `package.json` / `pnpm-workspace.yaml` / `.npmrc`:
273
+ 它们是 `dsh-workbench setup` 生成的,模块要加依赖就加在**模块自己**的 `package.json` 里。
274
+ EOF
275
+ fi
276
+
277
+ if [ -d "$DIY_DIR/node_modules" ]; then
278
+ echo "→ 工坊依赖已装,跳过"
279
+ elif ! command -v pnpm >/dev/null 2>&1; then
280
+ echo "→ ⚠️ PATH 上没有 pnpm,工坊依赖没装。装好 pnpm 后跑一次:" >&2
281
+ echo " cd $DIY_DIR && pnpm install" >&2
282
+ else
283
+ echo "→ 装工坊依赖(第一次会下载 react / typescript / vitest,几百 MB,之后不再下)"
284
+ (cd "$DIY_DIR" && pnpm install 2>&1 | tail -6)
285
+ fi
286
+
171
287
  # 装 bundle 的目标:开发态是这个仓库(link),安装包态是源上那个包。
172
288
  # 判据是「git 顶层在不在」—— npm pack 的产物里不含 .git(见 package.json 的 files)。
173
289
  # ⚠️ 不能判 `$REPO_DIR/.git`:workspace 之后 .git 在仓库根(同 bin/dsh-workbench)。