@analyticscli/growth-engineer 0.1.1-preview.6 → 0.1.1-preview.8
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/index.js +1 -1
- package/dist/index.js.map +1 -1
- 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 +203 -22
- 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.',
|
|
@@ -3438,13 +3523,34 @@ async function guidePaddleConnector(rl, secrets) {
|
|
|
3438
3523
|
printBullets([
|
|
3439
3524
|
'Open Paddle > Developer Tools > Authentication.',
|
|
3440
3525
|
'Use the API keys tab and create a new live API key.',
|
|
3441
|
-
'
|
|
3526
|
+
'Minimum: grant `metrics.read` so account-level revenue, MRR, refunds, chargebacks, subscribers, and checkout conversion work.',
|
|
3527
|
+
'Recommended for better Growth Engineer analysis: grant all available read-only permissions (`*.read`), including products, prices, discounts, customers, transactions, subscriptions, adjustments, reports, and notifications.',
|
|
3528
|
+
'Do not grant any write permissions (`*.write`) unless another workflow explicitly needs them.',
|
|
3442
3529
|
'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
|
|
3443
3530
|
'Paste the key here so it is stored only in the local chmod 600 secrets file.',
|
|
3444
3531
|
]);
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
|
|
3532
|
+
const accounts = [];
|
|
3533
|
+
let index = 0;
|
|
3534
|
+
while (true) {
|
|
3535
|
+
const label = await ask(rl, index === 0 ? 'Paddle account label' : 'Next Paddle account label (empty = done)', index === 0 ? 'Paddle' : '');
|
|
3536
|
+
if (!label.trim())
|
|
3537
|
+
break;
|
|
3538
|
+
const tokenEnv = paddleTokenEnvForAccount(index, label);
|
|
3539
|
+
const apiKey = await maybePromptSecret(rl, `Paste ${tokenEnv} into this local terminal`, tokenEnv);
|
|
3540
|
+
if (apiKey)
|
|
3541
|
+
secrets[tokenEnv] = apiKey;
|
|
3542
|
+
accounts.push({
|
|
3543
|
+
id: paddleAccountIdFromLabel(label, index),
|
|
3544
|
+
label: label.trim(),
|
|
3545
|
+
tokenEnv,
|
|
3546
|
+
environment: 'live',
|
|
3547
|
+
});
|
|
3548
|
+
index += 1;
|
|
3549
|
+
const addAnother = await askYesNo(rl, 'Add another Paddle account?', false);
|
|
3550
|
+
if (!addAnother)
|
|
3551
|
+
break;
|
|
3552
|
+
}
|
|
3553
|
+
return accounts;
|
|
3448
3554
|
}
|
|
3449
3555
|
async function guideSeoConnector(rl, secrets) {
|
|
3450
3556
|
printSection('SEO / Google Search Console / DataForSEO', [
|
|
@@ -3476,10 +3582,15 @@ async function guideSeoConnector(rl, secrets) {
|
|
|
3476
3582
|
secrets.DATAFORSEO_PASSWORD = password;
|
|
3477
3583
|
}
|
|
3478
3584
|
}
|
|
3479
|
-
function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
3585
|
+
function buildAccountSignalExtraSourceConfig(key, existing = {}, accounts = []) {
|
|
3480
3586
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3481
3587
|
if (!definition)
|
|
3482
3588
|
return existing;
|
|
3589
|
+
const accountConfig = accounts.length > 0
|
|
3590
|
+
? {
|
|
3591
|
+
accounts: mergeConnectorAccounts(existing.accounts, accounts),
|
|
3592
|
+
}
|
|
3593
|
+
: {};
|
|
3483
3594
|
return {
|
|
3484
3595
|
...buildExtraSourceConfig(definition.service, {
|
|
3485
3596
|
key: definition.key,
|
|
@@ -3503,9 +3614,28 @@ function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
|
3503
3614
|
signalKind: definition.sourceKind,
|
|
3504
3615
|
experimental: Boolean(definition.experimental),
|
|
3505
3616
|
hint: existing.hint || definition.signalHint,
|
|
3617
|
+
...accountConfig,
|
|
3506
3618
|
};
|
|
3507
3619
|
}
|
|
3508
|
-
|
|
3620
|
+
function mergeConnectorAccounts(existingAccounts, nextAccounts) {
|
|
3621
|
+
const merged = new Map();
|
|
3622
|
+
for (const account of Array.isArray(existingAccounts) ? existingAccounts : []) {
|
|
3623
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3624
|
+
if (id)
|
|
3625
|
+
merged.set(id, account);
|
|
3626
|
+
}
|
|
3627
|
+
for (const account of nextAccounts) {
|
|
3628
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3629
|
+
if (!id)
|
|
3630
|
+
continue;
|
|
3631
|
+
merged.set(id, {
|
|
3632
|
+
...(merged.get(id) || {}),
|
|
3633
|
+
...account,
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
return [...merged.values()];
|
|
3637
|
+
}
|
|
3638
|
+
async function upsertAccountSignalConnectorConfig(configPath, key, accounts = []) {
|
|
3509
3639
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3510
3640
|
if (!definition)
|
|
3511
3641
|
return false;
|
|
@@ -3514,7 +3644,7 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
|
3514
3644
|
const extra = Array.isArray(sources.extra) ? sources.extra : [];
|
|
3515
3645
|
const nextExtra = extra.filter((source) => String(source?.key || source?.service || '') !== definition.key);
|
|
3516
3646
|
const existing = extra.find((source) => String(source?.key || source?.service || '') === definition.key) || {};
|
|
3517
|
-
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing));
|
|
3647
|
+
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing, accounts));
|
|
3518
3648
|
config.sources = {
|
|
3519
3649
|
...sources,
|
|
3520
3650
|
extra: nextExtra,
|
|
@@ -3522,28 +3652,58 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
|
3522
3652
|
await writeJsonFile(configPath, config);
|
|
3523
3653
|
return true;
|
|
3524
3654
|
}
|
|
3655
|
+
function accountSignalTokenEnvForAccount(baseEnv, key, index, label) {
|
|
3656
|
+
if (index === 0)
|
|
3657
|
+
return baseEnv;
|
|
3658
|
+
const suffix = toConfigId(label || key, `${key}_${index + 1}`).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
3659
|
+
return `${baseEnv}_${suffix}`.replace(/_+/g, '_');
|
|
3660
|
+
}
|
|
3525
3661
|
async function guideAccountSignalConnector(rl, secrets, key) {
|
|
3526
3662
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3527
3663
|
if (!definition)
|
|
3528
|
-
return;
|
|
3664
|
+
return [];
|
|
3529
3665
|
printSection(definition.label, [
|
|
3530
3666
|
definition.summary,
|
|
3531
3667
|
'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.',
|
|
3532
3668
|
]);
|
|
3533
3669
|
process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
|
|
3534
3670
|
printBullets(definition.steps);
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3671
|
+
const accounts = [];
|
|
3672
|
+
let index = 0;
|
|
3673
|
+
while (true) {
|
|
3674
|
+
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, '') : '');
|
|
3675
|
+
if (!label.trim())
|
|
3676
|
+
break;
|
|
3677
|
+
const credentialEnvs = {};
|
|
3678
|
+
for (const credential of definition.credentials) {
|
|
3679
|
+
const envName = accountSignalTokenEnvForAccount(credential.env, key, index, label);
|
|
3680
|
+
const defaultValue = index === 0 ? credential.defaultValue ?? process.env[credential.env] ?? '' : '';
|
|
3681
|
+
const prompt = envName === credential.env ? credential.prompt : `${credential.prompt.replace(credential.env, envName)}`;
|
|
3682
|
+
const value = credential.optional
|
|
3683
|
+
? await maybePromptSecret(rl, prompt, envName)
|
|
3684
|
+
: await maybePromptSecret(rl, prompt, envName);
|
|
3685
|
+
const finalValue = value || defaultValue;
|
|
3686
|
+
credentialEnvs[credential.env] = envName;
|
|
3687
|
+
if (finalValue)
|
|
3688
|
+
secrets[envName] = finalValue;
|
|
3689
|
+
else if (!credential.optional) {
|
|
3690
|
+
process.stdout.write(`${envName} was not saved. ${definition.label} setup remains pending for ${label}; rerun this wizard when ready.\n`);
|
|
3691
|
+
}
|
|
3545
3692
|
}
|
|
3693
|
+
accounts.push({
|
|
3694
|
+
id: toConfigId(label, `${key}_${index + 1}`),
|
|
3695
|
+
label: label.trim(),
|
|
3696
|
+
credentialEnvs,
|
|
3697
|
+
tokenEnv: credentialEnvs[definition.credentials[0]?.env] || definition.credentials[0]?.env || null,
|
|
3698
|
+
accountWide: true,
|
|
3699
|
+
projectScope: 'discover_from_account',
|
|
3700
|
+
});
|
|
3701
|
+
index += 1;
|
|
3702
|
+
const addAnother = await askYesNo(rl, `Add another ${definition.label} account?`, false);
|
|
3703
|
+
if (!addAnother)
|
|
3704
|
+
break;
|
|
3546
3705
|
}
|
|
3706
|
+
return accounts;
|
|
3547
3707
|
}
|
|
3548
3708
|
async function guideSentryConnector(rl, secrets) {
|
|
3549
3709
|
printSection('Sentry / GlitchTip', [
|
|
@@ -3852,6 +4012,7 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3852
4012
|
process.stdout.write('\n');
|
|
3853
4013
|
const secrets = {};
|
|
3854
4014
|
let sentryAccounts = [];
|
|
4015
|
+
let paddleAccounts = [];
|
|
3855
4016
|
let coolifyConfig = null;
|
|
3856
4017
|
if (selected.includes('analytics')) {
|
|
3857
4018
|
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
@@ -3900,12 +4061,13 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3900
4061
|
if (selected.includes('paddle')) {
|
|
3901
4062
|
while (true) {
|
|
3902
4063
|
clearTerminal();
|
|
3903
|
-
await guidePaddleConnector(rl, secrets);
|
|
4064
|
+
paddleAccounts = await guidePaddleConnector(rl, secrets);
|
|
3904
4065
|
const check = await runImmediateConnectorHealthCheck({
|
|
3905
4066
|
rl,
|
|
3906
4067
|
configPath: args.config,
|
|
3907
4068
|
connector: 'paddle',
|
|
3908
4069
|
secrets,
|
|
4070
|
+
paddleAccounts,
|
|
3909
4071
|
});
|
|
3910
4072
|
if (!check.retry)
|
|
3911
4073
|
break;
|
|
@@ -3974,8 +4136,8 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3974
4136
|
for (const connector of selected.filter(isAccountSignalConnector)) {
|
|
3975
4137
|
while (true) {
|
|
3976
4138
|
clearTerminal();
|
|
3977
|
-
await guideAccountSignalConnector(rl, secrets, connector);
|
|
3978
|
-
await upsertAccountSignalConnectorConfig(args.config, connector);
|
|
4139
|
+
const accountSignalAccounts = await guideAccountSignalConnector(rl, secrets, connector);
|
|
4140
|
+
await upsertAccountSignalConnectorConfig(args.config, connector, accountSignalAccounts);
|
|
3979
4141
|
const check = await runImmediateConnectorHealthCheck({
|
|
3980
4142
|
rl,
|
|
3981
4143
|
configPath: args.config,
|
|
@@ -4002,6 +4164,12 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
4002
4164
|
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
4003
4165
|
}
|
|
4004
4166
|
}
|
|
4167
|
+
if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
|
|
4168
|
+
const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
|
|
4169
|
+
if (readiness.ok) {
|
|
4170
|
+
process.stdout.write(`Configured ${paddleAccounts.length} Paddle account(s) in ${args.config}.\n`);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4005
4173
|
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
4006
4174
|
process.stdout.write(`Configured Coolify monitoring for ${coolifyConfig.baseUrl} in ${args.config}.\n`);
|
|
4007
4175
|
}
|
|
@@ -4026,6 +4194,19 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
4026
4194
|
});
|
|
4027
4195
|
}
|
|
4028
4196
|
}
|
|
4197
|
+
if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
|
|
4198
|
+
const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
|
|
4199
|
+
if (readiness.ok) {
|
|
4200
|
+
process.stdout.write(`Paddle account config is up to date in ${args.config}.\n`);
|
|
4201
|
+
}
|
|
4202
|
+
else {
|
|
4203
|
+
postSetupBlockers.push({
|
|
4204
|
+
check: 'connection:paddle',
|
|
4205
|
+
detail: readiness.detail,
|
|
4206
|
+
remediation: 'Rerun Paddle setup so the active config persists sources.paddle.enabled=true and sources.paddle.accounts[].',
|
|
4207
|
+
});
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4029
4210
|
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
4030
4211
|
process.stdout.write(`Coolify config is up to date in ${args.config}.\n`);
|
|
4031
4212
|
}
|