@analyticscli/growth-engineer 0.1.0-preview.1 → 0.1.0-preview.10
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/config.js +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +30 -6
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +83 -33
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +92 -18
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +15 -3
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +730 -661
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/config.example.json +20 -20
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { createInterface } from 'node:readline/promises';
|
|
7
7
|
import { emitKeypressEvents } from 'node:readline';
|
|
8
8
|
import { createPrivateKey } from 'node:crypto';
|
|
9
|
-
import {
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { buildExtraSourceConfig, getDefaultSourceCommand, } from './openclaw-growth-shared.mjs';
|
|
10
11
|
import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
11
12
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
12
13
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
13
14
|
const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
|
|
15
|
+
const DEFAULT_GROWTH_INTERVAL_MINUTES = 1440;
|
|
16
|
+
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
17
|
+
const GROWTH_ENGINEER_PACKAGE_SPEC = process.env.OPENCLAW_GROWTH_ENGINEER_PACKAGE || '@analyticscli/growth-engineer@preview';
|
|
18
|
+
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
19
|
const CONNECTOR_KEYS = ['analytics', 'github', 'revenuecat', 'sentry', 'asc'];
|
|
20
|
+
class WizardAbortError extends Error {
|
|
21
|
+
exitCode;
|
|
22
|
+
constructor(message, exitCode = 130) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'WizardAbortError';
|
|
25
|
+
this.exitCode = exitCode;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
const CONNECTOR_DEFINITIONS = [
|
|
16
29
|
{
|
|
17
30
|
key: 'analytics',
|
|
@@ -47,33 +60,33 @@ const CONNECTOR_DEFINITIONS = [
|
|
|
47
60
|
const DEFAULT_CADENCE_PLAN = [
|
|
48
61
|
{
|
|
49
62
|
key: 'daily',
|
|
50
|
-
title: 'Daily production guardrail',
|
|
63
|
+
title: 'Daily Sentry and production guardrail',
|
|
51
64
|
intervalDays: 1,
|
|
52
65
|
criticalOnly: true,
|
|
53
|
-
focusAreas: ['crash', 'conversion', 'paywall'],
|
|
54
|
-
sourcePriorities: ['sentry', 'glitchtip', 'analytics', 'asc_cli', '
|
|
55
|
-
objective: '
|
|
56
|
-
instructions: '
|
|
66
|
+
focusAreas: ['sentry_errors', 'crash', 'onboarding', 'conversion', 'paywall', 'purchase'],
|
|
67
|
+
sourcePriorities: ['sentry', 'glitchtip', 'analytics', 'revenuecat', 'asc_cli', 'feedback', 'github'],
|
|
68
|
+
objective: 'Analyze every configured project for critical production blockers: Sentry/GlitchTip errors, crashes, onboarding or purchase drop-offs, zero-conversion days, missing buyers, very low users, and other silent business anomalies.',
|
|
69
|
+
instructions: 'Compare against recent baselines across connected sources and code changes. If the finding is critical, produce the exact fix or next debugging step and prefer a GitHub issue or draft PR when GitHub write access is configured; otherwise hand off via OpenClaw chat. Avoid generic growth ideas.',
|
|
57
70
|
},
|
|
58
71
|
{
|
|
59
72
|
key: 'weekly',
|
|
60
|
-
title: 'Weekly
|
|
73
|
+
title: 'Weekly executive product and growth summary',
|
|
61
74
|
intervalDays: 7,
|
|
62
75
|
criticalOnly: false,
|
|
63
|
-
focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention'],
|
|
64
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
|
|
65
|
-
objective: '
|
|
66
|
-
instructions: 'Pick one to three high-confidence
|
|
76
|
+
focusAreas: ['conversion', 'paywall', 'onboarding', 'marketing', 'retention', 'stability'],
|
|
77
|
+
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
|
|
78
|
+
objective: 'Create an executive summary across all configured projects, connectors, recent releases, code changes, revenue, activation, retention, reviews, and production stability.',
|
|
79
|
+
instructions: 'Pick one to three high-confidence improvements with evidence, expected KPI movement, likely code/store surfaces, owner-ready next steps, and a verification plan. Create GitHub issues or draft PR proposals only when the evidence is specific enough.',
|
|
67
80
|
},
|
|
68
81
|
{
|
|
69
82
|
key: 'monthly',
|
|
70
|
-
title: 'Monthly business and
|
|
83
|
+
title: 'Monthly deep product, business, and code review',
|
|
71
84
|
intervalDays: 30,
|
|
72
85
|
criticalOnly: false,
|
|
73
|
-
focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding'],
|
|
74
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
|
|
75
|
-
objective: 'Compare MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage,
|
|
76
|
-
instructions: 'Decide what should be built, changed, or
|
|
86
|
+
focusAreas: ['conversion', 'paywall', 'retention', 'marketing', 'onboarding', 'codebase'],
|
|
87
|
+
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry', 'github'],
|
|
88
|
+
objective: 'Compare all configured projects month-over-month: MRR, trial conversion, churn, acquisition quality, store conversion, retention, review themes, feature usage, crash totals, and codebase changes.',
|
|
89
|
+
instructions: 'Decide what should be built, changed, deleted, or instrumented next. Tie conclusions to connector data plus codebase evidence and explain why each recommendation should move revenue, activation, retention, stability, or acquisition quality.',
|
|
77
90
|
},
|
|
78
91
|
{
|
|
79
92
|
key: 'quarterly',
|
|
@@ -81,8 +94,8 @@ const DEFAULT_CADENCE_PLAN = [
|
|
|
81
94
|
intervalDays: 91,
|
|
82
95
|
criticalOnly: false,
|
|
83
96
|
focusAreas: ['marketing', 'paywall', 'retention', 'conversion', 'onboarding'],
|
|
84
|
-
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback'],
|
|
85
|
-
objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, and major funnel bets.',
|
|
97
|
+
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'github', 'sentry'],
|
|
98
|
+
objective: 'Revisit positioning, pricing/packaging, onboarding architecture, roadmap assumptions, tracking quality, codebase constraints, and major funnel bets across every configured project.',
|
|
86
99
|
instructions: 'Find structural constraints and durable opportunities. Tie recommendations to cohort behavior, monetization, reviews, channel quality, and shipped changes.',
|
|
87
100
|
},
|
|
88
101
|
{
|
|
@@ -92,7 +105,7 @@ const DEFAULT_CADENCE_PLAN = [
|
|
|
92
105
|
criticalOnly: false,
|
|
93
106
|
focusAreas: ['retention', 'conversion', 'paywall', 'marketing', 'general'],
|
|
94
107
|
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
|
|
95
|
-
objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether strategy still matches the best users.',
|
|
108
|
+
objective: 'Audit connector coverage, SDK instrumentation, event taxonomy, data reliability, memory, growth loops, and whether product/code strategy still matches the best users across configured projects.',
|
|
96
109
|
instructions: 'Prioritize measurement fixes and system changes that make future analysis more trustworthy. Identify stale events, missing attribution, weak identity, and misleading dashboards.',
|
|
97
110
|
},
|
|
98
111
|
{
|
|
@@ -102,7 +115,7 @@ const DEFAULT_CADENCE_PLAN = [
|
|
|
102
115
|
criticalOnly: false,
|
|
103
116
|
focusAreas: ['marketing', 'retention', 'paywall', 'conversion', 'general'],
|
|
104
117
|
sourcePriorities: ['analytics', 'revenuecat', 'asc_cli', 'feedback', 'sentry'],
|
|
105
|
-
objective: 'Reset strategy from evidence: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
|
|
118
|
+
objective: 'Reset strategy from evidence across every configured project: market/channel fit, monetization model, retention ceiling, product scope, and whether to double down, reposition, rebuild, or sunset major surfaces/features.',
|
|
106
119
|
instructions: 'Use the full year of memory, releases, revenue, acquisition, reviews, code changes, and cohort behavior. Produce strategic experiments and stop-doing decisions.',
|
|
107
120
|
},
|
|
108
121
|
];
|
|
@@ -210,6 +223,91 @@ function quote(value) {
|
|
|
210
223
|
}
|
|
211
224
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
212
225
|
}
|
|
226
|
+
function resolveRuntimeScriptPath(scriptName) {
|
|
227
|
+
const candidates = [
|
|
228
|
+
path.join(RUNTIME_DIR, scriptName),
|
|
229
|
+
path.resolve('scripts', scriptName),
|
|
230
|
+
path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
|
|
231
|
+
];
|
|
232
|
+
return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
|
|
233
|
+
}
|
|
234
|
+
function nodeRuntimeScriptCommand(scriptName) {
|
|
235
|
+
return `node ${quote(resolveRuntimeScriptPath(scriptName))}`;
|
|
236
|
+
}
|
|
237
|
+
function growthEngineerPackageCommand(args) {
|
|
238
|
+
return `npx -y ${quote(GROWTH_ENGINEER_PACKAGE_SPEC)} ${args}`;
|
|
239
|
+
}
|
|
240
|
+
function getWizardDefaultSourceCommand(sourceName) {
|
|
241
|
+
const normalized = String(sourceName || '').trim().toLowerCase();
|
|
242
|
+
if (normalized === 'analytics' || normalized === 'analyticscli') {
|
|
243
|
+
return nodeRuntimeScriptCommand('export-analytics-summary.mjs');
|
|
244
|
+
}
|
|
245
|
+
if (normalized === 'revenuecat' || normalized === 'revenue-cat') {
|
|
246
|
+
return nodeRuntimeScriptCommand('export-revenuecat-summary.mjs');
|
|
247
|
+
}
|
|
248
|
+
if (normalized === 'sentry' || normalized === 'glitchtip') {
|
|
249
|
+
return nodeRuntimeScriptCommand('export-sentry-summary.mjs');
|
|
250
|
+
}
|
|
251
|
+
if (normalized === 'feedback') {
|
|
252
|
+
return getDefaultSourceCommand('feedback');
|
|
253
|
+
}
|
|
254
|
+
if (['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(normalized)) {
|
|
255
|
+
return nodeRuntimeScriptCommand('export-asc-summary.mjs');
|
|
256
|
+
}
|
|
257
|
+
return getDefaultSourceCommand(sourceName);
|
|
258
|
+
}
|
|
259
|
+
function replaceLegacyRuntimeScriptCommand(command) {
|
|
260
|
+
const trimmed = String(command || '').trim();
|
|
261
|
+
if (!trimmed)
|
|
262
|
+
return trimmed;
|
|
263
|
+
return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-preflight\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
264
|
+
}
|
|
265
|
+
function normalizeWizardSourceCommand(sourceName, source) {
|
|
266
|
+
const current = replaceLegacyRuntimeScriptCommand(source?.command || '');
|
|
267
|
+
return current || getWizardDefaultSourceCommand(sourceName);
|
|
268
|
+
}
|
|
269
|
+
function migrateRuntimeSourceCommands(config) {
|
|
270
|
+
if (!config || typeof config !== 'object')
|
|
271
|
+
return config;
|
|
272
|
+
const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
|
|
273
|
+
const nextSources = { ...sources };
|
|
274
|
+
for (const sourceName of ['analytics', 'revenuecat', 'sentry']) {
|
|
275
|
+
if (nextSources[sourceName]?.mode === 'command') {
|
|
276
|
+
nextSources[sourceName] = {
|
|
277
|
+
...nextSources[sourceName],
|
|
278
|
+
command: normalizeWizardSourceCommand(sourceName, nextSources[sourceName]),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (Array.isArray(nextSources.extra)) {
|
|
283
|
+
nextSources.extra = nextSources.extra.map((source) => {
|
|
284
|
+
if (!source || source.mode !== 'command')
|
|
285
|
+
return source;
|
|
286
|
+
const service = String(source.service || source.key || '').toLowerCase();
|
|
287
|
+
const sourceName = ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service)
|
|
288
|
+
? 'asc'
|
|
289
|
+
: service;
|
|
290
|
+
return {
|
|
291
|
+
...source,
|
|
292
|
+
command: normalizeWizardSourceCommand(sourceName, source),
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
...config,
|
|
298
|
+
sources: nextSources,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async function migrateRuntimeSourceCommandsFile(configPath) {
|
|
302
|
+
const existing = await readJsonIfPresent(configPath).catch(() => null);
|
|
303
|
+
if (!existing || typeof existing !== 'object')
|
|
304
|
+
return null;
|
|
305
|
+
const migrated = migrateRuntimeSourceCommands(existing);
|
|
306
|
+
if (JSON.stringify(existing.sources || {}) !== JSON.stringify(migrated.sources || {})) {
|
|
307
|
+
await writeJsonFile(configPath, migrated);
|
|
308
|
+
}
|
|
309
|
+
return migrated;
|
|
310
|
+
}
|
|
213
311
|
function normalizeConnectorKey(value) {
|
|
214
312
|
const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
215
313
|
if (!normalized)
|
|
@@ -268,23 +366,28 @@ function withMissingRequiredAnalyticsConnector(selected) {
|
|
|
268
366
|
return orderConnectors(selected);
|
|
269
367
|
return orderConnectors(['analytics', ...selected]);
|
|
270
368
|
}
|
|
271
|
-
async function
|
|
272
|
-
return askConnectorSelectionWithHealth(rl, {}, []);
|
|
273
|
-
}
|
|
274
|
-
async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = []) {
|
|
369
|
+
async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
|
|
275
370
|
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
276
|
-
return await askConnectorSelectionByText(rl, healthByConnector);
|
|
371
|
+
return await askConnectorSelectionByText(rl, healthByConnector, copy);
|
|
277
372
|
}
|
|
278
373
|
rl.pause();
|
|
374
|
+
let completed = false;
|
|
279
375
|
try {
|
|
280
|
-
|
|
376
|
+
const selected = await askConnectorSelectionByKeys(healthByConnector, initialSelected, copy);
|
|
377
|
+
completed = true;
|
|
378
|
+
return selected;
|
|
281
379
|
}
|
|
282
380
|
finally {
|
|
283
|
-
|
|
381
|
+
if (completed) {
|
|
382
|
+
rl.resume();
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
process.stdin.pause();
|
|
386
|
+
}
|
|
284
387
|
}
|
|
285
388
|
}
|
|
286
|
-
async function askConnectorSelectionByText(rl, healthByConnector = {}) {
|
|
287
|
-
printConnectorIntro();
|
|
389
|
+
async function askConnectorSelectionByText(rl, healthByConnector = {}, copy = {}) {
|
|
390
|
+
printConnectorIntro(copy);
|
|
288
391
|
for (const group of connectorPickerGroups(healthByConnector)) {
|
|
289
392
|
process.stdout.write(`${ANSI.bold}${group.title}${ANSI.reset}\n`);
|
|
290
393
|
for (const connector of group.connectors) {
|
|
@@ -322,26 +425,117 @@ function orderConnectors(keys) {
|
|
|
322
425
|
const selected = new Set(keys);
|
|
323
426
|
return CONNECTOR_KEYS.filter((key) => selected.has(key));
|
|
324
427
|
}
|
|
325
|
-
function printConnectorIntro() {
|
|
326
|
-
process.stdout.write(`\n${ANSI.bold}OpenClaw connector setup${ANSI.reset}\n`);
|
|
327
|
-
|
|
428
|
+
function printConnectorIntro(copy = {}) {
|
|
429
|
+
process.stdout.write(`\n${ANSI.bold}${copy.introTitle || 'OpenClaw connector setup'}${ANSI.reset}\n`);
|
|
430
|
+
const detail = copy.introDetail === undefined
|
|
431
|
+
? 'You can configure connector secrets here. API keys stay in this host\'s local secrets file, not in chat or config JSON.'
|
|
432
|
+
: copy.introDetail;
|
|
433
|
+
if (detail) {
|
|
434
|
+
process.stdout.write(`${ANSI.dim}${detail}${ANSI.reset}\n`);
|
|
435
|
+
}
|
|
436
|
+
process.stdout.write('\n');
|
|
328
437
|
}
|
|
329
|
-
async function
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
438
|
+
async function askMenuChoice(rl, { title, subtitle = 'Use Up/Down to move, Enter to continue.', options, defaultValue, renderHeader, }) {
|
|
439
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
440
|
+
process.stdout.write(`\n${title}\n`);
|
|
441
|
+
options.forEach((option, index) => {
|
|
442
|
+
process.stdout.write(` ${index + 1}) ${option.label}: ${option.detail}\n`);
|
|
443
|
+
});
|
|
444
|
+
const defaultIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
|
|
445
|
+
const answer = await ask(rl, `Setup area (1-${options.length})`, String(defaultIndex + 1));
|
|
446
|
+
const selected = options[Number(answer.trim()) - 1] || options[defaultIndex];
|
|
447
|
+
return selected.value;
|
|
448
|
+
}
|
|
449
|
+
rl.pause();
|
|
450
|
+
let completed = false;
|
|
337
451
|
try {
|
|
338
|
-
|
|
452
|
+
const selected = await askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader });
|
|
453
|
+
completed = true;
|
|
454
|
+
return selected;
|
|
339
455
|
}
|
|
340
456
|
finally {
|
|
341
|
-
|
|
342
|
-
|
|
457
|
+
if (completed) {
|
|
458
|
+
rl.resume();
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
process.stdin.pause();
|
|
462
|
+
}
|
|
343
463
|
}
|
|
344
464
|
}
|
|
465
|
+
async function askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader, }) {
|
|
466
|
+
emitKeypressEvents(process.stdin);
|
|
467
|
+
const wasRaw = process.stdin.isRaw;
|
|
468
|
+
const wasPaused = process.stdin.isPaused();
|
|
469
|
+
process.stdin.setRawMode(true);
|
|
470
|
+
process.stdin.resume();
|
|
471
|
+
let cursorIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
|
|
472
|
+
return await new Promise((resolve, reject) => {
|
|
473
|
+
const cleanup = () => {
|
|
474
|
+
process.stdin.off('keypress', onKeypress);
|
|
475
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
476
|
+
if (wasPaused) {
|
|
477
|
+
process.stdin.pause();
|
|
478
|
+
}
|
|
479
|
+
process.stdout.write(ANSI.showCursor);
|
|
480
|
+
};
|
|
481
|
+
const render = () => {
|
|
482
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
483
|
+
renderHeader?.();
|
|
484
|
+
process.stdout.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
|
|
485
|
+
process.stdout.write(`${ANSI.dim}${subtitle}${ANSI.reset}\n\n`);
|
|
486
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
487
|
+
const option = options[index];
|
|
488
|
+
const pointer = index === cursorIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';
|
|
489
|
+
const number = `${index + 1})`;
|
|
490
|
+
process.stdout.write(`${pointer} ${number} ${ANSI.bold}${option.label}${ANSI.reset}\n`);
|
|
491
|
+
writeWrapped(option.detail, ' ', ANSI.dim);
|
|
492
|
+
}
|
|
493
|
+
process.stdout.write(`\n${ANSI.dim}Esc/Q cancels. Number keys 1-${options.length} select directly.${ANSI.reset}\n`);
|
|
494
|
+
};
|
|
495
|
+
const cancel = () => {
|
|
496
|
+
cleanup();
|
|
497
|
+
process.stdout.write('\n');
|
|
498
|
+
reject(new WizardAbortError('Setup cancelled.'));
|
|
499
|
+
};
|
|
500
|
+
const finish = () => {
|
|
501
|
+
cleanup();
|
|
502
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
503
|
+
resolve(options[cursorIndex]?.value || defaultValue);
|
|
504
|
+
};
|
|
505
|
+
const onKeypress = (_text, key) => {
|
|
506
|
+
if (key?.ctrl && key?.name === 'c') {
|
|
507
|
+
cancel();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (key?.name === 'escape' || key?.name === 'q') {
|
|
511
|
+
cancel();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (key?.name === 'up' || key?.name === 'k') {
|
|
515
|
+
cursorIndex = (cursorIndex - 1 + options.length) % options.length;
|
|
516
|
+
}
|
|
517
|
+
else if (key?.name === 'down' || key?.name === 'j') {
|
|
518
|
+
cursorIndex = (cursorIndex + 1) % options.length;
|
|
519
|
+
}
|
|
520
|
+
else if (key?.name === 'return' || key?.name === 'enter') {
|
|
521
|
+
finish();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
else if (/^[1-9]$/.test(String(_text || ''))) {
|
|
525
|
+
const selectedIndex = Number(_text) - 1;
|
|
526
|
+
if (options[selectedIndex]) {
|
|
527
|
+
cursorIndex = selectedIndex;
|
|
528
|
+
finish();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
render();
|
|
533
|
+
};
|
|
534
|
+
process.stdin.on('keypress', onKeypress);
|
|
535
|
+
process.stdout.write(ANSI.hideCursor);
|
|
536
|
+
render();
|
|
537
|
+
});
|
|
538
|
+
}
|
|
345
539
|
function normalizeConnectorProgressKey(key) {
|
|
346
540
|
const normalized = String(key || '').trim().toLowerCase();
|
|
347
541
|
if (normalized === 'analytics' || normalized === 'analyticscli')
|
|
@@ -439,9 +633,6 @@ function connectorStatusLabel(key, healthByConnector = {}) {
|
|
|
439
633
|
return 'not configured';
|
|
440
634
|
return `configured, ${connectorHealthLabel(health.status)}`;
|
|
441
635
|
}
|
|
442
|
-
function formatConnectorHealthLine(key, healthByConnector = {}) {
|
|
443
|
-
return `${ANSI.dim}${formatConnectorHealthText(key, healthByConnector)}${ANSI.reset}`;
|
|
444
|
-
}
|
|
445
636
|
function formatConnectorHealthText(key, healthByConnector = {}) {
|
|
446
637
|
const health = getConnectorHealth(key, healthByConnector);
|
|
447
638
|
const label = connectorStatusLabel(key, healthByConnector);
|
|
@@ -517,7 +708,7 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
517
708
|
},
|
|
518
709
|
]));
|
|
519
710
|
}
|
|
520
|
-
const result = await runCommandCaptureWithProgress(
|
|
711
|
+
const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json`, onProgress);
|
|
521
712
|
const payload = parseJsonFromStdout(result.stdout);
|
|
522
713
|
const connectors = payload?.connectors && typeof payload.connectors === 'object' ? payload.connectors : {};
|
|
523
714
|
const healthByConnector = {
|
|
@@ -529,11 +720,11 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
529
720
|
};
|
|
530
721
|
return Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
|
|
531
722
|
}
|
|
532
|
-
function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '') {
|
|
723
|
+
function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '', copy = {}) {
|
|
533
724
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
534
|
-
printConnectorIntro();
|
|
535
|
-
process.stdout.write(`${ANSI.bold}Select connectors to set up or overwrite now${ANSI.reset}\n`);
|
|
536
|
-
writeWrapped('Use Up/Down to move, Space to toggle optional connectors, A to toggle all optional connectors, Enter to continue.', '', ANSI.dim);
|
|
725
|
+
printConnectorIntro(copy);
|
|
726
|
+
process.stdout.write(`${ANSI.bold}${copy.actionTitle || 'Select connectors to set up or overwrite now'}${ANSI.reset}\n`);
|
|
727
|
+
writeWrapped(copy.helpText || 'Use Up/Down to move, Space to toggle optional connectors, A to toggle all optional connectors, Enter to continue.', '', ANSI.dim);
|
|
537
728
|
process.stdout.write('\n');
|
|
538
729
|
let index = 0;
|
|
539
730
|
for (const group of connectorPickerGroups(healthByConnector)) {
|
|
@@ -559,16 +750,18 @@ function renderConnectorPicker(cursorIndex, selected, required, healthByConnecto
|
|
|
559
750
|
}
|
|
560
751
|
process.stdout.write(`${ANSI.dim}Esc/Q cancels. Number keys 1-${CONNECTOR_DEFINITIONS.length} also toggle connectors.${ANSI.reset}\n`);
|
|
561
752
|
}
|
|
562
|
-
async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = []) {
|
|
753
|
+
async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = [], copy = {}) {
|
|
563
754
|
emitKeypressEvents(process.stdin);
|
|
564
755
|
const wasRaw = process.stdin.isRaw;
|
|
565
756
|
const wasPaused = process.stdin.isPaused();
|
|
566
757
|
process.stdin.setRawMode(true);
|
|
567
758
|
process.stdin.resume();
|
|
568
759
|
let cursorIndex = 0;
|
|
569
|
-
const required = getRequiredConnectorKeys();
|
|
760
|
+
const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
|
|
570
761
|
const initial = new Set(initialSelected);
|
|
571
|
-
const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
|
|
762
|
+
const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
|
|
763
|
+
initial.has(key) ||
|
|
764
|
+
(copy.mode !== 'input' && !isConnectorLocallyConfigured(key))));
|
|
572
765
|
let warning = '';
|
|
573
766
|
return await new Promise((resolve, reject) => {
|
|
574
767
|
const displayItems = () => connectorPickerDisplayItems(healthByConnector);
|
|
@@ -586,7 +779,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
586
779
|
required.forEach((key) => selected.add(key));
|
|
587
780
|
if (selected.size === 0) {
|
|
588
781
|
warning = 'No connectors selected. Select a connector to update or press Esc to cancel.';
|
|
589
|
-
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
|
|
782
|
+
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
|
|
590
783
|
return;
|
|
591
784
|
}
|
|
592
785
|
cleanup();
|
|
@@ -596,7 +789,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
596
789
|
const cancel = () => {
|
|
597
790
|
cleanup();
|
|
598
791
|
process.stdout.write('\n');
|
|
599
|
-
reject(new
|
|
792
|
+
reject(new WizardAbortError('Connector setup cancelled.'));
|
|
600
793
|
};
|
|
601
794
|
const toggleCurrent = () => {
|
|
602
795
|
const connector = selectedDisplayConnector();
|
|
@@ -671,11 +864,11 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
671
864
|
}
|
|
672
865
|
}
|
|
673
866
|
}
|
|
674
|
-
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
|
|
867
|
+
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
|
|
675
868
|
};
|
|
676
869
|
process.stdin.on('keypress', onKeypress);
|
|
677
870
|
process.stdout.write(ANSI.hideCursor);
|
|
678
|
-
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
|
|
871
|
+
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
|
|
679
872
|
});
|
|
680
873
|
}
|
|
681
874
|
async function commandExists(commandName) {
|
|
@@ -914,16 +1107,16 @@ function summarizeFailureFix(connector, blockers) {
|
|
|
914
1107
|
if (/revoked|unauthorized|UNAUTHORIZED/i.test(combined)) {
|
|
915
1108
|
return 'Paste a fresh AnalyticsCLI readonly CLI token in the wizard, then let setup retest.';
|
|
916
1109
|
}
|
|
917
|
-
return 'Verify the AnalyticsCLI token can list projects. Per-project query failures are reported as warnings and should not block connector setup.';
|
|
1110
|
+
return 'Verify the AnalyticsCLI token can list accessible projects. Per-project query failures are reported as warnings and should not block connector setup.';
|
|
918
1111
|
}
|
|
919
1112
|
if (connector === 'sentry') {
|
|
920
1113
|
if (/404|Not Found/i.test(combined)) {
|
|
921
|
-
return 'Rerun Sentry/GlitchTip setup and use the correct base URL +
|
|
1114
|
+
return 'Rerun Sentry/GlitchTip setup and use the correct base URL + visible org. Project scope stays unpinned and is resolved from app context later.';
|
|
922
1115
|
}
|
|
923
|
-
return 'Verify the Sentry/GlitchTip token, base URL,
|
|
1116
|
+
return 'Verify the Sentry/GlitchTip token, base URL, and org, then rerun setup.';
|
|
924
1117
|
}
|
|
925
1118
|
if (connector === 'github') {
|
|
926
|
-
return '
|
|
1119
|
+
return 'Verify the GitHub token. Repo scope is inferred from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context.';
|
|
927
1120
|
}
|
|
928
1121
|
if (connector === 'revenuecat') {
|
|
929
1122
|
return 'Paste a RevenueCat v2 secret API key with read-only project permissions, then rerun setup.';
|
|
@@ -1048,11 +1241,6 @@ function printSetupSuccess(payload) {
|
|
|
1048
1241
|
process.stdout.write(`${payload.message}\n`);
|
|
1049
1242
|
}
|
|
1050
1243
|
}
|
|
1051
|
-
function healthCheckFailures(payload) {
|
|
1052
|
-
return Array.isArray(payload?.checks)
|
|
1053
|
-
? payload.checks.filter((check) => check?.status === 'fail')
|
|
1054
|
-
: [];
|
|
1055
|
-
}
|
|
1056
1244
|
function connectorFromCheckName(name) {
|
|
1057
1245
|
const value = String(name || '');
|
|
1058
1246
|
if (value.includes('analytics') || value.includes('ANALYTICSCLI'))
|
|
@@ -1111,146 +1299,12 @@ function cleanHealthDetail(detail) {
|
|
|
1111
1299
|
}
|
|
1112
1300
|
return truncate(raw, 180);
|
|
1113
1301
|
}
|
|
1114
|
-
function actionForHealthFailure(failure, configPath) {
|
|
1115
|
-
const name = String(failure?.name || '');
|
|
1116
|
-
const detail = String(failure?.detail || '');
|
|
1117
|
-
if (name === 'project:github-repo' || /project\.githubRepo/i.test(detail)) {
|
|
1118
|
-
return `No action required for Sentry setup. Set project.githubRepo in ${configPath} only if you want GitHub issue/PR delivery now.`;
|
|
1119
|
-
}
|
|
1120
|
-
if (name.includes('analytics') || /ANALYTICSCLI|analytics/i.test(detail)) {
|
|
1121
|
-
return 'Paste a fresh AnalyticsCLI readonly token, then let the wizard retest AnalyticsCLI.';
|
|
1122
|
-
}
|
|
1123
|
-
if (name.includes('sentry') || /Sentry|GlitchTip/i.test(detail)) {
|
|
1124
|
-
return 'Only fix this if token, org, or base URL is missing or invalid.';
|
|
1125
|
-
}
|
|
1126
|
-
if (name.includes('github')) {
|
|
1127
|
-
return 'Configure GitHub token/repo access, or leave GitHub delivery disabled.';
|
|
1128
|
-
}
|
|
1129
|
-
if (name.includes('revenuecat')) {
|
|
1130
|
-
return 'Paste a RevenueCat v2 secret API key with read-only project permissions.';
|
|
1131
|
-
}
|
|
1132
|
-
if (name.includes('asc')) {
|
|
1133
|
-
return 'Paste ASC API key details or rerun ASC setup when ready.';
|
|
1134
|
-
}
|
|
1135
|
-
return 'Use the connector setup flow below to refresh this configuration.';
|
|
1136
|
-
}
|
|
1137
1302
|
function isDeferredGitHubFailure(failure) {
|
|
1138
1303
|
const name = String(failure?.name || '');
|
|
1139
1304
|
const detail = String(failure?.detail || '');
|
|
1140
1305
|
return (name === 'project:github-repo' ||
|
|
1141
1306
|
(name === 'connection:github' && /project\.githubRepo|repo is missing|repo is not configured/i.test(detail)));
|
|
1142
1307
|
}
|
|
1143
|
-
function isDeferredSentryProjectFailure(failure) {
|
|
1144
|
-
const name = String(failure?.name || '');
|
|
1145
|
-
const detail = String(failure?.detail || '');
|
|
1146
|
-
return name.includes('sentry') && /No Sentry projects configured/i.test(detail);
|
|
1147
|
-
}
|
|
1148
|
-
function summarizeHealthFailure(failure, configPath) {
|
|
1149
|
-
const name = String(failure?.name || '');
|
|
1150
|
-
const detail = String(failure?.detail || '');
|
|
1151
|
-
const connector = connectorFromCheckName(`${name} ${detail}`) || 'setup';
|
|
1152
|
-
if (connector === 'analytics' && /invalid token|unauthorized|token has been revoked/i.test(detail)) {
|
|
1153
|
-
return {
|
|
1154
|
-
connector,
|
|
1155
|
-
status: 'token invalid or expired',
|
|
1156
|
-
action: 'paste a fresh readonly token',
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
if (connector === 'sentry' && /No Sentry projects configured/i.test(detail)) {
|
|
1160
|
-
return {
|
|
1161
|
-
connector,
|
|
1162
|
-
status: 'project scope deferred',
|
|
1163
|
-
action: 'no user action; OpenClaw discovers visible projects from org + token',
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
if (connector === 'github' && isDeferredGitHubFailure(failure)) {
|
|
1167
|
-
return {
|
|
1168
|
-
connector,
|
|
1169
|
-
status: 'repo not known yet',
|
|
1170
|
-
action: `optional; set project.githubRepo in ${configPath} only for GitHub delivery`,
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
return {
|
|
1174
|
-
connector,
|
|
1175
|
-
status: cleanHealthDetail(detail),
|
|
1176
|
-
action: actionForHealthFailure(failure, configPath),
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
function printHealthFailures(failures, configPath) {
|
|
1180
|
-
const summarized = [];
|
|
1181
|
-
const seen = new Set();
|
|
1182
|
-
for (const failure of failures) {
|
|
1183
|
-
if (isDeferredGitHubFailure(failure))
|
|
1184
|
-
continue;
|
|
1185
|
-
if (isDeferredSentryProjectFailure(failure))
|
|
1186
|
-
continue;
|
|
1187
|
-
const summary = summarizeHealthFailure(failure, configPath);
|
|
1188
|
-
const key = `${summary.connector}:${summary.status}:${summary.action}`;
|
|
1189
|
-
if (seen.has(key))
|
|
1190
|
-
continue;
|
|
1191
|
-
seen.add(key);
|
|
1192
|
-
summarized.push(summary);
|
|
1193
|
-
}
|
|
1194
|
-
if (summarized.length === 0) {
|
|
1195
|
-
process.stdout.write('\nOnly deferred optional checks remain.\n\n');
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
process.stdout.write('\nNeeds attention\n');
|
|
1199
|
-
process.stdout.write('---------------\n');
|
|
1200
|
-
for (const summary of summarized) {
|
|
1201
|
-
process.stdout.write(`- ${connectorTitle(summary.connector)}: ${summary.status}\n`);
|
|
1202
|
-
process.stdout.write(` Next: ${summary.action}\n`);
|
|
1203
|
-
}
|
|
1204
|
-
process.stdout.write('\n');
|
|
1205
|
-
}
|
|
1206
|
-
function inferConnectorsFromHealthFailures(failures) {
|
|
1207
|
-
const inferred = new Set();
|
|
1208
|
-
for (const failure of failures) {
|
|
1209
|
-
if (isDeferredGitHubFailure(failure))
|
|
1210
|
-
continue;
|
|
1211
|
-
if (isDeferredSentryProjectFailure(failure))
|
|
1212
|
-
continue;
|
|
1213
|
-
const connector = connectorFromCheckName(`${failure?.name || ''} ${failure?.detail || ''}`);
|
|
1214
|
-
if (connector)
|
|
1215
|
-
inferred.add(connector);
|
|
1216
|
-
}
|
|
1217
|
-
return orderConnectors([...inferred]);
|
|
1218
|
-
}
|
|
1219
|
-
async function getHealthCheckPlan(configPath, selected) {
|
|
1220
|
-
const config = await readJsonIfPresent(configPath).catch(() => null);
|
|
1221
|
-
const items = [
|
|
1222
|
-
{
|
|
1223
|
-
key: 'preflight',
|
|
1224
|
-
label: 'Local preflight',
|
|
1225
|
-
detail: 'config, dependencies, source wiring',
|
|
1226
|
-
status: 'pending',
|
|
1227
|
-
},
|
|
1228
|
-
];
|
|
1229
|
-
const selectedSet = new Set(selected);
|
|
1230
|
-
const hasAnalytics = selectedSet.has('analytics') ||
|
|
1231
|
-
Boolean(process.env.ANALYTICSCLI_ACCESS_TOKEN?.trim() || process.env.ANALYTICSCLI_READONLY_TOKEN?.trim()) ||
|
|
1232
|
-
(config?.sources?.analytics && config.sources.analytics.enabled !== false);
|
|
1233
|
-
const sentryAccounts = Array.isArray(config?.sources?.sentry?.accounts) ? config.sources.sentry.accounts : [];
|
|
1234
|
-
const hasSentry = selectedSet.has('sentry') ||
|
|
1235
|
-
sentryAccounts.length > 0 ||
|
|
1236
|
-
Boolean(process.env.SENTRY_AUTH_TOKEN?.trim() || process.env.GLITCHTIP_AUTH_TOKEN?.trim());
|
|
1237
|
-
const hasRevenueCat = selectedSet.has('revenuecat') ||
|
|
1238
|
-
Boolean(process.env.REVENUECAT_API_KEY?.trim()) ||
|
|
1239
|
-
(config?.sources?.revenuecat && config.sources.revenuecat.enabled !== false);
|
|
1240
|
-
const githubRepo = String(config?.project?.githubRepo || '').trim();
|
|
1241
|
-
const hasGitHub = selectedSet.has('github') || Boolean(process.env.GITHUB_TOKEN?.trim()) || Boolean(githubRepo);
|
|
1242
|
-
if (hasAnalytics)
|
|
1243
|
-
items.push({ key: 'analytics', label: 'AnalyticsCLI', detail: 'token auth + readonly query', status: 'pending' });
|
|
1244
|
-
if (hasSentry)
|
|
1245
|
-
items.push({ key: 'sentry', label: 'Sentry / GlitchTip', detail: 'token/org API + project discovery', status: 'pending' });
|
|
1246
|
-
if (hasRevenueCat)
|
|
1247
|
-
items.push({ key: 'revenuecat', label: 'RevenueCat', detail: 'API key auth + project read', status: 'pending' });
|
|
1248
|
-
if (hasGitHub && githubRepo)
|
|
1249
|
-
items.push({ key: 'github', label: 'GitHub', detail: `repo access (${githubRepo})`, status: 'pending' });
|
|
1250
|
-
if (hasGitHub && !githubRepo)
|
|
1251
|
-
items.push({ key: 'github', label: 'GitHub', detail: 'skipped until repo is known', status: 'pending' });
|
|
1252
|
-
return items;
|
|
1253
|
-
}
|
|
1254
1308
|
function healthStatusLabel(status) {
|
|
1255
1309
|
if (status === 'running')
|
|
1256
1310
|
return 'running';
|
|
@@ -1299,9 +1353,6 @@ function updateHealthProgress(items, event) {
|
|
|
1299
1353
|
}
|
|
1300
1354
|
return false;
|
|
1301
1355
|
}
|
|
1302
|
-
function allProgressItemsFinished(items) {
|
|
1303
|
-
return items.length > 0 && items.every((item) => !['pending', 'running'].includes(String(item.status || '')));
|
|
1304
|
-
}
|
|
1305
1356
|
function buildSetupTestProgressPlan(selected) {
|
|
1306
1357
|
const selectedSet = new Set(selected);
|
|
1307
1358
|
const items = [
|
|
@@ -1403,7 +1454,7 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
|
|
|
1403
1454
|
...process.env,
|
|
1404
1455
|
...secrets,
|
|
1405
1456
|
};
|
|
1406
|
-
const command =
|
|
1457
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(configPath)} --setup-only --connectors ${quote(connector)} --only-connectors ${quote(connector)}`;
|
|
1407
1458
|
let result = await runSetupCommandWithProgress(command, env, [connector], `Checking ${connectorLabel(connector)} immediately after setup...`);
|
|
1408
1459
|
let payload = parseJsonFromStdout(result.stdout);
|
|
1409
1460
|
if (connector === 'asc') {
|
|
@@ -1430,39 +1481,6 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
|
|
|
1430
1481
|
process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed or is only waiting on optional/deferred context.\n`);
|
|
1431
1482
|
return { ok: true, retry: false, result, payload };
|
|
1432
1483
|
}
|
|
1433
|
-
async function offerConfiguredConnectionFixes(rl, configPath, selected) {
|
|
1434
|
-
if (!(await fileExists(configPath)))
|
|
1435
|
-
return selected;
|
|
1436
|
-
clearTerminal();
|
|
1437
|
-
const plan = await getHealthCheckPlan(configPath, selected);
|
|
1438
|
-
renderHealthProgress(plan, 'Starting live checks...');
|
|
1439
|
-
const command = `node scripts/openclaw-growth-preflight.mjs --config ${quote(configPath)} --test-connections --progress-json`;
|
|
1440
|
-
const result = await runCommandCaptureWithProgress(command, (event) => {
|
|
1441
|
-
if (updateHealthProgress(plan, event)) {
|
|
1442
|
-
renderHealthProgress(plan);
|
|
1443
|
-
}
|
|
1444
|
-
});
|
|
1445
|
-
renderHealthProgress(plan, 'Checks finished.');
|
|
1446
|
-
const payload = parseJsonFromStdout(result.stdout);
|
|
1447
|
-
const failures = healthCheckFailures(payload).filter((failure) => !isDeferredGitHubFailure(failure) && !isDeferredSentryProjectFailure(failure));
|
|
1448
|
-
if (payload?.ok === true || failures.length === 0) {
|
|
1449
|
-
process.stdout.write('Configured connectors look healthy.\n\n');
|
|
1450
|
-
return selected;
|
|
1451
|
-
}
|
|
1452
|
-
printHealthFailures(failures, configPath);
|
|
1453
|
-
const inferred = inferConnectorsFromHealthFailures(failures);
|
|
1454
|
-
if (inferred.length === 0) {
|
|
1455
|
-
process.stdout.write('Continuing with the connector(s) you selected.\n\n');
|
|
1456
|
-
return selected;
|
|
1457
|
-
}
|
|
1458
|
-
const fixNow = await askYesNo(rl, `Fix now (${inferred.join(', ')})?`, true);
|
|
1459
|
-
clearTerminal();
|
|
1460
|
-
if (!fixNow) {
|
|
1461
|
-
process.stdout.write('Continuing with selected connector(s).\n\n');
|
|
1462
|
-
return selected;
|
|
1463
|
-
}
|
|
1464
|
-
return orderConnectors([...new Set([...selected, ...inferred])]);
|
|
1465
|
-
}
|
|
1466
1484
|
function getUserLocalBinDir() {
|
|
1467
1485
|
return process.env.HOME ? path.join(process.env.HOME, '.local', 'bin') : null;
|
|
1468
1486
|
}
|
|
@@ -1818,13 +1836,13 @@ function getGrowthRunCommand(config, displayConfigPath) {
|
|
|
1818
1836
|
if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.runCommand) {
|
|
1819
1837
|
return config.security.connectorSecrets.runCommand;
|
|
1820
1838
|
}
|
|
1821
|
-
return `
|
|
1839
|
+
return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
|
|
1822
1840
|
}
|
|
1823
1841
|
function getConnectorHealthCommand(config, displayConfigPath) {
|
|
1824
1842
|
if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.healthCommand) {
|
|
1825
1843
|
return config.security.connectorSecrets.healthCommand;
|
|
1826
1844
|
}
|
|
1827
|
-
return `
|
|
1845
|
+
return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
|
|
1828
1846
|
}
|
|
1829
1847
|
async function maybePromptSecret(rl, label, envName) {
|
|
1830
1848
|
const existing = process.env[envName]?.trim();
|
|
@@ -1874,12 +1892,6 @@ function printSentryTokenGuidance({ baseUrl, tokenEnv }) {
|
|
|
1874
1892
|
'Optional for richer release context: `project:releases`.',
|
|
1875
1893
|
]);
|
|
1876
1894
|
}
|
|
1877
|
-
function parseCommaList(value) {
|
|
1878
|
-
return String(value || '')
|
|
1879
|
-
.split(',')
|
|
1880
|
-
.map((entry) => entry.trim())
|
|
1881
|
-
.filter(Boolean);
|
|
1882
|
-
}
|
|
1883
1895
|
function buildUrl(baseUrl, pathname, params = {}) {
|
|
1884
1896
|
const url = new URL(pathname, `${String(baseUrl || 'https://sentry.io').replace(/\/$/, '')}/`);
|
|
1885
1897
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -1904,7 +1916,7 @@ function apiListItems(payload) {
|
|
|
1904
1916
|
return payload.teams;
|
|
1905
1917
|
return [];
|
|
1906
1918
|
}
|
|
1907
|
-
async function fetchSentryJsonPage({
|
|
1919
|
+
async function fetchSentryJsonPage({ token, url }) {
|
|
1908
1920
|
const normalizedToken = String(token || '').trim();
|
|
1909
1921
|
const response = await fetch(url, {
|
|
1910
1922
|
method: 'GET',
|
|
@@ -1938,7 +1950,7 @@ async function fetchSentryJsonList({ baseUrl, token, url }) {
|
|
|
1938
1950
|
const pages = [];
|
|
1939
1951
|
let nextUrl = url;
|
|
1940
1952
|
for (let page = 0; nextUrl && page < 10; page += 1) {
|
|
1941
|
-
const result = await fetchSentryJsonPage({
|
|
1953
|
+
const result = await fetchSentryJsonPage({ token, url: nextUrl });
|
|
1942
1954
|
pages.push(result.detail);
|
|
1943
1955
|
if (!result.ok)
|
|
1944
1956
|
return { ...result, payload: items, detail: pages.join('; ') };
|
|
@@ -2069,7 +2081,7 @@ async function upsertSentryAccountsConfig(configPath, accounts) {
|
|
|
2069
2081
|
...(config.sources?.sentry || {}),
|
|
2070
2082
|
enabled: true,
|
|
2071
2083
|
mode: 'command',
|
|
2072
|
-
command:
|
|
2084
|
+
command: getWizardDefaultSourceCommand('sentry'),
|
|
2073
2085
|
accounts: [...merged.values()],
|
|
2074
2086
|
},
|
|
2075
2087
|
};
|
|
@@ -2482,13 +2494,13 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2482
2494
|
org = await ask(rl, `Sentry org slug for ${label} (leave empty to defer)`, index === 0 ? process.env.SENTRY_ORG || '' : '');
|
|
2483
2495
|
}
|
|
2484
2496
|
const environment = await ask(rl, `Sentry environment for ${label}`, index === 0 ? process.env.SENTRY_ENVIRONMENT || 'production' : 'production');
|
|
2485
|
-
let projects = [];
|
|
2486
2497
|
if (org.trim() && token) {
|
|
2487
|
-
process.stdout.write(`
|
|
2498
|
+
process.stdout.write(`Checking visible Sentry projects for ${label} without pinning project scope...\n`);
|
|
2488
2499
|
const discovery = await discoverSentryProjects({ baseUrl, token, org });
|
|
2500
|
+
let verifiedVisibleProjects = false;
|
|
2489
2501
|
if (discovery.ok && discovery.projects.length > 0) {
|
|
2490
|
-
|
|
2491
|
-
process.stdout.write(`
|
|
2502
|
+
verifiedVisibleProjects = true;
|
|
2503
|
+
process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned so OpenClaw/Hermes can decide per run.\n`);
|
|
2492
2504
|
}
|
|
2493
2505
|
else {
|
|
2494
2506
|
const fallbackOrgs = discoveredOrganizations
|
|
@@ -2498,15 +2510,14 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2498
2510
|
process.stdout.write(`Trying visible org ${fallbackOrg}...\n`);
|
|
2499
2511
|
const fallbackDiscovery = await discoverSentryProjects({ baseUrl, token, org: fallbackOrg });
|
|
2500
2512
|
if (fallbackDiscovery.ok && fallbackDiscovery.projects.length > 0) {
|
|
2501
|
-
|
|
2502
|
-
|
|
2513
|
+
org = fallbackOrg;
|
|
2514
|
+
verifiedVisibleProjects = true;
|
|
2515
|
+
process.stdout.write(`Using org ${fallbackOrg}; found ${fallbackDiscovery.projects.length} visible project(s). Project scope remains unpinned.\n`);
|
|
2503
2516
|
break;
|
|
2504
2517
|
}
|
|
2505
2518
|
}
|
|
2506
|
-
if (
|
|
2507
|
-
process.stdout.write(`Could not
|
|
2508
|
-
const manualProjects = parseCommaList(await ask(rl, `Project slugs for ${label} (comma-separated, leave empty to let app context decide)`, ''));
|
|
2509
|
-
projects = manualProjects;
|
|
2519
|
+
if (!verifiedVisibleProjects && !discovery.ok) {
|
|
2520
|
+
process.stdout.write(`Could not verify visible projects automatically (${discovery.detail}). Project scope will be resolved from app context later.\n`);
|
|
2510
2521
|
}
|
|
2511
2522
|
}
|
|
2512
2523
|
}
|
|
@@ -2519,7 +2530,6 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2519
2530
|
baseUrl,
|
|
2520
2531
|
tokenEnv,
|
|
2521
2532
|
...(org.trim() ? { org: org.trim() } : {}),
|
|
2522
|
-
...(projects.length > 0 ? { projects } : {}),
|
|
2523
2533
|
...(environment.trim() ? { environment: environment.trim() } : {}),
|
|
2524
2534
|
});
|
|
2525
2535
|
if (index === 0) {
|
|
@@ -2679,6 +2689,144 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
2679
2689
|
const code = await rerunCurrentWizardWithoutSelfUpdate();
|
|
2680
2690
|
process.exit(code ?? 0);
|
|
2681
2691
|
}
|
|
2692
|
+
async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, allowIsolationPrompt = true, }) {
|
|
2693
|
+
clearTerminal();
|
|
2694
|
+
printConnectorIntro();
|
|
2695
|
+
process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
|
|
2696
|
+
for (const key of selected) {
|
|
2697
|
+
process.stdout.write(` - ${connectorLabel(key)}\n`);
|
|
2698
|
+
}
|
|
2699
|
+
process.stdout.write('\n');
|
|
2700
|
+
const secrets = {};
|
|
2701
|
+
let sentryAccounts = [];
|
|
2702
|
+
if (selected.includes('analytics')) {
|
|
2703
|
+
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
2704
|
+
while (true) {
|
|
2705
|
+
clearTerminal();
|
|
2706
|
+
await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
|
|
2707
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2708
|
+
rl,
|
|
2709
|
+
configPath: args.config,
|
|
2710
|
+
connector: 'analytics',
|
|
2711
|
+
secrets,
|
|
2712
|
+
});
|
|
2713
|
+
if (!check.retry)
|
|
2714
|
+
break;
|
|
2715
|
+
forceFreshAnalyticsToken = true;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
if (selected.includes('github')) {
|
|
2719
|
+
while (true) {
|
|
2720
|
+
clearTerminal();
|
|
2721
|
+
await guideGitHubConnector(rl, secrets);
|
|
2722
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2723
|
+
rl,
|
|
2724
|
+
configPath: args.config,
|
|
2725
|
+
connector: 'github',
|
|
2726
|
+
secrets,
|
|
2727
|
+
});
|
|
2728
|
+
if (!check.retry)
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (selected.includes('revenuecat')) {
|
|
2733
|
+
while (true) {
|
|
2734
|
+
clearTerminal();
|
|
2735
|
+
await guideRevenueCatConnector(rl, secrets);
|
|
2736
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2737
|
+
rl,
|
|
2738
|
+
configPath: args.config,
|
|
2739
|
+
connector: 'revenuecat',
|
|
2740
|
+
secrets,
|
|
2741
|
+
});
|
|
2742
|
+
if (!check.retry)
|
|
2743
|
+
break;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (selected.includes('sentry')) {
|
|
2747
|
+
while (true) {
|
|
2748
|
+
clearTerminal();
|
|
2749
|
+
sentryAccounts = await guideSentryConnector(rl, secrets);
|
|
2750
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2751
|
+
rl,
|
|
2752
|
+
configPath: args.config,
|
|
2753
|
+
connector: 'sentry',
|
|
2754
|
+
secrets,
|
|
2755
|
+
sentryAccounts,
|
|
2756
|
+
});
|
|
2757
|
+
if (!check.retry)
|
|
2758
|
+
break;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
if (selected.includes('asc')) {
|
|
2762
|
+
while (true) {
|
|
2763
|
+
clearTerminal();
|
|
2764
|
+
await guideAscConnector(rl, secrets);
|
|
2765
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2766
|
+
rl,
|
|
2767
|
+
configPath: args.config,
|
|
2768
|
+
connector: 'asc',
|
|
2769
|
+
secrets,
|
|
2770
|
+
});
|
|
2771
|
+
if (!check.retry)
|
|
2772
|
+
break;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
const secretsFile = resolveSecretsFile();
|
|
2776
|
+
const wroteSecrets = Object.keys(secrets).length > 0;
|
|
2777
|
+
clearTerminal();
|
|
2778
|
+
if (wroteSecrets) {
|
|
2779
|
+
await writeSecretsFile(secretsFile, secrets);
|
|
2780
|
+
process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
|
|
2781
|
+
}
|
|
2782
|
+
else {
|
|
2783
|
+
process.stdout.write('\nNo new secrets were written.\n');
|
|
2784
|
+
}
|
|
2785
|
+
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2786
|
+
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
2787
|
+
}
|
|
2788
|
+
const env = {
|
|
2789
|
+
...process.env,
|
|
2790
|
+
...secrets,
|
|
2791
|
+
};
|
|
2792
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
|
|
2793
|
+
let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
|
|
2794
|
+
let setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
2795
|
+
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2796
|
+
process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
|
|
2797
|
+
}
|
|
2798
|
+
if (selected.includes('asc')) {
|
|
2799
|
+
try {
|
|
2800
|
+
const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
|
|
2801
|
+
if (ascWebAuthChanged) {
|
|
2802
|
+
setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
|
|
2803
|
+
setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
catch (error) {
|
|
2807
|
+
process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (setupResult.ok && setupPayload?.ok !== false) {
|
|
2811
|
+
printSetupSuccess(setupPayload);
|
|
2812
|
+
if (wroteSecrets) {
|
|
2813
|
+
process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
|
|
2814
|
+
}
|
|
2815
|
+
const configureIsolation = allowIsolationPrompt && ENABLE_ISOLATED_SECRET_RUNNER_WIZARD && await askYesNo(rl, 'Generate an isolated secret runner so OpenClaw can run health checks without reading API keys?', true);
|
|
2816
|
+
if (configureIsolation) {
|
|
2817
|
+
const config = await loadEditableConfig(args.config);
|
|
2818
|
+
const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
|
|
2819
|
+
await writeJsonFile(path.resolve(args.config), config);
|
|
2820
|
+
const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
|
|
2821
|
+
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
2822
|
+
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
2823
|
+
}
|
|
2824
|
+
return true;
|
|
2825
|
+
}
|
|
2826
|
+
printSetupFailure({ result: setupResult, payload: setupPayload, command });
|
|
2827
|
+
process.exitCode = 1;
|
|
2828
|
+
return false;
|
|
2829
|
+
}
|
|
2682
2830
|
async function runConnectorSetupWizard(args) {
|
|
2683
2831
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2684
2832
|
throw new Error('Connector wizard requires an interactive terminal.');
|
|
@@ -2687,151 +2835,18 @@ async function runConnectorSetupWizard(args) {
|
|
|
2687
2835
|
try {
|
|
2688
2836
|
clearTerminal();
|
|
2689
2837
|
printConnectorIntro();
|
|
2838
|
+
await migrateRuntimeSourceCommandsFile(args.config);
|
|
2690
2839
|
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(args.config, onProgress));
|
|
2691
2840
|
const existingFixes = connectorKeysNeedingAttention(healthByConnector);
|
|
2692
2841
|
const requestedConnectors = args.connectors ? parseConnectorList(args.connectors) : [];
|
|
2693
2842
|
const chosenConnectors = requestedConnectors.length > 0
|
|
2694
2843
|
? orderConnectors([...new Set([...requestedConnectors, ...existingFixes])])
|
|
2695
2844
|
: await askConnectorSelectionWithHealth(rl, healthByConnector, existingFixes);
|
|
2696
|
-
|
|
2845
|
+
const selected = withMissingRequiredAnalyticsConnector(chosenConnectors);
|
|
2697
2846
|
if (selected.length === 0) {
|
|
2698
2847
|
throw new Error('No supported connectors selected. Use analytics, github, revenuecat, sentry, asc, or all.');
|
|
2699
2848
|
}
|
|
2700
|
-
|
|
2701
|
-
printConnectorIntro();
|
|
2702
|
-
process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
|
|
2703
|
-
for (const key of selected) {
|
|
2704
|
-
process.stdout.write(` - ${connectorLabel(key)}\n`);
|
|
2705
|
-
}
|
|
2706
|
-
process.stdout.write('\n');
|
|
2707
|
-
const secrets = {};
|
|
2708
|
-
let sentryAccounts = [];
|
|
2709
|
-
if (selected.includes('analytics')) {
|
|
2710
|
-
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
2711
|
-
while (true) {
|
|
2712
|
-
clearTerminal();
|
|
2713
|
-
await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
|
|
2714
|
-
const check = await runImmediateConnectorHealthCheck({
|
|
2715
|
-
rl,
|
|
2716
|
-
configPath: args.config,
|
|
2717
|
-
connector: 'analytics',
|
|
2718
|
-
secrets,
|
|
2719
|
-
});
|
|
2720
|
-
if (!check.retry)
|
|
2721
|
-
break;
|
|
2722
|
-
forceFreshAnalyticsToken = true;
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
if (selected.includes('github')) {
|
|
2726
|
-
while (true) {
|
|
2727
|
-
clearTerminal();
|
|
2728
|
-
await guideGitHubConnector(rl, secrets);
|
|
2729
|
-
const check = await runImmediateConnectorHealthCheck({
|
|
2730
|
-
rl,
|
|
2731
|
-
configPath: args.config,
|
|
2732
|
-
connector: 'github',
|
|
2733
|
-
secrets,
|
|
2734
|
-
});
|
|
2735
|
-
if (!check.retry)
|
|
2736
|
-
break;
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
if (selected.includes('revenuecat')) {
|
|
2740
|
-
while (true) {
|
|
2741
|
-
clearTerminal();
|
|
2742
|
-
await guideRevenueCatConnector(rl, secrets);
|
|
2743
|
-
const check = await runImmediateConnectorHealthCheck({
|
|
2744
|
-
rl,
|
|
2745
|
-
configPath: args.config,
|
|
2746
|
-
connector: 'revenuecat',
|
|
2747
|
-
secrets,
|
|
2748
|
-
});
|
|
2749
|
-
if (!check.retry)
|
|
2750
|
-
break;
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
if (selected.includes('sentry')) {
|
|
2754
|
-
while (true) {
|
|
2755
|
-
clearTerminal();
|
|
2756
|
-
sentryAccounts = await guideSentryConnector(rl, secrets);
|
|
2757
|
-
const check = await runImmediateConnectorHealthCheck({
|
|
2758
|
-
rl,
|
|
2759
|
-
configPath: args.config,
|
|
2760
|
-
connector: 'sentry',
|
|
2761
|
-
secrets,
|
|
2762
|
-
sentryAccounts,
|
|
2763
|
-
});
|
|
2764
|
-
if (!check.retry)
|
|
2765
|
-
break;
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
if (selected.includes('asc')) {
|
|
2769
|
-
while (true) {
|
|
2770
|
-
clearTerminal();
|
|
2771
|
-
await guideAscConnector(rl, secrets);
|
|
2772
|
-
const check = await runImmediateConnectorHealthCheck({
|
|
2773
|
-
rl,
|
|
2774
|
-
configPath: args.config,
|
|
2775
|
-
connector: 'asc',
|
|
2776
|
-
secrets,
|
|
2777
|
-
});
|
|
2778
|
-
if (!check.retry)
|
|
2779
|
-
break;
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
const secretsFile = resolveSecretsFile();
|
|
2783
|
-
const wroteSecrets = Object.keys(secrets).length > 0;
|
|
2784
|
-
clearTerminal();
|
|
2785
|
-
if (wroteSecrets) {
|
|
2786
|
-
await writeSecretsFile(secretsFile, secrets);
|
|
2787
|
-
process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
|
|
2788
|
-
}
|
|
2789
|
-
else {
|
|
2790
|
-
process.stdout.write('\nNo new secrets were written.\n');
|
|
2791
|
-
}
|
|
2792
|
-
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2793
|
-
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
2794
|
-
}
|
|
2795
|
-
const env = {
|
|
2796
|
-
...process.env,
|
|
2797
|
-
...secrets,
|
|
2798
|
-
};
|
|
2799
|
-
const command = `node scripts/openclaw-growth-start.mjs --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
|
|
2800
|
-
let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
|
|
2801
|
-
let setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
2802
|
-
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2803
|
-
process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
|
|
2804
|
-
}
|
|
2805
|
-
if (selected.includes('asc')) {
|
|
2806
|
-
try {
|
|
2807
|
-
const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
|
|
2808
|
-
if (ascWebAuthChanged) {
|
|
2809
|
-
setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
|
|
2810
|
-
setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
catch (error) {
|
|
2814
|
-
process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
if (setupResult.ok && setupPayload?.ok !== false) {
|
|
2818
|
-
printSetupSuccess(setupPayload);
|
|
2819
|
-
if (wroteSecrets) {
|
|
2820
|
-
process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
|
|
2821
|
-
}
|
|
2822
|
-
const configureIsolation = ENABLE_ISOLATED_SECRET_RUNNER_WIZARD && await askYesNo(rl, 'Generate an isolated secret runner so OpenClaw can run health checks without reading API keys?', true);
|
|
2823
|
-
if (configureIsolation) {
|
|
2824
|
-
const config = await loadEditableConfig(args.config);
|
|
2825
|
-
const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
|
|
2826
|
-
await writeJsonFile(path.resolve(args.config), config);
|
|
2827
|
-
const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
|
|
2828
|
-
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
2829
|
-
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
2830
|
-
}
|
|
2831
|
-
return;
|
|
2832
|
-
}
|
|
2833
|
-
printSetupFailure({ result: setupResult, payload: setupPayload, command });
|
|
2834
|
-
process.exitCode = 1;
|
|
2849
|
+
await runConnectorSetupSteps({ rl, args, selected, healthByConnector });
|
|
2835
2850
|
}
|
|
2836
2851
|
finally {
|
|
2837
2852
|
rl.close();
|
|
@@ -2870,57 +2885,6 @@ async function askYesNo(rl, label, defaultYes = true) {
|
|
|
2870
2885
|
}
|
|
2871
2886
|
}
|
|
2872
2887
|
}
|
|
2873
|
-
async function askChoice(rl, label, options, defaultValue) {
|
|
2874
|
-
const normalizedDefault = options.includes(defaultValue) ? defaultValue : options[0];
|
|
2875
|
-
while (true) {
|
|
2876
|
-
const answer = (await rl.question(`${label} (${options.join('/')}) [${normalizedDefault}]: `))
|
|
2877
|
-
.trim()
|
|
2878
|
-
.toLowerCase();
|
|
2879
|
-
if (!answer) {
|
|
2880
|
-
return normalizedDefault;
|
|
2881
|
-
}
|
|
2882
|
-
if (options.includes(answer)) {
|
|
2883
|
-
return answer;
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
async function askSourceConfig(rl, sourceName, defaultPath, hint, options = {}) {
|
|
2888
|
-
const forceEnabled = Boolean(options.forceEnabled);
|
|
2889
|
-
const defaultCommand = String(options.defaultCommand || getDefaultSourceCommand(sourceName) || '').trim();
|
|
2890
|
-
const defaultMode = defaultCommand ? 'command' : 'file';
|
|
2891
|
-
const defaultEnabled = options.defaultEnabled ?? sourceName === 'analytics';
|
|
2892
|
-
const enabled = forceEnabled
|
|
2893
|
-
? true
|
|
2894
|
-
: await askYesNo(rl, `Enable source "${sourceName}"?`, defaultEnabled);
|
|
2895
|
-
if (!enabled) {
|
|
2896
|
-
return {
|
|
2897
|
-
enabled: false,
|
|
2898
|
-
mode: 'file',
|
|
2899
|
-
path: defaultPath,
|
|
2900
|
-
hint,
|
|
2901
|
-
};
|
|
2902
|
-
}
|
|
2903
|
-
process.stdout.write(`Where to get ${sourceName} data:\n${hint}\n`);
|
|
2904
|
-
const modeInput = await ask(rl, 'Mode (file/command)', defaultMode);
|
|
2905
|
-
const mode = modeInput.toLowerCase() === 'command' ? 'command' : 'file';
|
|
2906
|
-
const value = await ask(rl, mode === 'file' ? `${sourceName} JSON file path` : `${sourceName} command`, mode === 'file' ? defaultPath : defaultCommand);
|
|
2907
|
-
if (mode === 'file') {
|
|
2908
|
-
return {
|
|
2909
|
-
enabled: true,
|
|
2910
|
-
mode,
|
|
2911
|
-
path: value,
|
|
2912
|
-
hint,
|
|
2913
|
-
};
|
|
2914
|
-
}
|
|
2915
|
-
return {
|
|
2916
|
-
enabled: true,
|
|
2917
|
-
mode,
|
|
2918
|
-
command: value,
|
|
2919
|
-
hint,
|
|
2920
|
-
...(options.cursorMode ? { cursorMode: options.cursorMode } : {}),
|
|
2921
|
-
...(options.initialLookback ? { initialLookback: options.initialLookback } : {}),
|
|
2922
|
-
};
|
|
2923
|
-
}
|
|
2924
2888
|
function printCadencePlan(cadences) {
|
|
2925
2889
|
process.stdout.write('\nDefault growth cadence:\n');
|
|
2926
2890
|
for (const cadence of cadences) {
|
|
@@ -2930,16 +2894,28 @@ function printCadencePlan(cadences) {
|
|
|
2930
2894
|
process.stdout.write('\n');
|
|
2931
2895
|
}
|
|
2932
2896
|
async function askToolUsage(rl) {
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2897
|
+
return await askMenuChoice(rl, {
|
|
2898
|
+
title: 'How should OpenClaw Growth Engineer run?',
|
|
2899
|
+
subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
|
|
2900
|
+
defaultValue: 'production_autopilot',
|
|
2901
|
+
options: [
|
|
2902
|
+
{
|
|
2903
|
+
value: 'production_autopilot',
|
|
2904
|
+
label: 'Production autopilot',
|
|
2905
|
+
detail: 'Notify, draft issues/PR handoffs, and analyze on schedule.',
|
|
2906
|
+
},
|
|
2907
|
+
{
|
|
2908
|
+
value: 'advisory',
|
|
2909
|
+
label: 'Advisory only',
|
|
2910
|
+
detail: 'Analyze and write OpenClaw chat summaries; no GitHub artifacts by default.',
|
|
2911
|
+
},
|
|
2912
|
+
{
|
|
2913
|
+
value: 'manual_reports',
|
|
2914
|
+
label: 'Manual reports',
|
|
2915
|
+
detail: 'Mostly one-off runs with conservative scheduling.',
|
|
2916
|
+
},
|
|
2917
|
+
],
|
|
2918
|
+
});
|
|
2943
2919
|
}
|
|
2944
2920
|
async function askCadencePlan(rl) {
|
|
2945
2921
|
const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence }));
|
|
@@ -2964,27 +2940,45 @@ async function askCadencePlan(rl) {
|
|
|
2964
2940
|
return cadences;
|
|
2965
2941
|
}
|
|
2966
2942
|
async function askWizardGoal(rl) {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2943
|
+
return await askMenuChoice(rl, {
|
|
2944
|
+
title: 'What do you want to configure?',
|
|
2945
|
+
subtitle: 'Use Up/Down to move, Enter to continue, or press 1-4.',
|
|
2946
|
+
defaultValue: 'full',
|
|
2947
|
+
renderHeader: printWizardHeader,
|
|
2948
|
+
options: [
|
|
2949
|
+
{
|
|
2950
|
+
value: 'connectors',
|
|
2951
|
+
label: 'Connectors',
|
|
2952
|
+
detail: 'Credentials, provider setup, and health checks.',
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
value: 'outputs_intervals',
|
|
2956
|
+
label: 'Outputs and intervals',
|
|
2957
|
+
detail: 'Daily/weekly/monthly jobs, GitHub issue/PR delivery, and OpenClaw chat notifications.',
|
|
2958
|
+
},
|
|
2959
|
+
{
|
|
2960
|
+
value: 'full',
|
|
2961
|
+
label: 'Full setup',
|
|
2962
|
+
detail: 'Project, connectors, outputs, intervals, and sources.',
|
|
2963
|
+
},
|
|
2964
|
+
{
|
|
2965
|
+
value: 'intervals',
|
|
2966
|
+
label: 'Advanced intervals only',
|
|
2967
|
+
detail: 'Runner wake-up interval and connector health check cadence.',
|
|
2968
|
+
},
|
|
2969
|
+
],
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
function printWizardHeader() {
|
|
2973
|
+
process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
|
|
2974
|
+
process.stdout.write('This wizard can configure connector secrets. Normal config is written to config JSON; API keys stay in the local chmod 600 secrets file.\n\n');
|
|
2980
2975
|
}
|
|
2981
2976
|
async function buildDefaultWizardConfig() {
|
|
2982
|
-
const detectedRepo = await detectGitHubRepo();
|
|
2983
2977
|
return {
|
|
2984
2978
|
version: 7,
|
|
2985
2979
|
generatedAt: new Date().toISOString(),
|
|
2986
2980
|
project: {
|
|
2987
|
-
githubRepo:
|
|
2981
|
+
githubRepo: '',
|
|
2988
2982
|
repoRoot: '.',
|
|
2989
2983
|
outFile: 'data/openclaw-growth-engineer/issues.generated.json',
|
|
2990
2984
|
maxIssues: 4,
|
|
@@ -2995,17 +2989,17 @@ async function buildDefaultWizardConfig() {
|
|
|
2995
2989
|
analytics: {
|
|
2996
2990
|
enabled: true,
|
|
2997
2991
|
mode: 'command',
|
|
2998
|
-
command:
|
|
2992
|
+
command: getWizardDefaultSourceCommand('analytics'),
|
|
2999
2993
|
},
|
|
3000
2994
|
revenuecat: {
|
|
3001
2995
|
enabled: false,
|
|
3002
2996
|
mode: 'command',
|
|
3003
|
-
command:
|
|
2997
|
+
command: getWizardDefaultSourceCommand('revenuecat'),
|
|
3004
2998
|
},
|
|
3005
2999
|
sentry: {
|
|
3006
|
-
enabled:
|
|
3000
|
+
enabled: true,
|
|
3007
3001
|
mode: 'command',
|
|
3008
|
-
command:
|
|
3002
|
+
command: getWizardDefaultSourceCommand('sentry'),
|
|
3009
3003
|
},
|
|
3010
3004
|
feedback: {
|
|
3011
3005
|
enabled: true,
|
|
@@ -3015,12 +3009,12 @@ async function buildDefaultWizardConfig() {
|
|
|
3015
3009
|
initialLookback: '30d',
|
|
3016
3010
|
},
|
|
3017
3011
|
extra: [
|
|
3018
|
-
buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command:
|
|
3012
|
+
buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
|
|
3019
3013
|
],
|
|
3020
3014
|
},
|
|
3021
3015
|
schedule: {
|
|
3022
|
-
intervalMinutes:
|
|
3023
|
-
connectorHealthCheckIntervalMinutes:
|
|
3016
|
+
intervalMinutes: DEFAULT_GROWTH_INTERVAL_MINUTES,
|
|
3017
|
+
connectorHealthCheckIntervalMinutes: DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES,
|
|
3024
3018
|
skipIfNoDataChange: true,
|
|
3025
3019
|
skipIfIssueSetUnchanged: true,
|
|
3026
3020
|
cadences: DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence })),
|
|
@@ -3028,6 +3022,8 @@ async function buildDefaultWizardConfig() {
|
|
|
3028
3022
|
actions: {
|
|
3029
3023
|
autoCreateIssues: false,
|
|
3030
3024
|
autoCreatePullRequests: false,
|
|
3025
|
+
autoCreateWhenGitHubWriteAccess: true,
|
|
3026
|
+
disableAutoCreateGitHubArtifacts: false,
|
|
3031
3027
|
mode: 'issue',
|
|
3032
3028
|
usageMode: 'production_autopilot',
|
|
3033
3029
|
draftPullRequests: true,
|
|
@@ -3101,10 +3097,127 @@ async function buildDefaultWizardConfig() {
|
|
|
3101
3097
|
},
|
|
3102
3098
|
};
|
|
3103
3099
|
}
|
|
3100
|
+
function buildRecommendedSourceConfig() {
|
|
3101
|
+
return {
|
|
3102
|
+
analytics: {
|
|
3103
|
+
enabled: true,
|
|
3104
|
+
mode: 'command',
|
|
3105
|
+
command: getWizardDefaultSourceCommand('analytics'),
|
|
3106
|
+
},
|
|
3107
|
+
revenuecat: {
|
|
3108
|
+
enabled: false,
|
|
3109
|
+
mode: 'command',
|
|
3110
|
+
command: getWizardDefaultSourceCommand('revenuecat'),
|
|
3111
|
+
},
|
|
3112
|
+
sentry: {
|
|
3113
|
+
enabled: true,
|
|
3114
|
+
mode: 'command',
|
|
3115
|
+
command: getWizardDefaultSourceCommand('sentry'),
|
|
3116
|
+
},
|
|
3117
|
+
feedback: {
|
|
3118
|
+
enabled: true,
|
|
3119
|
+
mode: 'command',
|
|
3120
|
+
command: getDefaultSourceCommand('feedback'),
|
|
3121
|
+
cursorMode: 'auto_since_last_fetch',
|
|
3122
|
+
initialLookback: '30d',
|
|
3123
|
+
},
|
|
3124
|
+
extra: [
|
|
3125
|
+
buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
|
|
3126
|
+
],
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
function getInputChannelInitialSelection(config) {
|
|
3130
|
+
const sources = config?.sources || {};
|
|
3131
|
+
const extraSources = Array.isArray(sources.extra) ? sources.extra : [];
|
|
3132
|
+
const selected = new Set();
|
|
3133
|
+
const hasExplicitSources = Boolean(config?.sources);
|
|
3134
|
+
if (!hasExplicitSources || sources.analytics?.enabled !== false)
|
|
3135
|
+
selected.add('analytics');
|
|
3136
|
+
if (sources.revenuecat?.enabled === true || isConnectorLocallyConfigured('revenuecat'))
|
|
3137
|
+
selected.add('revenuecat');
|
|
3138
|
+
if (!hasExplicitSources || sources.sentry?.enabled !== false)
|
|
3139
|
+
selected.add('sentry');
|
|
3140
|
+
if (extraSources.some((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()) &&
|
|
3141
|
+
source?.enabled !== false) ||
|
|
3142
|
+
isConnectorLocallyConfigured('asc')) {
|
|
3143
|
+
selected.add('asc');
|
|
3144
|
+
}
|
|
3145
|
+
if (config?.deliveries?.github?.enabled ||
|
|
3146
|
+
config?.actions?.autoCreateIssues ||
|
|
3147
|
+
config?.actions?.autoCreatePullRequests ||
|
|
3148
|
+
isConnectorLocallyConfigured('github')) {
|
|
3149
|
+
selected.add('github');
|
|
3150
|
+
}
|
|
3151
|
+
return orderConnectors([...selected]);
|
|
3152
|
+
}
|
|
3153
|
+
function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}) {
|
|
3154
|
+
const selected = new Set(selectedConnectors);
|
|
3155
|
+
const recommended = buildRecommendedSourceConfig();
|
|
3156
|
+
const migratedSources = migrateRuntimeSourceCommands({ sources: existingSources }).sources || {};
|
|
3157
|
+
const existingExtra = Array.isArray(migratedSources.extra) ? migratedSources.extra : [];
|
|
3158
|
+
const ascSource = existingExtra.find((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()));
|
|
3159
|
+
const nonAscExtra = existingExtra.filter((source) => source !== ascSource);
|
|
3160
|
+
return {
|
|
3161
|
+
...recommended,
|
|
3162
|
+
...migratedSources,
|
|
3163
|
+
analytics: {
|
|
3164
|
+
...recommended.analytics,
|
|
3165
|
+
...(migratedSources.analytics || {}),
|
|
3166
|
+
command: normalizeWizardSourceCommand('analytics', {
|
|
3167
|
+
...recommended.analytics,
|
|
3168
|
+
...(migratedSources.analytics || {}),
|
|
3169
|
+
}),
|
|
3170
|
+
enabled: selected.has('analytics'),
|
|
3171
|
+
},
|
|
3172
|
+
revenuecat: {
|
|
3173
|
+
...recommended.revenuecat,
|
|
3174
|
+
...(migratedSources.revenuecat || {}),
|
|
3175
|
+
command: normalizeWizardSourceCommand('revenuecat', {
|
|
3176
|
+
...recommended.revenuecat,
|
|
3177
|
+
...(migratedSources.revenuecat || {}),
|
|
3178
|
+
}),
|
|
3179
|
+
enabled: selected.has('revenuecat'),
|
|
3180
|
+
},
|
|
3181
|
+
sentry: {
|
|
3182
|
+
...recommended.sentry,
|
|
3183
|
+
...(migratedSources.sentry || {}),
|
|
3184
|
+
command: normalizeWizardSourceCommand('sentry', {
|
|
3185
|
+
...recommended.sentry,
|
|
3186
|
+
...(migratedSources.sentry || {}),
|
|
3187
|
+
}),
|
|
3188
|
+
enabled: selected.has('sentry'),
|
|
3189
|
+
},
|
|
3190
|
+
feedback: {
|
|
3191
|
+
...recommended.feedback,
|
|
3192
|
+
...(migratedSources.feedback || {}),
|
|
3193
|
+
enabled: selected.has('analytics'),
|
|
3194
|
+
},
|
|
3195
|
+
extra: [
|
|
3196
|
+
...nonAscExtra,
|
|
3197
|
+
{
|
|
3198
|
+
...buildExtraSourceConfig('asc-cli', {
|
|
3199
|
+
enabled: selected.has('asc'),
|
|
3200
|
+
mode: 'command',
|
|
3201
|
+
command: getWizardDefaultSourceCommand('asc'),
|
|
3202
|
+
}),
|
|
3203
|
+
...(ascSource || {}),
|
|
3204
|
+
command: normalizeWizardSourceCommand('asc', {
|
|
3205
|
+
...buildExtraSourceConfig('asc-cli', {
|
|
3206
|
+
enabled: selected.has('asc'),
|
|
3207
|
+
mode: 'command',
|
|
3208
|
+
command: getWizardDefaultSourceCommand('asc'),
|
|
3209
|
+
}),
|
|
3210
|
+
...(ascSource || {}),
|
|
3211
|
+
}),
|
|
3212
|
+
enabled: selected.has('asc'),
|
|
3213
|
+
},
|
|
3214
|
+
],
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3104
3217
|
async function loadEditableConfig(configPath) {
|
|
3105
3218
|
const existing = await readJsonIfPresent(configPath).catch(() => null);
|
|
3106
3219
|
if (existing && typeof existing === 'object')
|
|
3107
|
-
return existing;
|
|
3220
|
+
return migrateRuntimeSourceCommands(existing);
|
|
3108
3221
|
return await buildDefaultWizardConfig();
|
|
3109
3222
|
}
|
|
3110
3223
|
function mergeNotificationChannels(baseChannels, extraChannels) {
|
|
@@ -3148,28 +3261,39 @@ async function askNotificationChannels(rl, config) {
|
|
|
3148
3261
|
return channels;
|
|
3149
3262
|
}
|
|
3150
3263
|
async function askOutputConfig(rl, config) {
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3264
|
+
printSection('Outputs and notifications', [
|
|
3265
|
+
'OpenClaw chat is always enabled so the agent has a readable handoff.',
|
|
3266
|
+
'GitHub issues or draft PRs are optional and only run when a token plus an inferred repo are available.',
|
|
3267
|
+
]);
|
|
3155
3268
|
const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
|
|
3156
3269
|
const currentAutoCreate = Boolean(config?.actions?.autoCreateIssues || config?.actions?.autoCreatePullRequests || config?.deliveries?.github?.autoCreate);
|
|
3157
|
-
const
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3270
|
+
const outputChoice = await askMenuChoice(rl, {
|
|
3271
|
+
title: 'Output mode',
|
|
3272
|
+
subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
|
|
3273
|
+
defaultValue: currentAutoCreate ? (currentMode === 'pull_request' ? 'pull_request' : 'issue') : 'chat',
|
|
3274
|
+
options: [
|
|
3275
|
+
{
|
|
3276
|
+
value: 'chat',
|
|
3277
|
+
label: 'OpenClaw chat',
|
|
3278
|
+
detail: 'Write readable summaries and leave GitHub as runtime fallback.',
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
value: 'issue',
|
|
3282
|
+
label: 'GitHub issues',
|
|
3283
|
+
detail: 'Auto-create issues for concrete findings when GitHub access allows it.',
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
value: 'pull_request',
|
|
3287
|
+
label: 'Draft PR proposals',
|
|
3288
|
+
detail: 'Auto-create draft PR-oriented proposal branches for implementation-ready fixes.',
|
|
3289
|
+
},
|
|
3290
|
+
],
|
|
3291
|
+
});
|
|
3292
|
+
const summaryOnly = outputChoice === 'chat';
|
|
3293
|
+
const mode = outputChoice === 'pull_request' ? 'pull_request' : 'issue';
|
|
3294
|
+
const autoCreate = !summaryOnly;
|
|
3166
3295
|
if (!summaryOnly) {
|
|
3167
|
-
|
|
3168
|
-
const currentRepo = config?.project?.githubRepo || detectedRepo || '';
|
|
3169
|
-
config.project = {
|
|
3170
|
-
...(config.project || {}),
|
|
3171
|
-
githubRepo: await ask(rl, 'GitHub repo for issue/PR delivery (owner/name)', currentRepo),
|
|
3172
|
-
};
|
|
3296
|
+
process.stdout.write('GitHub repo scope is not pinned by the wizard; OpenClaw/Hermes will infer it from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context when creating issues/PRs.\n');
|
|
3173
3297
|
}
|
|
3174
3298
|
const channels = await askNotificationChannels(rl, config);
|
|
3175
3299
|
const connectorHealthChannels = channels.map((channel) => {
|
|
@@ -3186,6 +3310,8 @@ async function askOutputConfig(rl, config) {
|
|
|
3186
3310
|
mode,
|
|
3187
3311
|
autoCreateIssues: mode === 'issue' && autoCreate,
|
|
3188
3312
|
autoCreatePullRequests: mode === 'pull_request' && autoCreate,
|
|
3313
|
+
autoCreateWhenGitHubWriteAccess: config.actions?.autoCreateWhenGitHubWriteAccess !== false,
|
|
3314
|
+
disableAutoCreateGitHubArtifacts: config.actions?.disableAutoCreateGitHubArtifacts === true,
|
|
3189
3315
|
draftPullRequests: true,
|
|
3190
3316
|
proposalBranchPrefix: config?.actions?.proposalBranchPrefix || 'openclaw/proposals',
|
|
3191
3317
|
};
|
|
@@ -3238,11 +3364,61 @@ async function askOutputConfig(rl, config) {
|
|
|
3238
3364
|
};
|
|
3239
3365
|
return config;
|
|
3240
3366
|
}
|
|
3367
|
+
async function askGitHubArtifactDetails(rl, config) {
|
|
3368
|
+
const githubEnabled = Boolean(config?.actions?.autoCreateIssues ||
|
|
3369
|
+
config?.actions?.autoCreatePullRequests ||
|
|
3370
|
+
config?.deliveries?.github?.enabled ||
|
|
3371
|
+
config?.deliveries?.github?.autoCreate);
|
|
3372
|
+
config.project = {
|
|
3373
|
+
...(config.project || {}),
|
|
3374
|
+
githubRepo: '',
|
|
3375
|
+
repoRoot: config.project?.repoRoot || '.',
|
|
3376
|
+
outFile: config.project?.outFile || 'data/openclaw-growth-engineer/issues.generated.json',
|
|
3377
|
+
maxIssues: Number(config.project?.maxIssues || 4),
|
|
3378
|
+
titlePrefix: config.project?.titlePrefix || '[Growth]',
|
|
3379
|
+
labels: Array.isArray(config.project?.labels) && config.project.labels.length > 0
|
|
3380
|
+
? config.project.labels
|
|
3381
|
+
: ['ai-growth', 'autogenerated', 'product'],
|
|
3382
|
+
};
|
|
3383
|
+
if (!githubEnabled) {
|
|
3384
|
+
return config;
|
|
3385
|
+
}
|
|
3386
|
+
process.stdout.write('\nGitHub repo scope is not pinned by the wizard. OpenClaw/Hermes infers it from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context.\n');
|
|
3387
|
+
const customize = await askYesNo(rl, 'Customize GitHub issue/PR limits, labels, or chart attachment settings?', false);
|
|
3388
|
+
if (!customize) {
|
|
3389
|
+
config.charting = {
|
|
3390
|
+
...(config.charting || {}),
|
|
3391
|
+
enabled: config.charting?.enabled === true,
|
|
3392
|
+
command: config.charting?.command || null,
|
|
3393
|
+
};
|
|
3394
|
+
return config;
|
|
3395
|
+
}
|
|
3396
|
+
const labelsRaw = await ask(rl, 'GitHub labels for created issues/PRs', config.project.labels.join(','));
|
|
3397
|
+
config.project.labels = labelsRaw
|
|
3398
|
+
.split(',')
|
|
3399
|
+
.map((value) => value.trim())
|
|
3400
|
+
.filter(Boolean);
|
|
3401
|
+
config.project.maxIssues = Number.parseInt(await ask(rl, 'Maximum GitHub artifacts per run', String(config.project.maxIssues || 4)), 10) || 4;
|
|
3402
|
+
config.project.titlePrefix = await ask(rl, 'GitHub artifact title prefix', config.project.titlePrefix || '[Growth]');
|
|
3403
|
+
const enableCharting = await askYesNo(rl, 'Attach generated charts to GitHub artifacts when useful?', config.charting?.enabled === true);
|
|
3404
|
+
config.charting = {
|
|
3405
|
+
...(config.charting || {}),
|
|
3406
|
+
enabled: enableCharting,
|
|
3407
|
+
command: enableCharting
|
|
3408
|
+
? await ask(rl, 'Optional chart command override', config.charting?.command || '')
|
|
3409
|
+
: null,
|
|
3410
|
+
};
|
|
3411
|
+
return config;
|
|
3412
|
+
}
|
|
3241
3413
|
async function askIntervalConfig(rl, config) {
|
|
3414
|
+
printSection('Schedule and analysis depth', [
|
|
3415
|
+
'The runner wakes up often, but larger reviews only run on their daily/weekly/monthly cadence.',
|
|
3416
|
+
'Connector health checks are separate and default to every 6 hours.',
|
|
3417
|
+
]);
|
|
3242
3418
|
const currentSchedule = config?.schedule || {};
|
|
3243
|
-
const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(currentSchedule.intervalMinutes || 1440)), 10) || 1440;
|
|
3244
|
-
const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || 720)), 10) || 720;
|
|
3245
3419
|
const usageMode = await askToolUsage(rl);
|
|
3420
|
+
const intervalMinutes = Number.parseInt(await ask(rl, 'Growth runner wake-up interval in minutes', String(currentSchedule.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES)), 10) || DEFAULT_GROWTH_INTERVAL_MINUTES;
|
|
3421
|
+
const connectorHealthCheckIntervalMinutes = Number.parseInt(await ask(rl, 'Connector health check interval in minutes', String(currentSchedule.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES)), 10) || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES;
|
|
3246
3422
|
const cadences = await askCadencePlan(rl);
|
|
3247
3423
|
config.schedule = {
|
|
3248
3424
|
...currentSchedule,
|
|
@@ -3258,11 +3434,31 @@ async function askIntervalConfig(rl, config) {
|
|
|
3258
3434
|
};
|
|
3259
3435
|
return config;
|
|
3260
3436
|
}
|
|
3437
|
+
async function askOutputsAndIntervalsConfig(rl, config) {
|
|
3438
|
+
const withIntervals = await askIntervalConfig(rl, config);
|
|
3439
|
+
const withOutput = await askOutputConfig(rl, withIntervals);
|
|
3440
|
+
return await askGitHubArtifactDetails(rl, withOutput);
|
|
3441
|
+
}
|
|
3442
|
+
async function askInputSourceConfig(rl, config, configPath) {
|
|
3443
|
+
config = migrateRuntimeSourceCommands(config);
|
|
3444
|
+
await ensureDirForFile(configPath);
|
|
3445
|
+
await writeJsonFile(configPath, config);
|
|
3446
|
+
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress));
|
|
3447
|
+
const selected = await askConnectorSelectionWithHealth(rl, healthByConnector, getInputChannelInitialSelection(config), {
|
|
3448
|
+
introTitle: 'Input channels',
|
|
3449
|
+
introDetail: null,
|
|
3450
|
+
actionTitle: 'Select input channels',
|
|
3451
|
+
helpText: 'Use Up/Down to move, Space to toggle channels, A to toggle all channels, Enter to continue.',
|
|
3452
|
+
mode: 'input',
|
|
3453
|
+
});
|
|
3454
|
+
config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {});
|
|
3455
|
+
return { config, selected, healthByConnector };
|
|
3456
|
+
}
|
|
3261
3457
|
async function writeOpenClawJobManifest(configPath, config) {
|
|
3262
3458
|
const manifestPath = path.resolve('.openclaw/jobs/openclaw-growth-engineer.json');
|
|
3263
3459
|
const displayConfigPath = path.relative(process.cwd(), configPath) || configPath;
|
|
3264
|
-
const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes ||
|
|
3265
|
-
const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes ||
|
|
3460
|
+
const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES));
|
|
3461
|
+
const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES));
|
|
3266
3462
|
const actionMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
|
|
3267
3463
|
const growthRunCommand = getGrowthRunCommand(config, displayConfigPath);
|
|
3268
3464
|
const connectorHealthCommand = getConnectorHealthCommand(config, displayConfigPath);
|
|
@@ -3317,8 +3513,7 @@ async function main() {
|
|
|
3317
3513
|
}
|
|
3318
3514
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3319
3515
|
try {
|
|
3320
|
-
|
|
3321
|
-
process.stdout.write('This wizard writes non-secret config only. Connector secrets stay in the local secrets file.\n\n');
|
|
3516
|
+
printWizardHeader();
|
|
3322
3517
|
const goal = await askWizardGoal(rl);
|
|
3323
3518
|
if (goal === 'connectors') {
|
|
3324
3519
|
rl.close();
|
|
@@ -3336,160 +3531,40 @@ async function main() {
|
|
|
3336
3531
|
process.stdout.write('OpenClaw can run and update growth jobs plus non-secret connector config from the manifest; connector API keys stay behind the connector wizard.\n');
|
|
3337
3532
|
return;
|
|
3338
3533
|
}
|
|
3339
|
-
if (goal === '
|
|
3340
|
-
const config = await
|
|
3534
|
+
if (goal === 'outputs_intervals') {
|
|
3535
|
+
const config = await askOutputsAndIntervalsConfig(rl, await loadEditableConfig(configPath));
|
|
3341
3536
|
const secretAccess = await askSecretAccessModel(rl, configPath, config);
|
|
3342
3537
|
await writeJsonFile(configPath, config);
|
|
3343
3538
|
const manifestPath = await writeOpenClawJobManifest(configPath, config);
|
|
3344
|
-
process.stdout.write(`\nSaved output config: ${configPath}\n`);
|
|
3539
|
+
process.stdout.write(`\nSaved output and interval config: ${configPath}\n`);
|
|
3345
3540
|
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
3346
3541
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3347
|
-
process.stdout.write('
|
|
3542
|
+
process.stdout.write('Daily checks prioritize Sentry and production anomalies; larger cadences analyze all configured projects and connectors.\n');
|
|
3348
3543
|
return;
|
|
3349
3544
|
}
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
const
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
const
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
forceEnabled: true,
|
|
3364
|
-
defaultCommand: getDefaultSourceCommand('analytics'),
|
|
3365
|
-
});
|
|
3366
|
-
const revenuecat = await askSourceConfig(rl, 'revenuecat', 'data/openclaw-growth-engineer/revenuecat_summary.example.json', getDefaultSourceHint('revenuecat'));
|
|
3367
|
-
const sentry = await askSourceConfig(rl, 'sentry', 'data/openclaw-growth-engineer/sentry_summary.example.json', getDefaultSourceHint('sentry'));
|
|
3368
|
-
const feedback = await askSourceConfig(rl, 'feedback', 'data/openclaw-growth-engineer/feedback_summary.example.json', getDefaultSourceHint('feedback'), {
|
|
3369
|
-
defaultEnabled: true,
|
|
3370
|
-
defaultCommand: getDefaultSourceCommand('feedback'),
|
|
3371
|
-
cursorMode: 'auto_since_last_fetch',
|
|
3372
|
-
initialLookback: '30d',
|
|
3373
|
-
});
|
|
3374
|
-
const extraSourcesRaw = await ask(rl, 'Extra connectors (comma-separated, e.g. firebase-crashlytics,app-store-reviews,play-console)', '');
|
|
3375
|
-
const extraSources = extraSourcesRaw
|
|
3376
|
-
.split(',')
|
|
3377
|
-
.map((value) => value.trim())
|
|
3378
|
-
.filter(Boolean)
|
|
3379
|
-
.map((service) => {
|
|
3380
|
-
const defaultCommand = getDefaultSourceCommand(service);
|
|
3381
|
-
return buildExtraSourceConfig(service, defaultCommand ? {} : { mode: 'file', path: getDefaultSourcePath(service) });
|
|
3545
|
+
let config = await loadEditableConfig(configPath);
|
|
3546
|
+
config.version = Number(config.version || 7);
|
|
3547
|
+
config.generatedAt = new Date().toISOString();
|
|
3548
|
+
const inputSetup = await askInputSourceConfig(rl, config, configPath);
|
|
3549
|
+
config = inputSetup.config;
|
|
3550
|
+
await ensureDirForFile(configPath);
|
|
3551
|
+
await writeJsonFile(configPath, config);
|
|
3552
|
+
const connectorsOk = await runConnectorSetupSteps({
|
|
3553
|
+
rl,
|
|
3554
|
+
args: { ...args, config: configPath },
|
|
3555
|
+
selected: inputSetup.selected,
|
|
3556
|
+
healthByConnector: inputSetup.healthByConnector,
|
|
3557
|
+
allowIsolationPrompt: false,
|
|
3382
3558
|
});
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
: '';
|
|
3393
|
-
const config = {
|
|
3394
|
-
version: 1,
|
|
3395
|
-
generatedAt: new Date().toISOString(),
|
|
3396
|
-
project: {
|
|
3397
|
-
githubRepo,
|
|
3398
|
-
repoRoot: '.',
|
|
3399
|
-
outFile: 'data/openclaw-growth-engineer/issues.generated.json',
|
|
3400
|
-
maxIssues,
|
|
3401
|
-
titlePrefix: '[Growth]',
|
|
3402
|
-
labels,
|
|
3403
|
-
},
|
|
3404
|
-
sources: {
|
|
3405
|
-
analytics,
|
|
3406
|
-
revenuecat,
|
|
3407
|
-
sentry,
|
|
3408
|
-
feedback,
|
|
3409
|
-
extra: extraSources,
|
|
3410
|
-
},
|
|
3411
|
-
schedule: {
|
|
3412
|
-
intervalMinutes,
|
|
3413
|
-
connectorHealthCheckIntervalMinutes: 720,
|
|
3414
|
-
skipIfNoDataChange: true,
|
|
3415
|
-
skipIfIssueSetUnchanged: true,
|
|
3416
|
-
cadences,
|
|
3417
|
-
},
|
|
3418
|
-
actions: {
|
|
3419
|
-
autoCreateIssues,
|
|
3420
|
-
autoCreatePullRequests,
|
|
3421
|
-
mode: actionMode,
|
|
3422
|
-
usageMode,
|
|
3423
|
-
draftPullRequests: true,
|
|
3424
|
-
proposalBranchPrefix: 'openclaw/proposals',
|
|
3425
|
-
},
|
|
3426
|
-
deliveries: {
|
|
3427
|
-
openclawChat: {
|
|
3428
|
-
enabled: true,
|
|
3429
|
-
markdownPath: '.openclaw/chat/latest.md',
|
|
3430
|
-
jsonPath: '.openclaw/chat/latest.json',
|
|
3431
|
-
},
|
|
3432
|
-
github: {
|
|
3433
|
-
enabled: autoCreateIssues || autoCreatePullRequests,
|
|
3434
|
-
mode: actionMode,
|
|
3435
|
-
autoCreate: autoCreateIssues || autoCreatePullRequests,
|
|
3436
|
-
draftPullRequests: true,
|
|
3437
|
-
proposalBranchPrefix: 'openclaw/proposals',
|
|
3438
|
-
},
|
|
3439
|
-
slack: {
|
|
3440
|
-
enabled: false,
|
|
3441
|
-
webhookEnv: 'SLACK_WEBHOOK_URL',
|
|
3442
|
-
},
|
|
3443
|
-
webhook: {
|
|
3444
|
-
enabled: false,
|
|
3445
|
-
urlEnv: 'OPENCLAW_WEBHOOK_URL',
|
|
3446
|
-
method: 'POST',
|
|
3447
|
-
headers: {},
|
|
3448
|
-
},
|
|
3449
|
-
discord: {
|
|
3450
|
-
enabled: false,
|
|
3451
|
-
command: 'node scripts/discord-openclaw-bridge.mjs send --stdin',
|
|
3452
|
-
},
|
|
3453
|
-
},
|
|
3454
|
-
charting: {
|
|
3455
|
-
enabled: enableCharting,
|
|
3456
|
-
command: chartCommand || null,
|
|
3457
|
-
},
|
|
3458
|
-
notifications: {
|
|
3459
|
-
connectorHealth: {
|
|
3460
|
-
enabled: true,
|
|
3461
|
-
channels: [
|
|
3462
|
-
{
|
|
3463
|
-
type: 'openclaw-chat',
|
|
3464
|
-
enabled: true,
|
|
3465
|
-
markdownPath: '.openclaw/chat/connector-health.md',
|
|
3466
|
-
jsonPath: '.openclaw/chat/connector-health.json',
|
|
3467
|
-
},
|
|
3468
|
-
],
|
|
3469
|
-
},
|
|
3470
|
-
growthRun: {
|
|
3471
|
-
enabled: true,
|
|
3472
|
-
channels: [
|
|
3473
|
-
{
|
|
3474
|
-
type: 'openclaw-chat',
|
|
3475
|
-
enabled: true,
|
|
3476
|
-
markdownPath: '.openclaw/chat/growth-summary.md',
|
|
3477
|
-
jsonPath: '.openclaw/chat/growth-summary.json',
|
|
3478
|
-
},
|
|
3479
|
-
],
|
|
3480
|
-
},
|
|
3481
|
-
},
|
|
3482
|
-
secrets: {
|
|
3483
|
-
githubTokenEnv: 'GITHUB_TOKEN',
|
|
3484
|
-
githubTokenRef: { source: 'env', provider: 'default', id: 'GITHUB_TOKEN' },
|
|
3485
|
-
analyticsTokenEnv: 'ANALYTICSCLI_ACCESS_TOKEN',
|
|
3486
|
-
analyticsTokenRef: { source: 'env', provider: 'default', id: 'ANALYTICSCLI_ACCESS_TOKEN' },
|
|
3487
|
-
revenuecatTokenEnv: 'REVENUECAT_API_KEY',
|
|
3488
|
-
revenuecatTokenRef: { source: 'env', provider: 'default', id: 'REVENUECAT_API_KEY' },
|
|
3489
|
-
sentryTokenEnv: 'SENTRY_AUTH_TOKEN',
|
|
3490
|
-
sentryTokenRef: { source: 'env', provider: 'default', id: 'SENTRY_AUTH_TOKEN' },
|
|
3491
|
-
},
|
|
3492
|
-
};
|
|
3559
|
+
if (!connectorsOk) {
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
config = await loadEditableConfig(configPath);
|
|
3563
|
+
config.version = Number(config.version || 7);
|
|
3564
|
+
config.generatedAt = new Date().toISOString();
|
|
3565
|
+
config = await askIntervalConfig(rl, config);
|
|
3566
|
+
config = await askOutputConfig(rl, config);
|
|
3567
|
+
config = await askGitHubArtifactDetails(rl, config);
|
|
3493
3568
|
const secretAccess = await askSecretAccessModel(rl, configPath, config);
|
|
3494
3569
|
await ensureDirForFile(configPath);
|
|
3495
3570
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
@@ -3499,14 +3574,8 @@ async function main() {
|
|
|
3499
3574
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3500
3575
|
process.stdout.write('\nNext steps:\n');
|
|
3501
3576
|
process.stdout.write(`1) Set secrets in OpenClaw secret store (env var names in config.secrets)\n`);
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
process.stdout.write(`3) Run once: node scripts/openclaw-growth-runner.mjs --config ${configPath}\n`);
|
|
3505
|
-
process.stdout.write(`4) Run interval loop: node scripts/openclaw-growth-runner.mjs --config ${configPath} --loop\n`);
|
|
3506
|
-
return;
|
|
3507
|
-
}
|
|
3508
|
-
process.stdout.write(`2) Run once: node scripts/openclaw-growth-runner.mjs --config ${configPath}\n`);
|
|
3509
|
-
process.stdout.write(`3) Run interval loop: node scripts/openclaw-growth-runner.mjs --config ${configPath} --loop\n`);
|
|
3577
|
+
process.stdout.write(`2) Run once: ${growthEngineerPackageCommand(`run --config ${quote(configPath)}`)}\n`);
|
|
3578
|
+
process.stdout.write(`3) Run interval loop: ${growthEngineerPackageCommand(`run --config ${quote(configPath)} --loop`)}\n`);
|
|
3510
3579
|
}
|
|
3511
3580
|
finally {
|
|
3512
3581
|
rl.close();
|
|
@@ -3514,6 +3583,6 @@ async function main() {
|
|
|
3514
3583
|
}
|
|
3515
3584
|
main().catch((error) => {
|
|
3516
3585
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
3517
|
-
process.exitCode = 1;
|
|
3586
|
+
process.exitCode = error instanceof WizardAbortError ? error.exitCode : 1;
|
|
3518
3587
|
});
|
|
3519
3588
|
//# sourceMappingURL=openclaw-growth-wizard.mjs.map
|