invoket 0.1.7 → 0.1.8

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 CHANGED
@@ -1,74 +1,91 @@
1
1
  # invoket
2
2
 
3
- A TypeScript task runner for Bun that uses type annotations to parse CLI arguments.
3
+ TypeScript task runner for Bun. Write typed methods, get a CLI for free.
4
4
 
5
- ## Features
6
-
7
- - **Type-safe CLI parsing** — TypeScript types determine how arguments are parsed
8
- - **Zero configuration** — Just write a `Tasks` class with typed methods
9
- - **JSON support** — Object and array parameters are automatically parsed from JSON
10
- - **Namespace support** — Organize tasks with `db:migrate` style namespaces
11
- - **Rest parameters** — Support for `...args` variadic parameters
12
- - **Auto-generated help** — JSDoc descriptions become CLI help text
5
+ ```typescript
6
+ import { Context } from "invoket/context";
13
7
 
14
- ## Installation
8
+ export class Tasks {
9
+ /** Deploy to an environment */
10
+ async deploy(c: Context, env: string, force: boolean = false) {
11
+ await c.run(`deploy.sh ${env}${force ? " --force" : ""}`);
12
+ }
13
+ }
14
+ ```
15
15
 
16
16
  ```bash
17
- bun link invoket
17
+ $ invt deploy prod --force
18
18
  ```
19
19
 
20
- ## Quick Start
20
+ No config files. No argument parser boilerplate. Your TypeScript types *are* the CLI definition.
21
+
22
+ ## Why not just write a script?
23
+
24
+ You could. But then you write arg parsing, help text, and error handling every time. With invoket, you write a method and get all three for free. Your `tasks.ts` becomes a growing toolbox — every task documented, discoverable via `invt --help`, and callable by name with typed arguments.
25
+
26
+ One script solves one problem. A `tasks.ts` file is a project's command centre.
27
+
28
+ ## Installation
21
29
 
22
30
  ```bash
23
- invt # Show help
24
- invt hello World 3 # Run task with args
25
- invt db:migrate up # Run namespaced task
26
- invt --version # Show version
31
+ bun add -d invoket # Add to project
32
+ bun link invoket # Or link globally for development
27
33
  ```
28
34
 
29
- ## Writing Tasks
35
+ ## Quick Start
30
36
 
31
- Create a `tasks.ts` file with a `Tasks` class:
37
+ Create `tasks.ts`:
32
38
 
33
39
  ```typescript
34
40
  import { Context } from "invoket/context";
35
41
 
36
- interface SearchParams {
37
- query: string;
38
- limit?: number;
39
- }
40
-
41
42
  /**
42
- * Project build and deployment tasks
43
+ * My project tasks
43
44
  */
44
45
  export class Tasks {
45
- /** Say hello with a name and repeat count */
46
- async hello(c: Context, name: string, count: number) {
46
+ /**
47
+ * Say hello
48
+ * @flag name -n
49
+ * @flag count -c
50
+ */
51
+ async hello(c: Context, name: string, count: number = 1) {
47
52
  for (let i = 0; i < count; i++) {
48
53
  console.log(`Hello, ${name}!`);
49
54
  }
50
55
  }
51
56
 
52
57
  /** Search with JSON parameters */
53
- async search(c: Context, entity: string, params: SearchParams) {
58
+ async search(c: Context, entity: string, params: { query: string; limit?: number }) {
54
59
  console.log(`Searching ${entity}: ${params.query}`);
55
60
  }
56
61
 
57
- /** Install packages (rest params) */
62
+ /** Install packages */
58
63
  async install(c: Context, ...packages: string[]) {
59
64
  for (const pkg of packages) {
60
- await c.run(`npm install ${pkg}`);
65
+ await c.run(`bun add ${pkg}`);
61
66
  }
62
67
  }
63
68
  }
64
69
  ```
65
70
 
71
+ Run it:
72
+
73
+ ```bash
74
+ invt # Show help
75
+ invt hello World # Positional args
76
+ invt hello -n World -c 3 # Short flags
77
+ invt hello --name=World --count=3 # Long flags
78
+ invt search users '{"query":"bob"}' # JSON params
79
+ invt install react vue angular # Rest params
80
+ invt hello -h # Task-specific help
81
+ ```
82
+
66
83
  ## Namespaces
