@plasmickit/cli 0.0.81 → 0.0.83
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/package.json +1 -1
- package/skill/plasmic/SKILL.md +2 -0
- package/src/index.mjs +77 -22
- package/src/progress.mjs +122 -0
- package/src/protocol.mjs +3 -1
- package/src/references.mjs +280 -68
package/package.json
CHANGED
package/skill/plasmic/SKILL.md
CHANGED
|
@@ -19,6 +19,8 @@ Inspect uses accessible Desktop MCP reads and does not require edit permission o
|
|
|
19
19
|
|
|
20
20
|
The command checks the NAS release manifest every time, downloads changed resources and verifies their hashes before returning absolute paths. Read all returned `mustRead` files, starting with `referencePath`; resolve subsequent links relative to the reference file. Use the returned `resourceRoot` for verification scripts. For a task spanning multiple modes, resolve and read each mode before its work begins.
|
|
21
21
|
|
|
22
|
+
The CLI streams readable progress to stderr and reserves stdout for the final JSON result. Tell the user when resource preparation starts, then relay meaningful stage changes in their language: current and latest versions, already up to date, or download and integrity verification. Do not narrate cache saving, update locking, compatibility checks or workflow entrypoints. When the command runs longer than the tool's initial response window, use its background/session mode and retrieve incremental output so the user sees progress while it runs. Waiting messages report elapsed time, not an estimated completion percentage. If the tool buffers all output until exit, explain that limitation and report that preparation is still running; do not invent progress. A ready workflow means its files are available for you to read, not that you have already read or executed them. Read `mustRead` before starting dependent work.
|
|
23
|
+
|
|
22
24
|
The runner uses the latest published `@plasmickit/cli`, distinct from official `@plasmicapp/cli`. Later commands can use `npx -y @plasmickit/cli@<cliVersion>` from the result to keep the task on that CLI version, or the global `plasmickit` after checking `plasmickit version check`.
|
|
23
25
|
|
|
24
26
|
If the command fails, stop dependent work and report the error. Do not silently use cached references. If it requires a newer CLI before npm has that version, install the returned `cliUrl` with `npm install -g <cliUrl>` and rerun. Do not copy the resource bundle into this skill or treat an old transcript as the current component contract.
|
package/src/index.mjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseArgs } from "node:util";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import path from "node:path";
|
|
5
2
|
import { realpathSync } from "node:fs";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { formatProgress } from "./progress.mjs";
|
|
8
7
|
import { CLI_VERSION } from "./protocol.mjs";
|
|
8
|
+
import {
|
|
9
|
+
checkCliVersion,
|
|
10
|
+
checkReferences,
|
|
11
|
+
referencePath,
|
|
12
|
+
resolveContext,
|
|
13
|
+
updateReferences,
|
|
14
|
+
} from "./references.mjs";
|
|
15
|
+
import { installSkill } from "./skill.mjs";
|
|
9
16
|
|
|
10
17
|
const usage = `plasmickit commands:
|
|
11
18
|
skill install [--dry-run]
|
|
@@ -15,31 +22,79 @@ const usage = `plasmickit commands:
|
|
|
15
22
|
--version
|
|
16
23
|
`;
|
|
17
24
|
export async function main(argv) {
|
|
18
|
-
const { values, positionals } = parseArgs({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const { values, positionals } = parseArgs({
|
|
26
|
+
args: argv,
|
|
27
|
+
allowPositionals: true,
|
|
28
|
+
options: {
|
|
29
|
+
"dry-run": { type: "boolean" },
|
|
30
|
+
mode: { type: "string" },
|
|
31
|
+
feed: { type: "string" },
|
|
32
|
+
home: { type: "string" },
|
|
33
|
+
help: { type: "boolean" },
|
|
34
|
+
version: { type: "boolean" },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (values.version) {
|
|
38
|
+
process.stdout.write(CLI_VERSION + "\n");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!positionals.length || values.help) {
|
|
42
|
+
process.stdout.write(usage);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
24
45
|
const [command, action] = positionals;
|
|
25
|
-
if (positionals.length !== 2)
|
|
46
|
+
if (positionals.length !== 2) {
|
|
47
|
+
throw new Error("Expected a command and action; use --help");
|
|
48
|
+
}
|
|
49
|
+
values.onProgress = (event) =>
|
|
50
|
+
process.stderr.write(
|
|
51
|
+
formatProgress({
|
|
52
|
+
...event,
|
|
53
|
+
versionKind: command === "version" ? "cli" : "resources",
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
26
56
|
let output;
|
|
27
|
-
if (command === "skill" && action === "install")
|
|
28
|
-
|
|
29
|
-
else if (command === "
|
|
30
|
-
|
|
57
|
+
if (command === "skill" && action === "install") {
|
|
58
|
+
output = await installSkill({ dryRun: values["dry-run"] });
|
|
59
|
+
} else if (command === "version" && action === "check") {
|
|
60
|
+
output = await checkCliVersion(values);
|
|
61
|
+
} else if (command === "references" && action === "check") {
|
|
62
|
+
output = await checkReferences(values);
|
|
63
|
+
} else if (command === "references" && action === "update") {
|
|
31
64
|
const { manifest: _manifest, ...result } = await updateReferences(values);
|
|
32
65
|
output = result;
|
|
66
|
+
} else if (command === "references" && action === "path") {
|
|
67
|
+
output = await referencePath(values);
|
|
68
|
+
} else if (command === "context" && action === "resolve") {
|
|
69
|
+
output = await resolveContext(values.mode, values);
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(`Unknown command: ${command} ${action}`);
|
|
33
72
|
}
|
|
34
|
-
else if (command === "references" && action === "path") output = await referencePath(values);
|
|
35
|
-
else if (command === "context" && action === "resolve") output = await resolveContext(values.mode, values);
|
|
36
|
-
else throw new Error(`Unknown command: ${command} ${action}`);
|
|
37
73
|
process.stdout.write(JSON.stringify({ ok: true, ...output }, null, 2) + "\n");
|
|
38
|
-
if (output.ok === false)
|
|
74
|
+
if (output.ok === false) {
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
39
77
|
}
|
|
40
|
-
if (
|
|
78
|
+
if (
|
|
79
|
+
process.argv[1] &&
|
|
80
|
+
realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)
|
|
81
|
+
) {
|
|
41
82
|
main(process.argv.slice(2)).catch((error) => {
|
|
42
|
-
process.stderr.write(
|
|
83
|
+
process.stderr.write(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
ok: false,
|
|
86
|
+
error: error.message,
|
|
87
|
+
...(error.stage ? { stage: error.stage } : {}),
|
|
88
|
+
...(error.cliUrl
|
|
89
|
+
? {
|
|
90
|
+
cliUrl: error.cliUrl,
|
|
91
|
+
cliVersion: error.cliVersion,
|
|
92
|
+
latestCliVersion: error.latestCliVersion,
|
|
93
|
+
minCliVersion: error.minCliVersion,
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
}) + "\n",
|
|
97
|
+
);
|
|
43
98
|
process.exitCode = 1;
|
|
44
99
|
});
|
|
45
100
|
}
|
package/src/progress.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const labels = {
|
|
2
|
+
manifest: ["版本检查", "正在检查版本", "版本检查完成"],
|
|
3
|
+
download: ["资源下载", "正在下载资源", "下载完成"],
|
|
4
|
+
verify: ["资源校验", "正在校验资源", "校验通过"],
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export async function withProgress(options, stage, action, details = {}) {
|
|
8
|
+
const started = Date.now();
|
|
9
|
+
let state = details;
|
|
10
|
+
let lastUpdate = 0;
|
|
11
|
+
const emit = (status) =>
|
|
12
|
+
options.onProgress?.({
|
|
13
|
+
type: "resource-progress",
|
|
14
|
+
stage,
|
|
15
|
+
...state,
|
|
16
|
+
status,
|
|
17
|
+
elapsedMs: Date.now() - started,
|
|
18
|
+
});
|
|
19
|
+
emit("running");
|
|
20
|
+
const timer = options.onProgress
|
|
21
|
+
? setInterval(() => emit("running"), 2000)
|
|
22
|
+
: undefined;
|
|
23
|
+
timer?.unref();
|
|
24
|
+
try {
|
|
25
|
+
const result = await action((update) => {
|
|
26
|
+
state = { ...state, ...update };
|
|
27
|
+
if (
|
|
28
|
+
("bytes" in update || "completedFiles" in update) &&
|
|
29
|
+
Date.now() - lastUpdate >= 500
|
|
30
|
+
) {
|
|
31
|
+
lastUpdate = Date.now();
|
|
32
|
+
emit("running");
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
emit("completed");
|
|
36
|
+
return result;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
error.stage ??= stage;
|
|
39
|
+
state = {
|
|
40
|
+
...state,
|
|
41
|
+
error: error.message,
|
|
42
|
+
timedOut:
|
|
43
|
+
error.name === "TimeoutError" || error.cause?.name === "TimeoutError",
|
|
44
|
+
};
|
|
45
|
+
emit("failed");
|
|
46
|
+
throw error;
|
|
47
|
+
} finally {
|
|
48
|
+
clearInterval(timer);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function size(bytes) {
|
|
53
|
+
return bytes >= 1024 * 1024
|
|
54
|
+
? `${(bytes / 1024 / 1024).toFixed(1)} MiB`
|
|
55
|
+
: bytes >= 1024
|
|
56
|
+
? `${(bytes / 1024).toFixed(1)} KiB`
|
|
57
|
+
: `${bytes} B`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatProgress(event) {
|
|
61
|
+
if (event.status !== "failed") {
|
|
62
|
+
if (
|
|
63
|
+
event.stage === "cache" &&
|
|
64
|
+
event.status === "completed" &&
|
|
65
|
+
event.upToDate !== undefined
|
|
66
|
+
) {
|
|
67
|
+
return event.upToDate
|
|
68
|
+
? "✓ 当前资源已是最新版本\n"
|
|
69
|
+
: "… 正在准备加载资源\n";
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
event.stage === "activate" &&
|
|
73
|
+
event.status === "completed" &&
|
|
74
|
+
event.updated
|
|
75
|
+
) {
|
|
76
|
+
return "✓ 资源加载完成\n";
|
|
77
|
+
}
|
|
78
|
+
if (!labels[event.stage]) {
|
|
79
|
+
return event.status === "running" && event.elapsedMs >= 2000
|
|
80
|
+
? `… 正在加载资源,已等待 ${Math.floor(event.elapsedMs / 1000)} 秒\n`
|
|
81
|
+
: "";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const [label, running, completed] = labels[event.stage] ?? ["资源加载"];
|
|
85
|
+
const icon =
|
|
86
|
+
event.status === "completed" ? "✓" : event.status === "failed" ? "✗" : "…";
|
|
87
|
+
let message = event.status === "completed" ? completed : running;
|
|
88
|
+
if (event.status === "failed") {
|
|
89
|
+
message = event.timedOut
|
|
90
|
+
? "请求超过 30 秒,请检查网络和资源服务后重试"
|
|
91
|
+
: `未完成:${event.error}`;
|
|
92
|
+
if (event.error === "fetch failed") {
|
|
93
|
+
message = "无法连接资源服务,请检查网络或资源地址后重试";
|
|
94
|
+
}
|
|
95
|
+
if (event.error?.startsWith("Resource update already in progress:")) {
|
|
96
|
+
message = "已有资源更新任务正在进行,请等待该任务完成后重试";
|
|
97
|
+
}
|
|
98
|
+
const http = /HTTP (\d+)/.exec(event.error ?? "");
|
|
99
|
+
if (http) {
|
|
100
|
+
message = `资源服务返回 HTTP ${http[1]},请检查服务状态或资源地址后重试`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const details = [];
|
|
104
|
+
if (event.stage === "manifest" && event.version) {
|
|
105
|
+
message =
|
|
106
|
+
event.versionKind === "cli"
|
|
107
|
+
? `当前版本 ${event.currentCliVersion},最新版本 ${event.cliVersion}`
|
|
108
|
+
: `当前版本 ${event.currentVersion ?? "尚未加载"},最新版本 ${event.version}`;
|
|
109
|
+
}
|
|
110
|
+
if (event.stage === "download" && event.bytes !== undefined) {
|
|
111
|
+
details.push(
|
|
112
|
+
`${size(event.bytes)}${event.totalBytes ? ` / ${size(event.totalBytes)}` : ""}`,
|
|
113
|
+
);
|
|
114
|
+
if (event.totalBytes && event.bytes <= event.totalBytes) {
|
|
115
|
+
details.push(`${Math.floor((event.bytes / event.totalBytes) * 100)}%`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (event.status === "running" && event.elapsedMs >= 2000) {
|
|
119
|
+
details.push(`仍在处理,已等待 ${Math.floor(event.elapsedMs / 1000)} 秒`);
|
|
120
|
+
}
|
|
121
|
+
return `${icon} [${label}] ${message}${details.length ? ` · ${details.join(" · ")}` : ""}\n`;
|
|
122
|
+
}
|
package/src/protocol.mjs
CHANGED
|
@@ -39,10 +39,12 @@ export function resourcePath(name) {
|
|
|
39
39
|
}
|
|
40
40
|
export function validateManifest(manifest) {
|
|
41
41
|
if (!manifest || manifest.schemaVersion !== 1 || !/^\d+\.\d+\.\d+$/.test(manifest.version) ||
|
|
42
|
+
!/^\d+\.\d+\.\d+$/.test(manifest.cliVersion) ||
|
|
42
43
|
!/^\d+\.\d+\.\d+$/.test(manifest.minCliVersion) || manifest.releaseId !== releaseId(manifest)) {
|
|
43
44
|
throw new Error("Invalid resource manifest or release checksum");
|
|
44
45
|
}
|
|
45
|
-
if (compareVersions(manifest.minCliVersion, manifest.
|
|
46
|
+
if (compareVersions(manifest.minCliVersion, manifest.cliVersion) > 0) throw new Error("Required CLI version exceeds published CLI version");
|
|
47
|
+
compareVersions(manifest.version, manifest.version);
|
|
46
48
|
const files = new Map();
|
|
47
49
|
if (!Array.isArray(manifest.files) || !manifest.files.length) throw new Error("Empty resource manifest");
|
|
48
50
|
for (const file of manifest.files) {
|
package/src/references.mjs
CHANGED
|
@@ -1,74 +1,176 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
lstat,
|
|
3
|
+
mkdir,
|
|
4
|
+
mkdtemp,
|
|
5
|
+
readFile,
|
|
6
|
+
rename,
|
|
7
|
+
rm,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
2
10
|
import { homedir } from "node:os";
|
|
3
11
|
import path from "node:path";
|
|
4
|
-
import {
|
|
12
|
+
import { withProgress } from "./progress.mjs";
|
|
13
|
+
import {
|
|
14
|
+
CLI_VERSION,
|
|
15
|
+
MAX_BYTES,
|
|
16
|
+
MODE_REFERENCES,
|
|
17
|
+
artifactUrl,
|
|
18
|
+
feedUrl,
|
|
19
|
+
requiresNewCli,
|
|
20
|
+
sha256,
|
|
21
|
+
unpackResources,
|
|
22
|
+
validateManifest,
|
|
23
|
+
} from "./protocol.mjs";
|
|
5
24
|
|
|
6
|
-
const cacheRoot = (options) =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
const cacheRoot = (options) =>
|
|
26
|
+
path.resolve(
|
|
27
|
+
options.home ||
|
|
28
|
+
process.env.PLASMIC_RESOURCE_HOME ||
|
|
29
|
+
path.join(homedir(), ".cache", "plasmic"),
|
|
30
|
+
);
|
|
31
|
+
async function download(url, progress) {
|
|
32
|
+
const response = await fetch(url, {
|
|
33
|
+
cache: "no-store",
|
|
34
|
+
signal: AbortSignal.timeout(30000),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Cannot download ${url}: HTTP ${response.status}`);
|
|
38
|
+
}
|
|
10
39
|
const chunks = [];
|
|
11
40
|
let size = 0;
|
|
12
41
|
for await (const chunk of response.body) {
|
|
13
42
|
size += chunk.length;
|
|
14
|
-
if (size > MAX_BYTES)
|
|
43
|
+
if (size > MAX_BYTES) {
|
|
44
|
+
throw new Error("Resource download exceeds limit");
|
|
45
|
+
}
|
|
15
46
|
chunks.push(chunk);
|
|
47
|
+
progress?.({ bytes: size });
|
|
16
48
|
}
|
|
17
49
|
return Buffer.concat(chunks);
|
|
18
50
|
}
|
|
19
51
|
async function latest(options) {
|
|
20
52
|
const feed = feedUrl(options.feed);
|
|
21
|
-
const manifest =
|
|
53
|
+
const manifest = await withProgress(options, "manifest", async (progress) => {
|
|
54
|
+
const state = await current(cacheRoot(options));
|
|
55
|
+
const checkedManifest = validateManifest(
|
|
56
|
+
JSON.parse(
|
|
57
|
+
(await download(new URL("latest.json", feed))).toString("utf8"),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
progress({
|
|
61
|
+
version: checkedManifest.version,
|
|
62
|
+
currentVersion: state?.version ?? null,
|
|
63
|
+
currentCliVersion: CLI_VERSION,
|
|
64
|
+
cliVersion: checkedManifest.cliVersion,
|
|
65
|
+
});
|
|
66
|
+
return checkedManifest;
|
|
67
|
+
});
|
|
22
68
|
const cliUrl = artifactUrl(feed, manifest, "cli");
|
|
23
69
|
return { feed, manifest, cliUrl };
|
|
24
70
|
}
|
|
25
71
|
function versionInfo(release) {
|
|
26
72
|
return {
|
|
27
73
|
cliVersion: CLI_VERSION,
|
|
28
|
-
latestCliVersion: release.manifest.
|
|
74
|
+
latestCliVersion: release.manifest.cliVersion,
|
|
29
75
|
minCliVersion: release.manifest.minCliVersion,
|
|
30
|
-
cliUpdateAvailable: requiresNewCli(release.manifest.
|
|
76
|
+
cliUpdateAvailable: requiresNewCli(release.manifest.cliVersion),
|
|
31
77
|
cliCompatible: !requiresNewCli(release.manifest.minCliVersion),
|
|
32
78
|
cliUrl: release.cliUrl,
|
|
33
79
|
};
|
|
34
80
|
}
|
|
35
81
|
function requireCompatibleCli(release) {
|
|
36
82
|
if (requiresNewCli(release.manifest.minCliVersion)) {
|
|
37
|
-
const error = new Error(
|
|
83
|
+
const error = new Error(
|
|
84
|
+
`Resources require CLI ${release.manifest.minCliVersion}; install ${release.cliUrl} and rerun`,
|
|
85
|
+
);
|
|
38
86
|
Object.assign(error, versionInfo(release));
|
|
39
87
|
throw error;
|
|
40
88
|
}
|
|
41
89
|
}
|
|
42
|
-
export async function checkCliVersion(options = {}) {
|
|
90
|
+
export async function checkCliVersion(options = {}) {
|
|
91
|
+
return versionInfo(await latest(options));
|
|
92
|
+
}
|
|
43
93
|
async function current(root) {
|
|
44
|
-
try {
|
|
45
|
-
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(await readFile(path.join(root, "current.json"), "utf8"));
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code === "ENOENT") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
46
102
|
}
|
|
47
|
-
async function verifyLocal(directory, manifest) {
|
|
103
|
+
async function verifyLocal(directory, manifest, progress) {
|
|
48
104
|
try {
|
|
49
|
-
if (!(await lstat(directory)).isDirectory())
|
|
50
|
-
|
|
105
|
+
if (!(await lstat(directory)).isDirectory()) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const cachedText = await readFile(
|
|
109
|
+
path.join(directory, "manifest.json"),
|
|
110
|
+
"utf8",
|
|
111
|
+
);
|
|
51
112
|
try {
|
|
52
113
|
const cached = validateManifest(JSON.parse(cachedText));
|
|
53
|
-
if (cached.releaseId !== manifest.releaseId)
|
|
54
|
-
|
|
114
|
+
if (cached.releaseId !== manifest.releaseId) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
let completedFiles = 0;
|
|
55
121
|
for (const file of manifest.files) {
|
|
56
122
|
const target = path.join(directory, file.path);
|
|
57
123
|
// Resource directories/files must be real cache entries, never symlinks.
|
|
58
124
|
let parent = path.dirname(target);
|
|
59
125
|
while (parent !== directory) {
|
|
60
|
-
if (!(await lstat(parent)).isDirectory())
|
|
126
|
+
if (!(await lstat(parent)).isDirectory()) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
61
129
|
parent = path.dirname(parent);
|
|
62
130
|
}
|
|
63
131
|
const stat = await lstat(target);
|
|
64
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
!stat.isFile() ||
|
|
134
|
+
stat.size !== file.size ||
|
|
135
|
+
sha256(await readFile(target)) !== file.sha256
|
|
136
|
+
) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
progress?.({
|
|
140
|
+
completedFiles: ++completedFiles,
|
|
141
|
+
totalFiles: manifest.files.length,
|
|
142
|
+
});
|
|
65
143
|
}
|
|
66
144
|
return true;
|
|
67
145
|
} catch (error) {
|
|
68
|
-
if (["ENOENT", "ENOTDIR"].includes(error.code))
|
|
146
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
69
149
|
throw error;
|
|
70
150
|
}
|
|
71
151
|
}
|
|
152
|
+
async function checkCache(directory, manifest, options, state) {
|
|
153
|
+
return withProgress(
|
|
154
|
+
options,
|
|
155
|
+
"cache",
|
|
156
|
+
async (progress) => {
|
|
157
|
+
const valid = await verifyLocal(directory, manifest, progress);
|
|
158
|
+
progress({
|
|
159
|
+
valid,
|
|
160
|
+
...(state !== undefined
|
|
161
|
+
? {
|
|
162
|
+
upToDate:
|
|
163
|
+
valid &&
|
|
164
|
+
state?.releaseId === manifest.releaseId &&
|
|
165
|
+
state?.feed === feedUrl(options.feed),
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
168
|
+
});
|
|
169
|
+
return valid;
|
|
170
|
+
},
|
|
171
|
+
{ completedFiles: 0, totalFiles: manifest.files.length },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
72
174
|
function result(root, release, extra = {}) {
|
|
73
175
|
const resourceRoot = path.join(root, "releases", release.manifest.releaseId);
|
|
74
176
|
return {
|
|
@@ -81,66 +183,176 @@ function result(root, release, extra = {}) {
|
|
|
81
183
|
};
|
|
82
184
|
}
|
|
83
185
|
export async function checkReferences(options = {}) {
|
|
84
|
-
const root = cacheRoot(options),
|
|
186
|
+
const root = cacheRoot(options),
|
|
187
|
+
release = await latest(options),
|
|
188
|
+
state = await current(root);
|
|
85
189
|
const directory = path.join(root, "releases", release.manifest.releaseId);
|
|
86
|
-
const valid = await
|
|
87
|
-
return result(root, release, {
|
|
190
|
+
const valid = await checkCache(directory, release.manifest, options, state);
|
|
191
|
+
return result(root, release, {
|
|
192
|
+
updateAvailable:
|
|
193
|
+
state?.releaseId !== release.manifest.releaseId ||
|
|
194
|
+
state?.feed !== release.feed ||
|
|
195
|
+
!valid,
|
|
196
|
+
localVersion: state?.version || null,
|
|
197
|
+
});
|
|
88
198
|
}
|
|
89
199
|
export async function updateReferences(options = {}) {
|
|
90
|
-
const root = cacheRoot(options),
|
|
91
|
-
|
|
92
|
-
await
|
|
200
|
+
const root = cacheRoot(options),
|
|
201
|
+
release = await latest(options);
|
|
202
|
+
await withProgress(options, "compatibility", () =>
|
|
203
|
+
requireCompatibleCli(release),
|
|
204
|
+
);
|
|
93
205
|
const lock = path.join(root, ".update-lock");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
206
|
+
await withProgress(options, "lock", async () => {
|
|
207
|
+
await mkdir(root, { recursive: true });
|
|
208
|
+
try {
|
|
209
|
+
await mkdir(lock);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error.code === "EEXIST") {
|
|
212
|
+
throw new Error(`Resource update already in progress: ${lock}`);
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
return await installResources(root, release, options);
|
|
219
|
+
} finally {
|
|
220
|
+
await rm(lock, { recursive: true, force: true });
|
|
98
221
|
}
|
|
99
|
-
try { return await installResources(root, release); }
|
|
100
|
-
finally { await rm(lock, { recursive: true, force: true }); }
|
|
101
222
|
}
|
|
102
|
-
async function installResources(root, release) {
|
|
223
|
+
async function installResources(root, release, options) {
|
|
103
224
|
const directory = path.join(root, "releases", release.manifest.releaseId);
|
|
104
225
|
const state = await current(root);
|
|
105
|
-
let updated =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
226
|
+
let updated =
|
|
227
|
+
state?.releaseId !== release.manifest.releaseId ||
|
|
228
|
+
state?.feed !== release.feed;
|
|
229
|
+
if (!(await checkCache(directory, release.manifest, options, state))) {
|
|
230
|
+
const bytes = await withProgress(
|
|
231
|
+
options,
|
|
232
|
+
"download",
|
|
233
|
+
(progress) =>
|
|
234
|
+
download(
|
|
235
|
+
artifactUrl(release.feed, release.manifest, "resources"),
|
|
236
|
+
progress,
|
|
237
|
+
),
|
|
238
|
+
{ bytes: 0, totalBytes: release.manifest.artifacts.resources.size },
|
|
239
|
+
);
|
|
240
|
+
const files = await withProgress(options, "verify", () =>
|
|
241
|
+
unpackResources(bytes, release.manifest),
|
|
242
|
+
);
|
|
243
|
+
await withProgress(
|
|
244
|
+
options,
|
|
245
|
+
"install",
|
|
246
|
+
async (progress) => {
|
|
247
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
248
|
+
const staging = await mkdtemp(
|
|
249
|
+
path.join(root, "releases", ".download-"),
|
|
250
|
+
);
|
|
251
|
+
try {
|
|
252
|
+
let completedFiles = 0;
|
|
253
|
+
for (const file of files) {
|
|
254
|
+
const target = path.join(staging, file.path);
|
|
255
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
256
|
+
await writeFile(target, file.content, { flag: "wx" });
|
|
257
|
+
progress({
|
|
258
|
+
completedFiles: ++completedFiles,
|
|
259
|
+
totalFiles: files.length,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
await writeFile(
|
|
263
|
+
path.join(staging, "manifest.json"),
|
|
264
|
+
JSON.stringify(release.manifest),
|
|
265
|
+
);
|
|
266
|
+
// Replace corrupt entries only after the complete replacement has been verified.
|
|
267
|
+
if (!(await verifyLocal(directory, release.manifest))) {
|
|
268
|
+
await rm(directory, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await rename(staging, directory);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (
|
|
274
|
+
!["EEXIST", "ENOTEMPTY"].includes(error.code) ||
|
|
275
|
+
!(await verifyLocal(directory, release.manifest))
|
|
276
|
+
) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
updated = true;
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(staging, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{ completedFiles: 0, totalFiles: files.length },
|
|
286
|
+
);
|
|
125
287
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
288
|
+
await withProgress(
|
|
289
|
+
options,
|
|
290
|
+
"activate",
|
|
291
|
+
async () => {
|
|
292
|
+
const temporary = await mkdtemp(path.join(root, ".current-"));
|
|
293
|
+
try {
|
|
294
|
+
await writeFile(
|
|
295
|
+
path.join(temporary, "current.json"),
|
|
296
|
+
JSON.stringify({
|
|
297
|
+
feed: release.feed,
|
|
298
|
+
releaseId: release.manifest.releaseId,
|
|
299
|
+
version: release.manifest.version,
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
await rename(
|
|
303
|
+
path.join(temporary, "current.json"),
|
|
304
|
+
path.join(root, "current.json"),
|
|
305
|
+
);
|
|
306
|
+
} finally {
|
|
307
|
+
await rm(temporary, { recursive: true, force: true });
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{ updated },
|
|
311
|
+
);
|
|
131
312
|
return result(root, release, { updated, manifest: release.manifest });
|
|
132
313
|
}
|
|
133
314
|
export async function referencePath(options = {}) {
|
|
134
|
-
const root = cacheRoot(options),
|
|
135
|
-
|
|
315
|
+
const root = cacheRoot(options),
|
|
316
|
+
state = await current(root);
|
|
317
|
+
if (!state || !/^[a-f0-9]{64}$/.test(state.releaseId)) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"No verified local resources; run plasmickit references update",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
136
322
|
const directory = path.join(root, "releases", state.releaseId);
|
|
137
|
-
const manifest = validateManifest(
|
|
138
|
-
|
|
139
|
-
|
|
323
|
+
const manifest = validateManifest(
|
|
324
|
+
JSON.parse(await readFile(path.join(directory, "manifest.json"), "utf8")),
|
|
325
|
+
);
|
|
326
|
+
if (
|
|
327
|
+
manifest.releaseId !== state.releaseId ||
|
|
328
|
+
!(await checkCache(directory, manifest, options))
|
|
329
|
+
) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
"Local resources are corrupt; run plasmickit references update",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return result(
|
|
335
|
+
root,
|
|
336
|
+
{ manifest, cliUrl: artifactUrl(feedUrl(state.feed), manifest, "cli") },
|
|
337
|
+
{ freshness: "unchecked" },
|
|
338
|
+
);
|
|
140
339
|
}
|
|
141
340
|
export async function resolveContext(mode, options = {}) {
|
|
142
|
-
if (!Object.hasOwn(MODE_REFERENCES, mode))
|
|
341
|
+
if (!Object.hasOwn(MODE_REFERENCES, mode)) {
|
|
342
|
+
throw new Error("--mode must be inspect, prototype or codegen");
|
|
343
|
+
}
|
|
143
344
|
const { manifest, ...resources } = await updateReferences(options);
|
|
144
|
-
|
|
145
|
-
|
|
345
|
+
return withProgress(options, "workflow", (progress) => {
|
|
346
|
+
const references = [
|
|
347
|
+
manifest.entrypoints.guide,
|
|
348
|
+
"references/desktop-mcp.md",
|
|
349
|
+
manifest.entrypoints[mode],
|
|
350
|
+
...MODE_REFERENCES[mode],
|
|
351
|
+
];
|
|
352
|
+
const mustRead = references.map((reference) =>
|
|
353
|
+
path.join(resources.resourceRoot, reference),
|
|
354
|
+
);
|
|
355
|
+
progress({ mode, mustRead });
|
|
356
|
+
return { ...resources, mode, mustRead };
|
|
357
|
+
});
|
|
146
358
|
}
|