@senso-ai/shipables 0.1.0 → 0.1.1

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
@@ -101,6 +101,72 @@ var init_config = __esm({
101
101
  }
102
102
  });
103
103
 
104
+ // src/lib/errors.ts
105
+ var errors_exports = {};
106
+ __export(errors_exports, {
107
+ CliError: () => CliError,
108
+ EXIT_AUTH: () => EXIT_AUTH,
109
+ EXIT_CONFLICT: () => EXIT_CONFLICT,
110
+ EXIT_ERROR: () => EXIT_ERROR,
111
+ EXIT_NETWORK: () => EXIT_NETWORK,
112
+ EXIT_NOT_FOUND: () => EXIT_NOT_FOUND,
113
+ EXIT_SUCCESS: () => EXIT_SUCCESS,
114
+ EXIT_USAGE: () => EXIT_USAGE,
115
+ EXIT_VALIDATION: () => EXIT_VALIDATION,
116
+ authError: () => authError,
117
+ conflictError: () => conflictError,
118
+ networkError: () => networkError,
119
+ notFoundError: () => notFoundError,
120
+ usageError: () => usageError,
121
+ validationError: () => validationError
122
+ });
123
+ function authError(message, fix) {
124
+ return new CliError(message, "AUTH_REQUIRED", EXIT_AUTH, fix || "Run `shipables login` to authenticate.");
125
+ }
126
+ function notFoundError(message, fix) {
127
+ return new CliError(message, "NOT_FOUND", EXIT_NOT_FOUND, fix);
128
+ }
129
+ function networkError(message, fix) {
130
+ return new CliError(
131
+ message,
132
+ "NETWORK_ERROR",
133
+ EXIT_NETWORK,
134
+ fix || "Check your internet connection or registry URL with: shipables config get registry"
135
+ );
136
+ }
137
+ function validationError(message, fix) {
138
+ return new CliError(message, "VALIDATION_ERROR", EXIT_VALIDATION, fix);
139
+ }
140
+ function usageError(message, fix) {
141
+ return new CliError(message, "USAGE_ERROR", EXIT_USAGE, fix);
142
+ }
143
+ function conflictError(message, fix) {
144
+ return new CliError(message, "CONFLICT", EXIT_CONFLICT, fix);
145
+ }
146
+ var EXIT_SUCCESS, EXIT_ERROR, EXIT_USAGE, EXIT_AUTH, EXIT_NOT_FOUND, EXIT_NETWORK, EXIT_VALIDATION, EXIT_CONFLICT, CliError;
147
+ var init_errors = __esm({
148
+ "src/lib/errors.ts"() {
149
+ "use strict";
150
+ EXIT_SUCCESS = 0;
151
+ EXIT_ERROR = 1;
152
+ EXIT_USAGE = 2;
153
+ EXIT_AUTH = 3;
154
+ EXIT_NOT_FOUND = 4;
155
+ EXIT_NETWORK = 5;
156
+ EXIT_VALIDATION = 6;
157
+ EXIT_CONFLICT = 7;
158
+ CliError = class extends Error {
159
+ constructor(message, code, exitCode, fix) {
160
+ super(message);
161
+ this.code = code;
162
+ this.exitCode = exitCode;
163
+ this.fix = fix;
164
+ this.name = "CliError";
165
+ }
166
+ };
167
+ }
168
+ });
169
+
104
170
  // src/registry/client.ts
105
171
  import { createReadStream } from "fs";
106
172
  import { readFile as readFile2 } from "fs/promises";
