agental 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +536 -103
- package/package.json +6 -1
package/dist/cli.js
CHANGED
|
@@ -46,7 +46,7 @@ import { parseArgs } from "node:util";
|
|
|
46
46
|
|
|
47
47
|
// ../src/cli-build.ts
|
|
48
48
|
var PUBLISHED = true;
|
|
49
|
-
var CLI_VERSION = true ? "0.
|
|
49
|
+
var CLI_VERSION = true ? "0.4.0" : "dev";
|
|
50
50
|
var DEFAULT_API_URL = true ? "https://agental.io" : "http://127.0.0.1:8741";
|
|
51
51
|
var PACKAGE_NAME = "agental";
|
|
52
52
|
var USER_AGENT = `agental-cli/${CLI_VERSION}`;
|
|
@@ -560,13 +560,13 @@ function expiryFrom(given, now = Date.now()) {
|
|
|
560
560
|
}
|
|
561
561
|
return new Date(moment).toISOString();
|
|
562
562
|
}
|
|
563
|
-
async function templateIdsOf(client,
|
|
564
|
-
if (
|
|
565
|
-
return
|
|
563
|
+
async function templateIdsOf(client, words2) {
|
|
564
|
+
if (words2.every((word) => word.startsWith("tpl_"))) {
|
|
565
|
+
return words2;
|
|
566
566
|
}
|
|
567
567
|
const body = await ask2(client, "GET", "/v1/templates");
|
|
568
568
|
const templates = Array.isArray(body?.templates) ? body.templates : [];
|
|
569
|
-
return
|
|
569
|
+
return words2.map((word) => {
|
|
570
570
|
if (word.startsWith("tpl_")) {
|
|
571
571
|
return word;
|
|
572
572
|
}
|
|
@@ -1238,8 +1238,8 @@ async function sshConnect(client, args) {
|
|
|
1238
1238
|
// ../src/cli-onboard.ts
|
|
1239
1239
|
var SKILLS_CLI = "skills@1.7.2";
|
|
1240
1240
|
var READY_MS = 3 * 60 * 1e3;
|
|
1241
|
-
var
|
|
1242
|
-
var
|
|
1241
|
+
var CARD_MS = 15 * 60 * 1e3;
|
|
1242
|
+
var CARD_LOOK_MS = 3e3;
|
|
1243
1243
|
async function api(base, key2, method, pathname, body, headers = {}) {
|
|
1244
1244
|
let response;
|
|
1245
1245
|
try {
|
|
@@ -1436,34 +1436,34 @@ async function atTheLimit(base, key2, term, message) {
|
|
|
1436
1436
|
process.exitCode = 1;
|
|
1437
1437
|
return null;
|
|
1438
1438
|
}
|
|
1439
|
-
async function
|
|
1439
|
+
async function saveCard(base, key2, term, message) {
|
|
1440
1440
|
const policy = await api(base, key2, "GET", "/v1/policy");
|
|
1441
|
-
const
|
|
1441
|
+
const page = String(policy.body["consoleUrl"] ?? "");
|
|
1442
1442
|
term.say(`
|
|
1443
1443
|
${bold("No new sandbox:")} ${message}`);
|
|
1444
1444
|
if (!term.interactive) {
|
|
1445
|
-
term.say(`
|
|
1445
|
+
term.say(` Save a card: ${page}
|
|
1446
1446
|
`);
|
|
1447
1447
|
return false;
|
|
1448
1448
|
}
|
|
1449
|
-
if (!await yes(`
|
|
1450
|
-
term.say(`
|
|
1449
|
+
if (!await yes(` Save a card in your browser now? Nothing is charged, and the free plan gives $10 of credit. [Y/n] `)) {
|
|
1450
|
+
term.say(` Save one when you like: ${page}
|
|
1451
1451
|
`);
|
|
1452
1452
|
return false;
|
|
1453
1453
|
}
|
|
1454
|
-
openBrowser(
|
|
1455
|
-
term.say(` ${dim("If it did not open:")} ${
|
|
1456
|
-
term.say(` ${dim("waiting for your
|
|
1457
|
-
const until = Date.now() +
|
|
1454
|
+
openBrowser(page);
|
|
1455
|
+
term.say(` ${dim("If it did not open:")} ${page}`);
|
|
1456
|
+
term.say(` ${dim("waiting for your card to be saved\u2026 (Ctrl-C to stop)")}`);
|
|
1457
|
+
const until = Date.now() + CARD_MS;
|
|
1458
1458
|
while (Date.now() < until) {
|
|
1459
|
-
await sleep(
|
|
1459
|
+
await sleep(CARD_LOOK_MS);
|
|
1460
1460
|
const billing2 = await api(base, key2, "GET", "/v1/billing").catch(() => null);
|
|
1461
1461
|
if (billing2?.status === 200 && billing2.body["needsPlan"] === false) {
|
|
1462
|
-
term.say(` ${done("your
|
|
1462
|
+
term.say(` ${done("your card is saved")}`);
|
|
1463
1463
|
return true;
|
|
1464
1464
|
}
|
|
1465
1465
|
}
|
|
1466
|
-
term.say(` No
|
|
1466
|
+
term.say(` No card saved in ${CARD_MS / 6e4} minutes. Run the command again once it is.
|
|
1467
1467
|
`);
|
|
1468
1468
|
return false;
|
|
1469
1469
|
}
|
|
@@ -1483,7 +1483,7 @@ async function newSandbox(base, key2, term) {
|
|
|
1483
1483
|
return await atTheLimit(base, key2, term, errorOf(made).message);
|
|
1484
1484
|
}
|
|
1485
1485
|
if (made.status === 402 && errorOf(made).code === "billing_required") {
|
|
1486
|
-
if (await
|
|
1486
|
+
if (await saveCard(base, key2, term, errorOf(made).message)) {
|
|
1487
1487
|
return await newSandbox(base, key2, term);
|
|
1488
1488
|
}
|
|
1489
1489
|
process.exitCode = 1;
|
|
@@ -1654,9 +1654,9 @@ async function ask4(client, method, pathname, body) {
|
|
|
1654
1654
|
function pad(text, width) {
|
|
1655
1655
|
return text.length >= width ? `${text.slice(0, width - 1)}\u2026 ` : text.padEnd(width + 1);
|
|
1656
1656
|
}
|
|
1657
|
-
function print2(json, body,
|
|
1657
|
+
function print2(json, body, words2) {
|
|
1658
1658
|
process.stdout.write(json ? `${JSON.stringify(body, null, 2)}
|
|
1659
|
-
` :
|
|
1659
|
+
` : words2);
|
|
1660
1660
|
}
|
|
1661
1661
|
async function organizations(client) {
|
|
1662
1662
|
const listed = (await ask4(client, "GET", "/v1/organizations"))["organizations"];
|
|
@@ -1734,8 +1734,8 @@ agental org use ${organization.id} signs this command in to it.
|
|
|
1734
1734
|
async function actingTeam(client) {
|
|
1735
1735
|
const { organizations: list3, acting } = await organizations(client);
|
|
1736
1736
|
const team = list3.find((organization) => organization.id === acting);
|
|
1737
|
-
if (!team
|
|
1738
|
-
throw new Error("This command's key acts in
|
|
1737
|
+
if (!team) {
|
|
1738
|
+
throw new Error("This command's key acts in no organization you are in. agental org use <organization> signs it in to one.");
|
|
1739
1739
|
}
|
|
1740
1740
|
return team;
|
|
1741
1741
|
}
|
|
@@ -1850,11 +1850,11 @@ function runOf(run2) {
|
|
|
1850
1850
|
const code = run2.exitCode === null ? "" : ` (exit ${run2.exitCode})`;
|
|
1851
1851
|
return `${run2.outcome}${code} ${at2(run2.dueAt)}`;
|
|
1852
1852
|
}
|
|
1853
|
-
function commandLine(
|
|
1854
|
-
if (
|
|
1855
|
-
return
|
|
1853
|
+
function commandLine(words2) {
|
|
1854
|
+
if (words2.length === 1) {
|
|
1855
|
+
return words2[0] ?? "";
|
|
1856
1856
|
}
|
|
1857
|
-
return
|
|
1857
|
+
return words2.map((word) => /^[A-Za-z0-9_./:=@%+,-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
1858
1858
|
}
|
|
1859
1859
|
function timeoutMsOf(text) {
|
|
1860
1860
|
const match = /^([1-9][0-9]*)(s|m|h|d)$/.exec(text.trim());
|
|
@@ -1884,6 +1884,9 @@ function fieldsOf(options) {
|
|
|
1884
1884
|
if (options["keep-memory"] && options["disk-only"]) {
|
|
1885
1885
|
throw new Error("Give --keep-memory or --disk-only, not both.");
|
|
1886
1886
|
}
|
|
1887
|
+
if (options["delete-after"] && options["keep-sandbox"]) {
|
|
1888
|
+
throw new Error("Give --delete-after or --keep-sandbox, not both.");
|
|
1889
|
+
}
|
|
1887
1890
|
if (options.overlap !== void 0 && !OVERLAPS.includes(options.overlap)) {
|
|
1888
1891
|
throw new Error(`--overlap is skip, replace or parallel, not ${options.overlap}.`);
|
|
1889
1892
|
}
|
|
@@ -1900,7 +1903,8 @@ function fieldsOf(options) {
|
|
|
1900
1903
|
...options.timeout === void 0 ? {} : { timeoutMs: timeoutMsOf(options.timeout) },
|
|
1901
1904
|
...options.overlap === void 0 ? {} : { overlap: options.overlap },
|
|
1902
1905
|
...ifStopped === void 0 ? {} : { ifStopped },
|
|
1903
|
-
...options["keep-memory"] ? { keepMemory: true } : options["disk-only"] ? { keepMemory: false } : {}
|
|
1906
|
+
...options["keep-memory"] ? { keepMemory: true } : options["disk-only"] ? { keepMemory: false } : {},
|
|
1907
|
+
...options["delete-after"] ? { deleteAfter: true } : options["keep-sandbox"] ? { deleteAfter: false } : {}
|
|
1904
1908
|
};
|
|
1905
1909
|
}
|
|
1906
1910
|
function print3(schedule2, json) {
|
|
@@ -1915,7 +1919,7 @@ function print3(schedule2, json) {
|
|
|
1915
1919
|
["command", schedule2.command],
|
|
1916
1920
|
["when", whenOf(schedule2)],
|
|
1917
1921
|
["if stopped", ifStoppedOf(schedule2)],
|
|
1918
|
-
["overlap", schedule2.overlap],
|
|
1922
|
+
...schedule2.at ? [["after", schedule2.deleteAfter ? "deletes the sandbox, if the command exits 0" : "keeps the sandbox"]] : [["overlap", schedule2.overlap]],
|
|
1919
1923
|
["timeout", `${Math.round(schedule2.timeoutMs / 1e3)}s`],
|
|
1920
1924
|
["next", schedule2.enabled ? schedule2.nextRunAt === null ? afterOf(schedule2) : at2(schedule2.nextRunAt) : schedule2.pausedReason ? `paused: ${schedule2.pausedReason}` : "paused"],
|
|
1921
1925
|
["last run", runOf(schedule2.lastRun)]
|
|
@@ -2018,7 +2022,7 @@ async function schedule(client, positionals, options) {
|
|
|
2018
2022
|
at2(run2.dueAt),
|
|
2019
2023
|
run2.outcome,
|
|
2020
2024
|
run2.exitCode === null ? "-" : String(run2.exitCode),
|
|
2021
|
-
run2.startedSandbox ? run2.stoppedSandbox === null ? "started, left running" : `started, stopped (${run2.stoppedSandbox})` : "-",
|
|
2025
|
+
run2.deletedSandbox ? run2.startedSandbox ? "started, deleted" : "deleted" : run2.startedSandbox ? run2.stoppedSandbox === null ? "started, left running" : `started, stopped (${run2.stoppedSandbox})` : "-",
|
|
2022
2026
|
run2.commandId ?? (run2.reason ?? "-")
|
|
2023
2027
|
])
|
|
2024
2028
|
])
|
|
@@ -2043,7 +2047,7 @@ async function schedule(client, positionals, options) {
|
|
|
2043
2047
|
} else {
|
|
2044
2048
|
const changes = { ...fieldsOf(options), ...command.length > 0 ? { command: commandLine(command) } : {} };
|
|
2045
2049
|
if (Object.keys(changes).length === 0) {
|
|
2046
|
-
throw new Error("schedule update needs something to change: --every, --cron, --at, --tz, --name, --timeout, --overlap, --if-stopped, --keep-memory or --disk-only, or a new command after --.");
|
|
2050
|
+
throw new Error("schedule update needs something to change: --every, --cron, --at, --tz, --name, --timeout, --overlap, --if-stopped, --keep-memory or --disk-only, --delete-after or --keep-sandbox, or a new command after --.");
|
|
2047
2051
|
}
|
|
2048
2052
|
print3(await ask5(client, "PATCH", path4, changes), options.json);
|
|
2049
2053
|
}
|
|
@@ -2066,7 +2070,8 @@ var LIFECYCLE_EVENTS = [
|
|
|
2066
2070
|
"sandbox.auto_stop_set",
|
|
2067
2071
|
"sandbox.labels_changed",
|
|
2068
2072
|
"sandbox.scheduled_run",
|
|
2069
|
-
"sandbox.schedule_paused"
|
|
2073
|
+
"sandbox.schedule_paused",
|
|
2074
|
+
"sandbox.low_cpu"
|
|
2070
2075
|
];
|
|
2071
2076
|
var WEBHOOK_EVENTS = [...LIFECYCLE_EVENTS, "prompt.created"];
|
|
2072
2077
|
var EMAIL_EVENTS = ["email.received", "email.delivered", "email.bounced"];
|
|
@@ -2367,12 +2372,19 @@ var MICROS_PER_DOLLAR = 1e6;
|
|
|
2367
2372
|
function plan(id, dollarsPerMonth, sizes, runningUnits, freeBurstUnits) {
|
|
2368
2373
|
return { id, dollarsPerMonth, includedMicros: dollarsPerMonth * MICROS_PER_DOLLAR, sizes, runningUnits, freeBurstUnits };
|
|
2369
2374
|
}
|
|
2370
|
-
var
|
|
2371
|
-
|
|
2375
|
+
var FREE_PLAN = plan("free", 0, ["tiny", "small"], 20, 20);
|
|
2376
|
+
var PAID_PLANS = [
|
|
2372
2377
|
plan("plan100", 100, ["tiny", "small", "medium"], 100, 100),
|
|
2373
2378
|
plan("plan500", 500, MACHINE_SIZE_NAMES, 600, 200),
|
|
2374
2379
|
plan("plan2000", 2e3, MACHINE_SIZE_NAMES, 2e3, 400)
|
|
2375
2380
|
];
|
|
2381
|
+
var PLANS = [FREE_PLAN, ...PAID_PLANS];
|
|
2382
|
+
var FREE = {
|
|
2383
|
+
/** The credit given once, in millionths of a US dollar. It does not expire. */
|
|
2384
|
+
creditMicros: 10 * MICROS_PER_DOLLAR,
|
|
2385
|
+
/** Small sandboxes an account on the free plan runs and starts at once before it has paid for anything. */
|
|
2386
|
+
unpaidUnits: 2
|
|
2387
|
+
};
|
|
2376
2388
|
var TOP_UP_DOLLARS = { min: 15, max: 1e5 };
|
|
2377
2389
|
var TOP_UP_BONUS = { fromDollars: 200, fromPercent: 2, toDollars: 1e4, toPercent: 25 };
|
|
2378
2390
|
function topUpBonusPercent(dollars) {
|
|
@@ -2394,6 +2406,100 @@ function topUpByDollars(dollars) {
|
|
|
2394
2406
|
}
|
|
2395
2407
|
var TOP_UPS = [20, 50, 100, 250, 500, 1e3, 5e3, 1e4].map((dollars) => topUpByDollars(dollars));
|
|
2396
2408
|
|
|
2409
|
+
// ../src/sandbox-where.ts
|
|
2410
|
+
var TERMS = /* @__PURE__ */ new Set(["status", "size", "region", "template", "tag", "archived", "memory", "outbound", "stops", "made-by", "activity"]);
|
|
2411
|
+
function words(text) {
|
|
2412
|
+
const found = [];
|
|
2413
|
+
for (const match of text.matchAll(/(?:[^\s"]+|"[^"]*")+/g)) {
|
|
2414
|
+
found.push(match[0].replace(/"/g, ""));
|
|
2415
|
+
}
|
|
2416
|
+
return found;
|
|
2417
|
+
}
|
|
2418
|
+
function whereQuery(text, query = new URLSearchParams()) {
|
|
2419
|
+
const plain2 = [];
|
|
2420
|
+
for (const word of words(text)) {
|
|
2421
|
+
const time = /^(made|stopped)([<>])(.+)$/.exec(word);
|
|
2422
|
+
if (time) {
|
|
2423
|
+
const [, what, side, at4] = time;
|
|
2424
|
+
if (what === "made") {
|
|
2425
|
+
query.append(side === ">" ? "createdAfter" : "createdBefore", at4);
|
|
2426
|
+
} else if (side === "<") {
|
|
2427
|
+
query.append("stoppedBefore", at4);
|
|
2428
|
+
} else {
|
|
2429
|
+
throw new Error(`${word}: stopped takes < only, for sandboxes stopped before a time.`);
|
|
2430
|
+
}
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
const pair = /^(-?)([a-z0-9][a-z0-9_.-]*):(.*)$/.exec(word);
|
|
2434
|
+
if (!pair) {
|
|
2435
|
+
plain2.push(word);
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
const [, not, key2, value] = pair;
|
|
2439
|
+
if (value === "") {
|
|
2440
|
+
throw new Error(`${word}: say a value after the colon.`);
|
|
2441
|
+
}
|
|
2442
|
+
if (!TERMS.has(key2)) {
|
|
2443
|
+
query.append(`${not ? "notMetadata" : "metadata"}[${key2}]`, value);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (not && key2 !== "tag" && key2 !== "activity") {
|
|
2447
|
+
throw new Error(`${word}: only tags, activity and metadata keys can be left out with -.`);
|
|
2448
|
+
}
|
|
2449
|
+
switch (key2) {
|
|
2450
|
+
case "status":
|
|
2451
|
+
for (const each of value.split(",")) {
|
|
2452
|
+
if (each === "starting") {
|
|
2453
|
+
query.append("status", "creating");
|
|
2454
|
+
query.append("status", "restoring");
|
|
2455
|
+
} else if (each === "failed") {
|
|
2456
|
+
query.append("status", "error");
|
|
2457
|
+
} else if (each === "archived") {
|
|
2458
|
+
query.append("status", "stopped");
|
|
2459
|
+
query.set("archived", "true");
|
|
2460
|
+
} else {
|
|
2461
|
+
query.append("status", each);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
break;
|
|
2465
|
+
case "template":
|
|
2466
|
+
query.append("templateId", value);
|
|
2467
|
+
break;
|
|
2468
|
+
case "tag":
|
|
2469
|
+
for (const each of value.split(",")) {
|
|
2470
|
+
query.append(not ? "notTag" : "tag", each);
|
|
2471
|
+
}
|
|
2472
|
+
break;
|
|
2473
|
+
case "memory":
|
|
2474
|
+
if (value !== "kept" && value !== "lost") {
|
|
2475
|
+
throw new Error(`${word}: memory is kept or lost.`);
|
|
2476
|
+
}
|
|
2477
|
+
query.set("memoryKept", value === "kept" ? "true" : "false");
|
|
2478
|
+
break;
|
|
2479
|
+
case "made-by":
|
|
2480
|
+
query.append("createdBy", value);
|
|
2481
|
+
break;
|
|
2482
|
+
case "activity":
|
|
2483
|
+
for (const each of value.split(",")) {
|
|
2484
|
+
query.append(not ? "notActivity" : "activity", each);
|
|
2485
|
+
}
|
|
2486
|
+
break;
|
|
2487
|
+
case "archived":
|
|
2488
|
+
query.set("archived", value);
|
|
2489
|
+
break;
|
|
2490
|
+
case "stops":
|
|
2491
|
+
query.set("stops", value);
|
|
2492
|
+
break;
|
|
2493
|
+
default:
|
|
2494
|
+
query.append(key2, value);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
if (plain2.length > 0) {
|
|
2498
|
+
query.set("q", plain2.join(" "));
|
|
2499
|
+
}
|
|
2500
|
+
return query;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2397
2503
|
// ../src/cli.ts
|
|
2398
2504
|
var DEFAULT_URL = DEFAULT_API_URL;
|
|
2399
2505
|
var SSH_COMMANDS = {
|
|
@@ -2506,7 +2612,7 @@ async function main() {
|
|
|
2506
2612
|
expires: { type: "string" },
|
|
2507
2613
|
"keep-streams": { type: "boolean" },
|
|
2508
2614
|
"keep-memory": { type: "boolean" },
|
|
2509
|
-
// agental schedule: when it runs, in what time zone, how long a run may take,
|
|
2615
|
+
// agental schedule: when it runs, in what time zone, how long a run may take, what it does when it is stopped or the last still runs, and whether a one-off deletes its sandbox after.
|
|
2510
2616
|
every: { type: "string" },
|
|
2511
2617
|
cron: { type: "string" },
|
|
2512
2618
|
tz: { type: "string" },
|
|
@@ -2514,12 +2620,16 @@ async function main() {
|
|
|
2514
2620
|
overlap: { type: "string" },
|
|
2515
2621
|
"if-stopped": { type: "string" },
|
|
2516
2622
|
"disk-only": { type: "boolean" },
|
|
2623
|
+
"delete-after": { type: "boolean" },
|
|
2624
|
+
"keep-sandbox": { type: "boolean" },
|
|
2517
2625
|
ttl: { type: "string" },
|
|
2518
2626
|
"max-burst-fee": { type: "string" },
|
|
2519
2627
|
in: { type: "string" },
|
|
2520
2628
|
never: { type: "boolean", default: false },
|
|
2521
2629
|
outbound: { type: "string" },
|
|
2522
2630
|
allow: { type: "string", multiple: true },
|
|
2631
|
+
// Where a sandbox's web traffic leaves, as agental create takes it: box, or residential:<country>[:sticky|rotating].
|
|
2632
|
+
egress: { type: "string" },
|
|
2523
2633
|
"ssh-key": { type: "string", multiple: true },
|
|
2524
2634
|
none: { type: "boolean", default: false },
|
|
2525
2635
|
host: { type: "string", multiple: true },
|
|
@@ -2545,6 +2655,11 @@ async function main() {
|
|
|
2545
2655
|
// Metadata as <key>=<value>, given again for each key.
|
|
2546
2656
|
meta: { type: "string", multiple: true },
|
|
2547
2657
|
anywhere: { type: "boolean", default: false },
|
|
2658
|
+
where: { type: "string" },
|
|
2659
|
+
facets: { type: "string" },
|
|
2660
|
+
yes: { type: "boolean", default: false },
|
|
2661
|
+
confirm: { type: "string" },
|
|
2662
|
+
sort: { type: "string" },
|
|
2548
2663
|
role: { type: "string" },
|
|
2549
2664
|
"revoke-keys": { type: "boolean", default: false },
|
|
2550
2665
|
help: { type: "boolean", short: "h", default: false }
|
|
@@ -2560,7 +2675,13 @@ async function main() {
|
|
|
2560
2675
|
await create({ ...values, template: once(values.template, "--template") });
|
|
2561
2676
|
return;
|
|
2562
2677
|
case "list":
|
|
2563
|
-
await list2(values.json, labelsFilter(values));
|
|
2678
|
+
await list2(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.sort, values.limit);
|
|
2679
|
+
return;
|
|
2680
|
+
case "jobs":
|
|
2681
|
+
await jobs(positionals.slice(1), values.json);
|
|
2682
|
+
return;
|
|
2683
|
+
case "summary":
|
|
2684
|
+
await summary(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.facets);
|
|
2564
2685
|
return;
|
|
2565
2686
|
case "tags":
|
|
2566
2687
|
await labels("tags", "sandbox", positionals.slice(1), values.json);
|
|
@@ -2575,15 +2696,30 @@ async function main() {
|
|
|
2575
2696
|
await exec(positionals.slice(1));
|
|
2576
2697
|
return;
|
|
2577
2698
|
case "delete":
|
|
2699
|
+
if (values.where !== void 0 && positionals[1] !== void 0) {
|
|
2700
|
+
throw new Error("delete takes a sandbox's ID or --where, not both");
|
|
2701
|
+
}
|
|
2702
|
+
if (values.where !== void 0) {
|
|
2703
|
+
await bulk("delete", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json, values.confirm);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2578
2706
|
await remove2(positionals[1]);
|
|
2579
2707
|
return;
|
|
2580
2708
|
case "start":
|
|
2709
|
+
if (values.where !== void 0) {
|
|
2710
|
+
await bulk("start", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2581
2713
|
await lifecycle("start", positionals[1], maxBurstFeeHeaders(values["max-burst-fee"]));
|
|
2582
2714
|
return;
|
|
2583
2715
|
case "auto-stop":
|
|
2584
2716
|
await autoStop(positionals[1], values.in, values.at, values.never === true, values.json);
|
|
2585
2717
|
return;
|
|
2586
2718
|
case "stop":
|
|
2719
|
+
if (values.where !== void 0) {
|
|
2720
|
+
await bulk("stop", whereQuery(values.where, labelsFilter(values)), values["keep-memory"] === true ? { keepMemory: true } : {}, values.yes === true, values.json);
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2587
2723
|
await lifecycle("stop", positionals[1], {}, values["keep-memory"] === true ? { keepMemory: true } : void 0);
|
|
2588
2724
|
return;
|
|
2589
2725
|
case "recover":
|
|
@@ -2641,6 +2777,9 @@ async function main() {
|
|
|
2641
2777
|
case "network":
|
|
2642
2778
|
await network(positionals.slice(1), values);
|
|
2643
2779
|
return;
|
|
2780
|
+
case "egress":
|
|
2781
|
+
await egress(positionals.slice(1), values);
|
|
2782
|
+
return;
|
|
2644
2783
|
case "templates":
|
|
2645
2784
|
await listTemplates(values.json);
|
|
2646
2785
|
return;
|
|
@@ -2746,6 +2885,7 @@ async function create(values) {
|
|
|
2746
2885
|
);
|
|
2747
2886
|
}
|
|
2748
2887
|
const allow = values.allow === void 0 ? void 0 : listedIn(values.allow);
|
|
2888
|
+
const egressAsked = values.egress === void 0 ? void 0 : egressFrom(values.egress.split(":"), "--egress takes box, or residential:<country>, as residential:de or residential:us:rotating.");
|
|
2749
2889
|
const sshKeys2 = values["ssh-key"] === void 0 ? void 0 : publicKeysFrom(values["ssh-key"]);
|
|
2750
2890
|
const box = boxNamed(values.box);
|
|
2751
2891
|
const templateId = values.template === void 0 ? void 0 : (await templateNamed(values.template)).id;
|
|
@@ -2759,6 +2899,7 @@ async function create(values) {
|
|
|
2759
2899
|
...templateId ? { templateId } : {},
|
|
2760
2900
|
...regionsOption(values.region),
|
|
2761
2901
|
...allow ? { network: { allow } } : {},
|
|
2902
|
+
...egressAsked ? { egress: egressAsked } : {},
|
|
2762
2903
|
...ttlSeconds === void 0 ? {} : { ttlSeconds },
|
|
2763
2904
|
...sshKeys2 === void 0 ? {} : { sshKeys: sshKeys2 },
|
|
2764
2905
|
...given
|
|
@@ -2888,20 +3029,39 @@ async function regions(asJson) {
|
|
|
2888
3029
|
}
|
|
2889
3030
|
process.stdout.write("* where a sandbox is made when no region is named\n");
|
|
2890
3031
|
}
|
|
2891
|
-
async function list2(asJson, filter) {
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3032
|
+
async function list2(asJson, filter, sort, limit) {
|
|
3033
|
+
const sandboxes = [];
|
|
3034
|
+
let cursor = null;
|
|
3035
|
+
do {
|
|
3036
|
+
const query = new URLSearchParams(filter);
|
|
3037
|
+
if (sort !== void 0) {
|
|
3038
|
+
query.set("sort", sort);
|
|
3039
|
+
}
|
|
3040
|
+
query.set("limit", limit ?? "200");
|
|
3041
|
+
if (cursor !== null) {
|
|
3042
|
+
query.set("cursor", cursor);
|
|
3043
|
+
}
|
|
3044
|
+
const response = await request(`/v1/sandboxes?${query}`, { method: "GET" });
|
|
3045
|
+
const body = await readBody(response);
|
|
3046
|
+
if (!response.ok) {
|
|
3047
|
+
fail(response.status, body);
|
|
3048
|
+
}
|
|
3049
|
+
sandboxes.push(...readSandboxes(body));
|
|
3050
|
+
const next = body && typeof body === "object" ? body["nextCursor"] : null;
|
|
3051
|
+
cursor = typeof next === "string" ? next : null;
|
|
3052
|
+
if (limit !== void 0 && cursor !== null) {
|
|
3053
|
+
process.stderr.write(`There are more. --cursor is not taken here: drop --limit to list them all, or ask GET /v1/sandboxes?${query.toString().replace(/&?cursor=[^&]*/, "")}&cursor=${cursor}
|
|
3054
|
+
`);
|
|
3055
|
+
cursor = null;
|
|
3056
|
+
}
|
|
3057
|
+
} while (cursor !== null);
|
|
2898
3058
|
if (asJson) {
|
|
2899
3059
|
process.stdout.write(`${JSON.stringify(sandboxes, null, 2)}
|
|
2900
3060
|
`);
|
|
2901
3061
|
return;
|
|
2902
3062
|
}
|
|
2903
3063
|
if (sandboxes.length === 0) {
|
|
2904
|
-
process.stdout.write(filter.size > 0 ? "No sandboxes
|
|
3064
|
+
process.stdout.write(filter.size > 0 ? "No sandboxes match.\n" : "No sandboxes.\n");
|
|
2905
3065
|
return;
|
|
2906
3066
|
}
|
|
2907
3067
|
const names = sandboxes.some((sandbox) => typeof sandbox["templateId"] === "string") ? await templateNames() : /* @__PURE__ */ new Map();
|
|
@@ -2927,6 +3087,165 @@ async function list2(asJson, filter) {
|
|
|
2927
3087
|
`);
|
|
2928
3088
|
}
|
|
2929
3089
|
}
|
|
3090
|
+
function dollarsOf(micros) {
|
|
3091
|
+
return `$${(micros / 1e6).toFixed(4)}`;
|
|
3092
|
+
}
|
|
3093
|
+
async function summary(asJson, filter, facets) {
|
|
3094
|
+
const query = new URLSearchParams(filter);
|
|
3095
|
+
if (facets !== void 0) {
|
|
3096
|
+
query.set("facets", facets);
|
|
3097
|
+
}
|
|
3098
|
+
const response = await request(`/v1/sandbox-summary${query.size > 0 ? `?${query}` : ""}`, { method: "GET" });
|
|
3099
|
+
const body = await readBody(response);
|
|
3100
|
+
if (!response.ok) {
|
|
3101
|
+
fail(response.status, body);
|
|
3102
|
+
}
|
|
3103
|
+
if (asJson) {
|
|
3104
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}
|
|
3105
|
+
`);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const read2 = body;
|
|
3109
|
+
const line = (name, money2) => `${pad2(name)} ${String(money2.count).padStart(6)} ${dollarsOf(money2.microsPerHour).padStart(11)} an hour ${dollarsOf(money2.microsPerHour * 24).padStart(11)} a day ${dollarsOf(money2.spentMicros).padStart(11)} today
|
|
3110
|
+
`;
|
|
3111
|
+
for (const [state2, money2] of Object.entries(read2.byState)) {
|
|
3112
|
+
if (money2.count > 0) {
|
|
3113
|
+
process.stdout.write(line(state2, money2));
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
process.stdout.write(line("total", read2.total));
|
|
3117
|
+
const running = read2.byState["running"];
|
|
3118
|
+
if (running && running.count > 0) {
|
|
3119
|
+
process.stdout.write(`Stopped, the ${running.count} running would cost ${dollarsOf(read2.ifStopped.microsPerHour)} an hour, ${dollarsOf(running.microsPerHour - read2.ifStopped.microsPerHour)} less.
|
|
3120
|
+
`);
|
|
3121
|
+
}
|
|
3122
|
+
process.stdout.write("A day is at the rate of now. Today is the UTC day, and spent is for these sandboxes alone: the account's month is on Billing.\n");
|
|
3123
|
+
for (const [field, facet] of Object.entries(read2.facets ?? {})) {
|
|
3124
|
+
process.stdout.write(`
|
|
3125
|
+
${field}
|
|
3126
|
+
`);
|
|
3127
|
+
for (const each of facet.values) {
|
|
3128
|
+
process.stdout.write(` ${pad2(each.value)} ${String(each.count).padStart(6)} ${dollarsOf(each.microsPerHour).padStart(11)} an hour
|
|
3129
|
+
`);
|
|
3130
|
+
}
|
|
3131
|
+
if (facet.more > 0) {
|
|
3132
|
+
process.stdout.write(` and ${facet.more} more
|
|
3133
|
+
`);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function filterOf(query) {
|
|
3138
|
+
const filter = {};
|
|
3139
|
+
for (const [name, value] of query) {
|
|
3140
|
+
filter[name] = [...filter[name] ?? [], value];
|
|
3141
|
+
}
|
|
3142
|
+
return filter;
|
|
3143
|
+
}
|
|
3144
|
+
async function typedCount(count) {
|
|
3145
|
+
const { createInterface: createInterface2 } = await import("node:readline/promises");
|
|
3146
|
+
const lines = createInterface2({ input: process.stdin, output: process.stderr });
|
|
3147
|
+
try {
|
|
3148
|
+
return (await lines.question(`They cannot be brought back. Type ${count} to delete them: `)).trim();
|
|
3149
|
+
} finally {
|
|
3150
|
+
lines.close();
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
async function confirmed(question) {
|
|
3154
|
+
if (!process.stdin.isTTY) {
|
|
3155
|
+
return false;
|
|
3156
|
+
}
|
|
3157
|
+
const { createInterface: createInterface2 } = await import("node:readline/promises");
|
|
3158
|
+
const lines = createInterface2({ input: process.stdin, output: process.stderr });
|
|
3159
|
+
try {
|
|
3160
|
+
return /^y(es)?$/i.test((await lines.question(`${question} [y/N] `)).trim());
|
|
3161
|
+
} finally {
|
|
3162
|
+
lines.close();
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
async function bulk(action, filter, options, yes2, asJson, confirm) {
|
|
3166
|
+
const previewed = await request("/v1/sandbox-jobs/preview", { method: "POST", body: JSON.stringify({ action, options, select: { filter: filterOf(filter) } }) });
|
|
3167
|
+
const preview = await readBody(previewed);
|
|
3168
|
+
if (!previewed.ok) {
|
|
3169
|
+
fail(previewed.status, preview);
|
|
3170
|
+
}
|
|
3171
|
+
const read2 = preview;
|
|
3172
|
+
process.stderr.write(`${action === "stop" ? "Stop" : action === "start" ? "Start" : "Delete"} ${read2.count} ${read2.count === 1 ? "sandbox" : "sandboxes"}: ${read2.sample.map((each) => each.name).join(", ")}${read2.count > read2.sample.length ? `, and ${read2.count - read2.sample.length} more` : ""}.
|
|
3173
|
+
`);
|
|
3174
|
+
process.stderr.write(`They cost ${dollarsOf(read2.microsPerHour.now)} an hour now, and ${dollarsOf(read2.microsPerHour.after)} an hour after.
|
|
3175
|
+
`);
|
|
3176
|
+
let count = null;
|
|
3177
|
+
if (action === "delete") {
|
|
3178
|
+
const given = confirm ?? (process.stdin.isTTY ? await typedCount(read2.count) : "");
|
|
3179
|
+
if (given !== String(read2.count)) {
|
|
3180
|
+
process.stderr.write(`Nothing was deleted. Type ${read2.count}, or send --confirm ${read2.count}, to delete them.
|
|
3181
|
+
`);
|
|
3182
|
+
process.exitCode = 1;
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
count = read2.count;
|
|
3186
|
+
} else if (!yes2 && !await confirmed("Go on?")) {
|
|
3187
|
+
process.stderr.write("Nothing was done. --yes goes on without asking.\n");
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const made = await request("/v1/sandbox-jobs", { method: "POST", headers: { "Idempotency-Key": `cli-${read2.id}` }, body: JSON.stringify({ previewId: read2.id, ...count === null ? {} : { confirm: count } }) });
|
|
3191
|
+
let job = await readBody(made);
|
|
3192
|
+
if (!made.ok) {
|
|
3193
|
+
fail(made.status, job);
|
|
3194
|
+
}
|
|
3195
|
+
while (job.state === "running") {
|
|
3196
|
+
process.stderr.write(`\r${job.id}: ${job.counts["done"] ?? 0} done, ${job.counts["skipped"] ?? 0} skipped, ${job.counts["failed"] ?? 0} failed of ${job.total}`);
|
|
3197
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3198
|
+
const read3 = await request(`/v1/sandbox-jobs/${job.id}`, { method: "GET" });
|
|
3199
|
+
job = await readBody(read3);
|
|
3200
|
+
if (!read3.ok) {
|
|
3201
|
+
fail(read3.status, job);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
process.stderr.write("\n");
|
|
3205
|
+
if (asJson) {
|
|
3206
|
+
process.stdout.write(`${JSON.stringify(job, null, 2)}
|
|
3207
|
+
`);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
process.stdout.write(`${job.id} ${job.state}: ${job.counts["done"] ?? 0} done, ${job.counts["skipped"] ?? 0} skipped, ${job.counts["failed"] ?? 0} failed, ${job.counts["uncertain"] ?? 0} uncertain, of ${job.total}.
|
|
3211
|
+
`);
|
|
3212
|
+
for (const problem of job.problems) {
|
|
3213
|
+
process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
|
|
3214
|
+
`);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
async function jobs(positionals, asJson) {
|
|
3218
|
+
const [verb, id] = positionals;
|
|
3219
|
+
const path4 = verb === void 0 || verb === "list" ? "/v1/sandbox-jobs" : verb === "show" && id ? `/v1/sandbox-jobs/${encodeURIComponent(id)}` : verb === "cancel" && id ? `/v1/sandbox-jobs/${encodeURIComponent(id)}/cancel` : null;
|
|
3220
|
+
if (path4 === null) {
|
|
3221
|
+
throw new Error("jobs takes list, show <job-id> or cancel <job-id>");
|
|
3222
|
+
}
|
|
3223
|
+
const response = await request(path4, { method: verb === "cancel" ? "POST" : "GET" });
|
|
3224
|
+
const body = await readBody(response);
|
|
3225
|
+
if (!response.ok) {
|
|
3226
|
+
fail(response.status, body);
|
|
3227
|
+
}
|
|
3228
|
+
if (asJson) {
|
|
3229
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}
|
|
3230
|
+
`);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
const listed = verb === void 0 || verb === "list" ? body.jobs ?? [] : [body];
|
|
3234
|
+
if (listed.length === 0) {
|
|
3235
|
+
process.stdout.write("No jobs.\n");
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
for (const job of listed) {
|
|
3239
|
+
process.stdout.write(`${job.id} ${pad2(job.action)} ${pad2(job.state)} ${job.counts["done"] ?? 0}/${job.total} done, ${job.counts["failed"] ?? 0} failed, ${job.counts["skipped"] ?? 0} skipped ${job.createdAt}
|
|
3240
|
+
`);
|
|
3241
|
+
if (listed.length === 1) {
|
|
3242
|
+
for (const problem of job.problems) {
|
|
3243
|
+
process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
|
|
3244
|
+
`);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
2930
3249
|
async function exec(positionals) {
|
|
2931
3250
|
const id = positionals[0];
|
|
2932
3251
|
const command = positionals.slice(1);
|
|
@@ -3371,7 +3690,7 @@ function labelsInWords(body) {
|
|
|
3371
3690
|
return [`tags: ${tags.length > 0 ? tags.join(", ") : "none"}`, `metadata: ${metadata.length > 0 ? metadata.join(", ") : "none"}`, ""].join("\n");
|
|
3372
3691
|
}
|
|
3373
3692
|
async function labels(what, holder, positionals, asJson) {
|
|
3374
|
-
const [named, action, ...
|
|
3693
|
+
const [named, action, ...words2] = positionals;
|
|
3375
3694
|
const command = `${holder === "template" ? "template " : ""}${what}`;
|
|
3376
3695
|
const usage = what === "tags" ? `${command} <${holder}> [add|rm <tag>...]` : `${command} <${holder}> [set <key>=<value>... | unset <key>...]`;
|
|
3377
3696
|
if (!named) {
|
|
@@ -3383,7 +3702,7 @@ async function labels(what, holder, positionals, asJson) {
|
|
|
3383
3702
|
if (action === void 0) {
|
|
3384
3703
|
body = await call2(path4, { method: "GET" });
|
|
3385
3704
|
} else if (what === "tags" && (action === "add" || action === "rm")) {
|
|
3386
|
-
const tags = listedIn(
|
|
3705
|
+
const tags = listedIn(words2).map((tag) => tag.toLowerCase());
|
|
3387
3706
|
if (tags.length === 0) {
|
|
3388
3707
|
throw new Error(`${command} ${action} requires <tag>...`);
|
|
3389
3708
|
}
|
|
@@ -3395,10 +3714,10 @@ async function labels(what, holder, positionals, asJson) {
|
|
|
3395
3714
|
body = await call2(`${path4}/tags/${encodeURIComponent(tag)}`, { method: action === "add" ? "PUT" : "DELETE" });
|
|
3396
3715
|
}
|
|
3397
3716
|
} else if (what === "meta" && (action === "set" || action === "unset")) {
|
|
3398
|
-
if (
|
|
3717
|
+
if (words2.length === 0) {
|
|
3399
3718
|
throw new Error(`${command} ${action} requires ${action === "set" ? "<key>=<value>" : "<key>"}...`);
|
|
3400
3719
|
}
|
|
3401
|
-
const metadata = action === "set" ? metadataGiven(
|
|
3720
|
+
const metadata = action === "set" ? metadataGiven(words2) : Object.fromEntries(words2.map((key2) => [key2, null]));
|
|
3402
3721
|
body = await call2(path4, { method: "PATCH", body: JSON.stringify({ metadata }) });
|
|
3403
3722
|
} else {
|
|
3404
3723
|
throw new Error(`${command} takes ${usage}`);
|
|
@@ -3444,6 +3763,72 @@ async function network(positionals, values) {
|
|
|
3444
3763
|
].join("\n")
|
|
3445
3764
|
);
|
|
3446
3765
|
}
|
|
3766
|
+
function egressFrom(words2, how) {
|
|
3767
|
+
const [via, country, session, ...more] = words2.map((word) => word.trim());
|
|
3768
|
+
if (via === "box" && country === void 0) {
|
|
3769
|
+
return { via: "box" };
|
|
3770
|
+
}
|
|
3771
|
+
if (via !== "residential" || country === void 0 || !/^[a-zA-Z]{2}$/.test(country) || more.length > 0 || session !== void 0 && session !== "sticky" && session !== "rotating") {
|
|
3772
|
+
throw new Error(how);
|
|
3773
|
+
}
|
|
3774
|
+
return { via: "residential", country: country.toLowerCase(), ...session === void 0 ? {} : { session } };
|
|
3775
|
+
}
|
|
3776
|
+
function egressInWords(body) {
|
|
3777
|
+
if (body?.via !== "residential") {
|
|
3778
|
+
return "from its server (box)";
|
|
3779
|
+
}
|
|
3780
|
+
return `residential, from ${String(body.country ?? "").toUpperCase()}, ${body.session === "rotating" ? "a new address each connection" : "one address kept (sticky)"}`;
|
|
3781
|
+
}
|
|
3782
|
+
async function egress(positionals, values) {
|
|
3783
|
+
const [first, ...rest] = positionals;
|
|
3784
|
+
const print4 = (body2, words2) => {
|
|
3785
|
+
process.stdout.write(values.json ? `${JSON.stringify(body2, null, 2)}
|
|
3786
|
+
` : words2);
|
|
3787
|
+
};
|
|
3788
|
+
if (first === void 0 || first === "use-case" || first === "budget") {
|
|
3789
|
+
let body2;
|
|
3790
|
+
if (first === "use-case") {
|
|
3791
|
+
const useCase = rest.join(" ").trim();
|
|
3792
|
+
if (useCase === "") {
|
|
3793
|
+
throw new Error('egress use-case takes what your sandboxes will use residential addresses for, in a few sentences, as agental egress use-case "Our agents test sign-up flows from the countries we sell in."');
|
|
3794
|
+
}
|
|
3795
|
+
body2 = await call2("/v1/egress/use-case", { method: "PUT", body: JSON.stringify({ useCase }) });
|
|
3796
|
+
} else if (first === "budget") {
|
|
3797
|
+
const given = rest[0];
|
|
3798
|
+
const dollars = given === void 0 || given.trim() === "" ? Number.NaN : Number(given.replace(/^\$/, ""));
|
|
3799
|
+
if (rest.length !== 1 || given !== "default" && (!Number.isFinite(dollars) || dollars < 0)) {
|
|
3800
|
+
throw new Error("egress budget takes the most residential traffic may cost in a month, in US dollars, as agental egress budget 20, or default.");
|
|
3801
|
+
}
|
|
3802
|
+
body2 = await call2("/v1/egress/budget", { method: "PUT", body: JSON.stringify({ budgetMicros: given === "default" ? null : Math.round(dollars * 1e6) }) });
|
|
3803
|
+
} else {
|
|
3804
|
+
body2 = await call2("/v1/egress", { method: "GET" });
|
|
3805
|
+
}
|
|
3806
|
+
const account3 = body2 && typeof body2 === "object" ? body2 : {};
|
|
3807
|
+
const number = (name) => typeof account3[name] === "number" ? account3[name] : 0;
|
|
3808
|
+
const missing = Array.isArray(account3["missing"]) ? account3["missing"].map(String) : [];
|
|
3809
|
+
const needs = {
|
|
3810
|
+
plan: "a plan of Fleet ($100 a month) or larger: agental billing",
|
|
3811
|
+
card: "a card saved: agental billing card",
|
|
3812
|
+
use_case: 'what your sandboxes will use it for: agental egress use-case "<what for>"'
|
|
3813
|
+
};
|
|
3814
|
+
const refused = account3["refused"];
|
|
3815
|
+
const lines = !account3["offered"] ? ["Residential egress is not offered here."] : [
|
|
3816
|
+
`residential: ${account3["enabled"] ? "on for this account" : refused ? `refused: ${String(refused.reason ?? "")}` : "not yet"}`,
|
|
3817
|
+
...missing.map((need) => ` needs ${needs[need] ?? need}`),
|
|
3818
|
+
`price: ${money(number("microsPerGb"))} a GB, both directions counted`,
|
|
3819
|
+
`this month: ${money(number("spentMicros"))} of a ${money(number("budgetMicros"))} budget${account3["budgetSet"] ? "" : " (the default)"}`,
|
|
3820
|
+
...account3["blocked"] && account3["blocked"] !== "refused" ? [`blocked now: ${String(account3["blocked"]).replaceAll("_", " ")}`] : []
|
|
3821
|
+
];
|
|
3822
|
+
print4(body2, `${lines.join("\n")}
|
|
3823
|
+
`);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
const path4 = `/v1/sandboxes/${encodeURIComponent(first)}/egress`;
|
|
3827
|
+
const how = `egress takes <sandbox-id>, and then nothing, box, or residential <country> [sticky|rotating], as agental egress ${first} residential de.`;
|
|
3828
|
+
const body = await call2(path4, rest.length === 0 ? { method: "GET" } : { method: "PUT", body: JSON.stringify(egressFrom(rest, how)) });
|
|
3829
|
+
print4(body, `${first}: ${egressInWords(body)}
|
|
3830
|
+
`);
|
|
3831
|
+
}
|
|
3447
3832
|
function templateSshInWords(ssh2) {
|
|
3448
3833
|
const keys = ssh2.keys.length === 1 ? "1 key" : `${ssh2.keys.length} keys`;
|
|
3449
3834
|
return `${ssh2.enabled ? "on" : "off"} as a sandbox is made, ${keys} let in`;
|
|
@@ -3513,7 +3898,6 @@ function showTemplate(each) {
|
|
|
3513
3898
|
// Said only where some are. An API from before variables could be kept out of the sandbox says nothing of them.
|
|
3514
3899
|
...each.hidden && each.hidden.length > 0 ? [`kept out: ${list3(each.hidden)}`] : [],
|
|
3515
3900
|
`files: ${list3(each.files)}`,
|
|
3516
|
-
`repo: ${each.repo ? `${each.repo.name} ${each.repo.url}` : "none"}`,
|
|
3517
3901
|
// An API from before templates held rules says nothing of one.
|
|
3518
3902
|
...each.network ? [`network: ${ruleInWords(each.network)}`] : [],
|
|
3519
3903
|
// An API from before templates gave SSH says nothing of it.
|
|
@@ -3526,10 +3910,10 @@ function showTemplate(each) {
|
|
|
3526
3910
|
}
|
|
3527
3911
|
async function template(positionals, values) {
|
|
3528
3912
|
const [action, first, ...rest] = positionals;
|
|
3529
|
-
const usage = "template takes create, show, rename, delete, start,
|
|
3530
|
-
const print4 = (body,
|
|
3913
|
+
const usage = "template takes create, show, rename, delete, start, network, egress, ssh, ssh-keys, tags, meta, sandboxes, apply, of or put. agental help says how.";
|
|
3914
|
+
const print4 = (body, words2) => {
|
|
3531
3915
|
process.stdout.write(values.json ? `${JSON.stringify(body, null, 2)}
|
|
3532
|
-
` :
|
|
3916
|
+
` : words2);
|
|
3533
3917
|
};
|
|
3534
3918
|
const need = (word, what) => {
|
|
3535
3919
|
if (!word) {
|
|
@@ -3589,20 +3973,6 @@ async function template(positionals, values) {
|
|
|
3589
3973
|
`);
|
|
3590
3974
|
return;
|
|
3591
3975
|
}
|
|
3592
|
-
case "repo": {
|
|
3593
|
-
const found = await templateNamed(need(first, "<template>"));
|
|
3594
|
-
const path4 = `/v1/templates/${encodeURIComponent(found.id)}/repo`;
|
|
3595
|
-
if (values.remove) {
|
|
3596
|
-
await call2(path4, { method: "DELETE" });
|
|
3597
|
-
process.stdout.write(`${found.name} links to no repository
|
|
3598
|
-
`);
|
|
3599
|
-
return;
|
|
3600
|
-
}
|
|
3601
|
-
const repo = rest.length === 0 ? await call2(path4, { method: "GET" }) : await call2(path4, { method: "PUT", body: JSON.stringify({ name: need(rest[0], "<template> <name> <url>"), url: need(rest[1], "<template> <name> <url>") }) });
|
|
3602
|
-
print4(repo, repo ? `repo: ${repo.name} ${repo.url}
|
|
3603
|
-
` : "repo: none\n");
|
|
3604
|
-
return;
|
|
3605
|
-
}
|
|
3606
3976
|
case "network": {
|
|
3607
3977
|
const found = await templateNamed(need(first, "<template>"));
|
|
3608
3978
|
const path4 = `/v1/templates/${encodeURIComponent(found.id)}/network`;
|
|
@@ -3630,6 +4000,15 @@ ${mode === void 0 ? "" : "A sandbox on it takes this when it next starts, or now
|
|
|
3630
4000
|
);
|
|
3631
4001
|
return;
|
|
3632
4002
|
}
|
|
4003
|
+
case "egress": {
|
|
4004
|
+
const found = await templateNamed(need(first, "<template>"));
|
|
4005
|
+
const path4 = `/v1/templates/${encodeURIComponent(found.id)}/egress`;
|
|
4006
|
+
const how = `template egress takes <template>, and then nothing, box, or residential <country> [sticky|rotating], as agental template egress ${first ?? ""} residential de.`;
|
|
4007
|
+
const set = await call2(path4, rest.length === 0 ? { method: "GET" } : { method: "PUT", body: JSON.stringify(egressFrom(rest, how)) });
|
|
4008
|
+
print4(set, `${found.name}: ${egressInWords(set)}${rest.length === 0 ? "" : ", for sandboxes made from it from now on"}
|
|
4009
|
+
`);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
3633
4012
|
case "ssh": {
|
|
3634
4013
|
const found = await templateNamed(need(first, "<template>"));
|
|
3635
4014
|
const path4 = `/v1/templates/${encodeURIComponent(found.id)}/ssh`;
|
|
@@ -3728,19 +4107,19 @@ async function keptOutNow(path4) {
|
|
|
3728
4107
|
}
|
|
3729
4108
|
async function secrets(positionals, values) {
|
|
3730
4109
|
const usage = "secrets requires <sandbox-id>, or --template <template>, and then nothing, get, set, hosts, unset, get-file, put-file or rm-file";
|
|
3731
|
-
const
|
|
4110
|
+
const words2 = [...positionals];
|
|
3732
4111
|
let base;
|
|
3733
4112
|
let sandboxId;
|
|
3734
4113
|
if (values.template !== void 0) {
|
|
3735
4114
|
base = `/v1/templates/${encodeURIComponent((await templateNamed(values.template)).id)}/secrets`;
|
|
3736
4115
|
} else {
|
|
3737
|
-
sandboxId =
|
|
4116
|
+
sandboxId = words2.shift();
|
|
3738
4117
|
if (!sandboxId) {
|
|
3739
4118
|
throw new Error(usage);
|
|
3740
4119
|
}
|
|
3741
4120
|
base = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/secrets`;
|
|
3742
4121
|
}
|
|
3743
|
-
const [action, ...args] =
|
|
4122
|
+
const [action, ...args] = words2;
|
|
3744
4123
|
const fileQuery = (path4) => {
|
|
3745
4124
|
if (!path4) {
|
|
3746
4125
|
throw new Error(`secrets ${action ?? ""} requires <path>`);
|
|
@@ -4015,14 +4394,14 @@ async function ci(positionals, options) {
|
|
|
4015
4394
|
return;
|
|
4016
4395
|
}
|
|
4017
4396
|
if (body.repositories.length === 0) {
|
|
4018
|
-
process.stdout.write(`No repository has a pool yet. Connect GitHub in the console (Settings, GitHub), then ask for runs-on: agental
|
|
4397
|
+
process.stdout.write(`No repository has a pool yet. Connect GitHub in the console (Settings, GitHub), then ask for runs-on: agental (its template's size, medium with none) or agental-<size> in a workflow. Each pool keeps ${body.defaultPoolMax} sandboxes of each size.
|
|
4019
4398
|
`);
|
|
4020
4399
|
return;
|
|
4021
4400
|
}
|
|
4022
4401
|
for (const item of body.repositories) {
|
|
4023
4402
|
const busy = item.sandboxes.filter((sandbox) => sandbox.busy).length;
|
|
4024
4403
|
process.stdout.write(
|
|
4025
|
-
`${item.repository} pool ${item.poolMax ?? `${body.defaultPoolMax} (default)`} template ${item.templateId ?? "runs-on (empty)"} ${item.sandboxes.length} sandboxes, ${busy} busy
|
|
4404
|
+
`${item.repository} pool ${item.poolMax ?? `${body.defaultPoolMax} (default)`} template ${item.templateId ?? "runs-on (empty)"} agental is ${item.size} ${item.sandboxes.length} sandboxes, ${busy} busy
|
|
4026
4405
|
`
|
|
4027
4406
|
);
|
|
4028
4407
|
}
|
|
@@ -4132,7 +4511,7 @@ async function telemetry(positionals, given, asJson) {
|
|
|
4132
4511
|
}
|
|
4133
4512
|
async function billing(positionals, asJson) {
|
|
4134
4513
|
const [what, amount, ...rest] = positionals;
|
|
4135
|
-
const usage = "billing takes nothing, prices, top-up <dollars>, or auto-reload <dollars | off>";
|
|
4514
|
+
const usage = "billing takes nothing, prices, card, top-up <dollars>, or auto-reload <dollars | off>";
|
|
4136
4515
|
if (rest.length > 0 || what !== "top-up" && what !== "auto-reload" && amount !== void 0) {
|
|
4137
4516
|
throw new Error(usage);
|
|
4138
4517
|
}
|
|
@@ -4149,6 +4528,17 @@ async function billing(positionals, asJson) {
|
|
|
4149
4528
|
print4(body, () => describePrices(body));
|
|
4150
4529
|
return;
|
|
4151
4530
|
}
|
|
4531
|
+
case "card": {
|
|
4532
|
+
const body = await asked("/v1/billing/checkout", { method: "POST", body: JSON.stringify({ card: true }) });
|
|
4533
|
+
print4(
|
|
4534
|
+
body,
|
|
4535
|
+
() => body.url ? `Save a card for the free plan at:
|
|
4536
|
+
${body.url}
|
|
4537
|
+
Nothing is charged. Once it is saved, the account is given its credit, once, and may run sandboxes: agental billing shows it within a minute.
|
|
4538
|
+
` : "Nothing to save.\n"
|
|
4539
|
+
);
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
4152
4542
|
case "top-up": {
|
|
4153
4543
|
const dollars = Number(amount);
|
|
4154
4544
|
if (amount === void 0 || !/^\d+$/.test(amount) || dollars <= 0) {
|
|
@@ -4196,19 +4586,27 @@ function dayOf(iso) {
|
|
|
4196
4586
|
}
|
|
4197
4587
|
function describeBilling(body) {
|
|
4198
4588
|
const lines = [];
|
|
4199
|
-
const
|
|
4200
|
-
if (
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
"
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4589
|
+
const free = body.free ?? null;
|
|
4590
|
+
if (free && body.plan) {
|
|
4591
|
+
lines.push(["Plan", "free: nothing a month. What runs is paid from your credits, at list price."]);
|
|
4592
|
+
if (!free.cardSaved) {
|
|
4593
|
+
lines.push(["Card", "none saved. Save one, charged nothing, to run sandboxes and be given the free plan's credit: agental billing card."]);
|
|
4594
|
+
} else if (!free.paid) {
|
|
4595
|
+
lines.push(["At once", `${free.unpaidUnits} small sandboxes until you buy a top-up, then ${(body.plan.runningUnits ?? 20).toLocaleString("en-US")}. A tiny counts half.`]);
|
|
4596
|
+
}
|
|
4597
|
+
lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
|
|
4598
|
+
lines.push(["Credits left", `${money(body.topUpMicros)}, the free credit and top-ups. They do not expire.`]);
|
|
4599
|
+
if ((body.carriedMicros ?? 0) > 0) {
|
|
4600
|
+
lines.push(["Owed", `${money(body.carriedMicros ?? 0)} an earlier month ran past your credits, taken from the next top-up first`]);
|
|
4601
|
+
}
|
|
4602
|
+
if (body.autoReload) {
|
|
4603
|
+
lines.push(["Auto-reload", describeAutoReload(body.autoReload)]);
|
|
4604
|
+
} else if (body.canAutoReload) {
|
|
4605
|
+
lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
|
|
4606
|
+
}
|
|
4607
|
+
} else if (body.plan && (body.status === "active" || body.status === "past_due")) {
|
|
4608
|
+
lines.push(["Plan", `$${body.plan.dollarsPerMonth} a month, ${body.cancelAtPeriodEnd ? "ends" : "renews"} ${dayOf(body.period?.end ?? null)}`]);
|
|
4609
|
+
lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
|
|
4212
4610
|
if ((body.carriedMicros ?? 0) > 0) {
|
|
4213
4611
|
lines.push(["Carried", `${money(body.carriedMicros ?? 0)} an earlier month could not pay, taken from this month's credits first`]);
|
|
4214
4612
|
}
|
|
@@ -4224,15 +4622,18 @@ function describeBilling(body) {
|
|
|
4224
4622
|
lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
|
|
4225
4623
|
}
|
|
4226
4624
|
} else {
|
|
4227
|
-
lines.push(["Plan",
|
|
4625
|
+
lines.push(["Plan", "none. Plans are not required here yet."]);
|
|
4228
4626
|
}
|
|
4229
4627
|
if (!body.canRun && body.reason) {
|
|
4230
|
-
lines.push([
|
|
4628
|
+
lines.push([
|
|
4629
|
+
"Running",
|
|
4630
|
+
body.reason === "out_of_credit" ? `no: ${free ? "your credits are" : "the plan's credits and the top-ups are"} used. agental billing top-up <dollars> buys more.` : body.reason === "billing_required" ? "no: save a card first, charged nothing, with agental billing card." : `no: ${body.reason}`
|
|
4631
|
+
]);
|
|
4231
4632
|
}
|
|
4232
4633
|
if (body.lapse) {
|
|
4233
4634
|
lines.push([
|
|
4234
4635
|
"Deleted on",
|
|
4235
|
-
body.lapse.reason === "unpaid" ? `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account owes for what it used past its plan. agental billing top-up <dollars> buys enough to pay it and keep them.` : `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account has no
|
|
4636
|
+
body.lapse.reason === "unpaid" ? `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account owes for what it used past its plan. agental billing top-up <dollars> buys enough to pay it and keep them.` : body.lapse.reason === "out_of_credit" ? `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as your credits are used. agental billing top-up <dollars> keeps them.` : `${dayOf(body.lapse.deletesAt)}: your sandboxes and snapshots, as the account has no card saved. agental billing card saves one, charged nothing, to keep them.`
|
|
4236
4637
|
]);
|
|
4237
4638
|
}
|
|
4238
4639
|
if (body.testMode) {
|
|
@@ -4241,6 +4642,14 @@ function describeBilling(body) {
|
|
|
4241
4642
|
return lines.map(([label, value]) => `${label.padEnd(14)}${value}
|
|
4242
4643
|
`).join("");
|
|
4243
4644
|
}
|
|
4645
|
+
function usedParts(body) {
|
|
4646
|
+
const parts = [
|
|
4647
|
+
...(body.mailMicros ?? 0) > 0 ? [`${money(body.mailMicros ?? 0)} by email`] : [],
|
|
4648
|
+
...(body.stoppedMicros ?? 0) > 0 ? [`${money(body.stoppedMicros ?? 0)} by stopped sandboxes`] : [],
|
|
4649
|
+
...(body.snapshotMicros ?? 0) > 0 ? [`${money(body.snapshotMicros ?? 0)} by snapshots past the free ones`] : []
|
|
4650
|
+
];
|
|
4651
|
+
return parts.length > 0 ? `, ${parts.join(" and ")} of it` : "";
|
|
4652
|
+
}
|
|
4244
4653
|
function describeAutoReload(autoReload) {
|
|
4245
4654
|
if (!autoReload) {
|
|
4246
4655
|
return "off. Nothing is charged by itself when your credits run out.";
|
|
@@ -4257,7 +4666,14 @@ function describePrices(body) {
|
|
|
4257
4666
|
for (const tier of body.tiers) {
|
|
4258
4667
|
const sizes = tier.sizes === void 0 ? "" : tier.sizes.length === body.plans.length ? "every size" : tier.sizes.join(", ");
|
|
4259
4668
|
const speed = tier.runningUnits !== void 0 && tier.freeBurstUnits !== void 0 ? `${tier.runningUnits.toLocaleString("en-US")} running, ${tier.freeBurstUnits.toLocaleString("en-US")} start free` : tier.burst ? `${(tier.burst.perQuarterMb / 1024).toLocaleString("en-US")} GiB per 15 minutes` : typeof tier.sandboxesAtOnce === "number" ? `${tier.sandboxesAtOnce.toLocaleString("en-US")} sandboxes at once` : "";
|
|
4260
|
-
|
|
4669
|
+
const price = tier.dollarsPerMonth === 0 ? "Free" : `$${tier.dollarsPerMonth.toLocaleString("en-US")}`;
|
|
4670
|
+
const included = tier.dollarsPerMonth === 0 ? "pay as you go" : `${wholeMoney(tier.includedMicros)} of credits`;
|
|
4671
|
+
out.push(` ${price.padEnd(8)}${included.padEnd(28)}${speed.padEnd(34)}${sizes}`.trimEnd());
|
|
4672
|
+
}
|
|
4673
|
+
if (body.free) {
|
|
4674
|
+
out.push(
|
|
4675
|
+
` Free needs a card saved, charged nothing, and gives ${wholeMoney(body.free.creditMicros)} of credit once. It runs ${body.free.unpaidUnits} small sandboxes at once until you buy a top-up.`
|
|
4676
|
+
);
|
|
4261
4677
|
}
|
|
4262
4678
|
if (body.tiers.some((tier) => tier.runningUnits !== void 0)) {
|
|
4263
4679
|
out.push(" Counted in small sandboxes: a medium is 2, a large 4, a tiny half.");
|
|
@@ -4518,9 +4934,10 @@ Setting up
|
|
|
4518
4934
|
|
|
4519
4935
|
Usage
|
|
4520
4936
|
agental create --name <name> [--template <template>] [--size tiny|small|medium|large] [--region <region>[,<region>...]] [--ttl <time> | --ttl default] [--idempotency-key <key>]
|
|
4521
|
-
${operator ? "[--box <name-or-id>] " : ""}[--allow <address>[,<address>...]] [--max-burst-fee <dollars>]
|
|
4937
|
+
${operator ? "[--box <name-or-id>] " : ""}[--allow <address>[,<address>...]] [--egress box | residential:<country>[:sticky|rotating]] [--max-burst-fee <dollars>]
|
|
4522
4938
|
[--ssh-key <public-key-or-file>]... [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]...
|
|
4523
|
-
agental
|
|
4939
|
+
agental summary [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--facets <field>[,<field>...]] [--json]
|
|
4940
|
+
agental list [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--sort=<sort>] [--limit <n>] [--json]
|
|
4524
4941
|
agental tags <sandbox-id> [add|rm <tag>...] [--json]
|
|
4525
4942
|
agental meta <sandbox-id> [set <key>=<value>... | unset <key>...] [--json]
|
|
4526
4943
|
agental regions [--json]
|
|
@@ -4528,17 +4945,22 @@ Usage
|
|
|
4528
4945
|
agental delete <sandbox-id>
|
|
4529
4946
|
agental start <sandbox-id> [--max-burst-fee <dollars>]
|
|
4530
4947
|
agental stop <sandbox-id> [--keep-memory]
|
|
4948
|
+
agental stop --where '<words>' [--keep-memory] [--yes] every sandbox the words find, as one job
|
|
4949
|
+
agental start --where '<words>' [--yes]
|
|
4950
|
+
agental delete --where '<words>' [--confirm <count>] asks for the count to be typed
|
|
4951
|
+
agental jobs [list | show <job-id> | cancel <job-id>] [--json]
|
|
4531
4952
|
agental recover <sandbox-id> [--max-burst-fee <dollars>]
|
|
4532
4953
|
agental auto-stop <sandbox-id> [--in <time> | --at <date-time> | --never] [--json]
|
|
4533
4954
|
agental network <sandbox-id> [--allow <address>[,<address>...] | --none] [--json]
|
|
4955
|
+
agental egress <sandbox-id> [box | residential <country> [sticky | rotating]] [--json]
|
|
4534
4956
|
agental templates [--json]
|
|
4535
4957
|
agental template create <name> [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]...
|
|
4536
4958
|
agental template show <template> [--json]
|
|
4537
4959
|
agental template rename <template> <new-name>
|
|
4538
4960
|
agental template delete <template>
|
|
4539
4961
|
agental template start <template> [--size tiny|small|medium|large] [--snapshot <snapshot-id> | --new-machine] [--ttl <time> | --ttl default]
|
|
4540
|
-
agental template repo <template> [<name> <url> | --remove]
|
|
4541
4962
|
agental template network <template> [open | blocked | allowlist <address>...] [--json]
|
|
4963
|
+
agental template egress <template> [box | residential <country> [sticky | rotating]] [--json]
|
|
4542
4964
|
agental template ssh <template> [on | off] [--json]
|
|
4543
4965
|
agental template ssh-keys <template> [<public-key-or-file>... | --remove <key-id>] [--json]
|
|
4544
4966
|
agental template tags <template> [add|rm <tag>...] [--json]
|
|
@@ -4560,11 +4982,12 @@ Usage
|
|
|
4560
4982
|
agental data-retention [1 | 7 | 30 | 90 | never] [--json]
|
|
4561
4983
|
agental schedules <sandbox-id> [--json]
|
|
4562
4984
|
agental schedule add <sandbox-id> (--every 15m|1h|1d | --cron "<m h dom mon dow>" [--tz <zone>] | --at <date-time>) [--name <name>] [--timeout <time>]
|
|
4563
|
-
[--if-stopped start|skip] [--keep-memory] [--overlap skip|replace|parallel] [--json] -- <command> [args...]
|
|
4985
|
+
[--if-stopped start|skip] [--keep-memory] [--overlap skip|replace|parallel] [--delete-after] [--json] -- <command> [args...]
|
|
4564
4986
|
agental schedule show|run|pause|resume|delete <sandbox-id> <schedule> [--json]
|
|
4565
4987
|
agental schedule runs <sandbox-id> <schedule> [--limit <n>] [--json]
|
|
4566
4988
|
agental schedule update <sandbox-id> <schedule> [--every <time> | --cron "<expr>" | --at <date-time>] [--tz <zone>] [--name <name>] [--timeout <time>]
|
|
4567
|
-
[--if-stopped start|skip] [--keep-memory | --disk-only] [--overlap skip|replace|parallel] [--
|
|
4989
|
+
[--if-stopped start|skip] [--keep-memory | --disk-only] [--overlap skip|replace|parallel] [--delete-after | --keep-sandbox] [--json]
|
|
4990
|
+
[-- <command> [args...]]
|
|
4568
4991
|
agental webhooks [--json]
|
|
4569
4992
|
agental webhook add <url> (--events <kind>[,<kind>...] | --all) [--description <text>] [--json]
|
|
4570
4993
|
agental webhook show <webhook-id> [--json]
|
|
@@ -4584,8 +5007,10 @@ Usage
|
|
|
4584
5007
|
agental key show <key> [--json]
|
|
4585
5008
|
agental key allow-from <key> (<address-or-range>... | --anywhere) [--json]
|
|
4586
5009
|
agental key delete <key>
|
|
4587
|
-
agental billing [prices | top-up <dollars> | auto-reload <dollars | off>] [--json]
|
|
5010
|
+
agental billing [prices | card | top-up <dollars> | auto-reload <dollars | off>] [--json]
|
|
4588
5011
|
agental burst [budget <dollars> | budget default] [--json]
|
|
5012
|
+
agental egress [budget <dollars> | budget default] [--json]
|
|
5013
|
+
agental egress use-case "<what your sandboxes will use it for>" [--json]
|
|
4589
5014
|
agental account export [--out <file>]
|
|
4590
5015
|
agental ssh <sandbox-id> [--tmux] [ssh options] [command...]
|
|
4591
5016
|
agental scp [scp options] <source>... <target>
|
|
@@ -4647,7 +5072,7 @@ sandbox is not restarted, and what is sent to it, commands, files and SSH, is no
|
|
|
4647
5072
|
create --allow gives a new sandbox its own from its first boot. These work where GET
|
|
4648
5073
|
/v1/policy says networkPolicy.
|
|
4649
5074
|
A template is what a sandbox is made as and held to: its start (a size, and a snapshot or a new
|
|
4650
|
-
machine), its variables and files, and
|
|
5075
|
+
machine), its variables and files, and its outbound rule. Every account has one named default,
|
|
4651
5076
|
which a sandbox is on when create names none. <template> is a template's id or its name. A
|
|
4652
5077
|
sandbox's commands get its template's variables and files as they were when it last started,
|
|
4653
5078
|
with its own on top: secrets <sandbox-id> sets its own, and secrets --template sets a
|
|
@@ -4700,8 +5125,10 @@ schedules lists the commands a sandbox runs on a schedule, with when each runs n
|
|
|
4700
5125
|
last run ended. schedule add makes one: what comes after -- is its command, run by sh -c, and
|
|
4701
5126
|
--every 5m, 15m, 1h or 1d, counted from now, never more often than every 5 minutes, or --cron
|
|
4702
5127
|
with five fields in the time zone --tz names, UTC unless it does, says when; --at a date and time
|
|
4703
|
-
runs it once, as for a check to come back to.
|
|
4704
|
-
|
|
5128
|
+
runs it once, as for a check to come back to. With --at, --delete-after deletes the sandbox after
|
|
5129
|
+
the run, if the command exits 0, with a key that may delete it; a run that fails leaves it to be
|
|
5130
|
+
looked at, and update --keep-sandbox takes it back. When a run finds the sandbox stopped it
|
|
5131
|
+
starts it, runs, and stops it again when the command ends, keeping its disk, or its memory too with
|
|
4705
5132
|
--keep-memory; with --if-stopped skip it runs only while the sandbox runs. A sandbox someone
|
|
4706
5133
|
started again, sent a command or moved the stop of meanwhile is left running. While a run still
|
|
4707
5134
|
goes the next is skipped; --overlap replace stops it instead, and parallel runs both. A run gets
|
|
@@ -4725,8 +5152,10 @@ tries.
|
|
|
4725
5152
|
org lists the organizations you are in, your own and your teams, and marks the one this command
|
|
4726
5153
|
acts in. A key acts in the organization it was made in, for good, so org use signs in again in
|
|
4727
5154
|
the browser for a key made in the one given, by its id or name, and deletes the key the command
|
|
4728
|
-
had.
|
|
4729
|
-
|
|
5155
|
+
had. Your own organization takes members on the $500 or $2000 plan, with you its only owner.
|
|
5156
|
+
org create makes a team besides, with you as its owner and a plan of its own; it has members
|
|
5157
|
+
besides its owners on the $500 or $2000 plan. members lists the organization the command acts
|
|
5158
|
+
in, your own or a team, and members remove takes one
|
|
4730
5159
|
out by id or email; keys they made in it go on working, as a member's, until they are revoked,
|
|
4731
5160
|
and --revoke-keys revokes them in the same step. invite asks an email address in, as a member or
|
|
4732
5161
|
with --role admin; the invitation is open for seven days.
|
|
@@ -4754,8 +5183,12 @@ server it is on, which changes when it moves. On a network with IPv6 the API see
|
|
|
4754
5183
|
address, so a key limited to IPv4 addresses alone is refused there.
|
|
4755
5184
|
billing says where your account stands: its plan, what it has used this month at list price,
|
|
4756
5185
|
what is left of the month's credits, and what is left of the top-ups, which are used
|
|
4757
|
-
after it and do not expire.
|
|
4758
|
-
|
|
5186
|
+
after it and do not expire. The free plan costs nothing a month and pays for what runs from
|
|
5187
|
+
credits alone; it makes tiny and small sandboxes. prices lists the plans with the sizes each
|
|
5188
|
+
makes, the top-ups with their bonus and the hourly price of each size. card prints the address
|
|
5189
|
+
of a page to save a card for the free plan: nothing is charged, and once it is saved the account
|
|
5190
|
+
is given $10 of credit, once. Until it buys a top-up it runs 2 small sandboxes at once, then 20.
|
|
5191
|
+
top-up <dollars> starts buying credits for
|
|
4759
5192
|
any whole number of dollars from 15 to 100,000, and prints the address of the page to pay on.
|
|
4760
5193
|
From $200 a top-up adds a bonus, 2% there and growing to 25% at $10,000: $500 adds $537.
|
|
4761
5194
|
Nothing is charged until it is paid there. auto-reload <dollars> has the card saved at your
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agental",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The agental command: Linux sandboxes for your agents, from the terminal. npx agental@latest signs you in and installs the agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VERTIXER LTD",
|
|
7
7
|
"homepage": "https://agental.io/docs/cli",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/honzabit/agental.git",
|
|
11
|
+
"directory": "cli"
|
|
12
|
+
},
|
|
8
13
|
"bugs": {
|
|
9
14
|
"email": "squad@agental.io"
|
|
10
15
|
},
|