@nemus-cli/nemus 0.2.9 → 0.2.11
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 +84 -0
- package/README.md +68 -17
- package/dist/cli/ai-prompt.js +2 -2
- package/dist/commands/configure.js +2 -2
- package/dist/commands/delete.js +75 -42
- package/dist/commands/doctor.js +35 -7
- package/dist/commands/list.js +24 -1
- package/dist/commands/status.js +37 -8
- package/dist/mcp/install.js +2 -2
- package/dist/utils/branch-operations.js +22 -34
- package/dist/utils/cleanup-operations.js +5 -5
- package/dist/utils/ghq-integration.js +29 -19
- package/dist/utils/git-operations.js +2 -3
- package/dist/utils/health-checks.js +2 -1
- package/dist/utils/logger.js +10 -6
- package/dist/utils/output.js +23 -0
- package/package.json +4 -1
- package/src/cli/ai-prompt.test.ts +5 -1
- package/src/cli/ai-prompt.ts +3 -3
- package/src/commands/configure.ts +3 -3
- package/src/commands/delete.ts +75 -41
- package/src/commands/doctor.ts +32 -7
- package/src/commands/list.test.ts +30 -0
- package/src/commands/list.ts +26 -2
- package/src/commands/status.ts +35 -8
- package/src/mcp/install.ts +5 -4
- package/src/utils/branch-operations.test.ts +41 -0
- package/src/utils/branch-operations.ts +26 -38
- package/src/utils/cleanup-operations.ts +6 -6
- package/src/utils/ghq-integration.test.ts +29 -5
- package/src/utils/ghq-integration.ts +31 -32
- package/src/utils/git-operations.ts +3 -4
- package/src/utils/health-checks.ts +3 -2
- package/src/utils/logger.ts +11 -6
- package/src/utils/output.test.ts +36 -0
- package/src/utils/output.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.11] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`--json` output** for the read-only reporting commands `list`, `status`, and
|
|
15
|
+
`doctor`. Emits exactly one JSON document to stdout (no table, no interactive
|
|
16
|
+
prompt), so `nemus list --json | jq …` and CI/scripts get stable,
|
|
17
|
+
machine-readable output. `status --json` / `doctor --json` require an explicit
|
|
18
|
+
workspace name (they never prompt).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **stdout/stderr hygiene.** Diagnostic logs (info/success/error/warning/step)
|
|
23
|
+
now go to **stderr**, leaving **stdout** for a command's actual data. This is
|
|
24
|
+
what makes `--json` pipe cleanly and lets non-TTY consumers separate data from
|
|
25
|
+
progress. Human-facing table/plain output is unchanged on stdout.
|
|
26
|
+
|
|
27
|
+
## [0.2.10] - 2026-08-27
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- **Eliminated the shell-injection surface in git/shell operations.** Every
|
|
32
|
+
`exec`/`execSync` call that interpolated a value into a shell string was
|
|
33
|
+
migrated to `execFile`/argv (no shell). Most notably, the MCP `branch_create`
|
|
34
|
+
and `switch_branch` tools built `git checkout -b <name>` by unquoted
|
|
35
|
+
interpolation with no branch-name validation, so a crafted (but valid) git ref
|
|
36
|
+
could run arbitrary commands; branch names are now passed as argv elements and
|
|
37
|
+
can never be interpreted by a shell. Also migrated `ghq`/clone, workspace
|
|
38
|
+
cleanup (`du`, `git clean`), disk-usage health checks, agent-availability
|
|
39
|
+
probing, and the shell-integration/MCP installers.
|
|
40
|
+
- **`w delete` no longer deletes arbitrary directories.** The `--workspace` flag
|
|
41
|
+
path validated names through `safeWorkspacePath()` (allowlist + containment)
|
|
42
|
+
and skips unknown/invalid names instead of `fs.rm`-ing a guessed path; the
|
|
43
|
+
interactive path is guarded the same way.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **Monorepo (npm workspaces).** The repo is now an npm-workspaces monorepo:
|
|
48
|
+
the published CLI stays at the root (`@nemus-cli/nemus`, unchanged) and new
|
|
49
|
+
packages live under `packages/*`.
|
|
50
|
+
- **`@nemus-cli/cloud` (private, P1).** An optional, vendor-neutral cloud/IaC
|
|
51
|
+
package — not required for local Nemus. Two seams so far: the **forge-auth**
|
|
52
|
+
seam (`ForgeTokenSource`) with `pat` and a dependency-free `github-app` source
|
|
53
|
+
that mints least-privilege, auto-refreshing installation tokens; and the
|
|
54
|
+
**execution** seam (`Runner`/`Provisioner` + neutral `TaskSpec`/
|
|
55
|
+
`TargetDescriptor`/`Capabilities` + a name registry) with an in-box
|
|
56
|
+
**Docker runner** (needs no cloud account); a **`SecretSource`** seam
|
|
57
|
+
(`env`/`dotenv`/`gh` + `resolveSecretsToEnv`); and a **`GitForge`** seam
|
|
58
|
+
(`openPR`/`getChecks`/`comment`) with a dependency-free GitHub implementation;
|
|
59
|
+
and the **in-image agent orchestrator** (runner-image env contract, versioned
|
|
60
|
+
`result.json` schema, and `runAgentTask`: clone all → run the agent once over
|
|
61
|
+
the workspace → open a PR per changed repo, with per-repo error isolation);
|
|
62
|
+
the **OCI image + `nemus-cloud-agent` entrypoint** (a Dockerfile with node +
|
|
63
|
+
git + gh + pi/claude); and the **provisioning** seam (P2, in progress): a
|
|
64
|
+
generic `OpenTofuProvisioner` (delegates to `tofu`/`terraform`, maps a module's
|
|
65
|
+
`target` output → `TargetDescriptor`) + registry, plus a first **AWS Fargate**
|
|
66
|
+
OpenTofu module (`iac/fargate/`, validated with real `tofu validate`); and the
|
|
67
|
+
matching **`aws-fargate` Runner** (dependency-free, shells the `aws` CLI:
|
|
68
|
+
`register-task-definition` → `run-task`, `describe-tasks` → status, CloudWatch
|
|
69
|
+
`logs tail` streaming, `stop-task`); and the **`nemus-cloud` CLI** (own bin,
|
|
70
|
+
dependency-free) — `up`/`down` drive the provisioner, `run` launches the agent
|
|
71
|
+
image on a target (`--follow` logs, `--wait` for the exit code). This
|
|
72
|
+
completes P2. **P3 (in progress):** a bounded **CI-loop** (`runCiLoop`) that
|
|
73
|
+
drives a PR to green over `GitForge` — poll checks, run a fix pass on failure,
|
|
74
|
+
commit/push, repeat; anti-runaway guards (max iterations, no-change → stuck,
|
|
75
|
+
poll-budget → timeout) + a best-effort "needs a human" give-up comment; and
|
|
76
|
+
(P4) a vendor-neutral **notification seam** — `Notifier` with Slack (incoming
|
|
77
|
+
webhook) + generic webhook sinks (`notifierFromEnv`), wired into the CI-loop as
|
|
78
|
+
optional out-of-band report-back.
|
|
79
|
+
**Code-host breadth:** the `GitForge` seam gained a dependency-free **GitLab**
|
|
80
|
+
implementation (`GitLabForge` — merge requests, commit statuses, MR notes;
|
|
81
|
+
self-managed via `GITLAB_API_URL`) alongside GitHub, plus a **forge registry**
|
|
82
|
+
(`createForge`/`registerForge`/`registeredForges` + `NEMUS_FORGE_HOST`) so a
|
|
83
|
+
run targets GitHub or GitLab — or a custom "bring your own backend" host
|
|
84
|
+
(Gitea, Bitbucket, …) — with no code change. Docs in `packages/cloud/README.md`.
|
|
85
|
+
**CI-loop + notifier wired into the agent image:** a second container entry
|
|
86
|
+
mode `NEMUS_MODE=fix-pr` (`runFixPr`/`parseFixPrEnv`, CLI `nemus-cloud fix-pr
|
|
87
|
+
--repo --pr --branch`) drives an *existing* PR to green with the bounded
|
|
88
|
+
CI-loop (P3) + optional Slack/webhook notifications (P4) — clone, checkout PR
|
|
89
|
+
head, `runCiLoop`, then write the same versioned `result.json` with
|
|
90
|
+
`mode: 'fix-pr'` + a compact `ci` summary. So P3+P4 are usable end-to-end in a
|
|
91
|
+
container, on GitHub or GitLab.
|
|
92
|
+
Design: `docs/plans/2026-08-26-cloud-iac.md`.
|
|
93
|
+
|
|
10
94
|
## [0.2.9] - 2026-08-26
|
|
11
95
|
|
|
12
96
|
### Fixed
|
package/README.md
CHANGED
|
@@ -3,46 +3,82 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<b>
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
<b>Work on many Git repos as if they were one project.</b><br/>
|
|
7
|
+
Nemus pulls the repos you're working on into a single folder, runs commands across
|
|
8
|
+
all of them at once, and gives your AI coding agent the whole picture.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
+
<a href="https://me-public.github.io/nemus/"><img src="https://img.shields.io/badge/website-nemus-3FB950.svg" alt="Website" /></a>
|
|
12
13
|
<a href="https://github.com/me-public/nemus/actions/workflows/ci.yml"><img src="https://github.com/me-public/nemus/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
13
14
|
<a href="https://www.npmjs.com/package/@nemus-cli/nemus"><img src="https://img.shields.io/npm/v/@nemus-cli/nemus.svg" alt="npm" /></a>
|
|
14
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
|
|
15
16
|
<a href="https://www.npmjs.com/package/@nemus-cli/nemus"><img src="https://img.shields.io/npm/dm/@nemus-cli/nemus.svg" alt="npm downloads" /></a>
|
|
16
17
|
<img src="https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg" alt="Node >= 22" />
|
|
18
|
+
<a href="https://x.com/nemus_cli"><img src="https://img.shields.io/badge/follow-%40nemus__cli-000000.svg?logo=x&logoColor=white" alt="Follow @nemus_cli on X" /></a>
|
|
17
19
|
</p>
|
|
18
20
|
|
|
19
21
|
<p align="center">
|
|
20
|
-
📦 <b><a href="https://www.npmjs.com/package/@nemus-cli/nemus">@nemus-cli/nemus</a></b> on npm — <code>npm install -g @nemus-cli/nemus</code>
|
|
22
|
+
🌐 <b><a href="https://me-public.github.io/nemus/">me-public.github.io/nemus</a></b> — 📦 <b><a href="https://www.npmjs.com/package/@nemus-cli/nemus">@nemus-cli/nemus</a></b> on npm — 𝕏 <b><a href="https://x.com/nemus_cli">@nemus_cli</a></b> — <code>npm install -g @nemus-cli/nemus</code>
|
|
21
23
|
</p>
|
|
22
24
|
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="https://raw.githubusercontent.com/me-public/nemus/main/docs/assets/nemus-cli-demo.mp4"><img src="https://raw.githubusercontent.com/me-public/nemus/main/docs/assets/nemus-cli-demo.gif" alt="Nemus terminal demo — describe a workspace in plain English, Nemus clones the repos and writes the agent context, then run across every repo at once" width="820" /></a>
|
|
27
|
+
</p>
|
|
28
|
+
<p align="center"><sub>A real recording — <code>nemus -- "…"</code> turns a sentence into a ready workspace. <a href="https://raw.githubusercontent.com/me-public/nemus/main/docs/assets/nemus-cli-demo.mp4">▶ watch as MP4</a></sub></p>
|
|
29
|
+
|
|
23
30
|
---
|
|
24
31
|
|
|
25
|
-
##
|
|
32
|
+
## What is Nemus?
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
Your product's code is probably split across **many separate Git repos** — frontend,
|
|
35
|
+
backend, auth, payments, shared libraries. Working on one feature means cloning,
|
|
36
|
+
pulling, branching, and testing each repo by hand, one at a time — and your AI coding
|
|
37
|
+
agent only ever sees the single repo you happened to open.
|
|
38
|
+
|
|
39
|
+
**Nemus lets you pull the repos you're working on into one folder and treat them as a
|
|
40
|
+
single project:**
|
|
41
|
+
|
|
42
|
+
- **One command clones them all** — and keeps them in sync.
|
|
43
|
+
- **Run any git or shell command across every repo at once** — `status`, `pull`,
|
|
44
|
+
`branch`, `npm test`, anything.
|
|
45
|
+
- **Your AI agent sees the whole set at once** — so a prompt like *"add idempotency
|
|
46
|
+
keys to the payments flow"* works even when that flow spans five repos, not one.
|
|
47
|
+
|
|
48
|
+
> **In short:** a **monorepo-style workflow across your many repos — without merging them.**
|
|
49
|
+
|
|
50
|
+
### Before vs. after
|
|
51
|
+
|
|
52
|
+
**Without Nemus** — clone each repo by hand, `cd` into every one, pull, branch, run
|
|
53
|
+
tests, and repeat. Your agent only sees whichever repo you opened.
|
|
54
|
+
|
|
55
|
+
**With Nemus:**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
nemus create -w payments -r api,web,ledger,gateway,workers # clone all 5 into one workspace
|
|
59
|
+
nemus run payments "npm test" # run the tests in all 5 at once
|
|
60
|
+
nemus -- "add idempotency keys to the payments flow" # let an agent work across all 5
|
|
61
|
+
```
|
|
31
62
|
|
|
32
|
-
|
|
63
|
+
> Prefer to point-and-pick? Just run `nemus create` for an interactive repo picker with
|
|
64
|
+
> fuzzy search, or `nemus -- "create a workspace with the payments repos"` in plain English.
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
of related repos — in sync and lets you act across all of them at once: clone, pull,
|
|
36
|
-
branch, run commands, check status, and diff. Then it connects that workspace to a
|
|
37
|
-
coding agent (Claude Code, pi, OpenCode, Codex, Gemini) with generated context files,
|
|
38
|
-
skills, and an MCP server, so "work on the payments stack" becomes a single prompt.
|
|
66
|
+
## What you get
|
|
39
67
|
|
|
40
|
-
- 🌳 **Workspaces** — group repos, clone them all, jump in.
|
|
68
|
+
- 🌳 **Workspaces** — group repos, clone them all in parallel, jump in.
|
|
41
69
|
- 🔁 **Operate in bulk** — status, sync, diff, run, branch across every repo.
|
|
42
70
|
- 📦 **Suites & snapshots** — save reusable repo collections and exact states.
|
|
43
|
-
- 🤖 **Agent-native** — first-class integration with multiple coding agents
|
|
71
|
+
- 🤖 **Agent-native** — first-class integration with multiple coding agents
|
|
72
|
+
(Claude Code, pi, OpenCode, Codex, Gemini) via context files, skills, and an MCP server.
|
|
44
73
|
- 🩺 **Healthy by default** — health checks, dependency analysis, retries.
|
|
45
74
|
|
|
75
|
+
## The name
|
|
76
|
+
|
|
77
|
+
**Nemus** (_NEH-mus_) is Latin for a **grove** — a small wood of trees sharing
|
|
78
|
+
soil and roots. It's a fitting picture of what the tool manages: a cluster of
|
|
79
|
+
repositories, each its own tree, growing together in one workspace. It also nods
|
|
80
|
+
to the branch-and-commit shape of Git itself.
|
|
81
|
+
|
|
46
82
|
## A full clone per workspace — on purpose
|
|
47
83
|
|
|
48
84
|
This is the core design decision, so it's worth being explicit: **every workspace
|
|
@@ -187,6 +223,21 @@ web feature/x ⚠ 2 modified ↑1 2 files
|
|
|
187
223
|
shared-lib main ✓ Clean ↓3 -
|
|
188
224
|
```
|
|
189
225
|
|
|
226
|
+
### Scripting: `--json`
|
|
227
|
+
|
|
228
|
+
`list`, `status`, and `doctor` accept `--json` for stable, machine-readable
|
|
229
|
+
output. Diagnostics go to stderr, so stdout is a single JSON document you can
|
|
230
|
+
pipe straight into `jq` or a CI step:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
nemus list --json | jq -r '.workspaces[].name'
|
|
234
|
+
nemus status my-workspace --json | jq '.clean'
|
|
235
|
+
nemus doctor my-workspace --json | jq '.score'
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`status`/`doctor` with `--json` need an explicit workspace name (they never
|
|
239
|
+
prompt).
|
|
240
|
+
|
|
190
241
|
### Suites (reusable repo collections)
|
|
191
242
|
|
|
192
243
|
```bash
|
package/dist/cli/ai-prompt.js
CHANGED
|
@@ -50,7 +50,7 @@ const logger_1 = require("../utils/logger");
|
|
|
50
50
|
const colors_1 = require("../utils/colors");
|
|
51
51
|
const agent_config_1 = require("../utils/agent-config");
|
|
52
52
|
const validation_1 = require("../utils/validation");
|
|
53
|
-
const
|
|
53
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
54
54
|
exports.AI_PROMPT_FILE = path.join(os.homedir(), '.workspace-ai-prompt');
|
|
55
55
|
const EXTRACT_SCHEMA = JSON.stringify({
|
|
56
56
|
type: 'object',
|
|
@@ -129,7 +129,7 @@ function buildInvestigationPreamble(workspaceName, task, opts = {}) {
|
|
|
129
129
|
async function isPrimaryAgentAvailable() {
|
|
130
130
|
const agent = (0, agent_config_1.getPrimaryAgent)();
|
|
131
131
|
try {
|
|
132
|
-
await
|
|
132
|
+
await execFileAsync('which', [agent.launchCommand]);
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
135
|
catch {
|
|
@@ -148,7 +148,7 @@ async function handleConfigure() {
|
|
|
148
148
|
(0, logger_1.logInfo)('Installing MCP server...');
|
|
149
149
|
try {
|
|
150
150
|
const mcpInstallScript = path.join(__dirname, '..', '..', 'dist', 'mcp', 'install.js');
|
|
151
|
-
(0, child_process_1.
|
|
151
|
+
(0, child_process_1.execFileSync)('node', [mcpInstallScript, 'install'], { stdio: 'inherit' });
|
|
152
152
|
mcpInstalled = true;
|
|
153
153
|
}
|
|
154
154
|
catch {
|
|
@@ -198,7 +198,7 @@ function installShellIntegration() {
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
try {
|
|
201
|
-
(0, child_process_1.
|
|
201
|
+
(0, child_process_1.execFileSync)('bash', [scriptPath, shellType], { stdio: 'inherit' });
|
|
202
202
|
}
|
|
203
203
|
catch {
|
|
204
204
|
(0, logger_1.logWarning)('Shell integration install failed — you can run it manually:');
|
package/dist/commands/delete.js
CHANGED
|
@@ -39,8 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.registerDeleteCommand = registerDeleteCommand;
|
|
40
40
|
exports.main = main;
|
|
41
41
|
const fs = __importStar(require("fs/promises"));
|
|
42
|
-
const
|
|
43
|
-
const path = __importStar(require("path"));
|
|
42
|
+
const validation_1 = require("../utils/validation");
|
|
44
43
|
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
45
44
|
const prompts_1 = require("../utils/prompts");
|
|
46
45
|
const logger_1 = require("../utils/logger");
|
|
@@ -67,9 +66,33 @@ async function handleDelete(opts) {
|
|
|
67
66
|
if (opts.workspace) {
|
|
68
67
|
const selectedNames = (0, command_helpers_1.parseList)(opts.workspace);
|
|
69
68
|
const workspaces = await (0, workspace_meta_1.listWorkspaces)();
|
|
69
|
+
const known = new Map(workspaces.map(ws => [ws.name, ws]));
|
|
70
|
+
// Resolve to validated, existing targets. safeWorkspacePath() both
|
|
71
|
+
// enforces the name allowlist and pins the path inside WORKSPACES_DIR, so
|
|
72
|
+
// a name like "../../etc" can never reach fs.rm; unknown names are skipped
|
|
73
|
+
// rather than deleted at a guessed path.
|
|
74
|
+
const targets = [];
|
|
70
75
|
for (const name of selectedNames) {
|
|
71
|
-
const workspace =
|
|
72
|
-
|
|
76
|
+
const workspace = known.get(name);
|
|
77
|
+
if (!workspace) {
|
|
78
|
+
(0, logger_1.logError)(`Workspace "${name}" not found — skipping`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let workspacePath;
|
|
82
|
+
try {
|
|
83
|
+
workspacePath = (0, validation_1.safeWorkspacePath)(name);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
(0, logger_1.logError)(error instanceof Error ? error.message : `Invalid workspace name "${name}"`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
targets.push({ name, path: workspacePath, workspace });
|
|
90
|
+
}
|
|
91
|
+
if (targets.length === 0) {
|
|
92
|
+
(0, logger_1.logInfo)('Nothing to delete');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const { name, path: workspacePath, workspace } of targets) {
|
|
73
96
|
if (workspace?.metadata) {
|
|
74
97
|
console.log(`${(0, colors_1.colorize)(name, 'cyan')}`);
|
|
75
98
|
console.log(` Repositories: ${workspace.metadata.repositories.length}`);
|
|
@@ -86,9 +109,9 @@ async function handleDelete(opts) {
|
|
|
86
109
|
{
|
|
87
110
|
type: 'confirm',
|
|
88
111
|
name: 'confirmed',
|
|
89
|
-
message:
|
|
90
|
-
? `Delete workspace ${
|
|
91
|
-
: `Delete these ${
|
|
112
|
+
message: targets.length === 1
|
|
113
|
+
? `Delete workspace ${targets[0].name}?`
|
|
114
|
+
: `Delete these ${targets.length} workspaces?`,
|
|
92
115
|
default: true,
|
|
93
116
|
},
|
|
94
117
|
]);
|
|
@@ -97,8 +120,7 @@ async function handleDelete(opts) {
|
|
|
97
120
|
process.exit(0);
|
|
98
121
|
}
|
|
99
122
|
}
|
|
100
|
-
for (const name of
|
|
101
|
-
const workspacePath = path.join(config_1.WORKSPACES_DIR, name);
|
|
123
|
+
for (const { name, path: workspacePath } of targets) {
|
|
102
124
|
try {
|
|
103
125
|
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
104
126
|
(0, logger_1.logSuccess)(`Deleted "${(0, colors_1.colorize)(name, 'cyan')}"`);
|
|
@@ -119,44 +141,55 @@ async function handleDelete(opts) {
|
|
|
119
141
|
break;
|
|
120
142
|
}
|
|
121
143
|
const selectedNames = await (0, prompts_1.promptMultiWorkspaceSelection)(workspaces);
|
|
144
|
+
// Resolve + validate paths once. Names come from disk, but safeWorkspacePath
|
|
145
|
+
// must not throw mid-flow and crash the interactive session, so skip any
|
|
146
|
+
// name that fails the allowlist rather than aborting.
|
|
147
|
+
const resolved = [];
|
|
122
148
|
for (const name of selectedNames) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (workspace?.metadata) {
|
|
126
|
-
console.log(`${(0, colors_1.colorize)(name, 'cyan')}`);
|
|
127
|
-
console.log(` Repositories: ${workspace.metadata.repositories.length}`);
|
|
128
|
-
console.log(` Created: ${new Date(workspace.metadata.createdAt).toLocaleString()}`);
|
|
129
|
-
console.log(` Path: ${workspacePath}`);
|
|
149
|
+
try {
|
|
150
|
+
resolved.push({ name, path: (0, validation_1.safeWorkspacePath)(name), workspace: workspaces.find(ws => ws.name === name) });
|
|
130
151
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
console.log(` Path: ${workspacePath}`);
|
|
152
|
+
catch (error) {
|
|
153
|
+
(0, logger_1.logError)(error instanceof Error ? error.message : `Invalid workspace name "${name}"`);
|
|
134
154
|
}
|
|
135
155
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
},
|
|
148
|
-
]);
|
|
149
|
-
if (confirmed) {
|
|
150
|
-
for (const name of selectedNames) {
|
|
151
|
-
const workspacePath = path.join(config_1.WORKSPACES_DIR, name);
|
|
152
|
-
try {
|
|
153
|
-
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
154
|
-
(0, logger_1.logSuccess)(`Deleted "${(0, colors_1.colorize)(name, 'cyan')}"`);
|
|
156
|
+
if (resolved.length > 0) {
|
|
157
|
+
for (const { name, path: workspacePath, workspace } of resolved) {
|
|
158
|
+
if (workspace?.metadata) {
|
|
159
|
+
console.log(`${(0, colors_1.colorize)(name, 'cyan')}`);
|
|
160
|
+
console.log(` Repositories: ${workspace.metadata.repositories.length}`);
|
|
161
|
+
console.log(` Created: ${new Date(workspace.metadata.createdAt).toLocaleString()}`);
|
|
162
|
+
console.log(` Path: ${workspacePath}`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.log(`${(0, colors_1.colorize)(name, 'cyan')}`);
|
|
166
|
+
console.log(` Path: ${workspacePath}`);
|
|
155
167
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
170
|
+
(0, logger_1.logWarning)('This will permanently delete all cloned repositories in the selected workspaces!');
|
|
171
|
+
const confirmMessage = resolved.length === 1
|
|
172
|
+
? `Delete workspace ${resolved[0].name}?`
|
|
173
|
+
: `Delete these ${resolved.length} workspaces?`;
|
|
174
|
+
const { confirmed } = await inquirer_1.default.prompt([
|
|
175
|
+
{
|
|
176
|
+
type: 'confirm',
|
|
177
|
+
name: 'confirmed',
|
|
178
|
+
message: confirmMessage,
|
|
179
|
+
default: true,
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
if (confirmed) {
|
|
183
|
+
for (const { name, path: workspacePath } of resolved) {
|
|
184
|
+
try {
|
|
185
|
+
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
186
|
+
(0, logger_1.logSuccess)(`Deleted "${(0, colors_1.colorize)(name, 'cyan')}"`);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
(0, logger_1.logError)(`Failed to delete "${name}"`);
|
|
190
|
+
if (error instanceof Error)
|
|
191
|
+
(0, logger_1.logError)(error.message);
|
|
192
|
+
}
|
|
160
193
|
}
|
|
161
194
|
}
|
|
162
195
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -39,6 +39,7 @@ const config_1 = require("../utils/config");
|
|
|
39
39
|
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
40
40
|
const health_checks_1 = require("../utils/health-checks");
|
|
41
41
|
const logger_1 = require("../utils/logger");
|
|
42
|
+
const output_1 = require("../utils/output");
|
|
42
43
|
const colors_1 = require("../utils/colors");
|
|
43
44
|
const command_helpers_1 = require("../utils/command-helpers");
|
|
44
45
|
const displayHealthCheck = (result) => {
|
|
@@ -96,19 +97,42 @@ function registerDoctorCommand(parent) {
|
|
|
96
97
|
.alias('doc')
|
|
97
98
|
.description('Run comprehensive health checks')
|
|
98
99
|
.argument('[workspace]', 'Workspace name')
|
|
99
|
-
.
|
|
100
|
-
|
|
100
|
+
.option('--json', 'Output as JSON')
|
|
101
|
+
.action(async (workspace, opts) => {
|
|
102
|
+
await handleDoctor(workspace, opts);
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
|
-
async function handleDoctor(workspaceArg) {
|
|
105
|
+
async function handleDoctor(workspaceArg, opts = {}) {
|
|
106
|
+
// In --json mode, failures are parseable JSON on stdout + exit 1; otherwise a
|
|
107
|
+
// human log on stderr. `process.exit(1)` stays the last statement so TS still
|
|
108
|
+
// narrows `metadata` to non-null below.
|
|
104
109
|
try {
|
|
110
|
+
// JSON mode is non-interactive: require an explicit workspace rather than prompt.
|
|
111
|
+
if (opts.json && !workspaceArg) {
|
|
112
|
+
(0, output_1.outputJsonError)('doctor --json requires a workspace name');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
105
115
|
const selectedWorkspace = await (0, command_helpers_1.resolveWorkspace)(workspaceArg);
|
|
106
116
|
const workspacePath = path.join(config_1.WORKSPACES_DIR, selectedWorkspace);
|
|
107
117
|
const metadata = await (0, workspace_meta_1.loadMetadata)(workspacePath);
|
|
108
118
|
if (!metadata) {
|
|
109
|
-
(
|
|
119
|
+
if (opts.json)
|
|
120
|
+
(0, output_1.outputJsonError)(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
121
|
+
else
|
|
122
|
+
(0, logger_1.logError)(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
110
123
|
process.exit(1);
|
|
111
124
|
}
|
|
125
|
+
if (opts.json) {
|
|
126
|
+
const results = await (0, health_checks_1.runAllHealthChecks)(workspacePath, metadata);
|
|
127
|
+
const score = (0, health_checks_1.calculateHealthScore)(results);
|
|
128
|
+
const overall = results.some(r => r.status === 'error')
|
|
129
|
+
? 'error'
|
|
130
|
+
: results.some(r => r.status === 'warning')
|
|
131
|
+
? 'warning'
|
|
132
|
+
: 'healthy';
|
|
133
|
+
(0, output_1.outputJson)({ workspace: selectedWorkspace, score, status: overall, checks: results });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
112
136
|
(0, logger_1.logStep)(`Running health checks for workspace: ${(0, colors_1.colorize)(selectedWorkspace, 'cyan')}`);
|
|
113
137
|
(0, logger_1.logInfo)('This may take a moment...');
|
|
114
138
|
const results = await (0, health_checks_1.runAllHealthChecks)(workspacePath, metadata);
|
|
@@ -132,9 +156,13 @@ async function handleDoctor(workspaceArg) {
|
|
|
132
156
|
}
|
|
133
157
|
}
|
|
134
158
|
catch (error) {
|
|
135
|
-
(
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
if (opts.json) {
|
|
160
|
+
(0, output_1.outputJsonError)(error instanceof Error ? error.message : 'Failed to run health checks');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
(0, logger_1.logError)('Failed to run health checks');
|
|
164
|
+
if (error instanceof Error)
|
|
165
|
+
(0, logger_1.logError)(error.message);
|
|
138
166
|
}
|
|
139
167
|
process.exit(1);
|
|
140
168
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -44,6 +44,7 @@ const fsPromises = __importStar(require("fs/promises"));
|
|
|
44
44
|
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
45
45
|
const claude_sessions_1 = require("../utils/claude-sessions");
|
|
46
46
|
const logger_1 = require("../utils/logger");
|
|
47
|
+
const output_1 = require("../utils/output");
|
|
47
48
|
const colors_1 = require("../utils/colors");
|
|
48
49
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
49
50
|
const inquirer_autocomplete_prompt_1 = __importDefault(require("inquirer-autocomplete-prompt"));
|
|
@@ -57,6 +58,7 @@ function registerListCommand(parent) {
|
|
|
57
58
|
.alias('l')
|
|
58
59
|
.description('List workspaces and navigate to one')
|
|
59
60
|
.option('-a, --archived', 'Show archived workspaces')
|
|
61
|
+
.option('--json', 'Output as JSON (no interactive selection)')
|
|
60
62
|
.action(async (opts) => {
|
|
61
63
|
await handleList(opts);
|
|
62
64
|
});
|
|
@@ -72,6 +74,10 @@ async function handleList(opts) {
|
|
|
72
74
|
(0, claude_sessions_1.getWorkspaceSessions)(),
|
|
73
75
|
]);
|
|
74
76
|
if (workspaces.length === 0) {
|
|
77
|
+
if (opts.json) {
|
|
78
|
+
(0, output_1.outputJson)({ archived: showArchived, count: 0, workspaces: [] });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
75
81
|
console.log('\n' + '='.repeat(60));
|
|
76
82
|
console.log((0, colors_1.colorize)(title, 'bright'));
|
|
77
83
|
console.log('='.repeat(60) + '\n');
|
|
@@ -108,6 +114,22 @@ async function handleList(opts) {
|
|
|
108
114
|
return 1;
|
|
109
115
|
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
110
116
|
});
|
|
117
|
+
// JSON mode: one document to stdout, no table, no interactive selection.
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
(0, output_1.outputJson)({
|
|
120
|
+
archived: showArchived,
|
|
121
|
+
count: items.length,
|
|
122
|
+
workspaces: items.map(i => ({
|
|
123
|
+
name: i.name,
|
|
124
|
+
path: i.wsPath,
|
|
125
|
+
repoCount: i.repoCount,
|
|
126
|
+
createdAt: i.createdAt || null,
|
|
127
|
+
lastActive: i.lastActiveLabel,
|
|
128
|
+
hasSession: i.hasSession,
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
111
133
|
console.log('');
|
|
112
134
|
console.log((0, colors_1.colorize)(' ' + title, 'bright') + (0, colors_1.colorize)(' (sorted by last active)', 'dim'));
|
|
113
135
|
console.log((0, colors_1.colorize)(' ' + '─'.repeat(54), 'dim'));
|
|
@@ -187,5 +209,6 @@ async function handleList(opts) {
|
|
|
187
209
|
async function main() {
|
|
188
210
|
const args = process.argv.slice(2);
|
|
189
211
|
const archived = args.includes('--archived') || args.includes('-a');
|
|
190
|
-
|
|
212
|
+
const json = args.includes('--json');
|
|
213
|
+
await handleList({ archived, json });
|
|
191
214
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -39,6 +39,7 @@ const config_1 = require("../utils/config");
|
|
|
39
39
|
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
40
40
|
const git_status_1 = require("../utils/git-status");
|
|
41
41
|
const logger_1 = require("../utils/logger");
|
|
42
|
+
const output_1 = require("../utils/output");
|
|
42
43
|
const colors_1 = require("../utils/colors");
|
|
43
44
|
const command_helpers_1 = require("../utils/command-helpers");
|
|
44
45
|
const displayStatusTable = (statuses) => {
|
|
@@ -109,29 +110,57 @@ function registerStatusCommand(parent) {
|
|
|
109
110
|
.alias('st')
|
|
110
111
|
.description('Show git status across all repos')
|
|
111
112
|
.argument('[workspace]', 'Workspace name')
|
|
112
|
-
.
|
|
113
|
-
|
|
113
|
+
.option('--json', 'Output as JSON')
|
|
114
|
+
.action(async (workspace, opts) => {
|
|
115
|
+
await handleStatus(workspace, opts);
|
|
114
116
|
});
|
|
115
117
|
}
|
|
116
|
-
async function handleStatus(workspaceArg) {
|
|
118
|
+
async function handleStatus(workspaceArg, opts = {}) {
|
|
119
|
+
// In --json mode, failures are parseable JSON on stdout + exit 1; otherwise a
|
|
120
|
+
// human log on stderr. `process.exit(1)` stays the last statement so TS still
|
|
121
|
+
// narrows `metadata` to non-null below.
|
|
117
122
|
try {
|
|
123
|
+
// JSON mode is non-interactive: require an explicit workspace rather than prompt.
|
|
124
|
+
if (opts.json && !workspaceArg) {
|
|
125
|
+
(0, output_1.outputJsonError)('status --json requires a workspace name');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
118
128
|
const selectedWorkspace = await (0, command_helpers_1.resolveWorkspace)(workspaceArg);
|
|
119
129
|
const workspacePath = path.join(config_1.WORKSPACES_DIR, selectedWorkspace);
|
|
120
130
|
const metadata = await (0, workspace_meta_1.loadMetadata)(workspacePath);
|
|
121
131
|
if (!metadata) {
|
|
122
|
-
(
|
|
132
|
+
if (opts.json)
|
|
133
|
+
(0, output_1.outputJsonError)(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
134
|
+
else
|
|
135
|
+
(0, logger_1.logError)(`Workspace metadata not found for: ${selectedWorkspace}`);
|
|
123
136
|
process.exit(1);
|
|
124
137
|
}
|
|
138
|
+
const repoDirectoryNames = metadata.repositories.map(r => r.directoryName);
|
|
139
|
+
if (opts.json) {
|
|
140
|
+
const statuses = await (0, git_status_1.getAllReposStatus)(workspacePath, repoDirectoryNames, 3);
|
|
141
|
+
(0, output_1.outputJson)({
|
|
142
|
+
workspace: selectedWorkspace,
|
|
143
|
+
path: workspacePath,
|
|
144
|
+
repoCount: statuses.length,
|
|
145
|
+
clean: statuses.every(s => s.clean),
|
|
146
|
+
repositories: statuses,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
125
150
|
(0, logger_1.logStep)(`Checking status for workspace: ${(0, colors_1.colorize)(selectedWorkspace, 'cyan')}`);
|
|
126
151
|
(0, logger_1.logInfo)(`Found ${metadata.repositories.length} repositories`);
|
|
127
|
-
const repoDirectoryNames = metadata.repositories.map(r => r.directoryName);
|
|
128
152
|
const statuses = await (0, git_status_1.getAllReposStatus)(workspacePath, repoDirectoryNames, 3);
|
|
129
153
|
displayStatusTable(statuses);
|
|
130
154
|
}
|
|
131
155
|
catch (error) {
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
(0,
|
|
156
|
+
const msg = error instanceof Error ? error.message : 'Failed to check workspace status';
|
|
157
|
+
if (opts.json) {
|
|
158
|
+
(0, output_1.outputJsonError)(msg);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
(0, logger_1.logError)('Failed to check workspace status');
|
|
162
|
+
if (error instanceof Error)
|
|
163
|
+
(0, logger_1.logError)(error.message);
|
|
135
164
|
}
|
|
136
165
|
process.exit(1);
|
|
137
166
|
}
|
package/dist/mcp/install.js
CHANGED
|
@@ -70,7 +70,7 @@ function installShellIntegration() {
|
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
try {
|
|
73
|
-
(0, child_process_1.
|
|
73
|
+
(0, child_process_1.execFileSync)('bash', [scriptPath, shellType], { stdio: 'inherit' });
|
|
74
74
|
(0, logger_1.logSuccess)('Shell integration installed (auto-CD for w/workspace commands)');
|
|
75
75
|
}
|
|
76
76
|
catch {
|
|
@@ -294,7 +294,7 @@ async function install() {
|
|
|
294
294
|
const serverPath = getMcpServerPath();
|
|
295
295
|
(0, logger_1.logInfo)(`MCP server path: ${(0, colors_1.colorize)(serverPath, 'cyan')}`);
|
|
296
296
|
try {
|
|
297
|
-
(0, child_process_1.
|
|
297
|
+
(0, child_process_1.execFileSync)('claude', ['mcp', 'add', 'nemus', '-s', 'user', '--', 'node', serverPath], { stdio: 'pipe' });
|
|
298
298
|
(0, logger_1.logSuccess)('MCP server registered globally with Claude Code');
|
|
299
299
|
console.log('\nYou can now use nemus tools in any Claude Code session.');
|
|
300
300
|
console.log('Try asking: "list my workspaces" or "what\'s the status of my-workspace"');
|