@productbrain/cli 0.1.0-beta.9096 → 0.1.0-beta.9113

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.
@@ -1,773 +0,0 @@
1
- /**
2
- * pb method ls / pb method show <name> / pb method diff <name> / pb method publish <name>
3
- * WP-329 S1-S3: Method visibility CLI + publish.
4
- *
5
- * Lists all .productbrain/ skills and rules with per-surface adapter status,
6
- * shows detailed fork-detection output for a single method, diffs against
7
- * the Chain, and publishes to the Chain methods collection.
8
- *
9
- * Chain: WP-329 (Method Publish + Visibility), DEC-299 (JSON-default output),
10
- * DEC-598 (local-only monotonic versioning, Option B).
11
- */
12
- import { createHash } from 'node:crypto';
13
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
14
- import { join, resolve } from 'node:path';
15
- import { cwd } from 'node:process';
16
- import { MARKER } from '../generators/adapters.js';
17
- import { kernelCall, kernelCallWithSession } from '../lib/client.js';
18
- import { readSession } from '../lib/session.js';
19
- import { runCliCommand } from '../lib/runner.js';
20
- import { CLIError, ErrorCode } from '../lib/errors.js';
21
- // ---------------------------------------------------------------------------
22
- // Constants
23
- // ---------------------------------------------------------------------------
24
- const CODEX_MARKER_PREFIX = '<!-- auto-generated by pb handshake — source: .productbrain/';
25
- /**
26
- * Normalize path references in Cursor adapter bodies before comparison.
27
- * pb handshake rewrites .productbrain/rules/ → .cursor/rules/ in .cursor/ adapters.
28
- */
29
- function normalizeCursorAdapterBody(body) {
30
- return body.replace(/\.cursor\/rules\//g, '.productbrain/rules/');
31
- }
32
- // ---------------------------------------------------------------------------
33
- // Filesystem helpers
34
- // ---------------------------------------------------------------------------
35
- /** Returns project root (the directory containing .productbrain/). */
36
- function projectRoot() {
37
- // Walk up from cwd until we find .productbrain/, or fall back to cwd
38
- let dir = cwd();
39
- for (let i = 0; i < 8; i++) {
40
- if (existsSync(join(dir, '.productbrain')))
41
- return dir;
42
- const parent = resolve(dir, '..');
43
- if (parent === dir)
44
- break;
45
- dir = parent;
46
- }
47
- return cwd();
48
- }
49
- // Intentionally NOT folded into lib/frontmatter.ts (parseSetupFrontmatter): this
50
- // parser has different semantics (multi-line value support, MethodFrontmatter shape).
51
- /** Parse YAML-ish frontmatter block from a .md file (simple key: value, supports multi-line values). */
52
- function parseFrontmatter(content) {
53
- if (!content.startsWith('---')) {
54
- return { frontmatter: {}, body: content };
55
- }
56
- const end = content.indexOf('\n---', 3);
57
- if (end === -1) {
58
- return { frontmatter: {}, body: content };
59
- }
60
- const fmBlock = content.slice(4, end); // skip the opening ---\n
61
- const body = content.slice(end + 4); // skip the closing ---
62
- const frontmatter = {};
63
- let i = 0;
64
- const lines = fmBlock.split('\n');
65
- while (i < lines.length) {
66
- const line = lines[i];
67
- const colonIdx = line.indexOf(':');
68
- if (colonIdx < 1) {
69
- i++;
70
- continue;
71
- }
72
- const key = line.slice(0, colonIdx).trim();
73
- let value = line.slice(colonIdx + 1).trim();
74
- // Multi-line value: starts with >- or |
75
- if (value === '>-' || value === '>' || value === '|') {
76
- const parts = [];
77
- i++;
78
- while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) {
79
- parts.push(lines[i].trim());
80
- i++;
81
- }
82
- frontmatter[key] = parts.join(' ').trim();
83
- continue;
84
- }
85
- // Boolean values
86
- if (value === 'true') {
87
- frontmatter[key] = true;
88
- i++;
89
- continue;
90
- }
91
- if (value === 'false') {
92
- frontmatter[key] = false;
93
- i++;
94
- continue;
95
- }
96
- // Strip surrounding quotes
97
- if ((value.startsWith('"') && value.endsWith('"')) ||
98
- (value.startsWith("'") && value.endsWith("'"))) {
99
- value = value.slice(1, -1);
100
- }
101
- frontmatter[key] = value;
102
- i++;
103
- }
104
- return { frontmatter, body: body.trimStart() };
105
- }
106
- /**
107
- * Strip frontmatter from adapter files so we can compare body content.
108
- * Also strip CODEX MARKER comment line if present.
109
- */
110
- function extractAdapterBody(content) {
111
- // Strip frontmatter block if present
112
- let text = content;
113
- if (text.startsWith('---')) {
114
- const end = text.indexOf('\n---', 3);
115
- if (end !== -1) {
116
- text = text.slice(end + 4).trimStart();
117
- }
118
- }
119
- // Strip MARKER comment line (first line if it matches)
120
- const firstNewline = text.indexOf('\n');
121
- if (firstNewline !== -1) {
122
- const firstLine = text.slice(0, firstNewline).trim();
123
- if (firstLine.startsWith('<!--') && firstLine.includes('auto-generated by pb handshake')) {
124
- text = text.slice(firstNewline + 1).trimStart();
125
- }
126
- }
127
- return text.trim();
128
- }
129
- /** Load all methods from .productbrain/skills/ and .productbrain/rules/. */
130
- function loadAllSources(root) {
131
- const sources = [];
132
- const skillsDir = join(root, '.productbrain', 'skills');
133
- const rulesDir = join(root, '.productbrain', 'rules');
134
- for (const [dir, type] of [[skillsDir, 'skill'], [rulesDir, 'rule']]) {
135
- if (!existsSync(dir))
136
- continue;
137
- const files = readdirSync(dir).filter((f) => f.endsWith('.md'));
138
- for (const file of files) {
139
- const filePath = join(dir, file);
140
- let content;
141
- try {
142
- content = readFileSync(filePath, 'utf8');
143
- }
144
- catch {
145
- continue;
146
- }
147
- const { frontmatter, body } = parseFrontmatter(content);
148
- const stem = file.replace(/\.md$/, '');
149
- sources.push({
150
- name: stem,
151
- type,
152
- sourcePath: filePath,
153
- frontmatter,
154
- body,
155
- });
156
- }
157
- }
158
- return sources;
159
- }
160
- // ---------------------------------------------------------------------------
161
- // Adapter status detection
162
- // ---------------------------------------------------------------------------
163
- function checkCursor(root, source) {
164
- // Rules have adapters in .cursor/rules/<name>.mdc; skills do NOT have cursor adapters
165
- if (source.type === 'skill')
166
- return 'missing';
167
- const adapterPath = join(root, '.cursor', 'rules', `${source.name}.mdc`);
168
- if (!existsSync(adapterPath))
169
- return 'missing';
170
- let adapterContent;
171
- try {
172
- adapterContent = readFileSync(adapterPath, 'utf8');
173
- }
174
- catch {
175
- return 'missing';
176
- }
177
- const adapterBody = normalizeCursorAdapterBody(extractAdapterBody(adapterContent));
178
- const sourceBody = source.body.trim();
179
- return adapterBody === sourceBody ? 'ok' : 'forked';
180
- }
181
- function checkCodex(root, source) {
182
- // Only skills have codex adapters in .codex/skills/
183
- if (source.type === 'rule')
184
- return 'missing';
185
- const adapterPath = join(root, '.codex', 'skills', `${source.name}.md`);
186
- if (!existsSync(adapterPath))
187
- return 'missing';
188
- let adapterContent;
189
- try {
190
- adapterContent = readFileSync(adapterPath, 'utf8');
191
- }
192
- catch {
193
- return 'missing';
194
- }
195
- // Check MARKER presence
196
- if (!adapterContent.includes(CODEX_MARKER_PREFIX))
197
- return 'forked';
198
- const adapterBody = extractAdapterBody(adapterContent);
199
- const sourceBody = source.body.trim();
200
- return adapterBody === sourceBody ? 'ok' : 'forked';
201
- }
202
- function checkCopilot(root, source) {
203
- // Only rules contribute to copilot-instructions.md (skills are not included)
204
- if (source.type === 'skill')
205
- return 'missing';
206
- const copilotPath = join(root, '.github', 'copilot-instructions.md');
207
- if (!existsSync(copilotPath))
208
- return 'missing';
209
- let copilotContent;
210
- try {
211
- copilotContent = readFileSync(copilotPath, 'utf8');
212
- }
213
- catch {
214
- return 'missing';
215
- }
216
- // The Copilot instructions are hand-maintained, COMMITTED SSOT — they carry NO auto-gen MARKER,
217
- // so handshake classifies them as user-owned and never projects per-rule summaries into them
218
- // (DEC-1343). A markerless file is therefore in-sync by definition: it is not a pb write target,
219
- // so it can never be "forked" from the Chain. Reporting `forked` here (and telling the user to run
220
- // `pb handshake`) would be a permanent false out-of-sync on every committed Copilot file.
221
- if (!copilotContent.includes(MARKER))
222
- return 'ok';
223
- // pb-generated copilot-instructions.md (MARKER present) stores summaries (### name), not bodies.
224
- return copilotContent.includes(`### ${source.name}`) ? 'ok' : 'forked';
225
- }
226
- function checkAdapters(root, source) {
227
- return {
228
- cursor: checkCursor(root, source),
229
- codex: checkCodex(root, source),
230
- copilot: checkCopilot(root, source),
231
- };
232
- }
233
- // ---------------------------------------------------------------------------
234
- // Chain query helper
235
- // ---------------------------------------------------------------------------
236
- /** Attempt to fetch published Chain entry for a method by name. Returns null on failure. */
237
- async function fetchChainMethod(name) {
238
- try {
239
- const results = await kernelCall('chain.searchEntries', { query: name, collectionSlug: 'methods' });
240
- if (!Array.isArray(results))
241
- return null;
242
- // Find exact name match in results
243
- const match = results.find((r) => {
244
- const entryName = (r.data?.name ?? r.name);
245
- return entryName === name;
246
- });
247
- if (!match)
248
- return null;
249
- const data = match.data ?? {};
250
- const fmRaw = data.frontmatter;
251
- let frontmatter;
252
- if (fmRaw) {
253
- try {
254
- frontmatter = JSON.parse(fmRaw);
255
- }
256
- catch { /* ignore */ }
257
- }
258
- return {
259
- _id: match._id,
260
- version: data.version != null ? String(data.version) : undefined,
261
- publishedAt: data.publishedAt,
262
- publishedBy: data.publishedBy,
263
- manifestHash: data.manifestHash,
264
- body: data.body,
265
- frontmatter,
266
- };
267
- }
268
- catch {
269
- // Chain unreachable or collection doesn't exist yet — not fatal
270
- return null;
271
- }
272
- }
273
- // ---------------------------------------------------------------------------
274
- // Diff helpers
275
- // ---------------------------------------------------------------------------
276
- /**
277
- * Produce a minimal unified diff between two texts.
278
- * Returns an array of diff lines (context lines prefixed with space,
279
- * removals prefixed with -, additions prefixed with +).
280
- * Context window: 3 lines around each change.
281
- */
282
- function computeUnifiedDiff(oldText, newText, context = 3) {
283
- const oldLines = oldText.split('\n');
284
- const newLines = newText.split('\n');
285
- // Myers-style LCS — compute edit script via DP table
286
- const m = oldLines.length;
287
- const n = newLines.length;
288
- // dp[i][j] = length of LCS for oldLines[0..i-1] and newLines[0..j-1]
289
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
290
- for (let i = 1; i <= m; i++) {
291
- for (let j = 1; j <= n; j++) {
292
- if (oldLines[i - 1] === newLines[j - 1]) {
293
- dp[i][j] = dp[i - 1][j - 1] + 1;
294
- }
295
- else {
296
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
297
- }
298
- }
299
- }
300
- const edits = [];
301
- let i = m;
302
- let j = n;
303
- while (i > 0 || j > 0) {
304
- if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
305
- edits.unshift({ op: ' ', line: oldLines[i - 1], oldIdx: i - 1, newIdx: j - 1 });
306
- i--;
307
- j--;
308
- }
309
- else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
310
- edits.unshift({ op: '+', line: newLines[j - 1], newIdx: j - 1 });
311
- j--;
312
- }
313
- else {
314
- edits.unshift({ op: '-', line: oldLines[i - 1], oldIdx: i - 1 });
315
- i--;
316
- }
317
- }
318
- if (edits.length === 0)
319
- return [];
320
- // Find indices of changed lines
321
- const changedAt = new Set();
322
- for (let k = 0; k < edits.length; k++) {
323
- if (edits[k].op !== ' ')
324
- changedAt.add(k);
325
- }
326
- if (changedAt.size === 0)
327
- return []; // identical
328
- // Build context windows — include context lines around changes
329
- const included = new Set();
330
- for (const idx of changedAt) {
331
- for (let d = -context; d <= context; d++) {
332
- const pos = idx + d;
333
- if (pos >= 0 && pos < edits.length)
334
- included.add(pos);
335
- }
336
- }
337
- const output = [];
338
- let prevIncluded = false;
339
- for (let k = 0; k < edits.length; k++) {
340
- if (included.has(k)) {
341
- if (!prevIncluded && k > 0) {
342
- output.push('...');
343
- }
344
- output.push(`${edits[k].op}${edits[k].line}`);
345
- prevIncluded = true;
346
- }
347
- else {
348
- prevIncluded = false;
349
- }
350
- }
351
- return output;
352
- }
353
- /** Count hook-related diff lines in the edit script (lines with changes touching hooks). */
354
- function countHookChanges(diffLines, localAlwaysApply, chainAlwaysApply) {
355
- const HOOK_KEYWORDS = /hook|settings\.json|alwaysApply/i;
356
- let count = 0;
357
- for (const line of diffLines) {
358
- if ((line.startsWith('+') || line.startsWith('-')) && HOOK_KEYWORDS.test(line)) {
359
- count++;
360
- }
361
- }
362
- // If alwaysApply changed between local and chain, count that as at least 1
363
- if (count === 0 && localAlwaysApply !== chainAlwaysApply) {
364
- count = 1;
365
- }
366
- return count;
367
- }
368
- // ---------------------------------------------------------------------------
369
- // pb method ls
370
- // ---------------------------------------------------------------------------
371
- export async function runMethodList() {
372
- await runCliCommand({
373
- fn: async () => {
374
- const root = projectRoot();
375
- const sources = loadAllSources(root);
376
- if (sources.length === 0) {
377
- return { methods: [], total: 0 };
378
- }
379
- // Sort: rules first (alpha), then skills (alpha)
380
- sources.sort((a, b) => {
381
- if (a.type !== b.type)
382
- return a.type === 'rule' ? -1 : 1;
383
- return a.name.localeCompare(b.name);
384
- });
385
- // Batch Chain lookups in parallel (best-effort)
386
- const chainResults = await Promise.allSettled(sources.map((s) => fetchChainMethod(s.name)));
387
- const methods = sources.map((source, i) => {
388
- const chainEntry = chainResults[i].status === 'fulfilled' ? chainResults[i].value : null;
389
- const adapters = checkAdapters(root, source);
390
- return {
391
- name: source.name,
392
- type: source.type,
393
- version: chainEntry?.version != null ? String(chainEntry.version) : '-',
394
- published: chainEntry !== null,
395
- cursor: adapters.cursor === 'ok',
396
- codex: adapters.codex === 'ok',
397
- };
398
- });
399
- return { methods, total: methods.length };
400
- },
401
- formatPretty: (result) => formatMethodList(result),
402
- });
403
- }
404
- // ---------------------------------------------------------------------------
405
- // pb method show <name>
406
- // ---------------------------------------------------------------------------
407
- export async function runMethodShow(options) {
408
- await runCliCommand({
409
- fn: async () => {
410
- const root = projectRoot();
411
- const sources = loadAllSources(root);
412
- const source = sources.find((s) => s.name === options.name);
413
- if (!source) {
414
- throw new CLIError(`Method '${options.name}' not found in .productbrain/skills/ or .productbrain/rules/.`, {
415
- code: ErrorCode.VALIDATION_FAILED,
416
- category: 'validation',
417
- guidance: `Run 'pb method ls' to see available methods.`,
418
- });
419
- }
420
- const [chainEntry, adapters] = await Promise.all([
421
- fetchChainMethod(source.name),
422
- Promise.resolve(checkAdapters(root, source)),
423
- ]);
424
- return {
425
- name: source.name,
426
- type: source.type,
427
- frontmatter: source.frontmatter,
428
- chain: chainEntry,
429
- adapters,
430
- };
431
- },
432
- formatPretty: (result) => formatMethodDetail(result),
433
- });
434
- }
435
- // ---------------------------------------------------------------------------
436
- // pb method diff <name>
437
- // ---------------------------------------------------------------------------
438
- export async function runMethodDiff(options) {
439
- await runCliCommand({
440
- fn: async () => {
441
- const root = projectRoot();
442
- const sources = loadAllSources(root);
443
- const source = sources.find((s) => s.name === options.name);
444
- if (!source) {
445
- throw new CLIError(`Method '${options.name}' not found in .productbrain/skills/ or .productbrain/rules/.`, {
446
- code: ErrorCode.VALIDATION_FAILED,
447
- category: 'validation',
448
- guidance: `Run 'pb method ls' to see available methods.`,
449
- });
450
- }
451
- const chainEntry = await fetchChainMethod(source.name);
452
- if (!chainEntry || chainEntry.body === undefined) {
453
- return {
454
- name: source.name,
455
- published: false,
456
- };
457
- }
458
- const localBody = source.body.trim();
459
- const chainBody = (chainEntry.body ?? '').trim();
460
- const diffLines = computeUnifiedDiff(chainBody, localBody);
461
- // Detect hook-related changes — read alwaysApply from stored frontmatter JSON
462
- const chainAlwaysApply = chainEntry.frontmatter?.alwaysApply;
463
- const hookChanges = countHookChanges(diffLines, source.frontmatter.alwaysApply, chainAlwaysApply);
464
- const hasFanOut = hookChanges > 0;
465
- return {
466
- name: source.name,
467
- published: true,
468
- diffLines,
469
- hookChanges,
470
- hasFanOut,
471
- };
472
- },
473
- formatPretty: (result) => formatMethodDiff(result),
474
- });
475
- }
476
- // ---------------------------------------------------------------------------
477
- // pb method publish <name>
478
- // ---------------------------------------------------------------------------
479
- export async function runMethodPublish(options) {
480
- await runCliCommand({
481
- fn: async () => {
482
- // 1. Require active session — publish is a write operation (DEC-9, BR-8)
483
- const session = readSession();
484
- if (!session) {
485
- throw new CLIError('No active session.', {
486
- code: ErrorCode.SESSION_REQUIRED,
487
- category: 'session',
488
- // DEC-9
489
- guidance: 'Run `pb session start` first. Write access requires a tracked session.',
490
- });
491
- }
492
- // 2. Find local source file
493
- const root = projectRoot();
494
- const sources = loadAllSources(root);
495
- const source = sources.find((s) => s.name === options.name);
496
- if (!source) {
497
- throw new CLIError(`Method '${options.name}' not found in .productbrain/skills/ or .productbrain/rules/.`, {
498
- code: ErrorCode.VALIDATION_FAILED,
499
- category: 'validation',
500
- guidance: `Run 'pb method ls' to see available methods.`,
501
- });
502
- }
503
- // 3. Compute SHA-256 hash of the local body (everything after frontmatter)
504
- const localBody = source.body.trim();
505
- const manifestHash = createHash('sha256').update(localBody, 'utf8').digest('hex');
506
- // 4. Query Chain for existing published entry
507
- const existing = await fetchChainMethod(source.name);
508
- // 5. D4 check — refuse on no-op (unless --force)
509
- if (!options.force && existing && existing.manifestHash === manifestHash) {
510
- const existingVersion = typeof existing.version === 'string'
511
- ? parseInt(existing.version, 10)
512
- : existing.version ?? 1;
513
- throw new CLIError(`No changes to publish. Local body matches last-published version (v${existingVersion}). Use --force to override.`, {
514
- code: ErrorCode.VALIDATION_FAILED,
515
- category: 'validation',
516
- guidance: `Run 'pb method diff ${options.name}' to inspect differences.`,
517
- });
518
- }
519
- // 6. Compute the next method version number
520
- const existingVersion = existing
521
- ? (typeof existing.version === 'string'
522
- ? parseInt(existing.version, 10)
523
- : existing.version ?? 0)
524
- : 0;
525
- const newVersion = existingVersion + 1;
526
- // 7. Serialize frontmatter for storage
527
- const fm = source.frontmatter;
528
- const frontmatterJson = JSON.stringify({
529
- alwaysApply: fm.alwaysApply,
530
- globs: fm.globs,
531
- description: fm.description,
532
- });
533
- const publishedAt = new Date().toISOString();
534
- const publishedBy = session.sessionId;
535
- const entryData = {
536
- name: source.name,
537
- version: String(newVersion),
538
- sourceType: source.type,
539
- body: localBody,
540
- frontmatter: frontmatterJson,
541
- publishedAt,
542
- publishedBy,
543
- manifestHash,
544
- };
545
- // 8. Write to Chain — create or update
546
- let created;
547
- if (!existing || !existing._id) {
548
- // No existing entry — createEntry
549
- await kernelCallWithSession('chain.createEntry', {
550
- collectionSlug: 'methods',
551
- name: source.name,
552
- // `pb method publish` is an explicit publish action — bypasses governance-mode
553
- // resolution and commits directly (DEC-896 intentional, mirrors UI publish at
554
- // src/routes/api/brain/capture/+server.ts).
555
- status: 'active',
556
- data: entryData,
557
- sessionId: session.sessionId,
558
- createdBy: `agent:${session.sessionId}`,
559
- });
560
- created = true;
561
- }
562
- else {
563
- // Existing entry — updateEntry (LWW: last-write-wins)
564
- const updateResult = await kernelCallWithSession('chain.updateEntry', {
565
- entryId: existing._id,
566
- data: entryData,
567
- changedBy: `agent:${session.sessionId}`,
568
- changeNote: `Publish v${newVersion} via pb method publish`,
569
- });
570
- // WP-465 slice ⑤ (Option B): a coherency refusal RETURNS a structured payload — the
571
- // entry is unchanged, so report a refusal instead of a successful publish.
572
- if (updateResult?.outcome === 'refused') {
573
- throw new CLIError(updateResult.refusal?.reason ?? `Publish of '${source.name}' was refused by the coherency gate.`, {
574
- code: ErrorCode.VALIDATION_FAILED,
575
- category: 'validation',
576
- guidance: 'Link the entry to the strategic spine and retry, or resolve the coherency challenge before publishing.',
577
- });
578
- }
579
- created = false;
580
- }
581
- return {
582
- name: source.name,
583
- version: newVersion,
584
- manifestHash,
585
- created,
586
- previousVersion: newVersion > 1 ? existingVersion : undefined,
587
- };
588
- },
589
- formatPretty: (result) => formatMethodPublish(result),
590
- });
591
- }
592
- // ---------------------------------------------------------------------------
593
- // Formatters
594
- // ---------------------------------------------------------------------------
595
- function statusIcon(published) {
596
- return published ? 'yes' : 'no ';
597
- }
598
- function adapterIcon(ok) {
599
- return ok ? '\u2713' : '-';
600
- }
601
- function adapterStatusLabel(status) {
602
- if (status === 'ok')
603
- return 'ok';
604
- if (status === 'forked')
605
- return 'forked';
606
- return 'missing';
607
- }
608
- function formatMethodList(result) {
609
- if (result.total === 0) {
610
- return 'No methods found. Expected .productbrain/skills/ and .productbrain/rules/ directories.';
611
- }
612
- const lines = [];
613
- lines.push(`Methods (${result.total}):`);
614
- lines.push('');
615
- const maxName = Math.max(...result.methods.map((m) => m.name.length), 4);
616
- const maxVersion = Math.max(...result.methods.map((m) => m.version.length), 7);
617
- const header = [
618
- 'Name'.padEnd(maxName),
619
- 'Type '.padEnd(6),
620
- 'Version'.padEnd(maxVersion),
621
- 'Published',
622
- 'Cursor',
623
- 'Codex',
624
- ].join(' ');
625
- lines.push(` ${header}`);
626
- lines.push(` ${'-'.repeat(header.length)}`);
627
- // Group by type
628
- const rules = result.methods.filter((m) => m.type === 'rule');
629
- const skills = result.methods.filter((m) => m.type === 'skill');
630
- const renderRow = (m) => {
631
- return ' ' + [
632
- m.name.padEnd(maxName),
633
- m.type.padEnd(6),
634
- m.version.padEnd(maxVersion),
635
- statusIcon(m.published).padEnd(10),
636
- adapterIcon(m.cursor).padEnd(7),
637
- adapterIcon(m.codex),
638
- ].join(' ');
639
- };
640
- if (rules.length > 0) {
641
- lines.push('');
642
- lines.push(' Rules:');
643
- for (const m of rules) {
644
- lines.push(renderRow(m));
645
- }
646
- }
647
- if (skills.length > 0) {
648
- lines.push('');
649
- lines.push(' Skills:');
650
- for (const m of skills) {
651
- lines.push(renderRow(m));
652
- }
653
- }
654
- lines.push('');
655
- lines.push(` ${rules.length} rules, ${skills.length} skills`);
656
- lines.push(' Run `pb method show <name>` for fork status and Chain details.');
657
- return lines.join('\n');
658
- }
659
- function formatMethodDetail(result) {
660
- const lines = [];
661
- const { name, type, frontmatter, chain, adapters } = result;
662
- lines.push('');
663
- lines.push(`Method: ${name}`);
664
- lines.push(`Type: ${type}`);
665
- if (frontmatter.description) {
666
- lines.push(`Desc: ${frontmatter.description}`);
667
- }
668
- if (frontmatter.alwaysApply !== undefined) {
669
- lines.push(`Always: ${frontmatter.alwaysApply}`);
670
- }
671
- if (frontmatter.globs) {
672
- lines.push(`Globs: ${frontmatter.globs}`);
673
- }
674
- if (frontmatter.level) {
675
- lines.push(`Level: ${frontmatter.level}`);
676
- }
677
- lines.push('');
678
- if (chain) {
679
- lines.push('Published:');
680
- if (chain.version)
681
- lines.push(` Version: ${chain.version}`);
682
- if (chain.publishedAt)
683
- lines.push(` Published: ${chain.publishedAt}`);
684
- if (chain.publishedBy)
685
- lines.push(` By: ${chain.publishedBy}`);
686
- if (chain.manifestHash)
687
- lines.push(` Hash: ${chain.manifestHash}`);
688
- }
689
- else {
690
- lines.push('Published: no (not in Chain methods collection)');
691
- }
692
- lines.push('');
693
- lines.push('Adapter status:');
694
- lines.push(` cursor: ${adapterStatusLabel(adapters.cursor)}`);
695
- lines.push(` codex: ${adapterStatusLabel(adapters.codex)}`);
696
- lines.push(` copilot: ${adapterStatusLabel(adapters.copilot)}`);
697
- const forked = Object.entries(adapters).filter(([, v]) => v === 'forked').map(([k]) => k);
698
- const missing = Object.entries(adapters).filter(([, v]) => v === 'missing').map(([k]) => k);
699
- if (forked.length > 0 || missing.length > 0) {
700
- lines.push('');
701
- if (forked.length > 0) {
702
- lines.push(` ! Forked adapters (out of sync): ${forked.join(', ')}`);
703
- lines.push(' Run `pb handshake` to regenerate adapter files.');
704
- }
705
- if (missing.length > 0) {
706
- lines.push(` - Missing adapters: ${missing.join(', ')}`);
707
- lines.push(' Run `pb handshake` to generate adapter files.');
708
- }
709
- }
710
- else {
711
- lines.push('');
712
- lines.push(' All adapters in sync.');
713
- }
714
- return lines.join('\n');
715
- }
716
- function formatMethodPublish(result) {
717
- const lines = [];
718
- lines.push('');
719
- lines.push(`Published: ${result.name} v${result.version}`);
720
- lines.push(`Action: ${result.created ? 'created' : 'updated'}`);
721
- lines.push(`Hash: ${result.manifestHash.slice(0, 16)}...`);
722
- if (result.previousVersion !== undefined) {
723
- lines.push('');
724
- lines.push(`rollback with: pb method publish ${result.name} --version ${result.previousVersion}`);
725
- }
726
- lines.push('');
727
- return lines.join('\n');
728
- }
729
- function formatMethodDiff(result) {
730
- const lines = [];
731
- if (!result.published) {
732
- lines.push('');
733
- lines.push(`Not published yet. Run \`pb method publish ${result.name}\` to publish.`);
734
- lines.push('');
735
- return lines.join('\n');
736
- }
737
- const diffLines = result.diffLines ?? [];
738
- if (diffLines.length === 0) {
739
- lines.push('');
740
- lines.push(`Method: ${result.name}`);
741
- lines.push('');
742
- lines.push(' No changes. Local matches published version.');
743
- lines.push('');
744
- return lines.join('\n');
745
- }
746
- lines.push('');
747
- lines.push(`Method: ${result.name}`);
748
- lines.push('--- chain (published)');
749
- lines.push('+++ local');
750
- lines.push('');
751
- for (const dl of diffLines) {
752
- if (dl === '...') {
753
- lines.push(' ...');
754
- }
755
- else if (dl.startsWith('+')) {
756
- lines.push(`+ ${dl.slice(1)}`);
757
- }
758
- else if (dl.startsWith('-')) {
759
- lines.push(`- ${dl.slice(1)}`);
760
- }
761
- else {
762
- lines.push(` ${dl.slice(1)}`);
763
- }
764
- }
765
- if (result.hasFanOut) {
766
- const n = result.hookChanges ?? 0;
767
- lines.push('');
768
- lines.push(`hooks: +${n} (claude-native, +fan-out to cursor,copilot via .claude/settings.json per INS-740)`);
769
- }
770
- lines.push('');
771
- return lines.join('\n');
772
- }
773
- //# sourceMappingURL=method.js.map