fullstackgtm 0.43.0 → 0.45.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 +378 -1
- package/README.md +37 -14
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +29 -2
- package/dist/cli/audit.d.ts +15 -0
- package/dist/cli/audit.js +392 -0
- package/dist/cli/auth.d.ts +80 -0
- package/dist/cli/auth.js +500 -0
- package/dist/cli/call.d.ts +1 -0
- package/dist/cli/call.js +373 -0
- package/dist/cli/capabilities.d.ts +30 -0
- package/dist/cli/capabilities.js +197 -0
- package/dist/cli/draft.d.ts +7 -0
- package/dist/cli/draft.js +87 -0
- package/dist/cli/enrich.d.ts +8 -0
- package/dist/cli/enrich.js +788 -0
- package/dist/cli/fix.d.ts +33 -0
- package/dist/cli/fix.js +344 -0
- package/dist/cli/help.d.ts +25 -0
- package/dist/cli/help.js +609 -0
- package/dist/cli/icp.d.ts +7 -0
- package/dist/cli/icp.js +229 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +58 -0
- package/dist/cli/market.d.ts +1 -0
- package/dist/cli/market.js +391 -0
- package/dist/cli/plans.d.ts +5 -0
- package/dist/cli/plans.js +454 -0
- package/dist/cli/schedule.d.ts +8 -0
- package/dist/cli/schedule.js +479 -0
- package/dist/cli/shared.d.ts +70 -0
- package/dist/cli/shared.js +331 -0
- package/dist/cli/signals.d.ts +7 -0
- package/dist/cli/signals.js +412 -0
- package/dist/cli/suggest.d.ts +49 -0
- package/dist/cli/suggest.js +135 -0
- package/dist/cli/tam.d.ts +9 -0
- package/dist/cli/tam.js +387 -0
- package/dist/cli/ui.d.ts +121 -0
- package/dist/cli/ui.js +375 -0
- package/dist/cli.d.ts +1 -57
- package/dist/cli.js +104 -4556
- package/dist/connector.d.ts +15 -0
- package/dist/connector.js +35 -0
- package/dist/connectors/hubspot.d.ts +3 -1
- package/dist/connectors/hubspot.js +10 -3
- package/dist/connectors/outboxChannel.d.ts +64 -0
- package/dist/connectors/outboxChannel.js +170 -0
- package/dist/connectors/prospectSources.d.ts +22 -0
- package/dist/connectors/prospectSources.js +43 -0
- package/dist/connectors/salesforce.d.ts +3 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/signalSources.d.ts +105 -0
- package/dist/connectors/signalSources.js +264 -0
- package/dist/connectors/theirstack.d.ts +84 -0
- package/dist/connectors/theirstack.js +125 -0
- package/dist/icp.d.ts +52 -4
- package/dist/icp.js +112 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- package/dist/judgeEval.d.ts +7 -0
- package/dist/judgeEval.js +8 -1
- package/dist/marketClassify.d.ts +2 -0
- package/dist/marketClassify.js +7 -1
- package/dist/mcp-bin.js +2 -2
- package/dist/mcp.js +259 -166
- package/dist/runReport.d.ts +26 -0
- package/dist/runReport.js +15 -0
- package/dist/schedule.d.ts +80 -2
- package/dist/schedule.js +272 -5
- package/dist/signals.d.ts +54 -0
- package/dist/signals.js +64 -0
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/tam.d.ts +225 -0
- package/dist/tam.js +470 -0
- package/dist/types.d.ts +11 -0
- package/docs/api.md +91 -11
- package/docs/outbox-format.md +92 -0
- package/docs/recipes.md +37 -0
- package/docs/roadmap-to-1.0.md +31 -1
- package/docs/signal-spool-format.md +175 -0
- package/docs/tam.md +195 -0
- package/llms.txt +83 -11
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +4 -3
- package/src/audit.ts +27 -1
- package/src/cli/audit.ts +447 -0
- package/src/cli/auth.ts +549 -0
- package/src/cli/call.ts +398 -0
- package/src/cli/capabilities.ts +215 -0
- package/src/cli/draft.ts +101 -0
- package/src/cli/enrich.ts +885 -0
- package/src/cli/fix.ts +372 -0
- package/src/cli/help.ts +664 -0
- package/src/cli/icp.ts +265 -0
- package/src/cli/init.ts +65 -0
- package/src/cli/market.ts +423 -0
- package/src/cli/plans.ts +523 -0
- package/src/cli/schedule.ts +526 -0
- package/src/cli/shared.ts +375 -0
- package/src/cli/signals.ts +434 -0
- package/src/cli/suggest.ts +151 -0
- package/src/cli/tam.ts +435 -0
- package/src/cli/ui.ts +426 -0
- package/src/cli.ts +103 -5170
- package/src/connector.ts +46 -0
- package/src/connectors/hubspot.ts +14 -2
- package/src/connectors/outboxChannel.ts +202 -0
- package/src/connectors/prospectSources.ts +57 -0
- package/src/connectors/salesforce.ts +18 -2
- package/src/connectors/signalSources.ts +314 -0
- package/src/connectors/theirstack.ts +170 -0
- package/src/icp.ts +120 -34
- package/src/index.ts +32 -0
- package/src/init.ts +3 -0
- package/src/judge.ts +7 -0
- package/src/judgeEval.ts +8 -1
- package/src/marketClassify.ts +8 -1
- package/src/mcp-bin.ts +2 -2
- package/src/mcp.ts +130 -57
- package/src/runReport.ts +39 -0
- package/src/schedule.ts +330 -7
- package/src/signals.ts +90 -0
- package/src/spoolFiles.ts +116 -0
- package/src/tam.ts +654 -0
- package/src/types.ts +12 -0
package/src/cli/fix.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { auditSnapshot, defaultPolicy } from "../audit.ts";
|
|
6
|
+
import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.ts";
|
|
7
|
+
import { applyPatchPlan } from "../connector.ts";
|
|
8
|
+
import { patchPlanToMarkdown } from "../format.ts";
|
|
9
|
+
import { createFilePlanStore } from "../planStore.ts";
|
|
10
|
+
import { resolveRecord, type ResolveCandidate } from "../resolve.ts";
|
|
11
|
+
import { buildBulkUpdatePlan } from "../bulkUpdate.ts";
|
|
12
|
+
import { buildDedupePlan, type DedupeOptions } from "../dedupe.ts";
|
|
13
|
+
import { buildReassignPlans, type ReassignObjectType } from "../reassign.ts";
|
|
14
|
+
import { suggestValues } from "../suggest.ts";
|
|
15
|
+
import type { PatchPlan, PatchPlanRun } from "../types.ts";
|
|
16
|
+
import { confirmRequested, connectorFor, isOptionValue, numericOption, option, readSnapshot, repeatedOption, saveRequested } from "./shared.ts";
|
|
17
|
+
import { box, colorEnabled, createStatusLine, paint } from "./ui.ts";
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The resolve gate: exit 0 = safe to create, exit 2 = match found (exists or
|
|
22
|
+
* ambiguous — do NOT blind-create), exit 1 = error. Built for sync jobs and
|
|
23
|
+
* webhook handlers to call before any record creation.
|
|
24
|
+
*/
|
|
25
|
+
export async function resolveCommand(args: string[]) {
|
|
26
|
+
const [objectType, ...rest] = args;
|
|
27
|
+
if (!objectType || !["account", "contact", "deal"].includes(objectType)) {
|
|
28
|
+
throw new Error("Usage: fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]");
|
|
29
|
+
}
|
|
30
|
+
const candidate: ResolveCandidate = {
|
|
31
|
+
objectType: objectType as ResolveCandidate["objectType"],
|
|
32
|
+
name: option(rest, "--name") ?? undefined,
|
|
33
|
+
domain: option(rest, "--domain") ?? undefined,
|
|
34
|
+
email: option(rest, "--email") ?? undefined,
|
|
35
|
+
accountId: option(rest, "--account-id") ?? undefined,
|
|
36
|
+
};
|
|
37
|
+
const snapshot = await readSnapshot(rest);
|
|
38
|
+
const result = resolveRecord(snapshot, candidate);
|
|
39
|
+
if (rest.includes("--json")) {
|
|
40
|
+
console.log(JSON.stringify(result, null, 2));
|
|
41
|
+
} else {
|
|
42
|
+
const marker = result.verdict === "safe_to_create" ? "✓" : result.verdict === "exists" ? "=" : "?";
|
|
43
|
+
console.log(`${marker} [${result.verdict}] ${result.reason}`);
|
|
44
|
+
for (const m of result.matches) {
|
|
45
|
+
console.log(` ${m.id} "${m.name}" — matched by ${m.matchedBy}: ${m.detail}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (result.verdict !== "safe_to_create") process.exitCode = 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Governed generic writes: build a dry-run patch plan from a snapshot filter
|
|
53
|
+
* plus field assignments (or --archive). Never writes — approve and apply the
|
|
54
|
+
* plan like any audit plan; compare-and-set protects every operation.
|
|
55
|
+
*/
|
|
56
|
+
export async function bulkUpdateCommand(args: string[]) {
|
|
57
|
+
const [objectType, ...rest] = args;
|
|
58
|
+
if (!objectType || !["account", "contact", "deal"].includes(objectType)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Usage: fullstackgtm bulk-update <account|contact|deal> --where <field=value|field!=value|field~substr|field:empty|field:notempty> [--where …] (--set <field>=<value> [--set …] | --archive) [source options] [--reason <text>] [--max-operations <n>] [--save] [--out <path>] [--json]",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const where = repeatedOption(rest, "--where");
|
|
64
|
+
const set: Record<string, string> = {};
|
|
65
|
+
for (const pair of repeatedOption(rest, "--set")) {
|
|
66
|
+
const separator = pair.indexOf("=");
|
|
67
|
+
if (separator === -1) throw new Error(`--set must look like <field>=<value>, got "${pair}"`);
|
|
68
|
+
set[pair.slice(0, separator)] = pair.slice(separator + 1);
|
|
69
|
+
}
|
|
70
|
+
const snapshot = await readSnapshot(rest);
|
|
71
|
+
const plan = buildBulkUpdatePlan(snapshot, {
|
|
72
|
+
objectType: objectType as "account" | "contact" | "deal",
|
|
73
|
+
where,
|
|
74
|
+
set: Object.keys(set).length > 0 ? set : undefined,
|
|
75
|
+
archive: rest.includes("--archive"),
|
|
76
|
+
forceArchiveDuplicates: rest.includes("--force-archive-duplicates"),
|
|
77
|
+
createTask: option(rest, "--create-task") ?? undefined,
|
|
78
|
+
require: repeatedOption(rest, "--require"),
|
|
79
|
+
guard: repeatedOption(rest, "--guard"),
|
|
80
|
+
reason: option(rest, "--reason") ?? undefined,
|
|
81
|
+
maxOperations: numericOption(rest, "--max-operations"),
|
|
82
|
+
// --today resolves the comparison `today` literal (e.g. closeDate<today);
|
|
83
|
+
// defaults to the system date inside buildBulkUpdatePlan when omitted.
|
|
84
|
+
today: option(rest, "--today") ?? undefined,
|
|
85
|
+
});
|
|
86
|
+
await emitPlan(plan, rest);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Shared plan output plumbing: --out, --save (with the approve/apply hint), --json or markdown. */
|
|
90
|
+
async function emitPlan(plan: PatchPlan, args: string[]) {
|
|
91
|
+
const out = option(args, "--out");
|
|
92
|
+
if (out) {
|
|
93
|
+
writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(plan, null, 2)}\n`);
|
|
94
|
+
}
|
|
95
|
+
if (saveRequested(args)) {
|
|
96
|
+
await createFilePlanStore().save(plan);
|
|
97
|
+
console.error(
|
|
98
|
+
`Saved plan ${plan.id} (${plan.operations.length} operations). Review with \`fullstackgtm plans show ${plan.id}\`, approve with \`fullstackgtm plans approve ${plan.id} --operations <ids|all>\`, then \`fullstackgtm apply --plan-id ${plan.id} --provider <name>\`.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (args.includes("--json")) {
|
|
102
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
103
|
+
} else {
|
|
104
|
+
console.log(patchPlanToMarkdown(plan));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Governed duplicate cleanup: group by a normalized identity key, propose one
|
|
110
|
+
* merge_records per duplicate group with a deterministic survivor. Never
|
|
111
|
+
* writes — approve and apply the plan like any audit plan.
|
|
112
|
+
*/
|
|
113
|
+
export async function dedupeCommand(args: string[]) {
|
|
114
|
+
const [objectType, ...rest] = args;
|
|
115
|
+
if (!objectType || !["account", "contact", "deal"].includes(objectType)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Usage: fullstackgtm dedupe <account|contact|deal> --key <domain|email|name> [--keep richest|oldest] [source options] [--reason <text>] [--max-operations <n>] [--save] [--out <path>] [--json]",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const key = option(rest, "--key");
|
|
121
|
+
if (!key || !["domain", "email", "name"].includes(key)) {
|
|
122
|
+
throw new Error("dedupe requires --key <domain|email|name> (the identity field duplicates share).");
|
|
123
|
+
}
|
|
124
|
+
const keep = option(rest, "--keep") ?? undefined;
|
|
125
|
+
const snapshot = await readSnapshot(rest);
|
|
126
|
+
const plan = buildDedupePlan(snapshot, {
|
|
127
|
+
objectType: objectType as DedupeOptions["objectType"],
|
|
128
|
+
key: key as DedupeOptions["key"],
|
|
129
|
+
keep: (keep ?? undefined) as DedupeOptions["keep"],
|
|
130
|
+
reason: option(rest, "--reason") ?? undefined,
|
|
131
|
+
maxOperations: numericOption(rest, "--max-operations"),
|
|
132
|
+
});
|
|
133
|
+
await emitPlan(plan, rest);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ownership handoff playbook: compile one bulk-update-style plan per object
|
|
138
|
+
* type. Each plan carries its full filter, so eligibility (including the
|
|
139
|
+
* --except-deal-stage exclusion) is re-verified per record at apply time.
|
|
140
|
+
*/
|
|
141
|
+
export async function reassignCommand(args: string[]) {
|
|
142
|
+
const from = option(args, "--from");
|
|
143
|
+
const to = option(args, "--to");
|
|
144
|
+
const assignUnowned = args.includes("--assign-unowned");
|
|
145
|
+
if (!to || (!from && !assignUnowned)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Usage: fullstackgtm reassign (--from <ownerId> | --assign-unowned) --to <ownerId> [--objects account,contact,deal] [--where <expr> …] [--except-deal-stage <stage>] [--include-closed-deals] [source options] [--reason <text>] [--max-operations <n>] [--save] [--out <path>] [--json]\n\n--assign-unowned claims every OWNERLESS record (ownerId:empty) for --to — the backfill twin of `enrich acquire`'s create-time assignment.",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const objects = option(args, "--objects")
|
|
151
|
+
?.split(",")
|
|
152
|
+
.map((value) => value.trim())
|
|
153
|
+
.filter(Boolean) as ReassignObjectType[] | undefined;
|
|
154
|
+
const snapshot = await readSnapshot(args);
|
|
155
|
+
const plans = buildReassignPlans(snapshot, {
|
|
156
|
+
fromOwnerId: from ?? undefined,
|
|
157
|
+
toOwnerId: to,
|
|
158
|
+
assignUnowned,
|
|
159
|
+
objects,
|
|
160
|
+
where: repeatedOption(args, "--where"),
|
|
161
|
+
exceptDealStage: option(args, "--except-deal-stage") ?? undefined,
|
|
162
|
+
includeClosedDeals: args.includes("--include-closed-deals"),
|
|
163
|
+
reason: option(args, "--reason") ?? undefined,
|
|
164
|
+
maxOperations: numericOption(args, "--max-operations"),
|
|
165
|
+
});
|
|
166
|
+
const out = option(args, "--out");
|
|
167
|
+
if (out) {
|
|
168
|
+
writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(plans, null, 2)}\n`);
|
|
169
|
+
}
|
|
170
|
+
if (args.includes("--json")) {
|
|
171
|
+
console.log(JSON.stringify(plans, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const store = saveRequested(args) ? createFilePlanStore() : null;
|
|
175
|
+
for (const plan of plans) {
|
|
176
|
+
if (store) await store.save(plan);
|
|
177
|
+
console.log(`${plan.id} ${String(plan.operations.length).padStart(3)} operation(s) ${plan.title}`);
|
|
178
|
+
console.log(` ${plan.summary}`);
|
|
179
|
+
}
|
|
180
|
+
if (store) {
|
|
181
|
+
console.log(
|
|
182
|
+
`\nSaved ${plans.length} plan(s). For each: \`fullstackgtm plans show <id>\`, \`fullstackgtm plans approve <id> --operations <ids|all>\`, then \`fullstackgtm apply --plan-id <id> --provider <name>\`.`,
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
console.log("\nDry run only — re-run with --save to store the plans for approval.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* One-shot composite for a single audit rule: audit → save → suggest →
|
|
191
|
+
* approve only suggestion-backed operations meeting the confidence bar (plus
|
|
192
|
+
* operations that carry concrete values and need no human input) → apply
|
|
193
|
+
* (only with --yes). Every stage goes through the same gates as the manual
|
|
194
|
+
* chain; placeholder values below the bar stay unapproved.
|
|
195
|
+
*/
|
|
196
|
+
export async function fixCommand(args: string[]) {
|
|
197
|
+
const ruleId = option(args, "--rule");
|
|
198
|
+
const provider = option(args, "--provider");
|
|
199
|
+
if (!ruleId || !provider) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
"Usage: fullstackgtm fix --rule <ruleId> --provider <name> [--min-confidence high|low] [--include-creates] [--today <iso>] [--yes|--confirm] [--dry-run]",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const minConfidence = option(args, "--min-confidence") ?? "high";
|
|
205
|
+
if (!["high", "low"].includes(minConfidence)) {
|
|
206
|
+
throw new Error("--min-confidence must be high or low");
|
|
207
|
+
}
|
|
208
|
+
const includeCreates = args.includes("--include-creates");
|
|
209
|
+
|
|
210
|
+
const loaded = loadConfig(option(args, "--config") ?? undefined);
|
|
211
|
+
const configured = await resolveConfiguredRules(loaded);
|
|
212
|
+
const rule = configured.find((candidate) => candidate.id === ruleId);
|
|
213
|
+
if (!rule) {
|
|
214
|
+
throw new Error(`Unknown rule: ${ruleId}. Available rules: ${configured.map((r) => r.id).join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
const policy = mergePolicy(defaultPolicy(), loaded?.config);
|
|
217
|
+
const today = option(args, "--today");
|
|
218
|
+
if (today) policy.today = today;
|
|
219
|
+
|
|
220
|
+
const snapshot = await readSnapshot(args);
|
|
221
|
+
const plan = auditSnapshot(snapshot, policy, [rule]);
|
|
222
|
+
if (plan.operations.length === 0) {
|
|
223
|
+
console.log(`fix ${ruleId}: audit proposed 0 operations — nothing to fix.`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const store = createFilePlanStore();
|
|
227
|
+
await store.save(plan);
|
|
228
|
+
|
|
229
|
+
const suggestions = suggestValues(plan, snapshot);
|
|
230
|
+
const accepted = new Set(minConfidence === "low" ? ["high", "low"] : ["high"]);
|
|
231
|
+
const overrides: Record<string, string> = {};
|
|
232
|
+
let belowBar = 0;
|
|
233
|
+
for (const suggestion of suggestions) {
|
|
234
|
+
if (
|
|
235
|
+
suggestion.suggestedValue &&
|
|
236
|
+
(accepted.has(suggestion.confidence) || (includeCreates && suggestion.confidence === "create"))
|
|
237
|
+
) {
|
|
238
|
+
overrides[suggestion.operationId] = suggestion.suggestedValue;
|
|
239
|
+
} else {
|
|
240
|
+
belowBar += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Approve operations whose placeholder got a qualifying suggested value,
|
|
244
|
+
// plus operations that already carry a concrete value (no human input
|
|
245
|
+
// needed — nothing to guess). Everything else stays unapproved.
|
|
246
|
+
const placeholderIds = new Set(suggestions.map((suggestion) => suggestion.operationId));
|
|
247
|
+
const approvedIds = plan.operations
|
|
248
|
+
.map((operation) => operation.id)
|
|
249
|
+
.filter((id) => overrides[id] !== undefined || !placeholderIds.has(id));
|
|
250
|
+
|
|
251
|
+
const lines = [
|
|
252
|
+
`fix ${ruleId} via ${provider}:`,
|
|
253
|
+
` proposed: ${plan.operations.length} operation(s) — plan ${plan.id} (saved)`,
|
|
254
|
+
` suggested: ${Object.keys(overrides).length} value(s) at ${minConfidence}+ confidence${includeCreates ? " (creates included)" : ""}${belowBar > 0 ? `; ${belowBar} below the bar (left unapproved)` : ""}`,
|
|
255
|
+
` approved: ${approvedIds.length} of ${plan.operations.length}`,
|
|
256
|
+
];
|
|
257
|
+
if (approvedIds.length === 0) {
|
|
258
|
+
lines.push(" applied: 0 — no operation met the confidence bar");
|
|
259
|
+
console.log(lines.join("\n"));
|
|
260
|
+
console.log(
|
|
261
|
+
`\nWiden with --min-confidence low / --include-creates, or approve manually: \`fullstackgtm plans approve ${plan.id} --operations <ids> --value <opId>=<value>\`.`,
|
|
262
|
+
);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await store.approveOperations(plan.id, approvedIds, overrides);
|
|
266
|
+
|
|
267
|
+
// `--confirm` is the standardized alias for the legacy `--yes`; `--dry-run`
|
|
268
|
+
// wins over both and locks the stop-before-apply path.
|
|
269
|
+
if (!confirmRequested(args, "--yes")) {
|
|
270
|
+
lines.push(" applied: 0 (stopped before apply — pass --yes to write)");
|
|
271
|
+
console.log(lines.join("\n"));
|
|
272
|
+
// The decision moment: frame the exact command that writes. Rich-only —
|
|
273
|
+
// piped output keeps the plain two-line hint byte-for-byte.
|
|
274
|
+
const p = paint(colorEnabled(process.stdout));
|
|
275
|
+
if (p.enabled) {
|
|
276
|
+
console.log("");
|
|
277
|
+
console.log(box([`fullstackgtm apply --plan-id ${plan.id} --provider ${provider}`], p, "Apply with").join("\n"));
|
|
278
|
+
} else {
|
|
279
|
+
console.log(`\nApply with:\n fullstackgtm apply --plan-id ${plan.id} --provider ${provider}`);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const connector = await connectorFor(provider, args);
|
|
284
|
+
// Live safety ticker on interactive terminals (stderr; inert otherwise).
|
|
285
|
+
const ticker = createStatusLine();
|
|
286
|
+
let run: PatchPlanRun;
|
|
287
|
+
try {
|
|
288
|
+
run = await applyPatchPlan(connector, plan, {
|
|
289
|
+
approvedOperationIds: approvedIds,
|
|
290
|
+
valueOverrides: overrides,
|
|
291
|
+
onOperation: ticker.active
|
|
292
|
+
? (progress) =>
|
|
293
|
+
ticker.set(
|
|
294
|
+
`Applying ${progress.completed}/${progress.total} · ✓ ${progress.applied} applied · ${progress.failed} failed · ${progress.conflicts} conflict${progress.conflicts === 1 ? "" : "s"}`,
|
|
295
|
+
)
|
|
296
|
+
: undefined,
|
|
297
|
+
});
|
|
298
|
+
} finally {
|
|
299
|
+
ticker.done();
|
|
300
|
+
}
|
|
301
|
+
await store.recordRun(plan.id, run);
|
|
302
|
+
const counts: Record<string, number> = { applied: 0, conflict: 0, skipped: 0, failed: 0 };
|
|
303
|
+
for (const result of run.results) counts[result.status] = (counts[result.status] ?? 0) + 1;
|
|
304
|
+
lines.push(
|
|
305
|
+
` applied: ${counts.applied} · conflicts: ${counts.conflict} · skipped: ${counts.skipped} · failed: ${counts.failed}`,
|
|
306
|
+
);
|
|
307
|
+
console.log(lines.join("\n"));
|
|
308
|
+
if (run.status === "failed") process.exitCode = 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function suggest(args: string[]) {
|
|
312
|
+
const planId = option(args, "--plan-id");
|
|
313
|
+
const planPath = option(args, "--plan");
|
|
314
|
+
if (!planId && !planPath) {
|
|
315
|
+
// Same legible guess as `apply <id>`: name the exact corrected command.
|
|
316
|
+
const positional = args.find((arg) => !arg.startsWith("-") && !isOptionValue(args, arg));
|
|
317
|
+
if (positional) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`suggest takes the plan as a flag, not a positional. Try: fullstackgtm suggest --plan-id ${positional}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
throw new Error("suggest requires --plan <path> or --plan-id <id>");
|
|
323
|
+
}
|
|
324
|
+
let plan: PatchPlan;
|
|
325
|
+
if (planId) {
|
|
326
|
+
const stored = await createFilePlanStore().get(planId);
|
|
327
|
+
if (!stored) throw new Error(`No stored plan with id ${planId}.`);
|
|
328
|
+
plan = stored.plan;
|
|
329
|
+
} else {
|
|
330
|
+
plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath!), "utf8")) as PatchPlan;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const snapshot = await readSnapshot(args);
|
|
334
|
+
const suggestions = suggestValues(plan, snapshot);
|
|
335
|
+
const payload = { planId: planId ?? planPath, suggestions };
|
|
336
|
+
|
|
337
|
+
const outPath = option(args, "--out");
|
|
338
|
+
if (outPath) writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(payload, null, 2)}\n`);
|
|
339
|
+
|
|
340
|
+
if (args.includes("--json")) {
|
|
341
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (suggestions.length === 0) {
|
|
346
|
+
console.log("No requires_human_* placeholder operations in this plan — nothing to suggest.");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const byConfidence: Record<string, number> = {};
|
|
350
|
+
for (const s of suggestions) byConfidence[s.confidence] = (byConfidence[s.confidence] ?? 0) + 1;
|
|
351
|
+
console.log(`Suggestions for ${suggestions.length} placeholder operation(s):\n`);
|
|
352
|
+
for (const s of suggestions) {
|
|
353
|
+
const marker =
|
|
354
|
+
s.confidence === "high" ? "✓" : s.confidence === "low" ? "~" : s.confidence === "create" ? "+" : "✗";
|
|
355
|
+
console.log(`${marker} [${s.confidence}] ${s.operationId} ${s.objectName ?? s.objectId}`);
|
|
356
|
+
console.log(` ${s.suggestedValue ? `→ ${s.suggestedValue}` : "(no suggestion)"} — ${s.reason}`);
|
|
357
|
+
}
|
|
358
|
+
console.log(`\n${Object.entries(byConfidence).map(([k, v]) => `${k}: ${v}`).join(" · ")}`);
|
|
359
|
+
if (planId && (byConfidence.high ?? 0) > 0 && !outPath) {
|
|
360
|
+
console.log(
|
|
361
|
+
`\nChain it:\n fullstackgtm suggest --plan-id ${planId} ${snapshotSourceHint(args)}--out suggestions.json\n fullstackgtm plans approve ${planId} --values-from suggestions.json\n fullstackgtm apply --plan-id ${planId} --provider <name>`,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function snapshotSourceHint(args: string[]) {
|
|
367
|
+
const provider = option(args, "--provider");
|
|
368
|
+
if (provider) return `--provider ${provider} `;
|
|
369
|
+
const input = option(args, "--input");
|
|
370
|
+
if (input) return `--input ${input} `;
|
|
371
|
+
return "";
|
|
372
|
+
}
|