@analyticscli/growth-engineer 0.1.0-preview.14 → 0.1.0-preview.18

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 (40) hide show
  1. package/dist/config.d.ts +775 -22
  2. package/dist/config.js +39 -5
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.js +134 -21
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/export-asc-summary.mjs +1 -1
  7. package/dist/runtime/export-asc-summary.mjs.map +1 -1
  8. package/dist/runtime/export-coolify-summary.d.mts +2 -0
  9. package/dist/runtime/export-coolify-summary.mjs +230 -0
  10. package/dist/runtime/export-coolify-summary.mjs.map +1 -0
  11. package/dist/runtime/export-paddle-summary.d.mts +2 -0
  12. package/dist/runtime/export-paddle-summary.mjs +170 -0
  13. package/dist/runtime/export-paddle-summary.mjs.map +1 -0
  14. package/dist/runtime/export-sentry-summary.mjs +265 -38
  15. package/dist/runtime/export-sentry-summary.mjs.map +1 -1
  16. package/dist/runtime/export-seo-summary.d.mts +2 -0
  17. package/dist/runtime/export-seo-summary.mjs +503 -0
  18. package/dist/runtime/export-seo-summary.mjs.map +1 -0
  19. package/dist/runtime/openclaw-exporters-lib.d.mts +50 -0
  20. package/dist/runtime/openclaw-exporters-lib.mjs +761 -57
  21. package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
  22. package/dist/runtime/openclaw-growth-engineer.mjs +163 -4
  23. package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -1
  24. package/dist/runtime/openclaw-growth-env.mjs +5 -0
  25. package/dist/runtime/openclaw-growth-env.mjs.map +1 -1
  26. package/dist/runtime/openclaw-growth-preflight.mjs +399 -26
  27. package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
  28. package/dist/runtime/openclaw-growth-runner.mjs +564 -69
  29. package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
  30. package/dist/runtime/openclaw-growth-shared.d.mts +150 -2
  31. package/dist/runtime/openclaw-growth-shared.mjs +489 -7
  32. package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
  33. package/dist/runtime/openclaw-growth-start.mjs +584 -48
  34. package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
  35. package/dist/runtime/openclaw-growth-status.mjs +82 -6
  36. package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
  37. package/dist/runtime/openclaw-growth-wizard.mjs +1501 -105
  38. package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
  39. package/package.json +3 -1
  40. package/templates/config.example.json +120 -71
@@ -12,6 +12,41 @@ const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
12
12
  const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
13
13
  const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
14
14
  (process.env.HOME ? path.join(process.env.HOME, '.local') : path.join(process.cwd(), '.analyticscli-npm'));
