@sanlabs/sanbox-cli 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.js CHANGED
@@ -11,7 +11,6 @@ export const readLocalConfig = (cwd = process.cwd()) => {
11
11
  const record = parsed;
12
12
  return {
13
13
  api_url: typeof record.api_url === "string" ? record.api_url : undefined,
14
- org: typeof record.org === "string" ? record.org : undefined,
15
14
  default_template: typeof record.default_template === "string" ? record.default_template : undefined
16
15
  };
17
16
  }
@@ -24,19 +23,13 @@ export const readLocalConfig = (cwd = process.cwd()) => {
24
23
  export const readConfig = (flags = {}) => {
25
24
  const localConfig = readLocalConfig();
26
25
  const apiUrl = String(flags["api-url"] || process.env.SANBOX_API_URL || localConfig.api_url || defaultApiUrl).replace(/\/+$/, "");
27
- const org = String(flags.org || process.env.SANBOX_ORG || localConfig.org || "").trim();
28
26
  const apiKey = process.env.SANBOX_API_KEY || "";
29
- if (!org) {
30
- throw new CliError("org_required", "Organization context is required.", {
31
- nextActions: [commandAction(["sanbox", "context", "--json"], "Select an organization and retry with SANBOX_ORG set.", { SANBOX_ORG: "<org-slug>" })]
32
- });
33
- }
34
27
  if (!apiKey) {
35
28
  throw new CliError("api_key_required", "SANBOX_API_KEY is required.", {
36
29
  nextActions: [commandAction(["sanbox", "auth", "check", "--json"], "Set a Sanbox control-plane API key and check access.", { SANBOX_API_KEY: "<sanbox-api-key>" })]
37
30
  });
38
31
  }
39
- return { apiUrl, org, apiKey };
32
+ return { apiUrl, apiKey };
40
33
  };
41
34
  export function readTemplateSelection(flags = {}, options = {}) {
42
35
  const flagValue = flags.template;
@@ -52,7 +45,7 @@ export function readTemplateSelection(flags = {}, options = {}) {
52
45
  return null;
53
46
  throw new CliError("template_required", "A template must be selected explicitly.", {
54
47
  nextActions: [
55
- commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the selected organization."),
48
+ commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization."),
56
49
  commandAction(["sanbox", "run", "<task>", "--template", "<template-id>"], "Run with an explicit template."),
57
50
  commandAction(["sanbox", "context", "--json"], "Select a template for this shell and inspect the resolved context.", { SANBOX_TEMPLATE: "<template-id>" })
58
51
  ]
package/dist/inputs.js ADDED
@@ -0,0 +1,150 @@
1
+ import crypto from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import fg from "fast-glob";
6
+ import ignore from "ignore";
7
+ import yazl from "yazl";
8
+ const maximumInputBytes = 64 * 1024 * 1024;
9
+ const maximumInputFiles = 10_000;
10
+ const defaultIgnorePatterns = [
11
+ ".git/**",
12
+ "**/.git/**",
13
+ "node_modules/**",
14
+ "**/node_modules/**",
15
+ "dist/**",
16
+ "**/dist/**",
17
+ "build/**",
18
+ "**/build/**",
19
+ "coverage/**",
20
+ "**/coverage/**",
21
+ ".next/**",
22
+ "**/.next/**",
23
+ ".turbo/**",
24
+ "**/.turbo/**",
25
+ ".env",
26
+ ".env.*",
27
+ "**/.env",
28
+ "**/.env.*",
29
+ "**/*secret*",
30
+ "**/*Secret*",
31
+ "**/*token*",
32
+ "**/*Token*",
33
+ "**/*credential*",
34
+ "**/*Credential*",
35
+ "**/*.pem",
36
+ "**/*.key",
37
+ "**/id_rsa",
38
+ "**/id_ed25519"
39
+ ];
40
+ const normalizePattern = async (cwd, pattern) => {
41
+ const slashPattern = pattern.replaceAll(path.sep, "/");
42
+ const normalizedPath = path.posix.normalize(slashPattern);
43
+ if (path.isAbsolute(pattern) || normalizedPath === ".." || normalizedPath.startsWith("../")) {
44
+ throw new Error(`Input path must be relative to the current directory: ${pattern}`);
45
+ }
46
+ if (pattern === ".")
47
+ return "**/*";
48
+ const normalized = normalizedPath.replace(/\/+$/, "");
49
+ if (normalized.includes("*"))
50
+ return normalized;
51
+ try {
52
+ const stat = await fs.stat(path.resolve(cwd, normalized));
53
+ if (stat.isDirectory())
54
+ return `${normalized}/**`;
55
+ }
56
+ catch (error) {
57
+ if (error.code !== "ENOENT")
58
+ throw error;
59
+ }
60
+ return normalized;
61
+ };
62
+ const readSanboxIgnore = async (cwd) => {
63
+ try {
64
+ const raw = await fs.readFile(path.join(cwd, ".sanboxignore"), "utf8");
65
+ return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
66
+ }
67
+ catch (error) {
68
+ if (error.code === "ENOENT")
69
+ return [];
70
+ throw error;
71
+ }
72
+ };
73
+ const selectedInputs = async (cwd, inputs) => {
74
+ const patterns = await Promise.all(inputs.map((input) => normalizePattern(cwd, input)));
75
+ const ignored = ignore().add([...defaultIgnorePatterns, ...(await readSanboxIgnore(cwd))]);
76
+ const candidates = await fg(patterns, {
77
+ cwd,
78
+ dot: true,
79
+ onlyFiles: true,
80
+ followSymbolicLinks: false,
81
+ unique: true
82
+ });
83
+ const normalizedCandidates = candidates
84
+ .map((item) => item.replaceAll(path.sep, "/"))
85
+ .filter((item) => !item.startsWith("../") && !path.isAbsolute(item))
86
+ .filter((item) => !ignored.ignores(item))
87
+ .sort();
88
+ const files = [];
89
+ for (const item of normalizedCandidates) {
90
+ const stat = await fs.lstat(path.join(cwd, item));
91
+ if (stat.isFile())
92
+ files.push(item);
93
+ }
94
+ return { patterns, files };
95
+ };
96
+ const zipToBuffer = async (zip) => new Promise((resolve, reject) => {
97
+ const chunks = [];
98
+ zip.outputStream.on("data", (chunk) => chunks.push(chunk));
99
+ zip.outputStream.on("error", reject);
100
+ zip.outputStream.on("end", () => resolve(Buffer.concat(chunks)));
101
+ zip.end();
102
+ });
103
+ export const sha256 = (buffer) => crypto.createHash("sha256").update(buffer).digest("hex");
104
+ export const previewInputs = async (input) => {
105
+ const { patterns, files } = await selectedInputs(input.cwd, input.inputs);
106
+ const previewFiles = [];
107
+ let totalBytes = 0;
108
+ for (const relative of files) {
109
+ const stat = await fs.stat(path.join(input.cwd, relative));
110
+ if (!stat.isFile())
111
+ continue;
112
+ totalBytes += stat.size;
113
+ previewFiles.push({ path: relative, size: stat.size });
114
+ }
115
+ return { patterns, files: previewFiles, totalBytes };
116
+ };
117
+ export const buildInputBundle = async (input) => {
118
+ const { files } = await selectedInputs(input.cwd, input.inputs);
119
+ if (files.length === 0)
120
+ throw new Error("No files matched the supplied --input values.");
121
+ if (files.length > maximumInputFiles)
122
+ throw new Error(`Inputs exceed the ${maximumInputFiles}-file limit.`);
123
+ const zip = new yazl.ZipFile();
124
+ const metadata = [];
125
+ let totalBytes = 0;
126
+ for (const relative of files) {
127
+ const absolute = path.join(input.cwd, relative);
128
+ const file = await fs.open(absolute, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
129
+ try {
130
+ const stat = await file.stat();
131
+ if (!stat.isFile())
132
+ continue;
133
+ if (totalBytes + stat.size > maximumInputBytes)
134
+ throw new Error("Inputs exceed the 64 MiB uncompressed limit.");
135
+ const content = await file.readFile();
136
+ totalBytes += content.byteLength;
137
+ if (totalBytes > maximumInputBytes)
138
+ throw new Error("Inputs exceed the 64 MiB uncompressed limit.");
139
+ metadata.push({ path: relative, size: content.byteLength, sha256: sha256(content) });
140
+ zip.addBuffer(content, `input/${relative}`, { mode: stat.mode });
141
+ }
142
+ finally {
143
+ await file.close();
144
+ }
145
+ }
146
+ const buffer = await zipToBuffer(zip);
147
+ if (buffer.byteLength > maximumInputBytes)
148
+ throw new Error("Inputs exceed the 64 MiB upload limit.");
149
+ return { buffer, sha256: sha256(buffer), files: metadata };
150
+ };
package/dist/output.js CHANGED
@@ -77,7 +77,9 @@ export const summarizeRun = (payload) => {
77
77
  const selection = [
78
78
  templateId ? `template=${templateId}` : "",
79
79
  run.provider_id ? `provider=${run.provider_id}` : "",
80
- run.model_id ? `model=${run.model_id}` : ""
80
+ run.model_id ? `model=${run.model_id}` : "",
81
+ run.sandbox_state ? `sandbox=${run.sandbox_state}` : "",
82
+ run.snapshot_generation ? `snapshot=${run.snapshot_generation}` : ""
81
83
  ].filter(Boolean).join(" ");
82
84
  return `${run.id} ${run.status}${selection ? ` ${selection}` : ""}${run.exit_code === null ? "" : ` exit=${run.exit_code}`}${run.error ? ` error=${run.error}` : ""}`;
83
85
  };
package/dist/runs.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { buildTaskDossier, readDossierFile } from "./dossier.js";
5
- const terminalStatuses = new Set(["completed", "failed", "canceled", "expired"]);
6
- const inlineLimitBytes = 8 * 1024 * 1024;
4
+ import { buildInputBundle } from "./inputs.js";
5
+ const terminalStatuses = new Set(["completed", "failed", "canceled"]);
7
6
  export const isTerminalRun = (run) => terminalStatuses.has(run.status);
8
7
  export const waitForRun = async (client, runId, options = {}) => {
9
8
  const pollIntervalMs = options.pollIntervalMs ?? 2000;
@@ -18,32 +17,21 @@ export const waitForRun = async (client, runId, options = {}) => {
18
17
  return payload;
19
18
  };
20
19
  export const createRun = async (client, options) => {
21
- const dossier = options.dossierPath
22
- ? await readDossierFile(path.resolve(options.cwd, options.dossierPath))
23
- : await buildTaskDossier({
24
- cwd: options.cwd,
25
- task: options.task || "",
26
- include: options.include,
27
- cliVersion: options.cliVersion
20
+ let inputCollectionId;
21
+ if (options.inputs.length > 0) {
22
+ const bundle = await buildInputBundle({ cwd: options.cwd, inputs: options.inputs });
23
+ const uploaded = await client.uploadInputCollection({
24
+ buffer: bundle.buffer,
25
+ sha256: bundle.sha256,
26
+ fileCount: bundle.files.length
28
27
  });
29
- if (dossier.buffer.byteLength > inlineLimitBytes) {
30
- throw new Error(`Dossier is ${dossier.buffer.byteLength} bytes; inline dossier limit is ${inlineLimitBytes} bytes.`);
28
+ inputCollectionId = uploaded.input_collection.id;
31
29
  }
32
- const manifest = {
33
- dossier: {
34
- inline_base64: dossier.buffer.toString("base64"),
35
- sha256: dossier.sha256
36
- },
37
- sanbox_cli: {
38
- files: dossier.files,
39
- source: options.dossierPath ? "dossier_file" : "generated_task"
40
- }
41
- };
42
30
  return client.createRun({
43
31
  external_run_id: options.externalRunId,
44
32
  workload_id: options.templateId,
45
- manifest,
46
- retention_ttl_seconds: options.retentionTtlSeconds
33
+ instruction: options.instruction,
34
+ ...(inputCollectionId ? { input_collection_id: inputCollectionId } : {})
47
35
  });
48
36
  };
49
37
  export const readTasks = async (tasksPath) => {
@@ -64,7 +52,7 @@ export const readTasks = async (tasksPath) => {
64
52
  return {
65
53
  task,
66
54
  external_run_id: record.external_run_id ? String(record.external_run_id) : undefined,
67
- include: Array.isArray(record.include) ? record.include.map(String) : undefined
55
+ input: Array.isArray(record.input) ? record.input.map(String) : undefined
68
56
  };
69
57
  });
70
58
  };
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = "0.0.3";
1
+ export const version = "0.0.5";
package/dist/watch.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { SanboxApiError } from "./api.js";
2
- const terminalStatuses = new Set(["completed", "failed", "canceled", "expired"]);
3
- const terminalEventKinds = new Set(["run.completed", "run.failed", "run.canceled", "run.expired"]);
2
+ const terminalStatuses = new Set(["completed", "failed", "canceled"]);
3
+ const terminalEventKinds = new Set(["run.completed", "run.failed", "run.canceled"]);
4
4
  const retryableNetworkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH"]);
5
5
  export class WatchInterruptedError extends Error {
6
6
  constructor() {
@@ -38,6 +38,65 @@ const assertActive = (signal, deadline, now, runId) => {
38
38
  throw new Error(`Timed out watching run ${runId}.`);
39
39
  };
40
40
  export const isTerminalStatus = (status) => terminalStatuses.has(status);
41
+ export const watchEventsUntil = async (client, runId, options) => {
42
+ const pageSize = Math.max(1, Math.min(500, Math.floor(options.pageSize ?? 200)));
43
+ const pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? 2000));
44
+ const timeoutSeconds = Math.max(1, options.timeoutSeconds ?? 1800);
45
+ const maxReconnectDelayMs = Math.max(250, options.maxReconnectDelayMs ?? 10_000);
46
+ const sleep = options.sleep ?? defaultSleep;
47
+ const now = options.now ?? Date.now;
48
+ const random = options.random ?? Math.random;
49
+ const deadline = now() + timeoutSeconds * 1000;
50
+ let cursor = Math.max(0, Math.floor(options.afterEventId ?? 0));
51
+ const request = async (operation) => {
52
+ let attempt = 0;
53
+ for (;;) {
54
+ assertActive(options.signal, deadline, now, runId);
55
+ try {
56
+ return await operation();
57
+ }
58
+ catch (error) {
59
+ if (options.signal?.aborted)
60
+ throw new WatchInterruptedError();
61
+ if (error instanceof WatchInterruptedError || !isRetryable(error))
62
+ throw error;
63
+ attempt += 1;
64
+ const exponential = Math.min(maxReconnectDelayMs, 250 * (2 ** Math.min(attempt - 1, 8)));
65
+ const remainingMs = deadline - now();
66
+ if (remainingMs <= 0)
67
+ throw new Error(`Timed out watching run ${runId}.`);
68
+ const delayMs = Math.min(remainingMs, Math.max(1, Math.floor(exponential * (0.8 + random() * 0.4))));
69
+ options.onRetry?.({ error, attempt, delayMs });
70
+ await sleep(delayMs, options.signal);
71
+ }
72
+ }
73
+ };
74
+ for (;;) {
75
+ assertActive(options.signal, deadline, now, runId);
76
+ let delivered = 0;
77
+ let fetchAnotherPage = true;
78
+ while (fetchAnotherPage) {
79
+ const page = await request(() => client.listEvents(runId, cursor, pageSize, options.signal));
80
+ const previousCursor = cursor;
81
+ const ordered = [...page.events].sort((left, right) => left.id - right.id);
82
+ const seen = new Set();
83
+ for (const event of ordered) {
84
+ if (!Number.isSafeInteger(event.id) || event.id <= cursor || seen.has(event.id))
85
+ continue;
86
+ seen.add(event.id);
87
+ await options.onEvent?.(event);
88
+ cursor = event.id;
89
+ delivered += 1;
90
+ if (options.stopWhen(event))
91
+ return event;
92
+ }
93
+ const reportedMore = page.has_more ?? page.events.length >= pageSize;
94
+ fetchAnotherPage = reportedMore && cursor > previousCursor;
95
+ }
96
+ if (delivered === 0)
97
+ await sleep(pollIntervalMs, options.signal);
98
+ }
99
+ };
41
100
  export const watchRun = async (client, runId, options) => {
42
101
  const pageSize = Math.max(1, Math.min(500, Math.floor(options.pageSize ?? 200)));
43
102
  const pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? 2000));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanlabs/sanbox-cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
package/dist/dossier.js DELETED
@@ -1,187 +0,0 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { inflateRawSync } from "node:zlib";
5
- import fg from "fast-glob";
6
- import ignore from "ignore";
7
- import yazl from "yazl";
8
- const defaultIgnorePatterns = [
9
- ".git/**",
10
- "**/.git/**",
11
- "node_modules/**",
12
- "**/node_modules/**",
13
- "dist/**",
14
- "**/dist/**",
15
- "build/**",
16
- "**/build/**",
17
- "coverage/**",
18
- "**/coverage/**",
19
- ".next/**",
20
- "**/.next/**",
21
- ".turbo/**",
22
- "**/.turbo/**",
23
- ".env",
24
- ".env.*",
25
- "**/.env",
26
- "**/.env.*",
27
- "**/*secret*",
28
- "**/*Secret*",
29
- "**/*token*",
30
- "**/*Token*",
31
- "**/*credential*",
32
- "**/*Credential*",
33
- "**/*.pem",
34
- "**/*.key",
35
- "**/id_rsa",
36
- "**/id_ed25519"
37
- ];
38
- const normalizePattern = async (cwd, pattern) => {
39
- if (pattern === ".")
40
- return "**/*";
41
- const normalized = pattern.replaceAll(path.sep, "/").replace(/\/+$/, "");
42
- if (normalized.includes("*"))
43
- return normalized;
44
- try {
45
- const stat = await fs.stat(path.resolve(cwd, normalized));
46
- if (stat.isDirectory())
47
- return `${normalized}/**`;
48
- }
49
- catch (error) {
50
- if (error.code !== "ENOENT")
51
- throw error;
52
- }
53
- return normalized;
54
- };
55
- const normalizeInclude = async (cwd, patterns) => {
56
- if (patterns.length === 0)
57
- return ["**/*"];
58
- return Promise.all(patterns.map((pattern) => normalizePattern(cwd, pattern)));
59
- };
60
- const readSanboxIgnore = async (cwd) => {
61
- try {
62
- const raw = await fs.readFile(path.join(cwd, ".sanboxignore"), "utf8");
63
- return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
64
- }
65
- catch (error) {
66
- if (error.code === "ENOENT")
67
- return [];
68
- throw error;
69
- }
70
- };
71
- const zipToBuffer = async (zip) => new Promise((resolve, reject) => {
72
- const chunks = [];
73
- zip.outputStream.on("data", (chunk) => chunks.push(chunk));
74
- zip.outputStream.on("error", reject);
75
- zip.outputStream.on("end", () => resolve(Buffer.concat(chunks)));
76
- zip.end();
77
- });
78
- export const sha256 = (buffer) => crypto.createHash("sha256").update(buffer).digest("hex");
79
- const selectedFiles = async (cwd, include) => {
80
- const patterns = await normalizeInclude(cwd, include);
81
- const ig = ignore().add([...defaultIgnorePatterns, ...(await readSanboxIgnore(cwd))]);
82
- const candidates = await fg(patterns, {
83
- cwd,
84
- dot: true,
85
- onlyFiles: true,
86
- followSymbolicLinks: false,
87
- unique: true
88
- });
89
- const files = candidates
90
- .map((item) => item.replaceAll(path.sep, "/"))
91
- .filter((item) => !item.startsWith("../") && !path.isAbsolute(item))
92
- .filter((item) => !ig.ignores(item))
93
- .sort();
94
- return { patterns, files };
95
- };
96
- export const previewTaskDossier = async (input) => {
97
- const { patterns, files } = await selectedFiles(input.cwd, input.include);
98
- const previewFiles = [];
99
- let totalBytes = 0;
100
- for (const rel of files) {
101
- const stat = await fs.stat(path.join(input.cwd, rel));
102
- if (!stat.isFile())
103
- continue;
104
- totalBytes += stat.size;
105
- previewFiles.push({ path: rel, size: stat.size });
106
- }
107
- return { patterns, files: previewFiles, totalBytes };
108
- };
109
- export const buildTaskDossier = async (input) => {
110
- const { files } = await selectedFiles(input.cwd, input.include);
111
- const zip = new yazl.ZipFile();
112
- const fileMetadata = [];
113
- const runbook = [
114
- "# Sanbox Task",
115
- "",
116
- "Follow these instructions exactly.",
117
- "",
118
- input.task.trim(),
119
- "",
120
- "Write durable results under `/workspace/output`.",
121
- "Use files under `/workspace/dossier/input/repo` as the provided source context."
122
- ].join("\n");
123
- zip.addBuffer(Buffer.from(runbook, "utf8"), "RUNBOOK.md");
124
- for (const rel of files) {
125
- const abs = path.join(input.cwd, rel);
126
- const stat = await fs.stat(abs);
127
- if (!stat.isFile())
128
- continue;
129
- const content = await fs.readFile(abs);
130
- fileMetadata.push({ path: rel, size: stat.size, sha256: sha256(content) });
131
- zip.addBuffer(content, `input/repo/${rel}`);
132
- }
133
- const manifest = {
134
- source: "sanbox-cli",
135
- cli_version: input.cliVersion,
136
- created_at: new Date().toISOString(),
137
- task: input.task,
138
- files: fileMetadata
139
- };
140
- zip.addBuffer(Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"), "manifest.json");
141
- const buffer = await zipToBuffer(zip);
142
- return { buffer, sha256: sha256(buffer), files: fileMetadata };
143
- };
144
- export const readDossierFile = async (dossierPath) => {
145
- const buffer = await fs.readFile(dossierPath);
146
- return { buffer, sha256: sha256(buffer), files: [] };
147
- };
148
- export const writeTaskDossier = async (input) => {
149
- const dossier = await buildTaskDossier(input);
150
- await fs.mkdir(path.dirname(input.outPath), { recursive: true });
151
- await fs.writeFile(input.outPath, dossier.buffer);
152
- return dossier;
153
- };
154
- const readUInt16 = (buffer, offset) => buffer.readUInt16LE(offset);
155
- const readUInt32 = (buffer, offset) => buffer.readUInt32LE(offset);
156
- export const inspectDossierFile = async (dossierPath) => {
157
- const buffer = await fs.readFile(dossierPath);
158
- const entries = [];
159
- let runbook;
160
- let manifest;
161
- let offset = 0;
162
- while (offset + 30 <= buffer.length && readUInt32(buffer, offset) === 0x04034b50) {
163
- const compressionMethod = readUInt16(buffer, offset + 8);
164
- const compressedSize = readUInt32(buffer, offset + 18);
165
- const uncompressedSize = readUInt32(buffer, offset + 22);
166
- const nameLength = readUInt16(buffer, offset + 26);
167
- const extraLength = readUInt16(buffer, offset + 28);
168
- const nameStart = offset + 30;
169
- const dataStart = nameStart + nameLength + extraLength;
170
- const dataEnd = dataStart + compressedSize;
171
- if (dataEnd > buffer.length)
172
- break;
173
- const entryPath = buffer.subarray(nameStart, nameStart + nameLength).toString("utf8");
174
- entries.push({ path: entryPath, compressedSize, uncompressedSize });
175
- if (entryPath === "RUNBOOK.md" || entryPath === "manifest.json") {
176
- const compressed = buffer.subarray(dataStart, dataEnd);
177
- const content = compressionMethod === 0 ? compressed : inflateRawSync(compressed);
178
- const text = content.toString("utf8");
179
- if (entryPath === "RUNBOOK.md")
180
- runbook = text;
181
- if (entryPath === "manifest.json")
182
- manifest = JSON.parse(text);
183
- }
184
- offset = dataEnd;
185
- }
186
- return { entries, runbook, manifest };
187
- };