fraim-framework 2.0.30 → 2.0.34
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/bin/fraim.js +3 -18
- package/dist/src/cli/commands/init.js +29 -2
- package/dist/src/cli/commands/sync.js +82 -70
- package/dist/src/utils/script-sync-utils.js +216 -0
- package/dist/tests/debug-tools.js +6 -5
- package/dist/tests/test-chalk-regression.js +58 -8
- package/dist/tests/test-cli.js +70 -5
- package/dist/tests/test-end-to-end-hybrid-validation.js +328 -0
- package/dist/tests/test-first-run-journey.js +43 -3
- package/dist/tests/test-hybrid-script-execution.js +340 -0
- package/dist/tests/test-mcp-connection.js +2 -2
- package/dist/tests/test-mcp-issue-integration.js +12 -4
- package/dist/tests/test-mcp-lifecycle-methods.js +4 -4
- package/dist/tests/test-node-compatibility.js +24 -2
- package/dist/tests/test-prep-issue.js +4 -1
- package/dist/tests/test-script-location-independence.js +173 -0
- package/dist/tests/test-script-sync.js +557 -0
- package/dist/tests/test-session-rehydration.js +2 -2
- package/dist/tests/test-standalone.js +3 -3
- package/dist/tests/test-sync-version-update.js +1 -1
- package/dist/tests/test-telemetry.js +2 -2
- package/dist/tests/test-user-journey.js +8 -4
- package/dist/tests/test-utils.js +13 -0
- package/dist/tests/test-wizard.js +2 -2
- package/package.json +3 -3
- package/registry/rules/agent-testing-guidelines.md +502 -502
- package/registry/rules/ephemeral-execution.md +37 -27
- package/registry/rules/local-development.md +253 -251
- package/registry/rules/successful-debugging-patterns.md +491 -482
- package/registry/scripts/prep-issue.sh +468 -468
- package/registry/workflows/bootstrap/evaluate-code-quality.md +8 -2
- package/registry/workflows/bootstrap/verify-test-coverage.md +8 -2
- package/registry/workflows/customer-development/thank-customers.md +203 -193
- package/registry/workflows/customer-development/weekly-newsletter.md +366 -362
- package/registry/workflows/performance/analyze-performance.md +65 -63
- package/registry/workflows/product-building/implement.md +6 -2
- package/registry/workflows/product-building/prep-issue.md +11 -24
- package/registry/workflows/product-building/resolve.md +5 -1
- package/registry/workflows/replicate/replicate-discovery.md +336 -0
- package/registry/workflows/replicate/replicate-to-issues.md +319 -0
- package/registry/workflows/reviewer/review-implementation-vs-design-spec.md +632 -632
- package/.windsurf/rules/windsurf-rules.md +0 -7
- package/.windsurf/workflows/resolve-issue.md +0 -6
- package/.windsurf/workflows/retrospect.md +0 -6
- package/.windsurf/workflows/start-design.md +0 -6
- package/.windsurf/workflows/start-impl.md +0 -6
- package/.windsurf/workflows/start-spec.md +0 -6
- package/.windsurf/workflows/start-tests.md +0 -6
- package/registry/scripts/build-scripts-generator.ts +0 -216
- package/registry/scripts/cleanup-branch.ts +0 -303
- package/registry/scripts/fraim-config.ts +0 -63
- package/registry/scripts/generate-engagement-emails.ts +0 -744
- package/registry/scripts/generic-issues-api.ts +0 -110
- package/registry/scripts/newsletter-helpers.ts +0 -874
- package/registry/scripts/openapi-generator.ts +0 -695
- package/registry/scripts/performance/profile-server.ts +0 -370
- package/registry/scripts/run-thank-you-workflow.ts +0 -122
- package/registry/scripts/send-newsletter-simple.ts +0 -104
- package/registry/scripts/send-thank-you-emails.ts +0 -57
- package/registry/workflows/replicate/re-implementation-strategy.md +0 -226
- package/registry/workflows/replicate/use-case-extraction.md +0 -135
- package/registry/workflows/replicate/visual-analysis.md +0 -154
- package/registry/workflows/replicate/website-discovery-analysis.md +0 -231
- package/sample_package.json +0 -18
- /package/registry/scripts/{replicate/comprehensive-explorer.py → comprehensive-explorer.py} +0 -0
- /package/registry/scripts/{replicate/interactive-explorer.py → interactive-explorer.py} +0 -0
- /package/registry/scripts/{replicate/scrape-site.py → scrape-site.py} +0 -0
package/bin/fraim.js
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* FRAIM
|
|
5
|
-
*
|
|
6
|
-
* This file delegates to the compiled TypeScript implementation.
|
|
4
|
+
* FRAIM CLI Entry Point
|
|
5
|
+
* This wrapper loads and executes the compiled CLI from dist/
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
// In production/installed package, code is in dist/
|
|
11
|
-
require('../dist/src/cli/fraim.js');
|
|
12
|
-
} catch (error) {
|
|
13
|
-
if (error.code === 'MODULE_NOT_FOUND') {
|
|
14
|
-
// In development (local clone), we might use tsx if dist is missing
|
|
15
|
-
// But typically we should just tell user to build.
|
|
16
|
-
console.error('❌ Could not find FRAIM CLI implementation.');
|
|
17
|
-
console.error(' If you are running from source, please run "npm run build" first.');
|
|
18
|
-
console.error(` Error details: ${error.message}`);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
} else {
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
8
|
+
require('../dist/src/cli/fraim.js');
|
|
@@ -13,6 +13,7 @@ const sync_1 = require("./sync");
|
|
|
13
13
|
const types_1 = require("../../fraim/types");
|
|
14
14
|
const git_utils_1 = require("../../utils/git-utils");
|
|
15
15
|
const version_utils_1 = require("../../utils/version-utils");
|
|
16
|
+
const script_sync_utils_1 = require("../../utils/script-sync-utils");
|
|
16
17
|
const runInit = async () => {
|
|
17
18
|
const projectRoot = process.cwd();
|
|
18
19
|
const fraimDir = path_1.default.join(projectRoot, '.fraim');
|
|
@@ -27,17 +28,23 @@ const runInit = async () => {
|
|
|
27
28
|
}
|
|
28
29
|
if (!fs_1.default.existsSync(configPath)) {
|
|
29
30
|
const remoteInfo = (0, git_utils_1.getGitRemoteInfo)();
|
|
31
|
+
if (!remoteInfo.repo) {
|
|
32
|
+
console.log(chalk_1.default.red('❌ Error: No git remote found. FRAIM requires a git repository with remote origin.'));
|
|
33
|
+
console.log(chalk_1.default.yellow('💡 Make sure you\'re in a git repository and have set up a remote origin:'));
|
|
34
|
+
console.log(chalk_1.default.gray(' git remote add origin <your-repo-url>'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
30
37
|
const config = {
|
|
31
38
|
...types_1.DEFAULT_FRAIM_CONFIG,
|
|
32
39
|
version: (0, version_utils_1.getFraimVersion)(),
|
|
33
40
|
project: {
|
|
34
41
|
...types_1.DEFAULT_FRAIM_CONFIG.project,
|
|
35
|
-
name:
|
|
42
|
+
name: remoteInfo.repo
|
|
36
43
|
},
|
|
37
44
|
git: {
|
|
38
45
|
...types_1.DEFAULT_FRAIM_CONFIG.git,
|
|
39
46
|
repoOwner: remoteInfo.owner || types_1.DEFAULT_FRAIM_CONFIG.git.repoOwner,
|
|
40
|
-
repoName: remoteInfo.repo
|
|
47
|
+
repoName: remoteInfo.repo
|
|
41
48
|
}
|
|
42
49
|
};
|
|
43
50
|
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -54,6 +61,26 @@ const runInit = async () => {
|
|
|
54
61
|
console.log(chalk_1.default.blue('\n🎉 FRAIM initialized successfully!'));
|
|
55
62
|
// Sync workflows from registry
|
|
56
63
|
await (0, sync_1.runSync)({});
|
|
64
|
+
// Sync scripts to user directory
|
|
65
|
+
console.log(chalk_1.default.blue('🔄 Syncing FRAIM scripts to user directory...'));
|
|
66
|
+
// Find registry path (same logic as sync command)
|
|
67
|
+
let registryPath = path_1.default.join(__dirname, '../../../../registry');
|
|
68
|
+
if (!fs_1.default.existsSync(registryPath)) {
|
|
69
|
+
registryPath = path_1.default.join(__dirname, '../../../registry');
|
|
70
|
+
}
|
|
71
|
+
if (!fs_1.default.existsSync(registryPath)) {
|
|
72
|
+
registryPath = path_1.default.join(projectRoot, 'registry');
|
|
73
|
+
}
|
|
74
|
+
if (fs_1.default.existsSync(registryPath)) {
|
|
75
|
+
const syncResult = (0, script_sync_utils_1.syncScriptsToUserDirectory)(registryPath);
|
|
76
|
+
console.log(chalk_1.default.green(`✅ Synced ${syncResult.synced} self-contained scripts to user directory.`));
|
|
77
|
+
if (syncResult.ephemeral > 0) {
|
|
78
|
+
console.log(chalk_1.default.gray(` ${syncResult.ephemeral} dependent scripts will use ephemeral execution.`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(chalk_1.default.yellow('⚠️ Registry not found, skipping script sync.'));
|
|
83
|
+
}
|
|
57
84
|
// Trigger First Run Experience
|
|
58
85
|
await (0, first_run_1.runFirstRunExperience)();
|
|
59
86
|
process.exit(0);
|
|
@@ -12,6 +12,7 @@ const digest_utils_1 = require("../../utils/digest-utils");
|
|
|
12
12
|
const stub_generator_1 = require("../../utils/stub-generator");
|
|
13
13
|
const config_loader_1 = require("../../fraim/config-loader");
|
|
14
14
|
const version_utils_1 = require("../../utils/version-utils");
|
|
15
|
+
const script_sync_utils_1 = require("../../utils/script-sync-utils");
|
|
15
16
|
const runSync = async (options) => {
|
|
16
17
|
const projectRoot = process.cwd();
|
|
17
18
|
const config = (0, config_loader_1.loadFraimConfig)();
|
|
@@ -59,82 +60,93 @@ const runSync = async (options) => {
|
|
|
59
60
|
const existingDigest = fs_1.default.existsSync(digestPath) ? fs_1.default.readFileSync(digestPath, 'utf8') : '';
|
|
60
61
|
if (currentDigest === existingDigest && !options.force) {
|
|
61
62
|
console.log(chalk_1.default.green('✅ Workflows are already in sync.'));
|
|
62
|
-
return;
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
// Get all workflows from registry (recursive)
|
|
70
|
-
const getFiles = (dir) => {
|
|
71
|
-
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
72
|
-
const files = entries
|
|
73
|
-
.filter(e => !e.isDirectory() && e.name.endsWith('.md'))
|
|
74
|
-
.map(e => path_1.default.join(dir, e.name));
|
|
75
|
-
const folders = entries.filter(e => e.isDirectory());
|
|
76
|
-
for (const folder of folders) {
|
|
77
|
-
files.push(...getFiles(path_1.default.join(dir, folder.name)));
|
|
78
|
-
}
|
|
79
|
-
return files;
|
|
80
|
-
};
|
|
81
|
-
const registryFiles = getFiles(registryWorkflowsPath);
|
|
82
|
-
const generatedStubs = [];
|
|
83
|
-
for (const file of registryFiles) {
|
|
84
|
-
const content = fs_1.default.readFileSync(file, 'utf8');
|
|
85
|
-
const { intent, principles } = (0, stub_generator_1.parseRegistryWorkflow)(content);
|
|
86
|
-
const fileName = path_1.default.basename(file);
|
|
87
|
-
const workflowName = fileName.replace('.md', '');
|
|
88
|
-
// Calculate relative path from registry/workflows to preserve structure
|
|
89
|
-
const relativePath = path_1.default.relative(registryWorkflowsPath, file);
|
|
90
|
-
const relativeDir = path_1.default.dirname(relativePath);
|
|
91
|
-
// Ensure target directory exists
|
|
92
|
-
const targetDir = path_1.default.join(workflowsDir, relativeDir);
|
|
93
|
-
if (!fs_1.default.existsSync(targetDir)) {
|
|
94
|
-
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
64
|
+
else {
|
|
65
|
+
const registryWorkflowsPath = path_1.default.join(registryPath, 'workflows');
|
|
66
|
+
if (!fs_1.default.existsSync(registryWorkflowsPath)) {
|
|
67
|
+
console.log(chalk_1.default.yellow('⚠️ No workflows found in registry.'));
|
|
95
68
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!normalizedGenerated.includes(normalizedStub)) {
|
|
124
|
-
fs_1.default.unlinkSync(path_1.default.join(workflowsDir, stub));
|
|
125
|
-
console.log(chalk_1.default.yellow(` - ${stub} (removed from registry)`));
|
|
126
|
-
// Cleanup empty directories
|
|
127
|
-
try {
|
|
128
|
-
const dir = path_1.default.dirname(path_1.default.join(workflowsDir, stub));
|
|
129
|
-
if (fs_1.default.readdirSync(dir).length === 0) {
|
|
130
|
-
fs_1.default.rmdirSync(dir);
|
|
69
|
+
else {
|
|
70
|
+
// Get all workflows from registry (recursive)
|
|
71
|
+
const getFiles = (dir) => {
|
|
72
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
73
|
+
const files = entries
|
|
74
|
+
.filter(e => !e.isDirectory() && e.name.endsWith('.md'))
|
|
75
|
+
.map(e => path_1.default.join(dir, e.name));
|
|
76
|
+
const folders = entries.filter(e => e.isDirectory());
|
|
77
|
+
for (const folder of folders) {
|
|
78
|
+
files.push(...getFiles(path_1.default.join(dir, folder.name)));
|
|
79
|
+
}
|
|
80
|
+
return files;
|
|
81
|
+
};
|
|
82
|
+
const registryFiles = getFiles(registryWorkflowsPath);
|
|
83
|
+
const generatedStubs = [];
|
|
84
|
+
for (const file of registryFiles) {
|
|
85
|
+
const content = fs_1.default.readFileSync(file, 'utf8');
|
|
86
|
+
const { intent, principles } = (0, stub_generator_1.parseRegistryWorkflow)(content);
|
|
87
|
+
const fileName = path_1.default.basename(file);
|
|
88
|
+
const workflowName = fileName.replace('.md', '');
|
|
89
|
+
// Calculate relative path from registry/workflows to preserve structure
|
|
90
|
+
const relativePath = path_1.default.relative(registryWorkflowsPath, file);
|
|
91
|
+
const relativeDir = path_1.default.dirname(relativePath);
|
|
92
|
+
// Ensure target directory exists
|
|
93
|
+
const targetDir = path_1.default.join(workflowsDir, relativeDir);
|
|
94
|
+
if (!fs_1.default.existsSync(targetDir)) {
|
|
95
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
131
96
|
}
|
|
97
|
+
const stubContent = (0, stub_generator_1.generateWorkflowStub)(workflowName, file, intent, principles);
|
|
98
|
+
const stubPath = path_1.default.join(targetDir, fileName);
|
|
99
|
+
fs_1.default.writeFileSync(stubPath, stubContent);
|
|
100
|
+
generatedStubs.push(relativePath); // Store relative path for cleanup tracking
|
|
101
|
+
console.log(chalk_1.default.gray(` + ${workflowName} (${relativeDir === '.' ? 'root' : relativeDir})`));
|
|
132
102
|
}
|
|
133
|
-
|
|
103
|
+
// Cleanup stubs that no longer exist in registry
|
|
104
|
+
// Helper to get all local stubs recursively
|
|
105
|
+
const getLocalStubs = (dir) => {
|
|
106
|
+
if (!fs_1.default.existsSync(dir))
|
|
107
|
+
return [];
|
|
108
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
109
|
+
const files = entries
|
|
110
|
+
.filter(e => !e.isDirectory() && e.name.endsWith('.md'))
|
|
111
|
+
.map(e => path_1.default.relative(workflowsDir, path_1.default.join(dir, e.name)));
|
|
112
|
+
const folders = entries.filter(e => e.isDirectory());
|
|
113
|
+
for (const folder of folders) {
|
|
114
|
+
files.push(...getLocalStubs(path_1.default.join(dir, folder.name)));
|
|
115
|
+
}
|
|
116
|
+
return files;
|
|
117
|
+
};
|
|
118
|
+
const localStubs = getLocalStubs(workflowsDir);
|
|
119
|
+
for (const stub of localStubs) {
|
|
120
|
+
// standardise path separators for comparison
|
|
121
|
+
const normalizedStub = stub.replace(/\\/g, '/');
|
|
122
|
+
const normalizedGenerated = generatedStubs.map(s => s.replace(/\\/g, '/'));
|
|
123
|
+
if (!normalizedGenerated.includes(normalizedStub)) {
|
|
124
|
+
fs_1.default.unlinkSync(path_1.default.join(workflowsDir, stub));
|
|
125
|
+
console.log(chalk_1.default.yellow(` - ${stub} (removed from registry)`));
|
|
126
|
+
// Cleanup empty directories
|
|
127
|
+
try {
|
|
128
|
+
const dir = path_1.default.dirname(path_1.default.join(workflowsDir, stub));
|
|
129
|
+
if (fs_1.default.readdirSync(dir).length === 0) {
|
|
130
|
+
fs_1.default.rmdirSync(dir);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (e) { }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
fs_1.default.writeFileSync(digestPath, currentDigest);
|
|
137
|
+
console.log(chalk_1.default.green(`\n✅ Workflow sync complete. Generated ${generatedStubs.length} stubs.`));
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
|
-
|
|
137
|
-
console.log(chalk_1.default.
|
|
140
|
+
// Always sync scripts, regardless of workflow sync status
|
|
141
|
+
console.log(chalk_1.default.blue('\n🔄 Syncing FRAIM scripts to user directory...'));
|
|
142
|
+
const syncResult = (0, script_sync_utils_1.syncScriptsToUserDirectory)(registryPath);
|
|
143
|
+
const cleanedScriptCount = (0, script_sync_utils_1.cleanupObsoleteUserScripts)(registryPath);
|
|
144
|
+
if (syncResult.synced > 0 || cleanedScriptCount > 0) {
|
|
145
|
+
console.log(chalk_1.default.green(`✅ Script sync complete. Updated ${syncResult.synced} scripts, removed ${cleanedScriptCount} obsolete scripts.`));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log(chalk_1.default.green('✅ Scripts are already in sync.'));
|
|
149
|
+
}
|
|
138
150
|
};
|
|
139
151
|
exports.runSync = runSync;
|
|
140
152
|
exports.syncCommand = new commander_1.Command('sync')
|
|
@@ -0,0 +1,216 @@
|
|
|
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.getUserFraimDir = getUserFraimDir;
|
|
7
|
+
exports.getUserScriptsDir = getUserScriptsDir;
|
|
8
|
+
exports.ensureUserFraimDirectories = ensureUserFraimDirectories;
|
|
9
|
+
exports.getRegistryScripts = getRegistryScripts;
|
|
10
|
+
exports.copyScriptToUserDirectory = copyScriptToUserDirectory;
|
|
11
|
+
exports.syncScriptsToUserDirectory = syncScriptsToUserDirectory;
|
|
12
|
+
exports.cleanupObsoleteUserScripts = cleanupObsoleteUserScripts;
|
|
13
|
+
exports.getUserScriptPath = getUserScriptPath;
|
|
14
|
+
exports.userScriptExists = userScriptExists;
|
|
15
|
+
exports.validateUserScripts = validateUserScripts;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const os_1 = __importDefault(require("os"));
|
|
19
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
20
|
+
/**
|
|
21
|
+
* Get the user-level FRAIM directory path
|
|
22
|
+
* Can be overridden with FRAIM_USER_DIR environment variable for testing
|
|
23
|
+
*/
|
|
24
|
+
function getUserFraimDir() {
|
|
25
|
+
return process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get the user-level scripts directory path
|
|
29
|
+
*/
|
|
30
|
+
function getUserScriptsDir() {
|
|
31
|
+
return path_1.default.join(getUserFraimDir(), 'scripts');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ensure the user-level FRAIM directories exist
|
|
35
|
+
*/
|
|
36
|
+
function ensureUserFraimDirectories() {
|
|
37
|
+
const userFraimDir = getUserFraimDir();
|
|
38
|
+
const userScriptsDir = getUserScriptsDir();
|
|
39
|
+
if (!fs_1.default.existsSync(userFraimDir)) {
|
|
40
|
+
fs_1.default.mkdirSync(userFraimDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
if (!fs_1.default.existsSync(userScriptsDir)) {
|
|
43
|
+
fs_1.default.mkdirSync(userScriptsDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get all script files from the registry (recursively)
|
|
48
|
+
*/
|
|
49
|
+
function getRegistryScripts(registryPath) {
|
|
50
|
+
const scriptsPath = path_1.default.join(registryPath, 'scripts');
|
|
51
|
+
if (!fs_1.default.existsSync(scriptsPath)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const getAllFiles = (dir, baseDir = dir) => {
|
|
55
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
let files = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
// Recursively get files from subdirectories
|
|
61
|
+
files = files.concat(getAllFiles(fullPath, baseDir));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Include all files (not just specific extensions)
|
|
65
|
+
files.push(fullPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
};
|
|
70
|
+
return getAllFiles(scriptsPath);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Copy a script file to the user scripts directory with proper permissions
|
|
74
|
+
* Preserves directory structure from registry/scripts/
|
|
75
|
+
*/
|
|
76
|
+
function copyScriptToUserDirectory(sourcePath, registryScriptsPath) {
|
|
77
|
+
const userScriptsDir = getUserScriptsDir();
|
|
78
|
+
// Calculate relative path from registry/scripts to preserve structure
|
|
79
|
+
const relativePath = path_1.default.relative(registryScriptsPath, sourcePath);
|
|
80
|
+
const targetPath = path_1.default.join(userScriptsDir, relativePath);
|
|
81
|
+
// Ensure target directory exists
|
|
82
|
+
const targetDir = path_1.default.dirname(targetPath);
|
|
83
|
+
if (!fs_1.default.existsSync(targetDir)) {
|
|
84
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
// Copy the file
|
|
87
|
+
fs_1.default.copyFileSync(sourcePath, targetPath);
|
|
88
|
+
// Set executable permissions on Unix systems for shell scripts
|
|
89
|
+
if (process.platform !== 'win32' && sourcePath.endsWith('.sh')) {
|
|
90
|
+
try {
|
|
91
|
+
fs_1.default.chmodSync(targetPath, 0o755);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.warn(chalk_1.default.yellow(`⚠️ Could not set executable permissions for ${relativePath}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sync all scripts from registry to user directory
|
|
100
|
+
*/
|
|
101
|
+
function syncScriptsToUserDirectory(registryPath) {
|
|
102
|
+
ensureUserFraimDirectories();
|
|
103
|
+
const registryScriptsPath = path_1.default.join(registryPath, 'scripts');
|
|
104
|
+
const allScripts = getRegistryScripts(registryPath);
|
|
105
|
+
let syncedCount = 0;
|
|
106
|
+
console.log(chalk_1.default.gray(` Syncing all scripts to ~/.fraim/scripts/:`));
|
|
107
|
+
for (const scriptPath of allScripts) {
|
|
108
|
+
const relativePath = path_1.default.relative(registryScriptsPath, scriptPath);
|
|
109
|
+
const userScriptPath = path_1.default.join(getUserScriptsDir(), relativePath);
|
|
110
|
+
try {
|
|
111
|
+
// Check if script needs updating
|
|
112
|
+
let needsUpdate = true;
|
|
113
|
+
if (fs_1.default.existsSync(userScriptPath)) {
|
|
114
|
+
const registryContent = fs_1.default.readFileSync(scriptPath, 'utf-8');
|
|
115
|
+
const userContent = fs_1.default.readFileSync(userScriptPath, 'utf-8');
|
|
116
|
+
needsUpdate = registryContent !== userContent;
|
|
117
|
+
}
|
|
118
|
+
if (needsUpdate) {
|
|
119
|
+
copyScriptToUserDirectory(scriptPath, registryScriptsPath);
|
|
120
|
+
syncedCount++;
|
|
121
|
+
console.log(chalk_1.default.gray(` + ${relativePath}`));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(chalk_1.default.gray(` = ${relativePath} (up to date)`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.warn(chalk_1.default.yellow(`⚠️ Could not sync script ${relativePath}: ${error}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { synced: syncedCount, ephemeral: 0 };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clean up scripts in user directory that no longer exist in registry
|
|
135
|
+
*/
|
|
136
|
+
function cleanupObsoleteUserScripts(registryPath) {
|
|
137
|
+
const userScriptsDir = getUserScriptsDir();
|
|
138
|
+
if (!fs_1.default.existsSync(userScriptsDir)) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
const registryScriptsPath = path_1.default.join(registryPath, 'scripts');
|
|
142
|
+
const registryScripts = getRegistryScripts(registryPath);
|
|
143
|
+
const registryRelativePaths = registryScripts.map(scriptPath => path_1.default.relative(registryScriptsPath, scriptPath));
|
|
144
|
+
// Get all files in user scripts directory recursively
|
|
145
|
+
const getAllUserFiles = (dir, baseDir = dir) => {
|
|
146
|
+
if (!fs_1.default.existsSync(dir))
|
|
147
|
+
return [];
|
|
148
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
149
|
+
let files = [];
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
files = files.concat(getAllUserFiles(fullPath, baseDir));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
files.push(path_1.default.relative(baseDir, fullPath));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return files;
|
|
160
|
+
};
|
|
161
|
+
const userFiles = getAllUserFiles(userScriptsDir);
|
|
162
|
+
let cleanedCount = 0;
|
|
163
|
+
for (const userFile of userFiles) {
|
|
164
|
+
// Normalize path separators for comparison
|
|
165
|
+
const normalizedUserFile = userFile.replace(/\\/g, '/');
|
|
166
|
+
const normalizedRegistry = registryRelativePaths.map(p => p.replace(/\\/g, '/'));
|
|
167
|
+
if (!normalizedRegistry.includes(normalizedUserFile)) {
|
|
168
|
+
try {
|
|
169
|
+
const userFilePath = path_1.default.join(userScriptsDir, userFile);
|
|
170
|
+
fs_1.default.unlinkSync(userFilePath);
|
|
171
|
+
cleanedCount++;
|
|
172
|
+
console.log(chalk_1.default.yellow(` - ${userFile} (removed from registry)`));
|
|
173
|
+
// Cleanup empty directories
|
|
174
|
+
try {
|
|
175
|
+
const dir = path_1.default.dirname(userFilePath);
|
|
176
|
+
if (fs_1.default.existsSync(dir) && fs_1.default.readdirSync(dir).length === 0) {
|
|
177
|
+
fs_1.default.rmdirSync(dir);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (e) { }
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.warn(chalk_1.default.yellow(`⚠️ Could not remove obsolete script ${userFile}: ${error}`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return cleanedCount;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get the path to a script in the user directory
|
|
191
|
+
*/
|
|
192
|
+
function getUserScriptPath(scriptName) {
|
|
193
|
+
return path_1.default.join(getUserScriptsDir(), scriptName);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if a script exists in the user directory
|
|
197
|
+
*/
|
|
198
|
+
function userScriptExists(scriptName) {
|
|
199
|
+
return fs_1.default.existsSync(getUserScriptPath(scriptName));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Validate that all expected scripts are present in user directory
|
|
203
|
+
*/
|
|
204
|
+
function validateUserScripts(expectedScripts) {
|
|
205
|
+
const missing = [];
|
|
206
|
+
const present = [];
|
|
207
|
+
for (const scriptName of expectedScripts) {
|
|
208
|
+
if (userScriptExists(scriptName)) {
|
|
209
|
+
present.push(scriptName);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
missing.push(scriptName);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { missing, present };
|
|
216
|
+
}
|
|
@@ -13,7 +13,7 @@ async function debugListTools() {
|
|
|
13
13
|
console.log(' 🔍 Debugging Available Tools...');
|
|
14
14
|
let fraimProcess;
|
|
15
15
|
let dbService;
|
|
16
|
-
const PORT =
|
|
16
|
+
const PORT = Math.floor(Math.random() * 1000) + 10000; // Random port to avoid conflicts
|
|
17
17
|
const TEST_API_KEY = 'debug-tools-key';
|
|
18
18
|
const BASE_URL = `http://localhost:${PORT}`;
|
|
19
19
|
try {
|
|
@@ -24,8 +24,8 @@ async function debugListTools() {
|
|
|
24
24
|
await db.collection('fraim_api_keys').updateOne({ key: TEST_API_KEY }, { $set: { userId: 'debug@test.com', orgId: 'debug-org', isActive: true, createdAt: new Date() } }, { upsert: true });
|
|
25
25
|
// 2. Start server
|
|
26
26
|
const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
27
|
-
const serverScript = path_1.default.resolve(__dirname, '../src/fraim-mcp-server.
|
|
28
|
-
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['
|
|
27
|
+
const serverScript = path_1.default.resolve(__dirname, '../dist/src/fraim-mcp-server.js');
|
|
28
|
+
fraimProcess = (0, node_child_process_1.spawn)(npxCommand, ['node', `"${serverScript}"`], {
|
|
29
29
|
env: { ...process.env, FRAIM_MCP_PORT: PORT.toString(), FRAIM_SKIP_INDEX_ON_START: 'true' },
|
|
30
30
|
shell: true
|
|
31
31
|
});
|
|
@@ -65,8 +65,9 @@ async function debugListTools() {
|
|
|
65
65
|
finally {
|
|
66
66
|
if (dbService)
|
|
67
67
|
await dbService.close().catch(() => { });
|
|
68
|
-
if (fraimProcess && fraimProcess.pid)
|
|
69
|
-
(0, tree_kill_1.default)(fraimProcess.pid);
|
|
68
|
+
if (fraimProcess && fraimProcess.pid) {
|
|
69
|
+
await new Promise((resolve) => (0, tree_kill_1.default)(fraimProcess.pid, 'SIGKILL', () => resolve()));
|
|
70
|
+
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
async function runTest(testCase) {
|
|
@@ -41,10 +41,21 @@ async function testChalkConflictScenario() {
|
|
|
41
41
|
console.log(' This ensures pinned chalk v4 works even with chalk v5 present\n');
|
|
42
42
|
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'chalk-regression-'));
|
|
43
43
|
console.log(` 📂 Temp dir: ${tempDir}`);
|
|
44
|
+
let tarballPath = null;
|
|
44
45
|
try {
|
|
45
46
|
// 1. Pack fraim-framework
|
|
46
47
|
console.log(' 📦 Packing fraim-framework...');
|
|
47
48
|
const projectRoot = process.cwd();
|
|
49
|
+
// Clean up any existing tarballs first
|
|
50
|
+
const existingTarballs = fs_1.default.readdirSync(projectRoot).filter(f => f.startsWith('fraim-framework-') && f.endsWith('.tgz'));
|
|
51
|
+
for (const tarball of existingTarballs) {
|
|
52
|
+
try {
|
|
53
|
+
fs_1.default.unlinkSync(path_1.default.join(projectRoot, tarball));
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
// Ignore cleanup errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
48
59
|
const packResult = (0, node_child_process_1.execSync)('npm pack', {
|
|
49
60
|
cwd: projectRoot,
|
|
50
61
|
encoding: 'utf-8'
|
|
@@ -54,7 +65,7 @@ async function testChalkConflictScenario() {
|
|
|
54
65
|
console.log(' ❌ Failed to create tarball');
|
|
55
66
|
return false;
|
|
56
67
|
}
|
|
57
|
-
|
|
68
|
+
tarballPath = path_1.default.join(projectRoot, tarballName);
|
|
58
69
|
console.log(` ✅ Created: ${tarballName}`);
|
|
59
70
|
// 2. Create test project with chalk v5
|
|
60
71
|
console.log(' 📝 Creating test project with chalk v5...');
|
|
@@ -161,7 +172,9 @@ try {
|
|
|
161
172
|
console.log(' ⚠️ This might cause issues in some environments');
|
|
162
173
|
console.log(' ⚠️ But CLI loaded successfully, so test passes');
|
|
163
174
|
}
|
|
164
|
-
fs_1.default.
|
|
175
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
176
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
177
|
+
}
|
|
165
178
|
return true;
|
|
166
179
|
}
|
|
167
180
|
else if (hasERR_REQUIRE_ESM) {
|
|
@@ -173,19 +186,31 @@ try {
|
|
|
173
186
|
else {
|
|
174
187
|
console.log(' ❌ fraim-framework has chalk v4 but still got error (unexpected)');
|
|
175
188
|
}
|
|
176
|
-
fs_1.default.
|
|
189
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
190
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
191
|
+
}
|
|
177
192
|
return false;
|
|
178
193
|
}
|
|
179
194
|
else {
|
|
180
195
|
console.log(' ❌ Got unexpected error (exit code: ' + testResult.code + ')');
|
|
181
196
|
console.log(' ❌ Check the error output above');
|
|
182
|
-
fs_1.default.
|
|
197
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
198
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
199
|
+
}
|
|
183
200
|
return false;
|
|
184
201
|
}
|
|
185
202
|
}
|
|
186
203
|
catch (error) {
|
|
187
204
|
console.error(' ❌ Conflict scenario test failed with exception:', error);
|
|
188
205
|
console.error(' ❌ This is likely a test infrastructure issue, not a chalk issue');
|
|
206
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
207
|
+
try {
|
|
208
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
// Ignore cleanup errors
|
|
212
|
+
}
|
|
213
|
+
}
|
|
189
214
|
return false;
|
|
190
215
|
}
|
|
191
216
|
finally {
|
|
@@ -203,10 +228,21 @@ async function testFreshInstall() {
|
|
|
203
228
|
console.log(' This simulates: npx fraim-framework init\n');
|
|
204
229
|
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'chalk-fresh-'));
|
|
205
230
|
console.log(` 📂 Temp dir: ${tempDir}`);
|
|
231
|
+
let tarballPath = null;
|
|
206
232
|
try {
|
|
207
233
|
// 1. Pack fraim-framework
|
|
208
234
|
console.log(' 📦 Packing fraim-framework...');
|
|
209
235
|
const projectRoot = process.cwd();
|
|
236
|
+
// Clean up any existing tarballs first
|
|
237
|
+
const existingTarballs = fs_1.default.readdirSync(projectRoot).filter(f => f.startsWith('fraim-framework-') && f.endsWith('.tgz'));
|
|
238
|
+
for (const tarball of existingTarballs) {
|
|
239
|
+
try {
|
|
240
|
+
fs_1.default.unlinkSync(path_1.default.join(projectRoot, tarball));
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
// Ignore cleanup errors
|
|
244
|
+
}
|
|
245
|
+
}
|
|
210
246
|
const packResult = (0, node_child_process_1.execSync)('npm pack', {
|
|
211
247
|
cwd: projectRoot,
|
|
212
248
|
encoding: 'utf-8'
|
|
@@ -216,7 +252,7 @@ async function testFreshInstall() {
|
|
|
216
252
|
console.log(' ❌ Failed to create tarball');
|
|
217
253
|
return false;
|
|
218
254
|
}
|
|
219
|
-
|
|
255
|
+
tarballPath = path_1.default.join(projectRoot, tarballName);
|
|
220
256
|
console.log(` ✅ Created: ${tarballName}`);
|
|
221
257
|
// 2. Create empty project
|
|
222
258
|
console.log(' 📝 Creating empty project...');
|
|
@@ -277,24 +313,38 @@ try {
|
|
|
277
313
|
const cliLoaded = testResult.stdout.includes('✓ FRAIM CLI loaded successfully') || testResult.stderr.includes('Usage: fraim');
|
|
278
314
|
if (cliLoaded && !hasERR_REQUIRE_ESM) {
|
|
279
315
|
console.log(' ✅ SUCCESS: Fresh install works correctly');
|
|
280
|
-
fs_1.default.
|
|
316
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
317
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
318
|
+
}
|
|
281
319
|
return true;
|
|
282
320
|
}
|
|
283
321
|
else if (hasERR_REQUIRE_ESM) {
|
|
284
322
|
console.log(' ❌ REGRESSION: Got ERR_REQUIRE_ESM error!');
|
|
285
|
-
fs_1.default.
|
|
323
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
324
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
325
|
+
}
|
|
286
326
|
return false;
|
|
287
327
|
}
|
|
288
328
|
else {
|
|
289
329
|
console.log(' ❌ Got unexpected error (exit code: ' + testResult.code + ')');
|
|
290
330
|
console.log(' ❌ Check the error output above');
|
|
291
|
-
fs_1.default.
|
|
331
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
332
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
333
|
+
}
|
|
292
334
|
return false;
|
|
293
335
|
}
|
|
294
336
|
}
|
|
295
337
|
catch (error) {
|
|
296
338
|
console.error(' ❌ Fresh install test failed with exception:', error);
|
|
297
339
|
console.error(' ❌ This is likely a test infrastructure issue, not a chalk issue');
|
|
340
|
+
if (tarballPath && fs_1.default.existsSync(tarballPath)) {
|
|
341
|
+
try {
|
|
342
|
+
fs_1.default.unlinkSync(tarballPath);
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
// Ignore cleanup errors
|
|
346
|
+
}
|
|
347
|
+
}
|
|
298
348
|
return false;
|
|
299
349
|
}
|
|
300
350
|
finally {
|