@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/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
@@ -25,7 +25,11 @@ export type JobKind =
25
25
  | "captions"
26
26
  | "compose"
27
27
  | "gif"
28
- | "stills";
28
+ | "stills"
29
+ | "frames"
30
+ | "qa"
31
+ | "inspect"
32
+ | "walkthrough";
29
33
 
30
34
  export type JobStatus = "running" | "succeeded" | "failed" | "canceled";
31
35