trekoon 0.1.7 → 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 +81 -15
- package/README.md +181 -21
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +95 -0
- package/src/commands/dep.ts +20 -1
- package/src/commands/epic.ts +141 -7
- package/src/commands/help.ts +266 -17
- package/src/commands/quickstart.ts +88 -24
- package/src/commands/subtask.ts +98 -6
- package/src/commands/sync.ts +130 -52
- package/src/commands/task.ts +369 -7
- package/src/domain/tracker-domain.ts +113 -7
- package/src/domain/types.ts +7 -0
- package/src/index.ts +1 -1
- package/src/io/output.ts +98 -5
- package/src/runtime/cli-shell.ts +160 -24
- package/src/runtime/command-types.ts +18 -0
- package/src/runtime/version.ts +20 -0
- package/src/storage/path.ts +58 -1
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,9 +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";
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
import { type CliContext, type CliResult, type CompatibilityMode, type OutputMode } from "./command-types";
|
|
15
|
+
import { CLI_VERSION } from "./version";
|
|
16
|
+
import { resolveStoragePaths } from "../storage/path";
|
|
17
17
|
|
|
18
18
|
const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
19
19
|
"help",
|
|
@@ -32,6 +32,9 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
|
32
32
|
|
|
33
33
|
export interface ParsedInvocation {
|
|
34
34
|
readonly mode: OutputMode;
|
|
35
|
+
readonly compatibilityMode: CompatibilityMode | null;
|
|
36
|
+
readonly compatibilityModeRaw: string | null;
|
|
37
|
+
readonly compatibilityModeMissingValue: boolean;
|
|
35
38
|
readonly command: string | null;
|
|
36
39
|
readonly args: readonly string[];
|
|
37
40
|
readonly wantsHelp: boolean;
|
|
@@ -45,11 +48,18 @@ export interface ParseInvocationOptions {
|
|
|
45
48
|
export function parseInvocation(argv: readonly string[], options: ParseInvocationOptions = {}): ParsedInvocation {
|
|
46
49
|
const stdoutIsTTY: boolean = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
47
50
|
let explicitMode: OutputMode | null = null;
|
|
51
|
+
let compatibilityModeRaw: string | null = null;
|
|
52
|
+
let compatibilityModeMissingValue = false;
|
|
48
53
|
let wantsHelp = false;
|
|
49
54
|
let wantsVersion = false;
|
|
50
55
|
const positionals: string[] = [];
|
|
51
56
|
|
|
52
|
-
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
|
+
|
|
53
63
|
if (token === "--json") {
|
|
54
64
|
explicitMode = "json";
|
|
55
65
|
continue;
|
|
@@ -70,11 +80,29 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
|
|
|
70
80
|
continue;
|
|
71
81
|
}
|
|
72
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
|
+
|
|
73
95
|
positionals.push(token);
|
|
74
96
|
}
|
|
75
97
|
|
|
98
|
+
const compatibilityMode: CompatibilityMode | null =
|
|
99
|
+
compatibilityModeRaw === "legacy-sync-command-ids" ? compatibilityModeRaw : null;
|
|
100
|
+
|
|
76
101
|
return {
|
|
77
102
|
mode: explicitMode ?? (stdoutIsTTY ? "human" : "json"),
|
|
103
|
+
compatibilityMode,
|
|
104
|
+
compatibilityModeRaw,
|
|
105
|
+
compatibilityModeMissingValue,
|
|
78
106
|
command: positionals[0] ?? null,
|
|
79
107
|
args: positionals.slice(1),
|
|
80
108
|
wantsHelp,
|
|
@@ -82,11 +110,96 @@ export function parseInvocation(argv: readonly string[], options: ParseInvocatio
|
|
|
82
110
|
};
|
|
83
111
|
}
|
|
84
112
|
|
|
85
|
-
export function renderShellResult(result: CliResult, mode: OutputMode): string {
|
|
86
|
-
|
|
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
|
+
};
|
|
87
140
|
}
|
|
88
141
|
|
|
89
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
|
+
|
|
90
203
|
if (parsed.wantsVersion) {
|
|
91
204
|
return okResult({
|
|
92
205
|
command: "version",
|
|
@@ -102,19 +215,23 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
102
215
|
args: parsed.command ? [parsed.command] : [],
|
|
103
216
|
};
|
|
104
217
|
|
|
105
|
-
return runHelp(helpContext);
|
|
218
|
+
return withStorageRootDiagnostics(await runHelp(helpContext), cwd);
|
|
106
219
|
}
|
|
107
220
|
|
|
108
221
|
if (!parsed.command) {
|
|
109
|
-
return
|
|
222
|
+
return withStorageRootDiagnostics(
|
|
223
|
+
await runHelp({
|
|
110
224
|
mode: parsed.mode,
|
|
111
225
|
args: [],
|
|
112
226
|
cwd,
|
|
113
|
-
|
|
227
|
+
}),
|
|
228
|
+
cwd,
|
|
229
|
+
);
|
|
114
230
|
}
|
|
115
231
|
|
|
116
232
|
if (!SUPPORTED_ROOT_COMMANDS.includes(parsed.command)) {
|
|
117
|
-
return
|
|
233
|
+
return withStorageRootDiagnostics(
|
|
234
|
+
failResult({
|
|
118
235
|
command: "shell",
|
|
119
236
|
human: `Unknown command: ${parsed.command}\nRun 'trekoon --help' for usage.`,
|
|
120
237
|
data: {
|
|
@@ -125,7 +242,9 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
125
242
|
code: "unknown_command",
|
|
126
243
|
message: `Unknown command '${parsed.command}'`,
|
|
127
244
|
},
|
|
128
|
-
|
|
245
|
+
}),
|
|
246
|
+
cwd,
|
|
247
|
+
);
|
|
129
248
|
}
|
|
130
249
|
|
|
131
250
|
const context: CliContext = {
|
|
@@ -134,33 +253,47 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
134
253
|
cwd,
|
|
135
254
|
};
|
|
136
255
|
|
|
256
|
+
let result: CliResult;
|
|
257
|
+
|
|
137
258
|
switch (parsed.command) {
|
|
138
259
|
case "help":
|
|
139
|
-
|
|
260
|
+
result = await runHelp(context);
|
|
261
|
+
break;
|
|
140
262
|
case "init":
|
|
141
|
-
|
|
263
|
+
result = await runInit(context);
|
|
264
|
+
break;
|
|
142
265
|
case "quickstart":
|
|
143
|
-
|
|
266
|
+
result = await runQuickstart(context);
|
|
267
|
+
break;
|
|
144
268
|
case "wipe":
|
|
145
|
-
|
|
269
|
+
result = await runWipe(context);
|
|
270
|
+
break;
|
|
146
271
|
case "epic":
|
|
147
|
-
|
|
272
|
+
result = await runEpic(context);
|
|
273
|
+
break;
|
|
148
274
|
case "task":
|
|
149
|
-
|
|
275
|
+
result = await runTask(context);
|
|
276
|
+
break;
|
|
150
277
|
case "subtask":
|
|
151
|
-
|
|
278
|
+
result = await runSubtask(context);
|
|
279
|
+
break;
|
|
152
280
|
case "dep":
|
|
153
|
-
|
|
281
|
+
result = await runDep(context);
|
|
282
|
+
break;
|
|
154
283
|
case "events":
|
|
155
|
-
|
|
284
|
+
result = await runEvents(context);
|
|
285
|
+
break;
|
|
156
286
|
case "migrate":
|
|
157
|
-
|
|
287
|
+
result = await runMigrate(context);
|
|
288
|
+
break;
|
|
158
289
|
case "sync":
|
|
159
|
-
|
|
290
|
+
result = await runSync(context);
|
|
291
|
+
break;
|
|
160
292
|
case "skills":
|
|
161
|
-
|
|
293
|
+
result = await runSkills(context);
|
|
294
|
+
break;
|
|
162
295
|
default:
|
|
163
|
-
|
|
296
|
+
result = failResult({
|
|
164
297
|
command: "shell",
|
|
165
298
|
human: `Unhandled command: ${parsed.command}`,
|
|
166
299
|
data: { command: parsed.command },
|
|
@@ -169,5 +302,8 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
169
302
|
message: `No shell handler for '${parsed.command}'`,
|
|
170
303
|
},
|
|
171
304
|
});
|
|
305
|
+
break;
|
|
172
306
|
}
|
|
307
|
+
|
|
308
|
+
return withStorageRootDiagnostics(result, cwd);
|
|
173
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
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
interface PackageManifest {
|
|
4
|
+
readonly version?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function readCliVersion(): string {
|
|
8
|
+
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
9
|
+
const packageJsonContent: string = readFileSync(packageJsonPath, "utf8");
|
|
10
|
+
const packageManifest: PackageManifest = JSON.parse(packageJsonContent) as PackageManifest;
|
|
11
|
+
const version: string | undefined = packageManifest.version;
|
|
12
|
+
|
|
13
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
14
|
+
throw new Error("package.json is missing a valid version field.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const CLI_VERSION: string = readCliVersion();
|
package/src/storage/path.ts
CHANGED
|
@@ -1,22 +1,79 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { resolve } from "node:path";
|
|
2
3
|
|
|
3
4
|
const DB_DIRNAME = ".trekoon";
|
|
4
5
|
const DB_FILENAME = "trekoon.db";
|
|
5
6
|
|
|
6
7
|
export interface StoragePaths {
|
|
8
|
+
readonly invocationCwd: string;
|
|
7
9
|
readonly worktreeRoot: string;
|
|
8
10
|
readonly storageDir: string;
|
|
9
11
|
readonly databaseFile: string;
|
|
12
|
+
readonly diagnostics: StoragePathDiagnostics;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StoragePathIssue {
|
|
16
|
+
readonly code: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly invocationCwd: string;
|
|
19
|
+
readonly canonicalRoot: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StoragePathDiagnostics {
|
|
23
|
+
readonly invocationCwd: string;
|
|
24
|
+
readonly canonicalRoot: string;
|
|
25
|
+
readonly warnings: readonly StoragePathIssue[];
|
|
26
|
+
readonly errors: readonly StoragePathIssue[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveGitTopLevel(workingDirectory: string): string | null {
|
|
30
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
31
|
+
cwd: workingDirectory,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const topLevel: string = result.stdout.trim();
|
|
41
|
+
if (!topLevel) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return resolve(topLevel);
|
|
10
46
|
}
|
|
11
47
|
|
|
12
48
|
export function resolveStoragePaths(workingDirectory: string = process.cwd()): StoragePaths {
|
|
13
|
-
const
|
|
49
|
+
const invocationCwd: string = resolve(workingDirectory);
|
|
50
|
+
const canonicalRoot: string = resolveGitTopLevel(invocationCwd) ?? invocationCwd;
|
|
51
|
+
const worktreeRoot: string = canonicalRoot;
|
|
14
52
|
const storageDir: string = resolve(worktreeRoot, DB_DIRNAME);
|
|
15
53
|
const databaseFile: string = resolve(storageDir, DB_FILENAME);
|
|
54
|
+
const warnings: StoragePathIssue[] = [];
|
|
55
|
+
|
|
56
|
+
if (invocationCwd !== canonicalRoot) {
|
|
57
|
+
warnings.push({
|
|
58
|
+
code: "storage_root_diverged_from_cwd",
|
|
59
|
+
message: "Resolved storage root differs from invocation cwd.",
|
|
60
|
+
invocationCwd,
|
|
61
|
+
canonicalRoot,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const diagnostics: StoragePathDiagnostics = {
|
|
66
|
+
invocationCwd,
|
|
67
|
+
canonicalRoot,
|
|
68
|
+
warnings,
|
|
69
|
+
errors: [],
|
|
70
|
+
};
|
|
16
71
|
|
|
17
72
|
return {
|
|
73
|
+
invocationCwd,
|
|
18
74
|
worktreeRoot,
|
|
19
75
|
storageDir,
|
|
20
76
|
databaseFile,
|
|
77
|
+
diagnostics,
|
|
21
78
|
};
|
|
22
79
|
}
|