autokap 1.6.2 → 1.6.4
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/dist/cli-config.js +69 -25
- package/dist/cli-contract.d.ts +19 -5
- package/dist/cli-contract.js +55 -151
- package/dist/cli-doctor.d.ts +0 -1
- package/dist/cli-doctor.js +15 -143
- package/dist/cli-runner.d.ts +1 -1
- package/dist/cli-runner.js +2 -2
- package/dist/cli.js +24 -1163
- package/dist/execution-schema.d.ts +3 -3
- package/dist/program-signing.d.ts +1 -1
- package/dist/skill-packaging.d.ts +0 -16
- package/dist/skill-packaging.js +1 -51
- package/dist/video-narration-schema.d.ts +1 -1
- package/package.json +2 -6
- package/readme.md +15 -12
- package/assets/skill/OPCODE-REFERENCE.md +0 -625
- package/assets/skill/README.md +0 -38
- package/assets/skill/SKILL.md +0 -590
- package/assets/skill/references/STANDARDS.md +0 -236
- package/assets/skill/references/examples.md +0 -88
- package/assets/skill/references/mock-data.md +0 -178
- package/dist/auth-capture.d.ts +0 -17
- package/dist/auth-capture.js +0 -199
- package/dist/cli-utils.d.ts +0 -5
- package/dist/cli-utils.js +0 -14
- package/dist/version-check.d.ts +0 -4
- package/dist/version-check.js +0 -102
package/dist/cli.js
CHANGED
|
@@ -6,21 +6,13 @@ 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,
|
|
10
|
-
import { renderSkillSingleFile, writeSkillExport } from './skill-packaging.js';
|
|
11
|
-
import { displayNewVersionNoticeIfAvailable } from './version-check.js';
|
|
9
|
+
import { writeConfig, requireConfig, getConfigPath, getDefaultApiBaseUrl, getDefaultWsUrl, LOCAL_API_BASE_URL, LOCAL_WS_URL, API_BASE_URL_ENV_VAR, WS_URL_ENV_VAR, } from './cli-config.js';
|
|
12
10
|
// ── Program definition ──────────────────────────────────────────────
|
|
13
11
|
export const program = new Command();
|
|
14
12
|
program
|
|
15
13
|
.name('autokap')
|
|
16
14
|
.version(version)
|
|
17
15
|
.description('AI-powered screenshot capture — local Playwright proxy');
|
|
18
|
-
function getProjectPrimaryUrl(project) {
|
|
19
|
-
return (project.environments.local ??
|
|
20
|
-
project.environments.staging ??
|
|
21
|
-
project.environments.prod ??
|
|
22
|
-
null);
|
|
23
|
-
}
|
|
24
16
|
function fatal(message) {
|
|
25
17
|
logger.error(message);
|
|
26
18
|
process.exit(1);
|
|
@@ -53,33 +45,6 @@ async function requestJson(config, pathname, init, errorPrefix, searchParams) {
|
|
|
53
45
|
fatal(`${errorPrefix}: ${error.message}`);
|
|
54
46
|
}
|
|
55
47
|
}
|
|
56
|
-
async function requestText(config, pathname, init, errorPrefix, searchParams) {
|
|
57
|
-
try {
|
|
58
|
-
const response = await fetch(buildApiUrl(config, pathname, searchParams), init);
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
fatal(`${errorPrefix}: ${await readApiError(response)}`);
|
|
61
|
-
}
|
|
62
|
-
return await response.text();
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
fatal(`${errorPrefix}: ${error.message}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function ensureListFormat(format) {
|
|
69
|
-
if (format !== 'json' && format !== 'table') {
|
|
70
|
-
fatal('Invalid --format. Use "json" or "table".');
|
|
71
|
-
}
|
|
72
|
-
return format;
|
|
73
|
-
}
|
|
74
|
-
function ensureExportFormat(format) {
|
|
75
|
-
if (format !== 'json' && format !== 'csv') {
|
|
76
|
-
fatal('Invalid --format. Use "json" or "csv".');
|
|
77
|
-
}
|
|
78
|
-
return format;
|
|
79
|
-
}
|
|
80
|
-
function printJson(value) {
|
|
81
|
-
console.log(JSON.stringify(value, null, 2));
|
|
82
|
-
}
|
|
83
48
|
function displayPresetName(preset) {
|
|
84
49
|
const name = preset.name?.trim();
|
|
85
50
|
return name && name.length > 0 ? name : preset.id;
|
|
@@ -114,49 +79,10 @@ function cloudCaptureProgressCheckpoint(event) {
|
|
|
114
79
|
return null;
|
|
115
80
|
}
|
|
116
81
|
}
|
|
117
|
-
function buildEndpointAssetUrl(config, endpointId) {
|
|
118
|
-
return `${config.apiBaseUrl}/api/v1/assets/${endpointId}`;
|
|
119
|
-
}
|
|
120
|
-
function toEndpointOutputRow(config, endpoint) {
|
|
121
|
-
return {
|
|
122
|
-
...endpoint,
|
|
123
|
-
url: buildEndpointAssetUrl(config, endpoint.id),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
function toCsv(rows, columns) {
|
|
127
|
-
const escape = (value) => {
|
|
128
|
-
const text = value === null || value === undefined ? '' : String(value);
|
|
129
|
-
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
130
|
-
};
|
|
131
|
-
const header = columns.join(',');
|
|
132
|
-
const body = rows.map((row) => columns.map((column) => escape(row[column])).join(','));
|
|
133
|
-
return [header, ...body].join('\n');
|
|
134
|
-
}
|
|
135
|
-
async function loadProject(config, projectId) {
|
|
136
|
-
const data = await requestJson(config, `/api/v1/projects/${projectId}`, { headers: authHeaders(config) }, 'Failed to get project');
|
|
137
|
-
return data.project;
|
|
138
|
-
}
|
|
139
|
-
async function listProjectAccounts(config, projectId) {
|
|
140
|
-
const data = await requestJson(config, `/api/cli/projects/${projectId}/credentials`, { headers: authHeaders(config) }, 'Failed to list accounts');
|
|
141
|
-
return data.accounts;
|
|
142
|
-
}
|
|
143
|
-
async function resolveProjectAccount(config, projectId, accountNameOrId) {
|
|
144
|
-
const accounts = await listProjectAccounts(config, projectId);
|
|
145
|
-
const needle = accountNameOrId.toLowerCase();
|
|
146
|
-
const account = accounts.find((candidate) => candidate.id === accountNameOrId ||
|
|
147
|
-
candidate.name.toLowerCase() === needle);
|
|
148
|
-
if (!account) {
|
|
149
|
-
const available = accounts.map((candidate) => candidate.name).join(', ');
|
|
150
|
-
fatal(available
|
|
151
|
-
? `Account "${accountNameOrId}" not found. Available accounts: ${available}`
|
|
152
|
-
: `Account "${accountNameOrId}" not found. This project has no accounts.`);
|
|
153
|
-
}
|
|
154
|
-
return account;
|
|
155
|
-
}
|
|
156
82
|
// ── login command ───────────────────────────────────────────────────
|
|
157
83
|
program
|
|
158
84
|
.command('login <key>')
|
|
159
|
-
.description('Authenticate the AutoKap CLI with your
|
|
85
|
+
.description('Authenticate the AutoKap CLI with your API key')
|
|
160
86
|
.option('--api-base-url <url>', `API base URL (env: ${API_BASE_URL_ENV_VAR})`, getDefaultApiBaseUrl())
|
|
161
87
|
.option('--ws-url <url>', `WebSocket server URL (env: ${WS_URL_ENV_VAR})`)
|
|
162
88
|
.action(async (key, opts) => {
|
|
@@ -166,7 +92,7 @@ program
|
|
|
166
92
|
headers: { Authorization: `Bearer ${key}` },
|
|
167
93
|
});
|
|
168
94
|
if (!res.ok) {
|
|
169
|
-
logger.error('Invalid
|
|
95
|
+
logger.error('Invalid API key. Generate one in the AutoKap dashboard.');
|
|
170
96
|
process.exit(1);
|
|
171
97
|
}
|
|
172
98
|
}
|
|
@@ -182,64 +108,9 @@ program
|
|
|
182
108
|
logger.success(`Authenticated. Key stored in ${getConfigPath()}`);
|
|
183
109
|
process.exit(0);
|
|
184
110
|
});
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
.description('Verify CLI connection with the AutoKap server')
|
|
189
|
-
.action(async () => {
|
|
190
|
-
const config = await requireConfig();
|
|
191
|
-
try {
|
|
192
|
-
const res = await fetch(`${config.apiBaseUrl}/api/cli/validate`, {
|
|
193
|
-
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
194
|
-
});
|
|
195
|
-
if (!res.ok) {
|
|
196
|
-
logger.error('CLI key is no longer valid. Run `npx autokap@latest init --cli-key <key>` to re-authenticate.');
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
logger.error(`Cannot reach API: ${err.message}`);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
logger.success('Connection verified. Stored CLI key is active.');
|
|
205
|
-
process.exit(0);
|
|
206
|
-
});
|
|
207
|
-
// ── logout command ─────────────────────────────────────────────────
|
|
208
|
-
program
|
|
209
|
-
.command('logout')
|
|
210
|
-
.description('Remove stored credentials')
|
|
211
|
-
.action(async () => {
|
|
212
|
-
await deleteConfig();
|
|
213
|
-
logger.success(`Logged out. Credentials removed from ${getConfigPath()}`);
|
|
214
|
-
process.exit(0);
|
|
215
|
-
});
|
|
216
|
-
// ── whoami command ─────────────────────────────────────────────────
|
|
217
|
-
program
|
|
218
|
-
.command('whoami')
|
|
219
|
-
.description('Show the account linked to the current CLI key')
|
|
220
|
-
.action(async () => {
|
|
221
|
-
const config = await requireConfig();
|
|
222
|
-
try {
|
|
223
|
-
const res = await fetch(`${config.apiBaseUrl}/api/cli/validate`, {
|
|
224
|
-
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
225
|
-
});
|
|
226
|
-
if (!res.ok) {
|
|
227
|
-
logger.error('CLI key is no longer valid. Run `npx autokap@latest init --cli-key <key>` to re-authenticate.');
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
|
-
const data = await res.json();
|
|
231
|
-
const display = data.name
|
|
232
|
-
? `${data.name} (${data.email})`
|
|
233
|
-
: data.email ?? 'Unknown';
|
|
234
|
-
logger.info(`Logged in as: ${display}`);
|
|
235
|
-
logger.info(`Server: ${config.apiBaseUrl}`);
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
logger.error(`Cannot reach API: ${err.message}`);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
process.exit(0);
|
|
242
|
-
});
|
|
111
|
+
// Auth commands (whoami / logout / ping) live in @autokap/mcp; this binary keeps
|
|
112
|
+
// `login` only for CI / Cloud Run. Local dev can still inspect the config via
|
|
113
|
+
// `autokap doctor` or `cat ~/.autokap/config.json`.
|
|
243
114
|
async function runOutdatedPresetsLocally(opts) {
|
|
244
115
|
if (opts.output) {
|
|
245
116
|
fatal('`--output` is not supported with `--outdated`; run an individual preset when local copies are needed.');
|
|
@@ -356,8 +227,19 @@ program
|
|
|
356
227
|
// launched on Fly.io. When present, we POST a completion callback at exit
|
|
357
228
|
// so the `capture_runs` row flips out of `queued` — without this the
|
|
358
229
|
// backend rate-limiter blocks future cloud recaptures.
|
|
230
|
+
//
|
|
231
|
+
// Both `--cloud` AND `AUTOKAP_RUN_ID` are required to enable checkpoint
|
|
232
|
+
// posts. Earlier code keyed only on `AUTOKAP_RUN_ID`, so a stray env var
|
|
233
|
+
// (e.g. leaked from a parent shell that recently ran a cloud job) would
|
|
234
|
+
// silently start firing checkpoints during a plain local recapture. Worse,
|
|
235
|
+
// the row pointed to by that stale id might already be terminal, so the
|
|
236
|
+
// posts would 4xx and pollute the dashboard's error logs. Gate strictly.
|
|
359
237
|
const cloudRunId = process.env.AUTOKAP_RUN_ID;
|
|
360
|
-
|
|
238
|
+
if (cloudRunId && !opts.cloud) {
|
|
239
|
+
throw new Error('AUTOKAP_RUN_ID is set but --cloud was not passed. AUTOKAP_RUN_ID is reserved for ' +
|
|
240
|
+
'`auto-recapture --cloud` runs dispatched by the AutoKap cloud orchestrator. Unset the env var or add --cloud.');
|
|
241
|
+
}
|
|
242
|
+
const checkpointUrl = opts.cloud && cloudRunId
|
|
361
243
|
? buildApiUrl(config, `/api/cli/cloud-recapture/${cloudRunId}/checkpoint`)
|
|
362
244
|
: null;
|
|
363
245
|
if (opts.cloud && cloudRunId) {
|
|
@@ -608,1035 +490,15 @@ program
|
|
|
608
490
|
});
|
|
609
491
|
process.exit(0);
|
|
610
492
|
});
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
projectCmd
|
|
616
|
-
.command('list')
|
|
617
|
-
.description('List accessible projects')
|
|
618
|
-
.action(async () => {
|
|
619
|
-
const config = await requireConfig();
|
|
620
|
-
const data = await requestJson(config, '/api/v1/projects', { headers: authHeaders(config) }, 'Failed to list projects');
|
|
621
|
-
printJson(data.projects);
|
|
622
|
-
process.exit(0);
|
|
623
|
-
});
|
|
624
|
-
projectCmd
|
|
625
|
-
.command('get <project-id>')
|
|
626
|
-
.description('Get project details')
|
|
627
|
-
.action(async (projectId) => {
|
|
628
|
-
const config = await requireConfig();
|
|
629
|
-
const project = await loadProject(config, projectId);
|
|
630
|
-
printJson(project);
|
|
631
|
-
process.exit(0);
|
|
632
|
-
});
|
|
633
|
-
projectCmd
|
|
634
|
-
.command('create')
|
|
635
|
-
.description('Create a project')
|
|
636
|
-
.requiredOption('--name <name>', 'Project name')
|
|
637
|
-
.requiredOption('--base-url <url>', 'Base URL of the chosen environment')
|
|
638
|
-
.option('--environment <name>', "Environment slot to seed: 'local' or 'prod'", 'local')
|
|
639
|
-
.option('--description <text>', 'Project description')
|
|
640
|
-
.action(async (opts) => {
|
|
641
|
-
const config = await requireConfig();
|
|
642
|
-
const env = opts.environment.toLowerCase();
|
|
643
|
-
if (env !== 'local' && env !== 'prod') {
|
|
644
|
-
logger.error("--environment must be 'local' or 'prod'");
|
|
645
|
-
process.exit(1);
|
|
646
|
-
}
|
|
647
|
-
const data = await requestJson(config, '/api/v1/projects', {
|
|
648
|
-
method: 'POST',
|
|
649
|
-
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
650
|
-
body: JSON.stringify({
|
|
651
|
-
name: opts.name,
|
|
652
|
-
environment: env,
|
|
653
|
-
base_url: opts.baseUrl,
|
|
654
|
-
description: opts.description,
|
|
655
|
-
}),
|
|
656
|
-
}, 'Failed to create project');
|
|
657
|
-
console.log(data.project.id);
|
|
658
|
-
process.exit(0);
|
|
659
|
-
});
|
|
660
|
-
// ── capture commands ───────────────────────────────────────────────
|
|
661
|
-
const captureCmd = program
|
|
662
|
-
.command('capture')
|
|
663
|
-
.description('Query captured outputs');
|
|
664
|
-
captureCmd
|
|
665
|
-
.command('list')
|
|
666
|
-
.description('List captures')
|
|
667
|
-
.option('--project <id>', 'Filter by project ID')
|
|
668
|
-
.option('--preset <id>', 'Filter by preset ID')
|
|
669
|
-
.option('--limit <n>', 'Limit results', '50')
|
|
670
|
-
.option('--offset <n>', 'Offset results', '0')
|
|
671
|
-
.action(async (opts) => {
|
|
672
|
-
const config = await requireConfig();
|
|
673
|
-
const searchParams = new URLSearchParams();
|
|
674
|
-
if (opts.project)
|
|
675
|
-
searchParams.set('project_id', opts.project);
|
|
676
|
-
if (opts.preset)
|
|
677
|
-
searchParams.set('preset_id', opts.preset);
|
|
678
|
-
searchParams.set('limit', String(Math.max(1, Number.parseInt(opts.limit, 10) || 50)));
|
|
679
|
-
searchParams.set('offset', String(Math.max(0, Number.parseInt(opts.offset, 10) || 0)));
|
|
680
|
-
const data = await requestJson(config, '/api/v1/captures', { headers: authHeaders(config) }, 'Failed to list captures', searchParams);
|
|
681
|
-
printJson(data);
|
|
682
|
-
process.exit(0);
|
|
683
|
-
});
|
|
684
|
-
program
|
|
685
|
-
.command('usage')
|
|
686
|
-
.description('Show usage for the current billing period')
|
|
687
|
-
.action(async () => {
|
|
688
|
-
const config = await requireConfig();
|
|
689
|
-
const data = await requestJson(config, '/api/v1/usage', { headers: authHeaders(config) }, 'Failed to get usage');
|
|
690
|
-
printJson(data);
|
|
691
|
-
process.exit(0);
|
|
692
|
-
});
|
|
693
|
-
// ── preset commands ────────────────────────────────────────────────
|
|
694
|
-
async function readJsonInput(filePath) {
|
|
695
|
-
if (filePath === '-') {
|
|
696
|
-
const chunks = [];
|
|
697
|
-
for await (const chunk of process.stdin)
|
|
698
|
-
chunks.push(chunk);
|
|
699
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
700
|
-
}
|
|
701
|
-
const raw = await fs.readFile(filePath, 'utf8');
|
|
702
|
-
return JSON.parse(raw);
|
|
703
|
-
}
|
|
704
|
-
async function readSecretFromStdin(label) {
|
|
705
|
-
const chunks = [];
|
|
706
|
-
for await (const chunk of process.stdin)
|
|
707
|
-
chunks.push(chunk);
|
|
708
|
-
const value = Buffer.concat(chunks).toString('utf8').trim();
|
|
709
|
-
if (!value) {
|
|
710
|
-
fatal(`${label} is empty on stdin.`);
|
|
711
|
-
}
|
|
712
|
-
return value;
|
|
713
|
-
}
|
|
714
|
-
const presetCmd = program
|
|
715
|
-
.command('preset')
|
|
716
|
-
.description('Manage capture presets');
|
|
717
|
-
presetCmd
|
|
718
|
-
.command('create')
|
|
719
|
-
.description('Create a new preset from a JSON config file')
|
|
720
|
-
.requiredOption('--project <id>', 'Project ID')
|
|
721
|
-
.requiredOption('--name <name>', 'Preset name')
|
|
722
|
-
.option('--description <text>', 'Preset description', '')
|
|
723
|
-
.requiredOption('--config <file>', 'Path to config JSON file (use "-" for stdin)')
|
|
724
|
-
.action(async (opts) => {
|
|
725
|
-
const cfg = await requireConfig();
|
|
726
|
-
let configJson;
|
|
727
|
-
try {
|
|
728
|
-
configJson = await readJsonInput(opts.config);
|
|
729
|
-
}
|
|
730
|
-
catch (err) {
|
|
731
|
-
logger.error(`Failed to read config: ${err.message}`);
|
|
732
|
-
process.exit(1);
|
|
733
|
-
}
|
|
734
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets`, {
|
|
735
|
-
method: 'POST',
|
|
736
|
-
headers: {
|
|
737
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
738
|
-
'Content-Type': 'application/json',
|
|
739
|
-
},
|
|
740
|
-
body: JSON.stringify({
|
|
741
|
-
project_id: opts.project,
|
|
742
|
-
name: opts.name,
|
|
743
|
-
description: opts.description,
|
|
744
|
-
config: configJson,
|
|
745
|
-
}),
|
|
746
|
-
});
|
|
747
|
-
if (!res.ok) {
|
|
748
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
749
|
-
logger.error(`Failed to create preset: ${body.error || res.statusText}`);
|
|
750
|
-
process.exit(1);
|
|
751
|
-
}
|
|
752
|
-
const data = await res.json();
|
|
753
|
-
// Output only the preset ID so callers can chain: autokap run $(autokap preset create ...)
|
|
754
|
-
console.log(data.preset.id);
|
|
755
|
-
process.exit(0);
|
|
756
|
-
});
|
|
757
|
-
presetCmd
|
|
758
|
-
.command('update <preset-id>')
|
|
759
|
-
.description('Update an existing preset config')
|
|
760
|
-
.option('--name <name>', 'New preset name')
|
|
761
|
-
.option('--description <text>', 'New description')
|
|
762
|
-
.requiredOption('--config <file>', 'Path to config JSON file (use "-" for stdin)')
|
|
763
|
-
.action(async (presetId, opts) => {
|
|
764
|
-
const cfg = await requireConfig();
|
|
765
|
-
let configJson;
|
|
766
|
-
try {
|
|
767
|
-
configJson = await readJsonInput(opts.config);
|
|
768
|
-
}
|
|
769
|
-
catch (err) {
|
|
770
|
-
logger.error(`Failed to read config: ${err.message}`);
|
|
771
|
-
process.exit(1);
|
|
772
|
-
}
|
|
773
|
-
const payload = { config: configJson };
|
|
774
|
-
if (opts.name !== undefined)
|
|
775
|
-
payload.name = opts.name;
|
|
776
|
-
if (opts.description !== undefined)
|
|
777
|
-
payload.description = opts.description;
|
|
778
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
|
|
779
|
-
method: 'PATCH',
|
|
780
|
-
headers: {
|
|
781
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
782
|
-
'Content-Type': 'application/json',
|
|
783
|
-
},
|
|
784
|
-
body: JSON.stringify(payload),
|
|
785
|
-
});
|
|
786
|
-
if (!res.ok) {
|
|
787
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
788
|
-
logger.error(`Failed to update preset: ${body.error || res.statusText}`);
|
|
789
|
-
process.exit(1);
|
|
790
|
-
}
|
|
791
|
-
const data = await res.json();
|
|
792
|
-
console.log(data.preset.id);
|
|
793
|
-
process.exit(0);
|
|
794
|
-
});
|
|
795
|
-
presetCmd
|
|
796
|
-
.command('delete <preset-id>')
|
|
797
|
-
.description('Delete a preset (soft-delete)')
|
|
798
|
-
.action(async (presetId) => {
|
|
799
|
-
const cfg = await requireConfig();
|
|
800
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
|
|
801
|
-
method: 'DELETE',
|
|
802
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
803
|
-
});
|
|
804
|
-
if (!res.ok) {
|
|
805
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
806
|
-
logger.error(`Failed to delete preset: ${body.error || res.statusText}`);
|
|
807
|
-
process.exit(1);
|
|
808
|
-
}
|
|
809
|
-
logger.success(`Preset ${presetId} deleted`);
|
|
810
|
-
process.exit(0);
|
|
811
|
-
});
|
|
812
|
-
presetCmd
|
|
813
|
-
.command('get <preset-id>')
|
|
814
|
-
.description('Get preset details')
|
|
815
|
-
.action(async (presetId) => {
|
|
816
|
-
const cfg = await requireConfig();
|
|
817
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
|
|
818
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
819
|
-
});
|
|
820
|
-
if (!res.ok) {
|
|
821
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
822
|
-
logger.error(`Failed to get preset: ${body.error || res.statusText}`);
|
|
823
|
-
process.exit(1);
|
|
824
|
-
}
|
|
825
|
-
const data = await res.json();
|
|
826
|
-
console.log(JSON.stringify(data.preset, null, 2));
|
|
827
|
-
process.exit(0);
|
|
828
|
-
});
|
|
829
|
-
presetCmd
|
|
830
|
-
.command('list')
|
|
831
|
-
.description('List presets for a project')
|
|
832
|
-
.requiredOption('--project <id>', 'Project ID')
|
|
833
|
-
.action(async (opts) => {
|
|
834
|
-
const cfg = await requireConfig();
|
|
835
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/v1/presets?project_id=${encodeURIComponent(opts.project)}`, { headers: { Authorization: `Bearer ${cfg.apiKey}` } });
|
|
836
|
-
if (!res.ok) {
|
|
837
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
838
|
-
logger.error(`Failed to list presets: ${body.error || res.statusText}`);
|
|
839
|
-
process.exit(1);
|
|
840
|
-
}
|
|
841
|
-
const data = await res.json();
|
|
842
|
-
const rows = data.presets.map((p) => ({
|
|
843
|
-
id: p.id,
|
|
844
|
-
name: p.name,
|
|
845
|
-
description: p.description ?? '',
|
|
846
|
-
}));
|
|
847
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
848
|
-
process.exit(0);
|
|
849
|
-
});
|
|
850
|
-
presetCmd
|
|
851
|
-
.command('info <preset-id>')
|
|
852
|
-
.description('Get structured integration info for a preset (endpoints, variants, URL params)')
|
|
853
|
-
.action(async (presetId) => {
|
|
854
|
-
const cfg = await requireConfig();
|
|
855
|
-
// Fetch preset + endpoints in parallel
|
|
856
|
-
const [presetRes, endpointsRes] = await Promise.all([
|
|
857
|
-
fetch(`${cfg.apiBaseUrl}/api/v1/presets/${presetId}`, {
|
|
858
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
859
|
-
}),
|
|
860
|
-
fetch(`${cfg.apiBaseUrl}/api/v1/endpoints?preset_id=${encodeURIComponent(presetId)}`, {
|
|
861
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
862
|
-
}),
|
|
863
|
-
]);
|
|
864
|
-
if (!presetRes.ok) {
|
|
865
|
-
const body = await presetRes.json().catch(() => ({ error: presetRes.statusText }));
|
|
866
|
-
logger.error(`Failed to get preset: ${body.error || presetRes.statusText}`);
|
|
867
|
-
process.exit(1);
|
|
868
|
-
}
|
|
869
|
-
if (!endpointsRes.ok) {
|
|
870
|
-
const body = await endpointsRes.json().catch(() => ({ error: endpointsRes.statusText }));
|
|
871
|
-
logger.error(`Failed to list endpoints: ${body.error || endpointsRes.statusText}`);
|
|
872
|
-
process.exit(1);
|
|
873
|
-
}
|
|
874
|
-
const { preset } = await presetRes.json();
|
|
875
|
-
const { endpoints } = await endpointsRes.json();
|
|
876
|
-
// Extract variants from config
|
|
877
|
-
const config = preset.config ?? {};
|
|
878
|
-
const langs = Array.isArray(config.langs) ? config.langs : [];
|
|
879
|
-
const themes = Array.isArray(config.themes) ? config.themes : [];
|
|
880
|
-
const targets = Array.isArray(config.targets)
|
|
881
|
-
? config.targets
|
|
882
|
-
.map((t) => ({ id: t.id, label: t.label, viewport: t.viewport, deviceFrame: t.deviceFrame ?? null }))
|
|
883
|
-
: [];
|
|
884
|
-
const captureMode = config.captureMode ?? 'screenshot';
|
|
885
|
-
// Build per-endpoint info with type-specific URL params
|
|
886
|
-
const endpointEntries = endpoints.map((ep) => {
|
|
887
|
-
const assetType = ep.asset_type ?? 'screenshot';
|
|
888
|
-
const url = `${cfg.apiBaseUrl}/api/v1/assets/${ep.id}`;
|
|
889
|
-
let urlParams;
|
|
890
|
-
if (assetType === 'screenshot') {
|
|
891
|
-
urlParams = {
|
|
892
|
-
lang: 'Language variant',
|
|
893
|
-
theme: 'Color theme ("light" or "dark")',
|
|
894
|
-
target: 'Device target ID',
|
|
895
|
-
w: 'Max width in px (1-2560)',
|
|
896
|
-
quality: 'Image quality 1-100',
|
|
897
|
-
format: 'Output format: "webp", "png", "jpg"',
|
|
898
|
-
scale: 'Resolution multiplier (0.5-4)',
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
else if (assetType === 'clip') {
|
|
902
|
-
urlParams = {
|
|
903
|
-
lang: 'Language variant',
|
|
904
|
-
theme: 'Color theme ("light" or "dark")',
|
|
905
|
-
target: 'Device target ID',
|
|
906
|
-
format: 'Output format: "gif" (default), "mp4"',
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
910
|
-
urlParams = {};
|
|
911
|
-
}
|
|
912
|
-
return {
|
|
913
|
-
id: ep.id,
|
|
914
|
-
label: ep.label ?? null,
|
|
915
|
-
capture_name: ep.capture_name ?? null,
|
|
916
|
-
asset_type: assetType,
|
|
917
|
-
url,
|
|
918
|
-
url_params: urlParams,
|
|
919
|
-
};
|
|
920
|
-
});
|
|
921
|
-
const info = {
|
|
922
|
-
preset: {
|
|
923
|
-
id: preset.id,
|
|
924
|
-
name: preset.name,
|
|
925
|
-
description: preset.description ?? null,
|
|
926
|
-
capture_mode: captureMode,
|
|
927
|
-
},
|
|
928
|
-
variants: { langs, themes, targets },
|
|
929
|
-
endpoints: endpointEntries,
|
|
930
|
-
};
|
|
931
|
-
console.log(JSON.stringify(info, null, 2));
|
|
932
|
-
process.exit(0);
|
|
933
|
-
});
|
|
934
|
-
// ── video commands ─────────────────────────────────────────────────
|
|
935
|
-
//
|
|
936
|
-
// Mirrors the preset commands but targets `/api/video-projects` (CLI-keyed
|
|
937
|
-
// route used by the IDE skill flow). The payload file contains the full
|
|
938
|
-
// body expected by the API (projectId, title, user_script, legacy
|
|
939
|
-
// narration_voice/narration_locale aliases, narration_by_app_locale,
|
|
940
|
-
// app_locale/app_locales, app_theme/app_themes, cursor_theme,
|
|
941
|
-
// credentials_account_id, mockDataInjection, program). On `update`, the same payload shape is sent
|
|
942
|
-
// via PATCH. TTS is generated later by `autokap run`.
|
|
943
|
-
const videoCmd = program
|
|
944
|
-
.command('video')
|
|
945
|
-
.description('Manage demo videos');
|
|
946
|
-
videoCmd
|
|
947
|
-
.command('create')
|
|
948
|
-
.description('Create a new demo video from a JSON payload file')
|
|
949
|
-
.requiredOption('--payload <file>', 'Path to payload JSON file (use "-" for stdin)')
|
|
950
|
-
.action(async (opts) => {
|
|
951
|
-
const cfg = await requireConfig();
|
|
952
|
-
let payload;
|
|
953
|
-
try {
|
|
954
|
-
payload = await readJsonInput(opts.payload);
|
|
955
|
-
}
|
|
956
|
-
catch (err) {
|
|
957
|
-
logger.error(`Failed to read payload: ${err.message}`);
|
|
958
|
-
process.exit(1);
|
|
959
|
-
}
|
|
960
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/video-projects`, {
|
|
961
|
-
method: 'POST',
|
|
962
|
-
headers: {
|
|
963
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
964
|
-
'Content-Type': 'application/json',
|
|
965
|
-
},
|
|
966
|
-
body: JSON.stringify(payload),
|
|
967
|
-
});
|
|
968
|
-
if (!res.ok) {
|
|
969
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
970
|
-
logger.error(`Failed to create video: ${body.error || res.statusText}`);
|
|
971
|
-
process.exit(1);
|
|
972
|
-
}
|
|
973
|
-
const data = await res.json();
|
|
974
|
-
// Output only the video ID so callers can chain: autokap run $(autokap video create ...)
|
|
975
|
-
console.log(data.video.id);
|
|
976
|
-
process.exit(0);
|
|
977
|
-
});
|
|
978
|
-
videoCmd
|
|
979
|
-
.command('update <video-id>')
|
|
980
|
-
.description('Update an existing demo video from a JSON payload file')
|
|
981
|
-
.requiredOption('--payload <file>', 'Path to payload JSON file (use "-" for stdin)')
|
|
982
|
-
.action(async (videoId, opts) => {
|
|
983
|
-
const cfg = await requireConfig();
|
|
984
|
-
let payload;
|
|
985
|
-
try {
|
|
986
|
-
payload = await readJsonInput(opts.payload);
|
|
987
|
-
}
|
|
988
|
-
catch (err) {
|
|
989
|
-
logger.error(`Failed to read payload: ${err.message}`);
|
|
990
|
-
process.exit(1);
|
|
991
|
-
}
|
|
992
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/video-projects/${videoId}`, {
|
|
993
|
-
method: 'PATCH',
|
|
994
|
-
headers: {
|
|
995
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
996
|
-
'Content-Type': 'application/json',
|
|
997
|
-
},
|
|
998
|
-
body: JSON.stringify(payload),
|
|
999
|
-
});
|
|
1000
|
-
if (!res.ok) {
|
|
1001
|
-
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
1002
|
-
logger.error(`Failed to update video: ${body.error || res.statusText}`);
|
|
1003
|
-
process.exit(1);
|
|
1004
|
-
}
|
|
1005
|
-
const data = await res.json();
|
|
1006
|
-
console.log(data.video.id);
|
|
1007
|
-
process.exit(0);
|
|
1008
|
-
});
|
|
1009
|
-
// ── auth commands ──────────────────────────────────────────────────
|
|
1010
|
-
const authCmd = program
|
|
1011
|
-
.command('auth')
|
|
1012
|
-
.description('Manage authentication sessions for project credentials accounts');
|
|
1013
|
-
authCmd
|
|
1014
|
-
.command('capture <project-id>')
|
|
1015
|
-
.description('Open a browser, let you log in, and save the resulting session to a project credentials account')
|
|
1016
|
-
.option('--account <name-or-id>', 'Account name or ID to attach the session to. If omitted, lists accounts to choose from.')
|
|
1017
|
-
.option('--url <url>', 'URL to open in the browser. Defaults to the project URL.')
|
|
1018
|
-
.action(async (projectId, opts) => {
|
|
1019
|
-
const cfg = await requireConfig();
|
|
1020
|
-
const allAccounts = await listProjectAccounts(cfg, projectId);
|
|
1021
|
-
const accounts = allAccounts.filter((a) => a.type === 'session');
|
|
1022
|
-
if (!accounts.length) {
|
|
1023
|
-
if (allAccounts.length > 0) {
|
|
1024
|
-
logger.error(`No session-type accounts in project ${projectId}. This project has ${allAccounts.length} credentials account(s), but none are session accounts. Create a new account with type "session" in Settings → Login accounts.`);
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
logger.error(`No accounts found for project ${projectId}. Create a session account in Settings → Login accounts first.`);
|
|
1028
|
-
}
|
|
1029
|
-
process.exit(1);
|
|
1030
|
-
}
|
|
1031
|
-
let account;
|
|
1032
|
-
if (opts.account) {
|
|
1033
|
-
const needle = opts.account.toLowerCase();
|
|
1034
|
-
account = accounts.find((a) => a.id === opts.account || a.name.toLowerCase() === needle);
|
|
1035
|
-
if (!account) {
|
|
1036
|
-
// If the account exists but is a credentials account, give a precise error.
|
|
1037
|
-
const otherTypeMatch = allAccounts.find((a) => a.id === opts.account || a.name.toLowerCase() === needle);
|
|
1038
|
-
if (otherTypeMatch) {
|
|
1039
|
-
logger.error(`Account "${opts.account}" exists but is a credentials account (email/password). auth capture only works with session accounts.`);
|
|
1040
|
-
}
|
|
1041
|
-
else {
|
|
1042
|
-
logger.error(`Account "${opts.account}" not found in project ${projectId}.`);
|
|
1043
|
-
logger.info(`Available session accounts: ${accounts.map((a) => a.name).join(', ')}`);
|
|
1044
|
-
}
|
|
1045
|
-
process.exit(1);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
else if (accounts.length === 1) {
|
|
1049
|
-
account = accounts[0];
|
|
1050
|
-
logger.info(`Using the only session account in this project: "${account.name}"`);
|
|
1051
|
-
}
|
|
1052
|
-
else {
|
|
1053
|
-
logger.info('Multiple session accounts found. Re-run with --account <name>:');
|
|
1054
|
-
for (const a of accounts) {
|
|
1055
|
-
const status = a.storage_state_updated_at
|
|
1056
|
-
? `session updated ${a.storage_state_updated_at}`
|
|
1057
|
-
: 'no session yet';
|
|
1058
|
-
logger.info(` • ${a.name} (${status})`);
|
|
1059
|
-
}
|
|
1060
|
-
process.exit(0);
|
|
1061
|
-
}
|
|
1062
|
-
let startUrl = opts.url;
|
|
1063
|
-
if (!startUrl) {
|
|
1064
|
-
const project = await loadProject(cfg, projectId);
|
|
1065
|
-
startUrl = getProjectPrimaryUrl(project) ?? undefined;
|
|
1066
|
-
}
|
|
1067
|
-
if (!startUrl) {
|
|
1068
|
-
logger.error(`No environment URL configured for project ${projectId}. Configure local/staging/prod in the AutoKap web UI, or pass --url.`);
|
|
1069
|
-
process.exit(1);
|
|
1070
|
-
}
|
|
1071
|
-
logger.info(`Opening ${startUrl}`);
|
|
1072
|
-
const { captureAuthSession } = await import('./auth-capture.js');
|
|
1073
|
-
await captureAuthSession({
|
|
1074
|
-
apiBaseUrl: cfg.apiBaseUrl,
|
|
1075
|
-
apiKey: cfg.apiKey,
|
|
1076
|
-
projectId,
|
|
1077
|
-
accountId: account.id,
|
|
1078
|
-
startUrl,
|
|
1079
|
-
});
|
|
1080
|
-
});
|
|
1081
|
-
const authAccountCmd = authCmd
|
|
1082
|
-
.command('account')
|
|
1083
|
-
.description('Manage project credential accounts');
|
|
1084
|
-
authAccountCmd
|
|
1085
|
-
.command('list <project-id>')
|
|
1086
|
-
.description('List accounts for a project')
|
|
1087
|
-
.action(async (projectId) => {
|
|
1088
|
-
const config = await requireConfig();
|
|
1089
|
-
const accounts = await listProjectAccounts(config, projectId);
|
|
1090
|
-
printJson(accounts);
|
|
1091
|
-
process.exit(0);
|
|
1092
|
-
});
|
|
1093
|
-
authAccountCmd
|
|
1094
|
-
.command('create <project-id>')
|
|
1095
|
-
.description('Create a project credential account')
|
|
1096
|
-
.requiredOption('--name <name>', 'Account name')
|
|
1097
|
-
.option('--type <type>', 'Account type: credentials or session', 'credentials')
|
|
1098
|
-
.option('--email <email>', 'Email for credentials accounts')
|
|
1099
|
-
.option('--password-stdin', 'Read the password for credentials accounts from stdin', false)
|
|
1100
|
-
.action(async (projectId, opts) => {
|
|
1101
|
-
const type = opts.type === 'session' ? 'session' : opts.type === 'credentials' ? 'credentials' : null;
|
|
1102
|
-
if (!type) {
|
|
1103
|
-
fatal('Invalid --type. Use "credentials" or "session".');
|
|
1104
|
-
}
|
|
1105
|
-
const password = opts.passwordStdin
|
|
1106
|
-
? await readSecretFromStdin('Password')
|
|
1107
|
-
: undefined;
|
|
1108
|
-
if (type === 'session' && (opts.email || password)) {
|
|
1109
|
-
fatal('Session accounts do not accept --email or --password-stdin.');
|
|
1110
|
-
}
|
|
1111
|
-
const config = await requireConfig();
|
|
1112
|
-
const data = await requestJson(config, `/api/cli/projects/${projectId}/credentials`, {
|
|
1113
|
-
method: 'POST',
|
|
1114
|
-
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
1115
|
-
body: JSON.stringify({
|
|
1116
|
-
name: opts.name,
|
|
1117
|
-
type,
|
|
1118
|
-
email: opts.email,
|
|
1119
|
-
password,
|
|
1120
|
-
}),
|
|
1121
|
-
}, 'Failed to create account');
|
|
1122
|
-
console.log(data.account.id);
|
|
1123
|
-
process.exit(0);
|
|
1124
|
-
});
|
|
1125
|
-
authAccountCmd
|
|
1126
|
-
.command('update <project-id>')
|
|
1127
|
-
.description('Update a project credential account')
|
|
1128
|
-
.requiredOption('--account <name-or-id>', 'Account name or ID')
|
|
1129
|
-
.option('--name <name>', 'New account name')
|
|
1130
|
-
.option('--email <email>', 'Replace the email on a credentials account')
|
|
1131
|
-
.option('--password-stdin', 'Read the replacement password from stdin', false)
|
|
1132
|
-
.option('--clear-email', 'Remove the stored email from a credentials account', false)
|
|
1133
|
-
.option('--clear-password', 'Remove the stored password from a credentials account', false)
|
|
1134
|
-
.action(async (projectId, opts) => {
|
|
1135
|
-
if (opts.email && opts.clearEmail)
|
|
1136
|
-
fatal('Cannot use --email and --clear-email together.');
|
|
1137
|
-
if (opts.passwordStdin && opts.clearPassword)
|
|
1138
|
-
fatal('Cannot use --password-stdin and --clear-password together.');
|
|
1139
|
-
const password = opts.passwordStdin
|
|
1140
|
-
? await readSecretFromStdin('Password')
|
|
1141
|
-
: undefined;
|
|
1142
|
-
const config = await requireConfig();
|
|
1143
|
-
const account = await resolveProjectAccount(config, projectId, opts.account);
|
|
1144
|
-
const payload = {};
|
|
1145
|
-
if (opts.name !== undefined)
|
|
1146
|
-
payload.name = opts.name;
|
|
1147
|
-
if (opts.email !== undefined)
|
|
1148
|
-
payload.email = opts.email;
|
|
1149
|
-
if (password !== undefined)
|
|
1150
|
-
payload.password = password;
|
|
1151
|
-
if (opts.clearEmail)
|
|
1152
|
-
payload.email = null;
|
|
1153
|
-
if (opts.clearPassword)
|
|
1154
|
-
payload.password = null;
|
|
1155
|
-
if (Object.keys(payload).length === 0) {
|
|
1156
|
-
fatal('No changes provided. Pass at least one update option.');
|
|
1157
|
-
}
|
|
1158
|
-
const data = await requestJson(config, `/api/cli/projects/${projectId}/credentials/${account.id}`, {
|
|
1159
|
-
method: 'PATCH',
|
|
1160
|
-
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
1161
|
-
body: JSON.stringify(payload),
|
|
1162
|
-
}, 'Failed to update account');
|
|
1163
|
-
console.log(data.account.id);
|
|
1164
|
-
process.exit(0);
|
|
1165
|
-
});
|
|
1166
|
-
authAccountCmd
|
|
1167
|
-
.command('delete <project-id>')
|
|
1168
|
-
.description('Delete a project credential account')
|
|
1169
|
-
.requiredOption('--account <name-or-id>', 'Account name or ID')
|
|
1170
|
-
.action(async (projectId, opts) => {
|
|
1171
|
-
const config = await requireConfig();
|
|
1172
|
-
const account = await resolveProjectAccount(config, projectId, opts.account);
|
|
1173
|
-
await requestJson(config, `/api/cli/projects/${projectId}/credentials/${account.id}`, {
|
|
1174
|
-
method: 'DELETE',
|
|
1175
|
-
headers: authHeaders(config),
|
|
1176
|
-
}, 'Failed to delete account');
|
|
1177
|
-
logger.success(`Account ${account.name} deleted`);
|
|
1178
|
-
process.exit(0);
|
|
1179
|
-
});
|
|
1180
|
-
const authSessionCmd = authCmd
|
|
1181
|
-
.command('session')
|
|
1182
|
-
.description('Manage captured browser sessions');
|
|
1183
|
-
authSessionCmd
|
|
1184
|
-
.command('clear <project-id>')
|
|
1185
|
-
.description('Clear the stored browser session for a session account')
|
|
1186
|
-
.requiredOption('--account <name-or-id>', 'Account name or ID')
|
|
1187
|
-
.action(async (projectId, opts) => {
|
|
1188
|
-
const config = await requireConfig();
|
|
1189
|
-
const account = await resolveProjectAccount(config, projectId, opts.account);
|
|
1190
|
-
if (account.type !== 'session') {
|
|
1191
|
-
fatal(`Account "${account.name}" is a credentials account. Only session accounts can clear a stored browser session.`);
|
|
1192
|
-
}
|
|
1193
|
-
await requestJson(config, `/api/cli/projects/${projectId}/credentials/${account.id}/session`, {
|
|
1194
|
-
method: 'DELETE',
|
|
1195
|
-
headers: authHeaders(config),
|
|
1196
|
-
}, 'Failed to clear session');
|
|
1197
|
-
logger.success(`Cleared session for ${account.name}`);
|
|
1198
|
-
process.exit(0);
|
|
1199
|
-
});
|
|
1200
|
-
// ── endpoints commands ─────────────────────────────────────────────
|
|
1201
|
-
const endpointsCmd = program
|
|
1202
|
-
.command('endpoints')
|
|
1203
|
-
.description('Manage screenshot endpoints (dev links)');
|
|
1204
|
-
endpointsCmd
|
|
1205
|
-
.command('list')
|
|
1206
|
-
.description('List endpoints for a preset or project')
|
|
1207
|
-
.option('--preset <id>', 'Preset ID')
|
|
1208
|
-
.option('--project <id>', 'Project ID')
|
|
1209
|
-
.option('--format <fmt>', 'Output format: json or table', 'json')
|
|
1210
|
-
.action(async (opts) => {
|
|
1211
|
-
if ((opts.preset ? 1 : 0) + (opts.project ? 1 : 0) !== 1) {
|
|
1212
|
-
fatal('Pass exactly one of --preset or --project.');
|
|
1213
|
-
}
|
|
1214
|
-
const format = ensureListFormat(opts.format);
|
|
1215
|
-
const cfg = await requireConfig();
|
|
1216
|
-
const searchParams = new URLSearchParams();
|
|
1217
|
-
if (opts.preset)
|
|
1218
|
-
searchParams.set('preset_id', opts.preset);
|
|
1219
|
-
if (opts.project)
|
|
1220
|
-
searchParams.set('project_id', opts.project);
|
|
1221
|
-
const data = await requestJson(cfg, '/api/v1/endpoints', { headers: authHeaders(cfg) }, 'Failed to list endpoints', searchParams);
|
|
1222
|
-
const rows = data.endpoints.map((endpoint) => toEndpointOutputRow(cfg, endpoint));
|
|
1223
|
-
if (format === 'table') {
|
|
1224
|
-
console.table(rows.map((row) => ({
|
|
1225
|
-
id: row.id,
|
|
1226
|
-
capture_name: row.capture_name ?? '(default)',
|
|
1227
|
-
label: row.label,
|
|
1228
|
-
asset_type: row.asset_type ?? 'screenshot',
|
|
1229
|
-
url: row.url,
|
|
1230
|
-
})));
|
|
1231
|
-
}
|
|
1232
|
-
else {
|
|
1233
|
-
printJson(rows);
|
|
1234
|
-
}
|
|
1235
|
-
process.exit(0);
|
|
1236
|
-
});
|
|
1237
|
-
endpointsCmd
|
|
1238
|
-
.command('get <endpoint-id>')
|
|
1239
|
-
.description('Get endpoint details')
|
|
1240
|
-
.action(async (endpointId) => {
|
|
1241
|
-
const cfg = await requireConfig();
|
|
1242
|
-
const data = await requestJson(cfg, `/api/v1/endpoints/${endpointId}`, { headers: authHeaders(cfg) }, 'Failed to get endpoint');
|
|
1243
|
-
printJson(toEndpointOutputRow(cfg, data.endpoint));
|
|
1244
|
-
process.exit(0);
|
|
1245
|
-
});
|
|
1246
|
-
endpointsCmd
|
|
1247
|
-
.command('export')
|
|
1248
|
-
.description('Export endpoints as JSON or CSV')
|
|
1249
|
-
.option('--preset <id>', 'Preset ID')
|
|
1250
|
-
.option('--project <id>', 'Project ID')
|
|
1251
|
-
.option('--format <fmt>', 'Output format: json or csv', 'json')
|
|
1252
|
-
.action(async (opts) => {
|
|
1253
|
-
if ((opts.preset ? 1 : 0) + (opts.project ? 1 : 0) !== 1) {
|
|
1254
|
-
fatal('Pass exactly one of --preset or --project.');
|
|
1255
|
-
}
|
|
1256
|
-
const format = ensureExportFormat(opts.format);
|
|
1257
|
-
const cfg = await requireConfig();
|
|
1258
|
-
if (opts.preset) {
|
|
1259
|
-
const searchParams = new URLSearchParams({
|
|
1260
|
-
preset_id: opts.preset,
|
|
1261
|
-
format,
|
|
1262
|
-
});
|
|
1263
|
-
if (format === 'csv') {
|
|
1264
|
-
const csv = await requestText(cfg, '/api/v1/endpoints/export', { headers: authHeaders(cfg) }, 'Failed to export endpoints', searchParams);
|
|
1265
|
-
process.stdout.write(csv);
|
|
1266
|
-
}
|
|
1267
|
-
else {
|
|
1268
|
-
const data = await requestJson(cfg, '/api/v1/endpoints/export', { headers: authHeaders(cfg) }, 'Failed to export endpoints', searchParams);
|
|
1269
|
-
printJson(data);
|
|
1270
|
-
}
|
|
1271
|
-
process.exit(0);
|
|
1272
|
-
}
|
|
1273
|
-
const searchParams = new URLSearchParams({ project_id: opts.project });
|
|
1274
|
-
const data = await requestJson(cfg, '/api/v1/endpoints', { headers: authHeaders(cfg) }, 'Failed to list endpoints', searchParams);
|
|
1275
|
-
const rows = data.endpoints.map((endpoint) => toEndpointOutputRow(cfg, endpoint));
|
|
1276
|
-
if (format === 'csv') {
|
|
1277
|
-
process.stdout.write(`${toCsv(rows, ['id', 'label', 'url', 'capture_type', 'asset_type', 'capture_name'])}\n`);
|
|
1278
|
-
}
|
|
1279
|
-
else {
|
|
1280
|
-
printJson({ endpoints: rows });
|
|
1281
|
-
}
|
|
1282
|
-
process.exit(0);
|
|
1283
|
-
});
|
|
1284
|
-
// ── skill command ───────────────────────────────────────────────────
|
|
1285
|
-
// Agent output path mappings
|
|
1286
|
-
const AGENT_PATHS = {
|
|
1287
|
-
claude: '.claude/commands/autokap-preset.md',
|
|
1288
|
-
codex: '.agents/skills/autokap-preset/SKILL.md',
|
|
1289
|
-
cursor: '.cursor/rules/autokap-preset.md',
|
|
1290
|
-
windsurf: '.windsurf/rules/autokap-preset.md',
|
|
1291
|
-
copilot: '.github/instructions/autokap-preset.instructions.md',
|
|
1292
|
-
};
|
|
1293
|
-
program
|
|
1294
|
-
.command('skill')
|
|
1295
|
-
.description('Output or install an AutoKap skill for AI coding agents')
|
|
1296
|
-
.option('--output <path>', 'Write the generated skill output to this path instead of stdout')
|
|
1297
|
-
.option('--agent <name>', 'Target AI coding agent: claude, codex, cursor, windsurf, copilot (auto-resolves output path and packaging mode)')
|
|
1298
|
-
.option('--project-url <url>', 'Replace the project URL placeholder in the skill')
|
|
1299
|
-
.option('--project-id <id>', 'Replace the project ID placeholder in the skill')
|
|
1300
|
-
.option('--api-base-url <url>', 'Replace the API base URL placeholder (default: https://autokap.app)')
|
|
1301
|
-
.action(async (opts) => {
|
|
1302
|
-
const pathMap = AGENT_PATHS;
|
|
1303
|
-
// Resolve --agent to an output path
|
|
1304
|
-
if (opts.agent) {
|
|
1305
|
-
const agentKey = opts.agent.toLowerCase();
|
|
1306
|
-
if (!pathMap[agentKey]) {
|
|
1307
|
-
logger.error(`Unknown agent "${opts.agent}". Supported: ${Object.keys(pathMap).join(', ')}`);
|
|
1308
|
-
process.exit(1);
|
|
1309
|
-
}
|
|
1310
|
-
if (opts.output) {
|
|
1311
|
-
logger.error('Cannot use both --agent and --output. Pick one.');
|
|
1312
|
-
process.exit(1);
|
|
1313
|
-
}
|
|
1314
|
-
opts.output = pathMap[agentKey];
|
|
1315
|
-
}
|
|
1316
|
-
try {
|
|
1317
|
-
if (opts.output) {
|
|
1318
|
-
const result = await writeSkillExport({
|
|
1319
|
-
type: 'preset',
|
|
1320
|
-
agent: opts.agent?.toLowerCase(),
|
|
1321
|
-
outputPath: opts.output,
|
|
1322
|
-
placeholders: opts,
|
|
1323
|
-
});
|
|
1324
|
-
if (result.mode === 'bundle') {
|
|
1325
|
-
logger.success(`Skill bundle written to: ${path.dirname(opts.output)}`);
|
|
1326
|
-
}
|
|
1327
|
-
else {
|
|
1328
|
-
logger.success(`Skill file written to: ${opts.output}`);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
else {
|
|
1332
|
-
const content = await renderSkillSingleFile({
|
|
1333
|
-
type: 'preset',
|
|
1334
|
-
agent: opts.agent?.toLowerCase(),
|
|
1335
|
-
placeholders: opts,
|
|
1336
|
-
});
|
|
1337
|
-
process.stdout.write(content);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
catch (error) {
|
|
1341
|
-
logger.error(error.message);
|
|
1342
|
-
process.exit(1);
|
|
1343
|
-
}
|
|
1344
|
-
process.exit(0);
|
|
1345
|
-
});
|
|
1346
|
-
// ── init command ───────────────────────────────────────────────────
|
|
1347
|
-
program
|
|
1348
|
-
.command('init')
|
|
1349
|
-
.description('Set up AutoKap: authenticate and install the AI skill in one step')
|
|
1350
|
-
.option('--cli-key <key>', 'Your AutoKap CLI key')
|
|
1351
|
-
.option('--project-id <id>', 'Your project ID (optional, can be set later)')
|
|
1352
|
-
.option('--agent <name>', 'Target AI coding agent: claude, codex, cursor, windsurf, copilot (default: auto-detect)')
|
|
1353
|
-
.option('--api-base-url <url>', `API base URL (env: ${API_BASE_URL_ENV_VAR})`, getDefaultApiBaseUrl())
|
|
1354
|
-
.option('--ws-url <url>', `WebSocket server URL (env: ${WS_URL_ENV_VAR})`)
|
|
1355
|
-
.action(async (opts) => {
|
|
1356
|
-
const wsUrl = opts.wsUrl?.trim() || getDefaultWsUrl(opts.apiBaseUrl);
|
|
1357
|
-
// If --cli-key not provided, prompt interactively
|
|
1358
|
-
let cliKey = opts.cliKey;
|
|
1359
|
-
if (!cliKey) {
|
|
1360
|
-
if (process.stdin.isTTY === false) {
|
|
1361
|
-
logger.error(`CLI key is required in non-interactive shells. Pass --cli-key <key> or set ${API_KEY_ENV_VAR} for CI runs.`);
|
|
1362
|
-
process.exit(1);
|
|
1363
|
-
}
|
|
1364
|
-
const readline = await import('node:readline');
|
|
1365
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1366
|
-
cliKey = await new Promise((resolve) => {
|
|
1367
|
-
rl.question('Enter your AutoKap CLI key:', (answer) => {
|
|
1368
|
-
rl.close();
|
|
1369
|
-
resolve(answer.trim());
|
|
1370
|
-
});
|
|
1371
|
-
});
|
|
1372
|
-
if (!cliKey) {
|
|
1373
|
-
logger.error('CLI key is required. Get one at https://autokap.app/settings');
|
|
1374
|
-
process.exit(1);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
// Validate the key
|
|
1378
|
-
logger.info('Validating CLI key...');
|
|
1379
|
-
try {
|
|
1380
|
-
const res = await fetch(`${opts.apiBaseUrl}/api/cli/validate`, {
|
|
1381
|
-
headers: { Authorization: `Bearer ${cliKey}` },
|
|
1382
|
-
});
|
|
1383
|
-
if (!res.ok) {
|
|
1384
|
-
logger.error('Invalid CLI key. Generate one at https://autokap.app/settings');
|
|
1385
|
-
process.exit(1);
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
catch (err) {
|
|
1389
|
-
logger.error(`Cannot reach API: ${err.message}`);
|
|
1390
|
-
process.exit(1);
|
|
1391
|
-
}
|
|
1392
|
-
// Store credentials
|
|
1393
|
-
await writeConfig({
|
|
1394
|
-
apiKey: cliKey,
|
|
1395
|
-
apiBaseUrl: opts.apiBaseUrl,
|
|
1396
|
-
wsUrl,
|
|
1397
|
-
});
|
|
1398
|
-
logger.success(`Authenticated. Key stored in ${getConfigPath()}`);
|
|
1399
|
-
// Determine agent target
|
|
1400
|
-
let agentKey = opts.agent?.toLowerCase();
|
|
1401
|
-
if (!agentKey) {
|
|
1402
|
-
// Auto-detect from directory structure
|
|
1403
|
-
const detectors = [
|
|
1404
|
-
['.claude', 'claude'],
|
|
1405
|
-
['.agents', 'codex'],
|
|
1406
|
-
['.cursor', 'cursor'],
|
|
1407
|
-
['.windsurf', 'windsurf'],
|
|
1408
|
-
['.github/instructions', 'copilot'],
|
|
1409
|
-
];
|
|
1410
|
-
for (const [dir, key] of detectors) {
|
|
1411
|
-
try {
|
|
1412
|
-
await fs.access(dir);
|
|
1413
|
-
agentKey = key;
|
|
1414
|
-
break;
|
|
1415
|
-
}
|
|
1416
|
-
catch { /* try next */ }
|
|
1417
|
-
}
|
|
1418
|
-
if (!agentKey)
|
|
1419
|
-
agentKey = 'claude'; // Default
|
|
1420
|
-
logger.info(`Detected agent: ${agentKey}`);
|
|
1421
|
-
}
|
|
1422
|
-
if (!AGENT_PATHS[agentKey]) {
|
|
1423
|
-
logger.error(`Unknown agent "${agentKey}". Supported: ${Object.keys(AGENT_PATHS).join(', ')}`);
|
|
1424
|
-
process.exit(1);
|
|
1425
|
-
}
|
|
1426
|
-
// Install skill file
|
|
1427
|
-
const skillOutput = AGENT_PATHS[agentKey];
|
|
1428
|
-
try {
|
|
1429
|
-
const result = await writeSkillExport({
|
|
1430
|
-
type: 'preset',
|
|
1431
|
-
agent: agentKey,
|
|
1432
|
-
outputPath: skillOutput,
|
|
1433
|
-
placeholders: {
|
|
1434
|
-
projectId: opts.projectId,
|
|
1435
|
-
apiBaseUrl: opts.apiBaseUrl !== DEFAULT_API_BASE_URL ? opts.apiBaseUrl : undefined,
|
|
1436
|
-
},
|
|
1437
|
-
});
|
|
1438
|
-
if (result.mode === 'bundle') {
|
|
1439
|
-
logger.success(`Skill bundle installed to: ${path.dirname(skillOutput)}`);
|
|
1440
|
-
}
|
|
1441
|
-
else {
|
|
1442
|
-
logger.success(`Skill installed to: ${skillOutput}`);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
catch (error) {
|
|
1446
|
-
logger.error(error.message);
|
|
1447
|
-
process.exit(1);
|
|
1448
|
-
}
|
|
1449
|
-
console.log('');
|
|
1450
|
-
console.log('Setup complete! You can now:');
|
|
1451
|
-
console.log(` - Ask your AI agent to create presets using the autokap-preset skill`);
|
|
1452
|
-
console.log(` - Run captures locally: autokap run <preset-id>`);
|
|
1453
|
-
process.exit(0);
|
|
1454
|
-
});
|
|
1455
|
-
// ── proxy command ──────────────────────────────────────────────────
|
|
1456
|
-
const proxyCmd = program
|
|
1457
|
-
.command('proxy')
|
|
1458
|
-
.description('Manage capture proxy for edge caching');
|
|
1459
|
-
proxyCmd
|
|
1460
|
-
.command('register')
|
|
1461
|
-
.description('Register a user-side proxy for faster capture loading')
|
|
1462
|
-
.requiredOption('--project <id>', 'Project ID')
|
|
1463
|
-
.requiredOption('--proxy-url <url>', 'Proxy base URL (e.g. https://myapp.com/api/autokap/assets)')
|
|
1464
|
-
.option('--webhook-url <url>', 'Webhook URL for cache invalidation (e.g. https://myapp.com/api/autokap/webhook)')
|
|
1465
|
-
.option('--signing-secret <secret>', 'Webhook signing secret (auto-generated if omitted)')
|
|
1466
|
-
.action(async (opts) => {
|
|
1467
|
-
const cfg = await requireConfig();
|
|
1468
|
-
const body = {
|
|
1469
|
-
proxy_base_url: opts.proxyUrl,
|
|
1470
|
-
};
|
|
1471
|
-
if (opts.webhookUrl)
|
|
1472
|
-
body.webhook_url = opts.webhookUrl;
|
|
1473
|
-
if (opts.signingSecret)
|
|
1474
|
-
body.signing_secret = opts.signingSecret;
|
|
1475
|
-
const res = await fetch(`${cfg.apiBaseUrl}/api/v1/projects/${opts.project}/proxy`, {
|
|
1476
|
-
method: 'PATCH',
|
|
1477
|
-
headers: {
|
|
1478
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
1479
|
-
'Content-Type': 'application/json',
|
|
1480
|
-
},
|
|
1481
|
-
body: JSON.stringify(body),
|
|
1482
|
-
});
|
|
1483
|
-
if (!res.ok) {
|
|
1484
|
-
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
1485
|
-
logger.error(`Failed to register proxy: ${data.error || res.statusText}`);
|
|
1486
|
-
process.exit(1);
|
|
1487
|
-
}
|
|
1488
|
-
const result = await res.json();
|
|
1489
|
-
logger.info(`Proxy registered: ${result.proxy_base_url}`);
|
|
1490
|
-
if (result.webhook_url) {
|
|
1491
|
-
logger.info(`Webhook configured: ${result.webhook_url}`);
|
|
1492
|
-
}
|
|
1493
|
-
if (result.signing_secret) {
|
|
1494
|
-
console.log('');
|
|
1495
|
-
console.log('Add this to your environment variables:');
|
|
1496
|
-
console.log(` AUTOKAP_WEBHOOK_SECRET=${result.signing_secret}`);
|
|
1497
|
-
console.log('');
|
|
1498
|
-
}
|
|
1499
|
-
process.exit(0);
|
|
1500
|
-
});
|
|
1501
|
-
proxyCmd
|
|
1502
|
-
.command('verify')
|
|
1503
|
-
.description('Verify the configured proxy for a project')
|
|
1504
|
-
.requiredOption('--project <id>', 'Project ID')
|
|
1505
|
-
.action(async (opts) => {
|
|
1506
|
-
const config = await requireConfig();
|
|
1507
|
-
const project = await loadProject(config, opts.project);
|
|
1508
|
-
if (!project.proxy_base_url) {
|
|
1509
|
-
fatal(`Project ${opts.project} has no proxy configured. Run "autokap proxy register" first.`);
|
|
1510
|
-
}
|
|
1511
|
-
const result = await requestJson(config, '/api/v1/proxy/verify', {
|
|
1512
|
-
method: 'POST',
|
|
1513
|
-
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
1514
|
-
body: JSON.stringify({
|
|
1515
|
-
project_id: opts.project,
|
|
1516
|
-
proxy_base_url: project.proxy_base_url,
|
|
1517
|
-
}),
|
|
1518
|
-
}, 'Failed to verify proxy');
|
|
1519
|
-
printJson(result);
|
|
1520
|
-
if (!result.success) {
|
|
1521
|
-
process.exit(1);
|
|
1522
|
-
}
|
|
1523
|
-
process.exit(0);
|
|
1524
|
-
});
|
|
1525
|
-
// ── branding command ───────────────────────────────────────────────
|
|
1526
|
-
const brandingCmd = program
|
|
1527
|
-
.command('branding')
|
|
1528
|
-
.description('Manage project branding and design system');
|
|
1529
|
-
brandingCmd
|
|
1530
|
-
.command('import <file-path>')
|
|
1531
|
-
.description('Import a design.md file as the project design system')
|
|
1532
|
-
.requiredOption('--project <id>', 'Project ID')
|
|
1533
|
-
.option('--dry', 'Validate and extract tokens without persisting', false)
|
|
1534
|
-
.option('--json', 'JSON output for IDE assistants', false)
|
|
1535
|
-
.option('--force-light-only', 'Skip dark-mode AI derivation', false)
|
|
1536
|
-
.action(async (filePath, opts) => {
|
|
1537
|
-
const config = await requireConfig();
|
|
1538
|
-
const resolved = path.resolve(process.cwd(), filePath);
|
|
1539
|
-
let content;
|
|
1540
|
-
try {
|
|
1541
|
-
content = await fs.readFile(resolved, 'utf8');
|
|
1542
|
-
}
|
|
1543
|
-
catch (e) {
|
|
1544
|
-
fatal(`Cannot read file at ${resolved}: ${e.message}`);
|
|
1545
|
-
}
|
|
1546
|
-
if (content.length > 200_000) {
|
|
1547
|
-
fatal(`File too large (${content.length} bytes, max 200000).`);
|
|
1548
|
-
}
|
|
1549
|
-
if (content.trim().length === 0) {
|
|
1550
|
-
fatal(`File at ${resolved} is empty.`);
|
|
1551
|
-
}
|
|
1552
|
-
const result = await requestJson(config, `/api/cli/projects/${opts.project}/design-system/import`, {
|
|
1553
|
-
method: 'POST',
|
|
1554
|
-
headers: authHeaders(config, { 'Content-Type': 'application/json' }),
|
|
1555
|
-
body: JSON.stringify({
|
|
1556
|
-
source_markdown: content,
|
|
1557
|
-
source_path: filePath,
|
|
1558
|
-
options: {
|
|
1559
|
-
dry_run: opts.dry,
|
|
1560
|
-
force_light_only: opts.forceLightOnly,
|
|
1561
|
-
},
|
|
1562
|
-
}),
|
|
1563
|
-
}, 'Failed to import design.md');
|
|
1564
|
-
if (opts.json) {
|
|
1565
|
-
printJson(result);
|
|
1566
|
-
process.exit(0);
|
|
1567
|
-
}
|
|
1568
|
-
const ds = result.design_system;
|
|
1569
|
-
if (ds?.meta?.name) {
|
|
1570
|
-
logger.info(`Design system "${ds.meta.name}" imported for project ${opts.project}.`);
|
|
1571
|
-
}
|
|
1572
|
-
else {
|
|
1573
|
-
logger.info(`Design system imported for project ${opts.project}.`);
|
|
1574
|
-
}
|
|
1575
|
-
const lightColors = ds?.light?.colors ?? {};
|
|
1576
|
-
const colorKeys = Object.keys(lightColors);
|
|
1577
|
-
if (colorKeys.length > 0) {
|
|
1578
|
-
logger.info(` Colors: ${colorKeys.length} (${colorKeys.slice(0, 5).join(', ')}${colorKeys.length > 5 ? '…' : ''})`);
|
|
1579
|
-
}
|
|
1580
|
-
const titleFont = ds?.light?.typography?.title?.fontFamily;
|
|
1581
|
-
if (titleFont) {
|
|
1582
|
-
logger.info(` Title font: ${titleFont}`);
|
|
1583
|
-
}
|
|
1584
|
-
if (ds?.dark?.colors) {
|
|
1585
|
-
logger.info(` Dark mode: derived (${Object.keys(ds.dark.colors).length} colors)`);
|
|
1586
|
-
}
|
|
1587
|
-
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
1588
|
-
logger.info(` Warnings:`);
|
|
1589
|
-
for (const w of result.warnings) {
|
|
1590
|
-
logger.info(` - ${w}`);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
if (opts.dry) {
|
|
1594
|
-
logger.info(` (dry-run: nothing persisted)`);
|
|
1595
|
-
}
|
|
1596
|
-
process.exit(0);
|
|
1597
|
-
});
|
|
1598
|
-
brandingCmd
|
|
1599
|
-
.command('export')
|
|
1600
|
-
.description('Export the project design system as design.md (or JSON)')
|
|
1601
|
-
.requiredOption('--project <id>', 'Project ID')
|
|
1602
|
-
.option('--out <path>', 'Write to file instead of stdout')
|
|
1603
|
-
.option('--format <fmt>', 'Output format: md or json', 'md')
|
|
1604
|
-
.action(async (opts) => {
|
|
1605
|
-
if (opts.format !== 'md' && opts.format !== 'json') {
|
|
1606
|
-
fatal('Invalid --format. Use "md" or "json".');
|
|
1607
|
-
}
|
|
1608
|
-
const config = await requireConfig();
|
|
1609
|
-
const searchParams = opts.format === 'json' ? new URLSearchParams({ format: 'json' }) : undefined;
|
|
1610
|
-
if (opts.format === 'json') {
|
|
1611
|
-
const payload = await requestJson(config, `/api/cli/projects/${opts.project}/design-system`, { headers: authHeaders(config) }, 'Failed to export design system', searchParams);
|
|
1612
|
-
const body = JSON.stringify(payload, null, 2);
|
|
1613
|
-
if (opts.out) {
|
|
1614
|
-
await fs.writeFile(path.resolve(process.cwd(), opts.out), body, 'utf8');
|
|
1615
|
-
logger.info(`Wrote design system JSON to ${opts.out}`);
|
|
1616
|
-
}
|
|
1617
|
-
else {
|
|
1618
|
-
process.stdout.write(`${body}\n`);
|
|
1619
|
-
}
|
|
1620
|
-
process.exit(0);
|
|
1621
|
-
}
|
|
1622
|
-
const result = await requestJson(config, `/api/cli/projects/${opts.project}/design-system`, { headers: authHeaders(config) }, 'Failed to export design system');
|
|
1623
|
-
if (opts.out) {
|
|
1624
|
-
await fs.writeFile(path.resolve(process.cwd(), opts.out), result.content, 'utf8');
|
|
1625
|
-
logger.info(`Wrote design.md to ${opts.out}`);
|
|
1626
|
-
}
|
|
1627
|
-
else {
|
|
1628
|
-
process.stdout.write(result.content);
|
|
1629
|
-
if (!result.content.endsWith('\n'))
|
|
1630
|
-
process.stdout.write('\n');
|
|
1631
|
-
}
|
|
1632
|
-
process.exit(0);
|
|
1633
|
-
});
|
|
493
|
+
// Project, preset, capture, usage, video, auth, endpoints, skill, init, proxy,
|
|
494
|
+
// and branding workflows live in @autokap/mcp. This binary exposes only the
|
|
495
|
+
// commands needed by CI and Cloud Run (login / run / auto-recapture / doctor).
|
|
496
|
+
// See MIGRATION-v1-to-v2.md for the CLI→MCP tool mapping (kept for older users).
|
|
1634
497
|
// ── doctor command ──────────────────────────────────────────────────
|
|
1635
498
|
program
|
|
1636
499
|
.command('doctor')
|
|
1637
|
-
.description('Check environment and dependencies (Node, Chromium, ffmpeg, config,
|
|
1638
|
-
.option('--fix', 'Attempt to auto-fix detected issues (Chromium install
|
|
1639
|
-
.option('--agent <name>', 'Override agent for skill check: claude, codex, cursor, windsurf, copilot')
|
|
500
|
+
.description('Check environment and dependencies (Node, Chromium, ffmpeg, config, API key)')
|
|
501
|
+
.option('--fix', 'Attempt to auto-fix detected issues (Chromium install)', false)
|
|
1640
502
|
.option('--json', 'Output a structured JSON report to stdout (for AI assistants and tooling)', false)
|
|
1641
503
|
.action(async (opts) => {
|
|
1642
504
|
const { runDoctor } = await import('./cli-doctor.js');
|
|
@@ -1649,7 +511,6 @@ const isDirectExecution = resolvedArgv && await resolvedArgv.then(p => {
|
|
|
1649
511
|
return base === 'cli.js' || base === 'cli.ts';
|
|
1650
512
|
});
|
|
1651
513
|
if (isDirectExecution) {
|
|
1652
|
-
await displayNewVersionNoticeIfAvailable(version);
|
|
1653
514
|
program.parseAsync().catch(async (err) => {
|
|
1654
515
|
logger.error(err.message);
|
|
1655
516
|
process.exit(1);
|