@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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/dist/config.d.ts +1663 -0
  4. package/dist/config.js +266 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +1188 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/runtime/export-analytics-summary.d.mts +2 -0
  10. package/dist/runtime/export-analytics-summary.mjs +303 -0
  11. package/dist/runtime/export-analytics-summary.mjs.map +1 -0
  12. package/dist/runtime/export-asc-summary.d.mts +2 -0
  13. package/dist/runtime/export-asc-summary.mjs +376 -0
  14. package/dist/runtime/export-asc-summary.mjs.map +1 -0
  15. package/dist/runtime/export-revenuecat-summary.d.mts +2 -0
  16. package/dist/runtime/export-revenuecat-summary.mjs +176 -0
  17. package/dist/runtime/export-revenuecat-summary.mjs.map +1 -0
  18. package/dist/runtime/export-sentry-summary.d.mts +2 -0
  19. package/dist/runtime/export-sentry-summary.mjs +352 -0
  20. package/dist/runtime/export-sentry-summary.mjs.map +1 -0
  21. package/dist/runtime/openclaw-exporters-lib.d.mts +101 -0
  22. package/dist/runtime/openclaw-exporters-lib.mjs +1276 -0
  23. package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -0
  24. package/dist/runtime/openclaw-feedback-api.d.mts +2 -0
  25. package/dist/runtime/openclaw-feedback-api.mjs +255 -0
  26. package/dist/runtime/openclaw-feedback-api.mjs.map +1 -0
  27. package/dist/runtime/openclaw-growth-charts.py +154 -0
  28. package/dist/runtime/openclaw-growth-engineer.d.mts +2 -0
  29. package/dist/runtime/openclaw-growth-engineer.mjs +1258 -0
  30. package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -0
  31. package/dist/runtime/openclaw-growth-env.d.mts +9 -0
  32. package/dist/runtime/openclaw-growth-env.mjs +125 -0
  33. package/dist/runtime/openclaw-growth-env.mjs.map +1 -0
  34. package/dist/runtime/openclaw-growth-preflight.d.mts +2 -0
  35. package/dist/runtime/openclaw-growth-preflight.mjs +1111 -0
  36. package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -0
  37. package/dist/runtime/openclaw-growth-runner.d.mts +2 -0
  38. package/dist/runtime/openclaw-growth-runner.mjs +1302 -0
  39. package/dist/runtime/openclaw-growth-runner.mjs.map +1 -0
  40. package/dist/runtime/openclaw-growth-shared.d.mts +33 -0
  41. package/dist/runtime/openclaw-growth-shared.mjs +208 -0
  42. package/dist/runtime/openclaw-growth-shared.mjs.map +1 -0
  43. package/dist/runtime/openclaw-growth-start.d.mts +2 -0
  44. package/dist/runtime/openclaw-growth-start.mjs +1575 -0
  45. package/dist/runtime/openclaw-growth-start.mjs.map +1 -0
  46. package/dist/runtime/openclaw-growth-status.d.mts +2 -0
  47. package/dist/runtime/openclaw-growth-status.mjs +387 -0
  48. package/dist/runtime/openclaw-growth-status.mjs.map +1 -0
  49. package/dist/runtime/openclaw-growth-wizard.d.mts +2 -0
  50. package/dist/runtime/openclaw-growth-wizard.mjs +3519 -0
  51. package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -0
  52. package/dist/shell.d.ts +17 -0
  53. package/dist/shell.js +40 -0
  54. package/dist/shell.js.map +1 -0
  55. package/package.json +38 -0
  56. package/templates/analytics_summary.example.json +40 -0
  57. package/templates/config.example.json +197 -0
  58. package/templates/feedback_summary.example.json +37 -0
  59. package/templates/revenuecat_summary.example.json +25 -0
  60. package/templates/sentry_summary.example.json +23 -0
