feat(live): v2 sidecar upgrade + preserve per-project config on build

Unify the design-system panel's data shape around DESIGN.md frontmatter
as the primary source of truth; the sidecar carries only what Stitch's
frontmatter schema can't (extensions + live component HTML + narrative).
Also fix a long-standing build bug that destroyed per-project config.

Shape changes:
- Server /design-system.json now returns { parsed, sidecar, hasMd,
  hasSidecar, mdNewerThanJson, parseError?, sidecarError? }. No more
  mode switching; both layers ship when present and the panel merges.
- Panel consolidates renderSidecarVisual + renderParsedMdVisual into a
  single renderDesignVisual that merges frontmatter primitives with
  sidecar extensions.colorMeta / typographyMeta. Helpers for color,
  typography, radii model-building. Parsed-md narrative synthesis
  survives as a fallback when no sidecar.
- DESIGN.json rewritten at schemaVersion 2: extensions.{colorMeta,
  typographyMeta, shadows, motion, breakpoints} + components (with
  refersTo pointing back to frontmatter component keys) + narrative.
  Token primitives no longer duplicated in the sidecar.

Build fix:
- scripts/build.js:634 wiped .claude/skills/ (and every other harness
  dir) on each rebuild, then recopied from dist. After commit b0feed0
  unbundled per-project config.json from dist, the sync destroyed the
  user's live-mode config on every build without replacing it.
- Added stashPerProjectArtifacts / restorePerProjectArtifacts in
  scripts/lib/utils.js. Hoisted PER_PROJECT_SCRIPT_ARTIFACTS to a
  module-level export so build.js and readSourceFiles share one
  source of truth. Build now preserves config.json across the sync.

Verified in browser: panel renders 10 colors, 9 typography roles, 3
shadows, 6 grouped components, 9 rules, 25 do/don't items, all merged
correctly from frontmatter + v2 sidecar with zero console errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-23 12:09:55 -07:00
co-authored by Claude Opus 4.7
parent a5cd7bf859
commit c160ffc38d
27 changed files with 2063 additions and 1170 deletions
+6 -1
View File
@@ -18,7 +18,7 @@
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { readSourceFiles, readPatterns } from './lib/utils.js';
import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProjectArtifacts } from './lib/utils.js';
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
import { createAllZips } from './lib/zip.js';
import { generateSubPages } from './build-sub-pages.js';
@@ -631,8 +631,13 @@ async function build() {
const skillsDest = path.join(ROOT_DIR, configDir, 'skills');
if (fs.existsSync(skillsSrc)) {
// Preserve per-project script artifacts (e.g. live-mode config.json)
// across the rm + recopy. The build intentionally doesn't ship them,
// so without this the sync destroys local state on every rebuild.
const stashed = stashPerProjectArtifacts(skillsDest);
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
copyDirSync(skillsSrc, skillsDest);
restorePerProjectArtifacts(skillsDest, stashed);
}
}
+43 -10
View File
@@ -1,6 +1,45 @@
import fs from 'fs';
import path from 'path';
// Per-project artifacts live inside `scripts/` of an installed skill but
// belong to the consuming project, not the distributable skill. The build
// excludes them from dist, and the harness-sync step preserves them across
// the rm+recopy so local state isn't destroyed on every rebuild.
// - config.json: live-mode inject target list for the current project.
// Written by the agent at first /impeccable live; tied to the project's
// filesystem layout. Losing it resets the user's glob + exclusions.
export const PER_PROJECT_SCRIPT_ARTIFACTS = new Set(['config.json']);
// Walk the harness-dir skill tree and return any per-project script
// artifacts found, ready for restoration after a full sync rm+recopy.
// Returns [{ relPath, content: Buffer }], where relPath is relative to
// the passed-in rootDir (typically `<configDir>/skills`).
export function stashPerProjectArtifacts(rootDir) {
if (!fs.existsSync(rootDir)) return [];
const out = [];
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) { walk(p); continue; }
// Only preserve files inside a skill's scripts/ directory.
if (path.basename(path.dirname(p)) !== 'scripts') continue;
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(entry.name)) {
out.push({ relPath: path.relative(rootDir, p), content: fs.readFileSync(p) });
}
}
};
walk(rootDir);
return out;
}
export function restorePerProjectArtifacts(rootDir, stashed) {
for (const { relPath, content } of stashed) {
const target = path.join(rootDir, relPath);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, content);
}
}
/**
* Parse frontmatter from markdown content
* Returns { frontmatter: object, body: string }
@@ -147,20 +186,14 @@ export function readSourceFiles(rootDir) {
}
}
// Read script files if they exist.
//
// Per-project artifacts (state files that belong to the consuming
// project, not the distributable skill) must be excluded here so
// the build never bundles them into the skill that ships to users.
// - config.json: the live-mode inject-target list for the current
// project. Written by the agent at first /impeccable live; tied
// to the project's filesystem layout.
const PER_PROJECT_ARTIFACTS = new Set(['config.json']);
// Read script files if they exist. PER_PROJECT_SCRIPT_ARTIFACTS
// (defined at module top) are excluded from the distributable skill
// so the build never bundles one project's state into another's.
const scripts = [];
const scriptsDir = path.join(entryPath, 'scripts');
if (fs.existsSync(scriptsDir)) {
const scriptFiles = fs.readdirSync(scriptsDir).filter(f => {
if (PER_PROJECT_ARTIFACTS.has(f)) return false;
if (PER_PROJECT_SCRIPT_ARTIFACTS.has(f)) return false;
return fs.statSync(path.join(scriptsDir, f)).isFile();
});
for (const scriptFile of scriptFiles) {