ai-builder 0.1.5 → 0.1.7

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.
Files changed (2) hide show
  1. package/dist/index.js +166 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,6 +9,82 @@ import * as readline from "readline";
9
9
  import chalk from "chalk";
10
10
  import ora from "ora";
11
11
 
12
+ // src/services/metadata.ts
13
+ import * as os2 from "os";
14
+
15
+ // src/services/anonymous-id.ts
16
+ import * as crypto from "crypto";
17
+ import * as fs from "fs";
18
+ import * as os from "os";
19
+ import * as path from "path";
20
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "ai-builder");
21
+ var ANONYMOUS_ID_FILE = path.join(CONFIG_DIR, "anonymous-id");
22
+ function isValidUuid(str) {
23
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
24
+ return uuidRegex.test(str);
25
+ }
26
+ function getAnonymousId() {
27
+ if (fs.existsSync(ANONYMOUS_ID_FILE)) {
28
+ try {
29
+ const id = fs.readFileSync(ANONYMOUS_ID_FILE, "utf-8").trim();
30
+ if (isValidUuid(id)) {
31
+ return id;
32
+ }
33
+ } catch {
34
+ }
35
+ }
36
+ const newId = crypto.randomUUID();
37
+ try {
38
+ if (!fs.existsSync(CONFIG_DIR)) {
39
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
40
+ }
41
+ fs.writeFileSync(ANONYMOUS_ID_FILE, `${newId}
42
+ `, { mode: 384 });
43
+ } catch {
44
+ }
45
+ return newId;
46
+ }
47
+
48
+ // src/services/metadata.ts
49
+ function getShell() {
50
+ const shell = process.env.SHELL || process.env.COMSPEC;
51
+ if (!shell) return null;
52
+ const parts = shell.split(/[/\\]/);
53
+ return parts[parts.length - 1] || null;
54
+ }
55
+ function getTerminal() {
56
+ return process.env.TERM_PROGRAM || // iTerm.app, Apple_Terminal, vscode
57
+ process.env.TERMINAL_EMULATOR || // Some Linux terminals
58
+ (process.env.WT_SESSION ? "windows-terminal" : null);
59
+ }
60
+ function detectCi() {
61
+ return !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.BUILD_NUMBER || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.JENKINS_URL);
62
+ }
63
+ function getLocale() {
64
+ return process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE || null;
65
+ }
66
+ function getTimezone() {
67
+ try {
68
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
69
+ } catch {
70
+ return process.env.TZ || null;
71
+ }
72
+ }
73
+ function collectMetadata() {
74
+ return {
75
+ anonymousId: getAnonymousId(),
76
+ osType: os2.platform(),
77
+ osVersion: os2.release(),
78
+ osArch: os2.arch(),
79
+ nodeVersion: process.version,
80
+ shell: getShell(),
81
+ terminal: getTerminal(),
82
+ isCi: detectCi(),
83
+ locale: getLocale(),
84
+ timezone: getTimezone()
85
+ };
86
+ }
87
+
12
88
  // src/services/api.ts
13
89
  var API_BASE = process.env.AI_BUILDER_API_URL || "https://aibuilder.sh/api";
14
90
  var isDebug = process.env.DEBUG?.includes("ai-builder");
@@ -67,12 +143,24 @@ async function searchArtifacts(options) {
67
143
  }
