@claudiv/cli 0.1.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.
- package/LICENSE +21 -0
- package/README.md +430 -0
- package/bin/claudiv.js +407 -0
- package/dist/claude-api.d.ts +20 -0
- package/dist/claude-api.js +117 -0
- package/dist/claude-cli.d.ts +18 -0
- package/dist/claude-cli.js +124 -0
- package/dist/claude-client.d.ts +16 -0
- package/dist/claude-client.js +44 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +67 -0
- package/dist/dev-server.d.ts +10 -0
- package/dist/dev-server.js +118 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +305 -0
- package/dist/updater.d.ts +29 -0
- package/dist/updater.js +79 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +36 -0
- package/dist/watcher.d.ts +22 -0
- package/dist/watcher.js +66 -0
- package/package.json +69 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Claude client interface (abstracts CLI vs API mode)
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, HierarchyContext } from '@claudiv/core';
|
|
5
|
+
export interface ClaudeClient {
|
|
6
|
+
sendPrompt(userMessage: string, context: HierarchyContext): AsyncGenerator<string>;
|
|
7
|
+
checkAvailable(): Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create Claude client based on configuration
|
|
11
|
+
*/
|
|
12
|
+
export declare function createClaudeClient(config: Config): ClaudeClient;
|
|
13
|
+
/**
|
|
14
|
+
* Verify Claude is available before starting
|
|
15
|
+
*/
|
|
16
|
+
export declare function verifyClaudeAvailable(client: ClaudeClient, mode: 'cli' | 'api'): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Claude client interface (abstracts CLI vs API mode)
|
|
3
|
+
*/
|
|
4
|
+
import { ClaudeCLIClient } from './claude-cli.js';
|
|
5
|
+
import { ClaudeAPIClient } from './claude-api.js';
|
|
6
|
+
import { logger } from './utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create Claude client based on configuration
|
|
9
|
+
*/
|
|
10
|
+
export function createClaudeClient(config) {
|
|
11
|
+
if (config.mode === 'cli') {
|
|
12
|
+
logger.info('Using Claude Code CLI (subscription mode)');
|
|
13
|
+
return new ClaudeCLIClient();
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
logger.info('Using Anthropic API (pay-per-use mode)');
|
|
17
|
+
if (!config.apiKey) {
|
|
18
|
+
logger.error('API key is required for API mode');
|
|
19
|
+
throw new Error('Missing API key');
|
|
20
|
+
}
|
|
21
|
+
return new ClaudeAPIClient(config.apiKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Verify Claude is available before starting
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyClaudeAvailable(client, mode) {
|
|
28
|
+
logger.processing(`Checking ${mode === 'cli' ? 'Claude Code CLI' : 'Claude API'} availability...`);
|
|
29
|
+
const isAvailable = await client.checkAvailable();
|
|
30
|
+
if (!isAvailable) {
|
|
31
|
+
if (mode === 'cli') {
|
|
32
|
+
logger.error('Claude Code CLI is not installed or not working');
|
|
33
|
+
logger.info('Install Claude Code: https://code.claude.com');
|
|
34
|
+
logger.info('Or switch to API mode by setting MODE=api in .env');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
logger.error('Claude API is not available');
|
|
38
|
+
logger.info('Check your ANTHROPIC_API_KEY in .env');
|
|
39
|
+
logger.info('Or switch to CLI mode by setting MODE=cli in .env');
|
|
40
|
+
}
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
logger.success(`${mode === 'cli' ? 'Claude Code CLI' : 'Claude API'} is ready`);
|
|
44
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader for GUI-driven spec editor
|
|
3
|
+
*/
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { existsSync, readdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { logger } from './utils/logger.js';
|
|
8
|
+
// Load .env file
|
|
9
|
+
dotenv.config();
|
|
10
|
+
/**
|
|
11
|
+
* Load and validate configuration
|
|
12
|
+
*/
|
|
13
|
+
export function loadConfig() {
|
|
14
|
+
// Determine mode
|
|
15
|
+
const mode = (process.env.MODE?.toLowerCase() || 'cli');
|
|
16
|
+
if (mode !== 'cli' && mode !== 'api') {
|
|
17
|
+
logger.error(`Invalid MODE: ${process.env.MODE}. Must be 'cli' or 'api'`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
// Get API key if in API mode
|
|
21
|
+
let apiKey;
|
|
22
|
+
if (mode === 'api') {
|
|
23
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
logger.error('ANTHROPIC_API_KEY is required when MODE=api');
|
|
26
|
+
logger.info('Set ANTHROPIC_API_KEY in .env file or environment');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Spec file location - look for .cdml files
|
|
31
|
+
// Check if user specified a file via CLI argument
|
|
32
|
+
const cliFile = process.argv[2];
|
|
33
|
+
let specFile;
|
|
34
|
+
if (cliFile) {
|
|
35
|
+
specFile = join(process.cwd(), cliFile);
|
|
36
|
+
if (!existsSync(specFile)) {
|
|
37
|
+
logger.error(`File not found: ${cliFile}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Look for .cdml files in current directory
|
|
43
|
+
const files = readdirSync(process.cwd());
|
|
44
|
+
const cdmlFiles = files.filter(f => f.endsWith('.cdml'));
|
|
45
|
+
if (cdmlFiles.length === 0) {
|
|
46
|
+
logger.error('No .cdml files found in current directory');
|
|
47
|
+
logger.info('Create a .cdml file to get started (e.g., app.cdml)');
|
|
48
|
+
logger.info('Example: <app><button gen>Make a blue button</button></app>');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if (cdmlFiles.length > 1) {
|
|
52
|
+
logger.warn(`Multiple .cdml files found: ${cdmlFiles.join(', ')}`);
|
|
53
|
+
logger.info(`Using ${cdmlFiles[0]} (specify file as argument to use a different one)`);
|
|
54
|
+
}
|
|
55
|
+
specFile = join(process.cwd(), cdmlFiles[0]);
|
|
56
|
+
}
|
|
57
|
+
// Configuration values
|
|
58
|
+
const config = {
|
|
59
|
+
mode,
|
|
60
|
+
apiKey,
|
|
61
|
+
specFile,
|
|
62
|
+
debounceMs: 300,
|
|
63
|
+
claudeTimeout: 60000, // 60 seconds
|
|
64
|
+
};
|
|
65
|
+
logger.debug(`Configuration loaded: mode=${mode}, specFile=${specFile}`);
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite dev server for hosting generated code
|
|
3
|
+
*/
|
|
4
|
+
import { createServer } from 'vite';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { logger } from './utils/logger.js';
|
|
7
|
+
export class DevServer {
|
|
8
|
+
server = null;
|
|
9
|
+
port = 30004;
|
|
10
|
+
async start() {
|
|
11
|
+
try {
|
|
12
|
+
// Create Vite server
|
|
13
|
+
this.server = await createServer({
|
|
14
|
+
root: process.cwd(),
|
|
15
|
+
server: {
|
|
16
|
+
port: this.port,
|
|
17
|
+
strictPort: false, // Auto-increment if port is busy
|
|
18
|
+
open: true, // Open browser automatically
|
|
19
|
+
},
|
|
20
|
+
plugins: [
|
|
21
|
+
{
|
|
22
|
+
name: 'spec-code-server',
|
|
23
|
+
configureServer(server) {
|
|
24
|
+
server.middlewares.use(async (req, res, next) => {
|
|
25
|
+
// Serve generated code as the root
|
|
26
|
+
const outputFile = 'app.html'; // TODO: Make configurable
|
|
27
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
28
|
+
if (existsSync(outputFile)) {
|
|
29
|
+
// Read and serve the file directly
|
|
30
|
+
const { readFile } = await import('fs/promises');
|
|
31
|
+
const content = await readFile(outputFile, 'utf-8');
|
|
32
|
+
res.statusCode = 200;
|
|
33
|
+
res.setHeader('Content-Type', 'text/html');
|
|
34
|
+
res.end(content);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Serve a placeholder if output file doesn't exist yet
|
|
39
|
+
res.statusCode = 200;
|
|
40
|
+
res.setHeader('Content-Type', 'text/html');
|
|
41
|
+
res.end(`
|
|
42
|
+
<!DOCTYPE html>
|
|
43
|
+
<html>
|
|
44
|
+
<head>
|
|
45
|
+
<title>Waiting for generated code</title>
|
|
46
|
+
<meta http-equiv="refresh" content="2">
|
|
47
|
+
<style>
|
|
48
|
+
body {
|
|
49
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
height: 100vh;
|
|
54
|
+
margin: 0;
|
|
55
|
+
background: #f5f5f5;
|
|
56
|
+
}
|
|
57
|
+
.message {
|
|
58
|
+
text-align: center;
|
|
59
|
+
padding: 2rem;
|
|
60
|
+
background: white;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
63
|
+
}
|
|
64
|
+
.spinner {
|
|
65
|
+
border: 3px solid #f3f3f3;
|
|
66
|
+
border-top: 3px solid #3498db;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
width: 40px;
|
|
69
|
+
height: 40px;
|
|
70
|
+
animation: spin 1s linear infinite;
|
|
71
|
+
margin: 1rem auto;
|
|
72
|
+
}
|
|
73
|
+
@keyframes spin {
|
|
74
|
+
0% { transform: rotate(0deg); }
|
|
75
|
+
100% { transform: rotate(360deg); }
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<div class="message">
|
|
81
|
+
<div class="spinner"></div>
|
|
82
|
+
<h2>Waiting for code generation...</h2>
|
|
83
|
+
<p>Add a <code>gen</code> attribute to elements in your .cdml file to trigger generation.</p>
|
|
84
|
+
</div>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
87
|
+
`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
next();
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
await this.server.listen();
|
|
98
|
+
const info = this.server.config.logger.info;
|
|
99
|
+
this.port = this.server.config.server.port || this.port;
|
|
100
|
+
logger.success(`🌐 Dev server running at http://localhost:${this.port}`);
|
|
101
|
+
logger.info('💡 Browser will auto-reload when code is regenerated');
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const err = error;
|
|
105
|
+
logger.error(`Failed to start dev server: ${err.message}`);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async stop() {
|
|
110
|
+
if (this.server) {
|
|
111
|
+
await this.server.close();
|
|
112
|
+
logger.info('Dev server stopped');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
getPort() {
|
|
116
|
+
return this.port;
|
|
117
|
+
}
|
|
118
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main orchestrator for GUI-driven spec editor
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, writeFile, chmod } from 'fs/promises';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { logger } from './utils/logger.js';
|
|
8
|
+
import { SpecFileWatcher } from './watcher.js';
|
|
9
|
+
import { parseSpecFile, generateCode, extractCodeBlocks } from '@claudiv/core';
|
|
10
|
+
import { createClaudeClient, verifyClaudeAvailable } from './claude-client.js';
|
|
11
|
+
import { updateSpecWithResponse } from './updater.js';
|
|
12
|
+
import { DevServer } from './dev-server.js';
|
|
13
|
+
/**
|
|
14
|
+
* Main application entry point
|
|
15
|
+
*/
|
|
16
|
+
async function main() {
|
|
17
|
+
logger.info('🚀 GUI-driven spec editor starting...');
|
|
18
|
+
// Load configuration
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
// Create Claude client
|
|
21
|
+
const claudeClient = createClaudeClient(config);
|
|
22
|
+
// Verify Claude is available
|
|
23
|
+
await verifyClaudeAvailable(claudeClient, config.mode);
|
|
24
|
+
// TODO: Implement regeneration for multi-file .cdml architecture
|
|
25
|
+
// (spec.cdml, rules.cdml, models.cdml, tests.cdml)
|
|
26
|
+
// For now, generation happens on-demand through chat patterns
|
|
27
|
+
// Create file watcher
|
|
28
|
+
const watcher = new SpecFileWatcher(config);
|
|
29
|
+
// Handle file changes
|
|
30
|
+
watcher.on('change', async (filePath) => {
|
|
31
|
+
try {
|
|
32
|
+
await processSpecFile(filePath, config, claudeClient, watcher);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const err = error;
|
|
36
|
+
logger.error(`Error processing ${filePath}: ${err.message}`);
|
|
37
|
+
if (process.env.DEBUG) {
|
|
38
|
+
console.error(err.stack);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Start watching
|
|
43
|
+
watcher.start();
|
|
44
|
+
logger.success(`✓ Watching ${config.specFile.split('/').pop()} for changes...`);
|
|
45
|
+
logger.info('💡 Tip: Add gen/retry/undo attribute to any element to trigger AI');
|
|
46
|
+
logger.info('💡 Example: <create-button gen>Make a blue button</create-button>');
|
|
47
|
+
logger.info('💡 Example: <my-button color="blue" size="large" gen />');
|
|
48
|
+
logger.info('');
|
|
49
|
+
// Start Vite dev server
|
|
50
|
+
const devServer = new DevServer();
|
|
51
|
+
await devServer.start();
|
|
52
|
+
logger.info('');
|
|
53
|
+
// Handle graceful shutdown
|
|
54
|
+
const shutdown = async () => {
|
|
55
|
+
logger.info('\n👋 Shutting down...');
|
|
56
|
+
watcher.stop();
|
|
57
|
+
await devServer.stop();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
};
|
|
60
|
+
process.on('SIGINT', shutdown);
|
|
61
|
+
process.on('SIGTERM', shutdown);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Process .cdml file for chat patterns
|
|
65
|
+
*/
|
|
66
|
+
async function processSpecFile(filePath, config, claudeClient, watcher) {
|
|
67
|
+
// Skip if we're updating the file ourselves
|
|
68
|
+
if (watcher.isCurrentlyUpdating()) {
|
|
69
|
+
logger.debug('Skipping processing (internal update in progress)');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
logger.processing(`Processing ${filePath.split('/').pop()}...`);
|
|
73
|
+
// Read file content
|
|
74
|
+
const content = await readFile(filePath, 'utf-8');
|
|
75
|
+
// Parse spec file
|
|
76
|
+
const parsed = parseSpecFile(content);
|
|
77
|
+
if (parsed.chatPatterns.length === 0) {
|
|
78
|
+
logger.debug('No new chat patterns found');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
logger.info(`Found ${parsed.chatPatterns.length} chat pattern(s) to process`);
|
|
82
|
+
// Process each chat pattern
|
|
83
|
+
for (const pattern of parsed.chatPatterns) {
|
|
84
|
+
await processChatPattern(pattern, config, claudeClient, watcher, parsed.dom);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Process a single chat pattern
|
|
89
|
+
*/
|
|
90
|
+
async function processChatPattern(pattern, config, claudeClient, watcher, $) {
|
|
91
|
+
const { action, element, elementName, specAttributes, userMessage, context, elementPath } = pattern;
|
|
92
|
+
// Build display message
|
|
93
|
+
const attrPreview = Object.entries(specAttributes).length > 0
|
|
94
|
+
? ` [${Object.entries(specAttributes).slice(0, 2).map(([k, v]) => `${k}="${v}"`).join(', ')}...]`
|
|
95
|
+
: '';
|
|
96
|
+
const messagePreview = userMessage
|
|
97
|
+
? userMessage.substring(0, 60) + (userMessage.length > 60 ? '...' : '')
|
|
98
|
+
: 'attribute-based spec';
|
|
99
|
+
logger.info(`Processing ${action}: <${elementName}${attrPreview}>`);
|
|
100
|
+
logger.info(` Message: "${messagePreview}"`);
|
|
101
|
+
logger.debug(` Context: ${elementPath}`);
|
|
102
|
+
try {
|
|
103
|
+
// Read existing code file if it exists (extension depends on target language)
|
|
104
|
+
const targetExtensions = {
|
|
105
|
+
html: '.html',
|
|
106
|
+
bash: '.sh',
|
|
107
|
+
python: '.py',
|
|
108
|
+
javascript: '.js',
|
|
109
|
+
typescript: '.ts',
|
|
110
|
+
go: '.go',
|
|
111
|
+
rust: '.rs',
|
|
112
|
+
};
|
|
113
|
+
const ext = targetExtensions[pattern.target] || '.html';
|
|
114
|
+
const codeFilePath = config.specFile.replace('.cdml', ext);
|
|
115
|
+
if (existsSync(codeFilePath)) {
|
|
116
|
+
const existingCode = await readFile(codeFilePath, 'utf-8');
|
|
117
|
+
context.existingCode = existingCode;
|
|
118
|
+
logger.debug(`Loaded existing ${pattern.target} code for context`);
|
|
119
|
+
}
|
|
120
|
+
// Build full prompt combining element name, attributes, user message, and action instructions
|
|
121
|
+
const fullPrompt = buildFullPrompt($, element, elementName, specAttributes, userMessage, pattern.actionInstructions);
|
|
122
|
+
// Debug: log the prompt to see what's being sent
|
|
123
|
+
logger.debug('=== FULL PROMPT BEING SENT ===');
|
|
124
|
+
logger.debug(fullPrompt);
|
|
125
|
+
logger.debug('=== END PROMPT ===');
|
|
126
|
+
// Accumulate full response
|
|
127
|
+
let fullResponse = '';
|
|
128
|
+
// Send to Claude and stream response
|
|
129
|
+
for await (const chunk of claudeClient.sendPrompt(fullPrompt, context)) {
|
|
130
|
+
fullResponse += chunk;
|
|
131
|
+
// Optionally: show progress
|
|
132
|
+
if (process.env.DEBUG) {
|
|
133
|
+
process.stdout.write('.');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (process.env.DEBUG) {
|
|
137
|
+
process.stdout.write('\n');
|
|
138
|
+
}
|
|
139
|
+
logger.success('Received response from Claude');
|
|
140
|
+
// Check if response contains code blocks
|
|
141
|
+
const codeBlocks = extractCodeBlocks(fullResponse);
|
|
142
|
+
const hasCode = codeBlocks.length > 0;
|
|
143
|
+
// Generate code file using universal generator (if there's code)
|
|
144
|
+
if (hasCode) {
|
|
145
|
+
const generated = await generateCode(fullResponse, pattern, context);
|
|
146
|
+
// Determine output file path
|
|
147
|
+
const outputFile = config.specFile.replace('.cdml', generated.fileExtension);
|
|
148
|
+
// Write generated code
|
|
149
|
+
await writeFile(outputFile, generated.code, 'utf-8');
|
|
150
|
+
// Make executable if it's a script
|
|
151
|
+
if (generated.metadata?.executable) {
|
|
152
|
+
await chmod(outputFile, 0o755);
|
|
153
|
+
logger.debug(`Made ${outputFile} executable`);
|
|
154
|
+
}
|
|
155
|
+
logger.success(`Generated ${pattern.target} code → ${outputFile}`);
|
|
156
|
+
}
|
|
157
|
+
// Update spec.html: remove action attribute and add <ai> child with response
|
|
158
|
+
watcher.setUpdating(true);
|
|
159
|
+
try {
|
|
160
|
+
await updateSpecWithResponse($, element, action, fullResponse, config.specFile, hasCode);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
watcher.setUpdating(false);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const err = error;
|
|
168
|
+
logger.error(`Failed to process chat pattern: ${err.message}`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build full prompt from element name, spec attributes, and user message
|
|
174
|
+
*/
|
|
175
|
+
function buildFullPrompt($, element, elementName, specAttributes, userMessage, actionInstructions) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
// Add action instructions if provided (e.g. gen="use opus 4.6")
|
|
178
|
+
if (actionInstructions) {
|
|
179
|
+
parts.push(`Generation Instructions: ${actionInstructions}`);
|
|
180
|
+
parts.push('');
|
|
181
|
+
}
|
|
182
|
+
// Add element name as semantic header
|
|
183
|
+
parts.push(`Element: ${elementName}`);
|
|
184
|
+
parts.push('');
|
|
185
|
+
// Add spec attributes if any
|
|
186
|
+
if (Object.keys(specAttributes).length > 0) {
|
|
187
|
+
parts.push('Specifications:');
|
|
188
|
+
for (const [key, value] of Object.entries(specAttributes)) {
|
|
189
|
+
parts.push(` ${key}: ${value}`);
|
|
190
|
+
}
|
|
191
|
+
parts.push('');
|
|
192
|
+
}
|
|
193
|
+
// Extract nested element specifications directly from the element's children
|
|
194
|
+
const nestedSpecs = extractNestedSpecifications($, element);
|
|
195
|
+
if (nestedSpecs.length > 0) {
|
|
196
|
+
parts.push('NESTED COMPONENTS TO IMPLEMENT:');
|
|
197
|
+
parts.push('The following components MUST be implemented (do not use placeholder comments):');
|
|
198
|
+
parts.push('');
|
|
199
|
+
function formatSpec(spec, indent, index) {
|
|
200
|
+
const prefix = index > 0 ? `${index}. ` : '';
|
|
201
|
+
const lockStatus = spec.isLocked ? ' [LOCKED - DO NOT REGENERATE]' : '';
|
|
202
|
+
parts.push(`${indent}${prefix}<${spec.elementName}>${lockStatus}`);
|
|
203
|
+
if (spec.isLocked) {
|
|
204
|
+
parts.push(`${indent} ⚠️ This component is LOCKED - keep existing implementation, do NOT regenerate`);
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(spec.attributes).length > 0) {
|
|
207
|
+
parts.push(`${indent} Attributes:`);
|
|
208
|
+
for (const [key, value] of Object.entries(spec.attributes)) {
|
|
209
|
+
parts.push(`${indent} - ${key}: ${value}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (spec.textContent) {
|
|
213
|
+
parts.push(`${indent} Content: ${spec.textContent}`);
|
|
214
|
+
}
|
|
215
|
+
// Recursively format children
|
|
216
|
+
if (spec.children && spec.children.length > 0) {
|
|
217
|
+
parts.push(`${indent} Contains nested components:`);
|
|
218
|
+
spec.children.forEach((child, childIndex) => {
|
|
219
|
+
formatSpec(child, indent + ' ', childIndex + 1);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
parts.push('');
|
|
223
|
+
}
|
|
224
|
+
nestedSpecs.forEach((spec, index) => {
|
|
225
|
+
formatSpec(spec, '', index + 1);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
// Add user message if any
|
|
229
|
+
if (userMessage) {
|
|
230
|
+
parts.push('Full Description:');
|
|
231
|
+
parts.push(userMessage);
|
|
232
|
+
}
|
|
233
|
+
return parts.join('\n');
|
|
234
|
+
}
|
|
235
|
+
function extractNestedSpecifications($, element) {
|
|
236
|
+
const specs = [];
|
|
237
|
+
function extractRecursive(parentElement, depth, parentLocked) {
|
|
238
|
+
const result = [];
|
|
239
|
+
const $parent = $(parentElement);
|
|
240
|
+
$parent.children().each((_, child) => {
|
|
241
|
+
if (child.type === 'tag') {
|
|
242
|
+
const childElement = child;
|
|
243
|
+
const elementName = childElement.name;
|
|
244
|
+
// Skip <ai> elements (these are AI responses, not specifications)
|
|
245
|
+
if (elementName === 'ai') {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const $child = $(childElement);
|
|
249
|
+
const attrs = childElement.attribs || {};
|
|
250
|
+
// Check lock/unlock status
|
|
251
|
+
const hasLock = 'lock' in attrs;
|
|
252
|
+
const hasUnlock = 'unlock' in attrs;
|
|
253
|
+
// Determine if this element is locked:
|
|
254
|
+
// - If parent is locked and element doesn't have unlock => locked
|
|
255
|
+
// - If element has lock attribute => locked
|
|
256
|
+
// - If element has unlock attribute => unlocked (overrides parent lock)
|
|
257
|
+
const isLocked = hasLock || (parentLocked && !hasUnlock);
|
|
258
|
+
// Extract attributes (exclude lock/unlock/gen/retry/undo as they're control attributes)
|
|
259
|
+
const attributes = {};
|
|
260
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
261
|
+
if (key !== 'lock' && key !== 'unlock' && key !== 'gen' && key !== 'retry' && key !== 'undo') {
|
|
262
|
+
attributes[key] = value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Get direct text content (not from children)
|
|
266
|
+
const textContent = $child.contents()
|
|
267
|
+
.filter((_, node) => node.type === 'text')
|
|
268
|
+
.text()
|
|
269
|
+
.trim();
|
|
270
|
+
// Recursively extract children, passing down the locked state
|
|
271
|
+
const children = extractRecursive(childElement, depth + 1, isLocked);
|
|
272
|
+
const hasChildren = children.length > 0;
|
|
273
|
+
result.push({
|
|
274
|
+
elementName,
|
|
275
|
+
attributes,
|
|
276
|
+
textContent,
|
|
277
|
+
hasChildren,
|
|
278
|
+
depth,
|
|
279
|
+
isLocked,
|
|
280
|
+
children: hasChildren ? children : undefined,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
// Check if the root element (the one with gen/retry/undo) has a lock attribute
|
|
288
|
+
// If it does, all its children are locked by default (unless they have unlock)
|
|
289
|
+
const rootAttrs = element.attribs || {};
|
|
290
|
+
const rootHasLock = 'lock' in rootAttrs;
|
|
291
|
+
specs.push(...extractRecursive(element, 0, rootHasLock));
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
logger.debug(`Could not parse nested specifications: ${error}`);
|
|
295
|
+
}
|
|
296
|
+
return specs;
|
|
297
|
+
}
|
|
298
|
+
// Start the application
|
|
299
|
+
main().catch((error) => {
|
|
300
|
+
logger.error(`Fatal error: ${error.message}`);
|
|
301
|
+
if (process.env.DEBUG) {
|
|
302
|
+
console.error(error.stack);
|
|
303
|
+
}
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML-ish updater - inserts AI responses into spec.html
|
|
3
|
+
*/
|
|
4
|
+
import type { CheerioAPI } from 'cheerio';
|
|
5
|
+
import type { Element } from 'domhandler';
|
|
6
|
+
/**
|
|
7
|
+
* Strip code blocks from response and replace with reference
|
|
8
|
+
*/
|
|
9
|
+
export declare function stripCodeBlocks(response: string, hasCode: boolean): string;
|
|
10
|
+
/**
|
|
11
|
+
* Update element with AI response: remove action attribute and add/update <ai> child
|
|
12
|
+
*/
|
|
13
|
+
export declare function updateElementWithResponse($: CheerioAPI, element: Element, action: 'gen' | 'retry' | 'undo', response: string, hasCode?: boolean): void;
|
|
14
|
+
/**
|
|
15
|
+
* Serialize cheerio DOM back to HTML string
|
|
16
|
+
*/
|
|
17
|
+
export declare function serializeToHTML($: CheerioAPI): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Write updated content back to spec.html safely
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeSpecFile(filePath: string, content: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Grace period to prevent immediate re-trigger of watcher
|
|
24
|
+
*/
|
|
25
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Complete update flow: remove action attribute, add AI response, serialize, write file
|
|
28
|
+
*/
|
|
29
|
+
export declare function updateSpecWithResponse($: CheerioAPI, element: Element, action: 'gen' | 'retry' | 'undo', response: string, filePath: string, hasCode?: boolean): Promise<void>;
|