@nemus-cli/nemus 0.2.9 → 0.2.10
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 +54 -0
- package/README.md +53 -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/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/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/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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.10] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- **Eliminated the shell-injection surface in git/shell operations.** Every
|
|
15
|
+
`exec`/`execSync` call that interpolated a value into a shell string was
|
|
16
|
+
migrated to `execFile`/argv (no shell). Most notably, the MCP `branch_create`
|
|
17
|
+
and `switch_branch` tools built `git checkout -b <name>` by unquoted
|
|
18
|
+
interpolation with no branch-name validation, so a crafted (but valid) git ref
|
|
19
|
+
could run arbitrary commands; branch names are now passed as argv elements and
|
|
20
|
+
can never be interpreted by a shell. Also migrated `ghq`/clone, workspace
|
|
21
|
+
cleanup (`du`, `git clean`), disk-usage health checks, agent-availability
|
|
22
|
+
probing, and the shell-integration/MCP installers.
|
|
23
|
+
- **`w delete` no longer deletes arbitrary directories.** The `--workspace` flag
|
|
24
|
+
path validated names through `safeWorkspacePath()` (allowlist + containment)
|
|
25
|
+
and skips unknown/invalid names instead of `fs.rm`-ing a guessed path; the
|
|
26
|
+
interactive path is guarded the same way.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Monorepo (npm workspaces).** The repo is now an npm-workspaces monorepo:
|
|
31
|
+
the published CLI stays at the root (`@nemus-cli/nemus`, unchanged) and new
|
|
32
|
+
packages live under `packages/*`.
|
|
33
|
+
- **`@nemus-cli/cloud` (private, P1).** An optional, vendor-neutral cloud/IaC
|
|
34
|
+
package — not required for local Nemus. Two seams so far: the **forge-auth**
|
|
35
|
+
seam (`ForgeTokenSource`) with `pat` and a dependency-free `github-app` source
|
|
36
|
+
that mints least-privilege, auto-refreshing installation tokens; and the
|
|
37
|
+
**execution** seam (`Runner`/`Provisioner` + neutral `TaskSpec`/
|
|
38
|
+
`TargetDescriptor`/`Capabilities` + a name registry) with an in-box
|
|
39
|
+
**Docker runner** (needs no cloud account); a **`SecretSource`** seam
|
|
40
|
+
(`env`/`dotenv`/`gh` + `resolveSecretsToEnv`); and a **`GitForge`** seam
|
|
41
|
+
(`openPR`/`getChecks`/`comment`) with a dependency-free GitHub implementation;
|
|
42
|
+
and the **in-image agent orchestrator** (runner-image env contract, versioned
|
|
43
|
+
`result.json` schema, and `runAgentTask`: clone all → run the agent once over
|
|
44
|
+
the workspace → open a PR per changed repo, with per-repo error isolation);
|
|
45
|
+
the **OCI image + `nemus-cloud-agent` entrypoint** (a Dockerfile with node +
|
|
46
|
+
git + gh + pi/claude); and the **provisioning** seam (P2, in progress): a
|
|
47
|
+
generic `OpenTofuProvisioner` (delegates to `tofu`/`terraform`, maps a module's
|
|
48
|
+
`target` output → `TargetDescriptor`) + registry, plus a first **AWS Fargate**
|
|
49
|
+
OpenTofu module (`iac/fargate/`, validated with real `tofu validate`); and the
|
|
50
|
+
matching **`aws-fargate` Runner** (dependency-free, shells the `aws` CLI:
|
|
51
|
+
`register-task-definition` → `run-task`, `describe-tasks` → status, CloudWatch
|
|
52
|
+
`logs tail` streaming, `stop-task`); and the **`nemus-cloud` CLI** (own bin,
|
|
53
|
+
dependency-free) — `up`/`down` drive the provisioner, `run` launches the agent
|
|
54
|
+
image on a target (`--follow` logs, `--wait` for the exit code). This
|
|
55
|
+
completes P2. **P3 (in progress):** a bounded **CI-loop** (`runCiLoop`) that
|
|
56
|
+
drives a PR to green over `GitForge` — poll checks, run a fix pass on failure,
|
|
57
|
+
commit/push, repeat; anti-runaway guards (max iterations, no-change → stuck,
|
|
58
|
+
poll-budget → timeout) + a best-effort "needs a human" give-up comment; and
|
|
59
|
+
(P4) a vendor-neutral **notification seam** — `Notifier` with Slack (incoming
|
|
60
|
+
webhook) + generic webhook sinks (`notifierFromEnv`), wired into the CI-loop as
|
|
61
|
+
optional out-of-band report-back.
|
|
62
|
+
Design: `docs/plans/2026-08-26-cloud-iac.md`.
|
|
63
|
+
|
|
10
64
|
## [0.2.9] - 2026-08-26
|
|
11
65
|
|
|
12
66
|
### 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
|
|
31
51
|
|
|
32
|
-
|
|
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.
|
|
33
54
|
|
|
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.
|
|
55
|
+
**With Nemus:**
|
|
39
56
|
|
|
40
|
-
|
|
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
|
+
```
|
|
62
|
+
|
|
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.
|
|
65
|
+
|
|
66
|
+
## What you get
|
|
67
|
+
|
|
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
|
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/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"');
|
|
@@ -38,7 +38,10 @@ const child_process_1 = require("child_process");
|
|
|
38
38
|
const util_1 = require("util");
|
|
39
39
|
const fs = __importStar(require("fs/promises"));
|
|
40
40
|
const git_status_1 = require("./git-status");
|
|
41
|
-
|
|
41
|
+
// execFile (no shell): git args are passed as an argv array, so a branch name
|
|
42
|
+
// containing shell metacharacters can never be interpreted as a command.
|
|
43
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
44
|
+
const git = (args, opts) => execFileAsync('git', args, opts);
|
|
42
45
|
const GIT_TIMEOUT = 30000;
|
|
43
46
|
const switchBranch = async (repoPath, repoName, targetBranch) => {
|
|
44
47
|
try {
|
|
@@ -55,7 +58,7 @@ const switchBranch = async (repoPath, repoName, targetBranch) => {
|
|
|
55
58
|
}
|
|
56
59
|
// Check if it's a git repository
|
|
57
60
|
try {
|
|
58
|
-
await
|
|
61
|
+
await git(['rev-parse', '--git-dir'], { cwd: repoPath });
|
|
59
62
|
}
|
|
60
63
|
catch {
|
|
61
64
|
return {
|
|
@@ -65,7 +68,7 @@ const switchBranch = async (repoPath, repoName, targetBranch) => {
|
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
// Get current branch
|
|
68
|
-
const { stdout: currentBranchOutput } = await
|
|
71
|
+
const { stdout: currentBranchOutput } = await git(['branch', '--show-current'], { cwd: repoPath });
|
|
69
72
|
const currentBranch = currentBranchOutput.trim();
|
|
70
73
|
if (currentBranch === targetBranch) {
|
|
71
74
|
return {
|
|
@@ -76,7 +79,7 @@ const switchBranch = async (repoPath, repoName, targetBranch) => {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
// Check for uncommitted changes
|
|
79
|
-
const { stdout: statusOutput } = await
|
|
82
|
+
const { stdout: statusOutput } = await git(['status', '--porcelain'], { cwd: repoPath });
|
|
80
83
|
if (statusOutput.trim().length > 0) {
|
|
81
84
|
return {
|
|
82
85
|
repo: repoName,
|
|
@@ -86,17 +89,17 @@ const switchBranch = async (repoPath, repoName, targetBranch) => {
|
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
// Fetch to ensure we have latest branches
|
|
89
|
-
await
|
|
92
|
+
await git(['fetch'], { cwd: repoPath, timeout: GIT_TIMEOUT });
|
|
90
93
|
// Check if branch exists locally
|
|
91
94
|
try {
|
|
92
|
-
await
|
|
95
|
+
await git(['rev-parse', '--verify', targetBranch], { cwd: repoPath });
|
|
93
96
|
}
|
|
94
97
|
catch {
|
|
95
98
|
// Branch doesn't exist locally, check remote
|
|
96
99
|
try {
|
|
97
|
-
await
|
|
100
|
+
await git(['rev-parse', '--verify', `origin/${targetBranch}`], { cwd: repoPath });
|
|
98
101
|
// Branch exists on remote, create local tracking branch
|
|
99
|
-
await
|
|
102
|
+
await git(['checkout', '-b', targetBranch, `origin/${targetBranch}`], { cwd: repoPath });
|
|
100
103
|
return {
|
|
101
104
|
repo: repoName,
|
|
102
105
|
status: 'success',
|
|
@@ -114,7 +117,7 @@ const switchBranch = async (repoPath, repoName, targetBranch) => {
|
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
// Switch to existing local branch
|
|
117
|
-
await
|
|
120
|
+
await git(['checkout', targetBranch], { cwd: repoPath });
|
|
118
121
|
return {
|
|
119
122
|
repo: repoName,
|
|
120
123
|
status: 'success',
|
|
@@ -147,16 +150,10 @@ const createBranch = async (repoPath, repoName, branchName, baseBranch, force) =
|
|
|
147
150
|
}
|
|
148
151
|
// Checkout base branch if specified
|
|
149
152
|
if (baseBranch) {
|
|
150
|
-
await
|
|
151
|
-
cwd: repoPath,
|
|
152
|
-
timeout: GIT_TIMEOUT,
|
|
153
|
-
});
|
|
153
|
+
await git(['checkout', baseBranch], { cwd: repoPath, timeout: GIT_TIMEOUT });
|
|
154
154
|
}
|
|
155
155
|
// Create and checkout new branch
|
|
156
|
-
await
|
|
157
|
-
cwd: repoPath,
|
|
158
|
-
timeout: GIT_TIMEOUT,
|
|
159
|
-
});
|
|
156
|
+
await git(['checkout', '-b', branchName], { cwd: repoPath, timeout: GIT_TIMEOUT });
|
|
160
157
|
return {
|
|
161
158
|
repo: repoName,
|
|
162
159
|
success: true,
|
|
@@ -176,22 +173,16 @@ exports.createBranch = createBranch;
|
|
|
176
173
|
const mergeBranch = async (repoPath, repoName, sourceBranch, targetBranch, options) => {
|
|
177
174
|
try {
|
|
178
175
|
// Checkout target branch
|
|
179
|
-
await
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
// Build merge command
|
|
184
|
-
let mergeCmd = `git merge ${sourceBranch}`;
|
|
176
|
+
await git(['checkout', targetBranch], { cwd: repoPath, timeout: GIT_TIMEOUT });
|
|
177
|
+
// Build merge argv
|
|
178
|
+
const mergeArgs = ['merge', sourceBranch];
|
|
185
179
|
if (options?.noFf)
|
|
186
|
-
|
|
180
|
+
mergeArgs.push('--no-ff');
|
|
187
181
|
if (options?.ffOnly)
|
|
188
|
-
|
|
182
|
+
mergeArgs.push('--ff-only');
|
|
189
183
|
if (options?.squash)
|
|
190
|
-
|
|
191
|
-
await
|
|
192
|
-
cwd: repoPath,
|
|
193
|
-
timeout: GIT_TIMEOUT,
|
|
194
|
-
});
|
|
184
|
+
mergeArgs.push('--squash');
|
|
185
|
+
await git(mergeArgs, { cwd: repoPath, timeout: GIT_TIMEOUT });
|
|
195
186
|
return {
|
|
196
187
|
repo: repoName,
|
|
197
188
|
success: true,
|
|
@@ -210,10 +201,7 @@ const mergeBranch = async (repoPath, repoName, sourceBranch, targetBranch, optio
|
|
|
210
201
|
exports.mergeBranch = mergeBranch;
|
|
211
202
|
const rebaseBranch = async (repoPath, repoName, targetBranch) => {
|
|
212
203
|
try {
|
|
213
|
-
await
|
|
214
|
-
cwd: repoPath,
|
|
215
|
-
timeout: GIT_TIMEOUT,
|
|
216
|
-
});
|
|
204
|
+
await git(['rebase', targetBranch], { cwd: repoPath, timeout: GIT_TIMEOUT });
|
|
217
205
|
return {
|
|
218
206
|
repo: repoName,
|
|
219
207
|
success: true,
|
|
@@ -38,10 +38,10 @@ const child_process_1 = require("child_process");
|
|
|
38
38
|
const util_1 = require("util");
|
|
39
39
|
const fs = __importStar(require("fs/promises"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
|
-
const
|
|
41
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
42
42
|
const calculateDirSize = async (dirPath) => {
|
|
43
43
|
try {
|
|
44
|
-
const { stdout } = await
|
|
44
|
+
const { stdout } = await execFileAsync('du', ['-sk', dirPath]);
|
|
45
45
|
const sizeInKB = parseInt(stdout.split('\t')[0], 10);
|
|
46
46
|
return sizeInKB;
|
|
47
47
|
}
|
|
@@ -98,9 +98,9 @@ const removeBuildArtifacts = async (repoPath) => {
|
|
|
98
98
|
exports.removeBuildArtifacts = removeBuildArtifacts;
|
|
99
99
|
const gitClean = async (repoPath, dryRun = false) => {
|
|
100
100
|
try {
|
|
101
|
-
const
|
|
102
|
-
const { stdout } = await
|
|
103
|
-
const lines = stdout.split('\n').filter(l => l.trim());
|
|
101
|
+
const args = dryRun ? ['clean', '-fdxn'] : ['clean', '-fdx'];
|
|
102
|
+
const { stdout } = await execFileAsync('git', args, { cwd: repoPath });
|
|
103
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
104
104
|
return {
|
|
105
105
|
operation: 'Git clean',
|
|
106
106
|
filesRemoved: lines.length,
|