fss-link 1.6.12 → 1.7.6

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/docs/README.md CHANGED
@@ -171,14 +171,15 @@ Reference files directly in your message with `@`:
171
171
 
172
172
  ## Testing
173
173
 
174
- **Do not run `npm test` from the project root.** It launches vitest across all workspaces in parallel (256+ test files) and will consume all available system memory. See [VITEST-SAFETY.md](../VITEST-SAFETY.md) for safe testing methods and emergency kill procedures.
174
+ Full workspace test suites are safe with the `**/dist/**` excludes in all vitest configs. The historical OOM crash was caused by vitest picking up duplicate compiled `.js` test files from `packages/*/dist/`; this has been fixed. See [VITEST-SAFETY.md](../VITEST-SAFETY.md) for full guidance and emergency kill procedures.
175
175
 
176
176
  ```bash
177
- # Safe: run individual test files
178
- npx vitest run packages/cli/src/config/database.test.ts
179
-
180
- # Safe: run one workspace
177
+ # Run all tests for a workspace
181
178
  npm run test --workspace=packages/cli
179
+ npm run test --workspace=packages/core
180
+
181
+ # Run a single test file
182
+ npx vitest run packages/cli/src/config/database.test.ts
182
183
  ```
183
184
 
184
185
  ## Documentation
package/docs/TOOLS.md CHANGED
@@ -148,6 +148,10 @@ Fetches a URL and returns its content. Handles HTML (extracted as readable text)
148
148
  | Parameter | Type | Description |
149
149
  |-----------|------|-------------|
150
150
  | `url` | string | URL to fetch |
151
+ | `prompt` | string | Instructions for what to extract from the page |
152
+ | `include_images` | boolean | When true, extract and return image URLs, alt text, and context found on the page |
153
+
154
+ For persistent research with file output, use `web_scraper` instead.
151
155
 
152
156
  ### web_search
153
157
 
@@ -156,9 +160,12 @@ Searches the web using a search engine API.
156
160
  | Parameter | Type | Description |
157
161
  |-----------|------|-------------|
158
162
  | `query` | string | Search query |
159
- | `limit` | integer | Maximum results (optional) |
163
+ | `max_results` | integer | Maximum results to return (1–20, default 5) |
164
+ | `search_depth` | string | `"basic"` (fast, default) or `"advanced"` (AI-powered, costs more) |
165
+
166
+ Requires a search API key (Tavily or Brave Search — configured automatically, Tavily preferred).
160
167
 
161
- Requires a search API key (Tavily or similar) configured via `--tavily-api-key` or environment variable.
168
+ For persistent research with file output, use `web_scraper` instead.
162
169
 
163
170
  ### fetch_image
164
171
 
@@ -185,16 +192,182 @@ Specialized parsers for structured documents. These extract text, tables, metada
185
192
 
186
193
  | Tool | Formats | What It Extracts |
187
194
  |------|---------|-----------------|
188
- | `read_pdf` | PDF | Text, page structure, metadata |
189
- | `read_excel` | XLSX, XLS, CSV | Sheets, cells, formulas, structure |
190
- | `read_word` | DOCX | Text, headings, tables, styles |
191
- | `read_email` | EML, MSG | Headers, body, attachments metadata |
195
+ | `pdf_parser` | PDF | Text, page structure, metadata, visual page rendering via `view_page` |
196
+ | `excel_parser` | XLSX, XLS, CSV | Sheets, cells, formulas, structure, multi-sheet processing |
197
+ | `word_parser` | DOCX, DOC, MD | Text, headings, tables, styles, embedded images |
198
+ | `email_parser` | EML, MSG, MBOX | Headers, body, attachments metadata, multi-message .mbox |
192
199
  | `read_data` | CSV, JSON, YAML, XML, TOML | Parsed data with schema inference |
193
200
 
