@analyticscli/growth-engineer 0.1.0-preview.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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/config.d.ts +1663 -0
- package/dist/config.js +266 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1188 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/export-analytics-summary.d.mts +2 -0
- package/dist/runtime/export-analytics-summary.mjs +303 -0
- package/dist/runtime/export-analytics-summary.mjs.map +1 -0
- package/dist/runtime/export-asc-summary.d.mts +2 -0
- package/dist/runtime/export-asc-summary.mjs +376 -0
- package/dist/runtime/export-asc-summary.mjs.map +1 -0
- package/dist/runtime/export-revenuecat-summary.d.mts +2 -0
- package/dist/runtime/export-revenuecat-summary.mjs +176 -0
- package/dist/runtime/export-revenuecat-summary.mjs.map +1 -0
- package/dist/runtime/export-sentry-summary.d.mts +2 -0
- package/dist/runtime/export-sentry-summary.mjs +352 -0
- package/dist/runtime/export-sentry-summary.mjs.map +1 -0
- package/dist/runtime/openclaw-exporters-lib.d.mts +101 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +1276 -0
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -0
- package/dist/runtime/openclaw-feedback-api.d.mts +2 -0
- package/dist/runtime/openclaw-feedback-api.mjs +255 -0
- package/dist/runtime/openclaw-feedback-api.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-charts.py +154 -0
- package/dist/runtime/openclaw-growth-engineer.d.mts +2 -0
- package/dist/runtime/openclaw-growth-engineer.mjs +1258 -0
- package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-env.d.mts +9 -0
- package/dist/runtime/openclaw-growth-env.mjs +125 -0
- package/dist/runtime/openclaw-growth-env.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-preflight.d.mts +2 -0
- package/dist/runtime/openclaw-growth-preflight.mjs +1111 -0
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-runner.d.mts +2 -0
- package/dist/runtime/openclaw-growth-runner.mjs +1302 -0
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-shared.d.mts +33 -0
- package/dist/runtime/openclaw-growth-shared.mjs +208 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-start.d.mts +2 -0
- package/dist/runtime/openclaw-growth-start.mjs +1575 -0
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-status.d.mts +2 -0
- package/dist/runtime/openclaw-growth-status.mjs +387 -0
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -0
- package/dist/runtime/openclaw-growth-wizard.d.mts +2 -0
- package/dist/runtime/openclaw-growth-wizard.mjs +3519 -0
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -0
- package/dist/shell.d.ts +17 -0
- package/dist/shell.js +40 -0
- package/dist/shell.js.map +1 -0
- package/package.json +38 -0
- package/templates/analytics_summary.example.json +40 -0
- package/templates/config.example.json +197 -0
- package/templates/feedback_summary.example.json +37 -0
- package/templates/revenuecat_summary.example.json +25 -0
- package/templates/sentry_summary.example.json +23 -0
|
@@ -0,0 +1,1575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { getActionMode, getDefaultSourceCommand } from './openclaw-growth-shared.mjs';
|
|
7
|
+
import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
8
|
+
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
9
|
+
const DEFAULT_TEMPLATE_PATH = 'data/openclaw-growth-engineer/config.example.json';
|
|
10
|
+
const DEFAULT_HEARTBEAT_PATH = 'HEARTBEAT.md';
|
|
11
|
+
const HEARTBEAT_MARKER_START = '<!-- openclaw-growth-engineer:start -->';
|
|
12
|
+
const HEARTBEAT_MARKER_END = '<!-- openclaw-growth-engineer:end -->';
|
|
13
|
+
const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
|
|
14
|
+
const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
|
|
15
|
+
(process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
|
|
16
|
+
function printHelpAndExit(exitCode, reason = null) {
|
|
17
|
+
if (reason) {
|
|
18
|
+
process.stderr.write(`${reason}\n\n`);
|
|
19
|
+
}
|
|
20
|
+
process.stdout.write(`
|
|
21
|
+
OpenClaw Growth Start
|
|
22
|
+
|
|
23
|
+
Bootstraps setup and first run in one deterministic flow:
|
|
24
|
+
1) Ensure config exists (auto-bootstrap from template when missing)
|
|
25
|
+
2) Run preflight
|
|
26
|
+
3) If preflight passes, run first pass
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
node scripts/openclaw-growth-start.mjs [options]
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
|
|
33
|
+
--project <id> Optional AnalyticsCLI project ID pin for generated source commands
|
|
34
|
+
--asc-app <id> Optional ASC app ID filter (defaults to all accessible apps)
|
|
35
|
+
--connectors <list> Install/enable connector helpers (analytics,github,asc,revenuecat,sentry,all)
|
|
36
|
+
--only-connectors <list>
|
|
37
|
+
Limit live preflight checks to analytics,github,asc,revenuecat,sentry
|
|
38
|
+
--setup-only Run bootstrap + preflight only (skip first run)
|
|
39
|
+
--no-test-connections Skip live API smoke checks in preflight
|
|
40
|
+
--progress-json Emit machine-readable setup progress to stderr
|
|
41
|
+
--help, -h Show help
|
|
42
|
+
`);
|
|
43
|
+
process.exit(exitCode);
|
|
44
|
+
}
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const args = {
|
|
47
|
+
config: DEFAULT_CONFIG_PATH,
|
|
48
|
+
project: '',
|
|
49
|
+
ascApp: '',
|
|
50
|
+
run: true,
|
|
51
|
+
testConnections: true,
|
|
52
|
+
connectors: [],
|
|
53
|
+
onlyConnectors: [],
|
|
54
|
+
progressJson: false,
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
57
|
+
const token = argv[i];
|
|
58
|
+
const next = argv[i + 1];
|
|
59
|
+
if (token === '--') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
else if (token === '--config') {
|
|
63
|
+
args.config = next || args.config;
|
|
64
|
+
i += 1;
|
|
65
|
+
}
|
|
66
|
+
else if (token === '--project') {
|
|
67
|
+
args.project = String(next || '').trim();
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
else if (token === '--asc-app') {
|
|
71
|
+
args.ascApp = String(next || '').trim();
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
else if (token === '--connectors') {
|
|
75
|
+
args.connectors = parseConnectorList(next || '');
|
|
76
|
+
i += 1;
|
|
77
|
+
}
|
|
78
|
+
else if (token === '--only-connectors') {
|
|
79
|
+
args.onlyConnectors = parseConnectorList(next || '');
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
else if (token === '--setup-only') {
|
|
83
|
+
args.run = false;
|
|
84
|
+
}
|
|
85
|
+
else if (token === '--no-test-connections') {
|
|
86
|
+
args.testConnections = false;
|
|
87
|
+
}
|
|
88
|
+
else if (token === '--progress-json') {
|
|
89
|
+
args.progressJson = true;
|
|
90
|
+
}
|
|
91
|
+
else if (token === '--help' || token === '-h') {
|
|
92
|
+
printHelpAndExit(0);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
printHelpAndExit(1, `Unknown argument: ${token}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
100
|
+
function normalizeConnectorKey(value) {
|
|
101
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
102
|
+
if (!normalized)
|
|
103
|
+
return null;
|
|
104
|
+
if (normalized === 'all')
|
|
105
|
+
return 'all';
|
|
106
|
+
if (['analytics', 'analyticscli', 'product-analytics', 'events'].includes(normalized))
|
|
107
|
+
return 'analytics';
|
|
108
|
+
if (['github', 'gh', 'github-code', 'codebase', 'code-access'].includes(normalized))
|
|
109
|
+
return 'github';
|
|
110
|
+
if (['asc', 'asc-cli', 'app-store-connect', 'appstoreconnect', 'app-store'].includes(normalized))
|
|
111
|
+
return 'asc';
|
|
112
|
+
if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
|
|
113
|
+
return 'revenuecat';
|
|
114
|
+
if (['sentry', 'sentry-api', 'sentry-mcp', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
|
|
115
|
+
return 'sentry';
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function parseConnectorList(value) {
|
|
119
|
+
if (!String(value || '').trim())
|
|
120
|
+
return [];
|
|
121
|
+
const connectors = new Set();
|
|
122
|
+
for (const entry of String(value).split(',')) {
|
|
123
|
+
const connector = normalizeConnectorKey(entry);
|
|
124
|
+
if (!connector) {
|
|
125
|
+
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use analytics, github, asc, revenuecat, sentry, or all.`);
|
|
126
|
+
}
|
|
127
|
+
if (connector === 'all') {
|
|
128
|
+
connectors.add('analytics');
|
|
129
|
+
connectors.add('github');
|
|
130
|
+
connectors.add('asc');
|
|
131
|
+
connectors.add('revenuecat');
|
|
132
|
+
connectors.add('sentry');
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
connectors.add(connector);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [...connectors];
|
|
139
|
+
}
|
|
140
|
+
function quote(value) {
|
|
141
|
+
if (/^[a-zA-Z0-9_./:-]+$/.test(String(value))) {
|
|
142
|
+
return String(value);
|
|
143
|
+
}
|
|
144
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
145
|
+
}
|
|
146
|
+
function truncate(value, max = 240) {
|
|
147
|
+
const text = String(value || '');
|
|
148
|
+
if (text.length <= max)
|
|
149
|
+
return text;
|
|
150
|
+
return `${text.slice(0, max)}...`;
|
|
151
|
+
}
|
|
152
|
+
function resolveShellCommand() {
|
|
153
|
+
const candidates = [
|
|
154
|
+
process.env.OPENCLAW_SHELL,
|
|
155
|
+
process.env.SHELL,
|
|
156
|
+
'/bin/zsh',
|
|
157
|
+
'/bin/bash',
|
|
158
|
+
'/usr/bin/bash',
|
|
159
|
+
'/bin/sh',
|
|
160
|
+
'/usr/bin/sh',
|
|
161
|
+
].filter((value) => typeof value === 'string' && value.trim().length > 0);
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
if (existsSync(candidate)) {
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return 'sh';
|
|
168
|
+
}
|
|
169
|
+
function emitProgress(enabled, event) {
|
|
170
|
+
if (!enabled)
|
|
171
|
+
return;
|
|
172
|
+
process.stderr.write(`OPENCLAW_PROGRESS ${JSON.stringify(event)}\n`);
|
|
173
|
+
}
|
|
174
|
+
function runShellCommand(command, timeoutMs = 120_000, options = {}) {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
const child = spawn(resolveShellCommand(), ['-c', command], {
|
|
177
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
178
|
+
});
|
|
179
|
+
let stdout = '';
|
|
180
|
+
let stderr = '';
|
|
181
|
+
let stderrBuffer = '';
|
|
182
|
+
let settled = false;
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
if (settled)
|
|
185
|
+
return;
|
|
186
|
+
settled = true;
|
|
187
|
+
child.kill('SIGTERM');
|
|
188
|
+
resolve({
|
|
189
|
+
ok: false,
|
|
190
|
+
code: null,
|
|
191
|
+
stdout,
|
|
192
|
+
stderr: `${stderr}\nTimed out after ${timeoutMs}ms`,
|
|
193
|
+
});
|
|
194
|
+
}, timeoutMs);
|
|
195
|
+
child.stdout.on('data', (chunk) => {
|
|
196
|
+
stdout += String(chunk);
|
|
197
|
+
});
|
|
198
|
+
child.stderr.on('data', (chunk) => {
|
|
199
|
+
const text = String(chunk);
|
|
200
|
+
stderr += text;
|
|
201
|
+
if (!options.onStderrLine)
|
|
202
|
+
return;
|
|
203
|
+
stderrBuffer += text;
|
|
204
|
+
const lines = stderrBuffer.split(/\r?\n/);
|
|
205
|
+
stderrBuffer = lines.pop() || '';
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
options.onStderrLine(line);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
child.on('close', (code) => {
|
|
211
|
+
if (settled)
|
|
212
|
+
return;
|
|
213
|
+
settled = true;
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
if (options.onStderrLine && stderrBuffer.trim()) {
|
|
216
|
+
options.onStderrLine(stderrBuffer);
|
|
217
|
+
}
|
|
218
|
+
resolve({
|
|
219
|
+
ok: code === 0,
|
|
220
|
+
code,
|
|
221
|
+
stdout,
|
|
222
|
+
stderr,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function fileExists(filePath) {
|
|
228
|
+
try {
|
|
229
|
+
await fs.access(filePath);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function commandExists(commandName) {
|
|
237
|
+
const result = await runShellCommand(`command -v ${quote(commandName)} >/dev/null 2>&1`, 30_000);
|
|
238
|
+
return result.ok;
|
|
239
|
+
}
|
|
240
|
+
async function resolveCommandPath(commandName) {
|
|
241
|
+
const result = await runShellCommand(`command -v ${quote(commandName)}`, 30_000);
|
|
242
|
+
return result.ok ? result.stdout.trim() : null;
|
|
243
|
+
}
|
|
244
|
+
function prependToPath(binDir) {
|
|
245
|
+
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH || ''}`;
|
|
246
|
+
}
|
|
247
|
+
function getPathProfileEntries(binDir) {
|
|
248
|
+
const entries = [binDir];
|
|
249
|
+
if (process.env.HOME && path.resolve(binDir) === path.resolve(process.env.HOME, '.local', 'bin')) {
|
|
250
|
+
entries.push(path.join(process.env.HOME, '.local', 'analyticscli-npm', 'bin'));
|
|
251
|
+
}
|
|
252
|
+
return entries;
|
|
253
|
+
}
|
|
254
|
+
function renderProfilePathEntries(binDir) {
|
|
255
|
+
const home = process.env.HOME ? path.resolve(process.env.HOME) : null;
|
|
256
|
+
return getPathProfileEntries(binDir)
|
|
257
|
+
.map((entry) => {
|
|
258
|
+
const resolved = path.resolve(entry);
|
|
259
|
+
if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
|
|
260
|
+
return `$HOME/${path.relative(home, resolved)}`;
|
|
261
|
+
}
|
|
262
|
+
return entry;
|
|
263
|
+
})
|
|
264
|
+
.join(':');
|
|
265
|
+
}
|
|
266
|
+
async function ensureProfilePath(binDir) {
|
|
267
|
+
if (process.env.ANALYTICSCLI_SKIP_PROFILE_UPDATE === 'true' || !process.env.HOME) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const line = `export PATH="${renderProfilePathEntries(binDir)}:$PATH"`;
|
|
271
|
+
const profiles = ['.profile', '.bashrc', '.bash_profile', '.zshrc', '.zprofile'].map((name) => path.join(process.env.HOME, name));
|
|
272
|
+
let wrote = false;
|
|
273
|
+
for (const profile of profiles) {
|
|
274
|
+
let current = '';
|
|
275
|
+
try {
|
|
276
|
+
current = await fs.readFile(profile, 'utf8');
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
await fs.mkdir(path.dirname(profile), { recursive: true });
|
|
280
|
+
}
|
|
281
|
+
if (!current.includes(line)) {
|
|
282
|
+
await fs.appendFile(profile, `\n# AnalyticsCLI CLI user-local npm bin\n${line}\n`, 'utf8');
|
|
283
|
+
wrote = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return wrote;
|
|
287
|
+
}
|
|
288
|
+
async function verifyFreshShellProfile() {
|
|
289
|
+
if (!process.env.HOME) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
const cleanPath = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
293
|
+
const probes = [
|
|
294
|
+
{
|
|
295
|
+
shell: '/bin/bash',
|
|
296
|
+
command: 'for f in "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.profile"; do [[ -f "$f" ]] && source "$f" >/dev/null 2>&1 || true; done; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
shell: '/usr/bin/bash',
|
|
300
|
+
command: 'for f in "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.profile"; do [[ -f "$f" ]] && source "$f" >/dev/null 2>&1 || true; done; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
shell: '/bin/zsh',
|
|
304
|
+
command: 'for f in "$HOME/.zprofile" "$HOME/.zshrc" "$HOME/.profile"; do [[ -f "$f" ]] && source "$f" >/dev/null 2>&1 || true; done; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
shell: '/usr/bin/zsh',
|
|
308
|
+
command: 'for f in "$HOME/.zprofile" "$HOME/.zshrc" "$HOME/.profile"; do [[ -f "$f" ]] && source "$f" >/dev/null 2>&1 || true; done; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
shell: '/bin/sh',
|
|
312
|
+
command: '[ -f "$HOME/.profile" ] && . "$HOME/.profile" >/dev/null 2>&1 || true; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
shell: '/usr/bin/sh',
|
|
316
|
+
command: '[ -f "$HOME/.profile" ] && . "$HOME/.profile" >/dev/null 2>&1 || true; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
for (const probe of probes) {
|
|
320
|
+
if (!(await fileExists(probe.shell))) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const result = await runShellCommand(`env HOME=${quote(process.env.HOME)} PATH=${quote(cleanPath)} ${quote(probe.shell)} -lc ${quote(probe.command)}`, 30_000);
|
|
324
|
+
if (result.ok) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
function isUserLocalBin(binDir) {
|
|
331
|
+
if (!process.env.HOME) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const home = path.resolve(process.env.HOME);
|
|
335
|
+
const resolved = path.resolve(binDir);
|
|
336
|
+
return resolved === home || resolved.startsWith(`${home}${path.sep}`);
|
|
337
|
+
}
|
|
338
|
+
function isPermissionFailure(output) {
|
|
339
|
+
return /EACCES|permission denied|access denied|operation not permitted/i.test(String(output || ''));
|
|
340
|
+
}
|
|
341
|
+
async function ensureAnalyticsCliInstalled() {
|
|
342
|
+
const beforePath = await resolveCommandPath('analyticscli');
|
|
343
|
+
const npmExists = await commandExists('npm');
|
|
344
|
+
if (!npmExists) {
|
|
345
|
+
if (beforePath) {
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
detail: `analyticscli binary found at ${beforePath}; npm unavailable, so package update was skipped`,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
detail: `analyticscli binary missing and npm is unavailable; install ${ANALYTICSCLI_PACKAGE_SPEC}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const globalInstall = await runShellCommand(`npm install -g ${quote(ANALYTICSCLI_PACKAGE_SPEC)}`, 180_000);
|
|
357
|
+
if (!globalInstall.ok) {
|
|
358
|
+
const installOutput = `${globalInstall.stderr}\n${globalInstall.stdout}`;
|
|
359
|
+
if (isPermissionFailure(installOutput)) {
|
|
360
|
+
await fs.mkdir(ANALYTICSCLI_NPM_PREFIX, { recursive: true });
|
|
361
|
+
const localInstall = await runShellCommand(`npm install -g --prefix ${quote(ANALYTICSCLI_NPM_PREFIX)} ${quote(ANALYTICSCLI_PACKAGE_SPEC)}`, 180_000);
|
|
362
|
+
if (!localInstall.ok) {
|
|
363
|
+
return beforePath
|
|
364
|
+
? {
|
|
365
|
+
ok: true,
|
|
366
|
+
detail: `analyticscli binary found at ${beforePath}; update failed globally and in user-local prefix (${truncate(localInstall.stderr || localInstall.stdout)})`,
|
|
367
|
+
}
|
|
368
|
+
: {
|
|
369
|
+
ok: false,
|
|
370
|
+
detail: `npm install failed globally and in user-local prefix ${ANALYTICSCLI_NPM_PREFIX}: ${truncate(localInstall.stderr || localInstall.stdout)}`,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const localBinDir = path.join(ANALYTICSCLI_NPM_PREFIX, 'bin');
|
|
374
|
+
prependToPath(localBinDir);
|
|
375
|
+
await ensureProfilePath(localBinDir);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
return beforePath
|
|
379
|
+
? {
|
|
380
|
+
ok: true,
|
|
381
|
+
detail: `analyticscli binary found at ${beforePath}; package update failed (${truncate(installOutput)})`,
|
|
382
|
+
}
|
|
383
|
+
: {
|
|
384
|
+
ok: false,
|
|
385
|
+
detail: `npm install -g ${ANALYTICSCLI_PACKAGE_SPEC} failed: ${truncate(installOutput)}`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const afterPath = await resolveCommandPath('analyticscli');
|
|
390
|
+
if (afterPath) {
|
|
391
|
+
const helpCheck = await runShellCommand('analyticscli --help >/dev/null 2>&1', 30_000);
|
|
392
|
+
if (!helpCheck.ok) {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
detail: `analyticscli binary found at ${afterPath}, but --help failed: ${truncate(helpCheck.stderr || helpCheck.stdout)}`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const binDir = path.dirname(afterPath);
|
|
399
|
+
if (isUserLocalBin(binDir)) {
|
|
400
|
+
await ensureProfilePath(binDir);
|
|
401
|
+
if (!(await verifyFreshShellProfile())) {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
detail: `analyticscli works at ${afterPath}, but a fresh shell still cannot resolve it after profile update; add ${renderProfilePathEntries(binDir)} to PATH`,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
detail: `analyticscli package ensured via ${ANALYTICSCLI_PACKAGE_SPEC}; binary found at ${afterPath}; shell profiles updated and fresh shell verification passed`,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return afterPath
|
|
414
|
+
? {
|
|
415
|
+
ok: true,
|
|
416
|
+
detail: `analyticscli package ensured via ${ANALYTICSCLI_PACKAGE_SPEC}; binary found at ${afterPath}`,
|
|
417
|
+
}
|
|
418
|
+
: {
|
|
419
|
+
ok: false,
|
|
420
|
+
detail: `Installed ${ANALYTICSCLI_PACKAGE_SPEC}, but analyticscli is still not on PATH`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async function readJson(filePath) {
|
|
424
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
425
|
+
return JSON.parse(raw);
|
|
426
|
+
}
|
|
427
|
+
async function writeJson(filePath, value) {
|
|
428
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
429
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
function formatHeartbeatInterval(minutes) {
|
|
432
|
+
const intervalMinutes = Math.max(1, Math.floor(Number(minutes) || 1440));
|
|
433
|
+
if (intervalMinutes % 1440 === 0)
|
|
434
|
+
return `${intervalMinutes / 1440}d`;
|
|
435
|
+
if (intervalMinutes % 60 === 0)
|
|
436
|
+
return `${intervalMinutes / 60}h`;
|
|
437
|
+
return `${intervalMinutes}m`;
|
|
438
|
+
}
|
|
439
|
+
function getHeartbeatInterval(config) {
|
|
440
|
+
const scheduleInterval = Number(config?.schedule?.intervalMinutes);
|
|
441
|
+
const healthInterval = Number(config?.schedule?.connectorHealthCheckIntervalMinutes);
|
|
442
|
+
return Math.min(Number.isFinite(scheduleInterval) && scheduleInterval > 0 ? scheduleInterval : 1440, Number.isFinite(healthInterval) && healthInterval > 0 ? healthInterval : 720);
|
|
443
|
+
}
|
|
444
|
+
function relativeWorkspacePath(filePath) {
|
|
445
|
+
const relative = path.relative(process.cwd(), filePath);
|
|
446
|
+
return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : filePath;
|
|
447
|
+
}
|
|
448
|
+
function isEffectivelyEmptyHeartbeat(value) {
|
|
449
|
+
return String(value || '')
|
|
450
|
+
.split(/\r?\n/)
|
|
451
|
+
.map((line) => line.trim())
|
|
452
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('<!--') && !line.startsWith('-->'))
|
|
453
|
+
.length === 0;
|
|
454
|
+
}
|
|
455
|
+
function renderHeartbeatBlock(configPath, config) {
|
|
456
|
+
const interval = formatHeartbeatInterval(getHeartbeatInterval(config));
|
|
457
|
+
const displayConfigPath = relativeWorkspacePath(configPath);
|
|
458
|
+
return `${HEARTBEAT_MARKER_START}
|
|
459
|
+
tasks:
|
|
460
|
+
|
|
461
|
+
- name: openclaw-growth-engineer-run
|
|
462
|
+
interval: ${interval}
|
|
463
|
+
prompt: "Run \`node scripts/openclaw-growth-runner.mjs --config ${displayConfigPath}\` 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 \`node scripts/openclaw-growth-wizard.mjs --connectors\`. If there is no actionable output, reply HEARTBEAT_OK."
|
|
464
|
+
|
|
465
|
+
# Keep this section small. Do not put secrets in HEARTBEAT.md.
|
|
466
|
+
${HEARTBEAT_MARKER_END}`;
|
|
467
|
+
}
|
|
468
|
+
async function ensureGrowthHeartbeat(configPath, config) {
|
|
469
|
+
const heartbeatPath = path.resolve(DEFAULT_HEARTBEAT_PATH);
|
|
470
|
+
const block = renderHeartbeatBlock(configPath, config);
|
|
471
|
+
let existing = '';
|
|
472
|
+
let existed = true;
|
|
473
|
+
try {
|
|
474
|
+
existing = await fs.readFile(heartbeatPath, 'utf8');
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
existed = false;
|
|
478
|
+
}
|
|
479
|
+
const markerPattern = new RegExp(`${HEARTBEAT_MARKER_START}[\\s\\S]*?${HEARTBEAT_MARKER_END}`);
|
|
480
|
+
const next = markerPattern.test(existing)
|
|
481
|
+
? existing.replace(markerPattern, block)
|
|
482
|
+
: isEffectivelyEmptyHeartbeat(existing)
|
|
483
|
+
? `# OpenClaw heartbeat checklist\n\n${block}\n`
|
|
484
|
+
: `${existing.trimEnd()}\n\n${block}\n`;
|
|
485
|
+
if (next !== existing) {
|
|
486
|
+
await fs.writeFile(heartbeatPath, next, 'utf8');
|
|
487
|
+
return {
|
|
488
|
+
path: heartbeatPath,
|
|
489
|
+
interval: formatHeartbeatInterval(getHeartbeatInterval(config)),
|
|
490
|
+
created: !existed || isEffectivelyEmptyHeartbeat(existing),
|
|
491
|
+
updated: existed && !isEffectivelyEmptyHeartbeat(existing),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
path: heartbeatPath,
|
|
496
|
+
interval: formatHeartbeatInterval(getHeartbeatInterval(config)),
|
|
497
|
+
created: false,
|
|
498
|
+
updated: false,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function appendHelperDetail(details, label, result) {
|
|
502
|
+
if (result.ok) {
|
|
503
|
+
details.push(`${label}: ok`);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
details.push(`${label}: ${truncate(result.stderr || result.stdout || `exit ${result.code ?? 'unknown'}`)}`);
|
|
507
|
+
}
|
|
508
|
+
async function installClawHubSkill(skillName, details) {
|
|
509
|
+
if (await commandExists('clawhub')) {
|
|
510
|
+
const result = await runShellCommand(`clawhub install ${quote(skillName)} || clawhub install ${quote(skillName)} --force`, 180_000);
|
|
511
|
+
await appendHelperDetail(details, `ClawHub skill ${skillName}`, result);
|
|
512
|
+
return result.ok;
|
|
513
|
+
}
|
|
514
|
+
if (await commandExists('npx')) {
|
|
515
|
+
const result = await runShellCommand(`npx -y clawhub install ${quote(skillName)} || npx -y clawhub install ${quote(skillName)} --force`, 180_000);
|
|
516
|
+
await appendHelperDetail(details, `ClawHub skill ${skillName}`, result);
|
|
517
|
+
return result.ok;
|
|
518
|
+
}
|
|
519
|
+
details.push(`ClawHub skill ${skillName}: skipped because neither clawhub nor npx is available`);
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
async function installAgentSkill(repo, details) {
|
|
523
|
+
if (!(await commandExists('npx'))) {
|
|
524
|
+
details.push(`Agent skill ${repo}: skipped because npx is unavailable`);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
const result = await runShellCommand(`npx -y skills add ${quote(repo)}`, 180_000);
|
|
528
|
+
await appendHelperDetail(details, `Agent skill ${repo}`, result);
|
|
529
|
+
return result.ok;
|
|
530
|
+
}
|
|
531
|
+
async function installSystemBinary(commandName, details) {
|
|
532
|
+
if (await commandExists(commandName)) {
|
|
533
|
+
details.push(`${commandName} binary found at ${await resolveCommandPath(commandName)}`);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
if (await commandExists('brew')) {
|
|
537
|
+
const result = await runShellCommand(`brew install ${quote(commandName)}`, 600_000);
|
|
538
|
+
await appendHelperDetail(details, `brew install ${commandName}`, result);
|
|
539
|
+
}
|
|
540
|
+
else if (await commandExists('apt-get')) {
|
|
541
|
+
const prefix = process.getuid?.() === 0 ? '' : 'sudo -n ';
|
|
542
|
+
const result = await runShellCommand(`${prefix}apt-get update && ${prefix}apt-get install -y ${quote(commandName)}`, 600_000);
|
|
543
|
+
await appendHelperDetail(details, `apt-get install ${commandName}`, result);
|
|
544
|
+
}
|
|
545
|
+
else if (await commandExists('winget')) {
|
|
546
|
+
const packageId = commandName === 'gh' ? 'GitHub.cli' : commandName;
|
|
547
|
+
const result = await runShellCommand(`winget install --id ${quote(packageId)} -e --silent`, 600_000);
|
|
548
|
+
await appendHelperDetail(details, `winget install ${packageId}`, result);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
details.push(`No supported non-interactive installer found for ${commandName}`);
|
|
552
|
+
}
|
|
553
|
+
const installedPath = await resolveCommandPath(commandName);
|
|
554
|
+
if (installedPath) {
|
|
555
|
+
details.push(`${commandName} binary found at ${installedPath}`);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
function getUserLocalBinDir() {
|
|
561
|
+
return process.env.HOME ? path.join(process.env.HOME, '.local', 'bin') : null;
|
|
562
|
+
}
|
|
563
|
+
function prependPath(dir) {
|
|
564
|
+
const current = process.env.PATH || '';
|
|
565
|
+
if (!current.split(':').includes(dir)) {
|
|
566
|
+
process.env.PATH = `${dir}:${current}`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function getGitHubCliReleaseAssetName(version) {
|
|
570
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : '';
|
|
571
|
+
if (process.platform === 'linux' && arch) {
|
|
572
|
+
return `gh_${version}_linux_${arch}.tar.gz`;
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
async function resolveGitHubCliReleaseAssetUrl() {
|
|
577
|
+
const response = await fetch('https://api.github.com/repos/cli/cli/releases/latest', {
|
|
578
|
+
headers: {
|
|
579
|
+
Accept: 'application/vnd.github+json',
|
|
580
|
+
'User-Agent': 'openclaw-growth-start',
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
if (!response.ok) {
|
|
584
|
+
throw new Error(`GitHub CLI release lookup failed (${response.status})`);
|
|
585
|
+
}
|
|
586
|
+
const release = await response.json();
|
|
587
|
+
const version = String(release?.tag_name || '').replace(/^v/, '');
|
|
588
|
+
const assetName = getGitHubCliReleaseAssetName(version);
|
|
589
|
+
if (!assetName) {
|
|
590
|
+
throw new Error(`No user-local gh installer is defined for ${process.platform}/${process.arch}`);
|
|
591
|
+
}
|
|
592
|
+
const asset = Array.isArray(release?.assets) ? release.assets.find((entry) => entry?.name === assetName) : null;
|
|
593
|
+
if (!asset?.browser_download_url) {
|
|
594
|
+
throw new Error(`GitHub CLI release asset not found: ${assetName}`);
|
|
595
|
+
}
|
|
596
|
+
return asset.browser_download_url;
|
|
597
|
+
}
|
|
598
|
+
async function installGitHubCliUserLocal(details) {
|
|
599
|
+
const binDir = getUserLocalBinDir();
|
|
600
|
+
if (!binDir) {
|
|
601
|
+
details.push('gh user-local install skipped because HOME is not set');
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (!(await commandExists('curl'))) {
|
|
605
|
+
details.push('gh user-local install skipped because curl is unavailable');
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
if (!(await commandExists('tar'))) {
|
|
609
|
+
details.push('gh user-local install skipped because tar is unavailable');
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const url = await resolveGitHubCliReleaseAssetUrl();
|
|
614
|
+
const cacheDir = path.join(process.env.HOME, '.cache', 'openclaw-gh');
|
|
615
|
+
const command = [
|
|
616
|
+
'set -eu',
|
|
617
|
+
`mkdir -p ${quote(binDir)} ${quote(cacheDir)}`,
|
|
618
|
+
`tmp="$(mktemp -d ${quote(path.join(cacheDir, 'gh.XXXXXX'))})"`,
|
|
619
|
+
'trap \'rm -rf "$tmp"\' EXIT',
|
|
620
|
+
`curl -fsSL ${quote(url)} -o "$tmp/gh.tar.gz"`,
|
|
621
|
+
'tar -xzf "$tmp/gh.tar.gz" -C "$tmp"',
|
|
622
|
+
'gh_bin="$(find "$tmp" -path "*/bin/gh" -type f | head -n 1)"',
|
|
623
|
+
'test -n "$gh_bin"',
|
|
624
|
+
`cp "$gh_bin" ${quote(path.join(binDir, 'gh'))}`,
|
|
625
|
+
`chmod 755 ${quote(path.join(binDir, 'gh'))}`,
|
|
626
|
+
'for profile in "$HOME/.profile" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.zprofile"; do touch "$profile"; grep -Fq \'export PATH="$HOME/.local/bin:$PATH"\' "$profile" || printf \'\\n# OpenClaw user-local bin\\nexport PATH="$HOME/.local/bin:$PATH"\\n\' >> "$profile"; done',
|
|
627
|
+
].join(' && ');
|
|
628
|
+
const result = await runShellCommand(command, 600_000);
|
|
629
|
+
prependPath(binDir);
|
|
630
|
+
await appendHelperDetail(details, `user-local gh install to ${path.join(binDir, 'gh')}`, result);
|
|
631
|
+
const installedPath = await resolveCommandPath('gh');
|
|
632
|
+
if (installedPath) {
|
|
633
|
+
details.push(`gh binary found at ${installedPath}`);
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
details.push(`user-local gh install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
639
|
+
}
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
function resolveMcpNpmCacheDir() {
|
|
643
|
+
return process.env.OPENCLAW_MCP_NPM_CACHE ||
|
|
644
|
+
(process.env.HOME ? path.join(process.env.HOME, '.cache', 'openclaw-mcp-npm') : path.join(process.cwd(), '.openclaw-mcp-npm-cache'));
|
|
645
|
+
}
|
|
646
|
+
function escapeTomlString(value) {
|
|
647
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
648
|
+
}
|
|
649
|
+
async function upsertRevenueCatCodexMcpConfig(apiKey) {
|
|
650
|
+
if (!process.env.HOME)
|
|
651
|
+
return null;
|
|
652
|
+
const configDir = path.join(process.env.HOME, '.codex');
|
|
653
|
+
const configFile = path.join(configDir, 'config.toml');
|
|
654
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
655
|
+
let existing = '';
|
|
656
|
+
try {
|
|
657
|
+
existing = await fs.readFile(configFile, 'utf8');
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
existing = '';
|
|
661
|
+
}
|
|
662
|
+
const block = `[mcp_servers.revenuecat]
|
|
663
|
+
command = "npx"
|
|
664
|
+
args = ["--yes", "--cache", "${escapeTomlString(resolveMcpNpmCacheDir())}", "mcp-remote", "https://mcp.revenuecat.ai/mcp", "--header", "Authorization: Bearer \${AUTH_TOKEN}"]
|
|
665
|
+
env = { AUTH_TOKEN = "${escapeTomlString(apiKey)}" }
|
|
666
|
+
type = "stdio"
|
|
667
|
+
startup_timeout_ms = 20000
|
|
668
|
+
`;
|
|
669
|
+
const pattern = /(?:^|\n)\[mcp_servers\.revenuecat\]\n(?:.*\n)*?(?=\n\[|\s*$)/m;
|
|
670
|
+
const next = pattern.test(existing)
|
|
671
|
+
? existing.replace(pattern, `${existing.startsWith('[mcp_servers.revenuecat]') ? '' : '\n'}${block}`)
|
|
672
|
+
: `${existing.trimEnd()}${existing.trim() ? '\n\n' : ''}${block}`;
|
|
673
|
+
await fs.writeFile(configFile, `${next.trimEnd()}\n`, 'utf8');
|
|
674
|
+
return configFile;
|
|
675
|
+
}
|
|
676
|
+
async function upsertSentryCodexMcpConfig(token) {
|
|
677
|
+
if (!process.env.HOME)
|
|
678
|
+
return null;
|
|
679
|
+
const configDir = path.join(process.env.HOME, '.codex');
|
|
680
|
+
const configFile = path.join(configDir, 'config.toml');
|
|
681
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
682
|
+
let existing = '';
|
|
683
|
+
try {
|
|
684
|
+
existing = await fs.readFile(configFile, 'utf8');
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
existing = '';
|
|
688
|
+
}
|
|
689
|
+
const envEntries = [
|
|
690
|
+
`SENTRY_ACCESS_TOKEN = "${escapeTomlString(token)}"`,
|
|
691
|
+
process.env.SENTRY_BASE_URL && process.env.SENTRY_BASE_URL !== 'https://sentry.io'
|
|
692
|
+
? `SENTRY_HOST = "${escapeTomlString(String(process.env.SENTRY_BASE_URL).replace(/^https?:\/\//, '').replace(/\/$/, ''))}"`
|
|
693
|
+
: null,
|
|
694
|
+
].filter(Boolean);
|
|
695
|
+
const block = `[mcp_servers.sentry]
|
|
696
|
+
command = "npx"
|
|
697
|
+
args = ["--yes", "--cache", "${escapeTomlString(resolveMcpNpmCacheDir())}", "@sentry/mcp-server@latest"]
|
|
698
|
+
env = { ${envEntries.join(', ')} }
|
|
699
|
+
type = "stdio"
|
|
700
|
+
startup_timeout_ms = 30000
|
|
701
|
+
`;
|
|
702
|
+
const pattern = /(?:^|\n)\[mcp_servers\.sentry\]\n(?:.*\n)*?(?=\n\[|\s*$)/m;
|
|
703
|
+
const next = pattern.test(existing)
|
|
704
|
+
? existing.replace(pattern, `${existing.startsWith('[mcp_servers.sentry]') ? '' : '\n'}${block}`)
|
|
705
|
+
: `${existing.trimEnd()}${existing.trim() ? '\n\n' : ''}${block}`;
|
|
706
|
+
await fs.writeFile(configFile, `${next.trimEnd()}\n`, 'utf8');
|
|
707
|
+
return configFile;
|
|
708
|
+
}
|
|
709
|
+
async function installRevenueCatConnector() {
|
|
710
|
+
const details = [];
|
|
711
|
+
if (!(await commandExists('npx'))) {
|
|
712
|
+
return { connector: 'revenuecat', ok: false, detail: 'npx is required for RevenueCat MCP transport but is unavailable' };
|
|
713
|
+
}
|
|
714
|
+
const check = await runShellCommand(`npx --yes --cache ${quote(resolveMcpNpmCacheDir())} mcp-remote`, 120_000);
|
|
715
|
+
const output = `${check.stderr}\n${check.stdout}`;
|
|
716
|
+
const available = check.ok || /Usage: .*mcp-remote|Usage: .*proxy\.ts/i.test(output);
|
|
717
|
+
if (!available) {
|
|
718
|
+
await appendHelperDetail(details, 'npx mcp-remote availability check', check);
|
|
719
|
+
return { connector: 'revenuecat', ok: false, detail: details.join('; ') };
|
|
720
|
+
}
|
|
721
|
+
details.push(`RevenueCat MCP transport mcp-remote is available via npx cache ${resolveMcpNpmCacheDir()}`);
|
|
722
|
+
const apiKey = String(process.env.REVENUECAT_API_KEY || '').trim();
|
|
723
|
+
if (apiKey) {
|
|
724
|
+
const configFile = await upsertRevenueCatCodexMcpConfig(apiKey);
|
|
725
|
+
details.push(configFile ? `RevenueCat MCP configured in ${configFile}` : 'RevenueCat MCP transport available; HOME missing so MCP config was not written');
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
details.push('Set REVENUECAT_API_KEY, then rerun this command to write the RevenueCat MCP client config');
|
|
729
|
+
}
|
|
730
|
+
return { connector: 'revenuecat', ok: true, detail: details.join('; ') };
|
|
731
|
+
}
|
|
732
|
+
async function installSentryConnector() {
|
|
733
|
+
const details = [];
|
|
734
|
+
if (await commandExists('npx')) {
|
|
735
|
+
const check = await runShellCommand(`npx --yes --cache ${quote(resolveMcpNpmCacheDir())} @sentry/mcp-server@latest --help`, 120_000);
|
|
736
|
+
const output = `${check.stderr}\n${check.stdout}`;
|
|
737
|
+
const available = check.ok || /sentry|mcp-server|access-token/i.test(output);
|
|
738
|
+
if (available) {
|
|
739
|
+
details.push(`Sentry MCP server is available via npx cache ${resolveMcpNpmCacheDir()}`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
await appendHelperDetail(details, 'Sentry MCP availability check', check);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
details.push('npx unavailable; Sentry MCP config was skipped, direct API exporter remains available');
|
|
747
|
+
}
|
|
748
|
+
const token = String(process.env.SENTRY_AUTH_TOKEN || '').trim();
|
|
749
|
+
if (token && (await commandExists('npx'))) {
|
|
750
|
+
const configFile = await upsertSentryCodexMcpConfig(token);
|
|
751
|
+
details.push(configFile ? `Sentry MCP configured in ${configFile}` : 'Sentry MCP available; HOME missing so MCP config was not written');
|
|
752
|
+
}
|
|
753
|
+
else if (!token) {
|
|
754
|
+
details.push('Set SENTRY_AUTH_TOKEN, then rerun this command to write Sentry MCP client config');
|
|
755
|
+
}
|
|
756
|
+
details.push('Sentry direct API exporter enabled via node scripts/export-sentry-summary.mjs');
|
|
757
|
+
return { connector: 'sentry', ok: true, detail: details.join('; ') };
|
|
758
|
+
}
|
|
759
|
+
async function installGitHubConnector() {
|
|
760
|
+
const details = [];
|
|
761
|
+
await installClawHubSkill('github', details);
|
|
762
|
+
let ok = await installSystemBinary('gh', details);
|
|
763
|
+
if (!ok) {
|
|
764
|
+
ok = await installGitHubCliUserLocal(details);
|
|
765
|
+
}
|
|
766
|
+
const repo = await detectGitHubRepo();
|
|
767
|
+
if (repo) {
|
|
768
|
+
details.push(`GitHub repo configured for code access: ${repo}`);
|
|
769
|
+
}
|
|
770
|
+
else if (process.env.GITHUB_TOKEN) {
|
|
771
|
+
details.push('GITHUB_TOKEN is set; repo selection is deferred per app/task');
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
details.push('GitHub token not configured yet; rerun connector wizard for github when ready');
|
|
775
|
+
}
|
|
776
|
+
return { connector: 'github', ok, detail: details.join('; ') };
|
|
777
|
+
}
|
|
778
|
+
async function installAscConnector() {
|
|
779
|
+
const details = [];
|
|
780
|
+
await installAgentSkill('rorkai/app-store-connect-cli-skills', details);
|
|
781
|
+
let ok = await installSystemBinary('asc', details);
|
|
782
|
+
if (!ok && (await commandExists('curl'))) {
|
|
783
|
+
const result = await runShellCommand('curl -fsSL https://asccli.sh/install | bash', 600_000);
|
|
784
|
+
await appendHelperDetail(details, 'asc install script', result);
|
|
785
|
+
ok = Boolean(await resolveCommandPath('asc'));
|
|
786
|
+
}
|
|
787
|
+
return { connector: 'asc', ok, detail: `${details.join('; ')}${ok ? '; next run asc auth status --validate or asc auth login' : ''}` };
|
|
788
|
+
}
|
|
789
|
+
async function installAnalyticsConnector() {
|
|
790
|
+
const analyticsCliPath = await resolveCommandPath('analyticscli');
|
|
791
|
+
return {
|
|
792
|
+
connector: 'analytics',
|
|
793
|
+
ok: Boolean(analyticsCliPath),
|
|
794
|
+
detail: analyticsCliPath
|
|
795
|
+
? `analyticscli binary found at ${analyticsCliPath}; token is read from the wizard-managed AnalyticsCLI environment`
|
|
796
|
+
: 'analyticscli binary missing after dependency setup',
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
async function enableConnectorConfig(configPath, connectors) {
|
|
800
|
+
if (connectors.length === 0 || !(await fileExists(configPath)))
|
|
801
|
+
return;
|
|
802
|
+
const config = await readJson(configPath);
|
|
803
|
+
const extra = Array.isArray(config.sources?.extra) ? config.sources.extra : [];
|
|
804
|
+
const next = {
|
|
805
|
+
...config,
|
|
806
|
+
sources: {
|
|
807
|
+
...(config.sources || {}),
|
|
808
|
+
analytics: connectors.includes('analytics')
|
|
809
|
+
? { ...(config.sources?.analytics || {}), enabled: true, mode: 'command', command: config.sources?.analytics?.command || getDefaultSourceCommand('analytics') }
|
|
810
|
+
: config.sources?.analytics,
|
|
811
|
+
revenuecat: connectors.includes('revenuecat')
|
|
812
|
+
? { ...(config.sources?.revenuecat || {}), enabled: true, mode: 'command', command: getDefaultSourceCommand('revenuecat') }
|
|
813
|
+
: config.sources?.revenuecat,
|
|
814
|
+
sentry: connectors.includes('sentry')
|
|
815
|
+
? { ...(config.sources?.sentry || {}), enabled: true, mode: 'command', command: getDefaultSourceCommand('sentry') }
|
|
816
|
+
: config.sources?.sentry,
|
|
817
|
+
extra: extra.map((source) => connectors.includes('asc') && source?.service === 'asc-cli'
|
|
818
|
+
? { ...source, enabled: true, mode: 'command', command: source.command || getDefaultSourceCommand('asc') }
|
|
819
|
+
: source),
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
await writeJson(configPath, next);
|
|
823
|
+
}
|
|
824
|
+
async function installConnectorHelpers(configPath, connectors) {
|
|
825
|
+
await enableConnectorConfig(configPath, connectors);
|
|
826
|
+
const results = [];
|
|
827
|
+
for (const connector of connectors) {
|
|
828
|
+
if (connector === 'analytics')
|
|
829
|
+
results.push(await installAnalyticsConnector());
|
|
830
|
+
if (connector === 'github')
|
|
831
|
+
results.push(await installGitHubConnector());
|
|
832
|
+
if (connector === 'asc')
|
|
833
|
+
results.push(await installAscConnector());
|
|
834
|
+
if (connector === 'revenuecat')
|
|
835
|
+
results.push(await installRevenueCatConnector());
|
|
836
|
+
if (connector === 'sentry')
|
|
837
|
+
results.push(await installSentryConnector());
|
|
838
|
+
}
|
|
839
|
+
return results;
|
|
840
|
+
}
|
|
841
|
+
function parseGitHubRepoFromRemote(remoteUrl) {
|
|
842
|
+
const value = String(remoteUrl || '').trim();
|
|
843
|
+
if (!value)
|
|
844
|
+
return null;
|
|
845
|
+
const sshMatch = value.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/i);
|
|
846
|
+
if (sshMatch)
|
|
847
|
+
return sshMatch[1];
|
|
848
|
+
const httpsMatch = value.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/i);
|
|
849
|
+
if (httpsMatch)
|
|
850
|
+
return httpsMatch[1];
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
function isConfiguredGitHubRepo(value) {
|
|
854
|
+
const repo = String(value || '').trim();
|
|
855
|
+
return Boolean(repo && repo !== 'owner/repo' && /^[^/\s]+\/[^/\s]+$/.test(repo));
|
|
856
|
+
}
|
|
857
|
+
async function detectGitHubRepo() {
|
|
858
|
+
const explicit = String(process.env.OPENCLAW_GITHUB_REPO || '').trim();
|
|
859
|
+
if (isConfiguredGitHubRepo(explicit))
|
|
860
|
+
return explicit;
|
|
861
|
+
const remoteResult = await runShellCommand('git config --get remote.origin.url', 10_000);
|
|
862
|
+
if (!remoteResult.ok)
|
|
863
|
+
return null;
|
|
864
|
+
return parseGitHubRepoFromRemote(remoteResult.stdout.trim());
|
|
865
|
+
}
|
|
866
|
+
async function ensureConfig(configPath) {
|
|
867
|
+
if (await fileExists(configPath)) {
|
|
868
|
+
const config = await readJson(configPath);
|
|
869
|
+
if (!isConfiguredGitHubRepo(config?.project?.githubRepo)) {
|
|
870
|
+
const detectedRepo = await detectGitHubRepo();
|
|
871
|
+
if (detectedRepo) {
|
|
872
|
+
config.project = {
|
|
873
|
+
...(config.project || {}),
|
|
874
|
+
githubRepo: detectedRepo,
|
|
875
|
+
};
|
|
876
|
+
await writeJson(configPath, config);
|
|
877
|
+
return {
|
|
878
|
+
created: false,
|
|
879
|
+
configPath,
|
|
880
|
+
githubRepo: detectedRepo,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
created: false,
|
|
886
|
+
configPath,
|
|
887
|
+
githubRepo: null,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const templatePath = path.resolve(DEFAULT_TEMPLATE_PATH);
|
|
891
|
+
const template = await readJson(templatePath);
|
|
892
|
+
const detectedRepo = await detectGitHubRepo();
|
|
893
|
+
const githubRepo = detectedRepo || '';
|
|
894
|
+
const config = {
|
|
895
|
+
...template,
|
|
896
|
+
generatedAt: new Date().toISOString(),
|
|
897
|
+
project: {
|
|
898
|
+
...template.project,
|
|
899
|
+
githubRepo,
|
|
900
|
+
repoRoot: '.',
|
|
901
|
+
},
|
|
902
|
+
sources: {
|
|
903
|
+
...template.sources,
|
|
904
|
+
analytics: {
|
|
905
|
+
enabled: true,
|
|
906
|
+
mode: 'command',
|
|
907
|
+
command: getDefaultSourceCommand('analytics'),
|
|
908
|
+
},
|
|
909
|
+
revenuecat: {
|
|
910
|
+
...(template.sources?.revenuecat || {}),
|
|
911
|
+
enabled: false,
|
|
912
|
+
mode: 'command',
|
|
913
|
+
command: getDefaultSourceCommand('revenuecat'),
|
|
914
|
+
},
|
|
915
|
+
sentry: {
|
|
916
|
+
...(template.sources?.sentry || {}),
|
|
917
|
+
enabled: false,
|
|
918
|
+
mode: 'command',
|
|
919
|
+
command: getDefaultSourceCommand('sentry'),
|
|
920
|
+
},
|
|
921
|
+
feedback: {
|
|
922
|
+
...(template.sources?.feedback || {}),
|
|
923
|
+
enabled: false,
|
|
924
|
+
},
|
|
925
|
+
extra: Array.isArray(template.sources?.extra) ? template.sources.extra : [],
|
|
926
|
+
},
|
|
927
|
+
actions: {
|
|
928
|
+
...template.actions,
|
|
929
|
+
mode: 'issue',
|
|
930
|
+
autoCreateIssues: false,
|
|
931
|
+
autoCreatePullRequests: false,
|
|
932
|
+
draftPullRequests: true,
|
|
933
|
+
proposalBranchPrefix: 'openclaw/proposals',
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
await writeJson(configPath, config);
|
|
937
|
+
return {
|
|
938
|
+
created: true,
|
|
939
|
+
configPath,
|
|
940
|
+
githubRepo,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function parseJsonFromStdout(stdout) {
|
|
944
|
+
const raw = String(stdout || '').trim();
|
|
945
|
+
if (!raw)
|
|
946
|
+
return null;
|
|
947
|
+
const firstBrace = raw.indexOf('{');
|
|
948
|
+
const firstBracket = raw.indexOf('[');
|
|
949
|
+
const starts = [firstBrace, firstBracket].filter((index) => index >= 0);
|
|
950
|
+
if (starts.length === 0)
|
|
951
|
+
return null;
|
|
952
|
+
const jsonStart = Math.min(...starts);
|
|
953
|
+
try {
|
|
954
|
+
return JSON.parse(raw.slice(jsonStart));
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function normalizeString(value) {
|
|
961
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
962
|
+
}
|
|
963
|
+
function extractProjectChoices(payload) {
|
|
964
|
+
const candidates = (() => {
|
|
965
|
+
if (Array.isArray(payload))
|
|
966
|
+
return payload;
|
|
967
|
+
if (payload && typeof payload === 'object') {
|
|
968
|
+
if (Array.isArray(payload.projects))
|
|
969
|
+
return payload.projects;
|
|
970
|
+
if (Array.isArray(payload.items))
|
|
971
|
+
return payload.items;
|
|
972
|
+
if (Array.isArray(payload.data))
|
|
973
|
+
return payload.data;
|
|
974
|
+
}
|
|
975
|
+
return [];
|
|
976
|
+
})();
|
|
977
|
+
const byId = new Map();
|
|
978
|
+
for (const candidate of candidates) {
|
|
979
|
+
if (!candidate || typeof candidate !== 'object')
|
|
980
|
+
continue;
|
|
981
|
+
const id = normalizeString(candidate.id) ||
|
|
982
|
+
normalizeString(candidate.projectId) ||
|
|
983
|
+
normalizeString(candidate.project_id);
|
|
984
|
+
if (!id)
|
|
985
|
+
continue;
|
|
986
|
+
const name = normalizeString(candidate.name) || normalizeString(candidate.displayName);
|
|
987
|
+
const slug = normalizeString(candidate.slug);
|
|
988
|
+
byId.set(id, {
|
|
989
|
+
id,
|
|
990
|
+
name,
|
|
991
|
+
slug,
|
|
992
|
+
label: name || slug || id,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
return [...byId.values()].sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
996
|
+
}
|
|
997
|
+
function isMissingProjectSelection(text) {
|
|
998
|
+
return /Project ID is missing|Pass --project <id>|analyticscli projects select/i.test(String(text || ''));
|
|
999
|
+
}
|
|
1000
|
+
function commandHasProjectFlag(command) {
|
|
1001
|
+
return /(^|\s)--project(\s|=|$)/.test(String(command || ''));
|
|
1002
|
+
}
|
|
1003
|
+
function appendProjectFlag(command, projectId) {
|
|
1004
|
+
const raw = String(command || '').trim();
|
|
1005
|
+
if (!raw || commandHasProjectFlag(raw))
|
|
1006
|
+
return raw;
|
|
1007
|
+
return `${raw} --project ${quote(projectId)}`;
|
|
1008
|
+
}
|
|
1009
|
+
function commandHasAscAppFlag(command) {
|
|
1010
|
+
return /(^|\s)--app(\s|=|$)/.test(String(command || ''));
|
|
1011
|
+
}
|
|
1012
|
+
function appendAscAppFlag(command, appId) {
|
|
1013
|
+
const raw = String(command || '').trim();
|
|
1014
|
+
if (!raw || commandHasAscAppFlag(raw))
|
|
1015
|
+
return raw;
|
|
1016
|
+
return `${raw} --app ${quote(appId)}`;
|
|
1017
|
+
}
|
|
1018
|
+
async function configureAnalyticsProject(configPath, projectId) {
|
|
1019
|
+
const normalizedProjectId = normalizeString(projectId);
|
|
1020
|
+
if (!normalizedProjectId)
|
|
1021
|
+
return false;
|
|
1022
|
+
const config = await readJson(configPath);
|
|
1023
|
+
let changed = false;
|
|
1024
|
+
for (const sourceName of ['analytics', 'feedback']) {
|
|
1025
|
+
const source = config?.sources?.[sourceName];
|
|
1026
|
+
if (!source || source.enabled === false || source.mode !== 'command' || !source.command) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const nextCommand = appendProjectFlag(source.command, normalizedProjectId);
|
|
1030
|
+
if (nextCommand !== source.command) {
|
|
1031
|
+
source.command = nextCommand;
|
|
1032
|
+
changed = true;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (!config.project || typeof config.project !== 'object') {
|
|
1036
|
+
config.project = {};
|
|
1037
|
+
}
|
|
1038
|
+
if (config.project.analyticsProjectId !== normalizedProjectId) {
|
|
1039
|
+
config.project.analyticsProjectId = normalizedProjectId;
|
|
1040
|
+
changed = true;
|
|
1041
|
+
}
|
|
1042
|
+
if (changed) {
|
|
1043
|
+
await writeJson(configPath, config);
|
|
1044
|
+
}
|
|
1045
|
+
return changed;
|
|
1046
|
+
}
|
|
1047
|
+
async function configureAscApp(configPath, appId) {
|
|
1048
|
+
const normalizedAppId = normalizeString(appId);
|
|
1049
|
+
if (!normalizedAppId)
|
|
1050
|
+
return false;
|
|
1051
|
+
const config = await readJson(configPath);
|
|
1052
|
+
let changed = false;
|
|
1053
|
+
const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
|
|
1054
|
+
for (const source of extraSources) {
|
|
1055
|
+
if (!source || typeof source !== 'object')
|
|
1056
|
+
continue;
|
|
1057
|
+
const service = String(source.service || source.key || '').trim().toLowerCase();
|
|
1058
|
+
if (!['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service))
|
|
1059
|
+
continue;
|
|
1060
|
+
if (source.mode === 'command' && source.command) {
|
|
1061
|
+
const nextCommand = appendAscAppFlag(source.command, normalizedAppId);
|
|
1062
|
+
if (nextCommand !== source.command) {
|
|
1063
|
+
source.command = nextCommand;
|
|
1064
|
+
changed = true;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (!config.project || typeof config.project !== 'object') {
|
|
1069
|
+
config.project = {};
|
|
1070
|
+
}
|
|
1071
|
+
if (config.project.ascAppId !== normalizedAppId) {
|
|
1072
|
+
config.project.ascAppId = normalizedAppId;
|
|
1073
|
+
changed = true;
|
|
1074
|
+
}
|
|
1075
|
+
if (changed) {
|
|
1076
|
+
await writeJson(configPath, config);
|
|
1077
|
+
}
|
|
1078
|
+
process.env.ASC_APP_ID = normalizedAppId;
|
|
1079
|
+
return changed;
|
|
1080
|
+
}
|
|
1081
|
+
function configHasEnabledAscSource(config) {
|
|
1082
|
+
const extraSources = Array.isArray(config?.sources?.extra) ? config.sources.extra : [];
|
|
1083
|
+
return extraSources.some((source) => {
|
|
1084
|
+
if (!source || typeof source !== 'object' || source.enabled === false)
|
|
1085
|
+
return false;
|
|
1086
|
+
const service = String(source.service || source.key || '').trim().toLowerCase();
|
|
1087
|
+
return ['asc', 'asc-cli', 'app-store-connect', 'app_store_connect'].includes(service);
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
function extractAscAppChoices(payload) {
|
|
1091
|
+
const candidates = (() => {
|
|
1092
|
+
if (Array.isArray(payload))
|
|
1093
|
+
return payload;
|
|
1094
|
+
if (payload && typeof payload === 'object') {
|
|
1095
|
+
if (Array.isArray(payload.apps))
|
|
1096
|
+
return payload.apps;
|
|
1097
|
+
if (Array.isArray(payload.items))
|
|
1098
|
+
return payload.items;
|
|
1099
|
+
if (Array.isArray(payload.data))
|
|
1100
|
+
return payload.data;
|
|
1101
|
+
}
|
|
1102
|
+
return [];
|
|
1103
|
+
})();
|
|
1104
|
+
const byId = new Map();
|
|
1105
|
+
for (const candidate of candidates) {
|
|
1106
|
+
if (!candidate || typeof candidate !== 'object')
|
|
1107
|
+
continue;
|
|
1108
|
+
const attrs = candidate.attributes && typeof candidate.attributes === 'object' ? candidate.attributes : {};
|
|
1109
|
+
const id = normalizeString(candidate.id) ||
|
|
1110
|
+
normalizeString(candidate.appId) ||
|
|
1111
|
+
normalizeString(candidate.app_id);
|
|
1112
|
+
if (!id)
|
|
1113
|
+
continue;
|
|
1114
|
+
const name = normalizeString(candidate.name) ||
|
|
1115
|
+
normalizeString(candidate.appName) ||
|
|
1116
|
+
normalizeString(candidate.displayName) ||
|
|
1117
|
+
normalizeString(attrs.name) ||
|
|
1118
|
+
normalizeString(attrs.bundleId);
|
|
1119
|
+
const bundleId = normalizeString(candidate.bundleId) ||
|
|
1120
|
+
normalizeString(candidate.bundle_id) ||
|
|
1121
|
+
normalizeString(attrs.bundleId);
|
|
1122
|
+
byId.set(id, {
|
|
1123
|
+
id,
|
|
1124
|
+
name,
|
|
1125
|
+
bundleId,
|
|
1126
|
+
label: [name || id, bundleId ? `(${bundleId})` : null, id !== name ? id : null].filter(Boolean).join(' '),
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
return [...byId.values()].sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
1130
|
+
}
|
|
1131
|
+
async function listAscApps() {
|
|
1132
|
+
const result = await runShellCommand('asc apps list --output json', 60_000);
|
|
1133
|
+
if (!result.ok) {
|
|
1134
|
+
return {
|
|
1135
|
+
ok: false,
|
|
1136
|
+
error: result.stderr || `exit ${result.code}`,
|
|
1137
|
+
apps: [],
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const payload = parseJsonFromStdout(result.stdout);
|
|
1141
|
+
return {
|
|
1142
|
+
ok: true,
|
|
1143
|
+
error: null,
|
|
1144
|
+
apps: extractAscAppChoices(payload),
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async function ensureAscAppConfigured(configPath, explicitAppId) {
|
|
1148
|
+
if (normalizeString(explicitAppId)) {
|
|
1149
|
+
const changed = await configureAscApp(configPath, explicitAppId);
|
|
1150
|
+
return { ok: true, configured: true, changed, appId: explicitAppId, appScope: 'single_app', needsUserInput: false };
|
|
1151
|
+
}
|
|
1152
|
+
const config = await readJson(configPath);
|
|
1153
|
+
if (!configHasEnabledAscSource(config)) {
|
|
1154
|
+
return { ok: true, configured: false, changed: false, appId: null, appScope: 'disabled', needsUserInput: false };
|
|
1155
|
+
}
|
|
1156
|
+
const configuredAppId = normalizeString(config.project?.ascAppId) || normalizeString(process.env.ASC_APP_ID);
|
|
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
|
+
}
|
|
1161
|
+
const appList = await listAscApps();
|
|
1162
|
+
if (!appList.ok) {
|
|
1163
|
+
return {
|
|
1164
|
+
ok: false,
|
|
1165
|
+
configured: false,
|
|
1166
|
+
changed: false,
|
|
1167
|
+
appId: null,
|
|
1168
|
+
needsUserInput: false,
|
|
1169
|
+
error: truncate(appList.error, 800),
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
return {
|
|
1173
|
+
ok: true,
|
|
1174
|
+
configured: true,
|
|
1175
|
+
changed: false,
|
|
1176
|
+
appId: null,
|
|
1177
|
+
appScope: 'all_accessible_apps',
|
|
1178
|
+
apps: appList.apps,
|
|
1179
|
+
appCount: appList.apps.length,
|
|
1180
|
+
needsUserInput: false,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
async function listAnalyticsProjects() {
|
|
1184
|
+
const result = await runShellCommand('analyticscli projects list --format json', 60_000);
|
|
1185
|
+
if (!result.ok) {
|
|
1186
|
+
return {
|
|
1187
|
+
ok: false,
|
|
1188
|
+
error: result.stderr || `exit ${result.code}`,
|
|
1189
|
+
projects: [],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const payload = parseJsonFromStdout(result.stdout);
|
|
1193
|
+
return {
|
|
1194
|
+
ok: true,
|
|
1195
|
+
error: null,
|
|
1196
|
+
projects: extractProjectChoices(payload),
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function configHasEnabledAnalyticsSource(config) {
|
|
1200
|
+
return Boolean(config?.sources?.analytics && config.sources.analytics.enabled !== false);
|
|
1201
|
+
}
|
|
1202
|
+
async function ensureAnalyticsProjectConfigured(configPath, explicitProjectId) {
|
|
1203
|
+
if (normalizeString(explicitProjectId)) {
|
|
1204
|
+
const changed = await configureAnalyticsProject(configPath, explicitProjectId);
|
|
1205
|
+
return { ok: true, configured: true, changed, projectId: explicitProjectId, projectScope: 'single_project', needsUserInput: false };
|
|
1206
|
+
}
|
|
1207
|
+
const config = await readJson(configPath);
|
|
1208
|
+
if (!configHasEnabledAnalyticsSource(config)) {
|
|
1209
|
+
return { ok: true, configured: false, changed: false, projectId: null, projectScope: 'disabled', needsUserInput: false };
|
|
1210
|
+
}
|
|
1211
|
+
const configuredProjectId = normalizeString(config.project?.analyticsProjectId);
|
|
1212
|
+
if (configuredProjectId) {
|
|
1213
|
+
const changed = await configureAnalyticsProject(configPath, configuredProjectId);
|
|
1214
|
+
return { ok: true, configured: true, changed, projectId: configuredProjectId, projectScope: 'single_project', needsUserInput: false };
|
|
1215
|
+
}
|
|
1216
|
+
const projectList = await listAnalyticsProjects();
|
|
1217
|
+
if (!projectList.ok) {
|
|
1218
|
+
return {
|
|
1219
|
+
ok: true,
|
|
1220
|
+
configured: false,
|
|
1221
|
+
changed: false,
|
|
1222
|
+
projectId: null,
|
|
1223
|
+
projectScope: 'all_accessible_projects',
|
|
1224
|
+
projectCount: null,
|
|
1225
|
+
needsUserInput: false,
|
|
1226
|
+
warning: truncate(projectList.error, 800),
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
ok: true,
|
|
1231
|
+
configured: false,
|
|
1232
|
+
changed: false,
|
|
1233
|
+
projectId: null,
|
|
1234
|
+
projectScope: 'all_accessible_projects',
|
|
1235
|
+
projectCount: projectList.projects.length,
|
|
1236
|
+
projects: projectList.projects,
|
|
1237
|
+
needsUserInput: false,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
async function buildProjectSelectionResponse({ configCreated, configPath, projectConfigured, rawError }) {
|
|
1241
|
+
const projectList = await listAnalyticsProjects();
|
|
1242
|
+
const projects = projectList.projects;
|
|
1243
|
+
return {
|
|
1244
|
+
ok: false,
|
|
1245
|
+
phase: 'analytics_project_scope_error',
|
|
1246
|
+
setupComplete: false,
|
|
1247
|
+
configCreated,
|
|
1248
|
+
configPath,
|
|
1249
|
+
projectConfigured,
|
|
1250
|
+
needsUserInput: false,
|
|
1251
|
+
question: null,
|
|
1252
|
+
message: 'An AnalyticsCLI command still requires a project pin, but connector setup should use all accessible projects by default.',
|
|
1253
|
+
projects,
|
|
1254
|
+
suggestedProjectId: null,
|
|
1255
|
+
nextCommand: `node scripts/openclaw-growth-start.mjs --config ${quote(configPath)}`,
|
|
1256
|
+
alternatePersistCommand: null,
|
|
1257
|
+
retryCommand: `node scripts/openclaw-growth-start.mjs --config ${quote(configPath)}`,
|
|
1258
|
+
rawError: truncate(rawError, 800),
|
|
1259
|
+
projectListError: projectList.ok ? null : truncate(projectList.error, 800),
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function remediationForCheck(checkName, configPath) {
|
|
1263
|
+
if (checkName === 'dependency:analyticscli') {
|
|
1264
|
+
return 'Run AnalyticsCLI CLI with `npx -y @analyticscli/cli@preview --help`, or use `@analyticscli/cli` after stable release.';
|
|
1265
|
+
}
|
|
1266
|
+
if (checkName === 'project:github-repo') {
|
|
1267
|
+
return `Set \`project.githubRepo\` in ${configPath} (owner/repo).`;
|
|
1268
|
+
}
|
|
1269
|
+
if (checkName.startsWith('secret:GITHUB_TOKEN')) {
|
|
1270
|
+
return 'Set `GITHUB_TOKEN` (fine-grained PAT with repository `Issues: Read/Write` and `Contents: Read`).';
|
|
1271
|
+
}
|
|
1272
|
+
if (checkName === 'source:analytics:file') {
|
|
1273
|
+
return 'Write `data/openclaw-growth-engineer/analytics_summary.json` via your analytics refresh step (API-key based source command/file generation).';
|
|
1274
|
+
}
|
|
1275
|
+
if (checkName === 'connection:analytics') {
|
|
1276
|
+
return 'Run `node scripts/openclaw-growth-wizard.mjs --connectors analytics` and paste a fresh AnalyticsCLI readonly CLI token into the local terminal wizard.';
|
|
1277
|
+
}
|
|
1278
|
+
if (checkName === 'connection:github') {
|
|
1279
|
+
return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>` + issues API.';
|
|
1280
|
+
}
|
|
1281
|
+
if (checkName === 'connection:github-pull-requests') {
|
|
1282
|
+
return 'Verify `GITHUB_TOKEN` and repo access to `/repos/<owner>/<repo>/pulls`, plus `Pull requests: Read/Write` and `Contents: Read/Write` scopes.';
|
|
1283
|
+
}
|
|
1284
|
+
if (checkName === 'connection:asc_cli') {
|
|
1285
|
+
return 'ASC setup should list App Store Connect apps and persist the selected app automatically. Rerun the connector wizard; if this repeats, update the skill/CLI rather than setting ASC_APP_ID by hand.';
|
|
1286
|
+
}
|
|
1287
|
+
return 'Fix this blocker and rerun start.';
|
|
1288
|
+
}
|
|
1289
|
+
function isInvalidAscPrivateKeyError(error) {
|
|
1290
|
+
return /invalid private key|failed to parse|asn1|sequence truncated|malformed/i.test(String(error || ''));
|
|
1291
|
+
}
|
|
1292
|
+
function describeAscAppSetupFailure(error) {
|
|
1293
|
+
if (isInvalidAscPrivateKeyError(error)) {
|
|
1294
|
+
return 'Stored ASC .p8 private key is invalid or truncated. The connector wizard must reject this before saving; rerun the updated wizard and paste the full .p8 file content from BEGIN PRIVATE KEY to END PRIVATE KEY.';
|
|
1295
|
+
}
|
|
1296
|
+
return `Could not list App Store Connect apps (${error || 'unknown error'})`;
|
|
1297
|
+
}
|
|
1298
|
+
function remediateAscAppSetupFailure(error) {
|
|
1299
|
+
if (isInvalidAscPrivateKeyError(error)) {
|
|
1300
|
+
return 'Rerun the updated connector wizard and paste the full downloaded .p8 file content. The wizard validates it before saving ASC_PRIVATE_KEY_PATH.';
|
|
1301
|
+
}
|
|
1302
|
+
return 'Verify ASC credentials, key role access, and `asc apps list --output json`.';
|
|
1303
|
+
}
|
|
1304
|
+
async function runPreflight(configPath, testConnections, progressJson = false, onlyConnectors = []) {
|
|
1305
|
+
const commandParts = [
|
|
1306
|
+
'node',
|
|
1307
|
+
'scripts/openclaw-growth-preflight.mjs',
|
|
1308
|
+
'--config',
|
|
1309
|
+
quote(configPath),
|
|
1310
|
+
];
|
|
1311
|
+
if (testConnections) {
|
|
1312
|
+
commandParts.push('--test-connections');
|
|
1313
|
+
}
|
|
1314
|
+
if (onlyConnectors.length > 0) {
|
|
1315
|
+
commandParts.push('--only-connectors', quote(onlyConnectors.join(',')));
|
|
1316
|
+
}
|
|
1317
|
+
if (progressJson) {
|
|
1318
|
+
commandParts.push('--progress-json');
|
|
1319
|
+
}
|
|
1320
|
+
const command = commandParts.join(' ');
|
|
1321
|
+
const result = await runShellCommand(command, 180_000, {
|
|
1322
|
+
onStderrLine: progressJson
|
|
1323
|
+
? (line) => {
|
|
1324
|
+
if (line.startsWith('OPENCLAW_PROGRESS ')) {
|
|
1325
|
+
process.stderr.write(`${line}\n`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
: undefined,
|
|
1329
|
+
});
|
|
1330
|
+
const payload = parseJsonFromStdout(result.stdout);
|
|
1331
|
+
return {
|
|
1332
|
+
shell: result,
|
|
1333
|
+
payload,
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
async function runFirstPass(configPath) {
|
|
1337
|
+
const command = `node scripts/openclaw-growth-runner.mjs --config ${quote(configPath)}`;
|
|
1338
|
+
return runShellCommand(command, 300_000);
|
|
1339
|
+
}
|
|
1340
|
+
async function main() {
|
|
1341
|
+
await loadOpenClawGrowthSecrets();
|
|
1342
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1343
|
+
const configPath = path.resolve(args.config);
|
|
1344
|
+
const configResult = await ensureConfig(configPath);
|
|
1345
|
+
const initialConfig = await readJson(configPath);
|
|
1346
|
+
await applyOpenClawSecretRefs(initialConfig);
|
|
1347
|
+
const heartbeat = await ensureGrowthHeartbeat(configPath, initialConfig);
|
|
1348
|
+
const projectConfigured = await configureAnalyticsProject(configPath, args.project);
|
|
1349
|
+
const ascAppConfiguredFromArg = await configureAscApp(configPath, args.ascApp);
|
|
1350
|
+
const analyticscliEnsure = await ensureAnalyticsCliInstalled();
|
|
1351
|
+
if (!analyticscliEnsure.ok) {
|
|
1352
|
+
process.stdout.write(`${JSON.stringify({
|
|
1353
|
+
ok: false,
|
|
1354
|
+
phase: 'dependency_setup',
|
|
1355
|
+
configCreated: configResult.created,
|
|
1356
|
+
configPath,
|
|
1357
|
+
heartbeat,
|
|
1358
|
+
projectConfigured,
|
|
1359
|
+
ascAppConfigured: ascAppConfiguredFromArg,
|
|
1360
|
+
blockers: [
|
|
1361
|
+
{
|
|
1362
|
+
check: 'dependency:analyticscli',
|
|
1363
|
+
detail: analyticscliEnsure.detail,
|
|
1364
|
+
remediation: `Install the npm package with \`npm install -g ${ANALYTICSCLI_PACKAGE_SPEC}\` or set ANALYTICSCLI_NPM_PREFIX to a writable prefix.`,
|
|
1365
|
+
},
|
|
1366
|
+
],
|
|
1367
|
+
}, null, 2)}\n`);
|
|
1368
|
+
process.exitCode = 1;
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (args.connectors.length > 0) {
|
|
1372
|
+
emitProgress(args.progressJson, {
|
|
1373
|
+
phase: 'start',
|
|
1374
|
+
key: 'connectorSetup',
|
|
1375
|
+
label: 'Connector helpers',
|
|
1376
|
+
detail: 'installing and enabling selected helpers',
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
const connectorSetup = args.connectors.length > 0 ? await installConnectorHelpers(configPath, args.connectors) : [];
|
|
1380
|
+
const failedConnectors = connectorSetup.filter((entry) => !entry.ok);
|
|
1381
|
+
if (args.connectors.length > 0) {
|
|
1382
|
+
emitProgress(args.progressJson, {
|
|
1383
|
+
phase: 'finish',
|
|
1384
|
+
key: 'connectorSetup',
|
|
1385
|
+
label: 'Connector helpers',
|
|
1386
|
+
detail: failedConnectors.length > 0
|
|
1387
|
+
? `${failedConnectors.length} helper setup step(s) need attention`
|
|
1388
|
+
: 'selected helpers enabled',
|
|
1389
|
+
status: failedConnectors.length > 0 ? 'fail' : 'pass',
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (failedConnectors.length > 0 && !args.run) {
|
|
1393
|
+
process.stdout.write(`${JSON.stringify({
|
|
1394
|
+
ok: false,
|
|
1395
|
+
phase: 'connector_setup',
|
|
1396
|
+
configCreated: configResult.created,
|
|
1397
|
+
configPath,
|
|
1398
|
+
heartbeat,
|
|
1399
|
+
projectConfigured,
|
|
1400
|
+
ascAppConfigured: ascAppConfiguredFromArg,
|
|
1401
|
+
connectorSetup,
|
|
1402
|
+
blockers: failedConnectors.map((entry) => ({
|
|
1403
|
+
check: `connector:${entry.connector}`,
|
|
1404
|
+
detail: entry.detail,
|
|
1405
|
+
remediation: entry.connector === 'analytics'
|
|
1406
|
+
? 'Paste a fresh AnalyticsCLI readonly token into the connector wizard so it can store ANALYTICSCLI_ACCESS_TOKEN.'
|
|
1407
|
+
: entry.connector === 'github'
|
|
1408
|
+
? 'Provide a GitHub token through the connector wizard for code access.'
|
|
1409
|
+
: entry.connector === 'asc'
|
|
1410
|
+
? '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
|
+
: entry.connector === 'sentry'
|
|
1412
|
+
? '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
|
+
: 'Set REVENUECAT_API_KEY and rerun connector setup to write RevenueCat MCP config.',
|
|
1414
|
+
})),
|
|
1415
|
+
}, null, 2)}\n`);
|
|
1416
|
+
process.exitCode = 1;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
emitProgress(args.progressJson, {
|
|
1420
|
+
phase: 'start',
|
|
1421
|
+
key: 'analyticsProject',
|
|
1422
|
+
label: 'AnalyticsCLI scope',
|
|
1423
|
+
detail: 'checking accessible analytics projects',
|
|
1424
|
+
});
|
|
1425
|
+
const analyticsProjectSetup = await ensureAnalyticsProjectConfigured(configPath, args.project);
|
|
1426
|
+
emitProgress(args.progressJson, {
|
|
1427
|
+
phase: 'finish',
|
|
1428
|
+
key: 'analyticsProject',
|
|
1429
|
+
label: 'AnalyticsCLI scope',
|
|
1430
|
+
detail: analyticsProjectSetup.ok
|
|
1431
|
+
? analyticsProjectSetup.projectId
|
|
1432
|
+
? `using ${analyticsProjectSetup.projectId}`
|
|
1433
|
+
: analyticsProjectSetup.projectScope === 'all_accessible_projects'
|
|
1434
|
+
? `using all accessible projects (${analyticsProjectSetup.projectCount ?? 'unknown'} found)`
|
|
1435
|
+
: 'no project pin needed'
|
|
1436
|
+
: 'analytics scope check failed',
|
|
1437
|
+
status: analyticsProjectSetup.ok ? 'pass' : 'fail',
|
|
1438
|
+
});
|
|
1439
|
+
emitProgress(args.progressJson, {
|
|
1440
|
+
phase: 'start',
|
|
1441
|
+
key: 'ascApp',
|
|
1442
|
+
label: 'ASC app scope',
|
|
1443
|
+
detail: 'resolving App Store Connect app scope',
|
|
1444
|
+
});
|
|
1445
|
+
const ascAppSetup = await ensureAscAppConfigured(configPath, args.ascApp);
|
|
1446
|
+
emitProgress(args.progressJson, {
|
|
1447
|
+
phase: 'finish',
|
|
1448
|
+
key: 'ascApp',
|
|
1449
|
+
label: 'ASC app scope',
|
|
1450
|
+
detail: ascAppSetup.ok
|
|
1451
|
+
? ascAppSetup.appId
|
|
1452
|
+
? `using app ${ascAppSetup.appId}`
|
|
1453
|
+
: ascAppSetup.appScope === 'all_accessible_apps'
|
|
1454
|
+
? `using all accessible apps (${ascAppSetup.appCount || 0} found)`
|
|
1455
|
+
: 'not enabled'
|
|
1456
|
+
: describeAscAppSetupFailure(ascAppSetup.error),
|
|
1457
|
+
status: ascAppSetup.ok ? 'pass' : 'fail',
|
|
1458
|
+
});
|
|
1459
|
+
if (!ascAppSetup.ok) {
|
|
1460
|
+
process.stdout.write(`${JSON.stringify({
|
|
1461
|
+
ok: false,
|
|
1462
|
+
phase: 'asc_app_setup',
|
|
1463
|
+
configCreated: configResult.created,
|
|
1464
|
+
configPath,
|
|
1465
|
+
heartbeat,
|
|
1466
|
+
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1467
|
+
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1468
|
+
ascAppConfigured: false,
|
|
1469
|
+
connectorSetup,
|
|
1470
|
+
needsUserInput: false,
|
|
1471
|
+
question: null,
|
|
1472
|
+
apps: [],
|
|
1473
|
+
nextCommand: null,
|
|
1474
|
+
blockers: [
|
|
1475
|
+
{
|
|
1476
|
+
check: 'connection:asc_app',
|
|
1477
|
+
detail: describeAscAppSetupFailure(ascAppSetup.error),
|
|
1478
|
+
remediation: remediateAscAppSetupFailure(ascAppSetup.error),
|
|
1479
|
+
},
|
|
1480
|
+
],
|
|
1481
|
+
}, null, 2)}\n`);
|
|
1482
|
+
process.exitCode = 1;
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const preflightResult = await runPreflight(configPath, args.testConnections, args.progressJson, args.onlyConnectors);
|
|
1486
|
+
const preflightPayload = preflightResult.payload;
|
|
1487
|
+
if (!preflightPayload) {
|
|
1488
|
+
throw new Error(`Preflight returned invalid output.\nstdout:\n${preflightResult.shell.stdout}\nstderr:\n${preflightResult.shell.stderr}`);
|
|
1489
|
+
}
|
|
1490
|
+
const failures = Array.isArray(preflightPayload.checks)
|
|
1491
|
+
? preflightPayload.checks.filter((check) => check.status === 'fail')
|
|
1492
|
+
: [];
|
|
1493
|
+
if (failures.length > 0) {
|
|
1494
|
+
const blockers = failures.map((check) => ({
|
|
1495
|
+
check: check.name,
|
|
1496
|
+
detail: check.detail,
|
|
1497
|
+
remediation: remediationForCheck(check.name, configPath),
|
|
1498
|
+
}));
|
|
1499
|
+
process.stdout.write(`${JSON.stringify({
|
|
1500
|
+
ok: false,
|
|
1501
|
+
phase: 'preflight',
|
|
1502
|
+
configCreated: configResult.created,
|
|
1503
|
+
configPath,
|
|
1504
|
+
heartbeat,
|
|
1505
|
+
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1506
|
+
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1507
|
+
ascAppConfigured: ascAppSetup.configured,
|
|
1508
|
+
ascAppId: ascAppSetup.appId || null,
|
|
1509
|
+
ascAppScope: ascAppSetup.appScope || null,
|
|
1510
|
+
githubRepo: configResult.githubRepo,
|
|
1511
|
+
connectorSetup,
|
|
1512
|
+
checks: preflightPayload.checks || [],
|
|
1513
|
+
blockers,
|
|
1514
|
+
}, null, 2)}\n`);
|
|
1515
|
+
process.exitCode = 1;
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (!args.run) {
|
|
1519
|
+
process.stdout.write(`${JSON.stringify({
|
|
1520
|
+
ok: true,
|
|
1521
|
+
phase: 'setup_complete',
|
|
1522
|
+
configCreated: configResult.created,
|
|
1523
|
+
configPath,
|
|
1524
|
+
heartbeat,
|
|
1525
|
+
projectConfigured: projectConfigured || analyticsProjectSetup.configured,
|
|
1526
|
+
analyticsProjectId: analyticsProjectSetup.projectId || null,
|
|
1527
|
+
ascAppConfigured: ascAppSetup.configured,
|
|
1528
|
+
ascAppId: ascAppSetup.appId || null,
|
|
1529
|
+
ascAppScope: ascAppSetup.appScope || null,
|
|
1530
|
+
connectorSetup,
|
|
1531
|
+
message: 'Preflight passed. First run skipped due to --setup-only.',
|
|
1532
|
+
}, null, 2)}\n`);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const runResult = await runFirstPass(configPath);
|
|
1536
|
+
if (!runResult.ok) {
|
|
1537
|
+
const rawError = runResult.stderr || `exit ${runResult.code}`;
|
|
1538
|
+
if (isMissingProjectSelection(rawError)) {
|
|
1539
|
+
process.stdout.write(`${JSON.stringify(await buildProjectSelectionResponse({
|
|
1540
|
+
configCreated: configResult.created,
|
|
1541
|
+
configPath,
|
|
1542
|
+
projectConfigured,
|
|
1543
|
+
rawError,
|
|
1544
|
+
}), null, 2)}\n`);
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
process.stdout.write(`${JSON.stringify({
|
|
1548
|
+
ok: false,
|
|
1549
|
+
phase: 'first_run',
|
|
1550
|
+
configCreated: configResult.created,
|
|
1551
|
+
configPath,
|
|
1552
|
+
heartbeat,
|
|
1553
|
+
projectConfigured,
|
|
1554
|
+
error: rawError,
|
|
1555
|
+
}, null, 2)}\n`);
|
|
1556
|
+
process.exitCode = 1;
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
const actionMode = getActionMode(await readJson(configPath));
|
|
1560
|
+
process.stdout.write(`${JSON.stringify({
|
|
1561
|
+
ok: true,
|
|
1562
|
+
phase: 'first_run_complete',
|
|
1563
|
+
configCreated: configResult.created,
|
|
1564
|
+
configPath,
|
|
1565
|
+
heartbeat,
|
|
1566
|
+
projectConfigured,
|
|
1567
|
+
actionMode,
|
|
1568
|
+
runnerOutput: runResult.stdout.trim(),
|
|
1569
|
+
}, null, 2)}\n`);
|
|
1570
|
+
}
|
|
1571
|
+
main().catch((error) => {
|
|
1572
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1573
|
+
process.exitCode = 1;
|
|
1574
|
+
});
|
|
1575
|
+
//# sourceMappingURL=openclaw-growth-start.mjs.map
|