ai-builder 0.1.6 → 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 +163 -62
  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,13 +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
152
  const result = await response.json();
76
- logger.debug({ type, slug, cliVersion, status: response.status, result }, "Install tracked");
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
+ );
77
164
  } catch (error) {
78
165
  logger.debug({ err: error, type, slug }, "Install tracking failed (non-critical)");
79
166
  }
@@ -93,23 +180,23 @@ async function resolveStack(slug) {
93
180
  }
94
181
 
95
182
  // src/services/installer.ts
96
- import * as fs2 from "fs";
97
- import * as path2 from "path";
183
+ import * as fs3 from "fs";
184
+ import * as path3 from "path";
98
185
 
99
186
  // src/services/lock-file.ts
100
- import * as fs from "fs";
101
- import * as path from "path";
187
+ import * as fs2 from "fs";
188
+ import * as path2 from "path";
102
189
  var LOCK_FILE_NAME = "ai-builder.lock.json";
103
190
  function getLockFilePath() {
104
- return path.join(process.cwd(), ".claude", LOCK_FILE_NAME);
191
+ return path2.join(process.cwd(), ".claude", LOCK_FILE_NAME);
105
192
  }
106
193
  function readLockFile() {
107
194
  const lockPath = getLockFilePath();
108
- if (!fs.existsSync(lockPath)) {
195
+ if (!fs2.existsSync(lockPath)) {
109
196
  return { version: 1, artifacts: {} };
110
197
  }
111
198
  try {
112
- const content = fs.readFileSync(lockPath, "utf-8");
199
+ const content = fs2.readFileSync(lockPath, "utf-8");
113
200
  return JSON.parse(content);
114
201
  } catch {
115
202
  return { version: 1, artifacts: {} };
@@ -117,11 +204,11 @@ function readLockFile() {
117
204
  }
118
205
  function writeLockFile(lockFile) {
119
206
  const lockPath = getLockFilePath();
120
- const lockDir = path.dirname(lockPath);
121
- if (!fs.existsSync(lockDir)) {
122
- fs.mkdirSync(lockDir, { recursive: true });
207
+ const lockDir = path2.dirname(lockPath);
208
+ if (!fs2.existsSync(lockDir)) {
209
+ fs2.mkdirSync(lockDir, { recursive: true });
123
210
  }
124
- fs.writeFileSync(lockPath, `${JSON.stringify(lockFile, null, 2)}
211
+ fs2.writeFileSync(lockPath, `${JSON.stringify(lockFile, null, 2)}
125
212
  `);
126
213
  }
127
214
  function addToLockFile(entry) {
@@ -148,14 +235,14 @@ function isInstalled(type, author, slug) {
148
235
 
149
236
  // src/services/installer.ts
150
237
  function getInstallPath(type, artifactName) {
151
- const basePath = path2.join(process.cwd(), ".claude");
238
+ const basePath = path3.join(process.cwd(), ".claude");
152
239
  switch (type) {
153
240
  case "skill":
154
- return path2.join(basePath, "skills", artifactName);
241
+ return path3.join(basePath, "skills", artifactName);
155
242
  case "agent":
156
- return path2.join(basePath, "agents", `${artifactName}.md`);
243
+ return path3.join(basePath, "agents", `${artifactName}.md`);
157
244
  case "command":
158
- return path2.join(basePath, "commands", `${artifactName}.md`);
245
+ return path3.join(basePath, "commands", `${artifactName}.md`);
159
246
  default:
160
247
  throw new Error(`Unknown artifact type: ${type}`);
161
248
  }
@@ -167,24 +254,24 @@ function installArtifact(artifact, options = {}) {
167
254
  if (!options.force && isInstalled(artifact.type, artifact.author, artifactName || artifact.name)) {
168
255
  return { installed: false, files: [] };
169
256
  }
170
- const dir = artifact.type === "skill" ? installPath : path2.dirname(installPath);
171
- if (!fs2.existsSync(dir)) {
172
- 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 });
173
260
  }
174
261
  if (artifact.type === "skill") {
175
- const skillMdPath = path2.join(installPath, "SKILL.md");
176
- fs2.writeFileSync(skillMdPath, artifact.content);
262
+ const skillMdPath = path3.join(installPath, "SKILL.md");
263
+ fs3.writeFileSync(skillMdPath, artifact.content);
177
264
  files.push(skillMdPath);
178
265
  if (artifact.bundleFiles) {
179
266
  for (const [filename, content] of Object.entries(artifact.bundleFiles)) {
180
- const filePath = path2.join(installPath, filename);
181
- fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
182
- fs2.writeFileSync(filePath, content);
267
+ const filePath = path3.join(installPath, filename);
268
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
269
+ fs3.writeFileSync(filePath, content);
183
270
  files.push(filePath);
184
271
  }
185
272
  }
186
273
  } else {
187
- fs2.writeFileSync(installPath, artifact.content);
274
+ fs3.writeFileSync(installPath, artifact.content);
188
275
  files.push(installPath);
189
276
  }
190
277
  addToLockFile({
@@ -193,19 +280,19 @@ function installArtifact(artifact, options = {}) {
193
280
  author: artifact.author,
194
281
  name: artifact.name,
195
282
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
196
- files: files.map((f) => path2.relative(process.cwd(), f))
283
+ files: files.map((f) => path3.relative(process.cwd(), f))
197
284
  });
198
285
  return { installed: true, files };
199
286
  }
200
287
  function uninstallArtifact(type, author, slug) {
201
288
  const installPath = getInstallPath(type, slug);
202
- if (!fs2.existsSync(installPath)) {
289
+ if (!fs3.existsSync(installPath)) {
203
290
  return false;
204
291
  }
205
292
  if (type === "skill") {
206
- fs2.rmSync(installPath, { recursive: true, force: true });
293
+ fs3.rmSync(installPath, { recursive: true, force: true });
207
294
  } else {
208
- fs2.unlinkSync(installPath);
295
+ fs3.unlinkSync(installPath);
209
296
  }
210
297
  removeFromLockFile(type, author, slug);
211
298
  return true;
@@ -243,22 +330,21 @@ async function addCommand(type, slug, options) {
243
330
  return;
244
331
  }
245
332
  const [author, artifactSlug] = slug.split("/");
246
- if (!options.force && isInstalled(type, author, artifactSlug)) {
247
- console.log(chalk.yellow(`${type} ${chalk.bold(slug)} is already installed.`));
248
- console.log("Use --force to reinstall.");
249
- return;
250
- }
251
- 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();
252
336
  try {
253
337
  const artifact = await resolveArtifact(type, slug);
254
- spinner.text = `Installing ${artifact.name}...`;
255
- 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 });
256
341
  if (!installed) {
257
- spinner.warn(`${artifact.name} is already installed. Use --force to reinstall.`);
342
+ spinner.warn(`${artifact.name} could not be installed.`);
258
343
  return;
259
344
  }
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);
@@ -315,19 +401,26 @@ async function installStack(stackSlug, options) {
315
401
  }
316
402
  console.log();
317
403
  let installed = 0;
318
- let skipped = 0;
404
+ let updated = 0;
319
405
  for (const artifact of stack.artifacts) {
320
- 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();
321
410
  try {
322
411
  const resolved = await resolveArtifact(artifact.type, artifact.slug);
323
- const result = installArtifact(resolved, { force: options.force });
412
+ const result = installArtifact(resolved, { force: isArtifactUpdate || options.force });
324
413
  if (result.installed) {
325
- artifactSpinner.succeed(`Installed ${chalk.green(artifact.name)}`);
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
+ }
326
421
  await trackInstall(artifact.type, artifact.slug, CLI_VERSION);
327
- installed++;
328
422
  } else {
329
- artifactSpinner.info(`${artifact.name} already installed`);
330
- skipped++;
423
+ artifactSpinner.warn(`${artifact.name} could not be installed`);
331
424
  }
332
425
  } catch (error) {
333
426
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -335,8 +428,16 @@ async function installStack(stackSlug, options) {
335
428
  }
336
429
  }
337
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";
338
439
  console.log(
339
- `${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"}`
340
441
  );
341
442
  console.log();
342
443
  console.log(chalk.dim(`Learn more: https://aibuilder.sh/stacks/${stackSlug}`));
@@ -360,9 +461,9 @@ async function confirm(question) {
360
461
  }
361
462
 
362
463
  // src/commands/completion.ts
363
- import * as fs3 from "fs";
364
- import * as os from "os";
365
- import * as path3 from "path";
464
+ import * as fs4 from "fs";
465
+ import * as os3 from "os";
466
+ import * as path4 from "path";
366
467
  import chalk2 from "chalk";
367
468
  var BASH_COMPLETION = `
368
469
  # ai-builder bash completion
@@ -488,14 +589,14 @@ _ai_builder() {
488
589
  _ai_builder "$@"
489
590
  `.trim();
490
591
  function getShellConfigPath(shell) {
491
- const home = os.homedir();
592
+ const home = os3.homedir();
492
593
  if (shell === "bash") {
493
- const bashrc = path3.join(home, ".bashrc");
494
- const bashProfile = path3.join(home, ".bash_profile");
495
- 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;
496
597
  }
497
598
  if (shell === "zsh") {
498
- return path3.join(home, ".zshrc");
599
+ return path4.join(home, ".zshrc");
499
600
  }
500
601
  return null;
501
602
  }
@@ -523,14 +624,14 @@ function completionCommand(shell, options) {
523
624
  }
524
625
  const completion = detectedShell === "zsh" ? ZSH_COMPLETION : BASH_COMPLETION;
525
626
  const marker = "# ai-builder completion";
526
- const existingContent = fs3.existsSync(configPath) ? fs3.readFileSync(configPath, "utf-8") : "";
627
+ const existingContent = fs4.existsSync(configPath) ? fs4.readFileSync(configPath, "utf-8") : "";
527
628
  if (existingContent.includes(marker)) {
528
629
  console.log(chalk2.yellow(`
529
630
  Completion already installed in ${configPath}`));
530
631
  console.log(chalk2.dim(" Restart your terminal to apply changes\n"));
531
632
  return;
532
633
  }
533
- fs3.appendFileSync(configPath, `
634
+ fs4.appendFileSync(configPath, `
534
635
  ${completion}
535
636
  `);
536
637
  console.log(chalk2.green(`
@@ -766,8 +867,8 @@ function formatNumber(num) {
766
867
  }
767
868
 
768
869
  // src/commands/status.ts
769
- import * as fs4 from "fs";
770
- import * as path4 from "path";
870
+ import * as fs5 from "fs";
871
+ import * as path5 from "path";
771
872
  import chalk6 from "chalk";
772
873
 
773
874
  // src/version.ts
@@ -781,8 +882,8 @@ var API_BASE2 = process.env.AI_BUILDER_API_URL || "https://aibuilder.sh/api";
781
882
  async function statusCommand() {
782
883
  console.log(chalk6.cyan.bold("\n ai-builder status\n"));
783
884
  console.log(` ${chalk6.dim("Version:")} ${chalk6.white(version)}`);
784
- const claudeDir = path4.join(process.cwd(), ".claude");
785
- const claudeDirExists = fs4.existsSync(claudeDir);
885
+ const claudeDir = path5.join(process.cwd(), ".claude");
886
+ const claudeDirExists = fs5.existsSync(claudeDir);
786
887
  console.log(
787
888
  ` ${chalk6.dim("Claude dir:")} ${claudeDirExists ? chalk6.green(".claude/ found") : chalk6.yellow(".claude/ not found")}`
788
889
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-builder",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "CLI for installing Claude Code artifacts from aibuilder.sh",
5
5
  "type": "module",
6
6
  "bin": {