@tandryukha/aidemo 0.8.0 → 0.15.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/.claude/skills/record-demo/SKILL.md +5 -2
- package/README.md +16 -4
- package/docs/AUTHORING.md +734 -48
- package/package.json +3 -2
- package/src/anchors.ts +128 -0
- package/src/attention.ts +365 -0
- package/src/caption-render.ts +17 -8
- package/src/captions.ts +244 -36
- package/src/capture.ts +113 -0
- package/src/cards.ts +15 -6
- package/src/cli.ts +711 -26
- package/src/compose.ts +1318 -101
- package/src/cursor-overlay.ts +7 -113
- package/src/cursor.ts +19 -4
- package/src/distribute.ts +37 -4
- package/src/expr.ts +130 -0
- package/src/ffmpeg.ts +224 -0
- package/src/frame.ts +205 -0
- package/src/frames.ts +208 -0
- package/src/golden.ts +27 -0
- package/src/guide.ts +102 -0
- package/src/i18n.ts +7 -4
- package/src/import-trace.ts +640 -0
- package/src/inspect.ts +316 -0
- package/src/lint.ts +673 -0
- package/src/login.ts +143 -0
- package/src/mcp/jobs.ts +5 -1
- package/src/mcp/server.ts +468 -27
- package/src/player.ts +1286 -41
- package/src/profile.ts +128 -0
- package/src/project.ts +12 -2
- package/src/qa.ts +295 -0
- package/src/recorder.ts +316 -21
- package/src/safe-regex.ts +36 -0
- package/src/setup.ts +248 -0
- package/src/starter.ts +244 -0
- package/src/stills.ts +15 -4
- package/src/types.ts +796 -22
- package/src/variants.ts +1 -1
- package/src/voice-local.ts +7 -1
- package/src/voice.ts +60 -1
- package/src/walkthrough.ts +250 -0
- package/src/zoom.ts +85 -39
package/src/login.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { chromeProfileDir } from "./config.js";
|
|
5
|
+
import { ensureDir, log, ok, step, sleep } from "./util.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manual-login helper for the recording profile (issue #22).
|
|
9
|
+
*
|
|
10
|
+
* Recording needs a Chrome profile that is already logged into the demo
|
|
11
|
+
* subject (e.g. ChatGPT with a dev connector enabled). You can't copy a login
|
|
12
|
+
* in: real Chrome encrypts cookies with the OS keychain, while Playwright
|
|
13
|
+
* launches with `--use-mock-keychain --password-store=basic` — so the login
|
|
14
|
+
* must happen ONCE in a Chrome using the same mock store. `aidemo login` runs
|
|
15
|
+
* that exact incantation (previously hand-copied from the docs), then waits
|
|
16
|
+
* for the user to quit Chrome so the profile lock is free for `record`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Chrome's profile-singleton lock: `SingletonLock` in the user-data dir is a
|
|
21
|
+
* symlink whose target is `<hostname>-<pid>`. Returns the pid, or null when
|
|
22
|
+
* the profile isn't locked.
|
|
23
|
+
*/
|
|
24
|
+
export async function profileLockPid(profileDir: string): Promise<number | null> {
|
|
25
|
+
try {
|
|
26
|
+
const target = await fs.readlink(join(profileDir, "SingletonLock"));
|
|
27
|
+
const pid = Number(target.split("-").pop());
|
|
28
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null; // no lock file → not locked
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pidAlive(pid: number): boolean {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(pid, 0);
|
|
37
|
+
return true;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// EPERM = exists but owned by someone else; ESRCH = gone.
|
|
40
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Preflight for `record`: a Chrome still running on the profile means
|
|
46
|
+
* Playwright can't take the lock, and the raw failure (a ProcessSingleton
|
|
47
|
+
* error after a long stall) doesn't say what to do. A LIVE lock aborts with
|
|
48
|
+
* the actual fix; a STALE lock (Chrome crashed and left it behind, which also
|
|
49
|
+
* breaks the launch) is removed.
|
|
50
|
+
*/
|
|
51
|
+
export async function ensureProfileUnlocked(profileDir: string): Promise<void> {
|
|
52
|
+
const pid = await profileLockPid(profileDir);
|
|
53
|
+
if (pid == null) return;
|
|
54
|
+
if (pidAlive(pid)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Chrome is still running on profile ${profileDir} (pid ${pid}) — ` +
|
|
57
|
+
`recording needs exclusive access to the profile. Quit that Chrome ` +
|
|
58
|
+
`completely (Cmd+Q, not just the window), then re-run. ` +
|
|
59
|
+
`(If no Chrome is actually running, delete ${join(profileDir, "SingletonLock")} ` +
|
|
60
|
+
`and retry. To log in on this profile first: aidemo login)`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
await fs.rm(join(profileDir, "SingletonLock"), { force: true });
|
|
64
|
+
log(`removed stale SingletonLock from ${profileDir} (previous Chrome exited uncleanly)`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface LoginOptions {
|
|
68
|
+
/** Page to open for the login. Default: https://chatgpt.com/ */
|
|
69
|
+
url?: string;
|
|
70
|
+
/** Launch Chrome and print next steps without waiting for it to quit. */
|
|
71
|
+
noWait?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Open a NORMAL Chrome window on the recording profile with the same
|
|
76
|
+
* cookie-store flags the recorder launches with, wait for the user to log in
|
|
77
|
+
* and quit, then report the profile ready. A normal window (not a Playwright
|
|
78
|
+
* launch) also dodges Google SSO's automation block.
|
|
79
|
+
*/
|
|
80
|
+
export async function login(profileDir?: string, opts: LoginOptions = {}): Promise<void> {
|
|
81
|
+
const dir = resolve(profileDir ?? chromeProfileDir());
|
|
82
|
+
const url = opts.url ?? "https://chatgpt.com/";
|
|
83
|
+
await ensureDir(dir);
|
|
84
|
+
|
|
85
|
+
const livePid = await profileLockPid(dir);
|
|
86
|
+
if (livePid != null && pidAlive(livePid)) {
|
|
87
|
+
log(`Chrome is already running on this profile (pid ${livePid}) — reusing that window.`);
|
|
88
|
+
} else {
|
|
89
|
+
await fs.rm(join(dir, "SingletonLock"), { force: true });
|
|
90
|
+
step(`Opening Chrome on profile ${dir}`);
|
|
91
|
+
const args = [
|
|
92
|
+
`--user-data-dir=${dir}`,
|
|
93
|
+
// Same cookie store the recorder uses — a login made without these
|
|
94
|
+
// flags is keychain-encrypted and unreadable at record time.
|
|
95
|
+
"--password-store=basic",
|
|
96
|
+
"--use-mock-keychain",
|
|
97
|
+
"--no-first-run",
|
|
98
|
+
"--no-default-browser-check",
|
|
99
|
+
url,
|
|
100
|
+
];
|
|
101
|
+
if (process.platform === "darwin") {
|
|
102
|
+
spawn("open", ["-na", "Google Chrome", "--args", ...args], {
|
|
103
|
+
stdio: "ignore",
|
|
104
|
+
detached: true,
|
|
105
|
+
}).unref();
|
|
106
|
+
} else {
|
|
107
|
+
const bin = process.env.CHROME_BIN || "google-chrome";
|
|
108
|
+
const child = spawn(bin, args, { stdio: "ignore", detached: true });
|
|
109
|
+
child.on("error", () => {
|
|
110
|
+
log(`could not launch "${bin}" (set CHROME_BIN) — open Chrome yourself with:`);
|
|
111
|
+
log(` ${bin} ${args.join(" ")}`);
|
|
112
|
+
});
|
|
113
|
+
child.unref();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log(`1. Log in at ${url} in the window that opened (finish any 2FA).`);
|
|
118
|
+
log(`2. Set the page up for recording (e.g. enable your app's dev connector).`);
|
|
119
|
+
log(`3. Quit Chrome COMPLETELY (Cmd+Q — not just the window).`);
|
|
120
|
+
const recordHint = `aidemo record <demo-dir>${profileDir ? ` --profile ${dir}` : ""}`;
|
|
121
|
+
if (opts.noWait) {
|
|
122
|
+
ok(`when you've quit Chrome, the profile is ready → ${recordHint}`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Wait for Chrome to take the lock (it starts async), then release it.
|
|
127
|
+
const appearDeadline = Date.now() + 60_000;
|
|
128
|
+
while ((await profileLockPid(dir)) == null && Date.now() < appearDeadline) {
|
|
129
|
+
await sleep(500);
|
|
130
|
+
}
|
|
131
|
+
if ((await profileLockPid(dir)) == null) {
|
|
132
|
+
log(`Chrome never took the profile lock (waited 60s) — did the window open?`);
|
|
133
|
+
log(`finish the login manually, quit Chrome, then: ${recordHint}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
log(`waiting for you to finish and quit Chrome… (Ctrl-C to stop waiting)`);
|
|
137
|
+
for (;;) {
|
|
138
|
+
const pid = await profileLockPid(dir);
|
|
139
|
+
if (pid == null || !pidAlive(pid)) break;
|
|
140
|
+
await sleep(1000);
|
|
141
|
+
}
|
|
142
|
+
ok(`profile ready → ${recordHint}`);
|
|
143
|
+
}
|
package/src/mcp/jobs.ts
CHANGED