infracensus-collector 1.4.3 → 1.4.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.
@@ -5,7 +5,7 @@ function isServiceVerb(word) {
5
5
  }
6
6
  async function runService(verb) {
7
7
  if (process.platform === "win32") {
8
- const win = await import("./windows-OVOLZLC6.js");
8
+ const win = await import("./windows-LCLS7RG3.js");
9
9
  if (verb === "install") return win.windowsInstall();
10
10
  if (verb === "uninstall") return win.windowsUninstall();
11
11
  return win.windowsStatus();
@@ -25,4 +25,4 @@ export {
25
25
  isServiceVerb,
26
26
  runService
27
27
  };
28
- //# sourceMappingURL=chunk-62YE5ELN.js.map
28
+ //# sourceMappingURL=chunk-5UH6WN52.js.map
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  SERVICE_VERBS,
4
4
  isServiceVerb
5
- } from "./chunk-62YE5ELN.js";
5
+ } from "./chunk-5UH6WN52.js";
6
6
  import {
7
7
  collectorVersion,
8
8
  primeCollectorApiKey
@@ -43,6 +43,7 @@ function helpText() {
43
43
  ` doctor Local health check: running, and talking to the cloud?`,
44
44
  ` uselog What this collector did, from this host's own ledger`,
45
45
  ` service install Register and start the background service`,
46
+ ` --replace re-points an existing registration at this copy`,
46
47
  ` (Linux: sudo, systemd unit \u2014 Windows: elevated, SCM service)`,
47
48
  ` service uninstall Stop and remove the service; the identity stays on disk`,
48
49
  ` service status Registered/active? exit 0 running \xB7 1 stopped \xB7 2 not installed`,
@@ -153,7 +154,7 @@ async function main(argv = process.argv.slice(2)) {
153
154
  await printStatus();
154
155
  return;
155
156
  case "service": {
156
- const { runService } = await import("./service-UQE4NMLV.js");
157
+ const { runService } = await import("./service-WXWYM3DG.js");
157
158
  process.exitCode = await runService(cmd.verb);
158
159
  return;
159
160
  }
package/dist/cli.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/cli.ts"],
4
- "sourcesContent": ["#!/usr/bin/env node\r\n/**\r\n * The collector's ONE packaged entrypoint \u2014 bundled to `dist/cli.js`, which\r\n * package.json's `bin` maps onto the installed `netdoc-collector` command (the\r\n * npm package is `infracensus-collector`; package name and command differ on\r\n * purpose, the way `typescript` installs `tsc`). In a checkout the same file\r\n * runs as `npx tsx src/cli.ts \u2026`.\r\n *\r\n * The hashbang above is the difference between a package that works and one\r\n * that no-ops on BOTH platforms, which the first CI run proved: on Linux npm\r\n * symlinks the bin straight at this file and the kernel executed it as SHELL\r\n * (`line 1: import: command not found`); on Windows npm's cmd-shim PARSES the\r\n * hashbang to pick the interpreter for the shim it generates, and with none\r\n * present the generated shim launched the .js through the OS file association\r\n * instead of node \u2014 three commands \"succeeding\" while doing nothing. esbuild\r\n * carries an entry file's hashbang into the bundle, and scripts/bundle.mjs\r\n * asserts it landed rather than trusting that behaviour across upgrades.\r\n *\r\n * \u2500\u2500 WHY A DISPATCHER AND NOT A REWRITE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n *\r\n * The per-module entry files (`src/index.ts`, `src/vmware-index.ts`, \u2026) each\r\n * load their own config, run against real hardware, print their own summary and\r\n * set their own exit code \u2014 and nothing in a CI environment can verify a\r\n * rewrite of code whose other end is a vCenter or a switch. So this file only\r\n * ROUTES: `scan vmware` dynamically imports `./vmware-index.js`, which\r\n * self-executes exactly as `pnpm vmware-scan` always has. One installed command\r\n * replaces nine pnpm aliases without touching what any of them does.\r\n *\r\n * The same goes for `enrol`/`doctor`/`uselog`: standalone scripts (shipped in\r\n * the package's scripts/) that must work before, or without, a built collector\r\n * \u2014 the rpm's shell wrapper used to route them, and `service/tools.ts` is that\r\n * seam now that the CLI is the only installed command.\r\n *\r\n * Subcommands:\r\n * run the daemon: poll the control plane for jobs + the local UI\r\n * scan [module] one one-shot scan (default: network); `--dry-run` passes through\r\n * status local status: enrolment, daemon liveness, last runs\r\n * enrol give this host its identity (key at a masked prompt, NEVER an argument)\r\n * doctor is it running, and is it talking to the cloud?\r\n * uselog what this collector did, from this host's own ledger\r\n * service <verb> install | uninstall | status \u2014 the background service\r\n * --version print the package version\r\n * --help this text\r\n *\r\n * Anything else prints the help and exits 2 \u2014 a service wrapper that garbles\r\n * its arguments should fail loudly, not fall back to a default that scans.\r\n */\r\nimport { readFileSync, realpathSync, existsSync } from \"node:fs\";\r\nimport { join } from \"node:path\";\r\nimport { pathToFileURL } from \"node:url\";\r\nimport { COLLECTOR_MODULES, findModule } from \"./ui/modules.js\";\r\nimport { SERVICE_VERBS, isServiceVerb, type ServiceVerb } from \"./service/index.js\";\r\nimport type { OperatorScript } from \"./service/tools.js\";\r\n// Found, not hop-counted: the bundled build flattens src/ into dist/, so the\r\n// only depth arithmetic that stays true in both layouts is \"walk up to the\r\n// nearest package.json\".\r\nimport { PKG_ROOT } from \"./pkgRoot.js\";\r\nimport { primeCollectorApiKey } from \"./vault/collectorKey.js\";\r\n// Imported AND re-exported: this module prints it (`--version`, `--help`,\r\n// `status`) and other callers have long imported it from here. One reader of\r\n// package.json, so the printed and the reported version can never disagree.\r\nimport { collectorVersion } from \"./version.js\";\r\nexport { collectorVersion };\r\n\r\n\r\nexport function helpText(): string {\r\n const modules = COLLECTOR_MODULES.map((m) => ` ${m.key.padEnd(15)} ${m.label}`).join(\"\\n\");\r\n return [\r\n `netdoc-collector ${collectorVersion()} \u2014 the InfraCensus on-prem collector`,\r\n ``,\r\n `Usage: netdoc-collector <command> [options]`,\r\n ``,\r\n `Commands:`,\r\n ` run Run the daemon: poll the control plane for scan jobs and`,\r\n ` serve the local status UI on http://127.0.0.1:9477`,\r\n ` scan [module] Run ONE scan in the foreground and exit (default: network).`,\r\n ` --dry-run builds the result without pushing it.`,\r\n ` modules:`,\r\n modules,\r\n ` status Show local status: enrolment, daemon liveness, last runs`,\r\n ` enrol Enrol this host (secrets are typed at a masked prompt,`,\r\n ` never given as an argument; --show-input echoes them in`,\r\n ` full, --replace re-keys)`,\r\n ` doctor Local health check: running, and talking to the cloud?`,\r\n ` uselog What this collector did, from this host's own ledger`,\r\n ` service install Register and start the background service`,\r\n ` (Linux: sudo, systemd unit \u2014 Windows: elevated, SCM service)`,\r\n ` service uninstall Stop and remove the service; the identity stays on disk`,\r\n ` service status Registered/active? exit 0 running \u00B7 1 stopped \u00B7 2 not installed`,\r\n ` --version Print the version`,\r\n ` --help Print this help`,\r\n ``,\r\n `Environment:`,\r\n ` COLLECTOR_UI_PORT Port for the local status UI (default 9477; 127.0.0.1 only)`,\r\n ` COLLECTOR_UI=0 Disable the local status UI entirely`,\r\n ` COLLECTOR_CONFIG Path to the config file (overrides the platform default)`,\r\n ].join(\"\\n\");\r\n}\r\n\r\n/** What one argv resolves to. Pure, so the dispatch is testable without running anything. */\r\nexport type ResolvedCommand =\r\n | { kind: \"run\" }\r\n | { kind: \"scan\"; module: string; entry: string }\r\n | { kind: \"status\" }\r\n | { kind: \"service\"; verb: ServiceVerb }\r\n /** enrol/doctor/uselog \u2014 routed to the standalone script of the same name. */\r\n | { kind: \"tool\"; script: OperatorScript; argv: string[] }\r\n | { kind: \"version\" }\r\n | { kind: \"help\"; exitCode: 0 }\r\n /** Unknown command or unknown module: help text + exit 2. */\r\n | { kind: \"usage-error\"; message: string };\r\n\r\nexport function resolveCommand(argv: readonly string[]): ResolvedCommand {\r\n const [first, ...rest] = argv;\r\n switch (first) {\r\n case \"run\":\r\n return { kind: \"run\" };\r\n case \"scan\": {\r\n // The first non-flag token after `scan` is the module; flags (`--dry-run`,\r\n // `--debug`, \u2026) pass through untouched to the module's own argv parsing.\r\n const moduleKey = rest.find((a) => !a.startsWith(\"-\")) ?? \"network\";\r\n const mod = findModule(moduleKey);\r\n if (!mod) return { kind: \"usage-error\", message: `unknown scan module \"${moduleKey}\"` };\r\n return { kind: \"scan\", module: mod.key, entry: mod.entry };\r\n }\r\n case \"status\":\r\n return { kind: \"status\" };\r\n case \"service\": {\r\n // No default verb: `service` alone is a garbled instruction, and every\r\n // verb here changes (or reports) what starts with the machine.\r\n const verb = rest.find((a) => !a.startsWith(\"-\"));\r\n if (!isServiceVerb(verb)) {\r\n return { kind: \"usage-error\", message: `service needs one of: ${SERVICE_VERBS.join(\", \")}` };\r\n }\r\n return { kind: \"service\", verb };\r\n }\r\n case \"enrol\":\r\n case \"doctor\":\r\n case \"uselog\":\r\n return { kind: \"tool\", script: first, argv: [...rest] };\r\n case \"--version\":\r\n case \"-v\":\r\n case \"version\":\r\n return { kind: \"version\" };\r\n case \"--help\":\r\n case \"-h\":\r\n case \"help\":\r\n return { kind: \"help\", exitCode: 0 };\r\n case undefined:\r\n // Bare invocation: a service wrapper or a script lost its argument. Help\r\n // plus a failing exit, so an empty variable in a unit file fails loudly\r\n // instead of silently doing nothing forever.\r\n return { kind: \"usage-error\", message: \"no command given\" };\r\n default:\r\n return { kind: \"usage-error\", message: `unknown command \"${first}\"` };\r\n }\r\n}\r\n\r\n/** `status` \u2014 everything answerable from THIS host, without starting anything. */\r\nasync function printStatus(): Promise<void> {\r\n const lines: string[] = [`netdoc-collector ${collectorVersion()}`];\r\n\r\n // Enrolment: the daemon's own relaxed load (an all-sites collector has no\r\n // site), so `status` agrees with what `run` would accept.\r\n try {\r\n const { loadConfig } = await import(\"./config.js\");\r\n const config = loadConfig({ requireScanTargets: false, requireSite: false });\r\n lines.push(`enrolled : yes`);\r\n lines.push(`control plane: ${config.controlPlaneUrl}`);\r\n lines.push(`collector id : ${config.collectorId}`);\r\n } catch (err) {\r\n const first = String((err as Error).message).split(\"\\n\")[0];\r\n lines.push(`enrolled : no \u2014 ${first}`);\r\n lines.push(` run \\`netdoc-collector enrol\\` (or \\`sudo netdoc-collector enrol\\`) first`);\r\n }\r\n\r\n // Daemon liveness from the PID file \u2014 the same cooperative record\r\n // `scripts/collector-ctl.mjs` uses, probed the same way (signal 0).\r\n const pidFile = join(PKG_ROOT, \".collector-daemon.pid\");\r\n let daemonLine = \"not running (no PID file)\";\r\n if (existsSync(pidFile)) {\r\n const pid = Number(readFileSync(pidFile, \"utf8\").trim());\r\n if (Number.isInteger(pid) && pid > 0) {\r\n try {\r\n process.kill(pid, 0);\r\n daemonLine = `running (PID ${pid})`;\r\n } catch {\r\n daemonLine = `not running (stale PID file for ${pid})`;\r\n }\r\n }\r\n }\r\n lines.push(`daemon : ${daemonLine}`);\r\n\r\n // Last runs per module, from the customer's own ledger \u2014 read, not invented.\r\n const { moduleStatusesFromLedger } = await import(\"./ui/status.js\");\r\n const rows = moduleStatusesFromLedger();\r\n lines.push(`modules :`);\r\n for (const m of rows) {\r\n const state = m.lastRunAt === null ? \"never\" : `${m.lastRunAt} ${m.ok === true ? \"ok\" : m.ok === false ? \"FAILED\" : \"\u2026\"}${m.summary ? ` ${m.summary}` : \"\"}`;\r\n lines.push(` ${m.key.padEnd(15)} ${state}`);\r\n }\r\n console.log(lines.join(\"\\n\"));\r\n}\r\n\r\n/**\r\n * Dispatch. Exported (and side-effect free at import time) so tests can drive\r\n * it directly; never calls `process.exit()` so an embedding test runner is not\r\n * killed mid-suite \u2014 exit codes travel via `process.exitCode`.\r\n */\r\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<void> {\r\n const cmd = resolveCommand(argv);\r\n\r\n // \u2500\u2500 THE COLLECTOR'S KEY, BEFORE ANYTHING ASKS FOR IT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // It may be sealed in the vault rather than sitting in `collector.config.json`,\r\n // and every config loader in this build reads `COLLECTOR_API_KEY` from the\r\n // environment ahead of the file. So the key is unsealed into the environment\r\n // ONCE, here, and thirteen loaders and nine one-shot module entrypoints go on\r\n // working exactly as they did \u2014 `cfg.apiKey` is still a plain string, still\r\n // required, still resolved the same way.\r\n //\r\n // The alternative was threading a promise for the key through all twenty-two\r\n // of them, which is a much larger diff with many more places to be subtly\r\n // wrong about a value that must never be logged.\r\n //\r\n // Placed after `resolveCommand` so a usage error or `--version` is answered\r\n // without opening anything: this can spawn a keystore shim, and `--help` has\r\n // no business waiting on DPAPI. It never throws \u2014 a host whose vault will not\r\n // open still runs on the plaintext key in its config, and the daemon reports\r\n // the vault problem properly a moment later.\r\n // A logger, because the one thing this can find and NOT fix \u2014 a vault sealed\r\n // to a different Windows account \u2014 is invisible otherwise: the failure\r\n // surfaces two frames later as \"COLLECTOR_API_KEY is required\" on a host that\r\n // is demonstrably enrolled.\r\n await primeCollectorApiKey({\r\n env: process.env,\r\n log: (level, message) => {\r\n if (level === \"debug\") return;\r\n console.error(`netdoc-collector: ${message}`);\r\n },\r\n });\r\n switch (cmd.kind) {\r\n case \"run\": {\r\n // `src/index.ts` self-executes on import and reads `process.argv` itself;\r\n // its daemon switch is the `--daemon` flag, so `run` IS `--daemon`.\r\n if (!process.argv.includes(\"--daemon\")) process.argv.push(\"--daemon\");\r\n await import(\"./index.js\");\r\n return;\r\n }\r\n case \"scan\": {\r\n // \u2500\u2500 THE LICENCE PRE-CHECK IS THE MODULE'S, NOT THIS DISPATCHER'S \u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // This file routes; it does not gate, and it must not start to. Every\r\n // target it can reach asks first, at the top of its own `main()`:\r\n // `vmware-index.ts` and its eight siblings call\r\n // `requireEntitlementToScan` before they construct a client, and\r\n // `scan network` lands in `./index.js`, whose `runCollector` calls it\r\n // before a single plugin runs. `entitlement/scanPathCoverage.test.ts`\r\n // derives that set from the `src/*-index.ts` glob and asserts the\r\n // ordering, so a tenth module is caught ungated on the day it is written\r\n // rather than the day somebody notices. A second check here would be a\r\n // second answer to a question one place already owns.\r\n //\r\n // \u2500\u2500 WHAT THIS COMMENT USED TO SAY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // It called this path a known hole and asked for \"one collector-\r\n // authenticated read \u2014 say `POST /api/v1/collector/entitlements`, behind\r\n // the same `entitlement(\"collector\")` middleware as jobs/next\". That was\r\n // built: `ROUTES.collectorEntitlement` is mounted exactly that way and\r\n // `entitlement/client.ts` is its caller. One property it asked for was\r\n // refused on the way, and the reversal is deliberate \u2014 it wanted an\r\n // unreachable plane to mean PROCEED, and the operator was told the cost\r\n // and chose fail-closed on 2026-09-01 (see `entitlement/gate.ts`). Do not\r\n // restore the fail-open half by citing this paragraph.\r\n //\r\n // \u2500\u2500 WHAT IS STILL NOT CHECKED, AND IS NOT A LICENCE QUESTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // The gate answers \"is this tenant on a confirmed paid licence?\", not\r\n // \"did they buy the VMware module\". Module level is still decided only at\r\n // job creation and at ingest, so `scan vmware` for a paying tenant who\r\n // never bought that module connects to their vCenter and learns from a\r\n // 403 on the push. What the refusal buys is that the inventory is not\r\n // sent, is not stored, is reported in words the operator can act on, and\r\n // is not retried (core/planRefusal.ts). Closing THAT needs the plane's\r\n // module levels, which this process must not compute for itself: a client\r\n // that can compute \"yes\" is a client that can be made to say \"yes\".\r\n //\r\n // The inverse guard: a stray `--daemon` handed to `scan network` would\r\n // otherwise flip the network entrypoint into daemon mode \u2014 strip it so\r\n // `scan` can only ever mean one foreground run.\r\n process.argv = process.argv.filter((a) => a !== \"--daemon\");\r\n await import(cmd.entry);\r\n return;\r\n }\r\n case \"status\":\r\n await printStatus();\r\n return;\r\n case \"service\": {\r\n const { runService } = await import(\"./service/index.js\");\r\n process.exitCode = await runService(cmd.verb);\r\n return;\r\n }\r\n case \"tool\": {\r\n // The scripts parse process.argv themselves; tools.ts rewrites it to\r\n // what each script would have seen invoked directly, wires the platform\r\n // paths (env file / ProgramData / COLLECTOR_CONFIG), then imports it.\r\n const { runOperatorScript } = await import(\"./service/tools.js\");\r\n await runOperatorScript(cmd.script, cmd.argv);\r\n return;\r\n }\r\n case \"version\":\r\n console.log(collectorVersion());\r\n return;\r\n case \"help\":\r\n console.log(helpText());\r\n return;\r\n case \"usage-error\":\r\n console.error(`netdoc-collector: ${cmd.message}\\n`);\r\n console.error(helpText());\r\n process.exitCode = 2;\r\n return;\r\n }\r\n}\r\n\r\n// Run only when THIS file is the executed entrypoint (`node dist/cli.js \u2026`,\r\n// `tsx src/cli.ts \u2026`) \u2014 never when a test imports it for the exports above.\r\nconst invokedDirectly = ((): boolean => {\r\n const arg = process.argv[1];\r\n if (!arg) return false;\r\n try {\r\n return pathToFileURL(realpathSync(arg)).href === import.meta.url;\r\n } catch {\r\n return false;\r\n }\r\n})();\r\n\r\nif (invokedDirectly) {\r\n main().catch((err) => {\r\n console.error(`${new Date().toISOString()} ERROR ${(err as Error).message}`);\r\n process.exitCode = 1;\r\n });\r\n}\r\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["#!/usr/bin/env node\r\n/**\r\n * The collector's ONE packaged entrypoint \u2014 bundled to `dist/cli.js`, which\r\n * package.json's `bin` maps onto the installed `netdoc-collector` command (the\r\n * npm package is `infracensus-collector`; package name and command differ on\r\n * purpose, the way `typescript` installs `tsc`). In a checkout the same file\r\n * runs as `npx tsx src/cli.ts \u2026`.\r\n *\r\n * The hashbang above is the difference between a package that works and one\r\n * that no-ops on BOTH platforms, which the first CI run proved: on Linux npm\r\n * symlinks the bin straight at this file and the kernel executed it as SHELL\r\n * (`line 1: import: command not found`); on Windows npm's cmd-shim PARSES the\r\n * hashbang to pick the interpreter for the shim it generates, and with none\r\n * present the generated shim launched the .js through the OS file association\r\n * instead of node \u2014 three commands \"succeeding\" while doing nothing. esbuild\r\n * carries an entry file's hashbang into the bundle, and scripts/bundle.mjs\r\n * asserts it landed rather than trusting that behaviour across upgrades.\r\n *\r\n * \u2500\u2500 WHY A DISPATCHER AND NOT A REWRITE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n *\r\n * The per-module entry files (`src/index.ts`, `src/vmware-index.ts`, \u2026) each\r\n * load their own config, run against real hardware, print their own summary and\r\n * set their own exit code \u2014 and nothing in a CI environment can verify a\r\n * rewrite of code whose other end is a vCenter or a switch. So this file only\r\n * ROUTES: `scan vmware` dynamically imports `./vmware-index.js`, which\r\n * self-executes exactly as `pnpm vmware-scan` always has. One installed command\r\n * replaces nine pnpm aliases without touching what any of them does.\r\n *\r\n * The same goes for `enrol`/`doctor`/`uselog`: standalone scripts (shipped in\r\n * the package's scripts/) that must work before, or without, a built collector\r\n * \u2014 the rpm's shell wrapper used to route them, and `service/tools.ts` is that\r\n * seam now that the CLI is the only installed command.\r\n *\r\n * Subcommands:\r\n * run the daemon: poll the control plane for jobs + the local UI\r\n * scan [module] one one-shot scan (default: network); `--dry-run` passes through\r\n * status local status: enrolment, daemon liveness, last runs\r\n * enrol give this host its identity (key at a masked prompt, NEVER an argument)\r\n * doctor is it running, and is it talking to the cloud?\r\n * uselog what this collector did, from this host's own ledger\r\n * service <verb> install | uninstall | status \u2014 the background service\r\n * --version print the package version\r\n * --help this text\r\n *\r\n * Anything else prints the help and exits 2 \u2014 a service wrapper that garbles\r\n * its arguments should fail loudly, not fall back to a default that scans.\r\n */\r\nimport { readFileSync, realpathSync, existsSync } from \"node:fs\";\r\nimport { join } from \"node:path\";\r\nimport { pathToFileURL } from \"node:url\";\r\nimport { COLLECTOR_MODULES, findModule } from \"./ui/modules.js\";\r\nimport { SERVICE_VERBS, isServiceVerb, type ServiceVerb } from \"./service/index.js\";\r\nimport type { OperatorScript } from \"./service/tools.js\";\r\n// Found, not hop-counted: the bundled build flattens src/ into dist/, so the\r\n// only depth arithmetic that stays true in both layouts is \"walk up to the\r\n// nearest package.json\".\r\nimport { PKG_ROOT } from \"./pkgRoot.js\";\r\nimport { primeCollectorApiKey } from \"./vault/collectorKey.js\";\r\n// Imported AND re-exported: this module prints it (`--version`, `--help`,\r\n// `status`) and other callers have long imported it from here. One reader of\r\n// package.json, so the printed and the reported version can never disagree.\r\nimport { collectorVersion } from \"./version.js\";\r\nexport { collectorVersion };\r\n\r\n\r\nexport function helpText(): string {\r\n const modules = COLLECTOR_MODULES.map((m) => ` ${m.key.padEnd(15)} ${m.label}`).join(\"\\n\");\r\n return [\r\n `netdoc-collector ${collectorVersion()} \u2014 the InfraCensus on-prem collector`,\r\n ``,\r\n `Usage: netdoc-collector <command> [options]`,\r\n ``,\r\n `Commands:`,\r\n ` run Run the daemon: poll the control plane for scan jobs and`,\r\n ` serve the local status UI on http://127.0.0.1:9477`,\r\n ` scan [module] Run ONE scan in the foreground and exit (default: network).`,\r\n ` --dry-run builds the result without pushing it.`,\r\n ` modules:`,\r\n modules,\r\n ` status Show local status: enrolment, daemon liveness, last runs`,\r\n ` enrol Enrol this host (secrets are typed at a masked prompt,`,\r\n ` never given as an argument; --show-input echoes them in`,\r\n ` full, --replace re-keys)`,\r\n ` doctor Local health check: running, and talking to the cloud?`,\r\n ` uselog What this collector did, from this host's own ledger`,\r\n ` service install Register and start the background service`,\r\n ` --replace re-points an existing registration at this copy`,\r\n ` (Linux: sudo, systemd unit \u2014 Windows: elevated, SCM service)`,\r\n ` service uninstall Stop and remove the service; the identity stays on disk`,\r\n ` service status Registered/active? exit 0 running \u00B7 1 stopped \u00B7 2 not installed`,\r\n ` --version Print the version`,\r\n ` --help Print this help`,\r\n ``,\r\n `Environment:`,\r\n ` COLLECTOR_UI_PORT Port for the local status UI (default 9477; 127.0.0.1 only)`,\r\n ` COLLECTOR_UI=0 Disable the local status UI entirely`,\r\n ` COLLECTOR_CONFIG Path to the config file (overrides the platform default)`,\r\n ].join(\"\\n\");\r\n}\r\n\r\n/** What one argv resolves to. Pure, so the dispatch is testable without running anything. */\r\nexport type ResolvedCommand =\r\n | { kind: \"run\" }\r\n | { kind: \"scan\"; module: string; entry: string }\r\n | { kind: \"status\" }\r\n | { kind: \"service\"; verb: ServiceVerb }\r\n /** enrol/doctor/uselog \u2014 routed to the standalone script of the same name. */\r\n | { kind: \"tool\"; script: OperatorScript; argv: string[] }\r\n | { kind: \"version\" }\r\n | { kind: \"help\"; exitCode: 0 }\r\n /** Unknown command or unknown module: help text + exit 2. */\r\n | { kind: \"usage-error\"; message: string };\r\n\r\nexport function resolveCommand(argv: readonly string[]): ResolvedCommand {\r\n const [first, ...rest] = argv;\r\n switch (first) {\r\n case \"run\":\r\n return { kind: \"run\" };\r\n case \"scan\": {\r\n // The first non-flag token after `scan` is the module; flags (`--dry-run`,\r\n // `--debug`, \u2026) pass through untouched to the module's own argv parsing.\r\n const moduleKey = rest.find((a) => !a.startsWith(\"-\")) ?? \"network\";\r\n const mod = findModule(moduleKey);\r\n if (!mod) return { kind: \"usage-error\", message: `unknown scan module \"${moduleKey}\"` };\r\n return { kind: \"scan\", module: mod.key, entry: mod.entry };\r\n }\r\n case \"status\":\r\n return { kind: \"status\" };\r\n case \"service\": {\r\n // No default verb: `service` alone is a garbled instruction, and every\r\n // verb here changes (or reports) what starts with the machine.\r\n const verb = rest.find((a) => !a.startsWith(\"-\"));\r\n if (!isServiceVerb(verb)) {\r\n return { kind: \"usage-error\", message: `service needs one of: ${SERVICE_VERBS.join(\", \")}` };\r\n }\r\n return { kind: \"service\", verb };\r\n }\r\n case \"enrol\":\r\n case \"doctor\":\r\n case \"uselog\":\r\n return { kind: \"tool\", script: first, argv: [...rest] };\r\n case \"--version\":\r\n case \"-v\":\r\n case \"version\":\r\n return { kind: \"version\" };\r\n case \"--help\":\r\n case \"-h\":\r\n case \"help\":\r\n return { kind: \"help\", exitCode: 0 };\r\n case undefined:\r\n // Bare invocation: a service wrapper or a script lost its argument. Help\r\n // plus a failing exit, so an empty variable in a unit file fails loudly\r\n // instead of silently doing nothing forever.\r\n return { kind: \"usage-error\", message: \"no command given\" };\r\n default:\r\n return { kind: \"usage-error\", message: `unknown command \"${first}\"` };\r\n }\r\n}\r\n\r\n/** `status` \u2014 everything answerable from THIS host, without starting anything. */\r\nasync function printStatus(): Promise<void> {\r\n const lines: string[] = [`netdoc-collector ${collectorVersion()}`];\r\n\r\n // Enrolment: the daemon's own relaxed load (an all-sites collector has no\r\n // site), so `status` agrees with what `run` would accept.\r\n try {\r\n const { loadConfig } = await import(\"./config.js\");\r\n const config = loadConfig({ requireScanTargets: false, requireSite: false });\r\n lines.push(`enrolled : yes`);\r\n lines.push(`control plane: ${config.controlPlaneUrl}`);\r\n lines.push(`collector id : ${config.collectorId}`);\r\n } catch (err) {\r\n const first = String((err as Error).message).split(\"\\n\")[0];\r\n lines.push(`enrolled : no \u2014 ${first}`);\r\n lines.push(` run \\`netdoc-collector enrol\\` (or \\`sudo netdoc-collector enrol\\`) first`);\r\n }\r\n\r\n // Daemon liveness from the PID file \u2014 the same cooperative record\r\n // `scripts/collector-ctl.mjs` uses, probed the same way (signal 0).\r\n const pidFile = join(PKG_ROOT, \".collector-daemon.pid\");\r\n let daemonLine = \"not running (no PID file)\";\r\n if (existsSync(pidFile)) {\r\n const pid = Number(readFileSync(pidFile, \"utf8\").trim());\r\n if (Number.isInteger(pid) && pid > 0) {\r\n try {\r\n process.kill(pid, 0);\r\n daemonLine = `running (PID ${pid})`;\r\n } catch {\r\n daemonLine = `not running (stale PID file for ${pid})`;\r\n }\r\n }\r\n }\r\n lines.push(`daemon : ${daemonLine}`);\r\n\r\n // Last runs per module, from the customer's own ledger \u2014 read, not invented.\r\n const { moduleStatusesFromLedger } = await import(\"./ui/status.js\");\r\n const rows = moduleStatusesFromLedger();\r\n lines.push(`modules :`);\r\n for (const m of rows) {\r\n const state = m.lastRunAt === null ? \"never\" : `${m.lastRunAt} ${m.ok === true ? \"ok\" : m.ok === false ? \"FAILED\" : \"\u2026\"}${m.summary ? ` ${m.summary}` : \"\"}`;\r\n lines.push(` ${m.key.padEnd(15)} ${state}`);\r\n }\r\n console.log(lines.join(\"\\n\"));\r\n}\r\n\r\n/**\r\n * Dispatch. Exported (and side-effect free at import time) so tests can drive\r\n * it directly; never calls `process.exit()` so an embedding test runner is not\r\n * killed mid-suite \u2014 exit codes travel via `process.exitCode`.\r\n */\r\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<void> {\r\n const cmd = resolveCommand(argv);\r\n\r\n // \u2500\u2500 THE COLLECTOR'S KEY, BEFORE ANYTHING ASKS FOR IT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // It may be sealed in the vault rather than sitting in `collector.config.json`,\r\n // and every config loader in this build reads `COLLECTOR_API_KEY` from the\r\n // environment ahead of the file. So the key is unsealed into the environment\r\n // ONCE, here, and thirteen loaders and nine one-shot module entrypoints go on\r\n // working exactly as they did \u2014 `cfg.apiKey` is still a plain string, still\r\n // required, still resolved the same way.\r\n //\r\n // The alternative was threading a promise for the key through all twenty-two\r\n // of them, which is a much larger diff with many more places to be subtly\r\n // wrong about a value that must never be logged.\r\n //\r\n // Placed after `resolveCommand` so a usage error or `--version` is answered\r\n // without opening anything: this can spawn a keystore shim, and `--help` has\r\n // no business waiting on DPAPI. It never throws \u2014 a host whose vault will not\r\n // open still runs on the plaintext key in its config, and the daemon reports\r\n // the vault problem properly a moment later.\r\n // A logger, because the one thing this can find and NOT fix \u2014 a vault sealed\r\n // to a different Windows account \u2014 is invisible otherwise: the failure\r\n // surfaces two frames later as \"COLLECTOR_API_KEY is required\" on a host that\r\n // is demonstrably enrolled.\r\n await primeCollectorApiKey({\r\n env: process.env,\r\n log: (level, message) => {\r\n if (level === \"debug\") return;\r\n console.error(`netdoc-collector: ${message}`);\r\n },\r\n });\r\n switch (cmd.kind) {\r\n case \"run\": {\r\n // `src/index.ts` self-executes on import and reads `process.argv` itself;\r\n // its daemon switch is the `--daemon` flag, so `run` IS `--daemon`.\r\n if (!process.argv.includes(\"--daemon\")) process.argv.push(\"--daemon\");\r\n await import(\"./index.js\");\r\n return;\r\n }\r\n case \"scan\": {\r\n // \u2500\u2500 THE LICENCE PRE-CHECK IS THE MODULE'S, NOT THIS DISPATCHER'S \u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // This file routes; it does not gate, and it must not start to. Every\r\n // target it can reach asks first, at the top of its own `main()`:\r\n // `vmware-index.ts` and its eight siblings call\r\n // `requireEntitlementToScan` before they construct a client, and\r\n // `scan network` lands in `./index.js`, whose `runCollector` calls it\r\n // before a single plugin runs. `entitlement/scanPathCoverage.test.ts`\r\n // derives that set from the `src/*-index.ts` glob and asserts the\r\n // ordering, so a tenth module is caught ungated on the day it is written\r\n // rather than the day somebody notices. A second check here would be a\r\n // second answer to a question one place already owns.\r\n //\r\n // \u2500\u2500 WHAT THIS COMMENT USED TO SAY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // It called this path a known hole and asked for \"one collector-\r\n // authenticated read \u2014 say `POST /api/v1/collector/entitlements`, behind\r\n // the same `entitlement(\"collector\")` middleware as jobs/next\". That was\r\n // built: `ROUTES.collectorEntitlement` is mounted exactly that way and\r\n // `entitlement/client.ts` is its caller. One property it asked for was\r\n // refused on the way, and the reversal is deliberate \u2014 it wanted an\r\n // unreachable plane to mean PROCEED, and the operator was told the cost\r\n // and chose fail-closed on 2026-09-01 (see `entitlement/gate.ts`). Do not\r\n // restore the fail-open half by citing this paragraph.\r\n //\r\n // \u2500\u2500 WHAT IS STILL NOT CHECKED, AND IS NOT A LICENCE QUESTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // The gate answers \"is this tenant on a confirmed paid licence?\", not\r\n // \"did they buy the VMware module\". Module level is still decided only at\r\n // job creation and at ingest, so `scan vmware` for a paying tenant who\r\n // never bought that module connects to their vCenter and learns from a\r\n // 403 on the push. What the refusal buys is that the inventory is not\r\n // sent, is not stored, is reported in words the operator can act on, and\r\n // is not retried (core/planRefusal.ts). Closing THAT needs the plane's\r\n // module levels, which this process must not compute for itself: a client\r\n // that can compute \"yes\" is a client that can be made to say \"yes\".\r\n //\r\n // The inverse guard: a stray `--daemon` handed to `scan network` would\r\n // otherwise flip the network entrypoint into daemon mode \u2014 strip it so\r\n // `scan` can only ever mean one foreground run.\r\n process.argv = process.argv.filter((a) => a !== \"--daemon\");\r\n await import(cmd.entry);\r\n return;\r\n }\r\n case \"status\":\r\n await printStatus();\r\n return;\r\n case \"service\": {\r\n const { runService } = await import(\"./service/index.js\");\r\n process.exitCode = await runService(cmd.verb);\r\n return;\r\n }\r\n case \"tool\": {\r\n // The scripts parse process.argv themselves; tools.ts rewrites it to\r\n // what each script would have seen invoked directly, wires the platform\r\n // paths (env file / ProgramData / COLLECTOR_CONFIG), then imports it.\r\n const { runOperatorScript } = await import(\"./service/tools.js\");\r\n await runOperatorScript(cmd.script, cmd.argv);\r\n return;\r\n }\r\n case \"version\":\r\n console.log(collectorVersion());\r\n return;\r\n case \"help\":\r\n console.log(helpText());\r\n return;\r\n case \"usage-error\":\r\n console.error(`netdoc-collector: ${cmd.message}\\n`);\r\n console.error(helpText());\r\n process.exitCode = 2;\r\n return;\r\n }\r\n}\r\n\r\n// Run only when THIS file is the executed entrypoint (`node dist/cli.js \u2026`,\r\n// `tsx src/cli.ts \u2026`) \u2014 never when a test imports it for the exports above.\r\nconst invokedDirectly = ((): boolean => {\r\n const arg = process.argv[1];\r\n if (!arg) return false;\r\n try {\r\n return pathToFileURL(realpathSync(arg)).href === import.meta.url;\r\n } catch {\r\n return false;\r\n }\r\n})();\r\n\r\nif (invokedDirectly) {\r\n main().catch((err) => {\r\n console.error(`${new Date().toISOString()} ERROR ${(err as Error).message}`);\r\n process.exitCode = 1;\r\n });\r\n}\r\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,10 +2,10 @@ import {
2
2
  SERVICE_VERBS,
3
3
  isServiceVerb,
4
4
  runService
5
- } from "./chunk-62YE5ELN.js";
5
+ } from "./chunk-5UH6WN52.js";
6
6
  export {
7
7
  SERVICE_VERBS,
8
8
  isServiceVerb,
9
9
  runService
10
10
  };
11
- //# sourceMappingURL=service-UQE4NMLV.js.map
11
+ //# sourceMappingURL=service-WXWYM3DG.js.map
@@ -15,7 +15,9 @@ function windowsDataDir() {
15
15
  function windowsConfigPath() {
16
16
  return join(windowsDataDir(), "collector.config.json");
17
17
  }
18
- function parseScQuery(text) {
18
+ function parseScQuery(text, exitCode) {
19
+ const NO_SUCH_SERVICE = 1060;
20
+ if (exitCode === NO_SUCH_SERVICE) return { installed: false, running: false, state: "not installed" };
19
21
  if (/FAILED 1060/.test(text)) return { installed: false, running: false, state: "not installed" };
20
22
  const word = /STATE\s*:\s*\d+\s+(\w+)/.exec(text)?.[1];
21
23
  if (!word) return { installed: false, running: false, state: "unknown" };
@@ -24,7 +26,7 @@ function parseScQuery(text) {
24
26
  function queryScm() {
25
27
  const res = spawnSync("sc.exe", ["query", SERVICE_ID], { encoding: "utf8" });
26
28
  if (res.error) return null;
27
- return parseScQuery(`${res.stdout ?? ""}${res.stderr ?? ""}`);
29
+ return parseScQuery(`${res.stdout ?? ""}${res.stderr ?? ""}`, res.status);
28
30
  }
29
31
  function isElevated() {
30
32
  const res = spawnSync("net", ["session"], { encoding: "utf8" });
@@ -240,8 +242,21 @@ async function windowsInstall() {
240
242
  }
241
243
  const already = queryScm();
242
244
  if (already?.installed) {
243
- console.error(`service install: a ${SERVICE_ID} service is already registered \u2014 run: netdoc-collector service uninstall first.`);
244
- return 1;
245
+ if (!process.argv.includes("--replace")) {
246
+ console.error(`service install: a ${SERVICE_ID} service is already registered.`);
247
+ console.error(` It currently runs: ${already.state}`);
248
+ console.error(" To point it at THIS copy of the collector, re-register:");
249
+ console.error(" netdoc-collector service install --replace");
250
+ console.error(" Or remove it first: netdoc-collector service uninstall");
251
+ return 1;
252
+ }
253
+ console.log(`service install: --replace \u2014 removing the existing ${SERVICE_ID} registration first.`);
254
+ const removed = await windowsUninstall();
255
+ if (removed !== 0) {
256
+ console.error("service install: the existing registration could not be removed, so nothing was changed.");
257
+ console.error(` Remove it by hand and try again: sc.exe delete ${SERVICE_ID}`);
258
+ return removed;
259
+ }
245
260
  }
246
261
  const logDir = join(windowsDataDir(), "logs");
247
262
  mkdirSync(logDir, { recursive: true });
@@ -320,4 +335,4 @@ export {
320
335
  windowsStatus,
321
336
  windowsUninstall
322
337
  };
323
- //# sourceMappingURL=windows-OVOLZLC6.js.map
338
+ //# sourceMappingURL=windows-LCLS7RG3.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/service/windows.ts"],
4
+ "sourcesContent": ["/**\r\n * `netdoc-collector service install|uninstall|status` \u2014 the Windows half.\r\n *\r\n * This is packaging/windows-service.mjs moved into the CLI, seam intact: the\r\n * zip's install-windows.ps1 used to call that helper because node-windows is\r\n * an event-emitter API and PowerShell cannot await it \u2014 a script that\r\n * registers a service and exits before the SCM row exists reports success it\r\n * has not earned. The PowerShell layer is gone (npm is the installer now),\r\n * but the emitter-to-exit-code discipline and the 120-second timeout survive\r\n * unchanged, because node-windows can still silently emit nothing when winsw\r\n * misbehaves, and a command hanging forever on a customer's host is worse\r\n * than one that fails loudly with a name to search for.\r\n *\r\n * The service's DISPLAY NAME is the contract \u2014 the docs and the CI proof say\r\n * `Get-Service -DisplayName \"InfraCensus Collector\"` \u2014 and this file is the\r\n * one place that decides it (SERVICE_DISPLAY_NAME below; the SCM id is\r\n * node-windows's derivation, see SERVICE_ID's header).\r\n *\r\n * The config lives in C:\\ProgramData\\InfraCensus Collector rather than beside\r\n * the code, so that an `npm uninstall -g` (or update) cannot silently destroy\r\n * an identity whose key was shown exactly once. `enrol` creates it there (see\r\n * service/tools.ts); install refuses to register a service that would only\r\n * crash-loop on a missing identity, and uninstall leaves the directory behind\r\n * ON PURPOSE, saying so.\r\n */\r\nimport { spawnSync } from \"node:child_process\";\r\nimport { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from \"node:fs\";\r\nimport { createRequire } from \"node:module\";\r\nimport { join } from \"node:path\";\r\nimport { PKG_ROOT } from \"../pkgRoot.js\";\r\n\r\n/**\r\n * The DISPLAY name is the contract \u2014 it is what the docs teach and what\r\n * `Get-Service -DisplayName \"InfraCensus Collector\"` answers to.\r\n *\r\n * The SCM id is NOT ours to choose, which the first CI proof run established\r\n * the hard way: node-windows defines `id` as a GETTER over the name \u2014\r\n * `name.replace(/[^\\w]/gi, \"\").toLowerCase()` \u2014 ignores any `id` passed in,\r\n * and registers the service under `<id>.exe` because the same string names the\r\n * winsw wrapper executable. Passing `id: \"InfraCensusCollector\"` did nothing;\r\n * the service came up as `infracensuscollector.exe` while install's verify\r\n * polled the name we wished it had, saw nothing, and rolled a WORKING\r\n * registration back. So SERVICE_ID is now DERIVED here with node-windows's own\r\n * formula, and `install()` asserts the constructed Service agrees before\r\n * registering \u2014 a node-windows upgrade that changes its derivation fails\r\n * loudly at that assertion instead of rolling back mysteriously.\r\n */\r\nexport const SERVICE_DISPLAY_NAME = \"InfraCensus Collector\";\r\nexport const SERVICE_ID = `${SERVICE_DISPLAY_NAME.replace(/[^\\w]/gi, \"\").toLowerCase()}.exe`;\r\n\r\n/** ProgramData, not Program Files: the identity must survive the binaries. */\r\nexport function windowsDataDir(): string {\r\n return join(process.env.ProgramData ?? \"C:\\\\ProgramData\", \"InfraCensus Collector\");\r\n}\r\nexport function windowsConfigPath(): string {\r\n return join(windowsDataDir(), \"collector.config.json\");\r\n}\r\n\r\n/**\r\n * `sc.exe query` prints `STATE : 4 RUNNING`. The word is what we read, not\r\n * the number \u2014 the word is what an operator sees in every other tool, and the\r\n * two agreeing is worth more than saving a regex. (Same parse the doctor's\r\n * service-state library uses; duplicated here because the CLI must not import\r\n * from scripts/ \u2014 see service/index.ts.)\r\n */\r\n/**\r\n * What `sc.exe query` said, read from its EXIT CODE first and its text second.\r\n *\r\n * \u2500\u2500 WHY THE EXIT CODE IS THE AUTHORITY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n *\r\n * This used to decide \"not installed\" by matching the literal string\r\n * `FAILED 1060`. That is English Windows. On a localised host the message is\r\n * translated, the match fails, no STATE line is found either, and the answer\r\n * falls through to `state: \"unknown\"` \u2014 which the callers then read in two\r\n * different directions. It produced a `service uninstall` that said both \"no\r\n * service is registered, nothing to remove\" AND \"still registered, close\r\n * anything holding it\" about the same host, in the same run.\r\n *\r\n * `sc.exe` sets ERRORLEVEL to the Win32 error, so 1060\r\n * (ERROR_SERVICE_DOES_NOT_EXIST) is the same number in every language. The text\r\n * match stays as a fallback for a caller that has only the output \u2014 the\r\n * parameter is optional precisely so the old shape keeps working.\r\n *\r\n * \"Unknown\" is still possible (a garbled read, sc.exe absent) and still answers\r\n * `installed: false`. That is the safe direction for the two callers: install\r\n * proceeds and SCM refuses it if a row really exists, while uninstall stops\r\n * claiming a service lingers when it cannot actually see one.\r\n */\r\nexport function parseScQuery(\r\n text: string,\r\n exitCode?: number | null,\r\n): { installed: boolean; running: boolean; state: string } {\r\n const NO_SUCH_SERVICE = 1060;\r\n if (exitCode === NO_SUCH_SERVICE) return { installed: false, running: false, state: \"not installed\" };\r\n if (/FAILED 1060/.test(text)) return { installed: false, running: false, state: \"not installed\" };\r\n const word = /STATE\\s*:\\s*\\d+\\s+(\\w+)/.exec(text)?.[1];\r\n if (!word) return { installed: false, running: false, state: \"unknown\" };\r\n return { installed: true, running: word === \"RUNNING\", state: word.toLowerCase() };\r\n}\r\n\r\nfunction queryScm(): { installed: boolean; running: boolean; state: string } | null {\r\n const res = spawnSync(\"sc.exe\", [\"query\", SERVICE_ID], { encoding: \"utf8\" });\r\n if (res.error) return null;\r\n return parseScQuery(`${res.stdout ?? \"\"}${res.stderr ?? \"\"}`, res.status);\r\n}\r\n\r\n/**\r\n * Elevation check: `net session` succeeds only in an elevated process. The\r\n * registrations below fail without it anyway \u2014 the point of asking first is\r\n * to fail with the fix in the message instead of an opaque SCM error.\r\n */\r\nexport function isElevated(): boolean {\r\n const res = spawnSync(\"net\", [\"session\"], { encoding: \"utf8\" });\r\n return !res.error && res.status === 0;\r\n}\r\n\r\n/** node-windows is CommonJS; createRequire resolves it from the installed\r\n * package's own node_modules (it is an optionalDependency \u2014 present on the\r\n * platform that needs it, never shipped to a Linux host's payload). */\r\nfunction loadNodeWindows(): { Service: new (config: object) => NodeJS.EventEmitter & { install(): void; uninstall(): void; start(): void } } | null {\r\n try {\r\n const require = createRequire(import.meta.url);\r\n return require(\"node-windows\") as ReturnType<typeof loadNodeWindows>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport function windowsStatus(): number {\r\n const state = queryScm();\r\n if (state === null) {\r\n console.error(\"service status: sc.exe is not available \u2014 cannot ask the Service Control Manager anything.\");\r\n return 2;\r\n }\r\n if (!state.installed) {\r\n console.log(`service \"${SERVICE_DISPLAY_NAME}\": not installed \u2014 run: netdoc-collector service install (elevated)`);\r\n return 2;\r\n }\r\n if (state.running) {\r\n console.log(`service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}): registered and running`);\r\n return 0;\r\n }\r\n console.log(`service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}): registered but ${state.state} \u2014 Get-Service -DisplayName \"${SERVICE_DISPLAY_NAME}\" has the story`);\r\n return 1;\r\n}\r\n\r\n/**\r\n * The last ~40 lines of every log the node-windows service host wrote \u2014\r\n * `<script dir>/daemon/<id>.{wrapper,out,err}.log`. Called on the\r\n * verify-failure path ONLY, before the rollback deletes the directory: these\r\n * files are the one place a wrapper-level death explains itself.\r\n */\r\nfunction dumpDaemonLogs(): void {\r\n const daemonDir = join(PKG_ROOT, \"dist\", \"daemon\");\r\n if (!existsSync(daemonDir)) {\r\n console.error(`service install: no ${daemonDir} to read \u2014 the service host never wrote a log.`);\r\n return;\r\n }\r\n // Enumerate and dump EVERYTHING, never a hand-guessed name list: the first\r\n // version of this looked for three expected filenames, found none, and\r\n // printed NOTHING \u2014 silent in exactly the moment it existed for, which is\r\n // the same defect it was built to fix, one layer down. Whatever the host\r\n // wrote under whatever name it chose, this prints it or says the directory\r\n // was empty.\r\n let entries: string[] = [];\r\n try {\r\n entries = readdirSync(daemonDir);\r\n } catch (err) {\r\n console.error(`service install: ${daemonDir} is unreadable (${err instanceof Error ? err.message : String(err)}).`);\r\n return;\r\n }\r\n console.error(`service install: ${daemonDir} contains: ${entries.join(\", \") || \"(nothing)\"}`);\r\n // The winsw LOG destination is the ProgramData logs dir (the logpath the\r\n // install sets) \u2014 list and tail that too; a healthy-but-then-dead child\r\n // writes its last words THERE, not beside the exe.\r\n const logDir = join(windowsDataDir(), \"logs\");\r\n if (existsSync(logDir)) {\r\n try {\r\n const logEntries = readdirSync(logDir);\r\n console.error(`service install: ${logDir} contains: ${logEntries.join(\", \") || \"(nothing)\"}`);\r\n for (const entry of logEntries) {\r\n const file = join(logDir, entry);\r\n try {\r\n const lines = readFileSync(file, \"utf8\").split(/\\r?\\n/).filter(Boolean);\r\n console.error(`--- logs/${entry} (last ${Math.min(lines.length, 60)} of ${lines.length} lines) ---`);\r\n for (const line of lines.slice(-60)) console.error(` ${line}`);\r\n } catch (err) {\r\n console.error(`--- logs/${entry}: unreadable (${err instanceof Error ? err.message : String(err)}) ---`);\r\n }\r\n }\r\n } catch { /* enumerating evidence must never throw past the rollback */ }\r\n } else {\r\n console.error(`service install: ${logDir} does not exist.`);\r\n }\r\n\r\n for (const entry of entries) {\r\n // Logs AND the winsw XML: a daemon dir with the exe and its XML but no log\r\n // at all (the round-six signature) means winsw died before it could open\r\n // one \u2014 and the thing winsw reads first is the XML, as it stands AFTER\r\n // node-windows's post-install munging. Those exact bytes are the evidence.\r\n const isLog = /\\.(log|txt|out|err)$/i.test(entry) || /\\.log\\./i.test(entry);\r\n const isXml = /\\.xml$/i.test(entry);\r\n if (!isLog && !isXml) continue;\r\n const file = join(daemonDir, entry);\r\n try {\r\n const lines = readFileSync(file, \"utf8\").split(/\\r?\\n/).filter(Boolean);\r\n console.error(`--- ${entry} (last ${Math.min(lines.length, 60)} of ${lines.length} lines) ---`);\r\n for (const line of lines.slice(-60)) console.error(` ${line}`);\r\n } catch (err) {\r\n console.error(`--- ${entry}: unreadable (${err instanceof Error ? err.message : String(err)}) ---`);\r\n }\r\n }\r\n\r\n // Three more witnesses, each answering what the files cannot:\r\n // sc qc \u2014 the SCM's registered config (binPath, logon account),\r\n // i.e. what Windows was actually told to run and as whom;\r\n // <winsw> status \u2014 winsw's own view of its state machine;\r\n // <winsw> test \u2014 winsw runs its service logic IN CONSOLE MODE and prints\r\n // the error a service start swallows. Time-boxed, because\r\n // on a healthy install `test` runs the daemon indefinitely.\r\n const winswExe = join(daemonDir, SERVICE_ID);\r\n const witnesses: { label: string; cmd: string; args: string[] }[] = [\r\n { label: \"sc.exe qc\", cmd: \"sc.exe\", args: [\"qc\", SERVICE_ID] },\r\n { label: \"winsw status\", cmd: winswExe, args: [\"status\"] },\r\n { label: \"winsw test\", cmd: winswExe, args: [\"test\"] },\r\n ];\r\n for (const { label, cmd, args } of witnesses) {\r\n if (cmd !== \"sc.exe\" && !existsSync(cmd)) continue;\r\n const res = spawnSync(cmd, args, { encoding: \"utf8\", timeout: 20_000 });\r\n const out = `${res.stdout ?? \"\"}${res.stderr ?? \"\"}`.trim();\r\n console.error(`--- ${label} (exit ${res.status ?? \"timeout/killed\"}) ---`);\r\n for (const line of out.split(/\\r?\\n/).filter(Boolean).slice(-40)) console.error(` ${line}`);\r\n }\r\n\r\n // The WINDOWS EVENT LOG \u2014 the witness a process that dies before its own\r\n // logging cannot silence. Round eight eliminated every file-based\r\n // explanation: the XML is sound, the SCM config is sound, the log directory\r\n // is SYSTEM-writable and stayed EMPTY, so winsw is dying before its logging\r\n // initializes. The SCM records every service death in the System channel\r\n // (event 7000/7009/7034-class), and winsw writes its own errors to the\r\n // Application channel before it writes files. `wevtutil` rather than\r\n // PowerShell, so this works from any elevated shell with no profile and no\r\n // codepage hazard.\r\n const sinceMs = 5 * 60 * 1000;\r\n for (const channel of [\"System\", \"Application\"]) {\r\n const res = spawnSync(\r\n \"wevtutil.exe\",\r\n [\"qe\", channel, `/q:*[System[TimeCreated[timediff(@SystemTime) <= ${sinceMs}]]]`, \"/f:text\", \"/c:300\"],\r\n { encoding: \"utf8\", timeout: 30_000, maxBuffer: 8 * 1024 * 1024 },\r\n );\r\n const text = `${res.stdout ?? \"\"}${res.stderr ?? \"\"}`;\r\n // Events come as multi-line blocks separated by \"Event[\" headers; keep the\r\n // whole block when any of its lines mentions the service or its host.\r\n const blocks = text.split(/^Event\\[/m).filter((b) =>\r\n /infracensus|winsw|Service Control Manager.*(?:terminat|fail|did not)/i.test(b),\r\n );\r\n console.error(`--- event log: ${channel} (last 5 min, ${blocks.length} matching event(s)) ---`);\r\n for (const block of blocks.slice(-8)) {\r\n for (const line of block.split(/\\r?\\n/).filter(Boolean).slice(0, 16)) console.error(` ${line}`);\r\n console.error(\" --\");\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Turn one node-windows operation into an exit code. Everything here is \"wait\r\n * for the emitter, or for the timeout, whichever speaks first\".\r\n */\r\nfunction driveService(command: \"install\" | \"uninstall\"): Promise<number> {\r\n const nodeWindows = loadNodeWindows();\r\n if (!nodeWindows) {\r\n console.error(\"service: the node-windows package is missing from this install \u2014 reinstall with: npm install -g infracensus-collector\");\r\n return Promise.resolve(1);\r\n }\r\n const script = join(PKG_ROOT, \"dist\", \"cli.js\");\r\n const svc = new nodeWindows.Service({\r\n // NO `id` here \u2014 node-windows ignores it (see SERVICE_ID's header). The\r\n // display name is the one input; the SCM id falls out of it.\r\n name: SERVICE_DISPLAY_NAME,\r\n description:\r\n \"InfraCensus on-premises network collector. Polls the control plane outbound for scan jobs; nothing connects in. Local UI on http://localhost:9477 (127.0.0.1 only).\",\r\n script,\r\n scriptOptions: \"run\",\r\n workingDirectory: PKG_ROOT,\r\n // SYSTEM-writable, ALWAYS set: winsw defaults its logs to the exe directory\r\n // (the npm prefix, admin-ACLed), and a winsw that cannot open its log dies\r\n // as a service without a trace \u2014 round seven, in one line. The installer\r\n // creates this directory before registration.\r\n logpath: join(windowsDataDir(), \"logs\"),\r\n // Two things the service process must know that the package cannot carry:\r\n // where the enrolled identity lives (ProgramData, so it survives an\r\n // `npm uninstall -g` of the binaries), and that in-app restarts must EXIT\r\n // for the service host to bring back rather than re-exec \u2014 two restart\r\n // authorities is how a host ends up with two daemons (core/daemon.ts\r\n // restartSelf; it exits 75, which the wrapper restarts).\r\n env:\r\n command === \"install\"\r\n ? [\r\n { name: \"COLLECTOR_CONFIG\", value: windowsConfigPath() },\r\n { name: \"COLLECTOR_RESTART_MODE\", value: \"exit\" },\r\n ]\r\n : undefined,\r\n });\r\n\r\n // The derivation assertion SERVICE_ID's header promises: node-windows's\r\n // Service computes its own id and exe name, and every sc.exe query, the\r\n // uninstaller, the CI proof and the docs' display-name teaching all assume\r\n // this file knows what that computation produced. If an upgrade changes it,\r\n // refuse HERE, before anything is registered under a name nothing can find.\r\n const actualScmName = `${(svc as unknown as { id: string }).id}.exe`;\r\n if (actualScmName !== SERVICE_ID) {\r\n console.error(\r\n `service ${command}: node-windows would register \"${actualScmName}\" but this build expects ` +\r\n `\"${SERVICE_ID}\" \u2014 its id derivation changed; update SERVICE_ID in src/service/windows.ts.`,\r\n );\r\n return Promise.resolve(1);\r\n }\r\n\r\n return new Promise<number>((resolve) => {\r\n // The timeout exists because node-windows can silently emit nothing when\r\n // winsw misbehaves, and a hang on a customer's host is worse than a loud\r\n // failure with a name to search for.\r\n const timeout = setTimeout(() => done(1, `timed out waiting for the ${command} to complete`), 120_000);\r\n function done(code: number, message: string): void {\r\n clearTimeout(timeout);\r\n (code === 0 ? console.log : console.error)(`service ${command}: ${message}`);\r\n resolve(code);\r\n }\r\n\r\n svc.on(\"error\", (err: Error) => done(1, `error \u2014 ${err?.message ?? err}`));\r\n svc.on(\"invalidinstallation\", () => done(1, \"the existing installation is damaged \u2014 run: netdoc-collector service uninstall, then install again\"));\r\n\r\n if (command === \"install\") {\r\n svc.on(\"alreadyinstalled\", () => done(1, `a \"${SERVICE_ID}\" service is already registered \u2014 run: netdoc-collector service uninstall first`));\r\n // The 'install' event is NOT the registration \u2014 that is the finding ten\r\n // CI rounds paid for. node-windows emits it BEFORE the SCM row exists:\r\n // round ten issued `sc start` from this very handler and the SCM\r\n // answered 1060, \"the specified service does not exist\", after which\r\n // the registration quietly completed and the service sat\r\n // installed-but-stopped forever. (Its own start() had been failing the\r\n // same way all along \u2014 `NET START` into the void \u2014 with the error\r\n // swallowed and 'start' emitted anyway.) So this handler claims only\r\n // what the event can honestly mean: the request was made. The caller\r\n // waits for the SCM row itself, then starts it.\r\n svc.on(\"install\", () => done(0, `registration requested \u2014 waiting for the SCM row to appear`));\r\n svc.install();\r\n } else {\r\n svc.on(\"alreadyuninstalled\", () => done(0, `no ${SERVICE_ID} service is registered \u2014 nothing to remove`));\r\n svc.on(\"uninstall\", () => done(0, `service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}) removed`));\r\n\r\n // STOP FIRST, OURSELVES, and wait until the SCM says stopped \u2014 then let\r\n // node-windows deregister. Its own uninstall stops and then deletes its\r\n // files in the same breath, and the winsw exe is still briefly locked by\r\n // the exiting service process; its `rm` is a bare unlinkSync with no\r\n // error handling, so that race is an UNCAUGHT EXCEPTION that kills this\r\n // process mid-uninstall (first seen 2026-08-27, after eleven rounds of\r\n // the install side). Stopping first and settling shrinks the window; the\r\n // uncaughtException net below closes what remains.\r\n spawnSync(\"sc.exe\", [\"stop\", SERVICE_ID], { encoding: \"utf8\", timeout: 30_000 });\r\n waitForScm((s) => !s.installed || !s.running, 30_000);\r\n spawnSync(process.platform === \"win32\" ? \"cmd.exe\" : \"sleep\", process.platform === \"win32\" ? [\"/c\", \"timeout\", \"/t\", \"2\", \"/nobreak\"] : [\"2\"], { timeout: 10_000 });\r\n\r\n // node-windows's post-stop file deletion can still lose the lock race.\r\n // When it does, the SCM row is ALREADY GONE (winsw uninstall ran) and\r\n // only the file cleanup died \u2014 so finish that cleanup ourselves, with\r\n // the retries the lock actually needs, instead of crashing a successful\r\n // uninstall at its last step.\r\n const daemonDir = join(PKG_ROOT, \"dist\", \"daemon\");\r\n const onCrash = (err: Error & { code?: string; path?: string }): void => {\r\n if (err?.code !== \"EPERM\" || !String(err?.path ?? \"\").includes(\"daemon\")) throw err;\r\n console.error(`service uninstall: a service-host file was still locked (${err.path}) \u2014 retrying the cleanup for up to 20s.`);\r\n const deadline = Date.now() + 20_000;\r\n const retry = (): void => {\r\n try {\r\n rmSync(daemonDir, { recursive: true, force: true });\r\n done(0, `service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}) removed (host files cleaned up after a short lock)`);\r\n } catch {\r\n if (Date.now() > deadline) {\r\n done(0, `service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}) removed \u2014 but ${daemonDir} could not be fully deleted (a file stayed locked). Remove it by hand; the service itself is gone.`);\r\n } else {\r\n setTimeout(retry, 2_000);\r\n }\r\n }\r\n };\r\n setTimeout(retry, 2_000);\r\n };\r\n process.once(\"uncaughtException\", onCrash);\r\n svc.uninstall();\r\n }\r\n });\r\n}\r\n\r\n/** Poll the SCM until the predicate holds, or the deadline passes. */\r\nfunction waitForScm(predicate: (s: { installed: boolean; running: boolean }) => boolean, ms: number): boolean {\r\n const deadline = Date.now() + ms;\r\n for (;;) {\r\n const state = queryScm();\r\n if (state && predicate(state)) return true;\r\n if (Date.now() >= deadline) return false;\r\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);\r\n }\r\n}\r\n\r\nexport async function windowsInstall(): Promise<number> {\r\n if (!isElevated()) {\r\n console.error(\"service install: registering a Windows service needs elevation \u2014 run this from an ELEVATED PowerShell.\");\r\n return 2;\r\n }\r\n const script = join(PKG_ROOT, \"dist\", \"cli.js\");\r\n if (!existsSync(script)) {\r\n console.error(`service install: ${script} does not exist \u2014 refusing to register a service that cannot start.`);\r\n return 1;\r\n }\r\n if (!existsSync(windowsConfigPath())) {\r\n console.error(`service install: this host is not enrolled (no config at ${windowsConfigPath()}).`);\r\n console.error(\"Run: netdoc-collector enrol (from this elevated PowerShell) \u2014 then install the service again.\");\r\n return 2;\r\n }\r\n const already = queryScm();\r\n if (already?.installed) {\r\n // \u2500\u2500 RE-REGISTERING IS THE COMMON CASE, NOT AN ERROR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // The registration pins an ABSOLUTE path to dist/cli.js. Move the install\r\n // \u2014 a machine-wide npm prefix instead of a user profile, which is the\r\n // supported layout \u2014 and the existing service still points at the old\r\n // copy, so it either runs the wrong build or fails to start at all. The\r\n // only way out was uninstall-then-install, and `service uninstall` is the\r\n // command that had just become unreliable. An operator who has already\r\n // moved their install should not need two commands and a working\r\n // uninstall to point the service at it.\r\n if (!process.argv.includes(\"--replace\")) {\r\n console.error(`service install: a ${SERVICE_ID} service is already registered.`);\r\n console.error(` It currently runs: ${already.state}`);\r\n console.error(\" To point it at THIS copy of the collector, re-register:\");\r\n console.error(\" netdoc-collector service install --replace\");\r\n console.error(\" Or remove it first: netdoc-collector service uninstall\");\r\n return 1;\r\n }\r\n console.log(`service install: --replace \u2014 removing the existing ${SERVICE_ID} registration first.`);\r\n const removed = await windowsUninstall();\r\n if (removed !== 0) {\r\n console.error(\"service install: the existing registration could not be removed, so nothing was changed.\");\r\n console.error(` Remove it by hand and try again: sc.exe delete ${SERVICE_ID}`);\r\n return removed;\r\n }\r\n }\r\n\r\n // The service host's log directory, created HERE, under the SYSTEM-writable\r\n // ProgramData tree \u2014 never left to winsw's default of \"next to the exe\".\r\n // The exe lives under the npm global prefix, whose ACLs belong to whoever\r\n // installed node/npm; the SERVICE runs as LocalSystem. Round seven's\r\n // witnesses showed the exact consequence of that mismatch: winsw ran\r\n // perfectly in console mode (as the admin who could write beside the exe)\r\n // and died as a service without managing to write one log line \u2014 winsw 1.x\r\n // treats \"cannot open my log file\" as instant, traceless death. This dir is\r\n // created by the elevated installer, so it exists with inherited\r\n // SYSTEM-writable ACLs before winsw ever needs it.\r\n const logDir = join(windowsDataDir(), \"logs\");\r\n mkdirSync(logDir, { recursive: true });\r\n\r\n const code = await driveService(\"install\");\r\n if (code !== 0) return code;\r\n\r\n // FIRST the row, THEN the start \u2014 in that order, asked of the SCM itself.\r\n // node-windows's events fire ahead of the registration (see driveService),\r\n // so the only thing that proves the service exists is sc.exe saying so.\r\n if (!waitForScm((s) => s.installed, 30_000)) {\r\n console.error(`service install: the registration was requested but no ${SERVICE_ID} row ever appeared in the SCM.`);\r\n dumpDaemonLogs();\r\n return 1;\r\n }\r\n const started = spawnSync(\"sc.exe\", [\"start\", SERVICE_ID], { encoding: \"utf8\", timeout: 30_000 });\r\n const startOut = `${started.stdout ?? \"\"}${started.stderr ?? \"\"}`.trim();\r\n for (const line of startOut.split(/\\r?\\n/).filter(Boolean).slice(0, 12)) console.log(` sc start: ${line}`);\r\n if (started.status !== 0 && !/START_PENDING|RUNNING/.test(startOut)) {\r\n console.error(`service install: the SCM refused to start ${SERVICE_ID} (sc start exit ${started.status ?? \"none\"}) \u2014 its output is above. Rolling the registration back.`);\r\n dumpDaemonLogs();\r\n await driveService(\"uninstall\");\r\n return 1;\r\n }\r\n\r\n // Verify the way the operator's own tools would: the SCM, not the\r\n // registration call's word for it.\r\n if (!waitForScm((s) => s.installed && s.running, 60_000)) {\r\n // The last state observed rides in the message: \"stuck at start_pending\"\r\n // and \"stopped\" are different diagnoses, and the poll knows which it saw.\r\n const last = queryScm();\r\n console.error(\r\n `service install: the service registered but never reached Running` +\r\n ` (last state: ${last === null ? \"sc.exe unavailable\" : last.installed ? last.state : \"not installed\"})` +\r\n ` \u2014 rolling the registration back.`,\r\n );\r\n // EVIDENCE BEFORE THE ROLLBACK, which is about to delete it: node-windows\r\n // keeps the service host's logs in a `daemon` directory beside the script\r\n // it wraps, and uninstall removes that directory wholesale. A service that\r\n // dies inside the wrapper leaves its only explanation THERE \u2014 the first CI\r\n // diagnosis of this path had nothing to read because the rollback had\r\n // already eaten it, and a customer phoning support would have been in the\r\n // same position.\r\n dumpDaemonLogs();\r\n await driveService(\"uninstall\");\r\n console.error(`service install: this host is back to how it was found (the enrolment under ${windowsDataDir()} is untouched).`);\r\n console.error(\"The usual causes are a mistyped enrolment or an unreachable control plane \u2014 netdoc-collector doctor will say which.\");\r\n return 1;\r\n }\r\n\r\n console.log(\"\");\r\n console.log(`service install: done. The collector runs as the ${SERVICE_ID} service and starts with Windows.`);\r\n console.log(\" local UI http://localhost:9477 (this machine only \u2014 it binds 127.0.0.1 and nothing else)\");\r\n console.log(` check it netdoc-collector doctor | netdoc-collector service status`);\r\n console.log(\" remove it netdoc-collector service uninstall (elevated)\");\r\n return 0;\r\n}\r\n\r\nexport async function windowsUninstall(): Promise<number> {\r\n if (!isElevated()) {\r\n console.error(\"service uninstall: removing a Windows service needs elevation \u2014 run this from an ELEVATED PowerShell.\");\r\n return 2;\r\n }\r\n const state = queryScm();\r\n if (state && !state.installed) {\r\n console.log(`service uninstall: no ${SERVICE_ID} service is registered \u2014 nothing to remove.`);\r\n return 0;\r\n }\r\n\r\n const code = await driveService(\"uninstall\");\r\n if (code !== 0) return code;\r\n\r\n // Deletion of an SCM row is asynchronous while handles are open; poll\r\n // rather than trusting the removal call's word for it.\r\n if (!waitForScm((s) => !s.installed, 30_000)) {\r\n console.error(`service uninstall: ${SERVICE_ID} is still registered \u2014 close anything holding it (services.msc, a stuck wrapper) and run this again.`);\r\n return 1;\r\n }\r\n\r\n // The identity is deliberately NOT deleted \u2014 same reasoning as the old\r\n // uninstaller, now printed by the one command that replaced it.\r\n console.log(\"\");\r\n if (existsSync(windowsDataDir())) {\r\n console.log(`LEFT BEHIND ON PURPOSE: ${windowsDataDir()}`);\r\n console.log(\" It holds this collector's config and its KEY, which the portal will never show again.\");\r\n console.log(\" Reinstalling later picks that identity up automatically.\");\r\n console.log(\" The key is STILL VALID until it is retired: remove the collector in the portal\");\r\n console.log(\" (Admin -> Collectors), then purge with:\");\r\n console.log(` Remove-Item -Recurse -Force \"${windowsDataDir()}\"`);\r\n } else {\r\n console.log(`service uninstall: no config was found at ${windowsDataDir()} \u2014 nothing of the collector remains but the npm package.`);\r\n }\r\n console.log(\" Remove the code itself with: npm uninstall -g infracensus-collector\");\r\n return 0;\r\n}\r\n"],
5
+ "mappings": ";;;;;AAyBA,SAAS,iBAAiB;AAC1B,SAAS,YAAY,WAAW,cAAc,aAAa,cAAc;AACzE,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAmBd,IAAM,uBAAuB;AAC7B,IAAM,aAAa,GAAG,qBAAqB,QAAQ,WAAW,EAAE,EAAE,YAAY,CAAC;AAG/E,SAAS,iBAAyB;AACvC,SAAO,KAAK,QAAQ,IAAI,eAAe,mBAAmB,uBAAuB;AACnF;AACO,SAAS,oBAA4B;AAC1C,SAAO,KAAK,eAAe,GAAG,uBAAuB;AACvD;AAgCO,SAAS,aACd,MACA,UACyD;AACzD,QAAM,kBAAkB;AACxB,MAAI,aAAa,gBAAiB,QAAO,EAAE,WAAW,OAAO,SAAS,OAAO,OAAO,gBAAgB;AACpG,MAAI,cAAc,KAAK,IAAI,EAAG,QAAO,EAAE,WAAW,OAAO,SAAS,OAAO,OAAO,gBAAgB;AAChG,QAAM,OAAO,0BAA0B,KAAK,IAAI,IAAI,CAAC;AACrD,MAAI,CAAC,KAAM,QAAO,EAAE,WAAW,OAAO,SAAS,OAAO,OAAO,UAAU;AACvE,SAAO,EAAE,WAAW,MAAM,SAAS,SAAS,WAAW,OAAO,KAAK,YAAY,EAAE;AACnF;AAEA,SAAS,WAA2E;AAClF,QAAM,MAAM,UAAU,UAAU,CAAC,SAAS,UAAU,GAAG,EAAE,UAAU,OAAO,CAAC;AAC3E,MAAI,IAAI,MAAO,QAAO;AACtB,SAAO,aAAa,GAAG,IAAI,UAAU,EAAE,GAAG,IAAI,UAAU,EAAE,IAAI,IAAI,MAAM;AAC1E;AAOO,SAAS,aAAsB;AACpC,QAAM,MAAM,UAAU,OAAO,CAAC,SAAS,GAAG,EAAE,UAAU,OAAO,CAAC;AAC9D,SAAO,CAAC,IAAI,SAAS,IAAI,WAAW;AACtC;AAKA,SAAS,kBAA2I;AAClJ,MAAI;AACF,UAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,WAAOA,SAAQ,cAAc;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,gBAAwB;AACtC,QAAM,QAAQ,SAAS;AACvB,MAAI,UAAU,MAAM;AAClB,YAAQ,MAAM,iGAA4F;AAC1G,WAAO;AAAA,EACT;AACA,MAAI,CAAC,MAAM,WAAW;AACpB,YAAQ,IAAI,YAAY,oBAAoB,0EAAqE;AACjH,WAAO;AAAA,EACT;AACA,MAAI,MAAM,SAAS;AACjB,YAAQ,IAAI,YAAY,oBAAoB,MAAM,UAAU,2BAA2B;AACvF,WAAO;AAAA,EACT;AACA,UAAQ,IAAI,YAAY,oBAAoB,MAAM,UAAU,qBAAqB,MAAM,KAAK,qCAAgC,oBAAoB,iBAAiB;AACjK,SAAO;AACT;AAQA,SAAS,iBAAuB;AAC9B,QAAM,YAAY,KAAK,UAAU,QAAQ,QAAQ;AACjD,MAAI,CAAC,WAAW,SAAS,GAAG;AAC1B,YAAQ,MAAM,uBAAuB,SAAS,qDAAgD;AAC9F;AAAA,EACF;AAOA,MAAI,UAAoB,CAAC;AACzB,MAAI;AACF,cAAU,YAAY,SAAS;AAAA,EACjC,SAAS,KAAK;AACZ,YAAQ,MAAM,oBAAoB,SAAS,mBAAmB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,IAAI;AAClH;AAAA,EACF;AACA,UAAQ,MAAM,oBAAoB,SAAS,cAAc,QAAQ,KAAK,IAAI,KAAK,WAAW,EAAE;AAI5F,QAAM,SAAS,KAAK,eAAe,GAAG,MAAM;AAC5C,MAAI,WAAW,MAAM,GAAG;AACtB,QAAI;AACF,YAAM,aAAa,YAAY,MAAM;AACrC,cAAQ,MAAM,oBAAoB,MAAM,cAAc,WAAW,KAAK,IAAI,KAAK,WAAW,EAAE;AAC5F,iBAAW,SAAS,YAAY;AAC9B,cAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,YAAI;AACF,gBAAM,QAAQ,aAAa,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,OAAO;AACtE,kBAAQ,MAAM,YAAY,KAAK,UAAU,KAAK,IAAI,MAAM,QAAQ,EAAE,CAAC,OAAO,MAAM,MAAM,aAAa;AACnG,qBAAW,QAAQ,MAAM,MAAM,GAAG,EAAG,SAAQ,MAAM,KAAK,IAAI,EAAE;AAAA,QAChE,SAAS,KAAK;AACZ,kBAAQ,MAAM,YAAY,KAAK,iBAAiB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,OAAO;AAAA,QACzG;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAAgE;AAAA,EAC1E,OAAO;AACL,YAAQ,MAAM,oBAAoB,MAAM,kBAAkB;AAAA,EAC5D;AAEA,aAAW,SAAS,SAAS;AAK3B,UAAM,QAAQ,wBAAwB,KAAK,KAAK,KAAK,WAAW,KAAK,KAAK;AAC1E,UAAM,QAAQ,UAAU,KAAK,KAAK;AAClC,QAAI,CAAC,SAAS,CAAC,MAAO;AACtB,UAAM,OAAO,KAAK,WAAW,KAAK;AAClC,QAAI;AACF,YAAM,QAAQ,aAAa,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE,OAAO,OAAO;AACtE,cAAQ,MAAM,OAAO,KAAK,UAAU,KAAK,IAAI,MAAM,QAAQ,EAAE,CAAC,OAAO,MAAM,MAAM,aAAa;AAC9F,iBAAW,QAAQ,MAAM,MAAM,GAAG,EAAG,SAAQ,MAAM,KAAK,IAAI,EAAE;AAAA,IAChE,SAAS,KAAK;AACZ,cAAQ,MAAM,OAAO,KAAK,iBAAiB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,OAAO;AAAA,IACpG;AAAA,EACF;AASA,QAAM,WAAW,KAAK,WAAW,UAAU;AAC3C,QAAM,YAA8D;AAAA,IAClE,EAAE,OAAO,aAAa,KAAK,UAAU,MAAM,CAAC,MAAM,UAAU,EAAE;AAAA,IAC9D,EAAE,OAAO,gBAAgB,KAAK,UAAU,MAAM,CAAC,QAAQ,EAAE;AAAA,IACzD,EAAE,OAAO,cAAc,KAAK,UAAU,MAAM,CAAC,MAAM,EAAE;AAAA,EACvD;AACA,aAAW,EAAE,OAAO,KAAK,KAAK,KAAK,WAAW;AAC5C,QAAI,QAAQ,YAAY,CAAC,WAAW,GAAG,EAAG;AAC1C,UAAM,MAAM,UAAU,KAAK,MAAM,EAAE,UAAU,QAAQ,SAAS,IAAO,CAAC;AACtE,UAAM,MAAM,GAAG,IAAI,UAAU,EAAE,GAAG,IAAI,UAAU,EAAE,GAAG,KAAK;AAC1D,YAAQ,MAAM,OAAO,KAAK,UAAU,IAAI,UAAU,gBAAgB,OAAO;AACzE,eAAW,QAAQ,IAAI,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,GAAG,EAAG,SAAQ,MAAM,KAAK,IAAI,EAAE;AAAA,EAC7F;AAWA,QAAM,UAAU,IAAI,KAAK;AACzB,aAAW,WAAW,CAAC,UAAU,aAAa,GAAG;AAC/C,UAAM,MAAM;AAAA,MACV;AAAA,MACA,CAAC,MAAM,SAAS,oDAAoD,OAAO,OAAO,WAAW,QAAQ;AAAA,MACrG,EAAE,UAAU,QAAQ,SAAS,KAAQ,WAAW,IAAI,OAAO,KAAK;AAAA,IAClE;AACA,UAAM,OAAO,GAAG,IAAI,UAAU,EAAE,GAAG,IAAI,UAAU,EAAE;AAGnD,UAAM,SAAS,KAAK,MAAM,WAAW,EAAE;AAAA,MAAO,CAAC,MAC7C,wEAAwE,KAAK,CAAC;AAAA,IAChF;AACA,YAAQ,MAAM,kBAAkB,OAAO,iBAAiB,OAAO,MAAM,yBAAyB;AAC9F,eAAW,SAAS,OAAO,MAAM,EAAE,GAAG;AACpC,iBAAW,QAAQ,MAAM,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,GAAG,EAAE,EAAG,SAAQ,MAAM,KAAK,IAAI,EAAE;AAC/F,cAAQ,MAAM,MAAM;AAAA,IACtB;AAAA,EACF;AACF;AAMA,SAAS,aAAa,SAAmD;AACvE,QAAM,cAAc,gBAAgB;AACpC,MAAI,CAAC,aAAa;AAChB,YAAQ,MAAM,4HAAuH;AACrI,WAAO,QAAQ,QAAQ,CAAC;AAAA,EAC1B;AACA,QAAM,SAAS,KAAK,UAAU,QAAQ,QAAQ;AAC9C,QAAM,MAAM,IAAI,YAAY,QAAQ;AAAA;AAAA;AAAA,IAGlC,MAAM;AAAA,IACN,aACE;AAAA,IACF;AAAA,IACA,eAAe;AAAA,IACf,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKlB,SAAS,KAAK,eAAe,GAAG,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOtC,KACE,YAAY,YACR;AAAA,MACE,EAAE,MAAM,oBAAoB,OAAO,kBAAkB,EAAE;AAAA,MACvD,EAAE,MAAM,0BAA0B,OAAO,OAAO;AAAA,IAClD,IACA;AAAA,EACR,CAAC;AAOD,QAAM,gBAAgB,GAAI,IAAkC,EAAE;AAC9D,MAAI,kBAAkB,YAAY;AAChC,YAAQ;AAAA,MACN,WAAW,OAAO,kCAAkC,aAAa,6BAC3D,UAAU;AAAA,IAClB;AACA,WAAO,QAAQ,QAAQ,CAAC;AAAA,EAC1B;AAEA,SAAO,IAAI,QAAgB,CAAC,YAAY;AAItC,UAAM,UAAU,WAAW,MAAM,KAAK,GAAG,6BAA6B,OAAO,cAAc,GAAG,IAAO;AACrG,aAAS,KAAK,MAAc,SAAuB;AACjD,mBAAa,OAAO;AACpB,OAAC,SAAS,IAAI,QAAQ,MAAM,QAAQ,OAAO,WAAW,OAAO,KAAK,OAAO,EAAE;AAC3E,cAAQ,IAAI;AAAA,IACd;AAEA,QAAI,GAAG,SAAS,CAAC,QAAe,KAAK,GAAG,gBAAW,KAAK,WAAW,GAAG,EAAE,CAAC;AACzE,QAAI,GAAG,uBAAuB,MAAM,KAAK,GAAG,yGAAoG,CAAC;AAEjJ,QAAI,YAAY,WAAW;AACzB,UAAI,GAAG,oBAAoB,MAAM,KAAK,GAAG,MAAM,UAAU,sFAAiF,CAAC;AAW3I,UAAI,GAAG,WAAW,MAAM,KAAK,GAAG,iEAA4D,CAAC;AAC7F,UAAI,QAAQ;AAAA,IACd,OAAO;AACL,UAAI,GAAG,sBAAsB,MAAM,KAAK,GAAG,MAAM,UAAU,iDAA4C,CAAC;AACxG,UAAI,GAAG,aAAa,MAAM,KAAK,GAAG,YAAY,oBAAoB,MAAM,UAAU,WAAW,CAAC;AAU9F,gBAAU,UAAU,CAAC,QAAQ,UAAU,GAAG,EAAE,UAAU,QAAQ,SAAS,IAAO,CAAC;AAC/E,iBAAW,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,EAAE,SAAS,GAAM;AACpD,gBAAU,QAAQ,aAAa,UAAU,YAAY,SAAS,QAAQ,aAAa,UAAU,CAAC,MAAM,WAAW,MAAM,KAAK,UAAU,IAAI,CAAC,GAAG,GAAG,EAAE,SAAS,IAAO,CAAC;AAOlK,YAAM,YAAY,KAAK,UAAU,QAAQ,QAAQ;AACjD,YAAM,UAAU,CAAC,QAAwD;AACvE,YAAI,KAAK,SAAS,WAAW,CAAC,OAAO,KAAK,QAAQ,EAAE,EAAE,SAAS,QAAQ,EAAG,OAAM;AAChF,gBAAQ,MAAM,4DAA4D,IAAI,IAAI,8CAAyC;AAC3H,cAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,cAAM,QAAQ,MAAY;AACxB,cAAI;AACF,mBAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAClD,iBAAK,GAAG,YAAY,oBAAoB,MAAM,UAAU,sDAAsD;AAAA,UAChH,QAAQ;AACN,gBAAI,KAAK,IAAI,IAAI,UAAU;AACzB,mBAAK,GAAG,YAAY,oBAAoB,MAAM,UAAU,wBAAmB,SAAS,oGAAoG;AAAA,YAC1L,OAAO;AACL,yBAAW,OAAO,GAAK;AAAA,YACzB;AAAA,UACF;AAAA,QACF;AACA,mBAAW,OAAO,GAAK;AAAA,MACzB;AACA,cAAQ,KAAK,qBAAqB,OAAO;AACzC,UAAI,UAAU;AAAA,IAChB;AAAA,EACF,CAAC;AACH;AAGA,SAAS,WAAW,WAAqE,IAAqB;AAC5G,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,aAAS;AACP,UAAM,QAAQ,SAAS;AACvB,QAAI,SAAS,UAAU,KAAK,EAAG,QAAO;AACtC,QAAI,KAAK,IAAI,KAAK,SAAU,QAAO;AACnC,YAAQ,KAAK,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC,GAAG,GAAG,GAAG,GAAI;AAAA,EACnE;AACF;AAEA,eAAsB,iBAAkC;AACtD,MAAI,CAAC,WAAW,GAAG;AACjB,YAAQ,MAAM,6GAAwG;AACtH,WAAO;AAAA,EACT;AACA,QAAM,SAAS,KAAK,UAAU,QAAQ,QAAQ;AAC9C,MAAI,CAAC,WAAW,MAAM,GAAG;AACvB,YAAQ,MAAM,oBAAoB,MAAM,0EAAqE;AAC7G,WAAO;AAAA,EACT;AACA,MAAI,CAAC,WAAW,kBAAkB,CAAC,GAAG;AACpC,YAAQ,MAAM,4DAA4D,kBAAkB,CAAC,IAAI;AACjG,YAAQ,MAAM,sGAAiG;AAC/G,WAAO;AAAA,EACT;AACA,QAAM,UAAU,SAAS;AACzB,MAAI,SAAS,WAAW;AAWtB,QAAI,CAAC,QAAQ,KAAK,SAAS,WAAW,GAAG;AACvC,cAAQ,MAAM,sBAAsB,UAAU,iCAAiC;AAC/E,cAAQ,MAAM,wBAAwB,QAAQ,KAAK,EAAE;AACrD,cAAQ,MAAM,2DAA2D;AACzE,cAAQ,MAAM,gDAAgD;AAC9D,cAAQ,MAAM,0DAA0D;AACxE,aAAO;AAAA,IACT;AACA,YAAQ,IAAI,2DAAsD,UAAU,sBAAsB;AAClG,UAAM,UAAU,MAAM,iBAAiB;AACvC,QAAI,YAAY,GAAG;AACjB,cAAQ,MAAM,0FAA0F;AACxG,cAAQ,MAAM,qDAAqD,UAAU,EAAE;AAC/E,aAAO;AAAA,IACT;AAAA,EACF;AAYA,QAAM,SAAS,KAAK,eAAe,GAAG,MAAM;AAC5C,YAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAErC,QAAM,OAAO,MAAM,aAAa,SAAS;AACzC,MAAI,SAAS,EAAG,QAAO;AAKvB,MAAI,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,GAAM,GAAG;AAC3C,YAAQ,MAAM,0DAA0D,UAAU,gCAAgC;AAClH,mBAAe;AACf,WAAO;AAAA,EACT;AACA,QAAM,UAAU,UAAU,UAAU,CAAC,SAAS,UAAU,GAAG,EAAE,UAAU,QAAQ,SAAS,IAAO,CAAC;AAChG,QAAM,WAAW,GAAG,QAAQ,UAAU,EAAE,GAAG,QAAQ,UAAU,EAAE,GAAG,KAAK;AACvE,aAAW,QAAQ,SAAS,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,GAAG,EAAE,EAAG,SAAQ,IAAI,eAAe,IAAI,EAAE;AAC1G,MAAI,QAAQ,WAAW,KAAK,CAAC,wBAAwB,KAAK,QAAQ,GAAG;AACnE,YAAQ,MAAM,6CAA6C,UAAU,mBAAmB,QAAQ,UAAU,MAAM,8DAAyD;AACzK,mBAAe;AACf,UAAM,aAAa,WAAW;AAC9B,WAAO;AAAA,EACT;AAIA,MAAI,CAAC,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,SAAS,GAAM,GAAG;AAGxD,UAAM,OAAO,SAAS;AACtB,YAAQ;AAAA,MACN,kFACmB,SAAS,OAAO,uBAAuB,KAAK,YAAY,KAAK,QAAQ,eAAe;AAAA,IAEzG;AAQA,mBAAe;AACf,UAAM,aAAa,WAAW;AAC9B,YAAQ,MAAM,+EAA+E,eAAe,CAAC,iBAAiB;AAC9H,YAAQ,MAAM,0HAAqH;AACnI,WAAO;AAAA,EACT;AAEA,UAAQ,IAAI,EAAE;AACd,UAAQ,IAAI,oDAAoD,UAAU,mCAAmC;AAC7G,UAAQ,IAAI,uGAAkG;AAC9G,UAAQ,IAAI,+EAA+E;AAC3F,UAAQ,IAAI,iEAAiE;AAC7E,SAAO;AACT;AAEA,eAAsB,mBAAoC;AACxD,MAAI,CAAC,WAAW,GAAG;AACjB,YAAQ,MAAM,4GAAuG;AACrH,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,SAAS;AACvB,MAAI,SAAS,CAAC,MAAM,WAAW;AAC7B,YAAQ,IAAI,yBAAyB,UAAU,kDAA6C;AAC5F,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,MAAM,aAAa,WAAW;AAC3C,MAAI,SAAS,EAAG,QAAO;AAIvB,MAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAM,GAAG;AAC5C,YAAQ,MAAM,sBAAsB,UAAU,2GAAsG;AACpJ,WAAO;AAAA,EACT;AAIA,UAAQ,IAAI,EAAE;AACd,MAAI,WAAW,eAAe,CAAC,GAAG;AAChC,YAAQ,IAAI,2BAA2B,eAAe,CAAC,EAAE;AACzD,YAAQ,IAAI,yFAAyF;AACrG,YAAQ,IAAI,4DAA4D;AACxE,YAAQ,IAAI,kFAAkF;AAC9F,YAAQ,IAAI,2CAA2C;AACvD,YAAQ,IAAI,oCAAoC,eAAe,CAAC,GAAG;AAAA,EACrE,OAAO;AACL,YAAQ,IAAI,6CAA6C,eAAe,CAAC,+DAA0D;AAAA,EACrI;AACA,UAAQ,IAAI,uEAAuE;AACnF,SAAO;AACT;",
6
+ "names": ["require"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infracensus-collector",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "description": "InfraCensus on-premises collector: discovers your network (ICMP/ARP/SNMP/SSH, VMware, Hyper-V, SQL Server, load balancers, voice, Microsoft Entra ID, Azure) and pushes normalized inventory snapshots outbound to your InfraCensus control plane. Device credentials never leave the host.",
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/service/windows.ts"],
4
- "sourcesContent": ["/**\r\n * `netdoc-collector service install|uninstall|status` \u2014 the Windows half.\r\n *\r\n * This is packaging/windows-service.mjs moved into the CLI, seam intact: the\r\n * zip's install-windows.ps1 used to call that helper because node-windows is\r\n * an event-emitter API and PowerShell cannot await it \u2014 a script that\r\n * registers a service and exits before the SCM row exists reports success it\r\n * has not earned. The PowerShell layer is gone (npm is the installer now),\r\n * but the emitter-to-exit-code discipline and the 120-second timeout survive\r\n * unchanged, because node-windows can still silently emit nothing when winsw\r\n * misbehaves, and a command hanging forever on a customer's host is worse\r\n * than one that fails loudly with a name to search for.\r\n *\r\n * The service's DISPLAY NAME is the contract \u2014 the docs and the CI proof say\r\n * `Get-Service -DisplayName \"InfraCensus Collector\"` \u2014 and this file is the\r\n * one place that decides it (SERVICE_DISPLAY_NAME below; the SCM id is\r\n * node-windows's derivation, see SERVICE_ID's header).\r\n *\r\n * The config lives in C:\\ProgramData\\InfraCensus Collector rather than beside\r\n * the code, so that an `npm uninstall -g` (or update) cannot silently destroy\r\n * an identity whose key was shown exactly once. `enrol` creates it there (see\r\n * service/tools.ts); install refuses to register a service that would only\r\n * crash-loop on a missing identity, and uninstall leaves the directory behind\r\n * ON PURPOSE, saying so.\r\n */\r\nimport { spawnSync } from \"node:child_process\";\r\nimport { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from \"node:fs\";\r\nimport { createRequire } from \"node:module\";\r\nimport { join } from \"node:path\";\r\nimport { PKG_ROOT } from \"../pkgRoot.js\";\r\n\r\n/**\r\n * The DISPLAY name is the contract \u2014 it is what the docs teach and what\r\n * `Get-Service -DisplayName \"InfraCensus Collector\"` answers to.\r\n *\r\n * The SCM id is NOT ours to choose, which the first CI proof run established\r\n * the hard way: node-windows defines `id` as a GETTER over the name \u2014\r\n * `name.replace(/[^\\w]/gi, \"\").toLowerCase()` \u2014 ignores any `id` passed in,\r\n * and registers the service under `<id>.exe` because the same string names the\r\n * winsw wrapper executable. Passing `id: \"InfraCensusCollector\"` did nothing;\r\n * the service came up as `infracensuscollector.exe` while install's verify\r\n * polled the name we wished it had, saw nothing, and rolled a WORKING\r\n * registration back. So SERVICE_ID is now DERIVED here with node-windows's own\r\n * formula, and `install()` asserts the constructed Service agrees before\r\n * registering \u2014 a node-windows upgrade that changes its derivation fails\r\n * loudly at that assertion instead of rolling back mysteriously.\r\n */\r\nexport const SERVICE_DISPLAY_NAME = \"InfraCensus Collector\";\r\nexport const SERVICE_ID = `${SERVICE_DISPLAY_NAME.replace(/[^\\w]/gi, \"\").toLowerCase()}.exe`;\r\n\r\n/** ProgramData, not Program Files: the identity must survive the binaries. */\r\nexport function windowsDataDir(): string {\r\n return join(process.env.ProgramData ?? \"C:\\\\ProgramData\", \"InfraCensus Collector\");\r\n}\r\nexport function windowsConfigPath(): string {\r\n return join(windowsDataDir(), \"collector.config.json\");\r\n}\r\n\r\n/**\r\n * `sc.exe query` prints `STATE : 4 RUNNING`. The word is what we read, not\r\n * the number \u2014 the word is what an operator sees in every other tool, and the\r\n * two agreeing is worth more than saving a regex. (Same parse the doctor's\r\n * service-state library uses; duplicated here because the CLI must not import\r\n * from scripts/ \u2014 see service/index.ts.)\r\n */\r\nexport function parseScQuery(text: string): { installed: boolean; running: boolean; state: string } {\r\n if (/FAILED 1060/.test(text)) return { installed: false, running: false, state: \"not installed\" };\r\n const word = /STATE\\s*:\\s*\\d+\\s+(\\w+)/.exec(text)?.[1];\r\n if (!word) return { installed: false, running: false, state: \"unknown\" };\r\n return { installed: true, running: word === \"RUNNING\", state: word.toLowerCase() };\r\n}\r\n\r\nfunction queryScm(): { installed: boolean; running: boolean; state: string } | null {\r\n const res = spawnSync(\"sc.exe\", [\"query\", SERVICE_ID], { encoding: \"utf8\" });\r\n if (res.error) return null;\r\n return parseScQuery(`${res.stdout ?? \"\"}${res.stderr ?? \"\"}`);\r\n}\r\n\r\n/**\r\n * Elevation check: `net session` succeeds only in an elevated process. The\r\n * registrations below fail without it anyway \u2014 the point of asking first is\r\n * to fail with the fix in the message instead of an opaque SCM error.\r\n */\r\nexport function isElevated(): boolean {\r\n const res = spawnSync(\"net\", [\"session\"], { encoding: \"utf8\" });\r\n return !res.error && res.status === 0;\r\n}\r\n\r\n/** node-windows is CommonJS; createRequire resolves it from the installed\r\n * package's own node_modules (it is an optionalDependency \u2014 present on the\r\n * platform that needs it, never shipped to a Linux host's payload). */\r\nfunction loadNodeWindows(): { Service: new (config: object) => NodeJS.EventEmitter & { install(): void; uninstall(): void; start(): void } } | null {\r\n try {\r\n const require = createRequire(import.meta.url);\r\n return require(\"node-windows\") as ReturnType<typeof loadNodeWindows>;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport function windowsStatus(): number {\r\n const state = queryScm();\r\n if (state === null) {\r\n console.error(\"service status: sc.exe is not available \u2014 cannot ask the Service Control Manager anything.\");\r\n return 2;\r\n }\r\n if (!state.installed) {\r\n console.log(`service \"${SERVICE_DISPLAY_NAME}\": not installed \u2014 run: netdoc-collector service install (elevated)`);\r\n return 2;\r\n }\r\n if (state.running) {\r\n console.log(`service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}): registered and running`);\r\n return 0;\r\n }\r\n console.log(`service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}): registered but ${state.state} \u2014 Get-Service -DisplayName \"${SERVICE_DISPLAY_NAME}\" has the story`);\r\n return 1;\r\n}\r\n\r\n/**\r\n * The last ~40 lines of every log the node-windows service host wrote \u2014\r\n * `<script dir>/daemon/<id>.{wrapper,out,err}.log`. Called on the\r\n * verify-failure path ONLY, before the rollback deletes the directory: these\r\n * files are the one place a wrapper-level death explains itself.\r\n */\r\nfunction dumpDaemonLogs(): void {\r\n const daemonDir = join(PKG_ROOT, \"dist\", \"daemon\");\r\n if (!existsSync(daemonDir)) {\r\n console.error(`service install: no ${daemonDir} to read \u2014 the service host never wrote a log.`);\r\n return;\r\n }\r\n // Enumerate and dump EVERYTHING, never a hand-guessed name list: the first\r\n // version of this looked for three expected filenames, found none, and\r\n // printed NOTHING \u2014 silent in exactly the moment it existed for, which is\r\n // the same defect it was built to fix, one layer down. Whatever the host\r\n // wrote under whatever name it chose, this prints it or says the directory\r\n // was empty.\r\n let entries: string[] = [];\r\n try {\r\n entries = readdirSync(daemonDir);\r\n } catch (err) {\r\n console.error(`service install: ${daemonDir} is unreadable (${err instanceof Error ? err.message : String(err)}).`);\r\n return;\r\n }\r\n console.error(`service install: ${daemonDir} contains: ${entries.join(\", \") || \"(nothing)\"}`);\r\n // The winsw LOG destination is the ProgramData logs dir (the logpath the\r\n // install sets) \u2014 list and tail that too; a healthy-but-then-dead child\r\n // writes its last words THERE, not beside the exe.\r\n const logDir = join(windowsDataDir(), \"logs\");\r\n if (existsSync(logDir)) {\r\n try {\r\n const logEntries = readdirSync(logDir);\r\n console.error(`service install: ${logDir} contains: ${logEntries.join(\", \") || \"(nothing)\"}`);\r\n for (const entry of logEntries) {\r\n const file = join(logDir, entry);\r\n try {\r\n const lines = readFileSync(file, \"utf8\").split(/\\r?\\n/).filter(Boolean);\r\n console.error(`--- logs/${entry} (last ${Math.min(lines.length, 60)} of ${lines.length} lines) ---`);\r\n for (const line of lines.slice(-60)) console.error(` ${line}`);\r\n } catch (err) {\r\n console.error(`--- logs/${entry}: unreadable (${err instanceof Error ? err.message : String(err)}) ---`);\r\n }\r\n }\r\n } catch { /* enumerating evidence must never throw past the rollback */ }\r\n } else {\r\n console.error(`service install: ${logDir} does not exist.`);\r\n }\r\n\r\n for (const entry of entries) {\r\n // Logs AND the winsw XML: a daemon dir with the exe and its XML but no log\r\n // at all (the round-six signature) means winsw died before it could open\r\n // one \u2014 and the thing winsw reads first is the XML, as it stands AFTER\r\n // node-windows's post-install munging. Those exact bytes are the evidence.\r\n const isLog = /\\.(log|txt|out|err)$/i.test(entry) || /\\.log\\./i.test(entry);\r\n const isXml = /\\.xml$/i.test(entry);\r\n if (!isLog && !isXml) continue;\r\n const file = join(daemonDir, entry);\r\n try {\r\n const lines = readFileSync(file, \"utf8\").split(/\\r?\\n/).filter(Boolean);\r\n console.error(`--- ${entry} (last ${Math.min(lines.length, 60)} of ${lines.length} lines) ---`);\r\n for (const line of lines.slice(-60)) console.error(` ${line}`);\r\n } catch (err) {\r\n console.error(`--- ${entry}: unreadable (${err instanceof Error ? err.message : String(err)}) ---`);\r\n }\r\n }\r\n\r\n // Three more witnesses, each answering what the files cannot:\r\n // sc qc \u2014 the SCM's registered config (binPath, logon account),\r\n // i.e. what Windows was actually told to run and as whom;\r\n // <winsw> status \u2014 winsw's own view of its state machine;\r\n // <winsw> test \u2014 winsw runs its service logic IN CONSOLE MODE and prints\r\n // the error a service start swallows. Time-boxed, because\r\n // on a healthy install `test` runs the daemon indefinitely.\r\n const winswExe = join(daemonDir, SERVICE_ID);\r\n const witnesses: { label: string; cmd: string; args: string[] }[] = [\r\n { label: \"sc.exe qc\", cmd: \"sc.exe\", args: [\"qc\", SERVICE_ID] },\r\n { label: \"winsw status\", cmd: winswExe, args: [\"status\"] },\r\n { label: \"winsw test\", cmd: winswExe, args: [\"test\"] },\r\n ];\r\n for (const { label, cmd, args } of witnesses) {\r\n if (cmd !== \"sc.exe\" && !existsSync(cmd)) continue;\r\n const res = spawnSync(cmd, args, { encoding: \"utf8\", timeout: 20_000 });\r\n const out = `${res.stdout ?? \"\"}${res.stderr ?? \"\"}`.trim();\r\n console.error(`--- ${label} (exit ${res.status ?? \"timeout/killed\"}) ---`);\r\n for (const line of out.split(/\\r?\\n/).filter(Boolean).slice(-40)) console.error(` ${line}`);\r\n }\r\n\r\n // The WINDOWS EVENT LOG \u2014 the witness a process that dies before its own\r\n // logging cannot silence. Round eight eliminated every file-based\r\n // explanation: the XML is sound, the SCM config is sound, the log directory\r\n // is SYSTEM-writable and stayed EMPTY, so winsw is dying before its logging\r\n // initializes. The SCM records every service death in the System channel\r\n // (event 7000/7009/7034-class), and winsw writes its own errors to the\r\n // Application channel before it writes files. `wevtutil` rather than\r\n // PowerShell, so this works from any elevated shell with no profile and no\r\n // codepage hazard.\r\n const sinceMs = 5 * 60 * 1000;\r\n for (const channel of [\"System\", \"Application\"]) {\r\n const res = spawnSync(\r\n \"wevtutil.exe\",\r\n [\"qe\", channel, `/q:*[System[TimeCreated[timediff(@SystemTime) <= ${sinceMs}]]]`, \"/f:text\", \"/c:300\"],\r\n { encoding: \"utf8\", timeout: 30_000, maxBuffer: 8 * 1024 * 1024 },\r\n );\r\n const text = `${res.stdout ?? \"\"}${res.stderr ?? \"\"}`;\r\n // Events come as multi-line blocks separated by \"Event[\" headers; keep the\r\n // whole block when any of its lines mentions the service or its host.\r\n const blocks = text.split(/^Event\\[/m).filter((b) =>\r\n /infracensus|winsw|Service Control Manager.*(?:terminat|fail|did not)/i.test(b),\r\n );\r\n console.error(`--- event log: ${channel} (last 5 min, ${blocks.length} matching event(s)) ---`);\r\n for (const block of blocks.slice(-8)) {\r\n for (const line of block.split(/\\r?\\n/).filter(Boolean).slice(0, 16)) console.error(` ${line}`);\r\n console.error(\" --\");\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Turn one node-windows operation into an exit code. Everything here is \"wait\r\n * for the emitter, or for the timeout, whichever speaks first\".\r\n */\r\nfunction driveService(command: \"install\" | \"uninstall\"): Promise<number> {\r\n const nodeWindows = loadNodeWindows();\r\n if (!nodeWindows) {\r\n console.error(\"service: the node-windows package is missing from this install \u2014 reinstall with: npm install -g infracensus-collector\");\r\n return Promise.resolve(1);\r\n }\r\n const script = join(PKG_ROOT, \"dist\", \"cli.js\");\r\n const svc = new nodeWindows.Service({\r\n // NO `id` here \u2014 node-windows ignores it (see SERVICE_ID's header). The\r\n // display name is the one input; the SCM id falls out of it.\r\n name: SERVICE_DISPLAY_NAME,\r\n description:\r\n \"InfraCensus on-premises network collector. Polls the control plane outbound for scan jobs; nothing connects in. Local UI on http://localhost:9477 (127.0.0.1 only).\",\r\n script,\r\n scriptOptions: \"run\",\r\n workingDirectory: PKG_ROOT,\r\n // SYSTEM-writable, ALWAYS set: winsw defaults its logs to the exe directory\r\n // (the npm prefix, admin-ACLed), and a winsw that cannot open its log dies\r\n // as a service without a trace \u2014 round seven, in one line. The installer\r\n // creates this directory before registration.\r\n logpath: join(windowsDataDir(), \"logs\"),\r\n // Two things the service process must know that the package cannot carry:\r\n // where the enrolled identity lives (ProgramData, so it survives an\r\n // `npm uninstall -g` of the binaries), and that in-app restarts must EXIT\r\n // for the service host to bring back rather than re-exec \u2014 two restart\r\n // authorities is how a host ends up with two daemons (core/daemon.ts\r\n // restartSelf; it exits 75, which the wrapper restarts).\r\n env:\r\n command === \"install\"\r\n ? [\r\n { name: \"COLLECTOR_CONFIG\", value: windowsConfigPath() },\r\n { name: \"COLLECTOR_RESTART_MODE\", value: \"exit\" },\r\n ]\r\n : undefined,\r\n });\r\n\r\n // The derivation assertion SERVICE_ID's header promises: node-windows's\r\n // Service computes its own id and exe name, and every sc.exe query, the\r\n // uninstaller, the CI proof and the docs' display-name teaching all assume\r\n // this file knows what that computation produced. If an upgrade changes it,\r\n // refuse HERE, before anything is registered under a name nothing can find.\r\n const actualScmName = `${(svc as unknown as { id: string }).id}.exe`;\r\n if (actualScmName !== SERVICE_ID) {\r\n console.error(\r\n `service ${command}: node-windows would register \"${actualScmName}\" but this build expects ` +\r\n `\"${SERVICE_ID}\" \u2014 its id derivation changed; update SERVICE_ID in src/service/windows.ts.`,\r\n );\r\n return Promise.resolve(1);\r\n }\r\n\r\n return new Promise<number>((resolve) => {\r\n // The timeout exists because node-windows can silently emit nothing when\r\n // winsw misbehaves, and a hang on a customer's host is worse than a loud\r\n // failure with a name to search for.\r\n const timeout = setTimeout(() => done(1, `timed out waiting for the ${command} to complete`), 120_000);\r\n function done(code: number, message: string): void {\r\n clearTimeout(timeout);\r\n (code === 0 ? console.log : console.error)(`service ${command}: ${message}`);\r\n resolve(code);\r\n }\r\n\r\n svc.on(\"error\", (err: Error) => done(1, `error \u2014 ${err?.message ?? err}`));\r\n svc.on(\"invalidinstallation\", () => done(1, \"the existing installation is damaged \u2014 run: netdoc-collector service uninstall, then install again\"));\r\n\r\n if (command === \"install\") {\r\n svc.on(\"alreadyinstalled\", () => done(1, `a \"${SERVICE_ID}\" service is already registered \u2014 run: netdoc-collector service uninstall first`));\r\n // The 'install' event is NOT the registration \u2014 that is the finding ten\r\n // CI rounds paid for. node-windows emits it BEFORE the SCM row exists:\r\n // round ten issued `sc start` from this very handler and the SCM\r\n // answered 1060, \"the specified service does not exist\", after which\r\n // the registration quietly completed and the service sat\r\n // installed-but-stopped forever. (Its own start() had been failing the\r\n // same way all along \u2014 `NET START` into the void \u2014 with the error\r\n // swallowed and 'start' emitted anyway.) So this handler claims only\r\n // what the event can honestly mean: the request was made. The caller\r\n // waits for the SCM row itself, then starts it.\r\n svc.on(\"install\", () => done(0, `registration requested \u2014 waiting for the SCM row to appear`));\r\n svc.install();\r\n } else {\r\n svc.on(\"alreadyuninstalled\", () => done(0, `no ${SERVICE_ID} service is registered \u2014 nothing to remove`));\r\n svc.on(\"uninstall\", () => done(0, `service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}) removed`));\r\n\r\n // STOP FIRST, OURSELVES, and wait until the SCM says stopped \u2014 then let\r\n // node-windows deregister. Its own uninstall stops and then deletes its\r\n // files in the same breath, and the winsw exe is still briefly locked by\r\n // the exiting service process; its `rm` is a bare unlinkSync with no\r\n // error handling, so that race is an UNCAUGHT EXCEPTION that kills this\r\n // process mid-uninstall (first seen 2026-08-27, after eleven rounds of\r\n // the install side). Stopping first and settling shrinks the window; the\r\n // uncaughtException net below closes what remains.\r\n spawnSync(\"sc.exe\", [\"stop\", SERVICE_ID], { encoding: \"utf8\", timeout: 30_000 });\r\n waitForScm((s) => !s.installed || !s.running, 30_000);\r\n spawnSync(process.platform === \"win32\" ? \"cmd.exe\" : \"sleep\", process.platform === \"win32\" ? [\"/c\", \"timeout\", \"/t\", \"2\", \"/nobreak\"] : [\"2\"], { timeout: 10_000 });\r\n\r\n // node-windows's post-stop file deletion can still lose the lock race.\r\n // When it does, the SCM row is ALREADY GONE (winsw uninstall ran) and\r\n // only the file cleanup died \u2014 so finish that cleanup ourselves, with\r\n // the retries the lock actually needs, instead of crashing a successful\r\n // uninstall at its last step.\r\n const daemonDir = join(PKG_ROOT, \"dist\", \"daemon\");\r\n const onCrash = (err: Error & { code?: string; path?: string }): void => {\r\n if (err?.code !== \"EPERM\" || !String(err?.path ?? \"\").includes(\"daemon\")) throw err;\r\n console.error(`service uninstall: a service-host file was still locked (${err.path}) \u2014 retrying the cleanup for up to 20s.`);\r\n const deadline = Date.now() + 20_000;\r\n const retry = (): void => {\r\n try {\r\n rmSync(daemonDir, { recursive: true, force: true });\r\n done(0, `service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}) removed (host files cleaned up after a short lock)`);\r\n } catch {\r\n if (Date.now() > deadline) {\r\n done(0, `service \"${SERVICE_DISPLAY_NAME}\" (${SERVICE_ID}) removed \u2014 but ${daemonDir} could not be fully deleted (a file stayed locked). Remove it by hand; the service itself is gone.`);\r\n } else {\r\n setTimeout(retry, 2_000);\r\n }\r\n }\r\n };\r\n setTimeout(retry, 2_000);\r\n };\r\n process.once(\"uncaughtException\", onCrash);\r\n svc.uninstall();\r\n }\r\n });\r\n}\r\n\r\n/** Poll the SCM until the predicate holds, or the deadline passes. */\r\nfunction waitForScm(predicate: (s: { installed: boolean; running: boolean }) => boolean, ms: number): boolean {\r\n const deadline = Date.now() + ms;\r\n for (;;) {\r\n const state = queryScm();\r\n if (state && predicate(state)) return true;\r\n if (Date.now() >= deadline) return false;\r\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);\r\n }\r\n}\r\n\r\nexport async function windowsInstall(): Promise<number> {\r\n if (!isElevated()) {\r\n console.error(\"service install: registering a Windows service needs elevation \u2014 run this from an ELEVATED PowerShell.\");\r\n return 2;\r\n }\r\n const script = join(PKG_ROOT, \"dist\", \"cli.js\");\r\n if (!existsSync(script)) {\r\n console.error(`service install: ${script} does not exist \u2014 refusing to register a service that cannot start.`);\r\n return 1;\r\n }\r\n if (!existsSync(windowsConfigPath())) {\r\n console.error(`service install: this host is not enrolled (no config at ${windowsConfigPath()}).`);\r\n console.error(\"Run: netdoc-collector enrol (from this elevated PowerShell) \u2014 then install the service again.\");\r\n return 2;\r\n }\r\n const already = queryScm();\r\n if (already?.installed) {\r\n console.error(`service install: a ${SERVICE_ID} service is already registered \u2014 run: netdoc-collector service uninstall first.`);\r\n return 1;\r\n }\r\n\r\n // The service host's log directory, created HERE, under the SYSTEM-writable\r\n // ProgramData tree \u2014 never left to winsw's default of \"next to the exe\".\r\n // The exe lives under the npm global prefix, whose ACLs belong to whoever\r\n // installed node/npm; the SERVICE runs as LocalSystem. Round seven's\r\n // witnesses showed the exact consequence of that mismatch: winsw ran\r\n // perfectly in console mode (as the admin who could write beside the exe)\r\n // and died as a service without managing to write one log line \u2014 winsw 1.x\r\n // treats \"cannot open my log file\" as instant, traceless death. This dir is\r\n // created by the elevated installer, so it exists with inherited\r\n // SYSTEM-writable ACLs before winsw ever needs it.\r\n const logDir = join(windowsDataDir(), \"logs\");\r\n mkdirSync(logDir, { recursive: true });\r\n\r\n const code = await driveService(\"install\");\r\n if (code !== 0) return code;\r\n\r\n // FIRST the row, THEN the start \u2014 in that order, asked of the SCM itself.\r\n // node-windows's events fire ahead of the registration (see driveService),\r\n // so the only thing that proves the service exists is sc.exe saying so.\r\n if (!waitForScm((s) => s.installed, 30_000)) {\r\n console.error(`service install: the registration was requested but no ${SERVICE_ID} row ever appeared in the SCM.`);\r\n dumpDaemonLogs();\r\n return 1;\r\n }\r\n const started = spawnSync(\"sc.exe\", [\"start\", SERVICE_ID], { encoding: \"utf8\", timeout: 30_000 });\r\n const startOut = `${started.stdout ?? \"\"}${started.stderr ?? \"\"}`.trim();\r\n for (const line of startOut.split(/\\r?\\n/).filter(Boolean).slice(0, 12)) console.log(` sc start: ${line}`);\r\n if (started.status !== 0 && !/START_PENDING|RUNNING/.test(startOut)) {\r\n console.error(`service install: the SCM refused to start ${SERVICE_ID} (sc start exit ${started.status ?? \"none\"}) \u2014 its output is above. Rolling the registration back.`);\r\n dumpDaemonLogs();\r\n await driveService(\"uninstall\");\r\n return 1;\r\n }\r\n\r\n // Verify the way the operator's own tools would: the SCM, not the\r\n // registration call's word for it.\r\n if (!waitForScm((s) => s.installed && s.running, 60_000)) {\r\n // The last state observed rides in the message: \"stuck at start_pending\"\r\n // and \"stopped\" are different diagnoses, and the poll knows which it saw.\r\n const last = queryScm();\r\n console.error(\r\n `service install: the service registered but never reached Running` +\r\n ` (last state: ${last === null ? \"sc.exe unavailable\" : last.installed ? last.state : \"not installed\"})` +\r\n ` \u2014 rolling the registration back.`,\r\n );\r\n // EVIDENCE BEFORE THE ROLLBACK, which is about to delete it: node-windows\r\n // keeps the service host's logs in a `daemon` directory beside the script\r\n // it wraps, and uninstall removes that directory wholesale. A service that\r\n // dies inside the wrapper leaves its only explanation THERE \u2014 the first CI\r\n // diagnosis of this path had nothing to read because the rollback had\r\n // already eaten it, and a customer phoning support would have been in the\r\n // same position.\r\n dumpDaemonLogs();\r\n await driveService(\"uninstall\");\r\n console.error(`service install: this host is back to how it was found (the enrolment under ${windowsDataDir()} is untouched).`);\r\n console.error(\"The usual causes are a mistyped enrolment or an unreachable control plane \u2014 netdoc-collector doctor will say which.\");\r\n return 1;\r\n }\r\n\r\n console.log(\"\");\r\n console.log(`service install: done. The collector runs as the ${SERVICE_ID} service and starts with Windows.`);\r\n console.log(\" local UI http://localhost:9477 (this machine only \u2014 it binds 127.0.0.1 and nothing else)\");\r\n console.log(` check it netdoc-collector doctor | netdoc-collector service status`);\r\n console.log(\" remove it netdoc-collector service uninstall (elevated)\");\r\n return 0;\r\n}\r\n\r\nexport async function windowsUninstall(): Promise<number> {\r\n if (!isElevated()) {\r\n console.error(\"service uninstall: removing a Windows service needs elevation \u2014 run this from an ELEVATED PowerShell.\");\r\n return 2;\r\n }\r\n const state = queryScm();\r\n if (state && !state.installed) {\r\n console.log(`service uninstall: no ${SERVICE_ID} service is registered \u2014 nothing to remove.`);\r\n return 0;\r\n }\r\n\r\n const code = await driveService(\"uninstall\");\r\n if (code !== 0) return code;\r\n\r\n // Deletion of an SCM row is asynchronous while handles are open; poll\r\n // rather than trusting the removal call's word for it.\r\n if (!waitForScm((s) => !s.installed, 30_000)) {\r\n console.error(`service uninstall: ${SERVICE_ID} is still registered \u2014 close anything holding it (services.msc, a stuck wrapper) and run this again.`);\r\n return 1;\r\n }\r\n\r\n // The identity is deliberately NOT deleted \u2014 same reasoning as the old\r\n // uninstaller, now printed by the one command that replaced it.\r\n console.log(\"\");\r\n if (existsSync(windowsDataDir())) {\r\n console.log(`LEFT BEHIND ON PURPOSE: ${windowsDataDir()}`);\r\n console.log(\" It holds this collector's config and its KEY, which the portal will never show again.\");\r\n console.log(\" Reinstalling later picks that identity up automatically.\");\r\n console.log(\" The key is STILL VALID until it is retired: remove the collector in the portal\");\r\n console.log(\" (Admin -> Collectors), then purge with:\");\r\n console.log(` Remove-Item -Recurse -Force \"${windowsDataDir()}\"`);\r\n } else {\r\n console.log(`service uninstall: no config was found at ${windowsDataDir()} \u2014 nothing of the collector remains but the npm package.`);\r\n }\r\n console.log(\" Remove the code itself with: npm uninstall -g infracensus-collector\");\r\n return 0;\r\n}\r\n"],
5
- "mappings": "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",
6
- "names": ["require"]
7
- }