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 +21 -0
- package/README.md +73 -1
- package/package.json +9 -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 -28
- 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/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)
|
|
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.
|
|
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",
|
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,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
|
-
});
|