201
+ **pdf_parser extras:**
202
+ - `mode="preview"` — light deterministic overview (page count, metadata, first 40 lines) without dumping body text
203
+ - `view_page=N` — renders a specific page as an image for visual inspection (sees layout, tables, diagrams, signatures)
204
+ - `pages=[3, 5, 7]` or `page_range={start, end}` — extract specific pages with boundary detection confidence labels
205
+
206
+ **excel_parser extras:**
207
+ - `read_all_sheets` — read all sheets or just the first one
208
+ - `range="A1:C10"` — extract a specific cell range
209
+ - `include_formatting` — include cell formatting information
210
+ - `preserve_formulas` — preserve Excel formulas in output
211
+
212
+ **word_parser extras:**
213
+ - `extract_images` — extract embedded images from the document
214
+ - `preserve_formatting` — preserve document formatting
215
+
216
+ **email_parser extras:**
217
+ - `max_messages` — maximum number of messages to parse from .mbox files (default: 50, max: 500)
218
+ - `prefer_html` — prefer HTML body over plain text when both are present
219
+
220
+ ---
221
+
222
+ ## Audio
223
+
224
+ ### read_audio
225
+
226
+ Read an audio file and return its metadata and/or a segment-level transcript.
227
+
228
+ | Parameter | Type | Description |
229
+ |-----------|------|-------------|
230
+ | `absolute_path` | string | Absolute path to the audio file (mp3, wav, flac, ogg, m4a, etc.) |
231
+ | `mode` | string | `metadata` for format/tag info only, `transcribe` for transcript only, or `full` (default) for both. |
232
+
233
+ - Extracts duration, sample rate, channels, codec, bitrate, and embedded tags (title, artist, album, year, genre)
234
+ - Transcription requires a Whisper-compatible server at `FSS_WHISPER_URL` (default `localhost:11453`, timeout 120s via `FSS_WHISPER_TIMEOUT`)
235
+ - Returns segment-level transcript with timestamps and derived silence regions (gaps ≥ 0.5s)
236
+ - Supports abort/cancellation during transcription
237
+
238
+ ---
239
+
240
+ ## Image Generation & Editing
241
+
242
+ ### generate_image
243
+
244
+ Create new images from text descriptions via ComfyUI (FLUX.2 Klein). Single or batch mode. Upscaling is applied by generating at full resolution.
245
+
246
+ | Parameter | Type | Description |
247
+ |-----------|------|-------------|
248
+ | `prompt` | string | Image generation prompt (single mode). Describe the image you want in detail. |
249
+ | `batch_content` | string | Markdown batch content (batch mode). Use `###` headings as prompts, optional bullet overrides below each (seed, steps, negative_prompt, etc). |
250
+ | `negative_prompt` | string | What to avoid in the image (single mode only). |
251
+ | `width` | integer | Base width before upscaling (default: 1024). |
252
+ | `height` | integer | Base height before upscaling (default: 1024). |
253
+ | `steps` | integer | Sampling steps (default: 20 for 9b model). Higher = more detail, slower. |
254
+ | `cfg_scale` | number | CFG scale (default: 1.0 for FLUX.2). Lower = more creative, higher = follows prompt more strictly. |
255
+ | `seed` | integer | Random seed for reproducibility (-1 = random). |
256
+ | `model` | string | Workflow preset: `9b` (FLUX.2 Klein 9B, best quality), `4b` (FLUX.2 Klein 4B, faster), `flux-q8` (quantized 8-bit). Default: `9b`. |
257
+ | `output_dir` | string | Output directory. Defaults to current working directory. |
258
+ | `output_filename` | string | Custom filename for the output image. Auto-generated if omitted. |
259
+
260
+ ### edit_image
261
+
262
+ Edit an existing image using ComfyUI Qwen-Edit workflow with instruction-based modifications and optional reference images. ALWAYS creates a NEW output file — the original image is NEVER modified or overwritten.
263
+
264
+ | Parameter | Type | Description |
265
+ |-----------|------|-------------|
266
+ | `input_image` | string | Input image file path to edit (required). |
267
+ | `prompt` | string | Edit instruction describing what to change (required). Be specific about what to keep and what to change. |
268
+ | `reference_images` | array | Optional reference image paths for style or character consistency. Can be repeated up to 3×. |
269
+ | `steps` | integer | Sampling steps (default: 4 for Lightning; 8 for more detail). |
270
+ | `cfg` | number | CFG scale (default: 1.0 — Lightning works at 1.0). |
271
+ | `seed` | integer | Random seed for reproducibility (-1 = random). |
272
+ | `output_dir` | string | Output directory for the new edited image. Defaults to current working directory. |
273
+ | `output_filename` | string | Custom filename for the output image. Auto-generated if omitted. |
274
+
275
+ ---
276
+
277
+ ## Web Scraping
278
+
279
+ ### web_scraper
280
+
281
+ Scrape web pages, search the web, or crawl sites. Saves extracted content to files.
282
+
283
+ | Parameter | Type | Description |
284
+ |-----------|------|-------------|
285
+ | `operation` | string | Type of operation: `"scrape"` (direct URLs), `"crawl"` (recursive), `"search"` (search-driven), `"summarise"` (synthesise previous scrapes with LLM). |
286
+ | `sources` | array | List of URLs or file paths to process (required for scrape/crawl operations). |
287
+ | `searchQuery` | string | Search query for search-driven scraping (required for search operation). |
288
+ | `outputDir` | string | Directory where scraped content will be saved (default: `~/.fss-link/scraper/`). |
289
+ | `maxResults` | number | Maximum number of search results to process (for search operation, default: 10, max: 50). |
290
+ | `maxDepth` | number | Maximum crawl depth for "crawl" operation (default: 2, max: 10). |
291
+ | `maxPages` | number | Maximum pages to crawl (budget cap, default: 20, max: 100). |
292
+ | `topic` | string | Filter results by topic relevance (keyword matching). Pages not relevant to this topic are dropped. |
293
+ | `minQuality` | number | Minimum content quality score 0–1 (default: 0.3). Drops empty/boilerplate pages. |
294
+ | `collectMedia` | boolean | Collect images from scraped pages. Downloads to media/ subfolder. |
295
+ | `framing` | string | Perspective/lens for synthesis (summarise operation only). Not a question — a framing directive that guides how sources are synthesised. |
296
+
297
+ - Supports topic filtering, quality thresholding, search result ranking
298
+ - Media collection (opt-in) downloads images from scraped pages
299
+ - Rate limiting, robots.txt compliance built in
300
+ - Uses Brave/Tavily search APIs when available
301
+
302
+ ---
303
+
304
+ ## Structured Data Inspection
305
+
306
+ ### inspect_structured
307
+
308
+ Inspect structured data files (JSON, JSONL/NDJSON, YAML, TOML, XML, CSV, TSV). Returns the file's shape — keys, types, array lengths, depth — with addressable paths, instead of dumping raw content. Far cheaper than `read_file` for large structured files where syntax noise overwhelms the actual data.
309
+
310
+ | Parameter | Type | Description |
311
+ |-----------|------|-------------|
312
+ | `absolute_path` | string | Absolute path to the structured data file. |
313
+ | `mode` | string | `"overview"` (default) returns the structural shape with addressable paths. `"extract"` returns one subtree, located by `path`. |
314
+ | `path` | string | Path to extract (extract mode only). Dot/bracket notation, e.g. `"Properties.Children[0]"` or `'items[0]["name"]'`. |
315
+ | `max_depth` | integer | Overview only — descend at most this many levels (default 4). |
316
+ | `max_array_samples` | integer | Overview only — number of array elements to expand inline (default 3). |
317
+
318
+ Supported formats: `.json`, `.jsonl`, `.ndjson`, `.yaml`, `.yml`, `.toml`, `.xml`, `.csv`, `.tsv`
319
+
194
320
  ---
