@calibrate-ds/cli 0.1.25 → 0.1.27
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/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +4 -2
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +26 -26
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/document.d.ts.map +1 -1
- package/dist/commands/document.js +2 -1
- package/dist/commands/document.js.map +1 -1
- package/dist/commands/generate-components.d.ts.map +1 -1
- package/dist/commands/generate-components.js +11 -5
- package/dist/commands/generate-components.js.map +1 -1
- package/dist/commands/implement.d.ts.map +1 -1
- package/dist/commands/implement.js +2 -1
- package/dist/commands/implement.js.map +1 -1
- package/dist/commands/prompt.js +2 -1
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +3 -13
- package/dist/commands/scan.js.map +1 -1
- package/dist/mcp/tools/action-tools.d.ts.map +1 -1
- package/dist/mcp/tools/action-tools.js +189 -217
- package/dist/mcp/tools/action-tools.js.map +1 -1
- package/dist/mcp/tools/component-tools.d.ts.map +1 -1
- package/dist/mcp/tools/component-tools.js +84 -29
- package/dist/mcp/tools/component-tools.js.map +1 -1
- package/dist/mcp/tools/governance-tools.d.ts.map +1 -1
- package/dist/mcp/tools/governance-tools.js +70 -184
- package/dist/mcp/tools/governance-tools.js.map +1 -1
- package/dist/mcp/tools/info-tools.d.ts.map +1 -1
- package/dist/mcp/tools/info-tools.js +44 -172
- package/dist/mcp/tools/info-tools.js.map +1 -1
- package/dist/mcp/tools/token-tools.d.ts.map +1 -1
- package/dist/mcp/tools/token-tools.js +71 -59
- package/dist/mcp/tools/token-tools.js.map +1 -1
- package/dist/mcp/tools/verify-tool.d.ts.map +1 -1
- package/dist/mcp/tools/verify-tool.js +0 -37
- package/dist/mcp/tools/verify-tool.js.map +1 -1
- package/dist/utils/managed-components.d.ts +14 -0
- package/dist/utils/managed-components.d.ts.map +1 -0
- package/dist/utils/managed-components.js +23 -0
- package/dist/utils/managed-components.js.map +1 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAyBA,wBAAsB,WAAW,kBA+GhC"}
|
package/dist/commands/scan.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { loadConfig, resolveTokenSource, exportAllComponentContexts, loadManifest, resolveDSPaths, enrichIconsWithSvg, enrichBitmapAssets, enrichComponentThumbnails, getDesignToolAdapter, saveSnapshot, generateSystemClaude } from "@calibrate-ds/core";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import * as readline from "node:readline";
|
|
5
4
|
import { logger } from "../utils/logger.js";
|
|
6
|
-
function ask(question) {
|
|
7
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
-
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
9
|
-
}
|
|
10
5
|
/**
|
|
11
6
|
* Counts layout-tree nodes that received an `imageAssetPath` tag from
|
|
12
7
|
* enrichBitmapAssets. Used purely for the post-scan summary line — the
|
|
@@ -34,7 +29,8 @@ export async function scanCommand() {
|
|
|
34
29
|
logger.info("Loading ptb.config.json...");
|
|
35
30
|
const config = await loadConfig();
|
|
36
31
|
const { provider, fileKey, accessTokenEnv } = config.designTool;
|
|
37
|
-
//
|
|
32
|
+
// Inform (don't block) if there are pending components — scanning accumulates changes,
|
|
33
|
+
// it does not reset the diff baseline. Stamps are preserved across scans.
|
|
38
34
|
if (process.stdout.isTTY) {
|
|
39
35
|
const { workspaceRoot } = resolveDSPaths(config);
|
|
40
36
|
const lockPath = path.join(workspaceRoot, "ptb.lock");
|
|
@@ -42,13 +38,7 @@ export async function scanCommand() {
|
|
|
42
38
|
const lock = JSON.parse(await fs.readFile(lockPath, "utf-8"));
|
|
43
39
|
const pending = Object.values(lock.components).filter(e => !e.stampedHash || e.stampedHash !== e.designHash);
|
|
44
40
|
if (pending.length > 0) {
|
|
45
|
-
logger.
|
|
46
|
-
logger.dim(" Scanning now will overwrite the current diff baseline.");
|
|
47
|
-
const answer = await ask(" Continue scanning? (Y/n) ");
|
|
48
|
-
if (answer.toLowerCase() === "n") {
|
|
49
|
-
logger.info("Scan cancelled. Run 'ptb diff' to review pending changes.");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
41
|
+
logger.info(`${pending.length} component(s) with pending changes — scanning will accumulate, not reset.`);
|
|
52
42
|
}
|
|
53
43
|
}
|
|
54
44
|
catch { /* no ptb.lock yet — first scan, proceed */ }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC3P,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/action-tools.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/action-tools.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,gBAAgB,CAAC;AAqC/D,eAAO,MAAM,WAAW,EAAE,YAwuBzB,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getDesignToolAdapter, saveSnapshot, exportAllComponentContexts, loadManifest, updateComponentArtifacts, loadConfig, getGenerator, syncManifestWithReport, createNamingStrategy, resolveDSPaths, isIconComponent, normalizeIconDisplayName, getComponentContext, generateComponentStories, generateComponentPrompt, generateComponentDoc, buildReverseDepsMap, formatMetadataHeader, pruneStaleComponents, resolveTokenSource, enrichIconsWithSvg, enrichBitmapAssets, enrichComponentThumbnails, } from "@calibrate-ds/core";
|
|
6
6
|
import { invalidateCache, resolveCwd } from "../server.js";
|
|
7
|
+
import { getManagedSnapshot } from "../../utils/managed-components.js";
|
|
7
8
|
function toSlug(name) {
|
|
8
9
|
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9
10
|
}
|
|
@@ -42,136 +43,144 @@ async function saveLock(workspaceRoot, lock) {
|
|
|
42
43
|
export const actionTools = {
|
|
43
44
|
name: "action",
|
|
44
45
|
register(server, getContext) {
|
|
45
|
-
server.tool("
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Mirror the full enrichment pipeline from scan.ts so MCP scans
|
|
71
|
-
// produce the same snapshot as CLI scans (tokens, icons, bitmaps, thumbnails).
|
|
72
|
-
const tokenResolution = await resolveTokenSource(config, token);
|
|
73
|
-
if (tokenResolution.tokens.length > 0) {
|
|
74
|
-
model.tokens = tokenResolution.tokens;
|
|
46
|
+
server.tool("run", `Execute a PTB pipeline stage. Use this to scan Figma, generate code, or maintain the local design system state.
|
|
47
|
+
|
|
48
|
+
PIPELINE ORDER — run stages in this sequence after first setup:
|
|
49
|
+
1. scan → fetch Figma snapshot, enrich tokens/icons/thumbnails, auto-exports context
|
|
50
|
+
2. tokens → regenerate CSS design token files from snapshot
|
|
51
|
+
3. generate → scaffold typed React component shells (accepts optional name param)
|
|
52
|
+
4. context → re-export context files + update ptb.lock without re-scanning Figma
|
|
53
|
+
5. prune → remove generated folders for components deleted from Figma
|
|
54
|
+
|
|
55
|
+
WHEN TO USE EACH:
|
|
56
|
+
scan — Figma file has changed or this is the first run. Requires FIGMA_ACCESS_TOKEN (or configured env var) in env.
|
|
57
|
+
tokens — After scan, or whenever token values changed in Figma.
|
|
58
|
+
generate — After scan, or to scaffold a specific new component (use name param).
|
|
59
|
+
context — After implementing components to refresh ptb.lock hashes; or after scan if you skipped it.
|
|
60
|
+
prune — Components were deleted in Figma and their folders should be removed from the repo.`, {
|
|
61
|
+
stage: z.enum(["scan", "generate", "tokens", "context", "prune"])
|
|
62
|
+
.describe("Pipeline stage to execute"),
|
|
63
|
+
name: z.string().optional().default(".")
|
|
64
|
+
.describe("For stage:'generate' only — component name or '.' for all. Ignored by other stages."),
|
|
65
|
+
}, async ({ stage, name = "." }) => {
|
|
66
|
+
if (stage === "scan") {
|
|
67
|
+
let config;
|
|
68
|
+
try {
|
|
69
|
+
const cwd = await resolveCwd();
|
|
70
|
+
config = await loadConfig(cwd);
|
|
75
71
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await enrichComponentThumbnails(model, fileKey, token, config, adapter);
|
|
79
|
-
await saveSnapshot(model, config);
|
|
80
|
-
invalidateCache();
|
|
81
|
-
const manifest = await loadManifest(config).catch(() => ({ components: {} }));
|
|
82
|
-
await exportAllComponentContexts(model, config, manifest);
|
|
83
|
-
return { content: [{ type: "text", text: `Scan complete. ${model.components.length} components, ${model.tokens?.length ?? 0} token collections.` }] };
|
|
84
|
-
}
|
|
85
|
-
catch (e) {
|
|
86
|
-
return { content: [{ type: "text", text: `Scan failed: ${e.message}` }] };
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
server.tool("generate_components", "Regenerate typed React component shells from the latest snapshot. Equivalent to 'ptb generate-components'. Pass a component name or '.' for all.", { name: z.string().default(".").describe("Component name or '.' for all") }, async ({ name }) => {
|
|
90
|
-
let ctx;
|
|
91
|
-
try {
|
|
92
|
-
ctx = await getContext();
|
|
93
|
-
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
return { content: [{ type: "text", text: e.message }] };
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
const { snapshot, config } = ctx;
|
|
99
|
-
const targets = name === "." || name === "*"
|
|
100
|
-
? snapshot.components
|
|
101
|
-
: snapshot.components.filter(c => c.name.toLowerCase() === name.toLowerCase() || toSlug(c.name) === toSlug(name));
|
|
102
|
-
if (targets.length === 0) {
|
|
103
|
-
return { content: [{ type: "text", text: `No component found matching '${name}'.` }] };
|
|
72
|
+
catch (e) {
|
|
73
|
+
return { content: [{ type: "text", text: e.message }] };
|
|
104
74
|
}
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return { content: [{ type: "text", text: e.message }] };
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
const { snapshot, config } = ctx;
|
|
137
|
-
if (!snapshot.tokens?.length) {
|
|
138
|
-
return { content: [{ type: "text", text: "No token collections in snapshot. Run run_scan first." }] };
|
|
75
|
+
const tokenEnv = config.designTool.accessTokenEnv;
|
|
76
|
+
const token = process.env[tokenEnv];
|
|
77
|
+
if (!token) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `${tokenEnv} is not set in the MCP server environment.\n\nThe MCP server is a separate process from your terminal — it does not inherit your shell exports.\n\nFix options:\n 1. Add to ~/.zshrc or ~/.bashrc: export ${tokenEnv}=your_token\n Then launch your IDE from the terminal (not the dock): e.g. 'cursor .' or 'code .'\n 2. Mac only (dock-launched IDEs): run in terminal: launchctl setenv ${tokenEnv} your_token\n Then fully quit and relaunch your IDE.\n NOTE: If you already ran launchctl setenv, the MCP server may have started before the token was set. Fully quitting and relaunching your IDE is required — the running server cannot see tokens set after it started.\n 3. Alternatively, run 'ptb scan' directly in your terminal where the token is already exported.`,
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const fileKey = config.designTool.fileKey;
|
|
87
|
+
const adapter = getDesignToolAdapter(config.designTool.provider);
|
|
88
|
+
const model = await adapter.scanDesignFile({ fileKey, accessToken: token });
|
|
89
|
+
const tokenResolution = await resolveTokenSource(config, token);
|
|
90
|
+
if (tokenResolution.tokens.length > 0)
|
|
91
|
+
model.tokens = tokenResolution.tokens;
|
|
92
|
+
await enrichIconsWithSvg(model, fileKey, token, adapter);
|
|
93
|
+
await enrichBitmapAssets(model, fileKey, token, config);
|
|
94
|
+
await enrichComponentThumbnails(model, fileKey, token, config, adapter);
|
|
95
|
+
await saveSnapshot(model, config);
|
|
96
|
+
invalidateCache();
|
|
97
|
+
const manifest = await loadManifest(config).catch(() => ({ components: {} }));
|
|
98
|
+
await exportAllComponentContexts(model, config, manifest);
|
|
99
|
+
return { content: [{ type: "text", text: `Scan complete. ${model.components.length} components, ${model.tokens?.length ?? 0} token collections.` }] };
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return { content: [{ type: "text", text: `Scan failed: ${e.message}` }] };
|
|
139
103
|
}
|
|
140
|
-
const generator = getGenerator(config.framework.name || "react");
|
|
141
|
-
const report = await generator.writeTokens(snapshot, config);
|
|
142
|
-
const totalTokenFiles = report.createdFiles.length + report.skippedFiles.length;
|
|
143
|
-
return { content: [{ type: "text", text: `Generated ${report.createdFiles.length} new token files, skipped ${report.skippedFiles.length} (already exist). Total: ${totalTokenFiles} token files on disk.` }] };
|
|
144
|
-
}
|
|
145
|
-
catch (e) {
|
|
146
|
-
return { content: [{ type: "text", text: `Token generation failed: ${e.message}` }] };
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
server.tool("export_context", "Export component context files to .ptb/context/ and update ptb.lock. Equivalent to 'ptb export context'. Run after a scan to make context available to AI tools.", async () => {
|
|
150
|
-
let ctx;
|
|
151
|
-
try {
|
|
152
|
-
ctx = await getContext();
|
|
153
104
|
}
|
|
154
|
-
|
|
155
|
-
|
|
105
|
+
if (stage === "generate") {
|
|
106
|
+
let ctx;
|
|
107
|
+
try {
|
|
108
|
+
ctx = await getContext();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
return { content: [{ type: "text", text: e.message }] };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const { snapshot, config } = ctx;
|
|
115
|
+
const managedSnapshot = getManagedSnapshot(snapshot, config);
|
|
116
|
+
const targets = name === "." || name === "*"
|
|
117
|
+
? managedSnapshot.components
|
|
118
|
+
: managedSnapshot.components.filter(c => c.name.toLowerCase() === name.toLowerCase() || toSlug(c.name) === toSlug(name));
|
|
119
|
+
if (targets.length === 0) {
|
|
120
|
+
return { content: [{ type: "text", text: `No component found matching '${name}'.` }] };
|
|
121
|
+
}
|
|
122
|
+
const generator = getGenerator(config.framework.name || "react");
|
|
123
|
+
const report = await generator.writeComponents(managedSnapshot, config);
|
|
124
|
+
await syncManifestWithReport(config, report);
|
|
125
|
+
await updateBridge(targets, config);
|
|
126
|
+
const totalFiles = report.createdFiles.length + report.skippedFiles.length;
|
|
127
|
+
const lines = [
|
|
128
|
+
`Generated ${report.createdFiles.length} new files, skipped ${report.skippedFiles.length} (already exist). Total: ${totalFiles} files across ${targets.length} components.`,
|
|
129
|
+
``,
|
|
130
|
+
`These are scaffold shells — prop signatures and CSS stubs, not full implementations.`,
|
|
131
|
+
`After you implement each component, call submit_work to record it as done:`,
|
|
132
|
+
` • submit_work({ name: "ComponentName", folder: "src/components/..." })`,
|
|
133
|
+
` • Bulk stamp (CLI only): ptb stamp component .`,
|
|
134
|
+
``,
|
|
135
|
+
`Until stamped, get_status will show them as not-stamped and ptb status --fail-on-stale will flag them.`,
|
|
136
|
+
];
|
|
137
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
return { content: [{ type: "text", text: `Generation failed: ${e.message}` }] };
|
|
141
|
+
}
|
|
156
142
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
143
|
+
if (stage === "tokens") {
|
|
144
|
+
let ctx;
|
|
145
|
+
try {
|
|
146
|
+
ctx = await getContext();
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
return { content: [{ type: "text", text: e.message }] };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const { snapshot, config } = ctx;
|
|
153
|
+
if (!snapshot.tokens?.length) {
|
|
154
|
+
return { content: [{ type: "text", text: "No token collections in snapshot. Run run({ stage:'scan' }) first." }] };
|
|
155
|
+
}
|
|
156
|
+
const generator = getGenerator(config.framework.name || "react");
|
|
157
|
+
const report = await generator.writeTokens(snapshot, config);
|
|
158
|
+
const totalTokenFiles = report.createdFiles.length + report.skippedFiles.length;
|
|
159
|
+
return { content: [{ type: "text", text: `Generated ${report.createdFiles.length} new token files, skipped ${report.skippedFiles.length} (already exist). Total: ${totalTokenFiles} token files on disk.` }] };
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
return { content: [{ type: "text", text: `Token generation failed: ${e.message}` }] };
|
|
163
|
+
}
|
|
162
164
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
if (stage === "context") {
|
|
166
|
+
let ctx;
|
|
167
|
+
try {
|
|
168
|
+
ctx = await getContext();
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
return { content: [{ type: "text", text: e.message }] };
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const { snapshot, config } = ctx;
|
|
175
|
+
const manifest = await loadManifest(config).catch(() => ({ components: {} }));
|
|
176
|
+
const paths = await exportAllComponentContexts(snapshot, config, manifest);
|
|
177
|
+
return { content: [{ type: "text", text: `Exported ${paths.length} context files to .ptb/context/.` }] };
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return { content: [{ type: "text", text: `Export failed: ${e.message}` }] };
|
|
181
|
+
}
|
|
165
182
|
}
|
|
166
|
-
|
|
167
|
-
server.tool("stamp_component", `Mark a component as implemented against the current design. Equivalent to 'ptb stamp component <name>'. Use '.' to stamp all components.
|
|
168
|
-
|
|
169
|
-
Optionally pass folder + artifacts to register the component's files in manifest.json — this is required for the ptb dev playground to pick up the component automatically. If ptb dev is running, the playground will hot-reload as soon as the manifest is updated.`, {
|
|
170
|
-
name: z.string().describe("Component name or '.' for all"),
|
|
171
|
-
message: z.string().optional().describe("Optional note about what was implemented"),
|
|
172
|
-
folder: z.string().optional().describe("Relative path from packageRoot to the component folder, e.g. 'src/components/web/btn-web'. Required for playground auto-registration."),
|
|
173
|
-
artifacts: z.array(z.string()).optional().describe("List of artifact paths relative to packageRoot, e.g. ['src/components/web/btn-web/BtnWeb.tsx', ...]. Defaults to [folder] if omitted."),
|
|
174
|
-
}, async ({ name, message, folder, artifacts }) => {
|
|
183
|
+
// stage === "prune"
|
|
175
184
|
let ctx;
|
|
176
185
|
try {
|
|
177
186
|
ctx = await getContext();
|
|
@@ -180,61 +189,19 @@ Optionally pass folder + artifacts to register the component's files in manifest
|
|
|
180
189
|
return { content: [{ type: "text", text: e.message }] };
|
|
181
190
|
}
|
|
182
191
|
try {
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
return { content: [{ type: "text", text: "No
|
|
186
|
-
}
|
|
187
|
-
const identity = getGitIdentity();
|
|
188
|
-
const now = new Date().toISOString();
|
|
189
|
-
const stamped = [];
|
|
190
|
-
const entries = name === "." || name === "*"
|
|
191
|
-
? Object.entries(lock.components)
|
|
192
|
-
: Object.entries(lock.components).filter(([slug, e]) => slug === toSlug(name) || e.displayName.toLowerCase() === name.toLowerCase());
|
|
193
|
-
if (entries.length === 0) {
|
|
194
|
-
return { content: [{ type: "text", text: `No component found in ptb.lock matching '${name}'.` }] };
|
|
195
|
-
}
|
|
196
|
-
for (const [slug, entry] of entries) {
|
|
197
|
-
// Build stampedContract from the live snapshot so diff is
|
|
198
|
-
// scan-timing-independent (same logic as CLI stampInLock).
|
|
199
|
-
const modelComponent = ctx.snapshot.components.find(c => c.name.toLowerCase() === entry.displayName.toLowerCase() ||
|
|
200
|
-
c.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") === slug);
|
|
201
|
-
const stampedContract = modelComponent ? {
|
|
202
|
-
...(modelComponent.variantAxes ? { variantAxes: modelComponent.variantAxes } : {}),
|
|
203
|
-
...(modelComponent.properties ? { properties: modelComponent.properties } : {}),
|
|
204
|
-
...(modelComponent.tokenBindings ? { tokenBindings: modelComponent.tokenBindings } : {}),
|
|
205
|
-
...(modelComponent.styleMetadata ? { styleMetadata: modelComponent.styleMetadata } : {}),
|
|
206
|
-
...(modelComponent.layout ? { layout: modelComponent.layout } : {}),
|
|
207
|
-
} : undefined;
|
|
208
|
-
lock.components[slug] = {
|
|
209
|
-
...entry,
|
|
210
|
-
stampedHash: entry.designHash,
|
|
211
|
-
stampedAt: now,
|
|
212
|
-
stampedBy: identity,
|
|
213
|
-
workStatus: "stamped",
|
|
214
|
-
...(message ? { stampedMessage: message } : {}),
|
|
215
|
-
...(stampedContract ? { stampedContract } : {}),
|
|
216
|
-
};
|
|
217
|
-
stamped.push(entry.displayName);
|
|
218
|
-
}
|
|
219
|
-
await saveLock(ctx.workspaceRoot, lock);
|
|
220
|
-
// If the caller provided file paths, register them in manifest.json.
|
|
221
|
-
// This triggers the ptb dev playground watcher so the component appears
|
|
222
|
-
// in the playground without a manual `ptb dev` restart.
|
|
223
|
-
const manifestNotes = [];
|
|
224
|
-
if (folder && entries.length === 1) {
|
|
225
|
-
const [, entry] = entries[0];
|
|
226
|
-
const comp = ctx.snapshot.components.find((c) => c.name === entry.displayName);
|
|
227
|
-
if (comp) {
|
|
228
|
-
const resolvedArtifacts = artifacts ?? [folder];
|
|
229
|
-
await updateComponentArtifacts(ctx.config, comp.id, comp.name, folder, resolvedArtifacts, ctx.config.framework?.name ?? "react");
|
|
230
|
-
manifestNotes.push(`Registered in manifest.json → folder: ${folder}`);
|
|
231
|
-
}
|
|
192
|
+
const result = await pruneStaleComponents(ctx.config, ctx.snapshot);
|
|
193
|
+
if (result.found === 0) {
|
|
194
|
+
return { content: [{ type: "text", text: "No stale artifacts to prune." }] };
|
|
232
195
|
}
|
|
233
|
-
|
|
234
|
-
|
|
196
|
+
return {
|
|
197
|
+
content: [{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: JSON.stringify({ found: result.found, deleted: result.deleted, skipped: result.skipped, errors: result.errors }, null, 2),
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
235
202
|
}
|
|
236
203
|
catch (e) {
|
|
237
|
-
return { content: [{ type: "text", text: `
|
|
204
|
+
return { content: [{ type: "text", text: `Prune failed: ${e.message}` }] };
|
|
238
205
|
}
|
|
239
206
|
});
|
|
240
207
|
server.tool("assign_component", "Assign a component to a developer. Stores assignment metadata (priority, due date, notes, acceptance criteria) in ptb.lock.", {
|
|
@@ -301,7 +268,7 @@ TOKEN AUTHORITY — what to trust, what to verify:
|
|
|
301
268
|
DIVISION OF LABOR WITH FIGMA MCP (composite workflow — most accurate):
|
|
302
269
|
PTB → token names, dependency graph, variant axes, state/content contracts (read contextFile)
|
|
303
270
|
Figma → exact hex values, spacing, visual arrangement (get_variable_defs, get_screenshot)
|
|
304
|
-
Map: Figma hex →
|
|
271
|
+
Map: Figma hex → get_token(hex) → --ptb-* cssVar name → emit both together
|
|
305
272
|
|
|
306
273
|
TYPICAL WORKFLOW:
|
|
307
274
|
1. Call start_work("<name>") — read blockedBy and tokenConflicts first
|
|
@@ -433,18 +400,24 @@ TYPICAL WORKFLOW:
|
|
|
433
400
|
return { content: [{ type: "text", text: `start_work failed: ${e.message}` }] };
|
|
434
401
|
}
|
|
435
402
|
});
|
|
436
|
-
server.tool("submit_work", `Submit completed implementation work for a component.
|
|
403
|
+
server.tool("submit_work", `Submit completed implementation work for a component. Use after writing all component files.
|
|
437
404
|
|
|
438
|
-
Stamps the component (records the current design hash
|
|
439
|
-
its work lifecycle.
|
|
405
|
+
Stamps the component (records the current design hash + design contract snapshot) and
|
|
406
|
+
transitions its work lifecycle. This is the canonical "I'm done" signal — always call
|
|
407
|
+
this after implement_component and writing the code.
|
|
440
408
|
|
|
441
409
|
transitionTo options:
|
|
442
410
|
- "stamped" (default): stamps immediately — for solo work or when you're the reviewer
|
|
443
|
-
- "in_review": marks as ready for review without stamping — for teams where a reviewer stamps later
|
|
411
|
+
- "in_review": marks as ready for review without stamping — for teams where a reviewer stamps later
|
|
412
|
+
|
|
413
|
+
folder + artifacts: register the component's files in manifest.json so the ptb dev
|
|
414
|
+
playground picks it up automatically (hot-reloads on save if ptb dev is running).`, {
|
|
444
415
|
name: z.string().describe("Component name to submit"),
|
|
445
|
-
transitionTo: z.enum(["stamped", "in_review"]).optional().describe("Target status
|
|
416
|
+
transitionTo: z.enum(["stamped", "in_review"]).optional().describe("Target status. Default: 'stamped'"),
|
|
446
417
|
message: z.string().optional().describe("Optional implementation note"),
|
|
447
|
-
|
|
418
|
+
folder: z.string().optional().describe("Relative path from packageRoot to the component folder, e.g. 'src/components/web/btn-web'. Required for playground auto-registration."),
|
|
419
|
+
artifacts: z.array(z.string()).optional().describe("Artifact paths relative to packageRoot, e.g. ['src/components/web/btn-web/BtnWeb.tsx']. Defaults to [folder] if omitted."),
|
|
420
|
+
}, async ({ name, transitionTo = "stamped", message, folder, artifacts }) => {
|
|
448
421
|
let ctx;
|
|
449
422
|
try {
|
|
450
423
|
ctx = await getContext();
|
|
@@ -455,7 +428,7 @@ transitionTo options:
|
|
|
455
428
|
try {
|
|
456
429
|
const lock = await loadLock(ctx.workspaceRoot);
|
|
457
430
|
if (!lock) {
|
|
458
|
-
return { content: [{ type: "text", text: "No ptb.lock found. Run
|
|
431
|
+
return { content: [{ type: "text", text: "No ptb.lock found. Run run({ stage:'context' }) first." }] };
|
|
459
432
|
}
|
|
460
433
|
const entries = Object.entries(lock.components).filter(([slug, e]) => slug === toSlug(name) || e.displayName.toLowerCase() === name.toLowerCase());
|
|
461
434
|
if (entries.length === 0) {
|
|
@@ -465,6 +438,15 @@ transitionTo options:
|
|
|
465
438
|
const now = new Date().toISOString();
|
|
466
439
|
for (const [slug, entry] of entries) {
|
|
467
440
|
if (transitionTo === "stamped") {
|
|
441
|
+
const modelComponent = ctx.snapshot.components.find(c => c.name.toLowerCase() === entry.displayName.toLowerCase() ||
|
|
442
|
+
c.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") === slug);
|
|
443
|
+
const stampedContract = modelComponent ? {
|
|
444
|
+
...(modelComponent.variantAxes ? { variantAxes: modelComponent.variantAxes } : {}),
|
|
445
|
+
...(modelComponent.properties ? { properties: modelComponent.properties } : {}),
|
|
446
|
+
...(modelComponent.tokenBindings ? { tokenBindings: modelComponent.tokenBindings } : {}),
|
|
447
|
+
...(modelComponent.styleMetadata ? { styleMetadata: modelComponent.styleMetadata } : {}),
|
|
448
|
+
...(modelComponent.layout ? { layout: modelComponent.layout } : {}),
|
|
449
|
+
} : undefined;
|
|
468
450
|
lock.components[slug] = {
|
|
469
451
|
...entry,
|
|
470
452
|
stampedHash: entry.designHash,
|
|
@@ -472,6 +454,7 @@ transitionTo options:
|
|
|
472
454
|
stampedBy: identity,
|
|
473
455
|
workStatus: "stamped",
|
|
474
456
|
...(message ? { stampedMessage: message } : {}),
|
|
457
|
+
...(stampedContract ? { stampedContract } : {}),
|
|
475
458
|
};
|
|
476
459
|
}
|
|
477
460
|
else {
|
|
@@ -483,11 +466,20 @@ transitionTo options:
|
|
|
483
466
|
}
|
|
484
467
|
}
|
|
485
468
|
await saveLock(ctx.workspaceRoot, lock);
|
|
469
|
+
// Register artifact paths in manifest.json for playground hot-reload
|
|
470
|
+
const manifestNotes = [];
|
|
471
|
+
if (folder && transitionTo === "stamped" && entries.length === 1) {
|
|
472
|
+
const [, entry] = entries[0];
|
|
473
|
+
const comp = ctx.snapshot.components.find(c => c.name === entry.displayName);
|
|
474
|
+
if (comp) {
|
|
475
|
+
await updateComponentArtifacts(ctx.config, comp.id, comp.name, folder, artifacts ?? [folder], ctx.config.framework?.name ?? "react");
|
|
476
|
+
manifestNotes.push(`Registered in manifest.json → folder: ${folder}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
486
479
|
const names = entries.map(([, e]) => e.displayName).join(", ");
|
|
487
|
-
const action = transitionTo === "stamped"
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return { content: [{ type: "text", text: `${action}: ${names}` }] };
|
|
480
|
+
const action = transitionTo === "stamped" ? `Stamped and marked as done` : `Submitted for review (not yet stamped)`;
|
|
481
|
+
const detail = manifestNotes.length > 0 ? `\n${manifestNotes.join("\n")}` : "";
|
|
482
|
+
return { content: [{ type: "text", text: `${action}: ${names}${detail}` }] };
|
|
491
483
|
}
|
|
492
484
|
catch (e) {
|
|
493
485
|
return { content: [{ type: "text", text: `submit_work failed: ${e.message}` }] };
|
|
@@ -509,8 +501,7 @@ TOKEN AUTHORITY — what to trust, what to verify:
|
|
|
509
501
|
|
|
510
502
|
WORKFLOW — follow this exactly:
|
|
511
503
|
1. Check unbuiltDependencies. If any exist, call implement_component for each one in buildOrder first.
|
|
512
|
-
- autoGeneratedIcons are handled by ptb automatically —
|
|
513
|
-
they are not tracked in ptb.lock.
|
|
504
|
+
- autoGeneratedIcons are handled by ptb automatically — they are not tracked in ptb.lock.
|
|
514
505
|
2. For each component in buildOrder (dependencies first, then the requested component):
|
|
515
506
|
a. Read contextFile for the full implementation brief (promptText, renderTree, variantDiffs).
|
|
516
507
|
b. Write the required files: <Name>.tsx, <Name>.types.ts, <Name>.module.css, index.ts
|
|
@@ -518,8 +509,12 @@ WORKFLOW — follow this exactly:
|
|
|
518
509
|
- Populate story args from contentSourceMap (username, title, etc.) using the base values
|
|
519
510
|
shown in the Content Contracts section of promptText.
|
|
520
511
|
- Include at least one populated story so the playground/Storybook preview matches the design.
|
|
521
|
-
d.
|
|
522
|
-
|
|
512
|
+
d. If Storybook is running, call run_verify now (before submit_work).
|
|
513
|
+
- Use the pixel match score and AI differences to correct visual drift.
|
|
514
|
+
- Target score ≥ 0.90. If score < 0.90 or assessment is "significant-differences", fix the CSS/TSX and re-verify.
|
|
515
|
+
- The scan thumbnail in thumbnailPath is the visual ground truth — do not invent chrome not shown there.
|
|
516
|
+
e. Call submit_work with the folder path so the playground updates and stampedContract is written.
|
|
517
|
+
3. Never skip submit_work — it records stampedContract and keeps ptb.lock in sync.
|
|
523
518
|
|
|
524
519
|
If a dependency also has its own unbuilt dependencies, implement_component will surface those
|
|
525
520
|
recursively when you call it for that dependency.`, { name: z.string().describe("Component name to implement, e.g. 'SearchBar' or 'BtnWeb'") }, async ({ name }) => {
|
|
@@ -552,7 +547,7 @@ recursively when you call it for that dependency.`, { name: z.string().describe(
|
|
|
552
547
|
depIds.delete(comp.id);
|
|
553
548
|
const depComponents = ctx.snapshot.components.filter((c) => depIds.has(c.id));
|
|
554
549
|
// Icons are auto-generated deterministically by ptb — they are NOT tracked
|
|
555
|
-
// in ptb.lock (writeLockFile skips them), so calling
|
|
550
|
+
// in ptb.lock (writeLockFile skips them), so calling submit_work on them
|
|
556
551
|
// will always fail with "not found". Split them out so the agent never tries.
|
|
557
552
|
const iconDeps = depComponents.filter((c) => isIconComponent(c.name));
|
|
558
553
|
const nonIconDeps = depComponents.filter((c) => !isIconComponent(c.name));
|
|
@@ -598,6 +593,7 @@ recursively when you call it for that dependency.`, { name: z.string().describe(
|
|
|
598
593
|
slug: toSlug(comp.name),
|
|
599
594
|
kind: comp.kind?.value ?? "unknown",
|
|
600
595
|
category: comp.categoryPath ?? "unclassified",
|
|
596
|
+
...(comp.thumbnailPath ? { thumbnailPath: comp.thumbnailPath } : {}),
|
|
601
597
|
},
|
|
602
598
|
contextFile: contextFilePath,
|
|
603
599
|
unbuiltDependencies,
|
|
@@ -639,30 +635,6 @@ recursively when you call it for that dependency.`, { name: z.string().describe(
|
|
|
639
635
|
return { content: [{ type: "text", text: `implement_component failed: ${e.message}` }] };
|
|
640
636
|
}
|
|
641
637
|
});
|
|
642
|
-
server.tool("run_prune", "Remove stale generated component folders that are no longer in the design system (present in manifest but deleted from Figma). Also cleans up ptb.lock entries. Equivalent to 'ptb prune'.", async () => {
|
|
643
|
-
let ctx;
|
|
644
|
-
try {
|
|
645
|
-
ctx = await getContext();
|
|
646
|
-
}
|
|
647
|
-
catch (e) {
|
|
648
|
-
return { content: [{ type: "text", text: e.message }] };
|
|
649
|
-
}
|
|
650
|
-
try {
|
|
651
|
-
const result = await pruneStaleComponents(ctx.config, ctx.snapshot);
|
|
652
|
-
if (result.found === 0) {
|
|
653
|
-
return { content: [{ type: "text", text: "No stale artifacts to prune." }] };
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
content: [{
|
|
657
|
-
type: "text",
|
|
658
|
-
text: JSON.stringify({ found: result.found, deleted: result.deleted, skipped: result.skipped, errors: result.errors }, null, 2),
|
|
659
|
-
}],
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
catch (e) {
|
|
663
|
-
return { content: [{ type: "text", text: `run_prune failed: ${e.message}` }] };
|
|
664
|
-
}
|
|
665
|
-
});
|
|
666
638
|
server.tool("document_component", "Generate MDX docs and/or Storybook stories for a component. Pass '.' to document all. The docs tool is read from ptb.config.json (docs.tool). Equivalent to 'ptb document component <name>'.", {
|
|
667
639
|
name: z.string().describe("Component name or '.' for all"),
|
|
668
640
|
format: z.enum(["mdx", "stories", "both"]).optional().describe("Output format. Default: mdx"),
|
|
@@ -726,7 +698,7 @@ recursively when you call it for that dependency.`, { name: z.string().describe(
|
|
|
726
698
|
const componentFile = path.join(baseOutputDir, `${codeName}.${ext}`);
|
|
727
699
|
const componentExists = await fs.access(componentFile).then(() => true).catch(() => false);
|
|
728
700
|
if (!componentExists) {
|
|
729
|
-
skipped.push(`${codeName}.stories.tsx (${codeName}.${ext} not found — run
|
|
701
|
+
skipped.push(`${codeName}.stories.tsx (${codeName}.${ext} not found — run run({ stage:'generate' }) first)`);
|
|
730
702
|
continue;
|
|
731
703
|
}
|
|
732
704
|
const storiesContent = generateComponentStories(context, codeName, fileKey);
|