trekoon 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/trekoon/SKILL.md +160 -6
- package/README.md +55 -2
- package/package.json +10 -2
- package/src/commands/events.ts +88 -0
- package/src/commands/help.ts +9 -1
- package/src/commands/migrate.ts +123 -0
- package/src/commands/skills.ts +265 -0
- package/src/commands/subtask.ts +121 -2
- package/src/domain/tracker-domain.ts +18 -37
- package/src/runtime/cli-shell.ts +15 -0
- package/src/storage/database.ts +11 -2
- package/src/storage/events-retention.ts +138 -0
- package/src/storage/migrations.ts +340 -19
- package/src/storage/schema.ts +1 -0
- package/src/storage/types.ts +1 -0
- package/src/sync/service.ts +9 -1
- package/AGENTS.md +0 -54
- package/CONTRIBUTING.md +0 -18
- package/bun.lock +0 -29
- package/tests/commands/dep.test.ts +0 -101
- package/tests/commands/epic.test.ts +0 -383
- package/tests/commands/subtask.test.ts +0 -132
- package/tests/commands/sync/sync-command.test.ts +0 -1
- package/tests/commands/sync.test.ts +0 -199
- package/tests/commands/task.test.ts +0 -474
- package/tests/integration/sync-workflow.test.ts +0 -279
- package/tests/io/human-table.test.ts +0 -81
- package/tests/runtime/output-mode.test.ts +0 -54
- package/tests/storage/database.test.ts +0 -91
- package/tsconfig.json +0 -19
|
@@ -2,10 +2,142 @@ import { Database } from "bun:sqlite";
|
|
|
2
2
|
|
|
3
3
|
import { BASE_SCHEMA_STATEMENTS, SCHEMA_VERSION } from "./schema";
|
|
4
4
|
|
|
5
|
+
const BASE_MIGRATION_VERSION = 1;
|
|
5
6
|
const BASE_MIGRATION_NAME = `0001_base_schema_v${SCHEMA_VERSION}`;
|
|
7
|
+
const LEGACY_BASE_MIGRATION_NAME_PATTERNS: readonly string[] = [
|
|
8
|
+
"0001_base_schema_v*",
|
|
9
|
+
];
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
const BASE_ROLLBACK_STATEMENTS: readonly string[] = [
|
|
12
|
+
"DROP TABLE IF EXISTS sync_conflicts;",
|
|
13
|
+
"DROP TABLE IF EXISTS sync_cursors;",
|
|
14
|
+
"DROP TABLE IF EXISTS git_context;",
|
|
15
|
+
"DROP TABLE IF EXISTS events;",
|
|
16
|
+
"DROP TABLE IF EXISTS dependencies;",
|
|
17
|
+
"DROP TABLE IF EXISTS subtasks;",
|
|
18
|
+
"DROP TABLE IF EXISTS tasks;",
|
|
19
|
+
"DROP TABLE IF EXISTS epics;",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const INDEX_MIGRATION_UP_STATEMENTS: readonly string[] = [
|
|
23
|
+
"CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);",
|
|
24
|
+
"CREATE INDEX IF NOT EXISTS idx_events_git_branch ON events(git_branch);",
|
|
25
|
+
"CREATE INDEX IF NOT EXISTS idx_events_created_at_id ON events(created_at, id);",
|
|
26
|
+
"CREATE INDEX IF NOT EXISTS idx_dependencies_source ON dependencies(source_id);",
|
|
27
|
+
"CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on ON dependencies(depends_on_id);",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const INDEX_MIGRATION_DOWN_STATEMENTS: readonly string[] = [
|
|
31
|
+
"DROP INDEX IF EXISTS idx_events_created_at;",
|
|
32
|
+
"DROP INDEX IF EXISTS idx_events_git_branch;",
|
|
33
|
+
"DROP INDEX IF EXISTS idx_events_created_at_id;",
|
|
34
|
+
"DROP INDEX IF EXISTS idx_dependencies_source;",
|
|
35
|
+
"DROP INDEX IF EXISTS idx_dependencies_depends_on;",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const EVENT_ARCHIVE_MIGRATION_UP_STATEMENTS: readonly string[] = [
|
|
39
|
+
`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS event_archive (
|
|
41
|
+
id TEXT PRIMARY KEY,
|
|
42
|
+
entity_kind TEXT NOT NULL,
|
|
43
|
+
entity_id TEXT NOT NULL,
|
|
44
|
+
operation TEXT NOT NULL,
|
|
45
|
+
payload TEXT NOT NULL,
|
|
46
|
+
git_branch TEXT,
|
|
47
|
+
git_head TEXT,
|
|
48
|
+
created_at INTEGER NOT NULL,
|
|
49
|
+
updated_at INTEGER NOT NULL,
|
|
50
|
+
version INTEGER NOT NULL DEFAULT 1
|
|
51
|
+
);
|
|
52
|
+
`,
|
|
53
|
+
"CREATE INDEX IF NOT EXISTS idx_event_archive_created_at ON event_archive(created_at);",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const EVENT_ARCHIVE_MIGRATION_DOWN_STATEMENTS: readonly string[] = [
|
|
57
|
+
"DROP INDEX IF EXISTS idx_event_archive_created_at;",
|
|
58
|
+
"DROP TABLE IF EXISTS event_archive;",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
interface Migration {
|
|
62
|
+
readonly version: number;
|
|
63
|
+
readonly name: string;
|
|
64
|
+
up(db: Database): void;
|
|
65
|
+
down(db: Database): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AppliedMigrationRow {
|
|
69
|
+
readonly version: number;
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly applied_at: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AppliedMigration {
|
|
75
|
+
readonly version: number;
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly appliedAt: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface MigrationStatus {
|
|
81
|
+
readonly currentVersion: number;
|
|
82
|
+
readonly latestVersion: number;
|
|
83
|
+
readonly applied: readonly AppliedMigration[];
|
|
84
|
+
readonly pending: ReadonlyArray<{ version: number; name: string }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RollbackSummary {
|
|
88
|
+
readonly fromVersion: number;
|
|
89
|
+
readonly toVersion: number;
|
|
90
|
+
readonly rolledBack: number;
|
|
91
|
+
readonly rolledBackMigrations: readonly string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const MIGRATIONS: readonly Migration[] = [
|
|
95
|
+
{
|
|
96
|
+
version: BASE_MIGRATION_VERSION,
|
|
97
|
+
name: BASE_MIGRATION_NAME,
|
|
98
|
+
up(db: Database): void {
|
|
99
|
+
for (const statement of BASE_SCHEMA_STATEMENTS) {
|
|
100
|
+
db.exec(statement);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
down(db: Database): void {
|
|
104
|
+
for (const statement of BASE_ROLLBACK_STATEMENTS) {
|
|
105
|
+
db.exec(statement);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
version: 2,
|
|
111
|
+
name: "0002_sync_dependency_indexes",
|
|
112
|
+
up(db: Database): void {
|
|
113
|
+
for (const statement of INDEX_MIGRATION_UP_STATEMENTS) {
|
|
114
|
+
db.exec(statement);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
down(db: Database): void {
|
|
118
|
+
for (const statement of INDEX_MIGRATION_DOWN_STATEMENTS) {
|
|
119
|
+
db.exec(statement);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
version: 3,
|
|
125
|
+
name: "0003_event_archive_retention",
|
|
126
|
+
up(db: Database): void {
|
|
127
|
+
for (const statement of EVENT_ARCHIVE_MIGRATION_UP_STATEMENTS) {
|
|
128
|
+
db.exec(statement);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
down(db: Database): void {
|
|
132
|
+
for (const statement of EVENT_ARCHIVE_MIGRATION_DOWN_STATEMENTS) {
|
|
133
|
+
db.exec(statement);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
function migrationTableExists(db: Database): boolean {
|
|
140
|
+
const row = db
|
|
9
141
|
.query(
|
|
10
142
|
`
|
|
11
143
|
SELECT COUNT(*) AS count
|
|
@@ -15,32 +147,221 @@ function hasMigration(db: Database, name: string): boolean {
|
|
|
15
147
|
)
|
|
16
148
|
.get() as { count: number } | null;
|
|
17
149
|
|
|
18
|
-
|
|
19
|
-
|
|
150
|
+
return (row?.count ?? 0) > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasMigrationVersionColumn(db: Database): boolean {
|
|
154
|
+
const columns = db.query("PRAGMA table_info(schema_migrations);").all() as Array<{ name: string }>;
|
|
155
|
+
return columns.some((column) => column.name === "version");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureMigrationTable(db: Database): void {
|
|
159
|
+
db.exec(`
|
|
160
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
161
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
162
|
+
version INTEGER NOT NULL UNIQUE,
|
|
163
|
+
name TEXT NOT NULL UNIQUE,
|
|
164
|
+
applied_at INTEGER NOT NULL
|
|
165
|
+
);
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ensureMigrationVersionColumn(db: Database): void {
|
|
170
|
+
if (!migrationTableExists(db) || hasMigrationVersionColumn(db)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
db.exec("ALTER TABLE schema_migrations ADD COLUMN version INTEGER;");
|
|
175
|
+
db.query("UPDATE schema_migrations SET version = ? WHERE version IS NULL AND name = ?;").run(BASE_MIGRATION_VERSION, BASE_MIGRATION_NAME);
|
|
176
|
+
|
|
177
|
+
for (const legacyPattern of LEGACY_BASE_MIGRATION_NAME_PATTERNS) {
|
|
178
|
+
db.query("UPDATE schema_migrations SET version = ? WHERE version IS NULL AND name GLOB ?;").run(
|
|
179
|
+
BASE_MIGRATION_VERSION,
|
|
180
|
+
legacyPattern,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_schema_migrations_version ON schema_migrations(version);");
|
|
185
|
+
|
|
186
|
+
const unresolvedRow = db
|
|
187
|
+
.query(
|
|
188
|
+
`
|
|
189
|
+
SELECT COUNT(*) AS count
|
|
190
|
+
FROM schema_migrations
|
|
191
|
+
WHERE version IS NULL;
|
|
192
|
+
`,
|
|
193
|
+
)
|
|
194
|
+
.get() as { count: number } | null;
|
|
195
|
+
|
|
196
|
+
if ((unresolvedRow?.count ?? 0) > 0) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"Unable to infer one or more schema_migrations.version values during legacy upgrade. Repair schema_migrations entries manually so every row has a valid version, then rerun migrations.",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateMigrationPlan(): void {
|
|
204
|
+
const seen = new Set<number>();
|
|
205
|
+
|
|
206
|
+
for (let index = 0; index < MIGRATIONS.length; index += 1) {
|
|
207
|
+
const migration: Migration = MIGRATIONS[index]!;
|
|
208
|
+
|
|
209
|
+
if (migration.version !== index + 1) {
|
|
210
|
+
throw new Error(`Migration versions must be contiguous from 1 (found ${migration.version} at index ${index}).`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (seen.has(migration.version)) {
|
|
214
|
+
throw new Error(`Duplicate migration version ${migration.version}.`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
seen.add(migration.version);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function runExclusive<T>(db: Database, operation: () => T): T {
|
|
222
|
+
db.exec("BEGIN EXCLUSIVE TRANSACTION;");
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const result: T = operation();
|
|
226
|
+
db.exec("COMMIT;");
|
|
227
|
+
return result;
|
|
228
|
+
} catch (error: unknown) {
|
|
229
|
+
db.exec("ROLLBACK;");
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function currentVersion(db: Database): number {
|
|
235
|
+
if (!migrationTableExists(db)) {
|
|
236
|
+
return 0;
|
|
20
237
|
}
|
|
21
238
|
|
|
22
|
-
const row
|
|
23
|
-
.query("SELECT
|
|
24
|
-
.get(
|
|
239
|
+
const row = db
|
|
240
|
+
.query("SELECT COALESCE(MAX(version), 0) AS version FROM schema_migrations;")
|
|
241
|
+
.get() as { version: number } | null;
|
|
25
242
|
|
|
26
|
-
return
|
|
243
|
+
return row?.version ?? 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function listAppliedMigrations(db: Database): AppliedMigrationRow[] {
|
|
247
|
+
if (!migrationTableExists(db)) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return db
|
|
252
|
+
.query(
|
|
253
|
+
`
|
|
254
|
+
SELECT version, name, applied_at
|
|
255
|
+
FROM schema_migrations
|
|
256
|
+
WHERE version IS NOT NULL
|
|
257
|
+
ORDER BY version ASC;
|
|
258
|
+
`,
|
|
259
|
+
)
|
|
260
|
+
.all() as AppliedMigrationRow[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function migrationForVersion(version: number): Migration {
|
|
264
|
+
const found = MIGRATIONS.find((migration) => migration.version === version);
|
|
265
|
+
|
|
266
|
+
if (!found) {
|
|
267
|
+
throw new Error(`No migration definition found for version ${version}.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return found;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function recordMigration(db: Database, migration: Migration): void {
|
|
274
|
+
db.query("INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?);").run(
|
|
275
|
+
migration.version,
|
|
276
|
+
migration.name,
|
|
277
|
+
Date.now(),
|
|
278
|
+
);
|
|
27
279
|
}
|
|
28
280
|
|
|
29
281
|
export function migrateDatabase(db: Database): void {
|
|
30
|
-
|
|
31
|
-
|
|
282
|
+
runExclusive(db, (): void => {
|
|
283
|
+
ensureMigrationTable(db);
|
|
284
|
+
ensureMigrationVersionColumn(db);
|
|
285
|
+
validateMigrationPlan();
|
|
286
|
+
|
|
287
|
+
const version: number = currentVersion(db);
|
|
288
|
+
|
|
289
|
+
for (const migration of MIGRATIONS) {
|
|
290
|
+
if (migration.version <= version) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
migration.up(db);
|
|
295
|
+
recordMigration(db, migration);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function describeMigrations(db: Database): MigrationStatus {
|
|
301
|
+
ensureMigrationTable(db);
|
|
302
|
+
ensureMigrationVersionColumn(db);
|
|
303
|
+
validateMigrationPlan();
|
|
304
|
+
|
|
305
|
+
const appliedRows: AppliedMigrationRow[] = listAppliedMigrations(db);
|
|
306
|
+
const latestVersion: number = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
307
|
+
const activeVersion: number = appliedRows[appliedRows.length - 1]?.version ?? 0;
|
|
308
|
+
const appliedVersions = new Set(appliedRows.map((row) => row.version));
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
currentVersion: activeVersion,
|
|
312
|
+
latestVersion,
|
|
313
|
+
applied: appliedRows.map((row) => ({
|
|
314
|
+
version: row.version,
|
|
315
|
+
name: row.name,
|
|
316
|
+
appliedAt: row.applied_at,
|
|
317
|
+
})),
|
|
318
|
+
pending: MIGRATIONS.filter((migration) => !appliedVersions.has(migration.version)).map((migration) => ({
|
|
319
|
+
version: migration.version,
|
|
320
|
+
name: migration.name,
|
|
321
|
+
})),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function rollbackDatabase(db: Database, targetVersion: number): RollbackSummary {
|
|
326
|
+
if (!Number.isInteger(targetVersion) || targetVersion < 0) {
|
|
327
|
+
throw new Error("Rollback target version must be a non-negative integer.");
|
|
32
328
|
}
|
|
33
329
|
|
|
34
|
-
|
|
330
|
+
return runExclusive(db, (): RollbackSummary => {
|
|
331
|
+
ensureMigrationTable(db);
|
|
332
|
+
ensureMigrationVersionColumn(db);
|
|
333
|
+
validateMigrationPlan();
|
|
35
334
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
335
|
+
const fromVersion: number = currentVersion(db);
|
|
336
|
+
if (targetVersion > fromVersion) {
|
|
337
|
+
throw new Error(`Cannot roll back to version ${targetVersion}; current version is ${fromVersion}.`);
|
|
39
338
|
}
|
|
40
339
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
340
|
+
const appliedDescending = db
|
|
341
|
+
.query(
|
|
342
|
+
`
|
|
343
|
+
SELECT version, name, applied_at
|
|
344
|
+
FROM schema_migrations
|
|
345
|
+
WHERE version IS NOT NULL AND version > ?
|
|
346
|
+
ORDER BY version DESC;
|
|
347
|
+
`,
|
|
348
|
+
)
|
|
349
|
+
.all(targetVersion) as AppliedMigrationRow[];
|
|
350
|
+
|
|
351
|
+
const rolledBackMigrations: string[] = [];
|
|
352
|
+
|
|
353
|
+
for (const row of appliedDescending) {
|
|
354
|
+
const migration: Migration = migrationForVersion(row.version);
|
|
355
|
+
migration.down(db);
|
|
356
|
+
db.query("DELETE FROM schema_migrations WHERE version = ?;").run(row.version);
|
|
357
|
+
rolledBackMigrations.push(migration.name);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
fromVersion,
|
|
362
|
+
toVersion: targetVersion,
|
|
363
|
+
rolledBack: appliedDescending.length,
|
|
364
|
+
rolledBackMigrations,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
46
367
|
}
|
package/src/storage/schema.ts
CHANGED
package/src/storage/types.ts
CHANGED
package/src/sync/service.ts
CHANGED
|
@@ -41,11 +41,19 @@ interface EventPayload {
|
|
|
41
41
|
readonly fields?: Record<string, unknown>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
function parsePayload(rawPayload: string): EventPayload {
|
|
45
49
|
try {
|
|
46
50
|
const parsed: unknown = JSON.parse(rawPayload);
|
|
47
51
|
|
|
48
|
-
if (
|
|
52
|
+
if (isObjectRecord(parsed)) {
|
|
53
|
+
if ("fields" in parsed && !isObjectRecord(parsed.fields)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
return parsed as EventPayload;
|
|
50
58
|
}
|
|
51
59
|
} catch {
|
package/AGENTS.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
This file contains guidelines for agents.
|
|
4
|
-
|
|
5
|
-
## Mandatory: Atomic Commit Policy
|
|
6
|
-
|
|
7
|
-
Every code change MUST be followed by an immediate commit.
|
|
8
|
-
|
|
9
|
-
**Commit format**:
|
|
10
|
-
```
|
|
11
|
-
<imperative verb> <what changed> ← Line 1: max 50 chars
|
|
12
|
-
<blank line> ← Line 2: blank
|
|
13
|
-
<why/context, one point per line> ← Body: max 72 chars per line
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
**Rules**:
|
|
17
|
-
1. One commit per logical change
|
|
18
|
-
2. Small, atomic commits - one file per commit preferred
|
|
19
|
-
3. Never batch unrelated changes
|
|
20
|
-
|
|
21
|
-
**Enforcement**:
|
|
22
|
-
- After any file modification, stop and commit before modifying another file
|
|
23
|
-
- Run `git status --short` after each commit to verify clean tree
|
|
24
|
-
- At milestones: run `bun run build && bun run lint && bun run test`
|
|
25
|
-
|
|
26
|
-
**Anti-patterns**:
|
|
27
|
-
- ❌ Multiple unrelated files in one commit
|
|
28
|
-
- ❌ Generic messages like "Update file", "WIP", "Fix stuff"
|
|
29
|
-
- ❌ Commit message over 50 chars on first line
|
|
30
|
-
|
|
31
|
-
## Coding Conventions
|
|
32
|
-
|
|
33
|
-
For Bun/TypeScript code:
|
|
34
|
-
- **Imports**: Group order (stdlib → third-party → local), explicit named imports, remove unused
|
|
35
|
-
- **Formatting**: Consistent quotes, avoid mixed tabs/spaces
|
|
36
|
-
- **Types**: Prefer explicit types at API boundaries, avoid `any` unless justified
|
|
37
|
-
- **Naming**: `camelCase` (vars/functions), `PascalCase` (types), `UPPER_SNAKE_CASE` (constants)
|
|
38
|
-
|
|
39
|
-
**Error handling**:
|
|
40
|
-
- Never silently swallow errors
|
|
41
|
-
- Include actionable context (operation + endpoint + status code)
|
|
42
|
-
- Redact secrets from errors and logs
|
|
43
|
-
|
|
44
|
-
## Agent Behavior
|
|
45
|
-
|
|
46
|
-
- Prefer minimal, targeted edits; avoid broad rewrites
|
|
47
|
-
- Preserve existing examples unless fixing factual issues
|
|
48
|
-
- For CLI changes, prioritize startup speed and low-latency UX
|
|
49
|
-
- Keep commands compatible with macOS and Linux shells
|
|
50
|
-
|
|
51
|
-
## Security
|
|
52
|
-
|
|
53
|
-
- Never commit secrets (tokens, credentials)
|
|
54
|
-
- Redact secrets from errors and logs
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Contributing to Trekoon
|
|
2
|
-
|
|
3
|
-
## No-copy implementation policy
|
|
4
|
-
|
|
5
|
-
Trekoon is implemented in this repository root. The `trekker/` directory is
|
|
6
|
-
reference-only.
|
|
7
|
-
|
|
8
|
-
- Do not copy code or files from `trekker/` into root `src/`.
|
|
9
|
-
- Do not mirror file layout one-to-one from `trekker/`.
|
|
10
|
-
- Write root implementation code directly, with Trekoon-native structure.
|
|
11
|
-
|
|
12
|
-
## PR checklist
|
|
13
|
-
|
|
14
|
-
- [ ] Any new logic was written directly in root project files.
|
|
15
|
-
- [ ] Changes were reviewed for suspiciously identical blocks/comments versus
|
|
16
|
-
`trekker/` reference code.
|
|
17
|
-
- [ ] Sync-related writes preserve git context (`branch`, `head`, `worktree`).
|
|
18
|
-
- [ ] README command/flag examples match actual implemented CLI behavior.
|
package/bun.lock
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 0,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "trekoon",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"@toon-format/toon": "^2.1.0",
|
|
9
|
-
},
|
|
10
|
-
"devDependencies": {
|
|
11
|
-
"@types/bun": "^1.3.9",
|
|
12
|
-
"typescript": "^5.9.3",
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
"packages": {
|
|
17
|
-
"@toon-format/toon": ["@toon-format/toon@2.1.0", "", {}, "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="],
|
|
18
|
-
|
|
19
|
-
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
|
20
|
-
|
|
21
|
-
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
22
|
-
|
|
23
|
-
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
|
24
|
-
|
|
25
|
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
26
|
-
|
|
27
|
-
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
-
|
|
7
|
-
import { runDep } from "../../src/commands/dep";
|
|
8
|
-
import { runEpic } from "../../src/commands/epic";
|
|
9
|
-
import { runSubtask } from "../../src/commands/subtask";
|
|
10
|
-
import { runTask } from "../../src/commands/task";
|
|
11
|
-
|
|
12
|
-
const tempDirs: string[] = [];
|
|
13
|
-
|
|
14
|
-
function createWorkspace(): string {
|
|
15
|
-
const workspace = mkdtempSync(join(tmpdir(), "trekoon-dep-"));
|
|
16
|
-
tempDirs.push(workspace);
|
|
17
|
-
return workspace;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
afterEach((): void => {
|
|
21
|
-
while (tempDirs.length > 0) {
|
|
22
|
-
const next = tempDirs.pop();
|
|
23
|
-
if (next) {
|
|
24
|
-
rmSync(next, { recursive: true, force: true });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
async function createTaskGraph(cwd: string): Promise<{ taskA: string; taskB: string; subtask: string }> {
|
|
30
|
-
const epic = await runEpic({
|
|
31
|
-
cwd,
|
|
32
|
-
mode: "human",
|
|
33
|
-
args: ["create", "--title", "Roadmap", "--description", "desc"],
|
|
34
|
-
});
|
|
35
|
-
const epicId = (epic.data as { epic: { id: string } }).epic.id;
|
|
36
|
-
|
|
37
|
-
const taskA = await runTask({
|
|
38
|
-
cwd,
|
|
39
|
-
mode: "human",
|
|
40
|
-
args: ["create", "--epic", epicId, "--title", "Task A", "--description", "desc a"],
|
|
41
|
-
});
|
|
42
|
-
const taskAId = (taskA.data as { task: { id: string } }).task.id;
|
|
43
|
-
|
|
44
|
-
const taskB = await runTask({
|
|
45
|
-
cwd,
|
|
46
|
-
mode: "human",
|
|
47
|
-
args: ["create", "--epic", epicId, "--title", "Task B", "--description", "desc b"],
|
|
48
|
-
});
|
|
49
|
-
const taskBId = (taskB.data as { task: { id: string } }).task.id;
|
|
50
|
-
|
|
51
|
-
const subtask = await runSubtask({
|
|
52
|
-
cwd,
|
|
53
|
-
mode: "human",
|
|
54
|
-
args: ["create", "--task", taskBId, "--title", "Subtask"],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
taskA: taskAId,
|
|
59
|
-
taskB: taskBId,
|
|
60
|
-
subtask: (subtask.data as { subtask: { id: string } }).subtask.id,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe("dep command", (): void => {
|
|
65
|
-
test("supports add/list/remove", async (): Promise<void> => {
|
|
66
|
-
const cwd = createWorkspace();
|
|
67
|
-
const nodes = await createTaskGraph(cwd);
|
|
68
|
-
|
|
69
|
-
const added = await runDep({ cwd, mode: "human", args: ["add", nodes.taskA, nodes.subtask] });
|
|
70
|
-
expect(added.ok).toBeTrue();
|
|
71
|
-
|
|
72
|
-
const listed = await runDep({ cwd, mode: "human", args: ["list", nodes.taskA] });
|
|
73
|
-
expect(listed.ok).toBeTrue();
|
|
74
|
-
expect((listed.data as { dependencies: unknown[] }).dependencies.length).toBe(1);
|
|
75
|
-
|
|
76
|
-
const removed = await runDep({ cwd, mode: "human", args: ["remove", nodes.taskA, nodes.subtask] });
|
|
77
|
-
expect(removed.ok).toBeTrue();
|
|
78
|
-
expect((removed.data as { removed: number }).removed).toBe(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("enforces referential checks for task/subtask nodes", async (): Promise<void> => {
|
|
82
|
-
const cwd = createWorkspace();
|
|
83
|
-
const nodes = await createTaskGraph(cwd);
|
|
84
|
-
|
|
85
|
-
const bad = await runDep({ cwd, mode: "human", args: ["add", nodes.taskA, "missing-node-id"] });
|
|
86
|
-
expect(bad.ok).toBeFalse();
|
|
87
|
-
expect(bad.error?.code).toBe("not_found");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("detects dependency cycles", async (): Promise<void> => {
|
|
91
|
-
const cwd = createWorkspace();
|
|
92
|
-
const nodes = await createTaskGraph(cwd);
|
|
93
|
-
|
|
94
|
-
const first = await runDep({ cwd, mode: "human", args: ["add", nodes.taskA, nodes.taskB] });
|
|
95
|
-
expect(first.ok).toBeTrue();
|
|
96
|
-
|
|
97
|
-
const cycle = await runDep({ cwd, mode: "human", args: ["add", nodes.taskB, nodes.taskA] });
|
|
98
|
-
expect(cycle.ok).toBeFalse();
|
|
99
|
-
expect(cycle.error?.code).toBe("invalid_dependency");
|
|
100
|
-
});
|
|
101
|
-
});
|