@@ -122,6 +188,7 @@ var init_client = __esm({
122
188
  "src/registry/client.ts"() {
123
189
  "use strict";
124
190
  init_config();
191
+ init_errors();
125
192
  cachedVersion = null;
126
193
  RegistryClient = class {
127
194
  registryUrl = null;
@@ -165,7 +232,7 @@ var init_client = __esm({
165
232
  return await fetch(url, init);
166
233
  } catch (err) {
167
234
  if (err instanceof TypeError && err.message === "fetch failed") {
168
- throw new Error(
235
+ throw networkError(
169
236
  `Could not connect to registry at ${base}. Check your internet connection or registry URL.`
170
237
  );
171
238
  }
@@ -186,7 +253,9 @@ var init_client = __esm({
186
253
  headers: await this.getHeaders()
187
254
  });
188
255
  if (!resp.ok) {
189
- throw new Error(`Search failed: ${resp.status} ${resp.statusText}`);
256
+ throw networkError(
257
+ `Search for '${params.q || ""}' failed (HTTP ${resp.status}). Verify your registry URL with: shipables config get registry`
258
+ );
190
259
  }
191
260
  return await resp.json();
192
261
  }
@@ -198,10 +267,13 @@ var init_client = __esm({
198
267
  });
199
268
  if (!resp.ok) {
200
269
  if (resp.status === 404) {
201
- throw new Error(`Skill not found: ${fullName}`);
270
+ throw notFoundError(
271
+ `Skill '${fullName}' not found in the registry.`,
272
+ `Search for available skills with: shipables search <query>`
273
+ );
202
274
  }
203
- throw new Error(
204
- `Failed to get skill detail: ${resp.status} ${resp.statusText}`
275
+ throw networkError(
276
+ `Failed to get skill detail for '${fullName}' (HTTP ${resp.status}).`
205
277
  );
206
278
  }
207
279
  return await resp.json();
@@ -215,10 +287,13 @@ var init_client = __esm({
215
287
  );
216
288
  if (!resp.ok) {
217
289
  if (resp.status === 404) {
218
- throw new Error(`Version not found: ${fullName}@${version}`);
290
+ throw notFoundError(
291
+ `Version '${fullName}@${version}' not found.`,
292
+ `List available versions with: shipables info ${fullName}`
293
+ );
219
294
  }
220
- throw new Error(
221
- `Failed to get version: ${resp.status} ${resp.statusText}`
295
+ throw networkError(
296
+ `Failed to get version '${fullName}@${version}' (HTTP ${resp.status}).`
222
297
  );
223
298
  }
224
299
  return await resp.json();
@@ -231,8 +306,9 @@ var init_client = __esm({
231
306
  { headers: await this.getHeaders() }
232
307
  );
233
308
  if (!resp.ok) {
234
- throw new Error(
235
- `Failed to download tarball: ${resp.status} ${resp.statusText}`
309
+ throw networkError(
310
+ `Failed to download tarball '${filename}' (HTTP ${resp.status}).`,
311
+ `Re-run the install command. If the error persists, the package may have been removed.`
236
312
  );
237
313
  }
238
314
  const integrity = resp.headers.get("X-Integrity") || "";
@@ -243,8 +319,9 @@ var init_client = __esm({
243
319
  const base = await this.getBaseUrl();
244
320
  const headers = await this.getHeaders(true);
245
321
  if (!headers["Authorization"]) {
246
- throw new Error(
247
- "Not authenticated. Run `shipables login` first."
322
+ throw authError(
323
+ "Not authenticated. You must be logged in to publish.",
324
+ "Run `shipables login` to authenticate, then re-run `shipables publish`."
248
325
  );
249
326
  }
250
327
  const fileStream = createReadStream(tarballPath);
@@ -265,13 +342,25 @@ var init_client = __esm({
265
342
  });
266
343
  if (!resp.ok) {
267
344
  const body = await resp.text();
268
- let message = `Publish failed: ${resp.status} ${resp.statusText}`;
345
+ let message = `Publish failed (HTTP ${resp.status}).`;
346
+ let details = "";
269
347
  try {
270
348
  const err = JSON.parse(body);
271
349
  if (err.error?.message) message = err.error.message;
350
+ if (err.error?.details) details = JSON.stringify(err.error.details);
272
351
  } catch {
273
352
  }
274
- throw new Error(message);
353
+ if (resp.status === 401) {
354
+ throw authError(message, "Run `shipables login` to re-authenticate, then re-run `shipables publish`.");
355
+ }
356
+ if (resp.status === 409) {
357
+ const { conflictError: conflictError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
358
+ throw conflictError2(
359
+ message + (details ? ` Details: ${details}` : ""),
360
+ "Bump the version in shipables.json and re-run `shipables publish`."
361
+ );
362
+ }
363
+ throw new Error(message + (details ? ` Details: ${details}` : ""));
275
364
  }
276
365
  return await resp.json();
277
366
  }
@@ -287,15 +376,21 @@ var init_client = __esm({
287
376
  );
288
377
  if (!resp.ok) {
289
378
  if (resp.status === 404) {
290
- throw new Error(`Version not found: ${fullName}@${version}`);
379
+ throw notFoundError(
380
+ `Version '${fullName}@${version}' not found.`,
381
+ `Check available versions with: shipables info ${fullName}`
382
+ );
291
383
  }
292
384
  const body = await resp.text();
293
- let message = `Unpublish failed: ${resp.status} ${resp.statusText}`;
385
+ let message = `Unpublish failed (HTTP ${resp.status}).`;
294
386
  try {
295
387
  const err = JSON.parse(body);
296
388
  if (err.error?.message) message = err.error.message;
297
389
  } catch {
298
390
  }
391
+ if (resp.status === 401) {
392
+ throw authError(message, "Run `shipables login` to re-authenticate.");
393
+ }
299
394
  throw new Error(message);
300
395
  }
301
396
  }
@@ -308,10 +403,13 @@ var init_client = __esm({
308
403
  );
309
404
  if (!resp.ok) {
310
405
  if (resp.status === 404) {
311
- throw new Error(`Skill not found: ${fullName}`);
406
+ throw notFoundError(
407
+ `Skill '${fullName}' not found.`,
408
+ `Search for available skills with: shipables search <query>`
409
+ );
312
410
  }
313
- throw new Error(
314
- `Failed to get download stats: ${resp.status} ${resp.statusText}`
411
+ throw networkError(
412
+ `Failed to get download stats for '${fullName}' (HTTP ${resp.status}).`
315
413
  );
316
414
  }
317
415
  return await resp.json();
@@ -324,10 +422,13 @@ var init_client = __esm({
324
422
  );
325
423
  if (!resp.ok) {
326
424
  if (resp.status === 404) {
327
- throw new Error(`User not found: ${username}`);
425
+ throw notFoundError(
426
+ `User '${username}' not found.`,
427
+ `Check the username and try again.`
428
+ );
328
429
  }
329
- throw new Error(
330
- `Failed to get user profile: ${resp.status} ${resp.statusText}`
430
+ throw networkError(
431
+ `Failed to get user profile for '${username}' (HTTP ${resp.status}).`
331
432
  );
332
433
  }
333
434
  return await resp.json();
@@ -339,8 +440,8 @@ var init_client = __esm({
339
440
  { headers: await this.getHeaders() }
340
441
  );
341
442
  if (!resp.ok) {
342
- throw new Error(
343
- `Failed to get user skills: ${resp.status} ${resp.statusText}`
443
+ throw networkError(
444
+ `Failed to get skills for user '${username}' (HTTP ${resp.status}).`
344
445
  );
345
446
  }
346
447
  return await resp.json();
@@ -352,9 +453,12 @@ var init_client = __esm({
352
453
  });
353
454
  if (!resp.ok) {
354
455
  if (resp.status === 401) {
355
- throw new Error("Not authenticated. Run `shipables login` first.");
456
+ throw authError(
457
+ "Not authenticated or session expired.",
458
+ "Run `shipables login` to re-authenticate."
459
+ );
356
460
  }
357
- throw new Error(`Failed to get profile: ${resp.status}`);
461
+ throw networkError(`Failed to get profile (HTTP ${resp.status}).`);
358
462
  }
359
463
  return await resp.json();
360
464
  }
@@ -825,27 +929,49 @@ var init_config2 = __esm({
825
929
 
826
930
  // src/lib/output.ts
827
931
  import chalk from "chalk";
932
+ import ora from "ora";
933
+ function isInteractive() {
934
+ return process.stdin.isTTY === true && !process.env.CI;
935
+ }
936
+ function setJsonMode(enabled) {
937
+ jsonMode = enabled;
938
+ }
939
+ function getJsonMode() {
940
+ return jsonMode;
941
+ }
828
942
  function success(message) {
943
+ if (jsonMode) return;
829
944
  console.log(chalk.green(" \u2713 ") + message);
830
945
  }
831
946
  function error(message) {
947
+ if (jsonMode) return;
948
+ console.error(chalk.red(" \u2717 ") + message);
949
+ }
950
+ function errorWithFix(message, fix) {
951
+ if (jsonMode) return;
832
952
  console.error(chalk.red(" \u2717 ") + message);
953
+ console.error(chalk.dim(" Fix: ") + fix);
833
954
  }
834
955
  function warn(message) {
956
+ if (jsonMode) return;
835
957
  console.log(chalk.yellow(" \u26A0 ") + message);
836
958
  }
837
959
  function info(message) {
960
+ if (jsonMode) return;
838
961
  console.log(chalk.cyan(" ") + message);
839
962
  }
840
963
  function blank() {
964
+ if (jsonMode) return;
841
965
  console.log();
842
966
  }
843
967
  function header(message) {
968
+ if (jsonMode) return;
844
969
  console.log();
845
970
  console.log(chalk.bold(" " + message));
846
971
  console.log();
847
972
  }
848
973
  function table(headers, rows, columnWidths) {
974
+ if (jsonMode) return;
849
975
  const widths = columnWidths || headers.map(
850
976
  (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length))
851
977
  );
@@ -864,9 +990,47 @@ function formatBytes(bytes) {
864
990
  function formatNumber(n) {
865
991
  return n.toLocaleString("en-US");
866
992
  }
993
+ function spinner(text) {
994
+ if (process.stdin.isTTY && !process.env.CI && !jsonMode) {
995
+ return ora(text).start();
996
+ }
997
+ if (!jsonMode) {
998
+ console.error(` \u2026 ${text}`);
999
+ }
1000
+ return {
1001
+ succeed(msg) {
1002
+ if (!jsonMode && msg) console.error(` \u2713 ${msg}`);
1003
+ return this;
1004
+ },
1005
+ fail(msg) {
1006
+ if (!jsonMode && msg) console.error(` \u2717 ${msg}`);
1007
+ return this;
1008
+ },
1009
+ stop() {
1010
+ return this;
1011
+ },
1012
+ start(msg) {
1013
+ if (!jsonMode && msg) console.error(` \u2026 ${msg}`);
1014
+ return this;
1015
+ },
1016
+ warn(msg) {
1017
+ if (!jsonMode && msg) console.error(` \u26A0 ${msg}`);
1018
+ return this;
1019
+ },
1020
+ info(msg) {
1021
+ if (!jsonMode && msg) console.error(` \u2139 ${msg}`);
1022
+ return this;
1023
+ },
1024
+ text,
1025
+ isSpinning: false
1026
+ };
1027
+ }
1028
+ var jsonMode;
867
1029
  var init_output = __esm({
868
1030
  "src/lib/output.ts"() {
869
1031
  "use strict";
1032
+ init_errors();
1033
+ jsonMode = false;
870
1034
  }
871
1035
  });
872
1036
 
@@ -879,7 +1043,6 @@ import { cp, mkdir as mkdir3 } from "fs/promises";
879
1043
  import { join as join5, resolve } from "path";
880
1044
  import { Command } from "commander";
881
1045
  import chalk2 from "chalk";
882
- import ora from "ora";
883
1046
  import { checkbox } from "@inquirer/prompts";
884
1047
  import { input, password } from "@inquirer/prompts";
885
1048
  import { writeFile as writeFile4 } from "fs/promises";
@@ -909,13 +1072,24 @@ function getBareName(fullName) {
909
1072
  return fullName;
910
1073
  }
911
1074
  function createInstallCommand() {
912
- return new Command("install").argument("<skill>", "Skill name, optionally with version (e.g., neo4j, neo4j@1.2.0)").option("--claude", "Install for Claude Code").option("--cursor", "Install for Cursor").option("--codex", "Install for Codex CLI").option("--copilot", "Install for VS Code / Copilot").option("--gemini", "Install for Gemini CLI").option("--cline", "Install for Cline").option("--all", "Install for all detected agents").option("-g, --global", "Install to user-level skills directory").option("-y, --yes", "Skip confirmation prompts").option("--no-mcp", "Skip MCP server configuration").option("--registry <url>", "Use a custom registry URL").description("Install a skill from the registry").action(async (skillArg, options) => {
1075
+ return new Command("install").argument("<skill>", "Skill name, optionally with version (e.g., Chippers255/ai-commits, Chippers255/ai-commits@1.0.0)").option("--claude", "Install for Claude Code").option("--cursor", "Install for Cursor").option("--codex", "Install for Codex CLI").option("--copilot", "Install for VS Code / Copilot").option("--gemini", "Install for Gemini CLI").option("--cline", "Install for Cline").option("--all", "Install for all detected agents").option("-g, --global", "Install to user-level skills directory").option("-y, --yes", "Skip confirmation prompts").option("--no-mcp", "Skip MCP server configuration").option("--env <values...>", "Set MCP environment variables (e.g., --env API_KEY=xxx DB_HOST=localhost)").option("--registry <url>", "Use a custom registry URL").description(
1076
+ "Install a skill from the registry into the current project. Downloads the skill package, extracts it to agent-specific directories, and configures MCP servers if the skill declares them."
1077
+ ).addHelpText("after", `
1078
+ Examples:
1079
+ shipables install neo4j --claude Install latest version for Claude Code
1080
+ shipables install neo4j@1.2.0 --all Install specific version for all agents
1081
+ shipables install @myorg/tool --cursor -y Install scoped skill, skip prompts
1082
+ shipables install neo4j --claude --no-mcp Install without MCP configuration
1083
+ shipables install neo4j --claude --env API_KEY=xxx Pass MCP env vars
1084
+ `).action(async (skillArg, options) => {
913
1085
  try {
914
1086
  await runInstall(skillArg, options);
915
1087
  } catch (err) {
916
- error(
917
- err instanceof Error ? err.message : String(err)
918
- );
1088
+ if (err instanceof CliError) {
1089
+ errorWithFix(err.message, err.fix || "");
1090
+ process.exit(err.exitCode);
1091
+ }
1092
+ error(err instanceof Error ? err.message : String(err));
919
1093
  process.exit(1);
920
1094
  }
921
1095
  });
@@ -924,21 +1098,30 @@ async function runInstall(skillArg, options) {
924
1098
  const { name: skillName, version: requestedVersion } = parseSkillArg(skillArg);
925
1099
  const bareName = getBareName(skillName);
926
1100
  const scope = options.global ? "global" : "project";
927
- const spinner = ora(`Resolving ${skillName}@${requestedVersion}...`).start();
1101
+ const envMap = {};
1102
+ if (options.env) {
1103
+ for (const pair of options.env) {
1104
+ const eqIdx = pair.indexOf("=");
1105
+ if (eqIdx > 0) {
1106
+ envMap[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
1107
+ }
1108
+ }
1109
+ }
1110
+ const resolveSpinner = spinner(`Resolving ${skillName}@${requestedVersion}...`);
928
1111
  let versionDetail;
929
1112
  try {
930
1113
  versionDetail = await registry.getVersionDetail(skillName, requestedVersion);
931
- spinner.succeed(
1114
+ resolveSpinner.succeed(
932
1115
  `Resolved ${skillName}@${requestedVersion} \u2192 ${chalk2.bold(versionDetail.version)}`
933
1116
  );
934
1117
  } catch (err) {
935
- spinner.fail(`Failed to resolve ${skillName}@${requestedVersion}`);
1118
+ resolveSpinner.fail(`Failed to resolve ${skillName}@${requestedVersion}`);
936
1119
  throw err;
937
1120
  }
938
1121
  const filename = `${bareName}-${versionDetail.version}.tgz`;
939
- const downloadSpinner = ora(
1122
+ const downloadSpinner = spinner(
940
1123
  `Downloading ${filename} (${formatSize(versionDetail.size_bytes)})...`
941
- ).start();
1124
+ );
942
1125
  let tarballData;
943
1126
  let serverIntegrity;
944
1127
  try {
@@ -951,7 +1134,7 @@ async function runInstall(skillArg, options) {
951
1134
  throw err;
952
1135
  }
953
1136
  if (serverIntegrity && versionDetail.tarball_sha512) {
954
- const integritySpinner = ora("Verifying integrity...").start();
1137
+ const integritySpinner = spinner("Verifying integrity...");
955
1138
  if (verifyIntegrity(tarballData, versionDetail.tarball_sha512)) {
956
1139
  integritySpinner.succeed("Integrity verified");
957
1140
  } else {
@@ -1002,7 +1185,7 @@ async function runInstall(skillArg, options) {
1002
1185
  const envSchemas = manifest.config?.env || [];
1003
1186
  if (envSchemas.length > 0) {
1004
1187
  for (const envVar of envSchemas) {
1005
- const value = await promptEnvVar(envVar, options.yes);
1188
+ const value = await promptEnvVar(envVar, options.yes, envMap);
1006
1189
  envValues[envVar.name] = value;
1007
1190
  }
1008
1191
  blank();
@@ -1062,6 +1245,11 @@ async function selectAgents(options) {
1062
1245
  return detected.length > 0 ? detected : ALL_ADAPTERS;
1063
1246
  }
1064
1247
  if (detected.length === 0) {
1248
+ if (!isInteractive()) {
1249
+ throw usageError(
1250
+ "No agents detected and no agent flags specified. Use --claude, --cursor, --codex, --copilot, --gemini, --cline, or --all."
1251
+ );
1252
+ }
1065
1253
  console.log(
1066
1254
  chalk2.yellow(
1067
1255
  "\n No agents auto-detected. Select which agents to install for:\n"
@@ -1079,6 +1267,9 @@ async function selectAgents(options) {
1079
1267
  if (detected.length === 1) {
1080
1268
  return detected;
1081
1269
  }
1270
+ if (!isInteractive()) {
1271
+ return detected;
1272
+ }
1082
1273
  const selected = await checkbox({
1083
1274
  message: "Multiple agents detected. Select which to install for:",
1084
1275
  choices: detected.map((a) => ({
@@ -1089,7 +1280,10 @@ async function selectAgents(options) {
1089
1280
  });
1090
1281
  return selected.map((name) => getAdapter(name)).filter(Boolean);
1091
1282
  }
1092
- async function promptEnvVar(envVar, autoYes) {
1283
+ async function promptEnvVar(envVar, autoYes, envMap) {
1284
+ if (envMap && envVar.name in envMap) {
1285
+ return envMap[envVar.name];
1286
+ }
1093
1287
  const label = [
1094
1288
  envVar.name,
1095
1289
  envVar.required ? "(required" : "(optional",
@@ -1099,6 +1293,13 @@ async function promptEnvVar(envVar, autoYes) {
1099
1293
  if (autoYes && envVar.default) {
1100
1294
  return envVar.default;
1101
1295
  }
1296
+ if (!isInteractive()) {
1297
+ if (envVar.default) {
1298
+ return envVar.default;
1299
+ }
1300
+ warn(`No value provided for ${envVar.name} \u2014 using empty string. Pass --env ${envVar.name}=<value> to set it.`);
1301
+ return "";
1302
+ }
1102
1303
  if (envVar.secret) {
1103
1304
  return await password({
1104
1305
  message: ` Enter value for ${envVar.name}:`
@@ -1140,6 +1341,8 @@ var init_install = __esm({
1140
1341
  init_config();
1141
1342
  init_config2();
1142
1343
  init_output();
1344
+ init_output();
1345
+ init_errors();
1143
1346
  }
1144
1347
  });
1145
1348
 
@@ -1151,15 +1354,28 @@ import { Command as Command15 } from "commander";
1151
1354
  init_config();
1152
1355
  init_adapters();
1153
1356
  init_output();
1357
+ init_errors();
1154
1358
  import { rm } from "fs/promises";
1155
1359
  import { resolve as resolve2 } from "path";
1156
1360
  import { Command as Command2 } from "commander";
1157
1361
  import chalk3 from "chalk";
1158
1362
  function createUninstallCommand() {
1159
- return new Command2("uninstall").argument("<skill>", "Skill name to uninstall").option("--claude", "Uninstall from Claude Code").option("--cursor", "Uninstall from Cursor").option("--codex", "Uninstall from Codex CLI").option("--copilot", "Uninstall from VS Code / Copilot").option("--gemini", "Uninstall from Gemini CLI").option("--cline", "Uninstall from Cline").option("-g, --global", "Uninstall from global skills directory").description("Remove a skill and its configuration").action(async (skillName, options) => {
1363
+ return new Command2("uninstall").argument("<skill>", "Skill name to uninstall").option("--claude", "Uninstall from Claude Code only").option("--cursor", "Uninstall from Cursor only").option("--codex", "Uninstall from Codex CLI only").option("--copilot", "Uninstall from VS Code / Copilot only").option("--gemini", "Uninstall from Gemini CLI only").option("--cline", "Uninstall from Cline only").option("-g, --global", "Uninstall from global skills directory").description(
1364
+ "Remove a skill and its MCP configuration. Removes skill files and MCP server entries for all agents, or specific agents if flags are provided."
1365
+ ).addHelpText("after", `
1366
+ Examples:
1367
+ shipables uninstall Chippers255/ai-commits Remove from all agents
1368
+ shipables uninstall Chippers255/ai-commits --claude Remove from Claude Code only
1369
+ shipables uninstall Chippers255/ai-commits -g Remove globally installed skill
1370
+ shipables uninstall @org/tool --cursor Remove scoped skill from Cursor
1371
+ `).action(async (skillName, options) => {
1160
1372
  try {
1161
1373
  await runUninstall(skillName, options);
1162
1374
  } catch (err) {
1375
+ if (err instanceof CliError) {
1376
+ errorWithFix(err.message, err.fix || "");
1377
+ process.exit(err.exitCode);
1378
+ }
1163
1379
  error(err instanceof Error ? err.message : String(err));
1164
1380
  process.exit(1);
1165
1381
  }
@@ -1169,8 +1385,9 @@ async function runUninstall(skillName, options) {
1169
1385
  const projectPath = options.global ? "__global__" : process.cwd();
1170
1386
  const record = await getInstallation(projectPath, skillName);
1171
1387
  if (!record) {
1172
- throw new Error(
1173
- `${skillName} is not installed${options.global ? " globally" : " in this project"}`
1388
+ throw notFoundError(
1389
+ `'${skillName}' is not installed${options.global ? " globally" : " in this project"}.`,
1390
+ `List installed skills with: shipables list${options.global ? " --global" : ""}`
1174
1391
  );
1175
1392
  }
1176
1393
  const flagMap = {
@@ -1184,8 +1401,9 @@ async function runUninstall(skillName, options) {
1184
1401
  const hasFlags = Object.values(flagMap).some(Boolean);
1185
1402
  const targetAgents = hasFlags ? record.agents.filter((a) => flagMap[a]) : record.agents;
1186
1403
  if (targetAgents.length === 0) {
1187
- throw new Error(
1188
- `${skillName} is not installed for the specified agent(s)`
1404
+ throw notFoundError(
1405
+ `'${skillName}' is not installed for the specified agent(s).`,
1406
+ `Currently installed for: ${record.agents.join(", ")}`
1189
1407
  );
1190
1408
  }
1191
1409
  for (const agentName of targetAgents) {
@@ -1235,40 +1453,56 @@ async function runUninstall(skillName, options) {
1235
1453
  success(
1236
1454
  `Uninstalled ${chalk3.bold(skillName)} from ${targetAgents.map((a) => getAdapter(a)?.displayName || a).join(", ")}`
1237
1455
  );
1456
+ info("Verify with: shipables list");
1238
1457
  blank();
1239
1458
  }
1240
1459
 
1241
1460
  // src/commands/search.ts
1242
1461
  init_client();
1243
1462
  init_output();
1463
+ init_output();
1464
+ init_errors();
1244
1465
  import { Command as Command3 } from "commander";
1245
- import ora2 from "ora";
1246
1466
  function createSearchCommand() {
1247
- return new Command3("search").argument("<query>", "Search query").option("--agent <name>", "Filter by agent compatibility").option("--category <slug>", "Filter by category").option("--limit <n>", "Max results", "10").option("--json", "Output as JSON").description("Search the registry for skills").action(async (query, options) => {
1467
+ return new Command3("search").argument("<query>", "Search query").option("--agent <name>", "Filter by agent compatibility (claude, cursor, codex, copilot, gemini, cline)").option("--category <slug>", "Filter by category").option("--limit <n>", "Max results", "10").option("--json", "Output as JSON").description(
1468
+ "Search the registry for skills. Returns skill name, version, weekly downloads, and description. Use --json for structured output."
1469
+ ).addHelpText("after", `
1470
+ Examples:
1471
+ shipables search "database" Search for database-related skills
1472
+ shipables search "auth" --agent claude Search skills compatible with Claude
1473
+ shipables search "testing" --category qa Search within a category
1474
+ shipables search "deploy" --json Get results as JSON
1475
+ shipables search "api" --limit 20 Get more results
1476
+ `).action(async (query, options) => {
1248
1477
  try {
1249
1478
  await runSearch(query, options);
1250
1479
  } catch (err) {
1480
+ if (err instanceof CliError) {
1481
+ errorWithFix(err.message, err.fix || "");
1482
+ process.exit(err.exitCode);
1483
+ }
1251
1484
  error(err instanceof Error ? err.message : String(err));
1252
1485
  process.exit(1);
1253
1486
  }
1254
1487
  });
1255
1488
  }
1256
1489
  async function runSearch(query, options) {
1257
- const spinner = ora2("Searching...").start();
1490
+ const searchSpinner = spinner("Searching...");
1258
1491
  const result = await registry.search({
1259
1492
  q: query,
1260
1493
  agent: options.agent,
1261
1494
  category: options.category,
1262
1495
  limit: parseInt(options.limit || "10", 10)
1263
1496
  });
1264
- spinner.stop();
1265
- if (options.json) {
1497
+ searchSpinner.stop();
1498
+ if (options.json || getJsonMode()) {
1266
1499
  console.log(JSON.stringify(result, null, 2));
1267
1500
  return;
1268
1501
  }
1269
1502
  if (result.skills.length === 0) {
1270
1503
  blank();
1271
- info("No skills found matching your query.");
1504
+ info(`No skills found matching "${query}".`);
1505
+ info("Try a broader query or browse categories at https://shipables.dev");
1272
1506
  blank();
1273
1507
  return;
1274
1508
  }
@@ -1287,6 +1521,10 @@ async function runSearch(query, options) {
1287
1521
  info(
1288
1522
  `${result.pagination.total} result${result.pagination.total === 1 ? "" : "s"} found`
1289
1523
  );
1524
+ if (result.skills.length > 0) {
1525
+ info(`Get details: shipables info ${result.skills[0].full_name}`);
1526
+ info(`Install: shipables install ${result.skills[0].full_name} --<agent>`);
1527
+ }
1290
1528
  blank();
1291
1529
  }
1292
1530
  function truncate(s, max) {
@@ -1297,24 +1535,36 @@ function truncate(s, max) {
1297
1535
  // src/commands/info.ts
1298
1536
  init_client();
1299
1537
  init_output();
1538
+ init_output();
1539
+ init_errors();
1300
1540
  import { Command as Command4 } from "commander";
1301
1541
  import chalk4 from "chalk";
1302
- import ora3 from "ora";
1303
1542
  function createInfoCommand() {
1304
- return new Command4("info").argument("<skill>", "Skill name").option("--json", "Output as JSON").description("Show detailed information about a skill").action(async (skillName, options) => {
1543
+ return new Command4("info").argument("<skill>", "Skill name (e.g., Chippers255/ai-commits, @org/my-skill)").option("--json", "Output as JSON").description(
1544
+ "Show detailed information about a skill including versions, MCP servers, categories, and download counts."
1545
+ ).addHelpText("after", `
1546
+ Examples:
1547
+ shipables info Chippers255/ai-commits Show details for a skill
1548
+ shipables info @org/my-skill Show details for a scoped skill
1549
+ shipables info Chippers255/ai-commits --json Get details as JSON
1550
+ `).action(async (skillName, options) => {
1305
1551
  try {
1306
1552
  await runInfo(skillName, options);
1307
1553
  } catch (err) {
1554
+ if (err instanceof CliError) {
1555
+ errorWithFix(err.message, err.fix || "");
1556
+ process.exit(err.exitCode);
1557
+ }
1308
1558
  error(err instanceof Error ? err.message : String(err));
1309
1559
  process.exit(1);
1310
1560
  }
1311
1561
  });
1312
1562
  }
1313
1563
  async function runInfo(skillName, options) {
1314
- const spinner = ora3(`Fetching info for ${skillName}...`).start();
1564
+ const infoSpinner = spinner(`Fetching info for ${skillName}...`);
1315
1565
  const detail = await registry.getSkillDetail(skillName);
1316
- spinner.stop();
1317
- if (options.json) {
1566
+ infoSpinner.stop();
1567
+ if (options.json || getJsonMode()) {
1318
1568
  console.log(JSON.stringify(detail, null, 2));
1319
1569
  return;
1320
1570
  }
@@ -1350,7 +1600,7 @@ async function runInfo(skillName, options) {
1350
1600
  }
1351
1601
  if (detail.config?.env && detail.config.env.length > 0) {
1352
1602
  blank();
1353
- info("Config:");
1603
+ info("Required environment variables:");
1354
1604
  for (const envVar of detail.config.env) {
1355
1605
  const flags = [
1356
1606
  envVar.required ? "required" : "optional",
@@ -1376,17 +1626,26 @@ async function runInfo(skillName, options) {
1376
1626
  }
1377
1627
  }
1378
1628
  blank();
1379
- info(`Install: ${chalk4.cyan(`npx shipables install ${detail.full_name}`)}`);
1629
+ info(`Install: shipables install ${detail.full_name}`);
1630
+ info(`Install for specific agent: shipables install ${detail.full_name} --claude`);
1380
1631
  blank();
1381
1632
  }
1382
1633
 
1383
1634
  // src/commands/list.ts
1384
1635
  init_config();
1385
1636
  init_output();
1637
+ init_output();
1386
1638
  import { Command as Command5 } from "commander";
1387
- import chalk5 from "chalk";
1388
1639
  function createListCommand() {
1389
- return new Command5("list").alias("ls").option("--json", "Output as JSON").option("-g, --global", "Show globally installed skills").description("List installed skills").action(async (options) => {
1640
+ return new Command5("list").alias("ls").option("--json", "Output as JSON").option("-g, --global", "Show globally installed skills").description(
1641
+ "List installed skills in the current project (or globally with -g). Shows skill name, version, and which agents it's installed for."
1642
+ ).addHelpText("after", `
1643
+ Examples:
1644
+ shipables list List skills in current project
1645
+ shipables list --global List globally installed skills
1646
+ shipables list --json Get installed skills as JSON
1647
+ shipables ls Alias for list
1648
+ `).action(async (options) => {
1390
1649
  try {
1391
1650
  await runList(options);
1392
1651
  } catch (err) {
@@ -1399,7 +1658,7 @@ async function runList(options) {
1399
1658
  const projectPath = options.global ? "__global__" : process.cwd();
1400
1659
  const installations = await getProjectInstallations(projectPath);
1401
1660
  const entries = Object.entries(installations);
1402
- if (options.json) {
1661
+ if (options.json || getJsonMode()) {
1403
1662
  console.log(JSON.stringify(installations, null, 2));
1404
1663
  return;
1405
1664
  }
@@ -1408,9 +1667,8 @@ async function runList(options) {
1408
1667
  info(
1409
1668
  options.global ? "No skills installed globally." : "No skills installed in this project."
1410
1669
  );
1411
- info(
1412
- chalk5.dim("Install one with: npx shipables install <skill>")
1413
- );
1670
+ info("Install one with: shipables install <skill>");
1671
+ info("Search for skills: shipables search <query>");
1414
1672
  blank();
1415
1673
  return;
1416
1674
  }
@@ -1427,6 +1685,7 @@ async function runList(options) {
1427
1685
  );
1428
1686
  blank();
1429
1687
  info(`${entries.length} skill${entries.length === 1 ? "" : "s"} installed`);
1688
+ info("Check for updates: shipables update");
1430
1689
  blank();
1431
1690
  }
1432
1691
 
@@ -1434,12 +1693,22 @@ async function runList(options) {
1434
1693
  init_config();
1435
1694
  init_client();
1436
1695
  init_output();
1696
+ init_output();
1697
+ init_errors();
1437
1698
  import { Command as Command6 } from "commander";
1438
- import chalk6 from "chalk";
1439
- import ora4 from "ora";
1699
+ import chalk5 from "chalk";
1440
1700
  import { confirm } from "@inquirer/prompts";
1441
1701
  function createUpdateCommand() {
1442
- return new Command6("update").argument("[skill]", "Skill to update (omit to update all)").option("--dry-run", "Show what would be updated without making changes").option("-g, --global", "Update globally installed skills").option("--self", "Update the shipables CLI itself").description("Update installed skills to latest version").action(async (skill, options) => {
1702
+ return new Command6("update").argument("[skill]", "Skill to update (omit to update all)").option("--dry-run", "Show what would be updated without making changes").option("-g, --global", "Update globally installed skills").option("-y, --yes", "Skip confirmation prompt").option("--self", "Update the shipables CLI itself").description(
1703
+ "Update installed skills to latest version. If no skill is specified, checks all installed skills for updates. Use --yes to skip the confirmation prompt."
1704
+ ).addHelpText("after", `
1705
+ Examples:
1706
+ shipables update Check and update all installed skills
1707
+ shipables update Chippers255/ai-commits Update a specific skill
1708
+ shipables update --dry-run Show available updates without installing
1709
+ shipables update --yes Update all without confirmation
1710
+ shipables update --self Update the shipables CLI itself
1711
+ `).action(async (skill, options) => {
1443
1712
  try {
1444
1713
  if (options.self) {
1445
1714
  await runSelfUpdate();
@@ -1447,6 +1716,10 @@ function createUpdateCommand() {
1447
1716
  }
1448
1717
  await runUpdate(skill, options);
1449
1718
  } catch (err) {
1719
+ if (err instanceof CliError) {
1720
+ errorWithFix(err.message, err.fix || "");
1721
+ process.exit(err.exitCode);
1722
+ }
1450
1723
  error(err instanceof Error ? err.message : String(err));
1451
1724
  process.exit(1);
1452
1725
  }
@@ -1456,7 +1729,7 @@ async function runSelfUpdate() {
1456
1729
  blank();
1457
1730
  info("To update the shipables CLI, run:");
1458
1731
  blank();
1459
- console.log(chalk6.cyan(" npm update -g shipables"));
1732
+ console.log(chalk5.cyan(" npm update -g @senso-ai/shipables"));
1460
1733
  blank();
1461
1734
  info("Or if you installed with npx, it automatically uses the latest version.");
1462
1735
  blank();
@@ -1468,14 +1741,18 @@ async function runUpdate(skill, options) {
1468
1741
  if (entries.length === 0) {
1469
1742
  blank();
1470
1743
  info("No skills installed to update.");
1744
+ info("Install a skill with: shipables install <skill>");
1471
1745
  blank();
1472
1746
  return;
1473
1747
  }
1474
1748
  const toCheck = skill ? entries.filter(([name]) => name === skill) : entries;
1475
1749
  if (toCheck.length === 0) {
1476
- throw new Error(`${skill} is not installed`);
1750
+ throw notFoundError(
1751
+ `'${skill}' is not installed${options.global ? " globally" : " in this project"}.`,
1752
+ `List installed skills with: shipables list${options.global ? " --global" : ""}`
1753
+ );
1477
1754
  }
1478
- const spinner = ora4("Checking for updates...").start();
1755
+ const checkSpinner = spinner("Checking for updates...");
1479
1756
  const updates = [];
1480
1757
  for (const [name, record] of toCheck) {
1481
1758
  try {
@@ -1497,7 +1774,7 @@ async function runUpdate(skill, options) {
1497
1774
  });
1498
1775
  }
1499
1776
  }
1500
- spinner.stop();
1777
+ checkSpinner.stop();
1501
1778
  blank();
1502
1779
  table(
1503
1780
  ["SKILL", "CURRENT", "LATEST", "STATUS"],
@@ -1505,33 +1782,36 @@ async function runUpdate(skill, options) {
1505
1782
  u.name,
1506
1783
  u.currentVersion,
1507
1784
  u.latestVersion,
1508
- u.hasUpdate ? chalk6.yellow("update available") : chalk6.green("up to date")
1785
+ u.hasUpdate ? chalk5.yellow("update available") : chalk5.green("up to date")
1509
1786
  ]),
1510
1787
  [25, 10, 10, 20]
1511
1788
  );
1512
1789
  const updatable = updates.filter((u) => u.hasUpdate);
1513
1790
  if (updatable.length === 0) {
1514
1791
  blank();
1515
- success("All skills are up to date");
1792
+ success("All skills are up to date.");
1516
1793
  blank();
1517
1794
  return;
1518
1795
  }
1519
1796
  if (options.dryRun) {
1520
1797
  blank();
1521
1798
  info(
1522
- `${updatable.length} skill${updatable.length === 1 ? "" : "s"} would be updated`
1799
+ `${updatable.length} skill${updatable.length === 1 ? "" : "s"} would be updated.`
1523
1800
  );
1801
+ info("Run without --dry-run to apply updates.");
1524
1802
  blank();
1525
1803
  return;
1526
1804
  }
1527
- blank();
1528
- const shouldUpdate = await confirm({
1529
- message: `Update ${updatable.length} skill${updatable.length === 1 ? "" : "s"}?`,
1530
- default: true
1531
- });
1532
- if (!shouldUpdate) {
1533
- info("Update cancelled.");
1534
- return;
1805
+ if (!options.yes && isInteractive()) {
1806
+ blank();
1807
+ const shouldUpdate = await confirm({
1808
+ message: `Update ${updatable.length} skill${updatable.length === 1 ? "" : "s"}?`,
1809
+ default: true
1810
+ });
1811
+ if (!shouldUpdate) {
1812
+ info("Update cancelled.");
1813
+ return;
1814
+ }
1535
1815
  }
1536
1816
  const { createInstallCommand: createInstallCommand2 } = await Promise.resolve().then(() => (init_install(), install_exports));
1537
1817
  for (const u of updatable) {
@@ -1559,8 +1839,9 @@ async function runUpdate(skill, options) {
1559
1839
  }
1560
1840
  blank();
1561
1841
  success(
1562
- `${updatable.length} skill${updatable.length === 1 ? "" : "s"} updated`
1842
+ `${updatable.length} skill${updatable.length === 1 ? "" : "s"} updated.`
1563
1843
  );
1844
+ info("Run `shipables doctor` to verify installations.");
1564
1845
  blank();
1565
1846
  }
1566
1847
 
@@ -1568,8 +1849,7 @@ async function runUpdate(skill, options) {
1568
1849
  import { access as access2, readFile as readFile7, stat as stat3 } from "fs/promises";
1569
1850
  import { join as join6 } from "path";
1570
1851
  import { Command as Command7 } from "commander";
1571
- import chalk7 from "chalk";
1572
- import ora5 from "ora";
1852
+ import chalk6 from "chalk";
1573
1853
  import { confirm as confirm2, select } from "@inquirer/prompts";
1574
1854
 
1575
1855
  // src/skill/parser.ts
@@ -1670,15 +1950,30 @@ init_hash();
1670
1950
  init_client();
1671
1951
  init_config();
1672
1952
  init_output();
1953
+ init_output();
1954
+ init_errors();
1673
1955
  function createPublishCommand() {
1674
1956
  return new Command7("publish").option("--dry-run", "Pack and validate without uploading").option("--tag <tag>", "Dist-tag (e.g., beta, next)", "latest").option(
1675
1957
  "--access <level>",
1676
1958
  "Access level for scoped packages",
1677
1959
  "public"
1678
- ).description("Publish the skill in the current directory").action(async (options) => {
1960
+ ).option("-y, --yes", "Skip confirmation prompt").option("--scope <scope>", "Publish scope: personal username or @org name (e.g., --scope @myorg)").description(
1961
+ "Pack and publish the skill in the current directory to the registry. Requires SKILL.md and shipables.json. You must be logged in (shipables login). Use --yes and --scope for non-interactive publishing."
1962
+ ).addHelpText("after", `
1963
+ Examples:
1964
+ shipables publish Interactive publish
1965
+ shipables publish --dry-run Validate and pack without uploading
1966
+ shipables publish --yes Publish without confirmation prompt
1967
+ shipables publish --yes --scope @myorg Publish under an org, no prompts
1968
+ shipables publish --yes --scope personal Publish under your personal account
1969
+ `).action(async (options) => {
1679
1970
  try {
1680
1971
  await runPublish(options);
1681
1972
  } catch (err) {
1973
+ if (err instanceof CliError) {
1974
+ errorWithFix(err.message, err.fix || "");
1975
+ process.exit(err.exitCode);
1976
+ }
1682
1977
  error(err instanceof Error ? err.message : String(err));
1683
1978
  process.exit(1);
1684
1979
  }
@@ -1691,15 +1986,17 @@ async function runPublish(options) {
1691
1986
  try {
1692
1987
  await access2(skillMdPath);
1693
1988
  } catch {
1694
- throw new Error(
1695
- "No SKILL.md found in the current directory. Run `shipables init` to create one."
1989
+ throw validationError(
1990
+ "No SKILL.md found in the current directory.",
1991
+ "Run `shipables init` to create one, or `shipables init --name <name> --description <desc>` for non-interactive scaffolding."
1696
1992
  );
1697
1993
  }
1698
1994
  try {
1699
1995
  await access2(manifestPath);
1700
1996
  } catch {
1701
- throw new Error(
1702
- "No shipables.json found in the current directory. Run `shipables init` to create one."
1997
+ throw validationError(
1998
+ "No shipables.json found in the current directory.",
1999
+ "Run `shipables init` to create one, or `shipables init --name <name> --description <desc>` for non-interactive scaffolding."
1703
2000
  );
1704
2001
  }
1705
2002
  header("Validating:");
@@ -1713,7 +2010,10 @@ async function runPublish(options) {
1713
2010
  for (const err of manifestErrors) {
1714
2011
  error(err);
1715
2012
  }
1716
- throw new Error("shipables.json validation failed");
2013
+ throw validationError(
2014
+ "shipables.json validation failed.",
2015
+ "Fix the errors listed above in shipables.json, then re-run: shipables publish"
2016
+ );
1717
2017
  }
1718
2018
  success(
1719
2019
  `shipables.json \u2014 version: ${manifest.version}, schema valid`
@@ -1722,16 +2022,17 @@ async function runPublish(options) {
1722
2022
  const version = manifest.version;
1723
2023
  const token = await getToken();
1724
2024
  if (!token && !options.dryRun) {
1725
- throw new Error(
1726
- "Not authenticated. Run `shipables login` first."
2025
+ throw authError(
2026
+ "Not authenticated. You must be logged in to publish.",
2027
+ "Run `shipables login` to authenticate, then re-run `shipables publish`."
1727
2028
  );
1728
2029
  }
1729
2030
  let fullName = skillBareName;
1730
2031
  if (!options.dryRun && token) {
1731
- fullName = await resolvePublishScope(skillBareName);
2032
+ fullName = await resolvePublishScope(skillBareName, options.scope);
1732
2033
  }
1733
2034
  blank();
1734
- console.log(` Publishing ${chalk7.bold(`${fullName}@${version}`)}...`);
2035
+ console.log(` Publishing ${chalk6.bold(`${fullName}@${version}`)}...`);
1735
2036
  let ignorePatterns = [];
1736
2037
  try {
1737
2038
  const ignoreRaw = await readFile7(join6(cwd, ".shipablesignore"), "utf-8");
@@ -1740,7 +2041,7 @@ async function runPublish(options) {
1740
2041
  }
1741
2042
  const tarballName = `${skillBareName}-${version}.tgz`;
1742
2043
  const tarballPath = join6(cwd, tarballName);
1743
- const packSpinner = ora5("Packing tarball...").start();
2044
+ const packSpinner = spinner("Packing tarball...");
1744
2045
  await createTarball(cwd, tarballPath, ignorePatterns);
1745
2046
  const tarballStat = await stat3(tarballPath);
1746
2047
  const hash = await sha512File(tarballPath);
@@ -1754,34 +2055,39 @@ async function runPublish(options) {
1754
2055
  console.log(` ${file}`);
1755
2056
  }
1756
2057
  if (files.length > 20) {
1757
- console.log(chalk7.dim(` ... and ${files.length - 20} more files`));
2058
+ console.log(chalk6.dim(` ... and ${files.length - 20} more files`));
1758
2059
  }
1759
2060
  if (options.dryRun) {
1760
2061
  blank();
1761
- success("Dry run complete \u2014 tarball created but not uploaded");
2062
+ success(`Dry run complete \u2014 ${tarballName} (${formatBytes(tarballStat.size)}) packed but not uploaded.`);
2063
+ info(`Validated: SKILL.md (name: ${skillBareName}), shipables.json (version: ${version})`);
1762
2064
  const { unlink } = await import("fs/promises");
1763
2065
  await unlink(tarballPath);
1764
2066
  blank();
1765
2067
  return;
1766
2068
  }
1767
- blank();
1768
- const shouldPublish = await confirm2({
1769
- message: `Publish ${fullName}@${version} to the shipables registry?`,
1770
- default: true
1771
- });
1772
- if (!shouldPublish) {
1773
- const { unlink } = await import("fs/promises");
1774
- await unlink(tarballPath);
1775
- info("Publish cancelled.");
1776
- return;
2069
+ if (!options.yes && isInteractive()) {
2070
+ blank();
2071
+ const shouldPublish = await confirm2({
2072
+ message: `Publish ${fullName}@${version} to the shipables registry?`,
2073
+ default: true
2074
+ });
2075
+ if (!shouldPublish) {
2076
+ const { unlink } = await import("fs/promises");
2077
+ await unlink(tarballPath);
2078
+ info("Publish cancelled.");
2079
+ return;
2080
+ }
1777
2081
  }
1778
- const uploadSpinner = ora5("Publishing...").start();
2082
+ const uploadSpinner = spinner("Publishing...");
1779
2083
  try {
1780
2084
  const result = await registry.publishWithName(fullName, tarballPath);
1781
- uploadSpinner.succeed(`Published ${chalk7.bold(`${fullName}@${version}`)}`);
2085
+ uploadSpinner.succeed(`Published ${chalk6.bold(`${fullName}@${version}`)}`);
2086
+ blank();
1782
2087
  if (result.url) {
1783
- info(result.url);
2088
+ info(`View: ${result.url}`);
1784
2089
  }
2090
+ info(`Install with: npx shipables install ${fullName}`);
1785
2091
  } catch (err) {
1786
2092
  uploadSpinner.fail("Publish failed");
1787
2093
  throw err;
@@ -1794,7 +2100,7 @@ async function runPublish(options) {
1794
2100
  }
1795
2101
  blank();
1796
2102
  }
1797
- async function resolvePublishScope(bareName) {
2103
+ async function resolvePublishScope(bareName, scopeFlag) {
1798
2104
  let me;
1799
2105
  try {
1800
2106
  me = await registry.getMe();
@@ -1805,6 +2111,17 @@ async function resolvePublishScope(bareName) {
1805
2111
  const publishableOrgs = orgs.filter(
1806
2112
  (o) => o.role === "owner" || o.role === "admin"
1807
2113
  );
2114
+ if (scopeFlag) {
2115
+ if (scopeFlag === "personal" || scopeFlag === me.username) {
2116
+ return `${me.username}/${bareName}`;
2117
+ }
2118
+ const orgName = scopeFlag.replace(/^@/, "");
2119
+ const org = publishableOrgs.find((o) => o.name === orgName);
2120
+ if (org) {
2121
+ return `@${org.name}/${bareName}`;
2122
+ }
2123
+ return `@${orgName}/${bareName}`;
2124
+ }
1808
2125
  if (publishableOrgs.length === 0) {
1809
2126
  return `${me.username}/${bareName}`;
1810
2127
  }
@@ -1821,6 +2138,10 @@ async function resolvePublishScope(bareName) {
1821
2138
  if (choices.length === 1) {
1822
2139
  return choices[0].value;
1823
2140
  }
2141
+ if (!isInteractive()) {
2142
+ info(`Auto-selected publish scope: ${choices[0].value}. Use --scope to specify a different scope.`);
2143
+ return choices[0].value;
2144
+ }
1824
2145
  const selected = await select({
1825
2146
  message: "Publish as:",
1826
2147
  choices
@@ -1859,15 +2180,27 @@ async function listAllFiles(dir, ignorePatterns, prefix = "") {
1859
2180
  init_client();
1860
2181
  init_config();
1861
2182
  init_output();
2183
+ init_output();
2184
+ init_errors();
1862
2185
  import { Command as Command8 } from "commander";
1863
- import chalk8 from "chalk";
1864
- import ora6 from "ora";
2186
+ import chalk7 from "chalk";
1865
2187
  import { confirm as confirm3 } from "@inquirer/prompts";
1866
2188
  function createUnpublishCommand() {
1867
- return new Command8("unpublish").argument("<skill>", "Skill name with version (e.g., my-skill@1.0.0)").option("-f, --force", "Skip confirmation prompt").description("Yank a published version (within 72 hours of publish)").action(async (skillArg, options) => {
2189
+ return new Command8("unpublish").argument("<skill>", "Skill name with version (e.g., my-skill@1.0.0)").option("-f, --force", "Skip confirmation prompt").description(
2190
+ "Yank a published version from the registry (within 72 hours of publish). Yanked versions cannot be re-published with the same version number. Use --force to skip the confirmation prompt."
2191
+ ).addHelpText("after", `
2192
+ Examples:
2193
+ shipables unpublish my-skill@1.0.0 Interactive confirmation
2194
+ shipables unpublish my-skill@1.0.0 --force Skip confirmation
2195
+ shipables unpublish @org/tool@2.1.0 -f Unpublish a scoped skill
2196
+ `).action(async (skillArg, options) => {
1868
2197
  try {
1869
2198
  await runUnpublish(skillArg, options);
1870
2199
  } catch (err) {
2200
+ if (err instanceof CliError) {
2201
+ errorWithFix(err.message, err.fix || "");
2202
+ process.exit(err.exitCode);
2203
+ }
1871
2204
  error(err instanceof Error ? err.message : String(err));
1872
2205
  process.exit(1);
1873
2206
  }
@@ -1876,21 +2209,25 @@ function createUnpublishCommand() {
1876
2209
  async function runUnpublish(skillArg, options) {
1877
2210
  const token = await getToken();
1878
2211
  if (!token) {
1879
- throw new Error("Not authenticated. Run `shipables login` first.");
2212
+ throw authError(
2213
+ "Not authenticated. You must be logged in to unpublish.",
2214
+ "Run `shipables login` to authenticate."
2215
+ );
1880
2216
  }
1881
2217
  const { name, version } = parseSkillVersion(skillArg);
1882
2218
  if (!version) {
1883
- throw new Error(
1884
- "You must specify a version to unpublish (e.g., my-skill@1.0.0). Unpublishing an entire package is not supported."
2219
+ throw usageError(
2220
+ "You must specify a version to unpublish (e.g., my-skill@1.0.0). Unpublishing an entire package is not supported.",
2221
+ "Use the format: shipables unpublish <skill>@<version>"
1885
2222
  );
1886
2223
  }
1887
2224
  blank();
1888
2225
  warn(
1889
- `This will yank ${chalk8.bold(`${name}@${version}`)} from the registry.`
2226
+ `This will yank ${chalk7.bold(`${name}@${version}`)} from the registry.`
1890
2227
  );
1891
2228
  warn("Yanked versions cannot be re-published with the same version number.");
1892
2229
  blank();
1893
- if (!options.force) {
2230
+ if (!options.force && isInteractive()) {
1894
2231
  const shouldProceed = await confirm3({
1895
2232
  message: `Are you sure you want to unpublish ${name}@${version}?`,
1896
2233
  default: false
@@ -1900,12 +2237,12 @@ async function runUnpublish(skillArg, options) {
1900
2237
  return;
1901
2238
  }
1902
2239
  }
1903
- const spinner = ora6(`Unpublishing ${name}@${version}...`).start();
2240
+ const unpubSpinner = spinner(`Unpublishing ${name}@${version}...`);
1904
2241
  try {
1905
2242
  await registry.unpublishVersion(name, version);
1906
- spinner.succeed(`Unpublished ${chalk8.bold(`${name}@${version}`)}`);
2243
+ unpubSpinner.succeed(`Unpublished ${chalk7.bold(`${name}@${version}`)}`);
1907
2244
  } catch (err) {
1908
- spinner.fail("Unpublish failed");
2245
+ unpubSpinner.fail("Unpublish failed");
1909
2246
  throw err;
1910
2247
  }
1911
2248
  blank();
@@ -1931,10 +2268,12 @@ function parseSkillVersion(arg) {
1931
2268
 
1932
2269
  // src/commands/init.ts
1933
2270
  init_output();
2271
+ init_errors();
2272
+ init_output();
1934
2273
  import { mkdir as mkdir4, writeFile as writeFile5, access as access3 } from "fs/promises";
1935
2274
  import { join as join7 } from "path";
1936
2275
  import { Command as Command9 } from "commander";
1937
- import chalk9 from "chalk";
2276
+ import chalk8 from "chalk";
1938
2277
  import { input as input2, confirm as confirm4, checkbox as checkbox2 } from "@inquirer/prompts";
1939
2278
  var CATEGORIES = [
1940
2279
  "databases",
@@ -1954,16 +2293,54 @@ var CATEGORIES = [
1954
2293
  "other"
1955
2294
  ];
1956
2295
  function createInitCommand() {
1957
- return new Command9("init").description("Scaffold a new skill in the current directory").action(async () => {
2296
+ return new Command9("init").description(
2297
+ "Scaffold a new skill in the current directory. Creates SKILL.md, shipables.json, README.md, and .shipablesignore. Use flags (--name, --description) for non-interactive mode."
2298
+ ).option("--name <name>", "Skill name (lowercase, hyphens, max 64 chars)").option("--description <desc>", "Description of the skill").option("--author <name>", "Author name").option("--author-github <gh>", "Author GitHub username").option("--license <license>", "License identifier (default: MIT)").option("--category <cat...>", "Categories (repeatable)").option("--mcp-package <pkg>", "MCP server npm package name").option("--scripts", "Include example scripts/ directory").option("--no-scripts", "Do not include example scripts/ directory").option("--references", "Include example references/ directory").option("--no-references", "Do not include example references/ directory").option("-y, --yes", "Accept defaults for optional fields").addHelpText(
2299
+ "after",
2300
+ `
2301
+ Examples:
2302
+ shipables init Interactive mode
2303
+ shipables init --name my-skill --description "..." Non-interactive with required fields
2304
+ shipables init --name my-skill --description "..." --mcp-package @org/server --category databases`
2305
+ ).action(async (opts) => {
1958
2306
  try {
1959
- await runInit();
2307
+ await runInit(opts);
1960
2308
  } catch (err) {
2309
+ if (err instanceof CliError) {
2310
+ error(err.message);
2311
+ if (err.fix) {
2312
+ info(`Fix: ${err.fix}`);
2313
+ }
2314
+ process.exit(err.exitCode);
2315
+ }
1961
2316
  error(err instanceof Error ? err.message : String(err));
1962
2317
  process.exit(1);
1963
2318
  }
1964
2319
  });
1965
2320
  }
1966
- async function runInit() {
2321
+ function validateName(val) {
2322
+ if (!val) return "Name is required";
2323
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(val))
2324
+ return "Must be lowercase letters, numbers, and hyphens only";
2325
+ if (val.length > 64) return "Max 64 characters";
2326
+ return true;
2327
+ }
2328
+ function validateDescription(val) {
2329
+ if (!val) return "Description is required";
2330
+ if (val.length > 1024) return "Max 1024 characters";
2331
+ return true;
2332
+ }
2333
+ function validateCategories(cats) {
2334
+ for (const c of cats) {
2335
+ if (!CATEGORIES.includes(c)) {
2336
+ throw usageError(
2337
+ `Invalid category: "${c}"`,
2338
+ `Valid categories: ${CATEGORIES.join(", ")}`
2339
+ );
2340
+ }
2341
+ }
2342
+ }
2343
+ async function runInit(opts) {
1967
2344
  header("Create a new Shipables skill");
1968
2345
  const cwd = process.cwd();
1969
2346
  try {
@@ -1974,86 +2351,164 @@ async function runInit() {
1974
2351
  throw err;
1975
2352
  }
1976
2353
  }
2354
+ const interactive = isInteractive();
2355
+ const hasRequiredFlags = opts.name !== void 0 && opts.description !== void 0;
2356
+ if (!interactive && !hasRequiredFlags) {
2357
+ throw usageError(
2358
+ "Non-interactive environment detected. --name and --description are required.",
2359
+ "Provide required flags: shipables init --name <name> --description <desc>"
2360
+ );
2361
+ }
2362
+ if (hasRequiredFlags) {
2363
+ const nameResult = validateName(opts.name);
2364
+ if (nameResult !== true) {
2365
+ throw usageError(nameResult);
2366
+ }
2367
+ const descResult = validateDescription(opts.description);
2368
+ if (descResult !== true) {
2369
+ throw usageError(descResult);
2370
+ }
2371
+ const name2 = opts.name;
2372
+ const description2 = opts.description;
2373
+ const authorName2 = opts.author ?? process.env.USER ?? "your-name";
2374
+ const authorGithub2 = opts.authorGithub ?? authorName2;
2375
+ const license2 = opts.license ?? "MIT";
2376
+ const categories2 = opts.category ?? [];
2377
+ if (categories2.length > 0) {
2378
+ validateCategories(categories2);
2379
+ }
2380
+ const includeMcp2 = opts.mcpPackage !== void 0;
2381
+ const mcpPackage2 = opts.mcpPackage ?? "";
2382
+ const includeScripts2 = opts.scripts ?? false;
2383
+ const includeReferences2 = opts.references ?? false;
2384
+ await generateFiles({
2385
+ name: name2,
2386
+ description: description2,
2387
+ authorName: authorName2,
2388
+ authorGithub: authorGithub2,
2389
+ license: license2,
2390
+ categories: categories2,
2391
+ includeMcp: includeMcp2,
2392
+ mcpPackage: mcpPackage2,
2393
+ mcpEnvVars: [],
2394
+ includeScripts: includeScripts2,
2395
+ includeReferences: includeReferences2,
2396
+ skillDir: cwd
2397
+ });
2398
+ return;
2399
+ }
1977
2400
  const name = await input2({
1978
2401
  message: "Skill name (lowercase, hyphens, max 64 chars):",
1979
- validate: (val) => {
1980
- if (!val) return "Name is required";
1981
- if (!/^[a-z0-9][a-z0-9-]*$/.test(val))
1982
- return "Must be lowercase letters, numbers, and hyphens only";
1983
- if (val.length > 64) return "Max 64 characters";
1984
- return true;
1985
- }
2402
+ default: opts.name,
2403
+ validate: (val) => validateName(val)
1986
2404
  });
1987
2405
  const description = await input2({
1988
2406
  message: "Description (what the skill does and when to use it):",
1989
- validate: (val) => {
1990
- if (!val) return "Description is required";
1991
- if (val.length > 1024) return "Max 1024 characters";
1992
- return true;
1993
- }
2407
+ default: opts.description,
2408
+ validate: (val) => validateDescription(val)
1994
2409
  });
1995
- const authorName = await input2({
2410
+ const authorName = opts.yes ? opts.author ?? process.env.USER ?? "your-name" : await input2({
1996
2411
  message: "Author name:",
1997
- default: process.env.USER || "your-name"
2412
+ default: opts.author ?? process.env.USER ?? "your-name"
1998
2413
  });
1999
- const authorGithub = await input2({
2414
+ const authorGithub = opts.yes ? opts.authorGithub ?? authorName : await input2({
2000
2415
  message: "Author GitHub username:",
2001
- default: authorName
2416
+ default: opts.authorGithub ?? authorName
2002
2417
  });
2003
- const license = await input2({
2418
+ const license = opts.yes ? opts.license ?? "MIT" : await input2({
2004
2419
  message: "License:",
2005
- default: "MIT"
2420
+ default: opts.license ?? "MIT"
2006
2421
  });
2007
- const categories = await checkbox2({
2422
+ const categories = opts.yes ? opts.category ?? [] : opts.category !== void 0 ? opts.category : await checkbox2({
2008
2423
  message: "Categories (select with space):",
2009
2424
  choices: CATEGORIES.map((c) => ({ name: c, value: c }))
2010
2425
  });
2011
- const includeMcp = await confirm4({
2012
- message: "Include MCP server configuration?",
2013
- default: false
2014
- });
2426
+ if (categories.length > 0) {
2427
+ validateCategories(categories);
2428
+ }
2429
+ let includeMcp;
2015
2430
  let mcpPackage = "";
2016
2431
  let mcpEnvVars = [];
2017
- if (includeMcp) {
2018
- mcpPackage = await input2({
2019
- message: "MCP server npm package name:",
2020
- validate: (val) => val ? true : "Package name is required"
2432
+ if (opts.mcpPackage !== void 0) {
2433
+ includeMcp = true;
2434
+ mcpPackage = opts.mcpPackage;
2435
+ } else if (opts.yes) {
2436
+ includeMcp = false;
2437
+ } else {
2438
+ includeMcp = await confirm4({
2439
+ message: "Include MCP server configuration?",
2440
+ default: false
2021
2441
  });
2022
- let addMore = true;
2023
- while (addMore) {
2024
- const envName = await input2({
2025
- message: "Environment variable name (or empty to finish):"
2026
- });
2027
- if (!envName) break;
2028
- const envDesc = await input2({
2029
- message: `Description for ${envName}:`
2030
- });
2031
- const envRequired = await confirm4({
2032
- message: `Is ${envName} required?`,
2033
- default: true
2034
- });
2035
- const envSecret = await confirm4({
2036
- message: `Is ${envName} a secret?`,
2037
- default: false
2038
- });
2039
- mcpEnvVars.push({
2040
- name: envName,
2041
- description: envDesc,
2042
- required: envRequired,
2043
- secret: envSecret
2442
+ if (includeMcp) {
2443
+ mcpPackage = await input2({
2444
+ message: "MCP server npm package name:",
2445
+ validate: (val) => val ? true : "Package name is required"
2044
2446
  });
2447
+ let addMore = true;
2448
+ while (addMore) {
2449
+ const envName = await input2({
2450
+ message: "Environment variable name (or empty to finish):"
2451
+ });
2452
+ if (!envName) break;
2453
+ const envDesc = await input2({
2454
+ message: `Description for ${envName}:`
2455
+ });
2456
+ const envRequired = await confirm4({
2457
+ message: `Is ${envName} required?`,
2458
+ default: true
2459
+ });
2460
+ const envSecret = await confirm4({
2461
+ message: `Is ${envName} a secret?`,
2462
+ default: false
2463
+ });
2464
+ mcpEnvVars.push({
2465
+ name: envName,
2466
+ description: envDesc,
2467
+ required: envRequired,
2468
+ secret: envSecret
2469
+ });
2470
+ }
2045
2471
  }
2046
2472
  }
2047
- const includeScripts = await confirm4({
2473
+ const includeScripts = opts.scripts !== void 0 ? opts.scripts : opts.yes ? false : await confirm4({
2048
2474
  message: "Include example scripts/ directory?",
2049
2475
  default: false
2050
2476
  });
2051
- const includeReferences = await confirm4({
2477
+ const includeReferences = opts.references !== void 0 ? opts.references : opts.yes ? false : await confirm4({
2052
2478
  message: "Include example references/ directory?",
2053
2479
  default: false
2054
2480
  });
2481
+ await generateFiles({
2482
+ name,
2483
+ description,
2484
+ authorName,
2485
+ authorGithub,
2486
+ license,
2487
+ categories,
2488
+ includeMcp,
2489
+ mcpPackage,
2490
+ mcpEnvVars,
2491
+ includeScripts,
2492
+ includeReferences,
2493
+ skillDir: cwd
2494
+ });
2495
+ }
2496
+ async function generateFiles(opts) {
2497
+ const {
2498
+ name,
2499
+ description,
2500
+ authorName,
2501
+ authorGithub,
2502
+ license,
2503
+ categories,
2504
+ includeMcp,
2505
+ mcpPackage,
2506
+ mcpEnvVars,
2507
+ includeScripts,
2508
+ includeReferences,
2509
+ skillDir
2510
+ } = opts;
2055
2511
  header("Creating files:");
2056
- const skillDir = cwd;
2057
2512
  const skillMd = generateSkillMd(
2058
2513
  name,
2059
2514
  description,
@@ -2118,12 +2573,12 @@ Detailed reference documentation goes here.
2118
2573
  success("references/example.md");
2119
2574
  }
2120
2575
  blank();
2121
- success(`Skill ${chalk9.bold(name)} initialized!`);
2576
+ success(`Skill ${chalk8.bold(name)} initialized!`);
2122
2577
  blank();
2123
2578
  info("Next steps:");
2124
- info(` 1. Edit ${chalk9.cyan("SKILL.md")} with your agent instructions`);
2125
- info(` 2. Edit ${chalk9.cyan("shipables.json")} with your metadata`);
2126
- info(` 3. Run ${chalk9.cyan("shipables publish")} to publish`);
2579
+ info(` 1. Edit ${chalk8.cyan("SKILL.md")} with your agent instructions`);
2580
+ info(` 2. Edit ${chalk8.cyan("shipables.json")} with your metadata`);
2581
+ info(` 3. Run ${chalk8.cyan("shipables publish")} to publish`);
2127
2582
  blank();
2128
2583
  }
2129
2584
  function generateSkillMd(name, description, license, author, includeReferences, includeMcp, mcpPackage) {
@@ -2214,31 +2669,73 @@ function titleCase(s) {
2214
2669
  // src/commands/login.ts
2215
2670
  init_config();
2216
2671
  init_output();
2672
+ init_output();
2673
+ init_errors();
2217
2674
  import { createServer } from "http";
2218
2675
  import { Command as Command10 } from "commander";
2219
- import chalk10 from "chalk";
2220
- import ora7 from "ora";
2676
+ import chalk9 from "chalk";
2221
2677
  function createLoginCommand() {
2222
- return new Command10("login").description("Authenticate with the Shipables registry via GitHub").action(async () => {
2678
+ return new Command10("login").description(
2679
+ "Authenticate with the Shipables registry. Uses GitHub OAuth by default. For CI or AI agents, use --token with a pre-generated API token."
2680
+ ).option(
2681
+ "--token <token>",
2682
+ "Authenticate with an API token (for CI/AI agents). Generate at https://shipables.dev/settings/tokens"
2683
+ ).addHelpText(
2684
+ "after",
2685
+ "\nExamples:\n shipables login Interactive GitHub OAuth\n shipables login --token cbl_xxxxx Non-interactive token auth (CI/AI agents)\n"
2686
+ ).action(async (options) => {
2223
2687
  try {
2224
- await runLogin();
2688
+ await runLogin(options);
2225
2689
  } catch (err) {
2690
+ if (err instanceof CliError) {
2691
+ error(err.message);
2692
+ process.exit(err.exitCode);
2693
+ }
2226
2694
  error(err instanceof Error ? err.message : String(err));
2227
2695
  process.exit(1);
2228
2696
  }
2229
2697
  });
2230
2698
  }
2231
2699
  function createLogoutCommand() {
2232
- return new Command10("logout").description("Remove stored credentials").action(async () => {
2700
+ return new Command10("logout").description("Remove stored credentials from ~/.shipables/config.json").action(async () => {
2233
2701
  try {
2234
2702
  await runLogout();
2235
2703
  } catch (err) {
2704
+ if (err instanceof CliError) {
2705
+ error(err.message);
2706
+ process.exit(err.exitCode);
2707
+ }
2236
2708
  error(err instanceof Error ? err.message : String(err));
2237
2709
  process.exit(1);
2238
2710
  }
2239
2711
  });
2240
2712
  }
2241
- async function runLogin() {
2713
+ async function runLogin(options) {
2714
+ if (options.token) {
2715
+ const registryUrl2 = await getRegistry();
2716
+ const config = await loadConfig();
2717
+ config.token = options.token;
2718
+ try {
2719
+ const resp = await fetch(`${registryUrl2}/api/v1/auth/me`, {
2720
+ headers: {
2721
+ Authorization: `Bearer ${options.token}`,
2722
+ "User-Agent": "shipables-cli"
2723
+ }
2724
+ });
2725
+ if (resp.ok) {
2726
+ const me = await resp.json();
2727
+ config.username = me.username;
2728
+ }
2729
+ } catch {
2730
+ }
2731
+ await saveConfig(config);
2732
+ blank();
2733
+ success(`Logged in as ${chalk9.bold(config.username || "unknown")}`);
2734
+ info(chalk9.dim("Token stored in ~/.shipables/config.json"));
2735
+ info("You can now publish skills with: shipables publish");
2736
+ blank();
2737
+ return;
2738
+ }
2242
2739
  const registryUrl = await getRegistry();
2243
2740
  const { port, tokenPromise, server } = await startCallbackServer();
2244
2741
  const callbackUrl = `http://localhost:${port}/callback`;
@@ -2246,13 +2743,13 @@ async function runLogin() {
2246
2743
  blank();
2247
2744
  info("Open this URL in your browser to sign in with GitHub:");
2248
2745
  blank();
2249
- info(chalk10.cyan(loginUrl));
2746
+ info(chalk9.cyan(loginUrl));
2250
2747
  blank();
2251
2748
  await tryOpenBrowser(loginUrl);
2252
- const spinner = ora7("Waiting for authentication...").start();
2749
+ const spin = spinner("Waiting for authentication...");
2253
2750
  try {
2254
2751
  const token = await tokenPromise;
2255
- spinner.stop();
2752
+ spin.stop();
2256
2753
  const config = await loadConfig();
2257
2754
  config.token = token;
2258
2755
  try {
@@ -2271,14 +2768,14 @@ async function runLogin() {
2271
2768
  await saveConfig(config);
2272
2769
  blank();
2273
2770
  success(
2274
- `Logged in as ${chalk10.bold(config.username || "unknown")}`
2771
+ `Logged in as ${chalk9.bold(config.username || "unknown")}`
2275
2772
  );
2276
2773
  info(
2277
- chalk10.dim("Token stored in ~/.shipables/config.json")
2774
+ chalk9.dim("Token stored in ~/.shipables/config.json")
2278
2775
  );
2279
2776
  blank();
2280
2777
  } catch (err) {
2281
- spinner.fail("Authentication failed");
2778
+ spin.fail("Authentication failed");
2282
2779
  throw err;
2283
2780
  } finally {
2284
2781
  server.close();
@@ -2360,13 +2857,26 @@ init_config();
2360
2857
  init_adapters();
2361
2858
  init_client();
2362
2859
  init_output();
2860
+ init_output();
2861
+ init_errors();
2363
2862
  import { Command as Command11 } from "commander";
2364
- import chalk11 from "chalk";
2863
+ import chalk10 from "chalk";
2365
2864
  function createDoctorCommand() {
2366
- return new Command11("doctor").description("Check health of installed skills and agent configurations").option("-g, --global", "Check globally installed skills").action(async (options) => {
2865
+ return new Command11("doctor").description(
2866
+ "Check health of installed skills and agent configurations. Validates skill directories exist, MCP configs are present, and checks for updates."
2867
+ ).option("-g, --global", "Check globally installed skills").option("--json", "Output results as JSON").addHelpText("after", `
2868
+ Examples:
2869
+ shipables doctor Check all skills in current project
2870
+ shipables doctor --global Check globally installed skills
2871
+ shipables doctor --json Get results as JSON
2872
+ `).action(async (options) => {
2367
2873
  try {
2368
2874
  await runDoctor(options);
2369
2875
  } catch (err) {
2876
+ if (err instanceof CliError) {
2877
+ errorWithFix(err.message, err.fix || "");
2878
+ process.exit(err.exitCode);
2879
+ }
2370
2880
  error(err instanceof Error ? err.message : String(err));
2371
2881
  process.exit(1);
2372
2882
  }
@@ -2377,19 +2887,26 @@ async function runDoctor(options) {
2377
2887
  const installations = await getProjectInstallations(projectPath);
2378
2888
  const entries = Object.entries(installations);
2379
2889
  if (entries.length === 0) {
2890
+ if (options.json || getJsonMode()) {
2891
+ console.log(JSON.stringify({ ok: true, skills: [], issues: 0, updates: 0 }, null, 2));
2892
+ return;
2893
+ }
2380
2894
  blank();
2381
2895
  info("No skills installed to check.");
2896
+ info("Install a skill with: shipables install <skill>");
2382
2897
  blank();
2383
2898
  return;
2384
2899
  }
2385
2900
  header("Checking installed skills...");
2386
2901
  let totalIssues = 0;
2387
2902
  let totalUpdates = 0;
2903
+ const jsonResults = [];
2388
2904
  for (const [skillName, record] of entries) {
2389
2905
  const agentList = record.agents.join(", ");
2390
2906
  console.log(
2391
- ` ${chalk11.bold(`${skillName}@${record.version}`)} (${agentList}):`
2907
+ ` ${chalk10.bold(`${skillName}@${record.version}`)} (${agentList}):`
2392
2908
  );
2909
+ const skillChecks = [];
2393
2910
  for (const agentName of record.agents) {
2394
2911
  const adapter = getAdapter(agentName);
2395
2912
  if (!adapter) continue;
@@ -2398,6 +2915,7 @@ async function runDoctor(options) {
2398
2915
  if (skillDir) {
2399
2916
  const result = await adapter.validate(skillName, skillDir, mcpConfig);
2400
2917
  for (const check of result.checks) {
2918
+ skillChecks.push({ agent: agentName, ...check });
2401
2919
  if (check.status === "pass") {
2402
2920
  success(check.label);
2403
2921
  } else if (check.status === "fail") {
@@ -2409,9 +2927,11 @@ async function runDoctor(options) {
2409
2927
  }
2410
2928
  }
2411
2929
  }
2930
+ let updateAvailable = null;
2412
2931
  try {
2413
2932
  const detail = await registry.getSkillDetail(skillName);
2414
2933
  if (detail.latest_version !== record.version) {
2934
+ updateAvailable = detail.latest_version;
2415
2935
  warn(
2416
2936
  `Update available: ${record.version} \u2192 ${detail.latest_version}`
2417
2937
  );
@@ -2419,10 +2939,21 @@ async function runDoctor(options) {
2419
2939
  }
2420
2940
  } catch {
2421
2941
  }
2942
+ jsonResults.push({
2943
+ name: skillName,
2944
+ version: record.version,
2945
+ agents: record.agents,
2946
+ checks: skillChecks,
2947
+ update_available: updateAvailable
2948
+ });
2422
2949
  blank();
2423
2950
  }
2951
+ if (options.json || getJsonMode()) {
2952
+ console.log(JSON.stringify({ ok: totalIssues === 0, skills: jsonResults, issues: totalIssues, updates: totalUpdates }, null, 2));
2953
+ return;
2954
+ }
2424
2955
  if (totalIssues === 0 && totalUpdates === 0) {
2425
- success("All checks passed");
2956
+ success("All checks passed.");
2426
2957
  } else {
2427
2958
  const parts = [];
2428
2959
  if (totalIssues > 0) {
@@ -2434,6 +2965,12 @@ async function runDoctor(options) {
2434
2965
  );
2435
2966
  }
2436
2967
  info(`Summary: ${parts.join(", ")}`);
2968
+ if (totalIssues > 0) {
2969
+ info("Fix issues by re-installing: shipables install <skill> --<agent>");
2970
+ }
2971
+ if (totalUpdates > 0) {
2972
+ info("Apply updates with: shipables update");
2973
+ }
2437
2974
  }
2438
2975
  blank();
2439
2976
  }
@@ -2441,15 +2978,29 @@ async function runDoctor(options) {
2441
2978
  // src/commands/config.ts
2442
2979
  init_config();
2443
2980
  init_output();
2981
+ init_errors();
2444
2982
  import { Command as Command12 } from "commander";
2445
- import chalk12 from "chalk";
2983
+ import chalk11 from "chalk";
2446
2984
  var VALID_KEYS = ["registry", "defaultAgents", "scope"];
2447
2985
  function createConfigCommand() {
2448
- const cmd = new Command12("config").description("Manage CLI configuration");
2449
- cmd.command("set <key> <value>").description("Set a config value").action(async (key, value) => {
2986
+ const cmd = new Command12("config").description(
2987
+ "Manage CLI configuration. Config is stored in ~/.shipables/config.json. Valid keys: registry, defaultAgents, scope."
2988
+ ).addHelpText("after", `
2989
+ Examples:
2990
+ shipables config list Show all config values
2991
+ shipables config get registry Get the registry URL
2992
+ shipables config set registry https://... Set a custom registry URL
2993
+ shipables config set defaultAgents claude,cursor Set default agents
2994
+ shipables config delete scope Remove a config value
2995
+ `);
2996
+ cmd.command("set <key> <value>").description("Set a config value (valid keys: registry, defaultAgents, scope)").action(async (key, value) => {
2450
2997
  try {
2451
2998
  await runConfigSet(key, value);
2452
2999
  } catch (err) {
3000
+ if (err instanceof CliError) {
3001
+ errorWithFix(err.message, err.fix || "");
3002
+ process.exit(err.exitCode);
3003
+ }
2453
3004
  error(err instanceof Error ? err.message : String(err));
2454
3005
  process.exit(1);
2455
3006
  }
@@ -2458,11 +3009,15 @@ function createConfigCommand() {
2458
3009
  try {
2459
3010
  await runConfigGet(key);
2460
3011
  } catch (err) {
3012
+ if (err instanceof CliError) {
3013
+ errorWithFix(err.message, err.fix || "");
3014
+ process.exit(err.exitCode);
3015
+ }
2461
3016
  error(err instanceof Error ? err.message : String(err));
2462
3017
  process.exit(1);
2463
3018
  }
2464
3019
  });
2465
- cmd.command("list").description("Show all config").action(async () => {
3020
+ cmd.command("list").description("Show all config values").action(async () => {
2466
3021
  try {
2467
3022
  await runConfigList();
2468
3023
  } catch (err) {
@@ -2474,6 +3029,10 @@ function createConfigCommand() {
2474
3029
  try {
2475
3030
  await runConfigDelete(key);
2476
3031
  } catch (err) {
3032
+ if (err instanceof CliError) {
3033
+ errorWithFix(err.message, err.fix || "");
3034
+ process.exit(err.exitCode);
3035
+ }
2477
3036
  error(err instanceof Error ? err.message : String(err));
2478
3037
  process.exit(1);
2479
3038
  }
@@ -2482,8 +3041,9 @@ function createConfigCommand() {
2482
3041
  }
2483
3042
  function validateKey(key) {
2484
3043
  if (!VALID_KEYS.includes(key)) {
2485
- throw new Error(
2486
- `Invalid config key: ${key}. Valid keys: ${VALID_KEYS.join(", ")}`
3044
+ throw usageError(
3045
+ `Invalid config key: '${key}'.`,
3046
+ `Valid keys: ${VALID_KEYS.join(", ")}. Example: shipables config set registry https://api.shipables.dev`
2487
3047
  );
2488
3048
  }
2489
3049
  return key;
@@ -2516,15 +3076,15 @@ async function runConfigList() {
2516
3076
  blank();
2517
3077
  for (const key of VALID_KEYS) {
2518
3078
  const value = config[key];
2519
- const display = value === void 0 ? chalk12.dim("(not set)") : typeof value === "object" ? JSON.stringify(value) : String(value);
2520
- console.log(` ${chalk12.bold(key)}: ${display}`);
3079
+ const display = value === void 0 ? chalk11.dim("(not set)") : typeof value === "object" ? JSON.stringify(value) : String(value);
3080
+ console.log(` ${chalk11.bold(key)}: ${display}`);
2521
3081
  }
2522
3082
  if (config.username) {
2523
3083
  console.log(
2524
- ` ${chalk12.bold("auth")}: logged in as ${chalk12.green(config.username)}`
3084
+ ` ${chalk11.bold("auth")}: logged in as ${chalk11.green(config.username)}`
2525
3085
  );
2526
3086
  } else {
2527
- console.log(` ${chalk12.bold("auth")}: ${chalk12.dim("not logged in")}`);
3087
+ console.log(` ${chalk11.bold("auth")}: ${chalk11.dim("not logged in")}`);
2528
3088
  }
2529
3089
  blank();
2530
3090
  }
@@ -2539,30 +3099,42 @@ async function runConfigDelete(key) {
2539
3099
  // src/commands/stats.ts
2540
3100
  init_client();
2541
3101
  init_output();
3102
+ init_output();
3103
+ init_errors();
2542
3104
  import { Command as Command13 } from "commander";
2543
- import chalk13 from "chalk";
2544
- import ora8 from "ora";
3105
+ import chalk12 from "chalk";
2545
3106
  function createStatsCommand() {
2546
- return new Command13("stats").argument("<skill>", "Skill name").option("--period <period>", "Time period: week, month, year", "month").option("--json", "Output as JSON").description("Show download statistics for a skill").action(async (skillName, options) => {
3107
+ return new Command13("stats").argument("<skill>", "Skill name (e.g., Chippers255/ai-commits, @org/my-skill)").option("--period <period>", "Time period: week, month, year", "month").option("--json", "Output as JSON").description(
3108
+ "Show download statistics for a skill with a daily breakdown chart."
3109
+ ).addHelpText("after", `
3110
+ Examples:
3111
+ shipables stats Chippers255/ai-commits Show monthly download stats
3112
+ shipables stats Chippers255/ai-commits --period week Show weekly stats
3113
+ shipables stats @org/tool --json Get stats as JSON
3114
+ `).action(async (skillName, options) => {
2547
3115
  try {
2548
3116
  await runStats(skillName, options);
2549
3117
  } catch (err) {
3118
+ if (err instanceof CliError) {
3119
+ errorWithFix(err.message, err.fix || "");
3120
+ process.exit(err.exitCode);
3121
+ }
2550
3122
  error(err instanceof Error ? err.message : String(err));
2551
3123
  process.exit(1);
2552
3124
  }
2553
3125
  });
2554
3126
  }
2555
3127
  async function runStats(skillName, options) {
2556
- const spinner = ora8(`Fetching download stats for ${skillName}...`).start();
3128
+ const statsSpinner = spinner(`Fetching download stats for ${skillName}...`);
2557
3129
  const stats = await registry.getDownloadStats(skillName, options.period);
2558
- spinner.stop();
2559
- if (options.json) {
3130
+ statsSpinner.stop();
3131
+ if (options.json || getJsonMode()) {
2560
3132
  console.log(JSON.stringify(stats, null, 2));
2561
3133
  return;
2562
3134
  }
2563
3135
  blank();
2564
3136
  console.log(
2565
- ` ${chalk13.bold(stats.skill)} \u2014 ${stats.period} downloads: ${chalk13.bold(formatNumber(stats.total))}`
3137
+ ` ${chalk12.bold(stats.skill)} \u2014 ${stats.period} downloads: ${chalk12.bold(formatNumber(stats.total))}`
2566
3138
  );
2567
3139
  blank();
2568
3140
  if (stats.daily.length > 0) {
@@ -2572,7 +3144,7 @@ async function runStats(skillName, options) {
2572
3144
  const bar = "\u2588".repeat(Math.round(day.count / maxCount * barWidth));
2573
3145
  const date = day.date.slice(5);
2574
3146
  const count = String(day.count).padStart(6);
2575
- console.log(` ${chalk13.dim(date)} ${count} ${chalk13.cyan(bar)}`);
3147
+ console.log(` ${chalk12.dim(date)} ${count} ${chalk12.cyan(bar)}`);
2576
3148
  }
2577
3149
  }
2578
3150
  blank();
@@ -2582,14 +3154,26 @@ async function runStats(skillName, options) {
2582
3154
  init_client();
2583
3155
  init_config();
2584
3156
  init_output();
3157
+ init_output();
3158
+ init_errors();
2585
3159
  import { Command as Command14 } from "commander";
2586
- import chalk14 from "chalk";
2587
- import ora9 from "ora";
3160
+ import chalk13 from "chalk";
2588
3161
  function createProfileCommand() {
2589
- return new Command14("profile").argument("[username]", "Username to look up (defaults to current user)").option("--json", "Output as JSON").description("Show user profile and published skills").action(async (username, options) => {
3162
+ return new Command14("profile").argument("[username]", "Username to look up (defaults to current user)").option("--json", "Output as JSON").description(
3163
+ "Show user profile and published skills. Defaults to the currently logged-in user if no username is provided."
3164
+ ).addHelpText("after", `
3165
+ Examples:
3166
+ shipables profile Show your profile
3167
+ shipables profile johndoe Show another user's profile
3168
+ shipables profile --json Get your profile as JSON
3169
+ `).action(async (username, options) => {
2590
3170
  try {
2591
3171
  await runProfile(username, options);
2592
3172
  } catch (err) {
3173
+ if (err instanceof CliError) {
3174
+ errorWithFix(err.message, err.fix || "");
3175
+ process.exit(err.exitCode);
3176
+ }
2593
3177
  error(err instanceof Error ? err.message : String(err));
2594
3178
  process.exit(1);
2595
3179
  }
@@ -2603,29 +3187,30 @@ async function runProfile(username, options) {
2603
3187
  } else {
2604
3188
  const token = await getToken();
2605
3189
  if (!token) {
2606
- throw new Error(
2607
- "No username provided and not logged in. Run `shipables login` or provide a username."
3190
+ throw authError(
3191
+ "No username provided and not logged in.",
3192
+ "Run `shipables login` first, or provide a username: shipables profile <username>"
2608
3193
  );
2609
3194
  }
2610
3195
  const me = await registry.getMe();
2611
3196
  username = me.username;
2612
3197
  }
2613
3198
  }
2614
- const spinner = ora9(`Fetching profile for ${username}...`).start();
3199
+ const profileSpinner = spinner(`Fetching profile for ${username}...`);
2615
3200
  const [profile, skillsResult] = await Promise.all([
2616
3201
  registry.getUserProfile(username),
2617
3202
  registry.getUserSkills(username)
2618
3203
  ]);
2619
- spinner.stop();
2620
- if (options.json) {
3204
+ profileSpinner.stop();
3205
+ if (options.json || getJsonMode()) {
2621
3206
  console.log(JSON.stringify({ profile, skills: skillsResult.skills }, null, 2));
2622
3207
  return;
2623
3208
  }
2624
3209
  blank();
2625
3210
  const displayName = profile.display_name || profile.username;
2626
- console.log(` ${chalk14.bold(displayName)}`);
3211
+ console.log(` ${chalk13.bold(displayName)}`);
2627
3212
  if (profile.display_name && profile.display_name !== profile.username) {
2628
- console.log(` ${chalk14.dim(`@${profile.username}`)}`);
3213
+ console.log(` ${chalk13.dim(`@${profile.username}`)}`);
2629
3214
  }
2630
3215
  blank();
2631
3216
  info(`Skills published: ${profile.skills_count}`);
@@ -2651,8 +3236,32 @@ function truncate2(s, max) {
2651
3236
  }
2652
3237
 
2653
3238
  // src/index.ts
3239
+ init_output();
2654
3240
  var program = new Command15();
2655
- program.name("shipables").description("CLI for installing, managing, and publishing AI agent skills").version("0.1.0");
3241
+ program.name("shipables").description(
3242
+ "CLI for installing, managing, and publishing AI agent skills. Works with Claude Code, Cursor, Codex, Copilot, Gemini, and Cline. Skills follow the Agent Skills open standard (https://agentskills.io)."
3243
+ ).version("0.1.0").option("--json", "Output structured JSON (supported by most commands)").on("option:json", () => {
3244
+ setJsonMode(true);
3245
+ }).addHelpText("after", `
3246
+ Examples:
3247
+ shipables search "database" Search for database-related skills
3248
+ shipables install Chippers255/ai-commits --claude Install a skill for Claude Code
3249
+ shipables install @org/tool --all Install a scoped skill for all agents
3250
+ shipables list List skills installed in current project
3251
+ shipables info Chippers255/ai-commits --json Get skill details as JSON
3252
+ shipables init --name my-skill --description "A skill for X"
3253
+ Scaffold a new skill (non-interactive)
3254
+ shipables publish --yes --scope @myorg Publish without prompts
3255
+ shipables login --token cbl_xxxxx Non-interactive auth for CI/AI agents
3256
+
3257
+ Non-interactive usage (CI/AI agents):
3258
+ Most commands work non-interactively. Use flags instead of prompts:
3259
+ - install: --claude/--cursor/--all for agents, --env KEY=VAL for MCP vars
3260
+ - init: --name and --description skip all prompts
3261
+ - publish: --yes skips confirmation, --scope sets org
3262
+ - login: --token skips browser OAuth
3263
+ - All commands: --json for structured output
3264
+ `);
2656
3265
  program.addCommand(createInstallCommand());
2657
3266
  program.addCommand(createUninstallCommand());
2658
3267
  program.addCommand(createSearchCommand());