@amistio/cli 0.1.6 → 0.1.7
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 +24 -0
- package/dist/index.js +544 -77
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createHash as
|
|
5
|
-
import { writeFile as
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
import { createHash as createHash6, randomUUID } from "node:crypto";
|
|
5
|
+
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
6
|
+
import os7 from "node:os";
|
|
7
|
+
import path13 from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
// ../shared/src/schemas.ts
|
|
@@ -141,6 +141,21 @@ var runnerToolCapabilitySchema = z.object({
|
|
|
141
141
|
supportsBranchIsolation: z.boolean().optional(),
|
|
142
142
|
supportsGitWorktreeIsolation: z.boolean().optional()
|
|
143
143
|
});
|
|
144
|
+
var runnerResourceUsageSchema = z.object({
|
|
145
|
+
sampledAt: isoDateTimeSchema,
|
|
146
|
+
processUptimeSeconds: z.number().nonnegative().optional(),
|
|
147
|
+
processMemoryRssBytes: z.number().int().nonnegative().optional(),
|
|
148
|
+
processMemoryHeapUsedBytes: z.number().int().nonnegative().optional(),
|
|
149
|
+
processMemoryHeapTotalBytes: z.number().int().nonnegative().optional(),
|
|
150
|
+
processCpuUserMicros: z.number().int().nonnegative().optional(),
|
|
151
|
+
processCpuSystemMicros: z.number().int().nonnegative().optional(),
|
|
152
|
+
processCpuPercent: z.number().nonnegative().optional(),
|
|
153
|
+
systemMemoryTotalBytes: z.number().int().nonnegative().optional(),
|
|
154
|
+
systemMemoryFreeBytes: z.number().int().nonnegative().optional(),
|
|
155
|
+
systemLoadAverage1m: z.number().nonnegative().optional(),
|
|
156
|
+
systemLoadAverage5m: z.number().nonnegative().optional(),
|
|
157
|
+
systemLoadAverage15m: z.number().nonnegative().optional()
|
|
158
|
+
});
|
|
144
159
|
var workIsolationModeSchema = z.enum(["none", "primaryCheckout", "branch", "gitWorktree"]);
|
|
145
160
|
var repositoryLinkSourceSchema = z.enum(["web", "cli"]);
|
|
146
161
|
var repositoryCloneStatusSchema = z.enum(["notCloned", "cloned", "validated", "failed"]);
|
|
@@ -341,6 +356,7 @@ var runnerHeartbeatItemSchema = baseItemSchema.extend({
|
|
|
341
356
|
preferenceSource: runnerPreferenceSourceSchema.optional(),
|
|
342
357
|
preferenceStatus: runnerPreferenceStatusSchema.optional(),
|
|
343
358
|
preferenceMessage: z.string().optional(),
|
|
359
|
+
resourceUsage: runnerResourceUsageSchema.optional(),
|
|
344
360
|
lastSeenAt: isoDateTimeSchema
|
|
345
361
|
});
|
|
346
362
|
var runnerSettingsItemSchema = baseItemSchema.extend({
|
|
@@ -1231,8 +1247,11 @@ function credentialKey(accountId, projectId, repositoryLinkId) {
|
|
|
1231
1247
|
}
|
|
1232
1248
|
|
|
1233
1249
|
// src/control-plane.ts
|
|
1250
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1234
1251
|
import { mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1235
1252
|
import path3 from "node:path";
|
|
1253
|
+
import { promisify as promisify2 } from "node:util";
|
|
1254
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1236
1255
|
var controlPlaneFolders = [
|
|
1237
1256
|
path3.join("docs", "architecture"),
|
|
1238
1257
|
path3.join("docs", "context"),
|
|
@@ -1279,6 +1298,17 @@ async function writeProjectLink(rootDir, metadata) {
|
|
|
1279
1298
|
await writeFile2(filePath, createProjectLinkMarkdown(metadata), "utf8");
|
|
1280
1299
|
return filePath;
|
|
1281
1300
|
}
|
|
1301
|
+
async function resolvePairingRoot(rootDir, options = {}) {
|
|
1302
|
+
const requestedRoot = path3.resolve(rootDir);
|
|
1303
|
+
const gitRoot = await readGitTopLevel(requestedRoot).catch(() => void 0);
|
|
1304
|
+
if (gitRoot) {
|
|
1305
|
+
return gitRoot;
|
|
1306
|
+
}
|
|
1307
|
+
if (options.explicitRoot) {
|
|
1308
|
+
return requestedRoot;
|
|
1309
|
+
}
|
|
1310
|
+
throw new Error("Run `amistio pair` from inside the repository checkout, or pass --root <repo-root> explicitly.");
|
|
1311
|
+
}
|
|
1282
1312
|
async function readProjectLink(rootDir) {
|
|
1283
1313
|
const candidatePaths = [
|
|
1284
1314
|
path3.join(rootDir, "docs", "context", "amistio-project.md"),
|
|
@@ -1327,6 +1357,14 @@ async function exists(filePath) {
|
|
|
1327
1357
|
return false;
|
|
1328
1358
|
}
|
|
1329
1359
|
}
|
|
1360
|
+
async function readGitTopLevel(rootDir) {
|
|
1361
|
+
const { stdout } = await execFileAsync2("git", ["-C", rootDir, "rev-parse", "--show-toplevel"], { maxBuffer: 1024 * 1024 });
|
|
1362
|
+
const gitRoot = stdout.trim();
|
|
1363
|
+
if (!gitRoot) {
|
|
1364
|
+
throw new Error("Git top-level path was empty.");
|
|
1365
|
+
}
|
|
1366
|
+
return path3.resolve(gitRoot);
|
|
1367
|
+
}
|
|
1330
1368
|
|
|
1331
1369
|
// src/api-client.ts
|
|
1332
1370
|
import { z as z3 } from "zod";
|
|
@@ -1617,8 +1655,8 @@ var toolSessionMutationSchema = z3.object({
|
|
|
1617
1655
|
});
|
|
1618
1656
|
function resolveApiUrl(apiUrl, urlPath) {
|
|
1619
1657
|
const base = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
|
|
1620
|
-
const
|
|
1621
|
-
return new URL(`${base}${
|
|
1658
|
+
const path14 = urlPath.startsWith("/") ? urlPath : `/${urlPath}`;
|
|
1659
|
+
return new URL(`${base}${path14}`);
|
|
1622
1660
|
}
|
|
1623
1661
|
|
|
1624
1662
|
// src/orchestrator.ts
|
|
@@ -2364,6 +2402,221 @@ async function readRunnerDaemonMetadataFile(filePath) {
|
|
|
2364
2402
|
}
|
|
2365
2403
|
}
|
|
2366
2404
|
|
|
2405
|
+
// src/runner-service.ts
|
|
2406
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
2407
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2408
|
+
import { mkdir as mkdir6, readFile as readFile4, rm as rm2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2409
|
+
import os4 from "node:os";
|
|
2410
|
+
import path7 from "node:path";
|
|
2411
|
+
function detectRunnerServicePlatform(platform = process.platform) {
|
|
2412
|
+
if (platform === "darwin") return "launchd";
|
|
2413
|
+
if (platform === "linux") return "systemd";
|
|
2414
|
+
return "unsupported";
|
|
2415
|
+
}
|
|
2416
|
+
function createRunnerServiceDescriptor(input) {
|
|
2417
|
+
const platform = input.platform ?? detectRunnerServicePlatform();
|
|
2418
|
+
if (platform === "unsupported") {
|
|
2419
|
+
throw new Error("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
|
|
2420
|
+
}
|
|
2421
|
+
const homeDir = input.homeDir ?? os4.homedir();
|
|
2422
|
+
const serviceName = runnerServiceName(input);
|
|
2423
|
+
const serviceFilePath = runnerServiceFilePath(platform, serviceName, homeDir);
|
|
2424
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2425
|
+
const command = [input.executablePath ?? process.execPath, input.scriptPath ?? process.argv[1], ...input.args];
|
|
2426
|
+
const logPath = path7.join(input.metadataDir ?? defaultRunnerMetadataDir(), `${runnerServiceKey(input)}.service.log`);
|
|
2427
|
+
const metadata = {
|
|
2428
|
+
schemaVersion: 1,
|
|
2429
|
+
accountId: input.accountId,
|
|
2430
|
+
projectId: input.projectId,
|
|
2431
|
+
repositoryLinkId: input.repositoryLinkId,
|
|
2432
|
+
runnerId: input.runnerId,
|
|
2433
|
+
rootDir: path7.resolve(input.rootDir),
|
|
2434
|
+
apiUrl: input.apiUrl,
|
|
2435
|
+
serviceName,
|
|
2436
|
+
serviceFilePath,
|
|
2437
|
+
platform,
|
|
2438
|
+
status: "installed",
|
|
2439
|
+
createdAt: now,
|
|
2440
|
+
updatedAt: now,
|
|
2441
|
+
args: input.args
|
|
2442
|
+
};
|
|
2443
|
+
return {
|
|
2444
|
+
metadata,
|
|
2445
|
+
content: platform === "launchd" ? createLaunchdPlist({ command, label: serviceName, logPath, rootDir: metadata.rootDir }) : createSystemdUnit({ command, description: `Amistio runner ${input.runnerId}`, logPath, rootDir: metadata.rootDir })
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
async function installRunnerService(input, options = {}) {
|
|
2449
|
+
const descriptor = createRunnerServiceDescriptor(input);
|
|
2450
|
+
await mkdir6(path7.dirname(descriptor.metadata.serviceFilePath), { recursive: true });
|
|
2451
|
+
await mkdir6(input.metadataDir ?? defaultRunnerMetadataDir(), { recursive: true });
|
|
2452
|
+
await writeFile6(descriptor.metadata.serviceFilePath, descriptor.content, { encoding: "utf8", mode: 384 });
|
|
2453
|
+
await writeRunnerServiceMetadata(descriptor.metadata, input.metadataDir);
|
|
2454
|
+
if (options.activate !== false) {
|
|
2455
|
+
const activation = await activateRunnerService(descriptor.metadata);
|
|
2456
|
+
if (!activation.succeeded) {
|
|
2457
|
+
throw new Error(`Startup service file was written to ${descriptor.metadata.serviceFilePath}, but activation failed: ${activation.message}`);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
return descriptor.metadata;
|
|
2461
|
+
}
|
|
2462
|
+
async function removeRunnerService(input) {
|
|
2463
|
+
const metadata = await readRunnerServiceMetadata(input, input.metadataDir);
|
|
2464
|
+
if (!metadata) {
|
|
2465
|
+
return void 0;
|
|
2466
|
+
}
|
|
2467
|
+
await deactivateRunnerService(metadata).catch(() => void 0);
|
|
2468
|
+
await rm2(metadata.serviceFilePath, { force: true });
|
|
2469
|
+
await rm2(runnerServiceMetadataPath(input, input.metadataDir ?? defaultRunnerMetadataDir()), { force: true });
|
|
2470
|
+
return { ...metadata, status: "removed", updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2471
|
+
}
|
|
2472
|
+
async function readRunnerServiceMetadata(input, metadataDir = defaultRunnerMetadataDir()) {
|
|
2473
|
+
try {
|
|
2474
|
+
const parsed = JSON.parse(await readFile4(runnerServiceMetadataPath(input, metadataDir), "utf8"));
|
|
2475
|
+
if (parsed.schemaVersion !== 1 || !parsed.serviceName || !parsed.serviceFilePath) {
|
|
2476
|
+
return void 0;
|
|
2477
|
+
}
|
|
2478
|
+
return parsed;
|
|
2479
|
+
} catch {
|
|
2480
|
+
return void 0;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
async function writeRunnerServiceMetadata(metadata, metadataDir = defaultRunnerMetadataDir()) {
|
|
2484
|
+
await mkdir6(metadataDir, { recursive: true });
|
|
2485
|
+
await writeFile6(runnerServiceMetadataPath(metadata, metadataDir), JSON.stringify(metadata, null, 2), { encoding: "utf8", mode: 384 });
|
|
2486
|
+
}
|
|
2487
|
+
async function runnerServiceRuntimeStatus(metadata) {
|
|
2488
|
+
if (metadata.platform === "launchd") {
|
|
2489
|
+
const target = launchdTarget(metadata);
|
|
2490
|
+
const result2 = await runProcess("launchctl", ["print", target], 5e3);
|
|
2491
|
+
return result2.exitCode === 0 ? "loaded" : "not loaded";
|
|
2492
|
+
}
|
|
2493
|
+
const result = await runProcess("systemctl", ["--user", "is-active", metadata.serviceName], 5e3);
|
|
2494
|
+
return result.exitCode === 0 ? result.output.trim() || "active" : "not active";
|
|
2495
|
+
}
|
|
2496
|
+
async function activateRunnerService(metadata) {
|
|
2497
|
+
if (metadata.platform === "launchd") {
|
|
2498
|
+
await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 5e3).catch(() => void 0);
|
|
2499
|
+
const result = await runProcess("launchctl", ["bootstrap", launchdDomain(), metadata.serviceFilePath], 1e4);
|
|
2500
|
+
return result.exitCode === 0 ? { succeeded: true, message: "launchd service loaded." } : { succeeded: false, message: result.output || `launchctl exited with ${result.exitCode}.` };
|
|
2501
|
+
}
|
|
2502
|
+
const reload = await runProcess("systemctl", ["--user", "daemon-reload"], 1e4);
|
|
2503
|
+
if (reload.exitCode !== 0) {
|
|
2504
|
+
return { succeeded: false, message: reload.output || `systemctl daemon-reload exited with ${reload.exitCode}.` };
|
|
2505
|
+
}
|
|
2506
|
+
const enable = await runProcess("systemctl", ["--user", "enable", "--now", metadata.serviceName], 2e4);
|
|
2507
|
+
return enable.exitCode === 0 ? { succeeded: true, message: "systemd user service enabled." } : { succeeded: false, message: enable.output || `systemctl enable exited with ${enable.exitCode}.` };
|
|
2508
|
+
}
|
|
2509
|
+
async function deactivateRunnerService(metadata) {
|
|
2510
|
+
if (metadata.platform === "launchd") {
|
|
2511
|
+
await runProcess("launchctl", ["bootout", launchdDomain(), metadata.serviceFilePath], 1e4);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
await runProcess("systemctl", ["--user", "disable", "--now", metadata.serviceName], 2e4);
|
|
2515
|
+
await runProcess("systemctl", ["--user", "daemon-reload"], 1e4).catch(() => void 0);
|
|
2516
|
+
}
|
|
2517
|
+
function createLaunchdPlist(input) {
|
|
2518
|
+
const commandItems = input.command.map((item) => ` <string>${xmlEscape(item)}</string>`).join("\n");
|
|
2519
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2520
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2521
|
+
<plist version="1.0">
|
|
2522
|
+
<dict>
|
|
2523
|
+
<key>Label</key>
|
|
2524
|
+
<string>${xmlEscape(input.label)}</string>
|
|
2525
|
+
<key>ProgramArguments</key>
|
|
2526
|
+
<array>
|
|
2527
|
+
${commandItems}
|
|
2528
|
+
</array>
|
|
2529
|
+
<key>WorkingDirectory</key>
|
|
2530
|
+
<string>${xmlEscape(input.rootDir)}</string>
|
|
2531
|
+
<key>EnvironmentVariables</key>
|
|
2532
|
+
<dict>
|
|
2533
|
+
<key>AMISTIO_RUNNER_MODE</key>
|
|
2534
|
+
<string>background</string>
|
|
2535
|
+
</dict>
|
|
2536
|
+
<key>RunAtLoad</key>
|
|
2537
|
+
<true/>
|
|
2538
|
+
<key>KeepAlive</key>
|
|
2539
|
+
<true/>
|
|
2540
|
+
<key>StandardOutPath</key>
|
|
2541
|
+
<string>${xmlEscape(input.logPath)}</string>
|
|
2542
|
+
<key>StandardErrorPath</key>
|
|
2543
|
+
<string>${xmlEscape(input.logPath)}</string>
|
|
2544
|
+
</dict>
|
|
2545
|
+
</plist>
|
|
2546
|
+
`;
|
|
2547
|
+
}
|
|
2548
|
+
function createSystemdUnit(input) {
|
|
2549
|
+
return `[Unit]
|
|
2550
|
+
Description=${input.description}
|
|
2551
|
+
|
|
2552
|
+
[Service]
|
|
2553
|
+
Type=simple
|
|
2554
|
+
WorkingDirectory=${systemdEscape(input.rootDir)}
|
|
2555
|
+
Environment=AMISTIO_RUNNER_MODE=background
|
|
2556
|
+
ExecStart=${input.command.map(systemdEscape).join(" ")}
|
|
2557
|
+
Restart=always
|
|
2558
|
+
RestartSec=5
|
|
2559
|
+
StandardOutput=append:${systemdEscape(input.logPath)}
|
|
2560
|
+
StandardError=append:${systemdEscape(input.logPath)}
|
|
2561
|
+
|
|
2562
|
+
[Install]
|
|
2563
|
+
WantedBy=default.target
|
|
2564
|
+
`;
|
|
2565
|
+
}
|
|
2566
|
+
function runnerServiceFilePath(platform, serviceName, homeDir) {
|
|
2567
|
+
if (platform === "launchd") {
|
|
2568
|
+
return path7.join(homeDir, "Library", "LaunchAgents", `${serviceName}.plist`);
|
|
2569
|
+
}
|
|
2570
|
+
return path7.join(homeDir, ".config", "systemd", "user", `${serviceName}.service`);
|
|
2571
|
+
}
|
|
2572
|
+
function runnerServiceMetadataPath(input, metadataDir) {
|
|
2573
|
+
return path7.join(metadataDir, `${runnerServiceKey(input)}.service.json`);
|
|
2574
|
+
}
|
|
2575
|
+
function runnerServiceName(input) {
|
|
2576
|
+
return `com.amistio.runner.${runnerServiceKey(input).slice(0, 20)}`;
|
|
2577
|
+
}
|
|
2578
|
+
function runnerServiceKey(input) {
|
|
2579
|
+
return createHash3("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.runnerId}`).digest("hex");
|
|
2580
|
+
}
|
|
2581
|
+
function launchdDomain() {
|
|
2582
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
2583
|
+
return uid === void 0 ? "gui/0" : `gui/${uid}`;
|
|
2584
|
+
}
|
|
2585
|
+
function launchdTarget(metadata) {
|
|
2586
|
+
return `${launchdDomain()}/${metadata.serviceName}`;
|
|
2587
|
+
}
|
|
2588
|
+
function xmlEscape(value) {
|
|
2589
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
2590
|
+
}
|
|
2591
|
+
function systemdEscape(value) {
|
|
2592
|
+
return value.includes(" ") || value.includes(" ") || value.includes('"') ? `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\"')}"` : value;
|
|
2593
|
+
}
|
|
2594
|
+
function runProcess(command, args, timeoutMs) {
|
|
2595
|
+
return new Promise((resolve) => {
|
|
2596
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2597
|
+
let output = "";
|
|
2598
|
+
const timeout = setTimeout(() => {
|
|
2599
|
+
output += `Timed out while running ${command}.
|
|
2600
|
+
`;
|
|
2601
|
+
child.kill("SIGTERM");
|
|
2602
|
+
}, timeoutMs);
|
|
2603
|
+
child.stdout?.on("data", (chunk) => {
|
|
2604
|
+
output += chunk.toString("utf8");
|
|
2605
|
+
});
|
|
2606
|
+
child.stderr?.on("data", (chunk) => {
|
|
2607
|
+
output += chunk.toString("utf8");
|
|
2608
|
+
});
|
|
2609
|
+
child.on("error", (error) => {
|
|
2610
|
+
clearTimeout(timeout);
|
|
2611
|
+
resolve({ exitCode: 1, output: error.message });
|
|
2612
|
+
});
|
|
2613
|
+
child.on("close", (code) => {
|
|
2614
|
+
clearTimeout(timeout);
|
|
2615
|
+
resolve({ exitCode: code ?? 1, output: output.trim() });
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2367
2620
|
// src/session-policy.ts
|
|
2368
2621
|
var maxIdleMs = 24 * 60 * 60 * 1e3;
|
|
2369
2622
|
var maxTotalMs = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -2498,8 +2751,8 @@ function tokens(value) {
|
|
|
2498
2751
|
}
|
|
2499
2752
|
|
|
2500
2753
|
// src/sync.ts
|
|
2501
|
-
import { mkdir as
|
|
2502
|
-
import
|
|
2754
|
+
import { mkdir as mkdir7, readdir as readdir3, readFile as readFile5, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
|
|
2755
|
+
import path8 from "node:path";
|
|
2503
2756
|
var legacySyncRoots = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
2504
2757
|
var syncRoots = legacySyncRoots.map((syncRoot) => `docs/${syncRoot}`);
|
|
2505
2758
|
async function collectSyncStatus(rootDir, webDocuments = []) {
|
|
@@ -2578,7 +2831,7 @@ async function readLocalSyncedDocuments(rootDir) {
|
|
|
2578
2831
|
const markdownFiles = await findMarkdownFiles(rootDir);
|
|
2579
2832
|
const documents = [];
|
|
2580
2833
|
for (const fullPath of markdownFiles) {
|
|
2581
|
-
const raw = await
|
|
2834
|
+
const raw = await readFile5(fullPath, "utf8");
|
|
2582
2835
|
const parsed = parseSyncedMarkdown(raw);
|
|
2583
2836
|
if (!parsed) {
|
|
2584
2837
|
continue;
|
|
@@ -2628,8 +2881,8 @@ async function materializeBrainDocuments(rootDir, documents, options = {}) {
|
|
|
2628
2881
|
result.skipped.push(document.repoPath);
|
|
2629
2882
|
continue;
|
|
2630
2883
|
}
|
|
2631
|
-
await
|
|
2632
|
-
await
|
|
2884
|
+
await mkdir7(path8.dirname(fullPath), { recursive: true });
|
|
2885
|
+
await writeFile7(fullPath, createSyncedDocumentMarkdown(document), "utf8");
|
|
2633
2886
|
result.written.push(document.repoPath);
|
|
2634
2887
|
}
|
|
2635
2888
|
return result;
|
|
@@ -2690,7 +2943,7 @@ function parseSyncedMarkdown(content) {
|
|
|
2690
2943
|
}
|
|
2691
2944
|
async function readExistingSyncedDocument(fullPath) {
|
|
2692
2945
|
try {
|
|
2693
|
-
const raw = await
|
|
2946
|
+
const raw = await readFile5(fullPath, "utf8");
|
|
2694
2947
|
const parsed = parseSyncedMarkdown(raw);
|
|
2695
2948
|
if (!parsed) {
|
|
2696
2949
|
return { exists: true };
|
|
@@ -2715,7 +2968,7 @@ async function readExistingSyncedDocument(fullPath) {
|
|
|
2715
2968
|
async function findMarkdownFiles(rootDir) {
|
|
2716
2969
|
const files = [];
|
|
2717
2970
|
for (const syncRoot of syncRoots) {
|
|
2718
|
-
const fullRoot =
|
|
2971
|
+
const fullRoot = path8.join(rootDir, syncRoot);
|
|
2719
2972
|
if (!await exists2(fullRoot)) {
|
|
2720
2973
|
continue;
|
|
2721
2974
|
}
|
|
@@ -2725,7 +2978,7 @@ async function findMarkdownFiles(rootDir) {
|
|
|
2725
2978
|
}
|
|
2726
2979
|
async function walkMarkdownFiles(directory, files) {
|
|
2727
2980
|
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
2728
|
-
const fullPath =
|
|
2981
|
+
const fullPath = path8.join(directory, entry.name);
|
|
2729
2982
|
if (entry.isDirectory()) {
|
|
2730
2983
|
await walkMarkdownFiles(fullPath, files);
|
|
2731
2984
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2734,23 +2987,23 @@ async function walkMarkdownFiles(directory, files) {
|
|
|
2734
2987
|
}
|
|
2735
2988
|
}
|
|
2736
2989
|
function safeRepoPath(rootDir, repoPath) {
|
|
2737
|
-
if (
|
|
2990
|
+
if (path8.isAbsolute(repoPath)) {
|
|
2738
2991
|
throw new Error(`Refusing to use absolute repo path: ${repoPath}`);
|
|
2739
2992
|
}
|
|
2740
|
-
const normalized =
|
|
2741
|
-
if (normalized === ".." || normalized.startsWith(`..${
|
|
2993
|
+
const normalized = path8.normalize(repoPath);
|
|
2994
|
+
if (normalized === ".." || normalized.startsWith(`..${path8.sep}`)) {
|
|
2742
2995
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
2743
2996
|
}
|
|
2744
|
-
const root =
|
|
2745
|
-
const fullPath =
|
|
2746
|
-
if (!fullPath.startsWith(`${root}${
|
|
2997
|
+
const root = path8.resolve(rootDir);
|
|
2998
|
+
const fullPath = path8.resolve(root, normalized);
|
|
2999
|
+
if (!fullPath.startsWith(`${root}${path8.sep}`)) {
|
|
2747
3000
|
throw new Error(`Refusing to use path outside the repository: ${repoPath}`);
|
|
2748
3001
|
}
|
|
2749
3002
|
return fullPath;
|
|
2750
3003
|
}
|
|
2751
3004
|
function isControlPlanePath(repoPath) {
|
|
2752
|
-
const normalized =
|
|
2753
|
-
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${
|
|
3005
|
+
const normalized = path8.normalize(repoPath);
|
|
3006
|
+
return syncRoots.some((syncRoot) => normalized === syncRoot || normalized.startsWith(`${syncRoot}${path8.sep}`));
|
|
2754
3007
|
}
|
|
2755
3008
|
function canonicalControlPlaneRepoPath(repoPath) {
|
|
2756
3009
|
const normalized = repoPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
@@ -2761,11 +3014,11 @@ function canonicalControlPlaneRepoPath(repoPath) {
|
|
|
2761
3014
|
return normalized;
|
|
2762
3015
|
}
|
|
2763
3016
|
function toRepoPath(rootDir, fullPath) {
|
|
2764
|
-
return
|
|
3017
|
+
return path8.relative(rootDir, fullPath).split(path8.sep).join("/");
|
|
2765
3018
|
}
|
|
2766
3019
|
function inferTitle(content, repoPath) {
|
|
2767
3020
|
const heading = content.split("\n").find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim();
|
|
2768
|
-
return heading ||
|
|
3021
|
+
return heading || path8.basename(repoPath, path8.extname(repoPath));
|
|
2769
3022
|
}
|
|
2770
3023
|
function parseFrontmatterFromSyncedDocument(frontmatter) {
|
|
2771
3024
|
return {
|
|
@@ -2786,9 +3039,9 @@ async function exists2(filePath) {
|
|
|
2786
3039
|
}
|
|
2787
3040
|
|
|
2788
3041
|
// src/tool-session-store.ts
|
|
2789
|
-
import { mkdir as
|
|
2790
|
-
import
|
|
2791
|
-
import
|
|
3042
|
+
import { mkdir as mkdir8, readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
|
|
3043
|
+
import os5 from "node:os";
|
|
3044
|
+
import path9 from "node:path";
|
|
2792
3045
|
var LocalToolSessionStore = class {
|
|
2793
3046
|
constructor(filePath = defaultSessionStorePath()) {
|
|
2794
3047
|
this.filePath = filePath;
|
|
@@ -2802,12 +3055,12 @@ var LocalToolSessionStore = class {
|
|
|
2802
3055
|
async setProviderSessionId(toolSessionId, toolName, providerSessionId) {
|
|
2803
3056
|
const data = await this.read();
|
|
2804
3057
|
data[toolSessionId] = { toolName, providerSessionId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2805
|
-
await
|
|
2806
|
-
await
|
|
3058
|
+
await mkdir8(path9.dirname(this.filePath), { recursive: true });
|
|
3059
|
+
await writeFile8(this.filePath, JSON.stringify(data, null, 2), "utf8");
|
|
2807
3060
|
}
|
|
2808
3061
|
async read() {
|
|
2809
3062
|
try {
|
|
2810
|
-
return JSON.parse(await
|
|
3063
|
+
return JSON.parse(await readFile6(this.filePath, "utf8"));
|
|
2811
3064
|
} catch {
|
|
2812
3065
|
return {};
|
|
2813
3066
|
}
|
|
@@ -2815,12 +3068,12 @@ var LocalToolSessionStore = class {
|
|
|
2815
3068
|
};
|
|
2816
3069
|
function defaultSessionStorePath() {
|
|
2817
3070
|
if (process.platform === "darwin") {
|
|
2818
|
-
return
|
|
3071
|
+
return path9.join(os5.homedir(), "Library", "Application Support", "Amistio", "tool-sessions.json");
|
|
2819
3072
|
}
|
|
2820
3073
|
if (process.platform === "win32") {
|
|
2821
|
-
return
|
|
3074
|
+
return path9.join(process.env.APPDATA ?? os5.homedir(), "Amistio", "tool-sessions.json");
|
|
2822
3075
|
}
|
|
2823
|
-
return
|
|
3076
|
+
return path9.join(process.env.XDG_STATE_HOME ?? path9.join(os5.homedir(), ".local", "state"), "amistio", "tool-sessions.json");
|
|
2824
3077
|
}
|
|
2825
3078
|
|
|
2826
3079
|
// src/work-runner.ts
|
|
@@ -3107,7 +3360,7 @@ function stripJsonFence(value) {
|
|
|
3107
3360
|
}
|
|
3108
3361
|
|
|
3109
3362
|
// src/runner-status.ts
|
|
3110
|
-
import { createHash as
|
|
3363
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3111
3364
|
var watchStateReminderMs = 60 * 1e3;
|
|
3112
3365
|
function formatWatchStartupContext(input) {
|
|
3113
3366
|
return [
|
|
@@ -3128,17 +3381,136 @@ function watchStateKey(action) {
|
|
|
3128
3381
|
return [action.kind, action.message, action.workItemId, action.documentId, action.runnerId].filter(Boolean).join(":");
|
|
3129
3382
|
}
|
|
3130
3383
|
function stableRunnerId(input) {
|
|
3131
|
-
const digest =
|
|
3384
|
+
const digest = createHash4("sha256").update(`${input.accountId}:${input.projectId}:${input.repositoryLinkId}:${input.machineId}`).digest("hex").slice(0, 20);
|
|
3132
3385
|
return `runner_${digest}`;
|
|
3133
3386
|
}
|
|
3134
3387
|
|
|
3388
|
+
// src/runner-resources.ts
|
|
3389
|
+
import os6 from "node:os";
|
|
3390
|
+
var defaultRuntime = {
|
|
3391
|
+
nowMs: () => Date.now(),
|
|
3392
|
+
memoryUsage: () => process.memoryUsage(),
|
|
3393
|
+
uptime: () => process.uptime(),
|
|
3394
|
+
cpuUsage: () => process.cpuUsage(),
|
|
3395
|
+
totalmem: () => os6.totalmem(),
|
|
3396
|
+
freemem: () => os6.freemem(),
|
|
3397
|
+
loadavg: () => os6.loadavg()
|
|
3398
|
+
};
|
|
3399
|
+
var previousRunnerResourceSample;
|
|
3400
|
+
function sampleCurrentRunnerResourceUsage() {
|
|
3401
|
+
const sample = collectRunnerResourceUsage(previousRunnerResourceSample);
|
|
3402
|
+
previousRunnerResourceSample = sample.state;
|
|
3403
|
+
return sample.resourceUsage;
|
|
3404
|
+
}
|
|
3405
|
+
function collectRunnerResourceUsage(previous, runtime = defaultRuntime) {
|
|
3406
|
+
const sampledAtMs = runtime.nowMs();
|
|
3407
|
+
const memory = runtime.memoryUsage();
|
|
3408
|
+
const cpu = runtime.cpuUsage();
|
|
3409
|
+
const load = runtime.loadavg();
|
|
3410
|
+
const totalMemory = runtime.totalmem();
|
|
3411
|
+
const freeMemory = runtime.freemem();
|
|
3412
|
+
const resourceUsage = {
|
|
3413
|
+
sampledAt: new Date(sampledAtMs).toISOString(),
|
|
3414
|
+
processUptimeSeconds: roundNumber(runtime.uptime(), 3),
|
|
3415
|
+
processMemoryRssBytes: Math.round(memory.rss),
|
|
3416
|
+
processMemoryHeapUsedBytes: Math.round(memory.heapUsed),
|
|
3417
|
+
processMemoryHeapTotalBytes: Math.round(memory.heapTotal),
|
|
3418
|
+
processCpuUserMicros: Math.round(cpu.user),
|
|
3419
|
+
processCpuSystemMicros: Math.round(cpu.system),
|
|
3420
|
+
systemMemoryTotalBytes: Math.round(totalMemory),
|
|
3421
|
+
systemMemoryFreeBytes: Math.round(freeMemory)
|
|
3422
|
+
};
|
|
3423
|
+
const cpuPercent = processCpuPercent(previous, { sampledAtMs, processCpuUserMicros: cpu.user, processCpuSystemMicros: cpu.system });
|
|
3424
|
+
if (cpuPercent !== void 0) {
|
|
3425
|
+
resourceUsage.processCpuPercent = cpuPercent;
|
|
3426
|
+
}
|
|
3427
|
+
if (load.length >= 3) {
|
|
3428
|
+
resourceUsage.systemLoadAverage1m = roundNumber(load[0], 2);
|
|
3429
|
+
resourceUsage.systemLoadAverage5m = roundNumber(load[1], 2);
|
|
3430
|
+
resourceUsage.systemLoadAverage15m = roundNumber(load[2], 2);
|
|
3431
|
+
}
|
|
3432
|
+
return {
|
|
3433
|
+
resourceUsage,
|
|
3434
|
+
state: {
|
|
3435
|
+
sampledAtMs,
|
|
3436
|
+
processCpuUserMicros: cpu.user,
|
|
3437
|
+
processCpuSystemMicros: cpu.system
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
}
|
|
3441
|
+
function formatRunnerResourceUsage(resourceUsage) {
|
|
3442
|
+
if (!resourceUsage) {
|
|
3443
|
+
return "unavailable until this runner reports a sample.";
|
|
3444
|
+
}
|
|
3445
|
+
const parts = [
|
|
3446
|
+
resourceUsage.processMemoryRssBytes !== void 0 ? `RSS ${formatBytes(resourceUsage.processMemoryRssBytes)}` : void 0,
|
|
3447
|
+
heapSummary(resourceUsage),
|
|
3448
|
+
resourceUsage.processCpuPercent !== void 0 ? `CPU ${formatPercent(resourceUsage.processCpuPercent)}` : "CPU warming up",
|
|
3449
|
+
systemMemorySummary(resourceUsage),
|
|
3450
|
+
loadSummary(resourceUsage),
|
|
3451
|
+
`sampled ${resourceUsage.sampledAt}`
|
|
3452
|
+
].filter(Boolean);
|
|
3453
|
+
return parts.length ? parts.join("; ") : "unavailable until this runner reports a complete sample.";
|
|
3454
|
+
}
|
|
3455
|
+
function formatBytes(bytes) {
|
|
3456
|
+
if (bytes === void 0 || !Number.isFinite(bytes)) {
|
|
3457
|
+
return "unknown";
|
|
3458
|
+
}
|
|
3459
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
3460
|
+
let value = Math.max(0, bytes);
|
|
3461
|
+
let unitIndex = 0;
|
|
3462
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
3463
|
+
value /= 1024;
|
|
3464
|
+
unitIndex += 1;
|
|
3465
|
+
}
|
|
3466
|
+
const digits = Number.isInteger(value) || value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
3467
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
3468
|
+
}
|
|
3469
|
+
function formatPercent(value) {
|
|
3470
|
+
if (value === void 0 || !Number.isFinite(value)) {
|
|
3471
|
+
return "unknown";
|
|
3472
|
+
}
|
|
3473
|
+
return `${roundNumber(value, 1).toFixed(1)}%`;
|
|
3474
|
+
}
|
|
3475
|
+
function processCpuPercent(previous, current) {
|
|
3476
|
+
if (!previous) return void 0;
|
|
3477
|
+
const elapsedMicros = (current.sampledAtMs - previous.sampledAtMs) * 1e3;
|
|
3478
|
+
const cpuDeltaMicros = current.processCpuUserMicros + current.processCpuSystemMicros - previous.processCpuUserMicros - previous.processCpuSystemMicros;
|
|
3479
|
+
if (elapsedMicros <= 0 || cpuDeltaMicros < 0) return void 0;
|
|
3480
|
+
return roundNumber(cpuDeltaMicros / elapsedMicros * 100, 1);
|
|
3481
|
+
}
|
|
3482
|
+
function heapSummary(resourceUsage) {
|
|
3483
|
+
if (resourceUsage.processMemoryHeapUsedBytes === void 0 && resourceUsage.processMemoryHeapTotalBytes === void 0) {
|
|
3484
|
+
return void 0;
|
|
3485
|
+
}
|
|
3486
|
+
return `heap ${formatBytes(resourceUsage.processMemoryHeapUsedBytes)} / ${formatBytes(resourceUsage.processMemoryHeapTotalBytes)}`;
|
|
3487
|
+
}
|
|
3488
|
+
function systemMemorySummary(resourceUsage) {
|
|
3489
|
+
if (resourceUsage.systemMemoryTotalBytes === void 0 || resourceUsage.systemMemoryFreeBytes === void 0 || resourceUsage.systemMemoryTotalBytes <= 0) {
|
|
3490
|
+
return void 0;
|
|
3491
|
+
}
|
|
3492
|
+
const usedBytes = Math.max(0, resourceUsage.systemMemoryTotalBytes - resourceUsage.systemMemoryFreeBytes);
|
|
3493
|
+
const usedPercent = usedBytes / resourceUsage.systemMemoryTotalBytes * 100;
|
|
3494
|
+
return `system memory ${formatBytes(usedBytes)} / ${formatBytes(resourceUsage.systemMemoryTotalBytes)} used (${formatPercent(usedPercent)})`;
|
|
3495
|
+
}
|
|
3496
|
+
function loadSummary(resourceUsage) {
|
|
3497
|
+
if (resourceUsage.systemLoadAverage1m === void 0 || resourceUsage.systemLoadAverage5m === void 0 || resourceUsage.systemLoadAverage15m === void 0) {
|
|
3498
|
+
return void 0;
|
|
3499
|
+
}
|
|
3500
|
+
return `load ${resourceUsage.systemLoadAverage1m.toFixed(2)} / ${resourceUsage.systemLoadAverage5m.toFixed(2)} / ${resourceUsage.systemLoadAverage15m.toFixed(2)}`;
|
|
3501
|
+
}
|
|
3502
|
+
function roundNumber(value, digits) {
|
|
3503
|
+
const factor = 10 ** digits;
|
|
3504
|
+
return Math.round(value * factor) / factor;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3135
3507
|
// src/importer.ts
|
|
3136
|
-
import { execFile as
|
|
3137
|
-
import { createHash as
|
|
3138
|
-
import { readdir as readdir4, readFile as
|
|
3139
|
-
import
|
|
3140
|
-
import { promisify as
|
|
3141
|
-
var
|
|
3508
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3509
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
3510
|
+
import { readdir as readdir4, readFile as readFile7, stat as stat4 } from "node:fs/promises";
|
|
3511
|
+
import path10 from "node:path";
|
|
3512
|
+
import { promisify as promisify3 } from "node:util";
|
|
3513
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3142
3514
|
var defaultMaxFileKb = 256;
|
|
3143
3515
|
var controlPlaneRoots2 = ["architecture", "context", "decisions", "features", "memory", "plans", "prompts", "workflows"];
|
|
3144
3516
|
var excludedDirectoryNames = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm-store", ".next", "dist", "build", "coverage", ".cache", "cache", "tmp", "temp", "vendor"]);
|
|
@@ -3155,12 +3527,12 @@ var documentFolderByType = {
|
|
|
3155
3527
|
workflow: "docs/workflows"
|
|
3156
3528
|
};
|
|
3157
3529
|
async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
3158
|
-
const requestedRoot =
|
|
3530
|
+
const requestedRoot = path10.resolve(rootDir);
|
|
3159
3531
|
const root = await runGit2(["-C", requestedRoot, "rev-parse", "--show-toplevel"]).catch(() => requestedRoot);
|
|
3160
3532
|
const detectedBranch = await runGit2(["-C", root, "symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => defaultBranch);
|
|
3161
3533
|
const originUrl = await runGit2(["-C", root, "remote", "get-url", "origin"]).catch(() => void 0);
|
|
3162
3534
|
const parsedCloneUrl = originUrl ? parseOptionalOriginCloneUrl(originUrl) : void 0;
|
|
3163
|
-
const repoName = (parsedCloneUrl?.repoName ??
|
|
3535
|
+
const repoName = (parsedCloneUrl?.repoName ?? path10.basename(root)) || "repository";
|
|
3164
3536
|
const fingerprintSeed = parsedCloneUrl ? `origin:${parsedCloneUrl.normalizedKey}` : `repo:${repoName}:${detectedBranch || defaultBranch}`;
|
|
3165
3537
|
return {
|
|
3166
3538
|
rootDir: root,
|
|
@@ -3172,7 +3544,7 @@ async function inspectLocalRepository(rootDir, defaultBranch) {
|
|
|
3172
3544
|
};
|
|
3173
3545
|
}
|
|
3174
3546
|
async function scanLegacyDocuments(options) {
|
|
3175
|
-
const rootDir =
|
|
3547
|
+
const rootDir = path10.resolve(options.rootDir);
|
|
3176
3548
|
const maxBytes = (options.maxFileKb ?? defaultMaxFileKb) * 1024;
|
|
3177
3549
|
const skipped = [];
|
|
3178
3550
|
const candidates = [];
|
|
@@ -3191,7 +3563,7 @@ async function scanLegacyDocuments(options) {
|
|
|
3191
3563
|
skipped.push({ repoPath, reason: "excluded" });
|
|
3192
3564
|
continue;
|
|
3193
3565
|
}
|
|
3194
|
-
const fullPath =
|
|
3566
|
+
const fullPath = path10.join(rootDir, ...repoPath.split("/"));
|
|
3195
3567
|
const fileStat = await stat4(fullPath).catch(() => void 0);
|
|
3196
3568
|
if (!fileStat?.isFile()) {
|
|
3197
3569
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
@@ -3201,7 +3573,7 @@ async function scanLegacyDocuments(options) {
|
|
|
3201
3573
|
skipped.push({ repoPath, reason: "tooLarge" });
|
|
3202
3574
|
continue;
|
|
3203
3575
|
}
|
|
3204
|
-
const content = await
|
|
3576
|
+
const content = await readFile7(fullPath, "utf8").catch(() => void 0);
|
|
3205
3577
|
if (content === void 0) {
|
|
3206
3578
|
skipped.push({ repoPath, reason: "unreadable" });
|
|
3207
3579
|
continue;
|
|
@@ -3291,8 +3663,8 @@ async function listRepositoryPaths(rootDir) {
|
|
|
3291
3663
|
async function walkRepository(rootDir, directory, files) {
|
|
3292
3664
|
const entries = await readdir4(directory, { withFileTypes: true }).catch(() => []);
|
|
3293
3665
|
for (const entry of entries) {
|
|
3294
|
-
const fullPath =
|
|
3295
|
-
const repoPath = normalizeRepoPath3(
|
|
3666
|
+
const fullPath = path10.join(directory, entry.name);
|
|
3667
|
+
const repoPath = normalizeRepoPath3(path10.relative(rootDir, fullPath));
|
|
3296
3668
|
if (entry.isDirectory()) {
|
|
3297
3669
|
if (!excludedDirectoryNames.has(entry.name)) {
|
|
3298
3670
|
await walkRepository(rootDir, fullPath, files);
|
|
@@ -3357,9 +3729,9 @@ function uniqueDestinationPath(basePath, sourcePath, usedPaths) {
|
|
|
3357
3729
|
usedPaths.add(basePath);
|
|
3358
3730
|
return basePath;
|
|
3359
3731
|
}
|
|
3360
|
-
const extension =
|
|
3361
|
-
const directory =
|
|
3362
|
-
const basename =
|
|
3732
|
+
const extension = path10.posix.extname(basePath) || ".md";
|
|
3733
|
+
const directory = path10.posix.dirname(basePath);
|
|
3734
|
+
const basename = path10.posix.basename(basePath, extension);
|
|
3363
3735
|
const uniquePath = `${directory}/${basename}-${hashText(sourcePath, 8)}${extension}`;
|
|
3364
3736
|
usedPaths.add(uniquePath);
|
|
3365
3737
|
return uniquePath;
|
|
@@ -3376,7 +3748,7 @@ function inferTitle2(content, repoPath) {
|
|
|
3376
3748
|
const body = stripFrontmatter(content);
|
|
3377
3749
|
const heading = body.split("\n").find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, "").trim();
|
|
3378
3750
|
if (heading) return heading;
|
|
3379
|
-
const basename =
|
|
3751
|
+
const basename = path10.posix.basename(repoPath, path10.posix.extname(repoPath)).replace(/[-_]+/g, " ").trim();
|
|
3380
3752
|
return titleCase(basename || "Imported Document");
|
|
3381
3753
|
}
|
|
3382
3754
|
function stripFrontmatter(content) {
|
|
@@ -3398,19 +3770,19 @@ function stableImportDocumentId(accountId, projectId, repositoryLinkId, sourcePa
|
|
|
3398
3770
|
return `doc_import_${hashText(`${accountId}\0${projectId}\0${repositoryLinkId}\0${sourcePath}`, 24)}`;
|
|
3399
3771
|
}
|
|
3400
3772
|
function hashText(value, length) {
|
|
3401
|
-
return
|
|
3773
|
+
return createHash5("sha256").update(value).digest("hex").slice(0, length);
|
|
3402
3774
|
}
|
|
3403
3775
|
function normalizeRepoPath3(value) {
|
|
3404
3776
|
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
3405
3777
|
}
|
|
3406
3778
|
async function runGit2(args) {
|
|
3407
|
-
const { stdout } = await
|
|
3779
|
+
const { stdout } = await execFileAsync3("git", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
3408
3780
|
return stdout.trim();
|
|
3409
3781
|
}
|
|
3410
3782
|
|
|
3411
3783
|
// src/runner-actions.ts
|
|
3412
|
-
import { spawn as
|
|
3413
|
-
import
|
|
3784
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3785
|
+
import path11 from "node:path";
|
|
3414
3786
|
function buildBackgroundRunnerArgs(options) {
|
|
3415
3787
|
const args = [
|
|
3416
3788
|
"run",
|
|
@@ -3420,7 +3792,7 @@ function buildBackgroundRunnerArgs(options) {
|
|
|
3420
3792
|
"--runner-id",
|
|
3421
3793
|
options.runnerId,
|
|
3422
3794
|
"--root",
|
|
3423
|
-
|
|
3795
|
+
path11.resolve(options.root),
|
|
3424
3796
|
"--session",
|
|
3425
3797
|
options.session,
|
|
3426
3798
|
"--interval-seconds",
|
|
@@ -3458,7 +3830,7 @@ async function runOfficialCliUpdate() {
|
|
|
3458
3830
|
}
|
|
3459
3831
|
function runOfficialUpdateProcess(command, args, timeoutMs) {
|
|
3460
3832
|
return new Promise((resolve) => {
|
|
3461
|
-
const child =
|
|
3833
|
+
const child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3462
3834
|
let output = "";
|
|
3463
3835
|
const updateTimeout = setTimeout(() => {
|
|
3464
3836
|
output += "Timed out while running official CLI update.\n";
|
|
@@ -3486,11 +3858,11 @@ function truncateProcessOutput(value) {
|
|
|
3486
3858
|
}
|
|
3487
3859
|
|
|
3488
3860
|
// src/git-worktree.ts
|
|
3489
|
-
import { execFile as
|
|
3490
|
-
import { mkdir as
|
|
3491
|
-
import
|
|
3492
|
-
import { promisify as
|
|
3493
|
-
var
|
|
3861
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
3862
|
+
import { mkdir as mkdir9, stat as stat5 } from "node:fs/promises";
|
|
3863
|
+
import path12 from "node:path";
|
|
3864
|
+
import { promisify as promisify4 } from "node:util";
|
|
3865
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
3494
3866
|
function needsGitWorktreeIsolation(workItem) {
|
|
3495
3867
|
return (workItem.workKind ?? "implementation") === "implementation";
|
|
3496
3868
|
}
|
|
@@ -3517,7 +3889,7 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
|
3517
3889
|
await assertExistingWorktree(worktreePath, identity.branch);
|
|
3518
3890
|
return { ...identity, baseRevision, worktreePath };
|
|
3519
3891
|
}
|
|
3520
|
-
await
|
|
3892
|
+
await mkdir9(path12.dirname(worktreePath), { recursive: true });
|
|
3521
3893
|
const branchExists = await gitCommandSucceeds(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${identity.branch}`]);
|
|
3522
3894
|
const worktreeArgs = branchExists ? ["worktree", "add", worktreePath, identity.branch] : ["worktree", "add", "-b", identity.branch, worktreePath, baseRevision];
|
|
3523
3895
|
await gitOutput(repoRoot, worktreeArgs).catch((error) => {
|
|
@@ -3526,9 +3898,9 @@ async function prepareGitWorktreeIsolation(rootDir, workItem) {
|
|
|
3526
3898
|
return { ...identity, baseRevision, worktreePath };
|
|
3527
3899
|
}
|
|
3528
3900
|
function localWorktreePath(repoRoot, worktreeKey) {
|
|
3529
|
-
const repoName =
|
|
3901
|
+
const repoName = path12.basename(repoRoot);
|
|
3530
3902
|
const worktreeSlug = worktreeKey.split("/").filter(Boolean).pop() ?? "work";
|
|
3531
|
-
return
|
|
3903
|
+
return path12.join(path12.dirname(repoRoot), `${repoName}.worktrees`, worktreeSlug);
|
|
3532
3904
|
}
|
|
3533
3905
|
async function assertExistingWorktree(worktreePath, branch) {
|
|
3534
3906
|
await gitOutput(worktreePath, ["rev-parse", "--is-inside-work-tree"]);
|
|
@@ -3551,11 +3923,11 @@ async function assertBaseRevision(repoRoot, baseRevision, currentHead) {
|
|
|
3551
3923
|
}
|
|
3552
3924
|
}
|
|
3553
3925
|
async function gitOutput(cwd, args) {
|
|
3554
|
-
const { stdout } = await
|
|
3926
|
+
const { stdout } = await execFileAsync4("git", args, { cwd, maxBuffer: 1024 * 1024 });
|
|
3555
3927
|
return stdout.trim();
|
|
3556
3928
|
}
|
|
3557
3929
|
async function gitCommandSucceeds(cwd, args) {
|
|
3558
|
-
return
|
|
3930
|
+
return execFileAsync4("git", args, { cwd }).then(() => true, () => false);
|
|
3559
3931
|
}
|
|
3560
3932
|
async function pathExists(value) {
|
|
3561
3933
|
return stat5(value).then(() => true, () => false);
|
|
@@ -3698,6 +4070,7 @@ program.command("import").description("Pair an existing checkout and import lega
|
|
|
3698
4070
|
console.log(`Next: amistio sync status${formatApiUrlFlag(options.apiUrl)}`);
|
|
3699
4071
|
});
|
|
3700
4072
|
program.command("pair").description("Pair this repository with an Amistio web project").requiredOption("--account <accountId>", "Amistio account ID").requiredOption("--project <projectId>", "Amistio project ID").option("--repository-link <repositoryLinkId>", "Existing repository link ID").option("--default-branch <branch>", "Default branch", "main").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--pairing-code <code>", "Short-lived pairing code from the Amistio app").option("--token <token>", "Runner/device credential to store outside the repository").option("--root <path>", "Repository root", defaultRoot).action(async (options, command) => {
|
|
4073
|
+
const pairingRoot = await resolvePairingRoot(options.root, { explicitRoot: command.getOptionValueSource("root") === "cli" });
|
|
3701
4074
|
let repositoryLinkId = options.repositoryLink ?? `repo_${randomUUID()}`;
|
|
3702
4075
|
let credential = options.token;
|
|
3703
4076
|
if (options.pairingCode) {
|
|
@@ -3709,7 +4082,7 @@ program.command("pair").description("Pair this repository with an Amistio web pr
|
|
|
3709
4082
|
projectId: options.project,
|
|
3710
4083
|
pairingCode: options.pairingCode,
|
|
3711
4084
|
repositoryLinkId,
|
|
3712
|
-
repoName: inferRepoName(
|
|
4085
|
+
repoName: inferRepoName(pairingRoot),
|
|
3713
4086
|
repoFingerprint: createRepoFingerprint(options.account, options.project, repositoryLinkId),
|
|
3714
4087
|
defaultBranch: options.defaultBranch
|
|
3715
4088
|
});
|
|
@@ -3717,7 +4090,7 @@ program.command("pair").description("Pair this repository with an Amistio web pr
|
|
|
3717
4090
|
credential = credential ?? pairing.token;
|
|
3718
4091
|
console.log(`Pairing confirmed for ${pairing.repositoryLink.repoName}.`);
|
|
3719
4092
|
}
|
|
3720
|
-
const filePath = await writeProjectLink(
|
|
4093
|
+
const filePath = await writeProjectLink(pairingRoot, {
|
|
3721
4094
|
amistioAccountId: options.account,
|
|
3722
4095
|
amistioProjectId: options.project,
|
|
3723
4096
|
repositoryLinkId,
|
|
@@ -3844,7 +4217,7 @@ work.command("prompt").description("Print or write an approved work prompt witho
|
|
|
3844
4217
|
}
|
|
3845
4218
|
const prompt = await createRunnerWorkPrompt(context.client, context.metadata.amistioProjectId, workItem);
|
|
3846
4219
|
if (options.out) {
|
|
3847
|
-
await
|
|
4220
|
+
await writeFile9(options.out, prompt, "utf8");
|
|
3848
4221
|
console.log(`Wrote work prompt to ${options.out}.`);
|
|
3849
4222
|
} else {
|
|
3850
4223
|
console.log(prompt);
|
|
@@ -3921,7 +4294,7 @@ program.command("run").description("Claim and run approved Amistio work locally"
|
|
|
3921
4294
|
projectId: context.metadata.amistioProjectId,
|
|
3922
4295
|
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
3923
4296
|
runnerId,
|
|
3924
|
-
rootDir:
|
|
4297
|
+
rootDir: path13.resolve(options.root),
|
|
3925
4298
|
apiUrl: options.apiUrl,
|
|
3926
4299
|
args: buildBackgroundRunnerArgs(resolvedOptions)
|
|
3927
4300
|
});
|
|
@@ -4006,6 +4379,7 @@ runner.command("status").description("Show background runner status for the pair
|
|
|
4006
4379
|
console.log("No background runner metadata found for this paired repository.");
|
|
4007
4380
|
if (runners.length) {
|
|
4008
4381
|
console.log(`Last runner heartbeat: ${runners[0].runnerId} ${runners[0].status} at ${runners[0].lastSeenAt}.`);
|
|
4382
|
+
console.log(`Resource usage: ${formatRunnerResourceUsage(runners[0].resourceUsage)}`);
|
|
4009
4383
|
}
|
|
4010
4384
|
return;
|
|
4011
4385
|
}
|
|
@@ -4026,6 +4400,7 @@ runner.command("status").description("Show background runner status for the pair
|
|
|
4026
4400
|
if (heartbeat) {
|
|
4027
4401
|
console.log(` Last heartbeat: ${heartbeat.status} at ${heartbeat.lastSeenAt}${heartbeat.version ? ` (${heartbeat.version})` : ""}`);
|
|
4028
4402
|
}
|
|
4403
|
+
console.log(` Resource usage: ${formatRunnerResourceUsage(heartbeat?.resourceUsage)}`);
|
|
4029
4404
|
}
|
|
4030
4405
|
});
|
|
4031
4406
|
runner.command("stop").description("Stop a background runner for the paired repository").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Runner ID to stop when multiple background runners exist").action(async (options) => {
|
|
@@ -4050,15 +4425,106 @@ runner.command("stop").description("Stop a background runner for the paired repo
|
|
|
4050
4425
|
return;
|
|
4051
4426
|
}
|
|
4052
4427
|
const record = records[0];
|
|
4428
|
+
const existingRunner = await context.client.listRunners(context.metadata.amistioProjectId).then((result) => result.runners.find((runner2) => runner2.runnerId === record.runnerId && runner2.repositoryLinkId === context.metadata.repositoryLinkId)).catch(() => void 0);
|
|
4053
4429
|
const stopResult = await stopRunnerDaemonProcess(record);
|
|
4054
4430
|
await markRunnerDaemonStopped(record);
|
|
4055
4431
|
await context.client.sendRunnerHeartbeat(context.metadata.amistioProjectId, record.runnerId, context.metadata.repositoryLinkId, "offline", {
|
|
4056
4432
|
version: CLI_VERSION,
|
|
4057
4433
|
mode: "background",
|
|
4058
|
-
hostname: record.hostname
|
|
4434
|
+
hostname: record.hostname,
|
|
4435
|
+
...existingRunner?.resourceUsage ? { resourceUsage: existingRunner.resourceUsage } : {}
|
|
4059
4436
|
}).catch(() => void 0);
|
|
4060
4437
|
console.log(stopResult === "stopped" ? `Stopped background runner ${record.runnerId}.` : `Marked background runner ${record.runnerId} stopped; process was not running.`);
|
|
4061
4438
|
});
|
|
4439
|
+
var runnerService = runner.command("service").description("Manage a user-level startup service for the paired runner");
|
|
4440
|
+
runnerService.command("install").description("Install a user-level startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").option("--tool <name>", "Local tool to use: auto, opencode, claude, codex, copilot, gemini, aider, cursor-agent").option("--invocation-channel <channel>", "Local invocation channel: auto, sdk, or command", parseInvocationChannel).option("--model <model>", "Model to request when the selected local tool supports model selection").option("--session <policy>", "Tool session policy: auto, new, continue:<toolSessionId>, or none", "auto").option("--interval-seconds <seconds>", "Polling interval for the service runner", parsePositiveInteger, 10).option("--no-stream", "Capture local tool output instead of streaming it").option("--verbose", "Print detailed runner errors").option("--dry-run", "Print the startup service descriptor without installing it").action(async (options) => {
|
|
4441
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4442
|
+
if (!context) {
|
|
4443
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
if (!context.token) {
|
|
4447
|
+
console.log("No local runner credential found. Run `amistio pair --pairing-code <code>` to store this machine credential.");
|
|
4448
|
+
process.exitCode = 1;
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
const platform = detectRunnerServicePlatform();
|
|
4452
|
+
if (platform === "unsupported") {
|
|
4453
|
+
console.log("Startup services are supported for user-level launchd on macOS and systemd user services on Linux.");
|
|
4454
|
+
process.exitCode = 1;
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4458
|
+
accountId: context.metadata.amistioAccountId,
|
|
4459
|
+
projectId: context.metadata.amistioProjectId,
|
|
4460
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4461
|
+
machineId: runnerMachineId()
|
|
4462
|
+
});
|
|
4463
|
+
const args = buildBackgroundRunnerArgs({ ...options, runnerId, apiUrl: options.apiUrl, root: options.root });
|
|
4464
|
+
const serviceInput = {
|
|
4465
|
+
accountId: context.metadata.amistioAccountId,
|
|
4466
|
+
projectId: context.metadata.amistioProjectId,
|
|
4467
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4468
|
+
runnerId,
|
|
4469
|
+
rootDir: path13.resolve(options.root),
|
|
4470
|
+
apiUrl: options.apiUrl,
|
|
4471
|
+
args,
|
|
4472
|
+
platform
|
|
4473
|
+
};
|
|
4474
|
+
if (options.dryRun) {
|
|
4475
|
+
const descriptor = createRunnerServiceDescriptor(serviceInput);
|
|
4476
|
+
console.log(`Startup service file: ${descriptor.metadata.serviceFilePath}`);
|
|
4477
|
+
console.log(descriptor.content.trim());
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
try {
|
|
4481
|
+
const metadata = await installRunnerService(serviceInput);
|
|
4482
|
+
console.log(`Installed startup service ${metadata.serviceName}.`);
|
|
4483
|
+
console.log(`Service file: ${metadata.serviceFilePath}`);
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
console.error(errorMessage3(error));
|
|
4486
|
+
process.exitCode = 1;
|
|
4487
|
+
}
|
|
4488
|
+
});
|
|
4489
|
+
runnerService.command("status").description("Show the startup service status for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").action(async (options) => {
|
|
4490
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4491
|
+
if (!context) {
|
|
4492
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4496
|
+
accountId: context.metadata.amistioAccountId,
|
|
4497
|
+
projectId: context.metadata.amistioProjectId,
|
|
4498
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4499
|
+
machineId: runnerMachineId()
|
|
4500
|
+
});
|
|
4501
|
+
const metadata = await readRunnerServiceMetadata({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
|
|
4502
|
+
if (!metadata) {
|
|
4503
|
+
console.log("No startup service metadata found for this paired repository runner.");
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
const runtimeStatus = await runnerServiceRuntimeStatus(metadata);
|
|
4507
|
+
console.log(`Startup service ${metadata.serviceName}: ${runtimeStatus}`);
|
|
4508
|
+
console.log(` Platform: ${metadata.platform}`);
|
|
4509
|
+
console.log(` File: ${metadata.serviceFilePath}`);
|
|
4510
|
+
console.log(` Root: ${metadata.rootDir}`);
|
|
4511
|
+
console.log(` API: ${metadata.apiUrl}`);
|
|
4512
|
+
});
|
|
4513
|
+
runnerService.command("remove").description("Remove the startup service for this paired repository runner").option("--api-url <url>", apiUrlOptionDescription, defaultApiUrl()).option("--root <path>", "Repository root", defaultRoot).option("--runner-id <runnerId>", "Stable runner ID").action(async (options) => {
|
|
4514
|
+
const context = await loadPairedApiContext(options.root, options.apiUrl);
|
|
4515
|
+
if (!context) {
|
|
4516
|
+
console.log("Repository is not paired. Run `amistio pair` first.");
|
|
4517
|
+
return;
|
|
4518
|
+
}
|
|
4519
|
+
const runnerId = options.runnerId ?? stableRunnerId({
|
|
4520
|
+
accountId: context.metadata.amistioAccountId,
|
|
4521
|
+
projectId: context.metadata.amistioProjectId,
|
|
4522
|
+
repositoryLinkId: context.metadata.repositoryLinkId,
|
|
4523
|
+
machineId: runnerMachineId()
|
|
4524
|
+
});
|
|
4525
|
+
const removed = await removeRunnerService({ accountId: context.metadata.amistioAccountId, projectId: context.metadata.amistioProjectId, repositoryLinkId: context.metadata.repositoryLinkId, runnerId });
|
|
4526
|
+
console.log(removed ? `Removed startup service ${removed.serviceName}.` : "No startup service metadata found for this paired repository runner.");
|
|
4527
|
+
});
|
|
4062
4528
|
async function runWatchIteration({ command, context, options, runnerId }) {
|
|
4063
4529
|
try {
|
|
4064
4530
|
return await runNextWorkItem({
|
|
@@ -4988,10 +5454,10 @@ function parseInvocationChannel(value) {
|
|
|
4988
5454
|
throw new Error(`Expected invocation channel auto, sdk, or command; received ${value}.`);
|
|
4989
5455
|
}
|
|
4990
5456
|
function inferRepoName(root) {
|
|
4991
|
-
return
|
|
5457
|
+
return path13.basename(path13.resolve(root)) || "repository";
|
|
4992
5458
|
}
|
|
4993
5459
|
function createRepoFingerprint(accountId, projectId, repositoryLinkId) {
|
|
4994
|
-
return
|
|
5460
|
+
return createHash6("sha256").update(`${accountId}:${projectId}:${repositoryLinkId}`).digest("hex");
|
|
4995
5461
|
}
|
|
4996
5462
|
function defaultApiUrl() {
|
|
4997
5463
|
const envApiUrl = process.env[AMISTIO_API_URL_ENV]?.trim();
|
|
@@ -5121,8 +5587,9 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
5121
5587
|
return {
|
|
5122
5588
|
version: CLI_VERSION,
|
|
5123
5589
|
mode,
|
|
5124
|
-
hostname:
|
|
5590
|
+
hostname: os7.hostname(),
|
|
5125
5591
|
...runnerIsolationCapabilityMetadata(),
|
|
5592
|
+
resourceUsage: sampleCurrentRunnerResourceUsage(),
|
|
5126
5593
|
...toolConfig?.capabilities ? { capabilities: toolConfig.capabilities } : {},
|
|
5127
5594
|
...toolConfig?.requestedTool ? { requestedTool: toolConfig.requestedTool } : {},
|
|
5128
5595
|
...toolConfig?.requestedInvocationChannel ? { requestedInvocationChannel: toolConfig.requestedInvocationChannel } : {},
|
|
@@ -5135,7 +5602,7 @@ function runnerHeartbeatMetadata(toolConfig, mode = currentRunnerMode()) {
|
|
|
5135
5602
|
};
|
|
5136
5603
|
}
|
|
5137
5604
|
function runnerMachineId() {
|
|
5138
|
-
return
|
|
5605
|
+
return createHash6("sha256").update(`${os7.hostname()}:${os7.platform()}:${os7.arch()}`).digest("hex").slice(0, 20);
|
|
5139
5606
|
}
|
|
5140
5607
|
async function delay(milliseconds) {
|
|
5141
5608
|
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|