@promigence/sdk 0.1.0-beta.4 → 0.1.0-beta.6

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.
@@ -5,7 +5,7 @@ import { type Bundle } from "./bundle.js";
5
5
  import { type PtyExit, type PtySession } from "./pty.js";
6
6
  import { type SshTunnel } from "./ssh.js";
7
7
  import type * as M from "./models.gen.js";
8
- import { type GithubConnect, type GithubDisconnected, type GithubRepos, type GithubStatus, type GithubUntracked } from "./github.js";
8
+ import { type ConnectRequest, type ConnectTicket, type GithubConnect, type GithubDisconnected, type GithubRepos, type GithubStatus, type GithubUntracked } from "./github.js";
9
9
  import { type GitlabConnect, type GitlabDisconnected, type GitlabRepos, type GitlabStatus, type GitlabUntracked } from "./gitlab.js";
10
10
  export type FetchLike = typeof globalThis.fetch;
11
11
  /**
@@ -343,7 +343,8 @@ export interface RunReport {
343
343
  [extra: string]: unknown;
344
344
  }
345
345
  /** A build of a snapshot (`POST /snapshots`, `GET /snapshots/builds/:id`). */
346
- export type BuildState = "queued" | "running" | "verified" | "rejected" | "failed";
346
+ /** `built`: finished and stored with no verification command to run; done, like `verified`. */
347
+ export type BuildState = "queued" | "running" | "verified" | "built" | "rejected" | "failed";
347
348
  export interface SecretRecord {
348
349
  name: string;
349
350
  /** Length of the stored value, so a caller can sanity-check without reading it. */
@@ -353,9 +354,9 @@ export interface SecretRecord {
353
354
  updated_at?: string;
354
355
  /**
355
356
  * The hosts the secret is bound to. Absent when it is not bound: a sandbox that names it is
356
- * then given the value. When present, the sandbox never sees the value: requests it sends to the
357
- * host's address in `urls` are authenticated with the secret; a request made straight to
358
- * `https://<host>/` is not.
357
+ * then given the value. When present, requests the sandbox sends to the host's address in
358
+ * `urls` are authenticated with the secret, a request made straight to `https://<host>/` is not,
359
+ * and the value stays out of the sandbox unless a run asks for it (`raw`).
359
360
  */
360
361
  hosts?: string[];
361
362
  /**
@@ -364,7 +365,7 @@ export interface SecretRecord {
364
365
  */
365
366
  urls?: string[];
366
367
  /**
367
- * The variables a sandbox that names this secret is also given, each pointed at its host's
368
+ * The variables a sandbox that names this secret is also given, each set to its host's
368
369
  * address in `urls`: `OPENAI_BASE_URL` for `api.openai.com`, `ANTHROPIC_BASE_URL` for
369
370
  * `api.anthropic.com`. Absent when there is none. A variable the caller sets itself wins.
370
371
  */
@@ -379,10 +380,10 @@ export interface SecretRecord {
379
380
  scan_responses?: boolean;
380
381
  }
381
382
  /**
382
- * `secrets.set` options: bind the secret to hosts. A sandbox that names a bound secret never sees
383
- * its value: requests it sends to the host's address the answer gives in `urls` (and to
384
- * `$OPENAI_BASE_URL` / `$ANTHROPIC_BASE_URL` for those providers) are authenticated with it, and
385
- * one sent straight to the host is not.
383
+ * `secrets.set` options: bind the secret to hosts. Requests a sandbox sends to the host's address
384
+ * the answer gives in `urls` (and to `$OPENAI_BASE_URL` / `$ANTHROPIC_BASE_URL` for those
385
+ * providers) are authenticated with it, one sent straight to the host is not, and the value stays
386
+ * out of the sandbox unless a run asks for it (`raw: "refused"` forbids that; the default allows it).
386
387
  *
387
388
  * Every option left out keeps what the secret already has, so setting a bound secret again with
388
389
  * a new value (a rotation) keeps its binding. `hosts: []` removes it.
@@ -1478,8 +1479,8 @@ export interface SandboxNetworkCut {
1478
1479
  * secrets: ["OPENAI_API_KEY", { name: "work_secret_7", as: "ANTHROPIC_WORK_SECRET" }]
1479
1480
  * ```
1480
1481
  *
1481
- * A secret bound to hosts (`secrets.set(name, value, { hosts })`) arrives as its placeholder;
1482
- * `mode: "raw"` asks for the value itself, and is refused for one stored with `raw: "refused"`.
1482
+ * A secret bound to hosts (`secrets.set(name, value, { hosts })`) does not put its value in the
1483
+ * sandbox; `mode: "raw"` asks for the value itself, and is refused for one stored with `raw: "refused"`.
1483
1484
  */
1484
1485
  export type SecretBinding = string | {
1485
1486
  name: string;
@@ -1649,6 +1650,14 @@ export interface SnapshotCreateOpts {
1649
1650
  verify?: VerifySpec;
1650
1651
  /** Image forms: a background process started before the snapshot, so forks inherit it running. */
1651
1652
  start?: string | string[];
1653
+ /**
1654
+ * Image and Dockerfile forms: environment variables for every command, terminal and SSH
1655
+ * session in this snapshot's sandboxes (and its own setup, verify and start), set over the
1656
+ * image's `ENV` as `docker run -e` sets them. A value may build on what the image sets with
1657
+ * `${containerEnv:NAME}` (or `${containerEnv:NAME:default}`):
1658
+ * `{ PATH: "${containerEnv:PATH}:/opt/tools/bin" }`.
1659
+ */
1660
+ env?: Record<string, string>;
1652
1661
  tier?: Tier;
1653
1662
  /** Image forms: the name of a stored secret holding the registry credentials. */
1654
1663
  registrySecret?: string;
@@ -1772,28 +1781,6 @@ export declare function imageRefNamesVersion(image: string): boolean;
1772
1781
  export declare function checkImageVersioned(image: string): void;
1773
1782
  /** A git context, pinned: `https://github.com/org/app@<40 hex>`. A branch is not a pin. */
1774
1783
  export declare const CONTEXT_GIT_RE: RegExp;
1775
- /** The prefix a packed directory travels under. */
1776
- export declare const CONTEXT_TAR_PREFIX = "tar;base64,";
1777
- /**
1778
- * Directories never worth sending. `.dockerignore` is **not** applied to a context that fits: it
1779
- * is sent with the context and the build applies it, so honouring it twice could only disagree
1780
- * with the build. The cost is that editing an ignored file still changes the snapshot's
1781
- * identity, which is the safe direction to be wrong in. Only a context over the limit — one that
1782
- * would be refused anyway — is packed again without what its `.dockerignore` excludes.
1783
- */
1784
- export declare const CONTEXT_SKIP: Set<string>;
1785
- /**
1786
- * A `.dockerignore`, as the build reads it: one pattern a line, `#` comments, `!` re-includes,
1787
- * Go `filepath.Match` syntax plus `**`, anchored at the context root, and a pattern that matches a
1788
- * directory matches everything under it. The last pattern that matches a path decides. Returns
1789
- * `undefined` for a file with no patterns.
1790
- */
1791
- export declare function dockerignoreMatcher(text: string): {
1792
- ignored: (rel: string) => boolean;
1793
- exceptions: boolean;
1794
- } | undefined;
1795
- /** The server's limit, refused here so a 9 MB context does not cross the wire to be told no. */
1796
- export declare const CONTEXT_MAX_BYTES: number;
1797
1784
  /**
1798
1785
  * What in a repository URL can carry a credential: a user name, password or token (`https://TOKEN@…`,
1799
1786
  * `https://user:pass@…`, `:token@`, a percent-encoded `@`, any `@` in a scheme URL, the scp form's
@@ -1813,19 +1800,6 @@ export declare const REPO_CREDENTIALS_FIX: string;
1813
1800
  export declare function checkRepoUrl(url: unknown, field: string): void;
1814
1801
  /** Refuse a context that is neither a pinned git URL nor a packed directory. */
1815
1802
  export declare function checkDockerfileContext(context: unknown): asserts context is string;
1816
- /**
1817
- * Pack a directory into the string `dockerfile.context` takes.
1818
- *
1819
- * The archive is only transport: the snapshot's identity is computed from the *entries* server
1820
- * side, so this does not have to agree byte-for-byte with `sdk-py`'s packer — and two calls on
1821
- * an unchanged directory give the same snapshot either way.
1822
- */
1823
- export declare function packContext(dir: string, opts?: {
1824
- maxBytes?: number;
1825
- keep?: readonly string[];
1826
- }): string;
1827
- /** The Dockerfile's path as the context sees it, for `dockerfile.path`. */
1828
- export declare function contextRelative(contextDir: string, dockerfilePath: string): string;
1829
1803
  /**
1830
1804
  * The `POST /snapshots` body for one of its three forms; throws `InvalidArgumentError` before
1831
1805
  * anything is sent. Kept field-for-field with `sdk-py` (`_ops.snapshot_create_body`) so the two
@@ -2115,6 +2089,14 @@ export declare class PromigenceClient {
2115
2089
  status: () => Promise<GithubStatus>;
2116
2090
  /** `POST /integrations/github/connect` — the install link (15 minutes); open it in a browser. */
2117
2091
  connect: () => Promise<GithubConnect>;
2092
+ /**
2093
+ * `POST /integrations/github/requests` — a connection started for you in the console: open
2094
+ * `connect_url` in a browser signed in to the console as you (a 404 means the console cannot start
2095
+ * it here: use `connect()`).
2096
+ */
2097
+ ticket: () => Promise<ConnectTicket>;
2098
+ /** `GET /integrations/github/requests/:id` — one of your connection requests: `pending`, or how it ended. */
2099
+ request: (requestId: string) => Promise<ConnectRequest>;
2118
2100
  /** `DELETE /integrations/github/installations/:id` — nothing is built from that account any more. */
2119
2101
  disconnect: (installationId: number | string) => Promise<GithubDisconnected>;
2120
2102
  /** `GET /integrations/github/repos` — one page (at most 100) across every connection. */
@@ -2135,6 +2117,14 @@ export declare class PromigenceClient {
2135
2117
  status: () => Promise<GitlabStatus>;
2136
2118
  /** `POST /integrations/gitlab/connect` — the authorize link (15 minutes); open it in a browser. */
2137
2119
  connect: () => Promise<GitlabConnect>;
2120
+ /**
2121
+ * `POST /integrations/gitlab/requests` — a connection started for you in the console: open
2122
+ * `connect_url` in a browser signed in to the console as you (a 404 means the console cannot start
2123
+ * it here: use `connect()`).
2124
+ */
2125
+ ticket: () => Promise<ConnectTicket>;
2126
+ /** `GET /integrations/gitlab/requests/:id` — one of your connection requests: `pending`, or how it ended. */
2127
+ request: (requestId: string) => Promise<ConnectRequest>;
2138
2128
  /** `DELETE /integrations/gitlab/connections/:id` — revoked on GitLab; nothing is built through it any more. */
2139
2129
  disconnect: (connectionId: string) => Promise<GitlabDisconnected>;
2140
2130
  /** `GET /integrations/gitlab/repos` — one page (at most 100) of one connection's projects. */
@@ -2636,6 +2626,9 @@ export declare class PromigenceClient {
2636
2626
  * A failure mid-loop leaves a **prefix** in the sandbox, not a corrupt file, and the
2637
2627
  * thrown error carries how far it got (`written` in the error's evidence).
2638
2628
  * Re-running the same `put` is safe: chunk 0 truncates.
2629
+ *
2630
+ * `mode` (`0o755`) sets the file's permission bits. Without it a file that already exists
2631
+ * keeps its mode, as a shell `>` does, and a new one is `0o644`.
2639
2632
  */
2640
2633
  put: (sandboxId: string, path: string, data: string | Uint8Array, opts?: {
2641
2634
  mode?: number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PromigenceClient = exports.LONG_POLL_MS = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_MAX_BYTES = exports.CONTEXT_SKIP = exports.CONTEXT_TAR_PREFIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
3
+ exports.PromigenceClient = exports.LONG_POLL_MS = exports.REPO_CREDENTIALS_FIX = exports.CONTEXT_GIT_RE = exports.IMAGE_DIGEST_RE = exports.MAX_SOURCES = exports.NOTICE_HEADER = exports.isWaiting = exports.FILE_TIMEOUT_MS = void 0;
4
4
  exports.execOutputBytes = execOutputBytes;
5
5
  exports.configDir = configDir;
6
6
  exports.credentialsPath = credentialsPath;
@@ -18,13 +18,10 @@ exports.imageRefProblem = imageRefProblem;
18
18
  exports.checkImageRef = checkImageRef;
19
19
  exports.imageRefNamesVersion = imageRefNamesVersion;
20
20
  exports.checkImageVersioned = checkImageVersioned;
21
- exports.dockerignoreMatcher = dockerignoreMatcher;
22
21
  exports.repoUrlCredential = repoUrlCredential;
23
22
  exports.withoutPin = withoutPin;
24
23
  exports.checkRepoUrl = checkRepoUrl;
25
24
  exports.checkDockerfileContext = checkDockerfileContext;
26
- exports.packContext = packContext;
27
- exports.contextRelative = contextRelative;
28
25
  exports.snapshotCreateBody = snapshotCreateBody;
29
26
  exports.errorFor = errorFor;
30
27
  exports.isKeyedCreate = isKeyedCreate;
@@ -44,6 +41,7 @@ const caller_js_1 = require("./caller.js");
44
41
  const pty_js_1 = require("./pty.js");
45
42
  const ssh_js_1 = require("./ssh.js");
46
43
  const http2_js_1 = require("./http2.js");
44
+ const context_js_1 = require("./context.js");
47
45
  const proxy_js_1 = require("./proxy.js");
48
46
  const trace_js_1 = require("./trace.js");
49
47
  const keyed_gen_js_1 = require("./keyed.gen.js");
@@ -443,81 +441,6 @@ function checkImageVersioned(image) {
443
441
  throw new core_js_1.InvalidArgumentError(`image ${JSON.stringify(ref)} names no tag or digest, so it would be pinned to whatever ${ref}:latest is today`, `Name the version: a tag (${ref}:<version>), ${ref}:latest if you mean the moving tag, or a digest (${ref}@sha256:<64 hex>).`);
444
442
  }
445
443
  exports.CONTEXT_GIT_RE = /^(?:https?:\/\/|ssh:\/\/|git:\/\/|git@)\S+@[0-9a-f]{40}$/i;
446
- exports.CONTEXT_TAR_PREFIX = "tar;base64,";
447
- exports.CONTEXT_SKIP = new Set([".git", "node_modules", "__pycache__", ".venv", ".DS_Store"]);
448
- function dockerignoreMatcher(text) {
449
- const rules = [];
450
- for (const raw of text.split(/\r?\n/)) {
451
- let line = raw.trim();
452
- if (!line || line.startsWith("#"))
453
- continue;
454
- const exclusion = line.startsWith("!");
455
- if (exclusion)
456
- line = line.slice(1).trim();
457
- const parts = [];
458
- for (const seg of line.split("/")) {
459
- if (seg === "" || seg === ".")
460
- continue;
461
- if (seg === "..")
462
- parts.pop();
463
- else
464
- parts.push(seg);
465
- }
466
- const pattern = parts.join("/");
467
- if (!pattern)
468
- continue;
469
- let re = "^";
470
- for (let i = 0; i < pattern.length; i++) {
471
- const ch = pattern[i];
472
- if (ch === "*") {
473
- if (pattern[i + 1] === "*") {
474
- i++;
475
- if (pattern[i + 1] === "/")
476
- i++;
477
- re += i + 1 >= pattern.length ? ".*" : "(.*/)?";
478
- }
479
- else {
480
- re += "[^/]*";
481
- }
482
- }
483
- else if (ch === "?") {
484
- re += "[^/]";
485
- }
486
- else if (ch === "\\") {
487
- re += i + 1 < pattern.length ? `\\${pattern[++i]}` : "\\\\";
488
- }
489
- else if (ch === "[" || ch === "]") {
490
- re += ch;
491
- }
492
- else {
493
- re += /[.+()|{}$]/.test(ch) ? `\\${ch}` : ch;
494
- }
495
- }
496
- try {
497
- rules.push({ re: new RegExp(`${re}$`), exclusion });
498
- }
499
- catch {
500
- }
501
- }
502
- if (rules.length === 0)
503
- return undefined;
504
- const ignored = (rel) => {
505
- const dirs = rel.split("/");
506
- let matched = false;
507
- for (const r of rules) {
508
- if (r.exclusion !== matched)
509
- continue;
510
- let hit = r.re.test(rel);
511
- for (let i = 1; !hit && i < dirs.length; i++)
512
- hit = r.re.test(dirs.slice(0, i).join("/"));
513
- if (hit)
514
- matched = !r.exclusion;
515
- }
516
- return matched;
517
- };
518
- return { ignored, exceptions: rules.some((r) => r.exclusion) };
519
- }
520
- exports.CONTEXT_MAX_BYTES = 8 * 1024 * 1024;
521
444
  function repoUrlCredential(url) {
522
445
  const u = url.trim();
523
446
  const sep = u.indexOf("://");
@@ -556,123 +479,17 @@ function checkRepoUrl(url, field) {
556
479
  }
557
480
  function checkDockerfileContext(context) {
558
481
  const c = typeof context === "string" ? context.trim() : "";
559
- if (!c.startsWith(exports.CONTEXT_TAR_PREFIX))
482
+ if (!c.startsWith(context_js_1.CONTEXT_TAR_PREFIX))
560
483
  checkRepoUrl(withoutPin(c), "dockerfile.context");
561
484
  if (!c) {
562
485
  throw new core_js_1.InvalidArgumentError("snapshots.create dockerfile needs a context", "Pass { dockerfile: { context: 'https://github.com/org/app@<40-char sha>', path: 'Dockerfile' } }, " +
563
486
  "or pack a directory with packContext('./app').");
564
487
  }
565
- if (c.startsWith(exports.CONTEXT_TAR_PREFIX) || exports.CONTEXT_GIT_RE.test(c))
488
+ if (c.startsWith(context_js_1.CONTEXT_TAR_PREFIX) || exports.CONTEXT_GIT_RE.test(c))
566
489
  return;
567
490
  throw new core_js_1.InvalidArgumentError(`dockerfile.context must be a git URL pinned to a full 40-character sha, got ${JSON.stringify(context)}`, "A branch or a tag is not a pin: pass URL@<40-char sha> (git rev-parse HEAD), or pack a local " +
568
491
  "directory with packContext('./app').");
569
492
  }
570
- function packContext(dir, opts = {}) {
571
- const max = opts.maxBytes ?? exports.CONTEXT_MAX_BYTES;
572
- const keep = new Set([".dockerignore", ...(opts.keep ?? [])]);
573
- const collect = (ignore) => {
574
- const entries = [];
575
- let total = 0;
576
- const walk = (abs, rel) => {
577
- for (const name of (0, node_fs_1.readdirSync)(abs).sort()) {
578
- if (exports.CONTEXT_SKIP.has(name))
579
- continue;
580
- const childAbs = (0, node_path_1.join)(abs, name);
581
- const childRel = rel ? `${rel}/${name}` : name;
582
- const st = (0, node_fs_1.lstatSync)(childAbs);
583
- const dropped = ignore !== undefined && !keep.has(childRel) && ignore.ignored(childRel);
584
- if (st.isDirectory()) {
585
- if (dropped && !ignore.exceptions)
586
- continue;
587
- if (!walk(childAbs, childRel))
588
- return false;
589
- continue;
590
- }
591
- if (dropped)
592
- continue;
593
- if (st.isSymbolicLink()) {
594
- entries.push({ path: childRel, mode: 0o777, body: Buffer.alloc(0), link: (0, node_fs_1.readlinkSync)(childAbs) });
595
- continue;
596
- }
597
- if (!st.isFile())
598
- continue;
599
- total += st.size;
600
- if (total > max)
601
- return false;
602
- entries.push({ path: childRel, mode: st.mode & 0o111 ? 0o755 : 0o644, body: (0, node_fs_1.readFileSync)(childAbs) });
603
- }
604
- return true;
605
- };
606
- return walk(dir, "") ? entries : undefined;
607
- };
608
- let entries = collect();
609
- if (entries === undefined) {
610
- let ignore;
611
- try {
612
- ignore = dockerignoreMatcher((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, ".dockerignore"), "utf8"));
613
- }
614
- catch {
615
- ignore = undefined;
616
- }
617
- if (ignore !== undefined)
618
- entries = collect(ignore);
619
- if (entries === undefined) {
620
- throw new core_js_1.InvalidArgumentError(`the build context is over ${max} bytes${ignore ? " even without what its .dockerignore excludes" : ""}`, `${ignore ? "Exclude more in .dockerignore" : "Add a .dockerignore"}, point --context at a smaller directory, or pass a git URL pinned to a full 40-character sha.`);
621
- }
622
- }
623
- if (entries.length === 0) {
624
- throw new core_js_1.InvalidArgumentError(`the build context ${JSON.stringify(dir)} is empty`, "Point --context at the directory holding your Dockerfile.");
625
- }
626
- return exports.CONTEXT_TAR_PREFIX + tarOf(entries).toString("base64");
627
- }
628
- function contextRelative(contextDir, dockerfilePath) {
629
- const rel = (0, node_path_1.relative)(contextDir, dockerfilePath).split(node_path_1.sep).join("/");
630
- if (!rel || rel.startsWith("..")) {
631
- throw new core_js_1.InvalidArgumentError(`${JSON.stringify(dockerfilePath)} is not inside the context ${JSON.stringify(contextDir)}`, "Pass --context DIR with the Dockerfile underneath it.");
632
- }
633
- return rel;
634
- }
635
- function tarOf(entries) {
636
- const out = [];
637
- for (const e of [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
638
- const head = Buffer.alloc(512);
639
- let name = e.path;
640
- let prefix = "";
641
- if (Buffer.byteLength(name) > 100) {
642
- const cut = name.lastIndexOf("/", name.length - 100);
643
- if (cut <= 0 || Buffer.byteLength(name.slice(cut + 1)) > 100 || Buffer.byteLength(name.slice(0, cut)) > 155) {
644
- throw new core_js_1.InvalidArgumentError(`the path ${JSON.stringify(e.path)} is too long for a build context`, "Shorten the path or pass a git URL pinned to a full 40-character sha.");
645
- }
646
- prefix = name.slice(0, cut);
647
- name = name.slice(cut + 1);
648
- }
649
- const put = (s, at, len) => { head.write(s.slice(0, len), at, len, "utf8"); };
650
- const oct = (n, at, len) => { put(n.toString(8).padStart(len - 1, "0") + "\0", at, len); };
651
- put(name, 0, 100);
652
- oct(e.mode, 100, 8);
653
- oct(0, 108, 8);
654
- oct(0, 116, 8);
655
- oct(e.link === undefined ? e.body.length : 0, 124, 12);
656
- oct(0, 136, 12);
657
- head.write(" ", 148, 8, "utf8");
658
- head.write(e.link === undefined ? "0" : "2", 156, 1, "utf8");
659
- if (e.link !== undefined)
660
- put(e.link, 157, 100);
661
- head.write("ustar\0", 257, 6, "binary");
662
- head.write("00", 263, 2, "utf8");
663
- put(prefix, 345, 155);
664
- let sum = 0;
665
- for (const b of head)
666
- sum += b;
667
- put(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
668
- out.push(head);
669
- if (e.link === undefined && e.body.length > 0) {
670
- out.push(e.body, Buffer.alloc((512 - (e.body.length % 512)) % 512));
671
- }
672
- }
673
- out.push(Buffer.alloc(1024));
674
- return Buffer.concat(out);
675
- }
676
493
  function sourcesBody(sources) {
677
494
  if (sources.length > exports.MAX_SOURCES) {
678
495
  throw new core_js_1.InvalidArgumentError(`snapshots.create takes at most ${exports.MAX_SOURCES} extra sources, got ${sources.length}`, "An environment assembled from more repositories than that is cheaper to build once as " +
@@ -720,6 +537,9 @@ function snapshotCreateBody(opts) {
720
537
  let body;
721
538
  const github = typeof o.repo === "string"
722
539
  && (o.repo.trim().startsWith(github_js_1.GITHUB_REPO_PREFIX) || o.repo.trim().startsWith(gitlab_js_1.GITLAB_REPO_PREFIX));
540
+ if (o.env !== undefined && Object.keys(o.env).length && forms[0] === "repo") {
541
+ throw new core_js_1.InvalidArgumentError("snapshots.create env is for an image or a Dockerfile", "A repository snapshot sets its environment in its setup steps, or pass env on each command.");
542
+ }
723
543
  if (!github && (o.ref !== undefined || o.from !== undefined || o.track)) {
724
544
  throw new core_js_1.InvalidArgumentError("snapshots.create ref, from and track are for a connected repository (repo: 'github:owner/name' or 'gitlab:group/project')", "Pass { repo: 'github:your-org/app', ref: 'main' }, or drop them for this form.");
725
545
  }
@@ -775,6 +595,8 @@ function snapshotCreateBody(opts) {
775
595
  body["tier"] = o.tier;
776
596
  if (o.registrySecret)
777
597
  body["registry_secret"] = o.registrySecret;
598
+ if (o.env !== undefined && Object.keys(o.env).length)
599
+ body["env"] = { ...o.env };
778
600
  }
779
601
  if (o.alias)
780
602
  body["alias"] = o.alias;
@@ -1521,6 +1343,11 @@ class PromigenceClient {
1521
1343
  github = {
1522
1344
  status: () => this.request({ method: "GET", path: "/integrations/github" }),
1523
1345
  connect: () => this.request({ method: "POST", path: "/integrations/github/connect", body: {} }),
1346
+ ticket: () => this.request({ method: "POST", path: "/integrations/github/requests", body: {} }),
1347
+ request: (requestId) => this.request({
1348
+ method: "GET",
1349
+ path: `/integrations/github/requests/${encodeURIComponent(requestId)}`,
1350
+ }),
1524
1351
  disconnect: (installationId) => this.request({
1525
1352
  method: "DELETE",
1526
1353
  path: `/integrations/github/installations/${encodeURIComponent(String(installationId))}`,
@@ -1539,6 +1366,11 @@ class PromigenceClient {
1539
1366
  gitlab = {
1540
1367
  status: () => this.request({ method: "GET", path: "/integrations/gitlab" }),
1541
1368
  connect: () => this.request({ method: "POST", path: "/integrations/gitlab/connect", body: {} }),
1369
+ ticket: () => this.request({ method: "POST", path: "/integrations/gitlab/requests", body: {} }),
1370
+ request: (requestId) => this.request({
1371
+ method: "GET",
1372
+ path: `/integrations/gitlab/requests/${encodeURIComponent(requestId)}`,
1373
+ }),
1542
1374
  disconnect: (connectionId) => this.request({
1543
1375
  method: "DELETE",
1544
1376
  path: `/integrations/gitlab/connections/${encodeURIComponent(connectionId)}`,
@@ -0,0 +1,119 @@
1
+ /**
2
+ * A local directory as a Dockerfile build context: what `packContext` sends, decided before
3
+ * anything leaves this machine.
4
+ *
5
+ * - **`.dockerignore` is applied here, always**, with the rules `docker build` uses: Go glob syntax
6
+ * plus `**`, `!` to bring a path back, the last matching line wins, a matched directory takes
7
+ * everything under it, and paths are read from the context root. A file the `.dockerignore`
8
+ * excludes is never uploaded. `<Dockerfile>.dockerignore` beside the Dockerfile is read instead
9
+ * of the root `.dockerignore` when it exists, as `docker build` does. The Dockerfile and the
10
+ * ignore files themselves are always sent, whatever a pattern says.
11
+ * - **`.git` is never sent** unless the `.dockerignore` has the line `!.git`: git history can hold
12
+ * credentials long deleted from the tree.
13
+ * - **Without a `.dockerignore`**, `node_modules`, `__pycache__`, `.venv` and `.DS_Store` are left
14
+ * out too, and a note says so; with one, only what it lists is left out.
15
+ * - Empty directories and permission bits travel as they are on disk; a set-user-ID or
16
+ * set-group-ID bit is cleared, with a note.
17
+ * - Files that look like credentials (`.env`, private keys, cloud credentials, ...) are refused,
18
+ * by name, unless `allowSecrets` says otherwise. The check goes by file name: it is
19
+ * best-effort and cannot see what a file holds, so look over the list (`listContext`) before
20
+ * you send a directory.
21
+ * - Symbolic links travel as links, never as the file they point at.
22
+ */
23
+ /** The prefix a packed directory travels under in `dockerfile.context`. */
24
+ export declare const CONTEXT_TAR_PREFIX = "tar;base64,";
25
+ /** The largest context accepted, counted in file bytes after `.dockerignore`. */
26
+ export declare const CONTEXT_MAX_BYTES: number;
27
+ /**
28
+ * Names left out when the context has no `.dockerignore`. With one, the `.dockerignore` alone
29
+ * decides, except for `.git` (sent only when it says `!.git`).
30
+ */
31
+ export declare const CONTEXT_SKIP: Set<string>;
32
+ /** The patterns of one ignore file, compiled. */
33
+ export interface DockerIgnore {
34
+ /** The ignore file these came from (`.dockerignore` or `<Dockerfile>.dockerignore`). */
35
+ readonly file: string;
36
+ /** The patterns as the build holds them, in file order: cleaned, an exception keeping its `!`. */
37
+ readonly patterns: readonly string[];
38
+ /** Whether any line is a `!` exception. */
39
+ readonly exceptions: boolean;
40
+ /** Whether a line says `!.git`, which is what sends `.git`. */
41
+ readonly sendsGit: boolean;
42
+ /**
43
+ * Is `path` (relative to the context root, `/`-separated) left out? The last pattern that
44
+ * matches it, or one of the directories above it, decides.
45
+ */
46
+ ignored(path: string): boolean;
47
+ }
48
+ /**
49
+ * Read an ignore file the way `docker build` reads it: a UTF-8 BOM is dropped; a line starting
50
+ * with `#` is a comment; each line is trimmed; `!` starts an exception; each pattern is cleaned
51
+ * (`a//b/../c` is `a/c`) and a leading `/` is dropped. A line the build cannot read (a lone `!`,
52
+ * an unclosed `[`, a trailing `\`) makes the build refuse the whole file, so it is refused here,
53
+ * naming the line, before anything is sent.
54
+ */
55
+ export declare function parseDockerignore(text: string, file?: string): DockerIgnore;
56
+ /**
57
+ * A `.dockerignore`'s patterns as a predicate, or `undefined` for a file with no patterns. Refuses
58
+ * a file the build could not read. Kept for code written against 0.1; `parseDockerignore` is the
59
+ * full reader.
60
+ */
61
+ export declare function dockerignoreMatcher(text: string): {
62
+ ignored: (rel: string) => boolean;
63
+ exceptions: boolean;
64
+ } | undefined;
65
+ /**
66
+ * Does this path look like a credential (`.env`, a private key, cloud or registry credentials, a
67
+ * token file, ...)? By name only: best-effort, it cannot see what a file holds. Templates such as
68
+ * `.env.example` are not.
69
+ */
70
+ export declare function secretLike(path: string): boolean;
71
+ /** What a build context is packed from and sent with. */
72
+ export interface ContextOptions {
73
+ /**
74
+ * The Dockerfile's path inside the context, `/`-separated (default `Dockerfile`). It is always
75
+ * sent, and `<it>.dockerignore`, when present, is read instead of the root `.dockerignore`.
76
+ */
77
+ dockerfile?: string;
78
+ /** More paths inside the context to send whatever `.dockerignore` says. */
79
+ keep?: readonly string[];
80
+ /** Refuse a context whose files add up to more than this many bytes (default `CONTEXT_MAX_BYTES`). */
81
+ maxBytes?: number;
82
+ /**
83
+ * Send files that look like credentials. Default `false`: they are refused, by name, and
84
+ * nothing is sent.
85
+ */
86
+ allowSecrets?: boolean;
87
+ /** Called with each note: what was left out by default, or changed on the way. */
88
+ onNote?: (note: string) => void;
89
+ }
90
+ /** What a build context would hold, before anything is sent. */
91
+ export interface ContextListing {
92
+ /** Every path that would be sent, sorted; a directory's ends in `/`. */
93
+ paths: string[];
94
+ /** How many of them are files or links. */
95
+ files: number;
96
+ /** The files' bytes. */
97
+ bytes: number;
98
+ /** What was left out by default or changed on the way, one sentence each. */
99
+ notes: string[];
100
+ /** The paths that would be sent and look like credentials. */
101
+ secrets: string[];
102
+ /** The ignore file applied, or `null` when there is none. */
103
+ ignoreFile: string | null;
104
+ }
105
+ /**
106
+ * What `packContext(dir, opts)` would send, without sending or packing anything: every path, the
107
+ * notes, and the files that look like credentials. Refuses only what `packContext` cannot read
108
+ * (an ignore file the build would refuse).
109
+ */
110
+ export declare function listContext(dir: string, opts?: ContextOptions): ContextListing;
111
+ /**
112
+ * Pack a directory into the string `dockerfile.context` takes. What is sent is decided here (see
113
+ * the module notes): files the `.dockerignore` excludes, and `.git`, are never uploaded; files
114
+ * that look like credentials are refused unless `allowSecrets`; a context over `maxBytes` after
115
+ * `.dockerignore` is refused. The same tree packs to the same snapshot whichever SDK packs it.
116
+ */
117
+ export declare function packContext(dir: string, opts?: ContextOptions): string;
118
+ /** The Dockerfile's path as the context sees it, for `dockerfile.path`. */
119
+ export declare function contextRelative(contextDir: string, dockerfilePath: string): string;