promigence 0.1.0-beta.3 → 0.1.0-beta.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/account.js +1 -1
- package/dist/commands.d.ts +1 -0
- package/dist/commands.js +60 -19
- package/dist/connectreq.js +125 -0
- package/dist/devcontainer.d.ts +2 -0
- package/dist/devcontainer.js +8 -13
- package/dist/dirsnap.js +67 -36
- package/dist/generated.d.ts +1 -0
- package/dist/generated.js +8 -4
- package/dist/github.js +16 -0
- package/dist/gitlab.js +16 -0
- package/dist/guide.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/login.js +34 -6
- package/dist/mcp.js +34 -10
- package/dist/routes.js +4 -0
- package/dist/sdk/client.d.ts +40 -47
- package/dist/sdk/client.js +18 -183
- package/dist/sdk/context.d.ts +119 -0
- package/dist/sdk/context.js +588 -0
- package/dist/sdk/core.d.ts +1 -1
- package/dist/sdk/core.js +2 -1
- package/dist/sdk/github.d.ts +27 -0
- package/dist/sdk/index.d.ts +1 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/models.gen.d.ts +121 -33
- package/dist/sdk/proxy.js +35 -0
- package/dist/sdk/pty.js +2 -3
- package/dist/sdk/sandbox.d.ts +8 -2
- package/dist/sdk/sandbox.js +3 -3
- package/dist/sdk/ssh.js +2 -3
- package/dist/surface.js +18 -14
- package/dist/uploadguard.js +75 -0
- package/llms.txt +8 -8
- package/package.json +1 -1
package/dist/account.js
CHANGED
|
@@ -333,7 +333,7 @@ function stateFlag(ctx, states) {
|
|
|
333
333
|
}
|
|
334
334
|
return wanted;
|
|
335
335
|
}
|
|
336
|
-
const BUILD_STATES = ["queued", "running", "verified", "rejected", "failed"];
|
|
336
|
+
const BUILD_STATES = ["queued", "running", "verified", "built", "rejected", "failed"];
|
|
337
337
|
const RUN_STATES = ["running", "completed", "cap_killed", "aborted"];
|
|
338
338
|
const snapshotBuilds = async (ctx) => {
|
|
339
339
|
const wanted = stateFlag(ctx, BUILD_STATES);
|
package/dist/commands.d.ts
CHANGED
|
@@ -99,5 +99,6 @@ export declare function outputTails(c: PromigenceClient, sandboxes: ReadonlyArra
|
|
|
99
99
|
}>;
|
|
100
100
|
}>): Promise<Map<string, OutputTail>>;
|
|
101
101
|
export declare function positional(ctx: Ctx, what: string, example: string): string;
|
|
102
|
+
export declare function filesPutTarArgs(out: string, parent: string, base: string, noMetadata?: string[]): string[];
|
|
102
103
|
export declare const HANDLERS: Readonly<Record<string, Handler>>;
|
|
103
104
|
export {};
|
package/dist/commands.js
CHANGED
|
@@ -3,11 +3,12 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeSync, } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
-
import { DEFAULT_API_URL, DEFAULT_SNAPSHOT, ERRORS_DOCS_URL, MAX_OUTPUT_BYTES_DEFAULT, CONTEXT_GIT_RE, PromigenceClient, checkRepoUrl, withoutPin, PromigenceError, SpendCapError, TimeoutError, checkImageRef, contextRelative, credentialsPath, FILE_PUT_CHUNK_BYTES, imageSnapshot, isWaiting, execOutputBytes, packContext, readCredentials, } from "./sdk/index.js";
|
|
6
|
+
import { DEFAULT_API_URL, DEFAULT_SNAPSHOT, ERRORS_DOCS_URL, MAX_OUTPUT_BYTES_DEFAULT, CONTEXT_GIT_RE, PromigenceClient, checkRepoUrl, withoutPin, PromigenceError, SpendCapError, TimeoutError, checkImageRef, contextRelative, credentialsPath, FILE_PUT_CHUNK_BYTES, imageSnapshot, isWaiting, execOutputBytes, listContext, packContext, readCredentials, } from "./sdk/index.js";
|
|
7
7
|
import { fmtMs, recordTiming } from "./timing.js";
|
|
8
8
|
import { capacityLine, queuedClause, queuedLines, statusLine } from "./capacity.js";
|
|
9
|
-
import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, resolveGiven, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
|
|
9
|
+
import { DevcontainerError, detectOne, load, planJson, planLines, planSetup, resolveGiven, snapshotEnv, translate, workspaceRootFor, wrapCommand, } from "./devcontainer.js";
|
|
10
10
|
import { detect, ecosystemOfCommands, imageCreateCommand, readConfig } from "./init.js";
|
|
11
|
+
import { CREDENTIAL_REMEDY, confirmOverride, credentialRefusal, emitUploadList, localTarNoMetadataArgs } from "./uploadguard.js";
|
|
11
12
|
import { emit, errorEnvelope, errorTextLines, exitWith, forkLine, forkSummary, gradeLine, ms, noCommandFix, okEnvelope, parseMaxOutput, recordNotice, sandboxGrade, parseCommandTimeout, parseCount, parseDuration, parseUsd, pickExit, reportSummaryLines, toPromigenceError, totalsLine, usageError, write, writeBytes, } from "./io.js";
|
|
12
13
|
import { buildPrice } from "./pricing.js";
|
|
13
14
|
import { DOCS_URL, WAIT_TIMEOUT_DEFAULT } from "./surface.js";
|
|
@@ -366,7 +367,7 @@ const secretsSet = async (ctx) => {
|
|
|
366
367
|
`${sh `promigence secrets set ${name}`}: stored ${res.bytes} bytes from ${source}`,
|
|
367
368
|
...(res.hosts?.length
|
|
368
369
|
? [
|
|
369
|
-
` bound to ${res.hosts.join(", ")}: the sandbox
|
|
370
|
+
` bound to ${res.hosts.join(", ")}: the value stays out of the sandbox unless a run asks for it (the variable holds ${res.placeholder ?? "something else"})`,
|
|
370
371
|
...boundUseLines(res),
|
|
371
372
|
...(res.raw === "refused" ? [" its value can never be given to a sandbox"] : []),
|
|
372
373
|
]
|
|
@@ -440,7 +441,7 @@ function isBuilding(b) {
|
|
|
440
441
|
return b.state === "queued" || b.state === "running";
|
|
441
442
|
}
|
|
442
443
|
function buildExit(b) {
|
|
443
|
-
if (b.state === "verified")
|
|
444
|
+
if (b.state === "verified" || b.state === "built")
|
|
444
445
|
return 0;
|
|
445
446
|
if (b.state === "rejected")
|
|
446
447
|
return 6;
|
|
@@ -463,7 +464,7 @@ export function verifyTiming(v) {
|
|
|
463
464
|
function buildLines(b) {
|
|
464
465
|
const note = b.deduped ? " (deduped: an identical build already ran)"
|
|
465
466
|
: b.idempotent_replay === true ? " (reused: the same request already built it)" : "";
|
|
466
|
-
const unverified = b.state === "verified" && b.verified === false;
|
|
467
|
+
const unverified = b.state === "built" || (b.state === "verified" && b.verified === false);
|
|
467
468
|
const shown = unverified ? "built (no --verify: not verified)" : b.state;
|
|
468
469
|
const lines = [`build ${b.build_id}: ${shown}${note}`];
|
|
469
470
|
if (b.snapshot_id)
|
|
@@ -487,7 +488,7 @@ async function emitBuild(ctx, b, extra = {}, more = []) {
|
|
|
487
488
|
const exit = buildExit(b);
|
|
488
489
|
const next = b.snapshot_deleted === true
|
|
489
490
|
? nexts(["promigence snapshot list", "this build's snapshot was deleted; the snapshots you have now"])
|
|
490
|
-
: b.snapshot_id && (b.state === "verified" || (!isBuilding(b) && b.state !== "rejected" && b.state !== "failed"))
|
|
491
|
+
: b.snapshot_id && (b.state === "verified" || b.state === "built" || (!isBuilding(b) && b.state !== "rejected" && b.state !== "failed"))
|
|
491
492
|
? nexts([sh `promigence run --snapshot ${b.snapshot_id} --count 2 --cap 1 -- CMD`, "fork it and run a command in every sandbox"], [sh `promigence snapshot inspect ${b.snapshot_id}`, "the manifest: provenance and verification"])
|
|
492
493
|
: b.state === "running"
|
|
493
494
|
? nexts([sh `promigence snapshot status ${b.build_id} --wait`, "keep waiting; the build is untouched"])
|
|
@@ -643,7 +644,27 @@ function refuseRepoCredentials(ctx) {
|
|
|
643
644
|
checkRepoUrl(withoutPin(context), "--context");
|
|
644
645
|
}
|
|
645
646
|
}
|
|
646
|
-
function
|
|
647
|
+
async function packedContext(ctx, dir, path) {
|
|
648
|
+
const listed = listContext(dir, { dockerfile: path });
|
|
649
|
+
if (bool(ctx, "--list-files")) {
|
|
650
|
+
return emitUploadList(ctx, {
|
|
651
|
+
kind: "dockerfile", root: dir, paths: listed.paths, files: listed.files, bytes: listed.bytes,
|
|
652
|
+
secrets: listed.secrets, notes: listed.notes, ignoreFile: listed.ignoreFile,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (listed.notes.length) {
|
|
656
|
+
await write(process.stderr, ctx.mode === "json"
|
|
657
|
+
? jsonLine(process.stderr, { hint: "context", notes: listed.notes })
|
|
658
|
+
: listed.notes.map((n) => fieldForTerminal(process.stderr, ` note: ${n}`)).join("\n") + "\n");
|
|
659
|
+
}
|
|
660
|
+
if (listed.secrets.length && !bool(ctx, "--allow-secrets"))
|
|
661
|
+
throw credentialRefusal(listed.secrets, CREDENTIAL_REMEDY.dockerfile);
|
|
662
|
+
const secretsAllowed = await confirmOverride(ctx, {
|
|
663
|
+
flag: "--allow-secrets", what: "files that look like credentials", items: listed.secrets,
|
|
664
|
+
});
|
|
665
|
+
return { context: packContext(dir, { dockerfile: path, allowSecrets: true }), secretsAllowed };
|
|
666
|
+
}
|
|
667
|
+
async function dockerfileOpts(ctx) {
|
|
647
668
|
const df = required(ctx, "--dockerfile", "Pass --dockerfile ./Dockerfile.");
|
|
648
669
|
const context = str(ctx, "--context");
|
|
649
670
|
if (context !== undefined && CONTEXT_GIT_RE.test(context.trim())) {
|
|
@@ -667,11 +688,17 @@ function dockerfileOpts(ctx) {
|
|
|
667
688
|
if (!existsSync(df)) {
|
|
668
689
|
throw usageError(`no Dockerfile at ${JSON.stringify(df)}`, "Pass --dockerfile with the path to your Dockerfile, or --context URL@SHA to build one from a repository.");
|
|
669
690
|
}
|
|
670
|
-
|
|
691
|
+
const packed = await packedContext(ctx, dir, path);
|
|
692
|
+
return { context: packed.context, path, ...(packed.secretsAllowed.length ? { secretsAllowed: packed.secretsAllowed } : {}) };
|
|
671
693
|
}
|
|
672
|
-
function devcontainerCreate(ctx, plan) {
|
|
694
|
+
async function devcontainerCreate(ctx, plan) {
|
|
673
695
|
let source;
|
|
674
696
|
let label;
|
|
697
|
+
let secretsAllowed = [];
|
|
698
|
+
const localEnvBaked = await confirmOverride(ctx, {
|
|
699
|
+
flag: "--devcontainer-local-env", what: "values from your environment, baked into the snapshot",
|
|
700
|
+
items: plan.localEnvBaked ?? [],
|
|
701
|
+
});
|
|
675
702
|
if (plan.source.kind === "image") {
|
|
676
703
|
source = { image: plan.source.image };
|
|
677
704
|
label = plan.source.image;
|
|
@@ -681,7 +708,9 @@ function devcontainerCreate(ctx, plan) {
|
|
|
681
708
|
throw usageError(`the devcontainer names a Dockerfile that is not here: ${plan.source.dockerfile}`, "Check `build.dockerfile` and `build.context` in the devcontainer.json, relative to the file itself.");
|
|
682
709
|
}
|
|
683
710
|
const path = contextRelative(plan.source.context, plan.source.dockerfile);
|
|
684
|
-
|
|
711
|
+
const packed = await packedContext(ctx, plan.source.context, path);
|
|
712
|
+
source = { dockerfile: { context: packed.context, path } };
|
|
713
|
+
secretsAllowed = packed.secretsAllowed;
|
|
685
714
|
label = `${path} in ${plan.source.context}`;
|
|
686
715
|
}
|
|
687
716
|
const repo = str(ctx, "--repo");
|
|
@@ -693,7 +722,8 @@ function devcontainerCreate(ctx, plan) {
|
|
|
693
722
|
throw usageError(`--commit must be a full 40-character sha, got ${JSON.stringify(commit)}`, "A branch is not a pin: pass --commit $(git rev-parse HEAD).");
|
|
694
723
|
}
|
|
695
724
|
const setup = planSetup(plan, { repo, commit, userSetup: list(ctx, "--setup") });
|
|
696
|
-
const
|
|
725
|
+
const env = snapshotEnv(plan);
|
|
726
|
+
const opts = { ...source, ...(setup.length ? { setup } : {}), ...(env ? { env } : {}) };
|
|
697
727
|
const verify = str(ctx, "--verify");
|
|
698
728
|
if (verify !== undefined)
|
|
699
729
|
opts.verify = { cmd: wrapCommand(plan, verify) };
|
|
@@ -709,6 +739,8 @@ function devcontainerCreate(ctx, plan) {
|
|
|
709
739
|
devcontainer: json,
|
|
710
740
|
...(repo === undefined ? {} : { repo, commit }),
|
|
711
741
|
...(tier === undefined ? {} : { requested_tier: tier }),
|
|
742
|
+
...(secretsAllowed.length ? { secret_files_allowed: secretsAllowed } : {}),
|
|
743
|
+
...(localEnvBaked.length ? { local_env_baked: localEnvBaked } : {}),
|
|
712
744
|
};
|
|
713
745
|
const lines = planLines(plan, setup);
|
|
714
746
|
if (repo !== undefined && commit !== undefined) {
|
|
@@ -739,7 +771,7 @@ const snapshotCreate = async (ctx) => {
|
|
|
739
771
|
({ opts, label, submitted, tier } = gh);
|
|
740
772
|
}
|
|
741
773
|
else if (plan !== undefined) {
|
|
742
|
-
const built = devcontainerCreate(ctx, plan);
|
|
774
|
+
const built = await devcontainerCreate(ctx, plan);
|
|
743
775
|
opts = built.opts;
|
|
744
776
|
label = built.label;
|
|
745
777
|
submitted = built.submitted;
|
|
@@ -767,11 +799,14 @@ const snapshotCreate = async (ctx) => {
|
|
|
767
799
|
submitted = { build_price: null, image };
|
|
768
800
|
}
|
|
769
801
|
else {
|
|
770
|
-
const dockerfile = dockerfileOpts(ctx);
|
|
802
|
+
const { secretsAllowed, ...dockerfile } = await dockerfileOpts(ctx);
|
|
771
803
|
source = { dockerfile };
|
|
772
804
|
const where = dockerfile.context.startsWith("tar;") ? (str(ctx, "--context") ?? dirname(str(ctx, "--dockerfile") ?? ".")) : dockerfile.context;
|
|
773
805
|
label = `${dockerfile.path ?? "Dockerfile"} in ${where}`;
|
|
774
|
-
submitted = {
|
|
806
|
+
submitted = {
|
|
807
|
+
build_price: null, dockerfile: { path: dockerfile.path ?? "Dockerfile", context: where },
|
|
808
|
+
...(secretsAllowed?.length ? { secret_files_allowed: secretsAllowed } : {}),
|
|
809
|
+
};
|
|
775
810
|
}
|
|
776
811
|
const verify = str(ctx, "--verify");
|
|
777
812
|
const start = str(ctx, "--start");
|
|
@@ -863,7 +898,7 @@ const snapshotCreate = async (ctx) => {
|
|
|
863
898
|
.map((s) => ` also ${s.repo}@${s.commit.slice(0, 7)} -> ${s.path ?? "beside the workspace, under its own name"}`)
|
|
864
899
|
.join("\n") + "\n");
|
|
865
900
|
}
|
|
866
|
-
if (started.deduped || started.state === "verified" || (started.snapshot_id && !isBuilding(started))) {
|
|
901
|
+
if (started.deduped || started.state === "verified" || started.state === "built" || (started.snapshot_id && !isBuilding(started))) {
|
|
867
902
|
return emitBuild(ctx, started, submitted);
|
|
868
903
|
}
|
|
869
904
|
const joined = started.joined === true;
|
|
@@ -1575,7 +1610,7 @@ async function runImageSnapshot(ctx, c) {
|
|
|
1575
1610
|
const buildOpts = { image, allowBuild: true };
|
|
1576
1611
|
const snapshotId = await imageSnapshot(c, buildOpts, {
|
|
1577
1612
|
onBuild: async (b) => {
|
|
1578
|
-
const free = b.deduped === true || b.joined === true || b.idempotent_replay === true || b.state === "verified";
|
|
1613
|
+
const free = b.deduped === true || b.joined === true || b.idempotent_replay === true || b.state === "verified" || b.state === "built";
|
|
1579
1614
|
charged = free ? 0 : (typeof b.build_price === "number" ? b.build_price : price);
|
|
1580
1615
|
buildId = b.build_id;
|
|
1581
1616
|
await write(process.stderr, ctx.mode === "json"
|
|
@@ -2306,7 +2341,7 @@ const snapshotVerify = async (ctx) => {
|
|
|
2306
2341
|
return emit(ctx.mode, env, lines, exit, ctx.pointer);
|
|
2307
2342
|
};
|
|
2308
2343
|
function localTar(args, what) {
|
|
2309
|
-
const r = spawnSync("tar", args, { encoding: "utf8" });
|
|
2344
|
+
const r = spawnSync("tar", args, { encoding: "utf8", env: { ...process.env, COPYFILE_DISABLE: "1" } });
|
|
2310
2345
|
if (r.error && r.error.code === "ENOENT") {
|
|
2311
2346
|
throw usageError("`tar` is not on PATH, and a directory transfer needs one locally", "Install tar (macOS and every Linux ship one), or move the files one at a time without --recursive.");
|
|
2312
2347
|
}
|
|
@@ -2328,13 +2363,14 @@ const filesPut = async (ctx) => {
|
|
|
2328
2363
|
const { sid, src, dst } = filesArgs(ctx);
|
|
2329
2364
|
if (!dst.startsWith("/"))
|
|
2330
2365
|
throw usageError(`DST ${dst} must be absolute inside the sandbox`, "Example: /tmp/fixture.json.");
|
|
2331
|
-
const r = await withLocalFile(src, (source) => client(ctx).sandboxes.files.putFrom(sid, dst, source));
|
|
2366
|
+
const r = await withLocalFile(src, (source) => client(ctx).sandboxes.files.putFrom(sid, dst, source, source.mode === undefined ? {} : { mode: source.mode }));
|
|
2332
2367
|
const next = nexts([sh `promigence exec --sandbox ${sid} -- ls -l ${dst}`, "see it in the sandbox"]);
|
|
2333
2368
|
return emit(ctx.mode, okEnvelope(ctx.cmd.name, { sandbox_id: sid, src, dst, bytes: r.bytes, chunks: r.chunks, mode: r.mode }, next), [`${src} -> ${sid}:${dst} (${bytes(r.bytes)}${r.chunks > 1 ? `, ${r.chunks} chunks` : ""})`], 0, ctx.pointer);
|
|
2334
2369
|
};
|
|
2335
2370
|
async function withLocalFile(path, fn) {
|
|
2336
2371
|
let fd;
|
|
2337
2372
|
let size;
|
|
2373
|
+
let mode;
|
|
2338
2374
|
try {
|
|
2339
2375
|
fd = openSync(path, "r");
|
|
2340
2376
|
const st = fstatSync(fd);
|
|
@@ -2343,6 +2379,7 @@ async function withLocalFile(path, fn) {
|
|
|
2343
2379
|
throw Object.assign(new Error("it is a directory"), { code: "EISDIR" });
|
|
2344
2380
|
}
|
|
2345
2381
|
size = st.isFile() ? st.size : -1;
|
|
2382
|
+
mode = st.isFile() && process.platform !== "win32" ? st.mode & 0o777 : undefined;
|
|
2346
2383
|
}
|
|
2347
2384
|
catch (e) {
|
|
2348
2385
|
const code = e.code;
|
|
@@ -2358,6 +2395,7 @@ async function withLocalFile(path, fn) {
|
|
|
2358
2395
|
const buf = Buffer.alloc(FILE_PUT_CHUNK_BYTES);
|
|
2359
2396
|
return await fn({
|
|
2360
2397
|
size,
|
|
2398
|
+
mode,
|
|
2361
2399
|
read: (offset, len) => {
|
|
2362
2400
|
const want = Math.min(len, buf.length);
|
|
2363
2401
|
let got = 0;
|
|
@@ -2375,6 +2413,9 @@ async function withLocalFile(path, fn) {
|
|
|
2375
2413
|
closeSync(fd);
|
|
2376
2414
|
}
|
|
2377
2415
|
}
|
|
2416
|
+
export function filesPutTarArgs(out, parent, base, noMetadata = localTarNoMetadataArgs()) {
|
|
2417
|
+
return ["-c", "-f", out, ...noMetadata, "-C", parent, base];
|
|
2418
|
+
}
|
|
2378
2419
|
const filesPutDir = async (ctx) => {
|
|
2379
2420
|
const { sid, src, dst } = filesArgs(ctx);
|
|
2380
2421
|
if (!dst.startsWith("/"))
|
|
@@ -2384,7 +2425,7 @@ const filesPutDir = async (ctx) => {
|
|
|
2384
2425
|
const tmp = join(mkdtempSync(join(tmpdir(), "promigence-put-")), "dir.tar");
|
|
2385
2426
|
const parent = dirname(resolve(src));
|
|
2386
2427
|
const base = basename(resolve(src));
|
|
2387
|
-
localTar(
|
|
2428
|
+
localTar(filesPutTarArgs(tmp, parent, base), `tarring ${src}`);
|
|
2388
2429
|
const bytesIn = statSync(tmp).size;
|
|
2389
2430
|
const c = client(ctx);
|
|
2390
2431
|
const scratch = `/tmp/.promigence-upload/cli-${Date.now().toString(36)}-${randomBytes(6).toString("hex")}.tar`;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { PromigenceError } from "./sdk/index.js";
|
|
2
|
+
import { bool, nexts } from "./commands.js";
|
|
3
|
+
import { emit, okEnvelope, write } from "./io.js";
|
|
4
|
+
import { headless, openBrowser } from "./login.js";
|
|
5
|
+
import { jsonLine } from "./term.js";
|
|
6
|
+
const NAMES = { github: "GitHub", gitlab: "GitLab" };
|
|
7
|
+
const CONNECTED = new Set(["connected", "updated", "already_connected"]);
|
|
8
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
+
export async function consoleTicket(mint) {
|
|
10
|
+
try {
|
|
11
|
+
return await mint();
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (e instanceof PromigenceError && e.code === "not_found")
|
|
15
|
+
return null;
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function endLine(host, r) {
|
|
20
|
+
const a = r.account ?? `the ${NAMES[host]} account`;
|
|
21
|
+
switch (r.state) {
|
|
22
|
+
case "connected":
|
|
23
|
+
return `connected: ${a}`;
|
|
24
|
+
case "updated":
|
|
25
|
+
return `already connected: ${a} (repository access updated)`;
|
|
26
|
+
case "already_connected":
|
|
27
|
+
return `already connected: ${a}`;
|
|
28
|
+
default:
|
|
29
|
+
return `${r.state}: ${r.message}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function fixFor(host, r) {
|
|
33
|
+
const again = `\`promigence ${host} connect\``;
|
|
34
|
+
const n = NAMES[host];
|
|
35
|
+
switch (r.state) {
|
|
36
|
+
case "wrong_user":
|
|
37
|
+
return `The page was opened in a browser signed in to another Promigence account, so nothing was started. Run ${again} again and open the page where you are signed in to the console as yourself.`;
|
|
38
|
+
case "declined":
|
|
39
|
+
return `The connection was declined, so nothing was connected. Run ${again} again to start over.`;
|
|
40
|
+
case "other_org":
|
|
41
|
+
return `That ${n} account is connected to another Promigence organization. Disconnect it there first (\`promigence ${host} disconnect\` with that organization's key), then run ${again} again.`;
|
|
42
|
+
case "needs_admin":
|
|
43
|
+
return `Connect it signed in to ${n} as an owner (admin) of that account, or ask one of its owners to; then run ${again} again.`;
|
|
44
|
+
case "not_installer":
|
|
45
|
+
return `The ${n} user who installed the App on that account can connect it now; an owner of the account can 15 minutes after the install. Then run ${again} again.`;
|
|
46
|
+
default:
|
|
47
|
+
return `Run ${again} again. If it fails the same way, write to support@promigence.ai with the time it happened.`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function consoleConnect(ctx, host, o) {
|
|
51
|
+
const n = NAMES[host];
|
|
52
|
+
const url = o.ticket.connect_url;
|
|
53
|
+
const opened = bool(ctx, "--no-open") || headless() ? false : openBrowser(url);
|
|
54
|
+
const noWait = bool(ctx, "--no-wait");
|
|
55
|
+
if (ctx.mode === "json") {
|
|
56
|
+
await write(process.stderr, jsonLine(process.stderr, {
|
|
57
|
+
hint: "connect_url", connect_url: url, request_id: o.ticket.request_id, expires_at: o.ticket.expires_at, opened,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await write(process.stderr, [
|
|
62
|
+
opened
|
|
63
|
+
? `Opening the Promigence console to connect ${n}…`
|
|
64
|
+
: `Open this page in a browser where you are signed in to the Promigence console (any device works):`,
|
|
65
|
+
` ${opened ? "If it did not open: " : ""}${url}`,
|
|
66
|
+
host === "github"
|
|
67
|
+
? " It starts the connection on GitHub, where you choose the account, and all repositories or only the ones you pick."
|
|
68
|
+
: " It starts the connection on GitLab, where you approve read-only access.",
|
|
69
|
+
" The page works once, within 5 minutes, and only signed in to the console as you, an owner of this organization: keep it to yourself.",
|
|
70
|
+
...(noWait ? [] : [` Waiting up to ${o.waitText} (Ctrl-C stops waiting; the connection still finishes in the browser).`]),
|
|
71
|
+
].join("\n") + "\n");
|
|
72
|
+
}
|
|
73
|
+
if (noWait) {
|
|
74
|
+
const next = nexts([`promigence ${host} status`, "once it is done: the connection"]);
|
|
75
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
76
|
+
connect_url: url, request_id: o.ticket.request_id, expires_at: o.ticket.expires_at, opened, waited: false, connected: [],
|
|
77
|
+
}, next), [`connect page: ${url}`, ` next: ${next[0].cmd}`], 0, ctx.pointer);
|
|
78
|
+
}
|
|
79
|
+
const t0 = Date.now();
|
|
80
|
+
let said = "";
|
|
81
|
+
for (;;) {
|
|
82
|
+
await sleep(Math.min(o.pollMs, Math.max(0, o.bound - (Date.now() - t0))));
|
|
83
|
+
const r = await o.poll();
|
|
84
|
+
if (r.state !== "pending")
|
|
85
|
+
return finish(ctx, host, r, { url, opened, waitedMs: Date.now() - t0, ...o });
|
|
86
|
+
if (r.message !== said && ctx.mode !== "json") {
|
|
87
|
+
said = r.message;
|
|
88
|
+
await write(process.stderr, ` ${r.message}\n`);
|
|
89
|
+
}
|
|
90
|
+
if (Date.now() - t0 >= o.bound) {
|
|
91
|
+
throw new PromigenceError({
|
|
92
|
+
code: "timeout",
|
|
93
|
+
message: `no ${n} connection finished within ${o.waitText}`,
|
|
94
|
+
fix: `Finish it in the browser if it is still open, then \`promigence ${host} status\`. Otherwise run \`promigence ${host} connect\` again for a new page (each works once, within 5 minutes).`,
|
|
95
|
+
exitCode: 11,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function finish(ctx, host, r, o) {
|
|
101
|
+
const n = NAMES[host];
|
|
102
|
+
if (CONNECTED.has(r.state)) {
|
|
103
|
+
const connected = await o.connections(r);
|
|
104
|
+
const next = nexts(...o.okNext);
|
|
105
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
106
|
+
connect_url: o.url, opened: o.opened, waited: true, waited_ms: o.waitedMs, state: r.state, request: r, connected,
|
|
107
|
+
}, next), [endLine(host, r), ` ${r.message}`, ` next: ${next[0].cmd}`], 0, ctx.pointer);
|
|
108
|
+
}
|
|
109
|
+
if (r.state === "pending_provider_approval") {
|
|
110
|
+
const next = nexts([`promigence ${host} connect`, `once an owner of the account approved it on ${n}`]);
|
|
111
|
+
const line = `waiting for an owner of ${r.account ?? "that account"} to approve it on ${n}; run \`promigence ${host} connect\` again after`;
|
|
112
|
+
return emit(ctx.mode, okEnvelope(ctx.cmd.name, {
|
|
113
|
+
connect_url: o.url, opened: o.opened, waited: true, waited_ms: o.waitedMs, state: r.state, request: r, connected: [],
|
|
114
|
+
}, next), [line, ` ${r.message}`], 0, ctx.pointer);
|
|
115
|
+
}
|
|
116
|
+
if (r.state === "expired") {
|
|
117
|
+
throw new PromigenceError({
|
|
118
|
+
code: "timeout",
|
|
119
|
+
message: r.message,
|
|
120
|
+
fix: `Nothing was connected. Run \`promigence ${host} connect\` again and finish it in the browser: the page works within 5 minutes, the connection within 15.`,
|
|
121
|
+
exitCode: 11,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
throw new PromigenceError({ code: "not_connected", message: `${r.state}: ${r.message}`, fix: fixFor(host, r), exitCode: 7 });
|
|
125
|
+
}
|
package/dist/devcontainer.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export interface Plan {
|
|
|
68
68
|
tierReason: string | undefined;
|
|
69
69
|
ignored: IgnoredProperty[];
|
|
70
70
|
notes: string[];
|
|
71
|
+
localEnvBaked?: string[];
|
|
71
72
|
}
|
|
72
73
|
export interface TranslateOptions {
|
|
73
74
|
workspaceRoot: string;
|
|
@@ -82,6 +83,7 @@ export declare function sizeGib(v: unknown, field: string, file: string): number
|
|
|
82
83
|
export declare function lifecycleSteps(v: unknown, field: string, ctx: SubstitutionContext, file: string): LifecycleStep[];
|
|
83
84
|
export declare function shellQuote(s: string): string;
|
|
84
85
|
export declare function wrapCommand(plan: Plan, command: string): string;
|
|
86
|
+
export declare function snapshotEnv(plan: Plan): Record<string, string> | undefined;
|
|
85
87
|
export interface SetupOptions {
|
|
86
88
|
repo?: string | undefined;
|
|
87
89
|
commit?: string | undefined;
|
package/dist/devcontainer.js
CHANGED
|
@@ -270,8 +270,8 @@ export function substituteString(input, ctx) {
|
|
|
270
270
|
if (!ctx.allowContainerEnv) {
|
|
271
271
|
throw refuse("container_env_scope", `\${containerEnv:…} in ${ctx.property}: the spec defines it for remoteEnv only`, "Move the value into remoteEnv, or write it out literally.", ctx.file);
|
|
272
272
|
}
|
|
273
|
-
const [key
|
|
274
|
-
return ctx.containerEnv[key] ??
|
|
273
|
+
const [key] = splitDefault(arg ?? "");
|
|
274
|
+
return ctx.containerEnv[key] ?? whole;
|
|
275
275
|
}
|
|
276
276
|
default: return whole;
|
|
277
277
|
}
|
|
@@ -368,7 +368,7 @@ export function translate(loaded, opts) {
|
|
|
368
368
|
const remoteEnv = stringMap(raw["remoteEnv"], "remoteEnv", file, (s, p) => substituteString(s, ctxFor(p, containerEnv, true)));
|
|
369
369
|
const env = { ...containerEnv, ...remoteEnv };
|
|
370
370
|
if (Object.keys(remoteEnv).length) {
|
|
371
|
-
notes.push("remoteEnv is applied the same way as containerEnv: both
|
|
371
|
+
notes.push("remoteEnv is applied the same way as containerEnv: both are set for every command, terminal and SSH session in the snapshot's sandboxes");
|
|
372
372
|
}
|
|
373
373
|
let source;
|
|
374
374
|
if (opts.imageOverride === undefined) {
|
|
@@ -468,6 +468,7 @@ export function translate(loaded, opts) {
|
|
|
468
468
|
tierReason,
|
|
469
469
|
ignored,
|
|
470
470
|
notes,
|
|
471
|
+
...(readFromHost.size > 0 ? { localEnvBaked: [...readFromHost].sort() } : {}),
|
|
471
472
|
};
|
|
472
473
|
}
|
|
473
474
|
function refuseFeatures(raw, file) {
|
|
@@ -675,20 +676,14 @@ export function shellQuote(s) {
|
|
|
675
676
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
676
677
|
}
|
|
677
678
|
export function wrapCommand(plan, command) {
|
|
678
|
-
const
|
|
679
|
-
const inner = [
|
|
680
|
-
`cd ${shellQuote(plan.workspaceFolder)}`,
|
|
681
|
-
...(exports.length ? [`export ${exports.join(" ")}`] : []),
|
|
682
|
-
command,
|
|
683
|
-
].join(" && ");
|
|
679
|
+
const inner = [`cd ${shellQuote(plan.workspaceFolder)}`, command].join(" && ");
|
|
684
680
|
return plan.user === undefined || plan.user === "root" ? inner : `su ${plan.user} -c ${shellQuote(inner)}`;
|
|
685
681
|
}
|
|
682
|
+
export function snapshotEnv(plan) {
|
|
683
|
+
return Object.keys(plan.env).length ? { ...plan.env } : undefined;
|
|
684
|
+
}
|
|
686
685
|
export function planSetup(plan, opts = {}) {
|
|
687
686
|
const steps = [];
|
|
688
|
-
const envLines = Object.entries(plan.env).map(([k, v]) => shellQuote(`${k}=${v}`));
|
|
689
|
-
if (envLines.length) {
|
|
690
|
-
steps.push(`printf '%s\\n' ${envLines.join(" ")} >> /etc/environment`);
|
|
691
|
-
}
|
|
692
687
|
if (opts.repo && opts.commit) {
|
|
693
688
|
const ws = shellQuote(plan.workspaceFolder);
|
|
694
689
|
const clone = [
|
package/dist/dirsnap.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { closeSync, fstatSync, lstatSync, mkdtempSync, openSync, readSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { closeSync, fstatSync, lstatSync, mkdtempSync, openSync, readSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
6
|
-
import { DEFAULT_SNAPSHOT, FILE_PUT_CHUNK_BYTES, PromigenceError } from "./sdk/index.js";
|
|
5
|
+
import { basename, join, resolve, sep } from "node:path";
|
|
6
|
+
import { DEFAULT_SNAPSHOT, FILE_PUT_CHUNK_BYTES, PromigenceError, secretLike } from "./sdk/index.js";
|
|
7
7
|
import { bool, client, list, nexts, str, waitBound } from "./commands.js";
|
|
8
8
|
import { emit, okEnvelope, parseDuration, usageError, write } from "./io.js";
|
|
9
9
|
import { jsonLine, sh } from "./term.js";
|
|
10
|
+
import { CREDENTIAL_REMEDY, confirmOverride, credentialRefusal, emitUploadList, localTarNoMetadataArgs } from "./uploadguard.js";
|
|
10
11
|
const BASE_WORKSPACE = "/work";
|
|
11
12
|
export const DIR_SANDBOX_TIMEOUT_DEFAULT = "30m";
|
|
12
13
|
const STEP_TIMEOUT_MS = 30 * 60_000;
|
|
13
14
|
const STEP_OUTPUT_BYTES = 256 * 1024;
|
|
14
15
|
const FAILED_TAIL_LINES = 20;
|
|
15
16
|
const PROGRESS_FROM_BYTES = 8 * 1024 * 1024;
|
|
16
|
-
const SECRETS_NAMED = 10;
|
|
17
17
|
const INTERRUPT_START_WAIT_MS = 15_000;
|
|
18
18
|
const INTERRUPTED = {
|
|
19
19
|
ended: () => "The build sandbox was ended.",
|
|
@@ -28,29 +28,7 @@ const NOT_WITH_DIR = [
|
|
|
28
28
|
"--devcontainer", "--start", "--cpu-class", "--grade", "--holdout", "--grade-score", "--grade-timeout",
|
|
29
29
|
"--grade-output", "--tier",
|
|
30
30
|
];
|
|
31
|
-
|
|
32
|
-
const SECRET_NAMES = [
|
|
33
|
-
/^\.env$/i, /^\.env\..+$/i, /\.env$/i, /^\.envrc$/i, /^\.dev\.vars$/i,
|
|
34
|
-
/\.(pem|key|p12|pfx|jks|keystore)$/i,
|
|
35
|
-
/^id_(rsa|ed25519|ecdsa|dsa)(?!.*\.pub$)/i,
|
|
36
|
-
/^\.npmrc$/i, /^\.pypirc$/i, /^\.netrc$/i, /^\.git-credentials$/i,
|
|
37
|
-
/^credentials$/i, /^credentials\.json$/i, /credentials.*\.json$/i, /^service-account.*\.json$/i,
|
|
38
|
-
/\.tfstate$/i, /\.tfstate\..+$/i, /\.tfvars(\.json)?$/i, /^kubeconfig$/i,
|
|
39
|
-
/^\.htpasswd$/i, /^secrets\.ya?ml$/i,
|
|
40
|
-
];
|
|
41
|
-
const SECRET_DIRS = new Set([".aws", ".ssh", ".gnupg"]);
|
|
42
|
-
const SECRET_PATHS = [/(^|\/)\.docker\/config\.json$/i, /(^|\/)\.kube\/config$/i];
|
|
43
|
-
export function secretLike(path) {
|
|
44
|
-
const parts = path.split("/").filter(Boolean);
|
|
45
|
-
const name = parts[parts.length - 1] ?? "";
|
|
46
|
-
if (parts.slice(0, -1).some((d) => SECRET_DIRS.has(d)))
|
|
47
|
-
return true;
|
|
48
|
-
if (SECRET_PATHS.some((re) => re.test(path)))
|
|
49
|
-
return true;
|
|
50
|
-
if (ENV_TEMPLATE.test(name))
|
|
51
|
-
return false;
|
|
52
|
-
return SECRET_NAMES.some((re) => re.test(name));
|
|
53
|
-
}
|
|
31
|
+
export { secretLike };
|
|
54
32
|
function git(dir, args) {
|
|
55
33
|
const r = spawnSync("git", ["-C", dir, ...args], { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 });
|
|
56
34
|
if (r.error && r.error.code === "ENOENT")
|
|
@@ -70,12 +48,38 @@ export function listTreeFiles(dir, opts = {}) {
|
|
|
70
48
|
throw usageError(`git ls-files failed in ${dir}: ${tracked.err.trim()}`, "Check the repository with `git status`.");
|
|
71
49
|
const seen = new Set();
|
|
72
50
|
const files = [];
|
|
51
|
+
const outside = [];
|
|
73
52
|
let missing = 0;
|
|
74
53
|
let bytes = 0;
|
|
54
|
+
const realRoot = realpathSync(dir);
|
|
55
|
+
const linkedDirs = new Map();
|
|
56
|
+
const throughLink = (p) => {
|
|
57
|
+
const parts = p.split("/");
|
|
58
|
+
for (let i = 1; i < parts.length; i++) {
|
|
59
|
+
const d = parts.slice(0, i).join("/");
|
|
60
|
+
let linked = linkedDirs.get(d);
|
|
61
|
+
if (linked === undefined) {
|
|
62
|
+
try {
|
|
63
|
+
linked = !lstatSync(join(dir, d)).isDirectory();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
linked = false;
|
|
67
|
+
}
|
|
68
|
+
linkedDirs.set(d, linked);
|
|
69
|
+
}
|
|
70
|
+
if (linked)
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
75
|
const take = (p) => {
|
|
76
76
|
if (!p || seen.has(p))
|
|
77
77
|
return false;
|
|
78
78
|
seen.add(p);
|
|
79
|
+
if (throughLink(p)) {
|
|
80
|
+
outside.push(p);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
79
83
|
let st;
|
|
80
84
|
try {
|
|
81
85
|
st = lstatSync(join(dir, p));
|
|
@@ -86,6 +90,20 @@ export function listTreeFiles(dir, opts = {}) {
|
|
|
86
90
|
}
|
|
87
91
|
if (st.isDirectory())
|
|
88
92
|
return false;
|
|
93
|
+
if (!st.isSymbolicLink()) {
|
|
94
|
+
let real;
|
|
95
|
+
try {
|
|
96
|
+
real = realpathSync(join(dir, p));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
missing += 1;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
103
|
+
outside.push(p);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
89
107
|
files.push(p);
|
|
90
108
|
if (st.isFile())
|
|
91
109
|
bytes += st.size;
|
|
@@ -103,15 +121,13 @@ export function listTreeFiles(dir, opts = {}) {
|
|
|
103
121
|
untracked += 1;
|
|
104
122
|
}
|
|
105
123
|
files.sort();
|
|
106
|
-
|
|
124
|
+
outside.sort();
|
|
125
|
+
return { files, missing, untracked, bytes, outside };
|
|
107
126
|
}
|
|
108
127
|
export function refuseSecrets(files, allow) {
|
|
109
128
|
const hits = files.filter(secretLike);
|
|
110
|
-
if (hits.length && !allow)
|
|
111
|
-
|
|
112
|
-
const more = hits.length > SECRETS_NAMED ? ` and ${hits.length - SECRETS_NAMED} more` : "";
|
|
113
|
-
throw usageError(`${hits.length} file${hits.length === 1 ? " looks" : "s look"} like ${hits.length === 1 ? "a credential" : "credentials"} and ${hits.length === 1 ? "was" : "were"} not uploaded: ${named}${more}; nothing was created`, "A snapshot keeps every file it holds, and every sandbox started from it has them. Remove them from git (`git rm --cached FILE` and add them to .gitignore), pass the values as secrets (`promigence secrets set`), or pass --allow-secrets if they are not real credentials.");
|
|
114
|
-
}
|
|
129
|
+
if (hits.length && !allow)
|
|
130
|
+
throw credentialRefusal(hits, CREDENTIAL_REMEDY.dir);
|
|
115
131
|
return hits;
|
|
116
132
|
}
|
|
117
133
|
export function human(n) {
|
|
@@ -130,7 +146,7 @@ export function packFiles(dir, files, out) {
|
|
|
130
146
|
const listPath = `${out}.list`;
|
|
131
147
|
writeFileSync(listPath, files.join("\0") + (files.length ? "\0" : ""));
|
|
132
148
|
try {
|
|
133
|
-
const r = spawnSync("tar", ["-c", "-f", out, "-C", dir, "--no-recursion", "--null", "-T", listPath], {
|
|
149
|
+
const r = spawnSync("tar", ["-c", "-f", out, ...localTarNoMetadataArgs(), "-C", dir, "--no-recursion", "--null", "-T", listPath], {
|
|
134
150
|
encoding: "utf8", env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
135
151
|
});
|
|
136
152
|
if (r.error && r.error.code === "ENOENT") {
|
|
@@ -172,7 +188,19 @@ export async function dirSnapshotCreate(ctx) {
|
|
|
172
188
|
if (!listing.files.length) {
|
|
173
189
|
throw usageError(`nothing to upload: git tracks no file in ${dirArg}`, "`git add` the files first, or pass --include-untracked for files git does not ignore.");
|
|
174
190
|
}
|
|
175
|
-
|
|
191
|
+
if (bool(ctx, "--list-files")) {
|
|
192
|
+
return emitUploadList(ctx, {
|
|
193
|
+
kind: "dir", root: dirArg, paths: listing.files, files: listing.files.length, bytes: listing.bytes,
|
|
194
|
+
secrets: listing.files.filter(secretLike),
|
|
195
|
+
notes: listing.outside.length
|
|
196
|
+
? [`Left out ${listing.outside.length} path${listing.outside.length === 1 ? "" : "s"} under a directory that is now a link or outside ${dirArg}: ${listing.outside.slice(0, 10).join(", ")}${listing.outside.length > 10 ? ` and ${listing.outside.length - 10} more` : ""}.`]
|
|
197
|
+
: [],
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const allowed = await confirmOverride(ctx, {
|
|
201
|
+
flag: "--allow-secrets", what: "files that look like credentials",
|
|
202
|
+
items: refuseSecrets(listing.files, bool(ctx, "--allow-secrets")),
|
|
203
|
+
});
|
|
176
204
|
const from = str(ctx, "--from") ?? DEFAULT_SNAPSHOT;
|
|
177
205
|
const alias = str(ctx, "--alias");
|
|
178
206
|
const setup = list(ctx, "--setup");
|
|
@@ -192,7 +220,8 @@ export async function dirSnapshotCreate(ctx) {
|
|
|
192
220
|
await write(process.stderr, ctx.mode === "json"
|
|
193
221
|
? jsonLine(process.stderr, { hint: "price", ...priceFields, unit: "USD" })
|
|
194
222
|
: `price: the sandbox is billed per second while the upload and setup run (~$${perMin.toFixed(4)}/min at tier ${q.tier}, at most $${q.max_cost.toFixed(2)} for its ${Math.round(timeoutMs / 60_000)} min limit); the snapshot itself is not charged\n` +
|
|
195
|
-
` uploading ${listing.files.length} file${listing.files.length === 1 ? "" : "s"} (${human(listing.bytes)}) from ${dirArg}${listing.untracked ? `, ${listing.untracked} untracked` : ""}${listing.missing ? `; ${listing.missing} deleted in the working tree, skipped` : ""}${allowed.length ? `; ${allowed.length} secret-looking, allowed by --allow-secrets` : ""}\n`
|
|
223
|
+
` uploading ${listing.files.length} file${listing.files.length === 1 ? "" : "s"} (${human(listing.bytes)}) from ${dirArg}${listing.untracked ? `, ${listing.untracked} untracked` : ""}${listing.missing ? `; ${listing.missing} deleted in the working tree, skipped` : ""}${allowed.length ? `; ${allowed.length} secret-looking, allowed by --allow-secrets` : ""}\n` +
|
|
224
|
+
(listing.outside.length ? ` skipped ${listing.outside.length} under a directory that is now a link or outside ${dirArg}: ${listing.outside.slice(0, 10).join(", ")}${listing.outside.length > 10 ? " ..." : ""}\n` : ""));
|
|
196
225
|
const scratchDir = mkdtempSync(join(tmpdir(), "promigence-dir-"));
|
|
197
226
|
const tarPath = join(scratchDir, "tree.tar");
|
|
198
227
|
let runId;
|
|
@@ -331,6 +360,8 @@ export async function dirSnapshotCreate(ctx) {
|
|
|
331
360
|
snapshot_id: result.snapshot_id,
|
|
332
361
|
alias: result.alias,
|
|
333
362
|
source: { kind: "dir", dir: dirArg, files: listing.files.length, untracked: listing.untracked, bytes: listing.bytes, packed_bytes: result.packed },
|
|
363
|
+
...(allowed.length ? { secret_files_allowed: allowed } : {}),
|
|
364
|
+
...(listing.outside.length ? { skipped_outside: listing.outside } : {}),
|
|
334
365
|
from,
|
|
335
366
|
workspace: result.workspace,
|
|
336
367
|
setup,
|
package/dist/generated.d.ts
CHANGED