ctxpkg 0.0.1
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 +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, basename } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import type { Command } from 'commander';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
formatHeader,
|
|
8
|
+
formatSuccess,
|
|
9
|
+
formatError,
|
|
10
|
+
formatInfo,
|
|
11
|
+
formatWarning,
|
|
12
|
+
formatTableHeader,
|
|
13
|
+
formatTableRow,
|
|
14
|
+
withErrorHandling,
|
|
15
|
+
chalk,
|
|
16
|
+
} from './cli.utils.ts';
|
|
17
|
+
import { createCliClient } from './cli.client.ts';
|
|
18
|
+
|
|
19
|
+
import { Services } from '#root/utils/utils.services.ts';
|
|
20
|
+
import { CollectionsService } from '#root/collections/collections.ts';
|
|
21
|
+
import type { CollectionSpec, Manifest } from '#root/collections/collections.schemas.ts';
|
|
22
|
+
import { config } from '#root/config/config.ts';
|
|
23
|
+
import type { GetBackendAPIResponse } from '#root/backend/backend.types.ts';
|
|
24
|
+
|
|
25
|
+
type SyncResult = GetBackendAPIResponse<'collections', 'sync'>;
|
|
26
|
+
|
|
27
|
+
const createCollectionsCli = (command: Command) => {
|
|
28
|
+
command.description('Manage collection packages for AI context');
|
|
29
|
+
|
|
30
|
+
// collections init
|
|
31
|
+
command
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('Create a new project config file')
|
|
34
|
+
.option('-f, --force', 'Overwrite existing file')
|
|
35
|
+
.action(
|
|
36
|
+
withErrorHandling(async (options: { force?: boolean }) => {
|
|
37
|
+
const services = new Services();
|
|
38
|
+
try {
|
|
39
|
+
const collectionsService = services.get(CollectionsService);
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
const configFile = (config as any).get('project.configFile') as string;
|
|
42
|
+
|
|
43
|
+
if (collectionsService.projectConfigExists() && !options.force) {
|
|
44
|
+
formatError(`${configFile} already exists. Use --force to overwrite.`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
collectionsService.initProjectConfig(process.cwd(), options.force);
|
|
49
|
+
formatSuccess(`Created ${configFile}`);
|
|
50
|
+
} finally {
|
|
51
|
+
await services.destroy();
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// collections add
|
|
57
|
+
command
|
|
58
|
+
.command('add')
|
|
59
|
+
.argument('<name>', 'Name/alias for the collection')
|
|
60
|
+
.argument(
|
|
61
|
+
'<url>',
|
|
62
|
+
'Manifest or bundle URL (supports https://, file://, git+https://, git+ssh://, or relative paths)',
|
|
63
|
+
)
|
|
64
|
+
.description('Add a collection to project or global config')
|
|
65
|
+
.option('-g, --global', 'Add to global config instead of project config')
|
|
66
|
+
.action(
|
|
67
|
+
withErrorHandling(async (name: string, url: string, options: { global?: boolean }) => {
|
|
68
|
+
const services = new Services();
|
|
69
|
+
try {
|
|
70
|
+
const collectionsService = services.get(CollectionsService);
|
|
71
|
+
|
|
72
|
+
if (!options.global && !collectionsService.projectConfigExists()) {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
const configFile = (config as any).get('project.configFile') as string;
|
|
75
|
+
formatError(`No ${configFile} found. Run 'collections init' first, or use -g for global.`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Normalize local paths to file:// URLs (but not git URLs)
|
|
80
|
+
let normalizedUrl = url;
|
|
81
|
+
if (
|
|
82
|
+
!url.startsWith('http://') &&
|
|
83
|
+
!url.startsWith('https://') &&
|
|
84
|
+
!url.startsWith('file://') &&
|
|
85
|
+
!url.startsWith('git+')
|
|
86
|
+
) {
|
|
87
|
+
normalizedUrl = `file://${url}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const spec: CollectionSpec = { url: normalizedUrl };
|
|
91
|
+
collectionsService.addToConfig(name, spec, { global: options.global });
|
|
92
|
+
|
|
93
|
+
const scope = options.global ? 'global config' : 'project config';
|
|
94
|
+
formatSuccess(`Added collection "${name}" to ${scope} (${normalizedUrl})`);
|
|
95
|
+
} finally {
|
|
96
|
+
await services.destroy();
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// collections remove
|
|
102
|
+
command
|
|
103
|
+
.command('remove')
|
|
104
|
+
.argument('<name>', 'Name of the collection to remove')
|
|
105
|
+
.description('Remove a collection from project or global config')
|
|
106
|
+
.option('-g, --global', 'Remove from global config instead of project config')
|
|
107
|
+
.option('--drop', 'Also drop indexed data from database')
|
|
108
|
+
.action(
|
|
109
|
+
withErrorHandling(async (name: string, options: { global?: boolean; drop?: boolean }) => {
|
|
110
|
+
const services = new Services();
|
|
111
|
+
const client = options.drop ? await createCliClient() : null;
|
|
112
|
+
try {
|
|
113
|
+
const collectionsService = services.get(CollectionsService);
|
|
114
|
+
|
|
115
|
+
if (options.global) {
|
|
116
|
+
if (!collectionsService.globalConfigExists()) {
|
|
117
|
+
formatError('No global config found.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (!collectionsService.projectConfigExists()) {
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
const configFile = (config as any).get('project.configFile') as string;
|
|
124
|
+
formatError(`No ${configFile} found.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const spec = collectionsService.getFromConfig(name, { global: options.global });
|
|
130
|
+
if (!spec) {
|
|
131
|
+
const scope = options.global ? 'global config' : 'project config';
|
|
132
|
+
formatError(`Collection "${name}" not found in ${scope}.`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.drop && client) {
|
|
137
|
+
const collectionId = collectionsService.computeCollectionId(spec);
|
|
138
|
+
await client.documents.dropCollection({ collection: collectionId });
|
|
139
|
+
await client.collections.delete({ id: collectionId });
|
|
140
|
+
formatInfo(`Dropped indexed data for "${name}"`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
collectionsService.removeFromConfig(name, { global: options.global });
|
|
144
|
+
const scope = options.global ? 'global config' : 'project config';
|
|
145
|
+
formatSuccess(`Removed "${name}" from ${scope}`);
|
|
146
|
+
} finally {
|
|
147
|
+
if (client) {
|
|
148
|
+
await client.disconnect();
|
|
149
|
+
}
|
|
150
|
+
await services.destroy();
|
|
151
|
+
}
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// collections list
|
|
156
|
+
command
|
|
157
|
+
.command('list')
|
|
158
|
+
.alias('ls')
|
|
159
|
+
.description('List configured collections and their status')
|
|
160
|
+
.option('-g, --global', 'Show only global collections')
|
|
161
|
+
.option('--no-global', 'Show only local collections')
|
|
162
|
+
.action(
|
|
163
|
+
withErrorHandling(async (options: { global?: boolean }) => {
|
|
164
|
+
const services = new Services();
|
|
165
|
+
const client = await createCliClient();
|
|
166
|
+
try {
|
|
167
|
+
const collectionsService = services.get(CollectionsService);
|
|
168
|
+
|
|
169
|
+
// Determine which collections to show
|
|
170
|
+
let entries: [string, CollectionSpec, 'local' | 'global'][] = [];
|
|
171
|
+
|
|
172
|
+
if (options.global === true) {
|
|
173
|
+
// Show only global
|
|
174
|
+
if (!collectionsService.globalConfigExists()) {
|
|
175
|
+
formatInfo('No global collections configured. Use "collections add -g" to add one.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const globalConfig = collectionsService.readGlobalConfig();
|
|
179
|
+
entries = Object.entries(globalConfig.collections).map(([name, spec]) => [name, spec, 'global']);
|
|
180
|
+
} else if (options.global === false) {
|
|
181
|
+
// Show only local
|
|
182
|
+
if (!collectionsService.projectConfigExists()) {
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
184
|
+
const configFile = (config as any).get('project.configFile') as string;
|
|
185
|
+
formatInfo(`No ${configFile} found. Run 'collections init' first.`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const projectConfig = collectionsService.readProjectConfig();
|
|
189
|
+
entries = Object.entries(projectConfig.collections).map(([name, spec]) => [name, spec, 'local']);
|
|
190
|
+
} else {
|
|
191
|
+
// Show both (default)
|
|
192
|
+
const allCollections = collectionsService.getAllCollections();
|
|
193
|
+
entries = Array.from(allCollections.entries()).map(([name, { spec, source }]) => [name, spec, source]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (entries.length === 0) {
|
|
197
|
+
formatInfo('No collections configured. Use "collections add" to add one.');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
formatHeader('Collections');
|
|
202
|
+
|
|
203
|
+
const maxNameLen = Math.max(...entries.map(([name]) => name.length), 10);
|
|
204
|
+
const urlLengths = entries.map(([, spec]) => spec.url.length);
|
|
205
|
+
const maxUrlLen = Math.min(Math.max(...urlLengths, 20), 45);
|
|
206
|
+
const showSource = options.global === undefined; // Show source column when showing both
|
|
207
|
+
|
|
208
|
+
const columns = [
|
|
209
|
+
{ name: 'Name', width: maxNameLen },
|
|
210
|
+
{ name: 'URL', width: maxUrlLen },
|
|
211
|
+
...(showSource ? [{ name: 'Source', width: 8 }] : []),
|
|
212
|
+
{ name: 'Status', width: 14 },
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
formatTableHeader(columns);
|
|
216
|
+
|
|
217
|
+
for (const [name, spec, source] of entries) {
|
|
218
|
+
const status = await client.collections.getSyncStatus({ spec });
|
|
219
|
+
const statusText = status === 'synced' ? chalk.green('✓ synced') : chalk.yellow('⚠ not synced');
|
|
220
|
+
|
|
221
|
+
let url = spec.url;
|
|
222
|
+
if (url.length > maxUrlLen) {
|
|
223
|
+
url = url.substring(0, maxUrlLen - 3) + '...';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const sourceColor = source === 'local' ? chalk.blue : chalk.magenta;
|
|
227
|
+
|
|
228
|
+
const row = [
|
|
229
|
+
{ value: name, width: maxNameLen, color: chalk.cyan },
|
|
230
|
+
{ value: url, width: maxUrlLen, color: chalk.white },
|
|
231
|
+
...(showSource ? [{ value: source, width: 8, color: sourceColor }] : []),
|
|
232
|
+
{ value: statusText, width: 14 },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
formatTableRow(row);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log();
|
|
239
|
+
} finally {
|
|
240
|
+
await client.disconnect();
|
|
241
|
+
await services.destroy();
|
|
242
|
+
}
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// collections sync
|
|
247
|
+
command
|
|
248
|
+
.command('sync')
|
|
249
|
+
.argument('[name]', 'Name of specific collection to sync (omit for all)')
|
|
250
|
+
.description('Sync collection(s) from config')
|
|
251
|
+
.option('-g, --global', 'Sync only global collections')
|
|
252
|
+
.option('--no-global', 'Sync only local collections')
|
|
253
|
+
.option('-f, --force', 'Re-index all documents (ignore hash cache)')
|
|
254
|
+
.option('--dry-run', 'Show what would happen without making changes')
|
|
255
|
+
.action(
|
|
256
|
+
withErrorHandling(
|
|
257
|
+
async (name: string | undefined, options: { global?: boolean; force?: boolean; dryRun?: boolean }) => {
|
|
258
|
+
const services = new Services();
|
|
259
|
+
try {
|
|
260
|
+
const collectionsService = services.get(CollectionsService);
|
|
261
|
+
|
|
262
|
+
// Build list of collections to sync based on options
|
|
263
|
+
let toSync: [string, CollectionSpec, 'local' | 'global'][] = [];
|
|
264
|
+
|
|
265
|
+
if (name) {
|
|
266
|
+
// Syncing a specific collection by name
|
|
267
|
+
if (options.global === true) {
|
|
268
|
+
// Explicitly global
|
|
269
|
+
const spec = collectionsService.getFromConfig(name, { global: true });
|
|
270
|
+
if (!spec) {
|
|
271
|
+
formatError(`Collection "${name}" not found in global config.`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
toSync = [[name, spec, 'global']];
|
|
275
|
+
} else if (options.global === false) {
|
|
276
|
+
// Explicitly local
|
|
277
|
+
const spec = collectionsService.getFromConfig(name, { global: false });
|
|
278
|
+
if (!spec) {
|
|
279
|
+
formatError(`Collection "${name}" not found in project config.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
toSync = [[name, spec, 'local']];
|
|
283
|
+
} else {
|
|
284
|
+
// Search local first, then global
|
|
285
|
+
const localSpec = collectionsService.getFromConfig(name, { global: false });
|
|
286
|
+
if (localSpec) {
|
|
287
|
+
toSync = [[name, localSpec, 'local']];
|
|
288
|
+
} else {
|
|
289
|
+
const globalSpec = collectionsService.getFromConfig(name, { global: true });
|
|
290
|
+
if (globalSpec) {
|
|
291
|
+
toSync = [[name, globalSpec, 'global']];
|
|
292
|
+
} else {
|
|
293
|
+
formatError(`Collection "${name}" not found in project or global config.`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
// Syncing all collections
|
|
300
|
+
if (options.global === true) {
|
|
301
|
+
// Only global
|
|
302
|
+
if (!collectionsService.globalConfigExists()) {
|
|
303
|
+
formatInfo('No global collections configured. Use "collections add -g" to add one.');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const globalConfig = collectionsService.readGlobalConfig();
|
|
307
|
+
toSync = Object.entries(globalConfig.collections).map(([n, spec]) => [n, spec, 'global']);
|
|
308
|
+
} else if (options.global === false) {
|
|
309
|
+
// Only local
|
|
310
|
+
if (!collectionsService.projectConfigExists()) {
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
312
|
+
const configFile = (config as any).get('project.configFile') as string;
|
|
313
|
+
formatError(`No ${configFile} found. Run 'collections init' first.`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const projectConfig = collectionsService.readProjectConfig();
|
|
317
|
+
toSync = Object.entries(projectConfig.collections).map(([n, spec]) => [n, spec, 'local']);
|
|
318
|
+
} else {
|
|
319
|
+
// Both local and global (default)
|
|
320
|
+
const allCollections = collectionsService.getAllCollections();
|
|
321
|
+
toSync = Array.from(allCollections.entries()).map(([n, { spec, source }]) => [n, spec, source]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (toSync.length === 0) {
|
|
326
|
+
formatInfo('No collections configured. Use "collections add" to add one.');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Handle dry-run mode separately
|
|
331
|
+
if (options.dryRun) {
|
|
332
|
+
formatWarning('Dry run mode - no changes will be made');
|
|
333
|
+
console.log();
|
|
334
|
+
|
|
335
|
+
for (const [collectionName, , source] of toSync) {
|
|
336
|
+
console.log(chalk.bold(`Syncing ${collectionName}`) + chalk.dim(` (${source})...`));
|
|
337
|
+
formatInfo(' Would sync this collection');
|
|
338
|
+
console.log();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(chalk.green.bold('All collections synced.'));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Actually sync collections
|
|
346
|
+
const client = await createCliClient();
|
|
347
|
+
try {
|
|
348
|
+
for (const [collectionName, spec, source] of toSync) {
|
|
349
|
+
console.log(chalk.bold(`Syncing ${collectionName}`) + chalk.dim(` (${source})...`));
|
|
350
|
+
|
|
351
|
+
const result = await client.collections.sync({
|
|
352
|
+
name: collectionName,
|
|
353
|
+
spec,
|
|
354
|
+
cwd: process.cwd(),
|
|
355
|
+
force: options.force,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
printSyncResult(collectionName, result);
|
|
359
|
+
}
|
|
360
|
+
} finally {
|
|
361
|
+
await client.disconnect();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
console.log(chalk.green.bold('All collections synced.'));
|
|
365
|
+
} finally {
|
|
366
|
+
await services.destroy();
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// collections manifest (subcommand group)
|
|
373
|
+
const manifestCmd = command.command('manifest').description('Manifest management commands');
|
|
374
|
+
|
|
375
|
+
// collections manifest init
|
|
376
|
+
manifestCmd
|
|
377
|
+
.command('init')
|
|
378
|
+
.description('Create a manifest.json for publishing')
|
|
379
|
+
.option('-n, --name <name>', 'Package name')
|
|
380
|
+
.option('-v, --version <version>', 'Package version', '1.0.0')
|
|
381
|
+
.action(
|
|
382
|
+
withErrorHandling(async (options: { name?: string; version?: string }) => {
|
|
383
|
+
const manifestPath = resolve(process.cwd(), 'manifest.json');
|
|
384
|
+
|
|
385
|
+
if (existsSync(manifestPath)) {
|
|
386
|
+
formatError('manifest.json already exists');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const manifest: Manifest = {
|
|
391
|
+
name: options.name || basename(process.cwd()),
|
|
392
|
+
version: options.version || '1.0.0',
|
|
393
|
+
description: '',
|
|
394
|
+
sources: {
|
|
395
|
+
glob: ['**/*.md'],
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
400
|
+
formatSuccess('Created manifest.json');
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// collections pack
|
|
405
|
+
command
|
|
406
|
+
.command('pack')
|
|
407
|
+
.description('Create a distributable bundle from a manifest')
|
|
408
|
+
.option('-m, --manifest <path>', 'Path to manifest file', 'manifest.json')
|
|
409
|
+
.option('-o, --output <path>', 'Output path for bundle')
|
|
410
|
+
.action(
|
|
411
|
+
withErrorHandling(async (options: { manifest: string; output?: string }) => {
|
|
412
|
+
const { createHash } = await import('node:crypto');
|
|
413
|
+
const { readFile, glob } = await import('node:fs/promises');
|
|
414
|
+
const tar = await import('tar');
|
|
415
|
+
|
|
416
|
+
const manifestPath = resolve(process.cwd(), options.manifest);
|
|
417
|
+
|
|
418
|
+
if (!existsSync(manifestPath)) {
|
|
419
|
+
formatError(`Manifest not found: ${manifestPath}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const manifestContent = await readFile(manifestPath, 'utf8');
|
|
424
|
+
const manifest = JSON.parse(manifestContent) as Manifest;
|
|
425
|
+
|
|
426
|
+
formatHeader('Creating Bundle');
|
|
427
|
+
formatInfo(`Name: ${chalk.cyan(manifest.name)}`);
|
|
428
|
+
formatInfo(`Version: ${chalk.cyan(manifest.version)}`);
|
|
429
|
+
console.log();
|
|
430
|
+
|
|
431
|
+
const manifestDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'));
|
|
432
|
+
|
|
433
|
+
// Collect files to include
|
|
434
|
+
const filesToInclude: string[] = ['manifest.json'];
|
|
435
|
+
|
|
436
|
+
if ('glob' in manifest.sources) {
|
|
437
|
+
for (const pattern of manifest.sources.glob) {
|
|
438
|
+
for await (const file of glob(pattern, { cwd: manifestDir || process.cwd() })) {
|
|
439
|
+
filesToInclude.push(file);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} else if ('files' in manifest.sources) {
|
|
443
|
+
for (const entry of manifest.sources.files) {
|
|
444
|
+
const path = typeof entry === 'string' ? entry : entry.path;
|
|
445
|
+
if (path) {
|
|
446
|
+
filesToInclude.push(path);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
formatInfo(`Including ${filesToInclude.length} files`);
|
|
452
|
+
|
|
453
|
+
// Create bundle
|
|
454
|
+
const outputPath = options.output || `${manifest.name}-${manifest.version}.tar.gz`;
|
|
455
|
+
const fullOutputPath = resolve(process.cwd(), outputPath);
|
|
456
|
+
|
|
457
|
+
await tar.create(
|
|
458
|
+
{
|
|
459
|
+
gzip: true,
|
|
460
|
+
file: fullOutputPath,
|
|
461
|
+
cwd: manifestDir || process.cwd(),
|
|
462
|
+
},
|
|
463
|
+
filesToInclude,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// Calculate hash
|
|
467
|
+
const bundleContent = await readFile(fullOutputPath);
|
|
468
|
+
const hash = createHash('sha256').update(bundleContent).digest('hex');
|
|
469
|
+
|
|
470
|
+
console.log();
|
|
471
|
+
formatSuccess(`Created ${outputPath}`);
|
|
472
|
+
formatInfo(`SHA256: ${chalk.dim(hash)}`);
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const printSyncResult = (name: string, result: SyncResult) => {
|
|
478
|
+
const parts = [];
|
|
479
|
+
if (result.added > 0) parts.push(chalk.green(`${result.added} added`));
|
|
480
|
+
if (result.updated > 0) parts.push(chalk.yellow(`${result.updated} updated`));
|
|
481
|
+
if (result.removed > 0) parts.push(chalk.red(`${result.removed} removed`));
|
|
482
|
+
|
|
483
|
+
if (parts.length === 0) {
|
|
484
|
+
console.log(chalk.green(` ✓ ${result.total} documents (no changes)`));
|
|
485
|
+
} else {
|
|
486
|
+
console.log(chalk.green(` ✓ ${result.total} documents (${parts.join(', ')})`));
|
|
487
|
+
}
|
|
488
|
+
console.log();
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export { createCollectionsCli };
|