noctrace 1.0.0 → 1.1.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/README.md +1 -0
- package/bin/noctrace.js +43 -8
- package/dist/server/server/docker.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,7 @@ No config files. No cloud. Everything stays local. Optional hooks for richer rea
|
|
|
143
143
|
| CLI Flag | Description |
|
|
144
144
|
|----------|-------------|
|
|
145
145
|
| `--docker <container>` | Attach to a running Docker container and stream its Claude Code sessions back to your host. Zero container setup |
|
|
146
|
+
| `--devcontainer <path>` | Resolve the running devcontainer for a local folder path and attach to it. Pass `.` for the current directory. Falls back to `--docker` if you pass a container name directly |
|
|
146
147
|
| `--install-hooks` | Configure Claude Code to push real-time events to noctrace |
|
|
147
148
|
| `--uninstall-hooks` | Remove noctrace hooks from Claude Code |
|
|
148
149
|
|
package/bin/noctrace.js
CHANGED
|
@@ -197,14 +197,11 @@ if (args.includes('--disable')) {
|
|
|
197
197
|
process.exit(0);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
207
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Run Docker watcher mode with a known, already-validated container ID.
|
|
202
|
+
* Shared by --docker and --devcontainer.
|
|
203
|
+
*/
|
|
204
|
+
async function runDockerMode(containerArg) {
|
|
208
205
|
const {
|
|
209
206
|
isValidContainerName,
|
|
210
207
|
assertContainerRunning,
|
|
@@ -320,6 +317,44 @@ if (args.includes('--docker')) {
|
|
|
320
317
|
await new Promise(() => {});
|
|
321
318
|
}
|
|
322
319
|
|
|
320
|
+
if (args.includes('--docker')) {
|
|
321
|
+
const containerArg = args[args.indexOf('--docker') + 1];
|
|
322
|
+
if (!containerArg || containerArg.startsWith('--')) {
|
|
323
|
+
console.error('[noctrace] Usage: npx noctrace --docker <container-name-or-id>');
|
|
324
|
+
console.error('[noctrace] Example: npx noctrace --docker my-claude-container');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
await runDockerMode(containerArg);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (args.includes('--devcontainer')) {
|
|
331
|
+
const devcontainerArg = args[args.indexOf('--devcontainer') + 1];
|
|
332
|
+
if (!devcontainerArg || devcontainerArg.startsWith('--')) {
|
|
333
|
+
console.error('[noctrace] Usage: npx noctrace --devcontainer <path-or-container>');
|
|
334
|
+
console.error('[noctrace] Examples:');
|
|
335
|
+
console.error('[noctrace] npx noctrace --devcontainer .');
|
|
336
|
+
console.error('[noctrace] npx noctrace --devcontainer /Users/me/myproject');
|
|
337
|
+
console.error('[noctrace] npx noctrace --devcontainer my-devcontainer-id');
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const { resolveDevcontainerContainer, defaultDockerRunner } =
|
|
342
|
+
await import('../dist/server/server/docker.js');
|
|
343
|
+
|
|
344
|
+
let resolvedContainer;
|
|
345
|
+
try {
|
|
346
|
+
resolvedContainer = resolveDevcontainerContainer(devcontainerArg, defaultDockerRunner);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
const lines = err.message.split('\n');
|
|
349
|
+
for (const line of lines) {
|
|
350
|
+
console.error(`[noctrace] ${line}`);
|
|
351
|
+
}
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await runDockerMode(resolvedContainer);
|
|
356
|
+
}
|
|
357
|
+
|
|
323
358
|
if (args.includes('--mcp')) {
|
|
324
359
|
// MCP mode: boot the Express server and speak JSON-RPC over stdio.
|
|
325
360
|
// stdout is the JSON-RPC channel — all logging must go to stderr.
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* watcher injection, and cleanup. All Docker commands go through the
|
|
6
6
|
* DockerRunner interface so callers (and tests) can swap in a stub.
|
|
7
7
|
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
8
10
|
// ---------------------------------------------------------------------------
|
|
9
11
|
// Default runner (real child_process)
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
@@ -150,3 +152,66 @@ export function cleanupWatcher(containerArg, runner) {
|
|
|
150
152
|
}
|
|
151
153
|
catch { /* container may be gone */ }
|
|
152
154
|
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Devcontainer support
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Look up a running container by an exact Docker label match.
|
|
160
|
+
* Returns the container ID (short form), or null when nothing matches.
|
|
161
|
+
*
|
|
162
|
+
* Uses `docker ps --filter "label=<label>=<value>" --format "{{.ID}}"`.
|
|
163
|
+
* The label and value are passed as a single `label=key=value` filter argument
|
|
164
|
+
* so no shell interpolation occurs.
|
|
165
|
+
*/
|
|
166
|
+
export function findContainerByLabel(label, value, runner) {
|
|
167
|
+
let output;
|
|
168
|
+
try {
|
|
169
|
+
output = runner.execSync('docker', ['ps', '--filter', `label=${label}=${value}`, '--format', '{{.ID}}'], { stdio: 'pipe' });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const id = output.trim().split('\n')[0]?.trim() ?? '';
|
|
175
|
+
return id.length > 0 ? id : null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a devcontainer argument to a concrete container ID.
|
|
179
|
+
*
|
|
180
|
+
* If `input` looks like a path (starts with `/`, `.`, `./`, or `~/`) it is
|
|
181
|
+
* resolved to an absolute path and looked up via the canonical
|
|
182
|
+
* `devcontainer.local_folder` label, falling back to the older
|
|
183
|
+
* `vsch.local.folder` label. When neither label matches an error is thrown
|
|
184
|
+
* with a clear hint pointing the user at `docker ps --filter "label=devcontainer.*"`.
|
|
185
|
+
*
|
|
186
|
+
* If `input` is not a path it is treated as a container name/ID.
|
|
187
|
+
* `isValidContainerName` is checked and the value is returned directly.
|
|
188
|
+
*
|
|
189
|
+
* @param cwd - Working directory used to resolve relative paths. Defaults to `process.cwd()`.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveDevcontainerContainer(input, runner, cwd) {
|
|
192
|
+
const isPath = input.startsWith('/') || input.startsWith('.') || input.startsWith('~/');
|
|
193
|
+
if (!isPath) {
|
|
194
|
+
if (!isValidContainerName(input)) {
|
|
195
|
+
throw new Error(`Invalid container name: "${input}"`);
|
|
196
|
+
}
|
|
197
|
+
return input;
|
|
198
|
+
}
|
|
199
|
+
// Resolve to an absolute path — devcontainer labels always store absolute paths.
|
|
200
|
+
// path.resolve does not expand ~ so handle that explicitly.
|
|
201
|
+
let absPath;
|
|
202
|
+
if (input.startsWith('~/')) {
|
|
203
|
+
absPath = path.join(os.homedir(), input.slice(2));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
absPath = path.resolve(cwd ?? process.cwd(), input);
|
|
207
|
+
}
|
|
208
|
+
// Try canonical label first, then the older VS Code label.
|
|
209
|
+
const id = findContainerByLabel('devcontainer.local_folder', absPath, runner) ??
|
|
210
|
+
findContainerByLabel('vsch.local.folder', absPath, runner);
|
|
211
|
+
if (id === null) {
|
|
212
|
+
throw new Error(`No devcontainer found for path: ${absPath}\n` +
|
|
213
|
+
`Hint: make sure the devcontainer is running, then check:\n` +
|
|
214
|
+
` docker ps --filter "label=devcontainer.local_folder"`);
|
|
215
|
+
}
|
|
216
|
+
return id;
|
|
217
|
+
}
|
package/package.json
CHANGED