@kitnai/cli 0.1.6 → 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/dist/index.js CHANGED
@@ -215,7 +215,7 @@ async function resolveDependencies(names, fetchItem) {
215
215
  const visited = /* @__PURE__ */ new Set();
216
216
  const items = /* @__PURE__ */ new Map();
217
217
  const edges = [];
218
- async function resolve(name) {
218
+ async function resolve3(name) {
219
219
  if (visited.has(name)) return;
220
220
  visited.add(name);
221
221
  const item = await fetchItem(name);
@@ -223,11 +223,11 @@ async function resolveDependencies(names, fetchItem) {
223
223
  const deps = item.registryDependencies ?? [];
224
224
  for (const dep of deps) {
225
225
  edges.push([dep, name]);
226
- await resolve(dep);
226
+ await resolve3(dep);
227
227
  }
228
228
  }
229
229
  for (const name of names) {
230
- await resolve(name);
230
+ await resolve3(name);
231
231
  }
232
232
  return topologicalSort(items, edges);
233
233
  }
@@ -326,25 +326,119 @@ var init_dep_installer = __esm({
326
326
  }
327
327
  });
328
328
 
329
- // src/installers/env-checker.ts
329
+ // src/installers/env-writer.ts
330
+ import * as p2 from "@clack/prompts";
330
331
  import pc3 from "picocolors";
331
- function checkEnvVars(envVars) {
332
- const missing = [];
333
- for (const [key, description] of Object.entries(envVars)) {
334
- if (!process.env[key]) {
335
- missing.push(` ${pc3.yellow(key)}: ${description}`);
332
+ import { readFile as readFile4, writeFile as writeFile4, access as access3 } from "fs/promises";
333
+ import { join as join4 } from "path";
334
+ function parseEnvKeys(content) {
335
+ const keys = /* @__PURE__ */ new Set();
336
+ for (const line of content.split("\n")) {
337
+ const trimmed = line.trim();
338
+ if (!trimmed || trimmed.startsWith("#")) continue;
339
+ const eqIndex = trimmed.indexOf("=");
340
+ if (eqIndex > 0) {
341
+ keys.add(trimmed.slice(0, eqIndex).trim());
336
342
  }
337
343
  }
338
- return missing;
344
+ return keys;
345
+ }
346
+ async function readEnvFile(path) {
347
+ try {
348
+ return await readFile4(path, "utf-8");
349
+ } catch {
350
+ return "";
351
+ }
339
352
  }
340
- var init_env_checker = __esm({
341
- "src/installers/env-checker.ts"() {
353
+ function collectEnvVars(items) {
354
+ const merged = {};
355
+ for (const item of items) {
356
+ if (item.envVars) {
357
+ Object.assign(merged, item.envVars);
358
+ }
359
+ }
360
+ return merged;
361
+ }
362
+ async function handleEnvVars(cwd, envVars) {
363
+ const keys = Object.keys(envVars);
364
+ if (keys.length === 0) return;
365
+ const envPath = join4(cwd, ".env");
366
+ const examplePath = join4(cwd, ".env.example");
367
+ const envContent = await readEnvFile(envPath);
368
+ const exampleContent = await readEnvFile(examplePath);
369
+ const envKeys = parseEnvKeys(envContent);
370
+ const exampleKeys = parseEnvKeys(exampleContent);
371
+ const missingFromExample = keys.filter((k) => !exampleKeys.has(k));
372
+ const missingFromEnv = keys.filter((k) => !envKeys.has(k) && !process.env[k]);
373
+ if (missingFromExample.length > 0) {
374
+ const lines = [];
375
+ if (exampleContent && !exampleContent.endsWith("\n")) lines.push("");
376
+ for (const key of missingFromExample) {
377
+ const config = envVars[key];
378
+ lines.push(`# ${config.description}${config.url ? ` (${config.url})` : ""}`);
379
+ lines.push(`${key}=`);
380
+ }
381
+ await writeFile4(examplePath, exampleContent + lines.join("\n") + "\n");
382
+ p2.log.info(`Updated ${pc3.cyan(".env.example")} with ${missingFromExample.length} variable(s)`);
383
+ }
384
+ if (missingFromEnv.length === 0) return;
385
+ p2.log.message("");
386
+ p2.log.warn(
387
+ `${missingFromEnv.length} environment variable(s) needed:`
388
+ );
389
+ for (const key of missingFromEnv) {
390
+ const config = envVars[key];
391
+ const req = config.required !== false ? pc3.red("*") : "";
392
+ p2.log.message(` ${pc3.yellow(key)}${req}: ${config.description}${config.url ? pc3.dim(` -> ${config.url}`) : ""}`);
393
+ }
394
+ const shouldPrompt = await p2.confirm({
395
+ message: "Would you like to enter values now?",
396
+ initialValue: true
397
+ });
398
+ if (p2.isCancel(shouldPrompt) || !shouldPrompt) {
399
+ p2.log.info(`Add them to ${pc3.cyan(".env")} when ready.`);
400
+ return;
401
+ }
402
+ const newEntries = [];
403
+ for (const key of missingFromEnv) {
404
+ const config = envVars[key];
405
+ const isSecret = config.secret !== false;
406
+ let value;
407
+ if (isSecret) {
408
+ value = await p2.password({
409
+ message: `${key}:`
410
+ });
411
+ } else {
412
+ value = await p2.text({
413
+ message: `${key}:`,
414
+ placeholder: config.description
415
+ });
416
+ }
417
+ if (p2.isCancel(value)) {
418
+ p2.log.info(`Skipped remaining variables. Add them to ${pc3.cyan(".env")} when ready.`);
419
+ break;
420
+ }
421
+ if (value) {
422
+ newEntries.push(`${key}=${value}`);
423
+ }
424
+ }
425
+ if (newEntries.length > 0) {
426
+ const existingEnv = await readEnvFile(envPath);
427
+ const lines = [];
428
+ if (existingEnv && !existingEnv.endsWith("\n")) lines.push("");
429
+ lines.push(...newEntries);
430
+ await writeFile4(envPath, existingEnv + lines.join("\n") + "\n");
431
+ p2.log.success(`Wrote ${newEntries.length} variable(s) to ${pc3.cyan(".env")}`);
432
+ }
433
+ }
434
+ var init_env_writer = __esm({
435
+ "src/installers/env-writer.ts"() {
342
436
  "use strict";
343
437
  }
344
438
  });
345
439
 
346
440
  // src/installers/import-rewriter.ts
347
- import { relative, join as join4 } from "path";
441
+ import { relative, join as join5 } from "path";
348
442
  function rewriteKitnImports(content, fileType, fileName, aliases) {
349
443
  const sourceAliasKey = TYPE_TO_ALIAS_KEY[fileType];
350
444
  if (!sourceAliasKey) return content;
@@ -356,7 +450,7 @@ function rewriteKitnImports(content, fileType, fileName, aliases) {
356
450
  return `${prefix}@kitn/${type}/${targetPath}${quote}`;
357
451
  }
358
452
  const targetDir = aliases[type];
359
- const targetFile = join4(targetDir, targetPath);
453
+ const targetFile = join5(targetDir, targetPath);
360
454
  let rel = relative(sourceDir, targetFile);
361
455
  rel = rel.split("\\").join("/");
362
456
  if (!rel.startsWith(".")) {
@@ -381,10 +475,10 @@ var init_import_rewriter = __esm({
381
475
  });
382
476
 
383
477
  // src/installers/tsconfig-patcher.ts
384
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
385
- import { join as join5 } from "path";
386
- function stripJsonc(text2) {
387
- return text2.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
478
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
479
+ import { join as join6 } from "path";
480
+ function stripJsonc(text3) {
481
+ return text3.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
388
482
  }
389
483
  function patchTsconfig(tsconfigContent, paths) {
390
484
  const config = JSON.parse(stripJsonc(tsconfigContent));
@@ -400,15 +494,15 @@ function patchTsconfig(tsconfigContent, paths) {
400
494
  return JSON.stringify(config, null, 2) + "\n";
401
495
  }
402
496
  async function patchProjectTsconfig(projectDir, paths) {
403
- const tsconfigPath = join5(projectDir, "tsconfig.json");
497
+ const tsconfigPath = join6(projectDir, "tsconfig.json");
404
498
  let content;
405
499
  try {
406
- content = await readFile4(tsconfigPath, "utf-8");
500
+ content = await readFile5(tsconfigPath, "utf-8");
407
501
  } catch {
408
502
  content = "{}";
409
503
  }
410
504
  const patched = patchTsconfig(content, paths);
411
- await writeFile4(tsconfigPath, patched);
505
+ await writeFile5(tsconfigPath, patched);
412
506
  }
413
507
  var init_tsconfig_patcher = __esm({
414
508
  "src/installers/tsconfig-patcher.ts"() {
@@ -457,7 +551,7 @@ var init_parse_ref = __esm({
457
551
 
458
552
  // src/registry/schema.ts
459
553
  import { z as z2 } from "zod";
460
- var componentType2, registryFileSchema, changelogEntrySchema, registryItemSchema, registryIndexItemSchema, registryIndexSchema, typeToDir;
554
+ var componentType2, registryFileSchema, changelogEntrySchema, envVarConfigSchema, componentConfigSchema, registryItemSchema, registryIndexItemSchema, registryIndexSchema, typeToDir;
461
555
  var init_schema = __esm({
462
556
  "src/registry/schema.ts"() {
463
557
  "use strict";
@@ -473,6 +567,31 @@ var init_schema = __esm({
473
567
  type: z2.enum(["feature", "fix", "breaking", "initial"]),
474
568
  note: z2.string()
475
569
  });
570
+ envVarConfigSchema = z2.object({
571
+ description: z2.string(),
572
+ required: z2.boolean().optional(),
573
+ secret: z2.boolean().optional(),
574
+ url: z2.string().optional()
575
+ });
576
+ componentConfigSchema = z2.object({
577
+ $schema: z2.string().optional(),
578
+ type: componentType2,
579
+ name: z2.string().optional(),
580
+ version: z2.string().optional(),
581
+ description: z2.string().optional(),
582
+ dependencies: z2.array(z2.string()).optional(),
583
+ devDependencies: z2.array(z2.string()).optional(),
584
+ registryDependencies: z2.array(z2.string()).optional(),
585
+ files: z2.array(z2.string()).optional(),
586
+ sourceDir: z2.string().optional(),
587
+ exclude: z2.array(z2.string()).optional(),
588
+ installDir: z2.string().optional(),
589
+ tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
590
+ envVars: z2.record(z2.string(), envVarConfigSchema).optional(),
591
+ categories: z2.array(z2.string()).optional(),
592
+ docs: z2.string().optional(),
593
+ changelog: z2.array(changelogEntrySchema).optional()
594
+ });
476
595
  registryItemSchema = z2.object({
477
596
  $schema: z2.string().optional(),
478
597
  name: z2.string(),
@@ -481,7 +600,7 @@ var init_schema = __esm({
481
600
  dependencies: z2.array(z2.string()).optional(),
482
601
  devDependencies: z2.array(z2.string()).optional(),
483
602
  registryDependencies: z2.array(z2.string()).optional(),
484
- envVars: z2.record(z2.string(), z2.string()).optional(),
603
+ envVars: z2.record(z2.string(), envVarConfigSchema).optional(),
485
604
  files: z2.array(registryFileSchema),
486
605
  installDir: z2.string().optional(),
487
606
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
@@ -521,19 +640,19 @@ var add_exports = {};
521
640
  __export(add_exports, {
522
641
  addCommand: () => addCommand
523
642
  });
524
- import * as p2 from "@clack/prompts";
643
+ import * as p3 from "@clack/prompts";
525
644
  import pc4 from "picocolors";
526
- import { join as join6 } from "path";
645
+ import { join as join7 } from "path";
527
646
  async function addCommand(components, opts) {
528
- p2.intro(pc4.bgCyan(pc4.black(" kitn add ")));
647
+ p3.intro(pc4.bgCyan(pc4.black(" kitn add ")));
529
648
  const cwd = process.cwd();
530
649
  const config = await readConfig(cwd);
531
650
  if (!config) {
532
- p2.log.error("No kitn.json found. Run `kitn init` first.");
651
+ p3.log.error("No kitn.json found. Run `kitn init` first.");
533
652
  process.exit(1);
534
653
  }
535
654
  if (components.length === 0) {
536
- p2.log.error("Please specify at least one component to add.");
655
+ p3.log.error("Please specify at least one component to add.");
537
656
  process.exit(1);
538
657
  }
539
658
  const resolvedComponents = components.map((c) => {
@@ -545,7 +664,7 @@ async function addCommand(components, opts) {
545
664
  });
546
665
  const refs = resolvedComponents.map(parseComponentRef);
547
666
  const fetcher = new RegistryFetcher(config.registries);
548
- const s = p2.spinner();
667
+ const s = p3.spinner();
549
668
  s.start("Resolving dependencies...");
550
669
  let resolved;
551
670
  try {
@@ -559,31 +678,27 @@ async function addCommand(components, opts) {
559
678
  });
560
679
  } catch (err) {
561
680
  s.stop(pc4.red("Failed to resolve dependencies"));
562
- p2.log.error(err.message);
681
+ p3.log.error(err.message);
563
682
  process.exit(1);
564
683
  }
565
684
  s.stop(`Resolved ${resolved.length} component(s)`);
566
- p2.log.info("Components to install:");
685
+ p3.log.info("Components to install:");
567
686
  for (const item of resolved) {
568
687
  const isExplicit = resolvedComponents.includes(item.name) || components.includes(item.name);
569
688
  const label = isExplicit ? item.name : `${item.name} ${pc4.dim("(dependency)")}`;
570
- p2.log.message(` ${pc4.cyan(label)}`);
689
+ p3.log.message(` ${pc4.cyan(label)}`);
571
690
  }
572
691
  const created = [];
573
692
  const updated = [];
574
693
  const skipped = [];
575
694
  const allDeps = [];
576
- const allEnvWarnings = [];
577
695
  for (const item of resolved) {
578
696
  if (item.dependencies) allDeps.push(...item.dependencies);
579
- if (item.envVars) {
580
- allEnvWarnings.push(...checkEnvVars(item.envVars));
581
- }
582
697
  if (item.type === "kitn:package") {
583
698
  const baseDir = config.aliases.base ?? "src/ai";
584
699
  for (const file of item.files) {
585
- const targetPath = join6(cwd, baseDir, file.path);
586
- const relativePath = join6(baseDir, file.path);
700
+ const targetPath = join7(cwd, baseDir, file.path);
701
+ const relativePath = join7(baseDir, file.path);
587
702
  const status = await checkFileStatus(targetPath, file.content);
588
703
  switch (status) {
589
704
  case "new" /* New */:
@@ -600,15 +715,15 @@ async function addCommand(components, opts) {
600
715
  } else {
601
716
  const existing = await readExistingFile(targetPath);
602
717
  const diff = generateDiff(relativePath, existing ?? "", file.content);
603
- p2.log.message(pc4.dim(diff));
604
- const action = await p2.select({
718
+ p3.log.message(pc4.dim(diff));
719
+ const action = await p3.select({
605
720
  message: `${relativePath} already exists and differs. What to do?`,
606
721
  options: [
607
722
  { value: "skip", label: "Keep local version" },
608
723
  { value: "overwrite", label: "Overwrite with registry version" }
609
724
  ]
610
725
  });
611
- if (!p2.isCancel(action) && action === "overwrite") {
726
+ if (!p3.isCancel(action) && action === "overwrite") {
612
727
  await writeComponentFile(targetPath, file.content);
613
728
  updated.push(relativePath);
614
729
  } else {
@@ -622,10 +737,10 @@ async function addCommand(components, opts) {
622
737
  const resolvedPaths = {};
623
738
  const installDir = item.installDir ?? item.name;
624
739
  for (const [key, values] of Object.entries(item.tsconfig)) {
625
- resolvedPaths[key] = values.map((v) => `./${join6(baseDir, installDir, v)}`);
740
+ resolvedPaths[key] = values.map((v) => `./${join7(baseDir, installDir, v)}`);
626
741
  }
627
742
  await patchProjectTsconfig(cwd, resolvedPaths);
628
- p2.log.info(`Patched tsconfig.json with paths: ${Object.keys(resolvedPaths).join(", ")}`);
743
+ p3.log.info(`Patched tsconfig.json with paths: ${Object.keys(resolvedPaths).join(", ")}`);
629
744
  }
630
745
  const installed = config.installed ?? {};
631
746
  const allContent = item.files.map((f) => f.content).join("\n");
@@ -635,7 +750,7 @@ async function addCommand(components, opts) {
635
750
  registry: ref.namespace,
636
751
  version: item.version ?? "1.0.0",
637
752
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
638
- files: item.files.map((f) => join6(baseDir, f.path)),
753
+ files: item.files.map((f) => join7(baseDir, f.path)),
639
754
  hash: contentHash(allContent)
640
755
  };
641
756
  config.installed = installed;
@@ -654,8 +769,8 @@ async function addCommand(components, opts) {
654
769
  }
655
770
  })();
656
771
  const fileName = file.path.split("/").pop();
657
- const targetPath = join6(cwd, config.aliases[aliasKey], fileName);
658
- const relativePath = join6(config.aliases[aliasKey], fileName);
772
+ const targetPath = join7(cwd, config.aliases[aliasKey], fileName);
773
+ const relativePath = join7(config.aliases[aliasKey], fileName);
659
774
  const content = rewriteKitnImports(file.content, item.type, fileName, config.aliases);
660
775
  const status = await checkFileStatus(targetPath, content);
661
776
  switch (status) {
@@ -673,15 +788,15 @@ async function addCommand(components, opts) {
673
788
  } else {
674
789
  const existing = await readExistingFile(targetPath);
675
790
  const diff = generateDiff(relativePath, existing ?? "", content);
676
- p2.log.message(pc4.dim(diff));
677
- const action = await p2.select({
791
+ p3.log.message(pc4.dim(diff));
792
+ const action = await p3.select({
678
793
  message: `${relativePath} already exists and differs. What to do?`,
679
794
  options: [
680
795
  { value: "skip", label: "Keep local version" },
681
796
  { value: "overwrite", label: "Overwrite with registry version" }
682
797
  ]
683
798
  });
684
- if (!p2.isCancel(action) && action === "overwrite") {
799
+ if (!p3.isCancel(action) && action === "overwrite") {
685
800
  await writeComponentFile(targetPath, content);
686
801
  updated.push(relativePath);
687
802
  } else {
@@ -716,7 +831,7 @@ async function addCommand(components, opts) {
716
831
  }
717
832
  })();
718
833
  const fileName = f.path.split("/").pop();
719
- return join6(config.aliases[aliasKey], fileName);
834
+ return join7(config.aliases[aliasKey], fileName);
720
835
  }),
721
836
  hash: contentHash(allContent)
722
837
  };
@@ -738,24 +853,22 @@ async function addCommand(components, opts) {
738
853
  }
739
854
  }
740
855
  if (created.length > 0) {
741
- p2.log.success(`Created ${created.length} file(s):`);
742
- for (const f of created) p2.log.message(` ${pc4.green("+")} ${f}`);
856
+ p3.log.success(`Created ${created.length} file(s):`);
857
+ for (const f of created) p3.log.message(` ${pc4.green("+")} ${f}`);
743
858
  }
744
859
  if (updated.length > 0) {
745
- p2.log.success(`Updated ${updated.length} file(s):`);
746
- for (const f of updated) p2.log.message(` ${pc4.yellow("~")} ${f}`);
860
+ p3.log.success(`Updated ${updated.length} file(s):`);
861
+ for (const f of updated) p3.log.message(` ${pc4.yellow("~")} ${f}`);
747
862
  }
748
863
  if (skipped.length > 0) {
749
- p2.log.info(`Skipped ${skipped.length} file(s):`);
750
- for (const f of skipped) p2.log.message(` ${pc4.dim("-")} ${f}`);
751
- }
752
- if (allEnvWarnings.length > 0) {
753
- p2.log.warn("Missing environment variables:");
754
- for (const w of allEnvWarnings) p2.log.message(w);
864
+ p3.log.info(`Skipped ${skipped.length} file(s):`);
865
+ for (const f of skipped) p3.log.message(` ${pc4.dim("-")} ${f}`);
755
866
  }
867
+ const allEnvVars = collectEnvVars(resolved);
868
+ await handleEnvVars(cwd, allEnvVars);
756
869
  for (const item of resolved) {
757
870
  if (item.docs) {
758
- p2.log.info(`${pc4.bold(item.name)}: ${item.docs}`);
871
+ p3.log.info(`${pc4.bold(item.name)}: ${item.docs}`);
759
872
  }
760
873
  }
761
874
  const installedNames = new Set(resolved.map((r) => r.name));
@@ -783,10 +896,10 @@ async function addCommand(components, opts) {
783
896
  }
784
897
  }
785
898
  if (hints.length > 0) {
786
- p2.log.message(pc4.bold("\nNext steps:"));
787
- for (const hint of hints) p2.log.message(hint);
899
+ p3.log.message(pc4.bold("\nNext steps:"));
900
+ for (const hint of hints) p3.log.message(hint);
788
901
  }
789
- p2.outro(pc4.green("Done!"));
902
+ p3.outro(pc4.green("Done!"));
790
903
  }
791
904
  var init_add = __esm({
792
905
  "src/commands/add.ts"() {
@@ -797,7 +910,7 @@ var init_add = __esm({
797
910
  init_resolver();
798
911
  init_file_writer();
799
912
  init_dep_installer();
800
- init_env_checker();
913
+ init_env_writer();
801
914
  init_import_rewriter();
802
915
  init_tsconfig_patcher();
803
916
  init_hash();
@@ -811,22 +924,22 @@ var list_exports = {};
811
924
  __export(list_exports, {
812
925
  listCommand: () => listCommand
813
926
  });
814
- import * as p3 from "@clack/prompts";
927
+ import * as p4 from "@clack/prompts";
815
928
  import pc5 from "picocolors";
816
929
  async function listCommand(opts) {
817
930
  const cwd = process.cwd();
818
931
  const config = await readConfig(cwd);
819
932
  if (!config) {
820
- p3.log.error("No kitn.json found. Run `kitn init` first.");
933
+ p4.log.error("No kitn.json found. Run `kitn init` first.");
821
934
  process.exit(1);
822
935
  }
823
936
  const fetcher = new RegistryFetcher(config.registries);
824
937
  const namespacesToFetch = opts.registry ? [opts.registry] : Object.keys(config.registries);
825
938
  if (opts.registry && !config.registries[opts.registry]) {
826
- p3.log.error(`Registry ${pc5.bold(opts.registry)} is not configured. Run ${pc5.bold("kitn registry list")} to see configured registries.`);
939
+ p4.log.error(`Registry ${pc5.bold(opts.registry)} is not configured. Run ${pc5.bold("kitn registry list")} to see configured registries.`);
827
940
  process.exit(1);
828
941
  }
829
- const s = p3.spinner();
942
+ const s = p4.spinner();
830
943
  s.start("Fetching registry index...");
831
944
  const allItems = [];
832
945
  const errors = [];
@@ -842,12 +955,12 @@ async function listCommand(opts) {
842
955
  }
843
956
  if (allItems.length === 0 && errors.length > 0) {
844
957
  s.stop(pc5.red("Failed to fetch registries"));
845
- for (const e of errors) p3.log.error(e);
958
+ for (const e of errors) p4.log.error(e);
846
959
  process.exit(1);
847
960
  }
848
961
  s.stop(`Found ${allItems.length} components across ${namespacesToFetch.length - errors.length} ${namespacesToFetch.length - errors.length === 1 ? "registry" : "registries"}`);
849
962
  for (const e of errors) {
850
- p3.log.warn(`${pc5.yellow("\u26A0")} Failed to fetch ${e}`);
963
+ p4.log.warn(`${pc5.yellow("\u26A0")} Failed to fetch ${e}`);
851
964
  }
852
965
  const installed = config.installed ?? {};
853
966
  const typeGroups = /* @__PURE__ */ new Map();
@@ -860,7 +973,7 @@ async function listCommand(opts) {
860
973
  let installedCount = 0;
861
974
  let updateCount = 0;
862
975
  for (const [group, items] of typeGroups) {
863
- p3.log.message(pc5.bold(`
976
+ p4.log.message(pc5.bold(`
864
977
  ${group.charAt(0).toUpperCase() + group.slice(1)}s:`));
865
978
  for (const item of items) {
866
979
  const displayName = item.namespace === "@kitn" ? item.name : `${item.namespace}/${item.name}`;
@@ -873,18 +986,18 @@ ${group.charAt(0).toUpperCase() + group.slice(1)}s:`));
873
986
  const hasUpdate = item.version && inst.version !== item.version;
874
987
  const updateTag = hasUpdate ? pc5.yellow(` \u2B06 v${item.version} available`) : "";
875
988
  if (hasUpdate) updateCount++;
876
- p3.log.message(` ${status} ${displayName.padEnd(20)} ${version} ${pc5.dim(item.description)}${updateTag}`);
989
+ p4.log.message(` ${status} ${displayName.padEnd(20)} ${version} ${pc5.dim(item.description)}${updateTag}`);
877
990
  } else {
878
991
  const status = pc5.dim("\u25CB");
879
- p3.log.message(` ${status} ${displayName.padEnd(20)} ${version} ${pc5.dim(item.description)}`);
992
+ p4.log.message(` ${status} ${displayName.padEnd(20)} ${version} ${pc5.dim(item.description)}`);
880
993
  }
881
994
  }
882
995
  }
883
996
  const availableCount = allItems.length - installedCount;
884
997
  const parts = [`${installedCount} installed`, `${availableCount} available`];
885
998
  if (updateCount > 0) parts.push(`${updateCount} update${updateCount === 1 ? "" : "s"} available`);
886
- p3.log.message("");
887
- p3.log.message(pc5.dim(` ${parts.join(", ")}`));
999
+ p4.log.message("");
1000
+ p4.log.message(pc5.dim(` ${parts.join(", ")}`));
888
1001
  }
889
1002
  var init_list = __esm({
890
1003
  "src/commands/list.ts"() {
@@ -899,13 +1012,13 @@ var diff_exports = {};
899
1012
  __export(diff_exports, {
900
1013
  diffCommand: () => diffCommand
901
1014
  });
902
- import * as p4 from "@clack/prompts";
903
- import { join as join7 } from "path";
1015
+ import * as p5 from "@clack/prompts";
1016
+ import { join as join8 } from "path";
904
1017
  async function diffCommand(componentName) {
905
1018
  const cwd = process.cwd();
906
1019
  const config = await readConfig(cwd);
907
1020
  if (!config) {
908
- p4.log.error("No kitn.json found. Run `kitn init` first.");
1021
+ p5.log.error("No kitn.json found. Run `kitn init` first.");
909
1022
  process.exit(1);
910
1023
  }
911
1024
  const input = componentName === "routes" ? config.framework ?? "hono" : componentName;
@@ -913,7 +1026,7 @@ async function diffCommand(componentName) {
913
1026
  const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
914
1027
  const installed = config.installed?.[installedKey];
915
1028
  if (!installed) {
916
- p4.log.error(`Component '${ref.name}' is not installed.`);
1029
+ p5.log.error(`Component '${ref.name}' is not installed.`);
917
1030
  process.exit(1);
918
1031
  }
919
1032
  const namespace = installed.registry ?? ref.namespace;
@@ -921,7 +1034,7 @@ async function diffCommand(componentName) {
921
1034
  const index = await fetcher.fetchIndex(namespace);
922
1035
  const indexItem = index.items.find((i) => i.name === ref.name);
923
1036
  if (!indexItem) {
924
- p4.log.error(`Component '${ref.name}' not found in ${namespace} registry.`);
1037
+ p5.log.error(`Component '${ref.name}' not found in ${namespace} registry.`);
925
1038
  process.exit(1);
926
1039
  }
927
1040
  const dir = typeToDir[indexItem.type];
@@ -930,11 +1043,11 @@ async function diffCommand(componentName) {
930
1043
  for (const file of registryItem.files) {
931
1044
  if (indexItem.type === "kitn:package") {
932
1045
  const baseDir = config.aliases.base ?? "src/ai";
933
- const localPath = join7(cwd, baseDir, file.path);
934
- const relativePath = join7(baseDir, file.path);
1046
+ const localPath = join8(cwd, baseDir, file.path);
1047
+ const relativePath = join8(baseDir, file.path);
935
1048
  const localContent = await readExistingFile(localPath);
936
1049
  if (localContent === null) {
937
- p4.log.warn(`${relativePath}: file missing locally`);
1050
+ p5.log.warn(`${relativePath}: file missing locally`);
938
1051
  hasDiff = true;
939
1052
  } else if (localContent !== file.content) {
940
1053
  const diff = generateDiff(relativePath, localContent, file.content);
@@ -955,10 +1068,10 @@ async function diffCommand(componentName) {
955
1068
  return "storage";
956
1069
  }
957
1070
  })();
958
- const localPath = join7(cwd, config.aliases[aliasKey], fileName);
1071
+ const localPath = join8(cwd, config.aliases[aliasKey], fileName);
959
1072
  const localContent = await readExistingFile(localPath);
960
1073
  if (localContent === null) {
961
- p4.log.warn(`${fileName}: file missing locally`);
1074
+ p5.log.warn(`${fileName}: file missing locally`);
962
1075
  hasDiff = true;
963
1076
  } else if (localContent !== file.content) {
964
1077
  const diff = generateDiff(fileName, localContent, file.content);
@@ -968,7 +1081,7 @@ async function diffCommand(componentName) {
968
1081
  }
969
1082
  }
970
1083
  if (!hasDiff) {
971
- p4.log.success(`${ref.name}: up to date, no differences.`);
1084
+ p5.log.success(`${ref.name}: up to date, no differences.`);
972
1085
  }
973
1086
  }
974
1087
  var init_diff = __esm({
@@ -987,15 +1100,15 @@ var remove_exports = {};
987
1100
  __export(remove_exports, {
988
1101
  removeCommand: () => removeCommand
989
1102
  });
990
- import * as p5 from "@clack/prompts";
1103
+ import * as p6 from "@clack/prompts";
991
1104
  import pc6 from "picocolors";
992
- import { join as join8 } from "path";
1105
+ import { join as join9 } from "path";
993
1106
  import { unlink } from "fs/promises";
994
1107
  async function removeCommand(componentName) {
995
1108
  const cwd = process.cwd();
996
1109
  const config = await readConfig(cwd);
997
1110
  if (!config) {
998
- p5.log.error("No kitn.json found. Run `kitn init` first.");
1111
+ p6.log.error("No kitn.json found. Run `kitn init` first.");
999
1112
  process.exit(1);
1000
1113
  }
1001
1114
  const input = componentName === "routes" ? config.framework ?? "hono" : componentName;
@@ -1003,24 +1116,24 @@ async function removeCommand(componentName) {
1003
1116
  const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1004
1117
  const installed = config.installed?.[installedKey];
1005
1118
  if (!installed) {
1006
- p5.log.error(`Component '${ref.name}' is not installed.`);
1119
+ p6.log.error(`Component '${ref.name}' is not installed.`);
1007
1120
  process.exit(1);
1008
1121
  }
1009
- const shouldRemove = await p5.confirm({
1122
+ const shouldRemove = await p6.confirm({
1010
1123
  message: `Remove ${ref.name}? This will delete ${installed.files.length} file(s).`,
1011
1124
  initialValue: false
1012
1125
  });
1013
- if (p5.isCancel(shouldRemove) || !shouldRemove) {
1014
- p5.cancel("Remove cancelled.");
1126
+ if (p6.isCancel(shouldRemove) || !shouldRemove) {
1127
+ p6.cancel("Remove cancelled.");
1015
1128
  process.exit(0);
1016
1129
  }
1017
1130
  const deleted = [];
1018
1131
  for (const filePath of installed.files) {
1019
1132
  try {
1020
- await unlink(join8(cwd, filePath));
1133
+ await unlink(join9(cwd, filePath));
1021
1134
  deleted.push(filePath);
1022
1135
  } catch {
1023
- p5.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
1136
+ p6.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
1024
1137
  }
1025
1138
  }
1026
1139
  delete config.installed[installedKey];
@@ -1029,8 +1142,8 @@ async function removeCommand(componentName) {
1029
1142
  }
1030
1143
  await writeConfig(cwd, config);
1031
1144
  if (deleted.length > 0) {
1032
- p5.log.success(`Removed ${ref.name}:`);
1033
- for (const f of deleted) p5.log.message(` ${pc6.red("-")} ${f}`);
1145
+ p6.log.success(`Removed ${ref.name}:`);
1146
+ for (const f of deleted) p6.log.message(` ${pc6.red("-")} ${f}`);
1034
1147
  }
1035
1148
  }
1036
1149
  var init_remove = __esm({
@@ -1046,18 +1159,18 @@ var update_exports = {};
1046
1159
  __export(update_exports, {
1047
1160
  updateCommand: () => updateCommand
1048
1161
  });
1049
- import * as p6 from "@clack/prompts";
1162
+ import * as p7 from "@clack/prompts";
1050
1163
  async function updateCommand(components) {
1051
1164
  if (components.length === 0) {
1052
1165
  const cwd = process.cwd();
1053
1166
  const config = await readConfig(cwd);
1054
1167
  if (!config) {
1055
- p6.log.error("No kitn.json found. Run `kitn init` first.");
1168
+ p7.log.error("No kitn.json found. Run `kitn init` first.");
1056
1169
  process.exit(1);
1057
1170
  }
1058
1171
  const installed = config.installed;
1059
1172
  if (!installed || Object.keys(installed).length === 0) {
1060
- p6.log.info("No installed components to update.");
1173
+ p7.log.info("No installed components to update.");
1061
1174
  return;
1062
1175
  }
1063
1176
  components = Object.keys(installed);
@@ -1072,36 +1185,593 @@ var init_update = __esm({
1072
1185
  }
1073
1186
  });
1074
1187
 
1188
+ // src/registry/build-output.ts
1189
+ import { readdir, writeFile as writeFile6, mkdir as mkdir3, access as access4 } from "fs/promises";
1190
+ import { join as join10, resolve } from "path";
1191
+ async function fileExists(path) {
1192
+ try {
1193
+ await access4(path);
1194
+ return true;
1195
+ } catch {
1196
+ return false;
1197
+ }
1198
+ }
1199
+ async function walkForRegistryJson(dir) {
1200
+ const results = [];
1201
+ let entries;
1202
+ try {
1203
+ entries = await readdir(dir, { withFileTypes: true });
1204
+ } catch {
1205
+ return results;
1206
+ }
1207
+ if (await fileExists(join10(dir, "registry.json"))) {
1208
+ results.push(dir);
1209
+ return results;
1210
+ }
1211
+ for (const entry of entries) {
1212
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
1213
+ const subResults = await walkForRegistryJson(join10(dir, entry.name));
1214
+ results.push(...subResults);
1215
+ }
1216
+ }
1217
+ return results;
1218
+ }
1219
+ async function scanForComponents(cwd, paths) {
1220
+ const resolvedCwd = resolve(cwd);
1221
+ if (paths && paths.length > 0) {
1222
+ const results = [];
1223
+ for (const p12 of paths) {
1224
+ const absPath = resolve(resolvedCwd, p12);
1225
+ if (await fileExists(join10(absPath, "registry.json"))) {
1226
+ results.push(absPath);
1227
+ continue;
1228
+ }
1229
+ let entries;
1230
+ try {
1231
+ entries = await readdir(absPath, { withFileTypes: true });
1232
+ } catch {
1233
+ continue;
1234
+ }
1235
+ for (const entry of entries) {
1236
+ if (entry.isDirectory()) {
1237
+ const subDir = join10(absPath, entry.name);
1238
+ if (await fileExists(join10(subDir, "registry.json"))) {
1239
+ results.push(subDir);
1240
+ }
1241
+ }
1242
+ }
1243
+ }
1244
+ return results;
1245
+ }
1246
+ return walkForRegistryJson(resolvedCwd);
1247
+ }
1248
+ function parseVersionFromFilename(name, componentName) {
1249
+ const prefix = `${componentName}@`;
1250
+ const suffix = ".json";
1251
+ if (name.startsWith(prefix) && name.endsWith(suffix)) {
1252
+ return name.slice(prefix.length, -suffix.length);
1253
+ }
1254
+ return null;
1255
+ }
1256
+ async function writeRegistryOutput(outputDir, items) {
1257
+ const written = [];
1258
+ const skipped = [];
1259
+ const resolvedOutput = resolve(outputDir);
1260
+ const indexItems = [];
1261
+ for (const item of items) {
1262
+ const dir = typeToDir[item.type];
1263
+ const typeDir = join10(resolvedOutput, dir);
1264
+ await mkdir3(typeDir, { recursive: true });
1265
+ const itemJson = JSON.stringify(item, null, 2);
1266
+ const latestPath = join10(typeDir, `${item.name}.json`);
1267
+ const latestRelative = `${dir}/${item.name}.json`;
1268
+ await writeFile6(latestPath, itemJson, "utf-8");
1269
+ written.push(latestRelative);
1270
+ if (item.version) {
1271
+ const versionedFilename = `${item.name}@${item.version}.json`;
1272
+ const versionedPath = join10(typeDir, versionedFilename);
1273
+ const versionedRelative = `${dir}/${versionedFilename}`;
1274
+ if (await fileExists(versionedPath)) {
1275
+ skipped.push(versionedRelative);
1276
+ } else {
1277
+ await writeFile6(versionedPath, itemJson, "utf-8");
1278
+ written.push(versionedRelative);
1279
+ }
1280
+ }
1281
+ const versions = [];
1282
+ let entries;
1283
+ try {
1284
+ entries = await readdir(typeDir);
1285
+ } catch {
1286
+ entries = [];
1287
+ }
1288
+ for (const filename of entries) {
1289
+ const ver = parseVersionFromFilename(filename, item.name);
1290
+ if (ver) {
1291
+ versions.push(ver);
1292
+ }
1293
+ }
1294
+ versions.sort();
1295
+ indexItems.push({
1296
+ name: item.name,
1297
+ type: item.type,
1298
+ description: item.description,
1299
+ ...item.registryDependencies && item.registryDependencies.length > 0 && {
1300
+ registryDependencies: item.registryDependencies
1301
+ },
1302
+ ...item.categories && item.categories.length > 0 && { categories: item.categories },
1303
+ ...item.version && { version: item.version },
1304
+ ...versions.length > 0 && { versions },
1305
+ ...item.updatedAt && { updatedAt: item.updatedAt }
1306
+ });
1307
+ }
1308
+ const index = {
1309
+ version: "1",
1310
+ items: indexItems
1311
+ };
1312
+ const indexPath = join10(resolvedOutput, "registry.json");
1313
+ await writeFile6(indexPath, JSON.stringify(index, null, 2), "utf-8");
1314
+ written.push("registry.json");
1315
+ return { written, skipped };
1316
+ }
1317
+ var SKIP_DIRS;
1318
+ var init_build_output = __esm({
1319
+ "src/registry/build-output.ts"() {
1320
+ "use strict";
1321
+ init_schema();
1322
+ SKIP_DIRS = /* @__PURE__ */ new Set([
1323
+ "node_modules",
1324
+ "dist",
1325
+ ".git",
1326
+ "r",
1327
+ "test",
1328
+ "tests",
1329
+ ".claude"
1330
+ ]);
1331
+ }
1332
+ });
1333
+
1334
+ // src/registry/builder.ts
1335
+ import { readFile as readFile7, readdir as readdir2 } from "fs/promises";
1336
+ import { join as join11, relative as relative3 } from "path";
1337
+ function isExcludedDevDep(name) {
1338
+ return EXCLUDED_DEV_DEPS.has(name) || name.startsWith("@types/");
1339
+ }
1340
+ function stripScope(name) {
1341
+ const match = name.match(/^@[^/]+\/(.+)$/);
1342
+ return match ? match[1] : name;
1343
+ }
1344
+ async function readTsFiles(dir, baseDir, exclude) {
1345
+ const results = [];
1346
+ const entries = await readdir2(dir, { withFileTypes: true });
1347
+ for (const entry of entries) {
1348
+ const fullPath = join11(dir, entry.name);
1349
+ const relPath = relative3(baseDir, fullPath);
1350
+ if (entry.isDirectory()) {
1351
+ const nested = await readTsFiles(fullPath, baseDir, exclude);
1352
+ results.push(...nested);
1353
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
1354
+ if (exclude.includes(relPath)) {
1355
+ continue;
1356
+ }
1357
+ const content = await readFile7(fullPath, "utf-8");
1358
+ results.push({ relativePath: relPath, content });
1359
+ }
1360
+ }
1361
+ return results;
1362
+ }
1363
+ async function buildComponent(componentDir) {
1364
+ let rawConfig;
1365
+ try {
1366
+ rawConfig = await readFile7(join11(componentDir, "registry.json"), "utf-8");
1367
+ } catch {
1368
+ throw new Error(
1369
+ `No registry.json found in ${componentDir}. Every component must have a registry.json file.`
1370
+ );
1371
+ }
1372
+ let config;
1373
+ try {
1374
+ config = componentConfigSchema.parse(JSON.parse(rawConfig));
1375
+ } catch (err) {
1376
+ throw new Error(
1377
+ `Invalid registry.json in ${componentDir}: ${err instanceof Error ? err.message : String(err)}`
1378
+ );
1379
+ }
1380
+ let pkg = null;
1381
+ try {
1382
+ const rawPkg = await readFile7(join11(componentDir, "package.json"), "utf-8");
1383
+ pkg = JSON.parse(rawPkg);
1384
+ } catch {
1385
+ }
1386
+ const name = config.name ?? (pkg?.name ? stripScope(pkg.name) : void 0);
1387
+ const version = config.version ?? pkg?.version;
1388
+ const description = config.description ?? pkg?.description;
1389
+ if (!name) {
1390
+ throw new Error(
1391
+ `Component in ${componentDir} is missing a name. Provide "name" in registry.json or have a package.json with a "name" field.`
1392
+ );
1393
+ }
1394
+ if (!description) {
1395
+ throw new Error(
1396
+ `Component in ${componentDir} is missing a description. Provide "description" in registry.json or have a package.json with a "description" field.`
1397
+ );
1398
+ }
1399
+ let dependencies = config.dependencies;
1400
+ let devDependencies = config.devDependencies;
1401
+ if (pkg && !config.dependencies) {
1402
+ const deps = [];
1403
+ if (pkg.dependencies) {
1404
+ for (const [depName, depVersion] of Object.entries(pkg.dependencies)) {
1405
+ if (depVersion !== "workspace:*") {
1406
+ deps.push(depName);
1407
+ }
1408
+ }
1409
+ }
1410
+ if (pkg.peerDependencies) {
1411
+ for (const [depName, depVersion] of Object.entries(pkg.peerDependencies)) {
1412
+ if (depVersion !== "workspace:*") {
1413
+ deps.push(depName);
1414
+ }
1415
+ }
1416
+ }
1417
+ if (deps.length > 0) {
1418
+ dependencies = deps;
1419
+ }
1420
+ }
1421
+ if (pkg && !config.devDependencies) {
1422
+ const devDeps = [];
1423
+ if (pkg.devDependencies) {
1424
+ for (const depName of Object.keys(pkg.devDependencies)) {
1425
+ if (!isExcludedDevDep(depName)) {
1426
+ devDeps.push(depName);
1427
+ }
1428
+ }
1429
+ }
1430
+ if (devDeps.length > 0) {
1431
+ devDependencies = devDeps;
1432
+ }
1433
+ }
1434
+ const isPackage = config.type === "kitn:package";
1435
+ const dirPrefix = config.installDir ?? typeToDir[config.type];
1436
+ let files;
1437
+ if (isPackage) {
1438
+ const sourceDir = config.sourceDir ?? "src";
1439
+ const sourcePath = join11(componentDir, sourceDir);
1440
+ const exclude = config.exclude ?? [];
1441
+ let tsFiles;
1442
+ try {
1443
+ tsFiles = await readTsFiles(sourcePath, sourcePath, exclude);
1444
+ } catch {
1445
+ throw new Error(
1446
+ `Cannot read source directory "${sourceDir}" in ${componentDir}. Make sure it exists.`
1447
+ );
1448
+ }
1449
+ files = tsFiles.map((f) => ({
1450
+ path: `${dirPrefix}/${f.relativePath}`,
1451
+ content: f.content,
1452
+ type: config.type
1453
+ }));
1454
+ } else {
1455
+ if (!config.files || config.files.length === 0) {
1456
+ throw new Error(
1457
+ `Component "${name}" (type: ${config.type}) has no "files" array in registry.json. Standalone components must list their source files.`
1458
+ );
1459
+ }
1460
+ files = await Promise.all(
1461
+ config.files.map(async (filePath) => {
1462
+ const fullPath = join11(componentDir, filePath);
1463
+ let content;
1464
+ try {
1465
+ content = await readFile7(fullPath, "utf-8");
1466
+ } catch {
1467
+ throw new Error(
1468
+ `Cannot read file "${filePath}" referenced in registry.json for component "${name}". Make sure the file exists at ${fullPath}.`
1469
+ );
1470
+ }
1471
+ return {
1472
+ path: `${dirPrefix}/${filePath}`,
1473
+ content,
1474
+ type: config.type
1475
+ };
1476
+ })
1477
+ );
1478
+ }
1479
+ const item = {
1480
+ name,
1481
+ type: config.type,
1482
+ description,
1483
+ files
1484
+ };
1485
+ if (version) item.version = version;
1486
+ if (dependencies && dependencies.length > 0) item.dependencies = dependencies;
1487
+ if (devDependencies && devDependencies.length > 0) item.devDependencies = devDependencies;
1488
+ if (config.registryDependencies && config.registryDependencies.length > 0) {
1489
+ item.registryDependencies = config.registryDependencies;
1490
+ }
1491
+ if (config.envVars) item.envVars = config.envVars;
1492
+ if (config.tsconfig) item.tsconfig = config.tsconfig;
1493
+ if (config.docs) item.docs = config.docs;
1494
+ if (config.categories && config.categories.length > 0) item.categories = config.categories;
1495
+ if (config.changelog && config.changelog.length > 0) item.changelog = config.changelog;
1496
+ if (isPackage && config.installDir) item.installDir = config.installDir;
1497
+ try {
1498
+ return registryItemSchema.parse(item);
1499
+ } catch (err) {
1500
+ throw new Error(
1501
+ `Built component "${name}" failed validation: ${err instanceof Error ? err.message : String(err)}`
1502
+ );
1503
+ }
1504
+ }
1505
+ var EXCLUDED_DEV_DEPS;
1506
+ var init_builder = __esm({
1507
+ "src/registry/builder.ts"() {
1508
+ "use strict";
1509
+ init_schema();
1510
+ EXCLUDED_DEV_DEPS = /* @__PURE__ */ new Set([
1511
+ "typescript",
1512
+ "@types/bun",
1513
+ "@types/node",
1514
+ "tsup",
1515
+ "vitest",
1516
+ "jest",
1517
+ "@types/jest"
1518
+ ]);
1519
+ }
1520
+ });
1521
+
1522
+ // src/commands/build.ts
1523
+ var build_exports = {};
1524
+ __export(build_exports, {
1525
+ buildCommand: () => buildCommand
1526
+ });
1527
+ import * as p8 from "@clack/prompts";
1528
+ import pc7 from "picocolors";
1529
+ import { resolve as resolve2, relative as relative4 } from "path";
1530
+ async function buildCommand(paths, opts) {
1531
+ p8.intro(pc7.bgCyan(pc7.black(" kitn build ")));
1532
+ const cwd = process.cwd();
1533
+ const outputDir = resolve2(cwd, opts.output ?? "dist/r");
1534
+ const s = p8.spinner();
1535
+ s.start("Scanning for components...");
1536
+ const componentDirs = await scanForComponents(cwd, paths.length > 0 ? paths : void 0);
1537
+ if (componentDirs.length === 0) {
1538
+ s.stop("No components found");
1539
+ p8.log.info(
1540
+ `No directories with ${pc7.bold("registry.json")} found. Run ${pc7.bold("kitn create")} to scaffold a component.`
1541
+ );
1542
+ return;
1543
+ }
1544
+ s.stop(`Found ${componentDirs.length} component(s)`);
1545
+ for (const dir of componentDirs) {
1546
+ p8.log.message(` ${pc7.dim(relative4(cwd, dir))}`);
1547
+ }
1548
+ s.start("Building components...");
1549
+ const items = [];
1550
+ const errors = [];
1551
+ for (const dir of componentDirs) {
1552
+ try {
1553
+ const item = await buildComponent(dir);
1554
+ items.push(item);
1555
+ } catch (err) {
1556
+ errors.push({ dir: relative4(cwd, dir), error: err.message });
1557
+ }
1558
+ }
1559
+ if (errors.length > 0) {
1560
+ s.stop(pc7.red(`Build failed with ${errors.length} error(s)`));
1561
+ for (const { dir, error } of errors) {
1562
+ p8.log.error(`${pc7.bold(dir)}: ${error}`);
1563
+ }
1564
+ process.exit(1);
1565
+ }
1566
+ const { written, skipped } = await writeRegistryOutput(outputDir, items);
1567
+ s.stop(pc7.green(`Built ${items.length} component(s)`));
1568
+ if (written.length > 0) {
1569
+ p8.log.success(`Wrote ${written.length} file(s):`);
1570
+ for (const f of written) {
1571
+ p8.log.message(` ${pc7.green("+")} ${f}`);
1572
+ }
1573
+ }
1574
+ if (skipped.length > 0) {
1575
+ p8.log.info(`Skipped ${skipped.length} file(s) (already exist):`);
1576
+ for (const f of skipped) {
1577
+ p8.log.message(` ${pc7.dim("-")} ${f}`);
1578
+ }
1579
+ }
1580
+ p8.outro(`Output: ${pc7.cyan(relative4(cwd, outputDir) || ".")}`);
1581
+ }
1582
+ var init_build = __esm({
1583
+ "src/commands/build.ts"() {
1584
+ "use strict";
1585
+ init_build_output();
1586
+ init_builder();
1587
+ }
1588
+ });
1589
+
1590
+ // src/commands/create.ts
1591
+ var create_exports = {};
1592
+ __export(create_exports, {
1593
+ createCommand: () => createCommand,
1594
+ createComponent: () => createComponent
1595
+ });
1596
+ import * as p9 from "@clack/prompts";
1597
+ import pc8 from "picocolors";
1598
+ import { join as join12 } from "path";
1599
+ import { mkdir as mkdir4, writeFile as writeFile7 } from "fs/promises";
1600
+ function toCamelCase(str) {
1601
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1602
+ }
1603
+ function toTitleCase(str) {
1604
+ return str.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1605
+ }
1606
+ function generateRegistryJson(type, name, sourceFile) {
1607
+ const base = {
1608
+ $schema: "https://kitn.dev/schema/registry.json",
1609
+ name,
1610
+ type: `kitn:${type}`,
1611
+ version: "0.1.0",
1612
+ description: "",
1613
+ files: [sourceFile],
1614
+ categories: []
1615
+ };
1616
+ if (type === "tool") {
1617
+ base.dependencies = ["ai", "zod"];
1618
+ } else if (type === "agent" || type === "storage") {
1619
+ base.dependencies = [];
1620
+ }
1621
+ return base;
1622
+ }
1623
+ function generateAgentSource(name) {
1624
+ const camel = toCamelCase(name);
1625
+ return `import type { AgentConfig } from "@kitnai/core";
1626
+
1627
+ export const ${camel}Config: AgentConfig = {
1628
+ name: "${name}",
1629
+ description: "",
1630
+ system: "You are a helpful assistant.",
1631
+ tools: [],
1632
+ };
1633
+ `;
1634
+ }
1635
+ function generateToolSource(name) {
1636
+ const camel = toCamelCase(name);
1637
+ return `import { tool } from "ai";
1638
+ import { z } from "zod";
1639
+
1640
+ export const ${camel} = tool({
1641
+ description: "",
1642
+ inputSchema: z.object({
1643
+ input: z.string().describe("Input parameter"),
1644
+ }),
1645
+ execute: async ({ input }) => {
1646
+ // TODO: implement
1647
+ return { result: input };
1648
+ },
1649
+ });
1650
+ `;
1651
+ }
1652
+ function generateSkillSource(name) {
1653
+ const title = toTitleCase(name);
1654
+ return `---
1655
+ name: ${name}
1656
+ description: ""
1657
+ ---
1658
+
1659
+ # ${title}
1660
+
1661
+ Describe what this skill does and how to use it.
1662
+ `;
1663
+ }
1664
+ function generateStorageSource(name) {
1665
+ const camel = toCamelCase("create-" + name);
1666
+ return `import type { StorageProvider } from "@kitnai/core";
1667
+
1668
+ export function ${camel}(config?: Record<string, unknown>): StorageProvider {
1669
+ // TODO: implement storage provider
1670
+ throw new Error("Not implemented");
1671
+ }
1672
+ `;
1673
+ }
1674
+ async function dirExists(path) {
1675
+ try {
1676
+ const { stat: stat2 } = await import("fs/promises");
1677
+ const s = await stat2(path);
1678
+ return s.isDirectory();
1679
+ } catch {
1680
+ return false;
1681
+ }
1682
+ }
1683
+ async function createComponent(type, name, opts) {
1684
+ if (!VALID_TYPES.includes(type)) {
1685
+ throw new Error(
1686
+ `Invalid component type: "${type}". Valid types: ${VALID_TYPES.join(", ")}`
1687
+ );
1688
+ }
1689
+ const cwd = opts?.cwd ?? process.cwd();
1690
+ const dir = join12(cwd, name);
1691
+ if (await dirExists(dir)) {
1692
+ throw new Error(`Directory "${name}" already exists`);
1693
+ }
1694
+ await mkdir4(dir, { recursive: true });
1695
+ const validType = type;
1696
+ const sourceFile = validType === "skill" ? "README.md" : `${name}.ts`;
1697
+ const registryJson = generateRegistryJson(validType, name, sourceFile);
1698
+ await writeFile7(
1699
+ join12(dir, "registry.json"),
1700
+ JSON.stringify(registryJson, null, 2) + "\n"
1701
+ );
1702
+ let source;
1703
+ switch (validType) {
1704
+ case "agent":
1705
+ source = generateAgentSource(name);
1706
+ break;
1707
+ case "tool":
1708
+ source = generateToolSource(name);
1709
+ break;
1710
+ case "skill":
1711
+ source = generateSkillSource(name);
1712
+ break;
1713
+ case "storage":
1714
+ source = generateStorageSource(name);
1715
+ break;
1716
+ }
1717
+ await writeFile7(join12(dir, sourceFile), source);
1718
+ return { dir, files: ["registry.json", sourceFile] };
1719
+ }
1720
+ async function createCommand(type, name) {
1721
+ p9.intro(pc8.bgCyan(pc8.black(" kitn create ")));
1722
+ try {
1723
+ const { dir, files } = await createComponent(type, name);
1724
+ p9.log.success(`Created ${pc8.bold(type)} component ${pc8.cyan(name)}`);
1725
+ for (const file of files) {
1726
+ p9.log.message(` ${pc8.green("+")} ${file}`);
1727
+ }
1728
+ const editFile = files.find((f) => f !== "registry.json") ?? files[0];
1729
+ p9.outro(
1730
+ `Edit ${pc8.cyan(`${name}/${editFile}`)}, then run ${pc8.bold("kitn build")}`
1731
+ );
1732
+ } catch (err) {
1733
+ p9.log.error(err.message);
1734
+ process.exit(1);
1735
+ }
1736
+ }
1737
+ var VALID_TYPES;
1738
+ var init_create = __esm({
1739
+ "src/commands/create.ts"() {
1740
+ "use strict";
1741
+ VALID_TYPES = ["agent", "tool", "skill", "storage"];
1742
+ }
1743
+ });
1744
+
1075
1745
  // src/commands/info.ts
1076
1746
  var info_exports = {};
1077
1747
  __export(info_exports, {
1078
1748
  infoCommand: () => infoCommand
1079
1749
  });
1080
- import * as p7 from "@clack/prompts";
1081
- import pc7 from "picocolors";
1750
+ import * as p10 from "@clack/prompts";
1751
+ import pc9 from "picocolors";
1082
1752
  async function infoCommand(component) {
1083
1753
  const cwd = process.cwd();
1084
1754
  const config = await readConfig(cwd);
1085
1755
  if (!config) {
1086
- p7.log.error("No kitn.json found. Run `kitn init` first.");
1756
+ p10.log.error("No kitn.json found. Run `kitn init` first.");
1087
1757
  process.exit(1);
1088
1758
  }
1089
1759
  const ref = parseComponentRef(component);
1090
1760
  const fetcher = new RegistryFetcher(config.registries);
1091
- const s = p7.spinner();
1761
+ const s = p10.spinner();
1092
1762
  s.start("Fetching component info...");
1093
1763
  let index;
1094
1764
  try {
1095
1765
  index = await fetcher.fetchIndex(ref.namespace);
1096
1766
  } catch (err) {
1097
- s.stop(pc7.red("Failed to fetch registry"));
1098
- p7.log.error(err.message);
1767
+ s.stop(pc9.red("Failed to fetch registry"));
1768
+ p10.log.error(err.message);
1099
1769
  process.exit(1);
1100
1770
  }
1101
1771
  const indexItem = index.items.find((i) => i.name === ref.name);
1102
1772
  if (!indexItem) {
1103
- s.stop(pc7.red("Component not found"));
1104
- p7.log.error(`Component '${ref.name}' not found in registry.`);
1773
+ s.stop(pc9.red("Component not found"));
1774
+ p10.log.error(`Component '${ref.name}' not found in registry.`);
1105
1775
  process.exit(1);
1106
1776
  }
1107
1777
  const dir = typeToDir[indexItem.type];
@@ -1109,8 +1779,8 @@ async function infoCommand(component) {
1109
1779
  try {
1110
1780
  item = await fetcher.fetchItem(ref.name, dir, ref.namespace, ref.version);
1111
1781
  } catch (err) {
1112
- s.stop(pc7.red("Failed to fetch component"));
1113
- p7.log.error(err.message);
1782
+ s.stop(pc9.red("Failed to fetch component"));
1783
+ p10.log.error(err.message);
1114
1784
  process.exit(1);
1115
1785
  }
1116
1786
  s.stop("Component found");
@@ -1118,62 +1788,62 @@ async function infoCommand(component) {
1118
1788
  const typeName = indexItem.type.replace("kitn:", "");
1119
1789
  console.log();
1120
1790
  console.log(
1121
- ` ${pc7.bold(item.name)} ${pc7.cyan(`v${version}`)}${" ".repeat(Math.max(1, 40 - item.name.length - version.length - 2))}${pc7.dim(ref.namespace)}`
1791
+ ` ${pc9.bold(item.name)} ${pc9.cyan(`v${version}`)}${" ".repeat(Math.max(1, 40 - item.name.length - version.length - 2))}${pc9.dim(ref.namespace)}`
1122
1792
  );
1123
- console.log(` ${pc7.dim(item.description)}`);
1793
+ console.log(` ${pc9.dim(item.description)}`);
1124
1794
  console.log();
1125
- console.log(` ${pc7.dim("Type:")} ${typeName}`);
1795
+ console.log(` ${pc9.dim("Type:")} ${typeName}`);
1126
1796
  if (item.dependencies?.length) {
1127
1797
  console.log(
1128
- ` ${pc7.dim("Dependencies:")} ${item.dependencies.join(", ")}`
1798
+ ` ${pc9.dim("Dependencies:")} ${item.dependencies.join(", ")}`
1129
1799
  );
1130
1800
  }
1131
1801
  if (item.registryDependencies?.length) {
1132
1802
  console.log(
1133
- ` ${pc7.dim("Registry deps:")} ${item.registryDependencies.join(", ")}`
1803
+ ` ${pc9.dim("Registry deps:")} ${item.registryDependencies.join(", ")}`
1134
1804
  );
1135
1805
  }
1136
1806
  if (item.categories?.length) {
1137
1807
  console.log(
1138
- ` ${pc7.dim("Categories:")} ${item.categories.join(", ")}`
1808
+ ` ${pc9.dim("Categories:")} ${item.categories.join(", ")}`
1139
1809
  );
1140
1810
  }
1141
1811
  if (item.updatedAt) {
1142
- console.log(` ${pc7.dim("Updated:")} ${item.updatedAt}`);
1812
+ console.log(` ${pc9.dim("Updated:")} ${item.updatedAt}`);
1143
1813
  }
1144
1814
  const versions = indexItem.versions;
1145
1815
  if (versions?.length) {
1146
- console.log(` ${pc7.dim("Versions:")} ${versions.join(", ")}`);
1816
+ console.log(` ${pc9.dim("Versions:")} ${versions.join(", ")}`);
1147
1817
  }
1148
1818
  if (item.changelog?.length) {
1149
1819
  console.log();
1150
- console.log(` ${pc7.bold("Changelog:")}`);
1820
+ console.log(` ${pc9.bold("Changelog:")}`);
1151
1821
  for (const entry of item.changelog) {
1152
- const tag = entry.type === "feature" ? pc7.green(entry.type) : entry.type === "fix" ? pc7.yellow(entry.type) : entry.type === "breaking" ? pc7.red(entry.type) : pc7.dim(entry.type);
1822
+ const tag = entry.type === "feature" ? pc9.green(entry.type) : entry.type === "fix" ? pc9.yellow(entry.type) : entry.type === "breaking" ? pc9.red(entry.type) : pc9.dim(entry.type);
1153
1823
  console.log(
1154
- ` ${pc7.cyan(entry.version)} ${pc7.dim(entry.date)} ${tag} ${entry.note}`
1824
+ ` ${pc9.cyan(entry.version)} ${pc9.dim(entry.date)} ${tag} ${entry.note}`
1155
1825
  );
1156
1826
  }
1157
1827
  }
1158
1828
  console.log();
1159
1829
  const fileCount = item.files.length;
1160
- console.log(` ${pc7.bold(`Files:`)} ${pc7.dim(`(${fileCount})`)}`);
1830
+ console.log(` ${pc9.bold(`Files:`)} ${pc9.dim(`(${fileCount})`)}`);
1161
1831
  const maxShown = 10;
1162
1832
  for (const file of item.files.slice(0, maxShown)) {
1163
- console.log(` ${pc7.dim(file.path)}`);
1833
+ console.log(` ${pc9.dim(file.path)}`);
1164
1834
  }
1165
1835
  if (fileCount > maxShown) {
1166
- console.log(` ${pc7.dim(`... and ${fileCount - maxShown} more`)}`);
1836
+ console.log(` ${pc9.dim(`... and ${fileCount - maxShown} more`)}`);
1167
1837
  }
1168
1838
  const installed = config.installed?.[item.name];
1169
1839
  if (installed) {
1170
1840
  console.log();
1171
1841
  console.log(
1172
- ` ${pc7.green("Installed")} ${pc7.dim(`v${installed.version}`)}`
1842
+ ` ${pc9.green("Installed")} ${pc9.dim(`v${installed.version}`)}`
1173
1843
  );
1174
1844
  if (version !== installed.version) {
1175
1845
  console.log(
1176
- ` ${pc7.yellow("Update available:")} ${pc7.dim(`v${installed.version}`)} \u2192 ${pc7.cyan(`v${version}`)}`
1846
+ ` ${pc9.yellow("Update available:")} ${pc9.dim(`v${installed.version}`)} \u2192 ${pc9.cyan(`v${version}`)}`
1177
1847
  );
1178
1848
  }
1179
1849
  }
@@ -1196,8 +1866,8 @@ __export(registry_exports, {
1196
1866
  registryListCommand: () => registryListCommand,
1197
1867
  registryRemoveCommand: () => registryRemoveCommand
1198
1868
  });
1199
- import * as p8 from "@clack/prompts";
1200
- import pc8 from "picocolors";
1869
+ import * as p11 from "@clack/prompts";
1870
+ import pc10 from "picocolors";
1201
1871
  async function registryAddCommand(namespace, url, opts = {}) {
1202
1872
  const cwd = opts.cwd ?? process.cwd();
1203
1873
  const config = await readConfig(cwd);
@@ -1216,8 +1886,8 @@ async function registryAddCommand(namespace, url, opts = {}) {
1216
1886
  }
1217
1887
  config.registries[namespace] = url;
1218
1888
  await writeConfig(cwd, config);
1219
- p8.log.success(`Added registry ${pc8.bold(namespace)}`);
1220
- p8.log.message(pc8.dim(` ${url}`));
1889
+ p11.log.success(`Added registry ${pc10.bold(namespace)}`);
1890
+ p11.log.message(pc10.dim(` ${url}`));
1221
1891
  }
1222
1892
  async function registryRemoveCommand(namespace, opts = {}) {
1223
1893
  const cwd = opts.cwd ?? process.cwd();
@@ -1239,11 +1909,11 @@ async function registryRemoveCommand(namespace, opts = {}) {
1239
1909
  }
1240
1910
  delete config.registries[namespace];
1241
1911
  await writeConfig(cwd, config);
1242
- p8.log.success(`Removed registry ${pc8.bold(namespace)}`);
1912
+ p11.log.success(`Removed registry ${pc10.bold(namespace)}`);
1243
1913
  if (affectedComponents.length > 0) {
1244
- p8.log.warn(`${affectedComponents.length} installed component(s) referenced this registry:`);
1914
+ p11.log.warn(`${affectedComponents.length} installed component(s) referenced this registry:`);
1245
1915
  for (const name of affectedComponents) {
1246
- p8.log.message(` ${pc8.yellow("!")} ${name}`);
1916
+ p11.log.message(` ${pc10.yellow("!")} ${name}`);
1247
1917
  }
1248
1918
  }
1249
1919
  return { affectedComponents };
@@ -1254,10 +1924,10 @@ async function registryListCommand(opts = {}) {
1254
1924
  if (!config) throw new Error("No kitn.json found. Run `kitn init` first.");
1255
1925
  const entries = Object.entries(config.registries).map(([namespace, url]) => ({ namespace, url }));
1256
1926
  if (entries.length === 0) {
1257
- p8.log.message(pc8.dim(" No registries configured."));
1927
+ p11.log.message(pc10.dim(" No registries configured."));
1258
1928
  } else {
1259
1929
  for (const { namespace, url } of entries) {
1260
- p8.log.message(` ${pc8.bold(namespace.padEnd(16))} ${pc8.dim(url)}`);
1930
+ p11.log.message(` ${pc10.bold(namespace.padEnd(16))} ${pc10.dim(url)}`);
1261
1931
  }
1262
1932
  }
1263
1933
  return entries;
@@ -1347,7 +2017,7 @@ function startUpdateCheck(currentVersion) {
1347
2017
  }
1348
2018
 
1349
2019
  // src/index.ts
1350
- var VERSION = true ? "0.1.6" : "0.0.0-dev";
2020
+ var VERSION = true ? "0.1.8" : "0.0.0-dev";
1351
2021
  var printUpdateNotice = startUpdateCheck(VERSION);
1352
2022
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
1353
2023
  program.command("init").description("Initialize kitn in your project").action(async () => {
@@ -1374,6 +2044,14 @@ program.command("update").description("Update installed components to latest reg
1374
2044
  const { updateCommand: updateCommand2 } = await Promise.resolve().then(() => (init_update(), update_exports));
1375
2045
  await updateCommand2(components);
1376
2046
  });
2047
+ program.command("build").description("Build registry JSON from components with registry.json files").argument("[paths...]", "directories to build (default: scan from cwd)").option("-o, --output <dir>", "output directory", "dist/r").action(async (paths, opts) => {
2048
+ const { buildCommand: buildCommand2 } = await Promise.resolve().then(() => (init_build(), build_exports));
2049
+ await buildCommand2(paths, opts);
2050
+ });
2051
+ program.command("create").description("Scaffold a new kitn component").argument("<type>", "component type (agent, tool, skill, storage)").argument("<name>", "component name").action(async (type, name) => {
2052
+ const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
2053
+ await createCommand2(type, name);
2054
+ });
1377
2055
  program.command("info").description("Show details about a component").argument("<component>", "component name (e.g. weather-agent, @acme/tool@1.0.0)").action(async (component) => {
1378
2056
  const { infoCommand: infoCommand2 } = await Promise.resolve().then(() => (init_info(), info_exports));
1379
2057
  await infoCommand2(component);