@pinchpass/cli 1.1.0 → 1.2.0

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 CHANGED
@@ -14,8 +14,26 @@ in the URL fragment.
14
14
  npm i -g @pinchpass/cli
15
15
  ```
16
16
 
17
- The install downloads the prebuilt `pinchpass` binary for your platform to
18
- `~/.local/bin` (or finds an existing install).
17
+ ```sh
18
+ brew install --cask rubybear-lgtm/tap/pinchpass
19
+ ```
20
+
21
+ The npm postinstall downloads the prebuilt `pinchpass` binary to `~/.local/bin`.
22
+ It is version-aware: when the binary it finds (on `PATH`, or in `~/.local/bin`,
23
+ `~/.openclaw/bin`, `~/.pi/bin`) does not report the package's version, it
24
+ downloads the matching release asset and replaces it. Downloads are verified
25
+ against the release's `checksums.txt`. Set `PINCHPASS_SKIP_DOWNLOAD=1` to skip.
26
+
27
+ ## Updating
28
+
29
+ ```sh
30
+ pinchpass --version # pinchpass v1.2.0 (abc1234)
31
+ npm update -g @pinchpass/cli # re-runs the postinstall
32
+ brew upgrade --cask pinchpass
33
+ go install github.com/rubybear-lgtm/pinchpass@latest
34
+ ```
35
+
36
+ The extension warns when the binary it resolves is older than the package.
19
37
 
20
38
  ## Pi (pi.dev)
21
39
 
@@ -30,10 +48,15 @@ That registers:
30
48
  - **`request_secret` tool** — two collection modes:
31
49
  - `via: "modal"` (default in the TUI): a Pi dialog collects each secret
32
50
  directly into `.env` — the value never reaches the LLM, never enters the
33
- transcript, and no server is started. Cancelable with `Esc`.
51
+ transcript, and no server is started. Cancelable with `Esc`. `ttl` does not
52
+ apply here and is ignored.
34
53
  - `via: "link"`: spawns the CLI, streams the one-time E2E-encrypted link to
35
- the user immediately, blocks until they submit or the TTL expires.
54
+ the user immediately, blocks until they submit or the TTL expires. The URL
55
+ (including its `#k=` decryption-key fragment) is shown to the model and
56
+ stays in the transcript, so prefer the modal when the secret is sensitive.
36
57
  - `via` defaults to `"auto"`: modal in the TUI, link everywhere else.
58
+ - `tunnel: true` always uses the link, even in the TUI — the modal cannot
59
+ serve a remote user.
37
60
  - **`pinchpass` skill** — full usage guidance for the model.
38
61
 
39
62
  The binary is resolved from `PATH`, `~/.local/bin`, `~/.openclaw/bin`, and
@@ -58,6 +81,7 @@ pinchpass request <secret-name>... [flags]
58
81
  | `-out` | `.env` | Output .env file path |
59
82
  | `-ttl` | 30 | Minutes until the link expires |
60
83
  | `-port` | random | Local port to bind |
84
+ | `-listen-addr` | `127.0.0.1` | Address to listen on |
61
85
  | `-json` | false | Machine-readable JSON output |
62
86
 
63
87
  ## License