15
+ const ACCOUNT_SIGNAL_CONNECTORS = [
16
+ 'stripe',
17
+ 'lemonsqueezy',
18
+ 'adapty',
19
+ 'superwall',
20
+ 'google-play',
21
+ 'datadog',
22
+ 'bugsnag',
23
+ 'intercom',
24
+ 'zendesk',
25
+ 'apple-search-ads',
26
+ 'google-ads',
27
+ 'meta-ads',
28
+ 'tiktok-ads',
29
+ 'vercel',
30
+ 'cloudflare',
31
+ 'resend',
32
+ 'customerio',
33
+ 'mailchimp',
34
+ 'appfollow',
35
+ 'apptweak',
36
+ 'linear',
37
+ 'postiz',
38
+ ];
39
+ const SUPPORTED_CONNECTORS = [
40
+ 'analytics',
41
+ 'github',
42
+ 'asc',
43
+ 'revenuecat',
44
+ 'paddle',
45
+ 'seo',
46
+ 'sentry',
47
+ 'coolify',
48
+ ...ACCOUNT_SIGNAL_CONNECTORS,
49
+ ];
15
50
  function printHelpAndExit(exitCode, reason = null) {
16
51
  if (reason) {
17
52
  process.stderr.write(`${reason}\n\n`);
@@ -28,7 +63,7 @@ Options:
28
63
  --config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
29
64
  --test-connections Run live API/connector smoke checks for enabled channels
30
65
  --only-connectors <list>
31
- Limit live checks to analytics,github,asc,revenuecat,sentry
66
+ Limit live checks to ${SUPPORTED_CONNECTORS.join(',')}
32
67
  --timeout-ms <ms> Connection test timeout in milliseconds (default: ${DEFAULT_CONNECTION_TIMEOUT_MS})
33
68
  --progress-json Emit machine-readable progress events on stderr
34
69
  --json Print JSON only (default)
@@ -99,8 +134,58 @@ function normalizeConnectorKey(value) {
99
134
  return 'asc';
100
135
  if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
101
136
  return 'revenuecat';
137
+ if (['paddle', 'paddle-billing', 'billing-metrics', 'web-revenue'].includes(normalized))
138
+ return 'paddle';
139
+ if (['seo', 'gsc', 'google-search-console', 'search-console', 'dataforseo', 'organic-search'].includes(normalized))
140
+ return 'seo';
102
141
  if (['sentry', 'sentry-api', 'sentry-mcp', 'glitchtip', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
103
142
  return 'sentry';
143
+ if (['coolify', 'coolify-api', 'deployment', 'deployments', 'hosting', 'infra', 'infrastructure'].includes(normalized))
144
+ return 'coolify';
145
+ if (['stripe', 'stripe-billing', 'stripe-payments'].includes(normalized))
146
+ return 'stripe';
147
+ if (['lemonsqueezy', 'lemon-squeezy', 'lemon', 'ls'].includes(normalized))
148
+ return 'lemonsqueezy';
149
+ if (['adapty', 'adapty-paywalls', 'adapty-subscriptions'].includes(normalized))
150
+ return 'adapty';
151
+ if (['superwall', 'superwall-paywalls'].includes(normalized))
152
+ return 'superwall';
153
+ if (['google-play', 'google-play-console', 'play-console', 'play-store', 'android-store'].includes(normalized))
154
+ return 'google-play';
155
+ if (['datadog', 'datadog-rum', 'datadog-apm', 'datadog-logs'].includes(normalized))
156
+ return 'datadog';
157
+ if (['bugsnag', 'bugsnag-crashes'].includes(normalized))
158
+ return 'bugsnag';
159
+ if (['intercom', 'intercom-support'].includes(normalized))
160
+ return 'intercom';
161
+ if (['zendesk', 'zendesk-support'].includes(normalized))
162
+ return 'zendesk';
163
+ if (['apple-search-ads', 'apple-ads', 'asa', 'search-ads'].includes(normalized))
164
+ return 'apple-search-ads';
165
+ if (['google-ads', 'adwords'].includes(normalized))
166
+ return 'google-ads';
167
+ if (['meta-ads', 'facebook-ads', 'instagram-ads', 'fb-ads'].includes(normalized))
168
+ return 'meta-ads';
169
+ if (['tiktok-ads', 'tiktok-business', 'tiktok-business-api'].includes(normalized))
170
+ return 'tiktok-ads';
171
+ if (['vercel', 'vercel-deployments', 'vercel-hosting'].includes(normalized))
172
+ return 'vercel';
173
+ if (['cloudflare', 'cf', 'cloudflare-workers', 'cloudflare-pages'].includes(normalized))
174
+ return 'cloudflare';
175
+ if (['resend', 'resend-email'].includes(normalized))
176
+ return 'resend';
177
+ if (['customerio', 'customer-io', 'customer.io', 'cio'].includes(normalized))
178
+ return 'customerio';
179
+ if (['mailchimp', 'mailchimp-marketing'].includes(normalized))
180
+ return 'mailchimp';
181
+ if (['appfollow', 'app-follow'].includes(normalized))
182
+ return 'appfollow';
183
+ if (['apptweak', 'app-tweak'].includes(normalized))
184
+ return 'apptweak';
185
+ if (['linear', 'linear-issues', 'linear-planning'].includes(normalized))
186
+ return 'linear';
187
+ if (['postiz', 'postiz-api', 'social-publishing', 'social-scheduler'].includes(normalized))
188
+ return 'postiz';
104
189
  return null;
105
190
  }
106
191
  function parseConnectorList(value) {
@@ -110,14 +195,10 @@ function parseConnectorList(value) {
110
195
  for (const entry of String(value).split(',')) {
111
196
  const connector = normalizeConnectorKey(entry);
112
197
  if (!connector) {
113
- printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use analytics, github, asc, revenuecat, sentry, or all.`);
198
+ printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
114
199
  }
115
200
  if (connector === 'all') {
116
- connectors.add('analytics');
117
- connectors.add('github');
118
- connectors.add('asc');
119
- connectors.add('revenuecat');
120
- connectors.add('sentry');
201
+ SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
121
202
  }
122
203
  else {
123
204
  connectors.add(connector);
@@ -150,19 +231,28 @@ function replaceLegacyRuntimeScriptCommand(command) {
150
231
  const trimmed = String(command || '').trim();
151
232
  if (!trimmed)
152
233
  return trimmed;
153
- return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
234
+ return trimmed.replace(/^node\s+scripts\/(export-analytics-summary\.mjs|export-revenuecat-summary\.mjs|export-paddle-summary\.mjs|export-seo-summary\.mjs|export-sentry-summary\.mjs|export-asc-summary\.mjs|openclaw-growth-start\.mjs|openclaw-growth-status\.mjs|openclaw-growth-preflight\.mjs|openclaw-growth-runner\.mjs|openclaw-growth-engineer\.mjs)(?=\s|$)/, (_match, scriptName) => nodeRuntimeScriptCommand(scriptName));
154
235
  }
155
236
  function commandHasConfigArg(command) {
156
237
  return /(?:^|\s)--config(?:=|\s|$)/.test(String(command || ''));
157
238
  }
158
- function commandShouldReceiveActiveConfig(command) {
159
- return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-analytics-summary|export-revenuecat-summary|export-sentry-summary|export-asc-summary)\.mjs(?:\s|$)/.test(String(command || ''));
239
+ function commandIsBuiltinExporter(command) {
240
+ return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-analytics-summary|export-revenuecat-summary|export-paddle-summary|export-seo-summary|export-sentry-summary|export-coolify-summary|export-asc-summary)\.mjs(?:\s|$)/.test(String(command || ''));
241
+ }
242
+ function commandSupportsActiveConfig(command) {
243
+ return /(?:^|\s)(?:node\s+)?(?:\S*\/)?(?:export-sentry-summary|export-coolify-summary)\.mjs(?:\s|$)/.test(String(command || ''));
160
244
  }
161
245
  function withActiveConfigArg(command, configPath) {
162
246
  const trimmed = String(command || '').trim();
163
- if (!trimmed || !configPath || !commandShouldReceiveActiveConfig(trimmed)) {
247
+ if (!trimmed || !configPath || !commandIsBuiltinExporter(trimmed)) {
164
248
  return trimmed;
165
249
  }
250
+ if (!commandSupportsActiveConfig(trimmed)) {
251
+ return trimmed
252
+ .replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, '$1')
253
+ .replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, '$1')
254
+ .trim();
255
+ }
166
256
  if (commandHasConfigArg(trimmed)) {
167
257
  return trimmed
168
258
  .replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${shellQuote(configPath)}`)
@@ -187,12 +277,21 @@ function resolveShellCommand() {
187
277
  }
188
278
  return 'sh';
189
279
  }
280
+ function hardenUnattendedShellCommand(command) {
281
+ return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
282
+ }
190
283
  function runShell(command, options = {}) {
191
284
  return new Promise((resolve) => {
192
- const child = spawn(resolveShellCommand(), ['-c', command], {
285
+ const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
193
286
  stdio: ['ignore', 'pipe', 'pipe'],
194
287
  cwd: options.cwd,
195
- env: options.env ? { ...process.env, ...options.env } : process.env,
288
+ env: {
289
+ ...process.env,
290
+ ...(options.env || {}),
291
+ DEBIAN_FRONTEND: 'noninteractive',
292
+ SUDO_ASKPASS: '/bin/false',
293
+ SUDO_PROMPT: '',
294
+ },
196
295
  });
197
296
  let stdout = '';
198
297
  let stderr = '';
@@ -604,9 +703,47 @@ async function testRevenueCatConnection(revenuecatToken, timeoutMs) {
604
703
  };
605
704
  }
606
705
  }
706
+ async function testPaddleConnection(paddleToken, timeoutMs) {
707
+ if (!paddleToken) {
708
+ return {
709
+ ok: false,
710
+ detail: 'missing token',
711
+ };
712
+ }
713
+ const to = new Date().toISOString().slice(0, 10);
714
+ const fromDate = new Date();
715
+ fromDate.setUTCDate(fromDate.getUTCDate() - 2);
716
+ const from = fromDate.toISOString().slice(0, 10);
717
+ try {
718
+ const response = await fetchWithTimeout(`https://api.paddle.com/metrics/revenue?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, {
719
+ method: 'GET',
720
+ headers: {
721
+ Accept: 'application/json',
722
+ Authorization: `Bearer ${paddleToken}`,
723
+ 'Paddle-Version': '1',
724
+ },
725
+ }, timeoutMs);
726
+ if (!response.ok) {
727
+ return {
728
+ ok: false,
729
+ detail: `HTTP ${response.status}: ${truncate(response.body)}`,
730
+ };
731
+ }
732
+ return {
733
+ ok: true,
734
+ detail: `HTTP ${response.status}`,
735
+ };
736
+ }
737
+ catch (error) {
738
+ return {
739
+ ok: false,
740
+ detail: error instanceof Error ? error.message : String(error),
741
+ };
742
+ }
743
+ }
607
744
  function describeAnalyticsConnectionFailure(detail, analyticsTokenEnv, hasAnalyticsToken) {
608
745
  if (!hasAnalyticsToken) {
609
- return `AnalyticsCLI needs query access. Run \`node scripts/openclaw-growth-wizard.mjs --connectors analytics\`, create or copy a readonly CLI token in dash.analyticscli.com -> API Keys, and paste it into the local terminal wizard. Raw error: ${detail}`;
746
+ return `AnalyticsCLI needs query access. Run \`npx -y @analyticscli/growth-engineer@preview wizard --connectors analytics\`, create or copy a readonly CLI token in dash.analyticscli.com -> API Keys, and paste it into the local terminal wizard. Raw error: ${detail}`;
610
747
  }
611
748
  return `AnalyticsCLI connection failed with \`${analyticsTokenEnv}\` set. Verify that the pasted readonly CLI token is current and has project access. Raw error: ${detail}`;
612
749
  }
@@ -643,6 +780,62 @@ async function testSentryConnection(sentryToken, timeoutMs, baseUrl = 'https://s
643
780
  };
644
781
  }
645
782
  }
783
+ function normalizeCoolifyBaseUrl(value) {
784
+ const raw = String(value || '').trim().replace(/\/+$/, '');
785
+ if (!raw)
786
+ return '';
787
+ return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
788
+ }
789
+ function resolveCoolifyApiBaseUrl(baseUrl) {
790
+ const normalized = normalizeCoolifyBaseUrl(baseUrl);
791
+ if (!normalized)
792
+ return '';
793
+ if (/\/api\/v1$/i.test(normalized))
794
+ return normalized;
795
+ if (/\/api$/i.test(normalized))
796
+ return `${normalized}/v1`;
797
+ return `${normalized}/api/v1`;
798
+ }
799
+ async function testCoolifyConnection(coolifyToken, timeoutMs, baseUrl) {
800
+ if (!coolifyToken) {
801
+ return {
802
+ ok: false,
803
+ detail: 'missing token',
804
+ };
805
+ }
806
+ const apiBaseUrl = resolveCoolifyApiBaseUrl(baseUrl);
807
+ if (!apiBaseUrl) {
808
+ return {
809
+ ok: false,
810
+ detail: 'missing base URL',
811
+ };
812
+ }
813
+ try {
814
+ const response = await fetchWithTimeout(`${apiBaseUrl}/applications?limit=1`, {
815
+ method: 'GET',
816
+ headers: {
817
+ Accept: 'application/json',
818
+ Authorization: `Bearer ${coolifyToken}`,
819
+ },
820
+ }, timeoutMs);
821
+ if (!response.ok) {
822
+ return {
823
+ ok: false,
824
+ detail: `HTTP ${response.status}: ${truncate(response.body)}`,
825
+ };
826
+ }
827
+ return {
828
+ ok: true,
829
+ detail: `HTTP ${response.status}`,
830
+ };
831
+ }
832
+ catch (error) {
833
+ return {
834
+ ok: false,
835
+ detail: error instanceof Error ? error.message : String(error),
836
+ };
837
+ }
838
+ }
646
839
  function normalizeSentryAccounts(config, sentryTokenEnv) {
647
840
  const sentrySource = config?.sources?.sentry;
648
841
  const accounts = Array.isArray(sentrySource?.accounts) ? sentrySource.accounts : [];
@@ -654,6 +847,13 @@ function normalizeSentryAccounts(config, sentryTokenEnv) {
654
847
  label: String(account?.label || account?.name || account?.id || `Sentry ${index + 1}`).trim(),
655
848
  tokenEnv: String(account?.tokenEnv || account?.token_env || account?.secretEnv || sentryTokenEnv).trim(),
656
849
  baseUrl: String(account?.baseUrl || account?.base_url || account?.url || 'https://sentry.io').trim(),
850
+ org: String(account?.org || account?.organization || '').trim(),
851
+ projects: Array.isArray(account?.projects)
852
+ ? account.projects.map((project) => String(typeof project === 'string' ? project : project?.project || project?.slug || '').trim()).filter(Boolean)
853
+ : account?.project
854
+ ? [String(account.project).trim()].filter(Boolean)
855
+ : [],
856
+ environment: String(account?.environment || process.env.SENTRY_ENVIRONMENT || 'production').trim(),
657
857
  }));
658
858
  }
659
859
  return [
@@ -662,9 +862,24 @@ function normalizeSentryAccounts(config, sentryTokenEnv) {
662
862
  label: 'Sentry',
663
863
  tokenEnv: sentryTokenEnv,
664
864
  baseUrl: String(process.env.SENTRY_BASE_URL || 'https://sentry.io').trim(),
865
+ org: String(process.env.SENTRY_ORG || '').trim(),
866
+ projects: String(process.env.SENTRY_PROJECT || '').trim() ? [String(process.env.SENTRY_PROJECT).trim()] : [],
867
+ environment: String(process.env.SENTRY_ENVIRONMENT || 'production').trim(),
665
868
  },
666
869
  ];
667
870
  }
871
+ function describeSentryAccountTarget(account) {
872
+ const parts = [
873
+ account.label,
874
+ `id=${account.key}`,
875
+ `baseUrl=${account.baseUrl || 'https://sentry.io'}`,
876
+ account.org ? `org=${account.org}` : null,
877
+ account.projects?.length ? `projects=${account.projects.join(',')}` : null,
878
+ account.environment ? `environment=${account.environment}` : null,
879
+ account.tokenEnv ? `tokenEnv=${account.tokenEnv}` : null,
880
+ ].filter(Boolean);
881
+ return parts.join(' ');
882
+ }
668
883
  async function testGitHubConnection(githubToken, githubRepo, timeoutMs, actionMode) {
669
884
  if (!githubToken) {
670
885
  return {
@@ -773,7 +988,10 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
773
988
  const tasks = [];
774
989
  const analyticsTokenEnv = getSecretName(config, 'analyticsTokenEnv', 'ANALYTICSCLI_ACCESS_TOKEN');
775
990
  const revenuecatTokenEnv = getSecretName(config, 'revenuecatTokenEnv', 'REVENUECAT_API_KEY');
991
+ const paddleTokenEnv = getSecretName(config, 'paddleTokenEnv', 'PADDLE_API_KEY');
992
+ const gscTokenEnv = getSecretName(config, 'gscTokenEnv', 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN');
776
993
  const sentryTokenEnv = getSecretName(config, 'sentryTokenEnv', 'SENTRY_AUTH_TOKEN');
994
+ const coolifyTokenEnv = getSecretName(config, 'coolifyTokenEnv', 'COOLIFY_API_TOKEN');
777
995
  const feedbackTokenEnv = getSecretName(config, 'feedbackTokenEnv', 'FEEDBACK_API_TOKEN');
778
996
  const githubTokenEnv = getSecretName(config, 'githubTokenEnv', 'GITHUB_TOKEN');
779
997
  const githubRepo = isConfiguredGitHubRepo(config?.project?.githubRepo)
@@ -840,6 +1058,81 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
840
1058
  },
841
1059
  });
842
1060
  }
1061
+ const paddleSource = config.sources?.paddle;
1062
+ if (onlyAllows(onlyConnectors, 'paddle')) {
1063
+ scheduleProgressGroup(tasks, checks, progressJson, {
1064
+ key: 'paddle',
1065
+ label: 'Paddle',
1066
+ detail: 'metrics API auth + revenue read',
1067
+ run: async (groupChecks) => {
1068
+ if (sourceEnabled(config, 'paddle')) {
1069
+ const token = process.env[paddleTokenEnv] || '';
1070
+ if (!token) {
1071
+ addCheck(groupChecks, 'connection:paddle', false, `${paddleTokenEnv} missing (required for live Paddle metrics API test)`, paddleSource?.mode === 'command' ? 'fail' : 'warn');
1072
+ }
1073
+ else {
1074
+ const paddleConnection = await testPaddleConnection(token, timeoutMs);
1075
+ addCheck(groupChecks, 'connection:paddle', paddleConnection.ok, paddleConnection.ok
1076
+ ? `Paddle metrics auth check passed (${paddleConnection.detail})`
1077
+ : `Paddle metrics auth check failed (${paddleConnection.detail})`);
1078
+ }
1079
+ if (paddleSource?.mode === 'command') {
1080
+ const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(paddleSource.command || '').trim()), configPath);
1081
+ if (!command) {
1082
+ addCheck(groupChecks, 'connection:paddle-command', false, 'paddle source uses command mode but no command configured');
1083
+ }
1084
+ else {
1085
+ const commandCheck = await testCommandSourceJson(`${command} --last 2d --max-signals 1`, commandCwd);
1086
+ addCheck(groupChecks, 'connection:paddle-command', commandCheck.ok, commandCheck.ok
1087
+ ? 'Paddle command smoke test passed'
1088
+ : `Paddle command smoke test failed (${commandCheck.detail})`);
1089
+ }
1090
+ }
1091
+ }
1092
+ else {
1093
+ addCheck(groupChecks, 'connection:paddle', true, 'source disabled');
1094
+ }
1095
+ },
1096
+ });
1097
+ }
1098
+ const seoSource = config.sources?.seo;
1099
+ if (onlyAllows(onlyConnectors, 'seo')) {
1100
+ scheduleProgressGroup(tasks, checks, progressJson, {
1101
+ key: 'seo',
1102
+ label: 'SEO / GSC',
1103
+ detail: 'Search Console auth or CSV/DataForSEO config',
1104
+ run: async (groupChecks) => {
1105
+ if (sourceEnabled(config, 'seo')) {
1106
+ const hasGscCredential = Boolean(process.env[gscTokenEnv] ||
1107
+ process.env.GSC_ACCESS_TOKEN ||
1108
+ process.env.GOOGLE_APPLICATION_CREDENTIALS ||
1109
+ process.env.GSC_SERVICE_ACCOUNT_JSON ||
1110
+ process.env.GOOGLE_SERVICE_ACCOUNT_JSON);
1111
+ addCheck(groupChecks, 'connection:seo:gsc-credentials', hasGscCredential || seoSource?.mode !== 'command', hasGscCredential
1112
+ ? 'GSC credential is configured'
1113
+ : 'GSC credential missing; command can still run in CSV-only mode if --gsc-csv/--csv is configured', hasGscCredential ? 'pass' : 'warn');
1114
+ addCheck(groupChecks, 'connection:seo:gsc-site', true, process.env.GSC_SITE_URL || seoSource?.siteUrl || seoSource?.site_url
1115
+ ? 'GSC site/property is pinned intentionally'
1116
+ : 'no GSC site/property pinned; exporter will list and query all verified Search Console properties', 'pass');
1117
+ if (seoSource?.mode === 'command') {
1118
+ const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(seoSource.command || '').trim()), configPath);
1119
+ if (!command) {
1120
+ addCheck(groupChecks, 'connection:seo-command', false, 'seo source uses command mode but no command configured');
1121
+ }
1122
+ else {
1123
+ const commandCheck = await testCommandSourceJson(`${command} --row-limit 5 --max-signals 1`, commandCwd);
1124
+ addCheck(groupChecks, 'connection:seo-command', commandCheck.ok, commandCheck.ok
1125
+ ? 'SEO command smoke test passed'
1126
+ : `SEO command smoke test failed (${commandCheck.detail})`, hasGscCredential || /--csv|--gsc-csv/.test(command) ? 'fail' : 'warn');
1127
+ }
1128
+ }
1129
+ }
1130
+ else {
1131
+ addCheck(groupChecks, 'connection:seo', true, 'source disabled');
1132
+ }
1133
+ },
1134
+ });
1135
+ }
843
1136
  const sentrySource = config.sources?.sentry;
