@kitnai/cli 0.1.23 → 0.1.24

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
@@ -9,9 +9,72 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/utils/detect.ts
13
+ import { access } from "fs/promises";
14
+ import { join } from "path";
15
+ async function detectPackageManager(dir) {
16
+ for (const [lockfile, pm] of LOCKFILE_MAP) {
17
+ try {
18
+ await access(join(dir, lockfile));
19
+ return pm;
20
+ } catch {
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ function getRunCommand(pm) {
26
+ switch (pm) {
27
+ case "bun":
28
+ return "bunx";
29
+ case "pnpm":
30
+ return "pnpm dlx";
31
+ case "yarn":
32
+ return "yarn dlx";
33
+ case "npm":
34
+ return "npx";
35
+ }
36
+ }
37
+ function detectCliInstaller() {
38
+ const userAgent = process.env.npm_config_user_agent ?? "";
39
+ if (userAgent.startsWith("bun/")) return "bun";
40
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
41
+ if (userAgent.startsWith("yarn/")) return "yarn";
42
+ if (userAgent.startsWith("npm/")) return "npm";
43
+ const invoker = process.env._ ?? "";
44
+ if (invoker.includes("bun")) return "bun";
45
+ if (invoker.includes("pnpm")) return "pnpm";
46
+ if (invoker.includes("yarn")) return "yarn";
47
+ return "npm";
48
+ }
49
+ function getGlobalInstallCommand(pm, pkg) {
50
+ switch (pm) {
51
+ case "bun":
52
+ return `bun install -g ${pkg}`;
53
+ case "pnpm":
54
+ return `pnpm add -g ${pkg}`;
55
+ case "yarn":
56
+ return `yarn global add ${pkg}`;
57
+ case "npm":
58
+ return `npm install -g ${pkg}`;
59
+ }
60
+ }
61
+ var LOCKFILE_MAP;
62
+ var init_detect = __esm({
63
+ "src/utils/detect.ts"() {
64
+ "use strict";
65
+ LOCKFILE_MAP = [
66
+ ["bun.lock", "bun"],
67
+ ["bun.lockb", "bun"],
68
+ ["pnpm-lock.yaml", "pnpm"],
69
+ ["yarn.lock", "yarn"],
70
+ ["package-lock.json", "npm"]
71
+ ];
72
+ }
73
+ });
74
+
12
75
  // src/utils/update-check.ts
13
76
  import { readFile, writeFile, mkdir } from "fs/promises";
14
- import { join } from "path";
77
+ import { join as join2 } from "path";
15
78
  import { homedir } from "os";
16
79
  import pc from "picocolors";
17
80
  async function readCache() {
@@ -65,10 +128,13 @@ function startUpdateCheck(currentVersion) {
65
128
  }
66
129
  }
67
130
  if (latest && isNewer(latest, currentVersion)) {
131
+ const pm = detectCliInstaller();
132
+ const runCmd = `${getRunCommand(pm)} @kitnai/cli@latest`;
133
+ const installCmd = getGlobalInstallCommand(pm, "@kitnai/cli");
68
134
  message = [
69
135
  "",
70
136
  pc.yellow(` Update available: ${pc.dim(currentVersion)} \u2192 ${pc.green(latest)}`),
71
- pc.dim(` Run ${pc.cyan("npx @kitnai/cli@latest")} or ${pc.cyan("npm i -g @kitnai/cli")}`),
137
+ pc.dim(` Run ${pc.cyan(runCmd)} or ${pc.cyan(installCmd)}`),
72
138
  ""
73
139
  ].join("\n");
74
140
  }
@@ -83,15 +149,16 @@ var CACHE_DIR, CACHE_FILE, CHECK_INTERVAL;
83
149
  var init_update_check = __esm({
84
150
  "src/utils/update-check.ts"() {
85
151
  "use strict";
86
- CACHE_DIR = join(homedir(), ".kitn");
87
- CACHE_FILE = join(CACHE_DIR, "update-check.json");
152
+ init_detect();
153
+ CACHE_DIR = join2(homedir(), ".kitn");
154
+ CACHE_FILE = join2(CACHE_DIR, "update-check.json");
88
155
  CHECK_INTERVAL = 60 * 60 * 1e3;
89
156
  }
90
157
  });
91
158
 
92
159
  // src/utils/config.ts
93
160
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
94
- import { join as join2 } from "path";
161
+ import { join as join3 } from "path";
95
162
  import { z } from "zod";
96
163
  function getRegistryUrl(entry) {
97
164
  return typeof entry === "string" ? entry : entry.url;
@@ -102,7 +169,7 @@ function resolveRoutesAlias(config) {
102
169
  }
103
170
  async function readConfig(projectDir) {
104
171
  try {
105
- const raw = await readFile2(join2(projectDir, CONFIG_FILE), "utf-8");
172
+ const raw = await readFile2(join3(projectDir, CONFIG_FILE), "utf-8");
106
173
  return configSchema.parse(JSON.parse(raw));
107
174
  } catch {
108
175
  return null;
@@ -110,16 +177,16 @@ async function readConfig(projectDir) {
110
177
  }
111
178
  async function writeConfig(projectDir, config) {
112
179
  const data = { $schema: "https://kitn.dev/schema/config.json", ...config };
113
- await writeFile2(join2(projectDir, CONFIG_FILE), JSON.stringify(data, null, 2) + "\n");
180
+ await writeFile2(join3(projectDir, CONFIG_FILE), JSON.stringify(data, null, 2) + "\n");
114
181
  }
115
182
  function getInstallPath(config, type, fileName, namespace) {
116
183
  const aliasKey = typeToAliasKey[type];
117
184
  const base = config.aliases[aliasKey];
118
185
  if (namespace && namespace !== "@kitn") {
119
186
  const nsDir = namespace.replace("@", "");
120
- return join2(base, nsDir, fileName);
187
+ return join3(base, nsDir, fileName);
121
188
  }
122
- return join2(base, fileName);
189
+ return join3(base, fileName);
123
190
  }
124
191
  var componentType, installedComponentSchema, registryEntrySchema, registryValueSchema, configSchema, FRAMEWORK_TO_ADAPTER, CONFIG_FILE, typeToAliasKey;
125
192
  var init_config = __esm({
@@ -128,10 +195,13 @@ var init_config = __esm({
128
195
  componentType = z.enum(["kitn:agent", "kitn:tool", "kitn:skill", "kitn:storage", "kitn:package"]);
129
196
  installedComponentSchema = z.object({
130
197
  registry: z.string().optional(),
198
+ type: componentType,
199
+ slot: z.string().optional(),
131
200
  version: z.string(),
132
201
  installedAt: z.string(),
133
202
  files: z.array(z.string()),
134
- hash: z.string()
203
+ hash: z.string(),
204
+ registryDependencies: z.array(z.string()).optional()
135
205
  });
136
206
  registryEntrySchema = z.object({
137
207
  url: z.string(),
@@ -170,7 +240,7 @@ var init_config = __esm({
170
240
 
171
241
  // src/installers/tsconfig-patcher.ts
172
242
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
173
- import { join as join3 } from "path";
243
+ import { join as join4 } from "path";
174
244
  function stripJsonc(text3) {
175
245
  return text3.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
176
246
  }
@@ -209,7 +279,7 @@ function patchTsconfig(tsconfigContent, paths, removePrefixes) {
209
279
  return JSON.stringify(config, null, 2) + "\n";
210
280
  }
211
281
  async function patchProjectTsconfig(projectDir, paths, removePrefixes) {
212
- const tsconfigPath = join3(projectDir, "tsconfig.json");
282
+ const tsconfigPath = join4(projectDir, "tsconfig.json");
213
283
  let content;
214
284
  try {
215
285
  content = await readFile3(tsconfigPath, "utf-8");
@@ -259,33 +329,6 @@ var init_barrel_manager = __esm({
259
329
  }
260
330
  });
261
331
 
262
- // src/utils/detect.ts
263
- import { access } from "fs/promises";
264
- import { join as join4 } from "path";
265
- async function detectPackageManager(dir) {
266
- for (const [lockfile, pm] of LOCKFILE_MAP) {
267
- try {
268
- await access(join4(dir, lockfile));
269
- return pm;
270
- } catch {
271
- }
272
- }
273
- return null;
274
- }
275
- var LOCKFILE_MAP;
276
- var init_detect = __esm({
277
- "src/utils/detect.ts"() {
278
- "use strict";
279
- LOCKFILE_MAP = [
280
- ["bun.lock", "bun"],
281
- ["bun.lockb", "bun"],
282
- ["pnpm-lock.yaml", "pnpm"],
283
- ["yarn.lock", "yarn"],
284
- ["package-lock.json", "npm"]
285
- ];
286
- }
287
- });
288
-
289
332
  // src/registry/fetcher.ts
290
333
  function urlOf(entry) {
291
334
  return typeof entry === "string" ? entry : entry.url;
@@ -729,6 +772,7 @@ var init_schema = __esm({
729
772
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
730
773
  envVars: z2.record(z2.string(), envVarConfigSchema).optional(),
731
774
  categories: z2.array(z2.string()).optional(),
775
+ slot: z2.string().optional(),
732
776
  docs: z2.string().optional(),
733
777
  changelog: z2.array(changelogEntrySchema).optional()
734
778
  });
@@ -746,6 +790,7 @@ var init_schema = __esm({
746
790
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
747
791
  docs: z2.string().optional(),
748
792
  categories: z2.array(z2.string()).optional(),
793
+ slot: z2.string().optional(),
749
794
  version: z2.string().optional(),
750
795
  updatedAt: z2.string().optional(),
751
796
  changelog: z2.array(changelogEntrySchema).optional()
@@ -756,6 +801,7 @@ var init_schema = __esm({
756
801
  description: z2.string(),
757
802
  registryDependencies: z2.array(z2.string()).optional(),
758
803
  categories: z2.array(z2.string()).optional(),
804
+ slot: z2.string().optional(),
759
805
  version: z2.string().optional(),
760
806
  versions: z2.array(z2.string()).optional(),
761
807
  updatedAt: z2.string().optional()
@@ -782,9 +828,9 @@ __export(add_exports, {
782
828
  });
783
829
  import * as p2 from "@clack/prompts";
784
830
  import pc3 from "picocolors";
785
- import { join as join7 } from "path";
831
+ import { join as join7, dirname as dirname3 } from "path";
786
832
  import { existsSync } from "fs";
787
- import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir3 } from "fs/promises";
833
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir3, unlink } from "fs/promises";
788
834
  import { relative as relative2 } from "path";
789
835
  async function addCommand(components, opts) {
790
836
  p2.intro(pc3.bgCyan(pc3.black(" kitn add ")));
@@ -795,8 +841,63 @@ async function addCommand(components, opts) {
795
841
  process.exit(1);
796
842
  }
797
843
  if (components.length === 0) {
798
- p2.log.error("Please specify at least one component to add.");
799
- process.exit(1);
844
+ const fetcher2 = new RegistryFetcher(config.registries);
845
+ const s2 = p2.spinner();
846
+ s2.start("Fetching registry...");
847
+ const allItems = [];
848
+ for (const namespace of Object.keys(config.registries)) {
849
+ try {
850
+ const index = await fetcher2.fetchIndex(namespace);
851
+ for (const item of index.items) {
852
+ allItems.push({ name: item.name, type: item.type, description: item.description, namespace });
853
+ }
854
+ } catch {
855
+ }
856
+ }
857
+ s2.stop(`Found ${allItems.length} component(s)`);
858
+ if (allItems.length === 0) {
859
+ p2.log.warn("No components found in configured registries.");
860
+ process.exit(0);
861
+ }
862
+ const installed = new Set(Object.keys(config.installed ?? {}));
863
+ const typeLabels = {
864
+ "kitn:agent": "Agents",
865
+ "kitn:tool": "Tools",
866
+ "kitn:skill": "Skills",
867
+ "kitn:storage": "Storage",
868
+ "kitn:package": "Packages"
869
+ };
870
+ const groups = /* @__PURE__ */ new Map();
871
+ for (const item of allItems) {
872
+ if (!groups.has(item.type)) groups.set(item.type, []);
873
+ groups.get(item.type).push(item);
874
+ }
875
+ const options = [];
876
+ for (const [type, items] of groups) {
877
+ const label = typeLabels[type] ?? type;
878
+ options.push({ value: `__separator_${type}`, label: pc3.bold(`\u2500\u2500 ${label} ${"\u2500".repeat(Math.max(0, 40 - label.length))}`), hint: "" });
879
+ for (const item of items) {
880
+ const isInstalled = installed.has(item.name);
881
+ options.push({
882
+ value: item.name,
883
+ label: isInstalled ? pc3.dim(`${item.name} (installed)`) : item.name,
884
+ hint: item.description
885
+ });
886
+ }
887
+ }
888
+ const selected = await p2.multiselect({
889
+ message: "Select components to install:",
890
+ options
891
+ });
892
+ if (p2.isCancel(selected)) {
893
+ p2.cancel("Cancelled.");
894
+ process.exit(0);
895
+ }
896
+ components = selected.filter((s3) => !s3.startsWith("__separator_"));
897
+ if (components.length === 0) {
898
+ p2.log.warn("No components selected.");
899
+ process.exit(0);
900
+ }
800
901
  }
801
902
  let typeFilter;
802
903
  const firstAlias = resolveTypeAlias(components[0]);
@@ -952,6 +1053,68 @@ async function addCommand(components, opts) {
952
1053
  process.exit(1);
953
1054
  }
954
1055
  s.stop(`Resolved ${resolved.length} component(s)`);
1056
+ const slotReplacements = /* @__PURE__ */ new Map();
1057
+ for (const item of resolved) {
1058
+ if (!item.slot) continue;
1059
+ const existing = Object.entries(config.installed ?? {}).find(
1060
+ ([key, entry]) => key !== item.name && entry.slot === item.slot
1061
+ );
1062
+ if (!existing) continue;
1063
+ const [existingKey] = existing;
1064
+ const action = await p2.select({
1065
+ message: `${pc3.bold(existingKey)} already fills the ${pc3.cyan(item.slot)} slot. What would you like to do?`,
1066
+ options: [
1067
+ { value: "replace", label: `Replace ${existingKey} with ${item.name}` },
1068
+ { value: "add", label: `Add alongside ${existingKey}` }
1069
+ ]
1070
+ });
1071
+ if (p2.isCancel(action)) {
1072
+ p2.cancel("Cancelled.");
1073
+ process.exit(0);
1074
+ }
1075
+ if (action === "replace") {
1076
+ slotReplacements.set(existingKey, item.name);
1077
+ }
1078
+ }
1079
+ if (slotReplacements.size > 0) {
1080
+ const baseDir2 = config.aliases.base ?? "src/ai";
1081
+ for (const [oldKey] of slotReplacements) {
1082
+ const oldEntry = config.installed[oldKey];
1083
+ if (!oldEntry) continue;
1084
+ for (const filePath of oldEntry.files) {
1085
+ try {
1086
+ await unlink(join7(cwd, filePath));
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ const barrelPath2 = join7(cwd, baseDir2, "index.ts");
1091
+ const barrelEligibleDirs = /* @__PURE__ */ new Set([
1092
+ config.aliases.agents,
1093
+ config.aliases.tools,
1094
+ config.aliases.skills
1095
+ ]);
1096
+ if (existsSync(barrelPath2)) {
1097
+ let barrelContent = await readFile6(barrelPath2, "utf-8");
1098
+ let barrelChanged = false;
1099
+ for (const filePath of oldEntry.files) {
1100
+ const fileDir = dirname3(filePath);
1101
+ if (!barrelEligibleDirs.has(fileDir)) continue;
1102
+ const barrelDir2 = join7(cwd, baseDir2);
1103
+ const importPath = "./" + relative2(barrelDir2, join7(cwd, filePath)).replace(/\\/g, "/");
1104
+ const updated2 = removeImportFromBarrel(barrelContent, importPath);
1105
+ if (updated2 !== barrelContent) {
1106
+ barrelContent = updated2;
1107
+ barrelChanged = true;
1108
+ }
1109
+ }
1110
+ if (barrelChanged) {
1111
+ await writeFile6(barrelPath2, barrelContent);
1112
+ }
1113
+ }
1114
+ delete config.installed[oldKey];
1115
+ p2.log.info(`Replaced ${pc3.dim(oldKey)} \u2192 ${pc3.cyan(slotReplacements.get(oldKey))}`);
1116
+ }
1117
+ }
955
1118
  p2.log.info("Components to install:\n" + resolved.map((item) => {
956
1119
  const isExplicit = expandedNames.includes(item.name) || components.includes(item.name);
957
1120
  const label = isExplicit ? item.name : `${item.name} ${pc3.dim("(dependency)")}`;
@@ -1017,10 +1180,13 @@ async function addCommand(components, opts) {
1017
1180
  const installedKey = ref.namespace === "@kitn" ? item.name : `${ref.namespace}/${item.name}`;
1018
1181
  installed[installedKey] = {
1019
1182
  registry: ref.namespace,
1183
+ type: item.type,
1184
+ ...item.slot && { slot: item.slot },
1020
1185
  version: item.version ?? "1.0.0",
1021
1186
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1022
1187
  files: item.files.map((f) => join7(baseDir2, f.path)),
1023
- hash: contentHash(allContent)
1188
+ hash: contentHash(allContent),
1189
+ registryDependencies: item.registryDependencies
1024
1190
  };
1025
1191
  config.installed = installed;
1026
1192
  } else {
@@ -1086,13 +1252,16 @@ async function addCommand(components, opts) {
1086
1252
  const installedKey = ns === "@kitn" ? item.name : `${ns}/${item.name}`;
1087
1253
  installed[installedKey] = {
1088
1254
  registry: ns,
1255
+ type: item.type,
1256
+ ...item.slot && { slot: item.slot },
1089
1257
  version: item.version ?? "1.0.0",
1090
1258
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1091
1259
  files: item.files.map((f) => {
1092
1260
  const fileName = f.path.split("/").pop();
1093
1261
  return getInstallPath(config, item.type, fileName, ns);
1094
1262
  }),
1095
- hash: contentHash(allContent)
1263
+ hash: contentHash(allContent),
1264
+ registryDependencies: item.registryDependencies
1096
1265
  };
1097
1266
  config.installed = installed;
1098
1267
  }
@@ -1609,36 +1778,16 @@ __export(remove_exports, {
1609
1778
  });
1610
1779
  import * as p6 from "@clack/prompts";
1611
1780
  import pc6 from "picocolors";
1612
- import { join as join10, relative as relative3, dirname as dirname3 } from "path";
1613
- import { unlink, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1781
+ import { join as join10, relative as relative3, dirname as dirname4 } from "path";
1782
+ import { unlink as unlink2, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1614
1783
  import { existsSync as existsSync2 } from "fs";
1615
- async function removeCommand(componentName) {
1616
- const cwd = process.cwd();
1617
- const config = await readConfig(cwd);
1618
- if (!config) {
1619
- p6.log.error("No kitn.json found. Run `kitn init` first.");
1620
- process.exit(1);
1621
- }
1622
- const input = componentName === "routes" ? resolveRoutesAlias(config) : componentName;
1623
- const ref = parseComponentRef(input);
1624
- const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1784
+ async function removeSingleComponent(installedKey, config, cwd) {
1625
1785
  const installed = config.installed?.[installedKey];
1626
- if (!installed) {
1627
- p6.log.error(`Component '${ref.name}' is not installed.`);
1628
- process.exit(1);
1629
- }
1630
- const shouldRemove = await p6.confirm({
1631
- message: `Remove ${ref.name}? This will delete ${installed.files.length} file(s).`,
1632
- initialValue: false
1633
- });
1634
- if (p6.isCancel(shouldRemove) || !shouldRemove) {
1635
- p6.cancel("Remove cancelled.");
1636
- process.exit(0);
1637
- }
1786
+ if (!installed) return;
1638
1787
  const deleted = [];
1639
1788
  for (const filePath of installed.files) {
1640
1789
  try {
1641
- await unlink(join10(cwd, filePath));
1790
+ await unlink2(join10(cwd, filePath));
1642
1791
  deleted.push(filePath);
1643
1792
  } catch {
1644
1793
  p6.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
@@ -1656,7 +1805,7 @@ async function removeCommand(componentName) {
1656
1805
  let barrelContent = await readFile7(barrelPath, "utf-8");
1657
1806
  let barrelChanged = false;
1658
1807
  for (const filePath of deleted) {
1659
- const fileDir = dirname3(filePath);
1808
+ const fileDir = dirname4(filePath);
1660
1809
  if (!barrelEligibleDirs.has(fileDir)) continue;
1661
1810
  const importPath = "./" + relative3(barrelDir, join10(cwd, filePath)).replace(/\\/g, "/");
1662
1811
  const updated = removeImportFromBarrel(barrelContent, importPath);
@@ -1671,14 +1820,113 @@ async function removeCommand(componentName) {
1671
1820
  }
1672
1821
  }
1673
1822
  delete config.installed[installedKey];
1823
+ if (deleted.length > 0) {
1824
+ p6.log.success(`Removed ${installedKey}:
1825
+ ` + deleted.map((f) => ` ${pc6.red("-")} ${f}`).join("\n"));
1826
+ }
1827
+ }
1828
+ async function offerOrphanRemoval(removedDeps, config, cwd) {
1829
+ if (removedDeps.size === 0) return;
1830
+ const remaining = Object.entries(config.installed ?? {});
1831
+ const neededDeps = /* @__PURE__ */ new Set();
1832
+ for (const [, entry] of remaining) {
1833
+ const deps = entry.registryDependencies;
1834
+ if (deps) {
1835
+ for (const dep of deps) {
1836
+ neededDeps.add(dep);
1837
+ }
1838
+ }
1839
+ }
1840
+ const orphans = [...removedDeps].filter(
1841
+ (dep) => dep !== "core" && !neededDeps.has(dep) && config.installed?.[dep]
1842
+ );
1843
+ if (orphans.length === 0) return;
1844
+ const selected = await p6.multiselect({
1845
+ message: "The following dependencies are no longer used. Remove them?",
1846
+ options: orphans.map((dep) => ({
1847
+ value: dep,
1848
+ label: dep,
1849
+ hint: `${config.installed[dep].files.length} file(s)`
1850
+ })),
1851
+ initialValues: orphans
1852
+ // all checked by default
1853
+ });
1854
+ if (p6.isCancel(selected)) return;
1855
+ for (const key of selected) {
1856
+ await removeSingleComponent(key, config, cwd);
1857
+ }
1858
+ }
1859
+ async function removeCommand(componentName) {
1860
+ const cwd = process.cwd();
1861
+ const config = await readConfig(cwd);
1862
+ if (!config) {
1863
+ p6.log.error("No kitn.json found. Run `kitn init` first.");
1864
+ process.exit(1);
1865
+ }
1866
+ if (!componentName) {
1867
+ const installed2 = config.installed ?? {};
1868
+ const installedKeys = Object.keys(installed2);
1869
+ if (installedKeys.length === 0) {
1870
+ p6.log.warn("No components installed.");
1871
+ process.exit(0);
1872
+ }
1873
+ const selected = await p6.multiselect({
1874
+ message: "Select components to remove:",
1875
+ options: installedKeys.map((key) => ({
1876
+ value: key,
1877
+ label: key,
1878
+ hint: `${installed2[key].files.length} file(s)`
1879
+ }))
1880
+ });
1881
+ if (p6.isCancel(selected)) {
1882
+ p6.cancel("Cancelled.");
1883
+ process.exit(0);
1884
+ }
1885
+ const selectedKeys = selected;
1886
+ if (selectedKeys.length === 0) {
1887
+ p6.log.warn("No components selected.");
1888
+ process.exit(0);
1889
+ }
1890
+ for (const key of selectedKeys) {
1891
+ await removeSingleComponent(key, config, cwd);
1892
+ }
1893
+ const allRemovedDeps = /* @__PURE__ */ new Set();
1894
+ for (const key of selectedKeys) {
1895
+ const entry = installed2[key];
1896
+ if (entry?.registryDependencies) {
1897
+ for (const dep of entry.registryDependencies) {
1898
+ allRemovedDeps.add(dep);
1899
+ }
1900
+ }
1901
+ }
1902
+ await offerOrphanRemoval(allRemovedDeps, config, cwd);
1903
+ await writeConfig(cwd, config);
1904
+ p6.outro(pc6.green("Done!"));
1905
+ return;
1906
+ }
1907
+ const input = componentName === "routes" ? resolveRoutesAlias(config) : componentName;
1908
+ const ref = parseComponentRef(input);
1909
+ const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1910
+ const installed = config.installed?.[installedKey];
1911
+ if (!installed) {
1912
+ p6.log.error(`Component '${ref.name}' is not installed.`);
1913
+ process.exit(1);
1914
+ }
1915
+ const shouldRemove = await p6.confirm({
1916
+ message: `Remove ${ref.name}? This will delete ${installed.files.length} file(s).`,
1917
+ initialValue: false
1918
+ });
1919
+ if (p6.isCancel(shouldRemove) || !shouldRemove) {
1920
+ p6.cancel("Remove cancelled.");
1921
+ process.exit(0);
1922
+ }
1923
+ await removeSingleComponent(installedKey, config, cwd);
1924
+ const removedDeps = new Set(installed.registryDependencies ?? []);
1925
+ await offerOrphanRemoval(removedDeps, config, cwd);
1674
1926
  if (Object.keys(config.installed).length === 0) {
1675
1927
  delete config.installed;
1676
1928
  }
1677
1929
  await writeConfig(cwd, config);
1678
- if (deleted.length > 0) {
1679
- p6.log.success(`Removed ${ref.name}:
1680
- ` + deleted.map((f) => ` ${pc6.red("-")} ${f}`).join("\n"));
1681
- }
1682
1930
  }
1683
1931
  var init_remove = __esm({
1684
1932
  "src/commands/remove.ts"() {
@@ -2013,6 +2261,7 @@ var check_exports = {};
2013
2261
  __export(check_exports, {
2014
2262
  checkCommand: () => checkCommand
2015
2263
  });
2264
+ import { execSync as execSync2 } from "child_process";
2016
2265
  import * as p10 from "@clack/prompts";
2017
2266
  import pc9 from "picocolors";
2018
2267
  async function checkCommand(currentVersion) {
@@ -2028,7 +2277,22 @@ async function checkCommand(currentVersion) {
2028
2277
  }
2029
2278
  if (isNewer(latest, currentVersion)) {
2030
2279
  s.stop(pc9.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
2031
- p10.log.message(` Run: ${pc9.cyan("npm i -g @kitnai/cli")}`);
2280
+ const pm = detectCliInstaller();
2281
+ const installCmd = getGlobalInstallCommand(pm, "@kitnai/cli");
2282
+ const shouldUpdate = await p10.confirm({ message: "Update now?" });
2283
+ if (p10.isCancel(shouldUpdate) || !shouldUpdate) {
2284
+ p10.log.message(` Run: ${pc9.cyan(installCmd)}`);
2285
+ } else {
2286
+ const us = p10.spinner();
2287
+ us.start("Updating...");
2288
+ try {
2289
+ execSync2(installCmd, { stdio: "pipe" });
2290
+ us.stop(pc9.green(`Updated to v${latest}`));
2291
+ } catch {
2292
+ us.stop(pc9.red("Update failed"));
2293
+ p10.log.message(` Run manually: ${pc9.cyan(installCmd)}`);
2294
+ }
2295
+ }
2032
2296
  } else {
2033
2297
  s.stop(pc9.green("You're on the latest version"));
2034
2298
  }
@@ -2037,6 +2301,7 @@ async function checkCommand(currentVersion) {
2037
2301
  var init_check = __esm({
2038
2302
  "src/commands/check.ts"() {
2039
2303
  "use strict";
2304
+ init_detect();
2040
2305
  init_update_check();
2041
2306
  }
2042
2307
  });
@@ -2140,14 +2405,14 @@ var init_registry = __esm({
2140
2405
  // src/index.ts
2141
2406
  init_update_check();
2142
2407
  import { Command } from "commander";
2143
- var VERSION = true ? "0.1.23" : "0.0.0-dev";
2408
+ var VERSION = true ? "0.1.24" : "0.0.0-dev";
2144
2409
  var printUpdateNotice = startUpdateCheck(VERSION);
2145
2410
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
2146
2411
  program.command("init").description("Initialize kitn in your project").option("-r, --runtime <runtime>", "runtime to use (bun, node, deno)").option("-f, --framework <framework>", "HTTP framework (hono, hono-openapi, elysia)").option("-b, --base <path>", "base directory for components (default: src/ai)").option("-y, --yes", "accept all defaults without prompting").action(async (opts) => {
2147
2412
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2148
2413
  await initCommand2(opts);
2149
2414
  });
2150
- program.command("add").description("Add components from the registry (supports type-first: kitn add agent <name>)").argument("[components...]", "component names or type followed by names").option("-o, --overwrite", "overwrite existing files without prompting").option("-t, --type <type>", "filter by component type during resolution").action(async (components, opts) => {
2415
+ program.command("add").alias("install").description("Add components from the registry (supports type-first: kitn add agent <name>)").argument("[components...]", "component names or type followed by names").option("-o, --overwrite", "overwrite existing files without prompting").option("-t, --type <type>", "filter by component type during resolution").action(async (components, opts) => {
2151
2416
  const { addCommand: addCommand2 } = await Promise.resolve().then(() => (init_add(), add_exports));
2152
2417
  await addCommand2(components, opts);
2153
2418
  });
@@ -2159,7 +2424,7 @@ program.command("diff").description("Show differences between local and registry
2159
2424
  const { diffCommand: diffCommand2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
2160
2425
  await diffCommand2(component);
2161
2426
  });
2162
- program.command("remove").description("Remove an installed component").argument("<component>", "component name to remove").action(async (component) => {
2427
+ program.command("remove").alias("uninstall").description("Remove an installed component").argument("[component]", "component name to remove (interactive if omitted)").action(async (component) => {
2163
2428
  const { removeCommand: removeCommand2 } = await Promise.resolve().then(() => (init_remove(), remove_exports));
2164
2429
  await removeCommand2(component);
2165
2430
  });