@outerlayer/cli 0.1.1 → 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 +134 -0
- package/README.md +238 -77
- 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-3YPKXE5L.js +1683 -0
- package/dist/{chunk-HNCCD2IX.js → chunk-5DLBAXY5.js} +209 -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-RCQXYLMO.js → chunk-DVDEBNQJ.js} +18 -18
- package/dist/{work-pr-cmd-GOYSPEN2.js → chunk-DVYHCMB2.js} +10 -25
- 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-BQB3U2VD.js → chunk-G7FSV7JX.js} +5113 -3729
- 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-3VQ4XXQA.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-WZBHBH3J.js +20 -0
- package/dist/{chunk-IWIUYLDR.js → chunk-XCCLFVXM.js} +118 -189
- package/dist/{context-materialize-J6CRQ7O7.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-EFJIG2VT.js → cli-NHUXABYH.js} +597 -965
- 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-KPPD3YLO.js → emit-artifact-cmd-KTYW2TN2.js} +25 -19
- package/dist/emit-cmd-DCERAU27.js +9 -0
- package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
- package/dist/{emit-finding-cmd-VR5VEGXJ.js → emit-finding-cmd-UR4ID45Z.js} +35 -30
- package/dist/{emit-result-cmd-YOA5BJKL.js → emit-result-cmd-FDTUKGKE.js} +35 -61
- 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-JHVSZYC5.js → status-2UZKKNB7.js} +31 -12
- 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-NRLXJUST.js → watch-5C4ZBBGO.js} +20 -7
- package/dist/{work-claim-cmd-HXB7K27H.js → work-claim-cmd-LSO2B2EX.js} +34 -19
- package/dist/work-cmd-UOQO2AD4.js +17 -0
- package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
- package/dist/{work-launch-YNNCKIH3.js → work-launch-YI4CJDAP.js} +2 -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-JJP7YLMN.js +0 -25
- package/dist/chunk-KKEU6FFR.js +0 -589
- package/dist/chunk-OZ7C3XUE.js +0 -34
- package/dist/chunk-WQ6VGRGZ.js +0 -150
- package/dist/emit-commit-credit-cmd-656BRAQ4.js +0 -121
- package/dist/hook-fast-I2XTHDE6.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-BWE2BS6J.js +0 -646
- package/dist/sync-cmd-S7Q6DWU3.js +0 -15
- package/dist/work-cmd-ABP6XDBP.js +0 -16
|
@@ -1,16 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { detectActiveSession } from './chunk-BFESLKPP.js';
|
|
4
|
+
import { sanitizeTerminal } from './chunk-6Y63SXFO.js';
|
|
5
|
+
import { resolveRepoIdentity } from './chunk-G7FSV7JX.js';
|
|
6
|
+
import { parseTrackerNumber, trackerNumberError } from './chunk-WZBHBH3J.js';
|
|
7
|
+
import { defaultFetch } from './chunk-BCLJSQCV.js';
|
|
8
|
+
import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-BOLTI6LR.js';
|
|
9
|
+
import { signRunnerRequest } from './chunk-M5POMKKW.js';
|
|
6
10
|
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
7
11
|
|
|
8
12
|
createRequire(import.meta.url);
|
|
9
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
|
+
|
|
10
40
|
// src/work-cmd.ts
|
|
11
41
|
var GREEN = "\x1B[32m";
|
|
42
|
+
var YELLOW = "\x1B[33m";
|
|
12
43
|
var DIM = "\x1B[2m";
|
|
13
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
|
+
}
|
|
14
59
|
var WorkCommandError = class extends Error {
|
|
15
60
|
/** True when the same request is worth sending again: the gateway was
|
|
16
61
|
* unreachable, answered a body that never came from it, or failed in a way
|
|
@@ -21,27 +66,18 @@ var WorkCommandError = class extends Error {
|
|
|
21
66
|
* reached a response. A caller that has to tell one refusal from another —
|
|
22
67
|
* a claim lost to another host from a key that no longer works — reads it
|
|
23
68
|
* rather than matching on the message. */
|
|
24
|
-
constructor(message, retryable = false, status) {
|
|
69
|
+
constructor(message, retryable = false, status, code) {
|
|
25
70
|
super(message);
|
|
26
71
|
this.retryable = retryable;
|
|
27
72
|
this.status = status;
|
|
73
|
+
this.code = code;
|
|
28
74
|
}
|
|
29
75
|
};
|
|
30
|
-
var
|
|
76
|
+
var WORK_LINK_RETRY_DELAYS_MS = [1e3, 5e3, 15e3, 6e4];
|
|
31
77
|
function resolveCredentials(opts) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
|
|
36
|
-
const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
|
|
37
|
-
const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
|
|
38
|
-
if (!url || !apiKey || !appId) {
|
|
39
|
-
const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
|
|
40
|
-
throw new WorkCommandError(
|
|
41
|
-
`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.`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
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 };
|
|
45
81
|
}
|
|
46
82
|
function resolveRepository(repoFlag, cwd) {
|
|
47
83
|
if (repoFlag) return repoFlag;
|
|
@@ -59,7 +95,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
59
95
|
}
|
|
60
96
|
function parseRetries(raw) {
|
|
61
97
|
const n = parseTrackerNumber(raw);
|
|
62
|
-
return n === void 0 ? 0 : Math.min(n,
|
|
98
|
+
return n === void 0 ? 0 : Math.min(n, WORK_LINK_RETRY_DELAYS_MS.length);
|
|
63
99
|
}
|
|
64
100
|
function resolveSubject(opts) {
|
|
65
101
|
const issue = parsePositiveInt(opts.issue, "--issue");
|
|
@@ -68,33 +104,74 @@ function resolveSubject(opts) {
|
|
|
68
104
|
}
|
|
69
105
|
return { issue };
|
|
70
106
|
}
|
|
71
|
-
|
|
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) {
|
|
72
138
|
let response;
|
|
73
139
|
const endpoint = new URL(path, url).toString();
|
|
140
|
+
const payload = JSON.stringify(body);
|
|
141
|
+
const signature = await signatureHeaders("POST", endpoint, payload ?? "", signer);
|
|
74
142
|
try {
|
|
75
143
|
response = await fetchImpl(endpoint, {
|
|
76
144
|
method: "POST",
|
|
77
145
|
headers: {
|
|
78
146
|
"content-type": "application/json",
|
|
79
147
|
authorization: `Bearer ${apiKey}`,
|
|
80
|
-
"x-outerlayer-app-id": appId
|
|
148
|
+
"x-outerlayer-app-id": appId,
|
|
149
|
+
...signature
|
|
81
150
|
},
|
|
82
|
-
body:
|
|
151
|
+
body: payload,
|
|
152
|
+
...deadline ? { signal: deadline.signal } : {}
|
|
83
153
|
});
|
|
84
154
|
} catch (err) {
|
|
85
155
|
throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`, true);
|
|
86
156
|
}
|
|
87
157
|
if (response.status === 401) {
|
|
88
|
-
throw new WorkCommandError(
|
|
89
|
-
|
|
158
|
+
throw await signatureRefusalOf(response) ?? new WorkCommandError(
|
|
159
|
+
unauthorizedMessage(apiKey),
|
|
90
160
|
false,
|
|
91
161
|
401
|
|
92
162
|
);
|
|
93
163
|
}
|
|
94
164
|
let json;
|
|
95
165
|
try {
|
|
96
|
-
json = await response
|
|
97
|
-
} 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
|
+
}
|
|
98
175
|
throw new WorkCommandError(
|
|
99
176
|
`${endpoint} answered ${response.status} with a body that is not JSON \u2014 the request may not have reached the gateway`,
|
|
100
177
|
true,
|
|
@@ -105,25 +182,48 @@ async function postJson(url, path, apiKey, appId, body, fetchImpl) {
|
|
|
105
182
|
throw new WorkCommandError(
|
|
106
183
|
json.error?.message ?? `request failed (${response.status})`,
|
|
107
184
|
response.status >= 500 || response.status === 429,
|
|
108
|
-
response.status
|
|
185
|
+
response.status,
|
|
186
|
+
json.error?.code
|
|
109
187
|
);
|
|
110
188
|
}
|
|
111
189
|
return { status: response.status, data: json.data ?? json };
|
|
112
190
|
}
|
|
113
|
-
|
|
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) {
|
|
114
209
|
const endpoint = new URL(path, url).toString();
|
|
115
210
|
let response;
|
|
211
|
+
const signature = await signatureHeaders("GET", endpoint, "", signer);
|
|
116
212
|
try {
|
|
117
213
|
response = await fetchImpl(endpoint, {
|
|
118
214
|
method: "GET",
|
|
119
|
-
headers: {
|
|
215
|
+
headers: {
|
|
216
|
+
authorization: `Bearer ${apiKey}`,
|
|
217
|
+
"x-outerlayer-app-id": appId,
|
|
218
|
+
...signature
|
|
219
|
+
}
|
|
120
220
|
});
|
|
121
221
|
} catch (err) {
|
|
122
222
|
throw new WorkCommandError(`network error reaching ${endpoint}: ${String(err)}`);
|
|
123
223
|
}
|
|
124
224
|
if (response.status === 401) {
|
|
125
|
-
throw new WorkCommandError(
|
|
126
|
-
|
|
225
|
+
throw await signatureRefusalOf(response) ?? new WorkCommandError(
|
|
226
|
+
unauthorizedMessage(apiKey),
|
|
127
227
|
false,
|
|
128
228
|
401
|
|
129
229
|
);
|
|
@@ -142,7 +242,8 @@ async function getJson(url, path, apiKey, appId, fetchImpl) {
|
|
|
142
242
|
throw new WorkCommandError(
|
|
143
243
|
json.error?.message ?? `request failed (${response.status})`,
|
|
144
244
|
response.status >= 500 || response.status === 429,
|
|
145
|
-
response.status
|
|
245
|
+
response.status,
|
|
246
|
+
json.error?.code
|
|
146
247
|
);
|
|
147
248
|
}
|
|
148
249
|
return { status: response.status, data: json.data ?? json, pagination: json.pagination ?? null };
|
|
@@ -173,33 +274,65 @@ function environmentFacts(opts) {
|
|
|
173
274
|
...ci ? { ci } : {}
|
|
174
275
|
};
|
|
175
276
|
}
|
|
176
|
-
|
|
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) {
|
|
177
299
|
const cwd = opts.cwd ?? process.cwd();
|
|
178
300
|
const env = opts.env ?? process.env;
|
|
179
301
|
const home = opts.home ?? homeFromEnv(env);
|
|
180
302
|
const { url, apiKey, appId } = resolveCredentials(opts);
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
184
|
-
const body = {
|
|
185
|
-
subject: { repository, ...subject },
|
|
186
|
-
note: opts.note,
|
|
187
|
-
...environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId })
|
|
188
|
-
};
|
|
303
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
304
|
+
const facts = environmentFacts({ cwd, env, home, sessionIdOverride: opts.sessionId });
|
|
189
305
|
const retries = parseRetries(opts.retry);
|
|
190
306
|
const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
|
|
198
|
-
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
|
+
);
|
|
199
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 };
|
|
200
328
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
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);
|
|
203
336
|
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
204
337
|
return { data, output };
|
|
205
338
|
}
|
|
@@ -214,9 +347,14 @@ async function runWorkLinkSession(opts) {
|
|
|
214
347
|
if (!sessionId) {
|
|
215
348
|
throw new WorkCommandError("no session id detected \u2014 pass --session-id or run from a live session");
|
|
216
349
|
}
|
|
217
|
-
const fetchImpl = opts.fetchImpl ??
|
|
350
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
218
351
|
const identity = resolveRepoIdentity(cwd);
|
|
219
|
-
const body = {
|
|
352
|
+
const body = {
|
|
353
|
+
sessionId,
|
|
354
|
+
repository: identity.gitRepo ?? "",
|
|
355
|
+
...opts.noLease ? { noLease: true } : {},
|
|
356
|
+
...opts.ended ? { ended: true } : {}
|
|
357
|
+
};
|
|
220
358
|
const retries = parseRetries(opts.retry);
|
|
221
359
|
const sleep = opts.sleepImpl ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
222
360
|
let data;
|
|
@@ -226,10 +364,12 @@ async function runWorkLinkSession(opts) {
|
|
|
226
364
|
break;
|
|
227
365
|
} catch (err) {
|
|
228
366
|
if (attempt >= retries || !(err instanceof WorkCommandError) || !err.retryable) throw err;
|
|
229
|
-
await sleep(
|
|
367
|
+
await sleep(WORK_LINK_RETRY_DELAYS_MS[attempt]);
|
|
230
368
|
}
|
|
231
369
|
}
|
|
232
|
-
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}`;
|
|
233
373
|
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
234
374
|
return { data, output };
|
|
235
375
|
}
|
|
@@ -241,7 +381,7 @@ async function runWorkRemove(opts) {
|
|
|
241
381
|
if (!opts.reason) {
|
|
242
382
|
throw new WorkCommandError("--reason is required \u2014 a removal records why");
|
|
243
383
|
}
|
|
244
|
-
const fetchImpl = opts.fetchImpl ??
|
|
384
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
245
385
|
const itemNumber = parsePositiveInt(opts.item, "--item");
|
|
246
386
|
if (itemNumber !== void 0) {
|
|
247
387
|
const { data: data2 } = await postJson(
|
|
@@ -288,7 +428,7 @@ async function runWorkStatus(opts) {
|
|
|
288
428
|
`/v1/work-items/${fetchByItem}`,
|
|
289
429
|
creds.apiKey,
|
|
290
430
|
creds.appId,
|
|
291
|
-
opts.fetchImpl ??
|
|
431
|
+
opts.fetchImpl ?? defaultFetch
|
|
292
432
|
);
|
|
293
433
|
const body = opts.json ? JSON.stringify(data2) : JSON.stringify(data2, null, 2);
|
|
294
434
|
if (!opts.quiet) process.stdout.write(body + "\n");
|
|
@@ -296,7 +436,7 @@ async function runWorkStatus(opts) {
|
|
|
296
436
|
}
|
|
297
437
|
const subject = resolveSubject(opts);
|
|
298
438
|
const repository = resolveRepository(opts.repo, cwd);
|
|
299
|
-
const fetchImpl = opts.fetchImpl ??
|
|
439
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
300
440
|
const number = subject.issue;
|
|
301
441
|
const item = await findLiveItem(creds, repository, subject, fetchImpl);
|
|
302
442
|
if (!item) {
|
|
@@ -310,7 +450,7 @@ async function runWorkStatus(opts) {
|
|
|
310
450
|
async function runWorkList(opts) {
|
|
311
451
|
const cwd = opts.cwd ?? process.cwd();
|
|
312
452
|
const { url, apiKey, appId } = resolveCredentials(opts);
|
|
313
|
-
const fetchImpl = opts.fetchImpl ??
|
|
453
|
+
const fetchImpl = opts.fetchImpl ?? defaultFetch;
|
|
314
454
|
const repository = opts.repo ?? resolveRepoIdentity(cwd).gitRepo;
|
|
315
455
|
if (opts.needs !== void 0 && opts.needs !== "amend") {
|
|
316
456
|
throw new WorkCommandError('--needs only accepts "amend"');
|
|
@@ -320,6 +460,7 @@ async function runWorkList(opts) {
|
|
|
320
460
|
if (opts.stage) query.set("stage", opts.stage);
|
|
321
461
|
if (opts.section) query.set("section", opts.section);
|
|
322
462
|
if (opts.unclaimed) query.set("unclaimed", "true");
|
|
463
|
+
if (opts.claimed) query.set("claimed", "true");
|
|
323
464
|
if (opts.startable) query.set("startable", "true");
|
|
324
465
|
if (opts.needs) query.set("needs", opts.needs);
|
|
325
466
|
const items = [];
|
|
@@ -331,9 +472,16 @@ async function runWorkList(opts) {
|
|
|
331
472
|
cursor = page.pagination?.cursor ?? null;
|
|
332
473
|
} while (cursor);
|
|
333
474
|
const data = items;
|
|
334
|
-
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");
|
|
335
483
|
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
336
484
|
return { data, output };
|
|
337
485
|
}
|
|
338
486
|
|
|
339
|
-
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 };
|