@nemus-cli/nemus 0.4.0 → 0.8.0

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.
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG_KEYS = exports.CONFIG_SCHEMA = void 0;
4
+ exports.isConfigKey = isConfigKey;
5
+ exports.parseConfigValue = parseConfigValue;
6
+ exports.applyConfigSet = applyConfigSet;
7
+ exports.applyConfigUnset = applyConfigUnset;
8
+ exports.formatConfigValue = formatConfigValue;
9
+ const config_1 = require("./config");
10
+ const AGENT_VALUES = ['claude', 'pi', 'opencode', 'codex', 'gemini'];
11
+ exports.CONFIG_SCHEMA = {
12
+ workspacesDir: { type: 'string', describe: 'Directory where workspaces are created' },
13
+ githubOrg: { type: 'string', allowEmpty: true, describe: 'Default GitHub org for repo lookups' },
14
+ cloneProtocol: { type: 'enum', values: ['ssh', 'https'], describe: 'Protocol used to clone repos' },
15
+ aiAgent: {
16
+ type: 'enum',
17
+ values: [...AGENT_VALUES, 'both', 'auto'],
18
+ describe: 'AI agent(s) to integrate with',
19
+ },
20
+ primaryAgent: {
21
+ type: 'enum',
22
+ values: [...AGENT_VALUES, 'auto'],
23
+ describe: 'Agent launched when opening a workspace',
24
+ },
25
+ autoLaunchClaude: { type: 'boolean', describe: 'Auto-launch the agent after creating a workspace' },
26
+ generateClaudeContext: { type: 'boolean', describe: 'Generate agent context files (AGENTS.md)' },
27
+ installMcp: { type: 'boolean', describe: 'Install the MCP server during configure' },
28
+ piWorkspaceInputStatus: { type: 'boolean', describe: "Show workspace status in Pi's input area" },
29
+ claudeWorkspaceStatusLine: { type: 'boolean', describe: "Show workspace table in Claude's status line" },
30
+ autoReportBugs: { type: 'boolean', describe: 'Auto-file a GitHub issue when a command crashes' },
31
+ };
32
+ exports.CONFIG_KEYS = Object.keys(exports.CONFIG_SCHEMA).sort();
33
+ const TRUE_WORDS = new Set(['true', '1', 'yes', 'on', 'y']);
34
+ const FALSE_WORDS = new Set(['false', '0', 'no', 'off', 'n']);
35
+ /** True if `key` is a writable config key. */
36
+ function isConfigKey(key) {
37
+ return Object.prototype.hasOwnProperty.call(exports.CONFIG_SCHEMA, key);
38
+ }
39
+ /** Validate + coerce a raw string for `key` into the field's typed value. */
40
+ function parseConfigValue(key, raw) {
41
+ const spec = exports.CONFIG_SCHEMA[key];
42
+ if (spec.type === 'boolean') {
43
+ const v = raw.trim().toLowerCase();
44
+ if (TRUE_WORDS.has(v))
45
+ return { ok: true, value: true };
46
+ if (FALSE_WORDS.has(v))
47
+ return { ok: true, value: false };
48
+ return { ok: false, error: `${key} expects a boolean (true/false); got "${raw}"` };
49
+ }
50
+ if (spec.type === 'enum') {
51
+ // Enum values are all lowercase, so normalize input like booleans do —
52
+ // `HTTPS` / ` https ` should resolve to the canonical value, not fail.
53
+ const v = raw.trim().toLowerCase();
54
+ if (spec.values.includes(v)) {
55
+ return { ok: true, value: v };
56
+ }
57
+ return { ok: false, error: `${key} must be one of: ${spec.values.join(', ')}; got "${raw}"` };
58
+ }
59
+ // string: trim surrounding whitespace (a stray space in a path/org is almost
60
+ // always a mistake), but preserve case.
61
+ const trimmed = raw.trim();
62
+ if (!spec.allowEmpty && trimmed === '') {
63
+ return { ok: false, error: `${key} cannot be empty` };
64
+ }
65
+ return { ok: true, value: trimmed };
66
+ }
67
+ /** Apply a `set` to a config object, returning a NEW config or an error. Pure. */
68
+ function applyConfigSet(current, key, raw) {
69
+ if (!isConfigKey(key)) {
70
+ return { ok: false, error: `Unknown config key "${key}". Run "nemus config list" to see valid keys.` };
71
+ }
72
+ const parsed = parseConfigValue(key, raw);
73
+ if (!parsed.ok)
74
+ return parsed;
75
+ return { ok: true, next: { ...current, [key]: parsed.value }, value: parsed.value };
76
+ }
77
+ /** Reset a key to its default value, returning a NEW config or an error. Pure. */
78
+ function applyConfigUnset(current, key) {
79
+ if (!isConfigKey(key)) {
80
+ return { ok: false, error: `Unknown config key "${key}". Run "nemus config list" to see valid keys.` };
81
+ }
82
+ const value = config_1.CONFIG_DEFAULTS[key];
83
+ return { ok: true, next: { ...current, [key]: value }, value };
84
+ }
85
+ /** Render a config value for plain (scriptable) stdout output. */
86
+ function formatConfigValue(value) {
87
+ return typeof value === 'boolean' ? String(value) : String(value ?? '');
88
+ }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.config = exports.CLONE_MAX_BUFFER = exports.CLONE_TIMEOUT_MS = exports.CONFIG_PATH = exports.META_FILENAME = exports.SUITES_FILE = exports.HISTORY_FILE = exports.CACHE_DIR = exports.WORKSPACES_DIR = void 0;
36
+ exports.config = exports.CLONE_MAX_BUFFER = exports.CLONE_TIMEOUT_MS = exports.CONFIG_PATH = exports.META_FILENAME = exports.SUITES_FILE = exports.HISTORY_FILE = exports.CACHE_DIR = exports.WORKSPACES_DIR = exports.CONFIG_DEFAULTS = void 0;
37
37
  exports.getUserConfig = getUserConfig;
