autokap 1.1.0 → 1.1.3
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/skill/OPCODE-REFERENCE.md +1 -41
- package/assets/skill/README.md +0 -1
- package/assets/skill/SKILL.md +9 -32
- package/assets/skill/references/examples.md +1 -17
- package/dist/billing-operation-logging.d.ts +1 -3
- package/dist/billing-operation-logging.js +0 -4
- package/dist/browser.js +164 -1
- package/dist/capture-strategy.d.ts +2 -8
- package/dist/capture-strategy.js +2 -30
- package/dist/cli-config.d.ts +2 -1
- package/dist/cli-config.js +18 -2
- package/dist/cli-contract.d.ts +1 -0
- package/dist/cli-contract.js +8 -2
- package/dist/cli-runner-local.d.ts +2 -0
- package/dist/cli-runner-local.js +12 -21
- package/dist/cli-runner.d.ts +4 -0
- package/dist/cli-runner.js +45 -53
- package/dist/cli.js +89 -44
- package/dist/execution-schema.d.ts +143 -331
- package/dist/execution-schema.js +43 -28
- package/dist/execution-types.d.ts +6 -151
- package/dist/execution-types.js +1 -3
- package/dist/logger.js +1 -1
- package/dist/mockup-html.d.ts +2 -0
- package/dist/mockup-html.js +13 -10
- package/dist/mockup.js +2 -2
- package/dist/opcode-actions.js +0 -2
- package/dist/opcode-runner.js +0 -121
- package/dist/program-signing.d.ts +50 -72
- package/dist/security.js +2 -2
- package/dist/server-capture-runtime.d.ts +0 -1
- package/dist/server-capture-runtime.js +0 -3
- package/dist/server-credit-usage.d.ts +1 -1
- package/dist/skill-packaging.d.ts +1 -1
- package/dist/skill-packaging.js +1 -11
- package/dist/types.d.ts +2 -2
- package/dist/web-playwright-local.d.ts +0 -14
- package/dist/web-playwright-local.js +2 -194
- package/package.json +2 -19
- package/readme.md +13 -0
- package/assets/skill/STUDIO-SKILL.md +0 -476
- package/assets/skill/references/interactive-demo.md +0 -225
package/dist/cli-runner-local.js
CHANGED
|
@@ -24,7 +24,9 @@ export async function runLocal(presetId, opts) {
|
|
|
24
24
|
}
|
|
25
25
|
const run = await runCapture({
|
|
26
26
|
presetId,
|
|
27
|
+
env: opts.env,
|
|
27
28
|
program,
|
|
29
|
+
allowUploadFailure: opts.allowUploadFailure,
|
|
28
30
|
headed: opts.headed,
|
|
29
31
|
onProgress: (event) => {
|
|
30
32
|
const prefix = `[capture][${event.variantId}]`;
|
|
@@ -73,27 +75,16 @@ async function persistArtifactsLocally(presetId, outputDirOption, variants) {
|
|
|
73
75
|
for (const variant of variants) {
|
|
74
76
|
for (let index = 0; index < variant.artifacts.length; index += 1) {
|
|
75
77
|
const artifact = variant.artifacts[index];
|
|
76
|
-
const ext = artifact.
|
|
77
|
-
? '
|
|
78
|
-
: artifact.mimeType === 'image/
|
|
79
|
-
? '
|
|
80
|
-
: artifact.mimeType
|
|
81
|
-
? '
|
|
82
|
-
: artifact.mimeType.includes('
|
|
83
|
-
? '
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
: 'webm';
|
|
87
|
-
let suffix = '';
|
|
88
|
-
if (artifact.mediaMode === 'dom') {
|
|
89
|
-
if (artifact.fragmentName && artifact.parentStateName) {
|
|
90
|
-
suffix = `-${sanitizePathToken(artifact.parentStateName)}-fragment-${sanitizePathToken(artifact.fragmentName)}`;
|
|
91
|
-
}
|
|
92
|
-
else if (artifact.stateName) {
|
|
93
|
-
suffix = `-${sanitizePathToken(artifact.stateName)}`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const fileName = `capture-${sanitizePathToken(presetId.slice(0, 8))}-${sanitizePathToken(variant.variantId)}${suffix}-${index}.${ext}`;
|
|
78
|
+
const ext = artifact.mimeType === 'image/png'
|
|
79
|
+
? 'png'
|
|
80
|
+
: artifact.mimeType === 'image/jpeg'
|
|
81
|
+
? 'jpg'
|
|
82
|
+
: artifact.mimeType.includes('gif')
|
|
83
|
+
? 'gif'
|
|
84
|
+
: artifact.mimeType.includes('mp4')
|
|
85
|
+
? 'mp4'
|
|
86
|
+
: 'webm';
|
|
87
|
+
const fileName = `capture-${sanitizePathToken(presetId.slice(0, 8))}-${sanitizePathToken(variant.variantId)}-${index}.${ext}`;
|
|
97
88
|
const filePath = resolveContainedPath(outputDir, fileName);
|
|
98
89
|
await fs.writeFile(filePath, artifact.buffer);
|
|
99
90
|
logger.info(`[capture] Artifact saved locally: ${filePath}`);
|
package/dist/cli-runner.d.ts
CHANGED
|
@@ -15,8 +15,12 @@ import type { ExecutionProgram, RunResult } from './execution-types.js';
|
|
|
15
15
|
export interface CLIRunnerOptions {
|
|
16
16
|
/** Preset ID to run */
|
|
17
17
|
presetId: string;
|
|
18
|
+
/** Project environment name used by the server to resolve capture URLs */
|
|
19
|
+
env?: string;
|
|
18
20
|
/** Override: provide program directly instead of fetching from server */
|
|
19
21
|
program?: ExecutionProgram;
|
|
22
|
+
/** Keep the legacy success result when upload/telemetry persistence fails */
|
|
23
|
+
allowUploadFailure?: boolean;
|
|
20
24
|
/** Selector memory map (fetched from server or cached locally) */
|
|
21
25
|
selectorMemory?: Record<string, string[]>;
|
|
22
26
|
/** Show browser window. Default: false (headless) */
|
package/dist/cli-runner.js
CHANGED
|
@@ -26,6 +26,7 @@ import { logger } from './logger.js';
|
|
|
26
26
|
import { callLLM } from './llm-provider.js';
|
|
27
27
|
import { APP_VERSION } from './version.js';
|
|
28
28
|
import { normalizeAllowedOrigins, normalizeHttpOrigin, verifySignedExecutionProgramEnvelope, } from './program-signing.js';
|
|
29
|
+
const MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR = 1;
|
|
29
30
|
const HEALER_SYSTEM_PROMPT = 'You repair failed deterministic browser opcodes. Respond only with JSON.';
|
|
30
31
|
// ── Main entry point ────────────────────────────────────────────────
|
|
31
32
|
export async function runCapture(options) {
|
|
@@ -46,7 +47,7 @@ export async function runCapture(options) {
|
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
else {
|
|
49
|
-
const fetched = await fetchProgram(config, options.presetId);
|
|
50
|
+
const fetched = await fetchProgram(config, options.presetId, options.env);
|
|
50
51
|
if (!fetched.success) {
|
|
51
52
|
return { success: false, error: fetched.error };
|
|
52
53
|
}
|
|
@@ -109,18 +110,26 @@ export async function runCapture(options) {
|
|
|
109
110
|
logger.info(`[capture] Concurrency cap resolved to ${program.maxParallelCaptures} parallel variant(s)`);
|
|
110
111
|
}
|
|
111
112
|
const createAdapter = async (variant) => {
|
|
113
|
+
const recordable = program.mediaMode === 'clip';
|
|
114
|
+
const requestedDeviceScaleFactor = variant.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
115
|
+
const runtimeDeviceScaleFactor = recordable && Number.isFinite(requestedDeviceScaleFactor)
|
|
116
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
117
|
+
: requestedDeviceScaleFactor;
|
|
112
118
|
const browserOptions = {
|
|
113
119
|
headed: options.headed ?? false,
|
|
114
120
|
viewport: variant.viewport,
|
|
115
|
-
deviceScaleFactor:
|
|
121
|
+
deviceScaleFactor: runtimeDeviceScaleFactor,
|
|
116
122
|
lang: variant.locale,
|
|
117
123
|
colorScheme: variant.theme,
|
|
118
124
|
storageState: program.preconditions.storageState,
|
|
119
125
|
};
|
|
120
|
-
const recordable = program.mediaMode === 'clip';
|
|
121
126
|
let recordingDir;
|
|
122
127
|
let browser;
|
|
123
128
|
logger.info(`[capture] Launching browser${browserOptions.headed ? ' (headed)' : ''}…`);
|
|
129
|
+
if (recordable && runtimeDeviceScaleFactor !== requestedDeviceScaleFactor) {
|
|
130
|
+
logger.info(`[capture] Clip capture scale capped at ${runtimeDeviceScaleFactor} ` +
|
|
131
|
+
`(requested ${requestedDeviceScaleFactor}) to preserve recording FPS`);
|
|
132
|
+
}
|
|
124
133
|
if (recordable) {
|
|
125
134
|
recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
|
|
126
135
|
browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'));
|
|
@@ -151,15 +160,29 @@ export async function runCapture(options) {
|
|
|
151
160
|
logger.info(`[capture] Captures saved successfully — total ${totalDurationSec}s`);
|
|
152
161
|
}
|
|
153
162
|
catch (err) {
|
|
154
|
-
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
logger.error(`[capture] Failed to upload results: ${message}`);
|
|
165
|
+
if (!options.allowUploadFailure) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
runResult,
|
|
169
|
+
error: runResult.success
|
|
170
|
+
? `upload failed: ${message}`
|
|
171
|
+
: `${runResult.error ?? 'capture failed'}; upload failed: ${message}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
logger.warn('[capture] Continuing after upload failure because --allow-upload-failure was set');
|
|
155
175
|
}
|
|
156
176
|
return { success: runResult.success, runResult };
|
|
157
177
|
}
|
|
158
178
|
// ── Server communication ────────────────────────────────────────────
|
|
159
|
-
async function fetchProgram(config, presetId) {
|
|
179
|
+
async function fetchProgram(config, presetId, environmentName) {
|
|
160
180
|
try {
|
|
161
|
-
const url = `${config.apiBaseUrl}/api/cli/programs/${presetId}
|
|
162
|
-
|
|
181
|
+
const url = new URL(`${config.apiBaseUrl}/api/cli/programs/${presetId}`);
|
|
182
|
+
if (environmentName) {
|
|
183
|
+
url.searchParams.set('env', environmentName);
|
|
184
|
+
}
|
|
185
|
+
const response = await fetch(url.toString(), {
|
|
163
186
|
headers: {
|
|
164
187
|
'Authorization': `Bearer ${config.apiKey}`,
|
|
165
188
|
'Content-Type': 'application/json',
|
|
@@ -167,7 +190,7 @@ async function fetchProgram(config, presetId) {
|
|
|
167
190
|
},
|
|
168
191
|
});
|
|
169
192
|
if (!response.ok) {
|
|
170
|
-
return { success: false, error: await formatServerError(response, url) };
|
|
193
|
+
return { success: false, error: await formatServerError(response, url.toString()) };
|
|
171
194
|
}
|
|
172
195
|
const data = await response.json();
|
|
173
196
|
const envelope = verifySignedExecutionProgramEnvelope({
|
|
@@ -195,7 +218,7 @@ async function uploadResults(config, program, result) {
|
|
|
195
218
|
const formData = new FormData();
|
|
196
219
|
const filename = buildArtifactFilename(program.presetId, variant.variantId, artifact);
|
|
197
220
|
uploadedCount += 1;
|
|
198
|
-
const label = artifact.captureName ?? artifact.clipName ??
|
|
221
|
+
const label = artifact.captureName ?? artifact.clipName ?? filename;
|
|
199
222
|
logger.info(`[capture] Exporting capture ${uploadedCount}/${totalArtifacts}: ${label}`);
|
|
200
223
|
formData.append('file', new Blob([new Uint8Array(artifact.buffer)], { type: artifact.mimeType }), filename);
|
|
201
224
|
formData.append('presetId', program.presetId);
|
|
@@ -214,7 +237,10 @@ async function uploadResults(config, program, result) {
|
|
|
214
237
|
if (variantSpec?.deviceFrame) {
|
|
215
238
|
formData.append('deviceFrame', variantSpec.deviceFrame);
|
|
216
239
|
}
|
|
217
|
-
const
|
|
240
|
+
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
241
|
+
const deviceScaleFactor = artifact.mediaMode === 'clip' && Number.isFinite(requestedDeviceScaleFactor)
|
|
242
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
243
|
+
: requestedDeviceScaleFactor;
|
|
218
244
|
if (Number.isFinite(deviceScaleFactor)) {
|
|
219
245
|
formData.append('deviceScaleFactor', String(deviceScaleFactor));
|
|
220
246
|
}
|
|
@@ -253,35 +279,6 @@ async function uploadResults(config, program, result) {
|
|
|
253
279
|
if (typeof artifact.trimStartMs === 'number') {
|
|
254
280
|
formData.append('trimStartMs', String(artifact.trimStartMs));
|
|
255
281
|
}
|
|
256
|
-
// ── Interactive demo (mediaMode === 'dom') ──
|
|
257
|
-
if (artifact.stateName) {
|
|
258
|
-
formData.append('stateName', artifact.stateName);
|
|
259
|
-
}
|
|
260
|
-
if (artifact.domAssetUrls && artifact.domAssetUrls.length > 0) {
|
|
261
|
-
formData.append('domAssetUrls', JSON.stringify(artifact.domAssetUrls));
|
|
262
|
-
}
|
|
263
|
-
if (artifact.domThumbnailBuffer) {
|
|
264
|
-
formData.append('domThumbnail', new Blob([new Uint8Array(artifact.domThumbnailBuffer)], { type: 'image/png' }), 'thumbnail.png');
|
|
265
|
-
}
|
|
266
|
-
// Phase 5: fragment fields
|
|
267
|
-
if (artifact.fragmentName) {
|
|
268
|
-
formData.append('fragmentName', artifact.fragmentName);
|
|
269
|
-
}
|
|
270
|
-
// Phase 8: variant of the fragment capture (defaults to 'default'
|
|
271
|
-
// server-side when omitted, but we always send it explicitly so the
|
|
272
|
-
// upload row's unique key includes it).
|
|
273
|
-
if (artifact.fragmentVariantName) {
|
|
274
|
-
formData.append('fragmentVariantName', artifact.fragmentVariantName);
|
|
275
|
-
}
|
|
276
|
-
if (artifact.parentStateName) {
|
|
277
|
-
formData.append('parentStateName', artifact.parentStateName);
|
|
278
|
-
}
|
|
279
|
-
if (artifact.mountStrategy) {
|
|
280
|
-
formData.append('mountStrategy', artifact.mountStrategy);
|
|
281
|
-
}
|
|
282
|
-
if (artifact.mountTargetSelector) {
|
|
283
|
-
formData.append('mountTargetSelector', artifact.mountTargetSelector);
|
|
284
|
-
}
|
|
285
282
|
const response = await fetch(`${config.apiBaseUrl}/api/cli/artifacts`, {
|
|
286
283
|
method: 'POST',
|
|
287
284
|
headers: { 'Authorization': `Bearer ${config.apiKey}` },
|
|
@@ -438,17 +435,15 @@ function createHealerLLMProvider(llmConfig) {
|
|
|
438
435
|
};
|
|
439
436
|
}
|
|
440
437
|
function buildArtifactFilename(presetId, variantId, artifact) {
|
|
441
|
-
const ext = artifact.
|
|
442
|
-
? '
|
|
443
|
-
: artifact.mimeType === 'image/
|
|
444
|
-
? '
|
|
445
|
-
: artifact.mimeType
|
|
446
|
-
? '
|
|
447
|
-
: artifact.mimeType.includes('
|
|
448
|
-
? '
|
|
449
|
-
:
|
|
450
|
-
? 'mp4'
|
|
451
|
-
: 'webm';
|
|
438
|
+
const ext = artifact.mimeType === 'image/jpeg'
|
|
439
|
+
? 'jpg'
|
|
440
|
+
: artifact.mimeType === 'image/png'
|
|
441
|
+
? 'png'
|
|
442
|
+
: artifact.mimeType.includes('gif')
|
|
443
|
+
? 'gif'
|
|
444
|
+
: artifact.mimeType.includes('mp4')
|
|
445
|
+
? 'mp4'
|
|
446
|
+
: 'webm';
|
|
452
447
|
const stepToken = typeof artifact.stepIndex === 'number' ? `-${artifact.stepIndex}` : '';
|
|
453
448
|
return `${sanitizeArtifactToken(presetId)}-${sanitizeArtifactToken(variantId)}${stepToken}.${ext}`;
|
|
454
449
|
}
|
|
@@ -497,9 +492,6 @@ function sanitizeArtifactForTelemetry(artifact) {
|
|
|
497
492
|
altText: redactTelemetryText(artifact.altText),
|
|
498
493
|
captureUrl: redactUrl(artifact.captureUrl),
|
|
499
494
|
elementSelector: undefined,
|
|
500
|
-
domAssetUrls: artifact.domAssetUrls
|
|
501
|
-
?.map((entry) => redactUrl(entry))
|
|
502
|
-
.filter((entry) => Boolean(entry)),
|
|
503
495
|
};
|
|
504
496
|
}
|
|
505
497
|
function sanitizeHealerPatches(patches) {
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import fs from 'node:fs/promises';
|
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const { version } = require('../package.json');
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
|
-
import { writeConfig, deleteConfig, requireConfig, getConfigPath, DEFAULT_API_BASE_URL, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
|
|
9
|
+
import { writeConfig, deleteConfig, requireConfig, getConfigPath, DEFAULT_API_BASE_URL, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_KEY_ENV_VAR, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
|
|
10
10
|
import { renderSkillSingleFile, writeSkillExport } from './skill-packaging.js';
|
|
11
11
|
// ── Program definition ──────────────────────────────────────────────
|
|
12
12
|
export const program = new Command();
|
|
@@ -14,6 +14,12 @@ program
|
|
|
14
14
|
.name('autokap')
|
|
15
15
|
.version(version)
|
|
16
16
|
.description('AI-powered screenshot capture — local Playwright proxy');
|
|
17
|
+
function getProjectPrimaryUrl(project) {
|
|
18
|
+
return (project.environments.local ??
|
|
19
|
+
project.environments.staging ??
|
|
20
|
+
project.environments.prod ??
|
|
21
|
+
null);
|
|
22
|
+
}
|
|
17
23
|
function fatal(message) {
|
|
18
24
|
logger.error(message);
|
|
19
25
|
process.exit(1);
|
|
@@ -205,6 +211,8 @@ program
|
|
|
205
211
|
.description('Run a capture using the deterministic opcode engine (local Playwright)')
|
|
206
212
|
.option('--headed', 'Show browser window for debugging', false)
|
|
207
213
|
.option('--local', `Use the local AutoKap dev server (${LOCAL_API_BASE_URL})`, false)
|
|
214
|
+
.option('--env <name>', "Project environment to capture against. Falls back to the project's default environment when omitted.")
|
|
215
|
+
.option('--allow-upload-failure', 'Keep a successful capture exit code even if artifact upload fails', false)
|
|
208
216
|
.option('--output <dir>', 'Optional output directory for local artifact copies')
|
|
209
217
|
.option('--program <file>', 'Path to a program JSON file')
|
|
210
218
|
.option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
|
|
@@ -222,6 +230,59 @@ program
|
|
|
222
230
|
const { runLocal } = await import('./cli-runner-local.js');
|
|
223
231
|
await runLocal(presetId, opts);
|
|
224
232
|
});
|
|
233
|
+
// ── auto-recapture command ─────────────────────────────────────────
|
|
234
|
+
program
|
|
235
|
+
.command('auto-recapture')
|
|
236
|
+
.description('Run every preset enabled for CI auto-recapture in a project')
|
|
237
|
+
.requiredOption('--project <id>', 'Project ID')
|
|
238
|
+
.option('--env <name>', "Project environment to capture against. Falls back to the project's default environment when omitted.")
|
|
239
|
+
.option('--headed', 'Show browser window for debugging', false)
|
|
240
|
+
.option('--local', `Use the local AutoKap dev server (${LOCAL_API_BASE_URL})`, false)
|
|
241
|
+
.option('--allow-upload-failure', 'Keep a successful capture exit code even if artifact upload fails', false)
|
|
242
|
+
.option('--debug', 'Verbose logging: per-substep timing, opcode dumps, recovery strategy traces', false)
|
|
243
|
+
.action(async (opts) => {
|
|
244
|
+
if (opts.debug) {
|
|
245
|
+
const { setDebugEnabled } = await import('./logger.js');
|
|
246
|
+
setDebugEnabled(true);
|
|
247
|
+
logger.info('[capture] Debug mode enabled — verbose logging on');
|
|
248
|
+
}
|
|
249
|
+
if (opts.local) {
|
|
250
|
+
process.env[API_BASE_URL_ENV_VAR] = LOCAL_API_BASE_URL;
|
|
251
|
+
process.env[WS_URL_ENV_VAR] = LOCAL_WS_URL;
|
|
252
|
+
logger.info(`Using local AutoKap dev server: ${LOCAL_API_BASE_URL}`);
|
|
253
|
+
}
|
|
254
|
+
const config = await requireConfig();
|
|
255
|
+
const data = await requestJson(config, `/api/cli/projects/${opts.project}/auto-recapture-presets`, { headers: authHeaders(config) }, 'Failed to list auto-recapture presets');
|
|
256
|
+
if (data.presets.length === 0) {
|
|
257
|
+
logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
const { runCapture } = await import('./cli-runner.js');
|
|
261
|
+
const failures = [];
|
|
262
|
+
for (const preset of data.presets) {
|
|
263
|
+
const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
|
|
264
|
+
logger.info(`[auto-recapture] Running ${label}`);
|
|
265
|
+
const result = await runCapture({
|
|
266
|
+
presetId: preset.id,
|
|
267
|
+
env: opts.env,
|
|
268
|
+
headed: opts.headed,
|
|
269
|
+
allowUploadFailure: opts.allowUploadFailure,
|
|
270
|
+
});
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
const error = result.error ?? result.runResult?.error ?? 'capture failed';
|
|
273
|
+
failures.push({ id: preset.id, name: preset.name, error });
|
|
274
|
+
logger.error(`[auto-recapture] Failed ${label}: ${error}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (failures.length > 0) {
|
|
278
|
+
logger.error(`[auto-recapture] ${failures.length}/${data.presets.length} preset(s) failed: ${failures
|
|
279
|
+
.map((failure) => failure.name ?? failure.id)
|
|
280
|
+
.join(', ')}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
logger.success(`[auto-recapture] ${data.presets.length} preset(s) recaptured successfully`);
|
|
284
|
+
process.exit(0);
|
|
285
|
+
});
|
|
225
286
|
// ── project commands ───────────────────────────────────────────────
|
|
226
287
|
const projectCmd = program
|
|
227
288
|
.command('project')
|
|
@@ -248,16 +309,23 @@ projectCmd
|
|
|
248
309
|
.command('create')
|
|
249
310
|
.description('Create a project')
|
|
250
311
|
.requiredOption('--name <name>', 'Project name')
|
|
251
|
-
.requiredOption('--url <url>', '
|
|
312
|
+
.requiredOption('--base-url <url>', 'Base URL of the chosen environment')
|
|
313
|
+
.option('--environment <name>', "Environment slot to seed: 'local' or 'prod'", 'local')
|
|
252
314
|
.option('--description <text>', 'Project description')
|
|
253
315
|
.action(async (opts) => {
|
|
254
316
|
const config = await requireConfig();
|
|
317
|
+
const env = opts.environment.toLowerCase();
|
|
318
|
+
if (env !== 'local' && env !== 'prod') {
|
|
319
|
+
logger.error("--environment must be 'local' or 'prod'");
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
255
322
|
const data = await requestJson(config, '/api/v1/projects', {
|
|
256
323
|
method: 'POST',
|
|
257
324
|
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
258
325
|
body: JSON.stringify({
|
|
259
326
|
name: opts.name,
|
|
260
|
-
|
|
327
|
+
environment: env,
|
|
328
|
+
base_url: opts.baseUrl,
|
|
261
329
|
description: opts.description,
|
|
262
330
|
}),
|
|
263
331
|
}, 'Failed to create project');
|
|
@@ -492,19 +560,9 @@ presetCmd
|
|
|
492
560
|
// Build per-endpoint info with type-specific URL params
|
|
493
561
|
const endpointEntries = endpoints.map((ep) => {
|
|
494
562
|
const assetType = ep.asset_type ?? 'screenshot';
|
|
495
|
-
const url =
|
|
496
|
-
? `${cfg.apiBaseUrl}/demo/${presetId}`
|
|
497
|
-
: `${cfg.apiBaseUrl}/api/v1/assets/${ep.id}`;
|
|
563
|
+
const url = `${cfg.apiBaseUrl}/api/v1/assets/${ep.id}`;
|
|
498
564
|
let urlParams;
|
|
499
|
-
if (assetType === '
|
|
500
|
-
urlParams = {
|
|
501
|
-
embed: 'Set to "1" for iframe embedding',
|
|
502
|
-
lang: 'Language variant',
|
|
503
|
-
theme: 'Color theme ("light" or "dark")',
|
|
504
|
-
target: 'Device target ID',
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
else if (assetType === 'screenshot') {
|
|
565
|
+
if (assetType === 'screenshot') {
|
|
508
566
|
urlParams = {
|
|
509
567
|
lang: 'Language variant',
|
|
510
568
|
theme: 'Color theme ("light" or "dark")',
|
|
@@ -513,8 +571,6 @@ presetCmd
|
|
|
513
571
|
quality: 'Image quality 1-100',
|
|
514
572
|
format: 'Output format: "webp", "png", "jpg"',
|
|
515
573
|
scale: 'Resolution multiplier (0.5-4)',
|
|
516
|
-
render: 'Set to "studio" for Studio render',
|
|
517
|
-
slot: 'Studio composition slot',
|
|
518
574
|
};
|
|
519
575
|
}
|
|
520
576
|
else if (assetType === 'clip') {
|
|
@@ -523,14 +579,6 @@ presetCmd
|
|
|
523
579
|
theme: 'Color theme ("light" or "dark")',
|
|
524
580
|
target: 'Device target ID',
|
|
525
581
|
format: 'Output format: "gif" (default), "mp4"',
|
|
526
|
-
render: 'Set to "studio" for Studio render',
|
|
527
|
-
slot: 'Studio composition slot',
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
else if (assetType === 'composition') {
|
|
531
|
-
urlParams = {
|
|
532
|
-
scale: 'Resolution multiplier (0.5-4)',
|
|
533
|
-
format: 'Output format: "png", "webp", "jpg"',
|
|
534
582
|
};
|
|
535
583
|
}
|
|
536
584
|
else {
|
|
@@ -555,12 +603,6 @@ presetCmd
|
|
|
555
603
|
variants: { langs, themes, targets },
|
|
556
604
|
endpoints: endpointEntries,
|
|
557
605
|
};
|
|
558
|
-
if (captureMode === 'interactive_demo') {
|
|
559
|
-
info.interactive_demo = {
|
|
560
|
-
url: `${cfg.apiBaseUrl}/demo/${presetId}`,
|
|
561
|
-
embed_url: `${cfg.apiBaseUrl}/demo/${presetId}?embed=1`,
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
606
|
console.log(JSON.stringify(info, null, 2));
|
|
565
607
|
process.exit(0);
|
|
566
608
|
});
|
|
@@ -617,7 +659,15 @@ authCmd
|
|
|
617
659
|
}
|
|
618
660
|
process.exit(0);
|
|
619
661
|
}
|
|
620
|
-
|
|
662
|
+
let startUrl = opts.url;
|
|
663
|
+
if (!startUrl) {
|
|
664
|
+
const project = await loadProject(cfg, projectId);
|
|
665
|
+
startUrl = getProjectPrimaryUrl(project) ?? undefined;
|
|
666
|
+
}
|
|
667
|
+
if (!startUrl) {
|
|
668
|
+
logger.error(`No environment URL configured for project ${projectId}. Configure local/staging/prod in the AutoKap web UI, or pass --url.`);
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
621
671
|
logger.info(`Opening ${startUrl}`);
|
|
622
672
|
const { captureAuthSession } = await import('./auth-capture.js');
|
|
623
673
|
await captureAuthSession({
|
|
@@ -840,25 +890,16 @@ const AGENT_PATHS = {
|
|
|
840
890
|
windsurf: '.windsurf/rules/autokap-preset.md',
|
|
841
891
|
copilot: '.github/instructions/autokap-preset.instructions.md',
|
|
842
892
|
};
|
|
843
|
-
const STUDIO_AGENT_PATHS = {
|
|
844
|
-
claude: '.claude/commands/autokap-studio.md',
|
|
845
|
-
codex: '.agents/skills/autokap-studio/SKILL.md',
|
|
846
|
-
cursor: '.cursor/rules/autokap-studio.md',
|
|
847
|
-
windsurf: '.windsurf/rules/autokap-studio.md',
|
|
848
|
-
copilot: '.github/instructions/autokap-studio.instructions.md',
|
|
849
|
-
};
|
|
850
893
|
program
|
|
851
894
|
.command('skill')
|
|
852
895
|
.description('Output or install an AutoKap skill for AI coding agents')
|
|
853
896
|
.option('--output <path>', 'Write the generated skill output to this path instead of stdout')
|
|
854
897
|
.option('--agent <name>', 'Target AI coding agent: claude, codex, cursor, windsurf, copilot (auto-resolves output path and packaging mode)')
|
|
855
|
-
.option('--type <type>', 'Skill type: preset (default) or studio (composition designer)')
|
|
856
898
|
.option('--project-url <url>', 'Replace the project URL placeholder in the skill')
|
|
857
899
|
.option('--project-id <id>', 'Replace the project ID placeholder in the skill')
|
|
858
900
|
.option('--api-base-url <url>', 'Replace the API base URL placeholder (default: https://autokap.app)')
|
|
859
901
|
.action(async (opts) => {
|
|
860
|
-
const
|
|
861
|
-
const pathMap = skillType === 'studio' ? STUDIO_AGENT_PATHS : AGENT_PATHS;
|
|
902
|
+
const pathMap = AGENT_PATHS;
|
|
862
903
|
// Resolve --agent to an output path
|
|
863
904
|
if (opts.agent) {
|
|
864
905
|
const agentKey = opts.agent.toLowerCase();
|
|
@@ -875,7 +916,7 @@ program
|
|
|
875
916
|
try {
|
|
876
917
|
if (opts.output) {
|
|
877
918
|
const result = await writeSkillExport({
|
|
878
|
-
type:
|
|
919
|
+
type: 'preset',
|
|
879
920
|
agent: opts.agent?.toLowerCase(),
|
|
880
921
|
outputPath: opts.output,
|
|
881
922
|
placeholders: opts,
|
|
@@ -889,7 +930,7 @@ program
|
|
|
889
930
|
}
|
|
890
931
|
else {
|
|
891
932
|
const content = await renderSkillSingleFile({
|
|
892
|
-
type:
|
|
933
|
+
type: 'preset',
|
|
893
934
|
agent: opts.agent?.toLowerCase(),
|
|
894
935
|
placeholders: opts,
|
|
895
936
|
});
|
|
@@ -916,6 +957,10 @@ program
|
|
|
916
957
|
// If --cli-key not provided, prompt interactively
|
|
917
958
|
let cliKey = opts.cliKey;
|
|
918
959
|
if (!cliKey) {
|
|
960
|
+
if (process.stdin.isTTY === false) {
|
|
961
|
+
logger.error(`CLI key is required in non-interactive shells. Pass --cli-key <key> or set ${API_KEY_ENV_VAR} for CI runs.`);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
919
964
|
const readline = await import('node:readline');
|
|
920
965
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
921
966
|
cliKey = await new Promise((resolve) => {
|