@ninkch/dsh-artcraft 0.2.5 → 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.
- package/index.js +74 -48
- 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.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
throw new Error('
|
|
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
|
/* ============================ 插件入口 ============================ */
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-artcraft",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Drive the open-source ArtCraft studio apps (PhotoCraft / FilmCraft / EffectCraft) from DeepSeek Harness
|
|
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",
|