agental 0.2.0 → 0.3.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 +444 -102
- 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.3.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,95 @@ 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"]);
|
|
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") {
|
|
2447
|
+
throw new Error(`${word}: only tags 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 "archived":
|
|
2483
|
+
query.set("archived", value);
|
|
2484
|
+
break;
|
|
2485
|
+
case "stops":
|
|
2486
|
+
query.set("stops", value);
|
|
2487
|
+
break;
|
|
2488
|
+
default:
|
|
2489
|
+
query.append(key2, value);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (plain2.length > 0) {
|
|
2493
|
+
query.set("q", plain2.join(" "));
|
|
2494
|
+
}
|
|
2495
|
+
return query;
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2397
2498
|
// ../src/cli.ts
|
|
2398
2499
|
var DEFAULT_URL = DEFAULT_API_URL;
|
|
2399
2500
|
var SSH_COMMANDS = {
|
|
@@ -2506,7 +2607,7 @@ async function main() {
|
|
|
2506
2607
|
expires: { type: "string" },
|
|
2507
2608
|
"keep-streams": { type: "boolean" },
|
|
2508
2609
|
"keep-memory": { type: "boolean" },
|
|
2509
|
-
// agental schedule: when it runs, in what time zone, how long a run may take,
|
|
2610
|
+
// 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
2611
|
every: { type: "string" },
|
|
2511
2612
|
cron: { type: "string" },
|
|
2512
2613
|
tz: { type: "string" },
|
|
@@ -2514,6 +2615,8 @@ async function main() {
|
|
|
2514
2615
|
overlap: { type: "string" },
|
|
2515
2616
|
"if-stopped": { type: "string" },
|
|
2516
2617
|
"disk-only": { type: "boolean" },
|
|
2618
|
+
"delete-after": { type: "boolean" },
|
|
2619
|
+
"keep-sandbox": { type: "boolean" },
|
|
2517
2620
|
ttl: { type: "string" },
|
|
2518
2621
|
"max-burst-fee": { type: "string" },
|
|
2519
2622
|
in: { type: "string" },
|
|
@@ -2545,6 +2648,11 @@ async function main() {
|
|
|
2545
2648
|
// Metadata as <key>=<value>, given again for each key.
|
|
2546
2649
|
meta: { type: "string", multiple: true },
|
|
2547
2650
|
anywhere: { type: "boolean", default: false },
|
|
2651
|
+
where: { type: "string" },
|
|
2652
|
+
facets: { type: "string" },
|
|
2653
|
+
yes: { type: "boolean", default: false },
|
|
2654
|
+
confirm: { type: "string" },
|
|
2655
|
+
sort: { type: "string" },
|
|
2548
2656
|
role: { type: "string" },
|
|
2549
2657
|
"revoke-keys": { type: "boolean", default: false },
|
|
2550
2658
|
help: { type: "boolean", short: "h", default: false }
|
|
@@ -2560,7 +2668,13 @@ async function main() {
|
|
|
2560
2668
|
await create({ ...values, template: once(values.template, "--template") });
|
|
2561
2669
|
return;
|
|
2562
2670
|
case "list":
|
|
2563
|
-
await list2(values.json, labelsFilter(values));
|
|
2671
|
+
await list2(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.sort, values.limit);
|
|
2672
|
+
return;
|
|
2673
|
+
case "jobs":
|
|
2674
|
+
await jobs(positionals.slice(1), values.json);
|
|
2675
|
+
return;
|
|
2676
|
+
case "summary":
|
|
2677
|
+
await summary(values.json, whereQuery(values.where ?? "", labelsFilter(values)), values.facets);
|
|
2564
2678
|
return;
|
|
2565
2679
|
case "tags":
|
|
2566
2680
|
await labels("tags", "sandbox", positionals.slice(1), values.json);
|
|
@@ -2575,15 +2689,30 @@ async function main() {
|
|
|
2575
2689
|
await exec(positionals.slice(1));
|
|
2576
2690
|
return;
|
|
2577
2691
|
case "delete":
|
|
2692
|
+
if (values.where !== void 0 && positionals[1] !== void 0) {
|
|
2693
|
+
throw new Error("delete takes a sandbox's ID or --where, not both");
|
|
2694
|
+
}
|
|
2695
|
+
if (values.where !== void 0) {
|
|
2696
|
+
await bulk("delete", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json, values.confirm);
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2578
2699
|
await remove2(positionals[1]);
|
|
2579
2700
|
return;
|
|
2580
2701
|
case "start":
|
|
2702
|
+
if (values.where !== void 0) {
|
|
2703
|
+
await bulk("start", whereQuery(values.where, labelsFilter(values)), {}, values.yes === true, values.json);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2581
2706
|
await lifecycle("start", positionals[1], maxBurstFeeHeaders(values["max-burst-fee"]));
|
|
2582
2707
|
return;
|
|
2583
2708
|
case "auto-stop":
|
|
2584
2709
|
await autoStop(positionals[1], values.in, values.at, values.never === true, values.json);
|
|
2585
2710
|
return;
|
|
2586
2711
|
case "stop":
|
|
2712
|
+
if (values.where !== void 0) {
|
|
2713
|
+
await bulk("stop", whereQuery(values.where, labelsFilter(values)), values["keep-memory"] === true ? { keepMemory: true } : {}, values.yes === true, values.json);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2587
2716
|
await lifecycle("stop", positionals[1], {}, values["keep-memory"] === true ? { keepMemory: true } : void 0);
|
|
2588
2717
|
return;
|
|
2589
2718
|
case "recover":
|
|
@@ -2888,20 +3017,39 @@ async function regions(asJson) {
|
|
|
2888
3017
|
}
|
|
2889
3018
|
process.stdout.write("* where a sandbox is made when no region is named\n");
|
|
2890
3019
|
}
|
|
2891
|
-
async function list2(asJson, filter) {
|
|
2892
|
-
const
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3020
|
+
async function list2(asJson, filter, sort, limit) {
|
|
3021
|
+
const sandboxes = [];
|
|
3022
|
+
let cursor = null;
|
|
3023
|
+
do {
|
|
3024
|
+
const query = new URLSearchParams(filter);
|
|
3025
|
+
if (sort !== void 0) {
|
|
3026
|
+
query.set("sort", sort);
|
|
3027
|
+
}
|
|
3028
|
+
query.set("limit", limit ?? "200");
|
|
3029
|
+
if (cursor !== null) {
|
|
3030
|
+
query.set("cursor", cursor);
|
|
3031
|
+
}
|
|
3032
|
+
const response = await request(`/v1/sandboxes?${query}`, { method: "GET" });
|
|
3033
|
+
const body = await readBody(response);
|
|
3034
|
+
if (!response.ok) {
|
|
3035
|
+
fail(response.status, body);
|
|
3036
|
+
}
|
|
3037
|
+
sandboxes.push(...readSandboxes(body));
|
|
3038
|
+
const next = body && typeof body === "object" ? body["nextCursor"] : null;
|
|
3039
|
+
cursor = typeof next === "string" ? next : null;
|
|
3040
|
+
if (limit !== void 0 && cursor !== null) {
|
|
3041
|
+
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}
|
|
3042
|
+
`);
|
|
3043
|
+
cursor = null;
|
|
3044
|
+
}
|
|
3045
|
+
} while (cursor !== null);
|
|
2898
3046
|
if (asJson) {
|
|
2899
3047
|
process.stdout.write(`${JSON.stringify(sandboxes, null, 2)}
|
|
2900
3048
|
`);
|
|
2901
3049
|
return;
|
|
2902
3050
|
}
|
|
2903
3051
|
if (sandboxes.length === 0) {
|
|
2904
|
-
process.stdout.write(filter.size > 0 ? "No sandboxes
|
|
3052
|
+
process.stdout.write(filter.size > 0 ? "No sandboxes match.\n" : "No sandboxes.\n");
|
|
2905
3053
|
return;
|
|
2906
3054
|
}
|
|
2907
3055
|
const names = sandboxes.some((sandbox) => typeof sandbox["templateId"] === "string") ? await templateNames() : /* @__PURE__ */ new Map();
|
|
@@ -2927,6 +3075,165 @@ async function list2(asJson, filter) {
|
|
|
2927
3075
|
`);
|
|
2928
3076
|
}
|
|
2929
3077
|
}
|
|
3078
|
+
function dollarsOf(micros) {
|
|
3079
|
+
return `$${(micros / 1e6).toFixed(4)}`;
|
|
3080
|
+
}
|
|
3081
|
+
async function summary(asJson, filter, facets) {
|
|
3082
|
+
const query = new URLSearchParams(filter);
|
|
3083
|
+
if (facets !== void 0) {
|
|
3084
|
+
query.set("facets", facets);
|
|
3085
|
+
}
|
|
3086
|
+
const response = await request(`/v1/sandbox-summary${query.size > 0 ? `?${query}` : ""}`, { method: "GET" });
|
|
3087
|
+
const body = await readBody(response);
|
|
3088
|
+
if (!response.ok) {
|
|
3089
|
+
fail(response.status, body);
|
|
3090
|
+
}
|
|
3091
|
+
if (asJson) {
|
|
3092
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}
|
|
3093
|
+
`);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
const read2 = body;
|
|
3097
|
+
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
|
|
3098
|
+
`;
|
|
3099
|
+
for (const [state2, money2] of Object.entries(read2.byState)) {
|
|
3100
|
+
if (money2.count > 0) {
|
|
3101
|
+
process.stdout.write(line(state2, money2));
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
process.stdout.write(line("total", read2.total));
|
|
3105
|
+
const running = read2.byState["running"];
|
|
3106
|
+
if (running && running.count > 0) {
|
|
3107
|
+
process.stdout.write(`Stopped, the ${running.count} running would cost ${dollarsOf(read2.ifStopped.microsPerHour)} an hour, ${dollarsOf(running.microsPerHour - read2.ifStopped.microsPerHour)} less.
|
|
3108
|
+
`);
|
|
3109
|
+
}
|
|
3110
|
+
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");
|
|
3111
|
+
for (const [field, facet] of Object.entries(read2.facets ?? {})) {
|
|
3112
|
+
process.stdout.write(`
|
|
3113
|
+
${field}
|
|
3114
|
+
`);
|
|
3115
|
+
for (const each of facet.values) {
|
|
3116
|
+
process.stdout.write(` ${pad2(each.value)} ${String(each.count).padStart(6)} ${dollarsOf(each.microsPerHour).padStart(11)} an hour
|
|
3117
|
+
`);
|
|
3118
|
+
}
|
|
3119
|
+
if (facet.more > 0) {
|
|
3120
|
+
process.stdout.write(` and ${facet.more} more
|
|
3121
|
+
`);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function filterOf(query) {
|
|
3126
|
+
const filter = {};
|
|
3127
|
+
for (const [name, value] of query) {
|
|
3128
|
+
filter[name] = [...filter[name] ?? [], value];
|
|
3129
|
+
}
|
|
3130
|
+
return filter;
|
|
3131
|
+
}
|
|
3132
|
+
async function typedCount(count) {
|
|
3133
|
+
const { createInterface: createInterface2 } = await import("node:readline/promises");
|
|
3134
|
+
const lines = createInterface2({ input: process.stdin, output: process.stderr });
|
|
3135
|
+
try {
|
|
3136
|
+
return (await lines.question(`They cannot be brought back. Type ${count} to delete them: `)).trim();
|
|
3137
|
+
} finally {
|
|
3138
|
+
lines.close();
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
async function confirmed(question) {
|
|
3142
|
+
if (!process.stdin.isTTY) {
|
|
3143
|
+
return false;
|
|
3144
|
+
}
|
|
3145
|
+
const { createInterface: createInterface2 } = await import("node:readline/promises");
|
|
3146
|
+
const lines = createInterface2({ input: process.stdin, output: process.stderr });
|
|
3147
|
+
try {
|
|
3148
|
+
return /^y(es)?$/i.test((await lines.question(`${question} [y/N] `)).trim());
|
|
3149
|
+
} finally {
|
|
3150
|
+
lines.close();
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
async function bulk(action, filter, options, yes2, asJson, confirm) {
|
|
3154
|
+
const previewed = await request("/v1/sandbox-jobs/preview", { method: "POST", body: JSON.stringify({ action, options, select: { filter: filterOf(filter) } }) });
|
|
3155
|
+
const preview = await readBody(previewed);
|
|
3156
|
+
if (!previewed.ok) {
|
|
3157
|
+
fail(previewed.status, preview);
|
|
3158
|
+
}
|
|
3159
|
+
const read2 = preview;
|
|
3160
|
+
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` : ""}.
|
|
3161
|
+
`);
|
|
3162
|
+
process.stderr.write(`They cost ${dollarsOf(read2.microsPerHour.now)} an hour now, and ${dollarsOf(read2.microsPerHour.after)} an hour after.
|
|
3163
|
+
`);
|
|
3164
|
+
let count = null;
|
|
3165
|
+
if (action === "delete") {
|
|
3166
|
+
const given = confirm ?? (process.stdin.isTTY ? await typedCount(read2.count) : "");
|
|
3167
|
+
if (given !== String(read2.count)) {
|
|
3168
|
+
process.stderr.write(`Nothing was deleted. Type ${read2.count}, or send --confirm ${read2.count}, to delete them.
|
|
3169
|
+
`);
|
|
3170
|
+
process.exitCode = 1;
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
count = read2.count;
|
|
3174
|
+
} else if (!yes2 && !await confirmed("Go on?")) {
|
|
3175
|
+
process.stderr.write("Nothing was done. --yes goes on without asking.\n");
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
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 } }) });
|
|
3179
|
+
let job = await readBody(made);
|
|
3180
|
+
if (!made.ok) {
|
|
3181
|
+
fail(made.status, job);
|
|
3182
|
+
}
|
|
3183
|
+
while (job.state === "running") {
|
|
3184
|
+
process.stderr.write(`\r${job.id}: ${job.counts["done"] ?? 0} done, ${job.counts["skipped"] ?? 0} skipped, ${job.counts["failed"] ?? 0} failed of ${job.total}`);
|
|
3185
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3186
|
+
const read3 = await request(`/v1/sandbox-jobs/${job.id}`, { method: "GET" });
|
|
3187
|
+
job = await readBody(read3);
|
|
3188
|
+
if (!read3.ok) {
|
|
3189
|
+
fail(read3.status, job);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
process.stderr.write("\n");
|
|
3193
|
+
if (asJson) {
|
|
3194
|
+
process.stdout.write(`${JSON.stringify(job, null, 2)}
|
|
3195
|
+
`);
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
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}.
|
|
3199
|
+
`);
|
|
3200
|
+
for (const problem of job.problems) {
|
|
3201
|
+
process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
|
|
3202
|
+
`);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
async function jobs(positionals, asJson) {
|
|
3206
|
+
const [verb, id] = positionals;
|
|
3207
|
+
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;
|
|
3208
|
+
if (path4 === null) {
|
|
3209
|
+
throw new Error("jobs takes list, show <job-id> or cancel <job-id>");
|
|
3210
|
+
}
|
|
3211
|
+
const response = await request(path4, { method: verb === "cancel" ? "POST" : "GET" });
|
|
3212
|
+
const body = await readBody(response);
|
|
3213
|
+
if (!response.ok) {
|
|
3214
|
+
fail(response.status, body);
|
|
3215
|
+
}
|
|
3216
|
+
if (asJson) {
|
|
3217
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}
|
|
3218
|
+
`);
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
const listed = verb === void 0 || verb === "list" ? body.jobs ?? [] : [body];
|
|
3222
|
+
if (listed.length === 0) {
|
|
3223
|
+
process.stdout.write("No jobs.\n");
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
for (const job of listed) {
|
|
3227
|
+
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}
|
|
3228
|
+
`);
|
|
3229
|
+
if (listed.length === 1) {
|
|
3230
|
+
for (const problem of job.problems) {
|
|
3231
|
+
process.stdout.write(` ${problem.sandboxId} ${problem.state}${problem.reason ? `: ${problem.reason}` : ""}
|
|
3232
|
+
`);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
2930
3237
|
async function exec(positionals) {
|
|
2931
3238
|
const id = positionals[0];
|
|
2932
3239
|
const command = positionals.slice(1);
|
|
@@ -3371,7 +3678,7 @@ function labelsInWords(body) {
|
|
|
3371
3678
|
return [`tags: ${tags.length > 0 ? tags.join(", ") : "none"}`, `metadata: ${metadata.length > 0 ? metadata.join(", ") : "none"}`, ""].join("\n");
|
|
3372
3679
|
}
|
|
3373
3680
|
async function labels(what, holder, positionals, asJson) {
|
|
3374
|
-
const [named, action, ...
|
|
3681
|
+
const [named, action, ...words2] = positionals;
|
|
3375
3682
|
const command = `${holder === "template" ? "template " : ""}${what}`;
|
|
3376
3683
|
const usage = what === "tags" ? `${command} <${holder}> [add|rm <tag>...]` : `${command} <${holder}> [set <key>=<value>... | unset <key>...]`;
|
|
3377
3684
|
if (!named) {
|
|
@@ -3383,7 +3690,7 @@ async function labels(what, holder, positionals, asJson) {
|
|
|
3383
3690
|
if (action === void 0) {
|
|
3384
3691
|
body = await call2(path4, { method: "GET" });
|
|
3385
3692
|
} else if (what === "tags" && (action === "add" || action === "rm")) {
|
|
3386
|
-
const tags = listedIn(
|
|
3693
|
+
const tags = listedIn(words2).map((tag) => tag.toLowerCase());
|
|
3387
3694
|
if (tags.length === 0) {
|
|
3388
3695
|
throw new Error(`${command} ${action} requires <tag>...`);
|
|
3389
3696
|
}
|
|
@@ -3395,10 +3702,10 @@ async function labels(what, holder, positionals, asJson) {
|
|
|
3395
3702
|
body = await call2(`${path4}/tags/${encodeURIComponent(tag)}`, { method: action === "add" ? "PUT" : "DELETE" });
|
|
3396
3703
|
}
|
|
3397
3704
|
} else if (what === "meta" && (action === "set" || action === "unset")) {
|
|
3398
|
-
if (
|
|
3705
|
+
if (words2.length === 0) {
|
|
3399
3706
|
throw new Error(`${command} ${action} requires ${action === "set" ? "<key>=<value>" : "<key>"}...`);
|
|
3400
3707
|
}
|
|
3401
|
-
const metadata = action === "set" ? metadataGiven(
|
|
3708
|
+
const metadata = action === "set" ? metadataGiven(words2) : Object.fromEntries(words2.map((key2) => [key2, null]));
|
|
3402
3709
|
body = await call2(path4, { method: "PATCH", body: JSON.stringify({ metadata }) });
|
|
3403
3710
|
} else {
|
|
3404
3711
|
throw new Error(`${command} takes ${usage}`);
|
|
@@ -3513,7 +3820,6 @@ function showTemplate(each) {
|
|
|
3513
3820
|
// Said only where some are. An API from before variables could be kept out of the sandbox says nothing of them.
|
|
3514
3821
|
...each.hidden && each.hidden.length > 0 ? [`kept out: ${list3(each.hidden)}`] : [],
|
|
3515
3822
|
`files: ${list3(each.files)}`,
|
|
3516
|
-
`repo: ${each.repo ? `${each.repo.name} ${each.repo.url}` : "none"}`,
|
|
3517
3823
|
// An API from before templates held rules says nothing of one.
|
|
3518
3824
|
...each.network ? [`network: ${ruleInWords(each.network)}`] : [],
|
|
3519
3825
|
// An API from before templates gave SSH says nothing of it.
|
|
@@ -3526,10 +3832,10 @@ function showTemplate(each) {
|
|
|
3526
3832
|
}
|
|
3527
3833
|
async function template(positionals, values) {
|
|
3528
3834
|
const [action, first, ...rest] = positionals;
|
|
3529
|
-
const usage = "template takes create, show, rename, delete, start,
|
|
3530
|
-
const print4 = (body,
|
|
3835
|
+
const usage = "template takes create, show, rename, delete, start, network, ssh, ssh-keys, tags, meta, sandboxes, apply, of or put. agental help says how.";
|
|
3836
|
+
const print4 = (body, words2) => {
|
|
3531
3837
|
process.stdout.write(values.json ? `${JSON.stringify(body, null, 2)}
|
|
3532
|
-
` :
|
|
3838
|
+
` : words2);
|
|
3533
3839
|
};
|
|
3534
3840
|
const need = (word, what) => {
|
|
3535
3841
|
if (!word) {
|
|
@@ -3589,20 +3895,6 @@ async function template(positionals, values) {
|
|
|
3589
3895
|
`);
|
|
3590
3896
|
return;
|
|
3591
3897
|
}
|
|
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
3898
|
case "network": {
|
|
3607
3899
|
const found = await templateNamed(need(first, "<template>"));
|
|
3608
3900
|
const path4 = `/v1/templates/${encodeURIComponent(found.id)}/network`;
|
|
@@ -3728,19 +4020,19 @@ async function keptOutNow(path4) {
|
|
|
3728
4020
|
}
|
|
3729
4021
|
async function secrets(positionals, values) {
|
|
3730
4022
|
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
|
|
4023
|
+
const words2 = [...positionals];
|
|
3732
4024
|
let base;
|
|
3733
4025
|
let sandboxId;
|
|
3734
4026
|
if (values.template !== void 0) {
|
|
3735
4027
|
base = `/v1/templates/${encodeURIComponent((await templateNamed(values.template)).id)}/secrets`;
|
|
3736
4028
|
} else {
|
|
3737
|
-
sandboxId =
|
|
4029
|
+
sandboxId = words2.shift();
|
|
3738
4030
|
if (!sandboxId) {
|
|
3739
4031
|
throw new Error(usage);
|
|
3740
4032
|
}
|
|
3741
4033
|
base = `/v1/sandboxes/${encodeURIComponent(sandboxId)}/secrets`;
|
|
3742
4034
|
}
|
|
3743
|
-
const [action, ...args] =
|
|
4035
|
+
const [action, ...args] = words2;
|
|
3744
4036
|
const fileQuery = (path4) => {
|
|
3745
4037
|
if (!path4) {
|
|
3746
4038
|
throw new Error(`secrets ${action ?? ""} requires <path>`);
|
|
@@ -4015,14 +4307,14 @@ async function ci(positionals, options) {
|
|
|
4015
4307
|
return;
|
|
4016
4308
|
}
|
|
4017
4309
|
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
|
|
4310
|
+
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
4311
|
`);
|
|
4020
4312
|
return;
|
|
4021
4313
|
}
|
|
4022
4314
|
for (const item of body.repositories) {
|
|
4023
4315
|
const busy = item.sandboxes.filter((sandbox) => sandbox.busy).length;
|
|
4024
4316
|
process.stdout.write(
|
|
4025
|
-
`${item.repository} pool ${item.poolMax ?? `${body.defaultPoolMax} (default)`} template ${item.templateId ?? "runs-on (empty)"} ${item.sandboxes.length} sandboxes, ${busy} busy
|
|
4317
|
+
`${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
4318
|
`
|
|
4027
4319
|
);
|
|
4028
4320
|
}
|
|
@@ -4132,7 +4424,7 @@ async function telemetry(positionals, given, asJson) {
|
|
|
4132
4424
|
}
|
|
4133
4425
|
async function billing(positionals, asJson) {
|
|
4134
4426
|
const [what, amount, ...rest] = positionals;
|
|
4135
|
-
const usage = "billing takes nothing, prices, top-up <dollars>, or auto-reload <dollars | off>";
|
|
4427
|
+
const usage = "billing takes nothing, prices, card, top-up <dollars>, or auto-reload <dollars | off>";
|
|
4136
4428
|
if (rest.length > 0 || what !== "top-up" && what !== "auto-reload" && amount !== void 0) {
|
|
4137
4429
|
throw new Error(usage);
|
|
4138
4430
|
}
|
|
@@ -4149,6 +4441,17 @@ async function billing(positionals, asJson) {
|
|
|
4149
4441
|
print4(body, () => describePrices(body));
|
|
4150
4442
|
return;
|
|
4151
4443
|
}
|
|
4444
|
+
case "card": {
|
|
4445
|
+
const body = await asked("/v1/billing/checkout", { method: "POST", body: JSON.stringify({ card: true }) });
|
|
4446
|
+
print4(
|
|
4447
|
+
body,
|
|
4448
|
+
() => body.url ? `Save a card for the free plan at:
|
|
4449
|
+
${body.url}
|
|
4450
|
+
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.
|
|
4451
|
+
` : "Nothing to save.\n"
|
|
4452
|
+
);
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4152
4455
|
case "top-up": {
|
|
4153
4456
|
const dollars = Number(amount);
|
|
4154
4457
|
if (amount === void 0 || !/^\d+$/.test(amount) || dollars <= 0) {
|
|
@@ -4196,19 +4499,27 @@ function dayOf(iso) {
|
|
|
4196
4499
|
}
|
|
4197
4500
|
function describeBilling(body) {
|
|
4198
4501
|
const lines = [];
|
|
4199
|
-
const
|
|
4200
|
-
if (
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
"
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4502
|
+
const free = body.free ?? null;
|
|
4503
|
+
if (free && body.plan) {
|
|
4504
|
+
lines.push(["Plan", "free: nothing a month. What runs is paid from your credits, at list price."]);
|
|
4505
|
+
if (!free.cardSaved) {
|
|
4506
|
+
lines.push(["Card", "none saved. Save one, charged nothing, to run sandboxes and be given the free plan's credit: agental billing card."]);
|
|
4507
|
+
} else if (!free.paid) {
|
|
4508
|
+
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.`]);
|
|
4509
|
+
}
|
|
4510
|
+
lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
|
|
4511
|
+
lines.push(["Credits left", `${money(body.topUpMicros)}, the free credit and top-ups. They do not expire.`]);
|
|
4512
|
+
if ((body.carriedMicros ?? 0) > 0) {
|
|
4513
|
+
lines.push(["Owed", `${money(body.carriedMicros ?? 0)} an earlier month ran past your credits, taken from the next top-up first`]);
|
|
4514
|
+
}
|
|
4515
|
+
if (body.autoReload) {
|
|
4516
|
+
lines.push(["Auto-reload", describeAutoReload(body.autoReload)]);
|
|
4517
|
+
} else if (body.canAutoReload) {
|
|
4518
|
+
lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
|
|
4519
|
+
}
|
|
4520
|
+
} else if (body.plan && (body.status === "active" || body.status === "past_due")) {
|
|
4521
|
+
lines.push(["Plan", `$${body.plan.dollarsPerMonth} a month, ${body.cancelAtPeriodEnd ? "ends" : "renews"} ${dayOf(body.period?.end ?? null)}`]);
|
|
4522
|
+
lines.push(["Used", `${money(body.usedMicros)} this month at list price${usedParts(body)}`]);
|
|
4212
4523
|
if ((body.carriedMicros ?? 0) > 0) {
|
|
4213
4524
|
lines.push(["Carried", `${money(body.carriedMicros ?? 0)} an earlier month could not pay, taken from this month's credits first`]);
|
|
4214
4525
|
}
|
|
@@ -4224,15 +4535,18 @@ function describeBilling(body) {
|
|
|
4224
4535
|
lines.push(["Auto-reload", "off. agental billing auto-reload <dollars> charges your saved card for a top-up when your credits run out."]);
|
|
4225
4536
|
}
|
|
4226
4537
|
} else {
|
|
4227
|
-
lines.push(["Plan",
|
|
4538
|
+
lines.push(["Plan", "none. Plans are not required here yet."]);
|
|
4228
4539
|
}
|
|
4229
4540
|
if (!body.canRun && body.reason) {
|
|
4230
|
-
lines.push([
|
|
4541
|
+
lines.push([
|
|
4542
|
+
"Running",
|
|
4543
|
+
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}`
|
|
4544
|
+
]);
|
|
4231
4545
|
}
|
|
4232
4546
|
if (body.lapse) {
|
|
4233
4547
|
lines.push([
|
|
4234
4548
|
"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
|
|
4549
|
+
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
4550
|
]);
|
|
4237
4551
|
}
|
|
4238
4552
|
if (body.testMode) {
|
|
@@ -4241,6 +4555,14 @@ function describeBilling(body) {
|
|
|
4241
4555
|
return lines.map(([label, value]) => `${label.padEnd(14)}${value}
|
|
4242
4556
|
`).join("");
|
|
4243
4557
|
}
|
|
4558
|
+
function usedParts(body) {
|
|
4559
|
+
const parts = [
|
|
4560
|
+
...(body.mailMicros ?? 0) > 0 ? [`${money(body.mailMicros ?? 0)} by email`] : [],
|
|
4561
|
+
...(body.stoppedMicros ?? 0) > 0 ? [`${money(body.stoppedMicros ?? 0)} by stopped sandboxes`] : [],
|
|
4562
|
+
...(body.snapshotMicros ?? 0) > 0 ? [`${money(body.snapshotMicros ?? 0)} by snapshots past the free ones`] : []
|
|
4563
|
+
];
|
|
4564
|
+
return parts.length > 0 ? `, ${parts.join(" and ")} of it` : "";
|
|
4565
|
+
}
|
|
4244
4566
|
function describeAutoReload(autoReload) {
|
|
4245
4567
|
if (!autoReload) {
|
|
4246
4568
|
return "off. Nothing is charged by itself when your credits run out.";
|
|
@@ -4257,7 +4579,14 @@ function describePrices(body) {
|
|
|
4257
4579
|
for (const tier of body.tiers) {
|
|
4258
4580
|
const sizes = tier.sizes === void 0 ? "" : tier.sizes.length === body.plans.length ? "every size" : tier.sizes.join(", ");
|
|
4259
4581
|
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
|
-
|
|
4582
|
+
const price = tier.dollarsPerMonth === 0 ? "Free" : `$${tier.dollarsPerMonth.toLocaleString("en-US")}`;
|
|
4583
|
+
const included = tier.dollarsPerMonth === 0 ? "pay as you go" : `${wholeMoney(tier.includedMicros)} of credits`;
|
|
4584
|
+
out.push(` ${price.padEnd(8)}${included.padEnd(28)}${speed.padEnd(34)}${sizes}`.trimEnd());
|
|
4585
|
+
}
|
|
4586
|
+
if (body.free) {
|
|
4587
|
+
out.push(
|
|
4588
|
+
` 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.`
|
|
4589
|
+
);
|
|
4261
4590
|
}
|
|
4262
4591
|
if (body.tiers.some((tier) => tier.runningUnits !== void 0)) {
|
|
4263
4592
|
out.push(" Counted in small sandboxes: a medium is 2, a large 4, a tiny half.");
|
|
@@ -4520,7 +4849,8 @@ Usage
|
|
|
4520
4849
|
agental create --name <name> [--template <template>] [--size tiny|small|medium|large] [--region <region>[,<region>...]] [--ttl <time> | --ttl default] [--idempotency-key <key>]
|
|
4521
4850
|
${operator ? "[--box <name-or-id>] " : ""}[--allow <address>[,<address>...]] [--max-burst-fee <dollars>]
|
|
4522
4851
|
[--ssh-key <public-key-or-file>]... [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]...
|
|
4523
|
-
agental
|
|
4852
|
+
agental summary [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--facets <field>[,<field>...]] [--json]
|
|
4853
|
+
agental list [--where '<words>'] [--tag <tag>[,<tag>...]]... [--meta <key>=<value>]... [--sort=<sort>] [--limit <n>] [--json]
|
|
4524
4854
|
agental tags <sandbox-id> [add|rm <tag>...] [--json]
|
|
4525
4855
|
agental meta <sandbox-id> [set <key>=<value>... | unset <key>...] [--json]
|
|
4526
4856
|
agental regions [--json]
|
|
@@ -4528,6 +4858,10 @@ Usage
|
|
|
4528
4858
|
agental delete <sandbox-id>
|
|
4529
4859
|
agental start <sandbox-id> [--max-burst-fee <dollars>]
|
|
4530
4860
|
agental stop <sandbox-id> [--keep-memory]
|
|
4861
|
+
agental stop --where '<words>' [--keep-memory] [--yes] every sandbox the words find, as one job
|
|
4862
|
+
agental start --where '<words>' [--yes]
|
|
4863
|
+
agental delete --where '<words>' [--confirm <count>] asks for the count to be typed
|
|
4864
|
+
agental jobs [list | show <job-id> | cancel <job-id>] [--json]
|
|
4531
4865
|
agental recover <sandbox-id> [--max-burst-fee <dollars>]
|
|
4532
4866
|
agental auto-stop <sandbox-id> [--in <time> | --at <date-time> | --never] [--json]
|
|
4533
4867
|
agental network <sandbox-id> [--allow <address>[,<address>...] | --none] [--json]
|
|
@@ -4537,7 +4871,6 @@ Usage
|
|
|
4537
4871
|
agental template rename <template> <new-name>
|
|
4538
4872
|
agental template delete <template>
|
|
4539
4873
|
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
4874
|
agental template network <template> [open | blocked | allowlist <address>...] [--json]
|
|
4542
4875
|
agental template ssh <template> [on | off] [--json]
|
|
4543
4876
|
agental template ssh-keys <template> [<public-key-or-file>... | --remove <key-id>] [--json]
|
|
@@ -4560,11 +4893,12 @@ Usage
|
|
|
4560
4893
|
agental data-retention [1 | 7 | 30 | 90 | never] [--json]
|
|
4561
4894
|
agental schedules <sandbox-id> [--json]
|
|
4562
4895
|
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...]
|
|
4896
|
+
[--if-stopped start|skip] [--keep-memory] [--overlap skip|replace|parallel] [--delete-after] [--json] -- <command> [args...]
|
|
4564
4897
|
agental schedule show|run|pause|resume|delete <sandbox-id> <schedule> [--json]
|
|
4565
4898
|
agental schedule runs <sandbox-id> <schedule> [--limit <n>] [--json]
|
|
4566
4899
|
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] [--
|
|
4900
|
+
[--if-stopped start|skip] [--keep-memory | --disk-only] [--overlap skip|replace|parallel] [--delete-after | --keep-sandbox] [--json]
|
|
4901
|
+
[-- <command> [args...]]
|
|
4568
4902
|
agental webhooks [--json]
|
|
4569
4903
|
agental webhook add <url> (--events <kind>[,<kind>...] | --all) [--description <text>] [--json]
|
|
4570
4904
|
agental webhook show <webhook-id> [--json]
|
|
@@ -4584,7 +4918,7 @@ Usage
|
|
|
4584
4918
|
agental key show <key> [--json]
|
|
4585
4919
|
agental key allow-from <key> (<address-or-range>... | --anywhere) [--json]
|
|
4586
4920
|
agental key delete <key>
|
|
4587
|
-
agental billing [prices | top-up <dollars> | auto-reload <dollars | off>] [--json]
|
|
4921
|
+
agental billing [prices | card | top-up <dollars> | auto-reload <dollars | off>] [--json]
|
|
4588
4922
|
agental burst [budget <dollars> | budget default] [--json]
|
|
4589
4923
|
agental account export [--out <file>]
|
|
4590
4924
|
agental ssh <sandbox-id> [--tmux] [ssh options] [command...]
|
|
@@ -4647,7 +4981,7 @@ sandbox is not restarted, and what is sent to it, commands, files and SSH, is no
|
|
|
4647
4981
|
create --allow gives a new sandbox its own from its first boot. These work where GET
|
|
4648
4982
|
/v1/policy says networkPolicy.
|
|
4649
4983
|
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
|
|
4984
|
+
machine), its variables and files, and its outbound rule. Every account has one named default,
|
|
4651
4985
|
which a sandbox is on when create names none. <template> is a template's id or its name. A
|
|
4652
4986
|
sandbox's commands get its template's variables and files as they were when it last started,
|
|
4653
4987
|
with its own on top: secrets <sandbox-id> sets its own, and secrets --template sets a
|
|
@@ -4700,8 +5034,10 @@ schedules lists the commands a sandbox runs on a schedule, with when each runs n
|
|
|
4700
5034
|
last run ended. schedule add makes one: what comes after -- is its command, run by sh -c, and
|
|
4701
5035
|
--every 5m, 15m, 1h or 1d, counted from now, never more often than every 5 minutes, or --cron
|
|
4702
5036
|
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
|
-
|
|
5037
|
+
runs it once, as for a check to come back to. With --at, --delete-after deletes the sandbox after
|
|
5038
|
+
the run, if the command exits 0, with a key that may delete it; a run that fails leaves it to be
|
|
5039
|
+
looked at, and update --keep-sandbox takes it back. When a run finds the sandbox stopped it
|
|
5040
|
+
starts it, runs, and stops it again when the command ends, keeping its disk, or its memory too with
|
|
4705
5041
|
--keep-memory; with --if-stopped skip it runs only while the sandbox runs. A sandbox someone
|
|
4706
5042
|
started again, sent a command or moved the stop of meanwhile is left running. While a run still
|
|
4707
5043
|
goes the next is skipped; --overlap replace stops it instead, and parallel runs both. A run gets
|
|
@@ -4725,8 +5061,10 @@ tries.
|
|
|
4725
5061
|
org lists the organizations you are in, your own and your teams, and marks the one this command
|
|
4726
5062
|
acts in. A key acts in the organization it was made in, for good, so org use signs in again in
|
|
4727
5063
|
the browser for a key made in the one given, by its id or name, and deletes the key the command
|
|
4728
|
-
had.
|
|
4729
|
-
|
|
5064
|
+
had. Your own organization takes members on the $500 or $2000 plan, with you its only owner.
|
|
5065
|
+
org create makes a team besides, with you as its owner and a plan of its own; it has members
|
|
5066
|
+
besides its owners on the $500 or $2000 plan. members lists the organization the command acts
|
|
5067
|
+
in, your own or a team, and members remove takes one
|
|
4730
5068
|
out by id or email; keys they made in it go on working, as a member's, until they are revoked,
|
|
4731
5069
|
and --revoke-keys revokes them in the same step. invite asks an email address in, as a member or
|
|
4732
5070
|
with --role admin; the invitation is open for seven days.
|
|
@@ -4754,8 +5092,12 @@ server it is on, which changes when it moves. On a network with IPv6 the API see
|
|
|
4754
5092
|
address, so a key limited to IPv4 addresses alone is refused there.
|
|
4755
5093
|
billing says where your account stands: its plan, what it has used this month at list price,
|
|
4756
5094
|
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
|
-
|
|
5095
|
+
after it and do not expire. The free plan costs nothing a month and pays for what runs from
|
|
5096
|
+
credits alone; it makes tiny and small sandboxes. prices lists the plans with the sizes each
|
|
5097
|
+
makes, the top-ups with their bonus and the hourly price of each size. card prints the address
|
|
5098
|
+
of a page to save a card for the free plan: nothing is charged, and once it is saved the account
|
|
5099
|
+
is given $10 of credit, once. Until it buys a top-up it runs 2 small sandboxes at once, then 20.
|
|
5100
|
+
top-up <dollars> starts buying credits for
|
|
4759
5101
|
any whole number of dollars from 15 to 100,000, and prints the address of the page to pay on.
|
|
4760
5102
|
From $200 a top-up adds a bonus, 2% there and growing to 25% at $10,000: $500 adds $537.
|
|
4761
5103
|
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.3.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
|
},
|