@cannbot-plugin/cannbot-harness 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/lib/client.js +828 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +156 -6
- package/package.json +3 -2
- package/runtime/initializer-compat.cjs +25 -0
package/lib/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { isUserInvocable, renderSkillContent } from "@deepseek-ai/dsh-skill";
|
|
|
7
7
|
import z from "@deepseek-ai/schemastery";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
11
|
-
import { cp, mkdir, mkdtemp, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
11
|
+
import { cp, lstat, mkdir, mkdtemp, open, readFile, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
//#region src/workflows.ts
|
|
14
14
|
const WORKFLOWS = [
|
|
@@ -273,6 +273,145 @@ async function withContentLock(path, action) {
|
|
|
273
273
|
if (await readFile(path, "utf8").catch(() => "") === owner) await unlink(path);
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/initializer-transaction.ts
|
|
278
|
+
const transient = new Set([
|
|
279
|
+
"EIO",
|
|
280
|
+
"EBUSY",
|
|
281
|
+
"EPERM",
|
|
282
|
+
"EACCES",
|
|
283
|
+
"ENOENT"
|
|
284
|
+
]);
|
|
285
|
+
async function retry(action) {
|
|
286
|
+
for (let attempt = 0;; attempt++) try {
|
|
287
|
+
return await action();
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!transient.has(error.code ?? "") || attempt === 6) throw error;
|
|
290
|
+
await new Promise((done) => setTimeout(done, Math.min(100 * 2 ** attempt, 800)));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function inside$1(root, path) {
|
|
294
|
+
const target = resolve(root, path), diff = relative(root, target);
|
|
295
|
+
if (!diff || diff === ".." || diff.startsWith(".." + sep) || isAbsolute(diff)) throw new Error("Invalid initialization recovery path");
|
|
296
|
+
return target;
|
|
297
|
+
}
|
|
298
|
+
async function present(path) {
|
|
299
|
+
try {
|
|
300
|
+
await lstat(path);
|
|
301
|
+
return true;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (error.code === "ENOENT") return false;
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async function checkParent(workspace, path) {
|
|
308
|
+
let parent = resolve(path, "..");
|
|
309
|
+
while (parent !== workspace && !await present(parent)) parent = resolve(parent, "..");
|
|
310
|
+
if (parent !== workspace) inside$1(workspace, await realpath(parent));
|
|
311
|
+
}
|
|
312
|
+
/** Snapshot only installer-owned destinations; dependency repositories are never removed. */
|
|
313
|
+
async function initializeTransaction(workspace, recoveryRoot, descriptor, resources, initialize) {
|
|
314
|
+
await mkdir(recoveryRoot, { recursive: true });
|
|
315
|
+
const journalPath = join(recoveryRoot, "recovery.json");
|
|
316
|
+
async function restore(journal) {
|
|
317
|
+
if (journal.workspace !== workspace || !relative(recoveryRoot, journal.directory).startsWith("snapshot-") || relative(recoveryRoot, journal.directory).includes(sep)) throw new Error("Invalid initialization recovery journal");
|
|
318
|
+
for (const entry of journal.entries) {
|
|
319
|
+
const destination = inside$1(workspace, entry.path), saved = inside$1(journal.directory, entry.slot);
|
|
320
|
+
const parent = resolve(destination, "..");
|
|
321
|
+
await checkParent(workspace, destination);
|
|
322
|
+
await retry(() => rm(destination, {
|
|
323
|
+
recursive: true,
|
|
324
|
+
force: true,
|
|
325
|
+
maxRetries: 3,
|
|
326
|
+
retryDelay: 100
|
|
327
|
+
}));
|
|
328
|
+
if (entry.present) {
|
|
329
|
+
await mkdir(parent, { recursive: true });
|
|
330
|
+
await retry(() => cp(saved, destination, {
|
|
331
|
+
recursive: true,
|
|
332
|
+
dereference: false,
|
|
333
|
+
verbatimSymlinks: true
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const previous = await readFile(journalPath, "utf8").catch((error) => {
|
|
339
|
+
if (error.code === "ENOENT") return "";
|
|
340
|
+
throw error;
|
|
341
|
+
});
|
|
342
|
+
if (previous) {
|
|
343
|
+
const journal = JSON.parse(previous);
|
|
344
|
+
await restore(journal);
|
|
345
|
+
await rm(journalPath);
|
|
346
|
+
await rm(journal.directory, {
|
|
347
|
+
recursive: true,
|
|
348
|
+
force: true,
|
|
349
|
+
maxRetries: 3,
|
|
350
|
+
retryDelay: 100
|
|
351
|
+
}).catch(() => {});
|
|
352
|
+
}
|
|
353
|
+
const paths = new Set(descriptor.skills.map((skill) => `.dsh/skills/${skill.name}`));
|
|
354
|
+
paths.add(`.cannbot/plugins/${descriptor.id}`);
|
|
355
|
+
for (const file of [
|
|
356
|
+
"AGENTS.md",
|
|
357
|
+
"CLAUDE.md",
|
|
358
|
+
".dsh/cannbot-plugin.json",
|
|
359
|
+
".cannbot/settings.json",
|
|
360
|
+
".cannbot/permissions"
|
|
361
|
+
]) paths.add(file);
|
|
362
|
+
const manifestPath = join(resources, ".claude-plugin/plugin.json");
|
|
363
|
+
if (await present(manifestPath)) {
|
|
364
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
365
|
+
for (const path of manifest.agents ?? []) {
|
|
366
|
+
const text = await readFile(inside$1(resources, path), "utf8");
|
|
367
|
+
const name = /^name:\s*([^\r\n]+)$/m.exec(text)?.[1]?.trim();
|
|
368
|
+
if (!name || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(name)) throw new Error("Invalid initializer agent name");
|
|
369
|
+
paths.add(`.dsh/agents/${name}.md`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const directory = await mkdtemp(join(recoveryRoot, "snapshot-"));
|
|
373
|
+
const journal = {
|
|
374
|
+
workspace,
|
|
375
|
+
directory,
|
|
376
|
+
entries: []
|
|
377
|
+
};
|
|
378
|
+
for (const path of paths) {
|
|
379
|
+
const source = inside$1(workspace, path);
|
|
380
|
+
await checkParent(workspace, source);
|
|
381
|
+
const exists = await present(source), slot = String(journal.entries.length);
|
|
382
|
+
if (exists) await retry(() => cp(source, join(directory, slot), {
|
|
383
|
+
recursive: true,
|
|
384
|
+
dereference: false,
|
|
385
|
+
verbatimSymlinks: true
|
|
386
|
+
}));
|
|
387
|
+
journal.entries.push({
|
|
388
|
+
path,
|
|
389
|
+
present: exists,
|
|
390
|
+
slot
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const temporary = journalPath + ".tmp";
|
|
394
|
+
await writeFile(temporary, JSON.stringify(journal), { mode: 384 });
|
|
395
|
+
await rename(temporary, journalPath);
|
|
396
|
+
try {
|
|
397
|
+
await initialize();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
try {
|
|
400
|
+
await restore(journal);
|
|
401
|
+
await rm(journalPath);
|
|
402
|
+
} catch (recovery) {
|
|
403
|
+
throw new Error(`${error.message}\n自动恢复未完成,备份保留在 ${directory}:${recovery.message}`);
|
|
404
|
+
}
|
|
405
|
+
throw new Error(`${error.message}\n已恢复本次初始化前的 Skills 和工作区配置。`);
|
|
406
|
+
}
|
|
407
|
+
await rm(journalPath);
|
|
408
|
+
await rm(directory, {
|
|
409
|
+
recursive: true,
|
|
410
|
+
force: true,
|
|
411
|
+
maxRetries: 3,
|
|
412
|
+
retryDelay: 100
|
|
413
|
+
}).catch(() => {});
|
|
414
|
+
}
|
|
276
415
|
const CATALOG_PACKAGE = "@cannbot-plugin/workflow-catalog";
|
|
277
416
|
const REGISTRY = "https://registry.npmjs.org/";
|
|
278
417
|
const MIRROR = "https://registry.npmmirror.com/";
|
|
@@ -733,7 +872,13 @@ var ContentManager = class {
|
|
|
733
872
|
async runInitializer(entry, args, cwd) {
|
|
734
873
|
await new Promise((accept, reject) => {
|
|
735
874
|
let output = "", stopped = false;
|
|
736
|
-
const
|
|
875
|
+
const compatibility = join(import.meta.dirname, "../runtime/initializer-compat.cjs");
|
|
876
|
+
const child = spawn(process.execPath, [
|
|
877
|
+
"--require",
|
|
878
|
+
compatibility,
|
|
879
|
+
entry,
|
|
880
|
+
...args
|
|
881
|
+
], {
|
|
737
882
|
cwd,
|
|
738
883
|
windowsHide: true,
|
|
739
884
|
detached: process.platform !== "win32",
|
|
@@ -744,6 +889,8 @@ var ContentManager = class {
|
|
|
744
889
|
],
|
|
745
890
|
env: {
|
|
746
891
|
...process.env,
|
|
892
|
+
...process.versions.electron ? { ELECTRON_RUN_AS_NODE: "1" } : {},
|
|
893
|
+
CANNBOT_INITIALIZER_WORKSPACE: cwd,
|
|
747
894
|
GIT_TERMINAL_PROMPT: "0",
|
|
748
895
|
GCM_INTERACTIVE: "never"
|
|
749
896
|
}
|
|
@@ -800,7 +947,9 @@ var ContentManager = class {
|
|
|
800
947
|
let job = this.jobs.get(jobKey);
|
|
801
948
|
if (!job) {
|
|
802
949
|
job = withContentLock(join(this.root, "initialized", key, "workspace.lock"), async () => {
|
|
803
|
-
|
|
950
|
+
const recoveryRoot = join(this.root, "initialized", key, "recovery");
|
|
951
|
+
const skillsPresent = descriptor.skills.every((skill) => existsSync(join(workspace, ".dsh/skills", skill.name, "SKILL.md")));
|
|
952
|
+
if (await readFile(marker, "utf8").catch(() => "") === receipt && !existsSync(join(recoveryRoot, "recovery.json")) && (descriptor.initialization.kind !== "cannbot-cli" || skillsPresent)) return;
|
|
804
953
|
this.publish({
|
|
805
954
|
phase: "initializing",
|
|
806
955
|
message: `Preparing ${id}@${record.version} for this workspace…`,
|
|
@@ -808,7 +957,8 @@ var ContentManager = class {
|
|
|
808
957
|
});
|
|
809
958
|
if (descriptor.initialization.kind === "cannbot-cli") {
|
|
810
959
|
const root = inside(this.root, record.directory);
|
|
811
|
-
|
|
960
|
+
const entry = inside(root, descriptor.initialization.entry);
|
|
961
|
+
await initializeTransaction(workspace, recoveryRoot, descriptor, join(root, "resources"), () => this.runInitializer(entry, [
|
|
812
962
|
"install",
|
|
813
963
|
id,
|
|
814
964
|
"--tool",
|
|
@@ -817,7 +967,7 @@ var ContentManager = class {
|
|
|
817
967
|
workspace,
|
|
818
968
|
"--plugin-dir",
|
|
819
969
|
join(root, "resources")
|
|
820
|
-
], workspace);
|
|
970
|
+
], workspace));
|
|
821
971
|
}
|
|
822
972
|
await writeFile(marker, receipt, { mode: 384 });
|
|
823
973
|
this.publish({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cannbot-plugin/cannbot-harness",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CANNBot Harness:
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "CANNBot Harness: Ascend development workbench with independently versioned workflow packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN THIRD_PARTY_NOTICES.md",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"lib",
|
|
17
|
+
"runtime/initializer-compat.cjs",
|
|
17
18
|
"workflow-defaults.json",
|
|
18
19
|
"locale",
|
|
19
20
|
"cordis.patch.yml",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Used only by the official initializer subprocess. Never patches the DSH host.
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const root = process.env.CANNBOT_INITIALIZER_WORKSPACE
|
|
5
|
+
if (process.platform === 'win32' && root) {
|
|
6
|
+
const cp = fs.cpSync
|
|
7
|
+
const wait = new Int32Array(new SharedArrayBuffer(4))
|
|
8
|
+
const managed = value => {
|
|
9
|
+
if (typeof value !== 'string') return false
|
|
10
|
+
const relative = path.relative(root, path.resolve(value))
|
|
11
|
+
return !path.isAbsolute(relative) && ['.dsh', '.cannbot'].some(name => relative.startsWith(name + path.sep))
|
|
12
|
+
}
|
|
13
|
+
fs.cpSync = function (source, destination, options) {
|
|
14
|
+
for (let attempt = 0; ; attempt++) {
|
|
15
|
+
try { return cp(source, destination, options) }
|
|
16
|
+
catch (error) {
|
|
17
|
+
// Windows may report EIO while a just-deleted directory is still closing.
|
|
18
|
+
// Do not rerun the whole installer, or retry missing source files.
|
|
19
|
+
if (!managed(destination) || !fs.existsSync(source) || !['EIO', 'EBUSY', 'EPERM', 'EACCES', 'ENOENT'].includes(error.code) || attempt === 6) throw error
|
|
20
|
+
Atomics.wait(wait, 0, 0, Math.min(100 * 2 ** attempt, 800))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
require('node:module').syncBuiltinESMExports()
|
|
25
|
+
}
|