@outerlayer/cli 0.2.0 → 0.4.1

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 +132 -0
  2. package/README.md +229 -60
  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-7MFH6HNC.js +94 -0
  7. package/dist/chunk-3TFPDVTI.js +94 -0
  8. package/dist/{chunk-4TNZMO7V.js → chunk-4C4THABW.js} +5566 -754
  9. package/dist/{chunk-R5KBGVII.js → chunk-5QRP3MVS.js} +30 -14
  10. package/dist/chunk-5Y3QQRUH.js +300 -0
  11. package/dist/chunk-774HEQL6.js +178 -0
  12. package/dist/{work-pr-cmd-AZQPWH4H.js → chunk-7WERLFVR.js} +9 -26
  13. package/dist/chunk-ABIWDUMS.js +104 -0
  14. package/dist/chunk-AUFG23AA.js +534 -0
  15. package/dist/chunk-B2G7JHHB.js +42 -0
  16. package/dist/chunk-BCJNHMZT.js +34 -0
  17. package/dist/chunk-BCLJSQCV.js +56 -0
  18. package/dist/chunk-BFESLKPP.js +61 -0
  19. package/dist/chunk-BJ3KTMDH.js +63 -0
  20. package/dist/chunk-BKO6JEZI.js +47 -0
  21. package/dist/chunk-BOLTI6LR.js +37 -0
  22. package/dist/chunk-CBPPJSAR.js +89 -0
  23. package/dist/chunk-DM3VFDS3.js +1461 -0
  24. package/dist/{chunk-65WXAANR.js → chunk-DVDEBNQJ.js} +17 -2
  25. package/dist/{chunk-TFUIDMOB.js → chunk-EABW6AJQ.js} +12 -1
  26. package/dist/{chunk-KFYJV2ZG.js → chunk-EMY4I27X.js} +1 -1
  27. package/dist/{chunk-NTTPJV35.js → chunk-F47JBIAW.js} +3 -1
  28. package/dist/chunk-F6GFZXZ3.js +60 -0
  29. package/dist/{chunk-YOOSBOKS.js → chunk-F7CU5ABH.js} +1 -1
  30. package/dist/{emit-cmd-TWSYYEZI.js → chunk-H5NCNVDA.js} +68 -17
  31. package/dist/chunk-HZRNMGLF.js +2233 -0
  32. package/dist/chunk-LF6MLJCY.js +38 -0
  33. package/dist/{chunk-U32VSRLO.js → chunk-LT5TZHYH.js} +95 -634
  34. package/dist/chunk-LYCDNMOH.js +83 -0
  35. package/dist/chunk-M5POMKKW.js +1051 -0
  36. package/dist/{mcp-install-cmd-FDQH6SEN.js → chunk-MQ3IPHIZ.js} +35 -18
  37. package/dist/{chunk-D77LS3UI.js → chunk-N5FOE5PS.js} +33 -4
  38. package/dist/chunk-N6LURUEF.js +64 -0
  39. package/dist/chunk-OGFGZQA3.js +23 -0
  40. package/dist/chunk-QDQEUVUF.js +9 -0
  41. package/dist/chunk-QOZKPLVJ.js +226 -0
  42. package/dist/chunk-QY22NZUW.js +1764 -0
  43. package/dist/chunk-RA3O54FC.js +30 -0
  44. package/dist/chunk-RFUPN6KX.js +71 -0
  45. package/dist/chunk-SY4GK4SP.js +55 -0
  46. package/dist/chunk-TBT347UY.js +41 -0
  47. package/dist/chunk-TWNWNS2Q.js +1084 -0
  48. package/dist/{chunk-NXLLURA4.js → chunk-UFFXNXLQ.js} +115 -71
  49. package/dist/chunk-USO2DKBD.js +421 -0
  50. package/dist/chunk-W4SQCOZU.js +206 -0
  51. package/dist/{chunk-Q6CL5THG.js → chunk-W5FVUZXV.js} +208 -61
  52. package/dist/chunk-WFJ65NUD.js +383 -0
  53. package/dist/{chunk-A3WLZX2F.js → chunk-WIOZAJ2W.js} +15 -2
  54. package/dist/chunk-WO2BXCTQ.js +83 -0
  55. package/dist/{chunk-I3ETLSNE.js → chunk-XCCLFVXM.js} +120 -11
  56. package/dist/{context-materialize-6WKT3RBQ.js → chunk-XDDW4FRS.js} +134 -26
  57. package/dist/chunk-Y7KJLXYN.js +232 -0
  58. package/dist/chunk-YFHIIH4B.js +102 -0
  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-W62AFRSW.js} +616 -878
  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-ZTJONP37.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-44CQVWGT.js +6 -0
  70. package/dist/docker-NEGDLU6D.js +7 -0
  71. package/dist/doctor-53F2PYY7.js +43 -0
  72. package/dist/doctor-ZLGZYMDK.js +426 -0
  73. package/dist/{emit-artifact-cmd-UQVT3OKW.js → emit-artifact-cmd-AKZP7TMF.js} +87 -23
  74. package/dist/emit-cmd-DCERAU27.js +9 -0
  75. package/dist/emit-criteria-cmd-7MXQVCKD.js +166 -0
  76. package/dist/{emit-finding-cmd-FDOU2OPD.js → emit-finding-cmd-N2FTGCVC.js} +34 -31
  77. package/dist/{emit-result-cmd-H2T4C2K3.js → emit-result-cmd-4CPDBMN2.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-55Y4LHWF.js +36 -0
  87. package/dist/init-XXEKWZQP.js +221 -0
  88. package/dist/init-cmd-WLST2F7G.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-JFIBP4B7.js +43 -0
  95. package/dist/machine-ZBPT2J3R.js +35 -0
  96. package/dist/mcp-install-cmd-RLK4SO3N.js +9 -0
  97. package/dist/{mcp-serve-cmd-57EZZOTL.js → mcp-serve-cmd-N6UD2KBX.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-UJXWZRN5.js} +30 -13
  105. package/dist/{statusline-fast-3C5OXHDD.js → statusline-fast-SVTMBMD7.js} +3 -2
  106. package/dist/sync-cmd-4G4A7EVN.js +28 -0
  107. package/dist/version-BWM6VLDI.js +6 -0
  108. package/dist/{watch-3DTPJETH.js → watch-7SKJKGAA.js} +19 -8
  109. package/dist/{work-claim-cmd-5AUNCLS2.js → work-claim-cmd-5F7VZ42N.js} +34 -21
  110. package/dist/work-cmd-QAUZ7MTD.js +17 -0
  111. package/dist/work-comment-cmd-BNCSBTLH.js +106 -0
  112. package/dist/{work-launch-LT663PB3.js → work-launch-YI4CJDAP.js} +1 -1
  113. package/dist/work-open-pr-cmd-VFYZHIIW.js +156 -0
  114. package/dist/work-pr-cmd-PV32ZLSI.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 +108 -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
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import { C } from './chunk-YRYMNBGQ.js';
4
- import { readCloudConfig } from './chunk-TFUIDMOB.js';
5
- import './chunk-VNVZDWO3.js';
6
- import './chunk-Z5GEFMRW.js';
7
- import { existsSync, readFileSync, writeFileSync } from 'fs';
8
- import { join } from 'path';
4
+ import { readCloudConfig } from './chunk-EABW6AJQ.js';
5
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
6
+ import { join, dirname } from 'path';
9
7
 
10
8
  createRequire(import.meta.url);
11
9
  var McpInstallError = class extends Error {
@@ -14,7 +12,7 @@ function defaultMcpUrl(home) {
14
12
  const origin = readCloudConfig(home).url;
15
13
  if (!origin) {
16
14
  throw new McpInstallError(
17
- "no gateway URL: pass --url <your gateway>, or run `outerlayer login --url <your gateway>` first."
15
+ "no gateway URL: pass --url <your gateway>, or run `outerlayer login` first."
18
16
  );
19
17
  }
20
18
  return `${origin.replace(/\/+$/, "")}/v1/mcp`;
@@ -33,6 +31,35 @@ function buildStdioServerEntry(command, url, appId) {
33
31
  if (appId) args.push("--app-id", appId);
34
32
  return { type: "stdio", command, args };
35
33
  }
34
+ function readMcpDoc(absPath, relPath) {
35
+ if (!existsSync(absPath)) return {};
36
+ let doc;
37
+ try {
38
+ doc = JSON.parse(readFileSync(absPath, "utf8"));
39
+ } catch {
40
+ throw new McpInstallError(`${relPath} exists and is not valid JSON \u2014 fix or remove it, then run the command again`);
41
+ }
42
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
43
+ throw new McpInstallError(`${relPath} exists and its top level is not a JSON object \u2014 refusing to overwrite`);
44
+ }
45
+ return doc;
46
+ }
47
+ function addStdioMcpEntryIfAbsent(repoRoot, name = "outerlayer") {
48
+ const relPath = existsSync(join(repoRoot, ".outerlayer", "mcp.json")) ? ".outerlayer/mcp.json" : ".mcp.json";
49
+ const absPath = join(repoRoot, relPath);
50
+ const doc = readMcpDoc(absPath, relPath);
51
+ const servers = doc.mcpServers;
52
+ if (servers !== void 0 && (servers === null || typeof servers !== "object" || Array.isArray(servers))) {
53
+ throw new McpInstallError(`${relPath} has an "mcpServers" value that is not an object \u2014 refusing to overwrite`);
54
+ }
55
+ const existing = servers ?? {};
56
+ if (name in existing) return { path: relPath, added: false };
57
+ const entry = buildStdioServerEntry(DEFAULT_MCP_COMMAND, void 0, void 0);
58
+ mkdirSync(dirname(absPath), { recursive: true });
59
+ writeFileSync(absPath, `${JSON.stringify({ ...doc, mcpServers: { ...existing, [name]: entry } }, null, 2)}
60
+ `);
61
+ return { path: relPath, added: true };
62
+ }
36
63
  function runMcpInstall(opts = {}) {
37
64
  const cwd = opts.cwd ?? process.cwd();
38
65
  if (!existsSync(cwd)) throw new McpInstallError(`no such directory: ${cwd}`);
@@ -41,17 +68,7 @@ function runMcpInstall(opts = {}) {
41
68
  const url = transport === "http" ? opts.url ?? defaultMcpUrl(opts.home) : opts.url ?? null;
42
69
  const relPath = ".mcp.json";
43
70
  const absPath = join(cwd, relPath);
44
- let doc = {};
45
- if (existsSync(absPath)) {
46
- try {
47
- doc = JSON.parse(readFileSync(absPath, "utf8"));
48
- } catch {
49
- throw new McpInstallError(`${relPath} exists and is not valid JSON \u2014 fix or remove it before running "mcp install"`);
50
- }
51
- if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
52
- throw new McpInstallError(`${relPath} exists and its top level is not a JSON object \u2014 refusing to overwrite`);
53
- }
54
- }
71
+ const doc = readMcpDoc(absPath, relPath);
55
72
  const existingServers = doc.mcpServers && typeof doc.mcpServers === "object" && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {};
56
73
  const entry = transport === "http" ? buildServerEntry(url, opts.appId) : buildStdioServerEntry(opts.command ?? DEFAULT_MCP_COMMAND, url ?? void 0, opts.appId);
57
74
  const changed = JSON.stringify(existingServers[name] ?? null) !== JSON.stringify(entry);
@@ -72,4 +89,4 @@ function handleMcpInstallError(err) {
72
89
  process.exitCode = 1;
73
90
  }
74
91
 
75
- export { DEFAULT_MCP_COMMAND, McpInstallError, buildServerEntry, buildStdioServerEntry, defaultMcpUrl, handleMcpInstallError, runMcpInstall };
92
+ export { DEFAULT_MCP_COMMAND, McpInstallError, addStdioMcpEntryIfAbsent, buildServerEntry, buildStdioServerEntry, defaultMcpUrl, handleMcpInstallError, runMcpInstall };
@@ -1,11 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { isSameProcess } from './chunk-OZ7C3XUE.js';
4
- import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-JJP7YLMN.js';
5
- import { existsSync, readFileSync, rmSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
4
+ import { readFileSync, existsSync, rmSync, mkdirSync, writeFileSync } from 'fs';
6
5
  import { dirname } from 'path';
6
+ import { execFileSync } from 'child_process';
7
7
 
8
8
  createRequire(import.meta.url);
9
+ function startTimeOf(pid) {
10
+ if (process.platform === "linux") return startTimeOfLinux(pid);
11
+ return startTimeOfPs(pid);
12
+ }
13
+ function startTimeOfLinux(pid) {
14
+ try {
15
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
16
+ const afterComm = stat.slice(stat.lastIndexOf(")") + 1).trim();
17
+ const fields = afterComm.split(/\s+/);
18
+ if (fields[0] === "Z" || fields[0] === "X") return null;
19
+ const starttime = fields[19];
20
+ return starttime ?? null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function startTimeOfPs(pid) {
26
+ try {
27
+ const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" }).trim();
28
+ return out.length > 0 ? out : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function isSameProcess(pid, start) {
34
+ return startTimeOf(pid) === start;
35
+ }
36
+
37
+ // src/runner/pidfile.ts
9
38
  function readPidFile(path) {
10
39
  if (!existsSync(path)) return null;
11
40
  try {
@@ -39,4 +68,4 @@ function claimPidFile(path, self, isSameProcessFn = isSameProcess) {
39
68
  writePidFile(path, self);
40
69
  }
41
70
 
42
- export { RunnerAlreadyRunningError, claimPidFile, readPidFile, releasePidFile };
71
+ export { RunnerAlreadyRunningError, claimPidFile, isSameProcess, readPidFile, releasePidFile, startTimeOf };
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { C } from './chunk-YRYMNBGQ.js';
4
+ import { readCloudConfig, removeCloudConfigKeys } from './chunk-EABW6AJQ.js';
5
+ import { homeFromEnv } from './chunk-VNVZDWO3.js';
6
+
7
+ createRequire(import.meta.url);
8
+
9
+ // src/logout-cmd.ts
10
+ var DEFAULT_DASHBOARD = "https://app.outerlayer.ai";
11
+ var REQUEST_TIMEOUT_MS = 15e3;
12
+ function realLogoutDeps() {
13
+ return { fetchImpl: fetch, stdout: process.stdout, stderr: process.stderr };
14
+ }
15
+ async function revokeTokenOnDashboard(fetchImpl, dashboard, token) {
16
+ try {
17
+ const res = await fetchImpl(`${dashboard.replace(/\/+$/, "")}/api/cli/logout`, {
18
+ method: "POST",
19
+ headers: { authorization: `Bearer ${token}` },
20
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
21
+ });
22
+ if (res.status >= 500) return "unreachable";
23
+ return res.status >= 200 && res.status < 300 ? "revoked" : "refused";
24
+ } catch {
25
+ return "unreachable";
26
+ }
27
+ }
28
+ async function runLogoutCommand(deps) {
29
+ const home = deps.home ?? homeFromEnv();
30
+ const config = readCloudConfig(home);
31
+ const token = config.accountToken;
32
+ if (!token) {
33
+ deps.stdout.write("Not logged in.\n");
34
+ return 0;
35
+ }
36
+ const outcome = await revokeTokenOnDashboard(deps.fetchImpl, config.dashboard ?? DEFAULT_DASHBOARD, token);
37
+ try {
38
+ removeCloudConfigKeys(["accountToken", "dashboard"], home);
39
+ } catch (err) {
40
+ deps.stderr.write(
41
+ `${C.red}\u2717${C.reset} Could not remove the login from the config file: ${err instanceof Error ? err.message : String(err)}. ` + (outcome === "revoked" ? "The token is revoked on the dashboard, but a copy is still on this machine; delete it from the config file.\n" : "The token may still be valid; revoke it on the CLI logins list at /profile/security.\n")
42
+ );
43
+ return 1;
44
+ }
45
+ if (outcome === "refused") {
46
+ deps.stderr.write(
47
+ `${C.red}\u2717${C.reset} Removed the login from this machine, but the dashboard did not confirm the revoke. The token may stay valid until you revoke it on the CLI logins list at /profile/security.
48
+ `
49
+ );
50
+ return 1;
51
+ }
52
+ if (outcome === "unreachable") {
53
+ deps.stderr.write(
54
+ `${C.red}\u2717${C.reset} Removed the login from this machine, but could not reach the dashboard to revoke it. The token may stay valid until you revoke it on the CLI logins list at /profile/security.
55
+ `
56
+ );
57
+ return 1;
58
+ }
59
+ deps.stdout.write(`${C.green}\u2713${C.reset} Logged out. The token is revoked and removed from this machine.
60
+ `);
61
+ return 0;
62
+ }
63
+
64
+ export { realLogoutDeps, revokeTokenOnDashboard, runLogoutCommand };
@@ -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,9 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+
4
+ createRequire(import.meta.url);
5
+
6
+ // src/runner/protocol.ts
7
+ var RUNNER_PROTOCOL = 12;
8
+
9
+ export { RUNNER_PROTOCOL };
@@ -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 };