@ninkch/dsh-artcraft 0.2.4 → 0.2.6

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 (2) hide show
  1. package/index.js +78 -49
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -39,7 +39,7 @@ import crypto from 'node:crypto';
39
39
  import { fileURLToPath } from 'node:url';
40
40
 
41
41
  const NAME = 'artcraft';
42
- const VERSION = '0.2.4';
42
+ const VERSION = '0.2.6';
43
43
  const HERE = path.dirname(fileURLToPath(import.meta.url));
44
44
 
45
45
  const BINS = ['photocraft', 'vectorcraft', 'filmcraft', 'lightcraft', 'pdfcraft', 'effectcraft', 'designcraft'];
@@ -76,21 +76,29 @@ async function loadConfig() {
76
76
  bins: { ...DEFAULT_CONFIG.bins },
77
77
  control: { ...DEFAULT_PORTS },
78
78
  };
79
- try {
80
- const user = JSON.parse(await fs.readFile(path.join(HERE, 'artcraft.config.json'), 'utf8'));
81
- if (user && typeof user.dataRoot === 'string' && user.dataRoot) cfg.dataRoot = user.dataRoot;
82
- if (user && user.bins) {
83
- for (const k of BINS) {
84
- if (typeof user.bins[k] === 'string' && user.bins[k]) cfg.bins[k] = user.bins[k];
79
+ // 配置查找顺序:~/.dsh(持久,推荐 —— 更新替换包目录也不丢)→ 包目录(旧版兼容)
80
+ const candidates = [
81
+ path.join(os.homedir(), '.dsh', 'artcraft.config.json'),
82
+ path.join(HERE, 'artcraft.config.json'),
83
+ ];
84
+ for (const candidate of candidates) {
85
+ try {
86
+ const user = JSON.parse(await fs.readFile(candidate, 'utf8'));
87
+ if (user && typeof user.dataRoot === 'string' && user.dataRoot) cfg.dataRoot = user.dataRoot;
88
+ if (user && user.bins) {
89
+ for (const k of BINS) {
90
+ if (typeof user.bins[k] === 'string' && user.bins[k]) cfg.bins[k] = user.bins[k];
91
+ }
85
92
  }
86
- }
87
- if (user && user.control) {
88
- for (const k of Object.keys(DEFAULT_PORTS)) {
89
- const p = Number(user.control[k]);
90
- if (Number.isInteger(p) && p > 0 && p < 65536) cfg.control[k] = p;
93
+ if (user && user.control) {
94
+ for (const k of Object.keys(DEFAULT_PORTS)) {
95
+ const p = Number(user.control[k]);
96
+ if (Number.isInteger(p) && p > 0 && p < 65536) cfg.control[k] = p;
97
+ }
91
98
  }
92
- }
93
- } catch { /* 无配置文件时用默认值 */ }
99
+ break; // 第一个可解析的配置生效
100
+ } catch { /* 该候选不存在/非法 → 试下一个 */ }
101
+ }
94
102
  return cfg;
95
103
  }
96
104
 
@@ -501,7 +509,7 @@ async function fetchLatestVersion() {
501
509
  if (res.status === 404) return { latest: null, note: 'npm 上还没有这个包' };
502
510
  if (!res.ok) throw new Error('registry 返回 ' + res.status);
503
511
  const j = await res.json();
504
- return { latest: j.version ?? null };
512
+ return { latest: j.version ?? null, tarball: j.dist?.tarball ?? null, integrity: j.dist?.integrity ?? null };
505
513
  } catch (e) {
506
514
  return { latest: null, note: '查询 npm 失败: ' + String(e?.cause?.message || e?.message || e) };
507
515
  }
@@ -523,29 +531,10 @@ async function updateCheck() {
523
531
  };
524
532
  }
525
533
 
