@dsh-workbench/core 0.1.15 → 0.1.16
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/README.md +19 -0
- package/bin/dsh-workbench +5 -1
- package/cordis.patch.yml +22 -0
- package/dist/host/index.js +176 -2
- package/dist/types/shared/api/host/runtime-seed.d.ts +40 -0
- package/dist/types/shared/api/host/runtime-seed.test.d.ts +1 -0
- package/docs/HANDOFF.md +82 -44
- package/docs/OPERATIONS.md +22 -1
- package/package.json +5 -4
- package/scripts/seed-workshop.sh +216 -0
package/README.md
CHANGED
|
@@ -36,6 +36,23 @@ npx @dsh-workbench/core # ② 起工作台,默认 :3081
|
|
|
36
36
|
|
|
37
37
|
打开 <http://localhost:3081> 就是工作台。你原来那份 `dsh web` 继续用 `:3080`,两边互不影响。
|
|
38
38
|
|
|
39
|
+
## 另一种装法:就在你那份 `dsh web` 里装(单 profile)
|
|
40
|
+
|
|
41
|
+
不想多一个 profile、多一个端口、多一个进程?那就**在 `dsh web` 里装**——
|
|
42
|
+
侧栏 **Plugins → Add plugin**,填 `@dsh-workbench/core`(包在公网 npm 上,默认源就能解析),
|
|
43
|
+
**Install → Enable now**,然后**刷新一次页面**。左栏换成工作台,右侧多出面板列。
|
|
44
|
+
|
|
45
|
+
- 装的是**你现在这个 profile**:会话、存储、附件接着用你原来的(**不分家**);
|
|
46
|
+
面板与模块配置照旧落在 `~/.dsh/workbench/**`。
|
|
47
|
+
- 外壳会**自己把缺的那几件补齐**(建隔离 opencli 的入口、把 `dsh-wb-module` 挂上 PATH、
|
|
48
|
+
后台把 DIY 工坊骨架建起来)—— 不需要跑 `setup`、也不需要启动器。后台脚本的输出在
|
|
49
|
+
`~/.dsh/workbench/seed.log`(DIY 工坊的依赖是几百 MB,第一次启动在后台下)。
|
|
50
|
+
- 第一次打开照旧走启动体检,自己装隔离 opencli 运行时;装不上时给一条能照抄到终端的命令。
|
|
51
|
+
- 与 `setup` 那条路的差别:**没有独立端口/进程**、**会话不隔离**、**改外壳源码不热重载**,
|
|
52
|
+
DIY 工坊的依赖是第一次启动在后台装的。取舍与理由见
|
|
53
|
+
[ADR-0037](https://github.com/code-farmer-i/dsh-workbench/blob/main/docs/adr/0037-workbench-installs-into-any-profile.md)。
|
|
54
|
+
- 想回到"两条实例并存",就删掉这个包(Plugins 页里卸载),再走上面的 `setup` 那条路 —— 数据都还在。
|
|
55
|
+
|
|
39
56
|
## 首次打开会自己做什么
|
|
40
57
|
|
|
41
58
|
- **装 opencli 的隔离运行时**:启动体检自己装,落在 `~/.dsh/workbench/opencli/`,
|
|
@@ -61,6 +78,8 @@ npx @dsh-workbench/core # ② 起工作台,默认 :3081
|
|
|
61
78
|
|
|
62
79
|
## 和 `dsh web` 的关系
|
|
63
80
|
|
|
81
|
+
(下面说的是 `setup` 那条路;装在 web 自己的 profile 里那条见上一节。)
|
|
82
|
+
|
|
64
83
|
- **另一个 profile、另一个端口、另一个进程**:`workbench` profile 在 `~/.dsh/profiles/workbench`;
|
|
65
84
|
会话 / 存储 / 附件 / 模块配置全走 `~/.dsh/workbench/**`,与 web **数据分家**。只有一样有意共享:
|
|
66
85
|
API 凭据(`~/.dsh/.credentials.yaml`,一机一把 key)。
|
package/bin/dsh-workbench
CHANGED
|
@@ -258,8 +258,12 @@ fi
|
|
|
258
258
|
# (症状:启动体检永远说"隔离运行时没装好",而它其实是没权限跑)。
|
|
259
259
|
# 所以这里写一个**我们自己拥有**的转发脚本,x 位由 chmod 当场给。
|
|
260
260
|
# ⚠️ 先 rm -f:旧版本留下的软链还在时,`>` 会顺着软链把真脚本覆盖掉。
|
|
261
|
+
# ⚠️ DSH_HOME 必须**钉在命令里**:执行缝不继承它,而 bin/opencli-isolated 用
|
|
262
|
+
# `${DSH_HOME:-$HOME/.dsh}` 推根 —— 不钉的话,DSH_HOME 不是默认值的机器上,
|
|
263
|
+
# 隔离运行时会被认成用户真家目录那一份(实测:同一条路上的 seed-workshop.sh
|
|
264
|
+
# 就是这样把入口写进了 ~/.dsh,见 ADR-0037 的"执行缝不继承 DSH_HOME")。
|
|
261
265
|
rm -f "$OPENCLI_ROOT/bin/opencli"
|
|
262
|
-
printf '#!/bin/sh\nexec bash %q "$@"\n' "$DSH_WORKBENCH_ROOT/bin/opencli-isolated" \
|
|
266
|
+
printf '#!/bin/sh\nexec env DSH_HOME=%q bash %q "$@"\n' "$DSH_HOME" "$DSH_WORKBENCH_ROOT/bin/opencli-isolated" \
|
|
263
267
|
>"$OPENCLI_ROOT/bin/opencli"
|
|
264
268
|
chmod +x "$OPENCLI_ROOT/bin/opencli"
|
|
265
269
|
export DSH_WORKBENCH_OPENCLI="$OPENCLI_ROOT/bin/opencli"
|
package/cordis.patch.yml
CHANGED
|
@@ -62,6 +62,28 @@
|
|
|
62
62
|
# schemastery 的 Config 不吞未知键(已实测),所以这个选项会原样传进 chokidar。
|
|
63
63
|
followSymlinks: false
|
|
64
64
|
|
|
65
|
+
# ── 工作台运行时的**可写根**:让外壳在**任意 profile** 里自足 ──────────────
|
|
66
|
+
#
|
|
67
|
+
# 这一行与 `--patch profile/sandbox.patch.yml` 是同一件事(启动器每次启动都叠那一层)。
|
|
68
|
+
# 为什么这里还要再来一份:外壳现在可以被**直接装进别人的 profile**(dsh web 的
|
|
69
|
+
# Plugins 页 → Add plugin → `@dsh-workbench/core`)。那条路上没有启动器,
|
|
70
|
+
# 而少了这一行,插件发起的命令(启动体检装隔离 opencli、市场装 opencli 插件、
|
|
71
|
+
# opencli 自己的 trace / browser-profile 写入)全都撞文件沙箱 —— 可写根是
|
|
72
|
+
# dsh-base 出厂那句 `process.cwd()`,`$DSH_HOME/workbench` 不在它下面。
|
|
73
|
+
#
|
|
74
|
+
# 为什么放**组合层**而不是让宿主自己改策略:`sandboxPolicy` 的 workspaceRoot 是
|
|
75
|
+
# 启动时读的部署策略,插件没有正当入口改它(改服务字段是绕过策略,不是声明策略)。
|
|
76
|
+
# bundle 层正好在 profile 用户层**下面** —— 所以"装了就生效"与"用户自己写一行就
|
|
77
|
+
# 覆盖它"同时成立,这正是这一层该有的位置。
|
|
78
|
+
#
|
|
79
|
+
# 代价写在明处(ADR-0037):这个 profile 里**无会话的**插件命令,可写根从
|
|
80
|
+
# `process.cwd()` 变成 `$DSH_HOME/workbench`。有会话的命令不受影响(会话用自己的
|
|
81
|
+
# cwd),工作台自己的命令本来也只该写这一棵树。
|
|
82
|
+
- id: sandbox-policy
|
|
83
|
+
config:
|
|
84
|
+
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
|
|
85
|
+
workspaceRoot: !!js dshHomePath('workbench')
|
|
86
|
+
|
|
65
87
|
- insert:
|
|
66
88
|
- id: workbench
|
|
67
89
|
name: '@dsh-workbench/core'
|
package/dist/host/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import fs, { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import path, { join, resolve, sep } from "node:path";
|
|
2
|
+
import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path, { delimiter, dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
//#region ../shared/dist/config/constants.js
|
|
5
7
|
var PLUGIN_ID = "@dsh-workbench/core";
|
|
6
8
|
var API_PREFIX = "/workbench/api";
|
|
@@ -4028,6 +4030,153 @@ function assembleHostUnits(registry) {
|
|
|
4028
4030
|
for (const face of HOST_UNITS) registry.register(face);
|
|
4029
4031
|
}
|
|
4030
4032
|
//#endregion
|
|
4033
|
+
//#region src/shared/api/host/runtime-seed.ts
|
|
4034
|
+
var WORKSHOP_SKILLS_DIR = "workbench-module-diy";
|
|
4035
|
+
function runtimeSeedPaths(root) {
|
|
4036
|
+
const opencliRoot = join(root, "opencli");
|
|
4037
|
+
const opencliBinDir = join(opencliRoot, "bin");
|
|
4038
|
+
const workshop = join(root, "diy");
|
|
4039
|
+
return {
|
|
4040
|
+
root,
|
|
4041
|
+
entryDir: join(root, "bin"),
|
|
4042
|
+
opencliRoot,
|
|
4043
|
+
opencliBinDir,
|
|
4044
|
+
opencliBin: join(opencliBinDir, "opencli"),
|
|
4045
|
+
opencliHome: join(opencliRoot, "home"),
|
|
4046
|
+
workshop,
|
|
4047
|
+
workshopPackages: join(workshop, "packages"),
|
|
4048
|
+
workshopManifest: join(workshop, "package.json"),
|
|
4049
|
+
skillsLink: join(workshop, ".agents", "skills", WORKSHOP_SKILLS_DIR)
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
function runtimeSeedRootOf(ctx) {
|
|
4053
|
+
const homePath = ctx.get("dshHomePath");
|
|
4054
|
+
if (typeof homePath !== "function") return null;
|
|
4055
|
+
try {
|
|
4056
|
+
return homePath("workbench");
|
|
4057
|
+
} catch {
|
|
4058
|
+
return null;
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
function packageRootOf(moduleUrl) {
|
|
4062
|
+
return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
|
|
4063
|
+
}
|
|
4064
|
+
function isInstalledCopy(packageRoot) {
|
|
4065
|
+
return packageRoot.split(sep).includes("node_modules");
|
|
4066
|
+
}
|
|
4067
|
+
function opencliWrapperScript(packageRoot, home) {
|
|
4068
|
+
const script = join(packageRoot, "bin", "opencli-isolated");
|
|
4069
|
+
return `#!/bin/sh\nexec env DSH_HOME=${quoteShellArg(home)} bash ${quoteShellArg(script)} "$@"\n`;
|
|
4070
|
+
}
|
|
4071
|
+
function workshopSeedPlan(packageRoot, home, root) {
|
|
4072
|
+
const script = join(packageRoot, "scripts", "seed-workshop.sh");
|
|
4073
|
+
const env = { DSH_HOME: home };
|
|
4074
|
+
const log = join(root, "seed.log");
|
|
4075
|
+
return {
|
|
4076
|
+
entry: {
|
|
4077
|
+
file: "bash",
|
|
4078
|
+
args: [script, "entry"],
|
|
4079
|
+
env,
|
|
4080
|
+
log
|
|
4081
|
+
},
|
|
4082
|
+
full: {
|
|
4083
|
+
file: "bash",
|
|
4084
|
+
args: [script],
|
|
4085
|
+
env,
|
|
4086
|
+
log
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
function runDetached(command, onError, spawnImpl = spawn) {
|
|
4091
|
+
let fd = null;
|
|
4092
|
+
try {
|
|
4093
|
+
fd = openSync(command.log, "a");
|
|
4094
|
+
const child = spawnImpl(command.file, [...command.args], {
|
|
4095
|
+
env: {
|
|
4096
|
+
...process.env,
|
|
4097
|
+
...command.env
|
|
4098
|
+
},
|
|
4099
|
+
detached: true,
|
|
4100
|
+
stdio: [
|
|
4101
|
+
"ignore",
|
|
4102
|
+
fd,
|
|
4103
|
+
fd
|
|
4104
|
+
]
|
|
4105
|
+
});
|
|
4106
|
+
child.on("error", (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
|
|
4107
|
+
child.unref();
|
|
4108
|
+
} catch (error) {
|
|
4109
|
+
onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
|
|
4110
|
+
} finally {
|
|
4111
|
+
if (fd !== null) closeQuietly(fd);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
function closeQuietly(fd) {
|
|
4115
|
+
try {
|
|
4116
|
+
closeSync(fd);
|
|
4117
|
+
} catch {
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
function prependPath(entryDir, env = process.env) {
|
|
4122
|
+
const parts = (env.PATH ?? "").split(delimiter).filter((part) => part !== "");
|
|
4123
|
+
if (parts.includes(entryDir)) return false;
|
|
4124
|
+
env.PATH = [entryDir, ...parts].join(delimiter);
|
|
4125
|
+
return true;
|
|
4126
|
+
}
|
|
4127
|
+
function seedRuntimeFiles(options) {
|
|
4128
|
+
const paths = runtimeSeedPaths(options.root);
|
|
4129
|
+
const done = [];
|
|
4130
|
+
try {
|
|
4131
|
+
for (const dir of [
|
|
4132
|
+
paths.entryDir,
|
|
4133
|
+
paths.opencliBinDir,
|
|
4134
|
+
paths.opencliHome,
|
|
4135
|
+
paths.workshopPackages
|
|
4136
|
+
]) mkdirSync(dir, { recursive: true });
|
|
4137
|
+
const wanted = opencliWrapperScript(options.packageRoot, options.home);
|
|
4138
|
+
if (readText(paths.opencliBin) !== wanted) {
|
|
4139
|
+
rmSync(paths.opencliBin, { force: true });
|
|
4140
|
+
writeFileSync(paths.opencliBin, wanted, "utf8");
|
|
4141
|
+
chmodSync(paths.opencliBin, 493);
|
|
4142
|
+
done.push(paths.opencliBin);
|
|
4143
|
+
}
|
|
4144
|
+
if (linkSkills(paths.skillsLink, join(options.packageRoot, "skills", "workbench-module-diy"), options.log)) done.push(paths.skillsLink);
|
|
4145
|
+
} catch (error) {
|
|
4146
|
+
options.log?.(`自举没做完:${String(error)}`);
|
|
4147
|
+
}
|
|
4148
|
+
return done;
|
|
4149
|
+
}
|
|
4150
|
+
function readText(file) {
|
|
4151
|
+
try {
|
|
4152
|
+
return readFileSync(file, "utf8");
|
|
4153
|
+
} catch {
|
|
4154
|
+
return null;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
function readLink(link) {
|
|
4158
|
+
try {
|
|
4159
|
+
return readlinkSync(link);
|
|
4160
|
+
} catch {
|
|
4161
|
+
return null;
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function linkSkills(link, source, log) {
|
|
4165
|
+
if (!existsSync(source) || readLink(link) === source) return false;
|
|
4166
|
+
try {
|
|
4167
|
+
mkdirSync(dirname(link), { recursive: true });
|
|
4168
|
+
rmSync(link, {
|
|
4169
|
+
force: true,
|
|
4170
|
+
recursive: true
|
|
4171
|
+
});
|
|
4172
|
+
symlinkSync(source, link, "dir");
|
|
4173
|
+
return true;
|
|
4174
|
+
} catch (error) {
|
|
4175
|
+
log?.(`DIY 指引没挂上(${link}):${String(error)}`);
|
|
4176
|
+
return false;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
//#endregion
|
|
4031
4180
|
//#region src/shared/api/host/events.ts
|
|
4032
4181
|
var HEARTBEAT_MS = 25e3;
|
|
4033
4182
|
function frame(payload) {
|
|
@@ -5294,9 +5443,34 @@ var inject = ["webServer"];
|
|
|
5294
5443
|
function asArgs(value) {
|
|
5295
5444
|
return typeof value === "object" && value !== null ? value : {};
|
|
5296
5445
|
}
|
|
5446
|
+
function warn(ctx, message) {
|
|
5447
|
+
ctx.logger?.warn?.(`[dsh-workbench] ${message}`);
|
|
5448
|
+
}
|
|
5449
|
+
function seedWorkbenchRuntime(ctx) {
|
|
5450
|
+
const root = runtimeSeedRootOf(ctx);
|
|
5451
|
+
if (root === null) return;
|
|
5452
|
+
const home = dirname(root);
|
|
5453
|
+
const packageRoot = packageRootOf(import.meta.url);
|
|
5454
|
+
const paths = runtimeSeedPaths(root);
|
|
5455
|
+
const log = (message) => warn(ctx, message);
|
|
5456
|
+
for (const file of seedRuntimeFiles({
|
|
5457
|
+
root,
|
|
5458
|
+
home,
|
|
5459
|
+
packageRoot,
|
|
5460
|
+
log
|
|
5461
|
+
})) log(`自举写下 ${file}`);
|
|
5462
|
+
if (prependPath(paths.entryDir)) log(`PATH 前插 ${paths.entryDir}`);
|
|
5463
|
+
const plan = workshopSeedPlan(packageRoot, home, root);
|
|
5464
|
+
runDetached(plan.entry, log);
|
|
5465
|
+
if (isInstalledCopy(packageRoot) && !existsSync(paths.workshopManifest)) {
|
|
5466
|
+
log("DIY 工坊骨架不在:后台建起来(第一次会下载依赖,几百 MB)");
|
|
5467
|
+
runDetached(plan.full, log);
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5297
5470
|
function apply(ctx) {
|
|
5298
5471
|
const facts = createFactLog();
|
|
5299
5472
|
const values = createValueHandoff();
|
|
5473
|
+
seedWorkbenchRuntime(ctx);
|
|
5300
5474
|
const opencli = opencliCommandOf(ctx);
|
|
5301
5475
|
const routes = {
|
|
5302
5476
|
health: healthHandler(ctx, opencli),
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { DshHostContext } from '@dsh-workbench/shared';
|
|
3
|
+
export declare const WORKSHOP_SKILLS_DIR = "workbench-module-diy";
|
|
4
|
+
export type SeedLog = (message: string) => void;
|
|
5
|
+
export interface RuntimeSeedPaths {
|
|
6
|
+
readonly root: string;
|
|
7
|
+
readonly entryDir: string;
|
|
8
|
+
readonly opencliRoot: string;
|
|
9
|
+
readonly opencliBinDir: string;
|
|
10
|
+
readonly opencliBin: string;
|
|
11
|
+
readonly opencliHome: string;
|
|
12
|
+
readonly workshop: string;
|
|
13
|
+
readonly workshopPackages: string;
|
|
14
|
+
readonly workshopManifest: string;
|
|
15
|
+
readonly skillsLink: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RuntimeSeedOptions {
|
|
18
|
+
readonly root: string;
|
|
19
|
+
readonly home: string;
|
|
20
|
+
readonly packageRoot: string;
|
|
21
|
+
readonly log?: SeedLog;
|
|
22
|
+
}
|
|
23
|
+
export declare function runtimeSeedPaths(root: string): RuntimeSeedPaths;
|
|
24
|
+
export declare function runtimeSeedRootOf(ctx: DshHostContext): string | null;
|
|
25
|
+
export declare function packageRootOf(moduleUrl: string): string;
|
|
26
|
+
export declare function isInstalledCopy(packageRoot: string): boolean;
|
|
27
|
+
export declare function opencliWrapperScript(packageRoot: string, home: string): string;
|
|
28
|
+
export interface SeedCommand {
|
|
29
|
+
readonly file: string;
|
|
30
|
+
readonly args: readonly string[];
|
|
31
|
+
readonly env: Readonly<Record<string, string>>;
|
|
32
|
+
readonly log: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function workshopSeedPlan(packageRoot: string, home: string, root: string): {
|
|
35
|
+
readonly entry: SeedCommand;
|
|
36
|
+
readonly full: SeedCommand;
|
|
37
|
+
};
|
|
38
|
+
export declare function runDetached(command: SeedCommand, onError: SeedLog, spawnImpl?: typeof spawn): void;
|
|
39
|
+
export declare function prependPath(entryDir: string, env?: NodeJS.ProcessEnv): boolean;
|
|
40
|
+
export declare function seedRuntimeFiles(options: RuntimeSeedOptions): readonly string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/docs/HANDOFF.md
CHANGED
|
@@ -1,37 +1,55 @@
|
|
|
1
1
|
# 把工作台给别人用
|
|
2
2
|
|
|
3
|
-
工作台是一个 **dsh
|
|
4
|
-
`
|
|
5
|
-
|
|
3
|
+
工作台是一个 **dsh 插件包**,现在发在**公网 npm** 上:`@dsh-workbench/core`
|
|
4
|
+
(实测 `npm view @dsh-workbench/core version` 出号,`npm pack` 的产物里
|
|
5
|
+
`dsh.bundle.patch` / `cordis.patch.yml` / `client.js` / `dist/host/index.js` 都在)。
|
|
6
|
+
所以**对方不需要配任何私有源**就能装。
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
两种给法,先挑一种:
|
|
9
|
+
|
|
10
|
+
| 给法 | 对方要做的事 | 换来的 |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| **A. 装进他自己的 `dsh web`**(最短) | Plugins → Add plugin → `@dsh-workbench/core` → Enable now → **刷新页面** | 就在那份 web 里换成工作台;**同一个 profile、同一份会话**、同一个端口 |
|
|
13
|
+
| **B. 独立实例**(要数据分家就选它) | `npx --yes @dsh-workbench/core@latest setup` → `npx @dsh-workbench/core` | 另一个 profile、另一个端口(:3081),他自己那份 `dsh web` **一行不改** |
|
|
14
|
+
|
|
15
|
+
一条路径一句话:**A = 点一下装(单 profile);B = `setup` 然后起(双 profile)**。
|
|
16
|
+
取舍与代价见 [`docs/adr/0037`](../../../docs/adr/0037-workbench-installs-into-any-profile.md)。
|
|
8
17
|
|
|
9
18
|
## 一、对方机器上要有的
|
|
10
19
|
|
|
11
20
|
| # | 东西 | 为什么 | 怎么有 |
|
|
12
21
|
| --- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
|
|
13
22
|
| 1 | `dsh` | 本体。本仓库在 `0.1.7-rc.2` 上验过 | `npm i -g @deepseek-ai/dsh`(公网 npm 上有) |
|
|
14
|
-
| 2 | `node` + **`pnpm`** | dsh 的 `plugin`
|
|
15
|
-
| 3 | 跑过一次 `dsh web` | `setup` 拿 `~/.dsh/profiles/web`
|
|
16
|
-
| 4 |
|
|
17
|
-
| 5 | Coding 账号 + opencli 登录态 | 面板的数据来自 opencli 的 coding 插件 |
|
|
23
|
+
| 2 | `node` + **`pnpm`** | dsh 的 `plugin` 子命令(以及 web 的 Plugins 页)都是**转发给 pnpm** 的;没有 pnpm 会直接报 `dsh: pnpm was not found` | 各自装 |
|
|
24
|
+
| 3 | 跑过一次 `dsh web` | 走 A 需要它给出那个 profile;走 B 时 `setup` 拿 `~/.dsh/profiles/web` 当模板。没有它会**明确报错**,不会静默建出半个 profile | `dsh web` |
|
|
25
|
+
| 4 | 一个能解析这个包的源 | 包在**公网 npm** 上,默认源即可。只有将来改发内网源时,才要 `npm config set @<scope>:registry …` + `npm login`(带 scope 的包只去 `.npmrc` 里给这个 scope 指的源找) | `npm view @dsh-workbench/core version` 出号即可 |
|
|
26
|
+
| 5 | Coding 账号 + opencli 登录态 | 面板的数据来自 opencli 的 coding 插件 | 见第五节 |
|
|
18
27
|
|
|
19
|
-
##
|
|
28
|
+
## 二、A:装进他自己的 `dsh web`(插件页,一条龙)
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
在 `dsh web`(:3080)里:
|
|
31
|
+
|
|
32
|
+
1. 侧栏 **Plugins → Add plugin**,填 `@dsh-workbench/core`;
|
|
33
|
+
2. **Install → Enable now**;
|
|
34
|
+
3. **刷新一次页面**(客户端 bundle 是页面加载时装进去的)。
|
|
35
|
+
|
|
36
|
+
装完左栏就是工作台、右侧多出面板列。缺的那几件(隔离 opencli 的入口、`dsh-wb-module`、
|
|
37
|
+
DIY 工坊骨架、可写根)由**外壳自己**补齐 —— 不用跑 `setup`,也不用起第二个实例。
|
|
38
|
+
|
|
39
|
+
差别(先跟对方说清):**会话 / 存储不分家**(接着用他原来那份)、**没有独立端口与进程**、
|
|
40
|
+
**改外壳源码不热重载**、DIY 工坊依赖是首次启动在后台装的。
|
|
25
41
|
|
|
26
|
-
|
|
42
|
+
## 三、B:两步装(独立 profile,占 :3081)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# ① 建 profile + 把工作台装进去,然后起(占 :3081,dsh web 继续用 :3080)
|
|
27
46
|
npx --yes @dsh-workbench/core@latest setup
|
|
28
47
|
npx @dsh-workbench/core
|
|
29
48
|
```
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
不设这一行,症状是 `404 Not Found - GET https://registry.npmjs.org/@pagoda%2fdsh-workbench`。
|
|
50
|
+
> 若团队后来把包改发到内网源,这两条命令之前要各加一次:
|
|
51
|
+
> `npm config set @<scope>:registry "$NPM_REGISTRY"` 与 `npm login --registry=$NPM_REGISTRY`。
|
|
52
|
+
> 不设的话症状是 `404 Not Found - GET https://registry.npmjs.org/@<scope>%2F…`。
|
|
35
53
|
|
|
36
54
|
> ⚠️ **0.1.6 的已知缺陷**:`setup` 可能死在
|
|
37
55
|
> `…/@dsh-workbench/core/scripts/seed-profile.sh: Permission denied`(发出去的包里随包脚本丢了
|
|
@@ -76,7 +94,7 @@ Settings → 插件里把 `@dsh-workbench/core` 关掉后,工作台会**退化
|
|
|
76
94
|
要重新同步得重跑 `setup`。有意共享的只有两样:**API 凭据**(`~/.dsh/.credentials.yaml`,
|
|
77
95
|
一机一把 key)和 `$DSH_HOME/cordis.patch.yml`(机器公共层,对所有 profile 实时生效)。
|
|
78
96
|
|
|
79
|
-
##
|
|
97
|
+
## 四、不想用 npx?仓库副本也可以
|
|
80
98
|
|
|
81
99
|
`bin/dsh-workbench` 认得出自己是不是仓库副本(看有没有 `.git`):
|
|
82
100
|
|
|
@@ -88,7 +106,7 @@ git clone <仓库> && cd dsh-workbench && pnpm install
|
|
|
88
106
|
|
|
89
107
|
两种身份装出来的东西**同一个包名**,所以对方以后换成正装也不会打架。
|
|
90
108
|
|
|
91
|
-
##
|
|
109
|
+
## 五、数据通路(面板要有数据)
|
|
92
110
|
|
|
93
111
|
1. **opencli 隔离运行时**。第一次打开工作台,启动体检会**自己装**它(`@jackwener/opencli`,
|
|
94
112
|
公网包),落在 `$DSH_HOME/workbench/opencli/toolchain/`,**与对方本机的 `opencli` 完全隔离**
|
|
@@ -142,35 +160,52 @@ git clone <仓库> && cd dsh-workbench && pnpm install
|
|
|
142
160
|
装的时候 `bin/opencli-isolated` 会把 `~/.npmrc` 指回去,就为了拿到源的 token;
|
|
143
161
|
npm 缓存则一律落在那台机器自己的隔离运行时里(**不碰** `~/.npm`)。
|
|
144
162
|
|
|
145
|
-
##
|
|
163
|
+
## 六、不用管的
|
|
146
164
|
|
|
147
165
|
- **`dsh web` 一行不改**:`./scripts/verify.sh` 是外部证据 —— 比对 web 的 profile 哈希、
|
|
148
166
|
两个 profile 的组合树差异、数据是否分家,而不是自证。
|
|
149
167
|
- 会话 / 存储 / 附件各自独立;工作台是**另一个端口、另一个进程**,可与 `dsh web` 同开。
|
|
150
168
|
- 改工作台设置只落在 `profiles/workbench/cordis.patch.yml`。
|
|
151
169
|
|
|
152
|
-
##
|
|
170
|
+
## 七、发版(发起人)
|
|
153
171
|
|
|
154
172
|
```bash
|
|
155
173
|
cd <仓库根>
|
|
156
|
-
pnpm release:dry #
|
|
157
|
-
pnpm release
|
|
174
|
+
pnpm release:dry # 彩排:门禁 → 选版本号 → 构建 → publish --dry-run;不上传,结束回滚版本号
|
|
175
|
+
pnpm release # 真发:上面那一串 + 逐包上传 + git 收尾(提交 / tag / 推送)
|
|
158
176
|
```
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
不用先 `npm version`:版本号由脚本选(交互,或 `--target 0.2.0`),同步到根与所有包的
|
|
179
|
+
manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
|
|
180
|
+
|
|
181
|
+
**门禁整次发布只跑一遍**(preflight 那次 `pnpm run check`;`--resume` 也跑)。发布走
|
|
182
|
+
`pnpm -r publish --ignore-scripts`:各包的 `prepublishOnly` / `prepack` 是留给**手敲**
|
|
183
|
+
`pnpm publish` 的人兜底的(外壳那份 = 整仓门禁);不关掉它们,pnpm 会在**彩排与真发各再跑
|
|
184
|
+
一遍**整门禁 —— 一次发布就是 3 遍门禁 + 3 遍 vitest + 每个包多两轮构建(实测 pnpm 10.32.1:
|
|
185
|
+
`--dry-run` 一样触发)。来历见
|
|
186
|
+
[`.scratch/dsh-ui-alignment/spec.md`](../../../.scratch/dsh-ui-alignment/spec.md) §18.45。
|
|
187
|
+
|
|
188
|
+
脚本把这几件事变成**硬失败**(都在任何上传之前):
|
|
189
|
+
|
|
190
|
+
- **工作区不干净** → 默认拒(发布会把它们 `git add -A` 一起提交),`--allow-dirty` 才放行;
|
|
191
|
+
- **目标版本必须高于当前版本**;目标版本若在源上**已完整存在**,直接拒(要补发用 `--resume`);
|
|
192
|
+
- **npm 没登录** → 就地拉起 `npm login`;
|
|
193
|
+
- **彩排不过**(`publish --dry-run` 打不出包)→ 零上传退出。
|
|
194
|
+
|
|
195
|
+
只能自己盯、脚本不查的两条:
|
|
161
196
|
|
|
162
197
|
- **`dist/` 在 `.gitignore` 里**:npm 打包的兜底就是 `.gitignore`,不在 `files` 里显式列出
|
|
163
|
-
就会漏掉构建产物 —— 症状不是报错,而是"别人装上后宿主侧起不来"
|
|
164
|
-
|
|
165
|
-
- `private: true` 没摘;
|
|
198
|
+
就会漏掉构建产物 —— 症状不是报错,而是"别人装上后宿主侧起不来"(外壳与 `module-hello` 的
|
|
199
|
+
`files` 都显式列了 `dist` / `client.js`,但发布脚本不核这件事);
|
|
166
200
|
- **随包脚本的可执行位**:`pnpm publish` 只给 `package.json#bin` 点名的那**一个**文件 755,
|
|
167
201
|
其余在 tarball 里一律 644(`npm pack` 会保留,而 git 里的 x 位不进 tarball)——
|
|
168
202
|
所以随包脚本一律不吃 x 位:启动器里 `bash` 呼 `seed-profile.sh`、opencli 那条稳定路径写成
|
|
169
|
-
|
|
203
|
+
自己拥有的转发脚本。这条由外壳 `selfcheck` 的判据钉着(门禁里跑)。
|
|
204
|
+
这类错**本机复现不出来**,只在别人机器上炸
|
|
170
205
|
(0.1.6 就是这么坏在 `seed-profile.sh: Permission denied` 上的,见
|
|
171
206
|
[`.scratch/dsh-ui-alignment/spec.md`](../../../.scratch/dsh-ui-alignment/spec.md) §18.41)。
|
|
172
207
|
|
|
173
|
-
##
|
|
208
|
+
## 八、版本与兼容(实话)
|
|
174
209
|
|
|
175
210
|
`package.json` 的 `peerDependencies` 里**没有** `@deepseek-ai/dsh`,这是有意的:
|
|
176
211
|
dsh 目前是 `0.1.7-rc.2`(预发布),声明范围会让版本不一致的人**装不上**;不声明则是
|
|
@@ -195,29 +230,31 @@ dsh 会用这条做**安装预检**(`evaluatePluginCompatibility`),不满
|
|
|
195
230
|
## 发布(一个脚本,一条命令)
|
|
196
231
|
|
|
197
232
|
```bash
|
|
198
|
-
pnpm release # 门禁 →
|
|
199
|
-
pnpm release:dry #
|
|
233
|
+
pnpm release # 门禁 → 选版本号 → 构建 → 彩排 → 逐包上传 → git 收尾
|
|
234
|
+
pnpm release:dry # 同上,但 publish 只 dry-run:零上传,结束回滚版本号
|
|
200
235
|
```
|
|
201
236
|
|
|
202
|
-
`scripts/release.mjs`
|
|
203
|
-
谁要发、发到哪、要发什么,全部从各包自述清单读,不写死名单:
|
|
237
|
+
`scripts/release.mjs` 是**唯一**的发布入口。发什么、发到哪、谁先谁后,都从各包自述清单读:
|
|
204
238
|
|
|
205
|
-
- **发不发** =
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
|
|
239
|
+
- **发不发** = `packages/*` 一级目录里,非 `private` 且自述了 `publishConfig`(`access` 或 `registry`);
|
|
240
|
+
私有模块必须显式写发布目标这条,由门禁的 `contracts` 判据守着(不许默认落到公网);
|
|
241
|
+
- **发到哪** = 包自己的 `publishConfig.registry`(pnpm 在 recursive publish 里逐包取它),没写的
|
|
242
|
+
落到脚本顶部的 `REGISTRY`(npmjs);本仓现在这 4 个包都发公网,命令一律带 `--access public`。
|
|
243
|
+
注意 `inspectTarget` **只查 npmjs** —— "这个版本发出去没有"对发内网源的包看不准;
|
|
244
|
+
- **什么顺序** = pnpm 自己拓扑排序(`shared` → `core` → 用到它们的模块);
|
|
245
|
+
- **发什么** = pnpm 按各包 `files` / `.gitignore` 打包;**打包清单没人替你核**(见上一章
|
|
246
|
+
「只能自己盯」)。要看清单用 `dsh-wb-module publish`:它只 dry-run、打印清单,不发布。
|
|
209
247
|
|
|
210
|
-
|
|
211
|
-
发布用 `pnpm publish`(不是 `npm publish`):只有 pnpm 会把 `workspace:*` 改写成真实版本,
|
|
248
|
+
发布必须走 `pnpm publish`(不是 `npm publish`):只有 pnpm 会把 `workspace:*` 改写成真实版本,
|
|
212
249
|
`npm pack` / `npm publish` 不会 —— 直接发别人装不上。
|
|
213
250
|
|
|
214
|
-
###
|
|
251
|
+
### 版本同步规则(约定 —— 门禁只守其中一条)
|
|
215
252
|
|
|
216
253
|
| 关系 | 规则 |
|
|
217
254
|
| --- | --- |
|
|
218
|
-
| 模块 ↔ 外壳 | 模块 `peerDependencies['@dsh-workbench/core'] = ^0.1.0`。**`^0.x` 锁次版本**:外壳升到 `0.2.0` 时 `^0.1.0`
|
|
255
|
+
| 模块 ↔ 外壳 | 模块 `peerDependencies['@dsh-workbench/core'] = ^0.1.0`。**`^0.x` 锁次版本**:外壳升到 `0.2.0` 时 `^0.1.0` 不再满足 —— 要把每个模块的范围一起改(或一起升)。门禁只守"peer 依赖存在"(`contracts` 判据),**范围合不合体没人替你算**,靠自觉 |
|
|
219
256
|
| `shared` ↔ 外壳/模块 | 依赖写的是 `workspace:*`,发布时会被改写成**当时那个版本号** → `shared` 必须先发;`shared` 升版后,用到它的包要重新发一次才会带上新范围 |
|
|
220
|
-
| `module-tools` ↔ `init` 模板 | 模板里 `@dsh-workbench/core` / `module-tools` 的范围要跟着本仓版本走,否则 `init`
|
|
257
|
+
| `module-tools` ↔ `init` 模板 | 模板里 `@dsh-workbench/core` / `module-tools` 的范围要跟着本仓版本走,否则 `init` 生成的是过期范围 —— **现在没有任何判据检查它**(上一版 release 脚本查过,换成现在这份之后这条没了),升外壳次版本时记得一起改 |
|
|
221
258
|
|
|
222
259
|
### 中断了怎么续跑(发布是可中断的)
|
|
223
260
|
|
|
@@ -228,7 +265,8 @@ pnpm release:resume # 从现场恢复;**已在源上的包自动跳过**
|
|
|
228
265
|
```
|
|
229
266
|
|
|
230
267
|
- **不带 `--resume` 重跑会被拒**:目标版本必须高于当前版本;源上若已有同名同版本,脚本会说"先提版本号"。这正是为续跑设计的语义(不是 bug)。
|
|
268
|
+
- **`--resume` 照样跑一遍门禁**:断了之后很可能改过包,而门禁整次发布只有 preflight 那一处。真要跳过用 `--skip-gate`(发布不会再补跑一遍)。
|
|
231
269
|
- `pnpm release:dry` 是**彩排**:改版本号 → 构建 → `publish --dry-run` → **回滚本地版本号**,不上传。
|
|
232
270
|
- 发布**成功之后**才做 git 收尾:`git add -A` → `commit -m "chore: 发布版本 v<ver>"` → `tag v<ver>` → `push` → `push origin v<ver>`。git 收尾失败时包已经发出去了,脚本会把该手打的命令打出来,并提示可以 `pnpm release:resume`。
|
|
233
|
-
- 常用旗标:`--target <ver>` 跳出版本选择 · `-y/--yes` 跳过确认 · `--allow-dirty` · `--skip-gate
|
|
271
|
+
- 常用旗标:`--target <ver>` 跳出版本选择 · `-y/--yes` 跳过确认 · `--allow-dirty` · `--skip-gate` / `--skip-rehearsal`(都不推荐) · `--skip-build`(用磁盘上现有产物 —— `prepack` 已关,不再兜底重建)。
|
|
234
272
|
- 依赖:`enquirer`(交互选择版本)与 `semver`(版本比较)是根上的 devDependencies。
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -28,7 +28,28 @@
|
|
|
28
28
|
前提:`dsh` 和 `pnpm` 在 PATH 上(`dsh plugin` 是转发给 pnpm 的),
|
|
29
29
|
且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)。
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
**给别人用**(两条路:插件页装进他自己的 web / 独立实例 `setup`;包在公网 npm 上)见 [`HANDOFF.md`](HANDOFF.md)。
|
|
32
|
+
|
|
33
|
+
### 变体:装进**当前** profile(在 `dsh web` 里装,不起第二个实例)
|
|
34
|
+
|
|
35
|
+
`dsh web` 侧栏 **Plugins → Add plugin** → 填 `@dsh-workbench/core`(公网 npm 上有,默认源即可)
|
|
36
|
+
→ **Install → Enable now** → **刷新页面**。装的就是 `dsh web` 自己那个 profile。
|
|
37
|
+
|
|
38
|
+
**完成判据**:刷新后左栏是工作台的、右侧出现面板列;`$DSH_HOME/workbench/opencli/bin/opencli`
|
|
39
|
+
存在且指向本包 `bin/opencli-isolated`;`$DSH_HOME/workbench/bin/dsh-wb-module` 可执行
|
|
40
|
+
(这三条不再依赖启动器 —— 宿主自举写的)。
|
|
41
|
+
|
|
42
|
+
这条路上没有启动器,缺的几件由**宿主自举**补齐(建目录、写隔离 opencli 入口、前插 `PATH`、
|
|
43
|
+
后台建 DIY 工坊骨架),可写根由 bundle 层那一行 `sandbox-policy` 放行 —— 见
|
|
44
|
+
[ADR-0037](../../../docs/adr/0037-workbench-installs-into-any-profile.md)。
|
|
45
|
+
|
|
46
|
+
自举是**静默**的:它写了什么只在宿主日志里,后台脚本(工坊骨架 / 依赖)的输出在
|
|
47
|
+
`$DSH_HOME/workbench/seed.log`。DIY 那个工坊“怎么都没建起来”时先看这两个地方。
|
|
48
|
+
|
|
49
|
+
差别:**会话 / 存储不隔离**(接着用你原来那份)、**没有独立端口与进程**、
|
|
50
|
+
**改外壳源码不热重载**、DIY 工坊依赖是首次启动在后台装的(几百 MB,失败只落日志)。
|
|
51
|
+
|
|
52
|
+
撤销:Plugins 页卸载 `@dsh-workbench/core`(配置与数据不清);`~/.dsh/workbench/**` 想清就手删。
|
|
32
53
|
|
|
33
54
|
## 日常
|
|
34
55
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
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/ 有几条。skills/ 是 DIY 模式挂给 agent 的指引(经 skill-filesystem 的 customSkillDirs 挂载),漏了不报错、只是 DIY 的 agent 什么都不知道 —— selfcheck 有一条判据盯着它。",
|
|
25
|
+
"//files": "npm 的兜底是 .gitignore —— 而 dist/ 在 .gitignore 里,不显式列出就会**漏掉宿主入口**(exports['.'] 指向它)。发出去之前用 npm pack --dry-run 数一遍 dist/ 有几条。skills/ 是 DIY 模式挂给 agent 的指引(经 skill-filesystem 的 customSkillDirs 挂载),漏了不报错、只是 DIY 的 agent 什么都不知道 —— selfcheck 有一条判据盯着它。scripts/ 下是**随包出货的脚本**:启动器与宿主自举都要跑它们(seed-profile.sh / seed-workshop.sh),漏一个的症状是「仓库里怎么都对、装出去那台静默什么也没做」。",
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
28
|
"client.js",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"skills",
|
|
34
34
|
"preset",
|
|
35
35
|
"scripts/seed-profile.sh",
|
|
36
|
+
"scripts/seed-workshop.sh",
|
|
36
37
|
"README.md",
|
|
37
38
|
"docs/HANDOFF.md",
|
|
38
39
|
"docs/OPERATIONS.md"
|
|
@@ -81,12 +82,12 @@
|
|
|
81
82
|
"vite": "^8.3.1",
|
|
82
83
|
"vitest": "^5.0.2"
|
|
83
84
|
},
|
|
84
|
-
"//publishConfig": "
|
|
85
|
+
"//publishConfig": "只影响**发布这个包**,不影响别人解析依赖。现在发**公网 npm**(scripts/release.mjs 走 registry.npmjs.org),所以这里只有 access;哪天真要改发内网源,地址写在这一段的 registry 里(release.mjs 会优先读它)。",
|
|
85
86
|
"publishConfig": {
|
|
86
87
|
"access": "public"
|
|
87
88
|
},
|
|
88
89
|
"dependencies": {
|
|
89
|
-
"@dsh-workbench/shared": "0.1.
|
|
90
|
+
"@dsh-workbench/shared": "0.1.16"
|
|
90
91
|
},
|
|
91
92
|
"scripts": {
|
|
92
93
|
"build": "node scripts/build.mjs && node scripts/build-types.mjs",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# DIY 工坊:骨架 + 稳定入口(`dsh-wb-module`)。
|
|
4
|
+
#
|
|
5
|
+
# 由两处共用,所以逻辑只有**一份**:
|
|
6
|
+
# · `seed-profile.sh`(= `dsh-workbench setup`)—— 第一次把工坊建起来、装依赖;
|
|
7
|
+
# · `bin/dsh-workbench`(每次启动)—— 入口每次重写一遍(幂等),骨架不在才自愈。
|
|
8
|
+
#
|
|
9
|
+
# 为什么启动器要自愈而不是只提醒:干净机器上 agent 进工坊第一件事就是 build / check,
|
|
10
|
+
# 而"不在 workspace 里"报出来的话指着别处(实测卡在这一步,它读到的像是"模板写错了")。
|
|
11
|
+
# 提醒是对人的;agent 读不到那句提醒。
|
|
12
|
+
#
|
|
13
|
+
# 用法:
|
|
14
|
+
# bash scripts/seed-workshop.sh # 完整:骨架(缺才建)+ overrides + 依赖(缺才装)+ 入口
|
|
15
|
+
# bash scripts/seed-workshop.sh entry # 只写入口(每次启动用;别的都不碰)
|
|
16
|
+
#
|
|
17
|
+
# 幂等:存在的文件不覆盖、依赖在就跳过安装、重复跑不产生副作用。
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
|
22
|
+
SELF="${BASH_SOURCE[0]}"
|
|
23
|
+
while [ -L "$SELF" ]; do
|
|
24
|
+
SELF="$(cd "$(dirname "$SELF")" && pwd -P)/$(readlink "$SELF")"
|
|
25
|
+
done
|
|
26
|
+
REPO_DIR="$(cd "$(dirname "$SELF")/.." && pwd -P)"
|
|
27
|
+
GIT_TOP="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
28
|
+
MODE="${1:-skeleton}"
|
|
29
|
+
|
|
30
|
+
# ── 这份副本到底是"仓库副本"还是"装出来的副本" ──────────────────────────────
|
|
31
|
+
#
|
|
32
|
+
# `git rev-parse --show-toplevel` 只是"往上第一个 .git"——**装出来的副本也可能落在别人的
|
|
33
|
+
# 仓库下面**。实测:`DSH_HOME` 恰好在某个 git 仓库里时(开发机上就是本仓),
|
|
34
|
+
# `<profile>/node_modules/@dsh-workbench/core` 一路往上找到了那个仓库,于是这份副本被当成
|
|
35
|
+
# 仓库副本,去 link 一个不存在的 `../module-tools`,脚本当场死在 `cd` 上 —— 而它是**后台**
|
|
36
|
+
# 被拉起来的(宿主自举 / 启动器),症状就是"工坊怎么都没建起来,一声不响"。
|
|
37
|
+
#
|
|
38
|
+
# 所以认仓库要认**本仓的形状**:根上同时有 packages/workbench 与 packages/module-tools。
|
|
39
|
+
if [ -n "$GIT_TOP" ] && [ -f "$GIT_TOP/packages/workbench/package.json" ] &&
|
|
40
|
+
[ -f "$GIT_TOP/packages/module-tools/package.json" ]; then
|
|
41
|
+
IN_REPO=1
|
|
42
|
+
else
|
|
43
|
+
IN_REPO=0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ── 稳定入口:$DSH_HOME/workbench/bin/dsh-wb-module ──────────────────────────
|
|
47
|
+
#
|
|
48
|
+
# 为什么要它:指引让 agent 写 `pnpm exec dsh-wb-module …`(到处都对,但它依赖 cwd 在工坊里、
|
|
49
|
+
# 且那句 pnpm 前缀每次都要写对)。这里给一个**与 cwd 无关**的同名入口,启动器把它
|
|
50
|
+
# 挂进 PATH —— 于是"在工坊里"和"在别的目录里"都叫得动同一个工具链。
|
|
51
|
+
#
|
|
52
|
+
# 转发脚本而不是软链:同 `bin/opencli-isolated` 那条坑 —— pnpm publish 之后包里的脚本没有 x 位,
|
|
53
|
+
# 软链过去一执行就是 Permission denied。x 位由 chmod 当场给。
|
|
54
|
+
# ⚠️ 先 rm -f:旧版本留下的软链还在时,`>` 会顺着软链把真脚本覆盖掉。
|
|
55
|
+
ENTRY_DIR="$DSH_HOME/workbench/bin"
|
|
56
|
+
mkdir -p "$ENTRY_DIR"
|
|
57
|
+
rm -f "$ENTRY_DIR/dsh-wb-module"
|
|
58
|
+
cat >"$ENTRY_DIR/dsh-wb-module" <<EOF
|
|
59
|
+
#!/bin/sh
|
|
60
|
+
BIN="$DSH_HOME/workbench/diy/node_modules/.bin/dsh-wb-module"
|
|
61
|
+
if [ -x "\$BIN" ]; then exec "\$BIN" "\$@"; fi
|
|
62
|
+
if command -v pnpm >/dev/null 2>&1; then
|
|
63
|
+
exec pnpm --dir "$DSH_HOME/workbench/diy" exec dsh-wb-module "\$@"
|
|
64
|
+
fi
|
|
65
|
+
echo "dsh-wb-module: 工坊的依赖还没装 —— 先跑 dsh-workbench setup" >&2
|
|
66
|
+
exit 127
|
|
67
|
+
EOF
|
|
68
|
+
chmod +x "$ENTRY_DIR/dsh-wb-module"
|
|
69
|
+
|
|
70
|
+
if [ "$MODE" = "entry" ]; then
|
|
71
|
+
echo "→ DIY 工坊入口:$ENTRY_DIR/dsh-wb-module"
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# ── 第 6 步:建 DIY 工坊 ────────────────────────────────────────────────
|
|
76
|
+
#
|
|
77
|
+
# 工坊是 DIY 模式的落点:agent 在那里造模块(一个模块一个目录),
|
|
78
|
+
# 而 **DIY 会话的 cwd 就是它** —— dsh 的沙箱把"会话 cwd"当作 workspace-write 的边界,
|
|
79
|
+
# 所以源码写在工坊里天然放行,不需要给用户开 danger-full-access。
|
|
80
|
+
#
|
|
81
|
+
# 为什么它是一个 pnpm workspace 而不是随便一个目录:模块要构建就得有
|
|
82
|
+
# module-tools / core / shared / react / typescript / vitest;把它做成 workspace 根,
|
|
83
|
+
# 依赖只装一次,每个模块只是 packages/ 下多一个目录。而且 module-tools 的发现逻辑
|
|
84
|
+
# 认 `pnpm-workspace.yaml`,于是模块单独跑 check 时能找到同仓的外壳(kit / standalone 两条判据
|
|
85
|
+
# 才不会静默跳过 —— 跳过等于没验)。
|
|
86
|
+
#
|
|
87
|
+
# 两个身份同 bin/dsh-workbench:**仓库副本**把三个 @dsh-workbench/* 用 link 指进本仓
|
|
88
|
+
# (改工具链立刻生效),**安装包副本**从源上按版本装。
|
|
89
|
+
DIY_DIR="$DSH_HOME/workbench/diy"
|
|
90
|
+
mkdir -p "$DIY_DIR/packages"
|
|
91
|
+
|
|
92
|
+
if [ "$IN_REPO" = "1" ]; then
|
|
93
|
+
CORE_SPEC="link:$REPO_DIR"
|
|
94
|
+
TOOLS_SPEC="link:$(cd "$REPO_DIR/../module-tools" && pwd)"
|
|
95
|
+
SHARED_SPEC="link:$(cd "$REPO_DIR/../shared" && pwd)"
|
|
96
|
+
else
|
|
97
|
+
CORE_SPEC="@dsh-workbench/core@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
|
|
98
|
+
TOOLS_SPEC="@dsh-workbench/module-tools@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
|
|
99
|
+
SHARED_SPEC="@dsh-workbench/shared@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# 工坊必须**钉住自己那一代 pnpm**(与仓库根钉的同一个)。
|
|
103
|
+
#
|
|
104
|
+
# 不钉的话它用用户机器上那个 pnpm —— 实测这台是 11.21.0,而仓库钉的是 10.32.1,
|
|
105
|
+
# 而**两代的设置住址与键名都不一样**:`store-dir` 在 pnpm 10 从 `.npmrc` 读、
|
|
106
|
+
# 在 pnpm 11 只认 `pnpm-workspace.yaml` 里的 `storeDir`;放行依赖构建的键
|
|
107
|
+
# 在 pnpm 10 是 `onlyBuiltDependencies`、11 是 `allowBuilds`(11 还会往
|
|
108
|
+
# pnpm-workspace.yaml 里写一行 `esbuild: set this to true or false` 占位)。
|
|
109
|
+
# 结果是同一份工坊配置在这台机器上对、在另一台上错,而且错法都是静默或半静默的。
|
|
110
|
+
# 钉了之后两边一致,且这一整轮验证过的就是这一代。
|
|
111
|
+
PNPM_PIN=""
|
|
112
|
+
if [ "$IN_REPO" = "1" ] && [ -f "$GIT_TOP/package.json" ]; then
|
|
113
|
+
PNPM_PIN="$(node -p "require('$GIT_TOP/package.json').packageManager || ''" 2>/dev/null || true)"
|
|
114
|
+
fi
|
|
115
|
+
PNPM_PIN="${PNPM_PIN:-pnpm@10.32.1}"
|
|
116
|
+
|
|
117
|
+
if [ -f "$DIY_DIR/package.json" ]; then
|
|
118
|
+
echo "→ DIY 工坊已存在,跳过建骨架(要重来就先删掉 ${DIY_DIR})"
|
|
119
|
+
else
|
|
120
|
+
echo "→ 建 DIY 工坊 $DIY_DIR"
|
|
121
|
+
cat >"$DIY_DIR/package.json" <<EOF
|
|
122
|
+
{
|
|
123
|
+
"name": "dsh-workbench-diy",
|
|
124
|
+
"version": "0.0.0",
|
|
125
|
+
"private": true,
|
|
126
|
+
"description": "DIY 工坊:工作台模块在这里造。这个目录由 dsh-workbench 的 setup 生成,手改会被覆盖。",
|
|
127
|
+
"packageManager": "$PNPM_PIN",
|
|
128
|
+
"devDependencies": {
|
|
129
|
+
"@dsh-workbench/core": "$CORE_SPEC",
|
|
130
|
+
"@dsh-workbench/module-tools": "$TOOLS_SPEC",
|
|
131
|
+
"@dsh-workbench/shared": "$SHARED_SPEC",
|
|
132
|
+
"@types/node": "^24.19.0",
|
|
133
|
+
"@types/react": "^19.3.0",
|
|
134
|
+
"esbuild": "^0.28.2",
|
|
135
|
+
"react": "^19.3.0",
|
|
136
|
+
"typescript": "5.9",
|
|
137
|
+
"vitest": "^5.0.2"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
EOF
|
|
141
|
+
cat >"$DIY_DIR/pnpm-workspace.yaml" <<'EOF'
|
|
142
|
+
packages:
|
|
143
|
+
- "packages/*"
|
|
144
|
+
|
|
145
|
+
# esbuild 的构建脚本要放行:pnpm 默认不跑依赖的 postinstall,而模块的构建就是 esbuild。
|
|
146
|
+
# 不放行的话每次 install 都会打一行
|
|
147
|
+
# `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.2` —— 看着像失败,
|
|
148
|
+
# 其实退出码是 0(实测踩过)。
|
|
149
|
+
# 钉在 10.32.1 之后这个键名是对的(同一代 pnpm 也从 package.json 的 `pnpm` 段读它,
|
|
150
|
+
# 仓库根就是这么放的);换 pnpm 11 会变成 `allowBuilds`,所以上面那句「钉住 pnpm」是前提。
|
|
151
|
+
onlyBuiltDependencies:
|
|
152
|
+
- esbuild
|
|
153
|
+
EOF
|
|
154
|
+
cat >"$DIY_DIR/.npmrc" <<EOF
|
|
155
|
+
# 工坊自己的 store 与 cache。
|
|
156
|
+
#
|
|
157
|
+
# 为什么必须指进来:agent 的 shell 跑在**会话沙箱**里,可写根就是会话 cwd(= 这个目录)。
|
|
158
|
+
# 而 pnpm 默认的 store(例如 ~/Library/pnpm/store/v10)与 npm 默认的 cache(~/.npm)
|
|
159
|
+
# 都在可写根**之外** —— 不指进来的话,agent 在会话里跑一次 pnpm install 会被沙箱拒(EPERM),
|
|
160
|
+
# 而"给模块加一个依赖"只有这一条路。setup 时的安装由人跑,不受沙箱限制,但用的也是这份配置,
|
|
161
|
+
# 于是装一次之后 agent 那边也能继续装。
|
|
162
|
+
store-dir=$DIY_DIR/.pnpm-store
|
|
163
|
+
cache-dir=$DIY_DIR/.npm-cache
|
|
164
|
+
EOF
|
|
165
|
+
cat >"$DIY_DIR/README.md" <<'EOF'
|
|
166
|
+
# DIY 工坊
|
|
167
|
+
|
|
168
|
+
工作台模块在这里造。**一个模块一个目录**,都住在 `packages/` 下。
|
|
169
|
+
|
|
170
|
+
- 这个目录是「DIY 会话」的工作目录 —— 在对话里让 agent 造模块,它就写在这里。
|
|
171
|
+
- 依赖已经装好(`@dsh-workbench/core` / `module-tools` / `shared` / react / typescript / vitest)。
|
|
172
|
+
- 造一个新模块:`dsh-wb-module init <名字> --dir packages/module-<名字>`
|
|
173
|
+
(`dsh-wb-module` 挂在 PATH 上、与 cwd 无关;写成 `pnpm exec dsh-wb-module …` 也对)
|
|
174
|
+
- 每个模块自己的四件事:`pnpm build` / `pnpm typecheck` / `pnpm test` / `pnpm selfcheck`,
|
|
175
|
+
外加 `dsh-wb-module check` 的十条判据。
|
|
176
|
+
|
|
177
|
+
不要手改这个目录下的 `package.json` / `pnpm-workspace.yaml` / `.npmrc`:
|
|
178
|
+
它们是 `dsh-workbench setup` 生成的,模块要加依赖就加在**模块自己**的 `package.json` 里。
|
|
179
|
+
EOF
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# 工坊里只该有**一份** `@dsh-workbench/*` —— 模块自己声明的 `^0.1.x` 也算。
|
|
183
|
+
#
|
|
184
|
+
# 不钉的话 pnpm 会给每个模块**另装一份来源上的包**,于是同一个工坊里两代工具链并存:
|
|
185
|
+
# 模块自己的 `pnpm build` / `pnpm exec dsh-wb-module` 用的是源上那份(模板、判据、报错都是旧的,
|
|
186
|
+
# `init --template diy` 还会直接报"模板不存在"),而工坊根 `.bin/dsh-wb-module` 是 link 到本仓那份;
|
|
187
|
+
# `check --only kit` 比的也会是那份旧外壳(实测:0.1.13 与 0.1.14 两台工具并存,全程一声不响)。
|
|
188
|
+
#
|
|
189
|
+
# 用 pnpm 的 overrides 而不是改模块模板:模板里写死 link 路径会把"装到别人机器上"这条弄坏,
|
|
190
|
+
# overrides 活在**这个工坊**里,模块的 package.json 仍旧是干净的 `^0.1.x`。
|
|
191
|
+
# 放在第一次 `pnpm install` **之前**,这样一次装就是对的;已存在的工坊再跑一次 setup 也会被修好。
|
|
192
|
+
if [ "$IN_REPO" = "1" ] && [ -f "$DIY_DIR/package.json" ]; then
|
|
193
|
+
node - "$DIY_DIR/package.json" "$CORE_SPEC" "$TOOLS_SPEC" "$SHARED_SPEC" <<'NODE'
|
|
194
|
+
const [file, core, tools, shared] = process.argv.slice(2);
|
|
195
|
+
const fs = require('node:fs');
|
|
196
|
+
const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
197
|
+
manifest.pnpm = {
|
|
198
|
+
overrides: {
|
|
199
|
+
'@dsh-workbench/core': core,
|
|
200
|
+
'@dsh-workbench/module-tools': tools,
|
|
201
|
+
'@dsh-workbench/shared': shared,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
205
|
+
NODE
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if [ -d "$DIY_DIR/node_modules" ]; then
|
|
209
|
+
echo "→ 工坊依赖已装,跳过"
|
|
210
|
+
elif ! command -v pnpm >/dev/null 2>&1; then
|
|
211
|
+
echo "→ ⚠️ PATH 上没有 pnpm,工坊依赖没装。装好 pnpm 后跑一次:" >&2
|
|
212
|
+
echo " cd $DIY_DIR && pnpm install" >&2
|
|
213
|
+
else
|
|
214
|
+
echo "→ 装工坊依赖(第一次会下载 react / typescript / vitest,几百 MB,之后不再下)"
|
|
215
|
+
(cd "$DIY_DIR" && pnpm install 2>&1 | tail -6)
|
|
216
|
+
fi
|