67
84
 
68
- Organize related tasks into namespaces:
85
+ Group related tasks:
69
86
 
70
87
  ```typescript
71
- class DbNamespace {
88
+ class Db {
72
89
  /** Run database migrations */
73
90
  async migrate(c: Context, direction: string = "up") {
74
91
  await c.run(`prisma migrate ${direction}`);
@@ -81,128 +98,383 @@ class DbNamespace {
81
98
  }
82
99
 
83
100
  export class Tasks {
84
- db = new DbNamespace();
101
+ db = new Db();
85
102
  }
86
103
  ```
87
104
 
88
- Call with `invt db:migrate up` or `invt db.seed`.
105
+ ```bash
106
+ invt db:migrate up # colon separator
107
+ invt db.seed # dot separator also works
108
+ ```
89
109
 
90
- ## Type Mapping
110
+ ## Arguments
91
111
 
92
- | TypeScript | CLI Display | Example Input |
93
- |------------|-------------|---------------|
94
- | `name: string` | `<name>` | `hello` |
95
- | `name: string = "default"` | `[name]` | `hello` (optional) |
112
+ ### Type Mapping
113
+
114
+ | TypeScript | CLI | Example |
115
+ |------------|-----|---------|
116
+ | `name: string` | `<name>` (required) | `hello` |
117
+ | `name: string = "default"` | `[name]` (optional) | `hello` |
96
118
  | `count: number` | `<count>` | `42` |
97
- | `force: boolean` | `<force>` | `true` or `1` |
119
+ | `force: boolean` | `<force>` | `true`, `1`, `false`, `0` |
98
120
  | `params: SomeInterface` | `<params>` | `'{"key": "value"}'` |
99
- | `items: string[]` | `<items>` | `'["a", "b", "c"]'` |
100
- | `...args: string[]` | `[args...]` | `a b c` (variadic) |
101
-
102
- ## CLI Flags
121
+ | `items: string[]` | `<items>` | `'["a", "b"]'` |
122
+ | `...args: string[]` | `[args...]` (variadic) | `a b c` |
103
123
 
104
- | Flag | Description |
105
- |------|-------------|
106
- | `-h`, `--help` | Show help with all tasks |
107
- | `<task> -h` | Show help for a specific task |
108
- | `-l`, `--list` | List available tasks |
109
- | `--version` | Show version |
124
+ ### Flags
110
125
 
111
- ### Task-Specific Help
126
+ Every parameter automatically gets a `--long` flag. Add `@flag` annotations for short flags and aliases:
112
127
 
113
- Get detailed help for any task:
128
+ ```typescript
129
+ /**
130
+ * @flag env -e --environment
131
+ * @flag force -f
132
+ */
133
+ async deploy(c: Context, env: string, force: boolean = false) {}
134
+ ```
114
135
 
115
136
  ```bash
116
- invt hello -h
117
- # Usage: invt hello <name> <count>
118
- #
119
- # Say hello with a name and repeat count
120
- #
121
- # Arguments:
122
- # name string (required)
123
- # count number (required)
124
-
125
- invt db:migrate --help
126
- # Usage: invt db:migrate [direction]
127
- #
128
- # Run database migrations
129
- #
130
- # Arguments:
131
- # direction string (optional)
137
+ invt deploy prod # positional
138
+ invt deploy --env=prod --force # long flags
139
+ invt deploy -e prod -f # short flags
140
+ invt deploy --environment=prod # alias
141
+ invt deploy --no-force # boolean negation
142
+ invt deploy --force=false # explicit boolean
143
+ invt install -- --not-a-flag # -- stops flag parsing
132
144
  ```
133
145
 
146
+ Flags and positional args can be freely mixed in any order.
147
+
148
+ ### CLI Flags
149
+
150
+ | Flag | Description |
151
+ |------|-------------|
152
+ | `-h`, `--help` | Show all tasks |
153
+ | `<task> -h` | Help for a specific task |
154
+ | `-l`, `--list` | List tasks |
155
+ | `--version` | Show version |
156
+
134
157
  ## Context API
135
158
 
