@easybits.cloud/cli 0.9.1 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/index.js +283 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -91,7 +91,9 @@ easybits agents restart helper
91
91
  easybits agents try helper "who are you?" # one full turn as text: verify what you set up
92
92
  easybits agents logs helper --since "10 min ago"
93
93
  easybits agents doctor helper # exits 1 on a problem; status "error" shows why it did not start
94
- easybits agents export helper --out helper.json [--show-secrets] [--with-files]
94
+ easybits agents export helper --out ./helper # the agent as a folder: agent.yaml + skills/ + files/
95
+ easybits apply ./helper --dry-run # after editing: the plan (+ ~ - !); --prune also removes
96
+ easybits apply ./helper --create --name helper-2 # a new agent from the folder
95
97
  easybits agents create --like helper --name helper-2 [--copy-files] --dry-run # clone its setup, never its env
96
98
  easybits agents create --from helper.json --name helper-3
97
99
 
@@ -157,6 +159,15 @@ Host *.ghosty
157
159
 
158
160
  Full reference: https://www.easybits.cloud/docs#cli · https://www.easybits.cloud/en/docs/cli.md
159
161
 
162
+ ## Changes in 0.10.0
163
+
164
+ - **The agent as a file**: `agents export <agent> [--out DIR|file.yaml|file.json]` and
165
+ `easybits apply <file|dir> [--agent X] [--create] [--prune] [--dry-run] [--yes]`. Declarative
166
+ plan computed by the server (`+ ~ - !`; removing needs `--prune`), secrets exported as `${NAME}`
167
+ and filled from your environment, previous file saved to `~/.easybits/backups/`.
168
+ `create --from <file>` accepts the new files too.
169
+ - `agents create --timeout` is now `--ttl` (the old flag still works and says so).
170
+
160
171
  ## Changes in 0.9.0
161
172
 
162
173
  - Configure agents with a machine (ghosty-lite, goose) from the terminal, same contract as
package/dist/index.js CHANGED
@@ -134,9 +134,9 @@ async function readEnvFile(path, usage2) {
134
134
  }
135
135
  const out = {};
136
136
  for (const [n, raw] of text.split(/\r?\n/).entries()) {
137
- const line = raw.trim();
138
- if (!line || line.startsWith("#")) continue;
139
- const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line);
137
+ const line2 = raw.trim();
138
+ if (!line2 || line2.startsWith("#")) continue;
139
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line2);
140
140
  if (!m) throw usageError(`${path === "-" ? "stdin" : path}:${n + 1}: expected KEY=VALUE.`, usage2);
141
141
  let v = m[2];
142
142
  if (v.length >= 2 && (v[0] === '"' || v[0] === "'") && v.endsWith(v[0])) v = v.slice(1, -1);
@@ -175,9 +175,9 @@ function table(rows, cols, empty = "Nothing here yet.") {
175
175
  const widths = cols.map(
176
176
  ([k, label]) => Math.min(48, Math.max(label.length, ...rows.map((r) => cell(r[k]).length)))
177
177
  );
178
- const line = (vals) => vals.map((v, i) => i === vals.length - 1 ? v : v.slice(0, widths[i]).padEnd(widths[i])).join(" ").trimEnd();
179
- console.log(line(cols.map(([, l]) => l)));
180
- for (const r of rows) console.log(line(cols.map(([k]) => cell(r[k]))));
178
+ const line2 = (vals) => vals.map((v, i) => i === vals.length - 1 ? v : v.slice(0, widths[i]).padEnd(widths[i])).join(" ").trimEnd();
179
+ console.log(line2(cols.map(([, l]) => l)));
180
+ for (const r of rows) console.log(line2(cols.map(([k]) => cell(r[k]))));
181
181
  }
