openpouch 0.2.0 → 0.2.2
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 +7 -0
- package/openpouch.js +92 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,13 @@ npx openpouch deploy
|
|
|
10
10
|
|
|
11
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
12
|
|
|
13
|
+
> **Framework frontends (React/Vite/Next/Svelte…): deploy the _built output_, not the source folder.** `openpouch deploy` ships whatever folder you run it in, as-is. So build first, then deploy the build directory:
|
|
14
|
+
> ```bash
|
|
15
|
+
> npm run build
|
|
16
|
+
> cd dist && npx openpouch deploy # Vite → dist/ · CRA → build/ · Next static export → out/
|
|
17
|
+
> ```
|
|
18
|
+
> A raw, unbuilt source folder is served as static files (the app won't run). Server-side build-on-deploy is on the roadmap.
|
|
19
|
+
|
|
13
20
|
## Accounts (optional)
|
|
14
21
|
|
|
15
22
|
Start anonymous, or create a free account for higher limits — entirely from the agent, no dashboard:
|
package/openpouch.js
CHANGED
|
@@ -4259,9 +4259,8 @@ function findUsableApproval(file, match, now) {
|
|
|
4259
4259
|
}
|
|
4260
4260
|
|
|
4261
4261
|
// packages/cli/src/commands/activate.ts
|
|
4262
|
-
import { mkdir, writeFile as writeFile3 } from "node:fs/promises";
|
|
4263
|
-
import {
|
|
4264
|
-
import { join as join3 } from "node:path";
|
|
4262
|
+
import { mkdir, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
4263
|
+
import { dirname } from "node:path";
|
|
4265
4264
|
|
|
4266
4265
|
// packages/cli/src/shared.ts
|
|
4267
4266
|
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -4832,12 +4831,17 @@ var PROVIDER_CREDENTIALS = {
|
|
|
4832
4831
|
};
|
|
4833
4832
|
var ANONYMOUS_KEY = "anonymous";
|
|
4834
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
|
+
}
|
|
4835
4840
|
async function resolveRunKey(env) {
|
|
4836
4841
|
const fromEnv = env[RUN_KEY.envVar]?.trim();
|
|
4837
4842
|
if (fromEnv) return fromEnv;
|
|
4838
4843
|
try {
|
|
4839
|
-
const
|
|
4840
|
-
const fromFile = (await readFile2(join2(home, ".openpouch", RUN_KEY.file), "utf8")).trim();
|
|
4844
|
+
const fromFile = (await readFile2(runKeyPath(env), "utf8")).trim();
|
|
4841
4845
|
if (fromFile.length > 0) return fromFile;
|
|
4842
4846
|
} catch {
|
|
4843
4847
|
}
|
|
@@ -4893,6 +4897,15 @@ function keyPublicPrefix(key) {
|
|
|
4893
4897
|
const m = /^(op_live_[0-9a-f]+)_/.exec(key);
|
|
4894
4898
|
return m ? m[1] : "op_live_\u2026";
|
|
4895
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
|
+
}
|
|
4896
4909
|
async function activateCommand(ctx, opts) {
|
|
4897
4910
|
const account = opts.account?.trim();
|
|
4898
4911
|
const token = opts.token?.trim();
|
|
@@ -4911,17 +4924,29 @@ async function activateCommand(ctx, opts) {
|
|
|
4911
4924
|
try {
|
|
4912
4925
|
const res = await adapter.verifyEmail(account, token);
|
|
4913
4926
|
let saved = false;
|
|
4914
|
-
let
|
|
4927
|
+
let reason;
|
|
4928
|
+
const keyPath = opts.keyFile?.trim() || runKeyPath(ctx.env);
|
|
4915
4929
|
try {
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
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}
|
|
4921
4945
|
`, { mode: 384 });
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4946
|
+
saved = true;
|
|
4947
|
+
}
|
|
4948
|
+
} catch (e) {
|
|
4949
|
+
reason = `could not save the key to ${keyPath}: ${e.message}`;
|
|
4925
4950
|
}
|
|
4926
4951
|
return summarized(
|
|
4927
4952
|
EXIT.OK,
|
|
@@ -4930,12 +4955,12 @@ async function activateCommand(ctx, opts) {
|
|
|
4930
4955
|
account: res.account,
|
|
4931
4956
|
keyPrefix: keyPublicPrefix(res.key),
|
|
4932
4957
|
saved,
|
|
4933
|
-
...saved ? { keyPath } : { key: res.key }
|
|
4958
|
+
...saved ? { keyPath } : { reason, key: res.key }
|
|
4934
4959
|
},
|
|
4935
4960
|
activateSummary(saved),
|
|
4936
4961
|
[
|
|
4937
4962
|
`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
|
|
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}`],
|
|
4939
4964
|
` key: ${keyPublicPrefix(res.key)}\u2026`
|
|
4940
4965
|
]
|
|
4941
4966
|
);
|
|
@@ -4952,33 +4977,33 @@ import { createInterface } from "node:readline/promises";
|
|
|
4952
4977
|
|
|
4953
4978
|
// packages/cli/src/approvals.ts
|
|
4954
4979
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4955
|
-
import { mkdir as mkdir2, readFile as
|
|
4956
|
-
import { homedir as
|
|
4957
|
-
import { join as
|
|
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";
|
|
4958
4983
|
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4959
4984
|
async function loadApprovals(cwd) {
|
|
4960
4985
|
try {
|
|
4961
|
-
const raw = await
|
|
4986
|
+
const raw = await readFile4(join3(cwd, APPROVALS_FILENAME), "utf8");
|
|
4962
4987
|
return approvalsFileSchema.parse(JSON.parse(raw));
|
|
4963
4988
|
} catch {
|
|
4964
4989
|
return { version: 0, requests: [] };
|
|
4965
4990
|
}
|
|
4966
4991
|
}
|
|
4967
4992
|
async function saveApprovals(cwd, file) {
|
|
4968
|
-
await mkdir2(
|
|
4993
|
+
await mkdir2(join3(cwd, APPROVALS_DIR), { recursive: true });
|
|
4969
4994
|
await ensureGitignored(cwd);
|
|
4970
4995
|
await writeFile4(
|
|
4971
|
-
|
|
4996
|
+
join3(cwd, APPROVALS_FILENAME),
|
|
4972
4997
|
`${JSON.stringify(approvalsFileSchema.parse(file), null, 2)}
|
|
4973
4998
|
`,
|
|
4974
4999
|
"utf8"
|
|
4975
5000
|
);
|
|
4976
5001
|
}
|
|
4977
5002
|
async function ensureGitignored(cwd) {
|
|
4978
|
-
const path =
|
|
5003
|
+
const path = join3(cwd, ".gitignore");
|
|
4979
5004
|
let content = "";
|
|
4980
5005
|
try {
|
|
4981
|
-
content = await
|
|
5006
|
+
content = await readFile4(path, "utf8");
|
|
4982
5007
|
} catch {
|
|
4983
5008
|
}
|
|
4984
5009
|
if (!content.split("\n").some((line) => line.trim() === ".openpouch/")) {
|
|
@@ -4999,10 +5024,10 @@ function newApprovalRequest(match, now) {
|
|
|
4999
5024
|
};
|
|
5000
5025
|
}
|
|
5001
5026
|
async function getApproverSecret(createIfMissing, home) {
|
|
5002
|
-
const dir =
|
|
5003
|
-
const path =
|
|
5027
|
+
const dir = join3(home ?? homedir2(), ".openpouch");
|
|
5028
|
+
const path = join3(dir, "approver.secret");
|
|
5004
5029
|
try {
|
|
5005
|
-
const existing = (await
|
|
5030
|
+
const existing = (await readFile4(path, "utf8")).trim();
|
|
5006
5031
|
if (existing.length > 0) return existing;
|
|
5007
5032
|
} catch {
|
|
5008
5033
|
}
|
|
@@ -5322,11 +5347,11 @@ async function deployCommand(ctx, environment) {
|
|
|
5322
5347
|
}
|
|
5323
5348
|
|
|
5324
5349
|
// packages/cli/src/commands/init.ts
|
|
5325
|
-
import { join as
|
|
5350
|
+
import { join as join6 } from "node:path";
|
|
5326
5351
|
|
|
5327
5352
|
// packages/cli/src/agentsmd.ts
|
|
5328
|
-
import { readFile as
|
|
5329
|
-
import { join as
|
|
5353
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
|
|
5354
|
+
import { join as join4 } from "node:path";
|
|
5330
5355
|
var AGENTS_MD_FILENAME = "AGENTS.md";
|
|
5331
5356
|
var START = "<!-- openpouch:start -->";
|
|
5332
5357
|
var END = "<!-- openpouch:end -->";
|
|
@@ -5338,6 +5363,7 @@ function renderAgentsSection(projectName) {
|
|
|
5338
5363
|
"",
|
|
5339
5364
|
`${projectName} deploys via openpouch \u2014 an agent-native deployment control plane. This works the same from any agent harness.`,
|
|
5340
5365
|
"",
|
|
5366
|
+
"- **Framework frontends (React/Vite/Next/Svelte\u2026): deploy the _built output_, not the source folder.** `openpouch deploy` ships whatever directory you run it in, as-is \u2014 so build first, then deploy the build directory: `npm run build`, then `cd dist && openpouch deploy` (Vite\u2192`dist/`, CRA\u2192`build/`, Next static export\u2192`out/`). A raw, unbuilt source folder is served as static files (the app won't run); server-side build-on-deploy is on the roadmap.",
|
|
5341
5367
|
"- **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`.",
|
|
5342
5368
|
"- **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.",
|
|
5343
5369
|
"- **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.",
|
|
@@ -5349,11 +5375,11 @@ function renderAgentsSection(projectName) {
|
|
|
5349
5375
|
].join("\n");
|
|
5350
5376
|
}
|
|
5351
5377
|
async function upsertAgentsSection(cwd, projectName) {
|
|
5352
|
-
const path =
|
|
5378
|
+
const path = join4(cwd, AGENTS_MD_FILENAME);
|
|
5353
5379
|
const section = renderAgentsSection(projectName);
|
|
5354
5380
|
let existing;
|
|
5355
5381
|
try {
|
|
5356
|
-
existing = await
|
|
5382
|
+
existing = await readFile5(path, "utf8");
|
|
5357
5383
|
} catch {
|
|
5358
5384
|
existing = void 0;
|
|
5359
5385
|
}
|
|
@@ -5380,8 +5406,8 @@ ${section}
|
|
|
5380
5406
|
}
|
|
5381
5407
|
|
|
5382
5408
|
// packages/cli/src/detect.ts
|
|
5383
|
-
import { readFile as
|
|
5384
|
-
import { basename, extname, join as
|
|
5409
|
+
import { readFile as readFile6, readdir } from "node:fs/promises";
|
|
5410
|
+
import { basename, extname, join as join5 } from "node:path";
|
|
5385
5411
|
var FRAMEWORK_BY_DEP = [
|
|
5386
5412
|
["next", "nextjs"],
|
|
5387
5413
|
["astro", "astro"],
|
|
@@ -5416,7 +5442,7 @@ async function collectSourceFiles(root) {
|
|
|
5416
5442
|
}
|
|
5417
5443
|
for (const entry of entries) {
|
|
5418
5444
|
if (files.length >= MAX_FILES) break;
|
|
5419
|
-
const full =
|
|
5445
|
+
const full = join5(current.dir, entry.name);
|
|
5420
5446
|
if (entry.isDirectory()) {
|
|
5421
5447
|
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".") && current.depth < MAX_DEPTH) {
|
|
5422
5448
|
stack.push({ dir: full, depth: current.depth + 1 });
|
|
@@ -5433,7 +5459,7 @@ async function scanEnvVarNames(root) {
|
|
|
5433
5459
|
for (const file of await collectSourceFiles(root)) {
|
|
5434
5460
|
let content;
|
|
5435
5461
|
try {
|
|
5436
|
-
const buf = await
|
|
5462
|
+
const buf = await readFile6(file);
|
|
5437
5463
|
if (buf.byteLength > MAX_FILE_BYTES) continue;
|
|
5438
5464
|
content = buf.toString("utf8");
|
|
5439
5465
|
} catch {
|
|
@@ -5451,7 +5477,7 @@ async function scanEnvVarNames(root) {
|
|
|
5451
5477
|
async function detectProject(cwd) {
|
|
5452
5478
|
let pkg = {};
|
|
5453
5479
|
try {
|
|
5454
|
-
pkg = JSON.parse(await
|
|
5480
|
+
pkg = JSON.parse(await readFile6(join5(cwd, "package.json"), "utf8"));
|
|
5455
5481
|
} catch {
|
|
5456
5482
|
}
|
|
5457
5483
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
@@ -5561,8 +5587,8 @@ async function initCommand(ctx, flags) {
|
|
|
5561
5587
|
"This is an openpouch bug \u2014 please report it."
|
|
5562
5588
|
);
|
|
5563
5589
|
}
|
|
5564
|
-
await writeJsonFile(
|
|
5565
|
-
await writeJsonFile(
|
|
5590
|
+
await writeJsonFile(join6(ctx.cwd, MANIFEST_FILENAME), validated.value);
|
|
5591
|
+
await writeJsonFile(join6(ctx.cwd, POLICY_FILENAME), defaultPolicy());
|
|
5566
5592
|
const agentsMd = await upsertAgentsSection(ctx.cwd, detected.name);
|
|
5567
5593
|
hints.push("policy default: previews autonomous, production requires approval \u2014 edit deploy.policy.json to change");
|
|
5568
5594
|
return summarized(
|
|
@@ -5602,8 +5628,8 @@ async function initCommand(ctx, flags) {
|
|
|
5602
5628
|
|
|
5603
5629
|
// packages/cli/src/commands/instant.ts
|
|
5604
5630
|
import { spawn } from "node:child_process";
|
|
5605
|
-
import { readFile as
|
|
5606
|
-
import { basename as basename2, join as
|
|
5631
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
5632
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
5607
5633
|
var TAR_EXCLUDES = [
|
|
5608
5634
|
"./node_modules",
|
|
5609
5635
|
"./.git",
|
|
@@ -5632,7 +5658,7 @@ function buildTarball(cwd) {
|
|
|
5632
5658
|
}
|
|
5633
5659
|
async function projectHint(cwd) {
|
|
5634
5660
|
try {
|
|
5635
|
-
const pkg = JSON.parse(await
|
|
5661
|
+
const pkg = JSON.parse(await readFile7(join7(cwd, "package.json"), "utf8"));
|
|
5636
5662
|
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
5637
5663
|
} catch {
|
|
5638
5664
|
}
|
|
@@ -5641,12 +5667,16 @@ async function projectHint(cwd) {
|
|
|
5641
5667
|
async function instantCommand(ctx) {
|
|
5642
5668
|
const loaded = await loadManifest(ctx.cwd);
|
|
5643
5669
|
if (loaded.status === "ok") {
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5670
|
+
const envs = Object.values(loaded.manifest.environments ?? {});
|
|
5671
|
+
const hasGovernedEnv = envs.some((e) => e.provider !== "openpouch-run");
|
|
5672
|
+
if (hasGovernedEnv) {
|
|
5673
|
+
return errorResult(
|
|
5674
|
+
EXIT.USAGE,
|
|
5675
|
+
"usage",
|
|
5676
|
+
"this project already has deploy.manifest.json",
|
|
5677
|
+
"Use `openpouch preview` / `openpouch prod` for a configured project. `openpouch deploy` is the zero-config instant lane."
|
|
5678
|
+
);
|
|
5679
|
+
}
|
|
5650
5680
|
}
|
|
5651
5681
|
let tarball;
|
|
5652
5682
|
try {
|
|
@@ -5673,8 +5703,8 @@ async function instantCommand(ctx) {
|
|
|
5673
5703
|
preview: { provider: "openpouch-run", serviceId: result2.slug, url: result2.url }
|
|
5674
5704
|
}
|
|
5675
5705
|
};
|
|
5676
|
-
await writeJsonFile(
|
|
5677
|
-
await writeJsonFile(
|
|
5706
|
+
await writeJsonFile(join7(ctx.cwd, MANIFEST_FILENAME), manifest);
|
|
5707
|
+
await writeJsonFile(join7(ctx.cwd, POLICY_FILENAME), defaultPolicy());
|
|
5678
5708
|
await appendDeployment(ctx.cwd, {
|
|
5679
5709
|
id: result2.slug,
|
|
5680
5710
|
environment: "preview",
|
|
@@ -6216,8 +6246,9 @@ var USAGE = [
|
|
|
6216
6246
|
" --github (signup) create an account via GitHub (prints the authorize URL)",
|
|
6217
6247
|
" --account <id> (activate) the account id from signup",
|
|
6218
6248
|
" --token <tok> (activate) the verification token from the signup email",
|
|
6249
|
+
" --key-file <p> (activate) where to save the issued key (default ~/.openpouch/openpouch-run.key; never overwrites a non-openpouch file)",
|
|
6219
6250
|
"",
|
|
6220
|
-
"account key: deploys send Authorization: Bearer from OPENPOUCH_API_KEY or ~/.openpouch/openpouch-run.key (optional \u2014 no key = anonymous)",
|
|
6251
|
+
"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)",
|
|
6221
6252
|
"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"
|
|
6222
6253
|
].join("\n");
|
|
6223
6254
|
function render(result2, json) {
|
|
@@ -6240,7 +6271,8 @@ async function run(argv, ctx) {
|
|
|
6240
6271
|
limit: { type: "string" },
|
|
6241
6272
|
email: { type: "string" },
|
|
6242
6273
|
account: { type: "string" },
|
|
6243
|
-
token: { type: "string" }
|
|
6274
|
+
token: { type: "string" },
|
|
6275
|
+
"key-file": { type: "string" }
|
|
6244
6276
|
},
|
|
6245
6277
|
allowPositionals: true
|
|
6246
6278
|
});
|
|
@@ -6269,7 +6301,14 @@ async function run(argv, ctx) {
|
|
|
6269
6301
|
case "signup":
|
|
6270
6302
|
return render(await signupCommand(ctx, { email: parsed.values.email, github: parsed.values.github === true }), json);
|
|
6271
6303
|
case "activate":
|
|
6272
|
-
return render(
|
|
6304
|
+
return render(
|
|
6305
|
+
await activateCommand(ctx, {
|
|
6306
|
+
account: parsed.values.account,
|
|
6307
|
+
token: parsed.values.token,
|
|
6308
|
+
keyFile: parsed.values["key-file"]
|
|
6309
|
+
}),
|
|
6310
|
+
json
|
|
6311
|
+
);
|
|
6273
6312
|
case "whoami":
|
|
6274
6313
|
return render(await whoamiCommand(ctx), json);
|
|
6275
6314
|
case "init":
|
package/package.json
CHANGED