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.
- package/README.md +18 -4
- package/openpouch.js +616 -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,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/
|
|
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
|
-
}
|
|
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
|
|
4346
|
-
import { homedir
|
|
4347
|
-
import { join as
|
|
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 =
|
|
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}`,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"] ??
|
|
4745
|
-
const fromFile = (await
|
|
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({
|
|
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
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
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
|
|
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
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"- **
|
|
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
|
|
5381
|
+
existing = await readFile5(path, "utf8");
|
|
5087
5382
|
} catch {
|
|
5088
5383
|
existing = void 0;
|
|
5089
5384
|
}
|
|
5090
5385
|
if (existing === void 0) {
|
|
5091
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
5229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5298
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
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
|
|
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
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
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
|
-
|
|
5423
|
-
|
|
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
|
-
|
|
5560
|
-
|
|
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
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
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
|
-
|
|
5672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
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 (
|
|
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
|
|
4
|
-
"description": "openpouch 🦘 —
|
|
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.
|
|
31
|
+
"homepage": "https://openpouch.dev",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "git+https://github.com/openpouch/openpouch.git"
|