trekoon 0.1.8 → 0.1.9
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/.agents/skills/trekoon/SKILL.md +42 -0
- package/README.md +71 -12
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +82 -0
- package/src/commands/epic.ts +70 -1
- package/src/commands/help.ts +12 -1
- package/src/commands/subtask.ts +23 -1
- package/src/commands/sync.ts +130 -52
- package/src/commands/task.ts +71 -1
- package/src/index.ts +1 -1
- package/src/io/output.ts +98 -5
- package/src/runtime/cli-shell.ts +159 -22
- package/src/runtime/command-types.ts +18 -0
- package/src/storage/path.ts +58 -1
package/src/commands/sync.ts
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
|
+
import { findUnknownOption, parseArgs, readMissingOptionValue, readOption, suggestOptions } from "./arg-parser";
|
|
2
|
+
|
|
1
3
|
import { failResult, okResult } from "../io/output";
|
|
2
4
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
3
5
|
import { MissingBranchDatabaseError } from "../sync/branch-db";
|
|
4
6
|
import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
|
|
5
|
-
import { type
|
|
7
|
+
import { type SyncResolution } from "../sync/types";
|
|
8
|
+
|
|
9
|
+
const STATUS_OPTIONS = ["from"] as const;
|
|
10
|
+
const PULL_OPTIONS = ["from"] as const;
|
|
11
|
+
const RESOLVE_OPTIONS = ["use"] as const;
|
|
12
|
+
const CONFLICTS_LIST_OPTIONS = ["mode"] as const;
|
|
13
|
+
const CONFLICTS_SHOW_OPTIONS: readonly string[] = [];
|
|
14
|
+
|
|
15
|
+
function resolveSyncCommandId(subcommand: string | undefined, conflictsSubcommand: string | undefined): string {
|
|
16
|
+
if (subcommand === "status") {
|
|
17
|
+
return "sync.status";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (subcommand === "pull") {
|
|
21
|
+
return "sync.pull";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (subcommand === "resolve") {
|
|
25
|
+
return "sync.resolve";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (subcommand !== "conflicts") {
|
|
29
|
+
return "sync";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (conflictsSubcommand === "list") {
|
|
33
|
+
return "sync.conflicts.list";
|
|
34
|
+
}
|
|
6
35
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (index < 0) {
|
|
10
|
-
return null;
|
|
36
|
+
if (conflictsSubcommand === "show") {
|
|
37
|
+
return "sync.conflicts.show";
|
|
11
38
|
}
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
return value && !value.startsWith("--") ? value : null;
|
|
40
|
+
return "sync.conflicts";
|
|
15
41
|
}
|
|
16
42
|
|
|
17
|
-
function usage(message: string): CliResult {
|
|
43
|
+
function usage(message: string, command = "sync"): CliResult {
|
|
18
44
|
return failResult({
|
|
19
|
-
command
|
|
45
|
+
command,
|
|
20
46
|
human: `${message}\nUsage: trekoon sync <status|pull|resolve|conflicts> [options]`,
|
|
21
47
|
data: { message },
|
|
22
48
|
error: {
|
|
@@ -26,6 +52,28 @@ function usage(message: string): CliResult {
|
|
|
26
52
|
});
|
|
27
53
|
}
|
|
28
54
|
|
|
55
|
+
function prefixedOptions(options: readonly string[]): string[] {
|
|
56
|
+
return options.map((option) => `--${option}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
|
|
60
|
+
const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
|
|
61
|
+
const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
|
|
62
|
+
return failResult({
|
|
63
|
+
command,
|
|
64
|
+
human: `Unknown option --${option}.${suggestionMessage}`,
|
|
65
|
+
data: {
|
|
66
|
+
option: `--${option}`,
|
|
67
|
+
allowedOptions: prefixedOptions(allowedOptions),
|
|
68
|
+
suggestions,
|
|
69
|
+
},
|
|
70
|
+
error: {
|
|
71
|
+
code: "unknown_option",
|
|
72
|
+
message: `Unknown option --${option}`,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
29
77
|
function statusMessage(sourceBranch: string, ahead: number, behind: number, conflicts: number): string {
|
|
30
78
|
return [
|
|
31
79
|
`Sync status against '${sourceBranch}'`,
|
|
@@ -61,26 +109,11 @@ function formatConflictList(
|
|
|
61
109
|
.join("\n");
|
|
62
110
|
}
|
|
63
111
|
|
|
64
|
-
function parseConflictMode(args: readonly string[]): SyncConflictMode | null {
|
|
65
|
-
const modeIndex = args.indexOf("--mode");
|
|
66
|
-
if (modeIndex < 0) {
|
|
67
|
-
return "pending";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const explicitMode = args[modeIndex + 1];
|
|
71
|
-
if (!explicitMode || explicitMode.startsWith("--")) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (explicitMode === "pending" || explicitMode === "all") {
|
|
76
|
-
return explicitMode;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
112
|
export async function runSync(context: CliContext): Promise<CliResult> {
|
|
83
|
-
const
|
|
113
|
+
const parsed = parseArgs(context.args);
|
|
114
|
+
const subcommand: string | undefined = parsed.positional[0];
|
|
115
|
+
const conflictsSubcommand: string | undefined = subcommand === "conflicts" ? parsed.positional[1] : undefined;
|
|
116
|
+
const resolvedCommand: string = resolveSyncCommandId(subcommand, conflictsSubcommand);
|
|
84
117
|
|
|
85
118
|
if (!subcommand) {
|
|
86
119
|
return usage("Missing sync subcommand.");
|
|
@@ -88,26 +121,46 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
88
121
|
|
|
89
122
|
try {
|
|
90
123
|
if (subcommand === "status") {
|
|
91
|
-
const
|
|
124
|
+
const statusUnknownOption = findUnknownOption(parsed, STATUS_OPTIONS);
|
|
125
|
+
if (statusUnknownOption !== undefined) {
|
|
126
|
+
return unknownOption("sync.status", statusUnknownOption, STATUS_OPTIONS);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const missingFromOption = readMissingOptionValue(parsed.missingOptionValues, "from");
|
|
130
|
+
if (missingFromOption !== undefined) {
|
|
131
|
+
return usage("sync status requires --from <branch> when provided.", "sync.status");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sourceBranch: string = readOption(parsed.options, "from") ?? "main";
|
|
92
135
|
const summary = syncStatus(context.cwd, sourceBranch);
|
|
93
136
|
|
|
94
137
|
return okResult({
|
|
95
|
-
command: "sync
|
|
138
|
+
command: "sync.status",
|
|
96
139
|
human: statusMessage(summary.sourceBranch, summary.ahead, summary.behind, summary.pendingConflicts),
|
|
97
140
|
data: summary,
|
|
98
141
|
});
|
|
99
142
|
}
|
|
100
143
|
|
|
101
144
|
if (subcommand === "pull") {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
return
|
|
145
|
+
const pullUnknownOption = findUnknownOption(parsed, PULL_OPTIONS);
|
|
146
|
+
if (pullUnknownOption !== undefined) {
|
|
147
|
+
return unknownOption("sync.pull", pullUnknownOption, PULL_OPTIONS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const missingFromOption = readMissingOptionValue(parsed.missingOptionValues, "from");
|
|
151
|
+
if (missingFromOption !== undefined) {
|
|
152
|
+
return usage("sync pull requires --from <branch>.", "sync.pull");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sourceBranch: string | undefined = readOption(parsed.options, "from");
|
|
156
|
+
if (sourceBranch === undefined) {
|
|
157
|
+
return usage("sync pull requires --from <branch>.", "sync.pull");
|
|
105
158
|
}
|
|
106
159
|
|
|
107
160
|
const summary = syncPull(context.cwd, sourceBranch);
|
|
108
161
|
|
|
109
162
|
return okResult({
|
|
110
|
-
command: "sync
|
|
163
|
+
command: "sync.pull",
|
|
111
164
|
human: [
|
|
112
165
|
`Pulled from '${summary.sourceBranch}'`,
|
|
113
166
|
`Scanned events: ${summary.scannedEvents}`,
|
|
@@ -123,42 +176,62 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
123
176
|
}
|
|
124
177
|
|
|
125
178
|
if (subcommand === "resolve") {
|
|
126
|
-
const
|
|
127
|
-
|
|
179
|
+
const resolveUnknownOption = findUnknownOption(parsed, RESOLVE_OPTIONS);
|
|
180
|
+
if (resolveUnknownOption !== undefined) {
|
|
181
|
+
return unknownOption("sync.resolve", resolveUnknownOption, RESOLVE_OPTIONS);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const conflictId: string | undefined = parsed.positional[1];
|
|
185
|
+
const missingResolutionOption = readMissingOptionValue(parsed.missingOptionValues, "use");
|
|
186
|
+
if (missingResolutionOption !== undefined) {
|
|
187
|
+
return usage("sync resolve requires <conflict-id> --use ours|theirs.", "sync.resolve");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rawResolution: string | undefined = readOption(parsed.options, "use");
|
|
128
191
|
|
|
129
192
|
if (!conflictId || !rawResolution) {
|
|
130
|
-
return usage("sync resolve requires <conflict-id> --use ours|theirs.");
|
|
193
|
+
return usage("sync resolve requires <conflict-id> --use ours|theirs.", "sync.resolve");
|
|
131
194
|
}
|
|
132
195
|
|
|
133
196
|
if (rawResolution !== "ours" && rawResolution !== "theirs") {
|
|
134
|
-
return usage("sync resolve --use only accepts ours|theirs.");
|
|
197
|
+
return usage("sync resolve --use only accepts ours|theirs.", "sync.resolve");
|
|
135
198
|
}
|
|
136
199
|
|
|
137
200
|
const summary = syncResolve(context.cwd, conflictId, rawResolution as SyncResolution);
|
|
138
201
|
|
|
139
202
|
return okResult({
|
|
140
|
-
command: "sync
|
|
203
|
+
command: "sync.resolve",
|
|
141
204
|
human: `Resolved ${summary.conflictId} using ${summary.resolution}.`,
|
|
142
205
|
data: summary,
|
|
143
206
|
});
|
|
144
207
|
}
|
|
145
208
|
|
|
146
209
|
if (subcommand === "conflicts") {
|
|
147
|
-
const conflictsCommand: string | undefined =
|
|
210
|
+
const conflictsCommand: string | undefined = parsed.positional[1];
|
|
148
211
|
if (!conflictsCommand) {
|
|
149
|
-
|
|
212
|
+
return usage("sync conflicts requires list|show.", "sync.conflicts");
|
|
150
213
|
}
|
|
151
214
|
|
|
152
215
|
if (conflictsCommand === "list") {
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
216
|
+
const listUnknownOption = findUnknownOption(parsed, CONFLICTS_LIST_OPTIONS);
|
|
217
|
+
if (listUnknownOption !== undefined) {
|
|
218
|
+
return unknownOption("sync.conflicts.list", listUnknownOption, CONFLICTS_LIST_OPTIONS);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const missingModeOption = readMissingOptionValue(parsed.missingOptionValues, "mode");
|
|
222
|
+
if (missingModeOption !== undefined) {
|
|
223
|
+
return usage("sync conflicts list --mode only accepts pending|all.", "sync.conflicts.list");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const mode = readOption(parsed.options, "mode") ?? "pending";
|
|
227
|
+
if (mode !== "pending" && mode !== "all") {
|
|
228
|
+
return usage("sync conflicts list --mode only accepts pending|all.", "sync.conflicts.list");
|
|
156
229
|
}
|
|
157
230
|
|
|
158
231
|
const conflicts = listSyncConflicts(context.cwd, mode);
|
|
159
232
|
|
|
160
233
|
return okResult({
|
|
161
|
-
command: "sync
|
|
234
|
+
command: "sync.conflicts.list",
|
|
162
235
|
human: formatConflictList(conflicts),
|
|
163
236
|
data: {
|
|
164
237
|
mode,
|
|
@@ -168,15 +241,20 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
168
241
|
}
|
|
169
242
|
|
|
170
243
|
if (conflictsCommand === "show") {
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
return
|
|
244
|
+
const showUnknownOption = findUnknownOption(parsed, CONFLICTS_SHOW_OPTIONS);
|
|
245
|
+
if (showUnknownOption !== undefined) {
|
|
246
|
+
return unknownOption("sync.conflicts.show", showUnknownOption, CONFLICTS_SHOW_OPTIONS);
|
|
174
247
|
}
|
|
175
248
|
|
|
249
|
+
const conflictId: string | undefined = parsed.positional[2];
|
|
250
|
+
if (!conflictId) {
|
|
251
|
+
return usage("sync conflicts show requires <conflict-id>.", "sync.conflicts.show");
|
|
252
|
+
}
|
|
253
|
+
|
|
176
254
|
const conflict = getSyncConflict(context.cwd, conflictId);
|
|
177
255
|
|
|
178
256
|
return okResult({
|
|
179
|
-
command: "sync
|
|
257
|
+
command: "sync.conflicts.show",
|
|
180
258
|
human: [
|
|
181
259
|
`Conflict: ${conflict.id}`,
|
|
182
260
|
`Entity: ${conflict.entityKind} ${conflict.entityId}`,
|
|
@@ -191,14 +269,14 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
191
269
|
});
|
|
192
270
|
}
|
|
193
271
|
|
|
194
|
-
|
|
272
|
+
return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`, "sync.conflicts");
|
|
195
273
|
}
|
|
196
274
|
|
|
197
275
|
return usage(`Unknown sync subcommand '${subcommand}'.`);
|
|
198
276
|
} catch (error) {
|
|
199
277
|
if (error instanceof MissingBranchDatabaseError) {
|
|
200
278
|
return failResult({
|
|
201
|
-
command:
|
|
279
|
+
command: resolvedCommand,
|
|
202
280
|
human: error.message,
|
|
203
281
|
data: {
|
|
204
282
|
reason: "missing_branch_db",
|
|
@@ -213,7 +291,7 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
213
291
|
const message = error instanceof Error ? error.message : "Unknown sync error.";
|
|
214
292
|
|
|
215
293
|
return failResult({
|
|
216
|
-
command:
|
|
294
|
+
command: resolvedCommand,
|
|
217
295
|
human: message,
|
|
218
296
|
data: {
|
|
219
297
|
reason: "sync_failed",
|
package/src/commands/task.ts
CHANGED
|
@@ -537,7 +537,29 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
537
537
|
command: "task.list",
|
|
538
538
|
human,
|
|
539
539
|
data: { tasks },
|
|
540
|
-
...(context.mode === "human"
|
|
540
|
+
...(context.mode === "human"
|
|
541
|
+
? {}
|
|
542
|
+
: {
|
|
543
|
+
meta: {
|
|
544
|
+
pagination: listed.pagination,
|
|
545
|
+
defaults: {
|
|
546
|
+
statuses: !includeAll && statuses === undefined ? [...DEFAULT_OPEN_TASK_STATUSES] : null,
|
|
547
|
+
limit: !includeAll && parsedLimit === undefined ? DEFAULT_TASK_LIST_LIMIT : null,
|
|
548
|
+
cursor: parsedCursor === undefined ? 0 : null,
|
|
549
|
+
view: view === undefined ? "table" : null,
|
|
550
|
+
},
|
|
551
|
+
filters: {
|
|
552
|
+
epicId: epicId ?? null,
|
|
553
|
+
statuses: selectedStatuses ?? null,
|
|
554
|
+
includeAll,
|
|
555
|
+
},
|
|
556
|
+
truncation: {
|
|
557
|
+
applied: listed.pagination.hasMore,
|
|
558
|
+
returned: tasks.length,
|
|
559
|
+
limit: selectedLimit ?? null,
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
541
563
|
});
|
|
542
564
|
}
|
|
543
565
|
case "show": {
|
|
@@ -593,6 +615,22 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
593
615
|
command: "task.show",
|
|
594
616
|
human: formatTask(task),
|
|
595
617
|
data: { task, includeAll: false },
|
|
618
|
+
...(context.mode === "human"
|
|
619
|
+
? {}
|
|
620
|
+
: {
|
|
621
|
+
meta: {
|
|
622
|
+
defaults: {
|
|
623
|
+
view: view === undefined ? effectiveView : null,
|
|
624
|
+
},
|
|
625
|
+
filters: {
|
|
626
|
+
includeAll: false,
|
|
627
|
+
},
|
|
628
|
+
truncation: {
|
|
629
|
+
applied: true,
|
|
630
|
+
scope: "compact",
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
}),
|
|
596
634
|
});
|
|
597
635
|
}
|
|
598
636
|
|
|
@@ -603,6 +641,22 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
603
641
|
command: "task.show",
|
|
604
642
|
human: formatTaskShowTree(taskTree),
|
|
605
643
|
data: { task: taskTree, includeAll: true, subtasksCount: taskTree.subtasks.length },
|
|
644
|
+
...(context.mode === "human"
|
|
645
|
+
? {}
|
|
646
|
+
: {
|
|
647
|
+
meta: {
|
|
648
|
+
defaults: {
|
|
649
|
+
view: view === undefined ? effectiveView : null,
|
|
650
|
+
},
|
|
651
|
+
filters: {
|
|
652
|
+
includeAll: true,
|
|
653
|
+
},
|
|
654
|
+
truncation: {
|
|
655
|
+
applied: effectiveView === "tree",
|
|
656
|
+
scope: "tree",
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
}),
|
|
606
660
|
});
|
|
607
661
|
}
|
|
608
662
|
|
|
@@ -610,6 +664,22 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
610
664
|
command: "task.show",
|
|
611
665
|
human: effectiveView === "table" ? formatTaskShowTable(taskTree) : formatTaskShowDetail(taskTree),
|
|
612
666
|
data: { task: taskTree, includeAll: true, subtasksCount: taskTree.subtasks.length },
|
|
667
|
+
...(context.mode === "human"
|
|
668
|
+
? {}
|
|
669
|
+
: {
|
|
670
|
+
meta: {
|
|
671
|
+
defaults: {
|
|
672
|
+
view: view === undefined ? effectiveView : null,
|
|
673
|
+
},
|
|
674
|
+
filters: {
|
|
675
|
+
includeAll: true,
|
|
676
|
+
},
|
|
677
|
+
truncation: {
|
|
678
|
+
applied: false,
|
|
679
|
+
scope: "full",
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
}),
|
|
613
683
|
});
|
|
614
684
|
}
|
|
615
685
|
case "ready": {
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { executeShell, parseInvocation, renderShellResult } from "./runtime/cli-
|
|
|
5
5
|
export async function run(argv: readonly string[] = process.argv.slice(2)): Promise<void> {
|
|
6
6
|
const parsed = parseInvocation(argv);
|
|
7
7
|
const result = await executeShell(parsed);
|
|
8
|
-
const rendered: string = renderShellResult(result, parsed.mode);
|
|
8
|
+
const rendered: string = renderShellResult(result, parsed.mode, parsed.compatibilityMode);
|
|
9
9
|
|
|
10
10
|
if (result.ok) {
|
|
11
11
|
process.stdout.write(`${rendered}\n`);
|
package/src/io/output.ts
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
import { encode } from "@toon-format/toon";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type CliResult,
|
|
4
|
+
type CompatibilityMetadata,
|
|
5
|
+
type CompatibilityMode,
|
|
6
|
+
type ContractMetadata,
|
|
7
|
+
type OutputMode,
|
|
8
|
+
type ToonEnvelope,
|
|
9
|
+
type ToonError,
|
|
10
|
+
} from "../runtime/command-types";
|
|
11
|
+
|
|
12
|
+
const CONTRACT_VERSION = "1.0.0";
|
|
13
|
+
const COMPATIBILITY_DEPRECATED_SINCE = "0.1.8";
|
|
14
|
+
const COMPATIBILITY_REMOVAL_AFTER = "2026-09-30";
|
|
15
|
+
|
|
16
|
+
interface RenderOptions {
|
|
17
|
+
readonly compatibilityMode?: CompatibilityMode | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toLegacySyncCommandId(command: string): string {
|
|
21
|
+
const mapping: Record<string, string> = {
|
|
22
|
+
"sync.status": "sync_status",
|
|
23
|
+
"sync.pull": "sync_pull",
|
|
24
|
+
"sync.resolve": "sync_resolve",
|
|
25
|
+
"sync.conflicts": "sync_conflicts",
|
|
26
|
+
"sync.conflicts.list": "sync_conflicts_list",
|
|
27
|
+
"sync.conflicts.show": "sync_conflicts_show",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return mapping[command] ?? command;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveCompatibilityCommand(command: string, compatibilityMode: CompatibilityMode | null): string {
|
|
34
|
+
if (compatibilityMode === "legacy-sync-command-ids") {
|
|
35
|
+
return toLegacySyncCommandId(command);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return command;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createCompatibilityMetadata(command: string, compatibilityMode: CompatibilityMode | null): CompatibilityMetadata | undefined {
|
|
42
|
+
if (compatibilityMode !== "legacy-sync-command-ids") {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const compatibilityCommand: string = toLegacySyncCommandId(command);
|
|
47
|
+
return {
|
|
48
|
+
mode: compatibilityMode,
|
|
49
|
+
warningCode: "compatibility_mode_deprecated",
|
|
50
|
+
deprecatedSince: COMPATIBILITY_DEPRECATED_SINCE,
|
|
51
|
+
removalAfter: COMPATIBILITY_REMOVAL_AFTER,
|
|
52
|
+
migration: "Drop --compat legacy-sync-command-ids and parse canonical dotted command IDs.",
|
|
53
|
+
canonicalCommand: command,
|
|
54
|
+
compatibilityCommand,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hashString(value: string): string {
|
|
59
|
+
let hash = 2166136261;
|
|
60
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
61
|
+
hash ^= value.charCodeAt(index);
|
|
62
|
+
hash = Math.imul(hash, 16777619);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createContractMetadata(result: CliResult, compatibilityMode: CompatibilityMode | null): ContractMetadata {
|
|
69
|
+
const requestSignature = JSON.stringify({
|
|
70
|
+
ok: result.ok,
|
|
71
|
+
command: result.command,
|
|
72
|
+
data: result.data,
|
|
73
|
+
error: result.error ?? null,
|
|
74
|
+
meta: result.meta ?? null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const base: ContractMetadata = {
|
|
78
|
+
contractVersion: CONTRACT_VERSION,
|
|
79
|
+
requestId: `req-${hashString(requestSignature)}`,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const compatibility = createCompatibilityMetadata(result.command, compatibilityMode);
|
|
83
|
+
if (!compatibility) {
|
|
84
|
+
return base;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
compatibility,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
3
92
|
|
|
4
93
|
export interface ResultInput {
|
|
5
94
|
readonly command: string;
|
|
@@ -45,18 +134,22 @@ export function failResult(input: ResultInput & { readonly error: ToonError }):
|
|
|
45
134
|
};
|
|
46
135
|
}
|
|
47
136
|
|
|
48
|
-
export function toToonEnvelope(result: CliResult): ToonEnvelope {
|
|
137
|
+
export function toToonEnvelope(result: CliResult, options: RenderOptions = {}): ToonEnvelope {
|
|
138
|
+
const compatibilityMode: CompatibilityMode | null = options.compatibilityMode ?? null;
|
|
139
|
+
const command: string = resolveCompatibilityCommand(result.command, compatibilityMode);
|
|
140
|
+
|
|
49
141
|
return {
|
|
50
142
|
ok: result.ok,
|
|
51
|
-
command
|
|
143
|
+
command,
|
|
52
144
|
data: result.data,
|
|
145
|
+
metadata: createContractMetadata(result, compatibilityMode),
|
|
53
146
|
...(result.error ? { error: result.error } : {}),
|
|
54
147
|
...(result.meta ? { meta: result.meta } : {}),
|
|
55
148
|
};
|
|
56
149
|
}
|
|
57
150
|
|
|
58
|
-
export function renderResult(result: CliResult, mode: OutputMode): string {
|
|
59
|
-
const envelope: ToonEnvelope = toToonEnvelope(result);
|
|
151
|
+
export function renderResult(result: CliResult, mode: OutputMode, options: RenderOptions = {}): string {
|
|
152
|
+
const envelope: ToonEnvelope = toToonEnvelope(result, options);
|
|
60
153
|
|
|
61
154
|
if (mode === "json") {
|
|
62
155
|
return JSON.stringify(envelope);
|