195
321
 
196
322
  ## Agent Utilities
197
323
 
324
+ ### wait_for
325
+
326
+ Wait for a long-running background process to complete by polling for one of two completion signals: a file appearing on disk, or a regex matching in a log file.
327
+
328
+ | Parameter | Type | Description |
329
+ |-----------|------|-------------|
330
+ | `wait_for_file` | object | Wait for a file to appear at a given absolute path. Set `stable_for_seconds` to require the file's size AND modification time to stop changing before counting as complete. |
331
+ | `wait_for_log_match` | object | Wait for a regex to match somewhere in the tail of a log file. Use when the output file appears mid-process or has unpredictable size. |
332
+ | `max_total_seconds` | number | Hard cap on total wait time, in seconds. Minimum 2, maximum 3600 (1 hour). |
333
+ | `interval_seconds` | number | Seconds between checks. Default 5, minimum 2. |
334
+ | `progress_pattern` | string | Optional regex for live progress display. If not set, the tool auto-detects common patterns (segments, percent, step counter, timing). |
335
+ | `progress_log_path` | string | Optional path for progress tracking. Defaults to `wait_for_log_match.log_path`. |
336
+
337
+ **`wait_for_file` properties:**
338
+
339
+ | Property | Type | Description |
340
+ |----------|------|-------------|
341
+ | `path` | string | Absolute path to the target file. |
342
+ | `stable_for_seconds` | number | File must exist and its size AND mtime must be unchanged for this many seconds. |
343
+
344
+ **`wait_for_log_match` properties:**
345
+
346
+ | Property | Type | Description |
347
+ |----------|------|-------------|
348
+ | `log_path` | string | Absolute path to the log file. |
349
+ | `pattern` | string | JavaScript regex matched against the last 64 KB of the log. Anchor tightly with `^` and `$`. |
350
+ | `min_matches` | number | Minimum matches required before declaring done. Default 1. Use >1 for noisy logs. |
351
+ | `anchored_to_end` | number | Require the match to appear in the last N lines. Ensures completion signal is at the END of output. |
352
+
353
+ **Auto-progress detection:** When `progress_pattern` is not set, the tool auto-detects patterns from the log: segment counters (`158/162 segments`), percentages (`75%`), step counters (`Step 15/20`), timing (`12.3s / 45s`), and generic counters (`42 of 100`).
354
+
355
+ **ETA estimation:** The tool tracks progress snapshots and displays estimated time remaining (e.g., `~3m 12s remaining`) based on the current progress rate.
356
+
357
+ **Progress delta:** Shows how progress changed since last check (e.g., `158/162 → 159/162`).
358
+
359
+ **Example:** Wait for a build log to show "Build completed successfully" in the last 5 lines:
360
+
361
+ ```
362
+ wait_for_log_match:
363
+ log_path: /path/to/build.log
364
+ pattern: "^Build completed successfully$"
365
+ min_matches: 1
366
+ anchored_to_end: 5
367
+ max_total_seconds: 300
368
+ interval_seconds: 5
369
+ ```
370
+
198
371
  ### workplan_update (formerly `todo_write`)
