@frehilm/ordna-core 0.1.4 → 0.2.1

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 (65) hide show
  1. package/README.md +34 -0
  2. package/dist/config.d.ts +26 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +41 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/git.d.ts +10 -0
  7. package/dist/git.d.ts.map +1 -1
  8. package/dist/git.js +14 -27
  9. package/dist/git.js.map +1 -1
  10. package/dist/index.d.ts +3 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/schema.d.ts +18 -1
  15. package/dist/schema.d.ts.map +1 -1
  16. package/dist/schema.js.map +1 -1
  17. package/dist/storage/auto-detect.d.ts +82 -0
  18. package/dist/storage/auto-detect.d.ts.map +1 -0
  19. package/dist/storage/auto-detect.js +158 -0
  20. package/dist/storage/auto-detect.js.map +1 -0
  21. package/dist/storage/auto-push.d.ts +36 -0
  22. package/dist/storage/auto-push.d.ts.map +1 -0
  23. package/dist/storage/auto-push.js +100 -0
  24. package/dist/storage/auto-push.js.map +1 -0
  25. package/dist/storage/backend.d.ts +86 -0
  26. package/dist/storage/backend.d.ts.map +1 -0
  27. package/dist/storage/backend.js +14 -0
  28. package/dist/storage/backend.js.map +1 -0
  29. package/dist/storage/backends/file.d.ts +31 -0
  30. package/dist/storage/backends/file.d.ts.map +1 -0
  31. package/dist/storage/backends/file.js +213 -0
  32. package/dist/storage/backends/file.js.map +1 -0
  33. package/dist/storage/backends/hybrid.d.ts +39 -0
  34. package/dist/storage/backends/hybrid.d.ts.map +1 -0
  35. package/dist/storage/backends/hybrid.js +270 -0
  36. package/dist/storage/backends/hybrid.js.map +1 -0
  37. package/dist/storage/backends/namespace.d.ts +55 -0
  38. package/dist/storage/backends/namespace.d.ts.map +1 -0
  39. package/dist/storage/backends/namespace.js +907 -0
  40. package/dist/storage/backends/namespace.js.map +1 -0
  41. package/dist/storage/file-io.d.ts +38 -0
  42. package/dist/storage/file-io.d.ts.map +1 -0
  43. package/dist/storage/file-io.js +69 -0
  44. package/dist/storage/file-io.js.map +1 -0
  45. package/dist/storage/git-ref.d.ts +77 -0
  46. package/dist/storage/git-ref.d.ts.map +1 -0
  47. package/dist/storage/git-ref.js +184 -0
  48. package/dist/storage/git-ref.js.map +1 -0
  49. package/dist/storage/markdown.d.ts +17 -0
  50. package/dist/storage/markdown.d.ts.map +1 -0
  51. package/dist/storage/markdown.js +17 -0
  52. package/dist/storage/markdown.js.map +1 -0
  53. package/dist/storage/sync-ref.d.ts +82 -0
  54. package/dist/storage/sync-ref.d.ts.map +1 -0
  55. package/dist/storage/sync-ref.js +191 -0
  56. package/dist/storage/sync-ref.js.map +1 -0
  57. package/dist/store.d.ts +56 -8
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/store.js +93 -115
  60. package/dist/store.js.map +1 -1
  61. package/dist/watcher.d.ts +33 -1
  62. package/dist/watcher.d.ts.map +1 -1
  63. package/dist/watcher.js +12 -30
  64. package/dist/watcher.js.map +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,100 @@
