@shipwellapp/cli 0.1.1 → 0.2.0

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 +397 -6
  2. package/package.json +1 -2
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import chalk7 from "chalk";
5
6
 
6
7
  // src/commands/analyze.ts
7
8
  import ora from "ora";
@@ -735,6 +736,66 @@ function extractTag(text, tag) {
735
736
  return match ? match[1].trim() : null;
736
737
  }
737
738
 
739
+ // src/lib/store.ts
740
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
741
+ import { join as join2 } from "path";
742
+ import { homedir } from "os";
743
+ var CONFIG_DIR = join2(homedir(), ".shipwell");
744
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
745
+ function ensureDir() {
746
+ if (!existsSync(CONFIG_DIR)) {
747
+ mkdirSync(CONFIG_DIR, { recursive: true });
748
+ }
749
+ }
750
+ function loadConfig() {
751
+ try {
752
+ ensureDir();
753
+ if (existsSync(CONFIG_FILE)) {
754
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
755
+ }
756
+ } catch {
757
+ }
758
+ return {};
759
+ }
760
+ function saveConfig(config2) {
761
+ ensureDir();
762
+ writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
763
+ }
764
+ function getUser() {
765
+ return loadConfig().user;
766
+ }
767
+ function setUser(user) {
768
+ const config2 = loadConfig();
769
+ config2.user = user;
770
+ saveConfig(config2);
771
+ }
772
+ function clearUser() {
773
+ const config2 = loadConfig();
774
+ delete config2.user;
775
+ saveConfig(config2);
776
+ }
777
+ function getApiKey() {
778
+ return loadConfig().apiKey;
779
+ }
780
+ function setApiKey(key) {
781
+ const config2 = loadConfig();
782
+ config2.apiKey = key;
783
+ saveConfig(config2);
784
+ }
785
+ function clearApiKey() {
786
+ const config2 = loadConfig();
787
+ delete config2.apiKey;
788
+ saveConfig(config2);
789
+ }
790
+ function getModel() {
791
+ return loadConfig().model;
792
+ }
793
+ function setModel(model) {
794
+ const config2 = loadConfig();
795
+ config2.model = model;
796
+ saveConfig(config2);
797
+ }
798
+
738
799
  // src/commands/analyze.ts
739
800
  var accent = chalk.hex("#6366f1");
740
801
  var dim = chalk.dim;
@@ -775,14 +836,21 @@ function formatMetric(m) {
775
836
  return ` ${dim("\u2022")} ${m.label}: ${chalk.red(m.before)} ${dim("\u2192")} ${chalk.green(m.after)}${m.unit ? dim(` ${m.unit}`) : ""}`;
776
837
  }
