takomi 2.1.39 → 2.1.41
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/.agents/plugins/marketplace.json +22 -22
- package/README.md +217 -422
- package/assets/.agent/skills/remotion/rules/assets/charts-bar-chart.tsx +173 -173
- package/assets/.agent/skills/remotion/rules/assets/text-animations-typewriter.tsx +100 -100
- package/assets/.agent/skills/remotion/rules/text-animations.md +20 -20
- package/assets/.agent/skills/remotion/rules/videos.md +171 -171
- package/assets/.agent/skills/remotion-real-ui-video/SKILL.md +264 -0
- package/assets/.agent/skills/remotion-real-ui-video/references/anti-patterns.md +114 -0
- package/assets/.agent/skills/remotion-real-ui-video/references/completion-checklist.md +75 -0
- package/assets/.agent/skills/remotion-real-ui-video/references/real-ui-bridge-example.md +93 -0
- package/assets/.agent/skills/takomi-flow/SKILL.md +244 -235
- package/docs/features/TakomiFlow_Portable_Plugin.md +82 -78
- package/docs/takomi-flow-onboarding.md +86 -76
- package/docs/takomi-flow-public-distribution.md +94 -94
- package/package.json +1 -1
- package/plugins/takomi-flow/.mcp.json +11 -11
- package/plugins/takomi-flow/assets/capabilities.json +203 -171
- package/plugins/takomi-flow/assets/collection.schema.json +29 -29
- package/plugins/takomi-flow/assets/examples.json +94 -94
- package/plugins/takomi-flow/assets/request.schema.json +91 -67
- package/plugins/takomi-flow/assets/result.schema.json +46 -45
- package/plugins/takomi-flow/assets/templates/image-request.json +17 -13
- package/plugins/takomi-flow/assets/templates/video-request.json +19 -15
- package/plugins/takomi-flow/package.json +38 -38
- package/plugins/takomi-flow/pnpm-lock.yaml +813 -813
- package/plugins/takomi-flow/references/flow-provider-contract.md +349 -326
- package/plugins/takomi-flow/scripts/lib/agent-plan.mjs +140 -130
- package/plugins/takomi-flow/scripts/lib/api.mjs +113 -113
- package/plugins/takomi-flow/scripts/lib/args.mjs +66 -57
- package/plugins/takomi-flow/scripts/lib/audit.mjs +65 -65
- package/plugins/takomi-flow/scripts/lib/browser.mjs +78 -39
- package/plugins/takomi-flow/scripts/lib/capabilities.mjs +11 -11
- package/plugins/takomi-flow/scripts/lib/collect.mjs +53 -53
- package/plugins/takomi-flow/scripts/lib/commands.mjs +192 -175
- package/plugins/takomi-flow/scripts/lib/doctor.mjs +77 -77
- package/plugins/takomi-flow/scripts/lib/examples.mjs +17 -17
- package/plugins/takomi-flow/scripts/lib/flow-media.mjs +39 -0
- package/plugins/takomi-flow/scripts/lib/flow-outcome.mjs +115 -90
- package/plugins/takomi-flow/scripts/lib/flow-project-session.mjs +153 -0
- package/plugins/takomi-flow/scripts/lib/flow-ui.mjs +204 -165
- package/plugins/takomi-flow/scripts/lib/generation.mjs +190 -90
- package/plugins/takomi-flow/scripts/lib/inspect.mjs +81 -81
- package/plugins/takomi-flow/scripts/lib/mcp-prompts.mjs +39 -39
- package/plugins/takomi-flow/scripts/lib/mcp-resources.mjs +16 -16
- package/plugins/takomi-flow/scripts/lib/mcp-tools.mjs +165 -150
- package/plugins/takomi-flow/scripts/lib/media.mjs +78 -78
- package/plugins/takomi-flow/scripts/lib/observe.mjs +54 -54
- package/plugins/takomi-flow/scripts/lib/paths.mjs +37 -37
- package/plugins/takomi-flow/scripts/lib/playwright-loader.mjs +23 -23
- package/plugins/takomi-flow/scripts/lib/prompts.mjs +72 -72
- package/plugins/takomi-flow/scripts/lib/report.mjs +141 -141
- package/plugins/takomi-flow/scripts/lib/request-validator.mjs +73 -58
- package/plugins/takomi-flow/scripts/lib/request.mjs +66 -55
- package/plugins/takomi-flow/scripts/lib/resource-files.mjs +69 -69
- package/plugins/takomi-flow/scripts/lib/result.mjs +31 -31
- package/plugins/takomi-flow/scripts/lib/review.mjs +44 -44
- package/plugins/takomi-flow/scripts/lib/selftest.mjs +153 -153
- package/plugins/takomi-flow/scripts/lib/settings-plan.mjs +44 -39
- package/plugins/takomi-flow/scripts/lib/templates.mjs +37 -37
- package/plugins/takomi-flow/scripts/lib/trusted-chrome.mjs +67 -67
- package/plugins/takomi-flow/scripts/lib/workflow.mjs +56 -56
- package/plugins/takomi-flow/scripts/mcp-server.mjs +18 -18
- package/plugins/takomi-flow/scripts/mcp-smoke.mjs +144 -142
- package/plugins/takomi-flow/scripts/takomi-flow.mjs +17 -17
- package/plugins/takomi-flow/skills/takomi-flow/SKILL.md +244 -235
- package/plugins/takomi-flow/skills/takomi-flow/agents/openai.yaml +7 -7
- package/scripts/install-takomi-flow.ps1 +85 -85
- package/src/skills-catalog.js +10 -9
|
@@ -1,175 +1,192 @@
|
|
|
1
|
-
import { defaultProfileDir, defaultRunsDir, FLOW_URL, resolvePath } from './paths.mjs';
|
|
2
|
-
import { closeFlowBrowser, launchFlowBrowser } from './browser.mjs';
|
|
3
|
-
import { createRun, baseResult, saveResult } from './result.mjs';
|
|
4
|
-
import { capture, inspectFlowState, openFlow } from './flow-ui.mjs';
|
|
5
|
-
import { launchTrustedChrome } from './trusted-chrome.mjs';
|
|
6
|
-
import * as api from './api.mjs';
|
|
7
|
-
|
|
8
|
-
export async function handleCommand(command, args) {
|
|
9
|
-
if (command === 'prepare') return prepare(args);
|
|
10
|
-
if (command === 'workflow') return printJson(await api.workflow({
|
|
11
|
-
...args,
|
|
12
|
-
outputDir: args['output-dir'],
|
|
13
|
-
allowSpend:
|
|
14
|
-
extractFrames: args['extract-frames'],
|
|
15
|
-
sourceAssets: args.assets,
|
|
16
|
-
allowBrowser:
|
|
17
|
-
profileDir: args['profile-dir'],
|
|
18
|
-
browserChannel: args['browser-channel'],
|
|
19
|
-
cdpUrl: args['cdp-url'],
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (command === '
|
|
40
|
-
if (command === '
|
|
41
|
-
|
|
42
|
-
outputDir: args['output-dir'],
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1
|
+
import { defaultProfileDir, defaultRunsDir, FLOW_URL, resolvePath } from './paths.mjs';
|
|
2
|
+
import { closeFlowBrowser, launchFlowBrowser } from './browser.mjs';
|
|
3
|
+
import { createRun, baseResult, saveResult } from './result.mjs';
|
|
4
|
+
import { capture, inspectFlowState, openFlow } from './flow-ui.mjs';
|
|
5
|
+
import { launchTrustedChrome } from './trusted-chrome.mjs';
|
|
6
|
+
import * as api from './api.mjs';
|
|
7
|
+
|
|
8
|
+
export async function handleCommand(command, args) {
|
|
9
|
+
if (command === 'prepare') return prepare(args);
|
|
10
|
+
if (command === 'workflow') return printJson(await api.workflow({
|
|
11
|
+
...args,
|
|
12
|
+
outputDir: args['output-dir'],
|
|
13
|
+
allowSpend: booleanArg(args['allow-spend']),
|
|
14
|
+
extractFrames: args['extract-frames'],
|
|
15
|
+
sourceAssets: args.assets,
|
|
16
|
+
allowBrowser: booleanArg(args['allow-browser']),
|
|
17
|
+
profileDir: args['profile-dir'],
|
|
18
|
+
browserChannel: args['browser-channel'],
|
|
19
|
+
cdpUrl: args['cdp-url'],
|
|
20
|
+
headless: booleanArg(args.headless),
|
|
21
|
+
submit: booleanArg(args.submit),
|
|
22
|
+
projectUrl: args['project-url'],
|
|
23
|
+
reuseCurrentProject: args['reuse-current-project'],
|
|
24
|
+
allowNewProject: args['allow-new-project'],
|
|
25
|
+
freshChatOnFailure: args['fresh-chat-on-failure'],
|
|
26
|
+
editorWaitMs: args['editor-wait-ms'],
|
|
27
|
+
}));
|
|
28
|
+
if (command === 'template') return template(args);
|
|
29
|
+
if (command === 'examples') return printJson(api.examples({ name: args.name }));
|
|
30
|
+
if (command === 'trusted-chrome') return printJson(launchTrustedChrome({
|
|
31
|
+
profileDir: args['profile-dir'],
|
|
32
|
+
chromePath: args['chrome-path'],
|
|
33
|
+
port: args.port,
|
|
34
|
+
url: args.url,
|
|
35
|
+
printCommand: booleanArg(args['print-command']),
|
|
36
|
+
}));
|
|
37
|
+
if (command === 'validate') return printJson(api.validate({ request: args.request }));
|
|
38
|
+
if (command === 'bootstrap') return bootstrap(args);
|
|
39
|
+
if (command === 'doctor') return doctor(args);
|
|
40
|
+
if (command === 'audit') return printJson(await api.audit({
|
|
41
|
+
profileDir: args['profile-dir'],
|
|
42
|
+
outputDir: args['output-dir'],
|
|
43
|
+
limit: args.limit,
|
|
44
|
+
}));
|
|
45
|
+
if (command === 'capabilities') return printJson(api.capabilities());
|
|
46
|
+
if (command === 'plan') return printJson(api.plan({
|
|
47
|
+
...args,
|
|
48
|
+
outputDir: args['output-dir'],
|
|
49
|
+
allowSpend: booleanArg(args['allow-spend']),
|
|
50
|
+
extractFrames: args['extract-frames'],
|
|
51
|
+
sourceAssets: args.assets,
|
|
52
|
+
submit: booleanArg(args.submit),
|
|
53
|
+
targetDir: args['target-dir'],
|
|
54
|
+
}));
|
|
55
|
+
if (command === 'observe') return observe(args);
|
|
56
|
+
if (command === 'smoke') return smoke(args);
|
|
57
|
+
if (command === 'generate') return printJson(await api.generate({
|
|
58
|
+
request: args.request,
|
|
59
|
+
profileDir: args['profile-dir'],
|
|
60
|
+
browserChannel: args['browser-channel'],
|
|
61
|
+
cdpUrl: args['cdp-url'],
|
|
62
|
+
headless: booleanArg(args.headless),
|
|
63
|
+
projectUrl: args['project-url'],
|
|
64
|
+
reuseCurrentProject: args['reuse-current-project'],
|
|
65
|
+
allowNewProject: args['allow-new-project'],
|
|
66
|
+
freshChatOnFailure: args['fresh-chat-on-failure'],
|
|
67
|
+
editorWaitMs: args['editor-wait-ms'],
|
|
68
|
+
}));
|
|
69
|
+
if (command === 'selftest') return printJson(await api.selftest({ outputDir: args['output-dir'] }));
|
|
70
|
+
if (command === 'inspect') return printJson(api.inspect({ run: args.run }));
|
|
71
|
+
if (command === 'latest') return printJson(api.latest({ outputDir: args['output-dir'] }));
|
|
72
|
+
if (command === 'runs') return printJson(api.runs({ outputDir: args['output-dir'], limit: args.limit }));
|
|
73
|
+
if (command === 'assets') return assets(args);
|
|
74
|
+
if (command === 'review') return printJson(await api.review({
|
|
75
|
+
run: args.run,
|
|
76
|
+
frames: args.frames || 0,
|
|
77
|
+
reportPath: args['report-path'],
|
|
78
|
+
}));
|
|
79
|
+
if (command === 'collect') return printJson(await api.collect({
|
|
80
|
+
run: args.run,
|
|
81
|
+
targetDir: args['target-dir'],
|
|
82
|
+
frames: args.frames || 0,
|
|
83
|
+
includeFrames: booleanArg(args['include-frames']),
|
|
84
|
+
reportPath: args['report-path'],
|
|
85
|
+
}));
|
|
86
|
+
if (command === 'report') return printJson(api.report({
|
|
87
|
+
run: args.run,
|
|
88
|
+
outputDir: args['output-dir'],
|
|
89
|
+
limit: args.limit,
|
|
90
|
+
reportPath: args['report-path'],
|
|
91
|
+
}));
|
|
92
|
+
throw new Error(`Unknown command: ${command}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function booleanArg(value) {
|
|
96
|
+
if (value === undefined || value === null || value === '') return false;
|
|
97
|
+
if (typeof value === 'boolean') return value;
|
|
98
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function observe(cliArgs) {
|
|
102
|
+
printJson(await api.observe({
|
|
103
|
+
profileDir: cliArgs['profile-dir'],
|
|
104
|
+
outputDir: cliArgs['output-dir'],
|
|
105
|
+
browserChannel: cliArgs['browser-channel'],
|
|
106
|
+
cdpUrl: cliArgs['cdp-url'],
|
|
107
|
+
headless: booleanArg(cliArgs.headless),
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function doctor(cliArgs) {
|
|
112
|
+
const report = await api.doctor({
|
|
113
|
+
profileDir: cliArgs['profile-dir'],
|
|
114
|
+
outputDir: cliArgs['output-dir'],
|
|
115
|
+
});
|
|
116
|
+
printJson(report);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function assets(cliArgs) {
|
|
120
|
+
printJson(await api.assets({ run: cliArgs.run, frames: cliArgs.frames || 0 }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function prepare(cliArgs) {
|
|
124
|
+
printJson(api.prepare(cliArgs));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function template(cliArgs) {
|
|
128
|
+
printJson(api.template({ kind: cliArgs.kind, outputDir: cliArgs['output-dir'] }));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function bootstrap(cliArgs) {
|
|
132
|
+
const outputDir = resolvePath(cliArgs['output-dir'], defaultRunsDir());
|
|
133
|
+
const run = createRun({ outputDir }, 'bootstrap');
|
|
134
|
+
const profileDir = resolvePath(cliArgs['profile-dir'], defaultProfileDir());
|
|
135
|
+
const result = baseResult('manual_action_required', run, {
|
|
136
|
+
command: 'bootstrap',
|
|
137
|
+
flowUrl: FLOW_URL,
|
|
138
|
+
profileDir,
|
|
139
|
+
manualActions: ['Log into Google Flow in the opened browser, then close it when finished.'],
|
|
140
|
+
});
|
|
141
|
+
const browser = await launchFlowBrowser({
|
|
142
|
+
profileDir,
|
|
143
|
+
downloadsDir: run.downloadsDir,
|
|
144
|
+
browserChannel: cliArgs['browser-channel'],
|
|
145
|
+
cdpUrl: cliArgs['cdp-url'],
|
|
146
|
+
headless: booleanArg(cliArgs.headless),
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
await openFlow(browser.page);
|
|
150
|
+
result.screenshots.push(await capture(browser.page, run, 'bootstrap-opened'));
|
|
151
|
+
Object.assign(result, await inspectFlowState(browser.page));
|
|
152
|
+
console.log(`[TakomiFlow] Browser opened for login/profile setup: ${FLOW_URL}`);
|
|
153
|
+
console.log('[TakomiFlow] Close the browser window when login/setup is complete.');
|
|
154
|
+
if (!browser.attached) await browser.context.waitForEvent('close', { timeout: 30 * 60 * 1000 }).catch(() => {});
|
|
155
|
+
} finally {
|
|
156
|
+
await closeFlowBrowser(browser);
|
|
157
|
+
saveResult(run, result);
|
|
158
|
+
console.log(`[TakomiFlow] Result: ${result.metadataPath}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function smoke(cliArgs) {
|
|
163
|
+
const outputDir = resolvePath(cliArgs['output-dir'], defaultRunsDir());
|
|
164
|
+
const run = createRun({ outputDir }, 'smoke');
|
|
165
|
+
const profileDir = resolvePath(cliArgs['profile-dir'], defaultProfileDir());
|
|
166
|
+
const browser = await launchFlowBrowser({
|
|
167
|
+
profileDir,
|
|
168
|
+
downloadsDir: run.downloadsDir,
|
|
169
|
+
browserChannel: cliArgs['browser-channel'],
|
|
170
|
+
cdpUrl: cliArgs['cdp-url'],
|
|
171
|
+
headless: booleanArg(cliArgs.headless),
|
|
172
|
+
});
|
|
173
|
+
const result = baseResult('ok', run, { command: 'smoke', flowUrl: FLOW_URL, profileDir });
|
|
174
|
+
try {
|
|
175
|
+
await openFlow(browser.page);
|
|
176
|
+
result.screenshots.push(await capture(browser.page, run, 'smoke-flow'));
|
|
177
|
+
const state = await inspectFlowState(browser.page);
|
|
178
|
+
Object.assign(result, state);
|
|
179
|
+
if (state.manualActions.length) result.status = 'manual_action_required';
|
|
180
|
+
} catch (error) {
|
|
181
|
+
result.status = 'failed';
|
|
182
|
+
result.errors.push(error.message);
|
|
183
|
+
} finally {
|
|
184
|
+
await closeFlowBrowser(browser);
|
|
185
|
+
saveResult(run, result);
|
|
186
|
+
printJson(result);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function printJson(value) {
|
|
191
|
+
console.log(JSON.stringify(value, null, 2));
|
|
192
|
+
}
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
import { defaultProfileDir, defaultRunsDir, ensureDir, FLOW_URL } from './paths.mjs';
|
|
6
|
-
import { loadPlaywright } from './playwright-loader.mjs';
|
|
7
|
-
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
|
|
10
|
-
export async function runDoctor(options = {}) {
|
|
11
|
-
const profileDir = path.resolve(options.profileDir || defaultProfileDir());
|
|
12
|
-
const outputDir = path.resolve(options.outputDir || defaultRunsDir());
|
|
13
|
-
const checks = [];
|
|
14
|
-
checks.push({ name: 'node', status: 'ok', version: process.version });
|
|
15
|
-
checks.push(await checkPlaywright());
|
|
16
|
-
checks.push(await checkExecutable('ffmpeg', ['-version']));
|
|
17
|
-
checks.push(await checkExecutable('ffprobe', ['-version']));
|
|
18
|
-
checks.push(checkPath('profileDir', profileDir, false));
|
|
19
|
-
checks.push(checkPath('outputDir', outputDir, true));
|
|
20
|
-
const failed = checks.some(check => check.status === 'failed');
|
|
21
|
-
return {
|
|
22
|
-
schemaVersion: 1,
|
|
23
|
-
status: failed ? 'failed' : 'ok',
|
|
24
|
-
flowUrl: FLOW_URL,
|
|
25
|
-
profileDir,
|
|
26
|
-
outputDir,
|
|
27
|
-
checks,
|
|
28
|
-
recommendations: recommendations(checks),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function checkPlaywright() {
|
|
33
|
-
try {
|
|
34
|
-
await loadPlaywright();
|
|
35
|
-
return { name: 'playwright', status: 'ok' };
|
|
36
|
-
} catch (error) {
|
|
37
|
-
return { name: 'playwright', status: 'failed', message: error.message };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function checkExecutable(name, args) {
|
|
42
|
-
try {
|
|
43
|
-
const { stdout, stderr } = await execFileAsync(name, args, { timeout: 10000 });
|
|
44
|
-
const version = `${stdout || stderr}`.split(/\r?\n/).find(Boolean) || '';
|
|
45
|
-
return { name, status: 'ok', version };
|
|
46
|
-
} catch (error) {
|
|
47
|
-
return { name, status: 'warning', message: `${name} is unavailable: ${error.message}` };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function checkPath(name, targetPath, createIfMissing) {
|
|
52
|
-
if (fs.existsSync(targetPath)) {
|
|
53
|
-
return { name, status: 'ok', path: targetPath, exists: true };
|
|
54
|
-
}
|
|
55
|
-
if (createIfMissing) {
|
|
56
|
-
ensureDir(targetPath);
|
|
57
|
-
return { name, status: 'ok', path: targetPath, exists: true, created: true };
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
name,
|
|
61
|
-
status: 'warning',
|
|
62
|
-
path: targetPath,
|
|
63
|
-
exists: false,
|
|
64
|
-
message: 'Profile does not exist yet. Run bootstrap to create it and log into Flow.',
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function recommendations(checks) {
|
|
69
|
-
return checks
|
|
70
|
-
.filter(check => check.status !== 'ok')
|
|
71
|
-
.map(check => {
|
|
72
|
-
if (check.name === 'playwright') return 'Run pnpm install in the TakomiFlow plugin folder.';
|
|
73
|
-
if (check.name === 'ffmpeg' || check.name === 'ffprobe') return 'Install FFmpeg if media metadata or frame extraction is needed.';
|
|
74
|
-
if (check.name === 'profileDir') return 'Run bootstrap in headed mode and log into Google Flow manually.';
|
|
75
|
-
return check.message || `Review ${check.name}.`;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { defaultProfileDir, defaultRunsDir, ensureDir, FLOW_URL } from './paths.mjs';
|
|
6
|
+
import { loadPlaywright } from './playwright-loader.mjs';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export async function runDoctor(options = {}) {
|
|
11
|
+
const profileDir = path.resolve(options.profileDir || defaultProfileDir());
|
|
12
|
+
const outputDir = path.resolve(options.outputDir || defaultRunsDir());
|
|
13
|
+
const checks = [];
|
|
14
|
+
checks.push({ name: 'node', status: 'ok', version: process.version });
|
|
15
|
+
checks.push(await checkPlaywright());
|
|
16
|
+
checks.push(await checkExecutable('ffmpeg', ['-version']));
|
|
17
|
+
checks.push(await checkExecutable('ffprobe', ['-version']));
|
|
18
|
+
checks.push(checkPath('profileDir', profileDir, false));
|
|
19
|
+
checks.push(checkPath('outputDir', outputDir, true));
|
|
20
|
+
const failed = checks.some(check => check.status === 'failed');
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
status: failed ? 'failed' : 'ok',
|
|
24
|
+
flowUrl: FLOW_URL,
|
|
25
|
+
profileDir,
|
|
26
|
+
outputDir,
|
|
27
|
+
checks,
|
|
28
|
+
recommendations: recommendations(checks),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function checkPlaywright() {
|
|
33
|
+
try {
|
|
34
|
+
await loadPlaywright();
|
|
35
|
+
return { name: 'playwright', status: 'ok' };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { name: 'playwright', status: 'failed', message: error.message };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function checkExecutable(name, args) {
|
|
42
|
+
try {
|
|
43
|
+
const { stdout, stderr } = await execFileAsync(name, args, { timeout: 10000 });
|
|
44
|
+
const version = `${stdout || stderr}`.split(/\r?\n/).find(Boolean) || '';
|
|
45
|
+
return { name, status: 'ok', version };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return { name, status: 'warning', message: `${name} is unavailable: ${error.message}` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkPath(name, targetPath, createIfMissing) {
|
|
52
|
+
if (fs.existsSync(targetPath)) {
|
|
53
|
+
return { name, status: 'ok', path: targetPath, exists: true };
|
|
54
|
+
}
|
|
55
|
+
if (createIfMissing) {
|
|
56
|
+
ensureDir(targetPath);
|
|
57
|
+
return { name, status: 'ok', path: targetPath, exists: true, created: true };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
status: 'warning',
|
|
62
|
+
path: targetPath,
|
|
63
|
+
exists: false,
|
|
64
|
+
message: 'Profile does not exist yet. Run bootstrap to create it and log into Flow.',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function recommendations(checks) {
|
|
69
|
+
return checks
|
|
70
|
+
.filter(check => check.status !== 'ok')
|
|
71
|
+
.map(check => {
|
|
72
|
+
if (check.name === 'playwright') return 'Run pnpm install in the TakomiFlow plugin folder.';
|
|
73
|
+
if (check.name === 'ffmpeg' || check.name === 'ffprobe') return 'Install FFmpeg if media metadata or frame extraction is needed.';
|
|
74
|
+
if (check.name === 'profileDir') return 'Run bootstrap in headed mode and log into Google Flow manually.';
|
|
75
|
+
return check.message || `Review ${check.name}.`;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const PLUGIN_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
7
|
-
const EXAMPLES_PATH = path.join(PLUGIN_ROOT, 'assets', 'examples.json');
|
|
8
|
-
|
|
9
|
-
export function loadExamples(name = undefined) {
|
|
10
|
-
const data = JSON.parse(fs.readFileSync(EXAMPLES_PATH, 'utf8'));
|
|
11
|
-
if (!name) return data;
|
|
12
|
-
const example = data.examples.find(item => item.name === name);
|
|
13
|
-
if (!example) {
|
|
14
|
-
throw new Error(`Unknown example "${name}". Available: ${data.examples.map(item => item.name).join(', ')}`);
|
|
15
|
-
}
|
|
16
|
-
return { schemaVersion: data.schemaVersion, example };
|
|
17
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const PLUGIN_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
7
|
+
const EXAMPLES_PATH = path.join(PLUGIN_ROOT, 'assets', 'examples.json');
|
|
8
|
+
|
|
9
|
+
export function loadExamples(name = undefined) {
|
|
10
|
+
const data = JSON.parse(fs.readFileSync(EXAMPLES_PATH, 'utf8'));
|
|
11
|
+
if (!name) return data;
|
|
12
|
+
const example = data.examples.find(item => item.name === name);
|
|
13
|
+
if (!example) {
|
|
14
|
+
throw new Error(`Unknown example "${name}". Available: ${data.examples.map(item => item.name).join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
return { schemaVersion: data.schemaVersion, example };
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export async function listMediaIds(page) {
|
|
2
|
+
const ids = await page.locator('img[src*="media.getMediaUrlRedirect"], video[src*="media.getMediaUrlRedirect"]').evaluateAll(items => (
|
|
3
|
+
items.map(item => mediaIdFromUrl(item.getAttribute('src') || item.src || '')).filter(Boolean)
|
|
4
|
+
)).catch(() => []);
|
|
5
|
+
return [...new Set(ids)];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function newMediaIds(page, baselineIds = []) {
|
|
9
|
+
const baseline = new Set(baselineIds);
|
|
10
|
+
return (await listMediaIds(page)).filter(id => !baseline.has(id));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function openGeneratedMediaById(page, mediaId) {
|
|
14
|
+
if (!mediaId) return false;
|
|
15
|
+
const candidates = [
|
|
16
|
+
page.locator(`img[src*="${mediaId}"]`).first(),
|
|
17
|
+
page.locator(`video[src*="${mediaId}"]`).first(),
|
|
18
|
+
];
|
|
19
|
+
for (const locator of candidates) {
|
|
20
|
+
if (!(await locator.count().catch(() => 0))) continue;
|
|
21
|
+
try {
|
|
22
|
+
await locator.scrollIntoViewIfNeeded().catch(() => {});
|
|
23
|
+
await locator.click({ timeout: 10000, force: true });
|
|
24
|
+
await page.waitForTimeout(3000);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mediaIdFromUrl(value) {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(value, 'https://labs.google');
|
|
34
|
+
return url.searchParams.get('name');
|
|
35
|
+
} catch {
|
|
36
|
+
const match = /[?&]name=([^&]+)/.exec(value);
|
|
37
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
38
|
+
}
|
|
39
|
+
}
|