trekoon 0.2.7 → 0.2.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.
Files changed (45) hide show
  1. package/README.md +60 -0
  2. package/docs/commands.md +100 -0
  3. package/docs/quickstart.md +74 -1
  4. package/package.json +2 -1
  5. package/src/board/assets/app.js +589 -0
  6. package/src/board/assets/components/ClampedText.js +31 -0
  7. package/src/board/assets/components/Component.js +271 -0
  8. package/src/board/assets/components/ConfirmDialog.js +81 -0
  9. package/src/board/assets/components/EpicRow.js +64 -0
  10. package/src/board/assets/components/EpicsOverview.js +80 -0
  11. package/src/board/assets/components/Inspector.js +335 -0
  12. package/src/board/assets/components/Notice.js +80 -0
  13. package/src/board/assets/components/SubtaskModal.js +100 -0
  14. package/src/board/assets/components/TaskCard.js +82 -0
  15. package/src/board/assets/components/TaskModal.js +99 -0
  16. package/src/board/assets/components/TopBar.js +167 -0
  17. package/src/board/assets/components/Workspace.js +308 -0
  18. package/src/board/assets/components/assetMap.js +80 -0
  19. package/src/board/assets/components/helpers.js +244 -0
  20. package/src/board/assets/fonts/inter-latin.woff2 +0 -0
  21. package/src/board/assets/fonts/material-symbols-rounded.woff2 +0 -0
  22. package/src/board/assets/index.html +39 -0
  23. package/src/board/assets/main.js +11 -0
  24. package/src/board/assets/manifest.json +12 -0
  25. package/src/board/assets/runtime/delegation.js +309 -0
  26. package/src/board/assets/state/actions.js +454 -0
  27. package/src/board/assets/state/api.js +281 -0
  28. package/src/board/assets/state/store.js +472 -0
  29. package/src/board/assets/state/url.js +184 -0
  30. package/src/board/assets/state/utils.js +222 -0
  31. package/src/board/assets/styles/board.css +1811 -0
  32. package/src/board/assets/styles/fonts.css +22 -0
  33. package/src/board/install.ts +196 -0
  34. package/src/board/open-browser.ts +131 -0
  35. package/src/board/routes.ts +308 -0
  36. package/src/board/server.ts +185 -0
  37. package/src/board/snapshot.ts +277 -0
  38. package/src/board/types.ts +43 -0
  39. package/src/commands/board.ts +158 -0
  40. package/src/commands/help.ts +21 -0
  41. package/src/commands/init.ts +29 -0
  42. package/src/domain/mutation-service.ts +40 -0
  43. package/src/domain/tracker-domain.ts +11 -3
  44. package/src/runtime/cli-shell.ts +5 -0
  45. package/src/storage/path.ts +36 -0
@@ -1,5 +1,7 @@
1
1
  import { unexpectedFailureResult } from "./error-utils";
2
2
 
3
+ import { ensureBoardInstalled } from "../board/install";
4
+ import { BoardInstallError } from "../board/types";
3
5
  import { DomainError } from "../domain/types";
4
6
  import { failResult, okResult } from "../io/output";
5
7
  import { type CliContext, type CliResult } from "../runtime/command-types";
