@frehilm/ordna-core 0.1.4 → 0.2.0
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/README.md +34 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -1
- package/dist/config.js.map +1 -1
- package/dist/git.d.ts +10 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +14 -27
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +18 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/storage/auto-detect.d.ts +82 -0
- package/dist/storage/auto-detect.d.ts.map +1 -0
- package/dist/storage/auto-detect.js +158 -0
- package/dist/storage/auto-detect.js.map +1 -0
- package/dist/storage/auto-push.d.ts +36 -0
- package/dist/storage/auto-push.d.ts.map +1 -0
- package/dist/storage/auto-push.js +100 -0
- package/dist/storage/auto-push.js.map +1 -0
- package/dist/storage/backend.d.ts +86 -0
- package/dist/storage/backend.d.ts.map +1 -0
- package/dist/storage/backend.js +14 -0
- package/dist/storage/backend.js.map +1 -0
- package/dist/storage/backends/file.d.ts +31 -0
- package/dist/storage/backends/file.d.ts.map +1 -0
- package/dist/storage/backends/file.js +213 -0
- package/dist/storage/backends/file.js.map +1 -0
- package/dist/storage/backends/hybrid.d.ts +39 -0
- package/dist/storage/backends/hybrid.d.ts.map +1 -0
- package/dist/storage/backends/hybrid.js +270 -0
- package/dist/storage/backends/hybrid.js.map +1 -0
- package/dist/storage/backends/namespace.d.ts +55 -0
- package/dist/storage/backends/namespace.d.ts.map +1 -0
- package/dist/storage/backends/namespace.js +907 -0
- package/dist/storage/backends/namespace.js.map +1 -0
- package/dist/storage/file-io.d.ts +38 -0
- package/dist/storage/file-io.d.ts.map +1 -0
- package/dist/storage/file-io.js +69 -0
- package/dist/storage/file-io.js.map +1 -0
- package/dist/storage/git-ref.d.ts +77 -0
- package/dist/storage/git-ref.d.ts.map +1 -0
- package/dist/storage/git-ref.js +184 -0
- package/dist/storage/git-ref.js.map +1 -0
- package/dist/storage/markdown.d.ts +17 -0
- package/dist/storage/markdown.d.ts.map +1 -0
- package/dist/storage/markdown.js +17 -0
- package/dist/storage/markdown.js.map +1 -0
- package/dist/storage/sync-ref.d.ts +82 -0
- package/dist/storage/sync-ref.d.ts.map +1 -0
- package/dist/storage/sync-ref.js +191 -0
- package/dist/storage/sync-ref.js.map +1 -0
- package/dist/store.d.ts +56 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +93 -115
- package/dist/store.js.map +1 -1
- package/dist/watcher.d.ts +33 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +12 -30
- package/dist/watcher.js.map +1 -1
- 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"}
|