182
182
  function pickFields(rec, fields) {
183
183
  if (!fields?.length) return rec;
@@ -260,7 +260,7 @@ async function oauthLogin(ctx, opts = {}) {
260
260
  const challenge = b64url(createHash("sha256").update(verifier).digest());
261
261
  const state = b64url(randomBytes(16));
262
262
  const server = createServer();
263
- await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
263
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
264
264
  const port = server.address().port;
265
265
  const redirectUri = `http://127.0.0.1:${port}/cb`;
266
266
  try {
@@ -274,7 +274,7 @@ async function oauthLogin(ctx, opts = {}) {
274
274
  code_challenge: challenge,
275
275
  code_challenge_method: "S256"
276
276
  });
277
- const code = await new Promise((resolve2, reject) => {
277
+ const code = await new Promise((resolve3, reject) => {
278
278
  const timer = setTimeout(
279
279
  () => reject(new CliError("Login timed out after 5 minutes.", EXIT.AUTH, "Run: easybits login", "login_timeout")),
280
280
  LOGIN_TIMEOUT_MS
@@ -290,7 +290,7 @@ async function oauthLogin(ctx, opts = {}) {
290
290
  clearTimeout(timer);
291
291
  if (ok) {
292
292
  res.writeHead(302, { location: `${base}/oauth/listo` }).end();
293
- resolve2(got);
293
+ resolve3(got);
294
294
  } else {
295
295
  res.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(FAIL_PAGE(base));
296
296
  reject(
@@ -950,8 +950,8 @@ var sshProxy = {
950
950
  const { url } = await sb.sshTicket();
951
951
  const ws = new WebSocket(url);
952
952
  ws.binaryType = "arraybuffer";
953
- await new Promise((resolve2, reject) => {
954
- ws.onopen = () => resolve2();
953
+ await new Promise((resolve3, reject) => {
954
+ ws.onopen = () => resolve3();
955
955
  ws.onerror = () => reject(new CliError("easybits ssh-proxy: could not open the tunnel"));
956
956
  });
957
957
  ws.onerror = () => {
@@ -1673,7 +1673,7 @@ var db = {
1673
1673
  };
1674
1674
 
1675
1675
  // src/commands/agents.ts
1676
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1676
+ import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync4 } from "fs";
1677
1677
 
1678
1678
  // src/api.ts
1679
1679
  import { EasybitsError as EasybitsError2, resolveBaseUrl as resolveBaseUrl4 } from "@easybits.cloud/sdk";
@@ -1722,9 +1722,9 @@ function skillFrontmatter(md) {
1722
1722
  if (!m) return { error: "SKILL.md has no frontmatter (--- name / description ---)" };
1723
1723
  const out = {};
1724
1724
  for (const key of ["name", "description"]) {
1725
- const line = m[1].split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
1726
- if (!line) return { ...out, error: `frontmatter has no \`${key}:\`` };
1727
- const value = line.slice(key.length + 1).trim();
1725
+ const line2 = m[1].split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
1726
+ if (!line2) return { ...out, error: `frontmatter has no \`${key}:\`` };
1727
+ const value = line2.slice(key.length + 1).trim();
1728
1728
  if (!value) return { ...out, error: `\`${key}:\` is empty` };
1729
1729
  const quoted = /^(["']).*\1$/.test(value);
1730
1730
  if (!quoted && value.includes(": ")) return { ...out, error: `\`${key}:\` has an unquoted ": " \u2014 quote the value or the skill is dropped` };
@@ -2412,7 +2412,240 @@ async function applyExport(ctx, id, x, opts) {
2412
2412
  return copied;
2413
2413
  }
2414
2414
 
2415
+ // src/commands/spec.ts
2416
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
2417
+ import { homedir as homedir3 } from "os";
2418
+ import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
2419
+
2420
+ // src/specfile.ts
2421
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
2422
+ import { join as join4, relative as relative2 } from "path";
2423
+ function walkLocal(root) {
2424
+ const out = [];
2425
+ const visit = (d) => {
2426
+ for (const e of readdirSync2(d, { withFileTypes: true })) {
2427
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
2428
+ const p = join4(d, e.name);
2429
+ if (e.isDirectory()) visit(p);
2430
+ else if (e.isFile()) out.push(relative2(root, p).split("\\").join("/"));
2431
+ }
2432
+ };
2433
+ if (existsSync7(root)) visit(root);
2434
+ return out;
2435
+ }
2436
+ function readUploads(dir) {
2437
+ const skills2 = {};
2438
+ const skillsRoot = join4(dir, "skills");
2439
+ if (existsSync7(skillsRoot)) {
2440
+ for (const e of readdirSync2(skillsRoot, { withFileTypes: true })) {
2441
+ if (!e.isDirectory() || e.name.startsWith(".")) continue;
2442
+ skills2[e.name] = walkLocal(join4(skillsRoot, e.name)).map((path) => ({ path, contentBase64: readFileSync7(join4(skillsRoot, e.name, path)).toString("base64") }));
2443
+ }
2444
+ }
2445
+ const filesRoot = join4(dir, "files");
2446
+ const files3 = Object.fromEntries(walkLocal(filesRoot).map((p) => [p, readFileSync7(join4(filesRoot, p)).toString("base64")]));
2447
+ return { ...Object.keys(skills2).length ? { skills: skills2 } : {}, ...Object.keys(files3).length ? { files: files3 } : {} };
2448
+ }
2449
+ function secretsFor(text, env = process.env) {
2450
+ const names = [...new Set([...text.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((m) => m[1]))];
2451
+ const secrets = {};
2452
+ const missing = [];
2453
+ for (const n of names) env[n] !== void 0 ? secrets[n] = env[n] : missing.push(n);
2454
+ return { secrets, missing };
2455
+ }
2456
+ function specId(text) {
2457
+ return /^id:\s*["']?([^"'\s#]+)/m.exec(text)?.[1] ?? /^\s{0,2}"id"\s*:\s*"([^"]+)"/m.exec(text)?.[1];
2458
+ }
2459
+
2460
+ // src/commands/spec.ts
2461
+ var SPEC_NAMES = ["agent.yaml", "agent.yml", "agent.json"];
2462
+ var isFileTarget = (p) => /\.(ya?ml|json)$/i.test(p);
2463
+ var exportSpec = {
2464
+ summary: "Export an agent as a file you can edit and apply (secrets as ${NAME}, never their value)",
2465
+ usage: "easybits agents export <agent> [--out <dir | file.yaml | file.json>]",
2466
+ options: {
2467
+ out: { type: "string", value: "path", description: "A folder (agent.yaml + skills/ + files/) or a .yaml/.json file" },
2468
+ // El formato de clonado de 0.9 (JSON con contenidos), para `create --from` viejo.
2469
+ "show-secrets": { type: "boolean", description: "Legacy clone JSON (0.9) with MCP values unmasked" },
2470
+ "with-files": { type: "boolean", description: "Legacy clone JSON (0.9) with the file contents" }
2471
+ },
2472
+ examples: [
2473
+ "easybits agents export helper > helper.yaml",
2474
+ "easybits agents export helper --out ./helper # agent.yaml + skills/ + files/, versionable in git",
2475
+ "easybits apply ./helper --dry-run # after editing: see the plan"
2476
+ ],
2477
+ async run(ctx) {
2478
+ if (bool(ctx, "show-secrets") || bool(ctx, "with-files")) return exportAgent.run.call(exportAgent, ctx);
2479
+ const id = need(ctx, 0, "agent", this.usage);
2480
+ const out = str(ctx, "out");
2481
+ const getYaml = () => api(ctx, "GET", `/agents/${id}/export`);
2482
+ const getJson = async () => (await api(ctx, "GET", `/agents/${id}/export?format=json`)).spec;
2483
+ if (!out) {
2484
+ if (ctx.json) return emit(ctx, await getJson(), () => {
2485
+ });
2486
+ process.stdout.write(await getYaml());
2487
+ return;
2488
+ }
2489
+ if (isFileTarget(out)) {
2490
+ writeFileSync5(out, /\.json$/i.test(out) ? JSON.stringify(await getJson(), null, 2) + "\n" : await getYaml());
2491
+ emit(ctx, { saved: resolve2(out) }, () => console.log(`Saved ${resolve2(out)}
2492
+ Edit it, then: easybits apply ${out} --dry-run`));
2493
+ return;
2494
+ }
2495
+ const yaml = await getYaml();
2496
+ const dir = resolve2(out);
2497
+ mkdirSync4(dir, { recursive: true });
2498
+ writeFileSync5(join5(dir, "agent.yaml"), yaml);
2499
+ const x = await buildExport(ctx, id, { secrets: false, fileContents: true });
2500
+ const put = (path, b64) => {
2501
+ mkdirSync4(dirname2(path), { recursive: true });
2502
+ writeFileSync5(path, Buffer.from(b64, "base64"));
2503
+ };
2504
+ for (const s of x.skills ?? []) {
2505
+ writeFileSync5((mkdirSync4(join5(dir, "skills", s.slug), { recursive: true }), join5(dir, "skills", s.slug, "SKILL.md")), s.markdown);
2506
+ for (const a of s.assets) put(join5(dir, "skills", s.slug, a.name), a.contentBase64);
2507
+ }
2508
+ for (const f of x.files ?? []) if (f.contentBase64 !== void 0) put(join5(dir, "files", f.path), f.contentBase64);
2509
+ const summary = { saved: dir, skills: (x.skills ?? []).map((s) => s.slug), files: (x.files ?? []).map((f) => f.path) };
2510
+ emit(
2511
+ ctx,
2512
+ summary,
2513
+ () => console.log(
2514
+ `Saved ${dir}/agent.yaml${summary.skills.length ? ` + skills/ (${summary.skills.join(", ")})` : ""}${summary.files.length ? ` + files/ (${summary.files.length})` : ""}
2515
+ Edit it, then: easybits apply ${out} --dry-run`
2516
+ )
2517
+ );
2518
+ }
2519
+ };
2520
+ function readTarget(target, usage2) {
2521
+ if (!existsSync8(target)) throw usageError(`Not found: ${target}`, usage2);
2522
+ if (statSync3(target).isDirectory()) {
2523
+ const name = SPEC_NAMES.find((n) => existsSync8(join5(target, n)));
2524
+ if (!name) throw usageError(`${target} has no agent.yaml.`, usage2);
2525
+ return { text: readFileSync8(join5(target, name), "utf8"), dir: target };
2526
+ }
2527
+ return { text: readFileSync8(target, "utf8") };
2528
+ }
2529
+ var line = (c) => `${c.op} ${c.what}`;
2530
+ var printPlan = (plan) => console.log(plan.map(line).join("\n"));
2531
+ function saveBackup(id, yaml) {
2532
+ const dir = join5(homedir3(), ".easybits", "backups");
2533
+ mkdirSync4(dir, { recursive: true });
2534
+ const path = join5(dir, `${id}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.yaml`);
2535
+ writeFileSync5(path, yaml);
2536
+ return path;
2537
+ }
2538
+ async function applyTarget(ctx, target, o) {
2539
+ const { text, dir } = readTarget(target, o.usage);
2540
+ const { secrets, missing } = secretsFor(text);
2541
+ const uploads = dir ? readUploads(dir) : {};
2542
+ if (o.create) {
2543
+ if (o.agent) throw usageError("Use --agent or --create, not both.", o.usage);
2544
+ const body2 = { spec: text, secrets, ...o.name ? { name: o.name } : {} };
2545
+ const plan2 = await api(ctx, "POST", "/agents/apply", { ...body2, dryRun: true });
2546
+ if (o.dryRun) {
2547
+ emit(ctx, { create: true, ...plan2, ...uploads.skills ? { skills: Object.keys(uploads.skills) } : {}, ...uploads.files ? { files: Object.keys(uploads.files) } : {} }, () => {
2548
+ printPlan(plan2.plan);
2549
+ for (const s of Object.keys(uploads.skills ?? {})) console.log(`+ skill ${s}`);
2550
+ for (const f of Object.keys(uploads.files ?? {})) console.log(`+ file ${f}`);
2551
+ console.log("(dry run: nothing created)");
2552
+ });
2553
+ return;
2554
+ }
2555
+ const created = await api(ctx, "POST", "/agents/apply", body2);
2556
+ const id2 = created.agentId;
2557
+ if (!ctx.json) process.stderr.write(`Created ${id2}; applying the rest\u2026
2558
+ `);
2559
+ let rest;
2560
+ if (/\n?(skills|files):/m.test(text) || uploads.skills || uploads.files) {
2561
+ await waitMachine(ctx, id2).catch(() => {
2562
+ });
2563
+ rest = await api(ctx, "POST", `/agents/${id2}/apply`, { spec: text, secrets, ...uploads, ...o.name ? { name: o.name } : {} });
2564
+ }
2565
+ const extra = (rest?.plan ?? []).filter((c) => c.op !== "!" && !/^(prompt|env)\b/.test(c.what));
2566
+ const res = { agentId: id2, sandboxId: created.sandboxId, plan: [...created.plan, ...extra], applied: rest?.applied, failed: rest?.failed };
2567
+ emit(ctx, res, () => {
2568
+ printPlan(res.plan);
2569
+ console.log(`Created ${id2}.${res.failed ? "" : ` Try it: easybits agents try ${id2} "hello"`}`);
2570
+ if (res.failed) console.error(`Stopped at \xAB${res.failed.what}\xBB: ${res.failed.error}`);
2571
+ });
2572
+ if (res.failed) process.exitCode = EXIT.API;
2573
+ return;
2574
+ }
2575
+ const ref = o.agent ?? specId(text);
2576
+ if (!ref) throw usageError("The file has no agent id.", "Point it at one with --agent <name|id>, or make a new one with --create.");
2577
+ const id = await resolveRef(ctx, "agent", ref);
2578
+ if (missing.length && !ctx.json) process.stderr.write(`Not in your environment (keep their current value): ${missing.join(", ")}
2579
+ `);
2580
+ const path = `/agents/${id}/apply`;
2581
+ const body = { spec: text, secrets, ...uploads, ...o.prune ? { prune: true } : {} };
2582
+ const plan = await api(ctx, "POST", path, { ...body, dryRun: true });
2583
+ const actionable = plan.plan.filter((c) => c.op !== "!");
2584
+ if (o.dryRun || !actionable.length) {
2585
+ emit(ctx, plan, () => {
2586
+ if (plan.plan.length) printPlan(plan.plan);
2587
+ console.log(actionable.length ? "(dry run: nothing changed)" : plan.plan.length ? "Nothing to apply (see the ! lines)." : "No changes: the agent already matches the file.");
2588
+ });
2589
+ return;
2590
+ }
2591
+ requireYesIfHeadless(ctx);
2592
+ await confirm(ctx, `${plan.plan.map(line).join("\n")}
2593
+ Apply ${actionable.length} change(s) to ${id}?`);
2594
+ const r = await api(ctx, "POST", path, body);
2595
+ const backup = r.before ? saveBackup(id, r.before) : void 0;
2596
+ const { before: _b, ...shown } = r;
2597
+ emit(ctx, { ...shown, ...backup ? { backup } : {} }, () => {
2598
+ console.log(`Applied ${r.applied?.length ?? 0} change(s) to ${id}.`);
2599
+ if (backup) console.log(`Before (to roll back: easybits apply ${backup} --agent ${id}): ${backup}`);
2600
+ if (r.failed) console.error(`Stopped at \xAB${r.failed.what}\xBB: ${r.failed.error}`);
2601
+ });
2602
+ if (r.failed) process.exitCode = EXIT.API;
2603
+ }
2604
+ var apply = {
2605
+ name: "apply",
2606
+ group: "Compute",
2607
+ summary: "Make an agent match a file from `agents export` (plan, then apply)",
2608
+ synopsis: "apply <file|dir>",
2609
+ leaf: {
2610
+ summary: "Apply an agent file or folder: shows the plan (+ add, ~ change, - remove, ! skipped), then applies it",
2611
+ usage: "easybits apply <file|dir> [--agent <agent>] [--create [--name <name>]] [--prune] [--dry-run] [--yes]",
2612
+ options: {
2613
+ agent: { type: "string", value: "agent", description: "Apply to this agent (default: the id in the file)" },
2614
+ create: { type: "boolean", description: "Make a new agent from the file instead" },
2615
+ name: { type: "string", value: "name", description: "--create: name for the new agent" },
2616
+ prune: { type: "boolean", description: "Also remove what the file doesn't list (env, MCP, skills, files)" },
2617
+ "dry-run": { type: "boolean", description: "Show the plan; change nothing" },
2618
+ ...YES_OPTION
2619
+ },
2620
+ examples: [
2621
+ "easybits agents export helper --out ./helper",
2622
+ "easybits apply ./helper --dry-run",
2623
+ "ANTHROPIC_API_KEY=\u2026 easybits apply ./helper --yes # ${ANTHROPIC_API_KEY} in the file gets this value",
2624
+ "easybits apply ./helper --create --name helper-2 # a copy",
2625
+ "easybits apply helper.yaml --prune --dry-run # what would be removed"
2626
+ ],
2627
+ async run(ctx) {
2628
+ const target = need(ctx, 0, "file|dir", this.usage);
2629
+ await applyTarget(ctx, target, {
2630
+ agent: str(ctx, "agent"),
2631
+ create: bool(ctx, "create"),
2632
+ name: str(ctx, "name"),
2633
+ prune: bool(ctx, "prune"),
2634
+ dryRun: bool(ctx, "dry-run"),
2635
+ usage: this.usage
2636
+ });
2637
+ }
2638
+ }
2639
+ };
2640
+
2415
2641
  // src/commands/agents.ts
2642
+ function isLegacyExport(path) {
2643
+ try {
2644
+ return existsSync9(path) && !statSync4(path).isDirectory() && /"kind"\s*:\s*"easybits\.agent"/.test(readFileSync9(path, "utf8"));
2645
+ } catch {
2646
+ return false;
2647
+ }
2648
+ }
2416
2649
  var agents = {
2417
2650
  name: "agents",
2418
2651
  aliases: ["agent"],
@@ -2443,7 +2676,7 @@ var agents = {
2443
2676
  create: {
2444
2677
  aliases: ["new"],
2445
2678
  summary: "Create an agent from a template, or clone one (--like / --from)",
2446
- usage: "easybits agents create (--template <template> | --like <agent> | --from <export.json>) [--name <name>] [--prompt <text> | --prompt-file <file>] [--prompt-mode append|replace] [--mcp-file <json>] [--copy-files] [--dotenv <path>] [--env K=V]... [--timeout <s>] [--dry-run]",
2679
+ usage: "easybits agents create (--template <template> | --like <agent> | --from <export.json>) [--name <name>] [--prompt <text> | --prompt-file <file>] [--prompt-mode append|replace] [--mcp-file <json>] [--copy-files] [--dotenv <path>] [--env K=V]... [--ttl <s>] [--dry-run]",
2447
2680
  options: {
2448
2681
  template: { type: "string", value: "template", description: "Agent template (see: easybits docs agents)" },
2449
2682
  like: { type: "string", value: "agent", description: "Clone another agent's setup: template, prompt, MCP, skills (never its env)" },
@@ -2456,7 +2689,7 @@ var agents = {
2456
2689
  "copy-files": { type: "boolean", description: `--like/--from: also copy the knowledge files in /data/work` },
2457
2690
  env: { type: "string", multiple: true, value: "K=V", description: "Env for the agent, non-secret (repeatable)" },
2458
2691
  ...ENV_FILE_OPTION,
2459
- timeout: { type: "string", value: "seconds", description: "Lifetime before auto-destroy" },
2692
+ ttl: { type: "string", value: "seconds", description: "Lifetime before auto-destroy (was --timeout)" },
2460
2693
  "dry-run": { type: "boolean", description: "Print the plan; create nothing" }
2461
2694
  },
2462
2695
  examples: [
@@ -2475,15 +2708,19 @@ var agents = {
2475
2708
  if (promptInline != null && promptFile) throw usageError("Use --prompt or --prompt-file, not both.", this.usage);
2476
2709
  const mode = str(ctx, "prompt-mode");
2477
2710
  if (mode && mode !== "append" && mode !== "replace") throw usageError("--prompt-mode must be append or replace.", this.usage);
2478
- if (promptFile && !existsSync7(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
2711
+ if (promptFile && !existsSync9(promptFile)) throw usageError(`File not found: ${promptFile}`, this.usage);
2479
2712
  const mcpFile = str(ctx, "mcp-file");
2480
- if (mcpFile && !existsSync7(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
2713
+ if (mcpFile && !existsSync9(mcpFile)) throw usageError(`File not found: ${mcpFile}`, this.usage);
2481
2714
  if (bool(ctx, "copy-files") && !like && !from) throw usageError("--copy-files needs --like or --from.", this.usage);
2715
+ if (from && !isLegacyExport(from)) {
2716
+ await applyTarget(ctx, from, { create: true, name: str(ctx, "name"), dryRun: bool(ctx, "dry-run"), usage: this.usage });
2717
+ return;
2718
+ }
2482
2719
  const source = from ? readExport(from, this.usage) : like ? await buildExport(ctx, await resolveRef(ctx, "agent", like), { secrets: true, fileContents: bool(ctx, "copy-files") }) : void 0;
2483
2720
  const template = str(ctx, "template") ?? source?.template;
2484
2721
  if (!template) throw usageError("Missing --template (or --like / --from).", this.usage);
2485
2722
  const env = await envFrom(ctx, this.usage);
2486
- const prompt = promptInline ?? (promptFile ? readFileSync7(promptFile, "utf8") : source?.systemPrompt);
2723
+ const prompt = promptInline ?? (promptFile ? readFileSync9(promptFile, "utf8") : source?.systemPrompt);
2487
2724
  const promptMode = mode ?? source?.systemPromptMode;
2488
2725
  const multiline = !!prompt && /[\r\n]/.test(prompt.trimEnd());
2489
2726
  const promptAfter = multiline ? prompt : void 0;
@@ -2492,7 +2729,7 @@ var agents = {
2492
2729
  }
2493
2730
  if (prompt && !multiline) env.SYSTEM_PROMPT = prompt.trimEnd();
2494
2731
  if (promptMode && (prompt || mode) && !multiline) env.SYSTEM_PROMPT_MODE = promptMode;
2495
- const mcpServers = mcpFile ? parseMcpFile(readFileSync7(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2732
+ const mcpServers = mcpFile ? parseMcpFile(readFileSync9(mcpFile, "utf8"), this.usage) : source?.mcpServers;
2496
2733
  if (mcpServers && hasMasked(mcpServers)) {
2497
2734
  throw usageError(`${from ?? "the source"} has masked MCP values (\u2026***).`, "Re-export with --show-secrets, or pass --mcp-file");
2498
2735
  }
@@ -2520,7 +2757,7 @@ var agents = {
2520
2757
  template,
2521
2758
  name,
2522
2759
  env,
2523
- timeoutSeconds: int(ctx, "timeout", this.usage),
2760
+ timeoutSeconds: int(ctx, "ttl", this.usage),
2524
2761
  ...mcpServers?.length ? { mcpServers } : {}
2525
2762
  });
2526
2763
  if (promptAfter || source?.skills?.length || bool(ctx, "copy-files") && source?.files?.length) await waitMachine(ctx, a.agentId);
@@ -2579,7 +2816,7 @@ var agents = {
2579
2816
  try: tryTurn,
2580
2817
  logs,
2581
2818
  doctor,
2582
- export: exportAgent
2819
+ export: exportSpec
2583
2820
  }
2584
2821
  };
2585
2822
 
@@ -2630,16 +2867,16 @@ var docs = {
2630
2867
  };
2631
2868
 
2632
2869
  // src/commands/doctor.ts
2633
- import { existsSync as existsSync8, statSync as statSync3 } from "fs";
2634
- import { homedir as homedir4 } from "os";
2635
- import { join as join5 } from "path";
2870
+ import { existsSync as existsSync10, statSync as statSync5 } from "fs";
2871
+ import { homedir as homedir5 } from "os";
2872
+ import { join as join7 } from "path";
2636
2873
  import { resolveBaseUrl as resolveBaseUrl6 } from "@easybits.cloud/sdk";
2637
2874
 
2638
2875
  // src/update.ts
2639
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
2640
- import { homedir as homedir3 } from "os";
2641
- import { dirname as dirname2, join as join4 } from "path";
2642
- var FILE = join4(process.env.XDG_CONFIG_HOME || join4(homedir3(), ".config"), "easybits", "update.json");
2876
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
2877
+ import { homedir as homedir4 } from "os";
2878
+ import { dirname as dirname3, join as join6 } from "path";
2879
+ var FILE = join6(process.env.XDG_CONFIG_HOME || join6(homedir4(), ".config"), "easybits", "update.json");
2643
2880
  var DAY = 24 * 60 * 60 * 1e3;
2644
2881
  var UPGRADE = "npm i -g @easybits.cloud/cli@latest";
2645
2882
  function newer(a, b) {
@@ -2651,7 +2888,7 @@ function newer(a, b) {
2651
2888
  var updateCheckDisabled = () => Boolean(process.env.EASYBITS_NO_UPDATE_CHECK || process.env.CI);
2652
2889
  function readCache() {
2653
2890
  try {
2654
- return JSON.parse(readFileSync8(FILE, "utf8"));
2891
+ return JSON.parse(readFileSync10(FILE, "utf8"));
2655
2892
  } catch {
2656
2893
  return {};
2657
2894
  }
@@ -2661,8 +2898,8 @@ async function fetchLatest(timeoutMs = 1500) {
2661
2898
  const r = await fetch("https://registry.npmjs.org/@easybits.cloud/cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
2662
2899
  const latest = (await r.json()).version;
2663
2900
  if (!latest) return null;
2664
- mkdirSync4(dirname2(FILE), { recursive: true });
2665
- writeFileSync5(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
2901
+ mkdirSync5(dirname3(FILE), { recursive: true });
2902
+ writeFileSync6(FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
2666
2903
  return latest;
2667
2904
  } catch {
2668
2905
  return null;
@@ -2686,7 +2923,7 @@ async function updateNotice(current) {
2686
2923
  }
2687
2924
 
2688
2925
  // src/commands/doctor.ts
2689
- var VERSION = true ? "0.9.1" : "dev";
2926
+ var VERSION = true ? "0.10.0" : "dev";
2690
2927
  var doctor2 = {
2691
2928
  name: "doctor",
2692
2929
  group: "Account",
@@ -2729,9 +2966,9 @@ var doctor2 = {
2729
2966
  checks.push({ check: "credential-env", ok: true, detail: "EASYBITS_API_KEY wins over the saved login in ~/.easybitsrc" });
2730
2967
  }
2731
2968
  }
2732
- const rcPath = join5(homedir4(), ".easybitsrc");
2733
- if (existsSync8(rcPath)) {
2734
- const mode = statSync3(rcPath).mode & 511;
2969
+ const rcPath = join7(homedir5(), ".easybitsrc");
2970
+ if (existsSync10(rcPath)) {
2971
+ const mode = statSync5(rcPath).mode & 511;
2735
2972
  const ok2 = (mode & 63) === 0;
2736
2973
  checks.push({ check: "rc-file", ok: ok2, detail: `${rcPath} ${mode.toString(8).padStart(3, "0")}`, ...ok2 ? {} : { hint: `It holds tokens. Run: chmod 600 ${rcPath}` } });
2737
2974
  }
@@ -2757,6 +2994,7 @@ var COMMANDS = [
2757
2994
  usage,
2758
2995
  sandboxes,
2759
2996
  agents,
2997
+ apply,
2760
2998
  machines,
2761
2999
  domains,
2762
3000
  init,
@@ -2900,6 +3138,7 @@ function positionals(argv2) {
2900
3138
  }
2901
3139
  var MACHINE_SUBS = ["ls", "list", "deploy", "release", "releases", "logs", "rollback", "secrets"];
2902
3140
  var SANDBOX_NAMES = ["sandboxes", "sandbox", "sb"];
3141
+ var AGENT_NAMES = ["agents", "agent"];
2903
3142
  function normalizeArgs(input) {
2904
3143
  const argv2 = [...input];
2905
3144
  const notes = [];
@@ -2933,12 +3172,13 @@ function normalizeArgs(input) {
2933
3172
  argv2[pos[1]] = "deploy";
2934
3173
  notes.push("`machines release` is now `machines deploy`.");
2935
3174
  }
2936
- if (SANDBOX_NAMES.includes(at(0) ?? "") && (at(1) === "create" || at(1) === "new")) {
3175
+ const noun = SANDBOX_NAMES.includes(at(0) ?? "") ? "sandboxes" : AGENT_NAMES.includes(at(0) ?? "") ? "agents" : null;
3176
+ if (noun && (at(1) === "create" || at(1) === "new")) {
2937
3177
  const stop = argv2.indexOf("--");
2938
3178
  for (let i = 0; i < (stop === -1 ? argv2.length : stop); i++) {
2939
3179
  if (argv2[i] === "--timeout" || argv2[i].startsWith("--timeout=")) {
2940
3180
  argv2[i] = argv2[i].replace("--timeout", "--ttl");
2941
- notes.push("`sandboxes create --timeout` is now `--ttl` (`--timeout` stays for `exec`).");
3181
+ notes.push(`\`${noun} create --timeout\` is now \`--ttl\`${noun === "sandboxes" ? " (`--timeout` stays for `exec`)" : ""}.`);
2942
3182
  }
2943
3183
  }
2944
3184
  }
@@ -2946,7 +3186,7 @@ function normalizeArgs(input) {
2946
3186
  }
2947
3187
 
2948
3188
  // src/index.ts
2949
- var VERSION2 = true ? "0.9.1" : "dev";
3189
+ var VERSION2 = true ? "0.10.0" : "dev";
2950
3190
  var GLOBAL_OPTIONS = {
2951
3191
  json: { type: "boolean" },
2952
3192
  token: { type: "string" },
@@ -3110,8 +3350,8 @@ main(argv).catch((err) => {
3110
3350
  `);
3111
3351
  }
3112
3352
  process.exitCode = e.exitCode || EXIT.API;
3113
- }).then(() => notice).then((line) => {
3114
- if (line) process.stderr.write(`
3115
- ${line}
3353
+ }).then(() => notice).then((line2) => {
3354
+ if (line2) process.stderr.write(`
3355
+ ${line2}
3116
3356
  `);
3117
3357
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Official CLI for EasyBits, the cloud for AI agents — sandboxes, hosting, domains, databases, agents and files from your terminal",
5
5
  "type": "module",
6
6
  "bin": {