attnmd 0.4.0 → 0.4.1

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 (3) hide show
  1. package/README.md +3 -5
  2. package/bin/attn.js +57 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,8 +17,8 @@
17
17
 
18
18
  <p align="center">
19
19
  <picture>
20
- <source media="(prefers-color-scheme: dark)" srcset="site/static/screenshots/collab-dark.png">
21
- <img src="site/static/screenshots/collab-light.png" alt="attn showing a shared markdown review with comments, suggestions, and a collaborator cursor" width="860">
20
+ <source media="(prefers-color-scheme: dark)" srcset="https://media.githubusercontent.com/media/lightsofapollo/attn/main/site/static/screenshots/collab-dark.png">
21
+ <img src="https://media.githubusercontent.com/media/lightsofapollo/attn/main/site/static/screenshots/collab-light.png" alt="attn showing a shared markdown review with comments, suggestions, and a collaborator cursor" width="860">
22
22
  </picture>
23
23
  </p>
24
24
 
@@ -104,7 +104,6 @@ attn review join 'attn://review/<room-id>#key=<secret>'
104
104
  For headless reviewers or agents:
105
105
 
106
106
  ```bash
107
- attn review register-agent reviewer
108
107
  attn review join 'attn://review/<room-id>#key=<secret>' --as-agent reviewer
109
108
  ```
110
109
 
@@ -149,8 +148,7 @@ attn --json spec.md # dump document structure as JSON
149
148
 
