foundry-design 0.2.0-beta.2 → 0.2.0-beta.20

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 (48) hide show
  1. package/README.md +163 -16
  2. package/dist/companion.d.ts +24 -0
  3. package/dist/companion.d.ts.map +1 -0
  4. package/dist/companion.js +89 -0
  5. package/dist/companion.js.map +1 -0
  6. package/dist/delivery-export.d.ts +27 -0
  7. package/dist/delivery-export.d.ts.map +1 -0
  8. package/dist/delivery-export.js +200 -0
  9. package/dist/delivery-export.js.map +1 -0
  10. package/dist/doctor.d.ts +24 -0
  11. package/dist/doctor.d.ts.map +1 -0
  12. package/dist/doctor.js +206 -0
  13. package/dist/doctor.js.map +1 -0
  14. package/dist/index.js +442 -120
  15. package/dist/index.js.map +1 -1
  16. package/dist/indexer.d.ts.map +1 -1
  17. package/dist/indexer.js +649 -27
  18. package/dist/indexer.js.map +1 -1
  19. package/dist/installer.d.ts +31 -0
  20. package/dist/installer.d.ts.map +1 -1
  21. package/dist/installer.js +551 -100
  22. package/dist/installer.js.map +1 -1
  23. package/dist/project.d.ts +8 -0
  24. package/dist/project.d.ts.map +1 -1
  25. package/dist/project.js +46 -1
  26. package/dist/project.js.map +1 -1
  27. package/dist/proxy.d.ts +7 -0
  28. package/dist/proxy.d.ts.map +1 -0
  29. package/dist/proxy.js +106 -0
  30. package/dist/proxy.js.map +1 -0
  31. package/dist/release.d.ts +5 -0
  32. package/dist/release.d.ts.map +1 -0
  33. package/dist/release.js +12 -0
  34. package/dist/release.js.map +1 -0
  35. package/dist/skill/foundry-design-control/SKILL.md +34 -11
  36. package/dist/skill/foundry-design-control/references/apply-run-contract.md +9 -4
  37. package/dist/skill/foundry-design-control/references/change-contract.md +2 -0
  38. package/dist/skill/foundry-design-control/references/component-workshop.md +46 -0
  39. package/dist/skill/foundry-design-control/references/design-branches.md +47 -0
  40. package/dist/skill/foundry-design-control/references/design-system-intelligence.md +40 -0
  41. package/dist/skill/foundry-design-control/references/motion-studio.md +101 -0
  42. package/dist/skill/foundry-design-control/references/portable-branch-records.md +38 -0
  43. package/dist/skill/foundry-design-control/references/responsive-design-lab.md +34 -0
  44. package/dist/skill/foundry-design-control/references/typography-studio.md +14 -0
  45. package/dist/skill/foundry-design-control/references/visual-agent-conversation.md +14 -0
  46. package/dist/skill/foundry-design-control/references/visual-workbench.md +27 -4
  47. package/dist/skill/foundry-design-control/scripts/foundry.sh +1 -5
  48. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,17 +1,23 @@
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';
4
+ import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
5
5
  import { basename, dirname, 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
- import { FoundryRuntime, SessionStore } from 'foundry-design-runtime';
10
- import { createSetupPlan, setupProject, uninstallProject, } from './installer.js';
11
- import { addSessionParams, detectPlatform } from './project.js';
10
+ import { FoundryRuntime, SessionStore, renderDeliveryMarkdown } from 'foundry-design-runtime';
11
+ import { createSetupPlan, createUpdatePlan, installHostAgentIntegration, installAgentIntegration, setupProject, updateProject, uninstallProject, uninstallHostAgentIntegration, } from './installer.js';
12
+ import { addSessionParams, detectPlatform, normalizeTargetUrl, resolveProjectRoot, } from './project.js';
12
13
  import { indexProjectDesign } from './indexer.js';
14
+ import { startBasicPreviewProxy } from './proxy.js';
15
+ import { FOUNDRY_VERSION, releasePreflight } from './release.js';
16
+ import { collectDoctorReport } from './doctor.js';
17
+ import { CompanionStore } from './companion.js';
18
+ import { DeliveryExportConflictError, renderPortableDeliveryJson, writeRepositoryDeliveryExport, } from './delivery-export.js';
13
19
  const args = process.argv.slice(2);
14
- const command = args[0] ?? 'help';
20
+ const command = args[0]?.startsWith('-') ? 'launch' : (args[0] ?? 'launch');
15
21
  const runtimeRepository = resolve(fileURLToPath(new URL('../../../', import.meta.url)));
16
22
  function flag(name) {
17
23
  const index = args.indexOf(name);
@@ -20,24 +26,43 @@ function flag(name) {
20
26
  function has(name) {
21
27
  return args.includes(name);
22
28
  }
23
- function projectRoot() {
24
- return resolve(process.env.INIT_CWD ?? process.cwd(), flag('--project') ?? '.');
29
+ async function projectRoot(options = {}) {
30
+ const specified = flag('--project');
31
+ const start = resolve(process.env.INIT_CWD ?? process.cwd(), specified ?? '.');
32
+ return resolveProjectRoot(start, {
33
+ explicit: Boolean(specified),
34
+ allowUninitialized: options.allowUninitialized,
35
+ });
25
36
  }
26
37
  function printHelp() {
27
38
  console.log(`Foundry Design Control
28
39
 
29
40
  Usage:
30
- foundry-design setup [--project PATH] [--agent codex,cursor,claude] [--url URL] [--yes]
41
+ foundry-design [--project PATH] [--url URL] [--yes] [--no-start]
42
+ foundry-design setup [--project PATH] [--agent codex,cursor,claude] [--project-agent] [--url URL] [--yes]
43
+ foundry-design update [--project PATH] [--agent codex,cursor,claude] [--yes]
44
+ foundry-design install [--agent codex,cursor,claude] [--yes]
45
+ foundry-design connect [--project PATH] [--url URL] [--yes]
46
+ foundry-design disconnect [--project PATH] [--yes]
47
+ foundry-design reset [--project PATH] [--agent codex,cursor,claude] [--url URL] [--yes]
48
+ foundry-design companion [--json]
31
49
  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]
50
+ foundry-design start [--project PATH] [--url URL] [--platform PLATFORM] [--new] [--no-open] [--no-dev]
51
+ foundry-design doctor [--project PATH] [--repair] [--json]
52
+ foundry-design status [--project PATH] [--json]
34
53
  foundry-design index [--project PATH] [--output FILE]
35
- foundry-design uninstall [--project PATH] [--yes]
54
+ foundry-design uninstall [--project PATH] [--global] [--agent codex,cursor,claude] [--yes]
36
55
  foundry-design export <SESSION_ID> [--format json|prompt|full] [--output FILE]
37
- foundry-design install-agent <cursor|claude|codex> [--project PATH]
56
+ foundry-design delivery export <DELIVERY_ID> --format repo|markdown|json --output PATH [--yes]
57
+ foundry-design install-agent <cursor|claude|codex> [--project-agent --project PATH]
38
58
 
39
59
  Foundry is local-only and never edits product source from inspector controls.`);
40
60
  }
61
+ async function pathExists(path) {
62
+ return access(path)
63
+ .then(() => true)
64
+ .catch(() => false);
65
+ }
41
66
  async function gitOutput(root, gitArgs) {
42
67
  return new Promise((resolveRevision) => {
43
68
  const child = spawn('git', gitArgs, {
@@ -105,18 +130,29 @@ function requestedAgents() {
105
130
  throw new Error(`Unsupported agent: ${invalid.join(', ')}`);
106
131
  return [...new Set(agents)];
107
132
  }
133
+ async function sharedAgents(root, additional = []) {
134
+ const installed = await new CompanionStore(homedir()).read();
135
+ const detected = (await createSetupPlan(root)).agents;
136
+ return [...new Set([...installed.agents, ...additional, ...detected])];
137
+ }
108
138
  async function setup() {
109
- const root = projectRoot();
139
+ console.log(`${releasePreflight('set up this project')}\n`);
140
+ const root = await projectRoot();
141
+ const requested = requestedAgents();
142
+ const projectAgentSetup = has('--project-agent');
143
+ const hostAgents = requested ?? (await sharedAgents(root));
110
144
  const options = {
111
- agents: requestedAgents(),
112
- targetUrl: flag('--url'),
145
+ agents: projectAgentSetup ? hostAgents : [],
146
+ targetUrl: normalizeTargetUrl(flag('--url')),
113
147
  packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
114
148
  };
115
149
  const plan = await createSetupPlan(root, options);
116
150
  console.log(`Foundry setup\n\nProject: ${root}\nPlatform: ${plan.platform}`);
117
151
  if (plan.framework)
118
152
  console.log(`Framework: ${plan.framework}`);
119
- console.log(`Agent integration: ${plan.agents.length ? plan.agents.join(', ') : 'plugin-provided'}`);
153
+ console.log(`Agent integration: ${projectAgentSetup
154
+ ? `${plan.agents.join(', ')} (project-scoped compatibility mode)`
155
+ : `${hostAgents.join(', ')} (shared across projects)`}`);
120
156
  console.log('\nFiles Foundry will manage:');
121
157
  for (const path of plan.files)
122
158
  console.log(` ${path}`);
@@ -128,22 +164,90 @@ async function setup() {
128
164
  return;
129
165
  }
130
166
  const result = await setupProject(root, options);
167
+ if (!projectAgentSetup) {
168
+ for (const agent of hostAgents) {
169
+ await installHostAgentIntegration(homedir(), agent, {
170
+ packageRoot: options.packageRoot,
171
+ });
172
+ }
173
+ await new CompanionStore(homedir()).recordInstallation(hostAgents);
174
+ await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
175
+ }
131
176
  console.log(`\n✓ Foundry configured ${result.changed.length} files.`);
177
+ for (const check of result.validation) {
178
+ const marker = check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!';
179
+ console.log(`${marker} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
180
+ }
132
181
  if (result.skillDirectories.length) {
133
182
  console.log(`✓ Installed the Foundry skill for ${result.agents.join(', ')}.`);
134
183
  }
184
+ if (!projectAgentSetup && hostAgents.length) {
185
+ console.log(`✓ Installed the shared Foundry connection for ${hostAgents.join(', ')}.`);
186
+ }
135
187
  if (result.targetUrl)
136
188
  console.log(`✓ Preview URL: ${result.targetUrl}`);
137
189
  if (result.devCommand)
138
190
  console.log(`✓ Development command: ${result.devCommand.command} ${result.devCommand.args.join(' ')}`);
139
- console.log('\nRestart your coding agent, then ask: "Start Foundry for this project."');
140
- console.log('You can also run: foundry-design start');
191
+ console.log('\nSetup complete. One coding-agent restart is required so it can load Foundry.');
192
+ console.log(`1. Restart ${!projectAgentSetup && hostAgents.length
193
+ ? hostAgents.join(', ')
194
+ : result.agents.length
195
+ ? result.agents.join(', ')
196
+ : 'your coding agent'}.`);
197
+ console.log('2. Reopen this exact project folder.');
198
+ console.log('3. Ask: "Start Foundry for this project and keep listening for Apply with agent requests."');
199
+ console.log('\nAfter that, the browser will confirm when the agent is ready.');
200
+ }
201
+ async function update() {
202
+ console.log(`${releasePreflight('update this project')}\n`);
203
+ const root = await projectRoot();
204
+ const requested = requestedAgents();
205
+ const projectAgentSetup = has('--project-agent');
206
+ const hostAgents = requested ?? (await sharedAgents(root));
207
+ const options = {
208
+ agents: projectAgentSetup ? hostAgents : [],
209
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
210
+ };
211
+ const plan = await createUpdatePlan(root, options);
212
+ console.log(`Foundry update\n\nProject: ${root}`);
213
+ console.log(`Agent integration: ${projectAgentSetup
214
+ ? `${plan.agents.join(', ')} (project-scoped compatibility mode)`
215
+ : `${hostAgents.join(', ')} (shared across projects)`}`);
216
+ console.log('\nFoundry will refresh these managed paths:');
217
+ for (const path of plan.files)
218
+ console.log(` ${path}`);
219
+ console.log('\nFiles changed since Foundry installed them will be preserved.');
220
+ if (!(await confirm('\nContinue with this update?'))) {
221
+ console.log('Update cancelled.');
222
+ return;
223
+ }
224
+ const result = await updateProject(root, options);
225
+ if (!projectAgentSetup) {
226
+ for (const agent of hostAgents)
227
+ await installHostAgentIntegration(homedir(), agent, {
228
+ packageRoot: options.packageRoot,
229
+ });
230
+ await new CompanionStore(homedir()).recordInstallation(hostAgents);
231
+ await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
232
+ }
233
+ console.log(`\n✓ Foundry refreshed ${result.changed.length} managed paths.`);
234
+ if (result.preserved.length) {
235
+ console.log('Preserved files containing user changes:');
236
+ for (const path of result.preserved)
237
+ console.log(` ${path}`);
238
+ }
239
+ for (const check of result.validation) {
240
+ const marker = check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!';
241
+ console.log(`${marker} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
242
+ }
243
+ console.log('\nThe project now uses the shared Foundry agent connection.');
141
244
  }
142
245
  async function initProject() {
246
+ console.log(`${releasePreflight('initialize this project')}\n`);
143
247
  const platform = (args[1] ?? 'web');
144
248
  if (!['web', 'swiftui', 'react-native'].includes(platform))
145
249
  throw new Error(`Unsupported platform: ${platform}`);
146
- const root = projectRoot();
250
+ const root = await projectRoot({ allowUninitialized: true });
147
251
  const configRoot = join(root, '.foundry');
148
252
  await mkdir(configRoot, { recursive: true });
149
253
  await writeFile(join(configRoot, 'foundry.config.json'), `${JSON.stringify({
@@ -184,7 +288,7 @@ async function initProject() {
184
288
  console.log('Import and call .foundry/web-adapter.ts once from your development-only client entry.');
185
289
  }
186
290
  async function start() {
187
- const root = projectRoot();
291
+ const root = await projectRoot();
188
292
  let config;
189
293
  try {
190
294
  config = JSON.parse(await readFile(join(root, '.foundry', 'foundry.config.json'), 'utf8'));
@@ -195,10 +299,8 @@ async function start() {
195
299
  const platform = flag('--platform') ??
196
300
  config?.platform ??
197
301
  (await detectPlatform(root));
198
- const targetUrl = flag('--url') ?? config?.targetUrl;
199
- if (platform === 'web' && config && !config.instrumented) {
200
- throw new Error('Foundry setup is incomplete. Connect .foundry/web-adapter.ts to a development-only client entry, then set instrumented to true.');
201
- }
302
+ const targetUrl = normalizeTargetUrl(flag('--url') ?? config?.targetUrl);
303
+ let basicPreview;
202
304
  let developmentServer;
203
305
  if (platform === 'web' &&
204
306
  targetUrl &&
@@ -224,54 +326,88 @@ async function start() {
224
326
  }
225
327
  }
226
328
  const projectRevision = await revision(root);
329
+ const configuredViewports = config?.design?.viewports ?? [];
330
+ const initialViewport = configuredViewports.find((viewport) => viewport.id.toLowerCase() === 'desktop' || viewport.label.toLowerCase() === 'desktop') ??
331
+ [...configuredViewports].sort((left, right) => right.width - left.width)[0] ?? {
332
+ width: 1440,
333
+ height: 900,
334
+ };
227
335
  const context = {
228
336
  projectRoot: root,
229
337
  revision: projectRevision,
230
338
  platform,
231
339
  targetUrl,
232
340
  targetName: basename(root),
341
+ viewport: { width: initialViewport.width, height: initialViewport.height ?? 900 },
233
342
  theme: 'system',
234
343
  breakpoint: 'current',
235
344
  state: 'current',
236
345
  };
237
346
  const store = new SessionStore();
238
- const session = await store.create(context);
347
+ const resumable = has('--new')
348
+ ? undefined
349
+ : (await store.list()).find((candidate) => candidate.changeSet.context.projectRoot === root &&
350
+ candidate.changeSet.context.revision === projectRevision &&
351
+ candidate.changeSet.context.targetUrl === targetUrl);
352
+ const session = resumable
353
+ ? await store.read(resumable.changeSet.sessionId)
354
+ : await store.create(context);
239
355
  if (platform === 'web') {
240
356
  const graph = await indexProjectDesign(root, config, projectRevision);
241
357
  await store.setDesignGraph(session.changeSet.sessionId, graph);
242
358
  console.log(`Indexed ${graph.tokens.length} tokens, ${graph.components.length} components, and ${graph.breakpoints.length} viewports.`);
243
359
  }
244
360
  const runtime = new FoundryRuntime({ store });
361
+ let ownsRuntime = false;
245
362
  try {
246
363
  await runtime.start();
364
+ ownsRuntime = true;
247
365
  }
248
366
  catch (error) {
249
367
  if (error.code !== 'EADDRINUSE')
250
368
  throw error;
251
- developmentServer?.kill('SIGTERM');
252
- console.error('Port 4387 is already in use. Stop the existing Foundry runtime before starting another session.');
253
- process.exitCode = 1;
254
- return;
369
+ const healthy = await fetch('http://127.0.0.1:4387/v1/health')
370
+ .then((response) => response.ok)
371
+ .catch(() => false);
372
+ if (!healthy)
373
+ throw new Error('Port 4387 is in use by another process. Stop it and run Foundry again.');
374
+ console.log('Using the existing local Foundry runtime.');
255
375
  }
256
376
  const reviewUrl = `http://127.0.0.1:4387/?session=${encodeURIComponent(session.changeSet.sessionId)}&token=${encodeURIComponent(session.token)}`;
377
+ if (platform === 'web' && targetUrl && config && !config.instrumented) {
378
+ basicPreview = await startBasicPreviewProxy(targetUrl, config.runtimeUrl);
379
+ console.log('Basic mode: Foundry is attached without changing the project entry point.');
380
+ console.log('Run setup again after adding a supported client entry to enable exact source mapping.');
381
+ }
257
382
  const productUrl = targetUrl && platform === 'web'
258
- ? addSessionParams(targetUrl, session.changeSet.sessionId, session.token)
383
+ ? addSessionParams(basicPreview?.url ?? targetUrl, session.changeSet.sessionId, session.token)
384
+ : reviewUrl;
385
+ const workspaceUrl = platform === 'web' && targetUrl
386
+ ? `${reviewUrl}&preview=${encodeURIComponent(productUrl)}`
259
387
  : reviewUrl;
260
388
  console.log(`Foundry session ${session.changeSet.sessionId}`);
389
+ if (resumable)
390
+ console.log('Resumed the most recent session for this project revision.');
261
391
  console.log(`Platform: ${platform}`);
262
- console.log(`Inspector: ${reviewUrl}`);
392
+ console.log(`Workspace: ${workspaceUrl}`);
263
393
  if (targetUrl && platform === 'web')
264
- console.log(`Instrumented preview: ${productUrl}`);
394
+ console.log(`${basicPreview ? 'Basic' : 'Precision'} preview: ${productUrl}`);
265
395
  if (platform === 'web') {
266
396
  console.log('\nFirst edit:');
267
397
  console.log(' 1. Option-click an element. Shift-click to add more.');
268
398
  console.log(' 2. Drag a blue handle or use the inspector for exact values.');
269
399
  console.log(' 3. Review the semantic mapping, then Apply with agent.');
400
+ console.log('\nAgent handoff:');
401
+ console.log(' Keep this process running while you design.');
402
+ console.log(' Foundry will show “Agent is ready” only while a coding agent is listening.');
403
+ console.log(' If it is not ready, ask the agent: “Keep listening for Apply with agent requests.”');
270
404
  }
271
405
  if (!has('--no-open'))
272
- await openUrl(productUrl);
406
+ await openUrl(workspaceUrl);
273
407
  const shutdown = async () => {
274
- await runtime.stop();
408
+ if (ownsRuntime)
409
+ await runtime.stop();
410
+ await basicPreview?.stop();
275
411
  developmentServer?.kill('SIGTERM');
276
412
  process.exit(0);
277
413
  };
@@ -280,7 +416,7 @@ async function start() {
280
416
  await new Promise(() => { });
281
417
  }
282
418
  async function indexDesign() {
283
- const root = projectRoot();
419
+ const root = await projectRoot();
284
420
  const config = await readFile(join(root, '.foundry', 'foundry.config.json'), 'utf8')
285
421
  .then((content) => JSON.parse(content))
286
422
  .catch(() => undefined);
@@ -295,7 +431,33 @@ async function indexDesign() {
295
431
  console.log(content);
296
432
  }
297
433
  async function uninstall() {
298
- const root = projectRoot();
434
+ if (has('--global')) {
435
+ const installed = await new CompanionStore(homedir()).read();
436
+ const agents = requestedAgents() ?? installed.agents;
437
+ if (!agents.length) {
438
+ console.log('No shared Foundry agent connection is registered.');
439
+ return;
440
+ }
441
+ if (!(await confirm(`Remove the shared Foundry connection for ${agents.join(', ')}?`))) {
442
+ console.log('Uninstall cancelled.');
443
+ return;
444
+ }
445
+ const preserved = [];
446
+ for (const agent of agents) {
447
+ const result = await uninstallHostAgentIntegration(homedir(), agent);
448
+ preserved.push(...result.preserved);
449
+ }
450
+ await new CompanionStore(homedir()).removeInstallation(agents);
451
+ console.log(`Removed the shared Foundry connection for ${agents.join(', ')}.`);
452
+ if (preserved.length) {
453
+ console.log('Preserved customized skill files:');
454
+ for (const path of preserved)
455
+ console.log(` ${path}`);
456
+ }
457
+ return;
458
+ }
459
+ console.log(`${releasePreflight('remove managed project integration')}\n`);
460
+ const root = await projectRoot();
299
461
  if (!(await confirm(`Remove Foundry-managed project integration from ${root}?`))) {
300
462
  console.log('Uninstall cancelled.');
301
463
  return;
@@ -308,68 +470,108 @@ async function uninstall() {
308
470
  console.log(` ${path}`);
309
471
  }
310
472
  }
473
+ async function disconnect() {
474
+ const root = await projectRoot();
475
+ if (!(await confirm(`Disconnect ${root} from Foundry's recent projects?`))) {
476
+ console.log('Disconnect cancelled.');
477
+ return;
478
+ }
479
+ await new CompanionStore(homedir()).unregisterProject(root);
480
+ console.log(`Disconnected ${root}. The project adapter remains installed and can reconnect later.`);
481
+ }
311
482
  async function doctor() {
312
- const root = projectRoot();
313
- const platform = await detectPlatform(root);
314
- const configPath = join(root, '.foundry', 'foundry.config.json');
315
- let config;
316
- try {
317
- config = JSON.parse(await readFile(configPath, 'utf8'));
483
+ const root = await projectRoot();
484
+ const report = await collectDoctorReport(root);
485
+ if (has('--json'))
486
+ console.log(JSON.stringify(report, null, 2));
487
+ else {
488
+ for (const check of report.checks) {
489
+ const marker = check.status === 'passed' ? '✓' : check.status === 'warning' ? '△' : '○';
490
+ console.log(`${marker} ${check.label}: ${check.detail}`);
491
+ }
492
+ console.log(report.ready
493
+ ? '\nFoundry is ready and an agent is actively listening.'
494
+ : '\nFoundry is not yet ready for Apply with agent. Configuration and a live listener are separate checks.');
318
495
  }
319
- catch {
320
- /* Report setup as missing below. */
321
- }
322
- const agentFiles = [
323
- join(root, '.codex', 'config.toml'),
324
- join(root, '.cursor', 'mcp.json'),
325
- join(root, '.mcp.json'),
326
- ];
327
- const configuredAgents = [];
328
- for (const path of agentFiles) {
329
- if ((await readFile(path, 'utf8').catch(() => '')).includes('foundry-design-control'))
330
- configuredAgents.push(path);
331
- }
332
- const pluginProvided = await readFile(join(root, '.foundry', 'install-manifest.json'), 'utf8')
333
- .then((content) => {
334
- const manifest = JSON.parse(content);
335
- return Array.isArray(manifest.agents) && manifest.agents.length === 0;
336
- })
337
- .catch(() => false);
338
- const checks = [
339
- ['Project', root, true],
340
- ['Detected platform', platform, true],
341
- ['Foundry config', configPath, Boolean(config)],
342
- [
343
- 'Instrumentation',
344
- config?.instrumented ? `${config.framework ?? config.platform}` : 'integration pending',
345
- Boolean(config?.instrumented),
346
- ],
347
- [
348
- 'Agent connection',
349
- configuredAgents.length
350
- ? configuredAgents.join(', ')
351
- : pluginProvided
352
- ? 'plugin-provided'
353
- : 'not configured',
354
- configuredAgents.length > 0 || pluginProvided,
355
- ],
356
- [
357
- 'Project preview',
358
- config?.targetUrl ?? 'not configured',
359
- config?.targetUrl ? await urlAvailable(config.targetUrl) : platform !== 'web',
360
- ],
361
- [
362
- 'Runtime health',
363
- 'http://127.0.0.1:4387/v1/health',
364
- await fetch('http://127.0.0.1:4387/v1/health')
365
- .then((response) => response.ok)
366
- .catch(() => false),
367
- ],
368
- ];
369
- for (const [label, value, passed] of checks)
370
- console.log(`${passed ? '✓' : '○'} ${label}: ${value}`);
371
- if (!config)
372
- console.log('Run: foundry-design setup');
496
+ if (has('--repair')) {
497
+ console.log(`\n${releasePreflight('repair this project and agent connection')}\n`);
498
+ const installed = report.checks.find((check) => check.id === 'integration')?.status !== 'failed';
499
+ const configuredAgents = installed ? (await createUpdatePlan(root)).agents : [];
500
+ const hostAgents = await sharedAgents(root, configuredAgents);
501
+ const result = installed
502
+ ? await updateProject(root, {
503
+ agents: [],
504
+ targetUrl: normalizeTargetUrl(flag('--url')),
505
+ })
506
+ : await setupProject(root, {
507
+ agents: [],
508
+ targetUrl: normalizeTargetUrl(flag('--url')),
509
+ });
510
+ for (const agent of hostAgents)
511
+ await installHostAgentIntegration(homedir(), agent, {
512
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
513
+ });
514
+ await new CompanionStore(homedir()).recordInstallation(hostAgents);
515
+ await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
516
+ console.log(`✓ Repaired ${result.changed.length} managed paths and the shared ${hostAgents.join(', ')} connection.`);
517
+ const repaired = await collectDoctorReport(root);
518
+ const requiredAfterRepair = new Set([
519
+ 'config',
520
+ 'project-connection',
521
+ 'integration',
522
+ 'agent-configured',
523
+ 'agent-config-valid',
524
+ 'agent-version',
525
+ ]);
526
+ const remaining = repaired.checks.filter((check) => requiredAfterRepair.has(check.id) && check.status === 'failed');
527
+ if (remaining.length) {
528
+ throw new Error(`Repair is incomplete: ${remaining.map((check) => check.label).join(', ')}`);
529
+ }
530
+ console.log('✓ Project and agent configuration now use the same Foundry release.');
531
+ if (repaired.checks.find((check) => check.id === 'agent-listening')?.status !== 'passed') {
532
+ console.log('△ Restart the active coding agent once to activate its listener.');
533
+ }
534
+ }
535
+ else if (report.checks.some((check) => check.status === 'failed')) {
536
+ console.log('Run: foundry-design doctor --repair');
537
+ }
538
+ }
539
+ async function launch() {
540
+ console.log(`${releasePreflight('install, update, and start Foundry')}\n`);
541
+ const root = await projectRoot();
542
+ const manifest = join(root, '.foundry', 'install-manifest.json');
543
+ const installed = await pathExists(manifest);
544
+ const configuredAgents = installed ? (await createUpdatePlan(root)).agents : [];
545
+ const hostAgents = await sharedAgents(root, configuredAgents);
546
+ console.log(`Foundry\n\nProject: ${root}`);
547
+ console.log(`${installed ? 'Refreshing' : 'Preparing'} the project and shared ${hostAgents.join(', ')} connection.`);
548
+ if (!(await confirm('Continue?'))) {
549
+ console.log('Foundry cancelled.');
550
+ return;
551
+ }
552
+ const result = installed
553
+ ? await updateProject(root, {
554
+ agents: [],
555
+ targetUrl: normalizeTargetUrl(flag('--url')),
556
+ })
557
+ : await setupProject(root, {
558
+ agents: [],
559
+ targetUrl: normalizeTargetUrl(flag('--url')),
560
+ });
561
+ for (const agent of hostAgents)
562
+ await installHostAgentIntegration(homedir(), agent, {
563
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
564
+ });
565
+ await new CompanionStore(homedir()).recordInstallation(hostAgents);
566
+ await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
567
+ console.log(`✓ ${installed ? 'Updated' : 'Installed'} and validated Foundry.`);
568
+ for (const check of result.validation)
569
+ console.log(`${check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!'} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
570
+ if (!installed)
571
+ console.log(`\nRestart ${hostAgents.join(', ')} once to load the shared connection. The visual session will open now and queued batches will wait safely until it reconnects.`);
572
+ if (has('--no-start'))
573
+ return;
574
+ await start();
373
575
  }
374
576
  async function exportSession() {
375
577
  const id = args[1];
@@ -395,45 +597,163 @@ async function exportSession() {
395
597
  else
396
598
  console.log(content);
397
599
  }
600
+ async function exportDelivery() {
601
+ if (args[1] !== 'export')
602
+ throw new Error('delivery requires the export subcommand');
603
+ const recordId = args[2];
604
+ if (!recordId)
605
+ throw new Error('delivery export requires a delivery record id');
606
+ const output = flag('--output');
607
+ if (!output)
608
+ throw new Error('delivery export requires --output PATH');
609
+ const format = flag('--format') ?? 'repo';
610
+ if (!['repo', 'markdown', 'json'].includes(format)) {
611
+ throw new Error('delivery export format must be repo, markdown, or json');
612
+ }
613
+ const store = new SessionStore();
614
+ const summary = (await store.list()).find((session) => session.deliveryRecords.some((record) => record.id === recordId));
615
+ if (!summary)
616
+ throw new Error(`Unknown delivery record: ${recordId}`);
617
+ const session = await store.read(summary.changeSet.sessionId);
618
+ const record = session.deliveryRecords.find((candidate) => candidate.id === recordId);
619
+ if (format === 'repo') {
620
+ if (!(await confirm(`Export ${record.title} into ${resolve(output)}?`))) {
621
+ console.log('Delivery export cancelled.');
622
+ return;
623
+ }
624
+ let manifest;
625
+ try {
626
+ manifest = await writeRepositoryDeliveryExport(output, session, record);
627
+ }
628
+ catch (error) {
629
+ if (error instanceof DeliveryExportConflictError && error.documentationPageIds.length) {
630
+ await store.markDocumentationConflicted(session.changeSet.sessionId, error.documentationPageIds);
631
+ }
632
+ throw error;
633
+ }
634
+ await store.recordDocumentationExports(session.changeSet.sessionId, manifest.documentationPages);
635
+ console.log(`Exported ${manifest.files.length} managed delivery files to ${resolve(output)}.`);
636
+ console.log('Foundry preserved the project root changelog and will refuse to overwrite edited generated docs.');
637
+ return;
638
+ }
639
+ const content = format === 'markdown'
640
+ ? renderDeliveryMarkdown(record, session.changeSet.context.projectRoot)
641
+ : renderPortableDeliveryJson(record, session.changeSet.context.projectRoot);
642
+ await mkdir(dirname(resolve(output)), { recursive: true });
643
+ await writeFile(resolve(output), `${content}\n`);
644
+ console.log(`Exported ${format} delivery record to ${resolve(output)}.`);
645
+ }
398
646
  async function installAgent() {
647
+ console.log(`${releasePreflight('install the coding-agent connection')}\n`);
399
648
  const agent = args[1];
400
649
  if (!agent || !['cursor', 'claude', 'codex'].includes(agent))
401
650
  throw new Error('install-agent requires cursor, claude, or codex');
402
- const root = projectRoot();
403
- const server = {
404
- command: 'npx',
405
- args: ['-y', 'foundry-design-mcp-server@beta'],
406
- env: { FOUNDRY_DESIGN_RUNTIME_URL: 'http://127.0.0.1:4387' },
407
- };
408
- if (agent === 'codex') {
409
- const directory = join(root, '.codex');
410
- await mkdir(directory, { recursive: true });
411
- const file = join(directory, 'foundry-mcp.toml');
412
- await writeFile(file, `# Merge this project-scoped server into your Codex MCP configuration.\n[mcp_servers.foundry-design-control]\ncommand = "npx"\nargs = ["-y", "foundry-design-mcp-server@beta"]\n[mcp_servers.foundry-design-control.env]\nFOUNDRY_DESIGN_RUNTIME_URL = "http://127.0.0.1:4387"\n`);
413
- console.log(`Wrote Codex MCP snippet: ${file}`);
651
+ if (!has('--project-agent')) {
652
+ const result = await installHostAgentIntegration(homedir(), agent, {
653
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
654
+ });
655
+ console.log(`Installed the shared Foundry connection for ${agent}.`);
656
+ console.log(`MCP configuration: ${result.configFile}`);
657
+ console.log(`Skill: ${result.skillDirectory}`);
658
+ if (result.preserved.length) {
659
+ console.log('Preserved customized skill files:');
660
+ for (const path of result.preserved)
661
+ console.log(` ${path}`);
662
+ }
663
+ await new CompanionStore(homedir()).recordInstallation([agent]);
664
+ console.log(`Restart ${agent === 'codex' ? 'Codex' : agent === 'cursor' ? 'Cursor' : 'Claude Code'} once. Future projects connect with "npx foundry-design" without another MCP install.`);
414
665
  return;
415
666
  }
416
- const file = agent === 'cursor' ? join(root, '.cursor', 'mcp.json') : join(root, '.mcp.json');
417
- await mkdir(dirname(file), { recursive: true });
418
- let current = {};
419
- try {
420
- current = JSON.parse(await readFile(file, 'utf8'));
667
+ const root = await projectRoot();
668
+ const file = await installAgentIntegration(root, agent, has('--local-mcp') ? runtimeRepository : undefined);
669
+ console.log(`Installed Foundry MCP configuration for ${agent}: ${file}`);
670
+ console.log(`Restart ${agent === 'codex' ? 'Codex' : agent === 'cursor' ? 'Cursor' : 'Claude Code'} before starting a Foundry apply session.`);
671
+ }
672
+ async function installHost() {
673
+ console.log(`${releasePreflight('install Foundry for your coding agents')}\n`);
674
+ const agents = requestedAgents() ?? ['codex', 'cursor', 'claude'];
675
+ if (!(await confirm(`Install the shared Foundry connection for ${agents.join(', ')}?`))) {
676
+ console.log('Installation cancelled.');
677
+ return;
421
678
  }
422
- catch {
423
- /* Create a new project config. */
679
+ for (const agent of agents) {
680
+ const result = await installHostAgentIntegration(homedir(), agent, {
681
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
682
+ });
683
+ console.log(`✓ ${agent}: ${result.configFile}`);
684
+ }
685
+ await new CompanionStore(homedir()).recordInstallation(agents);
686
+ console.log('\nFoundry is installed once for these agents. Restart each selected agent once.');
687
+ console.log('Then open any project and run: npx foundry-design');
688
+ }
689
+ async function reset() {
690
+ console.log(`${releasePreflight('reset managed project and agent integration')}\n`);
691
+ const root = await projectRoot();
692
+ const installed = await pathExists(join(root, '.foundry', 'install-manifest.json'));
693
+ const configuredAgents = installed ? (await createUpdatePlan(root)).agents : [];
694
+ const requested = requestedAgents();
695
+ const hostAgents = requested ?? (await sharedAgents(root, configuredAgents));
696
+ if (!(await confirm(`Reset Foundry-managed integration for ${root}?`))) {
697
+ console.log('Reset cancelled.');
698
+ return;
699
+ }
700
+ const options = {
701
+ agents: [],
702
+ targetUrl: normalizeTargetUrl(flag('--url')),
703
+ packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
704
+ };
705
+ const result = installed ? await updateProject(root, options) : await setupProject(root, options);
706
+ for (const agent of hostAgents)
707
+ await installHostAgentIntegration(homedir(), agent, options);
708
+ await new CompanionStore(homedir()).recordInstallation(hostAgents);
709
+ await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
710
+ console.log(`✓ Reset ${result.changed.length} managed paths and ${hostAgents.join(', ')} shared connections.`);
711
+ if (result.preserved.length) {
712
+ console.log('Preserved customized files:');
713
+ for (const path of result.preserved)
714
+ console.log(` ${path}`);
715
+ }
716
+ console.log('Restart the active coding agent once, reopen this project, then run npx foundry-design.');
717
+ }
718
+ async function companion() {
719
+ const state = await new CompanionStore(homedir()).read();
720
+ if (has('--json')) {
721
+ console.log(JSON.stringify(state, null, 2));
722
+ return;
723
+ }
724
+ console.log(`Foundry ${state.foundryVersion}`);
725
+ console.log(`Installed agents: ${state.agents.join(', ') || 'none'}`);
726
+ console.log(`Recent projects: ${state.projects.length}`);
727
+ for (const project of state.projects.slice(0, 5)) {
728
+ console.log(` ${project.root}${project.targetUrl ? ` · ${project.targetUrl}` : ''}`);
424
729
  }
425
- current.mcpServers = { ...current.mcpServers, 'foundry-design-control': server };
426
- await writeFile(file, `${JSON.stringify(current, null, 2)}\n`);
427
- console.log(`Installed Foundry MCP configuration for ${agent}: ${file}`);
428
730
  }
429
731
  try {
430
- if (command === 'setup')
732
+ if (has('--version') || command === 'version')
733
+ console.log(FOUNDRY_VERSION);
734
+ else if (has('--help') || command === 'help')
735
+ printHelp();
736
+ else if (command === 'launch')
737
+ await launch();
738
+ else if (command === 'setup')
431
739
  await setup();
740
+ else if (command === 'update')
741
+ await update();
742
+ else if (command === 'install')
743
+ await installHost();
744
+ else if (command === 'connect')
745
+ await launch();
746
+ else if (command === 'disconnect')
747
+ await disconnect();
748
+ else if (command === 'reset')
749
+ await reset();
750
+ else if (command === 'companion')
751
+ await companion();
432
752
  else if (command === 'init')
433
753
  await initProject();
434
754
  else if (command === 'start')
435
755
  await start();
436
- else if (command === 'doctor')
756
+ else if (command === 'doctor' || command === 'status')
437
757
  await doctor();
438
758
  else if (command === 'index')
439
759
  await indexDesign();
@@ -441,6 +761,8 @@ try {
441
761
  await uninstall();
442
762
  else if (command === 'export')
443
763
  await exportSession();
764
+ else if (command === 'delivery')
765
+ await exportDelivery();
444
766
  else if (command === 'install-agent')
445
767
  await installAgent();
446
768
  else