@analyticscli/growth-engineer 0.1.1-preview.0 → 0.1.1-preview.10
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 +52 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/discord-openclaw-bridge.mjs +309 -0
- package/dist/runtime/export-paddle-summary.mjs +73 -19
- 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 +65 -1
- package/dist/runtime/openclaw-exporters-lib.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-preflight.mjs +48 -15
- package/dist/runtime/openclaw-growth-preflight.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-runner.mjs +617 -28
- package/dist/runtime/openclaw-growth-runner.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-shared.mjs +3 -0
- package/dist/runtime/openclaw-growth-shared.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-start.mjs +115 -92
- package/dist/runtime/openclaw-growth-start.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-status.mjs +56 -36
- package/dist/runtime/openclaw-growth-status.mjs.map +1 -1
- package/dist/runtime/openclaw-growth-wizard.mjs +421 -64
- package/dist/runtime/openclaw-growth-wizard.mjs.map +1 -1
- package/package.json +3 -2
|
@@ -11,6 +11,7 @@ import { buildOpenClawCronAddCommand, buildHermesCronCreateCommand, buildGrowthR
|
|
|
11
11
|
import { loadOpenClawGrowthSecrets } from './openclaw-growth-env.mjs';
|
|
12
12
|
const DEFAULT_CONFIG_PATH = 'data/openclaw-growth-engineer/config.json';
|
|
13
13
|
const SELF_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
const SELF_UPDATE_SKILL_SLUG_CANDIDATES = ['growth-engineer', 'openclaw-growth-engineer'];
|
|
14
15
|
const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
|
|
15
16
|
const DEFAULT_GROWTH_INTERVAL_MINUTES = 90;
|
|
16
17
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
@@ -83,7 +84,7 @@ const CONNECTOR_DEFINITIONS = [
|
|
|
83
84
|
key: 'paddle',
|
|
84
85
|
label: 'Paddle Billing metrics',
|
|
85
86
|
summary: 'Read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
|
|
86
|
-
needs: 'A Paddle API key
|
|
87
|
+
needs: 'A scoped Paddle API key for the live account with metrics.read permission.',
|
|
87
88
|
},
|
|
88
89
|
{
|
|
89
90
|
key: 'seo',
|
|
@@ -855,6 +856,7 @@ function resolveRuntimeScriptPath(scriptName) {
|
|
|
855
856
|
const candidates = [
|
|
856
857
|
path.join(RUNTIME_DIR, scriptName),
|
|
857
858
|
path.resolve('scripts', scriptName),
|
|
859
|
+
path.resolve('skills/growth-engineer/scripts', scriptName),
|
|
858
860
|
path.resolve('skills/openclaw-growth-engineer/scripts', scriptName),
|
|
859
861
|
];
|
|
860
862
|
return candidates.find((candidate) => existsSync(candidate)) || path.join(RUNTIME_DIR, scriptName);
|
|
@@ -904,7 +906,9 @@ function sourceCommandNeedsActiveConfig(sourceName, command) {
|
|
|
904
906
|
const value = String(command || '').toLowerCase();
|
|
905
907
|
return (normalized === 'sentry' ||
|
|
906
908
|
normalized === 'glitchtip' ||
|
|
909
|
+
normalized === 'paddle' ||
|
|
907
910
|
normalized === 'coolify' ||
|
|
911
|
+
value.includes('export-paddle-summary') ||
|
|
908
912
|
value.includes('export-sentry-summary') ||
|
|
909
913
|
value.includes('export-coolify-summary') ||
|
|
910
914
|
value.includes('exporters coolify-summary'));
|
|
@@ -1090,7 +1094,7 @@ function withMissingRequiredAnalyticsConnector(selected) {
|
|
|
1090
1094
|
}
|
|
1091
1095
|
async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
|
|
1092
1096
|
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
1093
|
-
return await askConnectorSelectionByText(rl, healthByConnector, copy);
|
|
1097
|
+
return await askConnectorSelectionByText(rl, healthByConnector, initialSelected, copy);
|
|
1094
1098
|
}
|
|
1095
1099
|
rl.pause();
|
|
1096
1100
|
let completed = false;
|
|
@@ -1108,20 +1112,23 @@ async function askConnectorSelectionWithHealth(rl, healthByConnector = {}, initi
|
|
|
1108
1112
|
}
|
|
1109
1113
|
}
|
|
1110
1114
|
}
|
|
1111
|
-
async function askConnectorSelectionByText(rl, healthByConnector = {}, copy = {}) {
|
|
1115
|
+
async function askConnectorSelectionByText(rl, healthByConnector = {}, initialSelected = [], copy = {}) {
|
|
1112
1116
|
printConnectorIntro(copy);
|
|
1113
1117
|
for (const group of connectorPickerGroups(healthByConnector)) {
|
|
1114
1118
|
process.stdout.write(`${ANSI.bold}${group.title}${ANSI.reset}\n`);
|
|
1115
1119
|
for (const connector of group.connectors) {
|
|
1116
1120
|
const number = CONNECTOR_DEFINITIONS.findIndex((entry) => entry.key === connector.key) + 1;
|
|
1117
1121
|
process.stdout.write(` ${number}) ${connector.label}\n`);
|
|
1118
|
-
writeWrapped(formatConnectorHealthText(connector.key, healthByConnector), ' ', ANSI.dim);
|
|
1119
|
-
writeWrapped(connector.summary, ' ');
|
|
1120
1122
|
}
|
|
1121
1123
|
process.stdout.write('\n');
|
|
1122
1124
|
}
|
|
1125
|
+
const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
|
|
1126
|
+
const defaultSelection = orderConnectors([...new Set([...initialSelected, ...required])]);
|
|
1127
|
+
const defaultAnswer = defaultSelection.length > 0
|
|
1128
|
+
? defaultSelection.map((key) => String(CONNECTOR_DEFINITIONS.findIndex((entry) => entry.key === key) + 1)).join(',')
|
|
1129
|
+
: '';
|
|
1123
1130
|
while (true) {
|
|
1124
|
-
const answer = await ask(rl, 'Select connectors (comma-separated numbers/names, or all)',
|
|
1131
|
+
const answer = await ask(rl, 'Select connectors (comma-separated numbers/names, or all)', defaultAnswer);
|
|
1125
1132
|
const selected = parseConnectorAnswer(answer);
|
|
1126
1133
|
if (selected.length > 0)
|
|
1127
1134
|
return selected;
|
|
@@ -1445,14 +1452,19 @@ function normalizeConnectorProgressKey(key) {
|
|
|
1445
1452
|
return accountConnector;
|
|
1446
1453
|
return null;
|
|
1447
1454
|
}
|
|
1448
|
-
async function withConnectorHealthLoading(taskFactory) {
|
|
1455
|
+
async function withConnectorHealthLoading(taskFactory, expectedConnectors = [...CONNECTOR_KEYS]) {
|
|
1456
|
+
const expected = orderConnectors(expectedConnectors);
|
|
1457
|
+
if (expected.length === 0) {
|
|
1458
|
+
return await taskFactory(() => { });
|
|
1459
|
+
}
|
|
1449
1460
|
const frames = ['-', '\\', '|', '/'];
|
|
1450
1461
|
const completed = new Set();
|
|
1462
|
+
const expectedSet = new Set(expected);
|
|
1451
1463
|
let index = 0;
|
|
1452
1464
|
let current = 'starting';
|
|
1453
1465
|
const render = () => {
|
|
1454
|
-
const count = Math.min(completed.size,
|
|
1455
|
-
process.stdout.write(`\rChecking connector health ${count}/${
|
|
1466
|
+
const count = Math.min(completed.size, expected.length);
|
|
1467
|
+
process.stdout.write(`\rChecking connector health ${count}/${expected.length} (${current}) ${frames[index]}`);
|
|
1456
1468
|
};
|
|
1457
1469
|
const timer = setInterval(() => {
|
|
1458
1470
|
index = (index + 1) % frames.length;
|
|
@@ -1462,14 +1474,14 @@ async function withConnectorHealthLoading(taskFactory) {
|
|
|
1462
1474
|
try {
|
|
1463
1475
|
const result = await taskFactory((event) => {
|
|
1464
1476
|
const key = normalizeConnectorProgressKey(event?.key);
|
|
1465
|
-
if (!key)
|
|
1477
|
+
if (!key || !expectedSet.has(key))
|
|
1466
1478
|
return;
|
|
1467
1479
|
current = connectorLabel(key);
|
|
1468
1480
|
if (event?.phase === 'finish')
|
|
1469
1481
|
completed.add(key);
|
|
1470
1482
|
render();
|
|
1471
1483
|
});
|
|
1472
|
-
|
|
1484
|
+
expected.forEach((key) => completed.add(key));
|
|
1473
1485
|
current = 'done';
|
|
1474
1486
|
render();
|
|
1475
1487
|
process.stdout.write('\n');
|
|
@@ -1525,12 +1537,14 @@ function connectorStatusLabel(key, healthByConnector = {}) {
|
|
|
1525
1537
|
if (health.status === 'connected')
|
|
1526
1538
|
return configured ? 'configured, healthy' : 'healthy via local tool auth';
|
|
1527
1539
|
if (!configured)
|
|
1528
|
-
return '
|
|
1540
|
+
return '';
|
|
1529
1541
|
return `configured, ${connectorHealthLabel(health.status)}`;
|
|
1530
1542
|
}
|
|
1531
1543
|
function formatConnectorHealthText(key, healthByConnector = {}) {
|
|
1532
1544
|
const health = getConnectorHealth(key, healthByConnector);
|
|
1533
1545
|
const label = connectorStatusLabel(key, healthByConnector);
|
|
1546
|
+
if (!label)
|
|
1547
|
+
return '';
|
|
1534
1548
|
const detail = health.detail ? ` - ${health.detail}` : '';
|
|
1535
1549
|
return `Status: ${label}${detail}`;
|
|
1536
1550
|
}
|
|
@@ -1589,11 +1603,133 @@ function connectorPickerDisplayItems(healthByConnector = {}) {
|
|
|
1589
1603
|
return connectorPickerGroups(healthByConnector).flatMap((group) => group.connectors);
|
|
1590
1604
|
}
|
|
1591
1605
|
function connectorKeysNeedingAttention(healthByConnector = {}) {
|
|
1592
|
-
return CONNECTOR_KEYS.filter((key) =>
|
|
1606
|
+
return CONNECTOR_KEYS.filter((key) => {
|
|
1607
|
+
const health = getConnectorHealth(key, healthByConnector);
|
|
1608
|
+
const status = String(health.status || '');
|
|
1609
|
+
if (!['blocked', 'partial', 'unknown', 'not_connected'].includes(status))
|
|
1610
|
+
return false;
|
|
1611
|
+
return isConnectorLocallyConfigured(key) || status !== 'not_connected';
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
function isConfiguredSource(config, sourceName) {
|
|
1615
|
+
return Boolean(config?.sources?.[sourceName] && config.sources[sourceName].enabled !== false);
|
|
1616
|
+
}
|
|
1617
|
+
function configuredConnectorKeysFromConfig(config) {
|
|
1618
|
+
const configured = new Set();
|
|
1619
|
+
if (!config || typeof config !== 'object')
|
|
1620
|
+
return [];
|
|
1621
|
+
for (const key of ['analytics', 'revenuecat', 'paddle', 'seo', 'sentry', 'coolify']) {
|
|
1622
|
+
if (isConfiguredSource(config, key))
|
|
1623
|
+
configured.add(key);
|
|
1624
|
+
}
|
|
1625
|
+
if (isConfiguredGitHubRepo(config?.project?.githubRepo))
|
|
1626
|
+
configured.add('github');
|
|
1627
|
+
for (const source of Array.isArray(config?.sources?.extra) ? config.sources.extra : []) {
|
|
1628
|
+
if (!source || source.enabled === false)
|
|
1629
|
+
continue;
|
|
1630
|
+
if (source.service === 'asc-cli') {
|
|
1631
|
+
configured.add('asc');
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const connector = normalizeConnectorProgressKey(source.key || source.service || source.type);
|
|
1635
|
+
if (connector)
|
|
1636
|
+
configured.add(connector);
|
|
1637
|
+
}
|
|
1638
|
+
return [...configured];
|
|
1639
|
+
}
|
|
1640
|
+
async function connectorKeysForHealthCheck(configPath) {
|
|
1641
|
+
const configured = new Set();
|
|
1642
|
+
CONNECTOR_KEYS.forEach((key) => {
|
|
1643
|
+
if (isConnectorLocallyConfigured(key))
|
|
1644
|
+
configured.add(key);
|
|
1645
|
+
});
|
|
1646
|
+
const config = await readJsonIfPresent(configPath).catch(() => null);
|
|
1647
|
+
for (const key of configuredConnectorKeysFromConfig(config))
|
|
1648
|
+
configured.add(key);
|
|
1649
|
+
return orderConnectors([...configured]);
|
|
1650
|
+
}
|
|
1651
|
+
function connectorKeyFromRunnerHealthKey(key) {
|
|
1652
|
+
const normalized = String(key || '').trim();
|
|
1653
|
+
if (normalized === 'analyticscli')
|
|
1654
|
+
return 'analytics';
|
|
1655
|
+
if (normalized === 'appStoreConnect')
|
|
1656
|
+
return 'asc';
|
|
1657
|
+
const connector = normalizeConnectorProgressKey(normalized);
|
|
1658
|
+
return connector || null;
|
|
1659
|
+
}
|
|
1660
|
+
function activeIncidentStatusLabel(status) {
|
|
1661
|
+
const normalized = String(status || '').trim();
|
|
1662
|
+
return normalized || 'blocked';
|
|
1593
1663
|
}
|
|
1594
|
-
async function
|
|
1664
|
+
async function readActiveConnectorIncidents(configPath) {
|
|
1665
|
+
const statePath = deriveStatePathFromConfigPath(configPath);
|
|
1666
|
+
const state = await readJsonIfPresent(statePath).catch(() => null);
|
|
1667
|
+
const healthState = state?.connectorHealth;
|
|
1668
|
+
if (!healthState ||
|
|
1669
|
+
healthState.lastStatusOk !== false ||
|
|
1670
|
+
!healthState.activeIncidentFingerprint) {
|
|
1671
|
+
return {};
|
|
1672
|
+
}
|
|
1673
|
+
const alertJsonPath = healthState.lastAlertJsonPath
|
|
1674
|
+
? path.resolve(String(healthState.lastAlertJsonPath))
|
|
1675
|
+
: path.resolve(path.dirname(statePath), 'runtime/connector-health/latest.json');
|
|
1676
|
+
const alertJson = await readJsonIfPresent(alertJsonPath).catch(() => null);
|
|
1677
|
+
const unhealthyConnectors = Array.isArray(alertJson?.unhealthyConnectors)
|
|
1678
|
+
? alertJson.unhealthyConnectors
|
|
1679
|
+
: [];
|
|
1680
|
+
const incidents = {};
|
|
1681
|
+
for (const entry of unhealthyConnectors) {
|
|
1682
|
+
const key = connectorKeyFromRunnerHealthKey(entry?.key);
|
|
1683
|
+
if (!key)
|
|
1684
|
+
continue;
|
|
1685
|
+
incidents[key] = {
|
|
1686
|
+
...entry,
|
|
1687
|
+
status: activeIncidentStatusLabel(entry?.status),
|
|
1688
|
+
detail: String(entry?.detail || 'Runner still has an active connector-health incident').trim(),
|
|
1689
|
+
activeRunnerIncident: true,
|
|
1690
|
+
activeIncidentFingerprint: healthState.activeIncidentFingerprint,
|
|
1691
|
+
lastCheckedAt: healthState.lastCheckedAt || null,
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
return incidents;
|
|
1695
|
+
}
|
|
1696
|
+
function mergeActiveConnectorIncidents(healthByConnector, activeIncidents) {
|
|
1697
|
+
if (!activeIncidents || Object.keys(activeIncidents).length === 0) {
|
|
1698
|
+
return healthByConnector;
|
|
1699
|
+
}
|
|
1700
|
+
return Object.fromEntries(CONNECTOR_KEYS.map((key) => {
|
|
1701
|
+
const liveHealth = getConnectorHealth(key, healthByConnector);
|
|
1702
|
+
const incident = activeIncidents[key];
|
|
1703
|
+
if (!incident)
|
|
1704
|
+
return [key, liveHealth];
|
|
1705
|
+
if (liveHealth.status === 'connected') {
|
|
1706
|
+
return [
|
|
1707
|
+
key,
|
|
1708
|
+
{
|
|
1709
|
+
...liveHealth,
|
|
1710
|
+
status: 'partial',
|
|
1711
|
+
detail: `Live wizard check passed, but the runner still has an active ${incident.status} incident; run the connector health job once to record recovery.`,
|
|
1712
|
+
activeRunnerIncident: true,
|
|
1713
|
+
activeIncidentFingerprint: incident.activeIncidentFingerprint,
|
|
1714
|
+
lastCheckedAt: incident.lastCheckedAt,
|
|
1715
|
+
},
|
|
1716
|
+
];
|
|
1717
|
+
}
|
|
1718
|
+
return [
|
|
1719
|
+
key,
|
|
1720
|
+
{
|
|
1721
|
+
...liveHealth,
|
|
1722
|
+
...incident,
|
|
1723
|
+
status: incident.status || liveHealth.status || 'blocked',
|
|
1724
|
+
detail: incident.detail || liveHealth.detail,
|
|
1725
|
+
},
|
|
1726
|
+
];
|
|
1727
|
+
}));
|
|
1728
|
+
}
|
|
1729
|
+
async function getConnectorPickerHealth(configPath, onProgress = () => { }, onlyConnectors = []) {
|
|
1730
|
+
const activeIncidents = await readActiveConnectorIncidents(configPath);
|
|
1595
1731
|
if (!(await fileExists(configPath))) {
|
|
1596
|
-
|
|
1732
|
+
const fallbackHealth = Object.fromEntries(CONNECTOR_KEYS.map((key) => [
|
|
1597
1733
|
key,
|
|
1598
1734
|
{
|
|
1599
1735
|
status: isConnectorLocallyConfigured(key) ? 'unknown' : 'not_connected',
|
|
@@ -1602,8 +1738,14 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
1602
1738
|
: '',
|
|
1603
1739
|
},
|
|
1604
1740
|
]));
|
|
1741
|
+
return mergeActiveConnectorIncidents(fallbackHealth, activeIncidents);
|
|
1605
1742
|
}
|
|
1606
|
-
|
|
1743
|
+
if (onlyConnectors.length === 0) {
|
|
1744
|
+
const fallbackHealth = Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, {})]));
|
|
1745
|
+
return mergeActiveConnectorIncidents(fallbackHealth, activeIncidents);
|
|
1746
|
+
}
|
|
1747
|
+
const onlyArg = ` --only-connectors ${quote(orderConnectors(onlyConnectors).join(','))}`;
|
|
1748
|
+
const result = await runCommandCaptureWithProgress(`${nodeRuntimeScriptCommand('openclaw-growth-status.mjs')} --config ${quote(configPath)} --json --progress-json${onlyArg}`, onProgress);
|
|
1607
1749
|
const payload = parseJsonFromStdout(result.stdout);
|
|
1608
1750
|
const connectors = payload?.connectors && typeof payload.connectors === 'object' ? payload.connectors : {};
|
|
1609
1751
|
const healthByConnector = {
|
|
@@ -1616,7 +1758,8 @@ async function getConnectorPickerHealth(configPath, onProgress = () => { }) {
|
|
|
1616
1758
|
coolify: connectors.coolify,
|
|
1617
1759
|
asc: connectors.appStoreConnect,
|
|
1618
1760
|
};
|
|
1619
|
-
|
|
1761
|
+
const liveHealth = Object.fromEntries(CONNECTOR_KEYS.map((key) => [key, getConnectorHealth(key, healthByConnector)]));
|
|
1762
|
+
return mergeActiveConnectorIncidents(liveHealth, activeIncidents);
|
|
1620
1763
|
}
|
|
1621
1764
|
function renderConnectorPicker(cursorIndex, selected, required, healthByConnector = {}, warning = '', copy = {}) {
|
|
1622
1765
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
@@ -1637,11 +1780,9 @@ function renderConnectorPicker(cursorIndex, selected, required, healthByConnecto
|
|
|
1637
1780
|
const label = `${connector.label}${suffix}`;
|
|
1638
1781
|
const title = active ? `${ANSI.bold}${label}${ANSI.reset}` : label;
|
|
1639
1782
|
process.stdout.write(`${pointer} ${box} ${title}\n`);
|
|
1640
|
-
writeWrapped(connector.summary, ' ');
|
|
1641
|
-
writeWrapped(formatConnectorHealthText(connector.key, healthByConnector), ' ', ANSI.dim);
|
|
1642
|
-
process.stdout.write('\n');
|
|
1643
1783
|
index += 1;
|
|
1644
1784
|
}
|
|
1785
|
+
process.stdout.write('\n');
|
|
1645
1786
|
}
|
|
1646
1787
|
if (warning) {
|
|
1647
1788
|
process.stdout.write(`${ANSI.bold}${warning}${ANSI.reset}\n\n`);
|
|
@@ -1657,9 +1798,7 @@ async function askConnectorSelectionByKeys(healthByConnector = {}, initialSelect
|
|
|
1657
1798
|
let cursorIndex = 0;
|
|
1658
1799
|
const required = copy.mode === 'input' ? new Set() : getRequiredConnectorKeys();
|
|
1659
1800
|
const initial = new Set(initialSelected);
|
|
1660
|
-
const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) ||
|
|
1661
|
-
initial.has(key) ||
|
|
1662
|
-
(copy.mode !== 'input' && !isConnectorLocallyConfigured(key))));
|
|
1801
|
+
const selected = new Set(CONNECTOR_KEYS.filter((key) => required.has(key) || initial.has(key)));
|
|
1663
1802
|
let warning = '';
|
|
1664
1803
|
return await new Promise((resolve, reject) => {
|
|
1665
1804
|
const displayItems = () => connectorPickerDisplayItems(healthByConnector);
|
|
@@ -2020,7 +2159,7 @@ function summarizeFailureFix(connector, blockers) {
|
|
|
2020
2159
|
return 'Paste a RevenueCat v2 secret API key with read-only project permissions, then rerun setup.';
|
|
2021
2160
|
}
|
|
2022
2161
|
if (connector === 'paddle') {
|
|
2023
|
-
return 'Paste a Paddle API key with metrics.read permission
|
|
2162
|
+
return 'Paste a live Paddle API key from Developer Tools > Authentication v2 with metrics.read permission, then rerun setup.';
|
|
2024
2163
|
}
|
|
2025
2164
|
if (connector === 'seo') {
|
|
2026
2165
|
return 'Configure Search Console read access. Leave GSC_SITE_URL empty to scan all verified properties in the account, or set it only when you intentionally want one property.';
|
|
@@ -2099,12 +2238,20 @@ function payloadHasConnectorFailures(payload, connector) {
|
|
|
2099
2238
|
const blockers = Array.isArray(payload?.blockers) ? payload.blockers : [];
|
|
2100
2239
|
return blockers.some((blocker) => !isDeferredGitHubFailure(blocker) && connectorForBlocker(blocker) === connector);
|
|
2101
2240
|
}
|
|
2241
|
+
function payloadOtherConnectorFailures(payload, connector) {
|
|
2242
|
+
const blockers = Array.isArray(payload?.blockers) ? payload.blockers : [];
|
|
2243
|
+
return blockers.filter((blocker) => {
|
|
2244
|
+
if (isDeferredGitHubFailure(blocker))
|
|
2245
|
+
return false;
|
|
2246
|
+
const blockerConnector = connectorForBlocker(blocker);
|
|
2247
|
+
return blockerConnector !== connector && blockerConnector !== 'setup';
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2102
2250
|
async function askListSelection(rl, label, entries, options = {}) {
|
|
2103
2251
|
const includeManual = Boolean(options.includeManual);
|
|
2104
2252
|
const includeDefer = Boolean(options.includeDefer);
|
|
2105
2253
|
entries.forEach((entry, index) => {
|
|
2106
|
-
|
|
2107
|
-
process.stdout.write(` ${index + 1}) ${entry.label}${description}\n`);
|
|
2254
|
+
process.stdout.write(` ${index + 1}) ${entry.label}\n`);
|
|
2108
2255
|
});
|
|
2109
2256
|
const manualIndex = includeManual ? entries.length + 1 : null;
|
|
2110
2257
|
const deferIndex = includeDefer ? entries.length + (includeManual ? 2 : 1) : null;
|
|
@@ -2154,6 +2301,8 @@ function printSetupSuccess(payload) {
|
|
|
2154
2301
|
}
|
|
2155
2302
|
function connectorFromCheckName(name) {
|
|
2156
2303
|
const value = String(name || '');
|
|
2304
|
+
if (value.includes('asc') || value.includes('ASC_') || /App Store Connect|app-store-connect|app_store_connect|Analytics Report Request/i.test(value))
|
|
2305
|
+
return 'asc';
|
|
2157
2306
|
if (value.includes('analytics') || value.includes('ANALYTICSCLI'))
|
|
2158
2307
|
return 'analytics';
|
|
2159
2308
|
if (value.includes('github') || value.includes('GITHUB'))
|
|
@@ -2168,8 +2317,6 @@ function connectorFromCheckName(name) {
|
|
|
2168
2317
|
return 'sentry';
|
|
2169
2318
|
if (value.includes('coolify') || value.includes('COOLIFY'))
|
|
2170
2319
|
return 'coolify';
|
|
2171
|
-
if (value.includes('asc') || value.includes('ASC_'))
|
|
2172
|
-
return 'asc';
|
|
2173
2320
|
for (const key of ACCOUNT_SIGNAL_CONNECTOR_KEYS) {
|
|
2174
2321
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
2175
2322
|
const envMatch = definition?.credentials.some((credential) => value.includes(credential.env));
|
|
@@ -2388,10 +2535,13 @@ async function saveSecretsImmediately(secrets) {
|
|
|
2388
2535
|
process.stdout.write(`Saved local secrets to ${secretsFile} with chmod 600.\n`);
|
|
2389
2536
|
return true;
|
|
2390
2537
|
}
|
|
2391
|
-
async function runImmediateConnectorHealthCheck({ rl, configPath, connector, secrets, sentryAccounts = [], }) {
|
|
2538
|
+
async function runImmediateConnectorHealthCheck({ rl, configPath, connector, secrets, sentryAccounts = [], paddleAccounts = [], }) {
|
|
2392
2539
|
if (connector === 'sentry' && sentryAccounts.length > 0) {
|
|
2393
2540
|
await upsertSentryAccountsConfig(configPath, sentryAccounts);
|
|
2394
2541
|
}
|
|
2542
|
+
if (connector === 'paddle' && paddleAccounts.length > 0) {
|
|
2543
|
+
await upsertPaddleAccountsConfig(configPath, paddleAccounts);
|
|
2544
|
+
}
|
|
2395
2545
|
await saveSecretsImmediately(secrets);
|
|
2396
2546
|
const env = {
|
|
2397
2547
|
...process.env,
|
|
@@ -2409,6 +2559,17 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
|
|
|
2409
2559
|
const retry = await askYesNo(rl, `Re-enter ${connectorLabel(connector)} configuration now?`, true);
|
|
2410
2560
|
return { ok: false, retry, result, payload };
|
|
2411
2561
|
}
|
|
2562
|
+
const otherConnectorBlockers = payloadOtherConnectorFailures(payload, connector);
|
|
2563
|
+
if (otherConnectorBlockers.length > 0) {
|
|
2564
|
+
process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed, but another configured connector needs attention.\n`);
|
|
2565
|
+
printConciseSetupBlockers({
|
|
2566
|
+
...payload,
|
|
2567
|
+
blockers: otherConnectorBlockers,
|
|
2568
|
+
}, command, {
|
|
2569
|
+
hideRerunWhenClean: true,
|
|
2570
|
+
});
|
|
2571
|
+
return { ok: false, retry: false, result, payload };
|
|
2572
|
+
}
|
|
2412
2573
|
process.stdout.write(`\n${connectorLabel(connector)} immediate health check passed or is only waiting on optional/deferred context.\n`);
|
|
2413
2574
|
return { ok: true, retry: false, result, payload };
|
|
2414
2575
|
}
|
|
@@ -3059,6 +3220,71 @@ async function verifySentryAccountsConfig(configPath, expectedAccounts) {
|
|
|
3059
3220
|
}
|
|
3060
3221
|
return { ok: true, detail: `${realAccounts.length} active Sentry-compatible account(s) configured` };
|
|
3061
3222
|
}
|
|
3223
|
+
async function upsertPaddleAccountsConfig(configPath, accounts) {
|
|
3224
|
+
if (!accounts.length || !(await fileExists(configPath)))
|
|
3225
|
+
return false;
|
|
3226
|
+
const config = await readJsonFile(configPath);
|
|
3227
|
+
const existingAccounts = Array.isArray(config?.sources?.paddle?.accounts)
|
|
3228
|
+
? config.sources.paddle.accounts
|
|
3229
|
+
: [];
|
|
3230
|
+
const merged = new Map();
|
|
3231
|
+
for (const account of existingAccounts) {
|
|
3232
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3233
|
+
if (id)
|
|
3234
|
+
merged.set(id, account);
|
|
3235
|
+
}
|
|
3236
|
+
for (const account of accounts) {
|
|
3237
|
+
merged.set(account.id, {
|
|
3238
|
+
...(merged.get(account.id) || {}),
|
|
3239
|
+
...account,
|
|
3240
|
+
});
|
|
3241
|
+
}
|
|
3242
|
+
const tokenEnv = accounts[0]?.tokenEnv || config?.sources?.paddle?.tokenEnv || config?.secrets?.paddleTokenEnv || 'PADDLE_API_KEY';
|
|
3243
|
+
config.sources = {
|
|
3244
|
+
...(config.sources || {}),
|
|
3245
|
+
paddle: {
|
|
3246
|
+
...(config.sources?.paddle || {}),
|
|
3247
|
+
enabled: true,
|
|
3248
|
+
mode: 'command',
|
|
3249
|
+
command: normalizeWizardSourceCommand('paddle', config.sources?.paddle || {}, configPath),
|
|
3250
|
+
environment: config.sources?.paddle?.environment || 'live',
|
|
3251
|
+
tokenEnv,
|
|
3252
|
+
accounts: [...merged.values()],
|
|
3253
|
+
},
|
|
3254
|
+
};
|
|
3255
|
+
config.secrets = {
|
|
3256
|
+
...(config.secrets || {}),
|
|
3257
|
+
paddleTokenEnv: tokenEnv,
|
|
3258
|
+
paddleTokenRef: { source: 'env', provider: 'default', id: tokenEnv },
|
|
3259
|
+
};
|
|
3260
|
+
await writeJsonFile(configPath, config);
|
|
3261
|
+
return true;
|
|
3262
|
+
}
|
|
3263
|
+
async function verifyPaddleAccountsConfig(configPath, expectedAccounts) {
|
|
3264
|
+
if (!(await fileExists(configPath))) {
|
|
3265
|
+
return { ok: false, detail: `${configPath} does not exist` };
|
|
3266
|
+
}
|
|
3267
|
+
const config = await readJsonFile(configPath);
|
|
3268
|
+
const source = config?.sources?.paddle;
|
|
3269
|
+
if (!source || source.enabled !== true) {
|
|
3270
|
+
return { ok: false, detail: 'sources.paddle.enabled is not true' };
|
|
3271
|
+
}
|
|
3272
|
+
if (source.mode !== 'command') {
|
|
3273
|
+
return { ok: false, detail: 'sources.paddle.mode is not command' };
|
|
3274
|
+
}
|
|
3275
|
+
const configuredAccounts = Array.isArray(source.accounts) ? source.accounts : [];
|
|
3276
|
+
if (configuredAccounts.length === 0) {
|
|
3277
|
+
return { ok: false, detail: 'sources.paddle.accounts contains no account' };
|
|
3278
|
+
}
|
|
3279
|
+
const configuredIds = new Set(configuredAccounts.map((account) => String(account?.id || account?.key || '').trim()).filter(Boolean));
|
|
3280
|
+
const missingIds = expectedAccounts
|
|
3281
|
+
.map((account) => String(account?.id || '').trim())
|
|
3282
|
+
.filter((id) => id && !configuredIds.has(id));
|
|
3283
|
+
if (missingIds.length > 0) {
|
|
3284
|
+
return { ok: false, detail: `sources.paddle.accounts is missing configured account id(s): ${missingIds.join(', ')}` };
|
|
3285
|
+
}
|
|
3286
|
+
return { ok: true, detail: `${configuredAccounts.length} Paddle account(s) configured` };
|
|
3287
|
+
}
|
|
3062
3288
|
async function upsertCoolifyConfig(configPath, { baseUrl, tokenEnv = 'COOLIFY_API_TOKEN' }) {
|
|
3063
3289
|
if (!(await fileExists(configPath)))
|
|
3064
3290
|
return false;
|
|
@@ -3356,21 +3582,57 @@ async function guideRevenueCatConnector(rl, secrets) {
|
|
|
3356
3582
|
if (apiKey)
|
|
3357
3583
|
secrets.REVENUECAT_API_KEY = apiKey;
|
|
3358
3584
|
}
|
|
3585
|
+
function paddleAccountIdFromLabel(label, index) {
|
|
3586
|
+
const normalized = String(label || '')
|
|
3587
|
+
.trim()
|
|
3588
|
+
.toLowerCase()
|
|
3589
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
3590
|
+
.replace(/^_+|_+$/g, '');
|
|
3591
|
+
return normalized || `paddle_${index + 1}`;
|
|
3592
|
+
}
|
|
3593
|
+
function paddleTokenEnvForAccount(index, label) {
|
|
3594
|
+
if (index === 0)
|
|
3595
|
+
return 'PADDLE_API_KEY';
|
|
3596
|
+
const suffix = paddleAccountIdFromLabel(label, index).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
3597
|
+
const base = suffix && suffix !== `PADDLE_${index + 1}` ? `PADDLE_API_KEY_${suffix}` : `PADDLE_API_KEY_${index + 1}`;
|
|
3598
|
+
return base.replace(/_+/g, '_');
|
|
3599
|
+
}
|
|
3359
3600
|
async function guidePaddleConnector(rl, secrets) {
|
|
3360
3601
|
printSection('Paddle Billing metrics', [
|
|
3361
3602
|
'Use this when OpenClaw should read web checkout, revenue, MRR, refunds, chargebacks, and active subscriber metrics.',
|
|
3362
3603
|
]);
|
|
3363
|
-
process.stdout.write('\nCreate or update a Paddle API key here:\n https://vendors.paddle.com/authentication\n\n');
|
|
3604
|
+
process.stdout.write('\nCreate or update a scoped Paddle API key here:\n https://vendors.paddle.com/authentication-v2\n\n');
|
|
3364
3605
|
printBullets([
|
|
3365
3606
|
'Open Paddle > Developer Tools > Authentication.',
|
|
3366
|
-
'
|
|
3367
|
-
'
|
|
3607
|
+
'Use the API keys tab and create a new live API key.',
|
|
3608
|
+
'Minimum: grant `metrics.read` so account-level revenue, MRR, refunds, chargebacks, subscribers, and checkout conversion work.',
|
|
3609
|
+
'Recommended for better Growth Engineer analysis: grant all available read-only permissions (`*.read`), including products, prices, discounts, customers, transactions, subscriptions, adjustments, reports, and notifications.',
|
|
3610
|
+
'Do not grant any write permissions (`*.write`) unless another workflow explicitly needs them.',
|
|
3368
3611
|
'Do not select or hard-code a single product in the wizard; the Growth Engineer should keep account-level metrics context.',
|
|
3369
3612
|
'Paste the key here so it is stored only in the local chmod 600 secrets file.',
|
|
3370
3613
|
]);
|
|
3371
|
-
const
|
|
3372
|
-
|
|
3373
|
-
|
|
3614
|
+
const accounts = [];
|
|
3615
|
+
let index = 0;
|
|
3616
|
+
while (true) {
|
|
3617
|
+
const label = await ask(rl, index === 0 ? 'Paddle account label' : 'Next Paddle account label (empty = done)', index === 0 ? 'Paddle' : '');
|
|
3618
|
+
if (!label.trim())
|
|
3619
|
+
break;
|
|
3620
|
+
const tokenEnv = paddleTokenEnvForAccount(index, label);
|
|
3621
|
+
const apiKey = await maybePromptSecret(rl, `Paste ${tokenEnv} into this local terminal`, tokenEnv);
|
|
3622
|
+
if (apiKey)
|
|
3623
|
+
secrets[tokenEnv] = apiKey;
|
|
3624
|
+
accounts.push({
|
|
3625
|
+
id: paddleAccountIdFromLabel(label, index),
|
|
3626
|
+
label: label.trim(),
|
|
3627
|
+
tokenEnv,
|
|
3628
|
+
environment: 'live',
|
|
3629
|
+
});
|
|
3630
|
+
index += 1;
|
|
3631
|
+
const addAnother = await askYesNo(rl, 'Add another Paddle account?', false);
|
|
3632
|
+
if (!addAnother)
|
|
3633
|
+
break;
|
|
3634
|
+
}
|
|
3635
|
+
return accounts;
|
|
3374
3636
|
}
|
|
3375
3637
|
async function guideSeoConnector(rl, secrets) {
|
|
3376
3638
|
printSection('SEO / Google Search Console / DataForSEO', [
|
|
@@ -3402,10 +3664,15 @@ async function guideSeoConnector(rl, secrets) {
|
|
|
3402
3664
|
secrets.DATAFORSEO_PASSWORD = password;
|
|
3403
3665
|
}
|
|
3404
3666
|
}
|
|
3405
|
-
function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
3667
|
+
function buildAccountSignalExtraSourceConfig(key, existing = {}, accounts = []) {
|
|
3406
3668
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3407
3669
|
if (!definition)
|
|
3408
3670
|
return existing;
|
|
3671
|
+
const accountConfig = accounts.length > 0
|
|
3672
|
+
? {
|
|
3673
|
+
accounts: mergeConnectorAccounts(existing.accounts, accounts),
|
|
3674
|
+
}
|
|
3675
|
+
: {};
|
|
3409
3676
|
return {
|
|
3410
3677
|
...buildExtraSourceConfig(definition.service, {
|
|
3411
3678
|
key: definition.key,
|
|
@@ -3429,9 +3696,28 @@ function buildAccountSignalExtraSourceConfig(key, existing = {}) {
|
|
|
3429
3696
|
signalKind: definition.sourceKind,
|
|
3430
3697
|
experimental: Boolean(definition.experimental),
|
|
3431
3698
|
hint: existing.hint || definition.signalHint,
|
|
3699
|
+
...accountConfig,
|
|
3432
3700
|
};
|
|
3433
3701
|
}
|
|
3434
|
-
|
|
3702
|
+
function mergeConnectorAccounts(existingAccounts, nextAccounts) {
|
|
3703
|
+
const merged = new Map();
|
|
3704
|
+
for (const account of Array.isArray(existingAccounts) ? existingAccounts : []) {
|
|
3705
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3706
|
+
if (id)
|
|
3707
|
+
merged.set(id, account);
|
|
3708
|
+
}
|
|
3709
|
+
for (const account of nextAccounts) {
|
|
3710
|
+
const id = String(account?.id || account?.key || account?.label || '').trim();
|
|
3711
|
+
if (!id)
|
|
3712
|
+
continue;
|
|
3713
|
+
merged.set(id, {
|
|
3714
|
+
...(merged.get(id) || {}),
|
|
3715
|
+
...account,
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
return [...merged.values()];
|
|
3719
|
+
}
|
|
3720
|
+
async function upsertAccountSignalConnectorConfig(configPath, key, accounts = []) {
|
|
3435
3721
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3436
3722
|
if (!definition)
|
|
3437
3723
|
return false;
|
|
@@ -3440,7 +3726,7 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
|
3440
3726
|
const extra = Array.isArray(sources.extra) ? sources.extra : [];
|
|
3441
3727
|
const nextExtra = extra.filter((source) => String(source?.key || source?.service || '') !== definition.key);
|
|
3442
3728
|
const existing = extra.find((source) => String(source?.key || source?.service || '') === definition.key) || {};
|
|
3443
|
-
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing));
|
|
3729
|
+
nextExtra.push(buildAccountSignalExtraSourceConfig(key, existing, accounts));
|
|
3444
3730
|
config.sources = {
|
|
3445
3731
|
...sources,
|
|
3446
3732
|
extra: nextExtra,
|
|
@@ -3448,28 +3734,58 @@ async function upsertAccountSignalConnectorConfig(configPath, key) {
|
|
|
3448
3734
|
await writeJsonFile(configPath, config);
|
|
3449
3735
|
return true;
|
|
3450
3736
|
}
|
|
3737
|
+
function accountSignalTokenEnvForAccount(baseEnv, key, index, label) {
|
|
3738
|
+
if (index === 0)
|
|
3739
|
+
return baseEnv;
|
|
3740
|
+
const suffix = toConfigId(label || key, `${key}_${index + 1}`).toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
3741
|
+
return `${baseEnv}_${suffix}`.replace(/_+/g, '_');
|
|
3742
|
+
}
|
|
3451
3743
|
async function guideAccountSignalConnector(rl, secrets, key) {
|
|
3452
3744
|
const definition = getAccountSignalConnectorDefinition(key);
|
|
3453
3745
|
if (!definition)
|
|
3454
|
-
return;
|
|
3746
|
+
return [];
|
|
3455
3747
|
printSection(definition.label, [
|
|
3456
3748
|
definition.summary,
|
|
3457
3749
|
'Setup is account-wide. Do not paste project IDs, app IDs, product IDs, package names, paywall IDs, service names, or tags here.',
|
|
3458
3750
|
]);
|
|
3459
3751
|
process.stdout.write(`Docs: ${definition.docsUrl}\n\n`);
|
|
3460
3752
|
printBullets(definition.steps);
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3753
|
+
const accounts = [];
|
|
3754
|
+
let index = 0;
|
|
3755
|
+
while (true) {
|
|
3756
|
+
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, '') : '');
|
|
3757
|
+
if (!label.trim())
|
|
3758
|
+
break;
|
|
3759
|
+
const credentialEnvs = {};
|
|
3760
|
+
for (const credential of definition.credentials) {
|
|
3761
|
+
const envName = accountSignalTokenEnvForAccount(credential.env, key, index, label);
|
|
3762
|
+
const defaultValue = index === 0 ? credential.defaultValue ?? process.env[credential.env] ?? '' : '';
|
|
3763
|
+
const prompt = envName === credential.env ? credential.prompt : `${credential.prompt.replace(credential.env, envName)}`;
|
|
3764
|
+
const value = credential.optional
|
|
3765
|
+
? await maybePromptSecret(rl, prompt, envName)
|
|
3766
|
+
: await maybePromptSecret(rl, prompt, envName);
|
|
3767
|
+
const finalValue = value || defaultValue;
|
|
3768
|
+
credentialEnvs[credential.env] = envName;
|
|
3769
|
+
if (finalValue)
|
|
3770
|
+
secrets[envName] = finalValue;
|
|
3771
|
+
else if (!credential.optional) {
|
|
3772
|
+
process.stdout.write(`${envName} was not saved. ${definition.label} setup remains pending for ${label}; rerun this wizard when ready.\n`);
|
|
3773
|
+
}
|
|
3471
3774
|
}
|
|
3775
|
+
accounts.push({
|
|
3776
|
+
id: toConfigId(label, `${key}_${index + 1}`),
|
|
3777
|
+
label: label.trim(),
|
|
3778
|
+
credentialEnvs,
|
|
3779
|
+
tokenEnv: credentialEnvs[definition.credentials[0]?.env] || definition.credentials[0]?.env || null,
|
|
3780
|
+
accountWide: true,
|
|
3781
|
+
projectScope: 'discover_from_account',
|
|
3782
|
+
});
|
|
3783
|
+
index += 1;
|
|
3784
|
+
const addAnother = await askYesNo(rl, `Add another ${definition.label} account?`, false);
|
|
3785
|
+
if (!addAnother)
|
|
3786
|
+
break;
|
|
3472
3787
|
}
|
|
3788
|
+
return accounts;
|
|
3473
3789
|
}
|
|
3474
3790
|
async function guideSentryConnector(rl, secrets) {
|
|
3475
3791
|
printSection('Sentry / GlitchTip', [
|
|
@@ -3694,6 +4010,23 @@ async function filesHaveSameContent(leftPath, rightPath) {
|
|
|
3694
4010
|
return false;
|
|
3695
4011
|
}
|
|
3696
4012
|
}
|
|
4013
|
+
function getSelfUpdateSkillCandidates(workspaceRoot) {
|
|
4014
|
+
const explicit = String(process.env.OPENCLAW_GROWTH_SKILL_SLUG || '').trim();
|
|
4015
|
+
const uniqueSlugs = [...new Set([explicit, ...SELF_UPDATE_SKILL_SLUG_CANDIDATES].filter(Boolean))];
|
|
4016
|
+
return uniqueSlugs.map((slug) => {
|
|
4017
|
+
const skillRoot = path.join(workspaceRoot, 'skills', slug);
|
|
4018
|
+
return {
|
|
4019
|
+
slug,
|
|
4020
|
+
skillRoot,
|
|
4021
|
+
originPath: path.join(skillRoot, '.clawhub/origin.json'),
|
|
4022
|
+
wizardPath: path.join(skillRoot, 'scripts/openclaw-growth-wizard.mjs'),
|
|
4023
|
+
bootstrapPath: path.join(skillRoot, 'scripts/bootstrap-openclaw-workspace.sh'),
|
|
4024
|
+
};
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
function resolveInstalledSelfUpdateSkill(workspaceRoot) {
|
|
4028
|
+
return getSelfUpdateSkillCandidates(workspaceRoot).find((candidate) => existsSync(candidate.originPath)) || null;
|
|
4029
|
+
}
|
|
3697
4030
|
async function maybeSelfUpdateFromClawHub(args) {
|
|
3698
4031
|
if (args.noSelfUpdate)
|
|
3699
4032
|
return false;
|
|
@@ -3704,26 +4037,27 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
3704
4037
|
if (isFalseyEnv(process.env.OPENCLAW_GROWTH_SELF_UPDATE))
|
|
3705
4038
|
return false;
|
|
3706
4039
|
const workspaceRoot = process.cwd();
|
|
3707
|
-
const
|
|
3708
|
-
if (!
|
|
4040
|
+
const installedSkill = resolveInstalledSelfUpdateSkill(workspaceRoot);
|
|
4041
|
+
if (!installedSkill)
|
|
3709
4042
|
return false;
|
|
3710
4043
|
if (!(await commandExists('npx')))
|
|
3711
4044
|
return false;
|
|
3712
|
-
const force = String(process.env.OPENCLAW_GROWTH_SELF_UPDATE || '').trim().toLowerCase() === 'always';
|
|
4045
|
+
const force = args.connectorWizard || String(process.env.OPENCLAW_GROWTH_SELF_UPDATE || '').trim().toLowerCase() === 'always';
|
|
3713
4046
|
if (!(await shouldRunSelfUpdate(workspaceRoot, force)))
|
|
3714
4047
|
return false;
|
|
3715
|
-
const beforeOrigin = await readJsonIfPresent(
|
|
4048
|
+
const beforeOrigin = await readJsonIfPresent(installedSkill.originPath).catch(() => null);
|
|
3716
4049
|
const beforeVersion = String(beforeOrigin?.installedVersion || '');
|
|
3717
|
-
process.stdout.write(
|
|
3718
|
-
const updateResult = await runCommandCaptureWithTimeout(
|
|
3719
|
-
const afterOrigin = await readJsonIfPresent(
|
|
4050
|
+
process.stdout.write(`Checking for Growth Engineer skill updates (${installedSkill.slug})...\n`);
|
|
4051
|
+
const updateResult = await runCommandCaptureWithTimeout(`npx -y clawhub --no-input --dir skills update ${quote(installedSkill.slug)} --force`, { timeoutMs: 120_000 });
|
|
4052
|
+
const afterOrigin = await readJsonIfPresent(installedSkill.originPath).catch(() => null);
|
|
3720
4053
|
const afterVersion = String(afterOrigin?.installedVersion || beforeVersion || '');
|
|
3721
4054
|
const workspaceWizardPath = path.resolve(process.argv[1] || 'scripts/openclaw-growth-wizard.mjs');
|
|
3722
|
-
const
|
|
3723
|
-
const runtimeOutdated = !(await filesHaveSameContent(workspaceWizardPath, skillWizardPath));
|
|
4055
|
+
const runtimeOutdated = !(await filesHaveSameContent(workspaceWizardPath, installedSkill.wizardPath));
|
|
3724
4056
|
await writeSelfUpdateState(workspaceRoot, {
|
|
3725
4057
|
lastCheckedAt: new Date().toISOString(),
|
|
3726
4058
|
ok: updateResult.ok,
|
|
4059
|
+
skillSlug: installedSkill.slug,
|
|
4060
|
+
skillRoot: installedSkill.skillRoot,
|
|
3727
4061
|
previousVersion: beforeVersion || null,
|
|
3728
4062
|
installedVersion: afterVersion || null,
|
|
3729
4063
|
}).catch(() => { });
|
|
@@ -3741,7 +4075,7 @@ async function maybeSelfUpdateFromClawHub(args) {
|
|
|
3741
4075
|
else {
|
|
3742
4076
|
process.stdout.write('Refreshing workspace runtime from the installed OpenClaw Growth Engineer skill...\n');
|
|
3743
4077
|
}
|
|
3744
|
-
const bootstrapResult = await runCommandCaptureWithTimeout(
|
|
4078
|
+
const bootstrapResult = await runCommandCaptureWithTimeout(`bash ${quote(installedSkill.bootstrapPath)}`, { timeoutMs: 60_000 });
|
|
3745
4079
|
if (!bootstrapResult.ok) {
|
|
3746
4080
|
process.stdout.write(`${ANSI.dim}Workspace runtime refresh failed; continuing with current process.${ANSI.reset}\n`);
|
|
3747
4081
|
return false;
|
|
@@ -3760,6 +4094,7 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3760
4094
|
process.stdout.write('\n');
|
|
3761
4095
|
const secrets = {};
|
|
3762
4096
|
let sentryAccounts = [];
|
|
4097
|
+
let paddleAccounts = [];
|
|
3763
4098
|
let coolifyConfig = null;
|
|
3764
4099
|
if (selected.includes('analytics')) {
|
|
3765
4100
|
let forceFreshAnalyticsToken = shouldForceFreshAnalyticsToken(healthByConnector);
|
|
@@ -3808,12 +4143,13 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3808
4143
|
if (selected.includes('paddle')) {
|
|
3809
4144
|
while (true) {
|
|
3810
4145
|
clearTerminal();
|
|
3811
|
-
await guidePaddleConnector(rl, secrets);
|
|
4146
|
+
paddleAccounts = await guidePaddleConnector(rl, secrets);
|
|
3812
4147
|
const check = await runImmediateConnectorHealthCheck({
|
|
3813
4148
|
rl,
|
|
3814
4149
|
configPath: args.config,
|
|
3815
4150
|
connector: 'paddle',
|
|
3816
4151
|
secrets,
|
|
4152
|
+
paddleAccounts,
|
|
3817
4153
|
});
|
|
3818
4154
|
if (!check.retry)
|
|
3819
4155
|
break;
|
|
@@ -3882,8 +4218,8 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3882
4218
|
for (const connector of selected.filter(isAccountSignalConnector)) {
|
|
3883
4219
|
while (true) {
|
|
3884
4220
|
clearTerminal();
|
|
3885
|
-
await guideAccountSignalConnector(rl, secrets, connector);
|
|
3886
|
-
await upsertAccountSignalConnectorConfig(args.config, connector);
|
|
4221
|
+
const accountSignalAccounts = await guideAccountSignalConnector(rl, secrets, connector);
|
|
4222
|
+
await upsertAccountSignalConnectorConfig(args.config, connector, accountSignalAccounts);
|
|
3887
4223
|
const check = await runImmediateConnectorHealthCheck({
|
|
3888
4224
|
rl,
|
|
3889
4225
|
configPath: args.config,
|
|
@@ -3910,6 +4246,12 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3910
4246
|
process.stdout.write(`Configured ${sentryAccounts.length} Sentry-compatible account(s) in ${args.config}.\n`);
|
|
3911
4247
|
}
|
|
3912
4248
|
}
|
|
4249
|
+
if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
|
|
4250
|
+
const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
|
|
4251
|
+
if (readiness.ok) {
|
|
4252
|
+
process.stdout.write(`Configured ${paddleAccounts.length} Paddle account(s) in ${args.config}.\n`);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
3913
4255
|
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
3914
4256
|
process.stdout.write(`Configured Coolify monitoring for ${coolifyConfig.baseUrl} in ${args.config}.\n`);
|
|
3915
4257
|
}
|
|
@@ -3934,6 +4276,19 @@ async function runConnectorSetupSteps({ rl, args, selected, healthByConnector, a
|
|
|
3934
4276
|
});
|
|
3935
4277
|
}
|
|
3936
4278
|
}
|
|
4279
|
+
if (paddleAccounts.length > 0 && await upsertPaddleAccountsConfig(args.config, paddleAccounts)) {
|
|
4280
|
+
const readiness = await verifyPaddleAccountsConfig(args.config, paddleAccounts);
|
|
4281
|
+
if (readiness.ok) {
|
|
4282
|
+
process.stdout.write(`Paddle account config is up to date in ${args.config}.\n`);
|
|
4283
|
+
}
|
|
4284
|
+
else {
|
|
4285
|
+
postSetupBlockers.push({
|
|
4286
|
+
check: 'connection:paddle',
|
|
4287
|
+
detail: readiness.detail,
|
|
4288
|
+
remediation: 'Rerun Paddle setup so the active config persists sources.paddle.enabled=true and sources.paddle.accounts[].',
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
3937
4292
|
if (coolifyConfig?.baseUrl && await upsertCoolifyConfig(args.config, coolifyConfig)) {
|
|
3938
4293
|
process.stdout.write(`Coolify config is up to date in ${args.config}.\n`);
|
|
3939
4294
|
}
|
|
@@ -3976,7 +4331,8 @@ async function runConnectorSetupWizard(args) {
|
|
|
3976
4331
|
clearTerminal();
|
|
3977
4332
|
printConnectorIntro();
|
|
3978
4333
|
await migrateRuntimeSourceCommandsFile(args.config);
|
|
3979
|
-
const
|
|
4334
|
+
const healthCheckConnectors = await connectorKeysForHealthCheck(args.config);
|
|
4335
|
+
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(args.config, onProgress, healthCheckConnectors), healthCheckConnectors);
|
|
3980
4336
|
const existingFixes = connectorKeysNeedingAttention(healthByConnector);
|
|
3981
4337
|
const requestedConnectors = args.connectors ? parseConnectorList(args.connectors) : [];
|
|
3982
4338
|
const chosenConnectors = requestedConnectors.length > 0
|
|
@@ -4900,7 +5256,8 @@ async function askInputSourceConfig(rl, config, configPath) {
|
|
|
4900
5256
|
config = migrateRuntimeSourceCommands(config, configPath);
|
|
4901
5257
|
await ensureDirForFile(configPath);
|
|
4902
5258
|
await writeJsonFile(configPath, config);
|
|
4903
|
-
const
|
|
5259
|
+
const healthCheckConnectors = await connectorKeysForHealthCheck(configPath);
|
|
5260
|
+
const healthByConnector = await withConnectorHealthLoading((onProgress) => getConnectorPickerHealth(configPath, onProgress, healthCheckConnectors), healthCheckConnectors);
|
|
4904
5261
|
const selected = await askConnectorSelectionWithHealth(rl, healthByConnector, getInputChannelInitialSelection(config), {
|
|
4905
5262
|
introTitle: 'Input channels',
|
|
4906
5263
|
introDetail: null,
|