package/dist/index.js ADDED
@@ -0,0 +1,1188 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { basename, delimiter, dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { Command } from 'commander';
8
+ import { fileExists, parseOpenClawConfig, readJsonFile, readOpenClawConfig, toLegacyGrowthConfig, writeJsonFile, writeOpenClawConfig, } from './config.js';
9
+ import { isCommandAvailable, runCommand, runCommandInherited } from './shell.js';
10
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
11
+ const packageRoot = resolve(moduleDir, '..');
12
+ const localAnalyticsCliDir = resolve(packageRoot, '../cli');
13
+ const analyticsCliPackageSpec = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
14
+ const analyticsCliNpmPrefix = process.env.ANALYTICSCLI_NPM_PREFIX ||
15
+ (process.env.HOME ? join(process.env.HOME, '.local') : resolve(process.cwd(), '.analyticscli-npm'));
16
+ const program = new Command();
17
+ const shellQuote = (value) => {
18
+ if (/^[a-zA-Z0-9_./:@-]+$/.test(value)) {
19
+ return value;
20
+ }
21
+ return `'${value.replace(/'/g, `'\\''`)}'`;
22
+ };
23
+ const truncate = (value, max = 240) => value.length <= max ? value : `${value.slice(0, max)}...`;
24
+ const resolveCommandPath = (command) => {
25
+ const result = runCommand('sh', ['-c', `command -v ${shellQuote(command)}`], {
26
+ timeoutMs: 10_000,
27
+ });
28
+ return result.ok ? result.stdout.trim() : null;
29
+ };
30
+ const commandExists = (command) => resolveCommandPath(command) !== null;
31
+ const appendDetail = (details, label, result) => {
32
+ if (result.ok) {
33
+ details.push(`${label}: ok`);
34
+ return;
35
+ }
36
+ const output = truncate(`${result.stderr}\n${result.stdout}`.trim() || `exit code ${result.code ?? 'unknown'}`);
37
+ details.push(`${label}: ${result.timedOut ? 'timed out' : output}`);
38
+ };
39
+ const isPermissionFailure = (output) => /EACCES|permission denied|access denied|operation not permitted/i.test(output);
40
+ const isClawHubSuspiciousSkillFailure = (output) => /Use --force to install suspicious skills in non-interactive mode|Already installed: .*use --force/i.test(output);
41
+ const prependToPath = (binDir) => {
42
+ process.env.PATH = `${binDir}${delimiter}${process.env.PATH || ''}`;
43
+ };
44
+ const getPathProfileEntries = (binDir) => {
45
+ const entries = [binDir];
46
+ if (process.env.HOME && resolve(binDir) === resolve(process.env.HOME, '.local', 'bin')) {
47
+ entries.push(join(process.env.HOME, '.local', 'analyticscli-npm', 'bin'));
48
+ }
49
+ return entries;
50
+ };
51
+ const renderProfilePathEntries = (binDir) => getPathProfileEntries(binDir)
52
+ .map((entry) => {
53
+ const home = process.env.HOME ? resolve(process.env.HOME) : null;
54
+ const resolved = resolve(entry);
55
+ if (home && (resolved === home || resolved.startsWith(`${home}/`))) {
56
+ return `$HOME/${resolved.slice(home.length + 1)}`;
57
+ }
58
+ return entry;
59
+ })
60
+ .join(':');
61
+ const ensureProfilePath = (binDir) => {
62
+ if (process.env.ANALYTICSCLI_SKIP_PROFILE_UPDATE === 'true' || !process.env.HOME) {
63
+ return false;
64
+ }
65
+ const line = `export PATH="${renderProfilePathEntries(binDir)}:$PATH"`;
66
+ const profiles = ['.profile', '.bashrc', '.bash_profile', '.zshrc', '.zprofile'].map((name) => join(process.env.HOME, name));
67
+ let wrote = false;
68
+ for (const profile of profiles) {
69
+ let current = '';
70
+ if (existsSync(profile)) {
71
+ current = readFileSync(profile, 'utf8');
72
+ }
73
+ if (!current.includes(line)) {
74
+ appendFileSync(profile, `\n# AnalyticsCLI CLI user-local npm bin\n${line}\n`, 'utf8');
75
+ wrote = true;
76
+ }
77
+ }
78
+ return wrote;
79
+ };
80
+ const verifyFreshShellProfile = () => {
81
+ if (!process.env.HOME) {
82
+ return false;
83
+ }
84
+ const cleanPath = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
85
+ const probes = [
86
+ {
87
+ shell: '/bin/bash',
88
+ 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',
89
+ },
90
+ {
91
+ shell: '/usr/bin/bash',
92
+ 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',
93
+ },
94
+ {
95
+ shell: '/bin/zsh',
96
+ 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',
97
+ },
98
+ {
99
+ shell: '/usr/bin/zsh',
100
+ 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',
101
+ },
102
+ {
103
+ shell: '/bin/sh',
104
+ command: '[ -f "$HOME/.profile" ] && . "$HOME/.profile" >/dev/null 2>&1 || true; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
105
+ },
106
+ {
107
+ shell: '/usr/bin/sh',
108
+ command: '[ -f "$HOME/.profile" ] && . "$HOME/.profile" >/dev/null 2>&1 || true; command -v analyticscli >/dev/null 2>&1 && analyticscli --help >/dev/null 2>&1',
109
+ },
110
+ ];
111
+ return probes.some((probe) => {
112
+ if (!existsSync(probe.shell)) {
113
+ return false;
114
+ }
115
+ const result = runCommand('sh', [
116
+ '-c',
117
+ `env HOME=${shellQuote(process.env.HOME)} PATH=${shellQuote(cleanPath)} ${shellQuote(probe.shell)} -lc ${shellQuote(probe.command)}`,
118
+ ], { timeoutMs: 30_000 });
119
+ return result.ok;
120
+ });
121
+ };
122
+ const isUserLocalBin = (binDir) => {
123
+ if (!process.env.HOME) {
124
+ return false;
125
+ }
126
+ const home = resolve(process.env.HOME);
127
+ const resolved = resolve(binDir);
128
+ return resolved === home || resolved.startsWith(`${home}/`);
129
+ };
130
+ const ensureAnalyticsCliPackage = () => {
131
+ const beforePath = resolveCommandPath('analyticscli');
132
+ if (!isCommandAvailable('npm')) {
133
+ return beforePath
134
+ ? {
135
+ ok: true,
136
+ detail: `analyticscli binary found at ${beforePath}; npm unavailable, so package update was skipped`,
137
+ }
138
+ : {
139
+ ok: false,
140
+ detail: `analyticscli binary missing and npm is unavailable; install ${analyticsCliPackageSpec}`,
141
+ };
142
+ }
143
+ const globalInstall = runCommand('npm', ['install', '-g', analyticsCliPackageSpec], {
144
+ timeoutMs: 180_000,
145
+ });
146
+ if (!globalInstall.ok) {
147
+ const installOutput = `${globalInstall.stderr}\n${globalInstall.stdout}`;
148
+ if (isPermissionFailure(installOutput)) {
149
+ mkdirSync(analyticsCliNpmPrefix, { recursive: true });
150
+ const localInstall = runCommand('npm', ['install', '-g', '--prefix', analyticsCliNpmPrefix, analyticsCliPackageSpec], {
151
+ timeoutMs: 180_000,
152
+ });
153
+ if (!localInstall.ok) {
154
+ return beforePath
155
+ ? {
156
+ ok: true,
157
+ detail: `analyticscli binary found at ${beforePath}; update failed globally and in user-local prefix (${truncate(localInstall.stderr || localInstall.stdout)})`,
158
+ }
159
+ : {
160
+ ok: false,
161
+ detail: `npm install failed globally and in user-local prefix ${analyticsCliNpmPrefix}: ${truncate(localInstall.stderr || localInstall.stdout)}`,
162
+ };
163
+ }
164
+ const localBinDir = join(analyticsCliNpmPrefix, 'bin');
165
+ prependToPath(localBinDir);
166
+ ensureProfilePath(localBinDir);
167
+ }
168
+ else {
169
+ return beforePath
170
+ ? {
171
+ ok: true,
172
+ detail: `analyticscli binary found at ${beforePath}; package update failed (${truncate(installOutput)})`,
173
+ }
174
+ : {
175
+ ok: false,
176
+ detail: `npm install -g ${analyticsCliPackageSpec} failed: ${truncate(installOutput)}`,
177
+ };
178
+ }
179
+ }
180
+ const afterPath = resolveCommandPath('analyticscli');
181
+ if (afterPath) {
182
+ const helpCheck = runCommand('sh', ['-c', 'analyticscli --help >/dev/null 2>&1'], {
183
+ timeoutMs: 30_000,
184
+ });
185
+ if (!helpCheck.ok) {
186
+ return {
187
+ ok: false,
188
+ detail: `analyticscli binary found at ${afterPath}, but --help failed: ${truncate(helpCheck.stderr || helpCheck.stdout)}`,
189
+ };
190
+ }
191
+ const binDir = dirname(afterPath);
192
+ if (isUserLocalBin(binDir)) {
193
+ ensureProfilePath(binDir);
194
+ if (!verifyFreshShellProfile()) {
195
+ return {
196
+ ok: false,
197
+ detail: `analyticscli works at ${afterPath}, but a fresh shell still cannot resolve it after profile update; add ${renderProfilePathEntries(binDir)} to PATH`,
198
+ };
199
+ }
200
+ return {
201
+ ok: true,
202
+ detail: `analyticscli package ensured via ${analyticsCliPackageSpec}; binary found at ${afterPath}; shell profiles updated and fresh shell verification passed`,
203
+ };
204
+ }
205
+ }
206
+ return afterPath
207
+ ? {
208
+ ok: true,
209
+ detail: `analyticscli package ensured via ${analyticsCliPackageSpec}; binary found at ${afterPath}`,
210
+ }
211
+ : {
212
+ ok: false,
213
+ detail: `Installed ${analyticsCliPackageSpec}, but analyticscli is still not on PATH`,
214
+ };
215
+ };
216
+ const resolveRuntimeInvocation = (scriptName) => {
217
+ const sourcePath = resolve(packageRoot, 'src', 'runtime', `${scriptName}.mts`);
218
+ if (existsSync(sourcePath)) {
219
+ return {
220
+ command: 'tsx',
221
+ args: [sourcePath],
222
+ };
223
+ }
224
+ const distPath = resolve(packageRoot, 'dist', 'runtime', `${scriptName}.mjs`);
225
+ if (existsSync(distPath)) {
226
+ return {
227
+ command: 'node',
228
+ args: [distPath],
229
+ };
230
+ }
231
+ throw new Error(`Runtime script not found: ${scriptName}`);
232
+ };
233
+ const runRuntime = (scriptName, args, options) => {
234
+ const runtime = resolveRuntimeInvocation(scriptName);
235
+ return runCommand(runtime.command, [...runtime.args, ...args], options);
236
+ };
237
+ const runRuntimeInteractive = (scriptName, args, options) => {
238
+ const runtime = resolveRuntimeInvocation(scriptName);
239
+ return runCommandInherited(runtime.command, [...runtime.args, ...args], options);
240
+ };
241
+ const resolveTemplatePath = () => resolve(packageRoot, 'templates', 'config.example.json');
242
+ const forwardedArgsAfter = (needle) => {
243
+ const index = process.argv.indexOf(needle);
244
+ return index >= 0 ? process.argv.slice(index + 1) : [];
245
+ };
246
+ const parseGitHubRepoFromRemote = (remoteUrl) => {
247
+ const value = remoteUrl.trim();
248
+ if (!value) {
249
+ return null;
250
+ }
251
+ const sshMatch = value.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/i);
252
+ if (sshMatch?.[1]) {
253
+ return sshMatch[1];
254
+ }
255
+ const httpsMatch = value.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/i);
256
+ if (httpsMatch?.[1]) {
257
+ return httpsMatch[1];
258
+ }
259
+ return null;
260
+ };
261
+ const detectGitHubRepo = (cwd) => {
262
+ const result = runCommand('git', ['config', '--get', 'remote.origin.url'], {
263
+ cwd,
264
+ timeoutMs: 10_000,
265
+ });
266
+ if (!result.ok) {
267
+ return null;
268
+ }
269
+ return parseGitHubRepoFromRemote(result.stdout.trim());
270
+ };
271
+ const resolveDefaultSourceCommand = (repoRoot, command) => {
272
+ if (command === 'analytics') {
273
+ const repoScript = resolve(repoRoot, 'scripts', 'export-analytics-summary.mjs');
274
+ return fileExists(repoScript)
275
+ ? 'node scripts/export-analytics-summary.mjs'
276
+ : 'openclaw exporters analytics-summary';
277
+ }
278
+ if (command === 'feedback') {
279
+ return 'analyticscli feedback summary --format json';
280
+ }
281
+ const repoScript = resolve(repoRoot, 'scripts', 'export-asc-summary.mjs');
282
+ return fileExists(repoScript) ? 'node scripts/export-asc-summary.mjs' : 'openclaw exporters asc-summary';
283
+ };
284
+ const resolvePaths = (configPath) => {
285
+ const baseDir = dirname(resolve(configPath));
286
+ const openclawDir = resolve(baseDir, '.openclaw');
287
+ const runtimeDir = resolve(openclawDir, 'runtime');
288
+ return {
289
+ baseDir,
290
+ openclawDir,
291
+ runtimeDir,
292
+ legacyConfigPath: resolve(runtimeDir, 'legacy-config.json'),
293
+ legacyStatePath: resolve(runtimeDir, 'legacy-state.json'),
294
+ orchestratorStatePath: resolve(openclawDir, 'state.json'),
295
+ };
296
+ };
297
+ const loadTemplateConfig = async () => {
298
+ const raw = await readFile(resolveTemplatePath(), 'utf8');
299
+ return parseOpenClawConfig(JSON.parse(raw));
300
+ };
301
+ const createInitialConfig = async (configPath, repoRoot, force) => {
302
+ const targetPath = resolve(configPath);
303
+ if (!force && fileExists(targetPath)) {
304
+ throw Object.assign(new Error(`Config already exists: ${targetPath}`), { exitCode: 2 });
305
+ }
306
+ const template = await loadTemplateConfig();
307
+ const resolvedRepoRoot = resolve(repoRoot);
308
+ const configBaseDir = dirname(targetPath);
309
+ const repoRootForConfig = resolve(configBaseDir) === resolvedRepoRoot ? '.' : resolvedRepoRoot;
310
+ const githubRepo = detectGitHubRepo(repoRoot) ?? template.project.githubRepo;
311
+ const nextConfig = {
312
+ ...template,
313
+ version: 7,
314
+ generatedAt: new Date().toISOString(),
315
+ project: {
316
+ ...template.project,
317
+ githubRepo,
318
+ repoRoot: repoRootForConfig,
319
+ outFile: 'data/openclaw-growth-engineer/issues.generated.json',
320
+ },
321
+ sources: {
322
+ ...template.sources,
323
+ analytics: {
324
+ ...template.sources.analytics,
325
+ enabled: true,
326
+ mode: 'command',
327
+ command: resolveDefaultSourceCommand(repoRoot, 'analytics'),
328
+ },
329
+ revenuecat: {
330
+ ...template.sources.revenuecat,
331
+ enabled: false,
332
+ },
333
+ sentry: {
334
+ ...template.sources.sentry,
335
+ enabled: false,
336
+ },
337
+ feedback: {
338
+ ...template.sources.feedback,
339
+ enabled: true,
340
+ mode: 'command',
341
+ command: resolveDefaultSourceCommand(repoRoot, 'feedback'),
342
+ cursorMode: 'auto_since_last_fetch',
343
+ initialLookback: '30d',
344
+ },
345
+ extra: (template.sources.extra ?? []).map((source) => {
346
+ if (source.service === 'asc-cli') {
347
+ return {
348
+ ...source,
349
+ command: resolveDefaultSourceCommand(repoRoot, 'asc'),
350
+ };
351
+ }
352
+ return source;
353
+ }),
354
+ },
355
+ strategy: template.strategy,
356
+ deliveries: {
357
+ openclawChat: {
358
+ enabled: true,
359
+ markdownPath: '.openclaw/chat/latest.md',
360
+ jsonPath: '.openclaw/chat/latest.json',
361
+ },
362
+ github: {
363
+ enabled: false,
364
+ mode: 'issue',
365
+ autoCreate: false,
366
+ draftPullRequests: true,
367
+ proposalBranchPrefix: 'openclaw/proposals',
368
+ },
369
+ slack: {
370
+ enabled: false,
371
+ webhookEnv: 'SLACK_WEBHOOK_URL',
372
+ },
373
+ webhook: {
374
+ enabled: false,
375
+ urlEnv: 'OPENCLAW_WEBHOOK_URL',
376
+ method: 'POST',
377
+ headers: {},
378
+ },
379
+ },
380
+ };
381
+ await writeOpenClawConfig(targetPath, nextConfig);
382
+ };
383
+ const runSharedAnalyticsSetup = () => {
384
+ const sharedArgs = ['setup', '--skip-login', '--agents', 'all', '--no-auto-skill-update'];
385
+ const cliInstall = ensureAnalyticsCliPackage();
386
+ if (!cliInstall.ok) {
387
+ return cliInstall;
388
+ }
389
+ const interpretSetupResult = (result, fallbackDetail) => {
390
+ const detail = result.stderr.trim() || result.stdout.trim() || fallbackDetail;
391
+ if (!result.ok) {
392
+ return {
393
+ ok: false,
394
+ detail,
395
+ };
396
+ }
397
+ const jsonStart = result.stdout.indexOf('{');
398
+ if (jsonStart >= 0) {
399
+ try {
400
+ const payload = JSON.parse(result.stdout.slice(jsonStart));
401
+ const failedSkills = (payload.skillSetup ?? []).filter((entry) => entry.ok === false);
402
+ if (payload.ok === false || failedSkills.length > 0) {
403
+ return {
404
+ ok: false,
405
+ detail: failedSkills.length
406
+ ? `AnalyticsCLI setup reported failed skill setup: ${failedSkills
407
+ .map((entry) => `${entry.target ?? 'unknown'}: ${entry.detail ?? 'failed'}`)
408
+ .join('; ')}`
409
+ : detail,
410
+ };
411
+ }
412
+ }
413
+ catch {
414
+ // Keep the command result as the source of truth when stdout is not JSON.
415
+ }
416
+ }
417
+ return {
418
+ ok: true,
419
+ detail,
420
+ };
421
+ };
422
+ if (fileExists(resolve(localAnalyticsCliDir, 'src/index.ts')) && isCommandAvailable('pnpm')) {
423
+ const result = runCommand('pnpm', ['--filter', '@analyticscli/cli', 'dev', ...sharedArgs], {
424
+ cwd: resolve(packageRoot, '../..'),
425
+ timeoutMs: 10 * 60_000,
426
+ });
427
+ return interpretSetupResult(result, 'analyticscli local setup finished');
428
+ }
429
+ if (isCommandAvailable('analyticscli')) {
430
+ const result = runCommand('analyticscli', sharedArgs, {
431
+ timeoutMs: 10 * 60_000,
432
+ });
433
+ return interpretSetupResult(result, 'analyticscli setup finished');
434
+ }
435
+ const result = runCommand('npx', ['-y', '@analyticscli/cli@preview', ...sharedArgs], {
436
+ timeoutMs: 10 * 60_000,
437
+ });
438
+ return interpretSetupResult(result, 'analyticscli preview setup finished');
439
+ };
440
+ const normalizeConnectorKey = (value) => {
441
+ const normalized = value.trim().toLowerCase().replace(/[_\s]+/g, '-');
442
+ if (!normalized) {
443
+ return null;
444
+ }
445
+ if (normalized === 'all') {
446
+ return 'all';
447
+ }
448
+ if (['github', 'gh', 'github-code', 'codebase', 'code-access'].includes(normalized)) {
449
+ return 'github';
450
+ }
451
+ if (['asc', 'asc-cli', 'app-store-connect', 'appstoreconnect', 'app-store'].includes(normalized)) {
452
+ return 'asc';
453
+ }
454
+ if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized)) {
455
+ return 'revenuecat';
456
+ }
457
+ return null;
458
+ };
459
+ const parseConnectorList = (value) => {
460
+ if (!value?.trim()) {
461
+ return [];
462
+ }
463
+ const connectors = new Set();
464
+ for (const entry of value.split(',')) {
465
+ const connector = normalizeConnectorKey(entry);
466
+ if (!connector) {
467
+ throw Object.assign(new Error(`Unknown connector "${entry.trim()}". Use github, asc, revenuecat, or all.`), { exitCode: 2 });
468
+ }
469
+ if (connector === 'all') {
470
+ connectors.add('github');
471
+ connectors.add('asc');
472
+ connectors.add('revenuecat');
473
+ }
474
+ else {
475
+ connectors.add(connector);
476
+ }
477
+ }
478
+ return [...connectors];
479
+ };
480
+ const installClawHubSkill = (skillName, details) => {
481
+ const invoker = isCommandAvailable('clawhub')
482
+ ? { command: 'clawhub', prefix: [] }
483
+ : isCommandAvailable('npx')
484
+ ? { command: 'npx', prefix: ['-y', 'clawhub'] }
485
+ : null;
486
+ if (!invoker) {
487
+ details.push(`ClawHub skill ${skillName}: skipped because neither clawhub nor npx is available`);
488
+ return false;
489
+ }
490
+ let install = runCommand(invoker.command, [...invoker.prefix, 'install', skillName], {
491
+ timeoutMs: 120_000,
492
+ });
493
+ const installOutput = `${install.stderr}\n${install.stdout}`;
494
+ if (!install.ok && isClawHubSuspiciousSkillFailure(installOutput)) {
495
+ install = runCommand(invoker.command, [...invoker.prefix, 'install', skillName, '--force'], {
496
+ timeoutMs: 120_000,
497
+ });
498
+ }
499
+ appendDetail(details, `ClawHub skill ${skillName}`, install);
500
+ return install.ok;
501
+ };
502
+ const installCodexClaudeSkill = (repo, details) => {
503
+ if (!isCommandAvailable('npx')) {
504
+ details.push(`Agent skill ${repo}: skipped because npx is unavailable`);
505
+ return false;
506
+ }
507
+ const install = runCommand('npx', ['-y', 'skills', 'add', repo], {
508
+ timeoutMs: 180_000,
509
+ });
510
+ appendDetail(details, `Agent skill ${repo}`, install);
511
+ return install.ok;
512
+ };
513
+ const installGitHubConnector = () => {
514
+ const details = [];
515
+ installClawHubSkill('github', details);
516
+ const beforePath = resolveCommandPath('gh');
517
+ if (beforePath) {
518
+ details.push(`gh binary found at ${beforePath}`);
519
+ return {
520
+ connector: 'github',
521
+ ok: true,
522
+ detail: details.join('; '),
523
+ };
524
+ }
525
+ if (commandExists('brew')) {
526
+ const brewInstall = runCommand('brew', ['install', 'gh'], {
527
+ timeoutMs: 10 * 60_000,
528
+ });
529
+ appendDetail(details, 'brew install gh', brewInstall);
530
+ }
531
+ else if (commandExists('winget')) {
532
+ const wingetInstall = runCommand('winget', ['install', '--id', 'GitHub.cli', '-e', '--silent'], {
533
+ timeoutMs: 10 * 60_000,
534
+ });
535
+ appendDetail(details, 'winget install GitHub.cli', wingetInstall);
536
+ }
537
+ else {
538
+ details.push('No supported non-interactive gh installer found; install GitHub CLI via Homebrew, winget, or the official package for this OS');
539
+ }
540
+ const afterPath = resolveCommandPath('gh');
541
+ return {
542
+ connector: 'github',
543
+ ok: Boolean(afterPath),
544
+ detail: afterPath
545
+ ? `${details.join('; ')}; gh binary found at ${afterPath}; next run gh auth status or gh auth login`
546
+ : `${details.join('; ')}; GitHub CLI is still missing`,
547
+ };
548
+ };
549
+ const installAscConnector = () => {
550
+ const details = [];
551
+ installCodexClaudeSkill('rorkai/app-store-connect-cli-skills', details);
552
+ const beforePath = resolveCommandPath('asc');
553
+ if (beforePath) {
554
+ details.push(`asc binary found at ${beforePath}`);
555
+ return {
556
+ connector: 'asc',
557
+ ok: true,
558
+ detail: details.join('; '),
559
+ };
560
+ }
561
+ if (commandExists('brew')) {
562
+ const brewInstall = runCommand('brew', ['install', 'asc'], {
563
+ timeoutMs: 10 * 60_000,
564
+ });
565
+ appendDetail(details, 'brew install asc', brewInstall);
566
+ }
567
+ if (!resolveCommandPath('asc') && commandExists('curl')) {
568
+ const installScript = runCommand('sh', ['-c', 'curl -fsSL https://asccli.sh/install | bash'], {
569
+ timeoutMs: 10 * 60_000,
570
+ });
571
+ appendDetail(details, 'asc install script', installScript);
572
+ }
573
+ const afterPath = resolveCommandPath('asc');
574
+ return {
575
+ connector: 'asc',
576
+ ok: Boolean(afterPath),
577
+ detail: afterPath
578
+ ? `${details.join('; ')}; asc binary found at ${afterPath}; next run asc auth status --validate or asc auth login`
579
+ : `${details.join('; ')}; asc CLI is still missing`,
580
+ };
581
+ };
582
+ const escapeTomlString = (value) => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
583
+ const resolveMcpNpmCacheDir = () => process.env.OPENCLAW_MCP_NPM_CACHE ||
584
+ (process.env.HOME ? join(process.env.HOME, '.cache', 'openclaw-mcp-npm') : resolve(process.cwd(), '.openclaw-mcp-npm-cache'));
585
+ const upsertRevenueCatCodexMcpConfig = (apiKey) => {
586
+ if (!process.env.HOME) {
587
+ return null;
588
+ }
589
+ const configDir = join(process.env.HOME, '.codex');
590
+ const configFile = join(configDir, 'config.toml');
591
+ mkdirSync(configDir, { recursive: true });
592
+ const existing = existsSync(configFile) ? readFileSync(configFile, 'utf8') : '';
593
+ const block = `[mcp_servers.revenuecat]
594
+ command = "npx"
595
+ args = ["--yes", "--cache", "${escapeTomlString(resolveMcpNpmCacheDir())}", "mcp-remote", "https://mcp.revenuecat.ai/mcp", "--header", "Authorization: Bearer \${AUTH_TOKEN}"]
596
+ env = { AUTH_TOKEN = "${escapeTomlString(apiKey)}" }
597
+ type = "stdio"
598
+ startup_timeout_ms = 20000
599
+ `;
600
+ const pattern = /(?:^|\n)\[mcp_servers\.revenuecat\]\n(?:.*\n)*?(?=\n\[|\s*$)/m;
601
+ const next = pattern.test(existing)
602
+ ? existing.replace(pattern, `${existing.startsWith('[mcp_servers.revenuecat]') ? '' : '\n'}${block}`)
603
+ : `${existing.trimEnd()}${existing.trim() ? '\n\n' : ''}${block}`;
604
+ writeFileSync(configFile, `${next.trimEnd()}\n`, 'utf8');
605
+ return configFile;
606
+ };
607
+ const installRevenueCatConnector = () => {
608
+ const details = [];
609
+ if (!isCommandAvailable('npx')) {
610
+ return {
611
+ connector: 'revenuecat',
612
+ ok: false,
613
+ detail: 'npx is required for the RevenueCat MCP transport (mcp-remote), but npx is unavailable',
614
+ };
615
+ }
616
+ const mcpRemoteCheck = runCommand('npx', ['--yes', '--cache', resolveMcpNpmCacheDir(), 'mcp-remote'], {
617
+ timeoutMs: 120_000,
618
+ });
619
+ const mcpRemoteOutput = `${mcpRemoteCheck.stderr}\n${mcpRemoteCheck.stdout}`;
620
+ const mcpRemoteAvailable = mcpRemoteCheck.ok || /Usage: .*mcp-remote|Usage: .*proxy\.ts/i.test(mcpRemoteOutput);
621
+ if (mcpRemoteAvailable) {
622
+ details.push(`RevenueCat MCP transport mcp-remote is available via npx cache ${resolveMcpNpmCacheDir()}`);
623
+ }
624
+ else {
625
+ appendDetail(details, 'npx mcp-remote availability check', mcpRemoteCheck);
626
+ }
627
+ if (!mcpRemoteAvailable) {
628
+ return {
629
+ connector: 'revenuecat',
630
+ ok: false,
631
+ detail: details.join('; '),
632
+ };
633
+ }
634
+ const revenuecatApiKey = process.env.REVENUECAT_API_KEY?.trim();
635
+ if (revenuecatApiKey) {
636
+ const configFile = upsertRevenueCatCodexMcpConfig(revenuecatApiKey);
637
+ details.push(configFile
638
+ ? `RevenueCat MCP configured in ${configFile} using REVENUECAT_API_KEY`
639
+ : 'RevenueCat MCP transport is available; HOME is missing so MCP client config was not written');
640
+ }
641
+ else {
642
+ details.push('RevenueCat MCP transport is available; set REVENUECAT_API_KEY, then rerun this command to write the MCP client config');
643
+ }
644
+ return {
645
+ connector: 'revenuecat',
646
+ ok: true,
647
+ detail: details.join('; '),
648
+ };
649
+ };
650
+ const enableConnectorConfig = async (configPath, connectors, repoRoot) => {
651
+ if (connectors.length === 0 || !fileExists(resolve(configPath))) {
652
+ return;
653
+ }
654
+ const config = await readOpenClawConfig(resolve(configPath));
655
+ const nextConfig = {
656
+ ...config,
657
+ sources: {
658
+ ...config.sources,
659
+ revenuecat: connectors.includes('revenuecat')
660
+ ? {
661
+ ...config.sources.revenuecat,
662
+ enabled: true,
663
+ }
664
+ : config.sources.revenuecat,
665
+ extra: config.sources.extra.map((source) => {
666
+ if (connectors.includes('asc') && source.service === 'asc-cli') {
667
+ return {
668
+ ...source,
669
+ enabled: true,
670
+ mode: 'command',
671
+ command: source.command || resolveDefaultSourceCommand(repoRoot, 'asc'),
672
+ };
673
+ }
674
+ return source;
675
+ }),
676
+ },
677
+ };
678
+ await writeOpenClawConfig(resolve(configPath), nextConfig);
679
+ };
680
+ const installConnectorHelpers = async (configPath, connectors, repoRoot) => {
681
+ await enableConnectorConfig(configPath, connectors, repoRoot);
682
+ return connectors.map((connector) => {
683
+ if (connector === 'github') {
684
+ return installGitHubConnector();
685
+ }
686
+ if (connector === 'asc') {
687
+ return installAscConnector();
688
+ }
689
+ return installRevenueCatConnector();
690
+ });
691
+ };
692
+ const writeLegacyConfig = async (configPath, config) => {
693
+ const paths = resolvePaths(configPath);
694
+ await mkdir(paths.runtimeDir, { recursive: true });
695
+ await writeJsonFile(paths.legacyConfigPath, toLegacyGrowthConfig(resolve(configPath), config));
696
+ return paths;
697
+ };
698
+ const runPreflight = async (configPath, options) => {
699
+ const config = await readOpenClawConfig(resolve(configPath));
700
+ const paths = await writeLegacyConfig(configPath, config);
701
+ const runtimeArgs = ['--config', paths.legacyConfigPath, '--json'];
702
+ if (options.testConnections) {
703
+ runtimeArgs.push('--test-connections');
704
+ }
705
+ const runtime = runRuntime('openclaw-growth-preflight', runtimeArgs, {
706
+ cwd: dirname(resolve(configPath)),
707
+ timeoutMs: 120_000,
708
+ });
709
+ let runtimePayload = {
710
+ ok: false,
711
+ summary: {
712
+ pass: 0,
713
+ warn: 0,
714
+ fail: 1,
715
+ },
716
+ checks: [
717
+ {
718
+ name: 'runtime',
719
+ status: 'fail',
720
+ detail: runtime.stderr.trim() || runtime.stdout.trim() || 'preflight failed',
721
+ },
722
+ ],
723
+ };
724
+ if (runtime.stdout.trim()) {
725
+ runtimePayload = JSON.parse(runtime.stdout);
726
+ }
727
+ const extraChecks = [];
728
+ if (config.deliveries.openclawChat.enabled) {
729
+ extraChecks.push({
730
+ name: 'delivery:openclaw-chat',
731
+ status: 'pass',
732
+ detail: `writes ${config.deliveries.openclawChat.markdownPath} and ${config.deliveries.openclawChat.jsonPath}`,
733
+ });
734
+ }
735
+ if (config.deliveries.slack.enabled) {
736
+ const webhookEnv = config.deliveries.slack.webhookEnv;
737
+ extraChecks.push({
738
+ name: `secret:${webhookEnv}`,
739
+ status: process.env[webhookEnv] ? 'pass' : 'fail',
740
+ detail: process.env[webhookEnv] ? 'set' : 'missing (required for Slack delivery)',
741
+ });
742
+ }
743
+ if (config.deliveries.webhook.enabled) {
744
+ const urlEnv = config.deliveries.webhook.urlEnv;
745
+ extraChecks.push({
746
+ name: `secret:${urlEnv}`,
747
+ status: process.env[urlEnv] ? 'pass' : 'fail',
748
+ detail: process.env[urlEnv] ? 'set' : 'missing (required for webhook delivery)',
749
+ });
750
+ }
751
+ const checks = [...runtimePayload.checks, ...extraChecks];
752
+ const summary = checks.reduce((acc, check) => {
753
+ acc[check.status] += 1;
754
+ return acc;
755
+ }, { pass: 0, warn: 0, fail: 0 });
756
+ return {
757
+ ok: summary.fail === 0,
758
+ summary,
759
+ checks,
760
+ };
761
+ };
762
+ const renderPreflightChecklist = (result) => {
763
+ const blocking = result.checks.filter((check) => check.status === 'fail');
764
+ const warnings = result.checks.filter((check) => check.status === 'warn');
765
+ const lines = [
766
+ `Preflight failed: ${result.summary.pass} pass, ${result.summary.warn} warn, ${result.summary.fail} fail`,
767
+ '',
768
+ ];
769
+ const analyticsFailures = blocking.filter((check) => check.name.startsWith('connection:analytics'));
770
+ const feedbackAuthFailure = blocking.find((check) => check.name === 'connection:feedback' && /UNAUTHORIZED|Authentication required/i.test(check.detail));
771
+ if (analyticsFailures.length > 0 || feedbackAuthFailure) {
772
+ lines.push('Next required input: AnalyticsCLI analytics baseline');
773
+ lines.push('- Why: growth proposals need project analytics and feedback data before the first run can generate useful work.');
774
+ lines.push('- Minimum access: a readonly AnalyticsCLI CLI token with access to the target project, stored via `analyticscli login` or provided as `ANALYTICSCLI_ACCESS_TOKEN` from a secret store.');
775
+ lines.push('- Where: dash.analyticscli.com -> API Keys.');
776
+ lines.push('');
777
+ }
778
+ const remainingBlocking = blocking.filter((check) => !check.name.startsWith('connection:analytics') &&
779
+ !(check.name === 'connection:feedback' && /UNAUTHORIZED|Authentication required/i.test(check.detail)));
780
+ if (remainingBlocking.length > 0) {
781
+ lines.push('Other blockers:');
782
+ for (const check of remainingBlocking) {
783
+ lines.push(`- ${check.name}: ${check.detail}`);
784
+ }
785
+ lines.push('');
786
+ }
787
+ if (warnings.length > 0) {
788
+ lines.push('Warnings:');
789
+ for (const check of warnings.slice(0, 5)) {
790
+ lines.push(`- ${check.name}: ${check.detail}`);
791
+ }
792
+ }
793
+ return `${lines.join('\n').trimEnd()}\n`;
794
+ };
795
+ const computeIssuesFingerprint = (payload) => {
796
+ const normalized = payload.issues
797
+ .map((issue) => `${issue.title}|${issue.priority ?? 'medium'}|${issue.area ?? 'general'}`)
798
+ .sort()
799
+ .join('\n');
800
+ return createHash('sha256').update(normalized).digest('hex');
801
+ };
802
+ const buildSlackText = (payload) => {
803
+ const lines = [
804
+ `OpenClaw generated ${payload.issue_count} proposal(s) for ${payload.repo_root}.`,
805
+ ...payload.issues.slice(0, 5).map((issue, index) => `${index + 1}. ${issue.title}`),
806
+ ];
807
+ return lines.join('\n');
808
+ };
809
+ const buildOpenClawChatMarkdown = (payload) => {
810
+ const sections = [
811
+ '# OpenClaw Proposal Outbox',
812
+ '',
813
+ `Generated: ${payload.generated_at}`,
814
+ `Repo: ${payload.repo_root}`,
815
+ `Proposals: ${payload.issue_count}`,
816
+ '',
817
+ 'Use this file as the chat handoff for OpenClaw. Ask OpenClaw to inspect the generated proposals and either summarize them, create a GitHub issue/PR, or implement one of them.',
818
+ ];
819
+ for (const [index, issue] of payload.issues.entries()) {
820
+ sections.push('');
821
+ sections.push(`## ${index + 1}. ${issue.title}`);
822
+ sections.push(`- Priority: ${issue.priority ?? 'medium'}`);
823
+ sections.push(`- Area: ${issue.area ?? 'general'}`);
824
+ if (issue.source) {
825
+ sections.push(`- Source: ${issue.source}`);
826
+ }
827
+ if (issue.expected_impact) {
828
+ sections.push(`- Expected impact: ${issue.expected_impact}`);
829
+ }
830
+ if (issue.confidence) {
831
+ sections.push(`- Confidence: ${issue.confidence}`);
832
+ }
833
+ if (issue.files?.length) {
834
+ sections.push(`- Candidate files: ${issue.files.map((file) => `\`${file}\``).join(', ')}`);
835
+ }
836
+ sections.push('');
837
+ sections.push(issue.body.trim());
838
+ }
839
+ return `${sections.join('\n')}\n`;
840
+ };
841
+ const writeOpenClawChatOutbox = async (configPath, config, payload, fingerprint) => {
842
+ const baseDir = dirname(resolve(configPath));
843
+ const markdownPath = resolve(baseDir, config.deliveries.openclawChat.markdownPath);
844
+ const jsonPath = resolve(baseDir, config.deliveries.openclawChat.jsonPath);
845
+ await mkdir(dirname(markdownPath), { recursive: true });
846
+ await mkdir(dirname(jsonPath), { recursive: true });
847
+ await writeFile(markdownPath, buildOpenClawChatMarkdown(payload), 'utf8');
848
+ await writeJsonFile(jsonPath, {
849
+ channel: 'openclaw_chat',
850
+ generatedAt: payload.generated_at,
851
+ fingerprint,
852
+ repoRoot: payload.repo_root,
853
+ issueCount: payload.issue_count,
854
+ issues: payload.issues,
855
+ });
856
+ return {
857
+ markdownPath,
858
+ jsonPath,
859
+ };
860
+ };
861
+ const sendSlackMessage = async (config, payload) => {
862
+ const webhookEnv = config.deliveries.slack.webhookEnv;
863
+ const webhookUrl = process.env[webhookEnv];
864
+ if (!webhookUrl) {
865
+ throw new Error(`Missing ${webhookEnv} for Slack delivery.`);
866
+ }
867
+ const response = await fetch(webhookUrl, {
868
+ method: 'POST',
869
+ headers: {
870
+ 'content-type': 'application/json',
871
+ },
872
+ body: JSON.stringify({
873
+ text: buildSlackText(payload),
874
+ username: config.deliveries.slack.username,
875
+ }),
876
+ });
877
+ if (!response.ok) {
878
+ throw new Error(`Slack delivery failed (${response.status})`);
879
+ }
880
+ };
881
+ const sendWebhook = async (config, payload) => {
882
+ const urlEnv = config.deliveries.webhook.urlEnv;
883
+ const webhookUrl = process.env[urlEnv];
884
+ if (!webhookUrl) {
885
+ throw new Error(`Missing ${urlEnv} for webhook delivery.`);
886
+ }
887
+ const response = await fetch(webhookUrl, {
888
+ method: config.deliveries.webhook.method,
889
+ headers: {
890
+ 'content-type': 'application/json',
891
+ ...config.deliveries.webhook.headers,
892
+ },
893
+ body: JSON.stringify({
894
+ generatedAt: payload.generated_at,
895
+ repoRoot: payload.repo_root,
896
+ issues: payload.issues,
897
+ }),
898
+ });
899
+ if (!response.ok) {
900
+ throw new Error(`Webhook delivery failed (${response.status})`);
901
+ }
902
+ };
903
+ const deliverArtifacts = async (configPath, config, payload) => {
904
+ const fingerprint = computeIssuesFingerprint(payload);
905
+ const paths = resolvePaths(configPath);
906
+ const state = fileExists(paths.orchestratorStatePath)
907
+ ? await readJsonFile(paths.orchestratorStatePath)
908
+ : {};
909
+ const deliveryTargets = [];
910
+ if (config.schedule.skipIfIssueSetUnchanged && state.lastDeliveredFingerprint === fingerprint) {
911
+ return {
912
+ delivered: false,
913
+ fingerprint,
914
+ skippedReason: 'issue_set_unchanged',
915
+ deliveryTargets,
916
+ };
917
+ }
918
+ if (config.deliveries.openclawChat.enabled) {
919
+ await writeOpenClawChatOutbox(configPath, config, payload, fingerprint);
920
+ deliveryTargets.push('openclaw_chat');
921
+ }
922
+ if (config.deliveries.slack.enabled) {
923
+ await sendSlackMessage(config, payload);
924
+ deliveryTargets.push('slack');
925
+ }
926
+ if (config.deliveries.webhook.enabled) {
927
+ await sendWebhook(config, payload);
928
+ deliveryTargets.push('webhook');
929
+ }
930
+ await writeJsonFile(paths.orchestratorStatePath, {
931
+ lastDeliveredFingerprint: fingerprint,
932
+ lastDeliveredAt: new Date().toISOString(),
933
+ });
934
+ return {
935
+ delivered: true,
936
+ fingerprint,
937
+ skippedReason: null,
938
+ deliveryTargets,
939
+ };
940
+ };
941
+ const runOnce = async (configPath) => {
942
+ const config = await readOpenClawConfig(resolve(configPath));
943
+ const paths = await writeLegacyConfig(configPath, config);
944
+ const runtime = runRuntime('openclaw-growth-runner', ['--config', paths.legacyConfigPath, '--state', paths.legacyStatePath], {
945
+ cwd: dirname(resolve(configPath)),
946
+ timeoutMs: 20 * 60_000,
947
+ });
948
+ if (!runtime.ok) {
949
+ throw new Error(runtime.stderr.trim() || runtime.stdout.trim() || 'runner failed');
950
+ }
951
+ const issuesPath = resolve(dirname(resolve(configPath)), config.project.outFile);
952
+ const issuesPayload = await readJsonFile(issuesPath);
953
+ const deliveryResult = await deliverArtifacts(configPath, config, issuesPayload);
954
+ return {
955
+ runtimeOutput: runtime.stdout.trim(),
956
+ issuesPath,
957
+ issueCount: issuesPayload.issue_count,
958
+ deliveryResult,
959
+ };
960
+ };
961
+ const sleep = async (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
962
+ const resolvePackageName = () => {
963
+ try {
964
+ const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
965
+ return packageJson.name || null;
966
+ }
967
+ catch {
968
+ return null;
969
+ }
970
+ };
971
+ const cliName = resolvePackageName() === '@analyticscli/growth-engineer' ||
972
+ basename(process.argv[1] || '').startsWith('growth-engineer')
973
+ ? 'growth-engineer'
974
+ : 'openclaw';
975
+ program
976
+ .name(cliName)
977
+ .description(cliName === 'growth-engineer'
978
+ ? 'Growth Engineer CLI for connector setup, scheduling, health checks, and OpenClaw-compatible growth runs'
979
+ : 'Standalone OpenClaw orchestration CLI');
980
+ program
981
+ .command('init')
982
+ .description('Create an OpenClaw config file in the current repository')
983
+ .option('--config <file>', 'Config path', 'openclaw.config.json')
984
+ .option('--repo-root <dir>', 'Repository root', process.cwd())
985
+ .option('--force', 'Overwrite existing config', false)
986
+ .action(async (options) => {
987
+ try {
988
+ await createInitialConfig(options.config, resolve(options.repoRoot), Boolean(options.force));
989
+ process.stdout.write(`Created ${resolve(options.config)}\n`);
990
+ }
991
+ catch (error) {
992
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
993
+ process.exitCode = 1;
994
+ }
995
+ });
996
+ program
997
+ .command('wizard')
998
+ .description('Run the interactive Growth Engineer setup wizard')
999
+ .allowUnknownOption(true)
1000
+ .action(() => {
1001
+ const result = runRuntimeInteractive('openclaw-growth-wizard', forwardedArgsAfter('wizard'), {
1002
+ cwd: process.cwd(),
1003
+ });
1004
+ if (!result.ok) {
1005
+ process.exitCode = result.code ?? 1;
1006
+ }
1007
+ });
1008
+ program
1009
+ .command('setup')
1010
+ .description('Initialize config and reuse the shared AnalyticsCLI skill install flow')
1011
+ .option('--config <file>', 'Config path', 'openclaw.config.json')
1012
+ .option('--repo-root <dir>', 'Repository root', process.cwd())
1013
+ .option('--force', 'Overwrite existing config', false)
1014
+ .option('--skip-config', 'Skip config initialization', false)
1015
+ .option('--skip-shared-skills', 'Skip shared skill installation', false)
1016
+ .option('--connectors <list>', 'Install/enable connector helper tooling for selected connectors (github,asc,revenuecat,all)')
1017
+ .action(async (options) => {
1018
+ try {
1019
+ const selectedConnectors = parseConnectorList(options.connectors);
1020
+ const repoRoot = resolve(options.repoRoot);
1021
+ if (!options.skipConfig) {
1022
+ const configTarget = resolve(options.config);
1023
+ if (Boolean(options.force) || !fileExists(configTarget)) {
1024
+ await createInitialConfig(options.config, repoRoot, Boolean(options.force));
1025
+ process.stdout.write(`Config ready: ${configTarget}\n`);
1026
+ }
1027
+ else {
1028
+ process.stdout.write(`Config already exists: ${configTarget}\n`);
1029
+ }
1030
+ }
1031
+ if (!options.skipSharedSkills) {
1032
+ const setup = runSharedAnalyticsSetup();
1033
+ process.stdout.write(`${setup.detail}\n`);
1034
+ if (!setup.ok) {
1035
+ process.exitCode = 1;
1036
+ }
1037
+ }
1038
+ if (selectedConnectors.length > 0) {
1039
+ const results = await installConnectorHelpers(resolve(options.config), selectedConnectors, repoRoot);
1040
+ for (const result of results) {
1041
+ process.stdout.write(`Connector helper (${result.connector}): ${result.ok ? 'ok' : 'failed'} — ${result.detail}\n`);
1042
+ }
1043
+ if (results.some((result) => !result.ok)) {
1044
+ process.exitCode = 1;
1045
+ }
1046
+ }
1047
+ }
1048
+ catch (error) {
1049
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1050
+ process.exitCode = 1;
1051
+ }
1052
+ });
1053
+ program
1054
+ .command('preflight')
1055
+ .description('Validate runtime dependencies, signals, and delivery configuration')
1056
+ .option('--config <file>', 'Config path', 'openclaw.config.json')
1057
+ .option('--test-connections', 'Run live connection checks where supported', false)
1058
+ .option('--json', 'Print JSON result', false)
1059
+ .action(async (options) => {
1060
+ try {
1061
+ const result = await runPreflight(options.config, {
1062
+ testConnections: Boolean(options.testConnections),
1063
+ });
1064
+ if (options.json) {
1065
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1066
+ }
1067
+ else {
1068
+ process.stdout.write(`Preflight ${result.ok ? 'passed' : 'failed'}: ${result.summary.pass} pass, ${result.summary.warn} warn, ${result.summary.fail} fail\n`);
1069
+ for (const check of result.checks) {
1070
+ process.stdout.write(`- [${check.status}] ${check.name}: ${check.detail}\n`);
1071
+ }
1072
+ }
1073
+ if (!result.ok) {
1074
+ process.exitCode = 1;
1075
+ }
1076
+ }
1077
+ catch (error) {
1078
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1079
+ process.exitCode = 1;
1080
+ }
1081
+ });
1082
+ program
1083
+ .command('run')
1084
+ .description('Run one OpenClaw evaluation pass or stay in interval loop')
1085
+ .option('--config <file>', 'Config path', 'openclaw.config.json')
1086
+ .option('--loop', 'Keep running using schedule.intervalMinutes', false)
1087
+ .action(async (options) => {
1088
+ try {
1089
+ const config = await readOpenClawConfig(resolve(options.config));
1090
+ do {
1091
+ const result = await runOnce(options.config);
1092
+ process.stdout.write(`${result.runtimeOutput}\n`);
1093
+ process.stdout.write(`Issue drafts: ${result.issueCount} (${result.issuesPath})\n`);
1094
+ if (!result.deliveryResult.delivered && result.deliveryResult.skippedReason) {
1095
+ process.stdout.write(`Deliveries skipped: ${result.deliveryResult.skippedReason}\n`);
1096
+ }
1097
+ if (!options.loop) {
1098
+ break;
1099
+ }
1100
+ await sleep(config.schedule.intervalMinutes * 60_000);
1101
+ } while (options.loop);
1102
+ }
1103
+ catch (error) {
1104
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1105
+ process.exitCode = 1;
1106
+ }
1107
+ });
1108
+ program
1109
+ .command('start')
1110
+ .description('Create config if needed, run preflight, then execute one pass')
1111
+ .option('--config <file>', 'Config path', 'openclaw.config.json')
1112
+ .option('--repo-root <dir>', 'Repository root for initial config generation', process.cwd())
1113
+ .option('--test-connections', 'Run live connection checks during preflight', true)
1114
+ .option('--no-test-connections', 'Skip live connection checks during preflight')
1115
+ .action(async (options) => {
1116
+ try {
1117
+ if (!fileExists(resolve(options.config))) {
1118
+ await createInitialConfig(options.config, resolve(options.repoRoot), false);
1119
+ process.stdout.write(`Created ${resolve(options.config)}\n`);
1120
+ }
1121
+ const preflight = await runPreflight(options.config, {
1122
+ testConnections: Boolean(options.testConnections),
1123
+ });
1124
+ if (!preflight.ok) {
1125
+ process.stdout.write(renderPreflightChecklist(preflight));
1126
+ process.exitCode = 1;
1127
+ return;
1128
+ }
1129
+ const result = await runOnce(options.config);
1130
+ process.stdout.write(`${result.runtimeOutput}\n`);
1131
+ process.stdout.write(`Issue drafts: ${result.issueCount} (${result.issuesPath})\n`);
1132
+ }
1133
+ catch (error) {
1134
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1135
+ process.exitCode = 1;
1136
+ }
1137
+ });
1138
+ const exportersCommand = program.command('exporters').description('Connector export helpers');
1139
+ exportersCommand
1140
+ .command('analytics-summary')
1141
+ .description('Export the default analytics summary JSON')
1142
+ .allowUnknownOption(true)
1143
+ .action(() => {
1144
+ const result = runRuntime('export-analytics-summary', forwardedArgsAfter('analytics-summary'), {
1145
+ cwd: process.cwd(),
1146
+ timeoutMs: 20 * 60_000,
1147
+ });
1148
+ process.stdout.write(result.stdout);
1149
+ process.stderr.write(result.stderr);
1150
+ if (!result.ok) {
1151
+ process.exitCode = 1;
1152
+ }
1153
+ });
1154
+ exportersCommand
1155
+ .command('asc-summary')
1156
+ .description('Export the default ASC summary JSON')
1157
+ .allowUnknownOption(true)
1158
+ .action(() => {
1159
+ const result = runRuntime('export-asc-summary', forwardedArgsAfter('asc-summary'), {
1160
+ cwd: process.cwd(),
1161
+ timeoutMs: 20 * 60_000,
1162
+ });
1163
+ process.stdout.write(result.stdout);
1164
+ process.stderr.write(result.stderr);
1165
+ if (!result.ok) {
1166
+ process.exitCode = 1;
1167
+ }
1168
+ });
1169
+ program
1170
+ .command('feedback-api')
1171
+ .description('Run the lightweight feedback API ingestion endpoint')
1172
+ .allowUnknownOption(true)
1173
+ .action(() => {
1174
+ const result = runRuntime('openclaw-feedback-api', forwardedArgsAfter('feedback-api'), {
1175
+ cwd: process.cwd(),
1176
+ timeoutMs: 20 * 60_000,
1177
+ });
1178
+ process.stdout.write(result.stdout);
1179
+ process.stderr.write(result.stderr);
1180
+ if (!result.ok) {
1181
+ process.exitCode = 1;
1182
+ }
1183
+ });
1184
+ program.parseAsync(process.argv).catch((error) => {
1185
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1186
+ process.exitCode = 1;
1187
+ });
1188
+ //# sourceMappingURL=index.js.map