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.
- package/dist/cli/index.js +1 -1
- package/dist/embeddings/index.js +1 -1
- package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
- package/package.json +2 -10
- package/dist/transformers.node-bx3q9d7k.js +0 -33130
- package/src/cli/index.ts +0 -3356
- package/src/core/agents/harness.ts +0 -380
- package/src/core/agents/index.ts +0 -18
- package/src/core/agents/types.ts +0 -90
- package/src/core/index.ts +0 -118
- package/src/core/kernel/middleware.ts +0 -44
- package/src/core/kernel/trellis-kernel.ts +0 -593
- package/src/core/ontology/builtins.ts +0 -248
- package/src/core/ontology/index.ts +0 -34
- package/src/core/ontology/registry.ts +0 -209
- package/src/core/ontology/types.ts +0 -124
- package/src/core/ontology/validator.ts +0 -382
- package/src/core/persist/backend.ts +0 -74
- package/src/core/persist/sqlite-backend.ts +0 -298
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/registry.ts +0 -322
- package/src/core/plugins/types.ts +0 -126
- package/src/core/query/datalog.ts +0 -188
- package/src/core/query/engine.ts +0 -370
- package/src/core/query/index.ts +0 -34
- package/src/core/query/parser.ts +0 -481
- package/src/core/query/types.ts +0 -200
- package/src/core/store/eav-store.ts +0 -467
- package/src/decisions/auto-capture.ts +0 -136
- package/src/decisions/hooks.ts +0 -163
- package/src/decisions/index.ts +0 -261
- package/src/decisions/types.ts +0 -103
- package/src/embeddings/auto-embed.ts +0 -248
- package/src/embeddings/chunker.ts +0 -327
- package/src/embeddings/index.ts +0 -48
- package/src/embeddings/model.ts +0 -112
- package/src/embeddings/search.ts +0 -305
- package/src/embeddings/store.ts +0 -313
- package/src/embeddings/types.ts +0 -92
- package/src/engine.ts +0 -1125
- package/src/garden/cluster.ts +0 -330
- package/src/garden/garden.ts +0 -306
- package/src/garden/index.ts +0 -29
- package/src/git/git-exporter.ts +0 -286
- package/src/git/git-importer.ts +0 -329
- package/src/git/git-reader.ts +0 -189
- package/src/git/index.ts +0 -22
- package/src/identity/governance.ts +0 -211
- package/src/identity/identity.ts +0 -224
- package/src/identity/index.ts +0 -30
- package/src/identity/signing-middleware.ts +0 -97
- package/src/index.ts +0 -29
- package/src/links/index.ts +0 -49
- package/src/links/lifecycle.ts +0 -400
- package/src/links/parser.ts +0 -484
- package/src/links/ref-index.ts +0 -186
- package/src/links/resolver.ts +0 -314
- package/src/links/types.ts +0 -108
- package/src/mcp/index.ts +0 -22
- package/src/mcp/server.ts +0 -1278
- package/src/semantic/csharp-parser.ts +0 -493
- package/src/semantic/go-parser.ts +0 -585
- package/src/semantic/index.ts +0 -34
- package/src/semantic/java-parser.ts +0 -456
- package/src/semantic/python-parser.ts +0 -659
- package/src/semantic/ruby-parser.ts +0 -446
- package/src/semantic/rust-parser.ts +0 -784
- package/src/semantic/semantic-merge.ts +0 -210
- package/src/semantic/ts-parser.ts +0 -681
- package/src/semantic/types.ts +0 -175
- package/src/sync/http-transport.ts +0 -144
- package/src/sync/index.ts +0 -43
- package/src/sync/memory-transport.ts +0 -66
- package/src/sync/multi-repo.ts +0 -200
- package/src/sync/reconciler.ts +0 -237
- package/src/sync/sync-engine.ts +0 -258
- package/src/sync/types.ts +0 -104
- package/src/sync/ws-transport.ts +0 -145
- package/src/ui/client.html +0 -695
- package/src/ui/server.ts +0 -419
- package/src/vcs/blob-store.ts +0 -124
- package/src/vcs/branch.ts +0 -150
- package/src/vcs/checkpoint.ts +0 -64
- package/src/vcs/decompose.ts +0 -469
- package/src/vcs/diff.ts +0 -409
- package/src/vcs/engine-context.ts +0 -26
- package/src/vcs/index.ts +0 -23
- package/src/vcs/issue.ts +0 -800
- package/src/vcs/merge.ts +0 -425
- package/src/vcs/milestone.ts +0 -124
- package/src/vcs/ops.ts +0 -59
- package/src/vcs/types.ts +0 -213
- package/src/vcs/vcs-middleware.ts +0 -81
- package/src/watcher/fs-watcher.ts +0 -255
- package/src/watcher/index.ts +0 -9
- 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();
|