@outerlayer/cli 0.2.0 → 0.4.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 +124 -0
- package/README.md +226 -59
- package/dist/agent-setup-IFH3FB5X.js +197 -0
- package/dist/build-TZUUTRGH.js +9 -0
- package/dist/build-info.json +1 -1
- package/dist/check-WQP2CZDW.js +94 -0
- package/dist/chunk-2E54Z3P5.js +232 -0
- package/dist/chunk-3TT7RQYB.js +55 -0
- package/dist/{chunk-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
- package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -61
- package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
- package/dist/chunk-5Y3QQRUH.js +300 -0
- package/dist/chunk-6Y63SXFO.js +383 -0
- package/dist/chunk-774HEQL6.js +178 -0
- package/dist/chunk-7A6RVXPZ.js +2233 -0
- package/dist/chunk-7YIOOFVJ.js +71 -0
- package/dist/chunk-ABIWDUMS.js +104 -0
- package/dist/chunk-AUFG23AA.js +534 -0
- package/dist/chunk-B2G7JHHB.js +42 -0
- package/dist/chunk-BCJNHMZT.js +34 -0
- package/dist/chunk-BCLJSQCV.js +56 -0
- package/dist/chunk-BFESLKPP.js +61 -0
- package/dist/chunk-BJ3KTMDH.js +63 -0
- package/dist/chunk-BKO6JEZI.js +47 -0
- package/dist/chunk-BOLTI6LR.js +37 -0
- package/dist/chunk-CBPPJSAR.js +89 -0
- package/dist/chunk-CBZB6XY6.js +102 -0
- package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
- package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
- package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
- package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
- package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
- package/dist/chunk-F6GFZXZ3.js +60 -0
- package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
- package/dist/chunk-FAJSS2WN.js +1357 -0
- package/dist/chunk-FS2VYGZT.js +1764 -0
- package/dist/chunk-FV2CCGXK.js +9 -0
- package/dist/{chunk-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
- package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
- package/dist/chunk-HE2D4EY5.js +83 -0
- package/dist/chunk-L2DBRKRA.js +94 -0
- package/dist/chunk-LF6MLJCY.js +38 -0
- package/dist/chunk-M5POMKKW.js +1051 -0
- package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
- package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
- package/dist/chunk-N6LURUEF.js +64 -0
- package/dist/chunk-NYC54VBD.js +1084 -0
- package/dist/chunk-OGFGZQA3.js +23 -0
- package/dist/chunk-QOZKPLVJ.js +226 -0
- package/dist/chunk-RA3O54FC.js +30 -0
- package/dist/chunk-TBT347UY.js +41 -0
- package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
- package/dist/chunk-USO2DKBD.js +421 -0
- package/dist/chunk-W4SQCOZU.js +206 -0
- package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
- package/dist/chunk-WO2BXCTQ.js +83 -0
- package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
- package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
- package/dist/chunk-YYYXRJUW.js +101 -0
- package/dist/chunk-ZM2IMMYN.js +76 -0
- package/dist/chunk-ZMYPLFG3.js +71 -0
- package/dist/chunk-ZNA27WEV.js +470 -0
- package/dist/{cli-G7DILYJY.js → cli-NHUXABYH.js} +590 -876
- package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
- package/dist/config-XVJYZ4IQ.js +9 -0
- package/dist/connect-cmd-OOG4TPU7.js +16 -0
- package/dist/context-adopt-UMA4O5NY.js +105 -0
- package/dist/context-materialize-G2FUFC72.js +19 -0
- package/dist/dist-RW4UPPC2.js +6 -0
- package/dist/docker-NEGDLU6D.js +7 -0
- package/dist/doctor-53F2PYY7.js +43 -0
- package/dist/doctor-UFVL4PZY.js +426 -0
- package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
- package/dist/emit-cmd-DCERAU27.js +9 -0
- package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
- package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
- package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.js} +34 -62
- package/dist/exec-client-4XGXV3XN.js +8 -0
- package/dist/guest-init-GENRHNP7.js +8 -0
- package/dist/hook-fast-EYENPK6F.js +9 -0
- package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
- package/dist/host-key-KDYZ5ECC.js +7 -0
- package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
- package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/init-DXBCOAJK.js +36 -0
- package/dist/init-YEXK35CF.js +221 -0
- package/dist/init-cmd-NHR7PRSQ.js +135 -0
- package/dist/install-cmd-7WXOIFDO.js +55 -0
- package/dist/lima-XN65D7GN.js +55 -0
- package/dist/login-browser-7XGSV7MA.js +10 -0
- package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
- package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
- package/dist/loop-VZOLL4WQ.js +43 -0
- package/dist/machine-WSG52J75.js +35 -0
- package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
- package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
- package/dist/paths-OYKMVYJP.js +6 -0
- package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
- package/dist/real-deps-SU24ZA2K.js +21 -0
- package/dist/relay-L76HDX72.js +46 -0
- package/dist/settings-J2652U5N.js +7 -0
- package/dist/starter-pack-LIZYMKYQ.js +10 -0
- package/dist/{status-I27IST4L.js → status-2UZKKNB7.js} +30 -13
- package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
- package/dist/sync-cmd-VPPYCNNM.js +28 -0
- package/dist/version-BWM6VLDI.js +6 -0
- package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
- package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
- package/dist/work-cmd-UOQO2AD4.js +17 -0
- package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
- package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
- package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
- package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
- package/package.json +13 -3
- package/skill-pack/maintained/amend/SKILL.md +104 -0
- package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
- package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
- package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
- package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
- package/skill-pack/template/build/SKILL.md +193 -0
- package/skill-pack/template/build/references/agent-briefs.md +243 -0
- package/skill-pack/template/build/references/criteria-judge.md +91 -0
- package/skill-pack/template/build/references/evidence.md +42 -0
- package/skill-pack/template/build/references/release.md +74 -0
- package/skill-pack/template/build/references/review-briefs.md +275 -0
- package/skill-pack/template/build/references/review-loop.md +158 -0
- package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
- package/skill-pack/template/spec/SKILL.md +84 -0
- package/skill-pack/template/writing-specs/SKILL.md +134 -0
- package/dist/chunk-DCNOXRMV.js +0 -589
- package/dist/chunk-JJP7YLMN.js +0 -25
- package/dist/chunk-OZ7C3XUE.js +0 -34
- package/dist/chunk-WQ6VGRGZ.js +0 -150
- package/dist/hook-fast-J5LCDHUJ.js +0 -8
- package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
- package/dist/init-PTBITAUO.js +0 -103
- package/dist/login-cmd-IRX6LZT7.js +0 -62
- package/dist/loop-ASZRX3CZ.js +0 -648
- package/dist/sync-cmd-BBAUZ5JD.js +0 -17
- package/dist/work-cmd-2P4BVX47.js +0 -18
|
@@ -1,17 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { detectActiveSession } from './chunk-
|
|
4
|
-
import {
|
|
3
|
+
import { detectActiveSession } from './chunk-BFESLKPP.js';
|
|
4
|
+
import { sanitizeTerminal } from './chunk-6Y63SXFO.js';
|
|
5
|
+
import { resolveRepoIdentity } from './chunk-G7FSV7JX.js';
|
|
5
6
|
import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
|
|
6
|
-
import {
|
|
7
|
+
import { defaultFetch } from './chunk-BCLJSQCV.js';
|
|
8
|
+
import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-BOLTI6LR.js';
|
|
9
|
+
import { signRunnerRequest } from './chunk-M5POMKKW.js';
|
|
7
10
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
8
11
|
|
|
9
12
|
createRequire(import.meta.url);
|
|
10
13
|
|
|
14
|
+
// src/runner/signature-refusals.ts
|
|
15
|
+
var MESSAGES = /* @__PURE__ */ new Map([
|
|
16
|
+
[
|
|
17
|
+
"runner_key_signature_required",
|
|
18
|
+
"the gateway wants a signature on every request from this runner key, and this one had none. Upgrade the CLI, and check that the runner's host key file exists"
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"runner_key_signature_invalid",
|
|
22
|
+
"this runner key is bound to another host's key, or the request was altered after it was signed. If the key moved to this machine, a member clears the old binding on the factory's API keys page (Clear host key), and the next claim binds this host"
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"runner_key_signature_stale",
|
|
26
|
+
"the signature's time is more than five minutes from the gateway's clock. The clock of this host is the likely cause: check that it is set correctly and synchronised"
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"runner_key_signature_replayed",
|
|
30
|
+
"the gateway already accepted this exact signed request, so it was refused as a replay. Something is sending copies of the runner's requests; restart the runner"
|
|
31
|
+
]
|
|
32
|
+
]);
|
|
33
|
+
function signatureRefusalMessage(code) {
|
|
34
|
+
return MESSAGES.get(code);
|
|
35
|
+
}
|
|
36
|
+
function isSignatureRefusal(code) {
|
|
37
|
+
return MESSAGES.has(code);
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
// src/work-cmd.ts
|
|
12
41
|
var GREEN = "\x1B[32m";
|
|
42
|
+
var YELLOW = "\x1B[33m";
|
|
13
43
|
var DIM = "\x1B[2m";
|
|
14
44
|
var RESET = "\x1B[0m";
|
|
45
|
+
async function signatureHeaders(method, endpoint, payload, signer) {
|
|
46
|
+
if (signer === void 0) return {};
|
|
47
|
+
return { ...await signRunnerRequest({ method, url: endpoint, body: new TextEncoder().encode(payload), signer }) };
|
|
48
|
+
}
|
|
49
|
+
async function signatureRefusalOf(response) {
|
|
50
|
+
let code;
|
|
51
|
+
try {
|
|
52
|
+
code = (await response.json()).error.code;
|
|
53
|
+
} catch {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
const message = signatureRefusalMessage(code);
|
|
57
|
+
return message === void 0 ? void 0 : new WorkCommandError(`${code}: ${message}`, false, 401, code);
|
|
58
|
+
}
|
|
15
59
|
var WorkCommandError = class extends Error {
|
|
16
60
|
/** True when the same request is worth sending again: the gateway was
|
|
17
61
|
* unreachable, answered a body that never came from it, or failed in a way
|
|
@@ -22,27 +66,18 @@ var WorkCommandError = class extends Error {
|
|
|
22
66
|
* reached a response. A caller that has to tell one refusal from another —
|
|
23
67
|
* a claim lost to another host from a key that no longer works — reads it
|
|
24
68
|
* rather than matching on the message. */
|
|
25
|
-
constructor(message, retryable = false, status) {
|
|
69
|
+
constructor(message, retryable = false, status, code) {
|
|
26
70
|
super(message);
|
|
27
71
|
this.retryable = retryable;
|
|
28
72
|
this.status = status;
|
|
73
|
+
this.code = code;
|
|
29
74
|
}
|
|
30
75
|
};
|
|
31
|
-
var
|
|
76
|
+
var WORK_LINK_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 6e4];
|
|
32
77
|
function resolveCredentials(opts) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
|
|
37
|
-
const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
|
|
38
|
-
const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
|
|
39
|
-
if (!url || !apiKey || !appId) {
|
|
40
|
-
const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
|
|
41
|
-
throw new WorkCommandError(
|
|
42
|
-
`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.`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
return { url, apiKey, appId };
|
|
78
|
+
const resolved = resolveGatewayCredentials(opts);
|
|
79
|
+
if (!resolved.ok) throw new WorkCommandError(resolved.message);
|
|
80
|
+
return { url: resolved.url, apiKey: resolved.credential, appId: resolved.appId };
|
|
46
81
|
}
|
|
47
82
|
function resolveRepository(repoFlag, cwd) {
|
|
48
83
|
if (repoFlag) return repoFlag;
|
|
@@ -60,7 +95,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
60
95
|
}
|
|
61
96
|
function parseRetries(raw) {
|
|
62
97
|
const n = parseTrackerNumber(raw);
|
|
63
|
-
return n === void 0 ? 0 : Math.min(n,
|
|
98
|
+
return n === void 0 ? 0 : Math.min(n, WORK_LINK_RETRY_DELAYS_MS.length);
|
|
64
99
|
}
|
|
65
100
|
function resolveSubject(opts) {
|
|
66
101
|
const issue = parsePositiveInt(opts.issue, "--issue");
|
|
@@ -69,33 +104,74 @@ function resolveSubject(opts) {
|
|
|
69
104
|
}
|
|
70
105
|
return { issue };
|
|
71
106
|
}
|
|
72
|
-
|
|
107
|
+
var TRACKER_FLAG_HELP = 'expected "linear" or "jira" \u2014 GitHub issues are named by number, with no --tracker';
|
|
108
|
+
function resolveWorkSubject(opts) {
|
|
109
|
+
if (opts.issue === void 0) {
|
|
110
|
+
throw new WorkCommandError("pass --issue");
|
|
111
|
+
}
|
|
112
|
+
const asNumber = parseTrackerNumber(opts.issue);
|
|
113
|
+
if (asNumber !== void 0) {
|
|
114
|
+
if (opts.tracker !== void 0) {
|
|
115
|
+
throw new WorkCommandError(`--tracker cannot be used with --issue ${opts.issue}, a plain number: ${TRACKER_FLAG_HELP}`);
|
|
116
|
+
}
|
|
117
|
+
return { repository: resolveRepository(opts.repo, opts.cwd), issue: asNumber };
|
|
118
|
+
}
|
|
119
|
+
if (opts.tracker !== void 0 && opts.tracker !== "linear" && opts.tracker !== "jira") {
|
|
120
|
+
throw new WorkCommandError(`invalid --tracker "${opts.tracker}": ${TRACKER_FLAG_HELP}`);
|
|
121
|
+
}
|
|
122
|
+
return { key: String(opts.issue), tracker: opts.tracker, repository: opts.repo };
|
|
123
|
+
}
|
|
124
|
+
async function postJson(url, path, apiKey, appId, body, fetchImpl, timeoutMs, signer) {
|
|
125
|
+
const deadline = timeoutMs === void 0 ? void 0 : startDeadline(timeoutMs);
|
|
126
|
+
try {
|
|
127
|
+
return await postJsonBefore(url, path, apiKey, appId, body, fetchImpl, deadline, signer);
|
|
128
|
+
} finally {
|
|
129
|
+
deadline?.clear();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function startDeadline(ms) {
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const timer = setTimeout(() => controller.abort(), ms);
|
|
135
|
+
return { signal: controller.signal, ms, clear: () => clearTimeout(timer) };
|
|
136
|
+
}
|
|
137
|
+
async function postJsonBefore(url, path, apiKey, appId, body, fetchImpl, deadline, signer) {
|
|
73
138
|
let response;
|
|
74
139
|
const endpoint = new URL(path, url).toString();
|
|
140
|
+
const payload = JSON.stringify(body);
|
|
141
|
+
const signature = await signatureHeaders("POST", endpoint, payload ?? "", signer);
|
|
75
142
|
try {
|
|
76
143
|
response = await fetchImpl(endpoint, {
|
|
77
144
|
method: "POST",
|
|
78
145
|
headers: {
|
|
79
146
|
"content-type": "application/json",
|
|
80
147
|
authorization: `Bearer ${apiKey}`,
|
|
81
|
-
"x-outerlayer-app-id": appId
|
|
148
|
+
"x-outerlayer-app-id": appId,
|
|
149
|
+
...signature
|
|
82
150
|
},
|
|
83
|
-
body:
|
|
151
|
+
body: payload,
|
|
152
|
+
...deadline ? { signal: deadline.signal } : {}
|
|
84
153
|
});
|
|
85
154
|
} catch (err) {
|
|
86
155
|
throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`, true);
|
|
87
156
|
}
|
|
88
157
|
if (response.status === 401) {
|
|
89
|
-
throw new WorkCommandError(
|
|
90
|
-
|
|
158
|
+
throw await signatureRefusalOf(response) ?? new WorkCommandError(
|
|
159
|
+
unauthorizedMessage(apiKey),
|
|
91
160
|
false,
|
|
92
161
|
401
|
|
93
162
|
);
|
|
94
163
|
}
|
|
95
164
|
let json;
|
|
96
165
|
try {
|
|
97
|
-
json = await response
|
|
98
|
-
} catch {
|
|
166
|
+
json = await readBodyBefore(response, deadline);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err instanceof BodyDeadlineError) {
|
|
169
|
+
throw new WorkCommandError(
|
|
170
|
+
`${endpoint} answered ${response.status} but did not finish its body within ${err.ms}ms`,
|
|
171
|
+
true,
|
|
172
|
+
response.status
|
|
173
|
+
);
|
|
174
|
+
}
|
|
99
175
|
throw new WorkCommandError(
|
|
100
176
|
`${endpoint} answered ${response.status} with a body that is not JSON \u2014 the request may not have reached the gateway`,
|
|
101
177
|
true,
|
|
@@ -106,25 +182,48 @@ async function postJson(url, path, apiKey, appId, body, fetchImpl) {
|
|
|
106
182
|
throw new WorkCommandError(
|
|
107
183
|
json.error?.message ?? `request failed (${response.status})`,
|
|
108
184
|
response.status >= 500 || response.status === 429,
|
|
109
|
-
response.status
|
|
185
|
+
response.status,
|
|
186
|
+
json.error?.code
|
|
110
187
|
);
|
|
111
188
|
}
|
|
112
189
|
return { status: response.status, data: json.data ?? json };
|
|
113
190
|
}
|
|
114
|
-
|
|
191
|
+
var BodyDeadlineError = class extends Error {
|
|
192
|
+
constructor(ms) {
|
|
193
|
+
super(`the response body did not finish within ${ms}ms`);
|
|
194
|
+
this.ms = ms;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
function readBodyBefore(response, deadline) {
|
|
198
|
+
const read = response.json();
|
|
199
|
+
if (deadline === void 0) return read;
|
|
200
|
+
const { signal } = deadline;
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const expire = () => reject(new BodyDeadlineError(deadline.ms));
|
|
203
|
+
if (signal.aborted) expire();
|
|
204
|
+
signal.addEventListener("abort", expire);
|
|
205
|
+
read.then(resolve, reject);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function getJson(url, path, apiKey, appId, fetchImpl, signer) {
|
|
115
209
|
const endpoint = new URL(path, url).toString();
|
|
116
210
|
let response;
|
|
211
|
+
const signature = await signatureHeaders("GET", endpoint, "", signer);
|
|
117
212
|
try {
|
|
118
213
|
response = await fetchImpl(endpoint, {
|
|
119
214
|
method: "GET",
|
|
120
|
-
headers: {
|
|
215
|
+
headers: {
|
|
216
|
+
authorization: `Bearer ${apiKey}`,
|
|
217
|
+
"x-outerlayer-app-id": appId,
|
|
218
|
+
...signature
|
|
219
|
+
}
|
|
121
220
|
});
|
|
122
221
|
} catch (err) {
|
|
123
222
|
throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`);
|
|
124
223
|
}
|
|
125
224
|
if (response.status === 401) {
|
|
126
|
-
throw new WorkCommandError(
|
|
127
|
-
|
|
225
|
+
throw await signatureRefusalOf(response) ?? new WorkCommandError(
|
|
226
|
+
unauthorizedMessage(apiKey),
|
|
128
227
|
false,
|
|
129
228
|
401
|
|
130
229
|
);
|
|
@@ -143,7 +242,8 @@ async function getJson(url, path, apiKey, appId, fetchImpl) {
|
|
|
143
242
|
throw new WorkCommandError(
|
|
144
243
|
json.error?.message ?? `request failed (${response.status})`,
|
|
145
244
|
response.status >= 500 || response.status === 429,
|
|
146
|
-
response.status
|
|
245
|
+
response.status,
|
|
246
|
+
json.error?.code
|
|
147
247
|
);
|
|
148
248
|
}
|
|
149
249
|
return { status: response.status, data: json.data ?? json, pagination: json.pagination ?? null };
|
|
@@ -174,33 +274,65 @@ function environmentFacts(opts) {
|
|
|
174
274
|
...ci ? { ci } : {}
|
|
175
275
|
};
|
|
176
276
|
}
|
|
177
|
-
|
|
277
|
+
function isRetryableAnswer(err) {
|
|
278
|
+
const status = err instanceof WorkCommandError ? err.status ?? 0 : 0;
|
|
279
|
+
return status >= 500 || status === 429;
|
|
280
|
+
}
|
|
281
|
+
async function postJsonWithRetry(url, path, apiKey, appId, body, fetchImpl, retries, sleep) {
|
|
282
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
283
|
+
try {
|
|
284
|
+
return await postJson(url, path, apiKey, appId, body, fetchImpl);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (attempt >= retries || !isRetryableAnswer(err)) throw err;
|
|
287
|
+
await sleep(WORK_LINK_RETRY_DELAYS_MS[attempt]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function tokenWarning(data) {
|
|
292
|
+
const tokens = data.repositoryTokens;
|
|
293
|
+
if (tokens === void 0 || tokens === null || tokens.earnable !== false) return "";
|
|
294
|
+
const why = typeof tokens.message === "string" ? sanitizeTerminal(tokens.message) : "the gateway named no reason";
|
|
295
|
+
return `
|
|
296
|
+
${YELLOW}!${RESET} a container host will fail this build: ${why}`;
|
|
297
|
+
}
|
|
298
|
+
async function runWorkBuild(opts) {
|
|
178
299
|
const cwd = opts.cwd ?? process.cwd();
|
|
179
300
|
const env = opts.env ?? process.env;
|
|
180
301
|
const home = opts.home ?? homeFromEnv(env);
|
|
181
302
|
const { url, apiKey, appId } = resolveCredentials(opts);
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
185
|
-
const body = {
|
|
186
|
-
subject: { repository, ...subject },
|
|
187
|
-
note: opts.note,
|
|
188
|
-
...environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId })
|
|
189
|
-
};
|
|
303
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
304
|
+
const facts = environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId });
|
|
190
305
|
const retries = parseRetries(opts.retry);
|
|
191
306
|
const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
|
|
199
|
-
await sleep(WORK_ADD_RETRY_DELAYS_MS[attempt]);
|
|
307
|
+
const itemNumber = parsePositiveInt(opts.item, "--item");
|
|
308
|
+
if (opts.local) {
|
|
309
|
+
if (itemNumber !== void 0) {
|
|
310
|
+
throw new WorkCommandError(
|
|
311
|
+
`--local takes --issue, not --item: item #${itemNumber} is already on Work \u2014 start a session on it with OUTERLAYER_WORK=${itemNumber}`
|
|
312
|
+
);
|
|
200
313
|
}
|
|
314
|
+
const body2 = {
|
|
315
|
+
subject: resolveWorkSubject({ issue: opts.issue, tracker: opts.tracker, repo: opts.repo, cwd }),
|
|
316
|
+
note: opts.note,
|
|
317
|
+
...facts
|
|
318
|
+
};
|
|
319
|
+
const { data: data2 } = await postJsonWithRetry(url, "/v1/work-items", apiKey, appId, body2, fetchImpl, retries, sleep);
|
|
320
|
+
const label = data2.number != null ? `#${data2.number}` : String(data2.workItemId);
|
|
321
|
+
const launch = data2.number != null ? `OUTERLAYER_WORK=${data2.number} claude` : null;
|
|
322
|
+
const branch = typeof data2.branch === "string" && data2.branch !== "" ? data2.branch : null;
|
|
323
|
+
const output2 = opts.json ? JSON.stringify({ ...data2, ...launch !== null ? { launchCommand: launch } : {} }) : `${GREEN}\u2713${RESET} on Work for you to build ${DIM}\xB7${RESET} ${label} ${DIM}\xB7${RESET} no host will take it` + (branch !== null ? `
|
|
324
|
+
branch: ${branch}` : "") + (launch !== null ? `
|
|
325
|
+
start your session with: ${launch}` : "");
|
|
326
|
+
if (!opts.quiet) process.stdout.write(output2 + "\n");
|
|
327
|
+
return { data: data2, output: output2 };
|
|
201
328
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
329
|
+
const body = itemNumber !== void 0 ? { workItemId: String(itemNumber), note: opts.note, ...facts } : {
|
|
330
|
+
subject: resolveWorkSubject({ issue: opts.issue, tracker: opts.tracker, repo: opts.repo, cwd }),
|
|
331
|
+
note: opts.note,
|
|
332
|
+
...facts
|
|
333
|
+
};
|
|
334
|
+
const { data } = await postJsonWithRetry(url, "/v1/work-items/build", apiKey, appId, body, fetchImpl, retries, sleep);
|
|
335
|
+
const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} build requested ${DIM}\xB7${RESET} ${data.number != null ? `#${data.number}` : data.workItemId} ${DIM}\xB7${RESET} by ${data.attestor}` + tokenWarning(data);
|
|
204
336
|
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
205
337
|
return { data, output };
|
|
206
338
|
}
|
|
@@ -215,9 +347,14 @@ async function runWorkLinkSession(opts) {
|
|
|
215
347
|
if (!sessionId) {
|
|
216
348
|
throw new WorkCommandError("no session id detected \u2014 pass --session-id or run from a live session");
|
|
217
349
|
}
|
|
218
|
-
const fetchImpl = opts.fetchImpl ??
|
|
350
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
219
351
|
const identity = resolveRepoIdentity(cwd);
|
|
220
|
-
const body = {
|
|
352
|
+
const body = {
|
|
353
|
+
sessionId,
|
|
354
|
+
repository: identity.gitRepo ?? "",
|
|
355
|
+
...opts.noLease ? { noLease: true } : {},
|
|
356
|
+
...opts.ended ? { ended: true } : {}
|
|
357
|
+
};
|
|
221
358
|
const retries = parseRetries(opts.retry);
|
|
222
359
|
const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
223
360
|
let data;
|
|
@@ -227,10 +364,12 @@ async function runWorkLinkSession(opts) {
|
|
|
227
364
|
break;
|
|
228
365
|
} catch (err) {
|
|
229
366
|
if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
|
|
230
|
-
await sleep(
|
|
367
|
+
await sleep(WORK_LINK_RETRY_DELAYS_MS[attempt]);
|
|
231
368
|
}
|
|
232
369
|
}
|
|
233
|
-
const
|
|
370
|
+
const heldBy = typeof data.heldBy === "string" ? data.heldBy : void 0;
|
|
371
|
+
const linked = data.linked === true;
|
|
372
|
+
const output = opts.json ? JSON.stringify(data) : opts.ended ? linked ? `${GREEN}\u2713${RESET} ended ${DIM}\xB7${RESET} session ${sessionId} ${DIM}\xB7${RESET} #${number}` : `${DIM}\xB7${RESET} session ${sessionId} was not linked to #${number}` : heldBy ? `${DIM}\xB7${RESET} session ${sessionId} linked to #${number}, held by ${sanitizeTerminal(heldBy)} \u2014 no lease taken` : `${GREEN}\u2713${RESET} linked ${DIM}\xB7${RESET} session ${sessionId} ${DIM}\xB7${RESET} #${number}`;
|
|
234
373
|
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
235
374
|
return { data, output };
|
|
236
375
|
}
|
|
@@ -242,7 +381,7 @@ async function runWorkRemove(opts) {
|
|
|
242
381
|
if (!opts.reason) {
|
|
243
382
|
throw new WorkCommandError("--reason is required \u2014 a removal records why");
|
|
244
383
|
}
|
|
245
|
-
const fetchImpl = opts.fetchImpl ??
|
|
384
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
246
385
|
const itemNumber = parsePositiveInt(opts.item, "--item");
|
|
247
386
|
if (itemNumber !== void 0) {
|
|
248
387
|
const { data: data2 } = await postJson(
|
|
@@ -289,7 +428,7 @@ async function runWorkStatus(opts) {
|
|
|
289
428
|
`/v1/work-items/${fetchByItem}`,
|
|
290
429
|
creds.apiKey,
|
|
291
430
|
creds.appId,
|
|
292
|
-
opts.fetchImpl ??
|
|
431
|
+
opts.fetchImpl ?? defaultFetch
|
|
293
432
|
);
|
|
294
433
|
const body = opts.json ? JSON.stringify(data2) : JSON.stringify(data2, null, 2);
|
|
295
434
|
if (!opts.quiet) process.stdout.write(body + "\n");
|
|
@@ -297,7 +436,7 @@ async function runWorkStatus(opts) {
|
|
|
297
436
|
}
|
|
298
437
|
const subject = resolveSubject(opts);
|
|
299
438
|
const repository = resolveRepository(opts.repo, cwd);
|
|
300
|
-
const fetchImpl = opts.fetchImpl ??
|
|
439
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
301
440
|
const number = subject.issue;
|
|
302
441
|
const item = await findLiveItem(creds, repository, subject, fetchImpl);
|
|
303
442
|
if (!item) {
|
|
@@ -311,7 +450,7 @@ async function runWorkStatus(opts) {
|
|
|
311
450
|
async function runWorkList(opts) {
|
|
312
451
|
const cwd = opts.cwd ?? process.cwd();
|
|
313
452
|
const { url, apiKey, appId } = resolveCredentials(opts);
|
|
314
|
-
const fetchImpl = opts.fetchImpl ??
|
|
453
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
315
454
|
const repository = opts.repo ?? resolveRepoIdentity(cwd).gitRepo;
|
|
316
455
|
if (opts.needs !== void 0 && opts.needs !== "amend") {
|
|
317
456
|
throw new WorkCommandError('--needs only accepts "amend"');
|
|
@@ -321,6 +460,7 @@ async function runWorkList(opts) {
|
|
|
321
460
|
if (opts.stage) query.set("stage", opts.stage);
|
|
322
461
|
if (opts.section) query.set("section", opts.section);
|
|
323
462
|
if (opts.unclaimed) query.set("unclaimed", "true");
|
|
463
|
+
if (opts.claimed) query.set("claimed", "true");
|
|
324
464
|
if (opts.startable) query.set("startable", "true");
|
|
325
465
|
if (opts.needs) query.set("needs", opts.needs);
|
|
326
466
|
const items = [];
|
|
@@ -332,9 +472,16 @@ async function runWorkList(opts) {
|
|
|
332
472
|
cursor = page.pagination?.cursor ?? null;
|
|
333
473
|
} while (cursor);
|
|
334
474
|
const data = items;
|
|
335
|
-
const
|
|
475
|
+
const claimSuffix = (claim) => {
|
|
476
|
+
if (!claim) return "";
|
|
477
|
+
const holder = claim.holderKind === "session" ? `${claim.memberName || "a member"} (local session` : `${sanitizeTerminal(claim.host)} (${claim.kind}`;
|
|
478
|
+
return ` ${DIM}\xB7${RESET} claimed by ${holder}, since ${claim.createdAt}, until ${claim.expiresAt})`;
|
|
479
|
+
};
|
|
480
|
+
const output = opts.json ? JSON.stringify(data) : items.length === 0 ? "no live work items" : items.map(
|
|
481
|
+
(i) => `${i.trackerKey} ${DIM}\xB7${RESET} ${i.stage} ${DIM}\xB7${RESET} ${i.section} ${DIM}\xB7${RESET} ${i.title}${claimSuffix(i.claim)}`
|
|
482
|
+
).join("\n");
|
|
336
483
|
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
337
484
|
return { data, output };
|
|
338
485
|
}
|
|
339
486
|
|
|
340
|
-
export { WorkCommandError, findLiveItem, getJson, parsePositiveInt, postJson, resolveCredentials, resolveRepository,
|
|
487
|
+
export { WorkCommandError, findLiveItem, getJson, isSignatureRefusal, parsePositiveInt, postJson, resolveCredentials, resolveRepository, runWorkBuild, runWorkLinkSession, runWorkList, runWorkRemove, runWorkStatus, signatureRefusalMessage };
|
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, mkdirSync, copyFileSync, writeFileSync } from
|
|
|
5
5
|
import { join, dirname, isAbsolute } from 'path';
|
|
6
6
|
|
|
7
7
|
createRequire(import.meta.url);
|
|
8
|
-
var REGISTERED_EVENTS = ["SessionStart", "SessionEnd", "Stop"];
|
|
8
|
+
var REGISTERED_EVENTS = ["SessionStart", "SessionEnd", "Stop", "PostToolUse"];
|
|
9
9
|
var MARKER = "_outerlayer";
|
|
10
10
|
function settingsPath(scope, opts = {}) {
|
|
11
11
|
const home = opts.home ?? homeFromEnv();
|
|
@@ -43,6 +43,12 @@ function tokenizeCommand(command) {
|
|
|
43
43
|
function quoteBin(bin) {
|
|
44
44
|
return /\s/.test(bin) ? `"${bin}"` : bin;
|
|
45
45
|
}
|
|
46
|
+
function isScriptBin(bin) {
|
|
47
|
+
return /\.[cm]?js$/i.test(bin);
|
|
48
|
+
}
|
|
49
|
+
function cliCommand(cliBin) {
|
|
50
|
+
return isScriptBin(cliBin) ? `node ${quoteBin(cliBin)}` : quoteBin(cliBin);
|
|
51
|
+
}
|
|
46
52
|
function isOurBin(bin, cliBin) {
|
|
47
53
|
if (cliBin !== void 0 && normalizeBinPath(bin) === normalizeBinPath(cliBin)) return true;
|
|
48
54
|
const parts = bin.replace(/\\/g, "/").split("/");
|
|
@@ -63,30 +69,40 @@ function splitCliInvocation(command, cliBin) {
|
|
|
63
69
|
if (typeof command !== "string") return null;
|
|
64
70
|
const tokens = tokenizeCommand(command.trim());
|
|
65
71
|
if (!tokens) return null;
|
|
66
|
-
let i = 0;
|
|
67
72
|
const launcher = (tokens[0] ?? "").replace(/\\/g, "/").split("/").pop() ?? "";
|
|
68
|
-
|
|
73
|
+
const viaNode = launcher === "node" || launcher === "node.exe";
|
|
74
|
+
const viaNpx = launcher === "npx";
|
|
75
|
+
const i = viaNode || viaNpx ? 1 : 0;
|
|
69
76
|
const bin = tokens[i];
|
|
70
77
|
if (!bin || !isOurBin(bin, cliBin)) return null;
|
|
71
|
-
return { bin, args: tokens.slice(i + 1), viaNpx
|
|
78
|
+
return { bin, args: tokens.slice(i + 1), viaNpx, viaNode };
|
|
72
79
|
}
|
|
73
80
|
function parseCliInvocation(command, cliBin) {
|
|
74
81
|
return splitCliInvocation(command, cliBin)?.args ?? null;
|
|
75
82
|
}
|
|
76
83
|
function ourCommandBins(settings) {
|
|
77
|
-
|
|
84
|
+
return unique(ourCommandLaunches(settings).map((l) => l.bin));
|
|
85
|
+
}
|
|
86
|
+
function ourDirectScriptBins(settings) {
|
|
87
|
+
return unique(ourCommandLaunches(settings).filter((l) => !l.viaNode && isScriptBin(l.bin)).map((l) => l.bin));
|
|
88
|
+
}
|
|
89
|
+
function ourCommandLaunches(settings) {
|
|
90
|
+
const launches = [];
|
|
78
91
|
const add = (command) => {
|
|
79
92
|
const parsed = splitCliInvocation(command);
|
|
80
93
|
if (!parsed || parsed.viaNpx) return;
|
|
81
94
|
if (!["hook", "hook-wrap", "statusline"].includes(parsed.args[0] ?? "")) return;
|
|
82
95
|
if (parsed.bin.includes("$") || !isAbsolute(parsed.bin)) return;
|
|
83
|
-
|
|
96
|
+
launches.push({ bin: parsed.bin, viaNode: parsed.viaNode });
|
|
84
97
|
};
|
|
85
98
|
for (const matchers of Object.values(settings?.hooks ?? {})) {
|
|
86
99
|
for (const matcher of matchers ?? []) for (const h of matcher.hooks ?? []) add(h.command);
|
|
87
100
|
}
|
|
88
101
|
add(settings?.statusLine?.command);
|
|
89
|
-
return
|
|
102
|
+
return launches;
|
|
103
|
+
}
|
|
104
|
+
function unique(values) {
|
|
105
|
+
return [...new Set(values)];
|
|
90
106
|
}
|
|
91
107
|
function parseLifecycleHookCommand(command, cliBin) {
|
|
92
108
|
const args = parseCliInvocation(command, cliBin);
|
|
@@ -142,7 +158,7 @@ var SettingsParseError = class extends Error {
|
|
|
142
158
|
};
|
|
143
159
|
function ourEntry(cliBin, event) {
|
|
144
160
|
return {
|
|
145
|
-
hooks: [{ type: "command", command: `${
|
|
161
|
+
hooks: [{ type: "command", command: `${cliCommand(cliBin)} hook ${event}`, [MARKER]: true }]
|
|
146
162
|
};
|
|
147
163
|
}
|
|
148
164
|
function pruneOurHooks(blocks, cliBin) {
|
|
@@ -229,7 +245,7 @@ function wrapHookCommand(cliBin, h, opts = {}) {
|
|
|
229
245
|
if (isWrapped(h, cliBin)) return h;
|
|
230
246
|
const id = encodeWrappedCommand(h.command);
|
|
231
247
|
const flag = opts.captureOutput ? " --capture-output" : "";
|
|
232
|
-
return { type: "command", command: `${
|
|
248
|
+
return { type: "command", command: `${cliCommand(cliBin)} hook-wrap --id ${id}${flag}`, [WRAP_MARKER]: h.command };
|
|
233
249
|
}
|
|
234
250
|
function unwrapHookCommand(h, cliBin) {
|
|
235
251
|
const original = wrappedOriginalCommand(h, cliBin);
|
|
@@ -280,11 +296,11 @@ function repointWrappedHooks(settings, cliBin) {
|
|
|
280
296
|
const h = matcher.hooks[hi];
|
|
281
297
|
const parsed = splitCliInvocation(h.command, cliBin);
|
|
282
298
|
if (!parsed || parsed.args[0] !== "hook-wrap") continue;
|
|
283
|
-
if (normalizeBinPath(parsed.bin) === normalizeBinPath(cliBin)) continue;
|
|
299
|
+
if (normalizeBinPath(parsed.bin) === normalizeBinPath(cliBin) && parsed.viaNode === isScriptBin(cliBin)) continue;
|
|
284
300
|
const original = wrappedOriginalCommand(h, cliBin);
|
|
285
301
|
if (original === null) continue;
|
|
286
302
|
const flag = parsed.args.includes("--capture-output") ? " --capture-output" : "";
|
|
287
|
-
const desired = `${
|
|
303
|
+
const desired = `${cliCommand(cliBin)} hook-wrap --id ${encodeWrappedCommand(original)}${flag}`;
|
|
288
304
|
matcher.hooks[hi] = { ...h, command: desired, [WRAP_MARKER]: original };
|
|
289
305
|
changed = true;
|
|
290
306
|
}
|
|
@@ -293,10 +309,10 @@ function repointWrappedHooks(settings, cliBin) {
|
|
|
293
309
|
return { next, changed };
|
|
294
310
|
}
|
|
295
311
|
function statuslineCommand(cliBin) {
|
|
296
|
-
return `${
|
|
312
|
+
return `${cliCommand(cliBin)} statusline`;
|
|
297
313
|
}
|
|
298
314
|
function statuslineWrapCommand(cliBin, original) {
|
|
299
|
-
return `${
|
|
315
|
+
return `${cliCommand(cliBin)} statusline --wrap-id ${encodeWrappedCommand(original)}`;
|
|
300
316
|
}
|
|
301
317
|
function isOurStatuslineSlot(slot, cliBin) {
|
|
302
318
|
if (slot[MARKER] === true) return true;
|
|
@@ -377,4 +393,4 @@ function unwrapHooks(settings, cliBin) {
|
|
|
377
393
|
return { next, changed, unwrapped };
|
|
378
394
|
}
|
|
379
395
|
|
|
380
|
-
export { AUTO_WRAP_EVENTS, REGISTERED_EVENTS, SettingsParseError, cliBinResolvable, decodeWrappedCommand, isOurHookCommand, isOurStatuslineSlot, listHookCandidates, mergeHooks, mergeStatusline, ourCommandBins, readSettings, removeHooks, removeStatusline, repointWrappedHooks, settingsPath, statuslineWrappedOriginal, unwrapHooks, wrapHooks, wrappedOriginalCommand, writeSettings };
|
|
396
|
+
export { AUTO_WRAP_EVENTS, REGISTERED_EVENTS, SettingsParseError, cliBinResolvable, cliCommand, decodeWrappedCommand, encodeWrappedCommand, isOurHookCommand, isOurHookCommandFor, isOurStatuslineSlot, isWrapped, listHookCandidates, mergeHooks, mergeStatusline, ourCommandBins, ourDirectScriptBins, parseLifecycleHookCommand, readSettings, removeHooks, removeStatusline, repointWrappedHooks, settingsPath, statuslineCommand, statuslineWrappedOriginal, unwrapHooks, wrapHooks, wrappedOriginalCommand, writeSettings };
|