openpouch 0.1.0 → 0.2.1

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 +616 -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,93 +4258,14 @@ 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
- }
4261
+ // packages/cli/src/commands/activate.ts
4262
+ import { mkdir, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
4263
+ import { dirname } from "node:path";
4343
4264
 
4344
4265
  // packages/cli/src/shared.ts
4345
- import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
4346
- import { homedir as homedir2 } from "node:os";
4347
- import { join as join3 } from "node:path";
4266
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
4267
+ import { homedir } from "node:os";
4268
+ import { join as join2 } from "node:path";
4348
4269
 
4349
4270
  // packages/adapter-render/dist/index.js
4350
4271
  var RenderApiError = class extends Error {
@@ -4481,29 +4402,36 @@ var RunApiError = class extends Error {
4481
4402
  category;
4482
4403
  status;
4483
4404
  fix;
4484
- constructor(status, detail) {
4485
- const category = status === 429 ? "quota" : "provider";
4405
+ constructor(status, detail, fix) {
4406
+ const category = status === 401 ? "auth" : status === 429 || status === 413 ? "quota" : "provider";
4486
4407
  super(`openpouch-run ${status}: ${detail}`);
4487
4408
  this.name = "RunApiError";
4488
4409
  this.status = status;
4489
4410
  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).";
4411
+ 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
4412
  }
4492
4413
  };
4493
4414
  function createRunAdapter(config) {
4494
4415
  const doFetch = config.fetchImpl ?? fetch;
4495
4416
  const base = config.apiBase.replace(/\/$/, "");
4417
+ const authHeader = config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {};
4496
4418
  async function api(path, init) {
4497
- const res = await doFetch(`${base}${path}`, init);
4419
+ const res = await doFetch(`${base}${path}`, {
4420
+ ...init,
4421
+ headers: { ...authHeader, ...init?.headers }
4422
+ });
4498
4423
  if (!res.ok) {
4499
4424
  let detail = res.statusText;
4425
+ let fix;
4500
4426
  try {
4501
4427
  const body = await res.json();
4502
4428
  if (body.error)
4503
4429
  detail = body.error;
4430
+ if (body.fix)
4431
+ fix = body.fix;
4504
4432
  } catch {
4505
4433
  }
4506
- throw new RunApiError(res.status, detail);
4434
+ throw new RunApiError(res.status, detail, fix);
4507
4435
  }
4508
4436
  return res.json();
4509
4437
  }
@@ -4519,6 +4447,26 @@ function createRunAdapter(config) {
4519
4447
  const body = await api("/api/deployments", { method: "POST", headers, body: tarball });
4520
4448
  return body;
4521
4449
  },
4450
+ async whoami() {
4451
+ return await api("/api/account");
4452
+ },
4453
+ async signupEmail(email) {
4454
+ return await api("/api/accounts", {
4455
+ method: "POST",
4456
+ headers: { "content-type": "application/json" },
4457
+ body: JSON.stringify({ email })
4458
+ });
4459
+ },
4460
+ async verifyEmail(accountId, token) {
4461
+ return await api("/api/accounts/verify", {
4462
+ method: "POST",
4463
+ headers: { "content-type": "application/json" },
4464
+ body: JSON.stringify({ account: accountId, token })
4465
+ });
4466
+ },
4467
+ githubStartUrl() {
4468
+ return `${base}/api/auth/github/start`;
4469
+ },
4522
4470
  async listServices() {
4523
4471
  return [];
4524
4472
  },
@@ -4673,7 +4621,154 @@ function createVercelAdapter(config) {
4673
4621
  };
4674
4622
  }
4675
4623
 
4624
+ // packages/cli/src/summary.ts
4625
+ function humanList(items) {
4626
+ if (items.length === 0) return "";
4627
+ if (items.length === 1) return items[0] ?? "";
4628
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
4629
+ }
4630
+ function friendlyUtc(iso) {
4631
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/.exec(iso);
4632
+ return m ? `${m[1]} ${m[2]} UTC` : iso;
4633
+ }
4634
+ function instantDeploySummary(i) {
4635
+ 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.`;
4636
+ }
4637
+ function deploySummary(i) {
4638
+ const at = i.url !== void 0 ? ` at ${i.url}` : "";
4639
+ const approved = i.approvedBy !== void 0 ? `With your approval, ` : "";
4640
+ if (!i.live) {
4641
+ 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.`;
4642
+ }
4643
+ if (i.smokePassed === false) {
4644
+ 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.`;
4645
+ }
4646
+ const checked = i.smokePassed === true ? ` and I checked that it's responding correctly` : "";
4647
+ const lead = approved !== "" ? approved : "Your ";
4648
+ const subject = approved !== "" ? `your ${i.environment} app` : `${i.environment} app`;
4649
+ return `${lead}${subject} is live${at}${checked}. Nothing else is needed from you.`;
4650
+ }
4651
+ function inspectSummary(projectName, environments) {
4652
+ const names = Object.keys(environments);
4653
+ if (names.length === 0) {
4654
+ return `"${projectName}" isn't connected to any live service yet, so there's nothing deployed.`;
4655
+ }
4656
+ const lines = [`Here's the current status of "${projectName}":`];
4657
+ for (const [name, env] of Object.entries(environments)) {
4658
+ if (env.status === "unmapped" || env.service === void 0) {
4659
+ lines.push(`\u2022 ${name}: not connected to a live service yet.`);
4660
+ continue;
4661
+ }
4662
+ const where = env.service.url !== void 0 ? `live at ${env.service.url}` : `set up (no public address yet)`;
4663
+ const paused = env.service.suspended ? " It's currently paused." : "";
4664
+ const missing = env.envVars?.missingRequired ?? [];
4665
+ const needs = missing.length > 0 ? ` It still needs ${missing.length === 1 ? "this setting" : "these settings"} before it can fully work: ${humanList(missing)}.` : "";
4666
+ lines.push(`\u2022 ${name}: ${where}.${paused}${needs}`);
4667
+ }
4668
+ return lines.join("\n");
4669
+ }
4670
+ function verifySummary(i) {
4671
+ const total = i.checks.length;
4672
+ const passedCount = i.checks.filter((c) => c.passed).length;
4673
+ if (i.passed) {
4674
+ return `Good news \u2014 your ${i.environment} app at ${i.url} is healthy and responding correctly. ${total === 1 ? "The check" : `All ${total} checks`} passed.`;
4675
+ }
4676
+ const firstFail = i.checks.find((c) => !c.passed);
4677
+ const reason = firstFail?.detail !== void 0 ? ` (${firstFail.detail})` : "";
4678
+ 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.`;
4679
+ }
4680
+ function planSummary(projectName, plans) {
4681
+ const names = Object.keys(plans);
4682
+ if (names.length === 0) {
4683
+ return `"${projectName}" has no environments set up to deploy yet.`;
4684
+ }
4685
+ const lines = [`Here's what can be published for "${projectName}":`];
4686
+ for (const [name, p] of Object.entries(plans)) {
4687
+ if (!p.ready) {
4688
+ lines.push(`\u2022 ${name}: not ready yet \u2014 ${humanList(p.blockers)}.`);
4689
+ } else if (p.decision === "requires-approval") {
4690
+ lines.push(`\u2022 ${name}: ready, but it will need your approval before it goes live.`);
4691
+ } else {
4692
+ lines.push(`\u2022 ${name}: ready to go live.`);
4693
+ }
4694
+ }
4695
+ return lines.join("\n");
4696
+ }
4697
+ function initSummary(i) {
4698
+ if (i.alreadyInitialized === true) {
4699
+ return `"${i.name}" is already set up for deployment. You can deploy it whenever you'd like.`;
4700
+ }
4701
+ const kind = i.framework !== void 0 ? ` (a ${i.framework} app)` : "";
4702
+ const connected = i.matchedProvider !== void 0 ? ` I connected it to your existing ${i.matchedProvider} service.` : "";
4703
+ 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.`;
4704
+ }
4705
+ function logsSummary(environment, count) {
4706
+ if (count === 0) {
4707
+ return `There are no recent log entries for your ${environment} app.`;
4708
+ }
4709
+ return `Here are the ${count} most recent log entries from your ${environment} app \u2014 these are mainly useful for spotting errors.`;
4710
+ }
4711
+ function rollbackSummary(i) {
4712
+ if (!i.live) {
4713
+ return `The rollback did not complete \u2014 the previous version of your ${i.environment} app could not be brought back online. I can investigate.`;
4714
+ }
4715
+ const at = i.url !== void 0 ? ` at ${i.url}` : "";
4716
+ 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.` : "";
4717
+ return `I restored your ${i.environment} app to the previous working version. It's live again${at}.${health}`;
4718
+ }
4719
+ function approveListSummary(pending) {
4720
+ if (pending.length === 0) {
4721
+ return `Nothing is waiting for your approval right now.`;
4722
+ }
4723
+ const ids = pending.map((p) => p.id);
4724
+ 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(" ; ")}`;
4725
+ }
4726
+ function approveResultSummary(approved) {
4727
+ return approved ? `Approved. The change can now be published.` : `Not approved \u2014 nothing was published.`;
4728
+ }
4729
+ function approvalRequiredSummary(environment, requestId) {
4730
+ 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.`;
4731
+ }
4732
+ function signupEmailSummary(email) {
4733
+ 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.`;
4734
+ }
4735
+ function signupGithubSummary(url) {
4736
+ 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.`;
4737
+ }
4738
+ function activateSummary(saved) {
4739
+ 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.`;
4740
+ }
4741
+ function whoamiSummary(i) {
4742
+ if (!i.authenticated) {
4743
+ 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.`;
4744
+ }
4745
+ const using = i.liveDeployments !== void 0 && i.maxLive !== void 0 ? ` You're using ${i.liveDeployments} of your ${i.maxLive} live apps.` : "";
4746
+ return `You're signed in to openpouch on the ${i.tier} plan.${using} Everything looks fine.`;
4747
+ }
4748
+ var ERROR_LEADS = {
4749
+ auth: "I couldn't sign in to the hosting provider \u2014 an access key is missing or invalid.",
4750
+ "policy-denied": "This action isn't allowed by your current safety settings.",
4751
+ "human-required": "This needs a person to approve it \u2014 an agent can't do it.",
4752
+ env: "This project isn't fully set up for deployment yet.",
4753
+ quota: "The hosting provider reported that a limit was reached.",
4754
+ provider: "The hosting provider ran into a problem.",
4755
+ usage: "That command wasn't used correctly.",
4756
+ internal: "Something went wrong inside openpouch."
4757
+ };
4758
+ function errorSummary(category, message, fix) {
4759
+ const lead = ERROR_LEADS[category] ?? `Something didn't work: ${message}.`;
4760
+ return `${lead} What to do next: ${fix}`;
4761
+ }
4762
+
4676
4763
  // packages/cli/src/shared.ts
