@addai/node 0.27.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/vault-extension/assets/inter.woff2 +0 -0
- package/assets/vault-extension/background.js +45 -0
- package/assets/vault-extension/content.js +62 -0
- package/assets/vault-extension/icons/icon-128.png +0 -0
- package/assets/vault-extension/icons/icon-16.png +0 -0
- package/assets/vault-extension/icons/icon-32.png +0 -0
- package/assets/vault-extension/icons/icon-48.png +0 -0
- package/assets/vault-extension/manifest.json +39 -0
- package/dist/capabilities.js +2 -1
- package/dist/command-runner.js +109 -2
- package/dist/desktop/docker.d.ts +4 -1
- package/dist/desktop/docker.js +7 -3
- package/dist/desktop/engine.d.ts +13 -0
- package/dist/desktop/engine.js +35 -4
- package/dist/desktop/install-engine.d.ts +33 -2
- package/dist/desktop/install-engine.js +138 -12
- package/dist/desktop/learn/browser.d.ts +41 -0
- package/dist/desktop/learn/browser.js +136 -0
- package/dist/desktop/learn/injected.mjs +173 -0
- package/dist/desktop/learn/recorder.mjs +151 -0
- package/dist/desktop/learn/session.d.ts +51 -0
- package/dist/desktop/learn/session.js +224 -0
- package/dist/desktop/provider.d.ts +4 -0
- package/dist/desktop/start-engine.d.ts +7 -4
- package/dist/desktop/start-engine.js +50 -6
- package/dist/desktop/vault-extension.d.ts +21 -0
- package/dist/desktop/vault-extension.js +112 -0
- package/dist/desktop/vault-seed.mjs +112 -0
- package/dist/desktop/vault-session.d.ts +41 -0
- package/dist/desktop/vault-session.js +164 -0
- package/package.json +4 -3
- package/scripts/copy-assets.js +18 -0
- package/dist/tui.d.ts +0 -1
- package/dist/tui.js +0 -314
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// The CDP recorder. Runs INSIDE the desktop container, next to the browser.
|
|
2
|
+
//
|
|
3
|
+
// node /conf/learn/recorder.mjs
|
|
4
|
+
//
|
|
5
|
+
// Writes NDJSON on stdout, one event per line, `t` in ms since it attached.
|
|
6
|
+
// The daemon reads that stream directly — no file, no polling — and batches it
|
|
7
|
+
// into the lesson row.
|
|
8
|
+
//
|
|
9
|
+
// Nothing is installed to make this work: the image already has node 22, whose
|
|
10
|
+
// global WebSocket is enough for CDP, and Chrome, which the browser wrapper has
|
|
11
|
+
// already given a debug port. If the port is not there (a browser opened before
|
|
12
|
+
// the wrapper existed) it says so in one line and exits 3, and the lesson goes
|
|
13
|
+
// on without the browser track rather than failing.
|
|
14
|
+
import { injectedSource } from './injected.mjs';
|
|
15
|
+
|
|
16
|
+
const PORT = Number(process.env.ADDAI_LEARN_PORT || 9222);
|
|
17
|
+
const t0 = Date.now();
|
|
18
|
+
|
|
19
|
+
const emit = (o) => {
|
|
20
|
+
try { process.stdout.write(JSON.stringify({ t: Date.now() - t0, ...o }) + '\n'); }
|
|
21
|
+
catch { /* the daemon went away; nothing to do about it from here */ }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function version() {
|
|
25
|
+
// A browser that is starting up refuses for a moment. Three tries over ~3s
|
|
26
|
+
// covers the case where a lesson begins in the same breath as the browser.
|
|
27
|
+
for (let i = 0; i < 3; i++) {
|
|
28
|
+
try {
|
|
29
|
+
const r = await fetch(`http://127.0.0.1:${PORT}/json/version`);
|
|
30
|
+
if (r.ok) return await r.json();
|
|
31
|
+
} catch { /* not up yet */ }
|
|
32
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function client(url) {
|
|
38
|
+
const ws = new WebSocket(url);
|
|
39
|
+
let next = 1;
|
|
40
|
+
const pending = new Map();
|
|
41
|
+
const handlers = [];
|
|
42
|
+
|
|
43
|
+
const send = (method, params = {}, sessionId) =>
|
|
44
|
+
new Promise((resolve) => {
|
|
45
|
+
const id = next++;
|
|
46
|
+
pending.set(id, resolve);
|
|
47
|
+
const msg = { id, method, params };
|
|
48
|
+
if (sessionId) msg.sessionId = sessionId;
|
|
49
|
+
try { ws.send(JSON.stringify(msg)); } catch { pending.delete(id); resolve(null); }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
ws.addEventListener('message', (ev) => {
|
|
53
|
+
let m;
|
|
54
|
+
try { m = JSON.parse(ev.data); } catch { return; }
|
|
55
|
+
if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result ?? null); pending.delete(m.id); return; }
|
|
56
|
+
for (const h of handlers) h(m);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
ws,
|
|
61
|
+
send,
|
|
62
|
+
on: (fn) => handlers.push(fn),
|
|
63
|
+
ready: new Promise((resolve, reject) => {
|
|
64
|
+
ws.addEventListener('open', resolve);
|
|
65
|
+
ws.addEventListener('error', () => reject(new Error('cdp socket refused')));
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const v = await version();
|
|
72
|
+
if (!v || !v.webSocketDebuggerUrl) {
|
|
73
|
+
emit({ kind: 'no_port' });
|
|
74
|
+
process.exit(3);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cdp = client(v.webSocketDebuggerUrl);
|
|
78
|
+
await cdp.ready;
|
|
79
|
+
|
|
80
|
+
const source = injectedSource();
|
|
81
|
+
const attached = new Set();
|
|
82
|
+
|
|
83
|
+
const arm = async (sessionId) => {
|
|
84
|
+
if (attached.has(sessionId)) return;
|
|
85
|
+
attached.add(sessionId);
|
|
86
|
+
await cdp.send('Runtime.enable', {}, sessionId);
|
|
87
|
+
await cdp.send('Page.enable', {}, sessionId);
|
|
88
|
+
// The binding is how the page talks back. Added before the script that
|
|
89
|
+
// calls it, or the first click on an already-open page is lost.
|
|
90
|
+
await cdp.send('Runtime.addBinding', { name: '__addaiLearn' }, sessionId);
|
|
91
|
+
await cdp.send('Page.addScriptToEvaluateOnNewDocument', { source }, sessionId);
|
|
92
|
+
// …and once for the document that is already loaded, which the line above
|
|
93
|
+
// does not cover.
|
|
94
|
+
await cdp.send('Runtime.evaluate', { expression: source }, sessionId);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
cdp.on(async (m) => {
|
|
98
|
+
const s = m.sessionId;
|
|
99
|
+
if (m.method === 'Target.targetCreated' && m.params?.targetInfo?.type === 'page') {
|
|
100
|
+
await cdp.send('Target.attachToTarget', { targetId: m.params.targetInfo.targetId, flatten: true });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (m.method === 'Target.attachedToTarget') {
|
|
104
|
+
const info = m.params?.targetInfo ?? {};
|
|
105
|
+
if (info.type !== 'page') return;
|
|
106
|
+
emit({ kind: 'tab_open', url: info.url ?? null });
|
|
107
|
+
await arm(m.params.sessionId);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (m.method === 'Target.detachedFromTarget') {
|
|
111
|
+
attached.delete(m.params?.sessionId);
|
|
112
|
+
emit({ kind: 'tab_close' });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (m.method === 'Runtime.bindingCalled' && m.params?.name === '__addaiLearn') {
|
|
116
|
+
let ev;
|
|
117
|
+
try { ev = JSON.parse(m.params.payload); } catch { return; }
|
|
118
|
+
emit(ev);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (m.method === 'Page.frameNavigated' && !m.params?.frame?.parentId) {
|
|
122
|
+
emit({ kind: 'url', url: m.params.frame.url ?? null, title: null });
|
|
123
|
+
// A navigation replaces the execution context; re-arming is cheap and
|
|
124
|
+
// idempotent, and skipping it loses every click on the new page.
|
|
125
|
+
if (s) { attached.delete(s); await arm(s); }
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (m.method === 'Page.loadEventFired' && s) {
|
|
129
|
+
const r = await cdp.send('Runtime.evaluate',
|
|
130
|
+
{ expression: 'document.title', returnByValue: true }, s);
|
|
131
|
+
emit({ kind: 'load', title: r?.result?.value ?? null });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (m.method === 'Page.javascriptDialogOpening') {
|
|
135
|
+
emit({ kind: 'dialog', dtype: m.params?.type ?? null, message: m.params?.message ?? null });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
140
|
+
emit({ kind: 'watching' });
|
|
141
|
+
|
|
142
|
+
const bye = () => { try { cdp.ws.close(); } catch { /* already gone */ } process.exit(0); };
|
|
143
|
+
process.on('SIGTERM', bye);
|
|
144
|
+
process.on('SIGINT', bye);
|
|
145
|
+
cdp.ws.addEventListener('close', () => process.exit(0));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
main().catch((e) => {
|
|
149
|
+
emit({ kind: 'recorder_error', message: String(e && e.message ? e.message : e) });
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DesktopProvider } from '../provider';
|
|
2
|
+
import { DesktopRow } from '../spec';
|
|
3
|
+
export interface PageEvent {
|
|
4
|
+
t: number;
|
|
5
|
+
kind: string;
|
|
6
|
+
[k: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Split whole NDJSON lines out of a stream chunk.
|
|
10
|
+
*
|
|
11
|
+
* A chunk boundary lands in the middle of a line often enough to matter, so
|
|
12
|
+
* the remainder comes back for the next call rather than being parsed as junk
|
|
13
|
+
* and dropped. Junk that is genuinely junk IS dropped: a browser writing to
|
|
14
|
+
* stderr must not be able to kill a lesson.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseNdjson(buf: string): {
|
|
17
|
+
events: PageEvent[];
|
|
18
|
+
rest: string;
|
|
19
|
+
};
|
|
20
|
+
export type SendEvents = (events: PageEvent[]) => Promise<{
|
|
21
|
+
status?: string;
|
|
22
|
+
full?: boolean;
|
|
23
|
+
} | null>;
|
|
24
|
+
export interface Batcher {
|
|
25
|
+
push(ev: PageEvent): void;
|
|
26
|
+
flush(): Promise<void>;
|
|
27
|
+
stop(): void;
|
|
28
|
+
/** Set once the server says the lesson is over or the row is full. */
|
|
29
|
+
done: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Events go up in batches, not one at a time: a page that fires an input event
|
|
33
|
+
* per keystroke would otherwise be one HTTP request per keystroke.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createBatcher(send: SendEvents, opts?: {
|
|
36
|
+
everyMs?: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
}): Batcher;
|
|
39
|
+
export declare const lessonIsRunning: (recordingId: string) => boolean;
|
|
40
|
+
export interface StartResult {
|
|
41
|
+
browserTrack: boolean;
|
|
42
|
+
note?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function startLesson(opts: {
|
|
45
|
+
provider: DesktopProvider;
|
|
46
|
+
row: DesktopRow;
|
|
47
|
+
recordingId: string;
|
|
48
|
+
send: SendEvents;
|
|
49
|
+
onLog?: (s: string) => void;
|
|
50
|
+
}): Promise<StartResult>;
|
|
51
|
+
export declare function stopLesson(recordingId: string): Promise<void>;
|
|
@@ -0,0 +1,224 @@
|
|
|
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.lessonIsRunning = void 0;
|
|
37
|
+
exports.parseNdjson = parseNdjson;
|
|
38
|
+
exports.createBatcher = createBatcher;
|
|
39
|
+
exports.startLesson = startLesson;
|
|
40
|
+
exports.stopLesson = stopLesson;
|
|
41
|
+
// Running a lesson's container half.
|
|
42
|
+
//
|
|
43
|
+
// The daemon spawns recorder.mjs inside the desktop and reads its NDJSON on
|
|
44
|
+
// stdout. Two commands bracket it — desktop_record_start and
|
|
45
|
+
// desktop_record_stop — and between them this module owns one child process
|
|
46
|
+
// and one batch of events on its way to the lesson row.
|
|
47
|
+
//
|
|
48
|
+
// Everything the container needs is copied into /conf, which the daemon owns on
|
|
49
|
+
// the host and the container sees as a bind mount. That is why no image change
|
|
50
|
+
// is needed and why there is no `docker cp` anywhere in this feature.
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const provider_1 = require("../provider");
|
|
55
|
+
const win_1 = require("../../win");
|
|
56
|
+
const creds_1 = require("../creds");
|
|
57
|
+
const manager_1 = require("../manager");
|
|
58
|
+
const browser_1 = require("./browser");
|
|
59
|
+
/**
|
|
60
|
+
* Split whole NDJSON lines out of a stream chunk.
|
|
61
|
+
*
|
|
62
|
+
* A chunk boundary lands in the middle of a line often enough to matter, so
|
|
63
|
+
* the remainder comes back for the next call rather than being parsed as junk
|
|
64
|
+
* and dropped. Junk that is genuinely junk IS dropped: a browser writing to
|
|
65
|
+
* stderr must not be able to kill a lesson.
|
|
66
|
+
*/
|
|
67
|
+
function parseNdjson(buf) {
|
|
68
|
+
const events = [];
|
|
69
|
+
const parts = buf.split('\n');
|
|
70
|
+
const rest = parts.pop() ?? '';
|
|
71
|
+
for (const line of parts) {
|
|
72
|
+
const s = line.trim();
|
|
73
|
+
if (!s)
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const o = JSON.parse(s);
|
|
77
|
+
if (o && typeof o === 'object' && typeof o.kind === 'string')
|
|
78
|
+
events.push(o);
|
|
79
|
+
}
|
|
80
|
+
catch { /* not ours */ }
|
|
81
|
+
}
|
|
82
|
+
return { events, rest };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Events go up in batches, not one at a time: a page that fires an input event
|
|
86
|
+
* per keystroke would otherwise be one HTTP request per keystroke.
|
|
87
|
+
*/
|
|
88
|
+
function createBatcher(send, opts = {}) {
|
|
89
|
+
const everyMs = opts.everyMs ?? 2_000;
|
|
90
|
+
const max = opts.max ?? 50;
|
|
91
|
+
let queue = [];
|
|
92
|
+
let timer = null;
|
|
93
|
+
const b = {
|
|
94
|
+
done: false,
|
|
95
|
+
push(ev) {
|
|
96
|
+
if (b.done)
|
|
97
|
+
return;
|
|
98
|
+
queue.push(ev);
|
|
99
|
+
if (queue.length >= max)
|
|
100
|
+
void b.flush();
|
|
101
|
+
else if (!timer)
|
|
102
|
+
timer = setTimeout(() => { void b.flush(); }, everyMs);
|
|
103
|
+
},
|
|
104
|
+
async flush() {
|
|
105
|
+
if (timer) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
timer = null;
|
|
108
|
+
}
|
|
109
|
+
if (queue.length === 0)
|
|
110
|
+
return;
|
|
111
|
+
const batch = queue;
|
|
112
|
+
queue = [];
|
|
113
|
+
try {
|
|
114
|
+
const res = await send(batch);
|
|
115
|
+
// The row said the lesson is over, or it has taken all it will take.
|
|
116
|
+
if (res && (res.full || (res.status && res.status !== 'recording' && res.status !== 'compiling'))) {
|
|
117
|
+
b.done = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Best effort. Losing a batch costs detail in one lesson; throwing here
|
|
122
|
+
// would take down the command runner that owns the desktop.
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
stop() {
|
|
126
|
+
if (timer) {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
timer = null;
|
|
129
|
+
}
|
|
130
|
+
b.done = true;
|
|
131
|
+
queue = [];
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
return b;
|
|
135
|
+
}
|
|
136
|
+
const live = new Map();
|
|
137
|
+
const lessonIsRunning = (recordingId) => live.has(recordingId);
|
|
138
|
+
exports.lessonIsRunning = lessonIsRunning;
|
|
139
|
+
/** Assets the container runs. Copied rather than mounted from anywhere clever:
|
|
140
|
+
* /conf is already there, and a copy means the version that started a lesson
|
|
141
|
+
* is the version that finishes it even if the daemon rolls mid-recording. */
|
|
142
|
+
function stageAssets(confDir) {
|
|
143
|
+
const dir = path.join(confDir, 'learn');
|
|
144
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
145
|
+
for (const f of ['recorder.mjs', 'injected.mjs']) {
|
|
146
|
+
fs.copyFileSync(path.join(__dirname, f), path.join(dir, f));
|
|
147
|
+
}
|
|
148
|
+
return dir;
|
|
149
|
+
}
|
|
150
|
+
async function startLesson(opts) {
|
|
151
|
+
const { provider, row, recordingId, send } = opts;
|
|
152
|
+
const log = opts.onLog ?? (() => { });
|
|
153
|
+
if (live.has(recordingId))
|
|
154
|
+
return { browserTrack: live.get(recordingId).browserTrack };
|
|
155
|
+
const { confDir } = (0, manager_1.ensureDirs)(row.id);
|
|
156
|
+
stageAssets(confDir);
|
|
157
|
+
const run = async (cmd) => {
|
|
158
|
+
const r = await (0, creds_1.execInDesktopCapture)(provider, row, cmd, 20_000);
|
|
159
|
+
return { ok: r.exitCode === 0, out: r.stdout.toString('utf8') };
|
|
160
|
+
};
|
|
161
|
+
const wrapper = await (0, browser_1.ensureBrowserWrapper)(run, confDir);
|
|
162
|
+
if (wrapper.note)
|
|
163
|
+
log(`${wrapper.note}\n`);
|
|
164
|
+
const args = (0, provider_1.buildExecArgs)(row, ['node', '/conf/learn/recorder.mjs'], { cwd: '/work', env: {}, tty: false });
|
|
165
|
+
const inv = (0, win_1.resolveCliInvocation)(provider.id, args);
|
|
166
|
+
const child = (0, child_process_1.spawn)(inv.file, inv.args, { env: process.env, windowsHide: true });
|
|
167
|
+
const batcher = createBatcher(send);
|
|
168
|
+
const entry = { child, batcher, browserTrack: false };
|
|
169
|
+
live.set(recordingId, entry);
|
|
170
|
+
let settle = null;
|
|
171
|
+
const settled = new Promise((resolve) => { settle = resolve; });
|
|
172
|
+
const answer = (r) => { if (settle) {
|
|
173
|
+
settle(r);
|
|
174
|
+
settle = null;
|
|
175
|
+
} };
|
|
176
|
+
let rest = '';
|
|
177
|
+
child.stdout?.on('data', (b) => {
|
|
178
|
+
const parsed = parseNdjson(rest + b.toString('utf8'));
|
|
179
|
+
rest = parsed.rest;
|
|
180
|
+
for (const ev of parsed.events) {
|
|
181
|
+
if (ev.kind === 'no_port') {
|
|
182
|
+
answer({
|
|
183
|
+
browserTrack: false,
|
|
184
|
+
note: 'The browser was open before recording started, so it had no debugging '
|
|
185
|
+
+ 'port and the page detail could not be captured.',
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ev.kind === 'watching') {
|
|
190
|
+
entry.browserTrack = true;
|
|
191
|
+
answer({ browserTrack: true });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
batcher.push(ev);
|
|
195
|
+
if (batcher.done) {
|
|
196
|
+
stopLesson(recordingId);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
child.stderr?.on('data', (b) => log(b.toString('utf8').slice(0, 500)));
|
|
202
|
+
child.on('error', (err) => answer({ browserTrack: false, note: `recorder failed to start: ${err.message}` }));
|
|
203
|
+
child.on('exit', () => {
|
|
204
|
+
answer({ browserTrack: false, note: 'the browser recorder stopped early' });
|
|
205
|
+
void batcher.flush();
|
|
206
|
+
live.delete(recordingId);
|
|
207
|
+
});
|
|
208
|
+
// Do not wait forever for a first line: a lesson without the browser track is
|
|
209
|
+
// still a lesson, and the person is already recording.
|
|
210
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve({ browserTrack: entry.browserTrack }), 8_000).unref?.());
|
|
211
|
+
return Promise.race([settled, timeout]);
|
|
212
|
+
}
|
|
213
|
+
async function stopLesson(recordingId) {
|
|
214
|
+
const entry = live.get(recordingId);
|
|
215
|
+
if (!entry)
|
|
216
|
+
return;
|
|
217
|
+
live.delete(recordingId);
|
|
218
|
+
await entry.batcher.flush();
|
|
219
|
+
entry.batcher.stop();
|
|
220
|
+
try {
|
|
221
|
+
entry.child.kill('SIGTERM');
|
|
222
|
+
}
|
|
223
|
+
catch { /* already gone */ }
|
|
224
|
+
}
|
|
@@ -7,6 +7,10 @@ export interface EngineInfo {
|
|
|
7
7
|
running: boolean;
|
|
8
8
|
/** Set when running is false: what to do about it, in words. */
|
|
9
9
|
reason?: string;
|
|
10
|
+
/** Where the CLI actually is. Carried through so container calls spawn the
|
|
11
|
+
* same binary the probe found, not a bare name off a PATH that may not
|
|
12
|
+
* have it — see engineBinary(). */
|
|
13
|
+
binary?: string;
|
|
10
14
|
}
|
|
11
15
|
export interface ExecOpts {
|
|
12
16
|
cwd: string;
|
|
@@ -9,14 +9,17 @@ export interface StartPlan {
|
|
|
9
9
|
/** Launch and walk away — Docker Desktop is a GUI app that outlives us. */
|
|
10
10
|
detached: boolean;
|
|
11
11
|
}
|
|
12
|
+
export declare const MAC_APP = "/Applications/Docker.app";
|
|
12
13
|
/**
|
|
13
14
|
* Pure, so the choice is testable on any machine.
|
|
14
15
|
*
|
|
15
|
-
* @param programFiles
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* @param programFiles %ProgramFiles% on Windows. Passed in rather than read
|
|
17
|
+
* from the environment so the Windows path is testable
|
|
18
|
+
* from a Mac, which is where it is usually written.
|
|
19
|
+
* @param macAppInstalled same reason: a Mac without Docker must still be able
|
|
20
|
+
* to assert what a Mac with Docker does.
|
|
18
21
|
*/
|
|
19
|
-
export declare function startPlan(platform: NodeJS.Platform, isRoot: boolean, programFiles?: string): StartPlan;
|
|
22
|
+
export declare function startPlan(platform: NodeJS.Platform, isRoot: boolean, programFiles?: string, macAppInstalled?: boolean): StartPlan;
|
|
20
23
|
/** Build the argv for a privileged start. Password on stdin, never argv —
|
|
21
24
|
* argv is world-readable through /proc. */
|
|
22
25
|
export declare function sudoStartArgv(): {
|
|
@@ -1,6 +1,39 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.START_TIMEOUT_MS = void 0;
|
|
36
|
+
exports.START_TIMEOUT_MS = exports.MAC_APP = void 0;
|
|
4
37
|
exports.startPlan = startPlan;
|
|
5
38
|
exports.sudoStartArgv = sudoStartArgv;
|
|
6
39
|
exports.runningAsRoot = runningAsRoot;
|
|
@@ -19,21 +52,32 @@ exports.startEngine = startEngine;
|
|
|
19
52
|
// inheriting the install button's sudo round trip on machines that never need
|
|
20
53
|
// it.
|
|
21
54
|
const child_process_1 = require("child_process");
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
22
56
|
const win_1 = require("../win");
|
|
23
57
|
const engine_1 = require("./engine");
|
|
58
|
+
exports.MAC_APP = '/Applications/Docker.app';
|
|
24
59
|
/**
|
|
25
60
|
* Pure, so the choice is testable on any machine.
|
|
26
61
|
*
|
|
27
|
-
* @param programFiles
|
|
28
|
-
*
|
|
29
|
-
*
|
|
62
|
+
* @param programFiles %ProgramFiles% on Windows. Passed in rather than read
|
|
63
|
+
* from the environment so the Windows path is testable
|
|
64
|
+
* from a Mac, which is where it is usually written.
|
|
65
|
+
* @param macAppInstalled same reason: a Mac without Docker must still be able
|
|
66
|
+
* to assert what a Mac with Docker does.
|
|
30
67
|
*/
|
|
31
|
-
function startPlan(platform, isRoot, programFiles) {
|
|
68
|
+
function startPlan(platform, isRoot, programFiles, macAppInstalled = platform === 'darwin' && fs.existsSync(exports.MAC_APP)) {
|
|
32
69
|
if (platform === 'darwin') {
|
|
33
70
|
// `open -a` returns as soon as the app is launching; the daemon comes up
|
|
34
71
|
// a while later, which is why the caller polls rather than trusting exit 0.
|
|
72
|
+
//
|
|
73
|
+
// By path when the app is where it should be: `-a Docker` is a
|
|
74
|
+
// LaunchServices name lookup, and that database has not always caught up
|
|
75
|
+
// minutes after a fresh install — which is exactly when this button gets
|
|
76
|
+
// pressed. The name stays as the fallback for a Docker installed
|
|
77
|
+
// somewhere else.
|
|
78
|
+
const target = macAppInstalled ? exports.MAC_APP : 'Docker';
|
|
35
79
|
return {
|
|
36
|
-
file: 'open', args: ['-a',
|
|
80
|
+
file: 'open', args: ['-a', target],
|
|
37
81
|
manual: 'open -a Docker', needsRoot: false, detached: false,
|
|
38
82
|
};
|
|
39
83
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Where the extension lives inside a desktop. /conf is the host's confDir. */
|
|
2
|
+
export declare const EXTENSION_DIR = "/conf/vault/extension";
|
|
3
|
+
/** Every file the browser needs. Sourcemaps are deliberately absent: they are
|
|
4
|
+
* four times the size of the code and nobody debugs this from inside a
|
|
5
|
+
* container. */
|
|
6
|
+
export declare const EXTENSION_FILES: string[];
|
|
7
|
+
/** The vendored copy that ships inside this package. */
|
|
8
|
+
export declare function packagedExtensionDir(): string;
|
|
9
|
+
export interface VaultExtensionResult {
|
|
10
|
+
/** Whether anything was written this time. */
|
|
11
|
+
changed: boolean;
|
|
12
|
+
/** False when this build has no vendored extension to install. */
|
|
13
|
+
available: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Put the extension where the desktop's browser can load it.
|
|
17
|
+
*
|
|
18
|
+
* Copies only what differs, by size and mtime, so calling this on every
|
|
19
|
+
* desktop start is cheap after the first one.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureVaultExtension(confDir: string): VaultExtensionResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
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.EXTENSION_FILES = exports.EXTENSION_DIR = void 0;
|
|
37
|
+
exports.packagedExtensionDir = packagedExtensionDir;
|
|
38
|
+
exports.ensureVaultExtension = ensureVaultExtension;
|
|
39
|
+
// The +Ai Vault, inside a desktop's browser.
|
|
40
|
+
//
|
|
41
|
+
// The extension already exists — it is the same MV3 build people run on their
|
|
42
|
+
// own machines (addai-credentials/extension). It signs in by reading the
|
|
43
|
+
// `auth` cookie on add.ai and asks the vault edge function what that session
|
|
44
|
+
// may see, so it needs no desktop-specific server work at all: whoever the
|
|
45
|
+
// desktop browser is signed in as, that is the vault it shows. An entity that
|
|
46
|
+
// has logged into add.ai as itself sees exactly what it has been granted, and
|
|
47
|
+
// a person watching over its shoulder sees what they have been granted.
|
|
48
|
+
//
|
|
49
|
+
// It is served straight out of /conf. That directory is already bind-mounted
|
|
50
|
+
// from the host, so the browser can load the extension from disk without the
|
|
51
|
+
// files ever being copied into the container, without a new image, and without
|
|
52
|
+
// the GHCR publish that everything else about desktops is waiting on. Updating
|
|
53
|
+
// the extension is rewriting host files.
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
/** Where the extension lives inside a desktop. /conf is the host's confDir. */
|
|
57
|
+
exports.EXTENSION_DIR = '/conf/vault/extension';
|
|
58
|
+
/** Every file the browser needs. Sourcemaps are deliberately absent: they are
|
|
59
|
+
* four times the size of the code and nobody debugs this from inside a
|
|
60
|
+
* container. */
|
|
61
|
+
exports.EXTENSION_FILES = [
|
|
62
|
+
'manifest.json',
|
|
63
|
+
'background.js',
|
|
64
|
+
'content.js',
|
|
65
|
+
'icons/icon-16.png',
|
|
66
|
+
'icons/icon-32.png',
|
|
67
|
+
'icons/icon-48.png',
|
|
68
|
+
'icons/icon-128.png',
|
|
69
|
+
'assets/inter.woff2',
|
|
70
|
+
];
|
|
71
|
+
/** The vendored copy that ships inside this package. */
|
|
72
|
+
function packagedExtensionDir() {
|
|
73
|
+
// dist/desktop/vault-extension.js -> ../../.. is the package root.
|
|
74
|
+
return path.resolve(__dirname, '..', '..', 'assets', 'vault-extension');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Put the extension where the desktop's browser can load it.
|
|
78
|
+
*
|
|
79
|
+
* Copies only what differs, by size and mtime, so calling this on every
|
|
80
|
+
* desktop start is cheap after the first one.
|
|
81
|
+
*/
|
|
82
|
+
function ensureVaultExtension(confDir) {
|
|
83
|
+
const src = packagedExtensionDir();
|
|
84
|
+
if (!fs.existsSync(path.join(src, 'manifest.json'))) {
|
|
85
|
+
return { changed: false, available: false };
|
|
86
|
+
}
|
|
87
|
+
const dest = path.join(confDir, 'vault', 'extension');
|
|
88
|
+
let changed = false;
|
|
89
|
+
// The sign-in script travels with the extension: an extension with no
|
|
90
|
+
// session shows an empty vault, which reads as "broken" rather than "not
|
|
91
|
+
// signed in".
|
|
92
|
+
const seedFrom = path.join(__dirname, 'vault-seed.mjs');
|
|
93
|
+
const seedTo = path.join(confDir, 'vault', 'vault-seed.mjs');
|
|
94
|
+
if (fs.existsSync(seedFrom)
|
|
95
|
+
&& (!fs.existsSync(seedTo) || fs.statSync(seedTo).size !== fs.statSync(seedFrom).size)) {
|
|
96
|
+
fs.mkdirSync(path.dirname(seedTo), { recursive: true });
|
|
97
|
+
fs.copyFileSync(seedFrom, seedTo);
|
|
98
|
+
changed = true;
|
|
99
|
+
}
|
|
100
|
+
for (const rel of exports.EXTENSION_FILES) {
|
|
101
|
+
const from = path.join(src, rel);
|
|
102
|
+
if (!fs.existsSync(from))
|
|
103
|
+
continue;
|
|
104
|
+
const to = path.join(dest, rel);
|
|
105
|
+
if (fs.existsSync(to) && fs.statSync(to).size === fs.statSync(from).size)
|
|
106
|
+
continue;
|
|
107
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
108
|
+
fs.copyFileSync(from, to);
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
return { changed, available: true };
|
|
112
|
+
}
|