199
372
 
200
373
  Manages a structured work plan for the current session. Helps the agent track multi-step work.
@@ -214,6 +387,33 @@ Usage guidelines:
214
387
 
215
388
  Reads and writes to the agent's persistent memory system. Stores user preferences, project context, and learned patterns across sessions.
216
389
 
390
+ ### enter_worktree
391
+
392
+ Creates an isolated git worktree at `<projectRoot>/.fss-link/worktrees/<slug>` and returns its absolute path so subsequent file edits, shell commands, and other tools can operate inside it.
393
+
394
+ | Parameter | Type | Description |
395
+ |-----------|------|-------------|
396
+ | `name` | string | Optional slug (letters, digits, dot, underscore, hyphen; max 64 chars). Auto-generated when omitted. |
397
+
398
+ - Creates a new branch `<prefix><slug>` based on the current branch
399
+ - The worktree persists across the session until `exit_worktree` is invoked
400
+ - Multiple agents can work in parallel in different worktrees without interfering with each other or the main checkout
401
+ - **Only invoke when the user explicitly asks for a worktree** — not for routine bug fixes or feature work
402
+
403
+ ### exit_worktree
404
+
405
+ Exits a worktree previously created by `enter_worktree`.
406
+
407
+ | Parameter | Type | Description |
408
+ |-----------|------|-------------|
409
+ | `name` | string | Slug of the worktree to exit (must match the name used in `enter_worktree`). |
410
+ | `action` | string | `"keep"` preserves the worktree on disk; `"remove"` deletes it and its branch. |
411
+ | `discard_changes` | boolean | When `action="remove"`, must be true to delete a worktree with uncommitted changes. |
412
+
413
+ - `action='keep'` — preserves the worktree directory and branch on disk so it can be revisited later
414
+ - `action='remove'` — deletes the worktree directory and branch. Refuses to run if the worktree contains uncommitted changes unless `discard_changes: true` is set
415
+ - Only invoke when the user explicitly asks to leave or clean up a worktree
416
+
217
417
  ---
218
418
 
219
419
  ## TTS (Text-to-Speech)
@@ -230,11 +430,31 @@ Synthesizes speech from text using the FSS-TTS server.
230
430
 
231
431
  | Parameter | Type | Description |
232
432
  |-----------|------|-------------|
233
- | `text` | string | Text to speak |
234
- | `voice` | string | Voice name (Bella, Emma, George, Lewis, Sarah, Michael, Adam, Isabella, Nicole) |
433
+ | `text` | string | Text to speak (max 5000 characters) |
434
+ | `voice` | string | Voice personality to use (Bella, Emma, George, Lewis, Sarah, Michael, Adam, Isabella, Nicole). Default: Bella. |
435
+ | `wait_for_playback` | boolean | If true, block until the TTS server has finished playing the audio before returning. Default false (fire-and-forget). Essential in non-interactive (`-p`) mode. |
235
436
 
236
437
  Requires the FSS-TTS server running at `FSS_TTS_URL` (default `localhost:11450`).
237
438
 