4764
+ function summarized(exitCode, json, summary, human) {
4765
+ const { ok, ...rest } = json;
4766
+ return {
4767
+ exitCode,
4768
+ json: { ok, summary, ...rest },
4769
+ human: [...human, "", summary]
4770
+ };
4771
+ }
4677
4772
  var EXIT = {
4678
4773
  OK: 0,
4679
4774
  UNEXPECTED: 1,
@@ -4690,16 +4785,17 @@ function isProviderApiError(e) {
4690
4785
  return e instanceof Error && "category" in e && "fix" in e;
4691
4786
  }
4692
4787
  function errorResult(exitCode, category, message, fix) {
4788
+ const summary = errorSummary(category, message, fix);
4693
4789
  return {
4694
4790
  exitCode,
4695
- json: { ok: false, error: { category, message, fix } },
4696
- human: [`error [${category}]: ${message}`, `fix: ${fix}`]
4791
+ json: { ok: false, summary, error: { category, message, fix } },
4792
+ human: [`error [${category}]: ${message}`, `fix: ${fix}`, "", summary]
4697
4793
  };
4698
4794
  }
4699
4795
  async function loadManifest(cwd) {
4700
4796
  let raw;
4701
4797
  try {
4702
- raw = await readFile3(join3(cwd, MANIFEST_FILENAME), "utf8");
4798
+ raw = await readFile2(join2(cwd, MANIFEST_FILENAME), "utf8");
4703
4799
  } catch {
4704
4800
  return { status: "missing" };
4705
4801
  }
@@ -4713,13 +4809,13 @@ async function loadManifest(cwd) {
4713
4809
  return result2.ok ? { status: "ok", manifest: result2.value } : { status: "invalid", errors: result2.errors };
4714
4810
  }
4715
4811
  async function writeJsonFile(path, value) {
4716
- await writeFile3(path, `${JSON.stringify(value, null, 2)}
4812
+ await writeFile2(path, `${JSON.stringify(value, null, 2)}
4717
4813
  `, "utf8");
4718
4814
  }
4719
4815
  async function loadPolicy(cwd) {
4720
4816
  let raw;
4721
4817
  try {
4722
- raw = await readFile3(join3(cwd, POLICY_FILENAME), "utf8");
4818
+ raw = await readFile2(join2(cwd, POLICY_FILENAME), "utf8");
4723
4819
  } catch {
4724
4820
  return { status: "default", policy: defaultPolicy(), usedDefault: true };
4725
4821
  }
@@ -4734,15 +4830,32 @@ var PROVIDER_CREDENTIALS = {
4734
4830
  vercel: { envVar: "VERCEL_TOKEN", file: "vercel.token" }
4735
4831
  };
4736
4832
  var ANONYMOUS_KEY = "anonymous";
4833
+ var RUN_KEY = { envVar: "OPENPOUCH_API_KEY", file: "openpouch-run.key" };
4834
+ function runKeyPath(env) {
4835
+ const override = env["OPENPOUCH_KEY_FILE"]?.trim();
4836
+ if (override) return override;
4837
+ const home = env["OPENPOUCH_HOME"] ?? homedir();
4838
+ return join2(home, ".openpouch", RUN_KEY.file);
4839
+ }
4840
+ async function resolveRunKey(env) {
4841
+ const fromEnv = env[RUN_KEY.envVar]?.trim();
4842
+ if (fromEnv) return fromEnv;
4843
+ try {
4844
+ const fromFile = (await readFile2(runKeyPath(env), "utf8")).trim();
4845
+ if (fromFile.length > 0) return fromFile;
4846
+ } catch {
4847
+ }
4848
+ return ANONYMOUS_KEY;
4849
+ }
4737
4850
  async function resolveProviderApiKey(env, provider) {
4738
- if (provider === "openpouch-run") return ANONYMOUS_KEY;
4851
+ if (provider === "openpouch-run") return resolveRunKey(env);
4739
4852
  const cred = PROVIDER_CREDENTIALS[provider];
4740
4853
  if (!cred) return void 0;
4741
4854
  const fromEnv = env[cred.envVar]?.trim();
4742
4855
  if (fromEnv) return fromEnv;
4743
4856
  try {
4744
- const home = env["OPENPOUCH_HOME"] ?? homedir2();
4745
- const fromFile = (await readFile3(join3(home, ".openpouch", cred.file), "utf8")).trim();
4857
+ const home = env["OPENPOUCH_HOME"] ?? homedir();
4858
+ const fromFile = (await readFile2(join2(home, ".openpouch", cred.file), "utf8")).trim();
4746
4859
  return fromFile.length > 0 ? fromFile : void 0;
4747
4860
  } catch {
4748
4861
  return void 0;
@@ -4767,7 +4880,10 @@ function missingKeyError(provider) {
4767
4880
  function makeAdapter(ctx, provider, apiKey) {
4768
4881
  if (ctx.createAdapter !== void 0) return ctx.createAdapter(provider, apiKey);
4769
4882
  if (provider === "openpouch-run") {
4770
- return createRunAdapter({ apiBase: runApiBase(ctx.env) });
4883
+ return createRunAdapter({
4884
+ apiBase: runApiBase(ctx.env),
4885
+ ...apiKey && apiKey !== ANONYMOUS_KEY ? { apiKey } : {}
4886
+ });
4771
4887
  }
4772
4888
  if (provider === "vercel") {
4773
4889
  const teamId = ctx.env["VERCEL_TEAM_ID"]?.trim();
@@ -4776,6 +4892,169 @@ function makeAdapter(ctx, provider, apiKey) {
4776
4892
  return createRenderAdapter({ apiKey });
4777
4893
  }
4778
4894
 
4895
+ // packages/cli/src/commands/activate.ts
4896
+ function keyPublicPrefix(key) {
4897
+ const m = /^(op_live_[0-9a-f]+)_/.exec(key);
4898
+ return m ? m[1] : "op_live_\u2026";
4899
+ }
4900
+ function looksLikeOpenpouchKey(content) {
4901
+ const t = content.trim();
4902
+ if (t.length === 0) return true;
4903
+ if (t.includes("\n")) return false;
4904
+ return /^op_(?:live|test)_[0-9a-f]{6,}_[A-Za-z0-9_-]+$/.test(t);
4905
+ }
4906
+ function looksLikePrivateKey(content) {
4907
+ return /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/.test(content);
4908
+ }
4909
+ async function activateCommand(ctx, opts) {
4910
+ const account = opts.account?.trim();
4911
+ const token = opts.token?.trim();
4912
+ if (!account || !token) {
4913
+ return errorResult(
4914
+ EXIT.USAGE,
4915
+ "usage",
4916
+ "activate needs --account and --token",
4917
+ "Run `openpouch signup --email <you@example.com>` first; the verification email contains both."
4918
+ );
4919
+ }
4920
+ const adapter = makeAdapter(ctx, "openpouch-run", "anonymous");
4921
+ if (typeof adapter.verifyEmail !== "function") {
4922
+ return errorResult(EXIT.UNEXPECTED, "provider", "activation unavailable", "This is an internal wiring error.");
4923
+ }
4924
+ try {
4925
+ const res = await adapter.verifyEmail(account, token);
4926
+ let saved = false;
4927
+ let reason;
4928
+ const keyPath = opts.keyFile?.trim() || runKeyPath(ctx.env);
4929
+ try {
4930
+ let existing = null;
4931
+ try {
4932
+ existing = await readFile3(keyPath, "utf8");
4933
+ } catch (e) {
4934
+ if (e.code !== "ENOENT") throw e;
4935
+ }
4936
+ if (existing !== null && existing.trim().length > 0 && !looksLikeOpenpouchKey(existing)) {
4937
+ const how = "Set OPENPOUCH_KEY_FILE (or pass --key-file) to a free path, or move that file.";
4938
+ reason = looksLikePrivateKey(existing) ? `refusing to overwrite ${keyPath} \u2014 it looks like an existing private key. ${how}` : `refusing to overwrite ${keyPath} \u2014 it is not an openpouch key file. ${how}`;
4939
+ } else {
4940
+ await mkdir(dirname(keyPath), { recursive: true });
4941
+ if (existing !== null && existing.trim().length > 0) {
4942
+ await writeFile3(`${keyPath}.bak`, existing, { mode: 384 });
4943
+ }
4944
+ await writeFile3(keyPath, `${res.key}
4945
+ `, { mode: 384 });
4946
+ saved = true;
4947
+ }
4948
+ } catch (e) {
4949
+ reason = `could not save the key to ${keyPath}: ${e.message}`;
4950
+ }
4951
+ return summarized(
4952
+ EXIT.OK,
4953
+ {
4954
+ ok: true,
4955
+ account: res.account,
4956
+ keyPrefix: keyPublicPrefix(res.key),
4957
+ saved,
4958
+ ...saved ? { keyPath } : { reason, key: res.key }
4959
+ },
4960
+ activateSummary(saved),
4961
+ [
4962
+ `openpouch activate \u2713 ${res.account.id} [${res.account.tier}]`,
4963
+ ...saved ? [` saved your key to ${keyPath} (chmod 600) \u2014 deploys now run under your account`] : [` \u26A0 key NOT saved: ${reason}`, ` \u2192 use it now: export OPENPOUCH_API_KEY=${res.key}`],
4964
+ ` key: ${keyPublicPrefix(res.key)}\u2026`
4965
+ ]
4966
+ );
4967
+ } catch (e) {
4968
+ if (isProviderApiError(e)) {
4969
+ return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
4970
+ }
4971
+ throw e;
4972
+ }
4973
+ }
4974
+
4975
+ // packages/cli/src/commands/approve.ts
4976
+ import { createInterface } from "node:readline/promises";
4977
+
4978
+ // packages/cli/src/approvals.ts
4979
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
4980
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
4981
+ import { homedir as homedir2, userInfo } from "node:os";
4982
+ import { join as join3 } from "node:path";
4983
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
4984
+ async function loadApprovals(cwd) {
4985
+ try {
4986
+ const raw = await readFile4(join3(cwd, APPROVALS_FILENAME), "utf8");
4987
+ return approvalsFileSchema.parse(JSON.parse(raw));
4988
+ } catch {
4989
+ return { version: 0, requests: [] };
4990
+ }
4991
+ }
4992
+ async function saveApprovals(cwd, file) {
4993
+ await mkdir2(join3(cwd, APPROVALS_DIR), { recursive: true });
4994
+ await ensureGitignored(cwd);
4995
+ await writeFile4(
4996
+ join3(cwd, APPROVALS_FILENAME),
4997
+ `${JSON.stringify(approvalsFileSchema.parse(file), null, 2)}
4998
+ `,
4999
+ "utf8"
5000
+ );
5001
+ }
5002
+ async function ensureGitignored(cwd) {
5003
+ const path = join3(cwd, ".gitignore");
5004
+ let content = "";
5005
+ try {
5006
+ content = await readFile4(path, "utf8");
5007
+ } catch {
5008
+ }
5009
+ if (!content.split("\n").some((line) => line.trim() === ".openpouch/")) {
5010
+ const addition = `${content.length > 0 && !content.endsWith("\n") ? "\n" : ""}.openpouch/
5011
+ `;
5012
+ await writeFile4(path, content + addition, "utf8");
5013
+ }
5014
+ }
5015
+ function newApprovalRequest(match, now) {
5016
+ return {
5017
+ id: randomBytes(4).toString("hex"),
5018
+ action: match.action,
5019
+ environment: match.environment,
5020
+ serviceId: match.serviceId,
5021
+ requestedAt: now.toISOString(),
5022
+ expiresAt: new Date(now.getTime() + DEFAULT_TTL_MS).toISOString(),
5023
+ status: "pending"
5024
+ };
5025
+ }
5026
+ async function getApproverSecret(createIfMissing, home) {
5027
+ const dir = join3(home ?? homedir2(), ".openpouch");
5028
+ const path = join3(dir, "approver.secret");
5029
+ try {
5030
+ const existing = (await readFile4(path, "utf8")).trim();
5031
+ if (existing.length > 0) return existing;
5032
+ } catch {
5033
+ }
5034
+ if (!createIfMissing) return void 0;
5035
+ const secret = randomBytes(32).toString("hex");
5036
+ await mkdir2(dir, { recursive: true });
5037
+ await writeFile4(path, `${secret}
5038
+ `, { mode: 384 });
5039
+ return secret;
5040
+ }
5041
+ function signApproval(request, secret) {
5042
+ return createHmac("sha256", secret).update(canonicalApprovalPayload(request)).digest("hex");
5043
+ }
5044
+ function signatureValid(request, secret) {
5045
+ if (request.signature === void 0) return false;
5046
+ const expected = Buffer.from(signApproval(request, secret), "hex");
5047
+ const actual = Buffer.from(request.signature, "hex");
5048
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
5049
+ }
5050
+ function approverName() {
5051
+ try {
5052
+ return userInfo().username;
5053
+ } catch {
5054
+ return "unknown";
5055
+ }
5056
+ }
5057
+
4779
5058
  // packages/cli/src/commands/approve.ts
4780
5059
  async function realConfirm(question) {
4781
5060
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -4788,15 +5067,16 @@ async function approveCommand(ctx, requestId) {
4788
5067
  const now = /* @__PURE__ */ new Date();
4789
5068
  if (requestId === void 0) {
4790
5069
  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"] : [
5070
+ return summarized(
5071
+ EXIT.OK,
5072
+ { ok: true, pending },
5073
+ approveListSummary(pending),
5074
+ pending.length === 0 ? ["no pending approval requests"] : [
4795
5075
  "pending approval requests:",
4796
5076
  ...pending.map((r) => ` ${r.id} ${r.action} on ${r.environment} (service ${r.serviceId}, expires ${r.expiresAt})`),
4797
5077
  "approve with: openpouch approve <id>"
4798
5078
  ]
4799
- };
5079
+ );
4800
5080
  }
4801
5081
  const request = approvals.requests.find((r) => r.id === requestId);
4802
5082
  if (request === void 0) {
@@ -4826,7 +5106,7 @@ async function approveCommand(ctx, requestId) {
4826
5106
  `Approve "${request.action}" on ${request.environment} (service ${request.serviceId})? [y/N] `
4827
5107
  );
4828
5108
  if (!confirmed) {
4829
- return { exitCode: EXIT.OK, json: { ok: true, approved: false }, human: ["not approved"] };
5109
+ return summarized(EXIT.OK, { ok: true, approved: false }, approveResultSummary(false), ["not approved"]);
4830
5110
  }
4831
5111
  const secret = await getApproverSecret(true, ctx.env["OPENPOUCH_HOME"]);
4832
5112
  if (secret === void 0) {
@@ -4837,14 +5117,15 @@ async function approveCommand(ctx, requestId) {
4837
5117
  request.approvedBy = approverName();
4838
5118
  request.signature = signApproval(request, secret);
4839
5119
  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: [
5120
+ return summarized(
5121
+ EXIT.OK,
5122
+ { ok: true, approved: true, request: { id: request.id, approvedBy: request.approvedBy, approvedAt: request.approvedAt } },
5123
+ approveResultSummary(true),
5124
+ [
4844
5125
  `approved \u2713 ${request.id} (${request.action} on ${request.environment}) by ${request.approvedBy}`,
4845
5126
  "the agent can now re-run the deploy command"
4846
5127
  ]
4847
- };
5128
+ );
4848
5129
  }
4849
5130
 
4850
5131
  // packages/cli/src/deploy-engine.ts
@@ -4956,12 +5237,14 @@ async function passPolicyGate(ctx, policy, environment, action, serviceId, rerun
4956
5237
  approvals.requests.push(request);
4957
5238
  await saveApprovals(ctx.cwd, approvals);
4958
5239
  }
5240
+ const summary = approvalRequiredSummary(environment, request.id);
4959
5241
  return {
4960
5242
  ok: false,
4961
5243
  result: {
4962
5244
  exitCode: EXIT.POLICY,
4963
5245
  json: {
4964
5246
  ok: false,
5247
+ summary,
4965
5248
  error: {
4966
5249
  category: "approval-required",
4967
5250
  message: `"${action}" on ${environment} requires human approval`,
@@ -4972,7 +5255,9 @@ async function passPolicyGate(ctx, policy, environment, action, serviceId, rerun
4972
5255
  human: [
4973
5256
  `approval required: "${action}" on ${environment}`,
4974
5257
  `request id: ${request.id} (expires ${request.expiresAt})`,
4975
- `next: a human runs \`openpouch approve ${request.id}\`, then re-run \`${rerunCommand}\``
5258
+ `next: a human runs \`openpouch approve ${request.id}\`, then re-run \`${rerunCommand}\``,
5259
+ "",
5260
+ summary
4976
5261
  ]
4977
5262
  }
4978
5263
  };
@@ -5026,15 +5311,24 @@ async function deployCommand(ctx, environment) {
5026
5311
  });
