@atelier-ui/create-workspace 0.2.42 → 0.2.43

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +29 -4
  2. package/package.json +1 -1
  3. package/src/generators/preset/files/contracts/README.md +29 -0
  4. package/src/generators/preset/files/contracts/button.contract.ts.template +20 -0
  5. package/src/generators/preset/files/contracts/types.ts.template +55 -0
  6. package/src/generators/preset/files/figma/snapshot.json +164 -0
  7. package/src/generators/preset/files/storybook/angular/atl-button.stories.ts.template +38 -0
  8. package/src/generators/preset/files/storybook/angular/main.ts.template +39 -0
  9. package/src/generators/preset/files/storybook/angular/preview.ts.template +30 -0
  10. package/src/generators/preset/files/storybook/angular/tsconfig.json +16 -0
  11. package/src/generators/preset/files/storybook/angular/vitest.config.ts.template +40 -0
  12. package/src/generators/preset/files/storybook/angular/vitest.setup.ts.template +14 -0
  13. package/src/generators/preset/files/storybook/react/atl-button.stories.tsx +31 -0
  14. package/src/generators/preset/files/storybook/react/main.ts.template +37 -0
  15. package/src/generators/preset/files/storybook/react/preview.tsx +29 -0
  16. package/src/generators/preset/files/storybook/react/vitest.config.ts.template +32 -0
  17. package/src/generators/preset/files/storybook/react/vitest.setup.ts.template +8 -0
  18. package/src/generators/preset/files/storybook/vue/atl-button.stories.ts.template +34 -0
  19. package/src/generators/preset/files/storybook/vue/main.ts.template +38 -0
  20. package/src/generators/preset/files/storybook/vue/preview.ts.template +29 -0
  21. package/src/generators/preset/files/storybook/vue/vitest.config.ts.template +32 -0
  22. package/src/generators/preset/files/storybook/vue/vitest.setup.ts.template +9 -0
  23. package/src/generators/preset/files/styles/tokens.css +59 -43
  24. package/src/generators/preset/files/tools/scripts/check-contracts.mjs +1644 -0
  25. package/src/generators/preset/files/tools/scripts/figma-snapshot-contracts.mjs +370 -0
  26. package/src/generators/preset/files/tools/scripts/lib/docgen.mjs +573 -0
  27. package/src/generators/preset/files/tools/scripts/lib/ts-eval.js +126 -0
  28. package/src/generators/preset/files/tools/scripts/preflight.mjs +163 -31
  29. package/src/generators/preset/files/tools/stylelint-rules/index.js +21 -0
  30. package/src/generators/preset/files/tools/stylelint-rules/no-primitive-token.js +362 -0
  31. package/src/generators/preset/files/tools/stylelint-rules/no-raw-color-literal.js +154 -0
  32. package/src/generators/preset/files/tools/stylelint-rules/no-token-bypass.js +497 -0
  33. package/src/generators/preset/files/tools/stylelint-rules/no-undeclared-token.js +122 -0
  34. package/src/generators/preset/files/tools/stylelint-rules/utils.js +71 -0
  35. package/src/generators/preset/preset.d.ts +1 -0
  36. package/src/generators/preset/preset.js +955 -5
  37. package/src/generators/preset/preset.js.map +1 -1
  38. package/src/generators/preset/schema.d.ts +1 -0
  39. package/src/generators/preset/schema.json +5 -0
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { execSync, spawnSync } from 'node:child_process';
15
15
  import { createServer } from 'node:net';
16
- import { readFileSync, existsSync, statSync } from 'node:fs';
16
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
17
17
  import { homedir } from 'node:os';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { dirname, join, resolve } from 'node:path';
@@ -23,7 +23,8 @@ const __dirname = dirname(__filename);
23
23
  const ROOT = resolve(__dirname, '../..');
24
24
 
25
25
  // ── Styling ──────────────────────────────────────────────────────────
26
- const supportsColor = process.stdout.isTTY && process.env.NO_COLOR === undefined;
26
+ const supportsColor =
27
+ process.stdout.isTTY && process.env.NO_COLOR === undefined;
27
28
  const c = (code, s) => (supportsColor ? `\x1b[${code}m${s}\x1b[0m` : s);
