novahiz 0.11.2 → 0.11.4
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 +26 -0
- package/README.md +1 -1
- package/dist/cli.js +4 -1
- package/install/install.mjs +22 -4
- package/install/lib.d.mts +26 -0
- package/install/lib.mjs +67 -11
- package/install/retired.json +25 -0
- package/package.json +1 -1
- package/src/cli.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.11.4] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`novahiz setup` now completes on Linux** - `runSync()` executed
|
|
13
|
+
`home/src/cli.ts` directly with Node, which cannot read TypeScript
|
|
14
|
+
without `--experimental-strip-types`. On a fresh global install the home
|
|
15
|
+
has no `dist/` build, so the sync step failed with
|
|
16
|
+
`ERR_UNKNOWN_FILE_EXTENSION`. The flag is now added so the two-pass
|
|
17
|
+
install (core then extras) completes on Linux, macOS and Windows.
|
|
18
|
+
|
|
19
|
+
## [0.11.3] - 2026-10-22
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **`--clean` now sees the files an older version left behind** - the install
|
|
24
|
+
manifest never recorded what earlier installers had copied, so a home
|
|
25
|
+
upgraded across several versions kept dead entry points (`install/bootstrap.mjs`,
|
|
26
|
+
`install/install.sh`, `install/install.ps1`, `bin/veille-mensuelle.ps1`) that
|
|
27
|
+
`--clean` could not detect. `install/retired.json` now lists them with the
|
|
28
|
+
sha256 of the published content, line endings normalised to LF, and cleanup
|
|
29
|
+
removes an entry only when the file on disk still matches that fingerprint.
|
|
30
|
+
A file edited by hand no longer matches, comes back as kept, and is never
|
|
31
|
+
deleted. The inventory is read from the package, never from the home, so an
|
|
32
|
+
installation cannot widen the list of what it erases.
|
|
33
|
+
|
|
8
34
|
## [0.11.2] - 2026-10-22
|
|
9
35
|
|
|
10
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
17 catégories, 94 skills, 13 gate rules, 8 MCP providers — tout déterministe, tout local, tout JSON.
|
|
6
6
|
|
|
7
|
-
> **Version 0.11.
|
|
7
|
+
> **Version 0.11.3** — `npm install -g novahiz` pose le CLI, et le premier `novahiz` lancé termine l'installation (core puis extras). `CHANGELOG.md` et `scripts/` sont dans le tarball.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
┌──────────────────────────────────────────────────────────────────────────┐
|
package/dist/cli.js
CHANGED
|
@@ -35,7 +35,10 @@ function runSync() {
|
|
|
35
35
|
const home = homeDir();
|
|
36
36
|
const cli = join(home, "src", "cli.ts");
|
|
37
37
|
if (existsSync(cli)) {
|
|
38
|
-
|
|
38
|
+
// src/cli.ts est du TypeScript : Node ne le lit pas nativement. On ajoute
|
|
39
|
+
// --experimental-strip-types pour pouvoir l'exécuter depuis le home,
|
|
40
|
+
// qui ne contient pas le build dist/ (réservé au paquet publié).
|
|
41
|
+
spawnSync(process.execPath, ["--experimental-strip-types", cli, "sync"], {
|
|
39
42
|
encoding: "utf8",
|
|
40
43
|
stdio: "inherit",
|
|
41
44
|
env: { ...process.env, NOVAHIZ_HOME: home }
|
package/install/install.mjs
CHANGED
|
@@ -52,6 +52,21 @@ const STEPS = [
|
|
|
52
52
|
"Cleanup previous install"
|
|
53
53
|
];
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Inventaire des fichiers retires du paquet (`install/retired.json`).
|
|
57
|
+
*
|
|
58
|
+
* Le manifeste d'installation ne peut pas connaitre ce qu'une version
|
|
59
|
+
* anterieure a copie avant que ce champ existe : cet inventaire comble le trou.
|
|
60
|
+
* Il est lu depuis le paquet installe, jamais depuis le home, pour qu'une
|
|
61
|
+
* installation ne puisse pas elargir elle-meme la liste de ce qu'elle efface.
|
|
62
|
+
* Un fichier absent ou illisible rend une liste vide : le nettoyage retombe
|
|
63
|
+
* alors sur le seul manifeste, jamais sur une supposition.
|
|
64
|
+
*/
|
|
65
|
+
function readRetired() {
|
|
66
|
+
const parsed = readJson(join(repoRoot(import.meta.url), "install", "retired.json"), { files: [] });
|
|
67
|
+
return Array.isArray(parsed?.files) ? parsed : { files: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
const CORE_ITEMS = [
|
|
56
71
|
"src",
|
|
57
72
|
"catalog",
|
|
@@ -896,9 +911,10 @@ async function main() {
|
|
|
896
911
|
// --- Nettoyage de l'installation precedente ------------------------------
|
|
897
912
|
// Une mise a jour ne peut pas laisser derriere elle des skills renommes, des
|
|
898
913
|
// plugins retires ou des commandes devenues obsoles : ils restent charges au
|
|
899
|
-
// prochain demarrage. L'effacement
|
|
900
|
-
//
|
|
901
|
-
//
|
|
914
|
+
// prochain demarrage. L'effacement s'appuie sur deux preuves de propriete : le
|
|
915
|
+
// manifeste d'installation precedent, et pour les fichiers anterieurs a ce
|
|
916
|
+
// manifeste, l'empreinte publiee dans `retired.json`. Un fichier pose par un
|
|
917
|
+
// pack, par l'utilisateur, ou modifie a la main n'est jamais retenu.
|
|
902
918
|
ui.step("Cleanup previous install");
|
|
903
919
|
const ownedRoots = [
|
|
904
920
|
{ dest: skillsDir, src: join(home, "skills") },
|
|
@@ -906,9 +922,11 @@ async function main() {
|
|
|
906
922
|
{ dest: join(configDir, "agent"), src: join(home, "adapters", "opencode", "agent") },
|
|
907
923
|
{ dest: join(configDir, "commands"), src: join(home, "adapters", "opencode", "commands") }
|
|
908
924
|
];
|
|
909
|
-
const
|
|
925
|
+
const retired = readRetired();
|
|
926
|
+
const cleanup = cleanupPreviousInstall({ home, configDir, roots: ownedRoots, clean, dryRun, stepFailure, retired });
|
|
910
927
|
for (const path of cleanup.removed) note(` removed: ${relative(home, path)}`);
|
|
911
928
|
for (const path of cleanup.stale ?? []) note(` would remove: ${relative(home, path)}`);
|
|
929
|
+
for (const path of cleanup.kept ?? []) note(` kept (modified since install): ${relative(home, path)}`);
|
|
912
930
|
if (cleanup.removed.length > 0) {
|
|
913
931
|
// L'index tient encore la liste des skills supprimes : on le rejoue une
|
|
914
932
|
// fois, sinon le prochain routage classifiera vers un skill parti.
|
package/install/lib.d.mts
CHANGED
|
@@ -100,6 +100,8 @@ export interface CleanupResult {
|
|
|
100
100
|
removed: string[];
|
|
101
101
|
/** Present uniquement en mode a blanc : ce qui partirait. */
|
|
102
102
|
stale?: string[];
|
|
103
|
+
/** Fichiers retires mais retouches depuis : jamais supprimes, seulement dits. */
|
|
104
|
+
kept: string[];
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
export declare function withinDir(value: string, dir: string): boolean;
|
|
@@ -108,6 +110,29 @@ export declare function staleOwnedPaths(
|
|
|
108
110
|
roots: OwnedRoot[],
|
|
109
111
|
exists?: (path: string) => boolean,
|
|
110
112
|
): string[];
|
|
113
|
+
|
|
114
|
+
// --- Fichiers retires du paquet -------------------------------------------
|
|
115
|
+
export interface RetiredFile {
|
|
116
|
+
/** Chemin relatif au home, tel que l'ancienne version l'installait. */
|
|
117
|
+
path: string;
|
|
118
|
+
/** Version qui a cesse de le livrer. */
|
|
119
|
+
version: string;
|
|
120
|
+
/** sha256 du contenu publie, fins de ligne normalisees en LF. */
|
|
121
|
+
sha256: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RetiredInventory {
|
|
125
|
+
files: RetiredFile[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Empreinte d'un contenu, `\r\n` ramene a `\n` avant hachage. */
|
|
129
|
+
export declare function normalisedSha256(content: string | Buffer | Uint8Array): string;
|
|
130
|
+
export declare function retiredOwnedPaths(
|
|
131
|
+
retired: RetiredInventory | null | undefined,
|
|
132
|
+
home: string,
|
|
133
|
+
readFile?: (path: string) => Buffer,
|
|
134
|
+
exists?: (path: string) => boolean,
|
|
135
|
+
): { removed: string[]; kept: string[] };
|
|
111
136
|
export declare function cleanupPreviousInstall(options: {
|
|
112
137
|
home: string;
|
|
113
138
|
configDir: string;
|
|
@@ -115,6 +140,7 @@ export declare function cleanupPreviousInstall(options: {
|
|
|
115
140
|
clean: boolean;
|
|
116
141
|
dryRun?: boolean;
|
|
117
142
|
stepFailure?: string | null;
|
|
143
|
+
retired?: RetiredInventory | null;
|
|
118
144
|
}): CleanupResult;
|
|
119
145
|
export declare function detectedHarnesses(dirs: Record<string, string>, whichFn?: (cmd: string) => boolean): string[];
|
|
120
146
|
/**
|
package/install/lib.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
@@ -306,14 +307,58 @@ export function staleOwnedPaths(created, roots, exists = existsSync) {
|
|
|
306
307
|
return [...new Set(out)];
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Empreinte d'un fichier, fins de ligne normalisees en LF.
|
|
312
|
+
*
|
|
313
|
+
* Le paquet est livre en LF, l'installation ecrit en CRLF sous Windows : sans
|
|
314
|
+
* normalisation, le meme fichier ne se reconnait plus lui-meme d'une machine a
|
|
315
|
+
* l'autre. C'est cette empreinte, et pas les octets bruts, qui sert de preuve
|
|
316
|
+
* de propriete aux fichiers retires.
|
|
317
|
+
*/
|
|
318
|
+
export function normalisedSha256(content) {
|
|
319
|
+
return createHash("sha256").update(content.toString("utf8").replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Fichiers retires du paquet et encore presents dans le home.
|
|
324
|
+
*
|
|
325
|
+
* Le manifeste ne peut pas les connaitre : un fichier copie par une version
|
|
326
|
+
* anterieure a l'invention du champ `created` n'y figure pas, et rien ne le
|
|
327
|
+
* reference plus cote source. La preuve de propriete devient donc le contenu :
|
|
328
|
+
* l'entree ne part que si le fichier sur disque porte exactement l'empreinte
|
|
329
|
+
* publiee dans `retired.json`. Un fichier modifie a la main ne correspond plus
|
|
330
|
+
* et n'est jamais supprime ; il ressort dans `kept` pour etre dit a l'utilisateur.
|
|
331
|
+
*/
|
|
332
|
+
export function retiredOwnedPaths(retired, home, readFile = readFileSync, exists = existsSync) {
|
|
333
|
+
const removed = [];
|
|
334
|
+
const kept = [];
|
|
335
|
+
for (const entry of retired?.files ?? []) {
|
|
336
|
+
if (!entry || typeof entry.path !== "string" || typeof entry.sha256 !== "string") continue;
|
|
337
|
+
if (entry.path.length === 0 || entry.path.startsWith("..")) continue;
|
|
338
|
+
const target = join(home, entry.path);
|
|
339
|
+
if (!exists(target)) continue;
|
|
340
|
+
let digest;
|
|
341
|
+
try {
|
|
342
|
+
digest = normalisedSha256(readFile(target));
|
|
343
|
+
} catch {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (digest === entry.sha256.toLowerCase()) removed.push(target);
|
|
347
|
+
else kept.push(target);
|
|
348
|
+
}
|
|
349
|
+
return { removed: [...new Set(removed)], kept: [...new Set(kept)] };
|
|
350
|
+
}
|
|
351
|
+
|
|
309
352
|
/**
|
|
310
353
|
* Decide et applique le nettoyage de l'installation precedente.
|
|
311
354
|
*
|
|
312
|
-
* Renvoie `{ status, detail, removed }`, `status` etant la valeur attendue
|
|
313
|
-
* ui.finishStep ("done" ou "skip").
|
|
314
|
-
* d'installation
|
|
315
|
-
*
|
|
316
|
-
*
|
|
355
|
+
* Renvoie `{ status, detail, removed, kept }`, `status` etant la valeur attendue
|
|
356
|
+
* par ui.finishStep ("done" ou "skip"). Deux preuves de propriete, selon l'age
|
|
357
|
+
* du fichier : le manifeste d'installation (`created`, `backups`) pour ce que
|
|
358
|
+
* les versions recentes ont ecrit, et l'empreinte de `retired.json` pour ce que
|
|
359
|
+
* les versions anterieures ont pose sans jamais l'enregistrer. Un fichier de
|
|
360
|
+
* pack, de l'utilisateur, ou modifie a la main ne satisfait ni l'une ni l'autre
|
|
361
|
+
* et reste en place. Cas de sortie :
|
|
317
362
|
* - `--clean` absent, ou echec d'une etape precedente : skip, rien ne bouge ;
|
|
318
363
|
* - rien de retire : done ;
|
|
319
364
|
* - au moins un fichier retire : done, manifeste mis a jour.
|
|
@@ -321,12 +366,20 @@ export function staleOwnedPaths(created, roots, exists = existsSync) {
|
|
|
321
366
|
* Vit ici et non dans install.mjs : ce module est sans effet de bord a
|
|
322
367
|
* l'import, l'installateur execute main() au chargement.
|
|
323
368
|
*/
|
|
324
|
-
export function cleanupPreviousInstall({
|
|
369
|
+
export function cleanupPreviousInstall({
|
|
370
|
+
home,
|
|
371
|
+
configDir,
|
|
372
|
+
roots,
|
|
373
|
+
clean,
|
|
374
|
+
dryRun = false,
|
|
375
|
+
stepFailure = null,
|
|
376
|
+
retired = null
|
|
377
|
+
}) {
|
|
325
378
|
if (!clean) {
|
|
326
|
-
return { status: "skip", detail: "pass --clean to remove files from a previous install", removed: [] };
|
|
379
|
+
return { status: "skip", detail: "pass --clean to remove files from a previous install", removed: [], kept: [] };
|
|
327
380
|
}
|
|
328
381
|
if (stepFailure) {
|
|
329
|
-
return { status: "skip", detail: `an earlier step failed (${stepFailure}), cleanup aborted`, removed: [] };
|
|
382
|
+
return { status: "skip", detail: `an earlier step failed (${stepFailure}), cleanup aborted`, removed: [], kept: [] };
|
|
330
383
|
}
|
|
331
384
|
const manifest = loadManifest(home);
|
|
332
385
|
// Deux preuves de propriete, pas une. `created` ne liste que les fichiers
|
|
@@ -338,15 +391,18 @@ export function cleanupPreviousInstall({ home, configDir, roots, clean, dryRun =
|
|
|
338
391
|
...(manifest.created ?? []),
|
|
339
392
|
...(manifest.backups ?? []).map((entry) => (entry && entry.path) || "").filter((path) => path.length > 0)
|
|
340
393
|
];
|
|
341
|
-
const
|
|
394
|
+
const retiredScan = retiredOwnedPaths(retired, home);
|
|
395
|
+
const stale = [...new Set([...staleOwnedPaths(owned, roots), ...retiredScan.removed])];
|
|
396
|
+
const kept = retiredScan.kept.filter((item) => !stale.includes(item));
|
|
342
397
|
if (stale.length === 0) {
|
|
343
|
-
return { status: "done", detail: "no leftover file from a previous install", removed: [] };
|
|
398
|
+
return { status: "done", detail: "no leftover file from a previous install", removed: [], kept };
|
|
344
399
|
}
|
|
345
400
|
if (dryRun) {
|
|
346
401
|
return {
|
|
347
402
|
status: "skip",
|
|
348
403
|
detail: `${stale.length} file(s) would be removed by a real run with --clean`,
|
|
349
404
|
removed: [],
|
|
405
|
+
kept,
|
|
350
406
|
stale
|
|
351
407
|
};
|
|
352
408
|
}
|
|
@@ -364,7 +420,7 @@ export function cleanupPreviousInstall({ home, configDir, roots, clean, dryRun =
|
|
|
364
420
|
),
|
|
365
421
|
cleanedAt: new Date().toISOString()
|
|
366
422
|
});
|
|
367
|
-
return { status: "done", detail: `${removed.length} file(s) from a previous install removed`, removed };
|
|
423
|
+
return { status: "done", detail: `${removed.length} file(s) from a previous install removed`, removed, kept };
|
|
368
424
|
}
|
|
369
425
|
|
|
370
426
|
export function pruneEmptyDirs(paths, stops = []) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Files Novahiz used to ship and no longer does. `install --clean` removes one only when the file on disk still carries the published sha256, computed with line endings normalised to LF. A file edited by hand no longer matches and is never deleted; the installer reports it as kept.",
|
|
3
|
+
"files": [
|
|
4
|
+
{
|
|
5
|
+
"path": "install/bootstrap.mjs",
|
|
6
|
+
"version": "0.11.0",
|
|
7
|
+
"sha256": "d4f90e6d091844b06590de1c7d201cc9bac2f2bd9f548f3c93b2bcfc68bd646c"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "install/install.sh",
|
|
11
|
+
"version": "0.11.0",
|
|
12
|
+
"sha256": "9b2e7073f83c3aa9c0e9b269d8bec9187c22f0faebbcf0e71c2c74fb596dc2de"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "install/install.ps1",
|
|
16
|
+
"version": "0.11.0",
|
|
17
|
+
"sha256": "6b44ef53bc9f0761c84a27776461c4c79683d832af037cc9fbbe82ec8dc7fb02"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"path": "bin/veille-mensuelle.ps1",
|
|
21
|
+
"version": "0.11.0",
|
|
22
|
+
"sha256": "4c88aeb86e274b4d5c2ddfd513e1c152189823e66959bbd578669277e4ffaabc"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -39,7 +39,10 @@ function runSync(): void {
|
|
|
39
39
|
const home = homeDir();
|
|
40
40
|
const cli = join(home, "src", "cli.ts");
|
|
41
41
|
if (existsSync(cli)) {
|
|
42
|
-
|
|
42
|
+
// src/cli.ts est du TypeScript : Node ne le lit pas nativement. On ajoute
|
|
43
|
+
// --experimental-strip-types pour pouvoir l'exécuter depuis le home,
|
|
44
|
+
// qui ne contient pas le build dist/ (réservé au paquet publié).
|
|
45
|
+
spawnSync(process.execPath, ["--experimental-strip-types", cli, "sync"], {
|
|
43
46
|
encoding: "utf8",
|
|
44
47
|
stdio: "inherit",
|
|
45
48
|
env: { ...process.env, NOVAHIZ_HOME: home }
|