@xxkeefer/mrkl 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -26
- package/dist/cli.mjs +153 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<h1 align="center">mrkl</h1>
|
|
3
3
|
<p align="center">
|
|
4
|
+
📝 <i>mrkl, rhymes with sparkle</i> ✨
|
|
5
|
+
<br />
|
|
4
6
|
Lightweight CLI for structured markdown task tracking.
|
|
5
7
|
<br />
|
|
6
8
|
Track work in your repo, not in a separate app.
|
|
@@ -15,17 +17,17 @@
|
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
18
|
-
## Why mrkl?
|
|
20
|
+
## Why mrkl? 🤔
|
|
19
21
|
|
|
20
22
|
Most task trackers live outside your codebase. mrkl keeps tasks as markdown files right alongside your code — version-controlled, greppable, and readable by both humans and AI agents.
|
|
21
23
|
|
|
22
|
-
- **No external service** — tasks live in `.tasks/` as structured markdown
|
|
23
|
-
- **Git-native** — commit, branch, and diff your tasks like any other file
|
|
24
|
-
- **AI-agent friendly** — consistent YAML frontmatter makes tasks easy to parse programmatically
|
|
25
|
-
- **Conventional commits vocabulary** — task types mirror what you already use (`feat`, `fix`, `chore`, etc.)
|
|
26
|
-
- **Zero config** — one command to set up, sensible defaults for everything
|
|
24
|
+
- 🗂️ **No external service** — tasks live in `.tasks/` as structured markdown
|
|
25
|
+
- 🌿 **Git-native** — commit, branch, and diff your tasks like any other file
|
|
26
|
+
- 🤖 **AI-agent friendly** — consistent YAML frontmatter makes tasks easy to parse programmatically
|
|
27
|
+
- 📏 **Conventional commits vocabulary** — task types mirror what you already use (`feat`, `fix`, `chore`, etc.)
|
|
28
|
+
- ⚡ **Zero config** — one command to set up, sensible defaults for everything
|
|
27
29
|
|
|
28
|
-
## Install
|
|
30
|
+
## Install 📦
|
|
29
31
|
|
|
30
32
|
```sh
|
|
31
33
|
pnpm add -g @xxkeefer/mrkl
|
|
@@ -37,7 +39,7 @@ Or use without installing:
|
|
|
37
39
|
npx @xxkeefer/mrkl init MY_PROJECT
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
## Quick Start
|
|
42
|
+
## Quick Start 🚀
|
|
41
43
|
|
|
42
44
|
```sh
|
|
43
45
|
# Initialize in your project root
|
|
@@ -60,9 +62,25 @@ mrkl list --status todo
|
|
|
60
62
|
|
|
61
63
|
# Archive a completed task
|
|
62
64
|
mrkl done PROJ-001
|
|
65
|
+
|
|
66
|
+
# All commands have short aliases
|
|
67
|
+
mrkl c feat "dark mode" # create
|
|
68
|
+
mrkl ls --type fix # list
|
|
69
|
+
mrkl d PROJ-001 # done
|
|
70
|
+
mrkl x PROJ-002 # close
|
|
63
71
|
```
|
|
64
72
|
|
|
65
|
-
## Commands
|
|
73
|
+
## Commands 🛠️
|
|
74
|
+
|
|
75
|
+
| Command | Alias | Description |
|
|
76
|
+
|---------|-------|-------------|
|
|
77
|
+
| `init` | `i` | Initialize mrkl in the current project |
|
|
78
|
+
| `create` | `c` | Create a new task |
|
|
79
|
+
| `list` | `ls` | List active tasks |
|
|
80
|
+
| `done` | `d` | Mark a task as done and archive it |
|
|
81
|
+
| `close` | `x` | Close a task (won't do, duplicate, etc.) and archive it |
|
|
82
|
+
| `prune` | `p` | Delete archived tasks created on or before a given date |
|
|
83
|
+
| `install-skills` | — | Install bundled Claude Code skills |
|
|
66
84
|
|
|
67
85
|
### `mrkl init <prefix>`
|
|
68
86
|
|
|
@@ -89,10 +107,10 @@ Creates a new task file.
|
|
|
89
107
|
| `type` | Task type (see [Task Types](#task-types)) |
|
|
90
108
|
| `title` | Short description of the task |
|
|
91
109
|
|
|
92
|
-
| Option | Description |
|
|
93
|
-
|
|
94
|
-
| `--desc <text>` | Detailed description |
|
|
95
|
-
| `--ac <text>` | Acceptance criterion (repeatable) |
|
|
110
|
+
| Option | Alias | Description |
|
|
111
|
+
|--------|-------|-------------|
|
|
112
|
+
| `--desc <text>` | `-d` | Detailed description |
|
|
113
|
+
| `--ac <text>` | `-a` | Acceptance criterion (repeatable) |
|
|
96
114
|
|
|
97
115
|
```sh
|
|
98
116
|
mrkl create feat "search functionality" \
|
|
@@ -102,14 +120,16 @@ mrkl create feat "search functionality" \
|
|
|
102
120
|
--ac "highlights matching terms"
|
|
103
121
|
```
|
|
104
122
|
|
|
123
|
+
Running `mrkl create` with no arguments enters **interactive mode**, prompting for type, title, description, and acceptance criteria.
|
|
124
|
+
|
|
105
125
|
### `mrkl list [options]`
|
|
106
126
|
|
|
107
127
|
Lists all active tasks.
|
|
108
128
|
|
|
109
|
-
| Option | Description |
|
|
110
|
-
|
|
111
|
-
| `--type <type>` | Filter by task type |
|
|
112
|
-
| `--status <status>` | Filter by status (`todo`, `in-progress`, `done`) |
|
|
129
|
+
| Option | Alias | Description |
|
|
130
|
+
|--------|-------|-------------|
|
|
131
|
+
| `--type <type>` | `-t` | Filter by task type |
|
|
132
|
+
| `--status <status>` | `-s` | Filter by status (`todo`, `in-progress`, `done`) |
|
|
113
133
|
|
|
114
134
|
Non-conforming markdown files in the tasks directory are silently skipped.
|
|
115
135
|
|
|
@@ -123,6 +143,38 @@ Archives a completed task.
|
|
|
123
143
|
|
|
124
144
|
Moves the task file to `.tasks/.archive/` and sets its status to `done`.
|
|
125
145
|
|
|
146
|
+
### `mrkl close <id>`
|
|
147
|
+
|
|
148
|
+
Closes a task that won't be done — duplicates, out-of-scope work, etc.
|
|
149
|
+
|
|
150
|
+
| Argument | Description |
|
|
151
|
+
|----------|-------------|
|
|
152
|
+
| `id` | Task ID to close (e.g., `PROJ-002`) |
|
|
153
|
+
|
|
154
|
+
Sets the task status to `closed` and moves it to `.tasks/.archive/`.
|
|
155
|
+
|
|
156
|
+
### `mrkl prune <date> [options]`
|
|
157
|
+
|
|
158
|
+
Permanently deletes archived tasks created on or before a cutoff date.
|
|
159
|
+
|
|
160
|
+
| Argument | Description |
|
|
161
|
+
|----------|-------------|
|
|
162
|
+
| `date` | Cutoff date (`YYYY-MM-DD` or `YYYYMMDD`) |
|
|
163
|
+
|
|
164
|
+
| Option | Alias | Description |
|
|
165
|
+
|--------|-------|-------------|
|
|
166
|
+
| `--force` | `-f` | Skip confirmation prompt |
|
|
167
|
+
|
|
168
|
+
Shows a confirmation prompt listing tasks to be deleted unless `--force` is used.
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
# Delete archived tasks from January or earlier
|
|
172
|
+
mrkl prune 2026-01-31
|
|
173
|
+
|
|
174
|
+
# Skip confirmation
|
|
175
|
+
mrkl prune 2026-01-31 --force
|
|
176
|
+
```
|
|
177
|
+
|
|
126
178
|
### `mrkl install-skills`
|
|
127
179
|
|
|
128
180
|
Installs bundled Claude Code skills into the current project.
|
|
@@ -131,7 +183,7 @@ Copies skill directories from the mrkl package into `.claude/skills/` so they ar
|
|
|
131
183
|
|
|
132
184
|
```sh
|
|
133
185
|
mrkl install-skills
|
|
134
|
-
#
|
|
186
|
+
# 🧩 Installed plan-from-task
|
|
135
187
|
```
|
|
136
188
|
|
|
137
189
|
Currently ships with:
|
|
@@ -140,7 +192,7 @@ Currently ships with:
|
|
|
140
192
|
|-------|-------------|
|
|
141
193
|
| `plan-from-task` | Generate and execute implementation plans from mrkl task files |
|
|
142
194
|
|
|
143
|
-
## Task Types
|
|
195
|
+
## Task Types 🏷️
|
|
144
196
|
|
|
145
197
|
mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
|
|
146
198
|
|
|
@@ -157,7 +209,7 @@ mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
|
|
|
157
209
|
| `build` | Build system changes |
|
|
158
210
|
| `style` | Code style/formatting |
|
|
159
211
|
|
|
160
|
-
## Task File Format
|
|
212
|
+
## Task File Format 📄
|
|
161
213
|
|
|
162
214
|
Each task is a markdown file with YAML frontmatter:
|
|
163
215
|
|
|
@@ -185,7 +237,7 @@ Implement user authentication with OAuth2.
|
|
|
185
237
|
|
|
186
238
|
The format is intentionally simple — edit task files directly when you need to update descriptions, change status, or check off criteria.
|
|
187
239
|
|
|
188
|
-
## Project Structure
|
|
240
|
+
## Project Structure 🗂️
|
|
189
241
|
|
|
190
242
|
After initialization, mrkl adds the following to your project:
|
|
191
243
|
|
|
@@ -203,7 +255,7 @@ your-project/
|
|
|
203
255
|
|
|
204
256
|
Commit `.config/mrkl/` and `.tasks/` to version control. They're designed to be tracked alongside your code.
|
|
205
257
|
|
|
206
|
-
## Team Workflow
|
|
258
|
+
## Team Workflow 👥
|
|
207
259
|
|
|
208
260
|
When using mrkl with **git worktrees** or **protected branches**, task IDs can conflict if multiple branches create tasks concurrently. The fix is a simple convention: **separate planning from execution.**
|
|
209
261
|
|
|
@@ -227,7 +279,7 @@ mrkl done MRKL-019
|
|
|
227
279
|
|
|
228
280
|
The counter only increments on planning branches — one at a time — so IDs never conflict. See **[docs/workflow.md](docs/workflow.md)** for the full guide with examples and edge cases.
|
|
229
281
|
|
|
230
|
-
## Configuration
|
|
282
|
+
## Configuration ⚙️
|
|
231
283
|
|
|
232
284
|
Configuration lives in `.config/mrkl/mrkl.toml` (or `mrkl.toml` at the project root):
|
|
233
285
|
|
|
@@ -241,7 +293,7 @@ tasks_dir = ".tasks"
|
|
|
241
293
|
| `prefix` | *(required)* | Project prefix for task IDs |
|
|
242
294
|
| `tasks_dir` | `".tasks"` | Directory for task files |
|
|
243
295
|
|
|
244
|
-
## Development
|
|
296
|
+
## Development 🧑💻
|
|
245
297
|
|
|
246
298
|
```sh
|
|
247
299
|
git clone https://github.com/xxKeefer/mrkl.git
|
|
@@ -258,10 +310,10 @@ pnpm tsx src/cli.ts list
|
|
|
258
310
|
pnpm build
|
|
259
311
|
```
|
|
260
312
|
|
|
261
|
-
## Contributing
|
|
313
|
+
## Contributing 🤝
|
|
262
314
|
|
|
263
315
|
Contributions are welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for branch protection rules, merge strategy, and development setup.
|
|
264
316
|
|
|
265
|
-
## License
|
|
317
|
+
## License 📜
|
|
266
318
|
|
|
267
319
|
[MIT](LICENSE)
|
package/dist/cli.mjs
CHANGED
|
@@ -58,7 +58,7 @@ const initCommand = defineCommand({
|
|
|
58
58
|
const dir = process.cwd();
|
|
59
59
|
try {
|
|
60
60
|
initConfig(dir, { prefix: args.prefix });
|
|
61
|
-
consola.success("mrkl initialized");
|
|
61
|
+
consola.success("\u{1F389} mrkl initialized");
|
|
62
62
|
} catch (err) {
|
|
63
63
|
consola.error(String(err.message));
|
|
64
64
|
process.exit(1);
|
|
@@ -145,9 +145,7 @@ function createTask(opts) {
|
|
|
145
145
|
function listTasks(filter) {
|
|
146
146
|
const config = loadConfig(filter.dir);
|
|
147
147
|
const tasksDir = join(filter.dir, config.tasks_dir);
|
|
148
|
-
const files = readdirSync(tasksDir).filter(
|
|
149
|
-
(f) => f.endsWith(".md") && !f.startsWith(".")
|
|
150
|
-
);
|
|
148
|
+
const files = readdirSync(tasksDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
151
149
|
let tasks = files.flatMap((f) => {
|
|
152
150
|
try {
|
|
153
151
|
const content = readFileSync(join(tasksDir, f), "utf-8");
|
|
@@ -180,6 +178,70 @@ function archiveTask(dir, id) {
|
|
|
180
178
|
writeFileSync(archivePath, render(task));
|
|
181
179
|
unlinkSync(filePath);
|
|
182
180
|
}
|
|
181
|
+
function parseCutoffDate(input) {
|
|
182
|
+
const normalized = input.replace(/^(\d{4})(\d{2})(\d{2})$/, "$1-$2-$3");
|
|
183
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
|
|
184
|
+
throw new Error(`Invalid date format: "${input}". Expected YYYY-MM-DD or YYYYMMDD.`);
|
|
185
|
+
}
|
|
186
|
+
const [y, m, d] = normalized.split("-").map(Number);
|
|
187
|
+
const date = new Date(y, m - 1, d);
|
|
188
|
+
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
|
|
189
|
+
throw new Error(`Invalid date: "${input}" is not a real calendar date.`);
|
|
190
|
+
}
|
|
191
|
+
return normalized;
|
|
192
|
+
}
|
|
193
|
+
function normalizeCreatedDate(created) {
|
|
194
|
+
if (created instanceof Date) {
|
|
195
|
+
return created.toISOString().slice(0, 10);
|
|
196
|
+
}
|
|
197
|
+
return String(created);
|
|
198
|
+
}
|
|
199
|
+
function pruneTasks(dir, cutoff) {
|
|
200
|
+
const config = loadConfig(dir);
|
|
201
|
+
const archiveDir = join(dir, config.tasks_dir, ".archive");
|
|
202
|
+
if (!existsSync(archiveDir)) {
|
|
203
|
+
return { deleted: [], total: 0 };
|
|
204
|
+
}
|
|
205
|
+
const files = readdirSync(archiveDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
206
|
+
const deleted = [];
|
|
207
|
+
for (const f of files) {
|
|
208
|
+
try {
|
|
209
|
+
const content = readFileSync(join(archiveDir, f), "utf-8");
|
|
210
|
+
const task = parse(content, f);
|
|
211
|
+
const created = normalizeCreatedDate(task.created);
|
|
212
|
+
if (created <= cutoff) {
|
|
213
|
+
deleted.push({ id: task.id, title: task.title, created, filename: f });
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { deleted, total: files.length };
|
|
219
|
+
}
|
|
220
|
+
function executePrune(dir, filenames) {
|
|
221
|
+
const config = loadConfig(dir);
|
|
222
|
+
const archiveDir = join(dir, config.tasks_dir, ".archive");
|
|
223
|
+
for (const f of filenames) {
|
|
224
|
+
unlinkSync(join(archiveDir, f));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function closeTask(dir, id) {
|
|
228
|
+
const config = loadConfig(dir);
|
|
229
|
+
const tasksDir = join(dir, config.tasks_dir);
|
|
230
|
+
const idUpper = id.toUpperCase();
|
|
231
|
+
const file = readdirSync(tasksDir).find(
|
|
232
|
+
(f) => f.endsWith(".md") && f.toUpperCase().startsWith(idUpper)
|
|
233
|
+
);
|
|
234
|
+
if (!file) {
|
|
235
|
+
throw new Error(`Task ${id} not found`);
|
|
236
|
+
}
|
|
237
|
+
const filePath = join(tasksDir, file);
|
|
238
|
+
const content = readFileSync(filePath, "utf-8");
|
|
239
|
+
const task = parse(content, file);
|
|
240
|
+
task.status = "closed";
|
|
241
|
+
const archivePath = join(tasksDir, ".archive", file);
|
|
242
|
+
writeFileSync(archivePath, render(task));
|
|
243
|
+
unlinkSync(filePath);
|
|
244
|
+
}
|
|
183
245
|
|
|
184
246
|
const TASK_TYPES = [
|
|
185
247
|
"feat",
|
|
@@ -206,7 +268,7 @@ async function promptForTask(dir) {
|
|
|
206
268
|
});
|
|
207
269
|
if (typeof title === "symbol") process.exit(0);
|
|
208
270
|
if (!title.trim()) {
|
|
209
|
-
consola.error("Title cannot be empty");
|
|
271
|
+
consola.error("\u274C Title cannot be empty");
|
|
210
272
|
process.exit(1);
|
|
211
273
|
}
|
|
212
274
|
const desc = await consola.prompt("Description (optional, enter to skip)", {
|
|
@@ -262,7 +324,7 @@ const createCommand = defineCommand({
|
|
|
262
324
|
const dir = process.cwd();
|
|
263
325
|
const interactive = !args.type && !args.title;
|
|
264
326
|
if (!interactive && (!args.type || !args.title)) {
|
|
265
|
-
consola.error("Both type and title are required, or omit both for interactive mode");
|
|
327
|
+
consola.error("\u274C Both type and title are required, or omit both for interactive mode");
|
|
266
328
|
process.exit(1);
|
|
267
329
|
}
|
|
268
330
|
try {
|
|
@@ -270,7 +332,7 @@ const createCommand = defineCommand({
|
|
|
270
332
|
dir,
|
|
271
333
|
type: (() => {
|
|
272
334
|
if (!TASK_TYPES.includes(args.type)) {
|
|
273
|
-
consola.error(
|
|
335
|
+
consola.error(`\u274C Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
|
|
274
336
|
process.exit(1);
|
|
275
337
|
}
|
|
276
338
|
return args.type;
|
|
@@ -280,7 +342,7 @@ const createCommand = defineCommand({
|
|
|
280
342
|
acceptance_criteria: args.ac ? Array.isArray(args.ac) ? args.ac : [args.ac] : void 0
|
|
281
343
|
};
|
|
282
344
|
const task = createTask(opts);
|
|
283
|
-
consola.success(
|
|
345
|
+
consola.success(`\u{1F4DD} Created ${task.id}: ${task.title}`);
|
|
284
346
|
} catch (err) {
|
|
285
347
|
consola.error(String(err.message));
|
|
286
348
|
process.exit(1);
|
|
@@ -314,7 +376,7 @@ const listCommand = defineCommand({
|
|
|
314
376
|
status: args.status
|
|
315
377
|
});
|
|
316
378
|
if (tasks.length === 0) {
|
|
317
|
-
consola.info("No tasks found");
|
|
379
|
+
consola.info("\u{1F4ED} No tasks found");
|
|
318
380
|
return;
|
|
319
381
|
}
|
|
320
382
|
for (const task of tasks) {
|
|
@@ -343,7 +405,81 @@ const doneCommand = defineCommand({
|
|
|
343
405
|
const dir = process.cwd();
|
|
344
406
|
try {
|
|
345
407
|
archiveTask(dir, args.id);
|
|
346
|
-
consola.success(
|
|
408
|
+
consola.success(`\u2705 Archived ${args.id}`);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
consola.error(String(err.message));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const pruneCommand = defineCommand({
|
|
417
|
+
meta: {
|
|
418
|
+
name: "prune",
|
|
419
|
+
description: "Delete archived tasks created on or before a given date"
|
|
420
|
+
},
|
|
421
|
+
args: {
|
|
422
|
+
date: {
|
|
423
|
+
type: "positional",
|
|
424
|
+
description: "Cutoff date (YYYY-MM-DD or YYYYMMDD)",
|
|
425
|
+
required: true
|
|
426
|
+
},
|
|
427
|
+
force: {
|
|
428
|
+
type: "boolean",
|
|
429
|
+
alias: "f",
|
|
430
|
+
description: "Skip confirmation prompt",
|
|
431
|
+
default: false
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
async run({ args }) {
|
|
435
|
+
const dir = process.cwd();
|
|
436
|
+
let cutoff;
|
|
437
|
+
try {
|
|
438
|
+
cutoff = parseCutoffDate(args.date);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
consola.error(String(err.message));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const result = pruneTasks(dir, cutoff);
|
|
444
|
+
if (result.deleted.length === 0) {
|
|
445
|
+
consola.info(`\u{1F4ED} No archived tasks found on or before ${cutoff}`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
consola.info(`\u{1F50D} Found ${result.deleted.length} task(s) to prune:`);
|
|
449
|
+
for (const task of result.deleted) {
|
|
450
|
+
consola.log(` ${task.id} \u2014 ${task.title} (${task.created})`);
|
|
451
|
+
}
|
|
452
|
+
if (!args.force) {
|
|
453
|
+
const confirm = await consola.prompt("Delete these tasks?", {
|
|
454
|
+
type: "confirm"
|
|
455
|
+
});
|
|
456
|
+
if (typeof confirm === "symbol" || !confirm) {
|
|
457
|
+
consola.info("\u{1F44B} Aborted");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
executePrune(dir, result.deleted.map((t) => t.filename));
|
|
462
|
+
consola.success(`\u{1F9F9} Pruned ${result.deleted.length} archived task(s)`);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const closeCommand = defineCommand({
|
|
467
|
+
meta: {
|
|
468
|
+
name: "close",
|
|
469
|
+
description: "Close a task (won't do, duplicate, etc.) and archive it"
|
|
470
|
+
},
|
|
471
|
+
args: {
|
|
472
|
+
id: {
|
|
473
|
+
type: "positional",
|
|
474
|
+
description: "Task ID to close (e.g., VON-001)",
|
|
475
|
+
required: true
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
run({ args }) {
|
|
479
|
+
const dir = process.cwd();
|
|
480
|
+
try {
|
|
481
|
+
closeTask(dir, args.id);
|
|
482
|
+
consola.success(`\u{1F6AB} Closed ${args.id}`);
|
|
347
483
|
} catch (err) {
|
|
348
484
|
consola.error(String(err.message));
|
|
349
485
|
process.exit(1);
|
|
@@ -385,21 +521,21 @@ const installSkillsCommand = defineCommand({
|
|
|
385
521
|
const packageRoot = findPackageRoot();
|
|
386
522
|
const skillsDir = join(packageRoot, "skills");
|
|
387
523
|
if (!existsSync(skillsDir)) {
|
|
388
|
-
consola.error("No skills directory found in mrkl package");
|
|
524
|
+
consola.error("\u274C No skills directory found in mrkl package");
|
|
389
525
|
process.exit(1);
|
|
390
526
|
}
|
|
391
527
|
const skills = readdirSync(skillsDir).filter(
|
|
392
528
|
(f) => statSync(join(skillsDir, f)).isDirectory()
|
|
393
529
|
);
|
|
394
530
|
if (skills.length === 0) {
|
|
395
|
-
consola.info("No skills to install");
|
|
531
|
+
consola.info("\u{1F4ED} No skills to install");
|
|
396
532
|
return;
|
|
397
533
|
}
|
|
398
534
|
for (const skill of skills) {
|
|
399
535
|
const src = join(skillsDir, skill);
|
|
400
536
|
const target = join(dest, skill);
|
|
401
537
|
copyDirSync(src, target);
|
|
402
|
-
consola.success(
|
|
538
|
+
consola.success(`\u{1F9E9} Installed ${skill}`);
|
|
403
539
|
}
|
|
404
540
|
} catch (err) {
|
|
405
541
|
consola.error(String(err.message));
|
|
@@ -423,6 +559,10 @@ const main = defineCommand({
|
|
|
423
559
|
ls: listCommand,
|
|
424
560
|
done: doneCommand,
|
|
425
561
|
d: doneCommand,
|
|
562
|
+
prune: pruneCommand,
|
|
563
|
+
p: pruneCommand,
|
|
564
|
+
close: closeCommand,
|
|
565
|
+
x: closeCommand,
|
|
426
566
|
"install-skills": installSkillsCommand
|
|
427
567
|
}
|
|
428
568
|
});
|