@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +202 -0
  3. package/README.md +320 -0
  4. package/dist/build-info.json +1 -0
  5. package/dist/chunk-4H56Y7O6.js +589 -0
  6. package/dist/chunk-A3WLZX2F.js +120 -0
  7. package/dist/chunk-A6NNKRJU.js +650 -0
  8. package/dist/chunk-D77LS3UI.js +42 -0
  9. package/dist/chunk-DEDV5V4U.js +87324 -0
  10. package/dist/chunk-ECSYRPCC.js +334 -0
  11. package/dist/chunk-IWIUYLDR.js +472 -0
  12. package/dist/chunk-JJP7YLMN.js +25 -0
  13. package/dist/chunk-KFYJV2ZG.js +7978 -0
  14. package/dist/chunk-OZ7C3XUE.js +34 -0
  15. package/dist/chunk-RCQXYLMO.js +96 -0
  16. package/dist/chunk-TFUIDMOB.js +34 -0
  17. package/dist/chunk-VNVZDWO3.js +10 -0
  18. package/dist/chunk-VU34RWDU.js +339 -0
  19. package/dist/chunk-WQ6VGRGZ.js +150 -0
  20. package/dist/chunk-XN2XZFTK.js +165 -0
  21. package/dist/chunk-YMTXR7PJ.js +200 -0
  22. package/dist/chunk-YRYMNBGQ.js +54 -0
  23. package/dist/chunk-Z5GEFMRW.js +45 -0
  24. package/dist/chunk-ZJSNLVCE.js +58 -0
  25. package/dist/cli-3JYGL2OB.js +5311 -0
  26. package/dist/config-POF7DEQW.js +7 -0
  27. package/dist/context-materialize-J6CRQ7O7.js +589 -0
  28. package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
  29. package/dist/emit-cmd-TWSYYEZI.js +208 -0
  30. package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
  31. package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
  32. package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
  33. package/dist/hook-fast-CBW4ZETX.js +8 -0
  34. package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
  35. package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
  36. package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +25 -0
  39. package/dist/init-PTBITAUO.js +103 -0
  40. package/dist/login-cmd-IRX6LZT7.js +62 -0
  41. package/dist/logs-TRNPQM42.js +63 -0
  42. package/dist/loop-NFSOBUUJ.js +646 -0
  43. package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
  44. package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
  45. package/dist/paths-D2VGWWFI.js +6 -0
  46. package/dist/pidfile-PTW76F56.js +8 -0
  47. package/dist/status-WGOJTXZF.js +94 -0
  48. package/dist/statusline-fast-Z5U5CEC6.js +8 -0
  49. package/dist/sync-cmd-C5UGI4SF.js +15 -0
  50. package/dist/watch-V3K4PESQ.js +70 -0
  51. package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
  52. package/dist/work-cmd-CW26EZZO.js +16 -0
  53. package/dist/work-launch-YNNCKIH3.js +6 -0
  54. package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
  55. package/package.json +58 -0
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { staleBuildRefusal, staleBuildWarning, ARTIFACT_MAX_CAPTION_LENGTH, ArtifactCriterionIdSchema, ARTIFACT_MAX_FILENAME_LENGTH, readEffectiveArtifactSpoolState, localArtifactIds, isArtifactTombstoned, mediaTypeForArtifactPath, inferArtifactKind, resolveRepoIdentity, detectActiveSession, launchRefusalForSession, artifactBlobsDir, appendArtifactRecord, artifactsSpoolPath, findPendingArtifactRecord, detectCi, appendArtifactTombstone, ARTIFACT_MAX_REPLACES_TARGETS } 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 { createHash, randomUUID } from 'crypto';
15
+ import { statSync, readFileSync, mkdirSync, existsSync, writeFileSync, renameSync, unlinkSync } from 'fs';
16
+ import { resolve, basename, join } from 'path';
17
+
18
+ createRequire(import.meta.url);
19
+ var ARTIFACT_MAX_BYTES = 8 * 1024 * 1024;
20
+ var GREEN = "\x1B[32m";
21
+ var YELLOW = "\x1B[33m";
22
+ var DIM = "\x1B[2m";
23
+ var RESET = "\x1B[0m";
24
+ var EmitArtifactError = class extends Error {
25
+ };
26
+ var ARTIFACT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
27
+ function parseReplacesTargets(raw) {
28
+ const parts = raw.split(",");
29
+ const targets = [];
30
+ for (let i = 0; i < parts.length; i += 1) {
31
+ const id = parts[i];
32
+ if (id === "") {
33
+ throw new EmitArtifactError(`invalid --replaces "${raw}" \u2014 entry ${i + 1} is empty`);
34
+ }
35
+ if (!ARTIFACT_ID_PATTERN.test(id)) {
36
+ throw new EmitArtifactError(`invalid --replaces "${raw}" \u2014 "${id}" is not a valid artifact id`);
37
+ }
38
+ targets.push(id);
39
+ }
40
+ const seen = /* @__PURE__ */ new Set();
41
+ for (const id of targets) {
42
+ if (seen.has(id)) {
43
+ throw new EmitArtifactError(`invalid --replaces "${raw}" \u2014 "${id}" is named more than once`);
44
+ }
45
+ seen.add(id);
46
+ }
47
+ if (targets.length > ARTIFACT_MAX_REPLACES_TARGETS) {
48
+ throw new EmitArtifactError(
49
+ `invalid --replaces "${raw}" \u2014 names ${targets.length} targets, the cap is ${ARTIFACT_MAX_REPLACES_TARGETS}`
50
+ );
51
+ }
52
+ return targets;
53
+ }
54
+ function replacesSuffix(targets) {
55
+ if (targets === void 0 || targets.length === 0) return "";
56
+ if (targets.length === 1) return ` ${DIM}\xB7${RESET} replaces ${targets[0]}`;
57
+ return ` ${DIM}\xB7${RESET} replaces ${targets.length}: ${targets.join(", ")}`;
58
+ }
59
+ function prNumberFromCiEnv(env) {
60
+ const refMatch = env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\//);
61
+ if (refMatch) {
62
+ const n = parseTrackerNumber(refMatch[1]);
63
+ if (n !== void 0) return n;
64
+ }
65
+ if (env.GITHUB_EVENT_PATH) {
66
+ try {
67
+ const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, "utf8"));
68
+ const n = event.pull_request?.number;
69
+ if (typeof n === "number") {
70
+ const parsed = parseTrackerNumber(n);
71
+ if (parsed !== void 0) return parsed;
72
+ }
73
+ } catch {
74
+ }
75
+ }
76
+ return void 0;
77
+ }
78
+ async function runEmitArtifact(opts) {
79
+ const env = opts.env ?? process.env;
80
+ const refusal = staleBuildRefusal(env);
81
+ if (refusal) throw new EmitArtifactError(refusal);
82
+ const stale = staleBuildWarning();
83
+ if (stale && !opts.quiet) process.stderr.write(`${YELLOW}!${RESET} ${stale}
84
+ `);
85
+ const cwd = opts.cwd ?? process.cwd();
86
+ const home = opts.home ?? homeFromEnv(env);
87
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
88
+ const caption = opts.caption;
89
+ if (caption === void 0 || caption === "") {
90
+ throw new EmitArtifactError(
91
+ `missing --caption \u2014 say what this artifact proves, e.g. --caption "checkout flow passes"`
92
+ );
93
+ }
94
+ if (caption.length > ARTIFACT_MAX_CAPTION_LENGTH) {
95
+ throw new EmitArtifactError(
96
+ `caption is ${caption.length} characters \u2014 the cap is ${ARTIFACT_MAX_CAPTION_LENGTH}`
97
+ );
98
+ }
99
+ if (opts.criterionId !== void 0 && !ArtifactCriterionIdSchema.safeParse(opts.criterionId).success) {
100
+ throw new EmitArtifactError(
101
+ `invalid --for "${opts.criterionId}" \u2014 a criterion id is 1-64 characters of letters, digits, "_", ".", ":" or "-" (e.g. LOGIN-01)`
102
+ );
103
+ }
104
+ const criterionId = opts.criterionId;
105
+ let prNumber;
106
+ if (opts.pr !== void 0) {
107
+ if (parseTrackerNumber(opts.pr) === void 0) {
108
+ throw new EmitArtifactError(trackerNumberError("--pr", opts.pr));
109
+ }
110
+ prNumber = opts.pr;
111
+ }
112
+ const replacesTargets = opts.replaces !== void 0 ? parseReplacesTargets(opts.replaces) : void 0;
113
+ const cancelReplacedTargetLocally = (targetId, correctionCarriesReplaces) => {
114
+ try {
115
+ appendArtifactTombstone(targetId, home, now);
116
+ } catch (err) {
117
+ if (!opts.quiet) {
118
+ const consequence = correctionCarriesReplaces ? "the correction still names it, so the server supersedes it once both upload" : "it is still queued locally and will upload on the next outerlayer sync \u2014 nothing on the wire links it to this correction";
119
+ process.stderr.write(
120
+ `${YELLOW}!${RESET} could not cancel replaced artifact ${targetId} locally: ${String(err)} \u2014 ${consequence}
121
+ `
122
+ );
123
+ }
124
+ }
125
+ };
126
+ const filePath = resolve(cwd, opts.file);
127
+ let stat;
128
+ try {
129
+ stat = statSync(filePath);
130
+ } catch {
131
+ throw new EmitArtifactError(`no such file: ${opts.file}`);
132
+ }
133
+ if (!stat.isFile()) throw new EmitArtifactError(`not a regular file: ${opts.file}`);
134
+ const bytes = readFileSync(filePath);
135
+ if (bytes.length > ARTIFACT_MAX_BYTES) {
136
+ throw new EmitArtifactError(
137
+ `artifact exceeds 8 MiB (${(bytes.length / (1024 * 1024)).toFixed(1)} MiB) \u2014 the gateway refuses larger uploads`
138
+ );
139
+ }
140
+ const filename = basename(filePath);
141
+ if (filename.length > ARTIFACT_MAX_FILENAME_LENGTH) {
142
+ throw new EmitArtifactError(
143
+ `filename is ${filename.length} characters \u2014 the cap is ${ARTIFACT_MAX_FILENAME_LENGTH}`
144
+ );
145
+ }
146
+ if (replacesTargets !== void 0 && replacesTargets.length > 1) {
147
+ const { state } = readEffectiveArtifactSpoolState(home);
148
+ const localIds = localArtifactIds(home);
149
+ for (const target of replacesTargets) {
150
+ const entry = state.get(target);
151
+ if (entry === void 0) {
152
+ if (localIds.has(target)) {
153
+ throw new EmitArtifactError(
154
+ `--replaces "${target}" is still waiting in the local spool \u2014 run \`outerlayer sync\` first, then retry the correction.`
155
+ );
156
+ }
157
+ continue;
158
+ }
159
+ if (entry.state === "held" || entry.state === "pending") {
160
+ throw new EmitArtifactError(
161
+ `--replaces "${target}" is still waiting in the local spool \u2014 run \`outerlayer sync\` first, then retry the correction.`
162
+ );
163
+ }
164
+ if (entry.state === "dropped" && !entry.reachedServer) {
165
+ if (isArtifactTombstoned(target, home)) {
166
+ throw new EmitArtifactError(
167
+ `--replaces "${target}" has already been dropped locally, or its upload was never confirmed \u2014 run \`outerlayer sync\` first, then retry the correction.`
168
+ );
169
+ }
170
+ throw new EmitArtifactError(
171
+ `--replaces "${target}" was permanently dropped by an earlier sync and never reached the server \u2014 there is nothing left to sync, so it cannot be named in a correction; drop it from --replaces, or re-emit the original evidence fresh.`
172
+ );
173
+ }
174
+ }
175
+ }
176
+ const mediaType = mediaTypeForArtifactPath(filePath);
177
+ const kind = inferArtifactKind(mediaType);
178
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
179
+ const identity = resolveRepoIdentity(cwd);
180
+ const sessionId = detectActiveSession({
181
+ env,
182
+ home,
183
+ cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, cwd] : [cwd],
184
+ nowMs: now().getTime()
185
+ });
186
+ if (sessionId !== void 0) {
187
+ const refusal2 = launchRefusalForSession(sessionId, home);
188
+ if (refusal2 !== void 0) throw new EmitArtifactError(refusal2);
189
+ mkdirSync(artifactBlobsDir(home), { recursive: true });
190
+ const blobPath = join(artifactBlobsDir(home), sha256);
191
+ const blobExisted = existsSync(blobPath);
192
+ if (!blobExisted) {
193
+ const tmpPath = join(artifactBlobsDir(home), `.tmp-${randomUUID()}`);
194
+ writeFileSync(tmpPath, bytes);
195
+ renameSync(tmpPath, blobPath);
196
+ }
197
+ const record = {
198
+ rec: "artifact",
199
+ artifactId: randomUUID(),
200
+ t: now().toISOString(),
201
+ sessionId,
202
+ cwd,
203
+ ...identity.gitRepo !== void 0 ? { gitRepo: identity.gitRepo } : {},
204
+ ...identity.gitBranch !== void 0 ? { gitBranch: identity.gitBranch } : {},
205
+ ...identity.commitSha !== void 0 ? { commitSha: identity.commitSha } : {},
206
+ ...prNumber !== void 0 ? { prNumber } : {},
207
+ filename,
208
+ mediaType,
209
+ bytes: bytes.length,
210
+ sha256,
211
+ caption,
212
+ ...criterionId !== void 0 ? { criterionId } : {},
213
+ ...replacesTargets !== void 0 ? { replaces: replacesTargets[0] } : {},
214
+ ...replacesTargets !== void 0 && replacesTargets.length > 1 ? { replacesTargets } : {}
215
+ };
216
+ try {
217
+ appendArtifactRecord(record, home);
218
+ } catch (err) {
219
+ if (!blobExisted) {
220
+ try {
221
+ unlinkSync(blobPath);
222
+ } catch {
223
+ }
224
+ }
225
+ throw new EmitArtifactError(
226
+ `failed to record the artifact in the spool (${artifactsSpoolPath(home)}): ${String(err)} \u2014 the artifact was NOT captured; fix the spool path and re-run`
227
+ );
228
+ }
229
+ if (replacesTargets !== void 0 && replacesTargets.length === 1) {
230
+ const target = replacesTargets[0];
231
+ if (findPendingArtifactRecord(target, home) !== void 0) {
232
+ cancelReplacedTargetLocally(target, true);
233
+ }
234
+ }
235
+ const output2 = opts.json ? JSON.stringify({ spooled: true, record }) : `${GREEN}\u2713${RESET} artifact spooled ${DIM}\xB7${RESET} id ${record.artifactId} ${DIM}\xB7${RESET} ${kind} ${filename} ${DIM}\xB7${RESET} session ${sessionId.slice(0, 8)}${replacesSuffix(replacesTargets)} ${DIM}\xB7${RESET} uploads on the next ${YELLOW}outerlayer sync${RESET}`;
236
+ if (!opts.quiet) process.stdout.write(output2 + "\n");
237
+ return { spooled: true, record, output: output2 };
238
+ }
239
+ const ci = detectCi(env);
240
+ const repository = env.GITHUB_REPOSITORY;
241
+ prNumber ??= prNumberFromCiEnv(env);
242
+ if (prNumber === void 0 && identity.gitRepo === void 0) {
243
+ if (identity.repoRoot !== void 0) {
244
+ throw new EmitArtifactError(
245
+ "nothing to attach this to \u2014 this checkout has no git remote to identify the repo (and no recorded session or PR). Add a remote (git remote add origin <url>), pass --pr <number>, or run inside a recorded Claude Code session."
246
+ );
247
+ }
248
+ throw new EmitArtifactError(
249
+ "nothing to attach this to \u2014 no recorded session, no PR, and no git repo. Run inside a recorded Claude Code session, from a git checkout, in CI with PR context, or pass --pr <number>."
250
+ );
251
+ }
252
+ const fileConfig = readCloudConfig(home);
253
+ const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
254
+ const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
255
+ const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
256
+ if (!url || !apiKey || !appId) {
257
+ const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
258
+ throw new EmitArtifactError(
259
+ `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.`
260
+ );
261
+ }
262
+ const clientArtifactId = randomUUID();
263
+ const singleTarget = replacesTargets !== void 0 && replacesTargets.length === 1 ? replacesTargets[0] : void 0;
264
+ const targetPendingLocally = singleTarget !== void 0 && findPendingArtifactRecord(singleTarget, home) !== void 0;
265
+ const directTargets = targetPendingLocally ? void 0 : replacesTargets;
266
+ const request = {
267
+ schemaVersion: 1,
268
+ artifact: {
269
+ clientArtifactId,
270
+ filename,
271
+ mediaType,
272
+ bytes: bytes.length,
273
+ sha256,
274
+ caption,
275
+ ...criterionId !== void 0 ? { criterionId } : {},
276
+ emittedAt: now().toISOString(),
277
+ ci,
278
+ ...prNumber !== void 0 ? { prNumber } : {},
279
+ ...repository !== void 0 ? { repository } : {},
280
+ ...identity.gitRepo !== void 0 ? { gitRepo: identity.gitRepo } : {},
281
+ ...identity.gitBranch !== void 0 ? { gitBranch: identity.gitBranch } : {},
282
+ ...identity.commitSha !== void 0 ? { commitSha: identity.commitSha } : {},
283
+ ...directTargets !== void 0 && directTargets.length > 0 ? { replaces: directTargets[0] } : {},
284
+ ...directTargets !== void 0 && directTargets.length > 1 ? { replacesTargets: directTargets } : {}
285
+ },
286
+ blob: { data: bytes.toString("base64") }
287
+ };
288
+ const fetchImpl = opts.fetchImpl ?? fetch;
289
+ const endpoint = new URL("/v1/artifacts", url).toString();
290
+ let response;
291
+ try {
292
+ response = await fetchImpl(endpoint, {
293
+ method: "POST",
294
+ headers: {
295
+ "content-type": "application/json",
296
+ authorization: `Bearer ${apiKey}`,
297
+ "x-outerlayer-app-id": appId
298
+ },
299
+ body: JSON.stringify(request)
300
+ });
301
+ } catch (err) {
302
+ throw new EmitArtifactError(`network error reaching ${endpoint}: ${String(err)}`);
303
+ }
304
+ if (response.status === 401) {
305
+ throw new EmitArtifactError(
306
+ "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."
307
+ );
308
+ }
309
+ if (!response.ok) {
310
+ let detail = "";
311
+ try {
312
+ detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 160);
313
+ } catch {
314
+ }
315
+ throw new EmitArtifactError(`artifact upload failed (${response.status})${detail ? `: ${detail}` : ""}`);
316
+ }
317
+ if (targetPendingLocally && singleTarget !== void 0) {
318
+ cancelReplacedTargetLocally(singleTarget, false);
319
+ }
320
+ let data = {};
321
+ try {
322
+ data = (await response.json()).data ?? {};
323
+ } catch {
324
+ }
325
+ const provenance = typeof data.provenance === "string" ? data.provenance : "unknown";
326
+ const output = opts.json ? JSON.stringify({
327
+ ...data,
328
+ clientArtifactId,
329
+ // Gated on > 1, matching the wire/spool shape rule (AC-120-03): a
330
+ // one-element replacesTargets is a shape every schema in this
331
+ // system rejects (min 2) — `--json` must not hand a consumer a
332
+ // value that 400s if fed back onto any of those surfaces.
333
+ ...directTargets !== void 0 && directTargets.length > 1 ? { replacesTargets: directTargets } : {}
334
+ }) : `${GREEN}\u2713${RESET} artifact accepted ${DIM}\xB7${RESET} id ${clientArtifactId} ${DIM}\xB7${RESET} ${kind} ${filename} ${DIM}\xB7${RESET} provenance ${provenance}` + (prNumber !== void 0 ? ` ${DIM}\xB7${RESET} pr #${prNumber}` : "") + replacesSuffix(directTargets);
335
+ if (!opts.quiet) process.stdout.write(output + "\n");
336
+ return { spooled: false, data: { ...data, clientArtifactId }, output };
337
+ }
338
+
339
+ export { EmitArtifactError, runEmitArtifact };
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { emitTree, parseOuterlayerConfig, classifyTree, generatedHeaderSource, CONTEXT_PIN_FILENAME, normalizePath } from './chunk-KFYJV2ZG.js';
4
+ import './chunk-Z5GEFMRW.js';
5
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync, readdirSync } from 'fs';
6
+ import { join, dirname, relative, sep } from 'path';
7
+
8
+ createRequire(import.meta.url);
9
+ var CONTENT_KINDS = /* @__PURE__ */ new Set(["instructions", "skill", "skill-reference", "command", "mcp"]);
10
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
11
+ var NO_CONFIG_MESSAGE = 'no .outerlayer/config.json with a non-empty targets array \u2014 create one, e.g. {"targets": ["claude-code"]}';
12
+ var GREEN = "\x1B[32m";
13
+ var YELLOW = "\x1B[33m";
14
+ var RED = "\x1B[31m";
15
+ var DIM = "\x1B[2m";
16
+ var RESET = "\x1B[0m";
17
+ var EmitError = class extends Error {
18
+ };
19
+ function resolveTargetsFromConfig(repoRoot) {
20
+ const configPath = join(repoRoot, ".outerlayer", "config.json");
21
+ if (!existsSync(configPath)) throw new EmitError(NO_CONFIG_MESSAGE);
22
+ const raw = readFileSync(configPath, "utf8");
23
+ const result = parseOuterlayerConfig(raw);
24
+ if (result.ok) return result.config.targets;
25
+ if (result.errors.some((e) => e.code === "no_targets")) throw new EmitError(NO_CONFIG_MESSAGE);
26
+ const detail = result.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
27
+ throw new EmitError(`invalid .outerlayer/config.json \u2014 ${detail}`);
28
+ }
29
+ function toRepoRelative(repoRoot, absPath) {
30
+ return normalizePath(relative(repoRoot, absPath).split(sep).join("/"));
31
+ }
32
+ function discoverOuterlayerPaths(repoRoot) {
33
+ const out = [];
34
+ walk(repoRoot, repoRoot, false, out);
35
+ return out.sort();
36
+ }
37
+ function walk(dir, repoRoot, insideOuterlayer, out) {
38
+ let entries;
39
+ try {
40
+ entries = readdirSync(dir, { withFileTypes: true });
41
+ } catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ if (entry.isSymbolicLink()) continue;
46
+ const full = join(dir, entry.name);
47
+ if (entry.isDirectory()) {
48
+ if (!insideOuterlayer && SKIP_DIRS.has(entry.name)) continue;
49
+ if (!insideOuterlayer && existsSync(join(full, CONTEXT_PIN_FILENAME))) continue;
50
+ walk(full, repoRoot, insideOuterlayer || entry.name === ".outerlayer", out);
51
+ } else if (entry.isFile() && insideOuterlayer) {
52
+ out.push(toRepoRelative(repoRoot, full));
53
+ }
54
+ }
55
+ }
56
+ function buildEmitInput(repoRoot, targets) {
57
+ const discovered = discoverOuterlayerPaths(repoRoot);
58
+ const { entries } = classifyTree(discovered);
59
+ const classified = new Set(entries.map((e) => e.path));
60
+ const contents = /* @__PURE__ */ new Map();
61
+ for (const entry of entries) {
62
+ if (!CONTENT_KINDS.has(entry.kind)) continue;
63
+ contents.set(entry.path, readFileSync(join(repoRoot, entry.path), "utf8"));
64
+ }
65
+ const assetPaths = discovered.filter((p) => !classified.has(p));
66
+ return { input: { entries, contents, assetPaths, targets }, sourcePaths: classified };
67
+ }
68
+ function writeOutputs(repoRoot, result) {
69
+ const files = [];
70
+ for (const file of result.files) {
71
+ const abs = join(repoRoot, file.path);
72
+ mkdirSync(dirname(abs), { recursive: true });
73
+ writeFileSync(abs, file.content);
74
+ files.push(file.path);
75
+ }
76
+ const copies = [];
77
+ for (const copy of result.copies) {
78
+ const abs = join(repoRoot, copy.toPath);
79
+ mkdirSync(dirname(abs), { recursive: true });
80
+ copyFileSync(join(repoRoot, copy.fromSourcePath), abs);
81
+ copies.push(copy.toPath);
82
+ }
83
+ return { files, copies };
84
+ }
85
+ function renderSuccess(targets, files, copies) {
86
+ const copySuffix = copies.length > 0 ? `, ${copies.length} ${copies.length === 1 ? "copy" : "copies"}` : "";
87
+ return `${GREEN}\u2713${RESET} emitted ${files.length} file${files.length === 1 ? "" : "s"}${copySuffix} for ${DIM}${targets.join(", ")}${RESET}`;
88
+ }
89
+ function renderFailure(errors) {
90
+ const lines = [`${RED}\u2717${RESET} emit failed \u2014 nothing written`];
91
+ for (const e of errors) lines.push(` ${RED}\u2717${RESET} ${e.path}: ${e.message}`);
92
+ return lines.join("\n");
93
+ }
94
+ function renderCheck(targets, drift) {
95
+ if (drift.length === 0) return `${GREEN}\u2713${RESET} up to date for ${DIM}${targets.join(", ")}${RESET}`;
96
+ const lines = [`${RED}\u2717${RESET} drift detected (${drift.length}) \u2014 run \`outerlayer emit\` to update`];
97
+ for (const d of drift) lines.push(` ${RED}\u2717${RESET} ${d.kind.padEnd(8)} ${d.path}`);
98
+ return lines.join("\n");
99
+ }
100
+ function compareFile(repoRoot, path, expectedContent) {
101
+ const abs = join(repoRoot, path);
102
+ if (!existsSync(abs)) return { kind: "missing", path };
103
+ if (readFileSync(abs, "utf8") !== expectedContent) return { kind: "content", path };
104
+ return void 0;
105
+ }
106
+ function compareCopy(repoRoot, copy) {
107
+ const abs = join(repoRoot, copy.toPath);
108
+ if (!existsSync(abs)) return { kind: "missing", path: copy.toPath };
109
+ if (!readFileSync(abs).equals(readFileSync(join(repoRoot, copy.fromSourcePath)))) {
110
+ return { kind: "content", path: copy.toPath };
111
+ }
112
+ return void 0;
113
+ }
114
+ function discoverAllFiles(repoRoot) {
115
+ const out = [];
116
+ walkAll(repoRoot, repoRoot, out);
117
+ return out;
118
+ }
119
+ function walkAll(dir, repoRoot, out) {
120
+ let entries;
121
+ try {
122
+ entries = readdirSync(dir, { withFileTypes: true });
123
+ } catch {
124
+ return;
125
+ }
126
+ for (const entry of entries) {
127
+ if (entry.isSymbolicLink()) continue;
128
+ const full = join(dir, entry.name);
129
+ if (entry.isDirectory()) {
130
+ if (SKIP_DIRS.has(entry.name) || entry.name === ".outerlayer") continue;
131
+ walkAll(full, repoRoot, out);
132
+ } else if (entry.isFile()) {
133
+ out.push(toRepoRelative(repoRoot, full));
134
+ }
135
+ }
136
+ }
137
+ function findOrphans(repoRoot, computedFiles, sourcePaths) {
138
+ const orphans = [];
139
+ for (const path of discoverAllFiles(repoRoot)) {
140
+ if (computedFiles.has(path)) continue;
141
+ let content;
142
+ try {
143
+ content = readFileSync(join(repoRoot, path), "utf8");
144
+ } catch {
145
+ continue;
146
+ }
147
+ const sourcePath = generatedHeaderSource(content);
148
+ if (sourcePath === null) continue;
149
+ if (!sourcePaths.has(sourcePath)) orphans.push({ kind: "orphaned", path });
150
+ }
151
+ return orphans;
152
+ }
153
+ function runEmit(opts = {}) {
154
+ const cwd = opts.cwd ?? process.cwd();
155
+ if (!existsSync(cwd)) throw new EmitError(`no such directory: ${cwd}`);
156
+ const targets = resolveTargetsFromConfig(cwd);
157
+ const built = buildEmitInput(cwd, targets);
158
+ const result = emitTree(built.input);
159
+ const hasErrors = result.errors.length > 0;
160
+ if (opts.check) return runCheck(cwd, opts, targets, result, hasErrors, built.sourcePaths);
161
+ const written = hasErrors ? { files: [], copies: [] } : writeOutputs(cwd, result);
162
+ const exitCode = hasErrors ? 1 : 0;
163
+ const output = opts.json ? JSON.stringify({ targets, files: written.files, copies: written.copies, warnings: result.warnings, errors: result.errors }) : hasErrors ? renderFailure(result.errors) : renderSuccess(targets, written.files, written.copies);
164
+ if (!opts.quiet) {
165
+ if (opts.json) {
166
+ process.stdout.write(output + "\n");
167
+ } else if (hasErrors) {
168
+ process.stderr.write(output + "\n");
169
+ } else {
170
+ process.stdout.write(output + "\n");
171
+ for (const w of result.warnings) process.stderr.write(` ${YELLOW}!${RESET} ${w.path}: ${w.message}
172
+ `);
173
+ }
174
+ }
175
+ return { targets, files: written.files, copies: written.copies, warnings: result.warnings, errors: result.errors, output, exitCode };
176
+ }
177
+ function runCheck(cwd, opts, targets, result, hasErrors, sourcePaths) {
178
+ const drift = [];
179
+ if (!hasErrors) {
180
+ const computedFiles = new Set(result.files.map((f) => f.path));
181
+ for (const file of result.files) {
182
+ const d = compareFile(cwd, file.path, file.content);
183
+ if (d) drift.push(d);
184
+ }
185
+ for (const copy of result.copies) {
186
+ const d = compareCopy(cwd, copy);
187
+ if (d) drift.push(d);
188
+ }
189
+ drift.push(...findOrphans(cwd, computedFiles, sourcePaths));
190
+ drift.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
191
+ }
192
+ const exitCode = hasErrors || drift.length > 0 ? 1 : 0;
193
+ const output = opts.json ? JSON.stringify({ targets, drift, warnings: result.warnings, errors: result.errors }) : hasErrors ? renderFailure(result.errors) : renderCheck(targets, drift);
194
+ if (!opts.quiet) {
195
+ if (opts.json) {
196
+ process.stdout.write(output + "\n");
197
+ } else if (hasErrors || drift.length > 0) {
198
+ process.stderr.write(output + "\n");
199
+ } else {
200
+ process.stdout.write(output + "\n");
201
+ for (const w of result.warnings) process.stderr.write(` ${YELLOW}!${RESET} ${w.path}: ${w.message}
202
+ `);
203
+ }
204
+ }
205
+ return { targets, files: [], copies: [], warnings: result.warnings, errors: result.errors, drift, output, exitCode };
206
+ }
207
+
208
+ export { EmitError, runEmit };
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { resolveRepoIdentity, emitLaunchRefusal } from './chunk-DEDV5V4U.js';
4
+ import './chunk-YRYMNBGQ.js';
5
+ import { parseTrackerNumber, trackerNumberError } from './chunk-RCQXYLMO.js';
6
+ import './chunk-ZJSNLVCE.js';
7
+ import './chunk-A6NNKRJU.js';
8
+ import './chunk-IWIUYLDR.js';
9
+ import './chunk-YMTXR7PJ.js';
10
+ import './chunk-ECSYRPCC.js';
11
+ import { readCloudConfig } from './chunk-TFUIDMOB.js';
12
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
13
+ import './chunk-Z5GEFMRW.js';
14
+
15
+ createRequire(import.meta.url);
16
+
17
+ // src/emit-commit-credit-cmd.ts
18
+ var GREEN = "\x1B[32m";
19
+ var DIM = "\x1B[2m";
20
+ var RESET = "\x1B[0m";
21
+ var FULL_SHA = /^[0-9a-f]{40}$/i;
22
+ var EmitCommitCreditError = class extends Error {
23
+ };
24
+ async function runEmitCommitCredit(opts) {
25
+ const cwd = opts.cwd ?? process.cwd();
26
+ const home = opts.home ?? homeFromEnv();
27
+ const env = opts.env ?? process.env;
28
+ const shaInput = opts.sha ?? "";
29
+ if (!FULL_SHA.test(shaInput)) {
30
+ throw new EmitCommitCreditError(
31
+ `invalid --sha "${opts.sha ?? ""}" \u2014 expected a full 40-character hex SHA (git's abbreviated form isn't enough)`
32
+ );
33
+ }
34
+ const sha = shaInput.toLowerCase();
35
+ if (opts.pr === void 0) {
36
+ throw new EmitCommitCreditError("--pr is required \u2014 an attestation carries its PR anchor at emit time");
37
+ }
38
+ const prNumber = parseTrackerNumber(opts.pr);
39
+ if (prNumber === void 0) {
40
+ throw new EmitCommitCreditError(trackerNumberError("--pr", opts.pr));
41
+ }
42
+ const identity = resolveRepoIdentity(cwd);
43
+ const repository = env.GITHUB_REPOSITORY;
44
+ const gitRepo = repository === void 0 ? identity.gitRepo : void 0;
45
+ if (repository === void 0 && gitRepo === void 0) {
46
+ throw new EmitCommitCreditError(
47
+ "nothing to attach this to \u2014 no repository. Run in CI (GITHUB_REPOSITORY) or from a git checkout with a remote."
48
+ );
49
+ }
50
+ const refusal = emitLaunchRefusal({
51
+ env,
52
+ home,
53
+ cwds: identity.repoRoot !== void 0 ? [identity.repoRoot, cwd] : [cwd],
54
+ nowMs: Date.now()
55
+ });
56
+ if (refusal !== void 0) throw new EmitCommitCreditError(refusal);
57
+ const fileConfig = readCloudConfig(home);
58
+ const url = opts.url ?? env.OUTERLAYER_URL ?? fileConfig.url;
59
+ const apiKey = opts.apiKey ?? env.OUTERLAYER_API_KEY ?? fileConfig.apiKey;
60
+ const appId = opts.appId ?? env.OUTERLAYER_APP_ID ?? fileConfig.appId;
61
+ if (!url || !apiKey || !appId) {
62
+ const missing = [!url && "--url", !apiKey && "--api-key", !appId && "--app-id"].filter(Boolean).join(", ");
63
+ throw new EmitCommitCreditError(
64
+ `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.`
65
+ );
66
+ }
67
+ const request = {
68
+ schemaVersion: 1,
69
+ commitCredit: {
70
+ sha,
71
+ prNumber,
72
+ ...repository !== void 0 ? { repository } : {},
73
+ ...gitRepo !== void 0 ? { gitRepo } : {}
74
+ }
75
+ };
76
+ const fetchImpl = opts.fetchImpl ?? fetch;
77
+ let endpoint;
78
+ try {
79
+ endpoint = new URL("/v1/commit-credit", url).toString();
80
+ } catch {
81
+ throw new EmitCommitCreditError(`invalid --url "${url}" \u2014 not a URL`);
82
+ }
83
+ let response;
84
+ try {
85
+ response = await fetchImpl(endpoint, {
86
+ method: "POST",
87
+ headers: {
88
+ "content-type": "application/json",
89
+ authorization: `Bearer ${apiKey}`,
90
+ "x-outerlayer-app-id": appId
91
+ },
92
+ body: JSON.stringify(request)
93
+ });
94
+ } catch (err) {
95
+ throw new EmitCommitCreditError(`network error reaching ${endpoint}: ${String(err)}`);
96
+ }
97
+ if (response.status === 401) {
98
+ throw new EmitCommitCreditError(
99
+ "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."
100
+ );
101
+ }
102
+ if (!response.ok) {
103
+ let detail = "";
104
+ try {
105
+ detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 160);
106
+ } catch {
107
+ }
108
+ throw new EmitCommitCreditError(`commit-credit emit failed (${response.status})${detail ? `: ${detail}` : ""}`);
109
+ }
110
+ let data = {};
111
+ try {
112
+ data = (await response.json()).data ?? {};
113
+ } catch {
114
+ }
115
+ const output = opts.json ? JSON.stringify(data) : `${GREEN}\u2713${RESET} attested ${sha.slice(0, 7)} ${DIM}\xB7${RESET} PR #${prNumber}
116
+ ${DIM}(if this PR was decided more than a few hours ago, its comment is frozen and won't re-render)${RESET}`;
117
+ if (!opts.quiet) process.stdout.write(output + "\n");
118
+ return { data, output, exitCode: 0 };
119
+ }
120
+
121
+ export { EmitCommitCreditError, runEmitCommitCredit };