@nowcrew/daemon 0.6.88-beta.32 → 0.6.88-beta.34
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/dist/computer-main.js +0 -0
- package/dist/main.js +0 -0
- package/dist/marketplace-command.js +21 -0
- package/dist/marketplace-controller.js +14 -9
- package/dist/marketplace-remote.js +48 -0
- package/dist/marketplace-runner.js +36 -35
- package/dist/marketplace-source.js +103 -0
- package/package.json +9 -9
package/dist/computer-main.js
CHANGED
|
File without changes
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
export class MarketplaceFailure extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(code);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
export const runMarketplaceCommand = async (bin, args, options) => {
|
|
12
|
+
try {
|
|
13
|
+
const result = await exec(bin, [...args], { ...options, timeout: 120_000, maxBuffer: 4 * 1024 * 1024,
|
|
14
|
+
killSignal: "SIGKILL", encoding: "utf8" });
|
|
15
|
+
return result.stdout;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const value = error;
|
|
19
|
+
throw new MarketplaceFailure(value.code === "ENOENT" ? "cli_missing" : value.killed ? "command_timeout" : "command_failed");
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -11,17 +11,22 @@ export function createMarketplaceController(deps) {
|
|
|
11
11
|
const deadline = now() + 10 * 60_000;
|
|
12
12
|
let lease = null;
|
|
13
13
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
|
|
15
|
+
// 查询不使用任务执行槽;只有写入运行时共用市场/插件目录时才等待独占保护。
|
|
16
|
+
if (request.mode === "update") {
|
|
17
|
+
while (!stopped && now() < deadline && lease === null) {
|
|
18
|
+
lease = await deps.acquire();
|
|
19
|
+
if (!lease)
|
|
20
|
+
await wait();
|
|
21
|
+
}
|
|
22
|
+
if (!lease || stopped) {
|
|
23
|
+
operations.set(id, { operationId: id, status: "failed", results: [], errorCode: "runtime_busy" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
22
26
|
}
|
|
27
|
+
if (stopped || now() >= deadline)
|
|
28
|
+
throw new Error("operation_deadline");
|
|
23
29
|
operations.set(id, { operationId: id, status: "running", results: [], errorCode: null });
|
|
24
|
-
const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
|
|
25
30
|
const results = [];
|
|
26
31
|
for (const scope of scopes) {
|
|
27
32
|
if (stopped || now() >= deadline)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
|
|
6
|
+
import { MarketplaceFailure } from "./marketplace-command.js";
|
|
7
|
+
import { validateMarketplaceSource } from "./marketplace-source.js";
|
|
8
|
+
const Manifest = z.object({ name: z.literal(MARKETPLACE), plugins: z.array(z.object({ name: z.string() }).passthrough()) });
|
|
9
|
+
const Target = z.object({
|
|
10
|
+
version: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/),
|
|
11
|
+
source: z.object({ source: z.literal("git-subdir"),
|
|
12
|
+
url: z.literal("git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git"),
|
|
13
|
+
path: z.literal("plugins/AIinterview-plugin"), sha: z.string().regex(/^[a-f0-9]{40}$/) }),
|
|
14
|
+
});
|
|
15
|
+
export async function readRemoteMarketplace(runtime, registered, env, command) {
|
|
16
|
+
const source = validateMarketplaceSource(registered.url, registered.ref);
|
|
17
|
+
const temporary = await mkdtemp(join(tmpdir(), "nowwork-marketplace-query-"));
|
|
18
|
+
const options = { env, cwd: temporary };
|
|
19
|
+
try {
|
|
20
|
+
await command("git", ["init", "--bare", "--template=", "."], options);
|
|
21
|
+
await command("git", ["fetch", "--depth=1", "--no-tags", source.url, source.ref], options);
|
|
22
|
+
const revision = (await command("git", ["rev-parse", "--verify", "FETCH_HEAD"], options)).trim();
|
|
23
|
+
if (!/^[a-f0-9]{40}$/.test(revision))
|
|
24
|
+
throw new MarketplaceFailure("verification_failed");
|
|
25
|
+
const path = runtime === "claude" ? ".claude-plugin/marketplace.json" : ".agents/plugins/marketplace.json";
|
|
26
|
+
const raw = await command("git", ["show", `${revision}:${path}`], options);
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
33
|
+
}
|
|
34
|
+
const manifest = Manifest.safeParse(value);
|
|
35
|
+
if (!manifest.success)
|
|
36
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
37
|
+
const matches = manifest.data.plugins.filter(plugin => plugin.name === PLUGIN);
|
|
38
|
+
if (matches.length !== 1)
|
|
39
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
40
|
+
const target = Target.safeParse(matches[0]);
|
|
41
|
+
if (!target.success)
|
|
42
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
43
|
+
return { marketRevision: revision, targetVersion: target.data.version };
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await rm(temporary, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
/** 固定市场操作的短命 CLI 适配;原始命令输出不离开设备。 */
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
|
-
import { promisify } from "node:util";
|
|
4
2
|
import { createHash } from "node:crypto";
|
|
5
3
|
import { readFile, readdir, realpath, lstat, mkdtemp, rm } from "node:fs/promises";
|
|
6
4
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -8,28 +6,13 @@ import { join, resolve, isAbsolute, relative, sep } from "node:path";
|
|
|
8
6
|
import { applyProviderEnv } from "./provider-env.js";
|
|
9
7
|
import { augmentedPath } from "./runtime-path.js";
|
|
10
8
|
import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
|
|
11
|
-
|
|
9
|
+
import { MarketplaceFailure, runMarketplaceCommand } from "./marketplace-command.js";
|
|
10
|
+
import { readRemoteMarketplace } from "./marketplace-remote.js";
|
|
11
|
+
import { readMarketplaceSource } from "./marketplace-source.js";
|
|
12
|
+
export { MarketplaceFailure, runMarketplaceCommand } from "./marketplace-command.js";
|
|
12
13
|
const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
|
|
13
14
|
const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
|
|
14
15
|
const SOURCE_PATH = "plugins/AIinterview-plugin";
|
|
15
|
-
export class MarketplaceFailure extends Error {
|
|
16
|
-
code;
|
|
17
|
-
constructor(code) {
|
|
18
|
-
super(code);
|
|
19
|
-
this.code = code;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export const runMarketplaceCommand = async (bin, args, options) => {
|
|
23
|
-
try {
|
|
24
|
-
const result = await exec(bin, [...args], { ...options, timeout: 120_000, maxBuffer: 4 * 1024 * 1024,
|
|
25
|
-
killSignal: "SIGKILL", encoding: "utf8" });
|
|
26
|
-
return result.stdout;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
const value = error;
|
|
30
|
-
throw new MarketplaceFailure(value.code === "ENOENT" ? "cli_missing" : value.killed ? "command_timeout" : "command_failed");
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
16
|
const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
34
17
|
const parse = (text) => { try {
|
|
35
18
|
return JSON.parse(text);
|
|
@@ -117,6 +100,26 @@ export function parsePluginInventory(runtime, value, configHome) {
|
|
|
117
100
|
throw new MarketplaceFailure("invalid_inventory");
|
|
118
101
|
return { version: v, enabled: item.enabled, path, revision: sha(obj(item.source).sha) };
|
|
119
102
|
}
|
|
103
|
+
/** 查询与任务并行;外部更新改变安装清单时重读一次,避免混合两代安装事实。 */
|
|
104
|
+
async function readInstalledSnapshot(runtime, inventory) {
|
|
105
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
106
|
+
const before = await inventory();
|
|
107
|
+
let valid = before === null;
|
|
108
|
+
if (before !== null) {
|
|
109
|
+
try {
|
|
110
|
+
const manifest = obj(parse(await readFile(join(before.path, runtime === "claude" ? ".claude-plugin" : ".codex-plugin", "plugin.json"), "utf8")));
|
|
111
|
+
valid = manifest.name === PLUGIN && version(manifest.version) === before.version;
|
|
112
|
+
}
|
|
113
|
+
catch { /* 清单变化则重读;稳定但不可读的安装仍报告核验失败。 */ }
|
|
114
|
+
}
|
|
115
|
+
if (JSON.stringify(before) !== JSON.stringify(await inventory()))
|
|
116
|
+
continue;
|
|
117
|
+
if (!valid)
|
|
118
|
+
throw new MarketplaceFailure("verification_failed");
|
|
119
|
+
return before;
|
|
120
|
+
}
|
|
121
|
+
throw new MarketplaceFailure("configuration_changed");
|
|
122
|
+
}
|
|
120
123
|
/** 逐字节比对固定 Git 源子目录,不能只凭 CLI 退出码或版本字符串证明安装完成。 */
|
|
121
124
|
export async function verifyInstalledContent(path, revision, env, command) {
|
|
122
125
|
const temporary = await mkdtemp(join(tmpdir(), "nowwork-plugin-verify-"));
|
|
@@ -186,10 +189,19 @@ export async function runMarketplaceScope(scope, mode, command = runMarketplaceC
|
|
|
186
189
|
const root = market[0].root ?? market[0].installLocation;
|
|
187
190
|
if (typeof root !== "string" || !isAbsolute(root))
|
|
188
191
|
throw new MarketplaceFailure("invalid_inventory");
|
|
189
|
-
const
|
|
192
|
+
const registered = await readMarketplaceSource(scope.runtime, scope.configHome, market[0]);
|
|
193
|
+
const before = mode === "update" ? await inventory() : await readInstalledSnapshot(scope.runtime, inventory);
|
|
190
194
|
result = { ...result, beforeVersion: before?.version ?? null, afterVersion: before?.version ?? null, enabled: before?.enabled ?? null };
|
|
191
|
-
if (mode
|
|
192
|
-
|
|
195
|
+
if (mode === "inspect")
|
|
196
|
+
return { ...result, status: before ? "checked" : "not_installed", activation: "unverified" };
|
|
197
|
+
if (mode === "refresh") {
|
|
198
|
+
const latest = await readRemoteMarketplace(scope.runtime, registered, scope.env, command);
|
|
199
|
+
const current = await readMarketplaceSource(scope.runtime, scope.configHome, market[0]);
|
|
200
|
+
if (current.url !== registered.url || current.ref !== registered.ref)
|
|
201
|
+
throw new MarketplaceFailure("configuration_changed");
|
|
202
|
+
return { ...result, ...latest, status: "refreshed", activation: "not_changed" };
|
|
203
|
+
}
|
|
204
|
+
await run(["plugin", "marketplace", scope.runtime === "claude" ? "update" : "upgrade", MARKETPLACE]);
|
|
193
205
|
const revision = sha((await command("git", ["-C", root, "rev-parse", "HEAD"], options)).trim());
|
|
194
206
|
if (!revision)
|
|
195
207
|
throw new MarketplaceFailure("verification_failed");
|
|
@@ -200,19 +212,8 @@ export async function runMarketplaceScope(scope, mode, command = runMarketplaceC
|
|
|
200
212
|
const targetVersion = version(target?.version);
|
|
201
213
|
const targetSha = sha(source.sha);
|
|
202
214
|
result = { ...result, marketRevision: revision, targetVersion };
|
|
203
|
-
if (mode === "refresh")
|
|
204
|
-
return { ...result, status: "refreshed", activation: "not_changed" };
|
|
205
215
|
if (!before)
|
|
206
216
|
return { ...result, status: "not_installed", activation: "not_changed" };
|
|
207
|
-
if (mode === "inspect") {
|
|
208
|
-
const installed = obj(parse(await readFile(join(before.path, scope.runtime === "claude" ? ".claude-plugin" : ".codex-plugin", "plugin.json"), "utf8")));
|
|
209
|
-
const actualVersion = version(installed.version);
|
|
210
|
-
result = { ...result, afterVersion: actualVersion };
|
|
211
|
-
if (installed.name !== PLUGIN || actualVersion !== before.version)
|
|
212
|
-
throw new MarketplaceFailure("verification_failed");
|
|
213
|
-
// 清单可能来自市场元数据;没有文件内容核验就不报告已安装源提交。
|
|
214
|
-
return { ...result, status: "checked", installedRevision: null, activation: "unverified" };
|
|
215
|
-
}
|
|
216
217
|
if (!targetVersion || !targetSha || source.url !== SOURCE || source.path !== SOURCE_PATH || source.source !== "git-subdir")
|
|
217
218
|
throw new MarketplaceFailure("unsupported_installation");
|
|
218
219
|
// Codex add 会启用插件;不偷偷改变用户已禁用的状态。
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
import { MARKETPLACE } from "./marketplace-protocol.js";
|
|
5
|
+
import { MarketplaceFailure } from "./marketplace-command.js";
|
|
6
|
+
const REPOSITORY_PATH = "6128d2e757e7cd986dfae5ab/pm/nowwork-pm-skills-marketplace";
|
|
7
|
+
const obj = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8
|
+
/** 固定仓库身份,但保留用户登记的 SSH/HTTPS 传输方式;不允许 URL 内嵌凭据。 */
|
|
9
|
+
function allowedRepository(value) {
|
|
10
|
+
if (typeof value !== "string" || value.length > 4096 || /[\s\u0000-\u001f\u007f\\]/u.test(value))
|
|
11
|
+
return false;
|
|
12
|
+
let host;
|
|
13
|
+
let path;
|
|
14
|
+
const scp = /^git@([^/:]+):(.+)$/u.exec(value);
|
|
15
|
+
if (scp) {
|
|
16
|
+
host = scp[1];
|
|
17
|
+
path = scp[2];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (!["https:", "ssh:"].includes(url.protocol) || url.search || url.hash || url.password)
|
|
28
|
+
return false;
|
|
29
|
+
if (url.protocol === "https:" && (url.username || url.port))
|
|
30
|
+
return false;
|
|
31
|
+
if (url.protocol === "ssh:" && (url.username !== "git" || (url.port !== "" && url.port !== "22")))
|
|
32
|
+
return false;
|
|
33
|
+
host = url.hostname;
|
|
34
|
+
path = url.pathname.replace(/^\//u, "");
|
|
35
|
+
}
|
|
36
|
+
return host.toLowerCase() === "codeup.aliyun.com" && path.replace(/\/$/u, "").replace(/\.git$/u, "") === REPOSITORY_PATH;
|
|
37
|
+
}
|
|
38
|
+
export function validateMarketplaceSource(url, ref) {
|
|
39
|
+
const selectedRef = ref === undefined ? "HEAD" : ref;
|
|
40
|
+
if (!allowedRepository(url) || typeof selectedRef !== "string" || !selectedRef || selectedRef.length > 1024
|
|
41
|
+
|| /^[+-]/u.test(selectedRef) || selectedRef === "@" || selectedRef.includes("..") || selectedRef.includes("@{")
|
|
42
|
+
|| /[\s\u0000-\u001f\u007f~^:?*\[\\]/u.test(selectedRef)
|
|
43
|
+
|| selectedRef.split("/").some(part => !part || part.startsWith(".") || part.endsWith(".") || part.endsWith(".lock"))) {
|
|
44
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
45
|
+
}
|
|
46
|
+
return { url, ref: selectedRef };
|
|
47
|
+
}
|
|
48
|
+
async function registrationFile(path) {
|
|
49
|
+
try {
|
|
50
|
+
if ((await stat(path)).size > 1024 * 1024)
|
|
51
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
52
|
+
return await readFile(path, "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof MarketplaceFailure)
|
|
56
|
+
throw error;
|
|
57
|
+
throw new MarketplaceFailure("configuration_changed");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** 列表会省略 ref;以同一配置目录内的原生登记文件为准,不从已安装缓存猜分支。 */
|
|
61
|
+
export async function readMarketplaceSource(runtime, configHome, listed) {
|
|
62
|
+
let source;
|
|
63
|
+
let listedUrl;
|
|
64
|
+
if (runtime === "claude") {
|
|
65
|
+
let registered;
|
|
66
|
+
const text = await registrationFile(join(configHome, "plugins/known_marketplaces.json"));
|
|
67
|
+
try {
|
|
68
|
+
registered = obj(obj(JSON.parse(text))[MARKETPLACE]);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
72
|
+
}
|
|
73
|
+
const entry = obj(registered.source);
|
|
74
|
+
if (entry.source !== "git" || listed.source !== "git")
|
|
75
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
76
|
+
source = validateMarketplaceSource(entry.url, entry.ref);
|
|
77
|
+
listedUrl = listed.url ?? listed.repo;
|
|
78
|
+
if (typeof registered.installLocation !== "string" || typeof listed.installLocation !== "string")
|
|
79
|
+
throw new MarketplaceFailure("configuration_changed");
|
|
80
|
+
const canonical = (path) => realpath(path).catch(() => resolve(path));
|
|
81
|
+
if (await canonical(registered.installLocation) !== await canonical(listed.installLocation))
|
|
82
|
+
throw new MarketplaceFailure("configuration_changed");
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
let entry;
|
|
86
|
+
const text = await registrationFile(join(configHome, "config.toml"));
|
|
87
|
+
try {
|
|
88
|
+
entry = obj(obj(parseToml(text).marketplaces)[MARKETPLACE]);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
92
|
+
}
|
|
93
|
+
const origin = obj(listed.marketplaceSource);
|
|
94
|
+
if (entry.source_type !== "git" || origin.sourceType !== "git")
|
|
95
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
96
|
+
source = validateMarketplaceSource(entry.source, entry.ref);
|
|
97
|
+
listedUrl = origin.source;
|
|
98
|
+
}
|
|
99
|
+
// 同一轮清单与登记文件来源不一致,可能正被重新登记;不擅自改查另一来源。
|
|
100
|
+
if (listedUrl !== source.url)
|
|
101
|
+
throw new MarketplaceFailure("configuration_changed");
|
|
102
|
+
return source;
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.88-beta.
|
|
3
|
+
"version": "0.6.88-beta.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,18 +18,12 @@
|
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
23
|
-
"build": "tsc -p tsconfig.json",
|
|
24
|
-
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
25
|
-
"test": "vitest run",
|
|
26
|
-
"typecheck": "tsc --noEmit"
|
|
27
|
-
},
|
|
28
21
|
"dependencies": {
|
|
29
22
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
30
23
|
"@nowcrew/cli": "0.4.17",
|
|
31
24
|
"cross-spawn": "^7.0.6",
|
|
32
25
|
"semver": "7.8.5",
|
|
26
|
+
"smol-toml": "1.8.0",
|
|
33
27
|
"ws": "^8",
|
|
34
28
|
"yaml": "^2.8.1",
|
|
35
29
|
"zod": "^3.23.0"
|
|
@@ -45,5 +39,11 @@
|
|
|
45
39
|
"tsx": "^4.19.0",
|
|
46
40
|
"typescript": "^5.6.0",
|
|
47
41
|
"vitest": "^2.1.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
45
|
+
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
}
|