@codecademy/gamut 68.6.1-alpha.c211a2.0 → 68.6.1-alpha.d52035.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.
Files changed (35) hide show
  1. package/agent-tools/.claude-plugin/marketplace.json +16 -0
  2. package/agent-tools/.claude-plugin/plugin.json +7 -0
  3. package/agent-tools/.cursor-plugin/plugin.json +7 -0
  4. package/agent-tools/DESIGN.Codecademy.md +643 -0
  5. package/agent-tools/DESIGN.LXStudio.md +444 -0
  6. package/agent-tools/DESIGN.Percipio.md +435 -0
  7. package/agent-tools/DESIGN.md +1 -0
  8. package/agent-tools/agents/.gitkeep +0 -0
  9. package/agent-tools/commands/gamut-review.md +231 -0
  10. package/agent-tools/guidelines/components/buttons.md +91 -0
  11. package/agent-tools/guidelines/components/overview.md +44 -0
  12. package/agent-tools/guidelines/foundations/color.md +172 -0
  13. package/agent-tools/guidelines/foundations/modes.md +47 -0
  14. package/agent-tools/guidelines/foundations/spacing.md +66 -0
  15. package/agent-tools/guidelines/foundations/typography.md +50 -0
  16. package/agent-tools/guidelines/overview.md +38 -0
  17. package/agent-tools/guidelines/setup.md +42 -0
  18. package/agent-tools/rules/accessibility.mdc +68 -0
  19. package/agent-tools/skills/gamut-accessibility/SKILL.md +239 -0
  20. package/agent-tools/skills/gamut-color-mode/SKILL.md +138 -0
  21. package/agent-tools/skills/gamut-system-props/SKILL.md +173 -0
  22. package/agent-tools/skills/gamut-testing/SKILL.md +181 -0
  23. package/agent-tools/skills/gamut-theming/SKILL.md +113 -0
  24. package/agent-tools/skills/gamut-typography/SKILL.md +123 -0
  25. package/bin/commands/plugin/install.mjs +173 -0
  26. package/bin/commands/plugin/list.mjs +105 -0
  27. package/bin/commands/plugin/remove.mjs +116 -0
  28. package/bin/commands/plugin/update.mjs +49 -0
  29. package/bin/gamut.mjs +92 -0
  30. package/bin/lib/claude.mjs +52 -0
  31. package/bin/lib/cursor.mjs +40 -0
  32. package/bin/lib/figma.mjs +49 -0
  33. package/bin/lib/resolve-plugin-dir.mjs +38 -0
  34. package/bin/lib/run-command.mjs +22 -0
  35. package/package.json +11 -8
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: gamut-testing
3
+ description: Use this skill when writing or fixing unit tests for React components that use Gamut — setupRtl, MockGamutProvider, ColorMode in tests, emotion matchers, or removing jest.mock of @codecademy/gamut / gamut-styles.
4
+ ---
5
+
6
+ # Gamut Testing
7
+
8
+ Source: `@codecademy/gamut-tests` — [`index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut-tests/src/index.tsx)
9
+
10
+ ---
11
+
12
+ ## Core rule: never mock Gamut components
13
+
14
+ Do not use `jest.mock('@codecademy/gamut')` or manually mock individual Gamut components in test files. This silently bypasses emotion's theme context, produces tests that can't catch real rendering failures, and requires every test file to maintain its own fragile mock list.
15
+
16
+ Use `MockGamutProvider` or `setupRtl` — both are designed for exactly this purpose.
17
+
18
+ ---
19
+
20
+ ## What `MockGamutProvider` does
21
+
22
+ `MockGamutProvider` wraps `GamutProvider` with test-safe settings:
23
+
24
+ - `useCache={false}` — disables emotion's style injection cache so styles are predictable across tests
25
+ - `useGlobals={false}` — disables global CSS (Reboot, Typography, CSS Variables) to avoid side effects between test files
26
+ - `theme={coreTheme}` — provides the full Gamut token set so styled components resolve correctly
27
+
28
+ You should never construct a `GamutProvider` manually in a test. Use `MockGamutProvider`.
29
+
30
+ ---
31
+
32
+ ## Decision guide
33
+
34
+ | Scenario | Use |
35
+ |---|---|
36
+ | Standard component unit test | `setupRtl` from `@codecademy/gamut-tests` |
37
+ | Test that needs to vary `useLogicalProperties` | `render` + `MockGamutProvider` directly |
38
+ | Test that needs `ColorMode` context | `render` + `MockGamutProvider` + `<ColorMode>` |
39
+ | Visual test mock / Storybook wrapper | `MockGamutProvider` directly |
40
+
41
+ ---
42
+
43
+ ## `setupRtl` — preferred pattern
44
+
45
+ `setupRtl` from `@codecademy/gamut-tests` wraps `setupRtl` from `component-test-setup` with `MockGamutProvider` automatically. You do not need to add `MockGamutProvider` yourself.
46
+
47
+ ```tsx
48
+ import { setupRtl } from '@codecademy/gamut-tests';
49
+
50
+ import { MyComponent } from '../MyComponent';
51
+
52
+ const renderView = setupRtl(MyComponent, {
53
+ label: 'Default label',
54
+ onClick: jest.fn(),
55
+ });
56
+
57
+ it('renders the label', () => {
58
+ const { view } = renderView();
59
+ expect(view.getByText('Default label')).toBeInTheDocument();
60
+ });
61
+
62
+ it('accepts prop overrides', () => {
63
+ const { view } = renderView({ label: 'Override' });
64
+ expect(view.getByText('Override')).toBeInTheDocument();
65
+ });
66
+ ```
67
+
68
+ `renderView` returns `{ view, props, update }`:
69
+ - `view` — RTL `RenderResult` (getByRole, getByText, etc.)
70
+ - `props` — the resolved props passed to the component (useful for asserting on jest.fn() mocks)
71
+ - `update` — re-render with updated props without remounting
72
+
73
+ ### Accessing mock functions via `props`
74
+
75
+ ```tsx
76
+ it('calls onClick when clicked', async () => {
77
+ const { view, props } = renderView();
78
+ await userEvent.click(view.getByRole('button'));
79
+ expect(props.onClick).toHaveBeenCalled();
80
+ });
81
+ ```
82
+
83
+ ---
84
+
85
+ ## `MockGamutProvider` directly — when you need more control
86
+
87
+ Use `render` + `MockGamutProvider` when `setupRtl` doesn't give enough control over the wrapper — for example, when testing both logical and physical CSS property modes, or when adding `ColorMode`.
88
+
89
+ ```tsx
90
+ import { MockGamutProvider } from '@codecademy/gamut-tests';
91
+ import { render } from '@testing-library/react';
92
+
93
+ it('uses logical properties when enabled', () => {
94
+ const { container } = render(
95
+ <MockGamutProvider useLogicalProperties>
96
+ <MyComponent width="200px" />
97
+ </MockGamutProvider>
98
+ );
99
+ // assert on inlineSize rather than width
100
+ });
101
+ ```
102
+
103
+ ### Testing both logical and physical property modes
104
+
105
+ ```tsx
106
+ describe.each([
107
+ { useLogicalProperties: true, widthProp: 'inlineSize' },
108
+ { useLogicalProperties: false, widthProp: 'width' },
109
+ ])(
110
+ 'useLogicalProperties=$useLogicalProperties',
111
+ ({ useLogicalProperties, widthProp }) => {
112
+ it(`uses ${widthProp}`, () => {
113
+ const { container } = render(
114
+ <MockGamutProvider useLogicalProperties={useLogicalProperties}>
115
+ <MyComponent width="200px" />
116
+ </MockGamutProvider>
117
+ );
118
+ expect(container.firstChild).toHaveStyle({ [widthProp]: '200px' });
119
+ });
120
+ }
121
+ );
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Emotion style assertions
127
+
128
+ Install `@emotion/jest` matchers once per test file to enable CSS-in-JS assertions:
129
+
130
+ ```tsx
131
+ import { matchers } from '@emotion/jest';
132
+
133
+ expect.extend(matchers);
134
+ ```
135
+
136
+ Then assert on styles:
137
+
138
+ ```tsx
139
+ expect(element).toHaveStyle({ borderRadius: '2px' });
140
+ expect(element).toHaveStyleRule('padding', '1rem');
141
+ ```
142
+
143
+ Use `theme` from `@codecademy/gamut-styles` to avoid hardcoding token values:
144
+
145
+ ```tsx
146
+ import { theme } from '@codecademy/gamut-styles';
147
+
148
+ expect(element).toHaveStyle({ columnGap: theme.spacing[40] });
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Visual test wrappers
154
+
155
+ When creating mock components for visual tests or Storybook, wrap with `MockGamutProvider` and `ColorMode`:
156
+
157
+ ```tsx
158
+ import { MockGamutProvider } from '@codecademy/gamut-tests';
159
+ import { ColorMode } from '@codecademy/gamut-styles';
160
+
161
+ export const MyComponentMock: React.FC<ComponentProps<typeof MyComponent>> = (props) => (
162
+ <MockGamutProvider>
163
+ <ColorMode mode="light">
164
+ <MyComponent {...props} />
165
+ </ColorMode>
166
+ </MockGamutProvider>
167
+ );
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Common anti-patterns
173
+
174
+ | Anti-pattern | Fix |
175
+ |---|---|
176
+ | `jest.mock('@codecademy/gamut', () => ({ ... }))` | Remove mock; use `setupRtl` or `MockGamutProvider` |
177
+ | `jest.mock('@codecademy/gamut-styles', ...)` | Remove mock; `MockGamutProvider` handles theme context |
178
+ | Wrapping with `GamutProvider` directly in tests | Use `MockGamutProvider` — it sets `useCache={false}` and `useGlobals={false}` |
179
+ | Repeating `render(<MockGamutProvider>...</MockGamutProvider>)` in every test | Extract with `setupRtl`; define `renderView` once above the `describe` block |
180
+ | One `setupRtl` call per `it` block | Define `renderView` once outside `describe`, call it inside each `it` |
181
+ | Asserting on raw CSS token strings | Import `theme` from `@codecademy/gamut-styles` and use `theme.spacing[n]`, `theme.fontSize`, etc. |
@@ -0,0 +1,113 @@
1
+ ---
2
+ name: gamut-theming
3
+ description: Use this skill when working with GamutProvider themes, Emotion theme tokens, or behavior that differs across Core, Admin, Platform, LX Studio, and Percipio — including new themes, token access in styled components, or debugging theme-specific styles.
4
+ ---
5
+
6
+ # Gamut Theming
7
+
8
+ Source: `@codecademy/gamut-styles`
9
+
10
+ ## Overview
11
+
12
+ Gamut uses Emotion's theme system. All styled components have access to a typed theme object containing every design token. Themes are org-specific collections of tokens; components work across all themes without modification as long as they use token aliases rather than hardcoded values.
13
+
14
+ ## Available themes
15
+
16
+ | Theme | Used for |
17
+ |---|---|
18
+ | Core | Codecademy default (public-facing products) |
19
+ | Admin | Codecademy admin tools |
20
+ | Platform | Codecademy learning environment / shared platform surfaces |
21
+ | LX Studio | Learning Experience Studio |
22
+ | Percipio | Skillsoft Percipio platform |
23
+
24
+ The active theme is set at the app level via `<GamutProvider>`. Components inside receive the current theme via Emotion's context.
25
+
26
+ ## Accessing tokens in styled components
27
+
28
+ ### Via `css()` utility (recommended for static styles)
29
+
30
+ ```tsx
31
+ import { css } from '@codecademy/gamut-styles';
32
+ import styled from '@emotion/styled';
33
+
34
+ // Static color token
35
+ const Box = styled.div(css({ bg: 'navy-400', p: 4 }));
36
+
37
+ // Semantic color alias (adapts to color mode and theme)
38
+ const Text = styled.div(css({ color: 'primary', p: 4 }));
39
+ ```
40
+
41
+ ### Via `theme` prop (for dynamic styles)
42
+
43
+ Every Emotion styled component receives `theme` as a prop:
44
+
45
+ ```tsx
46
+ import styled from '@emotion/styled';
47
+
48
+ const DynamicBox = styled.div`
49
+ color: ${({ theme }) => theme.colors.blue};
50
+ font-size: ${({ theme }) => theme.fontSize[16]};
51
+ `;
52
+ ```
53
+
54
+ ### Via imported `theme` object (outside styled components)
55
+
56
+ ```tsx
57
+ import { css as emotionCss } from '@emotion/react';
58
+ import { theme } from '@codecademy/gamut-styles';
59
+
60
+ // For use in plain CSS-in-JS outside of styled components
61
+ const myStyles = emotionCss`
62
+ font-size: ${theme.fontSize[14]};
63
+ color: ${theme.colors['navy-400']};
64
+ `;
65
+ ```
66
+
67
+ ### Via `useTheme()` hook
68
+
69
+ ```tsx
70
+ import { useTheme } from '@emotion/react';
71
+
72
+ const MyComponent = () => {
73
+ const theme = useTheme();
74
+ return <div style={{ color: theme.colors.primary }} />;
75
+ };
76
+ ```
77
+
78
+ ## System props as the primary token API
79
+
80
+ For most styling needs, use **system props** (see the `gamut-system-props` skill) rather than accessing the theme directly. System props are the idiomatic way to use design tokens in Gamut components:
81
+
82
+ ```tsx
83
+ import { variance } from '@codecademy/variance';
84
+ import { system } from '@codecademy/gamut-styles';
85
+
86
+ const Card = styled.div(
87
+ variance.compose(system.layout, system.space, system.color)
88
+ );
89
+
90
+ // token scale values are validated at the type level
91
+ <Card bg="navy-400" p={16} width="100%" />;
92
+ ```
93
+
94
+ ## Theme-aware vs. color-mode-aware
95
+
96
+ | Concern | Tool |
97
+ |---|---|
98
+ | Tokens change per theme (colors, sizes, fonts) | Access via `theme.*` or system props |
99
+ | Colors change per light/dark mode | Use semantic color aliases + `<ColorMode>` |
100
+ | Background contrast | Use `<Background>` from ColorMode |
101
+
102
+ Semantic aliases like `primary`, `secondary`, `text`, `background` serve double duty: they resolve to theme-specific values **and** switch between light/dark variants automatically.
103
+
104
+ ## Creating a new theme
105
+
106
+ See `CreatingThemes.mdx` in the styleguide (`packages/styleguide/src/lib/Foundations/Theme/CreatingThemes.mdx`) for the full guide. Themes are defined in `@codecademy/gamut-styles` and must extend the base theme shape with all required token keys.
107
+
108
+ ## Key principles
109
+
110
+ - Prefer semantic token aliases over raw token values when the style needs to respond to color mode changes.
111
+ - Use raw tokens (e.g. `navy-400`) only for styles that should be fixed regardless of mode.
112
+ - Never hardcode hex values in styled components — always go through the theme/system props.
113
+ - The `GamutProvider` at the app root wires up the theme, color mode, and logical properties settings; components should not need to know which theme is active.
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: gamut-typography
3
+ description: Use this skill when creating or reviewing UI text in Gamut apps — headlines, body, captions, labels, code snippets, or text-heavy layouts — even if the user does not name fonts or tokens. Covers Apercu Pro, Suisse Intl Mono, scale, line heights, line length, and alignment.
4
+ ---
5
+
6
+ # Gamut Typography
7
+
8
+ > **Scope**: This skill covers typography for **Codecademy products** using the Core, Admin, or Platform themes (Apercu + Suisse). Percipio uses Roboto for all type — see `DESIGN.md` for Percipio-specific guidance. LX Studio uses Hanken Grotesk in place of both Apercu and Suisse.
9
+
10
+ ## Typefaces
11
+
12
+ Codecademy products use two typefaces:
13
+
14
+ ### Apercu Pro (`fontFamily: "base"`)
15
+
16
+ The primary typeface. Geometric-ish, humanist sans-serif. Use for:
17
+ - Headlines (Bold weight)
18
+ - Body / paragraph text (Regular weight)
19
+ - UI labels, menu items (Regular weight)
20
+ - Emphasis within body copy (Italic — **not** Bold)
21
+
22
+ **Rules:**
23
+ - Do not use Bold to emphasize text within a Regular-weight paragraph. Use Italic instead.
24
+ - Set with generous line-height for body text: **150–175%** of the type size (e.g. 16px type → 24–28px line-height).
25
+ - Headlines should use **100–110%** line-height to appear intentional and grouped.
26
+ - Text should be **left-aligned** by default.
27
+
28
+ ### Suisse Intl Mono (`fontFamily: "accent"`)
29
+
30
+ Monospace accent typeface. Use sparingly for:
31
+ - Code snippets and inline code
32
+ - Numbers, figures, and statistics
33
+ - Captions and labels that reference technical/engineering context
34
+ - Enumerated lists
35
+ - Quotations in a technical voice
36
+
37
+ **Rules:**
38
+ - Every character is the same width — avoid long paragraph-length prose in Suisse.
39
+ - It reads large for its point size: **reduce the size by ~10–15%** relative to Apercu text at the same visual scale (e.g. 14px Suisse ≈ 16px Apercu visually).
40
+ - Requires extra line-height to remain readable.
41
+
42
+ ## Font size scale (`fontSize`)
43
+
44
+ Sizes are accessed via the theme's `fontSize` scale. Common keys:
45
+
46
+ ```tsx
47
+ import { css } from '@codecademy/gamut-styles';
48
+ import styled from '@emotion/styled';
49
+
50
+ // Via system props
51
+ import { system } from '@codecademy/gamut-styles';
52
+ const Text = styled.p(system.typography);
53
+ <Text fontSize={16} />;
54
+
55
+ // Via css() utility
56
+ const Box = styled.div(css({ fontSize: 14 }));
57
+
58
+ // Via theme directly (outside styled components)
59
+ import { theme } from '@codecademy/gamut-styles';
60
+ const size = theme.fontSize[16];
61
+ ```
62
+
63
+ ## Line heights (`lineHeight`)
64
+
65
+ Line heights are limited to **multiples of 4px**. Type boxes are placed on an **8px placement grid**.
66
+
67
+ Guidelines:
68
+ - Body text: 150–175% of font size
69
+ - Headlines: 100–110% of font size
70
+
71
+ ## Line length
72
+
73
+ Controlling line length is essential for readability:
74
+
75
+ | Context | Target |
76
+ |---|---|
77
+ | Single-column body text | ~66 characters (max 85) |
78
+ | Multi-column layouts | ≤50 characters per line |
79
+ | Minimum | 45 characters |
80
+
81
+ **How to control line length**: Start with the right text style for the design, then adjust the width or column count of the text container.
82
+
83
+ ## Alignment
84
+
85
+ - **Left-align** paragraphs by default — this is easiest to read and supports grid alignment.
86
+ - **Center-align** only for short marketing headlines or specific interface components with brief text.
87
+ - **Never right-align** text in normal circumstances (exceptions: numbers, equations).
88
+ - Do not adjust letter-spacing.
89
+
90
+ ## Accessing typography tokens
91
+
92
+ ```tsx
93
+ // System props (recommended for styled components)
94
+ import { system } from '@codecademy/gamut-styles';
95
+ import { variance } from '@codecademy/variance';
96
+
97
+ const Heading = styled.h2(
98
+ variance.compose(system.typography, system.space)
99
+ );
100
+
101
+ <Heading fontSize={24} fontFamily="base" fontWeight="bold" lineHeight={1.1} mb={8} />;
102
+
103
+ // css() utility (recommended for static styles)
104
+ import { css } from '@codecademy/gamut-styles';
105
+
106
+ const Caption = styled.span(
107
+ css({ fontFamily: 'accent', fontSize: 12, color: 'secondary' })
108
+ );
109
+
110
+ // Theme object (outside styled components)
111
+ import { theme } from '@codecademy/gamut-styles';
112
+ import { css as emotionCss } from '@emotion/react';
113
+
114
+ const myStyles = emotionCss`
115
+ font-size: ${theme.fontSize[14]};
116
+ `;
117
+ ```
118
+
119
+ ## Semantic vs. visual sizing
120
+
121
+ - The term **"Title"** distinguishes visual size from semantic HTML hierarchy (H1–H6).
122
+ - A visually large title may use `<h2>` or `<p>` semantically — visual scale and semantic meaning are independent in Gamut.
123
+ - Choose HTML heading levels for document structure, choose font size for visual hierarchy.
@@ -0,0 +1,173 @@
1
+ import { cp, mkdir, readdir, rm, symlink } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ import { claudePluginSpec, marketplaceName } from '../../lib/claude.mjs';
5
+ import { cursorDestPath } from '../../lib/cursor.mjs';
6
+ import { resolveFigmaOutput } from '../../lib/figma.mjs';
7
+ import { getFlag, resolvePluginDir } from '../../lib/resolve-plugin-dir.mjs';
8
+ import { runCommand } from '../../lib/run-command.mjs';
9
+
10
+ export const TARGETS = ['cursor', 'claude', 'figma'];
11
+ export const SCOPES = ['all', 'skills', 'rules', 'commands', 'agents'];
12
+
13
+ export function help() {
14
+ console.log(`
15
+ Usage:
16
+ gamut plugin install [target] [options]
17
+
18
+ Install the Gamut plugin into an AI or design tool.
19
+
20
+ Arguments:
21
+ target Tool to install into (default: cursor)
22
+ cursor | claude | figma
23
+
24
+ Options:
25
+ --scope <scope> Content to install (default: all)
26
+ all | skills | rules | commands | agents
27
+ --output <path> [figma] Explicit destination directory for guidelines/.
28
+ If omitted, walks up from cwd to find figma.config.json.
29
+ --plugin-dir <path> Override the bundled agent-tools directory
30
+ -h, --help Show this help message
31
+
32
+ Examples:
33
+ gamut plugin install
34
+ gamut plugin install claude
35
+ gamut plugin install figma
36
+ gamut plugin install figma --output /path/to/project/guidelines
37
+ gamut plugin install cursor --scope skills
38
+ gamut plugin install cursor --plugin-dir ./my-agent-tools
39
+ `);
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /** Directories in the plugin source that should not be installed to Cursor. */
45
+ const CURSOR_IGNORE = new Set([
46
+ '.claude-plugin', // Claude Code manifest — not a Cursor concept
47
+ 'guidelines', // Figma Make only
48
+ ]);
49
+
50
+ /** @param {string} sourceRoot @param {string} scope */
51
+ async function installCursor(sourceRoot, scope) {
52
+ const dest = await cursorDestPath(sourceRoot);
53
+
54
+ if ((process.env.CURSOR_INSTALL_METHOD ?? 'copy') !== 'copy') {
55
+ // Symlink the whole plugin dir (dev convenience)
56
+ await rm(dest, { recursive: true, force: true });
57
+ await symlink(resolve(sourceRoot), dest, 'dir');
58
+ console.log(`Cursor: symlinked to ${dest}`);
59
+ return;
60
+ }
61
+
62
+ // Selective copy: always include the cursor manifest, then scoped content dirs
63
+ await rm(dest, { recursive: true, force: true });
64
+ await mkdir(dest, { recursive: true });
65
+
66
+ await cp(`${sourceRoot}/.cursor-plugin`, `${dest}/.cursor-plugin`, { recursive: true });
67
+
68
+ let dirs;
69
+ if (scope === 'all') {
70
+ const entries = await readdir(sourceRoot, { withFileTypes: true });
71
+ dirs = entries
72
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !CURSOR_IGNORE.has(e.name))
73
+ .map((e) => e.name);
74
+ } else {
75
+ dirs = [scope];
76
+ }
77
+
78
+ for (const dir of dirs) {
79
+ await cp(`${sourceRoot}/${dir}`, `${dest}/${dir}`, { recursive: true }).catch(() => {
80
+ // directory may be empty/missing — not an error
81
+ });
82
+ }
83
+
84
+ const scopeLabel = scope === 'all' ? 'all content' : scope;
85
+ console.log(`Cursor: installed (${scopeLabel}) → ${dest}`);
86
+ }
87
+
88
+ // Claude Code only loads from recognized plugin directories: skills/, commands/, agents/.
89
+ // rules/ is Cursor-specific (.mdc format); .cursor-plugin/ and guidelines/ are also
90
+ // present in sourceRoot but ignored by Claude Code.
91
+
92
+ /** @param {string} sourceRoot */
93
+ async function installClaude(sourceRoot) {
94
+ const spec = await claudePluginSpec(sourceRoot);
95
+ const mpName = marketplaceName(spec);
96
+ const root = resolve(sourceRoot);
97
+
98
+ let code = await runCommand('claude', ['plugin', 'marketplace', 'add', root, '--scope', 'user']);
99
+ if (code !== 0) {
100
+ console.warn(
101
+ `warning: "claude plugin marketplace add" exited ${code} — ` +
102
+ `if it's already registered this is safe to ignore.`,
103
+ );
104
+ code = await runCommand('claude', ['plugin', 'marketplace', 'update', mpName]);
105
+ if (code !== 0) {
106
+ throw new Error(
107
+ `claude plugin marketplace add/update failed (exit ${code}).\n` +
108
+ `Try manually: claude plugin marketplace add ${root}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ code = await runCommand('claude', ['plugin', 'install', spec, '--scope', 'user']);
114
+ if (code !== 0) {
115
+ throw new Error(
116
+ `claude plugin install failed (exit ${code}).\n` +
117
+ `Try manually: claude plugin install ${spec} --scope user`,
118
+ );
119
+ }
120
+
121
+ console.log(`Claude Code: installed ${spec} (user scope)`);
122
+ console.log(` Tip: run /reload-plugins in Claude Code if skills don't appear immediately.`);
123
+ console.log(` One-off without install: claude --plugin-dir ${root}`);
124
+ }
125
+
126
+ /**
127
+ * @param {string} sourceRoot
128
+ * @param {string | undefined} outputArg
129
+ */
130
+ async function installFigma(sourceRoot, outputArg) {
131
+ const src = join(sourceRoot, 'guidelines');
132
+ const { path: dest, discovered } = await resolveFigmaOutput(outputArg);
133
+
134
+ if (discovered) {
135
+ console.log(`Figma: found figma.config.json — installing to ${dest}`);
136
+ }
137
+
138
+ await rm(dest, { recursive: true, force: true });
139
+ await cp(src, dest, { recursive: true });
140
+ console.log(`Figma: installed guidelines/ → ${dest}`);
141
+ console.log(` In Figma Make, point your kit at this guidelines/ directory for design system context.`);
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /**
147
+ * gamut plugin install [cursor|claude|figma] [--scope all|skills|rules|commands|agents]
148
+ * [--plugin-dir <path>]
149
+ *
150
+ * @param {string[]} args
151
+ */
152
+ export default async function install(args) {
153
+ const target = args.find((a) => !a.startsWith('-')) ?? 'cursor';
154
+ const scope = getFlag(args, '--scope', 'all') ?? 'all';
155
+
156
+ if (!TARGETS.includes(target)) {
157
+ throw new Error(`Unknown target: "${target}". Choose from: ${TARGETS.join(', ')}`);
158
+ }
159
+ if (!SCOPES.includes(scope)) {
160
+ throw new Error(`Unknown scope: "${scope}". Choose from: ${SCOPES.join(', ')}`);
161
+ }
162
+
163
+ const pluginDir = await resolvePluginDir(args);
164
+
165
+ if (target === 'cursor') {
166
+ await installCursor(pluginDir, scope);
167
+ } else if (target === 'claude') {
168
+ await installClaude(pluginDir);
169
+ } else if (target === 'figma') {
170
+ const output = getFlag(args, '--output', undefined);
171
+ await installFigma(pluginDir, output);
172
+ }
173
+ }
@@ -0,0 +1,105 @@
1
+ import { stat } from 'node:fs/promises';
2
+
3
+ import { cursorDestPath } from '../../lib/cursor.mjs';
4
+ import { findFigmaConfigDir } from '../../lib/figma.mjs';
5
+ import { getFlag, resolvePluginDir } from '../../lib/resolve-plugin-dir.mjs';
6
+
7
+ export function help() {
8
+ console.log(`
9
+ Usage:
10
+ gamut plugin list [options]
11
+
12
+ Show installation status for all supported targets.
13
+
14
+ Options:
15
+ --output <path> [figma] Explicit path to DESIGN.md.
16
+ If omitted, walks up from cwd to find figma.config.json.
17
+ --plugin-dir <path> Override the bundled agent-tools directory
18
+ -h, --help Show this help message
19
+
20
+ Examples:
21
+ gamut plugin list
22
+ gamut plugin list --output ./docs/DESIGN.md
23
+ `);
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** @param {string} sourceRoot */
29
+ async function cursorStatus(sourceRoot) {
30
+ const dest = await cursorDestPath(sourceRoot);
31
+ const installed = !!(await stat(dest).catch(() => null));
32
+ return {
33
+ target: 'cursor',
34
+ status: installed ? '✓ installed' : '✗ not installed',
35
+ notes: installed ? dest : 'run: gamut plugin install cursor',
36
+ };
37
+ }
38
+
39
+ async function claudeStatus() {
40
+ // Claude Code doesn't expose a stable filesystem path we can check.
41
+ return {
42
+ target: 'claude',
43
+ status: '? unknown',
44
+ notes: 'run: claude plugin list',
45
+ };
46
+ }
47
+
48
+ /** @param {string | undefined} outputArg */
49
+ async function figmaStatus(outputArg) {
50
+ let dest;
51
+ if (outputArg) {
52
+ dest = outputArg;
53
+ } else {
54
+ const dir = await findFigmaConfigDir(process.cwd());
55
+ dest = dir ? `${dir}/DESIGN.md` : null;
56
+ }
57
+
58
+ if (!dest) {
59
+ return {
60
+ target: 'figma',
61
+ status: '? unknown',
62
+ notes: 'figma.config.json not found — run from your project root or use --output',
63
+ };
64
+ }
65
+
66
+ const installed = !!(await stat(dest).catch(() => null));
67
+ return {
68
+ target: 'figma',
69
+ status: installed ? '✓ installed' : '✗ not installed',
70
+ notes: installed ? dest : `run: gamut plugin install figma`,
71
+ };
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * gamut plugin list
78
+ *
79
+ * Shows installation status for each supported target.
80
+ *
81
+ * @param {string[]} args
82
+ */
83
+ export default async function list(args) {
84
+ const pluginDir = await resolvePluginDir(args);
85
+ const output = getFlag(args, '--output', undefined);
86
+
87
+ const rows = await Promise.all([
88
+ cursorStatus(pluginDir),
89
+ claudeStatus(),
90
+ figmaStatus(output),
91
+ ]);
92
+
93
+ const col0 = Math.max(...rows.map((r) => r.target.length));
94
+ const col1 = Math.max(...rows.map((r) => r.status.length));
95
+
96
+ const header = `${'Target'.padEnd(col0)} ${'Status'.padEnd(col1)} Path / Notes`;
97
+ const rule = '─'.repeat(header.length);
98
+
99
+ console.log(`\n${header}`);
100
+ console.log(rule);
101
+ for (const row of rows) {
102
+ console.log(`${row.target.padEnd(col0)} ${row.status.padEnd(col1)} ${row.notes}`);
103
+ }
104
+ console.log();
105
+ }