@counterfact-labs/cli 0.2.6 → 0.2.7
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/README.md +20 -0
- package/bundle/client-release.json +1 -1
- package/dist/adapters/completion-lock.js +95 -0
- package/dist/adapters/completion.js +132 -17
- package/dist/adapters/entry-files.js +126 -0
- package/dist/adapters/entry-selection.js +45 -0
- package/dist/adapters/files.js +2 -1
- package/dist/adapters/results.js +10 -2
- package/dist/adapters/selection.js +104 -0
- package/dist/adapters/settings.js +29 -2
- package/dist/analysis-core/analysis/schemas.js +1 -1
- package/dist/analysis-core/chunks/2PZF54KH.js +10 -0
- package/dist/analysis-core/chunks/{OPMA32A6.js → 6RWEWBHK.js} +1 -1
- package/dist/analysis-core/chunks/{PECX3OXB.js → 7NEWZ74F.js} +1 -1
- package/dist/analysis-core/chunks/{6QY5BV5B.js → CGCJN73D.js} +1 -1
- package/dist/analysis-core/chunks/CWYVUGG4.js +1 -0
- package/dist/analysis-core/chunks/{3JWEFCTJ.js → FBTOOUPL.js} +1 -1
- package/dist/analysis-core/chunks/GOQVKWP3.js +32 -0
- package/dist/analysis-core/chunks/IHAVDX4L.js +66 -0
- package/dist/analysis-core/chunks/IVULXWQN.js +21 -0
- package/dist/analysis-core/chunks/K5FUABWW.js +1 -0
- package/dist/analysis-core/chunks/KGQYR62R.js +7 -0
- package/dist/analysis-core/chunks/PY5M22CL.js +1 -0
- package/dist/analysis-core/chunks/{YDBO7P4X.js → RNTMH4MU.js} +1 -1
- package/dist/analysis-core/chunks/RUVMFAST.js +1 -0
- package/dist/analysis-core/chunks/S27VIQMZ.js +1 -0
- package/dist/analysis-core/chunks/{5DGPLXYB.js → SLJ6CW7W.js} +1 -1
- package/dist/analysis-core/chunks/{STI4JE7L.js → UEORNVQB.js} +1 -1
- package/dist/analysis-core/chunks/{X7WZAA7S.js → WYLIV2P2.js} +1 -1
- package/dist/analysis-core/contracts/analysis-selection.js +1 -0
- package/dist/analysis-core/contracts/module-resolution.js +1 -0
- package/dist/analysis-core/contracts/relay-analysis.js +1 -1
- package/dist/analysis-core/contracts/requests.js +1 -1
- package/dist/analysis-core/contracts/selection-paths.js +1 -0
- package/dist/analysis-core/conventions/analysis-fit.js +1 -1
- package/dist/analysis-core/conventions/file-admission.js +1 -1
- package/dist/analysis-core/import/calls/modules.js +1 -0
- package/dist/analysis-core/import/outside-loader-sources.js +1 -1
- package/dist/analysis-core/index.js +3 -3
- package/dist/analysis-core/requests/local-plan.js +1 -1
- package/dist/analysis-core/requests/registry.js +1 -1
- package/dist/analysis-core/requests/template-version.js +1 -1
- package/dist/analysis-core/requests/tools.js +1 -1
- package/dist/analysis-core/settings.js +1 -1
- package/dist/analysis-core/stages/engine-entry.js +1 -1
- package/dist/cli/analysis-selection.js +91 -0
- package/dist/cli/commands/analyze.js +31 -6
- package/dist/cli/completion-delivery.js +63 -0
- package/dist/cli/completion-recovery.js +68 -14
- package/dist/cli/context.js +2 -1
- package/dist/cli/main.js +21 -9
- package/dist/cli/mcp.js +70 -11
- package/dist/cli/prior-completion.js +35 -0
- package/dist/cli/relay-check.js +163 -97
- package/dist/cli/relay-failure.js +32 -0
- package/dist/cli/relay-preparation.js +24 -11
- package/dist/cli/results-upload.js +131 -0
- package/dist/cli/scope-choices.js +57 -0
- package/dist/contract/cli-refusals.js +8 -0
- package/dist/contract/cli-version.js +1 -1
- package/dist/contract/console.js +7 -2
- package/dist/contract/scope-choice.js +78 -0
- package/dist/contract/selection-metadata.js +9 -0
- package/dist/contract/selection-offer.js +43 -0
- package/dist/gatekeeper/console.js +36 -25
- package/package.json +2 -2
- package/dist/analysis-core/chunks/377MLK4M.js +0 -21
- package/dist/analysis-core/chunks/DLPVYWHZ.js +0 -1
- package/dist/analysis-core/chunks/FOKDE64Y.js +0 -1
- package/dist/analysis-core/chunks/KTNA2KTN.js +0 -7
- package/dist/analysis-core/chunks/NIOS7U6C.js +0 -41
- package/dist/analysis-core/chunks/QFY6A3MS.js +0 -66
- package/dist/analysis-core/chunks/SVMEWGD3.js +0 -1
- package/dist/analysis-core/chunks/ZTOVBAG7.js +0 -1
package/dist/cli/main.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
4
|
import { errorCode, errorText } from "./api.js";
|
|
5
|
-
import { readEmailSignIn } from "../adapters/settings.js";
|
|
5
|
+
import { readEmailSignIn, readSelectionSettings, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
6
6
|
import { commandContext } from "./context.js";
|
|
7
7
|
import { analyze, parseAnalyzeOptions } from "./commands/analyze.js";
|
|
8
8
|
import { finding } from "./commands/finding.js";
|
|
@@ -10,26 +10,38 @@ import { balance } from "./commands/balance.js";
|
|
|
10
10
|
import { login, parseLoginOptions } from "./commands/login.js";
|
|
11
11
|
import { logout } from "./commands/logout.js";
|
|
12
12
|
import { serveMcp } from "./mcp.js";
|
|
13
|
+
import { loadAnalysisScope } from "./scope-choices.js";
|
|
14
|
+
import { FILE_SCOPE_PUBLIC_USAGE } from "../contract/scope-choice.js";
|
|
13
15
|
const usage = "Usage: counterfact-labs analyze [path] [--proposal <json-file>]\n counterfact-labs finding <id>\n counterfact-labs balance | login | logout | mcp\n";
|
|
14
16
|
export async function run(args, options = {}) {
|
|
15
17
|
const write = options.write ?? (text => { process.stdout.write(text); });
|
|
16
18
|
const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
|
|
17
|
-
const commandOptions = { ...options, emailSignIn };
|
|
18
19
|
try {
|
|
19
20
|
const [command, ...rest] = args;
|
|
21
|
+
const env = options.startup?.settings ?? options.env ?? process.env;
|
|
22
|
+
// Help follows the CLI defaults without validating unrelated commands' analysis switches.
|
|
23
|
+
const selectionSettings = readSelectionSettings(env, { validate: command === "analyze" || command === "mcp" });
|
|
24
|
+
const commandOptions = { ...options, emailSignIn, env: { ...(options.env ?? process.env), ...selectionSettings,
|
|
25
|
+
...(command === "analyze" || command === "mcp" ? {} : {
|
|
26
|
+
CF_FILE_SCOPE: undefined, CF_SCOPE_CHOICES: undefined, CF_LOCAL_IMPORT_RESOLUTION: undefined,
|
|
27
|
+
}) } };
|
|
28
|
+
const choices = scopeChoicesEnabled(selectionSettings);
|
|
29
|
+
const commandUsage = choices ? FILE_SCOPE_PUBLIC_USAGE : usage;
|
|
20
30
|
if (["--help", "-h", "help"].includes(command) || !command) {
|
|
21
|
-
write(
|
|
31
|
+
write(commandUsage);
|
|
22
32
|
return 0;
|
|
23
33
|
}
|
|
24
34
|
if (command === "analyze") {
|
|
25
35
|
let parsed;
|
|
26
36
|
try {
|
|
27
|
-
parsed = parseAnalyzeOptions(rest);
|
|
37
|
+
parsed = parseAnalyzeOptions(rest, choices);
|
|
28
38
|
}
|
|
29
39
|
catch {
|
|
30
|
-
throw new Error(
|
|
40
|
+
throw new Error(commandUsage.trimEnd());
|
|
31
41
|
}
|
|
32
|
-
|
|
42
|
+
const ctx = commandContext(commandOptions);
|
|
43
|
+
const scope = await loadAnalysisScope(parsed.options, ctx.cwd ?? process.cwd(), ctx.startup.settings);
|
|
44
|
+
return await analyze(parsed.path, { ...parsed.options, scope }, ctx);
|
|
33
45
|
}
|
|
34
46
|
if (command === "finding" && rest.length === 1 && !rest[0].startsWith("-"))
|
|
35
47
|
return await finding(rest[0], commandContext(commandOptions));
|
|
@@ -39,12 +51,12 @@ export async function run(args, options = {}) {
|
|
|
39
51
|
parsed = parseLoginOptions(rest);
|
|
40
52
|
}
|
|
41
53
|
catch {
|
|
42
|
-
throw new Error(
|
|
54
|
+
throw new Error(commandUsage.trimEnd());
|
|
43
55
|
}
|
|
44
56
|
return await login(commandContext(commandOptions), parsed);
|
|
45
57
|
}
|
|
46
58
|
if (rest.length)
|
|
47
|
-
throw new Error(
|
|
59
|
+
throw new Error(commandUsage.trimEnd());
|
|
48
60
|
if (command === "balance")
|
|
49
61
|
return await balance(commandContext(commandOptions));
|
|
50
62
|
if (command === "login")
|
|
@@ -55,7 +67,7 @@ export async function run(args, options = {}) {
|
|
|
55
67
|
await serveMcp(process.stdin, process.stdout, options);
|
|
56
68
|
return 0;
|
|
57
69
|
}
|
|
58
|
-
throw new Error(
|
|
70
|
+
throw new Error(commandUsage.trimEnd());
|
|
59
71
|
}
|
|
60
72
|
catch (error) {
|
|
61
73
|
write(errorText(error) + "\n");
|
package/dist/cli/mcp.js
CHANGED
|
@@ -21,6 +21,34 @@ import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY
|
|
|
21
21
|
import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
|
|
22
22
|
import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
|
|
23
23
|
import { readFindingSettings } from "./finding-settings.js";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import { readCliSettings, readSelectionSettings, selectionModeEnabled, scopeChoicesEnabled } from "../adapters/settings.js";
|
|
26
|
+
import { resolveAnalysisSelection } from "./analysis-selection.js";
|
|
27
|
+
import { analysisScopeSchema, chooseScopeMcpResultSchema } from "../contract/scope-choice.js";
|
|
28
|
+
import { scopeChoiceRefusal } from "../contract/selection-offer.js";
|
|
29
|
+
import { loadAnalysisScope, scopeChoiceText } from "./scope-choices.js";
|
|
30
|
+
const fileAnalysisDescription = "Analyze an agent folder or one agent file on this computer. For a file, Counterfact reads that file and the project files it imports inside the launch folder. Each call that runs an analysis charges the user's balance unless a free analysis is available, so call it only when the user asks for an analysis. A free first analysis needs no top-up. After edits, suggest an analysis and wait for the user to ask.";
|
|
31
|
+
const scopeChoiceDescription = "If Counterfact returns scope choices, show what each includes and ask the user to choose before calling again with that scope, unless the user already chose it. Do not run all choices automatically.";
|
|
32
|
+
const fileAnalysisEnding = "Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.";
|
|
33
|
+
const filePathDescription = "The agent folder or agent file on this computer, inside the launch folder.";
|
|
34
|
+
const fileScopeDescription = "Optional: a scope returned by Counterfact, using the same snapshot and selected file. Include all imports, no imports, or the listed imports chosen by the user.";
|
|
35
|
+
const fileProposalDescription = "Optional: the files you believe make up the agent, with each file's role. For a folder, paths are relative to that folder. For a file, paths are relative to the launch folder and must belong to the selected file's import scope.";
|
|
36
|
+
const fileProposalPathDescription = "Paths are relative to the selected folder, or to the launch folder when selecting a file.";
|
|
37
|
+
function fileAnalysisTool(choices) {
|
|
38
|
+
const proposal = structuredClone(agentProposalInputSchema);
|
|
39
|
+
if (typeof proposal.properties?.files === "object" && proposal.properties.files && "items" in proposal.properties.files) {
|
|
40
|
+
const files = proposal.properties.files.items;
|
|
41
|
+
if (files && !Array.isArray(files) && typeof files === "object" && typeof files.properties?.path === "object")
|
|
42
|
+
files.properties.path.description = fileProposalPathDescription;
|
|
43
|
+
}
|
|
44
|
+
const scope = z.toJSONSchema(analysisScopeSchema);
|
|
45
|
+
delete scope.$schema;
|
|
46
|
+
return { name: ANALYZE_TOOL_NAME,
|
|
47
|
+
description: [fileAnalysisDescription, ...(choices ? [scopeChoiceDescription] : []), fileAnalysisEnding].join(" "),
|
|
48
|
+
inputSchema: { type: "object", properties: { path: { type: "string", description: filePathDescription },
|
|
49
|
+
proposal: { ...proposal, description: fileProposalDescription }, ...(choices ? { scope: { ...scope, description: fileScopeDescription } } : {}) },
|
|
50
|
+
required: ["path"], additionalProperties: false } };
|
|
51
|
+
}
|
|
24
52
|
const legacyTools = [
|
|
25
53
|
{
|
|
26
54
|
name: ANALYZE_TOOL_NAME,
|
|
@@ -48,9 +76,15 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
48
76
|
const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
|
|
49
77
|
const lastAnalysis = new Map();
|
|
50
78
|
const findingSettings = readFindingSettings(options.env);
|
|
79
|
+
const analysisSwitches = readSelectionSettings(options.startup?.settings ?? options.env ?? process.env);
|
|
80
|
+
const choicesEnabled = scopeChoicesEnabled(analysisSwitches);
|
|
81
|
+
const selectionEnabled = selectionModeEnabled(analysisSwitches);
|
|
82
|
+
const analysisEnv = selectionEnabled ? { ...(options.env ?? process.env), ...analysisSwitches } : undefined;
|
|
83
|
+
let analysisStartup = options.startup;
|
|
51
84
|
const { numbersEnabled, fixesEnabled } = findingSettings;
|
|
52
85
|
const references = new FindingReferences();
|
|
53
|
-
const
|
|
86
|
+
const emailEnv = analysisEnv && Object.hasOwn(analysisEnv, "CF_EMAIL_SIGN_IN") ? analysisEnv : options.env;
|
|
87
|
+
const emailSignIn = options.emailSignIn ?? readEmailSignIn(emailEnv);
|
|
54
88
|
const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
|
|
55
89
|
{
|
|
56
90
|
name: "sign_in_status",
|
|
@@ -76,7 +110,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
76
110
|
}, required: ["email", "code"], additionalProperties: false },
|
|
77
111
|
},
|
|
78
112
|
] : legacyTools;
|
|
79
|
-
const tools = signInTools.map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
113
|
+
const tools = signInTools.map(tool => tool.name === ANALYZE_TOOL_NAME && selectionEnabled ? fileAnalysisTool(choicesEnabled) : tool).map(tool => tool.name === "get_finding" && numbersEnabled ? {
|
|
80
114
|
...tool, inputSchema: numberedFindingInputSchema,
|
|
81
115
|
} : tool).concat(fixesEnabled ? fixRequestTools : []);
|
|
82
116
|
let pendingSignIn;
|
|
@@ -91,7 +125,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
91
125
|
if (!object(args))
|
|
92
126
|
throw new ProtocolError(-32602, "Tool arguments must be an object.");
|
|
93
127
|
const name = params.name;
|
|
94
|
-
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
128
|
+
const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal", ...(choicesEnabled ? ["scope"] : [])] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
|
|
95
129
|
: name === "claim_fix" ? ["id"]
|
|
96
130
|
: name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
|
|
97
131
|
: emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
|
|
@@ -117,6 +151,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
117
151
|
let reason;
|
|
118
152
|
let freeAnalysisAvailable = false;
|
|
119
153
|
let agentChoice;
|
|
154
|
+
let scopeChoice;
|
|
120
155
|
let findingIds;
|
|
121
156
|
let findings, projectId;
|
|
122
157
|
let loadedFinding;
|
|
@@ -126,13 +161,22 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
126
161
|
let started = false;
|
|
127
162
|
let signIn;
|
|
128
163
|
try {
|
|
129
|
-
|
|
164
|
+
// Listing tools must not validate settings used only when a tool runs.
|
|
165
|
+
if (selectionEnabled)
|
|
166
|
+
analysisStartup ??= readCliSettings(analysisEnv, emailSignIn);
|
|
167
|
+
const analysisSettings = analysisStartup?.settings ?? {};
|
|
168
|
+
const ctx = commandContext({ ...options, ...(analysisStartup ? { startup: analysisStartup } : {}), env: { ...(analysisEnv ?? options.env ?? process.env), ...analysisSwitches, CF_FINDING_AUDIENCE: audienceChoice },
|
|
130
169
|
onCheckFailure: failure => { reason = failure.reason; options.onCheckFailure?.(failure); },
|
|
131
170
|
emailSignIn, findingSettings, signal, onCheckStart: () => {
|
|
132
171
|
started = true;
|
|
133
172
|
if (guarded && analysisFolder)
|
|
134
173
|
lastAnalysis.set(analysisFolder, ctx.now());
|
|
135
174
|
options.onCheckStart?.();
|
|
175
|
+
}, onScopeChoice: choice => {
|
|
176
|
+
scopeChoice = choice;
|
|
177
|
+
if (guarded && analysisFolder)
|
|
178
|
+
lastAnalysis.delete(analysisFolder);
|
|
179
|
+
options.onScopeChoice?.(choice);
|
|
136
180
|
}, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
|
|
137
181
|
findingIds = result.findingIds ?? [];
|
|
138
182
|
if (numbersEnabled) {
|
|
@@ -244,7 +288,7 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
244
288
|
signal.addEventListener("abort", cancel, { once: true });
|
|
245
289
|
try {
|
|
246
290
|
signal.throwIfAborted();
|
|
247
|
-
const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
291
|
+
const flow = await startSignIn(commandContext({ ...options, startup: ctx.startup, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
|
|
248
292
|
pending = { flow, controller, opened: false };
|
|
249
293
|
pendingSignIn = pending;
|
|
250
294
|
signInFailure = undefined;
|
|
@@ -272,18 +316,25 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
272
316
|
const launchFolder = await launch;
|
|
273
317
|
if (guarded && !launchFolder)
|
|
274
318
|
throw new CliRefusalError("launch_folder_unavailable");
|
|
275
|
-
const
|
|
276
|
-
const
|
|
319
|
+
const scope = await loadAnalysisScope({ scope: args.scope }, cwd, analysisSettings);
|
|
320
|
+
const selection = selectionEnabled ? await resolveAnalysisSelection(args.path, launchFolder ?? cwd, analysisSettings, scope) : undefined;
|
|
321
|
+
if (guarded && selection?.kind === "folder")
|
|
322
|
+
await resolveMcpAgentFolder(selection.path, launchFolder, ctx.homeDir);
|
|
323
|
+
const folder = selection?.path ?? (guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd));
|
|
324
|
+
const key = selection ? JSON.stringify([selection.launchRoot, selection.path]) : folder;
|
|
325
|
+
const last = lastAnalysis.get(key);
|
|
277
326
|
if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
|
|
278
327
|
throw new CliRefusalError("repeat_too_soon");
|
|
279
328
|
}
|
|
280
|
-
analysisFolder =
|
|
329
|
+
analysisFolder = key;
|
|
281
330
|
try {
|
|
282
|
-
exitCode = await analyze(folder, { proposal: args.proposal }, ctx);
|
|
331
|
+
exitCode = await analyze(folder, { proposal: args.proposal, scope, selection }, ctx);
|
|
283
332
|
}
|
|
284
333
|
finally {
|
|
285
|
-
if (
|
|
286
|
-
lastAnalysis.
|
|
334
|
+
if (reason === "results_pending")
|
|
335
|
+
lastAnalysis.delete(key);
|
|
336
|
+
else if (guarded && started && !scopeChoice)
|
|
337
|
+
lastAnalysis.set(key, ctx.now());
|
|
287
338
|
}
|
|
288
339
|
}
|
|
289
340
|
}
|
|
@@ -293,6 +344,14 @@ export async function serveMcp(input, output, options = {}) {
|
|
|
293
344
|
reason = failure.reason;
|
|
294
345
|
exitCode = errorCode(error);
|
|
295
346
|
}
|
|
347
|
+
if (scopeChoice) {
|
|
348
|
+
const refusal = scopeChoiceRefusal(scopeChoice);
|
|
349
|
+
if (!refusal)
|
|
350
|
+
return chooseScopeMcpResultSchema.parse({ content: [{ type: "text", text: scopeChoiceText(scopeChoice) }], isError: false,
|
|
351
|
+
structuredContent: { ...scopeChoice, exitCode: 0 } });
|
|
352
|
+
text = refusal + "\n";
|
|
353
|
+
exitCode = 1;
|
|
354
|
+
}
|
|
296
355
|
// Some clients (Claude Code among them) give the model only the structured result when one is present, so it
|
|
297
356
|
// repeats the text: without it the model cannot read the result.
|
|
298
357
|
return { content: [{ type: "text", text }], isError: exitCode !== 0,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { discardCompletion, loadCompletions, matchesCompletionSetup } from "../adapters/completion.js";
|
|
2
|
+
import { completionRecoveryDecision } from "./completion-recovery.js";
|
|
3
|
+
export class CompletionSettingsMismatchError extends Error {
|
|
4
|
+
reason = "completion_settings_mismatch";
|
|
5
|
+
}
|
|
6
|
+
/** Leave other setups alone before the runner assigns any reservation or recovery state. */
|
|
7
|
+
export async function priorCompletion(folder, options) {
|
|
8
|
+
const records = await loadCompletions(folder, { retainUnsaved: options.retryEnabled,
|
|
9
|
+
consoleOrigin: options.consoleOrigin, projectId: options.projectId, identity: options.identity });
|
|
10
|
+
const notices = new Set();
|
|
11
|
+
let prior;
|
|
12
|
+
for (const item of records) {
|
|
13
|
+
const { record } = item;
|
|
14
|
+
if ((record.uploaded || record.finished) && !options.retryUpload)
|
|
15
|
+
continue;
|
|
16
|
+
const localMismatch = !matchesCompletionSetup(record, options);
|
|
17
|
+
const decision = completionRecoveryDecision({ retryEnabled: options.retryEnabled, payload: record.payload,
|
|
18
|
+
localMismatch, timing: { completedAt: record.completedAt, absoluteExpiresAt: record.absoluteExpiresAt },
|
|
19
|
+
now: (options.now ?? Date.now)(), statusLookup: { kind: "skipped" }, uploadOutcome: { kind: record.uploaded ? "saved" : "pending" } });
|
|
20
|
+
if (decision.action === "leave") {
|
|
21
|
+
notices.add(decision.message);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (localMismatch && decision.action === "give-up") {
|
|
25
|
+
await discardCompletion(folder, item.path, options.identity);
|
|
26
|
+
if (decision.message)
|
|
27
|
+
notices.add(decision.message);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (localMismatch)
|
|
31
|
+
throw new CompletionSettingsMismatchError("message" in decision ? decision.message : undefined);
|
|
32
|
+
prior ??= item;
|
|
33
|
+
}
|
|
34
|
+
return { prior, notices: [...notices] };
|
|
35
|
+
}
|