844
1137
  if (onlyAllows(onlyConnectors, 'sentry')) {
845
1138
  scheduleProgressGroup(tasks, checks, progressJson, {
@@ -852,14 +1145,15 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
852
1145
  for (const account of sentryAccounts) {
853
1146
  const token = process.env[account.tokenEnv] || '';
854
1147
  const checkName = sentryAccounts.length > 1 ? `connection:sentry:${account.key}` : 'connection:sentry';
1148
+ const accountTarget = describeSentryAccountTarget(account);
855
1149
  if (!token) {
856
- addCheck(groupChecks, checkName, false, `${account.tokenEnv} missing (required for live Sentry API test for ${account.label})`, sentrySource?.mode === 'command' ? 'fail' : 'warn');
1150
+ addCheck(groupChecks, checkName, false, `${account.tokenEnv} missing (required for live Sentry API test for ${accountTarget})`, sentrySource?.mode === 'command' ? 'fail' : 'warn');
857
1151
  continue;
858
1152
  }
859
1153
  const sentryConnection = await testSentryConnection(token, timeoutMs, account.baseUrl);
860
1154
  addCheck(groupChecks, checkName, sentryConnection.ok, sentryConnection.ok
861
- ? `${account.label} auth check passed (${sentryConnection.detail})`
862
- : `${account.label} auth check failed (${sentryConnection.detail})`);
1155
+ ? `${accountTarget} auth check passed (${sentryConnection.detail})`
1156
+ : `${accountTarget} auth check failed (${sentryConnection.detail})`);
863
1157
  }
864
1158
  if (sentrySource?.mode === 'command') {
865
1159
  const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(sentrySource.command || '').trim()), configPath);
@@ -870,12 +1164,56 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
870
1164
  const commandCheck = await testCommandSourceJson(`${command} --limit 1 --max-signals 1 --last 24h`, commandCwd);
871
1165
  addCheck(groupChecks, 'connection:sentry-command', commandCheck.ok, commandCheck.ok
872
1166
  ? 'Sentry command smoke test passed'
873
- : `Sentry command smoke test failed (${commandCheck.detail})`);
1167
+ : `Sentry command smoke test failed (${commandCheck.detail}); configured accounts: ${sentryAccounts.map(describeSentryAccountTarget).join(' | ')}`);
1168
+ }
1169
+ }
1170
+ }
1171
+ else {
1172
+ const requiredByConnectorSetup = onlyAllows(onlyConnectors, 'sentry') && onlyConnectors.length > 0;
1173
+ addCheck(groupChecks, 'connection:sentry', !requiredByConnectorSetup, requiredByConnectorSetup
1174
+ ? 'selected Sentry connector is still disabled in sources.sentry'
1175
+ : 'source disabled');
1176
+ }
1177
+ },
1178
+ });
1179
+ }
1180
+ const coolifySource = config.sources?.coolify;
1181
+ if (onlyAllows(onlyConnectors, 'coolify')) {
1182
+ scheduleProgressGroup(tasks, checks, progressJson, {
1183
+ key: 'coolify',
1184
+ label: 'Coolify',
1185
+ detail: 'API key auth + deployment/resource read',
1186
+ run: async (groupChecks) => {
1187
+ if (sourceEnabled(config, 'coolify')) {
1188
+ const token = process.env[coolifySource?.tokenEnv || coolifySource?.secretEnv || coolifyTokenEnv] || '';
1189
+ const baseUrl = String(coolifySource?.baseUrl || coolifySource?.base_url || process.env.COOLIFY_BASE_URL || '').trim();
1190
+ if (!token) {
1191
+ addCheck(groupChecks, 'connection:coolify', false, `${coolifySource?.tokenEnv || coolifySource?.secretEnv || coolifyTokenEnv} missing (required for live Coolify API test)`, coolifySource?.mode === 'command' ? 'fail' : 'warn');
1192
+ }
1193
+ else if (!baseUrl) {
1194
+ addCheck(groupChecks, 'connection:coolify', false, 'COOLIFY_BASE_URL or sources.coolify.baseUrl missing (required for live Coolify API test)', coolifySource?.mode === 'command' ? 'fail' : 'warn');
1195
+ }
1196
+ else {
1197
+ const coolifyConnection = await testCoolifyConnection(token, timeoutMs, baseUrl);
1198
+ addCheck(groupChecks, 'connection:coolify', coolifyConnection.ok, coolifyConnection.ok
1199
+ ? `Coolify auth check passed (${coolifyConnection.detail})`
1200
+ : `Coolify auth check failed (${coolifyConnection.detail})`);
1201
+ }
1202
+ if (coolifySource?.mode === 'command') {
1203
+ const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(coolifySource.command || '').trim()), configPath);
1204
+ if (!command) {
1205
+ addCheck(groupChecks, 'connection:coolify-command', false, 'coolify source uses command mode but no command configured');
1206
+ }
1207
+ else {
1208
+ const commandCheck = await testCommandSourceJson(`${command} --limit 1 --max-signals 1 --last 24h`, commandCwd);
1209
+ addCheck(groupChecks, 'connection:coolify-command', commandCheck.ok, commandCheck.ok
1210
+ ? 'Coolify command smoke test passed'
1211
+ : `Coolify command smoke test failed (${commandCheck.detail})`);
874
1212
  }
875
1213
  }
876
1214
  }
877
1215
  else {
878
- addCheck(groupChecks, 'connection:sentry', true, 'source disabled');
1216
+ addCheck(groupChecks, 'connection:coolify', true, 'source disabled');
879
1217
  }
880
1218
  },
881
1219
  });
