tenbo-dashboard 0.10.1 → 0.10.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.
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import path from 'node:path';
7
+ import { topLevelItemHelpLines } from '../scripts/itemCommandRegistry.mjs';
7
8
 
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
  const root = path.resolve(__dirname, '..');
@@ -85,12 +86,7 @@ Usage:
85
86
  tenbo-dashboard Launch the dashboard (http://localhost:5174)
86
87
  tenbo-dashboard serve Same as bare invocation (also: 'start', 'dev')
87
88
  tenbo-dashboard sync Refresh tenbo state after a change (metrics + init-check + validate, surfaces new findings)
88
- tenbo-dashboard item show <id> [--json]
89
- tenbo-dashboard item set-status <id> <now|next|later|done|dropped> [--json]
90
- tenbo-dashboard item add-note <id> "<note>" [--json]
91
- tenbo-dashboard item verify <id> --status <not_required|pending_live|verified|failed>
92
- [--evidence "<text>"] [--note "<text>"] [--json]
93
- tenbo-dashboard item link-commit <id> <sha> [--json]
89
+ ${topLevelItemHelpLines().join('\n')}
94
90
  tenbo-dashboard items Query roadmap items [--status <status>]
95
91
  [--verification <status>] [--goal <goal>]
96
92
  [--type <type>] [--priority <p0|p1|p2|p3>]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tenbo-dashboard",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Local-first architecture dashboard and CLI tools for .tenbo/ repos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,19 @@ beforeEach(() => {
30
30
  afterEach(() => rmSync(dir, { recursive: true, force: true }));
31
31
 
32
32
  describe('bin output', () => {
33
+ it('lists item completion in top-level help', () => {
34
+ const bin = path.resolve('bin/tenbo-dashboard.mjs');
35
+ const result = spawnSync(process.execPath, [bin, 'help'], {
36
+ cwd: dir,
37
+ encoding: 'utf8',
38
+ maxBuffer: 1024 * 1024,
39
+ });
40
+
41
+ expect(result.status).toBe(0);
42
+ expect(result.stderr).toBe('');
43
+ expect(result.stdout).toContain('tenbo-dashboard item complete <id>');
44
+ });
45
+
33
46
  it('flushes large JSON output before exiting', () => {
34
47
  const bin = path.resolve('bin/tenbo-dashboard.mjs');
35
48
  const result = spawnSync(process.execPath, [bin, 'items', '--status', 'done', '--json'], {
@@ -71,4 +84,70 @@ describe('bin output', () => {
71
84
  expect(result.stdout).toContain('--fields <a,b>');
72
85
  expect(result.stdout).not.toContain('ed-001');
73
86
  });
87
+
88
+ it('prints item help without requiring an item id', () => {
89
+ const bin = path.resolve('bin/tenbo-dashboard.mjs');
90
+ const result = spawnSync(process.execPath, [bin, 'item', '--help'], {
91
+ cwd: dir,
92
+ encoding: 'utf8',
93
+ maxBuffer: 1024 * 1024,
94
+ });
95
+
96
+ expect(result.status).toBe(0);
97
+ expect(result.stderr).toBe('');
98
+ expect(result.stdout).toContain('Usage: tenbo-dashboard item <command> <id>');
99
+ expect(result.stdout).toContain('complete');
100
+ expect(result.stdout).toContain('link-commit');
101
+ expect(result.stdout).not.toContain('ed-001');
102
+ });
103
+
104
+ it('prints item complete help without requiring an item id', () => {
105
+ const bin = path.resolve('bin/tenbo-dashboard.mjs');
106
+ const result = spawnSync(process.execPath, [bin, 'item', 'complete', '--help'], {
107
+ cwd: dir,
108
+ encoding: 'utf8',
109
+ maxBuffer: 1024 * 1024,
110
+ });
111
+
112
+ expect(result.status).toBe(0);
113
+ expect(result.stderr).toBe('');
114
+ expect(result.stdout).toContain('Usage: tenbo-dashboard item complete <id>');
115
+ expect(result.stdout).toContain('--doc-update today|YYYY-MM-DD');
116
+ expect(result.stdout).toContain('--commit <sha>');
117
+ });
118
+
119
+ it('completes status, doc_update, verification, and commit through the bin wrapper', () => {
120
+ const bin = path.resolve('bin/tenbo-dashboard.mjs');
121
+ const result = spawnSync(process.execPath, [
122
+ bin,
123
+ 'item',
124
+ 'complete',
125
+ 'ed-001',
126
+ '--evidence',
127
+ 'npm test -- --run',
128
+ '--doc-update',
129
+ 'today',
130
+ '--commit',
131
+ 'abc123',
132
+ '--json',
133
+ ], {
134
+ cwd: dir,
135
+ encoding: 'utf8',
136
+ maxBuffer: 1024 * 1024,
137
+ });
138
+ const today = new Date().toISOString().slice(0, 10);
139
+
140
+ expect(result.status).toBe(0);
141
+ expect(result.stderr).toBe('');
142
+ const payload = JSON.parse(result.stdout);
143
+ expect(payload.item).toMatchObject({
144
+ status: 'done',
145
+ doc_update: today,
146
+ links: ['commit:abc123'],
147
+ verification: {
148
+ status: 'verified',
149
+ evidence: ['npm test -- --run'],
150
+ },
151
+ });
152
+ });
74
153
  });
package/scripts/item.ts CHANGED
@@ -10,12 +10,28 @@ import {
10
10
  import { summarizeItem } from '../src/api/lib/itemProjection';
11
11
  import { checkItemEvidence } from '../src/api/lib/itemEvidence';
12
12
  import { hasFlag, positionalArgs, readOption, readRepeated } from './cliArgs';
13
- import { handleCliError, isMain, misuse, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
13
+ import { handleCliError, isMain, misuse, ok, repoRootFromCwd, runMain, serialize, type CliResult } from './cliResult';
14
+ import { findItemCommand, formatItemCommandHelp, formatItemHelp } from './itemCommandRegistry.mjs';
14
15
 
15
16
  export function runItemCli(repoRoot: string, args: string[]): CliResult {
16
17
  const json = hasFlag(args, '--json');
17
18
  const [command, itemId, value] = positionalArgs(args);
18
- if (!command || !itemId) return misuse('Usage: tenbo item <show|set-status|add-note|verify|link-commit> <item-id>', json);
19
+
20
+ if (hasFlag(args, '--help') || command === 'help') {
21
+ const helpCommand = command === 'help' ? itemId : command;
22
+ if (helpCommand) {
23
+ const help = formatItemCommandHelp(helpCommand);
24
+ if (help) return ok(help);
25
+ return misuse(`unknown item command: ${helpCommand}`, json);
26
+ }
27
+ return ok(formatItemHelp());
28
+ }
29
+
30
+ if (!command) return misuse(formatItemHelp().trimEnd(), json);
31
+ if (!itemId) {
32
+ const definition = findItemCommand(command);
33
+ return misuse(definition ? `Usage: ${definition.usage}` : `unknown item command: ${command}`, json);
34
+ }
19
35
 
20
36
  try {
21
37
  if (command === 'show') {
@@ -76,13 +92,13 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
76
92
  }
77
93
 
78
94
  if (command === 'set-status') {
79
- if (!value) return misuse('Usage: tenbo item set-status <item-id> <status>', json);
95
+ if (!value) return misuse(`Usage: ${findItemCommand('set-status')?.usage ?? 'tenbo-dashboard item set-status <id> <status>'}`, json);
80
96
  const result = setItemStatus(repoRoot, itemId, value);
81
97
  return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} status: ${result.item.status}\n`);
82
98
  }
83
99
 
84
100
  if (command === 'add-note') {
85
- if (!value) return misuse('Usage: tenbo item add-note <item-id> <note>', json);
101
+ if (!value) return misuse(`Usage: ${findItemCommand('add-note')?.usage ?? 'tenbo-dashboard item add-note <id> <note>'}`, json);
86
102
  const result = addItemNote(repoRoot, itemId, value);
87
103
  return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} note added\n`);
88
104
  }
@@ -93,7 +109,7 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
93
109
  return serialize(report, json, `${itemId} evidence: ${report.verdict}\n`);
94
110
  }
95
111
  const status = readOption(args, '--status');
96
- if (!status) return misuse('Usage: tenbo item verify <item-id> --status <status> [--evidence <text>] [--note <text>]', json);
112
+ if (!status) return misuse(`Usage: ${findItemCommand('verify')?.usage ?? 'tenbo-dashboard item verify <id> --status <status>'}`, json);
97
113
  const result = setItemVerification(repoRoot, itemId, {
98
114
  status,
99
115
  evidence: readRepeated(args, '--evidence'),
@@ -106,8 +122,8 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
106
122
  const date = readOption(args, '--date');
107
123
  const skipped = hasFlag(args, '--skipped');
108
124
  const reason = readOption(args, '--reason');
109
- if (!date && !skipped) return misuse('Usage: tenbo item doc-update <item-id> --date <today|YYYY-MM-DD> OR --skipped --reason "<text>"', json);
110
- if (skipped && !reason) return misuse('Usage: tenbo item doc-update <item-id> --skipped --reason "<text>"', json);
125
+ if (!date && !skipped) return misuse(`Usage: ${findItemCommand('doc-update')?.usage ?? 'tenbo-dashboard item doc-update <id> --date <today|YYYY-MM-DD>'}`, json);
126
+ if (skipped && !reason) return misuse('Usage: tenbo-dashboard item doc-update <id> --skipped --reason "<text>"', json);
111
127
  const today = new Date().toISOString().slice(0, 10);
112
128
  const docUpdate = skipped ? `skipped — ${reason}` : date === 'today' ? today : date;
113
129
  const result = setItemDocUpdate(repoRoot, itemId, docUpdate ?? today);
@@ -119,7 +135,7 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
119
135
  const docUpdateArg = readOption(args, '--doc-update');
120
136
  const commit = readOption(args, '--commit');
121
137
  const verificationStatus = readOption(args, '--verification');
122
- if (evidence.length === 0) return misuse('Usage: tenbo item complete <item-id> --evidence "<text>" [--doc-update today|YYYY-MM-DD] [--commit <sha>] [--verification <status>]', json);
138
+ if (evidence.length === 0) return misuse(`Usage: ${findItemCommand('complete')?.usage ?? 'tenbo-dashboard item complete <id> --evidence "<text>"'}`, json);
123
139
  const today = new Date().toISOString().slice(0, 10);
124
140
  const docUpdate = docUpdateArg === 'today' ? today : docUpdateArg;
125
141
  const result = completeItem(repoRoot, itemId, {
@@ -132,7 +148,7 @@ export function runItemCli(repoRoot: string, args: string[]): CliResult {
132
148
  }
133
149
 
134
150
  if (command === 'link-commit') {
135
- if (!value) return misuse('Usage: tenbo item link-commit <item-id> <sha>', json);
151
+ if (!value) return misuse(`Usage: ${findItemCommand('link-commit')?.usage ?? 'tenbo-dashboard item link-commit <id> <sha>'}`, json);
136
152
  const result = linkItemCommit(repoRoot, itemId, value);
137
153
  return serialize({ ok: true, scope: result.scopeId, item: result.item, validation: result.validation }, json, `${itemId} linked commit: ${value}\n`);
138
154
  }
@@ -0,0 +1,12 @@
1
+ export interface ItemCommandDefinition {
2
+ name: string;
3
+ usage: string;
4
+ summary: string;
5
+ details?: string[];
6
+ }
7
+
8
+ export const ITEM_COMMANDS: ItemCommandDefinition[];
9
+ export function findItemCommand(name: string): ItemCommandDefinition | undefined;
10
+ export function formatItemHelp(): string;
11
+ export function formatItemCommandHelp(name: string): string | undefined;
12
+ export function topLevelItemHelpLines(): string[];
@@ -0,0 +1,98 @@
1
+ export const ITEM_COMMANDS = [
2
+ {
3
+ name: 'show',
4
+ usage: 'tenbo-dashboard item show <id> [--json]',
5
+ summary: 'Show the full roadmap item.',
6
+ },
7
+ {
8
+ name: 'brief',
9
+ usage: 'tenbo-dashboard item brief <id> [--json]',
10
+ summary: 'Print compact execution context for one item.',
11
+ },
12
+ {
13
+ name: 'handoff',
14
+ usage: 'tenbo-dashboard item handoff <id> [--json]',
15
+ summary: 'Generate a copy/paste handoff prompt.',
16
+ },
17
+ {
18
+ name: 'set-status',
19
+ usage: 'tenbo-dashboard item set-status <id> <now|next|later|done|dropped> [--json]',
20
+ summary: 'Set the roadmap status.',
21
+ },
22
+ {
23
+ name: 'add-note',
24
+ usage: 'tenbo-dashboard item add-note <id> "<note>" [--json]',
25
+ summary: 'Append a dated item note.',
26
+ },
27
+ {
28
+ name: 'verify',
29
+ usage: 'tenbo-dashboard item verify <id> --check [--json]',
30
+ summary: 'Check whether recorded evidence matches completion state.',
31
+ details: [
32
+ 'tenbo-dashboard item verify <id> --status <not_required|pending_live|verified|failed>',
33
+ ' [--evidence "<text>"] [--note "<text>"] [--json]',
34
+ ],
35
+ },
36
+ {
37
+ name: 'doc-update',
38
+ usage: 'tenbo-dashboard item doc-update <id> --date <today|YYYY-MM-DD> [--json]',
39
+ summary: 'Record documentation freshness for an item.',
40
+ details: [
41
+ 'tenbo-dashboard item doc-update <id> --skipped --reason "<text>" [--json]',
42
+ ],
43
+ },
44
+ {
45
+ name: 'complete',
46
+ usage: 'tenbo-dashboard item complete <id> --evidence "<text>" [--doc-update today|YYYY-MM-DD] [--commit <sha>] [--verification <status>] [--json]',
47
+ summary: 'Complete status, verification, docs, and optional commit link in one transaction.',
48
+ },
49
+ {
50
+ name: 'link-commit',
51
+ usage: 'tenbo-dashboard item link-commit <id> <sha> [--json]',
52
+ summary: 'Attach a commit link to an item.',
53
+ },
54
+ ];
55
+
56
+ const commandByName = new Map(ITEM_COMMANDS.map((command) => [command.name, command]));
57
+
58
+ export function findItemCommand(name) {
59
+ return commandByName.get(name);
60
+ }
61
+
62
+ export function formatItemHelp() {
63
+ const maxNameLength = Math.max(...ITEM_COMMANDS.map((command) => command.name.length));
64
+ const commandLines = ITEM_COMMANDS.flatMap((command) => [
65
+ ` ${command.name.padEnd(maxNameLength)} ${command.summary}`,
66
+ ` ${' '.repeat(maxNameLength)} ${command.usage}`,
67
+ ]);
68
+
69
+ return [
70
+ 'Usage: tenbo-dashboard item <command> <id> [options]',
71
+ '',
72
+ 'Commands:',
73
+ ...commandLines,
74
+ '',
75
+ 'Run tenbo-dashboard item <command> --help for command-specific usage.',
76
+ '',
77
+ ].join('\n');
78
+ }
79
+
80
+ export function formatItemCommandHelp(name) {
81
+ const command = findItemCommand(name);
82
+ if (!command) return undefined;
83
+ return [
84
+ `Usage: ${command.usage}`,
85
+ '',
86
+ command.summary,
87
+ ...(command.details?.length ? ['', 'Additional forms:', ...command.details.map((line) => ` ${line}`)] : []),
88
+ '',
89
+ ].join('\n');
90
+ }
91
+
92
+ export function topLevelItemHelpLines() {
93
+ return ITEM_COMMANDS.flatMap((command) => {
94
+ const lines = [` ${command.usage}`];
95
+ if (command.details?.length) lines.push(...command.details.map((line) => ` ${line}`));
96
+ return lines;
97
+ });
98
+ }