novahiz 0.10.0 → 0.11.1

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 CHANGED
@@ -5,6 +5,61 @@ 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.1] - 2026-10-22
9
+
10
+ ### Fixed
11
+
12
+ - **`novahiz` no longer writes a debug line on every call** - 0.11.0 shipped
13
+ `bin/novahiz.mjs` with a leftover `console.error("DEBUG", ...)` that printed
14
+ the absolute home path and the auto-install state to stderr on each
15
+ invocation. The line is gone, and `tests/bin-entrypoint.test.ts` fails from
16
+ now on if the published entry point writes anything to stderr for
17
+ `--version`.
18
+
19
+ ## [0.11.0] - 2026-10-22
20
+
21
+ ### Changed
22
+
23
+ - **`npm install -g novahiz` is the whole install instruction** - the installer
24
+ now has two modes and two moments. `--core` is the local half: home, skills,
25
+ plugin, agent, commands, config, catalog. No network, no `sudo`, so it is safe
26
+ from a postinstall, a CI job or an agent with no terminal. `--extras` is the
27
+ network/system half (providers and toolchain, global MCP servers, docs index
28
+ seed, Obsidian vault, npm refresh) and is replayed at the first real `novahiz`
29
+ call. Without a flag the installer still does everything in one pass, so
30
+ `novahiz-install` keeps its historical behaviour.
31
+ - **The first `novahiz` run finishes the setup** - npm 11.16+ blocks install
32
+ scripts unless `allowScripts` covers the package, and a global install has no
33
+ `package.json` to record that approval in. `bin/novahiz.mjs` therefore runs
34
+ `--core` when no manifest exists, then `--extras` in the same invocation, and
35
+ clears `extrasPending`. `NOVAHIZ_NO_EXTRAS=1` and `NOVAHIZ_AUTOINSTALL=0`
36
+ remain the opt-outs.
37
+ - **Linux and macOS compatibility** - `which()` and `commandExists()` scan
38
+ `PATH` (plus `PATHEXT` on Windows) instead of spawning `which`/`where`, which
39
+ Debian minimal and Alpine do not ship. The opencode `shell` setting follows
40
+ `$SHELL` on macOS/Linux (zsh by default on macOS) instead of hardcoding
41
+ `bash`, and keeps the `pwsh`/`powershell` probe on Windows. CI gains an
42
+ `install` job that runs the core installer and the resulting CLI on ubuntu,
43
+ macos and windows.
44
+ - **Nothing privileged is executed any more** - the core toolchain bootstrap no
45
+ longer spawns `sudo apt-get install` or `sh -c "curl … | sh"`. Both are
46
+ printed as the exact manual command (`manual core python - run: …`), so a CI
47
+ job or an agent can no longer hang on a password prompt. `brew` and `winget`
48
+ (structured argv, no privilege escalation) stay executable.
49
+
50
+ ### Removed
51
+
52
+ - **Four redundant entry points** - `install/bootstrap.mjs`,
53
+ `install/install.sh`, `install/install.ps1` and `bin/veille-mensuelle.ps1`,
54
+ together with the `novahiz-bootstrap` bin and the `bootstrap` / `bootstrap:dry`
55
+ scripts. `novahiz-install` is the single installer; `npm run setup:core` and
56
+ `npm run setup:extras` cover the two modes.
57
+
58
+ ### Fixed
59
+
60
+ - **`novahiz --version` printed the usage** - the `command === undefined` branch
61
+ ran before the `--version` flag check.
62
+
8
63
  ## [0.10.0] - 2026-10-22
9
64
 
10
65
  ### 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.9.0** — publiée sur npm (`npm install -g --allow-scripts=novahiz novahiz` installe et configure tout : core, plugin, agent, skills packs et Playwright MCP). `CHANGELOG.md` et `scripts/` sont dans le tarball.
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 — En une ligne (recommandé)
44
+ ### Option 1 — npm (recommandé)
45
45
 
46
46
  ```bash
47
- npm install -g --allow-scripts=novahiz novahiz
47
+ npm install -g novahiz
48
+ novahiz doctor
48
49
  ```
49
50
 
50
- Une commande, tout est en place : CLI, core (skills, plugin, agent, commands), skills packs et Playwright MCP.
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 ce drapeau ?** npm 11.16+ ne considère plus les scripts de lifecycle (`postinstall`) comme autorisés tant qu'ils ne figurent pas dans `allowScripts`, et une installation globale n'a pas de `package.json` où l'inscrire : npm affiche alors `npm warn allow-scripts novahiz (...)`, et selon la version il saute le script — auquel cas `novahiz-install` n'a jamais tourné et rien n'est configuré. Le drapeau autorise explicitement ce paquet. Replis :
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
- # valeur permanente : `npm install -g novahiz` marche ensuite sans drapeau
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
- npm antérieur à 11.16 exécute les scripts par défaut : `npm install -g novahiz` suffit. Et si le blocage a eu lieu quand même, le premier `novahiz` lancé termine l'installation lui-même (désactivable avec `NOVAHIZ_AUTOINSTALL=0`).
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";
@@ -26,35 +26,61 @@ const src = join(root, "src", "cli.ts");
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. Finishing the job here means the package works
30
- // whether or not npm let the lifecycle script through: the first real `novahiz`
31
- // run installs the core (skills, plugin, agent, MCP), then behaves normally.
32
- // Opt out with NOVAHIZ_AUTOINSTALL=0. Only a package installed from npm is
33
- // touched; a repo checkout keeps its explicit `novahiz-install`.
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
- if (!infoOnly && !existsSync(join(home, ".novahiz-install.json"))) {
39
- process.stderr.write(
40
- "novahiz: npm skipped the postinstall script (allow-scripts), so the setup has not run yet.\n" +
41
- "novahiz: installing now — re-run it any time with `novahiz-install`.\n"
42
- );
43
- const setup = spawnSync(process.execPath, [join(root, "install", "install.mjs"), "--yes"], {
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("novahiz: automatic setup did not complete; run `novahiz-install` to see why.\n");
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("novahiz: dist/cli.js is missing from the installed package. Reinstall: npm install novahiz\n");
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
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
125
- usage();
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);
@@ -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().filter((entry) => report.toolchain.find((tool) => tool.id === entry.id)?.ok === false);
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
- ? { provider: entry.provider.id, step: "bootstrap", command: bootstrap.join(" ") }
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 probe = process.platform === "win32" ? "where" : "which";
7
- const result = spawnSync(probe, [command], { encoding: "utf8", shell: false });
8
- return result.status === 0;
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
- Clone the repository into the Novahiz home and run the installer:
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, the same command works in PowerShell. If you cloned somewhere else, pass the target home:
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` and `bootstrap.mjs` keep their MCP install list empty and purge 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).
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 through its official installer, then runs each provider's install command. 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.
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.