@tracemarketplace/cli 0.0.18 → 0.0.19
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/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +14 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/submit.d.ts +17 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +126 -14
- package/dist/commands/submit.js.map +1 -1
- package/dist/commands/submit.test.d.ts +2 -0
- package/dist/commands/submit.test.d.ts.map +1 -0
- package/dist/commands/submit.test.js +58 -0
- package/dist/commands/submit.test.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/submit-worker-state.d.ts +17 -0
- package/dist/submit-worker-state.d.ts.map +1 -0
- package/dist/submit-worker-state.js +88 -0
- package/dist/submit-worker-state.js.map +1 -0
- package/dist/submit-worker-state.test.d.ts +2 -0
- package/dist/submit-worker-state.test.d.ts.map +1 -0
- package/dist/submit-worker-state.test.js +45 -0
- package/dist/submit-worker-state.test.js.map +1 -0
- package/package.json +2 -2
- package/src/cli.ts +9 -2
- package/src/commands/status.ts +22 -0
- package/src/commands/submit.test.ts +64 -0
- package/src/commands/submit.ts +177 -21
- package/src/config.ts +8 -0
- package/src/submit-worker-state.test.ts +61 -0
- package/src/submit-worker-state.ts +127 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getSubmitLogPath, getSubmitWorkerStateDir } from "./config.js";
|
|
4
|
+
export function isProcessRunning(pid) {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
return err.code === "EPERM";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function writeSubmitWorkerState(profile, state, opts = {}) {
|
|
14
|
+
const dir = resolveStateDir(profile, opts.dir);
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
writeFileSync(join(dir, `${state.pid}.json`), JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
17
|
+
}
|
|
18
|
+
export function removeSubmitWorkerState(profile, pid, opts = {}) {
|
|
19
|
+
const dir = resolveStateDir(profile, opts.dir);
|
|
20
|
+
const path = join(dir, `${pid}.json`);
|
|
21
|
+
if (!existsSync(path)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
unlinkSync(path);
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
export function listActiveSubmitWorkers(profile, opts = {}) {
|
|
30
|
+
const dir = resolveStateDir(profile, opts.dir);
|
|
31
|
+
if (!existsSync(dir)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const workers = [];
|
|
35
|
+
for (const entry of readdirSync(dir)) {
|
|
36
|
+
if (!entry.endsWith(".json")) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const path = join(dir, entry);
|
|
40
|
+
const parsed = readSubmitWorkerStateFile(path);
|
|
41
|
+
if (!parsed || !isProcessRunning(parsed.pid)) {
|
|
42
|
+
try {
|
|
43
|
+
unlinkSync(path);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
workers.push(parsed);
|
|
49
|
+
}
|
|
50
|
+
return workers.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
51
|
+
}
|
|
52
|
+
export function createSubmitWorkerState(profile, pid, startedAt, readyChunkCount, readySessionCount) {
|
|
53
|
+
return {
|
|
54
|
+
pid,
|
|
55
|
+
startedAt,
|
|
56
|
+
readyChunkCount,
|
|
57
|
+
readySessionCount,
|
|
58
|
+
logPath: getSubmitLogPath(profile),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function resolveStateDir(profile, dir) {
|
|
62
|
+
return dir ?? getSubmitWorkerStateDir(profile);
|
|
63
|
+
}
|
|
64
|
+
function readSubmitWorkerStateFile(path) {
|
|
65
|
+
try {
|
|
66
|
+
const value = JSON.parse(readFileSync(path, "utf-8"));
|
|
67
|
+
if (typeof value.pid !== "number"
|
|
68
|
+
|| !Number.isInteger(value.pid)
|
|
69
|
+
|| value.pid <= 0
|
|
70
|
+
|| typeof value.startedAt !== "string"
|
|
71
|
+
|| typeof value.readyChunkCount !== "number"
|
|
72
|
+
|| typeof value.readySessionCount !== "number"
|
|
73
|
+
|| typeof value.logPath !== "string") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
pid: value.pid,
|
|
78
|
+
startedAt: value.startedAt,
|
|
79
|
+
readyChunkCount: value.readyChunkCount,
|
|
80
|
+
readySessionCount: value.readySessionCount,
|
|
81
|
+
logPath: value.logPath,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=submit-worker-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit-worker-state.js","sourceRoot":"","sources":["../src/submit-worker-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit-worker-state.test.d.ts","sourceRoot":"","sources":["../src/submit-worker-state.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { createSubmitWorkerState, listActiveSubmitWorkers, removeSubmitWorkerState, writeSubmitWorkerState, } from "./submit-worker-state.js";
|
|
6
|
+
describe("submit worker state", () => {
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
it("lists active workers and prunes stale ones", () => {
|
|
12
|
+
const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
|
|
13
|
+
tempDirs.push(dir);
|
|
14
|
+
writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 111, "2026-03-21T00:00:00.000Z", 3, 2), { dir });
|
|
15
|
+
writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 222, "2026-03-21T01:00:00.000Z", 1, 1), { dir });
|
|
16
|
+
vi.spyOn(process, "kill").mockImplementation((pid) => {
|
|
17
|
+
if (pid === 111) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const err = new Error("missing");
|
|
21
|
+
err.code = "ESRCH";
|
|
22
|
+
throw err;
|
|
23
|
+
});
|
|
24
|
+
const workers = listActiveSubmitWorkers("prod", { dir });
|
|
25
|
+
expect(workers).toEqual([
|
|
26
|
+
createSubmitWorkerState("prod", 111, "2026-03-21T00:00:00.000Z", 3, 2),
|
|
27
|
+
]);
|
|
28
|
+
expect(() => readFileSync(join(dir, "222.json"), "utf-8")).toThrow();
|
|
29
|
+
});
|
|
30
|
+
it("removes worker state files", () => {
|
|
31
|
+
const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
|
|
32
|
+
tempDirs.push(dir);
|
|
33
|
+
writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 333, "2026-03-21T00:00:00.000Z", 2, 1), { dir });
|
|
34
|
+
removeSubmitWorkerState("prod", 333, { dir });
|
|
35
|
+
expect(() => readFileSync(join(dir, "333.json"), "utf-8")).toThrow();
|
|
36
|
+
});
|
|
37
|
+
it("prunes invalid state payloads", () => {
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
|
|
39
|
+
tempDirs.push(dir);
|
|
40
|
+
writeFileSync(join(dir, "bad.json"), JSON.stringify({ nope: true }), "utf-8");
|
|
41
|
+
expect(listActiveSubmitWorkers("prod", { dir })).toEqual([]);
|
|
42
|
+
expect(() => readFileSync(join(dir, "bad.json"), "utf-8")).toThrow();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=submit-worker-state.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submit-worker-state.test.js","sourceRoot":"","sources":["../src/submit-worker-state.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tracemarketplace/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tracemp": "dist/cli.js"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"test:watch": "vitest"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@tracemarketplace/shared": "^0.0.
|
|
16
|
+
"@tracemarketplace/shared": "^0.0.14",
|
|
17
17
|
"better-sqlite3": "^12.8.0",
|
|
18
18
|
"chalk": "^5.3.0",
|
|
19
19
|
"commander": "^12.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
import { program } from "commander";
|
|
6
6
|
import { loginCommand } from "./commands/login.js";
|
|
7
7
|
import { whoamiCommand } from "./commands/whoami.js";
|
|
8
|
-
import { submitCommand } from "./commands/submit.js";
|
|
8
|
+
import { submitCommand, submitWorkerCommand } from "./commands/submit.js";
|
|
9
9
|
import { statusCommand } from "./commands/status.js";
|
|
10
10
|
import { historyCommand } from "./commands/history.js";
|
|
11
11
|
import { autoSubmitCommand } from "./commands/auto-submit.js";
|
|
@@ -45,7 +45,7 @@ program
|
|
|
45
45
|
.option("--tool <tool>", "Only submit from specific tool (claude-code|codex|cursor)")
|
|
46
46
|
.option("--session <id>", "Only submit a specific session ID")
|
|
47
47
|
.option("--dry-run", "Preview without submitting")
|
|
48
|
-
.option("--sync", "Wait
|
|
48
|
+
.option("--sync", "Wait in the foreground instead of launching a background submit worker")
|
|
49
49
|
.option("--created-since <duration>", "Only include sessions created within duration (e.g. 30d, 12h, 30m)", "30d")
|
|
50
50
|
.action((opts) =>
|
|
51
51
|
submitCommand({
|
|
@@ -58,6 +58,13 @@ program
|
|
|
58
58
|
}).catch(handleError)
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
+
program
|
|
62
|
+
.command("submit-worker", { hidden: true })
|
|
63
|
+
.description("Background submit worker (internal)")
|
|
64
|
+
.requiredOption("--manifest <path>", "Submit worker manifest path")
|
|
65
|
+
.option("--profile <name>", profileOptionDescription)
|
|
66
|
+
.action((opts) => submitWorkerCommand({ profile: opts.profile, manifest: opts.manifest }).catch(handleError));
|
|
67
|
+
|
|
61
68
|
program
|
|
62
69
|
.command("status")
|
|
63
70
|
.description("Show pending submissions and balance")
|
package/src/commands/status.ts
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
import { loadConfig, resolveProfile } from "../config.js";
|
|
3
3
|
import { ApiClient } from "../api-client.js";
|
|
4
4
|
import { loginCommandForProfile } from "../constants.js";
|
|
5
|
+
import { listActiveSubmitWorkers } from "../submit-worker-state.js";
|
|
5
6
|
|
|
6
7
|
export async function statusCommand(opts: { profile?: string } = {}): Promise<void> {
|
|
7
8
|
const profile = resolveProfile(opts.profile);
|
|
@@ -16,13 +17,34 @@ export async function statusCommand(opts: { profile?: string } = {}): Promise<vo
|
|
|
16
17
|
client.get("/api/v1/me") as Promise<{ email: string; balanceCents?: number; balance_cents?: number }>,
|
|
17
18
|
client.get("/api/v1/submissions") as Promise<any[]>,
|
|
18
19
|
]);
|
|
20
|
+
const activeWorkers = listActiveSubmitWorkers(config.profile);
|
|
19
21
|
|
|
20
22
|
const balance = ((me as any).balanceCents ?? (me as any).balance_cents ?? 0) / 100;
|
|
21
23
|
console.log(chalk.gray("Profile:"), config.profile);
|
|
22
24
|
console.log(chalk.gray("Server:"), config.serverUrl);
|
|
25
|
+
console.log(
|
|
26
|
+
chalk.bold("Background submit:"),
|
|
27
|
+
activeWorkers.length > 0
|
|
28
|
+
? chalk.yellow(`running (${activeWorkers.length})`)
|
|
29
|
+
: chalk.gray("idle"),
|
|
30
|
+
);
|
|
23
31
|
console.log(chalk.bold("Balance:"), chalk.green(`$${balance.toFixed(2)}`));
|
|
24
32
|
console.log(chalk.bold("Submissions:"), (subs as any[]).length);
|
|
25
33
|
|
|
34
|
+
if (activeWorkers.length > 0) {
|
|
35
|
+
activeWorkers.slice(0, 3).forEach((worker) => {
|
|
36
|
+
console.log(
|
|
37
|
+
chalk.gray(
|
|
38
|
+
` pid ${worker.pid} started ${worker.startedAt} ready ${worker.readyChunkCount} chunk(s) from ${worker.readySessionCount} session(s)`
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
if (activeWorkers.length > 3) {
|
|
43
|
+
console.log(chalk.gray(` ...and ${activeWorkers.length - 3} more worker(s)`));
|
|
44
|
+
}
|
|
45
|
+
console.log(chalk.gray(` Log: ${activeWorkers[0]!.logPath}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
const pending = (subs as any[]).filter((s) => s.acceptedCount === null);
|
|
27
49
|
if (pending.length > 0) {
|
|
28
50
|
console.log(chalk.yellow(`\n${pending.length} pending submission(s)`));
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSubmitWorkerRuntime, parseSubmitWorkerManifest } from "./submit.js";
|
|
3
|
+
|
|
4
|
+
const originalArgv = [...process.argv];
|
|
5
|
+
const originalExecArgv = [...process.execArgv];
|
|
6
|
+
|
|
7
|
+
describe("parseSubmitWorkerManifest", () => {
|
|
8
|
+
it("accepts valid session sources", () => {
|
|
9
|
+
const manifest = parseSubmitWorkerManifest(JSON.stringify({
|
|
10
|
+
createdAt: "2026-03-21T00:00:00.000Z",
|
|
11
|
+
readyChunkCount: 3,
|
|
12
|
+
readySessionCount: 2,
|
|
13
|
+
sources: [
|
|
14
|
+
{
|
|
15
|
+
tool: "codex_cli",
|
|
16
|
+
locator: "/tmp/session.jsonl",
|
|
17
|
+
label: "codex_cli:/tmp/session.jsonl",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
expect(manifest.readyChunkCount).toBe(3);
|
|
23
|
+
expect(manifest.readySessionCount).toBe(2);
|
|
24
|
+
expect(manifest.sources).toHaveLength(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects invalid source payloads", () => {
|
|
28
|
+
expect(() => parseSubmitWorkerManifest(JSON.stringify({
|
|
29
|
+
sources: [{ tool: "not-a-tool", locator: 123, label: null }],
|
|
30
|
+
}))).toThrow("Invalid submit worker manifest.");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("buildSubmitWorkerRuntime", () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
process.argv = [...originalArgv];
|
|
37
|
+
Object.defineProperty(process, "execArgv", {
|
|
38
|
+
configurable: true,
|
|
39
|
+
value: [...originalExecArgv],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("targets the hidden submit-worker command and strips inspect flags", () => {
|
|
44
|
+
process.argv = [process.execPath, "/tmp/tracemp-cli.js"];
|
|
45
|
+
Object.defineProperty(process, "execArgv", {
|
|
46
|
+
configurable: true,
|
|
47
|
+
value: ["--inspect=127.0.0.1:9229", "--loader", "tsx"],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(buildSubmitWorkerRuntime("prod", "/tmp/submit-job.json")).toEqual({
|
|
51
|
+
command: process.execPath,
|
|
52
|
+
args: [
|
|
53
|
+
"--loader",
|
|
54
|
+
"tsx",
|
|
55
|
+
"/tmp/tracemp-cli.js",
|
|
56
|
+
"submit-worker",
|
|
57
|
+
"--profile",
|
|
58
|
+
"prod",
|
|
59
|
+
"--manifest",
|
|
60
|
+
"/tmp/submit-job.json",
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
package/src/commands/submit.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync } from "fs";
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "fs";
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { join } from "path";
|
|
3
5
|
import chalk from "chalk";
|
|
4
6
|
import ora from "ora";
|
|
5
7
|
import inquirer from "inquirer";
|
|
6
8
|
import { extractClaudeCode, extractCodex, extractCursor, type NormalizedTrace } from "@tracemarketplace/shared";
|
|
7
|
-
import { loadConfig, loadState, resolveProfile, stateKey } from "../config.js";
|
|
9
|
+
import { getConfigDir, getSubmitLogPath, loadConfig, loadState, resolveProfile, stateKey } from "../config.js";
|
|
8
10
|
import { loginCommandForProfile } from "../constants.js";
|
|
9
11
|
import {
|
|
10
12
|
buildCursorSessionSource,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
type SessionSource,
|
|
17
19
|
} from "../flush.js";
|
|
18
20
|
import { CURSOR_DB_PATH, findFiles } from "../sessions.js";
|
|
21
|
+
import { createSubmitWorkerState, removeSubmitWorkerState, writeSubmitWorkerState } from "../submit-worker-state.js";
|
|
19
22
|
|
|
20
23
|
interface SubmitOptions {
|
|
21
24
|
profile?: string;
|
|
@@ -36,6 +39,18 @@ interface PlannedSession extends DiscoveredSession {
|
|
|
36
39
|
pending: boolean;
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
interface SubmitWorkerOptions {
|
|
43
|
+
profile?: string;
|
|
44
|
+
manifest: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SubmitWorkerManifest {
|
|
48
|
+
createdAt: string;
|
|
49
|
+
readyChunkCount: number;
|
|
50
|
+
readySessionCount: number;
|
|
51
|
+
sources: SessionSource[];
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function pushSessionIfNonEmpty(
|
|
40
55
|
sessions: DiscoveredSession[],
|
|
41
56
|
source: SessionSource,
|
|
@@ -269,24 +284,19 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
|
|
|
269
284
|
return;
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
const
|
|
273
|
-
opts.sync
|
|
274
|
-
? `Submitting ${readyChunkCount} finalized chunk(s) to ${config.profile}...`
|
|
275
|
-
: `Queuing ${readyChunkCount} finalized chunk(s) to ${config.profile}...`
|
|
276
|
-
).start();
|
|
287
|
+
const readySessions = plannedSessions.filter((session) => session.readyChunks > 0);
|
|
277
288
|
|
|
278
|
-
|
|
279
|
-
const
|
|
289
|
+
if (opts.sync) {
|
|
290
|
+
const uploadSpinner = ora(`Submitting ${readyChunkCount} finalized chunk(s) to ${config.profile}...`).start();
|
|
280
291
|
const prefetchedTraces = new Map(readySessions.map((s) => [`${s.source.tool}:${s.source.locator}`, s.trace]));
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
uploadSpinner.stop();
|
|
292
|
+
try {
|
|
293
|
+
const result = await flushTrackedSessions(
|
|
294
|
+
config,
|
|
295
|
+
readySessions.map((session) => session.source),
|
|
296
|
+
{ includeIdleTracked: false, prefetchedTraces, sync: true }
|
|
297
|
+
);
|
|
288
298
|
|
|
289
|
-
|
|
299
|
+
uploadSpinner.stop();
|
|
290
300
|
const failedSessions = result.results.filter((session) => session.error && session.error !== "Empty session");
|
|
291
301
|
console.log(chalk.green("\nSubmission complete!"));
|
|
292
302
|
console.log(` Uploaded chunks: ${chalk.bold(result.uploadedChunks)}`);
|
|
@@ -302,20 +312,166 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
|
|
|
302
312
|
console.log(chalk.gray(` ...and ${failedSessions.length - 3} more`));
|
|
303
313
|
}
|
|
304
314
|
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
console.
|
|
315
|
+
} catch (e) {
|
|
316
|
+
uploadSpinner.fail("Submission failed");
|
|
317
|
+
console.error(chalk.red(String(e)));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const launchSpinner = ora(`Starting background submit for ${readyChunkCount} finalized chunk(s)...`).start();
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const { logPath, pid } = spawnDetachedSubmitWorker(config.profile, {
|
|
327
|
+
createdAt: new Date().toISOString(),
|
|
328
|
+
readyChunkCount,
|
|
329
|
+
readySessionCount,
|
|
330
|
+
sources: readySessions.map((session) => session.source),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
launchSpinner.stop();
|
|
334
|
+
console.log(chalk.green(`\nStarted background submit for ${readyChunkCount} chunk(s).`));
|
|
335
|
+
if (pid) {
|
|
336
|
+
console.log(chalk.gray(` Worker PID: ${pid}`));
|
|
308
337
|
}
|
|
338
|
+
console.log(chalk.gray(` Log: ${logPath}`));
|
|
339
|
+
console.log(chalk.gray(" Run `tracemp status` later to check confirmation status."));
|
|
309
340
|
} catch (e) {
|
|
310
|
-
|
|
341
|
+
launchSpinner.fail("Could not start background submit");
|
|
311
342
|
console.error(chalk.red(String(e)));
|
|
312
343
|
process.exit(1);
|
|
313
344
|
}
|
|
314
345
|
}
|
|
315
346
|
|
|
347
|
+
export async function submitWorkerCommand(opts: SubmitWorkerOptions): Promise<void> {
|
|
348
|
+
const profile = resolveProfile(opts.profile);
|
|
349
|
+
try {
|
|
350
|
+
const manifest = parseSubmitWorkerManifest(await readFile(opts.manifest, "utf-8"));
|
|
351
|
+
const config = loadConfig(profile);
|
|
352
|
+
if (!config) {
|
|
353
|
+
throw new Error(`Not authenticated for profile '${profile}'. Run: ${loginCommandForProfile(profile)}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
console.log(
|
|
357
|
+
`[${new Date().toISOString()}] submit worker starting profile=${profile} ready_chunks=${manifest.readyChunkCount} sessions=${manifest.readySessionCount}`
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const result = await flushTrackedSessions(config, manifest.sources, { includeIdleTracked: false, sync: false });
|
|
361
|
+
const failedSessions = result.results.filter((session) => session.error && session.error !== "Empty session");
|
|
362
|
+
|
|
363
|
+
console.log(
|
|
364
|
+
`[${new Date().toISOString()}] submit worker queued chunks=${result.uploadedChunks} pending_sessions=${result.pendingSessions}`
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (failedSessions.length > 0) {
|
|
368
|
+
console.log(`[${new Date().toISOString()}] ${failedSessions.length} session(s) failed during submit:`);
|
|
369
|
+
failedSessions.slice(0, 10).forEach((session) => {
|
|
370
|
+
console.log(` ${session.source.label}: ${session.error}`);
|
|
371
|
+
});
|
|
372
|
+
if (failedSessions.length > 10) {
|
|
373
|
+
console.log(` ...and ${failedSessions.length - 10} more`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} finally {
|
|
377
|
+
removeSubmitWorkerState(profile, process.pid);
|
|
378
|
+
try {
|
|
379
|
+
unlinkSync(opts.manifest);
|
|
380
|
+
} catch {}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function buildSubmitWorkerRuntime(profile: string, manifestPath: string): { command: string; args: string[] } {
|
|
385
|
+
const cliEntrypoint = process.argv[1];
|
|
386
|
+
if (!cliEntrypoint) {
|
|
387
|
+
throw new Error("Unable to resolve tracemp CLI entrypoint for background submit.");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const execArgv = process.execArgv.filter((arg) => !arg.startsWith("--inspect"));
|
|
391
|
+
return {
|
|
392
|
+
command: process.execPath,
|
|
393
|
+
args: [...execArgv, cliEntrypoint, "submit-worker", "--profile", profile, "--manifest", manifestPath],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function parseSubmitWorkerManifest(raw: string): SubmitWorkerManifest {
|
|
398
|
+
const parsed = JSON.parse(raw) as Partial<SubmitWorkerManifest>;
|
|
399
|
+
if (!Array.isArray(parsed.sources) || parsed.sources.some((source) => !isSessionSource(source))) {
|
|
400
|
+
throw new Error("Invalid submit worker manifest.");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : new Date(0).toISOString(),
|
|
405
|
+
readyChunkCount: typeof parsed.readyChunkCount === "number" ? parsed.readyChunkCount : parsed.sources.length,
|
|
406
|
+
readySessionCount: typeof parsed.readySessionCount === "number" ? parsed.readySessionCount : parsed.sources.length,
|
|
407
|
+
sources: parsed.sources,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
316
411
|
function describeSessionStatus(session: PlannedSession): string {
|
|
317
412
|
if (session.readyChunks > 0 && session.pending) return "ready + pending";
|
|
318
413
|
if (session.readyChunks > 0) return "ready";
|
|
319
414
|
if (session.pending) return "pending";
|
|
320
415
|
return "up-to-date";
|
|
321
416
|
}
|
|
417
|
+
|
|
418
|
+
function isSessionSource(value: unknown): value is SessionSource {
|
|
419
|
+
if (!value || typeof value !== "object") {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const source = value as Record<string, unknown>;
|
|
424
|
+
return (
|
|
425
|
+
(source.tool === "claude_code" || source.tool === "codex_cli" || source.tool === "cursor")
|
|
426
|
+
&& typeof source.locator === "string"
|
|
427
|
+
&& typeof source.label === "string"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function spawnDetachedSubmitWorker(
|
|
432
|
+
profile: string,
|
|
433
|
+
manifest: SubmitWorkerManifest
|
|
434
|
+
): { logPath: string; pid: number | null } {
|
|
435
|
+
mkdirSync(getConfigDir(), { recursive: true });
|
|
436
|
+
const manifestPath = join(
|
|
437
|
+
getConfigDir(),
|
|
438
|
+
`submit-job-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}.json`
|
|
439
|
+
);
|
|
440
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
441
|
+
|
|
442
|
+
const logPath = getSubmitLogPath(profile);
|
|
443
|
+
const logFd = openSync(logPath, "a");
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const { command, args } = buildSubmitWorkerRuntime(profile, manifestPath);
|
|
447
|
+
const child = spawn(command, args, {
|
|
448
|
+
cwd: process.cwd(),
|
|
449
|
+
detached: true,
|
|
450
|
+
env: process.env,
|
|
451
|
+
stdio: ["ignore", logFd, logFd],
|
|
452
|
+
});
|
|
453
|
+
child.unref();
|
|
454
|
+
|
|
455
|
+
if (child.pid) {
|
|
456
|
+
writeSubmitWorkerState(
|
|
457
|
+
profile,
|
|
458
|
+
createSubmitWorkerState(
|
|
459
|
+
profile,
|
|
460
|
+
child.pid,
|
|
461
|
+
manifest.createdAt,
|
|
462
|
+
manifest.readyChunkCount,
|
|
463
|
+
manifest.readySessionCount,
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return { logPath, pid: child.pid ?? null };
|
|
469
|
+
} catch (err) {
|
|
470
|
+
try {
|
|
471
|
+
unlinkSync(manifestPath);
|
|
472
|
+
} catch {}
|
|
473
|
+
throw err;
|
|
474
|
+
} finally {
|
|
475
|
+
closeSync(logFd);
|
|
476
|
+
}
|
|
477
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -60,6 +60,14 @@ export function getAutoSubmitLogPath(profile?: string): string {
|
|
|
60
60
|
return join(getConfigDir(), `auto-submit${profileSuffix(profile)}.log`);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export function getSubmitLogPath(profile?: string): string {
|
|
64
|
+
return join(getConfigDir(), `submit${profileSuffix(profile)}.log`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getSubmitWorkerStateDir(profile?: string): string {
|
|
68
|
+
return join(getConfigDir(), `submit-workers${profileSuffix(profile)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
export function getDaemonStatePath(profile?: string): string {
|
|
64
72
|
return join(getConfigDir(), `daemon-state${profileSuffix(profile)}.json`);
|
|
65
73
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import {
|
|
6
|
+
createSubmitWorkerState,
|
|
7
|
+
listActiveSubmitWorkers,
|
|
8
|
+
removeSubmitWorkerState,
|
|
9
|
+
writeSubmitWorkerState,
|
|
10
|
+
} from "./submit-worker-state.js";
|
|
11
|
+
|
|
12
|
+
describe("submit worker state", () => {
|
|
13
|
+
const tempDirs: string[] = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("lists active workers and prunes stale ones", () => {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
|
|
21
|
+
tempDirs.push(dir);
|
|
22
|
+
|
|
23
|
+
writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 111, "2026-03-21T00:00:00.000Z", 3, 2), { dir });
|
|
24
|
+
writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 222, "2026-03-21T01:00:00.000Z", 1, 1), { dir });
|
|
25
|
+
|
|
26
|
+
vi.spyOn(process, "kill").mockImplementation((pid: number | NodeJS.Signals) => {
|
|
27
|
+
if (pid === 111) {
|
|
28
|
+
return true as never;
|
|
29
|
+
}
|
|
30
|
+
const err = new Error("missing") as NodeJS.ErrnoException;
|
|
31
|
+
err.code = "ESRCH";
|
|
32
|
+
throw err;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const workers = listActiveSubmitWorkers("prod", { dir });
|
|
36
|
+
|
|
37
|
+
expect(workers).toEqual([
|
|
38
|
+
createSubmitWorkerState("prod", 111, "2026-03-21T00:00:00.000Z", 3, 2),
|
|
39
|
+
]);
|
|
40
|
+
expect(() => readFileSync(join(dir, "222.json"), "utf-8")).toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("removes worker state files", () => {
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
|
|
45
|
+
tempDirs.push(dir);
|
|
46
|
+
|
|
47
|
+
writeSubmitWorkerState("prod", createSubmitWorkerState("prod", 333, "2026-03-21T00:00:00.000Z", 2, 1), { dir });
|
|
48
|
+
removeSubmitWorkerState("prod", 333, { dir });
|
|
49
|
+
|
|
50
|
+
expect(() => readFileSync(join(dir, "333.json"), "utf-8")).toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("prunes invalid state payloads", () => {
|
|
54
|
+
const dir = mkdtempSync(join(tmpdir(), "tracemp-submit-workers-"));
|
|
55
|
+
tempDirs.push(dir);
|
|
56
|
+
writeFileSync(join(dir, "bad.json"), JSON.stringify({ nope: true }), "utf-8");
|
|
57
|
+
|
|
58
|
+
expect(listActiveSubmitWorkers("prod", { dir })).toEqual([]);
|
|
59
|
+
expect(() => readFileSync(join(dir, "bad.json"), "utf-8")).toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|