fullstackgtm 0.44.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 +185 -1
- package/README.md +19 -7
- 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 +100 -5130
- 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/salesforce.d.ts +3 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/signalSources.js +7 -59
- package/dist/icp.d.ts +15 -11
- package/dist/icp.js +19 -36
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- 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/schedule.d.ts +80 -2
- package/dist/schedule.js +254 -1
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/types.d.ts +11 -0
- package/docs/api.md +73 -7
- package/docs/signal-spool-format.md +13 -0
- package/llms.txt +15 -6
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +1 -1
- 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 +99 -5829
- package/src/connector.ts +46 -0
- package/src/connectors/hubspot.ts +14 -2
- package/src/connectors/salesforce.ts +18 -2
- package/src/connectors/signalSources.ts +7 -56
- package/src/icp.ts +19 -35
- package/src/judge.ts +7 -0
- package/src/marketClassify.ts +8 -1
- package/src/mcp-bin.ts +2 -2
- package/src/mcp.ts +130 -57
- package/src/schedule.ts +310 -3
- package/src/spoolFiles.ts +116 -0
- package/src/types.ts +12 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { auditSnapshot, defaultPolicy } from "../audit.js";
|
|
5
|
+
import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
|
|
6
|
+
import { applyPatchPlan } from "../connector.js";
|
|
7
|
+
import { diffFindings, diffSnapshots, diffToMarkdown } from "../diff.js";
|
|
8
|
+
import { createChannelConnector } from "../connectors/outboxChannel.js";
|
|
9
|
+
import { formatPatchPlanRun, patchPlanToMarkdown } from "../format.js";
|
|
10
|
+
import { mergeSnapshots } from "../merge.js";
|
|
11
|
+
import { verifyApprovalDigests } from "../integrity.js";
|
|
12
|
+
import { buildAuditLog, verifyAuditLog } from "../auditLog.js";
|
|
13
|
+
import { createFilePlanStore } from "../planStore.js";
|
|
14
|
+
import { ENRICH_CONFIG_FILE_NAME, loadEnrichConfig } from "../enrich.js";
|
|
15
|
+
import { loadMeter, recordConsumption, remaining } from "../acquireMeter.js";
|
|
16
|
+
import { reportCounts } from "../runReport.js";
|
|
17
|
+
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.js";
|
|
18
|
+
import { colorEnabled, createStatusLine, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
|
|
19
|
+
import { unknownSubcommandError } from "./suggest.js";
|
|
20
|
+
function parseValueOverrides(args) {
|
|
21
|
+
const valueOverrides = {};
|
|
22
|
+
for (const pair of repeatedOption(args, "--value")) {
|
|
23
|
+
const separator = pair.indexOf("=");
|
|
24
|
+
if (separator === -1)
|
|
25
|
+
throw new Error(`--value must look like <operationId>=<value>`);
|
|
26
|
+
valueOverrides[pair.slice(0, separator)] = pair.slice(separator + 1);
|
|
27
|
+
}
|
|
28
|
+
return valueOverrides;
|
|
29
|
+
}
|
|
30
|
+
function tryLoadAcquireConfig(args) {
|
|
31
|
+
try {
|
|
32
|
+
const path = resolve(process.cwd(), option(args, "--config") ?? ENRICH_CONFIG_FILE_NAME);
|
|
33
|
+
if (!existsSync(path))
|
|
34
|
+
return undefined;
|
|
35
|
+
return loadEnrichConfig(path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function readSuggestionValues(path, minConfidence, includeCreates) {
|
|
42
|
+
const raw = JSON.parse(readFileSync(resolve(process.cwd(), path), "utf8"));
|
|
43
|
+
if (!Array.isArray(raw.suggestions)) {
|
|
44
|
+
throw new Error(`${path} is not a suggestions file (expected { suggestions: [...] } from \`fullstackgtm suggest --out\`).`);
|
|
45
|
+
}
|
|
46
|
+
const accepted = new Set(minConfidence === "low" ? ["high", "low"] : ["high"]);
|
|
47
|
+
const overrides = {};
|
|
48
|
+
let skipped = 0;
|
|
49
|
+
for (const s of raw.suggestions) {
|
|
50
|
+
if (!s.suggestedValue)
|
|
51
|
+
continue;
|
|
52
|
+
if (accepted.has(s.confidence) || (includeCreates && s.confidence === "create")) {
|
|
53
|
+
overrides[s.operationId] = s.suggestedValue;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
skipped += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { overrides, skipped };
|
|
60
|
+
}
|
|
61
|
+
export async function auditLogCommand(args) {
|
|
62
|
+
const [sub, ...rest] = args;
|
|
63
|
+
if (!sub || sub === "--help" || sub === "-h" || (sub !== "export" && sub !== "verify")) {
|
|
64
|
+
console.log(`Usage:
|
|
65
|
+
audit-log export [--out <path>] [--json] hash-chained, signed record of every apply run
|
|
66
|
+
audit-log verify [--in <path>] re-check an exported log's chain and signature
|
|
67
|
+
|
|
68
|
+
export flattens every apply run across all stored plans (this profile) into a
|
|
69
|
+
tamper-evident chain — each entry carries the prior entry's hash, and the chain
|
|
70
|
+
head is HMAC-signed with this install's key — so a change-management process can
|
|
71
|
+
archive one file and later prove it was not edited. verify recomputes the chain
|
|
72
|
+
and (if the signing key is present) the signature.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (sub === "export") {
|
|
76
|
+
const plans = await createFilePlanStore().list();
|
|
77
|
+
const log = buildAuditLog(plans, new Date().toISOString());
|
|
78
|
+
const payload = `${JSON.stringify(log, null, 2)}\n`;
|
|
79
|
+
const outPath = option(rest, "--out");
|
|
80
|
+
if (outPath) {
|
|
81
|
+
writeFileSync(resolve(process.cwd(), outPath), payload);
|
|
82
|
+
console.log(`Wrote ${outPath}: ${log.entryCount} run(s), chain head ${log.chainHead.slice(0, 12)}${log.signature ? " (signed)" : " (unsigned — no signing key on this install)"}.`);
|
|
83
|
+
}
|
|
84
|
+
else if (rest.includes("--json")) {
|
|
85
|
+
console.log(payload);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log(`${log.entryCount} apply run(s); chain head ${log.chainHead.slice(0, 12)}${log.signature ? ", signed" : ", unsigned"}. Pass --out <path> to archive, or --json to print.`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// verify
|
|
93
|
+
const inPath = option(rest, "--in");
|
|
94
|
+
if (!inPath)
|
|
95
|
+
throw new Error("audit-log verify requires --in <exported-log.json>");
|
|
96
|
+
const log = JSON.parse(readFileSync(resolve(process.cwd(), inPath), "utf8"));
|
|
97
|
+
const result = verifyAuditLog(log);
|
|
98
|
+
if (rest.includes("--json")) {
|
|
99
|
+
console.log(JSON.stringify(result, null, 2));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(result.ok ? `OK — ${result.detail}` : `TAMPERED — ${result.detail}`);
|
|
103
|
+
}
|
|
104
|
+
if (!result.ok)
|
|
105
|
+
process.exitCode = 2;
|
|
106
|
+
}
|
|
107
|
+
export async function apply(args) {
|
|
108
|
+
// `plans approve <id>` teaches the positional shape, so `apply <id>` is a
|
|
109
|
+
// legible guess — it used to be misdiagnosed as a missing --provider.
|
|
110
|
+
// Apply stays flag-explicit (it is the write step); name the exact
|
|
111
|
+
// corrected command instead of executing the inference.
|
|
112
|
+
const positional = args.find((arg) => !arg.startsWith("-") && !isOptionValue(args, arg));
|
|
113
|
+
if (positional && !option(args, "--plan-id") && !option(args, "--plan")) {
|
|
114
|
+
throw new Error("apply takes the plan as a flag, not a positional. " +
|
|
115
|
+
`Try: fullstackgtm apply --plan-id ${positional} --provider <hubspot|salesforce|stripe>`);
|
|
116
|
+
}
|
|
117
|
+
const provider = option(args, "--provider");
|
|
118
|
+
const channel = option(args, "--channel");
|
|
119
|
+
if (!provider && !channel) {
|
|
120
|
+
throw new Error("apply requires --provider <name> (CRM) or --channel <id> (e.g. outbox)");
|
|
121
|
+
}
|
|
122
|
+
if (provider && channel) {
|
|
123
|
+
throw new Error("apply takes --provider OR --channel, not both — a plan applies to one target.");
|
|
124
|
+
}
|
|
125
|
+
const planId = option(args, "--plan-id");
|
|
126
|
+
const planPath = option(args, "--plan");
|
|
127
|
+
if (!planId && !planPath)
|
|
128
|
+
throw new Error("apply requires --plan <path> or --plan-id <id>");
|
|
129
|
+
let plan;
|
|
130
|
+
let approvedOperationIds;
|
|
131
|
+
let valueOverrides;
|
|
132
|
+
const store = planId ? createFilePlanStore() : null;
|
|
133
|
+
if (planId && store) {
|
|
134
|
+
const stored = await store.get(planId);
|
|
135
|
+
if (!stored)
|
|
136
|
+
throw new Error(`No stored plan with id ${planId}.`);
|
|
137
|
+
if (stored.status !== "approved") {
|
|
138
|
+
throw new Error(`Plan ${planId} is ${stored.status}; approve operations first with \`fullstackgtm plans approve ${planId} --operations <ids|all>\`.`);
|
|
139
|
+
}
|
|
140
|
+
plan = stored.plan;
|
|
141
|
+
approvedOperationIds = stored.approvedOperationIds;
|
|
142
|
+
// Downgrade guard: an approved plan with no signatures is either pre-0.26
|
|
143
|
+
// (re-approve to gain them) or had its approvalDigests stripped to skip the
|
|
144
|
+
// integrity check. Either way, refuse rather than fall back to trusting the
|
|
145
|
+
// file. (A plan with zero approved operations has nothing to apply anyway.)
|
|
146
|
+
if (stored.approvedOperationIds.length > 0 && !stored.approvalDigests) {
|
|
147
|
+
throw new Error(`Refusing to apply plan ${planId}: it was approved without integrity signatures ` +
|
|
148
|
+
"(approved before 0.26.0, or its signatures were removed). Re-approve it with " +
|
|
149
|
+
`\`fullstackgtm plans approve ${planId} --operations <ids|all>\`.`);
|
|
150
|
+
}
|
|
151
|
+
// Integrity gate: the plan file is re-read from disk, so verify each approved
|
|
152
|
+
// operation still matches what was signed at approval. Verify against the
|
|
153
|
+
// EFFECTIVE overrides (stored ∪ apply-time --value): the invariant is "what
|
|
154
|
+
// gets written must equal what was signed", so an apply-time --value that
|
|
155
|
+
// changes a value the human did not approve is treated as tamper, not a live
|
|
156
|
+
// override. A mismatch means the plan/overrides were edited after approval —
|
|
157
|
+
// refuse the whole apply rather than write an unapproved value.
|
|
158
|
+
valueOverrides = { ...stored.valueOverrides, ...parseValueOverrides(args) };
|
|
159
|
+
const verification = verifyApprovalDigests(stored.plan.operations, stored.approvedOperationIds, valueOverrides, stored.approvalDigests);
|
|
160
|
+
if (!verification.ok) {
|
|
161
|
+
const detail = verification.reason === "no_key"
|
|
162
|
+
? "the plan-signing key is missing (was this plan approved on another machine?). Re-approve it here with `fullstackgtm plans approve`."
|
|
163
|
+
: `these operations differ from what was approved: ${verification.tampered.join(", ")}. ` +
|
|
164
|
+
"If you changed a value at apply time, set it at approval instead (`plans approve --value <op>=<v>`) and re-approve; " +
|
|
165
|
+
"otherwise the plan was edited after approval — review and re-approve.";
|
|
166
|
+
throw new Error(`Refusing to apply plan ${planId}: ${detail}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const approve = option(args, "--approve");
|
|
171
|
+
if (!approve) {
|
|
172
|
+
throw new Error('apply requires --approve <ids|all>; nothing is written without approval');
|
|
173
|
+
}
|
|
174
|
+
plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
|
|
175
|
+
approvedOperationIds =
|
|
176
|
+
approve === "all"
|
|
177
|
+
? plan.operations.map((operation) => operation.id)
|
|
178
|
+
: approve.split(",").map((id) => id.trim()).filter(Boolean);
|
|
179
|
+
valueOverrides = parseValueOverrides(args);
|
|
180
|
+
}
|
|
181
|
+
// Acquire meter: create_record ops are budgeted. Refuse up-front if the
|
|
182
|
+
// approved creates would exceed the current budget window (the plan was
|
|
183
|
+
// capped when `enrich acquire` ran, but the budget may have been spent down
|
|
184
|
+
// since), then charge the meter for what actually lands.
|
|
185
|
+
const approvedSet = new Set(approvedOperationIds);
|
|
186
|
+
const createOps = plan.operations.filter((op) => op.operation === "create_record" && approvedSet.has(op.id));
|
|
187
|
+
const createSpend = (op) => (op.afterValue?.estCostUsd) ?? 0;
|
|
188
|
+
if (createOps.length > 0) {
|
|
189
|
+
const acquireConfig = tryLoadAcquireConfig(args)?.acquire;
|
|
190
|
+
if (acquireConfig?.budget) {
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const head = remaining(loadMeter(now), acquireConfig.budget, 0, now);
|
|
193
|
+
const approvedSpend = createOps.reduce((sum, op) => sum + createSpend(op), 0);
|
|
194
|
+
const refusals = [];
|
|
195
|
+
if (head.records.day !== null && createOps.length > head.records.day)
|
|
196
|
+
refusals.push(`${createOps.length} creates > ${head.records.day} record(s) left today`);
|
|
197
|
+
if (head.records.month !== null && createOps.length > head.records.month)
|
|
198
|
+
refusals.push(`${createOps.length} creates > ${head.records.month} record(s) left this month`);
|
|
199
|
+
if (head.spendUsd.day !== null && approvedSpend > head.spendUsd.day)
|
|
200
|
+
refusals.push(`$${approvedSpend.toFixed(2)} > $${head.spendUsd.day.toFixed(2)} spend left today`);
|
|
201
|
+
if (head.spendUsd.month !== null && approvedSpend > head.spendUsd.month)
|
|
202
|
+
refusals.push(`$${approvedSpend.toFixed(2)} > $${head.spendUsd.month.toFixed(2)} spend left this month`);
|
|
203
|
+
if (refusals.length > 0) {
|
|
204
|
+
throw new Error(`Refusing to apply: acquire budget exceeded (${refusals.join("; ")}). ` +
|
|
205
|
+
"Re-run `fullstackgtm enrich acquire` to re-cap to the remaining budget, or wait for the window to reset.");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// A channel (e.g. outbox) renders approved ops to a local artifact and
|
|
210
|
+
// transmits nothing; a CRM provider writes records. Same governed apply path.
|
|
211
|
+
const connector = channel ? createChannelConnector(channel) : await connectorFor(provider, args);
|
|
212
|
+
// Interactive terminals get a live safety ticker on stderr while the run
|
|
213
|
+
// executes; the summary line it leaves behind is plain text. Inert otherwise.
|
|
214
|
+
const ticker = createStatusLine();
|
|
215
|
+
let run;
|
|
216
|
+
try {
|
|
217
|
+
run = await applyPatchPlan(connector, plan, {
|
|
218
|
+
approvedOperationIds,
|
|
219
|
+
valueOverrides,
|
|
220
|
+
onOperation: ticker.active
|
|
221
|
+
? (progress) => ticker.set(`Applying ${progress.completed}/${progress.total} · ✓ ${progress.applied} applied · ${progress.failed} failed · ${progress.conflicts} conflict${progress.conflicts === 1 ? "" : "s"} · ${progress.skipped} skipped`)
|
|
222
|
+
: undefined,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
ticker.done();
|
|
227
|
+
}
|
|
228
|
+
if (planId && store) {
|
|
229
|
+
await store.recordRun(planId, run);
|
|
230
|
+
}
|
|
231
|
+
// Charge the acquire meter for the creates that actually landed.
|
|
232
|
+
if (createOps.length > 0) {
|
|
233
|
+
const appliedIds = new Set(run.results.filter((result) => result.status === "applied").map((result) => result.operationId));
|
|
234
|
+
const landed = createOps.filter((op) => appliedIds.has(op.id));
|
|
235
|
+
if (landed.length > 0) {
|
|
236
|
+
const spend = landed.reduce((sum, op) => sum + createSpend(op), 0);
|
|
237
|
+
recordConsumption(new Date(), landed.length, spend);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Observability: apply-outcome tallies for the web run timeline.
|
|
241
|
+
const applyTally = { applied: 0, conflict: 0, skipped: 0, failed: 0 };
|
|
242
|
+
for (const result of run.results)
|
|
243
|
+
applyTally[result.status] = (applyTally[result.status] ?? 0) + 1;
|
|
244
|
+
reportCounts(applyTally);
|
|
245
|
+
// Rich-only closing line on stderr (ticker.active implies an interactive,
|
|
246
|
+
// color-safe stderr): the one-glance outcome of the run.
|
|
247
|
+
if (ticker.active) {
|
|
248
|
+
const pe = paint(true);
|
|
249
|
+
const badge = run.status === "applied"
|
|
250
|
+
? pe.green(`✓ ${applyTally.applied} operation(s) applied`)
|
|
251
|
+
: run.status === "partial"
|
|
252
|
+
? pe.yellow(`! partial: ${applyTally.applied} applied, ${applyTally.conflict} conflict(s), ${applyTally.failed} failed`)
|
|
253
|
+
: pe.red(`✗ ${run.status}: ${applyTally.applied} applied, ${applyTally.failed} failed`);
|
|
254
|
+
process.stderr.write(`${badge}${planId && store ? pe.dim(" — run recorded on the plan") : ""}\n`);
|
|
255
|
+
}
|
|
256
|
+
if (args.includes("--json")) {
|
|
257
|
+
console.log(JSON.stringify(run, null, 2));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log(formatPatchPlanRun(run));
|
|
261
|
+
}
|
|
262
|
+
if (run.status === "failed")
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
}
|
|
265
|
+
export async function diffCommand(args) {
|
|
266
|
+
const beforePath = option(args, "--before");
|
|
267
|
+
const afterPath = option(args, "--after");
|
|
268
|
+
if (!beforePath || !afterPath) {
|
|
269
|
+
throw new Error("diff requires --before <snapshot.json> and --after <snapshot.json>");
|
|
270
|
+
}
|
|
271
|
+
const before = JSON.parse(readFileSync(resolve(process.cwd(), beforePath), "utf8"));
|
|
272
|
+
const after = JSON.parse(readFileSync(resolve(process.cwd(), afterPath), "utf8"));
|
|
273
|
+
const loaded = loadConfig(option(args, "--config") ?? undefined);
|
|
274
|
+
const rules = selectedRules(args, await resolveConfiguredRules(loaded));
|
|
275
|
+
const policy = mergePolicy(defaultPolicy(), loaded?.config);
|
|
276
|
+
const today = option(args, "--today");
|
|
277
|
+
if (today)
|
|
278
|
+
policy.today = today;
|
|
279
|
+
const staleDealDays = numericOption(args, "--stale-days");
|
|
280
|
+
if (staleDealDays !== undefined)
|
|
281
|
+
policy.staleDealDays = staleDealDays;
|
|
282
|
+
const diff = diffSnapshots(before, after);
|
|
283
|
+
const drift = diffFindings(auditSnapshot(before, policy, rules), auditSnapshot(after, policy, rules));
|
|
284
|
+
if (args.includes("--json")) {
|
|
285
|
+
console.log(JSON.stringify({ diff, drift }, null, 2));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
console.log(diffToMarkdown(diff, drift));
|
|
289
|
+
}
|
|
290
|
+
if (args.includes("--fail-on-new-findings") && drift.newFindings.length > 0) {
|
|
291
|
+
process.exitCode = 2;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
export async function mergeCommand(args) {
|
|
295
|
+
const inputs = repeatedOption(args, "--input");
|
|
296
|
+
if (inputs.length < 2) {
|
|
297
|
+
throw new Error("merge requires at least two --input <snapshot.json> sources");
|
|
298
|
+
}
|
|
299
|
+
const out = option(args, "--out");
|
|
300
|
+
if (!out)
|
|
301
|
+
throw new Error("merge requires --out <merged.json>");
|
|
302
|
+
const snapshots = inputs.map((input) => JSON.parse(readFileSync(resolve(process.cwd(), input), "utf8")));
|
|
303
|
+
const { snapshot, report } = mergeSnapshots(snapshots);
|
|
304
|
+
writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
305
|
+
if (args.includes("--json")) {
|
|
306
|
+
console.log(JSON.stringify(report, null, 2));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log(`Merged ${report.sources.join(" + ")} into ${out}.`);
|
|
310
|
+
for (const type of ["user", "account", "contact"]) {
|
|
311
|
+
const count = report.matches.filter((match) => match.type === type).length;
|
|
312
|
+
console.log(` ${type} merges: ${count}`);
|
|
313
|
+
}
|
|
314
|
+
console.log(` conflicts: ${report.conflicts.length}`);
|
|
315
|
+
for (const conflict of report.conflicts) {
|
|
316
|
+
console.log(` ${conflict.type}/${conflict.recordId} ${conflict.field}: ${conflict.values
|
|
317
|
+
.map((entry) => `${entry.provider}=${JSON.stringify(entry.value)}`)
|
|
318
|
+
.join(" vs ")}`);
|
|
319
|
+
}
|
|
320
|
+
if (report.suggestions.length > 0) {
|
|
321
|
+
console.log(` review suggestions (same name, not auto-merged): ${report.suggestions.length}`);
|
|
322
|
+
for (const suggestion of report.suggestions) {
|
|
323
|
+
console.log(` "${suggestion.name}": ${suggestion.recordIds.join(", ")}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
console.log(`Audit the merged view with \`fullstackgtm audit --input ${out}\`.`);
|
|
327
|
+
}
|
|
328
|
+
export async function plansCommand(args) {
|
|
329
|
+
const store = createFilePlanStore();
|
|
330
|
+
const [subcommand, ...rest] = args;
|
|
331
|
+
if (subcommand === "list" || subcommand === undefined) {
|
|
332
|
+
const status = option(rest, "--status");
|
|
333
|
+
const plans = await store.list(status ?? undefined);
|
|
334
|
+
if (rest.includes("--json") || args.includes("--json")) {
|
|
335
|
+
console.log(JSON.stringify(plans.map((stored) => ({
|
|
336
|
+
id: stored.plan.id,
|
|
337
|
+
status: stored.status,
|
|
338
|
+
summary: stored.plan.summary,
|
|
339
|
+
approvedOperations: stored.approvedOperationIds.length,
|
|
340
|
+
runs: stored.runs.length,
|
|
341
|
+
createdAt: stored.createdAt,
|
|
342
|
+
})), null, 2));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (plans.length === 0) {
|
|
346
|
+
console.log("No stored plans. Create one with `fullstackgtm audit ... --save`.");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const p = paint(colorEnabled(process.stdout));
|
|
350
|
+
if (p.enabled) {
|
|
351
|
+
// Interactive terminals get an aligned table with status color-banding;
|
|
352
|
+
// the plain per-line format below is unchanged for pipes.
|
|
353
|
+
const rows = plans.map((stored) => [
|
|
354
|
+
stored.plan.id,
|
|
355
|
+
stored.status,
|
|
356
|
+
`${stored.approvedOperationIds.length} approved`,
|
|
357
|
+
`${stored.runs.length} run${stored.runs.length === 1 ? "" : "s"}`,
|
|
358
|
+
stored.plan.summary,
|
|
359
|
+
]);
|
|
360
|
+
// Long summaries would wrap and break the table's alignment. The summary
|
|
361
|
+
// is the LAST column: cap it to what's left of the terminal width after
|
|
362
|
+
// the fixed columns and their two-space gutters (floor of 24 so narrow
|
|
363
|
+
// terminals still show something useful).
|
|
364
|
+
const columns = process.stdout.columns ?? 80;
|
|
365
|
+
const fixedWidth = [0, 1, 2, 3].reduce((sum, index) => sum + Math.max(...rows.map((row) => row[index].length)), 0) + 2 * 4;
|
|
366
|
+
const summaryWidth = Math.max(24, columns - fixedWidth);
|
|
367
|
+
for (const row of rows)
|
|
368
|
+
row[4] = truncateToWidth(row[4], summaryWidth);
|
|
369
|
+
const statusPainter = (cell) => {
|
|
370
|
+
const painted = planStatusWord(cell.trimEnd(), p);
|
|
371
|
+
return painted + cell.slice(cell.trimEnd().length);
|
|
372
|
+
};
|
|
373
|
+
for (const line of table(rows, [null, statusPainter, p.dim, p.dim, null])) {
|
|
374
|
+
console.log(line);
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (const stored of plans) {
|
|
379
|
+
console.log(`${stored.plan.id} ${stored.status.padEnd(14)} ${stored.plan.summary} (${stored.approvedOperationIds.length} approved, ${stored.runs.length} runs)`);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (subcommand === "show") {
|
|
384
|
+
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
385
|
+
if (!planId)
|
|
386
|
+
throw new Error("Usage: fullstackgtm plans show <planId>");
|
|
387
|
+
const stored = await store.get(planId);
|
|
388
|
+
if (!stored)
|
|
389
|
+
throw new Error(`No stored plan with id ${planId}.`);
|
|
390
|
+
if (rest.includes("--json")) {
|
|
391
|
+
console.log(JSON.stringify(stored, null, 2));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const showPaint = paint(colorEnabled(process.stdout));
|
|
395
|
+
console.log(`Status: ${planStatusWord(stored.status, showPaint)}`);
|
|
396
|
+
console.log(`Approved operations: ${stored.approvedOperationIds.join(", ") || "none"}`);
|
|
397
|
+
console.log(`Runs: ${stored.runs.length}`);
|
|
398
|
+
console.log("");
|
|
399
|
+
console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (subcommand === "approve") {
|
|
403
|
+
const planId = rest.find((arg) => !arg.startsWith("--") && !isOptionValue(rest, arg));
|
|
404
|
+
if (!planId)
|
|
405
|
+
throw new Error("Usage: fullstackgtm plans approve <planId> --operations <ids|all> | --values-from <suggestions.json>");
|
|
406
|
+
const operations = option(rest, "--operations");
|
|
407
|
+
const valuesFrom = option(rest, "--values-from");
|
|
408
|
+
if (!operations && !valuesFrom) {
|
|
409
|
+
throw new Error("plans approve requires --operations <ids|all> and/or --values-from <suggestions.json>");
|
|
410
|
+
}
|
|
411
|
+
const stored = await store.get(planId);
|
|
412
|
+
if (!stored)
|
|
413
|
+
throw new Error(`No stored plan with id ${planId}.`);
|
|
414
|
+
// Values from a `fullstackgtm suggest --out` file. High-confidence only by
|
|
415
|
+
// default; widen with --min-confidence low, opt into record-creating
|
|
416
|
+
// values (create:<Name>) with --include-creates. Explicit --value wins.
|
|
417
|
+
let fileOverrides = {};
|
|
418
|
+
if (valuesFrom) {
|
|
419
|
+
const minConfidence = option(rest, "--min-confidence") ?? "high";
|
|
420
|
+
if (!["high", "low"].includes(minConfidence)) {
|
|
421
|
+
throw new Error("--min-confidence must be high or low");
|
|
422
|
+
}
|
|
423
|
+
const { overrides, skipped } = readSuggestionValues(valuesFrom, minConfidence, rest.includes("--include-creates"));
|
|
424
|
+
fileOverrides = overrides;
|
|
425
|
+
if (Object.keys(overrides).length === 0) {
|
|
426
|
+
throw new Error(`No suggestions in ${valuesFrom} meet the confidence bar (${skipped} below it). Re-run with --min-confidence low or --include-creates, or pass explicit --value overrides.`);
|
|
427
|
+
}
|
|
428
|
+
if (skipped > 0) {
|
|
429
|
+
console.log(`Skipped ${skipped} suggestion(s) below the confidence bar (widen with --min-confidence low / --include-creates).`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const explicitOverrides = parseValueOverrides(rest);
|
|
433
|
+
const operationIds = operations === "all"
|
|
434
|
+
? stored.plan.operations.map((operation) => operation.id)
|
|
435
|
+
: operations
|
|
436
|
+
? operations.split(",").map((id) => id.trim()).filter(Boolean)
|
|
437
|
+
: Object.keys(fileOverrides);
|
|
438
|
+
const updated = await store.approveOperations(planId, operationIds, {
|
|
439
|
+
...fileOverrides,
|
|
440
|
+
...explicitOverrides,
|
|
441
|
+
});
|
|
442
|
+
console.log(`Approved ${updated.approvedOperationIds.length} operation(s) on ${planId}. Apply with \`fullstackgtm apply --plan-id ${planId} --provider <name>\`.`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (subcommand === "reject") {
|
|
446
|
+
const planId = rest.find((arg) => !arg.startsWith("--"));
|
|
447
|
+
if (!planId)
|
|
448
|
+
throw new Error("Usage: fullstackgtm plans reject <planId>");
|
|
449
|
+
await store.reject(planId);
|
|
450
|
+
console.log(`Rejected ${planId}.`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
throw unknownSubcommandError("plans", subcommand, ["list", "show", "approve", "reject"]);
|
|
454
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schedule layer: declarative cadences for read/plan-side commands,
|
|
3
|
+
* materialized through a provider (MVP: the user crontab), with an
|
|
4
|
+
* append-only run history. The governance invariant — scheduling never
|
|
5
|
+
* auto-approves — is enforced at `add` time and re-checked at `run` time.
|
|
6
|
+
* Spec: docs/schedule.md (monorepo).
|
|
7
|
+
*/
|
|
8
|
+
export declare function scheduleCommand(args: string[]): Promise<void>;
|