run402 4.70.2 → 4.70.3

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "surface_version": "4.70.2",
2
+ "surface_version": "4.70.3",
3
3
  "verbs": [
4
4
  "repos create",
5
5
  "repos list",
@@ -6,7 +6,7 @@
6
6
  * subprocess — a plain directory scan mirrors what the OS loader itself
7
7
  * does to resolve an unqualified command name.
8
8
  */
9
- import { accessSync, constants } from "node:fs";
9
+ import {accessSync, constants, readFileSync } from "node:fs";
10
10
  import { delimiter, join } from "node:path";
11
11
 
12
12
  /** True iff `name` resolves to an executable file somewhere on `PATH`. Windows `PATHEXT` is out of scope — this CLI targets POSIX (`engines.node` + the hardened-git doc comments assume it) — so no `.exe`/`.cmd` suffix search. */
@@ -22,3 +22,31 @@ export function isExecutableOnPath(name, env = process.env) {
22
22
  }
23
23
  return false;
24
24
  }
25
+
26
+ /**
27
+ * After a restore into `dir`, the ONE thing that still stands between the
28
+ * caller and its first `git push` is git finding the remote helper the
29
+ * checkout's origin scheme names (`git-remote-kygit` for `kygit::`,
30
+ * `git-remote-run402` for `run402::`). A resume or join run through
31
+ * `npx` has the helper only inside the npx cache, so git cannot see it.
32
+ * Returns a `next_actions` entry naming the fix, or null when the helper
33
+ * is on PATH or the checkout's remote cannot be read (never throws).
34
+ */
35
+ export function remoteHelperNextAction(dir, env = process.env) {
36
+ let url = null;
37
+ try {
38
+ const cfg = readFileSync(join(dir, ".git", "config"), "utf8");
39
+ const m = cfg.match(/\[remote "origin"\][^[]*?\n\s*url\s*=\s*(\S+)/);
40
+ url = m ? m[1] : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ if (!url) return null;
45
+ const helper = url.startsWith("kygit::") ? "git-remote-kygit" : url.startsWith("run402::") ? "git-remote-run402" : null;
46
+ if (!helper || isExecutableOnPath(helper, env)) return null;
47
+ return {
48
+ type: "install_remote_helper",
49
+ command: "npm i -g @kychee/kygit run402",
50
+ why: `git push needs ${helper} on PATH and this session does not have it there (an npx run keeps it inside the npx cache); install both packages once and every git command in this checkout works`,
51
+ };
52
+ }
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import { describe, it } from "node:test";
8
8
  import assert from "node:assert/strict";
9
- import { mkdtempSync, chmodSync, writeFileSync, rmSync } from "node:fs";
9
+ import { mkdtempSync, mkdirSync, chmodSync, writeFileSync, rmSync } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
- import { isExecutableOnPath } from "./path-lookup.mjs";
12
+ import { isExecutableOnPath, remoteHelperNextAction } from "./path-lookup.mjs";
13
13
 
14
14
  describe("isExecutableOnPath", () => {
15
15
  it("finds an executable file on PATH", () => {
@@ -64,3 +64,29 @@ describe("isExecutableOnPath", () => {
64
64
  assert.equal(isExecutableOnPath("git-remote-kygit", { PATH: "" }), false);
65
65
  });
66
66
  });
67
+
68
+ describe("remoteHelperNextAction", () => {
69
+ it("names the missing helper for the checkout's remote scheme, and nothing when it is on PATH or the checkout is unreadable", () => {
70
+ const dir = mkdtempSync(join(tmpdir(), "run402-remote-helper-"));
71
+ try {
72
+ mkdirSync(join(dir, ".git"));
73
+ writeFileSync(join(dir, ".git", "config"), `[core]\n\trepositoryformatversion = 0\n[remote "origin"]\n\turl = kygit::org/name\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n`);
74
+ const emptyPath = { PATH: dir };
75
+ const missing = remoteHelperNextAction(dir, emptyPath);
76
+ assert.equal(missing?.type, "install_remote_helper");
77
+ assert.match(missing.why, /git-remote-kygit/);
78
+ assert.equal(missing.command, "npm i -g @kychee/kygit run402");
79
+ // present on PATH → nothing to say
80
+ const bin = join(dir, "bin"); mkdirSync(bin);
81
+ writeFileSync(join(bin, "git-remote-kygit"), "#!/bin/sh\n"); chmodSync(join(bin, "git-remote-kygit"), 0o755);
82
+ assert.equal(remoteHelperNextAction(dir, { PATH: bin }), null);
83
+ // run402:: scheme names the other helper
84
+ writeFileSync(join(dir, ".git", "config"), `[remote "origin"]\n\turl = run402::org/name\n`);
85
+ assert.match(remoteHelperNextAction(dir, emptyPath).why, /git-remote-run402/);
86
+ // no checkout → null, never a throw
87
+ assert.equal(remoteHelperNextAction(join(dir, "nope"), emptyPath), null);
88
+ } finally {
89
+ rmSync(dir, { recursive: true, force: true });
90
+ }
91
+ });
92
+ });
package/lib/repos.mjs CHANGED
@@ -1918,6 +1918,13 @@ async function resume(args) {
1918
1918
  try {
1919
1919
  const result = await sdk.gitvault.resume({ key, ...(to != null ? { to } : {}), onLine: (line) => console.error(line) });
1920
1920
  if (coldStart.next_action) result.next_actions = [...(result.next_actions ?? []), coldStart.next_action];
1921
+ // The restored checkout's first git command needs the remote helper on
1922
+ // PATH; an npx-run resume/join has it only in the npx cache. Name it.
1923
+ try {
1924
+ const { remoteHelperNextAction } = await import("./path-lookup.mjs");
1925
+ const helperAction = result.restored?.dir ? remoteHelperNextAction(result.restored.dir) : null;
1926
+ if (helperAction) result.next_actions = [...(result.next_actions ?? []), helperAction];
1927
+ } catch { /* never fails a completed claim */ }
1921
1928
  if (a.includes("--json")) {
1922
1929
  printJson(sdk, { ...result, cold_start: coldStart });
1923
1930
  } else {
@@ -2111,6 +2118,13 @@ async function joinInvite(args) {
2111
2118
  } catch {
2112
2119
  // never fails a completed join
2113
2120
  }
2121
+ // The restored checkout's first git command needs the remote helper on
2122
+ // PATH; an npx-run resume/join has it only in the npx cache. Name it.
2123
+ try {
2124
+ const { remoteHelperNextAction } = await import("./path-lookup.mjs");
2125
+ const helperAction = result.restored?.dir ? remoteHelperNextAction(result.restored.dir) : null;
2126
+ if (helperAction) result.next_actions = [...(result.next_actions ?? []), helperAction];
2127
+ } catch { /* never fails a completed claim */ }
2114
2128
  if (a.includes("--json")) {
2115
2129
  printJson(sdk, { ...result, cold_start: coldStart });
2116
2130
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.70.2",
3
+ "version": "4.70.3",
4
4
  "description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
5
5
  "type": "module",
6
6
  "bin": {