watchmen-cli 1.2.2 → 1.3.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.
Files changed (5) hide show
  1. package/README.md +18 -1
  2. package/bin/wm.js +32 -20
  3. package/package.json +12 -19
  4. package/shim.js +91 -0
  5. package/install.js +0 -272
package/README.md CHANGED
@@ -14,6 +14,22 @@ Or use directly:
14
14
  npx watchmen-cli version
15
15
  ```
16
16
 
17
+ ## How install works (no postinstall download)
18
+
19
+ `watchmen-cli` is a small JS shim. The platform binary ships inside one of
20
+ three sibling packages, picked by npm via `optionalDependencies` based on
21
+ your host's `os` + `cpu`:
22
+
23
+ | Platform | Package |
24
+ |---|---|
25
+ | macOS arm64 (Apple Silicon and Rosetta x86_64) | `watchmen-cli-darwin-arm64` |
26
+ | Linux x86_64 | `watchmen-cli-linux-x64` |
27
+ | Windows x86_64 | `watchmen-cli-win32-x64` |
28
+
29
+ There is no `postinstall` script, no network download at install time, and
30
+ no SHA-256 round-trip. npm itself verifies the tarball checksum of every
31
+ package it downloads from the registry.
32
+
17
33
  ## What it does
18
34
 
19
35
  WatchmenCLI scans your development environment across 13 dimensions (packages, runtimes, secrets, Docker, network, permissions, databases, and more), indexes 30+ document formats with OCR, and exposes everything as 24 MCP tools for AI coding assistants.
@@ -32,5 +48,6 @@ wm doctor # Self-diagnostic
32
48
  ## Links
33
49
 
34
50
  - Website: https://trywatchmen.cloud
35
- - GitHub: https://github.com/trywatchmen/watchmen-cli
51
+ - Trust & verification: https://trywatchmen.cloud/trust
52
+ - Public metadata repo: https://github.com/watchmencli/watchmen-cli-public
36
53
  - Docs: https://trywatchmen.cloud/docs
package/bin/wm.js CHANGED
@@ -1,28 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- const { execFileSync } = require("child_process");
5
- const path = require("path");
6
- const fs = require("fs");
4
+ // TH-P0-03 Platform-package entry shim.
5
+ // Resolves the platform binary via shim.js (which delegates to npm's
6
+ // optionalDependencies installation map) and execs it. Forwards args,
7
+ // stdio, env, and exit code transparently.
7
8
 
8
- const binDir = __dirname;
9
- const ext = process.platform === "win32" ? ".exe" : "";
10
- const binPath = path.join(binDir, `wm${ext}`);
11
-
12
- if (!fs.existsSync(binPath)) {
13
- console.error(
14
- "wm binary not found. Run: npm rebuild watchmen-cli\n" +
15
- `Expected at: ${binPath}`
16
- );
17
- process.exit(1);
18
- }
9
+ const { spawnSync } = require("child_process");
10
+ const { resolveBinary } = require("..");
19
11
 
12
+ let binary;
20
13
  try {
21
- execFileSync(binPath, process.argv.slice(2), {
22
- stdio: "inherit",
23
- env: process.env,
24
- });
14
+ binary = resolveBinary();
25
15
  } catch (err) {
26
- // execFileSync throws on non-zero exit — forward the exit code
27
- process.exit(err.status || 1);
16
+ console.error("\n " + (err.message || String(err)) + "\n");
17
+ process.exit(127);
18
+ }
19
+
20
+ const result = spawnSync(binary, process.argv.slice(2), {
21
+ stdio: "inherit",
22
+ env: process.env,
23
+ windowsHide: false,
24
+ });
25
+
26
+ if (result.error) {
27
+ console.error("\n Failed to spawn wm binary:");
28
+ console.error(" " + (result.error.message || String(result.error)));
29
+ console.error(" binary path: " + binary + "\n");
30
+ process.exit(127);
31
+ }
32
+
33
+ // Forward the exit status. signal-terminations get encoded as 128+signo
34
+ // (the standard shell convention).
35
+ if (result.signal) {
36
+ const SIG = { SIGINT: 2, SIGTERM: 15, SIGKILL: 9, SIGHUP: 1 };
37
+ process.exit(128 + (SIG[result.signal] || 0));
28
38
  }
39
+
40
+ process.exit(result.status || 0);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "watchmen-cli",
3
- "version": "1.2.2",
3
+ "version": "1.3.3",
4
4
  "description": "Capture, compare, and transfer your complete development environment",
5
- "main": "install.js",
5
+ "main": "shim.js",
6
6
  "keywords": [
7
7
  "cli",
8
8
  "environment",
@@ -15,34 +15,27 @@
15
15
  "homepage": "https://trywatchmen.cloud",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/trywatchmen/watchmen-cli.git"
18
+ "url": "git+https://github.com/watchmencli/watchmen-cli-public.git"
19
19
  },
20
20
  "bugs": {
21
- "url": "https://github.com/trywatchmen/watchmen-cli/issues"
21
+ "url": "https://github.com/watchmencli/watchmen-cli-public/issues"
22
22
  },
23
23
  "license": "MIT",
24
24
  "author": "Cristiano Pereira da Silva Muniz, LLC",
25
25
  "bin": {
26
26
  "wm": "bin/wm.js"
27
27
  },
28
- "scripts": {
29
- "postinstall": "node install.js"
30
- },
31
- "os": [
32
- "darwin",
33
- "linux",
34
- "win32"
35
- ],
36
- "cpu": [
37
- "arm64",
38
- "x64"
39
- ],
40
28
  "engines": {
41
29
  "node": ">=16"
42
30
  },
31
+ "optionalDependencies": {
32
+ "watchmen-cli-darwin-arm64": "1.3.3",
33
+ "watchmen-cli-linux-x64": "1.3.3",
34
+ "watchmen-cli-win32-x64": "1.3.3"
35
+ },
43
36
  "files": [
44
- "install.js",
45
- "README.md",
46
- "bin/"
37
+ "shim.js",
38
+ "bin/wm.js",
39
+ "README.md"
47
40
  ]
48
41
  }
package/shim.js ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+
3
+ // TH-P0-03 — Platform-package shim.
4
+ //
5
+ // `watchmen-cli` carries no binary itself. At install time, npm picks the
6
+ // correct platform package from `optionalDependencies` based on the host's
7
+ // platform + arch (npm 7+, pnpm, bun all honor `os` and `cpu` filtering).
8
+ // The binary lives inside that platform package's `bin/` directory.
9
+ //
10
+ // This file is the resolver. It is required by `bin/wm.js` (the spawned
11
+ // entrypoint) and by `package.json#main` (so callers can also do
12
+ // `require('watchmen-cli').path` programmatically).
13
+ //
14
+ // We intentionally do NOT do any network IO. No `postinstall`, no fetch,
15
+ // no SHA verification at install time — npm's own tarball checksum already
16
+ // verifies what we shipped. The binary's authenticity comes from npm
17
+ // publish provenance + the platform-package tarball SHA.
18
+
19
+ const path = require("path");
20
+
21
+ const PLATFORMS = {
22
+ // platform-key → npm-package-name + binary basename
23
+ // Only entries listed here get matched. Anything else is "unsupported".
24
+ "darwin-arm64": { pkg: "watchmen-cli-darwin-arm64", bin: "wm" },
25
+ "darwin-x64": { pkg: "watchmen-cli-darwin-arm64", bin: "wm" }, // Rosetta 2 fallback
26
+ "linux-x64": { pkg: "watchmen-cli-linux-x64", bin: "wm" },
27
+ "linux-arm64": { pkg: "watchmen-cli-linux-x64", bin: "wm" }, // best-effort until native arm64 ships
28
+ "win32-x64": { pkg: "watchmen-cli-win32-x64", bin: "wm.exe" },
29
+ };
30
+
31
+ function platformKey() {
32
+ return `${process.platform}-${process.arch}`;
33
+ }
34
+
35
+ function resolveBinary() {
36
+ const key = platformKey();
37
+ const entry = PLATFORMS[key];
38
+ if (!entry) {
39
+ const supported = Object.keys(PLATFORMS).join(", ");
40
+ throw new Error(
41
+ `Unsupported platform: ${key}\n` +
42
+ ` Supported: ${supported}\n\n` +
43
+ ` Manual install: https://trywatchmen.cloud/docs/getting-started/installation`
44
+ );
45
+ }
46
+ // Resolution strategy — try increasingly broad lookup paths. In a normal
47
+ // npm registry install the shim and platform package are siblings under
48
+ // node_modules/, and the default Node resolver finds the platform package
49
+ // by walking up from __dirname. For `file:` installs, symlinks point at
50
+ // the source dir which has no relevant node_modules, so we fall back to
51
+ // a default-paths resolve (which walks from process.cwd()).
52
+ let pkgJsonPath;
53
+ let lastErr;
54
+ const candidates = [
55
+ // 1. Default resolver — covers registry install (sibling under node_modules/).
56
+ () => require.resolve(`${entry.pkg}/package.json`),
57
+ // 2. Explicit paths from the shim's own directory upward — covers
58
+ // global installs where the shim is at <prefix>/lib/node_modules/...
59
+ () => require.resolve(`${entry.pkg}/package.json`, {
60
+ paths: [__dirname, path.dirname(__dirname), path.dirname(path.dirname(__dirname))],
61
+ }),
62
+ ];
63
+ for (const tryResolve of candidates) {
64
+ try { pkgJsonPath = tryResolve(); break; }
65
+ catch (err) { lastErr = err; }
66
+ }
67
+ if (!pkgJsonPath) {
68
+ throw new Error(
69
+ `Platform package "${entry.pkg}" was not installed.\n` +
70
+ ` This usually means npm's optionalDependencies filter excluded it,\n` +
71
+ ` or your installer (yarn, pnpm, bun) did not honor the os/cpu fields.\n` +
72
+ ` Try: npm install -g watchmen-cli@${require("./package.json").version}\n` +
73
+ ` Or download manually: https://trywatchmen.cloud/docs/getting-started/installation\n` +
74
+ ` Underlying error: ${lastErr ? (lastErr.code || lastErr.message) : "unknown"}`
75
+ );
76
+ }
77
+ const pkgRoot = path.dirname(pkgJsonPath);
78
+ return path.join(pkgRoot, "bin", entry.bin);
79
+ }
80
+
81
+ module.exports = {
82
+ /**
83
+ * Absolute path to the wm binary for the current platform.
84
+ * Throws if the platform package is missing.
85
+ */
86
+ get path() {
87
+ return resolveBinary();
88
+ },
89
+ resolveBinary,
90
+ platformKey,
91
+ };
package/install.js DELETED
@@ -1,272 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const https = require("https");
5
- const http = require("http");
6
- const fs = require("fs");
7
- const path = require("path");
8
- const crypto = require("crypto");
9
- const { execSync } = require("child_process");
10
-
11
- const VERSION = require("./package.json").version;
12
- // Versioned URL first (exact-pin, immune to CDN drift on the latest alias);
13
- // fall back to /latest/ when the versioned path 404s (e.g. release mid-promote).
14
- // Either way, the binary's reported version is asserted against VERSION below.
15
- const DOWNLOAD_BASE = "https://releases.trywatchmen.cloud/download/community";
16
- const VERSIONED_URL_TMPL = (slug) => `${DOWNLOAD_BASE}/${VERSION}/${slug}`;
17
- const LATEST_URL_TMPL = (slug) => `${DOWNLOAD_BASE}/${slug}`;
18
-
19
- // 60s connect timeout, 120s total — covers slow connections without hanging forever
20
- const CONNECT_TIMEOUT_MS = 60_000;
21
- const TOTAL_TIMEOUT_MS = 120_000;
22
-
23
- // Platform → download path mapping
24
- const PLATFORMS = {
25
- "darwin-arm64": "macos-arm64",
26
- "darwin-x64": "macos-arm64", // Rosetta 2 fallback
27
- "linux-x64": "linux-x86_64",
28
- "linux-arm64": "linux-x86_64", // best-effort — no native arm64 yet
29
- "win32-x64": "windows-x86_64",
30
- };
31
-
32
- function getPlatformKey() {
33
- return `${process.platform}-${process.arch}`;
34
- }
35
-
36
- function getPlatformSlug() {
37
- const key = getPlatformKey();
38
- const slug = PLATFORMS[key];
39
- if (!slug) {
40
- console.error(
41
- `\n Unsupported platform: ${key}\n` +
42
- ` Supported: ${Object.keys(PLATFORMS).join(", ")}\n\n` +
43
- ` You can download manually from: https://trywatchmen.cloud/docs/getting-started/installation\n`
44
- );
45
- process.exit(1);
46
- }
47
- return slug;
48
- }
49
-
50
- function formatBytes(bytes) {
51
- if (bytes < 1024) return `${bytes} B`;
52
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
53
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
54
- }
55
-
56
- function download(url, maxRedirects = 5) {
57
- return new Promise((resolve, reject) => {
58
- if (maxRedirects <= 0) {
59
- return reject(new Error("Too many redirects"));
60
- }
61
-
62
- const client = url.startsWith("https") ? https : http;
63
- const req = client.get(
64
- url,
65
- {
66
- headers: { "User-Agent": `watchmen-cli-npm/${VERSION}` },
67
- timeout: CONNECT_TIMEOUT_MS,
68
- },
69
- (res) => {
70
- // Follow redirects
71
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
72
- res.resume(); // drain response
73
- return download(res.headers.location, maxRedirects - 1).then(resolve).catch(reject);
74
- }
75
- if (res.statusCode !== 200) {
76
- res.resume();
77
- return reject(new Error(`HTTP ${res.statusCode} from ${url.split("?")[0]}`));
78
- }
79
-
80
- const contentLength = parseInt(res.headers["content-length"], 10) || 0;
81
- const chunks = [];
82
- let received = 0;
83
- const startTime = Date.now();
84
-
85
- // Progress reporting
86
- const progressInterval = setInterval(() => {
87
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
88
- if (contentLength > 0) {
89
- const pct = ((received / contentLength) * 100).toFixed(0);
90
- process.stdout.write(`\r Downloading... ${formatBytes(received)} / ${formatBytes(contentLength)} (${pct}%) [${elapsed}s]`);
91
- } else {
92
- process.stdout.write(`\r Downloading... ${formatBytes(received)} [${elapsed}s]`);
93
- }
94
- }, 500);
95
-
96
- res.on("data", (chunk) => {
97
- chunks.push(chunk);
98
- received += chunk.length;
99
- });
100
-
101
- res.on("end", () => {
102
- clearInterval(progressInterval);
103
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
104
- process.stdout.write(`\r Downloaded ${formatBytes(received)} in ${elapsed}s${" ".repeat(20)}\n`);
105
- resolve(Buffer.concat(chunks));
106
- });
107
-
108
- res.on("error", (err) => {
109
- clearInterval(progressInterval);
110
- reject(err);
111
- });
112
- }
113
- );
114
-
115
- req.on("timeout", () => {
116
- req.destroy();
117
- reject(new Error(`Connection timed out after ${CONNECT_TIMEOUT_MS / 1000}s`));
118
- });
119
-
120
- req.on("error", reject);
121
-
122
- // Hard total timeout
123
- setTimeout(() => {
124
- req.destroy();
125
- reject(new Error(`Download timed out after ${TOTAL_TIMEOUT_MS / 1000}s — try again or download manually from https://trywatchmen.cloud`));
126
- }, TOTAL_TIMEOUT_MS);
127
- });
128
- }
129
-
130
- async function main() {
131
- const platformSlug = getPlatformSlug();
132
- const platformKey = getPlatformKey();
133
- const isWindows = process.platform === "win32";
134
- const binDir = path.join(__dirname, "bin");
135
- const binName = isWindows ? "wm.exe" : "wm";
136
- const binPath = path.join(binDir, binName);
137
-
138
- // Skip if already installed at correct version
139
- if (fs.existsSync(binPath)) {
140
- try {
141
- const installed = execSync(`"${binPath}" version`, {
142
- encoding: "utf8",
143
- timeout: 10_000,
144
- });
145
- if (installed.includes(VERSION)) {
146
- console.log(` wm v${VERSION} already installed at ${binPath}`);
147
- return;
148
- }
149
- console.log(` Upgrading wm to v${VERSION}...`);
150
- } catch {
151
- // Binary exists but broken/wrong version — redownload
152
- }
153
- }
154
-
155
- const versionedUrl = VERSIONED_URL_TMPL(platformSlug);
156
- const latestUrl = LATEST_URL_TMPL(platformSlug);
157
-
158
- console.log(`\n WatchmenCLI v${VERSION} — ${platformKey}`);
159
-
160
- let binary;
161
- let sourceUrl;
162
- try {
163
- console.log(` ${versionedUrl}`);
164
- binary = await download(versionedUrl);
165
- sourceUrl = versionedUrl;
166
- } catch (err) {
167
- console.log(` (versioned URL unavailable: ${err.message.split("\n")[0]})`);
168
- console.log(` falling back to: ${latestUrl}\n`);
169
- binary = await download(latestUrl);
170
- sourceUrl = latestUrl;
171
- }
172
-
173
- // Validate minimum binary size (real binaries are >5MB)
174
- if (binary.length < 1024 * 1024) {
175
- throw new Error(
176
- `Downloaded file is only ${formatBytes(binary.length)} — expected >5MB.\n` +
177
- ` This usually means the download URL returned an error page.\n` +
178
- ` Try: npm cache clean --force && npm install -g watchmen-cli`
179
- );
180
- }
181
-
182
- // SHA-256 verify against the .sha256 sibling published alongside the binary.
183
- // Missing sibling is a warning (older releases don't publish it), not fatal.
184
- const binaryDigest = crypto.createHash("sha256").update(binary).digest("hex");
185
- try {
186
- const shaUrl = sourceUrl + ".sha256";
187
- const shaBytes = await download(shaUrl);
188
- const expectedDigest = shaBytes.toString("utf8").trim().split(/\s+/)[0].toLowerCase();
189
- if (expectedDigest && /^[0-9a-f]{64}$/.test(expectedDigest)) {
190
- if (expectedDigest !== binaryDigest) {
191
- throw new Error(
192
- `SHA-256 mismatch:\n` +
193
- ` expected: ${expectedDigest}\n` +
194
- ` got: ${binaryDigest}\n` +
195
- ` source: ${sourceUrl}\n` +
196
- `Install aborted. Try: npm cache clean --force && npm install -g watchmen-cli`
197
- );
198
- }
199
- console.log(` SHA-256 verified: ${binaryDigest.slice(0, 16)}...`);
200
- } else {
201
- console.warn(` (SHA-256 file present but unparseable; continuing without verification)`);
202
- }
203
- } catch (err) {
204
- if (err.message && err.message.startsWith("SHA-256 mismatch")) throw err;
205
- console.warn(` (SHA-256 sibling unavailable — continuing without verification: ${err.message.split("\n")[0]})`);
206
- }
207
-
208
- // Write binary
209
- fs.mkdirSync(binDir, { recursive: true });
210
- fs.writeFileSync(binPath, binary);
211
-
212
- // chmod on Unix (no-op concept on Windows, but doesn't hurt)
213
- if (!isWindows) {
214
- fs.chmodSync(binPath, 0o755);
215
- }
216
-
217
- // The downloaded binary must report the same version as the npm package.
218
- // Fail-closed on mismatch: the /latest/ fallback could otherwise serve
219
- // a stale version during a mid-promote window.
220
- try {
221
- const reported = execSync(`"${binPath}" version`, { encoding: "utf8", timeout: 10_000 });
222
- const reportedMatch = reported.match(/(\d+\.\d+\.\d+)/);
223
- const reportedVersion = reportedMatch ? reportedMatch[1] : null;
224
- if (reportedVersion && reportedVersion !== VERSION) {
225
- // Delete the bad binary so rerunning `npm install` actually retries
226
- try { fs.unlinkSync(binPath); } catch {}
227
- throw new Error(
228
- `Downloaded binary reports version ${reportedVersion}, but npm package is ${VERSION}.\n` +
229
- ` This means the release pipeline has drift between dist-tag and served binary.\n` +
230
- ` Source: ${sourceUrl}\n` +
231
- ` Install aborted. This is almost certainly a transient CDN cache issue —\n` +
232
- ` try again in a few minutes, or: npm cache clean --force && npm install -g watchmen-cli`
233
- );
234
- }
235
- console.log(` Installed successfully: ${binPath}`);
236
- console.log(` Reports: ${reported.split("\n")[0]}\n`);
237
- } catch (err) {
238
- if (err.message && err.message.includes("reports version")) throw err;
239
- console.warn(
240
- ` Warning: binary installed but verification failed.\n` +
241
- ` Path: ${binPath}\n` +
242
- ` You may need to run: wm doctor\n`
243
- );
244
- }
245
- }
246
-
247
- main().catch((err) => {
248
- console.error(`\n Failed to install wm: ${err.message}\n`);
249
- // On Linux/macOS permission errors, suggest the user-local npm prefix
250
- // (most users don't have sudo-npm configured).
251
- if (/EACCES|EPERM|permission denied/i.test(err.message || "") && process.platform !== "win32") {
252
- console.error(
253
- ` Looks like a permissions problem. On Linux/macOS without sudo, npm's\n` +
254
- ` global install path is root-owned. The sustainable fix is a user-local\n` +
255
- ` prefix:\n` +
256
- `\n` +
257
- ` mkdir -p ~/.npm-global\n` +
258
- ` npm config set prefix ~/.npm-global\n` +
259
- ` echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc\n` +
260
- ` source ~/.bashrc\n` +
261
- ` npm install -g watchmen-cli@${VERSION}\n` +
262
- `\n`
263
- );
264
- }
265
- console.error(
266
- ` Troubleshooting:\n` +
267
- ` 1. Check your internet connection\n` +
268
- ` 2. Try: npm cache clean --force && npm install -g watchmen-cli\n` +
269
- ` 3. Download manually: https://trywatchmen.cloud/docs/getting-started/installation\n`
270
- );
271
- process.exit(1);
272
- });