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.
@@ -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: detectGeminiCli,
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: () => availableByVersionProbe('codex'),
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
- let _cachedIDEs = null;
203
- let _cacheTimestamp = 0;
204
- let _cacheHomeDir = '';
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 detectInstalledIDEs = () => {
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
- if (_cachedIDEs !== null && _cacheHomeDir === currentHome && (now - _cacheTimestamp) < DETECT_CACHE_TTL_MS) {
210
- return _cachedIDEs;
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
- _cachedIDEs = exports.IDE_CONFIGS.filter(ide => ide.detectMethod()).map(ide => ({
239
+ const detected = exports.IDE_CONFIGS.filter((ide) => isDetectedForMode(ide, mode)).map(ide => ({
213
240
  ...ide,
214
241
  configPath: findBestConfigPath(ide)
215
242
  }));
216
- _cacheTimestamp = now;
217
- _cacheHomeDir = currentHome;
218
- return _cachedIDEs;
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, extraBinDir) {
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 = extraBinDir
68
- ? { ...process.env, PATH: `${extraBinDir}${path_1.default.delimiter}${process.env.PATH || ''}` }
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
- const fraimNodeBin = getFraimNodeBinPath();
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 escapedBin = fraimNodeBin.replace(/'/g, "''");
124
- // Guard pattern matches both the versioned subdirectory and the root fallback.
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
- `$bin = '${escapedBin}'`,
103
+ ...assignments,
127
104
  `$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
128
- `if ($cur -notlike "*$bin*") { [Environment]::SetEnvironmentVariable('PATH', "$bin;$cur", 'User') }`,
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 executable = process.platform === 'win32' && command === 'npm' ? 'npm.cmd' : command;
169
- const child = (0, child_process_1.spawn)(executable, args, {
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
- // Require at least one Hub-compatible CLI to be installed (binary on PATH)
745
- // OR fully IDE-configured. Accept freshly-installed binaries even before
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 hubCompatibleIds = new Set(['claude-code', 'codex']);
749
- const surfaces = buildConfiguredSurfaces();
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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.151",
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",