@3xhaust/oh-my-design 0.16.1

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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +282 -0
  3. package/README.md +282 -0
  4. package/adapters/build.ts +184 -0
  5. package/adapters/claude.ts +111 -0
  6. package/adapters/codex.ts +68 -0
  7. package/adapters/tokens.ts +21 -0
  8. package/adapters/tool-map.json +7 -0
  9. package/bin/omd-install.ts +76 -0
  10. package/bin/omd.ts +1774 -0
  11. package/core/asset-sourcing/index.ts +187 -0
  12. package/core/coach/index.ts +98 -0
  13. package/core/composition/app-shell-workbench.md +168 -0
  14. package/core/composition/asymmetric-diagonal-grid.md +178 -0
  15. package/core/composition/bento-grid.md +223 -0
  16. package/core/composition/editorial-index-labels.md +173 -0
  17. package/core/composition/form-wizard-stepper.md +132 -0
  18. package/core/composition/master-detail-flow.md +129 -0
  19. package/core/composition/section-inversion.md +182 -0
  20. package/core/composition/sidebar-margin-annotation.md +184 -0
  21. package/core/composition/split-screen-hero.md +224 -0
  22. package/core/composition/sticky-sidebar-scroll.md +219 -0
  23. package/core/composition/typographic-hero.md +100 -0
  24. package/core/composition-contract/index.ts +233 -0
  25. package/core/composition-contract/visual-richness.ts +101 -0
  26. package/core/config/index.ts +27 -0
  27. package/core/copy/index.ts +296 -0
  28. package/core/craft/finish-pass.md +356 -0
  29. package/core/craft/index.ts +33 -0
  30. package/core/design/index.ts +581 -0
  31. package/core/design/interaction-states.ts +189 -0
  32. package/core/eval-harness/index.ts +225 -0
  33. package/core/evidence/final.ts +444 -0
  34. package/core/evidence/task.ts +459 -0
  35. package/core/figma/client.ts +312 -0
  36. package/core/figma/diff.ts +261 -0
  37. package/core/figma/responsive.ts +310 -0
  38. package/core/figma/system.ts +466 -0
  39. package/core/figma/types.ts +185 -0
  40. package/core/frame/check-ux.ts +180 -0
  41. package/core/frame/index.ts +39 -0
  42. package/core/frame/write.ts +168 -0
  43. package/core/graphics/css-illustration-primitives.md +189 -0
  44. package/core/graphics/duotone-image-presets.md +173 -0
  45. package/core/graphics/gradient-mesh.md +178 -0
  46. package/core/graphics/noise-grain-texture.md +150 -0
  47. package/core/graphics/placeholder-policy.md +185 -0
  48. package/core/graphics/svg-geometric-patterns.md +145 -0
  49. package/core/history/index.ts +39 -0
  50. package/core/install/detect.ts +32 -0
  51. package/core/install/install.ts +321 -0
  52. package/core/install/patch-claude.ts +109 -0
  53. package/core/install/patch-codex.ts +82 -0
  54. package/core/interaction/index.ts +101 -0
  55. package/core/interaction/recipes/signature-lighting.md +203 -0
  56. package/core/ir/dom.ts +352 -0
  57. package/core/ir/normalize.ts +177 -0
  58. package/core/motion/easing.md +151 -0
  59. package/core/motion/energy.ts +188 -0
  60. package/core/motion/recipes/image-hover-distortion.md +198 -0
  61. package/core/motion/recipes/magnetic-hover.md +229 -0
  62. package/core/motion/recipes/marquee.md +202 -0
  63. package/core/motion/recipes/number-counter.md +242 -0
  64. package/core/motion/recipes/page-loader.md +240 -0
  65. package/core/motion/recipes/parallax.md +233 -0
  66. package/core/motion/recipes/scroll-reveal.md +210 -0
  67. package/core/motion/recipes/section-color-inversion.md +217 -0
  68. package/core/motion/recipes/split-text-entrance.md +222 -0
  69. package/core/motion/recipes/stagger-orchestrator.md +227 -0
  70. package/core/motion/recipes/sticky-scene-transition.md +246 -0
  71. package/core/motion/recipes/view-transitions.md +239 -0
  72. package/core/probe/index.ts +186 -0
  73. package/core/probe/schema.json +46 -0
  74. package/core/protocol/composition-contract.md +192 -0
  75. package/core/protocol/copy-deck.md +96 -0
  76. package/core/protocol/human-design-loop.md +447 -0
  77. package/core/protocol/slop-review.md +71 -0
  78. package/core/ref/batch.ts +94 -0
  79. package/core/ref/blueprint.ts +199 -0
  80. package/core/ref/distance.ts +225 -0
  81. package/core/ref/invariants.ts +162 -0
  82. package/core/ref/signal.ts +37 -0
  83. package/core/ref/store.ts +122 -0
  84. package/core/render/index.ts +607 -0
  85. package/core/rules/attribution.ts +159 -0
  86. package/core/rules/builtin/contrast.yaml +8 -0
  87. package/core/rules/builtin/focus.yaml +8 -0
  88. package/core/rules/builtin/hit-area.yaml +8 -0
  89. package/core/rules/builtin/ko.yaml +6 -0
  90. package/core/rules/builtin/motion.yaml +80 -0
  91. package/core/rules/builtin/slop.yaml +365 -0
  92. package/core/rules/builtin/spacing.yaml +9 -0
  93. package/core/rules/builtin/system.yaml +42 -0
  94. package/core/rules/builtin/token.yaml +8 -0
  95. package/core/rules/builtin/ux.yaml +125 -0
  96. package/core/rules/engine.ts +88 -0
  97. package/core/rules/leakage.ts +72 -0
  98. package/core/rules/motion-spec.ts +204 -0
  99. package/core/site/index.ts +100 -0
  100. package/core/slop/index.ts +427 -0
  101. package/core/slop/text-slop.ts +220 -0
  102. package/core/source-seal/index.ts +174 -0
  103. package/core/stack/index.ts +53 -0
  104. package/core/target/index.ts +183 -0
  105. package/core/theory/color.md +285 -0
  106. package/core/theory/components.md +272 -0
  107. package/core/theory/craft.md +270 -0
  108. package/core/theory/expressive.md +213 -0
  109. package/core/theory/imagegen.md +105 -0
  110. package/core/theory/layout.md +159 -0
  111. package/core/theory/motion.md +283 -0
  112. package/core/theory/typography.md +134 -0
  113. package/core/theory/ux.md +580 -0
  114. package/core/theory/voice.md +595 -0
  115. package/core/types.ts +684 -0
  116. package/package.json +40 -0
  117. package/src/agents/composer.agent.yaml +174 -0
  118. package/src/agents/eye.agent.yaml +208 -0
  119. package/src/agents/framer.agent.yaml +52 -0
  120. package/src/agents/glance.agent.yaml +19 -0
  121. package/src/agents/hand.agent.yaml +200 -0
  122. package/src/agents/scout.agent.yaml +71 -0
  123. package/src/agents/sketch.agent.yaml +64 -0
  124. package/src/agents/typesetter.agent.yaml +52 -0
  125. package/src/agents/writer.agent.yaml +63 -0
  126. package/src/skills/omd-coach/SKILL.md +40 -0
  127. package/src/skills/omd-critique/SKILL.md +77 -0
  128. package/src/skills/omd-figma/SKILL.md +256 -0
  129. package/src/skills/omd-humanize/SKILL.md +160 -0
  130. package/src/skills/omd-scout/SKILL.md +77 -0
  131. package/src/skills/omd-ultradesign/SKILL.md +521 -0
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { parse } from 'yaml';
6
+ import { emitCodex } from './codex.ts';
7
+ import { emitClaude, emitClaudePlugin, pluginizeSkill } from './claude.ts';
8
+ import { substituter } from './tokens.ts';
9
+ import type { AbstractAgent, Emitted, Host } from '../core/types.ts';
10
+
11
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
12
+
13
+ function readAll<T>(dir: string, ext: string, parseOne: (text: string) => T): T[] {
14
+ const path = join(root, dir);
15
+ if (!existsSync(path)) return [];
16
+ return readdirSync(path)
17
+ .filter((f) => f.endsWith(ext))
18
+ .map((f) => parseOne(readFileSync(join(path, f), 'utf8')));
19
+ }
20
+
21
+ interface Skill {
22
+ name: string;
23
+ description: string;
24
+ source: string;
25
+ }
26
+
27
+ function readSkills(): Skill[] {
28
+ const dir = join(root, 'src', 'skills');
29
+ if (!existsSync(dir)) return [];
30
+ const skills: Skill[] = [];
31
+ for (const name of readdirSync(dir)) {
32
+ const file = join(dir, name, 'SKILL.md');
33
+ if (!existsSync(file)) continue;
34
+ const source = readFileSync(file, 'utf8');
35
+ const match = /^---\n([\s\S]*?)\n---/.exec(source);
36
+ const frontmatter = (match?.[1] ? parse(match[1]) : {}) as { name?: string; description?: string };
37
+ skills.push({ name: frontmatter.name ?? name, description: frontmatter.description ?? '', source });
38
+ }
39
+ return skills;
40
+ }
41
+
42
+ const firstSentence = (s: string): string => (/^[^.。]*[.。]?/.exec(s.trim())?.[0] ?? '').trim();
43
+ const titleCase = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
44
+ export const skillDisplayName = (name: string): string => name.startsWith('omd-') ? `(omd) ${name.slice(4)}` : titleCase(name);
45
+
46
+ export const skillOpenaiMetadata = (name: string, description: string): string => [
47
+ 'interface:',
48
+ ` display_name: ${JSON.stringify(skillDisplayName(name))}`,
49
+ ` short_description: ${JSON.stringify(firstSentence(description))}`,
50
+ '',
51
+ ].join('\n');
52
+
53
+ function write(host: Host, rel: string, content: unknown): void {
54
+ const path = join(root, 'dist', host, rel);
55
+ mkdirSync(dirname(path), { recursive: true });
56
+ writeFileSync(path, typeof content === 'string' ? content : `${JSON.stringify(content, null, 2)}\n`);
57
+ }
58
+
59
+ export function build(): void {
60
+ // Wipe first. A file that stopped being emitted must stop being installed — a stale
61
+ // hooks/ directory left here is a gate that quietly comes back to life.
62
+ rmSync(join(root, 'dist'), { recursive: true, force: true });
63
+
64
+ const agents = readAll<AbstractAgent>('src/agents', '.agent.yaml', (t) => parse(t) as AbstractAgent);
65
+ const skills = readSkills();
66
+
67
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string };
68
+
69
+ const emitters: Record<Host, (opts: { agents: AbstractAgent[] }) => Emitted> = {
70
+ codex: (opts) => emitCodex({ ...opts, version: pkg.version }),
71
+ claude: emitClaude,
72
+ };
73
+
74
+ for (const host of Object.keys(emitters) as Host[]) {
75
+ const { files } = emitters[host]({ agents });
76
+ for (const [rel, content] of Object.entries(files)) write(host, rel, content);
77
+
78
+ const sub = substituter(host);
79
+ for (const skill of skills) {
80
+ write(host, `skills/${skill.name}/SKILL.md`, sub(skill.source));
81
+ if (host === 'codex') {
82
+ write(host, `skills/${skill.name}/agents/openai.yaml`, skillOpenaiMetadata(skill.name, skill.description));
83
+ }
84
+ }
85
+
86
+ // Copy theory pack so agents can read it via `omd pack dir` + /theory/
87
+ const theoryDir = join(root, 'core', 'theory');
88
+ if (existsSync(theoryDir)) {
89
+ for (const f of readdirSync(theoryDir).filter((f) => f.endsWith('.md'))) {
90
+ write(host, `core/theory/${f}`, readFileSync(join(theoryDir, f), 'utf8'));
91
+ }
92
+ }
93
+
94
+ // Copy motion pack (easing vocabulary + recipe cookbook) so agents can read it
95
+ // via `omd pack dir` + /motion/ and /motion/recipes/
96
+ const motionDir = join(root, 'core', 'motion');
97
+ if (existsSync(motionDir)) {
98
+ for (const f of readdirSync(motionDir).filter((f) => f.endsWith('.md'))) {
99
+ write(host, `core/motion/${f}`, readFileSync(join(motionDir, f), 'utf8'));
100
+ }
101
+ const recipesDir = join(motionDir, 'recipes');
102
+ if (existsSync(recipesDir)) {
103
+ for (const f of readdirSync(recipesDir).filter((f) => f.endsWith('.md'))) {
104
+ write(host, `core/motion/recipes/${f}`, readFileSync(join(recipesDir, f), 'utf8'));
105
+ }
106
+ }
107
+ }
108
+
109
+ // Copy composition pack (page-level composition recipes) so agents can read it
110
+ // via `omd pack dir` + /composition/
111
+ const compositionDir = join(root, 'core', 'composition');
112
+ if (existsSync(compositionDir)) {
113
+ for (const f of readdirSync(compositionDir).filter((f) => f.endsWith('.md'))) {
114
+ write(host, `core/composition/${f}`, readFileSync(join(compositionDir, f), 'utf8'));
115
+ }
116
+ }
117
+
118
+ // Copy graphics pack (background and image treatment recipes) so agents can read it
119
+ // via `omd pack dir` + /graphics/
120
+ const graphicsDir = join(root, 'core', 'graphics');
121
+ if (existsSync(graphicsDir)) {
122
+ for (const f of readdirSync(graphicsDir).filter((f) => f.endsWith('.md'))) {
123
+ write(host, `core/graphics/${f}`, readFileSync(join(graphicsDir, f), 'utf8'));
124
+ }
125
+ }
126
+
127
+ // Copy craft pack (finish-pass checklist and related craft material) so agents can read it
128
+ // via `omd pack dir` + /craft/
129
+ const craftDir = join(root, 'core', 'craft');
130
+ if (existsSync(craftDir)) {
131
+ for (const f of readdirSync(craftDir).filter((f) => f.endsWith('.md'))) {
132
+ write(host, `core/craft/${f}`, readFileSync(join(craftDir, f), 'utf8'));
133
+ }
134
+ }
135
+
136
+ // Copy the host-neutral loop contract alongside the theory/cookbook packs.
137
+ const protocolDir = join(root, 'core', 'protocol');
138
+ if (existsSync(protocolDir)) {
139
+ for (const f of readdirSync(protocolDir).filter((f) => f.endsWith('.md'))) {
140
+ write(host, `core/protocol/${f}`, readFileSync(join(protocolDir, f), 'utf8'));
141
+ }
142
+ }
143
+
144
+ console.log(`${host}: ${Object.keys(files).length} files, ${skills.length} skills`);
145
+ }
146
+
147
+ emitPlugin(agents, skills);
148
+ }
149
+
150
+ /**
151
+ * The marketplace flavor: written to <root>/skills, <root>/agents, <root>/.mcp.json —
152
+ * NOT dist/. This is what a marketplace install of the "omd" plugin actually loads, so
153
+ * unlike dist/ it must be committed. `join(root, 'skills')` is deliberately distinct from
154
+ * `join(root, 'src', 'skills')` (the source of truth, read by readSkills above) — this
155
+ * function must never wipe or write under src/.
156
+ */
157
+ function emitPlugin(agents: AbstractAgent[], skills: Skill[]): void {
158
+ for (const dir of ['skills', 'agents']) rmSync(join(root, dir), { recursive: true, force: true });
159
+ rmSync(join(root, '.mcp.json'), { force: true });
160
+
161
+ const pluginized = skills.map((skill) => pluginizeSkill(skill.source));
162
+ for (let i = 0; i < pluginized.length; i += 1) {
163
+ const { name, source } = pluginized[i]!;
164
+ const canonical = skills[i]!;
165
+ const path = join(root, 'skills', name, 'SKILL.md');
166
+ mkdirSync(dirname(path), { recursive: true });
167
+ writeFileSync(path, source);
168
+ const metadataPath = join(root, 'skills', name, 'agents', 'openai.yaml');
169
+ mkdirSync(dirname(metadataPath), { recursive: true });
170
+ writeFileSync(metadataPath, skillOpenaiMetadata(canonical.name, canonical.description));
171
+ }
172
+
173
+ const { files } = emitClaudePlugin({ agents });
174
+ for (const [rel, content] of Object.entries(files)) {
175
+ const path = join(root, rel);
176
+ mkdirSync(dirname(path), { recursive: true });
177
+ writeFileSync(path, typeof content === 'string' ? content : `${JSON.stringify(content, null, 2)}\n`);
178
+ }
179
+
180
+ console.log(`plugin (root): ${pluginized.length} skills, ${agents.length} agents`);
181
+ }
182
+
183
+ // Run when invoked directly (`node adapters/build.ts`), not when imported by install.ts.
184
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) build();
@@ -0,0 +1,111 @@
1
+ import { substituter } from './tokens.ts';
2
+ import type { AbstractAgent, Emitted } from '../core/types.ts';
3
+
4
+ const substitute = substituter('claude');
5
+
6
+ // Published server, not one we run: gives the model eyes (navigate/screenshot) without
7
+ // us hosting anything. Our own deterministic measurement stays in the `omd` CLI.
8
+ const MCP_SERVERS = {
9
+ mcpServers: {
10
+ 'chrome-devtools': {
11
+ command: 'npx',
12
+ args: ['-y', 'chrome-devtools-mcp@latest', '--headless', '--isolated'],
13
+ },
14
+ },
15
+ };
16
+
17
+ const yamlScalar = (s: string): string => JSON.stringify(s.replace(/\s*\n\s*/g, ' ').trim());
18
+
19
+ function emitAgentFile(agent: AbstractAgent): string {
20
+ const frontmatter = [
21
+ '---',
22
+ `name: ${agent.name}`,
23
+ `description: ${yamlScalar(agent.description)}`,
24
+ ];
25
+ if (agent.deny?.length) frontmatter.push(`disallowedTools: ${agent.deny.join(', ')}`);
26
+ frontmatter.push('---', '');
27
+ return `${frontmatter.join('\n')}\n${substitute(agent.instructions)}`;
28
+ }
29
+
30
+ export function emitClaude({ agents = [] }: { agents?: AbstractAgent[] } = {}): Emitted {
31
+ const files: Record<string, unknown> = {};
32
+
33
+
34
+ for (const agent of agents) files[`agents/${agent.name}.md`] = emitAgentFile(agent);
35
+
36
+ files['.mcp.json'] = MCP_SERVERS;
37
+
38
+ files['.claude-plugin/plugin.json'] = {
39
+ name: 'oh-my-design',
40
+ description: 'Design cognition loop — frame, diverge, see, reframe',
41
+ skills: './skills/',
42
+ agents: './agents/',
43
+ mcpServers: './.mcp.json',
44
+ };
45
+
46
+ return { files };
47
+ }
48
+
49
+ // ── Plugin flavor (repo root, marketplace-installed as `oh-my-design`) ──
50
+ //
51
+ // A marketplace plugin loads skills/agents under its own namespace: an agent installed
52
+ // from plugin "oh-my-design" is addressed as `oh-my-design:eye`, not `omd-eye`. The dist/
53
+ // emission above stays bare-name for direct/manual installs and must not change. This
54
+ // flavor is a second, parallel emission: same agents, same MCP server, but names stripped
55
+ // of their "omd-" prefix and cross-references rewritten to the "oh-my-design:" plugin form.
56
+
57
+ // "omd-scout" is both an agent and a skill name; either pattern maps it to
58
+ // "oh-my-design:scout" — harmless overlap, the contexts (subagent spawn vs. skill mention)
59
+ // differ. Agent pattern runs first only to keep the read order obvious; running skill
60
+ // pattern first would be equally correct since neither pattern's output can match the
61
+ // other's input. "omd-figma" is skill-only — it has no agent counterpart.
62
+ const AGENT_REF = /\bomd-(composer|framer|eye|glance|hand|scout|sketch|typesetter|writer)\b/g;
63
+ const SKILL_REF = /\bomd-(ultradesign|humanize|critique|coach|figma|scout)\b/g;
64
+
65
+ const pluginizeRefs = (text: string): string => text.replace(AGENT_REF, 'oh-my-design:$1').replace(SKILL_REF, 'oh-my-design:$1');
66
+
67
+ const stripOmdPrefix = (name: string): string => (name.startsWith('omd-') ? name.slice(4) : name);
68
+
69
+ function emitAgentFilePlugin(agent: AbstractAgent): string {
70
+ const frontmatter = [
71
+ '---',
72
+ `name: ${stripOmdPrefix(agent.name)}`,
73
+ `description: ${yamlScalar(pluginizeRefs(agent.description))}`,
74
+ ];
75
+ if (agent.deny?.length) frontmatter.push(`disallowedTools: ${agent.deny.join(', ')}`);
76
+ frontmatter.push('---', '');
77
+ return `${frontmatter.join('\n')}\n${pluginizeRefs(substitute(agent.instructions))}`;
78
+ }
79
+
80
+ export function emitClaudePlugin({ agents = [] }: { agents?: AbstractAgent[] } = {}): Emitted {
81
+ const files: Record<string, unknown> = {};
82
+
83
+ for (const agent of agents) files[`agents/${stripOmdPrefix(agent.name)}.md`] = emitAgentFilePlugin(agent);
84
+
85
+ files['.mcp.json'] = MCP_SERVERS;
86
+
87
+ return { files };
88
+ }
89
+
90
+ /**
91
+ * Rewrites one SKILL.md source for the plugin flavor: strips "omd-" from the skill's own
92
+ * `name:` frontmatter (its own identity, stripped bare — same treatment as an agent's own
93
+ * filename/frontmatter), then rewrites every cross-reference to another skill or agent to
94
+ * the "oh-my-design:" plugin form. Both steps run on the whole source, so a skill's
95
+ * description or body mentioning another skill or agent gets the plugin-resolvable form.
96
+ */
97
+ export function pluginizeSkill(source: string): { name: string; source: string } {
98
+ const nameLine = /^name:\s*(.+)$/m.exec(source);
99
+ const rawName = (nameLine?.[1] ?? '').trim().replace(/^["']|["']$/g, '');
100
+ const bareName = stripOmdPrefix(rawName);
101
+
102
+ let rewritten = source;
103
+ if (nameLine) {
104
+ rewritten = rewritten.slice(0, nameLine.index)
105
+ + `name: ${bareName}`
106
+ + rewritten.slice(nameLine.index + nameLine[0].length);
107
+ }
108
+ rewritten = pluginizeRefs(rewritten);
109
+
110
+ return { name: bareName, source: rewritten };
111
+ }
@@ -0,0 +1,68 @@
1
+ import { substituter } from './tokens.ts';
2
+ import type { AbstractAgent, Emitted } from '../core/types.ts';
3
+
4
+ const substitute = substituter('codex');
5
+
6
+ // Published server, not one we run: gives the model eyes (navigate/screenshot) without
7
+ // us hosting anything. Our own deterministic measurement stays in the `omd` CLI.
8
+ const MCP_SERVERS = {
9
+ mcpServers: {
10
+ 'chrome-devtools': {
11
+ command: 'npx',
12
+ args: ['-y', 'chrome-devtools-mcp@latest', '--headless', '--isolated'],
13
+ },
14
+ },
15
+ };
16
+
17
+ const tomlBasic = (s: string): string =>
18
+ `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
19
+
20
+ function tomlMultiline(s: string): string {
21
+ const body = s.replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
22
+ return `"""\n${body.endsWith('\n') ? body : `${body}\n`}"""`;
23
+ }
24
+
25
+
26
+ /**
27
+ * Codex agent TOML has no tool-restriction key — only name, description, model,
28
+ * model_reasoning_effort, developer_instructions, nickname_candidates, service_tier.
29
+ * Denial is therefore prose here, not policy, exactly as Codex's own read-only agents do
30
+ * it. Claude Code enforces the same list declaratively via disallowedTools.
31
+ */
32
+ function denialProse(deny: string[] | undefined): string {
33
+ if (!deny?.length) return '';
34
+ return `\n\nHARD CONSTRAINT: you have no write access. You must never call ${deny.join(', ')}, `
35
+ + 'and you must never ask another agent to write on your behalf. If a change is needed, '
36
+ + 'describe it and stop.\n';
37
+ }
38
+
39
+ const emitAgentFile = (agent: AbstractAgent): string => [
40
+ `name = ${tomlBasic(agent.name)}`,
41
+ `description = ${tomlBasic(agent.description)}`,
42
+ `model_reasoning_effort = ${tomlBasic(agent.reasoning)}`,
43
+ `developer_instructions = ${tomlMultiline(`${substitute(agent.instructions)}${denialProse(agent.deny)}`)}`,
44
+ '',
45
+ ].join('\n');
46
+
47
+ export function emitCodex({ agents = [], version = '0.0.0' }: { agents?: AbstractAgent[]; version?: string } = {}): Emitted {
48
+ const files: Record<string, unknown> = {};
49
+
50
+ for (const agent of agents) files[`agents/${agent.name}.toml`] = emitAgentFile(agent);
51
+
52
+ files['.mcp.json'] = MCP_SERVERS;
53
+
54
+ files['.codex-plugin/plugin.json'] = {
55
+ name: 'oh-my-design',
56
+ version,
57
+ description: 'Design cognition loop — frame, diverge, see, reframe',
58
+ skills: './skills/',
59
+ mcpServers: './.mcp.json',
60
+ interface: {
61
+ displayName: 'Oh My Design',
62
+ shortDescription: 'Design cognition loop — frame, diverge, see, reframe',
63
+ capabilities: ['Hooks', 'Skills', 'Subagents', 'Design Lint'],
64
+ },
65
+ };
66
+
67
+ return { files };
68
+ }
@@ -0,0 +1,21 @@
1
+ import toolMap from './tool-map.json' with { type: 'json' };
2
+ import type { Host } from '../core/types.ts';
3
+
4
+ interface ToolMap {
5
+ fileWrite: Record<Host, string>;
6
+ pluginRoot: Record<Host, string>;
7
+ }
8
+
9
+ const map = toolMap as unknown as ToolMap;
10
+
11
+ export function substituter(host: Host): (value: string) => string {
12
+ const tokens: Record<string, string> = {
13
+ '@fileWrite': map.fileWrite[host],
14
+ '@pluginRoot': map.pluginRoot[host],
15
+ };
16
+ return (value: string): string => {
17
+ let out = value;
18
+ for (const [token, replacement] of Object.entries(tokens)) out = out.split(token).join(replacement);
19
+ return out;
20
+ };
21
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "fileWrite": { "codex": "apply_patch", "claude": "Write|Edit" },
3
+ "shell": { "codex": "Bash", "claude": "Bash" },
4
+ "pluginRoot": { "codex": "${PLUGIN_ROOT}", "claude": "$CLAUDE_PLUGIN_ROOT" },
5
+ "agentFormat": { "codex": "toml", "claude": "md" },
6
+ "hookLayout": { "codex": "file-per-hook", "claude": "single-file" }
7
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { detectHosts } from '../core/install/detect.ts';
6
+ import { install, uninstall, doctor } from '../core/install/install.ts';
7
+
8
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
9
+
10
+ function usage(): never {
11
+ console.error(
12
+ 'usage: oh-my-design <command>\n\n'
13
+ + ' install copy the plugin into every detected host and patch its config\n'
14
+ + ' uninstall reverse exactly what install did (never touches .omd/)\n'
15
+ + ' doctor verify the install is healthy; exits 1 if anything is broken\n'
16
+ + ' --version print the oh-my-design package version',
17
+ );
18
+ process.exit(1);
19
+ }
20
+
21
+ async function main(): Promise<void> {
22
+ const [cmd] = process.argv.slice(2);
23
+
24
+ if (cmd === '--version') {
25
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { version: string };
26
+ console.log(pkg.version);
27
+ return;
28
+ }
29
+
30
+ if (cmd !== 'install' && cmd !== 'uninstall' && cmd !== 'doctor') usage();
31
+
32
+ const args = process.argv.slice(3);
33
+ const hostArg = args.find((a) => a.startsWith('--host'))?.split('=')[1] ?? args[args.indexOf('--host') + 1];
34
+
35
+ let hosts = detectHosts();
36
+ if (hostArg) {
37
+ hosts = hosts.filter((h) => h.host === hostArg);
38
+ if (hosts.length === 0) {
39
+ console.error(`No such host detected: ${hostArg}`);
40
+ process.exit(1);
41
+ }
42
+ }
43
+ if (hosts.length === 0) {
44
+ console.error('No supported host found: neither ~/.claude nor ~/.codex exists.');
45
+ process.exit(1);
46
+ }
47
+
48
+ if (cmd === 'install') {
49
+ for (const line of install(hosts)) console.log(line);
50
+ console.log(`\nInstalled for: ${hosts.map((h) => h.host).join(', ')}.`);
51
+ console.log('Open a session there and type /ultradesign.');
52
+ return;
53
+ }
54
+
55
+ if (cmd === 'uninstall') {
56
+ for (const line of uninstall(hosts)) console.log(line);
57
+ return;
58
+ }
59
+
60
+ // doctor
61
+ const results = doctor(hosts);
62
+ let failed = false;
63
+ for (const r of results) {
64
+ console.log(`${r.host}:`);
65
+ for (const c of r.checks) {
66
+ console.log(` [${c.ok ? 'ok' : 'FAIL'}] ${c.name}${c.detail ? ` — ${c.detail}` : ''}`);
67
+ if (!c.ok) failed = true;
68
+ }
69
+ }
70
+ process.exit(failed ? 1 : 0);
71
+ }
72
+
73
+ main().catch((err: unknown) => {
74
+ console.error(err instanceof Error ? err.message : String(err));
75
+ process.exit(1);
76
+ });