fullstackgtm 0.45.0 → 0.47.0
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/CHANGELOG.md +115 -1
- package/INSTALL_FOR_AGENTS.md +13 -11
- package/README.md +27 -18
- package/dist/backfill.d.ts +86 -0
- package/dist/backfill.js +251 -0
- package/dist/bin.js +4 -1
- package/dist/cli/auth.d.ts +2 -0
- package/dist/cli/auth.js +119 -12
- package/dist/cli/backfill.d.ts +1 -0
- package/dist/cli/backfill.js +125 -0
- package/dist/cli/backfillRuns.d.ts +1 -0
- package/dist/cli/backfillRuns.js +187 -0
- package/dist/cli/call.js +23 -9
- package/dist/cli/enrich.js +28 -10
- package/dist/cli/fix.js +9 -7
- package/dist/cli/help.js +60 -23
- package/dist/cli/plans.js +13 -11
- package/dist/cli/shared.d.ts +4 -3
- package/dist/cli/shared.js +31 -20
- package/dist/cli/ui.d.ts +21 -0
- package/dist/cli/ui.js +53 -1
- package/dist/cli.js +37 -41
- package/dist/connector.d.ts +8 -0
- package/dist/connector.js +104 -21
- package/dist/connectors/hubspot.d.ts +8 -1
- package/dist/connectors/hubspot.js +406 -13
- package/dist/connectors/hubspotAuth.js +5 -1
- package/dist/connectors/linkedin.js +14 -14
- package/dist/connectors/salesforce.d.ts +8 -1
- package/dist/connectors/salesforce.js +163 -2
- package/dist/connectors/stripe.d.ts +37 -0
- package/dist/connectors/stripe.js +103 -31
- package/dist/enrich.d.ts +7 -0
- package/dist/enrich.js +7 -0
- package/dist/health.d.ts +11 -69
- package/dist/health.js +4 -134
- package/dist/healthScore.d.ts +71 -0
- package/dist/healthScore.js +143 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/llm.d.ts +29 -0
- package/dist/llm.js +206 -0
- package/dist/market.d.ts +39 -1
- package/dist/market.js +116 -44
- package/dist/marketClassify.d.ts +9 -1
- package/dist/marketClassify.js +10 -1
- package/dist/marketTaxonomy.d.ts +6 -1
- package/dist/marketTaxonomy.js +4 -2
- package/dist/mcp-bin.js +29 -0
- package/dist/mcp.js +117 -4
- package/dist/progress.d.ts +96 -0
- package/dist/progress.js +142 -0
- package/dist/runReport.d.ts +24 -0
- package/dist/runReport.js +139 -4
- package/dist/types.d.ts +33 -1
- package/docs/api.md +4 -2
- package/docs/architecture.md +2 -0
- package/docs/linkedin-connector-spec.md +1 -1
- package/llms.txt +3 -3
- package/package.json +10 -3
- package/skills/fullstackgtm/SKILL.md +1 -0
- package/src/backfill.ts +340 -0
- package/src/bin.ts +5 -1
- package/src/cli/auth.ts +135 -15
- package/src/cli/backfill.ts +156 -0
- package/src/cli/backfillRuns.ts +198 -0
- package/src/cli/call.ts +26 -10
- package/src/cli/enrich.ts +33 -10
- package/src/cli/fix.ts +11 -10
- package/src/cli/help.ts +61 -23
- package/src/cli/plans.ts +15 -14
- package/src/cli/shared.ts +44 -27
- package/src/cli/ui.ts +72 -1
- package/src/cli.ts +38 -41
- package/src/connector.ts +110 -16
- package/src/connectors/hubspot.ts +423 -14
- package/src/connectors/hubspotAuth.ts +5 -1
- package/src/connectors/linkedin.ts +14 -14
- package/src/connectors/salesforce.ts +168 -3
- package/src/connectors/stripe.ts +154 -34
- package/src/enrich.ts +13 -0
- package/src/health.ts +14 -213
- package/src/healthScore.ts +223 -0
- package/src/index.ts +28 -1
- package/src/llm.ts +239 -0
- package/src/market.ts +157 -44
- package/src/marketClassify.ts +18 -1
- package/src/marketTaxonomy.ts +10 -2
- package/src/mcp-bin.ts +32 -0
- package/src/mcp.ts +140 -6
- package/src/progress.ts +197 -0
- package/src/runReport.ts +159 -4
- package/src/types.ts +35 -2
- package/docs/dx-punch-list.md +0 -87
- package/docs/roadmap-to-1.0.md +0 -211
package/src/cli/help.ts
CHANGED
|
@@ -9,11 +9,15 @@ export function usage() {
|
|
|
9
9
|
and apply only explicitly approved operations.
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
fullstackgtm login
|
|
13
|
-
fullstackgtm login
|
|
14
|
-
fullstackgtm login hubspot --
|
|
15
|
-
fullstackgtm login salesforce --
|
|
16
|
-
fullstackgtm login
|
|
12
|
+
fullstackgtm login hubspot hosted browser OAuth (default; no app needed)
|
|
13
|
+
fullstackgtm login salesforce hosted browser OAuth (default; no app needed)
|
|
14
|
+
fullstackgtm login hubspot --hosted [--via <url>]
|
|
15
|
+
fullstackgtm login salesforce --hosted [--via <url>]
|
|
16
|
+
fullstackgtm login --via <hosted url> pair with a team deployment (broker only)
|
|
17
|
+
fullstackgtm login hubspot --private-token [--no-validate] advanced: paste private app token
|
|
18
|
+
fullstackgtm login hubspot --oauth --client-id <id> [--port ${DEFAULT_LOOPBACK_PORT}] [--scopes a,b] advanced: BYO HubSpot app
|
|
19
|
+
fullstackgtm login salesforce --device --client-id <consumer key> [--login-url <url>] advanced: BYO Connected App
|
|
20
|
+
fullstackgtm login salesforce --instance-url <url> [--no-validate] advanced: paste access token
|
|
17
21
|
fullstackgtm login stripe [--no-validate]
|
|
18
22
|
fullstackgtm login anthropic | openai store an LLM API key for call parse/score
|
|
19
23
|
fullstackgtm login apollo store an Apollo API key for enrich pulls\n fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)\n fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin\n fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|heyreach|broker>
|
|
@@ -21,7 +25,7 @@ Usage:
|
|
|
21
25
|
Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
|
|
22
26
|
the process list and shell history. Pipe them on stdin or enter them at the
|
|
23
27
|
interactive prompt:
|
|
24
|
-
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot
|
|
28
|
+
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
25
29
|
fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
26
30
|
cold start: scaffold icp.json + enrich.config.json + a
|
|
27
31
|
PLAYBOOK wired for this workspace (the CLI ships primitives,
|
|
@@ -31,16 +35,18 @@ Usage:
|
|
|
31
35
|
fullstackgtm report [source options] [audit options] [report options]
|
|
32
36
|
fullstackgtm diff --before <a.json> --after <b.json> [--json] [--fail-on-new-findings]
|
|
33
37
|
fullstackgtm merge --input <a.json> --input <b.json> [...] --out <merged.json> [--json]
|
|
34
|
-
fullstackgtm call parse --transcript <file> [--title t] [--source fathom|granola|...] [--model m] [--
|
|
38
|
+
fullstackgtm call parse --transcript <file> [--title t] [--source fathom|granola|...] [--model m] [--llm] [--heuristics] [--json|--ndjson] [--out <path>]
|
|
35
39
|
fullstackgtm call classify --transcript <file>|--call <parsed.json> [--llm] [--deterministic] [--json]
|
|
36
40
|
fullstackgtm call score --transcript <file>|--call <parsed.json> [--call-type <t>] [--rubric <rubric.json>] [--model m] [--json|--out <path>]
|
|
37
41
|
fullstackgtm call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
|
|
38
42
|
fullstackgtm call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
|
|
39
|
-
calls become evidence: LLM extraction by default
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
--
|
|
43
|
-
|
|
43
|
+
calls become evidence: chunked LLM extraction by default when a
|
|
44
|
+
key resolves (ANTHROPIC_API_KEY/OPENAI_API_KEY or \`login
|
|
45
|
+
anthropic|openai\`), free deterministic fallback when none does;
|
|
46
|
+
--heuristics (alias --deterministic) forces the deterministic
|
|
47
|
+
engine, --llm forces the LLM path. Model: --model, else env
|
|
48
|
+
FSGTM_INSIGHTS_MODEL. Then link the call to its deal and propose
|
|
49
|
+
governed next-step writes.
|
|
44
50
|
fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
|
|
45
51
|
the create gate: exit 0 = safe to create, exit 2 = match
|
|
46
52
|
found (exists/ambiguous) — call before ANY record creation
|
|
@@ -80,6 +86,15 @@ Usage:
|
|
|
80
86
|
fill-blanks-only patch plan through the normal dry-run →
|
|
81
87
|
approve → apply gate. refresh re-checks stale stamped fields
|
|
82
88
|
and proposes updates only where the source value changed.
|
|
89
|
+
fullstackgtm backfill stripe|runs [--since <iso>] [--pipeline <id|label>] [--match-property <name>] [--skip-unmatched] [source options] [--save] [--dry-run] [--json]
|
|
90
|
+
revenue truth backfill: one closed-won deal per PAID Stripe
|
|
91
|
+
invoice (amount = invoice total, close date = paid date,
|
|
92
|
+
associated to the matched customer account, deduped by
|
|
93
|
+
invoice id). Reads Stripe + the CRM snapshot, emits a
|
|
94
|
+
dry-run create_record plan through the normal approve →
|
|
95
|
+
apply gate; unmatched customers get a proposed NEW account
|
|
96
|
+
in the same plan (freemail domains never used;
|
|
97
|
+
--skip-unmatched = report-only). HubSpot-only for now.
|
|
83
98
|
fullstackgtm signals fetch [--bucket job,…] [--source greenhouse,lever,ashby] [--watchlist <path|crm:seg>] [--keywords …] [--from <file.json|spool.jsonl|spool-dir>] [--save]
|
|
84
99
|
fullstackgtm signals list [--since 7d] [--bucket b] [--account d] [--unjudged]
|
|
85
100
|
fullstackgtm signals outcome --account <d> [--touch <id>] --result replied|meeting|bounced|no_reply
|
|
@@ -188,13 +203,14 @@ Authentication (checked in order):
|
|
|
188
203
|
1. --token-env <name> explicit env var for this invocation (hubspot)
|
|
189
204
|
2. ambient env HUBSPOT_ACCESS_TOKEN, or SALESFORCE_ACCESS_TOKEN +
|
|
190
205
|
SALESFORCE_INSTANCE_URL (CI, agent sandboxes)
|
|
191
|
-
3. stored provider login
|
|
192
|
-
(hubspot: private
|
|
193
|
-
salesforce:
|
|
194
|
-
|
|
195
|
-
4. broker
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
3. stored provider login BYO direct credentials kept in ~/.fullstackgtm
|
|
207
|
+
(hubspot: --private-token or loopback OAuth;
|
|
208
|
+
salesforce: pasted token or device flow). These
|
|
209
|
+
direct credentials override broker-hosted tokens.
|
|
210
|
+
4. hosted/broker login fullstackgtm login hubspot|salesforce (or --hosted):
|
|
211
|
+
hosted browser OAuth stores a broker credential;
|
|
212
|
+
provider tokens are minted server-side. \`login --via
|
|
213
|
+
<url>\` pairs with an existing team deployment broker.
|
|
198
214
|
|
|
199
215
|
Source options (snapshot and audit):
|
|
200
216
|
--sample Built-in minimal mock CRM data (default)
|
|
@@ -280,12 +296,13 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
280
296
|
summary: "connect a provider or LLM key (secrets via stdin/env, never argv)",
|
|
281
297
|
phase: "Setup",
|
|
282
298
|
synopsis: [
|
|
299
|
+
"fullstackgtm login hubspot | salesforce hosted browser OAuth (default)",
|
|
283
300
|
"fullstackgtm login --via <hosted url> pair with a team deployment",
|
|
284
|
-
"fullstackgtm login
|
|
301
|
+
"fullstackgtm login stripe",
|
|
285
302
|
"fullstackgtm login anthropic | openai | apollo",
|
|
286
303
|
],
|
|
287
304
|
detail:
|
|
288
|
-
"
|
|
305
|
+
"HubSpot/Salesforce use hosted browser OAuth by default; BYO app/token paths are advanced. Secrets are NEVER passed as flags — pipe on stdin or enter at the prompt: `echo \"$TOKEN\" | fullstackgtm login hubspot --private-token`.",
|
|
289
306
|
seeAlso: ["doctor", "logout", "profiles"],
|
|
290
307
|
},
|
|
291
308
|
logout: {
|
|
@@ -442,6 +459,27 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
442
459
|
"Extra `--where` scoping is account-lifted for deals/contacts. `--except-deal-stage <stage>` excludes that stage and any record whose account has an open deal in it, re-verified per record at apply.",
|
|
443
460
|
seeAlso: ["bulk-update", "plans", "apply"],
|
|
444
461
|
},
|
|
462
|
+
backfill: {
|
|
463
|
+
summary: "Stripe paid invoices → closed-won deal proposals (governed)",
|
|
464
|
+
phase: "Remediate",
|
|
465
|
+
synopsis: [
|
|
466
|
+
"fullstackgtm backfill stripe|runs [--since <iso>] [--pipeline <id|label>] [--match-property <name>] [--skip-unmatched] [source options] [--save] [--dry-run] [--json]",
|
|
467
|
+
],
|
|
468
|
+
detail:
|
|
469
|
+
"`backfill runs` replays LOCAL execution history to the paired hosted app (idempotent: deterministic run ids + server-side dedupe): every stored plan's apply runs and the health.jsonl timeline, so the hosted feed and Home health trend cover the engagement from its CLI-only beginning. Requires `login --via`; only what live reporting sends leaves the machine (statuses, counts, timestamps, scores — never CRM field values). `backfill stripe`: billing is the revenue source of truth — proposes ONE closed-won deal per paid Stripe invoice (amount = invoice total, close date = paid date, associated to the matched customer account, deduped by invoice id). Customers are matched to CRM accounts by domain, then exact name; a customer the CRM doesn't know gets an explicit proposed ACCOUNT create in the same plan (billing-email domain used unless it is freemail; a customer with no usable name/domain stays report-only; `--skip-unmatched` restores report-only for all unmatched). Emits a dry-run create_record plan — nothing is written until `plans approve` → `apply`, where the connector re-resolves each invoice id (and each account by domain/name) and creates only on a confirmed miss. Deal creation is HubSpot-only for now.",
|
|
470
|
+
options: [
|
|
471
|
+
["--since <iso>", "only invoices created on/after this timestamp"],
|
|
472
|
+
["--pipeline <id|label>", "target deal pipeline (default: the portal's default pipeline)"],
|
|
473
|
+
["--match-property <name>", "deal dedupe property stamped with the invoice id (default stripe_invoice_id)"],
|
|
474
|
+
["--skip-unmatched", "do not propose accounts for unmatched customers (report-only, the pre-1.3 behavior)"],
|
|
475
|
+
["--provider <name>", "CRM snapshot to match against: hubspot | salesforce"],
|
|
476
|
+
["--input <path>", "match against a saved snapshot JSON instead of a live pull"],
|
|
477
|
+
["--save", "persist the dry-run plan for approve → apply"],
|
|
478
|
+
["--json", "machine-readable {plan, counts, unmatched, proposedAccounts} (stripe) or replay summary (runs)"],
|
|
479
|
+
["--dry-run", "(runs) count what would be replayed without sending anything"],
|
|
480
|
+
],
|
|
481
|
+
seeAlso: ["enrich", "plans", "apply", "resolve"],
|
|
482
|
+
},
|
|
445
483
|
enrich: {
|
|
446
484
|
summary: "governed third-party enrichment (Apollo/Clay), fill-blanks-only",
|
|
447
485
|
phase: "Remediate",
|
|
@@ -457,7 +495,7 @@ export const HELP: Record<string, HelpEntry> = {
|
|
|
457
495
|
phase: "Remediate",
|
|
458
496
|
synopsis: ["fullstackgtm call parse|classify|score|link|plan … (run `call --help` for full options)"],
|
|
459
497
|
detail:
|
|
460
|
-
"`parse` normalizes any transcript into canonical segments + evidence
|
|
498
|
+
"`parse` normalizes any transcript into canonical segments + evidence — chunked LLM extraction by default when a key resolves (ANTHROPIC_API_KEY/OPENAI_API_KEY or `login anthropic|openai`), free deterministic fallback when none does; `--heuristics` forces the deterministic engine, `--llm` forces the LLM path, model via `--model` or env FSGTM_INSIGHTS_MODEL. `classify` picks the call type, `score` rates it against the type's rubric, `link` finds the deal, `plan` proposes governed next-step writes.",
|
|
461
499
|
seeAlso: ["plans", "apply"],
|
|
462
500
|
},
|
|
463
501
|
|
|
@@ -585,7 +623,7 @@ export function shortUsage() {
|
|
|
585
623
|
["Setup & health", ["login", "logout", "doctor", "capabilities", "robot-docs", "profiles", "health"]],
|
|
586
624
|
["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules"]],
|
|
587
625
|
["Prevent — gate writes", ["resolve"]],
|
|
588
|
-
["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich"]],
|
|
626
|
+
["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich", "backfill"]],
|
|
589
627
|
["Calls → evidence", ["call"]],
|
|
590
628
|
["Govern — the plan/apply spine", ["suggest", "plans", "apply", "audit-log", "merge"]],
|
|
591
629
|
["Market intelligence", ["market", "tam"]],
|
package/src/cli/plans.ts
CHANGED
|
@@ -14,11 +14,12 @@ import { buildAuditLog, verifyAuditLog } from "../auditLog.ts";
|
|
|
14
14
|
import { createFilePlanStore } from "../planStore.ts";
|
|
15
15
|
import { ENRICH_CONFIG_FILE_NAME, loadEnrichConfig, type EnrichConfig } from "../enrich.ts";
|
|
16
16
|
import { loadMeter, recordConsumption, remaining } from "../acquireMeter.ts";
|
|
17
|
-
import { reportCounts } from "../runReport.ts";
|
|
17
|
+
import { progressReporter, reportCounts } from "../runReport.ts";
|
|
18
|
+
import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.ts";
|
|
18
19
|
import type { ValueSuggestion } from "../suggest.ts";
|
|
19
20
|
import type { CanonicalGtmSnapshot, CreateRecordPayload, PatchOperation, PatchPlan, PatchPlanRun } from "../types.ts";
|
|
20
21
|
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.ts";
|
|
21
|
-
import { colorEnabled,
|
|
22
|
+
import { colorEnabled, createProgressRenderer, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.ts";
|
|
22
23
|
import { unknownSubcommandError } from "./suggest.ts";
|
|
23
24
|
|
|
24
25
|
|
|
@@ -233,23 +234,23 @@ export async function apply(args: string[]) {
|
|
|
233
234
|
// A channel (e.g. outbox) renders approved ops to a local artifact and
|
|
234
235
|
// transmits nothing; a CRM provider writes records. Same governed apply path.
|
|
235
236
|
const connector = channel ? createChannelConnector(channel) : await connectorFor(provider!, args);
|
|
236
|
-
// Interactive terminals get a live
|
|
237
|
-
// executes
|
|
238
|
-
|
|
237
|
+
// Interactive terminals get a live apply board on stderr while the run
|
|
238
|
+
// executes (preflight → operations → results, with a per-op safety ticker);
|
|
239
|
+
// piped runs render nothing. Either way the emitter streams heartbeats to
|
|
240
|
+
// the paired hosted app when a long run is in flight.
|
|
241
|
+
const renderer = createProgressRenderer(APPLY_STAGES);
|
|
242
|
+
const progress = createProgressEmitter(
|
|
243
|
+
composeListeners(renderer.listener, progressReporter()),
|
|
244
|
+
);
|
|
239
245
|
let run: PatchPlanRun;
|
|
240
246
|
try {
|
|
241
247
|
run = await applyPatchPlan(connector, plan, {
|
|
242
248
|
approvedOperationIds,
|
|
243
249
|
valueOverrides,
|
|
244
|
-
|
|
245
|
-
? (progress) =>
|
|
246
|
-
ticker.set(
|
|
247
|
-
`Applying ${progress.completed}/${progress.total} · ✓ ${progress.applied} applied · ${progress.failed} failed · ${progress.conflicts} conflict${progress.conflicts === 1 ? "" : "s"} · ${progress.skipped} skipped`,
|
|
248
|
-
)
|
|
249
|
-
: undefined,
|
|
250
|
+
progress,
|
|
250
251
|
});
|
|
251
252
|
} finally {
|
|
252
|
-
|
|
253
|
+
renderer.done();
|
|
253
254
|
}
|
|
254
255
|
if (planId && store) {
|
|
255
256
|
await store.recordRun(planId, run);
|
|
@@ -272,9 +273,9 @@ export async function apply(args: string[]) {
|
|
|
272
273
|
for (const result of run.results) applyTally[result.status] = (applyTally[result.status] ?? 0) + 1;
|
|
273
274
|
reportCounts(applyTally);
|
|
274
275
|
|
|
275
|
-
// Rich-only closing line on stderr (
|
|
276
|
+
// Rich-only closing line on stderr (renderer.active implies an interactive,
|
|
276
277
|
// color-safe stderr): the one-glance outcome of the run.
|
|
277
|
-
if (
|
|
278
|
+
if (renderer.active) {
|
|
278
279
|
const pe = paint(true);
|
|
279
280
|
const badge =
|
|
280
281
|
run.status === "applied"
|
package/src/cli/shared.ts
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { createHubspotConnector } from "../connectors/hubspot.ts";
|
|
6
|
-
import { createSalesforceConnector } from "../connectors/salesforce.ts";
|
|
7
|
-
import { createStripeConnector } from "../connectors/stripe.ts";
|
|
8
5
|
import { credentialsPath, getCredential, resolveHubspotConnection, resolveSalesforceConnection, storeCredential } from "../credentials.ts";
|
|
9
|
-
import { generateDemoSnapshot } from "../demo.ts";
|
|
10
6
|
import { builtinAuditRules } from "../rules.ts";
|
|
11
|
-
import { sampleSnapshot } from "../sampleData.ts";
|
|
12
7
|
import { detectProviderFromKey, resolveLlmCredential, validateLlmKey, type LlmProvider } from "../llm.ts";
|
|
13
8
|
import { parseIcp, type Icp } from "../icp.ts";
|
|
14
9
|
import { nearest } from "./suggest.ts";
|
|
15
10
|
import type { FieldMappings } from "../mappings.ts";
|
|
16
|
-
import type { CanonicalGtmSnapshot, GtmConnector
|
|
17
|
-
import {
|
|
11
|
+
import type { CanonicalGtmSnapshot, GtmConnector } from "../types.ts";
|
|
12
|
+
import {
|
|
13
|
+
composeListeners,
|
|
14
|
+
createProgressEmitter,
|
|
15
|
+
SNAPSHOT_PULL_STAGES,
|
|
16
|
+
STRIPE_SNAPSHOT_STAGES,
|
|
17
|
+
type ProgressEmitter,
|
|
18
|
+
} from "../progress.ts";
|
|
19
|
+
import { progressReporter } from "../runReport.ts";
|
|
20
|
+
import { createProgressRenderer } from "./ui.ts";
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
export function option(args: string[], name: string) {
|
|
@@ -112,27 +115,32 @@ async function salesforceConnection() {
|
|
|
112
115
|
export async function connectorFor(
|
|
113
116
|
provider: string,
|
|
114
117
|
args: string[],
|
|
115
|
-
|
|
118
|
+
progress?: ProgressEmitter,
|
|
116
119
|
): Promise<GtmConnector> {
|
|
120
|
+
// Connector modules are the two largest files in dist (~100KB together) and
|
|
121
|
+
// load lazily here: only live `--provider` runs pay for them, not --demo/
|
|
122
|
+
// --sample/--input runs or the always-eager help/version paths.
|
|
117
123
|
if (provider === "hubspot") {
|
|
118
124
|
const connection = await hubspotConnection(args);
|
|
125
|
+
const { createHubspotConnector } = await import("../connectors/hubspot.ts");
|
|
119
126
|
return createHubspotConnector({
|
|
120
127
|
getAccessToken: () => connection.accessToken,
|
|
121
128
|
fieldMappings: (connection.fieldMappings as FieldMappings | undefined) ?? undefined,
|
|
122
129
|
// Point at a mock/proxy HubSpot (tests, evals, request-recording).
|
|
123
130
|
apiBaseUrl: process.env.HUBSPOT_API_BASE_URL,
|
|
124
|
-
|
|
131
|
+
progress,
|
|
125
132
|
});
|
|
126
133
|
}
|
|
127
134
|
if (provider === "salesforce") {
|
|
128
135
|
const connection = await salesforceConnection();
|
|
136
|
+
const { createSalesforceConnector } = await import("../connectors/salesforce.ts");
|
|
129
137
|
return createSalesforceConnector({
|
|
130
138
|
getConnection: () => connection,
|
|
131
139
|
fieldMappings:
|
|
132
140
|
((connection as { fieldMappings?: unknown }).fieldMappings as
|
|
133
141
|
| FieldMappings
|
|
134
142
|
| undefined) ?? undefined,
|
|
135
|
-
|
|
143
|
+
progress,
|
|
136
144
|
});
|
|
137
145
|
}
|
|
138
146
|
if (provider === "stripe") {
|
|
@@ -143,7 +151,8 @@ export async function connectorFor(
|
|
|
143
151
|
"No Stripe credentials. Run `echo \"$STRIPE_KEY\" | fullstackgtm login stripe` or set STRIPE_SECRET_KEY. A restricted key with read access to Customers and Subscriptions is enough. (`fullstackgtm doctor` shows credential status.)",
|
|
144
152
|
);
|
|
145
153
|
}
|
|
146
|
-
|
|
154
|
+
const { createStripeConnector } = await import("../connectors/stripe.ts");
|
|
155
|
+
return createStripeConnector({ getApiKey: () => key, progress });
|
|
147
156
|
}
|
|
148
157
|
const providerSuggestion = nearest(provider.toLowerCase(), ["hubspot", "salesforce", "stripe"], 3);
|
|
149
158
|
throw new Error(
|
|
@@ -152,37 +161,45 @@ export async function connectorFor(
|
|
|
152
161
|
);
|
|
153
162
|
}
|
|
154
163
|
|
|
155
|
-
export async function readSnapshot(
|
|
164
|
+
export async function readSnapshot(
|
|
165
|
+
args: string[],
|
|
166
|
+
progress?: ProgressEmitter,
|
|
167
|
+
): Promise<CanonicalGtmSnapshot> {
|
|
156
168
|
const provider = option(args, "--provider");
|
|
157
169
|
if (provider) {
|
|
170
|
+
// A verb driving its own progress board (e.g. `backfill stripe`) passes
|
|
171
|
+
// its emitter through; the pull's stage/items events flow into it and no
|
|
172
|
+
// second board is painted.
|
|
173
|
+
if (progress) {
|
|
174
|
+
const connector = await connectorFor(provider, args, progress);
|
|
175
|
+
return connector.fetchSnapshot();
|
|
176
|
+
}
|
|
158
177
|
// Live pulls can page for minutes with nothing on screen. On an
|
|
159
|
-
// interactive terminal, a stderr
|
|
160
|
-
// piped/CI/agent runs
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
provider,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
status.set(
|
|
169
|
-
`Pulling ${progress.objectType}s from ${provider}… ${formatCount(progress.fetched)} fetched · ${formatDuration(Date.now() - startedAt)}`,
|
|
170
|
-
)
|
|
171
|
-
: undefined,
|
|
178
|
+
// interactive terminal, a stderr checklist ticks through the pull stages
|
|
179
|
+
// with running tallies; piped/CI/agent runs render nothing (zero bytes
|
|
180
|
+
// written). Either way the emitter feeds the broker heartbeat when the
|
|
181
|
+
// CLI is paired, so long pulls tick live on the hosted dashboard too.
|
|
182
|
+
const renderer = createProgressRenderer(
|
|
183
|
+
provider === "stripe" ? STRIPE_SNAPSHOT_STAGES : SNAPSHOT_PULL_STAGES,
|
|
184
|
+
);
|
|
185
|
+
const emitter = createProgressEmitter(
|
|
186
|
+
composeListeners(renderer.listener, progressReporter()),
|
|
172
187
|
);
|
|
188
|
+
const connector = await connectorFor(provider, args, emitter);
|
|
173
189
|
try {
|
|
174
190
|
return await connector.fetchSnapshot();
|
|
175
191
|
} finally {
|
|
176
|
-
|
|
192
|
+
renderer.done();
|
|
177
193
|
}
|
|
178
194
|
}
|
|
179
195
|
if (args.includes("--demo")) {
|
|
196
|
+
const { generateDemoSnapshot } = await import("../demo.ts");
|
|
180
197
|
return generateDemoSnapshot({
|
|
181
198
|
seed: numericOption(args, "--seed"),
|
|
182
199
|
today: option(args, "--today") ?? undefined,
|
|
183
200
|
});
|
|
184
201
|
}
|
|
185
|
-
if (args.includes("--sample")) return sampleSnapshot;
|
|
202
|
+
if (args.includes("--sample")) return (await import("../sampleData.ts")).sampleSnapshot;
|
|
186
203
|
const input = option(args, "--input");
|
|
187
204
|
if (!input) {
|
|
188
205
|
throw new Error(
|
package/src/cli/ui.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// - NO_COLOR (https://no-color.org), TERM=dumb, and CI disable styling;
|
|
12
12
|
// FORCE_COLOR (non-"0") re-enables it, matching the wider ecosystem.
|
|
13
13
|
|
|
14
|
+
import type { ProgressEvent, ProgressListener, ProgressSnapshot } from "../progress.ts";
|
|
15
|
+
|
|
14
16
|
type StreamLike = {
|
|
15
17
|
isTTY?: boolean;
|
|
16
18
|
columns?: number;
|
|
@@ -358,6 +360,76 @@ export function createChecklist(
|
|
|
358
360
|
};
|
|
359
361
|
}
|
|
360
362
|
|
|
363
|
+
export type ProgressRenderer = {
|
|
364
|
+
/**
|
|
365
|
+
* Feed to `createProgressEmitter` (compose with the broker streaming
|
|
366
|
+
* listener via `composeListeners` when the run should also heartbeat).
|
|
367
|
+
*/
|
|
368
|
+
listener: ProgressListener;
|
|
369
|
+
/** Stop animating and erase the board (callers print their own summary). */
|
|
370
|
+
done(): void;
|
|
371
|
+
readonly active: boolean;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const NOOP_RENDERER: ProgressRenderer = { listener: () => {}, done() {}, active: false };
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Adapter from the shared progress vocabulary (src/progress.ts) to the
|
|
378
|
+
* existing TTY primitives: `stage` events advance a live checklist (○ → ⠹ →
|
|
379
|
+
* ✓), while `items` / `note` / `opResult` / `meter` events annotate the
|
|
380
|
+
* running stage ("contacts 1,200/4,800", the apply ticker tallies, a fuel
|
|
381
|
+
* gauge). Outside an interactive TTY this is an inert no-op — piped/CI/agent
|
|
382
|
+
* output stays byte-identical (streaming listeners composed alongside still
|
|
383
|
+
* run; rendering is presentation only).
|
|
384
|
+
*/
|
|
385
|
+
export function createProgressRenderer(
|
|
386
|
+
stages: readonly string[],
|
|
387
|
+
stream: StreamLike = process.stderr,
|
|
388
|
+
env: UiEnv = process.env,
|
|
389
|
+
): ProgressRenderer {
|
|
390
|
+
const board = createChecklist(
|
|
391
|
+
stages.map((stage) => ({ id: stage, label: stage })),
|
|
392
|
+
stream,
|
|
393
|
+
env,
|
|
394
|
+
);
|
|
395
|
+
if (!board.active) return NOOP_RENDERER;
|
|
396
|
+
let current: string | null = null;
|
|
397
|
+
const noteFor = (event: ProgressEvent, snapshot: ProgressSnapshot): string | undefined => {
|
|
398
|
+
if (event.kind === "items") {
|
|
399
|
+
return event.total !== undefined
|
|
400
|
+
? `${formatCount(event.done)}/${formatCount(event.total)}`
|
|
401
|
+
: formatCount(event.done);
|
|
402
|
+
}
|
|
403
|
+
if (event.kind === "note") return event.note;
|
|
404
|
+
if (event.kind === "opResult") {
|
|
405
|
+
return `${GLYPH.ok} ${formatCount(snapshot.opsApplied)} applied · ${formatCount(snapshot.opsFailed)} failed · ${formatCount(snapshot.opsSkipped)} skipped`;
|
|
406
|
+
}
|
|
407
|
+
if (event.kind === "meter") {
|
|
408
|
+
const fraction = event.budget > 0 ? event.spent / event.budget : 0;
|
|
409
|
+
return `${formatBar(fraction, 10)} ${formatCount(event.spent)}/${formatCount(event.budget)} ${event.unit}`;
|
|
410
|
+
}
|
|
411
|
+
return undefined;
|
|
412
|
+
};
|
|
413
|
+
return {
|
|
414
|
+
listener(event, snapshot) {
|
|
415
|
+
if (event.kind === "stage") {
|
|
416
|
+
// Advance the board: close out the previous stage, spin the new one.
|
|
417
|
+
if (current && current !== event.stage) board.update(current, "ok");
|
|
418
|
+
current = event.stage;
|
|
419
|
+
board.update(current, "running");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// Events for a stage the board doesn't know (e.g. a nested pull inside a
|
|
423
|
+
// larger flow) are ignored by createChecklist's unknown-id no-op.
|
|
424
|
+
if (current) board.update(current, "running", noteFor(event, snapshot));
|
|
425
|
+
},
|
|
426
|
+
done() {
|
|
427
|
+
board.done();
|
|
428
|
+
},
|
|
429
|
+
active: true,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
361
433
|
/** Truncate to `max` display characters, ending in "…" when cut. */
|
|
362
434
|
export function truncateToWidth(text: string, max: number): string {
|
|
363
435
|
if (max <= 0) return "";
|
|
@@ -421,6 +493,5 @@ export function stylizePlanMarkdown(text: string, p: Paint): string {
|
|
|
421
493
|
|
|
422
494
|
/** Strip ANSI escape sequences (test helper + safety net for width math). */
|
|
423
495
|
export function stripAnsi(text: string): string {
|
|
424
|
-
// eslint-disable-next-line no-control-regex
|
|
425
496
|
return text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, "");
|
|
426
497
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { activeProfile, listProfiles, setActiveProfile } from "./credentials.ts";
|
|
2
|
-
import { audit, healthCommand, reportCommand, rulesCommand, snapshotCommand } from "./cli/audit.ts";
|
|
3
|
-
import { doctorCommand, login, logout } from "./cli/auth.ts";
|
|
4
2
|
import { capabilitiesCommand, printCommandHelpJson, robotDocsCommand, unknownCommandEnvelope } from "./cli/capabilities.ts";
|
|
5
|
-
import { callCommand } from "./cli/call.ts";
|
|
6
|
-
import { draftCommand } from "./cli/draft.ts";
|
|
7
|
-
import { enrichCommand } from "./cli/enrich.ts";
|
|
8
|
-
import { bulkUpdateCommand, dedupeCommand, fixCommand, reassignCommand, resolveCommand, suggest } from "./cli/fix.ts";
|
|
9
3
|
import { BESPOKE_HELP, commandHelp, HELP, shortUsage, stylizeShortUsage, usage } from "./cli/help.ts";
|
|
10
|
-
import { icpCommand } from "./cli/icp.ts";
|
|
11
|
-
import { initCommand } from "./cli/init.ts";
|
|
12
|
-
import { marketCommand } from "./cli/market.ts";
|
|
13
|
-
import { apply, auditLogCommand, diffCommand, mergeCommand, plansCommand } from "./cli/plans.ts";
|
|
14
|
-
import { scheduleCommand } from "./cli/schedule.ts";
|
|
15
4
|
import { readPackageInfo } from "./cli/shared.ts";
|
|
16
5
|
import { correctedCommand, detectFlagTypo, suggestCommand, unknownFlagEnvelope } from "./cli/suggest.ts";
|
|
17
6
|
import { colorEnabled, paint } from "./cli/ui.ts";
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
|
|
8
|
+
// Verb modules load lazily inside their dispatch branch below. The dispatcher
|
|
9
|
+
// used to import all of them eagerly, so `--version` compiled the full
|
|
10
|
+
// 78-module graph (audit, market, enrich, signals, schedule, …) before
|
|
11
|
+
// printing one line. Each branch now `await import()`s only its own module;
|
|
12
|
+
// help/typo/error paths and the exports above stay eager and byte-identical.
|
|
20
13
|
|
|
21
14
|
|
|
22
15
|
/**
|
|
@@ -129,95 +122,99 @@ export async function runCli(argv: string[]) {
|
|
|
129
122
|
return;
|
|
130
123
|
}
|
|
131
124
|
if (command === "login") {
|
|
132
|
-
await login(args);
|
|
125
|
+
await (await import("./cli/auth.ts")).login(args);
|
|
133
126
|
return;
|
|
134
127
|
}
|
|
135
128
|
if (command === "logout") {
|
|
136
|
-
logout(args);
|
|
129
|
+
(await import("./cli/auth.ts")).logout(args);
|
|
137
130
|
return;
|
|
138
131
|
}
|
|
139
132
|
if (command === "snapshot") {
|
|
140
|
-
await snapshotCommand(args);
|
|
133
|
+
await (await import("./cli/audit.ts")).snapshotCommand(args);
|
|
141
134
|
return;
|
|
142
135
|
}
|
|
143
136
|
if (command === "audit") {
|
|
144
|
-
await audit(args);
|
|
137
|
+
await (await import("./cli/audit.ts")).audit(args);
|
|
145
138
|
return;
|
|
146
139
|
}
|
|
147
140
|
if (command === "report") {
|
|
148
|
-
await reportCommand(args);
|
|
141
|
+
await (await import("./cli/audit.ts")).reportCommand(args);
|
|
149
142
|
return;
|
|
150
143
|
}
|
|
151
144
|
if (command === "rules") {
|
|
152
|
-
await rulesCommand(args);
|
|
145
|
+
await (await import("./cli/audit.ts")).rulesCommand(args);
|
|
153
146
|
return;
|
|
154
147
|
}
|
|
155
148
|
if (command === "doctor") {
|
|
156
|
-
await doctorCommand(args);
|
|
149
|
+
await (await import("./cli/auth.ts")).doctorCommand(args);
|
|
157
150
|
return;
|
|
158
151
|
}
|
|
159
152
|
if (command === "health") {
|
|
160
|
-
healthCommand(args);
|
|
153
|
+
(await import("./cli/audit.ts")).healthCommand(args);
|
|
161
154
|
return;
|
|
162
155
|
}
|
|
163
156
|
if (command === "suggest") {
|
|
164
|
-
await suggest(args);
|
|
157
|
+
await (await import("./cli/fix.ts")).suggest(args);
|
|
165
158
|
return;
|
|
166
159
|
}
|
|
167
160
|
if (command === "call") {
|
|
168
|
-
await callCommand(args);
|
|
161
|
+
await (await import("./cli/call.ts")).callCommand(args);
|
|
169
162
|
return;
|
|
170
163
|
}
|
|
171
164
|
if (command === "resolve") {
|
|
172
|
-
await resolveCommand(args);
|
|
165
|
+
await (await import("./cli/fix.ts")).resolveCommand(args);
|
|
173
166
|
return;
|
|
174
167
|
}
|
|
175
168
|
if (command === "bulk-update") {
|
|
176
|
-
await bulkUpdateCommand(args);
|
|
169
|
+
await (await import("./cli/fix.ts")).bulkUpdateCommand(args);
|
|
177
170
|
return;
|
|
178
171
|
}
|
|
179
172
|
if (command === "dedupe") {
|
|
180
|
-
await dedupeCommand(args);
|
|
173
|
+
await (await import("./cli/fix.ts")).dedupeCommand(args);
|
|
181
174
|
return;
|
|
182
175
|
}
|
|
183
176
|
if (command === "reassign") {
|
|
184
|
-
await reassignCommand(args);
|
|
177
|
+
await (await import("./cli/fix.ts")).reassignCommand(args);
|
|
185
178
|
return;
|
|
186
179
|
}
|
|
187
180
|
if (command === "fix") {
|
|
188
|
-
await fixCommand(args);
|
|
181
|
+
await (await import("./cli/fix.ts")).fixCommand(args);
|
|
189
182
|
return;
|
|
190
183
|
}
|
|
191
184
|
if (command === "market") {
|
|
192
|
-
await marketCommand(args);
|
|
185
|
+
await (await import("./cli/market.ts")).marketCommand(args);
|
|
193
186
|
return;
|
|
194
187
|
}
|
|
195
188
|
if (command === "enrich") {
|
|
196
|
-
await enrichCommand(args);
|
|
189
|
+
await (await import("./cli/enrich.ts")).enrichCommand(args);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (command === "backfill") {
|
|
193
|
+
await (await import("./cli/backfill.ts")).backfillCommand(args);
|
|
197
194
|
return;
|
|
198
195
|
}
|
|
199
196
|
if (command === "init") {
|
|
200
|
-
initCommand(args);
|
|
197
|
+
(await import("./cli/init.ts")).initCommand(args);
|
|
201
198
|
return;
|
|
202
199
|
}
|
|
203
200
|
if (command === "tam") {
|
|
204
|
-
await tamCommand(args);
|
|
201
|
+
await (await import("./cli/tam.ts")).tamCommand(args);
|
|
205
202
|
return;
|
|
206
203
|
}
|
|
207
204
|
if (command === "icp") {
|
|
208
|
-
await icpCommand(args);
|
|
205
|
+
await (await import("./cli/icp.ts")).icpCommand(args);
|
|
209
206
|
return;
|
|
210
207
|
}
|
|
211
208
|
if (command === "signals") {
|
|
212
|
-
await signalsCommand(args);
|
|
209
|
+
await (await import("./cli/signals.ts")).signalsCommand(args);
|
|
213
210
|
return;
|
|
214
211
|
}
|
|
215
212
|
if (command === "draft") {
|
|
216
|
-
await draftCommand(args);
|
|
213
|
+
await (await import("./cli/draft.ts")).draftCommand(args);
|
|
217
214
|
return;
|
|
218
215
|
}
|
|
219
216
|
if (command === "schedule") {
|
|
220
|
-
await scheduleCommand(args);
|
|
217
|
+
await (await import("./cli/schedule.ts")).scheduleCommand(args);
|
|
221
218
|
return;
|
|
222
219
|
}
|
|
223
220
|
if (command === "profiles") {
|
|
@@ -225,23 +222,23 @@ export async function runCli(argv: string[]) {
|
|
|
225
222
|
return;
|
|
226
223
|
}
|
|
227
224
|
if (command === "diff") {
|
|
228
|
-
await diffCommand(args);
|
|
225
|
+
await (await import("./cli/plans.ts")).diffCommand(args);
|
|
229
226
|
return;
|
|
230
227
|
}
|
|
231
228
|
if (command === "merge") {
|
|
232
|
-
await mergeCommand(args);
|
|
229
|
+
await (await import("./cli/plans.ts")).mergeCommand(args);
|
|
233
230
|
return;
|
|
234
231
|
}
|
|
235
232
|
if (command === "plans") {
|
|
236
|
-
await plansCommand(args);
|
|
233
|
+
await (await import("./cli/plans.ts")).plansCommand(args);
|
|
237
234
|
return;
|
|
238
235
|
}
|
|
239
236
|
if (command === "audit-log") {
|
|
240
|
-
await auditLogCommand(args);
|
|
237
|
+
await (await import("./cli/plans.ts")).auditLogCommand(args);
|
|
241
238
|
return;
|
|
242
239
|
}
|
|
243
240
|
if (command === "apply") {
|
|
244
|
-
await apply(args);
|
|
241
|
+
await (await import("./cli/plans.ts")).apply(args);
|
|
245
242
|
return;
|
|
246
243
|
}
|
|
247
244
|
|