@analyticscli/growth-engineer 0.1.0-preview.8 → 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 +847 -150
- 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 +816 -41
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +100 -34
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +1997 -226
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +3 -1
- package/templates/config.example.json +128 -65
|
@@ -3,16 +3,68 @@ 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
|
-
import {
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { buildGrowthRunnerCommand, buildOpenClawCronAddCommand, deriveSchedulerProofPathFromStatePath, deriveStatePathFromConfigPath, getActionMode, getAutomationConfig, getDefaultSourceCommand, getOpenClawCronEditDeliveryCommandFromInspection, buildHermesCronCreateCommand, inspectHermesCronInstall, inspectOpenClawCronInstall, repairOpenClawCronDeliveryStore, } from './openclaw-growth-shared.mjs';
|
|
7
8
|
import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
8
9
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
9
10
|
const DEFAULT_TEMPLATE_PATH = 'data/openclaw-growth-engineer/config.example.json';
|
|
10
11
|
const DEFAULT_HEARTBEAT_PATH = 'HEARTBEAT.md';
|
|
12
|
+
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
11
13
|
const HEARTBEAT_MARKER_START = '<!-- openclaw-growth-engineer:start -->';
|
|
12
14
|
const HEARTBEAT_MARKER_END = '<!-- openclaw-growth-engineer:end -->';
|
|
13
15
|
const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
|
|
14
16
|
const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
|
|
15
17
|
(process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
|
|
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
|
+
};
|
|
16
68
|
function printHelpAndExit(exitCode, reason = null) {
|
|
17
69
|
if (reason) {
|
|
18
70
|
process.stderr.write(`${reason}\n\n`);
|
|
@@ -32,25 +84,45 @@ Options:
|
|
|
32
84
|
--config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
|
|
33
85
|
--project <id> Optional AnalyticsCLI project ID pin for generated source commands
|
|
34
86
|
--asc-app <id> Optional ASC app ID filter (defaults to all accessible apps)
|
|
35
|
-
--connectors <list> Install/enable connector helpers (
|
|
87
|
+
--connectors <list> Install/enable connector helpers (${SUPPORTED_CONNECTORS.join(',')},all)
|
|
36
88
|
--only-connectors <list>
|
|
37
|
-
Limit live preflight checks to
|
|
89
|
+
Limit live preflight checks to ${SUPPORTED_CONNECTORS.join(',')}
|
|
38
90
|
--setup-only Run bootstrap + preflight only (skip first run)
|
|
39
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)
|
|
40
97
|
--progress-json Emit machine-readable setup progress to stderr
|
|
41
98
|
--help, -h Show help
|
|
42
99
|
`);
|
|
43
100
|
process.exit(exitCode);
|
|
44
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
|
+
}
|
|
45
114
|
function parseArgs(argv) {
|
|
46
115
|
const args = {
|
|
47
|
-
config:
|
|
116
|
+
config: resolveDefaultConfigPath(),
|
|
48
117
|
project: '',
|
|
49
118
|
ascApp: '',
|
|
50
119
|
run: true,
|
|
51
120
|
testConnections: true,
|
|
52
121
|
connectors: [],
|
|
53
122
|
onlyConnectors: [],
|
|
123
|
+
openclawCron: String(process.env.OPENCLAW_GROWTH_OPENCLAW_CRON || 'auto').trim().toLowerCase(),
|
|
124
|
+
openclawCronSchedule: '',
|
|
125
|
+
openclawCronTimezone: '',
|
|
54
126
|
progressJson: false,
|
|
55
127
|
};
|
|
56
128
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -85,6 +157,24 @@ function parseArgs(argv) {
|
|
|
85
157
|
else if (token === '--no-test-connections') {
|
|
86
158
|
args.testConnections = false;
|
|
87
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
|
+
}
|
|
88
178
|
else if (token === '--progress-json') {
|
|
89
179
|
args.progressJson = true;
|
|
90
180
|
}
|
|
@@ -95,8 +185,15 @@ function parseArgs(argv) {
|
|
|
95
185
|
printHelpAndExit(1, `Unknown argument: ${token}`);
|
|
96
186
|
}
|
|
97
187
|
}
|
|
188
|
+
args.openclawCron = validateOpenClawCronMode(args.openclawCron);
|
|
98
189
|
return args;
|
|
99
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
|
+
}
|
|
100
197
|
function normalizeConnectorKey(value) {
|
|
101
198
|
const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
102
199
|
if (!normalized)
|
|
@@ -111,8 +208,58 @@ function normalizeConnectorKey(value) {
|
|
|
111
208
|
return 'asc';
|
|
112
209
|
if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
|
|
113
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';
|
|
114
215
|
if (['sentry', 'sentry-api', 'sentry-mcp', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
|
|
115
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';
|
|
116
263
|
return null;
|
|
117
264
|
}
|
|
118
265
|
function parseConnectorList(value) {
|
|
@@ -122,14 +269,10 @@ function parseConnectorList(value) {
|
|
|
122
269
|
for (const entry of String(value).split(',')) {
|
|
123
270
|
const connector = normalizeConnectorKey(entry);
|
|
124
271
|
if (!connector) {
|
|
125
|
-
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use
|
|
272
|
+
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
|
|
126
273
|
}
|
|
127
274
|
if (connector === 'all') {
|
|
128
|
-
connectors.add(
|
|
129
|
-
connectors.add('github');
|
|
130
|
-
connectors.add('asc');
|
|
131
|
-
connectors.add('revenuecat');
|
|
132
|
-
connectors.add('sentry');
|
|
275
|
+
SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
|
|
133
276
|
}
|
|
134
277
|
else {
|
|
135
278
|
connectors.add(connector);
|
|
@@ -143,6 +286,83 @@ function quote(value) {
|
|
|
143
286
|
}
|
|
144
287
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
145
288
|
}
|
|
289
|
+
function resolveRuntimeScriptPath(scriptName) {
|
|
290
|
+
const candidates = [
|
|
291
|
+
path.join(RUNTIME_DIR, scriptName),
|
|
292
|
+
path.resolve('scripts', scriptName),
|
|
293
|
+
path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
|
|
294
|
+
];
|
|
295
|
+
return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
|
|
296
|
+
}
|
|
297
|
+
function nodeRuntimeScriptCommand(scriptName) {
|
|
298
|
+
return `node ${quote(resolveRuntimeScriptPath(scriptName))}`;
|
|
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
|
+
}
|
|
146
366
|
function truncate(value, max = 240) {
|
|
147
367
|
const text = String(value || '');
|
|
148
368
|
if (text.length <= max)
|
|
@@ -171,10 +391,19 @@ function emitProgress(enabled, event) {
|
|
|
171
391
|
return;
|
|
172
392
|
process.stderr.write(`OPENCLAW_PROGRESS ${JSON.stringify(event)}\n`);
|
|
173
393
|
}
|
|
394
|
+
function hardenUnattendedShellCommand(command) {
|
|
395
|
+
return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
|
|
396
|
+
}
|
|
174
397
|
function runShellCommand(command, timeoutMs = 120_000, options = {}) {
|
|
175
398
|
return new Promise((resolve) => {
|
|
176
|
-
const child = spawn(resolveShellCommand(), ['-c', command], {
|
|
399
|
+
const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
|
|
177
400
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
401
|
+
env: {
|
|
402
|
+
...process.env,
|
|
403
|
+
DEBIAN_FRONTEND: 'noninteractive',
|
|
404
|
+
SUDO_ASKPASS: '/bin/false',
|
|
405
|
+
SUDO_PROMPT: '',
|
|
406
|
+
},
|
|
178
407
|
});
|
|
179
408
|
let stdout = '';
|
|
180
409
|
let stderr = '';
|
|
@@ -455,12 +684,15 @@ function isEffectivelyEmptyHeartbeat(value) {
|
|
|
455
684
|
function renderHeartbeatBlock(configPath, config) {
|
|
456
685
|
const interval = formatHeartbeatInterval(getHeartbeatInterval(config));
|
|
457
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';
|
|
458
690
|
return `${HEARTBEAT_MARKER_START}
|
|
459
691
|
tasks:
|
|
460
692
|
|
|
461
693
|
- name: openclaw-growth-engineer-run
|
|
462
694
|
interval: ${interval}
|
|
463
|
-
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."
|
|
464
696
|
|
|
465
697
|
# Keep this section small. Do not put secrets in HEARTBEAT.md.
|
|
466
698
|
${HEARTBEAT_MARKER_END}`;
|
|
@@ -498,6 +730,219 @@ async function ensureGrowthHeartbeat(configPath, config) {
|
|
|
498
730
|
updated: false,
|
|
499
731
|
};
|
|
500
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
|
+
}
|
|
501
946
|
async function appendHelperDetail(details, label, result) {
|
|
502
947
|
if (result.ok) {
|
|
503
948
|
details.push(`${label}: ok`);
|
|
@@ -756,6 +1201,13 @@ async function installSentryConnector() {
|
|
|
756
1201
|
details.push('Sentry direct API exporter enabled via node scripts/export-sentry-summary.mjs');
|
|
757
1202
|
return { connector: 'sentry', ok: true, detail: details.join('; ') };
|
|
758
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
|
+
}
|
|
759
1211
|
async function installGitHubConnector() {
|
|
760
1212
|
const details = [];
|
|
761
1213
|
await installClawHubSkill('github', details);
|
|
@@ -796,6 +1248,20 @@ async function installAnalyticsConnector() {
|
|
|
796
1248
|
: 'analyticscli binary missing after dependency setup',
|
|
797
1249
|
};
|
|
798
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
|
+
}
|
|
799
1265
|
async function enableConnectorConfig(configPath, connectors) {
|
|
800
1266
|
if (connectors.length === 0 || !(await fileExists(configPath)))
|
|
801
1267
|
return;
|
|
@@ -806,17 +1272,35 @@ async function enableConnectorConfig(configPath, connectors) {
|
|
|
806
1272
|
sources: {
|
|
807
1273
|
...(config.sources || {}),
|
|
808
1274
|
analytics: connectors.includes('analytics')
|
|
809
|
-
? { ...(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) }
|
|
810
1276
|
: config.sources?.analytics,
|
|
811
1277
|
revenuecat: connectors.includes('revenuecat')
|
|
812
|
-
? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command:
|
|
1278
|
+
? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('revenuecat', config.sources?.revenuecat) }
|
|
813
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,
|
|
814
1286
|
sentry: connectors.includes('sentry')
|
|
815
|
-
? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command:
|
|
1287
|
+
? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command: normalizeSourceCommand('sentry', config.sources?.sentry) }
|
|
816
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,
|
|
817
1299
|
extra: extra.map((source) => connectors.includes('asc') && source?.service === 'asc-cli'
|
|
818
|
-
? { ...source, enabled: true, mode: 'command', command:
|
|
819
|
-
: 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),
|
|
820
1304
|
},
|
|
821
1305
|
};
|
|
822
1306
|
await writeJson(configPath, next);
|
|
@@ -833,8 +1317,16 @@ async function installConnectorHelpers(configPath, connectors) {
|
|
|
833
1317
|
results.push(await installAscConnector());
|
|
834
1318
|
if (connector === 'revenuecat')
|
|
835
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' });
|
|
836
1324
|
if (connector === 'sentry')
|
|
837
1325
|
results.push(await installSentryConnector());
|
|
1326
|
+
if (connector === 'coolify')
|
|
1327
|
+
results.push(await installCoolifyConnector());
|
|
1328
|
+
if (isAccountSignalConnector(connector))
|
|
1329
|
+
results.push(await installAccountSignalConnector(connector));
|
|
838
1330
|
}
|
|
839
1331
|
return results;
|
|
840
1332
|
}
|
|
@@ -865,7 +1357,9 @@ async function detectGitHubRepo() {
|
|
|
865
1357
|
}
|
|
866
1358
|
async function ensureConfig(configPath) {
|
|
867
1359
|
if (await fileExists(configPath)) {
|
|
868
|
-
const
|
|
1360
|
+
const originalConfig = await readJson(configPath);
|
|
1361
|
+
const config = migrateRuntimeSourceCommands(originalConfig);
|
|
1362
|
+
let changed = JSON.stringify(originalConfig.sources || {}) !== JSON.stringify(config.sources || {});
|
|
869
1363
|
if (!isConfiguredGitHubRepo(config?.project?.githubRepo)) {
|
|
870
1364
|
const detectedRepo = await detectGitHubRepo();
|
|
871
1365
|
if (detectedRepo) {
|
|
@@ -873,14 +1367,17 @@ async function ensureConfig(configPath) {
|
|
|
873
1367
|
...(config.project || {}),
|
|
874
1368
|
githubRepo: detectedRepo,
|
|
875
1369
|
};
|
|
876
|
-
|
|
877
|
-
return {
|
|
878
|
-
created: false,
|
|
879
|
-
configPath,
|
|
880
|
-
githubRepo: detectedRepo,
|
|
881
|
-
};
|
|
1370
|
+
changed = true;
|
|
882
1371
|
}
|
|
883
1372
|
}
|
|
1373
|
+
if (changed) {
|
|
1374
|
+
await writeJson(configPath, config);
|
|
1375
|
+
return {
|
|
1376
|
+
created: false,
|
|
1377
|
+
configPath,
|
|
1378
|
+
githubRepo: config.project?.githubRepo || null,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
884
1381
|
return {
|
|
885
1382
|
created: false,
|
|
886
1383
|
configPath,
|
|
@@ -904,19 +1401,27 @@ async function ensureConfig(configPath) {
|
|
|
904
1401
|
analytics: {
|
|
905
1402
|
enabled: true,
|
|
906
1403
|
mode: 'command',
|
|
907
|
-
command:
|
|
1404
|
+
command: getRuntimeSourceCommand('analytics'),
|
|
908
1405
|
},
|
|
909
1406
|
revenuecat: {
|
|
910
1407
|
...(template.sources?.revenuecat || {}),
|
|
911
1408
|
enabled: false,
|
|
912
1409
|
mode: 'command',
|
|
913
|
-
command:
|
|
1410
|
+
command: getRuntimeSourceCommand('revenuecat'),
|
|
914
1411
|
},
|
|
915
1412
|
sentry: {
|
|
916
1413
|
...(template.sources?.sentry || {}),
|
|
917
1414
|
enabled: false,
|
|
918
1415
|
mode: 'command',
|
|
919
|
-
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',
|
|
920
1425
|
},
|
|
921
1426
|
feedback: {
|
|
922
1427
|
...(template.sources?.feedback || {}),
|
|
@@ -927,7 +1432,7 @@ async function ensureConfig(configPath) {
|
|
|
927
1432
|
actions: {
|
|
928
1433
|
...template.actions,
|
|
929
1434
|
mode: 'issue',
|
|
930
|
-
autoCreateIssues:
|
|
1435
|
+
autoCreateIssues: true,
|
|
931
1436
|
autoCreatePullRequests: false,
|
|
932
1437
|
draftPullRequests: true,
|
|
933
1438
|
proposalBranchPrefix: 'openclaw/proposals',
|
|
@@ -1009,6 +1514,15 @@ function appendProjectFlag(command, projectId) {
|
|
|
1009
1514
|
function commandHasAscAppFlag(command) {
|
|
1010
1515
|
return /(^|\s)--app(\s|=|$)/.test(String(command || ''));
|
|
1011
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
|
+
}
|
|
1012
1526
|
function appendAscAppFlag(command, appId) {
|
|
1013
1527
|
const raw = String(command || '').trim();
|
|
1014
1528
|
if (!raw || commandHasAscAppFlag(raw))
|
|
@@ -1078,6 +1592,34 @@ async function configureAscApp(configPath, appId) {
|
|
|
1078
1592
|
process.env.ASC_APP_ID = normalizedAppId;
|
|
1079
1593
|
return changed;
|
|
1080
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
|
+
}
|
|
1081
1623
|
function configHasEnabledAscSource(config) {
|
|
1082
1624
|
const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
|
|
1083
1625
|
return extraSources.some((source) => {
|
|
@@ -1153,11 +1695,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
|
1153
1695
|
if (!configHasEnabledAscSource(config)) {
|
|
1154
1696
|
return { ok: true, configured: false, changed: false, appId: null, appScope: 'disabled', needsUserInput: false };
|
|
1155
1697
|
}
|
|
1156
|
-
const
|
|
1157
|
-
if (configuredAppId) {
|
|
1158
|
-
const changed = await configureAscApp(configPath, configuredAppId);
|
|
1159
|
-
return { ok: true, configured: true, changed, appId: configuredAppId, appScope: 'single_app', needsUserInput: false };
|
|
1160
|
-
}
|
|
1698
|
+
const changed = await configureAscAllApps(configPath);
|
|
1161
1699
|
const appList = await listAscApps();
|
|
1162
1700
|
if (!appList.ok) {
|
|
1163
1701
|
return {
|
|
@@ -1172,7 +1710,7 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
|
1172
1710
|
return {
|
|
1173
1711
|
ok: true,
|
|
1174
1712
|
configured: true,
|
|
1175
|
-
changed
|
|
1713
|
+
changed,
|
|
1176
1714
|
appId: null,
|
|
1177
1715
|
appScope: 'all_accessible_apps',
|
|
1178
1716
|
apps: appList.apps,
|
|
@@ -1180,6 +1718,131 @@ async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
|
1180
1718
|
needsUserInput: false,
|
|
1181
1719
|
};
|
|
1182
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
|
+
}
|
|
1183
1846
|
async function listAnalyticsProjects() {
|
|
1184
1847
|
const result = await runShellCommand('analyticscli projects list --format json', 60_000);
|
|
1185
1848
|
if (!result.ok) {
|
|
@@ -1273,7 +1936,7 @@ function remediationForCheck(checkName, configPath) {
|
|
|
1273
1936
|
return 'Write `data/openclaw-growth-engineer/analytics_summary.json` via your analytics refresh step (API-key based source command/file generation).';
|
|
1274
1937
|
}
|
|
1275
1938
|
if (checkName === 'connection:analytics') {
|
|
1276
|
-
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.';
|
|
1277
1940
|
}
|
|
1278
1941
|
if (checkName === 'connection:github') {
|
|
1279
1942
|
return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>` + issues API.';
|
|
@@ -1282,7 +1945,7 @@ function remediationForCheck(checkName, configPath) {
|
|
|
1282
1945
|
return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>/pulls`, plus `Pull requests: Read/Write` and `Contents: Read/Write` scopes.';
|
|
1283
1946
|
}
|
|
1284
1947
|
if (checkName === 'connection:asc_cli') {
|
|
1285
|
-
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.';
|
|
1286
1949
|
}
|
|
1287
1950
|
return 'Fix this blocker and rerun start.';
|
|
1288
1951
|
}
|
|
@@ -1303,8 +1966,7 @@ function remediateAscAppSetupFailure(error) {
|
|
|
1303
1966
|
}
|
|
1304
1967
|
async function runPreflight(configPath, testConnections, progressJson = false, onlyConnectors = []) {
|
|
1305
1968
|
const commandParts = [
|
|
1306
|
-
'
|
|
1307
|
-
'scripts/openclaw-growth-preflight.mjs',
|
|
1969
|
+
nodeRuntimeScriptCommand('openclaw-growth-preflight.mjs'),
|
|
1308
1970
|
'--config',
|
|
1309
1971
|
quote(configPath),
|
|
1310
1972
|
];
|
|
@@ -1334,17 +1996,61 @@ async function runPreflight(configPath, testConnections, progressJson = false, o
|
|
|
1334
1996
|
};
|
|
1335
1997
|
}
|
|
1336
1998
|
async function runFirstPass(configPath) {
|
|
1337
|
-
const
|
|
1999
|
+
const statePath = path.resolve(deriveStatePathFromConfigPath(configPath));
|
|
2000
|
+
const command = `${nodeRuntimeScriptCommand('openclaw-growth-runner.mjs')} --config ${quote(configPath)} --state ${quote(statePath)}`;
|
|
1338
2001
|
return runShellCommand(command, 300_000);
|
|
1339
2002
|
}
|
|
1340
2003
|
async function main() {
|
|
1341
2004
|
await loadOpenClawGrowthSecrets();
|
|
1342
2005
|
const args = parseArgs(process.argv.slice(2));
|
|
1343
2006
|
const configPath = path.resolve(args.config);
|
|
2007
|
+
const schedulerProofPath = path.resolve(deriveSchedulerProofPathFromStatePath(deriveStatePathFromConfigPath(configPath)));
|
|
1344
2008
|
const configResult = await ensureConfig(configPath);
|
|
1345
|
-
const initialConfig = await readJson(configPath);
|
|
2009
|
+
const initialConfig = applyOpenClawCronOverrides(await readJson(configPath), args);
|
|
1346
2010
|
await applyOpenClawSecretRefs(initialConfig);
|
|
1347
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
|
+
}
|
|
1348
2054
|
const projectConfigured = await configureAnalyticsProject(configPath, args.project);
|
|
1349
2055
|
const ascAppConfiguredFromArg = await configureAscApp(configPath, args.ascApp);
|
|
1350
2056
|
const analyticscliEnsure = await ensureAnalyticsCliInstalled();
|
|
@@ -1355,6 +2061,8 @@ async function main() {
|
|
|
1355
2061
|
configCreated: configResult.created,
|
|
1356
2062
|
configPath,
|
|
1357
2063
|
heartbeat,
|
|
2064
|
+
openclawCron,
|
|
2065
|
+
hermesCron,
|
|
1358
2066
|
projectConfigured,
|
|
1359
2067
|
ascAppConfigured: ascAppConfiguredFromArg,
|
|
1360
2068
|
blockers: [
|
|
@@ -1396,6 +2104,8 @@ async function main() {
|
|
|
1396
2104
|
configCreated: configResult.created,
|
|
1397
2105
|
configPath,
|
|
1398
2106
|
heartbeat,
|
|
2107
|
+
openclawCron,
|
|
2108
|
+
hermesCron,
|
|
1399
2109
|
projectConfigured,
|
|
1400
2110
|
ascAppConfigured: ascAppConfiguredFromArg,
|
|
1401
2111
|
connectorSetup,
|
|
@@ -1410,7 +2120,9 @@ async function main() {
|
|
|
1410
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.'
|
|
1411
2121
|
: entry.connector === 'sentry'
|
|
1412
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.'
|
|
1413
|
-
:
|
|
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.',
|
|
1414
2126
|
})),
|
|
1415
2127
|
}, null, 2)}\n`);
|
|
1416
2128
|
process.exitCode = 1;
|
|
@@ -1463,6 +2175,8 @@ async function main() {
|
|
|
1463
2175
|
configCreated: configResult.created,
|
|
1464
2176
|
configPath,
|
|
1465
2177
|
heartbeat,
|
|
2178
|
+
openclawCron,
|
|
2179
|
+
hermesCron,
|
|
1466
2180
|
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1467
2181
|
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1468
2182
|
ascAppConfigured: false,
|
|
@@ -1482,6 +2196,51 @@ async function main() {
|
|
|
1482
2196
|
process.exitCode = 1;
|
|
1483
2197
|
return;
|
|
1484
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
|
+
}
|
|
1485
2244
|
const preflightResult = await runPreflight(configPath, args.testConnections, args.progressJson, args.onlyConnectors);
|
|
1486
2245
|
const preflightPayload = preflightResult.payload;
|
|
1487
2246
|
if (!preflightPayload) {
|
|
@@ -1502,11 +2261,16 @@ async function main() {
|
|
|
1502
2261
|
configCreated: configResult.created,
|
|
1503
2262
|
configPath,
|
|
1504
2263
|
heartbeat,
|
|
2264
|
+
openclawCron,
|
|
2265
|
+
hermesCron,
|
|
1505
2266
|
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1506
2267
|
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1507
2268
|
ascAppConfigured: ascAppSetup.configured,
|
|
1508
2269
|
ascAppId: ascAppSetup.appId || null,
|
|
1509
2270
|
ascAppScope: ascAppSetup.appScope || null,
|
|
2271
|
+
ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
|
|
2272
|
+
ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
|
|
2273
|
+
ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
|
|
1510
2274
|
githubRepo: configResult.githubRepo,
|
|
1511
2275
|
connectorSetup,
|
|
1512
2276
|
checks: preflightPayload.checks || [],
|
|
@@ -1522,12 +2286,18 @@ async function main() {
|
|
|
1522
2286
|
configCreated: configResult.created,
|
|
1523
2287
|
configPath,
|
|
1524
2288
|
heartbeat,
|
|
2289
|
+
openclawCron,
|
|
2290
|
+
hermesCron,
|
|
1525
2291
|
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1526
2292
|
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1527
2293
|
ascAppConfigured: ascAppSetup.configured,
|
|
1528
2294
|
ascAppId: ascAppSetup.appId || null,
|
|
1529
2295
|
ascAppScope: ascAppSetup.appScope || null,
|
|
2296
|
+
ascAnalyticsRequestStatus: ascAnalyticsRequestSetup.status,
|
|
2297
|
+
ascAnalyticsRequestId: ascAnalyticsRequestSetup.requestId || null,
|
|
2298
|
+
ascAnalyticsRequestIds: ascAnalyticsRequestSetup.requestIds || [],
|
|
1530
2299
|
connectorSetup,
|
|
2300
|
+
schedulerProofPath,
|
|
1531
2301
|
message: 'Preflight passed. First run skipped due to --setup-only.',
|
|
1532
2302
|
}, null, 2)}\n`);
|
|
1533
2303
|
return;
|
|
@@ -1550,6 +2320,8 @@ async function main() {
|
|
|
1550
2320
|
configCreated: configResult.created,
|
|
1551
2321
|
configPath,
|
|
1552
2322
|
heartbeat,
|
|
2323
|
+
openclawCron,
|
|
2324
|
+
hermesCron,
|
|
1553
2325
|
projectConfigured,
|
|
1554
2326
|
error: rawError,
|
|
1555
2327
|
}, null, 2)}\n`);
|
|
@@ -1563,9 +2335,12 @@ async function main() {
|
|
|
1563
2335
|
configCreated: configResult.created,
|
|
1564
2336
|
configPath,
|
|
1565
2337
|
heartbeat,
|
|
2338
|
+
openclawCron,
|
|
2339
|
+
hermesCron,
|
|
1566
2340
|
projectConfigured,
|
|
1567
2341
|
actionMode,
|
|
1568
2342
|
runnerOutput: runResult.stdout.trim(),
|
|
2343
|
+
schedulerProofPath,
|
|
1569
2344
|
}, null, 2)}\n`);
|
|
1570
2345
|
}
|
|
1571
2346
|
main().catch((error) => {
|