@sascha384/tic 1.35.0 → 2.0.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -63
- package/dist/backends/availability.js +4 -2
- package/dist/backends/availability.js.map +1 -1
- package/dist/backends/factory.d.ts +5 -3
- package/dist/backends/factory.js +28 -43
- package/dist/backends/factory.js.map +1 -1
- package/dist/backends/files/hash.d.ts +1 -0
- package/dist/backends/files/hash.js +5 -0
- package/dist/backends/files/hash.js.map +1 -0
- package/dist/backends/files/index.d.ts +48 -0
- package/dist/backends/files/index.js +174 -0
- package/dist/backends/files/index.js.map +1 -0
- package/dist/backends/files/sync.d.ts +13 -0
- package/dist/backends/files/sync.js +69 -0
- package/dist/backends/files/sync.js.map +1 -0
- package/dist/backends/jira/config.d.ts +1 -1
- package/dist/backends/jira/config.js +6 -9
- package/dist/backends/jira/config.js.map +1 -1
- package/dist/backends/types.d.ts +12 -0
- package/dist/backends/types.js +5 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/cli/commands/config.js +27 -14
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/init.js +10 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +4 -4
- package/dist/cli/commands/mcp.js +16 -25
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/index.js +16 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/commands.js +0 -6
- package/dist/commands.js.map +1 -1
- package/dist/components/Header.js +3 -2
- package/dist/components/Header.js.map +1 -1
- package/dist/components/OverlayPanel.d.ts +2 -1
- package/dist/components/OverlayPanel.js +8 -1
- package/dist/components/OverlayPanel.js.map +1 -1
- package/dist/components/Settings.js +6 -11
- package/dist/components/Settings.js.map +1 -1
- package/dist/components/StatusScreen.js +1 -1
- package/dist/components/StatusScreen.js.map +1 -1
- package/dist/components/WorkItemForm.js +6 -9
- package/dist/components/WorkItemForm.js.map +1 -1
- package/dist/components/WorkItemList.js +102 -51
- package/dist/components/WorkItemList.js.map +1 -1
- package/dist/index.js +20 -8
- package/dist/index.js.map +1 -1
- package/dist/storage/config.d.ts +61 -0
- package/dist/storage/config.js +309 -0
- package/dist/storage/config.js.map +1 -0
- package/dist/storage/db.d.ts +11 -0
- package/dist/storage/db.js +34 -0
- package/dist/storage/db.js.map +1 -0
- package/dist/storage/index.d.ts +73 -0
- package/dist/storage/index.js +966 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/mappers.d.ts +35 -0
- package/dist/storage/mappers.js +70 -0
- package/dist/storage/mappers.js.map +1 -0
- package/dist/storage/schema.d.ts +1844 -0
- package/dist/storage/schema.js +197 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/syncQueue.d.ts +13 -0
- package/dist/storage/syncQueue.js +98 -0
- package/dist/storage/syncQueue.js.map +1 -0
- package/dist/storage/undo.d.ts +22 -0
- package/dist/storage/undo.js +129 -0
- package/dist/storage/undo.js.map +1 -0
- package/dist/stores/backendDataStore.d.ts +4 -1
- package/dist/stores/backendDataStore.js +61 -40
- package/dist/stores/backendDataStore.js.map +1 -1
- package/dist/stores/configStore.d.ts +3 -1
- package/dist/stores/configStore.js +25 -65
- package/dist/stores/configStore.js.map +1 -1
- package/dist/stores/filterStore.d.ts +2 -0
- package/dist/stores/filterStore.js +7 -2
- package/dist/stores/filterStore.js.map +1 -1
- package/dist/stores/uiStore.d.ts +0 -2
- package/dist/stores/uiStore.js.map +1 -1
- package/dist/stores/undoStore.d.ts +4 -0
- package/dist/stores/undoStore.js +32 -0
- package/dist/stores/undoStore.js.map +1 -1
- package/dist/sync/SyncManager.d.ts +3 -4
- package/dist/sync/SyncManager.js +78 -37
- package/dist/sync/SyncManager.js.map +1 -1
- package/dist/sync/types.d.ts +10 -1
- package/drizzle/0000_next_vance_astro.sql +183 -0
- package/drizzle/meta/0000_snapshot.json +1188 -0
- package/drizzle/meta/_journal.json +13 -0
- package/package.json +6 -1
- package/skills/config/SKILL.md +2 -2
- package/dist/backends/local/config.d.ts +0 -38
- package/dist/backends/local/config.js +0 -42
- package/dist/backends/local/config.js.map +0 -1
- package/dist/backends/local/index.d.ts +0 -45
- package/dist/backends/local/index.js +0 -291
- package/dist/backends/local/index.js.map +0 -1
- package/dist/sync/queue.d.ts +0 -12
- package/dist/sync/queue.js +0 -56
- package/dist/sync/queue.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# tic
|
|
2
2
|
|
|
3
|
-
A terminal UI for issue tracking, built for developers who live in the terminal. Track work items across multiple backends —
|
|
3
|
+
A terminal UI for issue tracking, built for developers who live in the terminal. Track work items across multiple backends — GitHub Issues, GitLab Issues, Azure DevOps Work Items, and Jira — with local SQLite storage.
|
|
4
4
|
|
|
5
5
|
Built with TypeScript and [Ink](https://github.com/vadimdemedes/ink).
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Keyboard-driven TUI** — browse, create, edit, and manage work items without leaving the terminal
|
|
10
|
-
- **Multiple backends** —
|
|
10
|
+
- **Multiple backends** — GitHub (via `gh`), GitLab (via `glab`), Azure DevOps (via `az`), Jira (via REST API)
|
|
11
11
|
- **Automatic backend detection** — selects backend based on git remote, or configure manually
|
|
12
|
-
- **
|
|
12
|
+
- **SQLite storage** — all data stored locally in `.tic/tic.db` with optional sync to remote backends
|
|
13
13
|
- **CLI commands** — scriptable commands for all operations (`tic item list`, `tic item create`, etc.)
|
|
14
14
|
- **Work item types** — organize by epic, issue, and task (configurable)
|
|
15
15
|
- **Iterations** — group work into sprints or milestones
|
|
@@ -39,16 +39,18 @@ tic init # Initialize (auto-detects backend from git remote)
|
|
|
39
39
|
tic # Launch the TUI
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
`tic init` creates a `.tic/` directory with a SQLite database to store your work items. For GitHub, GitLab, or Azure DevOps projects, it detects the backend from the git remote automatically. You can also specify a backend explicitly:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
tic init --backend github
|
|
46
46
|
tic init --backend gitlab
|
|
47
47
|
tic init --backend azure
|
|
48
48
|
tic init --backend jira
|
|
49
|
-
tic init --backend
|
|
49
|
+
tic init --backend none
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
The TUI also auto-initializes on first run if no `.tic/` directory exists.
|
|
53
|
+
|
|
52
54
|
## Usage
|
|
53
55
|
|
|
54
56
|
### List View
|
|
@@ -123,70 +125,22 @@ Deleting an item automatically cleans up references — children have their pare
|
|
|
123
125
|
|
|
124
126
|
## Storage
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
All data lives in `.tic/` at the root of your project:
|
|
127
129
|
|
|
128
130
|
```
|
|
129
131
|
.tic/
|
|
130
|
-
├──
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
├── templates/ # Work item templates
|
|
136
|
-
│ └── bug-report.md # Template with YAML frontmatter
|
|
137
|
-
└── trash/ # Soft-deleted items (for undo)
|
|
132
|
+
├── tic.db # SQLite database (all items, config, undo log)
|
|
133
|
+
└── items/ # Markdown mirrors (for sync / human-readable export)
|
|
134
|
+
├── 1.md
|
|
135
|
+
├── 2.md
|
|
136
|
+
└── ...
|
|
138
137
|
```
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```markdown
|
|
143
|
-
---
|
|
144
|
-
id: 1
|
|
145
|
-
title: Implement user login
|
|
146
|
-
type: task
|
|
147
|
-
status: in-progress
|
|
148
|
-
iteration: sprint-1
|
|
149
|
-
priority: high
|
|
150
|
-
assignee: alice
|
|
151
|
-
labels: auth, backend
|
|
152
|
-
parent: 3
|
|
153
|
-
depends_on:
|
|
154
|
-
- 2
|
|
155
|
-
created: 2026-01-15T10:00:00.000Z
|
|
156
|
-
updated: 2026-01-20T14:30:00.000Z
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
Full description of the work item goes here.
|
|
160
|
-
|
|
161
|
-
## Comments
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
author: alice
|
|
165
|
-
date: 2026-01-18T09:00:00.000Z
|
|
166
|
-
|
|
167
|
-
Decided to use JWT tokens for this.
|
|
168
|
-
```
|
|
139
|
+
The SQLite database (`.tic/tic.db`) is the single source of truth for work items, config, templates, and undo history. When a remote backend is configured, `SyncManager` also writes markdown mirrors to `.tic/items/` via `FilesBackend`.
|
|
169
140
|
|
|
170
|
-
Configuration in
|
|
171
|
-
|
|
172
|
-
```yaml
|
|
173
|
-
types:
|
|
174
|
-
- epic
|
|
175
|
-
- issue
|
|
176
|
-
- task
|
|
177
|
-
statuses:
|
|
178
|
-
- backlog
|
|
179
|
-
- todo
|
|
180
|
-
- in-progress
|
|
181
|
-
- review
|
|
182
|
-
- done
|
|
183
|
-
iterations:
|
|
184
|
-
- default
|
|
185
|
-
current_iteration: default
|
|
186
|
-
next_id: 1
|
|
187
|
-
```
|
|
141
|
+
Configuration (types, statuses, iterations, etc.) is stored in the `project_config` table and managed via the TUI settings screen or `tic config` CLI commands.
|
|
188
142
|
|
|
189
|
-
|
|
143
|
+
If upgrading from a legacy `.tic/config.yml` setup, the database will automatically migrate the YAML config on first open.
|
|
190
144
|
|
|
191
145
|
## Claude Code Integration
|
|
192
146
|
|
|
@@ -240,7 +194,7 @@ Add `--json` to any command for machine-readable output, or `--quiet` to suppres
|
|
|
240
194
|
|
|
241
195
|
| Backend | CLI Tool | Detection |
|
|
242
196
|
|---------|----------|-----------|
|
|
243
|
-
| Local
|
|
197
|
+
| Local only (SQLite) | — | Default fallback |
|
|
244
198
|
| GitHub Issues | [`gh`](https://cli.github.com/) | `github.com` in git remote |
|
|
245
199
|
| GitLab Issues | [`glab`](https://gitlab.com/gitlab-org/cli) | `gitlab.com` in git remote |
|
|
246
200
|
| Azure DevOps Work Items | [`az`](https://learn.microsoft.com/en-us/cli/azure/) | `dev.azure.com` or `visualstudio.com` in git remote |
|
|
@@ -3,7 +3,8 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
* Maps each backend to the CLI binary it requires, or null if no CLI is needed.
|
|
4
4
|
*/
|
|
5
5
|
export const BACKEND_CLI = {
|
|
6
|
-
|
|
6
|
+
none: null,
|
|
7
|
+
filesystem: null,
|
|
7
8
|
github: 'gh',
|
|
8
9
|
gitlab: 'glab',
|
|
9
10
|
azure: 'az',
|
|
@@ -29,7 +30,8 @@ export async function checkBackendAvailability(backend) {
|
|
|
29
30
|
*/
|
|
30
31
|
export async function checkAllBackendAvailability() {
|
|
31
32
|
const backends = [
|
|
32
|
-
'
|
|
33
|
+
'none',
|
|
34
|
+
'filesystem',
|
|
33
35
|
'github',
|
|
34
36
|
'gitlab',
|
|
35
37
|
'azure',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"availability.js","sourceRoot":"","sources":["../../src/backends/availability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG9C;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAuC;IAC7D,
|
|
1
|
+
{"version":3,"file":"availability.js","sourceRoot":"","sources":["../../src/backends/availability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG9C;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAuC;IAC7D,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,MAAM;IACd,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,IAAI;CACX,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAoB;IAEpB,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAEjC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,QAAQ,CACpB,MAAM,EACN,CAAC,WAAW,CAAC,EACb,EAAE,OAAO,EAAE,IAAI,EAAE,EACjB,CAAC,KAAK,EAAE,EAAE;YACR,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC,CACF,CAAC;QACF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B;IAG/C,MAAM,QAAQ,GAAkB;QAC9B,MAAM;QACN,YAAY;QACZ,QAAQ;QACR,QAAQ;QACR,OAAO;QACP,MAAM;KACP,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,wBAAwB,CAAC,CAAC,CAAC,CAAU,CAAC,CAC3E,CAAC;IACF,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAAiC,CAAC;AACrE,CAAC"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Backend } from './types.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { SyncQueueAdapter } from '../sync/types.js';
|
|
3
3
|
import { SyncManager } from '../sync/SyncManager.js';
|
|
4
|
-
export declare const VALID_BACKENDS: readonly ["
|
|
4
|
+
export declare const VALID_BACKENDS: readonly ["none", "filesystem", "github", "gitlab", "azure", "jira"];
|
|
5
5
|
export type BackendType = (typeof VALID_BACKENDS)[number];
|
|
6
6
|
export declare function detectBackend(root: string): BackendType;
|
|
7
7
|
export declare function createBackend(root: string): Promise<Backend>;
|
|
8
|
+
export declare function createRemoteBackend(root: string, backendType: string): Promise<Backend | null>;
|
|
8
9
|
export interface BackendSetup {
|
|
9
|
-
backend:
|
|
10
|
+
backend: Backend;
|
|
10
11
|
syncManager: SyncManager | null;
|
|
12
|
+
queue: SyncQueueAdapter | null;
|
|
11
13
|
}
|
|
12
14
|
export declare function createBackendWithSync(root: string): Promise<BackendSetup>;
|
package/dist/backends/factory.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { Storage } from '../storage/index.js';
|
|
3
|
+
import { SyncQueue } from '../storage/syncQueue.js';
|
|
3
4
|
import { configStore } from '../stores/configStore.js';
|
|
4
5
|
import { SyncManager } from '../sync/SyncManager.js';
|
|
5
|
-
import { SyncQueueStore } from '../sync/queue.js';
|
|
6
6
|
export const VALID_BACKENDS = [
|
|
7
|
-
'
|
|
7
|
+
'none',
|
|
8
|
+
'filesystem',
|
|
8
9
|
'github',
|
|
9
10
|
'gitlab',
|
|
10
11
|
'azure',
|
|
@@ -29,16 +30,24 @@ export function detectBackend(root) {
|
|
|
29
30
|
catch {
|
|
30
31
|
// Not a git repo or git not available
|
|
31
32
|
}
|
|
32
|
-
return '
|
|
33
|
+
return 'none';
|
|
33
34
|
}
|
|
34
35
|
export async function createBackend(root) {
|
|
36
|
+
const primary = Storage.create(root);
|
|
37
|
+
configStore.getState().setDatabase(primary.getDatabase());
|
|
35
38
|
if (!configStore.getState().loaded) {
|
|
36
39
|
await configStore.getState().init(root);
|
|
37
40
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
return primary;
|
|
42
|
+
}
|
|
43
|
+
export async function createRemoteBackend(root, backendType) {
|
|
44
|
+
switch (backendType) {
|
|
45
|
+
case 'none':
|
|
46
|
+
return null;
|
|
47
|
+
case 'filesystem': {
|
|
48
|
+
const { FilesBackend } = await import('./files/index.js');
|
|
49
|
+
return new FilesBackend(root);
|
|
50
|
+
}
|
|
42
51
|
case 'github': {
|
|
43
52
|
const { GitHubBackend } = await import('./github/index.js');
|
|
44
53
|
return new GitHubBackend(root);
|
|
@@ -56,47 +65,23 @@ export async function createBackend(root) {
|
|
|
56
65
|
return JiraBackend.create(root);
|
|
57
66
|
}
|
|
58
67
|
default:
|
|
59
|
-
|
|
68
|
+
return null;
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
export async function createBackendWithSync(root) {
|
|
72
|
+
const primary = Storage.create(root);
|
|
73
|
+
configStore.getState().setDatabase(primary.getDatabase());
|
|
63
74
|
if (!configStore.getState().loaded) {
|
|
64
75
|
await configStore.getState().init(root);
|
|
65
76
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let remote;
|
|
74
|
-
switch (backendType) {
|
|
75
|
-
case 'github': {
|
|
76
|
-
const { GitHubBackend } = await import('./github/index.js');
|
|
77
|
-
remote = new GitHubBackend(root);
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
case 'gitlab': {
|
|
81
|
-
const { GitLabBackend } = await import('./gitlab/index.js');
|
|
82
|
-
remote = new GitLabBackend(root);
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
case 'azure': {
|
|
86
|
-
const { AzureDevOpsBackend } = await import('./ado/index.js');
|
|
87
|
-
remote = new AzureDevOpsBackend(root);
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
case 'jira': {
|
|
91
|
-
const { JiraBackend } = await import('./jira/index.js');
|
|
92
|
-
remote = await JiraBackend.create(root);
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
default:
|
|
96
|
-
throw new Error(`Unknown backend "${backendType}". Valid backends: ${VALID_BACKENDS.join(', ')}`);
|
|
77
|
+
const config = configStore.getState().config;
|
|
78
|
+
const remote = await createRemoteBackend(root, config.backend ?? 'none');
|
|
79
|
+
let syncManager = null;
|
|
80
|
+
let queue = null;
|
|
81
|
+
if (remote) {
|
|
82
|
+
queue = new SyncQueue(primary.getDatabase());
|
|
83
|
+
syncManager = new SyncManager(primary, remote, queue);
|
|
97
84
|
}
|
|
98
|
-
|
|
99
|
-
const syncManager = new SyncManager(local, remote, queueStore);
|
|
100
|
-
return { backend: local, syncManager };
|
|
85
|
+
return { backend: primary, syncManager, queue };
|
|
101
86
|
}
|
|
102
87
|
//# sourceMappingURL=factory.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/backends/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/backends/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG9C,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM;IACN,YAAY;IACZ,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,MAAM;CACE,CAAC;AAGX,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,eAAe,EAAE;YACvC,GAAG,EAAE,IAAI;YACT,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,OAAO,QAAQ,CAAC;QACnD,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YAAE,OAAO,QAAQ,CAAC;QACnD,IACE,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC;YAChC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC;YACpC,wBAAwB,CAAC,IAAI,CAAC,MAAM,CAAC;YAErC,OAAO,OAAO,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACrC,WAAW,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1D,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY,EACZ,WAAmB;IAEnB,QAAQ,WAAW,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,OAAO,IAAI,CAAC;QACd,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;YAC1D,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;YAC5D,OAAO,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;YAC5D,OAAO,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;YAC9D,OAAO,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACtC,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;YACxD,OAAO,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;QACD;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,IAAY;IAEZ,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACrC,WAAW,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAE1D,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,WAAW,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC;IAC7C,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,CAAC;IAEzE,IAAI,WAAW,GAAuB,IAAI,CAAC;IAC3C,IAAI,KAAK,GAA4B,IAAI,CAAC;IAC1C,IAAI,MAAM,EAAE,CAAC;QACX,KAAK,GAAG,IAAI,SAAS,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;QAC7C,WAAW,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function contentHash(content: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../../src/backends/files/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseBackend } from '../types.js';
|
|
2
|
+
import type { BackendCapabilities, SyncableBackend } from '../types.js';
|
|
3
|
+
import type { WorkItem, NewWorkItem, NewComment, Comment, Template } from '../../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* FilesBackend is a filesystem-based sync destination.
|
|
6
|
+
*
|
|
7
|
+
* It delegates all I/O to the existing local/items.ts and local/templates.ts
|
|
8
|
+
* modules. Unlike LocalBackend, it does NOT:
|
|
9
|
+
* - Cache items (caching is the primary backend's job)
|
|
10
|
+
* - Validate relationships (that's the primary backend's job)
|
|
11
|
+
* - Manage config (config lives in the primary/SQLite)
|
|
12
|
+
* - Manage next_id (IDs come from the primary)
|
|
13
|
+
* - Support soft-delete (that's a primary backend concept)
|
|
14
|
+
*
|
|
15
|
+
* It implements SyncableBackend so it can be used as a sync destination
|
|
16
|
+
* with ID-preserving imports via importWorkItem.
|
|
17
|
+
*/
|
|
18
|
+
export declare class FilesBackend extends BaseBackend implements SyncableBackend {
|
|
19
|
+
private root;
|
|
20
|
+
constructor(root: string);
|
|
21
|
+
getRoot(): string;
|
|
22
|
+
getCapabilities(): BackendCapabilities;
|
|
23
|
+
getStatuses(): Promise<string[]>;
|
|
24
|
+
getIterations(): Promise<string[]>;
|
|
25
|
+
getWorkItemTypes(): Promise<string[]>;
|
|
26
|
+
getAssignees(): Promise<string[]>;
|
|
27
|
+
getLabels(): Promise<string[]>;
|
|
28
|
+
getCurrentIteration(): Promise<string>;
|
|
29
|
+
setCurrentIteration(_name: string): Promise<void>;
|
|
30
|
+
listWorkItems(_iteration?: string): Promise<WorkItem[]>;
|
|
31
|
+
getWorkItem(id: string): Promise<WorkItem>;
|
|
32
|
+
createWorkItem(_data: NewWorkItem): Promise<WorkItem>;
|
|
33
|
+
updateWorkItem(id: string, data: Partial<WorkItem>): Promise<WorkItem>;
|
|
34
|
+
deleteWorkItem(id: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Write a complete WorkItem preserving its existing ID.
|
|
37
|
+
* Used during sync to replicate items from primary to files.
|
|
38
|
+
*/
|
|
39
|
+
importWorkItem(item: WorkItem): Promise<WorkItem>;
|
|
40
|
+
addComment(workItemId: string, comment: NewComment): Promise<Comment>;
|
|
41
|
+
getItemUrl(id: string): string;
|
|
42
|
+
openItem(_id: string): Promise<void>;
|
|
43
|
+
listTemplates(): Promise<Template[]>;
|
|
44
|
+
getTemplate(slug: string): Promise<Template>;
|
|
45
|
+
createTemplate(template: Template): Promise<Template>;
|
|
46
|
+
updateTemplate(oldSlug: string, template: Template): Promise<Template>;
|
|
47
|
+
deleteTemplate(slug: string): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { BaseBackend, UnsupportedOperationError } from '../types.js';
|
|
4
|
+
import { readWorkItem, writeWorkItem, deleteWorkItem as removeWorkItemFile, listItemFiles, parseWorkItemFile, } from '../local/items.js';
|
|
5
|
+
import { listTemplates as listTemplateFiles, readTemplate, writeTemplate, deleteTemplate as removeTemplateFile, slugifyTemplateName, } from '../local/templates.js';
|
|
6
|
+
/**
|
|
7
|
+
* FilesBackend is a filesystem-based sync destination.
|
|
8
|
+
*
|
|
9
|
+
* It delegates all I/O to the existing local/items.ts and local/templates.ts
|
|
10
|
+
* modules. Unlike LocalBackend, it does NOT:
|
|
11
|
+
* - Cache items (caching is the primary backend's job)
|
|
12
|
+
* - Validate relationships (that's the primary backend's job)
|
|
13
|
+
* - Manage config (config lives in the primary/SQLite)
|
|
14
|
+
* - Manage next_id (IDs come from the primary)
|
|
15
|
+
* - Support soft-delete (that's a primary backend concept)
|
|
16
|
+
*
|
|
17
|
+
* It implements SyncableBackend so it can be used as a sync destination
|
|
18
|
+
* with ID-preserving imports via importWorkItem.
|
|
19
|
+
*/
|
|
20
|
+
export class FilesBackend extends BaseBackend {
|
|
21
|
+
root;
|
|
22
|
+
constructor(root) {
|
|
23
|
+
super(0); // no TTL cache
|
|
24
|
+
this.root = root;
|
|
25
|
+
}
|
|
26
|
+
getRoot() {
|
|
27
|
+
return this.root;
|
|
28
|
+
}
|
|
29
|
+
getCapabilities() {
|
|
30
|
+
return {
|
|
31
|
+
relationships: true,
|
|
32
|
+
customTypes: true,
|
|
33
|
+
customStatuses: true,
|
|
34
|
+
iterations: true,
|
|
35
|
+
comments: true,
|
|
36
|
+
fields: {
|
|
37
|
+
priority: true,
|
|
38
|
+
assignee: true,
|
|
39
|
+
labels: true,
|
|
40
|
+
parent: true,
|
|
41
|
+
dependsOn: true,
|
|
42
|
+
},
|
|
43
|
+
templates: true,
|
|
44
|
+
templateFields: {
|
|
45
|
+
type: true,
|
|
46
|
+
status: true,
|
|
47
|
+
priority: true,
|
|
48
|
+
assignee: true,
|
|
49
|
+
labels: true,
|
|
50
|
+
iteration: true,
|
|
51
|
+
parent: true,
|
|
52
|
+
dependsOn: true,
|
|
53
|
+
description: true,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// --- Metadata methods: return empty since metadata is the primary's concern ---
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
59
|
+
async getStatuses() {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
63
|
+
async getIterations() {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
67
|
+
async getWorkItemTypes() {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
71
|
+
async getAssignees() {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
75
|
+
async getLabels() {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
79
|
+
async getCurrentIteration() {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
83
|
+
async setCurrentIteration(_name) {
|
|
84
|
+
// no-op: iterations are managed by the primary backend
|
|
85
|
+
}
|
|
86
|
+
// --- Work item CRUD ---
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
88
|
+
async listWorkItems(_iteration) {
|
|
89
|
+
const files = await listItemFiles(this.root);
|
|
90
|
+
const items = await Promise.all(files.map(async (f) => {
|
|
91
|
+
const raw = await fs.readFile(f, 'utf-8');
|
|
92
|
+
return parseWorkItemFile(raw);
|
|
93
|
+
}));
|
|
94
|
+
// No iteration filtering — that's the primary's job
|
|
95
|
+
return items;
|
|
96
|
+
}
|
|
97
|
+
async getWorkItem(id) {
|
|
98
|
+
return readWorkItem(this.root, id);
|
|
99
|
+
}
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await
|
|
101
|
+
async createWorkItem(_data) {
|
|
102
|
+
throw new UnsupportedOperationError('createWorkItem', 'FilesBackend');
|
|
103
|
+
}
|
|
104
|
+
async updateWorkItem(id, data) {
|
|
105
|
+
const existing = await this.getWorkItem(id);
|
|
106
|
+
const updated = {
|
|
107
|
+
...existing,
|
|
108
|
+
...data,
|
|
109
|
+
id, // preserve ID
|
|
110
|
+
updated: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
await writeWorkItem(this.root, updated);
|
|
113
|
+
return updated;
|
|
114
|
+
}
|
|
115
|
+
async deleteWorkItem(id) {
|
|
116
|
+
await removeWorkItemFile(this.root, id);
|
|
117
|
+
// No relationship cleanup — that's the primary's job
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Write a complete WorkItem preserving its existing ID.
|
|
121
|
+
* Used during sync to replicate items from primary to files.
|
|
122
|
+
*/
|
|
123
|
+
async importWorkItem(item) {
|
|
124
|
+
await writeWorkItem(this.root, item);
|
|
125
|
+
return item;
|
|
126
|
+
}
|
|
127
|
+
// --- Comments ---
|
|
128
|
+
async addComment(workItemId, comment) {
|
|
129
|
+
const item = await this.getWorkItem(workItemId);
|
|
130
|
+
const newComment = {
|
|
131
|
+
author: comment.author,
|
|
132
|
+
date: new Date().toISOString(),
|
|
133
|
+
body: comment.body,
|
|
134
|
+
};
|
|
135
|
+
item.comments.push(newComment);
|
|
136
|
+
item.updated = new Date().toISOString();
|
|
137
|
+
await writeWorkItem(this.root, item);
|
|
138
|
+
return newComment;
|
|
139
|
+
}
|
|
140
|
+
// --- Item URL ---
|
|
141
|
+
getItemUrl(id) {
|
|
142
|
+
return path.resolve(this.root, '.tic', 'items', `${id}.md`);
|
|
143
|
+
}
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
145
|
+
async openItem(_id) {
|
|
146
|
+
// no-op: FilesBackend is a sync destination, not interactive
|
|
147
|
+
}
|
|
148
|
+
// --- Templates ---
|
|
149
|
+
async listTemplates() {
|
|
150
|
+
return listTemplateFiles(this.root);
|
|
151
|
+
}
|
|
152
|
+
async getTemplate(slug) {
|
|
153
|
+
return readTemplate(this.root, slug);
|
|
154
|
+
}
|
|
155
|
+
async createTemplate(template) {
|
|
156
|
+
const slug = slugifyTemplateName(template.name);
|
|
157
|
+
const t = { ...template, slug };
|
|
158
|
+
await writeTemplate(this.root, t);
|
|
159
|
+
return t;
|
|
160
|
+
}
|
|
161
|
+
async updateTemplate(oldSlug, template) {
|
|
162
|
+
const newSlug = slugifyTemplateName(template.name);
|
|
163
|
+
if (oldSlug !== newSlug) {
|
|
164
|
+
await removeTemplateFile(this.root, oldSlug);
|
|
165
|
+
}
|
|
166
|
+
const t = { ...template, slug: newSlug };
|
|
167
|
+
await writeTemplate(this.root, t);
|
|
168
|
+
return t;
|
|
169
|
+
}
|
|
170
|
+
async deleteTemplate(slug) {
|
|
171
|
+
await removeTemplateFile(this.root, slug);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/backends/files/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TicDatabase } from '../../storage/db.js';
|
|
2
|
+
/** Compute content hashes for all .tic/items/*.md files on disk. */
|
|
3
|
+
export declare function computeFileHashes(root: string): Promise<Map<string, string>>;
|
|
4
|
+
/** Detect which files changed, were added, or were deleted since last sync. */
|
|
5
|
+
export declare function detectChanges(db: TicDatabase, root: string): Promise<{
|
|
6
|
+
changed: string[];
|
|
7
|
+
added: string[];
|
|
8
|
+
deleted: string[];
|
|
9
|
+
}>;
|
|
10
|
+
/** Update the stored hash for an item (upsert). */
|
|
11
|
+
export declare function updateSyncState(db: TicDatabase, itemId: string, hash: string): void;
|
|
12
|
+
/** Remove the stored hash for an item. */
|
|
13
|
+
export declare function removeSyncState(db: TicDatabase, itemId: string): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import * as s from '../../storage/schema.js';
|
|
5
|
+
import { listItemFiles } from '../local/items.js';
|
|
6
|
+
import { contentHash } from './hash.js';
|
|
7
|
+
/**
|
|
8
|
+
* Extract the item ID from a full file path like `/foo/.tic/items/42.md`.
|
|
9
|
+
*/
|
|
10
|
+
function idFromPath(filePath) {
|
|
11
|
+
return path.basename(filePath, '.md');
|
|
12
|
+
}
|
|
13
|
+
/** Compute content hashes for all .tic/items/*.md files on disk. */
|
|
14
|
+
export async function computeFileHashes(root) {
|
|
15
|
+
const files = await listItemFiles(root);
|
|
16
|
+
const hashes = new Map();
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const id = idFromPath(file);
|
|
19
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
20
|
+
hashes.set(id, contentHash(raw));
|
|
21
|
+
}
|
|
22
|
+
return hashes;
|
|
23
|
+
}
|
|
24
|
+
/** Detect which files changed, were added, or were deleted since last sync. */
|
|
25
|
+
export async function detectChanges(db, root) {
|
|
26
|
+
const currentHashes = await computeFileHashes(root);
|
|
27
|
+
// Get stored hashes from the file_sync_state table
|
|
28
|
+
const rows = db.select().from(s.fileSyncState).all();
|
|
29
|
+
const storedHashes = new Map();
|
|
30
|
+
for (const row of rows) {
|
|
31
|
+
storedHashes.set(row.itemId, row.hash);
|
|
32
|
+
}
|
|
33
|
+
const changed = [];
|
|
34
|
+
const added = [];
|
|
35
|
+
const deleted = [];
|
|
36
|
+
// Check current files against stored
|
|
37
|
+
for (const [id, hash] of currentHashes) {
|
|
38
|
+
const stored = storedHashes.get(id);
|
|
39
|
+
if (!stored) {
|
|
40
|
+
added.push(id);
|
|
41
|
+
}
|
|
42
|
+
else if (stored !== hash) {
|
|
43
|
+
changed.push(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Check stored against current for deletions
|
|
47
|
+
for (const [id] of storedHashes) {
|
|
48
|
+
if (!currentHashes.has(id)) {
|
|
49
|
+
deleted.push(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { changed, added, deleted };
|
|
53
|
+
}
|
|
54
|
+
/** Update the stored hash for an item (upsert). */
|
|
55
|
+
export function updateSyncState(db, itemId, hash) {
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
db.insert(s.fileSyncState)
|
|
58
|
+
.values({ itemId, hash, syncedAt: now })
|
|
59
|
+
.onConflictDoUpdate({
|
|
60
|
+
target: s.fileSyncState.itemId,
|
|
61
|
+
set: { hash, syncedAt: now },
|
|
62
|
+
})
|
|
63
|
+
.run();
|
|
64
|
+
}
|
|
65
|
+
/** Remove the stored hash for an item. */
|
|
66
|
+
export function removeSyncState(db, itemId) {
|
|
67
|
+
db.delete(s.fileSyncState).where(eq(s.fileSyncState.itemId, itemId)).run();
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../../src/backends/files/sync.ts"],"names":[],"mappings":"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"}
|