novahiz 0.10.0 → 0.11.0
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 +44 -0
- package/README.md +16 -11
- package/bin/novahiz.mjs +41 -15
- package/dist/cli.js +6 -4
- package/dist/commands/inspect.js +30 -4
- package/dist/deps.js +47 -3
- package/dist/exec.js +1 -1
- package/docs/INSTALL.md +28 -3
- package/docs/PROVIDERS.md +3 -3
- package/install/install.mjs +103 -29
- package/install/lib.d.mts +29 -1
- package/install/lib.mjs +67 -11
- package/package.json +4 -5
- package/src/cli.ts +6 -4
- package/src/commands/inspect.ts +30 -6
- package/src/deps.ts +47 -3
- package/src/exec.ts +1 -1
- package/bin/veille-mensuelle.ps1 +0 -26
- package/install/bootstrap.mjs +0 -324
- package/install/install.ps1 +0 -3
- package/install/install.sh +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ 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.0] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **`npm install -g novahiz` is the whole install instruction** - the installer
|
|
13
|
+
now has two modes and two moments. `--core` is the local half: home, skills,
|
|
14
|
+
plugin, agent, commands, config, catalog. No network, no `sudo`, so it is safe
|
|
15
|
+
from a postinstall, a CI job or an agent with no terminal. `--extras` is the
|
|
16
|
+
network/system half (providers and toolchain, global MCP servers, docs index
|
|
17
|
+
seed, Obsidian vault, npm refresh) and is replayed at the first real `novahiz`
|
|
18
|
+
call. Without a flag the installer still does everything in one pass, so
|
|
19
|
+
`novahiz-install` keeps its historical behaviour.
|
|
20
|
+
- **The first `novahiz` run finishes the setup** - npm 11.16+ blocks install
|
|
21
|
+
scripts unless `allowScripts` covers the package, and a global install has no
|
|
22
|
+
`package.json` to record that approval in. `bin/novahiz.mjs` therefore runs
|
|
23
|
+
`--core` when no manifest exists, then `--extras` in the same invocation, and
|
|
24
|
+
clears `extrasPending`. `NOVAHIZ_NO_EXTRAS=1` and `NOVAHIZ_AUTOINSTALL=0`
|
|
25
|
+
remain the opt-outs.
|
|
26
|
+
- **Linux and macOS compatibility** - `which()` and `commandExists()` scan
|
|
27
|
+
`PATH` (plus `PATHEXT` on Windows) instead of spawning `which`/`where`, which
|
|
28
|
+
Debian minimal and Alpine do not ship. The opencode `shell` setting follows
|
|
29
|
+
`$SHELL` on macOS/Linux (zsh by default on macOS) instead of hardcoding
|
|
30
|
+
`bash`, and keeps the `pwsh`/`powershell` probe on Windows. CI gains an
|
|
31
|
+
`install` job that runs the core installer and the resulting CLI on ubuntu,
|
|
32
|
+
macos and windows.
|
|
33
|
+
- **Nothing privileged is executed any more** - the core toolchain bootstrap no
|
|
34
|
+
longer spawns `sudo apt-get install` or `sh -c "curl … | sh"`. Both are
|
|
35
|
+
printed as the exact manual command (`manual core python - run: …`), so a CI
|
|
36
|
+
job or an agent can no longer hang on a password prompt. `brew` and `winget`
|
|
37
|
+
(structured argv, no privilege escalation) stay executable.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
|
|
41
|
+
- **Four redundant entry points** - `install/bootstrap.mjs`,
|
|
42
|
+
`install/install.sh`, `install/install.ps1` and `bin/veille-mensuelle.ps1`,
|
|
43
|
+
together with the `novahiz-bootstrap` bin and the `bootstrap` / `bootstrap:dry`
|
|
44
|
+
scripts. `novahiz-install` is the single installer; `npm run setup:core` and
|
|
45
|
+
`npm run setup:extras` cover the two modes.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **`novahiz --version` printed the usage** - the `command === undefined` branch
|
|
50
|
+
ran before the `--version` flag check.
|
|
51
|
+
|
|
8
52
|
## [0.10.0] - 2026-10-22
|
|
9
53
|
|
|
10
54
|
### 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.
|
|
7
|
+
> **Version 0.11.0** — `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
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
@@ -41,28 +41,33 @@
|
|
|
41
41
|
|
|
42
42
|
## Démarrage rapide
|
|
43
43
|
|
|
44
|
-
### Option 1 —
|
|
44
|
+
### Option 1 — npm (recommandé)
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
npm install -g
|
|
47
|
+
npm install -g novahiz
|
|
48
|
+
novahiz doctor
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
`npm install -g` pose le CLI. Le premier `novahiz` lancé termine le reste, en deux passes : le **core** (skills, plugin, agent, commands, config, catalogue — local, sans réseau ni sudo), puis les **extras** (providers et toolchain, MCP globaux dont Playwright, vault Obsidian, rafraîchissement npm). Une seule fois.
|
|
51
52
|
|
|
52
|
-
**Pourquoi
|
|
53
|
+
**Pourquoi au premier lancement et pas dans le postinstall ?** npm 11.16+ ne lance un script de lifecycle que si le paquet y est explicitement autorisé, et une installation globale n'a pas de `package.json` où l'inscrire : npm avertit (`npm warn allow-scripts novahiz`) et saute le script. Le CLI fait donc le même travail à son premier appel, ce qui fonctionne quelle que soit la version de npm. Pour que tout se passe pendant le `npm install` :
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
|
-
|
|
56
|
-
npm config set allow-scripts=novahiz --location=user
|
|
57
|
-
|
|
58
|
-
# paquet déjà installé sans configuration : une commande suffit
|
|
59
|
-
novahiz-install --yes
|
|
56
|
+
npm install -g --allow-scripts=novahiz novahiz
|
|
60
57
|
```
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
Variantes :
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
NOVAHIZ_NO_EXTRAS=1 novahiz doctor # core seulement, aucun accès réseau
|
|
63
|
+
NOVAHIZ_AUTOINSTALL=0 novahiz doctor # ni core ni extras
|
|
64
|
+
novahiz-install --extras # rejouer les extras plus tard
|
|
65
|
+
```
|
|
63
66
|
|
|
64
67
|
> Piège : `npm install -g --allow-scripts=novahiz` **sans** le nom du paquet installe le dossier courant et échoue avec `ENOENT: package.json`. Le nom du paquet est obligatoire : `npm install -g --allow-scripts=novahiz novahiz`.
|
|
65
68
|
|
|
69
|
+
> Linux et macOS : les commandes système qui demandent `sudo` ou qui exécutent un script téléchargé ne sont jamais lancées. L'installeur affiche la ligne exacte (`sudo apt-get install -y python3`, `brew install uv`, …) et vous décidez. C'est ce qui rend l'installation utilisable sans terminal interactif.
|
|
70
|
+
|
|
66
71
|
> Linux : le serveur Playwright MCP s'exécute sous WebKit (règle maison : Chromium interdit). WebKit exige ses dépendances système : si le navigateur ne démarre pas, `npx playwright install-deps webkit` (sudo requis sur Debian/Ubuntu).
|
|
67
72
|
|
|
68
73
|
Puis vérifiez :
|
package/bin/novahiz.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
@@ -21,40 +21,66 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
21
21
|
const root = join(here, "..");
|
|
22
22
|
const underNodeModules = root.replace(/\\/g, "/").toLowerCase().includes("/node_modules/");
|
|
23
23
|
const dist = join(root, "dist", "cli.js");
|
|
24
|
-
const src = join(root, "src", "cli.ts");
|
|
24
|
+
const src = join(root, "src", "cli.ts"); console.error("DEBUG", JSON.stringify({underNodeModules, root, auto: process.env.NOVAHIZ_AUTOINSTALL, noextras: process.env.NOVAHIZ_NO_EXTRAS}));
|
|
25
25
|
|
|
26
26
|
// npm 11.16+ blocks install scripts unless `allowScripts` covers them, and a
|
|
27
27
|
// global install (`npm install -g`) has no package.json to record that
|
|
28
28
|
// approval in — so the postinstall can be skipped silently and the machine is
|
|
29
|
-
// left half configured.
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
29
|
+
// left half configured. The first real `novahiz` run therefore finishes the
|
|
30
|
+
// job, in two passes:
|
|
31
|
+
// 1. no install manifest -> install the core (skills, plugin, agent, config);
|
|
32
|
+
// 2. manifest says extrasPending -> run the network/system extras once
|
|
33
|
+
// (providers, global MCP servers, Obsidian vault, npm refresh).
|
|
34
|
+
// Opt out with NOVAHIZ_AUTOINSTALL=0 (both passes) or NOVAHIZ_NO_EXTRAS=1
|
|
35
|
+
// (extras only). A repo checkout keeps its explicit `novahiz-install`.
|
|
36
|
+
function readManifest(path) {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
if (underNodeModules && process.env.NOVAHIZ_AUTOINSTALL !== "0") {
|
|
35
46
|
const args = process.argv.slice(2);
|
|
36
47
|
const infoOnly = args.length > 0 && args.every((arg) => ["-v", "-V", "--version", "--help", "-h"].includes(arg));
|
|
37
48
|
const home = process.env.NOVAHIZ_HOME ? resolve(process.env.NOVAHIZ_HOME) : join(homedir(), ".config", "novahiz");
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
);
|
|
43
|
-
const setup = spawnSync(process.execPath, [
|
|
49
|
+
const manifestPath = join(home, ".novahiz-install.json");
|
|
50
|
+
const manifest = readManifest(manifestPath);
|
|
51
|
+
const installer = join(root, "install", "install.mjs");
|
|
52
|
+
const runInstaller = (mode, reason) => {
|
|
53
|
+
process.stderr.write(`novahiz: ${reason}\n`);
|
|
54
|
+
const setup = spawnSync(process.execPath, [installer, "--yes", mode], {
|
|
44
55
|
stdio: "inherit",
|
|
45
56
|
cwd: root,
|
|
46
57
|
env: process.env
|
|
47
58
|
});
|
|
48
59
|
if (setup.status !== 0) {
|
|
49
|
-
process.stderr.write(
|
|
60
|
+
process.stderr.write(`novahiz: setup (${mode}) did not complete; re-run it with \`novahiz-install ${mode}\`.\n`);
|
|
61
|
+
}
|
|
62
|
+
return setup.status === 0;
|
|
63
|
+
};
|
|
64
|
+
if (!infoOnly && manifest === null) {
|
|
65
|
+
const coreOk = runInstaller("--core", "first run - installing the core (the npm postinstall was skipped or did not finish).");
|
|
66
|
+
// Meme lancement, deux passes : le premier appel doit tout laisser pret, pas
|
|
67
|
+
// seulement le core. Les extras restent optionnels via NOVAHIZ_NO_EXTRAS=1.
|
|
68
|
+
if (coreOk && process.env.NOVAHIZ_NO_EXTRAS !== "1") {
|
|
69
|
+
runInstaller("--extras", "installing the extras (providers, global MCP servers, Obsidian vault). One time only; skip with NOVAHIZ_NO_EXTRAS=1.");
|
|
50
70
|
}
|
|
71
|
+
} else if (!infoOnly && manifest?.extrasPending === true && process.env.NOVAHIZ_NO_EXTRAS !== "1") {
|
|
72
|
+
runInstaller("--extras", "finishing the setup installed earlier (providers, global MCP servers, Obsidian vault). One time only; skip with NOVAHIZ_NO_EXTRAS=1.");
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
let target;
|
|
55
77
|
if (underNodeModules) {
|
|
56
78
|
if (!existsSync(dist)) {
|
|
57
|
-
process.stderr.write(
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
"novahiz: dist/cli.js is missing from this package copy.\n" +
|
|
81
|
+
"novahiz: npm blocked the `prepare` script (allow-scripts). Reinstall with:\n" +
|
|
82
|
+
"novahiz: npm install -g --allow-scripts=novahiz novahiz\n"
|
|
83
|
+
);
|
|
58
84
|
process.exit(1);
|
|
59
85
|
}
|
|
60
86
|
target = dist;
|
package/dist/cli.js
CHANGED
|
@@ -121,14 +121,16 @@ async function main(argv) {
|
|
|
121
121
|
process.env.NOVAHIZ_HOME = resolve(expandHome(parsed.flags.home));
|
|
122
122
|
}
|
|
123
123
|
const command = parsed.positionals[0];
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
124
|
+
// `--version` sans commande tombe sur `command === undefined` et affichait
|
|
125
|
+
// l'aide : le test du drapeau doit passer avant celui de la commande.
|
|
128
126
|
if (parsed.flags.version || parsed.flags.v) {
|
|
129
127
|
printVersion();
|
|
130
128
|
return;
|
|
131
129
|
}
|
|
130
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
131
|
+
usage();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
132
134
|
switch (command) {
|
|
133
135
|
case "init":
|
|
134
136
|
return commandInit(parsed);
|
package/dist/commands/inspect.js
CHANGED
|
@@ -6,10 +6,10 @@ import { loadCatalog, loadInstalledSkills, persistCatalog, scanSkills, writeCata
|
|
|
6
6
|
import { rankSkills } from "../relevance.js";
|
|
7
7
|
import { buildMcpEntries, enabledProviders, installCommands, pendingInstallCommands, updateCommands } from "../providers.js";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
import { bootstrapFor, coreBootstrap, dependencyReport, missingPrerequisites, reloadPathFromRegistry, toolchainStatus } from "../deps.js";
|
|
9
|
+
import { bootstrapFor, commandExists, coreBootstrap, dependencyReport, manualCoreBootstrap, missingPrerequisites, reloadPathFromRegistry, toolchainStatus } from "../deps.js";
|
|
10
10
|
import { classify } from "../classify.js";
|
|
11
11
|
import { isContinuation, isValidSessionId, readSessionClassification, writeSessionClassification } from "../session.js";
|
|
12
|
-
import { runScript } from "../exec.js";
|
|
12
|
+
import { bootstrapBinAllowed, runScript } from "../exec.js";
|
|
13
13
|
import { activeTask, buildWorkPackets, getTask } from "../ledger.js";
|
|
14
14
|
export function commandCheck() {
|
|
15
15
|
const root = NovahizHome();
|
|
@@ -313,12 +313,27 @@ export function commandDeps(parsed) {
|
|
|
313
313
|
// --core-only: the installer's path when the user declined providers —
|
|
314
314
|
// system dependencies (toolchain + npm health) still install automatically.
|
|
315
315
|
const coreOnly = flagOn(parsed, "core-only");
|
|
316
|
-
const corePlan = coreBootstrap()
|
|
316
|
+
const corePlan = coreBootstrap()
|
|
317
|
+
.filter((entry) => report.toolchain.find((tool) => tool.id === entry.id)?.ok === false)
|
|
318
|
+
.map((entry) => ({
|
|
319
|
+
...entry,
|
|
320
|
+
command: entry.argv.join(" "),
|
|
321
|
+
// Un bootstrap sudo ou un script telecharge execute par un shell n'est
|
|
322
|
+
// jamais lance : affiche, il reste une etape manuelle.
|
|
323
|
+
manual: manualCoreBootstrap(entry.argv) || !commandExists(entry.argv[0])
|
|
324
|
+
}));
|
|
317
325
|
const npmPlan = report.npm.ok ? [] : [{ step: "npm", missing: report.npm.missing, note: report.npm.note }];
|
|
318
326
|
const bootstrapPlan = missingPrerequisites(spec).map((entry) => {
|
|
319
327
|
const bootstrap = bootstrapFor(entry.provider);
|
|
320
328
|
return bootstrap && bootstrap.length > 0
|
|
321
|
-
? {
|
|
329
|
+
? {
|
|
330
|
+
provider: entry.provider.id,
|
|
331
|
+
step: "bootstrap",
|
|
332
|
+
command: bootstrap.join(" "),
|
|
333
|
+
// Hors allowlist de runScript (sudo, brew, winget, shell) : la
|
|
334
|
+
// commande est affichee, pas executee.
|
|
335
|
+
manual: !bootstrapBinAllowed(bootstrap[0])
|
|
336
|
+
}
|
|
322
337
|
: { provider: entry.provider.id, step: "bootstrap", note: `missing ${entry.missing.join(", ")}` };
|
|
323
338
|
});
|
|
324
339
|
const installed = new Set(scanSkills(spec, []).map((skill) => skill.id));
|
|
@@ -347,6 +362,11 @@ export function commandDeps(parsed) {
|
|
|
347
362
|
// through a plain spawn: runScript's catalog allowlist deliberately refuses
|
|
348
363
|
// shells and package managers, and this argv never comes from the catalog.
|
|
349
364
|
for (const entry of corePlan) {
|
|
365
|
+
if (entry.manual) {
|
|
366
|
+
process.stdout.write(`manual core ${entry.id} - run: ${entry.command}\n`);
|
|
367
|
+
results.push({ step: "core", tool: entry.id, command: entry.command, ok: false, manual: true });
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
350
370
|
const result = spawnSync(entry.argv[0], entry.argv.slice(1), { encoding: "utf8", windowsHide: true });
|
|
351
371
|
const ok = result.status === 0;
|
|
352
372
|
process.stdout.write(`${ok ? "ok " : "fail"} core ${entry.id}\n`);
|
|
@@ -394,6 +414,12 @@ export function commandDeps(parsed) {
|
|
|
394
414
|
results.push({ provider: entry.provider.id, step: "bootstrap", ok: false, note: `missing ${entry.missing.join(", ")}` });
|
|
395
415
|
continue;
|
|
396
416
|
}
|
|
417
|
+
if (!bootstrapBinAllowed(bootstrap[0])) {
|
|
418
|
+
// sudo, brew, winget, shell : hors de l'allowlist, jamais executes.
|
|
419
|
+
process.stdout.write(`manual bootstrap ${entry.provider.id} - run: ${bootstrap.join(" ")}\n`);
|
|
420
|
+
results.push({ provider: entry.provider.id, step: "bootstrap", command: bootstrap.join(" "), ok: false, manual: true });
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
397
423
|
const result = runScript(bootstrap);
|
|
398
424
|
process.stdout.write(`${result.ok ? "ok " : "fail"} bootstrap ${entry.provider.id}\n`);
|
|
399
425
|
if (!result.ok && result.stderr)
|
package/dist/deps.js
CHANGED
|
@@ -2,10 +2,54 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { expandHome } from "./spec.js";
|
|
5
|
+
// PATH scan plutot qu'un appel a `which`/`where` : ces binaires ne sont pas
|
|
6
|
+
// garantis (Debian minimal, conteneurs, Alpine sans which) et un spawn par
|
|
7
|
+
// verification ajoutait un echec silencieux. Meme semantique, sans dependance.
|
|
5
8
|
export function commandExists(command) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
+
const separator = process.platform === "win32" ? ";" : ":";
|
|
10
|
+
const dirs = String(process.env.PATH ?? process.env.Path ?? process.env.path ?? "")
|
|
11
|
+
.split(separator)
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (dirs.length === 0)
|
|
14
|
+
return false;
|
|
15
|
+
const hasExtension = /\.(exe|cmd|bat|com)$/i.test(command);
|
|
16
|
+
const extensions = process.platform === "win32" && !hasExtension
|
|
17
|
+
? String(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
18
|
+
.split(";")
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
: [""];
|
|
21
|
+
for (const dir of dirs) {
|
|
22
|
+
for (const extension of extensions) {
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(join(dir, command + extension)))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// une entree de PATH invalide ne doit pas casser la detection
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Une commande de bootstrap du toolchain core doit-elle rester manuelle ?
|
|
36
|
+
*
|
|
37
|
+
* Deux familles ne sont jamais executees par l'installateur :
|
|
38
|
+
* - `sudo` : demande un mot de passe sur le terminal. Un postinstall, un job
|
|
39
|
+
* de CI ou un agent n'ont pas de TTY : le spawn attend indefiniment ou
|
|
40
|
+
* echoue sans explication. L'installateur n'eleve jamais ses privileges.
|
|
41
|
+
* - un shell (`sh -c`, `bash -c`) qui execute un script telecharge : c'est ce
|
|
42
|
+
* que l'allowlist de exec.ts refuse pour les argv du catalogue, et un script
|
|
43
|
+
* distant n'est pas verifiable avant execution.
|
|
44
|
+
* La commande exacte est affichee ; l'utilisateur decide de la lancer.
|
|
45
|
+
*/
|
|
46
|
+
export function manualCoreBootstrap(argv) {
|
|
47
|
+
const bin = (argv[0] ?? "")
|
|
48
|
+
.replace(/\\/g, "/")
|
|
49
|
+
.split("/")
|
|
50
|
+
.pop()
|
|
51
|
+
?.toLowerCase() ?? "";
|
|
52
|
+
return ["sudo", "sh", "bash", "zsh", "dash", "curl", "wget"].includes(bin);
|
|
9
53
|
}
|
|
10
54
|
export function checkDependencies(spec) {
|
|
11
55
|
return spec.providers.map((provider) => {
|
package/dist/exec.js
CHANGED
|
@@ -51,7 +51,7 @@ function normalizeBin(bin) {
|
|
|
51
51
|
const base = bin.replace(/\\/g, "/").split("/").pop() ?? bin;
|
|
52
52
|
return base.toLowerCase();
|
|
53
53
|
}
|
|
54
|
-
function bootstrapBinAllowed(bin) {
|
|
54
|
+
export function bootstrapBinAllowed(bin) {
|
|
55
55
|
const name = normalizeBin(bin);
|
|
56
56
|
const withoutExt = name.replace(/\.(exe|cmd|bat)$/i, "");
|
|
57
57
|
return SAFE_BOOTSTRAP_BINS.has(name) || SAFE_BOOTSTRAP_BINS.has(withoutExt);
|
package/docs/INSTALL.md
CHANGED
|
@@ -4,14 +4,32 @@ Novahiz needs Node.js 22.18 or later. There are no runtime dependencies.
|
|
|
4
4
|
|
|
5
5
|
## Quick install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```
|
|
8
|
+
npm install -g novahiz
|
|
9
|
+
novahiz doctor
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`npm install -g` puts the CLI on your PATH. The first real `novahiz` command finishes the setup, in two passes:
|
|
13
|
+
|
|
14
|
+
1. **core** — local only: the Novahiz home, skills, plugin, agent, slash commands, config, catalog. No network, no `sudo`.
|
|
15
|
+
2. **extras** — providers and toolchain, global MCP servers, the Obsidian vault, the npm refresh. This is the part that downloads and installs.
|
|
16
|
+
|
|
17
|
+
npm 11.16 and later refuse to run a package's install scripts unless the package is explicitly allowed, and a global install has no `package.json` to record that approval in. The postinstall may therefore be skipped; the first-run pass covers it. To have the setup happen inside the npm install instead:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npm install -g --allow-scripts=novahiz novahiz
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The two passes run the same code either way. Skip the extras with `NOVAHIZ_NO_EXTRAS=1`, skip both passes with `NOVAHIZ_AUTOINSTALL=0`.
|
|
24
|
+
|
|
25
|
+
### From a checkout
|
|
8
26
|
|
|
9
27
|
```
|
|
10
28
|
git clone https://github.com/novahiz/novahiz ~/.config/novahiz
|
|
11
29
|
node ~/.config/novahiz/install/install.mjs
|
|
12
30
|
```
|
|
13
31
|
|
|
14
|
-
On Windows
|
|
32
|
+
Without a flag the installer does everything in one interactive run, as it always has. On Windows the same command works in PowerShell. If you cloned somewhere else, pass the target home:
|
|
15
33
|
|
|
16
34
|
```
|
|
17
35
|
node /path/to/novahiz/install/install.mjs --home ~/.config/novahiz
|
|
@@ -26,14 +44,19 @@ The installer runs `git`-free and never deletes your files. When run in a termin
|
|
|
26
44
|
5. Builds the catalog with `sync`.
|
|
27
45
|
6. Verifies provider dependencies, and installs them when you confirm or `--install-providers` is set.
|
|
28
46
|
7. Installs the Novahiz agent into `~/.config/opencode/agent/novahiz.md` and the four slash commands into `~/.config/opencode/commands/`.
|
|
47
|
+
8. Installs the global MCP servers and creates or verifies the Obsidian vault at `~/Documents/second-memory` — both are extras, so they run at the first CLI call when the install was core-only.
|
|
48
|
+
|
|
49
|
+
Nothing privileged is ever executed. Commands that need `sudo`, or that pipe a downloaded script into a shell, are printed with the exact line to run; the installer does not ask for a password and does not execute them. This is what makes `--core` safe to run from a postinstall, a CI job, or an agent with no terminal.
|
|
29
50
|
|
|
30
51
|
Restart opencode afterward. The plugin in `~/.config/opencode/plugins/novahiz-plugin.ts` is auto-discovered (confirmed by `opencode debug config`: `file:///…/plugins/novahiz-plugin.ts`, scope local), so it is not listed in `plugin[]` and you do not edit `opencode.jsonc` by hand. See [HARNESSES.md](HARNESSES.md) for the exact paths.
|
|
31
52
|
|
|
32
53
|
## Options
|
|
33
54
|
|
|
55
|
+
- `--core` runs only the local half: home, skills, plugin, agent, commands, config, catalog. No network, no `sudo`. This is what the npm postinstall and the first-run pass use.
|
|
56
|
+
- `--extras` runs only the network/system half: providers and toolchain, global MCP servers, docs index seed, Obsidian vault, npm refresh. Replay it any time with `novahiz-install --extras`.
|
|
34
57
|
- `--home <path>` installs the core to a custom location.
|
|
35
58
|
- `--scope project` targets `./.opencode` instead of the global config.
|
|
36
|
-
- `--dry-run` prints the actions and writes nothing.
|
|
59
|
+
- `--dry-run` prints the actions and writes nothing. The step list tells you which steps the current mode would skip.
|
|
37
60
|
- `--no-skills` skips the skills copy.
|
|
38
61
|
- `--force-skills` recopies a skill even when another scanned root already provides it.
|
|
39
62
|
- `--install-providers` runs the provider dependency bootstrap and install commands.
|
|
@@ -42,6 +65,8 @@ Restart opencode afterward. The plugin in `~/.config/opencode/plugins/novahiz-pl
|
|
|
42
65
|
- `--yes` skips every prompt. Use it in scripts and CI, where there is no terminal to answer.
|
|
43
66
|
- `--interactive` forces the prompts even when the output is not a terminal.
|
|
44
67
|
|
|
68
|
+
`--core` and `--extras` together mean "everything", which is also the behaviour when neither is given.
|
|
69
|
+
|
|
45
70
|
## What it touches
|
|
46
71
|
|
|
47
72
|
The installer configures opencode — the only harness Novahiz supports — merging `skills/` into your opencode skills directory and writing the plugin, the agent, and the slash commands; `--yes` accepts the detected configuration. It skips a skill that already exists in `~/.agents/skills` or another root the catalog scans, because two copies of one skill make the catalog describe one version while the harness loads the other. Pass `--force-skills` to copy anyway. Any file it overwrites is copied first to `<file>.novahiz-bak`, and the list is stored in `.novahiz-install.json`. It never deletes a file it did not create. `node install/uninstall.mjs` restores the backups and removes what the installer created; `--only <dir>` scopes that to one directory, and `--purge` also removes the Novahiz home.
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -51,7 +51,7 @@ MCP servers, with provenance from `catalog/providers.json`:
|
|
|
51
51
|
|
|
52
52
|
`novahiz-scheduler` is the local scheduler (own cron parser, atomic JSON store, stdio MCP server) written in this repository under `mcp/clepsydre`. It has zero npm dependency and no install step: the `command` points straight at `index.mjs`. It replaced the former AGPL `mcp-cron` package and the `scheduler-mcp` venv clone — no third-party scheduler code is used or vendored.
|
|
53
53
|
|
|
54
|
-
`security` (`security-mcp`) has been removed and must never be reinstalled — explicit user decision, 2026-10-05 ("jamais réinstallés"). The entry is gone from `catalog/providers.json`; `install.mjs`
|
|
54
|
+
`security` (`security-mcp`) has been removed and must never be reinstalled — explicit user decision, 2026-10-05 ("jamais réinstallés"). The entry is gone from `catalog/providers.json`; `install.mjs` keeps its MCP install list empty and purges any trace of `security-mcp` or `mcp-cron` (shim and global package) on every run; `providers.disabled` in `novahiz.config.json` keeps the id out of the registry. The `audit` category is served by `novahiz-scan` (house, clean-room).
|
|
55
55
|
|
|
56
56
|
`dart` requires the Dart SDK on `PATH` (`requires: ["dart"]`). Without it the MCP entry still registers, but the server fails to start; disable it or install the SDK.
|
|
57
57
|
|
|
@@ -139,7 +139,7 @@ Each provider declares its prerequisites in `requires` (the executable it needs)
|
|
|
139
139
|
- `npx` based providers need `npx`, which ships with Node.
|
|
140
140
|
- `dart` needs the Dart SDK on `PATH` (`dart --version`). Flutter installs ship it.
|
|
141
141
|
|
|
142
|
-
`Novahiz deps` checks every prerequisite and reports what is missing. `Novahiz deps --install` prints the plan (bootstrap and install steps) and runs nothing; with `--yes` it first bootstraps a missing prerequisite
|
|
142
|
+
`Novahiz deps` checks every prerequisite and reports what is missing. `Novahiz deps --install` prints the plan (bootstrap and install steps) and runs nothing; with `--yes` it first bootstraps a missing prerequisite when the command is a structured package-manager argv (`brew`, `winget`), then runs each provider's install command. Commands that need `sudo` or pipe a downloaded script into a shell are printed as `manual … - run: …` and never executed: the installer has no terminal to answer a password prompt. The installer runs the check on every install and, when `providers.autoInstall` is true or `--install-providers` is passed, runs the installs with `--yes` too.
|
|
143
143
|
|
|
144
144
|
## Troubleshooting
|
|
145
145
|
|
|
@@ -165,6 +165,6 @@ Confirm the tool count (8 `lodestone_*` tools) with the `tools/list` call above,
|
|
|
165
165
|
- `Novahiz providers` lists providers, optionally by `--category` or a query.
|
|
166
166
|
- `Novahiz providers --mcp-json` prints the MCP entry map.
|
|
167
167
|
- `Novahiz providers --install [--yes]` plans the official install commands; `--yes` executes them.
|
|
168
|
-
- `Novahiz deps [--install] [--yes]` checks prerequisites and plans or runs the bootstrap/install commands.
|
|
168
|
+
- `Novahiz deps [--install] [--yes]` checks prerequisites and plans or runs the bootstrap/install commands (privileged or downloaded-script bootstraps are printed, never run).
|
|
169
169
|
- MCP `novahiz_providers` and `novahiz_deps` expose the list and the dependency status over stdio.
|
|
170
170
|
- `Novahiz report` lists the provider ids.
|