@awarebydefault/display-case 1.0.0
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/LICENSE +21 -0
- package/README.md +309 -0
- package/display-case.prompt.md +64 -0
- package/docs/ai-agents.md +126 -0
- package/docs/cli.md +99 -0
- package/docs/configuration.md +410 -0
- package/docs/documentation-panel.md +50 -0
- package/docs/examples/README.md +14 -0
- package/docs/examples/multi-variant.case.tsx +30 -0
- package/docs/examples/plain.case.tsx +22 -0
- package/docs/examples/tweak-control.placard.md +80 -0
- package/docs/examples/tweaks.case.tsx +39 -0
- package/docs/hierarchy.md +59 -0
- package/docs/quick-start.md +78 -0
- package/docs/style-engines.md +180 -0
- package/docs/testing.md +245 -0
- package/docs/theming.md +97 -0
- package/docs/tweaks.md +75 -0
- package/docs/writing-cases.md +144 -0
- package/docs/writing-placard-docs.md +194 -0
- package/package.json +113 -0
- package/skills/display-case-author-case/README.md +20 -0
- package/skills/display-case-author-case/SKILL.md +40 -0
- package/skills/display-case-author-placard-doc/README.md +24 -0
- package/skills/display-case-author-placard-doc/SKILL.md +65 -0
- package/skills/display-case-review/README.md +19 -0
- package/skills/display-case-review/SKILL.md +30 -0
- package/skills/display-case-snapshot/README.md +20 -0
- package/skills/display-case-snapshot/SKILL.md +29 -0
- package/src/checks/a11y-scanner.test.ts +240 -0
- package/src/checks/a11y-scanner.ts +410 -0
- package/src/checks/check-text.test.ts +53 -0
- package/src/checks/check-text.ts +78 -0
- package/src/checks/check.test.ts +194 -0
- package/src/checks/check.ts +473 -0
- package/src/checks/providers/pixelmatch-diff.test.ts +79 -0
- package/src/checks/providers/pixelmatch-diff.ts +30 -0
- package/src/checks/providers/playwright-driver.ts +104 -0
- package/src/checks/ssr-check.test.ts +73 -0
- package/src/checks/ssr-check.ts +96 -0
- package/src/checks/structure-check.cross-package.test.ts +165 -0
- package/src/checks/structure-check.test.ts +651 -0
- package/src/checks/structure-check.ts +988 -0
- package/src/checks/tokens-check.test.ts +159 -0
- package/src/checks/tokens-check.ts +162 -0
- package/src/cli.ts +218 -0
- package/src/commands/agents.test.ts +24 -0
- package/src/commands/agents.ts +28 -0
- package/src/commands/init-run.test.ts +123 -0
- package/src/commands/init.test.ts +63 -0
- package/src/commands/init.ts +412 -0
- package/src/commands/publish.test.ts +210 -0
- package/src/commands/publish.ts +292 -0
- package/src/core/affected.test.ts +99 -0
- package/src/core/affected.ts +144 -0
- package/src/core/catalog.test.ts +152 -0
- package/src/core/catalog.ts +92 -0
- package/src/core/discovery.test.ts +184 -0
- package/src/core/discovery.ts +250 -0
- package/src/core/manifest.ts +41 -0
- package/src/core/mdx-lite/__fixtures__/box-stub.tsx +7 -0
- package/src/core/mdx-lite/index.ts +393 -0
- package/src/core/mdx-lite/mdx-lite.test.ts +345 -0
- package/src/core/mdx-plugin.test.ts +60 -0
- package/src/core/mdx-plugin.ts +30 -0
- package/src/flow-step.test-d.ts +39 -0
- package/src/index.test.ts +100 -0
- package/src/index.ts +564 -0
- package/src/render/collect-styles.emotion.test.tsx +114 -0
- package/src/render/collect-styles.test.tsx +72 -0
- package/src/render/collect-styles.ts +33 -0
- package/src/render/documents.test.ts +184 -0
- package/src/render/documents.ts +88 -0
- package/src/render/render-node.test.tsx +160 -0
- package/src/render/render-node.tsx +133 -0
- package/src/render/ssr-primer.test.tsx +25 -0
- package/src/render/ssr-primer.tsx +54 -0
- package/src/render/ssr-render.test.tsx +142 -0
- package/src/render/ssr-render.tsx +63 -0
- package/src/render/ssr-shell.test.tsx +57 -0
- package/src/render/ssr-shell.tsx +54 -0
- package/src/server/prod-server.ts +237 -0
- package/src/server/server.test.ts +117 -0
- package/src/server/server.ts +1039 -0
- package/src/style-engine.test-d.ts +37 -0
- package/src/testing/test-helpers.ts +27 -0
- package/src/types/pixelmatch.d.ts +12 -0
- package/src/ui/browser-entry.tsx +51 -0
- package/src/ui/chrome.css +485 -0
- package/src/ui/design-system/README.md +88 -0
- package/src/ui/design-system/components/controls/Button.case.tsx +52 -0
- package/src/ui/design-system/components/controls/Button.css +89 -0
- package/src/ui/design-system/components/controls/Button.placard.md +14 -0
- package/src/ui/design-system/components/controls/Button.test.tsx +45 -0
- package/src/ui/design-system/components/controls/Button.tsx +41 -0
- package/src/ui/design-system/components/controls/IconButton.case.tsx +52 -0
- package/src/ui/design-system/components/controls/IconButton.css +67 -0
- package/src/ui/design-system/components/controls/IconButton.placard.md +13 -0
- package/src/ui/design-system/components/controls/IconButton.test.tsx +39 -0
- package/src/ui/design-system/components/controls/IconButton.tsx +47 -0
- package/src/ui/design-system/components/controls/Input.case.tsx +50 -0
- package/src/ui/design-system/components/controls/Input.css +52 -0
- package/src/ui/design-system/components/controls/Input.placard.md +12 -0
- package/src/ui/design-system/components/controls/Input.test.tsx +43 -0
- package/src/ui/design-system/components/controls/Input.tsx +45 -0
- package/src/ui/design-system/components/controls/Select.case.tsx +48 -0
- package/src/ui/design-system/components/controls/Select.css +44 -0
- package/src/ui/design-system/components/controls/Select.placard.md +15 -0
- package/src/ui/design-system/components/controls/Select.test.tsx +57 -0
- package/src/ui/design-system/components/controls/Select.tsx +58 -0
- package/src/ui/design-system/components/controls/SelectMenu.case.tsx +100 -0
- package/src/ui/design-system/components/controls/SelectMenu.css +72 -0
- package/src/ui/design-system/components/controls/SelectMenu.placard.md +18 -0
- package/src/ui/design-system/components/controls/SelectMenu.test.tsx +66 -0
- package/src/ui/design-system/components/controls/SelectMenu.tsx +377 -0
- package/src/ui/design-system/components/index.ts +66 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.case.tsx +44 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/ColorRamp.tsx +51 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.case.tsx +38 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/DefinitionList.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.case.tsx +24 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/FontFamilies.tsx +41 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.case.tsx +27 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.placard.md +13 -0
- package/src/ui/design-system/components/primer-specimen/GlyphGrid.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.case.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.placard.md +7 -0
- package/src/ui/design-system/components/primer-specimen/LayoutMock.tsx +36 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.case.tsx +20 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.placard.md +12 -0
- package/src/ui/design-system/components/primer-specimen/SpacingScale.tsx +33 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.case.tsx +56 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.placard.md +17 -0
- package/src/ui/design-system/components/primer-specimen/SpecimenBoxRow.tsx +45 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.case.tsx +17 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.placard.md +16 -0
- package/src/ui/design-system/components/primer-specimen/StatusList.tsx +39 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.case.tsx +26 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/SwatchGrid.tsx +42 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.case.tsx +23 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.placard.md +14 -0
- package/src/ui/design-system/components/primer-specimen/TypeScale.tsx +34 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.case.tsx +28 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.placard.md +15 -0
- package/src/ui/design-system/components/primer-specimen/WeightSpecimen.tsx +46 -0
- package/src/ui/design-system/components/primer-specimen/index.ts +31 -0
- package/src/ui/design-system/components/primer-specimen/styles.css +476 -0
- package/src/ui/design-system/components/shell/A11yPage.case.tsx +237 -0
- package/src/ui/design-system/components/shell/A11yPage.placard.md +15 -0
- package/src/ui/design-system/components/shell/CaseTemplate.case.tsx +32 -0
- package/src/ui/design-system/components/shell/CaseTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/CasesPage.case.tsx +141 -0
- package/src/ui/design-system/components/shell/CasesPage.placard.md +12 -0
- package/src/ui/design-system/components/shell/PrimerPage.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerPage.placard.md +3 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.case.tsx +22 -0
- package/src/ui/design-system/components/shell/PrimerTemplate.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.case.tsx +57 -0
- package/src/ui/design-system/components/shell/ShellView.placard.md +5 -0
- package/src/ui/design-system/components/shell/ShellView.tsx +678 -0
- package/src/ui/design-system/components/shell/shell-fixtures.tsx +727 -0
- package/src/ui/design-system/components/showcase/A11yBadge.case.tsx +46 -0
- package/src/ui/design-system/components/showcase/A11yBadge.css +27 -0
- package/src/ui/design-system/components/showcase/A11yBadge.placard.md +11 -0
- package/src/ui/design-system/components/showcase/A11yBadge.test.tsx +31 -0
- package/src/ui/design-system/components/showcase/A11yBadge.tsx +41 -0
- package/src/ui/design-system/components/showcase/A11yPanel.case.tsx +121 -0
- package/src/ui/design-system/components/showcase/A11yPanel.css +198 -0
- package/src/ui/design-system/components/showcase/A11yPanel.placard.md +19 -0
- package/src/ui/design-system/components/showcase/A11yPanel.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/A11yPanel.tsx +144 -0
- package/src/ui/design-system/components/showcase/Chip.case.tsx +48 -0
- package/src/ui/design-system/components/showcase/Chip.css +51 -0
- package/src/ui/design-system/components/showcase/Chip.placard.md +13 -0
- package/src/ui/design-system/components/showcase/Chip.test.tsx +46 -0
- package/src/ui/design-system/components/showcase/Chip.tsx +54 -0
- package/src/ui/design-system/components/showcase/Eyebrow.case.tsx +30 -0
- package/src/ui/design-system/components/showcase/Eyebrow.css +16 -0
- package/src/ui/design-system/components/showcase/Eyebrow.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Eyebrow.test.tsx +38 -0
- package/src/ui/design-system/components/showcase/Eyebrow.tsx +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.case.tsx +35 -0
- package/src/ui/design-system/components/showcase/FlowNav.css +29 -0
- package/src/ui/design-system/components/showcase/FlowNav.placard.md +13 -0
- package/src/ui/design-system/components/showcase/FlowNav.test.tsx +48 -0
- package/src/ui/design-system/components/showcase/FlowNav.tsx +58 -0
- package/src/ui/design-system/components/showcase/ImpactTag.case.tsx +19 -0
- package/src/ui/design-system/components/showcase/ImpactTag.css +36 -0
- package/src/ui/design-system/components/showcase/ImpactTag.placard.md +14 -0
- package/src/ui/design-system/components/showcase/ImpactTag.test.tsx +40 -0
- package/src/ui/design-system/components/showcase/ImpactTag.tsx +35 -0
- package/src/ui/design-system/components/showcase/NavItem.case.tsx +86 -0
- package/src/ui/design-system/components/showcase/NavItem.css +111 -0
- package/src/ui/design-system/components/showcase/NavItem.placard.md +13 -0
- package/src/ui/design-system/components/showcase/NavItem.test.tsx +65 -0
- package/src/ui/design-system/components/showcase/NavItem.tsx +95 -0
- package/src/ui/design-system/components/showcase/RenderAddress.case.tsx +21 -0
- package/src/ui/design-system/components/showcase/RenderAddress.css +35 -0
- package/src/ui/design-system/components/showcase/RenderAddress.placard.md +7 -0
- package/src/ui/design-system/components/showcase/RenderAddress.test.tsx +26 -0
- package/src/ui/design-system/components/showcase/RenderAddress.tsx +43 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.case.tsx +84 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.css +61 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.placard.md +21 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.test.tsx +81 -0
- package/src/ui/design-system/components/showcase/SegmentedToggle.tsx +75 -0
- package/src/ui/design-system/components/showcase/Sidebar.case.tsx +67 -0
- package/src/ui/design-system/components/showcase/Sidebar.css +6 -0
- package/src/ui/design-system/components/showcase/Sidebar.placard.md +14 -0
- package/src/ui/design-system/components/showcase/Sidebar.test.tsx +32 -0
- package/src/ui/design-system/components/showcase/Sidebar.tsx +30 -0
- package/src/ui/design-system/components/showcase/Stage.case.tsx +51 -0
- package/src/ui/design-system/components/showcase/Stage.css +91 -0
- package/src/ui/design-system/components/showcase/Stage.placard.md +15 -0
- package/src/ui/design-system/components/showcase/Stage.test.tsx +84 -0
- package/src/ui/design-system/components/showcase/Stage.tsx +97 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.case.tsx +81 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.css +169 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.placard.md +20 -0
- package/src/ui/design-system/components/showcase/TweaksPanel.tsx +230 -0
- package/src/ui/design-system/components/showcase/Wordmark.case.tsx +42 -0
- package/src/ui/design-system/components/showcase/Wordmark.css +31 -0
- package/src/ui/design-system/components/showcase/Wordmark.placard.md +10 -0
- package/src/ui/design-system/components/showcase/Wordmark.test.tsx +22 -0
- package/src/ui/design-system/components/showcase/Wordmark.tsx +22 -0
- package/src/ui/design-system/primer-specimens/brand.tsx +26 -0
- package/src/ui/design-system/primer-specimens/colors.tsx +83 -0
- package/src/ui/design-system/primer-specimens/components.tsx +308 -0
- package/src/ui/design-system/primer-specimens/foundations.tsx +71 -0
- package/src/ui/design-system/primer-specimens/index.ts +25 -0
- package/src/ui/design-system/primer-specimens/showcase.tsx +68 -0
- package/src/ui/design-system/primer-specimens/spacing.tsx +101 -0
- package/src/ui/design-system/primer-specimens/type.tsx +75 -0
- package/src/ui/design-system/primer.mdx +236 -0
- package/src/ui/design-system/styles.css +14 -0
- package/src/ui/design-system/tokens/colors.css +172 -0
- package/src/ui/design-system/tokens/fonts.css +18 -0
- package/src/ui/design-system/tokens/spacing.css +48 -0
- package/src/ui/design-system/tokens/typography.css +49 -0
- package/src/ui/markdown.test.tsx +54 -0
- package/src/ui/markdown.tsx +19 -0
- package/src/ui/primer-mount.tsx +76 -0
- package/src/ui/primer.css +175 -0
- package/src/ui/primer.tsx +277 -0
- package/src/ui/render-mount.tsx +284 -0
- package/src/ui/shell-core.test.ts +340 -0
- package/src/ui/shell-core.ts +295 -0
- package/src/ui/shell.tsx +60 -0
- package/src/ui/test-ids.ts +53 -0
- package/src/ui/use-shell.ts +1230 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { rm } from 'node:fs/promises'
|
|
3
|
+
import { makeTempDir, writeFiles } from '../testing/test-helpers'
|
|
4
|
+
import { checkTokens } from './tokens-check'
|
|
5
|
+
|
|
6
|
+
// A minimal config whose only token source of truth is `tokens.css`. `extra`
|
|
7
|
+
// splices additional config fields (e.g. a tokens allowlist) into the literal.
|
|
8
|
+
const config = (extra = '') =>
|
|
9
|
+
`export default { title: 'Fixture', roots: ['**/*.case.tsx'], globalStyles: ['tokens.css']${extra} }\n`
|
|
10
|
+
|
|
11
|
+
describe('checkTokens', () => {
|
|
12
|
+
const dirs: string[] = []
|
|
13
|
+
const setup = async (files: Record<string, string>) => {
|
|
14
|
+
const dir = await makeTempDir()
|
|
15
|
+
dirs.push(dir)
|
|
16
|
+
await writeFiles(dir, files)
|
|
17
|
+
return dir
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
while (dirs.length)
|
|
22
|
+
await rm(dirs.pop() as string, { recursive: true, force: true })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('passes when every reference resolves to a defined token', async () => {
|
|
26
|
+
const dir = await setup({
|
|
27
|
+
'display-case.config.ts': config(),
|
|
28
|
+
'tokens.css': ':root { --color-bg: #fff; }',
|
|
29
|
+
'button.css': '.b { background: var(--color-bg); }',
|
|
30
|
+
})
|
|
31
|
+
const { violations, definedCount } = await checkTokens(dir)
|
|
32
|
+
expect(violations).toEqual([])
|
|
33
|
+
expect(definedCount).toBeGreaterThanOrEqual(1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('flags a reference to an undefined token', async () => {
|
|
37
|
+
const dir = await setup({
|
|
38
|
+
'display-case.config.ts': config(),
|
|
39
|
+
'tokens.css': ':root { --color-bg: #fff; }',
|
|
40
|
+
'button.css': '.b { color: var(--mystery); }', // allow: unknown-token
|
|
41
|
+
})
|
|
42
|
+
const { violations } = await checkTokens(dir)
|
|
43
|
+
expect(violations).toHaveLength(1)
|
|
44
|
+
expect(violations[0].token).toBe('--mystery')
|
|
45
|
+
expect(violations[0].hadFallback).toBe(false)
|
|
46
|
+
expect(violations[0].file.endsWith('button.css')).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('reports the 1-based line and column of the token', async () => {
|
|
50
|
+
const dir = await setup({
|
|
51
|
+
'display-case.config.ts': config(),
|
|
52
|
+
'tokens.css': ':root{--a:1;}',
|
|
53
|
+
'x.css': 'a{color:1;}\nb{fill:var(--missing);}', // allow: unknown-token
|
|
54
|
+
})
|
|
55
|
+
const { violations } = await checkTokens(dir)
|
|
56
|
+
expect(violations).toHaveLength(1)
|
|
57
|
+
expect(violations[0].line).toBe(2)
|
|
58
|
+
// `b{fill:` is 7 chars, `var(` opens at col 8, `--missing` begins at col 12.
|
|
59
|
+
expect(violations[0].column).toBe(12)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('a fallback value does not excuse an undefined token', async () => {
|
|
63
|
+
const dir = await setup({
|
|
64
|
+
'display-case.config.ts': config(),
|
|
65
|
+
'tokens.css': ':root{}',
|
|
66
|
+
'x.css': '.a{color:var(--ghost, #6b7280);}', // allow: unknown-token
|
|
67
|
+
})
|
|
68
|
+
const { violations } = await checkTokens(dir)
|
|
69
|
+
expect(violations).toHaveLength(1)
|
|
70
|
+
expect(violations[0].token).toBe('--ghost')
|
|
71
|
+
expect(violations[0].hadFallback).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('an allow-listed token name is treated as defined', async () => {
|
|
75
|
+
const dir = await setup({
|
|
76
|
+
'display-case.config.ts': config(`, tokens: { allow: ['--host-bg'] }`),
|
|
77
|
+
'tokens.css': ':root{}',
|
|
78
|
+
'x.css': '.a{background:var(--host-bg);}', // allow: unknown-token
|
|
79
|
+
})
|
|
80
|
+
const { violations, definedCount } = await checkTokens(dir)
|
|
81
|
+
expect(violations).toEqual([])
|
|
82
|
+
expect(definedCount).toBeGreaterThanOrEqual(1)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('an `allow: unknown-token` comment suppresses on the same or preceding line', async () => {
|
|
86
|
+
const dir = await setup({
|
|
87
|
+
'display-case.config.ts': config(),
|
|
88
|
+
'tokens.css': ':root{}',
|
|
89
|
+
'same.css': '.a{color:var(--x);} /* allow: unknown-token */',
|
|
90
|
+
'above.css': '/* allow: unknown-token */\n.a{color:var(--y);}',
|
|
91
|
+
})
|
|
92
|
+
const { violations } = await checkTokens(dir)
|
|
93
|
+
expect(violations).toEqual([])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('a var() inside a CSS comment is not treated as a reference', async () => {
|
|
97
|
+
const dir = await setup({
|
|
98
|
+
'display-case.config.ts': config(),
|
|
99
|
+
'tokens.css': ':root{}',
|
|
100
|
+
'x.css': '/* var(--commented) */\n.a{color:red;}', // allow: unknown-token
|
|
101
|
+
})
|
|
102
|
+
const { violations } = await checkTokens(dir)
|
|
103
|
+
expect(violations).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a var() inside a JS string literal is a real reference', async () => {
|
|
107
|
+
const dir = await setup({
|
|
108
|
+
'display-case.config.ts': config(),
|
|
109
|
+
'tokens.css': ':root{}',
|
|
110
|
+
'x.tsx': `export const s = { color: 'var(--js-token)' }\n`, // allow: unknown-token
|
|
111
|
+
})
|
|
112
|
+
const { violations } = await checkTokens(dir)
|
|
113
|
+
expect(violations.map((v) => v.token)).toContain('--js-token')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('an inline-style object key defines a token for the whole package', async () => {
|
|
117
|
+
const dir = await setup({
|
|
118
|
+
'display-case.config.ts': config(),
|
|
119
|
+
'tokens.css': ':root{}',
|
|
120
|
+
'comp.tsx': `export const x = { style: { '--ring': 'red' } }\n`,
|
|
121
|
+
'comp.css': '.a{outline-color:var(--ring);}',
|
|
122
|
+
})
|
|
123
|
+
const { violations } = await checkTokens(dir)
|
|
124
|
+
expect(violations).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('resolves a reference whose definition lives in a different file', async () => {
|
|
128
|
+
const dir = await setup({
|
|
129
|
+
'display-case.config.ts': config(),
|
|
130
|
+
'tokens.css': ':root{ --shared: 2px; }',
|
|
131
|
+
'a.css': '.a{ gap: var(--shared); }', // allow: unknown-token
|
|
132
|
+
})
|
|
133
|
+
const { violations } = await checkTokens(dir)
|
|
134
|
+
expect(violations).toEqual([])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('sorts violations by file, then line, then column', async () => {
|
|
138
|
+
const dir = await setup({
|
|
139
|
+
'display-case.config.ts': config(),
|
|
140
|
+
'tokens.css': ':root{}',
|
|
141
|
+
'b.css': '.a{color:var(--p);}', // allow: unknown-token
|
|
142
|
+
'a.css': '.a{color:var(--q);}\n.b{color:var(--r);}', // allow: unknown-token
|
|
143
|
+
})
|
|
144
|
+
const { violations } = await checkTokens(dir)
|
|
145
|
+
expect(violations.map((v) => v.token)).toEqual(['--q', '--r', '--p'])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('reports the number of scanned files', async () => {
|
|
149
|
+
const dir = await setup({
|
|
150
|
+
'display-case.config.ts': config(),
|
|
151
|
+
'tokens.css': ':root{--a:1;}',
|
|
152
|
+
'x.css': '.a{color:var(--a);}', // allow: unknown-token
|
|
153
|
+
})
|
|
154
|
+
const { scannedFiles } = await checkTokens(dir)
|
|
155
|
+
// config.ts + tokens.css + x.css (tokens.css is reached via both the glob
|
|
156
|
+
// and globalStyles, but the path set dedups it).
|
|
157
|
+
expect(scannedFiles).toBe(3)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { Glob } from 'bun'
|
|
3
|
+
import { resolveConfig } from '../core/discovery'
|
|
4
|
+
import { blankComments } from './check-text'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Static design-token conformance check for a Display-Case-ingested package.
|
|
8
|
+
*
|
|
9
|
+
* Display Case already knows the two halves of a package's token contract:
|
|
10
|
+
* - DEFINITIONS — the `globalStyles` CSS it injects into every preview
|
|
11
|
+
* (`--name: …` declarations), plus any custom properties a component sets at
|
|
12
|
+
* runtime via an inline `style={{ '--name': … }}` object.
|
|
13
|
+
* - REFERENCES — every `var(--name…)` in the package's own source (component
|
|
14
|
+
* CSS/TSX and the `.case.tsx` files).
|
|
15
|
+
*
|
|
16
|
+
* This pass flags any `var(--name)` whose name is neither defined in the package
|
|
17
|
+
* nor explicitly allow-listed. It catches the class of bug where a component
|
|
18
|
+
* borrows a foreign design system's token name (e.g. a shadcn-style
|
|
19
|
+
* `var(--muted-foreground, #6b7280)`) that never resolves and silently falls
|
|
20
|
+
* back to a hardcoded value — well-formed CSS, but a vocabulary violation that
|
|
21
|
+
* breaks theming and contrast.
|
|
22
|
+
*
|
|
23
|
+
* Intentionally opinionated: a `var(--x, fallback)` reference is still flagged
|
|
24
|
+
* even though the fallback makes it valid CSS. The whole point is to require
|
|
25
|
+
* every reference to resolve within *this* package's declared vocabulary.
|
|
26
|
+
*
|
|
27
|
+
* Static and browser-free — pure parse, so it is cheap enough to gate on commit.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface TokenViolation {
|
|
31
|
+
/** Absolute path to the file containing the reference. */
|
|
32
|
+
file: string
|
|
33
|
+
/** 1-based line number. */
|
|
34
|
+
line: number
|
|
35
|
+
/** 1-based column of the `--token`. */
|
|
36
|
+
column: number
|
|
37
|
+
/** The unresolved custom-property name, e.g. `--muted-foreground`. */
|
|
38
|
+
token: string
|
|
39
|
+
/** True when the reference carried a fallback (`var(--x, …)`). */
|
|
40
|
+
hadFallback: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TokenCheckResult {
|
|
44
|
+
pkgDir: string
|
|
45
|
+
scannedFiles: number
|
|
46
|
+
definedCount: number
|
|
47
|
+
violations: TokenViolation[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `var(` then a custom property, capturing a following comma (⇒ has fallback).
|
|
51
|
+
const REF_RE = /var\(\s*(--[A-Za-z0-9_-]+)\s*(,)?/g
|
|
52
|
+
// A CSS custom-property *declaration*: `--name:` at a value-position boundary.
|
|
53
|
+
// The boundary excludes `var(--name)` (preceded by `(`) and JS object keys
|
|
54
|
+
// (preceded by a quote), so only genuine definitions are harvested.
|
|
55
|
+
const CSS_DEF_RE = /(?:^|[\s;{,])(--[A-Za-z0-9_-]+)\s*:/g
|
|
56
|
+
// An inline-style definition in JS/TSX: a quoted object key, e.g.
|
|
57
|
+
// `style={{ '--ring': color }}`. These set the property at runtime.
|
|
58
|
+
const JS_DEF_RE = /['"`](--[A-Za-z0-9_-]+)['"`]\s*:/g
|
|
59
|
+
// Per-reference escape hatch (mirrors the repo's `allow: <reason>` convention).
|
|
60
|
+
const ESCAPE_RE = /allow:\s*unknown-token/
|
|
61
|
+
|
|
62
|
+
interface ScannedFile {
|
|
63
|
+
path: string
|
|
64
|
+
/** Comment-blanked text (offsets + line breaks preserved). */
|
|
65
|
+
clean: string
|
|
66
|
+
/** Original, unmodified lines — used only for the escape-comment lookup. */
|
|
67
|
+
rawLines: string[]
|
|
68
|
+
isCss: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectDefs(file: ScannedFile, into: Set<string>): void {
|
|
72
|
+
const re = file.isCss ? CSS_DEF_RE : JS_DEF_RE
|
|
73
|
+
re.lastIndex = 0
|
|
74
|
+
for (let m = re.exec(file.clean); m; m = re.exec(file.clean)) {
|
|
75
|
+
into.add(m[1])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collectRefs(
|
|
80
|
+
file: ScannedFile,
|
|
81
|
+
defined: Set<string>,
|
|
82
|
+
out: TokenViolation[],
|
|
83
|
+
): void {
|
|
84
|
+
const cleanLines = file.clean.split('\n')
|
|
85
|
+
cleanLines.forEach((line, idx) => {
|
|
86
|
+
REF_RE.lastIndex = 0
|
|
87
|
+
for (let m = REF_RE.exec(line); m; m = REF_RE.exec(line)) {
|
|
88
|
+
const token = m[1]
|
|
89
|
+
if (defined.has(token)) continue
|
|
90
|
+
// Escape may sit on the reference line or the line directly above it.
|
|
91
|
+
const raw = file.rawLines[idx] ?? ''
|
|
92
|
+
const above = idx > 0 ? (file.rawLines[idx - 1] ?? '') : ''
|
|
93
|
+
if (ESCAPE_RE.test(raw) || ESCAPE_RE.test(above)) continue
|
|
94
|
+
out.push({
|
|
95
|
+
file: file.path,
|
|
96
|
+
line: idx + 1,
|
|
97
|
+
column: m.index + m[0].indexOf(token) + 1,
|
|
98
|
+
token,
|
|
99
|
+
hadFallback: m[2] === ',',
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run the token-reference conformance pass over one ingested package.
|
|
107
|
+
* Resolves the package's Display Case config to learn `globalStyles` (token
|
|
108
|
+
* definitions) and scans the whole package source tree for definitions +
|
|
109
|
+
* references.
|
|
110
|
+
*/
|
|
111
|
+
export async function checkTokens(pkgDir: string): Promise<TokenCheckResult> {
|
|
112
|
+
const { config } = await resolveConfig(pkgDir)
|
|
113
|
+
|
|
114
|
+
// Gather the files that hold definitions and/or references: the configured
|
|
115
|
+
// global stylesheets (token source of truth) plus all package source.
|
|
116
|
+
const paths = new Set<string>()
|
|
117
|
+
for (const rel of config.globalStyles ?? []) paths.add(resolve(pkgDir, rel))
|
|
118
|
+
const glob = new Glob('**/*.{css,ts,tsx}')
|
|
119
|
+
for await (const match of glob.scan({ cwd: pkgDir, absolute: true })) {
|
|
120
|
+
if (
|
|
121
|
+
match.includes('/node_modules/') ||
|
|
122
|
+
match.includes('/.display-case/') ||
|
|
123
|
+
match.includes('/dist/')
|
|
124
|
+
) {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
paths.add(match)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const files: ScannedFile[] = []
|
|
131
|
+
for (const path of paths) {
|
|
132
|
+
if (!(await Bun.file(path).exists())) continue
|
|
133
|
+
const text = await Bun.file(path).text()
|
|
134
|
+
const isCss = path.endsWith('.css')
|
|
135
|
+
files.push({
|
|
136
|
+
path,
|
|
137
|
+
clean: blankComments(text, isCss),
|
|
138
|
+
rawLines: text.split('\n'),
|
|
139
|
+
isCss,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pass 1 — every defined token across the whole package (definitions may live
|
|
144
|
+
// in a different file than the reference), seeded with the config allowlist.
|
|
145
|
+
const defined = new Set<string>(config.tokens?.allow ?? [])
|
|
146
|
+
for (const file of files) collectDefs(file, defined)
|
|
147
|
+
|
|
148
|
+
// Pass 2 — flag references to names the package never defines.
|
|
149
|
+
const violations: TokenViolation[] = []
|
|
150
|
+
for (const file of files) collectRefs(file, defined, violations)
|
|
151
|
+
violations.sort(
|
|
152
|
+
(a, b) =>
|
|
153
|
+
a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
pkgDir,
|
|
158
|
+
scannedFiles: files.length,
|
|
159
|
+
definedCount: defined.size,
|
|
160
|
+
violations,
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { AGENT_TARGETS, DEFAULT_AGENT } from './commands/agents'
|
|
5
|
+
import { getManifest, startDisplayCase } from './server/server'
|
|
6
|
+
|
|
7
|
+
// Display Case is Bun-native at *runtime*, not just at install time: discovery,
|
|
8
|
+
// bundling, and serving all use Bun's built-in bundler and `Bun.serve`. Running
|
|
9
|
+
// the CLI under Node fails deep inside with an opaque `Bun is not defined`, so
|
|
10
|
+
// detect it up front and point the user at Bun.
|
|
11
|
+
if (typeof globalThis.Bun === 'undefined') {
|
|
12
|
+
console.error(
|
|
13
|
+
'display-case requires the Bun runtime (https://bun.sh).\n' +
|
|
14
|
+
'Run it with Bun — e.g. `bunx @awarebydefault/display-case .` or `bun run display-case` — not Node.',
|
|
15
|
+
)
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Display Case CLI.
|
|
21
|
+
*
|
|
22
|
+
* display-case <pkgDir> [--port=N] start the dev server
|
|
23
|
+
* display-case <pkgDir> --print-manifest print the manifest JSON and exit
|
|
24
|
+
* display-case check <pkgDir> [--a11y] [--visual] [--tokens] [--structure] [--ssr] [--update] [--strict] [--only=ids] [--changed[=ref]] [--port=N]
|
|
25
|
+
* display-case init <pkgDir> [--agent=claude] [--with-visual] [--dry-run] [--json]
|
|
26
|
+
* display-case uninstall <pkgDir> [--agent=claude] [--dry-run] [--json]
|
|
27
|
+
*
|
|
28
|
+
* With no phase flag, `check` runs all phases. Naming any phase flag runs only
|
|
29
|
+
* the named phase(s) — e.g. `--tokens` runs the (browser-free) token check alone.
|
|
30
|
+
* `init`/`uninstall` scaffold (or remove) AI-agent integration in the repo.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const argv = process.argv.slice(2)
|
|
34
|
+
|
|
35
|
+
function flag(name: string): boolean {
|
|
36
|
+
return argv.includes(`--${name}`)
|
|
37
|
+
}
|
|
38
|
+
function option(name: string): string | undefined {
|
|
39
|
+
return argv.find((a) => a.startsWith(`--${name}=`))?.split('=')[1]
|
|
40
|
+
}
|
|
41
|
+
function positionals(): string[] {
|
|
42
|
+
return argv.filter((a) => !a.startsWith('--'))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CONFIG_FILE = 'display-case.config.ts'
|
|
46
|
+
|
|
47
|
+
function fail(message: string): never {
|
|
48
|
+
console.error(message)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Nearest ancestor of `start` (inclusive) containing a config, or null. */
|
|
53
|
+
function discoverConfigDir(start: string): string | null {
|
|
54
|
+
let dir = resolve(start)
|
|
55
|
+
for (let i = 0; i < 24; i++) {
|
|
56
|
+
if (existsSync(join(dir, CONFIG_FILE))) return dir
|
|
57
|
+
const parent = resolve(dir, '..')
|
|
58
|
+
if (parent === dir) return null
|
|
59
|
+
dir = parent
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the package directory to operate on.
|
|
66
|
+
*
|
|
67
|
+
* - An explicit target (`display-case apps/foo`) is used as given and
|
|
68
|
+
* must contain a `display-case.config.ts` — a wrong directory fails loudly
|
|
69
|
+
* rather than serving an empty showcase.
|
|
70
|
+
* - The default (no argument, or `.`) discovers the nearest config by walking up
|
|
71
|
+
* from the current directory, so it works from a package root *or* any
|
|
72
|
+
* subdirectory. The bare form is identical to `.` — there is no separate
|
|
73
|
+
* "no-argument" mode to reason about.
|
|
74
|
+
*
|
|
75
|
+
* Resolution, the `.display-case/` build cache, and repo-relative paths all
|
|
76
|
+
* anchor to the resolved package, so running from a git worktree keeps
|
|
77
|
+
* everything inside that worktree and two checkouts never share a cache. The
|
|
78
|
+
* one rule: run it from inside the package (any depth), or pass the path
|
|
79
|
+
* explicitly — don't rely on a process cwd that points elsewhere.
|
|
80
|
+
*/
|
|
81
|
+
function resolvePkgDir(arg: string | undefined): string {
|
|
82
|
+
if (arg && arg !== '.') {
|
|
83
|
+
const dir = resolve(arg)
|
|
84
|
+
if (!existsSync(join(dir, CONFIG_FILE))) {
|
|
85
|
+
fail(`No ${CONFIG_FILE} in ${dir} — is that a Display Case package?`)
|
|
86
|
+
}
|
|
87
|
+
return dir
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const here = discoverConfigDir(process.cwd())
|
|
91
|
+
if (here) return here
|
|
92
|
+
fail(
|
|
93
|
+
`No ${CONFIG_FILE} found in ${process.cwd()} or any parent directory.\n` +
|
|
94
|
+
`Run from inside a Display Case package, or pass <pkgDir> explicitly.`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// An explicit `--port` wins; otherwise honor `DISPLAY_CASE_PORT` (the dev
|
|
99
|
+
// orchestrator sets this per-worktree so two checkouts don't collide), falling
|
|
100
|
+
// back to the server's own default. `startDisplayCase` treats the chosen port as
|
|
101
|
+
// preferred and bumps off a busy one, so this never hard-fails on a clash.
|
|
102
|
+
const portArg = option('port') ?? process.env.DISPLAY_CASE_PORT
|
|
103
|
+
const port = portArg ? Number(portArg) : undefined
|
|
104
|
+
|
|
105
|
+
if (argv[0] === 'init' || argv[0] === 'uninstall') {
|
|
106
|
+
const pkgDir = resolve(positionals()[1] ?? '.')
|
|
107
|
+
const agent = option('agent') ?? DEFAULT_AGENT
|
|
108
|
+
if (!AGENT_TARGETS[agent]) {
|
|
109
|
+
console.error(
|
|
110
|
+
`Unsupported agent "${agent}". Supported: ${Object.keys(AGENT_TARGETS).join(', ')}.`,
|
|
111
|
+
)
|
|
112
|
+
process.exit(1)
|
|
113
|
+
}
|
|
114
|
+
const { runInit, runUninstall, report } = await import('./commands/init')
|
|
115
|
+
// init may set up the visual toolchain: explicit --with-visual, or an
|
|
116
|
+
// interactive prompt when attached to a TTY (never in --json/--dry-run).
|
|
117
|
+
let withVisual = flag('with-visual')
|
|
118
|
+
if (
|
|
119
|
+
argv[0] === 'init' &&
|
|
120
|
+
!withVisual &&
|
|
121
|
+
process.stdin.isTTY &&
|
|
122
|
+
!flag('json') &&
|
|
123
|
+
!flag('dry-run')
|
|
124
|
+
) {
|
|
125
|
+
const answer = prompt(
|
|
126
|
+
'Set up visual-regression checking (Playwright + pixelmatch + pngjs)? [y/N]',
|
|
127
|
+
)
|
|
128
|
+
withVisual = /^y(es)?$/i.test(answer?.trim() ?? '')
|
|
129
|
+
}
|
|
130
|
+
const opts = {
|
|
131
|
+
agent,
|
|
132
|
+
dryRun: flag('dry-run'),
|
|
133
|
+
json: flag('json'),
|
|
134
|
+
withVisual,
|
|
135
|
+
}
|
|
136
|
+
const result = await (argv[0] === 'init' ? runInit : runUninstall)(
|
|
137
|
+
pkgDir,
|
|
138
|
+
opts,
|
|
139
|
+
)
|
|
140
|
+
report(result)
|
|
141
|
+
} else if (argv[0] === 'check') {
|
|
142
|
+
const pkgDir = resolvePkgDir(positionals()[1])
|
|
143
|
+
const { runChecks } = await import('./checks/check')
|
|
144
|
+
const { resolveConfig } = await import('./core/discovery')
|
|
145
|
+
const { config } = await resolveConfig(pkgDir)
|
|
146
|
+
// A named phase flag ⇒ run only the named phase(s). With no phase flag, run
|
|
147
|
+
// every phase except those a config opts out of via `check.defaultPhases`.
|
|
148
|
+
const explicit = {
|
|
149
|
+
tokens: flag('tokens'),
|
|
150
|
+
a11y: flag('a11y'),
|
|
151
|
+
visual: flag('visual'),
|
|
152
|
+
structure: flag('structure'),
|
|
153
|
+
ssr: flag('ssr'),
|
|
154
|
+
}
|
|
155
|
+
const anyExplicit =
|
|
156
|
+
explicit.tokens ||
|
|
157
|
+
explicit.a11y ||
|
|
158
|
+
explicit.visual ||
|
|
159
|
+
explicit.structure ||
|
|
160
|
+
explicit.ssr
|
|
161
|
+
const defaults = config.check?.defaultPhases ?? {}
|
|
162
|
+
const runs = (phase: keyof typeof explicit): boolean =>
|
|
163
|
+
explicit[phase] || (!anyExplicit && defaults[phase] !== false)
|
|
164
|
+
// Change-scoping for the render phases (a11y/visual): `--only=<ids/globs>`
|
|
165
|
+
// restricts to named components; `--changed[=<ref>]` restricts to components
|
|
166
|
+
// whose import closure touches a file changed since <ref> (default the base
|
|
167
|
+
// branch, overridable via DISPLAY_CASE_BASE_REF). Both are no-ops for the
|
|
168
|
+
// static phases. See src/core/affected.ts.
|
|
169
|
+
const onlyValue = option('only')
|
|
170
|
+
const changedActive = flag('changed') || option('changed') !== undefined
|
|
171
|
+
const ok = await runChecks(pkgDir, {
|
|
172
|
+
a11y: runs('a11y'),
|
|
173
|
+
visual: runs('visual'),
|
|
174
|
+
tokens: runs('tokens'),
|
|
175
|
+
structure: runs('structure'),
|
|
176
|
+
ssr: runs('ssr'),
|
|
177
|
+
update: flag('update'),
|
|
178
|
+
strict: flag('strict'),
|
|
179
|
+
only: onlyValue ? onlyValue.split(',').filter(Boolean) : undefined,
|
|
180
|
+
changedRef: changedActive
|
|
181
|
+
? (option('changed') ??
|
|
182
|
+
process.env.DISPLAY_CASE_BASE_REF ??
|
|
183
|
+
'origin/main')
|
|
184
|
+
: undefined,
|
|
185
|
+
port,
|
|
186
|
+
})
|
|
187
|
+
process.exit(ok ? 0 : 1)
|
|
188
|
+
} else if (argv[0] === 'publish') {
|
|
189
|
+
const pkgDir = resolvePkgDir(positionals()[1])
|
|
190
|
+
const { publish } = await import('./commands/publish')
|
|
191
|
+
const isStatic = flag('static')
|
|
192
|
+
console.log('Building deployable showcase…')
|
|
193
|
+
const { out } = await publish(pkgDir, {
|
|
194
|
+
out: option('out'),
|
|
195
|
+
base: option('base'),
|
|
196
|
+
static: isStatic,
|
|
197
|
+
})
|
|
198
|
+
console.log(`\n Published → ${out}`)
|
|
199
|
+
console.log(
|
|
200
|
+
isStatic
|
|
201
|
+
? ' Static export written — serve the directory with any static host.'
|
|
202
|
+
: ` Run it: (cd ${out} && bun install && bun server.ts) — or build the Dockerfile.`,
|
|
203
|
+
)
|
|
204
|
+
process.exit(0)
|
|
205
|
+
} else {
|
|
206
|
+
const pkgDir = resolvePkgDir(positionals()[0])
|
|
207
|
+
|
|
208
|
+
if (flag('print-manifest')) {
|
|
209
|
+
const manifest = await getManifest(pkgDir)
|
|
210
|
+
console.log(JSON.stringify(manifest, null, 2))
|
|
211
|
+
process.exit(0)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// `--dev` enables live reload of the Display Case app itself (chrome,
|
|
215
|
+
// components, primer) — see StartOptions.dev.
|
|
216
|
+
const server = await startDisplayCase(pkgDir, { port, dev: flag('dev') })
|
|
217
|
+
console.log(`\n Display Case → ${server.url}\n`)
|
|
218
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AGENT_TARGETS, DEFAULT_AGENT } from './agents'
|
|
3
|
+
|
|
4
|
+
describe('agent targets', () => {
|
|
5
|
+
test('DEFAULT_AGENT points at a registered target', () => {
|
|
6
|
+
expect(AGENT_TARGETS[DEFAULT_AGENT]).toBeDefined()
|
|
7
|
+
expect(AGENT_TARGETS[DEFAULT_AGENT].id).toBe(DEFAULT_AGENT)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('the claude target declares launch, skills, and instruction locations', () => {
|
|
11
|
+
const t = AGENT_TARGETS.claude
|
|
12
|
+
expect(t.id).toBe('claude')
|
|
13
|
+
expect(t.launchConfigPath).toBe('.claude/launch.json')
|
|
14
|
+
expect(t.skillsDir).toBe('.claude/skills')
|
|
15
|
+
expect(t.instructionsFiles).toEqual(['AGENTS.md', 'CLAUDE.md'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('every target’s id matches its registry key', () => {
|
|
19
|
+
for (const [key, target] of Object.entries(AGENT_TARGETS)) {
|
|
20
|
+
expect(target.id).toBe(key)
|
|
21
|
+
expect(target.instructionsFiles.length).toBeGreaterThan(0)
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-agent conventions for where launch config, skills, and instructions live.
|
|
3
|
+
* Adding a new agent is data, not control flow — `init`/`uninstall` are
|
|
4
|
+
* agent-agnostic and read everything they need from the selected target.
|
|
5
|
+
*/
|
|
6
|
+
export interface AgentTarget {
|
|
7
|
+
id: string
|
|
8
|
+
/** Launch/run configuration file, relative to the repo root. */
|
|
9
|
+
launchConfigPath: string
|
|
10
|
+
/** Directory the agent loads skills from, relative to the repo root. */
|
|
11
|
+
skillsDir: string
|
|
12
|
+
/**
|
|
13
|
+
* Candidate instruction files in priority order; the first that exists is
|
|
14
|
+
* used, else the first is created.
|
|
15
|
+
*/
|
|
16
|
+
instructionsFiles: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AGENT_TARGETS: Record<string, AgentTarget> = {
|
|
20
|
+
claude: {
|
|
21
|
+
id: 'claude',
|
|
22
|
+
launchConfigPath: '.claude/launch.json',
|
|
23
|
+
skillsDir: '.claude/skills',
|
|
24
|
+
instructionsFiles: ['AGENTS.md', 'CLAUDE.md'],
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_AGENT = 'claude'
|