mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
2180dd8e6a
We've had lots of type errors in our tests, scripts and other non-core
code despite all CI being green. This PR fixes that.
This PR also removes some redundant features like `@ts-check` inline
directives which have been replaced by proper ts config files.
<details>
## AI Summary
Audit of every CI workflow against every package's `package.json` and
`tsconfig` surfaced several gaps where CI did partial coverage. This PR
closes them and fixes every pre-existing type error the new gates
surfaced.
## Gaps closed
| # | Gap | Fix |
|---|---|---|
| 1 | `tsc --build` only covers `src/**`; test files (`test/**`,
`tests/**`) never typecheck. Vitest transforms tests with esbuild —
strips types instead of checking them. | New `tsconfig.test.json` per
test-bearing package (`composite: false`, `noEmit`, `rootDir: "."`,
includes `src/**` + `test/**` (or `tests/**`) + `vitest.config.ts`). New
`typecheck:tests` npm script per package. New CI step `npm run
typecheck:tests --workspaces --if-present`. |
| 2 | Build/publish scripts (`scripts/*.{cjs,mjs}`,
`packages/argent/scripts/*.cjs`, `packages/skills/scripts/install.js`) —
JS, never typechecked. `sync-readme.cjs` runs in `prepack`;
`postinstall.cjs` runs on every install; `bundle-tools.cjs` is
build-critical. A typo only surfaces at runtime. | Root
`tsconfig.scripts.json` with `allowJs` + `checkJs` + `noImplicitReturns`
covers all script paths. Root `typecheck:scripts` npm script. New CI
step. Per-file `// @ts-check` comments removed — tsconfig drives
inclusion. |
| 3 | `argent-cli` had no `typecheck:tests` script. When tests get added
there they'd silently bypass the gate. | Added `tsconfig.test.json` +
`typecheck:tests` script. `--if-present` would have skipped it anyway,
so this is future-proofing. |
| 4 | Local `prettier --check .` walked into the `argent-private`
submodule and reported violations there. CI didn't see this because
`actions/checkout@v4` doesn't init submodules — so local `prettier
--check` did not match CI behavior. | New `.prettierignore` excludes the
submodule plus `dist/`, `node_modules/`, lock files, `tsbuildinfo`.
No-op in CI; aligns local with remote. |
## Test type errors fixed
These were already present. The `npm test` passed because esbuild
stripped the offending types:
- **argent-installer**: widen `scope` literal type for
dead-code-elimination guard; non-null `addAllowlist!`/`removeAllowlist!`
(optional methods on adapter).
- **argent-mcp**: add `.js` extensions for NodeNext module resolution.
- **registry**: broaden `StaticBlueprintResult` blueprint api from `{
id; deps? }` to `Record<string, unknown>` so existing tests can pass
arbitrary api shapes.
- **tool-server**:
- non-null `zodSchema!` on tool defs that always carry one
(`launchAppTool`, `restartAppTool`, etc.)
- cast sentinel `"ignored"` strings to `DeviceInfo` in factory-rejection
tests
- match the 4-arg `dispatchByPlatform<IosServices, AndroidServices,
Params, Result>` signature (tests still passed the old 3-arg shape)
- `assertFlowRunResult` type guard for the `FlowRunResult |
FlowPrerequisiteNotice` union before `.steps` access
- swap broken `typeof import("supertest").default` for static `import
supertest` (supertest is `export = supertest`)
- fill in the 4 fields missing from a `NativeProfilerSessionApi` mock
(`xctraceProcess`, `recordingTimedOut`, `recordingExitedUnexpectedly`,
`lastExitInfo`)
- cast a vitest `Mock` to a plain function at one direct call site where
the `Procedure | Constructable` union loses callability
- fix `update-checker.test.ts` reading `../../package.json` (off-by-one
— went past tool-server; was relying on a Vite resolver quirk)
- replace one cross-package `../../../registry/src/index` import with
`@argent/registry` so registry's private `services` field isn't compared
between src and dist declarations
All fixes are type-level. Full `npm test --workspaces --if-present`
still reports **991 passed across 87 test files**.
</details>
348 lines
12 KiB
JavaScript
348 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
/**
|
|
* Full dev mode — no packing, no global install needed.
|
|
*
|
|
* What it does:
|
|
* 1. Builds native devtools dylibs (libArgentInjectionBootstrap, libNativeDevtoolsIos, libKeyboardPatch)
|
|
* 2. Builds the dispatcher TypeScript (tsc only, no esbuild bundles)
|
|
* 3. Sets up packages/argent/bin/ and packages/argent/dist/ for local use
|
|
* 4. Patches supported editor MCP configs to point argent at the local dist
|
|
* 5. Starts the tool-server from source via ts-node (no build needed)
|
|
* 6. Writes ~/.argent/tool-server.json so the local MCP picks it up
|
|
* 7. On exit: restores patched editor configs and stops the tool-server
|
|
*
|
|
* 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);
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
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 {}
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── Step 1: Build native devtools dylibs ─────────────────────────────────────
|
|
|
|
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"
|
|
);
|
|
|
|
// Try to init the submodule and rebuild. Failure is non-fatal if pre-built
|
|
// dylibs are already present — developers without argent-private access can
|
|
// still work on Argent using the committed binaries.
|
|
let submoduleReady = false;
|
|
try {
|
|
// Preserve an existing argent-private checkout so local branch switches
|
|
// are not reset back 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");
|
|
}
|
|
|
|
// ── Step 2: Build MCP TypeScript ─────────────────────────────────────────────
|
|
|
|
console.log("Building dispatcher TypeScript...");
|
|
execSync("npm run build:dispatcher -w @swmansion/argent", {
|
|
cwd: ROOT,
|
|
stdio: "inherit",
|
|
});
|
|
console.log("✓ Dispatcher TypeScript built\n");
|
|
|
|
// ── Step 3: Set up packages/argent/bin/ and skills/rules/agents ──────────────
|
|
|
|
const BIN_DIR = path.join(ARGENT_PKG, "bin");
|
|
const BIN_SRC = path.join(NATIVE_DEVTOOLS_PKG, "bin", "simulator-server");
|
|
const BIN_DEST = path.join(BIN_DIR, "simulator-server");
|
|
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");
|
|
} else {
|
|
console.warn("⚠ simulator-server binary not found — gestures won't work");
|
|
}
|
|
|
|
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");
|
|
|
|
// Stub tool-server.cjs — the launcher won't use it as long as the dev server
|
|
// is registered in the state file, but it needs to exist to avoid a crash.
|
|
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 {}
|
|
return;
|
|
}
|
|
|
|
writeJson(configPath, config);
|
|
}
|
|
|
|
// ── Step 4: Patch editor MCP configs to use local MCP dist ───────────────────
|
|
|
|
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"],
|
|
env: { ARGENT_MCP_LOG: LOG_FILE },
|
|
};
|
|
|
|
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"],
|
|
env: { ARGENT_MCP_LOG: LOG_FILE },
|
|
};
|
|
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");
|
|
}
|
|
|
|
// ── Cleanup on exit ───────────────────────────────────────────────────────────
|
|
|
|
let toolServerPid = null;
|
|
|
|
function cleanup() {
|
|
console.log("\nCleaning up...");
|
|
|
|
// Stop tool-server
|
|
if (toolServerPid && isProcessAlive(toolServerPid)) {
|
|
process.kill(toolServerPid, "SIGTERM");
|
|
}
|
|
try {
|
|
fs.unlinkSync(STATE_FILE);
|
|
} catch {}
|
|
|
|
// Restore editor configs
|
|
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() {
|
|
// ── Step 5: Kill any existing registered tool-server ───────────────────────
|
|
|
|
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 {}
|
|
|
|
// ── Step 6: Start tool-server from source ──────────────────────────────────
|
|
|
|
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`);
|
|
}
|
|
});
|
|
|
|
// ── Step 7: Wait for tool-server to be ready ───────────────────────────────
|
|
|
|
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.");
|
|
|
|
// ── Step 8: Write state file ────────────────────────────────────────────────
|
|
|
|
writeJson(STATE_FILE, {
|
|
port: PORT,
|
|
pid: toolServerPid,
|
|
startedAt: new Date().toISOString(),
|
|
bundlePath: "dev",
|
|
});
|
|
|
|
// ── Done ────────────────────────────────────────────────────────────────────
|
|
|
|
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.
|
|
`);
|
|
|
|
// Keep alive until tool-server exits or Ctrl+C
|
|
await new Promise((resolve) => {
|
|
toolServer.on("exit", resolve);
|
|
});
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
cleanup();
|
|
process.exit(1);
|
|
});
|