@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 fix:\n');
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 finished = items.filter((item) => !['pending', 'running'].includes(String(item.status || ''))).length;
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
- process.stdout.write(`${finished}/${items.length} checks finished.\n\n`);
2399
- for (const item of items) {
2400
- process.stdout.write(`[${healthStatusLabel(item.status)}] ${item.label}: ${item.detail}\n`);
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
- renderHealthProgress(plan, `${message}\nDo not close this terminal yet.`, 'Connector setup test');
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
- const primaryFinished = primaryProgressItemsFinished(plan);
2516
- if (primaryFinished) {
2517
- updateProgressItem(plan, 'finalize', 'running', 'command still running; parsing final output and follow-up work');
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
- }, { env, timeoutMs: 180_000 });
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
- Object.assign(process.env, secrets);
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 (reports key, empty = skip)', process.env.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
- const bootstrapIssuerId = await ask(rl, 'ASC_BOOTSTRAP_ISSUER_ID', issuerIdDefault);
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`);