foundry-design 0.2.0-beta.2 → 0.2.0-beta.21
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 +164 -17
- 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 +583 -124
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +690 -30
- package/dist/indexer.js.map +1 -1
- package/dist/installer.d.ts +46 -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 +9 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +50 -2
- 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 +41 -15
- package/dist/skill/foundry-design-control/references/apply-run-contract.md +13 -7
- package/dist/skill/foundry-design-control/references/change-contract.md +9 -6
- 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
|
-
import { createHash } from 'node:crypto';
|
|
4
|
-
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import { access, appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { createInterface } from 'node:readline/promises';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { renderChangePrompt, } from 'foundry-design-protocol';
|
|
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, {
|
|
@@ -70,6 +95,126 @@ async function revision(root) {
|
|
|
70
95
|
}
|
|
71
96
|
return `${head}-dirty-${hash.digest('hex').slice(0, 12)}`;
|
|
72
97
|
}
|
|
98
|
+
function safeSourcePath(root, input) {
|
|
99
|
+
const candidate = isAbsolute(input) ? relative(root, input) : input;
|
|
100
|
+
const normalized = candidate.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
101
|
+
const absolute = resolve(root, normalized);
|
|
102
|
+
const fromRoot = relative(root, absolute).replaceAll('\\', '/');
|
|
103
|
+
if (!normalized || fromRoot.startsWith('../') || isAbsolute(fromRoot)) {
|
|
104
|
+
throw new Error(`Source path is outside the project root: ${input}`);
|
|
105
|
+
}
|
|
106
|
+
return fromRoot;
|
|
107
|
+
}
|
|
108
|
+
async function sourceFileSnapshot(root, path, locations = []) {
|
|
109
|
+
const safePath = safeSourcePath(root, path);
|
|
110
|
+
const content = await readFile(resolve(root, safePath)).catch((error) => {
|
|
111
|
+
if (error.code === 'ENOENT')
|
|
112
|
+
return null;
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
const sourceLines = content?.toString('utf8').split(/\r?\n/) ?? [];
|
|
116
|
+
return {
|
|
117
|
+
path: safePath,
|
|
118
|
+
exists: content !== null,
|
|
119
|
+
sha256: content === null ? null : createHash('sha256').update(content).digest('hex'),
|
|
120
|
+
lineAnchors: content === null
|
|
121
|
+
? []
|
|
122
|
+
: [
|
|
123
|
+
...new Map(locations
|
|
124
|
+
.filter((location) => location.line &&
|
|
125
|
+
Number.isInteger(location.line) &&
|
|
126
|
+
location.line > 0 &&
|
|
127
|
+
location.line <= sourceLines.length &&
|
|
128
|
+
(!location.symbol || sourceLines[location.line - 1].includes(location.symbol)))
|
|
129
|
+
.map((location) => [
|
|
130
|
+
JSON.stringify([location.line, location.symbol]),
|
|
131
|
+
{
|
|
132
|
+
line: location.line,
|
|
133
|
+
endLine: location.line,
|
|
134
|
+
symbol: location.symbol,
|
|
135
|
+
sha256: createHash('sha256')
|
|
136
|
+
.update(sourceLines[location.line - 1])
|
|
137
|
+
.digest('hex'),
|
|
138
|
+
},
|
|
139
|
+
])).values(),
|
|
140
|
+
].sort((left, right) => left.line - right.line),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function parseChangedRanges(path, diff) {
|
|
144
|
+
const ranges = [];
|
|
145
|
+
for (const line of diff.split('\n')) {
|
|
146
|
+
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
147
|
+
if (!match)
|
|
148
|
+
continue;
|
|
149
|
+
ranges.push({
|
|
150
|
+
path,
|
|
151
|
+
oldStart: Number(match[1]),
|
|
152
|
+
oldLines: match[2] === undefined ? 1 : Number(match[2]),
|
|
153
|
+
newStart: Number(match[3]),
|
|
154
|
+
newLines: match[4] === undefined ? 1 : Number(match[4]),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return ranges;
|
|
158
|
+
}
|
|
159
|
+
async function gitChangedRanges(root, tracked, untracked) {
|
|
160
|
+
const ranges = [];
|
|
161
|
+
for (const path of tracked) {
|
|
162
|
+
const diff = (await gitOutput(root, ['diff', '--no-ext-diff', '--unified=0', 'HEAD', '--', path])) ?? '';
|
|
163
|
+
ranges.push(...parseChangedRanges(path, diff));
|
|
164
|
+
}
|
|
165
|
+
for (const path of untracked) {
|
|
166
|
+
const content = await readFile(resolve(root, path), 'utf8').catch(() => '');
|
|
167
|
+
ranges.push({
|
|
168
|
+
path,
|
|
169
|
+
oldStart: 0,
|
|
170
|
+
oldLines: 0,
|
|
171
|
+
newStart: 1,
|
|
172
|
+
newLines: content.split(/\r?\n/).length,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return ranges;
|
|
176
|
+
}
|
|
177
|
+
async function projectSourceState(root, sourcePaths, sourceLocations = []) {
|
|
178
|
+
const headRevision = await gitOutput(root, ['rev-parse', 'HEAD']);
|
|
179
|
+
if (!headRevision) {
|
|
180
|
+
const mappedPaths = [...new Set(sourcePaths.map((path) => safeSourcePath(root, path)))].sort();
|
|
181
|
+
if (!mappedPaths.length) {
|
|
182
|
+
return {
|
|
183
|
+
supported: false,
|
|
184
|
+
revision: null,
|
|
185
|
+
reason: 'No source-backed paths are available for filesystem verification.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const files = await Promise.all(mappedPaths.map((path) => sourceFileSnapshot(root, path, sourceLocations.filter((location) => safeSourcePath(root, location.path) === path))));
|
|
189
|
+
const fallbackRevision = createHash('sha256').update(JSON.stringify(files)).digest('hex');
|
|
190
|
+
return {
|
|
191
|
+
supported: true,
|
|
192
|
+
scope: 'mapped-files',
|
|
193
|
+
revision: `mapped-${fallbackRevision}`,
|
|
194
|
+
files,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const currentRevision = (await revision(root)) ?? headRevision;
|
|
198
|
+
const tracked = (await gitOutput(root, ['diff', '--name-only', 'HEAD']))?.split('\n').filter(Boolean) ?? [];
|
|
199
|
+
const untracked = (await gitOutput(root, ['ls-files', '--others', '--exclude-standard']))
|
|
200
|
+
?.split('\n')
|
|
201
|
+
.filter(Boolean) ?? [];
|
|
202
|
+
const paths = [
|
|
203
|
+
...new Set([...tracked, ...untracked, ...sourcePaths]
|
|
204
|
+
.map((file) => safeSourcePath(root, file))
|
|
205
|
+
.filter(Boolean)),
|
|
206
|
+
].sort();
|
|
207
|
+
const files = await Promise.all(paths.map((path) => sourceFileSnapshot(root, path, sourceLocations.filter((location) => safeSourcePath(root, location.path) === path))));
|
|
208
|
+
const changedRanges = await gitChangedRanges(root, tracked.map((path) => safeSourcePath(root, path)), untracked.map((path) => safeSourcePath(root, path)));
|
|
209
|
+
return {
|
|
210
|
+
supported: true,
|
|
211
|
+
scope: 'git',
|
|
212
|
+
revision: currentRevision,
|
|
213
|
+
headRevision,
|
|
214
|
+
files,
|
|
215
|
+
changedRanges,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
73
218
|
async function openUrl(url) {
|
|
74
219
|
const executable = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
75
220
|
const childArgs = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
@@ -105,18 +250,29 @@ function requestedAgents() {
|
|
|
105
250
|
throw new Error(`Unsupported agent: ${invalid.join(', ')}`);
|
|
106
251
|
return [...new Set(agents)];
|
|
107
252
|
}
|
|
253
|
+
async function sharedAgents(root, additional = []) {
|
|
254
|
+
const installed = await new CompanionStore(homedir()).read();
|
|
255
|
+
const detected = (await createSetupPlan(root)).agents;
|
|
256
|
+
return [...new Set([...installed.agents, ...additional, ...detected])];
|
|
257
|
+
}
|
|
108
258
|
async function setup() {
|
|
109
|
-
|
|
259
|
+
console.log(`${releasePreflight('set up this project')}\n`);
|
|
260
|
+
const root = await projectRoot();
|
|
261
|
+
const requested = requestedAgents();
|
|
262
|
+
const projectAgentSetup = has('--project-agent');
|
|
263
|
+
const hostAgents = requested ?? (await sharedAgents(root));
|
|
110
264
|
const options = {
|
|
111
|
-
agents:
|
|
112
|
-
targetUrl: flag('--url'),
|
|
265
|
+
agents: projectAgentSetup ? hostAgents : [],
|
|
266
|
+
targetUrl: normalizeTargetUrl(flag('--url')),
|
|
113
267
|
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
114
268
|
};
|
|
115
269
|
const plan = await createSetupPlan(root, options);
|
|
116
270
|
console.log(`Foundry setup\n\nProject: ${root}\nPlatform: ${plan.platform}`);
|
|
117
271
|
if (plan.framework)
|
|
118
272
|
console.log(`Framework: ${plan.framework}`);
|
|
119
|
-
console.log(`Agent integration: ${
|
|
273
|
+
console.log(`Agent integration: ${projectAgentSetup
|
|
274
|
+
? `${plan.agents.join(', ')} (project-scoped compatibility mode)`
|
|
275
|
+
: `${hostAgents.join(', ')} (shared across projects)`}`);
|
|
120
276
|
console.log('\nFiles Foundry will manage:');
|
|
121
277
|
for (const path of plan.files)
|
|
122
278
|
console.log(` ${path}`);
|
|
@@ -128,22 +284,90 @@ async function setup() {
|
|
|
128
284
|
return;
|
|
129
285
|
}
|
|
130
286
|
const result = await setupProject(root, options);
|
|
287
|
+
if (!projectAgentSetup) {
|
|
288
|
+
for (const agent of hostAgents) {
|
|
289
|
+
await installHostAgentIntegration(homedir(), agent, {
|
|
290
|
+
packageRoot: options.packageRoot,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
await new CompanionStore(homedir()).recordInstallation(hostAgents);
|
|
294
|
+
await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
|
|
295
|
+
}
|
|
131
296
|
console.log(`\n✓ Foundry configured ${result.changed.length} files.`);
|
|
297
|
+
for (const check of result.validation) {
|
|
298
|
+
const marker = check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!';
|
|
299
|
+
console.log(`${marker} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
|
|
300
|
+
}
|
|
132
301
|
if (result.skillDirectories.length) {
|
|
133
302
|
console.log(`✓ Installed the Foundry skill for ${result.agents.join(', ')}.`);
|
|
134
303
|
}
|
|
304
|
+
if (!projectAgentSetup && hostAgents.length) {
|
|
305
|
+
console.log(`✓ Installed the shared Foundry connection for ${hostAgents.join(', ')}.`);
|
|
306
|
+
}
|
|
135
307
|
if (result.targetUrl)
|
|
136
308
|
console.log(`✓ Preview URL: ${result.targetUrl}`);
|
|
137
309
|
if (result.devCommand)
|
|
138
310
|
console.log(`✓ Development command: ${result.devCommand.command} ${result.devCommand.args.join(' ')}`);
|
|
139
|
-
console.log('\
|
|
140
|
-
console.log(
|
|
311
|
+
console.log('\nSetup complete. One coding-agent restart is required so it can load Foundry.');
|
|
312
|
+
console.log(`1. Restart ${!projectAgentSetup && hostAgents.length
|
|
313
|
+
? hostAgents.join(', ')
|
|
314
|
+
: result.agents.length
|
|
315
|
+
? result.agents.join(', ')
|
|
316
|
+
: 'your coding agent'}.`);
|
|
317
|
+
console.log('2. Reopen this exact project folder.');
|
|
318
|
+
console.log('3. Ask: "Start Foundry for this project and keep listening for Apply with agent requests."');
|
|
319
|
+
console.log('\nAfter that, the browser will confirm when the agent is ready.');
|
|
320
|
+
}
|
|
321
|
+
async function update() {
|
|
322
|
+
console.log(`${releasePreflight('update this project')}\n`);
|
|
323
|
+
const root = await projectRoot();
|
|
324
|
+
const requested = requestedAgents();
|
|
325
|
+
const projectAgentSetup = has('--project-agent');
|
|
326
|
+
const hostAgents = requested ?? (await sharedAgents(root));
|
|
327
|
+
const options = {
|
|
328
|
+
agents: projectAgentSetup ? hostAgents : [],
|
|
329
|
+
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
330
|
+
};
|
|
331
|
+
const plan = await createUpdatePlan(root, options);
|
|
332
|
+
console.log(`Foundry update\n\nProject: ${root}`);
|
|
333
|
+
console.log(`Agent integration: ${projectAgentSetup
|
|
334
|
+
? `${plan.agents.join(', ')} (project-scoped compatibility mode)`
|
|
335
|
+
: `${hostAgents.join(', ')} (shared across projects)`}`);
|
|
336
|
+
console.log('\nFoundry will refresh these managed paths:');
|
|
337
|
+
for (const path of plan.files)
|
|
338
|
+
console.log(` ${path}`);
|
|
339
|
+
console.log('\nFiles changed since Foundry installed them will be preserved.');
|
|
340
|
+
if (!(await confirm('\nContinue with this update?'))) {
|
|
341
|
+
console.log('Update cancelled.');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const result = await updateProject(root, options);
|
|
345
|
+
if (!projectAgentSetup) {
|
|
346
|
+
for (const agent of hostAgents)
|
|
347
|
+
await installHostAgentIntegration(homedir(), agent, {
|
|
348
|
+
packageRoot: options.packageRoot,
|
|
349
|
+
});
|
|
350
|
+
await new CompanionStore(homedir()).recordInstallation(hostAgents);
|
|
351
|
+
await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
|
|
352
|
+
}
|
|
353
|
+
console.log(`\n✓ Foundry refreshed ${result.changed.length} managed paths.`);
|
|
354
|
+
if (result.preserved.length) {
|
|
355
|
+
console.log('Preserved files containing user changes:');
|
|
356
|
+
for (const path of result.preserved)
|
|
357
|
+
console.log(` ${path}`);
|
|
358
|
+
}
|
|
359
|
+
for (const check of result.validation) {
|
|
360
|
+
const marker = check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!';
|
|
361
|
+
console.log(`${marker} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
|
|
362
|
+
}
|
|
363
|
+
console.log('\nThe project now uses the shared Foundry agent connection.');
|
|
141
364
|
}
|
|
142
365
|
async function initProject() {
|
|
366
|
+
console.log(`${releasePreflight('initialize this project')}\n`);
|
|
143
367
|
const platform = (args[1] ?? 'web');
|
|
144
368
|
if (!['web', 'swiftui', 'react-native'].includes(platform))
|
|
145
369
|
throw new Error(`Unsupported platform: ${platform}`);
|
|
146
|
-
const root = projectRoot();
|
|
370
|
+
const root = await projectRoot({ allowUninitialized: true });
|
|
147
371
|
const configRoot = join(root, '.foundry');
|
|
148
372
|
await mkdir(configRoot, { recursive: true });
|
|
149
373
|
await writeFile(join(configRoot, 'foundry.config.json'), `${JSON.stringify({
|
|
@@ -184,7 +408,7 @@ async function initProject() {
|
|
|
184
408
|
console.log('Import and call .foundry/web-adapter.ts once from your development-only client entry.');
|
|
185
409
|
}
|
|
186
410
|
async function start() {
|
|
187
|
-
const root = projectRoot();
|
|
411
|
+
const root = await projectRoot();
|
|
188
412
|
let config;
|
|
189
413
|
try {
|
|
190
414
|
config = JSON.parse(await readFile(join(root, '.foundry', 'foundry.config.json'), 'utf8'));
|
|
@@ -195,10 +419,8 @@ async function start() {
|
|
|
195
419
|
const platform = flag('--platform') ??
|
|
196
420
|
config?.platform ??
|
|
197
421
|
(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
|
-
}
|
|
422
|
+
const targetUrl = normalizeTargetUrl(flag('--url') ?? config?.targetUrl);
|
|
423
|
+
let basicPreview;
|
|
202
424
|
let developmentServer;
|
|
203
425
|
if (platform === 'web' &&
|
|
204
426
|
targetUrl &&
|
|
@@ -224,54 +446,105 @@ async function start() {
|
|
|
224
446
|
}
|
|
225
447
|
}
|
|
226
448
|
const projectRevision = await revision(root);
|
|
449
|
+
const configuredViewports = config?.design?.viewports ?? [];
|
|
450
|
+
const initialViewport = configuredViewports.find((viewport) => viewport.id.toLowerCase() === 'desktop' || viewport.label.toLowerCase() === 'desktop') ??
|
|
451
|
+
[...configuredViewports].sort((left, right) => right.width - left.width)[0] ?? {
|
|
452
|
+
width: 1440,
|
|
453
|
+
height: 900,
|
|
454
|
+
};
|
|
227
455
|
const context = {
|
|
228
456
|
projectRoot: root,
|
|
229
457
|
revision: projectRevision,
|
|
230
458
|
platform,
|
|
231
459
|
targetUrl,
|
|
460
|
+
previewOrigin: targetUrl ? new URL(targetUrl).origin : undefined,
|
|
232
461
|
targetName: basename(root),
|
|
462
|
+
viewport: { width: initialViewport.width, height: initialViewport.height ?? 900 },
|
|
233
463
|
theme: 'system',
|
|
234
464
|
breakpoint: 'current',
|
|
235
465
|
state: 'current',
|
|
236
466
|
};
|
|
237
467
|
const store = new SessionStore();
|
|
238
|
-
const
|
|
468
|
+
const resumable = has('--new')
|
|
469
|
+
? undefined
|
|
470
|
+
: (await store.list()).find((candidate) => candidate.changeSet.context.projectRoot === root &&
|
|
471
|
+
candidate.changeSet.context.revision === projectRevision &&
|
|
472
|
+
candidate.changeSet.context.targetUrl === targetUrl);
|
|
473
|
+
const session = resumable
|
|
474
|
+
? await store.read(resumable.changeSet.sessionId)
|
|
475
|
+
: await store.create(context);
|
|
239
476
|
if (platform === 'web') {
|
|
240
477
|
const graph = await indexProjectDesign(root, config, projectRevision);
|
|
241
478
|
await store.setDesignGraph(session.changeSet.sessionId, graph);
|
|
242
479
|
console.log(`Indexed ${graph.tokens.length} tokens, ${graph.components.length} components, and ${graph.breakpoints.length} viewports.`);
|
|
243
480
|
}
|
|
244
|
-
const runtime = new FoundryRuntime({
|
|
481
|
+
const runtime = new FoundryRuntime({
|
|
482
|
+
store,
|
|
483
|
+
resolveProjectRevision: async ({ projectRoot: revisionRoot, sourcePaths, sourceLocations }) => projectSourceState(revisionRoot, sourcePaths, sourceLocations),
|
|
484
|
+
reindexProjectDesign: async ({ projectRoot: graphRoot }) => {
|
|
485
|
+
const currentConfig = await readFile(join(graphRoot, '.foundry', 'foundry.config.json'), 'utf8')
|
|
486
|
+
.then((content) => JSON.parse(content))
|
|
487
|
+
.catch(() => undefined);
|
|
488
|
+
return indexProjectDesign(graphRoot, currentConfig, await revision(graphRoot));
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
let ownsRuntime = false;
|
|
245
492
|
try {
|
|
246
493
|
await runtime.start();
|
|
494
|
+
ownsRuntime = true;
|
|
247
495
|
}
|
|
248
496
|
catch (error) {
|
|
249
497
|
if (error.code !== 'EADDRINUSE')
|
|
250
498
|
throw error;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
499
|
+
const healthy = await fetch('http://127.0.0.1:4387/v1/health')
|
|
500
|
+
.then((response) => response.ok)
|
|
501
|
+
.catch(() => false);
|
|
502
|
+
if (!healthy)
|
|
503
|
+
throw new Error('Port 4387 is in use by another process. Stop it and run Foundry again.');
|
|
504
|
+
console.log('Using the existing local Foundry runtime.');
|
|
255
505
|
}
|
|
256
506
|
const reviewUrl = `http://127.0.0.1:4387/?session=${encodeURIComponent(session.changeSet.sessionId)}&token=${encodeURIComponent(session.token)}`;
|
|
507
|
+
if (platform === 'web' && targetUrl && config && !config.instrumented) {
|
|
508
|
+
basicPreview = await startBasicPreviewProxy(targetUrl, config.runtimeUrl);
|
|
509
|
+
console.log('Basic mode: Foundry is attached without changing the project entry point.');
|
|
510
|
+
console.log('Run setup again after adding a supported client entry to enable exact source mapping.');
|
|
511
|
+
}
|
|
512
|
+
if (platform === 'web' && targetUrl) {
|
|
513
|
+
await store.setPreviewOrigin(session.changeSet.sessionId, new URL(basicPreview?.url ?? targetUrl).origin);
|
|
514
|
+
}
|
|
515
|
+
const previewCapability = platform === 'web' && targetUrl ? randomBytes(32).toString('base64url') : undefined;
|
|
516
|
+
if (previewCapability) {
|
|
517
|
+
await store.setPreviewCapability(session.changeSet.sessionId, previewCapability);
|
|
518
|
+
}
|
|
257
519
|
const productUrl = targetUrl && platform === 'web'
|
|
258
|
-
? addSessionParams(targetUrl, session.changeSet.sessionId, session.token)
|
|
520
|
+
? addSessionParams(basicPreview?.url ?? targetUrl, session.changeSet.sessionId, session.token, previewCapability)
|
|
521
|
+
: reviewUrl;
|
|
522
|
+
const workspaceUrl = platform === 'web' && targetUrl
|
|
523
|
+
? `${reviewUrl}&preview=${encodeURIComponent(productUrl)}`
|
|
259
524
|
: reviewUrl;
|
|
260
525
|
console.log(`Foundry session ${session.changeSet.sessionId}`);
|
|
526
|
+
if (resumable)
|
|
527
|
+
console.log('Resumed the most recent session for this project revision.');
|
|
261
528
|
console.log(`Platform: ${platform}`);
|
|
262
|
-
console.log(`
|
|
529
|
+
console.log(`Workspace: ${workspaceUrl}`);
|
|
263
530
|
if (targetUrl && platform === 'web')
|
|
264
|
-
console.log(
|
|
531
|
+
console.log(`${basicPreview ? 'Basic' : 'Precision'} preview: ${productUrl}`);
|
|
265
532
|
if (platform === 'web') {
|
|
266
533
|
console.log('\nFirst edit:');
|
|
267
534
|
console.log(' 1. Option-click an element. Shift-click to add more.');
|
|
268
535
|
console.log(' 2. Drag a blue handle or use the inspector for exact values.');
|
|
269
536
|
console.log(' 3. Review the semantic mapping, then Apply with agent.');
|
|
537
|
+
console.log('\nAgent handoff:');
|
|
538
|
+
console.log(' Keep this process running while you design.');
|
|
539
|
+
console.log(' Foundry will show “Agent is ready” only while a coding agent is listening.');
|
|
540
|
+
console.log(' If it is not ready, ask the agent: “Keep listening for Apply with agent requests.”');
|
|
270
541
|
}
|
|
271
542
|
if (!has('--no-open'))
|
|
272
|
-
await openUrl(
|
|
543
|
+
await openUrl(workspaceUrl);
|
|
273
544
|
const shutdown = async () => {
|
|
274
|
-
|
|
545
|
+
if (ownsRuntime)
|
|
546
|
+
await runtime.stop();
|
|
547
|
+
await basicPreview?.stop();
|
|
275
548
|
developmentServer?.kill('SIGTERM');
|
|
276
549
|
process.exit(0);
|
|
277
550
|
};
|
|
@@ -280,7 +553,7 @@ async function start() {
|
|
|
280
553
|
await new Promise(() => { });
|
|
281
554
|
}
|
|
282
555
|
async function indexDesign() {
|
|
283
|
-
const root = projectRoot();
|
|
556
|
+
const root = await projectRoot();
|
|
284
557
|
const config = await readFile(join(root, '.foundry', 'foundry.config.json'), 'utf8')
|
|
285
558
|
.then((content) => JSON.parse(content))
|
|
286
559
|
.catch(() => undefined);
|
|
@@ -295,7 +568,33 @@ async function indexDesign() {
|
|
|
295
568
|
console.log(content);
|
|
296
569
|
}
|
|
297
570
|
async function uninstall() {
|
|
298
|
-
|
|
571
|
+
if (has('--global')) {
|
|
572
|
+
const installed = await new CompanionStore(homedir()).read();
|
|
573
|
+
const agents = requestedAgents() ?? installed.agents;
|
|
574
|
+
if (!agents.length) {
|
|
575
|
+
console.log('No shared Foundry agent connection is registered.');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (!(await confirm(`Remove the shared Foundry connection for ${agents.join(', ')}?`))) {
|
|
579
|
+
console.log('Uninstall cancelled.');
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const preserved = [];
|
|
583
|
+
for (const agent of agents) {
|
|
584
|
+
const result = await uninstallHostAgentIntegration(homedir(), agent);
|
|
585
|
+
preserved.push(...result.preserved);
|
|
586
|
+
}
|
|
587
|
+
await new CompanionStore(homedir()).removeInstallation(agents);
|
|
588
|
+
console.log(`Removed the shared Foundry connection for ${agents.join(', ')}.`);
|
|
589
|
+
if (preserved.length) {
|
|
590
|
+
console.log('Preserved customized skill files:');
|
|
591
|
+
for (const path of preserved)
|
|
592
|
+
console.log(` ${path}`);
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
console.log(`${releasePreflight('remove managed project integration')}\n`);
|
|
597
|
+
const root = await projectRoot();
|
|
299
598
|
if (!(await confirm(`Remove Foundry-managed project integration from ${root}?`))) {
|
|
300
599
|
console.log('Uninstall cancelled.');
|
|
301
600
|
return;
|
|
@@ -308,68 +607,108 @@ async function uninstall() {
|
|
|
308
607
|
console.log(` ${path}`);
|
|
309
608
|
}
|
|
310
609
|
}
|
|
610
|
+
async function disconnect() {
|
|
611
|
+
const root = await projectRoot();
|
|
612
|
+
if (!(await confirm(`Disconnect ${root} from Foundry's recent projects?`))) {
|
|
613
|
+
console.log('Disconnect cancelled.');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
await new CompanionStore(homedir()).unregisterProject(root);
|
|
617
|
+
console.log(`Disconnected ${root}. The project adapter remains installed and can reconnect later.`);
|
|
618
|
+
}
|
|
311
619
|
async function doctor() {
|
|
312
|
-
const root = projectRoot();
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
620
|
+
const root = await projectRoot();
|
|
621
|
+
const report = await collectDoctorReport(root);
|
|
622
|
+
if (has('--json'))
|
|
623
|
+
console.log(JSON.stringify(report, null, 2));
|
|
624
|
+
else {
|
|
625
|
+
for (const check of report.checks) {
|
|
626
|
+
const marker = check.status === 'passed' ? '✓' : check.status === 'warning' ? '△' : '○';
|
|
627
|
+
console.log(`${marker} ${check.label}: ${check.detail}`);
|
|
628
|
+
}
|
|
629
|
+
console.log(report.ready
|
|
630
|
+
? '\nFoundry is ready and an agent is actively listening.'
|
|
631
|
+
: '\nFoundry is not yet ready for Apply with agent. Configuration and a live listener are separate checks.');
|
|
632
|
+
}
|
|
633
|
+
if (has('--repair')) {
|
|
634
|
+
console.log(`\n${releasePreflight('repair this project and agent connection')}\n`);
|
|
635
|
+
const installed = report.checks.find((check) => check.id === 'integration')?.status !== 'failed';
|
|
636
|
+
const configuredAgents = installed ? (await createUpdatePlan(root)).agents : [];
|
|
637
|
+
const hostAgents = await sharedAgents(root, configuredAgents);
|
|
638
|
+
const result = installed
|
|
639
|
+
? await updateProject(root, {
|
|
640
|
+
agents: [],
|
|
641
|
+
targetUrl: normalizeTargetUrl(flag('--url')),
|
|
642
|
+
})
|
|
643
|
+
: await setupProject(root, {
|
|
644
|
+
agents: [],
|
|
645
|
+
targetUrl: normalizeTargetUrl(flag('--url')),
|
|
646
|
+
});
|
|
647
|
+
for (const agent of hostAgents)
|
|
648
|
+
await installHostAgentIntegration(homedir(), agent, {
|
|
649
|
+
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
650
|
+
});
|
|
651
|
+
await new CompanionStore(homedir()).recordInstallation(hostAgents);
|
|
652
|
+
await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
|
|
653
|
+
console.log(`✓ Repaired ${result.changed.length} managed paths and the shared ${hostAgents.join(', ')} connection.`);
|
|
654
|
+
const repaired = await collectDoctorReport(root);
|
|
655
|
+
const requiredAfterRepair = new Set([
|
|
656
|
+
'config',
|
|
657
|
+
'project-connection',
|
|
658
|
+
'integration',
|
|
659
|
+
'agent-configured',
|
|
660
|
+
'agent-config-valid',
|
|
661
|
+
'agent-version',
|
|
662
|
+
]);
|
|
663
|
+
const remaining = repaired.checks.filter((check) => requiredAfterRepair.has(check.id) && check.status === 'failed');
|
|
664
|
+
if (remaining.length) {
|
|
665
|
+
throw new Error(`Repair is incomplete: ${remaining.map((check) => check.label).join(', ')}`);
|
|
666
|
+
}
|
|
667
|
+
console.log('✓ Project and agent configuration now use the same Foundry release.');
|
|
668
|
+
if (repaired.checks.find((check) => check.id === 'agent-listening')?.status !== 'passed') {
|
|
669
|
+
console.log('△ Restart the active coding agent once to activate its listener.');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else if (report.checks.some((check) => check.status === 'failed')) {
|
|
673
|
+
console.log('Run: foundry-design doctor --repair');
|
|
318
674
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
const
|
|
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
|
-
'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');
|
|
675
|
+
}
|
|
676
|
+
async function launch() {
|
|
677
|
+
console.log(`${releasePreflight('install, update, and start Foundry')}\n`);
|
|
678
|
+
const root = await projectRoot();
|
|
679
|
+
const manifest = join(root, '.foundry', 'install-manifest.json');
|
|
680
|
+
const installed = await pathExists(manifest);
|
|
681
|
+
const configuredAgents = installed ? (await createUpdatePlan(root)).agents : [];
|
|
682
|
+
const hostAgents = await sharedAgents(root, configuredAgents);
|
|
683
|
+
console.log(`Foundry\n\nProject: ${root}`);
|
|
684
|
+
console.log(`${installed ? 'Refreshing' : 'Preparing'} the project and shared ${hostAgents.join(', ')} connection.`);
|
|
685
|
+
if (!(await confirm('Continue?'))) {
|
|
686
|
+
console.log('Foundry cancelled.');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const result = installed
|
|
690
|
+
? await updateProject(root, {
|
|
691
|
+
agents: [],
|
|
692
|
+
targetUrl: normalizeTargetUrl(flag('--url')),
|
|
693
|
+
})
|
|
694
|
+
: await setupProject(root, {
|
|
695
|
+
agents: [],
|
|
696
|
+
targetUrl: normalizeTargetUrl(flag('--url')),
|
|
697
|
+
});
|
|
698
|
+
for (const agent of hostAgents)
|
|
699
|
+
await installHostAgentIntegration(homedir(), agent, {
|
|
700
|
+
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
701
|
+
});
|
|
702
|
+
await new CompanionStore(homedir()).recordInstallation(hostAgents);
|
|
703
|
+
await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
|
|
704
|
+
console.log(`✓ ${installed ? 'Updated' : 'Installed'} and validated Foundry.`);
|
|
705
|
+
for (const check of result.validation)
|
|
706
|
+
console.log(`${check.status === 'passed' ? '✓' : check.status === 'skipped' ? '–' : '!'} ${check.name}: ${check.status.replaceAll('-', ' ')}`);
|
|
707
|
+
if (!installed)
|
|
708
|
+
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.`);
|
|
709
|
+
if (has('--no-start'))
|
|
710
|
+
return;
|
|
711
|
+
await start();
|
|
373
712
|
}
|
|
374
713
|
async function exportSession() {
|
|
375
714
|
const id = args[1];
|
|
@@ -395,45 +734,163 @@ async function exportSession() {
|
|
|
395
734
|
else
|
|
396
735
|
console.log(content);
|
|
397
736
|
}
|
|
737
|
+
async function exportDelivery() {
|
|
738
|
+
if (args[1] !== 'export')
|
|
739
|
+
throw new Error('delivery requires the export subcommand');
|
|
740
|
+
const recordId = args[2];
|
|
741
|
+
if (!recordId)
|
|
742
|
+
throw new Error('delivery export requires a delivery record id');
|
|
743
|
+
const output = flag('--output');
|
|
744
|
+
if (!output)
|
|
745
|
+
throw new Error('delivery export requires --output PATH');
|
|
746
|
+
const format = flag('--format') ?? 'repo';
|
|
747
|
+
if (!['repo', 'markdown', 'json'].includes(format)) {
|
|
748
|
+
throw new Error('delivery export format must be repo, markdown, or json');
|
|
749
|
+
}
|
|
750
|
+
const store = new SessionStore();
|
|
751
|
+
const summary = (await store.list()).find((session) => session.deliveryRecords.some((record) => record.id === recordId));
|
|
752
|
+
if (!summary)
|
|
753
|
+
throw new Error(`Unknown delivery record: ${recordId}`);
|
|
754
|
+
const session = await store.read(summary.changeSet.sessionId);
|
|
755
|
+
const record = session.deliveryRecords.find((candidate) => candidate.id === recordId);
|
|
756
|
+
if (format === 'repo') {
|
|
757
|
+
if (!(await confirm(`Export ${record.title} into ${resolve(output)}?`))) {
|
|
758
|
+
console.log('Delivery export cancelled.');
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
let manifest;
|
|
762
|
+
try {
|
|
763
|
+
manifest = await writeRepositoryDeliveryExport(output, session, record);
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (error instanceof DeliveryExportConflictError && error.documentationPageIds.length) {
|
|
767
|
+
await store.markDocumentationConflicted(session.changeSet.sessionId, error.documentationPageIds);
|
|
768
|
+
}
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
await store.recordDocumentationExports(session.changeSet.sessionId, manifest.documentationPages);
|
|
772
|
+
console.log(`Exported ${manifest.files.length} managed delivery files to ${resolve(output)}.`);
|
|
773
|
+
console.log('Foundry preserved the project root changelog and will refuse to overwrite edited generated docs.');
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const content = format === 'markdown'
|
|
777
|
+
? renderDeliveryMarkdown(record, session.changeSet.context.projectRoot)
|
|
778
|
+
: renderPortableDeliveryJson(record, session.changeSet.context.projectRoot);
|
|
779
|
+
await mkdir(dirname(resolve(output)), { recursive: true });
|
|
780
|
+
await writeFile(resolve(output), `${content}\n`);
|
|
781
|
+
console.log(`Exported ${format} delivery record to ${resolve(output)}.`);
|
|
782
|
+
}
|
|
398
783
|
async function installAgent() {
|
|
784
|
+
console.log(`${releasePreflight('install the coding-agent connection')}\n`);
|
|
399
785
|
const agent = args[1];
|
|
400
786
|
if (!agent || !['cursor', 'claude', 'codex'].includes(agent))
|
|
401
787
|
throw new Error('install-agent requires cursor, claude, or codex');
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
788
|
+
if (!has('--project-agent')) {
|
|
789
|
+
const result = await installHostAgentIntegration(homedir(), agent, {
|
|
790
|
+
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
791
|
+
});
|
|
792
|
+
console.log(`Installed the shared Foundry connection for ${agent}.`);
|
|
793
|
+
console.log(`MCP configuration: ${result.configFile}`);
|
|
794
|
+
console.log(`Skill: ${result.skillDirectory}`);
|
|
795
|
+
if (result.preserved.length) {
|
|
796
|
+
console.log('Preserved customized skill files:');
|
|
797
|
+
for (const path of result.preserved)
|
|
798
|
+
console.log(` ${path}`);
|
|
799
|
+
}
|
|
800
|
+
await new CompanionStore(homedir()).recordInstallation([agent]);
|
|
801
|
+
console.log(`Restart ${agent === 'codex' ? 'Codex' : agent === 'cursor' ? 'Cursor' : 'Claude Code'} once. Future projects connect with "npx foundry-design" without another MCP install.`);
|
|
414
802
|
return;
|
|
415
803
|
}
|
|
416
|
-
const
|
|
417
|
-
await
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
804
|
+
const root = await projectRoot();
|
|
805
|
+
const file = await installAgentIntegration(root, agent, has('--local-mcp') ? runtimeRepository : undefined);
|
|
806
|
+
console.log(`Installed Foundry MCP configuration for ${agent}: ${file}`);
|
|
807
|
+
console.log(`Restart ${agent === 'codex' ? 'Codex' : agent === 'cursor' ? 'Cursor' : 'Claude Code'} before starting a Foundry apply session.`);
|
|
808
|
+
}
|
|
809
|
+
async function installHost() {
|
|
810
|
+
console.log(`${releasePreflight('install Foundry for your coding agents')}\n`);
|
|
811
|
+
const agents = requestedAgents() ?? ['codex', 'cursor', 'claude'];
|
|
812
|
+
if (!(await confirm(`Install the shared Foundry connection for ${agents.join(', ')}?`))) {
|
|
813
|
+
console.log('Installation cancelled.');
|
|
814
|
+
return;
|
|
421
815
|
}
|
|
422
|
-
|
|
423
|
-
|
|
816
|
+
for (const agent of agents) {
|
|
817
|
+
const result = await installHostAgentIntegration(homedir(), agent, {
|
|
818
|
+
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
819
|
+
});
|
|
820
|
+
console.log(`✓ ${agent}: ${result.configFile}`);
|
|
821
|
+
}
|
|
822
|
+
await new CompanionStore(homedir()).recordInstallation(agents);
|
|
823
|
+
console.log('\nFoundry is installed once for these agents. Restart each selected agent once.');
|
|
824
|
+
console.log('Then open any project and run: npx foundry-design');
|
|
825
|
+
}
|
|
826
|
+
async function reset() {
|
|
827
|
+
console.log(`${releasePreflight('reset managed project and agent integration')}\n`);
|
|
828
|
+
const root = await projectRoot();
|
|
829
|
+
const installed = await pathExists(join(root, '.foundry', 'install-manifest.json'));
|
|
830
|
+
const configuredAgents = installed ? (await createUpdatePlan(root)).agents : [];
|
|
831
|
+
const requested = requestedAgents();
|
|
832
|
+
const hostAgents = requested ?? (await sharedAgents(root, configuredAgents));
|
|
833
|
+
if (!(await confirm(`Reset Foundry-managed integration for ${root}?`))) {
|
|
834
|
+
console.log('Reset cancelled.');
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const options = {
|
|
838
|
+
agents: [],
|
|
839
|
+
targetUrl: normalizeTargetUrl(flag('--url')),
|
|
840
|
+
packageRoot: has('--local-mcp') ? runtimeRepository : undefined,
|
|
841
|
+
};
|
|
842
|
+
const result = installed ? await updateProject(root, options) : await setupProject(root, options);
|
|
843
|
+
for (const agent of hostAgents)
|
|
844
|
+
await installHostAgentIntegration(homedir(), agent, options);
|
|
845
|
+
await new CompanionStore(homedir()).recordInstallation(hostAgents);
|
|
846
|
+
await new CompanionStore(homedir()).registerProject(root, result.targetUrl);
|
|
847
|
+
console.log(`✓ Reset ${result.changed.length} managed paths and ${hostAgents.join(', ')} shared connections.`);
|
|
848
|
+
if (result.preserved.length) {
|
|
849
|
+
console.log('Preserved customized files:');
|
|
850
|
+
for (const path of result.preserved)
|
|
851
|
+
console.log(` ${path}`);
|
|
852
|
+
}
|
|
853
|
+
console.log('Restart the active coding agent once, reopen this project, then run npx foundry-design.');
|
|
854
|
+
}
|
|
855
|
+
async function companion() {
|
|
856
|
+
const state = await new CompanionStore(homedir()).read();
|
|
857
|
+
if (has('--json')) {
|
|
858
|
+
console.log(JSON.stringify(state, null, 2));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
console.log(`Foundry ${state.foundryVersion}`);
|
|
862
|
+
console.log(`Installed agents: ${state.agents.join(', ') || 'none'}`);
|
|
863
|
+
console.log(`Recent projects: ${state.projects.length}`);
|
|
864
|
+
for (const project of state.projects.slice(0, 5)) {
|
|
865
|
+
console.log(` ${project.root}${project.targetUrl ? ` · ${project.targetUrl}` : ''}`);
|
|
424
866
|
}
|
|
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
867
|
}
|
|
429
868
|
try {
|
|
430
|
-
if (command === '
|
|
869
|
+
if (has('--version') || command === 'version')
|
|
870
|
+
console.log(FOUNDRY_VERSION);
|
|
871
|
+
else if (has('--help') || command === 'help')
|
|
872
|
+
printHelp();
|
|
873
|
+
else if (command === 'launch')
|
|
874
|
+
await launch();
|
|
875
|
+
else if (command === 'setup')
|
|
431
876
|
await setup();
|
|
877
|
+
else if (command === 'update')
|
|
878
|
+
await update();
|
|
879
|
+
else if (command === 'install')
|
|
880
|
+
await installHost();
|
|
881
|
+
else if (command === 'connect')
|
|
882
|
+
await launch();
|
|
883
|
+
else if (command === 'disconnect')
|
|
884
|
+
await disconnect();
|
|
885
|
+
else if (command === 'reset')
|
|
886
|
+
await reset();
|
|
887
|
+
else if (command === 'companion')
|
|
888
|
+
await companion();
|
|
432
889
|
else if (command === 'init')
|
|
433
890
|
await initProject();
|
|
434
891
|
else if (command === 'start')
|
|
435
892
|
await start();
|
|
436
|
-
else if (command === 'doctor')
|
|
893
|
+
else if (command === 'doctor' || command === 'status')
|
|
437
894
|
await doctor();
|
|
438
895
|
else if (command === 'index')
|
|
439
896
|
await indexDesign();
|
|
@@ -441,6 +898,8 @@ try {
|
|
|
441
898
|
await uninstall();
|
|
442
899
|
else if (command === 'export')
|
|
443
900
|
await exportSession();
|
|
901
|
+
else if (command === 'delivery')
|
|
902
|
+
await exportDelivery();
|
|
444
903
|
else if (command === 'install-agent')
|
|
445
904
|
await installAgent();
|
|
446
905
|
else
|