@ia-qa/self-healing 1.6.7 → 1.6.10

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.
Files changed (88) hide show
  1. package/README.md +111 -0
  2. package/TUTORIAL.md +128 -2
  3. package/assets/logoKawaiiGreenTiny.png +0 -0
  4. package/dist/aom.d.ts +8 -0
  5. package/dist/aom.js.map +1 -1
  6. package/dist/cli/args.js +4 -2
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/audit.js +2 -8
  9. package/dist/cli/audit.js.map +1 -1
  10. package/dist/cli/diff.d.ts +25 -1
  11. package/dist/cli/diff.js +176 -20
  12. package/dist/cli/diff.js.map +1 -1
  13. package/dist/cli/emit.d.ts +71 -0
  14. package/dist/cli/emit.js +150 -0
  15. package/dist/cli/emit.js.map +1 -0
  16. package/dist/cli/fix.js +41 -3
  17. package/dist/cli/fix.js.map +1 -1
  18. package/dist/cli/history.d.ts +12 -0
  19. package/dist/cli/history.js +95 -0
  20. package/dist/cli/history.js.map +1 -0
  21. package/dist/cli/index.js +68 -3
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/ingest.js +5 -8
  24. package/dist/cli/ingest.js.map +1 -1
  25. package/dist/cli/init.js +32 -0
  26. package/dist/cli/init.js.map +1 -1
  27. package/dist/cli/map.d.ts +14 -1
  28. package/dist/cli/map.js +144 -33
  29. package/dist/cli/map.js.map +1 -1
  30. package/dist/cli/run.js +91 -8
  31. package/dist/cli/run.js.map +1 -1
  32. package/dist/cli/ui.d.ts +16 -0
  33. package/dist/cli/ui.js +294 -0
  34. package/dist/cli/ui.js.map +1 -0
  35. package/dist/cli-ai/index.js +3 -3
  36. package/dist/cli-ai/index.js.map +1 -1
  37. package/dist/config.d.ts +50 -0
  38. package/dist/config.js +63 -1
  39. package/dist/config.js.map +1 -1
  40. package/dist/evidence.d.ts +130 -0
  41. package/dist/evidence.js +232 -0
  42. package/dist/evidence.js.map +1 -0
  43. package/dist/fixEngine.d.ts +2 -0
  44. package/dist/fixEngine.js +7 -0
  45. package/dist/fixEngine.js.map +1 -1
  46. package/dist/history.d.ts +145 -0
  47. package/dist/history.js +230 -0
  48. package/dist/history.js.map +1 -0
  49. package/dist/htmlReport.d.ts +47 -0
  50. package/dist/htmlReport.js +103 -1
  51. package/dist/htmlReport.js.map +1 -1
  52. package/dist/ingest.d.ts +18 -0
  53. package/dist/ingest.js +0 -0
  54. package/dist/ingest.js.map +1 -1
  55. package/dist/junit.d.ts +53 -0
  56. package/dist/junit.js +150 -0
  57. package/dist/junit.js.map +1 -0
  58. package/dist/mcp/server.js +46 -5
  59. package/dist/mcp/server.js.map +1 -1
  60. package/dist/nameMask.d.ts +110 -0
  61. package/dist/nameMask.js +0 -0
  62. package/dist/nameMask.js.map +1 -0
  63. package/dist/ui/icon.d.ts +41 -0
  64. package/dist/ui/icon.js +115 -0
  65. package/dist/ui/icon.js.map +1 -0
  66. package/dist/ui/lock.d.ts +40 -0
  67. package/dist/ui/lock.js +138 -0
  68. package/dist/ui/lock.js.map +1 -0
  69. package/dist/ui/page.d.ts +1 -0
  70. package/dist/ui/page.js +657 -0
  71. package/dist/ui/page.js.map +1 -0
  72. package/dist/ui/registry.d.ts +62 -0
  73. package/dist/ui/registry.js +139 -0
  74. package/dist/ui/registry.js.map +1 -0
  75. package/dist/ui/server.d.ts +76 -0
  76. package/dist/ui/server.js +607 -0
  77. package/dist/ui/server.js.map +1 -0
  78. package/dist/ui/shortcut.d.ts +90 -0
  79. package/dist/ui/shortcut.js +176 -0
  80. package/dist/ui/shortcut.js.map +1 -0
  81. package/dist/update.d.ts +104 -0
  82. package/dist/update.js +297 -0
  83. package/dist/update.js.map +1 -0
  84. package/dist/volatile.d.ts +2 -0
  85. package/dist/volatile.js +2 -0
  86. package/dist/volatile.js.map +1 -1
  87. package/package.json +3 -2
  88. package/skills/ia-qa-heal/SKILL.md +37 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * A desktop shortcut that opens this project's console — `ia-qa-heal ui --shortcut`.