136
- Every task receives a `Context` object as the first parameter:
159
+ Every task receives a `Context` for shell execution:
137
160
 
138
161
  ```typescript
139
162
  async deploy(c: Context, env: string) {
140
- // Run shell commands
141
- await c.run("npm run build");
142
-
143
- // Capture output
144
- const { stdout } = await c.run("git rev-parse HEAD", { hide: true });
145
-
146
- // Ignore errors
147
- await c.run("rm -f temp.txt", { warn: true });
148
-
149
- // Echo command before running
150
- await c.run("npm test", { echo: true });
151
-
152
- // Change directory temporarily
153
- for await (const _ of c.cd("subdir")) {
163
+ await c.run("npm run build"); // run command
164
+ const { stdout } = await c.run("git rev-parse HEAD", { hide: true }); // capture output
165
+ await c.run("rm -f temp.txt", { warn: true }); // ignore errors
166
+ await c.run("npm test", { echo: true }); // echo before running
167
+ await c.run("make", { stream: true }); // stream output in real-time
168
+
169
+ for await (const _ of c.cd("subdir")) { // temporary cd
154
170
  await c.run("ls");
155
171
  }
156
-
157
- // Sudo
158
- await c.sudo("apt update");
159
-
160
- // Access config
161
- console.log(c.config); // { echo: false, warn: false, ... }
162
-
163
- // local() is alias for run()
164
- await c.local("echo hello");
172
+
173
+ await c.sudo("apt update"); // sudo prefix
174
+ await c.local("echo hello"); // alias for run()
165
175
  }
166
176
  ```
167
177
 
168
- ### Context Options
178
+ ### Options
169
179
 
170
180
  | Option | Type | Default | Description |
171
181
  |--------|------|---------|-------------|
172
182
  | `echo` | boolean | false | Print command before execution |
173
183
  | `warn` | boolean | false | Don't throw on non-zero exit |
174
184
  | `hide` | boolean | false | Capture output instead of printing |
175
- | `cwd` | string | process.cwd() | Working directory |
185
+ | `stream` | boolean | false | Stream output in real-time |
186
+ | `cwd` | string | `process.cwd()` | Working directory |
176
187
 
177
188
  ### RunResult
178
189
 
179
190
  ```typescript
180
191
  interface RunResult {
181
- stdout: string;
192
+ stdout: string; // captured output (empty when streaming)
182
193
  stderr: string;
183
194
  code: number;
184
- ok: boolean; // code === 0
185
- failed: boolean; // code !== 0
195
+ ok: boolean; // code === 0
196
+ failed: boolean; // code !== 0
197
+ }
198
+ ```
199
+
200
+ ### Error Handling
201
+
202
+ Failed commands throw `CommandError`:
203
+
204
+ ```typescript
205
+ import { CommandError } from "invoket/context";
206
+
207
+ try {
208
+ await c.run("exit 1");
209
+ } catch (e) {
210
+ if (e instanceof CommandError) {
211
+ console.log(e.result.code); // 1
212
+ console.log(e.result.stderr);
213
+ }
186
214
  }
187
215
  ```
188
216
 
217
+ Use `{ warn: true }` to suppress throws and inspect the result instead.
218
+
189
219
  ## Private Methods
190
220
 
191
- Methods starting with `_` are private and won't appear in help or be callable:
221
+ Prefix with `_` to hide from CLI:
192
222
 
193
223
  ```typescript
194
224
  export class Tasks {
195
- async publicTask(c: Context) { }
196
- async _privateHelper(c: Context) { } // Hidden
225
+ async publicTask(c: Context) {
226
+ this._helper();
227
+ }
228
+ async _helper() { } // not discoverable, not callable via CLI
197
229
  }
198
230
  ```
199
231
 
