@analyticscli/growth-engineer 0.1.1-preview.20 → 0.1.1-preview.22
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.
|
@@ -16,6 +16,7 @@ const ENABLE_ISOLATED_SECRET_RUNNER_WIZARD = false;
|
|
|
16
16
|
const DEFAULT_GROWTH_INTERVAL_MINUTES = 90;
|
|
17
17
|
const DEFAULT_CONNECTOR_HEALTH_INTERVAL_MINUTES = 360;
|
|
18
18
|
const DEFAULT_SCHEDULER_PROOF_PATH = 'data/openclaw-growth-engineer/runtime/scheduler-proof.jsonl';
|
|
19
|
+
const DELETE_SECRET = '__OPENCLAW_DELETE_SECRET__';
|
|
19
20
|
const GROWTH_ENGINEER_PACKAGE_SPEC = process.env.OPENCLAW_GROWTH_ENGINEER_PACKAGE || '@analyticscli/growth-engineer@preview';
|
|
20
21
|
const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
const ACCOUNT_SIGNAL_CONNECTOR_KEYS = [
|
|
@@ -2126,6 +2127,9 @@ function getPassingConnectorKeys(payload, failedConnectors = new Set()) {
|
|
|
2126
2127
|
}
|
|
2127
2128
|
function summarizeFailureReason(detail) {
|
|
2128
2129
|
const text = String(detail || '').replace(/\s+/g, ' ').trim();
|
|
2130
|
+
if (/ASC .*\.p8 private key is invalid|invalid private key|failed to parse|sequence truncated|malformed|asn1/i.test(text)) {
|
|
2131
|
+
return 'ASC auth failed: the .p8 key could not be parsed';
|
|
2132
|
+
}
|
|
2129
2133
|
if (/token has been revoked/i.test(text))
|
|
2130
2134
|
return 'token has been revoked';
|
|
2131
2135
|
if (/unauthorized|UNAUTHORIZED/i.test(text))
|
|
@@ -2168,6 +2172,9 @@ function summarizeFailureFix(connector, blockers) {
|
|
|
2168
2172
|
return 'Paste a Coolify base URL and read-only API token from Keys & Tokens / API tokens, then rerun setup.';
|
|
2169
2173
|
}
|
|
2170
2174
|
if (connector === 'asc') {
|
|
2175
|
+
if (/invalid|truncated|malformed|private key/i.test(combined)) {
|
|
2176
|
+
return 'Use the original downloaded AuthKey_<KEY_ID>.p8 for the Reports key. Old pasted ASC_PRIVATE_KEY values are removed when you choose a file path.';
|
|
2177
|
+
}
|
|
2171
2178
|
return 'Rerun ASC setup and verify ASC credentials, key role access, and `asc apps list --output json`.';
|
|
2172
2179
|
}
|
|
2173
2180
|
if (isAccountSignalConnector(connector)) {
|
|
@@ -2216,21 +2223,15 @@ function printConciseSetupBlockers(payload, command, options = {}) {
|
|
|
2216
2223
|
process.stdout.write(`Live checks passed: ${passingConnectors.map(connectorTitle).join(', ')}.\n`);
|
|
2217
2224
|
}
|
|
2218
2225
|
if (groups.size > 0) {
|
|
2219
|
-
process.stdout.write('\nNeeds
|
|
2226
|
+
process.stdout.write('\nNeeds attention:\n');
|
|
2220
2227
|
for (const [connector, connectorBlockers] of groups.entries()) {
|
|
2221
2228
|
const primary = connectorBlockers[0] || {};
|
|
2222
|
-
const reasons = [
|
|
2223
|
-
...new Set(connectorBlockers
|
|
2224
|
-
.map((blocker) => summarizeFailureReason(blocker.detail || blocker.check))
|
|
2225
|
-
.filter(Boolean)),
|
|
2226
|
-
];
|
|
2227
2229
|
process.stdout.write(`- ${connectorTitle(connector)}: ${summarizeFailureReason(primary.detail || primary.check)}\n`);
|
|
2228
|
-
process.stdout.write(` Why: ${reasons.join('; ')}.\n`);
|
|
2229
2230
|
process.stdout.write(` Fix: ${summarizeFailureFix(connector, connectorBlockers)}\n`);
|
|
2230
2231
|
}
|
|
2231
2232
|
}
|
|
2232
2233
|
printDeferredSetupNotes(blockers, focusConnectors);
|
|
2233
|
-
if (groups.size > 0 || !options.hideRerunWhenClean) {
|
|
2234
|
+
if (!options.hideRerun && (groups.size > 0 || !options.hideRerunWhenClean)) {
|
|
2234
2235
|
process.stdout.write(`\nRerun: ${command}\n`);
|
|
2235
2236
|
}
|
|
2236
2237
|
}
|
|
@@ -2375,9 +2376,9 @@ function isDeferredGitHubFailure(failure) {
|
|
|
2375
2376
|
return (name === 'project:github-repo' ||
|
|
2376
2377
|
(name === 'connection:github' && /project\.githubRepo|repo is missing|repo is not configured/i.test(detail)));
|
|
2377
2378
|
}
|
|
2378
|
-
function healthStatusLabel(status) {
|
|
2379
|
+
function healthStatusLabel(status, spinner = '') {
|
|
2379
2380
|
if (status === 'running')
|
|
2380
|
-
return 'running';
|
|
2381
|
+
return spinner ? `running ${spinner}` : 'running';
|
|
2381
2382
|
if (status === 'pass')
|
|
2382
2383
|
return 'done';
|
|
2383
2384
|
if (status === 'warn')
|
|
@@ -2386,18 +2387,27 @@ function healthStatusLabel(status) {
|
|
|
2386
2387
|
return 'needs attention';
|
|
2387
2388
|
if (status === 'deferred')
|
|
2388
2389
|
return 'deferred';
|
|
2389
|
-
return 'pending';
|
|
2390
|
+
return spinner ? `pending ${spinner}` : 'pending';
|
|
2390
2391
|
}
|
|
2391
|
-
function renderHealthProgress(items, message = 'Live checks running...', title = 'Health check') {
|
|
2392
|
+
function renderHealthProgress(items, message = 'Live checks running...', title = 'Health check', options = {}) {
|
|
2392
2393
|
if (process.stdout.isTTY)
|
|
2393
2394
|
clearTerminal();
|
|
2394
|
-
const
|
|
2395
|
+
const final = Boolean(options.final);
|
|
2396
|
+
const visibleItems = final
|
|
2397
|
+
? items.filter((item) => !['pending', 'running'].includes(String(item.status || '')) && item.key !== 'finalize')
|
|
2398
|
+
: items;
|
|
2399
|
+
const finished = visibleItems.filter((item) => !['pending', 'running'].includes(String(item.status || ''))).length;
|
|
2395
2400
|
process.stdout.write(`${title}\n`);
|
|
2396
2401
|
process.stdout.write('------------\n');
|
|
2397
2402
|
process.stdout.write(`${message}\n\n`);
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2403
|
+
if (final) {
|
|
2404
|
+
process.stdout.write('');
|
|
2405
|
+
}
|
|
2406
|
+
else {
|
|
2407
|
+
process.stdout.write(`${finished}/${visibleItems.length} checks finished.\n\n`);
|
|
2408
|
+
}
|
|
2409
|
+
for (const item of visibleItems) {
|
|
2410
|
+
process.stdout.write(`[${healthStatusLabel(item.status, options.spinner || '')}] ${item.label}: ${item.detail}\n`);
|
|
2401
2411
|
}
|
|
2402
2412
|
}
|
|
2403
2413
|
function updateHealthProgress(items, event) {
|
|
@@ -2508,22 +2518,41 @@ function updateProgressItem(items, key, status, detail) {
|
|
|
2508
2518
|
}
|
|
2509
2519
|
async function runSetupCommandWithProgress(command, env, selected, message) {
|
|
2510
2520
|
const plan = buildSetupTestProgressPlan(selected);
|
|
2511
|
-
|
|
2521
|
+
const spinnerFrames = ['-', '\\', '|', '/'];
|
|
2522
|
+
let spinnerIndex = 0;
|
|
2523
|
+
let currentMessage = message;
|
|
2524
|
+
const render = (nextMessage = currentMessage, options = {}) => {
|
|
2525
|
+
currentMessage = nextMessage;
|
|
2526
|
+
renderHealthProgress(plan, currentMessage, 'Connector setup test', {
|
|
2527
|
+
...options,
|
|
2528
|
+
spinner: spinnerFrames[spinnerIndex++ % spinnerFrames.length],
|
|
2529
|
+
});
|
|
2530
|
+
};
|
|
2531
|
+
render(message);
|
|
2532
|
+
const spinnerInterval = process.stdout.isTTY
|
|
2533
|
+
? setInterval(() => render(currentMessage), 800)
|
|
2534
|
+
: null;
|
|
2512
2535
|
const progressCommand = command.includes('--progress-json') ? command : `${command} --progress-json`;
|
|
2513
2536
|
const result = await runCommandCaptureWithProgress(progressCommand, (event) => {
|
|
2514
|
-
if (updateHealthProgress(plan, event))
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
const message = primaryFinished
|
|
2520
|
-
? 'Checks finished. Finalizing result; do not close this terminal yet.'
|
|
2521
|
-
: 'Connector setup test is still running. Do not close this terminal yet.';
|
|
2522
|
-
renderHealthProgress(plan, message, 'Connector setup test');
|
|
2537
|
+
if (!updateHealthProgress(plan, event))
|
|
2538
|
+
return;
|
|
2539
|
+
const primaryFinished = primaryProgressItemsFinished(plan);
|
|
2540
|
+
if (primaryFinished) {
|
|
2541
|
+
updateProgressItem(plan, 'finalize', 'running', 'finishing');
|
|
2523
2542
|
}
|
|
2524
|
-
|
|
2543
|
+
render(primaryFinished ? 'Finishing setup test...' : 'Testing connector setup...');
|
|
2544
|
+
}, { env, timeoutMs: 180_000 }).finally(() => {
|
|
2545
|
+
if (spinnerInterval)
|
|
2546
|
+
clearInterval(spinnerInterval);
|
|
2547
|
+
});
|
|
2548
|
+
const payload = parseJsonFromStdout(result.stdout);
|
|
2549
|
+
if (Array.isArray(payload?.blockers) && payload.blockers.length > 0) {
|
|
2550
|
+
if (process.stdout.isTTY)
|
|
2551
|
+
clearTerminal();
|
|
2552
|
+
return result;
|
|
2553
|
+
}
|
|
2525
2554
|
updateProgressItem(plan, 'finalize', 'pass', 'result received');
|
|
2526
|
-
renderHealthProgress(plan, 'Connector setup test finished.', 'Connector setup test');
|
|
2555
|
+
renderHealthProgress(plan, 'Connector setup test finished.', 'Connector setup test', { final: true });
|
|
2527
2556
|
return result;
|
|
2528
2557
|
}
|
|
2529
2558
|
async function saveSecretsImmediately(secrets) {
|
|
@@ -2531,7 +2560,7 @@ async function saveSecretsImmediately(secrets) {
|
|
|
2531
2560
|
return false;
|
|
2532
2561
|
const secretsFile = resolveSecretsFile();
|
|
2533
2562
|
await writeSecretsFile(secretsFile, secrets);
|
|
2534
|
-
|
|
2563
|
+
applySecretsToProcessEnv(secrets);
|
|
2535
2564
|
process.stdout.write(`Saved local secrets to ${secretsFile} with chmod 600.\n`);
|
|
2536
2565
|
return true;
|
|
2537
2566
|
}
|
|
@@ -2556,6 +2585,7 @@ async function runImmediateConnectorHealthCheck({ rl, configPath, connector, sec
|
|
|
2556
2585
|
printConciseSetupBlockers(payload, command, {
|
|
2557
2586
|
focusConnectors: [connector],
|
|
2558
2587
|
hideRerunWhenClean: true,
|
|
2588
|
+
hideRerun: true,
|
|
2559
2589
|
});
|
|
2560
2590
|
const retry = await askYesNo(rl, `Re-enter ${connectorLabel(connector)} configuration now?`, true);
|
|
2561
2591
|
return { ok: false, retry, result, payload };
|
|
@@ -2722,6 +2752,10 @@ async function readSecretsFile(filePath) {
|
|
|
2722
2752
|
async function writeSecretsFile(filePath, nextValues) {
|
|
2723
2753
|
const current = await readSecretsFile(filePath);
|
|
2724
2754
|
for (const [key, value] of Object.entries(nextValues)) {
|
|
2755
|
+
if (value === DELETE_SECRET) {
|
|
2756
|
+
current.delete(key);
|
|
2757
|
+
continue;
|
|
2758
|
+
}
|
|
2725
2759
|
if (value.trim())
|
|
2726
2760
|
current.set(key, value.trim());
|
|
2727
2761
|
}
|
|
@@ -2735,6 +2769,16 @@ async function writeSecretsFile(filePath, nextValues) {
|
|
|
2735
2769
|
await fs.writeFile(filePath, lines.join('\n'), { encoding: 'utf8', mode: 0o600 });
|
|
2736
2770
|
await fs.chmod(filePath, 0o600);
|
|
2737
2771
|
}
|
|
2772
|
+
function applySecretsToProcessEnv(nextValues) {
|
|
2773
|
+
for (const [key, value] of Object.entries(nextValues)) {
|
|
2774
|
+
if (value === DELETE_SECRET) {
|
|
2775
|
+
delete process.env[key];
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2778
|
+
if (value.trim())
|
|
2779
|
+
process.env[key] = value.trim();
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2738
2782
|
function renderBashSingleQuoted(value) {
|
|
2739
2783
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
2740
2784
|
}
|
|
@@ -3911,7 +3955,7 @@ async function guideAscConnector(rl, secrets) {
|
|
|
3911
3955
|
process.stdout.write(`${bold('Enter the Reports key now:')}\n`);
|
|
3912
3956
|
printBullets([
|
|
3913
3957
|
`${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.`,
|
|
3958
|
+
`${bold('Issuer ID')} from the API keys page. Same value for both keys.`,
|
|
3915
3959
|
`${bold('Vendor Number')} from Sales and Trends > Reports.`,
|
|
3916
3960
|
]);
|
|
3917
3961
|
const normalKeyPath = await askAscPrivateKeyPathWithKeyId(rl, {
|
|
@@ -3922,10 +3966,12 @@ async function guideAscConnector(rl, secrets) {
|
|
|
3922
3966
|
let keyId = normalKeyPath.keyId;
|
|
3923
3967
|
if (normalKeyPath.privateKeyPath) {
|
|
3924
3968
|
secrets.ASC_PRIVATE_KEY_PATH = normalKeyPath.privateKeyPath;
|
|
3969
|
+
secrets.ASC_PRIVATE_KEY = DELETE_SECRET;
|
|
3970
|
+
secrets.ASC_PRIVATE_KEY_B64 = DELETE_SECRET;
|
|
3925
3971
|
secrets.ASC_KEY_ID = keyId;
|
|
3926
3972
|
process.stdout.write(`Inferred ASC_KEY_ID=${keyId} from ${path.basename(normalKeyPath.privateKeyPath)}.\n`);
|
|
3927
3973
|
}
|
|
3928
|
-
const issuerId = await ask(rl, 'ASC_ISSUER_ID (
|
|
3974
|
+
const issuerId = await ask(rl, 'ASC_ISSUER_ID (same for both keys, empty = skip)', process.env.ASC_ISSUER_ID || '');
|
|
3929
3975
|
if (issuerId.trim())
|
|
3930
3976
|
secrets.ASC_ISSUER_ID = issuerId.trim();
|
|
3931
3977
|
if (!normalKeyPath.privateKeyPath) {
|
|
@@ -3942,6 +3988,8 @@ async function guideAscConnector(rl, secrets) {
|
|
|
3942
3988
|
await fs.writeFile(privateKeyPath, privateKeyContent, { encoding: 'utf8', mode: 0o600 });
|
|
3943
3989
|
await fs.chmod(privateKeyPath, 0o600);
|
|
3944
3990
|
secrets.ASC_PRIVATE_KEY_PATH = privateKeyPath;
|
|
3991
|
+
secrets.ASC_PRIVATE_KEY = DELETE_SECRET;
|
|
3992
|
+
secrets.ASC_PRIVATE_KEY_B64 = DELETE_SECRET;
|
|
3945
3993
|
process.stdout.write(`Saved ASC private key to ${privateKeyPath} with chmod 600.\n`);
|
|
3946
3994
|
}
|
|
3947
3995
|
const vendorNumber = await ask(rl, 'ASC_VENDOR_NUMBER (Sales and Trends > Reports)', process.env.ASC_VENDOR_NUMBER || '');
|
|
@@ -3963,7 +4011,10 @@ async function guideAscBootstrapAdminKey(rl, issuerIdDefault = '') {
|
|
|
3963
4011
|
keyLabel: 'the temporary Admin key',
|
|
3964
4012
|
});
|
|
3965
4013
|
let bootstrapKeyId = bootstrapKeyPath.keyId;
|
|
3966
|
-
|
|
4014
|
+
let bootstrapIssuerId = String(issuerIdDefault || '').trim();
|
|
4015
|
+
if (!bootstrapIssuerId) {
|
|
4016
|
+
bootstrapIssuerId = await ask(rl, 'ASC_ISSUER_ID (same API keys page)', process.env.ASC_ISSUER_ID || '');
|
|
4017
|
+
}
|
|
3967
4018
|
if (bootstrapKeyPath.privateKeyPath) {
|
|
3968
4019
|
bootstrapEnv.ASC_BOOTSTRAP_PRIVATE_KEY_PATH = bootstrapKeyPath.privateKeyPath;
|
|
3969
4020
|
process.stdout.write(`Inferred ASC_BOOTSTRAP_KEY_ID=${bootstrapKeyId} from ${path.basename(bootstrapKeyPath.privateKeyPath)}.\n`);
|