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/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 { type CliResult, type OutputMode, type ToonEnvelope, type ToonError } from "../runtime/command-types";
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: result.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);
@@ -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
- const CLI_VERSION = "0.1.0";
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 (const token of argv) {
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
- return renderResult(result, mode);
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 runHelp({
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 failResult({
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
- return runHelp(context);
260
+ result = await runHelp(context);
261
+ break;
140
262
  case "init":
141
- return runInit(context);
263
+ result = await runInit(context);
264
+ break;
142
265
  case "quickstart":
143
- return runQuickstart(context);
266
+ result = await runQuickstart(context);
267
+ break;
144
268
  case "wipe":
145
- return runWipe(context);
269
+ result = await runWipe(context);
270
+ break;
146
271
  case "epic":
147
- return runEpic(context);
272
+ result = await runEpic(context);
273
+ break;
148
274
  case "task":
149
- return runTask(context);
275
+ result = await runTask(context);
276
+ break;
150
277
  case "subtask":
151
- return runSubtask(context);
278
+ result = await runSubtask(context);
279
+ break;
152
280
  case "dep":
153
- return runDep(context);
281
+ result = await runDep(context);
282
+ break;
154
283
  case "events":
155
- return runEvents(context);
284
+ result = await runEvents(context);
285
+ break;
156
286
  case "migrate":
157
- return runMigrate(context);
287
+ result = await runMigrate(context);
288
+ break;
158
289
  case "sync":
159
- return runSync(context);
290
+ result = await runSync(context);
291
+ break;
160
292
  case "skills":
161
- return runSkills(context);
293
+ result = await runSkills(context);
294
+ break;
162
295
  default:
163
- return failResult({
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();
@@ -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 worktreeRoot: string = resolve(workingDirectory);
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
  }