28
29
  const red = (s) => c('31', s);
29
30
  const green = (s) => c('32', s);
@@ -54,7 +55,8 @@ const warn = (label, detail, fix) => record('warn', label, detail, fix);
54
55
  function which(cmd) {
55
56
  const finder = process.platform === 'win32' ? 'where' : 'which';
56
57
  const res = spawnSync(finder, [cmd], { encoding: 'utf8' });
57
- if (res.status === 0 && res.stdout.trim()) return res.stdout.trim().split('\n')[0];
58
+ if (res.status === 0 && res.stdout.trim())
59
+ return res.stdout.trim().split('\n')[0];
58
60
  return null;
59
61
  }
60
62
 
@@ -101,7 +103,10 @@ async function mcpPost(url, body, sessionId, timeoutMs = 10000) {
101
103
  sessionId: res.headers.get('mcp-session-id') ?? undefined,
102
104
  };
103
105
  } catch (err) {
104
- return { error: err?.name === 'AbortError' ? 'timeout' : String(err?.message ?? err) };
106
+ return {
107
+ error:
108
+ err?.name === 'AbortError' ? 'timeout' : String(err?.message ?? err),
109
+ };
105
110
  } finally {
106
111
  clearTimeout(timer);
107
112
  }
@@ -141,16 +146,28 @@ async function probeMcp(url) {
141
146
  if (init.error) return { level: 'unreachable', detail: init.error };
142
147
  const session = init.sessionId;
143
148
  if (session) {
144
- await mcpPost(url, { jsonrpc: '2.0', method: 'notifications/initialized' }, session, 5000);
149
+ await mcpPost(
150
+ url,
151
+ { jsonrpc: '2.0', method: 'notifications/initialized' },
152
+ session,
153
+ 5000,
154
+ );
145
155
  }
146
156
 
147
157
  // 2. tools/list — must be HTTP 200 with a parseable JSON-RPC tools array.
148
- const list = await mcpPost(url, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }, session);
149
- if (list.error) return { level: 'broken', detail: `tools/list failed (${list.error})` };
150
- if (list.status !== 200) return { level: 'broken', detail: `tools/list HTTP ${list.status}` };
158
+ const list = await mcpPost(
159
+ url,
160
+ { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} },
161
+ session,
162
+ );
163
+ if (list.error)
164
+ return { level: 'broken', detail: `tools/list failed (${list.error})` };
165
+ if (list.status !== 200)
166
+ return { level: 'broken', detail: `tools/list → HTTP ${list.status}` };
151
167
  const listMsg = parseJsonRpc(list.contentType, list.text, 2);
152
168
  const tools = listMsg?.result?.tools;
153
- if (!Array.isArray(tools)) return { level: 'broken', detail: 'tools/list → no parseable result' };
169
+ if (!Array.isArray(tools))
170
+ return { level: 'broken', detail: 'tools/list → no parseable result' };
154
171
 
155
172
  // 3. On an Atelier Storybook MCP, exercise one real tool call: manifests
156
173
  // are only fetched inside tool calls, and a broken manifest fetch comes
@@ -166,14 +183,21 @@ async function probeMcp(url) {
166
183
  },
167
184
  session,
168
185
  );
169
- if (call.error) return { level: 'broken', detail: `docs-list failed (${call.error})` };
170
- if (call.status !== 200) return { level: 'broken', detail: `docs-list HTTP ${call.status}` };
186
+ if (call.error)
187
+ return { level: 'broken', detail: `docs-list failed (${call.error})` };
188
+ if (call.status !== 200)
189
+ return { level: 'broken', detail: `docs-list → HTTP ${call.status}` };
171
190
  const callMsg = parseJsonRpc(call.contentType, call.text, 3);
172
191
  if (!callMsg?.result || callMsg.result.isError) {
173
192
  const reason =
174
- callMsg?.result?.content?.[0]?.text ?? callMsg?.error?.message ?? 'no parseable result';
193
+ callMsg?.result?.content?.[0]?.text ??
194
+ callMsg?.error?.message ??
195
+ 'no parseable result';
175
196
  const oneLine = String(reason).replace(/\s+/g, ' ').trim();
176
- return { level: 'broken', detail: `docs-list → ${oneLine.slice(0, 120)}` };
197
+ return {
198
+ level: 'broken',
199
+ detail: `docs-list → ${oneLine.slice(0, 120)}`,
200
+ };
177
201
  }
