trekoon 0.1.0 → 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Esmaabi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,6 +4,27 @@ AI-first issue tracking for humans and agents.
4
4
 
5
5
  Trekoon is a Bun-powered CLI focused on execution workflows where AI agents and humans share the same task graph.
6
6
 
7
+ ## Installation
8
+
9
+ Recommended (global install with Bun):
10
+
11
+ ```bash
12
+ bun add -g trekoon
13
+ ```
14
+
15
+ Then verify:
16
+
17
+ ```bash
18
+ trekoon --help
19
+ trekoon quickstart
20
+ ```
21
+
22
+ Alternative (npm global install):
23
+
24
+ ```bash
25
+ npm i -g trekoon
26
+ ```
27
+
7
28
  ## What Trekoon is
8
29
 
9
30
  - Local-first CLI issue tracker
@@ -31,6 +52,7 @@ Trekoon is a Bun-powered CLI focused on execution workflows where AI agents and
31
52
  - `trekoon subtask <create|list|update|delete>`
32
53
  - `trekoon dep <add|remove|list>`
33
54
  - `trekoon sync <status|pull|resolve>`
55
+ - `trekoon skills install [--link --editor opencode|claude] [--to <path>]`
34
56
  - `trekoon wipe --yes`
35
57
 
36
58
  Global output modes:
@@ -136,7 +158,57 @@ trekoon sync pull --from main
136
158
  trekoon sync resolve <conflict-id> --use ours