68
144
  async function trackInstall(type, slug, cliVersion) {
69
145
  try {
146
+ const metadata = collectMetadata();
70
147
  const response = await fetch(`${API_BASE}/install-events`, {
71
148
  method: "POST",
72
149
  headers: { "Content-Type": "application/json" },
73
- body: JSON.stringify({ type, slug, cliVersion })
150
+ body: JSON.stringify({ type, slug, cliVersion, metadata })
74
151
  });
75
- logger.debug({ type, slug, cliVersion, status: response.status }, "Install tracked");
152
+ const result = await response.json();
153
+ logger.debug(
154
+ {
155
+ type,
156
+ slug,
157
+ cliVersion,
158
+ anonymousId: metadata.anonymousId,
159
+ status: response.status,
160
+ result
161
+ },
162
+ "Install tracked"
163
+ );
76
164
  } catch (error) {
77
165
  logger.debug({ err: error, type, slug }, "Install tracking failed (non-critical)");
78
166
  }
@@ -92,23 +180,23 @@ async function resolveStack(slug) {
92
180
  }
93
181
 
94
182
  // src/services/installer.ts
95
- import * as fs2 from "fs";
96
- import * as path2 from "path";
183
+ import * as fs3 from "fs";
184
+ import * as path3 from "path";
97
185
 
98
186
  // src/services/lock-file.ts
99
- import * as fs from "fs";
100
- import * as path from "path";
187
+ import * as fs2 from "fs";
188
+ import * as path2 from "path";
101
189
  var LOCK_FILE_NAME = "ai-builder.lock.json";
102
190
  function getLockFilePath() {
103
- return path.join(process.cwd(), ".claude", LOCK_FILE_NAME);
191
+ return path2.join(process.cwd(), ".claude", LOCK_FILE_NAME);
104
192
  }
105
193
  function readLockFile() {
106
194
  const lockPath = getLockFilePath();
107
- if (!fs.existsSync(lockPath)) {
195
+ if (!fs2.existsSync(lockPath)) {
108
196
  return { version: 1, artifacts: {} };
109
197
  }
110
198
  try {
111
- const content = fs.readFileSync(lockPath, "utf-8");
199
+ const content = fs2.readFileSync(lockPath, "utf-8");
112
200
  return JSON.parse(content);
113
201
  } catch {
114
202
  return { version: 1, artifacts: {} };
@@ -116,11 +204,11 @@ function readLockFile() {
116
204
  }
117
205
  function writeLockFile(lockFile) {
118
206
  const lockPath = getLockFilePath();
119
- const lockDir = path.dirname(lockPath);
120
- if (!fs.existsSync(lockDir)) {
121
- fs.mkdirSync(lockDir, { recursive: true });
207
+ const lockDir = path2.dirname(lockPath);
208
+ if (!fs2.existsSync(lockDir)) {
209
+ fs2.mkdirSync(lockDir, { recursive: true });
122
210
  }
123
- fs.writeFileSync(lockPath, `${JSON.stringify(lockFile, null, 2)}
211
+ fs2.writeFileSync(lockPath, `${JSON.stringify(lockFile, null, 2)}
124
212
  `);
125
213
  }
126
214
  function addToLockFile(entry) {
@@ -147,14 +235,14 @@ function isInstalled(type, author, slug) {
147
235
 
148
236
  // src/services/installer.ts
149
237
  function getInstallPath(type, artifactName) {
150
- const basePath = path2.join(process.cwd(), ".claude");
238
+ const basePath = path3.join(process.cwd(), ".claude");
151
239
  switch (type) {
152
240
  case "skill":
153
- return path2.join(basePath, "skills", artifactName);
241
+ return path3.join(basePath, "skills", artifactName);
154
242
  case "agent":
155
- return path2.join(basePath, "agents", `${artifactName}.md`);
243
+ return path3.join(basePath, "agents", `${artifactName}.md`);
156
244
  case "command":
157
- return path2.join(basePath, "commands", `${artifactName}.md`);
245
+ return path3.join(basePath, "commands", `${artifactName}.md`);
158
246
  default:
159
247
  throw new Error(`Unknown artifact type: ${type}`);
160
248
  }
@@ -166,24 +254,24 @@ function installArtifact(artifact, options = {}) {
166
254
  if (!options.force && isInstalled(artifact.type, artifact.author, artifactName || artifact.name)) {
167
255
  return { installed: false, files: [] };
168
256
  }
169
- const dir = artifact.type === "skill" ? installPath : path2.dirname(installPath);
170
- if (!fs2.existsSync(dir)) {
171
- fs2.mkdirSync(dir, { recursive: true });
257
+ const dir = artifact.type === "skill" ? installPath : path3.dirname(installPath);
258
+ if (!fs3.existsSync(dir)) {
259
+ fs3.mkdirSync(dir, { recursive: true });
172
260
  }
173
261
  if (artifact.type === "skill") {
174
- const skillMdPath = path2.join(installPath, "SKILL.md");
175
- fs2.writeFileSync(skillMdPath, artifact.content);
262
+ const skillMdPath = path3.join(installPath, "SKILL.md");
263
+ fs3.writeFileSync(skillMdPath, artifact.content);
176
264
  files.push(skillMdPath);
177
265
  if (artifact.bundleFiles) {
178
266
  for (const [filename, content] of Object.entries(artifact.bundleFiles)) {
179
- const filePath = path2.join(installPath, filename);
180
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
181
- fs2.writeFileSync(filePath, content);
267
+ const filePath = path3.join(installPath, filename);
268
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
269
+ fs3.writeFileSync(filePath, content);
182
270
  files.push(filePath);
183
271
  }
184
272
  }
185
273
  } else {
186
- fs2.writeFileSync(installPath, artifact.content);
274
+ fs3.writeFileSync(installPath, artifact.content);
187
275
  files.push(installPath);
188
276
  }
189
277
  addToLockFile({
@@ -192,19 +280,19 @@ function installArtifact(artifact, options = {}) {
192
280
  author: artifact.author,
193
281
  name: artifact.name,
194
282
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
195
- files: files.map((f) => path2.relative(process.cwd(), f))
283
+ files: files.map((f) => path3.relative(process.cwd(), f))
196
284
  });
197
285
  return { installed: true, files };
198
286
  }
199
287
  function uninstallArtifact(type, author, slug) {
200
288
  const installPath = getInstallPath(type, slug);
201
- if (!fs2.existsSync(installPath)) {
289
+ if (!fs3.existsSync(installPath)) {
202
290
  return false;
203
291
  }
204
292
  if (type === "skill") {
205
- fs2.rmSync(installPath, { recursive: true, force: true });
293
+ fs3.rmSync(installPath, { recursive: true, force: true });
206
294
  } else {
207
- fs2.unlinkSync(installPath);
295
+ fs3.unlinkSync(installPath);
208
296
  }
209
297
  removeFromLockFile(type, author, slug);
210
298
  return true;
@@ -242,26 +330,25 @@ async function addCommand(type, slug, options) {
242
330
  return;
243
331
  }
244
332
  const [author, artifactSlug] = slug.split("/");
245
- if (!options.force && isInstalled(type, author, artifactSlug)) {
246
- console.log(chalk.yellow(`${type} ${chalk.bold(slug)} is already installed.`));
247
- console.log("Use --force to reinstall.");
248
- return;
249
- }
250
- const spinner = ora(`Resolving ${type} ${chalk.cyan(slug)}...`).start();
333
+ const isUpdate = isInstalled(type, author, artifactSlug);
334
+ const actionVerb = isUpdate ? "Updating" : "Resolving";
335
+ const spinner = ora(`${actionVerb} ${type} ${chalk.cyan(slug)}...`).start();
251
336
  try {
252
337
  const artifact = await resolveArtifact(type, slug);
253
- spinner.text = `Installing ${artifact.name}...`;
254
- const { installed } = installArtifact(artifact, { force: options.force });
338
+ const installVerb = isUpdate ? "Updating" : "Installing";
339
+ spinner.text = `${installVerb} ${artifact.name}...`;
340
+ const { installed } = installArtifact(artifact, { force: isUpdate || options.force });
255
341
  if (!installed) {
256
- spinner.warn(`${artifact.name} is already installed. Use --force to reinstall.`);
342
+ spinner.warn(`${artifact.name} could not be installed.`);
257
343
  return;
258
344
  }
259
- trackInstall(type, slug, CLI_VERSION);
345
+ const successVerb = isUpdate ? "Updated" : "Installed";
260
346
  spinner.succeed(
261
- `Installed ${chalk.green(artifact.name)} to ${chalk.dim(artifact.installPath)}`
347
+ `${successVerb} ${chalk.green(artifact.name)} to ${chalk.dim(artifact.installPath)}`
262
348
  );
263
349
  console.log();
264
350
  printUsageHint(type, artifactSlug);
351
+ await trackInstall(type, slug, CLI_VERSION);
265
352
  } catch (error) {
266
353
  const message = error instanceof Error ? error.message : "Unknown error";
267
354
  spinner.fail(chalk.red(`Failed to install: ${message}`));
@@ -314,19 +401,26 @@ async function installStack(stackSlug, options) {
314
401
  }
315
402
  console.log();
316
403
  let installed = 0;
317
- let skipped = 0;
404
+ let updated = 0;
318
405
  for (const artifact of stack.artifacts) {
319
- const artifactSpinner = ora(`Installing ${artifact.name}...`).start();
406
+ const [artifactAuthor, artifactSlug] = artifact.slug.split("/");
407
+ const isArtifactUpdate = isInstalled(artifact.type, artifactAuthor, artifactSlug);
408
+ const actionVerb = isArtifactUpdate ? "Updating" : "Installing";
409
+ const artifactSpinner = ora(`${actionVerb} ${artifact.name}...`).start();
320
410
  try {
321
411
  const resolved = await resolveArtifact(artifact.type, artifact.slug);
322
- const result = installArtifact(resolved, { force: options.force });
412
+ const result = installArtifact(resolved, { force: isArtifactUpdate || options.force });
323
413
  if (result.installed) {
324
- artifactSpinner.succeed(`Installed ${chalk.green(artifact.name)}`);
325
- trackInstall(artifact.type, artifact.slug, CLI_VERSION);
326
- installed++;
414
+ if (isArtifactUpdate) {
415
+ artifactSpinner.succeed(`Updated ${chalk.green(artifact.name)}`);
416
+ updated++;
417
+ } else {
418
+ artifactSpinner.succeed(`Installed ${chalk.green(artifact.name)}`);
419
+ installed++;
420
+ }
421
+ await trackInstall(artifact.type, artifact.slug, CLI_VERSION);
327
422
  } else {
328
- artifactSpinner.info(`${artifact.name} already installed`);
329
- skipped++;
423
+ artifactSpinner.warn(`${artifact.name} could not be installed`);
330
424
  }
331
425
  } catch (error) {
332
426
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -334,8 +428,16 @@ async function installStack(stackSlug, options) {
334
428
  }
335
429
  }
336
430
  console.log();
431
+ const parts = [];
432
+ if (installed > 0) {
433
+ parts.push(`installed ${installed}`);
434
+ }
435
+ if (updated > 0) {
436
+ parts.push(`updated ${updated}`);
437
+ }
438
+ const summary = parts.length > 0 ? parts.join(", ") : "no changes";
337
439
  console.log(
338
- `${chalk.green("Done!")} Installed ${installed} artifact${installed === 1 ? "" : "s"}${skipped > 0 ? `, skipped ${skipped}` : ""}`
440
+ `${chalk.green("Done!")} ${summary.charAt(0).toUpperCase() + summary.slice(1)} artifact${installed + updated === 1 ? "" : "s"}`
339
441
  );
340
442
  console.log();
341
443
  console.log(chalk.dim(`Learn more: https://aibuilder.sh/stacks/${stackSlug}`));
@@ -359,9 +461,9 @@ async function confirm(question) {
359
461
  }
360
462
 
361
463
  // src/commands/completion.ts
362
- import * as fs3 from "fs";
363
- import * as os from "os";
364
- import * as path3 from "path";
464
+ import * as fs4 from "fs";
465
+ import * as os3 from "os";
466
+ import * as path4 from "path";
365
467
  import chalk2 from "chalk";
366
468
  var BASH_COMPLETION = `
367
469
  # ai-builder bash completion
@@ -487,14 +589,14 @@ _ai_builder() {
487
589
  _ai_builder "$@"
488
590
  `.trim();
489
591
  function getShellConfigPath(shell) {
490
- const home = os.homedir();
592
+ const home = os3.homedir();
491
593
  if (shell === "bash") {
492
- const bashrc = path3.join(home, ".bashrc");
493
- const bashProfile = path3.join(home, ".bash_profile");
494
- return fs3.existsSync(bashrc) ? bashrc : bashProfile;
594
+ const bashrc = path4.join(home, ".bashrc");
595
+ const bashProfile = path4.join(home, ".bash_profile");
596
+ return fs4.existsSync(bashrc) ? bashrc : bashProfile;
495
597
  }
496
598
  if (shell === "zsh") {
497
- return path3.join(home, ".zshrc");
599
+ return path4.join(home, ".zshrc");
498
600
  }
499
601
  return null;
500
602
  }
@@ -522,14 +624,14 @@ function completionCommand(shell, options) {
522
624
  }
523
625
  const completion = detectedShell === "zsh" ? ZSH_COMPLETION : BASH_COMPLETION;
524
626
  const marker = "# ai-builder completion";
525
- const existingContent = fs3.existsSync(configPath) ? fs3.readFileSync(configPath, "utf-8") : "";
627
+ const existingContent = fs4.existsSync(configPath) ? fs4.readFileSync(configPath, "utf-8") : "";
526
628
  if (existingContent.includes(marker)) {
527
629
  console.log(chalk2.yellow(`
528
630
  Completion already installed in ${configPath}`));
529
631
  console.log(chalk2.dim(" Restart your terminal to apply changes\n"));
530
632
  return;
531
633
  }
532
- fs3.appendFileSync(configPath, `
634
+ fs4.appendFileSync(configPath, `
533
635
  ${completion}
534
636
  `);
535
637
  console.log(chalk2.green(`
@@ -765,8 +867,8 @@ function formatNumber(num) {
765
867
  }
766
868
 
767
869
  // src/commands/status.ts
768
- import * as fs4 from "fs";
769
- import * as path4 from "path";
870
+ import * as fs5 from "fs";
871
+ import * as path5 from "path";
770
872
  import chalk6 from "chalk";
771
873
 
772
874
  // src/version.ts
@@ -780,8 +882,8 @@ var API_BASE2 = process.env.AI_BUILDER_API_URL || "https://aibuilder.sh/api";
780
882
  async function statusCommand() {
781
883
  console.log(chalk6.cyan.bold("\n ai-builder status\n"));
782
884
  console.log(` ${chalk6.dim("Version:")} ${chalk6.white(version)}`);
783
- const claudeDir = path4.join(process.cwd(), ".claude");
784
- const claudeDirExists = fs4.existsSync(claudeDir);
885
+ const claudeDir = path5.join(process.cwd(), ".claude");
886
+ const claudeDirExists = fs5.existsSync(claudeDir);
785
887
  console.log(
786
888
  ` ${chalk6.dim("Claude dir:")} ${claudeDirExists ? chalk6.green(".claude/ found") : chalk6.yellow(".claude/ not found")}`
787
889
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-builder",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI for installing Claude Code artifacts from aibuilder.sh",
5
5
  "type": "module",
6
6
  "bin": {