439
+ ### tts_narrate
440
+
441
+ Play multiple voice segments sequentially — Bella says the intro, Emma continues, George adds a comment, etc. Each segment plays fully before the next begins. Use for multi-voice explanations, handoffs between speakers, or conversational narration.
442
+
443
+ | Parameter | Type | Description |
444
+ |-----------|------|-------------|
445
+ | `segments` | array | Ordered list of `{voice, text}` objects to play sequentially |
446
+
447
+ Each segment:
448
+
449
+ | Property | Type | Description |
450
+ |----------|------|-------------|
451
+ | `voice` | string | Voice name (Bella, Emma, George, Lewis, Sarah, Michael, Adam, Isabella, Nicole) |
452
+ | `text` | string | Text for this segment (max 1000 chars) |
453
+
454
+ - Minimum 2 segments, maximum 10
455
+ - Each segment plays fully before the next begins
456
+ - Supports abort/cancellation mid-sequence
457
+
238
458
  ---
239
459
 
240
460
  ## MCP (Model Context Protocol)
@@ -264,5 +484,5 @@ All file-modifying tools (edit, write_file) enforce:
264
484
  - **Workspace boundary checks** — can't write outside your project directory
265
485
  - **Diff previews** — see exactly what will change before approving
266
486
  - **Confirmation prompts** — unless in auto_edit or YOLO mode
267
- - **Tool result size limits** — outputs exceeding 25k characters are truncated to prevent context overflow
487
+ - **Tool result size limits** — shell output capped at 32 KB, read-file at 4,000 lines, core tool results at 40K chars to prevent context overflow
268
488
  - **Error messages include the failed path** — so the agent knows exactly what went wrong and doesn't retry blindly
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fss-link",
3
- "version": "1.6.12",
3
+ "version": "1.7.6",
4
4
  "engines": {
5
5
  "node": ">=20.0.0"
6
6
  },
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/FSSCoding/fss-link.git"
14
14
  },
15
15
  "config": {
16
- "sandboxImageUri": "ghcr.io/fsscoding/fss-link:1.6.12"
16
+ "sandboxImageUri": "ghcr.io/fsscoding/fss-link:1.7.5"
17
17
  },
18
18
  "scripts": {
19
19
  "start": "node scripts/start.js",
@@ -44,8 +44,8 @@
44
44
  "format": "prettier --experimental-cli --write .",
45
45
  "typecheck": "npm run typecheck --workspaces --if-present",
46
46
  "preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
47
- "prepare": "npm run bundle",
48
- "postinstall": "node scripts/postinstall-message.js",
47
+ "postinstall": "node scripts/postinstall-message.cjs",
48
+ "prepare": "patch-package || true",
49
49
  "prepare:package": "node scripts/prepare-package.js",
50
50
  "release:version": "node scripts/version.js",
51
51
  "telemetry": "node scripts/telemetry.js",
@@ -71,7 +71,7 @@
71
71
  "scripts/install-linux.sh",
72
72
  "scripts/install-macos.sh",
73
73
  "scripts/install-windows.ps1",
74
- "scripts/postinstall-message.js",
74
+ "scripts/postinstall-message.cjs",
75
75
  "scripts/prebundle-sync-dist.js",
76
76
  "scripts/prepare-package.js",
77
77
  "scripts/sandbox_command.js",
@@ -82,59 +82,83 @@
82
82
  "LICENSE"
83
83
  ],
