openpouch 0.1.0 → 0.2.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 +18 -4
  2. package/openpouch.js +578 -173
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,20 +1,34 @@
1
1
  # openpouch 🦘
2
2
 
3
- **The agent-native deployment control plane.** Deploy any folder to a live URL in one command — no account, no setup:
3
+ **The agent-native hosting platform — built _for_ coding agents, not walled against them.** Deploy any folder to a live URL in one command — no account, no dashboard, no CAPTCHA:
4
4
 
5
5
  ```bash
6
6
  npx openpouch deploy
7
7
  ```
8
8
 
9
- > **Status: technical preview.** The instant lane (`openpouch deploy`) serves **static** sites today (HTML/SPAs/build output); a runtime for dynamic apps is coming. The governed Render/Vercel lanes (`init`/`preview`/`prod`) are fuller. Feedback welcome on GitHub.
9
+ > **Status: technical preview.** `openpouch deploy` serves **static** sites (HTML/SPAs/build output) **and runs real Node.js apps** in a hardened container — autonomously, from any agent or terminal. Previews are ephemeral (unclaimed ones vanish after 72 h); durable production hosting and self-service billing are on the way. Feedback welcome on GitHub.
10
10
 
11
- You get a live `https://<slug>.openpouch.sh` preview plus a claim link. The agent deploys autonomously; a human claims it via the link (unclaimed previews vanish after 72 h). openpouch writes the deployment truth (`deploy.manifest.json`, `deploy.evidence.json`, `DEPLOYMENT.md`) back into your repo, so any agent can resume after context loss.
11
+ You get a live `https://<slug>.openpouch.sh` URL plus a claim link. The agent deploys autonomously; a human claims it via the link to keep it. openpouch writes the deployment truth (`deploy.manifest.json`, `deploy.evidence.json`, `DEPLOYMENT.md`) back into your repo, so any agent can resume after context loss.
12
+
13
+ ## Accounts (optional)
14
+
15
+ Start anonymous, or create a free account for higher limits — entirely from the agent, no dashboard:
16
+
17
+ ```bash
18
+ npx openpouch signup --email you@example.com # or: --github
19
+ npx openpouch activate --account <id> --token <token-from-email>
20
+ npx openpouch whoami --json # your tier + current usage
21
+ ```
22
+
23
+ Your API key is stored locally; deploys then run under your account and quota. Abuse is controlled with accounts, quotas, rate limits and an egress filter — **never** by asking a machine to prove it's human.
24
+
25
+ ## Bring your own provider
12
26
 
13
27
  Already on Render or Vercel? `openpouch init` detects your project and maps the service; then `openpouch preview` / `openpouch prod` run a governed pipeline — previews autonomous, production gated behind a human approval. Agents can never self-approve production.
14
28
 
15
29
  ## Commands
16
30
 
17
- `deploy` · `init` · `inspect` · `plan` · `preview` · `prod` · `approve` · `verify` · `logs` · `rollback` — every command supports `--json` and stable exit codes, so any agent harness with a shell can drive it. An MCP server exposes the same tools.
31
+ `deploy` · `signup` · `activate` · `whoami` · `init` · `inspect` · `plan` · `preview` · `prod` · `approve` · `verify` · `logs` · `rollback` — every command supports `--json` and stable exit codes, so any agent harness with a shell can drive it. An MCP server exposes the same tools.
18
32
 
19
33
  - Source & docs: https://github.com/openpouch/openpouch
20
34
  - License: Apache-2.0
package/openpouch.js CHANGED
@@ -4258,94 +4258,16 @@ function findUsableApproval(file, match, now) {
4258
4258
  return file.requests.find((r) => r.status === "approved" && r.action === match.action && r.environment === match.environment && r.serviceId === match.serviceId && !isExpired(r, now));
4259
4259
  }
4260
4260
 
