@codecademy/gamut 68.6.1-alpha.c211a2.0 → 68.6.1-alpha.df4bce.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 (36) 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 +52 -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 +107 -0
  15. package/agent-tools/guidelines/foundations/typography.md +83 -0
  16. package/agent-tools/guidelines/overview.md +40 -0
  17. package/agent-tools/guidelines/setup.md +81 -0
  18. package/agent-tools/rules/accessibility.mdc +78 -0
  19. package/agent-tools/skills/gamut-accessibility/SKILL.md +214 -0
  20. package/agent-tools/skills/gamut-color-mode/SKILL.md +138 -0
  21. package/agent-tools/skills/gamut-forms/SKILL.md +84 -0
  22. package/agent-tools/skills/gamut-system-props/SKILL.md +203 -0
  23. package/agent-tools/skills/gamut-testing/SKILL.md +221 -0
  24. package/agent-tools/skills/gamut-theming/SKILL.md +113 -0
  25. package/agent-tools/skills/gamut-typography/SKILL.md +75 -0
  26. package/bin/commands/plugin/install.mjs +173 -0
  27. package/bin/commands/plugin/list.mjs +105 -0
  28. package/bin/commands/plugin/remove.mjs +116 -0
  29. package/bin/commands/plugin/update.mjs +49 -0
  30. package/bin/gamut.mjs +92 -0
  31. package/bin/lib/claude.mjs +52 -0
  32. package/bin/lib/cursor.mjs +40 -0
  33. package/bin/lib/figma.mjs +49 -0
  34. package/bin/lib/resolve-plugin-dir.mjs +38 -0
  35. package/bin/lib/run-command.mjs +22 -0
  36. package/package.json +11 -8