526
- async function findRuntimeTools() {
527
- const isWin = process.platform === 'win32';
528
- const pnCmd = isWin ? 'pnpm.cmd' : 'pnpm';
529
- const probe = await runProcess(pnCmd, ['--version'], { timeoutMs: 20000 });
530
- if (probe.ok) return { pnpmCmd: pnCmd };
531
- try {
532
- const rtRoot = path.join(os.homedir(), '.dsh', 'dsh-runtimes');
533
- const entries = await fs.readdir(rtRoot, { withFileTypes: true });
534
- for (const e of entries) {
535
- if (!e.isDirectory()) continue;
536
- const pnpmMjs = path.join(rtRoot, e.name, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs');
537
- const nodeExe = path.join(rtRoot, e.name, 'dependencies', 'node', 'bin', isWin ? 'node.exe' : 'node');
538
- if ((await fileExists(pnpmMjs)) && (await fileExists(nodeExe))) {
539
- return { nodeExe, pnpmMjs };
540
- }
541
- }
542
- } catch { /* ignore */ }
543
- return {};
544
- }
545
-
546
534
  async function updateApply() {
547
535
  const info = await detectInstall();
548
- const { latest } = await fetchLatestVersion();
536
+ const meta = await fetchLatestVersion();
537
+ const latest = meta.latest;
549
538
  if (!latest) throw new Error('npm 上没有可用的发布版本');
550
539
  if (!semverGt(latest, VERSION)) return { ok: true, message: `已是最新(${VERSION})`, needRestart: false };
551
540
 
@@ -557,19 +546,56 @@ async function updateApply() {
557
546
  return { ok: r.ok, output: (r.stdout + '\n' + r.stderr).trim(), needRestart: r.ok, latest };
558
547
  }
559
548
 
560
- // npm 安装模式
561
- if (!info.profileDir) throw new Error('未定位到 Harness profile 目录,无法自动更新');
562
- const tools = await findRuntimeTools();
563
- let cmd, args;
564
- if (tools.pnpmCmd) {
565
- cmd = tools.pnpmCmd; args = ['add', `${PKG_NAME}@latest`, '--dir', info.profileDir];
566
- } else if (tools.pnpmMjs) {
567
- cmd = tools.nodeExe; args = [tools.pnpmMjs, 'add', `${PKG_NAME}@latest`, '--dir', info.profileDir];
568
- } else {
569
- throw new Error('未找到 pnpm(PATH 与 ~/.dsh/dsh-runtimes 均未命中),请手动在 profile 目录执行 pnpm add');
549
+ // npm 安装模式:直下官方 tarball → sha512 校验 → 原子换目录。
550
+ // 不经 pnpm 解析器 —— 规避供应链年龄门槛 / link 依赖联动 / 镜像同步延迟。
551
+ if (!meta.tarball || !meta.integrity) throw new Error('registry 元数据缺少 tarball/integrity,无法安全更新');
552
+ const here = info.here;
553
+ const tmp = path.join(os.tmpdir(), 'artcraft-up-' + crypto.randomUUID());
554
+ await fs.mkdir(tmp, { recursive: true });
555
+ try {
556
+ const tgzPath = path.join(tmp, 'pkg.tgz');
557
+ const res = await fetch(meta.tarball, { signal: AbortSignal.timeout(180000) });
558
+ if (!res.ok) throw new Error('下载 tarball 失败: HTTP ' + res.status);
559
+ const buf = Buffer.from(await res.arrayBuffer());
560
+ if (buf.length > 64 * 1024 * 1024) throw new Error('tarball 超过 64MB 上限,已终止');
561
+ const digest = crypto.createHash('sha512').update(buf).digest('base64');
562
+ if (digest !== String(meta.integrity).replace(/^sha512-/, '')) {
563
+ throw new Error('sha512 校验失败 —— 下载内容与 registry 元数据不符,已终止');
564
+ }
565
+ await fs.writeFile(tgzPath, buf);
566
+ const extract = await runProcess('tar', ['-xzf', tgzPath, '-C', tmp], { timeoutMs: 60000 });
567
+ if (!extract.ok) throw new Error('解压失败: ' + (extract.stderr || '系统 tar 不可用'));
568
+ const extractedPkg = JSON.parse(await fs.readFile(path.join(tmp, 'package', 'package.json'), 'utf8'));
569
+ if (extractedPkg.name !== PKG_NAME || extractedPkg.version !== latest) {
570
+ throw new Error(`tarball 内容与预期不符(${extractedPkg.name}@${extractedPkg.version})`);
571
+ }
572
+ // 原子换目录:rename 失败(跨卷等)时回退为整目录复制
573
+ const backup = here + '.old-' + Date.now();
574
+ try {
575
+ await fs.rename(here, backup).catch(() => { throw new Error('rename-busy'); });
576
+ await fs.rename(path.join(tmp, 'package'), here).catch(async () => {
577
+ await fs.cp(path.join(tmp, 'package'), here, { recursive: true, force: true });
578
+ });
579
+ await fs.rm(backup, { recursive: true, force: true }).catch(() => {});
580
+ } catch {
581
+ // 兜底:直接覆盖复制(不影响运行中的进程 —— 旧模块已在内存)
582
+ await fs.cp(path.join(tmp, 'package'), here, { recursive: true, force: true });
583
+ }
584
+ // 同步 profile package.json 的版本规格,防止未来 pnpm install 回滚
585
+ if (info.profileDir) {
586
+ try {
587
+ const pjPath = path.join(info.profileDir, 'package.json');
588
+ const pj = JSON.parse(await fs.readFile(pjPath, 'utf8'));
589
+ if (pj.dependencies && pj.dependencies[PKG_NAME] !== undefined) {
590
+ pj.dependencies[PKG_NAME] = latest;
591
+ await fs.writeFile(pjPath, JSON.stringify(pj, null, 2) + '\n');
592
+ }
593
+ } catch { /* 非关键 */ }
594
+ }
595
+ return { ok: true, output: `已更新到 v${latest}(sha512 校验通过,tarball 直装)`, needRestart: true, latest };
596
+ } finally {
597
+ await fs.rm(tmp, { recursive: true, force: true }).catch(() => {});
570
598
  }
571
- const r = await runProcess(cmd, args, { cwd: info.profileDir, timeoutMs: 300000, shell: !!tools.pnpmCmd });
572
- return { ok: r.ok, output: (r.stdout + '\n' + r.stderr).trim(), needRestart: r.ok, latest };
573
599
  }
574
600
 
575
601
  /* ============================ 插件入口 ============================ */
@@ -584,7 +610,10 @@ export function apply(ctx) {
584
610
  ctx.effect(() => {
585
611
  const disposers = [];
586
612
  const attempt = (def) => {
587
- const output = def.output ?? { type: 'object', additionalProperties: true };
613
+ const output = def.output ?? {
614
+ schema: { type: 'object', additionalProperties: true },
615
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
616
+ };
588
617
  for (const shape of [
589
618
  { name: def.name, description: def.description, parameters: def.schema, execute: def.execute },
590
619
  { name: def.name, description: def.description, inputSchema: def.schema, handler: def.execute },
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@ninkch/dsh-artcraft",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
- "description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness — agent tools, a same-origin HTTP API, and plugins-page status with self-update.",
5
+ "description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness ? agent tools, a same-origin HTTP API, and plugins-page status with self-update.",
6
6
  "keywords": [
7
7
  "deepseek-harness",
8
8
  "dsh-plugin",