@outerlayer/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +226 -59
  3. package/dist/agent-setup-IFH3FB5X.js +197 -0
  4. package/dist/build-TZUUTRGH.js +9 -0
  5. package/dist/build-info.json +1 -1
  6. package/dist/check-WQP2CZDW.js +94 -0
  7. package/dist/chunk-2E54Z3P5.js +232 -0
  8. package/dist/chunk-3TT7RQYB.js +55 -0
  9. package/dist/{chunk-U32VSRLO.js → chunk-3YPKXE5L.js} +90 -632
  10. package/dist/{chunk-Q6CL5THG.js → chunk-5DLBAXY5.js} +208 -61
  11. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  12. package/dist/chunk-5Y3QQRUH.js +300 -0
  13. package/dist/chunk-6Y63SXFO.js +383 -0
  14. package/dist/chunk-774HEQL6.js +178 -0
  15. package/dist/chunk-7A6RVXPZ.js +2233 -0
  16. package/dist/chunk-7YIOOFVJ.js +71 -0
  17. package/dist/chunk-ABIWDUMS.js +104 -0
  18. package/dist/chunk-AUFG23AA.js +534 -0
  19. package/dist/chunk-B2G7JHHB.js +42 -0
  20. package/dist/chunk-BCJNHMZT.js +34 -0
  21. package/dist/chunk-BCLJSQCV.js +56 -0
  22. package/dist/chunk-BFESLKPP.js +61 -0
  23. package/dist/chunk-BJ3KTMDH.js +63 -0
  24. package/dist/chunk-BKO6JEZI.js +47 -0
  25. package/dist/chunk-BOLTI6LR.js +37 -0
  26. package/dist/chunk-CBPPJSAR.js +89 -0
  27. package/dist/chunk-CBZB6XY6.js +102 -0
  28. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  29. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-DVYHCMB2.js} +9 -26
  30. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  31. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  32. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  33. package/dist/chunk-F6GFZXZ3.js +60 -0
  34. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  35. package/dist/chunk-FAJSS2WN.js +1357 -0
  36. package/dist/chunk-FS2VYGZT.js +1764 -0
  37. package/dist/chunk-FV2CCGXK.js +9 -0
  38. package/dist/{chunk-4TNZMO7V.js → chunk-G7FSV7JX.js} +3452 -751
  39. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  40. package/dist/chunk-HE2D4EY5.js +83 -0
  41. package/dist/chunk-L2DBRKRA.js +94 -0
  42. package/dist/chunk-LF6MLJCY.js +38 -0
  43. package/dist/chunk-M5POMKKW.js +1051 -0
  44. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  45. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  46. package/dist/chunk-N6LURUEF.js +64 -0
  47. package/dist/chunk-NYC54VBD.js +1084 -0
  48. package/dist/chunk-OGFGZQA3.js +23 -0
  49. package/dist/chunk-QOZKPLVJ.js +226 -0
  50. package/dist/chunk-RA3O54FC.js +30 -0
  51. package/dist/chunk-TBT347UY.js +41 -0
  52. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  53. package/dist/chunk-USO2DKBD.js +421 -0
  54. package/dist/chunk-W4SQCOZU.js +206 -0
  55. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  56. package/dist/chunk-WO2BXCTQ.js +83 -0
  57. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  58. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  59. package/dist/chunk-YYYXRJUW.js +101 -0
  60. package/dist/chunk-ZM2IMMYN.js +76 -0
  61. package/dist/chunk-ZMYPLFG3.js +71 -0
  62. package/dist/chunk-ZNA27WEV.js +470 -0
  63. package/dist/{cli-G7DILYJY.js → cli-NHUXABYH.js} +590 -876
  64. package/dist/{paths-D2VGWWFI.js → cli-build-K56DK4DS.js} +1 -1
  65. package/dist/config-XVJYZ4IQ.js +9 -0
  66. package/dist/connect-cmd-OOG4TPU7.js +16 -0
  67. package/dist/context-adopt-UMA4O5NY.js +105 -0
  68. package/dist/context-materialize-G2FUFC72.js +19 -0
  69. package/dist/dist-RW4UPPC2.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-UFVL4PZY.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-KTYW2TN2.js} +24 -20
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-VAUUSAYD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-UR4ID45Z.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-FDTUKGKE.js} +34 -62
  78. package/dist/exec-client-4XGXV3XN.js +8 -0
  79. package/dist/guest-init-GENRHNP7.js +8 -0
  80. package/dist/hook-fast-EYENPK6F.js +9 -0
  81. package/dist/{hook-wrap-fast-KXYNX3AD.js → hook-wrap-fast-NPLHJXFK.js} +2 -2
  82. package/dist/host-key-KDYZ5ECC.js +7 -0
  83. package/dist/import-capture-cmd-AVZ4VIVJ.js +68 -0
  84. package/dist/{import-ruler-cmd-7LH2QOMG.js → import-ruler-cmd-GUHL6IY5.js} +1 -1
  85. package/dist/index.js +4 -4
  86. package/dist/init-DXBCOAJK.js +36 -0
  87. package/dist/init-YEXK35CF.js +221 -0
  88. package/dist/init-cmd-NHR7PRSQ.js +135 -0
  89. package/dist/install-cmd-7WXOIFDO.js +55 -0
  90. package/dist/lima-XN65D7GN.js +55 -0
  91. package/dist/login-browser-7XGSV7MA.js +10 -0
  92. package/dist/{config-POF7DEQW.js → logout-cmd-QAUFDJ6J.js} +3 -1
  93. package/dist/{logs-TRNPQM42.js → logs-7RYUH5A6.js} +1 -1
  94. package/dist/loop-VZOLL4WQ.js +43 -0
  95. package/dist/machine-WSG52J75.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-JPTP5FMS.js} +20 -9
  98. package/dist/paths-OYKMVYJP.js +6 -0
  99. package/dist/{pidfile-PTW76F56.js → pidfile-UZRH774M.js} +2 -3
  100. package/dist/real-deps-SU24ZA2K.js +21 -0
  101. package/dist/relay-L76HDX72.js +46 -0
  102. package/dist/settings-J2652U5N.js +7 -0
  103. package/dist/starter-pack-LIZYMKYQ.js +10 -0
  104. package/dist/{status-I27IST4L.js → status-2UZKKNB7.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-VPPYCNNM.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-5C4ZBBGO.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-LSO2B2EX.js} +33 -20
  110. package/dist/work-cmd-UOQO2AD4.js +17 -0
  111. package/dist/work-comment-cmd-WF3EOLAE.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-4SSQZMYG.js +156 -0
  114. package/dist/work-pr-cmd-6AN6RRQJ.js +16 -0
  115. package/package.json +13 -3
  116. package/skill-pack/maintained/amend/SKILL.md +104 -0
  117. package/skill-pack/maintained/emitting-evidence/SKILL.md +99 -0
  118. package/skill-pack/maintained/emitting-evidence/references/agents-snippet.md +20 -0
  119. package/skill-pack/maintained/outerlayer/SKILL.md +55 -0
  120. package/skill-pack/maintained/reporting-findings/SKILL.md +148 -0
  121. package/skill-pack/template/build/SKILL.md +193 -0
  122. package/skill-pack/template/build/references/agent-briefs.md +243 -0
  123. package/skill-pack/template/build/references/criteria-judge.md +91 -0
  124. package/skill-pack/template/build/references/evidence.md +42 -0
  125. package/skill-pack/template/build/references/release.md +74 -0
  126. package/skill-pack/template/build/references/review-briefs.md +275 -0
  127. package/skill-pack/template/build/references/review-loop.md +158 -0
  128. package/skill-pack/template/build/scripts/record-criteria.mjs +235 -0
  129. package/skill-pack/template/spec/SKILL.md +84 -0
  130. package/skill-pack/template/writing-specs/SKILL.md +134 -0
  131. package/dist/chunk-DCNOXRMV.js +0 -589
  132. package/dist/chunk-JJP7YLMN.js +0 -25
  133. package/dist/chunk-OZ7C3XUE.js +0 -34
  134. package/dist/chunk-WQ6VGRGZ.js +0 -150
  135. package/dist/hook-fast-J5LCDHUJ.js +0 -8
  136. package/dist/import-capture-cmd-EUMBGIV3.js +0 -176
  137. package/dist/init-PTBITAUO.js +0 -103
  138. package/dist/login-cmd-IRX6LZT7.js +0 -62
  139. package/dist/loop-ASZRX3CZ.js +0 -648
  140. package/dist/sync-cmd-BBAUZ5JD.js +0 -17
  141. package/dist/work-cmd-2P4BVX47.js +0 -18
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ createRequire(import.meta.url);
8
+ function currentCliBuild(entryPath = fileURLToPath(import.meta.url)) {
9
+ const distDir = dirname(entryPath);
10
+ const read = (path) => {
11
+ try {
12
+ return JSON.parse(readFileSync(path, "utf8"));
13
+ } catch {
14
+ return {};
15
+ }
16
+ };
17
+ const id = read(join(distDir, "build-info.json")).id;
18
+ if (typeof id === "string" && id !== "") return { distDir, buildId: id };
19
+ const version = read(join(dirname(distDir), "package.json")).version;
20
+ return { distDir, buildId: `${typeof version === "string" ? version : "unknown"}+unstamped` };
21
+ }
22
+
23
+ export { currentCliBuild };
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { existsSync, readdirSync, readFileSync, lstatSync, realpathSync, writeFileSync } from 'fs';
4
+ import { join, resolve, relative, sep, basename, dirname } from 'path';
5
+
6
+ createRequire(import.meta.url);
7
+
8
+ // src/runner/image/properties.ts
9
+ var LIFECYCLE = ["onCreateCommand", "updateContentCommand", "postCreateCommand", "postStartCommand"];
10
+ var HONOURED = ["image", "build", "features", "containerEnv", "remoteUser", ...LIFECYCLE];
11
+ var IGNORED_BUILD_KEYS = ["options"];
12
+ var HOST_REFERENCE = /\$\{(?:localEnv|env)[:}]/;
13
+ var DROP = /* @__PURE__ */ Symbol("drop");
14
+ function isHonoured(property) {
15
+ return HONOURED.includes(property);
16
+ }
17
+ function isObject(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function featureName(id) {
21
+ const slash = id.lastIndexOf("/");
22
+ const cut = id.slice(slash + 1).search(/[:@]/);
23
+ return cut === -1 ? id : id.slice(0, slash + 1 + cut);
24
+ }
25
+ var DOCKER_FEATURE = "ghcr.io/devcontainers/features/docker-in-docker";
26
+ function asksForDocker(recipe, metadata) {
27
+ const isDocker = (id) => featureName(id) === DOCKER_FEATURE;
28
+ if (isObject(recipe.features) && Object.keys(recipe.features).some(isDocker)) return true;
29
+ return metadata.some((entry) => typeof entry.id === "string" && isDocker(entry.id));
30
+ }
31
+ function sameValue(a, b) {
32
+ return JSON.stringify(a) === JSON.stringify(b);
33
+ }
34
+ function isFilesOwnEntry(entry, recipe) {
35
+ if (typeof entry.id === "string") return false;
36
+ const keys = Object.keys(entry);
37
+ return keys.length > 0 && keys.every((key) => key in recipe && sameValue(entry[key], recipe[key]));
38
+ }
39
+ function withoutHostReferences(value, path, dropped) {
40
+ if (typeof value === "string") {
41
+ if (!HOST_REFERENCE.test(value)) return value;
42
+ dropped.push(path);
43
+ return DROP;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ if (!HOST_REFERENCE.test(JSON.stringify(value))) return value;
47
+ dropped.push(path);
48
+ return DROP;
49
+ }
50
+ if (isObject(value)) {
51
+ const kept = {};
52
+ for (const [key, item] of Object.entries(value)) {
53
+ if (HOST_REFERENCE.test(key)) {
54
+ dropped.push(`${path}.${key}`);
55
+ continue;
56
+ }
57
+ const clean = withoutHostReferences(item, `${path}.${key}`, dropped);
58
+ if (clean !== DROP) kept[key] = clean;
59
+ }
60
+ return kept;
61
+ }
62
+ return value;
63
+ }
64
+ function honouredWithoutHostReferences(entry) {
65
+ const kept = {};
66
+ const dropped = [];
67
+ for (const property of HONOURED) {
68
+ if (!(property in entry)) continue;
69
+ const value = entry[property];
70
+ const clean = withoutHostReferences(
71
+ property === "build" && isObject(value) ? withoutIgnoredBuildKeys(value) : value,
72
+ property,
73
+ dropped
74
+ );
75
+ if (clean !== DROP) kept[property] = clean;
76
+ }
77
+ return { kept, dropped };
78
+ }
79
+ function withoutIgnoredBuildKeys(build) {
80
+ return Object.fromEntries(Object.entries(build).filter(([key]) => !IGNORED_BUILD_KEYS.includes(key)));
81
+ }
82
+ function ignoredIn(entry, source) {
83
+ const found = [];
84
+ for (const property of Object.keys(entry)) {
85
+ if (property === "id" || property.startsWith("$") || isHonoured(property)) continue;
86
+ found.push({ property, source });
87
+ }
88
+ if (isObject(entry.build)) {
89
+ for (const key of IGNORED_BUILD_KEYS) {
90
+ if (key in entry.build) found.push({ property: `build.${key}`, source });
91
+ }
92
+ }
93
+ for (const property of honouredWithoutHostReferences(entry).dropped) found.push({ property, source });
94
+ return found;
95
+ }
96
+ function emptyApply() {
97
+ return { containerEnv: {}, lifecycle: { onCreateCommand: [], updateContentCommand: [], postCreateCommand: [], postStartCommand: [] } };
98
+ }
99
+ function applyEntry(apply, raw, source) {
100
+ const entry = honouredWithoutHostReferences(raw).kept;
101
+ if (typeof entry.remoteUser === "string") apply.remoteUser = entry.remoteUser;
102
+ if (isObject(entry.containerEnv)) {
103
+ for (const [key, value] of Object.entries(entry.containerEnv)) {
104
+ if (typeof value === "string") apply.containerEnv[key] = value;
105
+ }
106
+ }
107
+ for (const name of LIFECYCLE) {
108
+ if (entry[name] !== void 0) apply.lifecycle[name].push({ source, command: entry[name] });
109
+ }
110
+ }
111
+ function classifyProperties(recipe, metadata) {
112
+ const ignored = ignoredIn(recipe, "file");
113
+ const apply = emptyApply();
114
+ let fileApplied = false;
115
+ const sanitized = sanitizeRecipe(recipe);
116
+ for (const entry of metadata) {
117
+ if (isFilesOwnEntry(entry, sanitized)) {
118
+ applyEntry(apply, entry, "file");
119
+ fileApplied = true;
120
+ continue;
121
+ }
122
+ const source = typeof entry.id === "string" ? `feature ${featureName(entry.id)}` : "image label";
123
+ ignored.push(...ignoredIn(entry, source));
124
+ applyEntry(apply, entry, source);
125
+ }
126
+ if (!fileApplied) applyEntry(apply, recipe, "file");
127
+ return { docker: asksForDocker(recipe, metadata), honoured: HONOURED.filter((property) => property in recipe), ignored, apply };
128
+ }
129
+ function sanitizeRecipe(recipe) {
130
+ return honouredWithoutHostReferences(recipe).kept;
131
+ }
132
+ var RecipeError = class extends Error {
133
+ constructor(code, message, logPath) {
134
+ super(message);
135
+ this.code = code;
136
+ this.logPath = logPath;
137
+ }
138
+ };
139
+ var DIR_RECIPE = ".devcontainer/devcontainer.json";
140
+ var ROOT_RECIPE = ".devcontainer.json";
141
+ function locateRecipe(checkoutDir) {
142
+ if (existsSync(join(checkoutDir, DIR_RECIPE))) return { kind: "file", path: DIR_RECIPE };
143
+ if (existsSync(join(checkoutDir, ROOT_RECIPE))) return { kind: "file", path: ROOT_RECIPE };
144
+ const named = namedConfigurations(checkoutDir);
145
+ if (named.length > 0) {
146
+ throw new RecipeError(
147
+ "recipe_ambiguous",
148
+ `the repository has only named devcontainer configurations (${named.join(", ")}); add ${DIR_RECIPE} so the runner knows which to build`
149
+ );
150
+ }
151
+ return { kind: "default" };
152
+ }
153
+ function namedConfigurations(checkoutDir) {
154
+ const dir = join(checkoutDir, ".devcontainer");
155
+ if (!existsSync(dir)) return [];
156
+ return readdirSync(dir).sort().map((entry) => `.devcontainer/${entry}/devcontainer.json`).filter((path) => existsSync(join(checkoutDir, path)));
157
+ }
158
+ var COMMENTS = /("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?(?:\*\/|$)/g;
159
+ var TRAILING_COMMAS = /("(?:[^"\\]|\\.)*")|,(\s*[}\]])/g;
160
+ function parseJsonc(text) {
161
+ const withoutComments = text.replace(COMMENTS, (_match, str) => str ?? "");
162
+ return JSON.parse(withoutComments.replace(TRAILING_COMMAS, (_match, str, close) => str ?? close ?? ""));
163
+ }
164
+ function readRecipe(checkoutDir, location) {
165
+ if (location.kind === "default") return {};
166
+ const parsed = parseJsonc(readFileSync(join(checkoutDir, location.path), "utf8"));
167
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
168
+ throw new Error(`${location.path} is not a JSON object`);
169
+ }
170
+ return parsed;
171
+ }
172
+ function within(dir, path) {
173
+ const rel = relative(dir, path);
174
+ return rel !== ".." && !rel.startsWith(`..${sep}`);
175
+ }
176
+ function realish(path) {
177
+ let existing = path;
178
+ let rest = "";
179
+ while (!existsSync(existing)) {
180
+ rest = join(basename(existing), rest);
181
+ existing = dirname(existing);
182
+ }
183
+ return join(realpathSync(existing), rest);
184
+ }
185
+ function assertBuildable(checkoutDir, location, recipe) {
186
+ if (location.kind === "default") return;
187
+ const devcontainerDir = join(checkoutDir, ".devcontainer");
188
+ if (existsSync(devcontainerDir) && lstatSync(devcontainerDir).isSymbolicLink()) {
189
+ throw new RecipeError(
190
+ "recipe_context_outside",
191
+ ".devcontainer is a symbolic link, so the build could read files outside the repository; commit a real directory"
192
+ );
193
+ }
194
+ if (recipe.dockerComposeFile !== void 0) {
195
+ throw new RecipeError(
196
+ "recipe_needs_docker",
197
+ "the recipe uses dockerComposeFile; its services need Docker, which a container build does not have"
198
+ );
199
+ }
200
+ const build = recipe.build;
201
+ if (typeof build !== "object" || build === null) return;
202
+ const { dockerfile, context } = build;
203
+ const base = resolve(checkoutDir, location.path, "..");
204
+ const allowed = join(realpathSync(checkoutDir), ".devcontainer");
205
+ const contextPath = resolve(base, typeof context === "string" ? context : ".");
206
+ const paths = [contextPath];
207
+ if (typeof dockerfile === "string") paths.push(resolve(base, dockerfile));
208
+ for (const path of paths) {
209
+ if (!within(devcontainerDir, path) || !within(allowed, realish(path))) {
210
+ throw new RecipeError(
211
+ "recipe_context_outside",
212
+ `the recipe's build reads ${relative(checkoutDir, path) || "."}, outside .devcontainer/; move the build files into .devcontainer/`
213
+ );
214
+ }
215
+ }
216
+ }
217
+ function writeSanitizedRecipe(checkoutDir, location, recipe) {
218
+ if (location.kind === "default") throw new Error("the default image has no recipe to sanitize");
219
+ const path = location.path === ROOT_RECIPE ? join(checkoutDir, ".outerlayer-recipe.json") : join(checkoutDir, ".devcontainer", SANITIZED_NAME);
220
+ writeFileSync(path, `${JSON.stringify(sanitizeRecipe(recipe), null, 2)}
221
+ `);
222
+ return path;
223
+ }
224
+ var SANITIZED_NAME = "outerlayer-recipe.json";
225
+
226
+ export { RecipeError, SANITIZED_NAME, assertBuildable, classifyProperties, locateRecipe, readRecipe, writeSanitizedRecipe };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ createRequire(import.meta.url);
8
+ var FALLBACK_VERSION = "0.1.0";
9
+ function readJson(path) {
10
+ if (!existsSync(path)) return null;
11
+ try {
12
+ return JSON.parse(readFileSync(path, "utf8"));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function resolveVersion(entryPath = fileURLToPath(import.meta.url)) {
18
+ const distDir = dirname(entryPath);
19
+ const pkg = readJson(join(dirname(distDir), "package.json"));
20
+ const version = typeof pkg?.version === "string" ? pkg.version : FALLBACK_VERSION;
21
+ const buildInfo = readJson(join(distDir, "build-info.json"));
22
+ const buildId = typeof buildInfo?.id === "string" ? buildInfo.id : null;
23
+ return buildId ? `${version} (build ${buildId})` : version;
24
+ }
25
+ function resolvePackageVersion(entryPath = fileURLToPath(import.meta.url)) {
26
+ const pkg = readJson(join(dirname(dirname(entryPath)), "package.json"));
27
+ return typeof pkg?.version === "string" ? pkg.version : FALLBACK_VERSION;
28
+ }
29
+
30
+ export { resolvePackageVersion, resolveVersion };
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+
4
+ createRequire(import.meta.url);
5
+
6
+ // src/repo-filter.ts
7
+ function toMatcher(pattern) {
8
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
9
+ return new RegExp(`^${escaped}$`, "i");
10
+ }
11
+ var RepoFilter = class {
12
+ constructor(config) {
13
+ /** Sessions dropped, keyed by repo (or "(no repo)") — for the sync summary. */
14
+ this.skipped = /* @__PURE__ */ new Map();
15
+ this.include = (config?.include ?? []).map(toMatcher);
16
+ this.exclude = (config?.exclude ?? []).map(toMatcher);
17
+ }
18
+ get active() {
19
+ return this.include.length > 0 || this.exclude.length > 0;
20
+ }
21
+ /** True when the session should sync. Records skips for reporting. */
22
+ allows(repo) {
23
+ const label = repo || "(no repo)";
24
+ if (repo && this.exclude.some((m) => m.test(repo))) {
25
+ this.skipped.set(label, (this.skipped.get(label) ?? 0) + 1);
26
+ return false;
27
+ }
28
+ if (this.include.length > 0 && !(repo && this.include.some((m) => m.test(repo)))) {
29
+ this.skipped.set(label, (this.skipped.get(label) ?? 0) + 1);
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ get skippedTotal() {
35
+ let n = 0;
36
+ for (const c of this.skipped.values()) n += c;
37
+ return n;
38
+ }
39
+ };
40
+
41
+ export { RepoFilter };
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { deadHooksState, huskyPrepareScript, preparePackageManagerCommand } from './chunk-I3ETLSNE.js';
3
+ import { deadHooksState, huskyPrepareScript, preparePackageManagerCommand } from './chunk-XCCLFVXM.js';
4
+ import { readRepoConnection, findGitDirs } from './chunk-BJ3KTMDH.js';
4
5
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
5
6
  import { readFileSync, mkdirSync, writeFileSync, renameSync, existsSync, statSync, appendFileSync, openSync, opendirSync, unlinkSync } from 'fs';
6
7
  import { spawn, execFileSync } from 'child_process';
7
8
  import { createHash } from 'crypto';
8
- import { join, dirname, isAbsolute, resolve } from 'path';
9
+ import { join, dirname, resolve } from 'path';
9
10
 
10
11
  createRequire(import.meta.url);
11
12
  var SYNC_STALE_AFTER_MS = 6 * 60 * 60 * 1e3;
@@ -354,11 +355,17 @@ function appendFd(path) {
354
355
  return "ignore";
355
356
  }
356
357
  }
357
- var spawnDetached = (command, args, stderrPath) => {
358
- const child = spawn(command, args, { detached: true, stdio: ["ignore", "ignore", appendFd(stderrPath)] });
358
+ var spawnDetached = (command, args, stderrPath, cwd) => {
359
+ const child = spawn(command, args, {
360
+ detached: true,
361
+ stdio: ["ignore", "ignore", appendFd(stderrPath)],
362
+ ...cwd !== void 0 ? { cwd } : {}
363
+ });
364
+ child.on("error", () => {
365
+ });
359
366
  child.unref();
360
367
  };
361
- function maybeAutoSync(event, home = homeFromEnv(), spawnImpl = spawnDetached, now = Date.now()) {
368
+ function maybeAutoSync(event, home = homeFromEnv(), spawnImpl = spawnDetached, now = Date.now(), cwd = process.cwd()) {
362
369
  try {
363
370
  if (event !== "SessionEnd" && event !== "Stop") return;
364
371
  const marker = join(spoolDir(home), "last-auto-sync");
@@ -370,55 +377,114 @@ function maybeAutoSync(event, home = homeFromEnv(), spawnImpl = spawnDetached, n
370
377
  if (!existsSync(configPath)) return;
371
378
  const config = JSON.parse(readFileSync(configPath, "utf8"));
372
379
  if (config.autoSync === false) return;
373
- if (!config.url || !config.apiKey || !config.appId) return;
380
+ if (!config.url) return;
381
+ if (config.apiKey) {
382
+ if (!config.appId) return;
383
+ } else if (!config.accountToken || readRepoConnection(cwd) === void 0) return;
374
384
  writeFileSync(marker, String(now));
375
- spawnImpl(process.execPath, [process.argv[1], "sync", "--quiet"]);
385
+ spawnImpl(process.execPath, [process.argv[1], "sync", "--quiet"], void 0, cwd);
376
386
  } catch {
377
387
  }
378
388
  }
379
- var WORK_ADD_RETRIES = 4;
389
+ var WORK_LINK_RETRIES = 4;
380
390
  function hookErrorsContext(home, writtenBytes, seenBytes) {
381
391
  if (writtenBytes === 0) return null;
382
392
  if (writtenBytes === seenBytes) return null;
383
393
  const path = join(spoolDir(home), "hook-errors.log");
384
- return `OuterLayer recorded a hook failure since your last session started \u2014 a refused OUTERLAYER_WORK value, or a work-item session link that could not reach the Floor. The log is ${path}.`;
394
+ return `OuterLayer recorded a hook failure since your last session started \u2014 a refused OUTERLAYER_WORK value, or a work-item session link that could not reach Work. The log is ${path}.`;
385
395
  }
386
- async function handleWorkLaunch(sessionId, home, env, spawnImpl = spawnDetached) {
396
+ async function handleWorkLaunch(sessionId, home, env, spawnImpl = spawnDetached, cwd = process.cwd()) {
387
397
  if (!sessionId || env.OUTERLAYER_WORK === void 0) return void 0;
388
- const { parseWorkItemEnv, appendLaunchRecord } = await import('./work-launch-LT663PB3.js');
398
+ const { parseWorkItemEnv, appendLaunchRecord } = await import('./work-launch-YI4CJDAP.js');
389
399
  const parsed = parseWorkItemEnv(env.OUTERLAYER_WORK);
390
400
  if ("reason" in parsed) {
391
401
  logHookError(home, new Error(`OUTERLAYER_WORK ignored: ${parsed.reason}`));
392
402
  return `OuterLayer ignored this session's OUTERLAYER_WORK value: ${sanitizeForContext(parsed.reason)}. The session is not attached to a work item and will not upload. Hook failures are logged to ${join(spoolDir(home), "hook-errors.log")}.`;
393
403
  }
394
404
  appendLaunchRecord({ sessionId, item: parsed.number }, home);
405
+ const args = [
406
+ process.argv[1],
407
+ "work",
408
+ "link-session",
409
+ String(parsed.number),
410
+ "--session-id",
411
+ sessionId,
412
+ // The ordinary failure is a gateway that is briefly unreachable — a
413
+ // laptop that has not joined the network yet, a restart. Nothing else
414
+ // ever retries this link: the launch record is already written, so
415
+ // the session uploads whether or not Work ever hears about the
416
+ // link. Retrying costs nothing here (this process is detached; the
417
+ // hook has already returned) and cannot double-link, because the
418
+ // link is keyed by the session id passed above.
419
+ "--retry",
420
+ String(WORK_LINK_RETRIES)
421
+ ];
422
+ if (env.OUTERLAYER_JOB_DIR !== void 0) args.push("--no-lease");
395
423
  spawnImpl(
396
424
  process.execPath,
397
- [
398
- process.argv[1],
399
- "work",
400
- "link-session",
401
- String(parsed.number),
402
- "--session-id",
403
- sessionId,
404
- // The ordinary failure is a gateway that is briefly unreachable — a
405
- // laptop that has not joined the network yet, a restart. Nothing else
406
- // ever retries this link: the launch record is already written, so
407
- // the session uploads whether or not the Floor ever hears about the
408
- // link. Retrying costs nothing here (this process is detached; the
409
- // hook has already returned) and cannot double-link, because the
410
- // link is keyed by the session id passed above.
411
- "--retry",
412
- String(WORK_ADD_RETRIES)
413
- ],
425
+ args,
414
426
  // The link runs after this hook has exited, so its own failure
415
427
  // (offline, revoked key, unknown item number) has nowhere else to be
416
- // seen: without this the session uploads later with no Floor link and
428
+ // seen: without this the session uploads later with no Work link and
417
429
  // no trace of why.
418
- join(spoolDir(home), "hook-errors.log")
430
+ join(spoolDir(home), "hook-errors.log"),
431
+ cwd
419
432
  );
420
433
  return void 0;
421
434
  }
435
+ async function missingRenewalHookContext(home, cwd) {
436
+ try {
437
+ const { settingsPath, readSettings, isOurHookCommand } = await import('./settings-J2652U5N.js');
438
+ const userSettings = readSettings(settingsPath("user", { home }));
439
+ const projectSettings = readSettings(settingsPath("project", { cwd }));
440
+ if (userSettings === null && projectSettings === null) return void 0;
441
+ const registeredIn = (settings) => (settings.hooks?.PostToolUse ?? []).some((block) => (block.hooks ?? []).some((h) => isOurHookCommand(h)));
442
+ const registered = userSettings !== null && registeredIn(userSettings) || projectSettings !== null && registeredIn(projectSettings);
443
+ if (registered) return void 0;
444
+ return "This install has no PostToolUse hook, so this session's work-item lease will never be renewed while it works and may be lost to another worker mid-session. Run `outerlayer init` to register it.";
445
+ } catch {
446
+ return void 0;
447
+ }
448
+ }
449
+ var WORK_LEASE_RENEW_DEBOUNCE_MS = 5 * 60 * 1e3;
450
+ async function maybeRenewWorkLease(event, sessionId, home = homeFromEnv(), env = process.env, spawnImpl = spawnDetached, now = Date.now(), cwd = process.cwd()) {
451
+ try {
452
+ if (event !== "PostToolUse") return;
453
+ if (!sessionId || env.OUTERLAYER_WORK === void 0 || env.OUTERLAYER_JOB_DIR !== void 0) return;
454
+ const marker = join(spoolDir(home), `work-lease-renew-${sessionId}`);
455
+ try {
456
+ if (now - statSync(marker).mtimeMs < WORK_LEASE_RENEW_DEBOUNCE_MS) return;
457
+ } catch {
458
+ }
459
+ const { parseWorkItemEnv } = await import('./work-launch-YI4CJDAP.js');
460
+ const parsed = parseWorkItemEnv(env.OUTERLAYER_WORK);
461
+ if ("reason" in parsed) return;
462
+ writeFileSync(marker, String(now));
463
+ spawnImpl(
464
+ process.execPath,
465
+ [process.argv[1], "work", "link-session", String(parsed.number), "--session-id", sessionId],
466
+ join(spoolDir(home), "hook-errors.log"),
467
+ cwd
468
+ );
469
+ } catch {
470
+ }
471
+ }
472
+ async function handleWorkSessionEnd(event, sessionId, home = homeFromEnv(), env = process.env, spawnImpl = spawnDetached, cwd = process.cwd()) {
473
+ try {
474
+ if (event !== "SessionEnd") return;
475
+ if (!sessionId || env.OUTERLAYER_WORK === void 0) return;
476
+ const { parseWorkItemEnv } = await import('./work-launch-YI4CJDAP.js');
477
+ const parsed = parseWorkItemEnv(env.OUTERLAYER_WORK);
478
+ if ("reason" in parsed) return;
479
+ spawnImpl(
480
+ process.execPath,
481
+ [process.argv[1], "work", "link-session", String(parsed.number), "--session-id", sessionId, "--ended"],
482
+ join(spoolDir(home), "hook-errors.log"),
483
+ cwd
484
+ );
485
+ } catch {
486
+ }
487
+ }
422
488
  function syncHealthContext(home) {
423
489
  if (!existsSync(join(home, ".outerlayer", "config.json"))) return null;
424
490
  const health = assessSyncHealth(readSyncStatus(home));
@@ -479,7 +545,7 @@ function clearHealFailureMarker(home, repoRoot) {
479
545
  }
480
546
  }
481
547
  function deadHooksAutoHealContext(cwd, home) {
482
- if (findGitRoot(cwd) === void 0) return null;
548
+ if (findGitDirs(cwd) === void 0) return null;
483
549
  const state = deadHooksState(cwd);
484
550
  if (!state.dead || state.repoRoot === void 0 || state.hooksDir === void 0) return null;
485
551
  const hooksDirLabel = sanitizeForContext(state.hooksDir);
@@ -510,41 +576,9 @@ var PIN_FILENAME = ".outerlayer-pin.json";
510
576
  function sourceCachePath(gitCommonDir) {
511
577
  return join(gitCommonDir, "outerlayer", "source.json");
512
578
  }
513
- function findGitRoot(cwd) {
514
- let dir = cwd;
515
- for (; ; ) {
516
- const gitPath = join(dir, ".git");
517
- let stat;
518
- try {
519
- stat = statSync(gitPath);
520
- } catch {
521
- stat = void 0;
522
- }
523
- if (stat?.isDirectory()) return { repoRoot: dir, gitCommonDir: gitPath };
524
- if (stat?.isFile()) {
525
- try {
526
- const content = readFileSync(gitPath, "utf8").trim();
527
- const match = /^gitdir:\s*(.+)$/.exec(content);
528
- if (match) {
529
- const gitdir = isAbsolute(match[1]) ? match[1] : join(dir, match[1]);
530
- try {
531
- const commondir = readFileSync(join(gitdir, "commondir"), "utf8").trim();
532
- return { repoRoot: dir, gitCommonDir: isAbsolute(commondir) ? commondir : join(gitdir, commondir) };
533
- } catch {
534
- return { repoRoot: dir, gitCommonDir: gitdir };
535
- }
536
- }
537
- } catch {
538
- }
539
- }
540
- const parent = dirname(dir);
541
- if (parent === dir) return void 0;
542
- dir = parent;
543
- }
544
- }
545
579
  function contextPossiblyGoverned(cwd, home = homeFromEnv()) {
546
580
  try {
547
- const found = findGitRoot(cwd);
581
+ const found = findGitDirs(cwd);
548
582
  if (found === void 0) return false;
549
583
  const { repoRoot, gitCommonDir } = found;
550
584
  if (existsSync(join(repoRoot, PIN_FILENAME))) return true;
@@ -566,8 +600,6 @@ function contextPossiblyGoverned(cwd, home = homeFromEnv()) {
566
600
  }
567
601
  async function runHookFast(event, home = homeFromEnv(), read = readStdin, env = process.env, spawnImpl = spawnDetached) {
568
602
  try {
569
- const dir = spoolDir(home);
570
- mkdirSync(dir, { recursive: true });
571
603
  const stdin = read();
572
604
  let payload = {};
573
605
  try {
@@ -576,6 +608,11 @@ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env =
576
608
  payload = {};
577
609
  }
578
610
  const resolvedEvent = String(event ?? payload.hook_event_name ?? "unknown");
611
+ if (resolvedEvent === "PostToolUse" && (env.OUTERLAYER_WORK === void 0 || env.OUTERLAYER_JOB_DIR !== void 0)) {
612
+ return;
613
+ }
614
+ const dir = spoolDir(home);
615
+ mkdirSync(dir, { recursive: true });
579
616
  const record = {
580
617
  t: (/* @__PURE__ */ new Date()).toISOString(),
581
618
  event: resolvedEvent,
@@ -585,19 +622,23 @@ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env =
585
622
  };
586
623
  appendFileSync(join(dir, "events.jsonl"), JSON.stringify(record) + "\n");
587
624
  markHookFired(resolvedEvent, home);
588
- maybeAutoSync(resolvedEvent, home, spawnImpl);
589
625
  const sessionCwd = typeof payload.cwd === "string" ? payload.cwd : process.cwd();
626
+ maybeAutoSync(resolvedEvent, home, spawnImpl, Date.now(), sessionCwd);
627
+ const hookSessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
628
+ await maybeRenewWorkLease(resolvedEvent, hookSessionId, home, env, spawnImpl, Date.now(), sessionCwd);
629
+ await handleWorkSessionEnd(resolvedEvent, hookSessionId, home, env, spawnImpl, sessionCwd);
590
630
  let materializeMessage;
591
631
  let deadHooksMessage;
592
632
  let workLaunchMessage;
593
633
  let hookErrorsMessage;
634
+ let renewalHookMessage;
594
635
  if (resolvedEvent === "SessionStart") {
595
636
  const hookErrorsBytes = fileSizeBytes(join(dir, "hook-errors.log"));
596
637
  const hookErrorsSeen = hookErrorsSeenBytes(home);
597
638
  recordHookErrorsSeen(home, hookErrorsBytes);
598
639
  if (contextPossiblyGoverned(sessionCwd, home)) {
599
640
  try {
600
- const { materializeContext: materialize } = await import('./context-materialize-6WKT3RBQ.js');
641
+ const { materializeContext: materialize } = await import('./context-materialize-G2FUFC72.js');
601
642
  const sessionId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
602
643
  const result = await materialize({ cwd: sessionCwd, sessionId, home });
603
644
  materializeMessage = result.message;
@@ -612,20 +653,23 @@ async function runHookFast(event, home = homeFromEnv(), read = readStdin, env =
612
653
  }
613
654
  try {
614
655
  const sessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
615
- workLaunchMessage = await handleWorkLaunch(sessionId, home, env, spawnImpl);
656
+ workLaunchMessage = await handleWorkLaunch(sessionId, home, env, spawnImpl, sessionCwd);
616
657
  } catch (err) {
617
658
  logHookError(home, err);
618
659
  }
619
660
  if (workLaunchMessage === void 0) {
620
661
  hookErrorsMessage = hookErrorsContext(home, hookErrorsBytes, hookErrorsSeen) ?? void 0;
621
662
  }
663
+ if (env.OUTERLAYER_WORK !== void 0 && env.OUTERLAYER_JOB_DIR === void 0 && workLaunchMessage === void 0) {
664
+ renewalHookMessage = await missingRenewalHookContext(home, sessionCwd);
665
+ }
622
666
  }
623
667
  writeSessionStartContext(
624
668
  resolvedEvent,
625
669
  home,
626
670
  sessionCwd,
627
671
  (out) => process.stdout.write(out),
628
- [materializeMessage, deadHooksMessage, workLaunchMessage, hookErrorsMessage].filter(
672
+ [materializeMessage, deadHooksMessage, workLaunchMessage, hookErrorsMessage, renewalHookMessage].filter(
629
673
  (s) => s !== void 0
630
674
  )
631
675
  );
@@ -647,4 +691,4 @@ function logHookError(home, err) {
647
691
  }
648
692
  }
649
693
 
650
- export { AUTO_SYNC_DEBOUNCE_MS, SYNC_STALE_AFTER_MS, WORK_ADD_RETRIES, assessSyncHealth, checkInstalledCliStaleness, contextPossiblyGoverned, formatAgo, handleWorkLaunch, hookFiredMarkerPath, installedCliDistDir, maybeAutoSync, readSyncStatus, refreshCommand, runHookFast, sanitizeForContext, spoolDir, workspaceRootOf, writeSyncStatus };
694
+ export { AUTO_SYNC_DEBOUNCE_MS, SYNC_STALE_AFTER_MS, WORK_LEASE_RENEW_DEBOUNCE_MS, WORK_LINK_RETRIES, assessSyncHealth, checkInstalledCliStaleness, contextPossiblyGoverned, formatAgo, handleWorkLaunch, handleWorkSessionEnd, hookFiredMarkerPath, installedCliDistDir, maybeAutoSync, maybeRenewWorkLease, readSyncStatus, refreshCommand, runHookFast, sanitizeForContext, spoolDir, workspaceRootOf, writeSyncStatus };