@analyticscli/growth-engineer 0.1.1-preview.18 → 0.1.1-preview.20
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.
|
@@ -3524,13 +3524,14 @@ function printBullets(lines) {
|
|
|
3524
3524
|
}
|
|
3525
3525
|
process.stdout.write('\n');
|
|
3526
3526
|
}
|
|
3527
|
+
function bold(text) {
|
|
3528
|
+
return `${ANSI.bold}${text}${ANSI.reset}`;
|
|
3529
|
+
}
|
|
3527
3530
|
async function guideGitHubConnector(rl, secrets) {
|
|
3528
3531
|
printSection('GitHub code access', [
|
|
3529
|
-
'
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
'Open the token page, select the scopes you want, then paste the token here.',
|
|
3533
|
-
'You can rerun this wizard later to change GitHub permissions.',
|
|
3532
|
+
`${bold('Create token')} here: https://github.com/settings/tokens/new`,
|
|
3533
|
+
`${bold('Scopes')}: public repos = public_repo, private repos/issues/PRs = repo.`,
|
|
3534
|
+
`${bold('Only add workflow')} if OpenClaw should edit GitHub Actions files.`,
|
|
3534
3535
|
]);
|
|
3535
3536
|
let hasGh = await commandExists('gh');
|
|
3536
3537
|
if (!hasGh) {
|
|
@@ -3539,15 +3540,6 @@ async function guideGitHubConnector(rl, secrets) {
|
|
|
3539
3540
|
if (hasGh) {
|
|
3540
3541
|
process.stdout.write('GitHub CLI is available for helper commands.\n\n');
|
|
3541
3542
|
}
|
|
3542
|
-
process.stdout.write('Token URL: https://github.com/settings/tokens/new\n\n');
|
|
3543
|
-
process.stdout.write(`${ANSI.bold}Suggested scopes${ANSI.reset}\n`);
|
|
3544
|
-
printBullets([
|
|
3545
|
-
'Public repo only: select `public_repo`.',
|
|
3546
|
-
'Private repo access: select `repo` (classic GitHub tokens make private repo access broad).',
|
|
3547
|
-
'Create issues / draft PRs in private repos: `repo` is the relevant classic-token scope.',
|
|
3548
|
-
'Edit GitHub Actions workflow files: add `workflow` only if you explicitly want this.',
|
|
3549
|
-
'Usually do not select: packages, admin:org, hooks, gist, user, delete_repo, enterprise, codespace, copilot.',
|
|
3550
|
-
]);
|
|
3551
3543
|
const token = await maybePromptSecret(rl, 'Paste GITHUB_TOKEN into this local terminal', 'GITHUB_TOKEN');
|
|
3552
3544
|
if (token)
|
|
3553
3545
|
secrets.GITHUB_TOKEN = token;
|
|
@@ -3568,12 +3560,10 @@ function shouldForceFreshAnalyticsToken(healthByConnector = {}) {
|
|
|
3568
3560
|
return ['blocked', 'partial'].includes(String(health?.status || '')) || /revoked|unauthorized|invalid token/i.test(detail);
|
|
3569
3561
|
}
|
|
3570
3562
|
async function guideAnalyticsConnector(rl, secrets, options = {}) {
|
|
3571
|
-
printSection('AnalyticsCLI'
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
process.stdout.write('3. Create Access Token\n');
|
|
3576
|
-
process.stdout.write('4. Copy the Readonly CLI Token and paste it below\n\n');
|
|
3563
|
+
printSection('AnalyticsCLI', [
|
|
3564
|
+
`${bold('Create readonly CLI token')}: https://dash.analyticscli.com/`,
|
|
3565
|
+
`${bold('Path')}: Account -> API Keys -> Create Access Token.`,
|
|
3566
|
+
]);
|
|
3577
3567
|
const forceFresh = Boolean(options.forceFresh);
|
|
3578
3568
|
if (forceFresh && process.env.ANALYTICSCLI_ACCESS_TOKEN) {
|
|
3579
3569
|
process.stdout.write('Stored token failed. Paste a new token.\n\n');
|
|
@@ -3590,17 +3580,9 @@ async function guideAnalyticsConnector(rl, secrets, options = {}) {
|
|
|
3590
3580
|
}
|
|
3591
3581
|
async function guideRevenueCatConnector(rl, secrets) {
|
|
3592
3582
|
printSection('RevenueCat monetization data', [
|
|
3593
|
-
'
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
printBullets([
|
|
3597
|
-
'Select your app.',
|
|
3598
|
-
'In the sidebar, choose "Apps & providers".',
|
|
3599
|
-
'Click "API keys" and generate a new secret API key.',
|
|
3600
|
-
'Name it "analyticscli" and choose API version 2.',
|
|
3601
|
-
'Set Charts metrics permissions to read.',
|
|
3602
|
-
'Set Customer information permissions to read.',
|
|
3603
|
-
'Set Project configuration permissions to read.',
|
|
3583
|
+
`${bold('Create secret API key')}: https://app.revenuecat.com/`,
|
|
3584
|
+
`${bold('Path')}: Apps & providers -> API keys -> New secret key.`,
|
|
3585
|
+
`${bold('Permissions')}: API v2, read for Charts metrics, Customer information, Project configuration.`,
|
|
3604
3586
|
]);
|
|
3605
3587
|
const apiKey = await maybePromptSecret(rl, 'Paste REVENUECAT_API_KEY into this local terminal', 'REVENUECAT_API_KEY');
|
|
3606
3588
|
if (apiKey)
|
|
@@ -3623,17 +3605,9 @@ function paddleTokenEnvForAccount(index, label) {
|
|
|
3623
3605
|
}
|
|
3624
3606
|
async function guidePaddleConnector(rl, secrets) {
|
|
3625
3607
|
printSection('Paddle Billing metrics', [
|
|
3626
|
-
'
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
printBullets([
|
|
3630
|
-
'Open Paddle > Developer Tools > Authentication.',
|
|
3631
|
-
'Use the API keys tab and create a new live API key.',
|
|
3632
|
-
'Minimum: grant `metrics.read` so account-level revenue, MRR, refunds, chargebacks, subscribers, and checkout conversion work.',
|
|
3633
|
-
'Recommended for better Growth Engineer analysis: grant all available read-only permissions (`*.read`), including products, prices, discounts, customers, transactions, subscriptions, adjustments, reports, and notifications.',
|
|
3634
|
-
'Do not grant any write permissions (`*.write`) unless another workflow explicitly needs them.',
|
|
3635
|
-
'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
|
|
3636
|
-
'Paste the key here so it is stored only in the local chmod 600 secrets file.',
|
|
3608
|
+
`${bold('Create live API key')}: https://vendors.paddle.com/authentication-v2`,
|
|
3609
|
+
`${bold('Minimum')}: metrics.read. Better: all read-only *.read scopes.`,
|
|
3610
|
+
`${bold('Do not grant write scopes')} unless you explicitly need them elsewhere.`,
|
|
3637
3611
|
]);
|
|
3638
3612
|
const accounts = [];
|
|
3639
3613
|
let index = 0;
|
|
@@ -3660,17 +3634,10 @@ async function guidePaddleConnector(rl, secrets) {
|
|
|
3660
3634
|
}
|
|
3661
3635
|
async function guideSeoConnector(rl, secrets) {
|
|
3662
3636
|
printSection('SEO / Google Search Console / DataForSEO', [
|
|
3663
|
-
'
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
'Preferred: give the token/service account access to all Search Console properties you want analyzed.',
|
|
3668
|
-
'Leave the property URL empty to let the exporter list and query all verified GSC properties in the account.',
|
|
3669
|
-
'Enter a property URL only when you intentionally want to restrict analysis to one site.',
|
|
3670
|
-
'For OAuth token mode, paste a read-only Search Console token with `webmasters.readonly` scope.',
|
|
3671
|
-
'For service-account mode, add the service account email as a restricted/full user in Search Console, then set GOOGLE_APPLICATION_CREDENTIALS or GSC_SERVICE_ACCOUNT_JSON outside this wizard.',
|
|
3672
|
-
'DataForSEO is optional and paid. The exporter refuses paid calls unless the source command includes --confirm-paid and a small --max-paid-requests cap.',
|
|
3673
|
-
'CSV-only mode is also supported with --gsc-csv or --csv in sources.seo.command.',
|
|
3637
|
+
`${bold('GSC')}: https://search.google.com/search-console`,
|
|
3638
|
+
`${bold('Service account')}: https://console.cloud.google.com/iam-admin/serviceaccounts`,
|
|
3639
|
+
`${bold('Optional paid keyword data')}: https://app.dataforseo.com/api-dashboard`,
|
|
3640
|
+
`${bold('Default')}: leave property URL empty to use all verified GSC properties.`,
|
|
3674
3641
|
]);
|
|
3675
3642
|
const siteUrl = await ask(rl, 'Optional GSC property URL (empty = all verified properties)', process.env.GSC_SITE_URL || '');
|
|
3676
3643
|
if (siteUrl.trim())
|
|
@@ -3769,11 +3736,10 @@ async function guideAccountSignalConnector(rl, secrets, key) {
|
|
|
3769
3736
|
if (!definition)
|
|
3770
3737
|
return [];
|
|
3771
3738
|
printSection(definition.label, [
|
|
3772
|
-
definition.
|
|
3773
|
-
'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here
|
|
3739
|
+
`${bold('Docs')}: ${definition.docsUrl}`,
|
|
3740
|
+
`${bold('Setup is account-wide')}. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.`,
|
|
3741
|
+
`${bold('Paste only credentials')} below. The agent discovers accounts/apps/projects later.`,
|
|
3774
3742
|
]);
|
|
3775
|
-
process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
|
|
3776
|
-
printBullets(definition.steps);
|
|
3777
3743
|
const accounts = [];
|
|
3778
3744
|
let index = 0;
|
|
3779
3745
|
while (true) {
|
|
@@ -3813,8 +3779,8 @@ async function guideAccountSignalConnector(rl, secrets, key) {
|
|
|
3813
3779
|
}
|
|
3814
3780
|
async function guideSentryConnector(rl, secrets) {
|
|
3815
3781
|
printSection('Sentry / GlitchTip', [
|
|
3816
|
-
'
|
|
3817
|
-
'
|
|
3782
|
+
`${bold('Base URL')}: https://sentry.io for Sentry Cloud, otherwise your GlitchTip/self-hosted URL.`,
|
|
3783
|
+
`${bold('Token + org')} are needed. Project scope remains unpinned.`,
|
|
3818
3784
|
]);
|
|
3819
3785
|
const accounts = [];
|
|
3820
3786
|
let index = 0;
|
|
@@ -3866,7 +3832,7 @@ async function guideSentryConnector(rl, secrets) {
|
|
|
3866
3832
|
let verifiedVisibleProjects = false;
|
|
3867
3833
|
if (discovery.ok && discovery.projects.length > 0) {
|
|
3868
3834
|
verifiedVisibleProjects = true;
|
|
3869
|
-
process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned
|
|
3835
|
+
process.stdout.write(`Found ${discovery.projects.length} visible project(s). Project scope remains unpinned.\n`);
|
|
3870
3836
|
}
|
|
3871
3837
|
else {
|
|
3872
3838
|
const fallbackOrgs = discoveredOrganizations
|
|
@@ -3923,19 +3889,12 @@ function normalizeCoolifyBaseUrl(value) {
|
|
|
3923
3889
|
}
|
|
3924
3890
|
async function guideCoolifyConnector(rl, secrets) {
|
|
3925
3891
|
printSection('Coolify deployment monitoring', [
|
|
3926
|
-
'
|
|
3927
|
-
'
|
|
3892
|
+
`${bold('Create read-only API token')} in Coolify.`,
|
|
3893
|
+
`${bold('Do not use * or sensitive-token permissions')} for normal monitoring.`,
|
|
3928
3894
|
]);
|
|
3929
3895
|
const baseUrl = normalizeCoolifyBaseUrl(await ask(rl, 'Coolify base URL', process.env.COOLIFY_BASE_URL || 'https://coolify.wotaso.com'));
|
|
3930
3896
|
const tokenUrl = baseUrl ? `${baseUrl}/security/api-tokens` : 'https://<your-coolify-host>/security/api-tokens';
|
|
3931
|
-
process.stdout.write(
|
|
3932
|
-
printBullets([
|
|
3933
|
-
'Open the Coolify dashboard.',
|
|
3934
|
-
'In the sidebar, go to "Keys & Tokens".',
|
|
3935
|
-
'Open "API tokens".',
|
|
3936
|
-
'Create a new API key/token with read-only permissions.',
|
|
3937
|
-
'Copy the token once and paste it into this local terminal.',
|
|
3938
|
-
]);
|
|
3897
|
+
process.stdout.write(`${bold('Token page')}: ${tokenUrl}\n\n`);
|
|
3939
3898
|
const token = await maybePromptSecret(rl, 'Paste COOLIFY_API_TOKEN into this local terminal', 'COOLIFY_API_TOKEN');
|
|
3940
3899
|
if (baseUrl)
|
|
3941
3900
|
secrets.COOLIFY_BASE_URL = baseUrl;
|
|
@@ -3945,37 +3904,18 @@ async function guideCoolifyConnector(rl, secrets) {
|
|
|
3945
3904
|
}
|
|
3946
3905
|
async function guideAscConnector(rl, secrets) {
|
|
3947
3906
|
printSection('App Store Connect CLI', [
|
|
3948
|
-
'
|
|
3949
|
-
'
|
|
3950
|
-
|
|
3951
|
-
process.stdout.write('\nStep 1 - normal key, saved for Growth Engineer\n');
|
|
3952
|
-
printBullets([
|
|
3953
|
-
'Create an API key named "Growth Engineer Reports".',
|
|
3954
|
-
'Role: Sales and Reports. Finance or Admin also works.',
|
|
3955
|
-
'Optional extra roles: Customer Support for reviews, Developer for builds/TestFlight.',
|
|
3956
|
-
'Copy this key into the ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY prompts below.',
|
|
3957
|
-
]);
|
|
3958
|
-
process.stdout.write('\nStep 2 - temporary Admin key, used once\n');
|
|
3959
|
-
printBullets([
|
|
3960
|
-
'Create a second API key named "Growth Engineer Setup Admin".',
|
|
3961
|
-
'Role: Admin.',
|
|
3962
|
-
'Use it only when the wizard asks for ASC_BOOTSTRAP_* later.',
|
|
3963
|
-
'The wizard does not save this key to secrets.env. Revoke it in App Store Connect after setup.',
|
|
3964
|
-
]);
|
|
3965
|
-
process.stdout.write('\nWhy two keys?\n');
|
|
3966
|
-
printBullets([
|
|
3967
|
-
'Apple requires Admin once to create the initial App Analytics report request.',
|
|
3968
|
-
'After that, Growth Engineer only needs the normal reporting key for downloads.',
|
|
3907
|
+
`${bold('Create 2 API keys')} here: https://appstoreconnect.apple.com/access/integrations/api`,
|
|
3908
|
+
`1. ${bold('Reports key')} - role ${bold('Sales and Reports')} - saved for Growth Engineer.`,
|
|
3909
|
+
`2. ${bold('Setup key')} - role ${bold('Admin')} - used once, then revoke.`,
|
|
3969
3910
|
]);
|
|
3970
|
-
process.stdout.write('
|
|
3911
|
+
process.stdout.write(`${bold('Enter the Reports key now:')}\n`);
|
|
3971
3912
|
printBullets([
|
|
3972
|
-
'
|
|
3973
|
-
'
|
|
3974
|
-
'
|
|
3975
|
-
'Vendor Number: App Store Connect > Sales and Trends > Reports.',
|
|
3913
|
+
`${bold('.p8 path')} to Apple\'s original ${bold('AuthKey_<KEY_ID>.p8')} file. ${bold('Do not rename it')}; KEY_ID is read from the filename.`,
|
|
3914
|
+
`${bold('Issuer ID')} from the API keys page.`,
|
|
3915
|
+
`${bold('Vendor Number')} from Sales and Trends > Reports.`,
|
|
3976
3916
|
]);
|
|
3977
3917
|
const normalKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
|
|
3978
|
-
label: '
|
|
3918
|
+
label: 'Reports .p8 path (AuthKey_<KEY_ID>.p8, empty = paste)',
|
|
3979
3919
|
defaultValue: process.env.ASC_PRIVATE_KEY_PATH || '',
|
|
3980
3920
|
keyLabel: 'the normal reporting key',
|
|
3981
3921
|
});
|
|
@@ -3985,11 +3925,11 @@ async function guideAscConnector(rl, secrets) {
|
|
|
3985
3925
|
secrets.ASC_KEY_ID = keyId;
|
|
3986
3926
|
process.stdout.write(`Inferred ASC_KEY_ID=${keyId} from ${path.basename(normalKeyPath.privateKeyPath)}.\n`);
|
|
3987
3927
|
}
|
|
3988
|
-
const issuerId = await ask(rl, 'ASC_ISSUER_ID
|
|
3928
|
+
const issuerId = await ask(rl, 'ASC_ISSUER_ID (reports key, empty = skip)', process.env.ASC_ISSUER_ID || '');
|
|
3989
3929
|
if (issuerId.trim())
|
|
3990
3930
|
secrets.ASC_ISSUER_ID = issuerId.trim();
|
|
3991
3931
|
if (!normalKeyPath.privateKeyPath) {
|
|
3992
|
-
keyId = await ask(rl, 'ASC_KEY_ID
|
|
3932
|
+
keyId = await ask(rl, 'ASC_KEY_ID (from AuthKey_<KEY_ID>.p8, empty = skip)', process.env.ASC_KEY_ID || '');
|
|
3993
3933
|
if (keyId.trim())
|
|
3994
3934
|
secrets.ASC_KEY_ID = keyId.trim();
|
|
3995
3935
|
const privateKeyContent = await askAscPrivateKeyContent(rl, {
|
|
@@ -4004,30 +3944,26 @@ async function guideAscConnector(rl, secrets) {
|
|
|
4004
3944
|
secrets.ASC_PRIVATE_KEY_PATH = privateKeyPath;
|
|
4005
3945
|
process.stdout.write(`Saved ASC private key to ${privateKeyPath} with chmod 600.\n`);
|
|
4006
3946
|
}
|
|
4007
|
-
const vendorNumber = await ask(rl, 'ASC_VENDOR_NUMBER
|
|
3947
|
+
const vendorNumber = await ask(rl, 'ASC_VENDOR_NUMBER (Sales and Trends > Reports)', process.env.ASC_VENDOR_NUMBER || '');
|
|
4008
3948
|
if (vendorNumber.trim())
|
|
4009
3949
|
secrets.ASC_VENDOR_NUMBER = vendorNumber.trim();
|
|
4010
3950
|
return await guideAscBootstrapAdminKey(rl, issuerId.trim());
|
|
4011
3951
|
}
|
|
4012
3952
|
async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
|
|
4013
3953
|
const bootstrapEnv = {};
|
|
4014
|
-
process.stdout.write('
|
|
3954
|
+
process.stdout.write(`\n${bold('Enter the Setup Admin key:')}\n`);
|
|
4015
3955
|
printBullets([
|
|
4016
|
-
'
|
|
4017
|
-
'
|
|
4018
|
-
'
|
|
4019
|
-
'Use Apple\'s original AuthKey_<KEY_ID>.p8 file name so the wizard can infer ASC_BOOTSTRAP_KEY_ID.',
|
|
4020
|
-
'Do not rename the .p8 file.',
|
|
4021
|
-
'If you paste the .p8 content, the local temporary file is deleted after analytics initialization.',
|
|
4022
|
-
'Revoke this Admin key in App Store Connect after setup.',
|
|
3956
|
+
`${bold('Role must be Admin')} so Apple can create the first App Analytics report request.`,
|
|
3957
|
+
`${bold('Use original AuthKey_<KEY_ID>.p8 filename')} so KEY_ID is read automatically.`,
|
|
3958
|
+
`${bold('Not saved')} to secrets.env. Revoke this key after setup.`,
|
|
4023
3959
|
]);
|
|
4024
3960
|
const bootstrapKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
|
|
4025
|
-
label: '
|
|
3961
|
+
label: 'Setup Admin .p8 path (AuthKey_<KEY_ID>.p8, empty = paste)',
|
|
4026
3962
|
defaultValue: '',
|
|
4027
3963
|
keyLabel: 'the temporary Admin key',
|
|
4028
3964
|
});
|
|
4029
3965
|
let bootstrapKeyId = bootstrapKeyPath.keyId;
|
|
4030
|
-
const bootstrapIssuerId = await ask(rl, 'ASC_BOOTSTRAP_ISSUER_ID
|
|
3966
|
+
const bootstrapIssuerId = await ask(rl, 'ASC_BOOTSTRAP_ISSUER_ID', issuerIdDefault);
|
|
4031
3967
|
if (bootstrapKeyPath.privateKeyPath) {
|
|
4032
3968
|
bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = bootstrapKeyPath.privateKeyPath;
|
|
4033
3969
|
process.stdout.write(`Inferred ASC_BOOTSTRAP_KEY_ID=${bootstrapKeyId} from ${path.basename(bootstrapKeyPath.privateKeyPath)}.\n`);
|
|
@@ -4036,7 +3972,7 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
|
|
|
4036
3972
|
bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_DELETE_AFTER_USE = '1';
|
|
4037
3973
|
}
|
|
4038
3974
|
else {
|
|
4039
|
-
bootstrapKeyId = await ask(rl, 'ASC_BOOTSTRAP_KEY_ID
|
|
3975
|
+
bootstrapKeyId = await ask(rl, 'ASC_BOOTSTRAP_KEY_ID (from AuthKey_<KEY_ID>.p8)', '');
|
|
4040
3976
|
}
|
|
4041
3977
|
if (!bootstrapKeyId.trim() || !bootstrapIssuerId.trim())
|
|
4042
3978
|
return { bootstrapEnv };
|