@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.
- package/CHANGELOG.md +29 -4
- package/package.json +1 -1
- package/src/generators/preset/files/contracts/README.md +29 -0
- package/src/generators/preset/files/contracts/button.contract.ts.template +20 -0
- package/src/generators/preset/files/contracts/types.ts.template +55 -0
- package/src/generators/preset/files/figma/snapshot.json +164 -0
- package/src/generators/preset/files/storybook/angular/atl-button.stories.ts.template +38 -0
- package/src/generators/preset/files/storybook/angular/main.ts.template +39 -0
- package/src/generators/preset/files/storybook/angular/preview.ts.template +30 -0
- package/src/generators/preset/files/storybook/angular/tsconfig.json +16 -0
- package/src/generators/preset/files/storybook/angular/vitest.config.ts.template +40 -0
- package/src/generators/preset/files/storybook/angular/vitest.setup.ts.template +14 -0
- package/src/generators/preset/files/storybook/react/atl-button.stories.tsx +31 -0
- package/src/generators/preset/files/storybook/react/main.ts.template +37 -0
- package/src/generators/preset/files/storybook/react/preview.tsx +29 -0
- package/src/generators/preset/files/storybook/react/vitest.config.ts.template +32 -0
- package/src/generators/preset/files/storybook/react/vitest.setup.ts.template +8 -0
- package/src/generators/preset/files/storybook/vue/atl-button.stories.ts.template +34 -0
- package/src/generators/preset/files/storybook/vue/main.ts.template +38 -0
- package/src/generators/preset/files/storybook/vue/preview.ts.template +29 -0
- package/src/generators/preset/files/storybook/vue/vitest.config.ts.template +32 -0
- package/src/generators/preset/files/storybook/vue/vitest.setup.ts.template +9 -0
- package/src/generators/preset/files/styles/tokens.css +59 -43
- package/src/generators/preset/files/tools/scripts/check-contracts.mjs +1644 -0
- package/src/generators/preset/files/tools/scripts/figma-snapshot-contracts.mjs +370 -0
- package/src/generators/preset/files/tools/scripts/lib/docgen.mjs +573 -0
- package/src/generators/preset/files/tools/scripts/lib/ts-eval.js +126 -0
- package/src/generators/preset/files/tools/scripts/preflight.mjs +163 -31
- package/src/generators/preset/files/tools/stylelint-rules/index.js +21 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-primitive-token.js +362 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-raw-color-literal.js +154 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-token-bypass.js +497 -0
- package/src/generators/preset/files/tools/stylelint-rules/no-undeclared-token.js +122 -0
- package/src/generators/preset/files/tools/stylelint-rules/utils.js +71 -0
- package/src/generators/preset/preset.d.ts +1 -0
- package/src/generators/preset/preset.js +955 -5
- package/src/generators/preset/preset.js.map +1 -1
- package/src/generators/preset/schema.d.ts +1 -0
- 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 =
|
|
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())
|
|
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 {
|
|
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(
|
|
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(
|
|
149
|
-
|
|
150
|
-
|
|
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))
|
|
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)
|
|
170
|
-
|
|
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 ??
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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', {
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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 = [
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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`) +
|
|
662
|
+
green(`All hard checks passed`) +
|
|
663
|
+
dim(` · ${passes} ok, ${warnings} warning(s)`),
|
|
541
664
|
);
|
|
542
665
|
if (warnings > 0) {
|
|
543
|
-
console.log(
|
|
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`) +
|
|
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(
|
|
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
|
+
];
|