@jefuriiij/synthra 0.1.10 → 0.1.11
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 +109 -0
- package/README.md +5 -3
- package/dist/cli/index.js +182 -63
- package/dist/cli/index.js.map +1 -1
- package/dist/server/index.js +93 -26
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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.11] — 2026-05-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Dart parser actually runs now.** Was silently broken since v0.1 due to an
|
|
15
|
+
ABI mismatch (shipped wasm was ABI v15, pinned `web-tree-sitter` only
|
|
16
|
+
supported v13–v14). Every `.dart` file got zero symbols, zero imports —
|
|
17
|
+
the exception was swallowed by the parser's try/catch. Bumped
|
|
18
|
+
`web-tree-sitter` to `^0.25.10` to fix.
|
|
19
|
+
- **Real Dart symbol extraction.** Classes, mixins, extensions, enums,
|
|
20
|
+
typedefs, top-level functions, methods, getters, setters, constructors.
|
|
21
|
+
- **Dart import normalization.** `package:foo/bar.dart` and `dart:async` are
|
|
22
|
+
stripped (cross-project); bare `'sibling.dart'` is rewritten to
|
|
23
|
+
`./sibling.dart` so the project resolver can complete them.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Update check runs on every `syn .`** (no more 24h cache). If you're on
|
|
28
|
+
latest, stays silent. If outdated, prompts `[y/N]` as before.
|
|
29
|
+
- **Auto-update now shows a changelog.** After `npm install -g …@latest`
|
|
30
|
+
succeeds, Synthra prints the new version's section from this file before
|
|
31
|
+
telling you to re-run. Catches `npm install` outside of `syn .` too —
|
|
32
|
+
next startup compares your current version to `~/.synthra/last-seen-version.json`
|
|
33
|
+
and prints if it's newer.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [0.1.10] — 2026-05-29
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **CLAUDE.md policy v2 → v3.** Session-end now goes through
|
|
42
|
+
`context_remember({kind: "task"|"decision"|"next"})` instead of writing
|
|
43
|
+
`.synthra/CONTEXT.md` directly. The Stop hook always re-rendered CONTEXT.md
|
|
44
|
+
from `context-store.json` — under v2 Claude's direct writes were getting
|
|
45
|
+
wiped on session end. Existing v2 blocks auto-upgrade.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **Scanner ignores more build caches.** `.dart_tool/`, `.flutter-plugins`,
|
|
50
|
+
`.flutter-plugins-dependencies`, `.gradle/`, `target/`, `Pods/`,
|
|
51
|
+
`DerivedData/`, `__pycache__/`, `.venv/`, `venv/`, `.tox/`,
|
|
52
|
+
`.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`, `obj/`, `.vs/`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## [0.1.9] — 2026-05-29
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Crash on prototype-colliding symbol names.** `buildSymbolIndex` built
|
|
61
|
+
the lookup on a plain `{}`, so a symbol named `toString` (which every
|
|
62
|
+
Dart class overrides), `constructor`, `valueOf`, etc. resolved to the
|
|
63
|
+
inherited `Object.prototype` member and crashed on `.push`. Now uses
|
|
64
|
+
`Object.create(null)` on both fresh-build and load-from-disk paths.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## [0.1.8] — 2026-05-29
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **Interactive auto-update.** `syn .` checks npm at startup; if a newer
|
|
73
|
+
version is available, prompts `[y/N]`. On `y`, runs
|
|
74
|
+
`npm install -g @jefuriiij/synthra@latest` with stdio inherited and
|
|
75
|
+
exits with re-run instructions. Non-TTY runs (CI, piped stdin) fall
|
|
76
|
+
back to a silent one-line hint. `SYN_NO_UPDATE_CHECK=1` opts out.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## [0.1.7] — 2026-05-29
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **JS parser missed CommonJS imports + JS class names.** Unified TS/JS
|
|
85
|
+
query only matched ES `import_statement`, and used `(type_identifier)`
|
|
86
|
+
for class names — which is TS-grammar-only. Result: every `.js`/`.cjs`/
|
|
87
|
+
`.mjs` file silently produced zero imports, and any class in a JS file
|
|
88
|
+
was skipped. Split into `TS_QUERY` and `JS_QUERY`; JS query adds a
|
|
89
|
+
`require()` capture and uses `(identifier)` for class names.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## [0.1.6] — 2026-05-29
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- **MCP registration now uses `--scope project`** so the Claude Code IDE
|
|
98
|
+
extension actually sees Synthra. The previous `--scope local` wrote to
|
|
99
|
+
a per-project section of `~/.claude.json` that only the `claude` CLI
|
|
100
|
+
reads — invisible to the IDE.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## [0.1.5] and earlier
|
|
105
|
+
|
|
106
|
+
See [GitHub commits](https://github.com/jefuriiij/synthra/commits/main) for
|
|
107
|
+
detail. v0.1.5 introduced the v2 policy template with namespace + skip rules;
|
|
108
|
+
v0.1.4 fixed a DEP0190 deprecation on Windows; v0.1.3 was the dashboard
|
|
109
|
+
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
21
|
+
version: "0.1.11",
|
|
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": "
|
|
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,7 +2244,7 @@ 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
|
|
2249
2250
|
function firstLine(text, max = 200) {
|
|
@@ -2392,26 +2393,93 @@ async function parseCSharp(f, source) {
|
|
|
2392
2393
|
|
|
2393
2394
|
// src/scanner/parsers/dart.ts
|
|
2394
2395
|
var QUERY4 = `
|
|
2395
|
-
(class_definition (identifier) @class.name) @class
|
|
2396
|
-
(mixin_declaration (identifier) @
|
|
2397
|
-
(extension_declaration (identifier) @
|
|
2398
|
-
(
|
|
2396
|
+
(class_definition name: (identifier) @class.name) @class
|
|
2397
|
+
(mixin_declaration (identifier) @mixin.name) @mixin
|
|
2398
|
+
(extension_declaration name: (identifier) @ext.name) @ext
|
|
2399
|
+
(enum_declaration name: (identifier) @enum.name) @enum
|
|
2400
|
+
(type_alias (type_identifier) @typedef.name) @typedef
|
|
2401
|
+
|
|
2402
|
+
(program (function_signature name: (identifier) @function.name) @function)
|
|
2403
|
+
|
|
2404
|
+
(method_signature (function_signature name: (identifier) @method.name)) @method
|
|
2405
|
+
(method_signature (getter_signature name: (identifier) @getter.name)) @getter
|
|
2406
|
+
(method_signature (setter_signature name: (identifier) @setter.name)) @setter
|
|
2407
|
+
(constructor_signature name: (identifier) @ctor.name) @ctor
|
|
2408
|
+
|
|
2409
|
+
(import_or_export (library_import (import_specification (configurable_uri (uri (string_literal) @import)))))
|
|
2399
2410
|
`;
|
|
2411
|
+
var DECLS = [
|
|
2412
|
+
{ declCap: "class", nameCap: "class.name", kind: "class" },
|
|
2413
|
+
{ declCap: "mixin", nameCap: "mixin.name", kind: "class" },
|
|
2414
|
+
{ declCap: "ext", nameCap: "ext.name", kind: "class" },
|
|
2415
|
+
{ declCap: "enum", nameCap: "enum.name", kind: "enum" },
|
|
2416
|
+
{ declCap: "typedef", nameCap: "typedef.name", kind: "type" },
|
|
2417
|
+
{ declCap: "function", nameCap: "function.name", kind: "function" },
|
|
2418
|
+
{ declCap: "method", nameCap: "method.name", kind: "method" },
|
|
2419
|
+
{ declCap: "getter", nameCap: "getter.name", kind: "method" },
|
|
2420
|
+
{ declCap: "setter", nameCap: "setter.name", kind: "method" },
|
|
2421
|
+
{ declCap: "ctor", nameCap: "ctor.name", kind: "method" }
|
|
2422
|
+
];
|
|
2423
|
+
function firstLine2(text, max = 200) {
|
|
2424
|
+
const line = text.split(/\r?\n/, 1)[0] ?? "";
|
|
2425
|
+
return line.length > max ? line.slice(0, max) + "\u2026" : line;
|
|
2426
|
+
}
|
|
2427
|
+
function normalizeDartImport(raw) {
|
|
2428
|
+
const stripped = raw.replace(/^['"]|['"]$/g, "");
|
|
2429
|
+
if (!stripped) return null;
|
|
2430
|
+
if (stripped.startsWith("package:")) return null;
|
|
2431
|
+
if (stripped.startsWith("dart:")) return null;
|
|
2432
|
+
if (stripped.startsWith(".") || stripped.startsWith("/")) return stripped;
|
|
2433
|
+
return `./${stripped}`;
|
|
2434
|
+
}
|
|
2400
2435
|
async function parseDart(f, source) {
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2436
|
+
let symbols = [];
|
|
2437
|
+
let imports = [];
|
|
2438
|
+
try {
|
|
2439
|
+
const { parser, language } = await createParser("dart");
|
|
2440
|
+
const tree = parser.parse(source);
|
|
2441
|
+
if (!tree) return { file: f, source, symbols, imports, calls: [] };
|
|
2442
|
+
const query = language.query(QUERY4);
|
|
2443
|
+
const matches = query.matches(tree.rootNode);
|
|
2444
|
+
for (const match of matches) {
|
|
2445
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2446
|
+
for (const cap of match.captures) byName.set(cap.name, cap.node);
|
|
2447
|
+
let matched = null;
|
|
2448
|
+
for (const d of DECLS) {
|
|
2449
|
+
if (byName.has(d.declCap) && byName.has(d.nameCap)) {
|
|
2450
|
+
matched = d;
|
|
2451
|
+
break;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
if (matched) {
|
|
2455
|
+
const declNode = byName.get(matched.declCap);
|
|
2456
|
+
const nameNode = byName.get(matched.nameCap);
|
|
2457
|
+
symbols.push({
|
|
2458
|
+
name: nameNode.text,
|
|
2459
|
+
kind: matched.kind,
|
|
2460
|
+
startLine: declNode.startPosition.row + 1,
|
|
2461
|
+
endLine: declNode.endPosition.row + 1,
|
|
2462
|
+
signature: firstLine2(declNode.text)
|
|
2463
|
+
});
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
const importNode = byName.get("import");
|
|
2467
|
+
if (importNode) {
|
|
2468
|
+
const norm = normalizeDartImport(importNode.text);
|
|
2469
|
+
if (norm) imports.push(norm);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2473
|
+
symbols = symbols.filter((s) => {
|
|
2474
|
+
const k = `${s.name}:${s.startLine}`;
|
|
2475
|
+
if (seen.has(k)) return false;
|
|
2476
|
+
seen.add(k);
|
|
2477
|
+
return true;
|
|
2478
|
+
});
|
|
2479
|
+
imports = Array.from(new Set(imports));
|
|
2480
|
+
} catch {
|
|
2481
|
+
}
|
|
2482
|
+
return { file: f, source, symbols, imports, calls: [] };
|
|
2415
2483
|
}
|
|
2416
2484
|
|
|
2417
2485
|
// src/scanner/parsers/go.ts
|
|
@@ -2522,7 +2590,7 @@ var QUERY9 = `
|
|
|
2522
2590
|
(import_from_statement module_name: (dotted_name) @import.from)
|
|
2523
2591
|
(import_from_statement module_name: (relative_import) @import.from)
|
|
2524
2592
|
`;
|
|
2525
|
-
function
|
|
2593
|
+
function firstLine3(text, max = 200) {
|
|
2526
2594
|
const line = text.split(/\r?\n/, 1)[0] ?? "";
|
|
2527
2595
|
return line.length > max ? line.slice(0, max) + "\u2026" : line;
|
|
2528
2596
|
}
|
|
@@ -2548,7 +2616,7 @@ async function parsePython(f, source) {
|
|
|
2548
2616
|
kind: isMethod ? "method" : "function",
|
|
2549
2617
|
startLine: funcDecl.startPosition.row + 1,
|
|
2550
2618
|
endLine: funcDecl.endPosition.row + 1,
|
|
2551
|
-
signature:
|
|
2619
|
+
signature: firstLine3(funcDecl.text)
|
|
2552
2620
|
});
|
|
2553
2621
|
continue;
|
|
2554
2622
|
}
|
|
@@ -2560,7 +2628,7 @@ async function parsePython(f, source) {
|
|
|
2560
2628
|
kind: "class",
|
|
2561
2629
|
startLine: classDecl.startPosition.row + 1,
|
|
2562
2630
|
endLine: classDecl.endPosition.row + 1,
|
|
2563
|
-
signature:
|
|
2631
|
+
signature: firstLine3(classDecl.text)
|
|
2564
2632
|
});
|
|
2565
2633
|
continue;
|
|
2566
2634
|
}
|
|
@@ -2660,7 +2728,7 @@ function queryFor(grammar) {
|
|
|
2660
2728
|
function unquote(s) {
|
|
2661
2729
|
return s.replace(/^["'`]|["'`]$/g, "");
|
|
2662
2730
|
}
|
|
2663
|
-
function
|
|
2731
|
+
function firstLine4(text, max = 200) {
|
|
2664
2732
|
const line = text.split(/\r?\n/, 1)[0] ?? "";
|
|
2665
2733
|
return line.length > max ? line.slice(0, max) + "\u2026" : line;
|
|
2666
2734
|
}
|
|
@@ -2692,7 +2760,7 @@ async function parseTypeScript(f, source) {
|
|
|
2692
2760
|
kind: shape.kind,
|
|
2693
2761
|
startLine: shape.decl.startPosition.row + 1,
|
|
2694
2762
|
endLine: shape.decl.endPosition.row + 1,
|
|
2695
|
-
signature:
|
|
2763
|
+
signature: firstLine4(shape.decl.text)
|
|
2696
2764
|
});
|
|
2697
2765
|
continue;
|
|
2698
2766
|
}
|
|
@@ -2837,7 +2905,7 @@ async function loadGrammar(name) {
|
|
|
2837
2905
|
const cached = languageCache.get(name);
|
|
2838
2906
|
if (cached) return cached;
|
|
2839
2907
|
const wasmPath = require2.resolve(GRAMMAR_FILES[name]);
|
|
2840
|
-
const lang = await
|
|
2908
|
+
const lang = await Language.load(wasmPath);
|
|
2841
2909
|
languageCache.set(name, lang);
|
|
2842
2910
|
return lang;
|
|
2843
2911
|
}
|
|
@@ -4712,9 +4780,8 @@ import { join as join10 } from "path";
|
|
|
4712
4780
|
import { createInterface } from "readline/promises";
|
|
4713
4781
|
import spawn from "cross-spawn";
|
|
4714
4782
|
var PKG_NAME = "@jefuriiij/synthra";
|
|
4715
|
-
var
|
|
4716
|
-
var
|
|
4717
|
-
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4783
|
+
var SYNTHRA_DIR = join10(homedir3(), ".synthra");
|
|
4784
|
+
var LAST_SEEN_PATH = join10(SYNTHRA_DIR, "last-seen-version.json");
|
|
4718
4785
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
|
|
4719
4786
|
var FETCH_TIMEOUT_MS = 2e3;
|
|
4720
4787
|
var currentVersionCache = null;
|
|
@@ -4729,23 +4796,6 @@ async function getCurrentVersion() {
|
|
|
4729
4796
|
return "0.0.0";
|
|
4730
4797
|
}
|
|
4731
4798
|
}
|
|
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
4799
|
function isNewer(candidate, baseline) {
|
|
4750
4800
|
const a = candidate.split(/[.-]/).map((p) => Number(p));
|
|
4751
4801
|
const b = baseline.split(/[.-]/).map((p) => Number(p));
|
|
@@ -4776,23 +4826,88 @@ async function checkForUpdate() {
|
|
|
4776
4826
|
if (process.env.SYN_NO_UPDATE_CHECK === "1") {
|
|
4777
4827
|
return { current, latest: null, hasUpdate: false };
|
|
4778
4828
|
}
|
|
4779
|
-
const
|
|
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
|
-
}
|
|
4829
|
+
const latest = await fetchLatestFromRegistry();
|
|
4793
4830
|
const hasUpdate = latest ? isNewer(latest, current) : false;
|
|
4794
4831
|
return { current, latest, hasUpdate };
|
|
4795
4832
|
}
|
|
4833
|
+
async function readLastSeen() {
|
|
4834
|
+
try {
|
|
4835
|
+
const raw = await readFile14(LAST_SEEN_PATH, "utf8");
|
|
4836
|
+
const parsed = JSON.parse(raw);
|
|
4837
|
+
return parsed.version ?? null;
|
|
4838
|
+
} catch {
|
|
4839
|
+
return null;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
async function writeLastSeen(version) {
|
|
4843
|
+
try {
|
|
4844
|
+
await mkdir10(SYNTHRA_DIR, { recursive: true });
|
|
4845
|
+
const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4846
|
+
await writeFile9(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
4847
|
+
} catch {
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
function npmGlobalRoot() {
|
|
4851
|
+
return new Promise((resolve5) => {
|
|
4852
|
+
const chunks = [];
|
|
4853
|
+
const proc = spawn("npm", ["root", "-g"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
4854
|
+
proc.stdout?.on("data", (c) => chunks.push(c));
|
|
4855
|
+
proc.on("error", () => resolve5(null));
|
|
4856
|
+
proc.on("exit", (code) => {
|
|
4857
|
+
if (code !== 0) return resolve5(null);
|
|
4858
|
+
const out = Buffer.concat(chunks).toString("utf8").trim();
|
|
4859
|
+
resolve5(out || null);
|
|
4860
|
+
});
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
function extractChangelogSection(text, version) {
|
|
4864
|
+
const escapedVersion = version.replace(/\./g, "\\.");
|
|
4865
|
+
const headingRe = new RegExp(`^##\\s+\\[?v?${escapedVersion}\\]?.*$`, "m");
|
|
4866
|
+
const m = headingRe.exec(text);
|
|
4867
|
+
if (!m) return null;
|
|
4868
|
+
const startBody = m.index + m[0].length;
|
|
4869
|
+
const rest = text.slice(startBody);
|
|
4870
|
+
const nextHeadingIdx = rest.search(/^##\s+/m);
|
|
4871
|
+
const body = nextHeadingIdx < 0 ? rest : rest.slice(0, nextHeadingIdx);
|
|
4872
|
+
return body.replace(/^---\s*$/gm, "").trim() || null;
|
|
4873
|
+
}
|
|
4874
|
+
async function readInstalledChangelog() {
|
|
4875
|
+
const root = await npmGlobalRoot();
|
|
4876
|
+
if (!root) return null;
|
|
4877
|
+
try {
|
|
4878
|
+
return await readFile14(join10(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
|
|
4879
|
+
} catch {
|
|
4880
|
+
return null;
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
async function printChangelogForVersion(version) {
|
|
4884
|
+
const md = await readInstalledChangelog();
|
|
4885
|
+
if (!md) return;
|
|
4886
|
+
const section = extractChangelogSection(md, version);
|
|
4887
|
+
if (!section) return;
|
|
4888
|
+
log.info("");
|
|
4889
|
+
log.info(`What's new in ${version}:`);
|
|
4890
|
+
log.info("");
|
|
4891
|
+
for (const line of section.split(/\r?\n/)) {
|
|
4892
|
+
log.info(` ${line}`);
|
|
4893
|
+
}
|
|
4894
|
+
log.info("");
|
|
4895
|
+
}
|
|
4896
|
+
async function runStartupChangelogCheck() {
|
|
4897
|
+
try {
|
|
4898
|
+
const current = await getCurrentVersion();
|
|
4899
|
+
const lastSeen = await readLastSeen();
|
|
4900
|
+
if (!lastSeen) {
|
|
4901
|
+
await writeLastSeen(current);
|
|
4902
|
+
return;
|
|
4903
|
+
}
|
|
4904
|
+
if (isNewer(current, lastSeen)) {
|
|
4905
|
+
await printChangelogForVersion(current);
|
|
4906
|
+
await writeLastSeen(current);
|
|
4907
|
+
}
|
|
4908
|
+
} catch {
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4796
4911
|
async function promptYesNo(question) {
|
|
4797
4912
|
if (!process.stdin.isTTY) return false;
|
|
4798
4913
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -4834,7 +4949,10 @@ async function promptForUpdateOrLog() {
|
|
|
4834
4949
|
log.warn("npm install failed \u2014 continuing with current version.");
|
|
4835
4950
|
return;
|
|
4836
4951
|
}
|
|
4837
|
-
log.info(`\u2713 Updated to ${r.latest}
|
|
4952
|
+
log.info(`\u2713 Updated to ${r.latest}.`);
|
|
4953
|
+
await printChangelogForVersion(r.latest);
|
|
4954
|
+
await writeLastSeen(r.latest);
|
|
4955
|
+
log.info(`Please re-run: syn .`);
|
|
4838
4956
|
process.exit(0);
|
|
4839
4957
|
} catch {
|
|
4840
4958
|
}
|
|
@@ -4955,6 +5073,7 @@ async function defaultFlow(rawPath, opts) {
|
|
|
4955
5073
|
const projectRoot = resolve4(rawPath);
|
|
4956
5074
|
const paths = resolvePaths(projectRoot);
|
|
4957
5075
|
const cfg = loadConfig();
|
|
5076
|
+
await runStartupChangelogCheck();
|
|
4958
5077
|
await promptForUpdateOrLog();
|
|
4959
5078
|
await recordProject(projectRoot);
|
|
4960
5079
|
const scan = await scanCommand(rawPath);
|