@kitnai/cli 0.1.23 → 0.1.25

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
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
94
- import { join as join2 } from "path";
160
+ import { readFile as readFile2, writeFile as writeFile2, unlink } from "fs/promises";
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,28 +177,49 @@ 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");
181
+ }
182
+ async function readLock(projectDir) {
183
+ try {
184
+ const raw = await readFile2(join3(projectDir, LOCK_FILE), "utf-8");
185
+ return lockSchema.parse(JSON.parse(raw));
186
+ } catch {
187
+ return {};
188
+ }
189
+ }
190
+ async function writeLock(projectDir, lock) {
191
+ if (Object.keys(lock).length === 0) {
192
+ try {
193
+ await unlink(join3(projectDir, LOCK_FILE));
194
+ } catch {
195
+ }
196
+ return;
197
+ }
198
+ await writeFile2(join3(projectDir, LOCK_FILE), JSON.stringify(lock, null, 2) + "\n");
114
199
  }
115
200
  function getInstallPath(config, type, fileName, namespace) {
116
201
  const aliasKey = typeToAliasKey[type];
117
202
  const base = config.aliases[aliasKey];
118
203
  if (namespace && namespace !== "@kitn") {
119
204
  const nsDir = namespace.replace("@", "");
120
- return join2(base, nsDir, fileName);
205
+ return join3(base, nsDir, fileName);
121
206
  }
122
- return join2(base, fileName);
207
+ return join3(base, fileName);
123
208
  }