777
838
  async function analyzeCommand(operation, source, options) {
778
- const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY;
839
+ const user = getUser();
840
+ if (!user) {
841
+ console.error(chalk.red("\n Error: Not logged in.\n"));
842
+ console.error(dim(" Run ") + chalk.cyan("shipwell login") + dim(" to sign in with Google.\n"));
843
+ process.exit(1);
844
+ }
845
+ const apiKey = options.apiKey || process.env.ANTHROPIC_API_KEY || getApiKey();
779
846
  if (!apiKey) {
780
847
  console.error(chalk.red("\n Error: Anthropic API key is required.\n"));
781
- console.error(dim(" Set ANTHROPIC_API_KEY env var or use --api-key flag"));
782
- console.error(dim(" Example: shipwell audit ./my-repo --api-key sk-ant-...\n"));
848
+ console.error(dim(" Set it with: ") + chalk.cyan("shipwell config set api-key sk-ant-..."));
849
+ console.error(dim(" Or pass it: ") + chalk.cyan("shipwell audit ./repo --api-key sk-ant-..."));
850
+ console.error(dim(" Or set env: ") + chalk.cyan("export ANTHROPIC_API_KEY=sk-ant-...\n"));
783
851
  process.exit(1);
784
852
  }
785
- const model = options.model || process.env.SHIPWELL_MODEL || "claude-sonnet-4-5-20250929";
853
+ const model = options.model || process.env.SHIPWELL_MODEL || getModel() || "claude-sonnet-4-5-20250929";
786
854
  const startTime = Date.now();
787
855
  console.log();
788
856
  console.log(accent(" \u26F5 Shipwell"), dim("\u2014 Full Codebase Autopilot"));
@@ -879,9 +947,292 @@ async function analyzeCommand(operation, source, options) {
879
947
  console.log();
880
948
  }
881
949
 
950
+ // src/commands/login.ts
951
+ import chalk2 from "chalk";
952
+ import ora2 from "ora";
953
+
954
+ // src/lib/auth.ts
955
+ import http from "http";
956
+ import { exec } from "child_process";
957
+ import { platform } from "os";
958
+ function openBrowser(url) {
959
+ const plat = platform();
960
+ if (plat === "darwin") exec(`open "${url}"`);
961
+ else if (plat === "win32") exec(`start "" "${url}"`);
962
+ else exec(`xdg-open "${url}"`);
963
+ }
964
+ function startAuthFlow(baseUrl) {
965
+ return new Promise((resolve, reject) => {
966
+ const server = http.createServer((req, res) => {
967
+ const url = new URL(req.url || "/", "http://localhost");
968
+ if (url.pathname === "/callback") {
969
+ const name = url.searchParams.get("name") || "";
970
+ const email = url.searchParams.get("email") || "";
971
+ const uid = url.searchParams.get("uid") || "";
972
+ const photo = url.searchParams.get("photo") || void 0;
973
+ res.writeHead(200, { "Content-Type": "text/html" });
974
+ res.end(`<!DOCTYPE html>
975
+ <html>
976
+ <head><title>Shipwell</title>
977
+ <style>
978
+ body { background: #0a0a0f; color: #e4e4e7; font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
979
+ .card { text-align: center; padding: 3rem; }
980
+ .icon { font-size: 3rem; margin-bottom: 1rem; }
981
+ h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
982
+ p { color: #71717a; font-size: 0.875rem; margin: 0.25rem 0; }
983
+ .name { color: #818cf8; font-weight: 600; }
984
+ .hint { margin-top: 1.5rem; padding: 1rem; background: #18181b; border-radius: 0.75rem; border: 1px solid #27272a; }
985
+ code { color: #22d3ee; font-size: 0.8rem; }
986
+ </style>
987
+ </head>
988
+ <body>
989
+ <div class="card">
990
+ <div class="icon">\u26F5</div>
991
+ <h1>Welcome to Shipwell</h1>
992
+ <p>Logged in as <span class="name">${name}</span></p>
993
+ <p style="margin-top: 1rem; color: #52525b;">You can close this tab and return to your terminal.</p>
994
+ <div class="hint">
995
+ <p style="color: #a1a1aa; margin-bottom: 0.5rem;">Next, set your API key:</p>
996
+ <code>shipwell config set api-key sk-ant-...</code>
997
+ </div>
998
+ </div>
999
+ </body>
1000
+ </html>`);
1001
+ server.close();
1002
+ resolve({ name, email, uid, photo });
1003
+ } else {
1004
+ res.writeHead(404);
1005
+ res.end("Not found");
1006
+ }
1007
+ });
1008
+ server.listen(0, "127.0.0.1", () => {
1009
+ const addr = server.address();
1010
+ const port = typeof addr === "object" && addr ? addr.port : 0;
1011
+ openBrowser(`${baseUrl}/cli-auth?port=${port}`);
1012
+ });
1013
+ setTimeout(() => {
1014
+ server.close();
1015
+ reject(new Error("Authentication timed out (5 minutes)"));
1016
+ }, 5 * 60 * 1e3);
1017
+ });
1018
+ }
1019
+
1020
+ // src/commands/login.ts
1021
+ var accent2 = chalk2.hex("#6366f1");
1022
+ async function loginCommand() {
1023
+ const existing = getUser();
1024
+ if (existing) {
1025
+ console.log();
1026
+ console.log(` Already logged in as ${accent2(existing.name)} (${chalk2.dim(existing.email)})`);
1027
+ console.log(chalk2.dim(" Run 'shipwell logout' first to switch accounts."));
1028
+ console.log();
1029
+ return;
1030
+ }
1031
+ console.log();
1032
+ console.log(` ${accent2("\u26F5")} Opening browser to sign in...`);
1033
+ console.log();
1034
+ const spinner = ora2({ text: "Waiting for authentication...", color: "cyan", prefixText: " " }).start();
1035
+ try {
1036
+ const result = await startAuthFlow("https://shipwell.app");
1037
+ setUser(result);
1038
+ spinner.succeed(`Logged in as ${accent2(result.name)} (${chalk2.dim(result.email)})`);
1039
+ console.log();
1040
+ console.log(chalk2.dim(" Next, set your API key:"));
1041
+ console.log(` ${chalk2.cyan("shipwell config set api-key")} ${chalk2.dim("sk-ant-...")}`);
1042
+ console.log();
1043
+ } catch (err) {
1044
+ spinner.fail(err.message);
1045
+ process.exit(1);
1046
+ }
1047
+ }
1048
+
1049
+ // src/commands/logout.ts
1050
+ import chalk3 from "chalk";
1051
+ var accent3 = chalk3.hex("#6366f1");
1052
+ function logoutCommand() {
1053
+ const user = getUser();
1054
+ if (!user) {
1055
+ console.log();
1056
+ console.log(chalk3.dim(" Not logged in."));
1057
+ console.log();
1058
+ return;
1059
+ }
1060
+ clearUser();
1061
+ console.log();
1062
+ console.log(` ${chalk3.green("\u2713")} Logged out ${accent3(user.name)}`);
1063
+ console.log();
1064
+ }
1065
+
1066
+ // src/commands/whoami.ts
1067
+ import chalk4 from "chalk";
1068
+ var accent4 = chalk4.hex("#6366f1");
1069
+ var dim2 = chalk4.dim;
1070
+ function whoamiCommand() {
1071
+ const user = getUser();
1072
+ const apiKey = getApiKey();
1073
+ const model = getModel();
1074
+ console.log();
1075
+ if (user) {
1076
+ console.log(` ${accent4("\u26F5")} ${chalk4.bold(user.name)}`);
1077
+ console.log(` ${dim2(user.email)}`);
1078
+ } else {
1079
+ console.log(` ${dim2("Not logged in.")} Run ${chalk4.cyan("shipwell login")} to sign in.`);
1080
+ }
1081
+ console.log();
1082
+ console.log(` ${dim2("API Key")} ${apiKey ? chalk4.green("\u25CF") + " configured " + dim2(`(${apiKey.slice(0, 12)}...)`) : chalk4.yellow("\u25CF") + " not set"}`);
1083
+ console.log(` ${dim2("Model")} ${accent4(model || DEFAULT_MODEL)}${!model ? dim2(" (default)") : ""}`);
1084
+ console.log(` ${dim2("Config")} ${dim2("~/.shipwell/config.json")}`);
1085
+ console.log();
1086
+ }
1087
+
1088
+ // src/commands/config-cmd.ts
1089
+ import chalk5 from "chalk";
1090
+ var accent5 = chalk5.hex("#6366f1");
1091
+ var dim3 = chalk5.dim;
1092
+ function configShowCommand() {
1093
+ const config2 = loadConfig();
1094
+ console.log();
1095
+ console.log(` ${chalk5.bold("Configuration")} ${dim3("~/.shipwell/config.json")}`);
1096
+ console.log();
1097
+ console.log(` ${dim3("api-key")} ${config2.apiKey ? chalk5.green("\u25CF") + " " + dim3(`${config2.apiKey.slice(0, 12)}...`) : chalk5.yellow("\u25CF") + " not set"}`);
1098
+ console.log(` ${dim3("model")} ${accent5(config2.model || DEFAULT_MODEL)}${!config2.model ? dim3(" (default)") : ""}`);
1099
+ if (config2.user) {
1100
+ console.log(` ${dim3("user")} ${config2.user.name} ${dim3(`(${config2.user.email})`)}`);
1101
+ }
1102
+ console.log();
1103
+ console.log(dim3(" Set values:"));
1104
+ console.log(` ${chalk5.cyan("shipwell config set api-key")} ${dim3("<key>")}`);
1105
+ console.log(` ${chalk5.cyan("shipwell config set model")} ${dim3("<model-id>")}`);
1106
+ console.log();
1107
+ }
1108
+ function configSetCommand(key, value) {
1109
+ switch (key) {
1110
+ case "api-key": {
1111
+ if (!value.startsWith("sk-ant-")) {
1112
+ console.log(chalk5.yellow("\n Warning: API key doesn't look like an Anthropic key (expected sk-ant-...).\n"));
1113
+ }
1114
+ setApiKey(value);
1115
+ console.log(`
1116
+ ${chalk5.green("\u2713")} API key saved
1117
+ `);
1118
+ break;
1119
+ }
1120
+ case "model": {
1121
+ const validIds = AVAILABLE_MODELS.map((m) => m.id);
1122
+ if (!validIds.includes(value)) {
1123
+ console.error(chalk5.red(`
1124
+ Unknown model: ${value}`));
1125
+ console.error(dim3(` Available: ${validIds.join(", ")}
1126
+ `));
1127
+ process.exit(1);
1128
+ }
1129
+ setModel(value);
1130
+ console.log(`
1131
+ ${chalk5.green("\u2713")} Default model set to ${accent5(value)}
1132
+ `);
1133
+ break;
1134
+ }
1135
+ default:
1136
+ console.error(chalk5.red(`
1137
+ Unknown config key: ${key}`));
1138
+ console.error(dim3(` Available keys: api-key, model
1139
+ `));
1140
+ process.exit(1);
1141
+ }
1142
+ }
1143
+ function configDeleteCommand(key) {
1144
+ switch (key) {
1145
+ case "api-key":
1146
+ clearApiKey();
1147
+ console.log(`
1148
+ ${chalk5.green("\u2713")} API key removed
1149
+ `);
1150
+ break;
1151
+ case "model":
1152
+ setModel("");
1153
+ console.log(`
1154
+ ${chalk5.green("\u2713")} Model reset to default (${DEFAULT_MODEL})
1155
+ `);
1156
+ break;
1157
+ default:
1158
+ console.error(chalk5.red(`
1159
+ Unknown config key: ${key}`));
1160
+ process.exit(1);
1161
+ }
1162
+ }
1163
+
1164
+ // src/commands/models.ts
1165
+ import chalk6 from "chalk";
1166
+ var accent6 = chalk6.hex("#6366f1");
1167
+ var dim4 = chalk6.dim;
1168
+ function modelsCommand() {
1169
+ const currentModel = getModel() || DEFAULT_MODEL;
1170
+ console.log();
1171
+ console.log(` ${chalk6.bold("Available Models")}`);
1172
+ console.log();
1173
+ for (const m of AVAILABLE_MODELS) {
1174
+ const isCurrent = m.id === currentModel;
1175
+ const marker = isCurrent ? accent6("\u25CF") : dim4("\u25CB");
1176
+ const label = isCurrent ? chalk6.bold(m.label) : m.label;
1177
+ const id = dim4(m.id);
1178
+ const ctx = dim4(`${Math.round(m.contextWindow / 1e3)}K context`);
1179
+ const dflt = "default" in m && m.default ? chalk6.green(" default") : "";
1180
+ const active = isCurrent ? accent6(" \u2190 active") : "";
1181
+ console.log(` ${marker} ${label} ${id} ${ctx}${dflt}${active}`);
1182
+ }
1183
+ console.log();
1184
+ console.log(dim4(` Switch: shipwell config set model <model-id>`));
1185
+ console.log();
1186
+ }
1187
+
882
1188
  // src/index.ts
1189
+ var VERSION = "0.2.0";
1190
+ var accent7 = chalk7.hex("#6366f1");
1191
+ var dim5 = chalk7.dim;
1192
+ function showBanner() {
1193
+ const user = getUser();
1194
+ const apiKey = getApiKey();
1195
+ console.log();
1196
+ console.log(` ${accent7("\u26F5")} ${chalk7.bold("Shipwell")} ${dim5(`v${VERSION}`)}`);
1197
+ console.log(` ${dim5("Full Codebase Autopilot \u2014 powered by Claude")}`);
1198
+ console.log();
1199
+ if (!user) {
1200
+ console.log(` ${chalk7.yellow("\u25CF")} Not logged in`);
1201
+ console.log(` ${dim5("Get started:")} ${chalk7.cyan("shipwell login")}`);
1202
+ console.log();
1203
+ } else if (!apiKey) {
1204
+ console.log(` ${chalk7.green("\u25CF")} ${user.name} ${dim5(`(${user.email})`)}`);
1205
+ console.log(` ${chalk7.yellow("\u25CF")} API key not set`);
1206
+ console.log(` ${dim5("Set it:")} ${chalk7.cyan("shipwell config set api-key")} ${dim5("sk-ant-...")}`);
1207
+ console.log();
1208
+ } else {
1209
+ console.log(` ${chalk7.green("\u25CF")} ${user.name} ${dim5(`(${user.email})`)}`);
1210
+ console.log(` ${chalk7.green("\u25CF")} API key configured`);
1211
+ console.log();
1212
+ }
1213
+ console.log(` ${chalk7.bold("Analysis Commands")}`);
1214
+ console.log(` ${chalk7.cyan("shipwell audit")} ${dim5("<path>")} Security audit`);
1215
+ console.log(` ${chalk7.cyan("shipwell migrate")} ${dim5("<path>")} Migration plan`);
1216
+ console.log(` ${chalk7.cyan("shipwell refactor")} ${dim5("<path>")} Refactor analysis`);
1217
+ console.log(` ${chalk7.cyan("shipwell docs")} ${dim5("<path>")} Generate documentation`);
1218
+ console.log(` ${chalk7.cyan("shipwell upgrade")} ${dim5("<path>")} Dependency upgrade plan`);
1219
+ console.log();
1220
+ console.log(` ${chalk7.bold("Account & Config")}`);
1221
+ console.log(` ${chalk7.cyan("shipwell login")} Sign in with Google`);
1222
+ console.log(` ${chalk7.cyan("shipwell logout")} Sign out`);
1223
+ console.log(` ${chalk7.cyan("shipwell whoami")} Show current user & config`);
1224
+ console.log(` ${chalk7.cyan("shipwell config")} View configuration`);
1225
+ console.log(` ${chalk7.cyan("shipwell config set")} ${dim5("<k> <v>")} Set a config value`);
1226
+ console.log(` ${chalk7.cyan("shipwell models")} List available models`);
1227
+ console.log(` ${chalk7.cyan("shipwell update")} Update to latest version`);
1228
+ console.log();
1229
+ console.log(` ${dim5("Docs: https://shipwell.app \xB7 v" + VERSION)}`);
1230
+ console.log();
1231
+ }
883
1232
  var program = new Command();
884
- program.name("shipwell").description("Full Codebase Autopilot \u2014 deep cross-file analysis powered by Claude").version("0.1.0");
1233
+ program.name("shipwell").description("Full Codebase Autopilot \u2014 deep cross-file analysis powered by Claude").version(VERSION).action(() => {
1234
+ showBanner();
1235
+ });
885
1236
  var operations = ["audit", "migrate", "refactor", "docs", "upgrade"];
886
1237
  var opDesc = {
887
1238
  audit: "Run a security audit on a codebase",
@@ -891,8 +1242,48 @@ var opDesc = {
891
1242
  upgrade: "Analyze dependencies & plan safe upgrades"
892
1243
  };
893
1244
  for (const op of operations) {
894
- program.command(op).description(opDesc[op] || `Run ${op} analysis on a codebase`).argument("<source>", "Local path or GitHub URL").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env var)").option("-m, --model <model>", "Claude model to use (or set SHIPWELL_MODEL env var)").option("-t, --target <target>", "Migration target (for migrate operation)").option("-c, --context <context>", "Additional context for the analysis").option("-r, --raw", "Also print raw streaming output").action((source, options) => {
1245
+ program.command(op).description(opDesc[op] || `Run ${op} analysis on a codebase`).argument("<source>", "Local path or GitHub URL").option("-k, --api-key <key>", "Anthropic API key").option("-m, --model <model>", "Claude model to use").option("-t, --target <target>", "Migration target (for migrate)").option("-c, --context <context>", "Additional context for the analysis").option("-r, --raw", "Also print raw streaming output").action((source, options) => {
895
1246
  analyzeCommand(op, source, options);
896
1247
  });
897
1248
  }
1249
+ program.command("login").description("Sign in with Google via browser").action(() => {
1250
+ loginCommand();
1251
+ });
1252
+ program.command("logout").description("Sign out and clear stored credentials").action(() => {
1253
+ logoutCommand();
1254
+ });
1255
+ program.command("whoami").description("Show current user, API key status, and model").action(() => {
1256
+ whoamiCommand();
1257
+ });
1258
+ var config = program.command("config").description("View or modify configuration").action(() => {
1259
+ configShowCommand();
1260
+ });
1261
+ config.command("set").description("Set a config value (api-key, model)").argument("<key>", "Config key (api-key, model)").argument("<value>", "Config value").action((key, value) => {
1262
+ configSetCommand(key, value);
1263
+ });
1264
+ config.command("delete").description("Delete a config value").argument("<key>", "Config key (api-key, model)").action((key) => {
1265
+ configDeleteCommand(key);
1266
+ });
1267
+ program.command("models").description("List available Claude models").action(() => {
1268
+ modelsCommand();
1269
+ });
1270
+ program.command("update").description("Update Shipwell CLI to the latest version").action(async () => {
1271
+ const { execSync } = await import("child_process");
1272
+ const ora3 = (await import("ora")).default;
1273
+ const spinner = ora3({ text: "Checking for updates...", prefixText: " " }).start();
1274
+ try {
1275
+ const latest = execSync("npm view @shipwellapp/cli version", { encoding: "utf-8" }).trim();
1276
+ if (latest === VERSION) {
1277
+ spinner.succeed(`Already on the latest version (${accent7(VERSION)})`);
1278
+ } else {
1279
+ spinner.text = `Updating to v${latest}...`;
1280
+ execSync("npm install -g @shipwellapp/cli@latest", { stdio: "pipe" });
1281
+ spinner.succeed(`Updated to v${accent7(latest)} ${dim5(`(was v${VERSION})`)}`);
1282
+ }
1283
+ } catch (err) {
1284
+ spinner.fail("Update failed. Try manually:");
1285
+ console.log(` ${chalk7.cyan("npm install -g @shipwellapp/cli@latest")}`);
1286
+ }
1287
+ console.log();
1288
+ });
898
1289
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipwellapp/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Full Codebase Autopilot — deep cross-file analysis powered by Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,7 +43,6 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@anthropic-ai/sdk": "^0.39.0",
46
- "@shipwell/core": "workspace:*",
47
46
  "chalk": "^5.4.1",
48
47
  "commander": "^13.1.0",
49
48
  "glob": "^11.0.0",