mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
b232114422
Audits every comment in `src/` and `scripts/` against the code it
describes.
**462 files, 57 commits, net −8,194 lines.** Comment text only — the
whole branch is code-identical to `main`.
## Method
One subagent per file, strictly sequential. Scope was `src/` +
`scripts/` (459 files); three more files were added at the end because
they carried dead references of the same kind — two test headers citing
design docs that do not exist, and `publish-npm.yml` citing a retired
workflow. Each agent verified every comment — line, block, JSDoc, file
header, trailing — against the surrounding code and the rest of the
repo, following identifiers, paths, tool ids, config keys, env vars and
issue links to see whether they still exist and still behave as
described. Rules:
- **A false or misleading comment is deleted, not reworded.** If a claim
could not be confirmed by reading the source, it went. That is why the
deletion count is so much larger than the rewrite count.
- Survivors are cut to the shortest form carrying something the code
does not already say. Restatement, preamble, hedging, changelog prose
and ASCII banners are gone; the non-obvious *why* stays.
- Preserved byte-identical: license headers, pragmas and directives
(`@ts-*`, `eslint-disable`, shebangs, `/// <reference>`), JSDoc tag
tokens, everything inside a string or template literal, and the sole
comment inside an otherwise empty block (ESLint `no-empty` counts a
comment-bearing block as non-empty).
## Verification
Every file passed two independent gates before being recorded as done:
1. `comments-only` — the required check.
2. A second comment-stripping comparator with a proper mode stack,
written for this pass because `comments-only`'s flat scanner desyncs on
nested template literals and quote-bearing regex literals and then
reports comment lines as code changes. Two files hit that false FAIL
(`utils/android-profiler/pipeline/index.ts`,
`scripts/extract-tools.mjs`); in both the "changed code" it printed was
literally `//` lines, and the second checker confirmed the code was
byte-identical.
After the last file, all 462 changed files were re-checked against
`main` with the same comparator, rather than trusting any agent's
self-report. **459 code-identical; 2 are non-code (`.svg`, `.md`); 1
intentional.**
The intentional one is `packages/argent/scripts/bundle-tools.cjs`: the
changed template literal *is* the comment header of the file it
generates, `packages/native-devtools-android/src/bundled-meta.ts`.
Fixing only the generated file would have been reverted by the next
build, so the generator changed too — and it has been verified to
reproduce the committed generated file byte-for-byte.
## Representative false claims removed
Not wording nits — statements a reader would have acted on:
- **Reversed directions.** `proxyStart`'s JSDoc had the tunnel backwards
(it is a reverse tunnel: the host binds first and the simulator dials
in). A `paste()` doc had the pasteboard copy direction reversed.
- **Contradicted by the code below it.** A timeout budget multiplied by
three where the probes run concurrently — the same comment said so six
lines later. A "warn once" that warns on every call. A "binary search"
that is a linear scan.
- **Named things that do not exist.** A `vega-fast-cli` binary, a
`finish-recording.ts`, a `publish-next.yml` workflow, two
`profiler-react19-*.md` design docs, a `DebuggerTarget.ts`, a commit
hash git does not know, two tool ids, an `ensureEnv` cycle.
- **Wrong by construction.** "Welford accumulators" across four files
where the code keeps naive `n`/`sum`/`sumSq`; `sum`/`sumSq` documented
over `actualDuration` when reduce sums `selfDuration`; a strict-mode
halving written `n/2` where the code ceils; field docs listing enum
values the producers never emit.
- **Guarantees the code does not make.** A validation matrix claiming to
cover "EVERY tool" that skips flagless ones; a Pareto cutoff that
`slice(0, 20)` makes inert; an idempotence claim where the real rule is
at-or-ahead; a capability note describing a clean 400 the shape-based
device resolver can never produce.
- **Unverifiable assertions** about prebuilt binaries, external CLIs and
the cloud SDK — deleted rather than kept as folklore, since nothing in
the repo can confirm them.
- **Stale numbers**: invented Android tool versions, hard-coded tool
counts and description lengths that had drifted.
## Review
A Fable agent reviewed both halves adversarially for over-deletion,
misread code, `no-empty` hazards and byte-identity violations.
Second-half verdict: **SHIP**, with two one-line restores, both applied
in the final commit — the `npm view ""` rationale behind a blank-token
guard, and the note that `argent-mcp` keeps a copy of
`SECRET_PLACEHOLDER_MARKER` it cannot import.
## Code issues surfaced but deliberately not fixed
This pass changes comments only. Eight genuine findings are logged for a
follow-up:
1. `telemetry/src/consent.ts` — a non-ENOENT read error returns null and
falls through to the default-on path, so file errors *can* silently flip
telemetry on.
2. `chromium-server/navigation.ts` — `navigate()` is reachable from
`POST /api/navigate` with only a `typeof === "string"` check; open-url's
schema is a bare `z.string()`, so the "already validated by zod" premise
never held.
3. `http.ts` — `constantTimeEqual` returns early on a length mismatch,
so the auth token's length is observable.
4. `describe/index.ts:~114` — the ios-remote branch passes `{ isTvOs:
false }` unconditionally, so a remote tvOS simulator takes the iOS
ax-service path, though `isRemoteTvOsSimulator` exists and shake/paste
do use it.
5. `devices/boot-device.ts` — `-crash-report-mode never` is passed
unconditionally *and* appended again by the feature-detecting path, so
every emulator spawn passes it twice.
6. `react-profiler/pipeline/04-rank.ts` — `PARETO_THRESHOLD_PCT` is
dead: `slice(0, 20)` always wins.
7. `reaped-sessions.ts` — a user-facing hint string tells the agent that
`react-profiler-start { force: true }` disposes the debugger and
profiler session; it does not. Left byte-identical because it is a
string literal, not a comment.
8. `utils/simctl-backend.ts` — `localSimctl` is exported with no
importers anywhere.
## Docs
No documentation change is needed: this pass touches only source
comments, and no user-facing capability, tool, CLI flag, config key or
flow-file behaviour changed.
---------
Co-authored-by: filip131311 <f.kaminski2000@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
/**
|
|
* Full dev mode — runs argent from this checkout: no packing, no global install.
|
|
*
|
|
* Usage:
|
|
* npm run dev
|
|
* PORT=4000 npm run dev
|
|
*/
|
|
|
|
const { execSync, spawn } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
|
|
const ROOT = path.resolve(__dirname, "..");
|
|
const ARGENT_PKG = path.join(ROOT, "packages", "argent");
|
|
const TOOL_SERVER_PKG = path.join(ROOT, "packages", "tool-server");
|
|
const NATIVE_DEVTOOLS_PKG = path.join(ROOT, "packages", "native-devtools-ios");
|
|
const STATE_DIR = path.join(os.homedir(), ".argent");
|
|
const STATE_FILE = path.join(STATE_DIR, "tool-server.json");
|
|
const CLAUDE_JSON = path.join(os.homedir(), ".claude.json");
|
|
const CURSOR_DIR = path.join(os.homedir(), ".cursor");
|
|
const CURSOR_MCP_JSON = path.join(CURSOR_DIR, "mcp.json");
|
|
const PORT = parseInt(process.env.PORT ?? "3001", 10);
|
|
|
|
function readJson(filePath) {
|
|
if (!fs.existsSync(filePath)) return {};
|
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
}
|
|
|
|
function writeJson(filePath, data) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
}
|
|
|
|
function isProcessAlive(pid) {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function waitForHttp(url, timeoutMs = 20_000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
if (res.ok) return true;
|
|
} catch {
|
|
/* not up yet */
|
|
}
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Dylibs need Xcode to build and DYLD_INSERT_LIBRARIES into the iOS Simulator
|
|
// to run, so non-macOS hosts skip this and still get a usable dev setup.
|
|
if (process.platform === "darwin") {
|
|
const DYLIBS_DIR = path.join(NATIVE_DEVTOOLS_PKG, "dylibs");
|
|
const DYLIBS_EXIST = fs.existsSync(path.join(DYLIBS_DIR, "libNativeDevtoolsIos.dylib"));
|
|
const PRIVATE_NATIVE_DEVTOOLS_SRC = path.join(
|
|
ROOT,
|
|
"packages",
|
|
"argent-private",
|
|
"packages",
|
|
"native-devtools-ios",
|
|
"Sources",
|
|
"NativeDevtoolsIos"
|
|
);
|
|
|
|
// Non-fatal when pre-built dylibs are already present, so contributors
|
|
// without argent-private access can still run dev.
|
|
let submoduleReady = false;
|
|
try {
|
|
// Preserve an existing argent-private checkout so a local branch switch is
|
|
// not reset to the superproject's recorded gitlink on every dev run.
|
|
if (!fs.existsSync(PRIVATE_NATIVE_DEVTOOLS_SRC)) {
|
|
execSync("git submodule update --init packages/argent-private", {
|
|
cwd: ROOT,
|
|
stdio: "pipe",
|
|
});
|
|
}
|
|
submoduleReady = true;
|
|
} catch {
|
|
if (DYLIBS_EXIST) {
|
|
console.warn("⚠ argent-private submodule unavailable — using pre-built dylibs\n");
|
|
} else {
|
|
console.error("✗ argent-private submodule unavailable and no pre-built dylibs found.");
|
|
console.error(" Grant SSH access to github.com/software-mansion-labs/argent-private");
|
|
console.error(
|
|
" or obtain pre-built dylibs and place them in packages/native-devtools-ios/dylibs/"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (submoduleReady) {
|
|
console.log("Building native devtools dylibs...");
|
|
execSync("bash scripts/build.sh dev", {
|
|
cwd: NATIVE_DEVTOOLS_PKG,
|
|
stdio: "inherit",
|
|
});
|
|
console.log("✓ Native devtools dylibs built\n");
|
|
}
|
|
} else {
|
|
console.log(`⊘ Skipping native devtools dylibs build (macOS-only on ${process.platform})\n`);
|
|
}
|
|
|
|
console.log("Building dispatcher TypeScript...");
|
|
execSync("npm run build:dispatcher -w @swmansion/argent", {
|
|
cwd: ROOT,
|
|
stdio: "inherit",
|
|
});
|
|
console.log("✓ Dispatcher TypeScript built\n");
|
|
|
|
// Only the current host's key, unlike bundle-tools.cjs which copies every
|
|
// supported host's binary. Mirrors hostPlatformKey() in
|
|
// @argent/native-devtools-ios.
|
|
const HOST_PLATFORM_KEY =
|
|
process.platform === "linux" && process.arch === "arm64" ? "linux-arm64" : process.platform;
|
|
const BIN_DIR = path.join(ARGENT_PKG, "bin", HOST_PLATFORM_KEY);
|
|
// Mirrors simulatorServerBinaryName() in @argent/native-devtools-ios.
|
|
const BIN_BASENAME = process.platform === "win32" ? "simulator-server.exe" : "simulator-server";
|
|
const BIN_SRC = path.join(NATIVE_DEVTOOLS_PKG, "bin", HOST_PLATFORM_KEY, BIN_BASENAME);
|
|
const BIN_DEST = path.join(BIN_DIR, BIN_BASENAME);
|
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
if (fs.existsSync(BIN_SRC)) {
|
|
fs.copyFileSync(BIN_SRC, BIN_DEST);
|
|
fs.chmodSync(BIN_DEST, 0o755);
|
|
console.log(`✓ Copied simulator-server binary (${HOST_PLATFORM_KEY})`);
|
|
} else {
|
|
console.warn(
|
|
`⚠ simulator-server binary not found at ${BIN_SRC} — gestures won't work. ` +
|
|
`Run: bash scripts/download-simulator-server.sh`
|
|
);
|
|
}
|
|
|
|
for (const [srcName, destName] of [
|
|
["skills/skills", "skills"],
|
|
["skills/rules", "rules"],
|
|
["skills/agents", "agents"],
|
|
]) {
|
|
const src = path.join(ROOT, "packages", srcName);
|
|
const dest = path.join(ARGENT_PKG, destName);
|
|
if (fs.existsSync(src)) {
|
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
fs.cpSync(src, dest, { recursive: true });
|
|
}
|
|
}
|
|
console.log("✓ Copied skills/rules/agents");
|
|
|
|
// Dev never builds the esbuild bundle, and the launcher errors out when this
|
|
// path is missing.
|
|
const STUB = path.join(ARGENT_PKG, "dist", "tool-server.cjs");
|
|
if (!fs.existsSync(STUB)) {
|
|
fs.mkdirSync(path.dirname(STUB), { recursive: true });
|
|
fs.writeFileSync(
|
|
STUB,
|
|
"throw new Error('dev mode: tool-server stub — start npm run dev first');\n"
|
|
);
|
|
}
|
|
|
|
function restoreMcpEntry(configPath, originalEntry, existedBefore) {
|
|
const config = readJson(configPath);
|
|
if (!config.mcpServers) config.mcpServers = {};
|
|
|
|
if (originalEntry) {
|
|
config.mcpServers.argent = originalEntry;
|
|
} else {
|
|
delete config.mcpServers.argent;
|
|
}
|
|
|
|
if (config.mcpServers && Object.keys(config.mcpServers).length === 0) {
|
|
delete config.mcpServers;
|
|
}
|
|
|
|
if (!existedBefore && Object.keys(config).length === 0) {
|
|
try {
|
|
fs.unlinkSync(configPath);
|
|
} catch {
|
|
/* file already absent — nothing to remove */
|
|
}
|
|
return;
|
|
}
|
|
|
|
writeJson(configPath, config);
|
|
}
|
|
|
|
const LOCAL_MCP_ENTRY = path.join(ARGENT_PKG, "dist", "cli.js");
|
|
const LOG_FILE = path.join(STATE_DIR, "mcp-calls.log");
|
|
|
|
const claudeConfigExists = fs.existsSync(CLAUDE_JSON);
|
|
const claudeConfig = readJson(CLAUDE_JSON);
|
|
const originalArgentEntry = claudeConfig?.mcpServers?.argent ?? null;
|
|
const shouldPatchCursor = fs.existsSync(CURSOR_DIR);
|
|
const cursorConfigExists = fs.existsSync(CURSOR_MCP_JSON);
|
|
const cursorConfig = shouldPatchCursor ? readJson(CURSOR_MCP_JSON) : {};
|
|
const originalCursorArgentEntry = cursorConfig?.mcpServers?.argent ?? null;
|
|
|
|
const devMcpEntry = {
|
|
type: "stdio",
|
|
command: "node",
|
|
args: [LOCAL_MCP_ENTRY, "mcp"],
|
|
// Route the dev MCP at the running dev tool-server; without it the MCP
|
|
// auto-spawns dist/tool-server.cjs, which in dev is the throwing stub. The
|
|
// dev server runs auth-disabled, so no ARGENT_AUTH_TOKEN.
|
|
env: { ARGENT_MCP_LOG: LOG_FILE, ARGENT_TOOLS_URL: `http://127.0.0.1:${PORT}` },
|
|
};
|
|
|
|
if (!claudeConfig.mcpServers) claudeConfig.mcpServers = {};
|
|
claudeConfig.mcpServers.argent = devMcpEntry;
|
|
writeJson(CLAUDE_JSON, claudeConfig);
|
|
console.log(`✓ Patched ~/.claude.json → node ${LOCAL_MCP_ENTRY} mcp`);
|
|
|
|
if (shouldPatchCursor) {
|
|
if (!cursorConfig.mcpServers) cursorConfig.mcpServers = {};
|
|
cursorConfig.mcpServers.argent = {
|
|
command: "node",
|
|
args: [LOCAL_MCP_ENTRY, "mcp"],
|
|
// Same env as the Claude entry, so Cursor's MCP reuses the dev tool-server too.
|
|
env: devMcpEntry.env,
|
|
};
|
|
writeJson(CURSOR_MCP_JSON, cursorConfig);
|
|
console.log(`✓ Patched ~/.cursor/mcp.json → node ${LOCAL_MCP_ENTRY} mcp\n`);
|
|
} else {
|
|
console.log("• Skipped Cursor patch (no ~/.cursor directory found)\n");
|
|
}
|
|
|
|
let toolServerPid = null;
|
|
|
|
function cleanup() {
|
|
console.log("\nCleaning up...");
|
|
|
|
if (toolServerPid && isProcessAlive(toolServerPid)) {
|
|
process.kill(toolServerPid, "SIGTERM");
|
|
}
|
|
try {
|
|
fs.unlinkSync(STATE_FILE);
|
|
} catch {
|
|
/* state file already absent — nothing to remove */
|
|
}
|
|
|
|
restoreMcpEntry(CLAUDE_JSON, originalArgentEntry, claudeConfigExists);
|
|
console.log("✓ Restored ~/.claude.json");
|
|
if (shouldPatchCursor) {
|
|
restoreMcpEntry(CURSOR_MCP_JSON, originalCursorArgentEntry, cursorConfigExists);
|
|
console.log("✓ Restored ~/.cursor/mcp.json");
|
|
}
|
|
console.log("Done.");
|
|
}
|
|
|
|
process.on("SIGINT", () => {
|
|
cleanup();
|
|
process.exit(0);
|
|
});
|
|
process.on("SIGTERM", () => {
|
|
cleanup();
|
|
process.exit(0);
|
|
});
|
|
process.on("exit", cleanup);
|
|
|
|
async function main() {
|
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
const existingState = readJson(STATE_FILE);
|
|
if (existingState.pid && isProcessAlive(existingState.pid)) {
|
|
console.log(`Stopping existing tool-server (PID ${existingState.pid})...`);
|
|
process.kill(existingState.pid, "SIGTERM");
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
}
|
|
try {
|
|
fs.unlinkSync(STATE_FILE);
|
|
} catch {
|
|
/* state file already absent — nothing to remove */
|
|
}
|
|
|
|
console.log(`Starting dev tool-server on port ${PORT}...`);
|
|
|
|
const toolServer = spawn("npx", ["ts-node", "src/index.ts"], {
|
|
cwd: TOOL_SERVER_PKG,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: { ...process.env, PORT: String(PORT) },
|
|
});
|
|
|
|
toolServerPid = toolServer.pid;
|
|
|
|
const logStream = fs.createWriteStream(path.join(STATE_DIR, "tool-server.log"), { flags: "a" });
|
|
toolServer.stdout.pipe(logStream);
|
|
toolServer.stderr.pipe(logStream);
|
|
|
|
toolServer.on("exit", (code) => {
|
|
if (code !== 0 && code !== null) {
|
|
console.error(`\nTool-server exited with code ${code}. Check ~/.argent/tool-server.log`);
|
|
}
|
|
});
|
|
|
|
process.stdout.write("Waiting for tool-server");
|
|
const ready = await waitForHttp(`http://127.0.0.1:${PORT}/tools`);
|
|
if (!ready) {
|
|
console.error("\nTool-server failed to start. Check ~/.argent/tool-server.log");
|
|
cleanup();
|
|
process.exit(1);
|
|
}
|
|
console.log(" ready.");
|
|
|
|
writeJson(STATE_FILE, {
|
|
port: PORT,
|
|
pid: toolServerPid,
|
|
startedAt: new Date().toISOString(),
|
|
bundlePath: "dev",
|
|
});
|
|
|
|
console.log(`
|
|
✓ Dev environment ready
|
|
Tool-server: http://127.0.0.1:${PORT}/tools
|
|
MCP: ${LOCAL_MCP_ENTRY}
|
|
Logs: ~/.argent/tool-server.log
|
|
|
|
Start a new Claude Code or Cursor session to pick up the local MCP.
|
|
|
|
After tool-server code changes → Ctrl+C and re-run npm run dev
|
|
After MCP code changes → re-run npm run dev (rebuilds MCP automatically)
|
|
|
|
Press Ctrl+C to stop and restore global argent.
|
|
`);
|
|
|
|
await new Promise((resolve) => {
|
|
toolServer.on("exit", resolve);
|
|
});
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
cleanup();
|
|
process.exit(1);
|
|
});
|