@nowcrew/daemon 0.6.88-beta.33 → 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.
@@ -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
- while (!stopped && now() < deadline && lease === null) {
15
- lease = await deps.acquire();
16
- if (!lease)
17
- await wait();
18
- }
19
- if (!lease || stopped) {
20
- operations.set(id, { operationId: id, status: "failed", results: [], errorCode: "runtime_busy" });
21
- return;
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
- const exec = promisify(execFile);
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 before = await inventory();
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 !== "inspect")
192
- await run(["plugin", "marketplace", scope.runtime === "claude" ? "update" : "upgrade", MARKETPLACE]);
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.33",
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",
@@ -23,6 +23,7 @@
23
23
  "@nowcrew/cli": "0.4.17",
24
24
  "cross-spawn": "^7.0.6",
25
25
  "semver": "7.8.5",
26
+ "smol-toml": "1.8.0",
26
27
  "ws": "^8",
27
28
  "yaml": "^2.8.1",
28
29
  "zod": "^3.23.0"