@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,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { FindingBatchSchema, resolveRepoIdentity, detectActiveSession, launchRefusalForSession } from './chunk-DEDV5V4U.js';
|
|
4
|
+
import './chunk-YRYMNBGQ.js';
|
|
5
|
+
import { launchedItemForSession, parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
|
|
6
|
+
import { readLatestProvenanceForSession } from './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
|
+
import { readFileSync } from 'fs';
|
|
15
|
+
|
|
16
|
+
createRequire(import.meta.url);
|
|
17
|
+
var GREEN = "\x1B[32m";
|
|
18
|
+
var DIM = "\x1B[2m";
|
|
19
|
+
var RESET = "\x1B[0m";
|
|
20
|
+
var EmitFindingError = class extends Error {
|
|
21
|
+
};
|
|
22
|
+
function parseItemFlag(raw) {
|
|
23
|
+
if (raw === void 0) return void 0;
|
|
24
|
+
const number = parseTrackerNumber(raw);
|
|
25
|
+
if (number === void 0) {
|
|
26
|
+
throw new EmitFindingError(trackerNumberError("--item", raw));
|
|
27
|
+
}
|
|
28
|
+
return { number };
|
|
29
|
+
}
|
|
30
|
+
function parseIntFlag(raw, flag, opts) {
|
|
31
|
+
if (raw === void 0) return void 0;
|
|
32
|
+
if (!/^-?\d+$/.test(raw)) {
|
|
33
|
+
throw new EmitFindingError(`invalid ${flag} "${raw}" \u2014 expected a whole number`);
|
|
34
|
+
}
|
|
35
|
+
const n = Number(raw);
|
|
36
|
+
if (!Number.isSafeInteger(n) || n < opts.min) {
|
|
37
|
+
throw new EmitFindingError(`invalid ${flag} "${raw}" \u2014 expected a whole number >= ${opts.min}`);
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
41
|
+
async function readAllStdin() {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
44
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
45
|
+
}
|
|
46
|
+
function recordFromFlags(opts) {
|
|
47
|
+
const line = parseIntFlag(opts.line, "--line", { min: 1 });
|
|
48
|
+
const ruleLine = parseIntFlag(opts.ruleLine, "--rule-line", { min: 1 });
|
|
49
|
+
const turnIndex = parseIntFlag(opts.turn, "--turn", { min: 0 });
|
|
50
|
+
const round = parseIntFlag(opts.round, "--round", { min: 0 });
|
|
51
|
+
return {
|
|
52
|
+
id: opts.id,
|
|
53
|
+
subject: opts.subject,
|
|
54
|
+
title: opts.title,
|
|
55
|
+
file: opts.file,
|
|
56
|
+
line: line ?? null,
|
|
57
|
+
severity: opts.severity ?? null,
|
|
58
|
+
kind: opts.kind,
|
|
59
|
+
verdict: opts.verdict,
|
|
60
|
+
fixed: opts.fixed ?? false,
|
|
61
|
+
rule: opts.rulePath !== void 0 ? {
|
|
62
|
+
path: opts.rulePath,
|
|
63
|
+
line: ruleLine ?? null,
|
|
64
|
+
quote: opts.ruleQuote ?? "",
|
|
65
|
+
relation: opts.ruleRelation
|
|
66
|
+
} : null,
|
|
67
|
+
source: opts.source,
|
|
68
|
+
where: {
|
|
69
|
+
label: opts.where,
|
|
70
|
+
...opts.traceId !== void 0 ? { traceId: opts.traceId } : {},
|
|
71
|
+
...turnIndex !== void 0 ? { turnIndex } : {},
|
|
72
|
+
...round !== void 0 ? { round } : {}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function firstIssueMessage(result) {
|
|
77
|
+
const issue = result.error.issues[0];
|
|
78
|
+
return issue ? `${issue.path.join(".")}: ${issue.message}` : "malformed";
|
|
79
|
+
}
|
|
80
|
+
function detectUploadSessionId(opts) {
|
|
81
|
+
const identity = resolveRepoIdentity(opts.cwd);
|
|
82
|
+
return detectActiveSession({
|
|
83
|
+
env: opts.env,
|
|
84
|
+
home: opts.home,
|
|
85
|
+
cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, opts.cwd] : [opts.cwd],
|
|
86
|
+
nowMs: opts.now().getTime()
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function resolveUploadTarget(opts) {
|
|
90
|
+
const item = parseItemFlag(opts.item);
|
|
91
|
+
const sessionId = opts.sessionId;
|
|
92
|
+
if (sessionId !== void 0) {
|
|
93
|
+
const refusal = launchRefusalForSession(sessionId, opts.home);
|
|
94
|
+
if (refusal !== void 0) throw new EmitFindingError(refusal);
|
|
95
|
+
}
|
|
96
|
+
if (item !== void 0) {
|
|
97
|
+
return sessionId !== void 0 ? { item, sessionId } : { item };
|
|
98
|
+
}
|
|
99
|
+
if (sessionId === void 0) {
|
|
100
|
+
throw new EmitFindingError(
|
|
101
|
+
"nothing to attach this to \u2014 no work item. Pass --item <number>, or run inside a recorded session launched with OUTERLAYER_WORK \u2014 its own launch record names the item automatically."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const launchedNumber = launchedItemForSession(sessionId, opts.home);
|
|
105
|
+
if (launchedNumber === void 0) {
|
|
106
|
+
throw new EmitFindingError(
|
|
107
|
+
"nothing to attach this to \u2014 no work item. Pass --item <number>; this recorded session's own launch record names no item either."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return { item: { number: launchedNumber }, sessionId };
|
|
111
|
+
}
|
|
112
|
+
async function upload(batch, target, opts) {
|
|
113
|
+
const fileConfig = readCloudConfig(opts.home);
|
|
114
|
+
const url = opts.url ?? opts.env.OUTERLAYER_URL ?? fileConfig.url;
|
|
115
|
+
const apiKey = opts.apiKey ?? opts.env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
|
|
116
|
+
const appId = opts.appId ?? opts.env.OUTERLAYER_APP_ID ?? fileConfig.appId;
|
|
117
|
+
if (!url || !apiKey || !appId) {
|
|
118
|
+
const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
|
|
119
|
+
throw new EmitFindingError(
|
|
120
|
+
`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.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const request = {
|
|
124
|
+
...batch,
|
|
125
|
+
item: target.item,
|
|
126
|
+
...target.sessionId !== void 0 ? { sessionId: target.sessionId } : {}
|
|
127
|
+
};
|
|
128
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
129
|
+
const endpoint = new URL("/v1/findings", url).toString();
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = await fetchImpl(endpoint, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"content-type": "application/json",
|
|
136
|
+
authorization: `Bearer ${apiKey}`,
|
|
137
|
+
"x-outerlayer-app-id": appId
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(request)
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw new EmitFindingError(`network error reaching ${endpoint}: ${String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
if (response.status === 401) {
|
|
145
|
+
throw new EmitFindingError(
|
|
146
|
+
"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."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
let detail = "";
|
|
151
|
+
try {
|
|
152
|
+
detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 160);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
throw new EmitFindingError(`emit failed (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
let data = {};
|
|
158
|
+
try {
|
|
159
|
+
data = (await response.json()).data ?? {};
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
const count = typeof data.count === "number" ? data.count : batch.findings.length;
|
|
163
|
+
const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} recorded ${count} ${count === 1 ? "finding" : "findings"} ${DIM}\xB7${RESET} item #${target.item.number}`;
|
|
164
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
165
|
+
return { data, output, exitCode: 0 };
|
|
166
|
+
}
|
|
167
|
+
async function runEmitFinding(opts) {
|
|
168
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
169
|
+
const home = opts.home ?? homeFromEnv();
|
|
170
|
+
const env = opts.env ?? process.env;
|
|
171
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
172
|
+
const sessionId = detectUploadSessionId({ cwd, home, env, now });
|
|
173
|
+
const contextSourceSha = opts.contextSourceSha ?? (sessionId !== void 0 ? readLatestProvenanceForSession(sessionId, home)?.sha ?? void 0 : void 0);
|
|
174
|
+
const rawBatch = {
|
|
175
|
+
schemaVersion: 1,
|
|
176
|
+
...opts.headSha !== void 0 ? { headSha: opts.headSha } : {},
|
|
177
|
+
...contextSourceSha !== void 0 ? { contextSourceSha } : {},
|
|
178
|
+
findings: [recordFromFlags(opts)]
|
|
179
|
+
};
|
|
180
|
+
const parsed = FindingBatchSchema.safeParse(rawBatch);
|
|
181
|
+
if (!parsed.success) {
|
|
182
|
+
throw new EmitFindingError(`invalid finding \u2014 ${firstIssueMessage(parsed)}`);
|
|
183
|
+
}
|
|
184
|
+
const target = resolveUploadTarget({ item: opts.item, sessionId, home });
|
|
185
|
+
return upload(parsed.data, target, {
|
|
186
|
+
json: opts.json,
|
|
187
|
+
url: opts.url,
|
|
188
|
+
apiKey: opts.apiKey,
|
|
189
|
+
appId: opts.appId,
|
|
190
|
+
quiet: opts.quiet,
|
|
191
|
+
home,
|
|
192
|
+
env,
|
|
193
|
+
fetchImpl: opts.fetchImpl
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function runEmitFindings(opts) {
|
|
197
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
198
|
+
const home = opts.home ?? homeFromEnv();
|
|
199
|
+
const env = opts.env ?? process.env;
|
|
200
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
201
|
+
let text;
|
|
202
|
+
if (opts.file === "-") {
|
|
203
|
+
text = await (opts.readStdin ?? readAllStdin)();
|
|
204
|
+
} else {
|
|
205
|
+
try {
|
|
206
|
+
text = readFileSync(opts.file, "utf8");
|
|
207
|
+
} catch (err) {
|
|
208
|
+
throw new EmitFindingError(`cannot read "${opts.file}": ${String(err)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
raw = JSON.parse(text);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
throw new EmitFindingError(`"${opts.file}" is not valid JSON: ${String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
const parsed = FindingBatchSchema.safeParse(raw);
|
|
218
|
+
if (!parsed.success) {
|
|
219
|
+
throw new EmitFindingError(`invalid finding batch \u2014 ${firstIssueMessage(parsed)}`);
|
|
220
|
+
}
|
|
221
|
+
const sessionId = detectUploadSessionId({ cwd, home, env, now });
|
|
222
|
+
const target = resolveUploadTarget({ item: opts.item, sessionId, home });
|
|
223
|
+
return upload(parsed.data, target, {
|
|
224
|
+
json: opts.json,
|
|
225
|
+
url: opts.url,
|
|
226
|
+
apiKey: opts.apiKey,
|
|
227
|
+
appId: opts.appId,
|
|
228
|
+
quiet: opts.quiet,
|
|
229
|
+
home,
|
|
230
|
+
env,
|
|
231
|
+
fetchImpl: opts.fetchImpl
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export { EmitFindingError, runEmitFinding, runEmitFindings };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { EmittedNameSchema, ARTIFACT_REVIEW_CHECK_NAME, EMITTED_RESULT_MAX_LINK_LENGTH, EMITTED_RESULTS, EMITTED_RESULT_MAX_BODY_LENGTH, resolveRepoIdentity, detectActiveSession, launchRefusalForSession, detectCi } 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
|
+
import { randomUUID } from 'crypto';
|
|
15
|
+
import { readFileSync } from 'fs';
|
|
16
|
+
|
|
17
|
+
createRequire(import.meta.url);
|
|
18
|
+
var GREEN = "\x1B[32m";
|
|
19
|
+
var DIM = "\x1B[2m";
|
|
20
|
+
var RESET = "\x1B[0m";
|
|
21
|
+
var EmitResultError = class extends Error {
|
|
22
|
+
};
|
|
23
|
+
function parseItemFlag(raw) {
|
|
24
|
+
if (raw === void 0) return void 0;
|
|
25
|
+
const number = parseTrackerNumber(raw);
|
|
26
|
+
if (number === void 0) {
|
|
27
|
+
throw new EmitResultError(trackerNumberError("--item", raw));
|
|
28
|
+
}
|
|
29
|
+
return { number };
|
|
30
|
+
}
|
|
31
|
+
async function readAllStdin() {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
34
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
35
|
+
}
|
|
36
|
+
async function readBodyFile(path, readStdin) {
|
|
37
|
+
if (path === "-") return (readStdin ?? readAllStdin)();
|
|
38
|
+
try {
|
|
39
|
+
return readFileSync(path, "utf8");
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new EmitResultError(`cannot read --body-file "${path}": ${String(err)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function runEmitResult(opts) {
|
|
45
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
46
|
+
const home = opts.home ?? homeFromEnv();
|
|
47
|
+
const env = opts.env ?? process.env;
|
|
48
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
49
|
+
if (!EmittedNameSchema.safeParse(opts.name).success) {
|
|
50
|
+
throw new EmitResultError(
|
|
51
|
+
`invalid name "${opts.name}" \u2014 an emit name is 1-64 characters: a lowercase letter followed by lowercase letters, digits, ".", "_" or "-" (e.g. smoke.pass)`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const isArtifactReview = opts.name === ARTIFACT_REVIEW_CHECK_NAME;
|
|
55
|
+
const artifactId = opts.artifact === "" ? void 0 : opts.artifact;
|
|
56
|
+
if (isArtifactReview) {
|
|
57
|
+
if (artifactId === void 0) {
|
|
58
|
+
throw new EmitResultError(
|
|
59
|
+
`--artifact <id> is required for ${ARTIFACT_REVIEW_CHECK_NAME} \u2014 it names which artifact this verdict is on`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
} else if (artifactId !== void 0) {
|
|
63
|
+
throw new EmitResultError(`--artifact is only accepted with ${ARTIFACT_REVIEW_CHECK_NAME}`);
|
|
64
|
+
}
|
|
65
|
+
const link = opts.link === "" ? void 0 : opts.link;
|
|
66
|
+
if (isArtifactReview && link !== void 0) {
|
|
67
|
+
throw new EmitResultError(
|
|
68
|
+
`--link is refused for ${ARTIFACT_REVIEW_CHECK_NAME} \u2014 an artifact verdict is a judgment on evidence already on file, never a run to point at; pass --body / --body-file instead`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (link !== void 0) {
|
|
72
|
+
if (!link.startsWith("http://") && !link.startsWith("https://")) {
|
|
73
|
+
throw new EmitResultError(`invalid --link "${link}" \u2014 expected an http:// or https:// URL`);
|
|
74
|
+
}
|
|
75
|
+
if (/\s/.test(link)) {
|
|
76
|
+
throw new EmitResultError(`invalid --link \u2014 URLs cannot contain whitespace; percent-encode it`);
|
|
77
|
+
}
|
|
78
|
+
if (link.length > EMITTED_RESULT_MAX_LINK_LENGTH) {
|
|
79
|
+
throw new EmitResultError(
|
|
80
|
+
`link is ${link.length} characters \u2014 the cap is ${EMITTED_RESULT_MAX_LINK_LENGTH}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let result = "pass";
|
|
85
|
+
if (opts.result !== void 0) {
|
|
86
|
+
if (!EMITTED_RESULTS.includes(opts.result)) {
|
|
87
|
+
throw new EmitResultError(`invalid --result "${opts.result}" \u2014 expected "pass" or "fail"`);
|
|
88
|
+
}
|
|
89
|
+
result = opts.result;
|
|
90
|
+
}
|
|
91
|
+
if (opts.body !== void 0 && opts.bodyFile !== void 0) {
|
|
92
|
+
throw new EmitResultError(
|
|
93
|
+
`--body and --body-file both name the sentence \u2014 pass one, not both`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
let body;
|
|
97
|
+
if (opts.body !== void 0) {
|
|
98
|
+
body = opts.body;
|
|
99
|
+
} else if (opts.bodyFile !== void 0) {
|
|
100
|
+
body = await readBodyFile(opts.bodyFile, opts.readStdin);
|
|
101
|
+
} else if (isArtifactReview && result === "fail") {
|
|
102
|
+
body = await (opts.readStdin ?? readAllStdin)();
|
|
103
|
+
}
|
|
104
|
+
if (body !== void 0) {
|
|
105
|
+
body = body.trim();
|
|
106
|
+
if (body === "") {
|
|
107
|
+
throw new EmitResultError(
|
|
108
|
+
`the sentence is empty \u2014 --body and --body-file need something to say`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (body.length > EMITTED_RESULT_MAX_BODY_LENGTH) {
|
|
112
|
+
throw new EmitResultError(
|
|
113
|
+
`the sentence is ${body.length} characters \u2014 the cap is ${EMITTED_RESULT_MAX_BODY_LENGTH}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (isArtifactReview) {
|
|
118
|
+
if (result === "fail" && body === void 0) {
|
|
119
|
+
throw new EmitResultError(
|
|
120
|
+
`a failing artifact verdict needs a sentence \u2014 pass --body or --body-file with one sentence saying what is wrong`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} else if (result === "fail" && link === void 0 && body === void 0) {
|
|
124
|
+
throw new EmitResultError(
|
|
125
|
+
`a failing check needs something to act on \u2014 pass --link with the run URL, or --body / --body-file with one sentence saying what is wrong`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const item = parseItemFlag(opts.item);
|
|
129
|
+
const identity = resolveRepoIdentity(cwd);
|
|
130
|
+
const sessionId = detectActiveSession({
|
|
131
|
+
env,
|
|
132
|
+
home,
|
|
133
|
+
cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, cwd] : [cwd],
|
|
134
|
+
nowMs: now().getTime()
|
|
135
|
+
});
|
|
136
|
+
if (isArtifactReview && sessionId !== void 0) {
|
|
137
|
+
throw new EmitResultError(
|
|
138
|
+
"an artifact verdict is a person's act \u2014 record it with a person's API key, outside a recorded session."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!isArtifactReview) {
|
|
142
|
+
if (item === void 0 && sessionId === void 0) {
|
|
143
|
+
throw new EmitResultError(
|
|
144
|
+
"nothing to attach this to \u2014 no work item. Pass --item <number>; a recorded session alone does not name one, and a session with no --item is always refused as self-judgment."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (sessionId !== void 0) {
|
|
148
|
+
const refusal = launchRefusalForSession(sessionId, home);
|
|
149
|
+
if (refusal !== void 0) throw new EmitResultError(refusal);
|
|
150
|
+
}
|
|
151
|
+
if (item === void 0 && sessionId !== void 0) {
|
|
152
|
+
throw new EmitResultError(
|
|
153
|
+
"a session records a check only on a work item it was not launched for, named with --item \u2014 the item this session was launched for needs a machine key (an API key run outside the recorded session) or CI."
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const fileConfig = readCloudConfig(home);
|
|
158
|
+
const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
|
|
159
|
+
const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
|
|
160
|
+
const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
|
|
161
|
+
if (!url || !apiKey || !appId) {
|
|
162
|
+
const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
|
|
163
|
+
throw new EmitResultError(
|
|
164
|
+
`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.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const ci = detectCi(env);
|
|
168
|
+
const request = {
|
|
169
|
+
schemaVersion: 1,
|
|
170
|
+
emit: {
|
|
171
|
+
clientEmitId: randomUUID(),
|
|
172
|
+
name: opts.name,
|
|
173
|
+
result,
|
|
174
|
+
...link !== void 0 ? { link } : {},
|
|
175
|
+
...body !== void 0 ? { body } : {},
|
|
176
|
+
emittedAt: now().toISOString(),
|
|
177
|
+
...ci ? { ci: true } : {},
|
|
178
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
179
|
+
...item !== void 0 ? { item } : {},
|
|
180
|
+
...isArtifactReview ? { artifactId } : {}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
184
|
+
const endpoint = new URL("/v1/emitted-results", url).toString();
|
|
185
|
+
let response;
|
|
186
|
+
try {
|
|
187
|
+
response = await fetchImpl(endpoint, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: {
|
|
190
|
+
"content-type": "application/json",
|
|
191
|
+
authorization: `Bearer ${apiKey}`,
|
|
192
|
+
"x-outerlayer-app-id": appId
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify(request)
|
|
195
|
+
});
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw new EmitResultError(`network error reaching ${endpoint}: ${String(err)}`);
|
|
198
|
+
}
|
|
199
|
+
if (response.status === 401) {
|
|
200
|
+
throw new EmitResultError(
|
|
201
|
+
"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."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
let detail = "";
|
|
206
|
+
try {
|
|
207
|
+
detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 160);
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
throw new EmitResultError(`emit failed (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
211
|
+
}
|
|
212
|
+
let data = {};
|
|
213
|
+
try {
|
|
214
|
+
data = (await response.json()).data ?? {};
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
const anchorLabel = isArtifactReview ? `artifact ${artifactId}` : `item #${item.number}`;
|
|
218
|
+
const recordedId = typeof data.id === "string" ? data.id : "";
|
|
219
|
+
const idSuffix = recordedId ? ` ${DIM}\xB7${RESET} ${recordedId}` : "";
|
|
220
|
+
const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} emitted ${opts.name} (${result}) ${DIM}\xB7${RESET} ${anchorLabel}${idSuffix}`;
|
|
221
|
+
if (!opts.quiet) process.stdout.write(output + "\n");
|
|
222
|
+
return { data, output, exitCode: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { EmitResultError, runEmitResult };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { AUTO_SYNC_DEBOUNCE_MS, WORK_ADD_RETRIES, contextPossiblyGoverned, handleWorkLaunch, hookFiredMarkerPath, maybeAutoSync, runHookFast, spoolDir } from './chunk-A6NNKRJU.js';
|
|
4
|
+
import './chunk-IWIUYLDR.js';
|
|
5
|
+
import './chunk-VNVZDWO3.js';
|
|
6
|
+
import './chunk-Z5GEFMRW.js';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
export { hookExecSpoolPath, hookWrapStdio, installAbortForwarding, makeTailCollector, parseHookWrapArgs, runHookWrapFast, spoolDir } from './chunk-YMTXR7PJ.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,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import './chunk-Z5GEFMRW.js';
|
|
4
|
+
import { statSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join, dirname } from 'path';
|
|
6
|
+
|
|
7
|
+
createRequire(import.meta.url);
|
|
8
|
+
var GREEN = "\x1B[32m";
|
|
9
|
+
var DIM = "\x1B[2m";
|
|
10
|
+
var YELLOW = "\x1B[33m";
|
|
11
|
+
var RESET = "\x1B[0m";
|
|
12
|
+
var ImportCaptureError = class extends Error {
|
|
13
|
+
};
|
|
14
|
+
var SKILL_PATH = ".outerlayer/skills/emitting-evidence/SKILL.md";
|
|
15
|
+
var SNIPPET_PATH = ".outerlayer/skills/emitting-evidence/references/agents-snippet.md";
|
|
16
|
+
var AGENTS_MD_PATH = ".outerlayer/AGENTS.md";
|
|
17
|
+
var AGENTS_MD_MARKER = "<!-- outerlayer:capture-pack -->";
|
|
18
|
+
var SKILL_CONTENT = `---
|
|
19
|
+
name: emitting-evidence
|
|
20
|
+
description: >
|
|
21
|
+
Emit artifacts \u2014 screenshots, recordings, reports, logs \u2014 as proof that a
|
|
22
|
+
change works, bound to the pull request via \`outerlayer emit artifact\`.
|
|
23
|
+
Use when finishing work a spec criterion or tracked requirement covers,
|
|
24
|
+
when asked to "provide evidence", "attach a screenshot", or "prove it
|
|
25
|
+
works", and in CI steps that produce verifiable output.
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
An artifact is an exhibit: evidence *of* a specific change, anchored to the
|
|
29
|
+
pull request and rendered in its evidence comment. Emit the few exhibits the
|
|
30
|
+
spec asks for \u2014 a reviewer should see the change working, not a gallery.
|
|
31
|
+
|
|
32
|
+
## When to capture
|
|
33
|
+
|
|
34
|
+
Capture AFTER the state exists, never before: run the app, the test, or the
|
|
35
|
+
flow first, and shoot the working result. A screenshot of code is not
|
|
36
|
+
evidence; a screenshot of the rendered page is. If the state takes setup
|
|
37
|
+
(seed data, a logged-in user), finish the setup, verify by eye, then capture.
|
|
38
|
+
|
|
39
|
+
## Per-kind mechanics
|
|
40
|
+
|
|
41
|
+
Kind is inferred from the file's media type \u2014 name files honestly. Every
|
|
42
|
+
artifact shares one upload cap of 8 MiB, whatever its kind:
|
|
43
|
+
|
|
44
|
+
- **screenshot** (\`.png\`, \`.jpg\`) \u2014 one focused window or region showing the
|
|
45
|
+
proven state. Crop noise; keep enough chrome (URL bar, test summary line)
|
|
46
|
+
to show it is real.
|
|
47
|
+
- **video** (\`.webm\`, \`.mp4\`) \u2014 a short recording of the flow, start to
|
|
48
|
+
outcome. Video is the kind most likely to hit the 8 MiB cap: trim dead
|
|
49
|
+
time, prefer webm.
|
|
50
|
+
- **report** (\`.html\`, \`.pdf\`) \u2014 generated reports: coverage, benchmark,
|
|
51
|
+
audit output. Emit the file the tool produced, unedited.
|
|
52
|
+
- **log** (\`.txt\`, \`.log\`) \u2014 command output proving a run happened: test
|
|
53
|
+
runs, migrations, gate output. Pipe to a file and emit that file.
|
|
54
|
+
|
|
55
|
+
Anything else uploads as plain \`file\` \u2014 it is never guessed into a stronger
|
|
56
|
+
kind, so a \`.mov\` will NOT count where a video is required; convert first.
|
|
57
|
+
|
|
58
|
+
## Captions
|
|
59
|
+
|
|
60
|
+
One sentence, present tense, saying what the exhibit shows and what that
|
|
61
|
+
proves: "Signup blocked for a disallowed domain \u2014 the 403 page renders."
|
|
62
|
+
Never put secrets, tokens, or personal data in the caption \u2014 or the pixels.
|
|
63
|
+
|
|
64
|
+
## Binding with --for
|
|
65
|
+
|
|
66
|
+
When a criterion is the reason you captured, bind it:
|
|
67
|
+
|
|
68
|
+
outerlayer emit artifact shot.png --caption "\u2026" --for LOGIN-01
|
|
69
|
+
|
|
70
|
+
The id comes from your team's own spec or tracker \u2014 wherever criteria for
|
|
71
|
+
this change are written down. The PR comment's Evidence section shows the
|
|
72
|
+
id next to the artifact, and the dashboard's Floor lists it against the
|
|
73
|
+
work item \u2014 but deciding whether a given criterion still needs proof, and
|
|
74
|
+
of what kind, is your team's own check to write, typically a custom
|
|
75
|
+
validator recorded with \`outerlayer emit\`.
|
|
76
|
+
|
|
77
|
+
## The noise rule
|
|
78
|
+
|
|
79
|
+
Satisfy the declared proofs; don't document everything. One exhibit per
|
|
80
|
+
criterion is the norm. An unrequested artifact is worth emitting only when
|
|
81
|
+
it would change how a reviewer reads the diff. When in doubt, leave it out \u2014
|
|
82
|
+
evidence works because there is little of it.
|
|
83
|
+
|
|
84
|
+
## Mechanics
|
|
85
|
+
|
|
86
|
+
Inside a recorded session, just run the command \u2014 the artifact spools
|
|
87
|
+
locally and uploads with the next \`outerlayer sync\`, bound to this session
|
|
88
|
+
and turn. In CI, run it after the step that produced the file (repo and PR
|
|
89
|
+
come from the CI environment). From a plain machine it anchors through the
|
|
90
|
+
git checkout, or pass \`--pr <n>\` explicitly. If there is nothing to attach
|
|
91
|
+
to, the command refuses \u2014 emit from the work, not from nowhere.
|
|
92
|
+
`;
|
|
93
|
+
var AGENTS_SNIPPET_CONTENT = `${AGENTS_MD_MARKER}
|
|
94
|
+
## Emitting evidence
|
|
95
|
+
|
|
96
|
+
When you finish work a spec criterion or tracked requirement covers,
|
|
97
|
+
capture the working state and emit it:
|
|
98
|
+
|
|
99
|
+
outerlayer emit artifact <file> --caption "what it shows" [--for <criterion-id>] [--pr <n>]
|
|
100
|
+
|
|
101
|
+
Rules: capture AFTER the state exists (run it, then shoot it); kind comes
|
|
102
|
+
from the file type (png/jpg screenshot, webm/mp4 video, html/pdf report,
|
|
103
|
+
txt/log log \u2014 anything else is a plain file); every artifact caps at 8
|
|
104
|
+
MiB, and video is the kind most likely to hit it; captions are one
|
|
105
|
+
present-tense sentence with no secrets; bind an artifact to the id of the
|
|
106
|
+
requirement it proves \u2014 \`--for <id>\`, from the team's own spec or
|
|
107
|
+
tracker \u2014 and pick the kind that shows the state best; the platform
|
|
108
|
+
records the kind and the id and shows them next to the artifact; satisfy
|
|
109
|
+
what's asked for and stop \u2014 don't document everything. Inside a recorded
|
|
110
|
+
session the artifact uploads on the next \`outerlayer sync\`; in CI it
|
|
111
|
+
anchors via the CI environment; otherwise the git checkout or \`--pr\`
|
|
112
|
+
anchors it, and with nothing to attach to the command refuses.
|
|
113
|
+
`;
|
|
114
|
+
function writeOutFile(repoRoot, relPath, content) {
|
|
115
|
+
const abs = join(repoRoot, relPath);
|
|
116
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
117
|
+
writeFileSync(abs, content);
|
|
118
|
+
}
|
|
119
|
+
function runImportCapture(opts = {}) {
|
|
120
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
121
|
+
let cwdStat;
|
|
122
|
+
try {
|
|
123
|
+
cwdStat = statSync(cwd);
|
|
124
|
+
} catch {
|
|
125
|
+
throw new ImportCaptureError(`no such directory: ${cwd}`);
|
|
126
|
+
}
|
|
127
|
+
if (!cwdStat.isDirectory()) throw new ImportCaptureError(`not a directory: ${cwd}`);
|
|
128
|
+
const skillAbs = join(cwd, SKILL_PATH);
|
|
129
|
+
if (existsSync(skillAbs) && readFileSync(skillAbs, "utf8") !== SKILL_CONTENT) {
|
|
130
|
+
throw new ImportCaptureError(
|
|
131
|
+
`refusing to import \u2014 ${SKILL_PATH} already exists with local edits (never overwritten; remove it first to reinstall)`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const files = [];
|
|
135
|
+
for (const [relPath, content] of [
|
|
136
|
+
[SKILL_PATH, SKILL_CONTENT],
|
|
137
|
+
[SNIPPET_PATH, AGENTS_SNIPPET_CONTENT]
|
|
138
|
+
]) {
|
|
139
|
+
if (existsSync(join(cwd, relPath))) continue;
|
|
140
|
+
writeOutFile(cwd, relPath, content);
|
|
141
|
+
files.push(relPath);
|
|
142
|
+
}
|
|
143
|
+
let appendedAgentsMd = false;
|
|
144
|
+
const agentsMdAbs = join(cwd, AGENTS_MD_PATH);
|
|
145
|
+
if (existsSync(agentsMdAbs)) {
|
|
146
|
+
const existing = readFileSync(agentsMdAbs, "utf8");
|
|
147
|
+
if (!existing.includes(AGENTS_MD_MARKER)) {
|
|
148
|
+
writeFileSync(agentsMdAbs, existing + "\n" + AGENTS_SNIPPET_CONTENT);
|
|
149
|
+
appendedAgentsMd = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let output;
|
|
153
|
+
if (opts.json) {
|
|
154
|
+
output = JSON.stringify({ files, appendedAgentsMd });
|
|
155
|
+
} else {
|
|
156
|
+
const lines = files.map((f) => `${GREEN}\u2713${RESET} wrote ${f}`);
|
|
157
|
+
if (appendedAgentsMd) lines.push(`${GREEN}\u2713${RESET} appended the evidence snippet to ${AGENTS_MD_PATH}`);
|
|
158
|
+
if (files.length === 0 && !appendedAgentsMd) {
|
|
159
|
+
lines.push(`${DIM}capture pack already installed \u2014 nothing to write${RESET}`);
|
|
160
|
+
}
|
|
161
|
+
lines.push(`Run ${YELLOW}\`outerlayer emit\`${RESET} to compile the pack into your agent's native files.`);
|
|
162
|
+
output = lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
if (!opts.quiet) {
|
|
165
|
+
process.stdout.write(output + "\n");
|
|
166
|
+
if (!opts.json && !appendedAgentsMd && !existsSync(agentsMdAbs)) {
|
|
167
|
+
process.stdout.write(
|
|
168
|
+
`${DIM} no ${AGENTS_MD_PATH} to append to \u2014 paste ${SNIPPET_PATH} into your instructions file if your harness reads no skills${RESET}
|
|
169
|
+
`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { files, appendedAgentsMd, output };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { ImportCaptureError, runImportCapture };
|