@@ -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
+ }
@@ -0,0 +1,116 @@
1
+ import { rm, stat } from 'node:fs/promises';
2
+
3
+ import { claudePluginSpec, marketplaceName } from '../../lib/claude.mjs';
4
+ import { cursorDestPath } from '../../lib/cursor.mjs';
5
+ import { resolveFigmaOutput } from '../../lib/figma.mjs';
6
+ import { getFlag, resolvePluginDir } from '../../lib/resolve-plugin-dir.mjs';
7
+ import { runCommand } from '../../lib/run-command.mjs';
8
+ import { TARGETS } from './install.mjs';
9
+
10
+ export function help() {
11
+ console.log(`
12
+ Usage:
13
+ gamut plugin remove [target] [options]
14
+
15
+ Remove the installed Gamut plugin from an AI or design tool.
16
+
17
+ Arguments:
18
+ target Tool to remove from (default: cursor)
19
+ cursor | claude | figma
20
+
21
+ Options:
22
+ --output <path> [figma] Path to the DESIGN.md that was installed.
23
+ If omitted, walks up from cwd to find figma.config.json.
24
+ --plugin-dir <path> Override the bundled agent-tools directory
25
+ -h, --help Show this help message
26
+
27
+ Examples:
28
+ gamut plugin remove
29
+ gamut plugin remove claude
30
+ gamut plugin remove figma
31
+ gamut plugin remove figma --output ./docs/DESIGN.md
32
+ `);
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /** @param {string} sourceRoot */
38
+ async function removeCursor(sourceRoot) {
39
+ const dest = await cursorDestPath(sourceRoot);
40
+ const st = await stat(dest).catch(() => null);
41
+
42
+ if (!st) {
43
+ console.log(`Cursor: nothing to remove — ${dest} does not exist.`);
44
+ return;
45
+ }
46
+
47
+ await rm(dest, { recursive: true, force: true });
48
+ console.log(`Cursor: removed ${dest}`);
49
+ }
50
+
51
+ /** @param {string} sourceRoot */
52
+ async function removeClaude(sourceRoot) {
53
+ const spec = await claudePluginSpec(sourceRoot);
54
+ const mpName = marketplaceName(spec);
55
+ const pluginName = spec.split('@')[0];
56
+
57
+ let code = await runCommand('claude', ['plugin', 'remove', pluginName, '--scope', 'user']);
58
+ if (code !== 0) {
59
+ console.warn(
60
+ `warning: "claude plugin remove" exited ${code} — the plugin may not have been installed.`,
61
+ );
62
+ } else {
63
+ console.log(`Claude Code: removed plugin "${pluginName}"`);
64
+ }
65
+
66
+ code = await runCommand('claude', ['plugin', 'marketplace', 'remove', mpName]);
67
+ if (code !== 0) {
68
+ console.warn(
69
+ `warning: "claude plugin marketplace remove" exited ${code} — ` +
70
+ `the marketplace entry may not exist or the command syntax may differ. ` +
71
+ `Run "claude plugin marketplace list" to check.`,
72
+ );
73
+ } else {
74
+ console.log(`Claude Code: removed marketplace "${mpName}"`);
75
+ }
76
+ }
77
+
78
+ /** @param {string | undefined} outputArg */
79
+ async function removeFigma(outputArg) {
80
+ const { path: dest } = await resolveFigmaOutput(outputArg);
81
+ const st = await stat(dest).catch(() => null);
82
+
83
+ if (!st) {
84
+ console.log(`Figma: nothing to remove — ${dest} does not exist.`);
85
+ return;
86
+ }
87
+
88
+ await rm(dest, { force: true });
89
+ console.log(`Figma: removed ${dest}`);
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * gamut plugin remove [cursor|claude|figma] [--plugin-dir <path>]
96
+ *
97
+ * @param {string[]} args
98
+ */
99
+ export default async function remove(args) {
100
+ const target = args.find((a) => !a.startsWith('-')) ?? 'cursor';
101
+
102
+ if (!TARGETS.includes(target)) {
103
+ throw new Error(`Unknown target: "${target}". Choose from: ${TARGETS.join(', ')}`);
104
+ }
105
+
106
+ const pluginDir = await resolvePluginDir(args);
107
+
108
+ if (target === 'cursor') {
109
+ await removeCursor(pluginDir);
110
+ } else if (target === 'claude') {
111
+ await removeClaude(pluginDir);
112
+ } else if (target === 'figma') {
113
+ const output = getFlag(args, '--output', undefined);
114
+ await removeFigma(output);
115
+ }
116
+ }
@@ -0,0 +1,49 @@
1
+ import { getFlag } from '../../lib/resolve-plugin-dir.mjs';
2
+ import install, { TARGETS } from './install.mjs';
3
+
4
+ export function help() {
5
+ console.log(`
6
+ Usage:
7
+ gamut plugin update [target] [options]
8
+
9
+ Update the Gamut plugin in an AI or design tool.
10
+ Equivalent to re-running install — replaces the existing installation in place.
11
+
12
+ Arguments:
13
+ target Tool to update (default: cursor)
14
+ cursor | claude | figma
15
+
16
+ Options:
17
+ --scope <scope> Content to update (default: all)
18
+ all | skills | rules | commands | agents
19
+ --plugin-dir <path> Override the bundled agent-tools directory
20
+ -h, --help Show this help message
21
+
22
+ Examples:
23
+ gamut plugin update
24
+ gamut plugin update claude
25
+ gamut plugin update cursor --scope skills
26
+ `);
27
+ }
28
+
29
+ /**
30
+ * gamut plugin update [cursor|claude|figma] [--scope all|skills|rules|commands|agents]
31
+ * [--plugin-dir <path>]
32
+ *
33
+ * Re-runs install with the same arguments. For Cursor this does an in-place
34
+ * copy replacing any existing installation. For Claude Code it updates the
35
+ * marketplace entry and re-installs.
36
+ *
37
+ * @param {string[]} args
38
+ */
39
+ export default async function update(args) {
40
+ const target = args.find((a) => !a.startsWith('-')) ?? 'cursor';
41
+ const scope = getFlag(args, '--scope', 'all') ?? 'all';
42
+
43
+ if (!TARGETS.includes(target)) {
44
+ throw new Error(`Unknown target: "${target}". Choose from: ${TARGETS.join(', ')}`);
45
+ }
46
+
47
+ console.log(`Updating Gamut plugin for ${target}${scope !== 'all' ? ` (scope: ${scope})` : ''}…`);
48
+ await install(args);
49
+ }
package/bin/gamut.mjs ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Gamut CLI
5
+ *
6
+ * Usage:
7
+ * gamut plugin install [cursor|claude|figma] [--scope all|skills|rules|commands|agents]
8
+ * gamut plugin remove [cursor|claude|figma]
9
+ * gamut plugin update [cursor|claude|figma] [--scope all|skills|rules|commands|agents]
10
+ * gamut plugin list
11
+ */
12
+
13
+ const args = process.argv.slice(2);
14
+ const [noun, verb, ...rest] = args;
15
+
16
+ if (!noun || noun === '--help' || noun === '-h') {
17
+ printHelp();
18
+ process.exit(noun ? 0 : 1);
19
+ }
20
+
21
+ if (noun !== 'plugin') {
22
+ console.error(`Unknown command: "${noun}"`);
23
+ printHelp();
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!verb || verb === '--help' || verb === '-h') {
28
+ printPluginHelp();
29
+ process.exit(verb ? 0 : 1);
30
+ }
31
+
32
+ let cmd;
33
+ try {
34
+ cmd = await import(`./commands/plugin/${verb}.mjs`);
35
+ } catch {
36
+ console.error(`Unknown plugin subcommand: "${verb}"`);
37
+ printPluginHelp();
38
+ process.exit(1);
39
+ }
40
+
41
+ if (rest.includes('--help') || rest.includes('-h')) {
42
+ cmd.help();
43
+ process.exit(0);
44
+ }
45
+
46
+ try {
47
+ await cmd.default(rest);
48
+ } catch (/** @type {any} */ err) {
49
+ console.error(`Error: ${err.message}`);
50
+ process.exit(1);
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Help
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function printHelp() {
58
+ console.log(`
59
+ gamut — Gamut design system CLI
60
+
61
+ Usage:
62
+ gamut <command> [subcommand] [options]
63
+
64
+ Commands:
65
+ plugin Manage the Gamut plugin in your AI/design tools
66
+
67
+ Run "gamut plugin --help" for plugin subcommands.
68
+ `);
69
+ }
70
+
71
+ function printPluginHelp() {
72
+ console.log(`
73
+ gamut plugin — Manage the Gamut plugin
74
+
75
+ Subcommands:
76
+ install [target] [--scope <scope>] Install the plugin into a tool
77
+ remove [target] Remove an installed plugin
78
+ update [target] [--scope <scope>] Update an already-installed plugin
79
+ list Show installation status for all targets
80
+
81
+ Targets: cursor (default) | claude | figma
82
+ Scopes: all (default) | skills | rules | commands | agents
83
+
84
+ Examples:
85
+ gamut plugin install
86
+ gamut plugin install claude
87
+ gamut plugin install cursor --scope skills
88
+ gamut plugin remove claude
89
+ gamut plugin update
90
+ gamut plugin list
91
+ `);
92
+ }
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ /**
5
+ * Reads .claude-plugin/marketplace.json and returns a "name@marketplace" plugin spec.
6
+ *
7
+ * @param {string} sourceRoot
8
+ * @returns {Promise<string>}
9
+ */
10
+ export async function claudePluginSpec(sourceRoot) {
11
+ const mp = join(sourceRoot, '.claude-plugin', 'marketplace.json');
12
+ let text;
13
+ try {
14
+ text = await readFile(mp, 'utf8');
15
+ } catch {
16
+ throw new Error(
17
+ `Missing ${mp}.\n` +
18
+ `A .claude-plugin/marketplace.json is required for Claude Code installation.`,
19
+ );
20
+ }
21
+
22
+ const json =
23
+ /** @type {{ name?: string; plugins?: Array<{ name?: string; source?: string }> }} */ (
24
+ JSON.parse(text)
25
+ );
26
+ const { name: marketplaceName, plugins } = json;
27
+
28
+ if (!marketplaceName || !Array.isArray(plugins) || plugins.length === 0) {
29
+ throw new Error(`Invalid marketplace.json — needs "name" and "plugins[]": ${mp}`);
30
+ }
31
+
32
+ const entry =
33
+ plugins.find((p) => p.source === './' || p.source === '.' || p.source == null) ?? plugins[0];
34
+
35
+ if (!entry?.name) {
36
+ throw new Error(`No plugin name found in marketplace.json plugins[]: ${mp}`);
37
+ }
38
+
39
+ return `${entry.name}@${marketplaceName}`;
40
+ }
41
+
42
+ /**
43
+ * Returns just the marketplace name portion of a plugin spec ("name@marketplace").
44
+ *
45
+ * @param {string} spec
46
+ * @returns {string}
47
+ */
48
+ export function marketplaceName(spec) {
49
+ const name = spec.split('@')[1];
50
+ if (!name) throw new Error(`Could not parse marketplace name from plugin spec: ${spec}`);
51
+ return name;
52
+ }
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ /** @returns {string} */
6
+ export function cursorPluginsRoot() {
7
+ return process.env.CURSOR_PLUGINS_LOCAL ?? join(homedir(), '.cursor', 'plugins', 'local');
8
+ }
9
+
10
+ /**
11
+ * Reads the .cursor-plugin/plugin.json manifest and derives a folder name.
12
+ * Falls back to "gamut-agent-tools" if no manifest is found.
13
+ *
14
+ * @param {string} sourceRoot
15
+ * @returns {Promise<string>}
16
+ */
17
+ export async function cursorFolderName(sourceRoot) {
18
+ const manifest = join(sourceRoot, '.cursor-plugin', 'plugin.json');
19
+ try {
20
+ const text = await readFile(manifest, 'utf8');
21
+ const json = /** @type {{ name?: string }} */ (JSON.parse(text));
22
+ if (json.name && typeof json.name === 'string') {
23
+ return json.name.replace(/^@/, '').replace(/\//g, '-');
24
+ }
25
+ } catch {
26
+ // no manifest — use default
27
+ }
28
+ return 'gamut-agent-tools';
29
+ }
30
+
31
+ /**
32
+ * Returns the absolute path where the plugin is/should be installed for Cursor.
33
+ *
34
+ * @param {string} sourceRoot
35
+ * @returns {Promise<string>}
36
+ */
37
+ export async function cursorDestPath(sourceRoot) {
38
+ const folderName = await cursorFolderName(sourceRoot);
39
+ return join(cursorPluginsRoot(), folderName);
40
+ }
@@ -0,0 +1,49 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+
4
+ /**
5
+ * Walk up from `startDir` looking for a `figma.config.json` file.
6
+ * Returns the directory containing it, or null if not found before the filesystem root.
7
+ *
8
+ * @param {string} startDir
9
+ * @returns {Promise<string | null>}
10
+ */
11
+ export async function findFigmaConfigDir(startDir) {
12
+ let dir = startDir;
13
+ while (true) {
14
+ const st = await stat(join(dir, 'figma.config.json')).catch(() => null);
15
+ if (st?.isFile()) return dir;
16
+ const parent = dirname(dir);
17
+ if (parent === dir) return null;
18
+ dir = parent;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Resolves the destination directory for the guidelines/ folder.
24
+ *
25
+ * Priority:
26
+ * 1. --output <path> if provided (treated as the parent directory)
27
+ * 2. Directory containing the nearest figma.config.json (walking up from cwd)
28
+ *
29
+ * Throws with actionable guidance if neither resolves.
30
+ *
31
+ * @param {string | undefined} outputArg
32
+ * @returns {Promise<{ path: string; discovered: boolean }>}
33
+ */
34
+ export async function resolveFigmaOutput(outputArg) {
35
+ if (outputArg) {
36
+ return { path: resolve(outputArg), discovered: false };
37
+ }
38
+
39
+ const dir = await findFigmaConfigDir(process.cwd());
40
+ if (dir) {
41
+ return { path: join(dir, 'guidelines'), discovered: true };
42
+ }
43
+
44
+ throw new Error(
45
+ `Could not find figma.config.json in ${process.cwd()} or any parent directory.\n` +
46
+ `Provide the destination explicitly with --output:\n` +
47
+ ` gamut plugin install figma --output /path/to/your/project/guidelines`,
48
+ );
49
+ }