84
84
  "devDependencies": {
85
- "@types/fs-extra": "^11.0.4",
86
- "@types/marked": "^5.0.2",
87
- "@types/micromatch": "^4.0.9",
88
- "@types/mime-types": "^3.0.1",
89
- "@types/minimatch": "^5.1.2",
90
- "@types/mock-fs": "^4.13.4",
91
- "@types/qrcode-terminal": "^0.12.2",
92
- "@types/shell-quote": "^1.7.5",
93
- "@types/sql.js": "^1.4.9",
94
- "@types/turndown": "^5.0.5",
95
- "@types/jsdom": "^21.1.7",
96
- "@types/mozilla__readability": "^0.4.2",
97
- "@types/update-notifier": "^6.0.8",
98
- "@types/uuid": "^10.0.0",
99
- "@vitest/coverage-v8": "^4.0.0",
85
+ "@types/ink-testing-library": "^1.0.4",
86
+ "@vitest/coverage-v8": "^4.1.8",
100
87
  "concurrently": "^9.2.0",
101
88
  "cross-env": "^7.0.3",
102
- "esbuild": "^0.25.0",
103
89
  "eslint": "^9.24.0",
104
90
  "eslint-config-prettier": "^10.1.2",
105
91
  "eslint-plugin-import": "^2.31.0",
106
92
  "eslint-plugin-license-header": "^0.8.0",
93
+ "eslint-plugin-no-only-tests": "^3.4.0",
107
94
  "eslint-plugin-react": "^7.37.5",
108
95
  "eslint-plugin-react-hooks": "^5.2.0",
96
+ "eslint-plugin-vitest": "^0.5.4",
109
97
  "globals": "^16.0.0",
98
+ "ink": "^6.8.0",
99
+ "ink-testing-library": "^4.0.0",
110
100
  "json": "^11.0.0",
111
101
  "lodash": "^4.17.21",
112
- "memfs": "^4.17.2",
102
+ "memfs": "^4.57.3",
113
103
  "mnemonist": "^0.40.3",
114
104
  "mock-fs": "^5.5.0",
115
- "msw": "^2.10.4",
105
+ "msw": "^2.14.6",
106
+ "patch-package": "^8.0.1",
116
107
  "prettier": "^3.5.3",
108
+ "react": "^19.2.6",
117
109
  "react-devtools-core": "^7.0.1",
110
+ "react-dom": "^19.2.6",
118
111
  "tsx": "^4.20.3",
119
- "typescript-eslint": "^8.30.1",
120
- "vitest": "^4.0.0"
112
+ "typescript": "^5.9.3",
113
+ "typescript-eslint": "^8.30.1"
121
114
  },
122
115
  "dependencies": {
123
116
  "@google/genai": "1.13.0",
124
117
  "@iarna/toml": "^2.2.5",
125
118
  "@modelcontextprotocol/sdk": "^1.15.1",
126
119
  "@mozilla/readability": "^0.6.0",
120
+ "@testing-library/react": "^16.3.2",
121
+ "@types/command-exists": "^1.2.3",
122
+ "@types/dompurify": "^3.0.5",
123
+ "@types/express": "^5.0.3",
124
+ "@types/fs-extra": "^11.0.4",
125
+ "@types/html-to-text": "^9.0.4",
126
+ "@types/js-yaml": "^4.0.9",
127
+ "@types/jsdom": "^21.1.7",
128
+ "@types/marked": "^5.0.2",
129
+ "@types/micromatch": "^4.0.9",
130
+ "@types/mime-types": "^3.0.1",
131
+ "@types/minimatch": "^5.1.2",
132
+ "@types/mock-fs": "^4.13.4",
133
+ "@types/mozilla__readability": "^0.4.2",
134
+ "@types/node": "^25.9.1",
135
+ "@types/pdf-parse": "^1.1.5",
136
+ "@types/picomatch": "^4.0.3",
137
+ "@types/proper-lockfile": "^4.1.4",
138
+ "@types/qrcode-terminal": "^0.12.2",
139
+ "@types/react": "^19.2.15",
140
+ "@types/react-dom": "^19.2.3",
141
+ "@types/shell-quote": "^1.7.5",
142
+ "@types/sql.js": "^1.4.9",
143
+ "@types/turndown": "^5.0.6",
144
+ "@types/update-notifier": "^6.0.8",
145
+ "@types/uuid": "^10.0.0",
146
+ "@types/vscode": "^1.99.0",
147
+ "@types/ws": "^8.5.10",
148
+ "@types/xml2js": "^0.4.14",
149
+ "@types/yargs": "^17.0.33",
150
+ "@types/yauzl": "^2.10.3",
127
151
  "axios": "^1.11.0",
128
152
  "chalk": "^5.3.0",
129
153
  "cheerio": "^1.1.2",
130
154
  "command-exists": "^1.2.9",
131
155
  "diff": "^9.0.0",
132
156
  "dotenv": "^17.1.0",
157
+ "esbuild": "^0.25.0",
133
158
  "exceljs": "^4.4.0",
134
159
  "fs-extra": "^11.3.1",
135
160
  "glob": "^13.0.6",
136
161
  "highlight.js": "^11.11.1",
137
- "ink": "^6.1.1",
138
162
  "ink-big-text": "^2.0.0",
139
163
  "ink-gradient": "^3.0.0",
140
164
  "ink-link": "^4.1.0",
@@ -144,13 +168,13 @@
144
168
  "jsdom": "^27.0.0",
145
169
  "lowlight": "^3.3.0",
146
170
  "mime-types": "^3.0.1",
171
+ "music-metadata": "^11.12.3",
147
172
  "open": "^10.1.2",
148
173
  "p-limit": "^7.1.1",
149
174
  "pdf-to-img": "^6.0.0",
150
175
  "proper-lockfile": "^4.1.2",
176
+ "punycode.js": "^2.3.1",
151
177
  "qrcode-terminal": "^0.12.0",
152
- "react": "^19.2.0",
153
- "react-dom": "^19.2.0",
154
178
  "read-package-up": "^11.0.0",
155
179
  "sharp": "^0.34.4",
156
180
  "shell-quote": "^1.8.3",
@@ -164,21 +188,13 @@
164
188
  "turndown": "^7.2.0",
165
189
  "turndown-plugin-gfm": "^1.0.2",
166
190
  "undici": "^7.10.0",
191
+ "vite": "^8.0.14",
192
+ "vitest": "^4.1.7",
167
193
  "xml2js": "^0.6.0",
168
194
  "yargs": "^17.7.2",
169
195
  "yauzl": "^3.0.0",
170
196
  "zod": "^4.0.0"
171
197
  },