3
+ *
4
+ * The console already opens a browser by itself, so the only thing standing
5
+ * between a manual QA and their drift report is a terminal they have no reason
6
+ * to own. This removes it: double-click an icon, the console starts in the
7
+ * right project directory and the browser opens on it. No URL is ever typed,
8
+ * copied, or seen — which is also what keeps the session token out of the way
9
+ * instead of making it something a person has to handle.
10
+ *
11
+ * Nothing about the icon is cosmetic-only: an unlabelled `.cmd` on a desktop is
12
+ * a file people are right to be wary of double-clicking.
13
+ *
14
+ * **Per platform, and the honest limits.**
15
+ *
16
+ * - **Windows** — a real `.lnk`, which is the only shape that can carry a custom
17
+ * icon. It is created by shelling out to PowerShell's `WScript.Shell` (present
18
+ * on every supported Windows, and not a dependency), and it targets a `.cmd`
19
+ * kept *inside* the project rather than a second file on the desktop. If
20
+ * PowerShell refuses, the `.cmd` is placed on the desktop instead and the
21
+ * caller says so — a working shortcut with the default icon beats none.
22
+ * - **Linux** — a `.desktop` entry, whose `Icon=` takes the PNG directly.
23
+ * - **macOS** — a `.command`, with the **system's default icon**. Setting a
24
+ * custom one needs a resource fork via `Rez`/`fileicon` or Finder scripting;
25
+ * both are fragile and neither is worth a silent failure. Stated, not hidden.
26
+ */
27
+ export type Platform = 'win32' | 'darwin' | 'linux';
28
+ export interface ShortcutFile {
29
+ file: string;
30
+ contents: string;
31
+ mode?: number;
32
+ }
33
+ /** A Windows `.lnk`, which cannot be written as text — see `installShortcut`. */
34
+ export interface LnkPlan {
35
+ file: string;
36
+ target: string;
37
+ workingDir: string;
38
+ iconPath: string;
39
+ description: string;
40
+ }
41
+ export interface ShortcutPlan {
42
+ /** Plain-text files to write, in order. */
43
+ files: ShortcutFile[];
44
+ /** Present on Windows only. */
45
+ lnk?: LnkPlan;
46
+ /** Where the .ico has to be written for the lnk to find it. */
47
+ icoPath?: string;
48
+ /** True when this platform gets the system's default icon. */
49
+ defaultIconOnly?: boolean;
50
+ }
51
+ /**
52
+ * The desktop name. It carries the site because this icon sits on the desktop
53
+ * of every QA who uses the tool, which makes it the cheapest honest place to
54
+ * say where it comes from — and because "console" alone, months later, names
55
+ * nothing.
56
+ */
57
+ export declare const SHORTCUT_NAME = "ia-qa-heal console (ia-qa.com)";
58
+ /**
59
+ * What would be written, without writing it. Split out so the shape of every
60
+ * platform's launcher is testable on any platform — the one thing that would
61
+ * otherwise only ever be exercised on the maintainer's own OS.
62
+ */
63
+ export declare function planShortcut(opts: {
64
+ platform: Platform;
65
+ desktop: string;
66
+ projectDir: string;
67
+ configDir: string;
68
+ nodePath: string;
69
+ cliEntry: string;
70
+ logoPath: string;
71
+ name?: string;
72
+ }): ShortcutPlan;
73
+ /**
74
+ * Create the Windows `.lnk`. PowerShell's COM shell object is the only way to
75
+ * write one without either a native module or hand-assembling the binary Shell
76
+ * Link format. Returns false rather than throwing: the caller has a working
77
+ * fallback and a shortcut is not worth failing a command over.
78
+ *
79
+ * Every value is passed as an environment variable, never interpolated into the
80
+ * script text — a project path containing a quote would otherwise end up being
81
+ * executed as PowerShell.
82
+ */
83
+ export declare function createWindowsLnk(lnk: LnkPlan): boolean;
84
+ /**
85
+ * Where the desktop is. `XDG_DESKTOP_DIR` and localized folder names exist and
86
+ * are not resolvable without reading user config, so a missing `Desktop` falls
87
+ * back to the home directory rather than guessing wrong and writing an icon
88
+ * nobody finds.
89
+ */
90
+ export declare function desktopDir(home?: string): string;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SHORTCUT_NAME = void 0;
37
+ exports.planShortcut = planShortcut;
38
+ exports.createWindowsLnk = createWindowsLnk;
39
+ exports.desktopDir = desktopDir;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ /**
44
+ * The desktop name. It carries the site because this icon sits on the desktop
45
+ * of every QA who uses the tool, which makes it the cheapest honest place to
46
+ * say where it comes from — and because "console" alone, months later, names
47
+ * nothing.
48
+ */
49
+ exports.SHORTCUT_NAME = 'ia-qa-heal console (ia-qa.com)';
50
+ function quoteWin(s) {
51
+ return `"${s.replace(/"/g, '""')}"`;
52
+ }
53
+ function quotePosix(s) {
54
+ return `'${s.replace(/'/g, `'\\''`)}'`;
55
+ }
56
+ /**
57
+ * What would be written, without writing it. Split out so the shape of every
58
+ * platform's launcher is testable on any platform — the one thing that would
59
+ * otherwise only ever be exercised on the maintainer's own OS.
60
+ */
61
+ function planShortcut(opts) {
62
+ const name = opts.name ?? exports.SHORTCUT_NAME;
63
+ const description = 'Open the local selector-drift console for this project — ia-qa.com';
64
+ if (opts.platform === 'win32') {
65
+ // The .cmd lives in the project, not on the desktop: the desktop gets one
66
+ // icon, not a launcher plus its helper.
67
+ const cmd = path.join(opts.configDir, 'console.cmd');
68
+ return {
69
+ files: [
70
+ {
71
+ file: cmd,
72
+ contents: `@echo off\r\n` +
73
+ `title ${name}\r\n` +
74
+ `cd /d ${quoteWin(opts.projectDir)}\r\n` +
75
+ `echo ia-qa-heal console - ia-qa.com\r\n` +
76
+ `echo Starting... your browser will open on its own.\r\n` +
77
+ `echo Close this window to stop it.\r\n` +
78
+ `echo.\r\n` +
79
+ `${quoteWin(opts.nodePath)} ${quoteWin(opts.cliEntry)} ui\r\n` +
80
+ `if errorlevel 1 pause\r\n`,
81
+ },
82
+ ],
83
+ lnk: {
84
+ file: path.join(opts.desktop, `${name}.lnk`),
85
+ target: cmd,
86
+ workingDir: opts.projectDir,
87
+ iconPath: path.join(opts.configDir, 'console.ico'),
88
+ description,
89
+ },
90
+ icoPath: path.join(opts.configDir, 'console.ico'),
91
+ };
92
+ }
93
+ if (opts.platform === 'darwin') {
94
+ return {
95
+ files: [
96
+ {
97
+ file: path.join(opts.desktop, `${name}.command`),
98
+ contents: `#!/bin/sh\n` +
99
+ `# ${description}\n` +
100
+ `cd ${quotePosix(opts.projectDir)} || exit 1\n` +
101
+ `echo " ia-qa-heal console - ia-qa.com"\n` +
102
+ `echo " Starting... your browser will open on its own."\n` +
103
+ `echo " Close this window to stop it."\n` +
104
+ `exec ${quotePosix(opts.nodePath)} ${quotePosix(opts.cliEntry)} ui\n`,
105
+ mode: 0o755,
106
+ },
107
+ ],
108
+ defaultIconOnly: true,
109
+ };
110
+ }
111
+ return {
112
+ files: [
113
+ {
114
+ file: path.join(opts.desktop, `${name}.desktop`),
115
+ contents: `[Desktop Entry]\n` +
116
+ `Type=Application\n` +
117
+ `Name=${name}\n` +
118
+ `Comment=${description}\n` +
119
+ `Icon=${opts.logoPath}\n` +
120
+ `Exec=sh -c "cd ${quotePosix(opts.projectDir)} && ${quotePosix(opts.nodePath)} ${quotePosix(opts.cliEntry)} ui"\n` +
121
+ `Terminal=true\n` +
122
+ `Categories=Development;\n`,
123
+ mode: 0o755,
124
+ },
125
+ ],
126
+ };
127
+ }
128
+ /**
129
+ * Create the Windows `.lnk`. PowerShell's COM shell object is the only way to
130
+ * write one without either a native module or hand-assembling the binary Shell
131
+ * Link format. Returns false rather than throwing: the caller has a working
132
+ * fallback and a shortcut is not worth failing a command over.
133
+ *
134
+ * Every value is passed as an environment variable, never interpolated into the
135
+ * script text — a project path containing a quote would otherwise end up being
136
+ * executed as PowerShell.
137
+ */
138
+ function createWindowsLnk(lnk) {
139
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
140
+ const { execFileSync } = require('child_process');
141
+ const script = '$s = (New-Object -ComObject WScript.Shell).CreateShortcut($env:IAQA_LNK); ' +
142
+ '$s.TargetPath = $env:IAQA_TARGET; ' +
143
+ '$s.WorkingDirectory = $env:IAQA_CWD; ' +
144
+ '$s.Description = $env:IAQA_DESC; ' +
145
+ 'if (Test-Path $env:IAQA_ICON) { $s.IconLocation = $env:IAQA_ICON }; ' +
146
+ '$s.Save()';
147
+ try {
148
+ execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], {
149
+ stdio: 'ignore',
150
+ timeout: 15000,
151
+ env: {
152
+ ...process.env,
153
+ IAQA_LNK: lnk.file,
154
+ IAQA_TARGET: lnk.target,
155
+ IAQA_CWD: lnk.workingDir,
156
+ IAQA_DESC: lnk.description,
157
+ IAQA_ICON: lnk.iconPath,
158
+ },
159
+ });
160
+ return fs.existsSync(lnk.file);
161
+ }
162
+ catch {
163
+ return false;
164
+ }
165
+ }
166
+ /**
167
+ * Where the desktop is. `XDG_DESKTOP_DIR` and localized folder names exist and
168
+ * are not resolvable without reading user config, so a missing `Desktop` falls
169
+ * back to the home directory rather than guessing wrong and writing an icon
170
+ * nobody finds.
171
+ */
172
+ function desktopDir(home = os.homedir()) {
173
+ const candidate = path.join(home, 'Desktop');
174
+ return fs.existsSync(candidate) ? candidate : home;
175
+ }
176
+ //# sourceMappingURL=shortcut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortcut.js","sourceRoot":"","sources":["../../src/ui/shortcut.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * "You are running a version from six months ago" — the one fact the console's
3
+ * audience cannot obtain any other way.
4
+ *
5
+ * Every other door to this package resolves its own version already. An agent
6
+ * follows the documented MCP config, which is `npx -y -p @ia-qa/self-healing
7
+ * ia-qa-heal-mcp`: npx resolves the `latest` dist-tag on every server start, so
8
+ * the resolution *is* the check and nothing needs adding. A developer has a
9
+ * shell and `npm outdated`. The desktop shortcut has neither: it writes an
10
+ * absolute path to `node` plus `dist/cli/index.js`, so the console launches the
11
+ * bytes that are on disk, forever, and its whole premise is that the person
12
+ * clicking it does not own a terminal. That is the gap this fills, and the only
13
+ * one.
14
+ *
15
+ * **Why this is allowed to touch the network at all.** The package's claim is
16
+ * local-first, and that claim has to survive this file. Four constraints keep it
17
+ * true, and none of them are optional:
18
+ *
19
+ * 1. **It carries nothing.** A GET for a public package's dist-tags — 18 bytes
20
+ * back, no query, no body, no header about this machine. The user's app, test
21
+ * files, contracts, verdicts and history are not involved and never will be.
22
+ * npm learns an IP already knew when the package was installed from it.
23
+ * 2. **Once a day, from a cache** on this machine (`~/.ia-qa/update-check.json`,
24
+ * `IAQA_HOME` relocates it — same home as the project registry). Not once per
25
+ * request, and never from the page: the console's CSP is `connect-src 'self'`
26
+ * and stays that way, so the browser cannot reach a registry even if this
27
+ * file were wrong.
28
+ * 3. **Announced before it happens**, in the terminal, naming the host — the
29
+ * same manners `check` observes before it touches the user's own baseUrl.
30
+ * 4. **`--offline` removes it**, and a failure is silent: no network, a proxy, a
31
+ * 404, a hang past the timeout all degrade to "no banner". A version notice
32
+ * that could break someone's console would be worth strictly less than zero.
33
+ *
34
+ * And it stays a *notice*. Nothing here installs anything. The console spawns
35
+ * this package's own CLI with an allowlisted argv and that is the entirety of
36
+ * its authority (`ui/server.ts`); `npm install` is a different binary that runs
37
+ * arbitrary lifecycle scripts off the registry, so putting it behind a button
38
+ * would trade a bounded blast radius — "edits my test files" — for an unbounded
39
+ * one, on the strength of a token that is only meant to guard the former. The
40
+ * banner therefore ends at the command to paste.
41
+ */
42
+ export declare const PACKAGE_NAME = "@ia-qa/self-healing";
43
+ export declare const REGISTRY_HOST = "registry.npmjs.org";
44
+ export interface UpdateCache {
45
+ checkedAt: string;
46
+ latest: string;
47
+ }
48
+ export type InstallKind = 'global' | 'project' | 'npx' | 'checkout';
49
+ export interface InstallSite {
50
+ kind: InstallKind;
51
+ root?: string;
52
+ /** What to paste. `null` when there is nothing to do. */
53
+ command: string | null;
54
+ how: string;
55
+ }
56
+ export interface UpdateNotice extends InstallSite {
57
+ current: string;
58
+ latest: string;
59
+ checkedAt: string;
60
+ /** Named in the banner: a host the reader can recognise beats "online somewhere". */
61
+ registry: string;
62
+ }
63
+ /** The version actually running. `dist/update.js` and `src/update.ts` are both one level down. */
64
+ export declare function currentVersion(): string;
65
+ /**
66
+ * Is `latest` ahead of `current`? Deliberately not a semver dependency: three
67
+ * integers is the whole of what a dist-tag comparison needs, and anything it
68
+ * cannot parse returns false — an unreadable version must produce no banner
69
+ * rather than a wrong one. Pre-release suffixes are dropped, so a `1.7.0-rc.1`
70
+ * on this machine is not told to "update" to `1.6.9`.
71
+ */
72
+ export declare function isNewer(latest: string, current: string): boolean;
73
+ export declare function cachePath(home?: string): string;
74
+ export declare function readCache(home?: string): UpdateCache | null;
75
+ /** Would a launch actually make a request? What the announcement is gated on. */
76
+ export declare function updateCheckDue(home?: string, now?: number): boolean;
77
+ /** Resolves to `null` on every failure — no network, non-200, junk body, timeout. */
78
+ export declare function fetchLatest(timeoutMs?: number): Promise<string | null>;
79
+ /**
80
+ * Refresh the cached answer if it has aged out. Returns whatever is known —
81
+ * including a stale entry when the request failed, because losing yesterday's
82
+ * answer to a flaky network helps nobody. Never throws and never rejects.
83
+ */
84
+ export declare function refreshUpdateCache(opts?: {
85
+ home?: string;
86
+ fetch?: () => Promise<string | null>;
87
+ }): Promise<UpdateCache | null>;
88
+ /**
89
+ * How this copy got here, so the banner can name *the* command instead of
90
+ * listing three and making the reader choose — which is the actual friction for
91
+ * someone who does not own a terminal.
92
+ *
93
+ * The signal is the path, not a symlink probe: Node resolves module paths
94
+ * through symlinks by default, so an `npm link`ed package reports the checkout
95
+ * it points at rather than the `node_modules` entry. "Is there a `node_modules`
96
+ * ancestor" survives that, and the published tarball ships no `src/`, so a path
97
+ * outside `node_modules` is a working copy by construction.
98
+ */
99
+ export declare function describeInstall(pkgRoot?: string): InstallSite;
100
+ /**
101
+ * The banner's payload, or `null` when there is nothing to say. Reads the cache
102
+ * only — building the page must never wait on a socket.
103
+ */
104
+ export declare function updateNotice(home?: string, pkgRoot?: string): UpdateNotice | null;
package/dist/update.js ADDED
@@ -0,0 +1,297 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.REGISTRY_HOST = exports.PACKAGE_NAME = void 0;
37
+ exports.currentVersion = currentVersion;
38
+ exports.isNewer = isNewer;
39
+ exports.cachePath = cachePath;
40
+ exports.readCache = readCache;
41
+ exports.updateCheckDue = updateCheckDue;
42
+ exports.fetchLatest = fetchLatest;
43
+ exports.refreshUpdateCache = refreshUpdateCache;
44
+ exports.describeInstall = describeInstall;
45
+ exports.updateNotice = updateNotice;
46
+ const fs = __importStar(require("fs"));
47
+ const https = __importStar(require("https"));
48
+ const path = __importStar(require("path"));
49
+ const registry_1 = require("./ui/registry");
50
+ /**
51
+ * "You are running a version from six months ago" — the one fact the console's
52
+ * audience cannot obtain any other way.
53
+ *
54
+ * Every other door to this package resolves its own version already. An agent
55
+ * follows the documented MCP config, which is `npx -y -p @ia-qa/self-healing
56
+ * ia-qa-heal-mcp`: npx resolves the `latest` dist-tag on every server start, so
57
+ * the resolution *is* the check and nothing needs adding. A developer has a
58
+ * shell and `npm outdated`. The desktop shortcut has neither: it writes an
59
+ * absolute path to `node` plus `dist/cli/index.js`, so the console launches the
60
+ * bytes that are on disk, forever, and its whole premise is that the person
61
+ * clicking it does not own a terminal. That is the gap this fills, and the only
62
+ * one.
63
+ *
64
+ * **Why this is allowed to touch the network at all.** The package's claim is
65
+ * local-first, and that claim has to survive this file. Four constraints keep it
66
+ * true, and none of them are optional:
67
+ *
68
+ * 1. **It carries nothing.** A GET for a public package's dist-tags — 18 bytes
69
+ * back, no query, no body, no header about this machine. The user's app, test
70
+ * files, contracts, verdicts and history are not involved and never will be.
71
+ * npm learns an IP already knew when the package was installed from it.
72
+ * 2. **Once a day, from a cache** on this machine (`~/.ia-qa/update-check.json`,
73
+ * `IAQA_HOME` relocates it — same home as the project registry). Not once per
74
+ * request, and never from the page: the console's CSP is `connect-src 'self'`
75
+ * and stays that way, so the browser cannot reach a registry even if this
76
+ * file were wrong.
77
+ * 3. **Announced before it happens**, in the terminal, naming the host — the
78
+ * same manners `check` observes before it touches the user's own baseUrl.
79
+ * 4. **`--offline` removes it**, and a failure is silent: no network, a proxy, a
80
+ * 404, a hang past the timeout all degrade to "no banner". A version notice
81
+ * that could break someone's console would be worth strictly less than zero.
82
+ *
83
+ * And it stays a *notice*. Nothing here installs anything. The console spawns
84
+ * this package's own CLI with an allowlisted argv and that is the entirety of
85
+ * its authority (`ui/server.ts`); `npm install` is a different binary that runs
86
+ * arbitrary lifecycle scripts off the registry, so putting it behind a button
87
+ * would trade a bounded blast radius — "edits my test files" — for an unbounded
88
+ * one, on the strength of a token that is only meant to guard the former. The
89
+ * banner therefore ends at the command to paste.
90
+ */
91
+ exports.PACKAGE_NAME = '@ia-qa/self-healing';
92
+ exports.REGISTRY_HOST = 'registry.npmjs.org';
93
+ /** Just the dist-tags document, not the package metadata — 18 bytes, not 200 kB. */
94
+ const DIST_TAGS_PATH = '/-/package/@ia-qa%2fself-healing/dist-tags';
95
+ const TTL_MS = 24 * 60 * 60 * 1000;
96
+ const TIMEOUT_MS = 2500;
97
+ /** The version actually running. `dist/update.js` and `src/update.ts` are both one level down. */
98
+ function currentVersion() {
99
+ try {
100
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
101
+ return require('../package.json').version;
102
+ }
103
+ catch {
104
+ return '0.0.0';
105
+ }
106
+ }
107
+ /**
108
+ * Is `latest` ahead of `current`? Deliberately not a semver dependency: three
109
+ * integers is the whole of what a dist-tag comparison needs, and anything it
110
+ * cannot parse returns false — an unreadable version must produce no banner
111
+ * rather than a wrong one. Pre-release suffixes are dropped, so a `1.7.0-rc.1`
112
+ * on this machine is not told to "update" to `1.6.9`.
113
+ */
114
+ function isNewer(latest, current) {
115
+ const parse = (v) => String(v ?? '')
116
+ .trim()
117
+ .replace(/^v/, '')
118
+ .split(/[-+]/)[0]
119
+ .split('.')
120
+ .map((n) => Number.parseInt(n, 10));
121
+ const a = parse(latest);
122
+ const b = parse(current);
123
+ if (a.length === 0 || a.some((n) => !Number.isFinite(n)))
124
+ return false;
125
+ if (b.length === 0 || b.some((n) => !Number.isFinite(n)))
126
+ return false;
127
+ for (let i = 0; i < 3; i++) {
128
+ if ((a[i] ?? 0) !== (b[i] ?? 0))
129
+ return (a[i] ?? 0) > (b[i] ?? 0);
130
+ }
131
+ return false;
132
+ }
133
+ function cachePath(home) {
134
+ return path.join((0, registry_1.registryDir)(home), 'update-check.json');
135
+ }
136
+ function readCache(home) {
137
+ try {
138
+ const parsed = JSON.parse(fs.readFileSync(cachePath(home), 'utf8'));
139
+ if (typeof parsed?.latest !== 'string' || typeof parsed?.checkedAt !== 'string')
140
+ return null;
141
+ return parsed;
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ /** Would a launch actually make a request? What the announcement is gated on. */
148
+ function updateCheckDue(home, now = Date.now()) {
149
+ const cache = readCache(home);
150
+ if (!cache)
151
+ return true;
152
+ const at = Date.parse(cache.checkedAt);
153
+ return !Number.isFinite(at) || now - at >= TTL_MS || at > now;
154
+ }
155
+ /** Resolves to `null` on every failure — no network, non-200, junk body, timeout. */
156
+ function fetchLatest(timeoutMs = TIMEOUT_MS) {
157
+ return new Promise((resolve) => {
158
+ let done = false;
159
+ const finish = (v) => {
160
+ if (!done) {
161
+ done = true;
162
+ resolve(v);
163
+ }
164
+ };
165
+ try {
166
+ const req = https.get({
167
+ host: exports.REGISTRY_HOST,
168
+ path: DIST_TAGS_PATH,
169
+ headers: { accept: 'application/json', 'user-agent': `${exports.PACKAGE_NAME}/${currentVersion()}` },
170
+ timeout: timeoutMs,
171
+ }, (res) => {
172
+ if (res.statusCode !== 200) {
173
+ res.resume();
174
+ return finish(null);
175
+ }
176
+ let body = '';
177
+ res.setEncoding('utf8');
178
+ res.on('data', (c) => {
179
+ body += c;
180
+ if (body.length > 8192) {
181
+ req.destroy();
182
+ finish(null);
183
+ }
184
+ });
185
+ res.on('end', () => {
186
+ try {
187
+ const tag = JSON.parse(body).latest;
188
+ finish(typeof tag === 'string' && tag ? tag : null);
189
+ }
190
+ catch {
191
+ finish(null);
192
+ }
193
+ });
194
+ });
195
+ req.on('timeout', () => {
196
+ req.destroy();
197
+ finish(null);
198
+ });
199
+ req.on('error', () => finish(null));
200
+ }
201
+ catch {
202
+ finish(null);
203
+ }
204
+ });
205
+ }
206
+ /**
207
+ * Refresh the cached answer if it has aged out. Returns whatever is known —
208
+ * including a stale entry when the request failed, because losing yesterday's
209
+ * answer to a flaky network helps nobody. Never throws and never rejects.
210
+ */
211
+ async function refreshUpdateCache(opts = {}) {
212
+ const cached = readCache(opts.home);
213
+ if (!updateCheckDue(opts.home))
214
+ return cached;
215
+ const latest = await (opts.fetch ?? (() => fetchLatest()))().catch(() => null);
216
+ if (!latest)
217
+ return cached;
218
+ const next = { checkedAt: new Date().toISOString(), latest };
219
+ try {
220
+ fs.mkdirSync((0, registry_1.registryDir)(opts.home), { recursive: true });
221
+ fs.writeFileSync(cachePath(opts.home), JSON.stringify(next, null, 2) + '\n', {
222
+ encoding: 'utf8',
223
+ mode: 0o600,
224
+ });
225
+ }
226
+ catch {
227
+ /* an uncacheable answer just means we ask again next launch */
228
+ }
229
+ return next;
230
+ }
231
+ /**
232
+ * How this copy got here, so the banner can name *the* command instead of
233
+ * listing three and making the reader choose — which is the actual friction for
234
+ * someone who does not own a terminal.
235
+ *
236
+ * The signal is the path, not a symlink probe: Node resolves module paths
237
+ * through symlinks by default, so an `npm link`ed package reports the checkout
238
+ * it points at rather than the `node_modules` entry. "Is there a `node_modules`
239
+ * ancestor" survives that, and the published tarball ships no `src/`, so a path
240
+ * outside `node_modules` is a working copy by construction.
241
+ */
242
+ function describeInstall(pkgRoot = path.resolve(__dirname, '..')) {
243
+ const parts = pkgRoot.split(/[\\/]/);
244
+ const i = parts.lastIndexOf('node_modules');
245
+ if (i === -1) {
246
+ return {
247
+ kind: 'checkout',
248
+ root: pkgRoot,
249
+ command: null,
250
+ how: 'This console is running from a source checkout, not from an installed package. Update that checkout the way you normally would.',
251
+ };
252
+ }
253
+ const root = parts.slice(0, i).join(path.sep) || path.sep;
254
+ // npx keeps its own tree under the npm cache and re-resolves `latest` on every
255
+ // invocation, so this copy is already as current as its last start. Telling
256
+ // someone to install would be advice that makes their setup worse.
257
+ if (/[\\/]_npx[\\/]/.test(pkgRoot + path.sep)) {
258
+ return {
259
+ kind: 'npx',
260
+ root,
261
+ command: null,
262
+ how: 'Started through npx, which fetches the newest version every time it runs. Close it and start it again to pick this one up — there is nothing to install.',
263
+ };
264
+ }
265
+ if (fs.existsSync(path.join(root, 'package.json'))) {
266
+ return {
267
+ kind: 'project',
268
+ root,
269
+ command: `npm i -D ${exports.PACKAGE_NAME}@latest`,
270
+ how: `Installed inside this project. Run this from ${root}:`,
271
+ };
272
+ }
273
+ return {
274
+ kind: 'global',
275
+ root,
276
+ command: `npm i -g ${exports.PACKAGE_NAME}@latest`,
277
+ how: 'Installed once for this whole machine. Run this in any terminal:',
278
+ };
279
+ }
280
+ /**
281
+ * The banner's payload, or `null` when there is nothing to say. Reads the cache
282
+ * only — building the page must never wait on a socket.
283
+ */
284
+ function updateNotice(home, pkgRoot) {
285
+ const cache = readCache(home);
286
+ const current = currentVersion();
287
+ if (!cache || !isNewer(cache.latest, current))
288
+ return null;
289
+ return {
290
+ ...describeInstall(pkgRoot),
291
+ current,
292
+ latest: cache.latest,
293
+ checkedAt: cache.checkedAt,
294
+ registry: exports.REGISTRY_HOST,
295
+ };
296
+ }
297
+ //# sourceMappingURL=update.js.map