@isomorph.ai/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/packages/harbour-cli/src/agent-setup.js +235 -0
- package/dist/packages/harbour-cli/src/app-schema.js +142 -0
- package/dist/packages/harbour-cli/src/auth.js +197 -0
- package/dist/packages/harbour-cli/src/check.js +317 -0
- package/dist/packages/harbour-cli/src/cli.js +321 -0
- package/dist/packages/harbour-cli/src/config.js +97 -0
- package/dist/packages/harbour-cli/src/dev.js +125 -0
- package/dist/packages/harbour-cli/src/forwarder.js +168 -0
- package/dist/packages/harbour-cli/src/integrations.js +385 -0
- package/dist/packages/harbour-cli/src/jobs.js +53 -0
- package/dist/packages/harbour-cli/src/kit-bundle.js +41 -0
- package/dist/packages/harbour-cli/src/kit-bundle.manifest.js +32 -0
- package/dist/packages/harbour-cli/src/kit.js +149 -0
- package/dist/packages/harbour-cli/src/local-runtime.js +526 -0
- package/dist/packages/harbour-cli/src/operations.js +382 -0
- package/dist/packages/harbour-cli/src/output.js +273 -0
- package/dist/packages/harbour-cli/src/productionise.js +508 -0
- package/dist/packages/harbour-cli/src/remote-mcp-client.js +121 -0
- package/dist/packages/harbour-cli/src/retained-checks.js +435 -0
- package/dist/packages/harbour-cli/src/source-inventory.js +239 -0
- package/dist/packages/harbour-cli/src/starter.js +557 -0
- package/dist/packages/harbour-cli/src/upload.js +163 -0
- package/dist/packages/harbour-cli/src/version.js +1 -0
- package/dist/src/analyzer.js +685 -0
- package/dist/src/contracts.js +82 -0
- package/dist/src/digest.js +26 -0
- package/dist/src/secret-paths.js +38 -0
- package/dist/src/source-intake.js +125 -0
- package/package.json +43 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
2
|
+
import { relative, resolve, sep } from "node:path";
|
|
3
|
+
import { deflateRawSync } from "node:zlib";
|
|
4
|
+
import { sha256Bytes } from "../../../src/digest.js";
|
|
5
|
+
import { isProhibitedSecretPath } from "../../../src/secret-paths.js";
|
|
6
|
+
export async function archiveForManifest(root, manifest) {
|
|
7
|
+
const rootAbsolute = resolve(root);
|
|
8
|
+
if ((await lstat(rootAbsolute)).isSymbolicLink())
|
|
9
|
+
throw new Error("Isomorph rejected an unsafe app boundary.");
|
|
10
|
+
const files = [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
for (const entry of manifest.files) {
|
|
13
|
+
if (seen.has(entry.path) || isProhibitedSecretPath(entry.path) || entry.path.includes("\\") || entry.path.startsWith("/"))
|
|
14
|
+
throw new Error("Isomorph rejected an unsafe source path.");
|
|
15
|
+
seen.add(entry.path);
|
|
16
|
+
const absolute = resolve(rootAbsolute, entry.path);
|
|
17
|
+
if (!absolute.startsWith(rootAbsolute + sep))
|
|
18
|
+
throw new Error("Isomorph rejected a path outside the selected app.");
|
|
19
|
+
let current = rootAbsolute;
|
|
20
|
+
for (const component of entry.path.split("/")) {
|
|
21
|
+
current = resolve(current, component);
|
|
22
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
23
|
+
throw new Error("Isomorph rejected an unsafe source path.");
|
|
24
|
+
}
|
|
25
|
+
const info = await lstat(absolute);
|
|
26
|
+
if (!info.isFile())
|
|
27
|
+
throw new Error("The app changed while it was being packaged.");
|
|
28
|
+
const content = new Uint8Array(await readFile(absolute));
|
|
29
|
+
if (content.byteLength !== entry.bytes || await sha256Bytes(content) !== entry.sha256)
|
|
30
|
+
throw new Error(`The app changed while it was being packaged (${entry.path}).`);
|
|
31
|
+
files.push({ path: relative(rootAbsolute, absolute).split(sep).join("/"), content });
|
|
32
|
+
}
|
|
33
|
+
const body = new Uint8Array(deterministicZip(files.sort((left, right) => left.path.localeCompare(right.path))));
|
|
34
|
+
return { body, digest: await sha256Bytes(body) };
|
|
35
|
+
}
|
|
36
|
+
function deterministicZip(files) {
|
|
37
|
+
const local = [];
|
|
38
|
+
const central = [];
|
|
39
|
+
let offset = 0;
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const name = Buffer.from(file.path, "utf8");
|
|
42
|
+
const raw = Buffer.from(file.content);
|
|
43
|
+
const compressed = deflateRawSync(raw, { level: 9 });
|
|
44
|
+
const crc = crc32(raw);
|
|
45
|
+
const header = Buffer.alloc(30);
|
|
46
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
47
|
+
header.writeUInt16LE(20, 4);
|
|
48
|
+
header.writeUInt16LE(0x0800, 6);
|
|
49
|
+
header.writeUInt16LE(8, 8);
|
|
50
|
+
header.writeUInt16LE(0, 10);
|
|
51
|
+
header.writeUInt16LE(0x21, 12);
|
|
52
|
+
header.writeUInt32LE(crc, 14);
|
|
53
|
+
header.writeUInt32LE(compressed.byteLength, 18);
|
|
54
|
+
header.writeUInt32LE(raw.byteLength, 22);
|
|
55
|
+
header.writeUInt16LE(name.byteLength, 26);
|
|
56
|
+
const directory = Buffer.alloc(46);
|
|
57
|
+
directory.writeUInt32LE(0x02014b50, 0);
|
|
58
|
+
directory.writeUInt16LE(0x0314, 4);
|
|
59
|
+
directory.writeUInt16LE(20, 6);
|
|
60
|
+
directory.writeUInt16LE(0x0800, 8);
|
|
61
|
+
directory.writeUInt16LE(8, 10);
|
|
62
|
+
directory.writeUInt16LE(0, 12);
|
|
63
|
+
directory.writeUInt16LE(0x21, 14);
|
|
64
|
+
directory.writeUInt32LE(crc, 16);
|
|
65
|
+
directory.writeUInt32LE(compressed.byteLength, 20);
|
|
66
|
+
directory.writeUInt32LE(raw.byteLength, 24);
|
|
67
|
+
directory.writeUInt16LE(name.byteLength, 28);
|
|
68
|
+
directory.writeUInt32LE((0o100644 << 16) >>> 0, 38);
|
|
69
|
+
directory.writeUInt32LE(offset, 42);
|
|
70
|
+
local.push(header, name, compressed);
|
|
71
|
+
central.push(directory, name);
|
|
72
|
+
offset += header.byteLength + name.byteLength + compressed.byteLength;
|
|
73
|
+
}
|
|
74
|
+
const centralBytes = central.reduce((sum, value) => sum + value.byteLength, 0);
|
|
75
|
+
const end = Buffer.alloc(22);
|
|
76
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
77
|
+
end.writeUInt16LE(files.length, 8);
|
|
78
|
+
end.writeUInt16LE(files.length, 10);
|
|
79
|
+
end.writeUInt32LE(centralBytes, 12);
|
|
80
|
+
end.writeUInt32LE(offset, 16);
|
|
81
|
+
return Buffer.concat([...local, ...central, end]);
|
|
82
|
+
}
|
|
83
|
+
function crc32(bytes) {
|
|
84
|
+
let crc = 0xffffffff;
|
|
85
|
+
for (const byte of bytes) {
|
|
86
|
+
crc ^= byte;
|
|
87
|
+
for (let bit = 0; bit < 8; bit += 1)
|
|
88
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
89
|
+
}
|
|
90
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
91
|
+
}
|
|
92
|
+
export async function putMultipart(upload, body, progress) {
|
|
93
|
+
if (body.byteLength !== upload.compressedBytes)
|
|
94
|
+
throw new Error("The app package changed after the upload session was prepared.");
|
|
95
|
+
const completed = new Map((upload.acknowledgedParts ?? []).map(part => [part.partNumber, part.etag]));
|
|
96
|
+
let acknowledgedBytes = [...completed.keys()].reduce((sum, partNumber) => sum + Math.min(upload.partSizeBytes, body.byteLength - (partNumber - 1) * upload.partSizeBytes), 0);
|
|
97
|
+
progress?.(acknowledgedBytes, body.byteLength);
|
|
98
|
+
await mapConcurrent(upload.parts, 3, async (part) => {
|
|
99
|
+
const start = (part.partNumber - 1) * upload.partSizeBytes;
|
|
100
|
+
const bytes = body.subarray(start, Math.min(body.byteLength, start + upload.partSizeBytes));
|
|
101
|
+
if (part.url.startsWith("local://")) {
|
|
102
|
+
completed.set(part.partNumber, `local-${part.partNumber}`);
|
|
103
|
+
acknowledgedBytes += bytes.byteLength;
|
|
104
|
+
progress?.(Math.min(acknowledgedBytes, body.byteLength), body.byteLength);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let response;
|
|
108
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
109
|
+
try {
|
|
110
|
+
response = await fetch(part.url, { method: "PUT", headers: part.requiredHeaders ?? {}, body: Buffer.from(bytes), redirect: "error" });
|
|
111
|
+
if (response.ok)
|
|
112
|
+
break;
|
|
113
|
+
if (![408, 429, 500, 502, 503, 504].includes(response.status))
|
|
114
|
+
throw new Error(`The app package part upload failed (${response.status}).`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (attempt === 2)
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt));
|
|
121
|
+
}
|
|
122
|
+
if (!response?.ok)
|
|
123
|
+
throw new Error(`The app package part upload failed (${response?.status ?? "network"}).`);
|
|
124
|
+
const etag = response.headers.get("etag");
|
|
125
|
+
if (!etag)
|
|
126
|
+
throw new Error("The app package part upload did not return an ETag.");
|
|
127
|
+
completed.set(part.partNumber, etag);
|
|
128
|
+
acknowledgedBytes += bytes.byteLength;
|
|
129
|
+
progress?.(Math.min(acknowledgedBytes, body.byteLength), body.byteLength);
|
|
130
|
+
});
|
|
131
|
+
return [...completed].sort(([left], [right]) => left - right).map(([partNumber, etag]) => ({ partNumber, etag }));
|
|
132
|
+
}
|
|
133
|
+
async function mapConcurrent(items, concurrency, work) {
|
|
134
|
+
let cursor = 0;
|
|
135
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => { while (cursor < items.length)
|
|
136
|
+
await work(items[cursor++]); }));
|
|
137
|
+
}
|
|
138
|
+
export async function putExact(upload, body) {
|
|
139
|
+
if (upload.url.startsWith("local://"))
|
|
140
|
+
return;
|
|
141
|
+
const headers = { ...(upload.requiredHeaders ?? {}), ...(upload.contentType ? { "content-type": upload.contentType } : {}) };
|
|
142
|
+
let lastError;
|
|
143
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(upload.url, { method: "PUT", headers, body: Buffer.from(body), redirect: "error" });
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
if (![408, 429, 500, 502, 503, 504].includes(response.status))
|
|
148
|
+
throw new Error(`Isomorph source upload failed (${response.status}).`);
|
|
149
|
+
throw new Error(`Isomorph source upload failed (${response.status}).`);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
lastError = error;
|
|
155
|
+
const retryable = error instanceof Error && (!/\(\d{3}\)/.test(error.message) || /\((408|429|500|502|503|504)\)/.test(error.message));
|
|
156
|
+
if (attempt === 0 && retryable)
|
|
157
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
158
|
+
else
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw lastError instanceof Error ? lastError : new Error("Isomorph source upload failed.");
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CLI_VERSION = "0.2.0";
|