172
- "overrides": {
173
- "archiver": "^8.0.0",
174
- "unzipper": "^0.12.3",
175
- "fast-csv": "^5.0.7",
176
- "encoding-sniffer": "^1.0.2",
177
- "gaxios": "^7.1.4",
178
- "exceljs": {
179
- "uuid": "^9.0.1"
180
- }
181
- },
182
198
  "optionalDependencies": {
183
199
  "@lydell/node-pty": "1.1.0",
184
200
  "@lydell/node-pty-darwin-arm64": "1.1.0",
@@ -28,6 +28,10 @@ if (process.cwd().includes('packages')) {
28
28
 
29
29
  // Check 2: Bundle exists
30
30
  const bundlePath = path.join(projectRoot, 'bundle/fss-link.js');
31
+ // The bin (bundle/fss-link.js) is a tiny bootstrap; the real code lives in
32
+ // bundle/fss-link-main.js, which the bootstrap dynamically imports. Size and
33
+ // version-content checks must target the main bundle, not the bootstrap.
34
+ const mainBundlePath = path.join(projectRoot, 'bundle/fss-link-main.js');
31
35
  if (!fs.existsSync(bundlePath)) {
32
36
  console.error('❌ ERROR: bundle/fss-link.js not found');
33
37
  console.error(' Run: npm run bundle\n');
@@ -56,23 +60,30 @@ if (!fs.existsSync(bundlePath)) {
56
60
  console.log('✅ Bundle has correct shebang');
57
61
  }
58
62
 
59
- // Check 5: Bundle size is reasonable (> 1MB)
60
- const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
61
- if (stats.size < 1024 * 1024) {
62
- console.error(`❌ ERROR: Bundle suspiciously small (${sizeMB} MB)`);
63
- console.error(' Expected: > 1 MB');
64
- console.error(' This may indicate an incomplete build\n');
63
+ // Check 5: Main bundle exists and is a reasonable size (> 1MB)
64
+ if (!fs.existsSync(mainBundlePath)) {
65
+ console.error('❌ ERROR: bundle/fss-link-main.js not found');
66
+ console.error(' The bootstrap bin imports it Run: npm run bundle\n');
65
67
  errors++;
66
68
  } else {
67
- console.log(`✅ Bundle size reasonable (${sizeMB} MB)`);
69
+ const mainStats = fs.statSync(mainBundlePath);
70
+ const sizeMB = (mainStats.size / (1024 * 1024)).toFixed(2);
71
+ if (mainStats.size < 1024 * 1024) {
72
+ console.error(`❌ ERROR: Main bundle suspiciously small (${sizeMB} MB)`);
73
+ console.error(' Expected: > 1 MB');
74
+ console.error(' This may indicate an incomplete build\n');
75
+ errors++;
76
+ } else {
77
+ console.log(`✅ Main bundle size reasonable (${sizeMB} MB)`);
78
+ }
68
79
  }
69
80
  }
70
81
 
71
82
  // Check 6: Version in bundle matches package.json
72
83
  const pkgPath = path.join(projectRoot, 'package.json');
73
84
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
74
- if (fs.existsSync(bundlePath)) {
75
- const bundleContent = fs.readFileSync(bundlePath, 'utf8');
85
+ if (fs.existsSync(mainBundlePath)) {
86
+ const bundleContent = fs.readFileSync(mainBundlePath, 'utf8');
76
87
  // Check for version in various formats: "version":"1.2.8", version = "1.2.8", etc.
77
88
  const versionPatterns = [
78
89
  `"version":"${pkg.version}"`,
@@ -17,7 +17,7 @@
17
17
  // See the License for the specific language governing permissions and
18
18
  // limitations under the License.
19
19
 
20
- import { copyFileSync, existsSync, mkdirSync } from 'fs';
20
+ import { copyFileSync, existsSync, mkdirSync, chmodSync } from 'fs';
21
21
  import { dirname, join, basename } from 'path';
22
22
  import { fileURLToPath } from 'url';
23
23
  import { glob } from 'glob';
@@ -31,6 +31,15 @@ if (!existsSync(bundleDir)) {
31
31
  mkdirSync(bundleDir);
32
32
  }
33
33
 
34
+ // Install the bootstrap as the published bin (bundle/fss-link.js). It forces
35
+ // React's production build, then dynamically imports the esbuild output
36
+ // (bundle/fss-link-main.js). See fss-docs/DESIGN-NOTE-react-production-mode.md.
37
+ const bootstrapSrc = join(root, 'packages', 'cli', 'bootstrap.mjs');
38
+ const bootstrapDest = join(bundleDir, 'fss-link.js');
39
+ copyFileSync(bootstrapSrc, bootstrapDest);
40
+ chmodSync(bootstrapDest, 0o755);
41
+ console.log('Bootstrap bin installed at bundle/fss-link.js');
42
+
34
43
  // Find and copy all .sb files from packages to the root of the bundle directory
35
44
  const sbFiles = glob.sync('packages/**/*.sb', { cwd: root });
36
45
  for (const file of sbFiles) {
@@ -7,6 +7,24 @@
7
7
  // FSS Link postinstall guidance — printed after npm install -g fss-link
8
8
  'use strict';
9
9
 
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // Patch tr46 to use punycode.js instead of the deprecated built-in punycode module.
14
+ // This eliminates the Node 22 DEP0040 deprecation warning from the
15
+ // jsdom → whatwg-url → tr46 dependency chain.
16
+ try {
17
+ const tr46Path = path.join(__dirname, '..', 'node_modules', 'tr46', 'index.js');
18
+ if (fs.existsSync(tr46Path)) {
19
+ const content = fs.readFileSync(tr46Path, 'utf8');
20
+ if (content.includes('require("punycode/")')) {
21
+ fs.writeFileSync(tr46Path, content.replace('require("punycode/")', 'require("punycode.js")'));
22
+ }
23
+ }
24
+ } catch {
25
+ // Non-fatal — tr46 may not be installed or patch already applied
26
+ }
27
+
10
28
  const lines = [
11
29
  '',
12
30
  '╔════════════════════════════════════════╗',
@@ -36,15 +36,22 @@ if (!versionType) {
36
36
  // 2. Bump the version in the root and all workspace package.json files.
37
37
  run(`npm version ${versionType} --no-git-tag-version --allow-same-version`);
38
38
 
39
- // 3. Get all workspaces and filter out the one we don't want to version.
39
+ // 3. Get all workspaces and filter out the ones we don't want to version.
40
40
  const workspacesToExclude = [];
41
- const lsOutput = JSON.parse(
42
- execSync('npm ls --workspaces --json --depth=0').toString(),
43
- );
44
- const allWorkspaces = Object.keys(lsOutput.dependencies || {});
45
- const workspacesToVersion = allWorkspaces.filter(
46
- (wsName) => !workspacesToExclude.includes(wsName),
47
- );
41
+ const rootPkg = readJson(resolve(process.cwd(), 'package.json'));
42
+ const workspaceDirs = rootPkg.workspaces || [];
43
+ const workspacesToVersion = [];
44
+ for (const dir of workspaceDirs) {
45
+ const pkgPath = resolve(process.cwd(), dir, 'package.json');
46
+ try {
47
+ const wsPkg = readJson(pkgPath);
48
+ if (!workspacesToExclude.includes(wsPkg.name)) {
49
+ workspacesToVersion.push(wsPkg.name);
50
+ }
51
+ } catch {
52
+ // Skip directories without package.json
53
+ }
54
+ }
48
55
 
49
56
  for (const workspaceName of workspacesToVersion) {
50
57
  run(
@@ -78,6 +85,7 @@ if (cliPackageJson.config?.sandboxImageUri) {
78
85
  }
79
86
 
80
87
  // 8. Run `npm install` to update package-lock.json.
81
- run('npm install');
88
+ // --ignore-scripts prevents the prepare hook (bundle) from firing before deps exist.
89
+ run('npm install --ignore-scripts');
82
90
 
83
91
  console.log(`Successfully bumped versions to v${newVersion}.`);