kadai 0.4.0 → 0.5.0
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 +4 -1
- package/dist/cli.js +710 -123
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# kadai
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. Drop scripts into `.kadai/actions/`.
|
|
6
|
+
2. Run with `bunx kadai`.
|
|
7
|
+
3. Share them with your team in the repo.
|
|
8
|
+
4. Automatically make them discoverable by AI.
|
|
6
9
|
|
|
7
10
|
## Getting Started
|
|
8
11
|
|
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,8 @@ async function loadConfig(kadaiDir) {
|
|
|
45
45
|
const userConfig = mod.default ?? mod;
|
|
46
46
|
return {
|
|
47
47
|
actionsDir: userConfig.actionsDir ?? DEFAULT_CONFIG.actionsDir,
|
|
48
|
-
env: userConfig.env ?? DEFAULT_CONFIG.env
|
|
48
|
+
env: userConfig.env ?? DEFAULT_CONFIG.env,
|
|
49
|
+
plugins: userConfig.plugins
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
var DEFAULT_CONFIG;
|
|
@@ -198,14 +199,14 @@ async function getGitAddedDates(dir) {
|
|
|
198
199
|
} catch {}
|
|
199
200
|
return dates;
|
|
200
201
|
}
|
|
201
|
-
async function loadActions(actionsDir) {
|
|
202
|
+
async function loadActions(actionsDir, origin = { type: "local" }) {
|
|
202
203
|
const actions = [];
|
|
203
204
|
const gitDates = await getGitAddedDates(actionsDir);
|
|
204
|
-
await scanDirectory(actionsDir, actionsDir, [], actions, 0, gitDates);
|
|
205
|
+
await scanDirectory(actionsDir, actionsDir, [], actions, 0, gitDates, origin);
|
|
205
206
|
actions.sort((a, b) => a.meta.name.localeCompare(b.meta.name));
|
|
206
207
|
return actions;
|
|
207
208
|
}
|
|
208
|
-
async function scanDirectory(baseDir, currentDir, category, actions, depth, gitDates) {
|
|
209
|
+
async function scanDirectory(baseDir, currentDir, category, actions, depth, gitDates, origin = { type: "local" }) {
|
|
209
210
|
if (depth > 3)
|
|
210
211
|
return;
|
|
211
212
|
let entries;
|
|
@@ -219,7 +220,7 @@ async function scanDirectory(baseDir, currentDir, category, actions, depth, gitD
|
|
|
219
220
|
continue;
|
|
220
221
|
const fullPath = join2(currentDir, entry.name);
|
|
221
222
|
if (entry.isDirectory()) {
|
|
222
|
-
await scanDirectory(baseDir, fullPath, [...category, entry.name], actions, depth + 1, gitDates);
|
|
223
|
+
await scanDirectory(baseDir, fullPath, [...category, entry.name], actions, depth + 1, gitDates, origin);
|
|
223
224
|
} else if (entry.isFile()) {
|
|
224
225
|
const ext = `.${entry.name.split(".").pop()}`;
|
|
225
226
|
if (!SUPPORTED_EXTENSIONS.has(ext))
|
|
@@ -237,6 +238,7 @@ async function scanDirectory(baseDir, currentDir, category, actions, depth, gitD
|
|
|
237
238
|
category,
|
|
238
239
|
runtime: runtimeFromExtension(ext),
|
|
239
240
|
addedAt,
|
|
241
|
+
origin,
|
|
240
242
|
...shebang ? { shebang } : {}
|
|
241
243
|
});
|
|
242
244
|
}
|
|
@@ -274,12 +276,174 @@ var init_loader = __esm(() => {
|
|
|
274
276
|
]);
|
|
275
277
|
});
|
|
276
278
|
|
|
277
|
-
// src/core/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
279
|
+
// src/core/fetchers/github.ts
|
|
280
|
+
import { mkdir, rm } from "fs/promises";
|
|
281
|
+
import { join as join3 } from "path";
|
|
282
|
+
async function fetchGithubPlugin(source, destDir) {
|
|
283
|
+
const ref = source.ref ?? "main";
|
|
284
|
+
const repoUrl = `https://github.com/${source.github}.git`;
|
|
285
|
+
await mkdir(destDir, { recursive: true });
|
|
286
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--branch", ref, repoUrl, destDir], { stdout: "pipe", stderr: "pipe" });
|
|
287
|
+
const exitCode = await proc.exited;
|
|
288
|
+
if (exitCode !== 0) {
|
|
289
|
+
const stderr = await new Response(proc.stderr).text();
|
|
290
|
+
throw new Error(`Failed to clone "${source.github}" (ref: ${ref}): ${stderr.trim()}`);
|
|
291
|
+
}
|
|
292
|
+
const shaProc = Bun.spawn(["git", "rev-parse", "HEAD"], {
|
|
293
|
+
cwd: destDir,
|
|
294
|
+
stdout: "pipe",
|
|
295
|
+
stderr: "pipe"
|
|
296
|
+
});
|
|
297
|
+
const sha = (await new Response(shaProc.stdout).text()).trim();
|
|
298
|
+
await shaProc.exited;
|
|
299
|
+
await rm(join3(destDir, ".git"), { recursive: true, force: true });
|
|
300
|
+
return { resolvedVersion: sha };
|
|
301
|
+
}
|
|
302
|
+
async function checkGithubUpdate(source, currentSha) {
|
|
303
|
+
try {
|
|
304
|
+
const ref = source.ref ?? "main";
|
|
305
|
+
const repoUrl = `https://github.com/${source.github}.git`;
|
|
306
|
+
const proc = Bun.spawn(["git", "ls-remote", repoUrl, ref], {
|
|
307
|
+
stdout: "pipe",
|
|
308
|
+
stderr: "pipe"
|
|
309
|
+
});
|
|
310
|
+
const output = (await new Response(proc.stdout).text()).trim();
|
|
311
|
+
const exitCode = await proc.exited;
|
|
312
|
+
if (exitCode !== 0)
|
|
313
|
+
return false;
|
|
314
|
+
const remoteSha = output.split("\t")[0] ?? "";
|
|
315
|
+
if (!remoteSha)
|
|
316
|
+
return false;
|
|
317
|
+
return remoteSha !== currentSha;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
var init_github = () => {};
|
|
323
|
+
|
|
324
|
+
// src/core/fetchers/npm.ts
|
|
325
|
+
import { mkdir as mkdir2, unlink } from "fs/promises";
|
|
326
|
+
import { join as join4 } from "path";
|
|
327
|
+
function parseSemver(v) {
|
|
328
|
+
const withoutPrerelease = v.replace(/^v/, "").split("-")[0] ?? "";
|
|
329
|
+
const clean = withoutPrerelease.split("+")[0] ?? "";
|
|
330
|
+
const parts = clean.split(".");
|
|
331
|
+
if (parts.length !== 3)
|
|
332
|
+
return null;
|
|
333
|
+
const nums = parts.map(Number);
|
|
334
|
+
if (nums.some((n) => Number.isNaN(n)))
|
|
335
|
+
return null;
|
|
336
|
+
return nums;
|
|
337
|
+
}
|
|
338
|
+
function compareSemver(a, b) {
|
|
339
|
+
for (let i = 0;i < 3; i++) {
|
|
340
|
+
const av = a[i];
|
|
341
|
+
const bv = b[i];
|
|
342
|
+
if (av !== bv)
|
|
343
|
+
return av - bv;
|
|
344
|
+
}
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
function satisfies(version, range) {
|
|
348
|
+
if (range === "*" || range === "x")
|
|
349
|
+
return true;
|
|
350
|
+
const parsed = parseSemver(version);
|
|
351
|
+
if (!parsed)
|
|
352
|
+
return false;
|
|
353
|
+
if (range.startsWith("^")) {
|
|
354
|
+
const min = parseSemver(range.slice(1));
|
|
355
|
+
if (!min)
|
|
356
|
+
return false;
|
|
357
|
+
if (compareSemver(parsed, min) < 0)
|
|
358
|
+
return false;
|
|
359
|
+
if (min[0] === 0) {
|
|
360
|
+
return parsed[0] === 0 && parsed[1] === min[1];
|
|
361
|
+
}
|
|
362
|
+
return parsed[0] === min[0];
|
|
363
|
+
}
|
|
364
|
+
if (range.startsWith("~")) {
|
|
365
|
+
const min = parseSemver(range.slice(1));
|
|
366
|
+
if (!min)
|
|
367
|
+
return false;
|
|
368
|
+
if (compareSemver(parsed, min) < 0)
|
|
369
|
+
return false;
|
|
370
|
+
return parsed[0] === min[0] && parsed[1] === min[1];
|
|
371
|
+
}
|
|
372
|
+
const exact = parseSemver(range);
|
|
373
|
+
if (!exact)
|
|
374
|
+
return false;
|
|
375
|
+
return compareSemver(parsed, exact) === 0;
|
|
376
|
+
}
|
|
377
|
+
async function resolveVersion(source) {
|
|
378
|
+
const version = source.version ?? "latest";
|
|
379
|
+
const res = await fetch(`${REGISTRY}/${encodeURIComponent(source.npm)}`);
|
|
380
|
+
if (!res.ok) {
|
|
381
|
+
throw new Error(`Failed to fetch npm package "${source.npm}": ${res.status} ${res.statusText}`);
|
|
382
|
+
}
|
|
383
|
+
const meta = await res.json();
|
|
384
|
+
const tagVersion = meta["dist-tags"][version];
|
|
385
|
+
if (tagVersion) {
|
|
386
|
+
const versionData = meta.versions[tagVersion];
|
|
387
|
+
if (!versionData) {
|
|
388
|
+
throw new Error(`npm package "${source.npm}": version ${tagVersion} not found in registry`);
|
|
389
|
+
}
|
|
390
|
+
return { version: tagVersion, tarballUrl: versionData.dist.tarball };
|
|
391
|
+
}
|
|
392
|
+
const exactData = meta.versions[version];
|
|
393
|
+
if (exactData) {
|
|
394
|
+
return { version, tarballUrl: exactData.dist.tarball };
|
|
395
|
+
}
|
|
396
|
+
const allVersions = Object.keys(meta.versions);
|
|
397
|
+
const matching = allVersions.filter((v) => satisfies(v, version)).map((v) => ({ version: v, parsed: parseSemver(v) })).filter((v) => v.parsed !== null).sort((a, b) => compareSemver(b.parsed, a.parsed));
|
|
398
|
+
if (matching.length === 0) {
|
|
399
|
+
throw new Error(`npm package "${source.npm}": no version matching "${version}" found`);
|
|
400
|
+
}
|
|
401
|
+
const bestMatch = matching[0];
|
|
402
|
+
if (!bestMatch) {
|
|
403
|
+
throw new Error(`npm package "${source.npm}": no version matching "${version}" found`);
|
|
404
|
+
}
|
|
405
|
+
const best = bestMatch.version;
|
|
406
|
+
const bestData = meta.versions[best];
|
|
407
|
+
if (!bestData) {
|
|
408
|
+
throw new Error(`npm package "${source.npm}": version data for ${best} missing`);
|
|
409
|
+
}
|
|
410
|
+
return { version: best, tarballUrl: bestData.dist.tarball };
|
|
411
|
+
}
|
|
412
|
+
async function fetchNpmPlugin(source, destDir) {
|
|
413
|
+
const { version, tarballUrl } = await resolveVersion(source);
|
|
414
|
+
const tarballRes = await fetch(tarballUrl);
|
|
415
|
+
if (!tarballRes.ok || !tarballRes.body) {
|
|
416
|
+
throw new Error(`Failed to download tarball for "${source.npm}@${version}": ${tarballRes.status}`);
|
|
417
|
+
}
|
|
418
|
+
await mkdir2(destDir, { recursive: true });
|
|
419
|
+
const tarball = await tarballRes.arrayBuffer();
|
|
420
|
+
const tarballPath = join4(destDir, ".plugin.tgz");
|
|
421
|
+
await Bun.write(tarballPath, tarball);
|
|
422
|
+
const proc = Bun.spawn(["tar", "xzf", tarballPath, "--strip-components=1"], {
|
|
423
|
+
cwd: destDir,
|
|
424
|
+
stdout: "pipe",
|
|
425
|
+
stderr: "pipe"
|
|
426
|
+
});
|
|
427
|
+
const exitCode = await proc.exited;
|
|
428
|
+
if (exitCode !== 0) {
|
|
429
|
+
const stderr = await new Response(proc.stderr).text();
|
|
430
|
+
throw new Error(`Failed to extract tarball: ${stderr}`);
|
|
431
|
+
}
|
|
432
|
+
await unlink(tarballPath);
|
|
433
|
+
return { resolvedVersion: version };
|
|
434
|
+
}
|
|
435
|
+
async function checkNpmUpdate(source, currentVersion) {
|
|
436
|
+
try {
|
|
437
|
+
const { version } = await resolveVersion(source);
|
|
438
|
+
return version !== currentVersion;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
var REGISTRY = "https://registry.npmjs.org";
|
|
444
|
+
var init_npm = () => {};
|
|
445
|
+
|
|
446
|
+
// src/core/which.ts
|
|
283
447
|
function cachedWhich(bin) {
|
|
284
448
|
if (whichCache.has(bin))
|
|
285
449
|
return whichCache.get(bin) ?? null;
|
|
@@ -287,6 +451,219 @@ function cachedWhich(bin) {
|
|
|
287
451
|
whichCache.set(bin, result ?? null);
|
|
288
452
|
return result ?? null;
|
|
289
453
|
}
|
|
454
|
+
var whichCache;
|
|
455
|
+
var init_which = __esm(() => {
|
|
456
|
+
whichCache = new Map;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// src/core/pm.ts
|
|
460
|
+
import { join as join5 } from "path";
|
|
461
|
+
async function resolvePM(dir) {
|
|
462
|
+
const pkgJsonPath = join5(dir, "package.json");
|
|
463
|
+
try {
|
|
464
|
+
const file = Bun.file(pkgJsonPath);
|
|
465
|
+
if (await file.exists()) {
|
|
466
|
+
const pkg = await file.json();
|
|
467
|
+
if (typeof pkg.packageManager === "string") {
|
|
468
|
+
const bin = pkg.packageManager.split("@")[0];
|
|
469
|
+
if (bin && cachedWhich(bin)) {
|
|
470
|
+
return { bin, install: [bin, "install"] };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch {}
|
|
475
|
+
for (const candidate of PM_CHAIN) {
|
|
476
|
+
if (cachedWhich(candidate.bin)) {
|
|
477
|
+
return candidate;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
throw new Error(`No package manager found. Install bun or npm to use plugin dependencies.`);
|
|
481
|
+
}
|
|
482
|
+
var PM_CHAIN;
|
|
483
|
+
var init_pm = __esm(() => {
|
|
484
|
+
init_which();
|
|
485
|
+
PM_CHAIN = [
|
|
486
|
+
{ bin: "bun", install: ["bun", "install"] },
|
|
487
|
+
{ bin: "npm", install: ["npm", "install"] }
|
|
488
|
+
];
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// src/core/plugins.ts
|
|
492
|
+
import { existsSync } from "fs";
|
|
493
|
+
import { mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
494
|
+
import { isAbsolute, join as join6, resolve } from "path";
|
|
495
|
+
async function ensurePluginCacheDir(kadaiDir) {
|
|
496
|
+
const cacheDir = join6(kadaiDir, ".cache", "plugins");
|
|
497
|
+
await mkdir3(cacheDir, { recursive: true });
|
|
498
|
+
const gitignorePath = join6(kadaiDir, ".cache", ".gitignore");
|
|
499
|
+
const gitignoreFile = Bun.file(gitignorePath);
|
|
500
|
+
if (!await gitignoreFile.exists()) {
|
|
501
|
+
await Bun.write(gitignorePath, `*
|
|
502
|
+
`);
|
|
503
|
+
}
|
|
504
|
+
return cacheDir;
|
|
505
|
+
}
|
|
506
|
+
function cacheKeyFor(source) {
|
|
507
|
+
if ("npm" in source) {
|
|
508
|
+
const name2 = source.npm.replace("/", "--");
|
|
509
|
+
const version = source.version ?? "latest";
|
|
510
|
+
return `npm/${name2}@${version}`;
|
|
511
|
+
}
|
|
512
|
+
const name = source.github.replace("/", "--");
|
|
513
|
+
const ref = source.ref ?? "main";
|
|
514
|
+
return `github/${name}@${ref}`;
|
|
515
|
+
}
|
|
516
|
+
function pluginDisplayName(source) {
|
|
517
|
+
if ("npm" in source)
|
|
518
|
+
return source.npm;
|
|
519
|
+
if ("github" in source)
|
|
520
|
+
return source.github;
|
|
521
|
+
return source.path;
|
|
522
|
+
}
|
|
523
|
+
async function readPluginMeta(cacheDir) {
|
|
524
|
+
try {
|
|
525
|
+
const file = Bun.file(join6(cacheDir, ".plugin-meta.json"));
|
|
526
|
+
if (!await file.exists())
|
|
527
|
+
return null;
|
|
528
|
+
return await file.json();
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function writePluginMeta(cacheDir, meta) {
|
|
534
|
+
await Bun.write(join6(cacheDir, ".plugin-meta.json"), JSON.stringify(meta, null, 2));
|
|
535
|
+
}
|
|
536
|
+
async function loadCachedPlugins(kadaiDir, plugins) {
|
|
537
|
+
const allActions = [];
|
|
538
|
+
const cacheBase = join6(kadaiDir, ".cache", "plugins");
|
|
539
|
+
for (const source of plugins) {
|
|
540
|
+
if ("path" in source)
|
|
541
|
+
continue;
|
|
542
|
+
const key = cacheKeyFor(source);
|
|
543
|
+
const pluginCacheDir = join6(cacheBase, key);
|
|
544
|
+
const actionsDir = join6(pluginCacheDir, "actions");
|
|
545
|
+
if (!existsSync(actionsDir))
|
|
546
|
+
continue;
|
|
547
|
+
const name = pluginDisplayName(source);
|
|
548
|
+
const origin = { type: "plugin", pluginName: name };
|
|
549
|
+
const actions = await loadActions(actionsDir, origin);
|
|
550
|
+
for (const action of actions) {
|
|
551
|
+
action.category = [name, ...action.category];
|
|
552
|
+
action.id = `${name}/${action.id}`;
|
|
553
|
+
}
|
|
554
|
+
allActions.push(...actions);
|
|
555
|
+
}
|
|
556
|
+
return allActions;
|
|
557
|
+
}
|
|
558
|
+
async function installPluginDeps(pluginDir) {
|
|
559
|
+
const pkgJsonPath = join6(pluginDir, "package.json");
|
|
560
|
+
if (!existsSync(pkgJsonPath))
|
|
561
|
+
return;
|
|
562
|
+
const pm = await resolvePM(pluginDir);
|
|
563
|
+
const proc = Bun.spawn(pm.install, {
|
|
564
|
+
cwd: pluginDir,
|
|
565
|
+
stdout: "pipe",
|
|
566
|
+
stderr: "pipe"
|
|
567
|
+
});
|
|
568
|
+
const exitCode = await proc.exited;
|
|
569
|
+
if (exitCode !== 0) {
|
|
570
|
+
const stderr = await new Response(proc.stderr).text();
|
|
571
|
+
throw new Error(`Failed to install plugin dependencies in ${pluginDir}: ${stderr.trim()}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function syncPlugin(kadaiDir, source) {
|
|
575
|
+
const cacheBase = await ensurePluginCacheDir(kadaiDir);
|
|
576
|
+
const key = cacheKeyFor(source);
|
|
577
|
+
const pluginCacheDir = join6(cacheBase, key);
|
|
578
|
+
const meta = await readPluginMeta(pluginCacheDir);
|
|
579
|
+
if (meta) {
|
|
580
|
+
let needsUpdate = false;
|
|
581
|
+
if ("npm" in source) {
|
|
582
|
+
needsUpdate = await checkNpmUpdate(source, meta.resolvedVersion);
|
|
583
|
+
} else {
|
|
584
|
+
needsUpdate = await checkGithubUpdate(source, meta.resolvedVersion);
|
|
585
|
+
}
|
|
586
|
+
if (!needsUpdate)
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
await rm2(pluginCacheDir, { recursive: true, force: true });
|
|
590
|
+
await mkdir3(pluginCacheDir, { recursive: true });
|
|
591
|
+
let resolvedVersion;
|
|
592
|
+
if ("npm" in source) {
|
|
593
|
+
const result = await fetchNpmPlugin(source, pluginCacheDir);
|
|
594
|
+
resolvedVersion = result.resolvedVersion;
|
|
595
|
+
} else {
|
|
596
|
+
const result = await fetchGithubPlugin(source, pluginCacheDir);
|
|
597
|
+
resolvedVersion = result.resolvedVersion;
|
|
598
|
+
}
|
|
599
|
+
await installPluginDeps(pluginCacheDir);
|
|
600
|
+
await writePluginMeta(pluginCacheDir, {
|
|
601
|
+
fetchedAt: new Date().toISOString(),
|
|
602
|
+
source,
|
|
603
|
+
resolvedVersion
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
async function syncPlugins(kadaiDir, plugins, callbacks) {
|
|
607
|
+
const syncable = plugins.filter((p) => !("path" in p));
|
|
608
|
+
const SYNC_TIMEOUT_MS = 60000;
|
|
609
|
+
await Promise.allSettled(syncable.map(async (source) => {
|
|
610
|
+
const name = pluginDisplayName(source);
|
|
611
|
+
callbacks.onPluginStatus(name, "syncing");
|
|
612
|
+
try {
|
|
613
|
+
await Promise.race([
|
|
614
|
+
syncPlugin(kadaiDir, source),
|
|
615
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Sync timeout for ${name}`)), SYNC_TIMEOUT_MS))
|
|
616
|
+
]);
|
|
617
|
+
callbacks.onPluginStatus(name, "done");
|
|
618
|
+
} catch {
|
|
619
|
+
callbacks.onPluginStatus(name, "error");
|
|
620
|
+
}
|
|
621
|
+
}));
|
|
622
|
+
const allActions = await loadCachedPlugins(kadaiDir, plugins);
|
|
623
|
+
callbacks.onUpdate(allActions);
|
|
624
|
+
}
|
|
625
|
+
async function loadPathPlugin(kadaiDir, source) {
|
|
626
|
+
const pluginRoot = isAbsolute(source.path) ? source.path : resolve(kadaiDir, source.path);
|
|
627
|
+
const actionsDir = join6(pluginRoot, "actions");
|
|
628
|
+
if (!existsSync(actionsDir))
|
|
629
|
+
return [];
|
|
630
|
+
const name = source.path;
|
|
631
|
+
const origin = { type: "plugin", pluginName: name };
|
|
632
|
+
const actions = await loadActions(actionsDir, origin);
|
|
633
|
+
for (const action of actions) {
|
|
634
|
+
action.category = [name, ...action.category];
|
|
635
|
+
action.id = `${name}/${action.id}`;
|
|
636
|
+
}
|
|
637
|
+
return actions;
|
|
638
|
+
}
|
|
639
|
+
async function loadUserGlobalActions() {
|
|
640
|
+
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
641
|
+
if (!homeDir)
|
|
642
|
+
return [];
|
|
643
|
+
const actionsDir = join6(homeDir, ".kadai", "actions");
|
|
644
|
+
if (!existsSync(actionsDir))
|
|
645
|
+
return [];
|
|
646
|
+
const origin = { type: "plugin", pluginName: "~" };
|
|
647
|
+
const actions = await loadActions(actionsDir, origin);
|
|
648
|
+
for (const action of actions) {
|
|
649
|
+
action.category = ["~", ...action.category];
|
|
650
|
+
action.id = `~/${action.id}`;
|
|
651
|
+
}
|
|
652
|
+
return actions;
|
|
653
|
+
}
|
|
654
|
+
var init_plugins = __esm(() => {
|
|
655
|
+
init_github();
|
|
656
|
+
init_npm();
|
|
657
|
+
init_loader();
|
|
658
|
+
init_pm();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// src/core/runner.ts
|
|
662
|
+
var exports_runner = {};
|
|
663
|
+
__export(exports_runner, {
|
|
664
|
+
resolveCommand: () => resolveCommand,
|
|
665
|
+
parseShebangCommand: () => parseShebangCommand
|
|
666
|
+
});
|
|
290
667
|
function parseShebangCommand(shebang, filePath) {
|
|
291
668
|
if (!shebang || !shebang.startsWith("#!"))
|
|
292
669
|
return null;
|
|
@@ -326,9 +703,9 @@ function resolveCommand(action) {
|
|
|
326
703
|
return chainCmd;
|
|
327
704
|
return FALLBACK_COMMANDS[action.runtime](action.filePath);
|
|
328
705
|
}
|
|
329
|
-
var
|
|
706
|
+
var RUNTIME_CHAINS, FALLBACK_COMMANDS;
|
|
330
707
|
var init_runner = __esm(() => {
|
|
331
|
-
|
|
708
|
+
init_which();
|
|
332
709
|
RUNTIME_CHAINS = {
|
|
333
710
|
python: [["uv", "run"], ["python3"], ["python"]],
|
|
334
711
|
bash: [["bash"]],
|
|
@@ -347,6 +724,198 @@ var init_runner = __esm(() => {
|
|
|
347
724
|
};
|
|
348
725
|
});
|
|
349
726
|
|
|
727
|
+
// src/core/commands.ts
|
|
728
|
+
var exports_commands = {};
|
|
729
|
+
__export(exports_commands, {
|
|
730
|
+
handleSync: () => handleSync,
|
|
731
|
+
handleRun: () => handleRun,
|
|
732
|
+
handleList: () => handleList
|
|
733
|
+
});
|
|
734
|
+
import { join as join7 } from "path";
|
|
735
|
+
async function handleList(options) {
|
|
736
|
+
const { kadaiDir, all } = options;
|
|
737
|
+
const config = await loadConfig(kadaiDir);
|
|
738
|
+
const actionsDir = join7(kadaiDir, config.actionsDir ?? "actions");
|
|
739
|
+
let actions = await loadActions(actionsDir);
|
|
740
|
+
const globalActions = await loadUserGlobalActions();
|
|
741
|
+
actions = [...actions, ...globalActions];
|
|
742
|
+
if (config.plugins) {
|
|
743
|
+
for (const source of config.plugins) {
|
|
744
|
+
if ("path" in source) {
|
|
745
|
+
const pathActions = await loadPathPlugin(kadaiDir, source);
|
|
746
|
+
actions = [...actions, ...pathActions];
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const cachedActions = await loadCachedPlugins(kadaiDir, config.plugins);
|
|
750
|
+
actions = [...actions, ...cachedActions];
|
|
751
|
+
}
|
|
752
|
+
const filtered = all ? actions : actions.filter((a) => !a.meta.hidden);
|
|
753
|
+
const output = filtered.map((a) => ({
|
|
754
|
+
id: a.id,
|
|
755
|
+
name: a.meta.name,
|
|
756
|
+
emoji: a.meta.emoji,
|
|
757
|
+
description: a.meta.description,
|
|
758
|
+
category: a.category,
|
|
759
|
+
runtime: a.runtime,
|
|
760
|
+
confirm: a.meta.confirm ?? false,
|
|
761
|
+
fullscreen: a.meta.fullscreen ?? false,
|
|
762
|
+
origin: a.origin
|
|
763
|
+
}));
|
|
764
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
765
|
+
`);
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
async function handleRun(options) {
|
|
769
|
+
const { kadaiDir, actionId, cwd } = options;
|
|
770
|
+
const config = await loadConfig(kadaiDir);
|
|
771
|
+
const actionsDir = join7(kadaiDir, config.actionsDir ?? "actions");
|
|
772
|
+
let actions = await loadActions(actionsDir);
|
|
773
|
+
const globalActions = await loadUserGlobalActions();
|
|
774
|
+
actions = [...actions, ...globalActions];
|
|
775
|
+
if (config.plugins) {
|
|
776
|
+
for (const source of config.plugins) {
|
|
777
|
+
if ("path" in source) {
|
|
778
|
+
const pathActions = await loadPathPlugin(kadaiDir, source);
|
|
779
|
+
actions = [...actions, ...pathActions];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const cachedActions = await loadCachedPlugins(kadaiDir, config.plugins);
|
|
783
|
+
actions = [...actions, ...cachedActions];
|
|
784
|
+
}
|
|
785
|
+
const action = actions.find((a) => a.id === actionId);
|
|
786
|
+
if (!action) {
|
|
787
|
+
process.stderr.write(`Error: action "${actionId}" not found
|
|
788
|
+
`);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
if (action.runtime === "ink") {
|
|
792
|
+
const mod = await import(action.filePath);
|
|
793
|
+
if (typeof mod.default !== "function") {
|
|
794
|
+
process.stderr.write(`Error: "${action.filePath}" does not export a default function component
|
|
795
|
+
`);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
const cleanupFullscreen = action.meta.fullscreen ? enterFullscreen() : undefined;
|
|
799
|
+
const React = await import("react");
|
|
800
|
+
const { render } = await import("ink");
|
|
801
|
+
const instance = render(React.createElement(mod.default, {
|
|
802
|
+
cwd,
|
|
803
|
+
env: config.env ?? {},
|
|
804
|
+
args: [],
|
|
805
|
+
onExit: () => instance.unmount()
|
|
806
|
+
}));
|
|
807
|
+
await instance.waitUntilExit();
|
|
808
|
+
cleanupFullscreen?.();
|
|
809
|
+
process.exit(0);
|
|
810
|
+
}
|
|
811
|
+
const cmd = resolveCommand(action);
|
|
812
|
+
const env = {
|
|
813
|
+
...process.env,
|
|
814
|
+
...config.env ?? {}
|
|
815
|
+
};
|
|
816
|
+
process.stdin.removeAllListeners();
|
|
817
|
+
if (process.stdin.isTTY) {
|
|
818
|
+
process.stdin.setRawMode(false);
|
|
819
|
+
}
|
|
820
|
+
process.stdin.pause();
|
|
821
|
+
process.stdin.unref();
|
|
822
|
+
const proc = Bun.spawn(cmd, {
|
|
823
|
+
cwd,
|
|
824
|
+
stdout: "inherit",
|
|
825
|
+
stderr: "inherit",
|
|
826
|
+
stdin: "inherit",
|
|
827
|
+
env
|
|
828
|
+
});
|
|
829
|
+
const exitCode = await proc.exited;
|
|
830
|
+
process.exit(exitCode);
|
|
831
|
+
}
|
|
832
|
+
async function handleSync(options) {
|
|
833
|
+
const { kadaiDir } = options;
|
|
834
|
+
const config = await loadConfig(kadaiDir);
|
|
835
|
+
if (!config.plugins || config.plugins.length === 0) {
|
|
836
|
+
process.stdout.write(`No plugins configured.
|
|
837
|
+
`);
|
|
838
|
+
process.exit(0);
|
|
839
|
+
}
|
|
840
|
+
process.stdout.write(`Syncing plugins...
|
|
841
|
+
`);
|
|
842
|
+
const results = [];
|
|
843
|
+
await syncPlugins(kadaiDir, config.plugins, {
|
|
844
|
+
onPluginStatus: (name, status) => {
|
|
845
|
+
if (status === "syncing") {
|
|
846
|
+
process.stdout.write(` \u27F3 ${name}
|
|
847
|
+
`);
|
|
848
|
+
} else if (status === "done") {
|
|
849
|
+
results.push({ name, status: "done" });
|
|
850
|
+
} else if (status === "error") {
|
|
851
|
+
results.push({ name, status: "error" });
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
onUpdate: () => {}
|
|
855
|
+
});
|
|
856
|
+
process.stdout.write(`
|
|
857
|
+
`);
|
|
858
|
+
for (const r of results) {
|
|
859
|
+
const icon = r.status === "done" ? "\u2713" : "\u2717";
|
|
860
|
+
process.stdout.write(` ${icon} ${r.name}
|
|
861
|
+
`);
|
|
862
|
+
}
|
|
863
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
864
|
+
if (failed > 0) {
|
|
865
|
+
process.stdout.write(`
|
|
866
|
+
${failed} plugin(s) failed to sync.
|
|
867
|
+
`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
process.stdout.write(`
|
|
871
|
+
All plugins synced.
|
|
872
|
+
`);
|
|
873
|
+
process.exit(0);
|
|
874
|
+
}
|
|
875
|
+
var init_commands = __esm(() => {
|
|
876
|
+
init_config();
|
|
877
|
+
init_loader();
|
|
878
|
+
init_plugins();
|
|
879
|
+
init_runner();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// package.json
|
|
883
|
+
var require_package = __commonJS((exports, module) => {
|
|
884
|
+
module.exports = {
|
|
885
|
+
name: "kadai",
|
|
886
|
+
version: "0.5.0",
|
|
887
|
+
type: "module",
|
|
888
|
+
bin: {
|
|
889
|
+
kadai: "./dist/cli.js"
|
|
890
|
+
},
|
|
891
|
+
scripts: {
|
|
892
|
+
build: "bun build.ts",
|
|
893
|
+
check: "tsc --noEmit && biome check ./src ./test",
|
|
894
|
+
lint: "biome check ./src ./test",
|
|
895
|
+
"lint:fix": "biome check --write ./src ./test"
|
|
896
|
+
},
|
|
897
|
+
files: [
|
|
898
|
+
"dist/"
|
|
899
|
+
],
|
|
900
|
+
devDependencies: {
|
|
901
|
+
"@biomejs/biome": "^2.3.14",
|
|
902
|
+
"@types/bun": "latest",
|
|
903
|
+
"ink-testing-library": "^4.0.0"
|
|
904
|
+
},
|
|
905
|
+
peerDependencies: {
|
|
906
|
+
typescript: "^5"
|
|
907
|
+
},
|
|
908
|
+
dependencies: {
|
|
909
|
+
"@inkjs/ui": "^2.0.0",
|
|
910
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
911
|
+
"@types/react": "^19.2.14",
|
|
912
|
+
fuzzysort: "^3.1.0",
|
|
913
|
+
ink: "^6.7.0",
|
|
914
|
+
react: "^19.2.4"
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
});
|
|
918
|
+
|
|
350
919
|
// src/core/mcp.ts
|
|
351
920
|
var exports_mcp = {};
|
|
352
921
|
__export(exports_mcp, {
|
|
@@ -355,13 +924,13 @@ __export(exports_mcp, {
|
|
|
355
924
|
ensureMcpConfig: () => ensureMcpConfig,
|
|
356
925
|
actionIdToToolName: () => actionIdToToolName
|
|
357
926
|
});
|
|
358
|
-
import { join as
|
|
927
|
+
import { join as join8, resolve as resolve2 } from "path";
|
|
359
928
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
360
929
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
361
930
|
function resolveInvocationCommand() {
|
|
362
931
|
const script = process.argv[1] ?? "";
|
|
363
932
|
if (script.endsWith(".ts") || script.endsWith(".tsx")) {
|
|
364
|
-
return { command: "bun", args: [
|
|
933
|
+
return { command: "bun", args: [resolve2(script), "mcp"] };
|
|
365
934
|
}
|
|
366
935
|
return { command: "bunx", args: ["kadai", "mcp"] };
|
|
367
936
|
}
|
|
@@ -382,7 +951,7 @@ function buildToolDescription(action) {
|
|
|
382
951
|
return parts.join(" ");
|
|
383
952
|
}
|
|
384
953
|
async function ensureMcpConfig(projectRoot) {
|
|
385
|
-
const mcpJsonPath =
|
|
954
|
+
const mcpJsonPath = join8(projectRoot, ".mcp.json");
|
|
386
955
|
const mcpFile = Bun.file(mcpJsonPath);
|
|
387
956
|
const kadaiEntry = resolveInvocationCommand();
|
|
388
957
|
if (await mcpFile.exists()) {
|
|
@@ -415,8 +984,21 @@ async function startMcpServer(kadaiDir, cwd) {
|
|
|
415
984
|
let config = {};
|
|
416
985
|
if (kadaiDir) {
|
|
417
986
|
config = await loadConfig(kadaiDir);
|
|
418
|
-
const actionsDir =
|
|
419
|
-
|
|
987
|
+
const actionsDir = join8(kadaiDir, config.actionsDir ?? "actions");
|
|
988
|
+
let allActions = await loadActions(actionsDir);
|
|
989
|
+
const globalActions = await loadUserGlobalActions();
|
|
990
|
+
allActions = [...allActions, ...globalActions];
|
|
991
|
+
if (config.plugins) {
|
|
992
|
+
for (const source of config.plugins) {
|
|
993
|
+
if ("path" in source) {
|
|
994
|
+
const pathActions = await loadPathPlugin(kadaiDir, source);
|
|
995
|
+
allActions = [...allActions, ...pathActions];
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const cachedActions = await loadCachedPlugins(kadaiDir, config.plugins);
|
|
999
|
+
allActions = [...allActions, ...cachedActions];
|
|
1000
|
+
}
|
|
1001
|
+
visibleActions = allActions.filter((a) => !a.meta.hidden);
|
|
420
1002
|
}
|
|
421
1003
|
const server = new McpServer({ name: "kadai", version: "0.3.0" });
|
|
422
1004
|
const env = {
|
|
@@ -465,6 +1047,7 @@ ${stderr}`);
|
|
|
465
1047
|
var init_mcp = __esm(() => {
|
|
466
1048
|
init_config();
|
|
467
1049
|
init_loader();
|
|
1050
|
+
init_plugins();
|
|
468
1051
|
init_runner();
|
|
469
1052
|
});
|
|
470
1053
|
|
|
@@ -629,29 +1212,70 @@ function StatusBar() {
|
|
|
629
1212
|
var init_StatusBar = () => {};
|
|
630
1213
|
|
|
631
1214
|
// src/hooks/useActions.ts
|
|
632
|
-
import { join as
|
|
1215
|
+
import { join as join9 } from "path";
|
|
633
1216
|
import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
634
1217
|
function useActions({ kadaiDir }) {
|
|
635
1218
|
const [actions, setActions] = useState2([]);
|
|
636
1219
|
const [config, setConfig] = useState2({});
|
|
637
1220
|
const [loading, setLoading] = useState2(true);
|
|
1221
|
+
const [pluginSyncStatuses, setPluginSyncStatuses] = useState2(new Map);
|
|
638
1222
|
const actionsRef = useRef(actions);
|
|
639
1223
|
actionsRef.current = actions;
|
|
640
1224
|
useEffect2(() => {
|
|
641
1225
|
(async () => {
|
|
642
1226
|
const cfg = await loadConfig(kadaiDir);
|
|
643
1227
|
setConfig(cfg);
|
|
644
|
-
const actionsDir =
|
|
1228
|
+
const actionsDir = join9(kadaiDir, cfg.actionsDir ?? "actions");
|
|
645
1229
|
const localActions = await loadActions(actionsDir);
|
|
646
|
-
|
|
1230
|
+
let allActions = [...localActions];
|
|
1231
|
+
const globalActions = await loadUserGlobalActions();
|
|
1232
|
+
allActions = [...allActions, ...globalActions];
|
|
1233
|
+
if (cfg.plugins) {
|
|
1234
|
+
for (const source of cfg.plugins) {
|
|
1235
|
+
if ("path" in source) {
|
|
1236
|
+
const pathActions = await loadPathPlugin(kadaiDir, source);
|
|
1237
|
+
allActions = [...allActions, ...pathActions];
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (cfg.plugins) {
|
|
1242
|
+
const cachedActions = await loadCachedPlugins(kadaiDir, cfg.plugins);
|
|
1243
|
+
allActions = [...allActions, ...cachedActions];
|
|
1244
|
+
}
|
|
1245
|
+
setActions(allActions);
|
|
647
1246
|
setLoading(false);
|
|
1247
|
+
if (cfg.plugins && cfg.plugins.length > 0) {
|
|
1248
|
+
const initialStatuses = new Map;
|
|
1249
|
+
for (const source of cfg.plugins) {
|
|
1250
|
+
if (!("path" in source)) {
|
|
1251
|
+
initialStatuses.set(pluginDisplayName(source), "syncing");
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
setPluginSyncStatuses(initialStatuses);
|
|
1255
|
+
syncPlugins(kadaiDir, cfg.plugins, {
|
|
1256
|
+
onPluginStatus: (name, status) => {
|
|
1257
|
+
setPluginSyncStatuses((prev) => {
|
|
1258
|
+
const next = new Map(prev);
|
|
1259
|
+
next.set(name, status);
|
|
1260
|
+
return next;
|
|
1261
|
+
});
|
|
1262
|
+
},
|
|
1263
|
+
onUpdate: (freshPluginActions) => {
|
|
1264
|
+
setActions((prev) => {
|
|
1265
|
+
const nonCached = prev.filter((a) => a.origin.type === "local" || a.origin.type === "plugin" && (a.origin.pluginName === "~" || cfg.plugins?.some((p) => ("path" in p) && p.path === a.origin.pluginName)));
|
|
1266
|
+
return [...nonCached, ...freshPluginActions];
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
648
1271
|
})();
|
|
649
1272
|
}, [kadaiDir]);
|
|
650
|
-
return { actions, actionsRef, config, loading };
|
|
1273
|
+
return { actions, actionsRef, config, loading, pluginSyncStatuses };
|
|
651
1274
|
}
|
|
652
1275
|
var init_useActions = __esm(() => {
|
|
653
1276
|
init_config();
|
|
654
1277
|
init_loader();
|
|
1278
|
+
init_plugins();
|
|
655
1279
|
});
|
|
656
1280
|
|
|
657
1281
|
// src/hooks/useKeyboard.ts
|
|
@@ -1625,7 +2249,8 @@ import { Box as Box4, Text as Text4, useApp } from "ink";
|
|
|
1625
2249
|
import { jsxDEV as jsxDEV5, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
1626
2250
|
function MenuList({
|
|
1627
2251
|
items,
|
|
1628
|
-
selectedIndex
|
|
2252
|
+
selectedIndex,
|
|
2253
|
+
pluginSyncStatuses
|
|
1629
2254
|
}) {
|
|
1630
2255
|
const hasAnyNew = items.some((item) => item.isNew);
|
|
1631
2256
|
return /* @__PURE__ */ jsxDEV5(Fragment2, {
|
|
@@ -1653,12 +2278,16 @@ function MenuList({
|
|
|
1653
2278
|
/* @__PURE__ */ jsxDEV5(Text4, {
|
|
1654
2279
|
color: selected ? "cyan" : undefined,
|
|
1655
2280
|
children: [
|
|
1656
|
-
item.type === "category" ? "\uD83D\uDCC1 " : "",
|
|
2281
|
+
item.type === "category" ? item.isPlugin ? "\uD83D\uDCE6 " : "\uD83D\uDCC1 " : "",
|
|
1657
2282
|
item.type === "action" && item.emoji ? `${item.emoji} ` : "",
|
|
1658
2283
|
item.label,
|
|
1659
2284
|
item.type === "category" ? " \u25B8" : ""
|
|
1660
2285
|
]
|
|
1661
2286
|
}, undefined, true, undefined, this),
|
|
2287
|
+
item.type === "category" && item.isPlugin && pluginSyncStatuses?.get(item.value) === "syncing" && /* @__PURE__ */ jsxDEV5(Text4, {
|
|
2288
|
+
dimColor: true,
|
|
2289
|
+
children: " \u27F3"
|
|
2290
|
+
}, undefined, false, undefined, this),
|
|
1662
2291
|
item.description && /* @__PURE__ */ jsxDEV5(Text4, {
|
|
1663
2292
|
dimColor: true,
|
|
1664
2293
|
children: [
|
|
@@ -1680,7 +2309,7 @@ function App({ kadaiDir, onRunAction }) {
|
|
|
1680
2309
|
};
|
|
1681
2310
|
const search = useSearch();
|
|
1682
2311
|
const nav = useNavigation({ onExit: exit, onNavigate: search.resetSearch });
|
|
1683
|
-
const { actions, actionsRef, config, loading } = useActions({
|
|
2312
|
+
const { actions, actionsRef, config, loading, pluginSyncStatuses } = useActions({
|
|
1684
2313
|
kadaiDir
|
|
1685
2314
|
});
|
|
1686
2315
|
useKeyboard({
|
|
@@ -1744,7 +2373,8 @@ function App({ kadaiDir, onRunAction }) {
|
|
|
1744
2373
|
children: "No matching items"
|
|
1745
2374
|
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5(MenuList, {
|
|
1746
2375
|
items: filteredItems,
|
|
1747
|
-
selectedIndex: search.selectedIndex
|
|
2376
|
+
selectedIndex: search.selectedIndex,
|
|
2377
|
+
pluginSyncStatuses
|
|
1748
2378
|
}, undefined, false, undefined, this),
|
|
1749
2379
|
/* @__PURE__ */ jsxDEV5(StatusBar, {}, undefined, false, undefined, this)
|
|
1750
2380
|
]
|
|
@@ -1824,6 +2454,12 @@ function buildMenuItems(actions, path) {
|
|
|
1824
2454
|
const categories = new Set;
|
|
1825
2455
|
const items = [];
|
|
1826
2456
|
const newActionIds = new Set;
|
|
2457
|
+
const pluginCategories = new Set;
|
|
2458
|
+
for (const action of actions) {
|
|
2459
|
+
if (action.origin.type === "plugin" && action.category.length > 0) {
|
|
2460
|
+
pluginCategories.add(action.category[0]);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
1827
2463
|
for (const action of actions) {
|
|
1828
2464
|
if (isRecentlyAdded(action)) {
|
|
1829
2465
|
newActionIds.add(action.id);
|
|
@@ -1838,7 +2474,8 @@ function buildMenuItems(actions, path) {
|
|
|
1838
2474
|
items.push({
|
|
1839
2475
|
type: "category",
|
|
1840
2476
|
label: topCategory,
|
|
1841
|
-
value: topCategory
|
|
2477
|
+
value: topCategory,
|
|
2478
|
+
isPlugin: pluginCategories.has(topCategory)
|
|
1842
2479
|
});
|
|
1843
2480
|
}
|
|
1844
2481
|
} else {
|
|
@@ -1882,6 +2519,18 @@ function buildMenuItems(actions, path) {
|
|
|
1882
2519
|
items.sort((a, b) => {
|
|
1883
2520
|
if (a.type !== b.type)
|
|
1884
2521
|
return a.type === "category" ? -1 : 1;
|
|
2522
|
+
if (a.type === "category" && b.type === "category") {
|
|
2523
|
+
const aPlugin = a.isPlugin ?? false;
|
|
2524
|
+
const bPlugin = b.isPlugin ?? false;
|
|
2525
|
+
if (aPlugin !== bPlugin)
|
|
2526
|
+
return aPlugin ? -1 : 1;
|
|
2527
|
+
if (aPlugin && bPlugin) {
|
|
2528
|
+
if (a.value === "~")
|
|
2529
|
+
return -1;
|
|
2530
|
+
if (b.value === "~")
|
|
2531
|
+
return 1;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
1885
2534
|
return a.label.localeCompare(b.label);
|
|
1886
2535
|
});
|
|
1887
2536
|
return items;
|
|
@@ -1905,8 +2554,8 @@ __export(exports_init_wizard, {
|
|
|
1905
2554
|
writeInitFiles: () => writeInitFiles,
|
|
1906
2555
|
generateConfigFile: () => generateConfigFile
|
|
1907
2556
|
});
|
|
1908
|
-
import { existsSync, mkdirSync } from "fs";
|
|
1909
|
-
import { join as
|
|
2557
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
2558
|
+
import { join as join10 } from "path";
|
|
1910
2559
|
function generateConfigFile() {
|
|
1911
2560
|
const lines = [' // actionsDir: "actions",', " // env: {},"];
|
|
1912
2561
|
return `export default {
|
|
@@ -1916,10 +2565,10 @@ ${lines.join(`
|
|
|
1916
2565
|
`;
|
|
1917
2566
|
}
|
|
1918
2567
|
async function writeInitFiles(cwd) {
|
|
1919
|
-
const kadaiDir =
|
|
1920
|
-
const actionsDir =
|
|
2568
|
+
const kadaiDir = join10(cwd, ".kadai");
|
|
2569
|
+
const actionsDir = join10(kadaiDir, "actions");
|
|
1921
2570
|
mkdirSync(actionsDir, { recursive: true });
|
|
1922
|
-
const sampleAction =
|
|
2571
|
+
const sampleAction = join10(actionsDir, "hello.sh");
|
|
1923
2572
|
const sampleFile = Bun.file(sampleAction);
|
|
1924
2573
|
let sampleCreated = false;
|
|
1925
2574
|
if (!await sampleFile.exists()) {
|
|
@@ -1934,14 +2583,14 @@ echo "Add your own scripts to .kadai/actions/ to get started."
|
|
|
1934
2583
|
sampleCreated = true;
|
|
1935
2584
|
}
|
|
1936
2585
|
const configContent = generateConfigFile();
|
|
1937
|
-
const configPath =
|
|
2586
|
+
const configPath = join10(kadaiDir, "config.ts");
|
|
1938
2587
|
await Bun.write(configPath, configContent);
|
|
1939
2588
|
let skillCreated = false;
|
|
1940
|
-
const hasClaudeDir =
|
|
1941
|
-
const hasClaudeMd =
|
|
2589
|
+
const hasClaudeDir = existsSync2(join10(cwd, ".claude"));
|
|
2590
|
+
const hasClaudeMd = existsSync2(join10(cwd, "CLAUDE.md"));
|
|
1942
2591
|
if (hasClaudeDir || hasClaudeMd) {
|
|
1943
|
-
const skillDir =
|
|
1944
|
-
const skillPath =
|
|
2592
|
+
const skillDir = join10(cwd, ".claude", "skills", "kadai");
|
|
2593
|
+
const skillPath = join10(skillDir, "SKILL.md");
|
|
1945
2594
|
if (!await Bun.file(skillPath).exists()) {
|
|
1946
2595
|
mkdirSync(skillDir, { recursive: true });
|
|
1947
2596
|
await Bun.write(skillPath, generateSkillFile());
|
|
@@ -2053,6 +2702,9 @@ function parseArgs(argv) {
|
|
|
2053
2702
|
return { type: "interactive" };
|
|
2054
2703
|
}
|
|
2055
2704
|
const command = argv[0];
|
|
2705
|
+
if (command === "--version" || command === "-v") {
|
|
2706
|
+
return { type: "version" };
|
|
2707
|
+
}
|
|
2056
2708
|
switch (command) {
|
|
2057
2709
|
case "list": {
|
|
2058
2710
|
if (!argv.includes("--json")) {
|
|
@@ -2073,87 +2725,18 @@ function parseArgs(argv) {
|
|
|
2073
2725
|
}
|
|
2074
2726
|
case "mcp":
|
|
2075
2727
|
return { type: "mcp" };
|
|
2728
|
+
case "sync":
|
|
2729
|
+
return { type: "sync" };
|
|
2076
2730
|
default:
|
|
2077
2731
|
return {
|
|
2078
2732
|
type: "error",
|
|
2079
|
-
message: `Unknown command: ${command}. Available commands: list, run, mcp`
|
|
2733
|
+
message: `Unknown command: ${command}. Available commands: list, run, sync, mcp, --version`
|
|
2080
2734
|
};
|
|
2081
2735
|
}
|
|
2082
2736
|
}
|
|
2083
2737
|
|
|
2084
|
-
// src/core/commands.ts
|
|
2085
|
-
init_config();
|
|
2086
|
-
init_loader();
|
|
2087
|
-
init_runner();
|
|
2088
|
-
import { join as join3 } from "path";
|
|
2089
|
-
async function handleList(options) {
|
|
2090
|
-
const { kadaiDir, all } = options;
|
|
2091
|
-
const config = await loadConfig(kadaiDir);
|
|
2092
|
-
const actionsDir = join3(kadaiDir, config.actionsDir ?? "actions");
|
|
2093
|
-
const actions = await loadActions(actionsDir);
|
|
2094
|
-
const filtered = all ? actions : actions.filter((a) => !a.meta.hidden);
|
|
2095
|
-
const output = filtered.map((a) => ({
|
|
2096
|
-
id: a.id,
|
|
2097
|
-
name: a.meta.name,
|
|
2098
|
-
emoji: a.meta.emoji,
|
|
2099
|
-
description: a.meta.description,
|
|
2100
|
-
category: a.category,
|
|
2101
|
-
runtime: a.runtime,
|
|
2102
|
-
confirm: a.meta.confirm ?? false,
|
|
2103
|
-
fullscreen: a.meta.fullscreen ?? false
|
|
2104
|
-
}));
|
|
2105
|
-
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
2106
|
-
`);
|
|
2107
|
-
process.exit(0);
|
|
2108
|
-
}
|
|
2109
|
-
async function handleRun(options) {
|
|
2110
|
-
const { kadaiDir, actionId, cwd } = options;
|
|
2111
|
-
const config = await loadConfig(kadaiDir);
|
|
2112
|
-
const actionsDir = join3(kadaiDir, config.actionsDir ?? "actions");
|
|
2113
|
-
const actions = await loadActions(actionsDir);
|
|
2114
|
-
const action = actions.find((a) => a.id === actionId);
|
|
2115
|
-
if (!action) {
|
|
2116
|
-
process.stderr.write(`Error: action "${actionId}" not found
|
|
2117
|
-
`);
|
|
2118
|
-
process.exit(1);
|
|
2119
|
-
}
|
|
2120
|
-
if (action.runtime === "ink") {
|
|
2121
|
-
const mod = await import(action.filePath);
|
|
2122
|
-
if (typeof mod.default !== "function") {
|
|
2123
|
-
process.stderr.write(`Error: "${action.filePath}" does not export a default function component
|
|
2124
|
-
`);
|
|
2125
|
-
process.exit(1);
|
|
2126
|
-
}
|
|
2127
|
-
const cleanupFullscreen = action.meta.fullscreen ? enterFullscreen() : undefined;
|
|
2128
|
-
const React = await import("react");
|
|
2129
|
-
const { render } = await import("ink");
|
|
2130
|
-
const instance = render(React.createElement(mod.default, {
|
|
2131
|
-
cwd,
|
|
2132
|
-
env: config.env ?? {},
|
|
2133
|
-
args: [],
|
|
2134
|
-
onExit: () => instance.unmount()
|
|
2135
|
-
}));
|
|
2136
|
-
await instance.waitUntilExit();
|
|
2137
|
-
cleanupFullscreen?.();
|
|
2138
|
-
process.exit(0);
|
|
2139
|
-
}
|
|
2140
|
-
const cmd = resolveCommand(action);
|
|
2141
|
-
const env = {
|
|
2142
|
-
...process.env,
|
|
2143
|
-
...config.env ?? {}
|
|
2144
|
-
};
|
|
2145
|
-
const proc = Bun.spawn(cmd, {
|
|
2146
|
-
cwd,
|
|
2147
|
-
stdout: "inherit",
|
|
2148
|
-
stderr: "inherit",
|
|
2149
|
-
stdin: "inherit",
|
|
2150
|
-
env
|
|
2151
|
-
});
|
|
2152
|
-
const exitCode = await proc.exited;
|
|
2153
|
-
process.exit(exitCode);
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
2738
|
// src/cli.tsx
|
|
2739
|
+
init_commands();
|
|
2157
2740
|
init_loader();
|
|
2158
2741
|
var cwd = process.cwd();
|
|
2159
2742
|
var parsed = parseArgs(process.argv.slice(2));
|
|
@@ -2162,6 +2745,11 @@ if (parsed.type === "error") {
|
|
|
2162
2745
|
`);
|
|
2163
2746
|
process.exit(1);
|
|
2164
2747
|
}
|
|
2748
|
+
if (parsed.type === "version") {
|
|
2749
|
+
const { version } = await Promise.resolve().then(() => __toESM(require_package(), 1));
|
|
2750
|
+
console.log(version);
|
|
2751
|
+
process.exit(0);
|
|
2752
|
+
}
|
|
2165
2753
|
if (parsed.type === "mcp") {
|
|
2166
2754
|
const { ensureMcpConfig: ensureMcpConfig2, startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), exports_mcp));
|
|
2167
2755
|
const kadaiDir = findZcliDir(cwd);
|
|
@@ -2170,7 +2758,7 @@ if (parsed.type === "mcp") {
|
|
|
2170
2758
|
await startMcpServer2(kadaiDir, cwd);
|
|
2171
2759
|
await new Promise(() => {});
|
|
2172
2760
|
}
|
|
2173
|
-
if (parsed.type === "list" || parsed.type === "run") {
|
|
2761
|
+
if (parsed.type === "list" || parsed.type === "run" || parsed.type === "sync") {
|
|
2174
2762
|
const kadaiDir = findZcliDir(cwd);
|
|
2175
2763
|
if (!kadaiDir) {
|
|
2176
2764
|
process.stderr.write(`Error: No .kadai directory found. Run kadai to initialize.
|
|
@@ -2179,8 +2767,11 @@ if (parsed.type === "list" || parsed.type === "run") {
|
|
|
2179
2767
|
}
|
|
2180
2768
|
if (parsed.type === "list") {
|
|
2181
2769
|
await handleList({ kadaiDir, all: parsed.all });
|
|
2182
|
-
} else {
|
|
2770
|
+
} else if (parsed.type === "run") {
|
|
2183
2771
|
await handleRun({ kadaiDir, actionId: parsed.actionId, cwd });
|
|
2772
|
+
} else {
|
|
2773
|
+
const { handleSync: handleSync2 } = await Promise.resolve().then(() => (init_commands(), exports_commands));
|
|
2774
|
+
await handleSync2({ kadaiDir });
|
|
2184
2775
|
}
|
|
2185
2776
|
}
|
|
2186
2777
|
var { Readable } = await import("stream");
|
|
@@ -2299,22 +2890,18 @@ var env = {
|
|
|
2299
2890
|
console.log(`${action.meta.emoji ? `${action.meta.emoji} ` : ""}${action.meta.name}
|
|
2300
2891
|
`);
|
|
2301
2892
|
process.stdin.removeAllListeners("data");
|
|
2893
|
+
process.stdin.removeAllListeners("end");
|
|
2894
|
+
if (process.stdin.isTTY) {
|
|
2895
|
+
process.stdin.setRawMode(false);
|
|
2896
|
+
}
|
|
2897
|
+
process.stdin.pause();
|
|
2898
|
+
process.stdin.unref();
|
|
2302
2899
|
var proc = Bun.spawn(cmd, {
|
|
2303
2900
|
cwd,
|
|
2304
2901
|
stdout: "inherit",
|
|
2305
2902
|
stderr: "inherit",
|
|
2306
|
-
stdin: "
|
|
2903
|
+
stdin: "inherit",
|
|
2307
2904
|
env
|
|
2308
2905
|
});
|
|
2309
|
-
var forwardStdin = (data) => {
|
|
2310
|
-
try {
|
|
2311
|
-
const converted = Buffer.from(data.toString().replace(/\r/g, `
|
|
2312
|
-
`));
|
|
2313
|
-
proc.stdin.write(converted);
|
|
2314
|
-
proc.stdin.flush();
|
|
2315
|
-
} catch {}
|
|
2316
|
-
};
|
|
2317
|
-
process.stdin.on("data", forwardStdin);
|
|
2318
|
-
process.stdin.resume();
|
|
2319
2906
|
var exitCode = await proc.exited;
|
|
2320
2907
|
process.exit(exitCode);
|