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.
- package/README.md +18 -4
- package/openpouch.js +578 -173
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
# openpouch 🦘
|
|
2
2
|
|
|
3
|
-
**The agent-native
|
|
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.**
|
|
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`
|
|
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/
|
|
4262
|
-
import {
|
|
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 =
|
|
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}`,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"] ??
|
|
4745
|
-
const fromFile = (await
|
|
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({
|
|
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
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
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
|
|
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
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5325
|
+
import { join as join7 } from "node:path";
|
|
5057
5326
|
|
|
5058
5327
|
// packages/cli/src/agentsmd.ts
|
|
5059
|
-
import { readFile as readFile4, writeFile as
|
|
5060
|
-
import { join as
|
|
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
|
-
"- **
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
5229
|
-
|
|
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
|
-
|
|
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(
|
|
5293
|
-
await writeJsonFile(
|
|
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
|
-
|
|
5298
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
5399
|
-
await writeJsonFile(
|
|
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
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
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
|
-
|
|
5423
|
-
|
|
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
|
-
|
|
5560
|
-
|
|
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
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
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
|
-
|
|
5672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
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 (
|
|
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.
|
|
4
|
-
"description": "openpouch 🦘 —
|
|
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.
|
|
31
|
+
"homepage": "https://openpouch.dev",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "git+https://github.com/openpouch/openpouch.git"
|