tinker-agent 1.3.0 → 1.5.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 +39 -1
- package/README.md +271 -72
- package/bin/tinker.js +75 -25
- package/package.json +12 -3
- package/src/agent/runtime-session.ts +113 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +158 -262
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +226 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +732 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +108 -0
- package/src/cli/tui-memory.ts +67 -0
- package/src/cli/tui-runner.tsx +79 -49
- package/src/context/context-policy.ts +2 -2
- package/src/events/stdout-event-printer.ts +1 -0
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/memory/contracts.ts +148 -0
- package/src/memory/embedding-client.ts +105 -0
- package/src/memory/memory-coordinator.ts +556 -0
- package/src/memory/memory-extractor.ts +231 -0
- package/src/memory/memory-log.ts +88 -0
- package/src/memory/memory-search-tool.ts +100 -0
- package/src/memory/memory-store.ts +687 -0
- package/src/memory/vector.ts +153 -0
- package/src/model/fake-model-client.ts +971 -3
- package/src/model/model-context-profile.ts +0 -30
- package/src/observation/observation-builder.ts +20 -0
- package/src/session/session-store.ts +123 -0
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +19 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/types.ts +16 -0
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +72 -2
- package/src/tui/clipboard.ts +22 -0
- package/src/tui/components/footer.tsx +9 -4
- package/src/tui/components/memory-browser.tsx +151 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/event-store.ts +9 -2
- package/src/tui/slash-commands.ts +88 -24
- package/src/tui/workspace-file-search.ts +78 -71
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
runtimeIdFactory,
|
|
14
14
|
type RuntimeIdFactory,
|
|
15
15
|
type SessionId,
|
|
16
|
+
type TurnId,
|
|
16
17
|
} from "../ids/runtime-id";
|
|
17
18
|
import { loadMcpConfig } from "../mcp/mcp-config";
|
|
18
19
|
import {
|
|
@@ -71,11 +72,13 @@ import {
|
|
|
71
72
|
renderedMessageHash,
|
|
72
73
|
} from "../context/compiled-context-hash";
|
|
73
74
|
import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
|
|
75
|
+
import type { ToolExecutor } from "../tools/types";
|
|
74
76
|
import type { Refiner } from "../tools/web-fetch/refiner";
|
|
75
77
|
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
76
78
|
import {
|
|
77
79
|
SessionStore,
|
|
78
80
|
createSessionCompatibilityContract,
|
|
81
|
+
type CompletedTurnSnapshot,
|
|
79
82
|
type SessionRecoveryResult,
|
|
80
83
|
type StoredSkillActivation,
|
|
81
84
|
} from "../session/session-store";
|
|
@@ -92,6 +95,7 @@ import { TurnCancelledError } from "./turn-cancellation";
|
|
|
92
95
|
import type { ToolCompletionInput } from "../context/protocol-frame";
|
|
93
96
|
import type { BuiltContextRequest } from "../context/context-revision";
|
|
94
97
|
import type { CommittedToolCompletion } from "./session-ledger";
|
|
98
|
+
import type { PublicToolingConfig } from "../cli/public-config-contract";
|
|
95
99
|
import type {
|
|
96
100
|
IterationIdentity,
|
|
97
101
|
RunAgentResult,
|
|
@@ -211,6 +215,25 @@ export type SkillsUpdateSummary = {
|
|
|
211
215
|
readonly addedOverrideCount: number;
|
|
212
216
|
};
|
|
213
217
|
|
|
218
|
+
export type CompletedTurnHookInput = {
|
|
219
|
+
readonly workspaceRoot: string;
|
|
220
|
+
readonly sessionId: SessionId;
|
|
221
|
+
readonly turnId: TurnId;
|
|
222
|
+
readonly snapshot: CompletedTurnSnapshot;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export type CompletedTurnHookFailure = {
|
|
226
|
+
readonly workspaceRoot: string;
|
|
227
|
+
readonly sessionId: SessionId;
|
|
228
|
+
readonly turnId: TurnId;
|
|
229
|
+
readonly reason: "completed_turn_snapshot_failed" | "completed_turn_enqueue_failed";
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export type CompletedTurnHook = {
|
|
233
|
+
enqueue(input: CompletedTurnHookInput): void;
|
|
234
|
+
recordFailure(input: CompletedTurnHookFailure): void;
|
|
235
|
+
};
|
|
236
|
+
|
|
214
237
|
type CommonRuntimeSessionInput = {
|
|
215
238
|
workspaceRoot: string;
|
|
216
239
|
modelName: string;
|
|
@@ -231,6 +254,9 @@ type CommonRuntimeSessionInput = {
|
|
|
231
254
|
observationLogPath?: string;
|
|
232
255
|
};
|
|
233
256
|
webFetchRefiner?: Refiner;
|
|
257
|
+
toolingConfig?: PublicToolingConfig;
|
|
258
|
+
memorySearch?: ToolExecutor;
|
|
259
|
+
completedTurnHook?: CompletedTurnHook;
|
|
234
260
|
};
|
|
235
261
|
|
|
236
262
|
type CreateNewRuntimeSessionInput = CommonRuntimeSessionInput & {
|
|
@@ -531,6 +557,10 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
531
557
|
runtimeSession: session.context,
|
|
532
558
|
historyReader: store.historyReader(),
|
|
533
559
|
webFetchRefiner: input.webFetchRefiner,
|
|
560
|
+
toolingConfig: input.toolingConfig,
|
|
561
|
+
...(input.memorySearch === undefined
|
|
562
|
+
? {}
|
|
563
|
+
: { memorySearch: input.memorySearch }),
|
|
534
564
|
...(session.skillCatalog.skills.size === 0
|
|
535
565
|
? {}
|
|
536
566
|
: {
|
|
@@ -544,6 +574,8 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
544
574
|
session.mcpManager = await dependencies.createMcpManager({
|
|
545
575
|
config: mcpConfig,
|
|
546
576
|
runtimeSession: session.context,
|
|
577
|
+
timeoutMs: input.toolingConfig?.mcpTimeoutMs,
|
|
578
|
+
maxObservationChars: input.toolingConfig?.mcpMaxObservationChars,
|
|
547
579
|
});
|
|
548
580
|
for (const executor of session.mcpManager.executors) {
|
|
549
581
|
session.tooling.registry.register(executor, "MCP");
|
|
@@ -870,7 +902,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
870
902
|
return this.input.modelClient.inputModalities?.includes("image") === true;
|
|
871
903
|
}
|
|
872
904
|
|
|
873
|
-
importImage(
|
|
905
|
+
async importImage(
|
|
874
906
|
sourcePath: string,
|
|
875
907
|
signal: AbortSignal,
|
|
876
908
|
prospectiveMessageImageCount: number,
|
|
@@ -878,9 +910,6 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
878
910
|
if (this.state !== "ready") {
|
|
879
911
|
throw new Error(`Cannot import an image while RuntimeSession is ${this.state}.`);
|
|
880
912
|
}
|
|
881
|
-
if (!this.supportsImageInput()) {
|
|
882
|
-
throw new Error("Current model profile does not support image input.");
|
|
883
|
-
}
|
|
884
913
|
if (
|
|
885
914
|
!Number.isSafeInteger(prospectiveMessageImageCount) ||
|
|
886
915
|
prospectiveMessageImageCount < 1 ||
|
|
@@ -888,18 +917,35 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
888
917
|
) {
|
|
889
918
|
throw new Error("Prospective Prompt image count is invalid.");
|
|
890
919
|
}
|
|
891
|
-
const
|
|
892
|
-
this.
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
920
|
+
const assertImageAllowed = () => {
|
|
921
|
+
if (this.state !== "ready") {
|
|
922
|
+
throw new Error(
|
|
923
|
+
`Cannot import an image while RuntimeSession is ${this.state}.`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (!this.supportsImageInput()) {
|
|
927
|
+
throw new Error("Current model profile does not support image input.");
|
|
928
|
+
}
|
|
929
|
+
const activeImageCount = this.input.modelClient.prepare(
|
|
930
|
+
this.requireLedger().buildCommittedModelRequest(
|
|
931
|
+
this.requireTooling().registry.definitions(),
|
|
932
|
+
).request,
|
|
933
|
+
).mediaOccurrenceCount;
|
|
934
|
+
const aggregateImageCount = activeImageCount + prospectiveMessageImageCount;
|
|
935
|
+
if (aggregateImageCount > IMAGE_INPUT_POLICY.maxImagesPerRequest) {
|
|
936
|
+
throw new ModelRequestMediaAggregateError(
|
|
937
|
+
`Model request would have ${aggregateImageCount} images; maximum is ${IMAGE_INPUT_POLICY.maxImagesPerRequest}.`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
if (this.supportsImageInput()) {
|
|
943
|
+
assertImageAllowed();
|
|
901
944
|
}
|
|
902
|
-
return this.assetStore.importWorkspaceFile(sourcePath, {
|
|
945
|
+
return this.assetStore.importWorkspaceFile(sourcePath, {
|
|
946
|
+
signal,
|
|
947
|
+
accept: assertImageAllowed,
|
|
948
|
+
});
|
|
903
949
|
}
|
|
904
950
|
|
|
905
951
|
async verifyImageAssets(
|
|
@@ -1698,6 +1744,9 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1698
1744
|
await this.appendTerminalEvent(turn, result, projectedMessageCount);
|
|
1699
1745
|
pendingLedgerTurn.finish(result);
|
|
1700
1746
|
settled = true;
|
|
1747
|
+
if (result.status === "completed") {
|
|
1748
|
+
this.notifyCompletedTurn(turn);
|
|
1749
|
+
}
|
|
1701
1750
|
await this.settleClosedTurnSkills();
|
|
1702
1751
|
if (result.status === "completed") {
|
|
1703
1752
|
await this.performAutomaticContextMaintenance();
|
|
@@ -1745,6 +1794,55 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1745
1794
|
}
|
|
1746
1795
|
}
|
|
1747
1796
|
|
|
1797
|
+
private notifyCompletedTurn(turn: TurnIdentity): void {
|
|
1798
|
+
const hook = this.input.completedTurnHook;
|
|
1799
|
+
if (hook === undefined) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
let snapshot: CompletedTurnSnapshot;
|
|
1803
|
+
try {
|
|
1804
|
+
snapshot = this.store.readCompletedTurnSnapshot(turn.turnId);
|
|
1805
|
+
} catch {
|
|
1806
|
+
this.recordCompletedTurnHookFailure(
|
|
1807
|
+
hook,
|
|
1808
|
+
turn.turnId,
|
|
1809
|
+
"completed_turn_snapshot_failed",
|
|
1810
|
+
);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
try {
|
|
1814
|
+
hook.enqueue({
|
|
1815
|
+
workspaceRoot: this.input.workspaceRoot,
|
|
1816
|
+
sessionId: this.sessionId,
|
|
1817
|
+
turnId: turn.turnId,
|
|
1818
|
+
snapshot,
|
|
1819
|
+
});
|
|
1820
|
+
} catch {
|
|
1821
|
+
this.recordCompletedTurnHookFailure(
|
|
1822
|
+
hook,
|
|
1823
|
+
turn.turnId,
|
|
1824
|
+
"completed_turn_enqueue_failed",
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
private recordCompletedTurnHookFailure(
|
|
1830
|
+
hook: CompletedTurnHook,
|
|
1831
|
+
turnId: TurnId,
|
|
1832
|
+
reason: CompletedTurnHookFailure["reason"],
|
|
1833
|
+
): void {
|
|
1834
|
+
try {
|
|
1835
|
+
hook.recordFailure({
|
|
1836
|
+
workspaceRoot: this.input.workspaceRoot,
|
|
1837
|
+
sessionId: this.sessionId,
|
|
1838
|
+
turnId,
|
|
1839
|
+
reason,
|
|
1840
|
+
});
|
|
1841
|
+
} catch {
|
|
1842
|
+
// Optional completed-turn integrations never fault a committed turn.
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1748
1846
|
private async performAutomaticCompaction(
|
|
1749
1847
|
qualificationId: string,
|
|
1750
1848
|
): Promise<ContextCompactionResult | undefined> {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { PUBLIC_CLI_CONTRACT } from "./public-cli-contract";
|
|
3
|
+
import type { PromptSource } from "./prompt-source";
|
|
4
|
+
import { CliUsageError, type CliCommandScope } from "./output";
|
|
5
|
+
|
|
6
|
+
export type CliCommand =
|
|
7
|
+
| { readonly type: "tui"; readonly profileName?: string }
|
|
8
|
+
| {
|
|
9
|
+
readonly type: "run";
|
|
10
|
+
readonly profileName?: string;
|
|
11
|
+
readonly promptSource: PromptSource;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type CommandLineResult =
|
|
15
|
+
| { readonly type: "command"; readonly command: CliCommand }
|
|
16
|
+
| {
|
|
17
|
+
readonly type: "terminal";
|
|
18
|
+
readonly stdout: string;
|
|
19
|
+
readonly stderr: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SUCCESSFUL_TERMINAL_CODES = new Set([
|
|
23
|
+
"commander.help",
|
|
24
|
+
"commander.helpDisplayed",
|
|
25
|
+
"commander.version",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export async function parseCommandLine(
|
|
29
|
+
args: readonly string[],
|
|
30
|
+
packageVersion: string,
|
|
31
|
+
): Promise<CommandLineResult> {
|
|
32
|
+
const scopeHint = preflightArgv(args);
|
|
33
|
+
let stdout = "";
|
|
34
|
+
let stderr = "";
|
|
35
|
+
let selectedCommand: CliCommand | undefined;
|
|
36
|
+
const contract = PUBLIC_CLI_CONTRACT;
|
|
37
|
+
|
|
38
|
+
const program = new Command()
|
|
39
|
+
.name(contract.name)
|
|
40
|
+
.description(contract.description)
|
|
41
|
+
.helpOption(contract.helpFlags)
|
|
42
|
+
.version(packageVersion, contract.versionFlags)
|
|
43
|
+
.option(contract.tui.profileOption.flags, contract.tui.profileOption.description)
|
|
44
|
+
.helpCommand(contract.helpCommand.command, contract.helpCommand.description)
|
|
45
|
+
.showHelpAfterError('Run "tinker --help" for usage.')
|
|
46
|
+
.showSuggestionAfterError(false)
|
|
47
|
+
.allowExcessArguments(false)
|
|
48
|
+
.enablePositionalOptions()
|
|
49
|
+
.exitOverride()
|
|
50
|
+
.configureOutput({
|
|
51
|
+
writeOut: (value) => {
|
|
52
|
+
stdout += value;
|
|
53
|
+
},
|
|
54
|
+
writeErr: (value) => {
|
|
55
|
+
stderr += value;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program.action(() => {
|
|
60
|
+
const { profile } = program.opts<{ profile?: string }>();
|
|
61
|
+
selectedCommand = Object.freeze({
|
|
62
|
+
type: "tui",
|
|
63
|
+
...(profile === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { profileName: validateProfile(profile, "root") }),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command(contract.run.command)
|
|
71
|
+
.description(contract.run.description)
|
|
72
|
+
.option(contract.run.profileOption.flags, contract.run.profileOption.description)
|
|
73
|
+
.option(contract.run.stdinOption.flags, contract.run.stdinOption.description)
|
|
74
|
+
.option(contract.run.fileOption.flags, contract.run.fileOption.description)
|
|
75
|
+
.addHelpText("after", `\n${contract.run.helpAfter}\n`)
|
|
76
|
+
.showHelpAfterError('Run "tinker run --help" for usage.')
|
|
77
|
+
.showSuggestionAfterError(false)
|
|
78
|
+
.allowExcessArguments(false)
|
|
79
|
+
.exitOverride()
|
|
80
|
+
.action(
|
|
81
|
+
(
|
|
82
|
+
prompt: string | undefined,
|
|
83
|
+
options: { profile?: string; stdin?: boolean; file?: string },
|
|
84
|
+
) => {
|
|
85
|
+
const sources: PromptSource[] = [];
|
|
86
|
+
if (prompt !== undefined) {
|
|
87
|
+
sources.push({ kind: "argument", value: prompt });
|
|
88
|
+
}
|
|
89
|
+
if (options.stdin === true) {
|
|
90
|
+
sources.push({ kind: "stdin" });
|
|
91
|
+
}
|
|
92
|
+
if (options.file !== undefined) {
|
|
93
|
+
if (options.file.length === 0) {
|
|
94
|
+
throw new CliUsageError("--file requires a non-empty path.", "run");
|
|
95
|
+
}
|
|
96
|
+
sources.push({ kind: "file", filePath: options.file });
|
|
97
|
+
}
|
|
98
|
+
if (sources.length === 0) {
|
|
99
|
+
throw new CliUsageError(
|
|
100
|
+
"Exactly one prompt source is required: [prompt], --stdin, or --file <path>.",
|
|
101
|
+
"run",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (sources.length > 1) {
|
|
105
|
+
throw new CliUsageError(
|
|
106
|
+
"Prompt sources are mutually exclusive: use [prompt], --stdin, or --file <path>.",
|
|
107
|
+
"run",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const promptSource = sources[0];
|
|
111
|
+
if (promptSource === undefined) {
|
|
112
|
+
throw new CliUsageError("A prompt source is required.", "run");
|
|
113
|
+
}
|
|
114
|
+
selectedCommand = Object.freeze({
|
|
115
|
+
type: "run",
|
|
116
|
+
...(options.profile === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { profileName: validateProfile(options.profile, "run") }),
|
|
119
|
+
promptSource,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await program.parseAsync([...args], { from: "user" });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof CliUsageError) {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof CommanderError) {
|
|
131
|
+
if (SUCCESSFUL_TERMINAL_CODES.has(error.code)) {
|
|
132
|
+
return Object.freeze({ type: "terminal", stdout, stderr });
|
|
133
|
+
}
|
|
134
|
+
throw new CliUsageError(commanderErrorDetail(error), scopeHint);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (selectedCommand === undefined) {
|
|
140
|
+
throw new Error("Commander completed without selecting a command.");
|
|
141
|
+
}
|
|
142
|
+
const topLevelProfile = program.opts<{ profile?: string }>().profile;
|
|
143
|
+
if (selectedCommand.type === "run" && topLevelProfile !== undefined) {
|
|
144
|
+
throw new CliUsageError(
|
|
145
|
+
"The top-level --profile option only applies to the TUI; place --profile after run.",
|
|
146
|
+
"run",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({ type: "command", command: selectedCommand });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function preflightArgv(args: readonly string[]): CliCommandScope {
|
|
153
|
+
let scope: CliCommandScope = "root";
|
|
154
|
+
let rootBlocked = false;
|
|
155
|
+
let topProfileOccurrences = 0;
|
|
156
|
+
let runProfileOccurrences = 0;
|
|
157
|
+
let stdinOccurrences = 0;
|
|
158
|
+
let fileOccurrences = 0;
|
|
159
|
+
|
|
160
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
161
|
+
const token = args[index];
|
|
162
|
+
if (token === undefined || token === "--") {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
(!rootBlocked && scope === "root" && isRootTerminalOption(token)) ||
|
|
168
|
+
(scope === "run" && isRunTerminalOption(token))
|
|
169
|
+
) {
|
|
170
|
+
return scope;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (scope === "root" && !rootBlocked) {
|
|
174
|
+
const profile = readOptionOccurrence(
|
|
175
|
+
args,
|
|
176
|
+
index,
|
|
177
|
+
token,
|
|
178
|
+
"--profile",
|
|
179
|
+
"root",
|
|
180
|
+
"-p",
|
|
181
|
+
);
|
|
182
|
+
if (profile !== undefined) {
|
|
183
|
+
topProfileOccurrences += 1;
|
|
184
|
+
assertSingleOccurrence("--profile", topProfileOccurrences, "root");
|
|
185
|
+
validateProfile(profile.value, "root");
|
|
186
|
+
index += profile.consumedNext ? 1 : 0;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (token.startsWith("-")) {
|
|
190
|
+
rootBlocked = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (token === "run") {
|
|
194
|
+
scope = "run";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (token === "help") {
|
|
198
|
+
return "root";
|
|
199
|
+
}
|
|
200
|
+
throw new CliUsageError(`unknown command '${token}'`, "root");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (scope !== "run") {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const profile = readOptionOccurrence(args, index, token, "--profile", "run", "-p");
|
|
207
|
+
if (profile !== undefined) {
|
|
208
|
+
runProfileOccurrences += 1;
|
|
209
|
+
assertSingleOccurrence("--profile", runProfileOccurrences, "run");
|
|
210
|
+
validateProfile(profile.value, "run");
|
|
211
|
+
index += profile.consumedNext ? 1 : 0;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const file = readOptionOccurrence(args, index, token, "--file", "run");
|
|
215
|
+
if (file !== undefined) {
|
|
216
|
+
fileOccurrences += 1;
|
|
217
|
+
assertSingleOccurrence("--file", fileOccurrences, "run");
|
|
218
|
+
if (file.value.length === 0) {
|
|
219
|
+
throw new CliUsageError("--file requires a non-empty path.", "run");
|
|
220
|
+
}
|
|
221
|
+
index += file.consumedNext ? 1 : 0;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (token === "--stdin") {
|
|
225
|
+
stdinOccurrences += 1;
|
|
226
|
+
assertSingleOccurrence("--stdin", stdinOccurrences, "run");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return scope;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function readOptionOccurrence(
|
|
233
|
+
args: readonly string[],
|
|
234
|
+
index: number,
|
|
235
|
+
token: string,
|
|
236
|
+
longName: string,
|
|
237
|
+
scope: CliCommandScope,
|
|
238
|
+
shortName?: string,
|
|
239
|
+
): { readonly value: string; readonly consumedNext: boolean } | undefined {
|
|
240
|
+
if (token === longName || token === shortName) {
|
|
241
|
+
const next = args[index + 1];
|
|
242
|
+
if (next === undefined || next.startsWith("-")) {
|
|
243
|
+
throw new CliUsageError(`option '${token}' argument missing`, scope);
|
|
244
|
+
}
|
|
245
|
+
return { value: next, consumedNext: true };
|
|
246
|
+
}
|
|
247
|
+
const longPrefix = `${longName}=`;
|
|
248
|
+
if (token.startsWith(longPrefix)) {
|
|
249
|
+
return { value: token.slice(longPrefix.length), consumedNext: false };
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
shortName !== undefined &&
|
|
253
|
+
token.startsWith(shortName) &&
|
|
254
|
+
token !== shortName &&
|
|
255
|
+
!token.startsWith("--")
|
|
256
|
+
) {
|
|
257
|
+
return { value: token.slice(shortName.length), consumedNext: false };
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateProfile(value: string, scope: CliCommandScope): string {
|
|
263
|
+
if (value.trim() === "") {
|
|
264
|
+
throw new CliUsageError("--profile requires a non-empty value.", scope);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function assertSingleOccurrence(
|
|
270
|
+
option: string,
|
|
271
|
+
count: number,
|
|
272
|
+
scope: CliCommandScope,
|
|
273
|
+
): void {
|
|
274
|
+
if (count > 1) {
|
|
275
|
+
throw new CliUsageError(`${option} may only be specified once.`, scope);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isRootTerminalOption(token: string): boolean {
|
|
280
|
+
return (
|
|
281
|
+
token === "--help" || token === "-h" || token === "--version" || token === "-V"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isRunTerminalOption(token: string): boolean {
|
|
286
|
+
return token === "--help" || token === "-h";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function commanderErrorDetail(error: CommanderError): string {
|
|
290
|
+
return error.message.replace(/^error:\s*/u, "");
|
|
291
|
+
}
|