foundry-design 0.2.0-beta.1 → 0.2.0-beta.10

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/README.md CHANGED
@@ -4,35 +4,82 @@ Foundry is a local-first precision design workbench for Codex, Cursor, and Claud
4
4
 
5
5
  ## Beta installation
6
6
 
7
- Foundry `0.2.0-beta.1` is the first shareable beta. From the project you want to inspect, run:
7
+ Foundry is distributed through npm, so testers do not need GitHub access.
8
+
9
+ > **Current public beta:** `0.2.0-beta.10`. Both npm `latest` and `beta` point to the same tested release. Use the unqualified command below for normal installation.
10
+
11
+ Full documentation is available at [withfoundry.ai](https://withfoundry.ai).
12
+
13
+ ### Recommended: one command
14
+
15
+ Open a terminal in the project, or ask Codex, Cursor, or Claude Code to run:
16
+
17
+ ```bash
18
+ npx foundry-design
19
+ ```
20
+
21
+ That command detects the project and active coding agent, installs or safely updates Foundry, repairs the shared agent connection, validates its integration, starts the project when needed, and opens the visual session. Before changing local configuration, Foundry prints the exact CLI and MCP bridge versions it will use. On the first installation only, restart the coding agent once so it can load the shared MCP connection. A reviewed batch can be queued before the restart and will be claimed when the agent reconnects.
22
+
23
+ After the restart, reopen the same project folder and ask:
24
+
25
+ ```text
26
+ Start Foundry for this project and keep listening for Apply with agent requests.
27
+ ```
28
+
29
+ That is the normal workflow. Foundry resumes the most recent session for the current source revision and keeps all review and apply state local.
30
+
31
+ ### Manual installation
32
+
33
+ From the first project you want to inspect, run:
8
34
 
9
35
  ```bash
10
- npx foundry-design@beta setup
36
+ npx foundry-design setup --global
37
+ ```
38
+
39
+ For later projects, keep the shared agent connection and install only the development adapter:
40
+
41
+ ```bash
42
+ npx foundry-design setup --agent none
43
+ ```
44
+
45
+ Then restart the coding agent, reopen the same project folder, and ask:
46
+
47
+ ```text
48
+ Start Foundry for this project and keep listening for Apply with agent requests.
11
49
  ```
12
50
 
13
- Setup detects the platform, framework, package manager, development command, and installed coding agents. It previews every file it will manage, adds development-only instrumentation for supported web frameworks, configures the selected MCP clients, and records a reversible install manifest.
51
+ You do not need to run a second terminal command when the agent starts Foundry for you. Apply requests remain queued safely when the agent is offline. Run `npx foundry-design doctor --repair` if the connection or generated integration needs repair.
14
52
 
15
- Then start a session with:
53
+ Update an existing installation with:
16
54
 
17
55
  ```bash
18
- npx foundry-design@beta start
56
+ npx foundry-design update
19
57
  ```
20
58
 
21
- Foundry reuses an available project server or starts the detected development command, launches the loopback runtime, and opens an authenticated visual preview. Remove only Foundry-managed integration with:
59
+ The updater refreshes checksum-matched Foundry files, agent connections, and skill bundles. It preserves and reports files changed since setup, validates the project, and rolls back the update if Foundry introduces a TypeScript or lint failure. Restart the coding agent after updating.
60
+
61
+ Remove only Foundry-managed integration with:
22
62
 
23
63
  ```bash
24
- npx foundry-design@beta uninstall
64
+ npx foundry-design uninstall
25
65
  ```
26
66
 
27
67
  Supported automatic web integration currently includes Next.js App Router, Vite, and plain HTML. Generic web, SwiftUI, and React Native projects receive explicit setup guidance when a safe automatic edit is not available.
28
68
 
29
- Codex, Cursor, and Claude Code users can alternatively install the bundled plugin from this repository. The plugin contains the Foundry skill and MCP server definition, so project setup uses `--agent none` and does not create duplicate MCP configuration.
69
+ The npm setup above is the public beta installation path and includes the same portable skill and MCP connection used by the agent plugin bundle.
70
+
71
+ If an npm mirror or existing `npx` cache reports an old tag, bypass it with a temporary cache and the exact current release:
72
+
73
+ ```bash
74
+ FOUNDRY_NPX_CACHE="$(mktemp -d)"
75
+ npx --yes --prefer-online --registry=https://registry.npmjs.org --cache "$FOUNDRY_NPX_CACHE" --package=foundry-design@0.2.0-beta.10 foundry-design
76
+ ```
30
77
 
31
- The beta supports Node.js 20 or newer. Read [Privacy](PRIVACY.md), [Security](SECURITY.md), and the [beta changelog](CHANGELOG.md) before using it with sensitive work.
78
+ The beta supports Node.js 20 or newer. Read the [local-first safety model](https://withfoundry.ai/#safety) before using it with sensitive work.
32
79
 
33
80
  ## Agent plugin
34
81
 
35
- The portable plugin lives at `plugins/foundry-design-control`. The repository also includes marketplace manifests for Codex, Cursor, and Claude-compatible plugin import. Once installed, ask your agent:
82
+ The portable plugin lives at `plugins/foundry-design-control`. The repository also includes marketplace manifests for Codex, Cursor, and Claude-compatible plugin import. Until public marketplace review is complete, `setup --global` installs the same reusable skill and MCP connection from npm without requiring source-repository access. Once installed, ask your agent:
36
83
 
37
84
  ```text
38
85
  Start Foundry for this project.
@@ -42,7 +89,8 @@ Foundry remains local-first. Installing the plugin does not create an account, e
42
89
 
43
90
  ## Current capabilities
44
91
 
45
- - Measured browser selection with overlap cycling, parent/child traversal, hover previews, and an isolated Shadow DOM inspector
92
+ - A dedicated local design workspace with a live product canvas, equal Layers and Inspector docks, and a direct-overlay fallback when a product blocks framing
93
+ - Measured browser selection with overlap cycling, parent/child traversal, hover previews, and isolated Shadow DOM canvas instrumentation
46
94
  - A searchable, collapsible, virtualized live layer hierarchy with mapped-source context, Shadow DOM traversal, drag reordering, and selection persistence across HMR
47
95
  - Multi-selection, measured spacing annotations, sibling-aware resize snapping, keyboard nudging, alignment, distribution, and preview undo/redo
48
96
  - A revisioned local design graph for CSS tokens, components, Storybook variants, breakpoints, themes, states, and motion presets
@@ -54,9 +102,11 @@ Foundry remains local-first. Installing the plugin does not create an account, e
54
102
  - Design Health scans for contrast, overflow and clipping, touch targets, accessible names, reduced-motion coverage, and project spacing consistency, with evidence, intentional-issue ignores, and safe corrections routed into review
55
103
  - An in-app state workbench with real viewport frames, theme switching, forced interaction states, motion controls, and contextual verification
56
104
  - Persistent, coalescing change ledger with JSON and consolidated prompt export
57
- - Compact in-preview review with editable approved batches and unresolved-target blocking
105
+ - Center-workspace review with editable approved batches, grouped targets, and unresolved-target blocking
58
106
  - Persistent Apply with agent runs across Codex, Cursor, and Claude Code through MCP
107
+ - Leased agent handoffs that safely return abandoned or interrupted claims to the queue
59
108
  - Live source, rebuild, validation, retry, and rendered-verification progress
109
+ - Rendered verification that resumes after refresh and does not depend on Review remaining open
60
110
  - Local MCP bridge for agent access
61
111
  - Debug adapters for web, SwiftUI, and React Native on iOS Simulator
62
112
  - Verification records that compare requested and rendered values
@@ -90,7 +140,7 @@ The runtime binds to `127.0.0.1:4387`. Inspector preview changes remain temporar
90
140
  - `packages/react-native-adapter`: React Native debug bridge
91
141
  - `packages/mcp-server`: agent tools backed by the runtime
92
142
  - `packages/swiftui-adapter`: Swift package for inspectable SwiftUI views
93
- - `apps/inspector`: session dashboard and review surface
143
+ - `apps/inspector`: authenticated local design workspace, canvas shell, and review surface
94
144
  - `examples/web-fixture`: end-to-end browser fixture
95
145
 
96
146
  ## Safety model
package/dist/index.js CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
- import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
5
- import { basename, dirname, join, resolve } from 'node:path';
4
+ import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import { basename, join, resolve } from 'node:path';
6
6
  import { createInterface } from 'node:readline/promises';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { homedir } from 'node:os';
8
9
  import { renderChangePrompt } from 'foundry-design-protocol';
9
10
  import { FoundryRuntime, SessionStore } from 'foundry-design-runtime';
10
- import { createSetupPlan, setupProject, uninstallProject, } from './installer.js';
11
+ import { createSetupPlan, createUpdatePlan, installHostAgentIntegration, installAgentIntegration, setupProject, updateProject, uninstallProject, } from './installer.js';
11
12
  import { addSessionParams, detectPlatform } from './project.js';
12
13
  import { indexProjectDesign } from './indexer.js';
14
+ import { startBasicPreviewProxy } from './proxy.js';
15
+ import { FOUNDRY_VERSION, releasePreflight } from './release.js';
13
16
  const args = process.argv.slice(2);
14
- const command = args[0] ?? 'help';
17
+ const command = args[0]?.startsWith('-') ? 'launch' : (args[0] ?? 'launch');
15
18
  const runtimeRepository = resolve(fileURLToPath(new URL('../../../', import.meta.url)));
16
19
  function flag(name) {
17
20
  const index = args.indexOf(name);
@@ -27,17 +30,24 @@ function printHelp() {
27
30
  console.log(`Foundry Design Control
28
31
 
29
32
  Usage:
30
- foundry-design setup [--project PATH] [--agent codex,cursor,claude] [--url URL] [--yes]
33
+ foundry-design [--project PATH] [--url URL] [--yes] [--no-start]
34
+ foundry-design setup [--project PATH] [--agent codex,cursor,claude] [--global] [--url URL] [--yes]
35
+ foundry-design update [--project PATH] [--agent codex,cursor,claude] [--yes]
31
36
  foundry-design init <web|swiftui|react-native> [--project PATH]
32
- foundry-design start [--project PATH] [--url URL] [--platform PLATFORM] [--no-open] [--no-dev]
33
- foundry-design doctor [--project PATH]
37
+ foundry-design start [--project PATH] [--url URL] [--platform PLATFORM] [--new] [--no-open] [--no-dev]
38
+ foundry-design doctor [--project PATH] [--repair]
34
39
  foundry-design index [--project PATH] [--output FILE]
35
40
  foundry-design uninstall [--project PATH] [--yes]
36
41
  foundry-design export <SESSION_ID> [--format json|prompt|full] [--output FILE]
37
- foundry-design install-agent <cursor|claude|codex> [--project PATH]
42
+ foundry-design install-agent <cursor|claude|codex> [--global | --project PATH]
38
43
 
39
44
  Foundry is local-only and never edits product source from inspector controls.`);
40
45
  }
46
+ async function pathExists(path) {
47
+ return access(path)
48
+ .then(() => true)
49
+ .catch(() => false);
50
+ }
41
51
  async function gitOutput(root, gitArgs) {
42
52
  return new Promise((resolveRevision) => {
43
53
  const child = spawn('git', gitArgs, {
@@ -106,9 +116,15 @@ function requestedAgents() {
106
116
  return [...new Set(agents)];
107
117
  }
108
118
  async function setup() {
119
+ console.log(`${releasePreflight('set up this project')}\n`);
109
120
  const root = projectRoot();
121
+ const requested = requestedAgents();
122
+ const sharedAgentSetup = has('--global');
123
+ const detectedPlan = sharedAgentSetup
124
+ ? await createSetupPlan(root, { agents: requested })
125
+ : undefined;
110
126
  const options = {
111
- agents: requestedAgents(),
127
+ agents: sharedAgentSetup ? [] : requested,
112
128
  targetUrl: flag('--url'),
113
129
  packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
114
130
  };
@@ -116,7 +132,11 @@ async function setup() {
116
132
  console.log(`Foundry setup\n\nProject: ${root}\nPlatform: ${plan.platform}`);
117
133
  if (plan.framework)
118
134
  console.log(`Framework: ${plan.framework}`);
119
- console.log(`Agent integration: ${plan.agents.length ? plan.agents.join(', ') : 'plugin-provided'}`);
135
+ console.log(`Agent integration: ${sharedAgentSetup
136
+ ? `${detectedPlan?.agents.join(', ')} (shared across projects)`
137
+ : plan.agents.length
138
+ ? plan.agents.join(', ')
139
+ : 'plugin-provided'}`);
120
140
  console.log('\nFiles Foundry will manage:');
121
141
  for (const path of plan.files)
122
142
  console.log(` ${path}`);
@@ -128,14 +148,71 @@ async function setup() {
128
148
  return;
129
149
  }
130
150
  const result = await setupProject(root, options);
151
+ if (sharedAgentSetup) {
152
+ for (const agent of detectedPlan?.agents ?? []) {
153
+ await installHostAgentIntegration(homedir(), agent, {
154
+ packageRoot: options.packageRoot,
155
+ });
156
+ }
157
+ }
131
158
  console.log(`\n✓ Foundry configured ${result.changed.length} files.`);
159
+ for (const check of result.validation) {
160
+ const marker = check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!';
161
+ console.log(`${marker} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
162
+ }
163
+ if (result.skillDirectories.length) {
164
+ console.log(`✓ Installed the Foundry skill for ${result.agents.join(', ')}.`);
165
+ }
166
+ if (sharedAgentSetup && detectedPlan?.agents.length) {
167
+ console.log(`✓ Installed the shared Foundry connection for ${detectedPlan.agents.join(', ')}.`);
168
+ }
132
169
  if (result.targetUrl)
133
170
  console.log(`✓ Preview URL: ${result.targetUrl}`);
134
171
  if (result.devCommand)
135
172
  console.log(`✓ Development command: ${result.devCommand.command} ${result.devCommand.args.join(' ')}`);
136
- console.log('\nNext: foundry-design start');
173
+ console.log('\nSetup complete. One coding-agent restart is required so it can load Foundry.');
174
+ console.log(`1. Restart ${sharedAgentSetup && detectedPlan?.agents.length
175
+ ? detectedPlan.agents.join(', ')
176
+ : result.agents.length
177
+ ? result.agents.join(', ')
178
+ : 'your coding agent'}.`);
179
+ console.log('2. Reopen this exact project folder.');
180
+ console.log('3. Ask: "Start Foundry for this project and keep listening for Apply with agent requests."');
181
+ console.log('\nAfter that, the browser will confirm when the agent is ready.');
182
+ }
183
+ async function update() {
184
+ console.log(`${releasePreflight('update this project')}\n`);
185
+ const root = projectRoot();
186
+ const options = {
187
+ agents: requestedAgents(),
188
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
189
+ };
190
+ const plan = await createUpdatePlan(root, options);
191
+ console.log(`Foundry update\n\nProject: ${root}`);
192
+ console.log(`Agent integration: ${plan.agents.join(', ')}`);
193
+ console.log('\nFoundry will refresh these managed paths:');
194
+ for (const path of plan.files)
195
+ console.log(` ${path}`);
196
+ console.log('\nFiles changed since Foundry installed them will be preserved.');
197
+ if (!(await confirm('\nContinue with this update?'))) {
198
+ console.log('Update cancelled.');
199
+ return;
200
+ }
201
+ const result = await updateProject(root, options);
202
+ console.log(`\n✓ Foundry refreshed ${result.changed.length} managed paths.`);
203
+ if (result.preserved.length) {
204
+ console.log('Preserved files containing user changes:');
205
+ for (const path of result.preserved)
206
+ console.log(` ${path}`);
207
+ }
208
+ for (const check of result.validation) {
209
+ const marker = check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!';
210
+ console.log(`${marker} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
211
+ }
212
+ console.log('\nRestart your coding agent so it loads the refreshed Foundry connection and skill.');
137
213
  }
138
214
  async function initProject() {
215
+ console.log(`${releasePreflight('initialize this project')}\n`);
139
216
  const platform = (args[1] ?? 'web');
140
217
  if (!['web', 'swiftui', 'react-native'].includes(platform))
141
218
  throw new Error(`Unsupported platform: ${platform}`);
@@ -192,9 +269,7 @@ async function start() {
192
269
  config?.platform ??
193
270
  (await detectPlatform(root));
194
271
  const targetUrl = flag('--url') ?? config?.targetUrl;
195
- if (platform === 'web' && config && !config.instrumented) {
196
- throw new Error('Foundry setup is incomplete. Connect .foundry/web-adapter.ts to a development-only client entry, then set instrumented to true.');
197
- }
272
+ let basicPreview;
198
273
  let developmentServer;
199
274
  if (platform === 'web' &&
200
275
  targetUrl &&
@@ -220,54 +295,88 @@ async function start() {
220
295
  }
221
296
  }
222
297
  const projectRevision = await revision(root);
298
+ const configuredViewports = config?.design?.viewports ?? [];
299
+ const initialViewport = configuredViewports.find((viewport) => viewport.id.toLowerCase() === 'desktop' || viewport.label.toLowerCase() === 'desktop') ??
300
+ [...configuredViewports].sort((left, right) => right.width - left.width)[0] ?? {
301
+ width: 1440,
302
+ height: 900,
303
+ };
223
304
  const context = {
224
305
  projectRoot: root,
225
306
  revision: projectRevision,
226
307
  platform,
227
308
  targetUrl,
228
309
  targetName: basename(root),
310
+ viewport: { width: initialViewport.width, height: initialViewport.height ?? 900 },
229
311
  theme: 'system',
230
312
  breakpoint: 'current',
231
313
  state: 'current',
232
314
  };
233
315
  const store = new SessionStore();
234
- const session = await store.create(context);
316
+ const resumable = has('--new')
317
+ ? undefined
318
+ : (await store.list()).find((candidate) => candidate.changeSet.context.projectRoot === root &&
319
+ candidate.changeSet.context.revision === projectRevision &&
320
+ candidate.changeSet.context.targetUrl === targetUrl);
321
+ const session = resumable
322
+ ? await store.read(resumable.changeSet.sessionId)
323
+ : await store.create(context);
235
324
  if (platform === 'web') {
236
325
  const graph = await indexProjectDesign(root, config, projectRevision);
237
326
  await store.setDesignGraph(session.changeSet.sessionId, graph);
238
327
  console.log(`Indexed ${graph.tokens.length} tokens, ${graph.components.length} components, and ${graph.breakpoints.length} viewports.`);
239
328
  }
240
329
  const runtime = new FoundryRuntime({ store });
330
+ let ownsRuntime = false;
241
331
  try {
242
332
  await runtime.start();
333
+ ownsRuntime = true;
243
334
  }
244
335
  catch (error) {
245
336
  if (error.code !== 'EADDRINUSE')
246
337
  throw error;
247
- developmentServer?.kill('SIGTERM');
248
- console.error('Port 4387 is already in use. Stop the existing Foundry runtime before starting another session.');
249
- process.exitCode = 1;
250
- return;
338
+ const healthy = await fetch('http://127.0.0.1:4387/v1/health')
339
+ .then((response) => response.ok)
340
+ .catch(() => false);
341
+ if (!healthy)
342
+ throw new Error('Port 4387 is in use by another process. Stop it and run Foundry again.');
343
+ console.log('Using the existing local Foundry runtime.');
251
344
  }
252
345
  const reviewUrl = `http://127.0.0.1:4387/?session=${encodeURIComponent(session.changeSet.sessionId)}&token=${encodeURIComponent(session.token)}`;
346
+ if (platform === 'web' && targetUrl && config && !config.instrumented) {
347
+ basicPreview = await startBasicPreviewProxy(targetUrl, config.runtimeUrl);
348
+ console.log('Basic mode: Foundry is attached without changing the project entry point.');
349
+ console.log('Run setup again after adding a supported client entry to enable exact source mapping.');
350
+ }
253
351
  const productUrl = targetUrl && platform === 'web'
254
- ? addSessionParams(targetUrl, session.changeSet.sessionId, session.token)
352
+ ? addSessionParams(basicPreview?.url ?? targetUrl, session.changeSet.sessionId, session.token)
353
+ : reviewUrl;
354
+ const workspaceUrl = platform === 'web' && targetUrl
355
+ ? `${reviewUrl}&preview=${encodeURIComponent(productUrl)}`
255
356
  : reviewUrl;
256
357
  console.log(`Foundry session ${session.changeSet.sessionId}`);
358
+ if (resumable)
359
+ console.log('Resumed the most recent session for this project revision.');
257
360
  console.log(`Platform: ${platform}`);
258
- console.log(`Inspector: ${reviewUrl}`);
361
+ console.log(`Workspace: ${workspaceUrl}`);
259
362
  if (targetUrl && platform === 'web')
260
- console.log(`Instrumented preview: ${productUrl}`);
363
+ console.log(`${basicPreview ? 'Basic' : 'Precision'} preview: ${productUrl}`);
261
364
  if (platform === 'web') {
262
365
  console.log('\nFirst edit:');
263
366
  console.log(' 1. Option-click an element. Shift-click to add more.');
264
367
  console.log(' 2. Drag a blue handle or use the inspector for exact values.');
265
368
  console.log(' 3. Review the semantic mapping, then Apply with agent.');
369
+ console.log('\nAgent handoff:');
370
+ console.log(' Keep this process running while you design.');
371
+ console.log(' Foundry will show “Agent is ready” only while a coding agent is listening.');
372
+ console.log(' If it is not ready, ask the agent: “Keep listening for Apply with agent requests.”');
266
373
  }
267
374
  if (!has('--no-open'))
268
- await openUrl(productUrl);
375
+ await openUrl(workspaceUrl);
269
376
  const shutdown = async () => {
270
- await runtime.stop();
377
+ if (ownsRuntime)
378
+ await runtime.stop();
379
+ await basicPreview?.stop();
271
380
  developmentServer?.kill('SIGTERM');
272
381
  process.exit(0);
273
382
  };
@@ -291,6 +400,7 @@ async function indexDesign() {
291
400
  console.log(content);
292
401
  }
293
402
  async function uninstall() {
403
+ console.log(`${releasePreflight('remove managed project integration')}\n`);
294
404
  const root = projectRoot();
295
405
  if (!(await confirm(`Remove Foundry-managed project integration from ${root}?`))) {
296
406
  console.log('Uninstall cancelled.');
@@ -309,32 +419,39 @@ async function doctor() {
309
419
  const platform = await detectPlatform(root);
310
420
  const configPath = join(root, '.foundry', 'foundry.config.json');
311
421
  let config;
422
+ const manifest = await readFile(join(root, '.foundry', 'install-manifest.json'), 'utf8')
423
+ .then((content) => JSON.parse(content))
424
+ .catch(() => undefined);
312
425
  try {
313
426
  config = JSON.parse(await readFile(configPath, 'utf8'));
314
427
  }
315
428
  catch {
316
429
  /* Report setup as missing below. */
317
430
  }
318
- const agentFiles = [
431
+ const projectAgentFiles = [
319
432
  join(root, '.codex', 'config.toml'),
320
433
  join(root, '.cursor', 'mcp.json'),
321
434
  join(root, '.mcp.json'),
322
435
  ];
436
+ const sharedAgentFiles = [
437
+ join(homedir(), '.codex', 'config.toml'),
438
+ join(homedir(), '.cursor', 'mcp.json'),
439
+ join(homedir(), '.claude.json'),
440
+ ];
323
441
  const configuredAgents = [];
324
- for (const path of agentFiles) {
442
+ for (const path of [...projectAgentFiles, ...sharedAgentFiles]) {
325
443
  if ((await readFile(path, 'utf8').catch(() => '')).includes('foundry-design-control'))
326
444
  configuredAgents.push(path);
327
445
  }
328
- const pluginProvided = await readFile(join(root, '.foundry', 'install-manifest.json'), 'utf8')
329
- .then((content) => {
330
- const manifest = JSON.parse(content);
331
- return Array.isArray(manifest.agents) && manifest.agents.length === 0;
332
- })
333
- .catch(() => false);
334
446
  const checks = [
335
447
  ['Project', root, true],
336
448
  ['Detected platform', platform, true],
337
449
  ['Foundry config', configPath, Boolean(config)],
450
+ [
451
+ 'Managed integration',
452
+ manifest?.generatorVersion ?? 'not installed',
453
+ manifest?.generatorVersion === FOUNDRY_VERSION,
454
+ ],
338
455
  [
339
456
  'Instrumentation',
340
457
  config?.instrumented ? `${config.framework ?? config.platform}` : 'integration pending',
@@ -342,12 +459,8 @@ async function doctor() {
342
459
  ],
343
460
  [
344
461
  'Agent connection',
345
- configuredAgents.length
346
- ? configuredAgents.join(', ')
347
- : pluginProvided
348
- ? 'plugin-provided'
349
- : 'not configured',
350
- configuredAgents.length > 0 || pluginProvided,
462
+ configuredAgents.length ? configuredAgents.join(', ') : 'not configured',
463
+ configuredAgents.length > 0,
351
464
  ],
352
465
  [
353
466
  'Project preview',
@@ -364,8 +477,49 @@ async function doctor() {
364
477
  ];
365
478
  for (const [label, value, passed] of checks)
366
479
  console.log(`${passed ? '✓' : '○'} ${label}: ${value}`);
367
- if (!config)
368
- console.log('Run: foundry-design setup');
480
+ if (has('--repair')) {
481
+ console.log(`\n${releasePreflight('repair this project and agent connection')}\n`);
482
+ const detectedAgents = (await createSetupPlan(root)).agents;
483
+ const result = config
484
+ ? await updateProject(root, { agents: [] })
485
+ : await setupProject(root, { agents: [], targetUrl: flag('--url') });
486
+ for (const agent of detectedAgents)
487
+ await installHostAgentIntegration(homedir(), agent, {
488
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
489
+ });
490
+ console.log(`✓ Repaired ${result.changed.length} managed paths and the shared ${detectedAgents.join(', ')} connection.`);
491
+ }
492
+ else if (!config || !configuredAgents.length) {
493
+ console.log('Run: foundry-design doctor --repair');
494
+ }
495
+ }
496
+ async function launch() {
497
+ console.log(`${releasePreflight('install, update, and start Foundry')}\n`);
498
+ const root = projectRoot();
499
+ const manifest = join(root, '.foundry', 'install-manifest.json');
500
+ const installed = await pathExists(manifest);
501
+ const detectedAgents = (await createSetupPlan(root)).agents;
502
+ console.log(`Foundry\n\nProject: ${root}`);
503
+ console.log(`${installed ? 'Refreshing' : 'Preparing'} the project and shared ${detectedAgents.join(', ')} connection.`);
504
+ if (!(await confirm('Continue?'))) {
505
+ console.log('Foundry cancelled.');
506
+ return;
507
+ }
508
+ const result = installed
509
+ ? await updateProject(root, { agents: [], targetUrl: flag('--url') })
510
+ : await setupProject(root, { agents: [], targetUrl: flag('--url') });
511
+ for (const agent of detectedAgents)
512
+ await installHostAgentIntegration(homedir(), agent, {
513
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
514
+ });
515
+ console.log(`✓ ${installed ? 'Updated' : 'Installed'} and validated Foundry.`);
516
+ for (const check of result.validation)
517
+ console.log(`${check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!'} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
518
+ if (!installed)
519
+ console.log(`\nRestart ${detectedAgents.join(', ')} once to load the shared connection. The visual session will open now and queued batches will wait safely until it reconnects.`);
520
+ if (has('--no-start'))
521
+ return;
522
+ await start();
369
523
  }
370
524
  async function exportSession() {
371
525
  const id = args[1];
@@ -392,39 +546,41 @@ async function exportSession() {
392
546
  console.log(content);
393
547
  }
394
548
  async function installAgent() {
549
+ console.log(`${releasePreflight('install the coding-agent connection')}\n`);
395
550
  const agent = args[1];
396
551
  if (!agent || !['cursor', 'claude', 'codex'].includes(agent))
397
552
  throw new Error('install-agent requires cursor, claude, or codex');
398
- const root = projectRoot();
399
- const server = {
400
- command: 'pnpm',
401
- args: ['--dir', runtimeRepository, '--filter', 'foundry-design-mcp-server', 'start'],
402
- env: { FOUNDRY_DESIGN_RUNTIME_URL: 'http://127.0.0.1:4387' },
403
- };
404
- if (agent === 'codex') {
405
- const directory = join(root, '.codex');
406
- await mkdir(directory, { recursive: true });
407
- const file = join(directory, 'foundry-mcp.toml');
408
- await writeFile(file, `# Merge this project-scoped server into your Codex MCP configuration.\n[mcp_servers.foundry-design-control]\ncommand = "pnpm"\nargs = ["--dir", "${runtimeRepository}", "--filter", "foundry-design-mcp-server", "start"]\n[mcp_servers.foundry-design-control.env]\nFOUNDRY_DESIGN_RUNTIME_URL = "http://127.0.0.1:4387"\n`);
409
- console.log(`Wrote Codex MCP snippet: ${file}`);
553
+ if (has('--global')) {
554
+ const result = await installHostAgentIntegration(homedir(), agent, {
555
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
556
+ });
557
+ console.log(`Installed the shared Foundry connection for ${agent}.`);
558
+ console.log(`MCP configuration: ${result.configFile}`);
559
+ console.log(`Skill: ${result.skillDirectory}`);
560
+ if (result.preserved.length) {
561
+ console.log('Preserved customized skill files:');
562
+ for (const path of result.preserved)
563
+ console.log(` ${path}`);
564
+ }
565
+ console.log(`Restart ${agent === 'codex' ? 'Codex' : agent === 'cursor' ? 'Cursor' : 'Claude Code'} once. Future projects can use "npx foundry-design setup --agent none --yes" without another MCP install.`);
410
566
  return;
411
567
  }
412
- const file = agent === 'cursor' ? join(root, '.cursor', 'mcp.json') : join(root, '.mcp.json');
413
- await mkdir(dirname(file), { recursive: true });
414
- let current = {};
415
- try {
416
- current = JSON.parse(await readFile(file, 'utf8'));
417
- }
418
- catch {
419
- /* Create a new project config. */
420
- }
421
- current.mcpServers = { ...current.mcpServers, 'foundry-design-control': server };
422
- await writeFile(file, `${JSON.stringify(current, null, 2)}\n`);
568
+ const root = projectRoot();
569
+ const file = await installAgentIntegration(root, agent, has('--local-mcp') ? runtimeRepository : undefined);
423
570
  console.log(`Installed Foundry MCP configuration for ${agent}: ${file}`);
571
+ console.log(`Restart ${agent === 'codex' ? 'Codex' : agent === 'cursor' ? 'Cursor' : 'Claude Code'} before starting a Foundry apply session.`);
424
572
  }
425
573
  try {
426
- if (command === 'setup')
574
+ if (has('--version') || command === 'version')
575
+ console.log(FOUNDRY_VERSION);
576
+ else if (has('--help') || command === 'help')
577
+ printHelp();
578
+ else if (command === 'launch')
579
+ await launch();
580
+ else if (command === 'setup')
427
581
  await setup();
582
+ else if (command === 'update')
583
+ await update();
428
584
  else if (command === 'init')
429
585
  await initProject();
430
586
  else if (command === 'start')