@analyticscli/growth-engineer 0.1.1-preview.6 → 0.1.1-preview.7
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/runtime/export-paddle-summary.mjs +72 -18
- package/dist/runtime/export-paddle-summary.mjs.map +1 -1
- package/dist/runtime/openclaw-exporters-lib.d.mts +17 -0
- package/dist/runtime/openclaw-exporters-lib.mjs +64 -0
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +47 -15
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +200 -21
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -906,7 +906,9 @@ function sourceCommandNeedsActiveConfig(sourceName, command) {
|
|
|
906
906
|
const value = String(command || '').toLowerCase();
|
|
907
907
|
return (normalized === 'sentry' ||
|
|
908
908
|
normalized === 'glitchtip' ||
|
|
909
|
+
normalized === 'paddle' ||
|
|
909
910
|
normalized === 'coolify' ||
|
|
911
|
+
value.includes('export-paddle-summary') ||
|
|
910
912
|
value.includes('export-sentry-summary') ||
|
|
911
913
|
value.includes('export-coolify-summary') ||
|
|
912
914
|
value.includes('exporters coolify-summary'));
|
|
@@ -2451,10 +2453,13 @@ async function saveSecretsImmediately(secrets) {
|
|
|
2451
2453
|
process.stdout.write(`Saved local secrets to ${secretsFile} with chmod 600.\n`);
|
|
2452
2454
|
return true;
|
|
2453
2455
|
}
|
|
2454
|
-
async function runImmediateConnectorHealthCheck({ rl, configPath, connector, secrets, sentryAccounts = [], }) {
|
|
2456
|
+
async function runImmediateConnectorHealthCheck({ rl, configPath, connector, secrets, sentryAccounts = [], paddleAccounts = [], }) {
|
|
2455
2457
|
if (connector === 'sentry' && sentryAccounts.length > 0) {
|
|
2456
2458
|
await upsertSentryAccountsConfig(configPath, sentryAccounts);
|
|
2457
2459
|
}
|
|
2460
|
+
if (connector === 'paddle' && paddleAccounts.length > 0) {
|
|
2461
|
+
await upsertPaddleAccountsConfig(configPath, paddleAccounts);
|
|
2462
|
+
}
|
|
2458
2463
|
await saveSecretsImmediately(secrets);
|
|
2459
2464
|
const env = {
|
|
2460
2465
|
...process.env,
|
|
@@ -3133,6 +3138,71 @@ async function verifySentryAccountsConfig(configPath, expectedAccounts) {
|
|
|
3133
3138
|
}
|
|
3134
3139
|
return { ok: true, detail: `${realAccounts.length} active Sentry-compatible account(s) configured` };
|
|
3135
3140
|
}
|
|
3141
|
+
async function upsertPaddleAccountsConfig(configPath, accounts) {
|
|
3142
|
+
if (!accounts.length || !(await fileExists(configPath)))
|
|
3143
|
+
return false;
|
|
3144
|
+
const config = await readJsonFile(configPath);
|
|
3145
|
+
const existingAccounts = Array.isArray(config?.sources?.paddle?.accounts)
|
|
3146
|
+
? config.sources.paddle.accounts
|
|
3147
|
+
: [];
|
|
3148
|
+
const merged = new Map();
|
|
3149
|
+
for (const account of existingAccounts) {
|
|
3150
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3151
|
+
if (id)
|
|
3152
|
+
merged.set(id, account);
|
|
3153
|
+
}
|
|
3154
|
+
for (const account of accounts) {
|
|
3155
|
+
merged.set(account.id, {
|
|
3156
|
+
...(merged.get(account.id) || {}),
|
|
3157
|
+
...account,
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
const tokenEnv = accounts[0]?.tokenEnv || config?.sources?.paddle?.tokenEnv || config?.secrets?.paddleTokenEnv || 'PADDLE_API_KEY';
|
|
3161
|
+
config.sources = {
|
|
3162
|
+
...(config.sources || {}),
|
|
3163
|
+
paddle: {
|
|
3164
|
+
...(config.sources?.paddle || {}),
|
|
3165
|
+
enabled: true,
|
|
3166
|
+
mode: 'command',
|
|
3167
|
+
command: normalizeWizardSourceCommand('paddle', config.sources?.paddle || {}, configPath),
|
|
3168
|
+
environment: config.sources?.paddle?.environment || 'live',
|
|
3169
|
+
tokenEnv,
|
|
3170
|
+
accounts: [...merged.values()],
|
|
3171
|
+
},
|
|
3172
|
+
};
|
|
3173
|
+
config.secrets = {
|
|
3174
|
+
...(config.secrets || {}),
|
|
3175
|
+
paddleTokenEnv: tokenEnv,
|
|
3176
|
+
paddleTokenRef: { source: 'env', provider: 'default', id: tokenEnv },
|
|
3177
|
+
};
|
|
3178
|
+
await writeJsonFile(configPath, config);
|
|
3179
|
+
return true;
|
|
3180
|
+
}
|
|
3181
|
+
async function verifyPaddleAccountsConfig(configPath, expectedAccounts) {
|
|
3182
|
+
if (!(await fileExists(configPath))) {
|
|
3183
|
+
return { ok: false, detail: `${configPath} does not exist` };
|
|
3184
|
+
}
|
|
3185
|
+
const config = await readJsonFile(configPath);
|
|
3186
|
+
const source = config?.sources?.paddle;
|
|
3187
|
+
if (!source || source.enabled !== true) {
|
|
3188
|
+
return { ok: false, detail: 'sources.paddle.enabled is not true' };
|
|
3189
|
+
}
|
|
3190
|
+
if (source.mode !== 'command') {
|
|
3191
|
+
return { ok: false, detail: 'sources.paddle.mode is not command' };
|
|
3192
|
+
}
|
|
3193
|
+
const configuredAccounts = Array.isArray(source.accounts) ? source.accounts : [];
|
|
3194
|
+
if (configuredAccounts.length === 0) {
|
|
3195
|
+
return { ok: false, detail: 'sources.paddle.accounts contains no account' };
|
|
3196
|
+
}
|
|
3197
|
+
const configuredIds = new Set(configuredAccounts.map((account) => String(account?.id || account?.key || '').trim()).filter(Boolean));
|
|
3198
|
+
const missingIds = expectedAccounts
|
|
3199
|
+
.map((account) => String(account?.id || '').trim())
|
|
3200
|
+
.filter((id) => id && !configuredIds.has(id));
|
|
3201
|
+
if (missingIds.length > 0) {
|
|
3202
|
+
return { ok: false, detail: `sources.paddle.accounts is missing configured account id(s): ${missingIds.join(', ')}` };
|
|
3203
|
+
}
|
|
3204
|
+
return { ok: true, detail: `${configuredAccounts.length} Paddle account(s) configured` };
|
|
3205
|
+
}
|
|
3136
3206
|
async function upsertCoolifyConfig(configPath, { baseUrl, tokenEnv = 'COOLIFY_API_TOKEN' }) {
|
|
3137
3207
|
if (!(await fileExists(configPath)))
|
|
3138
3208
|
return false;
|
|
@@ -3430,6 +3500,21 @@ async function guideRevenueCatConnector(rl, secrets) {
|
|
|
3430
3500
|
if (apiKey)
|
|
3431
3501
|
secrets.REVENUECAT_API_KEY = apiKey;
|
|
3432
3502
|
}
|
|
3503
|
+
function paddleAccountIdFromLabel(label, index) {
|
|
3504
|
+
const normalized = String(label || '')
|
|
3505
|
+
.trim()
|
|
3506
|
+
.toLowerCase()
|
|
3507
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
3508
|
+
.replace(/^_+|_+$/g, '');
|
|
3509
|
+
return normalized || `paddle_${index + 1}`;
|
|
3510
|
+
}
|
|
3511
|
+
function paddleTokenEnvForAccount(index, label) {
|
|
3512
|
+
if (index === 0)
|
|
3513
|
+
return 'PADDLE_API_KEY';
|
|
3514
|
+
const suffix = paddleAccountIdFromLabel(label, index).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
3515
|
+
const base = suffix && suffix !== `PADDLE_${index + 1}` ? `PADDLE_API_KEY_${suffix}` : `PADDLE_API_KEY_${index + 1}`;
|
|
3516
|
+
return base.replace(/_+/g, '_');
|
|
3517
|
+
}
|
|
3433
3518
|
async function guidePaddleConnector(rl, secrets) {
|
|
3434
3519
|
printSection('Paddle Billing metrics', [
|
|
3435
3520
|
'Use this when OpenClaw should read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
|
|
@@ -3442,9 +3527,28 @@ async function guidePaddleConnector(rl, secrets) {
|
|
|
3442
3527
|
'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
|
|
3443
3528
|
'Paste the key here so it is stored only in the local chmod 600 secrets file.',
|
|
3444
3529
|
]);
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
|
|
3530
|
+
const accounts = [];
|
|
3531
|
+
let index = 0;
|
|
3532
|
+
while (true) {
|
|
3533
|
+
const label = await ask(rl, index === 0 ? 'Paddle account label' : 'Next Paddle account label (empty = done)', index === 0 ? 'Paddle' : '');
|
|
3534
|
+
if (!label.trim())
|
|
3535
|
+
break;
|
|
3536
|
+
const tokenEnv = paddleTokenEnvForAccount(index, label);
|
|
3537
|
+
const apiKey = await maybePromptSecret(rl, `Paste ${tokenEnv} into this local terminal`, tokenEnv);
|
|
3538
|
+
if (apiKey)
|
|
3539
|
+
secrets[tokenEnv] = apiKey;
|
|
3540
|
+
accounts.push({
|
|
3541
|
+
id: paddleAccountIdFromLabel(label, index),
|
|
3542
|
+
label: label.trim(),
|
|
3543
|
+
tokenEnv,
|
|
3544
|
+
environment: 'live',
|
|
3545
|
+
});
|
|
3546
|
+
index += 1;
|
|
3547
|
+
const addAnother = await askYesNo(rl, 'Add another Paddle account?', false);
|
|
3548
|
+
if (!addAnother)
|
|
3549
|
+
break;
|
|
3550
|
+
}
|
|
3551
|
+
return accounts;
|
|
3448
3552
|
}
|
|
3449
3553
|
async function guideSeoConnector(rl, secrets) {
|
|
3450
3554
|
printSection('SEO / Google Search Console / DataForSEO', [
|
|
@@ -3476,10 +3580,15 @@ async function guideSeoConnector(rl, secrets) {
|
|
|
3476
3580
|
secrets.DATAFORSEO_PASSWORD = password;
|
|
3477
3581
|
}
|
|
3478
3582
|
}
|
|
3479
|
-
function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
3583
|
+
function buildAccountSignalExtraSourceConfig(key, existing = {}, accounts = []) {
|
|
3480
3584
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3481
3585
|
if (!definition)
|
|
3482
3586
|
return existing;
|
|
3587
|
+
const accountConfig = accounts.length > 0
|
|
3588
|
+
? {
|
|
3589
|
+
accounts: mergeConnectorAccounts(existing.accounts, accounts),
|
|
3590
|
+
}
|
|
3591
|
+
: {};
|
|
3483
3592
|
return {
|
|
3484
3593
|
...buildExtraSourceConfig(definition.service, {
|
|
3485
3594
|
key: definition.key,
|
|
@@ -3503,9 +3612,28 @@ function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
|
3503
3612
|
signalKind: definition.sourceKind,
|
|
3504
3613
|
experimental: Boolean(definition.experimental),
|
|
3505
3614
|
hint: existing.hint || definition.signalHint,
|
|
3615
|
+
...accountConfig,
|
|
3506
3616
|
};
|
|
3507
3617
|
}
|
|
3508
|
-
|
|
3618
|
+
function mergeConnectorAccounts(existingAccounts, nextAccounts) {
|
|
3619
|
+
const merged = new Map();
|
|
3620
|
+
for (const account of Array.isArray(existingAccounts) ? existingAccounts : []) {
|
|
3621
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3622
|
+
if (id)
|
|
3623
|
+
merged.set(id, account);
|
|
3624
|
+
}
|
|
3625
|
+
for (const account of nextAccounts) {
|
|
3626
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3627
|
+
if (!id)
|
|
3628
|
+
continue;
|
|
3629
|
+
merged.set(id, {
|
|
3630
|
+
...(merged.get(id) || {}),
|
|
3631
|
+
...account,
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
return [...merged.values()];
|
|
3635
|
+
}
|
|
3636
|
+
async function upsertAccountSignalConnectorConfig(configPath, key, accounts = []) {
|
|
3509
3637
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3510
3638
|
if (!definition)
|
|
3511
3639
|
return false;
|
|
@@ -3514,7 +3642,7 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
|
3514
3642
|
const extra = Array.isArray(sources.extra) ? sources.extra : [];
|
|
3515
3643
|
const nextExtra = extra.filter((source) => String(source?.key || source?.service || '') !== definition.key);
|
|
3516
3644
|
const existing = extra.find((source) => String(source?.key || source?.service || '') === definition.key) || {};
|
|
3517
|
-
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing));
|
|
3645
|
+
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing, accounts));
|
|
3518
3646
|
config.sources = {
|
|
3519
3647
|
...sources,
|
|
3520
3648
|
extra: nextExtra,
|
|
@@ -3522,28 +3650,58 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
|
3522
3650
|
await writeJsonFile(configPath, config);
|
|
3523
3651
|
return true;
|
|
3524
3652
|
}
|
|
3653
|
+
function accountSignalTokenEnvForAccount(baseEnv, key, index, label) {
|
|
3654
|
+
if (index === 0)
|
|
3655
|
+
return baseEnv;
|
|
3656
|
+
const suffix = toConfigId(label || key, `${key}_${index + 1}`).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
3657
|
+
return `${baseEnv}_${suffix}`.replace(/_+/g, '_');
|
|
3658
|
+
}
|
|
3525
3659
|
async function guideAccountSignalConnector(rl, secrets, key) {
|
|
3526
3660
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3527
3661
|
if (!definition)
|
|
3528
|
-
return;
|
|
3662
|
+
return [];
|
|
3529
3663
|
printSection(definition.label, [
|
|
3530
3664
|
definition.summary,
|
|
3531
3665
|
'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.',
|
|
3532
3666
|
]);
|
|
3533
3667
|
process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
|
|
3534
3668
|
printBullets(definition.steps);
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3669
|
+
const accounts = [];
|
|
3670
|
+
let index = 0;
|
|
3671
|
+
while (true) {
|
|
3672
|
+
const label = await ask(rl, index === 0 ? `${definition.label} account label` : `Next ${definition.label} account label (empty = done)`, index === 0 ? definition.label.replace(/\s+\(experimental\)$/i, '') : '');
|
|
3673
|
+
if (!label.trim())
|
|
3674
|
+
break;
|
|
3675
|
+
const credentialEnvs = {};
|
|
3676
|
+
for (const credential of definition.credentials) {
|
|
3677
|
+
const envName = accountSignalTokenEnvForAccount(credential.env, key, index, label);
|
|
3678
|
+
const defaultValue = index === 0 ? credential.defaultValue ?? process.env[credential.env] ?? '' : '';
|
|
3679
|
+
const prompt = envName === credential.env ? credential.prompt : `${credential.prompt.replace(credential.env, envName)}`;
|
|
3680
|
+
const value = credential.optional
|
|
3681
|
+
? await maybePromptSecret(rl, prompt, envName)
|
|
3682
|
+
: await maybePromptSecret(rl, prompt, envName);
|
|
3683
|
+
const finalValue = value || defaultValue;
|
|
3684
|
+
credentialEnvs[credential.env] = envName;
|
|
3685
|
+
if (finalValue)
|
|
3686
|
+
secrets[envName] = finalValue;
|
|
3687
|
+
else if (!credential.optional) {
|
|
3688
|
+
process.stdout.write(`${envName} was not saved. ${definition.label} setup remains pending for ${label}; rerun this wizard when ready.\n`);
|
|
3689
|
+
}
|
|
3545
3690
|
}
|
|
3691
|
+
accounts.push({
|
|
3692
|
+
id: toConfigId(label, `${key}_${index + 1}`),
|
|
3693
|
+
label: label.trim(),
|
|
3694
|
+
credentialEnvs,
|
|
3695
|
+
tokenEnv: credentialEnvs[definition.credentials[0]?.env] || definition.credentials[0]?.env || null,
|
|
3696
|
+
accountWide: true,
|
|
3697
|
+
projectScope: 'discover_from_account',
|
|
3698
|
+
});
|
|
3699
|
+
index += 1;
|
|
3700
|
+
const addAnother = await askYesNo(rl, `Add another ${definition.label} account?`, false);
|
|
3701
|
+
if (!addAnother)
|
|
3702
|
+
break;
|
|
3546
3703
|
}
|
|
3704
|
+
return accounts;
|
|
3547
3705
|
}
|
|
3548
3706
|
async function guideSentryConnector(rl, secrets) {
|
|
3549
3707
|
printSection('Sentry / GlitchTip', [
|
|
@@ -3852,6 +4010,7 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3852
4010
|
process.stdout.write('\n');
|
|
3853
4011
|
const secrets = {};
|
|
3854
4012
|
let sentryAccounts = [];
|
|
4013
|
+
let paddleAccounts = [];
|
|
3855
4014
|
let coolifyConfig = null;
|
|
3856
4015
|
if (selected.includes('analytics')) {
|
|
3857
4016
|
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
@@ -3900,12 +4059,13 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3900
4059
|
if (selected.includes('paddle')) {
|
|
3901
4060
|
while (true) {
|
|
3902
4061
|
clearTerminal();
|
|
3903
|
-
await guidePaddleConnector(rl, secrets);
|
|
4062
|
+
paddleAccounts = await guidePaddleConnector(rl, secrets);
|
|
3904
4063
|
const check = await runImmediateConnectorHealthCheck({
|
|
3905
4064
|
rl,
|
|
3906
4065
|
configPath: args.config,
|
|
3907
4066
|
connector: 'paddle',
|
|
3908
4067
|
secrets,
|
|
4068
|
+
paddleAccounts,
|
|
3909
4069
|
});
|
|
3910
4070
|
if (!check.retry)
|
|
3911
4071
|
break;
|
|
@@ -3974,8 +4134,8 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3974
4134
|
for (const connector of selected.filter(isAccountSignalConnector)) {
|
|
3975
4135
|
while (true) {
|
|
3976
4136
|
clearTerminal();
|
|
3977
|
-
await guideAccountSignalConnector(rl, secrets, connector);
|
|
3978
|
-
await upsertAccountSignalConnectorConfig(args.config, connector);
|
|
4137
|
+
const accountSignalAccounts = await guideAccountSignalConnector(rl, secrets, connector);
|
|
4138
|
+
await upsertAccountSignalConnectorConfig(args.config, connector, accountSignalAccounts);
|
|
3979
4139
|
const check = await runImmediateConnectorHealthCheck({
|
|
3980
4140
|
rl,
|
|
3981
4141
|
configPath: args.config,
|
|
@@ -4002,6 +4162,12 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
4002
4162
|
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
4003
4163
|
}
|
|
4004
4164
|
}
|
|
4165
|
+
if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
|
|
4166
|
+
const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
|
|
4167
|
+
if (readiness.ok) {
|
|
4168
|
+
process.stdout.write(`Configured ${paddleAccounts.length} Paddle account(s) in ${args.config}.\n`);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4005
4171
|
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
4006
4172
|
process.stdout.write(`Configured Coolify monitoring for ${coolifyConfig.baseUrl} in ${args.config}.\n`);
|
|
4007
4173
|
}
|
|
@@ -4026,6 +4192,19 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
4026
4192
|
});
|
|
4027
4193
|
}
|
|
4028
4194
|
}
|
|
4195
|
+
if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
|
|
4196
|
+
const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
|
|
4197
|
+
if (readiness.ok) {
|
|
4198
|
+
process.stdout.write(`Paddle account config is up to date in ${args.config}.\n`);
|
|
4199
|
+
}
|
|
4200
|
+
else {
|
|
4201
|
+
postSetupBlockers.push({
|
|
4202
|
+
check: 'connection:paddle',
|
|
4203
|
+
detail: readiness.detail,
|
|
4204
|
+
remediation: 'Rerun Paddle setup so the active config persists sources.paddle.enabled=true and sources.paddle.accounts[].',
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4029
4208
|
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
4030
4209
|
process.stdout.write(`Coolify config is up to date in ${args.config}.\n`);
|
|
4031
4210
|
}
|