@analyticscli/growth-engineer 0.1.0-preview.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +925 -45
- package/dist/config.js +58 -6
- package/dist/config.js.map +1 -1
- package/dist/index.js +134 -21
- package/dist/index.js.map +1 -1
- package/dist/runtime/export-asc-summary.mjs +295 -4
- package/dist/runtime/export-asc-summary.mjs.map +1 -1
- package/dist/runtime/export-coolify-summary.d.mts +2 -0
- package/dist/runtime/export-coolify-summary.mjs +230 -0
- package/dist/runtime/export-coolify-summary.mjs.map +1 -0
- package/dist/runtime/export-paddle-summary.d.mts +2 -0
- package/dist/runtime/export-paddle-summary.mjs +170 -0
- package/dist/runtime/export-paddle-summary.mjs.map +1 -0
- package/dist/runtime/export-sentry-summary.mjs +265 -38
- package/dist/runtime/export-sentry-summary.mjs.map +1 -1
- package/dist/runtime/export-seo-summary.d.mts +2 -0
- package/dist/runtime/export-seo-summary.mjs +503 -0
- package/dist/runtime/export-seo-summary.mjs.map +1 -0
- package/dist/runtime/openclaw-exporters-lib.d.mts +51 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +769 -63
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-engineer.mjs +163 -4
- package/dist/runtime/openclaw-growth-engineer.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-env.mjs +5 -0
- package/dist/runtime/openclaw-growth-env.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +446 -30
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +847 -150
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.d.mts +158 -3
- package/dist/runtime/openclaw-growth-shared.mjs +574 -8
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +816 -41
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +100 -34
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +1997 -226
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +3 -1
- package/templates/config.example.json +128 -65
|
@@ -3,13 +3,50 @@ import { existsSync, promises as fs } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { classifyServiceKind, getActionMode, getAllSourceEntries, getDefaultSourceCommand, getGitHubActionNoun, getGitHubConnectionSummary, getGitHubRequirementText, shouldAutoCreateGitHubArtifact, } from './openclaw-growth-shared.mjs';
|
|
7
8
|
import { applyOpenClawSecretRefs, loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
8
9
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
9
10
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 15_000;
|
|
11
|
+
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const ANALYTICSCLI_PACKAGE_SPEC = process.env.ANALYTICSCLI_CLI_PACKAGE || '@analyticscli/cli@preview';
|
|
11
13
|
const ANALYTICSCLI_NPM_PREFIX = process.env.ANALYTICSCLI_NPM_PREFIX ||
|
|
12
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
|
+
];
|
|
13
50
|
function printHelpAndExit(exitCode, reason = null) {
|
|
14
51
|
if (reason) {
|
|
15
52
|
process.stderr.write(`${reason}\n\n`);
|
|
@@ -26,7 +63,7 @@ Options:
|
|
|
26
63
|
--config <file> Config path (default: ${DEFAULT_CONFIG_PATH})
|
|
27
64
|
--test-connections Run live API/connector smoke checks for enabled channels
|
|
28
65
|
--only-connectors <list>
|
|
29
|
-
Limit live checks to
|
|
66
|
+
Limit live checks to ${SUPPORTED_CONNECTORS.join(',')}
|
|
30
67
|
--timeout-ms <ms> Connection test timeout in milliseconds (default: ${DEFAULT_CONNECTION_TIMEOUT_MS})
|
|
31
68
|
--progress-json Emit machine-readable progress events on stderr
|
|
32
69
|
--json Print JSON only (default)
|
|
@@ -97,8 +134,58 @@ function normalizeConnectorKey(value) {
|
|
|
97
134
|
return 'asc';
|
|
98
135
|
if (['revenuecat', 'revenue-cat', 'rc', 'revenuecat-mcp'].includes(normalized))
|
|
99
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';
|
|
100
141
|
if (['sentry', 'sentry-api', 'sentry-mcp', 'glitchtip', 'crashes', 'errors', 'crash-reporting'].includes(normalized))
|
|
101
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';
|
|
102
189
|
return null;
|
|
103
190
|
}
|
|
104
191
|
function parseConnectorList(value) {
|
|
@@ -108,14 +195,10 @@ function parseConnectorList(value) {
|
|
|
108
195
|
for (const entry of String(value).split(',')) {
|
|
109
196
|
const connector = normalizeConnectorKey(entry);
|
|
110
197
|
if (!connector) {
|
|
111
|
-
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use
|
|
198
|
+
printHelpAndExit(1, `Unknown connector: ${entry.trim()}. Use ${SUPPORTED_CONNECTORS.join(', ')}, or all.`);
|
|
112
199
|
}
|
|
113
200
|
if (connector === 'all') {
|
|
114
|
-
connectors.add(
|
|
115
|
-
connectors.add('github');
|
|
116
|
-
connectors.add('asc');
|
|
117
|
-
connectors.add('revenuecat');
|
|
118
|
-
connectors.add('sentry');
|
|
201
|
+
SUPPORTED_CONNECTORS.forEach((supported) => connectors.add(supported));
|
|
119
202
|
}
|
|
120
203
|
else {
|
|
121
204
|
connectors.add(connector);
|
|
@@ -129,6 +212,54 @@ function shellQuote(value) {
|
|
|
129
212
|
}
|
|
130
213
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
131
214
|
}
|
|
215
|
+
function resolveRuntimeScriptPath(scriptName) {
|
|
216
|
+
const candidates = [
|
|
217
|
+
path.join(RUNTIME_DIR, scriptName),
|
|
218
|
+
path.join(process.cwd(), 'scripts', scriptName),
|
|
219
|
+
path.join(process.cwd(), 'skills', 'openclaw-growth-engineer', 'scripts', scriptName),
|
|
220
|
+
];
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
if (existsSync(candidate))
|
|
223
|
+
return candidate;
|
|
224
|
+
}
|
|
225
|
+
return path.join(RUNTIME_DIR, scriptName);
|
|
226
|
+
}
|
|
227
|
+
function nodeRuntimeScriptCommand(scriptName) {
|
|
228
|
+
return `node ${shellQuote(resolveRuntimeScriptPath(scriptName))}`;
|
|
229
|
+
}
|
|
230
|
+
function replaceLegacyRuntimeScriptCommand(command) {
|
|
231
|
+
const trimmed = String(command || '').trim();
|
|
232
|
+
if (!trimmed)
|
|
233
|
+
return trimmed;
|
|
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));
|
|
235
|
+
}
|
|
236
|
+
function commandHasConfigArg(command) {
|
|
237
|
+
return /(?:^|\s)--config(?:=|\s|$)/.test(String(command || ''));
|
|
238
|
+
}
|
|
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 || ''));
|
|
244
|
+
}
|
|
245
|
+
function withActiveConfigArg(command, configPath) {
|
|
246
|
+
const trimmed = String(command || '').trim();
|
|
247
|
+
if (!trimmed || !configPath || !commandIsBuiltinExporter(trimmed)) {
|
|
248
|
+
return trimmed;
|
|
249
|
+
}
|
|
250
|
+
if (!commandSupportsActiveConfig(trimmed)) {
|
|
251
|
+
return trimmed
|
|
252
|
+
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, '$1')
|
|
253
|
+
.replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, '$1')
|
|
254
|
+
.trim();
|
|
255
|
+
}
|
|
256
|
+
if (commandHasConfigArg(trimmed)) {
|
|
257
|
+
return trimmed
|
|
258
|
+
.replace(/(^|\s)--config=(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${shellQuote(configPath)}`)
|
|
259
|
+
.replace(/(^|\s)--config\s+(?:"[^"]*"|'[^']*'|\S+)/, `$1--config ${shellQuote(configPath)}`);
|
|
260
|
+
}
|
|
261
|
+
return `${trimmed} --config ${shellQuote(configPath)}`;
|
|
262
|
+
}
|
|
132
263
|
function resolveShellCommand() {
|
|
133
264
|
const candidates = [
|
|
134
265
|
process.env.OPENCLAW_SHELL,
|
|
@@ -146,12 +277,21 @@ function resolveShellCommand() {
|
|
|
146
277
|
}
|
|
147
278
|
return 'sh';
|
|
148
279
|
}
|
|
280
|
+
function hardenUnattendedShellCommand(command) {
|
|
281
|
+
return String(command || '').replace(/(^|[;&|]\s*)sudo(?!\s+-n(?:\s|$))(?=\s|$)/g, '$1sudo -n');
|
|
282
|
+
}
|
|
149
283
|
function runShell(command, options = {}) {
|
|
150
284
|
return new Promise((resolve) => {
|
|
151
|
-
const child = spawn(resolveShellCommand(), ['-c', command], {
|
|
285
|
+
const child = spawn(resolveShellCommand(), ['-c', hardenUnattendedShellCommand(command)], {
|
|
152
286
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
287
|
cwd: options.cwd,
|
|
154
|
-
env:
|
|
288
|
+
env: {
|
|
289
|
+
...process.env,
|
|
290
|
+
...(options.env || {}),
|
|
291
|
+
DEBIAN_FRONTEND: 'noninteractive',
|
|
292
|
+
SUDO_ASKPASS: '/bin/false',
|
|
293
|
+
SUDO_PROMPT: '',
|
|
294
|
+
},
|
|
155
295
|
});
|
|
156
296
|
let stdout = '';
|
|
157
297
|
let stderr = '';
|
|
@@ -388,7 +528,8 @@ function isPortableCommandDefault(sourceName, command) {
|
|
|
388
528
|
const expected = getDefaultSourceCommand(sourceName);
|
|
389
529
|
if (!expected)
|
|
390
530
|
return false;
|
|
391
|
-
|
|
531
|
+
const trimmed = String(command || '').trim();
|
|
532
|
+
return trimmed.startsWith(expected) || replaceLegacyRuntimeScriptCommand(trimmed) !== trimmed;
|
|
392
533
|
}
|
|
393
534
|
function truncate(value, max = 240) {
|
|
394
535
|
const text = String(value || '');
|
|
@@ -562,9 +703,47 @@ async function testRevenueCatConnection(revenuecatToken, timeoutMs) {
|
|
|
562
703
|
};
|
|
563
704
|
}
|
|
564
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
|
+
}
|
|
565
744
|
function describeAnalyticsConnectionFailure(detail, analyticsTokenEnv, hasAnalyticsToken) {
|
|
566
745
|
if (!hasAnalyticsToken) {
|
|
567
|
-
return `AnalyticsCLI needs query access. Run \`
|
|
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}`;
|
|
568
747
|
}
|
|
569
748
|
return `AnalyticsCLI connection failed with \`${analyticsTokenEnv}\` set. Verify that the pasted readonly CLI token is current and has project access. Raw error: ${detail}`;
|
|
570
749
|
}
|
|
@@ -601,6 +780,62 @@ async function testSentryConnection(sentryToken, timeoutMs, baseUrl = 'https://s
|
|
|
601
780
|
};
|
|
602
781
|
}
|
|
603
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
|
+
}
|
|
604
839
|
function normalizeSentryAccounts(config, sentryTokenEnv) {
|
|
605
840
|
const sentrySource = config?.sources?.sentry;
|
|
606
841
|
const accounts = Array.isArray(sentrySource?.accounts) ? sentrySource.accounts : [];
|
|
@@ -612,6 +847,13 @@ function normalizeSentryAccounts(config, sentryTokenEnv) {
|
|
|
612
847
|
label: String(account?.label || account?.name || account?.id || `Sentry ${index + 1}`).trim(),
|
|
613
848
|
tokenEnv: String(account?.tokenEnv || account?.token_env || account?.secretEnv || sentryTokenEnv).trim(),
|
|
614
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(),
|
|
615
857
|
}));
|
|
616
858
|
}
|
|
617
859
|
return [
|
|
@@ -620,9 +862,24 @@ function normalizeSentryAccounts(config, sentryTokenEnv) {
|
|
|
620
862
|
label: 'Sentry',
|
|
621
863
|
tokenEnv: sentryTokenEnv,
|
|
622
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(),
|
|
623
868
|
},
|
|
624
869
|
];
|
|
625
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
|
+
}
|
|
626
883
|
async function testGitHubConnection(githubToken, githubRepo, timeoutMs, actionMode) {
|
|
627
884
|
if (!githubToken) {
|
|
628
885
|
return {
|
|
@@ -727,11 +984,14 @@ async function testCommandSourceJson(command, cwd = process.cwd()) {
|
|
|
727
984
|
function onlyAllows(onlyConnectors, connector) {
|
|
728
985
|
return !Array.isArray(onlyConnectors) || onlyConnectors.length === 0 || onlyConnectors.includes(connector);
|
|
729
986
|
}
|
|
730
|
-
async function runConnectionChecks({ checks, config, timeoutMs, progressJson = false, onlyConnectors = [] }) {
|
|
987
|
+
async function runConnectionChecks({ checks, config, configPath, timeoutMs, progressJson = false, onlyConnectors = [] }) {
|
|
731
988
|
const tasks = [];
|
|
732
989
|
const analyticsTokenEnv = getSecretName(config, 'analyticsTokenEnv', 'ANALYTICSCLI_ACCESS_TOKEN');
|
|
733
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');
|
|
734
993
|
const sentryTokenEnv = getSecretName(config, 'sentryTokenEnv', 'SENTRY_AUTH_TOKEN');
|
|
994
|
+
const coolifyTokenEnv = getSecretName(config, 'coolifyTokenEnv', 'COOLIFY_API_TOKEN');
|
|
735
995
|
const feedbackTokenEnv = getSecretName(config, 'feedbackTokenEnv', 'FEEDBACK_API_TOKEN');
|
|
736
996
|
const githubTokenEnv = getSecretName(config, 'githubTokenEnv', 'GITHUB_TOKEN');
|
|
737
997
|
const githubRepo = isConfiguredGitHubRepo(config?.project?.githubRepo)
|
|
@@ -755,7 +1015,7 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
755
1015
|
? analyticsConnection.detail
|
|
756
1016
|
: describeAnalyticsConnectionFailure(analyticsConnection.detail, analyticsTokenEnv, hasAnalyticsToken), analyticsConnection.ok ? 'pass' : analyticsSource?.mode === 'command' ? 'fail' : 'warn');
|
|
757
1017
|
if (analyticsSource?.mode === 'command') {
|
|
758
|
-
const command = String(analyticsSource.command || '').trim();
|
|
1018
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(analyticsSource.command || '').trim()), configPath);
|
|
759
1019
|
if (!command) {
|
|
760
1020
|
addCheck(checks, 'connection:analytics-command', false, 'analytics source uses command mode but no command configured');
|
|
761
1021
|
}
|
|
@@ -798,6 +1058,81 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
798
1058
|
},
|
|
799
1059
|
});
|
|
800
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
|
+
}
|
|
801
1136
|
const sentrySource = config.sources?.sentry;
|
|
802
1137
|
if (onlyAllows(onlyConnectors, 'sentry')) {
|
|
803
1138
|
scheduleProgressGroup(tasks, checks, progressJson, {
|
|
@@ -810,17 +1145,18 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
810
1145
|
for (const account of sentryAccounts) {
|
|
811
1146
|
const token = process.env[account.tokenEnv] || '';
|
|
812
1147
|
const checkName = sentryAccounts.length > 1 ? `connection:sentry:${account.key}` : 'connection:sentry';
|
|
1148
|
+
const accountTarget = describeSentryAccountTarget(account);
|
|
813
1149
|
if (!token) {
|
|
814
|
-
addCheck(groupChecks, checkName, false, `${account.tokenEnv} missing (required for live Sentry API test for ${
|
|
1150
|
+
addCheck(groupChecks, checkName, false, `${account.tokenEnv} missing (required for live Sentry API test for ${accountTarget})`, sentrySource?.mode === 'command' ? 'fail' : 'warn');
|
|
815
1151
|
continue;
|
|
816
1152
|
}
|
|
817
1153
|
const sentryConnection = await testSentryConnection(token, timeoutMs, account.baseUrl);
|
|
818
1154
|
addCheck(groupChecks, checkName, sentryConnection.ok, sentryConnection.ok
|
|
819
|
-
? `${
|
|
820
|
-
: `${
|
|
1155
|
+
? `${accountTarget} auth check passed (${sentryConnection.detail})`
|
|
1156
|
+
: `${accountTarget} auth check failed (${sentryConnection.detail})`);
|
|
821
1157
|
}
|
|
822
1158
|
if (sentrySource?.mode === 'command') {
|
|
823
|
-
const command = String(sentrySource.command || '').trim();
|
|
1159
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(sentrySource.command || '').trim()), configPath);
|
|
824
1160
|
if (!command) {
|
|
825
1161
|
addCheck(groupChecks, 'connection:sentry-command', false, 'sentry source uses command mode but no command configured');
|
|
826
1162
|
}
|
|
@@ -828,12 +1164,56 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
828
1164
|
const commandCheck = await testCommandSourceJson(`${command} --limit 1 --max-signals 1 --last 24h`, commandCwd);
|
|
829
1165
|
addCheck(groupChecks, 'connection:sentry-command', commandCheck.ok, commandCheck.ok
|
|
830
1166
|
? 'Sentry command smoke test passed'
|
|
831
|
-
: `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})`);
|
|
832
1212
|
}
|
|
833
1213
|
}
|
|
834
1214
|
}
|
|
835
1215
|
else {
|
|
836
|
-
addCheck(groupChecks, 'connection:
|
|
1216
|
+
addCheck(groupChecks, 'connection:coolify', true, 'source disabled');
|
|
837
1217
|
}
|
|
838
1218
|
},
|
|
839
1219
|
});
|
|
@@ -843,7 +1223,7 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
843
1223
|
// Skip feedback during focused connector checks.
|
|
844
1224
|
}
|
|
845
1225
|
else if (sourceEnabled(config, 'feedback') && feedbackSource?.mode === 'command') {
|
|
846
|
-
const command = String(feedbackSource.command || '').trim();
|
|
1226
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(feedbackSource.command || '').trim()), configPath);
|
|
847
1227
|
if (!command) {
|
|
848
1228
|
addCheck(checks, 'connection:feedback', false, 'feedback source uses command mode but no command configured');
|
|
849
1229
|
}
|
|
@@ -867,13 +1247,20 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
867
1247
|
}
|
|
868
1248
|
for (const extraSource of getAllSourceEntries(config).filter((source) => !source.builtIn)) {
|
|
869
1249
|
const serviceKind = classifyServiceKind(extraSource.service || extraSource.key);
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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;
|
|
877
1264
|
if (!onlyAllows(onlyConnectors, connectorKind))
|
|
878
1265
|
continue;
|
|
879
1266
|
const checkName = `connection:${extraSource.key}`;
|
|
@@ -882,13 +1269,13 @@ async function runConnectionChecks({ checks, config, timeoutMs, progressJson = f
|
|
|
882
1269
|
continue;
|
|
883
1270
|
}
|
|
884
1271
|
if (extraSource.mode === 'command') {
|
|
885
|
-
const command = String(extraSource.command || '').trim();
|
|
1272
|
+
const command = withActiveConfigArg(replaceLegacyRuntimeScriptCommand(String(extraSource.command || '').trim()), configPath);
|
|
886
1273
|
if (!command) {
|
|
887
1274
|
addCheck(checks, checkName, false, 'source uses command mode but no command configured');
|
|
888
1275
|
continue;
|
|
889
1276
|
}
|
|
890
1277
|
const smokeCommand = connectorKind === 'asc' && command.includes('export-asc-summary')
|
|
891
|
-
? `${command} --
|
|
1278
|
+
? `${command} --reviews-limit 1 --feedback-limit 1 --max-signals 1`
|
|
892
1279
|
: command;
|
|
893
1280
|
const commandCheck = await testCommandSourceJson(smokeCommand, commandCwd);
|
|
894
1281
|
addCheck(checks, checkName, commandCheck.ok, commandCheck.ok
|
|
@@ -999,7 +1386,7 @@ async function main() {
|
|
|
999
1386
|
continue;
|
|
1000
1387
|
}
|
|
1001
1388
|
if (source.mode === 'command') {
|
|
1002
|
-
const command = String(source.command || '').trim();
|
|
1389
|
+
const command = replaceLegacyRuntimeScriptCommand(String(source.command || '').trim());
|
|
1003
1390
|
if (!command) {
|
|
1004
1391
|
addCheck(checks, `source:${sourceName}:command`, false, 'mode=command but no command configured');
|
|
1005
1392
|
continue;
|
|
@@ -1020,6 +1407,26 @@ async function main() {
|
|
|
1020
1407
|
const hasRevenuecatToken = Boolean(process.env[revenuecatTokenEnv]);
|
|
1021
1408
|
addCheck(checks, `secret:${revenuecatTokenEnv}`, hasRevenuecatToken, hasRevenuecatToken ? 'set (required for RevenueCat command mode)' : 'missing (required for RevenueCat command mode)');
|
|
1022
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
|
+
}
|
|
1023
1430
|
if (sourceName === 'sentry') {
|
|
1024
1431
|
const sentryTokenEnv = getSecretName(config, 'sentryTokenEnv', 'SENTRY_AUTH_TOKEN');
|
|
1025
1432
|
for (const account of normalizeSentryAccounts(config, sentryTokenEnv)) {
|
|
@@ -1029,6 +1436,14 @@ async function main() {
|
|
|
1029
1436
|
: `missing (required for ${account.label} Sentry command mode)`);
|
|
1030
1437
|
}
|
|
1031
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
|
+
}
|
|
1032
1447
|
if (!source.builtIn && source.secretEnv) {
|
|
1033
1448
|
const hasConnectorToken = Boolean(process.env[source.secretEnv]);
|
|
1034
1449
|
addCheck(checks, `secret:${source.secretEnv}`, hasConnectorToken, hasConnectorToken
|
|
@@ -1081,6 +1496,7 @@ async function main() {
|
|
|
1081
1496
|
await runConnectionChecks({
|
|
1082
1497
|
checks,
|
|
1083
1498
|
config,
|
|
1499
|
+
configPath,
|
|
1084
1500
|
progressJson: args.progressJson,
|
|
1085
1501
|
timeoutMs: args.timeoutMs,
|
|
1086
1502
|
onlyConnectors: args.onlyConnectors,
|