@homespunapps/cli 1.0.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.
@@ -0,0 +1,115 @@
1
+ // CLI auto-upgrade helpers — install-method detection and message formatting.
2
+ //
3
+ // Called by the top-level error handler when a relay returns 426
4
+ // `cli_upgrade_required`. The goal is to print a single, machine-parseable
5
+ // line the agent can lift verbatim — and tell the human (or the agent's
6
+ // harness) exactly what to run instead of a generic "upgrade @homespunapps/cli".
7
+ //
8
+ // Detection is best-effort: we inspect `process.execPath` and the CLI's own
9
+ // install path. There's no programmatic "ask npm what installed me" API, so
10
+ // the heuristics below are matched against the well-known install layouts
11
+ // of each package manager. Anything unrecognized lands in `unknown`, which
12
+ // means "tell the agent to ask the human" rather than guess.
13
+ /**
14
+ * Detection rules, ordered most-specific to least. Each rule looks at the
15
+ * directory the CLI is running from — caller passes `import.meta.url`-
16
+ * derived absolute path for the CLI entry. The actual file at that path
17
+ * doesn't need to exist; we're only pattern-matching the path itself, so
18
+ * tests can pass synthetic strings.
19
+ *
20
+ * Patterns are deliberately loose (substring tests, not exact prefixes) so
21
+ * the same rule handles per-user installs (`~/.npm-global/lib/node_modules/`),
22
+ * system installs (`/usr/lib/node_modules/`), and the macOS/Linux
23
+ * variations within each manager — without listing every layout.
24
+ */
25
+ export function detectInstallMethod(entryPath) {
26
+ // Volta wraps every binary in a shim under ~/.volta/tools/image/packages/
27
+ // and re-exports it via ~/.volta/bin/. Either path is a positive match.
28
+ if (entryPath.includes("/.volta/"))
29
+ return "volta";
30
+ // Bun's global registry: ~/.bun/install/global/node_modules/@homespunapps/cli/...
31
+ if (entryPath.includes("/.bun/install/global/"))
32
+ return "bun-global";
33
+ // npm global, in both common shapes:
34
+ // /usr/(local/)?lib/node_modules/@homespunapps/cli/... (system)
35
+ // ~/.npm-global/lib/node_modules/@homespunapps/cli/... (npm prefix)
36
+ // ~/.nvm/versions/node/vXX/lib/node_modules/... (nvm)
37
+ if (/\/lib\/node_modules\/@homespunapps\/cli\//.test(entryPath) ||
38
+ /\/lib\/node_modules\/\.bin\//.test(entryPath)) {
39
+ return "npm-global";
40
+ }
41
+ // npx caches the package under ~/Library/Caches/_npx (macOS) or
42
+ // ~/.npm/_npx (Linux) and runs it from a node_modules inside that dir.
43
+ // Treat this distinctly from a real vendored install: with an npx
44
+ // execution there is no project package.json owning the version and no
45
+ // global to upgrade — the user runs `npx @homespunapps/cli@<version>` each
46
+ // time, so the right answer is "ask the human / re-run with a newer
47
+ // explicit version".
48
+ if (entryPath.includes("/_npx/"))
49
+ return "unknown";
50
+ // Vendored: the CLI lives inside the *project's* node_modules — i.e. the
51
+ // user did `npm i @homespunapps/cli` (no -g) and runs it via a local script.
52
+ // We can't safely upgrade this for them; package.json owns it.
53
+ if (entryPath.includes("/node_modules/@homespunapps/cli/"))
54
+ return "vendored";
55
+ // pnpm temp, asdf, or anything else.
56
+ return "unknown";
57
+ }
58
+ /**
59
+ * Returns the shell command the human (or the agent, in a sandbox it owns)
60
+ * can run to upgrade @homespunapps/cli to satisfy `minVersion`. `null` means "no
61
+ * portable command exists — escalate to the human."
62
+ *
63
+ * Always pin the upgrade target to `>=${minVersion}` instead of `@latest`
64
+ * so a self-hosted relay that requires 0.0.7 doesn't drag the client to a
65
+ * future 0.1.0 that may have its own incompatibilities. The trailing
66
+ * `@latest`-equivalent is fine for the operator who deliberately
67
+ * fast-forwards.
68
+ */
69
+ export function upgradeCommandFor(method, minVersion) {
70
+ const spec = `@homespunapps/cli@>=${minVersion}`;
71
+ switch (method) {
72
+ case "npm-global":
73
+ return `npm install -g ${spec}`;
74
+ case "bun-global":
75
+ return `bun install -g ${spec}`;
76
+ case "volta":
77
+ return `volta install ${spec}`;
78
+ case "vendored":
79
+ case "unknown":
80
+ return null;
81
+ }
82
+ }
83
+ /**
84
+ * The deterministic stderr block the CLI prints on a 426 response. The agent
85
+ * is expected to read this verbatim and (per SKILL.md) run the printed
86
+ * command, then re-run its original `homespun` invocation once. Format is held
87
+ * stable across CLI versions so the skill's instructions don't drift — a
88
+ * change here is a contract change.
89
+ */
90
+ export function formatUpgradeMessage(err, method, command) {
91
+ // The relay's 426 payload puts the two version strings under details. We
92
+ // tolerate a missing/malformed details object so a misbehaving relay
93
+ // can't crash the CLI's own error path — show whatever we have.
94
+ const details = (err.details ?? {});
95
+ const minVersion = typeof details.min_version === "string" ? details.min_version : "?";
96
+ const yourVersion = typeof details.your_version === "string" ? details.your_version : "?";
97
+ const lines = [];
98
+ lines.push(`app: this relay requires @homespunapps/cli >= ${minVersion} (you have ${yourVersion}).`);
99
+ if (command !== null) {
100
+ lines.push(`To upgrade: ${command}`);
101
+ }
102
+ else if (method === "vendored") {
103
+ lines.push("Install method: vendored (inside a project's node_modules). Bump the @homespunapps/cli version in that project's package.json and re-install — the CLI isn't safe to upgrade globally for a vendored install.");
104
+ }
105
+ else {
106
+ lines.push("Install method: unknown. Ask the human to upgrade @homespunapps/cli — the install path didn't match any pattern we recognize (npm-global, bun-global, volta).");
107
+ }
108
+ return lines.join("\n");
109
+ }
110
+ /**
111
+ * Stable exit code used by the CLI on a `cli_upgrade_required` response.
112
+ * Sysexits.h's `EX_TEMPFAIL` — "temporary failure; retry after fixing".
113
+ * Documented in SKILL.md so an agent's harness can branch on it.
114
+ */
115
+ export const EXIT_CLI_UPGRADE_REQUIRED = 75;
@@ -0,0 +1,11 @@
1
+ // Single source of truth for the CLI version string.
2
+ //
3
+ // - `homespun --version` prints this verbatim.
4
+ // - Every HomespunClient construction passes it as `cliVersion`, which apps
5
+ // as the `x-homespun-cli-version` header on every relay request — drives the
6
+ // relay's version-skew check (HTTP 426 `cli_upgrade_required`).
7
+ //
8
+ // Keep this in lockstep with packages/cli/package.json's `version` field;
9
+ // they're consulted in different places (here for the runtime header,
10
+ // package.json for npm publish + dependency resolution).
11
+ export const VERSION = "0.0.29";
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@homespunapps/cli",
3
+ "version": "1.0.0",
4
+ "description": "Command-line client for the Homespun relay: create apps, inspect state, send and watch events.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "homespun",
9
+ "cli",
10
+ "agent",
11
+ "relay",
12
+ "human-in-the-loop"
13
+ ],
14
+ "homepage": "https://github.com/aerolalit/homespun#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/aerolalit/homespun.git",
18
+ "directory": "packages/cli"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/aerolalit/homespun/issues"
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "bin": {
30
+ "homespun": "dist/index.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "test:unit": "vitest run"
42
+ },
43
+ "dependencies": {
44
+ "@homespunapps/core": "^1.0.0",
45
+ "qrcode-terminal": "^0.12.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.9.2",
49
+ "@types/qrcode-terminal": "^0.12.2",
50
+ "typescript": "^6.0.3",
51
+ "vitest": "^4.1.8"
52
+ }
53
+ }