run402 4.70.2 → 4.70.4
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/gitvault-surface.json +1 -1
- package/lib/path-lookup.mjs +29 -1
- package/lib/path-lookup.test.mjs +28 -2
- package/lib/repos.mjs +14 -0
- package/lib/wallet-context.mjs +21 -4
- package/lib/wallet-context.test.mjs +12 -0
- package/package.json +1 -1
package/gitvault-surface.json
CHANGED
package/lib/path-lookup.mjs
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|
package/lib/path-lookup.test.mjs
CHANGED
|
@@ -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/lib/wallet-context.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* into the env var here, at the edge.
|
|
10
10
|
*
|
|
11
11
|
* Precedence (highest first):
|
|
12
|
-
* 1. --wallet <name> / --profile <name> (flag
|
|
12
|
+
* 1. --wallet <name> / --profile <name> (flag; under `repos`, only --wallet —
|
|
13
|
+
* `repos create|mirror|recover --profile <name>` is the AWS credential profile)
|
|
13
14
|
* 2. RUN402_WALLET / RUN402_PROFILE (env)
|
|
14
15
|
* 3. nearest .run402.local.json/.run402.json (directory binding, walk up)
|
|
15
16
|
* 4. config.json active_wallet (global `wallets use`)
|
|
@@ -41,6 +42,11 @@ export { findBindingKey, bindingFilePath, readBindingFile };
|
|
|
41
42
|
|
|
42
43
|
const DEFAULT = "default";
|
|
43
44
|
const GLOBAL_FLAGS = new Set(["--wallet", "--profile"]);
|
|
45
|
+
// `repos` owns `--profile` itself (the AWS credential profile for a BYO
|
|
46
|
+
// destination, a mirror, or a recovery source), so under that command only
|
|
47
|
+
// `--wallet` selects the wallet; RUN402_WALLET and the directory binding still
|
|
48
|
+
// apply. Stripping `--profile` there made the documented s3 flags unreachable.
|
|
49
|
+
const OWNS_PROFILE_FLAG = new Set(["repos"]);
|
|
44
50
|
// The `wallets` group is the management + escape surface — it must work even
|
|
45
51
|
// when selection is ambiguous (so you can `wallets unbind`), and it validates
|
|
46
52
|
// its own positional targets. `init` creates wallets, so it must not fail
|
|
@@ -50,7 +56,8 @@ const EXISTENCE_EXEMPT = new Set(["wallets", "init", "doctor"]);
|
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
58
|
* Split the global --wallet/--profile flag (and its value) out of argv so the
|
|
53
|
-
* subcommand never sees it
|
|
59
|
+
* subcommand never sees it — except `--profile` under a command that owns the
|
|
60
|
+
* flag (OWNS_PROFILE_FLAG), which is passed through untouched. Pure: no core imports, no side effects. Returns
|
|
54
61
|
* the cleaned argv and the selected flag (`{ flag, value }` or null). Last
|
|
55
62
|
* occurrence wins. A missing value is left as `value: undefined` for
|
|
56
63
|
* resolveWallet to reject with a precise error.
|
|
@@ -58,16 +65,26 @@ const EXISTENCE_EXEMPT = new Set(["wallets", "init", "doctor"]);
|
|
|
58
65
|
export function splitWalletFlag(rawArgv = []) {
|
|
59
66
|
const argv = [];
|
|
60
67
|
let flag = null;
|
|
68
|
+
// The command word: the first positional that is not the value of a global flag.
|
|
69
|
+
let first;
|
|
70
|
+
for (let i = 0; i < rawArgv.length && first === undefined; i++) {
|
|
71
|
+
const a = rawArgv[i];
|
|
72
|
+
if (typeof a !== "string" || a.startsWith("-")) continue;
|
|
73
|
+
if (i > 0 && GLOBAL_FLAGS.has(rawArgv[i - 1])) continue;
|
|
74
|
+
first = a;
|
|
75
|
+
}
|
|
76
|
+
const ownsProfile = OWNS_PROFILE_FLAG.has(first);
|
|
77
|
+
const isGlobal = (name) => GLOBAL_FLAGS.has(name) && !(ownsProfile && name === "--profile");
|
|
61
78
|
for (let i = 0; i < rawArgv.length; i++) {
|
|
62
79
|
const a = rawArgv[i];
|
|
63
80
|
if (typeof a === "string" && a.startsWith("--") && a.includes("=")) {
|
|
64
81
|
const name = a.slice(0, a.indexOf("="));
|
|
65
|
-
if (
|
|
82
|
+
if (isGlobal(name)) {
|
|
66
83
|
flag = { flag: name, value: a.slice(a.indexOf("=") + 1) };
|
|
67
84
|
continue;
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
|
-
if (typeof a === "string" &&
|
|
87
|
+
if (typeof a === "string" && isGlobal(a)) {
|
|
71
88
|
const next = rawArgv[i + 1];
|
|
72
89
|
if (next === undefined || (typeof next === "string" && next.startsWith("-"))) {
|
|
73
90
|
flag = { flag: a, value: undefined };
|
|
@@ -71,6 +71,18 @@ describe("splitWalletFlag", () => {
|
|
|
71
71
|
assert.deepEqual(r.argv, ["deploy", "apply", "--manifest", "x"]);
|
|
72
72
|
assert.equal(r.walletFlag.value, "foo");
|
|
73
73
|
});
|
|
74
|
+
it("leaves --profile to `repos`, which owns it as the AWS credential profile", () => {
|
|
75
|
+
const r = splitWalletFlag(["repos", "create", "byo-1", "--byo", "s3://b/p", "--profile", "acme", "--region", "us-east-1"]);
|
|
76
|
+
assert.equal(r.walletFlag, null);
|
|
77
|
+
assert.deepEqual(r.argv, ["repos", "create", "byo-1", "--byo", "s3://b/p", "--profile", "acme", "--region", "us-east-1"]);
|
|
78
|
+
const m = splitWalletFlag(["--wallet", "w", "repos", "mirror", "s3://m", "--profile=acme"]);
|
|
79
|
+
assert.deepEqual(m.walletFlag, { flag: "--wallet", value: "w" });
|
|
80
|
+
assert.deepEqual(m.argv, ["repos", "mirror", "s3://m", "--profile=acme"]);
|
|
81
|
+
const rec = splitWalletFlag(["repos", "recover", "s3://m", "--out", "./r", "--profile", "acme"]);
|
|
82
|
+
assert.equal(rec.walletFlag, null);
|
|
83
|
+
assert.ok(rec.argv.includes("--profile") && rec.argv.includes("acme"));
|
|
84
|
+
});
|
|
85
|
+
|
|
74
86
|
it("accepts --profile as an alias", () => {
|
|
75
87
|
const r = splitWalletFlag(["--profile", "p", "status"]);
|
|
76
88
|
assert.equal(r.walletFlag.flag, "--profile");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "4.70.
|
|
3
|
+
"version": "4.70.4",
|
|
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": {
|