@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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-09-01
11
+
12
+ ### Added
13
+
14
+ - **`nemus reflect history`** and **`nemus reflect show [id]`** to review saved
15
+ reports (under `~/.nemus/reflect/`) without re-running the judge. `show`
16
+ defaults to the latest, accepts an id or id-prefix, and supports
17
+ `--json` / `--markdown` / `--group-by`.
18
+ - **`nemus reflect --group-by kind|priority`** (default `priority`) to control
19
+ how recommendations are grouped in both the human and Markdown output.
20
+
21
+ ## [0.7.0] - 2026-09-01
22
+
23
+ ### Added
24
+
25
+ - **`nemus reflect --markdown`** — render the reflection report as clean,
26
+ severity-grouped Markdown to stdout (pipe into a file or an issue:
27
+ `nemus reflect --markdown > reflection.md`). Fenced example snippets are
28
+ escaped so they can't break out of their own code block.
29
+ - The human `reflect` report now prints a **severity summary line**
30
+ (e.g. `2 high · 1 medium`).
31
+
32
+ ## [0.6.0] - 2026-09-01
33
+
34
+ ### Added
35
+
36
+ - **`nemus config` command** for non-interactive configuration:
37
+ `config get [key]`, `set <key> <value>`, `unset <key>`, `list` (alias `ls`),
38
+ and `path`. Values are validated and coerced per field (booleans accept
39
+ `true/false/yes/no/on/off/1/0`; enums like `cloneProtocol` are checked), an
40
+ unknown key or invalid value exits non-zero with a clear message, and
41
+ `get`/`list` support `--json`. Complements the interactive `configure` wizard
42
+ and pairs well with `--quiet` for scripting.
43
+
44
+ ## [0.5.0] - 2026-09-01
45
+
46
+ ### Added
47
+
48
+ - **`--no-color` and `-q, --quiet` global flags.** `--no-color` disables ANSI
49
+ color; Nemus also honors the standard [`NO_COLOR`](https://no-color.org) env
50
+ var, auto-disables color when stdout isn't a TTY, and respects `FORCE_COLOR=1`.
51
+ `--quiet` suppresses routine progress logs (info/success/step) while still
52
+ showing warnings and errors; data output (including `--json`) is unaffected.
53
+
10
54
  ## [0.4.0] - 2026-09-01
11
55
 
12
56
  ### Changed
package/README.md CHANGED
@@ -243,6 +243,31 @@ The workspace-scoped ones (`status`/`doctor`/`analyze-deps`) need an explicit
243
243
  workspace name with `--json` (they never prompt). On failure, `--json` prints a
244
244
  parseable `{ "ok": false, "error": … }` to stdout and exits non-zero.
245
245
 
246
+ ### Configuration
247
+
248
+ Run `nemus configure` for the interactive wizard, or manage settings
249
+ non-interactively (handy for scripts and dotfiles):
250
+
251
+ ```bash
252
+ nemus config list # all keys, values, and descriptions
253
+ nemus config get cloneProtocol # print one value (raw, for scripts)
254
+ nemus config set cloneProtocol https # validated + coerced per key
255
+ nemus config set autoReportBugs yes # booleans accept true/false/yes/no/on/off/1/0
256
+ nemus config unset githubOrg # reset a key to its default
257
+ nemus config path # print the config file location
258
+ ```
259
+
260
+ `get`/`list` also accept `--json`. An unknown key or an invalid value exits
261
+ non-zero with a clear message (e.g. `cloneProtocol must be one of: ssh, https`).
262
+
263
+ ### Global flags
264
+
265
+ - `--no-color` — disable ANSI color. Nemus also honors the standard
266
+ [`NO_COLOR`](https://no-color.org) env var and auto-disables color when stdout
267
+ isn't a TTY (piped/redirected); `FORCE_COLOR=1` forces it on.
268
+ - `-q, --quiet` — suppress routine progress logs (info/success/step) while still
269
+ showing warnings and errors. Data (including `--json`) is unaffected.
270
+
246
271
  ### Shell completions
247
272
 
248
273
  Tab-complete subcommands and workspace names. `nemus completion <shell>` prints
@@ -286,9 +311,25 @@ nemus snapshot restore <id> # (sr)
286
311
  nemus reflect # (retro) analyze your last 10 workspaces' sessions
287
312
  nemus reflect --limit 5 # narrow the window
288
313
  nemus reflect --json # structured report for tooling
314
+ nemus reflect --markdown # Markdown report (grouped by severity) to paste/save
315
+ nemus reflect --group-by kind # group recommendations by kind instead of priority
289
316
  nemus reflect --dry-run # show what the judge sees, without calling the agent
290
317
  ```
291
318
 
319
+ `--markdown` writes a clean, severity-grouped report to stdout — pipe it into a
320
+ file or an issue: `nemus reflect --markdown > reflection.md`. `--group-by
321
+ kind|priority` (default `priority`) controls how recommendations are grouped in
322
+ both the human and Markdown output.
323
+
324
+ Every run is saved under `~/.nemus/reflect/`. Review past reports without
325
+ re-running the judge:
326
+
327
+ ```bash
328
+ nemus reflect history # list saved reports, newest first (--json)
329
+ nemus reflect show # print the latest saved report
330
+ nemus reflect show <id> # a specific one (--markdown / --json / --group-by)
331
+ ```
332
+
292
333
  `reflect` reads your recent agent **session transcripts** (Claude + pi), distills
293
334
  the prompts you sent, the failures the agent hit, and the tools it used, then asks
294
335
  **your own configured agent** (LLM-as-a-judge — no extra API key) to recommend
@@ -319,6 +360,20 @@ repos, create/update workspaces, check status, and more directly from a prompt.
319
360
 
320
361
  Run `nemus --help` for the full command reference.
321
362
 
363
+ ## Cloud (optional, self-hosted)
364
+
365
+ Nemus is **local-first** — everything above runs entirely on your machine. If you
366
+ want to hand a task to an agent that runs **headlessly on infrastructure you own**
367
+ (local Docker, AWS Fargate, or any Kubernetes cluster) and opens a PR for you,
368
+ there's an **optional, opt-in** package: [`@nemus-cli/cloud`](./packages/cloud).
369
+
370
+ It's a separate, vendor-neutral package built from swappable seams — runners
371
+ (`docker`, `aws-fargate`, `kubernetes`), IaC provisioners (OpenTofu/Terraform
372
+ modules), git forges (GitHub/GitLab), a bounded CI-fix loop, and notifiers — with
373
+ no cloud SDK in the core CLI. It is **not published to npm**; it lives in this
374
+ repo for you to build and run yourself. See
375
+ [`packages/cloud/README.md`](./packages/cloud/README.md) to get started.
376
+
322
377
  ## Configuration
323
378
 
324
379
  `nemus configure` writes `~/.nemus/config.json`. Environment
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerConfigCommand = registerConfigCommand;
4
+ const config_1 = require("../utils/config");
5
+ const config_schema_1 = require("../utils/config-schema");
6
+ const output_1 = require("../utils/output");
7
+ const logger_1 = require("../utils/logger");
8
+ const colors_1 = require("../utils/colors");
9
+ /**
10
+ * Non-interactive config management: `nemus config get/set/unset/list/path`.
11
+ * Complements the interactive `configure` wizard and is script-friendly —
12
+ * `get`/`list` write DATA to stdout (raw value, or JSON with --json), logs go to
13
+ * stderr. Values are validated/coerced against config-schema.ts.
14
+ */
15
+ function registerConfigCommand(parent) {
16
+ const config = parent.command('config').description('Get or set Nemus configuration');
17
+ config
18
+ .command('get')
19
+ .description('Print a config value (or all values with no key)')
20
+ .argument('[key]', 'Config key')
21
+ .option('--json', 'Output as JSON')
22
+ .action((key, opts) => {
23
+ const cfg = (0, config_1.getUserConfig)();
24
+ if (key === undefined) {
25
+ printAll(cfg, opts.json);
26
+ return;
27
+ }
28
+ if (!(0, config_schema_1.isConfigKey)(key)) {
29
+ if (opts.json)
30
+ (0, output_1.outputJsonError)(`Unknown config key "${key}"`);
31
+ else
32
+ (0, logger_1.logError)(`Unknown config key "${key}". Run "nemus config list" to see valid keys.`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ const value = cfg[key];
37
+ if (opts.json)
38
+ (0, output_1.outputJson)({ key, value });
39
+ else
40
+ process.stdout.write((0, config_schema_1.formatConfigValue)(value) + '\n');
41
+ });
42
+ config
43
+ .command('set')
44
+ .description('Set a config value')
45
+ .argument('<key>', 'Config key')
46
+ .argument('<value>', 'New value')
47
+ .option('--json', 'Output as JSON')
48
+ .action((key, value, opts) => {
49
+ const result = (0, config_schema_1.applyConfigSet)((0, config_1.getUserConfig)(), key, value);
50
+ if (!result.ok) {
51
+ if (opts.json)
52
+ (0, output_1.outputJsonError)(result.error);
53
+ else
54
+ (0, logger_1.logError)(result.error);
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+ (0, config_1.saveUserConfig)(result.next);
59
+ if (opts.json)
60
+ (0, output_1.outputJson)({ ok: true, key, value: result.value });
61
+ else
62
+ (0, logger_1.logSuccess)(`Set ${(0, colors_1.colorize)(key, 'cyan')} = ${(0, config_schema_1.formatConfigValue)(result.value)}`);
63
+ });
64
+ config
65
+ .command('unset')
66
+ .description('Reset a config value to its default')
67
+ .argument('<key>', 'Config key')
68
+ .option('--json', 'Output as JSON')
69
+ .action((key, opts) => {
70
+ const result = (0, config_schema_1.applyConfigUnset)((0, config_1.getUserConfig)(), key);
71
+ if (!result.ok) {
72
+ if (opts.json)
73
+ (0, output_1.outputJsonError)(result.error);
74
+ else
75
+ (0, logger_1.logError)(result.error);
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ (0, config_1.saveUserConfig)(result.next);
80
+ if (opts.json)
81
+ (0, output_1.outputJson)({ ok: true, key, value: result.value });
82
+ else
83
+ (0, logger_1.logSuccess)(`Reset ${(0, colors_1.colorize)(key, 'cyan')} to default (${(0, config_schema_1.formatConfigValue)(result.value)})`);
84
+ });
85
+ config
86
+ .command('list')
87
+ .alias('ls')
88
+ .description('List all config keys and current values')
89
+ .option('--json', 'Output as JSON')
90
+ .action((opts) => printAll((0, config_1.getUserConfig)(), opts.json));
91
+ config
92
+ .command('path')
93
+ .description('Print the path to the config file')
94
+ .action(() => {
95
+ process.stdout.write(config_1.CONFIG_PATH + '\n');
96
+ });
97
+ }
98
+ function printAll(cfg, json) {
99
+ if (json) {
100
+ const values = {};
101
+ for (const key of config_schema_1.CONFIG_KEYS)
102
+ values[key] = cfg[key];
103
+ (0, output_1.outputJson)({ path: config_1.CONFIG_PATH, values });
104
+ return;
105
+ }
106
+ const width = Math.max(...config_schema_1.CONFIG_KEYS.map((k) => k.length));
107
+ console.log((0, colors_1.colorize)('Nemus configuration', 'bright') + (0, colors_1.colorize)(` (${config_1.CONFIG_PATH})`, 'dim'));
108
+ for (const key of config_schema_1.CONFIG_KEYS) {
109
+ const val = (0, config_schema_1.formatConfigValue)(cfg[key]);
110
+ const shown = val === '' ? (0, colors_1.colorize)('(empty)', 'dim') : val;
111
+ console.log(` ${key.padEnd(width)} ${shown} ${(0, colors_1.colorize)(config_schema_1.CONFIG_SCHEMA[key].describe, 'dim')}`);
112
+ }
113
+ }
@@ -11,22 +11,55 @@ function registerReflectCommand(parent) {
11
11
  parent
12
12
  .command('reflect')
13
13
  .alias('retro')
14
- .description('Analyze your recent workspace sessions and suggest skill/prompt/context improvements (LLM-as-a-judge)')
14
+ .description('Analyze recent sessions for improvements, or review saved reports ("history"/"show")')
15
+ .argument('[subcommand]', '"history" or "show" — omit to run a new analysis')
16
+ .argument('[id]', 'report id when using "show" (default: latest)')
15
17
  .option('-n, --limit <n>', 'How many recent workspaces to analyze', '10')
16
18
  .option('-w, --workspace <name>', 'Analyze a single workspace by name (ignores --limit)')
17
19
  .option('--model <model>', 'Judge model override (agent-native pattern/id)')
18
20
  .option('--thinking <level>', 'Judge thinking level for pi: off|minimal|low|medium|high|xhigh|max')
19
21
  .option('--json', 'Output the report as JSON')
22
+ .option('--markdown', 'Output the report as Markdown (paste into an issue/PR)')
23
+ .option('--group-by <how>', 'Group recommendations by: priority | kind', 'priority')
20
24
  .option('--no-save', 'Do not save the report to ~/.nemus/reflect/')
21
25
  .option('--dry-run', 'Print the assembled corpus + judge prompt without calling the agent')
22
- .action(async (opts) => {
23
- await handleReflect(opts);
26
+ .addHelpText('after', '\nSaved reports:\n nemus reflect history List saved reports (newest first)\n nemus reflect show [id] Print a saved report (default: latest)\n')
27
+ // history/show are positional (not commander subcommands) on purpose: as
28
+ // subcommands they would share --json/--markdown/--group-by with this parent
29
+ // command, and the only commander fix (enablePositionalOptions on the root)
30
+ // breaks global flags placed after a subcommand (e.g. `nemus list --quiet`).
31
+ .action(async (subcommand, id, opts) => {
32
+ if (subcommand === 'history')
33
+ return handleHistory(opts);
34
+ if (subcommand === 'show')
35
+ return handleShow(id, opts);
36
+ if (subcommand !== undefined) {
37
+ const msg = `Unknown reflect subcommand "${subcommand}" (expected "history" or "show").`;
38
+ if (opts.json)
39
+ (0, output_1.outputJsonError)(msg);
40
+ else
41
+ (0, logger_1.logError)(msg);
42
+ process.exit(1);
43
+ }
44
+ return handleReflect(opts);
24
45
  });
25
46
  }
47
+ /** Validate --group-by; returns the value or exits non-zero with a clear error. */
48
+ function resolveGroupBy(raw, json) {
49
+ if (raw === undefined || raw === 'priority' || raw === 'kind')
50
+ return (raw ?? 'priority');
51
+ const msg = `--group-by must be "priority" or "kind"; got "${raw}"`;
52
+ if (json)
53
+ (0, output_1.outputJsonError)(msg);
54
+ else
55
+ (0, logger_1.logError)(msg);
56
+ process.exit(1);
57
+ }
26
58
  async function handleReflect(opts) {
27
59
  const limit = Math.max(1, Number.parseInt(opts.limit ?? '10', 10) || 10);
60
+ const groupBy = resolveGroupBy(opts.groupBy, opts.json);
28
61
  try {
29
- const showProgress = !opts.json && !opts.dryRun;
62
+ const showProgress = !opts.json && !opts.markdown && !opts.dryRun;
30
63
  if (showProgress) {
31
64
  (0, logger_1.logStep)(opts.workspace
32
65
  ? `Analyzing workspace ${(0, colors_1.colorize)(opts.workspace, 'cyan')}…`
@@ -66,7 +99,7 @@ async function handleReflect(opts) {
66
99
  const timeoutMs = Number.parseInt(process.env.NEMUS_JUDGE_TIMEOUT_MS ?? '', 10) || undefined;
67
100
  const model = opts.model ?? process.env.NEMUS_JUDGE_MODEL ?? undefined;
68
101
  const thinking = opts.thinking ?? process.env.NEMUS_JUDGE_THINKING ?? undefined;
69
- const stopSpinner = opts.json
102
+ const stopSpinner = opts.json || opts.markdown
70
103
  ? () => { }
71
104
  : startSpinner(`Judging ${withSessions} session(s) with your configured agent (this can take a minute)…`);
72
105
  let parsed;
@@ -95,7 +128,20 @@ async function handleReflect(opts) {
95
128
  (0, output_1.outputJson)({ analyzed: withSessions, workspaces: corpus.workspaces.length, ...report, savedTo });
96
129
  return;
97
130
  }
98
- printReport(report, corpus.workspaces.length, withSessions);
131
+ if (opts.markdown) {
132
+ // DATA channel: markdown to stdout, nothing else (a 'Saved report' note
133
+ // would corrupt a redirected .md file), so surface the path on stderr.
134
+ process.stdout.write((0, reflect_1.renderReportMarkdown)(report, {
135
+ analyzed: withSessions,
136
+ workspaces: corpus.workspaces.length,
137
+ workspace: opts.workspace,
138
+ generatedAt: new Date().toISOString(),
139
+ }, groupBy));
140
+ if (savedTo)
141
+ (0, logger_1.logInfo)(`Saved report to ${(0, colors_1.colorize)(savedTo, 'dim')}`);
142
+ return;
143
+ }
144
+ printReport(report, corpus.workspaces.length, withSessions, groupBy);
99
145
  if (savedTo)
100
146
  (0, logger_1.logInfo)(`Saved report to ${(0, colors_1.colorize)(savedTo, 'dim')}`);
101
147
  }
@@ -164,7 +210,7 @@ function priorityBadge(p) {
164
210
  return (0, colors_1.colorize)('● med', 'yellow');
165
211
  return (0, colors_1.colorize)('● low', 'gray');
166
212
  }
167
- function printReport(report, workspaces, analyzed) {
213
+ function printReport(report, workspaces, analyzed, groupBy = 'priority') {
168
214
  console.log('');
169
215
  console.log((0, colors_1.colorize)(' Reflection', 'bright') + (0, colors_1.colorize)(` (${analyzed} sessions across ${workspaces} workspaces)`, 'dim'));
170
216
  console.log((0, colors_1.colorize)(' ' + '─'.repeat(56), 'dim'));
@@ -175,19 +221,94 @@ function printReport(report, workspaces, analyzed) {
175
221
  console.log('\n ' + (0, colors_1.colorize)('No specific recommendations — looks solid.', 'green') + '\n');
176
222
  return;
177
223
  }
178
- // High priority first.
179
- const order = { high: 0, medium: 1, low: 2 };
180
- const recs = [...report.recommendations].sort((a, b) => order[a.priority] - order[b.priority]);
181
- console.log('');
182
- for (const r of recs) {
183
- const target = r.target ? (0, colors_1.colorize)(` [${r.target}]`, 'cyan') : '';
184
- console.log(` ${priorityBadge(r.priority)} ${(0, colors_1.colorize)(KIND_LABEL[r.kind], 'bright')} ${r.title}${target}`);
185
- if (r.detail)
186
- console.log(` ${r.detail.replace(/\n/g, '\n ')}`);
187
- if (r.example) {
188
- console.log((0, colors_1.colorize)(' example:', 'dim'));
189
- console.log((0, colors_1.colorize)(r.example.replace(/^/gm, ' '), 'dim'));
224
+ console.log('\n ' + (0, colors_1.colorize)((0, reflect_1.severitySummary)(report.recommendations), 'dim'));
225
+ for (const group of (0, reflect_1.groupRecommendations)(report.recommendations, groupBy)) {
226
+ console.log('\n ' + (0, colors_1.colorize)(group.heading, 'bright'));
227
+ for (const r of group.recs) {
228
+ const target = r.target ? (0, colors_1.colorize)(` [${r.target}]`, 'cyan') : '';
229
+ // Under a kind heading show the priority badge; under a priority heading
230
+ // show the kind label (the heading conveys the other axis).
231
+ const lead = groupBy === 'kind' ? priorityBadge(r.priority) : (0, colors_1.colorize)(KIND_LABEL[r.kind], 'bright');
232
+ console.log(` ${lead} ${r.title}${target}`);
233
+ if (r.detail)
234
+ console.log(` ${r.detail.replace(/\n/g, '\n ')}`);
235
+ if (r.example) {
236
+ console.log((0, colors_1.colorize)(' example:', 'dim'));
237
+ console.log((0, colors_1.colorize)(r.example.replace(/^/gm, ' '), 'dim'));
238
+ }
190
239
  }
191
- console.log('');
192
240
  }
241
+ console.log('');
242
+ }
243
+ async function handleHistory(opts) {
244
+ const reports = await (0, reflect_1.listSavedReports)();
245
+ if (opts.json) {
246
+ (0, output_1.outputJson)({
247
+ count: reports.length,
248
+ reports: reports.map((r) => ({
249
+ id: r.id,
250
+ generatedAt: r.generatedAt,
251
+ analyzed: r.analyzed,
252
+ workspaces: r.workspaces,
253
+ workspace: r.workspace,
254
+ recommendations: r.report.recommendations.length,
255
+ severity: (0, reflect_1.severitySummary)(r.report.recommendations),
256
+ })),
257
+ });
258
+ return;
259
+ }
260
+ if (reports.length === 0) {
261
+ (0, logger_1.logInfo)('No saved reflection reports yet. Run `nemus reflect` to create one.');
262
+ return;
263
+ }
264
+ console.log('');
265
+ console.log((0, colors_1.colorize)(' Saved reflections', 'bright') + (0, colors_1.colorize)(` (${reports.length})`, 'dim'));
266
+ console.log((0, colors_1.colorize)(' ' + '─'.repeat(56), 'dim'));
267
+ for (const r of reports) {
268
+ const when = r.generatedAt ? new Date(r.generatedAt).toLocaleString() : r.id;
269
+ const scope = r.workspace ? (0, colors_1.colorize)(` ${r.workspace}`, 'cyan') : (0, colors_1.colorize)(` ${r.analyzed} sessions`, 'dim');
270
+ const sev = r.report.recommendations.length
271
+ ? (0, colors_1.colorize)(` ${(0, reflect_1.severitySummary)(r.report.recommendations)}`, 'dim')
272
+ : (0, colors_1.colorize)(' no recs', 'green');
273
+ console.log(` ${(0, colors_1.colorize)(r.id, 'bright')}${scope}${sev}`);
274
+ console.log((0, colors_1.colorize)(` ${when}`, 'dim'));
275
+ }
276
+ console.log('');
277
+ console.log((0, colors_1.colorize)(' nemus reflect show <id> (or `latest`)', 'dim'));
278
+ }
279
+ async function handleShow(id, opts) {
280
+ const groupBy = resolveGroupBy(opts.groupBy, opts.json);
281
+ const matches = (0, reflect_1.findSavedMatches)(await (0, reflect_1.listSavedReports)(), id);
282
+ const saved = matches[0];
283
+ if (!saved) {
284
+ const msg = id && id !== 'latest'
285
+ ? `No saved report matching "${id}". Run "nemus reflect history" to list them.`
286
+ : 'No saved reflection reports yet. Run "nemus reflect" to create one.';
287
+ if (opts.json)
288
+ (0, output_1.outputJsonError)(msg);
289
+ else
290
+ (0, logger_1.logError)(msg);
291
+ process.exit(1);
292
+ }
293
+ // An id-prefix that matches several reports resolves to the newest — say so
294
+ // (stderr only, so --json/--markdown stdout stays clean) rather than quietly
295
+ // showing a possibly-unintended report. An exact id / "latest" never multi-matches.
296
+ if (matches.length > 1 && !opts.json) {
297
+ (0, logger_1.logWarning)(`"${id}" matched ${matches.length} reports; showing the newest (${saved.id}). Use a longer id to disambiguate.`);
298
+ }
299
+ const meta = {
300
+ analyzed: saved.analyzed,
301
+ workspaces: saved.workspaces,
302
+ workspace: saved.workspace,
303
+ generatedAt: saved.generatedAt,
304
+ };
305
+ if (opts.json) {
306
+ (0, output_1.outputJson)({ id: saved.id, ...meta, ...saved.report });
307
+ return;
308
+ }
309
+ if (opts.markdown) {
310
+ process.stdout.write((0, reflect_1.renderReportMarkdown)(saved.report, meta, groupBy));
311
+ return;
312
+ }
313
+ printReport(saved.report, saved.workspaces, saved.analyzed, groupBy);
193
314
  }
package/dist/program.js CHANGED
@@ -38,24 +38,18 @@ const commander_1 = require("commander");
38
38
  const path = __importStar(require("path"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const colors_1 = require("./utils/colors");
41
+ const banner_1 = require("./utils/banner");
42
+ const global_flags_1 = require("./utils/global-flags");
41
43
  // Read version from package.json
42
44
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
43
- const g = colors_1.colors.green;
44
- const d = colors_1.colors.dim;
45
- const b = colors_1.colors.bright;
46
- const r = colors_1.colors.reset;
47
- const INNER = 38;
48
- const titleLine = `>_ Nemus`;
49
- const titlePad = ' '.repeat(Math.max(0, INNER - 2 - titleLine.length));
50
- const versionLine = `v${pkg.version} · multi-repo workspaces`;
51
- const versionPad = ' '.repeat(Math.max(0, INNER - 7 - versionLine.length));
52
- const bar = '─'.repeat(INNER);
53
- const bannerText = `
54
- ${d} ╭${bar}╮${r}
55
- ${d} │${r} ${g}>_${r} ${b}Nemus${r}${titlePad}${d}│${r}
56
- ${d} │${r} ${d}${versionLine}${r}${versionPad}${d}│${r}
57
- ${d} ╰${bar}╯${r}
58
- `;
45
+ // --no-color must be applied BEFORE commander parses so it reaches the help
46
+ // banner (a preAction hook is too late for help output, and ES imports run
47
+ // before hooks). It's a long flag, so it can't be bundled — an argv scan is
48
+ // sufficient here. colors.ts already applied NO_COLOR / non-TTY at import.
49
+ // --quiet is handled in the preAction hook below (it only affects command
50
+ // logs, never help), which also catches bundled short forms like `-yq`.
51
+ if (process.argv.includes('--no-color'))
52
+ (0, colors_1.setColorEnabled)(false);
59
53
  exports.program = new commander_1.Command();
60
54
  exports.program
61
55
  .name('workspace')
@@ -63,7 +57,14 @@ exports.program
63
57
  .version(pkg.version, '-V, --version')
64
58
  .option('-f, --force-refresh', 'Force refresh GitHub repos (skip cache)')
65
59
  .option('-y, --yes', 'Skip confirmations')
66
- .addHelpText('before', bannerText);
60
+ .option('--no-color', 'Disable colored output (also honors NO_COLOR)')
61
+ .option('-q, --quiet', 'Suppress progress logs (keep warnings + errors)')
62
+ .addHelpText('before', () => (0, banner_1.renderHelpBanner)(pkg.version));
63
+ // Apply global --quiet / --color from commander's PARSED options (robust to
64
+ // bundled short flags like `-yq` that a raw argv scan misses). Runs before every
65
+ // command action; help output doesn't reach here, which is why --no-color is
66
+ // also pre-scanned above.
67
+ exports.program.hook('preAction', () => (0, global_flags_1.applyGlobalFlags)(exports.program.opts()));
67
68
  // Register top-level commands
68
69
  const create_1 = require("./commands/create");
69
70
  const list_1 = require("./commands/list");
@@ -83,6 +84,7 @@ const archive_1 = require("./commands/archive");
83
84
  const sessions_1 = require("./commands/sessions");
84
85
  const generate_docs_1 = require("./commands/generate-docs");
85
86
  const configure_1 = require("./commands/configure");
87
+ const config_1 = require("./commands/config");
86
88
  const configure_claude_1 = require("./commands/configure-claude");
87
89
  const ghq_status_1 = require("./commands/ghq-status");
88
90
  const save_context_1 = require("./commands/save-context");
@@ -108,6 +110,7 @@ const reflect_1 = require("./commands/reflect");
108
110
  (0, sessions_1.registerSessionsCommand)(exports.program);
109
111
  (0, generate_docs_1.registerGenerateDocsCommand)(exports.program);
110
112
  (0, configure_1.registerConfigureCommand)(exports.program);
113
+ (0, config_1.registerConfigCommand)(exports.program);
111
114
  (0, configure_claude_1.registerConfigureClaudeCommand)(exports.program);
112
115
  (0, ghq_status_1.registerGhqStatusCommand)(exports.program);
113
116
  (0, save_context_1.registerSaveContextCommand)(exports.program);
@@ -1,24 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.printBanner = exports.BANNER_PLAIN = exports.BANNER = void 0;
3
+ exports.renderHelpBanner = exports.printBanner = exports.renderBanner = exports.BANNER_PLAIN = void 0;
4
4
  const colors_1 = require("./colors");
5
- const g = colors_1.colors.green;
6
- const d = colors_1.colors.dim;
7
- const b = colors_1.colors.bright;
8
- const r = colors_1.colors.reset;
9
- exports.BANNER = `
10
- ${d} ╭──────────────────────────────────────╮${r}
11
- ${d} │${r} ${g}>_${r} ${b}Nemus${r} ${d}│${r}
12
- ${d} │${r} ${d}multi-repo workspaces${r} ${d}│${r}
13
- ${d} ╰──────────────────────────────────────╯${r}
14
- `;
15
5
  exports.BANNER_PLAIN = `
16
6
  ╭──────────────────────────────────────╮
17
7
  │ >_ Nemus │
18
8
  │ multi-repo workspaces │
19
9
  ╰──────────────────────────────────────╯
20
10
  `;
11
+ // Rendered live (not captured at import) so --no-color / NO_COLOR applied before
12
+ // this is called produce a plain banner. With color off, every colors.* is ''.
13
+ const renderBanner = () => {
14
+ const { green: g, dim: d, bright: b, reset: r } = colors_1.colors;
15
+ return `
16
+ ${d} ╭──────────────────────────────────────╮${r}
17
+ ${d} │${r} ${g}>_${r} ${b}Nemus${r} ${d}│${r}
18
+ ${d} │${r} ${d}multi-repo workspaces${r} ${d}│${r}
19
+ ${d} ╰──────────────────────────────────────╯${r}
20
+ `;
21
+ };
22
+ exports.renderBanner = renderBanner;
21
23
  const printBanner = () => {
22
- console.log(exports.BANNER);
24
+ console.log((0, exports.renderBanner)());
23
25
  };
24
26
  exports.printBanner = printBanner;
27
+ // The `--help` banner: a boxed splash with the version + tagline, width-fitted.
28
+ // Lives here alongside renderBanner() so the two renderers don't drift; also
29
+ // rendered live so --no-color / NO_COLOR yields a plain box.
30
+ const renderHelpBanner = (version) => {
31
+ const { green: g, dim: d, bright: b, reset: r } = colors_1.colors;
32
+ const INNER = 38;
33
+ const titleLine = `>_ Nemus`;
34
+ const titlePad = ' '.repeat(Math.max(0, INNER - 2 - titleLine.length));
35
+ const versionLine = `v${version} · multi-repo workspaces`;
36
+ const versionPad = ' '.repeat(Math.max(0, INNER - 7 - versionLine.length));
37
+ const bar = '─'.repeat(INNER);
38
+ return `
39
+ ${d} ╭${bar}╮${r}
40
+ ${d} │${r} ${g}>_${r} ${b}Nemus${r}${titlePad}${d}│${r}
41
+ ${d} │${r} ${d}${versionLine}${r}${versionPad}${d}│${r}
42
+ ${d} ╰${bar}╯${r}
43
+ `;
44
+ };
45
+ exports.renderHelpBanner = renderHelpBanner;
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.colorize = exports.colors = void 0;
4
- exports.colors = {
3
+ exports.colorize = exports.isColorEnabled = exports.colors = void 0;
4
+ exports.setColorEnabled = setColorEnabled;
5
+ exports.detectColorEnabled = detectColorEnabled;
6
+ // ANSI escape codes. `colors` starts as a copy of these but is emptied in place
7
+ // when color is disabled (--no-color / NO_COLOR / non-TTY), so BOTH `colorize()`
8
+ // and inline `colors.x` template usage go plain without touching call sites.
9
+ const ANSI = {
5
10
  reset: '\x1b[0m',
6
11
  bright: '\x1b[1m',
7
12
  dim: '\x1b[2m',
@@ -20,7 +25,43 @@ exports.colors = {
20
25
  bgYellow: '\x1b[43m',
21
26
  bgBlue: '\x1b[44m',
22
27
  };
28
+ // Live map read at every use. Mutated in place by setColorEnabled so previously
29
+ // imported references (e.g. `colors.gray` inside a template) see the change.
30
+ exports.colors = { ...ANSI };
31
+ let enabled = true;
32
+ /** Whether colored output is currently on. */
33
+ const isColorEnabled = () => enabled;
34
+ exports.isColorEnabled = isColorEnabled;
35
+ /** Turn color on/off. When off, every `colors.*` code becomes '' so output is
36
+ * plain; when on, the ANSI codes are restored. */
37
+ function setColorEnabled(on) {
38
+ enabled = on;
39
+ for (const key of Object.keys(ANSI)) {
40
+ exports.colors[key] = on ? ANSI[key] : '';
41
+ }
42
+ }
43
+ /**
44
+ * Decide whether to use color by default, following the common conventions:
45
+ * - `NO_COLOR` (any value, even empty) disables — see https://no-color.org
46
+ * - `FORCE_COLOR` (and not "0"/"false") forces it on, even when not a TTY
47
+ * - otherwise on only when the stdout stream is a TTY and `TERM` isn't `dumb`
48
+ */
49
+ function detectColorEnabled(env = process.env, isTTY = !!process.stdout.isTTY) {
50
+ if ('NO_COLOR' in env)
51
+ return false;
52
+ const force = env.FORCE_COLOR;
53
+ if (force !== undefined && force !== '' && force !== '0' && force.toLowerCase() !== 'false') {
54
+ return true;
55
+ }
56
+ if (env.TERM === 'dumb')
57
+ return false;
58
+ return isTTY;
59
+ }
23
60
  const colorize = (text, color) => {
24
61
  return `${exports.colors[color]}${text}${exports.colors.reset}`;
25
62
  };
26
63
  exports.colorize = colorize;
64
+ // Apply the environment default at import time so NO_COLOR / a non-TTY pipe is
65
+ // respected even before any CLI flag is parsed (an explicit --no-color / --color
66
+ // flag overrides this later).
67
+ setColorEnabled(detectColorEnabled());