@crazx/dsh 0.1.0-rc.7.zw.2
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +47 -0
- package/README.zh.md +47 -0
- package/config/agent-presets/code/agent.cordis.yml +262 -0
- package/config/agent-presets/code/preset.yml +3 -0
- package/config/agent-presets/cordis/agent.cordis.yml +262 -0
- package/config/agent-presets/cordis/preset.yml +3 -0
- package/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md +420 -0
- package/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +154 -0
- package/config/agent-presets/minimal/agent.cordis.yml +62 -0
- package/config/agent-presets/minimal/preset.yml +3 -0
- package/config/agent-presets/standard/agent.cordis.yml +251 -0
- package/config/agent-presets/standard/preset.yml +3 -0
- package/lib/bin.js +185 -0
- package/lib/dump-config-D-jtgwY3.js +52 -0
- package/lib/plugin-9h8shc4d.js +129 -0
- package/lib/profile-boot-BnJoK_kl.js +2 -0
- package/lib/profile-boot-DG5t9aNs.js +283 -0
- package/lib/web-log-CKn_UFWp.js +83 -0
- package/lib/web-log-DZLBFfre.js +96 -0
- package/package.json +101 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { PROFILE_PATCH_FILENAME, boot, composeEntries, healProfilesModuleFallback, installFailLoud, loadOptionalPatches, loadOverlayPatches, loadProfile, watchUserPatches } from "@deepseek-ai/dsh-app-boot";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
6
|
+
import { DSH_LAUNCH_ENVIRONMENT_KEY } from "@deepseek-ai/dsh-launch-environment";
|
|
7
|
+
import { provideCmdline } from "@deepseek-ai/dsh-cmdline";
|
|
8
|
+
//#region lib/types/process-shutdown.js
|
|
9
|
+
/** Bounded, escalating process shutdown for the long-lived CLI surfaces. */
|
|
10
|
+
/** Maximum grace allowed for the application tree to dispose before process exit. */
|
|
11
|
+
const PROCESS_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
12
|
+
/**
|
|
13
|
+
* Create one process-exit controller around an application disposer.
|
|
14
|
+
* @param dispose - Whole-application teardown that resolves at quiescence.
|
|
15
|
+
* @param forceExit - Function that exits the process immediately, replaceable by tests.
|
|
16
|
+
* @param complete - Function that records the natural completion code, replaceable by tests.
|
|
17
|
+
* @param timeoutMs - Grace before forced exit, replaceable by tests.
|
|
18
|
+
* @returns A controller whose normal calls coalesce and whose repeated signal call escalates.
|
|
19
|
+
*/
|
|
20
|
+
function createProcessShutdown(dispose, forceExit = (code) => {
|
|
21
|
+
process.exit(code);
|
|
22
|
+
}, complete = (code) => {
|
|
23
|
+
process.exitCode = code;
|
|
24
|
+
}, timeoutMs = PROCESS_SHUTDOWN_TIMEOUT_MS) {
|
|
25
|
+
let pending;
|
|
26
|
+
let timeout;
|
|
27
|
+
let completed = false;
|
|
28
|
+
let forceExited = false;
|
|
29
|
+
const clearExitTimeout = () => {
|
|
30
|
+
/* v8 ignore else -- shutdown() arms the timer before any asynchronous exit path can run. */
|
|
31
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
32
|
+
};
|
|
33
|
+
const forceExitOnce = (code) => {
|
|
34
|
+
if (forceExited) return;
|
|
35
|
+
forceExited = true;
|
|
36
|
+
clearExitTimeout();
|
|
37
|
+
forceExit(code);
|
|
38
|
+
};
|
|
39
|
+
const completeOnce = (code) => {
|
|
40
|
+
if (completed || forceExited) return;
|
|
41
|
+
completed = true;
|
|
42
|
+
clearExitTimeout();
|
|
43
|
+
complete(code);
|
|
44
|
+
};
|
|
45
|
+
const start = (code, forceAfterDispose) => {
|
|
46
|
+
if (pending !== void 0) return pending;
|
|
47
|
+
timeout = setTimeout(() => {
|
|
48
|
+
forceExitOnce(code);
|
|
49
|
+
}, timeoutMs);
|
|
50
|
+
pending = Promise.resolve().then(dispose).then(() => {
|
|
51
|
+
if (forceAfterDispose) forceExitOnce(code);
|
|
52
|
+
else completeOnce(code);
|
|
53
|
+
}, () => {
|
|
54
|
+
forceExitOnce(code);
|
|
55
|
+
});
|
|
56
|
+
return pending;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
shutdown(code) {
|
|
60
|
+
return start(code, false);
|
|
61
|
+
},
|
|
62
|
+
interrupt(code) {
|
|
63
|
+
if (pending !== void 0) {
|
|
64
|
+
forceExitOnce(code);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
start(code, true);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region lib/types/profile-boot.js
|
|
73
|
+
/**
|
|
74
|
+
* Shared profile boot for every `dsh` surface: resolve the profile, stack its
|
|
75
|
+
* patch layers (bundle layers in `dsh.profile.bundles` order, the profile's
|
|
76
|
+
* own `cordis.patch.yml`, `--patch` overlays, the telemetry switch), mount the
|
|
77
|
+
* tree over the profile's empty root config, keep the profile patch layer
|
|
78
|
+
* live, and wire fail-loud plus bounded shutdown.
|
|
79
|
+
*
|
|
80
|
+
* App flags are not the launcher's business: the invocation's inner arguments
|
|
81
|
+
* are provided to the tree through `ctx.cmdlineArgs`, where any injected app
|
|
82
|
+
* plugin may read the same immutable snapshot.
|
|
83
|
+
* @module @deepseek-ai/dsh/profile-boot
|
|
84
|
+
*/
|
|
85
|
+
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
|
|
86
|
+
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL("../config/agent-presets/", import.meta.url));
|
|
87
|
+
const NAME = "dsh";
|
|
88
|
+
/**
|
|
89
|
+
* The home-level user patch layer (`$DSH_HOME/cordis.patch.yml`), applied
|
|
90
|
+
* over every profile's own layer. Resolved per call, not at module load:
|
|
91
|
+
* `$DSH_HOME` may be set by the test or launcher after import.
|
|
92
|
+
* @returns the absolute patch-file path.
|
|
93
|
+
*/
|
|
94
|
+
function homePatchPath() {
|
|
95
|
+
return join(resolveDshHome(), PROFILE_PATCH_FILENAME);
|
|
96
|
+
}
|
|
97
|
+
/** Absolute path of this dsh installation's package.json (both anchors: src/ and lib/ sit one level under apps/cli). */
|
|
98
|
+
const INSTALL_ANCHOR = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
99
|
+
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets. */
|
|
100
|
+
const TELEMETRY_ROW_ID = "session-telemetry-otel";
|
|
101
|
+
/** The empty root entry list every profile tree patches over. */
|
|
102
|
+
const PROFILE_ROOT_CONFIG = `# dsh profile root — an empty entry list. The tree is composed as patches:
|
|
103
|
+
# each bundle in package.json's dsh.profile.bundles, then cordis.patch.yml, then any
|
|
104
|
+
# --patch overlays. Edit cordis.patch.yml, not this file.
|
|
105
|
+
[]
|
|
106
|
+
`;
|
|
107
|
+
/** Root config filename inside a profile directory. */
|
|
108
|
+
const PROFILE_ROOT_FILENAME = "cordis.yml";
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
|
111
|
+
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
|
|
112
|
+
* off-by-mistake over on-by-mistake. A composition without the telemetry row
|
|
113
|
+
* exports nothing, so the switch is then trivially satisfied and no patch is
|
|
114
|
+
* generated — custom profiles need not mount telemetry to run with the
|
|
115
|
+
* switch set.
|
|
116
|
+
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
|
117
|
+
* @param hasRow - whether the composition carries the telemetry row.
|
|
118
|
+
* @returns the disable patch, or `undefined` when no hard-disable patch is required.
|
|
119
|
+
*/
|
|
120
|
+
function resolveTelemetryPatch(disabledEnv, hasRow) {
|
|
121
|
+
if ((disabledEnv ?? "") === "" || !hasRow) return void 0;
|
|
122
|
+
return {
|
|
123
|
+
id: TELEMETRY_ROW_ID,
|
|
124
|
+
disabled: true
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Load a resolved profile for `name`: heal the shared module fallback, then
|
|
129
|
+
* (re)write the empty root config. The root is always rewritten: the whole
|
|
130
|
+
* composition is patch layers, and the vendored Loader's tree write-back (a
|
|
131
|
+
* plugin self-disposing persists the current tree) can bake composed rows
|
|
132
|
+
* into this file — which would duplicate every bundle insert on the next
|
|
133
|
+
* boot. The file exists on disk only because the Loader needs a real include
|
|
134
|
+
* root to anchor `baseUrl` at the profile directory (the config dump anchors
|
|
135
|
+
* on the same file, so both compose over the identical base).
|
|
136
|
+
* @param name - the profile name.
|
|
137
|
+
* @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
|
|
138
|
+
* @returns the loaded profile.
|
|
139
|
+
*/
|
|
140
|
+
function prepareProfile(name, userLayer = true) {
|
|
141
|
+
healProfilesModuleFallback(INSTALL_ANCHOR);
|
|
142
|
+
const profile = loadProfile(NAME, name, INSTALL_ANCHOR, void 0, { userLayer });
|
|
143
|
+
writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG);
|
|
144
|
+
return profile;
|
|
145
|
+
}
|
|
146
|
+
/** The full patch stack of one composed profile, in application order. */
|
|
147
|
+
function allPatches(composed) {
|
|
148
|
+
return [
|
|
149
|
+
...composed.bundlePatches,
|
|
150
|
+
...composed.profile.patches,
|
|
151
|
+
...composed.homePatches,
|
|
152
|
+
...composed.overlays
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Load `name` and compose its effective patch stack: bundle layers in
|
|
157
|
+
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
|
|
158
|
+
* platform on its own rows), the profile's user layer, the home-level user
|
|
159
|
+
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
|
|
160
|
+
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
|
|
161
|
+
* then the telemetry switch.
|
|
162
|
+
* @param name - the profile name.
|
|
163
|
+
* @param patchFiles - `--patch` overlay paths, in argv order.
|
|
164
|
+
* @returns the profile, its patch layers, and the composed row index.
|
|
165
|
+
*/
|
|
166
|
+
function composeProfile(name, patchFiles) {
|
|
167
|
+
const profile = prepareProfile(name);
|
|
168
|
+
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? [];
|
|
169
|
+
const overlays = patchFiles.flatMap((file) => loadOverlayPatches(NAME, resolve(file)));
|
|
170
|
+
const bundlePatches = profile.layers.flatMap((layer) => layer.patches);
|
|
171
|
+
const rows = /* @__PURE__ */ new Map();
|
|
172
|
+
for (const row of composeEntries([
|
|
173
|
+
bundlePatches,
|
|
174
|
+
profile.patches,
|
|
175
|
+
homePatches,
|
|
176
|
+
overlays
|
|
177
|
+
])) if (typeof row.id === "string") rows.set(row.id, row);
|
|
178
|
+
const composedOverlays = [...overlays];
|
|
179
|
+
if (rows.has("agent-presets")) composedOverlays.push({
|
|
180
|
+
id: "agent-presets",
|
|
181
|
+
config: {
|
|
182
|
+
...rows.get("agent-presets")?.config ?? {},
|
|
183
|
+
roots: [{
|
|
184
|
+
path: SHIPPED_PRESET_ROOT,
|
|
185
|
+
trust: "system"
|
|
186
|
+
}]
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID));
|
|
190
|
+
if (telemetryPatch !== void 0) composedOverlays.push(telemetryPatch);
|
|
191
|
+
return {
|
|
192
|
+
profile,
|
|
193
|
+
bundlePatches,
|
|
194
|
+
homePatches,
|
|
195
|
+
overlays: composedOverlays,
|
|
196
|
+
rows
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Re-throw a watcher-setup failure unless a shutdown already owns the tree:
|
|
201
|
+
* a signal aborted this invocation, or an app requested exit (`ctx.appExit`
|
|
202
|
+
* from a fast one-shot) and the root's disposal rejected the in-flight setup
|
|
203
|
+
* await. Either way the failure describes a tree that is exiting as asked,
|
|
204
|
+
* not a broken watch.
|
|
205
|
+
* @param ctx - the booted root context.
|
|
206
|
+
* @param signal - this invocation's signal-shutdown fact.
|
|
207
|
+
* @param error - the setup failure.
|
|
208
|
+
*/
|
|
209
|
+
function suppressShutdownError(ctx, signal, error) {
|
|
210
|
+
if (signal.aborted) return;
|
|
211
|
+
if (ctx.fiber.state !== 2 || ctx.get("loader") === void 0) return;
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Boot one profile invocation end to end and leave process lifetime to the
|
|
216
|
+
* mounted plugins (or to a one-shot runner the composition mounts).
|
|
217
|
+
* @param options - environment snapshot, profile name, overlays, and the booted app's own arguments.
|
|
218
|
+
* @returns the settled root context and the shutdown controller.
|
|
219
|
+
*/
|
|
220
|
+
async function runProfile(options) {
|
|
221
|
+
const composed = composeProfile(options.profile, options.patchFiles);
|
|
222
|
+
const app = {};
|
|
223
|
+
const shutdown = createProcessShutdown(async () => {
|
|
224
|
+
await app.current?.fiber.dispose();
|
|
225
|
+
});
|
|
226
|
+
const signalShutdown = new AbortController();
|
|
227
|
+
const interrupt = (code) => {
|
|
228
|
+
signalShutdown.abort();
|
|
229
|
+
shutdown.interrupt(code);
|
|
230
|
+
};
|
|
231
|
+
process.on("SIGTERM", () => {
|
|
232
|
+
interrupt(0);
|
|
233
|
+
});
|
|
234
|
+
process.on("SIGINT", () => {
|
|
235
|
+
interrupt(130);
|
|
236
|
+
});
|
|
237
|
+
installFailLoud(NAME, process, async () => {
|
|
238
|
+
await app.current?.fiber.dispose();
|
|
239
|
+
});
|
|
240
|
+
const rootConfig = join(composed.profile.dir, PROFILE_ROOT_FILENAME);
|
|
241
|
+
const composeLive = () => structuredClone([
|
|
242
|
+
...composed.bundlePatches,
|
|
243
|
+
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
|
|
244
|
+
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
|
|
245
|
+
...composed.overlays
|
|
246
|
+
]);
|
|
247
|
+
const ctx = await boot(NAME, rootConfig, structuredClone(allPatches(composed)), (hostCtx) => {
|
|
248
|
+
app.current = hostCtx;
|
|
249
|
+
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment);
|
|
250
|
+
provideCmdline(hostCtx, {
|
|
251
|
+
args: options.args,
|
|
252
|
+
exit: (code) => void shutdown.shutdown(code)
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
app.current = ctx;
|
|
256
|
+
if (!signalShutdown.signal.aborted && ctx.fiber.state === 2 && ctx.get("loader") !== void 0) try {
|
|
257
|
+
if (ctx.get("hmr") === void 0) {
|
|
258
|
+
if (ctx.get("timer") === void 0) await ctx.loader.create({ name: "@deepseek-ai/cordis-plugin-timer" });
|
|
259
|
+
await ctx.loader.create({
|
|
260
|
+
name: "@deepseek-ai/cordis-plugin-hmr",
|
|
261
|
+
config: { root: [] }
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
await watchUserPatches(ctx, {
|
|
265
|
+
binName: NAME,
|
|
266
|
+
filename: composed.profile.patchPath,
|
|
267
|
+
compose: composeLive
|
|
268
|
+
});
|
|
269
|
+
await watchUserPatches(ctx, {
|
|
270
|
+
binName: NAME,
|
|
271
|
+
filename: homePatchPath(),
|
|
272
|
+
compose: composeLive
|
|
273
|
+
});
|
|
274
|
+
} catch (error) {
|
|
275
|
+
suppressShutdownError(ctx, signalShutdown.signal, error);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
ctx,
|
|
279
|
+
shutdown
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { resolveTelemetryPatch as a, prepareProfile as i, PROFILE_ROOT_FILENAME as n, runProfile as o, homePatchPath as r, INSTALL_ANCHOR as t };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createWriteStream, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
//#region lib/types/web-log.js
|
|
6
|
+
/**
|
|
7
|
+
* web-log — fork-local `dsh web` boot with stdout/stderr persisted to a log
|
|
8
|
+
* file. Reimplements the retired `scripts/web-log.sh` as a launcher
|
|
9
|
+
* subcommand so the entry is `dsh web:log` / `dsh web:log:tmp` instead of a
|
|
10
|
+
* separate pnpm script.
|
|
11
|
+
*
|
|
12
|
+
* The boot runs as a child process re-invoking this very bin (`dsh web …`),
|
|
13
|
+
* so the tee is byte-exact and the logging wrapper never shares a process —
|
|
14
|
+
* or a crash — with the harness. Per launch one `web-<timestamp>.log` file is
|
|
15
|
+
* written, with a `web-latest.log` symlink alongside always naming the newest.
|
|
16
|
+
* @module @deepseek-ai/dsh/web-log
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the log directory: `DSH_WEB_LOG_DIR` wins; otherwise the tmp variant
|
|
20
|
+
* falls to the OS temp dir (auto-reaped) and the default to `$DSH_HOME/logs`.
|
|
21
|
+
*/
|
|
22
|
+
function resolveLogDir(env, tmp) {
|
|
23
|
+
if (env.DSH_WEB_LOG_DIR !== void 0 && env.DSH_WEB_LOG_DIR !== "") return env.DSH_WEB_LOG_DIR;
|
|
24
|
+
if (tmp) return join(env.TMPDIR ?? tmpdir(), "dsh-web-logs");
|
|
25
|
+
return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "logs");
|
|
26
|
+
}
|
|
27
|
+
/** The `yyyymmdd-HHMMSS` name stamp for one launch's log file. */
|
|
28
|
+
function logStamp(now) {
|
|
29
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
30
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Boot `dsh web` as a child of this bin, teeing its combined output to the
|
|
34
|
+
* console and the log file, and exit with the child's code. Never returns:
|
|
35
|
+
* the child's exit (or a spawn failure) ends this process.
|
|
36
|
+
*/
|
|
37
|
+
function runWebLog(options) {
|
|
38
|
+
const dir = resolveLogDir(process.env, options.tmp);
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
const log = join(dir, `web-${logStamp(/* @__PURE__ */ new Date())}.log`);
|
|
41
|
+
rmSync(join(dir, "web-latest.log"), { force: true });
|
|
42
|
+
symlinkSync(log, join(dir, "web-latest.log"));
|
|
43
|
+
const stream = createWriteStream(log, { flags: "a" });
|
|
44
|
+
const bin = process.argv[1];
|
|
45
|
+
if (bin === void 0) throw new Error("dsh web:log: no entry script to re-invoke");
|
|
46
|
+
const argv = [
|
|
47
|
+
...process.execArgv,
|
|
48
|
+
bin,
|
|
49
|
+
"web",
|
|
50
|
+
...options.patches.flatMap((patch) => ["--patch", patch]),
|
|
51
|
+
...options.args
|
|
52
|
+
];
|
|
53
|
+
const child = spawn(process.execPath, argv, { stdio: [
|
|
54
|
+
"inherit",
|
|
55
|
+
"pipe",
|
|
56
|
+
"pipe"
|
|
57
|
+
] });
|
|
58
|
+
const header = `[web-log] ${(/* @__PURE__ */ new Date()).toISOString()} starting (pid: ${child.pid ?? "?"}, log: ${log})\n`;
|
|
59
|
+
process.stdout.write(header);
|
|
60
|
+
stream.write(header);
|
|
61
|
+
child.stdout.on("data", (chunk) => {
|
|
62
|
+
process.stdout.write(chunk);
|
|
63
|
+
stream.write(chunk);
|
|
64
|
+
});
|
|
65
|
+
child.stderr.on("data", (chunk) => {
|
|
66
|
+
process.stderr.write(chunk);
|
|
67
|
+
stream.write(chunk);
|
|
68
|
+
});
|
|
69
|
+
const forward = (signal) => {
|
|
70
|
+
child.kill(signal);
|
|
71
|
+
};
|
|
72
|
+
process.on("SIGINT", forward.bind(null, "SIGINT"));
|
|
73
|
+
process.on("SIGTERM", forward.bind(null, "SIGTERM"));
|
|
74
|
+
child.on("error", (error) => {
|
|
75
|
+
process.stderr.write(`[web-log] failed to spawn dsh web: ${error.message}\n`);
|
|
76
|
+
stream.end(() => process.exit(1));
|
|
77
|
+
});
|
|
78
|
+
child.on("exit", (code) => {
|
|
79
|
+
stream.end(() => process.exit(code ?? 1));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { runWebLog };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createWriteStream, mkdirSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
//#region lib/types/web-log.js
|
|
6
|
+
/**
|
|
7
|
+
* web-log — fork-local `dsh web` boot with stdout/stderr persisted to a log
|
|
8
|
+
* file, exposed as the launcher subcommands `dsh web:log` / `dsh web:log:tmp`.
|
|
9
|
+
*
|
|
10
|
+
* The boot runs as a child process re-invoking this very bin (`dsh web …`),
|
|
11
|
+
* so the tee is byte-exact and the logging wrapper never shares a process —
|
|
12
|
+
* or a crash — with the harness. Per launch one `web-<timestamp>.log` file is
|
|
13
|
+
* written, with a `web-latest.log` symlink alongside always naming the newest.
|
|
14
|
+
* @module @deepseek-ai/dsh/web-log
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the log directory: `DSH_WEB_LOG_DIR` wins; otherwise the tmp variant
|
|
18
|
+
* falls to the OS temp dir (auto-reaped) and the default to `$DSH_HOME/logs`.
|
|
19
|
+
* @param env - the environment to read; injectable for tests.
|
|
20
|
+
* @param tmp - whether the OS temp dir is the fallback.
|
|
21
|
+
* @returns the directory this launch's log file belongs in.
|
|
22
|
+
*/
|
|
23
|
+
function resolveLogDir(env, tmp) {
|
|
24
|
+
if (env.DSH_WEB_LOG_DIR !== void 0 && env.DSH_WEB_LOG_DIR !== "") return env.DSH_WEB_LOG_DIR;
|
|
25
|
+
if (tmp) return join(env.TMPDIR ?? tmpdir(), "dsh-web-logs");
|
|
26
|
+
return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "logs");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The `yyyymmdd-HHMMSS` name stamp for one launch's log file.
|
|
30
|
+
* @param now - the launch time to format.
|
|
31
|
+
* @returns the zero-padded local-time stamp.
|
|
32
|
+
*/
|
|
33
|
+
function logStamp(now) {
|
|
34
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
35
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Boot `dsh web` as a child of this bin, teeing its combined output to the
|
|
39
|
+
* console and the log file, and exit with the child's code. Never returns:
|
|
40
|
+
* the child's exit (or a spawn failure) ends this process.
|
|
41
|
+
* @param options - the parsed `web:log` invocation: the log-dir variant and
|
|
42
|
+
* the flags forwarded to `dsh web`.
|
|
43
|
+
*/
|
|
44
|
+
function runWebLog(options) {
|
|
45
|
+
const dir = resolveLogDir(process.env, options.tmp);
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
const log = join(dir, `web-${logStamp(/* @__PURE__ */ new Date())}.log`);
|
|
48
|
+
rmSync(join(dir, "web-latest.log"), { force: true });
|
|
49
|
+
symlinkSync(log, join(dir, "web-latest.log"));
|
|
50
|
+
const stream = createWriteStream(log, { flags: "a" });
|
|
51
|
+
const bin = process.argv[1];
|
|
52
|
+
if (bin === void 0) throw new Error("dsh web:log: no entry script to re-invoke");
|
|
53
|
+
const argv = [
|
|
54
|
+
...process.execArgv,
|
|
55
|
+
bin,
|
|
56
|
+
"web",
|
|
57
|
+
...options.patches.flatMap((patch) => ["--patch", patch]),
|
|
58
|
+
...options.args
|
|
59
|
+
];
|
|
60
|
+
const child = spawn(process.execPath, argv, { stdio: [
|
|
61
|
+
"inherit",
|
|
62
|
+
"pipe",
|
|
63
|
+
"pipe"
|
|
64
|
+
] });
|
|
65
|
+
const header = `[web-log] ${(/* @__PURE__ */ new Date()).toISOString()} starting (pid: ${child.pid ?? "?"}, log: ${log})\n`;
|
|
66
|
+
process.stdout.write(header);
|
|
67
|
+
stream.write(header);
|
|
68
|
+
child.stdout.on("data", (chunk) => {
|
|
69
|
+
process.stdout.write(chunk);
|
|
70
|
+
stream.write(chunk);
|
|
71
|
+
});
|
|
72
|
+
child.stderr.on("data", (chunk) => {
|
|
73
|
+
process.stderr.write(chunk);
|
|
74
|
+
stream.write(chunk);
|
|
75
|
+
});
|
|
76
|
+
const forward = (signal) => {
|
|
77
|
+
child.kill(signal);
|
|
78
|
+
};
|
|
79
|
+
process.on("SIGINT", forward.bind(null, "SIGINT"));
|
|
80
|
+
process.on("SIGTERM", forward.bind(null, "SIGTERM"));
|
|
81
|
+
let finished = false;
|
|
82
|
+
const finish = (code) => {
|
|
83
|
+
if (finished) return;
|
|
84
|
+
finished = true;
|
|
85
|
+
stream.end(() => process.exit(code));
|
|
86
|
+
};
|
|
87
|
+
child.on("error", (error) => {
|
|
88
|
+
process.stderr.write(`[web-log] failed to spawn dsh web: ${error.message}\n`);
|
|
89
|
+
finish(1);
|
|
90
|
+
});
|
|
91
|
+
child.on("close", (code) => {
|
|
92
|
+
finish(code ?? 1);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { runWebLog };
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crazx/dsh",
|
|
3
|
+
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
|
|
4
|
+
"version": "0.1.0-rc.7.zw.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/aka-danielZhang/deepseek-harness.git",
|
|
11
|
+
"directory": "apps/cli"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"dsh": "lib/bin.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib/*.js",
|
|
19
|
+
"config"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@deepseek-ai/cordis-plugin-hmr": "^0.1.0-rc.7",
|
|
24
|
+
"@deepseek-ai/cordis-plugin-include": "^0.1.0-rc.7",
|
|
25
|
+
"@deepseek-ai/cordis-plugin-loader": "^0.1.0-rc.7",
|
|
26
|
+
"@deepseek-ai/cordis-plugin-timer": "^0.1.0-rc.7",
|
|
27
|
+
"@deepseek-ai/dsh-agent-tool-presentation": "^0.1.0-rc.7",
|
|
28
|
+
"@deepseek-ai/dsh-app-boot": "^0.1.0-rc.7",
|
|
29
|
+
"@deepseek-ai/dsh-base": "^0.1.0-rc.7",
|
|
30
|
+
"@deepseek-ai/dsh-cordis-client-runner": "^0.1.0-rc.7",
|
|
31
|
+
"@deepseek-ai/dsh-client-ui-agent-preset": "^0.1.0-rc.7",
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-cordis": "^0.1.0-rc.7",
|
|
33
|
+
"@deepseek-ai/dsh-command-compact": "^0.1.0-rc.7",
|
|
34
|
+
"@deepseek-ai/dsh-command-goal": "^0.1.0-rc.7",
|
|
35
|
+
"@deepseek-ai/dsh-compaction-basic": "^0.1.0-rc.7",
|
|
36
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": "^0.1.0-rc.7",
|
|
37
|
+
"@deepseek-ai/dsh-goal": "^0.1.0-rc.7",
|
|
38
|
+
"@deepseek-ai/dsh-goal-round-driver": "^0.1.0-rc.7",
|
|
39
|
+
"@deepseek-ai/dsh-cmdline": "^0.1.0-rc.7",
|
|
40
|
+
"@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.7",
|
|
41
|
+
"@deepseek-ai/dsh-fs-local": "^0.1.0-rc.7",
|
|
42
|
+
"@deepseek-ai/dsh-headless": "^0.1.0-rc.7",
|
|
43
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.7",
|
|
44
|
+
"@deepseek-ai/dsh-persona": "^0.1.0-rc.7",
|
|
45
|
+
"@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.7",
|
|
46
|
+
"@deepseek-ai/dsh-terminal": "^0.1.0-rc.7",
|
|
47
|
+
"@deepseek-ai/dsh-terminal-bash": "^0.1.0-rc.7",
|
|
48
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.1.0-rc.7",
|
|
49
|
+
"@deepseek-ai/dsh-pwsh-sandbox": "^0.1.0-rc.7",
|
|
50
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.0-rc.7",
|
|
51
|
+
"@deepseek-ai/dsh-session-reference": "^0.1.0-rc.7",
|
|
52
|
+
"@deepseek-ai/dsh-time-context": "^0.1.0-rc.7",
|
|
53
|
+
"@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
|
|
54
|
+
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.7",
|
|
55
|
+
"@deepseek-ai/dsh-jobs-local": "^0.1.0-rc.7",
|
|
56
|
+
"@deepseek-ai/dsh-tmux-context": "^0.1.0-rc.7",
|
|
57
|
+
"@deepseek-ai/dsh-token-meter": "^0.1.0-rc.7",
|
|
58
|
+
"@deepseek-ai/dsh-tool-ask-user": "^0.1.0-rc.7",
|
|
59
|
+
"@deepseek-ai/dsh-tool-bash": "^0.1.0-rc.7",
|
|
60
|
+
"@deepseek-ai/dsh-tool-bash-persistent": "^0.1.0-rc.7",
|
|
61
|
+
"@deepseek-ai/dsh-tool-fs": "^0.1.0-rc.7",
|
|
62
|
+
"@deepseek-ai/dsh-tool-fs-search": "^0.1.0-rc.7",
|
|
63
|
+
"@deepseek-ai/dsh-tool-goal": "^0.1.0-rc.7",
|
|
64
|
+
"@deepseek-ai/dsh-tool-pwsh": "^0.1.0-rc.7",
|
|
65
|
+
"@deepseek-ai/dsh-tool-ralph": "^0.1.0-rc.7",
|
|
66
|
+
"@deepseek-ai/dsh-schedule": "^0.1.0-rc.7",
|
|
67
|
+
"@deepseek-ai/dsh-tool-skill": "^0.1.0-rc.7",
|
|
68
|
+
"@deepseek-ai/dsh-tool-str-replace-editor": "^0.1.0-rc.7",
|
|
69
|
+
"@deepseek-ai/dsh-tool-subagent": "^0.1.0-rc.7",
|
|
70
|
+
"@deepseek-ai/dsh-tool-subagent-control": "^0.1.0-rc.7",
|
|
71
|
+
"@deepseek-ai/dsh-tool-jobs": "^0.1.0-rc.7",
|
|
72
|
+
"@deepseek-ai/dsh-tool-todo": "^0.1.0-rc.7",
|
|
73
|
+
"@deepseek-ai/dsh-tool-web": "^0.1.0-rc.7",
|
|
74
|
+
"@deepseek-ai/dsh-tool-workflow": "^0.1.0-rc.7",
|
|
75
|
+
"@deepseek-ai/dsh-web-app": "^0.1.0-rc.7",
|
|
76
|
+
"@deepseek-ai/dsh-workflow-worker-thread": "^0.1.0-rc.7",
|
|
77
|
+
"@deepseek-ai/dsh-agent-instructions": "^0.1.0-rc.7",
|
|
78
|
+
"commander": "^15.0.0",
|
|
79
|
+
"@deepseek-ai/cordis": "^0.1.0-rc.7",
|
|
80
|
+
"js-yaml": "^4.2.0",
|
|
81
|
+
"node-addon-require-builtin": "^0.1.4",
|
|
82
|
+
"@crazx/dsh-mcp-client": "0.1.0-rc.7.zw.2",
|
|
83
|
+
"@crazx/dsh-tool-cordis": "0.1.0-rc.7.zw.2"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
|
|
87
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
|
|
88
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
|
|
89
|
+
"@deepseek-ai/dsh-llm-mock-server": "^0.1.0-rc.7",
|
|
90
|
+
"@deepseek-ai/dsh-loader-smoke": "^0.1.0-rc.7",
|
|
91
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
92
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
|
|
93
|
+
"@deepseek-ai/dsh-subagent": "^0.1.0-rc.7",
|
|
94
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
|
|
95
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
96
|
+
"@types/js-yaml": "^4.0.9",
|
|
97
|
+
"execa": "^10.0.0",
|
|
98
|
+
"@crazx/dsh-host-frontend-static": "0.1.0-rc.7.zw.2",
|
|
99
|
+
"@crazx/dsh-host-apiproxy": "0.1.0-rc.7.zw.2"
|
|
100
|
+
}
|
|
101
|
+
}
|