voyageai-cli 1.20.6 → 1.22.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/CHANGELOG.md +142 -26
- package/README.md +130 -2
- package/package.json +3 -2
- package/src/cli.js +10 -0
- package/src/commands/bug.js +249 -0
- package/src/commands/eval.js +420 -10
- package/src/commands/generate.js +220 -0
- package/src/commands/playground.js +93 -0
- package/src/commands/purge.js +271 -0
- package/src/commands/refresh.js +322 -0
- package/src/commands/scaffold.js +217 -0
- package/src/lib/codegen.js +339 -0
- package/src/lib/explanations.js +155 -0
- package/src/lib/scaffold-structure.js +114 -0
- package/src/lib/templates/nextjs/README.md.tpl +106 -0
- package/src/lib/templates/nextjs/env.example.tpl +8 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
- package/src/lib/templates/nextjs/package.json.tpl +33 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
- package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
- package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
- package/src/lib/templates/nextjs/theme.js.tpl +84 -0
- package/src/lib/templates/python/README.md.tpl +145 -0
- package/src/lib/templates/python/app.py.tpl +221 -0
- package/src/lib/templates/python/chunker.py.tpl +127 -0
- package/src/lib/templates/python/env.example.tpl +12 -0
- package/src/lib/templates/python/mongo_client.py.tpl +125 -0
- package/src/lib/templates/python/requirements.txt.tpl +10 -0
- package/src/lib/templates/python/voyage_client.py.tpl +124 -0
- package/src/lib/templates/vanilla/README.md.tpl +156 -0
- package/src/lib/templates/vanilla/client.js.tpl +103 -0
- package/src/lib/templates/vanilla/connection.js.tpl +126 -0
- package/src/lib/templates/vanilla/env.example.tpl +11 -0
- package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
- package/src/lib/templates/vanilla/package.json.tpl +31 -0
- package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
- package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
- package/src/lib/templates/vanilla/server.js.tpl +81 -0
- package/src/lib/zip.js +130 -0
- package/src/playground/index.html +708 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const p = require('@clack/prompts');
|
|
6
|
+
const { loadProject } = require('../lib/project');
|
|
7
|
+
const { renderTemplate, buildContext, listTemplates } = require('../lib/codegen');
|
|
8
|
+
const { PROJECT_STRUCTURE } = require('../lib/scaffold-structure');
|
|
9
|
+
const ui = require('../lib/ui');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a directory if it doesn't exist.
|
|
13
|
+
*/
|
|
14
|
+
function ensureDir(dirPath) {
|
|
15
|
+
if (!fs.existsSync(dirPath)) {
|
|
16
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Write a file, creating parent directories as needed.
|
|
22
|
+
*/
|
|
23
|
+
function writeFile(filePath, content) {
|
|
24
|
+
ensureDir(path.dirname(filePath));
|
|
25
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a directory exists and is not empty.
|
|
30
|
+
*/
|
|
31
|
+
function directoryExists(dirPath) {
|
|
32
|
+
if (!fs.existsSync(dirPath)) return false;
|
|
33
|
+
const files = fs.readdirSync(dirPath);
|
|
34
|
+
return files.length > 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register the scaffold command.
|
|
39
|
+
* @param {import('commander').Command} program
|
|
40
|
+
*/
|
|
41
|
+
function registerScaffold(program) {
|
|
42
|
+
program
|
|
43
|
+
.command('scaffold <name>')
|
|
44
|
+
.description('Create a complete starter project')
|
|
45
|
+
.option('-t, --target <target>', 'Target framework: vanilla, nextjs, python', 'vanilla')
|
|
46
|
+
.option('-m, --model <model>', 'Override embedding model')
|
|
47
|
+
.option('--db <database>', 'Override database name')
|
|
48
|
+
.option('--collection <name>', 'Override collection name')
|
|
49
|
+
.option('--field <name>', 'Override embedding field name')
|
|
50
|
+
.option('--index <name>', 'Override vector index name')
|
|
51
|
+
.option('-d, --dimensions <n>', 'Override dimensions', parseInt)
|
|
52
|
+
.option('--no-rerank', 'Omit reranking from generated code')
|
|
53
|
+
.option('--rerank-model <model>', 'Rerank model to use')
|
|
54
|
+
.option('--force', 'Overwrite existing directory')
|
|
55
|
+
.option('--json', 'Output file manifest as JSON (no file creation)')
|
|
56
|
+
.option('--dry-run', 'Show what would be created without writing')
|
|
57
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
58
|
+
.action(async (name, opts) => {
|
|
59
|
+
try {
|
|
60
|
+
const target = opts.target;
|
|
61
|
+
const projectDir = path.resolve(process.cwd(), name);
|
|
62
|
+
|
|
63
|
+
// Validate target
|
|
64
|
+
if (!PROJECT_STRUCTURE[target]) {
|
|
65
|
+
console.error(ui.error(`Invalid target: ${target}`));
|
|
66
|
+
console.error(ui.dim(` Valid targets: ${Object.keys(PROJECT_STRUCTURE).join(', ')}`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const structure = PROJECT_STRUCTURE[target];
|
|
71
|
+
|
|
72
|
+
// Check if directory exists
|
|
73
|
+
if (directoryExists(projectDir) && !opts.force && !opts.dryRun && !opts.json) {
|
|
74
|
+
console.error(ui.error(`Directory already exists: ${name}`));
|
|
75
|
+
console.error(ui.dim(' Use --force to overwrite.'));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Load project config
|
|
80
|
+
let project = {};
|
|
81
|
+
try {
|
|
82
|
+
project = loadProject();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// No .vai.json, use defaults
|
|
85
|
+
if (!opts.quiet && !opts.json) {
|
|
86
|
+
console.error(ui.warn('No .vai.json found, using defaults'));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build context with overrides
|
|
91
|
+
const context = buildContext(project, {
|
|
92
|
+
model: opts.model,
|
|
93
|
+
db: opts.db,
|
|
94
|
+
collection: opts.collection,
|
|
95
|
+
field: opts.field,
|
|
96
|
+
index: opts.index,
|
|
97
|
+
dimensions: opts.dimensions,
|
|
98
|
+
rerank: opts.rerank,
|
|
99
|
+
rerankModel: opts.rerankModel,
|
|
100
|
+
projectName: name,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Build file manifest
|
|
104
|
+
const manifest = [];
|
|
105
|
+
|
|
106
|
+
// Render template files
|
|
107
|
+
for (const file of structure.files) {
|
|
108
|
+
const content = renderTemplate(target, file.template, context);
|
|
109
|
+
manifest.push({
|
|
110
|
+
path: file.output,
|
|
111
|
+
fullPath: path.join(projectDir, file.output),
|
|
112
|
+
source: `${target}/${file.template}`,
|
|
113
|
+
size: content.length,
|
|
114
|
+
content,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add extra static files
|
|
119
|
+
if (structure.extraFiles) {
|
|
120
|
+
for (const file of structure.extraFiles) {
|
|
121
|
+
const content = typeof file.content === 'function'
|
|
122
|
+
? file.content(context)
|
|
123
|
+
: file.content;
|
|
124
|
+
manifest.push({
|
|
125
|
+
path: file.output,
|
|
126
|
+
fullPath: path.join(projectDir, file.output),
|
|
127
|
+
source: 'static',
|
|
128
|
+
size: content.length,
|
|
129
|
+
content,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// JSON output mode
|
|
135
|
+
if (opts.json) {
|
|
136
|
+
const output = {
|
|
137
|
+
name,
|
|
138
|
+
target,
|
|
139
|
+
directory: projectDir,
|
|
140
|
+
files: manifest.map(f => ({
|
|
141
|
+
path: f.path,
|
|
142
|
+
size: f.size,
|
|
143
|
+
source: f.source,
|
|
144
|
+
})),
|
|
145
|
+
config: context,
|
|
146
|
+
};
|
|
147
|
+
console.log(JSON.stringify(output, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Dry run mode
|
|
152
|
+
if (opts.dryRun) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(ui.bold(`Would create: ${name}/ (${structure.description})`));
|
|
155
|
+
console.log('');
|
|
156
|
+
for (const file of manifest) {
|
|
157
|
+
console.log(` ${ui.cyan('+')} ${file.path} ${ui.dim(`(${file.size} bytes)`)}`);
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(ui.dim(`Total: ${manifest.length} files`));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create project directory
|
|
165
|
+
if (!opts.quiet) {
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(ui.bold(`Creating ${name}/ (${structure.description})`));
|
|
168
|
+
console.log('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
ensureDir(projectDir);
|
|
172
|
+
|
|
173
|
+
// Write all files
|
|
174
|
+
for (const file of manifest) {
|
|
175
|
+
writeFile(file.fullPath, file.content);
|
|
176
|
+
if (!opts.quiet) {
|
|
177
|
+
console.log(` ${ui.cyan('✓')} ${file.path}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Success message with next steps
|
|
182
|
+
if (!opts.quiet) {
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(ui.success(`Created ${manifest.length} files in ${name}/`));
|
|
185
|
+
console.log('');
|
|
186
|
+
|
|
187
|
+
// Use clack's note for next steps
|
|
188
|
+
const steps = [
|
|
189
|
+
`cd ${name}`,
|
|
190
|
+
`cp .env.example .env`,
|
|
191
|
+
`# Edit .env with your API keys`,
|
|
192
|
+
structure.postInstall,
|
|
193
|
+
structure.startCommand,
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
p.note(steps.join('\n'), 'Next steps');
|
|
197
|
+
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log(ui.dim('Configuration:'));
|
|
200
|
+
console.log(ui.dim(` Model: ${context.model}`));
|
|
201
|
+
console.log(ui.dim(` Database: ${context.db}.${context.collection}`));
|
|
202
|
+
console.log(ui.dim(` Dimensions: ${context.dimensions}`));
|
|
203
|
+
if (context.rerank) {
|
|
204
|
+
console.log(ui.dim(` Reranking: ${context.rerankModel}`));
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error(ui.error(err.message));
|
|
211
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { registerScaffold, PROJECT_STRUCTURE };
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Safely get the CLI version, handling both development and packaged Electron app.
|
|
8
|
+
* @returns {string} The version string or 'unknown'
|
|
9
|
+
*/
|
|
10
|
+
function getCliVersion() {
|
|
11
|
+
// Try multiple paths to find package.json
|
|
12
|
+
const possiblePaths = [
|
|
13
|
+
path.join(__dirname, '..', '..', 'package.json'), // Development: src/lib -> root
|
|
14
|
+
path.join(process.resourcesPath || '', 'cli-package.json'), // Packaged Electron app
|
|
15
|
+
path.join(__dirname, '..', 'package.json'), // Alternative structure
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const pkgPath of possiblePaths) {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(pkgPath)) {
|
|
21
|
+
const pkg = require(pkgPath);
|
|
22
|
+
if (pkg.version) return pkg.version;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Try next path
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lightweight template engine for code generation.
|
|
34
|
+
*
|
|
35
|
+
* Syntax:
|
|
36
|
+
* {{variable}} - Variable substitution
|
|
37
|
+
* {{variable.nested}} - Nested property access
|
|
38
|
+
* {{#if condition}}...{{/if}} - Conditional block
|
|
39
|
+
* {{#unless condition}}...{{/unless}} - Inverse conditional
|
|
40
|
+
* {{#each items}}...{{/each}} - Loop over array
|
|
41
|
+
* {{@index}} - Current loop index (0-based)
|
|
42
|
+
* {{@first}} - true if first iteration
|
|
43
|
+
* {{@last}} - true if last iteration
|
|
44
|
+
* {{this}} - Current item in loop
|
|
45
|
+
*
|
|
46
|
+
* No dependencies. All templates are .tpl files.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a nested property from an object using dot notation.
|
|
51
|
+
* @param {object} obj - The object to query
|
|
52
|
+
* @param {string} path - Dot-separated path (e.g., "chunk.strategy")
|
|
53
|
+
* @returns {*} The value or undefined
|
|
54
|
+
*/
|
|
55
|
+
function getPath(obj, path) {
|
|
56
|
+
if (!obj || !path) return undefined;
|
|
57
|
+
const parts = path.split('.');
|
|
58
|
+
let current = obj;
|
|
59
|
+
for (const part of parts) {
|
|
60
|
+
if (current == null) return undefined;
|
|
61
|
+
current = current[part];
|
|
62
|
+
}
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a value is truthy for template conditionals.
|
|
68
|
+
* Empty arrays and empty strings are falsy.
|
|
69
|
+
* @param {*} value
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
function isTruthy(value) {
|
|
73
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
74
|
+
if (value === '') return false;
|
|
75
|
+
return Boolean(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Escape special regex characters in a string.
|
|
80
|
+
* @param {string} str
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function escapeRegex(str) {
|
|
84
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a template string with the given context.
|
|
89
|
+
* @param {string} template - Template string with {{...}} placeholders
|
|
90
|
+
* @param {object} context - Data object for substitution
|
|
91
|
+
* @returns {string} Rendered output
|
|
92
|
+
*/
|
|
93
|
+
function render(template, context = {}) {
|
|
94
|
+
let result = template;
|
|
95
|
+
|
|
96
|
+
// Process {{#each items}}...{{/each}} blocks first (can be nested)
|
|
97
|
+
result = processEachBlocks(result, context);
|
|
98
|
+
|
|
99
|
+
// Process {{#if condition}}...{{/if}} blocks
|
|
100
|
+
result = processIfBlocks(result, context);
|
|
101
|
+
|
|
102
|
+
// Process {{#unless condition}}...{{/unless}} blocks
|
|
103
|
+
result = processUnlessBlocks(result, context);
|
|
104
|
+
|
|
105
|
+
// Process simple variable substitutions {{variable}} and {{variable.nested}}
|
|
106
|
+
result = result.replace(/\{\{([a-zA-Z_][\w.]*)\}\}/g, (match, varPath) => {
|
|
107
|
+
const value = getPath(context, varPath);
|
|
108
|
+
if (value === undefined || value === null) return '';
|
|
109
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
110
|
+
return String(value);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Process {{#each items}}...{{/each}} blocks.
|
|
118
|
+
* Supports nested each blocks and special variables: @index, @first, @last, this
|
|
119
|
+
*/
|
|
120
|
+
function processEachBlocks(template, context) {
|
|
121
|
+
// Match {{#each varName}}...{{/each}} - non-greedy, handles nesting
|
|
122
|
+
const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
|
123
|
+
|
|
124
|
+
let result = template;
|
|
125
|
+
let match;
|
|
126
|
+
let iterations = 0;
|
|
127
|
+
const maxIterations = 100; // Prevent infinite loops
|
|
128
|
+
|
|
129
|
+
// Keep processing until no more matches (handles nested blocks)
|
|
130
|
+
while ((match = eachRegex.exec(result)) !== null && iterations < maxIterations) {
|
|
131
|
+
const [fullMatch, varName, blockContent] = match;
|
|
132
|
+
const items = getPath(context, varName);
|
|
133
|
+
|
|
134
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
135
|
+
result = result.replace(fullMatch, '');
|
|
136
|
+
eachRegex.lastIndex = 0; // Reset regex
|
|
137
|
+
iterations++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rendered = items.map((item, index) => {
|
|
142
|
+
// Create loop context with special variables
|
|
143
|
+
const loopContext = {
|
|
144
|
+
...context,
|
|
145
|
+
'@index': index,
|
|
146
|
+
'@first': index === 0,
|
|
147
|
+
'@last': index === items.length - 1,
|
|
148
|
+
'this': item,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// If item is an object, spread its properties into context
|
|
152
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
153
|
+
Object.assign(loopContext, item);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return render(blockContent, loopContext);
|
|
157
|
+
}).join('');
|
|
158
|
+
|
|
159
|
+
result = result.replace(fullMatch, rendered);
|
|
160
|
+
eachRegex.lastIndex = 0; // Reset regex for next iteration
|
|
161
|
+
iterations++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Process {{#if condition}}...{{/if}} and {{#if condition}}...{{else}}...{{/if}} blocks.
|
|
169
|
+
* Handles nested blocks by processing iteratively from innermost to outermost.
|
|
170
|
+
*/
|
|
171
|
+
function processIfBlocks(template, context) {
|
|
172
|
+
let result = template;
|
|
173
|
+
let changed = true;
|
|
174
|
+
let iterations = 0;
|
|
175
|
+
const maxIterations = 50;
|
|
176
|
+
|
|
177
|
+
// Process iteratively until no more matches (handles nesting)
|
|
178
|
+
while (changed && iterations < maxIterations) {
|
|
179
|
+
changed = false;
|
|
180
|
+
iterations++;
|
|
181
|
+
|
|
182
|
+
// Match if-else blocks that don't contain nested {{#if (innermost first)
|
|
183
|
+
// This regex ensures we don't have another {{#if inside the captured groups
|
|
184
|
+
const ifElseRegex = /\{\{#if\s+(\w+(?:\.\w+)*)\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{else\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{\/if\}\}/;
|
|
185
|
+
|
|
186
|
+
let match = result.match(ifElseRegex);
|
|
187
|
+
if (match) {
|
|
188
|
+
const [fullMatch, varPath, ifBlock, elseBlock] = match;
|
|
189
|
+
const value = getPath(context, varPath);
|
|
190
|
+
const replacement = isTruthy(value) ? render(ifBlock, context) : render(elseBlock, context);
|
|
191
|
+
result = result.replace(fullMatch, replacement);
|
|
192
|
+
changed = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Match simple if blocks that don't contain nested {{#if
|
|
197
|
+
const ifRegex = /\{\{#if\s+(\w+(?:\.\w+)*)\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{\/if\}\}/;
|
|
198
|
+
|
|
199
|
+
match = result.match(ifRegex);
|
|
200
|
+
if (match) {
|
|
201
|
+
const [fullMatch, varPath, blockContent] = match;
|
|
202
|
+
const value = getPath(context, varPath);
|
|
203
|
+
const replacement = isTruthy(value) ? render(blockContent, context) : '';
|
|
204
|
+
result = result.replace(fullMatch, replacement);
|
|
205
|
+
changed = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Process {{#unless condition}}...{{/unless}} blocks.
|
|
214
|
+
*/
|
|
215
|
+
function processUnlessBlocks(template, context) {
|
|
216
|
+
const unlessRegex = /\{\{#unless\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/unless\}\}/g;
|
|
217
|
+
|
|
218
|
+
return template.replace(unlessRegex, (match, varPath, blockContent) => {
|
|
219
|
+
const value = getPath(context, varPath);
|
|
220
|
+
return isTruthy(value) ? '' : render(blockContent, context);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Load a template file from the templates directory.
|
|
226
|
+
* @param {string} target - Target framework: 'vanilla', 'nextjs', 'python'
|
|
227
|
+
* @param {string} name - Template name (with or without .tpl extension)
|
|
228
|
+
* @returns {string} Template content
|
|
229
|
+
*/
|
|
230
|
+
function loadTemplate(target, name) {
|
|
231
|
+
const templatesDir = path.join(__dirname, 'templates', target);
|
|
232
|
+
|
|
233
|
+
// Try exact name with .tpl
|
|
234
|
+
let filePath = path.join(templatesDir, name.endsWith('.tpl') ? name : `${name}.tpl`);
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(filePath)) {
|
|
237
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Try with common extensions
|
|
241
|
+
const extensions = ['.js.tpl', '.jsx.tpl', '.py.tpl', '.json.tpl', '.md.tpl'];
|
|
242
|
+
for (const ext of extensions) {
|
|
243
|
+
filePath = path.join(templatesDir, `${name}${ext}`);
|
|
244
|
+
if (fs.existsSync(filePath)) {
|
|
245
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
throw new Error(`Template not found: ${target}/${name}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* List available templates for a target.
|
|
254
|
+
* @param {string} target - Target framework
|
|
255
|
+
* @returns {string[]} List of template names (without .tpl extension)
|
|
256
|
+
*/
|
|
257
|
+
function listTemplates(target) {
|
|
258
|
+
const templatesDir = path.join(__dirname, 'templates', target);
|
|
259
|
+
|
|
260
|
+
if (!fs.existsSync(templatesDir)) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return fs.readdirSync(templatesDir)
|
|
265
|
+
.filter(f => f.endsWith('.tpl'))
|
|
266
|
+
.map(f => f.replace('.tpl', ''));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get all available targets (framework directories).
|
|
271
|
+
* @returns {string[]} List of target names
|
|
272
|
+
*/
|
|
273
|
+
function listTargets() {
|
|
274
|
+
const templatesDir = path.join(__dirname, 'templates');
|
|
275
|
+
|
|
276
|
+
if (!fs.existsSync(templatesDir)) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return fs.readdirSync(templatesDir)
|
|
281
|
+
.filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory());
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Render a template file with context.
|
|
286
|
+
* @param {string} target - Target framework
|
|
287
|
+
* @param {string} name - Template name
|
|
288
|
+
* @param {object} context - Data for substitution
|
|
289
|
+
* @returns {string} Rendered output
|
|
290
|
+
*/
|
|
291
|
+
function renderTemplate(target, name, context) {
|
|
292
|
+
const template = loadTemplate(target, name);
|
|
293
|
+
return render(template, context);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Build a context object from project config and CLI options.
|
|
298
|
+
* @param {object} project - Project config from .vai.json
|
|
299
|
+
* @param {object} options - CLI options (overrides)
|
|
300
|
+
* @returns {object} Merged context for templates
|
|
301
|
+
*/
|
|
302
|
+
function buildContext(project, options = {}) {
|
|
303
|
+
const context = {
|
|
304
|
+
// Core config
|
|
305
|
+
model: options.model || project.model || 'voyage-3-large',
|
|
306
|
+
db: options.db || project.db || 'myapp',
|
|
307
|
+
collection: options.collection || project.collection || 'documents',
|
|
308
|
+
field: options.field || project.field || 'embedding',
|
|
309
|
+
index: options.index || project.index || 'vector_index',
|
|
310
|
+
dimensions: options.dimensions || project.dimensions || 1024,
|
|
311
|
+
inputType: options.inputType || project.inputType || 'document',
|
|
312
|
+
|
|
313
|
+
// Chunk config
|
|
314
|
+
chunkStrategy: project.chunk?.strategy || 'recursive',
|
|
315
|
+
chunkSize: project.chunk?.size || 1000,
|
|
316
|
+
chunkOverlap: project.chunk?.overlap || 200,
|
|
317
|
+
|
|
318
|
+
// Feature flags
|
|
319
|
+
rerank: options.rerank !== false && options.noRerank !== true,
|
|
320
|
+
rerankModel: options.rerankModel || 'rerank-2.5',
|
|
321
|
+
|
|
322
|
+
// Metadata
|
|
323
|
+
generatedAt: new Date().toISOString(),
|
|
324
|
+
vaiVersion: getCliVersion(),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return context;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = {
|
|
331
|
+
render,
|
|
332
|
+
loadTemplate,
|
|
333
|
+
listTemplates,
|
|
334
|
+
listTargets,
|
|
335
|
+
renderTemplate,
|
|
336
|
+
buildContext,
|
|
337
|
+
getPath,
|
|
338
|
+
isTruthy,
|
|
339
|
+
};
|