trekoon 0.1.1 → 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/README.md +52 -1
- package/package.json +8 -1
- package/src/commands/help.ts +3 -0
- package/src/commands/skills.ts +265 -0
- package/src/runtime/cli-shell.ts +7 -0
- 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
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ npm i -g trekoon
|
|
|
52
52
|
- `trekoon subtask <create|list|update|delete>`
|
|
53
53
|
- `trekoon dep <add|remove|list>`
|
|
54
54
|
- `trekoon sync <status|pull|resolve>`
|
|
55
|
+
- `trekoon skills install [--link --editor opencode|claude] [--to <path>]`
|
|
55
56
|
- `trekoon wipe --yes`
|
|
56
57
|
|
|
57
58
|
Global output modes:
|
|
@@ -157,7 +158,57 @@ trekoon sync pull --from main
|
|
|
157
158
|
trekoon sync resolve <conflict-id> --use ours
|
|
158
159
|
```
|
|
159
160
|
|
|
160
|
-
### 6)
|
|
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
|
|
161
212
|
|
|
162
213
|
- [ ] `trekoon sync status` shows no unresolved conflicts
|
|
163
214
|
- [ ] done tasks/subtasks are marked completed
|
package/package.json
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trekoon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AI-first local issue tracker CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"trekoon": "./bin/trekoon"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
".agents/skills/trekoon/SKILL.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
10
17
|
"scripts": {
|
|
11
18
|
"run": "bun run ./src/index.ts",
|
|
12
19
|
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
package/src/commands/help.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/runtime/cli-shell.ts
CHANGED
|
@@ -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,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
|
-
});
|