@@ -80,6 +82,11 @@ export async function runInit(context: CliContext): Promise<CliResult> {
80
82
  try {
81
83
  database = openTrekoonDatabase(context.cwd);
82
84
  const diagnostics = database.diagnostics;
85
+ const bundledAssetRoot: string | undefined = process.env.TREKOON_BOARD_ASSET_ROOT;
86
+ const board = ensureBoardInstalled({
87
+ workingDirectory: context.cwd,
88
+ ...(bundledAssetRoot === undefined ? {} : { bundledAssetRoot }),
89
+ });
83
90
  const humanLines: string[] = [
84
91
  "Trekoon initialized.",
85
92
  `Storage mode: ${diagnostics.storageMode}`,
@@ -87,6 +94,8 @@ export async function runInit(context: CliContext): Promise<CliResult> {
87
94
  `Shared storage root: ${diagnostics.sharedStorageRoot}`,
88
95
  `Storage directory: ${database.paths.storageDir}`,
89
96
  `Database file: ${database.paths.databaseFile}`,
97
+ `Board assets: ${board.action}`,
98
+ `Board runtime root: ${board.paths.runtimeRoot}`,
90
99
  ...buildRecoverySummary(database),
91
100
  ];
92
101
 
@@ -101,6 +110,11 @@ export async function runInit(context: CliContext): Promise<CliResult> {
101
110
  sharedStorageRoot: diagnostics.sharedStorageRoot,
102
111
  storageDir: database.paths.storageDir,
103
112
  databaseFile: database.paths.databaseFile,
113
+ board: {
114
+ action: board.action,
115
+ paths: board.paths,
116
+ manifest: board.manifest,
117
+ },
104
118
  legacyStateDetected: diagnostics.legacyStateDetected,
105
119
  recoveryRequired: diagnostics.recoveryRequired,
106
120
  recoveryStatus: diagnostics.recoveryStatus,
@@ -120,6 +134,21 @@ export async function runInit(context: CliContext): Promise<CliResult> {
120
134
  }
121
135
  }
122
136
 
137
+ if (error instanceof BoardInstallError) {
138
+ return failResult({
139
+ command: "init",
140
+ human: error.message,
141
+ data: {
142
+ code: error.code,
143
+ ...error.details,
144
+ },
145
+ error: {
146
+ code: error.code,
147
+ message: error.message,
148
+ },
149
+ });
150
+ }
151
+
123
152
  return unexpectedFailureResult(error, {
124
153
  command: "init",
125
154
  human: "Unexpected init command failure",
@@ -18,6 +18,7 @@ import {
18
18
  type SearchField,
19
19
  type SearchNode,
20
20
  type SearchSummary,
21
+ type StatusCascadeBlocker,
21
22
  type StatusCascadePlan,
22
23
  type SubtaskRecord,
23
24
  type TaskRecord,
@@ -401,6 +402,45 @@ export class MutationService {
401
402
  })();
402
403
  }
403
404
 
405
+ describeError(error: unknown): string | undefined {
406
+ if (!(error instanceof DomainError) || error.code !== "dependency_blocked") {
407
+ return undefined;
408
+ }
409
+
410
+ const details = error.details as Record<string, unknown> | undefined;
411
+ const unresolvedDependencies = Array.isArray(details?.unresolvedDependencies)
412
+ ? details.unresolvedDependencies
413
+ : [];
414
+ if (unresolvedDependencies.length > 0) {
415
+ const blockers = unresolvedDependencies
416
+ .map((dependency) => {
417
+ if (!dependency || typeof dependency !== "object") {
418
+ return null;
419
+ }
420
+
421
+ const id = typeof dependency.id === "string" ? dependency.id : "unknown";
422
+ const kind = typeof dependency.kind === "string" ? dependency.kind : "dependency";
423
+ const status = typeof dependency.status === "string" ? dependency.status : "unknown";
424
+ return `${kind} ${id} is still ${status}`;
425
+ })
426
+ .filter((value): value is string => value !== null);
427
+
428
+ if (blockers.length > 0) {
429
+ return `Resolve dependencies first: ${blockers.join("; ")}.`;
430
+ }
431
+ }
432
+
433
+ const cascadeBlockers = Array.isArray(details?.blockers) ? details.blockers as StatusCascadeBlocker[] : [];
434
+ if (cascadeBlockers.length > 0) {
435
+ const blockers = cascadeBlockers.map((blocker) =>
436
+ `${blocker.sourceKind} ${blocker.sourceId} is blocked by ${blocker.dependsOnKind} ${blocker.dependsOnId} (${blocker.dependsOnStatus})`
437
+ );
438
+ return `Resolve dependencies first: ${blockers.join("; ")}.`;
439
+ }
440
+
441
+ return undefined;
442
+ }
443
+
404
444
  previewEpicReplacement(
405
445
  epicId: string,
406
446
  searchText: string,
@@ -138,6 +138,14 @@ function normalizeStatus(value: string | undefined): string {
138
138
  return assertNonEmpty("status", value);
139
139
  }
140
140
 
141
+ function normalizeSubtaskDescription(value: string | undefined): string {
142
+ if (value === undefined) {
143
+ return "";
144
+ }
145
+
146
+ return value.trim();
147
+ }
148
+
141
149
  function mapEpic(row: EpicRow): EpicRecord {
142
150
  return {
143
151
  id: row.id,
@@ -431,7 +439,7 @@ export class TrackerDomain {
431
439
  const id: string = randomUUID();
432
440
  const taskId: string = assertNonEmpty("taskId", input.taskId);
433
441
  const title: string = assertNonEmpty("title", input.title);
434
- const description: string = input.description === undefined ? "" : assertNonEmpty("description", input.description);
442
+ const description: string = normalizeSubtaskDescription(input.description);
435
443
  const status: string = normalizeStatus(input.status);
436
444
 
437
445
  this.getTaskOrThrow(taskId);
@@ -457,7 +465,7 @@ export class TrackerDomain {
457
465
  tempKey: assertNonEmpty("tempKey", spec.tempKey),
458
466
  taskId,
459
467
  title: assertNonEmpty("title", spec.title),
460
- description: spec.description === undefined ? "" : assertNonEmpty("description", spec.description),
468
+ description: normalizeSubtaskDescription(spec.description),
461
469
  status: normalizeStatus(spec.status),
462
470
  };
463
471
  });
@@ -574,7 +582,7 @@ export class TrackerDomain {
574
582
  const existing: SubtaskRecord = this.getSubtaskOrThrow(id);
575
583
  const nextTitle: string = input.title !== undefined ? assertNonEmpty("title", input.title) : existing.title;
576
584
  const nextDescription: string =
577
- input.description !== undefined ? assertNonEmpty("description", input.description) : existing.description;
585
+ input.description !== undefined ? normalizeSubtaskDescription(input.description) : existing.description;
578
586
  const nextStatus: string = input.status !== undefined ? assertNonEmpty("status", input.status) : existing.status;
579
587
  this.assertNoUnresolvedDependenciesForStatusTransition(id, "subtask", existing.status, nextStatus);
580
588
  const now: number = Date.now();
@@ -1,3 +1,4 @@
1
+ import { runBoard } from "../commands/board";
1
2
  import { runHelp } from "../commands/help";
2
3
  import { runDep } from "../commands/dep";
3
4
  import { runEpic } from "../commands/epic";
@@ -19,6 +20,7 @@ import { resolveStoragePaths } from "../storage/path";
19
20
 
20
21
  const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
21
22
  "help",
23
+ "board",
22
24
  "init",
23
25
  "quickstart",
24
26
  "session",
@@ -334,6 +336,9 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
334
336
  case "help":
335
337
  result = await runHelp(context);
336
338
  break;
339
+ case "board":
340
+ result = await runBoard(context);
341
+ break;
337
342
  case "init":
338
343
  result = await runInit(context);
339
344
  break;
@@ -4,6 +4,9 @@ import { resolve } from "node:path";
4
4
 
5
5
  export const TREKOON_STORAGE_DIRNAME = ".trekoon";
6
6
  export const TREKOON_DATABASE_FILENAME = "trekoon.db";
7
+ export const TREKOON_BOARD_DIRNAME = "board";
8
+ export const TREKOON_BOARD_ENTRY_FILENAME = "index.html";
9
+ export const TREKOON_BOARD_MANIFEST_FILENAME = "manifest.json";
7
10
 
8
11
  export function resolveLegacyWorktreeStorageDir(worktreeRoot: string): string {
9
12
  return resolve(worktreeRoot, TREKOON_STORAGE_DIRNAME);
@@ -15,6 +18,18 @@ export function resolveLegacyWorktreeDatabaseFile(worktreeRoot: string): string
15
18
 
16
19
  export type StorageMode = "cwd" | "git_common_dir";
17
20
 
21
+ export function resolveBoardStorageDir(storageDir: string): string {
22
+ return resolve(storageDir, TREKOON_BOARD_DIRNAME);
23
+ }
24
+
25
+ export function resolveBoardEntryFile(storageDir: string): string {
26
+ return resolve(resolveBoardStorageDir(storageDir), TREKOON_BOARD_ENTRY_FILENAME);
27
+ }
28
+
29
+ export function resolveBoardManifestFile(storageDir: string): string {
30
+ return resolve(resolveBoardStorageDir(storageDir), TREKOON_BOARD_MANIFEST_FILENAME);
31
+ }
32
+
18
33
  export interface StoragePaths {
19
34
  readonly invocationCwd: string;
20
35
  readonly storageMode: StorageMode;
@@ -23,6 +38,9 @@ export interface StoragePaths {
23
38
  readonly sharedStorageRoot: string;
24
39
  readonly storageDir: string;
25
40
  readonly databaseFile: string;
41
+ readonly boardDir: string;
42
+ readonly boardEntryFile: string;
43
+ readonly boardManifestFile: string;
26
44
  readonly diagnostics: StoragePathDiagnostics;
27
45
  }
28
46
 
@@ -35,6 +53,9 @@ export interface StoragePathIssue {
35
53
  readonly worktreeRoot: string;
36
54
  readonly sharedStorageRoot: string;
37
55
  readonly databaseFile: string;
56
+ readonly boardDir: string;
57
+ readonly boardEntryFile: string;
58
+ readonly boardManifestFile: string;
38
59
  }
39
60
 
40
61
  export interface StoragePathDiagnostics {
@@ -44,6 +65,9 @@ export interface StoragePathDiagnostics {
44
65
  readonly worktreeRoot: string;
45
66
  readonly sharedStorageRoot: string;
46
67
  readonly databaseFile: string;
68
+ readonly boardDir: string;
69
+ readonly boardEntryFile: string;
70
+ readonly boardManifestFile: string;
47
71
  readonly warnings: readonly StoragePathIssue[];
48
72
  readonly errors: readonly StoragePathIssue[];
49
73
  }
@@ -76,6 +100,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
76
100
  const sharedStorageRoot: string = repoCommonDir ? realpathSync(resolve(repoCommonDir, "..")) : invocationCwd;
77
101
  const storageDir: string = resolve(sharedStorageRoot, TREKOON_STORAGE_DIRNAME);
78
102
  const databaseFile: string = resolve(storageDir, TREKOON_DATABASE_FILENAME);
103
+ const boardDir: string = resolveBoardStorageDir(storageDir);
104
+ const boardEntryFile: string = resolveBoardEntryFile(storageDir);
105
+ const boardManifestFile: string = resolveBoardManifestFile(storageDir);
79
106
  const warnings: StoragePathIssue[] = [];
80
107
 
81
108
  const createIssue = (code: string, message: string): StoragePathIssue => ({
@@ -87,6 +114,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
87
114
  worktreeRoot,
88
115
  sharedStorageRoot,
89
116
  databaseFile,
117
+ boardDir,
118
+ boardEntryFile,
119
+ boardManifestFile,
90
120
  });
91
121
 
92
122
  if (invocationCwd !== worktreeRoot) {
@@ -111,6 +141,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
111
141
  worktreeRoot,
112
142
  sharedStorageRoot,
113
143
  databaseFile,
144
+ boardDir,
145
+ boardEntryFile,
146
+ boardManifestFile,
114
147
  warnings,
115
148
  errors: [],
116
149
  };
@@ -123,6 +156,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
123
156
  sharedStorageRoot,
124
157
  storageDir,
125
158
  databaseFile,
159
+ boardDir,
160
+ boardEntryFile,
161
+ boardManifestFile,
126
162
  diagnostics,
127
163
  };
128
164
  }