insta 0.0.68 → 0.0.70

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.
@@ -0,0 +1,267 @@
1
+ import { posix } from 'node:path';
2
+ import ignore from 'ignore';
3
+ export function compileIgnore(files, flavour) {
4
+ return flavour === 'git' ? compileGit(files) : compileDocker(files);
5
+ }
6
+ // ---- git ----
7
+ const depth = (base) => (base === '' ? 0 : base.split('/').length);
8
+ // One matcher per .gitignore, each asked only about the paths beneath its own directory and
9
+ // spelled relative to it, the way git reads them. Shallower files are consulted first, so a deeper
10
+ // file's last matching rule wins, which is gitignore's precedence, and a file with no matching
11
+ // rule leaves the verdict where the previous file put it.
12
+ //
13
+ // Case-sensitive on purpose. The package defaults to ignorecase, git itself follows
14
+ // core.ignorecase, which differs between a macOS laptop and the Linux box that extracts the
15
+ // archive. One tree has to pack to one identity everywhere, so the rule is the Linux one.
16
+ function compileGit(files) {
17
+ const scoped = [...files]
18
+ .sort((a, b) => depth(a.base) - depth(b.base))
19
+ .map((f) => ({ base: f.base, ig: ignore({ ignorecase: false }).add(f.text) }));
20
+ return {
21
+ excludes(relPath, isDir) {
22
+ let excluded = false;
23
+ for (const { base, ig } of scoped) {
24
+ const sub = base === '' ? relPath : relPath.startsWith(base + '/') ? relPath.slice(base.length + 1) : '';
25
+ if (sub === '')
26
+ continue;
27
+ // A trailing slash is how the package is told the path is a directory, for `logs/` rules.
28
+ const verdict = ig.test(isDir ? sub + '/' : sub);
29
+ if (verdict.ignored)
30
+ excluded = true;
31
+ else if (verdict.unignored)
32
+ excluded = false;
33
+ }
34
+ return excluded;
35
+ },
36
+ // git cannot re-include under an excluded directory, so there is never a reason to descend.
37
+ canPrune: () => true,
38
+ };
39
+ }
40
+ const escapeLiteral = (c) => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ // The rest of a pattern carries no glob syntax, so docker's fast paths apply to it.
42
+ const plain = (rest) => !/[*?[\]\\]/.test(rest);
43
+ // Glob to RegExp the way moby/patternmatcher compiles one (its compile(), read line by line):
44
+ // `*` and `?` stop at a separator; a `**` is an optional run of whole directories, `(.*/)?`, and
45
+ // the slash right after it is eaten with it, wherever it stands, so `a**/b` and `foo**bar` both
46
+ // reach `ab`/`foobar` at the root and `a/x/b`/`foo/x/bar` below, and neither reaches `fooXbar`.
47
+ // Two fast paths are broader than that regex and are matched exactly: a pattern that is `**` plus
48
+ // plain text is a suffix match (`**foo` takes `xfoo`), and one ending in `**` is a prefix match.
49
+ // Reading every interior `**` as `.*` excluded `fooXbar`, a file a local docker build keeps.
50
+ function translate(p) {
51
+ let out = '';
52
+ let i = 0;
53
+ while (i < p.length) {
54
+ if (p.startsWith('**', i)) {
55
+ if (p.charAt(i + 2) === '/') {
56
+ out += '(?:.*/)?'; // any number of directories, including none; the slash goes with it
57
+ i += 3;
58
+ }
59
+ else if (i + 2 === p.length) {
60
+ // Trailing: bare `**` is everything, `foo**` a prefix match. `abc/**` is everything INSIDE
61
+ // abc and NOT abc itself: making the suffix optional matched the directory too, and a rule
62
+ // that reads as "drop this tree but keep one file" then dropped the file with it.
63
+ out += i > 0 && p.charAt(i - 1) === '/' ? '.+' : '.*';
64
+ i += 2;
65
+ }
66
+ else if (i === 0 && plain(p.slice(2))) {
67
+ out += '.*'; // docker's suffixMatch: `**foo` is "ends with foo"
68
+ i += 2;
69
+ }
70
+ else {
71
+ out += '(?:.*/)?';
72
+ i += 2;
73
+ }
74
+ }
75
+ else if (p.charAt(i) === '*') {
76
+ out += '[^/]*';
77
+ i += 1;
78
+ }
79
+ else if (p.charAt(i) === '?') {
80
+ out += '[^/]';
81
+ i += 1;
82
+ }
83
+ else if (p.charAt(i) === '[') {
84
+ const cls = bracket(p, i);
85
+ if (cls) {
86
+ out += cls.re;
87
+ i = cls.end;
88
+ }
89
+ else {
90
+ out += '\\[';
91
+ i += 1;
92
+ }
93
+ }
94
+ else if (p.charAt(i) === '\\' && i + 1 < p.length) {
95
+ // Escape: the next character is data, not a wildcard. Go's filepath.Match honours it.
96
+ out += escapeLiteral(p.charAt(i + 1));
97
+ i += 2;
98
+ }
99
+ else {
100
+ out += escapeLiteral(p.charAt(i));
101
+ i += 1;
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ // Escape members, including an explicitly escaped hyphen. bracket() preserves raw range hyphens.
107
+ const escapeInClass = (c) => ('\\][^-'.includes(c) ? '\\' + c : c);
108
+ // Go/RE2's POSIX classes are ASCII, not JavaScript's Unicode \s/\w or locale-dependent classes.
109
+ // https://pkg.go.dev/regexp/syntax#hdr-Syntax
110
+ const POSIX_CLASSES = {
111
+ alnum: '0-9A-Za-z', alpha: 'A-Za-z', ascii: '\\x00-\\x7f', blank: '\\t ',
112
+ cntrl: '\\x00-\\x1f\\x7f', digit: '0-9', graph: '\\x21-\\x7e', lower: 'a-z',
113
+ print: '\\x20-\\x7e', punct: '\\x21-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7e',
114
+ space: '\\t\\n\\v\\f\\r ', upper: 'A-Z', word: '0-9A-Za-z_', xdigit: '0-9A-Fa-f',
115
+ };
116
+ function posixClass(name, negated) {
117
+ const body = Object.hasOwn(POSIX_CLASSES, name) ? POSIX_CLASSES[name] : undefined;
118
+ if (body === undefined)
119
+ throw new Error('unsupported POSIX class in .dockerignore: ' + name);
120
+ if (!negated)
121
+ return body;
122
+ // A complemented named class can be mixed with other members inside [...]. Expand its
123
+ // ranges instead of nesting a negated JS class, which would silently change the grammar.
124
+ const member = new RegExp('[' + body + ']', 'u');
125
+ const point = (n) => '\\u{' + n.toString(16) + '}';
126
+ const range = (a, b) => a === b ? point(a) : point(a) + '-' + point(b);
127
+ let out = '';
128
+ let start = 0;
129
+ for (let c = 0; c < 128; c++) {
130
+ if (!member.test(String.fromCodePoint(c)))
131
+ continue;
132
+ if (start < c)
133
+ out += range(start, c - 1);
134
+ start = c + 1;
135
+ }
136
+ return out + range(start, 0x10ffff);
137
+ }
138
+ // A bracket expression starting at p[start], or null when no `]` closes it and the `[` is a
139
+ // literal. docker hands the class to Go's regexp: a `]` right after the opening `[` (or after
140
+ // the `^`) is a MEMBER, not the close, `\` quotes the next character, and only `^` negates, so a
141
+ // `!` is an ordinary member. Unlike `*` and `?`, these Go regexp classes CAN match a separator.
142
+ // Verified against moby/patternmatcher compile() on 2026-09-11: it preserves bracket expressions
143
+ // without adding a separator exclusion (private[^x]token matches private/token).
144
+ function bracket(p, start) {
145
+ let j = start + 1;
146
+ let negated = false;
147
+ if (p.charAt(j) === '^') {
148
+ negated = true;
149
+ j += 1;
150
+ }
151
+ let body = '';
152
+ let first = true;
153
+ while (j < p.length) {
154
+ const c = p.charAt(j);
155
+ if (c === ']' && !first)
156
+ return { re: `[${negated ? '^' : ''}${body}]`, end: j + 1 };
157
+ first = false;
158
+ if (p.startsWith('[:', j)) {
159
+ const named = /^\[:(\^?)([a-z]+):\]/.exec(p.slice(j));
160
+ if (!named)
161
+ throw new Error('invalid POSIX class in .dockerignore');
162
+ body += posixClass(named[2], named[1] === '^');
163
+ j += named[0].length;
164
+ continue;
165
+ }
166
+ if (c === '\\' && j + 1 < p.length) {
167
+ body += escapeInClass(p.charAt(j + 1));
168
+ j += 2;
169
+ continue;
170
+ }
171
+ body += c === '-' ? c : escapeInClass(c);
172
+ j += 1;
173
+ }
174
+ return null;
175
+ }
176
+ // Wildcard-free head of a pattern; empty means it could match anywhere. Escape-aware for the same
177
+ // reason translate() is: `\*` is a literal star, so a head stopping at it would prune the wrong
178
+ // tree, and the head must be UNESCAPED because it is compared against real path text.
179
+ function literalHead(full) {
180
+ let out = '';
181
+ for (let i = 0; i < full.length; i++) {
182
+ const c = full.charAt(i);
183
+ if (c === '\\' && i + 1 < full.length) {
184
+ out += full.charAt(i + 1);
185
+ i += 1;
186
+ continue;
187
+ }
188
+ if (c === '*' || c === '?' || c === '[')
189
+ return out;
190
+ out += c;
191
+ }
192
+ return out;
193
+ }
194
+ // Docker's own preprocessing, in its order (moby/patternmatcher ReadAll): the comment test runs
195
+ // BEFORE trimming, so ` #x` is a pattern and not a comment, and every surviving pattern goes
196
+ // through filepath.Clean. Clean is the part that matters most here: it resolves `foo/../secrets`
197
+ // to `secrets` and DROPS a trailing slash, so `secrets/` excludes a file named `secrets` too.
198
+ // Treating that slash as directory-only, the way git does, under-excludes exactly the shape a
199
+ // user writes when they mean "keep this out".
200
+ function cleanDockerPattern(pat) {
201
+ const normalized = posix.normalize(pat);
202
+ const cut = normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
203
+ return cut === '' ? '.' : cut;
204
+ }
205
+ function compileDocker(files) {
206
+ const rules = [];
207
+ for (const f of files) {
208
+ // A UTF-8 BOM belongs to the FILE, not to its first pattern: without stripping it a
209
+ // BOM-prefixed `secrets.env` silently matches nothing.
210
+ const text = f.text.charCodeAt(0) === 0xfeff ? f.text.slice(1) : f.text;
211
+ for (const raw of text.split('\n')) {
212
+ const noEol = raw.replace(/\r+$/, '');
213
+ // Comment test first, untrimmed, then trim: docker's order, not ours.
214
+ if (noEol.startsWith('#'))
215
+ continue;
216
+ let pat = noEol.trim();
217
+ if (!pat)
218
+ continue;
219
+ const negated = pat.startsWith('!');
220
+ if (negated)
221
+ pat = pat.slice(1).trim();
222
+ if (!pat)
223
+ continue;
224
+ pat = cleanDockerPattern(pat);
225
+ if (pat === '.')
226
+ continue;
227
+ // Every pattern is anchored to the context root, slash or not.
228
+ if (pat.startsWith('/'))
229
+ pat = pat.slice(1);
230
+ if (!pat)
231
+ continue;
232
+ // The base is a real DIRECTORY NAME, not pattern syntax, so it is escaped as a literal and
233
+ // joined at the regex level, and prefixed to the prune head the same way, since that head
234
+ // is compared against real path text.
235
+ const prefix = f.base ? escapeLiteral(f.base) + '/' : '';
236
+ const head = f.base ? `${f.base}/${literalHead(pat)}` : literalHead(pat);
237
+ // Go matches runes: without Unicode mode, ? consumes half of a non-BMP filename,
238
+ // letting files the user excluded into the uploaded archive.
239
+ rules.push({ re: new RegExp('^' + prefix + translate(pat) + '$', 'u'), negated, literal: head });
240
+ }
241
+ }
242
+ // A rule matching an ancestor excludes the path too: excluding a dir excludes its contents.
243
+ const hits = (r, path) => {
244
+ if (r.re.test(path))
245
+ return true;
246
+ const parts = path.split('/');
247
+ for (let i = 1; i < parts.length; i++)
248
+ if (r.re.test(parts.slice(0, i).join('/')))
249
+ return true;
250
+ return false;
251
+ };
252
+ const negations = rules.filter((r) => r.negated);
253
+ return {
254
+ excludes(relPath) {
255
+ let excluded = false;
256
+ for (const r of rules)
257
+ if (hits(r, relPath))
258
+ excluded = !r.negated; // last match wins
259
+ return excluded;
260
+ },
261
+ // docker can re-include under an excluded directory, so prune only where no negation reaches.
262
+ canPrune(dirPath) {
263
+ return !negations.some((r) => r.literal === '' || r.literal.startsWith(dirPath + '/') || dirPath.startsWith(r.literal));
264
+ },
265
+ };
266
+ }
267
+ //# sourceMappingURL=pack-ignore.js.map
package/dist/pack.js ADDED
@@ -0,0 +1,229 @@
1
+ import { readdirSync, readFileSync, lstatSync, readlinkSync, existsSync, openSync, closeSync, fstatSync, constants } from 'node:fs';
2
+ import { join, sep } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ // Not node:zlib. The digest of this archive IS its identity: the id the object is stored under,
5
+ // the dedup key, and part of the approval-bound deploy body. The runtime's zlib is native and its
6
+ // output differs between the runtimes this CLI ships on -- the same tree packs to 360 bytes under
7
+ // Node 25 and 353 under Bun -- so with it the identity of a tree changed with the install channel.
8
+ // fflate is pure JS: same algorithm, same bytes, everywhere.
9
+ //
10
+ // PINNED EXACTLY in package.json, not caret-ranged, and that is load-bearing rather than tidy.
11
+ // A compiled binary bundles whatever the lockfile resolved, while `npx insta` resolves the range
12
+ // afresh against the registry. A patch release is free to emit different valid gzip for the same
13
+ // input, so a caret would let the two channels produce different digests for one tree -- exactly
14
+ // the property this dependency was taken on to guarantee.
15
+ import { gzipSync } from 'fflate';
16
+ import { compileIgnore } from './pack-ignore.js';
17
+ export const ARCHIVE_LIMITS = {
18
+ maxArchiveBytes: 256 * 1024 * 1024,
19
+ maxExtractedBytes: 1024 * 1024 * 1024,
20
+ maxFiles: 10000,
21
+ };
22
+ const BLOCK = 512;
23
+ const PAD = Buffer.alloc(BLOCK, 0);
24
+ // Only the exec bit matters: normalising to 0644 breaks entrypoints, raw mode leaks the umask.
25
+ const fileMode = (mode) => (mode & 0o111 ? 0o755 : 0o644);
26
+ const octal = (n, width) => n.toString(8).padStart(width - 1, '0') + '\0';
27
+ // ustar splits a long path across prefix(155) + name(100); refuse by name rather than truncate.
28
+ function splitName(path) {
29
+ if (Buffer.byteLength(path) <= 100)
30
+ return { name: path, prefix: '' };
31
+ for (let i = path.indexOf('/'); i !== -1; i = path.indexOf('/', i + 1)) {
32
+ const prefix = path.slice(0, i);
33
+ const name = path.slice(i + 1);
34
+ if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100)
35
+ return { name, prefix };
36
+ }
37
+ throw new Error(`path too long for a tar archive: ${path}`);
38
+ }
39
+ function header(path, mode, size, type) {
40
+ const h = Buffer.alloc(BLOCK, 0);
41
+ const { name, prefix } = splitName(path);
42
+ h.write(name, 0, 100, 'utf8');
43
+ h.write(octal(mode, 8), 100, 8, 'ascii');
44
+ h.write(octal(0, 8), 108, 8, 'ascii'); // uid, pinned
45
+ h.write(octal(0, 8), 116, 8, 'ascii'); // gid, pinned
46
+ h.write(octal(size, 12), 124, 12, 'ascii');
47
+ h.write(octal(0, 12), 136, 12, 'ascii'); // mtime, pinned
48
+ h.write(' ', 148, 8, 'ascii'); // checksum is summed as spaces, then overwritten
49
+ h.write(type, 156, 1, 'ascii');
50
+ h.write('ustar\0', 257, 6, 'ascii');
51
+ h.write('00', 263, 2, 'ascii');
52
+ h.write(prefix, 345, 155, 'utf8');
53
+ let sum = 0;
54
+ for (const b of h)
55
+ sum += b;
56
+ h.write(octal(sum, 7) + ' ', 148, 8, 'ascii');
57
+ return h;
58
+ }
59
+ // The builder fails the whole build on a symlink entry; hardlinks are fine, we only write type '0'.
60
+ function linkError(links) {
61
+ const shown = links.slice(0, 5).map((l) => ` ${l.path} -> ${l.target}`);
62
+ const more = links.length > shown.length ? [` … and ${links.length - shown.length} more`] : [];
63
+ return new Error([
64
+ 'a deploy archive cannot contain symlinks — the build gateway rejects them:',
65
+ ...shown,
66
+ ...more,
67
+ 'replace them with real files, or exclude them (.dockerignore, or .gitignore when there is no .dockerignore)',
68
+ ].join('\n'));
69
+ }
70
+ // .git blows the 10k entry cap on its own; .insta is CLI state. A deliberate departure from docker.
71
+ const ALWAYS_SKIP = new Set(['.git', '.insta']);
72
+ // docker keeps these whatever the ignore file says; avoids a remote-only "Dockerfile not found".
73
+ const KEPT_AT_ROOT = new Set(['Dockerfile', '.dockerignore']);
74
+ // One global sort emits a parent before its children, since a dir name prefixes everything inside.
75
+ function walk(root, rel, out, links, ig, files, flavour) {
76
+ const dirAbs = join(root, rel === '' ? '.' : rel.split('/').join(sep));
77
+ const names = readdirSync(dirAbs).sort();
78
+ // A nested .gitignore extends its own subtree; recompiled only where one exists. docker has none.
79
+ if (flavour === 'git' && rel !== '' && names.includes('.gitignore')) {
80
+ files = [...files, { base: rel, text: readFileSync(join(dirAbs, '.gitignore'), 'utf8') }];
81
+ ig = compileIgnore(files, 'git');
82
+ }
83
+ for (const name of names) {
84
+ if (ALWAYS_SKIP.has(name))
85
+ continue;
86
+ const relPath = rel === '' ? name : `${rel}/${name}`;
87
+ const keep = rel === '' && KEPT_AT_ROOT.has(name);
88
+ const abs = join(root, relPath.split('/').join(sep));
89
+ const st = lstatSync(abs);
90
+ if (st.isSymbolicLink()) {
91
+ // An ignored symlink is not the user's problem to solve.
92
+ if (!keep && ig.excludes(relPath, false))
93
+ continue;
94
+ links.push({ path: relPath, target: readlinkSync(abs) });
95
+ }
96
+ else if (st.isDirectory()) {
97
+ if (ig.excludes(relPath, true) && ig.canPrune(relPath))
98
+ continue;
99
+ // Emitted whenever we descend, so a re-included child has its parent.
100
+ out.push({ path: `${relPath}/`, mode: 0o755, size: 0, dir: true });
101
+ walk(root, relPath, out, links, ig, files, flavour);
102
+ }
103
+ else if (st.isFile()) {
104
+ if (!keep && ig.excludes(relPath, false))
105
+ continue;
106
+ out.push({ path: relPath, mode: fileMode(st.mode), size: st.size, dir: false, ino: st.ino, dev: st.dev });
107
+ }
108
+ }
109
+ }
110
+ // The walk classifies with lstat and the read happens later, so a plain readFileSync would
111
+ // FOLLOW a symlink that replaced the file in between and put a file from outside the directory
112
+ // into an archive that promises none. Two guards, and neither is a full one on its own:
113
+ //
114
+ // O_NOFOLLOW refuses when the final component is a symlink AT OPEN TIME, closing the swap the
115
+ // walk cannot see. Undefined on Windows, where it degrades to the check below.
116
+ //
117
+ // fstat on the OPEN HANDLE must still describe the file the walk measured: same inode, same
118
+ // device, same size. Its job is the tar's own consistency -- a file rewritten to a different
119
+ // length mid-pack would otherwise produce a header whose count disagrees with its payload.
120
+ //
121
+ // Two things neither closes, and both are stated rather than implied away:
122
+ //
123
+ // An ANCESTOR directory swapped for a symlink. Node exposes no openat, so resolving each
124
+ // component against a directory handle is not available here.
125
+ //
126
+ // A same-size plain file deleted and recreated. Measured on linux rather than assumed: the
127
+ // inode is REUSED and mtimeNs/ctimeNs are byte-identical for a delete+create inside one
128
+ // timestamp tick, so no stat-based identity can see it. It is also the least interesting case
129
+ // -- the symlink promise still holds, the tar stays well formed because the length did not
130
+ // move, and the archive simply carries a slightly newer copy of a file the caller owns.
131
+ //
132
+ // The residual on both is narrow: someone able to rewrite files and directories inside the tree
133
+ // being packed can already put any bytes they like into it by writing them.
134
+ export function readEntry(abs, e) {
135
+ const noFollow = constants.O_NOFOLLOW ?? 0;
136
+ let fd;
137
+ try {
138
+ fd = openSync(abs, constants.O_RDONLY | noFollow);
139
+ }
140
+ catch (err) {
141
+ if (err.code === 'ELOOP') {
142
+ throw new Error(`${e.path} became a symlink while packing — re-run the deploy`);
143
+ }
144
+ throw err;
145
+ }
146
+ try {
147
+ const st = fstatSync(fd);
148
+ const same = st.isFile() && st.size === e.size
149
+ && (e.ino === undefined || st.ino === e.ino) && (e.dev === undefined || st.dev === e.dev);
150
+ if (!same)
151
+ throw new Error(`${e.path} changed while packing — re-run the deploy`);
152
+ return readFileSync(fd);
153
+ }
154
+ finally {
155
+ closeSync(fd);
156
+ }
157
+ }
158
+ // A root .dockerignore wins outright; merging would drop artefacts the image needs.
159
+ function rootIgnore(absDir) {
160
+ const read = (name) => readFileSync(join(absDir, name), 'utf8');
161
+ if (existsSync(join(absDir, '.dockerignore'))) {
162
+ const files = [{ base: '', text: read('.dockerignore') }];
163
+ return { ig: compileIgnore(files, 'docker'), files, flavour: 'docker' };
164
+ }
165
+ const files = existsSync(join(absDir, '.gitignore')) ? [{ base: '', text: read('.gitignore') }] : [];
166
+ return { ig: compileIgnore(files, 'git'), files, flavour: 'git' };
167
+ }
168
+ const mib = (n) => `${(n / (1024 * 1024)).toFixed(1)} MiB`;
169
+ // Windows has no POSIX exec bit for lstat to report, so a script packs as 0644 and the image
170
+ // cannot run it. Same as `docker build` from Windows; say so rather than let it fail at start-up.
171
+ export function windowsModeCaveat(platform = process.platform) {
172
+ if (platform !== 'win32')
173
+ return null;
174
+ return 'packing on Windows: file permissions are not preserved, so an executable script arrives as 0644 — add `RUN chmod +x <path>` to your Dockerfile if the image runs one';
175
+ }
176
+ export function packDirectory(absDir, limits = {}) {
177
+ const cap = { ...ARCHIVE_LIMITS, ...limits };
178
+ const found = [];
179
+ const links = [];
180
+ const { ig, files, flavour } = rootIgnore(absDir);
181
+ walk(absDir, '', found, links, ig, files, flavour);
182
+ if (links.length)
183
+ throw linkError(links);
184
+ found.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
185
+ // Known from the walk alone, so both fail before a byte is read or compressed.
186
+ const extractedBytes = found.reduce((n, e) => n + e.size, 0);
187
+ if (found.length > cap.maxFiles) {
188
+ throw new Error(`archive has too many files: ${found.length} > ${cap.maxFiles} (directories count) — exclude what the build does not need`);
189
+ }
190
+ if (extractedBytes > cap.maxExtractedBytes) {
191
+ throw new Error(`archive would extract to ${mib(extractedBytes)}, over the ${mib(cap.maxExtractedBytes)} limit — exclude what the build does not need`);
192
+ }
193
+ const chunks = [];
194
+ for (const e of found) {
195
+ if (e.dir) {
196
+ chunks.push(header(e.path, e.mode, 0, '5'));
197
+ continue;
198
+ }
199
+ const data = readEntry(join(absDir, e.path.split('/').join(sep)), e);
200
+ chunks.push(header(e.path, e.mode, data.length, '0'), data);
201
+ const rem = data.length % BLOCK;
202
+ if (rem)
203
+ chunks.push(PAD.subarray(0, BLOCK - rem));
204
+ }
205
+ chunks.push(PAD, PAD); // two zero blocks close a tar
206
+ const tar = Buffer.concat(chunks);
207
+ // Drop the per-file buffers before the compressor allocates: concat has copied every byte, so
208
+ // holding the originals through gzip is a third full copy of the tree for nothing. This does
209
+ // not make the packer streaming -- the peak is still two copies plus the compressor's own
210
+ // working set -- but it is the part that costs nothing to give back.
211
+ chunks.length = 0;
212
+ const archive = Buffer.from(gzipSync(tar, { level: 9, mtime: 0 }));
213
+ // Pinned here as well as asked of the library: gzip carries its own mtime (4-7) and OS byte (9),
214
+ // and a header the packer writes itself cannot drift with a dependency's defaults.
215
+ archive.writeUInt32LE(0, 4);
216
+ archive[9] = 255;
217
+ if (archive.length > cap.maxArchiveBytes) {
218
+ throw new Error(`archive is too large: ${mib(archive.length)} > ${mib(cap.maxArchiveBytes)} — exclude what the build does not need`);
219
+ }
220
+ return {
221
+ archive,
222
+ sha256: createHash('sha256').update(archive).digest('hex'),
223
+ files: found.filter((e) => !e.dir).length,
224
+ entries: found.length,
225
+ extractedBytes,
226
+ hasDockerfile: found.some((e) => e.path === 'Dockerfile'),
227
+ };
228
+ }
229
+ //# sourceMappingURL=pack.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.68",
3
+ "version": "0.0.70",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [
@@ -12,13 +12,13 @@
12
12
  "platform"
13
13
  ],
14
14
  "license": "Apache-2.0",
15
- "homepage": "https://github.com/InsForge/insta-cli#readme",
15
+ "homepage": "https://github.com/InsForge/instacloud-cli#readme",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/InsForge/insta-cli.git"
18
+ "url": "git+https://github.com/InsForge/instacloud-cli.git"
19
19
  },
20
20
  "bugs": {
21
- "url": "https://github.com/InsForge/insta-cli/issues"
21
+ "url": "https://github.com/InsForge/instacloud-cli/issues"
22
22
  },
23
23
  "bin": {
24
24
  "insta": "dist/index.js"
@@ -44,6 +44,8 @@
44
44
  "dependencies": {
45
45
  "@clack/prompts": "^0.9.1",
46
46
  "commander": "^12.1.0",
47
+ "fflate": "0.8.3",
48
+ "ignore": "7.0.9",
47
49
  "yaml": "^2.9.0"
48
50
  },
49
51
  "devDependencies": {