fraim 2.0.128 → 2.0.130
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/dist/src/ai-hub/catalog.js +7 -4
- package/dist/src/cli/commands/first-run.js +14 -1
- package/dist/src/cli/commands/init-project.js +55 -121
- package/dist/src/cli/commands/setup.js +68 -43
- package/dist/src/cli/commands/sync.js +8 -1
- package/dist/src/cli/commands/workspace-config.js +31 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-global-integration.js +19 -0
- package/dist/src/cli/setup/user-level-sync.js +5 -0
- package/dist/src/cli/utils/project-bootstrap.js +3 -3
- package/dist/src/core/fraim-config-contract.js +145 -0
- package/dist/src/core/fraim-config-schema.generated.js +296 -0
- package/dist/src/core/utils/setup-preferences.js +41 -0
- package/dist/src/first-run/server.js +118 -18
- package/dist/src/first-run/session-service.js +282 -364
- package/dist/src/first-run/types.js +10 -21
- package/dist/src/local-mcp-server/stdio-server.js +28 -29
- package/dist/src/local-mcp-server/usage-collector.js +3 -0
- package/index.js +1 -1
- package/package.json +7 -5
- package/public/ai-hub/script.js +187 -1
- package/public/first-run/error-frame.js +100 -89
- package/public/first-run/index.html +5 -6
- package/public/first-run/script.js +275 -227
- package/public/first-run/styles.css +603 -386
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FRAIM_CONFIG_SCHEMA = void 0;
|
|
4
|
+
exports.listSupportedFraimConfigPaths = listSupportedFraimConfigPaths;
|
|
5
|
+
exports.isValidFraimConfigField = isValidFraimConfigField;
|
|
6
|
+
exports.sanitizeFraimConfigToSchema = sanitizeFraimConfigToSchema;
|
|
7
|
+
exports.validateWorkspaceFraimConfig = validateWorkspaceFraimConfig;
|
|
8
|
+
exports.assertValidWorkspaceFraimConfig = assertValidWorkspaceFraimConfig;
|
|
9
|
+
const fraim_config_schema_generated_1 = require("./fraim-config-schema.generated");
|
|
10
|
+
exports.FRAIM_CONFIG_SCHEMA = fraim_config_schema_generated_1.FRAIM_CONFIG_SCHEMA;
|
|
11
|
+
const SUPPORTED_FRAIM_CONFIG_PATHS = [...fraim_config_schema_generated_1.SUPPORTED_FRAIM_CONFIG_PATHS];
|
|
12
|
+
const SUPPORTED_FRAIM_CONFIG_PATH_SET = new Set(SUPPORTED_FRAIM_CONFIG_PATHS);
|
|
13
|
+
function isPlainObject(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function listSupportedFraimConfigPaths() {
|
|
17
|
+
return [...SUPPORTED_FRAIM_CONFIG_PATHS];
|
|
18
|
+
}
|
|
19
|
+
function isValidFraimConfigField(path) {
|
|
20
|
+
if (SUPPORTED_FRAIM_CONFIG_PATH_SET.has(path)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
for (const validField of SUPPORTED_FRAIM_CONFIG_PATH_SET) {
|
|
24
|
+
if (validField.startsWith(`${path}.`)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function sanitizeNode(node, value) {
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (node.kind === 'object') {
|
|
35
|
+
if (!isPlainObject(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const sanitized = {};
|
|
39
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
40
|
+
const childValue = sanitizeNode(child, value[key]);
|
|
41
|
+
if (childValue !== undefined) {
|
|
42
|
+
sanitized[key] = childValue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return sanitized;
|
|
46
|
+
}
|
|
47
|
+
if (node.kind === 'record') {
|
|
48
|
+
if (!isPlainObject(value)) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
const sanitized = {};
|
|
52
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
53
|
+
const childValue = sanitizeNode(node.value, entryValue);
|
|
54
|
+
if (childValue !== undefined) {
|
|
55
|
+
sanitized[key] = childValue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sanitized;
|
|
59
|
+
}
|
|
60
|
+
if (node.kind === 'array' && Array.isArray(value)) {
|
|
61
|
+
return value.map((entry) => sanitizeNode(node.element, entry));
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function validateNode(node, value, path, issues) {
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
if (node.required) {
|
|
68
|
+
issues.push({ path, message: `${path} is required.` });
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
switch (node.kind) {
|
|
73
|
+
case 'string':
|
|
74
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
75
|
+
issues.push({ path, message: `${path} must be a non-empty string.` });
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
case 'number':
|
|
79
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
80
|
+
issues.push({ path, message: `${path} must be a finite number.` });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
case 'boolean':
|
|
84
|
+
if (typeof value !== 'boolean') {
|
|
85
|
+
issues.push({ path, message: `${path} must be a boolean.` });
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
case 'enum':
|
|
89
|
+
if (typeof value !== 'string' || !node.values.includes(value)) {
|
|
90
|
+
issues.push({ path, message: `${path} must be one of: ${node.values.join(', ')}.` });
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
case 'array':
|
|
94
|
+
if (!Array.isArray(value)) {
|
|
95
|
+
issues.push({ path, message: `${path} must be an array.` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
value.forEach((entry, index) => validateNode(node.element, entry, `${path}[${index}]`, issues));
|
|
99
|
+
return;
|
|
100
|
+
case 'record':
|
|
101
|
+
if (!isPlainObject(value)) {
|
|
102
|
+
issues.push({ path, message: `${path} must be an object.` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
106
|
+
validateNode(node.value, entryValue, `${path}.${key}`, issues);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
case 'object':
|
|
110
|
+
if (!isPlainObject(value)) {
|
|
111
|
+
issues.push({ path, message: `${path} must be an object.` });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(value)) {
|
|
115
|
+
if (!(key in node.properties)) {
|
|
116
|
+
issues.push({
|
|
117
|
+
path: path ? `${path}.${key}` : key,
|
|
118
|
+
message: `${path ? `${path}.${key}` : key} is not part of the supported FRAIM config schema.`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
123
|
+
validateNode(child, value[key], path ? `${path}.${key}` : key, issues);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function sanitizeFraimConfigToSchema(rawConfig) {
|
|
129
|
+
return sanitizeNode(exports.FRAIM_CONFIG_SCHEMA, rawConfig);
|
|
130
|
+
}
|
|
131
|
+
function validateWorkspaceFraimConfig(rawConfig) {
|
|
132
|
+
const issues = [];
|
|
133
|
+
validateNode(exports.FRAIM_CONFIG_SCHEMA, rawConfig, '', issues);
|
|
134
|
+
return issues.map((issue) => ({
|
|
135
|
+
path: issue.path || '$',
|
|
136
|
+
message: issue.message
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
function assertValidWorkspaceFraimConfig(rawConfig) {
|
|
140
|
+
const issues = validateWorkspaceFraimConfig(rawConfig);
|
|
141
|
+
if (issues.length === 0) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Workspace FRAIM config validation failed:\n${issues.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n')}`);
|
|
145
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Generated from src/core/types.ts.
|
|
4
|
+
* This file exists so runtime validation and config token checks read the same persisted config shape the agent sees.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SUPPORTED_FRAIM_CONFIG_PATHS = exports.FRAIM_CONFIG_SCHEMA = void 0;
|
|
8
|
+
exports.FRAIM_CONFIG_SCHEMA = {
|
|
9
|
+
"kind": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"version": {
|
|
12
|
+
"kind": "string",
|
|
13
|
+
"required": true
|
|
14
|
+
},
|
|
15
|
+
"project": {
|
|
16
|
+
"kind": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"name": {
|
|
19
|
+
"kind": "string",
|
|
20
|
+
"required": true
|
|
21
|
+
},
|
|
22
|
+
"industry": {
|
|
23
|
+
"kind": "string"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": true
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"kind": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"provider": {
|
|
32
|
+
"kind": "enum",
|
|
33
|
+
"values": [
|
|
34
|
+
"github",
|
|
35
|
+
"ado",
|
|
36
|
+
"gitlab"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"owner": {
|
|
40
|
+
"kind": "string"
|
|
41
|
+
},
|
|
42
|
+
"name": {
|
|
43
|
+
"kind": "string"
|
|
44
|
+
},
|
|
45
|
+
"organization": {
|
|
46
|
+
"kind": "string"
|
|
47
|
+
},
|
|
48
|
+
"project": {
|
|
49
|
+
"kind": "string"
|
|
50
|
+
},
|
|
51
|
+
"namespace": {
|
|
52
|
+
"kind": "string"
|
|
53
|
+
},
|
|
54
|
+
"projectPath": {
|
|
55
|
+
"kind": "string"
|
|
56
|
+
},
|
|
57
|
+
"url": {
|
|
58
|
+
"kind": "string"
|
|
59
|
+
},
|
|
60
|
+
"defaultBranch": {
|
|
61
|
+
"kind": "string"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"issueTracking": {
|
|
66
|
+
"kind": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"provider": {
|
|
69
|
+
"kind": "enum",
|
|
70
|
+
"values": [
|
|
71
|
+
"jira",
|
|
72
|
+
"github",
|
|
73
|
+
"ado",
|
|
74
|
+
"linear",
|
|
75
|
+
"gitlab"
|
|
76
|
+
],
|
|
77
|
+
"required": true
|
|
78
|
+
},
|
|
79
|
+
"owner": {
|
|
80
|
+
"kind": "string"
|
|
81
|
+
},
|
|
82
|
+
"name": {
|
|
83
|
+
"kind": "string"
|
|
84
|
+
},
|
|
85
|
+
"organization": {
|
|
86
|
+
"kind": "string"
|
|
87
|
+
},
|
|
88
|
+
"project": {
|
|
89
|
+
"kind": "string"
|
|
90
|
+
},
|
|
91
|
+
"namespace": {
|
|
92
|
+
"kind": "string"
|
|
93
|
+
},
|
|
94
|
+
"projectPath": {
|
|
95
|
+
"kind": "string"
|
|
96
|
+
},
|
|
97
|
+
"baseUrl": {
|
|
98
|
+
"kind": "string"
|
|
99
|
+
},
|
|
100
|
+
"projectKey": {
|
|
101
|
+
"kind": "string"
|
|
102
|
+
},
|
|
103
|
+
"branchPattern": {
|
|
104
|
+
"kind": "string"
|
|
105
|
+
},
|
|
106
|
+
"apiToken": {
|
|
107
|
+
"kind": "string"
|
|
108
|
+
},
|
|
109
|
+
"email": {
|
|
110
|
+
"kind": "string"
|
|
111
|
+
},
|
|
112
|
+
"apiKey": {
|
|
113
|
+
"kind": "string"
|
|
114
|
+
},
|
|
115
|
+
"teamId": {
|
|
116
|
+
"kind": "string"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"mode": {
|
|
121
|
+
"kind": "enum",
|
|
122
|
+
"values": [
|
|
123
|
+
"conversational",
|
|
124
|
+
"integrated",
|
|
125
|
+
"split"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
"customizations": {
|
|
129
|
+
"kind": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"architectureDoc": {
|
|
132
|
+
"kind": "string"
|
|
133
|
+
},
|
|
134
|
+
"designSystem": {
|
|
135
|
+
"kind": "object",
|
|
136
|
+
"properties": {
|
|
137
|
+
"path": {
|
|
138
|
+
"kind": "string"
|
|
139
|
+
},
|
|
140
|
+
"brand": {
|
|
141
|
+
"kind": "string"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"validation": {
|
|
146
|
+
"kind": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"buildCommand": {
|
|
149
|
+
"kind": "string"
|
|
150
|
+
},
|
|
151
|
+
"testSuiteCommand": {
|
|
152
|
+
"kind": "string"
|
|
153
|
+
},
|
|
154
|
+
"smokeTestCommand": {
|
|
155
|
+
"kind": "string"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
"postCleanupHook": {
|
|
160
|
+
"kind": "string"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"remoteUrl": {
|
|
165
|
+
"kind": "string"
|
|
166
|
+
},
|
|
167
|
+
"apiKey": {
|
|
168
|
+
"kind": "string"
|
|
169
|
+
},
|
|
170
|
+
"competitors": {
|
|
171
|
+
"kind": "record",
|
|
172
|
+
"value": {
|
|
173
|
+
"kind": "string"
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"compliance": {
|
|
177
|
+
"kind": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"regulations": {
|
|
180
|
+
"kind": "array",
|
|
181
|
+
"element": {
|
|
182
|
+
"kind": "string"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"compliance_specifications": {
|
|
186
|
+
"kind": "record",
|
|
187
|
+
"value": {
|
|
188
|
+
"kind": "string"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"learning": {
|
|
194
|
+
"kind": "object",
|
|
195
|
+
"properties": {
|
|
196
|
+
"lastSynthesisDate": {
|
|
197
|
+
"kind": "string"
|
|
198
|
+
},
|
|
199
|
+
"scoreThreshold": {
|
|
200
|
+
"kind": "number"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"customer-communication": {
|
|
205
|
+
"kind": "object",
|
|
206
|
+
"properties": {
|
|
207
|
+
"productName": {
|
|
208
|
+
"kind": "string",
|
|
209
|
+
"required": true
|
|
210
|
+
},
|
|
211
|
+
"productUrl": {
|
|
212
|
+
"kind": "string",
|
|
213
|
+
"required": true
|
|
214
|
+
},
|
|
215
|
+
"senderDisplayName": {
|
|
216
|
+
"kind": "string",
|
|
217
|
+
"required": true
|
|
218
|
+
},
|
|
219
|
+
"senderEmail": {
|
|
220
|
+
"kind": "string",
|
|
221
|
+
"required": true
|
|
222
|
+
},
|
|
223
|
+
"senderReplyTo": {
|
|
224
|
+
"kind": "string"
|
|
225
|
+
},
|
|
226
|
+
"newsletterAudienceProvider": {
|
|
227
|
+
"kind": "string"
|
|
228
|
+
},
|
|
229
|
+
"deliveryProvider": {
|
|
230
|
+
"kind": "string",
|
|
231
|
+
"required": true
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
"required": true
|
|
237
|
+
};
|
|
238
|
+
exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
239
|
+
"version",
|
|
240
|
+
"project",
|
|
241
|
+
"project.name",
|
|
242
|
+
"project.industry",
|
|
243
|
+
"repository",
|
|
244
|
+
"repository.provider",
|
|
245
|
+
"repository.owner",
|
|
246
|
+
"repository.name",
|
|
247
|
+
"repository.organization",
|
|
248
|
+
"repository.project",
|
|
249
|
+
"repository.namespace",
|
|
250
|
+
"repository.projectPath",
|
|
251
|
+
"repository.url",
|
|
252
|
+
"repository.defaultBranch",
|
|
253
|
+
"issueTracking",
|
|
254
|
+
"issueTracking.provider",
|
|
255
|
+
"issueTracking.owner",
|
|
256
|
+
"issueTracking.name",
|
|
257
|
+
"issueTracking.organization",
|
|
258
|
+
"issueTracking.project",
|
|
259
|
+
"issueTracking.namespace",
|
|
260
|
+
"issueTracking.projectPath",
|
|
261
|
+
"issueTracking.baseUrl",
|
|
262
|
+
"issueTracking.projectKey",
|
|
263
|
+
"issueTracking.branchPattern",
|
|
264
|
+
"issueTracking.apiToken",
|
|
265
|
+
"issueTracking.email",
|
|
266
|
+
"issueTracking.apiKey",
|
|
267
|
+
"issueTracking.teamId",
|
|
268
|
+
"mode",
|
|
269
|
+
"customizations",
|
|
270
|
+
"customizations.architectureDoc",
|
|
271
|
+
"customizations.designSystem",
|
|
272
|
+
"customizations.designSystem.path",
|
|
273
|
+
"customizations.designSystem.brand",
|
|
274
|
+
"customizations.validation",
|
|
275
|
+
"customizations.validation.buildCommand",
|
|
276
|
+
"customizations.validation.testSuiteCommand",
|
|
277
|
+
"customizations.validation.smokeTestCommand",
|
|
278
|
+
"customizations.postCleanupHook",
|
|
279
|
+
"remoteUrl",
|
|
280
|
+
"apiKey",
|
|
281
|
+
"competitors",
|
|
282
|
+
"compliance",
|
|
283
|
+
"compliance.regulations",
|
|
284
|
+
"compliance.compliance_specifications",
|
|
285
|
+
"learning",
|
|
286
|
+
"learning.lastSynthesisDate",
|
|
287
|
+
"learning.scoreThreshold",
|
|
288
|
+
"customer-communication",
|
|
289
|
+
"customer-communication.productName",
|
|
290
|
+
"customer-communication.productUrl",
|
|
291
|
+
"customer-communication.senderDisplayName",
|
|
292
|
+
"customer-communication.senderEmail",
|
|
293
|
+
"customer-communication.senderReplyTo",
|
|
294
|
+
"customer-communication.newsletterAudienceProvider",
|
|
295
|
+
"customer-communication.deliveryProvider"
|
|
296
|
+
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeSetupHandoffChoice = writeSetupHandoffChoice;
|
|
7
|
+
exports.readSetupHandoffChoice = readSetupHandoffChoice;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const project_fraim_paths_1 = require("./project-fraim-paths");
|
|
11
|
+
const PREFS_FILE = 'preferences.json';
|
|
12
|
+
function getPrefsPath() {
|
|
13
|
+
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), PREFS_FILE);
|
|
14
|
+
}
|
|
15
|
+
function writeSetupHandoffChoice(choice) {
|
|
16
|
+
const dir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
17
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
18
|
+
const filePath = getPrefsPath();
|
|
19
|
+
let existing = {};
|
|
20
|
+
try {
|
|
21
|
+
existing = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// file doesn't exist or is invalid JSON — start fresh
|
|
25
|
+
}
|
|
26
|
+
existing.setupHandoffChoice = choice;
|
|
27
|
+
fs_1.default.writeFileSync(filePath, JSON.stringify(existing, null, 2));
|
|
28
|
+
}
|
|
29
|
+
function readSetupHandoffChoice() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs_1.default.readFileSync(getPrefsPath(), 'utf8');
|
|
32
|
+
const data = JSON.parse(raw);
|
|
33
|
+
if (data.setupHandoffChoice === 'ide' || data.setupHandoffChoice === 'hub') {
|
|
34
|
+
return data.setupHandoffChoice;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -9,6 +42,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
42
|
const path_1 = __importDefault(require("path"));
|
|
10
43
|
const child_process_1 = require("child_process");
|
|
11
44
|
const session_service_1 = require("./session-service");
|
|
45
|
+
const setup_preferences_1 = require("../core/utils/setup-preferences");
|
|
12
46
|
function resolveFirstRunPublicDir() {
|
|
13
47
|
const candidates = [
|
|
14
48
|
path_1.default.resolve(process.cwd(), 'public/first-run'),
|
|
@@ -22,31 +56,63 @@ function resolveFirstRunPublicDir() {
|
|
|
22
56
|
}
|
|
23
57
|
throw new Error('Could not locate public/first-run assets.');
|
|
24
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Open the platform's native folder picker and return the chosen path
|
|
61
|
+
* (or `null` if the user cancelled).
|
|
62
|
+
*
|
|
63
|
+
* Implementation notes:
|
|
64
|
+
* - Async (`spawn`, not `spawnSync`). The folder dialog blocks until the
|
|
65
|
+
* user dismisses it, which can be many seconds. With `spawnSync` the
|
|
66
|
+
* entire Node event loop freezes during that time - every other HTTP
|
|
67
|
+
* request to the FRE server gets `ERR_ABORTED`, and the FRE looks dead.
|
|
68
|
+
* - Windows: PowerShell needs `-STA` (Single-Threaded Apartment) for
|
|
69
|
+
* `System.Windows.Forms.FolderBrowserDialog` to work. We also create a
|
|
70
|
+
* hidden `$owner` form with `TopMost = $true` and pass it as the
|
|
71
|
+
* dialog's owner so the picker comes to the foreground instead of
|
|
72
|
+
* appearing behind the browser (which made the Browse button look
|
|
73
|
+
* broken).
|
|
74
|
+
*/
|
|
25
75
|
function pickProjectPath() {
|
|
76
|
+
// Return contract: Promise<string | null>
|
|
26
77
|
if (process.platform === 'win32') {
|
|
27
78
|
const script = [
|
|
28
79
|
'Add-Type -AssemblyName System.Windows.Forms',
|
|
29
80
|
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
81
|
+
'$dialog.Description = "Select a FRAIM project folder"',
|
|
30
82
|
'$dialog.ShowNewFolderButton = $true',
|
|
31
|
-
|
|
83
|
+
// Hidden owner form forces the dialog above the user's browser. Without
|
|
84
|
+
// this the dialog often appears behind the browser tab and the user
|
|
85
|
+
// sees nothing happen when they click Browse.
|
|
86
|
+
'$owner = New-Object System.Windows.Forms.Form',
|
|
87
|
+
'$owner.TopMost = $true',
|
|
88
|
+
'$owner.ShowInTaskbar = $false',
|
|
89
|
+
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
32
90
|
' Write-Output $dialog.SelectedPath',
|
|
33
91
|
'}',
|
|
92
|
+
'$owner.Dispose()',
|
|
34
93
|
].join('; ');
|
|
35
|
-
|
|
36
|
-
encoding: 'utf8',
|
|
37
|
-
});
|
|
38
|
-
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
94
|
+
return runPickerProcess('powershell', ['-NoProfile', '-STA', '-Command', script]);
|
|
39
95
|
}
|
|
40
96
|
if (process.platform === 'darwin') {
|
|
41
|
-
|
|
42
|
-
encoding: 'utf8',
|
|
43
|
-
});
|
|
44
|
-
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
97
|
+
return runPickerProcess('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select a FRAIM project folder")']);
|
|
45
98
|
}
|
|
46
|
-
|
|
47
|
-
|
|
99
|
+
return runPickerProcess('bash', ['-lc', 'zenity --file-selection --directory 2>/dev/null || kdialog --getexistingdirectory 2>/dev/null']);
|
|
100
|
+
}
|
|
101
|
+
function runPickerProcess(command, args) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const proc = (0, child_process_1.spawn)(command, args, {
|
|
104
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
105
|
+
windowsHide: true,
|
|
106
|
+
});
|
|
107
|
+
let stdout = '';
|
|
108
|
+
proc.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
109
|
+
proc.stderr?.on('data', () => { });
|
|
110
|
+
proc.on('close', () => {
|
|
111
|
+
const trimmed = stdout.trim();
|
|
112
|
+
resolve(trimmed || null);
|
|
113
|
+
});
|
|
114
|
+
proc.on('error', () => resolve(null));
|
|
48
115
|
});
|
|
49
|
-
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
50
116
|
}
|
|
51
117
|
function isCanonicalRowId(value) {
|
|
52
118
|
return typeof value === 'string' && session_service_1.FIRST_RUN_ROW_IDS.includes(value);
|
|
@@ -125,9 +191,9 @@ class FirstRunServer {
|
|
|
125
191
|
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not change agent.' });
|
|
126
192
|
}
|
|
127
193
|
});
|
|
128
|
-
this.app.post('/api/first-run/project-path/pick', (_req, res) => {
|
|
194
|
+
this.app.post('/api/first-run/project-path/pick', async (_req, res) => {
|
|
129
195
|
try {
|
|
130
|
-
const selectedPath = pickProjectPath();
|
|
196
|
+
const selectedPath = await pickProjectPath();
|
|
131
197
|
if (!selectedPath) {
|
|
132
198
|
return res.status(204).end();
|
|
133
199
|
}
|
|
@@ -138,16 +204,50 @@ class FirstRunServer {
|
|
|
138
204
|
}
|
|
139
205
|
});
|
|
140
206
|
this.app.post('/api/first-run/finish', (_req, res) => {
|
|
207
|
+
// Note: /finish writes the next-prompt artifact but does NOT trigger
|
|
208
|
+
// process shutdown. In v1 the Hub server is started in-process by
|
|
209
|
+
// /open-hub - if we resolved the finishPromise here, runFirstRun
|
|
210
|
+
// would call server.stop() and the parent process would exit,
|
|
211
|
+
// taking the just-started Hub down with it. The Hub handoff has
|
|
212
|
+
// to keep the parent alive. The CLI exits when the user Ctrl+Cs
|
|
213
|
+
// the terminal. v2 (#355) replaces this with a detached launcher.
|
|
141
214
|
const result = this.sessionService.finish();
|
|
142
|
-
this.finishResolver?.();
|
|
143
215
|
return res.json(result);
|
|
144
216
|
});
|
|
145
|
-
|
|
217
|
+
this.app.get('/api/first-run/ide-commands', async (_req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
const { detectInstalledIDEs } = await Promise.resolve().then(() => __importStar(require('../cli/setup/ide-detector')));
|
|
220
|
+
const { describeOnboardingInvocationSurfaces } = await Promise.resolve().then(() => __importStar(require('../cli/setup/ide-global-integration')));
|
|
221
|
+
const configuredIDEs = detectInstalledIDEs();
|
|
222
|
+
const commands = describeOnboardingInvocationSurfaces(configuredIDEs);
|
|
223
|
+
return res.json({ commands });
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not load IDE commands.' });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.app.post('/api/first-run/set-preference', (req, res) => {
|
|
230
|
+
const { choice } = req.body || {};
|
|
231
|
+
if (choice !== 'ide' && choice !== 'hub') {
|
|
232
|
+
return res.status(400).json({ error: 'choice must be "ide" or "hub"' });
|
|
233
|
+
}
|
|
234
|
+
(0, setup_preferences_1.writeSetupHandoffChoice)(choice);
|
|
235
|
+
return res.json({ ok: true });
|
|
236
|
+
});
|
|
237
|
+
// Hub-launch helper - starts an AiHubServer for the chosen project and
|
|
146
238
|
// opens the user's browser. v2 (#355) replaces the in-process spawn with
|
|
147
|
-
// a durable launcher binary.
|
|
239
|
+
// a durable launcher binary that survives independently.
|
|
148
240
|
this.app.post('/api/first-run/open-hub', async (_req, res) => {
|
|
149
241
|
try {
|
|
150
|
-
|
|
242
|
+
const result = await this.sessionService.openHub();
|
|
243
|
+
// Write the next-prompt artifact as a side effect of opening the
|
|
244
|
+
// Hub so the client doesn't need a separate /finish call. We
|
|
245
|
+
// intentionally do NOT resolve the finishPromise - see /finish
|
|
246
|
+
// handler comment above.
|
|
247
|
+
if (result.ok) {
|
|
248
|
+
this.sessionService.finish();
|
|
249
|
+
}
|
|
250
|
+
return res.json(result);
|
|
151
251
|
}
|
|
152
252
|
catch (error) {
|
|
153
253
|
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open Hub.' });
|