@gaoding/cli 0.0.0 → 1.0.0
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/LICENSE +21 -0
- package/README.md +40 -3
- package/contracts/operations/agent.send/input.schema.json +353 -0
- package/contracts/operations/agent.send/output.schema.json +271 -0
- package/contracts/operations/auth.status/input.schema.json +7 -0
- package/contracts/operations/auth.status/output.schema.json +132 -0
- package/contracts/operations/dam.copy/input.schema.json +19 -0
- package/contracts/operations/dam.copy/output.schema.json +19 -0
- package/contracts/operations/dam.delete/input.schema.json +23 -0
- package/contracts/operations/dam.delete/output.schema.json +6 -0
- package/contracts/operations/dam.download/input.schema.json +14 -0
- package/contracts/operations/dam.download/output.schema.json +25 -0
- package/contracts/operations/dam.folder.list/input.schema.json +15 -0
- package/contracts/operations/dam.folder.list/output.schema.json +37 -0
- package/contracts/operations/dam.get/input.schema.json +16 -0
- package/contracts/operations/dam.get/output.schema.json +46 -0
- package/contracts/operations/dam.list/input.schema.json +44 -0
- package/contracts/operations/dam.list/output.schema.json +71 -0
- package/contracts/operations/dam.move/input.schema.json +19 -0
- package/contracts/operations/dam.move/output.schema.json +18 -0
- package/contracts/operations/dam.recycle.list/input.schema.json +13 -0
- package/contracts/operations/dam.recycle.list/output.schema.json +34 -0
- package/contracts/operations/dam.recycle.restore/input.schema.json +14 -0
- package/contracts/operations/dam.recycle.restore/output.schema.json +6 -0
- package/contracts/operations/dam.rename/input.schema.json +18 -0
- package/contracts/operations/dam.rename/output.schema.json +18 -0
- package/contracts/operations/dam.repository.list/input.schema.json +8 -0
- package/contracts/operations/dam.repository.list/output.schema.json +52 -0
- package/contracts/operations/dam.search/input.schema.json +46 -0
- package/contracts/operations/dam.search/output.schema.json +71 -0
- package/contracts/operations/dam.tag.list/input.schema.json +12 -0
- package/contracts/operations/dam.tag.list/output.schema.json +26 -0
- package/contracts/operations/dam.upload/input.schema.json +31 -0
- package/contracts/operations/dam.upload/output.schema.json +46 -0
- package/contracts/operations/editor.apply/input.schema.json +134 -0
- package/contracts/operations/editor.apply/output.schema.json +15 -0
- package/contracts/operations/editor.connect/input.schema.json +15 -0
- package/contracts/operations/editor.connect/output.schema.json +17 -0
- package/contracts/operations/editor.disconnect/input.schema.json +8 -0
- package/contracts/operations/editor.disconnect/output.schema.json +14 -0
- package/contracts/operations/editor.save/input.schema.json +8 -0
- package/contracts/operations/editor.save/output.schema.json +23 -0
- package/contracts/operations/editor.screenshot/input.schema.json +8 -0
- package/contracts/operations/editor.screenshot/output.schema.json +16 -0
- package/contracts/operations/editor.snapshot/input.schema.json +8 -0
- package/contracts/operations/editor.snapshot/output.schema.json +113 -0
- package/contracts/operations/model.get/input.schema.json +18 -0
- package/contracts/operations/model.get/output.schema.json +191 -0
- package/contracts/operations/model.list/input.schema.json +15 -0
- package/contracts/operations/model.list/output.schema.json +111 -0
- package/contracts/operations/org.current/input.schema.json +7 -0
- package/contracts/operations/org.current/output.schema.json +49 -0
- package/contracts/operations/org.list/input.schema.json +7 -0
- package/contracts/operations/org.list/output.schema.json +71 -0
- package/contracts/operations/tool.call/input.schema.json +35 -0
- package/contracts/operations/tool.call/output.schema.json +129 -0
- package/contracts/operations/tool.list/input.schema.json +7 -0
- package/contracts/operations/tool.list/output.schema.json +45 -0
- package/dist/bin/gd-cli.js +60 -0
- package/dist/bin/postinstall.js +42 -0
- package/dist/src/bootstrap/create-cli.js +112 -0
- package/dist/src/bootstrap/create-runtime.js +290 -0
- package/dist/src/bootstrap/validators.js +157 -0
- package/dist/src/cli/action-binding.js +109 -0
- package/dist/src/cli/agent-commands.js +49 -0
- package/dist/src/cli/auth-commands.js +55 -0
- package/dist/src/cli/dam-commands.js +469 -0
- package/dist/src/cli/editor-commands.js +73 -0
- package/dist/src/cli/errors.js +157 -0
- package/dist/src/cli/model-commands.js +61 -0
- package/dist/src/cli/org-commands.js +58 -0
- package/dist/src/cli/presenter.js +105 -0
- package/dist/src/cli/prompt.js +30 -0
- package/dist/src/cli/tool-commands.js +77 -0
- package/dist/src/cli/update-command.js +14 -0
- package/dist/src/contracts/schema.js +4 -0
- package/dist/src/features/agent/creative-agent-adapter.js +36 -0
- package/dist/src/features/agent/creative-protocol.js +432 -0
- package/dist/src/features/agent/creative-stream.js +127 -0
- package/dist/src/features/agent/use-cases.js +150 -0
- package/dist/src/features/auth/access-policy.js +101 -0
- package/dist/src/features/auth/credential-store.js +62 -0
- package/dist/src/features/auth/sso-service.js +194 -0
- package/dist/src/features/auth/state.js +129 -0
- package/dist/src/features/auth/use-cases.js +136 -0
- package/dist/src/features/dam/asset-projection.js +278 -0
- package/dist/src/features/dam/dam-api-adapter.js +562 -0
- package/dist/src/features/dam/file-downloader.js +242 -0
- package/dist/src/features/dam/object-storage.js +193 -0
- package/dist/src/features/dam/operation-executor.js +72 -0
- package/dist/src/features/dam/registered-uploader.js +247 -0
- package/dist/src/features/dam/repository-catalog.js +127 -0
- package/dist/src/features/dam/storage-upload.js +141 -0
- package/dist/src/features/dam/transient-uploader.js +17 -0
- package/dist/src/features/dam/use-cases.js +470 -0
- package/dist/src/features/editor/bridge-client.js +109 -0
- package/dist/src/features/editor/bridge-process.js +222 -0
- package/dist/src/features/editor/bridge-server.js +337 -0
- package/dist/src/features/editor/protocol.js +1 -0
- package/dist/src/features/editor/public-url.js +36 -0
- package/dist/src/features/editor/session-state.js +78 -0
- package/dist/src/features/editor/session.js +207 -0
- package/dist/src/features/editor/use-cases.js +88 -0
- package/dist/src/features/org/org-service.js +76 -0
- package/dist/src/features/org/use-cases.js +156 -0
- package/dist/src/features/skill/bundled-skills.js +55 -0
- package/dist/src/features/skill/installer.js +271 -0
- package/dist/src/features/tool/catalog.js +83 -0
- package/dist/src/features/tool/dynamic-schema.js +128 -0
- package/dist/src/features/tool/mns-catalog-adapter.js +265 -0
- package/dist/src/features/tool/tool-api-adapter.js +352 -0
- package/dist/src/features/tool/use-cases.js +177 -0
- package/dist/src/features/update/latest-version.js +37 -0
- package/dist/src/features/update/update-notification.js +262 -0
- package/dist/src/features/update/update-service.js +183 -0
- package/dist/src/platform/json-input.js +67 -0
- package/dist/src/platform/local-json-file.js +59 -0
- package/dist/src/platform/open-browser.js +8 -0
- package/dist/src/platform/redact.js +80 -0
- package/dist/src/platform/remote-error-evidence.js +80 -0
- package/dist/src/platform/remote-protocol.js +10 -0
- package/dist/src/platform/safe-upload-file.js +146 -0
- package/dist/src/platform/signature.js +20 -0
- package/dist/src/platform/signed-http-transport.js +143 -0
- package/dist/src/platform/url-safety.js +100 -0
- package/dist/src/telemetry/invocation.js +156 -0
- package/dist/src/telemetry/sls-sink.js +31 -0
- package/package.json +54 -8
- package/skills/gd-cli/SKILL.md +15 -0
- package/skills/gd-cli/references/auth-org.md +9 -0
- package/skills/gd-cli/references/creation.md +115 -0
- package/skills/gd-cli/references/dam.md +52 -0
- package/skills/gd-cli/references/editor.md +254 -0
- package/skills/gd-cli/references/errors.md +13 -0
- package/skills/gd-cli/references/update.md +13 -0
- package/index.js +0 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { parseSafeRemoteAssetUrl, UrlSafetyError } from "../../platform/url-safety.js";
|
|
2
|
+
import { findToolModel, projectModelDetail, projectModelList, projectToolList, ToolInputError } from "./catalog.js";
|
|
3
|
+
import { assertModelArguments, buildToolArgumentsSchema } from "./dynamic-schema.js";
|
|
4
|
+
const mediaTypes = {
|
|
5
|
+
avif: "image/avif",
|
|
6
|
+
gif: "image/gif",
|
|
7
|
+
heic: "image/heic",
|
|
8
|
+
jpeg: "image/jpeg",
|
|
9
|
+
jpg: "image/jpeg",
|
|
10
|
+
mov: "video/quicktime",
|
|
11
|
+
mp3: "audio/mpeg",
|
|
12
|
+
mp4: "video/mp4",
|
|
13
|
+
png: "image/png",
|
|
14
|
+
wav: "audio/wav",
|
|
15
|
+
webm: "video/webm",
|
|
16
|
+
webp: "image/webp"
|
|
17
|
+
};
|
|
18
|
+
function inferRemoteMediaType(url) {
|
|
19
|
+
const segment = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
|
|
20
|
+
let filename = segment;
|
|
21
|
+
try {
|
|
22
|
+
filename = decodeURIComponent(segment);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// The extension remains usable when the path contains malformed escapes.
|
|
26
|
+
}
|
|
27
|
+
const extension = filename.split(".").at(-1)?.toLowerCase();
|
|
28
|
+
return extension === undefined || extension === filename.toLowerCase()
|
|
29
|
+
? "application/octet-stream"
|
|
30
|
+
: mediaTypes[extension] ?? "application/octet-stream";
|
|
31
|
+
}
|
|
32
|
+
function remoteMedia(value, name) {
|
|
33
|
+
try {
|
|
34
|
+
const url = parseSafeRemoteAssetUrl(value);
|
|
35
|
+
return {
|
|
36
|
+
type: "media",
|
|
37
|
+
url: value,
|
|
38
|
+
mime: inferRemoteMediaType(url),
|
|
39
|
+
name
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof UrlSafetyError)
|
|
44
|
+
throw new ToolInputError();
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function createToolUseCases(dependencies) {
|
|
49
|
+
async function load(access, signal) {
|
|
50
|
+
signal.throwIfAborted();
|
|
51
|
+
const catalog = await dependencies.telemetry.stage("catalog", () => (dependencies.catalog.load({ access, signal })));
|
|
52
|
+
signal.throwIfAborted();
|
|
53
|
+
for (const warning of catalog.warnings)
|
|
54
|
+
dependencies.warn(warning);
|
|
55
|
+
return catalog;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
async listTools({ access, signal }) {
|
|
59
|
+
return projectToolList(await load(access, signal));
|
|
60
|
+
},
|
|
61
|
+
async listModels({ input, access, signal }) {
|
|
62
|
+
return projectModelList(await load(access, signal), input.tool);
|
|
63
|
+
},
|
|
64
|
+
async getModel({ input, access, signal }) {
|
|
65
|
+
return projectModelDetail(await load(access, signal), input.model);
|
|
66
|
+
},
|
|
67
|
+
async getArgumentsSchema({ name, access, signal }) {
|
|
68
|
+
return buildToolArgumentsSchema(await load(access, signal), name);
|
|
69
|
+
},
|
|
70
|
+
async call({ input, access, signal }) {
|
|
71
|
+
const catalog = await load(access, signal);
|
|
72
|
+
const model = await dependencies.telemetry.stage("input", () => {
|
|
73
|
+
const modelName = input.arguments.model;
|
|
74
|
+
if (typeof modelName !== "string")
|
|
75
|
+
throw new ToolInputError();
|
|
76
|
+
const selected = findToolModel(catalog, input.name, modelName);
|
|
77
|
+
assertModelArguments(selected, input.arguments);
|
|
78
|
+
return selected;
|
|
79
|
+
});
|
|
80
|
+
const { model: ignoredModel, ...parameters } = input.arguments;
|
|
81
|
+
void ignoredModel;
|
|
82
|
+
dependencies.telemetry.annotate({
|
|
83
|
+
tool_name: model.tool,
|
|
84
|
+
model_id: model.model,
|
|
85
|
+
parameters
|
|
86
|
+
});
|
|
87
|
+
const prompt = [];
|
|
88
|
+
const requestParameters = {};
|
|
89
|
+
const directArguments = {};
|
|
90
|
+
const prepare = async () => {
|
|
91
|
+
for (const parameter of model.parameters) {
|
|
92
|
+
signal.throwIfAborted();
|
|
93
|
+
const value = input.arguments[parameter.name];
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
if (parameter.type === "string") {
|
|
97
|
+
if (parameter.name === "prompt") {
|
|
98
|
+
prompt.push({ type: "text", content: value });
|
|
99
|
+
directArguments[parameter.name] = value;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
requestParameters[parameter.name] = value;
|
|
103
|
+
directArguments[parameter.name] = value;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (parameter.type === "number") {
|
|
108
|
+
requestParameters[parameter.name] = value;
|
|
109
|
+
directArguments[parameter.name] = value;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const values = parameter.type === "uri[]" ? value : [value];
|
|
113
|
+
for (const mediaUrl of values) {
|
|
114
|
+
signal.throwIfAborted();
|
|
115
|
+
if (mediaUrl.startsWith("file:")) {
|
|
116
|
+
const uploaded = await dependencies.uploader.upload({ url: mediaUrl }, access, signal);
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
prompt.push({
|
|
119
|
+
type: "media",
|
|
120
|
+
url: uploaded.url,
|
|
121
|
+
mime: uploaded.mediaType,
|
|
122
|
+
name: parameter.name
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
prompt.push(remoteMedia(mediaUrl, parameter.name));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const hasLocalMedia = model.parameters.some((parameter) => {
|
|
132
|
+
const value = input.arguments[parameter.name];
|
|
133
|
+
return parameter.type === "uri"
|
|
134
|
+
? typeof value === "string" && value.startsWith("file:")
|
|
135
|
+
: parameter.type === "uri[]"
|
|
136
|
+
&& Array.isArray(value) && value.some((url) => url.startsWith("file:"));
|
|
137
|
+
});
|
|
138
|
+
if (hasLocalMedia) {
|
|
139
|
+
await dependencies.telemetry.stage("upload", prepare);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
await prepare();
|
|
143
|
+
}
|
|
144
|
+
signal.throwIfAborted();
|
|
145
|
+
const requiresIntent = prompt.some((item) => item.type === "media")
|
|
146
|
+
|| Object.values(requestParameters).some((value) => value === "");
|
|
147
|
+
const invocation = requiresIntent
|
|
148
|
+
? {
|
|
149
|
+
kind: "intent",
|
|
150
|
+
model: model.model,
|
|
151
|
+
prompt,
|
|
152
|
+
parameters: requestParameters,
|
|
153
|
+
optionalMediaParameters: model.parameters
|
|
154
|
+
.filter((parameter) => ((parameter.type === "uri" || parameter.type === "uri[]")
|
|
155
|
+
&& !parameter.required))
|
|
156
|
+
.map((parameter) => parameter.name)
|
|
157
|
+
}
|
|
158
|
+
: {
|
|
159
|
+
kind: "direct",
|
|
160
|
+
name: model.model,
|
|
161
|
+
arguments: directArguments
|
|
162
|
+
};
|
|
163
|
+
const content = await dependencies.execution.execute({
|
|
164
|
+
invocation,
|
|
165
|
+
access,
|
|
166
|
+
signal
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
content,
|
|
170
|
+
usage: {
|
|
171
|
+
model: model.model,
|
|
172
|
+
...(model.cost === undefined ? {} : { cost: model.cost })
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { valid } from "semver";
|
|
2
|
+
const REGISTRY_DIST_TAGS_URL = "https://registry.npmjs.org/@gaoding%2fcli";
|
|
3
|
+
export async function fetchLatestVersion(options) {
|
|
4
|
+
const headers = new Headers({
|
|
5
|
+
Accept: "application/vnd.npm.install-v1+json"
|
|
6
|
+
});
|
|
7
|
+
if (options.etag)
|
|
8
|
+
headers.set("If-None-Match", options.etag);
|
|
9
|
+
const response = await options.fetch(REGISTRY_DIST_TAGS_URL, {
|
|
10
|
+
headers,
|
|
11
|
+
redirect: "error",
|
|
12
|
+
signal: options.signal
|
|
13
|
+
});
|
|
14
|
+
if (response.status === 304)
|
|
15
|
+
return { status: "not_modified" };
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
throw new Error(`Registry status ${response.status}`);
|
|
18
|
+
const metadata = await response.json();
|
|
19
|
+
if (!isRecord(metadata))
|
|
20
|
+
throw new Error("Registry metadata invalid");
|
|
21
|
+
const tags = metadata["dist-tags"];
|
|
22
|
+
if (!isRecord(tags))
|
|
23
|
+
throw new Error("Registry dist-tags missing");
|
|
24
|
+
const latest = tags.latest;
|
|
25
|
+
if (typeof latest !== "string" || valid(latest.trim()) === null) {
|
|
26
|
+
throw new Error("Registry latest missing");
|
|
27
|
+
}
|
|
28
|
+
const etag = response.headers.get("ETag") ?? undefined;
|
|
29
|
+
return {
|
|
30
|
+
status: "modified",
|
|
31
|
+
version: latest.trim(),
|
|
32
|
+
...(etag ? { etag } : {})
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fileSystem from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { gt, valid } from "semver";
|
|
6
|
+
import { createLocalJsonFile } from "../../platform/local-json-file.js";
|
|
7
|
+
import { fetchLatestVersion } from "./latest-version.js";
|
|
8
|
+
const SUCCESS_CHECK_INTERVAL_MS = 60 * 60 * 1_000;
|
|
9
|
+
const FAILED_CHECK_INTERVAL_MS = 15 * 60 * 1_000;
|
|
10
|
+
const REGISTRY_TIMEOUT_MS = 2_000;
|
|
11
|
+
const STALE_LOCK_MS = 30_000;
|
|
12
|
+
export function createUpdateNotificationService(options) {
|
|
13
|
+
const fetchPackage = options.fetch ?? globalThis.fetch;
|
|
14
|
+
const now = options.now ?? (() => new Date());
|
|
15
|
+
const env = options.env ?? process.env;
|
|
16
|
+
const stateDirectory = join(options.homeDirectory ?? homedir(), ".gd");
|
|
17
|
+
const stateFile = options.stateFile ?? createLocalJsonFile({
|
|
18
|
+
directory: stateDirectory,
|
|
19
|
+
filename: "update-check.json"
|
|
20
|
+
});
|
|
21
|
+
const lock = options.lock ?? createLocalUpdateNotificationLock({
|
|
22
|
+
directory: stateDirectory,
|
|
23
|
+
now: () => new Date()
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
async check(signal) {
|
|
27
|
+
if (env.CI === "true")
|
|
28
|
+
return;
|
|
29
|
+
let release = null;
|
|
30
|
+
try {
|
|
31
|
+
signal.throwIfAborted();
|
|
32
|
+
release = await lock.acquire();
|
|
33
|
+
if (release === null)
|
|
34
|
+
return;
|
|
35
|
+
const checkedAt = now();
|
|
36
|
+
const today = localDate(checkedAt);
|
|
37
|
+
const state = await readState(stateFile);
|
|
38
|
+
if (isNewer(state.latest_version, options.currentVersion)
|
|
39
|
+
&& state.last_notified_on !== today) {
|
|
40
|
+
const reminded = withReminder(state, today, state.latest_version);
|
|
41
|
+
await writeState(stateFile, reminded);
|
|
42
|
+
notify(options, state.latest_version);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!isDue(state.next_check_at, checkedAt))
|
|
46
|
+
return;
|
|
47
|
+
let latestResult;
|
|
48
|
+
const timeout = signalWithTimeout(signal, REGISTRY_TIMEOUT_MS);
|
|
49
|
+
try {
|
|
50
|
+
latestResult = await fetchLatestVersion({
|
|
51
|
+
fetch: fetchPackage,
|
|
52
|
+
signal: timeout.signal,
|
|
53
|
+
...(state.etag ? { etag: state.etag } : {})
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
if (signal.aborted)
|
|
58
|
+
return;
|
|
59
|
+
await writeState(stateFile, {
|
|
60
|
+
...state,
|
|
61
|
+
schema_version: 1,
|
|
62
|
+
next_check_at: addMilliseconds(checkedAt, FAILED_CHECK_INTERVAL_MS)
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
timeout.dispose();
|
|
68
|
+
}
|
|
69
|
+
const latest = latestResult.status === "not_modified"
|
|
70
|
+
? state.latest_version
|
|
71
|
+
: latestResult.version;
|
|
72
|
+
if (latest === undefined || valid(latest) === null) {
|
|
73
|
+
await writeState(stateFile, {
|
|
74
|
+
...state,
|
|
75
|
+
schema_version: 1,
|
|
76
|
+
next_check_at: addMilliseconds(checkedAt, FAILED_CHECK_INTERVAL_MS)
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let nextState = {
|
|
81
|
+
...state,
|
|
82
|
+
schema_version: 1,
|
|
83
|
+
last_success_check_at: checkedAt.toISOString(),
|
|
84
|
+
next_check_at: addMilliseconds(checkedAt, SUCCESS_CHECK_INTERVAL_MS),
|
|
85
|
+
latest_version: latest,
|
|
86
|
+
...(latestResult.status === "modified" && latestResult.etag
|
|
87
|
+
? { etag: latestResult.etag }
|
|
88
|
+
: {})
|
|
89
|
+
};
|
|
90
|
+
if (latestResult.status === "modified" && latestResult.etag === undefined) {
|
|
91
|
+
delete nextState.etag;
|
|
92
|
+
}
|
|
93
|
+
const shouldNotify = gt(latest, options.currentVersion)
|
|
94
|
+
&& nextState.last_notified_on !== today;
|
|
95
|
+
if (shouldNotify)
|
|
96
|
+
nextState = withReminder(nextState, today, latest);
|
|
97
|
+
await writeState(stateFile, nextState);
|
|
98
|
+
if (shouldNotify)
|
|
99
|
+
notify(options, latest);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Availability checks are best-effort and never change command behavior.
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await release?.().catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function signalWithTimeout(parent, milliseconds) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const forwardAbort = () => { controller.abort(parent.reason); };
|
|
113
|
+
if (parent.aborted)
|
|
114
|
+
forwardAbort();
|
|
115
|
+
else
|
|
116
|
+
parent.addEventListener("abort", forwardAbort, { once: true });
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
controller.abort(new Error("Registry request timed out."));
|
|
119
|
+
}, milliseconds);
|
|
120
|
+
timer.unref();
|
|
121
|
+
return {
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
dispose() {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
parent.removeEventListener("abort", forwardAbort);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function createLocalUpdateNotificationLock(options) {
|
|
130
|
+
const path = join(options.directory, "update-check.lock");
|
|
131
|
+
return {
|
|
132
|
+
async acquire() {
|
|
133
|
+
await fileSystem.mkdir(options.directory, { mode: 0o700, recursive: true });
|
|
134
|
+
await fileSystem.chmod(options.directory, 0o700);
|
|
135
|
+
const first = await tryCreateLock(path);
|
|
136
|
+
if (first)
|
|
137
|
+
return releaseLock(path, first);
|
|
138
|
+
if (!await isStaleLock(path, options.now()))
|
|
139
|
+
return null;
|
|
140
|
+
await fileSystem.unlink(path).catch((error) => {
|
|
141
|
+
if (!isMissing(error))
|
|
142
|
+
throw error;
|
|
143
|
+
});
|
|
144
|
+
const recovered = await tryCreateLock(path);
|
|
145
|
+
return recovered ? releaseLock(path, recovered) : null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function tryCreateLock(path) {
|
|
150
|
+
const token = randomUUID();
|
|
151
|
+
let handle;
|
|
152
|
+
try {
|
|
153
|
+
handle = await fileSystem.open(path, "wx", 0o600);
|
|
154
|
+
await handle.writeFile(token, "utf8");
|
|
155
|
+
await handle.sync();
|
|
156
|
+
await handle.close();
|
|
157
|
+
return token;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await handle?.close().catch(() => undefined);
|
|
161
|
+
if (isExists(error))
|
|
162
|
+
return null;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function releaseLock(path, token) {
|
|
167
|
+
return async () => {
|
|
168
|
+
let current;
|
|
169
|
+
try {
|
|
170
|
+
current = await fileSystem.readFile(path, "utf8");
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (isMissing(error))
|
|
174
|
+
return;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
if (current === token)
|
|
178
|
+
await fileSystem.unlink(path);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function isStaleLock(path, now) {
|
|
182
|
+
try {
|
|
183
|
+
const stats = await fileSystem.stat(path);
|
|
184
|
+
return stats.mtimeMs + STALE_LOCK_MS <= now.getTime();
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (isMissing(error))
|
|
188
|
+
return true;
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readState(file) {
|
|
193
|
+
const text = await file.readText();
|
|
194
|
+
if (text === null)
|
|
195
|
+
return { schema_version: 1 };
|
|
196
|
+
try {
|
|
197
|
+
const value = JSON.parse(text);
|
|
198
|
+
return parseState(value) ?? { schema_version: 1 };
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return { schema_version: 1 };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function parseState(value) {
|
|
205
|
+
if (!isRecord(value) || value.schema_version !== 1)
|
|
206
|
+
return null;
|
|
207
|
+
for (const key of [
|
|
208
|
+
"last_success_check_at",
|
|
209
|
+
"next_check_at",
|
|
210
|
+
"latest_version",
|
|
211
|
+
"last_notified_on",
|
|
212
|
+
"last_notified_version",
|
|
213
|
+
"etag"
|
|
214
|
+
]) {
|
|
215
|
+
if (value[key] !== undefined && typeof value[key] !== "string")
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
async function writeState(file, state) {
|
|
221
|
+
await file.writeTextAtomically(`${JSON.stringify(state, null, 2)}\n`);
|
|
222
|
+
}
|
|
223
|
+
function withReminder(state, today, version) {
|
|
224
|
+
return {
|
|
225
|
+
...state,
|
|
226
|
+
schema_version: 1,
|
|
227
|
+
last_notified_on: today,
|
|
228
|
+
last_notified_version: version
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function notify(options, latest) {
|
|
232
|
+
options.notify(`gd-cli 有新版本 ${latest},当前版本为 ${options.currentVersion}。` +
|
|
233
|
+
"运行 gd-cli update 即可更新。");
|
|
234
|
+
}
|
|
235
|
+
function isNewer(version, currentVersion) {
|
|
236
|
+
return version !== undefined && valid(version) !== null && gt(version, currentVersion);
|
|
237
|
+
}
|
|
238
|
+
function isDue(nextCheckAt, now) {
|
|
239
|
+
if (nextCheckAt === undefined)
|
|
240
|
+
return true;
|
|
241
|
+
const timestamp = Date.parse(nextCheckAt);
|
|
242
|
+
return !Number.isFinite(timestamp) || timestamp <= now.getTime();
|
|
243
|
+
}
|
|
244
|
+
function addMilliseconds(value, milliseconds) {
|
|
245
|
+
return new Date(value.getTime() + milliseconds).toISOString();
|
|
246
|
+
}
|
|
247
|
+
function localDate(value) {
|
|
248
|
+
return [
|
|
249
|
+
String(value.getFullYear()).padStart(4, "0"),
|
|
250
|
+
String(value.getMonth() + 1).padStart(2, "0"),
|
|
251
|
+
String(value.getDate()).padStart(2, "0")
|
|
252
|
+
].join("-");
|
|
253
|
+
}
|
|
254
|
+
function isRecord(value) {
|
|
255
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
256
|
+
}
|
|
257
|
+
function isMissing(error) {
|
|
258
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
259
|
+
}
|
|
260
|
+
function isExists(error) {
|
|
261
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
262
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { gt } from "semver";
|
|
5
|
+
import { syncBundledSkills, verifyBundledSkills } from "../skill/installer.js";
|
|
6
|
+
import { fetchLatestVersion } from "./latest-version.js";
|
|
7
|
+
export class UpdateError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
nextSteps;
|
|
10
|
+
constructor(code, message, nextSteps) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.nextSteps = nextSteps;
|
|
14
|
+
this.name = "UpdateError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function detectInstallSource(input) {
|
|
18
|
+
const binaryPath = normalizePath(input.binaryPath);
|
|
19
|
+
const realBinaryPath = normalizePath(input.realBinaryPath);
|
|
20
|
+
const packageRoot = normalizePath(input.packageRoot);
|
|
21
|
+
const cwd = normalizePath(resolve(input.cwd));
|
|
22
|
+
const userAgent = input.env.npm_config_user_agent ?? "";
|
|
23
|
+
const npmExecPath = normalizePath(input.env.npm_execpath ?? "");
|
|
24
|
+
if (userAgent.startsWith("yarn/") || npmExecPath.includes("/yarn/")) {
|
|
25
|
+
return unsupported("Yarn 安装不支持自动更新");
|
|
26
|
+
}
|
|
27
|
+
if (containsTemporaryRunner(binaryPath) || containsTemporaryRunner(packageRoot)) {
|
|
28
|
+
return unsupported("临时运行方式不支持自动更新");
|
|
29
|
+
}
|
|
30
|
+
if (binaryPath.includes("/node_modules/.bin/")
|
|
31
|
+
|| packageRoot.startsWith(`${cwd}/node_modules/`)) {
|
|
32
|
+
return unsupported("项目依赖不支持自动更新");
|
|
33
|
+
}
|
|
34
|
+
if (!isWithin(realBinaryPath, packageRoot)) {
|
|
35
|
+
return unsupported("CLI binary 与 package 不匹配");
|
|
36
|
+
}
|
|
37
|
+
if (/\/global\/[^/]+\/\.pnpm\/@gaoding\+cli@[^/]+\/node_modules\/@gaoding\/cli$/u
|
|
38
|
+
.test(packageRoot)
|
|
39
|
+
|| /\/v\d+\/[0-9a-f]+-[0-9a-f]+-[0-9a-f]{16}\/node_modules\/@gaoding\/cli$/u
|
|
40
|
+
.test(packageRoot)
|
|
41
|
+
|| /\/v\d+\/[0-9a-f]+-[0-9a-f]+-[0-9a-f]{16}\/node_modules\/@gaoding\/cli\/dist\/bin\/gd-cli\.js$/u
|
|
42
|
+
.test(binaryPath)) {
|
|
43
|
+
return { kind: "global", manager: "pnpm", packageRoot: input.packageRoot };
|
|
44
|
+
}
|
|
45
|
+
if (packageRoot.endsWith("/lib/node_modules/@gaoding/cli")
|
|
46
|
+
|| packageRoot.endsWith("/npm/node_modules/@gaoding/cli")) {
|
|
47
|
+
return { kind: "global", manager: "npm", packageRoot: input.packageRoot };
|
|
48
|
+
}
|
|
49
|
+
return unsupported("无法确认 npm 或 pnpm 全局安装");
|
|
50
|
+
}
|
|
51
|
+
export function createUpdateService(options) {
|
|
52
|
+
const fetchPackage = options.fetch ?? globalThis.fetch;
|
|
53
|
+
const runProcess = options.runProcess ?? spawnProcess;
|
|
54
|
+
const syncSkills = options.syncSkills ?? syncBundledSkills;
|
|
55
|
+
const verifySkills = options.verifySkills ?? verifyBundledSkills;
|
|
56
|
+
const env = options.env ?? process.env;
|
|
57
|
+
return {
|
|
58
|
+
async run(signal) {
|
|
59
|
+
signal.throwIfAborted();
|
|
60
|
+
const latest = await options.telemetry.stage("registry", async () => {
|
|
61
|
+
try {
|
|
62
|
+
const result = await fetchLatestVersion({ fetch: fetchPackage, signal });
|
|
63
|
+
if (result.status !== "modified")
|
|
64
|
+
throw new Error("Registry response not modified");
|
|
65
|
+
return result.version;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
rethrowAbort(signal);
|
|
69
|
+
throw new UpdateError("UPDATE_FAILED", "无法获取 @gaoding/cli 最新版本。", ["请稍后重新执行 gd-cli update。"]);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
signal.throwIfAborted();
|
|
73
|
+
const skillOptions = {
|
|
74
|
+
env,
|
|
75
|
+
...(options.homeDirectory ? { homeDirectory: options.homeDirectory } : {})
|
|
76
|
+
};
|
|
77
|
+
if (!gt(latest, options.currentVersion)) {
|
|
78
|
+
await options.telemetry.stage("sync", async () => {
|
|
79
|
+
try {
|
|
80
|
+
await syncSkills(options.bundledSkills, skillOptions);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
rethrowAbort(signal);
|
|
84
|
+
throw new UpdateError("UPDATE_FAILED", `gd-cli ${options.currentVersion} 无需更新,但 Agent Skill 同步失败。`, ["请解决冲突后重新执行 gd-cli update。"]);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return { version: options.currentVersion, updated: false };
|
|
88
|
+
}
|
|
89
|
+
const binaryPath = options.binaryPath ?? process.argv[1] ?? "";
|
|
90
|
+
const source = detectInstallSource({
|
|
91
|
+
binaryPath,
|
|
92
|
+
realBinaryPath: safeRealpath(binaryPath),
|
|
93
|
+
packageRoot: options.packageRoot,
|
|
94
|
+
cwd: options.cwd ?? process.cwd(),
|
|
95
|
+
env
|
|
96
|
+
});
|
|
97
|
+
const nextSteps = manualCommands(latest);
|
|
98
|
+
if (source.kind === "unsupported") {
|
|
99
|
+
throw new UpdateError("UPDATE_FAILED", `当前安装方式不支持自动更新:${source.reason}。`, nextSteps);
|
|
100
|
+
}
|
|
101
|
+
const command = source.manager;
|
|
102
|
+
const args = source.manager === "npm"
|
|
103
|
+
? ["install", "--global", "--allow-scripts=@gaoding/cli", `@gaoding/cli@${latest}`]
|
|
104
|
+
: ["add", "--global", "--allow-build=@gaoding/cli", `@gaoding/cli@${latest}`];
|
|
105
|
+
await options.telemetry.stage("install", async () => {
|
|
106
|
+
try {
|
|
107
|
+
await runProcess(command, args, signal);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
rethrowAbort(signal);
|
|
111
|
+
throw new UpdateError("UPDATE_FAILED", `gd-cli 更新到 ${latest} 失败。`, nextSteps);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
signal.throwIfAborted();
|
|
115
|
+
const targetSkills = options.bundledSkills.map((skill) => ({
|
|
116
|
+
...skill,
|
|
117
|
+
version: latest
|
|
118
|
+
}));
|
|
119
|
+
await options.telemetry.stage("verify", async () => {
|
|
120
|
+
try {
|
|
121
|
+
await verifySkills(targetSkills, skillOptions);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
rethrowAbort(signal);
|
|
125
|
+
throw new UpdateError("UPDATE_PARTIAL", `gd-cli 已更新到 ${latest},但 Agent Skill 验收失败。`, ["请重新执行 gd-cli update。"]);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return { version: latest, updated: true };
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function spawnProcess(command, args, signal) {
|
|
133
|
+
return new Promise((resolvePromise, reject) => {
|
|
134
|
+
const child = spawn(command, args, {
|
|
135
|
+
stdio: "inherit",
|
|
136
|
+
shell: false,
|
|
137
|
+
signal
|
|
138
|
+
});
|
|
139
|
+
child.once("error", reject);
|
|
140
|
+
child.once("exit", (code, exitSignal) => {
|
|
141
|
+
if (code === 0) {
|
|
142
|
+
resolvePromise();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
reject(new Error(code === null
|
|
146
|
+
? `package manager terminated by ${exitSignal ?? "unknown signal"}`
|
|
147
|
+
: `package manager exited with ${code}`));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function manualCommands(version) {
|
|
153
|
+
return [
|
|
154
|
+
`npm install --global --allow-scripts=@gaoding/cli @gaoding/cli@${version}`,
|
|
155
|
+
`pnpm add --global --allow-build=@gaoding/cli @gaoding/cli@${version}`
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
function containsTemporaryRunner(path) {
|
|
159
|
+
return path.includes("/.npm/_npx/")
|
|
160
|
+
|| path.includes("/pnpm/dlx/")
|
|
161
|
+
|| path.includes("/.pnpm/dlx/");
|
|
162
|
+
}
|
|
163
|
+
function isWithin(path, directory) {
|
|
164
|
+
return path === directory || path.startsWith(`${directory}/`);
|
|
165
|
+
}
|
|
166
|
+
function normalizePath(path) {
|
|
167
|
+
return path.replace(/\\/gu, "/").replace(/\/+$/u, "");
|
|
168
|
+
}
|
|
169
|
+
function safeRealpath(path) {
|
|
170
|
+
try {
|
|
171
|
+
return realpathSync(path);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return path;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function unsupported(reason) {
|
|
178
|
+
return { kind: "unsupported", reason };
|
|
179
|
+
}
|
|
180
|
+
function rethrowAbort(signal) {
|
|
181
|
+
if (signal.aborted)
|
|
182
|
+
signal.throwIfAborted();
|
|
183
|
+
}
|