trellis 2.0.13 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/embeddings/index.js +1 -1
  3. package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
  4. package/package.json +2 -10
  5. package/dist/transformers.node-bx3q9d7k.js +0 -33130
  6. package/src/cli/index.ts +0 -3356
  7. package/src/core/agents/harness.ts +0 -380
  8. package/src/core/agents/index.ts +0 -18
  9. package/src/core/agents/types.ts +0 -90
  10. package/src/core/index.ts +0 -118
  11. package/src/core/kernel/middleware.ts +0 -44
  12. package/src/core/kernel/trellis-kernel.ts +0 -593
  13. package/src/core/ontology/builtins.ts +0 -248
  14. package/src/core/ontology/index.ts +0 -34
  15. package/src/core/ontology/registry.ts +0 -209
  16. package/src/core/ontology/types.ts +0 -124
  17. package/src/core/ontology/validator.ts +0 -382
  18. package/src/core/persist/backend.ts +0 -74
  19. package/src/core/persist/sqlite-backend.ts +0 -298
  20. package/src/core/plugins/index.ts +0 -17
  21. package/src/core/plugins/registry.ts +0 -322
  22. package/src/core/plugins/types.ts +0 -126
  23. package/src/core/query/datalog.ts +0 -188
  24. package/src/core/query/engine.ts +0 -370
  25. package/src/core/query/index.ts +0 -34
  26. package/src/core/query/parser.ts +0 -481
  27. package/src/core/query/types.ts +0 -200
  28. package/src/core/store/eav-store.ts +0 -467
  29. package/src/decisions/auto-capture.ts +0 -136
  30. package/src/decisions/hooks.ts +0 -163
  31. package/src/decisions/index.ts +0 -261
  32. package/src/decisions/types.ts +0 -103
  33. package/src/embeddings/auto-embed.ts +0 -248
  34. package/src/embeddings/chunker.ts +0 -327
  35. package/src/embeddings/index.ts +0 -48
  36. package/src/embeddings/model.ts +0 -112
  37. package/src/embeddings/search.ts +0 -305
  38. package/src/embeddings/store.ts +0 -313
  39. package/src/embeddings/types.ts +0 -92
  40. package/src/engine.ts +0 -1125
  41. package/src/garden/cluster.ts +0 -330
  42. package/src/garden/garden.ts +0 -306
  43. package/src/garden/index.ts +0 -29
  44. package/src/git/git-exporter.ts +0 -286
  45. package/src/git/git-importer.ts +0 -329
  46. package/src/git/git-reader.ts +0 -189
  47. package/src/git/index.ts +0 -22
  48. package/src/identity/governance.ts +0 -211
  49. package/src/identity/identity.ts +0 -224
  50. package/src/identity/index.ts +0 -30
  51. package/src/identity/signing-middleware.ts +0 -97
  52. package/src/index.ts +0 -29
  53. package/src/links/index.ts +0 -49
  54. package/src/links/lifecycle.ts +0 -400
  55. package/src/links/parser.ts +0 -484
  56. package/src/links/ref-index.ts +0 -186
  57. package/src/links/resolver.ts +0 -314
  58. package/src/links/types.ts +0 -108
  59. package/src/mcp/index.ts +0 -22
  60. package/src/mcp/server.ts +0 -1278
  61. package/src/semantic/csharp-parser.ts +0 -493
  62. package/src/semantic/go-parser.ts +0 -585
  63. package/src/semantic/index.ts +0 -34
  64. package/src/semantic/java-parser.ts +0 -456
  65. package/src/semantic/python-parser.ts +0 -659
  66. package/src/semantic/ruby-parser.ts +0 -446
  67. package/src/semantic/rust-parser.ts +0 -784
  68. package/src/semantic/semantic-merge.ts +0 -210
  69. package/src/semantic/ts-parser.ts +0 -681
  70. package/src/semantic/types.ts +0 -175
  71. package/src/sync/http-transport.ts +0 -144
  72. package/src/sync/index.ts +0 -43
  73. package/src/sync/memory-transport.ts +0 -66
  74. package/src/sync/multi-repo.ts +0 -200
  75. package/src/sync/reconciler.ts +0 -237
  76. package/src/sync/sync-engine.ts +0 -258
  77. package/src/sync/types.ts +0 -104
  78. package/src/sync/ws-transport.ts +0 -145
  79. package/src/ui/client.html +0 -695
  80. package/src/ui/server.ts +0 -419
  81. package/src/vcs/blob-store.ts +0 -124
  82. package/src/vcs/branch.ts +0 -150
  83. package/src/vcs/checkpoint.ts +0 -64
  84. package/src/vcs/decompose.ts +0 -469
  85. package/src/vcs/diff.ts +0 -409
  86. package/src/vcs/engine-context.ts +0 -26
  87. package/src/vcs/index.ts +0 -23
  88. package/src/vcs/issue.ts +0 -800
  89. package/src/vcs/merge.ts +0 -425
  90. package/src/vcs/milestone.ts +0 -124
  91. package/src/vcs/ops.ts +0 -59
  92. package/src/vcs/types.ts +0 -213
  93. package/src/vcs/vcs-middleware.ts +0 -81
  94. package/src/watcher/fs-watcher.ts +0 -255
  95. package/src/watcher/index.ts +0 -9
  96. package/src/watcher/ingestion.ts +0 -116
