@nemus-cli/nemus 0.2.11 → 0.2.12
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/CHANGELOG.md +13 -0
- package/README.md +10 -5
- package/dist/commands/analyze-deps.js +42 -8
- package/dist/commands/sessions.js +33 -6
- package/dist/commands/suite/index.js +3 -2
- package/dist/commands/suite/list.js +26 -7
- package/package.json +1 -1
- package/src/commands/analyze-deps.ts +38 -8
- package/src/commands/sessions.ts +32 -5
- package/src/commands/suite/index.ts +3 -2
- package/src/commands/suite/list.test.ts +62 -0
- package/src/commands/suite/list.ts +26 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.12] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--json` for more read-only reporting commands**, extending 0.2.11's set
|
|
15
|
+
(`list`/`status`/`doctor`) to `suite list`, `sessions`, and `analyze-deps`.
|
|
16
|
+
Each emits exactly one JSON document to stdout (no tables, no interactive
|
|
17
|
+
prompt): `suite list --json` (saved suites + entries), `sessions --json`
|
|
18
|
+
(workspace sessions, list-only — no resume), `analyze-deps --json` (per-repo
|
|
19
|
+
dependencies/dependents/missing + circular deps + suggested missing repos;
|
|
20
|
+
requires an explicit workspace). `--json` errors are parseable
|
|
21
|
+
`{ ok:false, error }` on stdout + exit 1, consistent with 0.2.11.
|
|
22
|
+
|
|
10
23
|
## [0.2.11] - 2026-08-27
|
|
11
24
|
|
|
12
25
|
### Added
|
package/README.md
CHANGED
|
@@ -225,18 +225,23 @@ shared-lib main ✓ Clean ↓3 -
|
|
|
225
225
|
|
|
226
226
|
### Scripting: `--json`
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
output
|
|
230
|
-
|
|
228
|
+
The read-only reporting commands accept `--json` for stable, machine-readable
|
|
229
|
+
output: `list`, `status`, `doctor`, `suite list`, `sessions`, and
|
|
230
|
+
`analyze-deps`. Diagnostics go to stderr, so stdout is a single JSON document
|
|
231
|
+
you can pipe straight into `jq` or a CI step:
|
|
231
232
|
|
|
232
233
|
```bash
|
|
233
234
|
nemus list --json | jq -r '.workspaces[].name'
|
|
234
235
|
nemus status my-workspace --json | jq '.clean'
|
|
235
236
|
nemus doctor my-workspace --json | jq '.score'
|
|
237
|
+
nemus suite list --json | jq -r '.suites[].name'
|
|
238
|
+
nemus sessions --json | jq -r '.sessions[].workspaceName'
|
|
239
|
+
nemus analyze-deps my-workspace --json | jq '.circularDependencies'
|
|
236
240
|
```
|
|
237
241
|
|
|
238
|
-
`status`/`doctor`
|
|
239
|
-
prompt).
|
|
242
|
+
The workspace-scoped ones (`status`/`doctor`/`analyze-deps`) need an explicit
|
|
243
|
+
workspace name with `--json` (they never prompt). On failure, `--json` prints a
|
|
244
|
+
parseable `{ "ok": false, "error": … }` to stdout and exits non-zero.
|
|
240
245
|
|
|
241
246
|
### Suites (reusable repo collections)
|
|
242
247
|
|
|
@@ -42,6 +42,7 @@ const config_1 = require("../utils/config");
|
|
|
42
42
|
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
43
43
|
const dependency_analyzer_1 = require("../utils/dependency-analyzer");
|
|
44
44
|
const logger_1 = require("../utils/logger");
|
|
45
|
+
const output_1 = require("../utils/output");
|
|
45
46
|
const colors_1 = require("../utils/colors");
|
|
46
47
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
47
48
|
const command_helpers_1 = require("../utils/command-helpers");
|
|
@@ -71,22 +72,51 @@ function registerAnalyzeDepsCommand(parent) {
|
|
|
71
72
|
.alias('ad')
|
|
72
73
|
.description('Analyze inter-repo dependencies')
|
|
73
74
|
.argument('[workspace]', 'Workspace name')
|
|
74
|
-
.
|
|
75
|
-
|
|
75
|
+
.option('--json', 'Output as JSON (no interactive save)')
|
|
76
|
+
.action(async (workspace, opts) => {
|
|
77
|
+
await handleAnalyzeDeps(workspace, opts);
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
|
-
async function handleAnalyzeDeps(workspaceArg) {
|
|
80
|
+
async function handleAnalyzeDeps(workspaceArg, opts = {}) {
|
|
79
81
|
try {
|
|
82
|
+
// JSON mode is non-interactive: require an explicit workspace rather than prompt.
|
|
83
|
+
if (opts.json && !workspaceArg) {
|
|
84
|
+
(0, output_1.outputJsonError)('analyze-deps --json requires a workspace name');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
80
87
|
const selectedWorkspace = await (0, command_helpers_1.resolveWorkspace)(workspaceArg);
|
|
81
88
|
const workspacePath = path.join(config_1.WORKSPACES_DIR, selectedWorkspace);
|
|
82
89
|
const metadata = await (0, workspace_meta_1.loadMetadata)(workspacePath);
|
|
83
90
|
if (!metadata) {
|
|
84
|
-
(
|
|
91
|
+
if (opts.json)
|
|
92
|
+
(0, output_1.outputJsonError)(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
93
|
+
else
|
|
94
|
+
(0, logger_1.logError)(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
85
95
|
process.exit(1);
|
|
86
96
|
}
|
|
97
|
+
const repoNames = metadata.repositories.map(r => r.name);
|
|
98
|
+
if (opts.json) {
|
|
99
|
+
const analyses = await (0, dependency_analyzer_1.analyzeDependencies)(workspacePath, repoNames);
|
|
100
|
+
const cycles = (0, dependency_analyzer_1.detectCircularDependencies)(analyses);
|
|
101
|
+
const missing = new Set();
|
|
102
|
+
for (const [, a] of analyses)
|
|
103
|
+
for (const dep of a.missingDependencies)
|
|
104
|
+
missing.add(dep);
|
|
105
|
+
(0, output_1.outputJson)({
|
|
106
|
+
workspace: selectedWorkspace,
|
|
107
|
+
repositories: Array.from(analyses.entries()).map(([name, a]) => ({
|
|
108
|
+
name,
|
|
109
|
+
dependencies: a.dependencies,
|
|
110
|
+
dependents: a.dependents,
|
|
111
|
+
missingDependencies: a.missingDependencies,
|
|
112
|
+
})),
|
|
113
|
+
circularDependencies: cycles,
|
|
114
|
+
missingRepositories: Array.from(missing),
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
87
118
|
(0, logger_1.logStep)(`Analyzing dependencies for workspace: ${(0, colors_1.colorize)(selectedWorkspace, 'cyan')}`);
|
|
88
119
|
(0, logger_1.logInfo)('Scanning package.json, Dockerfile, and docker-compose.yml files...');
|
|
89
|
-
const repoNames = metadata.repositories.map(r => r.name);
|
|
90
120
|
const analyses = await (0, dependency_analyzer_1.analyzeDependencies)(workspacePath, repoNames);
|
|
91
121
|
displayDependencyAnalysis(analyses);
|
|
92
122
|
const cycles = (0, dependency_analyzer_1.detectCircularDependencies)(analyses);
|
|
@@ -129,9 +159,13 @@ async function handleAnalyzeDeps(workspaceArg) {
|
|
|
129
159
|
}
|
|
130
160
|
}
|
|
131
161
|
catch (error) {
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
|
|
162
|
+
if (opts.json) {
|
|
163
|
+
(0, output_1.outputJsonError)(error instanceof Error ? error.message : 'Failed to analyze dependencies');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
(0, logger_1.logError)('Failed to analyze dependencies');
|
|
167
|
+
if (error instanceof Error)
|
|
168
|
+
(0, logger_1.logError)(error.message);
|
|
135
169
|
}
|
|
136
170
|
process.exit(1);
|
|
137
171
|
}
|
|
@@ -43,6 +43,7 @@ const fs = __importStar(require("fs/promises"));
|
|
|
43
43
|
const claude_sessions_1 = require("../utils/claude-sessions");
|
|
44
44
|
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
45
45
|
const logger_1 = require("../utils/logger");
|
|
46
|
+
const output_1 = require("../utils/output");
|
|
46
47
|
const colors_1 = require("../utils/colors");
|
|
47
48
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
48
49
|
const inquirer_autocomplete_prompt_1 = __importDefault(require("inquirer-autocomplete-prompt"));
|
|
@@ -55,14 +56,19 @@ function registerSessionsCommand(parent) {
|
|
|
55
56
|
.command('sessions')
|
|
56
57
|
.alias('ses')
|
|
57
58
|
.description('Resume a Claude session in a workspace')
|
|
58
|
-
.
|
|
59
|
-
|
|
59
|
+
.option('--json', 'List sessions as JSON (no interactive resume)')
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
await handleSessions(opts);
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
|
-
async function handleSessions() {
|
|
64
|
+
async function handleSessions(opts = {}) {
|
|
63
65
|
try {
|
|
64
66
|
const sessions = await (0, claude_sessions_1.getWorkspaceSessions)();
|
|
65
67
|
if (sessions.length === 0) {
|
|
68
|
+
if (opts.json) {
|
|
69
|
+
(0, output_1.outputJson)({ count: 0, sessions: [] });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
66
72
|
(0, logger_1.logInfo)('No workspace sessions found.');
|
|
67
73
|
console.log('\nYou can create a workspace with: nemus create');
|
|
68
74
|
console.log('Or navigate to one with: w go');
|
|
@@ -76,6 +82,22 @@ async function handleSessions() {
|
|
|
76
82
|
const repoLabel = repoCount > 0 ? `${repoCount} repos` : 'no repos';
|
|
77
83
|
return { session, repoLabel };
|
|
78
84
|
});
|
|
85
|
+
// JSON mode: list sessions to stdout, no resume prompt / temp-file writes.
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
(0, output_1.outputJson)({
|
|
88
|
+
count: items.length,
|
|
89
|
+
sessions: items.map(i => ({
|
|
90
|
+
workspaceName: i.session.workspaceName,
|
|
91
|
+
workspacePath: i.session.workspacePath,
|
|
92
|
+
sessionId: i.session.sessionId,
|
|
93
|
+
agentType: i.session.agentType ?? null,
|
|
94
|
+
lastActive: i.session.lastActiveLabel,
|
|
95
|
+
lastActiveAt: i.session.lastActiveAt.toISOString(),
|
|
96
|
+
repoCount: workspaceMap.get(i.session.workspaceName)?.metadata?.repositories?.length ?? 0,
|
|
97
|
+
})),
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
79
101
|
const maxNameLen = Math.max(...items.map(i => i.session.workspaceName.length));
|
|
80
102
|
console.log('');
|
|
81
103
|
console.log((0, colors_1.colorize)(' Workspace Sessions', 'bright') + (0, colors_1.colorize)(' (sorted by last active)', 'dim'));
|
|
@@ -120,9 +142,14 @@ async function handleSessions() {
|
|
|
120
142
|
catch (error) {
|
|
121
143
|
if (error?.name === 'ExitPromptError')
|
|
122
144
|
return;
|
|
123
|
-
(
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
if (opts.json) {
|
|
146
|
+
(0, output_1.outputJsonError)(error instanceof Error ? error.message : 'Failed to list sessions');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
(0, logger_1.logError)('Failed to list sessions');
|
|
150
|
+
if (error instanceof Error) {
|
|
151
|
+
(0, logger_1.logError)(error.message);
|
|
152
|
+
}
|
|
126
153
|
}
|
|
127
154
|
process.exit(1);
|
|
128
155
|
}
|
|
@@ -49,9 +49,10 @@ function registerSuiteCommands(parent) {
|
|
|
49
49
|
suite
|
|
50
50
|
.command('list')
|
|
51
51
|
.description('List all saved suites')
|
|
52
|
-
.
|
|
52
|
+
.option('--json', 'Output as JSON')
|
|
53
|
+
.action(async (opts) => {
|
|
53
54
|
const { main } = await Promise.resolve().then(() => __importStar(require('./list')));
|
|
54
|
-
await main();
|
|
55
|
+
await main(opts);
|
|
55
56
|
});
|
|
56
57
|
suite
|
|
57
58
|
.command('delete')
|
|
@@ -4,13 +4,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.main = main;
|
|
5
5
|
const suite_1 = require("../../utils/suite");
|
|
6
6
|
const logger_1 = require("../../utils/logger");
|
|
7
|
+
const output_1 = require("../../utils/output");
|
|
7
8
|
const colors_1 = require("../../utils/colors");
|
|
8
|
-
async function main() {
|
|
9
|
-
console.log('\n' + '='.repeat(60));
|
|
10
|
-
console.log((0, colors_1.colorize)('Saved Suites', 'bright'));
|
|
11
|
-
console.log('='.repeat(60) + '\n');
|
|
9
|
+
async function main(opts = {}) {
|
|
12
10
|
try {
|
|
13
11
|
const suites = await (0, suite_1.listSuites)();
|
|
12
|
+
if (opts.json) {
|
|
13
|
+
(0, output_1.outputJson)({
|
|
14
|
+
count: suites.length,
|
|
15
|
+
suites: suites.map(s => ({
|
|
16
|
+
name: s.name,
|
|
17
|
+
description: s.description ?? null,
|
|
18
|
+
repoCount: s.entries.length,
|
|
19
|
+
entries: s.entries.map(e => ({ directoryName: e.directoryName, repoName: e.repoName })),
|
|
20
|
+
createdAt: s.createdAt,
|
|
21
|
+
updatedAt: s.updatedAt,
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log('\n' + '='.repeat(60));
|
|
27
|
+
console.log((0, colors_1.colorize)('Saved Suites', 'bright'));
|
|
28
|
+
console.log('='.repeat(60) + '\n');
|
|
14
29
|
if (suites.length === 0) {
|
|
15
30
|
(0, logger_1.logInfo)('No suites found');
|
|
16
31
|
console.log('\nCreate a new suite with:');
|
|
@@ -38,9 +53,13 @@ async function main() {
|
|
|
38
53
|
console.log('='.repeat(60) + '\n');
|
|
39
54
|
}
|
|
40
55
|
catch (error) {
|
|
41
|
-
(
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
(0, output_1.outputJsonError)(error instanceof Error ? error.message : 'Failed to list suites');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
(0, logger_1.logError)('Failed to list suites');
|
|
61
|
+
if (error instanceof Error)
|
|
62
|
+
(0, logger_1.logError)(error.message);
|
|
44
63
|
}
|
|
45
64
|
process.exit(1);
|
|
46
65
|
}
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
updateWorkspaceMetadata,
|
|
10
10
|
} from '../utils/dependency-analyzer';
|
|
11
11
|
import { logError, logInfo, logStep, logSuccess } from '../utils/logger';
|
|
12
|
+
import { outputJson, outputJsonError } from '../utils/output';
|
|
12
13
|
import { colorize } from '../utils/colors';
|
|
13
14
|
import inquirer from 'inquirer';
|
|
14
15
|
import { resolveWorkspace } from '../utils/command-helpers';
|
|
@@ -42,26 +43,53 @@ export function registerAnalyzeDepsCommand(parent: Command) {
|
|
|
42
43
|
.alias('ad')
|
|
43
44
|
.description('Analyze inter-repo dependencies')
|
|
44
45
|
.argument('[workspace]', 'Workspace name')
|
|
45
|
-
.
|
|
46
|
-
|
|
46
|
+
.option('--json', 'Output as JSON (no interactive save)')
|
|
47
|
+
.action(async (workspace, opts) => {
|
|
48
|
+
await handleAnalyzeDeps(workspace, opts);
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
async function handleAnalyzeDeps(workspaceArg?: string) {
|
|
52
|
+
async function handleAnalyzeDeps(workspaceArg?: string, opts: { json?: boolean } = {}) {
|
|
51
53
|
try {
|
|
54
|
+
// JSON mode is non-interactive: require an explicit workspace rather than prompt.
|
|
55
|
+
if (opts.json && !workspaceArg) {
|
|
56
|
+
outputJsonError('analyze-deps --json requires a workspace name');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
52
59
|
const selectedWorkspace = await resolveWorkspace(workspaceArg);
|
|
53
60
|
const workspacePath = path.join(WORKSPACES_DIR, selectedWorkspace);
|
|
54
61
|
const metadata = await loadMetadata(workspacePath);
|
|
55
62
|
|
|
56
63
|
if (!metadata) {
|
|
57
|
-
|
|
64
|
+
if (opts.json) outputJsonError(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
65
|
+
else logError(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
58
66
|
process.exit(1);
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
const repoNames = metadata.repositories.map(r => r.name);
|
|
70
|
+
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
const analyses = await analyzeDependencies(workspacePath, repoNames);
|
|
73
|
+
const cycles = detectCircularDependencies(analyses);
|
|
74
|
+
const missing = new Set<string>();
|
|
75
|
+
for (const [, a] of analyses) for (const dep of a.missingDependencies) missing.add(dep);
|
|
76
|
+
outputJson({
|
|
77
|
+
workspace: selectedWorkspace,
|
|
78
|
+
repositories: Array.from(analyses.entries()).map(([name, a]) => ({
|
|
79
|
+
name,
|
|
80
|
+
dependencies: a.dependencies,
|
|
81
|
+
dependents: a.dependents,
|
|
82
|
+
missingDependencies: a.missingDependencies,
|
|
83
|
+
})),
|
|
84
|
+
circularDependencies: cycles,
|
|
85
|
+
missingRepositories: Array.from(missing),
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
logStep(`Analyzing dependencies for workspace: ${colorize(selectedWorkspace, 'cyan')}`);
|
|
62
91
|
logInfo('Scanning package.json, Dockerfile, and docker-compose.yml files...');
|
|
63
92
|
|
|
64
|
-
const repoNames = metadata.repositories.map(r => r.name);
|
|
65
93
|
const analyses = await analyzeDependencies(workspacePath, repoNames);
|
|
66
94
|
|
|
67
95
|
displayDependencyAnalysis(analyses);
|
|
@@ -110,9 +138,11 @@ async function handleAnalyzeDeps(workspaceArg?: string) {
|
|
|
110
138
|
}
|
|
111
139
|
}
|
|
112
140
|
} catch (error) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
outputJsonError(error instanceof Error ? error.message : 'Failed to analyze dependencies');
|
|
143
|
+
} else {
|
|
144
|
+
logError('Failed to analyze dependencies');
|
|
145
|
+
if (error instanceof Error) logError(error.message);
|
|
116
146
|
}
|
|
117
147
|
process.exit(1);
|
|
118
148
|
}
|
package/src/commands/sessions.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as fs from 'fs/promises';
|
|
|
5
5
|
import { getWorkspaceSessions, WorkspaceSession } from '../utils/claude-sessions';
|
|
6
6
|
import { listWorkspaces } from '../utils/workspace-meta';
|
|
7
7
|
import { logError, logInfo } from '../utils/logger';
|
|
8
|
+
import { outputJson, outputJsonError } from '../utils/output';
|
|
8
9
|
import { colorize } from '../utils/colors';
|
|
9
10
|
import inquirer from 'inquirer';
|
|
10
11
|
import autocompletePrompt from 'inquirer-autocomplete-prompt';
|
|
@@ -20,16 +21,21 @@ export function registerSessionsCommand(parent: Command) {
|
|
|
20
21
|
.command('sessions')
|
|
21
22
|
.alias('ses')
|
|
22
23
|
.description('Resume a Claude session in a workspace')
|
|
23
|
-
.
|
|
24
|
-
|
|
24
|
+
.option('--json', 'List sessions as JSON (no interactive resume)')
|
|
25
|
+
.action(async (opts) => {
|
|
26
|
+
await handleSessions(opts);
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
async function handleSessions() {
|
|
30
|
+
async function handleSessions(opts: { json?: boolean } = {}) {
|
|
29
31
|
try {
|
|
30
32
|
const sessions = await getWorkspaceSessions();
|
|
31
33
|
|
|
32
34
|
if (sessions.length === 0) {
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
outputJson({ count: 0, sessions: [] });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
33
39
|
logInfo('No workspace sessions found.');
|
|
34
40
|
console.log('\nYou can create a workspace with: nemus create');
|
|
35
41
|
console.log('Or navigate to one with: w go');
|
|
@@ -46,6 +52,23 @@ async function handleSessions() {
|
|
|
46
52
|
return { session, repoLabel };
|
|
47
53
|
});
|
|
48
54
|
|
|
55
|
+
// JSON mode: list sessions to stdout, no resume prompt / temp-file writes.
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
outputJson({
|
|
58
|
+
count: items.length,
|
|
59
|
+
sessions: items.map(i => ({
|
|
60
|
+
workspaceName: i.session.workspaceName,
|
|
61
|
+
workspacePath: i.session.workspacePath,
|
|
62
|
+
sessionId: i.session.sessionId,
|
|
63
|
+
agentType: i.session.agentType ?? null,
|
|
64
|
+
lastActive: i.session.lastActiveLabel,
|
|
65
|
+
lastActiveAt: i.session.lastActiveAt.toISOString(),
|
|
66
|
+
repoCount: workspaceMap.get(i.session.workspaceName)?.metadata?.repositories?.length ?? 0,
|
|
67
|
+
})),
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
const maxNameLen = Math.max(...items.map(i => i.session.workspaceName.length));
|
|
50
73
|
|
|
51
74
|
console.log('');
|
|
@@ -92,8 +115,12 @@ async function handleSessions() {
|
|
|
92
115
|
console.log(`\n${colorize('Resuming:', 'green')} ${session.workspaceName} (last active ${session.lastActiveLabel})`);
|
|
93
116
|
} catch (error) {
|
|
94
117
|
if ((error as any)?.name === 'ExitPromptError') return;
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
outputJsonError(error instanceof Error ? error.message : 'Failed to list sessions');
|
|
120
|
+
} else {
|
|
121
|
+
logError('Failed to list sessions');
|
|
122
|
+
if (error instanceof Error) { logError(error.message); }
|
|
123
|
+
}
|
|
97
124
|
process.exit(1);
|
|
98
125
|
}
|
|
99
126
|
}
|
|
@@ -17,9 +17,10 @@ export function registerSuiteCommands(parent: Command) {
|
|
|
17
17
|
suite
|
|
18
18
|
.command('list')
|
|
19
19
|
.description('List all saved suites')
|
|
20
|
-
.
|
|
20
|
+
.option('--json', 'Output as JSON')
|
|
21
|
+
.action(async (opts) => {
|
|
21
22
|
const { main } = await import('./list');
|
|
22
|
-
await main();
|
|
23
|
+
await main(opts);
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
suite
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { mockListSuites } = vi.hoisted(() => ({ mockListSuites: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
vi.mock('../../utils/suite', () => ({ listSuites: mockListSuites }));
|
|
6
|
+
vi.mock('../../utils/logger', () => ({ logInfo: vi.fn(), logError: vi.fn() }));
|
|
7
|
+
vi.mock('../../utils/colors', () => ({ colorize: (t: string) => t }));
|
|
8
|
+
|
|
9
|
+
import { main } from './list';
|
|
10
|
+
|
|
11
|
+
function makeSuite(name: string, entries = 1) {
|
|
12
|
+
return {
|
|
13
|
+
name,
|
|
14
|
+
description: `${name} desc`,
|
|
15
|
+
entries: Array.from({ length: entries }, (_, i) => ({ directoryName: `repo-${i}`, repoName: `repo-${i}` })),
|
|
16
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
17
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('suite list --json', () => {
|
|
22
|
+
let writeSpy: ReturnType<typeof vi.spyOn>;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
26
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => vi.restoreAllMocks());
|
|
29
|
+
|
|
30
|
+
it('emits one valid JSON document with normalized suites', async () => {
|
|
31
|
+
mockListSuites.mockResolvedValueOnce([makeSuite('fees', 2), makeSuite('platform', 1)]);
|
|
32
|
+
await main({ json: true });
|
|
33
|
+
expect(writeSpy).toHaveBeenCalledTimes(1);
|
|
34
|
+
const payload = JSON.parse(writeSpy.mock.calls[0][0] as string);
|
|
35
|
+
expect(payload.count).toBe(2);
|
|
36
|
+
expect(payload.suites[0]).toEqual({
|
|
37
|
+
name: 'fees',
|
|
38
|
+
description: 'fees desc',
|
|
39
|
+
repoCount: 2,
|
|
40
|
+
entries: [
|
|
41
|
+
{ directoryName: 'repo-0', repoName: 'repo-0' },
|
|
42
|
+
{ directoryName: 'repo-1', repoName: 'repo-1' },
|
|
43
|
+
],
|
|
44
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
45
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('empty list emits count 0 (no header/log noise)', async () => {
|
|
50
|
+
mockListSuites.mockResolvedValueOnce([]);
|
|
51
|
+
await main({ json: true });
|
|
52
|
+
const payload = JSON.parse(writeSpy.mock.calls[0][0] as string);
|
|
53
|
+
expect(payload).toEqual({ count: 0, suites: [] });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('non-json mode does not write to the stdout data channel', async () => {
|
|
57
|
+
mockListSuites.mockResolvedValueOnce([makeSuite('fees')]);
|
|
58
|
+
await main();
|
|
59
|
+
// human output goes through console.log (mocked), not process.stdout.write
|
|
60
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -2,16 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
import { listSuites } from '../../utils/suite';
|
|
4
4
|
import { logInfo, logError } from '../../utils/logger';
|
|
5
|
+
import { outputJson, outputJsonError } from '../../utils/output';
|
|
5
6
|
import { colorize } from '../../utils/colors';
|
|
6
7
|
|
|
7
|
-
export async function main() {
|
|
8
|
-
console.log('\n' + '='.repeat(60));
|
|
9
|
-
console.log(colorize('Saved Suites', 'bright'));
|
|
10
|
-
console.log('='.repeat(60) + '\n');
|
|
11
|
-
|
|
8
|
+
export async function main(opts: { json?: boolean } = {}) {
|
|
12
9
|
try {
|
|
13
10
|
const suites = await listSuites();
|
|
14
11
|
|
|
12
|
+
if (opts.json) {
|
|
13
|
+
outputJson({
|
|
14
|
+
count: suites.length,
|
|
15
|
+
suites: suites.map(s => ({
|
|
16
|
+
name: s.name,
|
|
17
|
+
description: s.description ?? null,
|
|
18
|
+
repoCount: s.entries.length,
|
|
19
|
+
entries: s.entries.map(e => ({ directoryName: e.directoryName, repoName: e.repoName })),
|
|
20
|
+
createdAt: s.createdAt,
|
|
21
|
+
updatedAt: s.updatedAt,
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('\n' + '='.repeat(60));
|
|
28
|
+
console.log(colorize('Saved Suites', 'bright'));
|
|
29
|
+
console.log('='.repeat(60) + '\n');
|
|
30
|
+
|
|
15
31
|
if (suites.length === 0) {
|
|
16
32
|
logInfo('No suites found');
|
|
17
33
|
console.log('\nCreate a new suite with:');
|
|
@@ -43,9 +59,11 @@ export async function main() {
|
|
|
43
59
|
|
|
44
60
|
console.log('='.repeat(60) + '\n');
|
|
45
61
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
if (opts.json) {
|
|
63
|
+
outputJsonError(error instanceof Error ? error.message : 'Failed to list suites');
|
|
64
|
+
} else {
|
|
65
|
+
logError('Failed to list suites');
|
|
66
|
+
if (error instanceof Error) logError(error.message);
|
|
49
67
|
}
|
|
50
68
|
process.exit(1);
|
|
51
69
|
}
|