fraim-framework 2.0.151 → 2.0.153
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/hosts.js +36 -11
- package/dist/src/ai-hub/server.js +47 -17
- package/dist/src/cli/commands/test-mcp.js +171 -0
- package/dist/src/cli/setup/first-run.js +242 -0
- package/dist/src/cli/setup/ide-detector.js +46 -18
- package/dist/src/cli/utils/managed-agent-paths.js +48 -0
- package/dist/src/core/config-writer.js +75 -0
- package/dist/src/core/utils/job-aliases.js +47 -0
- package/dist/src/core/utils/workflow-parser.js +174 -0
- package/dist/src/first-run/session-service.js +35 -48
- package/index.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/index.html +229 -221
- package/public/ai-hub/script.js +472 -408
- package/public/ai-hub/styles.css +660 -582
- package/public/first-run/index.html +35 -35
- package/public/first-run/script.js +667 -667
- package/public/first-run/styles.css +73 -73
|
@@ -62,11 +62,23 @@ const detectWindsurf = () => {
|
|
|
62
62
|
return checkMultiplePaths(paths);
|
|
63
63
|
};
|
|
64
64
|
const detectGeminiCli = () => {
|
|
65
|
-
// Require the binary to be in PATH. The ~/.gemini directory alone is not
|
|
66
|
-
// sufficient — it exists when only Antigravity or other Gemini-adjacent
|
|
67
|
-
// tools are installed, which would produce a false positive.
|
|
68
65
|
return availableByVersionProbe('gemini');
|
|
69
66
|
};
|
|
67
|
+
const detectGeminiSurface = () => {
|
|
68
|
+
const paths = [
|
|
69
|
+
'~/.gemini',
|
|
70
|
+
'~/AppData/Roaming/gemini',
|
|
71
|
+
'~/.config/gemini'
|
|
72
|
+
];
|
|
73
|
+
return checkMultiplePaths(paths);
|
|
74
|
+
};
|
|
75
|
+
const detectCodexSurface = () => {
|
|
76
|
+
const paths = [
|
|
77
|
+
'~/.codex',
|
|
78
|
+
'~/.codex/config.toml'
|
|
79
|
+
];
|
|
80
|
+
return checkMultiplePaths(paths);
|
|
81
|
+
};
|
|
70
82
|
exports.IDE_CONFIGS = [
|
|
71
83
|
{
|
|
72
84
|
name: 'Claude Code',
|
|
@@ -109,7 +121,7 @@ exports.IDE_CONFIGS = [
|
|
|
109
121
|
configFormat: 'json',
|
|
110
122
|
configType: 'gemini-cli',
|
|
111
123
|
invocationProfile: 'gemini-command',
|
|
112
|
-
detectMethod:
|
|
124
|
+
detectMethod: detectGeminiSurface,
|
|
113
125
|
supportsConfigBootstrap: true,
|
|
114
126
|
aliases: ['gemini', 'gemini-cli', 'gemini cli'],
|
|
115
127
|
alternativePaths: [
|
|
@@ -165,7 +177,7 @@ exports.IDE_CONFIGS = [
|
|
|
165
177
|
configFormat: 'toml',
|
|
166
178
|
configType: 'codex',
|
|
167
179
|
invocationProfile: 'codex-skill',
|
|
168
|
-
detectMethod:
|
|
180
|
+
detectMethod: detectCodexSurface,
|
|
169
181
|
description: 'Codex AI development environment'
|
|
170
182
|
},
|
|
171
183
|
{
|
|
@@ -184,11 +196,9 @@ exports.IDE_CONFIGS = [
|
|
|
184
196
|
}
|
|
185
197
|
];
|
|
186
198
|
const findBestConfigPath = (ide) => {
|
|
187
|
-
// First try the default path
|
|
188
199
|
if (fs_1.default.existsSync(expandPath(ide.configPath))) {
|
|
189
200
|
return ide.configPath;
|
|
190
201
|
}
|
|
191
|
-
// Then try alternative paths
|
|
192
202
|
if (ide.alternativePaths) {
|
|
193
203
|
for (const altPath of ide.alternativePaths) {
|
|
194
204
|
if (fs_1.default.existsSync(expandPath(altPath))) {
|
|
@@ -196,26 +206,44 @@ const findBestConfigPath = (ide) => {
|
|
|
196
206
|
}
|
|
197
207
|
}
|
|
198
208
|
}
|
|
199
|
-
// Return default path if nothing found (will be created)
|
|
200
209
|
return ide.configPath;
|
|
201
210
|
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
const _cachedIDEs = new Map();
|
|
212
|
+
const _cacheTimestamps = new Map();
|
|
213
|
+
const _cacheHomeDirs = new Map();
|
|
205
214
|
const DETECT_CACHE_TTL_MS = 5000;
|
|
206
|
-
const
|
|
215
|
+
const isDetectedForMode = (ide, mode) => {
|
|
216
|
+
if (mode === 'cli-runnable') {
|
|
217
|
+
switch (ide.configType) {
|
|
218
|
+
case 'claude-code':
|
|
219
|
+
return availableByVersionProbe('claude');
|
|
220
|
+
case 'codex':
|
|
221
|
+
return availableByVersionProbe('codex');
|
|
222
|
+
case 'gemini-cli':
|
|
223
|
+
return detectGeminiCli();
|
|
224
|
+
default:
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return ide.detectMethod();
|
|
229
|
+
};
|
|
230
|
+
const detectInstalledIDEs = (mode = 'config-surface') => {
|
|
207
231
|
const now = Date.now();
|
|
208
232
|
const currentHome = os_1.default.homedir();
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
const cached = _cachedIDEs.get(mode);
|
|
234
|
+
const cacheTimestamp = _cacheTimestamps.get(mode) || 0;
|
|
235
|
+
const cacheHomeDir = _cacheHomeDirs.get(mode) || '';
|
|
236
|
+
if (cached !== undefined && cacheHomeDir === currentHome && (now - cacheTimestamp) < DETECT_CACHE_TTL_MS) {
|
|
237
|
+
return cached;
|
|
211
238
|
}
|
|
212
|
-
|
|
239
|
+
const detected = exports.IDE_CONFIGS.filter((ide) => isDetectedForMode(ide, mode)).map(ide => ({
|
|
213
240
|
...ide,
|
|
214
241
|
configPath: findBestConfigPath(ide)
|
|
215
242
|
}));
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
243
|
+
_cachedIDEs.set(mode, detected);
|
|
244
|
+
_cacheTimestamps.set(mode, now);
|
|
245
|
+
_cacheHomeDirs.set(mode, currentHome);
|
|
246
|
+
return detected;
|
|
219
247
|
};
|
|
220
248
|
exports.detectInstalledIDEs = detectInstalledIDEs;
|
|
221
249
|
const getAllSupportedIDEs = () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
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.getManagedNodeRoot = getManagedNodeRoot;
|
|
7
|
+
exports.getPortableNodeBinPath = getPortableNodeBinPath;
|
|
8
|
+
exports.getManagedAgentBinDirs = getManagedAgentBinDirs;
|
|
9
|
+
exports.buildPathWithManagedAgentBins = buildPathWithManagedAgentBins;
|
|
10
|
+
exports.prependManagedAgentBinDirsToProcessPath = prependManagedAgentBinDirsToProcessPath;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const script_sync_utils_1 = require("./script-sync-utils");
|
|
14
|
+
function getManagedNodeRoot() {
|
|
15
|
+
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
16
|
+
}
|
|
17
|
+
function getPortableNodeBinPath() {
|
|
18
|
+
const nodeRoot = getManagedNodeRoot();
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
if (fs_1.default.existsSync(nodeRoot)) {
|
|
21
|
+
const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
22
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
|
|
23
|
+
.sort((a, b) => b.name.localeCompare(a.name))[0];
|
|
24
|
+
if (extractedDir) {
|
|
25
|
+
return path_1.default.join(nodeRoot, extractedDir.name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return nodeRoot;
|
|
29
|
+
}
|
|
30
|
+
return path_1.default.join(nodeRoot, 'bin');
|
|
31
|
+
}
|
|
32
|
+
function getManagedAgentBinDirs() {
|
|
33
|
+
const nodeRoot = getManagedNodeRoot();
|
|
34
|
+
const portableNodeBin = getPortableNodeBinPath();
|
|
35
|
+
const candidates = process.platform === 'win32'
|
|
36
|
+
? [nodeRoot, portableNodeBin]
|
|
37
|
+
: [path_1.default.join(nodeRoot, 'bin'), portableNodeBin];
|
|
38
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
39
|
+
}
|
|
40
|
+
function buildPathWithManagedAgentBins(basePath) {
|
|
41
|
+
const current = basePath ?? process.env.PATH ?? '';
|
|
42
|
+
const existing = current.split(path_1.default.delimiter).filter(Boolean);
|
|
43
|
+
const merged = [...getManagedAgentBinDirs(), ...existing];
|
|
44
|
+
return [...new Set(merged)].join(path_1.default.delimiter);
|
|
45
|
+
}
|
|
46
|
+
function prependManagedAgentBinDirsToProcessPath() {
|
|
47
|
+
process.env.PATH = buildPathWithManagedAgentBins(process.env.PATH);
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readFraimConfigRaw = readFraimConfigRaw;
|
|
4
|
+
exports.mergeFraimConfig = mergeFraimConfig;
|
|
5
|
+
exports.writeFraimConfigUpdate = writeFraimConfigUpdate;
|
|
6
|
+
exports.writeFraimConfig = writeFraimConfig;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const config_loader_1 = require("./config-loader");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function deepMerge(baseValue, updateValue) {
|
|
15
|
+
if (updateValue === undefined) {
|
|
16
|
+
return baseValue;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(updateValue)) {
|
|
19
|
+
return [...updateValue];
|
|
20
|
+
}
|
|
21
|
+
if (!isPlainObject(updateValue)) {
|
|
22
|
+
return updateValue;
|
|
23
|
+
}
|
|
24
|
+
const baseObject = isPlainObject(baseValue) ? baseValue : {};
|
|
25
|
+
const merged = { ...baseObject };
|
|
26
|
+
for (const [key, value] of Object.entries(updateValue)) {
|
|
27
|
+
merged[key] = deepMerge(baseObject[key], value);
|
|
28
|
+
}
|
|
29
|
+
return merged;
|
|
30
|
+
}
|
|
31
|
+
function ensureWritableFraimConfigShape(rawConfig) {
|
|
32
|
+
const config = { ...rawConfig };
|
|
33
|
+
if (typeof config.version !== 'string' || config.version.trim().length === 0) {
|
|
34
|
+
config.version = types_1.DEFAULT_FRAIM_CONFIG.version;
|
|
35
|
+
}
|
|
36
|
+
const projectConfig = isPlainObject(config.project) ? config.project : {};
|
|
37
|
+
config.project = {
|
|
38
|
+
...types_1.DEFAULT_FRAIM_CONFIG.project,
|
|
39
|
+
...projectConfig
|
|
40
|
+
};
|
|
41
|
+
const customizationsConfig = isPlainObject(config.customizations) ? config.customizations : {};
|
|
42
|
+
config.customizations = {
|
|
43
|
+
...types_1.DEFAULT_FRAIM_CONFIG.customizations,
|
|
44
|
+
...customizationsConfig
|
|
45
|
+
};
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
function readFraimConfigRaw(configPath) {
|
|
49
|
+
if (!(0, fs_1.existsSync)(configPath)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
53
|
+
if (!isPlainObject(parsed)) {
|
|
54
|
+
throw new Error('FRAIM config must contain a JSON object at the top level.');
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
function mergeFraimConfig(existingRawConfig, update) {
|
|
59
|
+
const rawConfig = ensureWritableFraimConfigShape(deepMerge(existingRawConfig, update));
|
|
60
|
+
return {
|
|
61
|
+
config: (0, config_loader_1.normalizeFraimConfig)(rawConfig),
|
|
62
|
+
created: Object.keys(existingRawConfig).length === 0,
|
|
63
|
+
rawConfig
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function writeFraimConfigUpdate(configPath, update) {
|
|
67
|
+
const existingRawConfig = readFraimConfigRaw(configPath);
|
|
68
|
+
const result = mergeFraimConfig(existingRawConfig, update);
|
|
69
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(configPath), { recursive: true });
|
|
70
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify(result.rawConfig, null, 2));
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function writeFraimConfig(configPath, config) {
|
|
74
|
+
return writeFraimConfigUpdate(configPath, config);
|
|
75
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = void 0;
|
|
4
|
+
exports.resolveFraimJobName = resolveFraimJobName;
|
|
5
|
+
exports.isListableFraimJob = isListableFraimJob;
|
|
6
|
+
const DEPRECATED_TO_CANONICAL_JOB_MAP = {
|
|
7
|
+
'learn-and-scale': 'upskill-employee',
|
|
8
|
+
'model-behavior': 'upskill-employee',
|
|
9
|
+
'promote-learning': 'upskill-employee',
|
|
10
|
+
'refine-jobs': 'upskill-employee',
|
|
11
|
+
'refine-skills': 'upskill-employee'
|
|
12
|
+
};
|
|
13
|
+
const DIRECT_JOB_ALIASES = {
|
|
14
|
+
'sleep on learnings': 'sleep-on-learnings'
|
|
15
|
+
};
|
|
16
|
+
exports.DEPRECATED_FRAIM_JOB_NAMES = new Set(Object.keys(DEPRECATED_TO_CANONICAL_JOB_MAP));
|
|
17
|
+
function normalizeJobLookupInput(input) {
|
|
18
|
+
return input.trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
19
|
+
}
|
|
20
|
+
function resolveFraimJobName(input) {
|
|
21
|
+
const normalizedJobName = normalizeJobLookupInput(input);
|
|
22
|
+
const directAliasTarget = DIRECT_JOB_ALIASES[input.trim().toLowerCase()];
|
|
23
|
+
if (directAliasTarget) {
|
|
24
|
+
return {
|
|
25
|
+
requestedJobName: input,
|
|
26
|
+
normalizedJobName,
|
|
27
|
+
canonicalJobName: directAliasTarget
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const deprecatedAliasTarget = DEPRECATED_TO_CANONICAL_JOB_MAP[normalizedJobName];
|
|
31
|
+
if (deprecatedAliasTarget) {
|
|
32
|
+
return {
|
|
33
|
+
requestedJobName: input,
|
|
34
|
+
normalizedJobName,
|
|
35
|
+
canonicalJobName: deprecatedAliasTarget,
|
|
36
|
+
deprecatedAliasTarget
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
requestedJobName: input,
|
|
41
|
+
normalizedJobName,
|
|
42
|
+
canonicalJobName: normalizedJobName
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isListableFraimJob(jobName) {
|
|
46
|
+
return !exports.DEPRECATED_FRAIM_JOB_NAMES.has(normalizeJobLookupInput(jobName));
|
|
47
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkflowParser = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
class WorkflowParser {
|
|
7
|
+
static extractMetadataBlock(content) {
|
|
8
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
9
|
+
if (frontmatterMatch) {
|
|
10
|
+
try {
|
|
11
|
+
return {
|
|
12
|
+
state: 'valid',
|
|
13
|
+
metadata: JSON.parse(frontmatterMatch[1]),
|
|
14
|
+
bodyStartIndex: frontmatterMatch[0].length
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { state: 'invalid' };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const trimmedStart = content.search(/\S/);
|
|
22
|
+
if (trimmedStart === -1 || content[trimmedStart] !== '{') {
|
|
23
|
+
return { state: 'none' };
|
|
24
|
+
}
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let inString = false;
|
|
27
|
+
let escaping = false;
|
|
28
|
+
for (let i = trimmedStart; i < content.length; i++) {
|
|
29
|
+
const ch = content[i];
|
|
30
|
+
if (inString) {
|
|
31
|
+
if (escaping) {
|
|
32
|
+
escaping = false;
|
|
33
|
+
}
|
|
34
|
+
else if (ch === '\\') {
|
|
35
|
+
escaping = true;
|
|
36
|
+
}
|
|
37
|
+
else if (ch === '"') {
|
|
38
|
+
inString = false;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === '"') {
|
|
43
|
+
inString = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '{') {
|
|
47
|
+
depth++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === '}') {
|
|
51
|
+
depth--;
|
|
52
|
+
if (depth === 0) {
|
|
53
|
+
const bodyStartIndex = i + 1;
|
|
54
|
+
const remainder = content.slice(bodyStartIndex).trimStart();
|
|
55
|
+
// `{...}\n---` is usually malformed frontmatter, not bare JSON metadata.
|
|
56
|
+
if (remainder.startsWith('---')) {
|
|
57
|
+
return { state: 'none' };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return {
|
|
61
|
+
state: 'valid',
|
|
62
|
+
metadata: JSON.parse(content.slice(trimmedStart, bodyStartIndex)),
|
|
63
|
+
bodyStartIndex
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { state: 'invalid' };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { state: 'none' };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse a workflow markdown file into a structured definition
|
|
76
|
+
* Supports three formats:
|
|
77
|
+
* 1. Phase-based workflows with JSON frontmatter
|
|
78
|
+
* 2. Phase-based workflows with bare leading JSON metadata
|
|
79
|
+
* 3. Simple workflows without metadata
|
|
80
|
+
*/
|
|
81
|
+
static parse(filePath) {
|
|
82
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
83
|
+
return null;
|
|
84
|
+
let content = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
85
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
86
|
+
content = content.slice(1);
|
|
87
|
+
}
|
|
88
|
+
const metadataBlock = this.extractMetadataBlock(content);
|
|
89
|
+
if (metadataBlock.state === 'invalid') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (metadataBlock.state === 'valid') {
|
|
93
|
+
return this.parsePhaseBasedWorkflow(filePath, content, metadataBlock.metadata, metadataBlock.bodyStartIndex);
|
|
94
|
+
}
|
|
95
|
+
return this.parseSimpleWorkflow(filePath, content);
|
|
96
|
+
}
|
|
97
|
+
static parsePhaseBasedWorkflow(filePath, content, metadata, bodyStartIndex) {
|
|
98
|
+
const contentAfterMetadata = content.substring(bodyStartIndex).trim();
|
|
99
|
+
const firstPhaseIndex = contentAfterMetadata.search(/^##\s+Phase:/m);
|
|
100
|
+
let overview = '';
|
|
101
|
+
let restOfContent = '';
|
|
102
|
+
if (firstPhaseIndex !== -1) {
|
|
103
|
+
overview = contentAfterMetadata.substring(0, firstPhaseIndex).trim();
|
|
104
|
+
restOfContent = contentAfterMetadata.substring(firstPhaseIndex);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
overview = contentAfterMetadata;
|
|
108
|
+
}
|
|
109
|
+
const phases = new Map();
|
|
110
|
+
const phaseSections = restOfContent.split(/^##\s+Phase:\s+/m);
|
|
111
|
+
if (!metadata.phases) {
|
|
112
|
+
metadata.phases = {};
|
|
113
|
+
}
|
|
114
|
+
for (let i = 1; i < phaseSections.length; i++) {
|
|
115
|
+
const section = phaseSections[i];
|
|
116
|
+
const sectionLines = section.split('\n');
|
|
117
|
+
const firstLine = sectionLines[0].trim();
|
|
118
|
+
const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
|
|
119
|
+
phases.set(id, `## Phase: ${section.trim()}`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
metadata,
|
|
123
|
+
overview,
|
|
124
|
+
phases,
|
|
125
|
+
isSimple: false,
|
|
126
|
+
path: filePath
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
static parseSimpleWorkflow(filePath, content) {
|
|
130
|
+
const workflowName = (0, path_1.basename)(filePath, '.md');
|
|
131
|
+
const metadata = {
|
|
132
|
+
name: workflowName
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
metadata,
|
|
136
|
+
overview: content.trim(),
|
|
137
|
+
phases: new Map(),
|
|
138
|
+
isSimple: true,
|
|
139
|
+
path: filePath
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
static parseContent(content, name, path) {
|
|
143
|
+
if (content.charCodeAt(0) === 0xfeff) {
|
|
144
|
+
content = content.slice(1);
|
|
145
|
+
}
|
|
146
|
+
const metadataBlock = this.extractMetadataBlock(content);
|
|
147
|
+
if (metadataBlock.state === 'invalid') {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (metadataBlock.state === 'valid') {
|
|
151
|
+
return this.parsePhaseBasedWorkflow(path || `content:${name}`, content, metadataBlock.metadata, metadataBlock.bodyStartIndex);
|
|
152
|
+
}
|
|
153
|
+
return this.parseSimpleWorkflow(path || `content:${name}`, content);
|
|
154
|
+
}
|
|
155
|
+
static getOverviewFromContent(content, name) {
|
|
156
|
+
const wf = this.parseContent(content, name);
|
|
157
|
+
return wf ? wf.overview : null;
|
|
158
|
+
}
|
|
159
|
+
static getOverview(filePath) {
|
|
160
|
+
const wf = this.parse(filePath);
|
|
161
|
+
return wf ? wf.overview : null;
|
|
162
|
+
}
|
|
163
|
+
static extractDescription(filePath) {
|
|
164
|
+
const wf = this.parse(filePath);
|
|
165
|
+
if (!wf)
|
|
166
|
+
return '';
|
|
167
|
+
const intentMatch = wf.overview.match(/## Intent\s+([\s\S]+?)(?:\r?\n##|$)/);
|
|
168
|
+
if (intentMatch)
|
|
169
|
+
return intentMatch[1].trim().split(/\r?\n/)[0];
|
|
170
|
+
const firstPara = wf.overview.split(/\r?\n/).find(l => l.trim() !== '' && !l.startsWith('#'));
|
|
171
|
+
return firstPara ? firstPara.trim() : '';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.WorkflowParser = WorkflowParser;
|
|
@@ -47,6 +47,7 @@ const ide_global_integration_1 = require("../cli/setup/ide-global-integration");
|
|
|
47
47
|
const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
|
|
48
48
|
const setup_1 = require("../cli/commands/setup");
|
|
49
49
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
50
|
+
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
50
51
|
const types_1 = require("./types");
|
|
51
52
|
Object.defineProperty(exports, "FIRST_RUN_ROW_IDS", { enumerable: true, get: function () { return types_1.FIRST_RUN_ROW_IDS; } });
|
|
52
53
|
const install_state_1 = require("./install-state");
|
|
@@ -59,13 +60,16 @@ function getFakeStateMode() {
|
|
|
59
60
|
}
|
|
60
61
|
return 'default';
|
|
61
62
|
}
|
|
62
|
-
function commandVersion(command,
|
|
63
|
+
function commandVersion(command, extraBinDirs) {
|
|
63
64
|
const executable = process.platform === 'win32' ? 'cmd.exe' : command;
|
|
64
65
|
const args = process.platform === 'win32'
|
|
65
66
|
? ['/d', '/s', '/c', `${command} --version`]
|
|
66
67
|
: ['--version'];
|
|
67
|
-
const env =
|
|
68
|
-
? {
|
|
68
|
+
const env = extraBinDirs && extraBinDirs.length > 0
|
|
69
|
+
? {
|
|
70
|
+
...process.env,
|
|
71
|
+
PATH: [...new Set([...extraBinDirs, ...(process.env.PATH || '').split(path_1.default.delimiter).filter(Boolean)])].join(path_1.default.delimiter),
|
|
72
|
+
}
|
|
69
73
|
: undefined;
|
|
70
74
|
const result = (0, child_process_1.spawnSync)(executable, args, {
|
|
71
75
|
encoding: 'utf8',
|
|
@@ -81,51 +85,25 @@ function ensureOutputDirs() {
|
|
|
81
85
|
fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
|
|
82
86
|
fs_1.default.mkdirSync(path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install'), { recursive: true });
|
|
83
87
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Returns the directory that contains node/npm/npx executables for FRAIM's
|
|
86
|
-
* portable Node installation.
|
|
87
|
-
*
|
|
88
|
-
* - Mac/Linux: ~/.fraim/node/bin (standard Unix layout)
|
|
89
|
-
* - Windows: ~/.fraim/node/node-v<version>-win-x64/ if extracted, else
|
|
90
|
-
* ~/.fraim/node/ as fallback (executables live at the root on Windows)
|
|
91
|
-
*/
|
|
92
|
-
function getFraimNodeBinPath() {
|
|
93
|
-
const nodeRoot = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
94
|
-
if (process.platform === 'win32') {
|
|
95
|
-
if (fs_1.default.existsSync(nodeRoot)) {
|
|
96
|
-
const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
97
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
|
|
98
|
-
.sort((a, b) => b.name.localeCompare(a.name))[0];
|
|
99
|
-
if (extractedDir) {
|
|
100
|
-
return path_1.default.join(nodeRoot, extractedDir.name);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return nodeRoot;
|
|
104
|
-
}
|
|
105
|
-
return path_1.default.join(nodeRoot, 'bin');
|
|
106
|
-
}
|
|
107
88
|
// Prepend the portable Node bin dir to process PATH once at module load so
|
|
108
89
|
// every spawnSync call (detection, login probe, change-agent) finds binaries
|
|
109
90
|
// installed there without needing per-call path overrides.
|
|
110
91
|
(function bootstrapFraimNodeBin() {
|
|
111
|
-
|
|
112
|
-
const current = process.env.PATH || '';
|
|
113
|
-
if (!current.split(path_1.default.delimiter).includes(fraimNodeBin)) {
|
|
114
|
-
process.env.PATH = `${fraimNodeBin}${path_1.default.delimiter}${current}`;
|
|
115
|
-
}
|
|
92
|
+
(0, managed_agent_paths_1.prependManagedAgentBinDirsToProcessPath)();
|
|
116
93
|
})();
|
|
117
94
|
function persistShellPath() {
|
|
118
|
-
const fraimNodeBin = getFraimNodeBinPath();
|
|
119
95
|
const marker = '# FRAIM managed binaries';
|
|
120
96
|
const exportLine = 'export PATH="$HOME/.fraim/node/bin:$PATH"';
|
|
121
97
|
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
122
98
|
if (process.platform === 'win32') {
|
|
123
|
-
const
|
|
124
|
-
|
|
99
|
+
const bins = (0, managed_agent_paths_1.getManagedAgentBinDirs)();
|
|
100
|
+
const assignments = bins.map((entry, index) => `$bin${index} = '${entry.replace(/'/g, "''")}'`);
|
|
101
|
+
const updates = bins.map((_, index) => `if ($cur -notlike "*$bin${index}*") { $cur = "$bin${index};$cur" }`);
|
|
125
102
|
const psCmd = [
|
|
126
|
-
|
|
103
|
+
...assignments,
|
|
127
104
|
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
128
|
-
|
|
105
|
+
...updates,
|
|
106
|
+
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
129
107
|
].join('; ');
|
|
130
108
|
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
131
109
|
return;
|
|
@@ -165,8 +143,10 @@ function appendInstallLog(line) {
|
|
|
165
143
|
}
|
|
166
144
|
function runProcess(command, args, env) {
|
|
167
145
|
return new Promise((resolve, reject) => {
|
|
168
|
-
const
|
|
169
|
-
|
|
146
|
+
const [realCmd, realArgs] = process.platform === 'win32'
|
|
147
|
+
? ['cmd.exe', ['/d', '/s', '/c', command, ...args]]
|
|
148
|
+
: [command, args];
|
|
149
|
+
const child = (0, child_process_1.spawn)(realCmd, realArgs, {
|
|
170
150
|
env: { ...process.env, ...(env || {}) },
|
|
171
151
|
shell: false,
|
|
172
152
|
});
|
|
@@ -650,6 +630,10 @@ class FirstRunSessionService {
|
|
|
650
630
|
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
651
631
|
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
652
632
|
await runProcess('npm', ['install', '-g', option.installPackage], { npm_config_prefix: prefix });
|
|
633
|
+
const ver = commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
634
|
+
if (!ver) {
|
|
635
|
+
throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH.`);
|
|
636
|
+
}
|
|
653
637
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
654
638
|
if (detectedIDEs.length > 0) {
|
|
655
639
|
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, {}, detectedIDEs.map((ide) => ide.name), {});
|
|
@@ -678,7 +662,7 @@ class FirstRunSessionService {
|
|
|
678
662
|
return { ok: true, message: `${option.label} login triggered (fake-mode).` };
|
|
679
663
|
}
|
|
680
664
|
try {
|
|
681
|
-
this.openTerminalWithCommand(option.loginCommand);
|
|
665
|
+
this.openTerminalWithCommand(this.buildManagedLoginCommand(option.loginCommand));
|
|
682
666
|
appendInstallLog(`agent-login-triggered ${agentId}`);
|
|
683
667
|
return {
|
|
684
668
|
ok: true,
|
|
@@ -701,7 +685,7 @@ class FirstRunSessionService {
|
|
|
701
685
|
if (this.fakeMode) {
|
|
702
686
|
return { ok: true, ready: true, message: `${option.label} is ready (fake-mode).` };
|
|
703
687
|
}
|
|
704
|
-
const ver = commandVersion(option.launchCommand);
|
|
688
|
+
const ver = commandVersion(option.launchCommand, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
705
689
|
if (ver) {
|
|
706
690
|
this.updateAgentSummaryRow();
|
|
707
691
|
this.persist();
|
|
@@ -736,20 +720,23 @@ class FirstRunSessionService {
|
|
|
736
720
|
}
|
|
737
721
|
}
|
|
738
722
|
}
|
|
723
|
+
buildManagedLoginCommand(command) {
|
|
724
|
+
const managedPath = (0, managed_agent_paths_1.buildPathWithManagedAgentBins)(process.env.PATH);
|
|
725
|
+
if (process.platform === 'win32') {
|
|
726
|
+
return `set "PATH=${managedPath}" && ${command}`;
|
|
727
|
+
}
|
|
728
|
+
return `export PATH="${managedPath}"; ${command}`;
|
|
729
|
+
}
|
|
739
730
|
async openHub() {
|
|
740
731
|
if (this.fakeMode) {
|
|
741
732
|
// Tests don't actually want a Hub server running — just confirm intent.
|
|
742
733
|
return { ok: true, message: 'Fake-mode Hub open requested.', hubUrl: 'http://127.0.0.1:0/ai-hub/?firstRun=true' };
|
|
743
734
|
}
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
// their IDE config files exist (created on first run / login).
|
|
735
|
+
// Hub launches a real CLI process, so folder-only config surfaces are not
|
|
736
|
+
// enough here. Require a runnable command in the managed or ambient PATH.
|
|
747
737
|
const hubCompatibleBinaries = ['claude', 'codex'];
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
const hasConfiguredCli = surfaces.some((s) => hubCompatibleIds.has(s.id));
|
|
751
|
-
const hasInstalledCli = hubCompatibleBinaries.some((cmd) => commandVersion(cmd) !== null);
|
|
752
|
-
if (!hasConfiguredCli && !hasInstalledCli) {
|
|
738
|
+
const hasInstalledCli = hubCompatibleBinaries.some((cmd) => commandVersion(cmd, (0, managed_agent_paths_1.getManagedAgentBinDirs)()) !== null);
|
|
739
|
+
if (!hasInstalledCli) {
|
|
753
740
|
return {
|
|
754
741
|
ok: false,
|
|
755
742
|
needsAgentSetup: true,
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.153",
|
|
4
4
|
"description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": "./bin/fraim.js",
|