@plasmickit/cli 0.0.82 → 0.0.84

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasmickit/cli",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
4
4
  "description": "Plasmic skill bootstrap and NAS workflow resource manager",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: plasmic
3
- description: Explain or review self-hosted Plasmic pages, components and templates; create or revise editable prototypes; implement or update native React project code from Plasmic designs. Use for Plasmic model/template inspection, prototype design and design-to-code work.
3
+ description: Explain or review self-hosted Plasmic pages, components and templates; select or create Teams, Workspaces and Projects for editable prototype design; implement or update native React project code from Plasmic designs. Use for Plasmic model/template inspection, prototype design and design-to-code work.
4
4
  ---
5
5
 
6
6
  # Plasmic
7
7
 
8
8
  This is a bootstrap skill. Workflows, component guidance and verification scripts live in a versioned resource bundle on the NAS, outside this installed skill.
9
9
 
10
- Choose the task mode before work: `inspect` for read-only page/template explanation or design review, `prototype` for design edits, or `codegen` for native React implementation and updates. Run the matching command:
10
+ Choose the task mode before work: `inspect` for read-only page/template explanation or design review, `prototype` for selecting/creating the design project and design edits, or `codegen` for native React implementation and updates. Run the matching command:
11
11
 
12
12
  ```sh
13
13
  npx -y @plasmickit/cli@latest context resolve --mode inspect
@@ -19,6 +19,10 @@ 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
+ For prototype work, follow the workflow's project-management reference to resolve Team → Workspace → Project before entering Studio design operations.
23
+
24
+ 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.
25
+
22
26
  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
27
 
24
28
  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 { checkCliVersion, checkReferences, referencePath, resolveContext, updateReferences } from "./references.mjs";
7
- import { installSkill } from "./skill.mjs";
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({ args: argv, allowPositionals: true, options: {
19
- "dry-run": { type: "boolean" }, mode: { type: "string" }, feed: { type: "string" }, home: { type: "string" },
20
- help: { type: "boolean" }, version: { type: "boolean" },
21
- } });
22
- if (values.version) { process.stdout.write(CLI_VERSION + "\n"); return; }
23
- if (!positionals.length || values.help) { process.stdout.write(usage); return; }
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) throw new Error("Expected a command and action; use --help");
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") output = await installSkill({ dryRun: values["dry-run"] });
28
- else if (command === "version" && action === "check") output = await checkCliVersion(values);
29
- else if (command === "references" && action === "check") output = await checkReferences(values);
30
- else if (command === "references" && action === "update") {
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) process.exitCode = 1;
74
+ if (output.ok === false) {
75
+ process.exitCode = 1;
76
+ }
39
77
  }
40
- if (process.argv[1] && realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
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(JSON.stringify({ ok: false, error: error.message, ...(error.cliUrl ? { cliUrl: error.cliUrl, cliVersion: error.cliVersion, latestCliVersion: error.latestCliVersion, minCliVersion: error.minCliVersion } : {}) }) + "\n");
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
  }
@@ -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
+ }
@@ -1,24 +1,70 @@
1
- import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
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 { artifactUrl, CLI_VERSION, MODE_REFERENCES, feedUrl, MAX_BYTES, requiresNewCli, sha256, unpackResources, validateManifest } from "./protocol.mjs";
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) => path.resolve(options.home || process.env.PLASMIC_RESOURCE_HOME || path.join(homedir(), ".cache", "plasmic"));
7
- async function download(url) {
8
- const response = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(30000) });
9
- if (!response.ok) throw new Error(`Cannot download ${url}: HTTP ${response.status}`);
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) throw new Error("Resource download exceeds limit");
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 = validateManifest(JSON.parse((await download(new URL("latest.json", feed))).toString("utf8")));
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
  }
@@ -34,41 +80,97 @@ function versionInfo(release) {
34
80
  }
35
81
  function requireCompatibleCli(release) {
36
82
  if (requiresNewCli(release.manifest.minCliVersion)) {
37
- const error = new Error(`Resources require CLI ${release.manifest.minCliVersion}; install ${release.cliUrl} and rerun`);
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 = {}) { return versionInfo(await latest(options)); }
90
+ export async function checkCliVersion(options = {}) {
91
+ return versionInfo(await latest(options));
92
+ }
43
93
  async function current(root) {
44
- try { return JSON.parse(await readFile(path.join(root, "current.json"), "utf8")); }
45
- catch (error) { if (error.code === "ENOENT") return null; throw error; }
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()) return false;
50
- const cachedText = await readFile(path.join(directory, "manifest.json"), "utf8");
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) return false;
54
- } catch { return false; }
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()) return false;
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 (!stat.isFile() || stat.size !== file.size || sha256(await readFile(target)) !== file.sha256) return false;
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)) return false;
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), release = await latest(options), state = await current(root);
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 verifyLocal(directory, release.manifest);
87
- return result(root, release, { updateAvailable: state?.releaseId !== release.manifest.releaseId || state?.feed !== release.feed || !valid, localVersion: state?.version || null });
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), release = await latest(options);
91
- requireCompatibleCli(release);
92
- await mkdir(root, { recursive: true });
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
- try { await mkdir(lock); }
95
- catch (error) {
96
- if (error.code === "EEXIST") throw new Error(`Resource update already in progress: ${lock}`);
97
- throw error;
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 = state?.releaseId !== release.manifest.releaseId || state?.feed !== release.feed;
106
- if (!await verifyLocal(directory, release.manifest)) {
107
- const files = unpackResources(await download(artifactUrl(release.feed, release.manifest, "resources")), release.manifest);
108
- await mkdir(path.dirname(directory), { recursive: true });
109
- const staging = await mkdtemp(path.join(root, "releases", ".download-"));
110
- try {
111
- for (const file of files) {
112
- const target = path.join(staging, file.path);
113
- await mkdir(path.dirname(target), { recursive: true });
114
- await writeFile(target, file.content, { flag: "wx" });
115
- }
116
- await writeFile(path.join(staging, "manifest.json"), JSON.stringify(release.manifest));
117
- // Replace corrupt entries only after the complete replacement has been verified.
118
- if (!await verifyLocal(directory, release.manifest)) await rm(directory, { recursive: true, force: true });
119
- try { await rename(staging, directory); }
120
- catch (error) {
121
- if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !await verifyLocal(directory, release.manifest)) throw error;
122
- }
123
- updated = true;
124
- } finally { await rm(staging, { recursive: true, force: true }); }
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
- const temporary = await mkdtemp(path.join(root, ".current-"));
127
- try {
128
- await writeFile(path.join(temporary, "current.json"), JSON.stringify({ feed: release.feed, releaseId: release.manifest.releaseId, version: release.manifest.version }));
129
- await rename(path.join(temporary, "current.json"), path.join(root, "current.json"));
130
- } finally { await rm(temporary, { recursive: true, force: true }); }
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), state = await current(root);
135
- if (!state || !/^[a-f0-9]{64}$/.test(state.releaseId)) throw new Error("No verified local resources; run plasmickit references update");
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(JSON.parse(await readFile(path.join(directory, "manifest.json"), "utf8")));
138
- if (manifest.releaseId !== state.releaseId || !await verifyLocal(directory, manifest)) throw new Error("Local resources are corrupt; run plasmickit references update");
139
- return result(root, { manifest, cliUrl: artifactUrl(feedUrl(state.feed), manifest, "cli") }, { freshness: "unchecked" });
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)) throw new Error("--mode must be inspect, prototype or codegen");
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
- const references = [manifest.entrypoints.guide, "references/desktop-mcp.md", manifest.entrypoints[mode], ...MODE_REFERENCES[mode]];
145
- return { ...resources, mode, mustRead: references.map((reference) => path.join(resources.resourceRoot, reference)) };
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
  }