@@ -909,13 +1247,20 @@ async function runConnectionChecks({ checks, config, configPath, timeoutMs, prog
909
1247
  }
910
1248
  for (const extraSource of getAllSourceEntries(config).filter((source) => !source.builtIn)) {
911
1249
  const serviceKind = classifyServiceKind(extraSource.service || extraSource.key);
912
- const connectorKind = serviceKind === 'store'
913
- ? 'asc'
914
- : serviceKind === 'revenue'
915
- ? 'revenuecat'
916
- : serviceKind === 'crash'
917
- ? 'sentry'
918
- : serviceKind;
1250
+ const explicitConnectorKind = normalizeConnectorKey(extraSource.key || extraSource.service);
1251
+ const connectorKind = explicitConnectorKind && explicitConnectorKind !== 'all'
1252
+ ? explicitConnectorKind
1253
+ : serviceKind === 'store'
1254
+ ? 'asc'
1255
+ : serviceKind === 'revenue'
1256
+ ? 'revenuecat'
1257
+ : serviceKind === 'crash'
1258
+ ? 'sentry'
1259
+ : serviceKind === 'infrastructure'
1260
+ ? 'coolify'
1261
+ : serviceKind === 'seo'
1262
+ ? 'seo'
1263
+ : serviceKind;
919
1264
  if (!onlyAllows(onlyConnectors, connectorKind))
920
1265
  continue;
921
1266
  const checkName = `connection:${extraSource.key}`;
@@ -1062,6 +1407,26 @@ async function main() {
1062
1407
  const hasRevenuecatToken = Boolean(process.env[revenuecatTokenEnv]);
1063
1408
  addCheck(checks, `secret:${revenuecatTokenEnv}`, hasRevenuecatToken, hasRevenuecatToken ? 'set (required for RevenueCat command mode)' : 'missing (required for RevenueCat command mode)');
1064
1409
  }
1410
+ if (sourceName === 'paddle') {
1411
+ const paddleTokenEnv = getSecretName(config, 'paddleTokenEnv', 'PADDLE_API_KEY');
1412
+ const hasPaddleToken = Boolean(process.env[paddleTokenEnv]);
1413
+ addCheck(checks, `secret:${paddleTokenEnv}`, hasPaddleToken, hasPaddleToken ? 'set (required for Paddle command mode)' : 'missing (required for Paddle command mode)');
1414
+ }
1415
+ if (sourceName === 'seo') {
1416
+ const gscTokenEnv = getSecretName(config, 'gscTokenEnv', 'GOOGLE_SEARCH_CONSOLE_ACCESS_TOKEN');
1417
+ const hasSearchConsoleAuth = Boolean(process.env[gscTokenEnv] ||
1418
+ process.env.GSC_ACCESS_TOKEN ||
1419
+ process.env.GOOGLE_APPLICATION_CREDENTIALS ||
1420
+ process.env.GSC_SERVICE_ACCOUNT_JSON ||
1421
+ process.env.GOOGLE_SERVICE_ACCOUNT_JSON);
1422
+ const commandText = String(source.command || '');
1423
+ const csvOnly = /--csv|--gsc-csv/.test(commandText);
1424
+ addCheck(checks, `secret:${gscTokenEnv}`, hasSearchConsoleAuth || csvOnly, hasSearchConsoleAuth
1425
+ ? 'set or service-account auth configured'
1426
+ : csvOnly
1427
+ ? 'not required for configured CSV-only SEO command'
1428
+ : 'missing (required for GSC API mode; CSV-only mode may use --gsc-csv/--csv)', hasSearchConsoleAuth || csvOnly ? 'pass' : 'warn');
1429
+ }
1065
1430
  if (sourceName === 'sentry') {
1066
1431
  const sentryTokenEnv = getSecretName(config, 'sentryTokenEnv', 'SENTRY_AUTH_TOKEN');
1067
1432
  for (const account of normalizeSentryAccounts(config, sentryTokenEnv)) {
@@ -1071,6 +1436,14 @@ async function main() {
1071
1436
  : `missing (required for ${account.label} Sentry command mode)`);
1072
1437
  }
1073
1438
  }
1439
+ if (sourceName === 'coolify') {
1440
+ const coolifyTokenEnv = getSecretName(config, 'coolifyTokenEnv', 'COOLIFY_API_TOKEN');
1441
+ const tokenEnv = String(source.tokenEnv || source.secretEnv || coolifyTokenEnv).trim();
1442
+ const hasCoolifyToken = Boolean(process.env[tokenEnv]);
1443
+ const hasCoolifyBaseUrl = Boolean(source.baseUrl || source.base_url || process.env.COOLIFY_BASE_URL);
1444
+ addCheck(checks, `secret:${tokenEnv}`, hasCoolifyToken, hasCoolifyToken ? 'set (required for Coolify command mode)' : 'missing (required for Coolify command mode)');
1445
+ addCheck(checks, 'source:coolify:base-url', hasCoolifyBaseUrl, hasCoolifyBaseUrl ? 'configured' : 'missing COOLIFY_BASE_URL or sources.coolify.baseUrl');
1446
+ }
1074
1447
  if (!source.builtIn && source.secretEnv) {
1075
1448
  const hasConnectorToken = Boolean(process.env[source.secretEnv]);
1076
1449
  addCheck(checks, `secret:${source.secretEnv}`, hasConnectorToken, hasConnectorToken