150
149
  ```bash
151
150
  attn review share docs/ # share a file or folder
152
- attn review join 'attn://review/...' # join through the running app
153
- attn review register-agent reviewer # create a headless reviewer identity
151
+ attn review join 'attn://review/...' # open/join through the app
154
152
  attn review join 'attn://review/...' --as-agent reviewer
155
153
  attn review list-agents
156
154
  attn review whoami
package/bin/attn.js CHANGED
@@ -62,14 +62,10 @@ async function main() {
62
62
  const version = packageJson.version;
63
63
  const headless = isHeadlessInvocation(args);
64
64
 
65
- // Prefer a natively-installed `attn` over anything we'd download. A dev
66
- // who built the binary, a Homebrew/cargo install, or a prior
67
- // `npx attnmd` that installed the ~/.local/bin alias should all be used
68
- // directly `npx attnmd` becomes a thin "use what's here, else fetch"
69
- // shim rather than always pulling its own copy. The recursion guard in
70
- // `findNativeBinary` excludes this package's own launcher so a global
71
- // `npm i -g attn` symlink can't loop back here.
72
- const nativeBinary = findNativeBinary();
65
+ // Prefer a natively-installed `attn` only when it matches this npm
66
+ // package. `npx attnmd@<version>` must not silently hand off to an older
67
+ // Homebrew/cargo/alias binary whose review protocol may be incompatible.
68
+ const nativeBinary = findNativeBinary(version);
73
69
  if (nativeBinary) {
74
70
  if (process.env.ATTN_DEBUG_LAUNCHER) {
75
71
  console.error(`attn: using native binary ${nativeBinary}`);
@@ -88,7 +84,8 @@ async function main() {
88
84
  }
89
85
 
90
86
  if (!appPath) {
91
- if (!existsSync(runtimeBinaryPath)) {
87
+ if (!isExpectedBinaryVersion(runtimeBinaryPath, version)) {
88
+ safeRemove(runtimeBinaryPath);
92
89
  await ensureRuntimeBinary(version);
93
90
  }
94
91
  if (!existsSync(runtimeBinaryPath)) {
@@ -118,13 +115,16 @@ async function main() {
118
115
  }
119
116
 
120
117
  async function resolveAppPath(version) {
121
- const globalApp = findGlobalAppInstall();
118
+ const globalApp = findGlobalAppInstall(version);
122
119
  if (globalApp) {
123
120
  return globalApp;
124
121
  }
125
122
 
126
123
  const managedVersionApp = join(managedAppsRoot, version, "attn.app");
127
- if (existsSync(managedVersionApp)) {
124
+ if (
125
+ existsSync(managedVersionApp) &&
126
+ isExpectedBinaryVersion(join(managedVersionApp, "Contents", "MacOS", "attn"), version)
127
+ ) {
128
128
  ensureCurrentAppLink(managedVersionApp);
129
129
  return managedVersionApp;
130
130
  }
@@ -156,13 +156,14 @@ async function ensureRuntimeBinary(version) {
156
156
  console.error(`attn: installed runtime binary ${runtimeBinaryPath}`);
157
157
  }
158
158
 
159
- function findGlobalAppInstall() {
159
+ function findGlobalAppInstall(version) {
160
160
  const candidates = [
161
161
  "/Applications/attn.app",
162
162
  join(userHome, "Applications", "attn.app"),
163
163
  ];
164
164
  for (const candidate of candidates) {
165
- if (existsSync(candidate)) {
165
+ const binary = join(candidate, "Contents", "MacOS", "attn");
166
+ if (existsSync(candidate) && isExpectedBinaryVersion(binary, version)) {
166
167
  return candidate;
167
168
  }
168
169
  }
@@ -182,7 +183,7 @@ function findGlobalAppInstall() {
182
183
  * whose realpath points back at bin/attn.js, can't cause infinite
183
184
  * recursion).
184
185
  */
185
- function findNativeBinary() {
186
+ function findNativeBinary(version) {
186
187
  const candidates = [];
187
188
 
188
189
  if (process.env.ATTN_BIN) {
@@ -202,7 +203,7 @@ function findNativeBinary() {
202
203
  );
203
204
 
204
205
  for (const candidate of candidates) {
205
- if (isUsableNativeBinary(candidate)) {
206
+ if (isUsableNativeBinary(candidate, version)) {
206
207
  return candidate;
207
208
  }
208
209
  }
@@ -231,7 +232,7 @@ function whichAttn() {
231
232
  * `<package>/bin/attn.js`; handing off to that would re-enter this script
232
233
  * forever.
233
234
  */
234
- function isUsableNativeBinary(candidate) {
235
+ function isUsableNativeBinary(candidate, version) {
235
236
  if (!candidate) return false;
236
237
  try {
237
238
  accessSync(candidate, fsConstants.X_OK);
@@ -269,7 +270,42 @@ function isUsableNativeBinary(candidate) {
269
270
  if (resolved.startsWith(binDirReal + "/") || resolved === binDirReal) {
270
271
  return false;
271
272
  }
272
- return true;
273
+ return isExpectedBinaryVersion(resolved, version);
274
+ }
275
+
276
+ function isExpectedBinaryVersion(binary, version) {
277
+ if (!binary || !existsSync(binary)) {
278
+ return false;
279
+ }
280
+ if (process.env.ATTN_SKIP_NATIVE_VERSION_CHECK === "1") {
281
+ return true;
282
+ }
283
+ const actual = readBinaryVersion(binary);
284
+ const ok = actual === version;
285
+ if (!ok && process.env.ATTN_DEBUG_LAUNCHER) {
286
+ const label = actual ? `version ${actual}` : "unknown version";
287
+ console.error(`attn: skipping ${binary} (${label}; need ${version})`);
288
+ }
289
+ return ok;
290
+ }
291
+
292
+ function readBinaryVersion(binary) {
293
+ const result = spawnSync(binary, ["--version"], {
294
+ encoding: "utf8",
295
+ timeout: 2000,
296
+ });
297
+ if (result.error || result.status !== 0) {
298
+ return null;
299
+ }
300
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`;
301
+ const match = output.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
302
+ return match ? match[0] : null;
303
+ }
304
+
305
+ function safeRemove(path) {
306
+ if (existsSync(path)) {
307
+ rmSync(path, { recursive: true, force: true });
308
+ }
273
309
  }
274
310
 
275
311
  function ensureCurrentAppLink(appPath) {
@@ -389,6 +425,9 @@ if [ ! -e "$APP_LINK" ]; then
389
425
  fi
390
426
  BINARY="$APP_LINK/Contents/MacOS/attn"
391
427
  HEADLESS=0
428
+ if [ "\${1:-}" = "review" ]; then
429
+ HEADLESS=1
430
+ fi
392
431
  for arg in "$@"; do
393
432
  case "$arg" in
394
433
  --status|--json|--check|--info|--eval|--click|--wait-for|--query|--fill)
@@ -487,7 +526,7 @@ function resolvePathArgs(args) {
487
526
  }
488
527
 
489
528
  function isHeadlessInvocation(args) {
490
- return args.some((arg) => HEADLESS_FLAGS.has(arg));
529
+ return args[0] === "review" || args.some((arg) => HEADLESS_FLAGS.has(arg));
491
530
  }
492
531
 
493
532
  function run(cmd, args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attnmd",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "A beautiful markdown viewer that launches from the CLI",
5
5
  "license": "MIT",
6
6
  "repository": {