@limecloud/cli 0.11.0 → 0.13.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/README.md +27 -2
- package/dist/identity.json +69 -11
- package/dist/knowledge.json +1 -1
- package/dist/management.json +5648 -4192
- package/dist/skills.json +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +19 -9
- package/src/compatibility.mjs +16 -8
- package/src/distribution.mjs +72 -0
- package/src/group-task.mjs +14 -9
- package/src/install.mjs +26 -5
- package/src/locales/en.json +32 -6
- package/src/locales/zh-CN.json +32 -6
- package/src/product-skill-access.mjs +68 -0
- package/src/product-skill-contracts.mjs +57 -0
- package/src/product-skill-files.mjs +105 -0
- package/src/product-skills.mjs +35 -15
- package/src/release-version.mjs +13 -0
- package/src/update.mjs +57 -0
package/src/product-skills.mjs
CHANGED
|
@@ -5,13 +5,15 @@ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
|
5
5
|
import { currentLocalizer, defaultLocale, localeRegistry, message, translate } from "./i18n.mjs";
|
|
6
6
|
import { CLIError } from "./protocol.mjs";
|
|
7
7
|
import { actionContract } from "./action-contracts.mjs";
|
|
8
|
-
import { commandOperations
|
|
9
|
-
import {
|
|
8
|
+
import { commandOperations } from "./commands.mjs";
|
|
9
|
+
import { compareActionContracts } from "./compatibility.mjs";
|
|
10
|
+
import { skillContractDeclaration, skillContractPath, validSkillContractDeclaration } from "./product-skill-contracts.mjs";
|
|
11
|
+
import { reviewSkillUpdate, skillActionAccess, skillEffect, validSkillAccess } from "./product-skill-access.mjs";
|
|
10
12
|
|
|
11
13
|
export const skillWrappers = ["codex", "skill"];
|
|
12
14
|
const httpMethods = new Set(["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"]);
|
|
13
15
|
// 仅概括正式HTTP动作,供安装前审阅;不表示当前权或已获得人的确认。
|
|
14
|
-
export
|
|
16
|
+
export { skillEffect };
|
|
15
17
|
const sha = value => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
16
18
|
const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === "object"
|
|
17
19
|
? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value;
|
|
@@ -23,10 +25,11 @@ const invalid = () => { throw new CLIError("skill_artifact_invalid", message("sk
|
|
|
23
25
|
|
|
24
26
|
// 只验证随已安装CLI提供的公开候选。摘要检查完整性,不构造独立发布证明或线上撤回结论。
|
|
25
27
|
export function validateSkillBundle(bundle, { registry = localeRegistry, version, contract } = {}) {
|
|
26
|
-
if (bundle?.schema_version !==
|
|
28
|
+
if (bundle?.schema_version !== 2 || bundle.package !== "@limecloud/cli" || !/^\d+\.\d+\.\d+$/.test(bundle.version) ||
|
|
27
29
|
version && bundle.version !== version || !Array.isArray(bundle.artifacts) || bundle.artifacts.length > 32) invalid();
|
|
28
30
|
const identities = new Set();
|
|
29
31
|
const actions = contract && new Map(commandOperations(contract).map(operation => [operation.operationId, actionContract(contract, operation)]));
|
|
32
|
+
const access = contract && new Map(commandOperations(contract).map(operation => [operation.operationId, skillActionAccess(contract, operation)]));
|
|
30
33
|
for (const artifact of bundle.artifacts) {
|
|
31
34
|
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) invalid();
|
|
32
35
|
const identity = `${artifact.id}:${artifact.wrapper}`;
|
|
@@ -40,19 +43,27 @@ export function validateSkillBundle(bundle, { registry = localeRegistry, version
|
|
|
40
43
|
// 同一CLI内混入另一份动作资料时先拒绝,不能拿当前Schema替候选悄悄解释旧步骤。
|
|
41
44
|
if (new Set(artifact.operations.map(action => action.operation_id)).size !== artifact.operations.length || actions && artifact.operations.some(action =>
|
|
42
45
|
!actions.has(action.operation_id) || JSON.stringify(canonical(action)) !== JSON.stringify(canonical(actions.get(action.operation_id))))) invalid();
|
|
46
|
+
if (!validSkillAccess(artifact.access_requirements, artifact.operations) || access && artifact.access_requirements.some(requirement =>
|
|
47
|
+
JSON.stringify(canonical(requirement)) !== JSON.stringify(canonical(access.get(requirement.operation_id))))) invalid();
|
|
43
48
|
for (const { code } of registry.locales) {
|
|
44
49
|
if (!["title", "description"].every(key => typeof artifact.localizations?.[code]?.[key] === "string" && artifact.localizations[code][key].trim())) invalid();
|
|
45
50
|
}
|
|
46
51
|
const prefix = artifact.wrapper === "codex" ? `skills/${artifact.id}/` : "";
|
|
52
|
+
if (artifact.action_contract_file !== skillContractPath(artifact.id, artifact.wrapper)) invalid();
|
|
47
53
|
const paths = new Set();
|
|
48
54
|
let bytes = 0;
|
|
49
55
|
for (const file of artifact.files) {
|
|
50
56
|
if (!file || !safePath(file.path) || paths.has(file.path) || typeof file.content !== "string" || !/\.(?:md|json)$/.test(file.path) ||
|
|
51
|
-
!(file.path === "plugin.json" && artifact.wrapper === "codex" || file.path === `${prefix}SKILL.md` || file.path.startsWith(`${prefix}references/`) && file.path.endsWith(".md")) ||
|
|
57
|
+
!(file.path === "plugin.json" && artifact.wrapper === "codex" || file.path === artifact.action_contract_file || file.path === `${prefix}SKILL.md` || file.path.startsWith(`${prefix}references/`) && file.path.endsWith(".md")) ||
|
|
52
58
|
Buffer.byteLength(file.content) !== file.bytes || file.sha256 !== sha(file.content) || file.bytes > 131072) invalid();
|
|
53
59
|
paths.add(file.path); bytes += file.bytes;
|
|
54
60
|
}
|
|
55
61
|
if (bytes > 524288 || !paths.has(`${prefix}SKILL.md`) || artifact.wrapper === "codex" && !paths.has("plugin.json") || artifact.digest !== skillArtifactDigest(artifact)) invalid();
|
|
62
|
+
let declaration;
|
|
63
|
+
try { declaration = JSON.parse(artifact.files.find(file => file.path === artifact.action_contract_file)?.content ?? ""); } catch { invalid(); }
|
|
64
|
+
if (!validSkillContractDeclaration(declaration, artifact) || JSON.stringify(canonical(declaration)) !== JSON.stringify(canonical(skillContractDeclaration({
|
|
65
|
+
...artifact, files: artifact.files.filter(file => file.path !== artifact.action_contract_file),
|
|
66
|
+
})))) invalid();
|
|
56
67
|
const entry = artifact.files.find(file => file.path === `${prefix}SKILL.md`).content;
|
|
57
68
|
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(entry)?.[1];
|
|
58
69
|
if (!frontmatter || /^name:\s*([a-z0-9-]+)\s*$/m.exec(frontmatter)?.[1] !== artifact.id || !/^description:\s*\S.+$/m.test(frontmatter)) invalid();
|
|
@@ -86,18 +97,27 @@ export function describeSkill(artifact, { includeFiles = false } = {}) {
|
|
|
86
97
|
installation: { performed_by_this_command: false, on_disk: "not-evaluated", current_session_loaded: "unknown" }, message: translate("skills.candidateNotice") };
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
export async function inspectSkill(artifact,
|
|
100
|
+
export async function inspectSkill(artifact, options = {}) {
|
|
90
101
|
const description = describeSkill(artifact, { includeFiles: true });
|
|
102
|
+
if (options.directoryInspection) {
|
|
103
|
+
description.directory_comparison = options.directoryInspection;
|
|
104
|
+
description.update_review = reviewSkillUpdate(artifact, options.directoryInspection.action_contracts);
|
|
105
|
+
description.installation.on_disk = "explicit-directory-inspected";
|
|
106
|
+
description.message = translate("skills.directoryNotice");
|
|
107
|
+
}
|
|
91
108
|
if (!options.online) return description;
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
remote: statuses.has("different") ? "different" : statuses.has("absent") ? "absent" : statuses.has("unknown") ? "unknown" : "matched",
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
const installed = description.directory_comparison?.action_contracts;
|
|
110
|
+
const installedActions = installed?.status === "declared" ? installed.operations : [];
|
|
111
|
+
const checked = await compareActionContracts([...artifact.operations, ...installedActions], options);
|
|
112
|
+
const summarize = checked => {
|
|
113
|
+
const actions = checked.map(({ compatibility, ...identity }) => ({ ...identity, ...compatibility }));
|
|
114
|
+
const statuses = new Set(actions.map(action => action.status));
|
|
115
|
+
return { remote: statuses.has("different") ? "different" : statuses.has("absent") ? "absent" : statuses.has("unknown") ? "unknown" : "matched",
|
|
116
|
+
complete: !statuses.has("unknown"), checked_at: new Date((options.now ?? Date.now)()).toISOString(), authorization: "not-evaluated", actions };
|
|
117
|
+
};
|
|
118
|
+
if (installedActions.length) installed.compatibility = summarize(checked.slice(artifact.operations.length));
|
|
119
|
+
return { ...description, compatibility: { ...description.compatibility, ...summarize(checked.slice(0, artifact.operations.length)) },
|
|
120
|
+
message: translate(installed ? "skills.directoryOnlineNotice" : "skills.onlineNotice") };
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
async function physicalDirectory(path) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// npm渠道和安装目标只接受本项目正式的三段版本,不接受标签、范围或预发行串。
|
|
2
|
+
export function releaseVersion(value) {
|
|
3
|
+
if (typeof value !== "string" || value.length > 64 || !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/.test(value)) return null;
|
|
4
|
+
const parts = value.split(".").map(Number);
|
|
5
|
+
return parts.every(Number.isSafeInteger) ? parts : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function compareReleaseVersions(left, right) {
|
|
9
|
+
const a = releaseVersion(left), b = releaseVersion(right);
|
|
10
|
+
if (!a || !b) throw new Error("Invalid release version");
|
|
11
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { message, translate } from "./i18n.mjs";
|
|
2
|
+
import { success } from "./output.mjs";
|
|
3
|
+
import { CLIError } from "./protocol.mjs";
|
|
4
|
+
import { fetchPublicJSON } from "./public-json.mjs";
|
|
5
|
+
import { compareReleaseVersions, releaseVersion } from "./release-version.mjs";
|
|
6
|
+
import { cliRegistry } from "./distribution.mjs";
|
|
7
|
+
|
|
8
|
+
export const cliUpdateURL = `${cliRegistry}/@limecloud%2fcli/latest`;
|
|
9
|
+
const reasonMessages = { registry_unavailable: "registryUnavailable", invalid_metadata: "invalidMetadata", latest_deprecated: "latestDeprecated" };
|
|
10
|
+
|
|
11
|
+
// latest是渠道推荐的观察,不是业务合同、发布者证明或本地安装来源的证明。
|
|
12
|
+
export async function checkCLIUpdate(version, { fetchMetadata = fetchPublicJSON, signal, now = Date.now } = {}) {
|
|
13
|
+
if (!releaseVersion(version)) throw new CLIError("invalid_arguments", message("install.invalidVersion"));
|
|
14
|
+
signal?.throwIfAborted();
|
|
15
|
+
const result = { package: "@limecloud/cli", current_version: version, registry: cliRegistry, channel: "latest", metadata_url: cliUpdateURL,
|
|
16
|
+
status: "unknown", reason: null, latest_version: null, observed_at: null, candidate: null,
|
|
17
|
+
installation_performed: false, installation_owner: "unknown", methods_updated: 0, current_session_loaded: "unknown",
|
|
18
|
+
provenance: "not-evaluated", online_withdrawal: "not-evaluated", compatibility: "not-evaluated", authorization: "not-evaluated" };
|
|
19
|
+
let metadata;
|
|
20
|
+
try { metadata = await fetchMetadata(cliUpdateURL, { signal, maxBytes: 256 * 1024 }); }
|
|
21
|
+
catch {
|
|
22
|
+
signal?.throwIfAborted();
|
|
23
|
+
result.reason = "registry_unavailable";
|
|
24
|
+
}
|
|
25
|
+
signal?.throwIfAborted();
|
|
26
|
+
result.observed_at = new Date(now()).toISOString();
|
|
27
|
+
if (!result.reason) {
|
|
28
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata) || metadata.name !== result.package || !releaseVersion(metadata.version) ||
|
|
29
|
+
metadata.deprecated !== undefined && typeof metadata.deprecated !== "string") result.reason = "invalid_metadata";
|
|
30
|
+
else {
|
|
31
|
+
result.latest_version = metadata.version;
|
|
32
|
+
if (metadata.deprecated) { result.status = "unavailable"; result.reason = "latest_deprecated"; }
|
|
33
|
+
else {
|
|
34
|
+
const comparison = compareReleaseVersions(metadata.version, version);
|
|
35
|
+
result.status = comparison > 0 ? "available" : comparison < 0 ? "ahead" : "current";
|
|
36
|
+
if (comparison > 0) result.candidate = { version: metadata.version, package_spec: `@limecloud/cli@${metadata.version}`,
|
|
37
|
+
// 固定argv只预览现有安装器;不执行元数据提供的命令/URL,也不推定当前安装前缀。
|
|
38
|
+
review_argv: ["npx", `--registry=${cliRegistry}`, `--@limecloud:registry=${cliRegistry}`, "--ignore-scripts",
|
|
39
|
+
`@limecloud/cli@${metadata.version}`, "install", "--dry-run", "--no-config"] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
result.message = translate(`update.${reasonMessages[result.reason] ?? result.status}`);
|
|
44
|
+
result.next_steps = [translate("update.owner"), translate("update.methods"), translate("update.recovery")];
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function writeUpdateCheck(io, result, format) {
|
|
49
|
+
if (format !== "table") { success(io, result, { identity: "local", format }); return; }
|
|
50
|
+
const rows = [
|
|
51
|
+
["currentVersion", result.current_version], ["latestVersion", result.latest_version], ["source", result.metadata_url],
|
|
52
|
+
["observedAt", result.observed_at], ["result", `${result.status}${result.reason ? ` / ${result.reason}` : ""}: ${result.message}`],
|
|
53
|
+
...(result.candidate ? [["review", JSON.stringify(result.candidate.review_argv)]] : []),
|
|
54
|
+
...result.next_steps.map(value => ["nextStep", value]),
|
|
55
|
+
];
|
|
56
|
+
success(io, rows.map(([key, value]) => ({ field: translate(`update.${key}`), value: value === null ? "null" : value })), { identity: "local", format, tableTruncate: false });
|
|
57
|
+
}
|