attnmd 0.3.5 → 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.
- package/README.md +135 -41
- package/bin/attn.js +167 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,46 +1,111 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<h1 align="center">attn</h1>
|
|
3
3
|
<p align="center">
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Read markdown beautifully. Review it together.<br>
|
|
5
|
+
Native window. End-to-end encrypted collaboration. No Electron.
|
|
6
6
|
</p>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
+
<a href="#collaboration">Collaboration</a> ·
|
|
10
11
|
<a href="#install">Install</a> ·
|
|
11
|
-
<a href="
|
|
12
|
-
<a href="
|
|
12
|
+
<a href="#usage">Usage</a> ·
|
|
13
|
+
<a href="https://github.com/lightsofapollo/attn/issues">Issues</a>
|
|
13
14
|
</p>
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
<p align="center">
|
|
18
|
-
<
|
|
19
|
+
<picture>
|
|
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
|
+
</picture>
|
|
19
23
|
</p>
|
|
20
24
|
|
|
21
25
|
```bash
|
|
22
26
|
attn .
|
|
23
27
|
```
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
attn opens your markdown in a native desktop window with a file tree, tabs,
|
|
30
|
+
live reload, and a real editor. When a document needs feedback, hit Share and
|
|
31
|
+
send an invite link. Reviewers can comment, suggest edits, and co-edit from the
|
|
32
|
+
same markdown surface.
|
|
33
|
+
|
|
34
|
+
No account. No browser tab. No Electron runtime. The review relay only sees
|
|
35
|
+
encrypted bytes.
|
|
26
36
|
|
|
27
37
|
## Why attn?
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
Markdown usually lives in your repo, but review often moves somewhere else:
|
|
40
|
+
screenshots, pasted docs, stale exported PDFs, or a SaaS editor with a copy of
|
|
41
|
+
your source.
|
|
42
|
+
|
|
43
|
+
attn keeps the source of truth local and makes review a layer over the file you
|
|
44
|
+
already have. You get a focused reader, a capable editor, and an encrypted
|
|
45
|
+
review room without changing how your project is organized.
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
- **Beautiful markdown rendering** - readable line length, careful typography,
|
|
50
|
+
syntax-highlighted code, tables, task lists, math, and Mermaid diagrams.
|
|
51
|
+
- **Live reload** - save in Vim, VS Code, Zed, or any editor and the native
|
|
52
|
+
window updates immediately.
|
|
53
|
+
- **Built-in editor** - toggle a ProseMirror editor with `Cmd+E` when you want
|
|
54
|
+
to edit in place.
|
|
55
|
+
- **Interactive checkboxes** - click a `- [ ]` task and attn writes the change
|
|
56
|
+
back to the file.
|
|
57
|
+
- **Project file tree** - browse folders, lazy-load large repos, and jump to
|
|
58
|
+
files with fuzzy search.
|
|
59
|
+
- **Tabs and project switching** - keep multiple files open and move between
|
|
60
|
+
remembered workspaces.
|
|
61
|
+
- **Native media preview** - images, video, and audio open alongside markdown.
|
|
62
|
+
- **Light and dark themes** - paper-and-ink light mode plus a low-glare dark
|
|
63
|
+
mode.
|
|
64
|
+
- **Single-instance CLI** - run `attn` from many terminals; one daemon receives
|
|
65
|
+
new files as tabs.
|
|
66
|
+
|
|
67
|
+
## Collaboration
|
|
68
|
+
|
|
69
|
+
attn's review flow is built for markdown that should stay private and local.
|
|
70
|
+
|
|
71
|
+
- **Share in one click** - use the Share button, the breadcrumb/share menu, or
|
|
72
|
+
`Cmd+Shift+S` to create a review room for the active markdown file.
|
|
73
|
+
- **Encrypted invite links** - send an `attn://review/...#key=...` link or the
|
|
74
|
+
generated `npx attnmd ...` command. The room key is in the invite fragment,
|
|
75
|
+
not on the relay.
|
|
76
|
+
- **Comments and suggestions** - reviewers anchor feedback to selected text;
|
|
77
|
+
suggestions appear beside the document with Accept/Reject actions.
|
|
78
|
+
- **Live cursors and co-editing** - connected reviewers show up in the document
|
|
79
|
+
and sidebar so you can see where people are reading or editing.
|
|
80
|
+
- **Hybrid transport** - attn uses direct peer-to-peer collaboration when it can
|
|
81
|
+
and falls back to the encrypted relay when it cannot.
|
|
82
|
+
- **Folder share** - share a directory to publish snapshots for every markdown
|
|
83
|
+
file under it; newly added markdown files are picked up by the watcher.
|
|
84
|
+
|
|
85
|
+
Owner flow:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
attn path/to/docs
|
|
89
|
+
# In the app: Share, or Cmd+Shift+S
|
|
90
|
+
```
|
|
30
91
|
|
|
31
|
-
|
|
92
|
+
CLI share flow:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
attn review share path/to/docs
|
|
96
|
+
```
|
|
32
97
|
|
|
33
|
-
|
|
98
|
+
Reviewer flow:
|
|
34
99
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
100
|
+
```bash
|
|
101
|
+
attn review join 'attn://review/<room-id>#key=<secret>'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For headless reviewers or agents:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
attn review join 'attn://review/<room-id>#key=<secret>' --as-agent reviewer
|
|
108
|
+
```
|
|
44
109
|
|
|
45
110
|
## Install
|
|
46
111
|
|
|
@@ -58,20 +123,15 @@ npx attnmd
|
|
|
58
123
|
npm install -g attnmd && attn
|
|
59
124
|
```
|
|
60
125
|
|
|
61
|
-
### From source
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
cargo install attn
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### From source (git)
|
|
126
|
+
### From source
|
|
68
127
|
|
|
69
128
|
```bash
|
|
70
129
|
git clone https://github.com/lightsofapollo/attn.git
|
|
71
|
-
cd attn
|
|
130
|
+
cd attn
|
|
131
|
+
cargo install --path .
|
|
72
132
|
```
|
|
73
133
|
|
|
74
|
-
Requires Rust 1.85+.
|
|
134
|
+
Requires Rust 1.85+. npm installs require Node 18+.
|
|
75
135
|
|
|
76
136
|
## Usage
|
|
77
137
|
|
|
@@ -80,17 +140,28 @@ attn # open current directory
|
|
|
80
140
|
attn README.md # open a file
|
|
81
141
|
attn ~/projects/myapp # open a project
|
|
82
142
|
attn --dark # force dark mode
|
|
83
|
-
attn --status todo.md # print task progress
|
|
143
|
+
attn --status todo.md # print task progress, e.g. "3/5 tasks complete"
|
|
84
144
|
attn --json spec.md # dump document structure as JSON
|
|
85
145
|
```
|
|
86
146
|
|
|
147
|
+
### Review CLI
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
attn review share docs/ # share a file or folder
|
|
151
|
+
attn review join 'attn://review/...' # open/join through the app
|
|
152
|
+
attn review join 'attn://review/...' --as-agent reviewer
|
|
153
|
+
attn review list-agents
|
|
154
|
+
attn review whoami
|
|
155
|
+
```
|
|
156
|
+
|
|
87
157
|
### Keyboard shortcuts
|
|
88
158
|
|
|
89
159
|
| Shortcut | Action |
|
|
90
160
|
|---|---|
|
|
91
161
|
| `Cmd+P` | Fuzzy file search |
|
|
92
162
|
| `Cmd+E` | Toggle editor |
|
|
93
|
-
| `Cmd+F` | Find
|
|
163
|
+
| `Cmd+F` | Find and replace |
|
|
164
|
+
| `Cmd+Shift+S` | Share for review |
|
|
94
165
|
| `Cmd+;` | Switch project |
|
|
95
166
|
| `Cmd+W` | Close tab |
|
|
96
167
|
| `Cmd+Tab` / `Cmd+Shift+Tab` | Navigate tabs |
|
|
@@ -100,37 +171,60 @@ attn --json spec.md # dump document structure as JSON
|
|
|
100
171
|
|
|
101
172
|
## How it works
|
|
102
173
|
|
|
103
|
-
The Svelte 5 frontend is compiled by Vite and
|
|
174
|
+
The Svelte 5 frontend is compiled by Vite and embedded into the Rust binary at
|
|
175
|
+
build time. There is no bundled web server and no extracted asset directory at
|
|
176
|
+
runtime.
|
|
177
|
+
|
|
178
|
+
On first launch, attn forks a daemon to the background. The daemon opens a
|
|
179
|
+
native window via [wry](https://github.com/tauri-apps/wry), watches your files,
|
|
180
|
+
and listens on a Unix socket. Later `attn` calls connect to that socket and
|
|
181
|
+
open new tabs in the existing window. If the binary changes after a rebuild,
|
|
182
|
+
the old daemon is replaced automatically.
|
|
104
183
|
|
|
105
|
-
|
|
184
|
+
Collaboration is layered on top of the local file model. The owner shares a
|
|
185
|
+
snapshot graph and review event log over an end-to-end encrypted room. The
|
|
186
|
+
relay handles discovery, mailbox fallback, and presence transport, but it does
|
|
187
|
+
not receive plaintext markdown or comments.
|
|
106
188
|
|
|
107
189
|
```
|
|
108
190
|
src/
|
|
109
191
|
main.rs CLI, native window, keyboard shortcuts
|
|
110
192
|
daemon.rs Unix socket IPC, single-instance daemon
|
|
111
193
|
watcher.rs File system monitoring with debouncing
|
|
112
|
-
markdown.rs Structure extraction
|
|
113
|
-
ipc.rs Webview
|
|
114
|
-
files.rs File tree
|
|
194
|
+
markdown.rs Structure extraction
|
|
195
|
+
ipc.rs Webview <-> Rust messaging
|
|
196
|
+
files.rs File tree and media type detection
|
|
115
197
|
projects.rs Project registry
|
|
198
|
+
review/ Encrypted review rooms, anchors, transport, apply flow
|
|
116
199
|
|
|
117
200
|
web/src/ Svelte 5 frontend
|
|
118
|
-
|
|
201
|
+
relay/ Cloudflare Worker relay for encrypted review traffic
|
|
202
|
+
site/ Public marketing site
|
|
119
203
|
```
|
|
120
204
|
|
|
121
|
-
##
|
|
205
|
+
## Development
|
|
122
206
|
|
|
123
207
|
```bash
|
|
124
208
|
task dev # Vite HMR + Rust in foreground
|
|
125
|
-
task dev ATTN_PATH=path/to/file.md #
|
|
209
|
+
task dev ATTN_PATH=path/to/file.md # open a specific file
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Builds:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
scripts/build.sh # debug build
|
|
216
|
+
scripts/build.sh release # release build with devtools/screenshots
|
|
217
|
+
scripts/build.sh prod # production release build
|
|
126
218
|
```
|
|
127
219
|
|
|
128
|
-
|
|
220
|
+
Useful gates:
|
|
129
221
|
|
|
130
222
|
```bash
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
223
|
+
npm run check --prefix web
|
|
224
|
+
npm test --prefix web
|
|
225
|
+
cargo test
|
|
226
|
+
npm test --prefix relay
|
|
227
|
+
npm run build --prefix site
|
|
134
228
|
```
|
|
135
229
|
|
|
136
230
|
## License
|
package/bin/attn.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
+
accessSync,
|
|
4
5
|
chmodSync,
|
|
6
|
+
constants: fsConstants,
|
|
5
7
|
createWriteStream,
|
|
6
8
|
existsSync,
|
|
7
9
|
mkdirSync,
|
|
8
10
|
readFileSync,
|
|
11
|
+
realpathSync,
|
|
9
12
|
renameSync,
|
|
10
13
|
rmSync,
|
|
11
14
|
symlinkSync,
|
|
@@ -59,6 +62,18 @@ async function main() {
|
|
|
59
62
|
const version = packageJson.version;
|
|
60
63
|
const headless = isHeadlessInvocation(args);
|
|
61
64
|
|
|
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);
|
|
69
|
+
if (nativeBinary) {
|
|
70
|
+
if (process.env.ATTN_DEBUG_LAUNCHER) {
|
|
71
|
+
console.error(`attn: using native binary ${nativeBinary}`);
|
|
72
|
+
}
|
|
73
|
+
run(nativeBinary, args);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
let appPath = null;
|
|
63
78
|
if (process.platform === "darwin") {
|
|
64
79
|
try {
|
|
@@ -69,7 +84,8 @@ async function main() {
|
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
if (!appPath) {
|
|
72
|
-
if (!
|
|
87
|
+
if (!isExpectedBinaryVersion(runtimeBinaryPath, version)) {
|
|
88
|
+
safeRemove(runtimeBinaryPath);
|
|
73
89
|
await ensureRuntimeBinary(version);
|
|
74
90
|
}
|
|
75
91
|
if (!existsSync(runtimeBinaryPath)) {
|
|
@@ -99,13 +115,16 @@ async function main() {
|
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
async function resolveAppPath(version) {
|
|
102
|
-
const globalApp = findGlobalAppInstall();
|
|
118
|
+
const globalApp = findGlobalAppInstall(version);
|
|
103
119
|
if (globalApp) {
|
|
104
120
|
return globalApp;
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
const managedVersionApp = join(managedAppsRoot, version, "attn.app");
|
|
108
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
existsSync(managedVersionApp) &&
|
|
126
|
+
isExpectedBinaryVersion(join(managedVersionApp, "Contents", "MacOS", "attn"), version)
|
|
127
|
+
) {
|
|
109
128
|
ensureCurrentAppLink(managedVersionApp);
|
|
110
129
|
return managedVersionApp;
|
|
111
130
|
}
|
|
@@ -137,19 +156,158 @@ async function ensureRuntimeBinary(version) {
|
|
|
137
156
|
console.error(`attn: installed runtime binary ${runtimeBinaryPath}`);
|
|
138
157
|
}
|
|
139
158
|
|
|
140
|
-
function findGlobalAppInstall() {
|
|
159
|
+
function findGlobalAppInstall(version) {
|
|
141
160
|
const candidates = [
|
|
142
161
|
"/Applications/attn.app",
|
|
143
162
|
join(userHome, "Applications", "attn.app"),
|
|
144
163
|
];
|
|
145
164
|
for (const candidate of candidates) {
|
|
146
|
-
|
|
165
|
+
const binary = join(candidate, "Contents", "MacOS", "attn");
|
|
166
|
+
if (existsSync(candidate) && isExpectedBinaryVersion(binary, version)) {
|
|
147
167
|
return candidate;
|
|
148
168
|
}
|
|
149
169
|
}
|
|
150
170
|
return null;
|
|
151
171
|
}
|
|
152
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Locate an already-installed `attn` we can hand off to instead of
|
|
175
|
+
* downloading. Priority:
|
|
176
|
+
* 1. $ATTN_BIN explicit override
|
|
177
|
+
* 2. `attn` on PATH (via `command -v`)
|
|
178
|
+
* 3. The ~/.local/bin alias this launcher installs on first run
|
|
179
|
+
* 4. Common manual / package-manager install dirs (Homebrew, cargo)
|
|
180
|
+
*
|
|
181
|
+
* Each candidate is validated by `isUsableNativeBinary`, which rejects
|
|
182
|
+
* anything inside this npm package (so a global `npm i -g attn` symlink,
|
|
183
|
+
* whose realpath points back at bin/attn.js, can't cause infinite
|
|
184
|
+
* recursion).
|
|
185
|
+
*/
|
|
186
|
+
function findNativeBinary(version) {
|
|
187
|
+
const candidates = [];
|
|
188
|
+
|
|
189
|
+
if (process.env.ATTN_BIN) {
|
|
190
|
+
candidates.push(process.env.ATTN_BIN);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const onPath = whichAttn();
|
|
194
|
+
if (onPath) {
|
|
195
|
+
candidates.push(onPath);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
candidates.push(
|
|
199
|
+
installLinkPath, // ~/.local/bin/attn — the alias we install ourselves
|
|
200
|
+
"/opt/homebrew/bin/attn",
|
|
201
|
+
"/usr/local/bin/attn",
|
|
202
|
+
join(userHome, ".cargo", "bin", "attn"),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
for (const candidate of candidates) {
|
|
206
|
+
if (isUsableNativeBinary(candidate, version)) {
|
|
207
|
+
return candidate;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Resolve `attn` on PATH without throwing. Returns null when not found. */
|
|
214
|
+
function whichAttn() {
|
|
215
|
+
const probe = spawnSync(
|
|
216
|
+
process.platform === "win32" ? "where" : "command",
|
|
217
|
+
process.platform === "win32" ? ["attn"] : ["-v", "attn"],
|
|
218
|
+
{ encoding: "utf8", shell: process.platform !== "win32" },
|
|
219
|
+
);
|
|
220
|
+
if (probe.status !== 0 || !probe.stdout) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const first = probe.stdout.split("\n").map((l) => l.trim()).find(Boolean);
|
|
224
|
+
return first || null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* True when `candidate` is an executable we can safely exec as a real
|
|
229
|
+
* `attn` — i.e. it exists, is executable, and (after symlink resolution)
|
|
230
|
+
* does NOT live inside this npm package. The last check is the recursion
|
|
231
|
+
* guard: a global `npm i -g attn` makes `attn` on PATH a symlink to
|
|
232
|
+
* `<package>/bin/attn.js`; handing off to that would re-enter this script
|
|
233
|
+
* forever.
|
|
234
|
+
*/
|
|
235
|
+
function isUsableNativeBinary(candidate, version) {
|
|
236
|
+
if (!candidate) return false;
|
|
237
|
+
try {
|
|
238
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
239
|
+
} catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
let resolved;
|
|
243
|
+
try {
|
|
244
|
+
resolved = realpathSync(candidate);
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
// Recursion guard: never hand off to another copy of THIS launcher.
|
|
249
|
+
// Two ways that happens:
|
|
250
|
+
// 1. The candidate resolves to this exact file (a symlink to it).
|
|
251
|
+
// 2. A global `npm i -g attn` makes `attn` on PATH a symlink to some
|
|
252
|
+
// package's `bin/attn.js`; any `.js` is a node launcher, not a
|
|
253
|
+
// native binary, so reject it.
|
|
254
|
+
// A compiled binary under this repo's `target/` (dev builds) is fine —
|
|
255
|
+
// it can't re-enter this script — so we deliberately DON'T reject the
|
|
256
|
+
// whole package dir, only the launcher itself + the `bin/` dir + `.js`.
|
|
257
|
+
try {
|
|
258
|
+
if (resolved === realpathSync(__filename)) return false;
|
|
259
|
+
} catch {
|
|
260
|
+
/* __filename always resolves; ignore */
|
|
261
|
+
}
|
|
262
|
+
if (resolved.endsWith(".js")) return false;
|
|
263
|
+
const binDirReal = (() => {
|
|
264
|
+
try {
|
|
265
|
+
return realpathSync(join(packageDir, "bin"));
|
|
266
|
+
} catch {
|
|
267
|
+
return join(packageDir, "bin");
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
270
|
+
if (resolved.startsWith(binDirReal + "/") || resolved === binDirReal) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
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
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
153
311
|
function ensureCurrentAppLink(appPath) {
|
|
154
312
|
mkdirSync(managedRoot, { recursive: true });
|
|
155
313
|
try {
|
|
@@ -267,6 +425,9 @@ if [ ! -e "$APP_LINK" ]; then
|
|
|
267
425
|
fi
|
|
268
426
|
BINARY="$APP_LINK/Contents/MacOS/attn"
|
|
269
427
|
HEADLESS=0
|
|
428
|
+
if [ "\${1:-}" = "review" ]; then
|
|
429
|
+
HEADLESS=1
|
|
430
|
+
fi
|
|
270
431
|
for arg in "$@"; do
|
|
271
432
|
case "$arg" in
|
|
272
433
|
--status|--json|--check|--info|--eval|--click|--wait-for|--query|--fill)
|
|
@@ -365,7 +526,7 @@ function resolvePathArgs(args) {
|
|
|
365
526
|
}
|
|
366
527
|
|
|
367
528
|
function isHeadlessInvocation(args) {
|
|
368
|
-
return args.some((arg) => HEADLESS_FLAGS.has(arg));
|
|
529
|
+
return args[0] === "review" || args.some((arg) => HEADLESS_FLAGS.has(arg));
|
|
369
530
|
}
|
|
370
531
|
|
|
371
532
|
function run(cmd, args) {
|