178
202
  }
179
203
 
@@ -249,7 +273,11 @@ function checkNpm() {
249
273
  const required = { major: 10, minor: 0, patch: 0 };
250
274
  const path = which('npm');
251
275
  if (!path) {
252
- fail('npm', 'not found on PATH', 'npm ships with Node.js — reinstall Node if missing');
276
+ fail(
277
+ 'npm',
278
+ 'not found on PATH',
279
+ 'npm ships with Node.js — reinstall Node if missing',
280
+ );
253
281
  return;
254
282
  }
255
283
  try {
@@ -258,7 +286,11 @@ function checkNpm() {
258
286
  if (isAtLeast(actual, required)) {
259
287
  ok('npm', `v${version}`);
260
288
  } else {
261
- warn('npm', `found v${version}, recommend >= 10.0.0`, 'Run `npm install -g npm@latest`');
289
+ warn(
290
+ 'npm',
291
+ `found v${version}, recommend >= 10.0.0`,
292
+ 'Run `npm install -g npm@latest`',
293
+ );
262
294
  }
263
295
  } catch (err) {
264
296
  fail('npm', String(err?.message ?? err), 'Reinstall Node.js');
@@ -268,14 +300,22 @@ function checkNpm() {
268
300
  function checkGit() {
269
301
  const path = which('git');
270
302
  if (!path) {
271
- fail('git', 'not found on PATH', 'Install from https://git-scm.com or `brew install git`');
303
+ fail(
304
+ 'git',
305
+ 'not found on PATH',
306
+ 'Install from https://git-scm.com or `brew install git`',
307
+ );
272
308
  return;
273
309
  }
274
310
  try {
275
311
  const version = execSync('git --version', { encoding: 'utf8' }).trim();
276
312
  ok('git', version);
277
313
  } catch {
278
- warn('git', 'installed but version check failed', 'Verify install with `git --version`');
314
+ warn(
315
+ 'git',
316
+ 'installed but version check failed',
317
+ 'Verify install with `git --version`',
318
+ );
279
319
  }
280
320
  }
281
321
 
@@ -290,18 +330,32 @@ function checkClaudeCli() {
290
330
  return;
291
331
  }
292
332
  try {
293
- const version = execSync('claude --version', { encoding: 'utf8', timeout: 5000 }).trim();
333
+ const version = execSync('claude --version', {
334
+ encoding: 'utf8',
335
+ timeout: 5000,
336
+ }).trim();
294
337
  ok('Claude Code CLI', version);
295
338
  } catch {
296
- warn('Claude Code CLI', 'installed but `claude --version` failed', 'Try `claude doctor`');
339
+ warn(
340
+ 'Claude Code CLI',
341
+ 'installed but `claude --version` failed',
342
+ 'Try `claude doctor`',
343
+ );
297
344
  }
298
345
  }
299
346
 
300
347
  async function checkFigmaSetup() {
301
348
  // 1. Desktop Bridge plugin manifest — the primary channel.
302
- const manifestPath = join(homedir(), '.figma-console-mcp', 'plugin', 'manifest.json');
349
+ const manifestPath = join(
350
+ homedir(),
351
+ '.figma-console-mcp',
352
+ 'plugin',
353
+ 'manifest.json',
354
+ );
303
355
  const pinnedVersion = getPinnedFigmaConsoleVersion();
304
- const pinnedSpec = pinnedVersion ? `figma-console-mcp@${pinnedVersion}` : 'figma-console-mcp@latest';
356
+ const pinnedSpec = pinnedVersion
357
+ ? `figma-console-mcp@${pinnedVersion}`
358
+ : 'figma-console-mcp@latest';
305
359
  if (existsSync(manifestPath)) {
306
360
  ok('Figma Desktop Bridge plugin', `manifest at ${manifestPath}`);
307
361
  } else {
@@ -322,7 +376,12 @@ async function checkFigmaSetup() {
322
376
  // — treating it as fatal would fail preflight over something that fixes
323
377
  // itself the moment Claude Code (re)starts the MCP.
324
378
  if (existsSync(manifestPath)) {
325
- const versionPath = join(homedir(), '.figma-console-mcp', 'plugin', '.version');
379
+ const versionPath = join(
380
+ homedir(),
381
+ '.figma-console-mcp',
382
+ 'plugin',
383
+ '.version',
384
+ );
326
385
  if (!existsSync(versionPath)) {
327
386
  warn(
328
387
  'Plugin files .version marker',
@@ -344,15 +403,23 @@ async function checkFigmaSetup() {
344
403
  }
345
404
 
346
405
  // 2. Bridge WebSocket port range — at least one port must be usable.
347
- const bridgePorts = [9223, 9224, 9225, 9226, 9227, 9228, 9229, 9230, 9231, 9232];
406
+ const bridgePorts = [
407
+ 9223, 9224, 9225, 9226, 9227, 9228, 9229, 9230, 9231, 9232,
408
+ ];
348
409
  let freeCount = 0;
349
410
  for (const p of bridgePorts) {
350
411
  if (await portFree(p)) freeCount += 1;
351
412
  }
352
413
  if (freeCount === bridgePorts.length) {
353
- ok('Bridge port range 9223–9232', 'all free (MCP will bind on first launch)');
414
+ ok(
415
+ 'Bridge port range 9223–9232',
416
+ 'all free (MCP will bind on first launch)',
417
+ );
354
418
  } else if (freeCount > 0) {
355
- ok('Bridge port range 9223–9232', `${freeCount}/10 free (bridge connection OK)`);
419
+ ok(
420
+ 'Bridge port range 9223–9232',
421
+ `${freeCount}/10 free (bridge connection OK)`,
422
+ );
356
423
  } else {
357
424
  warn(
358
425
  'Bridge port range 9223–9232',
@@ -461,7 +528,8 @@ function isDir(path) {
461
528
  }
462
529
 
463
530
  function detectEnvironment() {
464
- const isClone = isDir(resolve(ROOT, 'libs/spec')) && isDir(resolve(ROOT, 'plan/adr'));
531
+ const isClone =
532
+ isDir(resolve(ROOT, 'libs/spec')) && isDir(resolve(ROOT, 'plan/adr'));
465
533
  return isClone ? 'clone' : 'scaffold';
466
534
  }
467
535
 
@@ -485,6 +553,50 @@ const PORTS_BY_ENV = {
485
553
  ],
486
554
  };
487
555
 
556
+ // ── Playwright Chromium (scaffold only) ─────────────────────────────
557
+ // The scaffold's `npm run check:stories` runs every story headless in
558
+ // Chromium via @storybook/addon-vitest + @vitest/browser-playwright — a
559
+ // browser Playwright manages itself, downloaded by `npx playwright install
560
+ // chromium` (never as a postinstall hook: a workshop attendee's first
561
+ // `npm install` should not silently spend minutes downloading a browser).
562
+ // This is a best-effort, dependency-free heuristic (no `require('playwright')`
563
+ // here — this script has zero non-builtin imports and should keep working
564
+ // even before `npm install` has finished) — it looks for a `chromium-*`
565
+ // directory in Playwright's own browser cache, respecting
566
+ // `PLAYWRIGHT_BROWSERS_PATH` the same way Playwright itself does. A warning,
567
+ // not a hard failure: this only blocks `check:stories`, not the workshop.
568
+ function playwrightBrowsersDir() {
569
+ if (process.env.PLAYWRIGHT_BROWSERS_PATH)
570
+ return process.env.PLAYWRIGHT_BROWSERS_PATH;
571
+ const home = homedir();
572
+ if (process.platform === 'darwin')
573
+ return join(home, 'Library', 'Caches', 'ms-playwright');
574
+ if (process.platform === 'win32')
575
+ return join(home, 'AppData', 'Local', 'ms-playwright');
576
+ return join(home, '.cache', 'ms-playwright');
577
+ }
578
+
579
+ function checkPlaywrightChromium() {
580
+ const dir = playwrightBrowsersDir();
581
+ let hasChromium = false;
582
+ try {
583
+ hasChromium =
584
+ existsSync(dir) &&
585
+ readdirSync(dir).some((name) => name.startsWith('chromium'));
586
+ } catch {
587
+ hasChromium = false;
588
+ }
589
+ if (hasChromium) {
590
+ ok('Playwright Chromium', 'installed');
591
+ } else {
592
+ warn(
593
+ 'Playwright Chromium',
594
+ 'not found',
595
+ 'Run `npx playwright install chromium` — needed for `npm run check:stories`',
596
+ );
597
+ }
598
+ }
599
+
488
600
  async function checkPorts(env) {
489
601
  ok(
490
602
  'Environment',
@@ -495,7 +607,12 @@ async function checkPorts(env) {
495
607
  for (const { port, label } of PORTS_BY_ENV[env]) {
496
608
  const free = await portFree(port);
497
609
  if (free) ok(`Port ${port} (${label})`, 'free');
498
- else warn(`Port ${port} (${label})`, 'in use', `Run \`lsof -ti :${port} | xargs kill\` (macOS/Linux)`);
610
+ else
611
+ warn(
612
+ `Port ${port} (${label})`,
613
+ 'in use',
614
+ `Run \`lsof -ti :${port} | xargs kill\` (macOS/Linux)`,
615
+ );
499
616
  }
500
617
  }
501
618
 
@@ -529,6 +646,11 @@ async function main() {
529
646
  header('Local ports');
530
647
  await checkPorts(env);
531
648
 
649
+ if (env === 'scaffold') {
650
+ header('Storybook browser tests');
651
+ checkPlaywrightChromium();
652
+ }
653
+
532
654
  // Summary
533
655
  const failures = results.filter((r) => r.level === 'fail').length;
534
656
  const warnings = results.filter((r) => r.level === 'warn').length;
@@ -537,19 +659,29 @@ async function main() {
537
659
  console.log('');
538
660
  if (failures === 0) {
539
661
  console.log(
540
- green(`All hard checks passed`) + dim(` · ${passes} ok, ${warnings} warning(s)`),
662
+ green(`All hard checks passed`) +
663
+ dim(` · ${passes} ok, ${warnings} warning(s)`),
541
664
  );
542
665
  if (warnings > 0) {
543
- console.log(dim('Warnings are non-blocking — see https://atelier.pieper.io/troubleshooting'));
666
+ console.log(
667
+ dim(
668
+ 'Warnings are non-blocking — see https://atelier.pieper.io/troubleshooting',
669
+ ),
670
+ );
544
671
  }
545
672
  console.log('');
546
673
  process.exit(0);
547
674
  } else {
548
675
  console.log(
549
- red(`${failures} check(s) failed`) + dim(` · ${passes} ok, ${warnings} warning(s)`),
676
+ red(`${failures} check(s) failed`) +
677
+ dim(` · ${passes} ok, ${warnings} warning(s)`),
550
678
  );
551
679
  console.log(dim('Fix the items above, then re-run `npm run preflight`.'));
552
- console.log(dim('Full troubleshooting guide: https://atelier.pieper.io/troubleshooting'));
680
+ console.log(
681
+ dim(
682
+ 'Full troubleshooting guide: https://atelier.pieper.io/troubleshooting',
683
+ ),
684
+ );
553
685
  console.log('');
554
686
  process.exit(1);
555
687
  }
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Local stylelint plugin for this repo's own CSS-discipline invariants —
5
+ * the stylelint counterpart of `tools/eslint-rules/`. A stylelint plugin
6
+ * module may export a single rule definition or an array of them (stylelint
7
+ * flattens either shape when loading `plugins`); this file exports the
8
+ * array so `stylelint.config.mjs` only needs one entry in `plugins` to pick
9
+ * up every rule below.
10
+ *
11
+ * Each rule file calls `stylelint.createPlugin(ruleName, rule)` itself and
12
+ * is `require()`-able on its own — this index just collects them, the same
13
+ * relationship `tools/eslint-rules/index.js` has to its own rule files.
14
+ */
15
+
16
+ module.exports = [
17
+ require('./no-raw-color-literal'),
18
+ require('./no-undeclared-token'),
19
+ require('./no-primitive-token'),
20
+ require('./no-token-bypass'),
21
+ ];