autokap 1.6.2 → 1.6.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/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, 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
- 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 CLI key')
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 CLI key. Generate one in the AutoKap dashboard.');
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
- // ── ping command ───────────────────────────────────────────────────
186
- program
187
- .command('ping')
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
- const checkpointUrl = cloudRunId
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
- // ── project commands ───────────────────────────────────────────────
612
- const projectCmd = program
613
- .command('project')
614
- .description('Manage projects');
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, skill, version)')
1638
- .option('--fix', 'Attempt to auto-fix detected issues (Chromium install, skill reinstall)', false)
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);