fraim-framework 2.0.71 → 2.0.73

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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework CLI Entry Point
@@ -8,6 +8,7 @@ const commander_1 = require("commander");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
+ const inheritance_parser_1 = require("../../core/utils/inheritance-parser");
11
12
  exports.doctorCommand = new commander_1.Command('doctor')
12
13
  .description('Validate FRAIM installation and configuration')
13
14
  .action(async () => {
@@ -71,4 +72,58 @@ exports.doctorCommand = new commander_1.Command('doctor')
71
72
  else {
72
73
  console.log(chalk_1.default.red(`\n🩹 Found ${issuesFound} issues. See recommendations above.`));
73
74
  }
75
+ // 5. Check for overrides
76
+ const overridesDir = path_1.default.join(fraimDir, 'overrides');
77
+ if (fs_1.default.existsSync(overridesDir)) {
78
+ console.log(chalk_1.default.blue('\nšŸ“ Override Diagnostics:\n'));
79
+ const overrides = [];
80
+ const scanDir = (dir, base = '') => {
81
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ const relativePath = path_1.default.join(base, entry.name);
84
+ if (entry.isDirectory()) {
85
+ scanDir(path_1.default.join(dir, entry.name), relativePath);
86
+ }
87
+ else {
88
+ overrides.push(relativePath.replace(/\\/g, '/'));
89
+ }
90
+ }
91
+ };
92
+ scanDir(overridesDir);
93
+ if (overrides.length === 0) {
94
+ console.log(chalk_1.default.gray(' No active overrides found.'));
95
+ }
96
+ else {
97
+ console.log(chalk_1.default.green(` Found ${overrides.length} active override(s):\n`));
98
+ const parser = new inheritance_parser_1.InheritanceParser();
99
+ for (const override of overrides) {
100
+ const overridePath = path_1.default.join(overridesDir, override);
101
+ const content = fs_1.default.readFileSync(overridePath, 'utf-8');
102
+ const parseResult = parser.parse(content);
103
+ if (parseResult.hasImports) {
104
+ console.log(chalk_1.default.white(` šŸ“„ ${override}`));
105
+ console.log(chalk_1.default.gray(` Inherits from: ${parseResult.imports.join(', ')}`));
106
+ // Validate import syntax
107
+ let hasErrors = false;
108
+ for (const importPath of parseResult.imports) {
109
+ try {
110
+ parser.sanitizePath(importPath);
111
+ }
112
+ catch (error) {
113
+ console.log(chalk_1.default.red(` āš ļø Invalid import: ${error.message}`));
114
+ hasErrors = true;
115
+ }
116
+ }
117
+ if (!hasErrors) {
118
+ console.log(chalk_1.default.green(` āœ… Syntax valid`));
119
+ }
120
+ }
121
+ else {
122
+ console.log(chalk_1.default.white(` šŸ“„ ${override}`));
123
+ console.log(chalk_1.default.gray(` Full override (no inheritance)`));
124
+ }
125
+ console.log('');
126
+ }
127
+ }
128
+ }
74
129
  });
