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.
- package/README.md +163 -16
- package/dist/companion.d.ts +24 -0
- package/dist/companion.d.ts.map +1 -0
- package/dist/companion.js +89 -0
- package/dist/companion.js.map +1 -0
- package/dist/delivery-export.d.ts +27 -0
- package/dist/delivery-export.d.ts.map +1 -0
- package/dist/delivery-export.js +200 -0
- package/dist/delivery-export.js.map +1 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +206 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.js +442 -120
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +649 -27
- package/dist/indexer.js.map +1 -1
- package/dist/installer.d.ts +31 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +551 -100
- package/dist/installer.js.map +1 -1
- package/dist/project.d.ts +8 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +46 -1
- package/dist/project.js.map +1 -1
- package/dist/proxy.d.ts +7 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +106 -0
- package/dist/proxy.js.map +1 -0
- package/dist/release.d.ts +5 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +12 -0
- package/dist/release.js.map +1 -0
- package/dist/skill/foundry-design-control/SKILL.md +34 -11
- package/dist/skill/foundry-design-control/references/apply-run-contract.md +9 -4
- package/dist/skill/foundry-design-control/references/change-contract.md +2 -0
- package/dist/skill/foundry-design-control/references/component-workshop.md +46 -0
- package/dist/skill/foundry-design-control/references/design-branches.md +47 -0
- package/dist/skill/foundry-design-control/references/design-system-intelligence.md +40 -0
- package/dist/skill/foundry-design-control/references/motion-studio.md +101 -0
- package/dist/skill/foundry-design-control/references/portable-branch-records.md +38 -0
- package/dist/skill/foundry-design-control/references/responsive-design-lab.md +34 -0
- package/dist/skill/foundry-design-control/references/typography-studio.md +14 -0
- package/dist/skill/foundry-design-control/references/visual-agent-conversation.md +14 -0
- package/dist/skill/foundry-design-control/references/visual-workbench.md +27 -4
- package/dist/skill/foundry-design-control/scripts/foundry.sh +1 -5
- 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] ?? '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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: ${
|
|
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('\
|
|
140
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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(`
|
|
392
|
+
console.log(`Workspace: ${workspaceUrl}`);
|
|
263
393
|
if (targetUrl && platform === 'web')
|
|
264
|
-
console.log(
|
|
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(
|
|
406
|
+
await openUrl(workspaceUrl);
|
|
273
407
|
const shutdown = async () => {
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
'
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!
|
|
372
|
-
console.log('
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
417
|
-
await
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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 === '
|
|
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
|