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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/openpouch.js +92 -53
  3. 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 { homedir as homedir2 } from "node:os";
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 home = env["OPENPOUCH_HOME"] ?? homedir();
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 keyPath = "";
4927
+ let reason;
4928
+ const keyPath = opts.keyFile?.trim() || runKeyPath(ctx.env);
4915
4929
  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}
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
- saved = true;
4923
- } catch {
4924
- saved = false;
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 could not save the key file \u2014 set it yourself: export OPENPOUCH_API_KEY=${res.key}`,
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 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";
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 readFile3(join4(cwd, APPROVALS_FILENAME), "utf8");
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(join4(cwd, APPROVALS_DIR), { recursive: true });
4993
+ await mkdir2(join3(cwd, APPROVALS_DIR), { recursive: true });
4969
4994
  await ensureGitignored(cwd);
4970
4995
  await writeFile4(
4971
- join4(cwd, APPROVALS_FILENAME),
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 = join4(cwd, ".gitignore");
5003
+ const path = join3(cwd, ".gitignore");
4979
5004
  let content = "";
4980
5005
  try {
4981
- content = await readFile3(path, "utf8");
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 = join4(home ?? homedir3(), ".openpouch");
5003
- const path = join4(dir, "approver.secret");
5027
+ const dir = join3(home ?? homedir2(), ".openpouch");
5028
+ const path = join3(dir, "approver.secret");
5004
5029
  try {
5005
- const existing = (await readFile3(path, "utf8")).trim();
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 join7 } from "node:path";
5350
+ import { join as join6 } from "node:path";
5326
5351
 
5327
5352
  // packages/cli/src/agentsmd.ts
5328
- import { readFile as readFile4, writeFile as writeFile5 } from "node:fs/promises";
5329
- import { join as join5 } from "node:path";
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 = join5(cwd, AGENTS_MD_FILENAME);
5378
+ const path = join4(cwd, AGENTS_MD_FILENAME);
5353
5379
  const section = renderAgentsSection(projectName);
5354
5380
  let existing;
5355
5381
  try {
5356
- existing = await readFile4(path, "utf8");
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 readFile5, readdir } from "node:fs/promises";
5384
- import { basename, extname, join as join6 } from "node:path";
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 = join6(current.dir, entry.name);
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 readFile5(file);
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 readFile5(join6(cwd, "package.json"), "utf8"));
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(join7(ctx.cwd, MANIFEST_FILENAME), validated.value);
5565
- await writeJsonFile(join7(ctx.cwd, POLICY_FILENAME), defaultPolicy());
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 readFile6 } from "node:fs/promises";
5606
- import { basename as basename2, join as join8 } from "node:path";
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 readFile6(join8(cwd, "package.json"), "utf8"));
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
- return errorResult(
5645
- EXIT.USAGE,
5646
- "usage",
5647
- "this project already has deploy.manifest.json",
5648
- "Use `openpouch preview` / `openpouch prod` for a configured project. `openpouch deploy` is the zero-config instant lane."
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(join8(ctx.cwd, MANIFEST_FILENAME), manifest);
5677
- await writeJsonFile(join8(ctx.cwd, POLICY_FILENAME), defaultPolicy());
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(await activateCommand(ctx, { account: parsed.values.account, token: parsed.values.token }), json);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpouch",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
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",