137
159
  ```
138
160
 
139
- ### 6) Pre-merge checklist
161
+ ### 6) Install project-local Trekoon skill for agents
162
+
163
+ `trekoon skills install` always writes the bundled skill file into the current
164
+ repository at:
165
+
166
+ - `.agents/skills/trekoon/SKILL.md`
167
+
168
+ You can also create a project-local editor link:
169
+
170
+ ```bash
171
+ trekoon skills install
172
+ trekoon skills install --link --editor opencode
173
+ trekoon skills install --link --editor claude
174
+ trekoon skills install --link --editor opencode --to ./.custom-editor/skills
175
+ ```
176
+
177
+ Path behavior:
178
+
179
+ - Default opencode link path: `.opencode/skills/trekoon`
180
+ - Default claude link path: `.claude/skills/trekoon`
181
+ - `--to <path>` overrides the editor root for link creation only.
182
+ - `--to` does **not** move or copy `SKILL.md` to that path.
183
+ - Re-running install is idempotent: it refreshes `SKILL.md` and reuses/replaces
184
+ the same symlink target.
185
+ - If the link destination exists as a non-link path, install fails with an
186
+ actionable conflict error.
187
+
188
+ How `--to` works (step-by-step):
189
+
190
+ 1. Trekoon always installs/copies to:
191
+ - `<repo>/.agents/skills/trekoon/SKILL.md`
192
+ 2. If `--link` is present, Trekoon creates a `trekoon` symlink directory entry.
193
+ 3. `--to <path>` sets the symlink root directory.
194
+ 4. Final link path is:
195
+ - `<resolved-to-path>/trekoon -> <repo>/.agents/skills/trekoon`
196
+
197
+ Example:
198
+
199
+ ```bash
200
+ trekoon skills install --link --editor opencode --to ./.custom-editor/skills
201
+ ```
202
+
203
+ This produces:
204
+
205
+ - `<repo>/.agents/skills/trekoon/SKILL.md` (copied file)
206
+ - `<repo>/.custom-editor/skills/trekoon` (symlink)
207
+ - symlink target: `<repo>/.agents/skills/trekoon`
208
+
209
+ Trekoon does not mutate global editor config directories.
210
+
211
+ ### 7) Pre-merge checklist
140
212
 
141
213
  - [ ] `trekoon sync status` shows no unresolved conflicts
142
214
  - [ ] done tasks/subtasks are marked completed
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "trekoon",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "AI-first local issue tracker CLI.",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "bin": {
7
8
  "trekoon": "./bin/trekoon"
8
9
  },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ ".agents/skills/trekoon/SKILL.md",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
9
17
  "scripts": {
10
18
  "run": "bun run ./src/index.ts",
11
19
  "build": "bun build ./src/index.ts --outdir ./dist --target bun",
@@ -22,6 +22,7 @@ const ROOT_HELP = [
22
22
  " subtask Subtask lifecycle commands",
23
23
  " dep Dependency graph commands",
24
24
  " sync Cross-branch sync commands",
25
+ " skills Project-local skill install/link commands",
25
26
  ].join("\n");
26
27
 
27
28
  const COMMAND_HELP: Record<string, string> = {
@@ -35,6 +36,8 @@ const COMMAND_HELP: Record<string, string> = {
35
36
  subtask: "Usage: trekoon subtask <subcommand> [options] (list supports --view table|compact)",
36
37
  dep: "Usage: trekoon dep <subcommand> [options]",
37
38
  sync: "Usage: trekoon sync <subcommand> [options]",
39
+ skills:
40
+ "Usage: trekoon skills install [--link --editor opencode|claude] [--to <path>] (--to sets symlink root for --link only; install path always <cwd>/.agents/skills/trekoon/SKILL.md)",
38
41
  help: "Usage: trekoon help [command] [--json|--toon]",
39
42
  };
40
43
 
@@ -0,0 +1,265 @@
1
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { hasFlag, parseArgs, readMissingOptionValue, readOption } from "./arg-parser";
6
+
7
+ import { failResult, okResult } from "../io/output";
8
+ import { type CliContext, type CliResult } from "../runtime/command-types";
9
+
10
+ const SKILLS_USAGE = "Usage: trekoon skills install [--link --editor opencode|claude] [--to <path>]";
11
+ const EDITOR_NAMES = ["opencode", "claude"] as const;
12
+
13
+ type EditorName = (typeof EDITOR_NAMES)[number];
14
+
15
+ interface InstallOutcome {
16
+ readonly sourcePath: string;
17
+ readonly installedPath: string;
18
+ readonly installedDir: string;
19
+ readonly linkPath: string | null;
20
+ readonly linkTarget: string | null;
21
+ }
22
+
23
+ function invalidArgs(message: string): CliResult {
24
+ return failResult({
25
+ command: "skills",
26
+ human: `${message}\n${SKILLS_USAGE}`,
27
+ data: { message },
28
+ error: {
29
+ code: "invalid_args",
30
+ message,
31
+ },
32
+ });
33
+ }
34
+
35
+ function invalidInput(command: string, message: string, data: Record<string, unknown>): CliResult {
36
+ return failResult({
37
+ command,
38
+ human: message,
39
+ data: {
40
+ code: "invalid_input",
41
+ ...data,
42
+ },
43
+ error: {
44
+ code: "invalid_input",
45
+ message,
46
+ },
47
+ });
48
+ }
49
+
50
+ function resolveBundledSkillFilePath(): string {
51
+ return fileURLToPath(new URL("../../.agents/skills/trekoon/SKILL.md", import.meta.url));
52
+ }
53
+
54
+ function toAbsolutePath(cwd: string, pathValue: string): string {
55
+ if (isAbsolute(pathValue)) {
56
+ return pathValue;
57
+ }
58
+
59
+ return resolve(cwd, pathValue);
60
+ }
61
+
62
+ function resolveLinkRoot(cwd: string, editor: EditorName, toOverride: string | undefined): string {
63
+ if (toOverride !== undefined) {
64
+ return toAbsolutePath(cwd, toOverride);
65
+ }
66
+
67
+ if (editor === "opencode") {
68
+ return join(cwd, ".opencode", "skills");
69
+ }
70
+
71
+ return join(cwd, ".claude", "skills");
72
+ }
73
+
74
+ function replaceOrCreateSymlink(linkPath: string, targetPath: string): CliResult | null {
75
+ if (!existsSync(linkPath)) {
76
+ mkdirSync(dirname(linkPath), { recursive: true });
77
+ symlinkSync(targetPath, linkPath, "dir");
78
+ return null;
79
+ }
80
+
81
+ const existing = lstatSync(linkPath);
82
+ if (!existing.isSymbolicLink()) {
83
+ return failResult({
84
+ command: "skills.install",
85
+ human: `Cannot create symlink: path exists and is not a link (${linkPath}).`,
86
+ data: {
87
+ code: "path_conflict",
88
+ linkPath,
89
+ targetPath,
90
+ },
91
+ error: {
92
+ code: "path_conflict",
93
+ message: "Symlink destination exists as a non-link path",
94
+ },
95
+ });
96
+ }
97
+
98
+ const existingRawTarget: string = readlinkSync(linkPath);
99
+ const existingAbsoluteTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
100
+ const expectedTarget: string = resolve(targetPath);
101
+ if (existingAbsoluteTarget !== expectedTarget) {
102
+ return failResult({
103
+ command: "skills.install",
104
+ human: `Cannot replace existing link at ${linkPath}; it points to ${existingAbsoluteTarget}.`,
105
+ data: {
106
+ code: "path_conflict",
107
+ linkPath,
108
+ existingTarget: existingAbsoluteTarget,
109
+ expectedTarget,
110
+ },
111
+ error: {
112
+ code: "path_conflict",
113
+ message: "Symlink destination points to a different target",
114
+ },
115
+ });
116
+ }
117
+
118
+ rmSync(linkPath, { force: true });
119
+ symlinkSync(targetPath, linkPath, "dir");
120
+ return null;
121
+ }
122
+
123
+ function runSkillsInstall(context: CliContext): CliResult {
124
+ const parsed = parseArgs(context.args);
125
+ const missingValue = readMissingOptionValue(parsed.missingOptionValues, "editor", "to");
126
+ if (missingValue !== undefined) {
127
+ return invalidInput("skills.install", `Option --${missingValue} requires a value.`, {
128
+ option: missingValue,
129
+ });
130
+ }
131
+
132
+ if (parsed.positional.length > 1) {
133
+ return invalidArgs("Unexpected positional arguments for skills install.");
134
+ }
135
+
136
+ const wantsLink: boolean = hasFlag(parsed.flags, "link");
137
+ const rawEditor: string | undefined = readOption(parsed.options, "editor");
138
+ const rawTo: string | undefined = readOption(parsed.options, "to");
139
+
140
+ if (!wantsLink && rawEditor !== undefined) {
141
+ return invalidInput("skills.install", "--editor requires --link.", {
142
+ editor: rawEditor,
143
+ });
144
+ }
145
+
146
+ if (!wantsLink && rawTo !== undefined) {
147
+ return invalidInput("skills.install", "--to requires --link.", {
148
+ to: rawTo,
149
+ });
150
+ }
151
+
152
+ if (wantsLink && rawEditor === undefined) {
153
+ return invalidArgs("skills install --link requires --editor opencode|claude.");
154
+ }
155
+
156
+ if (rawEditor !== undefined && !EDITOR_NAMES.includes(rawEditor as EditorName)) {
157
+ return invalidInput("skills.install", "Invalid --editor value. Use: opencode, claude", {
158
+ editor: rawEditor,
159
+ allowedEditors: EDITOR_NAMES,
160
+ });
161
+ }
162
+
163
+ const editor: EditorName | undefined = rawEditor as EditorName | undefined;
164
+
165
+ const sourcePath: string = resolveBundledSkillFilePath();
166
+ if (!existsSync(sourcePath)) {
167
+ return failResult({
168
+ command: "skills.install",
169
+ human: `Bundled skill asset not found at ${sourcePath}`,
170
+ data: {
171
+ code: "missing_asset",
172
+ sourcePath,
173
+ },
174
+ error: {
175
+ code: "missing_asset",
176
+ message: "Bundled skill asset not found",
177
+ },
178
+ });
179
+ }
180
+
181
+ const installPath = join(context.cwd, ".agents", "skills", "trekoon", "SKILL.md");
182
+ const installDir = dirname(installPath);
183
+
184
+ let outcome: InstallOutcome;
185
+
186
+ try {
187
+ mkdirSync(installDir, { recursive: true });
188
+ copyFileSync(sourcePath, installPath);
189
+
190
+ let linkPath: string | null = null;
191
+ let linkTarget: string | null = null;
192
+
193
+ if (wantsLink && editor !== undefined) {
194
+ const linkRoot: string = resolveLinkRoot(context.cwd, editor, rawTo);
195
+ linkPath = join(linkRoot, "trekoon");
196
+ linkTarget = installDir;
197
+ const linkFailure = replaceOrCreateSymlink(linkPath, linkTarget);
198
+ if (linkFailure) {
199
+ return linkFailure;
200
+ }
201
+ }
202
+
203
+ outcome = {
204
+ sourcePath,
205
+ installedPath: installPath,
206
+ installedDir: installDir,
207
+ linkPath,
208
+ linkTarget,
209
+ };
210
+ } catch (error: unknown) {
211
+ const message = error instanceof Error ? error.message : "Unknown skills install failure";
212
+ return failResult({
213
+ command: "skills.install",
214
+ human: `Failed to install skill: ${message}`,
215
+ data: {
216
+ code: "install_failed",
217
+ message,
218
+ },
219
+ error: {
220
+ code: "install_failed",
221
+ message,
222
+ },
223
+ });
224
+ }
225
+
226
+ return okResult({
227
+ command: "skills.install",
228
+ human: outcome.linkPath
229
+ ? [
230
+ "Installed Trekoon skill and linked editor path.",
231
+ `Source: ${outcome.sourcePath}`,
232
+ `Installed file: ${outcome.installedPath}`,
233
+ `Link path: ${outcome.linkPath}`,
234
+ `Link target: ${outcome.linkTarget}`,
235
+ ].join("\n")
236
+ : [
237
+ "Installed Trekoon skill.",
238
+ `Source: ${outcome.sourcePath}`,
239
+ `Installed file: ${outcome.installedPath}`,
240
+ ].join("\n"),
241
+ data: {
242
+ sourcePath: outcome.sourcePath,
243
+ installedPath: outcome.installedPath,
244
+ installedDir: outcome.installedDir,
245
+ linked: outcome.linkPath !== null,
246
+ linkPath: outcome.linkPath,
247
+ linkTarget: outcome.linkTarget,
248
+ },
249
+ });
250
+ }
251
+
252
+ export async function runSkills(context: CliContext): Promise<CliResult> {
253
+ const parsed = parseArgs(context.args);
254
+ const subcommand: string | undefined = parsed.positional[0];
255
+ if (!subcommand) {
256
+ return invalidArgs("Missing skills subcommand.");
257
+ }
258
+
259
+ switch (subcommand) {
260
+ case "install":
261
+ return runSkillsInstall(context);
262
+ default:
263
+ return invalidArgs(`Unknown skills subcommand '${subcommand}'.`);
264
+ }
265
+ }
@@ -3,6 +3,7 @@ import { runDep } from "../commands/dep";
3
3
  import { runEpic } from "../commands/epic";
4
4
  import { runInit } from "../commands/init";
5
5
  import { runQuickstart } from "../commands/quickstart";
6
+ import { runSkills } from "../commands/skills";
6
7
  import { runSubtask } from "../commands/subtask";
7
8
  import { runSync } from "../commands/sync";
8
9
  import { runTask } from "../commands/task";
@@ -13,6 +14,7 @@ import { type CliContext, type CliResult, type OutputMode } from "./command-type
13
14
  const CLI_VERSION = "0.1.0";
14
15
 
15
16
  const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
17
+ "help",
16
18
  "init",
17
19
  "quickstart",
18
20
  "epic",
@@ -20,6 +22,7 @@ const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
20
22
  "subtask",
21
23
  "dep",
22
24
  "sync",
25
+ "skills",
23
26
  "wipe",
24
27
  ];
25
28
 
@@ -128,6 +131,8 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
128
131
  };
129
132
 
130
133
  switch (parsed.command) {
134
+ case "help":
135
+ return runHelp(context);
131
136
  case "init":
132
137
  return runInit(context);
133
138
  case "quickstart":
@@ -144,6 +149,8 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
144
149
  return runDep(context);
145
150
  case "sync":
146
151
  return runSync(context);
152
+ case "skills":
153
+ return runSkills(context);
147
154
  default:
148
155
  return failResult({
149
156
  command: "shell",
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,28 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "workspaces": {
4
- "": {
5
- "name": "trekoon",
6
- "dependencies": {
7
- "@toon-format/toon": "^2.1.0",
8
- },
9
- "devDependencies": {
10
- "@types/bun": "^1.3.0",
11
- "typescript": "^5.9.2",
12
- },
13
- },
14
- },
15
- "packages": {
16
- "@toon-format/toon": ["@toon-format/toon@2.1.0", "", {}, "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg=="],
17
-
18
- "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
19
-
20
- "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
21
-
22
- "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
23
-
24
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
25
-
26
- "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
27
- }
28
- }
@@ -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
- });