@outerlayer/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +202 -0
- package/README.md +320 -0
- package/dist/build-info.json +1 -0
- package/dist/chunk-4H56Y7O6.js +589 -0
- package/dist/chunk-A3WLZX2F.js +120 -0
- package/dist/chunk-A6NNKRJU.js +650 -0
- package/dist/chunk-D77LS3UI.js +42 -0
- package/dist/chunk-DEDV5V4U.js +87324 -0
- package/dist/chunk-ECSYRPCC.js +334 -0
- package/dist/chunk-IWIUYLDR.js +472 -0
- package/dist/chunk-JJP7YLMN.js +25 -0
- package/dist/chunk-KFYJV2ZG.js +7978 -0
- package/dist/chunk-OZ7C3XUE.js +34 -0
- package/dist/chunk-RCQXYLMO.js +96 -0
- package/dist/chunk-TFUIDMOB.js +34 -0
- package/dist/chunk-VNVZDWO3.js +10 -0
- package/dist/chunk-VU34RWDU.js +339 -0
- package/dist/chunk-WQ6VGRGZ.js +150 -0
- package/dist/chunk-XN2XZFTK.js +165 -0
- package/dist/chunk-YMTXR7PJ.js +200 -0
- package/dist/chunk-YRYMNBGQ.js +54 -0
- package/dist/chunk-Z5GEFMRW.js +45 -0
- package/dist/chunk-ZJSNLVCE.js +58 -0
- package/dist/cli-3JYGL2OB.js +5311 -0
- package/dist/config-POF7DEQW.js +7 -0
- package/dist/context-materialize-J6CRQ7O7.js +589 -0
- package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
- package/dist/emit-cmd-TWSYYEZI.js +208 -0
- package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
- package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
- package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
- package/dist/hook-fast-CBW4ZETX.js +8 -0
- package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
- package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
- package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/init-PTBITAUO.js +103 -0
- package/dist/login-cmd-IRX6LZT7.js +62 -0
- package/dist/logs-TRNPQM42.js +63 -0
- package/dist/loop-NFSOBUUJ.js +646 -0
- package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
- package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
- package/dist/paths-D2VGWWFI.js +6 -0
- package/dist/pidfile-PTW76F56.js +8 -0
- package/dist/status-WGOJTXZF.js +94 -0
- package/dist/statusline-fast-Z5U5CEC6.js +8 -0
- package/dist/sync-cmd-C5UGI4SF.js +15 -0
- package/dist/watch-V3K4PESQ.js +70 -0
- package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
- package/dist/work-cmd-CW26EZZO.js +16 -0
- package/dist/work-launch-YNNCKIH3.js +6 -0
- package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
- package/package.json +58 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { C } from './chunk-YRYMNBGQ.js';
|
|
4
|
+
import { readCloudConfig } from './chunk-TFUIDMOB.js';
|
|
5
|
+
import './chunk-VNVZDWO3.js';
|
|
6
|
+
import './chunk-Z5GEFMRW.js';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
|
|
10
|
+
createRequire(import.meta.url);
|
|
11
|
+
var McpInstallError = class extends Error {
|
|
12
|
+
};
|
|
13
|
+
function defaultMcpUrl(home) {
|
|
14
|
+
const origin = readCloudConfig(home).url;
|
|
15
|
+
if (!origin) {
|
|
16
|
+
throw new McpInstallError(
|
|
17
|
+
"no gateway URL: pass --url <your gateway>, or run `outerlayer login --url <your gateway>` first."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return `${origin.replace(/\/+$/, "")}/v1/mcp`;
|
|
21
|
+
}
|
|
22
|
+
var API_KEY_ENV_PLACEHOLDER = "${OUTERLAYER_API_KEY}";
|
|
23
|
+
var DEFAULT_MCP_TRANSPORT = "stdio";
|
|
24
|
+
var DEFAULT_MCP_COMMAND = "outerlayer";
|
|
25
|
+
function buildServerEntry(url, appId) {
|
|
26
|
+
const headers = { Authorization: `Bearer ${API_KEY_ENV_PLACEHOLDER}` };
|
|
27
|
+
if (appId) headers["X-Outerlayer-App-Id"] = appId;
|
|
28
|
+
return { type: "http", url, headers };
|
|
29
|
+
}
|
|
30
|
+
function buildStdioServerEntry(command, url, appId) {
|
|
31
|
+
const args = ["mcp", "serve"];
|
|
32
|
+
if (url) args.push("--url", url);
|
|
33
|
+
if (appId) args.push("--app-id", appId);
|
|
34
|
+
return { type: "stdio", command, args };
|
|
35
|
+
}
|
|
36
|
+
function runMcpInstall(opts = {}) {
|
|
37
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
38
|
+
if (!existsSync(cwd)) throw new McpInstallError(`no such directory: ${cwd}`);
|
|
39
|
+
const name = opts.name ?? "outerlayer";
|
|
40
|
+
const transport = opts.transport ?? DEFAULT_MCP_TRANSPORT;
|
|
41
|
+
const url = transport === "http" ? opts.url ?? defaultMcpUrl(opts.home) : opts.url ?? null;
|
|
42
|
+
const relPath = ".mcp.json";
|
|
43
|
+
const absPath = join(cwd, relPath);
|
|
44
|
+
let doc = {};
|
|
45
|
+
if (existsSync(absPath)) {
|
|
46
|
+
try {
|
|
47
|
+
doc = JSON.parse(readFileSync(absPath, "utf8"));
|
|
48
|
+
} catch {
|
|
49
|
+
throw new McpInstallError(`${relPath} exists and is not valid JSON \u2014 fix or remove it before running "mcp install"`);
|
|
50
|
+
}
|
|
51
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
52
|
+
throw new McpInstallError(`${relPath} exists and its top level is not a JSON object \u2014 refusing to overwrite`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const existingServers = doc.mcpServers && typeof doc.mcpServers === "object" && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {};
|
|
56
|
+
const entry = transport === "http" ? buildServerEntry(url, opts.appId) : buildStdioServerEntry(opts.command ?? DEFAULT_MCP_COMMAND, url ?? void 0, opts.appId);
|
|
57
|
+
const changed = JSON.stringify(existingServers[name] ?? null) !== JSON.stringify(entry);
|
|
58
|
+
const next = { ...doc, mcpServers: { ...existingServers, [name]: entry } };
|
|
59
|
+
writeFileSync(absPath, `${JSON.stringify(next, null, 2)}
|
|
60
|
+
`);
|
|
61
|
+
const where = transport === "http" ? `${url}` : `stdio: ${opts.command ?? DEFAULT_MCP_COMMAND} mcp serve`;
|
|
62
|
+
const keyNote = transport === "http" ? `Set OUTERLAYER_API_KEY in your environment \u2014 the key is referenced as ${API_KEY_ENV_PLACEHOLDER}, never written to this file.` : "The key is read from ~/.outerlayer/config.json (or OUTERLAYER_API_KEY) each time the client connects \u2014 never written to this file.";
|
|
63
|
+
const output = opts.json ? JSON.stringify({ path: relPath, server: name, transport, url, changed }) : changed ? `${C.green}\u2713${C.reset} Wrote "${name}" MCP server (${where}) to ${relPath}
|
|
64
|
+
${C.dim} ${keyNote}${C.reset}` : `${C.dim}"${name}" MCP server already configured in ${relPath} (no change)${C.reset}`;
|
|
65
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
66
|
+
return { path: relPath, server: name, transport, url, changed, output };
|
|
67
|
+
}
|
|
68
|
+
function handleMcpInstallError(err) {
|
|
69
|
+
if (!(err instanceof McpInstallError)) throw err;
|
|
70
|
+
process.stderr.write(`${C.red}\u2717${C.reset} ${err.message}
|
|
71
|
+
`);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { DEFAULT_MCP_COMMAND, McpInstallError, buildServerEntry, buildStdioServerEntry, defaultMcpUrl, handleMcpInstallError, runMcpInstall };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { C } from './chunk-YRYMNBGQ.js';
|
|
4
|
+
import { readCloudConfig, cloudConfigPath } from './chunk-TFUIDMOB.js';
|
|
5
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
6
|
+
import './chunk-Z5GEFMRW.js';
|
|
7
|
+
import { createInterface } from 'readline';
|
|
8
|
+
|
|
9
|
+
createRequire(import.meta.url);
|
|
10
|
+
var McpServeConfigError = class extends Error {
|
|
11
|
+
};
|
|
12
|
+
var JSON_RPC_PARSE_ERROR = -32700;
|
|
13
|
+
var JSON_RPC_GATEWAY_ERROR = -32e3;
|
|
14
|
+
function mcpEndpointFromOrigin(origin) {
|
|
15
|
+
return `${origin.replace(/\/+$/, "")}/v1/mcp`;
|
|
16
|
+
}
|
|
17
|
+
function resolveMcpServeTarget(opts = {}) {
|
|
18
|
+
const env = opts.env ?? process.env;
|
|
19
|
+
const home = opts.home ?? homeFromEnv(env);
|
|
20
|
+
const config = readCloudConfig(home);
|
|
21
|
+
const apiKey = env.OUTERLAYER_API_KEY || config.apiKey;
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new McpServeConfigError(
|
|
24
|
+
`no API key: set OUTERLAYER_API_KEY or save "apiKey" to ${cloudConfigPath(home)} (run "outerlayer sync" once with --api-key, or mint a key in the dashboard: Settings \u2192 API keys).`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const origin = env.OUTERLAYER_URL || config.url;
|
|
28
|
+
if (!opts.url && !origin) {
|
|
29
|
+
throw new McpServeConfigError(
|
|
30
|
+
`no gateway URL: set OUTERLAYER_URL or save "url" to ${cloudConfigPath(home)} (run \`outerlayer login --url <your gateway>\`), or pass --url.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const url = opts.url ?? mcpEndpointFromOrigin(origin);
|
|
34
|
+
const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? config.appId;
|
|
35
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
36
|
+
if (appId) headers["X-Outerlayer-App-Id"] = appId;
|
|
37
|
+
return { url, headers };
|
|
38
|
+
}
|
|
39
|
+
function errorResponse(id, code, message) {
|
|
40
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
41
|
+
}
|
|
42
|
+
function isNotification(message) {
|
|
43
|
+
return typeof message === "object" && message !== null && !Array.isArray(message) && !Object.hasOwn(message, "id");
|
|
44
|
+
}
|
|
45
|
+
function requestId(message) {
|
|
46
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return null;
|
|
47
|
+
const id = message.id;
|
|
48
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
49
|
+
}
|
|
50
|
+
function gatewayErrorMessage(body) {
|
|
51
|
+
if (typeof body !== "object" || body === null) return null;
|
|
52
|
+
const error = body.error;
|
|
53
|
+
if (typeof error === "object" && error !== null && typeof error.message === "string") {
|
|
54
|
+
return error.message;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
async function bridgeMessage(line, opts) {
|
|
59
|
+
let message;
|
|
60
|
+
try {
|
|
61
|
+
message = JSON.parse(line);
|
|
62
|
+
} catch {
|
|
63
|
+
return errorResponse(null, JSON_RPC_PARSE_ERROR, "Request body is not valid JSON.");
|
|
64
|
+
}
|
|
65
|
+
const notification = isNotification(message);
|
|
66
|
+
const id = requestId(message);
|
|
67
|
+
const doFetch = opts.fetch ?? fetch;
|
|
68
|
+
let status;
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
const response = await doFetch(opts.target.url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { ...opts.target.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
74
|
+
// Forward the client's own bytes: re-serializing could reorder keys
|
|
75
|
+
// or drop values the gateway's schema cares about.
|
|
76
|
+
body: line
|
|
77
|
+
});
|
|
78
|
+
status = response.status;
|
|
79
|
+
text = await response.text();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (notification) return null;
|
|
82
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
83
|
+
return errorResponse(id, JSON_RPC_GATEWAY_ERROR, `OuterLayer gateway unreachable at ${opts.target.url}: ${reason}`);
|
|
84
|
+
}
|
|
85
|
+
if (notification || status === 202 || text.trim() === "") return null;
|
|
86
|
+
let body;
|
|
87
|
+
try {
|
|
88
|
+
body = JSON.parse(text);
|
|
89
|
+
} catch {
|
|
90
|
+
body = void 0;
|
|
91
|
+
}
|
|
92
|
+
if (typeof body === "object" && body !== null && body.jsonrpc === "2.0") {
|
|
93
|
+
return JSON.stringify(body);
|
|
94
|
+
}
|
|
95
|
+
const detail = gatewayErrorMessage(body) ?? text.slice(0, 200);
|
|
96
|
+
const hint = status === 401 ? ` \u2014 check the API key in ${cloudConfigPath(opts.home ?? homeFromEnv())} (or OUTERLAYER_API_KEY)` : "";
|
|
97
|
+
return errorResponse(id, JSON_RPC_GATEWAY_ERROR, `OuterLayer gateway returned HTTP ${status}: ${detail}${hint}`);
|
|
98
|
+
}
|
|
99
|
+
async function runMcpServe(opts = {}) {
|
|
100
|
+
const target = resolveMcpServeTarget(opts);
|
|
101
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
102
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
103
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
104
|
+
const lines = createInterface({ input: stdin, crlfDelay: Infinity });
|
|
105
|
+
for await (const raw of lines) {
|
|
106
|
+
const line = raw.trim();
|
|
107
|
+
if (line === "") continue;
|
|
108
|
+
const task = bridgeMessage(line, { target, fetch: opts.fetch, home: opts.home }).then((out) => {
|
|
109
|
+
if (out !== null) stdout.write(out + "\n");
|
|
110
|
+
});
|
|
111
|
+
inFlight.add(task);
|
|
112
|
+
void task.finally(() => inFlight.delete(task));
|
|
113
|
+
}
|
|
114
|
+
await Promise.all(inFlight);
|
|
115
|
+
}
|
|
116
|
+
function handleMcpServeError(err) {
|
|
117
|
+
if (!(err instanceof McpServeConfigError)) throw err;
|
|
118
|
+
process.stderr.write(`${C.red}\u2717${C.reset} ${err.message}
|
|
119
|
+
`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { McpServeConfigError, bridgeMessage, handleMcpServeError, mcpEndpointFromOrigin, resolveMcpServeTarget, runMcpServe };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { RunnerAlreadyRunningError, claimPidFile, readPidFile, releasePidFile } from './chunk-D77LS3UI.js';
|
|
4
|
+
import './chunk-OZ7C3XUE.js';
|
|
5
|
+
import './chunk-JJP7YLMN.js';
|
|
6
|
+
import './chunk-Z5GEFMRW.js';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { formatDuration, formatClock, relativeToRunner, LEASE_SECONDS, RENEW_INTERVAL_SECONDS } from './chunk-4H56Y7O6.js';
|
|
4
|
+
import './chunk-OZ7C3XUE.js';
|
|
5
|
+
import { runnerDir, jobsDir } from './chunk-JJP7YLMN.js';
|
|
6
|
+
import './chunk-VU34RWDU.js';
|
|
7
|
+
import './chunk-DEDV5V4U.js';
|
|
8
|
+
import './chunk-YRYMNBGQ.js';
|
|
9
|
+
import './chunk-RCQXYLMO.js';
|
|
10
|
+
import './chunk-ZJSNLVCE.js';
|
|
11
|
+
import './chunk-A6NNKRJU.js';
|
|
12
|
+
import './chunk-IWIUYLDR.js';
|
|
13
|
+
import './chunk-YMTXR7PJ.js';
|
|
14
|
+
import './chunk-ECSYRPCC.js';
|
|
15
|
+
import './chunk-TFUIDMOB.js';
|
|
16
|
+
import './chunk-VNVZDWO3.js';
|
|
17
|
+
import './chunk-Z5GEFMRW.js';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
createRequire(import.meta.url);
|
|
21
|
+
var JOB_DIR_WIDTH = 34;
|
|
22
|
+
function truncate(text, width) {
|
|
23
|
+
return text.length <= width ? text : `${text.slice(0, width - 1)}\u2026`;
|
|
24
|
+
}
|
|
25
|
+
function isUnfinished(job) {
|
|
26
|
+
return job.outcome === void 0 && job.endedAt === void 0;
|
|
27
|
+
}
|
|
28
|
+
function table(headers, rows) {
|
|
29
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]?.length ?? 0)));
|
|
30
|
+
const render = (cells) => ` ${cells.map((cell, i) => i === cells.length - 1 ? cell : cell.padEnd(widths[i])).join(" ")}`.trimEnd();
|
|
31
|
+
return [render(headers), ...rows.map(render)];
|
|
32
|
+
}
|
|
33
|
+
function leaseCell(job, now) {
|
|
34
|
+
const sinceMs = now.getTime() - new Date(job.lastRenewAt ?? job.startedAt).getTime();
|
|
35
|
+
if (sinceMs >= LEASE_SECONDS * 1e3) return "expired";
|
|
36
|
+
const untilNext = RENEW_INTERVAL_SECONDS * 1e3 - sinceMs;
|
|
37
|
+
return untilNext <= 0 ? "renew due" : `renews ${formatDuration(untilNext)}`;
|
|
38
|
+
}
|
|
39
|
+
function renderStatus(input) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
const running = input.jobs.filter(({ job }) => isUnfinished(job));
|
|
42
|
+
const finished = input.jobs.filter(({ job }) => !isUnfinished(job)).sort((a, b) => (b.job.endedAt ?? "").localeCompare(a.job.endedAt ?? "")).slice(0, 20);
|
|
43
|
+
const header = input.process.state === "running" ? `runner ${input.host} running, pid ${input.process.pid} \xB7 config ${input.configPath} \xB7 ${running.length} of ${input.concurrency} slots in use` : input.process.state === "stale" ? `runner ${input.host} not running (stale pid file for pid ${input.process.pid}) \xB7 config ${input.configPath}` : `runner ${input.host} not running (pid file absent) \xB7 config ${input.configPath}`;
|
|
44
|
+
lines.push(header, "");
|
|
45
|
+
if (running.length === 0) {
|
|
46
|
+
lines.push("No running jobs.");
|
|
47
|
+
} else {
|
|
48
|
+
const root = runnerDir(input.configPath);
|
|
49
|
+
lines.push(
|
|
50
|
+
...table(
|
|
51
|
+
["ITEM", "QUEUE", "STAGE", "ELAPSED", "STARTED", "LEASE", "JOB DIR"],
|
|
52
|
+
running.map(({ dirName, job }) => [
|
|
53
|
+
`#${job.item}`,
|
|
54
|
+
job.queue,
|
|
55
|
+
// A job with no outcome under a runner that is not running is not
|
|
56
|
+
// running either; nothing is watching it.
|
|
57
|
+
input.process.state === "running" ? job.stage ?? "starting" : "orphaned",
|
|
58
|
+
formatDuration(input.now.getTime() - new Date(job.startedAt).getTime()),
|
|
59
|
+
formatClock(new Date(job.startedAt), input.now),
|
|
60
|
+
input.process.state === "running" ? leaseCell(job, input.now) : "\u2014",
|
|
61
|
+
truncate(relativeToRunner(root, join(jobsDir(input.configPath), dirName)), JOB_DIR_WIDTH)
|
|
62
|
+
])
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!input.recent) {
|
|
67
|
+
lines.push("", "No finished jobs in the last 24h. Use --recent to list them.");
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
lines.push("");
|
|
71
|
+
if (finished.length === 0) {
|
|
72
|
+
lines.push("No finished jobs.");
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
lines.push(
|
|
76
|
+
...table(
|
|
77
|
+
["ITEM", "QUEUE", "OUTCOME", "ENDED", "DURATION"],
|
|
78
|
+
finished.map(({ job }) => [
|
|
79
|
+
`#${job.item}`,
|
|
80
|
+
job.queue,
|
|
81
|
+
// A lease released with no outcome is an item this host declined
|
|
82
|
+
// before it ever started; "no outcome" describes the field rather
|
|
83
|
+
// than what happened. A release that never landed is marked,
|
|
84
|
+
// because the platform's own record of this attempt is missing.
|
|
85
|
+
`${job.outcome ?? "not attempted"}${job.releaseFailed === void 0 ? "" : " (release failed)"}`,
|
|
86
|
+
job.endedAt === void 0 ? "\u2014" : formatClock(new Date(job.endedAt), input.now),
|
|
87
|
+
job.endedAt === void 0 ? "\u2014" : formatDuration(new Date(job.endedAt).getTime() - new Date(job.startedAt).getTime())
|
|
88
|
+
])
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { renderStatus };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { STATE_FRESH_MS, WRAP_TIMEOUT_MS, localDateString, parseStatuslineArgs, parseStatuslineStdin, readStatuslineState, renderStatusline, runStatuslineFast, runWrappedStatusline, statuslineStatePath } from './chunk-XN2XZFTK.js';
|
|
4
|
+
import './chunk-ECSYRPCC.js';
|
|
5
|
+
import './chunk-VNVZDWO3.js';
|
|
6
|
+
import './chunk-Z5GEFMRW.js';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { CHUNK_PART_TARGET_BYTES, RETRYABLE_STATUSES, SyncConfigError, SyncTransportError, detectCi, isLoopbackUrl, resolveTier, runSync } from './chunk-DEDV5V4U.js';
|
|
4
|
+
import './chunk-YRYMNBGQ.js';
|
|
5
|
+
import './chunk-RCQXYLMO.js';
|
|
6
|
+
import './chunk-ZJSNLVCE.js';
|
|
7
|
+
import './chunk-A6NNKRJU.js';
|
|
8
|
+
import './chunk-IWIUYLDR.js';
|
|
9
|
+
import './chunk-YMTXR7PJ.js';
|
|
10
|
+
import './chunk-ECSYRPCC.js';
|
|
11
|
+
export { cloudConfigPath, readCloudConfig, writeCloudConfig } from './chunk-TFUIDMOB.js';
|
|
12
|
+
import './chunk-VNVZDWO3.js';
|
|
13
|
+
import './chunk-Z5GEFMRW.js';
|
|
14
|
+
|
|
15
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { StatuslineAggregator, CaptureDaemon, LiveUploader } from './chunk-DEDV5V4U.js';
|
|
4
|
+
import './chunk-YRYMNBGQ.js';
|
|
5
|
+
import './chunk-RCQXYLMO.js';
|
|
6
|
+
import './chunk-ZJSNLVCE.js';
|
|
7
|
+
import './chunk-A6NNKRJU.js';
|
|
8
|
+
import './chunk-IWIUYLDR.js';
|
|
9
|
+
import './chunk-YMTXR7PJ.js';
|
|
10
|
+
import './chunk-ECSYRPCC.js';
|
|
11
|
+
import './chunk-TFUIDMOB.js';
|
|
12
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
13
|
+
import './chunk-Z5GEFMRW.js';
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
|
|
17
|
+
createRequire(import.meta.url);
|
|
18
|
+
function makeWatchLogHandler(statusline, stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), uploader) {
|
|
19
|
+
return (e) => {
|
|
20
|
+
if (e.type === "mirrored") {
|
|
21
|
+
stdout(`mirrored ${e.file}
|
|
22
|
+
`);
|
|
23
|
+
statusline.notifyChange();
|
|
24
|
+
uploader?.notify(e.file);
|
|
25
|
+
}
|
|
26
|
+
if (e.type === "rescan") statusline.notifyChange();
|
|
27
|
+
if (e.type === "evicted") stdout(`evicted ${e.file}
|
|
28
|
+
`);
|
|
29
|
+
if (e.type === "error") stderr(`error ${e.file}: ${e.detail}
|
|
30
|
+
`);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function runWatch(opts = {}) {
|
|
34
|
+
const home = opts.home ?? homeFromEnv();
|
|
35
|
+
const stateDir = join(home, ".outerlayer");
|
|
36
|
+
mkdirSync(stateDir, { recursive: true });
|
|
37
|
+
const statusline = new StatuslineAggregator({ home });
|
|
38
|
+
if (opts.once) {
|
|
39
|
+
const daemon2 = new CaptureDaemon({ onLog: makeWatchLogHandler(statusline) });
|
|
40
|
+
daemon2.rescanNow();
|
|
41
|
+
statusline.stop();
|
|
42
|
+
process.stdout.write("Single sweep complete.\n");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const uploader = new LiveUploader({ home });
|
|
46
|
+
const daemon = new CaptureDaemon({ onLog: makeWatchLogHandler(statusline, void 0, void 0, uploader) });
|
|
47
|
+
writeFileSync(join(stateDir, "daemon.pid"), String(process.pid));
|
|
48
|
+
const heartbeat = join(stateDir, "daemon.heartbeat");
|
|
49
|
+
const beat = () => writeFileSync(heartbeat, (/* @__PURE__ */ new Date()).toISOString());
|
|
50
|
+
beat();
|
|
51
|
+
const heartbeatTimer = setInterval(beat, 6e4);
|
|
52
|
+
await daemon.start();
|
|
53
|
+
statusline.refresh();
|
|
54
|
+
process.stdout.write("OuterLayer daemon watching ~/.claude/projects (Ctrl-C to stop)\n");
|
|
55
|
+
const shutdown = async () => {
|
|
56
|
+
clearInterval(heartbeatTimer);
|
|
57
|
+
await daemon.stop();
|
|
58
|
+
statusline.stop();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
};
|
|
61
|
+
process.on("SIGINT", shutdown);
|
|
62
|
+
process.on("SIGTERM", shutdown);
|
|
63
|
+
process.on("unhandledRejection", (reason) => {
|
|
64
|
+
process.stderr.write(`daemon: unhandled error, continuing \u2014 ${String(reason)}
|
|
65
|
+
`);
|
|
66
|
+
});
|
|
67
|
+
await new Promise(() => void 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { makeWatchLogHandler, runWatch };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { resolveCredentials, WorkCommandError, parsePositiveInt, postJson, resolveRepository, findLiveItem } from './chunk-VU34RWDU.js';
|
|
4
|
+
export { WorkCommandError } from './chunk-VU34RWDU.js';
|
|
5
|
+
import './chunk-DEDV5V4U.js';
|
|
6
|
+
import './chunk-YRYMNBGQ.js';
|
|
7
|
+
import './chunk-RCQXYLMO.js';
|
|
8
|
+
import './chunk-ZJSNLVCE.js';
|
|
9
|
+
import './chunk-A6NNKRJU.js';
|
|
10
|
+
import './chunk-IWIUYLDR.js';
|
|
11
|
+
import './chunk-YMTXR7PJ.js';
|
|
12
|
+
import './chunk-ECSYRPCC.js';
|
|
13
|
+
import './chunk-TFUIDMOB.js';
|
|
14
|
+
import './chunk-VNVZDWO3.js';
|
|
15
|
+
import './chunk-Z5GEFMRW.js';
|
|
16
|
+
import { hostname } from 'os';
|
|
17
|
+
|
|
18
|
+
createRequire(import.meta.url);
|
|
19
|
+
var DEFAULT_LEASE_SECONDS = 900;
|
|
20
|
+
var CLAIM_OUTCOMES = ["ok", "failed", "timed_out", "idle", "lease_lost", "interrupted", "stopped"];
|
|
21
|
+
async function resolveItemIdentifier(opts, creds, cwd, fetchImpl) {
|
|
22
|
+
const itemNumber = parsePositiveInt(opts.item, "--item");
|
|
23
|
+
if (itemNumber !== void 0) return itemNumber;
|
|
24
|
+
const issue = parsePositiveInt(opts.issue, "--issue");
|
|
25
|
+
if (issue === void 0) {
|
|
26
|
+
throw new WorkCommandError("pass --item <number> or --issue <number>");
|
|
27
|
+
}
|
|
28
|
+
const repository = resolveRepository(opts.repo, cwd);
|
|
29
|
+
const found = await findLiveItem(creds, repository, { issue }, fetchImpl);
|
|
30
|
+
if (!found) {
|
|
31
|
+
throw new WorkCommandError(`no live item for ${repository}#${issue}`);
|
|
32
|
+
}
|
|
33
|
+
return found.id;
|
|
34
|
+
}
|
|
35
|
+
async function runWorkClaim(opts) {
|
|
36
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
37
|
+
const creds = resolveCredentials(opts);
|
|
38
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
39
|
+
if (opts.kind !== "implement" && opts.kind !== "amend") {
|
|
40
|
+
throw new WorkCommandError('--kind is required \u2014 pass "implement" or "amend"');
|
|
41
|
+
}
|
|
42
|
+
const host = opts.host ?? hostname();
|
|
43
|
+
const seconds = parsePositiveInt(opts.seconds, "--seconds") ?? DEFAULT_LEASE_SECONDS;
|
|
44
|
+
const itemId = await resolveItemIdentifier(opts, creds, cwd, fetchImpl);
|
|
45
|
+
const { data } = await postJson(
|
|
46
|
+
creds.url,
|
|
47
|
+
`/v1/work-items/${itemId}/claim`,
|
|
48
|
+
creds.apiKey,
|
|
49
|
+
creds.appId,
|
|
50
|
+
{ host, kind: opts.kind, seconds },
|
|
51
|
+
fetchImpl
|
|
52
|
+
);
|
|
53
|
+
const output = opts.json ? JSON.stringify(data) : `claimed until ${String(data.expiresAt)}`;
|
|
54
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
55
|
+
return { data, output };
|
|
56
|
+
}
|
|
57
|
+
async function runWorkRenew(opts) {
|
|
58
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
59
|
+
const creds = resolveCredentials(opts);
|
|
60
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
61
|
+
const host = opts.host ?? hostname();
|
|
62
|
+
const seconds = parsePositiveInt(opts.seconds, "--seconds") ?? DEFAULT_LEASE_SECONDS;
|
|
63
|
+
const itemId = await resolveItemIdentifier(opts, creds, cwd, fetchImpl);
|
|
64
|
+
const { data } = await postJson(
|
|
65
|
+
creds.url,
|
|
66
|
+
`/v1/work-items/${itemId}/claim/renew`,
|
|
67
|
+
creds.apiKey,
|
|
68
|
+
creds.appId,
|
|
69
|
+
{ host, seconds },
|
|
70
|
+
fetchImpl
|
|
71
|
+
);
|
|
72
|
+
const output = opts.json ? JSON.stringify(data) : `claimed until ${String(data.expiresAt)}`;
|
|
73
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
74
|
+
return { data, output };
|
|
75
|
+
}
|
|
76
|
+
async function runWorkRelease(opts) {
|
|
77
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
78
|
+
const creds = resolveCredentials(opts);
|
|
79
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
80
|
+
if (opts.outcome !== void 0 && !CLAIM_OUTCOMES.includes(opts.outcome)) {
|
|
81
|
+
throw new WorkCommandError(`--outcome must be one of: ${CLAIM_OUTCOMES.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
const host = opts.host ?? hostname();
|
|
84
|
+
const itemId = await resolveItemIdentifier(opts, creds, cwd, fetchImpl);
|
|
85
|
+
const { data } = await postJson(
|
|
86
|
+
creds.url,
|
|
87
|
+
`/v1/work-items/${itemId}/claim/release`,
|
|
88
|
+
creds.apiKey,
|
|
89
|
+
creds.appId,
|
|
90
|
+
opts.outcome === void 0 ? { host } : { host, outcome: opts.outcome },
|
|
91
|
+
fetchImpl
|
|
92
|
+
);
|
|
93
|
+
const output = opts.json ? JSON.stringify(data) : `released at ${String(data.releasedAt)}`;
|
|
94
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
95
|
+
return { data, output };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { runWorkClaim, runWorkRelease, runWorkRenew };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { WorkCommandError, findLiveItem, getJson, parsePositiveInt, postJson, resolveCredentials, resolveRepository, runWorkAdd, runWorkLinkSession, runWorkList, runWorkRemove, runWorkStatus } from './chunk-VU34RWDU.js';
|
|
4
|
+
import './chunk-DEDV5V4U.js';
|
|
5
|
+
import './chunk-YRYMNBGQ.js';
|
|
6
|
+
import './chunk-RCQXYLMO.js';
|
|
7
|
+
import './chunk-ZJSNLVCE.js';
|
|
8
|
+
import './chunk-A6NNKRJU.js';
|
|
9
|
+
import './chunk-IWIUYLDR.js';
|
|
10
|
+
import './chunk-YMTXR7PJ.js';
|
|
11
|
+
import './chunk-ECSYRPCC.js';
|
|
12
|
+
import './chunk-TFUIDMOB.js';
|
|
13
|
+
import './chunk-VNVZDWO3.js';
|
|
14
|
+
import './chunk-Z5GEFMRW.js';
|
|
15
|
+
|
|
16
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { appendLaunchRecord, launchStatusIsDecisive, launchedItemForSession, launchesPath, parseWorkItemEnv, readLaunchSpoolState } from './chunk-RCQXYLMO.js';
|
|
4
|
+
import './chunk-Z5GEFMRW.js';
|
|
5
|
+
|
|
6
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { resolveRepoIdentity, detectActiveSession } from './chunk-DEDV5V4U.js';
|
|
4
|
+
import './chunk-YRYMNBGQ.js';
|
|
5
|
+
import { parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
|
|
6
|
+
import './chunk-ZJSNLVCE.js';
|
|
7
|
+
import './chunk-A6NNKRJU.js';
|
|
8
|
+
import './chunk-IWIUYLDR.js';
|
|
9
|
+
import './chunk-YMTXR7PJ.js';
|
|
10
|
+
import './chunk-ECSYRPCC.js';
|
|
11
|
+
import { readCloudConfig } from './chunk-TFUIDMOB.js';
|
|
12
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
13
|
+
import './chunk-Z5GEFMRW.js';
|
|
14
|
+
|
|
15
|
+
createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
// src/work-pr-cmd.ts
|
|
18
|
+
var GREEN = "\x1B[32m";
|
|
19
|
+
var DIM = "\x1B[2m";
|
|
20
|
+
var RESET = "\x1B[0m";
|
|
21
|
+
var WorkPrCommandError = class extends Error {
|
|
22
|
+
};
|
|
23
|
+
function resolveCredentials(opts) {
|
|
24
|
+
const home = opts.home ?? homeFromEnv();
|
|
25
|
+
const env = opts.env ?? process.env;
|
|
26
|
+
const fileConfig = readCloudConfig(home);
|
|
27
|
+
const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
|
|
28
|
+
const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
|
|
29
|
+
const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
|
|
30
|
+
if (!url || !apiKey || !appId) {
|
|
31
|
+
const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
|
|
32
|
+
throw new WorkPrCommandError(
|
|
33
|
+
`missing ${missing}. Run \`outerlayer login --url <your gateway>\` and paste the key at the prompt, or set OUTERLAYER_URL / OUTERLAYER_API_KEY / OUTERLAYER_APP_ID. Mint a key in the dashboard: Settings \u2192 API keys.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return { url, apiKey, appId };
|
|
37
|
+
}
|
|
38
|
+
function resolveRepository(repoFlag, cwd) {
|
|
39
|
+
if (repoFlag) return repoFlag;
|
|
40
|
+
const identity = resolveRepoIdentity(cwd);
|
|
41
|
+
if (identity.gitRepo) return identity.gitRepo;
|
|
42
|
+
throw new WorkPrCommandError(
|
|
43
|
+
"no repository. Pass --repo <owner/name>, or run from a git checkout with a remote."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
function resolvePrNumber(raw) {
|
|
47
|
+
const n = parseTrackerNumber(raw);
|
|
48
|
+
if (n === void 0) throw new WorkPrCommandError(trackerNumberError("<n>", raw ?? ""));
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
async function runWorkPr(opts) {
|
|
52
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
53
|
+
const env = opts.env ?? process.env;
|
|
54
|
+
const home = opts.home ?? homeFromEnv(env);
|
|
55
|
+
const creds = resolveCredentials(opts);
|
|
56
|
+
const prNumber = resolvePrNumber(opts.number);
|
|
57
|
+
const repository = resolveRepository(opts.repo, cwd);
|
|
58
|
+
const identity = resolveRepoIdentity(cwd);
|
|
59
|
+
const sessionId = opts.sessionId ?? detectActiveSession({
|
|
60
|
+
env,
|
|
61
|
+
home,
|
|
62
|
+
cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, cwd] : [cwd],
|
|
63
|
+
nowMs: Date.now()
|
|
64
|
+
});
|
|
65
|
+
if (sessionId === void 0) {
|
|
66
|
+
throw new WorkPrCommandError(
|
|
67
|
+
"no active session detected. `work pr` declares a pull request from inside the session that opened it."
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
71
|
+
const endpoint = new URL("/v1/work-items/pull-requests", creds.url).toString();
|
|
72
|
+
let response;
|
|
73
|
+
try {
|
|
74
|
+
response = await fetchImpl(endpoint, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"content-type": "application/json",
|
|
78
|
+
authorization: `Bearer ${creds.apiKey}`,
|
|
79
|
+
"x-outerlayer-app-id": creds.appId
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify({ sessionId, pullRequest: { repository, number: prNumber } })
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw new WorkPrCommandError(`network error reaching ${endpoint}: ${String(err)}`);
|
|
85
|
+
}
|
|
86
|
+
if (response.status === 401) {
|
|
87
|
+
throw new WorkPrCommandError(
|
|
88
|
+
"not authorized \u2014 the API key is unknown, expired, or bound to a different app. Mint a fresh key: dashboard \u2192 Settings \u2192 API keys."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
let json;
|
|
92
|
+
try {
|
|
93
|
+
json = await response.json();
|
|
94
|
+
} catch {
|
|
95
|
+
throw new WorkPrCommandError(
|
|
96
|
+
`${endpoint} answered ${response.status} with a body that is not JSON \u2014 the request may not have reached the gateway`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok || !json.data) {
|
|
100
|
+
if (json.error?.code === "session_not_on_item") {
|
|
101
|
+
throw new WorkPrCommandError("this session is not launched on a work item.");
|
|
102
|
+
}
|
|
103
|
+
if (json.error?.code === "repository_not_connected") {
|
|
104
|
+
throw new WorkPrCommandError(`${repository} is not a workpiece of this factory.`);
|
|
105
|
+
}
|
|
106
|
+
throw new WorkPrCommandError(json.error?.message ?? `request failed (${response.status})`);
|
|
107
|
+
}
|
|
108
|
+
const data = json.data;
|
|
109
|
+
const verb = data.created ? "declared" : "already declared";
|
|
110
|
+
const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} ${verb} ${DIM}\xB7${RESET} ${data.repository}#${data.prNumber} ${DIM}\u2192${RESET} ${data.workItemId}`;
|
|
111
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
112
|
+
return { data, output };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { WorkPrCommandError, runWorkPr };
|