1
+ const DEBOUNCE_MS = 50;
2
+ /**
3
+ * Fire-and-forget push coalescer. Used by hybrid and namespace modes
4
+ * to keep their refs synced with `origin` without surprising the
5
+ * user with synchronous network calls on every mutation.
6
+ *
7
+ * Semantics:
8
+ * - `schedule()` is sync and never throws. Multiple rapid calls
9
+ * within ~50ms collapse into a single push.
10
+ * - Pushes run in the background. Errors are logged to stderr; the
11
+ * user's command stays successful.
12
+ * - If no `origin` remote is configured at first push time, the
13
+ * queue becomes a permanent no-op (decided once, cached forever).
14
+ * - `flush()` (called from `dispose()`) awaits any pending or
15
+ * in-flight push so the process can exit cleanly.
16
+ */
17
+ export class PushQueue {
18
+ git;
19
+ refspec;
20
+ label;
21
+ #timer = null;
22
+ #current = null;
23
+ #pending = false;
24
+ #remoteChecked = false;
25
+ #remoteExists = false;
26
+ constructor(git, refspec, label = "ordna") {
27
+ this.git = git;
28
+ this.refspec = refspec;
29
+ this.label = label;
30
+ }
31
+ /**
32
+ * Mark "a push is wanted." Coalesces with any other calls in the
33
+ * next 50ms. Cheap and synchronous — safe to call from any
34
+ * mutation site.
35
+ */
36
+ schedule() {
37
+ if (this.#timer)
38
+ clearTimeout(this.#timer);
39
+ const t = setTimeout(() => {
40
+ this.#timer = null;
41
+ this.#trigger();
42
+ }, DEBOUNCE_MS);
43
+ // Don't keep the Node process alive just for the debounce timer —
44
+ // the host (CLI command, web server) is what owns the lifetime.
45
+ t.unref?.();
46
+ this.#timer = t;
47
+ }
48
+ /**
49
+ * Await any pending or in-flight push. Called from
50
+ * `HybridBackend.dispose()` so the last mutation's push completes
51
+ * before the process exits.
52
+ */
53
+ async flush() {
54
+ // Fire any pending debounce timer immediately.
55
+ if (this.#timer) {
56
+ clearTimeout(this.#timer);
57
+ this.#timer = null;
58
+ this.#trigger();
59
+ }
60
+ // Drain. Each iteration awaits the current push; once it
61
+ // resolves, its `.finally` may have started another push if
62
+ // something was queued during the run. Loop until quiet.
63
+ while (this.#current) {
64
+ await this.#current;
65
+ }
66
+ }
67
+ #trigger() {
68
+ if (this.#current) {
69
+ // A push is mid-flight. Mark "do another after this one."
70
+ // The current run's `.finally` picks up the flag.
71
+ this.#pending = true;
72
+ return;
73
+ }
74
+ this.#startPush();
75
+ }
76
+ #startPush() {
77
+ this.#pending = false;
78
+ this.#current = this.#runOnce().finally(() => {
79
+ this.#current = null;
80
+ if (this.#pending)
81
+ this.#startPush();
82
+ });
83
+ }
84
+ async #runOnce() {
85
+ if (!this.#remoteChecked) {
86
+ this.#remoteChecked = true;
87
+ this.#remoteExists = await this.git.hasRemote();
88
+ }
89
+ if (!this.#remoteExists)
90
+ return;
91
+ try {
92
+ await this.git.pushRef(this.refspec);
93
+ }
94
+ catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ console.error(`[${this.label}] push failed: ${msg}`);
97
+ }
98
+ }
99
+ }
100
+ //# sourceMappingURL=auto-push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-push.js","sourceRoot":"","sources":["../../src/storage/auto-push.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,SAAS;IAQH;IACA;IACA;IATlB,MAAM,GAAyC,IAAI,CAAC;IACpD,QAAQ,GAAyB,IAAI,CAAC;IACtC,QAAQ,GAAG,KAAK,CAAC;IACjB,cAAc,GAAG,KAAK,CAAC;IACvB,aAAa,GAAG,KAAK,CAAC;IAEtB,YACkB,GAAc,EACd,OAAe,EACf,QAAQ,OAAO;QAFf,QAAG,GAAH,GAAG,CAAW;QACd,YAAO,GAAP,OAAO,CAAQ;QACf,UAAK,GAAL,KAAK,CAAU;IAC9B,CAAC;IAEJ;;;;OAIG;IACH,QAAQ;QACP,IAAI,IAAI,CAAC,MAAM;YAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE;YACzB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,QAAQ,EAAE,CAAC;QACjB,CAAC,EAAE,WAAW,CAAC,CAAC;QAChB,kEAAkE;QAClE,gEAAgE;QAC/D,CAAuC,CAAC,KAAK,EAAE,EAAE,CAAC;QACnD,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,KAAK;QACV,+CAA+C;QAC/C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,QAAQ,EAAE,CAAC;QACjB,CAAC;QACD,yDAAyD;QACzD,4DAA4D;QAC5D,yDAAyD;QACzD,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;YACtB,MAAM,IAAI,CAAC,QAAQ,CAAC;QACrB,CAAC;IACF,CAAC;IAED,QAAQ;QACP,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,0DAA0D;YAC1D,kDAAkD;YAClD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,OAAO;QACR,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;IACnB,CAAC;IAED,UAAU;QACT,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAC5C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,IAAI,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,UAAU,EAAE,CAAC;QACtC,CAAC,CAAC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,QAAQ;QACb,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YAC1B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,aAAa,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;QACjD,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO;QAChC,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,kBAAkB,GAAG,EAAE,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;CACD"}
@@ -0,0 +1,86 @@
1
+ import type { OrdnaConfig } from "../config.js";
2
+ import type { Task, TaskCreateInput, TaskUpdateInput } from "../schema.js";
3
+ import type { TaskEventListener } from "../watcher.js";
4
+ /**
5
+ * Reserved built-in status — accepted by `update` / `move` regardless
6
+ * of whether it appears in `config.statuses`. Lives at the storage
7
+ * layer (rather than `store.ts`) so backend implementations can
8
+ * import it without a circular dependency on `store.ts`.
9
+ */
10
+ export declare const ARCHIVED_STATUS = "archived";
11
+ /** True if `status` is in `config.statuses` or is the archived sentinel. */
12
+ export declare function isKnownStatus(config: OrdnaConfig, status: string): boolean;
13
+ /**
14
+ * Storage-mode-specific options accepted by `list()`.
15
+ *
16
+ * Same shape as the public `ListTasksOptions` from `../store.ts`. Kept
17
+ * separately so the backend interface is self-contained and doesn't
18
+ * cross-import the store layer.
19
+ */
20
+ export interface ListOptions {
21
+ status?: string;
22
+ assignee?: string;
23
+ tag?: string;
24
+ }
25
+ /**
26
+ * Internal seam between core's public API and the storage layer.
27
+ *
28
+ * **NOT EXPORTED from the package.** Lives under `storage/` and is
29
+ * referenced only by `store.ts` and the backend implementations
30
+ * themselves. Future modes (hybrid, namespace) are additional classes
31
+ * in `storage/backends/` that implement this interface; there is no
32
+ * plugin loader, no dynamic import, no `@frehilm/ordna-<name>`
33
+ * resolution.
34
+ *
35
+ * Construction is cheap (no I/O). The first method call on a fresh
36
+ * backend pays the I/O cost via the lazy `init()` pattern (see
37
+ * `FileBackend.#ensureInit`).
38
+ */
39
+ export interface Backend {
40
+ readonly kind: "file" | "hybrid" | "namespace";
41
+ /**
42
+ * Lazy setup. Not called by core; each backend method awaits its own
43
+ * internal `#ensureInit()` helper which calls `init()` once and
44
+ * caches the resolved promise. This is what keeps `createContext`
45
+ * synchronous.
46
+ */
47
+ init(): Promise<void>;
48
+ /**
49
+ * Clean up any resources the backend allocated (watcher handles,
50
+ * pending pushes, etc.). Long-lived hosts (`ordna web`, the TUI)
51
+ * call this on shutdown. One-shot CLI commands don't need to call
52
+ * it — the OS reclaims everything when the process exits.
53
+ */
54
+ dispose(): Promise<void>;
55
+ list(opts?: ListOptions): Promise<Task[]>;
56
+ get(id: string): Promise<Task | null>;
57
+ create(input: TaskCreateInput): Promise<Task>;
58
+ update(id: string, patch: TaskUpdateInput): Promise<Task>;
59
+ delete(id: string): Promise<void>;
60
+ watch(listener: TaskEventListener): () => Promise<void>;
61
+ /**
62
+ * File-mode-only operation in v1. Hybrid and namespace backends
63
+ * deliberately omit this — `commitTasks(ctx)` then throws a clear
64
+ * "this backend doesn't support commit" error.
65
+ */
66
+ commit?(message?: string): Promise<void>;
67
+ /**
68
+ * Pull remote updates into the backend's storage. Only namespace
69
+ * implements this in v1 — file mode has nothing to fetch, and
70
+ * hybrid's task content rides regular `git pull` on branches while
71
+ * its state ref auto-fetches on CAS conflict during writes. The
72
+ * presence of this method is the UI capability check (web shows
73
+ * the fetch button, TUI binds `r`) — consumers should detect via
74
+ * `typeof ctx.backend.fetch === "function"`.
75
+ *
76
+ * Returns the number of refs that changed (added / updated /
77
+ * removed by the fetch) and the wall-clock duration. Both surface
78
+ * in the UI as a "fetched N refs in Xms" toast.
79
+ */
80
+ fetch?(): Promise<FetchResult>;
81
+ }
82
+ export interface FetchResult {
83
+ refsUpdated: number;
84
+ durationMs: number;
85
+ }
86
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/storage/backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEvD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,aAAa,CAAC;AAE1C,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAG1E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;IAE/C;;;;;OAKG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtB;;;;;OAKG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzB,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC,KAAK,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExD;;;;OAIG;IACH,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reserved built-in status — accepted by `update` / `move` regardless
3
+ * of whether it appears in `config.statuses`. Lives at the storage
4
+ * layer (rather than `store.ts`) so backend implementations can
5
+ * import it without a circular dependency on `store.ts`.
6
+ */
7
+ export const ARCHIVED_STATUS = "archived";
8
+ /** True if `status` is in `config.statuses` or is the archived sentinel. */
9
+ export function isKnownStatus(config, status) {
10
+ if (status === ARCHIVED_STATUS)
11
+ return true;
12
+ return config.statuses.includes(status);
13
+ }
14
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/storage/backend.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;AAE1C,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,MAAmB,EAAE,MAAc;IAChE,IAAI,MAAM,KAAK,eAAe;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { OrdnaConfig } from "../../config.js";
2
+ import type { Task, TaskCreateInput, TaskUpdateInput } from "../../schema.js";
3
+ import type { TaskEventListener } from "../../watcher.js";
4
+ import { type Backend, type ListOptions } from "../backend.js";
5
+ /**
6
+ * File-system backend. Tasks are markdown files in `<cwd>/<tasksDir>`.
7
+ * This is the default mode and the current behavior of `@frehilm/ordna-core`.
8
+ *
9
+ * Construction is cheap (no I/O). The first method call triggers
10
+ * lazy `init()` via `#ensureInit()` so `createContext` stays
11
+ * synchronous — important because the IDE that embeds core would
12
+ * otherwise have to await context construction everywhere.
13
+ */
14
+ export declare class FileBackend implements Backend {
15
+ #private;
16
+ private readonly cwd;
17
+ private readonly config;
18
+ private readonly tasksDir;
19
+ readonly kind = "file";
20
+ constructor(cwd: string, config: OrdnaConfig, tasksDir: string);
21
+ init(): Promise<void>;
22
+ dispose(): Promise<void>;
23
+ list(options?: ListOptions): Promise<Task[]>;
24
+ get(id: string): Promise<Task | null>;
25
+ create(input: TaskCreateInput): Promise<Task>;
26
+ update(id: string, patch: TaskUpdateInput): Promise<Task>;
27
+ delete(id: string): Promise<void>;
28
+ watch(listener: TaskEventListener): () => Promise<void>;
29
+ commit(message?: string): Promise<void>;
30
+ }
31
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../../src/storage/backends/file.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,eAAe,CAAC;AAqB9E;;;;;;;;GAQG;AACH,qBAAa,WAAY,YAAW,OAAO;;IAOzC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR1B,QAAQ,CAAC,IAAI,UAAU;gBAML,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM;IAG5B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IASrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAOxB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IA4BhD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAKrC,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAwC7C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCzD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvC,KAAK,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;IAkCjD,MAAM,CAAC,OAAO,SAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;CAe7D"}
@@ -0,0 +1,213 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import chokidar, {} from "chokidar";
4
+ import { isKnownStatus } from "../backend.js";
5
+ import { deleteTaskFile, ensureTasksDir, filenameFor, listTaskBytes, nextIdFromScan, readTaskBytes, writeTaskBytes, } from "../file-io.js";
6
+ import { defaultSectionsFor, parseTask, parseTaskFile, serializeTask, } from "../markdown.js";
7
+ function today() {
8
+ return new Date().toISOString().slice(0, 10);
9
+ }
10
+ /**
11
+ * File-system backend. Tasks are markdown files in `<cwd>/<tasksDir>`.
12
+ * This is the default mode and the current behavior of `@frehilm/ordna-core`.
13
+ *
14
+ * Construction is cheap (no I/O). The first method call triggers
15
+ * lazy `init()` via `#ensureInit()` so `createContext` stays
16
+ * synchronous — important because the IDE that embeds core would
17
+ * otherwise have to await context construction everywhere.
18
+ */
19
+ export class FileBackend {
20
+ cwd;
21
+ config;
22
+ tasksDir;
23
+ kind = "file";
24
+ #initPromise = null;
25
+ #activeWatchers = new Set();
26
+ constructor(cwd, config, tasksDir) {
27
+ this.cwd = cwd;
28
+ this.config = config;
29
+ this.tasksDir = tasksDir;
30
+ }
31
+ async init() {
32
+ ensureTasksDir(this.tasksDir);
33
+ }
34
+ async #ensureInit() {
35
+ if (!this.#initPromise)
36
+ this.#initPromise = this.init();
37
+ return this.#initPromise;
38
+ }
39
+ async dispose() {
40
+ const closing = [];
41
+ for (const w of this.#activeWatchers)
42
+ closing.push(w.close());
43
+ this.#activeWatchers.clear();
44
+ await Promise.all(closing);
45
+ }
46
+ async list(options = {}) {
47
+ await this.#ensureInit();
48
+ const entries = await listTaskBytes(this.tasksDir);
49
+ const tasks = [];
50
+ for (const { filePath, raw } of entries) {
51
+ try {
52
+ tasks.push(parseTask(raw, filePath));
53
+ }
54
+ catch {
55
+ // Skip malformed tasks silently; surfaced via a dedicated validator later.
56
+ }
57
+ }
58
+ let filtered = tasks;
59
+ if (options.status)
60
+ filtered = filtered.filter((t) => t.status === options.status);
61
+ if (options.assignee)
62
+ filtered = filtered.filter((t) => t.assignee === options.assignee);
63
+ if (options.tag) {
64
+ const tag = options.tag;
65
+ filtered = filtered.filter((t) => t.tags.includes(tag));
66
+ }
67
+ filtered.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
68
+ return filtered;
69
+ }
70
+ async get(id) {
71
+ const tasks = await this.list();
72
+ return tasks.find((t) => t.id === id) ?? null;
73
+ }
74
+ async create(input) {
75
+ await this.#ensureInit();
76
+ const id = nextIdFromScan(this.config, this.tasksDir);
77
+ const status = input.status ?? this.config.statuses[0];
78
+ if (!status)
79
+ throw new Error("Config has no statuses defined.");
80
+ if (!isKnownStatus(this.config, status)) {
81
+ throw new Error(`Status "${status}" is not in configured statuses.`);
82
+ }
83
+ const now = today();
84
+ const task = {
85
+ id,
86
+ title: input.title,
87
+ status,
88
+ assignee: input.assignee ?? null,
89
+ priority: input.priority ?? null,
90
+ tags: input.tags ?? [],
91
+ depends_on: input.depends_on ?? [],
92
+ created_at: now,
93
+ updated_at: now,
94
+ sections: defaultSectionsFor(this.config.schema),
95
+ extra_frontmatter: {},
96
+ filePath: "",
97
+ rawContent: "",
98
+ };
99
+ const filename = filenameFor(id, task.title, this.config.schema, this.config);
100
+ task.filePath = join(this.tasksDir, filename);
101
+ const serialized = serializeTask(task, this.config.schema);
102
+ task.rawContent = serialized;
103
+ await writeTaskBytes(task.filePath, serialized);
104
+ return task;
105
+ }
106
+ async update(id, patch) {
107
+ await this.#ensureInit();
108
+ const existing = await this.get(id);
109
+ if (!existing)
110
+ throw new Error(`Task ${id} not found.`);
111
+ const next = {
112
+ ...existing,
113
+ title: patch.title ?? existing.title,
114
+ status: patch.status ?? existing.status,
115
+ assignee: patch.assignee !== undefined ? patch.assignee : existing.assignee,
116
+ priority: patch.priority !== undefined ? patch.priority : existing.priority,
117
+ tags: patch.tags ?? existing.tags,
118
+ depends_on: patch.depends_on ?? existing.depends_on,
119
+ sections: patch.sections ?? existing.sections,
120
+ updated_at: today(),
121
+ };
122
+ if (next.status !== existing.status &&
123
+ !isKnownStatus(this.config, next.status)) {
124
+ throw new Error(`Status "${next.status}" is not in configured statuses.`);
125
+ }
126
+ const serialized = serializeTask(next, this.config.schema);
127
+ next.rawContent = serialized;
128
+ // File backend always writes filePath on create; this guard is
129
+ // defensive against a misconfigured Task arriving from outside
130
+ // the backend (e.g., a future migration script).
131
+ if (!existing.filePath) {
132
+ throw new Error(`Task ${id} has no filePath; cannot update in file mode.`);
133
+ }
134
+ await writeTaskBytes(existing.filePath, serialized);
135
+ return next;
136
+ }
137
+ async delete(id) {
138
+ await this.#ensureInit();
139
+ const task = await this.get(id);
140
+ if (!task)
141
+ throw new Error(`Task ${id} not found.`);
142
+ if (!task.filePath) {
143
+ throw new Error(`Task ${id} has no filePath; cannot delete in file mode.`);
144
+ }
145
+ await deleteTaskFile(task.filePath);
146
+ }
147
+ watch(listener) {
148
+ const watcher = chokidar.watch(this.tasksDir, {
149
+ ignoreInitial: true,
150
+ depth: 0,
151
+ persistent: true,
152
+ });
153
+ this.#activeWatchers.add(watcher);
154
+ const emitIfMarkdown = async (type, filePath) => {
155
+ if (!filePath.endsWith(".md"))
156
+ return;
157
+ try {
158
+ const task = await parseTaskFile(filePath);
159
+ listener({ type, task });
160
+ }
161
+ catch {
162
+ // Ignore partial writes or malformed files.
163
+ }
164
+ };
165
+ watcher.on("add", (path) => void emitIfMarkdown("added", path));
166
+ watcher.on("change", (path) => void emitIfMarkdown("changed", path));
167
+ watcher.on("unlink", (path) => {
168
+ if (!path.endsWith(".md"))
169
+ return;
170
+ listener({ type: "removed", filePath: path });
171
+ });
172
+ return async () => {
173
+ this.#activeWatchers.delete(watcher);
174
+ await watcher.close();
175
+ };
176
+ }
177
+ async commit(message = "chore(tasks): update") {
178
+ await this.#ensureInit();
179
+ const tasksDirArg = this.config.tasksDir;
180
+ await runGit(this.cwd, ["add", "--", tasksDirArg]);
181
+ const status = await runGit(this.cwd, [
182
+ "status",
183
+ "--porcelain",
184
+ "--",
185
+ tasksDirArg,
186
+ ]);
187
+ if (status.stdout.trim().length === 0) {
188
+ throw new Error("No task changes to commit.");
189
+ }
190
+ await runGit(this.cwd, ["commit", "-m", message, "--", tasksDirArg]);
191
+ }
192
+ }
193
+ function runGit(cwd, args) {
194
+ return new Promise((resolve, reject) => {
195
+ const proc = spawn("git", args, { cwd });
196
+ let stdout = "";
197
+ let stderr = "";
198
+ proc.stdout.on("data", (chunk) => {
199
+ stdout += chunk.toString();
200
+ });
201
+ proc.stderr.on("data", (chunk) => {
202
+ stderr += chunk.toString();
203
+ });
204
+ proc.on("error", reject);
205
+ proc.on("close", (code) => {
206
+ if (code === 0)
207
+ resolve({ stdout, stderr });
208
+ else
209
+ reject(new Error(`git ${args.join(" ")} failed (${code}): ${stderr.trim()}`));
210
+ });
211
+ });
212
+ }
213
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../../src/storage/backends/file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { OrdnaConfig } from "../../config.js";
2
+ import type { Task, TaskCreateInput, TaskUpdateInput } from "../../schema.js";
3
+ import type { TaskEventListener } from "../../watcher.js";
4
+ import { type Backend, type ListOptions } from "../backend.js";
5
+ /**
6
+ * Hybrid storage backend.
7
+ *
8
+ * Tasks stay as markdown files in `tasks/*.md` — same on-disk layout
9
+ * as `FileBackend`, so existing tooling that reads task files Just
10
+ * Works. The difference: shared metadata moves into a single git ref
11
+ * at `refs/ordna/state`. The blob there holds:
12
+ *
13
+ * - `next_id`: a CAS-bumped allocator that prevents two offline
14
+ * collaborators from both picking the same task number
15
+ * - `ops`: an append-only audit log of every create / update /
16
+ * archive / delete
17
+ *
18
+ * Every mutation does two atomic writes (one to disk, one to the
19
+ * ref) and fires a best-effort coalesced push to keep `origin` in
20
+ * sync. Reads stay file-system-only (no ref involvement in v1).
21
+ */
22
+ export declare class HybridBackend implements Backend {
23
+ #private;
24
+ private readonly cwd;
25
+ private readonly config;
26
+ private readonly tasksDir;
27
+ readonly kind = "hybrid";
28
+ constructor(cwd: string, config: OrdnaConfig, tasksDir: string);
29
+ init(): Promise<void>;
30
+ dispose(): Promise<void>;
31
+ list(options?: ListOptions): Promise<Task[]>;
32
+ get(id: string): Promise<Task | null>;
33
+ create(input: TaskCreateInput): Promise<Task>;
34
+ update(id: string, patch: TaskUpdateInput): Promise<Task>;
35
+ delete(id: string): Promise<void>;
36
+ watch(listener: TaskEventListener): () => Promise<void>;
37
+ commit(message?: string): Promise<void>;
38
+ }
39
+ //# sourceMappingURL=hybrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hybrid.d.ts","sourceRoot":"","sources":["../../../src/storage/backends/hybrid.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EACX,IAAI,EACJ,eAAe,EACf,eAAe,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAEN,KAAK,OAAO,EACZ,KAAK,WAAW,EAEhB,MAAM,eAAe,CAAC;AA6BvB;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAW3C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAZ1B,QAAQ,CAAC,IAAI,YAAY;gBAUP,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM;IAK5B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAcxB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IA2BhD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAOrC,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAgD7C,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CzD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvC,KAAK,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;IAuCjD,MAAM,CAAC,OAAO,SAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;CA2C7D"}