@analyticscli/growth-engineer 0.1.0-preview.1 → 0.1.0-preview.11
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 +100 -46
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +2 -1
- package/dist/runtime/openclaw-growth-shared.mjs +32 -2
- package/dist/runtime/openclaw-growth-shared.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 +973 -676
- 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,281 @@ 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
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function askMultiChoice(rl, { title, subtitle = 'Use Up/Down to move, Space to toggle, Enter to continue.', options, defaultValues, requiredValues = [], minSelections = 1, renderHeader, }) {
|
|
466
|
+
const required = new Set(requiredValues);
|
|
467
|
+
const normalizeSelection = (values) => {
|
|
468
|
+
const selected = new Set(values);
|
|
469
|
+
requiredValues.forEach((value) => selected.add(value));
|
|
470
|
+
return options.map((option) => option.value).filter((value) => selected.has(value));
|
|
471
|
+
};
|
|
472
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
473
|
+
process.stdout.write(`\n${title}\n`);
|
|
474
|
+
options.forEach((option, index) => {
|
|
475
|
+
const checked = defaultValues.includes(option.value) || required.has(option.value) ? 'x' : ' ';
|
|
476
|
+
const requiredLabel = required.has(option.value) ? ' required' : '';
|
|
477
|
+
process.stdout.write(` ${index + 1}) [${checked}] ${option.label}${requiredLabel}: ${option.detail}\n`);
|
|
478
|
+
});
|
|
479
|
+
const answer = await ask(rl, `Select one or more (comma-separated 1-${options.length})`, normalizeSelection(defaultValues).map((value) => String(options.findIndex((option) => option.value === value) + 1)).join(','));
|
|
480
|
+
const selected = answer
|
|
481
|
+
.split(',')
|
|
482
|
+
.map((value) => Number.parseInt(value.trim(), 10) - 1)
|
|
483
|
+
.filter((index) => options[index])
|
|
484
|
+
.map((index) => options[index].value);
|
|
485
|
+
const normalized = normalizeSelection(selected);
|
|
486
|
+
return normalized.length >= minSelections ? normalized : normalizeSelection(defaultValues);
|
|
487
|
+
}
|
|
488
|
+
rl.pause();
|
|
489
|
+
let completed = false;
|
|
490
|
+
try {
|
|
491
|
+
const selected = await askMultiChoiceByKeys({
|
|
492
|
+
title,
|
|
493
|
+
subtitle,
|
|
494
|
+
options,
|
|
495
|
+
defaultValues: normalizeSelection(defaultValues),
|
|
496
|
+
requiredValues,
|
|
497
|
+
minSelections,
|
|
498
|
+
renderHeader,
|
|
499
|
+
});
|
|
500
|
+
completed = true;
|
|
501
|
+
return selected;
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
if (completed) {
|
|
505
|
+
rl.resume();
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
process.stdin.pause();
|
|
509
|
+
}
|
|
343
510
|
}
|
|
344
511
|
}
|
|
512
|
+
async function askMultiChoiceByKeys({ title, subtitle, options, defaultValues, requiredValues, minSelections, renderHeader, }) {
|
|
513
|
+
emitKeypressEvents(process.stdin);
|
|
514
|
+
const wasRaw = process.stdin.isRaw;
|
|
515
|
+
const wasPaused = process.stdin.isPaused();
|
|
516
|
+
process.stdin.setRawMode(true);
|
|
517
|
+
process.stdin.resume();
|
|
518
|
+
const required = new Set(requiredValues);
|
|
519
|
+
const selected = new Set(defaultValues);
|
|
520
|
+
requiredValues.forEach((value) => selected.add(value));
|
|
521
|
+
let cursorIndex = 0;
|
|
522
|
+
let warning = '';
|
|
523
|
+
return await new Promise((resolve, reject) => {
|
|
524
|
+
const cleanup = () => {
|
|
525
|
+
process.stdin.off('keypress', onKeypress);
|
|
526
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
527
|
+
if (wasPaused) {
|
|
528
|
+
process.stdin.pause();
|
|
529
|
+
}
|
|
530
|
+
process.stdout.write(ANSI.showCursor);
|
|
531
|
+
};
|
|
532
|
+
const selectedValues = () => options.map((option) => option.value).filter((value) => selected.has(value));
|
|
533
|
+
const render = () => {
|
|
534
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
535
|
+
renderHeader?.();
|
|
536
|
+
process.stdout.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
|
|
537
|
+
process.stdout.write(`${ANSI.dim}${subtitle}${ANSI.reset}\n\n`);
|
|
538
|
+
if (warning) {
|
|
539
|
+
process.stdout.write(`${ANSI.cyan}${warning}${ANSI.reset}\n\n`);
|
|
540
|
+
}
|
|
541
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
542
|
+
const option = options[index];
|
|
543
|
+
const pointer = index === cursorIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';
|
|
544
|
+
const checkbox = selected.has(option.value) ? '[x]' : '[ ]';
|
|
545
|
+
const requiredLabel = required.has(option.value) ? ` ${ANSI.dim}(required)${ANSI.reset}` : '';
|
|
546
|
+
process.stdout.write(`${pointer} ${checkbox} ${index + 1}) ${ANSI.bold}${option.label}${ANSI.reset}${requiredLabel}\n`);
|
|
547
|
+
writeWrapped(option.detail, ' ', ANSI.dim);
|
|
548
|
+
}
|
|
549
|
+
process.stdout.write(`\n${ANSI.dim}Esc/Q cancels. Space toggles, A toggles all optional items, Enter continues. Number keys 1-${options.length} toggle items.${ANSI.reset}\n`);
|
|
550
|
+
};
|
|
551
|
+
const cancel = () => {
|
|
552
|
+
cleanup();
|
|
553
|
+
process.stdout.write('\n');
|
|
554
|
+
reject(new WizardAbortError('Setup cancelled.'));
|
|
555
|
+
};
|
|
556
|
+
const finish = () => {
|
|
557
|
+
const values = selectedValues();
|
|
558
|
+
if (values.length < minSelections) {
|
|
559
|
+
warning = `Select at least ${minSelections} item${minSelections === 1 ? '' : 's'} to continue.`;
|
|
560
|
+
render();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
cleanup();
|
|
564
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
565
|
+
resolve(values);
|
|
566
|
+
};
|
|
567
|
+
const toggleIndex = (index) => {
|
|
568
|
+
const option = options[index];
|
|
569
|
+
if (!option)
|
|
570
|
+
return;
|
|
571
|
+
warning = '';
|
|
572
|
+
if (required.has(option.value)) {
|
|
573
|
+
selected.add(option.value);
|
|
574
|
+
warning = `${option.label} is required.`;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (selected.has(option.value))
|
|
578
|
+
selected.delete(option.value);
|
|
579
|
+
else
|
|
580
|
+
selected.add(option.value);
|
|
581
|
+
requiredValues.forEach((value) => selected.add(value));
|
|
582
|
+
};
|
|
583
|
+
const onKeypress = (_text, key) => {
|
|
584
|
+
if (key?.ctrl && key?.name === 'c') {
|
|
585
|
+
cancel();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (key?.name === 'escape' || key?.name === 'q') {
|
|
589
|
+
cancel();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (key?.name === 'up' || key?.name === 'k') {
|
|
593
|
+
cursorIndex = (cursorIndex - 1 + options.length) % options.length;
|
|
594
|
+
warning = '';
|
|
595
|
+
}
|
|
596
|
+
else if (key?.name === 'down' || key?.name === 'j') {
|
|
597
|
+
cursorIndex = (cursorIndex + 1) % options.length;
|
|
598
|
+
warning = '';
|
|
599
|
+
}
|
|
600
|
+
else if (key?.name === 'space') {
|
|
601
|
+
toggleIndex(cursorIndex);
|
|
602
|
+
}
|
|
603
|
+
else if (String(_text || '').toLowerCase() === 'a') {
|
|
604
|
+
const optional = options.filter((option) => !required.has(option.value));
|
|
605
|
+
const allSelected = optional.every((option) => selected.has(option.value));
|
|
606
|
+
optional.forEach((option) => {
|
|
607
|
+
if (allSelected)
|
|
608
|
+
selected.delete(option.value);
|
|
609
|
+
else
|
|
610
|
+
selected.add(option.value);
|
|
611
|
+
});
|
|
612
|
+
requiredValues.forEach((value) => selected.add(value));
|
|
613
|
+
warning = '';
|
|
614
|
+
}
|
|
615
|
+
else if (key?.name === 'return' || key?.name === 'enter') {
|
|
616
|
+
finish();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
else if (/^[1-9]$/.test(String(_text || ''))) {
|
|
620
|
+
toggleIndex(Number(_text) - 1);
|
|
621
|
+
}
|
|
622
|
+
render();
|
|
623
|
+
};
|
|
624
|
+
process.stdin.on('keypress', onKeypress);
|
|
625
|
+
process.stdout.write(ANSI.hideCursor);
|
|
626
|
+
render();
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async function askMenuChoiceByKeys({ title, subtitle, options, defaultValue, renderHeader, }) {
|
|
630
|
+
emitKeypressEvents(process.stdin);
|
|
631
|
+
const wasRaw = process.stdin.isRaw;
|
|
632
|
+
const wasPaused = process.stdin.isPaused();
|
|
633
|
+
process.stdin.setRawMode(true);
|
|
634
|
+
process.stdin.resume();
|
|
635
|
+
let cursorIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
|
|
636
|
+
return await new Promise((resolve, reject) => {
|
|
637
|
+
const cleanup = () => {
|
|
638
|
+
process.stdin.off('keypress', onKeypress);
|
|
639
|
+
process.stdin.setRawMode(Boolean(wasRaw));
|
|
640
|
+
if (wasPaused) {
|
|
641
|
+
process.stdin.pause();
|
|
642
|
+
}
|
|
643
|
+
process.stdout.write(ANSI.showCursor);
|
|
644
|
+
};
|
|
645
|
+
const render = () => {
|
|
646
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
647
|
+
renderHeader?.();
|
|
648
|
+
process.stdout.write(`\n${ANSI.bold}${title}${ANSI.reset}\n`);
|
|
649
|
+
process.stdout.write(`${ANSI.dim}${subtitle}${ANSI.reset}\n\n`);
|
|
650
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
651
|
+
const option = options[index];
|
|
652
|
+
const pointer = index === cursorIndex ? `${ANSI.cyan}>${ANSI.reset}` : ' ';
|
|
653
|
+
const number = `${index + 1})`;
|
|
654
|
+
process.stdout.write(`${pointer} ${number} ${ANSI.bold}${option.label}${ANSI.reset}\n`);
|
|
655
|
+
writeWrapped(option.detail, ' ', ANSI.dim);
|
|
656
|
+
}
|
|
657
|
+
process.stdout.write(`\n${ANSI.dim}Esc/Q cancels. Number keys 1-${options.length} select directly.${ANSI.reset}\n`);
|
|
658
|
+
};
|
|
659
|
+
const cancel = () => {
|
|
660
|
+
cleanup();
|
|
661
|
+
process.stdout.write('\n');
|
|
662
|
+
reject(new WizardAbortError('Setup cancelled.'));
|
|
663
|
+
};
|
|
664
|
+
const finish = () => {
|
|
665
|
+
cleanup();
|
|
666
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
667
|
+
resolve(options[cursorIndex]?.value || defaultValue);
|
|
668
|
+
};
|
|
669
|
+
const onKeypress = (_text, key) => {
|
|
670
|
+
if (key?.ctrl && key?.name === 'c') {
|
|
671
|
+
cancel();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (key?.name === 'escape' || key?.name === 'q') {
|
|
675
|
+
cancel();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (key?.name === 'up' || key?.name === 'k') {
|
|
679
|
+
cursorIndex = (cursorIndex - 1 + options.length) % options.length;
|
|
680
|
+
}
|
|
681
|
+
else if (key?.name === 'down' || key?.name === 'j') {
|
|
682
|
+
cursorIndex = (cursorIndex + 1) % options.length;
|
|
683
|
+
}
|
|
684
|
+
else if (key?.name === 'return' || key?.name === 'enter') {
|
|
685
|
+
finish();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
else if (/^[1-9]$/.test(String(_text || ''))) {
|
|
689
|
+
const selectedIndex = Number(_text) - 1;
|
|
690
|
+
if (options[selectedIndex]) {
|
|
691
|
+
cursorIndex = selectedIndex;
|
|
692
|
+
finish();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
render();
|
|
697
|
+
};
|
|
698
|
+
process.stdin.on('keypress', onKeypress);
|
|
699
|
+
process.stdout.write(ANSI.hideCursor);
|
|
700
|
+
render();
|
|
701
|
+
});
|
|
702
|
+
}
|
|
345
703
|
function normalizeConnectorProgressKey(key) {
|
|
346
704
|
const normalized = String(key || '').trim().toLowerCase();
|
|
347
705
|
if (normalized === 'analytics' || normalized === 'analyticscli')
|
|
@@ -439,9 +797,6 @@ function connectorStatusLabel(key, healthByConnector = {}) {
|
|
|
439
797
|
return 'not configured';
|
|
440
798
|
return `configured, ${connectorHealthLabel(health.status)}`;
|
|
441
799
|
}
|
|
442
|
-
function formatConnectorHealthLine(key, healthByConnector = {}) {
|
|
443
|
-
return `${ANSI.dim}${formatConnectorHealthText(key, healthByConnector)}${ANSI.reset}`;
|
|
444
|
-
}
|
|
445
800
|
function formatConnectorHealthText(key, healthByConnector = {}) {
|
|
446
801
|
const health = getConnectorHealth(key, healthByConnector);
|
|
447
802
|
const label = connectorStatusLabel(key, healthByConnector);
|
|
@@ -517,7 +872,7 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
517
872
|
},
|
|
518
873
|
]));
|
|
519
874
|
}
|
|
520
|
-
const result = await runCommandCaptureWithProgress(
|
|
875
|
+
const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json`, onProgress);
|
|
521
876
|
const payload = parseJsonFromStdout(result.stdout);
|
|
522
877
|
const connectors = payload?.connectors && typeof payload.connectors === 'object' ? payload.connectors : {};
|
|
523
878
|
const healthByConnector = {
|
|
@@ -529,11 +884,11 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
529
884
|
};
|
|
530
885
|
return Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
|
|
531
886
|
}
|
|
532
|
-
function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '') {
|
|
887
|
+
function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '', copy = {}) {
|
|
533
888
|
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);
|
|
889
|
+
printConnectorIntro(copy);
|
|
890
|
+
process.stdout.write(`${ANSI.bold}${copy.actionTitle || 'Select connectors to set up or overwrite now'}${ANSI.reset}\n`);
|
|
891
|
+
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
892
|
process.stdout.write('\n');
|
|
538
893
|
let index = 0;
|
|
539
894
|
for (const group of connectorPickerGroups(healthByConnector)) {
|
|
@@ -559,16 +914,18 @@ function renderConnectorPicker(cursorIndex, selected, required, healthByConnecto
|
|
|
559
914
|
}
|
|
560
915
|
process.stdout.write(`${ANSI.dim}Esc/Q cancels. Number keys 1-${CONNECTOR_DEFINITIONS.length} also toggle connectors.${ANSI.reset}\n`);
|
|
561
916
|
}
|
|
562
|
-
async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = []) {
|
|
917
|
+
async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelected = [], copy = {}) {
|
|
563
918
|
emitKeypressEvents(process.stdin);
|
|
564
919
|
const wasRaw = process.stdin.isRaw;
|
|
565
920
|
const wasPaused = process.stdin.isPaused();
|
|
566
921
|
process.stdin.setRawMode(true);
|
|
567
922
|
process.stdin.resume();
|
|
568
923
|
let cursorIndex = 0;
|
|
569
|
-
const required = getRequiredConnectorKeys();
|
|
924
|
+
const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
|
|
570
925
|
const initial = new Set(initialSelected);
|
|
571
|
-
const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
|
|
926
|
+
const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
|
|
927
|
+
initial.has(key) ||
|
|
928
|
+
(copy.mode !== 'input' && !isConnectorLocallyConfigured(key))));
|
|
572
929
|
let warning = '';
|
|
573
930
|
return await new Promise((resolve, reject) => {
|
|
574
931
|
const displayItems = () => connectorPickerDisplayItems(healthByConnector);
|
|
@@ -586,7 +943,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
586
943
|
required.forEach((key) => selected.add(key));
|
|
587
944
|
if (selected.size === 0) {
|
|
588
945
|
warning = 'No connectors selected. Select a connector to update or press Esc to cancel.';
|
|
589
|
-
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
|
|
946
|
+
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
|
|
590
947
|
return;
|
|
591
948
|
}
|
|
592
949
|
cleanup();
|
|
@@ -596,7 +953,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
596
953
|
const cancel = () => {
|
|
597
954
|
cleanup();
|
|
598
955
|
process.stdout.write('\n');
|
|
599
|
-
reject(new
|
|
956
|
+
reject(new WizardAbortError('Connector setup cancelled.'));
|
|
600
957
|
};
|
|
601
958
|
const toggleCurrent = () => {
|
|
602
959
|
const connector = selectedDisplayConnector();
|
|
@@ -671,11 +1028,11 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
671
1028
|
}
|
|
672
1029
|
}
|
|
673
1030
|
}
|
|
674
|
-
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
|
|
1031
|
+
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
|
|
675
1032
|
};
|
|
676
1033
|
process.stdin.on('keypress', onKeypress);
|
|
677
1034
|
process.stdout.write(ANSI.hideCursor);
|
|
678
|
-
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning);
|
|
1035
|
+
renderConnectorPicker(cursorIndex, selected, required, healthByConnector, warning, copy);
|
|
679
1036
|
});
|
|
680
1037
|
}
|
|
681
1038
|
async function commandExists(commandName) {
|
|
@@ -914,16 +1271,16 @@ function summarizeFailureFix(connector, blockers) {
|
|
|
914
1271
|
if (/revoked|unauthorized|UNAUTHORIZED/i.test(combined)) {
|
|
915
1272
|
return 'Paste a fresh AnalyticsCLI readonly CLI token in the wizard, then let setup retest.';
|
|
916
1273
|
}
|
|
917
|
-
return 'Verify the AnalyticsCLI token can list projects. Per-project query failures are reported as warnings and should not block connector setup.';
|
|
1274
|
+
return 'Verify the AnalyticsCLI token can list accessible projects. Per-project query failures are reported as warnings and should not block connector setup.';
|
|
918
1275
|
}
|
|
919
1276
|
if (connector === 'sentry') {
|
|
920
1277
|
if (/404|Not Found/i.test(combined)) {
|
|
921
|
-
return 'Rerun Sentry/GlitchTip setup and use the correct base URL +
|
|
1278
|
+
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
1279
|
}
|
|
923
|
-
return 'Verify the Sentry/GlitchTip token, base URL,
|
|
1280
|
+
return 'Verify the Sentry/GlitchTip token, base URL, and org, then rerun setup.';
|
|
924
1281
|
}
|
|
925
1282
|
if (connector === 'github') {
|
|
926
|
-
return '
|
|
1283
|
+
return 'Verify the GitHub token. Repo scope is inferred from OPENCLAW_GITHUB_REPO, the local git remote, or runtime context.';
|
|
927
1284
|
}
|
|
928
1285
|
if (connector === 'revenuecat') {
|
|
929
1286
|
return 'Paste a RevenueCat v2 secret API key with read-only project permissions, then rerun setup.';
|
|
@@ -1048,11 +1405,6 @@ function printSetupSuccess(payload) {
|
|
|
1048
1405
|
process.stdout.write(`${payload.message}\n`);
|
|
1049
1406
|
}
|
|
1050
1407
|
}
|
|
1051
|
-
function healthCheckFailures(payload) {
|
|
1052
|
-
return Array.isArray(payload?.checks)
|
|
1053
|
-
? payload.checks.filter((check) => check?.status === 'fail')
|
|
1054
|
-
: [];
|
|
1055
|
-
}
|
|
1056
1408
|
function connectorFromCheckName(name) {
|
|
1057
1409
|
const value = String(name || '');
|
|
1058
1410
|
if (value.includes('analytics') || value.includes('ANALYTICSCLI'))
|
|
@@ -1111,146 +1463,12 @@ function cleanHealthDetail(detail) {
|
|
|
1111
1463
|
}
|
|
1112
1464
|
return truncate(raw, 180);
|
|
1113
1465
|
}
|
|
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
1466
|
function isDeferredGitHubFailure(failure) {
|
|
1138
1467
|
const name = String(failure?.name || '');
|
|
1139
1468
|
const detail = String(failure?.detail || '');
|
|
1140
1469
|
return (name === 'project:github-repo' ||
|
|
1141
1470
|
(name === 'connection:github' && /project\.githubRepo|repo is missing|repo is not configured/i.test(detail)));
|
|
1142
1471
|
}
|
|
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
1472
|
function healthStatusLabel(status) {
|
|
1255
1473
|
if (status === 'running')
|
|
1256
1474
|
return 'running';
|
|
@@ -1299,9 +1517,6 @@ function updateHealthProgress(items, event) {
|
|
|
1299
1517
|
}
|
|
1300
1518
|
return false;
|
|
1301
1519
|
}
|
|
1302
|
-
function allProgressItemsFinished(items) {
|
|
1303
|
-
return items.length > 0 && items.every((item) => !['pending', 'running'].includes(String(item.status || '')));
|
|
1304
|
-
}
|
|
1305
1520
|
function buildSetupTestProgressPlan(selected) {
|
|
1306
1521
|
const selectedSet = new Set(selected);
|
|
1307
1522
|
const items = [
|
|
@@ -1403,7 +1618,7 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
|
|
|
1403
1618
|
...process.env,
|
|
1404
1619
|
...secrets,
|
|
1405
1620
|
};
|
|
1406
|
-
const command =
|
|
1621
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(configPath)} --setup-only --connectors ${quote(connector)} --only-connectors ${quote(connector)}`;
|
|
1407
1622
|
let result = await runSetupCommandWithProgress(command, env, [connector], `Checking ${connectorLabel(connector)} immediately after setup...`);
|
|
1408
1623
|
let payload = parseJsonFromStdout(result.stdout);
|
|
1409
1624
|
if (connector === 'asc') {
|
|
@@ -1430,39 +1645,6 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
|
|
|
1430
1645
|
process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed or is only waiting on optional/deferred context.\n`);
|
|
1431
1646
|
return { ok: true, retry: false, result, payload };
|
|
1432
1647
|
}
|
|
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
1648
|
function getUserLocalBinDir() {
|
|
1467
1649
|
return process.env.HOME ? path.join(process.env.HOME, '.local', 'bin') : null;
|
|
1468
1650
|
}
|
|
@@ -1818,13 +2000,13 @@ function getGrowthRunCommand(config, displayConfigPath) {
|
|
|
1818
2000
|
if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.runCommand) {
|
|
1819
2001
|
return config.security.connectorSecrets.runCommand;
|
|
1820
2002
|
}
|
|
1821
|
-
return `
|
|
2003
|
+
return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
|
|
1822
2004
|
}
|
|
1823
2005
|
function getConnectorHealthCommand(config, displayConfigPath) {
|
|
1824
2006
|
if (config?.security?.connectorSecrets?.mode === 'isolated-runner' && config.security.connectorSecrets.healthCommand) {
|
|
1825
2007
|
return config.security.connectorSecrets.healthCommand;
|
|
1826
2008
|
}
|
|
1827
|
-
return `
|
|
2009
|
+
return growthEngineerPackageCommand(`run --config ${quote(displayConfigPath)}`);
|
|
1828
2010
|
}
|
|
1829
2011
|
async function maybePromptSecret(rl, label, envName) {
|
|
1830
2012
|
const existing = process.env[envName]?.trim();
|
|
@@ -1874,12 +2056,6 @@ function printSentryTokenGuidance({ baseUrl, tokenEnv }) {
|
|
|
1874
2056
|
'Optional for richer release context: `project:releases`.',
|
|
1875
2057
|
]);
|
|
1876
2058
|
}
|
|
1877
|
-
function parseCommaList(value) {
|
|
1878
|
-
return String(value || '')
|
|
1879
|
-
.split(',')
|
|
1880
|
-
.map((entry) => entry.trim())
|
|
1881
|
-
.filter(Boolean);
|
|
1882
|
-
}
|
|
1883
2059
|
function buildUrl(baseUrl, pathname, params = {}) {
|
|
1884
2060
|
const url = new URL(pathname, `${String(baseUrl || 'https://sentry.io').replace(/\/$/, '')}/`);
|
|
1885
2061
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -1904,7 +2080,7 @@ function apiListItems(payload) {
|
|
|
1904
2080
|
return payload.teams;
|
|
1905
2081
|
return [];
|
|
1906
2082
|
}
|
|
1907
|
-
async function fetchSentryJsonPage({
|
|
2083
|
+
async function fetchSentryJsonPage({ token, url }) {
|
|
1908
2084
|
const normalizedToken = String(token || '').trim();
|
|
1909
2085
|
const response = await fetch(url, {
|
|
1910
2086
|
method: 'GET',
|
|
@@ -1938,7 +2114,7 @@ async function fetchSentryJsonList({ baseUrl, token, url }) {
|
|
|
1938
2114
|
const pages = [];
|
|
1939
2115
|
let nextUrl = url;
|
|
1940
2116
|
for (let page = 0; nextUrl && page < 10; page += 1) {
|
|
1941
|
-
const result = await fetchSentryJsonPage({
|
|
2117
|
+
const result = await fetchSentryJsonPage({ token, url: nextUrl });
|
|
1942
2118
|
pages.push(result.detail);
|
|
1943
2119
|
if (!result.ok)
|
|
1944
2120
|
return { ...result, payload: items, detail: pages.join('; ') };
|
|
@@ -2069,7 +2245,7 @@ async function upsertSentryAccountsConfig(configPath, accounts) {
|
|
|
2069
2245
|
...(config.sources?.sentry || {}),
|
|
2070
2246
|
enabled: true,
|
|
2071
2247
|
mode: 'command',
|
|
2072
|
-
command:
|
|
2248
|
+
command: getWizardDefaultSourceCommand('sentry'),
|
|
2073
2249
|
accounts: [...merged.values()],
|
|
2074
2250
|
},
|
|
2075
2251
|
};
|
|
@@ -2482,13 +2658,13 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2482
2658
|
org = await ask(rl, `Sentry org slug for ${label} (leave empty to defer)`, index === 0 ? process.env.SENTRY_ORG || '' : '');
|
|
2483
2659
|
}
|
|
2484
2660
|
const environment = await ask(rl, `Sentry environment for ${label}`, index === 0 ? process.env.SENTRY_ENVIRONMENT || 'production' : 'production');
|
|
2485
|
-
let projects = [];
|
|
2486
2661
|
if (org.trim() && token) {
|
|
2487
|
-
process.stdout.write(`
|
|
2662
|
+
process.stdout.write(`Checking visible Sentry projects for ${label} without pinning project scope...\n`);
|
|
2488
2663
|
const discovery = await discoverSentryProjects({ baseUrl, token, org });
|
|
2664
|
+
let verifiedVisibleProjects = false;
|
|
2489
2665
|
if (discovery.ok && discovery.projects.length > 0) {
|
|
2490
|
-
|
|
2491
|
-
process.stdout.write(`
|
|
2666
|
+
verifiedVisibleProjects = true;
|
|
2667
|
+
process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned so OpenClaw/Hermes can decide per run.\n`);
|
|
2492
2668
|
}
|
|
2493
2669
|
else {
|
|
2494
2670
|
const fallbackOrgs = discoveredOrganizations
|
|
@@ -2498,15 +2674,14 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2498
2674
|
process.stdout.write(`Trying visible org ${fallbackOrg}...\n`);
|
|
2499
2675
|
const fallbackDiscovery = await discoverSentryProjects({ baseUrl, token, org: fallbackOrg });
|
|
2500
2676
|
if (fallbackDiscovery.ok && fallbackDiscovery.projects.length > 0) {
|
|
2501
|
-
|
|
2502
|
-
|
|
2677
|
+
org = fallbackOrg;
|
|
2678
|
+
verifiedVisibleProjects = true;
|
|
2679
|
+
process.stdout.write(`Using org ${fallbackOrg}; found ${fallbackDiscovery.projects.length} visible project(s). Project scope remains unpinned.\n`);
|
|
2503
2680
|
break;
|
|
2504
2681
|
}
|
|
2505
2682
|
}
|
|
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;
|
|
2683
|
+
if (!verifiedVisibleProjects && !discovery.ok) {
|
|
2684
|
+
process.stdout.write(`Could not verify visible projects automatically (${discovery.detail}). Project scope will be resolved from app context later.\n`);
|
|
2510
2685
|
}
|
|
2511
2686
|
}
|
|
2512
2687
|
}
|
|
@@ -2519,7 +2694,6 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
2519
2694
|
baseUrl,
|
|
2520
2695
|
tokenEnv,
|
|
2521
2696
|
...(org.trim() ? { org: org.trim() } : {}),
|
|
2522
|
-
...(projects.length > 0 ? { projects } : {}),
|
|
2523
2697
|
...(environment.trim() ? { environment: environment.trim() } : {}),
|
|
2524
2698
|
});
|
|
2525
2699
|
if (index === 0) {
|
|
@@ -2679,6 +2853,144 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
2679
2853
|
const code = await rerunCurrentWizardWithoutSelfUpdate();
|
|
2680
2854
|
process.exit(code ?? 0);
|
|
2681
2855
|
}
|
|
2856
|
+
async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, allowIsolationPrompt = true, }) {
|
|
2857
|
+
clearTerminal();
|
|
2858
|
+
printConnectorIntro();
|
|
2859
|
+
process.stdout.write(`${ANSI.bold}Selected connectors${ANSI.reset}\n`);
|
|
2860
|
+
for (const key of selected) {
|
|
2861
|
+
process.stdout.write(` - ${connectorLabel(key)}\n`);
|
|
2862
|
+
}
|
|
2863
|
+
process.stdout.write('\n');
|
|
2864
|
+
const secrets = {};
|
|
2865
|
+
let sentryAccounts = [];
|
|
2866
|
+
if (selected.includes('analytics')) {
|
|
2867
|
+
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
2868
|
+
while (true) {
|
|
2869
|
+
clearTerminal();
|
|
2870
|
+
await guideAnalyticsConnector(rl, secrets, { forceFresh: forceFreshAnalyticsToken });
|
|
2871
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2872
|
+
rl,
|
|
2873
|
+
configPath: args.config,
|
|
2874
|
+
connector: 'analytics',
|
|
2875
|
+
secrets,
|
|
2876
|
+
});
|
|
2877
|
+
if (!check.retry)
|
|
2878
|
+
break;
|
|
2879
|
+
forceFreshAnalyticsToken = true;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
if (selected.includes('github')) {
|
|
2883
|
+
while (true) {
|
|
2884
|
+
clearTerminal();
|
|
2885
|
+
await guideGitHubConnector(rl, secrets);
|
|
2886
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2887
|
+
rl,
|
|
2888
|
+
configPath: args.config,
|
|
2889
|
+
connector: 'github',
|
|
2890
|
+
secrets,
|
|
2891
|
+
});
|
|
2892
|
+
if (!check.retry)
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
if (selected.includes('revenuecat')) {
|
|
2897
|
+
while (true) {
|
|
2898
|
+
clearTerminal();
|
|
2899
|
+
await guideRevenueCatConnector(rl, secrets);
|
|
2900
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2901
|
+
rl,
|
|
2902
|
+
configPath: args.config,
|
|
2903
|
+
connector: 'revenuecat',
|
|
2904
|
+
secrets,
|
|
2905
|
+
});
|
|
2906
|
+
if (!check.retry)
|
|
2907
|
+
break;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (selected.includes('sentry')) {
|
|
2911
|
+
while (true) {
|
|
2912
|
+
clearTerminal();
|
|
2913
|
+
sentryAccounts = await guideSentryConnector(rl, secrets);
|
|
2914
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2915
|
+
rl,
|
|
2916
|
+
configPath: args.config,
|
|
2917
|
+
connector: 'sentry',
|
|
2918
|
+
secrets,
|
|
2919
|
+
sentryAccounts,
|
|
2920
|
+
});
|
|
2921
|
+
if (!check.retry)
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
if (selected.includes('asc')) {
|
|
2926
|
+
while (true) {
|
|
2927
|
+
clearTerminal();
|
|
2928
|
+
await guideAscConnector(rl, secrets);
|
|
2929
|
+
const check = await runImmediateConnectorHealthCheck({
|
|
2930
|
+
rl,
|
|
2931
|
+
configPath: args.config,
|
|
2932
|
+
connector: 'asc',
|
|
2933
|
+
secrets,
|
|
2934
|
+
});
|
|
2935
|
+
if (!check.retry)
|
|
2936
|
+
break;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const secretsFile = resolveSecretsFile();
|
|
2940
|
+
const wroteSecrets = Object.keys(secrets).length > 0;
|
|
2941
|
+
clearTerminal();
|
|
2942
|
+
if (wroteSecrets) {
|
|
2943
|
+
await writeSecretsFile(secretsFile, secrets);
|
|
2944
|
+
process.stdout.write(`\nSaved local secrets to ${secretsFile} with chmod 600.\n`);
|
|
2945
|
+
}
|
|
2946
|
+
else {
|
|
2947
|
+
process.stdout.write('\nNo new secrets were written.\n');
|
|
2948
|
+
}
|
|
2949
|
+
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2950
|
+
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
2951
|
+
}
|
|
2952
|
+
const env = {
|
|
2953
|
+
...process.env,
|
|
2954
|
+
...secrets,
|
|
2955
|
+
};
|
|
2956
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-start.mjs')} --config ${quote(args.config)} --setup-only --connectors ${quote(selected.join(','))}`;
|
|
2957
|
+
let setupResult = await runSetupCommandWithProgress(command, env, selected, 'Testing connector setup...');
|
|
2958
|
+
let setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
2959
|
+
if (sentryAccounts.length > 0 && await upsertSentryAccountsConfig(args.config, sentryAccounts)) {
|
|
2960
|
+
process.stdout.write(`Sentry-compatible account config is up to date in ${args.config}.\n`);
|
|
2961
|
+
}
|
|
2962
|
+
if (selected.includes('asc')) {
|
|
2963
|
+
try {
|
|
2964
|
+
const ascWebAuthChanged = await ensureAscWebAnalyticsAuth(rl, secrets);
|
|
2965
|
+
if (ascWebAuthChanged) {
|
|
2966
|
+
setupResult = await runSetupCommandWithProgress(command, env, selected, 'Retesting connector setup after ASC web analytics login...');
|
|
2967
|
+
setupPayload = parseJsonFromStdout(setupResult.stdout);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
catch (error) {
|
|
2971
|
+
process.stdout.write(`ASC web analytics still needs attention: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (setupResult.ok && setupPayload?.ok !== false) {
|
|
2975
|
+
printSetupSuccess(setupPayload);
|
|
2976
|
+
if (wroteSecrets) {
|
|
2977
|
+
process.stdout.write('Future OpenClaw Growth commands load this secrets file automatically.\n');
|
|
2978
|
+
}
|
|
2979
|
+
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);
|
|
2980
|
+
if (configureIsolation) {
|
|
2981
|
+
const config = await loadEditableConfig(args.config);
|
|
2982
|
+
const secretAccess = await askSecretAccessModel(rl, path.resolve(args.config), config);
|
|
2983
|
+
await writeJsonFile(path.resolve(args.config), config);
|
|
2984
|
+
const manifestPath = await writeOpenClawJobManifest(path.resolve(args.config), config);
|
|
2985
|
+
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
2986
|
+
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
2987
|
+
}
|
|
2988
|
+
return true;
|
|
2989
|
+
}
|
|
2990
|
+
printSetupFailure({ result: setupResult, payload: setupPayload, command });
|
|
2991
|
+
process.exitCode = 1;
|
|
2992
|
+
return false;
|
|
2993
|
+
}
|
|
2682
2994
|
async function runConnectorSetupWizard(args) {
|
|
2683
2995
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2684
2996
|
throw new Error('Connector wizard requires an interactive terminal.');
|
|
@@ -2687,151 +2999,18 @@ async function runConnectorSetupWizard(args) {
|
|
|
2687
2999
|
try {
|
|
2688
3000
|
clearTerminal();
|
|
2689
3001
|
printConnectorIntro();
|
|
3002
|
+
await migrateRuntimeSourceCommandsFile(args.config);
|
|
2690
3003
|
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(args.config, onProgress));
|
|
2691
3004
|
const existingFixes = connectorKeysNeedingAttention(healthByConnector);
|
|
2692
3005
|
const requestedConnectors = args.connectors ? parseConnectorList(args.connectors) : [];
|
|
2693
3006
|
const chosenConnectors = requestedConnectors.length > 0
|
|
2694
3007
|
? orderConnectors([...new Set([...requestedConnectors, ...existingFixes])])
|
|
2695
3008
|
: await askConnectorSelectionWithHealth(rl, healthByConnector, existingFixes);
|
|
2696
|
-
|
|
3009
|
+
const selected = withMissingRequiredAnalyticsConnector(chosenConnectors);
|
|
2697
3010
|
if (selected.length === 0) {
|
|
2698
3011
|
throw new Error('No supported connectors selected. Use analytics, github, revenuecat, sentry, asc, or all.');
|
|
2699
3012
|
}
|
|
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;
|
|
3013
|
+
await runConnectorSetupSteps({ rl, args, selected, healthByConnector });
|
|
2835
3014
|
}
|
|
2836
3015
|
finally {
|
|
2837
3016
|
rl.close();
|
|
@@ -2870,89 +3049,89 @@ async function askYesNo(rl, label, defaultYes = true) {
|
|
|
2870
3049
|
}
|
|
2871
3050
|
}
|
|
2872
3051
|
}
|
|
2873
|
-
|
|
2874
|
-
const
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
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
|
-
};
|
|
3052
|
+
function truncateTableCell(value, width) {
|
|
3053
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
3054
|
+
if (text.length <= width)
|
|
3055
|
+
return text.padEnd(width, ' ');
|
|
3056
|
+
return `${text.slice(0, Math.max(0, width - 3))}...`.padEnd(width, ' ');
|
|
3057
|
+
}
|
|
3058
|
+
function printAsciiTable(headers, rows, widths) {
|
|
3059
|
+
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
|
|
3060
|
+
const renderRow = (cells) => `| ${cells.map((cell, index) => truncateTableCell(cell, widths[index])).join(' | ')} |`;
|
|
3061
|
+
process.stdout.write(`${border}\n`);
|
|
3062
|
+
process.stdout.write(`${renderRow(headers)}\n`);
|
|
3063
|
+
process.stdout.write(`${border}\n`);
|
|
3064
|
+
for (const row of rows) {
|
|
3065
|
+
process.stdout.write(`${renderRow(row)}\n`);
|
|
2914
3066
|
}
|
|
2915
|
-
|
|
2916
|
-
enabled: true,
|
|
2917
|
-
mode,
|
|
2918
|
-
command: value,
|
|
2919
|
-
hint,
|
|
2920
|
-
...(options.cursorMode ? { cursorMode: options.cursorMode } : {}),
|
|
2921
|
-
...(options.initialLookback ? { initialLookback: options.initialLookback } : {}),
|
|
2922
|
-
};
|
|
3067
|
+
process.stdout.write(`${border}\n`);
|
|
2923
3068
|
}
|
|
2924
3069
|
function printCadencePlan(cadences) {
|
|
2925
3070
|
process.stdout.write('\nDefault growth cadence:\n');
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
3071
|
+
printAsciiTable(['Cadence', 'Every', 'Mode', 'Primary focus', 'What it decides'], cadences.map((cadence) => [
|
|
3072
|
+
cadence.key,
|
|
3073
|
+
`${cadence.intervalDays}d`,
|
|
3074
|
+
cadence.criticalOnly ? 'critical only' : 'full review',
|
|
3075
|
+
Array.isArray(cadence.focusAreas) ? cadence.focusAreas.slice(0, 4).join(', ') : '',
|
|
3076
|
+
cadence.objective,
|
|
3077
|
+
]), [12, 7, 13, 30, 42]);
|
|
2930
3078
|
process.stdout.write('\n');
|
|
2931
3079
|
}
|
|
2932
3080
|
async function askToolUsage(rl) {
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3081
|
+
return await askMenuChoice(rl, {
|
|
3082
|
+
title: 'How should OpenClaw Growth Engineer run?',
|
|
3083
|
+
subtitle: 'Use Up/Down to move, Enter to continue, or press 1-3.',
|
|
3084
|
+
defaultValue: 'production_autopilot',
|
|
3085
|
+
options: [
|
|
3086
|
+
{
|
|
3087
|
+
value: 'production_autopilot',
|
|
3088
|
+
label: 'Production autopilot',
|
|
3089
|
+
detail: 'Notify, draft issues/PR handoffs, and analyze on schedule.',
|
|
3090
|
+
},
|
|
3091
|
+
{
|
|
3092
|
+
value: 'advisory',
|
|
3093
|
+
label: 'Advisory only',
|
|
3094
|
+
detail: 'Analyze and write OpenClaw chat summaries; no GitHub artifacts by default.',
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
value: 'manual_reports',
|
|
3098
|
+
label: 'Manual reports',
|
|
3099
|
+
detail: 'Mostly one-off runs with conservative scheduling.',
|
|
3100
|
+
},
|
|
3101
|
+
],
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
async function askCadencePlan(rl, existingCadences = []) {
|
|
3105
|
+
const existingByKey = new Map((Array.isArray(existingCadences) ? existingCadences : [])
|
|
3106
|
+
.filter((cadence) => cadence?.key)
|
|
3107
|
+
.map((cadence) => [String(cadence.key), cadence]));
|
|
3108
|
+
const cadences = DEFAULT_CADENCE_PLAN.map((cadence) => ({
|
|
3109
|
+
...cadence,
|
|
3110
|
+
...(existingByKey.get(cadence.key) || {}),
|
|
3111
|
+
}));
|
|
2946
3112
|
printCadencePlan(cadences);
|
|
2947
|
-
const
|
|
2948
|
-
|
|
3113
|
+
const selectedCadences = await askMultiChoice(rl, {
|
|
3114
|
+
title: 'Scheduled review cadences',
|
|
3115
|
+
subtitle: 'Use Up/Down to move, Space to toggle cadences, A to toggle all, Enter to continue.',
|
|
3116
|
+
defaultValues: cadences.filter((cadence) => cadence.enabled !== false).map((cadence) => cadence.key),
|
|
3117
|
+
minSelections: 1,
|
|
3118
|
+
options: cadences.map((cadence) => ({
|
|
3119
|
+
value: cadence.key,
|
|
3120
|
+
label: cadence.title,
|
|
3121
|
+
detail: `${cadence.intervalDays}d, ${cadence.criticalOnly ? 'critical only' : 'full review'} - ${cadence.objective}`,
|
|
3122
|
+
})),
|
|
3123
|
+
});
|
|
3124
|
+
const selected = new Set(selectedCadences);
|
|
3125
|
+
cadences.forEach((cadence) => {
|
|
3126
|
+
cadence.enabled = selected.has(cadence.key);
|
|
3127
|
+
});
|
|
3128
|
+
const customize = await askYesNo(rl, 'Customize objectives, instructions, focus areas, or source priorities for enabled cadences?', false);
|
|
3129
|
+
if (!customize)
|
|
2949
3130
|
return cadences;
|
|
2950
3131
|
for (const cadence of cadences) {
|
|
2951
|
-
|
|
2952
|
-
const enabled = await askYesNo(rl, `Enable ${cadence.key}?`, true);
|
|
2953
|
-
cadence.enabled = enabled;
|
|
2954
|
-
if (!enabled)
|
|
3132
|
+
if (cadence.enabled === false)
|
|
2955
3133
|
continue;
|
|
3134
|
+
process.stdout.write(`\n${cadence.title}\n`);
|
|
2956
3135
|
cadence.objective = await ask(rl, `${cadence.key} objective`, cadence.objective);
|
|
2957
3136
|
cadence.instructions = await ask(rl, `${cadence.key} instructions`, cadence.instructions);
|
|
2958
3137
|
const focusAreas = await ask(rl, `${cadence.key} focus areas (comma-separated)`, cadence.focusAreas.join(','));
|
|
@@ -2964,27 +3143,45 @@ async function askCadencePlan(rl) {
|
|
|
2964
3143
|
return cadences;
|
|
2965
3144
|
}
|
|
2966
3145
|
async function askWizardGoal(rl) {
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3146
|
+
return await askMenuChoice(rl, {
|
|
3147
|
+
title: 'What do you want to configure?',
|
|
3148
|
+
subtitle: 'Use Up/Down to move, Enter to continue, or press 1-4.',
|
|
3149
|
+
defaultValue: 'full',
|
|
3150
|
+
renderHeader: printWizardHeader,
|
|
3151
|
+
options: [
|
|
3152
|
+
{
|
|
3153
|
+
value: 'connectors',
|
|
3154
|
+
label: 'Connectors',
|
|
3155
|
+
detail: 'Credentials, provider setup, and health checks.',
|
|
3156
|
+
},
|
|
3157
|
+
{
|
|
3158
|
+
value: 'outputs_intervals',
|
|
3159
|
+
label: 'Outputs and intervals',
|
|
3160
|
+
detail: 'Daily/weekly/monthly jobs, GitHub issue/PR delivery, and OpenClaw chat notifications.',
|
|
3161
|
+
},
|
|
3162
|
+
{
|
|
3163
|
+
value: 'full',
|
|
3164
|
+
label: 'Full setup',
|
|
3165
|
+
detail: 'Project, connectors, outputs, intervals, and sources.',
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
value: 'intervals',
|
|
3169
|
+
label: 'Advanced intervals only',
|
|
3170
|
+
detail: 'Runner wake-up interval and connector health check cadence.',
|
|
3171
|
+
},
|
|
3172
|
+
],
|
|
3173
|
+
});
|
|
3174
|
+
}
|
|
3175
|
+
function printWizardHeader() {
|
|
3176
|
+
process.stdout.write('OpenClaw Growth Engineer - Setup Wizard\n');
|
|
3177
|
+
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
3178
|
}
|
|
2981
3179
|
async function buildDefaultWizardConfig() {
|
|
2982
|
-
const detectedRepo = await detectGitHubRepo();
|
|
2983
3180
|
return {
|
|
2984
3181
|
version: 7,
|
|
2985
3182
|
generatedAt: new Date().toISOString(),
|
|
2986
3183
|
project: {
|
|
2987
|
-
githubRepo:
|
|
3184
|
+
githubRepo: '',
|
|
2988
3185
|
repoRoot: '.',
|
|
2989
3186
|
outFile: 'data/openclaw-growth-engineer/issues.generated.json',
|
|
2990
3187
|
maxIssues: 4,
|
|
@@ -2995,17 +3192,17 @@ async function buildDefaultWizardConfig() {
|
|
|
2995
3192
|
analytics: {
|
|
2996
3193
|
enabled: true,
|
|
2997
3194
|
mode: 'command',
|
|
2998
|
-
command:
|
|
3195
|
+
command: getWizardDefaultSourceCommand('analytics'),
|
|
2999
3196
|
},
|
|
3000
3197
|
revenuecat: {
|
|
3001
3198
|
enabled: false,
|
|
3002
3199
|
mode: 'command',
|
|
3003
|
-
command:
|
|
3200
|
+
command: getWizardDefaultSourceCommand('revenuecat'),
|
|
3004
3201
|
},
|
|
3005
3202
|
sentry: {
|
|
3006
|
-
enabled:
|
|
3203
|
+
enabled: true,
|
|
3007
3204
|
mode: 'command',
|
|
3008
|
-
command:
|
|
3205
|
+
command: getWizardDefaultSourceCommand('sentry'),
|
|
3009
3206
|
},
|
|
3010
3207
|
feedback: {
|
|
3011
3208
|
enabled: true,
|
|
@@ -3015,12 +3212,12 @@ async function buildDefaultWizardConfig() {
|
|
|
3015
3212
|
initialLookback: '30d',
|
|
3016
3213
|
},
|
|
3017
3214
|
extra: [
|
|
3018
|
-
buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command:
|
|
3215
|
+
buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
|
|
3019
3216
|
],
|
|
3020
3217
|
},
|
|
3021
3218
|
schedule: {
|
|
3022
|
-
intervalMinutes:
|
|
3023
|
-
connectorHealthCheckIntervalMinutes:
|
|
3219
|
+
intervalMinutes: DEFAULT_GROWTH_INTERVAL_MINUTES,
|
|
3220
|
+
connectorHealthCheckIntervalMinutes: DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES,
|
|
3024
3221
|
skipIfNoDataChange: true,
|
|
3025
3222
|
skipIfIssueSetUnchanged: true,
|
|
3026
3223
|
cadences: DEFAULT_CADENCE_PLAN.map((cadence) => ({ ...cadence })),
|
|
@@ -3028,6 +3225,8 @@ async function buildDefaultWizardConfig() {
|
|
|
3028
3225
|
actions: {
|
|
3029
3226
|
autoCreateIssues: false,
|
|
3030
3227
|
autoCreatePullRequests: false,
|
|
3228
|
+
autoCreateWhenGitHubWriteAccess: true,
|
|
3229
|
+
disableAutoCreateGitHubArtifacts: false,
|
|
3031
3230
|
mode: 'issue',
|
|
3032
3231
|
usageMode: 'production_autopilot',
|
|
3033
3232
|
draftPullRequests: true,
|
|
@@ -3101,10 +3300,127 @@ async function buildDefaultWizardConfig() {
|
|
|
3101
3300
|
},
|
|
3102
3301
|
};
|
|
3103
3302
|
}
|
|
3303
|
+
function buildRecommendedSourceConfig() {
|
|
3304
|
+
return {
|
|
3305
|
+
analytics: {
|
|
3306
|
+
enabled: true,
|
|
3307
|
+
mode: 'command',
|
|
3308
|
+
command: getWizardDefaultSourceCommand('analytics'),
|
|
3309
|
+
},
|
|
3310
|
+
revenuecat: {
|
|
3311
|
+
enabled: false,
|
|
3312
|
+
mode: 'command',
|
|
3313
|
+
command: getWizardDefaultSourceCommand('revenuecat'),
|
|
3314
|
+
},
|
|
3315
|
+
sentry: {
|
|
3316
|
+
enabled: true,
|
|
3317
|
+
mode: 'command',
|
|
3318
|
+
command: getWizardDefaultSourceCommand('sentry'),
|
|
3319
|
+
},
|
|
3320
|
+
feedback: {
|
|
3321
|
+
enabled: true,
|
|
3322
|
+
mode: 'command',
|
|
3323
|
+
command: getDefaultSourceCommand('feedback'),
|
|
3324
|
+
cursorMode: 'auto_since_last_fetch',
|
|
3325
|
+
initialLookback: '30d',
|
|
3326
|
+
},
|
|
3327
|
+
extra: [
|
|
3328
|
+
buildExtraSourceConfig('asc-cli', { enabled: false, mode: 'command', command: getWizardDefaultSourceCommand('asc') }),
|
|
3329
|
+
],
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
function getInputChannelInitialSelection(config) {
|
|
3333
|
+
const sources = config?.sources || {};
|
|
3334
|
+
const extraSources = Array.isArray(sources.extra) ? sources.extra : [];
|
|
3335
|
+
const selected = new Set();
|
|
3336
|
+
const hasExplicitSources = Boolean(config?.sources);
|
|
3337
|
+
if (!hasExplicitSources || sources.analytics?.enabled !== false)
|
|
3338
|
+
selected.add('analytics');
|
|
3339
|
+
if (sources.revenuecat?.enabled === true || isConnectorLocallyConfigured('revenuecat'))
|
|
3340
|
+
selected.add('revenuecat');
|
|
3341
|
+
if (!hasExplicitSources || sources.sentry?.enabled !== false)
|
|
3342
|
+
selected.add('sentry');
|
|
3343
|
+
if (extraSources.some((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()) &&
|
|
3344
|
+
source?.enabled !== false) ||
|
|
3345
|
+
isConnectorLocallyConfigured('asc')) {
|
|
3346
|
+
selected.add('asc');
|
|
3347
|
+
}
|
|
3348
|
+
if (config?.deliveries?.github?.enabled ||
|
|
3349
|
+
config?.actions?.autoCreateIssues ||
|
|
3350
|
+
config?.actions?.autoCreatePullRequests ||
|
|
3351
|
+
isConnectorLocallyConfigured('github')) {
|
|
3352
|
+
selected.add('github');
|
|
3353
|
+
}
|
|
3354
|
+
return orderConnectors([...selected]);
|
|
3355
|
+
}
|
|
3356
|
+
function buildSourceConfigFromInputChannels(selectedConnectors, existingSources = {}) {
|
|
3357
|
+
const selected = new Set(selectedConnectors);
|
|
3358
|
+
const recommended = buildRecommendedSourceConfig();
|
|
3359
|
+
const migratedSources = migrateRuntimeSourceCommands({ sources: existingSources }).sources || {};
|
|
3360
|
+
const existingExtra = Array.isArray(migratedSources.extra) ? migratedSources.extra : [];
|
|
3361
|
+
const ascSource = existingExtra.find((source) => ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(String(source?.service || source?.key || '').toLowerCase()));
|
|
3362
|
+
const nonAscExtra = existingExtra.filter((source) => source !== ascSource);
|
|
3363
|
+
return {
|
|
3364
|
+
...recommended,
|
|
3365
|
+
...migratedSources,
|
|
3366
|
+
analytics: {
|
|
3367
|
+
...recommended.analytics,
|
|
3368
|
+
...(migratedSources.analytics || {}),
|
|
3369
|
+
command: normalizeWizardSourceCommand('analytics', {
|
|
3370
|
+
...recommended.analytics,
|
|
3371
|
+
...(migratedSources.analytics || {}),
|
|
3372
|
+
}),
|
|
3373
|
+
enabled: selected.has('analytics'),
|
|
3374
|
+
},
|
|
3375
|
+
revenuecat: {
|
|
3376
|
+
...recommended.revenuecat,
|
|
3377
|
+
...(migratedSources.revenuecat || {}),
|
|
3378
|
+
command: normalizeWizardSourceCommand('revenuecat', {
|
|
3379
|
+
...recommended.revenuecat,
|
|
3380
|
+
...(migratedSources.revenuecat || {}),
|
|
3381
|
+
}),
|
|
3382
|
+
enabled: selected.has('revenuecat'),
|
|
3383
|
+
},
|
|
3384
|
+
sentry: {
|
|
3385
|
+
...recommended.sentry,
|
|
3386
|
+
...(migratedSources.sentry || {}),
|
|
3387
|
+
command: normalizeWizardSourceCommand('sentry', {
|
|
3388
|
+
...recommended.sentry,
|
|
3389
|
+
...(migratedSources.sentry || {}),
|
|
3390
|
+
}),
|
|
3391
|
+
enabled: selected.has('sentry'),
|
|
3392
|
+
},
|
|
3393
|
+
feedback: {
|
|
3394
|
+
...recommended.feedback,
|
|
3395
|
+
...(migratedSources.feedback || {}),
|
|
3396
|
+
enabled: selected.has('analytics'),
|
|
3397
|
+
},
|
|
3398
|
+
extra: [
|
|
3399
|
+
...nonAscExtra,
|
|
3400
|
+
{
|
|
3401
|
+
...buildExtraSourceConfig('asc-cli', {
|
|
3402
|
+
enabled: selected.has('asc'),
|
|
3403
|
+
mode: 'command',
|
|
3404
|
+
command: getWizardDefaultSourceCommand('asc'),
|
|
3405
|
+
}),
|
|
3406
|
+
...(ascSource || {}),
|
|
3407
|
+
command: normalizeWizardSourceCommand('asc', {
|
|
3408
|
+
...buildExtraSourceConfig('asc-cli', {
|
|
3409
|
+
enabled: selected.has('asc'),
|
|
3410
|
+
mode: 'command',
|
|
3411
|
+
command: getWizardDefaultSourceCommand('asc'),
|
|
3412
|
+
}),
|
|
3413
|
+
...(ascSource || {}),
|
|
3414
|
+
}),
|
|
3415
|
+
enabled: selected.has('asc'),
|
|
3416
|
+
},
|
|
3417
|
+
],
|
|
3418
|
+
};
|
|
3419
|
+
}
|
|
3104
3420
|
async function loadEditableConfig(configPath) {
|
|
3105
3421
|
const existing = await readJsonIfPresent(configPath).catch(() => null);
|
|
3106
3422
|
if (existing && typeof existing === 'object')
|
|
3107
|
-
return existing;
|
|
3423
|
+
return migrateRuntimeSourceCommands(existing);
|
|
3108
3424
|
return await buildDefaultWizardConfig();
|
|
3109
3425
|
}
|
|
3110
3426
|
function mergeNotificationChannels(baseChannels, extraChannels) {
|
|
@@ -3148,28 +3464,55 @@ async function askNotificationChannels(rl, config) {
|
|
|
3148
3464
|
return channels;
|
|
3149
3465
|
}
|
|
3150
3466
|
async function askOutputConfig(rl, config) {
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3467
|
+
printSection('Outputs and notifications', [
|
|
3468
|
+
'OpenClaw chat is always enabled so the agent has a readable handoff.',
|
|
3469
|
+
'GitHub issues or draft PRs are optional and only run when a token plus an inferred repo are available.',
|
|
3470
|
+
]);
|
|
3155
3471
|
const currentMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3472
|
+
const configuredDestinations = Array.isArray(config?.actions?.outputDestinations)
|
|
3473
|
+
? config.actions.outputDestinations
|
|
3474
|
+
: [];
|
|
3475
|
+
const currentAutoCreateIssue = Boolean(config?.actions?.autoCreateIssues ||
|
|
3476
|
+
configuredDestinations.includes('github_issue') ||
|
|
3477
|
+
(config?.deliveries?.github?.autoCreate && currentMode !== 'pull_request'));
|
|
3478
|
+
const currentAutoCreatePullRequest = Boolean(config?.actions?.autoCreatePullRequests ||
|
|
3479
|
+
configuredDestinations.includes('github_pull_request') ||
|
|
3480
|
+
(config?.deliveries?.github?.autoCreate && currentMode === 'pull_request'));
|
|
3481
|
+
const outputChoices = await askMultiChoice(rl, {
|
|
3482
|
+
title: 'Output destinations',
|
|
3483
|
+
subtitle: 'Use Up/Down to move, Space to toggle outputs, A to toggle all optional outputs, Enter to continue.',
|
|
3484
|
+
defaultValues: [
|
|
3485
|
+
'chat',
|
|
3486
|
+
...(currentAutoCreateIssue ? ['issue'] : []),
|
|
3487
|
+
...(currentAutoCreatePullRequest ? ['pull_request'] : []),
|
|
3488
|
+
],
|
|
3489
|
+
requiredValues: ['chat'],
|
|
3490
|
+
minSelections: 1,
|
|
3491
|
+
options: [
|
|
3492
|
+
{
|
|
3493
|
+
value: 'chat',
|
|
3494
|
+
label: 'OpenClaw chat',
|
|
3495
|
+
detail: 'Write readable summaries and leave GitHub as runtime fallback.',
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
value: 'issue',
|
|
3499
|
+
label: 'GitHub issues',
|
|
3500
|
+
detail: 'Auto-create issues for concrete findings when GitHub access allows it.',
|
|
3501
|
+
},
|
|
3502
|
+
{
|
|
3503
|
+
value: 'pull_request',
|
|
3504
|
+
label: 'Draft PR proposals',
|
|
3505
|
+
detail: 'Auto-create draft PR-oriented proposal branches for implementation-ready fixes.',
|
|
3506
|
+
},
|
|
3507
|
+
],
|
|
3508
|
+
});
|
|
3509
|
+
const wantsIssue = outputChoices.includes('issue');
|
|
3510
|
+
const wantsPullRequest = outputChoices.includes('pull_request');
|
|
3511
|
+
const summaryOnly = !wantsIssue && !wantsPullRequest;
|
|
3512
|
+
const mode = wantsPullRequest ? 'pull_request' : 'issue';
|
|
3513
|
+
const autoCreate = wantsIssue || wantsPullRequest;
|
|
3166
3514
|
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
|
-
};
|
|
3515
|
+
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
3516
|
}
|
|
3174
3517
|
const channels = await askNotificationChannels(rl, config);
|
|
3175
3518
|
const connectorHealthChannels = channels.map((channel) => {
|
|
@@ -3184,8 +3527,15 @@ async function askOutputConfig(rl, config) {
|
|
|
3184
3527
|
config.actions = {
|
|
3185
3528
|
...(config.actions || {}),
|
|
3186
3529
|
mode,
|
|
3187
|
-
|
|
3188
|
-
|
|
3530
|
+
outputDestinations: [
|
|
3531
|
+
'openclaw_chat',
|
|
3532
|
+
...(wantsIssue ? ['github_issue'] : []),
|
|
3533
|
+
...(wantsPullRequest ? ['github_pull_request'] : []),
|
|
3534
|
+
],
|
|
3535
|
+
autoCreateIssues: wantsIssue,
|
|
3536
|
+
autoCreatePullRequests: wantsPullRequest,
|
|
3537
|
+
autoCreateWhenGitHubWriteAccess: config.actions?.autoCreateWhenGitHubWriteAccess !== false,
|
|
3538
|
+
disableAutoCreateGitHubArtifacts: config.actions?.disableAutoCreateGitHubArtifacts === true,
|
|
3189
3539
|
draftPullRequests: true,
|
|
3190
3540
|
proposalBranchPrefix: config?.actions?.proposalBranchPrefix || 'openclaw/proposals',
|
|
3191
3541
|
};
|
|
@@ -3201,6 +3551,10 @@ async function askOutputConfig(rl, config) {
|
|
|
3201
3551
|
...(config.deliveries?.github || {}),
|
|
3202
3552
|
enabled: !summaryOnly,
|
|
3203
3553
|
mode,
|
|
3554
|
+
modes: [
|
|
3555
|
+
...(wantsIssue ? ['issue'] : []),
|
|
3556
|
+
...(wantsPullRequest ? ['pull_request'] : []),
|
|
3557
|
+
],
|
|
3204
3558
|
autoCreate,
|
|
3205
3559
|
draftPullRequests: true,
|
|
3206
3560
|
proposalBranchPrefix: config?.actions?.proposalBranchPrefix || 'openclaw/proposals',
|
|
@@ -3238,12 +3592,62 @@ async function askOutputConfig(rl, config) {
|
|
|
3238
3592
|
};
|
|
3239
3593
|
return config;
|
|
3240
3594
|
}
|
|
3595
|
+
async function askGitHubArtifactDetails(rl, config) {
|
|
3596
|
+
const githubEnabled = Boolean(config?.actions?.autoCreateIssues ||
|
|
3597
|
+
config?.actions?.autoCreatePullRequests ||
|
|
3598
|
+
config?.deliveries?.github?.enabled ||
|
|
3599
|
+
config?.deliveries?.github?.autoCreate);
|
|
3600
|
+
config.project = {
|
|
3601
|
+
...(config.project || {}),
|
|
3602
|
+
githubRepo: '',
|
|
3603
|
+
repoRoot: config.project?.repoRoot || '.',
|
|
3604
|
+
outFile: config.project?.outFile || 'data/openclaw-growth-engineer/issues.generated.json',
|
|
3605
|
+
maxIssues: Number(config.project?.maxIssues || 4),
|
|
3606
|
+
titlePrefix: config.project?.titlePrefix || '[Growth]',
|
|
3607
|
+
labels: Array.isArray(config.project?.labels) && config.project.labels.length > 0
|
|
3608
|
+
? config.project.labels
|
|
3609
|
+
: ['ai-growth', 'autogenerated', 'product'],
|
|
3610
|
+
};
|
|
3611
|
+
if (!githubEnabled) {
|
|
3612
|
+
return config;
|
|
3613
|
+
}
|
|
3614
|
+
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');
|
|
3615
|
+
const customize = await askYesNo(rl, 'Customize GitHub issue/PR limits, labels, or chart attachment settings?', false);
|
|
3616
|
+
if (!customize) {
|
|
3617
|
+
config.charting = {
|
|
3618
|
+
...(config.charting || {}),
|
|
3619
|
+
enabled: config.charting?.enabled === true,
|
|
3620
|
+
command: config.charting?.command || null,
|
|
3621
|
+
};
|
|
3622
|
+
return config;
|
|
3623
|
+
}
|
|
3624
|
+
const labelsRaw = await ask(rl, 'GitHub labels for created issues/PRs', config.project.labels.join(','));
|
|
3625
|
+
config.project.labels = labelsRaw
|
|
3626
|
+
.split(',')
|
|
3627
|
+
.map((value) => value.trim())
|
|
3628
|
+
.filter(Boolean);
|
|
3629
|
+
config.project.maxIssues = Number.parseInt(await ask(rl, 'Maximum GitHub artifacts per run', String(config.project.maxIssues || 4)), 10) || 4;
|
|
3630
|
+
config.project.titlePrefix = await ask(rl, 'GitHub artifact title prefix', config.project.titlePrefix || '[Growth]');
|
|
3631
|
+
const enableCharting = await askYesNo(rl, 'Attach generated charts to GitHub artifacts when useful?', config.charting?.enabled === true);
|
|
3632
|
+
config.charting = {
|
|
3633
|
+
...(config.charting || {}),
|
|
3634
|
+
enabled: enableCharting,
|
|
3635
|
+
command: enableCharting
|
|
3636
|
+
? await ask(rl, 'Optional chart command override', config.charting?.command || '')
|
|
3637
|
+
: null,
|
|
3638
|
+
};
|
|
3639
|
+
return config;
|
|
3640
|
+
}
|
|
3241
3641
|
async function askIntervalConfig(rl, config) {
|
|
3642
|
+
printSection('Schedule and analysis depth', [
|
|
3643
|
+
'The runner wakes up often, but larger reviews only run on their daily/weekly/monthly cadence.',
|
|
3644
|
+
'Connector health checks are separate and default to every 6 hours.',
|
|
3645
|
+
]);
|
|
3242
3646
|
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
3647
|
const usageMode = await askToolUsage(rl);
|
|
3246
|
-
const
|
|
3648
|
+
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;
|
|
3649
|
+
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;
|
|
3650
|
+
const cadences = await askCadencePlan(rl, currentSchedule.cadences);
|
|
3247
3651
|
config.schedule = {
|
|
3248
3652
|
...currentSchedule,
|
|
3249
3653
|
intervalMinutes,
|
|
@@ -3258,11 +3662,31 @@ async function askIntervalConfig(rl, config) {
|
|
|
3258
3662
|
};
|
|
3259
3663
|
return config;
|
|
3260
3664
|
}
|
|
3665
|
+
async function askOutputsAndIntervalsConfig(rl, config) {
|
|
3666
|
+
const withIntervals = await askIntervalConfig(rl, config);
|
|
3667
|
+
const withOutput = await askOutputConfig(rl, withIntervals);
|
|
3668
|
+
return await askGitHubArtifactDetails(rl, withOutput);
|
|
3669
|
+
}
|
|
3670
|
+
async function askInputSourceConfig(rl, config, configPath) {
|
|
3671
|
+
config = migrateRuntimeSourceCommands(config);
|
|
3672
|
+
await ensureDirForFile(configPath);
|
|
3673
|
+
await writeJsonFile(configPath, config);
|
|
3674
|
+
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress));
|
|
3675
|
+
const selected = await askConnectorSelectionWithHealth(rl, healthByConnector, getInputChannelInitialSelection(config), {
|
|
3676
|
+
introTitle: 'Input channels',
|
|
3677
|
+
introDetail: null,
|
|
3678
|
+
actionTitle: 'Select input channels',
|
|
3679
|
+
helpText: 'Use Up/Down to move, Space to toggle channels, A to toggle all channels, Enter to continue.',
|
|
3680
|
+
mode: 'input',
|
|
3681
|
+
});
|
|
3682
|
+
config.sources = buildSourceConfigFromInputChannels(selected, config.sources || {});
|
|
3683
|
+
return { config, selected, healthByConnector };
|
|
3684
|
+
}
|
|
3261
3685
|
async function writeOpenClawJobManifest(configPath, config) {
|
|
3262
3686
|
const manifestPath = path.resolve('.openclaw/jobs/openclaw-growth-engineer.json');
|
|
3263
3687
|
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 ||
|
|
3688
|
+
const intervalMinutes = Math.max(1, Number(config?.schedule?.intervalMinutes || DEFAULT_GROWTH_INTERVAL_MINUTES));
|
|
3689
|
+
const connectorHealthCheckIntervalMinutes = Math.max(1, Number(config?.schedule?.connectorHealthCheckIntervalMinutes || DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES));
|
|
3266
3690
|
const actionMode = config?.actions?.mode || config?.deliveries?.github?.mode || 'issue';
|
|
3267
3691
|
const growthRunCommand = getGrowthRunCommand(config, displayConfigPath);
|
|
3268
3692
|
const connectorHealthCommand = getConnectorHealthCommand(config, displayConfigPath);
|
|
@@ -3317,8 +3741,7 @@ async function main() {
|
|
|
3317
3741
|
}
|
|
3318
3742
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3319
3743
|
try {
|
|
3320
|
-
|
|
3321
|
-
process.stdout.write('This wizard writes non-secret config only. Connector secrets stay in the local secrets file.\n\n');
|
|
3744
|
+
printWizardHeader();
|
|
3322
3745
|
const goal = await askWizardGoal(rl);
|
|
3323
3746
|
if (goal === 'connectors') {
|
|
3324
3747
|
rl.close();
|
|
@@ -3336,160 +3759,40 @@ async function main() {
|
|
|
3336
3759
|
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
3760
|
return;
|
|
3338
3761
|
}
|
|
3339
|
-
if (goal === '
|
|
3340
|
-
const config = await
|
|
3762
|
+
if (goal === 'outputs_intervals') {
|
|
3763
|
+
const config = await askOutputsAndIntervalsConfig(rl, await loadEditableConfig(configPath));
|
|
3341
3764
|
const secretAccess = await askSecretAccessModel(rl, configPath, config);
|
|
3342
3765
|
await writeJsonFile(configPath, config);
|
|
3343
3766
|
const manifestPath = await writeOpenClawJobManifest(configPath, config);
|
|
3344
|
-
process.stdout.write(`\nSaved output config: ${configPath}\n`);
|
|
3767
|
+
process.stdout.write(`\nSaved output and interval config: ${configPath}\n`);
|
|
3345
3768
|
process.stdout.write(`Saved OpenClaw job manifest: ${manifestPath}\n`);
|
|
3346
3769
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3347
|
-
process.stdout.write('
|
|
3770
|
+
process.stdout.write('Daily checks prioritize Sentry and production anomalies; larger cadences analyze all configured projects and connectors.\n');
|
|
3348
3771
|
return;
|
|
3349
3772
|
}
|
|
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) });
|
|
3773
|
+
let config = await loadEditableConfig(configPath);
|
|
3774
|
+
config.version = Number(config.version || 7);
|
|
3775
|
+
config.generatedAt = new Date().toISOString();
|
|
3776
|
+
const inputSetup = await askInputSourceConfig(rl, config, configPath);
|
|
3777
|
+
config = inputSetup.config;
|
|
3778
|
+
await ensureDirForFile(configPath);
|
|
3779
|
+
await writeJsonFile(configPath, config);
|
|
3780
|
+
const connectorsOk = await runConnectorSetupSteps({
|
|
3781
|
+
rl,
|
|
3782
|
+
args: { ...args, config: configPath },
|
|
3783
|
+
selected: inputSetup.selected,
|
|
3784
|
+
healthByConnector: inputSetup.healthByConnector,
|
|
3785
|
+
allowIsolationPrompt: false,
|
|
3382
3786
|
});
|
|
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
|
-
};
|
|
3787
|
+
if (!connectorsOk) {
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
config = await loadEditableConfig(configPath);
|
|
3791
|
+
config.version = Number(config.version || 7);
|
|
3792
|
+
config.generatedAt = new Date().toISOString();
|
|
3793
|
+
config = await askIntervalConfig(rl, config);
|
|
3794
|
+
config = await askOutputConfig(rl, config);
|
|
3795
|
+
config = await askGitHubArtifactDetails(rl, config);
|
|
3493
3796
|
const secretAccess = await askSecretAccessModel(rl, configPath, config);
|
|
3494
3797
|
await ensureDirForFile(configPath);
|
|
3495
3798
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
@@ -3499,14 +3802,8 @@ async function main() {
|
|
|
3499
3802
|
printSecretRunnerKitInstructions(secretAccess.kit);
|
|
3500
3803
|
process.stdout.write('\nNext steps:\n');
|
|
3501
3804
|
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`);
|
|
3805
|
+
process.stdout.write(`2) Run once: ${growthEngineerPackageCommand(`run --config ${quote(configPath)}`)}\n`);
|
|
3806
|
+
process.stdout.write(`3) Run interval loop: ${growthEngineerPackageCommand(`run --config ${quote(configPath)} --loop`)}\n`);
|
|
3510
3807
|
}
|
|
3511
3808
|
finally {
|
|
3512
3809
|
rl.close();
|
|
@@ -3514,6 +3811,6 @@ async function main() {
|
|
|
3514
3811
|
}
|
|
3515
3812
|
main().catch((error) => {
|
|
3516
3813
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
3517
|
-
process.exitCode = 1;
|
|
3814
|
+
process.exitCode = error instanceof WizardAbortError ? error.exitCode : 1;
|
|
3518
3815
|
});
|
|
3519
3816
|
//# sourceMappingURL=openclaw-growth-wizard.mjs.map
|