agileflow 2.89.3 → 2.90.1
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/CHANGELOG.md +10 -0
- package/lib/placeholder-registry.js +617 -0
- package/lib/smart-json-file.js +228 -1
- package/lib/table-formatter.js +519 -0
- package/lib/transient-status.js +374 -0
- package/lib/ui-manager.js +612 -0
- package/lib/validate-args.js +213 -0
- package/lib/validate-names.js +143 -0
- package/lib/validate-paths.js +434 -0
- package/lib/validate.js +37 -737
- package/package.json +3 -1
- package/scripts/check-update.js +17 -3
- package/scripts/lib/sessionRegistry.js +678 -0
- package/scripts/session-manager.js +77 -10
- package/scripts/tui/App.js +151 -0
- package/scripts/tui/index.js +31 -0
- package/scripts/tui/lib/crashRecovery.js +304 -0
- package/scripts/tui/lib/eventStream.js +309 -0
- package/scripts/tui/lib/keyboard.js +261 -0
- package/scripts/tui/lib/loopControl.js +371 -0
- package/scripts/tui/panels/OutputPanel.js +242 -0
- package/scripts/tui/panels/SessionPanel.js +170 -0
- package/scripts/tui/panels/TracePanel.js +298 -0
- package/scripts/tui/simple-tui.js +390 -0
- package/tools/cli/commands/config.js +7 -31
- package/tools/cli/commands/doctor.js +28 -39
- package/tools/cli/commands/list.js +47 -35
- package/tools/cli/commands/status.js +20 -38
- package/tools/cli/commands/tui.js +59 -0
- package/tools/cli/commands/uninstall.js +12 -39
- package/tools/cli/installers/core/installer.js +13 -0
- package/tools/cli/lib/command-context.js +382 -0
- package/tools/cli/lib/config-manager.js +394 -0
- package/tools/cli/lib/ide-registry.js +186 -0
- package/tools/cli/lib/npm-utils.js +17 -3
- package/tools/cli/lib/self-update.js +148 -0
- package/tools/cli/lib/validation-middleware.js +491 -0
|
@@ -13,6 +13,7 @@ const { displayLogo, displaySection, success, warning, info } = require('../lib/
|
|
|
13
13
|
const {
|
|
14
14
|
parseFrontmatter: parseYamlFrontmatter,
|
|
15
15
|
} = require('../../../scripts/lib/frontmatter-parser');
|
|
16
|
+
const { formatList, formatKeyValue, formatHeader, isTTY } = require('../../../lib/table-formatter');
|
|
16
17
|
|
|
17
18
|
const installer = new Installer();
|
|
18
19
|
|
|
@@ -300,73 +301,84 @@ function extractFirstLine(content) {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
/**
|
|
303
|
-
* Display compact output
|
|
304
|
+
* Display compact output using formatKeyValue
|
|
304
305
|
*/
|
|
305
306
|
function displayCompact(result, showCommands, showAgents, showSkills, showExperts) {
|
|
307
|
+
const data = {};
|
|
308
|
+
|
|
306
309
|
if (showCommands && result.commands?.length > 0) {
|
|
307
|
-
|
|
310
|
+
data.Commands = result.commands.map(c => c.name).join(', ');
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
if (showAgents && result.agents?.length > 0) {
|
|
311
|
-
|
|
314
|
+
data.Agents = result.agents.map(a => a.name).join(', ');
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
if (showSkills && result.skills?.length > 0) {
|
|
315
|
-
|
|
318
|
+
data.Skills = result.skills.map(s => s.name).join(', ');
|
|
316
319
|
}
|
|
317
320
|
|
|
318
321
|
if (showExperts && result.experts?.length > 0) {
|
|
319
|
-
|
|
322
|
+
data.Experts = result.experts.map(e => e.name).join(', ');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (Object.keys(data).length > 0) {
|
|
326
|
+
console.log(formatKeyValue(data, { alignValues: false }));
|
|
320
327
|
}
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
/**
|
|
324
|
-
* Display full output with descriptions
|
|
331
|
+
* Display full output with descriptions using formatList
|
|
325
332
|
*/
|
|
326
333
|
function displayFull(result, showCommands, showAgents, showSkills, showExperts) {
|
|
327
|
-
|
|
328
|
-
displaySection(`Commands (${result.commands.length})`);
|
|
334
|
+
const { BRAND_HEX } = require('../../../lib/colors');
|
|
329
335
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
if (showCommands && result.commands?.length > 0) {
|
|
337
|
+
console.log(formatHeader(`Commands (${result.commands.length})`));
|
|
338
|
+
const items = result.commands.map(cmd => ({
|
|
339
|
+
text: `${chalk.hex(BRAND_HEX)(cmd.name)}\n ${chalk.dim(cmd.description)}`,
|
|
340
|
+
status: 'active',
|
|
341
|
+
}));
|
|
342
|
+
console.log(formatList(items, { indent: ' ' }));
|
|
334
343
|
}
|
|
335
344
|
|
|
336
345
|
if (showAgents && result.agents?.length > 0) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
for (const agent of result.agents) {
|
|
346
|
+
console.log(formatHeader(`Agents (${result.agents.length})`));
|
|
347
|
+
const items = result.agents.map(agent => {
|
|
340
348
|
const modelBadge = agent.model !== 'default' ? chalk.dim(` [${agent.model}]`) : '';
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
return {
|
|
350
|
+
text: `${chalk.hex(BRAND_HEX)(agent.name)}${modelBadge}\n ${chalk.dim(agent.description)}`,
|
|
351
|
+
status: 'active',
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
console.log(formatList(items, { indent: ' ' }));
|
|
344
355
|
}
|
|
345
356
|
|
|
346
357
|
if (showSkills && result.skills?.length > 0) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
console.log(chalk.hex('#e8683a')(` ${skill.name}`));
|
|
351
|
-
console.log(chalk.dim(` ${skill.description}`));
|
|
358
|
+
console.log(formatHeader(`Skills (${result.skills.length})`));
|
|
359
|
+
const items = result.skills.map(skill => {
|
|
360
|
+
let desc = chalk.dim(skill.description);
|
|
352
361
|
if (skill.triggers?.length > 0) {
|
|
353
|
-
|
|
354
|
-
chalk.dim(
|
|
355
|
-
` Triggers: ${skill.triggers.slice(0, 3).join(', ')}${skill.triggers.length > 3 ? '...' : ''}`
|
|
356
|
-
)
|
|
357
|
-
);
|
|
362
|
+
desc += `\n ${chalk.dim(`Triggers: ${skill.triggers.slice(0, 3).join(', ')}${skill.triggers.length > 3 ? '...' : ''}`)}`;
|
|
358
363
|
}
|
|
359
|
-
|
|
364
|
+
return {
|
|
365
|
+
text: `${chalk.hex(BRAND_HEX)(skill.name)}\n ${desc}`,
|
|
366
|
+
status: 'active',
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
console.log(formatList(items, { indent: ' ' }));
|
|
360
370
|
}
|
|
361
371
|
|
|
362
372
|
if (showExperts && result.experts?.length > 0) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
for (const expert of result.experts) {
|
|
373
|
+
console.log(formatHeader(`Experts (${result.experts.length})`));
|
|
374
|
+
const items = result.experts.map(expert => {
|
|
366
375
|
const versionBadge = expert.version !== 'unknown' ? chalk.dim(` v${expert.version}`) : '';
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
376
|
+
return {
|
|
377
|
+
text: `${chalk.hex(BRAND_HEX)(expert.name)}${versionBadge}\n ${chalk.dim(expert.description)}`,
|
|
378
|
+
status: 'active',
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
console.log(formatList(items, { indent: ' ' }));
|
|
370
382
|
}
|
|
371
383
|
|
|
372
384
|
console.log(); // Final newline
|
|
@@ -11,6 +11,8 @@ const ora = require('ora');
|
|
|
11
11
|
const { Installer } = require('../installers/core/installer');
|
|
12
12
|
const { displayLogo, displaySection, success, warning, info } = require('../lib/ui');
|
|
13
13
|
const { checkForUpdate } = require('../lib/version-checker');
|
|
14
|
+
const { IdeRegistry } = require('../lib/ide-registry');
|
|
15
|
+
const { formatKeyValue, formatList, isTTY } = require('../../../lib/table-formatter');
|
|
14
16
|
|
|
15
17
|
const installer = new Installer();
|
|
16
18
|
|
|
@@ -33,14 +35,25 @@ module.exports = {
|
|
|
33
35
|
process.exit(0);
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
// Show installation info
|
|
37
|
-
console.log(
|
|
38
|
-
|
|
38
|
+
// Show installation info using formatKeyValue
|
|
39
|
+
console.log(
|
|
40
|
+
formatKeyValue({
|
|
41
|
+
Location: status.path,
|
|
42
|
+
Version: status.version,
|
|
43
|
+
})
|
|
44
|
+
);
|
|
39
45
|
|
|
40
46
|
// Count installed items
|
|
41
47
|
const counts = await installer.countInstalledItems(status.path);
|
|
42
48
|
|
|
43
|
-
console.log(
|
|
49
|
+
console.log(
|
|
50
|
+
formatKeyValue(
|
|
51
|
+
{
|
|
52
|
+
'\nCore': chalk.green('✓ Installed'),
|
|
53
|
+
},
|
|
54
|
+
{ alignValues: false }
|
|
55
|
+
)
|
|
56
|
+
);
|
|
44
57
|
info(`${counts.agents} agents`);
|
|
45
58
|
info(`${counts.commands} commands`);
|
|
46
59
|
info(`${counts.skills} skills`);
|
|
@@ -50,13 +63,13 @@ module.exports = {
|
|
|
50
63
|
console.log(chalk.bold('\nConfigured IDEs:'));
|
|
51
64
|
for (const ide of status.ides) {
|
|
52
65
|
// Check if IDE config exists
|
|
53
|
-
const ideConfigPath =
|
|
66
|
+
const ideConfigPath = IdeRegistry.getConfigPath(ide, directory);
|
|
54
67
|
const exists = await fs.pathExists(ideConfigPath);
|
|
55
68
|
|
|
56
69
|
if (exists) {
|
|
57
|
-
success(
|
|
70
|
+
success(IdeRegistry.getDisplayName(ide));
|
|
58
71
|
} else {
|
|
59
|
-
warning(`${
|
|
72
|
+
warning(`${IdeRegistry.getDisplayName(ide)} (config missing)`);
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
}
|
|
@@ -86,34 +99,3 @@ module.exports = {
|
|
|
86
99
|
}
|
|
87
100
|
},
|
|
88
101
|
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get IDE config path
|
|
92
|
-
* @param {string} projectDir - Project directory
|
|
93
|
-
* @param {string} ide - IDE name
|
|
94
|
-
* @returns {string}
|
|
95
|
-
*/
|
|
96
|
-
function getIdeConfigPath(projectDir, ide) {
|
|
97
|
-
const paths = {
|
|
98
|
-
'claude-code': '.claude/commands/agileflow',
|
|
99
|
-
cursor: '.cursor/rules/agileflow',
|
|
100
|
-
windsurf: '.windsurf/workflows/agileflow',
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
return path.join(projectDir, paths[ide] || '');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Format IDE name for display
|
|
108
|
-
* @param {string} ide - IDE name
|
|
109
|
-
* @returns {string}
|
|
110
|
-
*/
|
|
111
|
-
function formatIdeName(ide) {
|
|
112
|
-
const names = {
|
|
113
|
-
'claude-code': 'Claude Code',
|
|
114
|
-
cursor: 'Cursor',
|
|
115
|
-
windsurf: 'Windsurf',
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
return names[ide] || ide;
|
|
119
|
-
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgileFlow CLI - TUI Command
|
|
3
|
+
*
|
|
4
|
+
* Launches the Terminal User Interface for real-time session monitoring.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { spawn } = require('node:child_process');
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
name: 'tui',
|
|
14
|
+
description: 'Launch Terminal User Interface for session monitoring',
|
|
15
|
+
options: [['-d, --directory <path>', 'Project directory (default: current directory)']],
|
|
16
|
+
action: async options => {
|
|
17
|
+
try {
|
|
18
|
+
const directory = path.resolve(options.directory || '.');
|
|
19
|
+
|
|
20
|
+
// Check if AgileFlow is installed
|
|
21
|
+
const agileflowDir = path.join(directory, '.agileflow');
|
|
22
|
+
if (!(await fs.pathExists(agileflowDir))) {
|
|
23
|
+
console.error(chalk.red('Error:'), 'AgileFlow is not installed in this directory');
|
|
24
|
+
console.log(chalk.dim(`Run 'npx agileflow setup' first\n`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Find the TUI script
|
|
29
|
+
const tuiScript = path.join(__dirname, '../../..', 'scripts', 'tui', 'index.js');
|
|
30
|
+
|
|
31
|
+
if (!(await fs.pathExists(tuiScript))) {
|
|
32
|
+
console.error(chalk.red('Error:'), 'TUI script not found');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Spawn the TUI process with inherited stdio for interactive mode
|
|
37
|
+
const child = spawn('node', [tuiScript], {
|
|
38
|
+
cwd: directory,
|
|
39
|
+
stdio: 'inherit',
|
|
40
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on('error', err => {
|
|
44
|
+
console.error(chalk.red('Error launching TUI:'), err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
child.on('exit', code => {
|
|
49
|
+
process.exit(code || 0);
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(chalk.red('Error:'), err.message);
|
|
53
|
+
if (process.env.DEBUG) {
|
|
54
|
+
console.error(err.stack);
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -11,6 +11,7 @@ const { Installer } = require('../installers/core/installer');
|
|
|
11
11
|
const { IdeManager } = require('../installers/ide/manager');
|
|
12
12
|
const { displayLogo, displaySection, success, warning, error, confirm } = require('../lib/ui');
|
|
13
13
|
const { ErrorHandler } = require('../lib/error-handler');
|
|
14
|
+
const { IdeRegistry } = require('../lib/ide-registry');
|
|
14
15
|
|
|
15
16
|
const installer = new Installer();
|
|
16
17
|
const ideManager = new IdeManager();
|
|
@@ -40,17 +41,20 @@ module.exports = {
|
|
|
40
41
|
// Check if removing just one IDE
|
|
41
42
|
if (options.ide) {
|
|
42
43
|
const ideName = options.ide.toLowerCase();
|
|
43
|
-
displaySection('Removing IDE Configuration', `IDE: ${
|
|
44
|
+
displaySection('Removing IDE Configuration', `IDE: ${IdeRegistry.getDisplayName(ideName)}`);
|
|
44
45
|
|
|
45
46
|
if (!status.ides || !status.ides.includes(ideName)) {
|
|
46
|
-
warning(`${
|
|
47
|
+
warning(`${IdeRegistry.getDisplayName(ideName)} is not configured in this installation`);
|
|
47
48
|
console.log(chalk.dim(`Configured IDEs: ${(status.ides || []).join(', ') || 'none'}\n`));
|
|
48
49
|
process.exit(0);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// Confirm removal
|
|
52
53
|
if (!options.force) {
|
|
53
|
-
const proceed = await confirm(
|
|
54
|
+
const proceed = await confirm(
|
|
55
|
+
`Remove ${IdeRegistry.getDisplayName(ideName)} configuration?`,
|
|
56
|
+
false
|
|
57
|
+
);
|
|
54
58
|
if (!proceed) {
|
|
55
59
|
console.log(chalk.dim('\nCancelled\n'));
|
|
56
60
|
process.exit(0);
|
|
@@ -60,10 +64,10 @@ module.exports = {
|
|
|
60
64
|
console.log();
|
|
61
65
|
|
|
62
66
|
// Remove the IDE configuration
|
|
63
|
-
const configPath =
|
|
67
|
+
const configPath = IdeRegistry.getConfigPath(ideName, directory);
|
|
64
68
|
if (await fs.pathExists(configPath)) {
|
|
65
69
|
await fs.remove(configPath);
|
|
66
|
-
success(`Removed ${
|
|
70
|
+
success(`Removed ${IdeRegistry.getDisplayName(ideName)} configuration`);
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// Also remove spawnable agents for claude-code
|
|
@@ -87,7 +91,7 @@ module.exports = {
|
|
|
87
91
|
success('Updated manifest');
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
console.log(chalk.green(`\n${
|
|
94
|
+
console.log(chalk.green(`\n${IdeRegistry.getDisplayName(ideName)} has been removed.\n`));
|
|
91
95
|
if (status.ides.length > 1) {
|
|
92
96
|
console.log(
|
|
93
97
|
chalk.dim(`Remaining IDEs: ${status.ides.filter(i => i !== ideName).join(', ')}\n`)
|
|
@@ -114,10 +118,10 @@ module.exports = {
|
|
|
114
118
|
// Remove IDE configurations
|
|
115
119
|
if (status.ides && status.ides.length > 0) {
|
|
116
120
|
for (const ide of status.ides) {
|
|
117
|
-
const configPath =
|
|
121
|
+
const configPath = IdeRegistry.getConfigPath(ide, directory);
|
|
118
122
|
if (await fs.pathExists(configPath)) {
|
|
119
123
|
await fs.remove(configPath);
|
|
120
|
-
success(`Removed ${
|
|
124
|
+
success(`Removed ${IdeRegistry.getDisplayName(ide)} configuration`);
|
|
121
125
|
}
|
|
122
126
|
// Also remove spawnable agents for claude-code
|
|
123
127
|
if (ide === 'claude-code') {
|
|
@@ -149,34 +153,3 @@ module.exports = {
|
|
|
149
153
|
}
|
|
150
154
|
},
|
|
151
155
|
};
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get IDE config path
|
|
155
|
-
* @param {string} projectDir - Project directory
|
|
156
|
-
* @param {string} ide - IDE name
|
|
157
|
-
* @returns {string}
|
|
158
|
-
*/
|
|
159
|
-
function getIdeConfigPath(projectDir, ide) {
|
|
160
|
-
const paths = {
|
|
161
|
-
'claude-code': '.claude/commands/agileflow',
|
|
162
|
-
cursor: '.cursor/rules/agileflow',
|
|
163
|
-
windsurf: '.windsurf/workflows/agileflow',
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
return path.join(projectDir, paths[ide] || '');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Format IDE name for display
|
|
171
|
-
* @param {string} ide - IDE name
|
|
172
|
-
* @returns {string}
|
|
173
|
-
*/
|
|
174
|
-
function formatIdeName(ide) {
|
|
175
|
-
const names = {
|
|
176
|
-
'claude-code': 'Claude Code',
|
|
177
|
-
cursor: 'Cursor',
|
|
178
|
-
windsurf: 'Windsurf',
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
return names[ide] || ide;
|
|
182
|
-
}
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
getErrorCodeFromError,
|
|
18
18
|
attachErrorCode,
|
|
19
19
|
} = require('../../../../lib/error-codes');
|
|
20
|
+
const { setSecurePermissions, SECURE_FILE_MODE } = require('../../../../lib/smart-json-file');
|
|
20
21
|
|
|
21
22
|
const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.txt', '.json']);
|
|
22
23
|
|
|
@@ -504,6 +505,8 @@ class Installer {
|
|
|
504
505
|
};
|
|
505
506
|
|
|
506
507
|
await fs.writeFile(configPath, safeDump(config), 'utf8');
|
|
508
|
+
// Security: Set secure permissions (0o600) on config file
|
|
509
|
+
setSecurePermissions(configPath);
|
|
507
510
|
return;
|
|
508
511
|
}
|
|
509
512
|
|
|
@@ -531,6 +534,8 @@ class Installer {
|
|
|
531
534
|
};
|
|
532
535
|
|
|
533
536
|
await fs.writeFile(configPath, safeDump(next), 'utf8');
|
|
537
|
+
// Security: Set secure permissions (0o600) on config file
|
|
538
|
+
setSecurePermissions(configPath);
|
|
534
539
|
} catch (err) {
|
|
535
540
|
// If it's a typed parse error and not forcing, re-throw
|
|
536
541
|
if (err.errorCode === 'EPARSE' && !options.force) {
|
|
@@ -547,6 +552,8 @@ class Installer {
|
|
|
547
552
|
};
|
|
548
553
|
|
|
549
554
|
await fs.writeFile(configPath, safeDump(config), 'utf8');
|
|
555
|
+
// Security: Set secure permissions (0o600) on config file
|
|
556
|
+
setSecurePermissions(configPath);
|
|
550
557
|
}
|
|
551
558
|
}
|
|
552
559
|
}
|
|
@@ -578,6 +585,8 @@ class Installer {
|
|
|
578
585
|
};
|
|
579
586
|
|
|
580
587
|
await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
|
|
588
|
+
// Security: Set secure permissions (0o600) on manifest file
|
|
589
|
+
setSecurePermissions(manifestPath);
|
|
581
590
|
return;
|
|
582
591
|
}
|
|
583
592
|
|
|
@@ -608,6 +617,8 @@ class Installer {
|
|
|
608
617
|
};
|
|
609
618
|
|
|
610
619
|
await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
|
|
620
|
+
// Security: Set secure permissions (0o600) on manifest file
|
|
621
|
+
setSecurePermissions(manifestPath);
|
|
611
622
|
} catch (err) {
|
|
612
623
|
// If it's a typed parse error and not forcing, re-throw
|
|
613
624
|
if (err.errorCode === 'EPARSE' && !options.force) {
|
|
@@ -627,6 +638,8 @@ class Installer {
|
|
|
627
638
|
};
|
|
628
639
|
|
|
629
640
|
await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
|
|
641
|
+
// Security: Set secure permissions (0o600) on manifest file
|
|
642
|
+
setSecurePermissions(manifestPath);
|
|
630
643
|
}
|
|
631
644
|
}
|
|
632
645
|
}
|