38
38
  exports.getPackageVersion = getPackageVersion;
39
39
  exports.getCloneUrl = getCloneUrl;
@@ -77,7 +77,7 @@ const CACHE_DIR_RESOLVED = process.env.NEMUS_CACHE_DIR || process.env.WORKSPACE_
77
77
  migrateLegacyCacheDir(CACHE_DIR_RESOLVED);
78
78
  // Config file lives inside the cache dir.
79
79
  const CONFIG_FILE = path.join(CACHE_DIR_RESOLVED, 'config.json');
80
- const DEFAULTS = {
80
+ exports.CONFIG_DEFAULTS = {
81
81
  workspacesDir: path.join(HOME_DIR, 'workspaces'),
82
82
  githubOrg: '',
83
83
  autoLaunchClaude: true,
@@ -90,6 +90,8 @@ const DEFAULTS = {
90
90
  claudeWorkspaceStatusLine: true,
91
91
  autoReportBugs: false,
92
92
  };
93
+ // Internal alias retained for the many references below.
94
+ const DEFAULTS = exports.CONFIG_DEFAULTS;
93
95
  function loadConfigFileSync() {
94
96
  try {
95
97
  const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyGlobalFlags = applyGlobalFlags;
4
+ const colors_1 = require("./colors");
5
+ const logger_1 = require("./logger");
6
+ /**
7
+ * Apply parsed global flags to process-wide state. Kept pure over injected
8
+ * setters so it's testable without commander — the `preAction` hook in
9
+ * program.ts is a one-line call into this. Only ever turns features OFF here:
10
+ * color defaults on (and env/TTY detection already ran at import), so we act
11
+ * solely on an explicit `--no-color` (`color === false`).
12
+ */
13
+ function applyGlobalFlags(opts, deps = {
14
+ setColorEnabled: colors_1.setColorEnabled,
15
+ setQuiet: logger_1.setQuiet,
16
+ }) {
17
+ if (opts.color === false)
18
+ deps.setColorEnabled(false);
19
+ if (opts.quiet)
20
+ deps.setQuiet(true);
21
+ }
@@ -1,11 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.logStep = exports.logWarning = exports.logError = exports.logSuccess = exports.logInfo = void 0;
3
+ exports.logStep = exports.logWarning = exports.logError = exports.logSuccess = exports.logInfo = exports.isQuiet = exports.setQuiet = void 0;
4
4
  const colors_1 = require("./colors");
5
5
  // Diagnostics (info/success/error/warn/step) go to STDERR so stdout carries only
6
6
  // a command's actual data — required for clean `--json` piping (nemus list
7
7
  // --json | jq …) and for non-TTY consumers. Use `outputJson`/stdout for data.
8
8
  const logStream = (line) => console.error(line);
9
+ // `--quiet` silences routine progress (info/success/step) while KEEPING warnings
10
+ // and errors, which a script or human still needs to see.
11
+ let quiet = false;
12
+ const setQuiet = (on) => {
13
+ quiet = on;
14
+ };
15
+ exports.setQuiet = setQuiet;
16
+ const isQuiet = () => quiet;
17
+ exports.isQuiet = isQuiet;
9
18
  const getTimestamp = () => {
10
19
  const now = new Date();
11
20
  return now.toLocaleTimeString('en-US', {
@@ -16,10 +25,14 @@ const getTimestamp = () => {
16
25
  });
17
26
  };
18
27
  const logInfo = (message) => {
28
+ if (quiet)
29
+ return;
19
30
  logStream(`${colors_1.colors.gray}[${getTimestamp()}]${colors_1.colors.reset} ${message}`);
20
31
  };
21
32
  exports.logInfo = logInfo;
22
33
  const logSuccess = (message) => {
34
+ if (quiet)
35
+ return;
23
36
  logStream(`${colors_1.colors.gray}[${getTimestamp()}]${colors_1.colors.reset} ${(0, colors_1.colorize)('✓', 'green')} ${message}`);
24
37
  };
25
38
  exports.logSuccess = logSuccess;
@@ -32,6 +45,8 @@ const logWarning = (message) => {
32
45
  };
33
46
  exports.logWarning = logWarning;
34
47
  const logStep = (stepOrMessage, total, message) => {
48
+ if (quiet)
49
+ return;
35
50
  if (typeof stepOrMessage === 'string') {
36
51
  // Single parameter version: just a message
37
52
  logStream(`${colors_1.colors.gray}[${getTimestamp()}]${colors_1.colors.reset} ${(0, colors_1.colorize)('▸', 'cyan')} ${stepOrMessage}`);
@@ -33,7 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.REFLECT_REPORTS_DIR = exports.REFLECT_SCHEMA = void 0;
36
+ exports.REFLECT_REPORTS_DIR = exports.REFLECT_SCHEMA = exports.PRIORITY_HEADING = exports.KIND_LABEL = void 0;
37
+ exports.severityCounts = severityCounts;
38
+ exports.severitySummary = severitySummary;
39
+ exports.groupRecommendations = groupRecommendations;
40
+ exports.renderReportMarkdown = renderReportMarkdown;
37
41
  exports.isCorrectionPrompt = isCorrectionPrompt;
38
42
  exports.distillTranscript = distillTranscript;
39
43
  exports.classifyAgentsMd = classifyAgentsMd;
@@ -41,12 +45,117 @@ exports.findLatestTranscriptFile = findLatestTranscriptFile;
41
45
  exports.gatherReflectionCorpus = gatherReflectionCorpus;
42
46
  exports.parseReflectionReport = parseReflectionReport;
43
47
  exports.saveReflectionReport = saveReflectionReport;
48
+ exports.listSavedReports = listSavedReports;
49
+ exports.findSavedMatches = findSavedMatches;
50
+ exports.loadSavedReport = loadSavedReport;
44
51
  const fs = __importStar(require("fs/promises"));
45
52
  const path = __importStar(require("path"));
46
53
  const config_1 = require("./config");
47
54
  const workspace_meta_1 = require("./workspace-meta");
48
55
  const agent_config_1 = require("./agent-config");
49
56
  const claude_sessions_1 = require("./claude-sessions");
57
+ /** Count recommendations by priority. */
58
+ function severityCounts(recs) {
59
+ const counts = { high: 0, medium: 0, low: 0 };
60
+ for (const r of recs)
61
+ counts[r.priority]++;
62
+ return counts;
63
+ }
64
+ /** "3 high · 2 medium · 1 low", omitting zero buckets; '' when there are none. */
65
+ function severitySummary(recs) {
66
+ const c = severityCounts(recs);
67
+ return ['high', 'medium', 'low']
68
+ .filter((p) => c[p] > 0)
69
+ .map((p) => `${c[p]} ${p}`)
70
+ .join(' · ');
71
+ }
72
+ exports.KIND_LABEL = {
73
+ skill: 'Skill',
74
+ context: 'Context/AGENTS.md',
75
+ test: 'Test',
76
+ prompt: 'Prompt',
77
+ connectivity: 'Connectivity',
78
+ workflow: 'Workflow',
79
+ other: 'Other',
80
+ };
81
+ // Back-compat alias for existing references.
82
+ const MD_KIND_LABEL = exports.KIND_LABEL;
83
+ exports.PRIORITY_HEADING = {
84
+ high: 'High priority',
85
+ medium: 'Medium priority',
86
+ low: 'Low priority',
87
+ };
88
+ const KIND_ORDER = ['skill', 'context', 'test', 'prompt', 'connectivity', 'workflow', 'other'];
89
+ const PRIORITY_ORDER = ['high', 'medium', 'low'];
90
+ const PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
91
+ /**
92
+ * Split recommendations into ordered, non-empty groups by either priority
93
+ * (high→low) or kind (a fixed, stable order). When grouping by kind, each
94
+ * group's recs are sorted high-priority first. Pure + unit-tested; shared by the
95
+ * Markdown renderer and the human printer so the two never diverge.
96
+ */
97
+ function groupRecommendations(recs, groupBy) {
98
+ if (groupBy === 'kind') {
99
+ return KIND_ORDER.map((k) => ({
100
+ key: k,
101
+ heading: exports.KIND_LABEL[k],
102
+ recs: recs
103
+ .filter((r) => r.kind === k)
104
+ .sort((a, b) => PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority]),
105
+ })).filter((g) => g.recs.length > 0);
106
+ }
107
+ return PRIORITY_ORDER.map((p) => ({
108
+ key: p,
109
+ heading: exports.PRIORITY_HEADING[p],
110
+ recs: recs.filter((r) => r.priority === p),
111
+ })).filter((g) => g.recs.length > 0);
112
+ }
113
+ /** A fenced code block whose fence is guaranteed longer than any backtick run
114
+ * inside `body`, so the snippet can't break out of its own fence. */
115
+ function fencedBlock(body) {
116
+ const longest = Math.max(0, ...(body.match(/`+/g) ?? []).map((m) => m.length));
117
+ const fence = '`'.repeat(Math.max(3, longest + 1));
118
+ return `${fence}\n${body}\n${fence}`;
119
+ }
120
+ /**
121
+ * Render a reflection report as clean Markdown — for pasting into an issue/PR or
122
+ * saving alongside the JSON. Pure (no color, no I/O) so it's unit-tested.
123
+ * Recommendations are grouped by severity (high→low); each carries its kind,
124
+ * optional target, detail, and a fenced example.
125
+ */
126
+ function renderReportMarkdown(report, meta, groupBy = 'priority') {
127
+ const lines = ['# Reflection', ''];
128
+ const scope = meta.workspace
129
+ ? `workspace **${meta.workspace}**`
130
+ : `${meta.analyzed} session${meta.analyzed === 1 ? '' : 's'} across ${meta.workspaces} workspace${meta.workspaces === 1 ? '' : 's'}`;
131
+ const stamp = meta.generatedAt ? ` · ${meta.generatedAt}` : '';
132
+ lines.push(`_${scope}${stamp}_`, '');
133
+ if (report.summary.trim())
134
+ lines.push(report.summary.trim(), '');
135
+ if (report.recommendations.length === 0) {
136
+ lines.push('## Recommendations', '', '_No specific recommendations — looks solid._', '');
137
+ return lines.join('\n');
138
+ }
139
+ lines.push('## Recommendations', '', `**${severitySummary(report.recommendations)}**`, '');
140
+ for (const group of groupRecommendations(report.recommendations, groupBy)) {
141
+ lines.push(`### ${group.heading}`, '');
142
+ for (const r of group.recs) {
143
+ const target = r.target ? ` (\`${r.target}\`)` : '';
144
+ // Under a kind heading the [Kind] prefix is redundant; show a priority tag
145
+ // instead. Under a priority heading, show the kind.
146
+ const label = groupBy === 'kind' ? `_${r.priority}_ — ` : `[${exports.KIND_LABEL[r.kind]}] `;
147
+ lines.push(`- **${label}${r.title}**${target}`);
148
+ if (r.detail.trim()) {
149
+ lines.push(...r.detail.trim().split('\n').map((l) => ` ${l}`));
150
+ }
151
+ if (r.example?.trim()) {
152
+ lines.push('', ...fencedBlock(r.example.trim()).split('\n').map((l) => ` ${l}`));
153
+ }
154
+ lines.push('');
155
+ }
156
+ }
157
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
158
+ }
50
159
  // --------------------------------------------------------- transcript distill
51
160
  // Kept deliberately lean: the judge runs on the user's own (often local, slow)
52
161
  // agent, and a 10-workspace corpus at full verbosity produced a ~200KB prompt
@@ -408,3 +517,63 @@ async function saveReflectionReport(report, meta) {
408
517
  await fs.writeFile(file, JSON.stringify({ generatedAt: new Date().toISOString(), ...meta, ...report }, null, 2));
409
518
  return file;
410
519
  }
520
+ /**
521
+ * List saved reports under `~/.nemus/reflect/`, newest first (filenames start
522
+ * with an ISO timestamp, so a reverse name sort is chronological). Unreadable /
523
+ * unparseable files are skipped, not fatal. Returns [] if the dir is absent.
524
+ */
525
+ async function listSavedReports(dir = exports.REFLECT_REPORTS_DIR) {
526
+ let names;
527
+ try {
528
+ names = await fs.readdir(dir);
529
+ }
530
+ catch {
531
+ return [];
532
+ }
533
+ const jsons = names.filter((n) => n.endsWith('.json')).sort().reverse();
534
+ const out = [];
535
+ for (const name of jsons) {
536
+ const file = path.join(dir, name);
537
+ try {
538
+ const raw = JSON.parse(await fs.readFile(file, 'utf-8'));
539
+ out.push({
540
+ id: name.replace(/\.json$/, ''),
541
+ file,
542
+ generatedAt: typeof raw.generatedAt === 'string' ? raw.generatedAt : undefined,
543
+ analyzed: typeof raw.analyzed === 'number' ? raw.analyzed : 0,
544
+ workspaces: typeof raw.workspaces === 'number' ? raw.workspaces : 0,
545
+ workspace: typeof raw.workspace === 'string' ? raw.workspace : undefined,
546
+ report: parseReflectionReport(raw),
547
+ });
548
+ }
549
+ catch {
550
+ /* skip a corrupt/partial file */
551
+ }
552
+ }
553
+ return out;
554
+ }
555
+ /**
556
+ * Resolve a report reference against an (already newest-first) list. Returns ALL
557
+ * matches so a caller can detect ambiguity: `undefined`/`'latest'` -> the newest;
558
+ * an exact id -> that one; otherwise every id with the prefix (newest-first). An
559
+ * exact id always wins over prefixes, so an id can't be ambiguous with itself.
560
+ * Pure + unit-tested.
561
+ */
562
+ function findSavedMatches(all, ref) {
563
+ if (all.length === 0)
564
+ return [];
565
+ if (!ref || ref === 'latest')
566
+ return [all[0]];
567
+ const exact = all.find((r) => r.id === ref);
568
+ if (exact)
569
+ return [exact];
570
+ return all.filter((r) => r.id.startsWith(ref));
571
+ }
572
+ /**
573
+ * Load one saved report by id. `undefined`/`'latest'` returns the newest; an id
574
+ * is matched exactly, then as a prefix (newest match wins). Returns null when
575
+ * nothing matches. For ambiguity-aware callers, use findSavedMatches directly.
576
+ */
577
+ async function loadSavedReport(ref, dir = exports.REFLECT_REPORTS_DIR) {
578
+ return findSavedMatches(await listSavedReports(dir), ref)[0] ?? null;
579
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nemus-cli/nemus",
3
- "version": "0.4.0",
3
+ "version": "0.8.0",
4
4
  "workspaces": [
5
5
  "packages/*"
6
6
  ],
@@ -0,0 +1,112 @@
1
+ import { Command } from 'commander';
2
+ import { getUserConfig, saveUserConfig, CONFIG_PATH } from '../utils/config';
3
+ import {
4
+ CONFIG_KEYS,
5
+ CONFIG_SCHEMA,
6
+ isConfigKey,
7
+ applyConfigSet,
8
+ applyConfigUnset,
9
+ formatConfigValue,
10
+ } from '../utils/config-schema';
11
+ import { outputJson, outputJsonError } from '../utils/output';
12
+ import { logSuccess, logError } from '../utils/logger';
13
+ import { colorize } from '../utils/colors';
14
+
15
+ /**
16
+ * Non-interactive config management: `nemus config get/set/unset/list/path`.
17
+ * Complements the interactive `configure` wizard and is script-friendly —
18
+ * `get`/`list` write DATA to stdout (raw value, or JSON with --json), logs go to
19
+ * stderr. Values are validated/coerced against config-schema.ts.
20
+ */
21
+ export function registerConfigCommand(parent: Command): void {
22
+ const config = parent.command('config').description('Get or set Nemus configuration');
23
+
24
+ config
25
+ .command('get')
26
+ .description('Print a config value (or all values with no key)')
27
+ .argument('[key]', 'Config key')
28
+ .option('--json', 'Output as JSON')
29
+ .action((key: string | undefined, opts: { json?: boolean }) => {
30
+ const cfg = getUserConfig();
31
+ if (key === undefined) {
32
+ printAll(cfg, opts.json);
33
+ return;
34
+ }
35
+ if (!isConfigKey(key)) {
36
+ if (opts.json) outputJsonError(`Unknown config key "${key}"`);
37
+ else logError(`Unknown config key "${key}". Run "nemus config list" to see valid keys.`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ const value = cfg[key];
42
+ if (opts.json) outputJson({ key, value });
43
+ else process.stdout.write(formatConfigValue(value) + '\n');
44
+ });
45
+
46
+ config
47
+ .command('set')
48
+ .description('Set a config value')
49
+ .argument('<key>', 'Config key')
50
+ .argument('<value>', 'New value')
51
+ .option('--json', 'Output as JSON')
52
+ .action((key: string, value: string, opts: { json?: boolean }) => {
53
+ const result = applyConfigSet(getUserConfig(), key, value);
54
+ if (!result.ok) {
55
+ if (opts.json) outputJsonError(result.error);
56
+ else logError(result.error);
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ saveUserConfig(result.next);
61
+ if (opts.json) outputJson({ ok: true, key, value: result.value });
62
+ else logSuccess(`Set ${colorize(key, 'cyan')} = ${formatConfigValue(result.value)}`);
63
+ });
64
+
65
+ config
66
+ .command('unset')
67
+ .description('Reset a config value to its default')
68
+ .argument('<key>', 'Config key')
69
+ .option('--json', 'Output as JSON')
70
+ .action((key: string, opts: { json?: boolean }) => {
71
+ const result = applyConfigUnset(getUserConfig(), key);
72
+ if (!result.ok) {
73
+ if (opts.json) outputJsonError(result.error);
74
+ else logError(result.error);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ saveUserConfig(result.next);
79
+ if (opts.json) outputJson({ ok: true, key, value: result.value });
80
+ else logSuccess(`Reset ${colorize(key, 'cyan')} to default (${formatConfigValue(result.value)})`);
81
+ });
82
+
83
+ config
84
+ .command('list')
85
+ .alias('ls')
86
+ .description('List all config keys and current values')
87
+ .option('--json', 'Output as JSON')
88
+ .action((opts: { json?: boolean }) => printAll(getUserConfig(), opts.json));
89
+
90
+ config
91
+ .command('path')
92
+ .description('Print the path to the config file')
93
+ .action(() => {
94
+ process.stdout.write(CONFIG_PATH + '\n');
95
+ });
96
+ }
97
+
98
+ function printAll(cfg: ReturnType<typeof getUserConfig>, json?: boolean): void {
99
+ if (json) {
100
+ const values: Record<string, unknown> = {};
101
+ for (const key of CONFIG_KEYS) values[key] = cfg[key];
102
+ outputJson({ path: CONFIG_PATH, values });
103
+ return;
104
+ }
105
+ const width = Math.max(...CONFIG_KEYS.map((k) => k.length));
106
+ console.log(colorize('Nemus configuration', 'bright') + colorize(` (${CONFIG_PATH})`, 'dim'));
107
+ for (const key of CONFIG_KEYS) {
108
+ const val = formatConfigValue(cfg[key]);
109
+ const shown = val === '' ? colorize('(empty)', 'dim') : val;
110
+ console.log(` ${key.padEnd(width)} ${shown} ${colorize(CONFIG_SCHEMA[key].describe, 'dim')}`);
111
+ }
112
+ }