@@ -0,0 +1,202 @@
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.listOverridableCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ exports.listOverridableCommand = new commander_1.Command('list-overridable')
12
+ .description('List all FRAIM registry paths that can be overridden')
13
+ .option('--job-category <category>', 'Filter by workflow category (e.g., product-building, customer-development, marketing)')
14
+ .option('--rules', 'Show all overridable rules')
15
+ .action(async (options) => {
16
+ const projectRoot = process.cwd();
17
+ const fraimDir = path_1.default.join(projectRoot, '.fraim');
18
+ const configPath = path_1.default.join(fraimDir, 'config.json');
19
+ const overridesDir = path_1.default.join(fraimDir, 'overrides');
20
+ // Validate .fraim directory exists
21
+ if (!fs_1.default.existsSync(fraimDir)) {
22
+ console.log(chalk_1.default.red('āŒ .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
23
+ process.exit(1);
24
+ }
25
+ // Determine registry location (try framework root first, then node_modules)
26
+ let registryRoot = null;
27
+ const frameworkRoot = path_1.default.join(__dirname, '..', '..', '..');
28
+ const frameworkRegistry = path_1.default.join(frameworkRoot, 'registry');
29
+ if (fs_1.default.existsSync(frameworkRegistry)) {
30
+ registryRoot = frameworkRegistry;
31
+ }
32
+ else {
33
+ const nodeModulesRegistry = path_1.default.join(process.cwd(), 'node_modules', '@fraim', 'framework', 'registry');
34
+ if (fs_1.default.existsSync(nodeModulesRegistry)) {
35
+ registryRoot = nodeModulesRegistry;
36
+ }
37
+ }
38
+ if (!registryRoot) {
39
+ console.log(chalk_1.default.red('āŒ Could not find FRAIM registry. Please ensure @fraim/framework is installed.'));
40
+ process.exit(1);
41
+ }
42
+ console.log(chalk_1.default.blue('šŸ“‹ Overridable FRAIM Registry Paths:\n'));
43
+ // Get list of existing overrides
44
+ const existingOverrides = new Set();
45
+ if (fs_1.default.existsSync(overridesDir)) {
46
+ const scanDir = (dir, base = '') => {
47
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ const relativePath = path_1.default.join(base, entry.name);
50
+ if (entry.isDirectory()) {
51
+ scanDir(path_1.default.join(dir, entry.name), relativePath);
52
+ }
53
+ else {
54
+ existingOverrides.add(relativePath.replace(/\\/g, '/'));
55
+ }
56
+ }
57
+ };
58
+ scanDir(overridesDir);
59
+ }
60
+ // Handle --rules flag
61
+ if (options.rules) {
62
+ const rulesDir = path_1.default.join(registryRoot, 'rules');
63
+ if (fs_1.default.existsSync(rulesDir)) {
64
+ console.log(chalk_1.default.bold.cyan('Rules:\n'));
65
+ const ruleFiles = fs_1.default.readdirSync(rulesDir)
66
+ .filter(f => f.endsWith('.md'))
67
+ .sort();
68
+ for (const file of ruleFiles) {
69
+ const filePath = `rules/${file}`;
70
+ const hasOverride = existingOverrides.has(filePath);
71
+ const status = hasOverride
72
+ ? chalk_1.default.green('[OVERRIDDEN]')
73
+ : chalk_1.default.gray('[OVERRIDABLE]');
74
+ console.log(` ${status} ${filePath}`);
75
+ }
76
+ console.log('');
77
+ }
78
+ showTips();
79
+ return;
80
+ }
81
+ // Handle --job-category flag
82
+ if (options.jobCategory) {
83
+ const category = options.jobCategory;
84
+ const workflowsDir = path_1.default.join(registryRoot, 'workflows', category);
85
+ const templatesDir = path_1.default.join(registryRoot, 'templates', category);
86
+ if (!fs_1.default.existsSync(workflowsDir)) {
87
+ console.log(chalk_1.default.red(`āŒ Category "${category}" not found.`));
88
+ console.log(chalk_1.default.gray('\nAvailable categories:'));
89
+ const categoriesDir = path_1.default.join(registryRoot, 'workflows');
90
+ const categories = fs_1.default.readdirSync(categoriesDir, { withFileTypes: true })
91
+ .filter(d => d.isDirectory())
92
+ .map(d => d.name)
93
+ .sort();
94
+ categories.forEach(c => console.log(chalk_1.default.gray(` - ${c}`)));
95
+ process.exit(1);
96
+ }
97
+ // Show workflows for this category
98
+ console.log(chalk_1.default.bold.cyan(`Workflows (${category}):\n`));
99
+ const workflowFiles = fs_1.default.readdirSync(workflowsDir)
100
+ .filter(f => f.endsWith('.md'))
101
+ .sort();
102
+ for (const file of workflowFiles) {
103
+ const filePath = `workflows/${category}/${file}`;
104
+ const hasOverride = existingOverrides.has(filePath);
105
+ const status = hasOverride
106
+ ? chalk_1.default.green('[OVERRIDDEN]')
107
+ : chalk_1.default.gray('[OVERRIDABLE]');
108
+ console.log(` ${status} ${filePath}`);
109
+ }
110
+ console.log('');
111
+ // Show templates for this category (if they exist)
112
+ if (fs_1.default.existsSync(templatesDir)) {
113
+ console.log(chalk_1.default.bold.cyan(`Templates (${category}):\n`));
114
+ const templateFiles = fs_1.default.readdirSync(templatesDir)
115
+ .filter(f => f.endsWith('.md') || f.endsWith('.html') || f.endsWith('.csv') || f.endsWith('.yml'))
116
+ .sort();
117
+ for (const file of templateFiles) {
118
+ const filePath = `templates/${category}/${file}`;
119
+ const hasOverride = existingOverrides.has(filePath);
120
+ const status = hasOverride
121
+ ? chalk_1.default.green('[OVERRIDDEN]')
122
+ : chalk_1.default.gray('[OVERRIDABLE]');
123
+ console.log(` ${status} ${filePath}`);
124
+ }
125
+ console.log('');
126
+ }
127
+ showTips();
128
+ return;
129
+ }
130
+ // Default: Show available categories and existing overrides
131
+ console.log(chalk_1.default.bold.cyan('Available Workflow Categories:\n'));
132
+ const workflowsDir = path_1.default.join(registryRoot, 'workflows');
133
+ const categories = fs_1.default.readdirSync(workflowsDir, { withFileTypes: true })
134
+ .filter(d => d.isDirectory())
135
+ .map(d => d.name)
136
+ .sort();
137
+ // Group categories for better display
138
+ const columns = 3;
139
+ for (let i = 0; i < categories.length; i += columns) {
140
+ const row = categories.slice(i, i + columns);
141
+ const formatted = row.map(cat => chalk_1.default.gray(` • ${cat.padEnd(30)}`)).join('');
142
+ console.log(formatted);
143
+ }
144
+ console.log('');
145
+ // Show existing overrides
146
+ if (existingOverrides.size > 0) {
147
+ console.log(chalk_1.default.bold.cyan('Your Active Overrides:\n'));
148
+ // Group by type
149
+ const overridesByType = {
150
+ workflows: [],
151
+ templates: [],
152
+ rules: [],
153
+ other: []
154
+ };
155
+ for (const override of Array.from(existingOverrides).sort()) {
156
+ if (override.startsWith('workflows/')) {
157
+ overridesByType.workflows.push(override);
158
+ }
159
+ else if (override.startsWith('templates/')) {
160
+ overridesByType.templates.push(override);
161
+ }
162
+ else if (override.startsWith('rules/')) {
163
+ overridesByType.rules.push(override);
164
+ }
165
+ else {
166
+ overridesByType.other.push(override);
167
+ }
168
+ }
169
+ if (overridesByType.workflows.length > 0) {
170
+ console.log(chalk_1.default.gray(' Workflows:'));
171
+ overridesByType.workflows.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
172
+ console.log('');
173
+ }
174
+ if (overridesByType.templates.length > 0) {
175
+ console.log(chalk_1.default.gray(' Templates:'));
176
+ overridesByType.templates.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
177
+ console.log('');
178
+ }
179
+ if (overridesByType.rules.length > 0) {
180
+ console.log(chalk_1.default.gray(' Rules:'));
181
+ overridesByType.rules.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
182
+ console.log('');
183
+ }
184
+ if (overridesByType.other.length > 0) {
185
+ console.log(chalk_1.default.gray(' Other:'));
186
+ overridesByType.other.forEach(o => console.log(` ${chalk_1.default.green('[OVERRIDDEN]')} ${o}`));
187
+ console.log('');
188
+ }
189
+ }
190
+ else {
191
+ console.log(chalk_1.default.gray('No active overrides yet.\n'));
192
+ }
193
+ showTips();
194
+ function showTips() {
195
+ console.log(chalk_1.default.gray('Tips:'));
196
+ console.log(chalk_1.default.gray(' • Use "fraim override <path> --inherit" to inherit from global'));
197
+ console.log(chalk_1.default.gray(' • Use "fraim override <path> --copy" to copy current content'));
198
+ console.log(chalk_1.default.gray(' • Use --job-category <category> to see category-specific items'));
199
+ console.log(chalk_1.default.gray(' • Use --rules to see all overridable rules'));
200
+ console.log(chalk_1.default.gray(' • Overrides are stored in .fraim/overrides/'));
201
+ }
202
+ });
@@ -0,0 +1,181 @@
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.overrideCommand = void 0;
7
+ const commander_1 = require("commander");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const axios_1 = __importDefault(require("axios"));
12
+ const git_utils_1 = require("../../core/utils/git-utils");
13
+ exports.overrideCommand = new commander_1.Command('override')
14
+ .description('Create a local override for a FRAIM registry file')
15
+ .argument('<path>', 'Registry path to override (e.g., workflows/product-building/spec.md)')
16
+ .option('--inherit', 'Create override with {{ import }} directive (inherits from global)')
17
+ .option('--copy', 'Copy current content from server to local')
18
+ .option('--local', 'Fetch from local development server (port derived from git branch)')
19
+ .action(async (registryPath, options) => {
20
+ const projectRoot = process.cwd();
21
+ const fraimDir = path_1.default.join(projectRoot, '.fraim');
22
+ const configPath = path_1.default.join(fraimDir, 'config.json');
23
+ // Validate .fraim directory exists
24
+ if (!fs_1.default.existsSync(fraimDir)) {
25
+ console.log(chalk_1.default.red('āŒ .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
26
+ process.exit(1);
27
+ }
28
+ // Validate path format
29
+ if (registryPath.includes('..') || path_1.default.isAbsolute(registryPath)) {
30
+ console.log(chalk_1.default.red('āŒ Invalid path. Path must be relative and cannot contain ".."'));
31
+ process.exit(1);
32
+ }
33
+ // Ensure exactly one option is specified
34
+ if (options.inherit && options.copy) {
35
+ console.log(chalk_1.default.red('āŒ Cannot use both --inherit and --copy. Choose one.'));
36
+ process.exit(1);
37
+ }
38
+ if (!options.inherit && !options.copy) {
39
+ console.log(chalk_1.default.red('āŒ Must specify either --inherit or --copy.'));
40
+ process.exit(1);
41
+ }
42
+ // Create overrides directory structure
43
+ const overridePath = path_1.default.join(fraimDir, 'overrides', registryPath);
44
+ const overrideDir = path_1.default.dirname(overridePath);
45
+ if (!fs_1.default.existsSync(overrideDir)) {
46
+ fs_1.default.mkdirSync(overrideDir, { recursive: true });
47
+ }
48
+ // Check if override already exists
49
+ if (fs_1.default.existsSync(overridePath)) {
50
+ console.log(chalk_1.default.yellow(`āš ļø Override already exists: ${registryPath}`));
51
+ console.log(chalk_1.default.gray(` Location: ${overridePath}`));
52
+ process.exit(0);
53
+ }
54
+ let content;
55
+ if (options.inherit) {
56
+ // Create file with import directive
57
+ content = `{{ import: ${registryPath} }}\n\n<!-- Add your custom content below -->\n`;
58
+ console.log(chalk_1.default.blue(`šŸ“ Creating inherited override for: ${registryPath}`));
59
+ }
60
+ else {
61
+ // Fetch current content from server using MCP protocol
62
+ const isLocal = options.local || false;
63
+ const serverType = isLocal ? 'local server' : 'remote server';
64
+ console.log(chalk_1.default.blue(`šŸ“„ Fetching current content from ${serverType}: ${registryPath}`));
65
+ try {
66
+ // Read config to get remote URL
67
+ let config = {};
68
+ if (fs_1.default.existsSync(configPath)) {
69
+ config = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
70
+ }
71
+ // Determine server URL
72
+ let serverUrl;
73
+ if (isLocal) {
74
+ const localPort = process.env.FRAIM_MCP_PORT ? parseInt(process.env.FRAIM_MCP_PORT) : (0, git_utils_1.getPort)();
75
+ serverUrl = `http://localhost:${localPort}`;
76
+ }
77
+ else {
78
+ serverUrl = config.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
79
+ }
80
+ const apiKey = isLocal ? 'local-dev' : (config.apiKey || process.env.FRAIM_API_KEY);
81
+ const headers = {
82
+ 'Content-Type': 'application/json'
83
+ };
84
+ if (apiKey) {
85
+ headers['x-api-key'] = apiKey;
86
+ }
87
+ // First, establish session with fraim_connect
88
+ const connectRequest = {
89
+ jsonrpc: '2.0',
90
+ id: 0,
91
+ method: 'tools/call',
92
+ params: {
93
+ name: 'fraim_connect',
94
+ arguments: {
95
+ agent: { name: 'fraim-cli', model: 'override-command' },
96
+ machine: {
97
+ hostname: require('os').hostname(),
98
+ platform: process.platform,
99
+ memory: require('os').totalmem(),
100
+ cpus: require('os').cpus().length
101
+ },
102
+ repo: {
103
+ url: config.repository?.url || 'https://github.com/unknown/unknown',
104
+ owner: config.repository?.owner || 'unknown',
105
+ name: config.repository?.name || 'unknown',
106
+ branch: 'main'
107
+ }
108
+ }
109
+ }
110
+ };
111
+ await axios_1.default.post(`${serverUrl}/mcp`, connectRequest, { headers, timeout: 30000 });
112
+ // Now determine MCP tool and arguments based on path
113
+ let toolName;
114
+ let toolArgs;
115
+ if (registryPath.startsWith('workflows/')) {
116
+ // Extract workflow name from path
117
+ // e.g., "workflows/product-building/spec.md" -> "spec"
118
+ const parts = registryPath.split('/');
119
+ const workflowName = parts[parts.length - 1].replace('.md', '');
120
+ toolName = 'get_fraim_workflow';
121
+ toolArgs = { workflow: workflowName };
122
+ }
123
+ else {
124
+ toolName = 'get_fraim_file';
125
+ toolArgs = { path: registryPath };
126
+ }
127
+ console.log(chalk_1.default.gray(` Using MCP tool: ${toolName}`));
128
+ // Make MCP JSON-RPC request
129
+ const mcpRequest = {
130
+ jsonrpc: '2.0',
131
+ id: 1,
132
+ method: 'tools/call',
133
+ params: {
134
+ name: toolName,
135
+ arguments: toolArgs
136
+ }
137
+ };
138
+ const response = await axios_1.default.post(`${serverUrl}/mcp`, mcpRequest, {
139
+ headers,
140
+ timeout: 30000
141
+ });
142
+ // Extract content from MCP response
143
+ if (response.data.error) {
144
+ throw new Error(response.data.error.message || 'MCP request failed');
145
+ }
146
+ const result = response.data.result;
147
+ if (!result || !result.content || !Array.isArray(result.content)) {
148
+ throw new Error('Unexpected MCP response format');
149
+ }
150
+ // Extract text content from MCP response
151
+ const textContent = result.content.find((c) => c.type === 'text');
152
+ if (!textContent || !textContent.text) {
153
+ throw new Error('No text content in MCP response');
154
+ }
155
+ content = textContent.text;
156
+ console.log(chalk_1.default.green(`āœ… Fetched ${content.length} bytes from ${serverType}`));
157
+ }
158
+ catch (error) {
159
+ console.log(chalk_1.default.red(`āŒ Failed to fetch content from ${serverType}: ${error.message}`));
160
+ if (isLocal) {
161
+ console.log(chalk_1.default.gray(` Tip: Make sure the FRAIM server is running locally (npm run start:fraim)`));
162
+ }
163
+ else {
164
+ console.log(chalk_1.default.gray(` Tip: Check your API key and network connection`));
165
+ console.log(chalk_1.default.gray(` Or use --local to fetch from a locally running server`));
166
+ }
167
+ process.exit(1);
168
+ }
169
+ }
170
+ // Write override file
171
+ try {
172
+ fs_1.default.writeFileSync(overridePath, content, 'utf-8');
173
+ console.log(chalk_1.default.green(`āœ… Created override: ${registryPath}`));
174
+ console.log(chalk_1.default.gray(` Location: ${overridePath}`));
175
+ console.log(chalk_1.default.gray(` Edit this file to customize for your project`));
176
+ }
177
+ catch (error) {
178
+ console.log(chalk_1.default.red(`āŒ Failed to write override file: ${error.message}`));
179
+ process.exit(1);
180
+ }
181
+ });
@@ -12,6 +12,8 @@ const setup_1 = require("./commands/setup");
12
12
  const init_project_1 = require("./commands/init-project");
13
13
  const test_mcp_1 = require("./commands/test-mcp");
14
14
  const add_ide_1 = require("./commands/add-ide");
15
+ const override_1 = require("./commands/override");
16
+ const list_overridable_1 = require("./commands/list-overridable");
15
17
  const fs_1 = __importDefault(require("fs"));
16
18
  const path_1 = __importDefault(require("path"));
17
19
  const program = new commander_1.Command();
@@ -46,4 +48,6 @@ program.addCommand(setup_1.setupCommand);
46
48
  program.addCommand(init_project_1.initProjectCommand);
47
49
  program.addCommand(test_mcp_1.testMCPCommand);
48
50
  program.addCommand(add_ide_1.addIDECommand);
51
+ program.addCommand(override_1.overrideCommand);
52
+ program.addCommand(list_overridable_1.listOverridableCommand);
49
53
  program.parse(process.argv);
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ /**
3
+ * InheritanceParser
4
+ *
5
+ * Parses and resolves {{ import: path }} directives in registry files,
6
+ * enabling local overrides to inherit from global registry files.
7
+ *
8
+ * Security features:
9
+ * - Path traversal protection (rejects .. and absolute paths)
10
+ * - Circular import detection
11
+ * - Max depth limit (5 levels)
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.InheritanceParser = exports.InheritanceError = void 0;
15
+ class InheritanceError extends Error {
16
+ constructor(message, path) {
17
+ super(message);
18
+ this.path = path;
19
+ this.name = 'InheritanceError';
20
+ }
21
+ }
22
+ exports.InheritanceError = InheritanceError;
23
+ /**
24
+ * Regular expression to match {{ import: path }} directives
25
+ */
26
+ const IMPORT_REGEX = /\{\{\s*import:\s*([^\}]+)\s*\}\}/g;
27
+ class InheritanceParser {
28
+ constructor(maxDepth = 5) {
29
+ this.maxDepth = maxDepth;
30
+ }
31
+ /**
32
+ * Extract import directives from content without resolving them
33
+ */
34
+ extractImports(content) {
35
+ const imports = [];
36
+ let match;
37
+ // Reset regex state
38
+ IMPORT_REGEX.lastIndex = 0;
39
+ while ((match = IMPORT_REGEX.exec(content)) !== null) {
40
+ imports.push(match[1].trim());
41
+ }
42
+ return imports;
43
+ }
44
+ /**
45
+ * Sanitize and validate import path
46
+ *
47
+ * @throws {InheritanceError} If path is invalid
48
+ */
49
+ sanitizePath(path) {
50
+ const trimmed = path.trim();
51
+ // Reject empty paths
52
+ if (!trimmed) {
53
+ throw new InheritanceError('Import path cannot be empty');
54
+ }
55
+ // Reject absolute paths (Unix and Windows)
56
+ if (trimmed.startsWith('/') || trimmed.match(/^[A-Za-z]:\\/)) {
57
+ throw new InheritanceError(`Absolute paths not allowed: ${trimmed}`, trimmed);
58
+ }
59
+ // Reject path traversal attempts
60
+ if (trimmed.includes('..')) {
61
+ throw new InheritanceError(`Path traversal not allowed: ${trimmed}`, trimmed);
62
+ }
63
+ return trimmed;
64
+ }
65
+ /**
66
+ * Detect circular imports
67
+ *
68
+ * Special case: If the import path is the same as the current path,
69
+ * it's not a circular import - it's importing the parent/remote version.
70
+ * This allows local overrides to inherit from their remote counterparts.
71
+ *
72
+ * @throws {InheritanceError} If circular import detected
73
+ */
74
+ detectCircularImport(path, visited, isParentImport = false) {
75
+ // If this is a parent import (same path as current), allow it
76
+ if (isParentImport) {
77
+ return;
78
+ }
79
+ if (visited.has(path)) {
80
+ throw new InheritanceError(`Circular import detected: ${path}`, path);
81
+ }
82
+ }
83
+ /**
84
+ * Resolve all import directives in content recursively
85
+ *
86
+ * @param content - Content with {{ import }} directives
87
+ * @param currentPath - Path of current file (for circular detection)
88
+ * @param options - Resolution options
89
+ * @returns Resolved content with all imports replaced
90
+ *
91
+ * @throws {InheritanceError} If circular import, path traversal, or max depth exceeded
92
+ */
93
+ async resolve(content, currentPath, options) {
94
+ const depth = options.currentDepth || 0;
95
+ const visited = options.visited || new Set();
96
+ const maxDepth = options.maxDepth || this.maxDepth;
97
+ // Check depth limit
98
+ if (depth > maxDepth) {
99
+ throw new InheritanceError(`Max import depth exceeded (${maxDepth})`, currentPath);
100
+ }
101
+ // Check circular imports (but allow importing the same path as parent)
102
+ this.detectCircularImport(currentPath, visited, false);
103
+ visited.add(currentPath);
104
+ // Extract imports
105
+ const imports = this.extractImports(content);
106
+ if (imports.length === 0) {
107
+ return content;
108
+ }
109
+ // Resolve each import
110
+ let resolved = content;
111
+ for (const importPath of imports) {
112
+ // Sanitize path
113
+ const sanitized = this.sanitizePath(importPath);
114
+ // Check if this is a parent import (same path as current)
115
+ const isParentImport = sanitized === currentPath;
116
+ // Fetch parent content
117
+ let parentContent;
118
+ try {
119
+ parentContent = await options.fetchParent(sanitized);
120
+ }
121
+ catch (error) {
122
+ throw new InheritanceError(`Failed to fetch parent content: ${sanitized}. ${error.message}`, sanitized);
123
+ }
124
+ // Recursively resolve parent imports
125
+ // For parent imports, use a fresh visited set to allow the same path
126
+ const parentVisited = isParentImport ? new Set() : new Set(visited);
127
+ const resolvedParent = await this.resolve(parentContent, sanitized, {
128
+ ...options,
129
+ currentDepth: depth + 1,
130
+ visited: parentVisited
131
+ });
132
+ // Replace import directive with resolved parent content
133
+ const importDirective = `{{ import: ${importPath} }}`;
134
+ resolved = resolved.replace(importDirective, resolvedParent);
135
+ }
136
+ return resolved;
137
+ }
138
+ /**
139
+ * Parse content and return detailed information about imports
140
+ */
141
+ parse(content) {
142
+ const imports = this.extractImports(content);
143
+ return {
144
+ content,
145
+ imports,
146
+ hasImports: imports.length > 0
147
+ };
148
+ }
149
+ }
150
+ exports.InheritanceParser = InheritanceParser;