@plasmickit/cli 0.0.0-stage → 0.0.36

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,26 @@
1
1
  {
2
2
  "name": "@plasmickit/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.36",
4
+ "description": "Plasmic skill bootstrap and NAS workflow resource manager",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/shiguang-lab/plasmic.git",
9
+ "directory": "packages/plasmic-cli"
10
+ },
11
+ "publishConfig": {
12
+ "registry": "https://registry.npmjs.org/",
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=22.12.0"
18
+ },
19
+ "bin": {
20
+ "plasmickit": "src/index.mjs"
21
+ },
22
+ "files": [
23
+ "src",
24
+ "skill"
25
+ ]
26
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: plasmic
3
+ description: Design, revise, and verify editable product prototypes in self-hosted Plasmic, or read Plasmic pages and generate native React project code under maintained engineering standards. Use for Plasmic product prototyping and design-to-code development.
4
+ ---
5
+
6
+ # Plasmic
7
+
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
+
10
+ At the start of each task, choose `prototype` for product design or `codegen` for code generation and integration. Run:
11
+
12
+ ```sh
13
+ npx -y @plasmickit/cli@latest context resolve --mode prototype
14
+ # Or: npx -y @plasmickit/cli@latest context resolve --mode codegen
15
+ ```
16
+
17
+ In `codegen` mode, read page models, component contracts and interactions through Desktop MCP, then implement code in the target project's architecture. Generation follows the returned standards; it does not use `export_code` or the official Plasmic CLI.
18
+
19
+ 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 both modes, resolve and read each mode before its work begins.
20
+
21
+ The runner uses the latest published Plasmic 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`.
22
+
23
+ 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 ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+ import { realpathSync } from "node:fs";
6
+ import { checkCliVersion, checkReferences, referencePath, resolveContext, updateReferences } from "./references.mjs";
7
+ import { installSkill } from "./skill.mjs";
8
+ import { CLI_VERSION } from "./protocol.mjs";
9
+
10
+ const usage = `plasmickit commands:
11
+ skill install --target SKILLS_ROOT
12
+ version check [--feed HTTPS_URL]
13
+ references check|update|path [--feed HTTPS_URL] [--home CACHE_ROOT]
14
+ context resolve --mode prototype|codegen [--feed HTTPS_URL] [--home CACHE_ROOT]
15
+ --version
16
+ `;
17
+ export async function main(argv) {
18
+ const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: {
19
+ target: { type: "string" }, 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; }
24
+ const [command, action] = positionals;
25
+ if (positionals.length !== 2) throw new Error("Expected a command and action; use --help");
26
+ let output;
27
+ if (command === "skill" && action === "install") output = await installSkill(values.target);
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") {
31
+ const { manifest: _manifest, ...result } = await updateReferences(values);
32
+ output = result;
33
+ }
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
+ process.stdout.write(JSON.stringify({ ok: true, ...output }, null, 2) + "\n");
38
+ }
39
+ if (process.argv[1] && realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
40
+ main(process.argv.slice(2)).catch((error) => {
41
+ 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");
42
+ process.exitCode = 1;
43
+ });
44
+ }
@@ -0,0 +1,102 @@
1
+ import { createHash } from "node:crypto";
2
+ import { gunzipSync } from "node:zlib";
3
+ import { createRequire } from "node:module";
4
+
5
+ export const DEFAULT_FEED = "https://plasmic.studio.publib.cn/desktop-updates/plasmic";
6
+ export const CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
7
+ export const MIN_CLI_VERSION = "0.0.34";
8
+ export const CODEGEN_REFERENCES = [
9
+ "references/codegen/page-reading.md",
10
+ "references/codegen/project-code.md",
11
+ "references/codegen/interactions.md",
12
+ "references/codegen/acceptance.md",
13
+ ];
14
+ export function compareVersions(a, b) {
15
+ const parse = (version) => {
16
+ if (typeof version !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) throw new Error(`Invalid stable version: ${version}`);
17
+ const parts = version.split(".").map(Number);
18
+ if (!parts.every(Number.isSafeInteger)) throw new Error(`Invalid stable version: ${version}`);
19
+ return parts;
20
+ };
21
+ const left = parse(a), right = parse(b);
22
+ const changed = left.findIndex((part, index) => part !== right[index]);
23
+ return changed === -1 ? 0 : left[changed] > right[changed] ? 1 : -1;
24
+ }
25
+ export const MAX_BYTES = 16 * 1024 * 1024;
26
+ export const sha256 = (data) => createHash("sha256").update(data).digest("hex");
27
+ const canonical = (value) => JSON.stringify(value, function (_key, item) {
28
+ return item && typeof item === "object" && !Array.isArray(item)
29
+ ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item;
30
+ });
31
+ export function releaseId(manifest) {
32
+ const { releaseId: _id, ...content } = manifest;
33
+ return sha256(canonical(content));
34
+ }
35
+ export function resourcePath(name) {
36
+ if (typeof name !== "string" || !/^(references|scripts)\/(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]+\.(md|py|json)$/.test(name)) {
37
+ throw new Error(`Invalid resource path: ${name}`);
38
+ }
39
+ return name;
40
+ }
41
+ export function validateManifest(manifest) {
42
+ if (!manifest || manifest.schemaVersion !== 1 || !/^\d+\.\d+\.\d+$/.test(manifest.version) ||
43
+ !/^\d+\.\d+\.\d+$/.test(manifest.minCliVersion) || manifest.releaseId !== releaseId(manifest)) {
44
+ throw new Error("Invalid resource manifest or release checksum");
45
+ }
46
+ if (compareVersions(manifest.minCliVersion, manifest.version) > 0) throw new Error("Required CLI version exceeds release version");
47
+ const files = new Map();
48
+ if (!Array.isArray(manifest.files) || !manifest.files.length) throw new Error("Empty resource manifest");
49
+ for (const file of manifest.files) {
50
+ resourcePath(file.path);
51
+ if (files.has(file.path) || !/^[a-f0-9]{64}$/.test(file.sha256) || !Number.isSafeInteger(file.size) || file.size < 0 || file.size > MAX_BYTES) {
52
+ throw new Error("Invalid or duplicate resource entry");
53
+ }
54
+ files.set(file.path, file);
55
+ }
56
+ for (const mode of ["guide", "prototype", "codegen"]) {
57
+ const entry = manifest.entrypoints?.[mode];
58
+ if (!files.has(entry) || !entry.startsWith("references/") || !entry.endsWith(".md")) throw new Error(`Missing ${mode} entrypoint`);
59
+ }
60
+ if (!files.has("references/desktop-mcp.md")) throw new Error("Missing Desktop MCP reference");
61
+ for (const reference of CODEGEN_REFERENCES) {
62
+ if (!files.has(reference)) throw new Error(`Missing code-generation standard: ${reference}`);
63
+ }
64
+ for (const [kind, name] of [["resources", "resources.json.gz"], ["cli", "plasmic-cli.tgz"]]) {
65
+ const artifact = manifest.artifacts?.[kind];
66
+ if (!artifact || artifact.file !== name || !/^[a-f0-9]{64}$/.test(artifact.sha256) ||
67
+ !Number.isSafeInteger(artifact.size) || artifact.size <= 0 || artifact.size > MAX_BYTES) throw new Error(`Invalid ${kind} artifact`);
68
+ }
69
+ return manifest;
70
+ }
71
+ export function verifyArtifact(bytes, artifact) {
72
+ if (bytes.length !== artifact.size || sha256(bytes) !== artifact.sha256) throw new Error(`Artifact checksum mismatch: ${artifact.file}`);
73
+ }
74
+ export function unpackResources(bytes, manifest) {
75
+ verifyArtifact(bytes, manifest.artifacts.resources);
76
+ const bundle = JSON.parse(gunzipSync(bytes, { maxOutputLength: MAX_BYTES }).toString("utf8"));
77
+ if (bundle.schemaVersion !== 1 || !Array.isArray(bundle.files) || bundle.files.length !== manifest.files.length) throw new Error("Resource bundle does not match manifest");
78
+ const expected = new Map(manifest.files.map((file) => [file.path, file]));
79
+ for (const file of bundle.files) {
80
+ resourcePath(file.path);
81
+ const entry = expected.get(file.path);
82
+ if (!entry || typeof file.content !== "string" || Buffer.byteLength(file.content) !== entry.size || sha256(file.content) !== entry.sha256) {
83
+ throw new Error(`Resource checksum mismatch: ${file.path}`);
84
+ }
85
+ expected.delete(file.path);
86
+ }
87
+ return bundle.files;
88
+ }
89
+ export function feedUrl(value = process.env.PLASMIC_RESOURCE_URL || DEFAULT_FEED) {
90
+ const url = new URL(value.replace(/\/+$/, "") + "/");
91
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))) {
92
+ throw new Error("Resource feed must use HTTPS (HTTP is allowed only for loopback tests)");
93
+ }
94
+ if (url.search || url.hash || url.username || url.password) throw new Error("Invalid resource feed URL");
95
+ return url.href;
96
+ }
97
+ export function artifactUrl(feed, manifest, kind) {
98
+ return new URL(`releases/${manifest.releaseId}/${manifest.artifacts[kind].file}`, feed).href;
99
+ }
100
+ export function requiresNewCli(minimum, current = CLI_VERSION) {
101
+ return compareVersions(minimum, current) > 0;
102
+ }
@@ -0,0 +1,146 @@
1
+ import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { artifactUrl, CLI_VERSION, CODEGEN_REFERENCES, feedUrl, MAX_BYTES, requiresNewCli, sha256, unpackResources, validateManifest } from "./protocol.mjs";
5
+
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}`);
10
+ const chunks = [];
11
+ let size = 0;
12
+ for await (const chunk of response.body) {
13
+ size += chunk.length;
14
+ if (size > MAX_BYTES) throw new Error("Resource download exceeds limit");
15
+ chunks.push(chunk);
16
+ }
17
+ return Buffer.concat(chunks);
18
+ }
19
+ async function latest(options) {
20
+ const feed = feedUrl(options.feed);
21
+ const manifest = validateManifest(JSON.parse((await download(new URL("latest.json", feed))).toString("utf8")));
22
+ const cliUrl = artifactUrl(feed, manifest, "cli");
23
+ return { feed, manifest, cliUrl };
24
+ }
25
+ function versionInfo(release) {
26
+ return {
27
+ cliVersion: CLI_VERSION,
28
+ latestCliVersion: release.manifest.version,
29
+ minCliVersion: release.manifest.minCliVersion,
30
+ cliUpdateAvailable: requiresNewCli(release.manifest.version),
31
+ cliCompatible: !requiresNewCli(release.manifest.minCliVersion),
32
+ cliUrl: release.cliUrl,
33
+ };
34
+ }
35
+ function requireCompatibleCli(release) {
36
+ if (requiresNewCli(release.manifest.minCliVersion)) {
37
+ const error = new Error(`Resources require CLI ${release.manifest.minCliVersion}; install ${release.cliUrl} and rerun`);
38
+ Object.assign(error, versionInfo(release));
39
+ throw error;
40
+ }
41
+ }
42
+ export async function checkCliVersion(options = {}) { return versionInfo(await latest(options)); }
43
+ 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; }
46
+ }
47
+ async function verifyLocal(directory, manifest) {
48
+ try {
49
+ if (!(await lstat(directory)).isDirectory()) return false;
50
+ const cachedText = await readFile(path.join(directory, "manifest.json"), "utf8");
51
+ try {
52
+ const cached = validateManifest(JSON.parse(cachedText));
53
+ if (cached.releaseId !== manifest.releaseId) return false;
54
+ } catch { return false; }
55
+ for (const file of manifest.files) {
56
+ const target = path.join(directory, file.path);
57
+ // Resource directories/files must be real cache entries, never symlinks.
58
+ let parent = path.dirname(target);
59
+ while (parent !== directory) {
60
+ if (!(await lstat(parent)).isDirectory()) return false;
61
+ parent = path.dirname(parent);
62
+ }
63
+ const stat = await lstat(target);
64
+ if (!stat.isFile() || stat.size !== file.size || sha256(await readFile(target)) !== file.sha256) return false;
65
+ }
66
+ return true;
67
+ } catch (error) {
68
+ if (["ENOENT", "ENOTDIR"].includes(error.code)) return false;
69
+ throw error;
70
+ }
71
+ }
72
+ function result(root, release, extra = {}) {
73
+ const resourceRoot = path.join(root, "releases", release.manifest.releaseId);
74
+ return {
75
+ version: release.manifest.version,
76
+ releaseId: release.manifest.releaseId,
77
+ resourceRoot,
78
+ referencePath: path.join(resourceRoot, release.manifest.entrypoints.guide),
79
+ ...versionInfo(release),
80
+ ...extra,
81
+ };
82
+ }
83
+ export async function checkReferences(options = {}) {
84
+ const root = cacheRoot(options), release = await latest(options), state = await current(root);
85
+ 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 });
88
+ }
89
+ export async function updateReferences(options = {}) {
90
+ const root = cacheRoot(options), release = await latest(options);
91
+ requireCompatibleCli(release);
92
+ await mkdir(root, { recursive: true });
93
+ 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;
98
+ }
99
+ try { return await installResources(root, release); }
100
+ finally { await rm(lock, { recursive: true, force: true }); }
101
+ }
102
+ async function installResources(root, release) {
103
+ const directory = path.join(root, "releases", release.manifest.releaseId);
104
+ 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 }); }
125
+ }
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 }); }
131
+ return result(root, release, { updated, manifest: release.manifest });
132
+ }
133
+ 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");
136
+ 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" });
140
+ }
141
+ export async function resolveContext(mode, options = {}) {
142
+ if (!["prototype", "codegen"].includes(mode)) throw new Error("--mode must be prototype or codegen");
143
+ const { manifest, ...resources } = await updateReferences(options);
144
+ const references = [manifest.entrypoints.guide, "references/desktop-mcp.md", manifest.entrypoints[mode], ...(mode === "codegen" ? CODEGEN_REFERENCES : [])];
145
+ return { ...resources, mode, mustRead: references.map((reference) => path.join(resources.resourceRoot, reference)) };
146
+ }
package/src/skill.mjs ADDED
@@ -0,0 +1,11 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function installSkill(targetRoot) {
5
+ if (!targetRoot) throw new Error("skill install requires --target SKILLS_ROOT");
6
+ const target = path.resolve(targetRoot, "plasmic");
7
+ const content = await readFile(new URL("../skill/plasmic/SKILL.md", import.meta.url), "utf8");
8
+ await mkdir(target, { recursive: true });
9
+ await writeFile(path.join(target, "SKILL.md"), content);
10
+ return { target, skillPath: path.join(target, "SKILL.md"), mode: "bootstrap" };
11
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.