200
- ## Testing
232
+ ## Patterns
233
+
234
+ ### Project setup
235
+
236
+ ```typescript
237
+ /** Bootstrap dev environment */
238
+ async setup(c: Context) {
239
+ await c.run("bun install");
240
+ await c.run("cp .env.example .env", { warn: true });
241
+ await c.run("bun run db:migrate");
242
+ console.log("Ready to go!");
243
+ }
244
+ ```
245
+
246
+ ### Git workflow
247
+
248
+ ```typescript
249
+ /**
250
+ * Commit and push current branch
251
+ * @flag message -m
252
+ */
253
+ async ship(c: Context, message: string) {
254
+ const { stdout } = await c.run("git branch --show-current", { hide: true });
255
+ await c.run("git add -A");
256
+ await c.run(`git commit -m "${message}"`);
257
+ await c.run(`git push -u origin ${stdout.trim()}`);
258
+ }
259
+ ```
260
+
261
+ ### Run with fallback
262
+
263
+ ```typescript
264
+ /** Lint and fix */
265
+ async lint(c: Context) {
266
+ const result = await c.run("eslint . --fix", { warn: true, hide: true });
267
+ if (result.failed) {
268
+ console.log("Lint errors remain:");
269
+ console.log(result.stdout);
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### Capture and transform
275
+
276
+ ```typescript
277
+ /** Show outdated deps */
278
+ async deps(c: Context) {
279
+ const { stdout } = await c.run("bun outdated --json", { hide: true, warn: true });
280
+ const deps = JSON.parse(stdout || "[]");
281
+ for (const d of deps) console.log(`${d.name}: ${d.current} → ${d.latest}`);
282
+ }
283
+ ```
284
+
285
+ ### Scaffold files
286
+
287
+ ```typescript
288
+ /** @flag name -n */
289
+ async component(c: Context, name: string) {
290
+ const upper = name[0].toUpperCase() + name.slice(1);
291
+ await c.run(`mkdir -p src/components/${name}`);
292
+ await c.run(`cat > src/components/${name}/index.tsx << 'EOF'
293
+ export function ${upper}() {
294
+ return <div>${upper}</div>;
295
+ }
296
+ EOF`);
297
+ console.log(`Created src/components/${name}/index.tsx`);
298
+ }
299
+ ```
300
+
301
+ ## Agentic Tools
302
+
303
+ invoket shines as a toolbox for AI agents. Instead of writing ad-hoc scripts each session, the agent adds methods to `tasks.ts` that persist across sessions. `invt --help` shows what tools are available. The file becomes the project's growing command centre.
304
+
305
+ ### Memory — persist context across sessions
306
+
307
+ ```typescript
308
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
309
+
310
+ class Memory {
311
+ private dir = ".memory";
312
+
313
+ private ensure() {
314
+ if (!existsSync(this.dir)) mkdirSync(this.dir, { recursive: true });
315
+ }
316
+
317
+ /** Store a value by key */
318
+ async store(c: Context, key: string, ...value: string[]) {
319
+ this.ensure();
320
+ writeFileSync(`${this.dir}/${key}.md`, value.join(" "));
321
+ console.log(`Stored: ${key}`);
322
+ }
323
+
324
+ /** Recall a value by key */
325
+ async recall(c: Context, key: string) {
326
+ const path = `${this.dir}/${key}.md`;
327
+ if (!existsSync(path)) { console.log(`Not found: ${key}`); return; }
328
+ console.log(readFileSync(path, "utf-8"));
329
+ }
330
+
331
+ /** List all stored keys */
332
+ async list(c: Context) {
333
+ this.ensure();
334
+ const { stdout } = await c.run(`ls ${this.dir}`, { hide: true, warn: true });
335
+ console.log(stdout || "(empty)");
336
+ }
337
+ }
338
+
339
+ export class Tasks {
340
+ memory = new Memory();
341
+ }
342
+ ```
201
343
 
202
344
  ```bash
