trekoon 0.2.0 → 0.2.4

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.
@@ -1,43 +1,172 @@
1
- import { parseArgs } from "./arg-parser";
1
+ import {
2
+ findUnknownOption,
3
+ isValidCompactTempKey,
4
+ parseArgs,
5
+ parseCompactEntityRef,
6
+ parseCompactFields,
7
+ readMissingOptionValue,
8
+ readOptions,
9
+ readUnexpectedPositionals,
10
+ suggestOptions,
11
+ } from "./arg-parser";
12
+ import { unexpectedFailureResult } from "./error-utils";
2
13
 
3
14
  import { MutationService } from "../domain/mutation-service";
4
15
  import { TrackerDomain } from "../domain/tracker-domain";
5
- import { DomainError } from "../domain/types";
16
+ import {
17
+ COMPACT_TEMP_KEY_PREFIX,
18
+ type CompactBatchResultContract,
19
+ type CompactDependencySpec,
20
+ type CompactEntityRef,
21
+ } from "../domain/types";
6
22
  import { failResult, okResult } from "../io/output";
7
23
  import { type CliContext, type CliResult } from "../runtime/command-types";
8
- import { openTrekoonDatabase } from "../storage/database";
24
+ import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
9
25
 
10
26
  function failFromError(error: unknown): CliResult {
11
- if (error instanceof DomainError) {
12
- return failResult({
13
- command: "dep",
14
- human: error.message,
15
- data: {
16
- code: error.code,
17
- ...(error.details ?? {}),
18
- },
19
- error: {
20
- code: error.code,
21
- message: error.message,
22
- },
23
- });
24
- }
25
-
26
- return failResult({
27
+ return unexpectedFailureResult(error, {
27
28
  command: "dep",
28
29
  human: "Unexpected dep command failure",
29
- data: {},
30
+ });
31
+ }
32
+
33
+ const ADD_MANY_OPTIONS = ["dep"] as const;
34
+
35
+ function unknownOption(command: string, option: string, allowedOptions: readonly string[]): CliResult {
36
+ const suggestions = suggestOptions(option, allowedOptions).map((suggestion) => `--${suggestion}`);
37
+ const suggestionMessage = suggestions.length > 0 ? ` Did you mean ${suggestions.join(" or ")}?` : "";
38
+ return failResult({
39
+ command,
40
+ human: `Unknown option --${option}.${suggestionMessage}`,
41
+ data: {
42
+ option: `--${option}`,
43
+ allowedOptions: allowedOptions.map((allowedOption) => `--${allowedOption}`),
44
+ suggestions,
45
+ },
46
+ error: {
47
+ code: "unknown_option",
48
+ message: `Unknown option --${option}`,
49
+ },
50
+ });
51
+ }
52
+
53
+ function failMissingOptionValue(command: string, option: string): CliResult {
54
+ return failResult({
55
+ command,
56
+ human: `Option --${option} requires a value.`,
57
+ data: {
58
+ code: "invalid_input",
59
+ option,
60
+ },
61
+ error: {
62
+ code: "invalid_input",
63
+ message: `Option --${option} requires a value`,
64
+ },
65
+ });
66
+ }
67
+
68
+ function failBatchSpec(command: string, human: string, data: Record<string, unknown>): CliResult {
69
+ return failResult({
70
+ command,
71
+ human,
72
+ data,
30
73
  error: {
31
- code: "internal_error",
32
- message: "Unexpected dep command failure",
74
+ code: "invalid_input",
75
+ message: human,
33
76
  },
34
77
  });
35
78
  }
36
79
 
80
+ function failUnexpectedPositionals(command: string, unexpected: readonly string[]): CliResult {
81
+ return failBatchSpec(command, `Unexpected positional arguments: ${unexpected.join(", ")}.`, {
82
+ unexpectedPositionals: unexpected,
83
+ });
84
+ }
85
+
86
+ function validateCompactEntityRef(index: number, rawSpec: string, label: string, reference: CompactEntityRef): CliResult | undefined {
87
+ if (reference.kind === "temp_key" && !isValidCompactTempKey(reference.tempKey)) {
88
+ return failBatchSpec("dep.add-many", `${label} in --dep spec ${index + 1} must use ${COMPACT_TEMP_KEY_PREFIX}<temp-key> with letters, numbers, dot, dash, or underscore.`, {
89
+ option: "dep",
90
+ index,
91
+ rawSpec,
92
+ reference,
93
+ });
94
+ }
95
+
96
+ if (reference.kind === "id" && reference.id.trim().length === 0) {
97
+ return failBatchSpec("dep.add-many", `${label} in --dep spec ${index + 1} is required.`, {
98
+ option: "dep",
99
+ index,
100
+ rawSpec,
101
+ reference,
102
+ });
103
+ }
104
+
105
+ return undefined;
106
+ }
107
+
108
+ function parseDependencySpecs(rawSpecs: readonly string[]): { specs: CompactDependencySpec[]; error?: CliResult } {
109
+ const specs: CompactDependencySpec[] = [];
110
+
111
+ for (const [index, rawSpec] of rawSpecs.entries()) {
112
+ const parsed = parseCompactFields(rawSpec);
113
+ if (parsed.invalidEscape !== null) {
114
+ return {
115
+ specs: [],
116
+ error: failBatchSpec("dep.add-many", `Invalid escape sequence ${parsed.invalidEscape} in --dep spec ${index + 1}.`, {
117
+ option: "dep",
118
+ index,
119
+ rawSpec,
120
+ }),
121
+ };
122
+ }
123
+
124
+ if (parsed.hasDanglingEscape) {
125
+ return {
126
+ specs: [],
127
+ error: failBatchSpec("dep.add-many", `Trailing escape in --dep spec ${index + 1}.`, {
128
+ option: "dep",
129
+ index,
130
+ rawSpec,
131
+ }),
132
+ };
133
+ }
134
+
135
+ if (parsed.fields.length !== 2) {
136
+ return {
137
+ specs: [],
138
+ error: failBatchSpec("dep.add-many", `Dependency specs must use <source-ref>|<depends-on-ref> in --dep spec ${index + 1}.`, {
139
+ option: "dep",
140
+ index,
141
+ rawSpec,
142
+ fields: parsed.fields,
143
+ }),
144
+ };
145
+ }
146
+
147
+ const source = parseCompactEntityRef(parsed.fields[0] ?? "");
148
+ const sourceError = validateCompactEntityRef(index, rawSpec, "Source ref", source);
149
+ if (sourceError !== undefined) {
150
+ return { specs: [], error: sourceError };
151
+ }
152
+
153
+ const dependsOn = parseCompactEntityRef(parsed.fields[1] ?? "");
154
+ const dependsOnError = validateCompactEntityRef(index, rawSpec, "Depends-on ref", dependsOn);
155
+ if (dependsOnError !== undefined) {
156
+ return { specs: [], error: dependsOnError };
157
+ }
158
+
159
+ specs.push({ source, dependsOn });
160
+ }
161
+
162
+ return { specs };
163
+ }
164
+
37
165
  export async function runDep(context: CliContext): Promise<CliResult> {
38
- const database = openTrekoonDatabase(context.cwd);
166
+ let database: TrekoonDatabase | undefined;
39
167
 
40
168
  try {
169
+ database = openTrekoonDatabase(context.cwd);
41
170
  const parsed = parseArgs(context.args);
42
171
  const subcommand: string | undefined = parsed.positional[0];
43
172
  const sourceId: string = parsed.positional[1] ?? "";
@@ -55,6 +184,49 @@ export async function runDep(context: CliContext): Promise<CliResult> {
55
184
  data: { dependency },
56
185
  });
57
186
  }
187
+ case "add-many": {
188
+ const addManyUnknownOption = findUnknownOption(parsed, ADD_MANY_OPTIONS);
189
+ if (addManyUnknownOption !== undefined) {
190
+ return unknownOption("dep.add-many", addManyUnknownOption, ADD_MANY_OPTIONS);
191
+ }
192
+
193
+ const missingAddManyOption = readMissingOptionValue(parsed.missingOptionValues, "dep");
194
+ if (missingAddManyOption !== undefined) {
195
+ return failMissingOptionValue("dep.add-many", missingAddManyOption);
196
+ }
197
+
198
+ const unexpectedPositionals = readUnexpectedPositionals(parsed, 1);
199
+ if (unexpectedPositionals.length > 0) {
200
+ return failUnexpectedPositionals("dep.add-many", unexpectedPositionals);
201
+ }
202
+
203
+ const rawSpecs = readOptions(parsed.optionEntries, "dep");
204
+ if (rawSpecs.length === 0) {
205
+ return failBatchSpec("dep.add-many", "Provide at least one --dep spec.", {
206
+ option: "dep",
207
+ });
208
+ }
209
+
210
+ const specResult = parseDependencySpecs(rawSpecs);
211
+ if (specResult.error !== undefined) {
212
+ return specResult.error;
213
+ }
214
+
215
+ const created = mutations.addDependencyBatch({
216
+ specs: specResult.specs,
217
+ });
218
+ const result: CompactBatchResultContract = created.result;
219
+ return okResult({
220
+ command: "dep.add-many",
221
+ human: `Added ${created.dependencies.length} dependenc${created.dependencies.length === 1 ? "y" : "ies"}: ${created.dependencies
222
+ .map((dependency) => `${dependency.sourceId} -> ${dependency.dependsOnId}`)
223
+ .join("\n")}`,
224
+ data: {
225
+ dependencies: created.dependencies,
226
+ result,
227
+ },
228
+ });
229
+ }
58
230
  case "remove": {
59
231
  const removed: number = mutations.removeDependency(sourceId, dependsOnId);
60
232
 
@@ -108,7 +280,7 @@ export async function runDep(context: CliContext): Promise<CliResult> {
108
280
  default:
109
281
  return failResult({
110
282
  command: "dep",
111
- human: "Usage: trekoon dep <add|remove|list|reverse>",
283
+ human: "Usage: trekoon dep <add|add-many|remove|list|reverse>",
112
284
  data: {
113
285
  args: context.args,
114
286
  },
@@ -121,6 +293,6 @@ export async function runDep(context: CliContext): Promise<CliResult> {
121
293
  } catch (error: unknown) {
122
294
  return failFromError(error);
123
295
  } finally {
124
- database.close();
296
+ database?.close();
125
297
  }
126
298
  }