5027
5312
  const exitCode = !result2.deployLive ? EXIT.DEPLOY_FAILED : result2.smokePassed === false ? EXIT.VERIFY_FAILED : EXIT.OK;
5028
5313
  const d = result2.record;
5029
- return {
5314
+ return summarized(
5030
5315
  exitCode,
5031
- json: {
5316
+ {
5032
5317
  ok: exitCode === EXIT.OK,
5318
+ // R5: live link prominent at the top level when the deploy went live.
5319
+ ...d.url !== void 0 && result2.deployLive ? { url: d.url } : {},
5033
5320
  deployment: d,
5034
5321
  smokePassed: result2.smokePassed ?? null,
5035
5322
  evidence: ["deploy.evidence.json", "DEPLOYMENT.md"]
5036
5323
  },
5037
- human: [
5324
+ deploySummary({
5325
+ environment,
5326
+ live: result2.deployLive,
5327
+ url: d.url,
5328
+ smokePassed: result2.smokePassed,
5329
+ approvedBy: d.approvedBy
5330
+ }),
5331
+ [
5038
5332
  `openpouch ${environment === "preview" ? "preview" : "prod"} ${exitCode === EXIT.OK ? "\u2713" : "\u2717"} ${manifest.project.name} \u2192 ${environment}`,
5039
5333
  ` deploy: ${d.id} [${d.status}]${d.commit !== void 0 ? ` commit ${d.commit.slice(0, 10)}` : ""}`,
5040
5334
  ...d.url !== void 0 ? [` url: ${d.url}`] : [],
@@ -5043,7 +5337,7 @@ async function deployCommand(ctx, environment) {
5043
5337
  ...d.rollbackAnchor !== void 0 ? [` rollback anchor: ${d.rollbackAnchor}`] : [],
5044
5338
  ` evidence: deploy.evidence.json + DEPLOYMENT.md updated`
5045
5339
  ]
5046
- };
5340
+ );
5047
5341
  } catch (e) {
5048
5342
  if (isProviderApiError(e)) {
5049
5343
  return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5056,7 +5350,7 @@ async function deployCommand(ctx, environment) {
5056
5350
  import { join as join6 } from "node:path";
5057
5351
 
5058
5352
  // packages/cli/src/agentsmd.ts
5059
- import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
5353
+ import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
5060
5354
  import { join as join4 } from "node:path";
5061
5355
  var AGENTS_MD_FILENAME = "AGENTS.md";
5062
5356
  var START = "<!-- openpouch:start -->";
@@ -5071,7 +5365,8 @@ function renderAgentsSection(projectName) {
5071
5365
  "",
5072
5366
  "- **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
5367
  "- **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.",
5368
+ "- **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.",
5369
+ "- **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
5370
  "- **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
5371
  "- Never write secret values into any of these files; env vars are tracked by name only.",
5077
5372
  "",
@@ -5083,12 +5378,12 @@ async function upsertAgentsSection(cwd, projectName) {
5083
5378
  const section = renderAgentsSection(projectName);
5084
5379
  let existing;
5085
5380
  try {
5086
- existing = await readFile4(path, "utf8");
5381
+ existing = await readFile5(path, "utf8");
5087
5382
  } catch {
5088
5383
  existing = void 0;
5089
5384
  }
5090
5385
  if (existing === void 0) {
5091
- await writeFile4(path, `# AGENTS.md
5386
+ await writeFile5(path, `# AGENTS.md
5092
5387
 
5093
5388
  ${section}
5094
5389
  `, "utf8");
@@ -5105,12 +5400,12 @@ ${section}
5105
5400
  `;
5106
5401
  }
5107
5402
  if (next === existing) return "unchanged";
5108
- await writeFile4(path, next, "utf8");
5403
+ await writeFile5(path, next, "utf8");
5109
5404
  return "updated";
5110
5405
  }
5111
5406
 
5112
5407
  // packages/cli/src/detect.ts
5113
- import { readFile as readFile5, readdir } from "node:fs/promises";
5408
+ import { readFile as readFile6, readdir } from "node:fs/promises";
5114
5409
  import { basename, extname, join as join5 } from "node:path";
5115
5410
  var FRAMEWORK_BY_DEP = [
5116
5411
  ["next", "nextjs"],
@@ -5163,7 +5458,7 @@ async function scanEnvVarNames(root) {
5163
5458
  for (const file of await collectSourceFiles(root)) {
5164
5459
  let content;
5165
5460
  try {
5166
- const buf = await readFile5(file);
5461
+ const buf = await readFile6(file);
5167
5462
  if (buf.byteLength > MAX_FILE_BYTES) continue;
5168
5463
  content = buf.toString("utf8");
5169
5464
  } catch {
@@ -5181,7 +5476,7 @@ async function scanEnvVarNames(root) {
5181
5476
  async function detectProject(cwd) {
5182
5477
  let pkg = {};
5183
5478
  try {
5184
- pkg = JSON.parse(await readFile5(join5(cwd, "package.json"), "utf8"));
5479
+ pkg = JSON.parse(await readFile6(join5(cwd, "package.json"), "utf8"));
5185
5480
  } catch {
5186
5481
  }
5187
5482
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -5223,21 +5518,23 @@ function matchService(projectName, services) {
5223
5518
  async function initCommand(ctx, flags) {
5224
5519
  const existing = await loadManifest(ctx.cwd);
5225
5520
  if (existing.status !== "missing" && !flags.force) {
5521
+ const existingName = existing.status === "ok" ? existing.manifest.project.name : "this project";
5226
5522
  const agentsMd2 = existing.status === "ok" ? await upsertAgentsSection(ctx.cwd, existing.manifest.project.name) : "unchanged";
5227
- return {
5228
- exitCode: EXIT.OK,
5229
- json: {
5523
+ return summarized(
5524
+ EXIT.OK,
5525
+ {
5230
5526
  ok: true,
5231
5527
  alreadyInitialized: true,
5232
5528
  agentsMd: agentsMd2,
5233
5529
  hints: [`${MANIFEST_FILENAME} exists \u2014 use --force to regenerate, or run \`openpouch inspect\``]
5234
5530
  },
5235
- human: [
5531
+ initSummary({ name: existingName, alreadyInitialized: true }),
5532
+ [
5236
5533
  `openpouch init: already initialized (${MANIFEST_FILENAME} exists)`,
5237
5534
  ...agentsMd2 !== "unchanged" ? [` ${AGENTS_MD_FILENAME}: openpouch section ${agentsMd2}`] : [],
5238
5535
  "hint: use --force to regenerate, or run `openpouch inspect`"
5239
5536
  ]
5240
- };
5537
+ );
5241
5538
  }
5242
5539
  const detected = await detectProject(ctx.cwd);
5243
5540
  const hints = [];
@@ -5293,9 +5590,9 @@ async function initCommand(ctx, flags) {
5293
5590
  await writeJsonFile(join6(ctx.cwd, POLICY_FILENAME), defaultPolicy());
5294
5591
  const agentsMd = await upsertAgentsSection(ctx.cwd, detected.name);
5295
5592
  hints.push("policy default: previews autonomous, production requires approval \u2014 edit deploy.policy.json to change");
5296
- return {
5297
- exitCode: EXIT.OK,
5298
- json: {
5593
+ return summarized(
5594
+ EXIT.OK,
5595
+ {
5299
5596
  ok: true,
5300
5597
  created: [MANIFEST_FILENAME, POLICY_FILENAME],
5301
5598
  detected: {
@@ -5309,7 +5606,12 @@ async function initCommand(ctx, flags) {
5309
5606
  agentsMd,
5310
5607
  hints
5311
5608
  },
5312
- human: [
5609
+ initSummary({
5610
+ name: detected.name,
5611
+ framework: detected.framework,
5612
+ ...matched !== void 0 ? { matchedProvider: "render" } : {}
5613
+ }),
5614
+ [
5313
5615
  `openpouch init \u2713 ${detected.name}${detected.framework !== void 0 ? ` (${detected.framework})` : ""}`,
5314
5616
  ` created: ${MANIFEST_FILENAME}, ${POLICY_FILENAME}`,
5315
5617
  ` ${AGENTS_MD_FILENAME}: openpouch section ${agentsMd} (cross-harness discovery)`,
@@ -5320,12 +5622,12 @@ async function initCommand(ctx, flags) {
5320
5622
  ...hints.map((h) => ` hint: ${h}`),
5321
5623
  " next: openpouch inspect"
5322
5624
  ]
5323
- };
5625
+ );
5324
5626
  }
5325
5627
 
5326
5628
  // packages/cli/src/commands/instant.ts
5327
5629
  import { spawn } from "node:child_process";
5328
- import { readFile as readFile6 } from "node:fs/promises";
5630
+ import { readFile as readFile7 } from "node:fs/promises";
5329
5631
  import { basename as basename2, join as join7 } from "node:path";
5330
5632
  var TAR_EXCLUDES = [
5331
5633
  "./node_modules",
@@ -5355,7 +5657,7 @@ function buildTarball(cwd) {
5355
5657
  }
5356
5658
  async function projectHint(cwd) {
5357
5659
  try {
5358
- const pkg = JSON.parse(await readFile6(join7(cwd, "package.json"), "utf8"));
5660
+ const pkg = JSON.parse(await readFile7(join7(cwd, "package.json"), "utf8"));
5359
5661
  if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
5360
5662
  } catch {
5361
5663
  }
@@ -5364,12 +5666,16 @@ async function projectHint(cwd) {
5364
5666
  async function instantCommand(ctx) {
5365
5667
  const loaded = await loadManifest(ctx.cwd);
5366
5668
  if (loaded.status === "ok") {
5367
- return errorResult(
5368
- EXIT.USAGE,
5369
- "usage",
5370
- "this project already has deploy.manifest.json",
5371
- "Use `openpouch preview` / `openpouch prod` for a configured project. `openpouch deploy` is the zero-config instant lane."
5372
- );
5669
+ const envs = Object.values(loaded.manifest.environments ?? {});
5670
+ const hasGovernedEnv = envs.some((e) => e.provider !== "openpouch-run");
5671
+ if (hasGovernedEnv) {
5672
+ return errorResult(
5673
+ EXIT.USAGE,
5674
+ "usage",
5675
+ "this project already has deploy.manifest.json",
5676
+ "Use `openpouch preview` / `openpouch prod` for a configured project. `openpouch deploy` is the zero-config instant lane."
5677
+ );
5678
+ }
5373
5679
  }
5374
5680
  let tarball;
5375
5681
  try {
@@ -5381,7 +5687,8 @@ async function instantCommand(ctx) {
5381
5687
  return errorResult(EXIT.USAGE, "usage", "nothing to deploy", "The directory appears empty after excludes.");
5382
5688
  }
5383
5689
  const hint = await projectHint(ctx.cwd);
5384
- const adapter = makeAdapter(ctx, "openpouch-run", "anonymous");
5690
+ const runKey = await resolveProviderApiKey(ctx.env, "openpouch-run") ?? "anonymous";
5691
+ const adapter = makeAdapter(ctx, "openpouch-run", runKey);
5385
5692
  if (typeof adapter.instantDeploy !== "function") {
5386
5693
  return errorResult(EXIT.UNEXPECTED, "provider", "instant lane adapter unavailable", "This is an internal wiring error.");
5387
5694
  }
@@ -5406,27 +5713,33 @@ async function instantCommand(ctx) {
5406
5713
  deployedAt: (/* @__PURE__ */ new Date()).toISOString(),
5407
5714
  notes: `instant preview \u2014 expires ${result2.expiresAt}; claim: ${result2.claimUrl}`
5408
5715
  });
5409
- return {
5410
- exitCode: EXIT.OK,
5411
- json: {
5716
+ const kind = result2.kind ?? "static";
5717
+ return summarized(
5718
+ EXIT.OK,
5719
+ {
5412
5720
  ok: true,
5721
+ // R5: the live link is the primary, immediately shareable result — top-level.
5722
+ url: result2.url,
5723
+ claimUrl: result2.claimUrl,
5413
5724
  deployment: {
5414
5725
  id: result2.slug,
5415
5726
  url: result2.url,
5416
5727
  claimUrl: result2.claimUrl,
5417
5728
  expiresAt: result2.expiresAt,
5418
- status: result2.status
5729
+ status: result2.status,
5730
+ kind
5419
5731
  },
5420
5732
  evidence: ["deploy.manifest.json", "deploy.policy.json", "deploy.evidence.json", "DEPLOYMENT.md"]
5421
5733
  },
5422
- human: [
5423
- `openpouch deploy \u2713 ${hint} \u2192 instant preview`,
5734
+ instantDeploySummary({ name: hint, url: result2.url, claimUrl: result2.claimUrl, expiresAt: result2.expiresAt }),
5735
+ [
5736
+ `openpouch deploy \u2713 ${hint} \u2192 instant preview${kind === "dynamic" ? " (dynamic app \u2014 running in a container)" : ""}`,
5424
5737
  ` url: ${result2.url}`,
5425
5738
  ` expires: ${result2.expiresAt} (unclaimed previews vanish after 72h)`,
5426
5739
  ` claim: ${result2.claimUrl}`,
5427
5740
  ` wrote: deploy.manifest.json, deploy.policy.json, deploy.evidence.json, DEPLOYMENT.md`
5428
5741
  ]
5429
- };
5742
+ );
5430
5743
  } catch (e) {
5431
5744
  if (isProviderApiError(e)) {
5432
5745
  return errorResult(EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5555,16 +5868,17 @@ async function inspectCommand(ctx) {
5555
5868
  }
5556
5869
  }
5557
5870
  human.push(...hints.map((h) => ` hint: ${h}`));
5558
- return {
5559
- exitCode: EXIT.OK,
5560
- json: {
5871
+ return summarized(
5872
+ EXIT.OK,
5873
+ {
5561
5874
  ok: true,
5562
5875
  project: { name: manifest.project.name, framework: manifest.project.framework ?? null },
5563
5876
  environments,
5564
5877
  hints
5565
5878
  },
5879
+ inspectSummary(manifest.project.name, environments),
5566
5880
  human
5567
- };
5881
+ );
5568
5882
  }
5569
5883
 
5570
5884
  // packages/cli/src/commands/logs.ts
@@ -5590,14 +5904,15 @@ async function logsCommand(ctx, environment, limit) {
5590
5904
  const adapter = makeAdapter(ctx, cfg.provider, apiKey);
5591
5905
  try {
5592
5906
  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: [
5907
+ return summarized(
5908
+ EXIT.OK,
5909
+ { ok: true, environment, serviceId: cfg.serviceId, count: lines.length, logs: lines },
5910
+ logsSummary(environment, lines.length),
5911
+ [
5597
5912
  `openpouch logs \u2014 ${environment} (${cfg.serviceId}), ${lines.length} lines:`,
5598
5913
  ...lines.map((l) => ` ${l.timestamp ?? ""} ${l.message}`.trimEnd())
5599
5914
  ]
5600
- };
5915
+ );
5601
5916
  } catch (e) {
5602
5917
  if (isProviderApiError(e)) {
5603
5918
  return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5667,11 +5982,12 @@ async function planCommand(ctx) {
5667
5982
  }
5668
5983
  if (Object.keys(plans).length === 0) human.push(" no environments mapped \u2014 run `openpouch init --force` or edit the manifest");
5669
5984
  human.push(...hints.map((h) => ` hint: ${h}`));
5670
- return {
5671
- exitCode: EXIT.OK,
5672
- json: { ok: true, project: manifest.project.name, environments: plans, hints },
5985
+ return summarized(
5986
+ EXIT.OK,
5987
+ { ok: true, project: manifest.project.name, environments: plans, hints },
5988
+ planSummary(manifest.project.name, plans),
5673
5989
  human
5674
- };
5990
+ );
5675
5991
  }
5676
5992
 
5677
5993
  // packages/cli/src/commands/rollback.ts
@@ -5732,22 +6048,24 @@ async function rollbackCommand(ctx, environment) {
5732
6048
  });
5733
6049
  const exitCode = !result2.deployLive ? EXIT.DEPLOY_FAILED : result2.smokePassed === false ? EXIT.VERIFY_FAILED : EXIT.OK;
5734
6050
  const d = result2.record;
5735
- return {
6051
+ return summarized(
5736
6052
  exitCode,
5737
- json: {
6053
+ {
5738
6054
  ok: exitCode === EXIT.OK,
6055
+ ...d.url !== void 0 && result2.deployLive ? { url: d.url } : {},
5739
6056
  rolledBackTo: { commit: anchorDeploy.commit, anchorDeploy: latest.rollbackAnchor },
5740
6057
  deployment: d,
5741
6058
  smokePassed: result2.smokePassed ?? null
5742
6059
  },
5743
- human: [
6060
+ rollbackSummary({ environment, live: result2.deployLive, url: d.url, smokePassed: result2.smokePassed }),
6061
+ [
5744
6062
  `openpouch rollback ${exitCode === EXIT.OK ? "\u2713" : "\u2717"} ${environment} \u2192 commit ${anchorDeploy.commit.slice(0, 10)}`,
5745
6063
  ` deploy: ${d.id} [${d.status}]`,
5746
6064
  ...result2.smokePassed !== void 0 ? [` smoke: ${result2.smokePassed ? "passed" : "FAILED"}`] : [],
5747
6065
  ...d.approvedBy !== void 0 ? [` approved by: ${d.approvedBy}`] : [],
5748
6066
  ` evidence: deploy.evidence.json + DEPLOYMENT.md updated`
5749
6067
  ]
5750
- };
6068
+ );
5751
6069
  } catch (e) {
5752
6070
  if (isProviderApiError(e)) {
5753
6071
  return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
@@ -5756,6 +6074,57 @@ async function rollbackCommand(ctx, environment) {
5756
6074
  }
5757
6075
  }
5758
6076
 
6077
+ // packages/cli/src/commands/signup.ts
6078
+ async function signupCommand(ctx, opts) {
6079
+ const adapter = makeAdapter(ctx, "openpouch-run", "anonymous");
6080
+ if (opts.github === true) {
6081
+ const url = typeof adapter.githubStartUrl === "function" ? adapter.githubStartUrl() : "";
6082
+ return summarized(
6083
+ EXIT.OK,
6084
+ { ok: true, method: "github", startUrl: url },
6085
+ signupGithubSummary(url),
6086
+ ["openpouch signup (GitHub)", ` Open this URL in your browser and approve: ${url}`, " You'll get your API key on the page that follows."]
6087
+ );
6088
+ }
6089
+ const email = opts.email?.trim();
6090
+ if (!email) {
6091
+ return errorResult(
6092
+ EXIT.USAGE,
6093
+ "usage",
6094
+ "no signup method given",
6095
+ "Use `openpouch signup --email <you@example.com>` or `openpouch signup --github`."
6096
+ );
6097
+ }
6098
+ if (typeof adapter.signupEmail !== "function") {
6099
+ return errorResult(EXIT.UNEXPECTED, "provider", "signup unavailable", "This is an internal wiring error.");
6100
+ }
6101
+ try {
6102
+ const res = await adapter.signupEmail(email);
6103
+ return summarized(
6104
+ EXIT.OK,
6105
+ {
6106
+ ok: true,
6107
+ method: "email",
6108
+ accountId: res.accountId,
6109
+ message: res.message,
6110
+ ...res.devVerifyToken ? { devVerifyToken: res.devVerifyToken } : {}
6111
+ },
6112
+ signupEmailSummary(email),
6113
+ [
6114
+ `openpouch signup \u2713 ${email}`,
6115
+ ` account: ${res.accountId}`,
6116
+ ` ${res.message}`,
6117
+ ...res.devVerifyToken ? [` finish: openpouch activate --account ${res.accountId} --token ${res.devVerifyToken}`] : [` then: openpouch activate --account ${res.accountId} --token <token-from-email>`]
6118
+ ]
6119
+ );
6120
+ } catch (e) {
6121
+ if (isProviderApiError(e)) {
6122
+ return errorResult(EXIT.PROVIDER, e.category, e.message, e.fix);
6123
+ }
6124
+ throw e;
6125
+ }
6126
+ }
6127
+
5759
6128
  // packages/cli/src/commands/verify.ts
5760
6129
  async function verifyCommand(ctx, environment) {
5761
6130
  const loaded = await loadManifest(ctx.cwd);
@@ -5788,15 +6157,62 @@ async function verifyCommand(ctx, environment) {
5788
6157
  evidenceUpdated = true;
5789
6158
  }
5790
6159
  }
5791
- return {
5792
- exitCode: passed ? EXIT.OK : EXIT.VERIFY_FAILED,
5793
- json: { ok: passed, environment, url: cfg.url, checks: smoke, evidenceUpdated },
5794
- human: [
6160
+ return summarized(
6161
+ passed ? EXIT.OK : EXIT.VERIFY_FAILED,
6162
+ { ok: passed, environment, url: cfg.url, checks: smoke, evidenceUpdated },
6163
+ verifySummary({ environment, url: cfg.url, passed, checks: smoke }),
6164
+ [
5795
6165
  `openpouch verify ${passed ? "\u2713" : "\u2717"} ${environment} (${cfg.url})`,
5796
6166
  ...smoke.map((s) => ` ${s.passed ? "\u2713" : "\u2717"} ${s.name} \u2014 ${s.detail ?? ""}`),
5797
6167
  evidenceUpdated ? " evidence: smoke results appended to latest record" : " evidence: no deployment record for this environment yet"
5798
6168
  ]
5799
- };
6169
+ );
6170
+ }
6171
+
6172
+ // packages/cli/src/commands/whoami.ts
6173
+ async function whoamiCommand(ctx) {
6174
+ const key = await resolveProviderApiKey(ctx.env, "openpouch-run") ?? ANONYMOUS_KEY;
6175
+ if (key === ANONYMOUS_KEY) {
6176
+ return summarized(
6177
+ EXIT.OK,
6178
+ { ok: true, authenticated: false },
6179
+ whoamiSummary({ authenticated: false }),
6180
+ [
6181
+ "openpouch whoami \u2014 not signed in (anonymous instant lane)",
6182
+ " Deploys use the free anonymous tier.",
6183
+ " Create an account: openpouch signup --email <you@example.com> (or --github)"
6184
+ ]
6185
+ );
6186
+ }
6187
+ const adapter = makeAdapter(ctx, "openpouch-run", key);
6188
+ if (typeof adapter.whoami !== "function") {
6189
+ return errorResult(EXIT.UNEXPECTED, "provider", "account lookup unavailable", "This is an internal wiring error.");
6190
+ }
6191
+ try {
6192
+ const status = await adapter.whoami();
6193
+ const identities = status.account.identities.map((i) => i.label ?? i.value).join(", ") || "\u2014";
6194
+ return summarized(
6195
+ EXIT.OK,
6196
+ { ok: true, authenticated: true, account: status.account, tier: status.tier, usage: status.usage },
6197
+ whoamiSummary({
6198
+ authenticated: true,
6199
+ tier: status.tier.name,
6200
+ liveDeployments: status.usage.liveDeployments,
6201
+ maxLive: status.tier.maxLiveDeployments
6202
+ }),
6203
+ [
6204
+ `openpouch whoami \u2713 ${status.account.id} [${status.tier.name}]`,
6205
+ ` identities: ${identities}`,
6206
+ ` live apps: ${status.usage.liveDeployments}/${status.tier.maxLiveDeployments} (${status.usage.dynamicDeployments} running)`,
6207
+ ` deploys: ${status.usage.deploysLastHour}/hour, ${status.usage.deploysLastDay}/day`
6208
+ ]
6209
+ );
6210
+ } catch (e) {
6211
+ if (isProviderApiError(e)) {
6212
+ return errorResult(e.category === "auth" ? EXIT.AUTH : EXIT.PROVIDER, e.category, e.message, e.fix);
6213
+ }
6214
+ throw e;
6215
+ }
5800
6216
  }
5801
6217
 
5802
6218
  // packages/cli/src/main.ts
@@ -5806,7 +6222,10 @@ var USAGE = [
5806
6222
  "usage: openpouch <command> [--json]",
5807
6223
  "",
5808
6224
  "commands:",
5809
- " deploy zero-config instant preview on openpouch's own infra (no account/key; deploy-then-claim)",
6225
+ " deploy zero-config instant preview on openpouch's own infra (anonymous, or under your account if a key is set)",
6226
+ " signup create an openpouch account: --email <addr> or --github (gives you an API key)",
6227
+ " activate finish an email signup: --account <id> --token <token> (saves your API key)",
6228
+ " whoami show the account behind your API key: tier + current usage",
5810
6229
  " init detect the project, write deploy.manifest.json + deploy.policy.json (idempotent; --force regenerates)",
5811
6230
  " inspect answer: what is deployed, where, on which commit, which env vars are missing",
5812
6231
  " plan per environment: policy decision, blockers, readiness, next steps",
@@ -5822,7 +6241,13 @@ var USAGE = [
5822
6241
  " --force (init) overwrite existing manifest/policy",
5823
6242
  " --env <name> (verify/logs/rollback) target environment, default: production",
5824
6243
  " --limit <n> (logs) number of log lines, default: 50",
6244
+ " --email <addr> (signup) create an account anchored to this email",
6245
+ " --github (signup) create an account via GitHub (prints the authorize URL)",
6246
+ " --account <id> (activate) the account id from signup",
6247
+ " --token <tok> (activate) the verification token from the signup email",
6248
+ " --key-file <p> (activate) where to save the issued key (default ~/.openpouch/openpouch-run.key; never overwrites a non-openpouch file)",
5825
6249
  "",
6250
+ "account key: deploys send Authorization: Bearer from OPENPOUCH_API_KEY or ~/.openpouch/openpouch-run.key (override the path with OPENPOUCH_KEY_FILE; optional \u2014 no key = anonymous)",
5826
6251
  "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
6252
  ].join("\n");
5828
6253
  function render(result2, json) {
@@ -5840,8 +6265,13 @@ async function run(argv, ctx) {
5840
6265
  json: { type: "boolean", default: false },
5841
6266
  force: { type: "boolean", default: false },
5842
6267
  help: { type: "boolean", default: false },
6268
+ github: { type: "boolean", default: false },
5843
6269
  env: { type: "string" },
5844
- limit: { type: "string" }
6270
+ limit: { type: "string" },
6271
+ email: { type: "string" },
6272
+ account: { type: "string" },
6273
+ token: { type: "string" },
6274
+ "key-file": { type: "string" }
5845
6275
  },
5846
6276
  allowPositionals: true
5847
6277
  });
@@ -5867,6 +6297,19 @@ async function run(argv, ctx) {
5867
6297
  switch (command) {
5868
6298
  case "deploy":
5869
6299
  return render(await instantCommand(ctx), json);
6300
+ case "signup":
6301
+ return render(await signupCommand(ctx, { email: parsed.values.email, github: parsed.values.github === true }), json);
6302
+ case "activate":
6303
+ return render(
6304
+ await activateCommand(ctx, {
6305
+ account: parsed.values.account,
6306
+ token: parsed.values.token,
6307
+ keyFile: parsed.values["key-file"]
6308
+ }),
6309
+ json
6310
+ );
6311
+ case "whoami":
6312
+ return render(await whoamiCommand(ctx), json);
5870
6313
  case "init":
5871
6314
  return render(await initCommand(ctx, { force: parsed.values.force === true }), json);
5872
6315
  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.1",
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"