124
- var componentType, installedComponentSchema, registryEntrySchema, registryValueSchema, configSchema, FRAMEWORK_TO_ADAPTER, CONFIG_FILE, typeToAliasKey;
209
+ var componentType, installedComponentSchema, registryEntrySchema, registryValueSchema, configSchema, FRAMEWORK_TO_ADAPTER, CONFIG_FILE, LOCK_FILE, lockSchema, typeToAliasKey;
125
210
  var init_config = __esm({
126
211
  "src/utils/config.ts"() {
127
212
  "use strict";
128
213
  componentType = z.enum(["kitn:agent", "kitn:tool", "kitn:skill", "kitn:storage", "kitn:package"]);
129
214
  installedComponentSchema = z.object({
130
215
  registry: z.string().optional(),
216
+ type: componentType.optional(),
217
+ slot: z.string().optional(),
131
218
  version: z.string(),
132
219
  installedAt: z.string(),
133
220
  files: z.array(z.string()),
134
- hash: z.string()
221
+ hash: z.string(),
222
+ registryDependencies: z.array(z.string()).optional()
135
223
  });
136
224
  registryEntrySchema = z.object({
137
225
  url: z.string(),
@@ -150,8 +238,7 @@ var init_config = __esm({
150
238
  skills: z.string(),
151
239
  storage: z.string()
152
240
  }),
153
- registries: z.record(z.string(), registryValueSchema),
154
- installed: z.record(z.string(), installedComponentSchema).optional()
241
+ registries: z.record(z.string(), registryValueSchema)
155
242
  });
156
243
  FRAMEWORK_TO_ADAPTER = {
157
244
  hono: "hono",
@@ -159,6 +246,8 @@ var init_config = __esm({
159
246
  elysia: "elysia"
160
247
  };
161
248
  CONFIG_FILE = "kitn.json";
249
+ LOCK_FILE = "kitn.lock";
250
+ lockSchema = z.record(z.string(), installedComponentSchema);
162
251
  typeToAliasKey = {
163
252
  "kitn:agent": "agents",
164
253
  "kitn:tool": "tools",
@@ -170,7 +259,7 @@ var init_config = __esm({
170
259
 
171
260
  // src/installers/tsconfig-patcher.ts
172
261
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
173
- import { join as join3 } from "path";
262
+ import { join as join4 } from "path";
174
263
  function stripJsonc(text3) {
175
264
  return text3.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
176
265
  }
@@ -209,7 +298,7 @@ function patchTsconfig(tsconfigContent, paths, removePrefixes) {
209
298
  return JSON.stringify(config, null, 2) + "\n";
210
299
  }
211
300
  async function patchProjectTsconfig(projectDir, paths, removePrefixes) {
212
- const tsconfigPath = join3(projectDir, "tsconfig.json");
301
+ const tsconfigPath = join4(projectDir, "tsconfig.json");
213
302
  let content;
214
303
  try {
215
304
  content = await readFile3(tsconfigPath, "utf-8");
@@ -259,33 +348,6 @@ var init_barrel_manager = __esm({
259
348
  }
260
349
  });
261
350
 
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
351
  // src/registry/fetcher.ts
290
352
  function urlOf(entry) {
291
353
  return typeof entry === "string" ? entry : entry.url;
@@ -729,6 +791,7 @@ var init_schema = __esm({
729
791
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
730
792
  envVars: z2.record(z2.string(), envVarConfigSchema).optional(),
731
793
  categories: z2.array(z2.string()).optional(),
794
+ slot: z2.string().optional(),
732
795
  docs: z2.string().optional(),
733
796
  changelog: z2.array(changelogEntrySchema).optional()
734
797
  });
@@ -746,6 +809,7 @@ var init_schema = __esm({
746
809
  tsconfig: z2.record(z2.string(), z2.array(z2.string())).optional(),
747
810
  docs: z2.string().optional(),
748
811
  categories: z2.array(z2.string()).optional(),
812
+ slot: z2.string().optional(),
749
813
  version: z2.string().optional(),
750
814
  updatedAt: z2.string().optional(),
751
815
  changelog: z2.array(changelogEntrySchema).optional()
@@ -756,6 +820,7 @@ var init_schema = __esm({
756
820
  description: z2.string(),
757
821
  registryDependencies: z2.array(z2.string()).optional(),
758
822
  categories: z2.array(z2.string()).optional(),
823
+ slot: z2.string().optional(),
759
824
  version: z2.string().optional(),
760
825
  versions: z2.array(z2.string()).optional(),
761
826
  updatedAt: z2.string().optional()
@@ -782,9 +847,9 @@ __export(add_exports, {
782
847
  });
783
848
  import * as p2 from "@clack/prompts";
784
849
  import pc3 from "picocolors";
785
- import { join as join7 } from "path";
850
+ import { join as join7, dirname as dirname3 } from "path";
786
851
  import { existsSync } from "fs";
787
- import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir3 } from "fs/promises";
852
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
788
853
  import { relative as relative2 } from "path";
789
854
  async function addCommand(components, opts) {
790
855
  p2.intro(pc3.bgCyan(pc3.black(" kitn add ")));
@@ -794,9 +859,65 @@ async function addCommand(components, opts) {
794
859
  p2.log.error("No kitn.json found. Run `kitn init` first.");
795
860
  process.exit(1);
796
861
  }
862
+ const lock = await readLock(cwd);
797
863
  if (components.length === 0) {
798
- p2.log.error("Please specify at least one component to add.");
799
- process.exit(1);
864
+ const fetcher2 = new RegistryFetcher(config.registries);
865
+ const s2 = p2.spinner();
866
+ s2.start("Fetching registry...");
867
+ const allItems = [];
868
+ for (const namespace of Object.keys(config.registries)) {
869
+ try {
870
+ const index = await fetcher2.fetchIndex(namespace);
871
+ for (const item of index.items) {
872
+ allItems.push({ name: item.name, type: item.type, description: item.description, namespace });
873
+ }
874
+ } catch {
875
+ }
876
+ }
877
+ s2.stop(`Found ${allItems.length} component(s)`);
878
+ if (allItems.length === 0) {
879
+ p2.log.warn("No components found in configured registries.");
880
+ process.exit(0);
881
+ }
882
+ const installed = new Set(Object.keys(lock));
883
+ const typeLabels = {
884
+ "kitn:agent": "Agents",
885
+ "kitn:tool": "Tools",
886
+ "kitn:skill": "Skills",
887
+ "kitn:storage": "Storage",
888
+ "kitn:package": "Packages"
889
+ };
890
+ const groups = /* @__PURE__ */ new Map();
891
+ for (const item of allItems) {
892
+ if (!groups.has(item.type)) groups.set(item.type, []);
893
+ groups.get(item.type).push(item);
894
+ }
895
+ const options = [];
896
+ for (const [type, items] of groups) {
897
+ const label = typeLabels[type] ?? type;
898
+ options.push({ value: `__separator_${type}`, label: pc3.bold(`\u2500\u2500 ${label} ${"\u2500".repeat(Math.max(0, 40 - label.length))}`), hint: "" });
899
+ for (const item of items) {
900
+ const isInstalled = installed.has(item.name);
901
+ options.push({
902
+ value: item.name,
903
+ label: isInstalled ? pc3.dim(`${item.name} (installed)`) : item.name,
904
+ hint: item.description
905
+ });
906
+ }
907
+ }
908
+ const selected = await p2.multiselect({
909
+ message: "Select components to install:",
910
+ options
911
+ });
912
+ if (p2.isCancel(selected)) {
913
+ p2.cancel("Cancelled.");
914
+ process.exit(0);
915
+ }
916
+ components = selected.filter((s3) => !s3.startsWith("__separator_"));
917
+ if (components.length === 0) {
918
+ p2.log.warn("No components selected.");
919
+ process.exit(0);
920
+ }
800
921
  }
801
922
  let typeFilter;
802
923
  const firstAlias = resolveTypeAlias(components[0]);
@@ -952,6 +1073,68 @@ async function addCommand(components, opts) {
952
1073
  process.exit(1);
953
1074
  }
954
1075
  s.stop(`Resolved ${resolved.length} component(s)`);
1076
+ const slotReplacements = /* @__PURE__ */ new Map();
1077
+ for (const item of resolved) {
1078
+ if (!item.slot) continue;
1079
+ const existing = Object.entries(lock).find(
1080
+ ([key, entry]) => key !== item.name && entry.slot === item.slot
1081
+ );
1082
+ if (!existing) continue;
1083
+ const [existingKey] = existing;
1084
+ const action = await p2.select({
1085
+ message: `${pc3.bold(existingKey)} already fills the ${pc3.cyan(item.slot)} slot. What would you like to do?`,
1086
+ options: [
1087
+ { value: "replace", label: `Replace ${existingKey} with ${item.name}` },
1088
+ { value: "add", label: `Add alongside ${existingKey}` }
1089
+ ]
1090
+ });
1091
+ if (p2.isCancel(action)) {
1092
+ p2.cancel("Cancelled.");
1093
+ process.exit(0);
1094
+ }
1095
+ if (action === "replace") {
1096
+ slotReplacements.set(existingKey, item.name);
1097
+ }
1098
+ }
1099
+ if (slotReplacements.size > 0) {
1100
+ const baseDir2 = config.aliases.base ?? "src/ai";
1101
+ for (const [oldKey] of slotReplacements) {
1102
+ const oldEntry = lock[oldKey];
1103
+ if (!oldEntry) continue;
1104
+ for (const filePath of oldEntry.files) {
1105
+ try {
1106
+ await unlink2(join7(cwd, filePath));
1107
+ } catch {
1108
+ }
1109
+ }
1110
+ const barrelPath2 = join7(cwd, baseDir2, "index.ts");
1111
+ const barrelEligibleDirs = /* @__PURE__ */ new Set([
1112
+ config.aliases.agents,
1113
+ config.aliases.tools,
1114
+ config.aliases.skills
1115
+ ]);
1116
+ if (existsSync(barrelPath2)) {
1117
+ let barrelContent = await readFile6(barrelPath2, "utf-8");
1118
+ let barrelChanged = false;
1119
+ for (const filePath of oldEntry.files) {
1120
+ const fileDir = dirname3(filePath);
1121
+ if (!barrelEligibleDirs.has(fileDir)) continue;
1122
+ const barrelDir2 = join7(cwd, baseDir2);
1123
+ const importPath = "./" + relative2(barrelDir2, join7(cwd, filePath)).replace(/\\/g, "/");
1124
+ const updated2 = removeImportFromBarrel(barrelContent, importPath);
1125
+ if (updated2 !== barrelContent) {
1126
+ barrelContent = updated2;
1127
+ barrelChanged = true;
1128
+ }
1129
+ }
1130
+ if (barrelChanged) {
1131
+ await writeFile6(barrelPath2, barrelContent);
1132
+ }
1133
+ }
1134
+ delete lock[oldKey];
1135
+ p2.log.info(`Replaced ${pc3.dim(oldKey)} \u2192 ${pc3.cyan(slotReplacements.get(oldKey))}`);
1136
+ }
1137
+ }
955
1138
  p2.log.info("Components to install:\n" + resolved.map((item) => {
956
1139
  const isExplicit = expandedNames.includes(item.name) || components.includes(item.name);
957
1140
  const label = isExplicit ? item.name : `${item.name} ${pc3.dim("(dependency)")}`;
@@ -965,7 +1148,7 @@ async function addCommand(components, opts) {
965
1148
  for (const item of resolved) {
966
1149
  if (item.dependencies) allDeps.push(...item.dependencies);
967
1150
  if (item.devDependencies) allDevDeps.push(...item.devDependencies);
968
- const existingInstall = (config.installed ?? {})[item.name];
1151
+ const existingInstall = lock[item.name];
969
1152
  if (existingInstall && item.type === "kitn:package") {
970
1153
  const allContent = item.files.map((f) => f.content).join("\n");
971
1154
  if (contentHash(allContent) === existingInstall.hash) {
@@ -1011,18 +1194,19 @@ async function addCommand(components, opts) {
1011
1194
  break;
1012
1195
  }
1013
1196
  }
1014
- const installed = config.installed ?? {};
1015
1197
  const allContent = item.files.map((f) => f.content).join("\n");
1016
1198
  const ref = refs.find((r) => r.name === item.name) ?? { namespace: "@kitn", name: item.name, version: void 0 };
1017
1199
  const installedKey = ref.namespace === "@kitn" ? item.name : `${ref.namespace}/${item.name}`;
1018
- installed[installedKey] = {
1200
+ lock[installedKey] = {
1019
1201
  registry: ref.namespace,
1202
+ type: item.type,
1203
+ ...item.slot && { slot: item.slot },
1020
1204
  version: item.version ?? "1.0.0",
1021
1205
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1022
1206
  files: item.files.map((f) => join7(baseDir2, f.path)),
1023
- hash: contentHash(allContent)
1207
+ hash: contentHash(allContent),
1208
+ registryDependencies: item.registryDependencies
1024
1209
  };
1025
- config.installed = installed;
1026
1210
  } else {
1027
1211
  const ref = refs.find((r) => r.name === item.name) ?? { namespace: "@kitn", name: item.name, version: void 0 };
1028
1212
  const ns = ref.namespace;
@@ -1078,23 +1262,24 @@ async function addCommand(components, opts) {
1078
1262
  break;
1079
1263
  }
1080
1264
  }
1081
- const installed = config.installed ?? {};
1082
1265
  const allContent = item.files.map((f) => {
1083
1266
  const fn = f.path.split("/").pop();
1084
1267
  return rewriteKitnImports(f.content, item.type, fn, config.aliases);
1085
1268
  }).join("\n");
1086
1269
  const installedKey = ns === "@kitn" ? item.name : `${ns}/${item.name}`;
1087
- installed[installedKey] = {
1270
+ lock[installedKey] = {
1088
1271
  registry: ns,
1272
+ type: item.type,
1273
+ ...item.slot && { slot: item.slot },
1089
1274
  version: item.version ?? "1.0.0",
1090
1275
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1091
1276
  files: item.files.map((f) => {
1092
1277
  const fileName = f.path.split("/").pop();
1093
1278
  return getInstallPath(config, item.type, fileName, ns);
1094
1279
  }),
1095
- hash: contentHash(allContent)
1280
+ hash: contentHash(allContent),
1281
+ registryDependencies: item.registryDependencies
1096
1282
  };
1097
- config.installed = installed;
1098
1283
  }
1099
1284
  }
1100
1285
  const BARREL_ELIGIBLE = /* @__PURE__ */ new Set(["kitn:agent", "kitn:tool", "kitn:skill"]);
@@ -1139,6 +1324,7 @@ async function addCommand(components, opts) {
1139
1324
  }
1140
1325
  }
1141
1326
  await writeConfig(cwd, config);
1327
+ await writeLock(cwd, lock);
1142
1328
  const uniqueDeps = [...new Set(allDeps)];
1143
1329
  const uniqueDevDeps = [...new Set(allDevDeps)].filter((d) => !uniqueDeps.includes(d));
1144
1330
  const totalDeps = uniqueDeps.length + uniqueDevDeps.length;
@@ -1175,7 +1361,7 @@ async function addCommand(components, opts) {
1175
1361
  }
1176
1362
  }
1177
1363
  const resolvedNames = new Set(resolved.map((r) => r.name));
1178
- const projectInstalled = new Set(Object.keys(config.installed ?? {}));
1364
+ const projectInstalled = new Set(Object.keys(lock));
1179
1365
  const hints = [];
1180
1366
  const adapterName = resolveRoutesAlias(config);
1181
1367
  if (resolvedNames.has("core") && !resolvedNames.has(adapterName) && !projectInstalled.has(adapterName)) {
@@ -1437,7 +1623,7 @@ async function listCommand(typeFilter, opts) {
1437
1623
  for (const e of errors) {
1438
1624
  p4.log.warn(`${pc5.yellow("\u26A0")} Failed to fetch ${e}`);
1439
1625
  }
1440
- const installed = config.installed ?? {};
1626
+ const installed = await readLock(cwd);
1441
1627
  const typeGroups = /* @__PURE__ */ new Map();
1442
1628
  for (const item of allItems) {
1443
1629
  const group = item.type.replace("kitn:", "");
@@ -1530,8 +1716,9 @@ async function diffCommand(componentName) {
1530
1716
  }
1531
1717
  const input = componentName === "routes" ? resolveRoutesAlias(config) : componentName;
1532
1718
  const ref = parseComponentRef(input);
1719
+ const lock = await readLock(cwd);
1533
1720
  const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1534
- const installed = config.installed?.[installedKey];
1721
+ const installed = lock[installedKey];
1535
1722
  if (!installed) {
1536
1723
  p5.log.error(`Component '${ref.name}' is not installed.`);
1537
1724
  process.exit(1);
@@ -1609,36 +1796,16 @@ __export(remove_exports, {
1609
1796
  });
1610
1797
  import * as p6 from "@clack/prompts";
1611
1798
  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";
1799
+ import { join as join10, relative as relative3, dirname as dirname4 } from "path";
1800
+ import { unlink as unlink3, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
1614
1801
  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}`;
1625
- 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
- }
1802
+ async function removeSingleComponent(installedKey, lock, config, cwd) {
1803
+ const entry = lock[installedKey];
1804
+ if (!entry) return;
1638
1805
  const deleted = [];
1639
- for (const filePath of installed.files) {
1806
+ for (const filePath of entry.files) {
1640
1807
  try {
1641
- await unlink(join10(cwd, filePath));
1808
+ await unlink3(join10(cwd, filePath));
1642
1809
  deleted.push(filePath);
1643
1810
  } catch {
1644
1811
  p6.log.warn(`Could not delete ${filePath} (may have been moved or renamed)`);
@@ -1656,7 +1823,7 @@ async function removeCommand(componentName) {
1656
1823
  let barrelContent = await readFile7(barrelPath, "utf-8");
1657
1824
  let barrelChanged = false;
1658
1825
  for (const filePath of deleted) {
1659
- const fileDir = dirname3(filePath);
1826
+ const fileDir = dirname4(filePath);
1660
1827
  if (!barrelEligibleDirs.has(fileDir)) continue;
1661
1828
  const importPath = "./" + relative3(barrelDir, join10(cwd, filePath)).replace(/\\/g, "/");
1662
1829
  const updated = removeImportFromBarrel(barrelContent, importPath);
@@ -1670,16 +1837,111 @@ async function removeCommand(componentName) {
1670
1837
  p6.log.info(`Updated barrel file: ${join10(baseDir, "index.ts")}`);
1671
1838
  }
1672
1839
  }
1673
- delete config.installed[installedKey];
1674
- if (Object.keys(config.installed).length === 0) {
1675
- delete config.installed;
1676
- }
1677
- await writeConfig(cwd, config);
1840
+ delete lock[installedKey];
1678
1841
  if (deleted.length > 0) {
1679
- p6.log.success(`Removed ${ref.name}:
1842
+ p6.log.success(`Removed ${installedKey}:
1680
1843
  ` + deleted.map((f) => ` ${pc6.red("-")} ${f}`).join("\n"));
1681
1844
  }
1682
1845
  }
1846
+ async function offerOrphanRemoval(removedDeps, lock, config, cwd) {
1847
+ if (removedDeps.size === 0) return;
1848
+ const remaining = Object.entries(lock);
1849
+ const neededDeps = /* @__PURE__ */ new Set();
1850
+ for (const [, entry] of remaining) {
1851
+ if (entry.registryDependencies) {
1852
+ for (const dep of entry.registryDependencies) {
1853
+ neededDeps.add(dep);
1854
+ }
1855
+ }
1856
+ }
1857
+ const orphans = [...removedDeps].filter(
1858
+ (dep) => dep !== "core" && !neededDeps.has(dep) && lock[dep]
1859
+ );
1860
+ if (orphans.length === 0) return;
1861
+ const selected = await p6.multiselect({
1862
+ message: "The following dependencies are no longer used. Remove them?",
1863
+ options: orphans.map((dep) => ({
1864
+ value: dep,
1865
+ label: dep,
1866
+ hint: `${lock[dep].files.length} file(s)`
1867
+ })),
1868
+ initialValues: orphans
1869
+ // all checked by default
1870
+ });
1871
+ if (p6.isCancel(selected)) return;
1872
+ for (const key of selected) {
1873
+ await removeSingleComponent(key, lock, config, cwd);
1874
+ }
1875
+ }
1876
+ async function removeCommand(componentName) {
1877
+ const cwd = process.cwd();
1878
+ const config = await readConfig(cwd);
1879
+ if (!config) {
1880
+ p6.log.error("No kitn.json found. Run `kitn init` first.");
1881
+ process.exit(1);
1882
+ }
1883
+ const lock = await readLock(cwd);
1884
+ if (!componentName) {
1885
+ const installedKeys = Object.keys(lock);
1886
+ if (installedKeys.length === 0) {
1887
+ p6.log.warn("No components installed.");
1888
+ process.exit(0);
1889
+ }
1890
+ const selected = await p6.multiselect({
1891
+ message: "Select components to remove:",
1892
+ options: installedKeys.map((key) => ({
1893
+ value: key,
1894
+ label: key,
1895
+ hint: `${lock[key].files.length} file(s)`
1896
+ }))
1897
+ });
1898
+ if (p6.isCancel(selected)) {
1899
+ p6.cancel("Cancelled.");
1900
+ process.exit(0);
1901
+ }
1902
+ const selectedKeys = selected;
1903
+ if (selectedKeys.length === 0) {
1904
+ p6.log.warn("No components selected.");
1905
+ process.exit(0);
1906
+ }
1907
+ const allRemovedDeps = /* @__PURE__ */ new Set();
1908
+ for (const key of selectedKeys) {
1909
+ const entry2 = lock[key];
1910
+ if (entry2?.registryDependencies) {
1911
+ for (const dep of entry2.registryDependencies) {
1912
+ allRemovedDeps.add(dep);
1913
+ }
1914
+ }
1915
+ }
1916
+ for (const key of selectedKeys) {
1917
+ await removeSingleComponent(key, lock, config, cwd);
1918
+ }
1919
+ await offerOrphanRemoval(allRemovedDeps, lock, config, cwd);
1920
+ await writeLock(cwd, lock);
1921
+ p6.outro(pc6.green("Done!"));
1922
+ return;
1923
+ }
1924
+ const input = componentName === "routes" ? resolveRoutesAlias(config) : componentName;
1925
+ const ref = parseComponentRef(input);
1926
+ const installedKey = ref.namespace === "@kitn" ? ref.name : `${ref.namespace}/${ref.name}`;
1927
+ const entry = lock[installedKey];
1928
+ if (!entry) {
1929
+ p6.log.error(`Component '${ref.name}' is not installed.`);
1930
+ process.exit(1);
1931
+ }
1932
+ const shouldRemove = await p6.confirm({
1933
+ message: `Remove ${ref.name}? This will delete ${entry.files.length} file(s).`,
1934
+ initialValue: false
1935
+ });
1936
+ if (p6.isCancel(shouldRemove) || !shouldRemove) {
1937
+ p6.cancel("Remove cancelled.");
1938
+ process.exit(0);
1939
+ }
1940
+ const removedDeps = new Set(entry.registryDependencies ?? []);
1941
+ await removeSingleComponent(installedKey, lock, config, cwd);
1942
+ await offerOrphanRemoval(removedDeps, lock, config, cwd);
1943
+ await writeLock(cwd, lock);
1944
+ }
1683
1945
  var init_remove = __esm({
1684
1946
  "src/commands/remove.ts"() {
1685
1947
  "use strict";
@@ -1703,12 +1965,12 @@ async function updateCommand(components) {
1703
1965
  p7.log.error("No kitn.json found. Run `kitn init` first.");
1704
1966
  process.exit(1);
1705
1967
  }
1706
- const installed = config.installed;
1707
- if (!installed || Object.keys(installed).length === 0) {
1968
+ const lock = await readLock(cwd);
1969
+ if (Object.keys(lock).length === 0) {
1708
1970
  p7.log.info("No installed components to update.");
1709
1971
  return;
1710
1972
  }
1711
- components = Object.keys(installed);
1973
+ components = Object.keys(lock);
1712
1974
  }
1713
1975
  await addCommand(components, { overwrite: true });
1714
1976
  }
@@ -1984,7 +2246,8 @@ async function infoCommand(component) {
1984
2246
  if (fileCount > maxShown) {
1985
2247
  console.log(` ${pc8.dim(`... and ${fileCount - maxShown} more`)}`);
1986
2248
  }
1987
- const installed = config.installed?.[item.name];
2249
+ const lock = await readLock(cwd);
2250
+ const installed = lock[item.name];
1988
2251
  if (installed) {
1989
2252
  console.log();
1990
2253
  console.log(
@@ -2013,6 +2276,7 @@ var check_exports = {};
2013
2276
  __export(check_exports, {
2014
2277
  checkCommand: () => checkCommand
2015
2278
  });
2279
+ import { execSync as execSync2 } from "child_process";
2016
2280
  import * as p10 from "@clack/prompts";
2017
2281
  import pc9 from "picocolors";
2018
2282
  async function checkCommand(currentVersion) {
@@ -2028,7 +2292,22 @@ async function checkCommand(currentVersion) {
2028
2292
  }
2029
2293
  if (isNewer(latest, currentVersion)) {
2030
2294
  s.stop(pc9.yellow(`Update available: ${currentVersion} \u2192 ${latest}`));
2031
- p10.log.message(` Run: ${pc9.cyan("npm i -g @kitnai/cli")}`);
2295
+ const pm = detectCliInstaller();
2296
+ const installCmd = getGlobalInstallCommand(pm, "@kitnai/cli");
2297
+ const shouldUpdate = await p10.confirm({ message: "Update now?" });
2298
+ if (p10.isCancel(shouldUpdate) || !shouldUpdate) {
2299
+ p10.log.message(` Run: ${pc9.cyan(installCmd)}`);
2300
+ } else {
2301
+ const us = p10.spinner();
2302
+ us.start("Updating...");
2303
+ try {
2304
+ execSync2(installCmd, { stdio: "pipe" });
2305
+ us.stop(pc9.green(`Updated to v${latest}`));
2306
+ } catch {
2307
+ us.stop(pc9.red("Update failed"));
2308
+ p10.log.message(` Run manually: ${pc9.cyan(installCmd)}`);
2309
+ }
2310
+ }
2032
2311
  } else {
2033
2312
  s.stop(pc9.green("You're on the latest version"));
2034
2313
  }
@@ -2037,6 +2316,7 @@ async function checkCommand(currentVersion) {
2037
2316
  var init_check = __esm({
2038
2317
  "src/commands/check.ts"() {
2039
2318
  "use strict";
2319
+ init_detect();
2040
2320
  init_update_check();
2041
2321
  }
2042
2322
  });
@@ -2090,12 +2370,11 @@ async function registryRemoveCommand(namespace, opts = {}) {
2090
2370
  if (namespace === "@kitn" && !opts.force) {
2091
2371
  throw new Error("Cannot remove the default @kitn registry. Use --force to override.");
2092
2372
  }
2373
+ const lock = await readLock(cwd);
2093
2374
  const affectedComponents = [];
2094
- if (config.installed) {
2095
- for (const [name, entry] of Object.entries(config.installed)) {
2096
- if (entry.registry === namespace) {
2097
- affectedComponents.push(name);
2098
- }
2375
+ for (const [name, entry] of Object.entries(lock)) {
2376
+ if (entry.registry === namespace) {
2377
+ affectedComponents.push(name);
2099
2378
  }
2100
2379
  }
2101
2380
  delete config.registries[namespace];
@@ -2140,14 +2419,14 @@ var init_registry = __esm({
2140
2419
  // src/index.ts
2141
2420
  init_update_check();
2142
2421
  import { Command } from "commander";
2143
- var VERSION = true ? "0.1.23" : "0.0.0-dev";
2422
+ var VERSION = true ? "0.1.25" : "0.0.0-dev";
2144
2423
  var printUpdateNotice = startUpdateCheck(VERSION);
2145
2424
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
2146
2425
  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
2426
  const { initCommand: initCommand2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2148
2427
  await initCommand2(opts);
2149
2428
  });
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) => {
2429
+ 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
2430
  const { addCommand: addCommand2 } = await Promise.resolve().then(() => (init_add(), add_exports));
2152
2431
  await addCommand2(components, opts);
2153
2432
  });
@@ -2159,7 +2438,7 @@ program.command("diff").description("Show differences between local and registry
2159
2438
  const { diffCommand: diffCommand2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
2160
2439
  await diffCommand2(component);
2161
2440
  });
2162
- program.command("remove").description("Remove an installed component").argument("<component>", "component name to remove").action(async (component) => {
2441
+ program.command("remove").alias("uninstall").description("Remove an installed component").argument("[component]", "component name to remove (interactive if omitted)").action(async (component) => {
2163
2442
  const { removeCommand: removeCommand2 } = await Promise.resolve().then(() => (init_remove(), remove_exports));
2164
2443
  await removeCommand2(component);
2165
2444
  });