4261
- // packages/cli/src/commands/approve.ts
4262
- import { createInterface } from "node:readline/promises";
4263
-
4264
- // packages/cli/src/approvals.ts
4265
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
4266
- import { mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
4267
- import { homedir, userInfo } from "node:os";
4268
- import { join as join2 } from "node:path";
4269
- var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
4270
- async function loadApprovals(cwd) {
4271
- try {
4272
- const raw = await readFile2(join2(cwd, APPROVALS_FILENAME), "utf8");
4273
- return approvalsFileSchema.parse(JSON.parse(raw));
4274
- } catch {
4275
- return { version: 0, requests: [] };
4276
- }
4277
- }
4278
- async function saveApprovals(cwd, file) {
4279
- await mkdir(join2(cwd, APPROVALS_DIR), { recursive: true });
4280
- await ensureGitignored(cwd);
4281
- await writeFile2(
4282
- join2(cwd, APPROVALS_FILENAME),
4283
- `${JSON.stringify(approvalsFileSchema.parse(file), null, 2)}
4284
- `,
4285
- "utf8"
4286
- );
4287
- }
4288
- async function ensureGitignored(cwd) {
4289
- const path = join2(cwd, ".gitignore");
4290
- let content = "";
4291
- try {
4292
- content = await readFile2(path, "utf8");
4293
- } catch {
4294
- }
4295
- if (!content.split("\n").some((line) => line.trim() === ".openpouch/")) {
4296
- const addition = `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}.openpouch/
4297
- `;
4298
- await writeFile2(path, content + addition, "utf8");
4299
- }
4300
- }
4301
- function newApprovalRequest(match, now) {
4302
- return {
4303
- id: randomBytes(4).toString("hex"),
4304
- action: match.action,
4305
- environment: match.environment,
4306
- serviceId: match.serviceId,
4307
- requestedAt: now.toISOString(),
4308
- expiresAt: new Date(now.getTime() + DEFAULT_TTL_MS).toISOString(),
4309
- status: "pending"
4310
- };
4311
- }
4312
- async function getApproverSecret(createIfMissing, home) {
4313
- const dir = join2(home ?? homedir(), ".openpouch");
4314
- const path = join2(dir, "approver.secret");
4315
- try {
4316
- const existing = (await readFile2(path, "utf8")).trim();
4317
- if (existing.length > 0) return existing;
4318
- } catch {
4319
- }
4320
- if (!createIfMissing) return void 0;
4321
- const secret = randomBytes(32).toString("hex");
4322
- await mkdir(dir, { recursive: true });
4323
- await writeFile2(path, `${secret}
4324
- `, { mode: 384 });
4325
- return secret;
4326
- }
4327
- function signApproval(request, secret) {
4328
- return createHmac("sha256", secret).update(canonicalApprovalPayload(request)).digest("hex");
4329
- }
4330
- function signatureValid(request, secret) {
4331
- if (request.signature === void 0) return false;
4332
- const expected = Buffer.from(signApproval(request, secret), "hex");
4333
- const actual = Buffer.from(request.signature, "hex");
4334
- return expected.length === actual.length && timingSafeEqual(expected, actual);
4335
- }
4336
- function approverName() {
4337
- try {
4338
- return userInfo().username;
4339
- } catch {
4340
- return "unknown";
4341
- }
4342
- }
4343
-
4344
- // packages/cli/src/shared.ts
4345
- import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
4261
+ // packages/cli/src/commands/activate.ts
4262
+ import { mkdir, writeFile as writeFile3 } from "node:fs/promises";
4346
4263
  import { homedir as homedir2 } from "node:os";
4347
4264
  import { join as join3 } from "node:path";
4348
4265
 
4266
+ // packages/cli/src/shared.ts
4267
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
4268
+ import { homedir } from "node:os";
4269
+ import { join as join2 } from "node:path";
4270
+
4349
4271
  // packages/adapter-render/dist/index.js
4350
4272
  var RenderApiError = class extends Error {
4351
4273
  category;
@@ -4481,29 +4403,36 @@ var RunApiError = class extends Error {
4481
4403
  category;
4482
4404
  status;
4483
4405
  fix;
4484
- constructor(status, detail) {
4485
- const category = status === 429 ? "quota" : "provider";
4406
+ constructor(status, detail, fix) {
4407
+ const category = status === 401 ? "auth" : status === 429 || status === 413 ? "quota" : "provider";
4486
4408
  super(`openpouch-run ${status}: ${detail}`);
4487
4409
  this.name = "RunApiError";
4488
4410
  this.status = status;
4489
4411
  this.category = category;
4490
- this.fix = status === 429 ? "Instant-lane rate limit hit \u2014 wait and retry, or use a BYO provider for higher volume." : "Check the openpouch-run service status; the deployment may have expired (72h unclaimed).";
4412
+ this.fix = fix ?? (category === "auth" ? "Your openpouch API key is missing, invalid, or revoked \u2014 run `openpouch signup` to get one, or check OPENPOUCH_API_KEY / ~/.openpouch/openpouch-run.key." : category === "quota" ? "A limit was reached \u2014 wait for the window to roll over, delete an old preview, or use a higher tier." : "Check the openpouch-run service status; the deployment may have expired (72h unclaimed).");
4491
4413
  }
4492
4414
  };
4493
4415
  function createRunAdapter(config) {
4494
4416
  const doFetch = config.fetchImpl ?? fetch;
4495
4417
  const base = config.apiBase.replace(/\/$/, "");
4418
+ const authHeader = config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {};
4496
4419
  async function api(path, init) {
4497
- const res = await doFetch(`${base}${path}`, init);
4420
+ const res = await doFetch(`${base}${path}`, {
4421
+ ...init,
4422
+ headers: { ...authHeader, ...init?.headers }
4423
+ });
4498
4424
  if (!res.ok) {
4499
4425
  let detail = res.statusText;
4426
+ let fix;
4500
4427
  try {
4501
4428
  const body = await res.json();
4502
4429
  if (body.error)
4503
4430
  detail = body.error;
4431
+ if (body.fix)
4432
+ fix = body.fix;
4504
4433
  } catch {
4505
4434
  }
4506
- throw new RunApiError(res.status, detail);
4435
+ throw new RunApiError(res.status, detail, fix);
4507
4436
  }
4508
4437
  return res.json();
4509
4438
  }
@@ -4519,6 +4448,26 @@ function createRunAdapter(config) {
4519
4448
  const body = await api("/api/deployments", { method: "POST", headers, body: tarball });
4520
4449
  return body;
4521
4450
  },
4451
+ async whoami() {
4452
+ return await api("/api/account");
4453
+ },
4454
+ async signupEmail(email) {
4455
+ return await api("/api/accounts", {
4456
+ method: "POST",
4457
+ headers: { "content-type": "application/json" },
4458
+ body: JSON.stringify({ email })
4459
+ });
4460
+ },
4461
+ async verifyEmail(accountId, token) {
4462
+ return await api("/api/accounts/verify", {
4463
+ method: "POST",
4464
+ headers: { "content-type": "application/json" },
4465
+ body: JSON.stringify({ account: accountId, token })
4466
+ });
4467
+ },
4468
+ githubStartUrl() {
4469
+ return `${base}/api/auth/github/start`;
4470
+ },
4522
4471
  async listServices() {
4523
4472
  return [];
4524
4473
  },
@@ -4673,7 +4622,154 @@ function createVercelAdapter(config) {
4673
4622
  };
4674
4623
  }
4675
4624
 
4625
+ // packages/cli/src/summary.ts
4626
+ function humanList(items) {
4627
+ if (items.length === 0) return "";
4628
+ if (items.length === 1) return items[0] ?? "";
4629
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
4630
+ }
4631
+ function friendlyUtc(iso) {
4632
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso);
4633
+ return m ? `${m[1]} ${m[2]} UTC` : iso;
4634
+ }
4635
+ function instantDeploySummary(i) {
4636
+ return `Your app "${i.name}" is now live on the internet at ${i.url} \u2014 anyone you share that link with can open it right away. It's a free temporary preview and will disappear on ${friendlyUtc(i.expiresAt)} (about 72 hours) unless you save it. To keep it, open ${i.claimUrl}. Nothing technical is needed from you.`;
4637
+ }
4638
+ function deploySummary(i) {
4639
+ const at = i.url !== void 0 ? ` at ${i.url}` : "";
4640
+ const approved = i.approvedBy !== void 0 ? `With your approval, ` : "";
4641
+ if (!i.live) {
4642
+ return `The ${i.environment} update did not go live \u2014 something failed while deploying. Your previous version is unaffected. I can check the logs to find out why.`;
4643
+ }
4644
+ if (i.smokePassed === false) {
4645
+ return `${approved}your ${i.environment} app deployed and is online${at}, but my automatic health check found a problem \u2014 it may not be working correctly for visitors right now. You may want me to look into it.`;
4646
+ }
4647
+ const checked = i.smokePassed === true ? ` and I checked that it's responding correctly` : "";
4648
+ const lead = approved !== "" ? approved : "Your ";
4649
+ const subject = approved !== "" ? `your ${i.environment} app` : `${i.environment} app`;
4650
+ return `${lead}${subject} is live${at}${checked}. Nothing else is needed from you.`;
4651
+ }
4652
+ function inspectSummary(projectName, environments) {
4653
+ const names = Object.keys(environments);
4654
+ if (names.length === 0) {
4655
+ return `"${projectName}" isn't connected to any live service yet, so there's nothing deployed.`;
4656
+ }
4657
+ const lines = [`Here's the current status of "${projectName}":`];
4658
+ for (const [name, env] of Object.entries(environments)) {
4659
+ if (env.status === "unmapped" || env.service === void 0) {
4660
+ lines.push(`\u2022 ${name}: not connected to a live service yet.`);
4661
+ continue;
4662
+ }
4663
+ const where = env.service.url !== void 0 ? `live at ${env.service.url}` : `set up (no public address yet)`;
4664
+ const paused = env.service.suspended ? " It's currently paused." : "";
4665
+ const missing = env.envVars?.missingRequired ?? [];
4666
+ const needs = missing.length > 0 ? ` It still needs ${missing.length === 1 ? "this setting" : "these settings"} before it can fully work: ${humanList(missing)}.` : "";
4667
+ lines.push(`\u2022 ${name}: ${where}.${paused}${needs}`);
4668
+ }
4669
+ return lines.join("\n");
4670
+ }
4671
+ function verifySummary(i) {
4672
+ const total = i.checks.length;
4673
+ const passedCount = i.checks.filter((c) => c.passed).length;
4674
+ if (i.passed) {
4675
+ return `Good news \u2014 your ${i.environment} app at ${i.url} is healthy and responding correctly. ${total === 1 ? "The check" : `All ${total} checks`} passed.`;
4676
+ }
4677
+ const firstFail = i.checks.find((c) => !c.passed);
4678
+ const reason = firstFail?.detail !== void 0 ? ` (${firstFail.detail})` : "";
4679
+ return `Heads up \u2014 your ${i.environment} app at ${i.url} has a problem${reason}. ${passedCount} of ${total} checks passed. It may not be working correctly for visitors right now, so you may want me to look into it.`;
4680
+ }
4681
+ function planSummary(projectName, plans) {
4682
+ const names = Object.keys(plans);
4683
+ if (names.length === 0) {
4684
+ return `"${projectName}" has no environments set up to deploy yet.`;
4685
+ }
4686
+ const lines = [`Here's what can be published for "${projectName}":`];
4687
+ for (const [name, p] of Object.entries(plans)) {
4688
+ if (!p.ready) {
4689
+ lines.push(`\u2022 ${name}: not ready yet \u2014 ${humanList(p.blockers)}.`);
4690
+ } else if (p.decision === "requires-approval") {
4691
+ lines.push(`\u2022 ${name}: ready, but it will need your approval before it goes live.`);
4692
+ } else {
4693
+ lines.push(`\u2022 ${name}: ready to go live.`);
4694
+ }
4695
+ }
4696
+ return lines.join("\n");
4697
+ }
4698
+ function initSummary(i) {
4699
+ if (i.alreadyInitialized === true) {
4700
+ return `"${i.name}" is already set up for deployment. You can deploy it whenever you'd like.`;
4701
+ }
4702
+ const kind = i.framework !== void 0 ? ` (a ${i.framework} app)` : "";
4703
+ const connected = i.matchedProvider !== void 0 ? ` I connected it to your existing ${i.matchedProvider} service.` : "";
4704
+ return `I've set up "${i.name}"${kind} for deployment.${connected} You're ready to deploy it whenever you'd like \u2014 just say the word.`;
4705
+ }
4706
+ function logsSummary(environment, count) {
4707
+ if (count === 0) {
4708
+ return `There are no recent log entries for your ${environment} app.`;
4709
+ }
4710
+ return `Here are the ${count} most recent log entries from your ${environment} app \u2014 these are mainly useful for spotting errors.`;
4711
+ }
4712
+ function rollbackSummary(i) {
4713
+ if (!i.live) {
4714
+ return `The rollback did not complete \u2014 the previous version of your ${i.environment} app could not be brought back online. I can investigate.`;
4715
+ }
4716
+ const at = i.url !== void 0 ? ` at ${i.url}` : "";
4717
+ const health = i.smokePassed === false ? ` My health check still flags a problem, so you may want me to look closer.` : i.smokePassed === true ? ` I checked that it's responding correctly.` : "";
4718
+ return `I restored your ${i.environment} app to the previous working version. It's live again${at}.${health}`;
4719
+ }
4720
+ function approveListSummary(pending) {
4721
+ if (pending.length === 0) {
4722
+ return `Nothing is waiting for your approval right now.`;
4723
+ }
4724
+ const ids = pending.map((p) => p.id);
4725
+ return `There ${pending.length === 1 ? "is 1 change" : `are ${pending.length} changes`} waiting for your approval before going live. To approve, run this in your own terminal: ${ids.map((id) => `openpouch approve ${id}`).join(" ; ")}`;
4726
+ }
4727
+ function approveResultSummary(approved) {
4728
+ return approved ? `Approved. The change can now be published.` : `Not approved \u2014 nothing was published.`;
4729
+ }
4730
+ function approvalRequiredSummary(environment, requestId) {
4731
+ return `Publishing to ${environment} needs your go-ahead first \u2014 for safety, an agent can't approve production changes by itself. To approve, open your own terminal and run: openpouch approve ${requestId}. Then I'll continue.`;
4732
+ }
4733
+ function signupEmailSummary(email) {
4734
+ return `I've started creating your openpouch account for ${email}. Check that inbox for a message from openpouch and open the link inside to finish \u2014 that gives you a private key I use to publish apps under your account.`;
4735
+ }
4736
+ function signupGithubSummary(url) {
4737
+ return `To create your openpouch account with GitHub, open this link in your browser and approve it: ${url}. When you're done you'll get a private key I use to publish apps under your account.`;
4738
+ }
4739
+ function activateSummary(saved) {
4740
+ return saved ? `Your openpouch account is ready, and I've saved your private key on this computer \u2014 so from now on I publish apps under your account. Nothing else is needed from you.` : `Your openpouch account is ready. Keep the private key somewhere safe \u2014 it's what lets me publish apps under your account.`;
4741
+ }
4742
+ function whoamiSummary(i) {
4743
+ if (!i.authenticated) {
4744
+ return `You don't have an openpouch account set up here yet, so your apps publish anonymously on the free temporary tier. To get your own account \u2014 higher limits, and your apps grouped together \u2014 just say the word and I'll start it.`;
4745
+ }
4746
+ const using = i.liveDeployments !== void 0 && i.maxLive !== void 0 ? ` You're using ${i.liveDeployments} of your ${i.maxLive} live apps.` : "";
4747
+ return `You're signed in to openpouch on the ${i.tier} plan.${using} Everything looks fine.`;
4748
+ }
4749
+ var ERROR_LEADS = {
4750
+ auth: "I couldn't sign in to the hosting provider \u2014 an access key is missing or invalid.",
4751
+ "policy-denied": "This action isn't allowed by your current safety settings.",
4752
+ "human-required": "This needs a person to approve it \u2014 an agent can't do it.",
4753
+ env: "This project isn't fully set up for deployment yet.",
4754
+ quota: "The hosting provider reported that a limit was reached.",
4755
+ provider: "The hosting provider ran into a problem.",
4756
+ usage: "That command wasn't used correctly.",
4757
+ internal: "Something went wrong inside openpouch."
4758
+ };
4759
+ function errorSummary(category, message, fix) {
4760
+ const lead = ERROR_LEADS[category] ?? `Something didn't work: ${message}.`;
4761
+ return `${lead} What to do next: ${fix}`;
4762
+ }
4763
+
4676
4764
  // packages/cli/src/shared.ts
4765
+ function summarized(exitCode, json, summary, human) {
4766
+ const { ok, ...rest } = json;
4767
+ return {
4768
+ exitCode,
4769
+ json: { ok, summary, ...rest },
4770
+ human: [...human, "", summary]
4771
+ };
4772
+ }
4677
4773
  var EXIT = {
4678
4774
  OK: 0,
4679
4775
  UNEXPECTED: 1,
@@ -4690,16 +4786,17 @@ function isProviderApiError(e) {
4690
4786
  return e instanceof Error && "category" in e && "fix" in e;
4691
4787
  }
4692
4788
  function errorResult(exitCode, category, message, fix) {
4789
+ const summary = errorSummary(category, message, fix);
4693
4790
  return {
4694
4791
  exitCode,
4695
- json: { ok: false, error: { category, message, fix } },
4696
- human: [`error [${category}]: ${message}`, `fix: ${fix}`]
4792
+ json: { ok: false, summary, error: { category, message, fix } },
4793
+ human: [`error [${category}]: ${message}`, `fix: ${fix}`, "", summary]
4697
4794
  };
4698
4795
  }
4699
4796
  async function loadManifest(cwd) {
4700
4797
  let raw;
4701
4798
  try {
4702
- raw = await readFile3(join3(cwd, MANIFEST_FILENAME), "utf8");
4799
+ raw = await readFile2(join2(cwd, MANIFEST_FILENAME), "utf8");
4703
4800
  } catch {
4704
4801
  return { status: "missing" };
4705
4802
  }
@@ -4713,13 +4810,13 @@ async function loadManifest(cwd) {
4713
4810
  return result2.ok ? { status: "ok", manifest: result2.value } : { status: "invalid", errors: result2.errors };
4714
4811
  }
4715
4812
  async function writeJsonFile(path, value) {
4716
- await writeFile3(path, `${JSON.stringify(value, null, 2)}
4813
+ await writeFile2(path, `${JSON.stringify(value, null, 2)}
4717
4814
  `, "utf8");
4718
4815
  }
4719
4816
  async function loadPolicy(cwd) {
4720
4817
  let raw;
4721
4818
  try {
4722
- raw = await readFile3(join3(cwd, POLICY_FILENAME), "utf8");
4819
+ raw = await readFile2(join2(cwd, POLICY_FILENAME), "utf8");
4723
4820
  } catch {
4724
4821
  return { status: "default", policy: defaultPolicy(), usedDefault: true };
4725
4822
  }
@@ -4734,15 +4831,27 @@ var PROVIDER_CREDENTIALS = {
4734
4831
  vercel: { envVar: "VERCEL_TOKEN", file: "vercel.token" }
4735
4832
  };
4736
4833
  var ANONYMOUS_KEY = "anonymous";
4834
+ var RUN_KEY = { envVar: "OPENPOUCH_API_KEY", file: "openpouch-run.key" };
4835
+ async function resolveRunKey(env) {
4836
+ const fromEnv = env[RUN_KEY.envVar]?.trim();
4837
+ if (fromEnv) return fromEnv;
4838
+ try {
4839
+ const home = env["OPENPOUCH_HOME"] ?? homedir();
4840
+ const fromFile = (await readFile2(join2(home, ".openpouch", RUN_KEY.file), "utf8")).trim();
4841
+ if (fromFile.length > 0) return fromFile;
4842
+ } catch {
4843
+ }
4844
+ return ANONYMOUS_KEY;
4845
+ }
4737
4846
  async function resolveProviderApiKey(env, provider) {
4738
- if (provider === "openpouch-run") return ANONYMOUS_KEY;
4847
+ if (provider === "openpouch-run") return resolveRunKey(env);
4739
4848
  const cred = PROVIDER_CREDENTIALS[provider];
4740
4849
  if (!cred) return void 0;
4741
4850
  const fromEnv = env[cred.envVar]?.trim();
4742
4851
  if (fromEnv) return fromEnv;
4743
4852
  try {
4744
- const home = env["OPENPOUCH_HOME"] ?? homedir2();
4745
- const fromFile = (await readFile3(join3(home, ".openpouch", cred.file), "utf8")).trim();
4853
+ const home = env["OPENPOUCH_HOME"] ?? homedir();
4854
+ const fromFile = (await readFile2(join2(home, ".openpouch", cred.file), "utf8")).trim();
4746
4855
  return fromFile.length > 0 ? fromFile : void 0;
4747
4856
  } catch {
4748
4857
  return void 0;
@@ -4767,7 +4876,10 @@ function missingKeyError(provider) {
4767
4876
  function makeAdapter(ctx, provider, apiKey) {
4768
4877
  if (ctx.createAdapter !== void 0) return ctx.createAdapter(provider, apiKey);
4769
4878
  if (provider === "openpouch-run") {
4770
- return createRunAdapter({ apiBase: runApiBase(ctx.env) });
4879
+ return createRunAdapter({
4880
+ apiBase: runApiBase(ctx.env),
4881
+ ...apiKey && apiKey !== ANONYMOUS_KEY ? { apiKey } : {}
4882
+ });
4771
4883
  }
4772
4884
  if (provider === "vercel") {
4773
4885
  const teamId = ctx.env["VERCEL_TEAM_ID"]?.trim();
@@ -4776,6 +4888,148 @@ function makeAdapter(ctx, provider, apiKey) {
4776
4888
  return createRenderAdapter({ apiKey });
4777
4889
  }
4778
4890
 
4891
+ // packages/cli/src/commands/activate.ts
4892
+ function keyPublicPrefix(key) {
4893
+ const m = /^(op_live_[0-9a-f]+)_/.exec(key);
4894
+ return m ? m[1] : "op_live_\u2026";
4895
+ }
4896
+ async function activateCommand(ctx, opts) {
4897
+ const account = opts.account?.trim();
4898
+ const token = opts.token?.trim();
4899
+ if (!account || !token) {
4900
+ return errorResult(
4901
+ EXIT.USAGE,
4902
+ "usage",
4903
+ "activate needs --account and --token",
4904
+ "Run `openpouch signup --email <you@example.com>` first; the verification email contains both."
4905
+ );
4906
+ }
4907
+ const adapter = makeAdapter(ctx, "openpouch-run", "anonymous");
4908
+ if (typeof adapter.verifyEmail !== "function") {
4909
+ return errorResult(EXIT.UNEXPECTED, "provider", "activation unavailable", "This is an internal wiring error.");
4910
+ }
4911
+ try {
4912
+ const res = await adapter.verifyEmail(account, token);
4913
+ let saved = false;
4914
+ let keyPath = "";
4915
+ try {
4916
+ const home = ctx.env["OPENPOUCH_HOME"] ?? homedir2();
4917
+ const dir = join3(home, ".openpouch");
4918
+ await mkdir(dir, { recursive: true });
4919
+ keyPath = join3(dir, "openpouch-run.key");
4920
+ await writeFile3(keyPath, `${res.key}
4921
+ `, { mode: 384 });
4922
+ saved = true;
4923
+ } catch {
4924
+ saved = false;
4925
+ }
4926
+ return summarized(
4927
+ EXIT.OK,
4928
+ {
4929
+ ok: true,
4930
+ account: res.account,
4931
+ keyPrefix: keyPublicPrefix(res.key),
4932
+ saved,
4933
+ ...saved ? { keyPath } : { key: res.key }
4934
+ },
4935
+ activateSummary(saved),
4936
+ [
4937
+ `openpouch activate \u2713 ${res.account.id} [${res.account.tier}]`,
4938
+ saved ? ` saved your key to ${keyPath} (chmod 600) \u2014 deploys now run under your account` : ` \u26A0 could not save the key file \u2014 set it yourself: export OPENPOUCH_API_KEY=${res.key}`,
4939
+ ` key: ${keyPublicPrefix(res.key)}\u2026`
4940
+ ]
4941
+ );
4942
+ } catch (e) {
4943
+ if (isProviderApiError(e)) {
4944
+ return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
4945
+ }
4946
+ throw e;
4947
+ }
4948
+ }
4949
+
4950
+ // packages/cli/src/commands/approve.ts
4951
+ import { createInterface } from "node:readline/promises";
4952
+
4953
+ // packages/cli/src/approvals.ts
4954
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
4955
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
4956
+ import { homedir as homedir3, userInfo } from "node:os";
4957
+ import { join as join4 } from "node:path";
4958
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
4959
+ async function loadApprovals(cwd) {
4960
+ try {
4961
+ const raw = await readFile3(join4(cwd, APPROVALS_FILENAME), "utf8");
4962
+ return approvalsFileSchema.parse(JSON.parse(raw));
4963
+ } catch {
4964
+ return { version: 0, requests: [] };
4965
+ }
4966
+ }
4967
+ async function saveApprovals(cwd, file) {
4968
+ await mkdir2(join4(cwd, APPROVALS_DIR), { recursive: true });
4969
+ await ensureGitignored(cwd);
4970
+ await writeFile4(
4971
+ join4(cwd, APPROVALS_FILENAME),
4972
+ `${JSON.stringify(approvalsFileSchema.parse(file), null, 2)}
4973
+ `,
4974
+ "utf8"
4975
+ );
4976
+ }
4977
+ async function ensureGitignored(cwd) {
4978
+ const path = join4(cwd, ".gitignore");
4979
+ let content = "";
4980
+ try {
4981
+ content = await readFile3(path, "utf8");
4982
+ } catch {
4983
+ }
4984
+ if (!content.split("\n").some((line) => line.trim() === ".openpouch/")) {
4985
+ const addition = `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}.openpouch/
4986
+ `;
4987
+ await writeFile4(path, content + addition, "utf8");
4988
+ }
4989
+ }
4990
+ function newApprovalRequest(match, now) {
4991
+ return {
4992
+ id: randomBytes(4).toString("hex"),
4993
+ action: match.action,
4994
+ environment: match.environment,
4995
+ serviceId: match.serviceId,
4996
+ requestedAt: now.toISOString(),
4997
+ expiresAt: new Date(now.getTime() + DEFAULT_TTL_MS).toISOString(),
4998
+ status: "pending"
4999
+ };
5000
+ }
5001
+ async function getApproverSecret(createIfMissing, home) {
5002
+ const dir = join4(home ?? homedir3(), ".openpouch");
5003
+ const path = join4(dir, "approver.secret");
5004
+ try {
5005
+ const existing = (await readFile3(path, "utf8")).trim();
5006
+ if (existing.length > 0) return existing;
5007
+ } catch {
5008
+ }
5009
+ if (!createIfMissing) return void 0;
5010
+ const secret = randomBytes(32).toString("hex");
5011
+ await mkdir2(dir, { recursive: true });
5012
+ await writeFile4(path, `${secret}
5013
+ `, { mode: 384 });
5014
+ return secret;
5015
+ }
5016
+ function signApproval(request, secret) {
5017
+ return createHmac("sha256", secret).update(canonicalApprovalPayload(request)).digest("hex");
5018
+ }
5019
+ function signatureValid(request, secret) {
5020
+ if (request.signature === void 0) return false;
5021
+ const expected = Buffer.from(signApproval(request, secret), "hex");
5022
+ const actual = Buffer.from(request.signature, "hex");
5023
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
5024
+ }
5025
+ function approverName() {
5026
+ try {
5027
+ return userInfo().username;
5028
+ } catch {
5029
+ return "unknown";
5030
+ }
5031
+ }
5032
+
4779
5033
  // packages/cli/src/commands/approve.ts
4780
5034
  async function realConfirm(question) {
4781
5035
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -4788,15 +5042,16 @@ async function approveCommand(ctx, requestId) {
4788
5042
  const now = /* @__PURE__ */ new Date();
4789
5043
  if (requestId === void 0) {
4790
5044
  const pending = approvals.requests.filter((r) => r.status === "pending" && !isExpired(r, now));
4791
- return {
4792
- exitCode: EXIT.OK,
4793
- json: { ok: true, pending },
4794
- human: pending.length === 0 ? ["no pending approval requests"] : [
5045
+ return summarized(
5046
+ EXIT.OK,
5047
+ { ok: true, pending },
5048
+ approveListSummary(pending),
5049
+ pending.length === 0 ? ["no pending approval requests"] : [
4795
5050
  "pending approval requests:",
4796
5051
  ...pending.map((r) => ` ${r.id} ${r.action} on ${r.environment} (service ${r.serviceId}, expires ${r.expiresAt})`),
4797
5052
  "approve with: openpouch approve <id>"
4798
5053
  ]
4799
- };
5054
+ );
4800
5055
  }
4801
5056
  const request = approvals.requests.find((r) => r.id === requestId);
4802
5057
  if (request === void 0) {
@@ -4826,7 +5081,7 @@ async function approveCommand(ctx, requestId) {
4826
5081
  `Approve "${request.action}" on ${request.environment} (service ${request.serviceId})? [y/N] `
4827
5082
  );
4828
5083
  if (!confirmed) {
4829
- return { exitCode: EXIT.OK, json: { ok: true, approved: false }, human: ["not approved"] };
5084
+ return summarized(EXIT.OK, { ok: true, approved: false }, approveResultSummary(false), ["not approved"]);
4830
5085
  }
4831
5086
  const secret = await getApproverSecret(true, ctx.env["OPENPOUCH_HOME"]);
4832
5087
  if (secret === void 0) {
@@ -4837,14 +5092,15 @@ async function approveCommand(ctx, requestId) {
4837
5092
  request.approvedBy = approverName();
4838
5093
  request.signature = signApproval(request, secret);
4839
5094
  await saveApprovals(ctx.cwd, approvals);
4840
- return {
4841
- exitCode: EXIT.OK,
4842
- json: { ok: true, approved: true, request: { id: request.id, approvedBy: request.approvedBy, approvedAt: request.approvedAt } },
4843
- human: [
5095
+ return summarized(
5096
+ EXIT.OK,
5097
+ { ok: true, approved: true, request: { id: request.id, approvedBy: request.approvedBy, approvedAt: request.approvedAt } },
5098
+ approveResultSummary(true),
5099
+ [
4844
5100
  `approved \u2713 ${request.id} (${request.action} on ${request.environment}) by ${request.approvedBy}`,
4845
5101
  "the agent can now re-run the deploy command"
4846
5102
  ]
4847
- };
5103
+ );
4848
5104
  }
4849
5105
 
4850
5106
  // packages/cli/src/deploy-engine.ts
@@ -4956,12 +5212,14 @@ async function passPolicyGate(ctx, policy, environment, action, serviceId, rerun
4956
5212
  approvals.requests.push(request);
4957
5213
  await saveApprovals(ctx.cwd, approvals);
4958
5214
  }
5215
+ const summary = approvalRequiredSummary(environment, request.id);
4959
5216
  return {
4960
5217
  ok: false,
4961
5218
  result: {
4962
5219
  exitCode: EXIT.POLICY,
4963
5220
  json: {
4964
5221
  ok: false,
5222
+ summary,
4965
5223
  error: {
4966
5224
  category: "approval-required",
4967
5225
  message: `"${action}" on ${environment} requires human approval`,
@@ -4972,7 +5230,9 @@ async function passPolicyGate(ctx, policy, environment, action, serviceId, rerun
4972
5230
  human: [
4973
5231
  `approval required: "${action}" on ${environment}`,
4974
5232
  `request id: ${request.id} (expires ${request.expiresAt})`,
4975
- `next: a human runs \`openpouch approve ${request.id}\`, then re-run \`${rerunCommand}\``
5233
+ `next: a human runs \`openpouch approve ${request.id}\`, then re-run \`${rerunCommand}\``,
5234
+ "",
5235
+ summary
4976
5236
  ]
4977
5237
  }
4978
5238
  };
@@ -5026,15 +5286,24 @@ async function deployCommand(ctx, environment) {
5026
5286
  });
5027
5287
  const exitCode = !result2.deployLive ? EXIT.DEPLOY_FAILED : result2.smokePassed === false ? EXIT.VERIFY_FAILED : EXIT.OK;
5028
5288
  const d = result2.record;
5029
- return {
5289
+ return summarized(
5030
5290
  exitCode,
5031
- json: {
5291
+ {
5032
5292
  ok: exitCode === EXIT.OK,
5293
+ // R5: live link prominent at the top level when the deploy went live.
5294
+ ...d.url !== void 0 && result2.deployLive ? { url: d.url } : {},
5033
5295
  deployment: d,
5034
5296
  smokePassed: result2.smokePassed ?? null,
5035
5297
  evidence: ["deploy.evidence.json", "DEPLOYMENT.md"]
5036
5298
  },
5037
- human: [
5299
+ deploySummary({
5300
+ environment,
5301
+ live: result2.deployLive,
5302
+ url: d.url,
5303
+ smokePassed: result2.smokePassed,
5304
+ approvedBy: d.approvedBy
5305
+ }),
5306
+ [
5038
5307
  `openpouch ${environment === "preview" ? "preview" : "prod"} ${exitCode === EXIT.OK ? "\u2713" : "\u2717"} ${manifest.project.name} \u2192 ${environment}`,
5039
5308
  ` deploy: ${d.id} [${d.status}]${d.commit !== void 0 ? ` commit ${d.commit.slice(0, 10)}` : ""}`,
5040
5309
  ...d.url !== void 0 ? [` url: ${d.url}`] : [],
@@ -5043,7 +5312,7 @@ async function deployCommand(ctx, environment) {
5043
5312
  ...d.rollbackAnchor !== void 0 ? [` rollback anchor: ${d.rollbackAnchor}`] : [],
5044
5313
  ` evidence: deploy.evidence.json + DEPLOYMENT.md updated`
5045
5314
  ]
5046
- };
5315
+ );
5047
5316
  } catch (e) {
5048
5317
  if (isProviderApiError(e)) {
5049
5318
  return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5053,11 +5322,11 @@ async function deployCommand(ctx, environment) {
5053
5322
  }
5054
5323
 
5055
5324
  // packages/cli/src/commands/init.ts
5056
- import { join as join6 } from "node:path";
5325
+ import { join as join7 } from "node:path";
5057
5326
 
5058
5327
  // packages/cli/src/agentsmd.ts
5059
- import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
5060
- import { join as join4 } from "node:path";
5328
+ import { readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
5329
+ import { join as join5 } from "node:path";
5061
5330
  var AGENTS_MD_FILENAME = "AGENTS.md";
5062
5331
  var START = "<!-- openpouch:start -->";
5063
5332
  var END = "<!-- openpouch:end -->";
@@ -5071,7 +5340,8 @@ function renderAgentsSection(projectName) {
5071
5340
  "",
5072
5341
  "- **Deployment truth** (what is live, where, which commit, rollback anchor): read `DEPLOYMENT.md` or `deploy.evidence.json`. Config lives in `deploy.manifest.json`, agent permissions in `deploy.policy.json`.",
5073
5342
  "- **CLI:** `openpouch inspect` \xB7 `plan` \xB7 `preview` (autonomous if policy allows) \xB7 `prod` \xB7 `verify` \xB7 `logs` \xB7 `rollback`. Add `--json` for a single machine-readable JSON object; errors come as `{category, message, fix}` \u2014 act on the fix. Exit codes are documented API.",
5074
- "- **MCP alternative:** the `openpouch-mcp` stdio server exposes the same capabilities as typed tools.",
5343
+ "- **Relay the `summary` to your human:** every result (CLI `--json` and MCP) carries a top-level `summary` \u2014 plain-language, jargon-free text written for a non-technical operator. Pass it on verbatim: it says what happened, whether the app is live and healthy, the live link, and what (if anything) the human needs to do. The **live URL is the primary result** \u2014 on a successful deploy it's the top-level `url` field, ready to share.",
5344
+ "- **MCP alternative:** the `openpouch-mcp` stdio server exposes the same capabilities as typed tools (the `summary` is also surfaced as a leading text block to relay).",
5075
5345
  "- **Production rule:** `prod` and `rollback` may return `approvalRequest{id}`. **You cannot approve it \u2014 no agent can.** Ask your human to run `openpouch approve <id>` in their own terminal, then re-run the command.",
5076
5346
  "- Never write secret values into any of these files; env vars are tracked by name only.",
5077
5347
  "",
@@ -5079,7 +5349,7 @@ function renderAgentsSection(projectName) {
5079
5349
  ].join("\n");
5080
5350
  }
5081
5351
  async function upsertAgentsSection(cwd, projectName) {
5082
- const path = join4(cwd, AGENTS_MD_FILENAME);
5352
+ const path = join5(cwd, AGENTS_MD_FILENAME);
5083
5353
  const section = renderAgentsSection(projectName);
5084
5354
  let existing;
5085
5355
  try {
@@ -5088,7 +5358,7 @@ async function upsertAgentsSection(cwd, projectName) {
5088
5358
  existing = void 0;
5089
5359
  }
5090
5360
  if (existing === void 0) {
5091
- await writeFile4(path, `# AGENTS.md
5361
+ await writeFile5(path, `# AGENTS.md
5092
5362
 
5093
5363
  ${section}
5094
5364
  `, "utf8");
@@ -5105,13 +5375,13 @@ ${section}
5105
5375
  `;
5106
5376
  }
5107
5377
  if (next === existing) return "unchanged";
5108
- await writeFile4(path, next, "utf8");
5378
+ await writeFile5(path, next, "utf8");
5109
5379
  return "updated";
5110
5380
  }
5111
5381
 
5112
5382
  // packages/cli/src/detect.ts
5113
5383
  import { readFile as readFile5, readdir } from "node:fs/promises";
5114
- import { basename, extname, join as join5 } from "node:path";
5384
+ import { basename, extname, join as join6 } from "node:path";
5115
5385
  var FRAMEWORK_BY_DEP = [
5116
5386
  ["next", "nextjs"],
5117
5387
  ["astro", "astro"],
@@ -5146,7 +5416,7 @@ async function collectSourceFiles(root) {
5146
5416
  }
5147
5417
  for (const entry of entries) {
5148
5418
  if (files.length >= MAX_FILES) break;
5149
- const full = join5(current.dir, entry.name);
5419
+ const full = join6(current.dir, entry.name);
5150
5420
  if (entry.isDirectory()) {
5151
5421
  if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".") && current.depth < MAX_DEPTH) {
5152
5422
  stack.push({ dir: full, depth: current.depth + 1 });
@@ -5181,7 +5451,7 @@ async function scanEnvVarNames(root) {
5181
5451
  async function detectProject(cwd) {
5182
5452
  let pkg = {};
5183
5453
  try {
5184
- pkg = JSON.parse(await readFile5(join5(cwd, "package.json"), "utf8"));
5454
+ pkg = JSON.parse(await readFile5(join6(cwd, "package.json"), "utf8"));
5185
5455
  } catch {
5186
5456
  }
5187
5457
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -5223,21 +5493,23 @@ function matchService(projectName, services) {
5223
5493
  async function initCommand(ctx, flags) {
5224
5494
  const existing = await loadManifest(ctx.cwd);
5225
5495
  if (existing.status !== "missing" && !flags.force) {
5496
+ const existingName = existing.status === "ok" ? existing.manifest.project.name : "this project";
5226
5497
  const agentsMd2 = existing.status === "ok" ? await upsertAgentsSection(ctx.cwd, existing.manifest.project.name) : "unchanged";
5227
- return {
5228
- exitCode: EXIT.OK,
5229
- json: {
5498
+ return summarized(
5499
+ EXIT.OK,
5500
+ {
5230
5501
  ok: true,
5231
5502
  alreadyInitialized: true,
5232
5503
  agentsMd: agentsMd2,
5233
5504
  hints: [`${MANIFEST_FILENAME} exists \u2014 use --force to regenerate, or run \`openpouch inspect\``]
5234
5505
  },
5235
- human: [
5506
+ initSummary({ name: existingName, alreadyInitialized: true }),
5507
+ [
5236
5508
  `openpouch init: already initialized (${MANIFEST_FILENAME} exists)`,
5237
5509
  ...agentsMd2 !== "unchanged" ? [` ${AGENTS_MD_FILENAME}: openpouch section ${agentsMd2}`] : [],
5238
5510
  "hint: use --force to regenerate, or run `openpouch inspect`"
5239
5511
  ]
5240
- };
5512
+ );
5241
5513
  }
5242
5514
  const detected = await detectProject(ctx.cwd);
5243
5515
  const hints = [];
@@ -5289,13 +5561,13 @@ async function initCommand(ctx, flags) {
5289
5561
  "This is an openpouch bug \u2014 please report it."
5290
5562
  );
5291
5563
  }
5292
- await writeJsonFile(join6(ctx.cwd, MANIFEST_FILENAME), validated.value);
5293
- await writeJsonFile(join6(ctx.cwd, POLICY_FILENAME), defaultPolicy());
5564
+ await writeJsonFile(join7(ctx.cwd, MANIFEST_FILENAME), validated.value);
5565
+ await writeJsonFile(join7(ctx.cwd, POLICY_FILENAME), defaultPolicy());
5294
5566
  const agentsMd = await upsertAgentsSection(ctx.cwd, detected.name);
5295
5567
  hints.push("policy default: previews autonomous, production requires approval \u2014 edit deploy.policy.json to change");
5296
- return {
5297
- exitCode: EXIT.OK,
5298
- json: {
5568
+ return summarized(
5569
+ EXIT.OK,
5570
+ {
5299
5571
  ok: true,
5300
5572
  created: [MANIFEST_FILENAME, POLICY_FILENAME],
5301
5573
  detected: {
@@ -5309,7 +5581,12 @@ async function initCommand(ctx, flags) {
5309
5581
  agentsMd,
5310
5582
  hints
5311
5583
  },
5312
- human: [
5584
+ initSummary({
5585
+ name: detected.name,
5586
+ framework: detected.framework,
5587
+ ...matched !== void 0 ? { matchedProvider: "render" } : {}
5588
+ }),
5589
+ [
5313
5590
  `openpouch init \u2713 ${detected.name}${detected.framework !== void 0 ? ` (${detected.framework})` : ""}`,
5314
5591
  ` created: ${MANIFEST_FILENAME}, ${POLICY_FILENAME}`,
5315
5592
  ` ${AGENTS_MD_FILENAME}: openpouch section ${agentsMd} (cross-harness discovery)`,
@@ -5320,13 +5597,13 @@ async function initCommand(ctx, flags) {
5320
5597
  ...hints.map((h) => ` hint: ${h}`),
5321
5598
  " next: openpouch inspect"
5322
5599
  ]
5323
- };
5600
+ );
5324
5601
  }
5325
5602
 
5326
5603
  // packages/cli/src/commands/instant.ts
5327
5604
  import { spawn } from "node:child_process";
5328
5605
  import { readFile as readFile6 } from "node:fs/promises";
5329
- import { basename as basename2, join as join7 } from "node:path";
5606
+ import { basename as basename2, join as join8 } from "node:path";
5330
5607
  var TAR_EXCLUDES = [
5331
5608
  "./node_modules",
5332
5609
  "./.git",
@@ -5355,7 +5632,7 @@ function buildTarball(cwd) {
5355
5632
  }
5356
5633
  async function projectHint(cwd) {
5357
5634
  try {
5358
- const pkg = JSON.parse(await readFile6(join7(cwd, "package.json"), "utf8"));
5635
+ const pkg = JSON.parse(await readFile6(join8(cwd, "package.json"), "utf8"));
5359
5636
  if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
5360
5637
  } catch {
5361
5638
  }
@@ -5381,7 +5658,8 @@ async function instantCommand(ctx) {
5381
5658
  return errorResult(EXIT.USAGE, "usage", "nothing to deploy", "The directory appears empty after excludes.");
5382
5659
  }
5383
5660
  const hint = await projectHint(ctx.cwd);
5384
- const adapter = makeAdapter(ctx, "openpouch-run", "anonymous");
5661
+ const runKey = await resolveProviderApiKey(ctx.env, "openpouch-run") ?? "anonymous";
5662
+ const adapter = makeAdapter(ctx, "openpouch-run", runKey);
5385
5663
  if (typeof adapter.instantDeploy !== "function") {
5386
5664
  return errorResult(EXIT.UNEXPECTED, "provider", "instant lane adapter unavailable", "This is an internal wiring error.");
5387
5665
  }
@@ -5395,8 +5673,8 @@ async function instantCommand(ctx) {
5395
5673
  preview: { provider: "openpouch-run", serviceId: result2.slug, url: result2.url }
5396
5674
  }
5397
5675
  };
5398
- await writeJsonFile(join7(ctx.cwd, MANIFEST_FILENAME), manifest);
5399
- await writeJsonFile(join7(ctx.cwd, POLICY_FILENAME), defaultPolicy());
5676
+ await writeJsonFile(join8(ctx.cwd, MANIFEST_FILENAME), manifest);
5677
+ await writeJsonFile(join8(ctx.cwd, POLICY_FILENAME), defaultPolicy());
5400
5678
  await appendDeployment(ctx.cwd, {
5401
5679
  id: result2.slug,
5402
5680
  environment: "preview",
@@ -5406,27 +5684,33 @@ async function instantCommand(ctx) {
5406
5684
  deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
5407
5685
  notes: `instant preview \u2014 expires ${result2.expiresAt}; claim: ${result2.claimUrl}`
5408
5686
  });
5409
- return {
5410
- exitCode: EXIT.OK,
5411
- json: {
5687
+ const kind = result2.kind ?? "static";
5688
+ return summarized(
5689
+ EXIT.OK,
5690
+ {
5412
5691
  ok: true,
5692
+ // R5: the live link is the primary, immediately shareable result — top-level.
5693
+ url: result2.url,
5694
+ claimUrl: result2.claimUrl,
5413
5695
  deployment: {
5414
5696
  id: result2.slug,
5415
5697
  url: result2.url,
5416
5698
  claimUrl: result2.claimUrl,
5417
5699
  expiresAt: result2.expiresAt,
5418
- status: result2.status
5700
+ status: result2.status,
5701
+ kind
5419
5702
  },
5420
5703
  evidence: ["deploy.manifest.json", "deploy.policy.json", "deploy.evidence.json", "DEPLOYMENT.md"]
5421
5704
  },
5422
- human: [
5423
- `openpouch deploy \u2713 ${hint} \u2192 instant preview`,
5705
+ instantDeploySummary({ name: hint, url: result2.url, claimUrl: result2.claimUrl, expiresAt: result2.expiresAt }),
5706
+ [
5707
+ `openpouch deploy \u2713 ${hint} \u2192 instant preview${kind === "dynamic" ? " (dynamic app \u2014 running in a container)" : ""}`,
5424
5708
  ` url: ${result2.url}`,
5425
5709
  ` expires: ${result2.expiresAt} (unclaimed previews vanish after 72h)`,
5426
5710
  ` claim: ${result2.claimUrl}`,
5427
5711
  ` wrote: deploy.manifest.json, deploy.policy.json, deploy.evidence.json, DEPLOYMENT.md`
5428
5712
  ]
5429
- };
5713
+ );
5430
5714
  } catch (e) {
5431
5715
  if (isProviderApiError(e)) {
5432
5716
  return errorResult(EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5555,16 +5839,17 @@ async function inspectCommand(ctx) {
5555
5839
  }
5556
5840
  }
5557
5841
  human.push(...hints.map((h) => ` hint: ${h}`));
5558
- return {
5559
- exitCode: EXIT.OK,
5560
- json: {
5842
+ return summarized(
5843
+ EXIT.OK,
5844
+ {
5561
5845
  ok: true,
5562
5846
  project: { name: manifest.project.name, framework: manifest.project.framework ?? null },
5563
5847
  environments,
5564
5848
  hints
5565
5849
  },
5850
+ inspectSummary(manifest.project.name, environments),
5566
5851
  human
5567
- };
5852
+ );
5568
5853
  }
5569
5854
 
5570
5855
  // packages/cli/src/commands/logs.ts
@@ -5590,14 +5875,15 @@ async function logsCommand(ctx, environment, limit) {
5590
5875
  const adapter = makeAdapter(ctx, cfg.provider, apiKey);
5591
5876
  try {
5592
5877
  const lines = await adapter.getLogs(cfg.serviceId, { limit });
5593
- return {
5594
- exitCode: EXIT.OK,
5595
- json: { ok: true, environment, serviceId: cfg.serviceId, count: lines.length, logs: lines },
5596
- human: [
5878
+ return summarized(
5879
+ EXIT.OK,
5880
+ { ok: true, environment, serviceId: cfg.serviceId, count: lines.length, logs: lines },
5881
+ logsSummary(environment, lines.length),
5882
+ [
5597
5883
  `openpouch logs \u2014 ${environment} (${cfg.serviceId}), ${lines.length} lines:`,
5598
5884
  ...lines.map((l) => ` ${l.timestamp ?? ""} ${l.message}`.trimEnd())
5599
5885
  ]
5600
- };
5886
+ );
5601
5887
  } catch (e) {
5602
5888
  if (isProviderApiError(e)) {
5603
5889
  return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5667,11 +5953,12 @@ async function planCommand(ctx) {
5667
5953
  }
5668
5954
  if (Object.keys(plans).length === 0) human.push(" no environments mapped \u2014 run `openpouch init --force` or edit the manifest");
5669
5955
  human.push(...hints.map((h) => ` hint: ${h}`));
5670
- return {
5671
- exitCode: EXIT.OK,
5672
- json: { ok: true, project: manifest.project.name, environments: plans, hints },
5956
+ return summarized(
5957
+ EXIT.OK,
5958
+ { ok: true, project: manifest.project.name, environments: plans, hints },
5959
+ planSummary(manifest.project.name, plans),
5673
5960
  human
5674
- };
5961
+ );
5675
5962
  }
5676
5963
 
5677
5964
  // packages/cli/src/commands/rollback.ts
@@ -5732,22 +6019,24 @@ async function rollbackCommand(ctx, environment) {
5732
6019
  });
5733
6020
  const exitCode = !result2.deployLive ? EXIT.DEPLOY_FAILED : result2.smokePassed === false ? EXIT.VERIFY_FAILED : EXIT.OK;
5734
6021
  const d = result2.record;
5735
- return {
6022
+ return summarized(
5736
6023
  exitCode,
5737
- json: {
6024
+ {
5738
6025
  ok: exitCode === EXIT.OK,
6026
+ ...d.url !== void 0 && result2.deployLive ? { url: d.url } : {},
5739
6027
  rolledBackTo: { commit: anchorDeploy.commit, anchorDeploy: latest.rollbackAnchor },
5740
6028
  deployment: d,
5741
6029
  smokePassed: result2.smokePassed ?? null
5742
6030
  },
5743
- human: [
6031
+ rollbackSummary({ environment, live: result2.deployLive, url: d.url, smokePassed: result2.smokePassed }),
6032
+ [
5744
6033
  `openpouch rollback ${exitCode === EXIT.OK ? "\u2713" : "\u2717"} ${environment} \u2192 commit ${anchorDeploy.commit.slice(0, 10)}`,
5745
6034
  ` deploy: ${d.id} [${d.status}]`,
5746
6035
  ...result2.smokePassed !== void 0 ? [` smoke: ${result2.smokePassed ? "passed" : "FAILED"}`] : [],
5747
6036
  ...d.approvedBy !== void 0 ? [` approved by: ${d.approvedBy}`] : [],
5748
6037
  ` evidence: deploy.evidence.json + DEPLOYMENT.md updated`
5749
6038
  ]
5750
- };
6039
+ );
5751
6040
  } catch (e) {
5752
6041
  if (isProviderApiError(e)) {
5753
6042
  return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5756,6 +6045,57 @@ async function rollbackCommand(ctx, environment) {
5756
6045
  }
5757
6046
  }
5758
6047
 
6048
+ // packages/cli/src/commands/signup.ts
6049
+ async function signupCommand(ctx, opts) {
6050
+ const adapter = makeAdapter(ctx, "openpouch-run", "anonymous");
6051
+ if (opts.github === true) {
6052
+ const url = typeof adapter.githubStartUrl === "function" ? adapter.githubStartUrl() : "";
6053
+ return summarized(
6054
+ EXIT.OK,
6055
+ { ok: true, method: "github", startUrl: url },
6056
+ signupGithubSummary(url),
6057
+ ["openpouch signup (GitHub)", ` Open this URL in your browser and approve: ${url}`, " You'll get your API key on the page that follows."]
6058
+ );
6059
+ }
6060
+ const email = opts.email?.trim();
6061
+ if (!email) {
6062
+ return errorResult(
6063
+ EXIT.USAGE,
6064
+ "usage",
6065
+ "no signup method given",
6066
+ "Use `openpouch signup --email <you@example.com>` or `openpouch signup --github`."
6067
+ );
6068
+ }
6069
+ if (typeof adapter.signupEmail !== "function") {
6070
+ return errorResult(EXIT.UNEXPECTED, "provider", "signup unavailable", "This is an internal wiring error.");
6071
+ }
6072
+ try {
6073
+ const res = await adapter.signupEmail(email);
6074
+ return summarized(
6075
+ EXIT.OK,
6076
+ {
6077
+ ok: true,
6078
+ method: "email",
6079
+ accountId: res.accountId,
6080
+ message: res.message,
6081
+ ...res.devVerifyToken ? { devVerifyToken: res.devVerifyToken } : {}
6082
+ },
6083
+ signupEmailSummary(email),
6084
+ [
6085
+ `openpouch signup \u2713 ${email}`,
6086
+ ` account: ${res.accountId}`,
6087
+ ` ${res.message}`,
6088
+ ...res.devVerifyToken ? [` finish: openpouch activate --account ${res.accountId} --token ${res.devVerifyToken}`] : [` then: openpouch activate --account ${res.accountId} --token <token-from-email>`]
6089
+ ]
6090
+ );
6091
+ } catch (e) {
6092
+ if (isProviderApiError(e)) {
6093
+ return errorResult(EXIT.PROVIDER, e.category, e.message, e.fix);
6094
+ }
6095
+ throw e;
6096
+ }
6097
+ }
6098
+
5759
6099
  // packages/cli/src/commands/verify.ts
5760
6100
  async function verifyCommand(ctx, environment) {
5761
6101
  const loaded = await loadManifest(ctx.cwd);
@@ -5788,15 +6128,62 @@ async function verifyCommand(ctx, environment) {
5788
6128
  evidenceUpdated = true;
5789
6129
  }
5790
6130
  }
5791
- return {
5792
- exitCode: passed ? EXIT.OK : EXIT.VERIFY_FAILED,
5793
- json: { ok: passed, environment, url: cfg.url, checks: smoke, evidenceUpdated },
5794
- human: [
6131
+ return summarized(
6132
+ passed ? EXIT.OK : EXIT.VERIFY_FAILED,
6133
+ { ok: passed, environment, url: cfg.url, checks: smoke, evidenceUpdated },
6134
+ verifySummary({ environment, url: cfg.url, passed, checks: smoke }),
6135
+ [
5795
6136
  `openpouch verify ${passed ? "\u2713" : "\u2717"} ${environment} (${cfg.url})`,
5796
6137
  ...smoke.map((s) => ` ${s.passed ? "\u2713" : "\u2717"} ${s.name} \u2014 ${s.detail ?? ""}`),
5797
6138
  evidenceUpdated ? " evidence: smoke results appended to latest record" : " evidence: no deployment record for this environment yet"
5798
6139
  ]
5799
- };
6140
+ );
6141
+ }
6142
+
6143
+ // packages/cli/src/commands/whoami.ts
6144
+ async function whoamiCommand(ctx) {
6145
+ const key = await resolveProviderApiKey(ctx.env, "openpouch-run") ?? ANONYMOUS_KEY;
6146
+ if (key === ANONYMOUS_KEY) {
6147
+ return summarized(
6148
+ EXIT.OK,
6149
+ { ok: true, authenticated: false },
6150
+ whoamiSummary({ authenticated: false }),
6151
+ [
6152
+ "openpouch whoami \u2014 not signed in (anonymous instant lane)",
6153
+ " Deploys use the free anonymous tier.",
6154
+ " Create an account: openpouch signup --email <you@example.com> (or --github)"
6155
+ ]
6156
+ );
6157
+ }
6158
+ const adapter = makeAdapter(ctx, "openpouch-run", key);
6159
+ if (typeof adapter.whoami !== "function") {
6160
+ return errorResult(EXIT.UNEXPECTED, "provider", "account lookup unavailable", "This is an internal wiring error.");
6161
+ }
6162
+ try {
6163
+ const status = await adapter.whoami();
6164
+ const identities = status.account.identities.map((i) => i.label ?? i.value).join(", ") || "\u2014";
6165
+ return summarized(
6166
+ EXIT.OK,
6167
+ { ok: true, authenticated: true, account: status.account, tier: status.tier, usage: status.usage },
6168
+ whoamiSummary({
6169
+ authenticated: true,
6170
+ tier: status.tier.name,
6171
+ liveDeployments: status.usage.liveDeployments,
6172
+ maxLive: status.tier.maxLiveDeployments
6173
+ }),
6174
+ [
6175
+ `openpouch whoami \u2713 ${status.account.id} [${status.tier.name}]`,
6176
+ ` identities: ${identities}`,
6177
+ ` live apps: ${status.usage.liveDeployments}/${status.tier.maxLiveDeployments} (${status.usage.dynamicDeployments} running)`,
6178
+ ` deploys: ${status.usage.deploysLastHour}/hour, ${status.usage.deploysLastDay}/day`
6179
+ ]
6180
+ );
6181
+ } catch (e) {
6182
+ if (isProviderApiError(e)) {
6183
+ return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
6184
+ }
6185
+ throw e;
6186
+ }
5800
6187
  }
5801
6188
 
5802
6189
  // packages/cli/src/main.ts
@@ -5806,7 +6193,10 @@ var USAGE = [
5806
6193
  "usage: openpouch <command> [--json]",
5807
6194
  "",
5808
6195
  "commands:",
5809
- " deploy zero-config instant preview on openpouch's own infra (no account/key; deploy-then-claim)",
6196
+ " deploy zero-config instant preview on openpouch's own infra (anonymous, or under your account if a key is set)",
6197
+ " signup create an openpouch account: --email <addr> or --github (gives you an API key)",
6198
+ " activate finish an email signup: --account <id> --token <token> (saves your API key)",
6199
+ " whoami show the account behind your API key: tier + current usage",
5810
6200
  " init detect the project, write deploy.manifest.json + deploy.policy.json (idempotent; --force regenerates)",
5811
6201
  " inspect answer: what is deployed, where, on which commit, which env vars are missing",
5812
6202
  " plan per environment: policy decision, blockers, readiness, next steps",
@@ -5822,7 +6212,12 @@ var USAGE = [
5822
6212
  " --force (init) overwrite existing manifest/policy",
5823
6213
  " --env <name> (verify/logs/rollback) target environment, default: production",
5824
6214
  " --limit <n> (logs) number of log lines, default: 50",
6215
+ " --email <addr> (signup) create an account anchored to this email",
6216
+ " --github (signup) create an account via GitHub (prints the authorize URL)",
6217
+ " --account <id> (activate) the account id from signup",
6218
+ " --token <tok> (activate) the verification token from the signup email",
5825
6219
  "",
6220
+ "account key: deploys send Authorization: Bearer from OPENPOUCH_API_KEY or ~/.openpouch/openpouch-run.key (optional \u2014 no key = anonymous)",
5826
6221
  "exit codes: 0 ok \xB7 1 unexpected \xB7 2 usage \xB7 3 manifest/policy missing or invalid \xB7 4 auth \xB7 5 provider \xB7 6 policy gate (approval required/denied/human required) \xB7 7 deploy failed \xB7 8 verify failed"
5827
6222
  ].join("\n");
5828
6223
  function render(result2, json) {
@@ -5840,8 +6235,12 @@ async function run(argv, ctx) {
5840
6235
  json: { type: "boolean", default: false },
5841
6236
  force: { type: "boolean", default: false },
5842
6237
  help: { type: "boolean", default: false },
6238
+ github: { type: "boolean", default: false },
5843
6239
  env: { type: "string" },
5844
- limit: { type: "string" }
6240
+ limit: { type: "string" },
6241
+ email: { type: "string" },
6242
+ account: { type: "string" },
6243
+ token: { type: "string" }
5845
6244
  },
5846
6245
  allowPositionals: true
5847
6246
  });
@@ -5867,6 +6266,12 @@ async function run(argv, ctx) {
5867
6266
  switch (command) {
5868
6267
  case "deploy":
5869
6268
  return render(await instantCommand(ctx), json);
6269
+ case "signup":
6270
+ return render(await signupCommand(ctx, { email: parsed.values.email, github: parsed.values.github === true }), json);
6271
+ case "activate":
6272
+ return render(await activateCommand(ctx, { account: parsed.values.account, token: parsed.values.token }), json);
6273
+ case "whoami":
6274
+ return render(await whoamiCommand(ctx), json);
5870
6275
  case "init":
5871
6276
  return render(await initCommand(ctx, { force: parsed.values.force === true }), json);
5872
6277
  case "inspect":
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openpouch",
3
- "version": "0.1.0",
4
- "description": "openpouch 🦘 — the agent-native deployment control plane. Deploy any folder to a live URL in one command: npx openpouch deploy",
3
+ "version": "0.2.0",
4
+ "description": "openpouch 🦘 — agent-native hosting, built for coding agents. Deploy any folder to a live URL in one command: npx openpouch deploy",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "vercel",
29
29
  "preview"
30
30
  ],
31
- "homepage": "https://openpouch.sh",
31
+ "homepage": "https://openpouch.dev",
32
32
  "repository": {
33
33
  "type": "git",
34
34
  "url": "git+https://github.com/openpouch/openpouch.git"