@analyticscli/growth-engineer 0.1.0-preview.9 → 0.1.0
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.d.ts +925 -45
- package/dist/config.js +58 -6
- package/dist/config.js.map +1 -1
- package/dist/index.js +134 -21
- package/dist/index.js.map +1 -1
- package/dist/runtime/export-asc-summary.mjs +295 -4
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/export-coolify-summary.d.mts +2 -0
- package/dist/runtime/export-coolify-summary.mjs +230 -0
- package/dist/runtime/export-coolify-summary.mjs.map +1 -0
- package/dist/runtime/export-paddle-summary.d.mts +2 -0
- package/dist/runtime/export-paddle-summary.mjs +170 -0
- package/dist/runtime/export-paddle-summary.mjs.map +1 -0
- package/dist/runtime/export-sentry-summary.mjs +265 -38
- package/dist/runtime/export-sentry-summary.mjs.map +1 -1
- package/dist/runtime/export-seo-summary.d.mts +2 -0
- package/dist/runtime/export-seo-summary.mjs +503 -0
- package/dist/runtime/export-seo-summary.mjs.map +1 -0
- package/dist/runtime/openclaw-exporters-lib.d.mts +51 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +769 -63
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-engineer.mjs +163 -4
- package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-env.mjs +5 -0
- package/dist/runtime/openclaw-growth-env.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +446 -30
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +831 -146
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +158 -3
- package/dist/runtime/openclaw-growth-shared.mjs +574 -8
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +802 -39
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +85 -31
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +1952 -217
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +3 -1
- package/templates/config.example.json +128 -65
|
@@ -4,17 +4,67 @@ import path from 'node:path';
|
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { getActionMode, getDefaultSourceCommand } from './openclaw-growth-shared.mjs';
|
|
7
|
+
import { buildGrowthRunnerCommand, buildOpenClawCronAddCommand, deriveSchedulerProofPathFromStatePath, deriveStatePathFromConfigPath, getActionMode, getAutomationConfig, getDefaultSourceCommand, getOpenClawCronEditDeliveryCommandFromInspection, buildHermesCronCreateCommand, inspectHermesCronInstall, inspectOpenClawCronInstall, repairOpenClawCronDeliveryStore, } from './openclaw-growth-shared.mjs';
|
|
8
8
|
import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
9
9
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
10
10
|
const DEFAULT_TEMPLATE_PATH = 'data/openclaw-growth-engineer/config.example.json';
|
|
11
11
|
const DEFAULT_HEARTBEAT_PATH = 'HEARTBEAT.md';
|
|
12
|
+
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
12
13
|
const HEARTBEAT_MARKER_START = '<!-- openclaw-growth-engineer:start -->';
|
|
13
14
|
const HEARTBEAT_MARKER_END = '<!-- openclaw-growth-engineer:end -->';
|
|
14
15
|
const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
|
|
15
16
|
const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
|
|
16
17
|
(process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
|
|
17
18
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ACCOUNT_SIGNAL_CONNECTORS = [
|
|
20
|
+
'stripe',
|
|
21
|
+
'lemonsqueezy',
|
|
22
|
+
'adapty',
|
|
23
|
+
'superwall',
|
|
24
|
+
'google-play',
|
|
25
|
+
'datadog',
|
|
26
|
+
'bugsnag',
|
|
27
|
+
'intercom',
|
|
28
|
+
'zendesk',
|
|
29
|
+
'apple-search-ads',
|
|
30
|
+
'google-ads',
|
|
31
|
+
'meta-ads',
|
|
32
|
+
'tiktok-ads',
|
|
33
|
+
'vercel',
|
|
34
|
+
'cloudflare',
|
|
35
|
+
'resend',
|
|
36
|
+
'customerio',
|
|
37
|
+
'mailchimp',
|
|
38
|
+
'appfollow',
|
|
39
|
+
'apptweak',
|
|
40
|
+
'linear',
|
|
41
|
+
'postiz',
|
|
42
|
+
];
|
|
43
|
+
const SUPPORTED_CONNECTORS = ['analytics', 'github', 'asc', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify', ...ACCOUNT_SIGNAL_CONNECTORS];
|
|
44
|
+
const ACCOUNT_SIGNAL_SECRET_ENVS = {
|
|
45
|
+
stripe: ['STRIPE_API_KEY'],
|
|
46
|
+
lemonsqueezy: ['LEMON_SQUEEZY_API_KEY'],
|
|
47
|
+
adapty: ['ADAPTY_API_KEY'],
|
|
48
|
+
superwall: ['SUPERWALL_API_KEY'],
|
|
49
|
+
'google-play': ['GOOGLE_PLAY_SERVICE_ACCOUNT_JSON'],
|
|
50
|
+
datadog: ['DATADOG_API_KEY', 'DATADOG_APP_KEY'],
|
|
51
|
+
bugsnag: ['BUGSNAG_AUTH_TOKEN'],
|
|
52
|
+
intercom: ['INTERCOM_ACCESS_TOKEN'],
|
|
53
|
+
zendesk: ['ZENDESK_SUBDOMAIN', 'ZENDESK_EMAIL', 'ZENDESK_API_TOKEN'],
|
|
54
|
+
'apple-search-ads': ['APPLE_SEARCH_ADS_REFRESH_TOKEN'],
|
|
55
|
+
'google-ads': ['GOOGLE_ADS_DEVELOPER_TOKEN', 'GOOGLE_ADS_CLIENT_ID', 'GOOGLE_ADS_CLIENT_SECRET', 'GOOGLE_ADS_REFRESH_TOKEN'],
|
|
56
|
+
'meta-ads': ['META_ADS_ACCESS_TOKEN'],
|
|
57
|
+
'tiktok-ads': ['TIKTOK_ADS_ACCESS_TOKEN'],
|
|
58
|
+
vercel: ['VERCEL_ACCESS_TOKEN'],
|
|
59
|
+
cloudflare: ['CLOUDFLARE_API_TOKEN'],
|
|
60
|
+
resend: ['RESEND_API_KEY'],
|
|
61
|
+
customerio: ['CUSTOMERIO_APP_API_KEY'],
|
|
62
|
+
mailchimp: ['MAILCHIMP_API_KEY'],
|
|
63
|
+
appfollow: ['APPFOLLOW_API_TOKEN'],
|
|
64
|
+
apptweak: ['APPTWEAK_API_TOKEN'],
|
|
65
|
+
linear: ['LINEAR_API_KEY'],
|
|
66
|
+
postiz: ['POSTIZ_API_KEY'],
|
|
67
|
+
};
|
|
18
68
|
function printHelpAndExit(exitCode, reason = null) {
|
|
19
69
|
if (reason) {
|
|
20
70
|
process.stderr.write(`${reason}\n\n`);
|
|
@@ -34,25 +84,45 @@ Options:
|
|
|
34
84
|
--config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
|
|
35
85
|
--project <id> Optional AnalyticsCLI project ID pin for generated source commands
|
|
36
86
|
--asc-app <id> Optional ASC app ID filter (defaults to all accessible apps)
|
|
37
|
-
--connectors <list> Install/enable connector helpers (
|
|
87
|
+
--connectors <list> Install/enable connector helpers (${SUPPORTED_CONNECTORS.join(',')},all)
|
|
38
88
|
--only-connectors <list>
|
|
39
|
-
Limit live preflight checks to
|
|
89
|
+
Limit live preflight checks to ${SUPPORTED_CONNECTORS.join(',')}
|
|
40
90
|
--setup-only Run bootstrap + preflight only (skip first run)
|
|
41
91
|
--no-test-connections Skip live API smoke checks in preflight
|
|
92
|
+
--openclaw-cron <mode> Configure OpenClaw Gateway cron: auto, enable, require, disable (default: auto)
|
|
93
|
+
--openclaw-cron-schedule <expr>
|
|
94
|
+
Cron expression for OpenClaw Gateway cron (default: */30 * * * *)
|
|
95
|
+
--openclaw-cron-tz <tz>
|
|
96
|
+
Timezone for OpenClaw Gateway cron (default: TZ or UTC)
|
|
42
97
|
--progress-json Emit machine-readable setup progress to stderr
|
|
43
98
|
--help, -h Show help
|
|
44
99
|
`);
|
|
45
100
|
process.exit(exitCode);
|
|
46
101
|
}
|
|
102
|
+
function resolveDefaultConfigPath() {
|
|
103
|
+
const explicit = String(process.env.OPENCLAW_GROWTH_CONFIG_PATH || '').trim();
|
|
104
|
+
if (explicit)
|
|
105
|
+
return explicit;
|
|
106
|
+
const homeConfigPath = process.env.HOME ? path.join(process.env.HOME, 'data/openclaw-growth-engineer/config.json') : '';
|
|
107
|
+
const homeStatePath = process.env.HOME ? path.join(process.env.HOME, 'data/openclaw-growth-engineer/state.json') : '';
|
|
108
|
+
if (homeConfigPath && existsSync(homeConfigPath) && existsSync(homeStatePath))
|
|
109
|
+
return homeConfigPath;
|
|
110
|
+
if (!existsSync(DEFAULT_CONFIG_PATH) && homeConfigPath && existsSync(homeConfigPath))
|
|
111
|
+
return homeConfigPath;
|
|
112
|
+
return DEFAULT_CONFIG_PATH;
|
|
113
|
+
}
|
|
47
114
|
function parseArgs(argv) {
|
|
48
115
|
const args = {
|
|
49
|
-
config:
|
|
116
|
+
config: resolveDefaultConfigPath(),
|
|
50
117
|
project: '',
|
|
51
118
|
ascApp: '',
|
|
52
119
|
run: true,
|
|
53
120
|
testConnections: true,
|
|
54
121
|
connectors: [],
|
|
55
122
|
onlyConnectors: [],
|
|
123
|
+
openclawCron: String(process.env.OPENCLAW_GROWTH_OPENCLAW_CRON || 'auto').trim().toLowerCase(),
|
|
124
|
+
openclawCronSchedule: '',
|
|
125
|
+
openclawCronTimezone: '',
|
|
56
126
|
progressJson: false,
|
|
57
127
|
};
|
|
58
128
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -87,6 +157,24 @@ function parseArgs(argv) {
|
|
|
87
157
|
else if (token === '--no-test-connections') {
|
|
88
158
|
args.testConnections = false;
|
|
89
159
|
}
|
|
160
|
+
else if (token === '--openclaw-cron') {
|
|
161
|
+
args.openclawCron = String(next || '').trim().toLowerCase() || 'auto';
|
|
162
|
+
i += 1;
|
|
163
|
+
}
|
|
164
|
+
else if (token === '--enable-openclaw-cron') {
|
|
165
|
+
args.openclawCron = 'enable';
|
|
166
|
+
}
|
|
167
|
+
else if (token === '--no-openclaw-cron') {
|
|
168
|
+
args.openclawCron = 'disable';
|
|
169
|
+
}
|
|
170
|
+
else if (token === '--openclaw-cron-schedule') {
|
|
171
|
+
args.openclawCronSchedule = String(next || '').trim();
|
|
172
|
+
i += 1;
|
|
173
|
+
}
|
|
174
|
+
else if (token === '--openclaw-cron-tz') {
|
|
175
|
+
args.openclawCronTimezone = String(next || '').trim();
|
|
176
|
+
i += 1;
|
|
177
|
+
}
|
|
90
178
|
else if (token === '--progress-json') {
|
|
91
179
|
args.progressJson = true;
|
|
92
180
|
}
|
|
@@ -97,8 +185,15 @@ function parseArgs(argv) {
|
|
|
97
185
|
printHelpAndExit(1, `Unknown argument: ${token}`);
|
|
98
186
|
}
|
|
99
187
|
}
|
|
188
|
+
args.openclawCron = validateOpenClawCronMode(args.openclawCron);
|
|
100
189
|
return args;
|
|
101
190
|
}
|
|
191
|
+
function validateOpenClawCronMode(value) {
|
|
192
|
+
const mode = String(value || 'auto').trim().toLowerCase();
|
|
193
|
+
if (['auto', 'enable', 'require', 'disable'].includes(mode))
|
|
194
|
+
return mode;
|
|
195
|
+
printHelpAndExit(1, `Invalid --openclaw-cron mode: ${value}. Use auto, enable, require, or disable.`);
|
|
196
|
+
}
|
|
102
197
|
function normalizeConnectorKey(value) {
|
|
103
198
|
const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
104
199
|
if (!normalized)
|
|
@@ -113,8 +208,58 @@ function normalizeConnectorKey(value) {
|
|
|
113
208
|
return 'asc';
|
|
114
209
|
if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
|
|
115
210
|
return 'revenuecat';
|
|
211
|
+
if (['paddle', 'paddle-billing', 'billing-metrics', 'web-revenue'].includes(normalized))
|
|
212
|
+
return 'paddle';
|
|
213
|
+
if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo', 'organic-search'].includes(normalized))
|
|
214
|
+
return 'seo';
|
|
116
215
|
if (['sentry', 'sentry-api', 'sentry-mcp', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
|
|
117
216
|
return 'sentry';
|
|
217
|
+
if (['coolify', 'coolify-api', 'deployment', 'deployments', 'hosting', 'infra', 'infrastructure'].includes(normalized))
|
|
218
|
+
return 'coolify';
|
|
219
|
+
if (['stripe', 'stripe-billing', 'stripe-payments'].includes(normalized))
|
|
220
|
+
return 'stripe';
|
|
221
|
+
if (['lemonsqueezy', 'lemon-squeezy', 'lemon', 'ls'].includes(normalized))
|
|
222
|
+
return 'lemonsqueezy';
|
|
223
|
+
if (['adapty', 'adapty-paywalls', 'adapty-subscriptions'].includes(normalized))
|
|
224
|
+
return 'adapty';
|
|
225
|
+
if (['superwall', 'superwall-paywalls'].includes(normalized))
|
|
226
|
+
return 'superwall';
|
|
227
|
+
if (['google-play', 'google-play-console', 'play-console', 'play-store', 'android-store'].includes(normalized))
|
|
228
|
+
return 'google-play';
|
|
229
|
+
if (['datadog', 'datadog-rum', 'datadog-apm', 'datadog-logs'].includes(normalized))
|
|
230
|
+
return 'datadog';
|
|
231
|
+
if (['bugsnag', 'bugsnag-crashes'].includes(normalized))
|
|
232
|
+
return 'bugsnag';
|
|
233
|
+
if (['intercom', 'intercom-support'].includes(normalized))
|
|
234
|
+
return 'intercom';
|
|
235
|
+
if (['zendesk', 'zendesk-support'].includes(normalized))
|
|
236
|
+
return 'zendesk';
|
|
237
|
+
if (['apple-search-ads', 'apple-ads', 'asa', 'search-ads'].includes(normalized))
|
|
238
|
+
return 'apple-search-ads';
|
|
239
|
+
if (['google-ads', 'adwords'].includes(normalized))
|
|
240
|
+
return 'google-ads';
|
|
241
|
+
if (['meta-ads', 'facebook-ads', 'instagram-ads', 'fb-ads'].includes(normalized))
|
|
242
|
+
return 'meta-ads';
|
|
243
|
+
if (['tiktok-ads', 'tiktok-business', 'tiktok-business-api'].includes(normalized))
|
|
244
|
+
return 'tiktok-ads';
|
|
245
|
+
if (['vercel', 'vercel-deployments', 'vercel-hosting'].includes(normalized))
|
|
246
|
+
return 'vercel';
|
|
247
|
+
if (['cloudflare', 'cf', 'cloudflare-workers', 'cloudflare-pages'].includes(normalized))
|
|
248
|
+
return 'cloudflare';
|
|
249
|
+
if (['resend', 'resend-email'].includes(normalized))
|
|
250
|
+
return 'resend';
|
|
251
|
+
if (['customerio', 'customer-io', 'customer.io', 'cio'].includes(normalized))
|
|
252
|
+
return 'customerio';
|
|
253
|
+
if (['mailchimp', 'mailchimp-marketing'].includes(normalized))
|
|
254
|
+
return 'mailchimp';
|
|
255
|
+
if (['appfollow', 'app-follow'].includes(normalized))
|
|
256
|
+
return 'appfollow';
|
|
257
|
+
if (['apptweak', 'app-tweak'].includes(normalized))
|
|
258
|
+
return 'apptweak';
|
|
259
|
+
if (['linear', 'linear-issues', 'linear-planning'].includes(normalized))
|
|
260
|
+
return 'linear';
|
|
261
|
+
if (['postiz', 'postiz-api', 'social-publishing', 'social-scheduler'].includes(normalized))
|
|
262
|
+
return 'postiz';
|
|
118
263
|
return null;
|
|
119
264
|
}
|
|
120
265
|
function parseConnectorList(value) {
|
|
@@ -124,14 +269,10 @@ function parseConnectorList(value) {
|
|
|
124
269
|
for (const entry of String(value).split(',')) {
|
|
125
270
|
const connector = normalizeConnectorKey(entry);
|
|
126
271
|
if (!connector) {
|
|
127
|
-
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use
|
|
272
|
+
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
|
|
128
273
|
}
|
|
129
274
|
if (connector === 'all') {
|
|
130
|
-
connectors.add(
|
|
131
|
-
connectors.add('github');
|
|
132
|
-
connectors.add('asc');
|
|
133
|
-
connectors.add('revenuecat');
|
|
134
|
-
connectors.add('sentry');
|
|
275
|
+
SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
|
|
135
276
|
}
|
|
136
277
|
else {
|
|
137
278
|
connectors.add(connector);
|
|
@@ -156,6 +297,72 @@ function resolveRuntimeScriptPath(scriptName) {
|
|
|
156
297
|
function nodeRuntimeScriptCommand(scriptName) {
|
|
157
298
|
return `node ${quote(resolveRuntimeScriptPath(scriptName))}`;
|
|
158
299
|
}
|
|
300
|
+
function getRuntimeSourceCommand(sourceName) {
|
|
301
|
+
const normalized = String(sourceName || '').trim().toLowerCase();
|
|
302
|
+
if (normalized === 'analytics' || normalized === 'analyticscli') {
|
|
303
|
+
return nodeRuntimeScriptCommand('export-analytics-summary.mjs');
|
|
304
|
+
}
|
|
305
|
+
if (normalized === 'revenuecat' || normalized === 'revenue-cat') {
|
|
306
|
+
return nodeRuntimeScriptCommand('export-revenuecat-summary.mjs');
|
|
307
|
+
}
|
|
308
|
+
if (normalized === 'paddle') {
|
|
309
|
+
return nodeRuntimeScriptCommand('export-paddle-summary.mjs');
|
|
310
|
+
}
|
|
311
|
+
if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo'].includes(normalized)) {
|
|
312
|
+
return nodeRuntimeScriptCommand('export-seo-summary.mjs');
|
|
313
|
+
}
|
|
314
|
+
if (normalized === 'sentry' || normalized === 'glitchtip') {
|
|
315
|
+
return nodeRuntimeScriptCommand('export-sentry-summary.mjs');
|
|
316
|
+
}
|
|
317
|
+
if (normalized === 'coolify') {
|
|
318
|
+
return nodeRuntimeScriptCommand('export-coolify-summary.mjs');
|
|
319
|
+
}
|
|
320
|
+
if (['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(normalized)) {
|
|
321
|
+
return nodeRuntimeScriptCommand('export-asc-summary.mjs');
|
|
322
|
+
}
|
|
323
|
+
return getDefaultSourceCommand(sourceName);
|
|
324
|
+
}
|
|
325
|
+
function replaceLegacyRuntimeScriptCommand(command) {
|
|
326
|
+
const trimmed = String(command || '').trim();
|
|
327
|
+
if (!trimmed)
|
|
328
|
+
return trimmed;
|
|
329
|
+
return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-paddle-summary\.mjs|export-seo-summary\.mjs|export-sentry-summary\.mjs|export-coolify-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
|
|
330
|
+
}
|
|
331
|
+
function normalizeSourceCommand(sourceName, source) {
|
|
332
|
+
return replaceLegacyRuntimeScriptCommand(source?.command || '') || getRuntimeSourceCommand(sourceName);
|
|
333
|
+
}
|
|
334
|
+
function migrateRuntimeSourceCommands(config) {
|
|
335
|
+
if (!config || typeof config !== 'object')
|
|
336
|
+
return config;
|
|
337
|
+
const sources = config.sources && typeof config.sources === 'object' ? config.sources : {};
|
|
338
|
+
const nextSources = { ...sources };
|
|
339
|
+
for (const sourceName of ['analytics', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify']) {
|
|
340
|
+
if (nextSources[sourceName]?.mode === 'command') {
|
|
341
|
+
nextSources[sourceName] = {
|
|
342
|
+
...nextSources[sourceName],
|
|
343
|
+
command: normalizeSourceCommand(sourceName, nextSources[sourceName]),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(nextSources.extra)) {
|
|
348
|
+
nextSources.extra = nextSources.extra.map((source) => {
|
|
349
|
+
if (!source || source.mode !== 'command')
|
|
350
|
+
return source;
|
|
351
|
+
const service = String(source.service || source.key || '').toLowerCase();
|
|
352
|
+
const sourceName = ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service)
|
|
353
|
+
? 'asc'
|
|
354
|
+
: service;
|
|
355
|
+
return {
|
|
356
|
+
...source,
|
|
357
|
+
command: normalizeSourceCommand(sourceName, source),
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
...config,
|
|
363
|
+
sources: nextSources,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
159
366
|
function truncate(value, max = 240) {
|
|
160
367
|
const text = String(value || '');
|
|
161
368
|
if (text.length <= max)
|
|
@@ -184,10 +391,19 @@ function emitProgress(enabled, event) {
|
|
|
184
391
|
return;
|
|
185
392
|
process.stderr.write(`OPENCLAW_PROGRESS ${JSON.stringify(event)}\n`);
|
|
186
393
|
}
|
|
394
|
+
function hardenUnattendedShellCommand(command) {
|
|
395
|
+
return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
|
|
396
|
+
}
|
|
187
397
|
function runShellCommand(command, timeoutMs = 120_000, options = {}) {
|
|
188
398
|
return new Promise((resolve) => {
|
|
189
|
-
const child = spawn(resolveShellCommand(), ['-c', command], {
|
|
399
|
+
const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
|
|
190
400
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
401
|
+
env: {
|
|
402
|
+
...process.env,
|
|
403
|
+
DEBIAN_FRONTEND: 'noninteractive',
|
|
404
|
+
SUDO_ASKPASS: '/bin/false',
|
|
405
|
+
SUDO_PROMPT: '',
|
|
406
|
+
},
|
|
191
407
|
});
|
|
192
408
|
let stdout = '';
|
|
193
409
|
let stderr = '';
|
|
@@ -468,12 +684,15 @@ function isEffectivelyEmptyHeartbeat(value) {
|
|
|
468
684
|
function renderHeartbeatBlock(configPath, config) {
|
|
469
685
|
const interval = formatHeartbeatInterval(getHeartbeatInterval(config));
|
|
470
686
|
const displayConfigPath = relativeWorkspacePath(configPath);
|
|
687
|
+
const displayStatePath = deriveStatePathFromConfigPath(displayConfigPath);
|
|
688
|
+
const runnerCommand = buildGrowthRunnerCommand(displayConfigPath, displayStatePath);
|
|
689
|
+
const wizardCommand = 'npx -y @analyticscli/growth-engineer@preview wizard --connectors';
|
|
471
690
|
return `${HEARTBEAT_MARKER_START}
|
|
472
691
|
tasks:
|
|
473
692
|
|
|
474
693
|
- name: openclaw-growth-engineer-run
|
|
475
694
|
interval: ${interval}
|
|
476
|
-
prompt: "Run
|
|
695
|
+
prompt: "Run \`${runnerCommand}\` from the workspace if the config and runtime files exist. The runner owns schedule.cadences, connectorHealthCheckIntervalMinutes, skipIfNoDataChange, and skipIfIssueSetUnchanged. If it reports connector-health alerts, production crashes, generated issues, or actionable growth findings, summarize only the action and evidence. If setup files are missing, tell the user to run \`${wizardCommand}\`. If there is no actionable output, reply HEARTBEAT_OK."
|
|
477
696
|
|
|
478
697
|
# Keep this section small. Do not put secrets in HEARTBEAT.md.
|
|
479
698
|
${HEARTBEAT_MARKER_END}`;
|
|
@@ -511,6 +730,219 @@ async function ensureGrowthHeartbeat(configPath, config) {
|
|
|
511
730
|
updated: false,
|
|
512
731
|
};
|
|
513
732
|
}
|
|
733
|
+
function applyOpenClawCronOverrides(config, args) {
|
|
734
|
+
const automation = getAutomationConfig(config);
|
|
735
|
+
const cron = {
|
|
736
|
+
...automation.openclawCron,
|
|
737
|
+
...(args.openclawCronSchedule ? { schedule: args.openclawCronSchedule } : {}),
|
|
738
|
+
...(args.openclawCronTimezone ? { timezone: args.openclawCronTimezone } : {}),
|
|
739
|
+
};
|
|
740
|
+
return {
|
|
741
|
+
...config,
|
|
742
|
+
automation: {
|
|
743
|
+
...(config?.automation || {}),
|
|
744
|
+
openclawCron: cron,
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function getOpenClawCronProofCommands(configPath) {
|
|
749
|
+
const displayConfigPath = relativeWorkspacePath(configPath);
|
|
750
|
+
const statePath = deriveStatePathFromConfigPath(displayConfigPath);
|
|
751
|
+
const proofPath = deriveSchedulerProofPathFromStatePath(statePath);
|
|
752
|
+
return {
|
|
753
|
+
listCommand: 'openclaw cron list',
|
|
754
|
+
runsCommand: 'openclaw cron runs --id <job-id>',
|
|
755
|
+
stateCommand: `jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${quote(statePath)}`,
|
|
756
|
+
proofCommand: `tail -n 20 ${quote(proofPath)}`,
|
|
757
|
+
manualWakeCommand: `openclaw system event --text ${quote(`Run OpenClaw Growth Engineer now using config ${relativeWorkspacePath(configPath)} and inspect scheduler proof.`)} --mode now`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
async function ensureOpenClawCronSchedule(configPath, config, mode = 'auto') {
|
|
761
|
+
const normalizedMode = validateOpenClawCronMode(mode);
|
|
762
|
+
const automation = getAutomationConfig(config);
|
|
763
|
+
const proof = getOpenClawCronProofCommands(configPath);
|
|
764
|
+
if (normalizedMode === 'disable' || automation.openclawCron.enabled === false) {
|
|
765
|
+
return {
|
|
766
|
+
ok: true,
|
|
767
|
+
enabled: false,
|
|
768
|
+
installed: false,
|
|
769
|
+
status: 'disabled',
|
|
770
|
+
detail: 'OpenClaw Gateway cron setup disabled',
|
|
771
|
+
proof,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const openclawPath = await resolveCommandPath('openclaw');
|
|
775
|
+
if (!openclawPath) {
|
|
776
|
+
const detail = 'openclaw CLI not found on PATH; skipping OpenClaw Gateway cron setup';
|
|
777
|
+
return {
|
|
778
|
+
ok: normalizedMode === 'auto',
|
|
779
|
+
enabled: true,
|
|
780
|
+
installed: false,
|
|
781
|
+
status: normalizedMode === 'auto' ? 'skipped' : 'failed',
|
|
782
|
+
detail,
|
|
783
|
+
remediation: 'Run this setup inside the VPS shell where OpenClaw Gateway is installed, or install the openclaw CLI.',
|
|
784
|
+
proof,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const displayConfigPath = relativeWorkspacePath(configPath);
|
|
788
|
+
const addCommand = buildOpenClawCronAddCommand(displayConfigPath, config);
|
|
789
|
+
const inspection = await inspectOpenClawCronInstall({
|
|
790
|
+
configPath: displayConfigPath,
|
|
791
|
+
config,
|
|
792
|
+
runCommand: runShellCommand,
|
|
793
|
+
readFile: fs.readFile,
|
|
794
|
+
});
|
|
795
|
+
if (inspection.exists && inspection.verified) {
|
|
796
|
+
return {
|
|
797
|
+
ok: true,
|
|
798
|
+
enabled: true,
|
|
799
|
+
installed: true,
|
|
800
|
+
status: 'already_configured_verified',
|
|
801
|
+
detail: `OpenClaw cron job already exists and matches the Growth Engineer runner contract: ${automation.openclawCron.name}`,
|
|
802
|
+
schedule: automation.openclawCron.schedule,
|
|
803
|
+
timezone: automation.openclawCron.timezone,
|
|
804
|
+
source: inspection.source,
|
|
805
|
+
proof,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
if (inspection.exists && inspection.reason === 'delivery_mismatch') {
|
|
809
|
+
const editCommand = getOpenClawCronEditDeliveryCommandFromInspection(inspection, config);
|
|
810
|
+
if (editCommand) {
|
|
811
|
+
const edit = await runShellCommand(editCommand, 60_000);
|
|
812
|
+
if (edit.ok) {
|
|
813
|
+
return {
|
|
814
|
+
ok: true,
|
|
815
|
+
enabled: true,
|
|
816
|
+
installed: true,
|
|
817
|
+
status: 'repaired_delivery_cli',
|
|
818
|
+
detail: `Repaired OpenClaw cron delivery with: ${editCommand}`,
|
|
819
|
+
schedule: automation.openclawCron.schedule,
|
|
820
|
+
timezone: automation.openclawCron.timezone,
|
|
821
|
+
command: editCommand,
|
|
822
|
+
source: inspection.source,
|
|
823
|
+
proof,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const repair = await repairOpenClawCronDeliveryStore({
|
|
828
|
+
configPath: displayConfigPath,
|
|
829
|
+
config,
|
|
830
|
+
readFile: fs.readFile,
|
|
831
|
+
writeFile: fs.writeFile,
|
|
832
|
+
});
|
|
833
|
+
if (repair.repaired) {
|
|
834
|
+
return {
|
|
835
|
+
ok: true,
|
|
836
|
+
enabled: true,
|
|
837
|
+
installed: true,
|
|
838
|
+
status: 'repaired_delivery',
|
|
839
|
+
detail: `Repaired OpenClaw cron delivery for "${automation.openclawCron.name}" in ${repair.path}`,
|
|
840
|
+
schedule: automation.openclawCron.schedule,
|
|
841
|
+
timezone: automation.openclawCron.timezone,
|
|
842
|
+
source: repair.path,
|
|
843
|
+
command: editCommand || undefined,
|
|
844
|
+
proof,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const add = await runShellCommand(addCommand, 60_000);
|
|
849
|
+
const existingDetail = inspection.exists
|
|
850
|
+
? `Existing OpenClaw cron job "${automation.openclawCron.name}" was not verifiably wired to the current runner contract (${inspection.reason} via ${inspection.source}). `
|
|
851
|
+
: '';
|
|
852
|
+
return {
|
|
853
|
+
ok: add.ok || (normalizedMode === 'auto' && !inspection.exists),
|
|
854
|
+
enabled: true,
|
|
855
|
+
installed: add.ok,
|
|
856
|
+
status: add.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
|
|
857
|
+
detail: add.ok
|
|
858
|
+
? `${existingDetail}Configured OpenClaw cron job "${automation.openclawCron.name}" (${automation.openclawCron.schedule}, ${automation.openclawCron.timezone})`
|
|
859
|
+
: `${existingDetail}${add.stderr.trim() || add.stdout.trim() || `openclaw cron add exited ${add.code}`}`,
|
|
860
|
+
schedule: automation.openclawCron.schedule,
|
|
861
|
+
timezone: automation.openclawCron.timezone,
|
|
862
|
+
command: addCommand,
|
|
863
|
+
remediation: inspection.exists && !add.ok
|
|
864
|
+
? `Remove the stale OpenClaw cron job named "${automation.openclawCron.name}" with your installed OpenClaw CLI, then rerun: ${addCommand}`
|
|
865
|
+
: undefined,
|
|
866
|
+
proof,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
async function ensureHermesCronSchedule(configPath, config, mode = 'auto') {
|
|
870
|
+
const normalizedMode = validateOpenClawCronMode(mode);
|
|
871
|
+
const automation = getAutomationConfig(config);
|
|
872
|
+
const displayConfigPath = relativeWorkspacePath(configPath);
|
|
873
|
+
const statePath = deriveStatePathFromConfigPath(displayConfigPath);
|
|
874
|
+
const proofPath = deriveSchedulerProofPathFromStatePath(statePath);
|
|
875
|
+
const workdir = path.resolve(automation.hermesCron.workdir || process.cwd());
|
|
876
|
+
const proof = {
|
|
877
|
+
listCommand: 'hermes cron list',
|
|
878
|
+
statusCommand: 'hermes cron status <job-id>',
|
|
879
|
+
stateCommand: `jq '.connectorHealth, .cadences, .lastRunAt, .skippedReason' ${quote(statePath)}`,
|
|
880
|
+
proofCommand: `tail -n 20 ${quote(proofPath)}`,
|
|
881
|
+
};
|
|
882
|
+
if (normalizedMode === 'disable' || automation.hermesCron.enabled === false) {
|
|
883
|
+
return {
|
|
884
|
+
ok: true,
|
|
885
|
+
enabled: false,
|
|
886
|
+
installed: false,
|
|
887
|
+
status: 'disabled',
|
|
888
|
+
detail: 'Hermes cron setup disabled',
|
|
889
|
+
proof,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const hermesPath = await resolveCommandPath('hermes');
|
|
893
|
+
if (!hermesPath) {
|
|
894
|
+
return {
|
|
895
|
+
ok: normalizedMode === 'auto',
|
|
896
|
+
enabled: true,
|
|
897
|
+
installed: false,
|
|
898
|
+
status: normalizedMode === 'auto' ? 'skipped' : 'failed',
|
|
899
|
+
detail: 'hermes CLI not found on PATH; skipping Hermes cron setup',
|
|
900
|
+
remediation: 'Run this setup inside the host shell where Hermes Gateway is installed, or install the hermes CLI.',
|
|
901
|
+
proof,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
const createCommand = buildHermesCronCreateCommand(displayConfigPath, config, { workdir });
|
|
905
|
+
const inspection = await inspectHermesCronInstall({
|
|
906
|
+
configPath: displayConfigPath,
|
|
907
|
+
config,
|
|
908
|
+
runCommand: runShellCommand,
|
|
909
|
+
readFile: fs.readFile,
|
|
910
|
+
workdir,
|
|
911
|
+
});
|
|
912
|
+
if (inspection.exists && inspection.verified) {
|
|
913
|
+
return {
|
|
914
|
+
ok: true,
|
|
915
|
+
enabled: true,
|
|
916
|
+
installed: true,
|
|
917
|
+
status: 'already_configured_verified',
|
|
918
|
+
detail: `Hermes cron job already exists and matches the Growth Engineer runner contract: ${automation.hermesCron.name}`,
|
|
919
|
+
schedule: automation.hermesCron.schedule,
|
|
920
|
+
workdir,
|
|
921
|
+
source: inspection.source,
|
|
922
|
+
proof,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
const create = await runShellCommand(createCommand, 60_000);
|
|
926
|
+
const existingDetail = inspection.exists
|
|
927
|
+
? `Existing Hermes cron job "${automation.hermesCron.name}" was not verifiably wired to the current runner contract (${inspection.reason} via ${inspection.source}). `
|
|
928
|
+
: '';
|
|
929
|
+
return {
|
|
930
|
+
ok: create.ok || (normalizedMode === 'auto' && !inspection.exists),
|
|
931
|
+
enabled: true,
|
|
932
|
+
installed: create.ok,
|
|
933
|
+
status: create.ok ? (inspection.exists ? 'reconfigured' : 'configured') : inspection.exists ? 'needs_repair' : 'failed',
|
|
934
|
+
detail: create.ok
|
|
935
|
+
? `${existingDetail}Configured Hermes cron job "${automation.hermesCron.name}" (${automation.hermesCron.schedule})`
|
|
936
|
+
: `${existingDetail}${create.stderr.trim() || create.stdout.trim() || `hermes cron create exited ${create.code}`}`,
|
|
937
|
+
schedule: automation.hermesCron.schedule,
|
|
938
|
+
workdir,
|
|
939
|
+
command: createCommand,
|
|
940
|
+
remediation: inspection.exists && !create.ok
|
|
941
|
+
? `Remove the stale Hermes cron job named "${automation.hermesCron.name}" with your installed Hermes CLI, then rerun: ${createCommand}`
|
|
942
|
+
: undefined,
|
|
943
|
+
proof,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
514
946
|
async function appendHelperDetail(details, label, result) {
|
|
515
947
|
if (result.ok) {
|
|
516
948
|
details.push(`${label}: ok`);
|
|
@@ -769,6 +1201,13 @@ async function installSentryConnector() {
|
|
|
769
1201
|
details.push('Sentry direct API exporter enabled via node scripts/export-sentry-summary.mjs');
|
|
770
1202
|
return { connector: 'sentry', ok: true, detail: details.join('; ') };
|
|
771
1203
|
}
|
|
1204
|
+
async function installCoolifyConnector() {
|
|
1205
|
+
return {
|
|
1206
|
+
connector: 'coolify',
|
|
1207
|
+
ok: true,
|
|
1208
|
+
detail: 'Coolify uses the built-in read-only API exporter; create a token in Coolify Keys & Tokens / API tokens and store COOLIFY_BASE_URL plus COOLIFY_API_TOKEN with the connector wizard',
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
772
1211
|
async function installGitHubConnector() {
|
|
773
1212
|
const details = [];
|
|
774
1213
|
await installClawHubSkill('github', details);
|
|
@@ -809,6 +1248,20 @@ async function installAnalyticsConnector() {
|
|
|
809
1248
|
: 'analyticscli binary missing after dependency setup',
|
|
810
1249
|
};
|
|
811
1250
|
}
|
|
1251
|
+
function isAccountSignalConnector(connector) {
|
|
1252
|
+
return ACCOUNT_SIGNAL_CONNECTORS.includes(connector);
|
|
1253
|
+
}
|
|
1254
|
+
async function installAccountSignalConnector(connector) {
|
|
1255
|
+
const envs = ACCOUNT_SIGNAL_SECRET_ENVS[connector] || [];
|
|
1256
|
+
const missing = envs.filter((envName) => !String(process.env[envName] || '').trim());
|
|
1257
|
+
return {
|
|
1258
|
+
connector,
|
|
1259
|
+
ok: missing.length === 0,
|
|
1260
|
+
detail: missing.length === 0
|
|
1261
|
+
? 'account-wide credential is present; project/app/product scope is discovered later'
|
|
1262
|
+
: `missing account-wide credential(s): ${missing.join(', ')}`,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
812
1265
|
async function enableConnectorConfig(configPath, connectors) {
|
|
813
1266
|
if (connectors.length === 0 || !(await fileExists(configPath)))
|
|
814
1267
|
return;
|
|
@@ -819,17 +1272,35 @@ async function enableConnectorConfig(configPath, connectors) {
|
|
|
819
1272
|
sources: {
|
|
820
1273
|
...(config.sources || {}),
|
|
821
1274
|
analytics: connectors.includes('analytics')
|
|
822
|
-
? { ...(config.sources?.analytics || {}), enabled: true, mode: 'command', command: config.sources?.analytics
|
|
1275
|
+
? { ...(config.sources?.analytics || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('analytics', config.sources?.analytics) }
|
|
823
1276
|
: config.sources?.analytics,
|
|
824
1277
|
revenuecat: connectors.includes('revenuecat')
|
|
825
|
-
? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command:
|
|
1278
|
+
? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('revenuecat', config.sources?.revenuecat) }
|
|
826
1279
|
: config.sources?.revenuecat,
|
|
1280
|
+
paddle: connectors.includes('paddle')
|
|
1281
|
+
? { ...(config.sources?.paddle || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('paddle', config.sources?.paddle) }
|
|
1282
|
+
: config.sources?.paddle,
|
|
1283
|
+
seo: connectors.includes('seo')
|
|
1284
|
+
? { ...(config.sources?.seo || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('seo', config.sources?.seo) }
|
|
1285
|
+
: config.sources?.seo,
|
|
827
1286
|
sentry: connectors.includes('sentry')
|
|
828
|
-
? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command:
|
|
1287
|
+
? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('sentry', config.sources?.sentry) }
|
|
829
1288
|
: config.sources?.sentry,
|
|
1289
|
+
coolify: connectors.includes('coolify')
|
|
1290
|
+
? {
|
|
1291
|
+
...(config.sources?.coolify || {}),
|
|
1292
|
+
enabled: true,
|
|
1293
|
+
mode: 'command',
|
|
1294
|
+
command: normalizeSourceCommand('coolify', config.sources?.coolify),
|
|
1295
|
+
baseUrl: config.sources?.coolify?.baseUrl || process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com',
|
|
1296
|
+
tokenEnv: config.sources?.coolify?.tokenEnv || 'COOLIFY_API_TOKEN',
|
|
1297
|
+
}
|
|
1298
|
+
: config.sources?.coolify,
|
|
830
1299
|
extra: extra.map((source) => connectors.includes('asc') && source?.service === 'asc-cli'
|
|
831
|
-
? { ...source, enabled: true, mode: 'command', command:
|
|
832
|
-
: source)
|
|
1300
|
+
? { ...source, enabled: true, mode: 'command', command: normalizeSourceCommand('asc', source) }
|
|
1301
|
+
: connectors.includes(String(source?.key || source?.service || '')) && isAccountSignalConnector(String(source?.key || source?.service || ''))
|
|
1302
|
+
? { ...source, enabled: true, mode: source.mode || 'file' }
|
|
1303
|
+
: source),
|
|
833
1304
|
},
|
|
834
1305
|
};
|
|
835
1306
|
await writeJson(configPath, next);
|
|
@@ -846,8 +1317,16 @@ async function installConnectorHelpers(configPath, connectors) {
|
|
|
846
1317
|
results.push(await installAscConnector());
|
|
847
1318
|
if (connector === 'revenuecat')
|
|
848
1319
|
results.push(await installRevenueCatConnector());
|
|
1320
|
+
if (connector === 'paddle')
|
|
1321
|
+
results.push({ connector, ok: true, detail: 'Paddle uses the built-in metrics exporter; token is checked during preflight' });
|
|
1322
|
+
if (connector === 'seo')
|
|
1323
|
+
results.push({ connector, ok: true, detail: 'SEO/GSC uses the built-in exporter; credentials or CSV inputs are checked during preflight' });
|
|
849
1324
|
if (connector === 'sentry')
|
|
850
1325
|
results.push(await installSentryConnector());
|
|
1326
|
+
if (connector === 'coolify')
|
|
1327
|
+
results.push(await installCoolifyConnector());
|
|
1328
|
+
if (isAccountSignalConnector(connector))
|
|
1329
|
+
results.push(await installAccountSignalConnector(connector));
|
|
851
1330
|
}
|
|
852
1331
|
return results;
|
|
853
1332
|
}
|
|
@@ -878,7 +1357,9 @@ async function detectGitHubRepo() {
|
|
|
878
1357
|
}
|
|
879
1358
|
async function ensureConfig(configPath) {
|
|
880
1359
|
if (await fileExists(configPath)) {
|
|
881
|
-
const
|
|
1360
|
+
const originalConfig = await readJson(configPath);
|
|
1361
|
+
const config = migrateRuntimeSourceCommands(originalConfig);
|
|
1362
|
+
let changed = JSON.stringify(originalConfig.sources || {}) !== JSON.stringify(config.sources || {});
|
|
882
1363
|
if (!isConfiguredGitHubRepo(config?.project?.githubRepo)) {
|
|
883
1364
|
const detectedRepo = await detectGitHubRepo();
|
|
884
1365
|
if (detectedRepo) {
|
|
@@ -886,14 +1367,17 @@ async function ensureConfig(configPath) {
|
|
|
886
1367
|
...(config.project || {}),
|
|
887
1368
|
githubRepo: detectedRepo,
|
|
888
1369
|
};
|
|
889
|
-
|
|
890
|
-
return {
|
|
891
|
-
created: false,
|
|
892
|
-
configPath,
|
|
893
|
-
githubRepo: detectedRepo,
|
|
894
|
-
};
|
|
1370
|
+
changed = true;
|
|
895
1371
|
}
|
|
896
1372
|
}
|
|
1373
|
+
if (changed) {
|
|
1374
|
+
await writeJson(configPath, config);
|
|
1375
|
+
return {
|
|
1376
|
+
created: false,
|
|
1377
|
+
configPath,
|
|
1378
|
+
githubRepo: config.project?.githubRepo || null,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
897
1381
|
return {
|
|
898
1382
|
created: false,
|
|
899
1383
|
configPath,
|
|
@@ -917,19 +1401,27 @@ async function ensureConfig(configPath) {
|
|
|
917
1401
|
analytics: {
|
|
918
1402
|
enabled: true,
|
|
919
1403
|
mode: 'command',
|
|
920
|
-
command:
|
|
1404
|
+
command: getRuntimeSourceCommand('analytics'),
|
|
921
1405
|
},
|
|
922
1406
|
revenuecat: {
|
|
923
1407
|
...(template.sources?.revenuecat || {}),
|
|
924
1408
|
enabled: false,
|
|
925
1409
|
mode: 'command',
|
|
926
|
-
command:
|
|
1410
|
+
command: getRuntimeSourceCommand('revenuecat'),
|
|
927
1411
|
},
|
|
928
1412
|
sentry: {
|
|
929
1413
|
...(template.sources?.sentry || {}),
|
|
930
1414
|
enabled: false,
|
|
931
1415
|
mode: 'command',
|
|
932
|
-
command:
|
|
1416
|
+
command: getRuntimeSourceCommand('sentry'),
|
|
1417
|
+
},
|
|
1418
|
+
coolify: {
|
|
1419
|
+
...(template.sources?.coolify || {}),
|
|
1420
|
+
enabled: false,
|
|
1421
|
+
mode: 'command',
|
|
1422
|
+
command: getRuntimeSourceCommand('coolify'),
|
|
1423
|
+
baseUrl: template.sources?.coolify?.baseUrl || process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com',
|
|
1424
|
+
tokenEnv: template.sources?.coolify?.tokenEnv || 'COOLIFY_API_TOKEN',
|
|
933
1425
|
},
|
|
934
1426
|
feedback: {
|
|
935
1427
|
...(template.sources?.feedback || {}),
|
|
@@ -940,7 +1432,7 @@ async function ensureConfig(configPath) {
|
|
|
940
1432
|
actions: {
|
|
941
1433
|
...template.actions,
|
|
942
1434
|
mode: 'issue',
|
|
943
|
-
autoCreateIssues:
|
|
1435
|
+
autoCreateIssues: true,
|
|
944
1436
|
autoCreatePullRequests: false,
|
|
945
1437
|
draftPullRequests: true,
|
|
946
1438
|
proposalBranchPrefix: 'openclaw/proposals',
|
|
@@ -1022,6 +1514,15 @@ function appendProjectFlag(command, projectId) {
|
|
|
1022
1514
|
function commandHasAscAppFlag(command) {
|
|
1023
1515
|
return /(^|\s)--app(\s|=|$)/.test(String(command || ''));
|
|
1024
1516
|
}
|
|
1517
|
+
function removeAscAppFlag(command) {
|
|
1518
|
+
const raw = String(command || '').trim();
|
|
1519
|
+
if (!raw || !commandHasAscAppFlag(raw))
|
|
1520
|
+
return raw;
|
|
1521
|
+
return raw
|
|
1522
|
+
.replace(/(^|\s)--app(?:=(?:"[^"]*"|'[^']*'|\S+)|\s+(?:"[^"]*"|'[^']*'|\S+))/g, ' ')
|
|
1523
|
+
.replace(/\s+/g, ' ')
|
|
1524
|
+
.trim();
|
|
1525
|
+
}
|
|
1025
1526
|
function appendAscAppFlag(command, appId) {
|
|
1026
1527
|
const raw = String(command || '').trim();
|
|
1027
1528
|
if (!raw || commandHasAscAppFlag(raw))
|
|
@@ -1091,6 +1592,34 @@ async function configureAscApp(configPath, appId) {
|
|
|
1091
1592
|
process.env.ASC_APP_ID = normalizedAppId;
|
|
1092
1593
|
return changed;
|
|
1093
1594
|
}
|
|
1595
|
+
async function configureAscAllApps(configPath) {
|
|
1596
|
+
const config = await readJson(configPath);
|
|
1597
|
+
let changed = false;
|
|
1598
|
+
const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
|
|
1599
|
+
for (const source of extraSources) {
|
|
1600
|
+
if (!source || typeof source !== 'object')
|
|
1601
|
+
continue;
|
|
1602
|
+
const service = String(source.service || source.key || '').trim().toLowerCase();
|
|
1603
|
+
if (!['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service))
|
|
1604
|
+
continue;
|
|
1605
|
+
if (source.mode === 'command' && source.command) {
|
|
1606
|
+
const nextCommand = removeAscAppFlag(source.command);
|
|
1607
|
+
if (nextCommand !== source.command) {
|
|
1608
|
+
source.command = nextCommand;
|
|
1609
|
+
changed = true;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (config.project && typeof config.project === 'object' && config.project.ascAppId) {
|
|
1614
|
+
delete config.project.ascAppId;
|
|
1615
|
+
changed = true;
|
|
1616
|
+
}
|
|
1617
|
+
if (changed) {
|
|
1618
|
+
await writeJson(configPath, config);
|
|
1619
|
+
}
|
|
1620
|
+
delete process.env.ASC_APP_ID;
|
|
1621
|
+
return changed;
|
|
1622
|
+
}
|
|
1094
1623
|
function configHasEnabledAscSource(config) {
|
|
1095
1624
|
const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
|
|
1096
1625
|
return extraSources.some((source) => {
|
|
@@ -1166,11 +1695,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
|
1166
1695
|
if (!configHasEnabledAscSource(config)) {
|
|
1167
1696
|
return { ok: true, configured: false, changed: false, appId: null, appScope: 'disabled', needsUserInput: false };
|
|
1168
1697
|
}
|
|
1169
|
-
const
|
|
1170
|
-
if (configuredAppId) {
|
|
1171
|
-
const changed = await configureAscApp(configPath, configuredAppId);
|
|
1172
|
-
return { ok: true, configured: true, changed, appId: configuredAppId, appScope: 'single_app', needsUserInput: false };
|
|
1173
|
-
}
|
|
1698
|
+
const changed = await configureAscAllApps(configPath);
|
|
1174
1699
|
const appList = await listAscApps();
|
|
1175
1700
|
if (!appList.ok) {
|
|
1176
1701
|
return {
|
|
@@ -1185,7 +1710,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
|
1185
1710
|
return {
|
|
1186
1711
|
ok: true,
|
|
1187
1712
|
configured: true,
|
|
1188
|
-
changed
|
|
1713
|
+
changed,
|
|
1189
1714
|
appId: null,
|
|
1190
1715
|
appScope: 'all_accessible_apps',
|
|
1191
1716
|
apps: appList.apps,
|
|
@@ -1193,6 +1718,131 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
|
1193
1718
|
needsUserInput: false,
|
|
1194
1719
|
};
|
|
1195
1720
|
}
|
|
1721
|
+
function extractAscAnalyticsRequestIds(payload) {
|
|
1722
|
+
const candidates = (() => {
|
|
1723
|
+
if (Array.isArray(payload))
|
|
1724
|
+
return payload;
|
|
1725
|
+
if (payload && typeof payload === 'object') {
|
|
1726
|
+
if (Array.isArray(payload.requests))
|
|
1727
|
+
return payload.requests;
|
|
1728
|
+
if (Array.isArray(payload.analyticsReportRequests))
|
|
1729
|
+
return payload.analyticsReportRequests;
|
|
1730
|
+
if (Array.isArray(payload.items))
|
|
1731
|
+
return payload.items;
|
|
1732
|
+
if (Array.isArray(payload.data))
|
|
1733
|
+
return payload.data;
|
|
1734
|
+
}
|
|
1735
|
+
return [];
|
|
1736
|
+
})();
|
|
1737
|
+
const ids = [];
|
|
1738
|
+
for (const candidate of candidates) {
|
|
1739
|
+
if (!candidate || typeof candidate !== 'object')
|
|
1740
|
+
continue;
|
|
1741
|
+
const id = normalizeString(candidate.id) || normalizeString(candidate.requestId) || normalizeString(candidate.request_id);
|
|
1742
|
+
if (id)
|
|
1743
|
+
ids.push(id);
|
|
1744
|
+
}
|
|
1745
|
+
return [...new Set(ids)];
|
|
1746
|
+
}
|
|
1747
|
+
function extractAscAnalyticsRequestId(payload) {
|
|
1748
|
+
if (payload && typeof payload === 'object') {
|
|
1749
|
+
const id = normalizeString(payload.id) || normalizeString(payload.requestId) || normalizeString(payload.request_id);
|
|
1750
|
+
if (id)
|
|
1751
|
+
return id;
|
|
1752
|
+
}
|
|
1753
|
+
return extractAscAnalyticsRequestIds(payload)[0] || null;
|
|
1754
|
+
}
|
|
1755
|
+
async function listAscAnalyticsRequests(appId, state = '') {
|
|
1756
|
+
const stateArg = state ? ` --state ${quote(state)}` : '';
|
|
1757
|
+
const result = await runShellCommand(`asc analytics requests --app ${quote(appId)}${stateArg} --output json`, 60_000);
|
|
1758
|
+
if (!result.ok) {
|
|
1759
|
+
return { ok: false, ids: [], error: result.stderr || `exit ${result.code}` };
|
|
1760
|
+
}
|
|
1761
|
+
return { ok: true, ids: extractAscAnalyticsRequestIds(parseJsonFromStdout(result.stdout)), error: null };
|
|
1762
|
+
}
|
|
1763
|
+
async function ensureAscAnalyticsRequest(appId) {
|
|
1764
|
+
const normalizedAppId = normalizeString(appId);
|
|
1765
|
+
if (!normalizedAppId) {
|
|
1766
|
+
return { ok: true, status: 'skipped', requestId: null, detail: 'no single ASC app configured' };
|
|
1767
|
+
}
|
|
1768
|
+
const completedRequests = await listAscAnalyticsRequests(normalizedAppId, 'COMPLETED');
|
|
1769
|
+
if (!completedRequests.ok) {
|
|
1770
|
+
return { ok: false, status: 'query_failed', requestId: null, error: completedRequests.error };
|
|
1771
|
+
}
|
|
1772
|
+
if (completedRequests.ids.length > 0) {
|
|
1773
|
+
return { ok: true, status: 'completed', requestId: completedRequests.ids[0], detail: `completed request ${completedRequests.ids[0]}` };
|
|
1774
|
+
}
|
|
1775
|
+
const existingRequests = await listAscAnalyticsRequests(normalizedAppId);
|
|
1776
|
+
if (!existingRequests.ok) {
|
|
1777
|
+
return { ok: false, status: 'query_failed', requestId: null, error: existingRequests.error };
|
|
1778
|
+
}
|
|
1779
|
+
if (existingRequests.ids.length > 0) {
|
|
1780
|
+
return { ok: true, status: 'pending', requestId: existingRequests.ids[0], detail: `existing request ${existingRequests.ids[0]} is not completed yet` };
|
|
1781
|
+
}
|
|
1782
|
+
const created = await runShellCommand(`asc analytics request --app ${quote(normalizedAppId)} --access-type ONGOING --output json`, 60_000);
|
|
1783
|
+
if (!created.ok) {
|
|
1784
|
+
return { ok: false, status: 'create_failed', requestId: null, error: created.stderr || `exit ${created.code}` };
|
|
1785
|
+
}
|
|
1786
|
+
const requestId = extractAscAnalyticsRequestId(parseJsonFromStdout(created.stdout));
|
|
1787
|
+
return {
|
|
1788
|
+
ok: true,
|
|
1789
|
+
status: 'created',
|
|
1790
|
+
requestId,
|
|
1791
|
+
detail: requestId
|
|
1792
|
+
? `created ongoing request ${requestId}; report instances will appear after Apple processing`
|
|
1793
|
+
: 'created ongoing request; report instances will appear after Apple processing',
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
async function ensureAscAnalyticsRequestsForAppScope(ascAppSetup) {
|
|
1797
|
+
if (!ascAppSetup?.ok || ascAppSetup.appScope === 'disabled') {
|
|
1798
|
+
return { ok: true, status: 'skipped', requestId: null, requestIds: [], detail: 'ASC source is not enabled', results: [] };
|
|
1799
|
+
}
|
|
1800
|
+
const apps = ascAppSetup.appId
|
|
1801
|
+
? [{ id: ascAppSetup.appId }]
|
|
1802
|
+
: Array.isArray(ascAppSetup.apps)
|
|
1803
|
+
? ascAppSetup.apps.filter((app) => normalizeString(app?.id))
|
|
1804
|
+
: [];
|
|
1805
|
+
if (apps.length === 0) {
|
|
1806
|
+
return { ok: true, status: 'skipped', requestId: null, requestIds: [], detail: 'no accessible ASC apps found', results: [] };
|
|
1807
|
+
}
|
|
1808
|
+
const results = [];
|
|
1809
|
+
for (const app of apps) {
|
|
1810
|
+
const result = await ensureAscAnalyticsRequest(app.id);
|
|
1811
|
+
results.push({
|
|
1812
|
+
appId: app.id,
|
|
1813
|
+
appName: app.name || null,
|
|
1814
|
+
...result,
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
const failures = results.filter((result) => !result.ok);
|
|
1818
|
+
if (failures.length > 0) {
|
|
1819
|
+
return {
|
|
1820
|
+
ok: false,
|
|
1821
|
+
status: failures.length === results.length ? 'failed' : 'partial_failed',
|
|
1822
|
+
requestId: null,
|
|
1823
|
+
requestIds: results.map((result) => result.requestId).filter(Boolean),
|
|
1824
|
+
results,
|
|
1825
|
+
error: failures
|
|
1826
|
+
.map((failure) => `${failure.appName || failure.appId}: ${failure.error || failure.status || 'unknown error'}`)
|
|
1827
|
+
.join('; '),
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
const counts = results.reduce((memo, result) => {
|
|
1831
|
+
memo[result.status] = (memo[result.status] || 0) + 1;
|
|
1832
|
+
return memo;
|
|
1833
|
+
}, {});
|
|
1834
|
+
const detail = Object.entries(counts)
|
|
1835
|
+
.map(([status, count]) => `${count} ${status}`)
|
|
1836
|
+
.join(', ');
|
|
1837
|
+
return {
|
|
1838
|
+
ok: true,
|
|
1839
|
+
status: 'ok',
|
|
1840
|
+
requestId: results[0]?.requestId || null,
|
|
1841
|
+
requestIds: results.map((result) => result.requestId).filter(Boolean),
|
|
1842
|
+
detail: `checked ${results.length} ASC app(s): ${detail}`,
|
|
1843
|
+
results,
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1196
1846
|
async function listAnalyticsProjects() {
|
|
1197
1847
|
const result = await runShellCommand('analyticscli projects list --format json', 60_000);
|
|
1198
1848
|
if (!result.ok) {
|
|
@@ -1286,7 +1936,7 @@ function remediationForCheck(checkName, configPath) {
|
|
|
1286
1936
|
return 'Write `data/openclaw-growth-engineer/analytics_summary.json` via your analytics refresh step (API-key based source command/file generation).';
|
|
1287
1937
|
}
|
|
1288
1938
|
if (checkName === 'connection:analytics') {
|
|
1289
|
-
return 'Run `
|
|
1939
|
+
return 'Run `npx -y @analyticscli/growth-engineer@preview wizard --connectors analytics` and paste a fresh AnalyticsCLI readonly CLI token into the local terminal wizard.';
|
|
1290
1940
|
}
|
|
1291
1941
|
if (checkName === 'connection:github') {
|
|
1292
1942
|
return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>` + issues API.';
|
|
@@ -1295,7 +1945,7 @@ function remediationForCheck(checkName, configPath) {
|
|
|
1295
1945
|
return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>/pulls`, plus `Pull requests: Read/Write` and `Contents: Read/Write` scopes.';
|
|
1296
1946
|
}
|
|
1297
1947
|
if (checkName === 'connection:asc_cli') {
|
|
1298
|
-
return 'ASC setup should list App Store Connect apps
|
|
1948
|
+
return 'ASC setup should list all App Store Connect apps the API key can access. Rerun the connector wizard with an API key scoped to the whole account; do not set ASC_APP_ID for normal Growth Engineer runs.';
|
|
1299
1949
|
}
|
|
1300
1950
|
return 'Fix this blocker and rerun start.';
|
|
1301
1951
|
}
|
|
@@ -1346,17 +1996,61 @@ async function runPreflight(configPath, testConnections, progressJson = false, o
|
|
|
1346
1996
|
};
|
|
1347
1997
|
}
|
|
1348
1998
|
async function runFirstPass(configPath) {
|
|
1349
|
-
const
|
|
1999
|
+
const statePath = path.resolve(deriveStatePathFromConfigPath(configPath));
|
|
2000
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-runner.mjs')} --config ${quote(configPath)} --state ${quote(statePath)}`;
|
|
1350
2001
|
return runShellCommand(command, 300_000);
|
|
1351
2002
|
}
|
|
1352
2003
|
async function main() {
|
|
1353
2004
|
await loadOpenClawGrowthSecrets();
|
|
1354
2005
|
const args = parseArgs(process.argv.slice(2));
|
|
1355
2006
|
const configPath = path.resolve(args.config);
|
|
2007
|
+
const schedulerProofPath = path.resolve(deriveSchedulerProofPathFromStatePath(deriveStatePathFromConfigPath(configPath)));
|
|
1356
2008
|
const configResult = await ensureConfig(configPath);
|
|
1357
|
-
const initialConfig = await readJson(configPath);
|
|
2009
|
+
const initialConfig = applyOpenClawCronOverrides(await readJson(configPath), args);
|
|
1358
2010
|
await applyOpenClawSecretRefs(initialConfig);
|
|
1359
2011
|
const heartbeat = await ensureGrowthHeartbeat(configPath, initialConfig);
|
|
2012
|
+
const openclawCron = await ensureOpenClawCronSchedule(configPath, initialConfig, args.openclawCron);
|
|
2013
|
+
const hermesCron = await ensureHermesCronSchedule(configPath, initialConfig, args.openclawCron);
|
|
2014
|
+
if (!openclawCron.ok) {
|
|
2015
|
+
process.stdout.write(`${JSON.stringify({
|
|
2016
|
+
ok: false,
|
|
2017
|
+
phase: 'openclaw_cron_setup',
|
|
2018
|
+
configCreated: configResult.created,
|
|
2019
|
+
configPath,
|
|
2020
|
+
heartbeat,
|
|
2021
|
+
openclawCron,
|
|
2022
|
+
hermesCron,
|
|
2023
|
+
blockers: [
|
|
2024
|
+
{
|
|
2025
|
+
check: 'scheduler:openclaw-cron',
|
|
2026
|
+
detail: openclawCron.detail,
|
|
2027
|
+
remediation: openclawCron.remediation || 'Fix OpenClaw cron setup and rerun start.',
|
|
2028
|
+
},
|
|
2029
|
+
],
|
|
2030
|
+
}, null, 2)}\n`);
|
|
2031
|
+
process.exitCode = 1;
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (!hermesCron.ok) {
|
|
2035
|
+
process.stdout.write(`${JSON.stringify({
|
|
2036
|
+
ok: false,
|
|
2037
|
+
phase: 'hermes_cron_setup',
|
|
2038
|
+
configCreated: configResult.created,
|
|
2039
|
+
configPath,
|
|
2040
|
+
heartbeat,
|
|
2041
|
+
openclawCron,
|
|
2042
|
+
hermesCron,
|
|
2043
|
+
blockers: [
|
|
2044
|
+
{
|
|
2045
|
+
check: 'scheduler:hermes-cron',
|
|
2046
|
+
detail: hermesCron.detail,
|
|
2047
|
+
remediation: hermesCron.remediation || 'Fix Hermes cron setup and rerun start.',
|
|
2048
|
+
},
|
|
2049
|
+
],
|
|
2050
|
+
}, null, 2)}\n`);
|
|
2051
|
+
process.exitCode = 1;
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
1360
2054
|
const projectConfigured = await configureAnalyticsProject(configPath, args.project);
|
|
1361
2055
|
const ascAppConfiguredFromArg = await configureAscApp(configPath, args.ascApp);
|
|
1362
2056
|
const analyticscliEnsure = await ensureAnalyticsCliInstalled();
|
|
@@ -1367,6 +2061,8 @@ async function main() {
|
|
|
1367
2061
|
configCreated: configResult.created,
|
|
1368
2062
|
configPath,
|
|
1369
2063
|
heartbeat,
|
|
2064
|
+
openclawCron,
|
|
2065
|
+
hermesCron,
|
|
1370
2066
|
projectConfigured,
|
|
1371
2067
|
ascAppConfigured: ascAppConfiguredFromArg,
|
|
1372
2068
|
blockers: [
|
|
@@ -1408,6 +2104,8 @@ async function main() {
|
|
|
1408
2104
|
configCreated: configResult.created,
|
|
1409
2105
|
configPath,
|
|
1410
2106
|
heartbeat,
|
|
2107
|
+
openclawCron,
|
|
2108
|
+
hermesCron,
|
|
1411
2109
|
projectConfigured,
|
|
1412
2110
|
ascAppConfigured: ascAppConfiguredFromArg,
|
|
1413
2111
|
connectorSetup,
|
|
@@ -1422,7 +2120,9 @@ async function main() {
|
|
|
1422
2120
|
? 'Install the ASC CLI and provide ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY_PATH or ASC_PRIVATE_KEY. Resolve the app after auth succeeds.'
|
|
1423
2121
|
: entry.connector === 'sentry'
|
|
1424
2122
|
? 'Set SENTRY_AUTH_TOKEN plus SENTRY_ORG in the connector wizard. Defer project scope to app/repo context, or configure sources.sentry.accounts[].projects[] only when a fixed mapping is known.'
|
|
1425
|
-
:
|
|
2123
|
+
: entry.connector === 'coolify'
|
|
2124
|
+
? 'Set COOLIFY_BASE_URL and COOLIFY_API_TOKEN from Coolify Keys & Tokens / API tokens in the connector wizard.'
|
|
2125
|
+
: 'Set REVENUECAT_API_KEY and rerun connector setup to write RevenueCat MCP config.',
|
|
1426
2126
|
})),
|
|
1427
2127
|
}, null, 2)}\n`);
|
|
1428
2128
|
process.exitCode = 1;
|
|
@@ -1475,6 +2175,8 @@ async function main() {
|
|
|
1475
2175
|
configCreated: configResult.created,
|
|
1476
2176
|
configPath,
|
|
1477
2177
|
heartbeat,
|
|
2178
|
+
openclawCron,
|
|
2179
|
+
hermesCron,
|
|
1478
2180
|
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1479
2181
|
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1480
2182
|
ascAppConfigured: false,
|
|
@@ -1494,6 +2196,51 @@ async function main() {
|
|
|
1494
2196
|
process.exitCode = 1;
|
|
1495
2197
|
return;
|
|
1496
2198
|
}
|
|
2199
|
+
emitProgress(args.progressJson, {
|
|
2200
|
+
phase: 'start',
|
|
2201
|
+
key: 'ascAnalyticsRequest',
|
|
2202
|
+
label: 'ASC analytics reports',
|
|
2203
|
+
detail: 'checking ongoing Analytics Report Request',
|
|
2204
|
+
});
|
|
2205
|
+
const ascAnalyticsRequestSetup = await ensureAscAnalyticsRequestsForAppScope(ascAppSetup);
|
|
2206
|
+
emitProgress(args.progressJson, {
|
|
2207
|
+
phase: 'finish',
|
|
2208
|
+
key: 'ascAnalyticsRequest',
|
|
2209
|
+
label: 'ASC analytics reports',
|
|
2210
|
+
detail: ascAnalyticsRequestSetup.ok
|
|
2211
|
+
? ascAnalyticsRequestSetup.detail
|
|
2212
|
+
: `could not ensure Analytics Report Request (${truncate(ascAnalyticsRequestSetup.error, 240)})`,
|
|
2213
|
+
status: ascAnalyticsRequestSetup.ok ? 'pass' : 'fail',
|
|
2214
|
+
});
|
|
2215
|
+
if (!ascAnalyticsRequestSetup.ok) {
|
|
2216
|
+
process.stdout.write(`${JSON.stringify({
|
|
2217
|
+
ok: false,
|
|
2218
|
+
phase: 'asc_analytics_request_setup',
|
|
2219
|
+
configCreated: configResult.created,
|
|
2220
|
+
configPath,
|
|
2221
|
+
heartbeat,
|
|
2222
|
+
openclawCron,
|
|
2223
|
+
hermesCron,
|
|
2224
|
+
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
2225
|
+
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
2226
|
+
ascAppConfigured: ascAppSetup.configured,
|
|
2227
|
+
ascAppId: ascAppSetup.appId || null,
|
|
2228
|
+
ascAppScope: ascAppSetup.appScope || null,
|
|
2229
|
+
ascAnalyticsRequestResults: ascAnalyticsRequestSetup.results || [],
|
|
2230
|
+
connectorSetup,
|
|
2231
|
+
needsUserInput: false,
|
|
2232
|
+
question: null,
|
|
2233
|
+
blockers: [
|
|
2234
|
+
{
|
|
2235
|
+
check: 'connection:asc_analytics_request',
|
|
2236
|
+
detail: `Could not ensure App Store Connect Analytics Report Request: ${truncate(ascAnalyticsRequestSetup.error, 800)}`,
|
|
2237
|
+
remediation: 'Use an ASC API key with Admin for first setup so Growth Engineer can create the ongoing Analytics Report Request. After the request exists, rotate to Sales and Reports for steady-state downloads.',
|
|
2238
|
+
},
|
|
2239
|
+
],
|
|
2240
|
+
}, null, 2)}\n`);
|
|
2241
|
+
process.exitCode = 1;
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
1497
2244
|
const preflightResult = await runPreflight(configPath, args.testConnections, args.progressJson, args.onlyConnectors);
|
|
1498
2245
|
const preflightPayload = preflightResult.payload;
|
|
1499
2246
|
if (!preflightPayload) {
|
|
@@ -1514,11 +2261,16 @@ async function main() {
|
|
|
1514
2261
|
configCreated: configResult.created,
|
|
1515
2262
|
configPath,
|
|
1516
2263
|
heartbeat,
|
|
2264
|
+
openclawCron,
|
|
2265
|
+
hermesCron,
|
|
1517
2266
|
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1518
2267
|
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1519
2268
|
ascAppConfigured: ascAppSetup.configured,
|
|
1520
2269
|
ascAppId: ascAppSetup.appId || null,
|
|
1521
2270
|
ascAppScope: ascAppSetup.appScope || null,
|
|
2271
|
+
ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
|
|
2272
|
+
ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
|
|
2273
|
+
ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
|
|
1522
2274
|
githubRepo: configResult.githubRepo,
|
|
1523
2275
|
connectorSetup,
|
|
1524
2276
|
checks: preflightPayload.checks || [],
|
|
@@ -1534,12 +2286,18 @@ async function main() {
|
|
|
1534
2286
|
configCreated: configResult.created,
|
|
1535
2287
|
configPath,
|
|
1536
2288
|
heartbeat,
|
|
2289
|
+
openclawCron,
|
|
2290
|
+
hermesCron,
|
|
1537
2291
|
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1538
2292
|
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1539
2293
|
ascAppConfigured: ascAppSetup.configured,
|
|
1540
2294
|
ascAppId: ascAppSetup.appId || null,
|
|
1541
2295
|
ascAppScope: ascAppSetup.appScope || null,
|
|
2296
|
+
ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
|
|
2297
|
+
ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
|
|
2298
|
+
ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
|
|
1542
2299
|
connectorSetup,
|
|
2300
|
+
schedulerProofPath,
|
|
1543
2301
|
message: 'Preflight passed. First run skipped due to --setup-only.',
|
|
1544
2302
|
}, null, 2)}\n`);
|
|
1545
2303
|
return;
|
|
@@ -1562,6 +2320,8 @@ async function main() {
|
|
|
1562
2320
|
configCreated: configResult.created,
|
|
1563
2321
|
configPath,
|
|
1564
2322
|
heartbeat,
|
|
2323
|
+
openclawCron,
|
|
2324
|
+
hermesCron,
|
|
1565
2325
|
projectConfigured,
|
|
1566
2326
|
error: rawError,
|
|
1567
2327
|
}, null, 2)}\n`);
|
|
@@ -1575,9 +2335,12 @@ async function main() {
|
|
|
1575
2335
|
configCreated: configResult.created,
|
|
1576
2336
|
configPath,
|
|
1577
2337
|
heartbeat,
|
|
2338
|
+
openclawCron,
|
|
2339
|
+
hermesCron,
|
|
1578
2340
|
projectConfigured,
|
|
1579
2341
|
actionMode,
|
|
1580
2342
|
runnerOutput: runResult.stdout.trim(),
|
|
2343
|
+
schedulerProofPath,
|
|
1581
2344
|
}, null, 2)}\n`);
|
|
1582
2345
|
}
|
|
1583
2346
|
main().catch((error) => {
|