fraim-framework 2.0.24 → 2.0.26
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/.github/workflows/deploy-fraim.yml +3 -1
- package/dist/src/fraim/config-loader.js +19 -11
- package/dist/src/fraim/setup-wizard.js +28 -1
- package/dist/src/fraim/types.js +11 -0
- package/dist/src/fraim-mcp-server.js +22 -11
- package/dist/src/utils/git-utils.js +1 -1
- package/dist/tests/test-cli.js +169 -0
- package/dist/tests/test-first-run-journey.js +108 -0
- package/dist/tests/test-genericization.js +66 -0
- package/{test-prep-issue.ts → dist/tests/test-prep-issue.js} +93 -101
- package/{test-standalone.ts → dist/tests/test-standalone.js} +149 -161
- package/dist/tests/test-user-journey.js +231 -0
- package/dist/tests/test-utils.js +96 -0
- package/{test-wizard.ts → dist/tests/test-wizard.js} +71 -81
- package/package.json +9 -5
- package/registry/rules/architecture.md +1 -1
- package/registry/scripts/code-quality-check.sh +5 -4
- package/registry/scripts/evaluate-code-quality.ts +36 -0
- package/registry/scripts/{validate-coverage.ts → validate-test-coverage.ts} +39 -39
- package/registry/scripts/verify-test-coverage.ts +36 -0
- package/registry/templates/bootstrap/ARCHITECTURE-TEMPLATE.md +53 -0
- package/registry/templates/bootstrap/CODE-QUALITY-REPORT-TEMPLATE.md +37 -0
- package/registry/templates/bootstrap/TEST-COVERAGE-REPORT-TEMPLATE.md +35 -0
- package/registry/templates/business-development/PRICING-STRATEGY-TEMPLATE.md +126 -0
- package/registry/workflows/bootstrap/create-architecture.md +13 -12
- package/registry/workflows/bootstrap/evaluate-code-quality.md +30 -0
- package/registry/workflows/bootstrap/verify-test-coverage.md +31 -0
- package/registry/workflows/business-development/price-product.md +325 -0
- package/tsconfig.json +4 -4
- package/test-cli.ts +0 -155
- package/test-first-run-journey.ts +0 -122
- package/test-genericization.ts +0 -74
- package/test-user-journey.ts +0 -244
- package/test-utils.ts +0 -120
|
@@ -7,6 +7,7 @@ on:
|
|
|
7
7
|
- master
|
|
8
8
|
paths:
|
|
9
9
|
- 'src/fraim-mcp-server.ts'
|
|
10
|
+
- 'src/fraim/**'
|
|
10
11
|
- '.ai-agents/**'
|
|
11
12
|
- '.fraim/**'
|
|
12
13
|
- '.github/workflows/deploy-fraim.yml'
|
|
@@ -55,7 +56,8 @@ jobs:
|
|
|
55
56
|
# Copy necessary files
|
|
56
57
|
cp -r dist deploy-package/
|
|
57
58
|
cp -r node_modules deploy-package/
|
|
58
|
-
# Include .fraim directory for runtime
|
|
59
|
+
# Include registry and .fraim directory for runtime
|
|
60
|
+
cp -r registry deploy-package/
|
|
59
61
|
cp -r .fraim deploy-package/
|
|
60
62
|
|
|
61
63
|
cp package.json deploy-package/
|
|
@@ -8,7 +8,7 @@ exports.loadFraimConfig = loadFraimConfig;
|
|
|
8
8
|
exports.getConfigValue = getConfigValue;
|
|
9
9
|
const fs_1 = require("fs");
|
|
10
10
|
const path_1 = require("path");
|
|
11
|
-
const
|
|
11
|
+
const types_1 = require("./types");
|
|
12
12
|
/**
|
|
13
13
|
* Load FRAIM configuration from .fraim/config.json
|
|
14
14
|
* Falls back to defaults if file doesn't exist
|
|
@@ -17,43 +17,51 @@ function loadFraimConfig() {
|
|
|
17
17
|
const configPath = (0, path_1.join)(process.cwd(), '.fraim', 'config.json');
|
|
18
18
|
if (!(0, fs_1.existsSync)(configPath)) {
|
|
19
19
|
console.log('📋 No .fraim/config.json found, using defaults');
|
|
20
|
-
return { ...
|
|
20
|
+
return { ...types_1.DEFAULT_FRAIM_CONFIG };
|
|
21
21
|
}
|
|
22
22
|
try {
|
|
23
23
|
const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
24
24
|
const config = JSON.parse(configContent);
|
|
25
25
|
// Merge with defaults to ensure all required fields exist
|
|
26
26
|
const mergedConfig = {
|
|
27
|
-
...
|
|
27
|
+
...types_1.DEFAULT_FRAIM_CONFIG,
|
|
28
28
|
...config,
|
|
29
29
|
project: {
|
|
30
|
-
...
|
|
30
|
+
...types_1.DEFAULT_FRAIM_CONFIG.project,
|
|
31
31
|
...(config.project || {})
|
|
32
32
|
},
|
|
33
33
|
git: {
|
|
34
|
-
...
|
|
34
|
+
...types_1.DEFAULT_FRAIM_CONFIG.git,
|
|
35
35
|
...(config.git || {})
|
|
36
36
|
},
|
|
37
37
|
customizations: {
|
|
38
|
-
...
|
|
38
|
+
...types_1.DEFAULT_FRAIM_CONFIG.customizations,
|
|
39
39
|
...(config.customizations || {})
|
|
40
40
|
},
|
|
41
|
+
architecture: {
|
|
42
|
+
...types_1.DEFAULT_FRAIM_CONFIG.architecture,
|
|
43
|
+
...(config.architecture || {})
|
|
44
|
+
},
|
|
45
|
+
testing: {
|
|
46
|
+
...types_1.DEFAULT_FRAIM_CONFIG.testing,
|
|
47
|
+
...(config.testing || {})
|
|
48
|
+
},
|
|
41
49
|
persona: {
|
|
42
|
-
...
|
|
50
|
+
...types_1.DEFAULT_FRAIM_CONFIG.persona,
|
|
43
51
|
...(config.persona || {})
|
|
44
52
|
},
|
|
45
53
|
marketing: {
|
|
46
|
-
...
|
|
54
|
+
...types_1.DEFAULT_FRAIM_CONFIG.marketing,
|
|
47
55
|
...(config.marketing || {})
|
|
48
56
|
},
|
|
49
57
|
database: {
|
|
50
|
-
...
|
|
58
|
+
...types_1.DEFAULT_FRAIM_CONFIG.database,
|
|
51
59
|
...(config.database || {})
|
|
52
60
|
}
|
|
53
61
|
};
|
|
54
62
|
// Validate version
|
|
55
63
|
if (!mergedConfig.version) {
|
|
56
|
-
mergedConfig.version =
|
|
64
|
+
mergedConfig.version = types_1.DEFAULT_FRAIM_CONFIG.version;
|
|
57
65
|
}
|
|
58
66
|
console.log(`📋 Loaded FRAIM config from .fraim/config.json (version ${mergedConfig.version})`);
|
|
59
67
|
return mergedConfig;
|
|
@@ -61,7 +69,7 @@ function loadFraimConfig() {
|
|
|
61
69
|
catch (error) {
|
|
62
70
|
console.warn(`⚠️ Failed to load .fraim/config.json: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
63
71
|
console.warn(' Using default configuration');
|
|
64
|
-
return { ...
|
|
72
|
+
return { ...types_1.DEFAULT_FRAIM_CONFIG };
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
/**
|
|
@@ -69,6 +69,7 @@ function createDirectoryStructure() {
|
|
|
69
69
|
* Generate architecture.md template
|
|
70
70
|
*/
|
|
71
71
|
function generateArchitectureTemplate(config) {
|
|
72
|
+
const arch = config.architecture;
|
|
72
73
|
let template = `# Project Architecture
|
|
73
74
|
|
|
74
75
|
This document describes the architecture patterns and conventions for ${config.project.name}.
|
|
@@ -76,8 +77,34 @@ This document describes the architecture patterns and conventions for ${config.p
|
|
|
76
77
|
## Overview
|
|
77
78
|
|
|
78
79
|
- **Project Name:** ${config.project.name}
|
|
80
|
+
- **Project Type:** ${config.project.type || 'Not specified'}
|
|
81
|
+
- **Primary Language:** ${config.project.primaryLanguage || 'Not specified'}
|
|
82
|
+
- **Database:** ${config.project.database || 'Not specified'}
|
|
83
|
+
- **ORM:** ${config.project.orm || 'Not specified'}
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
`;
|
|
86
|
+
if (arch) {
|
|
87
|
+
template += `## Architecture Pattern
|
|
88
|
+
|
|
89
|
+
**Pattern:** ${arch.pattern || 'Not specified'}
|
|
90
|
+
|
|
91
|
+
`;
|
|
92
|
+
if (arch.llmFramework) {
|
|
93
|
+
template += `### LLM Integration
|
|
94
|
+
|
|
95
|
+
- **Framework:** ${arch.llmFramework}
|
|
96
|
+
- **Usage:** ${arch.llmUsage?.join(', ') || 'Not specified'}
|
|
97
|
+
- **Deterministic Usage:** ${arch.deterministicUsage?.join(', ') || 'Not specified'}
|
|
98
|
+
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
template += `## Development Guidelines
|
|
103
|
+
|
|
104
|
+
### Testing
|
|
105
|
+
- **Framework:** ${config.testing?.framework || 'Not specified'}
|
|
106
|
+
- **Test Location:** ${config.testing?.testLocation || 'Not specified'}
|
|
107
|
+
- **Test Naming:** ${config.testing?.testNaming || 'Not specified'}
|
|
81
108
|
|
|
82
109
|
### Git Workflow
|
|
83
110
|
- **Default Branch:** ${config.git.defaultBranch}
|
package/dist/src/fraim/types.js
CHANGED
|
@@ -18,6 +18,17 @@ exports.DEFAULT_FRAIM_CONFIG = {
|
|
|
18
18
|
repoOwner: '',
|
|
19
19
|
repoName: ''
|
|
20
20
|
},
|
|
21
|
+
architecture: {
|
|
22
|
+
pattern: 'llm-deterministic-separation',
|
|
23
|
+
llmFramework: 'baml',
|
|
24
|
+
llmUsage: [],
|
|
25
|
+
deterministicUsage: ['data-processing', 'api-calls']
|
|
26
|
+
},
|
|
27
|
+
testing: {
|
|
28
|
+
framework: 'tsx-test',
|
|
29
|
+
testLocation: 'root',
|
|
30
|
+
testNaming: 'test-*.ts'
|
|
31
|
+
},
|
|
21
32
|
customizations: {
|
|
22
33
|
workflowsPath: '.fraim/workflows'
|
|
23
34
|
},
|
|
@@ -82,9 +82,19 @@ class FraimMCPServer {
|
|
|
82
82
|
this.app.use(express_1.default.json());
|
|
83
83
|
// Load server version
|
|
84
84
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
// Try process.cwd() first as it's the root in most deployments
|
|
86
|
+
let pkgPath = (0, path_1.join)(process.cwd(), 'package.json');
|
|
87
|
+
if (!(0, fs_1.existsSync)(pkgPath)) {
|
|
88
|
+
// Fallback to relative to __dirname (dist/src/ -> ../../package.json)
|
|
89
|
+
pkgPath = (0, path_1.join)(__dirname, '..', '..', 'package.json');
|
|
90
|
+
}
|
|
91
|
+
if ((0, fs_1.existsSync)(pkgPath)) {
|
|
92
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf8'));
|
|
93
|
+
this.serverVersion = pkg.version;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.serverVersion = 'unknown';
|
|
97
|
+
}
|
|
88
98
|
}
|
|
89
99
|
catch (e) {
|
|
90
100
|
this.serverVersion = 'unknown';
|
|
@@ -217,8 +227,11 @@ class FraimMCPServer {
|
|
|
217
227
|
});
|
|
218
228
|
}
|
|
219
229
|
catch (error) {
|
|
220
|
-
|
|
221
|
-
|
|
230
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
231
|
+
console.error('❌ FRAIM AUTH: Error during verification:', msg);
|
|
232
|
+
if (error instanceof Error && error.stack)
|
|
233
|
+
console.error(error.stack);
|
|
234
|
+
res.status(500).json({ error: 'Internal Server Error', details: msg });
|
|
222
235
|
}
|
|
223
236
|
}
|
|
224
237
|
adminAuthMiddleware(req, res, next) {
|
|
@@ -956,12 +969,10 @@ Use this to discover which workflow to call with get_fraim_workflow.`,
|
|
|
956
969
|
const workflowPath = `workflows/${normalizedName}.md`;
|
|
957
970
|
const metadata = this.fileIndex.get(workflowPath);
|
|
958
971
|
if (!metadata) {
|
|
959
|
-
// Try alternative paths
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
`workflows
|
|
963
|
-
];
|
|
964
|
-
for (const altPath of alternativePaths) {
|
|
972
|
+
// Try alternative category paths
|
|
973
|
+
const categories = ['product-building', 'customer-development', 'business-development', 'marketing', 'performance', 'quality-assurance', 'reviewer', 'startup-credits', 'bootstrap', 'deploy'];
|
|
974
|
+
for (const cat of categories) {
|
|
975
|
+
const altPath = `workflows/${cat}/${normalizedName}.md`;
|
|
965
976
|
const altMetadata = this.fileIndex.get(altPath);
|
|
966
977
|
if (altMetadata) {
|
|
967
978
|
return this.returnWorkflowFile(altMetadata);
|
|
@@ -24,7 +24,7 @@ function getPort() {
|
|
|
24
24
|
catch (e) {
|
|
25
25
|
// Silently fail and use default
|
|
26
26
|
}
|
|
27
|
-
return Number(process.env.FRAIM_MCP_PORT) || 15300;
|
|
27
|
+
return Number(process.env.PORT) || Number(process.env.WEBSITES_PORT) || Number(process.env.FRAIM_MCP_PORT) || 15300;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Determines the database name based on the git branch
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_child_process_1 = require("node:child_process");
|
|
40
|
+
const test_utils_1 = require("./test-utils");
|
|
41
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
42
|
+
const fs_1 = __importDefault(require("fs"));
|
|
43
|
+
const path_1 = __importDefault(require("path"));
|
|
44
|
+
const os_1 = __importDefault(require("os"));
|
|
45
|
+
async function testCliLifecycle() {
|
|
46
|
+
console.log(' 🚀 Testing Fraim CLI Lifecycle...');
|
|
47
|
+
// Create a temp directory for the test project
|
|
48
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-test-'));
|
|
49
|
+
console.log(` 📂 Created temp dir: ${tempDir}`);
|
|
50
|
+
try {
|
|
51
|
+
const platform = process.platform;
|
|
52
|
+
const npx = platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
53
|
+
const cliScript = path_1.default.resolve(__dirname, '../src/cli/fraim.ts');
|
|
54
|
+
// Helper to run CLI commands
|
|
55
|
+
const runFraim = (args, cwdOverride) => {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const ps = (0, node_child_process_1.spawn)(npx, ['tsx', `"${cliScript}"`, ...args], {
|
|
58
|
+
cwd: cwdOverride || tempDir,
|
|
59
|
+
env: { ...process.env, TEST_MODE: 'true' }, // Disable interactive first-run
|
|
60
|
+
shell: true
|
|
61
|
+
});
|
|
62
|
+
let stdout = '';
|
|
63
|
+
let stderr = '';
|
|
64
|
+
ps.stdout.on('data', d => stdout += d.toString());
|
|
65
|
+
ps.stderr.on('data', d => stderr += d.toString());
|
|
66
|
+
ps.on('close', (code) => {
|
|
67
|
+
resolve({ stdout, stderr, code });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
// 1. Test `fraim init`
|
|
72
|
+
console.log(' Testing "fraim init"...');
|
|
73
|
+
// Create a fake git repo to test population
|
|
74
|
+
try {
|
|
75
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
76
|
+
execSync('git init', { cwd: tempDir });
|
|
77
|
+
execSync('git remote add origin https://github.com/test-owner/test-repo.git', { cwd: tempDir });
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
console.warn(' ⚠️ Could not initialize git in temp dir, skipping population test');
|
|
81
|
+
}
|
|
82
|
+
const initRes = await runFraim(['init'], tempDir);
|
|
83
|
+
if (initRes.code !== 0) {
|
|
84
|
+
console.error('Init failed:', initRes.stderr);
|
|
85
|
+
console.error('STDOUT:', initRes.stdout); // Keep this line for better debugging
|
|
86
|
+
}
|
|
87
|
+
node_assert_1.default.strictEqual(initRes.code, 0, 'Init should exit with 0');
|
|
88
|
+
node_assert_1.default.ok(fs_1.default.existsSync(path_1.default.join(tempDir, '.fraim')), '.fraim dir should exist');
|
|
89
|
+
const configPath = path_1.default.join(tempDir, '.fraim', 'config.json');
|
|
90
|
+
node_assert_1.default.ok(fs_1.default.existsSync(configPath), 'config.json should exist');
|
|
91
|
+
const config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
92
|
+
// Deep structure validation
|
|
93
|
+
node_assert_1.default.ok(config.project, 'config.project should exist');
|
|
94
|
+
node_assert_1.default.strictEqual(typeof config.project.name, 'string', 'project.name should be a string');
|
|
95
|
+
node_assert_1.default.ok(config.persona, 'config.persona should exist');
|
|
96
|
+
node_assert_1.default.strictEqual(config.persona.name, 'AI Agent', 'Default persona name should be correct');
|
|
97
|
+
node_assert_1.default.ok(config.marketing, 'config.marketing should exist');
|
|
98
|
+
node_assert_1.default.strictEqual(config.marketing.websiteUrl, '', 'websiteUrl should be empty placeholder');
|
|
99
|
+
node_assert_1.default.ok(config.git, 'config.git should exist');
|
|
100
|
+
// If git was initialized successfully, these should NOT be empty
|
|
101
|
+
const hasGit = fs_1.default.existsSync(path_1.default.join(tempDir, '.git'));
|
|
102
|
+
if (hasGit) {
|
|
103
|
+
node_assert_1.default.strictEqual(config.git.repoOwner, 'test-owner', 'repoOwner should be detected from git');
|
|
104
|
+
node_assert_1.default.strictEqual(config.git.repoName, 'test-repo', 'repoName should be detected from git');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.warn(' ⚠️ No .git found in tempDir, owner/repo detection skipped');
|
|
108
|
+
node_assert_1.default.strictEqual(config.git.repoOwner, '', 'repoOwner should be empty if no git');
|
|
109
|
+
node_assert_1.default.strictEqual(config.git.repoName, '', 'repoName should be empty if no git');
|
|
110
|
+
}
|
|
111
|
+
node_assert_1.default.ok(config.customizations, 'config.customizations should exist');
|
|
112
|
+
node_assert_1.default.strictEqual(config.customizations.workflowsPath, '.fraim/workflows');
|
|
113
|
+
node_assert_1.default.ok(config.architecture, 'architecture should be in generated config');
|
|
114
|
+
node_assert_1.default.ok(config.testing, 'testing should be in generated config');
|
|
115
|
+
node_assert_1.default.ok(!config.mcp, 'mcp should not be in generated config');
|
|
116
|
+
// 2. Test `fraim sync`
|
|
117
|
+
// We need a fake registry for sync to work against
|
|
118
|
+
const registryDir = path_1.default.join(tempDir, 'registry', 'workflows');
|
|
119
|
+
fs_1.default.mkdirSync(registryDir, { recursive: true });
|
|
120
|
+
const dummyWorkflow = `---
|
|
121
|
+
intent: Test Intent
|
|
122
|
+
principles:
|
|
123
|
+
- Test Principle
|
|
124
|
+
---
|
|
125
|
+
# Test Workflow`;
|
|
126
|
+
fs_1.default.writeFileSync(path_1.default.join(registryDir, 'test-workflow.md'), dummyWorkflow);
|
|
127
|
+
console.log(' Testing "fraim sync"...');
|
|
128
|
+
const syncRes = await runFraim(['sync']);
|
|
129
|
+
if (syncRes.code === 0) {
|
|
130
|
+
console.log(' ✅ Sync successful');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(' ⚠️ Sync warning (env dependent):', syncRes.stderr);
|
|
134
|
+
}
|
|
135
|
+
// 3. Test `fraim list`
|
|
136
|
+
console.log(' Testing "fraim list"...');
|
|
137
|
+
const listRes = await runFraim(['list']);
|
|
138
|
+
node_assert_1.default.strictEqual(listRes.code, 0);
|
|
139
|
+
// 4. Test `fraim doctor`
|
|
140
|
+
console.log(' Testing "fraim doctor"...');
|
|
141
|
+
const doctorRes = await runFraim(['doctor']);
|
|
142
|
+
node_assert_1.default.strictEqual(doctorRes.code, 0);
|
|
143
|
+
node_assert_1.default.ok(doctorRes.stdout.includes('Everything looks great'), 'Doctor should be happy');
|
|
144
|
+
console.log(' ✅ CLI Lifecycle verified!');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error(' ❌ CLI Test failed:', error);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
try {
|
|
153
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
catch (e) { }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function runCliTest(testCase) {
|
|
159
|
+
return await testCase.testFunction();
|
|
160
|
+
}
|
|
161
|
+
const testCases = [
|
|
162
|
+
{
|
|
163
|
+
name: 'Fraim CLI Lifecycle',
|
|
164
|
+
description: 'Tests init, sync, list, and doctor commands',
|
|
165
|
+
testFunction: testCliLifecycle,
|
|
166
|
+
tags: ['cli', 'fraim']
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
(0, test_utils_1.runTests)(testCases, runCliTest, 'Fraim CLI Tests');
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const test_utils_1 = require("./test-utils");
|
|
8
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
/**
|
|
13
|
+
* Helper to run the interactive first-run experience within 'fraim init'
|
|
14
|
+
*/
|
|
15
|
+
async function runInteractiveInit(cwd, interactions) {
|
|
16
|
+
const projectRoot = path_1.default.resolve(__dirname, '..');
|
|
17
|
+
const cliScript = path_1.default.resolve(projectRoot, 'src/cli/fraim.ts');
|
|
18
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const ps = (0, node_child_process_1.spawn)(npx, ['tsx', `"${cliScript}"`, 'init'], {
|
|
21
|
+
cwd,
|
|
22
|
+
env: { ...process.env, CI: 'false', TEST_MODE: 'false' },
|
|
23
|
+
shell: true,
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
25
|
+
});
|
|
26
|
+
let stdout = '';
|
|
27
|
+
let stderr = '';
|
|
28
|
+
let currentPromptIndex = 0;
|
|
29
|
+
const timeout = setTimeout(() => {
|
|
30
|
+
console.error(' ❌ Interactive init timed out');
|
|
31
|
+
ps.kill('SIGKILL');
|
|
32
|
+
resolve({ stdout, stderr, code: -1 });
|
|
33
|
+
}, 45000);
|
|
34
|
+
ps.stdout.on('data', d => {
|
|
35
|
+
const chunk = d.toString();
|
|
36
|
+
stdout += chunk;
|
|
37
|
+
if (currentPromptIndex < interactions.length) {
|
|
38
|
+
const target = interactions[currentPromptIndex];
|
|
39
|
+
if (chunk.includes(target.prompt)) {
|
|
40
|
+
currentPromptIndex++;
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
ps.stdin.write(target.response + '\n');
|
|
43
|
+
}, 500);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
ps.stderr.on('data', d => stderr += d.toString());
|
|
48
|
+
ps.on('close', code => {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
resolve({ stdout, stderr, code });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const testCases = [
|
|
55
|
+
{
|
|
56
|
+
name: 'Journey: Setup with Key, No Arch Doc',
|
|
57
|
+
testFunction: async () => {
|
|
58
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-jour-1-'));
|
|
59
|
+
try {
|
|
60
|
+
const res = await runInteractiveInit(tempDir, [
|
|
61
|
+
{ prompt: 'Do you have a FRAIM key?', response: 'key-123' },
|
|
62
|
+
{ prompt: 'Do you have an architecture document', response: 'n' }
|
|
63
|
+
]);
|
|
64
|
+
node_assert_1.default.strictEqual(res.code, 0);
|
|
65
|
+
node_assert_1.default.ok(res.stdout.includes('Key received'));
|
|
66
|
+
node_assert_1.default.ok(res.stdout.includes('Run the bootstrap/create-architecture workflow'));
|
|
67
|
+
// VERIFY STUBS EXIST
|
|
68
|
+
const workflowsDir = path_1.default.join(tempDir, '.fraim', 'workflows');
|
|
69
|
+
node_assert_1.default.ok(fs_1.default.existsSync(workflowsDir), 'Workflows dir missing');
|
|
70
|
+
const stubs = fs_1.default.readdirSync(workflowsDir);
|
|
71
|
+
node_assert_1.default.ok(stubs.length > 0, 'No workflow stubs created');
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
tags: ['cli', 'journey', 'init']
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Journey: Setup with Arch Doc Path',
|
|
82
|
+
testFunction: async () => {
|
|
83
|
+
const tempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-jour-2-'));
|
|
84
|
+
const archPath = 'docs/my-architecture.md';
|
|
85
|
+
try {
|
|
86
|
+
fs_1.default.mkdirSync(path_1.default.join(tempDir, '.fraim'), { recursive: true });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, '.fraim', 'config.json'), JSON.stringify({ version: '1.0.0' }));
|
|
88
|
+
fs_1.default.mkdirSync(path_1.default.join(tempDir, 'docs'), { recursive: true });
|
|
89
|
+
fs_1.default.writeFileSync(path_1.default.join(tempDir, archPath), '# Arch');
|
|
90
|
+
const res = await runInteractiveInit(tempDir, [
|
|
91
|
+
{ prompt: 'Do you have a FRAIM key?', response: '' },
|
|
92
|
+
{ prompt: 'Do you have an architecture document', response: 'y' },
|
|
93
|
+
{ prompt: 'provide the relative path', response: archPath }
|
|
94
|
+
]);
|
|
95
|
+
node_assert_1.default.strictEqual(res.code, 0);
|
|
96
|
+
node_assert_1.default.ok(res.stdout.includes(`Linked architecture document: ${archPath}`));
|
|
97
|
+
const config = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tempDir, '.fraim', 'config.json'), 'utf8'));
|
|
98
|
+
node_assert_1.default.strictEqual(config.customizations.architectureDoc, archPath);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
fs_1.default.rmSync(tempDir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
tags: ['cli', 'journey', 'init']
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
(0, test_utils_1.runTests)(testCases, (tc) => tc.testFunction(), 'Fraim Setup Journeys');
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
const test_utils_1 = require("./test-utils");
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
|
+
const types_1 = require("../src/fraim/types");
|
|
9
|
+
const template_processor_1 = require("../src/fraim/template-processor");
|
|
10
|
+
async function testConfigDefaults() {
|
|
11
|
+
console.log(' 🧪 Testing DEFAULT_FRAIM_CONFIG placeholders...');
|
|
12
|
+
try {
|
|
13
|
+
node_assert_1.default.strictEqual(types_1.DEFAULT_FRAIM_CONFIG.persona.name, 'AI Agent', 'Default persona name should be "AI Agent"');
|
|
14
|
+
node_assert_1.default.ok(types_1.DEFAULT_FRAIM_CONFIG.customizations?.workflowsPath, 'workflowsPath should exist');
|
|
15
|
+
node_assert_1.default.ok(types_1.DEFAULT_FRAIM_CONFIG.architecture, 'architecture should be restored');
|
|
16
|
+
node_assert_1.default.ok(types_1.DEFAULT_FRAIM_CONFIG.testing, 'testing should be restored');
|
|
17
|
+
node_assert_1.default.ok(!types_1.DEFAULT_FRAIM_CONFIG.mcp, 'mcp should still be removed');
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
console.error(' ❌ Default configuration verification failed:', e);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function testTemplateProcessor() {
|
|
26
|
+
console.log(' 🧪 Testing Template Processor with new config fields...');
|
|
27
|
+
try {
|
|
28
|
+
const mockConfig = {
|
|
29
|
+
...types_1.DEFAULT_FRAIM_CONFIG,
|
|
30
|
+
project: { ...types_1.DEFAULT_FRAIM_CONFIG.project, name: 'Generic Bot' },
|
|
31
|
+
persona: { ...types_1.DEFAULT_FRAIM_CONFIG.persona, name: 'Ava' },
|
|
32
|
+
marketing: {
|
|
33
|
+
...types_1.DEFAULT_FRAIM_CONFIG.marketing,
|
|
34
|
+
websiteUrl: 'https://ava.ai',
|
|
35
|
+
chatUrl: 'https://chat.ava.ai'
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const template = 'Welcome to {{config.project.name}}. I am {{config.persona.name}}. Visit {{config.marketing.websiteUrl}} or chat at {{config.marketing.chatUrl}}.';
|
|
39
|
+
const processed = (0, template_processor_1.processTemplateVariables)(template, mockConfig);
|
|
40
|
+
const expected = 'Welcome to Generic Bot. I am Ava. Visit https://ava.ai or chat at https://chat.ava.ai.';
|
|
41
|
+
node_assert_1.default.strictEqual(processed, expected, 'Template variables should be correctly replaced');
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.error(' ❌ Template processing verification failed:', e);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const testCases = [
|
|
50
|
+
{
|
|
51
|
+
name: 'Config Defaults',
|
|
52
|
+
description: 'Verify DEFAULT_FRAIM_CONFIG has empty placeholders',
|
|
53
|
+
testFunction: testConfigDefaults,
|
|
54
|
+
tags: ['config', 'genericization']
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'Template Processor',
|
|
58
|
+
description: 'Verify template variables for new config fields',
|
|
59
|
+
testFunction: testTemplateProcessor,
|
|
60
|
+
tags: ['templates', 'genericization']
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
async function runGenericizationTests(testCase) {
|
|
64
|
+
return await testCase.testFunction();
|
|
65
|
+
}
|
|
66
|
+
(0, test_utils_1.runTests)(testCases, runGenericizationTests, 'FRAIM Genericization');
|