@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.
- package/README.md +111 -0
- package/TUTORIAL.md +128 -2
- package/assets/logoKawaiiGreenTiny.png +0 -0
- package/dist/aom.d.ts +8 -0
- package/dist/aom.js.map +1 -1
- package/dist/cli/args.js +4 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/audit.js +2 -8
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/diff.d.ts +25 -1
- package/dist/cli/diff.js +176 -20
- package/dist/cli/diff.js.map +1 -1
- package/dist/cli/emit.d.ts +71 -0
- package/dist/cli/emit.js +150 -0
- package/dist/cli/emit.js.map +1 -0
- package/dist/cli/fix.js +41 -3
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/history.d.ts +12 -0
- package/dist/cli/history.js +95 -0
- package/dist/cli/history.js.map +1 -0
- package/dist/cli/index.js +68 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/ingest.js +5 -8
- package/dist/cli/ingest.js.map +1 -1
- package/dist/cli/init.js +32 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/map.d.ts +14 -1
- package/dist/cli/map.js +144 -33
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/run.js +91 -8
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/ui.d.ts +16 -0
- package/dist/cli/ui.js +294 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli-ai/index.js +3 -3
- package/dist/cli-ai/index.js.map +1 -1
- package/dist/config.d.ts +50 -0
- package/dist/config.js +63 -1
- package/dist/config.js.map +1 -1
- package/dist/evidence.d.ts +130 -0
- package/dist/evidence.js +232 -0
- package/dist/evidence.js.map +1 -0
- package/dist/fixEngine.d.ts +2 -0
- package/dist/fixEngine.js +7 -0
- package/dist/fixEngine.js.map +1 -1
- package/dist/history.d.ts +145 -0
- package/dist/history.js +230 -0
- package/dist/history.js.map +1 -0
- package/dist/htmlReport.d.ts +47 -0
- package/dist/htmlReport.js +103 -1
- package/dist/htmlReport.js.map +1 -1
- package/dist/ingest.d.ts +18 -0
- package/dist/ingest.js +0 -0
- package/dist/ingest.js.map +1 -1
- package/dist/junit.d.ts +53 -0
- package/dist/junit.js +150 -0
- package/dist/junit.js.map +1 -0
- package/dist/mcp/server.js +46 -5
- package/dist/mcp/server.js.map +1 -1
- package/dist/nameMask.d.ts +110 -0
- package/dist/nameMask.js +0 -0
- package/dist/nameMask.js.map +1 -0
- package/dist/ui/icon.d.ts +41 -0
- package/dist/ui/icon.js +115 -0
- package/dist/ui/icon.js.map +1 -0
- package/dist/ui/lock.d.ts +40 -0
- package/dist/ui/lock.js +138 -0
- package/dist/ui/lock.js.map +1 -0
- package/dist/ui/page.d.ts +1 -0
- package/dist/ui/page.js +657 -0
- package/dist/ui/page.js.map +1 -0
- package/dist/ui/registry.d.ts +62 -0
- package/dist/ui/registry.js +139 -0
- package/dist/ui/registry.js.map +1 -0
- package/dist/ui/server.d.ts +76 -0
- package/dist/ui/server.js +607 -0
- package/dist/ui/server.js.map +1 -0
- package/dist/ui/shortcut.d.ts +90 -0
- package/dist/ui/shortcut.js +176 -0
- package/dist/ui/shortcut.js.map +1 -0
- package/dist/update.d.ts +104 -0
- package/dist/update.js +297 -0
- package/dist/update.js.map +1 -0
- package/dist/volatile.d.ts +2 -0
- package/dist/volatile.js +2 -0
- package/dist/volatile.js.map +1 -1
- package/package.json +3 -2
- 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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"}
|
package/dist/update.d.ts
ADDED
|
@@ -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
|