voyageai-cli 1.20.5 → 1.21.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 +153 -2
- package/package.json +1 -1
- package/src/cli.js +12 -0
- package/src/commands/about.js +39 -3
- package/src/commands/doctor.js +325 -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/quickstart.js +203 -0
- package/src/commands/refresh.js +322 -0
- package/src/commands/scaffold.js +217 -0
- package/src/lib/codegen.js +313 -0
- package/src/lib/explanations.js +163 -1
- 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 +772 -33
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const p = require('@clack/prompts');
|
|
4
|
+
const { loadProject, saveProject } = require('../lib/project');
|
|
5
|
+
const { connect, close } = require('../lib/mongo');
|
|
6
|
+
const { generateEmbeddings } = require('../lib/api');
|
|
7
|
+
const { chunkText } = require('../lib/chunker');
|
|
8
|
+
const ui = require('../lib/ui');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Process documents in batches.
|
|
12
|
+
*/
|
|
13
|
+
async function processBatch(docs, embedder, options) {
|
|
14
|
+
const texts = docs.map(d => d.text);
|
|
15
|
+
const embeddings = await embedder(texts);
|
|
16
|
+
|
|
17
|
+
return docs.map((doc, i) => ({
|
|
18
|
+
...doc,
|
|
19
|
+
[options.field]: embeddings[i],
|
|
20
|
+
_model: options.model,
|
|
21
|
+
_embeddedAt: new Date(),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Re-chunk a document's text.
|
|
27
|
+
*/
|
|
28
|
+
function rechunkDocument(doc, options) {
|
|
29
|
+
const text = doc.text || doc.content || '';
|
|
30
|
+
if (!text) return [doc];
|
|
31
|
+
|
|
32
|
+
const chunks = chunkText(text, {
|
|
33
|
+
strategy: options.strategy || 'recursive',
|
|
34
|
+
chunkSize: options.chunkSize || 512,
|
|
35
|
+
overlap: options.overlap || 50,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return chunks.map((chunk, i) => ({
|
|
39
|
+
...doc,
|
|
40
|
+
text: chunk.text,
|
|
41
|
+
_chunkIndex: i,
|
|
42
|
+
_chunkCount: chunks.length,
|
|
43
|
+
metadata: {
|
|
44
|
+
...doc.metadata,
|
|
45
|
+
chunkIndex: i,
|
|
46
|
+
chunkCount: chunks.length,
|
|
47
|
+
originalId: doc._id?.toString(),
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute the refresh command.
|
|
54
|
+
*/
|
|
55
|
+
async function refresh(options = {}) {
|
|
56
|
+
const quiet = options.quiet || options.json;
|
|
57
|
+
|
|
58
|
+
// Load project config
|
|
59
|
+
const project = loadProject();
|
|
60
|
+
const db = options.db || project.db || process.env.VAI_DB || 'vai';
|
|
61
|
+
const collectionName = options.collection || project.collection || process.env.VAI_COLLECTION || 'embeddings';
|
|
62
|
+
const field = options.field || project.field || 'embedding';
|
|
63
|
+
const model = options.model || project.model || 'voyage-3.5-lite';
|
|
64
|
+
const dimensions = options.dimensions || project.dimensions;
|
|
65
|
+
const batchSize = options.batchSize || 25;
|
|
66
|
+
|
|
67
|
+
if (!quiet) {
|
|
68
|
+
p.intro(ui.title('vai refresh'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let client;
|
|
72
|
+
try {
|
|
73
|
+
// Connect to MongoDB
|
|
74
|
+
if (!quiet) {
|
|
75
|
+
p.log.step(`Connecting to database: ${db}`);
|
|
76
|
+
}
|
|
77
|
+
client = await connect(db);
|
|
78
|
+
const collection = client.db(db).collection(collectionName);
|
|
79
|
+
|
|
80
|
+
// Build filter
|
|
81
|
+
let filter = {};
|
|
82
|
+
if (options.filter) {
|
|
83
|
+
try {
|
|
84
|
+
filter = JSON.parse(options.filter);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new Error(`Invalid JSON filter: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Count documents
|
|
91
|
+
const totalCount = await collection.countDocuments(filter);
|
|
92
|
+
|
|
93
|
+
if (totalCount === 0) {
|
|
94
|
+
if (options.json) {
|
|
95
|
+
console.log(JSON.stringify({ success: true, count: 0, message: 'No documents to refresh' }));
|
|
96
|
+
} else {
|
|
97
|
+
p.log.success('No documents to refresh.');
|
|
98
|
+
p.outro('Nothing to do.');
|
|
99
|
+
}
|
|
100
|
+
return { success: true, count: 0 };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Show plan
|
|
104
|
+
const rechunkLabel = options.rechunk ? ` (re-chunking with ${options.strategy || 'recursive'})` : '';
|
|
105
|
+
const dimLabel = dimensions ? ` @ ${dimensions}d` : '';
|
|
106
|
+
|
|
107
|
+
if (options.json && options.dryRun) {
|
|
108
|
+
console.log(JSON.stringify({
|
|
109
|
+
dryRun: true,
|
|
110
|
+
count: totalCount,
|
|
111
|
+
model,
|
|
112
|
+
dimensions: dimensions || 'default',
|
|
113
|
+
rechunk: !!options.rechunk,
|
|
114
|
+
}));
|
|
115
|
+
return { success: true, dryRun: true, count: totalCount };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!quiet) {
|
|
119
|
+
p.log.info(`Found ${totalCount} document${totalCount === 1 ? '' : 's'} to refresh`);
|
|
120
|
+
p.log.info(`Target model: ${model}${dimLabel}${rechunkLabel}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Dry run - stop here
|
|
124
|
+
if (options.dryRun) {
|
|
125
|
+
if (!quiet) {
|
|
126
|
+
p.log.info('Dry run - no documents modified.');
|
|
127
|
+
p.outro(`Would refresh ${totalCount} document${totalCount === 1 ? '' : 's'}.`);
|
|
128
|
+
}
|
|
129
|
+
return { success: true, dryRun: true, count: totalCount };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Confirm unless --force
|
|
133
|
+
if (!options.force && !options.json) {
|
|
134
|
+
const confirmed = await p.confirm({
|
|
135
|
+
message: `Re-embed ${totalCount} document${totalCount === 1 ? '' : 's'}? This will update the embeddings in-place.`,
|
|
136
|
+
initialValue: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
140
|
+
p.log.info('Refresh cancelled.');
|
|
141
|
+
p.outro('No documents modified.');
|
|
142
|
+
return { success: false, cancelled: true };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Create embedder function
|
|
147
|
+
const embedder = async (texts) => {
|
|
148
|
+
const result = await generateEmbeddings(texts, {
|
|
149
|
+
model,
|
|
150
|
+
dimensions,
|
|
151
|
+
inputType: 'document',
|
|
152
|
+
});
|
|
153
|
+
return result.embeddings;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Process documents
|
|
157
|
+
let processed = 0;
|
|
158
|
+
let errors = 0;
|
|
159
|
+
const cursor = collection.find(filter);
|
|
160
|
+
let batch = [];
|
|
161
|
+
|
|
162
|
+
const spinner = !quiet ? p.spinner() : null;
|
|
163
|
+
if (spinner) spinner.start('Processing documents...');
|
|
164
|
+
|
|
165
|
+
while (await cursor.hasNext()) {
|
|
166
|
+
const doc = await cursor.next();
|
|
167
|
+
|
|
168
|
+
if (options.rechunk) {
|
|
169
|
+
// Re-chunk the document
|
|
170
|
+
const chunks = rechunkDocument(doc, options);
|
|
171
|
+
batch.push(...chunks);
|
|
172
|
+
} else {
|
|
173
|
+
batch.push(doc);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Process when batch is full
|
|
177
|
+
if (batch.length >= batchSize) {
|
|
178
|
+
try {
|
|
179
|
+
const updated = await processBatch(batch, embedder, { field, model });
|
|
180
|
+
|
|
181
|
+
// Replace documents in database
|
|
182
|
+
for (const updatedDoc of updated) {
|
|
183
|
+
if (options.rechunk && updatedDoc.metadata?.originalId) {
|
|
184
|
+
// For rechunked docs, insert new and delete original later
|
|
185
|
+
await collection.insertOne(updatedDoc);
|
|
186
|
+
} else {
|
|
187
|
+
// Update in place
|
|
188
|
+
await collection.updateOne(
|
|
189
|
+
{ _id: updatedDoc._id },
|
|
190
|
+
{ $set: { [field]: updatedDoc[field], _model: model, _embeddedAt: new Date() } }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
processed += batch.length;
|
|
196
|
+
if (spinner) spinner.message(`Processed ${processed}/${totalCount} documents...`);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
errors += batch.length;
|
|
199
|
+
if (!quiet) {
|
|
200
|
+
p.log.warn(`Batch error: ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
batch = [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Process remaining batch
|
|
208
|
+
if (batch.length > 0) {
|
|
209
|
+
try {
|
|
210
|
+
const updated = await processBatch(batch, embedder, { field, model });
|
|
211
|
+
|
|
212
|
+
for (const updatedDoc of updated) {
|
|
213
|
+
if (options.rechunk && updatedDoc.metadata?.originalId) {
|
|
214
|
+
await collection.insertOne(updatedDoc);
|
|
215
|
+
} else {
|
|
216
|
+
await collection.updateOne(
|
|
217
|
+
{ _id: updatedDoc._id },
|
|
218
|
+
{ $set: { [field]: updatedDoc[field], _model: model, _embeddedAt: new Date() } }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
processed += batch.length;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
errors += batch.length;
|
|
226
|
+
if (!quiet) {
|
|
227
|
+
p.log.warn(`Batch error: ${err.message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If rechunking, delete original documents
|
|
233
|
+
if (options.rechunk) {
|
|
234
|
+
const originalIds = await collection.distinct('metadata.originalId', filter);
|
|
235
|
+
if (originalIds.length > 0) {
|
|
236
|
+
// Convert string IDs back to ObjectIds for deletion
|
|
237
|
+
const { ObjectId } = require('mongodb');
|
|
238
|
+
const objectIds = originalIds
|
|
239
|
+
.filter(id => id)
|
|
240
|
+
.map(id => {
|
|
241
|
+
try { return new ObjectId(id); } catch { return null; }
|
|
242
|
+
})
|
|
243
|
+
.filter(id => id);
|
|
244
|
+
|
|
245
|
+
if (objectIds.length > 0) {
|
|
246
|
+
await collection.deleteMany({ _id: { $in: objectIds } });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (spinner) spinner.stop('Processing complete.');
|
|
252
|
+
|
|
253
|
+
// Update project config if model/dimensions changed
|
|
254
|
+
const configUpdated = (model !== project.model) || (dimensions && dimensions !== project.dimensions);
|
|
255
|
+
if (configUpdated && !options.json) {
|
|
256
|
+
try {
|
|
257
|
+
saveProject({
|
|
258
|
+
...project,
|
|
259
|
+
model,
|
|
260
|
+
...(dimensions && { dimensions }),
|
|
261
|
+
});
|
|
262
|
+
if (!quiet) {
|
|
263
|
+
p.log.info('Updated .vai.json with new model/dimensions.');
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// Ignore save errors
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (options.json) {
|
|
271
|
+
console.log(JSON.stringify({ success: true, processed, errors }));
|
|
272
|
+
} else {
|
|
273
|
+
if (errors > 0) {
|
|
274
|
+
p.log.warn(`Refreshed ${processed} documents with ${errors} errors.`);
|
|
275
|
+
} else {
|
|
276
|
+
p.log.success(`Refreshed ${processed} document${processed === 1 ? '' : 's'}.`);
|
|
277
|
+
}
|
|
278
|
+
p.outro('Refresh complete.');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { success: true, processed, errors };
|
|
282
|
+
|
|
283
|
+
} catch (err) {
|
|
284
|
+
if (options.json) {
|
|
285
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
286
|
+
} else {
|
|
287
|
+
p.log.error(`Refresh failed: ${err.message}`);
|
|
288
|
+
}
|
|
289
|
+
return { success: false, error: err.message };
|
|
290
|
+
} finally {
|
|
291
|
+
if (client) {
|
|
292
|
+
await close();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Register the refresh command with Commander.
|
|
299
|
+
*/
|
|
300
|
+
function register(program) {
|
|
301
|
+
program
|
|
302
|
+
.command('refresh')
|
|
303
|
+
.description('Re-embed documents with a new model, dimensions, or chunk settings')
|
|
304
|
+
.option('--db <database>', 'Database name')
|
|
305
|
+
.option('--collection <name>', 'Collection name')
|
|
306
|
+
.option('--field <name>', 'Embedding field name')
|
|
307
|
+
.option('-m, --model <model>', 'New embedding model')
|
|
308
|
+
.option('-d, --dimensions <n>', 'New dimensions', parseInt)
|
|
309
|
+
.option('--rechunk', 'Re-chunk text before re-embedding')
|
|
310
|
+
.option('-s, --strategy <strategy>', 'Chunk strategy (with --rechunk)')
|
|
311
|
+
.option('-c, --chunk-size <n>', 'Chunk size (with --rechunk)', parseInt)
|
|
312
|
+
.option('--overlap <n>', 'Chunk overlap (with --rechunk)', parseInt)
|
|
313
|
+
.option('--batch-size <n>', 'Texts per API call (default: 25)', parseInt)
|
|
314
|
+
.option('--filter <json>', 'Only refresh matching documents (JSON)')
|
|
315
|
+
.option('--force', 'Skip confirmation prompt')
|
|
316
|
+
.option('--dry-run', 'Show plan without executing')
|
|
317
|
+
.option('--json', 'Machine-readable output')
|
|
318
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
319
|
+
.action(refresh);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = { register, refresh };
|
|
@@ -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 };
|