trekoon 0.1.8 → 0.2.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/.agents/skills/trekoon/SKILL.md +117 -2
- package/README.md +158 -18
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +164 -0
- package/src/commands/epic.ts +256 -3
- package/src/commands/help.ts +45 -4
- package/src/commands/subtask.ts +209 -3
- package/src/commands/sync.ts +130 -52
- package/src/commands/task.ts +257 -3
- package/src/domain/mutation-service.ts +242 -1
- package/src/domain/tracker-domain.ts +171 -0
- package/src/domain/types.ts +27 -0
- 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/sync/event-writes.ts +21 -1
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
type EpicTree,
|
|
11
11
|
type NodeKind,
|
|
12
12
|
type ReverseDependencyNode,
|
|
13
|
+
type SearchEntityMatch,
|
|
14
|
+
type SearchField,
|
|
15
|
+
type SearchFieldMatch,
|
|
16
|
+
type SearchNode,
|
|
17
|
+
type SearchSummary,
|
|
13
18
|
type SubtaskRecord,
|
|
14
19
|
type TaskTreeDetailed,
|
|
15
20
|
type TaskRecord,
|
|
@@ -125,6 +130,55 @@ function mapDependency(row: DependencyRow): DependencyRecord {
|
|
|
125
130
|
};
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
function countMatches(value: string, searchText: string): number {
|
|
134
|
+
if (searchText.length === 0) {
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let count = 0;
|
|
139
|
+
let offset = 0;
|
|
140
|
+
while (offset <= value.length - searchText.length) {
|
|
141
|
+
const nextIndex = value.indexOf(searchText, offset);
|
|
142
|
+
if (nextIndex === -1) {
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
count += 1;
|
|
147
|
+
offset = nextIndex + searchText.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return count;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildMatchSnippet(value: string, searchText: string, contextSize = 24): string {
|
|
154
|
+
if (searchText.length === 0) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const matchIndex = value.indexOf(searchText);
|
|
159
|
+
if (matchIndex === -1) {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const start = Math.max(0, matchIndex - contextSize);
|
|
164
|
+
const end = Math.min(value.length, matchIndex + searchText.length + contextSize);
|
|
165
|
+
const rawSnippet = value.slice(start, end).replace(/\s+/g, " ").trim();
|
|
166
|
+
const prefix = start > 0 ? "…" : "";
|
|
167
|
+
const suffix = end < value.length ? "…" : "";
|
|
168
|
+
return `${prefix}${rawSnippet}${suffix}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function summarizeMatches(matches: readonly SearchEntityMatch[]): SearchSummary {
|
|
172
|
+
return {
|
|
173
|
+
matchedEntities: matches.length,
|
|
174
|
+
matchedFields: matches.reduce((total, match) => total + match.fields.length, 0),
|
|
175
|
+
totalMatches: matches.reduce(
|
|
176
|
+
(total, match) => total + match.fields.reduce((fieldTotal, field) => fieldTotal + field.count, 0),
|
|
177
|
+
0,
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
128
182
|
export class TrackerDomain {
|
|
129
183
|
readonly #db: Database;
|
|
130
184
|
|
|
@@ -433,6 +487,89 @@ export class TrackerDomain {
|
|
|
433
487
|
};
|
|
434
488
|
}
|
|
435
489
|
|
|
490
|
+
collectEpicSearchScope(epicId: string): readonly SearchNode[] {
|
|
491
|
+
const tree = this.buildEpicTreeDetailed(epicId);
|
|
492
|
+
|
|
493
|
+
return [
|
|
494
|
+
{
|
|
495
|
+
kind: "epic",
|
|
496
|
+
id: tree.id,
|
|
497
|
+
title: tree.title,
|
|
498
|
+
description: tree.description,
|
|
499
|
+
},
|
|
500
|
+
...tree.tasks.flatMap((task) => [
|
|
501
|
+
{
|
|
502
|
+
kind: "task" as const,
|
|
503
|
+
id: task.id,
|
|
504
|
+
title: task.title,
|
|
505
|
+
description: task.description,
|
|
506
|
+
},
|
|
507
|
+
...task.subtasks.map((subtask) => ({
|
|
508
|
+
kind: "subtask" as const,
|
|
509
|
+
id: subtask.id,
|
|
510
|
+
title: subtask.title,
|
|
511
|
+
description: subtask.description,
|
|
512
|
+
})),
|
|
513
|
+
]),
|
|
514
|
+
];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
collectTaskSearchScope(taskId: string): readonly SearchNode[] {
|
|
518
|
+
const tree = this.buildTaskTreeDetailed(taskId);
|
|
519
|
+
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
kind: "task",
|
|
523
|
+
id: tree.id,
|
|
524
|
+
title: tree.title,
|
|
525
|
+
description: tree.description,
|
|
526
|
+
},
|
|
527
|
+
...tree.subtasks.map((subtask) => ({
|
|
528
|
+
kind: "subtask" as const,
|
|
529
|
+
id: subtask.id,
|
|
530
|
+
title: subtask.title,
|
|
531
|
+
description: subtask.description,
|
|
532
|
+
})),
|
|
533
|
+
];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
collectSubtaskSearchScope(subtaskId: string): readonly SearchNode[] {
|
|
537
|
+
const subtask = this.getSubtaskOrThrow(subtaskId);
|
|
538
|
+
|
|
539
|
+
return [
|
|
540
|
+
{
|
|
541
|
+
kind: "subtask",
|
|
542
|
+
id: subtask.id,
|
|
543
|
+
title: subtask.title,
|
|
544
|
+
description: subtask.description,
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
searchEpicScope(epicId: string, searchText: string, fields: readonly SearchField[]): { readonly matches: readonly SearchEntityMatch[]; readonly summary: SearchSummary } {
|
|
550
|
+
const matches = this.collectSearchMatches(this.collectEpicSearchScope(epicId), searchText, fields);
|
|
551
|
+
return {
|
|
552
|
+
matches,
|
|
553
|
+
summary: summarizeMatches(matches),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
searchTaskScope(taskId: string, searchText: string, fields: readonly SearchField[]): { readonly matches: readonly SearchEntityMatch[]; readonly summary: SearchSummary } {
|
|
558
|
+
const matches = this.collectSearchMatches(this.collectTaskSearchScope(taskId), searchText, fields);
|
|
559
|
+
return {
|
|
560
|
+
matches,
|
|
561
|
+
summary: summarizeMatches(matches),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
searchSubtaskScope(subtaskId: string, searchText: string, fields: readonly SearchField[]): { readonly matches: readonly SearchEntityMatch[]; readonly summary: SearchSummary } {
|
|
566
|
+
const matches = this.collectSearchMatches(this.collectSubtaskSearchScope(subtaskId), searchText, fields);
|
|
567
|
+
return {
|
|
568
|
+
matches,
|
|
569
|
+
summary: summarizeMatches(matches),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
436
573
|
resolveNodeKind(id: string): "task" | "subtask" {
|
|
437
574
|
const task = this.getTask(id);
|
|
438
575
|
if (task) {
|
|
@@ -570,6 +707,40 @@ export class TrackerDomain {
|
|
|
570
707
|
return mapDependency(row);
|
|
571
708
|
}
|
|
572
709
|
|
|
710
|
+
private collectSearchMatches(
|
|
711
|
+
nodes: readonly SearchNode[],
|
|
712
|
+
searchText: string,
|
|
713
|
+
fields: readonly SearchField[],
|
|
714
|
+
): readonly SearchEntityMatch[] {
|
|
715
|
+
const matches: SearchEntityMatch[] = [];
|
|
716
|
+
|
|
717
|
+
for (const node of nodes) {
|
|
718
|
+
const matchedFields: SearchFieldMatch[] = [];
|
|
719
|
+
for (const field of fields) {
|
|
720
|
+
const count = countMatches(node[field], searchText);
|
|
721
|
+
if (count > 0) {
|
|
722
|
+
matchedFields.push({
|
|
723
|
+
field,
|
|
724
|
+
count,
|
|
725
|
+
snippet: buildMatchSnippet(node[field], searchText),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (matchedFields.length === 0) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
matches.push({
|
|
735
|
+
kind: node.kind,
|
|
736
|
+
id: node.id,
|
|
737
|
+
fields: matchedFields,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return matches;
|
|
742
|
+
}
|
|
743
|
+
|
|
573
744
|
private wouldCreateCycle(sourceId: string, dependsOnId: string): boolean {
|
|
574
745
|
const row = this.#db
|
|
575
746
|
.query(
|
package/src/domain/types.ts
CHANGED
|
@@ -85,6 +85,33 @@ export interface EpicTreeDetailed {
|
|
|
85
85
|
readonly tasks: ReadonlyArray<TaskTreeDetailed>;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export type SearchField = "title" | "description";
|
|
89
|
+
|
|
90
|
+
export interface SearchFieldMatch {
|
|
91
|
+
readonly field: SearchField;
|
|
92
|
+
readonly count: number;
|
|
93
|
+
readonly snippet: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SearchEntityMatch {
|
|
97
|
+
readonly kind: NodeKind;
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly fields: readonly SearchFieldMatch[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SearchSummary {
|
|
103
|
+
readonly matchedEntities: number;
|
|
104
|
+
readonly matchedFields: number;
|
|
105
|
+
readonly totalMatches: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SearchNode {
|
|
109
|
+
readonly kind: NodeKind;
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly title: string;
|
|
112
|
+
readonly description: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
export interface DomainErrorShape {
|
|
89
116
|
readonly code: string;
|
|
90
117
|
readonly message: string;
|
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);
|
package/src/runtime/cli-shell.ts
CHANGED
|
@@ -11,8 +11,9 @@ import { runSync } from "../commands/sync";
|
|
|
11
11
|
import { runTask } from "../commands/task";
|
|
12
12
|
import { runWipe } from "../commands/wipe";
|
|
13
13
|
import { failResult, okResult, renderResult } from "../io/output";
|
|
14
|
-
import { type CliContext, type CliResult, type OutputMode } from "./command-types";
|
|
14
|
+
import { type CliContext, type CliResult, type CompatibilityMode, type OutputMode } from "./command-types";
|
|
15
15
|
import { CLI_VERSION } from "./version";
|
|
16
|
+
import { resolveStoragePaths } from "../storage/path";
|
|
16
17
|
|
|
17
18
|
const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
18
19
|
"help",
|
|
@@ -31,6 +32,9 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
|
31
32
|
|
|
32
33
|
export interface ParsedInvocation {
|
|
33
34
|
readonly mode: OutputMode;
|
|
35
|
+
readonly compatibilityMode: CompatibilityMode | null;
|
|
36
|
+
readonly compatibilityModeRaw: string | null;
|
|
37
|
+
readonly compatibilityModeMissingValue: boolean;
|
|
34
38
|
readonly command: string | null;
|
|
35
39
|
readonly args: readonly string[];
|
|
36
40
|
readonly wantsHelp: boolean;
|
|
@@ -44,11 +48,18 @@ export interface ParseInvocationOptions {
|
|
|
44
48
|
export function parseInvocation(argv: readonly string[], options: ParseInvocationOptions = {}): ParsedInvocation {
|
|
45
49
|
const stdoutIsTTY: boolean = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
46
50
|
let explicitMode: OutputMode | null = null;
|
|
51
|
+
let compatibilityModeRaw: string | null = null;
|
|
52
|
+
let compatibilityModeMissingValue = false;
|
|
47
53
|
let wantsHelp = false;
|
|
48
54
|
let wantsVersion = false;
|
|
49
55
|
const positionals: string[] = [];
|
|
50
56
|
|
|
51
|
-
for (
|
|
57
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
58
|
+
const token: string | undefined = argv[index];
|
|
59
|
+
if (!token) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
if (token === "--json") {
|
|
53
64
|
explicitMode = "json";
|
|
54
65
|
continue;
|
|
@@ -69,11 +80,29 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
|
|
|
69
80
|
continue;
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
if (token === "--compat") {
|
|
84
|
+
const maybeValue: string | undefined = argv[index + 1];
|
|
85
|
+
if (!maybeValue || maybeValue.startsWith("--")) {
|
|
86
|
+
compatibilityModeMissingValue = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
compatibilityModeRaw = maybeValue;
|
|
91
|
+
index += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
positionals.push(token);
|
|
73
96
|
}
|
|
74
97
|
|
|
98
|
+
const compatibilityMode: CompatibilityMode | null =
|
|
99
|
+
compatibilityModeRaw === "legacy-sync-command-ids" ? compatibilityModeRaw : null;
|
|
100
|
+
|
|
75
101
|
return {
|
|
76
102
|
mode: explicitMode ?? (stdoutIsTTY ? "human" : "json"),
|
|
103
|
+
compatibilityMode,
|
|
104
|
+
compatibilityModeRaw,
|
|
105
|
+
compatibilityModeMissingValue,
|
|
77
106
|
command: positionals[0] ?? null,
|
|
78
107
|
args: positionals.slice(1),
|
|
79
108
|
wantsHelp,
|
|
@@ -81,11 +110,96 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
|
|
|
81
110
|
};
|
|
82
111
|
}
|
|
83
112
|
|
|
84
|
-
export function renderShellResult(result: CliResult, mode: OutputMode): string {
|
|
85
|
-
|
|
113
|
+
export function renderShellResult(result: CliResult, mode: OutputMode, compatibilityMode: CompatibilityMode | null = null): string {
|
|
114
|
+
const effectiveCompatibilityMode: CompatibilityMode | null =
|
|
115
|
+
compatibilityMode === "legacy-sync-command-ids" && result.command.startsWith("sync.")
|
|
116
|
+
? compatibilityMode
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
return renderResult(result, mode, { compatibilityMode: effectiveCompatibilityMode });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function withStorageRootDiagnostics(result: CliResult, cwd: string): CliResult {
|
|
123
|
+
const diagnostics = resolveStoragePaths(cwd).diagnostics;
|
|
124
|
+
if (diagnostics.warnings.length === 0 && diagnostics.errors.length === 0) {
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...result,
|
|
130
|
+
meta: {
|
|
131
|
+
...(result.meta ?? {}),
|
|
132
|
+
storageRootDiagnostics: {
|
|
133
|
+
invocationCwd: diagnostics.invocationCwd,
|
|
134
|
+
canonicalRoot: diagnostics.canonicalRoot,
|
|
135
|
+
warning: diagnostics.warnings[0] ?? null,
|
|
136
|
+
error: diagnostics.errors[0] ?? null,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
86
140
|
}
|
|
87
141
|
|
|
88
142
|
export async function executeShell(parsed: ParsedInvocation, cwd: string = process.cwd()): Promise<CliResult> {
|
|
143
|
+
if (parsed.compatibilityModeMissingValue) {
|
|
144
|
+
return failResult({
|
|
145
|
+
command: "shell",
|
|
146
|
+
human: "--compat requires an explicit mode value.",
|
|
147
|
+
data: {
|
|
148
|
+
option: "--compat",
|
|
149
|
+
allowedModes: ["legacy-sync-command-ids"],
|
|
150
|
+
},
|
|
151
|
+
error: {
|
|
152
|
+
code: "invalid_args",
|
|
153
|
+
message: "Missing compatibility mode value for --compat.",
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (parsed.compatibilityModeRaw !== null && parsed.compatibilityMode === null) {
|
|
159
|
+
return failResult({
|
|
160
|
+
command: "shell",
|
|
161
|
+
human: `Unsupported compatibility mode '${parsed.compatibilityModeRaw}'.`,
|
|
162
|
+
data: {
|
|
163
|
+
providedMode: parsed.compatibilityModeRaw,
|
|
164
|
+
allowedModes: ["legacy-sync-command-ids"],
|
|
165
|
+
},
|
|
166
|
+
error: {
|
|
167
|
+
code: "invalid_args",
|
|
168
|
+
message: `Unsupported compatibility mode '${parsed.compatibilityModeRaw}'.`,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (parsed.compatibilityMode !== null && parsed.mode === "human") {
|
|
174
|
+
return failResult({
|
|
175
|
+
command: "shell",
|
|
176
|
+
human: "Compatibility mode is machine-only; use --json or --toon.",
|
|
177
|
+
data: {
|
|
178
|
+
mode: parsed.mode,
|
|
179
|
+
compatibilityMode: parsed.compatibilityMode,
|
|
180
|
+
},
|
|
181
|
+
error: {
|
|
182
|
+
code: "invalid_args",
|
|
183
|
+
message: "Compatibility mode requires machine output mode.",
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (parsed.compatibilityMode === "legacy-sync-command-ids" && parsed.command !== "sync") {
|
|
189
|
+
return failResult({
|
|
190
|
+
command: "shell",
|
|
191
|
+
human: "--compat legacy-sync-command-ids only supports sync commands.",
|
|
192
|
+
data: {
|
|
193
|
+
compatibilityMode: parsed.compatibilityMode,
|
|
194
|
+
command: parsed.command,
|
|
195
|
+
},
|
|
196
|
+
error: {
|
|
197
|
+
code: "invalid_args",
|
|
198
|
+
message: "Compatibility mode can only be used with the sync command.",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
89
203
|
if (parsed.wantsVersion) {
|
|
90
204
|
return okResult({
|
|
91
205
|
command: "version",
|
|
@@ -101,19 +215,23 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
101
215
|
args: parsed.command ? [parsed.command] : [],
|
|
102
216
|
};
|
|
103
217
|
|
|
104
|
-
return runHelp(helpContext);
|
|
218
|
+
return withStorageRootDiagnostics(await runHelp(helpContext), cwd);
|
|
105
219
|
}
|
|
106
220
|
|
|
107
221
|
if (!parsed.command) {
|
|
108
|
-
return
|
|
222
|
+
return withStorageRootDiagnostics(
|
|
223
|
+
await runHelp({
|
|
109
224
|
mode: parsed.mode,
|
|
110
225
|
args: [],
|
|
111
226
|
cwd,
|
|
112
|
-
|
|
227
|
+
}),
|
|
228
|
+
cwd,
|
|
229
|
+
);
|
|
113
230
|
}
|
|
114
231
|
|
|
115
232
|
if (!SUPPORTED_ROOT_COMMANDS.includes(parsed.command)) {
|
|
116
|
-
return
|
|
233
|
+
return withStorageRootDiagnostics(
|
|
234
|
+
failResult({
|
|
117
235
|
command: "shell",
|
|
118
236
|
human: `Unknown command: ${parsed.command}\nRun 'trekoon --help' for usage.`,
|
|
119
237
|
data: {
|
|
@@ -124,7 +242,9 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
124
242
|
code: "unknown_command",
|
|
125
243
|
message: `Unknown command '${parsed.command}'`,
|
|
126
244
|
},
|
|
127
|
-
|
|
245
|
+
}),
|
|
246
|
+
cwd,
|
|
247
|
+
);
|
|
128
248
|
}
|
|
129
249
|
|
|
130
250
|
const context: CliContext = {
|
|
@@ -133,33 +253,47 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
133
253
|
cwd,
|
|
134
254
|
};
|
|
135
255
|
|
|
256
|
+
let result: CliResult;
|
|
257
|
+
|
|
136
258
|
switch (parsed.command) {
|
|
137
259
|
case "help":
|
|
138
|
-
|
|
260
|
+
result = await runHelp(context);
|
|
261
|
+
break;
|
|
139
262
|
case "init":
|
|
140
|
-
|
|
263
|
+
result = await runInit(context);
|
|
264
|
+
break;
|
|
141
265
|
case "quickstart":
|
|
142
|
-
|
|
266
|
+
result = await runQuickstart(context);
|
|
267
|
+
break;
|
|
143
268
|
case "wipe":
|
|
144
|
-
|
|
269
|
+
result = await runWipe(context);
|
|
270
|
+
break;
|
|
145
271
|
case "epic":
|
|
146
|
-
|
|
272
|
+
result = await runEpic(context);
|
|
273
|
+
break;
|
|
147
274
|
case "task":
|
|
148
|
-
|
|
275
|
+
result = await runTask(context);
|
|
276
|
+
break;
|
|
149
277
|
case "subtask":
|
|
150
|
-
|
|
278
|
+
result = await runSubtask(context);
|
|
279
|
+
break;
|
|
151
280
|
case "dep":
|
|
152
|
-
|
|
281
|
+
result = await runDep(context);
|
|
282
|
+
break;
|
|
153
283
|
case "events":
|
|
154
|
-
|
|
284
|
+
result = await runEvents(context);
|
|
285
|
+
break;
|
|
155
286
|
case "migrate":
|
|
156
|
-
|
|
287
|
+
result = await runMigrate(context);
|
|
288
|
+
break;
|
|
157
289
|
case "sync":
|
|
158
|
-
|
|
290
|
+
result = await runSync(context);
|
|
291
|
+
break;
|
|
159
292
|
case "skills":
|
|
160
|
-
|
|
293
|
+
result = await runSkills(context);
|
|
294
|
+
break;
|
|
161
295
|
default:
|
|
162
|
-
|
|
296
|
+
result = failResult({
|
|
163
297
|
command: "shell",
|
|
164
298
|
human: `Unhandled command: ${parsed.command}`,
|
|
165
299
|
data: { command: parsed.command },
|
|
@@ -168,5 +302,8 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
168
302
|
message: `No shell handler for '${parsed.command}'`,
|
|
169
303
|
},
|
|
170
304
|
});
|
|
305
|
+
break;
|
|
171
306
|
}
|
|
307
|
+
|
|
308
|
+
return withStorageRootDiagnostics(result, cwd);
|
|
172
309
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type OutputMode = "human" | "json" | "toon";
|
|
2
|
+
export type CompatibilityMode = "legacy-sync-command-ids";
|
|
2
3
|
|
|
3
4
|
export interface CliContext {
|
|
4
5
|
readonly mode: OutputMode;
|
|
@@ -11,10 +12,27 @@ export interface ToonError {
|
|
|
11
12
|
readonly message: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export interface ContractMetadata {
|
|
16
|
+
readonly contractVersion: string;
|
|
17
|
+
readonly requestId: string;
|
|
18
|
+
readonly compatibility?: CompatibilityMetadata;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CompatibilityMetadata {
|
|
22
|
+
readonly mode: CompatibilityMode;
|
|
23
|
+
readonly warningCode: "compatibility_mode_deprecated";
|
|
24
|
+
readonly deprecatedSince: string;
|
|
25
|
+
readonly removalAfter: string;
|
|
26
|
+
readonly migration: string;
|
|
27
|
+
readonly canonicalCommand: string;
|
|
28
|
+
readonly compatibilityCommand: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export interface ToonEnvelope {
|
|
15
32
|
readonly ok: boolean;
|
|
16
33
|
readonly command: string;
|
|
17
34
|
readonly data: unknown;
|
|
35
|
+
readonly metadata: ContractMetadata;
|
|
18
36
|
readonly error?: ToonError;
|
|
19
37
|
readonly meta?: Record<string, unknown>;
|
|
20
38
|
}
|