package/src/cli/index.ts DELETED
@@ -1,3356 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * TrellisVCS CLI
5
- *
6
- * Commands:
7
- * init Initialize a new TrellisVCS repository
8
- * status Show current repository status
9
- * log Show operation history
10
- * watch Start file watcher (foreground)
11
- * files List tracked files
12
- * import Import from a Git repository
13
- */
14
-
15
- import { Command } from 'commander';
16
- import chalk from 'chalk';
17
- import { resolve, join } from 'path';
18
- import { TrellisVcsEngine } from '../engine.js';
19
- import { TrellisKernel } from '../core/kernel/trellis-kernel.js';
20
- import { SqliteKernelBackend } from '../core/persist/sqlite-backend.js';
21
- import { QueryEngine, parseQuery, parseSimple } from '../core/query/index.js';
22
- import {
23
- OntologyRegistry,
24
- validateStore,
25
- builtinOntologies,
26
- } from '../core/ontology/index.js';
27
- import { buildRAGContext } from '../embeddings/auto-embed.js';
28
- import { VectorStore } from '../embeddings/store.js';
29
- import { embed } from '../embeddings/model.js';
30
- import { EmbeddingManager } from '../embeddings/search.js';
31
- import { importFromGit } from '../git/git-importer.js';
32
- import { exportToGit } from '../git/git-exporter.js';
33
- import {
34
- createIdentity,
35
- saveIdentity,
36
- loadIdentity,
37
- hasIdentity,
38
- toPublicIdentity,
39
- } from '../identity/index.js';
40
-
41
- const program = new Command();
42
-
43
- program
44
- .name('trellis')
45
- .description('TrellisVCS — graph-native, code-first version control')
46
- .version('0.1.0');
47
-
48
- function requireRepo(rootPath: string): void {
49
- if (!TrellisVcsEngine.isRepo(rootPath)) {
50
- console.error(
51
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
52
- );
53
- process.exit(1);
54
- }
55
- }
56
-
57
- // ---------------------------------------------------------------------------
58
- // trellis init
59
- // ---------------------------------------------------------------------------
60
-
61
- program
62
- .command('init')
63
- .description(
64
- 'Initialize a new TrellisVCS repository in the current directory',
65
- )
66
- .option('-p, --path <path>', 'Path to initialize', '.')
67
- .action(async (opts) => {
68
- const rootPath = resolve(opts.path);
69
-
70
- if (TrellisVcsEngine.isRepo(rootPath)) {
71
- console.log(chalk.yellow('Already a TrellisVCS repository.'));
72
- return;
73
- }
74
-
75
- const engine = new TrellisVcsEngine({ rootPath });
76
- let renderedProgress = false;
77
- const result = await engine.initRepo({
78
- onProgress: (progress) => {
79
- renderedProgress = true;
80
- if (progress.phase === 'done') {
81
- process.stdout.write('\r\x1b[2K');
82
- return;
83
- }
84
-
85
- const label =
86
- progress.phase === 'discovering'
87
- ? 'Discovering…'
88
- : progress.phase === 'hashing'
89
- ? 'Hashing…'
90
- : 'Recording…';
91
-
92
- process.stdout.write(
93
- `\r\x1b[2K ${chalk.dim(label)} ${progress.message}`,
94
- );
95
- },
96
- });
97
-
98
- if (renderedProgress) {
99
- process.stdout.write('\n');
100
- }
101
-
102
- console.log(chalk.green('✓ Initialized TrellisVCS repository'));
103
- console.log(` ${chalk.dim('Path:')} ${rootPath}`);
104
- console.log(
105
- ` ${chalk.dim('Ops:')} ${result.opsCreated} initial operations recorded`,
106
- );
107
- console.log(` ${chalk.dim('Config:')} .trellis/config.json`);
108
- console.log(` ${chalk.dim('Op log:')} .trellis/ops.json`);
109
- console.log();
110
- console.log(
111
- chalk.dim(
112
- 'The causal stream is now recording. Every file change will be tracked.',
113
- ),
114
- );
115
- });
116
-
117
- // ---------------------------------------------------------------------------
118
- // trellis repair
119
- // ---------------------------------------------------------------------------
120
-
121
- program
122
- .command('repair')
123
- .description('Attempt to repair a corrupted .trellis/ops.json file')
124
- .option('-p, --path <path>', 'Repository path', '.')
125
- .action((opts) => {
126
- const rootPath = resolve(opts.path);
127
- requireRepo(rootPath);
128
-
129
- console.log(chalk.yellow('Attempting to repair ops.json...'));
130
- const result = TrellisVcsEngine.repair(rootPath);
131
-
132
- if (result.lost === -1) {
133
- console.log(
134
- chalk.red(
135
- 'Could not recover any ops. A corrupted backup was saved as ops.json.corrupted',
136
- ),
137
- );
138
- } else if (result.recovered > 0) {
139
- console.log(chalk.green(`✓ Recovered ${result.recovered} ops.`));
140
- } else {
141
- console.log(chalk.green('ops.json is already valid. No repair needed.'));
142
- }
143
- });
144
-
145
- // ---------------------------------------------------------------------------
146
- // trellis status
147
- // ---------------------------------------------------------------------------
148
-
149
- program
150
- .command('status')
151
- .description('Show current repository status')
152
- .option('-p, --path <path>', 'Repository path', '.')
153
- .action(async (opts) => {
154
- const rootPath = resolve(opts.path);
155
-
156
- if (!TrellisVcsEngine.isRepo(rootPath)) {
157
- console.log(
158
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
159
- );
160
- process.exit(1);
161
- }
162
-
163
- const engine = new TrellisVcsEngine({ rootPath });
164
- engine.open();
165
- const st = engine.status();
166
-
167
- console.log(chalk.bold('TrellisVCS Status'));
168
- console.log();
169
- console.log(` ${chalk.dim('Branch:')} ${chalk.cyan(st.branch)}`);
170
- console.log(` ${chalk.dim('Total ops:')} ${st.totalOps}`);
171
- console.log(` ${chalk.dim('Tracked files:')} ${st.trackedFiles}`);
172
-
173
- if (st.lastOp) {
174
- console.log();
175
- console.log(
176
- ` ${chalk.dim('Last op:')} ${chalk.yellow(st.lastOp.kind)}`,
177
- );
178
- console.log(` ${chalk.dim(' at:')} ${st.lastOp.timestamp}`);
179
- if (st.lastOp.vcs?.filePath) {
180
- console.log(
181
- ` ${chalk.dim(' file:')} ${st.lastOp.vcs.filePath}`,
182
- );
183
- }
184
- }
185
-
186
- if (st.recentOps.length > 0) {
187
- console.log();
188
- console.log(chalk.dim(' Recent activity:'));
189
- // Show last 5 ops (excluding branch create for readability)
190
- const display = st.recentOps
191
- .filter((op) => op.kind !== 'vcs:branchCreate')
192
- .slice(-5);
193
- for (const op of display) {
194
- const kind = formatOpKind(op.kind);
195
- const file = op.vcs?.filePath ?? '';
196
- const time = formatRelativeTime(op.timestamp);
197
- console.log(` ${kind} ${chalk.white(file)} ${chalk.dim(time)}`);
198
- }
199
- }
200
- });
201
-
202
- // ---------------------------------------------------------------------------
203
- // trellis log
204
- // ---------------------------------------------------------------------------
205
-
206
- program
207
- .command('log')
208
- .description('Show operation history')
209
- .option('-p, --path <path>', 'Repository path', '.')
210
- .option('-n, --limit <n>', 'Number of ops to show', '20')
211
- .option('-f, --file <file>', 'Filter by file path')
212
- .action(async (opts) => {
213
- const rootPath = resolve(opts.path);
214
-
215
- if (!TrellisVcsEngine.isRepo(rootPath)) {
216
- console.log(
217
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
218
- );
219
- process.exit(1);
220
- }
221
-
222
- const engine = new TrellisVcsEngine({ rootPath });
223
- engine.open();
224
- const ops = engine.log({
225
- limit: parseInt(opts.limit, 10),
226
- filePath: opts.file,
227
- });
228
-
229
- if (ops.length === 0) {
230
- console.log(chalk.dim('No operations found.'));
231
- return;
232
- }
233
-
234
- console.log(chalk.bold(`Causal Stream — ${ops.length} ops`));
235
- console.log();
236
-
237
- for (const op of ops.reverse()) {
238
- const kind = formatOpKind(op.kind);
239
- const hash = chalk.dim(op.hash.slice(0, 28) + '…');
240
- const time = formatRelativeTime(op.timestamp);
241
- const file = op.vcs?.filePath ? chalk.white(op.vcs.filePath) : '';
242
- const rename = op.vcs?.oldFilePath
243
- ? chalk.dim(` (from ${op.vcs.oldFilePath})`)
244
- : '';
245
-
246
- console.log(` ${hash} ${kind} ${file}${rename} ${chalk.dim(time)}`);
247
- }
248
- });
249
-
250
- // ---------------------------------------------------------------------------
251
- // trellis files
252
- // ---------------------------------------------------------------------------
253
-
254
- program
255
- .command('files')
256
- .description('List all tracked files')
257
- .option('-p, --path <path>', 'Repository path', '.')
258
- .action(async (opts) => {
259
- const rootPath = resolve(opts.path);
260
-
261
- if (!TrellisVcsEngine.isRepo(rootPath)) {
262
- console.log(
263
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
264
- );
265
- process.exit(1);
266
- }
267
-
268
- const engine = new TrellisVcsEngine({ rootPath });
269
- engine.open();
270
- const files = engine.trackedFiles();
271
-
272
- if (files.length === 0) {
273
- console.log(chalk.dim('No tracked files.'));
274
- return;
275
- }
276
-
277
- console.log(chalk.bold(`Tracked Files — ${files.length}`));
278
- console.log();
279
-
280
- for (const f of files.sort((a, b) => a.path.localeCompare(b.path))) {
281
- const hash = f.contentHash
282
- ? chalk.dim(f.contentHash.slice(0, 12))
283
- : chalk.dim('(no hash)');
284
- console.log(` ${hash} ${f.path}`);
285
- }
286
- });
287
-
288
- // ---------------------------------------------------------------------------
289
- // trellis watch
290
- // ---------------------------------------------------------------------------
291
-
292
- program
293
- .command('watch')
294
- .description('Start file watcher (foreground, Ctrl+C to stop)')
295
- .option('-p, --path <path>', 'Repository path', '.')
296
- .action(async (opts) => {
297
- const rootPath = resolve(opts.path);
298
-
299
- if (!TrellisVcsEngine.isRepo(rootPath)) {
300
- console.log(
301
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
302
- );
303
- process.exit(1);
304
- }
305
-
306
- const engine = new TrellisVcsEngine({ rootPath });
307
- engine.open();
308
-
309
- console.log(
310
- chalk.green('✓ Watching for changes…') + chalk.dim(' (Ctrl+C to stop)'),
311
- );
312
- console.log();
313
-
314
- // Override engine's watch to add logging
315
- const originalWatch = engine.watch.bind(engine);
316
- engine.watch();
317
-
318
- // Keep process alive
319
- process.on('SIGINT', () => {
320
- engine.stop();
321
- console.log();
322
- console.log(chalk.dim('Watcher stopped.'));
323
- process.exit(0);
324
- });
325
- });
326
-
327
- // ---------------------------------------------------------------------------
328
- // trellis import
329
- // ---------------------------------------------------------------------------
330
-
331
- program
332
- .command('import')
333
- .description('Import from an existing Git repository')
334
- .requiredOption('--from <path>', 'Path to the Git repository to import from')
335
- .option('-p, --path <path>', 'Target TrellisVCS repository path', '.')
336
- .action(async (opts) => {
337
- const from = resolve(opts.from);
338
- const to = resolve(opts.path);
339
-
340
- console.log(chalk.dim(`Importing from Git: ${from}`));
341
- console.log(chalk.dim(`Target: ${to}`));
342
- console.log();
343
-
344
- try {
345
- const result = await importFromGit({
346
- from,
347
- to,
348
- onProgress: (p) => {
349
- if (p.phase === 'reading') {
350
- process.stdout.write(`\r ${chalk.dim('Reading…')} ${p.message}`);
351
- } else if (p.phase === 'importing') {
352
- process.stdout.write(
353
- `\r ${chalk.dim('Importing…')} ${p.current}/${p.total} commits`,
354
- );
355
- } else {
356
- process.stdout.write('\n');
357
- }
358
- },
359
- });
360
-
361
- console.log();
362
- console.log(chalk.green('✓ Git import complete'));
363
- console.log(` ${chalk.dim('Commits:')} ${result.commitsImported}`);
364
- console.log(` ${chalk.dim('Ops:')} ${result.opsCreated}`);
365
- console.log(` ${chalk.dim('Files:')} ${result.filesTracked}`);
366
- console.log(
367
- ` ${chalk.dim('Branches:')} ${result.branches.join(', ')}`,
368
- );
369
- console.log(
370
- ` ${chalk.dim('Duration:')} ${(result.duration / 1000).toFixed(1)}s`,
371
- );
372
- console.log();
373
- console.log(
374
- chalk.dim(
375
- 'Run `trellis status` or `trellis log` to explore the imported history.',
376
- ),
377
- );
378
- } catch (err: any) {
379
- console.error(chalk.red(`\nImport failed: ${err.message}`));
380
- process.exit(1);
381
- }
382
- });
383
-
384
- // ---------------------------------------------------------------------------
385
- // trellis export
386
- // ---------------------------------------------------------------------------
387
-
388
- program
389
- .command('export')
390
- .description('Export milestones to a Git repository')
391
- .requiredOption('--to <path>', 'Path to the target Git repository')
392
- .option('-p, --path <path>', 'Source TrellisVCS repository path', '.')
393
- .option('--author-name <name>', 'Author name for Git commits')
394
- .option('--author-email <email>', 'Author email for Git commits')
395
- .action(async (opts) => {
396
- const from = resolve(opts.path);
397
- const to = resolve(opts.to);
398
-
399
- if (!TrellisVcsEngine.isRepo(from)) {
400
- console.error(
401
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
402
- );
403
- process.exit(1);
404
- }
405
-
406
- console.log(chalk.dim(`Exporting from: ${from}`));
407
- console.log(chalk.dim(`Target Git repo: ${to}`));
408
- console.log();
409
-
410
- try {
411
- const result = await exportToGit({
412
- from,
413
- to,
414
- authorName: opts.authorName,
415
- authorEmail: opts.authorEmail,
416
- onProgress: (p) => {
417
- if (p.phase === 'preparing') {
418
- console.log(` ${chalk.dim(p.message)}`);
419
- } else if (p.phase === 'exporting') {
420
- process.stdout.write(
421
- `\r ${chalk.dim('Exporting…')} ${p.current}/${p.total} milestones`,
422
- );
423
- } else {
424
- process.stdout.write('\n');
425
- }
426
- },
427
- });
428
-
429
- console.log();
430
- console.log(chalk.green('✓ Git export complete'));
431
- console.log(` ${chalk.dim('Milestones:')} ${result.milestonesExported}`);
432
- console.log(` ${chalk.dim('Commits:')} ${result.commitsCreated}`);
433
- console.log(
434
- ` ${chalk.dim('Duration:')} ${(result.duration / 1000).toFixed(1)}s`,
435
- );
436
- } catch (err: any) {
437
- console.error(chalk.red(`\nExport failed: ${err.message}`));
438
- process.exit(1);
439
- }
440
- });
441
-
442
- // ---------------------------------------------------------------------------
443
- // trellis branch
444
- // ---------------------------------------------------------------------------
445
-
446
- program
447
- .command('branch')
448
- .description('Manage branches')
449
- .argument('[name]', 'Branch name to create or switch to')
450
- .option('-d, --delete <name>', 'Delete a branch')
451
- .option('-l, --list', 'List all branches')
452
- .option('-p, --path <path>', 'Repository path', '.')
453
- .action(async (name, opts) => {
454
- const rootPath = resolve(opts.path);
455
- if (!TrellisVcsEngine.isRepo(rootPath)) {
456
- console.error(
457
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
458
- );
459
- process.exit(1);
460
- }
461
-
462
- const engine = new TrellisVcsEngine({ rootPath });
463
- engine.open();
464
-
465
- // Delete
466
- if (opts.delete) {
467
- try {
468
- await engine.deleteBranch(opts.delete);
469
- console.log(chalk.green(`✓ Deleted branch '${opts.delete}'`));
470
- } catch (err: any) {
471
- console.error(chalk.red(err.message));
472
- process.exit(1);
473
- }
474
- return;
475
- }
476
-
477
- // Create or switch
478
- if (name) {
479
- const branches = engine.listBranches();
480
- const exists = branches.find((b) => b.name === name);
481
-
482
- if (exists) {
483
- // Switch to existing branch
484
- try {
485
- engine.switchBranch(name);
486
- console.log(chalk.green(`✓ Switched to branch '${name}'`));
487
- } catch (err: any) {
488
- console.error(chalk.red(err.message));
489
- process.exit(1);
490
- }
491
- } else {
492
- // Create new branch
493
- try {
494
- await engine.createBranch(name);
495
- engine.switchBranch(name);
496
- console.log(
497
- chalk.green(`✓ Created and switched to branch '${name}'`),
498
- );
499
- } catch (err: any) {
500
- console.error(chalk.red(err.message));
501
- process.exit(1);
502
- }
503
- }
504
- return;
505
- }
506
-
507
- // List (default)
508
- const branches = engine.listBranches();
509
- if (branches.length === 0) {
510
- console.log(chalk.dim('No branches'));
511
- return;
512
- }
513
-
514
- console.log(chalk.bold('Branches\n'));
515
- for (const b of branches) {
516
- const marker = b.isCurrent ? chalk.green('* ') : ' ';
517
- const name = b.isCurrent ? chalk.green(b.name) : b.name;
518
- const age = b.createdAt ? chalk.dim(formatRelativeTime(b.createdAt)) : '';
519
- console.log(`${marker}${name} ${age}`);
520
- }
521
- });
522
-
523
- // ---------------------------------------------------------------------------
524
- // trellis milestone
525
- // ---------------------------------------------------------------------------
526
-
527
- program
528
- .command('milestone')
529
- .description('Create or list milestones')
530
- .argument('[action]', '"create" or "list" (default: list)')
531
- .option('-m, --message <message>', 'Milestone message')
532
- .option('--from <hash>', 'Start op hash for the milestone range')
533
- .option('--to <hash>', 'End op hash for the milestone range')
534
- .option('-p, --path <path>', 'Repository path', '.')
535
- .action(async (action, opts) => {
536
- const rootPath = resolve(opts.path);
537
- if (!TrellisVcsEngine.isRepo(rootPath)) {
538
- console.error(
539
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
540
- );
541
- process.exit(1);
542
- }
543
-
544
- const engine = new TrellisVcsEngine({ rootPath });
545
- engine.open();
546
-
547
- if (action === 'create') {
548
- if (!opts.message) {
549
- console.error(
550
- chalk.red('Milestone message is required: --message "..."'),
551
- );
552
- process.exit(1);
553
- }
554
-
555
- try {
556
- const op = await engine.createMilestone(opts.message, {
557
- fromOpHash: opts.from,
558
- toOpHash: opts.to,
559
- });
560
- console.log(chalk.green(`✓ Milestone created`));
561
- console.log(` ${chalk.dim('ID:')} ${op.vcs?.milestoneId}`);
562
- console.log(` ${chalk.dim('Message:')} ${opts.message}`);
563
- console.log(` ${chalk.dim('Hash:')} ${op.hash.slice(0, 32)}…`);
564
- } catch (err: any) {
565
- console.error(chalk.red(`Failed: ${err.message}`));
566
- process.exit(1);
567
- }
568
- return;
569
- }
570
-
571
- // List (default)
572
- const milestones = engine.listMilestones();
573
- if (milestones.length === 0) {
574
- console.log(chalk.dim('No milestones'));
575
- return;
576
- }
577
-
578
- console.log(chalk.bold(`Milestones (${milestones.length})\n`));
579
- for (const m of milestones) {
580
- const age = m.createdAt ? formatRelativeTime(m.createdAt) : '';
581
- console.log(
582
- ` ${chalk.cyan('★')} ${chalk.bold(m.message ?? '(no message)')}`,
583
- );
584
- console.log(` ${chalk.dim('ID:')} ${m.id} ${chalk.dim(age)}`);
585
- if (m.affectedFiles.length > 0) {
586
- console.log(
587
- ` ${chalk.dim('Files:')} ${m.affectedFiles.slice(0, 5).join(', ')}${m.affectedFiles.length > 5 ? ` +${m.affectedFiles.length - 5} more` : ''}`,
588
- );
589
- }
590
- console.log();
591
- }
592
- });
593
-
594
- // ---------------------------------------------------------------------------
595
- // trellis checkpoint
596
- // ---------------------------------------------------------------------------
597
-
598
- program
599
- .command('checkpoint')
600
- .description('Create or list checkpoints')
601
- .argument('[action]', '"create" or "list" (default: list)')
602
- .option('-p, --path <path>', 'Repository path', '.')
603
- .action(async (action, opts) => {
604
- const rootPath = resolve(opts.path);
605
- if (!TrellisVcsEngine.isRepo(rootPath)) {
606
- console.error(
607
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
608
- );
609
- process.exit(1);
610
- }
611
-
612
- const engine = new TrellisVcsEngine({ rootPath });
613
- engine.open();
614
-
615
- if (action === 'create') {
616
- try {
617
- const op = await engine.createCheckpoint('manual');
618
- console.log(chalk.green(`✓ Checkpoint created`));
619
- console.log(` ${chalk.dim('Hash:')} ${op.hash.slice(0, 32)}…`);
620
- console.log(` ${chalk.dim('Trigger:')} manual`);
621
- } catch (err: any) {
622
- console.error(chalk.red(`Failed: ${err.message}`));
623
- process.exit(1);
624
- }
625
- return;
626
- }
627
-
628
- // List (default)
629
- const checkpoints = engine.listCheckpoints();
630
- if (checkpoints.length === 0) {
631
- console.log(chalk.dim('No checkpoints'));
632
- return;
633
- }
634
-
635
- console.log(chalk.bold(`Checkpoints (${checkpoints.length})\n`));
636
- for (const cp of checkpoints) {
637
- const age = cp.createdAt ? formatRelativeTime(cp.createdAt) : '';
638
- console.log(
639
- ` ${chalk.dim('●')} ${cp.id.slice(0, 32)} ${chalk.dim(cp.trigger ?? '')} ${chalk.dim(age)}`,
640
- );
641
- }
642
- });
643
-
644
- // ---------------------------------------------------------------------------
645
- // trellis diff
646
- // ---------------------------------------------------------------------------
647
-
648
- program
649
- .command('diff')
650
- .description('Show file-level diff between two points in history')
651
- .argument('[from]', 'Starting op hash or milestone ID')
652
- .argument('[to]', 'Ending op hash (default: current head)')
653
- .option('-p, --path <path>', 'Repository path', '.')
654
- .option('--stat', 'Show only summary stats')
655
- .action((from, to, opts) => {
656
- const rootPath = resolve(opts.path);
657
- if (!TrellisVcsEngine.isRepo(rootPath)) {
658
- console.error(
659
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
660
- );
661
- process.exit(1);
662
- }
663
-
664
- const engine = new TrellisVcsEngine({ rootPath });
665
- engine.open();
666
-
667
- let result;
668
- if (from && to) {
669
- result = engine.diffOps(from, to);
670
- } else if (from) {
671
- result = engine.diffFromOp(from);
672
- } else {
673
- // Diff from the first op to HEAD
674
- const ops = engine.getOps();
675
- if (ops.length < 2) {
676
- console.log(chalk.dim('Not enough history to diff.'));
677
- return;
678
- }
679
- result = engine.diffOps(ops[0].hash, ops[ops.length - 1].hash);
680
- }
681
-
682
- if (result.diffs.length === 0) {
683
- console.log(chalk.dim('No differences.'));
684
- return;
685
- }
686
-
687
- // Stats
688
- const s = result.stats;
689
- console.log(
690
- `${chalk.green(`+${s.added} added`)} ${chalk.yellow(`~${s.modified} modified`)} ${chalk.red(`-${s.removed} removed`)}${s.renamed ? ` ${chalk.blue(`→${s.renamed} renamed`)}` : ''}`,
691
- );
692
- console.log();
693
-
694
- if (opts.stat) return;
695
-
696
- // Detailed output
697
- for (const diff of result.diffs) {
698
- switch (diff.kind) {
699
- case 'fileAdded':
700
- console.log(`${chalk.green('+ ' + diff.path)}`);
701
- break;
702
- case 'fileDeleted':
703
- console.log(`${chalk.red('- ' + diff.path)}`);
704
- break;
705
- case 'fileRenamed':
706
- console.log(`${chalk.blue(`→ ${diff.oldPath} → ${diff.path}`)}`);
707
- break;
708
- case 'fileModified':
709
- console.log(`${chalk.yellow('~ ' + diff.path)}`);
710
- if (diff.unifiedDiff) {
711
- for (const line of diff.unifiedDiff.split('\n')) {
712
- if (line.startsWith('+')) {
713
- console.log(chalk.green(line));
714
- } else if (line.startsWith('-')) {
715
- console.log(chalk.red(line));
716
- } else if (line.startsWith('@@')) {
717
- console.log(chalk.cyan(line));
718
- } else {
719
- console.log(chalk.dim(line));
720
- }
721
- }
722
- }
723
- break;
724
- }
725
- console.log();
726
- }
727
- });
728
-
729
- // ---------------------------------------------------------------------------
730
- // trellis merge
731
- // ---------------------------------------------------------------------------
732
-
733
- program
734
- .command('merge')
735
- .description('Merge a branch into the current branch')
736
- .argument('<branch>', 'Source branch to merge')
737
- .option('-p, --path <path>', 'Repository path', '.')
738
- .option('--dry-run', 'Preview merge without applying changes')
739
- .action((branch, opts) => {
740
- const rootPath = resolve(opts.path);
741
- if (!TrellisVcsEngine.isRepo(rootPath)) {
742
- console.error(
743
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
744
- );
745
- process.exit(1);
746
- }
747
-
748
- const engine = new TrellisVcsEngine({ rootPath });
749
- engine.open();
750
-
751
- const result = engine.mergeBranch(branch);
752
-
753
- if (result.clean) {
754
- console.log(chalk.green('✓ Merge completed cleanly'));
755
- } else {
756
- console.log(
757
- chalk.yellow(`⚠ Merge has ${result.conflicts.length} conflict(s)`),
758
- );
759
- }
760
-
761
- const s = result.stats;
762
- console.log(` ${chalk.dim('Modified:')} ${s.modified}`);
763
- console.log(` ${chalk.dim('Deleted:')} ${s.deleted}`);
764
- console.log(` ${chalk.dim('Conflicted:')} ${s.conflicted}`);
765
-
766
- if (result.conflicts.length > 0) {
767
- console.log();
768
- console.log(chalk.bold('Conflicts:'));
769
- for (const c of result.conflicts) {
770
- console.log(` ${chalk.red('✗')} ${c.path} (${c.kind})`);
771
- }
772
- }
773
-
774
- if (opts.dryRun) {
775
- console.log();
776
- console.log(chalk.dim('(dry run — no changes applied)'));
777
- }
778
- });
779
-
780
- // ---------------------------------------------------------------------------
781
- // trellis parse
782
- // ---------------------------------------------------------------------------
783
-
784
- program
785
- .command('parse')
786
- .description('Parse a file into AST-level semantic entities')
787
- .argument('<file>', 'File to parse')
788
- .option('-p, --path <path>', 'Repository path', '.')
789
- .action((file, opts) => {
790
- const rootPath = resolve(opts.path);
791
- const engine = new TrellisVcsEngine({ rootPath });
792
- if (TrellisVcsEngine.isRepo(rootPath)) engine.open();
793
-
794
- const { readFileSync } = require('fs');
795
- const filePath = resolve(file);
796
- const content = readFileSync(filePath, 'utf-8');
797
- const result = engine.parseFile(content, file);
798
-
799
- if (!result) {
800
- console.log(chalk.dim(`No parser available for: ${file}`));
801
- return;
802
- }
803
-
804
- console.log(chalk.bold(`Parse: ${file}\n`));
805
- console.log(` ${chalk.dim('Language:')} ${result.language}`);
806
- console.log(
807
- ` ${chalk.dim('Declarations:')} ${result.declarations.length}`,
808
- );
809
- console.log(` ${chalk.dim('Imports:')} ${result.imports.length}`);
810
- console.log(` ${chalk.dim('Exports:')} ${result.exports.length}`);
811
-
812
- if (result.declarations.length > 0) {
813
- console.log();
814
- console.log(chalk.bold('Declarations:'));
815
- for (const d of result.declarations) {
816
- console.log(
817
- ` ${chalk.cyan(d.kind.padEnd(14))} ${chalk.bold(d.name)}${d.children.length ? ` (${d.children.length} members)` : ''}`,
818
- );
819
- for (const child of d.children) {
820
- console.log(` ${chalk.dim(child.kind.padEnd(14))} ${child.name}`);
821
- }
822
- }
823
- }
824
-
825
- if (result.imports.length > 0) {
826
- console.log();
827
- console.log(chalk.bold('Imports:'));
828
- for (const imp of result.imports) {
829
- const specs =
830
- imp.specifiers.length > 0 ? ` { ${imp.specifiers.join(', ')} }` : '';
831
- console.log(
832
- ` ${chalk.dim('from')} ${chalk.yellow(imp.source)}${specs}`,
833
- );
834
- }
835
- }
836
- });
837
-
838
- // ---------------------------------------------------------------------------
839
- // trellis sdiff (semantic diff)
840
- // ---------------------------------------------------------------------------
841
-
842
- program
843
- .command('sdiff')
844
- .description('Show semantic diff between two versions of a file')
845
- .argument('<fileA>', 'Old version of the file')
846
- .argument('<fileB>', 'New version of the file')
847
- .option('-p, --path <path>', 'Repository path', '.')
848
- .action((fileA, fileB, opts) => {
849
- const rootPath = resolve(opts.path);
850
- const engine = new TrellisVcsEngine({ rootPath });
851
- if (TrellisVcsEngine.isRepo(rootPath)) engine.open();
852
-
853
- const { readFileSync } = require('fs');
854
- const oldContent = readFileSync(resolve(fileA), 'utf-8');
855
- const newContent = readFileSync(resolve(fileB), 'utf-8');
856
- const patches = engine.semanticDiff(oldContent, newContent, fileA);
857
-
858
- if (patches.length === 0) {
859
- console.log(chalk.dim('No semantic differences.'));
860
- return;
861
- }
862
-
863
- console.log(chalk.bold(`Semantic diff: ${fileA} → ${fileB}\n`));
864
- console.log(` ${chalk.dim('Patches:')} ${patches.length}\n`);
865
-
866
- for (const patch of patches) {
867
- switch (patch.kind) {
868
- case 'symbolAdd':
869
- console.log(
870
- ` ${chalk.green('+')} ${chalk.green(`${patch.entity.kind}: ${patch.entity.name}`)}`,
871
- );
872
- break;
873
- case 'symbolRemove':
874
- console.log(
875
- ` ${chalk.red('-')} ${chalk.red(`${patch.entityName}`)} (removed)`,
876
- );
877
- break;
878
- case 'symbolModify':
879
- console.log(
880
- ` ${chalk.yellow('~')} ${chalk.yellow(patch.entityName)} (modified)`,
881
- );
882
- break;
883
- case 'symbolRename':
884
- console.log(
885
- ` ${chalk.blue('\u2192')} ${chalk.blue(`${patch.oldName} \u2192 ${patch.newName}`)} (renamed)`,
886
- );
887
- break;
888
- case 'importAdd':
889
- console.log(
890
- ` ${chalk.green('+')} import from ${chalk.yellow(patch.source)}`,
891
- );
892
- break;
893
- case 'importRemove':
894
- console.log(
895
- ` ${chalk.red('-')} import from ${chalk.yellow(patch.source)}`,
896
- );
897
- break;
898
- case 'importModify':
899
- console.log(
900
- ` ${chalk.yellow('~')} import from ${chalk.yellow(patch.source)} (specifiers changed)`,
901
- );
902
- break;
903
- case 'exportAdd':
904
- console.log(` ${chalk.green('+')} export ${chalk.bold(patch.name)}`);
905
- break;
906
- case 'exportRemove':
907
- console.log(` ${chalk.red('-')} export ${chalk.bold(patch.name)}`);
908
- break;
909
- case 'symbolMove':
910
- console.log(
911
- ` ${chalk.blue('\u2192')} ${chalk.blue(patch.entityName)} moved ${patch.oldFile} \u2192 ${patch.newFile}`,
912
- );
913
- break;
914
- }
915
- }
916
- });
917
-
918
- // ---------------------------------------------------------------------------
919
- // trellis sync
920
- // ---------------------------------------------------------------------------
921
-
922
- program
923
- .command('sync')
924
- .description('Sync operations with another TrellisVCS repository')
925
- .argument(
926
- '[action]',
927
- '"push", "pull", "status", or "reconcile" (default: status)',
928
- )
929
- .option('-p, --path <path>', 'Local repository path', '.')
930
- .option('--remote <remote>', 'Remote repository path')
931
- .action((action, opts) => {
932
- const rootPath = resolve(opts.path);
933
- if (!TrellisVcsEngine.isRepo(rootPath)) {
934
- console.error(
935
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
936
- );
937
- process.exit(1);
938
- }
939
-
940
- const engine = new TrellisVcsEngine({ rootPath });
941
- engine.open();
942
- const ops = engine.getOps();
943
-
944
- if (action === 'status' || !action) {
945
- console.log(chalk.bold('Sync Status\n'));
946
- console.log(` ${chalk.dim('Local ops:')} ${ops.length}`);
947
- console.log(
948
- ` ${chalk.dim('Head:')} ${ops.length > 0 ? ops[ops.length - 1].hash.slice(0, 16) + '\u2026' : '(none)'}`,
949
- );
950
- console.log(` ${chalk.dim('Branch:')} ${engine.getCurrentBranch()}`);
951
- return;
952
- }
953
-
954
- if (action === 'reconcile' && opts.remote) {
955
- const remotePath = resolve(opts.remote);
956
- if (!TrellisVcsEngine.isRepo(remotePath)) {
957
- console.error(chalk.red(`Not a TrellisVCS repository: ${remotePath}`));
958
- process.exit(1);
959
- }
960
-
961
- const remoteEngine = new TrellisVcsEngine({ rootPath: remotePath });
962
- remoteEngine.open();
963
- const remoteOps = remoteEngine.getOps();
964
-
965
- const { reconcile } = require('../sync/reconciler.js');
966
- const result = reconcile(ops, remoteOps);
967
-
968
- console.log(chalk.bold('Reconcile Result\n'));
969
- console.log(` ${chalk.dim('Merged ops:')} ${result.merged.length}`);
970
- console.log(` ${chalk.dim('Unique local:')} ${result.uniqueToA.length}`);
971
- console.log(
972
- ` ${chalk.dim('Unique remote:')} ${result.uniqueToB.length}`,
973
- );
974
- console.log(
975
- ` ${chalk.dim('Fork point:')} ${result.forkPoint?.slice(0, 16) ?? '(none)'}`,
976
- );
977
- console.log(
978
- ` ${chalk.dim('Clean:')} ${result.clean ? chalk.green('yes') : chalk.red('no')}`,
979
- );
980
-
981
- if (result.conflicts.length > 0) {
982
- console.log();
983
- console.log(chalk.bold('Conflicts:'));
984
- for (const c of result.conflicts) {
985
- console.log(` ${chalk.red('\u2717')} ${c.filePath}: ${c.reason}`);
986
- }
987
- }
988
- return;
989
- }
990
-
991
- console.log(
992
- chalk.dim('Use --remote <path> with reconcile to compare repositories.'),
993
- );
994
- console.log(
995
- chalk.dim('Full peer sync requires a transport layer (coming soon).'),
996
- );
997
- });
998
-
999
- // ---------------------------------------------------------------------------
1000
- // trellis garden
1001
- // ---------------------------------------------------------------------------
1002
-
1003
- program
1004
- .command('garden')
1005
- .description('Explore the Idea Garden — abandoned work clusters')
1006
- .argument(
1007
- '[action]',
1008
- '"list", "show <id>", "search", "revive <id>", or "stats" (default: list)',
1009
- )
1010
- .argument('[id]', 'Cluster ID (for show/revive)')
1011
- .option('-p, --path <path>', 'Repository path', '.')
1012
- .option('-f, --file <file>', 'Filter by file path')
1013
- .option('-k, --keyword <keyword>', 'Filter by keyword')
1014
- .option('-s, --status <status>', 'Filter by status (abandoned|draft|revived)')
1015
- .option('-n, --limit <n>', 'Max results', parseInt as any)
1016
- .action((action, id, opts) => {
1017
- const rootPath = resolve(opts.path);
1018
- if (!TrellisVcsEngine.isRepo(rootPath)) {
1019
- console.error(
1020
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
1021
- );
1022
- process.exit(1);
1023
- }
1024
-
1025
- const engine = new TrellisVcsEngine({ rootPath });
1026
- engine.open();
1027
- const garden = engine.garden();
1028
-
1029
- if (action === 'stats') {
1030
- const s = garden.stats();
1031
- console.log(chalk.bold('Idea Garden Stats\n'));
1032
- console.log(` ${chalk.dim('Total clusters:')} ${s.total}`);
1033
- console.log(` ${chalk.dim('Abandoned:')} ${s.abandoned}`);
1034
- console.log(` ${chalk.dim('Draft:')} ${s.draft}`);
1035
- console.log(` ${chalk.dim('Revived:')} ${s.revived}`);
1036
- console.log(` ${chalk.dim('Total ops:')} ${s.totalOps}`);
1037
- console.log(` ${chalk.dim('Total files:')} ${s.totalFiles}`);
1038
- return;
1039
- }
1040
-
1041
- if (action === 'show') {
1042
- if (!id) {
1043
- console.error(chalk.red('Usage: trellis garden show <cluster-id>'));
1044
- process.exit(1);
1045
- }
1046
- const cluster = garden.getCluster(id);
1047
- if (!cluster) {
1048
- console.error(chalk.red(`Cluster not found: ${id}`));
1049
- process.exit(1);
1050
- }
1051
-
1052
- console.log(chalk.bold(`Cluster: ${cluster.id}\n`));
1053
- console.log(
1054
- ` ${chalk.dim('Status:')} ${formatClusterStatus(cluster.status)}`,
1055
- );
1056
- console.log(` ${chalk.dim('Detected by:')} ${cluster.detectedBy}`);
1057
- console.log(` ${chalk.dim('Created:')} ${cluster.createdAt}`);
1058
- console.log(` ${chalk.dim('Abandoned:')} ${cluster.abandonedAt}`);
1059
- console.log(` ${chalk.dim('Ops:')} ${cluster.ops.length}`);
1060
- console.log(
1061
- ` ${chalk.dim('Files:')} ${cluster.affectedFiles.join(', ')}`,
1062
- );
1063
- if (cluster.estimatedIntent) {
1064
- console.log(` ${chalk.dim('Intent:')} ${cluster.estimatedIntent}`);
1065
- }
1066
- console.log();
1067
- console.log(chalk.bold('Operations:'));
1068
- for (const op of cluster.ops.slice(0, 20)) {
1069
- console.log(
1070
- ` ${formatOpKind(op.kind)} ${chalk.dim(op.hash.slice(0, 12))} ${op.vcs?.filePath ?? ''}`,
1071
- );
1072
- }
1073
- if (cluster.ops.length > 20) {
1074
- console.log(chalk.dim(` ... +${cluster.ops.length - 20} more`));
1075
- }
1076
- return;
1077
- }
1078
-
1079
- if (action === 'revive') {
1080
- if (!id) {
1081
- console.error(chalk.red('Usage: trellis garden revive <cluster-id>'));
1082
- process.exit(1);
1083
- }
1084
- const ops = garden.revive(id);
1085
- if (!ops) {
1086
- console.error(chalk.red(`Cluster not found: ${id}`));
1087
- process.exit(1);
1088
- }
1089
- console.log(chalk.green(`\u2713 Cluster revived: ${id}`));
1090
- console.log(` ${chalk.dim('Ops to replay:')} ${ops.length}`);
1091
- console.log(
1092
- ` ${chalk.dim('Files:')} ${[...new Set(ops.filter((o) => o.vcs?.filePath).map((o) => o.vcs!.filePath!))].join(', ')}`,
1093
- );
1094
- return;
1095
- }
1096
-
1097
- if (action === 'search') {
1098
- const results = garden.search({
1099
- file: opts.file,
1100
- keyword: opts.keyword,
1101
- status: opts.status as any,
1102
- limit: opts.limit,
1103
- });
1104
-
1105
- if (results.length === 0) {
1106
- console.log(chalk.dim('No matching clusters found.'));
1107
- return;
1108
- }
1109
-
1110
- console.log(chalk.bold(`Search results (${results.length})\n`));
1111
- for (const c of results) {
1112
- printClusterSummary(c);
1113
- }
1114
- return;
1115
- }
1116
-
1117
- // List (default)
1118
- const clusters = garden.search({
1119
- file: opts.file,
1120
- keyword: opts.keyword,
1121
- status: opts.status as any,
1122
- limit: opts.limit,
1123
- });
1124
-
1125
- if (clusters.length === 0) {
1126
- console.log(chalk.dim('No idea clusters found. The garden is empty.'));
1127
- return;
1128
- }
1129
-
1130
- console.log(chalk.bold(`Idea Garden (${clusters.length} clusters)\n`));
1131
- for (const c of clusters) {
1132
- printClusterSummary(c);
1133
- }
1134
- });
1135
-
1136
- function formatClusterStatus(status: string): string {
1137
- switch (status) {
1138
- case 'abandoned':
1139
- return chalk.yellow('abandoned');
1140
- case 'draft':
1141
- return chalk.blue('draft');
1142
- case 'revived':
1143
- return chalk.green('revived');
1144
- default:
1145
- return chalk.dim(status);
1146
- }
1147
- }
1148
-
1149
- function printClusterSummary(c: {
1150
- id: string;
1151
- status: string;
1152
- detectedBy: string;
1153
- ops: any[];
1154
- affectedFiles: string[];
1155
- createdAt: string;
1156
- abandonedAt: string;
1157
- }): void {
1158
- console.log(
1159
- ` ${chalk.cyan('\u2740')} ${chalk.bold(c.id)} ${formatClusterStatus(c.status)} ${chalk.dim(c.detectedBy)}`,
1160
- );
1161
- console.log(
1162
- ` ${chalk.dim('Ops:')} ${c.ops.length} ${chalk.dim('Files:')} ${c.affectedFiles.slice(0, 3).join(', ')}${c.affectedFiles.length > 3 ? ` +${c.affectedFiles.length - 3}` : ''}`,
1163
- );
1164
- console.log(
1165
- ` ${chalk.dim('Created:')} ${formatRelativeTime(c.createdAt)} ${chalk.dim('Abandoned:')} ${formatRelativeTime(c.abandonedAt)}`,
1166
- );
1167
- console.log();
1168
- }
1169
-
1170
- // ---------------------------------------------------------------------------
1171
- // trellis issue
1172
- // ---------------------------------------------------------------------------
1173
-
1174
- function formatIssueStatus(status: string | undefined): string {
1175
- switch (status) {
1176
- case 'backlog':
1177
- return chalk.gray('backlog');
1178
- case 'queue':
1179
- return chalk.blue('queue');
1180
- case 'in_progress':
1181
- return chalk.yellow('in_progress');
1182
- case 'paused':
1183
- return chalk.magenta('paused');
1184
- case 'closed':
1185
- return chalk.green('closed');
1186
- default:
1187
- return chalk.dim(status ?? 'unknown');
1188
- }
1189
- }
1190
-
1191
- function formatPriority(p: string | undefined): string {
1192
- switch (p) {
1193
- case 'critical':
1194
- return chalk.red('critical');
1195
- case 'high':
1196
- return chalk.yellow('high');
1197
- case 'medium':
1198
- return chalk.cyan('medium');
1199
- case 'low':
1200
- return chalk.dim('low');
1201
- default:
1202
- return chalk.dim(p ?? '');
1203
- }
1204
- }
1205
-
1206
- function formatCriterionStatus(status: string | undefined): string {
1207
- switch (status) {
1208
- case 'passed':
1209
- return chalk.green('✓ passed');
1210
- case 'failed':
1211
- return chalk.red('✗ failed');
1212
- case 'pending':
1213
- return chalk.dim('○ pending');
1214
- default:
1215
- return chalk.dim(status ?? 'pending');
1216
- }
1217
- }
1218
-
1219
- const issueCmd = program
1220
- .command('issue')
1221
- .description('Manage issues (task tracking)');
1222
-
1223
- issueCmd
1224
- .command('create')
1225
- .description('Create a new issue')
1226
- .requiredOption('-t, --title <title>', 'Issue title')
1227
- .option(
1228
- '-P, --priority <priority>',
1229
- 'Priority: critical, high, medium, low',
1230
- 'medium',
1231
- )
1232
- .option('-l, --labels <labels>', 'Comma-separated labels')
1233
- .option('--assignee <agentId>', 'Agent to assign')
1234
- .option('--parent <id>', 'Parent issue ID (for sub-tasks)')
1235
- .option('-d, --desc <description>', 'Short description')
1236
- .option(
1237
- '-S, --status <status>',
1238
- 'Initial status: backlog (default) or queue',
1239
- 'backlog',
1240
- )
1241
- .option(
1242
- '--ac <criteria...>',
1243
- 'Acceptance criteria. Prefix with "test:" for test commands',
1244
- )
1245
- .option('-p, --path <path>', 'Repository path', '.')
1246
- .action(async (opts) => {
1247
- const rootPath = resolve(opts.path);
1248
- requireRepo(rootPath);
1249
-
1250
- const engine = new TrellisVcsEngine({ rootPath });
1251
- engine.open();
1252
-
1253
- const labels = opts.labels
1254
- ? opts.labels.split(',').map((l: string) => l.trim())
1255
- : undefined;
1256
-
1257
- const criteria = opts.ac
1258
- ? opts.ac.map((ac: string) => {
1259
- if (ac.startsWith('test:')) {
1260
- return { description: ac.slice(5), command: ac.slice(5) };
1261
- }
1262
- return { description: ac };
1263
- })
1264
- : undefined;
1265
-
1266
- const op = await engine.createIssue(opts.title, {
1267
- priority: opts.priority,
1268
- labels,
1269
- assignee: opts.assignee,
1270
- parentId: opts.parent,
1271
- description: opts.desc,
1272
- status: opts.status,
1273
- criteria,
1274
- });
1275
-
1276
- const issueId = op.vcs?.issueId;
1277
- console.log(chalk.green(`✓ Issue created: ${chalk.bold(issueId)}`));
1278
- console.log(` ${chalk.dim('Title:')} ${opts.title}`);
1279
- console.log(` ${chalk.dim('Priority:')} ${formatPriority(opts.priority)}`);
1280
- if (labels) {
1281
- console.log(` ${chalk.dim('Labels:')} ${labels.join(', ')}`);
1282
- }
1283
- if (opts.parent) {
1284
- console.log(` ${chalk.dim('Parent:')} ${opts.parent}`);
1285
- }
1286
- if (criteria) {
1287
- console.log(
1288
- ` ${chalk.dim('Criteria:')} ${criteria.length} acceptance criteria`,
1289
- );
1290
- }
1291
- });
1292
-
1293
- issueCmd
1294
- .command('list')
1295
- .description('List issues')
1296
- .option(
1297
- '--status <status>',
1298
- 'Filter by status: backlog, queue, in_progress, paused, closed',
1299
- )
1300
- .option('--label <label>', 'Filter by label')
1301
- .option('--assignee <agentId>', 'Filter by assignee')
1302
- .option('--parent <id>', 'Filter by parent issue')
1303
- .option('-p, --path <path>', 'Repository path', '.')
1304
- .action((opts) => {
1305
- const rootPath = resolve(opts.path);
1306
- requireRepo(rootPath);
1307
-
1308
- const engine = new TrellisVcsEngine({ rootPath });
1309
- engine.open();
1310
-
1311
- const issues = engine.listIssues({
1312
- status: opts.status,
1313
- label: opts.label,
1314
- assignee: opts.assignee,
1315
- parentId: opts.parent,
1316
- });
1317
-
1318
- if (issues.length === 0) {
1319
- console.log(chalk.dim('No issues found.'));
1320
- return;
1321
- }
1322
-
1323
- console.log(chalk.bold(`Issues (${issues.length})\n`));
1324
- for (const issue of issues) {
1325
- const labels =
1326
- issue.labels.length > 0
1327
- ? chalk.dim(` [${issue.labels.join(',')}]`)
1328
- : '';
1329
- const assignee = issue.assignee ? chalk.dim(` → ${issue.assignee}`) : '';
1330
- const parent = issue.parentId ? chalk.dim(` ← ${issue.parentId}`) : '';
1331
- const blocked = issue.isBlocked ? chalk.yellow(' 🔒 blocked') : '';
1332
- const criteria =
1333
- issue.criteria.length > 0
1334
- ? chalk.dim(
1335
- ` (${issue.criteria.filter((c) => c.status === 'passed').length}/${issue.criteria.length} AC)`,
1336
- )
1337
- : '';
1338
- console.log(
1339
- ` ${formatPriority(issue.priority)} ${chalk.bold(issue.id)} ${formatIssueStatus(issue.status)} ${issue.title ?? ''}${labels}${assignee}${parent}${blocked}${criteria}`,
1340
- );
1341
- }
1342
- });
1343
-
1344
- issueCmd
1345
- .command('show')
1346
- .description('Show issue details')
1347
- .argument('<id>', 'Issue ID (e.g. TRL-1)')
1348
- .option('-p, --path <path>', 'Repository path', '.')
1349
- .action((id, opts) => {
1350
- const rootPath = resolve(opts.path);
1351
- requireRepo(rootPath);
1352
-
1353
- const engine = new TrellisVcsEngine({ rootPath });
1354
- engine.open();
1355
-
1356
- const issue = engine.getIssue(id);
1357
- if (!issue) {
1358
- console.error(chalk.red(`Issue not found: ${id}`));
1359
- process.exit(1);
1360
- }
1361
-
1362
- console.log(chalk.bold(`${issue.id}: ${issue.title ?? '(untitled)'}\n`));
1363
- if (issue.description) {
1364
- console.log(` ${chalk.dim(issue.description)}\n`);
1365
- }
1366
- console.log(
1367
- ` ${chalk.dim('Status:')} ${formatIssueStatus(issue.status)}`,
1368
- );
1369
- console.log(
1370
- ` ${chalk.dim('Priority:')} ${formatPriority(issue.priority)}`,
1371
- );
1372
- if (issue.labels.length > 0) {
1373
- console.log(` ${chalk.dim('Labels:')} ${issue.labels.join(', ')}`);
1374
- }
1375
- if (issue.assignee) {
1376
- console.log(` ${chalk.dim('Assignee:')} ${issue.assignee}`);
1377
- }
1378
- if (issue.parentId) {
1379
- console.log(` ${chalk.dim('Parent:')} ${issue.parentId}`);
1380
- }
1381
- if (issue.branchName) {
1382
- console.log(` ${chalk.dim('Branch:')} ${issue.branchName}`);
1383
- }
1384
- if (issue.blockedBy.length > 0) {
1385
- console.log(
1386
- ` ${chalk.dim('Blocked by:')} ${issue.blockedBy.map((b) => chalk.yellow(b)).join(', ')}`,
1387
- );
1388
- }
1389
- if (issue.blocking.length > 0) {
1390
- console.log(
1391
- ` ${chalk.dim('Blocking:')} ${issue.blocking.map((b) => chalk.cyan(b)).join(', ')}`,
1392
- );
1393
- }
1394
- if (issue.createdAt) {
1395
- console.log(
1396
- ` ${chalk.dim('Created:')} ${formatRelativeTime(issue.createdAt)}`,
1397
- );
1398
- }
1399
- if (issue.startedAt) {
1400
- console.log(
1401
- ` ${chalk.dim('Started:')} ${formatRelativeTime(issue.startedAt)}`,
1402
- );
1403
- }
1404
- if (issue.closedAt) {
1405
- console.log(
1406
- ` ${chalk.dim('Closed:')} ${formatRelativeTime(issue.closedAt)}`,
1407
- );
1408
- }
1409
-
1410
- if (issue.criteria.length > 0) {
1411
- console.log(`\n ${chalk.bold('Acceptance Criteria:')}`);
1412
- for (const c of issue.criteria) {
1413
- const desc = c.description ?? c.id;
1414
- const cmd = c.command ? chalk.dim(` (${c.command})`) : '';
1415
- console.log(` ${formatCriterionStatus(c.status)} ${desc}${cmd}`);
1416
- }
1417
- }
1418
- });
1419
-
1420
- issueCmd
1421
- .command('start')
1422
- .description('Start working on an issue (creates branch, auto-assigns)')
1423
- .argument('<id>', 'Issue ID')
1424
- .option('-p, --path <path>', 'Repository path', '.')
1425
- .action(async (id, opts) => {
1426
- const rootPath = resolve(opts.path);
1427
- requireRepo(rootPath);
1428
-
1429
- const engine = new TrellisVcsEngine({ rootPath });
1430
- engine.open();
1431
-
1432
- const op = await engine.startIssue(id);
1433
- const issue = engine.getIssue(id);
1434
- console.log(chalk.green(`✓ Started issue ${chalk.bold(id)}`));
1435
- if (issue?.branchName) {
1436
- console.log(` ${chalk.dim('Branch:')} ${issue.branchName}`);
1437
- }
1438
- if (issue?.assignee) {
1439
- console.log(` ${chalk.dim('Assignee:')} ${issue.assignee}`);
1440
- }
1441
- });
1442
-
1443
- issueCmd
1444
- .command('pause')
1445
- .description('Pause an in-progress issue (switches to default branch)')
1446
- .argument('<id>', 'Issue ID')
1447
- .requiredOption(
1448
- '-n, --note <note>',
1449
- 'Why paused and what must happen before resuming',
1450
- )
1451
- .option('-p, --path <path>', 'Repository path', '.')
1452
- .action(async (id, opts) => {
1453
- const rootPath = resolve(opts.path);
1454
- requireRepo(rootPath);
1455
-
1456
- const engine = new TrellisVcsEngine({ rootPath });
1457
- engine.open();
1458
-
1459
- await engine.pauseIssue(id, opts.note);
1460
- console.log(chalk.yellow(`⏸ Paused issue ${chalk.bold(id)}`));
1461
- console.log(` ${chalk.dim('Note:')} ${opts.note}`);
1462
- console.log(` ${chalk.dim('Switched to:')} ${engine.getCurrentBranch()}`);
1463
- });
1464
-
1465
- issueCmd
1466
- .command('resume')
1467
- .description('Resume a paused issue (switches to issue branch)')
1468
- .argument('<id>', 'Issue ID')
1469
- .option('-p, --path <path>', 'Repository path', '.')
1470
- .action(async (id, opts) => {
1471
- const rootPath = resolve(opts.path);
1472
- requireRepo(rootPath);
1473
-
1474
- const engine = new TrellisVcsEngine({ rootPath });
1475
- engine.open();
1476
-
1477
- await engine.resumeIssue(id);
1478
- const issue = engine.getIssue(id);
1479
- console.log(chalk.green(`▶ Resumed issue ${chalk.bold(id)}`));
1480
- if (issue?.branchName) {
1481
- console.log(` ${chalk.dim('Branch:')} ${issue.branchName}`);
1482
- }
1483
- });
1484
-
1485
- issueCmd
1486
- .command('triage')
1487
- .description('Move a backlog issue to queue (ready to start)')
1488
- .argument('<id>', 'Issue ID')
1489
- .option('-p, --path <path>', 'Repository path', '.')
1490
- .action(async (id, opts) => {
1491
- const rootPath = resolve(opts.path);
1492
- requireRepo(rootPath);
1493
-
1494
- const engine = new TrellisVcsEngine({ rootPath });
1495
- engine.open();
1496
-
1497
- await engine.triageIssue(id);
1498
- console.log(chalk.green(`✓ Triaged ${chalk.bold(id)} → queue`));
1499
- });
1500
-
1501
- issueCmd
1502
- .command('update')
1503
- .description('Update issue metadata')
1504
- .argument('<id>', 'Issue ID')
1505
- .option('--title <title>', 'New title')
1506
- .option('-d, --desc <description>', 'Short description')
1507
- .option(
1508
- '--status <status>',
1509
- 'New status: backlog, queue, in_progress, paused, closed',
1510
- )
1511
- .option('-P, --priority <priority>', 'Priority: critical, high, medium, low')
1512
- .option('-l, --labels <labels>', 'Comma-separated labels')
1513
- .option('--assignee <agentId>', 'Agent to assign')
1514
- .option('-p, --path <path>', 'Repository path', '.')
1515
- .action(async (id, opts) => {
1516
- const rootPath = resolve(opts.path);
1517
- requireRepo(rootPath);
1518
-
1519
- const engine = new TrellisVcsEngine({ rootPath });
1520
- engine.open();
1521
-
1522
- const updates: Record<string, any> = {};
1523
- if (opts.title !== undefined) updates.title = opts.title;
1524
- if (opts.desc !== undefined) updates.description = opts.desc;
1525
- if (opts.status !== undefined) updates.status = opts.status;
1526
- if (opts.priority !== undefined) updates.priority = opts.priority;
1527
- if (opts.labels !== undefined) {
1528
- updates.labels = opts.labels.split(',').map((l: string) => l.trim());
1529
- }
1530
- if (opts.assignee !== undefined) updates.assignee = opts.assignee;
1531
-
1532
- await engine.updateIssue(id, updates);
1533
- console.log(chalk.green(`✓ Updated ${chalk.bold(id)}`));
1534
- });
1535
-
1536
- issueCmd
1537
- .command('describe')
1538
- .description('Set an issue description')
1539
- .argument('<id>', 'Issue ID')
1540
- .argument('<description>', 'Short description text')
1541
- .option('-p, --path <path>', 'Repository path', '.')
1542
- .action(async (id, description, opts) => {
1543
- const rootPath = resolve(opts.path);
1544
- requireRepo(rootPath);
1545
-
1546
- const engine = new TrellisVcsEngine({ rootPath });
1547
- engine.open();
1548
-
1549
- await engine.updateIssue(id, { description });
1550
- console.log(chalk.green(`✓ Description set for ${chalk.bold(id)}`));
1551
- });
1552
-
1553
- issueCmd
1554
- .command('assign')
1555
- .description('Assign an issue to an agent')
1556
- .argument('<id>', 'Issue ID')
1557
- .requiredOption('--to <agentId>', 'Agent ID to assign')
1558
- .option('-p, --path <path>', 'Repository path', '.')
1559
- .action(async (id, opts) => {
1560
- const rootPath = resolve(opts.path);
1561
- requireRepo(rootPath);
1562
-
1563
- const engine = new TrellisVcsEngine({ rootPath });
1564
- engine.open();
1565
-
1566
- await engine.assignIssue(id, opts.to);
1567
- console.log(chalk.green(`✓ Assigned ${chalk.bold(id)} → ${opts.to}`));
1568
- });
1569
-
1570
- issueCmd
1571
- .command('ac')
1572
- .description('Add acceptance criterion to an issue')
1573
- .argument('<id>', 'Issue ID')
1574
- .argument('<description>', 'Criterion description')
1575
- .option('--test <command>', 'Shell command to validate (exit 0 = pass)')
1576
- .option('-p, --path <path>', 'Repository path', '.')
1577
- .action(async (id, description, opts) => {
1578
- const rootPath = resolve(opts.path);
1579
- requireRepo(rootPath);
1580
-
1581
- const engine = new TrellisVcsEngine({ rootPath });
1582
- engine.open();
1583
-
1584
- await engine.addCriterion(id, description, opts.test);
1585
- const cmdNote = opts.test ? chalk.dim(` (test: ${opts.test})`) : '';
1586
- console.log(
1587
- chalk.green(
1588
- `✓ Added criterion to ${chalk.bold(id)}: ${description}${cmdNote}`,
1589
- ),
1590
- );
1591
- });
1592
-
1593
- issueCmd
1594
- .command('ac-pass')
1595
- .description('Manually mark an acceptance criterion as passed')
1596
- .argument('<id>', 'Issue ID')
1597
- .argument('<index>', 'Criterion number (1-based)')
1598
- .option('-p, --path <path>', 'Repository path', '.')
1599
- .action(async (id, index, opts) => {
1600
- const rootPath = resolve(opts.path);
1601
- requireRepo(rootPath);
1602
-
1603
- const engine = new TrellisVcsEngine({ rootPath });
1604
- engine.open();
1605
-
1606
- await engine.setCriterionStatus(id, parseInt(index, 10), 'passed');
1607
- console.log(
1608
- chalk.green(
1609
- `✓ Criterion #${index} on ${chalk.bold(id)} marked as passed`,
1610
- ),
1611
- );
1612
- });
1613
-
1614
- issueCmd
1615
- .command('ac-fail')
1616
- .description('Manually mark an acceptance criterion as failed')
1617
- .argument('<id>', 'Issue ID')
1618
- .argument('<index>', 'Criterion number (1-based)')
1619
- .option('-p, --path <path>', 'Repository path', '.')
1620
- .action(async (id, index, opts) => {
1621
- const rootPath = resolve(opts.path);
1622
- requireRepo(rootPath);
1623
-
1624
- const engine = new TrellisVcsEngine({ rootPath });
1625
- engine.open();
1626
-
1627
- await engine.setCriterionStatus(id, parseInt(index, 10), 'failed');
1628
- console.log(
1629
- chalk.red(`✗ Criterion #${index} on ${chalk.bold(id)} marked as failed`),
1630
- );
1631
- });
1632
-
1633
- issueCmd
1634
- .command('check')
1635
- .description('Run acceptance criteria for an issue')
1636
- .argument('<id>', 'Issue ID')
1637
- .option('-p, --path <path>', 'Repository path', '.')
1638
- .action(async (id, opts) => {
1639
- const rootPath = resolve(opts.path);
1640
- requireRepo(rootPath);
1641
-
1642
- const engine = new TrellisVcsEngine({ rootPath });
1643
- engine.open();
1644
-
1645
- console.log(chalk.bold(`Running criteria for ${id}...\n`));
1646
- const results = await engine.runCriteria(id);
1647
-
1648
- if (results.length === 0) {
1649
- console.log(chalk.dim('No acceptance criteria defined.'));
1650
- return;
1651
- }
1652
-
1653
- for (const r of results) {
1654
- const desc = r.description ?? r.id;
1655
- const statusStr =
1656
- r.status === 'passed'
1657
- ? chalk.green('✓ PASSED')
1658
- : r.status === 'failed'
1659
- ? chalk.red('✗ FAILED')
1660
- : chalk.dim('○ SKIPPED');
1661
- console.log(` ${statusStr} ${desc}`);
1662
- if (r.command) {
1663
- console.log(` ${chalk.dim('$')} ${r.command}`);
1664
- }
1665
- if (r.output && r.status === 'failed') {
1666
- const lines = r.output.split('\n').slice(0, 5);
1667
- for (const line of lines) {
1668
- console.log(` ${chalk.dim(line)}`);
1669
- }
1670
- }
1671
- }
1672
-
1673
- const passed = results.filter((r) => r.status === 'passed').length;
1674
- const total = results.length;
1675
- console.log();
1676
- if (passed === total) {
1677
- console.log(
1678
- chalk.green(
1679
- `All ${total} criteria passed. Close with: trellis issue close ${id} --confirm`,
1680
- ),
1681
- );
1682
- } else {
1683
- console.log(chalk.yellow(`${passed}/${total} criteria passing.`));
1684
- }
1685
- });
1686
-
1687
- issueCmd
1688
- .command('close')
1689
- .description('Close an issue (requires all criteria pass + --confirm)')
1690
- .argument('<id>', 'Issue ID')
1691
- .option('--confirm', 'Confirm closure after criteria pass')
1692
- .option('-p, --path <path>', 'Repository path', '.')
1693
- .action(async (id, opts) => {
1694
- const rootPath = resolve(opts.path);
1695
- requireRepo(rootPath);
1696
-
1697
- const engine = new TrellisVcsEngine({ rootPath });
1698
- engine.open();
1699
-
1700
- try {
1701
- const result = await engine.closeIssue(id, { confirm: opts.confirm });
1702
-
1703
- if (!result.op) {
1704
- // Criteria passed but no --confirm
1705
- console.log(chalk.bold(`Criteria status for ${id}:\n`));
1706
- for (const r of result.criteriaResults) {
1707
- console.log(
1708
- ` ${formatCriterionStatus(r.status)} ${r.description ?? r.id}`,
1709
- );
1710
- }
1711
- console.log();
1712
- console.log(
1713
- chalk.yellow(
1714
- `All criteria pass. Re-run with --confirm to close: trellis issue close ${id} --confirm`,
1715
- ),
1716
- );
1717
- return;
1718
- }
1719
-
1720
- console.log(chalk.green(`✓ Issue ${chalk.bold(id)} closed`));
1721
- } catch (err: any) {
1722
- console.error(chalk.red(err.message));
1723
- process.exit(1);
1724
- }
1725
- });
1726
-
1727
- issueCmd
1728
- .command('reopen')
1729
- .description('Reopen a closed issue')
1730
- .argument('<id>', 'Issue ID')
1731
- .option('-p, --path <path>', 'Repository path', '.')
1732
- .action(async (id, opts) => {
1733
- const rootPath = resolve(opts.path);
1734
- requireRepo(rootPath);
1735
-
1736
- const engine = new TrellisVcsEngine({ rootPath });
1737
- engine.open();
1738
-
1739
- await engine.reopenIssue(id);
1740
- console.log(chalk.green(`✓ Issue ${chalk.bold(id)} reopened`));
1741
- });
1742
-
1743
- issueCmd
1744
- .command('block')
1745
- .description('Mark an issue as blocked by another issue')
1746
- .argument('<id>', 'Issue ID to block')
1747
- .argument('<blockedBy>', 'Issue ID that blocks it')
1748
- .option('-p, --path <path>', 'Repository path', '.')
1749
- .action(async (id, blockedBy, opts) => {
1750
- const rootPath = resolve(opts.path);
1751
- requireRepo(rootPath);
1752
-
1753
- const engine = new TrellisVcsEngine({ rootPath });
1754
- engine.open();
1755
-
1756
- await engine.blockIssue(id, blockedBy);
1757
- console.log(
1758
- chalk.yellow(
1759
- `🔒 ${chalk.bold(id)} is now blocked by ${chalk.bold(blockedBy)}`,
1760
- ),
1761
- );
1762
- });
1763
-
1764
- issueCmd
1765
- .command('unblock')
1766
- .description('Remove a blocking relationship')
1767
- .argument('<id>', 'Blocked issue ID')
1768
- .argument('<blockedBy>', 'Blocking issue ID to remove')
1769
- .option('-p, --path <path>', 'Repository path', '.')
1770
- .action(async (id, blockedBy, opts) => {
1771
- const rootPath = resolve(opts.path);
1772
- requireRepo(rootPath);
1773
-
1774
- const engine = new TrellisVcsEngine({ rootPath });
1775
- engine.open();
1776
-
1777
- await engine.unblockIssue(id, blockedBy);
1778
- console.log(
1779
- chalk.green(
1780
- `🔓 ${chalk.bold(id)} is no longer blocked by ${chalk.bold(blockedBy)}`,
1781
- ),
1782
- );
1783
- });
1784
-
1785
- issueCmd
1786
- .command('active')
1787
- .description('Show all active (in-progress) issues')
1788
- .option('-p, --path <path>', 'Repository path', '.')
1789
- .action((opts) => {
1790
- const rootPath = resolve(opts.path);
1791
- requireRepo(rootPath);
1792
-
1793
- const engine = new TrellisVcsEngine({ rootPath });
1794
- engine.open();
1795
-
1796
- const active = engine.getActiveIssues();
1797
- if (active.length === 0) {
1798
- console.log(chalk.dim('No active issues.'));
1799
- return;
1800
- }
1801
-
1802
- console.log(chalk.bold(`Active Issues (${active.length})\n`));
1803
- for (const issue of active) {
1804
- const branch = issue.branchName
1805
- ? chalk.dim(` on ${issue.branchName}`)
1806
- : '';
1807
- const assignee = issue.assignee ? chalk.dim(` → ${issue.assignee}`) : '';
1808
- console.log(
1809
- ` ${formatPriority(issue.priority)} ${chalk.bold(issue.id)} ${issue.title ?? ''}${branch}${assignee}`,
1810
- );
1811
- }
1812
- });
1813
-
1814
- issueCmd
1815
- .command('readiness')
1816
- .description(
1817
- 'Check if all issues are complete (no queue, paused, or in-progress)',
1818
- )
1819
- .option('-p, --path <path>', 'Repository path', '.')
1820
- .action((opts) => {
1821
- const rootPath = resolve(opts.path);
1822
- requireRepo(rootPath);
1823
-
1824
- const engine = new TrellisVcsEngine({ rootPath });
1825
- engine.open();
1826
-
1827
- const result = engine.checkCompletionReadiness();
1828
- console.log(result.summary);
1829
-
1830
- if (!result.ready) {
1831
- process.exit(1);
1832
- }
1833
- });
1834
-
1835
- // ---------------------------------------------------------------------------
1836
- // trellis decision
1837
- // ---------------------------------------------------------------------------
1838
-
1839
- const decisionCmd = program
1840
- .command('decision')
1841
- .description('Manage decision traces');
1842
-
1843
- decisionCmd
1844
- .command('list')
1845
- .description('List decision traces')
1846
- .option('-p, --path <path>', 'Repository path', '.')
1847
- .option(
1848
- '-t, --tool <pattern>',
1849
- 'Filter by tool name pattern (e.g. "trellis_issue_*")',
1850
- )
1851
- .option('-e, --entity <id>', 'Filter by related entity ID')
1852
- .option('-n, --limit <n>', 'Max results', '20')
1853
- .action((opts) => {
1854
- const rootPath = resolve(opts.path);
1855
- requireRepo(rootPath);
1856
-
1857
- const engine = new TrellisVcsEngine({ rootPath });
1858
- engine.open();
1859
-
1860
- const decisions = engine.queryDecisions({
1861
- toolPattern: opts.tool,
1862
- entityId: opts.entity,
1863
- limit: parseInt(opts.limit, 10),
1864
- });
1865
-
1866
- if (decisions.length === 0) {
1867
- console.log(chalk.dim('No decision traces found.'));
1868
- return;
1869
- }
1870
-
1871
- for (const d of decisions) {
1872
- const ts = d.createdAt ? chalk.dim(d.createdAt) : '';
1873
- console.log(`${chalk.cyan(d.id)} ${chalk.white(d.toolName)} ${ts}`);
1874
- if (d.rationale) {
1875
- console.log(` ${chalk.dim('→')} ${d.rationale}`);
1876
- }
1877
- }
1878
- });
1879
-
1880
- decisionCmd
1881
- .command('show')
1882
- .description('Show full details of a decision trace')
1883
- .argument('<id>', 'Decision ID (e.g. DEC-1)')
1884
- .option('-p, --path <path>', 'Repository path', '.')
1885
- .action((id, opts) => {
1886
- const rootPath = resolve(opts.path);
1887
- requireRepo(rootPath);
1888
-
1889
- const engine = new TrellisVcsEngine({ rootPath });
1890
- engine.open();
1891
-
1892
- const d = engine.getDecision(id);
1893
- if (!d) {
1894
- console.error(chalk.red(`Decision ${id} not found.`));
1895
- process.exit(1);
1896
- }
1897
-
1898
- console.log(`${chalk.bold('ID:')} ${d.id}`);
1899
- console.log(`${chalk.bold('Tool:')} ${d.toolName}`);
1900
- console.log(`${chalk.bold('Created:')} ${d.createdAt ?? 'unknown'}`);
1901
- console.log(`${chalk.bold('Agent:')} ${d.createdBy ?? 'unknown'}`);
1902
- if (d.context) console.log(`${chalk.bold('Context:')} ${d.context}`);
1903
- if (d.rationale) console.log(`${chalk.bold('Rationale:')} ${d.rationale}`);
1904
- if (d.alternatives && d.alternatives.length > 0) {
1905
- console.log(
1906
- `${chalk.bold('Alternatives:')} ${d.alternatives.join(', ')}`,
1907
- );
1908
- }
1909
- if (d.outputSummary) {
1910
- console.log(`${chalk.bold('Output:')} ${d.outputSummary}`);
1911
- }
1912
- if (d.relatedEntities.length > 0) {
1913
- console.log(
1914
- `${chalk.bold('Related:')} ${d.relatedEntities.join(', ')}`,
1915
- );
1916
- }
1917
- });
1918
-
1919
- decisionCmd
1920
- .command('chain')
1921
- .description('Trace all decisions that affected a given entity')
1922
- .argument(
1923
- '<entityId>',
1924
- 'Entity ID (e.g. "issue:TRL-5", "file:src/engine.ts")',
1925
- )
1926
- .option('-p, --path <path>', 'Repository path', '.')
1927
- .action((entityId, opts) => {
1928
- const rootPath = resolve(opts.path);
1929
- requireRepo(rootPath);
1930
-
1931
- const engine = new TrellisVcsEngine({ rootPath });
1932
- engine.open();
1933
-
1934
- const chain = engine.getDecisionChain(entityId);
1935
- if (chain.length === 0) {
1936
- console.log(chalk.dim(`No decision traces found for ${entityId}.`));
1937
- return;
1938
- }
1939
-
1940
- console.log(
1941
- chalk.bold(`Decision chain for ${entityId} (${chain.length} decisions):`),
1942
- );
1943
- for (const d of chain) {
1944
- const ts = d.createdAt ? chalk.dim(d.createdAt) : '';
1945
- console.log(` ${chalk.cyan(d.id)} ${chalk.white(d.toolName)} ${ts}`);
1946
- if (d.rationale) {
1947
- console.log(` ${chalk.dim('→')} ${d.rationale}`);
1948
- }
1949
- }
1950
- });
1951
-
1952
- // ---------------------------------------------------------------------------
1953
- // trellis identity
1954
- // ---------------------------------------------------------------------------
1955
-
1956
- program
1957
- .command('identity')
1958
- .description('Manage local identity (Ed25519 key pair)')
1959
- .argument('[action]', '"init" or "show" (default: show)')
1960
- .option('-p, --path <path>', 'Repository path', '.')
1961
- .option('--name <name>', 'Display name for new identity')
1962
- .option('--email <email>', 'Email for new identity')
1963
- .action((action, opts) => {
1964
- const rootPath = resolve(opts.path);
1965
- const trellisDir = join(rootPath, '.trellis');
1966
-
1967
- if (action === 'init') {
1968
- if (hasIdentity(trellisDir)) {
1969
- console.error(
1970
- chalk.yellow(
1971
- 'Identity already exists. Use `trellis identity` to view it.',
1972
- ),
1973
- );
1974
- process.exit(1);
1975
- }
1976
-
1977
- const name = opts.name ?? 'Anonymous';
1978
- const email = opts.email;
1979
-
1980
- const identity = createIdentity({ displayName: name, email });
1981
- saveIdentity(trellisDir, identity);
1982
-
1983
- console.log(chalk.green('✓ Identity created'));
1984
- console.log(` ${chalk.dim('Name:')} ${identity.displayName}`);
1985
- if (identity.email) {
1986
- console.log(` ${chalk.dim('Email:')} ${identity.email}`);
1987
- }
1988
- console.log(` ${chalk.dim('DID:')} ${identity.did}`);
1989
- console.log(` ${chalk.dim('ID:')} ${identity.entityId}`);
1990
- return;
1991
- }
1992
-
1993
- // Show (default)
1994
- const identity = loadIdentity(trellisDir);
1995
- if (!identity) {
1996
- console.log(
1997
- chalk.dim(
1998
- 'No identity configured. Run `trellis identity init --name "Your Name"`.',
1999
- ),
2000
- );
2001
- return;
2002
- }
2003
-
2004
- const pub = toPublicIdentity(identity);
2005
- console.log(chalk.bold('Identity\n'));
2006
- console.log(` ${chalk.dim('Name:')} ${pub.displayName}`);
2007
- if (pub.email) {
2008
- console.log(` ${chalk.dim('Email:')} ${pub.email}`);
2009
- }
2010
- console.log(` ${chalk.dim('DID:')} ${pub.did}`);
2011
- console.log(` ${chalk.dim('Entity ID:')} ${pub.entityId}`);
2012
- console.log(` ${chalk.dim('Public Key:')} ${pub.publicKey.slice(0, 32)}…`);
2013
- console.log(` ${chalk.dim('Created:')} ${pub.createdAt}`);
2014
- });
2015
-
2016
- // ---------------------------------------------------------------------------
2017
- // trellis refs
2018
- // ---------------------------------------------------------------------------
2019
-
2020
- program
2021
- .command('refs')
2022
- .description('List wiki-link references in files or find backlinks')
2023
- .argument('[file]', 'File to list outgoing refs for')
2024
- .option('-p, --path <path>', 'Repository path', '.')
2025
- .option(
2026
- '--backlinks <entity>',
2027
- 'Show all files referencing an entity (e.g. TRL-5)',
2028
- )
2029
- .option('--broken', 'List all broken and stale references')
2030
- .option('--stats', 'Show reference index statistics')
2031
- .action((file, opts) => {
2032
- const rootPath = resolve(opts.path);
2033
- if (!TrellisVcsEngine.isRepo(rootPath)) {
2034
- console.error(
2035
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2036
- );
2037
- process.exit(1);
2038
- }
2039
-
2040
- const engine = new TrellisVcsEngine({ rootPath });
2041
- engine.open();
2042
-
2043
- const { readFileSync } = require('fs');
2044
- const {
2045
- parseFileRefs,
2046
- buildRefIndex,
2047
- getOutgoingRefs,
2048
- getBacklinks,
2049
- getIndexStats,
2050
- } = require('../links/index.js');
2051
- const {
2052
- resolveRef,
2053
- resolveRefs,
2054
- createResolverContext,
2055
- } = require('../links/index.js');
2056
- const { StaleRefRegistry, getDiagnostics } = require('../links/index.js');
2057
-
2058
- // Build resolver context from engine
2059
- const resolverCtx = createResolverContext(engine);
2060
-
2061
- // Scan all tracked .md files and source files to build the index
2062
- const trackedFiles = engine.trackedFiles();
2063
- const fileContents: Array<{ path: string; content: string }> = [];
2064
- for (const f of trackedFiles) {
2065
- try {
2066
- const absPath = join(rootPath, f.path);
2067
- const content = readFileSync(absPath, 'utf-8');
2068
- fileContents.push({ path: f.path, content });
2069
- } catch {
2070
- // File may not exist on disk
2071
- }
2072
- }
2073
-
2074
- const index = buildRefIndex(fileContents, resolverCtx);
2075
-
2076
- // --stats: show index statistics
2077
- if (opts.stats) {
2078
- const stats = getIndexStats(index);
2079
- console.log(chalk.bold('Reference Index Stats\n'));
2080
- console.log(` ${chalk.dim('Files with refs:')} ${stats.totalFiles}`);
2081
- console.log(` ${chalk.dim('Total refs:')} ${stats.totalRefs}`);
2082
- console.log(` ${chalk.dim('Unique entities:')} ${stats.totalEntities}`);
2083
- return;
2084
- }
2085
-
2086
- // --backlinks <entity>: show all sources referencing an entity
2087
- if (opts.backlinks) {
2088
- const entity = opts.backlinks;
2089
- // Try common entity ID formats
2090
- const candidates = [
2091
- `issue:${entity}`,
2092
- `file:${entity}`,
2093
- `symbol:${entity}`,
2094
- `identity:${entity}`,
2095
- `milestone:${entity}`,
2096
- `decision:${entity}`,
2097
- entity, // raw entity ID
2098
- ];
2099
-
2100
- let found = false;
2101
- for (const eid of candidates) {
2102
- const sources = getBacklinks(index, eid);
2103
- if (sources.length > 0) {
2104
- console.log(
2105
- chalk.bold(
2106
- `Backlinks for ${chalk.cyan(eid)} (${sources.length})\n`,
2107
- ),
2108
- );
2109
- for (const s of sources) {
2110
- console.log(
2111
- ` ${chalk.dim(s.filePath)}:${s.line} ${chalk.dim(`(${s.context})`)}`,
2112
- );
2113
- }
2114
- found = true;
2115
- break;
2116
- }
2117
- }
2118
-
2119
- if (!found) {
2120
- console.log(chalk.dim(`No references found for: ${entity}`));
2121
- }
2122
- return;
2123
- }
2124
-
2125
- // --broken: list all broken and stale refs
2126
- if (opts.broken) {
2127
- const registry = new StaleRefRegistry();
2128
- const resolvedIds = new Set<string>();
2129
-
2130
- // Resolve all refs to build the resolved set
2131
- for (const [, refs] of index.outgoing) {
2132
- for (const ref of refs) {
2133
- const resolved = resolveRef(ref, resolverCtx);
2134
- if (resolved.state === 'resolved' && resolved.entityId) {
2135
- resolvedIds.add(resolved.entityId);
2136
- }
2137
- }
2138
- }
2139
-
2140
- const diags = getDiagnostics(index, registry, resolvedIds);
2141
-
2142
- if (diags.length === 0) {
2143
- console.log(chalk.green('✓ No broken or stale references found.'));
2144
- return;
2145
- }
2146
-
2147
- const stale = diags.filter((d: any) => d.state === 'stale');
2148
- const broken = diags.filter((d: any) => d.state === 'broken');
2149
-
2150
- if (broken.length > 0) {
2151
- console.log(
2152
- chalk.bold(chalk.red(`Broken references (${broken.length})\n`)),
2153
- );
2154
- for (const d of broken) {
2155
- console.log(
2156
- ` ${chalk.red('✗')} ${d.source.filePath}:${d.source.line} ${d.message}`,
2157
- );
2158
- }
2159
- console.log();
2160
- }
2161
-
2162
- if (stale.length > 0) {
2163
- console.log(
2164
- chalk.bold(chalk.yellow(`Stale references (${stale.length})\n`)),
2165
- );
2166
- for (const d of stale) {
2167
- console.log(
2168
- ` ${chalk.yellow('⚠')} ${d.source.filePath}:${d.source.line} ${d.message}`,
2169
- );
2170
- }
2171
- }
2172
- return;
2173
- }
2174
-
2175
- // Default: list outgoing refs for a specific file (or all files)
2176
- if (file) {
2177
- const refs = getOutgoingRefs(index, file);
2178
- if (refs.length === 0) {
2179
- console.log(chalk.dim(`No [[...]] references found in: ${file}`));
2180
- return;
2181
- }
2182
-
2183
- console.log(
2184
- chalk.bold(`References in ${chalk.cyan(file)} (${refs.length})\n`),
2185
- );
2186
- for (const ref of refs) {
2187
- const resolved = resolveRef(ref, resolverCtx);
2188
- const stateIcon =
2189
- resolved.state === 'resolved'
2190
- ? chalk.green('✓')
2191
- : resolved.state === 'stale'
2192
- ? chalk.yellow('⚠')
2193
- : chalk.red('✗');
2194
- const display = ref.alias ?? ref.raw;
2195
- const entityId = resolved.entityId ?? chalk.dim('unresolved');
2196
- console.log(
2197
- ` ${stateIcon} [[${display}]] → ${entityId} ${chalk.dim(`L${ref.source.line}`)}`,
2198
- );
2199
- }
2200
- } else {
2201
- // List all files with refs
2202
- const stats = getIndexStats(index);
2203
- if (stats.totalRefs === 0) {
2204
- console.log(
2205
- chalk.dim('No [[...]] references found in any tracked files.'),
2206
- );
2207
- return;
2208
- }
2209
-
2210
- console.log(
2211
- chalk.bold(
2212
- `References (${stats.totalRefs} across ${stats.totalFiles} files)\n`,
2213
- ),
2214
- );
2215
- for (const [filePath, refs] of index.outgoing) {
2216
- console.log(` ${chalk.cyan(filePath)} (${refs.length} refs)`);
2217
- for (const ref of refs) {
2218
- const resolved = resolveRef(ref, resolverCtx);
2219
- const stateIcon =
2220
- resolved.state === 'resolved'
2221
- ? chalk.green('✓')
2222
- : resolved.state === 'stale'
2223
- ? chalk.yellow('⚠')
2224
- : chalk.red('✗');
2225
- console.log(
2226
- ` ${stateIcon} [[${ref.raw}]] ${chalk.dim(`L${ref.source.line}`)}`,
2227
- );
2228
- }
2229
- }
2230
- }
2231
- });
2232
-
2233
- // ---------------------------------------------------------------------------
2234
- // trellis search
2235
- // ---------------------------------------------------------------------------
2236
-
2237
- program
2238
- .command('search')
2239
- .description('Semantic search across all embedded content')
2240
- .argument('<query>', 'Natural language search query')
2241
- .option('-p, --path <path>', 'Repository path', '.')
2242
- .option('-l, --limit <n>', 'Max results', '10')
2243
- .option(
2244
- '-t, --type <types>',
2245
- 'Filter by chunk type(s), comma-separated (issue_title,issue_desc,milestone_msg,markdown,code_entity,doc_comment,summary_md)',
2246
- )
2247
- .action(async (query, opts) => {
2248
- const rootPath = resolve(opts.path);
2249
- if (!TrellisVcsEngine.isRepo(rootPath)) {
2250
- console.error(
2251
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2252
- );
2253
- process.exit(1);
2254
- }
2255
-
2256
- const engine = new TrellisVcsEngine({ rootPath });
2257
- engine.open();
2258
-
2259
- const { EmbeddingManager } = require('../embeddings/index.js');
2260
-
2261
- const dbPath = join(rootPath, '.trellis', 'embeddings.db');
2262
- const manager = new EmbeddingManager(dbPath);
2263
-
2264
- try {
2265
- const searchOpts: any = {
2266
- limit: parseInt(opts.limit, 10) || 10,
2267
- };
2268
- if (opts.type) {
2269
- searchOpts.types = opts.type.split(',').map((t: string) => t.trim());
2270
- }
2271
-
2272
- const results = await manager.search(query, searchOpts);
2273
-
2274
- if (results.length === 0) {
2275
- console.log(
2276
- chalk.dim(
2277
- 'No results found. Try `trellis reindex` to build the index.',
2278
- ),
2279
- );
2280
- return;
2281
- }
2282
-
2283
- console.log(
2284
- chalk.bold(
2285
- `Search results for ${chalk.cyan(`"${query}"`)} (${results.length})\n`,
2286
- ),
2287
- );
2288
-
2289
- for (const r of results) {
2290
- const score = (r.score * 100).toFixed(1);
2291
- const typeTag = chalk.dim(`[${r.chunk.chunkType}]`);
2292
- const filePart = r.chunk.filePath
2293
- ? chalk.dim(` ${r.chunk.filePath}`)
2294
- : '';
2295
- const preview = r.chunk.content.slice(0, 120).replace(/\n/g, ' ');
2296
-
2297
- console.log(` ${chalk.green(`${score}%`)} ${typeTag}${filePart}`);
2298
- console.log(` ${chalk.dim(preview)}`);
2299
- console.log();
2300
- }
2301
- } finally {
2302
- manager.close();
2303
- }
2304
- });
2305
-
2306
- // ---------------------------------------------------------------------------
2307
- // trellis reindex
2308
- // ---------------------------------------------------------------------------
2309
-
2310
- program
2311
- .command('reindex')
2312
- .description('Rebuild the semantic embedding index')
2313
- .option('-p, --path <path>', 'Repository path', '.')
2314
- .action(async (opts) => {
2315
- const rootPath = resolve(opts.path);
2316
- if (!TrellisVcsEngine.isRepo(rootPath)) {
2317
- console.error(
2318
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2319
- );
2320
- process.exit(1);
2321
- }
2322
-
2323
- const engine = new TrellisVcsEngine({ rootPath });
2324
- engine.open();
2325
-
2326
- const { EmbeddingManager } = require('../embeddings/index.js');
2327
-
2328
- const dbPath = join(rootPath, '.trellis', 'embeddings.db');
2329
- const manager = new EmbeddingManager(dbPath);
2330
-
2331
- try {
2332
- console.log(chalk.dim('Loading embedding model…'));
2333
- const result = await manager.reindex(engine);
2334
- console.log(chalk.green(`✓ Indexed ${result.chunks} chunks`));
2335
-
2336
- const stats = manager.stats();
2337
- console.log(chalk.dim(` Types: ${JSON.stringify(stats.byType)}`));
2338
- } finally {
2339
- manager.close();
2340
- }
2341
- });
2342
-
2343
- // ---------------------------------------------------------------------------
2344
- // trellis ui
2345
- // ---------------------------------------------------------------------------
2346
-
2347
- program
2348
- .command('ui')
2349
- .description('Launch the interactive graph explorer in your browser')
2350
- .option('-p, --path <path>', 'Repository path', '.')
2351
- .option('--port <port>', 'Server port', '3333')
2352
- .option('--no-open', 'Do not auto-open browser')
2353
- .action(async (opts) => {
2354
- const rootPath = resolve(opts.path);
2355
- if (!TrellisVcsEngine.isRepo(rootPath)) {
2356
- console.error(
2357
- chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2358
- );
2359
- process.exit(1);
2360
- }
2361
-
2362
- const { startUIServer } = require('../ui/server.js');
2363
- const port = parseInt(opts.port, 10) || 3000;
2364
-
2365
- try {
2366
- const server = await startUIServer({ rootPath, port });
2367
- const url = `http://localhost:${server.port}`;
2368
- console.log(
2369
- chalk.green(`✓ Trellis Graph Explorer running at ${chalk.bold(url)}`),
2370
- );
2371
- console.log(chalk.dim(' Press Ctrl+C to stop\n'));
2372
-
2373
- // Auto-open browser
2374
- if (opts.open !== false) {
2375
- const { exec } = require('child_process');
2376
- const cmd =
2377
- process.platform === 'darwin'
2378
- ? 'open'
2379
- : process.platform === 'win32'
2380
- ? 'start'
2381
- : 'xdg-open';
2382
- exec(`${cmd} ${url}`);
2383
- }
2384
-
2385
- process.on('SIGINT', () => {
2386
- server.stop();
2387
- console.log(chalk.dim('\nServer stopped.'));
2388
- process.exit(0);
2389
- });
2390
- } catch (err: any) {
2391
- console.error(chalk.red(`Failed to start server: ${err.message}`));
2392
- process.exit(1);
2393
- }
2394
- });
2395
-
2396
- // ---------------------------------------------------------------------------
2397
- // Kernel helper — boots a TrellisKernel from a .trellis directory
2398
- // ---------------------------------------------------------------------------
2399
-
2400
- function bootKernel(rootPath: string): TrellisKernel {
2401
- const dbPath = join(rootPath, '.trellis', 'kernel.db');
2402
- const backend = new SqliteKernelBackend(dbPath);
2403
- const kernel = new TrellisKernel({
2404
- backend,
2405
- agentId: `agent:${process.env.USER ?? 'unknown'}`,
2406
- });
2407
- kernel.boot();
2408
- return kernel;
2409
- }
2410
-
2411
- // ---------------------------------------------------------------------------
2412
- // trellis entity
2413
- // ---------------------------------------------------------------------------
2414
-
2415
- const entityCmd = program
2416
- .command('entity')
2417
- .description('Manage graph entities (generic CRUD)');
2418
-
2419
- entityCmd
2420
- .command('create')
2421
- .description('Create a new entity in the graph')
2422
- .requiredOption('-i, --id <id>', 'Entity ID (e.g. "project:my-app")')
2423
- .requiredOption('-t, --type <type>', 'Entity type (e.g. "Project", "User")')
2424
- .option('-a, --attr <attrs...>', 'Attributes as key=value pairs')
2425
- .option('-p, --path <path>', 'Repository path', '.')
2426
- .action(async (opts: any) => {
2427
- const rootPath = resolve(opts.path);
2428
- requireRepo(rootPath);
2429
-
2430
- const kernel = bootKernel(rootPath);
2431
- try {
2432
- const attrs: Record<string, any> = {};
2433
- if (opts.attr) {
2434
- for (const pair of opts.attr) {
2435
- const eq = pair.indexOf('=');
2436
- if (eq === -1) continue;
2437
- const key = pair.slice(0, eq);
2438
- let val: any = pair.slice(eq + 1);
2439
- // Auto-coerce numbers and booleans
2440
- if (val === 'true') val = true;
2441
- else if (val === 'false') val = false;
2442
- else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2443
- attrs[key] = val;
2444
- }
2445
- }
2446
-
2447
- const result = await kernel.createEntity(opts.id, opts.type, attrs);
2448
- console.log(chalk.green(`✓ Entity created: ${chalk.bold(opts.id)}`));
2449
- console.log(` ${chalk.dim('Type:')} ${opts.type}`);
2450
- console.log(` ${chalk.dim('Facts:')} ${result.factsDelta.added}`);
2451
- console.log(` ${chalk.dim('Op:')} ${result.op.hash.slice(0, 32)}…`);
2452
- } finally {
2453
- kernel.close();
2454
- }
2455
- });
2456
-
2457
- entityCmd
2458
- .command('get')
2459
- .description('Get an entity by ID')
2460
- .argument('<id>', 'Entity ID')
2461
- .option('-p, --path <path>', 'Repository path', '.')
2462
- .option('--json', 'Output as JSON')
2463
- .action((id: any, opts: any) => {
2464
- const rootPath = resolve(opts.path);
2465
- requireRepo(rootPath);
2466
-
2467
- const kernel = bootKernel(rootPath);
2468
- try {
2469
- const entity = kernel.getEntity(id);
2470
- if (!entity) {
2471
- console.error(chalk.red(`Entity not found: ${id}`));
2472
- process.exit(1);
2473
- }
2474
-
2475
- if (opts.json) {
2476
- const obj: Record<string, any> = { id: entity.id, type: entity.type };
2477
- for (const f of entity.facts) {
2478
- if (f.a !== 'type') obj[f.a] = f.v;
2479
- }
2480
- obj._links = entity.links.map((l) => ({
2481
- attribute: l.a,
2482
- target: l.e2,
2483
- source: l.e1,
2484
- }));
2485
- console.log(JSON.stringify(obj, null, 2));
2486
- return;
2487
- }
2488
-
2489
- console.log(chalk.bold(`${entity.type}: ${entity.id}\n`));
2490
- for (const f of entity.facts) {
2491
- console.log(` ${chalk.dim(f.a.padEnd(20))} ${f.v}`);
2492
- }
2493
- if (entity.links.length > 0) {
2494
- console.log(`\n ${chalk.bold('Links:')}`);
2495
- for (const l of entity.links) {
2496
- const dir = l.e1 === id ? '→' : '←';
2497
- const other = l.e1 === id ? l.e2 : l.e1;
2498
- console.log(` ${dir} ${chalk.dim(l.a)} ${other}`);
2499
- }
2500
- }
2501
- } finally {
2502
- kernel.close();
2503
- }
2504
- });
2505
-
2506
- entityCmd
2507
- .command('update')
2508
- .description('Update attributes on an existing entity')
2509
- .argument('<id>', 'Entity ID')
2510
- .requiredOption('-a, --attr <attrs...>', 'Attributes as key=value pairs')
2511
- .option('-p, --path <path>', 'Repository path', '.')
2512
- .action(async (id: any, opts: any) => {
2513
- const rootPath = resolve(opts.path);
2514
- requireRepo(rootPath);
2515
-
2516
- const kernel = bootKernel(rootPath);
2517
- try {
2518
- const updates: Record<string, any> = {};
2519
- for (const pair of opts.attr) {
2520
- const eq = pair.indexOf('=');
2521
- if (eq === -1) continue;
2522
- const key = pair.slice(0, eq);
2523
- let val: any = pair.slice(eq + 1);
2524
- if (val === 'true') val = true;
2525
- else if (val === 'false') val = false;
2526
- else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2527
- updates[key] = val;
2528
- }
2529
-
2530
- await kernel.updateEntity(id, updates);
2531
- console.log(chalk.green(`✓ Updated ${chalk.bold(id)}`));
2532
- for (const [k, v] of Object.entries(updates)) {
2533
- console.log(` ${chalk.dim(k)} = ${v}`);
2534
- }
2535
- } finally {
2536
- kernel.close();
2537
- }
2538
- });
2539
-
2540
- entityCmd
2541
- .command('delete')
2542
- .description('Delete an entity and all its facts/links')
2543
- .argument('<id>', 'Entity ID')
2544
- .option('-p, --path <path>', 'Repository path', '.')
2545
- .action(async (id: any, opts: any) => {
2546
- const rootPath = resolve(opts.path);
2547
- requireRepo(rootPath);
2548
-
2549
- const kernel = bootKernel(rootPath);
2550
- try {
2551
- const entity = kernel.getEntity(id);
2552
- if (!entity) {
2553
- console.error(chalk.red(`Entity not found: ${id}`));
2554
- process.exit(1);
2555
- }
2556
- await kernel.deleteEntity(id);
2557
- console.log(chalk.green(`✓ Deleted entity ${chalk.bold(id)}`));
2558
- } finally {
2559
- kernel.close();
2560
- }
2561
- });
2562
-
2563
- entityCmd
2564
- .command('list')
2565
- .description('List entities, optionally filtered by type')
2566
- .option('-t, --type <type>', 'Filter by entity type')
2567
- .option('-f, --filter <filters...>', 'Attribute filters as key=value')
2568
- .option('--json', 'Output as JSON')
2569
- .option('-p, --path <path>', 'Repository path', '.')
2570
- .action((opts: any) => {
2571
- const rootPath = resolve(opts.path);
2572
- requireRepo(rootPath);
2573
-
2574
- const kernel = bootKernel(rootPath);
2575
- try {
2576
- let filters: Record<string, any> | undefined;
2577
- if (opts.filter) {
2578
- filters = {};
2579
- for (const pair of opts.filter) {
2580
- const eq = pair.indexOf('=');
2581
- if (eq === -1) continue;
2582
- const key = pair.slice(0, eq);
2583
- let val: any = pair.slice(eq + 1);
2584
- if (val === 'true') val = true;
2585
- else if (val === 'false') val = false;
2586
- else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2587
- filters[key] = val;
2588
- }
2589
- }
2590
-
2591
- const entities = kernel.listEntities(opts.type, filters);
2592
-
2593
- if (opts.json) {
2594
- const out = entities.map((e) => {
2595
- const obj: Record<string, any> = { id: e.id, type: e.type };
2596
- for (const f of e.facts) {
2597
- if (f.a !== 'type') obj[f.a] = f.v;
2598
- }
2599
- return obj;
2600
- });
2601
- console.log(JSON.stringify(out, null, 2));
2602
- return;
2603
- }
2604
-
2605
- if (entities.length === 0) {
2606
- console.log(chalk.dim('No entities found.'));
2607
- return;
2608
- }
2609
-
2610
- const typeLabel = opts.type ? ` (type: ${opts.type})` : '';
2611
- console.log(chalk.bold(`Entities (${entities.length})${typeLabel}\n`));
2612
- for (const e of entities) {
2613
- const nameFact = e.facts.find((f) => f.a === 'name');
2614
- const name = nameFact ? ` ${chalk.white(String(nameFact.v))}` : '';
2615
- console.log(
2616
- ` ${chalk.cyan(e.type.padEnd(16))} ${chalk.bold(e.id)}${name}`,
2617
- );
2618
- }
2619
- } finally {
2620
- kernel.close();
2621
- }
2622
- });
2623
-
2624
- // ---------------------------------------------------------------------------
2625
- // trellis fact
2626
- // ---------------------------------------------------------------------------
2627
-
2628
- const factCmd = program
2629
- .command('fact')
2630
- .description('Add or remove individual facts on entities');
2631
-
2632
- factCmd
2633
- .command('add')
2634
- .description('Add a fact to an entity')
2635
- .argument('<entity>', 'Entity ID')
2636
- .argument('<attribute>', 'Attribute name')
2637
- .argument('<value>', 'Value')
2638
- .option('-p, --path <path>', 'Repository path', '.')
2639
- .action(async (entity: any, attribute: any, value: any, opts: any) => {
2640
- const rootPath = resolve(opts.path);
2641
- requireRepo(rootPath);
2642
-
2643
- const kernel = bootKernel(rootPath);
2644
- try {
2645
- // Auto-coerce
2646
- let val: any = value;
2647
- if (val === 'true') val = true;
2648
- else if (val === 'false') val = false;
2649
- else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2650
-
2651
- await kernel.addFact(entity, attribute, val);
2652
- console.log(
2653
- chalk.green(
2654
- `✓ Added fact: ${chalk.bold(entity)}.${attribute} = ${val}`,
2655
- ),
2656
- );
2657
- } finally {
2658
- kernel.close();
2659
- }
2660
- });
2661
-
2662
- factCmd
2663
- .command('remove')
2664
- .description('Remove a fact from an entity')
2665
- .argument('<entity>', 'Entity ID')
2666
- .argument('<attribute>', 'Attribute name')
2667
- .argument('<value>', 'Value to remove')
2668
- .option('-p, --path <path>', 'Repository path', '.')
2669
- .action(async (entity: any, attribute: any, value: any, opts: any) => {
2670
- const rootPath = resolve(opts.path);
2671
- requireRepo(rootPath);
2672
-
2673
- const kernel = bootKernel(rootPath);
2674
- try {
2675
- let val: any = value;
2676
- if (val === 'true') val = true;
2677
- else if (val === 'false') val = false;
2678
- else if (!isNaN(Number(val)) && val !== '') val = Number(val);
2679
-
2680
- await kernel.removeFact(entity, attribute, val);
2681
- console.log(
2682
- chalk.green(
2683
- `✓ Removed fact: ${chalk.bold(entity)}.${attribute} = ${val}`,
2684
- ),
2685
- );
2686
- } finally {
2687
- kernel.close();
2688
- }
2689
- });
2690
-
2691
- factCmd
2692
- .command('query')
2693
- .description('Query facts by entity or attribute')
2694
- .option('-e, --entity <id>', 'Filter by entity ID')
2695
- .option('-a, --attribute <attr>', 'Filter by attribute')
2696
- .option('--json', 'Output as JSON')
2697
- .option('-p, --path <path>', 'Repository path', '.')
2698
- .action((opts: any) => {
2699
- const rootPath = resolve(opts.path);
2700
- requireRepo(rootPath);
2701
-
2702
- const kernel = bootKernel(rootPath);
2703
- try {
2704
- const store = kernel.getStore();
2705
- let facts;
2706
-
2707
- if (opts.entity) {
2708
- facts = store.getFactsByEntity(opts.entity);
2709
- } else if (opts.attribute) {
2710
- facts = store.getFactsByAttribute(opts.attribute);
2711
- } else {
2712
- facts = store.getAllFacts();
2713
- }
2714
-
2715
- if (opts.json) {
2716
- console.log(JSON.stringify(facts, null, 2));
2717
- return;
2718
- }
2719
-
2720
- if (facts.length === 0) {
2721
- console.log(chalk.dim('No facts found.'));
2722
- return;
2723
- }
2724
-
2725
- console.log(chalk.bold(`Facts (${facts.length})\n`));
2726
- for (const f of facts.slice(0, 100)) {
2727
- console.log(
2728
- ` ${chalk.cyan(f.e.padEnd(24))} ${chalk.dim(f.a.padEnd(20))} ${f.v}`,
2729
- );
2730
- }
2731
- if (facts.length > 100) {
2732
- console.log(chalk.dim(` … +${facts.length - 100} more`));
2733
- }
2734
- } finally {
2735
- kernel.close();
2736
- }
2737
- });
2738
-
2739
- // ---------------------------------------------------------------------------
2740
- // trellis link
2741
- // ---------------------------------------------------------------------------
2742
-
2743
- const linkCmd = program
2744
- .command('link')
2745
- .description('Add or remove links between entities');
2746
-
2747
- linkCmd
2748
- .command('add')
2749
- .description('Add a link between two entities')
2750
- .argument('<source>', 'Source entity ID')
2751
- .argument('<attribute>', 'Relationship attribute')
2752
- .argument('<target>', 'Target entity ID')
2753
- .option('-p, --path <path>', 'Repository path', '.')
2754
- .action(async (source: any, attribute: any, target: any, opts: any) => {
2755
- const rootPath = resolve(opts.path);
2756
- requireRepo(rootPath);
2757
-
2758
- const kernel = bootKernel(rootPath);
2759
- try {
2760
- await kernel.addLink(source, attribute, target);
2761
- console.log(
2762
- chalk.green(
2763
- `✓ Link: ${chalk.bold(source)} —[${attribute}]→ ${chalk.bold(target)}`,
2764
- ),
2765
- );
2766
- } finally {
2767
- kernel.close();
2768
- }
2769
- });
2770
-
2771
- linkCmd
2772
- .command('remove')
2773
- .description('Remove a link between two entities')
2774
- .argument('<source>', 'Source entity ID')
2775
- .argument('<attribute>', 'Relationship attribute')
2776
- .argument('<target>', 'Target entity ID')
2777
- .option('-p, --path <path>', 'Repository path', '.')
2778
- .action(async (source: any, attribute: any, target: any, opts: any) => {
2779
- const rootPath = resolve(opts.path);
2780
- requireRepo(rootPath);
2781
-
2782
- const kernel = bootKernel(rootPath);
2783
- try {
2784
- await kernel.removeLink(source, attribute, target);
2785
- console.log(
2786
- chalk.green(
2787
- `✓ Removed: ${chalk.bold(source)} —[${attribute}]→ ${chalk.bold(target)}`,
2788
- ),
2789
- );
2790
- } finally {
2791
- kernel.close();
2792
- }
2793
- });
2794
-
2795
- linkCmd
2796
- .command('query')
2797
- .description('Query links for an entity')
2798
- .option('-e, --entity <id>', 'Entity ID')
2799
- .option('-a, --attribute <attr>', 'Relationship attribute')
2800
- .option('--json', 'Output as JSON')
2801
- .option('-p, --path <path>', 'Repository path', '.')
2802
- .action((opts: any) => {
2803
- const rootPath = resolve(opts.path);
2804
- requireRepo(rootPath);
2805
-
2806
- const kernel = bootKernel(rootPath);
2807
- try {
2808
- const store = kernel.getStore();
2809
- let links;
2810
-
2811
- if (opts.entity && opts.attribute) {
2812
- links = store.getLinksByEntityAndAttribute(opts.entity, opts.attribute);
2813
- } else if (opts.entity) {
2814
- links = store.getLinksByEntity(opts.entity);
2815
- } else if (opts.attribute) {
2816
- links = store.getLinksByAttribute(opts.attribute);
2817
- } else {
2818
- links = store.getAllLinks();
2819
- }
2820
-
2821
- if (opts.json) {
2822
- console.log(JSON.stringify(links, null, 2));
2823
- return;
2824
- }
2825
-
2826
- if (links.length === 0) {
2827
- console.log(chalk.dim('No links found.'));
2828
- return;
2829
- }
2830
-
2831
- console.log(chalk.bold(`Links (${links.length})\n`));
2832
- for (const l of links.slice(0, 100)) {
2833
- console.log(
2834
- ` ${chalk.cyan(l.e1)} —[${chalk.dim(l.a)}]→ ${chalk.cyan(l.e2)}`,
2835
- );
2836
- }
2837
- if (links.length > 100) {
2838
- console.log(chalk.dim(` … +${links.length - 100} more`));
2839
- }
2840
- } finally {
2841
- kernel.close();
2842
- }
2843
- });
2844
-
2845
- // ---------------------------------------------------------------------------
2846
- // trellis query
2847
- // ---------------------------------------------------------------------------
2848
-
2849
- program
2850
- .command('query')
2851
- .description('Execute an EQL-S query against the graph')
2852
- .argument(
2853
- '<query>',
2854
- 'EQL-S query string (or "find ?e where attr = value" shorthand)',
2855
- )
2856
- .option('-p, --path <path>', 'Repository path', '.')
2857
- .option('--json', 'Output as JSON')
2858
- .action((queryStr: string, opts: any) => {
2859
- const rootPath = resolve(opts.path);
2860
- requireRepo(rootPath);
2861
-
2862
- const kernel = bootKernel(rootPath);
2863
- try {
2864
- const store = kernel.getStore();
2865
- const engine = new QueryEngine(store);
2866
-
2867
- let q;
2868
- try {
2869
- q = parseSimple(queryStr);
2870
- } catch {
2871
- try {
2872
- q = parseQuery(queryStr);
2873
- } catch (e: any) {
2874
- console.error(chalk.red(`Parse error: ${e.message}`));
2875
- process.exit(1);
2876
- return;
2877
- }
2878
- }
2879
-
2880
- const result = engine.execute(q!);
2881
-
2882
- if (opts.json) {
2883
- console.log(JSON.stringify(result.bindings, null, 2));
2884
- } else {
2885
- if (result.count === 0) {
2886
- console.log(chalk.dim('No results.'));
2887
- } else {
2888
- // Determine columns
2889
- const cols =
2890
- result.bindings.length > 0 ? Object.keys(result.bindings[0]) : [];
2891
-
2892
- // Print header
2893
- console.log(chalk.bold(cols.map((c) => `?${c}`).join('\t')));
2894
- console.log(chalk.dim('─'.repeat(cols.length * 20)));
2895
-
2896
- // Print rows
2897
- for (const row of result.bindings) {
2898
- console.log(cols.map((c) => String(row[c] ?? '')).join('\t'));
2899
- }
2900
-
2901
- console.log(
2902
- chalk.dim(
2903
- `\n${result.count} result(s) in ${result.executionTime.toFixed(1)}ms`,
2904
- ),
2905
- );
2906
- }
2907
- }
2908
- } finally {
2909
- kernel.close();
2910
- }
2911
- });
2912
-
2913
- // ---------------------------------------------------------------------------
2914
- // trellis repl
2915
- // ---------------------------------------------------------------------------
2916
-
2917
- program
2918
- .command('repl')
2919
- .description('Interactive EQL-S query shell')
2920
- .option('-p, --path <path>', 'Repository path', '.')
2921
- .action(async (opts: any) => {
2922
- const rootPath = resolve(opts.path);
2923
- requireRepo(rootPath);
2924
-
2925
- const kernel = bootKernel(rootPath);
2926
- const store = kernel.getStore();
2927
- const engine = new QueryEngine(store);
2928
-
2929
- console.log(chalk.cyan.bold('Trellis EQL-S REPL'));
2930
- console.log(
2931
- chalk.dim(
2932
- 'Type EQL-S queries or "find ?e where attr = value" shorthand.',
2933
- ),
2934
- );
2935
- console.log(chalk.dim('Type .exit to quit, .help for help.\n'));
2936
-
2937
- const readline = await import('readline');
2938
- const rl = readline.createInterface({
2939
- input: process.stdin,
2940
- output: process.stdout,
2941
- prompt: chalk.green('eql> '),
2942
- });
2943
-
2944
- rl.prompt();
2945
-
2946
- rl.on('line', (line: string) => {
2947
- const trimmed = line.trim();
2948
- if (!trimmed) {
2949
- rl.prompt();
2950
- return;
2951
- }
2952
-
2953
- if (trimmed === '.exit' || trimmed === '.quit') {
2954
- kernel.close();
2955
- rl.close();
2956
- return;
2957
- }
2958
-
2959
- if (trimmed === '.help') {
2960
- console.log(`
2961
- ${chalk.bold('EQL-S Query Syntax:')}
2962
- ${chalk.cyan('SELECT')} ?var1 ?var2 ${chalk.cyan('WHERE')} { patterns } [${chalk.cyan('FILTER')} ...] [${chalk.cyan('ORDER BY')} ...] [${chalk.cyan('LIMIT')} n]
2963
-
2964
- ${chalk.bold('Pattern types:')}
2965
- ${chalk.yellow('[?e "attr" "value"]')} Fact pattern (entity, attribute, value)
2966
- ${chalk.yellow('(?src "rel" ?tgt)')} Link pattern (source, relationship, target)
2967
- ${chalk.yellow('NOT [?e "attr" ?v]')} Negation
2968
- ${chalk.yellow('OR { ... } { ... }')} Disjunction
2969
-
2970
- ${chalk.bold('Shorthand:')}
2971
- ${chalk.yellow('find ?e where type = "Project"')}
2972
-
2973
- ${chalk.bold('Commands:')}
2974
- .exit / .quit Exit the REPL
2975
- .stats Show store statistics
2976
- .help Show this help
2977
- `);
2978
- rl.prompt();
2979
- return;
2980
- }
2981
-
2982
- if (trimmed === '.stats') {
2983
- const facts = store.getAllFacts();
2984
- const links = store.getAllLinks();
2985
- const types = new Set(
2986
- facts.filter((f) => f.a === 'type').map((f) => f.v),
2987
- );
2988
- console.log(` Facts: ${facts.length}`);
2989
- console.log(` Links: ${links.length}`);
2990
- console.log(` Entity types: ${[...types].join(', ') || '(none)'}`);
2991
- rl.prompt();
2992
- return;
2993
- }
2994
-
2995
- try {
2996
- let q;
2997
- try {
2998
- q = parseSimple(trimmed);
2999
- } catch {
3000
- q = parseQuery(trimmed);
3001
- }
3002
-
3003
- const result = engine.execute(q);
3004
-
3005
- if (result.count === 0) {
3006
- console.log(chalk.dim('No results.'));
3007
- } else {
3008
- const cols = Object.keys(result.bindings[0]);
3009
- console.log(chalk.bold(cols.map((c) => `?${c}`).join('\t')));
3010
- for (const row of result.bindings) {
3011
- console.log(cols.map((c) => String(row[c] ?? '')).join('\t'));
3012
- }
3013
- console.log(
3014
- chalk.dim(
3015
- `${result.count} result(s) in ${result.executionTime.toFixed(1)}ms`,
3016
- ),
3017
- );
3018
- }
3019
- } catch (e: any) {
3020
- console.error(chalk.red(`Error: ${e.message}`));
3021
- }
3022
-
3023
- rl.prompt();
3024
- });
3025
-
3026
- rl.on('close', () => {
3027
- kernel.close();
3028
- console.log(chalk.dim('Goodbye.'));
3029
- });
3030
- });
3031
-
3032
- // ---------------------------------------------------------------------------
3033
- // trellis ontology
3034
- // ---------------------------------------------------------------------------
3035
-
3036
- const ontologyCmd = program
3037
- .command('ontology')
3038
- .description('Manage and inspect ontology schemas');
3039
-
3040
- ontologyCmd
3041
- .command('list')
3042
- .description('List all registered ontologies (built-in + custom)')
3043
- .action(() => {
3044
- const registry = new OntologyRegistry();
3045
- for (const o of builtinOntologies) registry.register(o);
3046
-
3047
- const schemas = registry.list();
3048
- if (schemas.length === 0) {
3049
- console.log(chalk.dim('No ontologies registered.'));
3050
- return;
3051
- }
3052
-
3053
- console.log(chalk.bold(`Ontologies (${schemas.length})\n`));
3054
- for (const s of schemas) {
3055
- console.log(` ${chalk.cyan(s.id)} ${chalk.dim(`v${s.version}`)}`);
3056
- console.log(
3057
- ` ${s.name}${s.description ? ` — ${chalk.dim(s.description)}` : ''}`,
3058
- );
3059
- console.log(` Entities: ${s.entities.map((e) => e.name).join(', ')}`);
3060
- console.log(
3061
- ` Relations: ${s.relations.map((r) => r.name).join(', ')}`,
3062
- );
3063
- console.log();
3064
- }
3065
- });
3066
-
3067
- ontologyCmd
3068
- .command('inspect')
3069
- .description('Inspect a specific ontology or entity type')
3070
- .argument(
3071
- '<name>',
3072
- 'Ontology ID (e.g. "trellis:project") or entity type name (e.g. "Project")',
3073
- )
3074
- .action((name: string) => {
3075
- const registry = new OntologyRegistry();
3076
- for (const o of builtinOntologies) registry.register(o);
3077
-
3078
- // Try as ontology ID first
3079
- const schema = registry.get(name);
3080
- if (schema) {
3081
- console.log(
3082
- chalk.bold(
3083
- `${schema.name} ${chalk.dim(`(${schema.id} v${schema.version})`)}`,
3084
- ),
3085
- );
3086
- if (schema.description) console.log(chalk.dim(schema.description));
3087
- console.log();
3088
-
3089
- console.log(chalk.bold('Entity Types:'));
3090
- for (const e of schema.entities) {
3091
- console.log(
3092
- `\n ${chalk.cyan.bold(e.name)}${e.abstract ? chalk.dim(' (abstract)') : ''}${e.extends ? chalk.dim(` extends ${e.extends}`) : ''}`,
3093
- );
3094
- if (e.description) console.log(` ${chalk.dim(e.description)}`);
3095
- for (const a of e.attributes) {
3096
- const flags = [
3097
- a.required ? chalk.red('required') : null,
3098
- a.enum ? `enum[${a.enum.join('|')}]` : null,
3099
- a.default !== undefined ? `default=${a.default}` : null,
3100
- ]
3101
- .filter(Boolean)
3102
- .join(', ');
3103
- console.log(
3104
- ` ${chalk.yellow(a.name)}: ${a.type}${flags ? ` (${flags})` : ''}`,
3105
- );
3106
- }
3107
- }
3108
-
3109
- if (schema.relations.length > 0) {
3110
- console.log(chalk.bold('\nRelations:'));
3111
- for (const r of schema.relations) {
3112
- console.log(
3113
- ` ${chalk.yellow(r.name)}: ${r.sourceTypes.join('|')} → ${r.targetTypes.join('|')}${r.cardinality ? ` [${r.cardinality}]` : ''}`,
3114
- );
3115
- }
3116
- }
3117
- return;
3118
- }
3119
-
3120
- // Try as entity type name
3121
- const def = registry.getEntityDef(name);
3122
- if (def) {
3123
- const ontId = registry.getEntityOntology(name);
3124
- console.log(chalk.bold(`${def.name}`) + chalk.dim(` (from ${ontId})`));
3125
- if (def.description) console.log(chalk.dim(def.description));
3126
- if (def.abstract)
3127
- console.log(chalk.dim('(abstract — cannot be instantiated)'));
3128
- if (def.extends) console.log(chalk.dim(`extends ${def.extends}`));
3129
-
3130
- console.log(chalk.bold('\nAttributes:'));
3131
- for (const a of def.attributes) {
3132
- const flags = [
3133
- a.required ? chalk.red('required') : null,
3134
- a.enum ? `enum[${a.enum.join('|')}]` : null,
3135
- a.default !== undefined ? `default=${a.default}` : null,
3136
- ]
3137
- .filter(Boolean)
3138
- .join(', ');
3139
- console.log(
3140
- ` ${chalk.yellow(a.name)}: ${a.type}${flags ? ` (${flags})` : ''}`,
3141
- );
3142
- if (a.description) console.log(` ${chalk.dim(a.description)}`);
3143
- }
3144
-
3145
- const rels = registry.getRelationsForType(name);
3146
- if (rels.length > 0) {
3147
- console.log(chalk.bold('\nRelations:'));
3148
- for (const r of rels) {
3149
- const dir = r.sourceTypes.includes(name) ? '→' : '←';
3150
- console.log(
3151
- ` ${chalk.yellow(r.name)} ${dir} ${r.sourceTypes.includes(name) ? r.targetTypes.join('|') : r.sourceTypes.join('|')}`,
3152
- );
3153
- }
3154
- }
3155
- return;
3156
- }
3157
-
3158
- console.error(chalk.red(`Unknown ontology or entity type: "${name}"`));
3159
- console.log(
3160
- chalk.dim(
3161
- 'Available ontologies: ' +
3162
- registry
3163
- .list()
3164
- .map((s) => s.id)
3165
- .join(', '),
3166
- ),
3167
- );
3168
- console.log(
3169
- chalk.dim('Available types: ' + registry.listEntityTypes().join(', ')),
3170
- );
3171
- });
3172
-
3173
- ontologyCmd
3174
- .command('validate')
3175
- .description(
3176
- 'Validate all entities in the graph against registered ontologies',
3177
- )
3178
- .option('-p, --path <path>', 'Repository path', '.')
3179
- .option('--strict', 'Treat unknown types as errors')
3180
- .action((opts: any) => {
3181
- const rootPath = resolve(opts.path);
3182
- requireRepo(rootPath);
3183
-
3184
- const kernel = bootKernel(rootPath);
3185
- try {
3186
- const registry = new OntologyRegistry();
3187
- for (const o of builtinOntologies) registry.register(o);
3188
-
3189
- const store = kernel.getStore();
3190
- const result = validateStore(store, registry);
3191
-
3192
- if (result.errors.length > 0) {
3193
- console.log(chalk.red.bold(`✗ ${result.errors.length} error(s):\n`));
3194
- for (const err of result.errors) {
3195
- console.log(
3196
- ` ${chalk.red('ERROR')} ${chalk.bold(err.entityId)} (${err.entityType}) → ${err.field}: ${err.message}`,
3197
- );
3198
- }
3199
- }
3200
-
3201
- if (result.warnings.length > 0) {
3202
- console.log(
3203
- chalk.yellow.bold(`\n⚠ ${result.warnings.length} warning(s):\n`),
3204
- );
3205
- for (const w of result.warnings) {
3206
- console.log(
3207
- ` ${chalk.yellow('WARN')} ${chalk.bold(w.entityId)} (${w.entityType}) → ${w.field}: ${w.message}`,
3208
- );
3209
- }
3210
- }
3211
-
3212
- if (result.valid && result.warnings.length === 0) {
3213
- console.log(chalk.green('✓ All entities pass ontology validation.'));
3214
- } else if (result.valid) {
3215
- console.log(chalk.green('\n✓ Valid (with warnings).'));
3216
- } else {
3217
- console.log(chalk.red('\n✗ Validation failed.'));
3218
- }
3219
- } finally {
3220
- kernel.close();
3221
- }
3222
- });
3223
-
3224
- // ---------------------------------------------------------------------------
3225
- // trellis ask
3226
- // ---------------------------------------------------------------------------
3227
-
3228
- program
3229
- .command('ask')
3230
- .description('Natural language search over the graph (semantic search)')
3231
- .argument('<question>', 'Natural language query')
3232
- .option('-p, --path <path>', 'Repository path', '.')
3233
- .option('-n, --limit <n>', 'Max results', '5')
3234
- .option('--json', 'Output as JSON')
3235
- .option('--rag', 'Output as RAG context (for LLM consumption)')
3236
- .action(async (question: string, opts: any) => {
3237
- const rootPath = resolve(opts.path);
3238
- requireRepo(rootPath);
3239
-
3240
- const dbPath = join(rootPath, '.trellis', 'embeddings.db');
3241
- const vectorStore = new VectorStore(dbPath);
3242
-
3243
- try {
3244
- const limit = parseInt(opts.limit, 10) || 5;
3245
-
3246
- if (opts.rag) {
3247
- const ctx = await buildRAGContext(question, vectorStore, embed, {
3248
- maxChunks: limit,
3249
- });
3250
-
3251
- if (opts.json) {
3252
- console.log(JSON.stringify(ctx, null, 2));
3253
- } else {
3254
- console.log(chalk.bold.cyan('RAG Context'));
3255
- console.log(chalk.dim(`Query: ${ctx.query}`));
3256
- console.log(
3257
- chalk.dim(
3258
- `Chunks: ${ctx.chunks.length} | ~${ctx.estimatedTokens} tokens\n`,
3259
- ),
3260
- );
3261
- for (const c of ctx.chunks) {
3262
- console.log(
3263
- chalk.yellow(
3264
- `[${c.score.toFixed(3)}] ${c.entityId} (${c.chunkType})`,
3265
- ),
3266
- );
3267
- console.log(c.content);
3268
- console.log();
3269
- }
3270
- }
3271
- } else {
3272
- const queryVector = await embed(question);
3273
- const results = vectorStore.search(queryVector, { limit });
3274
-
3275
- if (opts.json) {
3276
- console.log(
3277
- JSON.stringify(
3278
- results.map((r) => ({
3279
- score: r.score,
3280
- entityId: r.chunk.entityId,
3281
- chunkType: r.chunk.chunkType,
3282
- content: r.chunk.content,
3283
- })),
3284
- null,
3285
- 2,
3286
- ),
3287
- );
3288
- } else {
3289
- if (results.length === 0) {
3290
- console.log(
3291
- chalk.dim(
3292
- 'No results. Run `trellis reindex` first to build the embedding index.',
3293
- ),
3294
- );
3295
- } else {
3296
- console.log(chalk.bold(`Results for: "${question}"\n`));
3297
- for (const r of results) {
3298
- const score = chalk.dim(`[${r.score.toFixed(3)}]`);
3299
- const entity = chalk.cyan(r.chunk.entityId);
3300
- const type = chalk.dim(`(${r.chunk.chunkType})`);
3301
- console.log(`${score} ${entity} ${type}`);
3302
- const preview =
3303
- r.chunk.content.length > 200
3304
- ? r.chunk.content.slice(0, 200) + '…'
3305
- : r.chunk.content;
3306
- console.log(` ${preview}\n`);
3307
- }
3308
- }
3309
- }
3310
- }
3311
- } catch (err: any) {
3312
- if (err.message?.includes('No transformers')) {
3313
- console.error(chalk.red('Embedding model not available.'));
3314
- console.error(chalk.dim('Install: bun add @huggingface/transformers'));
3315
- } else {
3316
- console.error(chalk.red(`Error: ${err.message}`));
3317
- }
3318
- } finally {
3319
- vectorStore.close();
3320
- }
3321
- });
3322
-
3323
- // ---------------------------------------------------------------------------
3324
- // Helpers
3325
- // ---------------------------------------------------------------------------
3326
-
3327
- function formatOpKind(kind: string): string {
3328
- const kindMap: Record<string, string> = {
3329
- 'vcs:fileAdd': chalk.green('+add'),
3330
- 'vcs:fileModify': chalk.yellow('~mod'),
3331
- 'vcs:fileDelete': chalk.red('-del'),
3332
- 'vcs:fileRename': chalk.blue('→ren'),
3333
- 'vcs:branchCreate': chalk.magenta('⊕branch'),
3334
- 'vcs:branchAdvance': chalk.magenta('→branch'),
3335
- 'vcs:milestoneCreate': chalk.cyan('★milestone'),
3336
- 'vcs:checkpointCreate': chalk.dim('●checkpoint'),
3337
- };
3338
- return kindMap[kind] ?? chalk.dim(kind);
3339
- }
3340
-
3341
- function formatRelativeTime(iso: string): string {
3342
- const now = Date.now();
3343
- const then = new Date(iso).getTime();
3344
- const diff = now - then;
3345
-
3346
- if (diff < 60_000) return 'just now';
3347
- if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
3348
- if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
3349
- return `${Math.floor(diff / 86_400_000)}d ago`;
3350
- }
3351
-
3352
- // ---------------------------------------------------------------------------
3353
- // Run
3354
- // ---------------------------------------------------------------------------
3355
-
3356
- program.parse();