package/dist/args.js ADDED
@@ -0,0 +1,30 @@
1
+ /** Tool/CLI argument types and translation to pinchpass CLI flags. */
2
+ /**
3
+ * Decide how to collect a secret.
4
+ *
5
+ * A tunnel or an explicit `link` request always uses the browser link, since
6
+ * the modal cannot leave the local machine. The modal is only reachable from
7
+ * the interactive TUI; everywhere else (and for `via: "auto"`) the link wins.
8
+ */
9
+ export function chooseVia(via, tunnel, mode) {
10
+ const requested = via ?? "auto";
11
+ const forceLink = requested === "link" || tunnel === true;
12
+ const modal = !forceLink && mode === "tui";
13
+ return {
14
+ via: modal ? "modal" : "link",
15
+ downgraded: requested === "modal" && !modal,
16
+ };
17
+ }
18
+ /** Build the `pinchpass request ...` argv for the given tool arguments. */
19
+ export function buildArgs(params) {
20
+ const args = ["request", ...params.names, "-json"];
21
+ if (params.tunnel)
22
+ args.push("-tunnel");
23
+ if (params.note)
24
+ args.push("-note", params.note);
25
+ if (typeof params.ttl === "number")
26
+ args.push("-ttl", String(params.ttl));
27
+ if (params.out)
28
+ args.push("-out", params.out);
29
+ return args;
30
+ }
package/dist/binary.js ADDED
@@ -0,0 +1,124 @@
1
+ /** Locate the pinchpass binary without requiring PATH configuration. */
2
+ import { accessSync, constants, readFileSync, statSync } from "node:fs";
3
+ import { execFileSync } from "node:child_process";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ /** Real binaries are multi-MB; a failed download can leave a tiny stub behind. */
7
+ export const MIN_VALID_BYTES = 100 * 1024;
8
+ /** Extra directories where the postinstall script may have placed the binary. */
9
+ export const FALLBACK_DIRS = [
10
+ join(homedir(), ".local", "bin"),
11
+ join(homedir(), ".openclaw", "bin"),
12
+ join(homedir(), ".pi", "bin"),
13
+ ];
14
+ export const BINARY_MISSING = `The pinchpass binary is not installed. Install it with one of:
15
+
16
+ npm i -g @pinchpass/cli # downloads the prebuilt binary for your platform
17
+ go install github.com/rubybear-lgtm/pinchpass@latest
18
+
19
+ Then try request_secret again.`;
20
+ export function binaryName(platform = process.platform) {
21
+ return platform === "win32" ? "pinchpass.exe" : "pinchpass";
22
+ }
23
+ /**
24
+ * Search explicit directories for an executable `pinchpass`.
25
+ *
26
+ * A candidate that looks like a real binary (>= MIN_VALID_BYTES) wins over one
27
+ * that does not, so a stale stub earlier in PATH cannot shadow a good install.
28
+ */
29
+ export function findBinaryIn(dirs, platform = process.platform) {
30
+ const name = binaryName(platform);
31
+ const seen = new Set();
32
+ const executable = [];
33
+ for (const dir of dirs) {
34
+ if (!dir || seen.has(dir))
35
+ continue;
36
+ seen.add(dir);
37
+ const candidate = join(dir, name);
38
+ try {
39
+ accessSync(candidate, constants.X_OK);
40
+ }
41
+ catch {
42
+ continue;
43
+ }
44
+ executable.push(candidate);
45
+ }
46
+ for (const candidate of executable) {
47
+ try {
48
+ if (statSync(candidate).size >= MIN_VALID_BYTES)
49
+ return candidate;
50
+ }
51
+ catch {
52
+ // keep looking
53
+ }
54
+ }
55
+ return executable[0] ?? null;
56
+ }
57
+ /** Find pinchpass on PATH, then in the known install directories. */
58
+ export function findBinary() {
59
+ const sep = process.platform === "win32" ? ";" : ":";
60
+ const pathDirs = (process.env.PATH ?? "").split(sep).filter(Boolean);
61
+ return findBinaryIn([...pathDirs, ...FALLBACK_DIRS]);
62
+ }
63
+ const PACKAGE_NAME = "@pinchpass/cli";
64
+ const UPDATE_HINT = `npm i -g ${PACKAGE_NAME} (or go install github.com/rubybear-lgtm/pinchpass@latest)`;
65
+ /** The version of this npm package, read from its own package.json. */
66
+ export function packageVersion() {
67
+ try {
68
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
69
+ return JSON.parse(raw).version ?? null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ /** Extract x.y.z from `pinchpass --version` output (null for older builds). */
76
+ export function parseVersion(text) {
77
+ const match = /(?:^|\s)v?(\d+\.\d+\.\d+)(?=\s|$)/.exec(text ?? "");
78
+ return match ? match[1] : null;
79
+ }
80
+ /** Numeric x.y.z comparison. Returns -1, 0, or 1. */
81
+ export function compareVersions(a, b) {
82
+ const pa = a.split(".").map(Number);
83
+ const pb = b.split(".").map(Number);
84
+ for (let i = 0; i < 3; i++) {
85
+ const diff = (pa[i] || 0) - (pb[i] || 0);
86
+ if (diff !== 0)
87
+ return diff < 0 ? -1 : 1;
88
+ }
89
+ return 0;
90
+ }
91
+ const versionCache = new Map();
92
+ /** The version a binary reports, or null when it predates --version. */
93
+ export function binaryVersion(bin) {
94
+ if (versionCache.has(bin))
95
+ return versionCache.get(bin) ?? null;
96
+ let result = null;
97
+ try {
98
+ const out = execFileSync(bin, ["--version"], {
99
+ encoding: "utf8",
100
+ stdio: ["ignore", "pipe", "ignore"],
101
+ timeout: 5000,
102
+ });
103
+ result = parseVersion(out);
104
+ }
105
+ catch {
106
+ result = null;
107
+ }
108
+ versionCache.set(bin, result);
109
+ return result;
110
+ }
111
+ /** A one-line update hint when the resolved binary is older than the package. */
112
+ export function updateNotice(bin) {
113
+ const pkg = packageVersion();
114
+ if (!pkg)
115
+ return null;
116
+ const installed = binaryVersion(bin);
117
+ if (!installed) {
118
+ return `this pinchpass binary predates version reporting — update it: ${UPDATE_HINT}`;
119
+ }
120
+ if (compareVersions(installed, pkg) < 0) {
121
+ return `pinchpass v${installed} is older than this package (v${pkg}) — update it: ${UPDATE_HINT}`;
122
+ }
123
+ return null;
124
+ }
package/dist/env.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `.env` writing helpers for the modal collection path.
3
+ *
4
+ * Mirrors `store.EnvStore.Save` in the Go CLI: values are written as
5
+ * `KEY="value"` with shell-safe escaping, and an existing key is replaced in
6
+ * place. Writes go through a temp file + rename so a crash or a concurrent
7
+ * reader never observes a half-written file.
8
+ */
9
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { randomBytes } from "node:crypto";
11
+ import { dirname, join } from "node:path";
12
+ /** Shell-escape a value for a double-quoted .env line (mirrors store.go). */
13
+ export function escapeEnvValue(s) {
14
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
15
+ }
16
+ /** Render the file contents with `key` set to `value`, replacing any existing line. */
17
+ export function renderEnv(existing, key, value) {
18
+ const line = `${key}="${escapeEnvValue(value)}"`;
19
+ const out = [];
20
+ let replaced = false;
21
+ for (const l of existing.split("\n")) {
22
+ const t = l.trim();
23
+ if (t === "")
24
+ continue;
25
+ if (t.startsWith(key + "=")) {
26
+ out.push(line);
27
+ replaced = true;
28
+ }
29
+ else {
30
+ out.push(l);
31
+ }
32
+ }
33
+ if (!replaced)
34
+ out.push(line);
35
+ return out.join("\n") + "\n";
36
+ }
37
+ /** Write/update a single KEY="value" line in a .env file. */
38
+ export function writeEnvKey(path, key, value) {
39
+ const dir = dirname(path);
40
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ let existing = "";
42
+ try {
43
+ existing = readFileSync(path, "utf8");
44
+ }
45
+ catch {
46
+ // new file
47
+ }
48
+ const tmp = join(dir, `.pinchpass-${randomBytes(6).toString("hex")}.tmp`);
49
+ try {
50
+ writeFileSync(tmp, renderEnv(existing, key, value), { mode: 0o600 });
51
+ renameSync(tmp, path);
52
+ }
53
+ catch (err) {
54
+ try {
55
+ unlinkSync(tmp);
56
+ }
57
+ catch {
58
+ // nothing to clean up
59
+ }
60
+ throw err;
61
+ }
62
+ }