@nowcrew/daemon 0.6.88-beta.26 → 0.6.88-beta.28
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/machine-info.js +4 -1
- package/dist/main.js +0 -0
- package/dist/marketplace-controller.js +75 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-runner.js +234 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/serve.js +8 -0
- package/package.json +9 -8
package/dist/machine-info.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retryHarnessProbe } from "./runtimes/harness-probe-retry.js";
|
|
2
|
+
import { MARKETPLACE_CAPABILITY } from "./marketplace-protocol.js";
|
|
2
3
|
/**
|
|
3
4
|
* 采集本机信息上报给控制面 (machine:hello):hostname / os / daemon 版本 / 已装 runtimes。
|
|
4
5
|
* runtimes 探测靠 `which <bin>`(win32 用 `where`),只报真实可执行的 CLI(用于展示 Detected Runtimes)。
|
|
@@ -36,6 +37,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
|
|
|
36
37
|
nativeRuntimeAdaptersReady: true,
|
|
37
38
|
});
|
|
38
39
|
export const DAEMON_CAPABILITIES = Object.freeze([
|
|
40
|
+
MARKETPLACE_CAPABILITY,
|
|
39
41
|
"scheduled_job_v1",
|
|
40
42
|
"reply_origin_v1",
|
|
41
43
|
"origin_decision_v1",
|
|
@@ -64,7 +66,8 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
|
|
|
64
66
|
const nativeWorkspaceReady = nativeWorkspacePlatform
|
|
65
67
|
&& readiness.resolverReady
|
|
66
68
|
&& readiness.nativeRuntimeAdaptersReady;
|
|
67
|
-
return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !==
|
|
69
|
+
return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
|
|
70
|
+
&& (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
|
|
68
71
|
&& (nativeWorkspacePlatform
|
|
69
72
|
|| (capability !== "project_skills_v1"
|
|
70
73
|
&& capability !== "project_skill_projection_v2"
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MarketplaceRequestSchema, marketplaceActive } from "./marketplace-protocol.js";
|
|
2
|
+
import { resolveMarketplaceScopes, runMarketplaceScope } from "./marketplace-runner.js";
|
|
3
|
+
export function createMarketplaceController(deps) {
|
|
4
|
+
const operations = new Map();
|
|
5
|
+
let stopped = false;
|
|
6
|
+
let current = null;
|
|
7
|
+
const now = deps.now ?? Date.now;
|
|
8
|
+
const wait = deps.wait ?? (() => new Promise(resolve => { setTimeout(resolve, 1000); }));
|
|
9
|
+
const execute = async (request) => {
|
|
10
|
+
const id = request.operationId;
|
|
11
|
+
const deadline = now() + 10 * 60_000;
|
|
12
|
+
let lease = null;
|
|
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;
|
|
22
|
+
}
|
|
23
|
+
operations.set(id, { operationId: id, status: "running", results: [], errorCode: null });
|
|
24
|
+
const scopes = await (deps.resolve ?? resolveMarketplaceScopes)(request.targets, deps.agentsRoot);
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const scope of scopes) {
|
|
27
|
+
if (stopped || now() >= deadline)
|
|
28
|
+
throw new Error("operation_deadline");
|
|
29
|
+
results.push(await (deps.run ?? runMarketplaceScope)(scope, request.mode));
|
|
30
|
+
operations.set(id, { operationId: id, status: "running", results: [...results], errorCode: null });
|
|
31
|
+
if (now() >= deadline)
|
|
32
|
+
throw new Error("operation_deadline");
|
|
33
|
+
}
|
|
34
|
+
const successes = results.filter(x => !["failed", "missing_cli"].includes(x.status)).length;
|
|
35
|
+
operations.set(id, { operationId: id, results, status: successes === results.length ? "completed" : successes ? "partial" : "failed", errorCode: null });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
const previous = operations.get(id);
|
|
39
|
+
operations.set(id, { ...previous, status: previous.results.some(x => !x.errorCode) ? "partial" : "failed", errorCode: "execution_failed" });
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
try {
|
|
43
|
+
await lease?.release();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
const previous = operations.get(id);
|
|
47
|
+
operations.set(id, { ...previous, status: "partial", errorCode: "execution_failed" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
handle(raw) {
|
|
53
|
+
const parsed = MarketplaceRequestSchema.safeParse(raw);
|
|
54
|
+
if (!parsed.success || stopped)
|
|
55
|
+
return { ok: false, error: "invalid_request" };
|
|
56
|
+
const request = parsed.data;
|
|
57
|
+
const existing = operations.get(request.operationId);
|
|
58
|
+
if (existing)
|
|
59
|
+
return { ok: true, data: existing };
|
|
60
|
+
if (request.action === "status")
|
|
61
|
+
return { ok: true, data: { operationId: request.operationId, status: "unknown", results: [], errorCode: "operation_unknown" } };
|
|
62
|
+
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
63
|
+
return { ok: false, error: "unsupported_platform" };
|
|
64
|
+
if (current !== null || [...operations.values()].some(x => marketplaceActive(x.status)))
|
|
65
|
+
return { ok: false, error: "runtime_busy" };
|
|
66
|
+
if (operations.size >= 100)
|
|
67
|
+
operations.delete(operations.keys().next().value);
|
|
68
|
+
const accepted = { operationId: request.operationId, status: "queued", results: [], errorCode: null };
|
|
69
|
+
operations.set(request.operationId, accepted);
|
|
70
|
+
current = execute(request).finally(() => { current = null; });
|
|
71
|
+
return { ok: true, data: accepted };
|
|
72
|
+
},
|
|
73
|
+
async stop() { stopped = true; await current; },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// 与另一个进程中的 marketplace-protocol.ts 保持一致;跨进程契约测试校验摘要。
|
|
3
|
+
export const MARKETPLACE_CAPABILITY = "marketplace_update_v1";
|
|
4
|
+
export const MARKETPLACE = "nowwork-pm";
|
|
5
|
+
export const PLUGIN = "AIinterview-plugin";
|
|
6
|
+
export const MarketplaceModeSchema = z.enum(["inspect", "refresh", "update"]);
|
|
7
|
+
export const MarketplaceStatusSchema = z.enum(["queued", "running", "completed", "partial", "failed", "unknown"]);
|
|
8
|
+
export const MarketplaceTargetSchema = z.object({
|
|
9
|
+
handle: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/).optional(),
|
|
10
|
+
runtime: z.enum(["claude", "codex"]),
|
|
11
|
+
provider: z.enum(["default", "custom", "deepseek"]).default("default"),
|
|
12
|
+
hasProviderKey: z.boolean().default(false),
|
|
13
|
+
paths: z.object({ HOME: z.string().min(1).max(4096).optional(), CODEX_HOME: z.string().min(1).max(4096).optional(),
|
|
14
|
+
CLAUDE_CONFIG_DIR: z.string().min(1).max(4096).optional() }).strict().default({}),
|
|
15
|
+
}).strict();
|
|
16
|
+
export const MarketplaceRequestSchema = z.object({
|
|
17
|
+
type: z.literal("marketplace:operation"), action: z.enum(["start", "status"]),
|
|
18
|
+
operationId: z.string().uuid(), mode: MarketplaceModeSchema.optional(),
|
|
19
|
+
targets: z.array(MarketplaceTargetSchema).min(1).max(202).optional(),
|
|
20
|
+
}).strict().refine(x => x.action !== "start" || (x.mode !== undefined && x.targets !== undefined));
|
|
21
|
+
const Version = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/).nullable();
|
|
22
|
+
const Revision = z.string().regex(/^[a-f0-9]{40}$/).nullable();
|
|
23
|
+
export const MarketplaceResultSchema = z.object({
|
|
24
|
+
runtime: z.enum(["claude", "codex"]), scopeId: z.string().regex(/^[a-f0-9]{16}$/),
|
|
25
|
+
agents: z.array(z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/)).max(200),
|
|
26
|
+
shared: z.boolean(), status: z.enum(["checked", "refreshed", "updated", "unchanged", "not_installed", "missing_cli", "failed"]),
|
|
27
|
+
beforeVersion: Version, afterVersion: Version, targetVersion: Version,
|
|
28
|
+
marketRevision: Revision, installedRevision: Revision, enabled: z.boolean().nullable(),
|
|
29
|
+
activation: z.enum(["not_changed", "reload_required", "unverified"]),
|
|
30
|
+
errorCode: z.enum(["scope_unavailable", "market_not_registered", "command_failed", "command_timeout", "invalid_inventory",
|
|
31
|
+
"verification_failed", "unsupported_installation", "cli_missing", "configuration_changed"]).nullable(),
|
|
32
|
+
}).strict();
|
|
33
|
+
export const MarketplaceReportSchema = z.object({
|
|
34
|
+
operationId: z.string().uuid(), status: MarketplaceStatusSchema,
|
|
35
|
+
results: z.array(MarketplaceResultSchema).max(202),
|
|
36
|
+
errorCode: z.enum(["runtime_busy", "execution_failed", "operation_unknown", "unsupported_platform", "too_many_scopes"]).nullable(),
|
|
37
|
+
}).strict();
|
|
38
|
+
export const marketplaceActive = (status) => status === "queued" || status === "running";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** 固定市场操作的短命 CLI 适配;原始命令输出不离开设备。 */
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { readFile, readdir, realpath, lstat, mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { join, resolve, isAbsolute, relative, sep } from "node:path";
|
|
8
|
+
import { applyProviderEnv } from "./provider-env.js";
|
|
9
|
+
import { augmentedPath } from "./runtime-path.js";
|
|
10
|
+
import { MARKETPLACE, PLUGIN } from "./marketplace-protocol.js";
|
|
11
|
+
const exec = promisify(execFile);
|
|
12
|
+
const SELECTOR = `${PLUGIN}@${MARKETPLACE}`;
|
|
13
|
+
const SOURCE = "git@codeup.aliyun.com:6128d2e757e7cd986dfae5ab/pm/AIinterview-agent-skill.git";
|
|
14
|
+
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
|
+
const obj = (x) => x !== null && typeof x === "object" && !Array.isArray(x) ? x : {};
|
|
34
|
+
const parse = (text) => { try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
39
|
+
} };
|
|
40
|
+
const version = (x) => typeof x === "string" && /^[A-Za-z0-9][A-Za-z0-9._+-]{0,79}$/.test(x) ? x : null;
|
|
41
|
+
const sha = (x) => typeof x === "string" && /^[a-f0-9]{40}$/.test(x) ? x : null;
|
|
42
|
+
const within = (root, candidate) => {
|
|
43
|
+
const child = relative(root, candidate);
|
|
44
|
+
return child === "" || (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child));
|
|
45
|
+
};
|
|
46
|
+
const canonical = async (path) => realpath(path).catch(() => resolve(path));
|
|
47
|
+
export async function resolveMarketplaceScopes(targets, agentsRoot, inherited = process.env) {
|
|
48
|
+
const scopes = new Map();
|
|
49
|
+
const trustedHome = await canonical(inherited.HOME ?? homedir());
|
|
50
|
+
for (const target of targets) {
|
|
51
|
+
const home = target.handle ? join(agentsRoot, target.handle, ".home") : homedir();
|
|
52
|
+
const agentHome = target.handle ? await canonical(home) : null;
|
|
53
|
+
const allowedHomeRoots = [trustedHome, ...(agentHome === null ? [] : [agentHome])];
|
|
54
|
+
const sharedConfigRoot = join(trustedHome, target.runtime === "codex" ? ".codex" : ".claude");
|
|
55
|
+
const agentConfigRoot = agentHome === null ? null : join(agentHome, target.runtime === "codex" ? ".codex" : ".claude");
|
|
56
|
+
const allowedConfigRoots = [sharedConfigRoot, ...(agentConfigRoot === null ? [] : [agentConfigRoot])];
|
|
57
|
+
const suppliedHome = target.paths.HOME;
|
|
58
|
+
if (suppliedHome !== undefined && !within(allowedHomeRoots[0], await canonical(suppliedHome))
|
|
59
|
+
&& (agentHome === null || !within(agentHome, await canonical(suppliedHome))))
|
|
60
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
61
|
+
const suppliedConfig = target.runtime === "codex" ? target.paths.CODEX_HOME : target.paths.CLAUDE_CONFIG_DIR;
|
|
62
|
+
const suppliedConfigPath = suppliedConfig === undefined ? null : await canonical(suppliedConfig);
|
|
63
|
+
if (suppliedConfigPath !== null && !allowedConfigRoots.some(root => within(root, suppliedConfigPath)))
|
|
64
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
65
|
+
// 与 local-executor 使用同一 provider 解析;凭据存在性只用于选择已配置的 provider,不传递凭据内容。
|
|
66
|
+
const env = applyProviderEnv({ ...inherited, ...target.paths }, target.runtime, target.handle ? {
|
|
67
|
+
provider: target.provider, providerApiKey: target.hasProviderKey ? "configured" : null,
|
|
68
|
+
} : {}, home);
|
|
69
|
+
for (const key of Object.keys(env))
|
|
70
|
+
if (/^(CREW_|ANTHROPIC_|OPENAI_|DEEPSEEK_)/.test(key))
|
|
71
|
+
delete env[key];
|
|
72
|
+
for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"])
|
|
73
|
+
delete env[key];
|
|
74
|
+
env.PATH = augmentedPath(inherited);
|
|
75
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
76
|
+
env.CI = "1";
|
|
77
|
+
const raw = target.runtime === "codex" ? env.CODEX_HOME ?? join(env.HOME ?? homedir(), ".codex")
|
|
78
|
+
: env.CLAUDE_CONFIG_DIR ?? join(env.HOME ?? homedir(), ".claude");
|
|
79
|
+
if (!isAbsolute(raw))
|
|
80
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
81
|
+
const configHome = await realpath(raw).catch(() => resolve(raw));
|
|
82
|
+
if (!allowedConfigRoots.some(root => within(root, configHome)))
|
|
83
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
84
|
+
if (target.runtime === "codex")
|
|
85
|
+
env.CODEX_HOME = configHome;
|
|
86
|
+
else
|
|
87
|
+
env.CLAUDE_CONFIG_DIR = configHome;
|
|
88
|
+
const key = `${target.runtime}:${configHome}`;
|
|
89
|
+
const prior = scopes.get(key);
|
|
90
|
+
if (prior) {
|
|
91
|
+
if (target.handle && !prior.agents.includes(target.handle))
|
|
92
|
+
prior.agents.push(target.handle);
|
|
93
|
+
prior.shared ||= !target.handle;
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
scopes.set(key, { runtime: target.runtime, configHome, env, agents: target.handle ? [target.handle] : [], shared: !target.handle });
|
|
97
|
+
}
|
|
98
|
+
return [...scopes.values()];
|
|
99
|
+
}
|
|
100
|
+
export function parsePluginInventory(runtime, value, configHome) {
|
|
101
|
+
const rows = Array.isArray(value) ? value : obj(value).installed;
|
|
102
|
+
if (!Array.isArray(rows))
|
|
103
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
104
|
+
const found = rows.map(obj).filter(x => (x.id ?? x.pluginId) === SELECTOR);
|
|
105
|
+
if (found.length === 0)
|
|
106
|
+
return null;
|
|
107
|
+
if (found.length !== 1)
|
|
108
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
109
|
+
const item = found[0];
|
|
110
|
+
const v = version(item.version);
|
|
111
|
+
if (!v || typeof item.enabled !== "boolean")
|
|
112
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
113
|
+
if (runtime === "claude" && item.scope !== "user")
|
|
114
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
115
|
+
const path = runtime === "claude" ? item.installPath : join(configHome, "plugins/cache", MARKETPLACE, PLUGIN, v);
|
|
116
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
117
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
118
|
+
return { version: v, enabled: item.enabled, path, revision: sha(obj(item.source).sha) };
|
|
119
|
+
}
|
|
120
|
+
/** 逐字节比对固定 Git 源子目录,不能只凭 CLI 退出码或版本字符串证明安装完成。 */
|
|
121
|
+
export async function verifyInstalledContent(path, revision, env, command) {
|
|
122
|
+
const temporary = await mkdtemp(join(tmpdir(), "nowwork-plugin-verify-"));
|
|
123
|
+
const options = { env, cwd: temporary };
|
|
124
|
+
try {
|
|
125
|
+
await command("git", ["init", "--bare", "."], options);
|
|
126
|
+
await command("git", ["fetch", "--depth=1", SOURCE, revision], options);
|
|
127
|
+
const tree = await command("git", ["ls-tree", "-rz", `${revision}:${SOURCE_PATH}`], options);
|
|
128
|
+
const expected = new Map();
|
|
129
|
+
for (const entry of tree.split("\0").filter(Boolean)) {
|
|
130
|
+
const match = /^(100644|100755) blob ([a-f0-9]{40})\t(.+)$/.exec(entry);
|
|
131
|
+
if (!match || match[3].split("/").some(x => x === ".." || x === "."))
|
|
132
|
+
throw new MarketplaceFailure("verification_failed");
|
|
133
|
+
expected.set(match[3], match[2]);
|
|
134
|
+
}
|
|
135
|
+
if (!expected.size || expected.size > 2000)
|
|
136
|
+
throw new MarketplaceFailure("verification_failed");
|
|
137
|
+
const actual = new Map();
|
|
138
|
+
let bytes = 0;
|
|
139
|
+
const walk = async (dir, prefix = "") => {
|
|
140
|
+
for (const name of await readdir(dir)) {
|
|
141
|
+
if (name === "__pycache__" || name === ".DS_Store")
|
|
142
|
+
continue;
|
|
143
|
+
const entry = join(dir, name);
|
|
144
|
+
const key = prefix + name;
|
|
145
|
+
const info = await lstat(entry);
|
|
146
|
+
if (info.isSymbolicLink())
|
|
147
|
+
throw new MarketplaceFailure("verification_failed");
|
|
148
|
+
if (info.isDirectory())
|
|
149
|
+
await walk(entry, key + "/");
|
|
150
|
+
else if (info.isFile()) {
|
|
151
|
+
bytes += info.size;
|
|
152
|
+
if (bytes > 100 * 1024 * 1024 || actual.size > 2000)
|
|
153
|
+
throw new MarketplaceFailure("verification_failed");
|
|
154
|
+
const data = await readFile(entry);
|
|
155
|
+
actual.set(key, createHash("sha1").update(`blob ${data.length}\0`).update(data).digest("hex"));
|
|
156
|
+
}
|
|
157
|
+
else
|
|
158
|
+
throw new MarketplaceFailure("verification_failed");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
await walk(path);
|
|
162
|
+
if (actual.size !== expected.size || [...expected].some(([name, hash]) => actual.get(name) !== hash))
|
|
163
|
+
throw new MarketplaceFailure("verification_failed");
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await rm(temporary, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function runMarketplaceScope(scope, mode, command = runMarketplaceCommand, verify = verifyInstalledContent) {
|
|
170
|
+
let result = { runtime: scope.runtime, scopeId: createHash("sha256").update(`${scope.runtime}:${scope.configHome}`).digest("hex").slice(0, 16),
|
|
171
|
+
agents: scope.agents, shared: scope.shared, status: "failed", beforeVersion: null, afterVersion: null, targetVersion: null,
|
|
172
|
+
marketRevision: null, installedRevision: null, enabled: null, activation: "unverified", errorCode: null };
|
|
173
|
+
const options = { env: scope.env, cwd: scope.configHome };
|
|
174
|
+
const run = (args) => command(scope.runtime, args, options);
|
|
175
|
+
const inventory = async () => parsePluginInventory(scope.runtime, parse(await run(["plugin", "list", "--json"])), scope.configHome);
|
|
176
|
+
try {
|
|
177
|
+
if (!(await lstat(scope.configHome)).isDirectory())
|
|
178
|
+
throw new MarketplaceFailure("scope_unavailable");
|
|
179
|
+
const markets = parse(await run(["plugin", "marketplace", "list", "--json"]));
|
|
180
|
+
const rows = Array.isArray(markets) ? markets : obj(markets).marketplaces;
|
|
181
|
+
if (!Array.isArray(rows))
|
|
182
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
183
|
+
const market = rows.map(obj).filter(x => x.name === MARKETPLACE);
|
|
184
|
+
if (market.length !== 1)
|
|
185
|
+
throw new MarketplaceFailure("market_not_registered");
|
|
186
|
+
const root = market[0].root ?? market[0].installLocation;
|
|
187
|
+
if (typeof root !== "string" || !isAbsolute(root))
|
|
188
|
+
throw new MarketplaceFailure("invalid_inventory");
|
|
189
|
+
const before = await inventory();
|
|
190
|
+
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]);
|
|
193
|
+
const revision = sha((await command("git", ["-C", root, "rev-parse", "HEAD"], options)).trim());
|
|
194
|
+
if (!revision)
|
|
195
|
+
throw new MarketplaceFailure("verification_failed");
|
|
196
|
+
const manifest = obj(parse(await readFile(join(root, scope.runtime === "claude" ? ".claude-plugin" : ".agents/plugins", "marketplace.json"), "utf8")));
|
|
197
|
+
const plugins = manifest.plugins;
|
|
198
|
+
const target = Array.isArray(plugins) ? plugins.map(obj).find(x => x.name === PLUGIN) : undefined;
|
|
199
|
+
const source = obj(target?.source);
|
|
200
|
+
const targetVersion = version(target?.version);
|
|
201
|
+
const targetSha = sha(source.sha);
|
|
202
|
+
result = { ...result, marketRevision: revision, targetVersion };
|
|
203
|
+
if (mode === "refresh")
|
|
204
|
+
return { ...result, status: "refreshed", activation: "not_changed" };
|
|
205
|
+
if (!before)
|
|
206
|
+
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
|
+
if (!targetVersion || !targetSha || source.url !== SOURCE || source.path !== SOURCE_PATH || source.source !== "git-subdir")
|
|
217
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
218
|
+
// Codex add 会启用插件;不偷偷改变用户已禁用的状态。
|
|
219
|
+
if (!before.enabled)
|
|
220
|
+
throw new MarketplaceFailure("unsupported_installation");
|
|
221
|
+
await run(scope.runtime === "claude" ? ["plugin", "update", SELECTOR, "--scope", "user", "--json"] : ["plugin", "add", SELECTOR, "--json"]);
|
|
222
|
+
const after = await inventory();
|
|
223
|
+
result = { ...result, afterVersion: after?.version ?? null, enabled: after?.enabled ?? null };
|
|
224
|
+
if (!after || after.version !== targetVersion || after.enabled !== before.enabled)
|
|
225
|
+
throw new MarketplaceFailure("verification_failed");
|
|
226
|
+
await verify(after.path, targetSha, scope.env, command);
|
|
227
|
+
return { ...result, status: before.version === after.version && before.revision === targetSha ? "unchanged" : "updated",
|
|
228
|
+
installedRevision: targetSha, activation: "reload_required" };
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const code = error instanceof MarketplaceFailure ? error.code : "scope_unavailable";
|
|
232
|
+
return { ...result, status: code === "cli_missing" ? "missing_cli" : "failed", errorCode: code };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createMarketplaceController } from "./marketplace-controller.js";
|
|
2
|
+
import { prepareDaemonRestart } from "./daemon-updater.js";
|
|
3
|
+
export function createServeMarketplace(config, localSlots, hostCoordinator) {
|
|
4
|
+
const controller = createMarketplaceController({ agentsRoot: config.agentsRoot,
|
|
5
|
+
acquire: async () => { const result = await prepareDaemonRestart({ localSlots, hostCoordinator }); return result.ok ? result : null; } });
|
|
6
|
+
return {
|
|
7
|
+
handle(raw, send) {
|
|
8
|
+
if (!raw || typeof raw !== "object" || !("type" in raw) || raw.type !== "marketplace:operation")
|
|
9
|
+
return false;
|
|
10
|
+
const { reqId, ...request } = raw;
|
|
11
|
+
if (typeof reqId === "string")
|
|
12
|
+
send({ type: "fs:result", reqId, ...controller.handle(request) });
|
|
13
|
+
return true;
|
|
14
|
+
},
|
|
15
|
+
stop: controller.stop,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -3,3 +3,13 @@ export const runtimeHealthProbeHandler = (configuredProbe, executionRuntimesKnow
|
|
|
3
3
|
...(executionRuntimesKnown ? { availableRuntimes: detectedExecutionRuntimes } : {}),
|
|
4
4
|
}));
|
|
5
5
|
export const handleRuntimeHealthProbeFrame = (decoded, configuredProbe, executionRuntimesKnown, detectedExecutionRuntimes) => handleRuntimeProbeFrame(decoded, runtimeHealthProbeHandler(configuredProbe, executionRuntimesKnown, detectedExecutionRuntimes));
|
|
6
|
+
/** 将探针结果按既有 RPC 帧返回,serve 只保留路由选择。 */
|
|
7
|
+
export async function replyRuntimeHealthProbe(decoded, configuredProbe, known, runtimes, send) {
|
|
8
|
+
const result = await handleRuntimeHealthProbeFrame(decoded, configuredProbe, known, runtimes);
|
|
9
|
+
if (result !== null) {
|
|
10
|
+
try {
|
|
11
|
+
send({ type: "fs:result", ...result });
|
|
12
|
+
}
|
|
13
|
+
catch { /* Server 查询超时处理断连 */ }
|
|
14
|
+
}
|
|
15
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -51,6 +51,7 @@ import { initializedProjectSkillsReconciler } from "./project-skills/initialized
|
|
|
51
51
|
import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
52
52
|
import { probeRuntimeHealth } from "./runtime-health.js";
|
|
53
53
|
import { handleRuntimeHealthProbeFrame } from "./runtime-health-handler.js";
|
|
54
|
+
import { createServeMarketplace } from "./marketplace-serve.js";
|
|
54
55
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
55
56
|
import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
|
|
56
57
|
import { bindStorageServerIdentity, createStorageIdentityAdmission, } from "./storage-serve-adapter.js";
|
|
@@ -126,6 +127,7 @@ export function serve(config, opts = {}) {
|
|
|
126
127
|
catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
|
|
127
128
|
},
|
|
128
129
|
});
|
|
130
|
+
const marketplace = createServeMarketplace(config, localSlots, hostCoordinator);
|
|
129
131
|
const storageIdentityAdmission = createStorageIdentityAdmission(opts.storageRuntime);
|
|
130
132
|
const projectionCoordinator = createAgentProjectionCoordinator();
|
|
131
133
|
const { projectRegistry, projectWorkspaceRegistry, projectDependencyProvisioner } = createProjectRegistryWiring(config.agentsRoot, opts.execution?.dependencies);
|
|
@@ -838,6 +840,11 @@ export function serve(config, opts = {}) {
|
|
|
838
840
|
}
|
|
839
841
|
return;
|
|
840
842
|
}
|
|
843
|
+
if (marketplace.handle(msg, frame => { try {
|
|
844
|
+
ws?.send(JSON.stringify(frame));
|
|
845
|
+
}
|
|
846
|
+
catch { /* server 会按请求超时处理 */ } }))
|
|
847
|
+
return;
|
|
841
848
|
if (await agentAbilityRuntime.tryHandleControlMessage(msg, serverCapabilities, (frame) => ws?.send(JSON.stringify(frame))))
|
|
842
849
|
return;
|
|
843
850
|
// 导入 raft agent 工作区:inspect 反填 name/description;import 复制用户内容
|
|
@@ -1328,6 +1335,7 @@ export function serve(config, opts = {}) {
|
|
|
1328
1335
|
const pending = [
|
|
1329
1336
|
webSocketClosed,
|
|
1330
1337
|
updateController.drain(),
|
|
1338
|
+
marketplace.stop(),
|
|
1331
1339
|
executionFrameQueue,
|
|
1332
1340
|
...executionRuns.values(),
|
|
1333
1341
|
...[...legacyRuns.values()].map((run) => run.done),
|
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.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
19
26
|
"dependencies": {
|
|
20
27
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
28
|
"@nowcrew/cli": "0.4.14",
|
|
@@ -36,11 +43,5 @@
|
|
|
36
43
|
"tsx": "^4.19.0",
|
|
37
44
|
"typescript": "^5.6.0",
|
|
38
45
|
"vitest": "^2.1.0"
|
|
39
|
-
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
43
|
-
"test": "vitest run",
|
|
44
|
-
"typecheck": "tsc --noEmit"
|
|
45
46
|
}
|
|
46
|
-
}
|
|
47
|
+
}
|