invoket 0.1.8 → 0.1.10
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/CLAUDE.md +57 -0
- package/README.md +118 -143
- package/package.json +3 -2
- package/src/cli.ts +64 -8
- package/src/parser.ts +41 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Add reusable commands to `tasks.ts`. invoket gives them typed args, `--help`, and error handling.
|
|
4
|
+
|
|
5
|
+
Use invoket when a command will be reused. Write a raw script for throwaway one-offs.
|
|
6
|
+
|
|
7
|
+
## Task skeleton
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Context } from "invoket/context";
|
|
11
|
+
|
|
12
|
+
export class Tasks {
|
|
13
|
+
/** Description (required — no JSDoc = not discovered) */
|
|
14
|
+
async taskName(c: Context, required: string, optional: number = 1) {}
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`async` + JSDoc + `c: Context` first param. `_prefix` = private.
|
|
19
|
+
|
|
20
|
+
## Types → CLI
|
|
21
|
+
|
|
22
|
+
`string` as-is, `number` rejects NaN, `boolean` accepts `true/false/1/0/--flag/--no-flag`, `Interface` and `Record` expect JSON string, `type[]` expects JSON array, `...args: string[]` collects remaining, `= default` and `| null` make optional.
|
|
23
|
+
|
|
24
|
+
## Flags
|
|
25
|
+
|
|
26
|
+
Auto: `--paramName`. For short flags add `@flag` in JSDoc:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
/** @flag env -e --environment */
|
|
30
|
+
async deploy(c: Context, env: string) {}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Namespaces
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
class Db {
|
|
37
|
+
/** Migrate */
|
|
38
|
+
async migrate(c: Context, direction: string = "up") {}
|
|
39
|
+
}
|
|
40
|
+
export class Tasks {
|
|
41
|
+
db = new Db(); // invt db:migrate up
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Context
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
await c.run(cmd); // throw on failure
|
|
49
|
+
await c.run(cmd, { warn: true }); // don't throw
|
|
50
|
+
const { stdout } = await c.run(cmd, { hide: true }); // capture
|
|
51
|
+
await c.run(cmd, { stream: true }); // real-time output
|
|
52
|
+
await c.run(cmd, { echo: true }); // print command first
|
|
53
|
+
await c.sudo(cmd); // sudo prefix
|
|
54
|
+
for await (const _ of c.cd(dir)) { } // temp cd
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Result: `{ stdout, stderr, code, ok, failed }`. Failure throws `CommandError` with `.result`.
|
package/README.md
CHANGED
|
@@ -28,13 +28,21 @@ One script solves one problem. A `tasks.ts` file is a project's command centre.
|
|
|
28
28
|
## Installation
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
bun add -d invoket
|
|
32
|
-
bun
|
|
31
|
+
bun add -d invoket # Add to project (use bunx invt to run)
|
|
32
|
+
bun add -g invoket # Or install globally (invt available on PATH)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
Then scaffold your project:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bunx invt --init # Creates tasks.ts and CLAUDE.md
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`--init` creates a starter `tasks.ts` and copies the `CLAUDE.md` reference card into your project so AI agents know how to write tasks.
|
|
42
|
+
|
|
35
43
|
## Quick Start
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
Edit `tasks.ts`:
|
|
38
46
|
|
|
39
47
|
```typescript
|
|
40
48
|
import { Context } from "invoket/context";
|
|
@@ -153,6 +161,7 @@ Flags and positional args can be freely mixed in any order.
|
|
|
153
161
|
| `<task> -h` | Help for a specific task |
|
|
154
162
|
| `-l`, `--list` | List tasks |
|
|
155
163
|
| `--version` | Show version |
|
|
164
|
+
| `--init` | Scaffold `tasks.ts` and `CLAUDE.md` |
|
|
156
165
|
|
|
157
166
|
## Context API
|
|
158
167
|
|
|
@@ -300,180 +309,146 @@ EOF`);
|
|
|
300
309
|
|
|
301
310
|
## Agentic Tools
|
|
302
311
|
|
|
303
|
-
|
|
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
|
-
}
|
|
312
|
+
AI agents like Claude Code have built-in tools for searching files, reading code, and running commands. What they lack is **project context** — the state of migrations, the history of decisions, the shape of your API, which tests are flaky and why. That knowledge lives in developers' heads, scattered across commits, issues, and Slack threads.
|
|
330
313
|
|
|
331
|
-
|
|
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
|
-
}
|
|
314
|
+
invoket lets you build a **structured, queryable project knowledge base** that agents can read and write through the same CLI interface humans use. Bun's built-in SQLite makes this trivial — no external database, no setup, just a `.ctx.db` file that travels with the project.
|
|
338
315
|
|
|
339
|
-
|
|
340
|
-
memory = new Memory();
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
```bash
|
|
345
|
-
invt memory:store arch "Monorepo with packages/api and packages/web"
|
|
346
|
-
invt memory:recall arch
|
|
347
|
-
invt memory:list
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
### Task planning — break work into steps
|
|
316
|
+
### Project context — a SQLite-backed knowledge base
|
|
351
317
|
|
|
352
318
|
```typescript
|
|
353
|
-
import {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
319
|
+
import { Context } from "invoket/context";
|
|
320
|
+
import { Database } from "bun:sqlite";
|
|
321
|
+
|
|
322
|
+
class Ctx {
|
|
323
|
+
private db: Database;
|
|
324
|
+
|
|
325
|
+
constructor() {
|
|
326
|
+
this.db = new Database(".ctx.db", { create: true });
|
|
327
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS context (
|
|
328
|
+
key TEXT PRIMARY KEY,
|
|
329
|
+
value TEXT NOT NULL,
|
|
330
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
331
|
+
)`);
|
|
332
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS decisions (
|
|
333
|
+
id INTEGER PRIMARY KEY,
|
|
334
|
+
subject TEXT NOT NULL,
|
|
335
|
+
decision TEXT NOT NULL,
|
|
336
|
+
rationale TEXT,
|
|
337
|
+
status TEXT DEFAULT 'active',
|
|
338
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
339
|
+
)`);
|
|
340
|
+
}
|
|
357
341
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
342
|
+
/** Store a key-value fact about the project */
|
|
343
|
+
async set(c: Context, key: string, ...value: string[]) {
|
|
344
|
+
this.db.run(
|
|
345
|
+
`INSERT OR REPLACE INTO context (key, value, updated_at)
|
|
346
|
+
VALUES (?, ?, datetime('now'))`,
|
|
347
|
+
[key, value.join(" ")]
|
|
348
|
+
);
|
|
349
|
+
console.log(`Set: ${key}`);
|
|
361
350
|
}
|
|
362
351
|
|
|
363
|
-
|
|
364
|
-
|
|
352
|
+
/** Retrieve a fact */
|
|
353
|
+
async get(c: Context, key: string) {
|
|
354
|
+
const row = this.db.query("SELECT value, updated_at FROM context WHERE key = ?").get(key) as any;
|
|
355
|
+
if (!row) { console.log(`Not found: ${key}`); return; }
|
|
356
|
+
console.log(`${row.value} (${row.updated_at})`);
|
|
365
357
|
}
|
|
366
358
|
|
|
367
|
-
/**
|
|
368
|
-
async
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
359
|
+
/** Search facts by keyword */
|
|
360
|
+
async search(c: Context, ...terms: string[]) {
|
|
361
|
+
const pattern = `%${terms.join(" ")}%`;
|
|
362
|
+
const rows = this.db.query(
|
|
363
|
+
"SELECT key, value FROM context WHERE key LIKE ? OR value LIKE ?"
|
|
364
|
+
).all(pattern, pattern) as any[];
|
|
365
|
+
for (const r of rows) console.log(`${r.key}: ${r.value}`);
|
|
373
366
|
}
|
|
374
367
|
|
|
375
|
-
/**
|
|
376
|
-
async
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
368
|
+
/** Record an architectural decision */
|
|
369
|
+
async decide(c: Context, subject: string, decision: string, ...rationale: string[]) {
|
|
370
|
+
this.db.run(
|
|
371
|
+
"INSERT INTO decisions (subject, decision, rationale) VALUES (?, ?, ?)",
|
|
372
|
+
[subject, decision, rationale.join(" ")]
|
|
373
|
+
);
|
|
374
|
+
console.log(`Recorded: ${subject}`);
|
|
381
375
|
}
|
|
382
376
|
|
|
383
|
-
/**
|
|
384
|
-
async
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
377
|
+
/** List active decisions */
|
|
378
|
+
async decisions(c: Context) {
|
|
379
|
+
const rows = this.db.query(
|
|
380
|
+
"SELECT id, subject, decision, rationale FROM decisions WHERE status = 'active' ORDER BY created_at DESC"
|
|
381
|
+
).all() as any[];
|
|
382
|
+
for (const r of rows) {
|
|
383
|
+
console.log(`#${r.id} ${r.subject}: ${r.decision}`);
|
|
384
|
+
if (r.rationale) console.log(` ${r.rationale}`);
|
|
389
385
|
}
|
|
390
386
|
}
|
|
391
387
|
|
|
392
|
-
/**
|
|
393
|
-
async
|
|
394
|
-
this.
|
|
395
|
-
|
|
388
|
+
/** Dump all context as JSON */
|
|
389
|
+
async dump(c: Context) {
|
|
390
|
+
const facts = this.db.query("SELECT key, value FROM context ORDER BY key").all();
|
|
391
|
+
const decisions = this.db.query("SELECT * FROM decisions WHERE status = 'active'").all();
|
|
392
|
+
console.log(JSON.stringify({ facts, decisions }, null, 2));
|
|
396
393
|
}
|
|
397
394
|
}
|
|
398
395
|
|
|
399
396
|
export class Tasks {
|
|
400
|
-
|
|
397
|
+
ctx = new Ctx();
|
|
401
398
|
}
|
|
402
399
|
```
|
|
403
400
|
|
|
404
401
|
```bash
|
|
405
|
-
|
|
406
|
-
invt
|
|
407
|
-
invt
|
|
408
|
-
invt
|
|
409
|
-
|
|
402
|
+
# Store project facts
|
|
403
|
+
invt ctx:set db "Postgres 16 on Supabase, migrations in prisma/"
|
|
404
|
+
invt ctx:set api "REST with /api/v2 prefix, auth via JWT middleware"
|
|
405
|
+
invt ctx:set deploy "Fly.io, auto-deploy on push to main"
|
|
406
|
+
|
|
407
|
+
# Record decisions with rationale
|
|
408
|
+
invt ctx:decide auth "JWT in httpOnly cookies" "Chose over localStorage for XSS protection"
|
|
409
|
+
invt ctx:decide orm "Prisma over Drizzle" "Team familiarity, existing migrations"
|
|
410
|
+
|
|
411
|
+
# Query context
|
|
412
|
+
invt ctx:get db
|
|
413
|
+
invt ctx:search auth
|
|
414
|
+
invt ctx:decisions
|
|
415
|
+
|
|
416
|
+
# Dump everything for agent context
|
|
417
|
+
invt ctx:dump
|
|
410
418
|
```
|
|
411
419
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
```typescript
|
|
415
|
-
import { appendFileSync, existsSync, readFileSync } from "fs";
|
|
420
|
+
An agent starts a session with `invt ctx:dump` and immediately has structured project knowledge — not flat markdown, not grep results, but queryable facts and decisions with timestamps.
|
|
416
421
|
|
|
417
|
-
|
|
418
|
-
private file = ".journal.md";
|
|
422
|
+
### Why SQLite?
|
|
419
423
|
|
|
420
|
-
|
|
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
|
-
}
|
|
424
|
+
Bun bundles SQLite natively — `import { Database } from "bun:sqlite"` just works. No dependencies, no server, no config. The `.ctx.db` file is a single file you can `.gitignore` or commit. You can extend the schema as the project grows: add tables for endpoints, test history, deployment logs, whatever your project needs.
|
|
433
425
|
|
|
434
|
-
|
|
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
|
-
```
|
|
426
|
+
### Growing the schema
|
|
443
427
|
|
|
444
|
-
|
|
428
|
+
The example above is a starting point. A real project might track more:
|
|
445
429
|
|
|
446
430
|
```typescript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
431
|
+
// Track API endpoints and their status
|
|
432
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS endpoints (
|
|
433
|
+
path TEXT PRIMARY KEY,
|
|
434
|
+
method TEXT,
|
|
435
|
+
handler TEXT,
|
|
436
|
+
auth TEXT DEFAULT 'required',
|
|
437
|
+
status TEXT DEFAULT 'active'
|
|
438
|
+
)`);
|
|
439
|
+
|
|
440
|
+
// Track test health
|
|
441
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS test_runs (
|
|
442
|
+
id INTEGER PRIMARY KEY,
|
|
443
|
+
suite TEXT,
|
|
444
|
+
passed INTEGER,
|
|
445
|
+
failed INTEGER,
|
|
446
|
+
skipped INTEGER,
|
|
447
|
+
ran_at TEXT DEFAULT (datetime('now'))
|
|
448
|
+
)`);
|
|
474
449
|
```
|
|
475
450
|
|
|
476
|
-
|
|
451
|
+
The namespace class is the interface. The schema is yours to shape around what your project actually needs to remember.
|
|
477
452
|
|
|
478
453
|
## Requirements
|
|
479
454
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "invoket",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "TypeScript task runner for Bun - uses type annotations to parse CLI arguments",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"./context": "./src/context.ts"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"src"
|
|
13
|
+
"src",
|
|
14
|
+
"CLAUDE.md"
|
|
14
15
|
],
|
|
15
16
|
"keywords": [
|
|
16
17
|
"task-runner",
|
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Context } from "./context";
|
|
3
|
+
import { dirname } from "path";
|
|
3
4
|
import {
|
|
4
5
|
discoverAllTasks,
|
|
5
6
|
discoverRuntimeNamespaces,
|
|
7
|
+
extractImports,
|
|
8
|
+
extractMethodsFromClass,
|
|
9
|
+
findUnresolvedNamespaces,
|
|
6
10
|
parseCommand,
|
|
7
11
|
parseCliArgs,
|
|
8
12
|
resolveArgs,
|
|
@@ -23,13 +27,18 @@ async function main() {
|
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// --init flag: scaffold tasks.ts and CLAUDE.md
|
|
31
|
+
if (args[0] === "--init") {
|
|
32
|
+
const { existsSync } = await import("fs");
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
|
|
35
|
+
const tasksFile = `${cwd}/tasks.ts`;
|
|
36
|
+
if (existsSync(tasksFile)) {
|
|
37
|
+
console.log("tasks.ts already exists, skipping.");
|
|
38
|
+
} else {
|
|
39
|
+
await Bun.write(
|
|
40
|
+
tasksFile,
|
|
41
|
+
`import { Context } from "invoket/context";
|
|
33
42
|
|
|
34
43
|
export class Tasks {
|
|
35
44
|
/** Say hello */
|
|
@@ -37,7 +46,36 @@ export class Tasks {
|
|
|
37
46
|
console.log("Hello, World!");
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
|
-
|
|
49
|
+
`,
|
|
50
|
+
);
|
|
51
|
+
console.log("Created tasks.ts");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const claudeFile = `${cwd}/CLAUDE.md`;
|
|
55
|
+
const claudeMdPath = new URL("../CLAUDE.md", import.meta.url).pathname;
|
|
56
|
+
const claudeMd = await Bun.file(claudeMdPath).text();
|
|
57
|
+
if (existsSync(claudeFile)) {
|
|
58
|
+
const existing = await Bun.file(claudeFile).text();
|
|
59
|
+
if (existing.includes("invoket")) {
|
|
60
|
+
console.log("CLAUDE.md already has invoket section, skipping.");
|
|
61
|
+
} else {
|
|
62
|
+
await Bun.write(claudeFile, existing.trimEnd() + "\n\n" + claudeMd);
|
|
63
|
+
console.log("Appended invoket guide to CLAUDE.md");
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
await Bun.write(claudeFile, claudeMd);
|
|
67
|
+
console.log("Created CLAUDE.md");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Find tasks.ts
|
|
74
|
+
let tasksPath: string;
|
|
75
|
+
try {
|
|
76
|
+
tasksPath = Bun.resolveSync("./tasks.ts", process.cwd());
|
|
77
|
+
} catch {
|
|
78
|
+
console.log("No tasks.ts found. Run 'invt --init' to get started.");
|
|
41
79
|
process.exit(1);
|
|
42
80
|
}
|
|
43
81
|
|
|
@@ -51,6 +89,24 @@ export class Tasks {
|
|
|
51
89
|
// Discover all tasks including namespaced
|
|
52
90
|
const discovered = discoverAllTasks(source);
|
|
53
91
|
|
|
92
|
+
// Resolve imported namespace classes from their source files
|
|
93
|
+
const imports = extractImports(source);
|
|
94
|
+
const unresolved = findUnresolvedNamespaces(source, discovered);
|
|
95
|
+
for (const [propName, className] of unresolved) {
|
|
96
|
+
const importPath = imports.get(className);
|
|
97
|
+
if (!importPath) continue;
|
|
98
|
+
try {
|
|
99
|
+
const resolvedPath = Bun.resolveSync(importPath, dirname(tasksPath));
|
|
100
|
+
const importedSource = await Bun.file(resolvedPath).text();
|
|
101
|
+
const methods = extractMethodsFromClass(importedSource, className);
|
|
102
|
+
if (methods.size > 0) {
|
|
103
|
+
discovered.namespaced.set(propName, methods);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Can't resolve import — runtime discovery will handle it
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
// Also discover imported namespaces from runtime
|
|
55
111
|
discoverRuntimeNamespaces(instance, discovered);
|
|
56
112
|
|
package/src/parser.ts
CHANGED
|
@@ -219,6 +219,47 @@ export function parseParams(
|
|
|
219
219
|
return params;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// Extract import statements: maps imported identifiers to their module paths
|
|
223
|
+
export function extractImports(
|
|
224
|
+
source: string,
|
|
225
|
+
): Map<string, string> {
|
|
226
|
+
const imports = new Map<string, string>();
|
|
227
|
+
const pattern =
|
|
228
|
+
/import\s+(?!\s*type\s)(?:\{([^}]+)\}|(\w+))\s+from\s+["']([^"']+)["']/g;
|
|
229
|
+
let match;
|
|
230
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
231
|
+
const [, namedImports, defaultImport, importPath] = match;
|
|
232
|
+
if (namedImports) {
|
|
233
|
+
for (const spec of namedImports.split(",")) {
|
|
234
|
+
const parts = spec.trim().split(/\s+as\s+/);
|
|
235
|
+
const localName = parts[parts.length - 1].trim();
|
|
236
|
+
if (localName) imports.set(localName, importPath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (defaultImport) {
|
|
240
|
+
imports.set(defaultImport, importPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return imports;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Find namespace assignments whose class wasn't found in the local source
|
|
247
|
+
export function findUnresolvedNamespaces(
|
|
248
|
+
source: string,
|
|
249
|
+
discovered: DiscoveredTasks,
|
|
250
|
+
): Map<string, string> {
|
|
251
|
+
const unresolved = new Map<string, string>(); // propName -> className
|
|
252
|
+
const nsPattern = /(\w+)\s*=\s*new\s+(\w+)\s*\(\s*\)/g;
|
|
253
|
+
let match;
|
|
254
|
+
while ((match = nsPattern.exec(source)) !== null) {
|
|
255
|
+
const [, propName, className] = match;
|
|
256
|
+
if (propName.startsWith("_")) continue;
|
|
257
|
+
if (discovered.namespaced.has(propName)) continue;
|
|
258
|
+
unresolved.set(propName, className);
|
|
259
|
+
}
|
|
260
|
+
return unresolved;
|
|
261
|
+
}
|
|
262
|
+
|
|
222
263
|
// Discover all tasks including namespaced ones (source parsing only)
|
|
223
264
|
export function discoverAllTasks(source: string): DiscoveredTasks {
|
|
224
265
|
const root = extractMethodsFromClass(source, "Tasks");
|