@jefuriiij/synthra 0.1.10 → 0.1.12

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/CHANGELOG.md ADDED
@@ -0,0 +1,122 @@
1
+ # Synthra changelog
2
+
3
+ Notable changes per version. This file ships inside the npm tarball — `syn .`
4
+ reads it after an auto-update to show you what changed.
5
+
6
+ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/releases).
7
+
8
+ ---
9
+
10
+ ## [0.1.12] — 2026-05-29
11
+
12
+ ### Fixed
13
+
14
+ - **`Language.query is deprecated` spam at scan time.** Every parsed file
15
+ printed the warning — 57 prints on a Flutter codebase, one per parsed
16
+ file. Switched all four parsers (TypeScript, JavaScript, Python, Dart,
17
+ plus the generic helper) from the deprecated `language.query(QUERY)`
18
+ to `new Query(language, QUERY)`. No behavior change, just clean
19
+ terminal output.
20
+
21
+ ---
22
+
23
+ ## [0.1.11] — 2026-05-29
24
+
25
+ ### Fixed
26
+
27
+ - **Dart parser actually runs now.** Was silently broken since v0.1 due to an
28
+ ABI mismatch (shipped wasm was ABI v15, pinned `web-tree-sitter` only
29
+ supported v13–v14). Every `.dart` file got zero symbols, zero imports —
30
+ the exception was swallowed by the parser's try/catch. Bumped
31
+ `web-tree-sitter` to `^0.25.10` to fix.
32
+ - **Real Dart symbol extraction.** Classes, mixins, extensions, enums,
33
+ typedefs, top-level functions, methods, getters, setters, constructors.
34
+ - **Dart import normalization.** `package:foo/bar.dart` and `dart:async` are
35
+ stripped (cross-project); bare `'sibling.dart'` is rewritten to
36
+ `./sibling.dart` so the project resolver can complete them.
37
+
38
+ ### Changed
39
+
40
+ - **Update check runs on every `syn .`** (no more 24h cache). If you're on
41
+ latest, stays silent. If outdated, prompts `[y/N]` as before.
42
+ - **Auto-update now shows a changelog.** After `npm install -g …@latest`
43
+ succeeds, Synthra prints the new version's section from this file before
44
+ telling you to re-run. Catches `npm install` outside of `syn .` too —
45
+ next startup compares your current version to `~/.synthra/last-seen-version.json`
46
+ and prints if it's newer.
47
+
48
+ ---
49
+
50
+ ## [0.1.10] — 2026-05-29
51
+
52
+ ### Changed
53
+
54
+ - **CLAUDE.md policy v2 → v3.** Session-end now goes through
55
+ `context_remember({kind: "task"|"decision"|"next"})` instead of writing
56
+ `.synthra/CONTEXT.md` directly. The Stop hook always re-rendered CONTEXT.md
57
+ from `context-store.json` — under v2 Claude's direct writes were getting
58
+ wiped on session end. Existing v2 blocks auto-upgrade.
59
+
60
+ ### Added
61
+
62
+ - **Scanner ignores more build caches.** `.dart_tool/`, `.flutter-plugins`,
63
+ `.flutter-plugins-dependencies`, `.gradle/`, `target/`, `Pods/`,
64
+ `DerivedData/`, `__pycache__/`, `.venv/`, `venv/`, `.tox/`,
65
+ `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`, `obj/`, `.vs/`.
66
+
67
+ ---
68
+
69
+ ## [0.1.9] — 2026-05-29
70
+
71
+ ### Fixed
72
+
73
+ - **Crash on prototype-colliding symbol names.** `buildSymbolIndex` built
74
+ the lookup on a plain `{}`, so a symbol named `toString` (which every
75
+ Dart class overrides), `constructor`, `valueOf`, etc. resolved to the
76
+ inherited `Object.prototype` member and crashed on `.push`. Now uses
77
+ `Object.create(null)` on both fresh-build and load-from-disk paths.
78
+
79
+ ---
80
+
81
+ ## [0.1.8] — 2026-05-29
82
+
83
+ ### Added
84
+
85
+ - **Interactive auto-update.** `syn .` checks npm at startup; if a newer
86
+ version is available, prompts `[y/N]`. On `y`, runs
87
+ `npm install -g @jefuriiij/synthra@latest` with stdio inherited and
88
+ exits with re-run instructions. Non-TTY runs (CI, piped stdin) fall
89
+ back to a silent one-line hint. `SYN_NO_UPDATE_CHECK=1` opts out.
90
+
91
+ ---
92
+
93
+ ## [0.1.7] — 2026-05-29
94
+
95
+ ### Fixed
96
+
97
+ - **JS parser missed CommonJS imports + JS class names.** Unified TS/JS
98
+ query only matched ES `import_statement`, and used `(type_identifier)`
99
+ for class names — which is TS-grammar-only. Result: every `.js`/`.cjs`/
100
+ `.mjs` file silently produced zero imports, and any class in a JS file
101
+ was skipped. Split into `TS_QUERY` and `JS_QUERY`; JS query adds a
102
+ `require()` capture and uses `(identifier)` for class names.
103
+
104
+ ---
105
+
106
+ ## [0.1.6] — 2026-05-29
107
+
108
+ ### Fixed
109
+
110
+ - **MCP registration now uses `--scope project`** so the Claude Code IDE
111
+ extension actually sees Synthra. The previous `--scope local` wrote to
112
+ a per-project section of `~/.claude.json` that only the `claude` CLI
113
+ reads — invisible to the IDE.
114
+
115
+ ---
116
+
117
+ ## [0.1.5] and earlier
118
+
119
+ See [GitHub commits](https://github.com/jefuriiij/synthra/commits/main) for
120
+ detail. v0.1.5 introduced the v2 policy template with namespace + skip rules;
121
+ v0.1.4 fixed a DEP0190 deprecation on Windows; v0.1.3 was the dashboard
122
+ redesign (Cool Marine palette, FAQ modal, savings audit row).
package/README.md CHANGED
@@ -177,13 +177,15 @@ Claude Code honors the block and pivots to the MCP tool. The structured pack is
177
177
 
178
178
  ## Self-update
179
179
 
180
- Daily fire-and-forget version check against the npm registry. When a newer version is available:
180
+ Every `syn .` checks the npm registry for a newer version (no cache — always fresh; 2s hard timeout; silent fallthrough on network failure). When you're on latest, the check stays silent and `syn .` proceeds. When you're outdated:
181
181
 
182
182
  - **Interactive shell** (TTY) → you see `[syn] Synthra X.Y.Z is available (you have A.B.C). Update now? [y/N]:` *before* the scan starts. Type `y` to install; press Enter (or anything else) to skip and continue with the current version.
183
- - **Non-interactive** (CI, piped stdin) → silent one-line log line, no prompt. Pure fire-and-forget.
183
+ - **Non-interactive** (CI, piped stdin) → silent one-line hint, no prompt.
184
184
  - **Disabled entirely** → set `SYN_NO_UPDATE_CHECK=1`.
185
185
 
186
- On `y`, Synthra spawns `npm install -g @jefuriiij/synthra@latest` with stdio inherited (you see npm's progress), then exits with re-run instructions — the running Node process is still the old version and can't hot-swap its own code mid-run. Cached at `~/.synthra/version-check.json` for 24h so you're never nagged on rapid `syn .` runs.
186
+ On `y`, Synthra spawns `npm install -g @jefuriiij/synthra@latest` with stdio inherited (you see npm's progress), then **prints the new version's `CHANGELOG.md` section** (so you know what you just got), then exits with re-run instructions — the running Node process is still the old version and can't hot-swap its own code mid-run.
187
+
188
+ If you upgrade via `npm install -g @jefuriiij/synthra@latest` directly (outside Synthra's prompt), the next `syn .` notices and prints the changelog anyway. It tracks the last-seen version at `~/.synthra/last-seen-version.json`.
187
189
 
188
190
  ---
189
191
 
package/dist/cli/index.js CHANGED
@@ -18,7 +18,7 @@ var init_package = __esm({
18
18
  "package.json"() {
19
19
  package_default = {
20
20
  name: "@jefuriiij/synthra",
21
- version: "0.1.10",
21
+ version: "0.1.12",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -39,6 +39,7 @@ var init_package = __esm({
39
39
  "dist",
40
40
  "bin",
41
41
  "README.md",
42
+ "CHANGELOG.md",
42
43
  "LICENSE",
43
44
  "ROADMAP.md"
44
45
  ],
@@ -71,7 +72,7 @@ var init_package = __esm({
71
72
  ignore: "^7.0.0",
72
73
  sade: "^1.8.1",
73
74
  "tree-sitter-wasms": "^0.1.12",
74
- "web-tree-sitter": "~0.22.6"
75
+ "web-tree-sitter": "^0.25.10"
75
76
  },
76
77
  devDependencies: {
77
78
  "@types/cross-spawn": "^6.0.6",
@@ -2103,7 +2104,7 @@ function extractKeywords(content, _ext) {
2103
2104
  }
2104
2105
 
2105
2106
  // src/scanner/extract.ts
2106
- var RESOLVE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".svelte", ".vue"];
2107
+ var RESOLVE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".svelte", ".vue", ".dart"];
2107
2108
  var INDEX_FILES = ["index.ts", "index.tsx", "index.js", "index.jsx", "__init__.py"];
2108
2109
  function fileId(relPath) {
2109
2110
  return `file:${relPath}`;
@@ -2243,9 +2244,10 @@ function buildSymbolIndex(graph) {
2243
2244
  // src/scanner/parser.ts
2244
2245
  import { readFile as readFile6 } from "fs/promises";
2245
2246
  import { createRequire } from "module";
2246
- import Parser from "web-tree-sitter";
2247
+ import { Language, Parser } from "web-tree-sitter";
2247
2248
 
2248
2249
  // src/scanner/parsers/_generic.ts
2250
+ import { Query } from "web-tree-sitter";
2249
2251
  function firstLine(text, max = 200) {
2250
2252
  const line = text.split(/\r?\n/, 1)[0] ?? "";
2251
2253
  return line.length > max ? line.slice(0, max) + "\u2026" : line;
@@ -2260,7 +2262,7 @@ async function runGenericParser(config, f, source) {
2260
2262
  const { parser, language } = await createParser(config.grammar);
2261
2263
  const tree = parser.parse(source);
2262
2264
  if (!tree) return { file: f, source, symbols, imports, calls: [] };
2263
- const query = language.query(config.query);
2265
+ const query = new Query(language, config.query);
2264
2266
  const matches = query.matches(tree.rootNode);
2265
2267
  for (const match of matches) {
2266
2268
  const byName = /* @__PURE__ */ new Map();
@@ -2391,27 +2393,95 @@ async function parseCSharp(f, source) {
2391
2393
  }
2392
2394
 
2393
2395
  // src/scanner/parsers/dart.ts
2396
+ import { Query as Query2 } from "web-tree-sitter";
2394
2397
  var QUERY4 = `
2395
- (class_definition (identifier) @class.name) @class
2396
- (mixin_declaration (identifier) @class.name) @mixin
2397
- (extension_declaration (identifier) @class.name) @ext
2398
- (function_signature (identifier) @function.name) @function
2398
+ (class_definition name: (identifier) @class.name) @class
2399
+ (mixin_declaration (identifier) @mixin.name) @mixin
2400
+ (extension_declaration name: (identifier) @ext.name) @ext
2401
+ (enum_declaration name: (identifier) @enum.name) @enum
2402
+ (type_alias (type_identifier) @typedef.name) @typedef
2403
+
2404
+ (program (function_signature name: (identifier) @function.name) @function)
2405
+
2406
+ (method_signature (function_signature name: (identifier) @method.name)) @method
2407
+ (method_signature (getter_signature name: (identifier) @getter.name)) @getter
2408
+ (method_signature (setter_signature name: (identifier) @setter.name)) @setter
2409
+ (constructor_signature name: (identifier) @ctor.name) @ctor
2410
+
2411
+ (import_or_export (library_import (import_specification (configurable_uri (uri (string_literal) @import)))))
2399
2412
  `;
2413
+ var DECLS = [
2414
+ { declCap: "class", nameCap: "class.name", kind: "class" },
2415
+ { declCap: "mixin", nameCap: "mixin.name", kind: "class" },
2416
+ { declCap: "ext", nameCap: "ext.name", kind: "class" },
2417
+ { declCap: "enum", nameCap: "enum.name", kind: "enum" },
2418
+ { declCap: "typedef", nameCap: "typedef.name", kind: "type" },
2419
+ { declCap: "function", nameCap: "function.name", kind: "function" },
2420
+ { declCap: "method", nameCap: "method.name", kind: "method" },
2421
+ { declCap: "getter", nameCap: "getter.name", kind: "method" },
2422
+ { declCap: "setter", nameCap: "setter.name", kind: "method" },
2423
+ { declCap: "ctor", nameCap: "ctor.name", kind: "method" }
2424
+ ];
2425
+ function firstLine2(text, max = 200) {
2426
+ const line = text.split(/\r?\n/, 1)[0] ?? "";
2427
+ return line.length > max ? line.slice(0, max) + "\u2026" : line;
2428
+ }
2429
+ function normalizeDartImport(raw) {
2430
+ const stripped = raw.replace(/^['"]|['"]$/g, "");
2431
+ if (!stripped) return null;
2432
+ if (stripped.startsWith("package:")) return null;
2433
+ if (stripped.startsWith("dart:")) return null;
2434
+ if (stripped.startsWith(".") || stripped.startsWith("/")) return stripped;
2435
+ return `./${stripped}`;
2436
+ }
2400
2437
  async function parseDart(f, source) {
2401
- return runGenericParser(
2402
- {
2403
- grammar: "dart",
2404
- query: QUERY4,
2405
- decls: [
2406
- { declCapture: "class", nameCapture: "class.name", kind: "class" },
2407
- { declCapture: "mixin", nameCapture: "class.name", kind: "class" },
2408
- { declCapture: "ext", nameCapture: "class.name", kind: "class" },
2409
- { declCapture: "function", nameCapture: "function.name", kind: "function" }
2410
- ]
2411
- },
2412
- f,
2413
- source
2414
- );
2438
+ let symbols = [];
2439
+ let imports = [];
2440
+ try {
2441
+ const { parser, language } = await createParser("dart");
2442
+ const tree = parser.parse(source);
2443
+ if (!tree) return { file: f, source, symbols, imports, calls: [] };
2444
+ const query = new Query2(language, QUERY4);
2445
+ const matches = query.matches(tree.rootNode);
2446
+ for (const match of matches) {
2447
+ const byName = /* @__PURE__ */ new Map();
2448
+ for (const cap of match.captures) byName.set(cap.name, cap.node);
2449
+ let matched = null;
2450
+ for (const d of DECLS) {
2451
+ if (byName.has(d.declCap) && byName.has(d.nameCap)) {
2452
+ matched = d;
2453
+ break;
2454
+ }
2455
+ }
2456
+ if (matched) {
2457
+ const declNode = byName.get(matched.declCap);
2458
+ const nameNode = byName.get(matched.nameCap);
2459
+ symbols.push({
2460
+ name: nameNode.text,
2461
+ kind: matched.kind,
2462
+ startLine: declNode.startPosition.row + 1,
2463
+ endLine: declNode.endPosition.row + 1,
2464
+ signature: firstLine2(declNode.text)
2465
+ });
2466
+ continue;
2467
+ }
2468
+ const importNode = byName.get("import");
2469
+ if (importNode) {
2470
+ const norm = normalizeDartImport(importNode.text);
2471
+ if (norm) imports.push(norm);
2472
+ }
2473
+ }
2474
+ const seen = /* @__PURE__ */ new Set();
2475
+ symbols = symbols.filter((s) => {
2476
+ const k = `${s.name}:${s.startLine}`;
2477
+ if (seen.has(k)) return false;
2478
+ seen.add(k);
2479
+ return true;
2480
+ });
2481
+ imports = Array.from(new Set(imports));
2482
+ } catch {
2483
+ }
2484
+ return { file: f, source, symbols, imports, calls: [] };
2415
2485
  }
2416
2486
 
2417
2487
  // src/scanner/parsers/go.ts
@@ -2515,6 +2585,7 @@ async function parsePhp(f, source) {
2515
2585
  }
2516
2586
 
2517
2587
  // src/scanner/parsers/python.ts
2588
+ import { Query as Query3 } from "web-tree-sitter";
2518
2589
  var QUERY9 = `
2519
2590
  (function_definition name: (identifier) @function.name) @function
2520
2591
  (class_definition name: (identifier) @class.name) @class
@@ -2522,7 +2593,7 @@ var QUERY9 = `
2522
2593
  (import_from_statement module_name: (dotted_name) @import.from)
2523
2594
  (import_from_statement module_name: (relative_import) @import.from)
2524
2595
  `;
2525
- function firstLine2(text, max = 200) {
2596
+ function firstLine3(text, max = 200) {
2526
2597
  const line = text.split(/\r?\n/, 1)[0] ?? "";
2527
2598
  return line.length > max ? line.slice(0, max) + "\u2026" : line;
2528
2599
  }
@@ -2533,7 +2604,7 @@ async function parsePython(f, source) {
2533
2604
  const { parser, language } = await createParser("python");
2534
2605
  const tree = parser.parse(source);
2535
2606
  if (!tree) return { file: f, source, symbols, imports, calls: [] };
2536
- const query = language.query(QUERY9);
2607
+ const query = new Query3(language, QUERY9);
2537
2608
  const matches = query.matches(tree.rootNode);
2538
2609
  for (const match of matches) {
2539
2610
  const byName = /* @__PURE__ */ new Map();
@@ -2548,7 +2619,7 @@ async function parsePython(f, source) {
2548
2619
  kind: isMethod ? "method" : "function",
2549
2620
  startLine: funcDecl.startPosition.row + 1,
2550
2621
  endLine: funcDecl.endPosition.row + 1,
2551
- signature: firstLine2(funcDecl.text)
2622
+ signature: firstLine3(funcDecl.text)
2552
2623
  });
2553
2624
  continue;
2554
2625
  }
@@ -2560,7 +2631,7 @@ async function parsePython(f, source) {
2560
2631
  kind: "class",
2561
2632
  startLine: classDecl.startPosition.row + 1,
2562
2633
  endLine: classDecl.endPosition.row + 1,
2563
- signature: firstLine2(classDecl.text)
2634
+ signature: firstLine3(classDecl.text)
2564
2635
  });
2565
2636
  continue;
2566
2637
  }
@@ -2631,6 +2702,7 @@ async function parseRust(f, source) {
2631
2702
  }
2632
2703
 
2633
2704
  // src/scanner/parsers/typescript.ts
2705
+ import { Query as Query4 } from "web-tree-sitter";
2634
2706
  var TS_QUERY = `
2635
2707
  (function_declaration name: (identifier) @function.name) @function
2636
2708
  (class_declaration name: (type_identifier) @class.name) @class
@@ -2660,7 +2732,7 @@ function queryFor(grammar) {
2660
2732
  function unquote(s) {
2661
2733
  return s.replace(/^["'`]|["'`]$/g, "");
2662
2734
  }
2663
- function firstLine3(text, max = 200) {
2735
+ function firstLine4(text, max = 200) {
2664
2736
  const line = text.split(/\r?\n/, 1)[0] ?? "";
2665
2737
  return line.length > max ? line.slice(0, max) + "\u2026" : line;
2666
2738
  }
@@ -2680,7 +2752,7 @@ async function parseTypeScript(f, source) {
2680
2752
  const { parser, language } = await createParser(grammar);
2681
2753
  const tree = parser.parse(source);
2682
2754
  if (!tree) return { file: f, source, symbols, imports, calls: [] };
2683
- const query = language.query(queryFor(grammar));
2755
+ const query = new Query4(language, queryFor(grammar));
2684
2756
  const matches = query.matches(tree.rootNode);
2685
2757
  for (const match of matches) {
2686
2758
  const byName = /* @__PURE__ */ new Map();
@@ -2692,7 +2764,7 @@ async function parseTypeScript(f, source) {
2692
2764
  kind: shape.kind,
2693
2765
  startLine: shape.decl.startPosition.row + 1,
2694
2766
  endLine: shape.decl.endPosition.row + 1,
2695
- signature: firstLine3(shape.decl.text)
2767
+ signature: firstLine4(shape.decl.text)
2696
2768
  });
2697
2769
  continue;
2698
2770
  }
@@ -2837,7 +2909,7 @@ async function loadGrammar(name) {
2837
2909
  const cached = languageCache.get(name);
2838
2910
  if (cached) return cached;
2839
2911
  const wasmPath = require2.resolve(GRAMMAR_FILES[name]);
2840
- const lang = await Parser.Language.load(wasmPath);
2912
+ const lang = await Language.load(wasmPath);
2841
2913
  languageCache.set(name, lang);
2842
2914
  return lang;
2843
2915
  }
@@ -4712,9 +4784,8 @@ import { join as join10 } from "path";
4712
4784
  import { createInterface } from "readline/promises";
4713
4785
  import spawn from "cross-spawn";
4714
4786
  var PKG_NAME = "@jefuriiij/synthra";
4715
- var CACHE_DIR = join10(homedir3(), ".synthra");
4716
- var CACHE_PATH = join10(CACHE_DIR, "version-check.json");
4717
- var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
4787
+ var SYNTHRA_DIR = join10(homedir3(), ".synthra");
4788
+ var LAST_SEEN_PATH = join10(SYNTHRA_DIR, "last-seen-version.json");
4718
4789
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
4719
4790
  var FETCH_TIMEOUT_MS = 2e3;
4720
4791
  var currentVersionCache = null;
@@ -4729,23 +4800,6 @@ async function getCurrentVersion() {
4729
4800
  return "0.0.0";
4730
4801
  }
4731
4802
  }
4732
- async function readCache() {
4733
- try {
4734
- const raw = await readFile14(CACHE_PATH, "utf8");
4735
- const parsed = JSON.parse(raw);
4736
- if (!parsed.checked_at || !parsed.latest_version) return null;
4737
- return parsed;
4738
- } catch {
4739
- return null;
4740
- }
4741
- }
4742
- async function writeCache(cache) {
4743
- try {
4744
- await mkdir10(CACHE_DIR, { recursive: true });
4745
- await writeFile9(CACHE_PATH, JSON.stringify(cache, null, 2), "utf8");
4746
- } catch {
4747
- }
4748
- }
4749
4803
  function isNewer(candidate, baseline) {
4750
4804
  const a = candidate.split(/[.-]/).map((p) => Number(p));
4751
4805
  const b = baseline.split(/[.-]/).map((p) => Number(p));
@@ -4776,23 +4830,88 @@ async function checkForUpdate() {
4776
4830
  if (process.env.SYN_NO_UPDATE_CHECK === "1") {
4777
4831
  return { current, latest: null, hasUpdate: false };
4778
4832
  }
4779
- const cache = await readCache();
4780
- const now = Date.now();
4781
- const cacheAge = cache ? now - Date.parse(cache.checked_at) : Infinity;
4782
- let latest = null;
4783
- if (cache && cacheAge < CACHE_TTL_MS) {
4784
- latest = cache.latest_version;
4785
- } else {
4786
- latest = await fetchLatestFromRegistry();
4787
- if (latest) {
4788
- await writeCache({ checked_at: (/* @__PURE__ */ new Date()).toISOString(), latest_version: latest });
4789
- } else if (cache) {
4790
- latest = cache.latest_version;
4791
- }
4792
- }
4833
+ const latest = await fetchLatestFromRegistry();
4793
4834
  const hasUpdate = latest ? isNewer(latest, current) : false;
4794
4835
  return { current, latest, hasUpdate };
4795
4836
  }
4837
+ async function readLastSeen() {
4838
+ try {
4839
+ const raw = await readFile14(LAST_SEEN_PATH, "utf8");
4840
+ const parsed = JSON.parse(raw);
4841
+ return parsed.version ?? null;
4842
+ } catch {
4843
+ return null;
4844
+ }
4845
+ }
4846
+ async function writeLastSeen(version) {
4847
+ try {
4848
+ await mkdir10(SYNTHRA_DIR, { recursive: true });
4849
+ const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4850
+ await writeFile9(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
4851
+ } catch {
4852
+ }
4853
+ }
4854
+ function npmGlobalRoot() {
4855
+ return new Promise((resolve5) => {
4856
+ const chunks = [];
4857
+ const proc = spawn("npm", ["root", "-g"], { stdio: ["ignore", "pipe", "ignore"] });
4858
+ proc.stdout?.on("data", (c) => chunks.push(c));
4859
+ proc.on("error", () => resolve5(null));
4860
+ proc.on("exit", (code) => {
4861
+ if (code !== 0) return resolve5(null);
4862
+ const out = Buffer.concat(chunks).toString("utf8").trim();
4863
+ resolve5(out || null);
4864
+ });
4865
+ });
4866
+ }
4867
+ function extractChangelogSection(text, version) {
4868
+ const escapedVersion = version.replace(/\./g, "\\.");
4869
+ const headingRe = new RegExp(`^##\\s+\\[?v?${escapedVersion}\\]?.*$`, "m");
4870
+ const m = headingRe.exec(text);
4871
+ if (!m) return null;
4872
+ const startBody = m.index + m[0].length;
4873
+ const rest = text.slice(startBody);
4874
+ const nextHeadingIdx = rest.search(/^##\s+/m);
4875
+ const body = nextHeadingIdx < 0 ? rest : rest.slice(0, nextHeadingIdx);
4876
+ return body.replace(/^---\s*$/gm, "").trim() || null;
4877
+ }
4878
+ async function readInstalledChangelog() {
4879
+ const root = await npmGlobalRoot();
4880
+ if (!root) return null;
4881
+ try {
4882
+ return await readFile14(join10(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
4883
+ } catch {
4884
+ return null;
4885
+ }
4886
+ }
4887
+ async function printChangelogForVersion(version) {
4888
+ const md = await readInstalledChangelog();
4889
+ if (!md) return;
4890
+ const section = extractChangelogSection(md, version);
4891
+ if (!section) return;
4892
+ log.info("");
4893
+ log.info(`What's new in ${version}:`);
4894
+ log.info("");
4895
+ for (const line of section.split(/\r?\n/)) {
4896
+ log.info(` ${line}`);
4897
+ }
4898
+ log.info("");
4899
+ }
4900
+ async function runStartupChangelogCheck() {
4901
+ try {
4902
+ const current = await getCurrentVersion();
4903
+ const lastSeen = await readLastSeen();
4904
+ if (!lastSeen) {
4905
+ await writeLastSeen(current);
4906
+ return;
4907
+ }
4908
+ if (isNewer(current, lastSeen)) {
4909
+ await printChangelogForVersion(current);
4910
+ await writeLastSeen(current);
4911
+ }
4912
+ } catch {
4913
+ }
4914
+ }
4796
4915
  async function promptYesNo(question) {
4797
4916
  if (!process.stdin.isTTY) return false;
4798
4917
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -4834,7 +4953,10 @@ async function promptForUpdateOrLog() {
4834
4953
  log.warn("npm install failed \u2014 continuing with current version.");
4835
4954
  return;
4836
4955
  }
4837
- log.info(`\u2713 Updated to ${r.latest}. Please re-run: syn .`);
4956
+ log.info(`\u2713 Updated to ${r.latest}.`);
4957
+ await printChangelogForVersion(r.latest);
4958
+ await writeLastSeen(r.latest);
4959
+ log.info(`Please re-run: syn .`);
4838
4960
  process.exit(0);
4839
4961
  } catch {
4840
4962
  }
@@ -4955,6 +5077,7 @@ async function defaultFlow(rawPath, opts) {
4955
5077
  const projectRoot = resolve4(rawPath);
4956
5078
  const paths = resolvePaths(projectRoot);
4957
5079
  const cfg = loadConfig();
5080
+ await runStartupChangelogCheck();
4958
5081
  await promptForUpdateOrLog();
4959
5082
  await recordProject(projectRoot);
4960
5083
  const scan = await scanCommand(rawPath);