prompt-language-shell 0.4.8 → 0.5.0

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.
Files changed (41) hide show
  1. package/dist/config/EXECUTE.md +279 -0
  2. package/dist/config/INTROSPECT.md +9 -6
  3. package/dist/config/PLAN.md +57 -6
  4. package/dist/config/VALIDATE.md +139 -0
  5. package/dist/handlers/answer.js +13 -20
  6. package/dist/handlers/command.js +26 -30
  7. package/dist/handlers/config.js +32 -24
  8. package/dist/handlers/execute.js +46 -0
  9. package/dist/handlers/execution.js +133 -81
  10. package/dist/handlers/introspect.js +13 -20
  11. package/dist/handlers/plan.js +31 -34
  12. package/dist/services/anthropic.js +28 -2
  13. package/dist/services/colors.js +3 -3
  14. package/dist/services/components.js +50 -1
  15. package/dist/services/config-loader.js +67 -0
  16. package/dist/services/execution-validator.js +110 -0
  17. package/dist/services/messages.js +1 -0
  18. package/dist/services/placeholder-resolver.js +120 -0
  19. package/dist/services/shell.js +118 -0
  20. package/dist/services/skill-expander.js +91 -0
  21. package/dist/services/skill-parser.js +169 -0
  22. package/dist/services/skills.js +26 -0
  23. package/dist/services/timing.js +38 -0
  24. package/dist/services/tool-registry.js +10 -0
  25. package/dist/services/utils.js +21 -0
  26. package/dist/tools/execute.tool.js +44 -0
  27. package/dist/tools/validate.tool.js +43 -0
  28. package/dist/types/handlers.js +1 -0
  29. package/dist/types/skills.js +4 -0
  30. package/dist/types/types.js +2 -0
  31. package/dist/ui/Answer.js +3 -9
  32. package/dist/ui/Command.js +3 -6
  33. package/dist/ui/Component.js +13 -1
  34. package/dist/ui/Config.js +2 -2
  35. package/dist/ui/Confirm.js +2 -2
  36. package/dist/ui/Execute.js +262 -0
  37. package/dist/ui/Introspect.js +5 -7
  38. package/dist/ui/Main.js +30 -69
  39. package/dist/ui/Spinner.js +10 -5
  40. package/dist/ui/Validate.js +120 -0
  41. package/package.json +7 -7
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import YAML from 'yaml';
5
+ /**
6
+ * Load user config from ~/.plsrc
7
+ */
8
+ export function loadUserConfig() {
9
+ const configPath = join(homedir(), '.plsrc');
10
+ if (!existsSync(configPath)) {
11
+ return {};
12
+ }
13
+ try {
14
+ const content = readFileSync(configPath, 'utf-8');
15
+ const parsed = YAML.parse(content);
16
+ if (parsed && typeof parsed === 'object') {
17
+ return parsed;
18
+ }
19
+ return {};
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ /**
26
+ * Check if config has a specific path
27
+ */
28
+ export function hasConfigPath(config, path) {
29
+ const parts = path.split('.');
30
+ let current = config;
31
+ for (const part of parts) {
32
+ if (current === null || current === undefined) {
33
+ return false;
34
+ }
35
+ if (typeof current !== 'object') {
36
+ return false;
37
+ }
38
+ current = current[part];
39
+ }
40
+ return (current !== null &&
41
+ current !== undefined &&
42
+ (typeof current === 'string' ||
43
+ typeof current === 'boolean' ||
44
+ typeof current === 'number'));
45
+ }
46
+ /**
47
+ * Get config value at path
48
+ */
49
+ export function getConfigValue(config, path) {
50
+ const parts = path.split('.');
51
+ let current = config;
52
+ for (const part of parts) {
53
+ if (current === null || current === undefined) {
54
+ return undefined;
55
+ }
56
+ if (typeof current !== 'object') {
57
+ return undefined;
58
+ }
59
+ current = current[part];
60
+ }
61
+ if (typeof current === 'string' ||
62
+ typeof current === 'boolean' ||
63
+ typeof current === 'number') {
64
+ return current;
65
+ }
66
+ return undefined;
67
+ }
@@ -0,0 +1,110 @@
1
+ import { extractPlaceholders, pathToString, resolveVariant, } from './placeholder-resolver.js';
2
+ import { loadUserConfig, hasConfigPath } from './config-loader.js';
3
+ import { loadSkills } from './skills.js';
4
+ import { expandSkillReferences } from './skill-expander.js';
5
+ import { getConfigType, parseSkillMarkdown } from './skill-parser.js';
6
+ /**
7
+ * Validate config requirements for execute tasks
8
+ * Returns missing config requirements
9
+ */
10
+ export function validateExecuteTasks(tasks) {
11
+ const userConfig = loadUserConfig();
12
+ const missing = [];
13
+ const seenPaths = new Set();
14
+ // Load and parse all skills for validation
15
+ const skillContents = loadSkills();
16
+ const parsedSkills = skillContents
17
+ .map((content) => parseSkillMarkdown(content))
18
+ .filter((s) => s !== null);
19
+ const skillLookup = (name) => parsedSkills.find((s) => s.name === name) || null;
20
+ for (const task of tasks) {
21
+ // Check if task originates from a skill
22
+ const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
23
+ if (skillName) {
24
+ // Task comes from a skill - check skill's Execution section
25
+ const skill = skillLookup(skillName);
26
+ if (!skill || !skill.execution) {
27
+ continue;
28
+ }
29
+ // Get variant from task params (if any)
30
+ // Try params.variant first, then look for other param keys that might be the variant
31
+ let variant = null;
32
+ if (typeof task.params?.variant === 'string') {
33
+ variant = task.params.variant.toLowerCase();
34
+ }
35
+ else if (task.params && typeof task.params === 'object') {
36
+ // Look for other params that could be the variant (e.g., product, target, option, etc.)
37
+ // Exclude known non-variant params
38
+ const excludeKeys = new Set(['skill', 'type']);
39
+ for (const [key, value] of Object.entries(task.params)) {
40
+ if (!excludeKeys.has(key) && typeof value === 'string') {
41
+ variant = value.toLowerCase();
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ // Expand skill references to get actual commands
47
+ const expanded = expandSkillReferences(skill.execution, skillLookup);
48
+ // Extract placeholders from actual commands
49
+ for (const line of expanded) {
50
+ const placeholders = extractPlaceholders(line);
51
+ for (const placeholder of placeholders) {
52
+ let resolvedPath;
53
+ if (placeholder.hasVariant) {
54
+ // Variant placeholder - resolve with variant from params
55
+ if (!variant) {
56
+ // No variant provided - skip this placeholder
57
+ continue;
58
+ }
59
+ const resolvedPathArray = resolveVariant(placeholder.path, variant);
60
+ resolvedPath = pathToString(resolvedPathArray);
61
+ }
62
+ else {
63
+ // Strict placeholder - use as-is
64
+ resolvedPath = pathToString(placeholder.path);
65
+ }
66
+ // Skip if already processed
67
+ if (seenPaths.has(resolvedPath)) {
68
+ continue;
69
+ }
70
+ seenPaths.add(resolvedPath);
71
+ // Check if config exists
72
+ if (!hasConfigPath(userConfig, resolvedPath)) {
73
+ // Get type from skill config
74
+ const type = skill.config
75
+ ? getConfigType(skill.config, resolvedPath)
76
+ : undefined;
77
+ missing.push({
78
+ path: resolvedPath,
79
+ type: type || 'string',
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ // Task doesn't come from a skill - check task action for placeholders
87
+ const placeholders = extractPlaceholders(task.action);
88
+ for (const placeholder of placeholders) {
89
+ // Skip variant placeholders - they should have been resolved during planning
90
+ if (placeholder.hasVariant) {
91
+ continue;
92
+ }
93
+ const path = placeholder.path.join('.');
94
+ // Skip if already processed
95
+ if (seenPaths.has(path)) {
96
+ continue;
97
+ }
98
+ seenPaths.add(path);
99
+ // Check if config exists
100
+ if (!hasConfigPath(userConfig, path)) {
101
+ missing.push({
102
+ path,
103
+ type: 'string', // Default to string for now
104
+ });
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return missing;
110
+ }
@@ -1,4 +1,5 @@
1
1
  import { loadDebugSetting } from './configuration.js';
2
+ export { formatDuration } from './utils.js';
2
3
  /**
3
4
  * Returns a natural language confirmation message for plan execution.
4
5
  * Randomly selects from variations to sound less robotic.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Check if a string is all uppercase (variant placeholder indicator)
3
+ */
4
+ function isUpperCase(str) {
5
+ return str === str.toUpperCase() && str !== str.toLowerCase();
6
+ }
7
+ /**
8
+ * Parse placeholder from string
9
+ * Returns placeholder info or null if no valid placeholder found
10
+ */
11
+ export function parsePlaceholder(text) {
12
+ // Match {path.to.property} format
13
+ const match = text.match(/\{([^}]+)\}/);
14
+ if (!match) {
15
+ return null;
16
+ }
17
+ const original = match[0];
18
+ const pathString = match[1];
19
+ const path = pathString.split('.');
20
+ // Check if any path component is uppercase (variant placeholder)
21
+ const variantIndex = path.findIndex((part) => isUpperCase(part));
22
+ const hasVariant = variantIndex !== -1;
23
+ return {
24
+ original,
25
+ path,
26
+ hasVariant,
27
+ variantIndex: hasVariant ? variantIndex : undefined,
28
+ };
29
+ }
30
+ /**
31
+ * Extract all placeholders from text
32
+ */
33
+ export function extractPlaceholders(text) {
34
+ const placeholders = [];
35
+ const regex = /\{([^}]+)\}/g;
36
+ let match;
37
+ while ((match = regex.exec(text)) !== null) {
38
+ const pathString = match[1];
39
+ const path = pathString.split('.');
40
+ const variantIndex = path.findIndex((part) => isUpperCase(part));
41
+ const hasVariant = variantIndex !== -1;
42
+ placeholders.push({
43
+ original: match[0],
44
+ path,
45
+ hasVariant,
46
+ variantIndex: hasVariant ? variantIndex : undefined,
47
+ });
48
+ }
49
+ return placeholders;
50
+ }
51
+ /**
52
+ * Replace uppercase component in path with actual variant name
53
+ * Returns new path with variant replaced
54
+ */
55
+ export function resolveVariant(path, variantName) {
56
+ return path.map((part) => (isUpperCase(part) ? variantName : part));
57
+ }
58
+ /**
59
+ * Convert path array to dot notation string
60
+ */
61
+ export function pathToString(path) {
62
+ return path.join('.');
63
+ }
64
+ /**
65
+ * Resolve placeholder value from config
66
+ * Returns the value at the specified path or undefined if not found
67
+ */
68
+ export function resolveFromConfig(config, path) {
69
+ let current = config;
70
+ for (const part of path) {
71
+ if (current === null || current === undefined) {
72
+ return undefined;
73
+ }
74
+ if (typeof current !== 'object') {
75
+ return undefined;
76
+ }
77
+ current = current[part];
78
+ }
79
+ if (typeof current === 'string' ||
80
+ typeof current === 'boolean' ||
81
+ typeof current === 'number') {
82
+ return current;
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Replace all placeholders in text with values from config
88
+ * Note: Variant placeholders (with uppercase components) must be resolved first
89
+ */
90
+ export function replacePlaceholders(text, config) {
91
+ return text.replace(/\{([^}]+)\}/g, (_match, pathString) => {
92
+ const path = pathString.split('.');
93
+ const value = resolveFromConfig(config, path);
94
+ if (value === undefined) {
95
+ // Keep placeholder if not found in config
96
+ return `{${pathString}}`;
97
+ }
98
+ return String(value);
99
+ });
100
+ }
101
+ /**
102
+ * Check if text contains any placeholders
103
+ */
104
+ export function hasPlaceholders(text) {
105
+ return /\{[^}]+\}/.test(text);
106
+ }
107
+ /**
108
+ * Get all unique config paths required by placeholders in text
109
+ * Note: Variant placeholders (with uppercase components) must be resolved first
110
+ */
111
+ export function getRequiredConfigPaths(text) {
112
+ const placeholders = extractPlaceholders(text);
113
+ const paths = new Set();
114
+ for (const placeholder of placeholders) {
115
+ if (!placeholder.hasVariant) {
116
+ paths.add(pathToString(placeholder.path));
117
+ }
118
+ }
119
+ return Array.from(paths);
120
+ }
@@ -0,0 +1,118 @@
1
+ export var ExecutionStatus;
2
+ (function (ExecutionStatus) {
3
+ ExecutionStatus["Pending"] = "pending";
4
+ ExecutionStatus["Running"] = "running";
5
+ ExecutionStatus["Success"] = "success";
6
+ ExecutionStatus["Failed"] = "failed";
7
+ ExecutionStatus["Aborted"] = "aborted";
8
+ })(ExecutionStatus || (ExecutionStatus = {}));
9
+ export var ExecutionResult;
10
+ (function (ExecutionResult) {
11
+ ExecutionResult["Success"] = "success";
12
+ ExecutionResult["Error"] = "error";
13
+ ExecutionResult["Aborted"] = "aborted";
14
+ })(ExecutionResult || (ExecutionResult = {}));
15
+ const DEFAULT_DELAY_GENERATOR = (index) => (Math.pow(3, index + 1) * Math.max(Math.random(), Math.random()) + 1) * 1000;
16
+ /**
17
+ * Dummy executor that simulates command execution with configurable delays.
18
+ * Supports mocked responses for testing different scenarios.
19
+ */
20
+ export class DummyExecutor {
21
+ mockedResponses = new Map();
22
+ delayGenerator;
23
+ constructor(delayGenerator = DEFAULT_DELAY_GENERATOR) {
24
+ this.delayGenerator = delayGenerator;
25
+ }
26
+ /**
27
+ * Set a mocked response for a specific command
28
+ */
29
+ mock(command, response) {
30
+ this.mockedResponses.set(command, response);
31
+ }
32
+ /**
33
+ * Clear all mocked responses
34
+ */
35
+ clearMocks() {
36
+ this.mockedResponses.clear();
37
+ }
38
+ execute(cmd, onProgress, index = 0) {
39
+ return new Promise((resolve) => {
40
+ onProgress?.(ExecutionStatus.Running);
41
+ const delay = this.delayGenerator(index);
42
+ setTimeout(() => {
43
+ const mocked = this.mockedResponses.get(cmd.command);
44
+ const commandResult = {
45
+ description: cmd.description,
46
+ command: cmd.command,
47
+ output: mocked?.output ?? '',
48
+ errors: mocked?.errors ?? '',
49
+ result: mocked?.result ?? ExecutionResult.Success,
50
+ error: mocked?.error,
51
+ };
52
+ onProgress?.(commandResult.result === ExecutionResult.Success
53
+ ? ExecutionStatus.Success
54
+ : ExecutionStatus.Failed);
55
+ resolve(commandResult);
56
+ }, delay);
57
+ });
58
+ }
59
+ }
60
+ /**
61
+ * Default executor uses DummyExecutor for development and testing.
62
+ * To implement real shell execution, create a RealExecutor class that:
63
+ * - Spawns process with cmd.command in shell mode using child_process.spawn()
64
+ * - Sets working directory from cmd.workdir
65
+ * - Handles cmd.timeout for command timeout
66
+ * - Captures stdout and stderr streams
67
+ * - Calls onProgress with Running/Success/Failed status
68
+ * - Returns CommandOutput with actual stdout, stderr, exitCode
69
+ * - Handles errors (spawn failures, timeouts, non-zero exit codes)
70
+ */
71
+ const executor = new DummyExecutor();
72
+ /**
73
+ * Execute a single shell command
74
+ */
75
+ export function executeCommand(cmd, onProgress, index = 0) {
76
+ return executor.execute(cmd, onProgress, index);
77
+ }
78
+ /**
79
+ * Execute multiple commands sequentially
80
+ */
81
+ export async function executeCommands(commands, onProgress) {
82
+ const results = [];
83
+ for (let i = 0; i < commands.length; i++) {
84
+ const cmd = commands[i];
85
+ onProgress?.({
86
+ currentIndex: i,
87
+ total: commands.length,
88
+ command: cmd,
89
+ status: ExecutionStatus.Running,
90
+ });
91
+ const output = await executeCommand(cmd, (status) => {
92
+ onProgress?.({
93
+ currentIndex: i,
94
+ total: commands.length,
95
+ command: cmd,
96
+ status,
97
+ output: status !== ExecutionStatus.Running ? results[i] : undefined,
98
+ });
99
+ }, i);
100
+ results.push(output);
101
+ // Update with final status
102
+ onProgress?.({
103
+ currentIndex: i,
104
+ total: commands.length,
105
+ command: cmd,
106
+ status: output.result === ExecutionResult.Success
107
+ ? ExecutionStatus.Success
108
+ : ExecutionStatus.Failed,
109
+ output,
110
+ });
111
+ // Stop if critical command failed
112
+ const isCritical = cmd.critical !== false;
113
+ if (output.result !== ExecutionResult.Success && isCritical) {
114
+ break;
115
+ }
116
+ }
117
+ return results;
118
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Parse skill reference from execution line
3
+ * Returns skill name if line is a reference, otherwise null
4
+ */
5
+ export function parseSkillReference(line) {
6
+ const match = line.match(/^\[(.+)\]$/);
7
+ return match ? match[1].trim() : null;
8
+ }
9
+ /**
10
+ * Check if execution line is a skill reference
11
+ */
12
+ export function isSkillReference(line) {
13
+ return /^\[.+\]$/.test(line.trim());
14
+ }
15
+ /**
16
+ * Expand skill references in execution commands
17
+ * Returns expanded execution lines with references replaced
18
+ * Throws error if circular reference detected
19
+ */
20
+ export function expandSkillReferences(execution, skillLookup, visited = new Set()) {
21
+ const expanded = [];
22
+ for (const line of execution) {
23
+ const skillName = parseSkillReference(line);
24
+ if (!skillName) {
25
+ // Not a reference, keep as-is
26
+ expanded.push(line);
27
+ continue;
28
+ }
29
+ // Check for circular reference
30
+ if (visited.has(skillName)) {
31
+ throw new Error(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`);
32
+ }
33
+ // Look up referenced skill
34
+ const skill = skillLookup(skillName);
35
+ if (!skill) {
36
+ // Referenced skill not found, keep as-is
37
+ expanded.push(line);
38
+ continue;
39
+ }
40
+ if (!skill.execution || skill.execution.length === 0) {
41
+ // Referenced skill has no execution, skip
42
+ continue;
43
+ }
44
+ // Recursively expand referenced skill's execution
45
+ const newVisited = new Set(visited);
46
+ newVisited.add(skillName);
47
+ const referencedExecution = expandSkillReferences(skill.execution, skillLookup, newVisited);
48
+ expanded.push(...referencedExecution);
49
+ }
50
+ return expanded;
51
+ }
52
+ /**
53
+ * Get all skill names referenced in execution (including nested)
54
+ * Returns unique set of skill names
55
+ */
56
+ export function getReferencedSkills(execution, skillLookup, visited = new Set()) {
57
+ const referenced = new Set();
58
+ for (const line of execution) {
59
+ const skillName = parseSkillReference(line);
60
+ if (!skillName || visited.has(skillName)) {
61
+ continue;
62
+ }
63
+ referenced.add(skillName);
64
+ const skill = skillLookup(skillName);
65
+ if (skill && skill.execution) {
66
+ const newVisited = new Set(visited);
67
+ newVisited.add(skillName);
68
+ const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
69
+ for (const name of nested) {
70
+ referenced.add(name);
71
+ }
72
+ }
73
+ }
74
+ return referenced;
75
+ }
76
+ /**
77
+ * Validate skill references don't form cycles
78
+ * Returns true if valid, false if circular reference detected
79
+ */
80
+ export function validateNoCycles(execution, skillLookup, visited = new Set()) {
81
+ try {
82
+ expandSkillReferences(execution, skillLookup, visited);
83
+ return true;
84
+ }
85
+ catch (error) {
86
+ if (error instanceof Error && error.message.includes('Circular')) {
87
+ return false;
88
+ }
89
+ throw error;
90
+ }
91
+ }
@@ -0,0 +1,169 @@
1
+ import YAML from 'yaml';
2
+ /**
3
+ * Parse a skill markdown file into structured definition
4
+ */
5
+ export function parseSkillMarkdown(content) {
6
+ const sections = extractSections(content);
7
+ // Name is required
8
+ if (!sections.name) {
9
+ return null;
10
+ }
11
+ // Description is required
12
+ if (!sections.description) {
13
+ return null;
14
+ }
15
+ // Steps are required
16
+ if (!sections.steps || sections.steps.length === 0) {
17
+ return null;
18
+ }
19
+ // Validate execution and steps have same count (if execution exists)
20
+ if (sections.execution &&
21
+ sections.execution.length !== sections.steps.length) {
22
+ return null;
23
+ }
24
+ const skill = {
25
+ name: sections.name,
26
+ description: sections.description,
27
+ steps: sections.steps,
28
+ };
29
+ if (sections.aliases && sections.aliases.length > 0) {
30
+ skill.aliases = sections.aliases;
31
+ }
32
+ if (sections.config) {
33
+ skill.config = sections.config;
34
+ }
35
+ if (sections.execution && sections.execution.length > 0) {
36
+ skill.execution = sections.execution;
37
+ }
38
+ return skill;
39
+ }
40
+ /**
41
+ * Extract sections from markdown content
42
+ */
43
+ function extractSections(content) {
44
+ const lines = content.split('\n');
45
+ const sections = {};
46
+ let currentSection = null;
47
+ let sectionLines = [];
48
+ for (const line of lines) {
49
+ // Check for section headers (### SectionName)
50
+ const headerMatch = line.match(/^###\s+(.+)$/);
51
+ if (headerMatch) {
52
+ // Process previous section
53
+ if (currentSection) {
54
+ processSectionContent(currentSection, sectionLines, sections);
55
+ }
56
+ // Start new section
57
+ currentSection = headerMatch[1].trim().toLowerCase();
58
+ sectionLines = [];
59
+ }
60
+ else if (currentSection) {
61
+ // Accumulate lines for current section
62
+ sectionLines.push(line);
63
+ }
64
+ }
65
+ // Process final section
66
+ if (currentSection) {
67
+ processSectionContent(currentSection, sectionLines, sections);
68
+ }
69
+ return sections;
70
+ }
71
+ /**
72
+ * Process accumulated section content
73
+ */
74
+ function processSectionContent(sectionName, lines, sections) {
75
+ const content = lines.join('\n').trim();
76
+ if (!content) {
77
+ return;
78
+ }
79
+ switch (sectionName) {
80
+ case 'name':
81
+ sections.name = content;
82
+ break;
83
+ case 'description':
84
+ sections.description = content;
85
+ break;
86
+ case 'aliases':
87
+ sections.aliases = extractBulletList(content);
88
+ break;
89
+ case 'config':
90
+ sections.config = parseConfigSchema(content);
91
+ break;
92
+ case 'steps':
93
+ sections.steps = extractBulletList(content);
94
+ break;
95
+ case 'execution':
96
+ sections.execution = extractBulletList(content);
97
+ break;
98
+ }
99
+ }
100
+ /**
101
+ * Extract bullet list items from content
102
+ */
103
+ function extractBulletList(content) {
104
+ const lines = content.split('\n');
105
+ const items = [];
106
+ for (const line of lines) {
107
+ const trimmed = line.trim();
108
+ // Match bullet points: "- text" or "* text"
109
+ const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
110
+ if (bulletMatch) {
111
+ items.push(bulletMatch[1].trim());
112
+ }
113
+ }
114
+ return items;
115
+ }
116
+ /**
117
+ * Parse YAML config schema
118
+ */
119
+ function parseConfigSchema(content) {
120
+ try {
121
+ const parsed = YAML.parse(content);
122
+ if (!parsed || typeof parsed !== 'object') {
123
+ return undefined;
124
+ }
125
+ return parsed;
126
+ }
127
+ catch {
128
+ return undefined;
129
+ }
130
+ }
131
+ /**
132
+ * Generate all config paths from schema
133
+ */
134
+ export function generateConfigPaths(schema, prefix = '') {
135
+ const paths = [];
136
+ for (const [key, value] of Object.entries(schema)) {
137
+ const fullKey = prefix ? `${prefix}.${key}` : key;
138
+ if (typeof value === 'string') {
139
+ // Leaf node with type annotation
140
+ paths.push(fullKey);
141
+ }
142
+ else if (typeof value === 'object') {
143
+ // Nested object - recurse
144
+ paths.push(...generateConfigPaths(value, fullKey));
145
+ }
146
+ }
147
+ return paths;
148
+ }
149
+ /**
150
+ * Get config type for a specific path
151
+ */
152
+ export function getConfigType(schema, path) {
153
+ const parts = path.split('.');
154
+ let current = schema;
155
+ for (const part of parts) {
156
+ if (typeof current === 'string') {
157
+ return undefined;
158
+ }
159
+ if (typeof current !== 'object') {
160
+ return undefined;
161
+ }
162
+ current = current[part];
163
+ }
164
+ if (typeof current === 'string' &&
165
+ (current === 'string' || current === 'boolean' || current === 'number')) {
166
+ return current;
167
+ }
168
+ return undefined;
169
+ }