203
- bun test
345
+ invt memory:store arch "Monorepo with packages/api and packages/web"
346
+ invt memory:recall arch
347
+ invt memory:list
204
348
  ```
205
349
 
350
+ ### Task planning — break work into steps
351
+
352
+ ```typescript
353
+ import { existsSync, readFileSync, writeFileSync } from "fs";
354
+
355
+ class Plan {
356
+ private file = ".plan.json";
357
+
358
+ private load(): { task: string; done: boolean }[] {
359
+ if (!existsSync(this.file)) return [];
360
+ return JSON.parse(readFileSync(this.file, "utf-8"));
361
+ }
362
+
363
+ private save(tasks: { task: string; done: boolean }[]) {
364
+ writeFileSync(this.file, JSON.stringify(tasks, null, 2));
365
+ }
366
+
367
+ /** Add a step to the plan */
368
+ async add(c: Context, ...task: string[]) {
369
+ const tasks = this.load();
370
+ tasks.push({ task: task.join(" "), done: false });
371
+ this.save(tasks);
372
+ console.log(`Added step ${tasks.length}: ${task.join(" ")}`);
373
+ }
374
+
375
+ /** Mark step as done */
376
+ async done(c: Context, step: number) {
377
+ const tasks = this.load();
378
+ tasks[step - 1].done = true;
379
+ this.save(tasks);
380
+ console.log(`Done: ${tasks[step - 1].task}`);
381
+ }
382
+
383
+ /** Show the plan */
384
+ async show(c: Context) {
385
+ const tasks = this.load();
386
+ if (!tasks.length) { console.log("No plan yet."); return; }
387
+ for (const [i, t] of tasks.entries()) {
388
+ console.log(`${t.done ? "✓" : " "} ${i + 1}. ${t.task}`);
389
+ }
390
+ }
391
+
392
+ /** Clear the plan */
393
+ async clear(c: Context) {
394
+ this.save([]);
395
+ console.log("Plan cleared.");
396
+ }
397
+ }
398
+
399
+ export class Tasks {
400
+ plan = new Plan();
401
+ }
402
+ ```
403
+
404
+ ```bash
405
+ invt plan:add "Set up database schema"
406
+ invt plan:add "Write API endpoints"
407
+ invt plan:add "Add tests"
408
+ invt plan:show
409
+ invt plan:done 1
410
+ ```
411
+
412
+ ### Session journal — log decisions
413
+
414
+ ```typescript
415
+ import { appendFileSync, existsSync, readFileSync } from "fs";
416
+
417
+ class Journal {
418
+ private file = ".journal.md";
419
+
420
+ /** Log a decision or finding */
421
+ async log(c: Context, ...entry: string[]) {
422
+ const ts = new Date().toISOString().slice(0, 16);
423
+ appendFileSync(this.file, `\n## ${ts}\n\n${entry.join(" ")}\n`);
424
+ console.log("Logged.");
425
+ }
426
+
427
+ /** Show recent entries */
428
+ async show(c: Context) {
429
+ if (!existsSync(this.file)) { console.log("No journal yet."); return; }
430
+ console.log(readFileSync(this.file, "utf-8"));
431
+ }
432
+ }
433
+
434
+ export class Tasks {
435
+ journal = new Journal();
436
+ }
437
+ ```
438
+
439
+ ```bash
440
+ invt journal:log "Chose Postgres over SQLite for concurrent writes"
441
+ invt journal:show
442
+ ```
443
+
444
+ ### Codebase search — structured context gathering
445
+
446
+ ```typescript
447
+ class Search {
448
+ /** Find files matching a pattern */
449
+ async files(c: Context, pattern: string) {
450
+ await c.run(`find . -name "${pattern}" -not -path "*/node_modules/*"`, { stream: true });
451
+ }
452
+
453
+ /** Search code for a pattern */
454
+ async code(c: Context, pattern: string, ...glob: string[]) {
455
+ const g = glob.length ? `--glob '${glob.join("' --glob '")}'` : "";
456
+ await c.run(`rg "${pattern}" ${g} --type-not binary`, { stream: true, warn: true });
457
+ }
458
+
459
+ /** Summarise project structure */
460
+ async tree(c: Context) {
461
+ await c.run("find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | head -50", { stream: true });
462
+ }
463
+ }
464
+
465
+ export class Tasks {
466
+ search = new Search();
467
+ }
468
+ ```
469
+
470
+ ```bash
471
+ invt search:code "async.*Context" "*.ts"
472
+ invt search:files "*.test.ts"
473
+ invt search:tree
474
+ ```
475
+
476
+ These tasks persist in the project. Every session, the agent starts with `invt --help` and has its full toolbox ready.
477
+
206
478
  ## Requirements
207
479
 
208
480
  - Bun >= 1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "invoket",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "TypeScript task runner for Bun - uses type annotations to parse CLI arguments",
6
6
  "bin": {