@keywaysh/cli 0.1.14 → 0.1.16

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/cli.js +410 -166
  2. package/package.json +4 -1
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/cli.ts
10
10
  import { Command } from "commander";
11
- import pc12 from "picocolors";
11
+ import pc13 from "picocolors";
12
12
 
13
13
  // src/cmds/init.ts
14
14
  import pc7 from "picocolors";
@@ -140,7 +140,7 @@ var INTERNAL_POSTHOG_HOST = "https://eu.i.posthog.com";
140
140
  // package.json
141
141
  var package_default = {
142
142
  name: "@keywaysh/cli",
143
- version: "0.1.14",
143
+ version: "0.1.16",
144
144
  description: "One link to all your secrets",
145
145
  type: "module",
146
146
  bin: {
@@ -185,9 +185,11 @@ var package_default = {
185
185
  node: ">=18.0.0"
186
186
  },
187
187
  dependencies: {
188
+ "@octokit/rest": "^22.0.1",
188
189
  "balanced-match": "^3.0.1",
189
190
  commander: "^14.0.0",
190
191
  conf: "^15.0.2",
192
+ "libsodium-wrappers": "^0.7.15",
191
193
  open: "^11.0.0",
192
194
  picocolors: "^1.1.1",
193
195
  "posthog-node": "^3.5.0",
@@ -195,6 +197,7 @@ var package_default = {
195
197
  },
196
198
  devDependencies: {
197
199
  "@types/balanced-match": "^3.0.2",
200
+ "@types/libsodium-wrappers": "^0.7.14",
198
201
  "@types/node": "^24.2.0",
199
202
  "@types/prompts": "^2.4.9",
200
203
  "@vitest/coverage-v8": "^3.0.0",
@@ -453,8 +456,8 @@ function getProviderAuthUrl(provider, accessToken, redirectUri) {
453
456
  if (redirectUri) params.set("redirect_uri", redirectUri);
454
457
  return `${API_BASE_URL}/v1/integrations/${provider}/authorize?${params}`;
455
458
  }
456
- async function getConnectionProjects(accessToken, connectionId) {
457
- const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/connections/${connectionId}/projects`, {
459
+ async function getAllProviderProjects(accessToken, provider) {
460
+ const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/providers/${provider}/all-projects`, {
458
461
  method: "GET",
459
462
  headers: {
460
463
  "User-Agent": USER_AGENT,
@@ -2043,9 +2046,139 @@ Summary: ${formatSummary(results)}`);
2043
2046
  }
2044
2047
  }
2045
2048
 
2046
- // src/cmds/connect.ts
2049
+ // src/cmds/ci.ts
2050
+ import { execSync as execSync3 } from "child_process";
2051
+ import { Octokit } from "@octokit/rest";
2047
2052
  import pc10 from "picocolors";
2048
2053
  import prompts8 from "prompts";
2054
+ function isGhAvailable() {
2055
+ try {
2056
+ execSync3("gh auth status", { stdio: "ignore" });
2057
+ return true;
2058
+ } catch {
2059
+ return false;
2060
+ }
2061
+ }
2062
+ function addSecretWithGh(repo, secretName, secretValue) {
2063
+ execSync3(`gh secret set ${secretName} --repo ${repo}`, {
2064
+ input: secretValue,
2065
+ stdio: ["pipe", "ignore", "ignore"]
2066
+ });
2067
+ }
2068
+ async function ciSetupCommand(options) {
2069
+ const repo = options.repo || detectGitRepo();
2070
+ if (!repo) {
2071
+ console.error(pc10.red("Not in a git repository. Use --repo owner/repo"));
2072
+ process.exit(1);
2073
+ }
2074
+ console.log(pc10.bold(`
2075
+ \u{1F510} Setting up GitHub Actions for ${repo}
2076
+ `));
2077
+ console.log(pc10.dim("Step 1: Keyway Authentication"));
2078
+ let keywayToken;
2079
+ try {
2080
+ keywayToken = await ensureLogin({ allowPrompt: true });
2081
+ console.log(pc10.green(" \u2713 Authenticated with Keyway\n"));
2082
+ } catch {
2083
+ console.error(pc10.red(" \u2717 Failed to authenticate with Keyway"));
2084
+ console.error(pc10.dim(" Run `keyway login` first"));
2085
+ process.exit(1);
2086
+ }
2087
+ const useGh = isGhAvailable();
2088
+ if (useGh) {
2089
+ console.log(pc10.dim("Step 2: Adding secret via GitHub CLI"));
2090
+ try {
2091
+ addSecretWithGh(repo, "KEYWAY_TOKEN", keywayToken);
2092
+ console.log(pc10.green(` \u2713 Secret KEYWAY_TOKEN added to ${repo}
2093
+ `));
2094
+ } catch (error) {
2095
+ const message = error instanceof Error ? error.message : String(error);
2096
+ console.error(pc10.red(` \u2717 Failed to add secret: ${message}`));
2097
+ console.error(pc10.dim(" Try running: gh auth login"));
2098
+ process.exit(1);
2099
+ }
2100
+ } else {
2101
+ console.log(pc10.dim("Step 2: Temporary GitHub PAT"));
2102
+ console.log(" gh CLI not found. We need a one-time GitHub PAT.");
2103
+ console.log(pc10.dim(" You can delete it immediately after setup.\n"));
2104
+ const patUrl = "https://github.com/settings/tokens/new?scopes=repo&description=Keyway%20CI%20Setup%20(temporary)";
2105
+ await openUrl(patUrl);
2106
+ const { githubToken } = await prompts8({
2107
+ type: "password",
2108
+ name: "githubToken",
2109
+ message: "Paste your GitHub PAT:"
2110
+ });
2111
+ if (!githubToken) {
2112
+ console.error(pc10.red("\n \u2717 GitHub PAT is required"));
2113
+ process.exit(1);
2114
+ }
2115
+ const octokit = new Octokit({ auth: githubToken });
2116
+ try {
2117
+ await octokit.users.getAuthenticated();
2118
+ console.log(pc10.green(" \u2713 GitHub PAT validated\n"));
2119
+ } catch {
2120
+ console.error(pc10.red(" \u2717 Invalid GitHub PAT"));
2121
+ process.exit(1);
2122
+ }
2123
+ console.log(pc10.dim("Step 3: Adding secret to repository"));
2124
+ const [owner, repoName] = repo.split("/");
2125
+ try {
2126
+ await addRepoSecret(octokit, owner, repoName, "KEYWAY_TOKEN", keywayToken);
2127
+ console.log(pc10.green(` \u2713 Secret KEYWAY_TOKEN added to ${repo}
2128
+ `));
2129
+ } catch (error) {
2130
+ const message = error instanceof Error ? error.message : String(error);
2131
+ if (message.includes("Not Found")) {
2132
+ console.error(pc10.red(` \u2717 Repository not found or no access: ${repo}`));
2133
+ console.error(pc10.dim(" Make sure the PAT has access to this repository"));
2134
+ } else {
2135
+ console.error(pc10.red(` \u2717 Failed to add secret: ${message}`));
2136
+ }
2137
+ process.exit(1);
2138
+ }
2139
+ }
2140
+ console.log(pc10.green(pc10.bold("\u2713 Setup complete!\n")));
2141
+ console.log("Add this to your workflow (.github/workflows/*.yml):\n");
2142
+ console.log(
2143
+ pc10.cyan(` - uses: keywaysh/keyway-action@v1
2144
+ with:
2145
+ token: \${{ secrets.KEYWAY_TOKEN }}
2146
+ environment: production`)
2147
+ );
2148
+ console.log();
2149
+ if (!useGh) {
2150
+ console.log(`\u{1F5D1}\uFE0F Delete the temporary PAT: ${pc10.underline("https://github.com/settings/tokens")}`);
2151
+ }
2152
+ console.log(pc10.dim(`\u{1F4D6} Docs: ${pc10.underline("https://docs.keyway.sh/ci")}
2153
+ `));
2154
+ }
2155
+ async function addRepoSecret(octokit, owner, repo, secretName, secretValue) {
2156
+ const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({
2157
+ owner,
2158
+ repo
2159
+ });
2160
+ const encryptedValue = await encryptSecret(publicKey.key, secretValue);
2161
+ await octokit.rest.actions.createOrUpdateRepoSecret({
2162
+ owner,
2163
+ repo,
2164
+ secret_name: secretName,
2165
+ encrypted_value: encryptedValue,
2166
+ key_id: publicKey.key_id
2167
+ });
2168
+ }
2169
+ async function encryptSecret(publicKey, secret) {
2170
+ const sodiumModule = await import("libsodium-wrappers");
2171
+ const sodium = sodiumModule.default || sodiumModule;
2172
+ await sodium.ready;
2173
+ const binkey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
2174
+ const binsec = sodium.from_string(secret);
2175
+ const encBytes = sodium.crypto_box_seal(binsec, binkey);
2176
+ return sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
2177
+ }
2178
+
2179
+ // src/cmds/connect.ts
2180
+ import pc11 from "picocolors";
2181
+ import prompts9 from "prompts";
2049
2182
  var TOKEN_AUTH_PROVIDERS = ["railway"];
2050
2183
  function getTokenCreationUrl(provider) {
2051
2184
  switch (provider) {
@@ -2058,37 +2191,37 @@ function getTokenCreationUrl(provider) {
2058
2191
  async function connectWithTokenFlow(accessToken, provider, displayName) {
2059
2192
  const tokenUrl = getTokenCreationUrl(provider);
2060
2193
  if (provider === "railway") {
2061
- console.log(pc10.yellow("\nTip: Select the workspace containing your projects."));
2062
- console.log(pc10.yellow(` Do NOT use "No workspace" - it won't have access to your projects.`));
2194
+ console.log(pc11.yellow("\nTip: Select the workspace containing your projects."));
2195
+ console.log(pc11.yellow(` Do NOT use "No workspace" - it won't have access to your projects.`));
2063
2196
  }
2064
2197
  await openUrl(tokenUrl);
2065
- const { token } = await prompts8({
2198
+ const { token } = await prompts9({
2066
2199
  type: "password",
2067
2200
  name: "token",
2068
2201
  message: `${displayName} API Token:`
2069
2202
  });
2070
2203
  if (!token) {
2071
- console.log(pc10.gray("Cancelled."));
2204
+ console.log(pc11.gray("Cancelled."));
2072
2205
  return false;
2073
2206
  }
2074
- console.log(pc10.gray("\nValidating token..."));
2207
+ console.log(pc11.gray("\nValidating token..."));
2075
2208
  try {
2076
2209
  const result = await connectWithToken(accessToken, provider, token);
2077
2210
  if (result.success) {
2078
- console.log(pc10.green(`
2211
+ console.log(pc11.green(`
2079
2212
  \u2713 Connected to ${displayName}!`));
2080
- console.log(pc10.gray(` Account: ${result.user.username}`));
2213
+ console.log(pc11.gray(` Account: ${result.user.username}`));
2081
2214
  if (result.user.teamName) {
2082
- console.log(pc10.gray(` Team: ${result.user.teamName}`));
2215
+ console.log(pc11.gray(` Team: ${result.user.teamName}`));
2083
2216
  }
2084
2217
  return true;
2085
2218
  } else {
2086
- console.log(pc10.red("\n\u2717 Connection failed."));
2219
+ console.log(pc11.red("\n\u2717 Connection failed."));
2087
2220
  return false;
2088
2221
  }
2089
2222
  } catch (error) {
2090
2223
  const message = error instanceof Error ? error.message : "Token validation failed";
2091
- console.log(pc10.red(`
2224
+ console.log(pc11.red(`
2092
2225
  \u2717 ${message}`));
2093
2226
  return false;
2094
2227
  }
@@ -2097,7 +2230,7 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
2097
2230
  const authUrl = getProviderAuthUrl(provider, accessToken);
2098
2231
  const startTime = /* @__PURE__ */ new Date();
2099
2232
  await openUrl(authUrl);
2100
- console.log(pc10.gray("Waiting for authorization..."));
2233
+ console.log(pc11.gray("Waiting for authorization..."));
2101
2234
  const maxAttempts = 60;
2102
2235
  let attempts = 0;
2103
2236
  while (attempts < maxAttempts) {
@@ -2109,15 +2242,15 @@ async function connectWithOAuthFlow(accessToken, provider, displayName) {
2109
2242
  (c) => c.provider === provider && new Date(c.createdAt) > startTime
2110
2243
  );
2111
2244
  if (newConn) {
2112
- console.log(pc10.green(`
2245
+ console.log(pc11.green(`
2113
2246
  \u2713 Connected to ${displayName}!`));
2114
2247
  return true;
2115
2248
  }
2116
2249
  } catch {
2117
2250
  }
2118
2251
  }
2119
- console.log(pc10.red("\n\u2717 Authorization timeout."));
2120
- console.log(pc10.gray("Run `keyway connections` to check if the connection was established."));
2252
+ console.log(pc11.red("\n\u2717 Authorization timeout."));
2253
+ console.log(pc11.gray("Run `keyway connections` to check if the connection was established."));
2121
2254
  return false;
2122
2255
  }
2123
2256
  async function connectCommand(provider, options = {}) {
@@ -2127,30 +2260,40 @@ async function connectCommand(provider, options = {}) {
2127
2260
  const providerInfo = providers.find((p) => p.name === provider.toLowerCase());
2128
2261
  if (!providerInfo) {
2129
2262
  const available = providers.map((p) => p.name).join(", ");
2130
- console.error(pc10.red(`Unknown provider: ${provider}`));
2131
- console.log(pc10.gray(`Available providers: ${available || "none"}`));
2263
+ console.error(pc11.red(`Unknown provider: ${provider}`));
2264
+ console.log(pc11.gray(`Available providers: ${available || "none"}`));
2132
2265
  process.exit(1);
2133
2266
  }
2134
2267
  if (!providerInfo.configured) {
2135
- console.error(pc10.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
2136
- console.log(pc10.gray("Contact your administrator to enable this integration."));
2268
+ console.error(pc11.red(`Provider ${providerInfo.displayName} is not configured on the server.`));
2269
+ console.log(pc11.gray("Contact your administrator to enable this integration."));
2137
2270
  process.exit(1);
2138
2271
  }
2139
2272
  const { connections } = await getConnections(accessToken);
2140
- const existingConnection = connections.find((c) => c.provider === provider.toLowerCase());
2141
- if (existingConnection) {
2142
- const { reconnect } = await prompts8({
2143
- type: "confirm",
2144
- name: "reconnect",
2145
- message: `You're already connected to ${providerInfo.displayName}. Reconnect?`,
2146
- initial: false
2273
+ const existingConnections = connections.filter((c) => c.provider === provider.toLowerCase());
2274
+ if (existingConnections.length > 0) {
2275
+ console.log(pc11.gray(`
2276
+ You have ${existingConnections.length} ${providerInfo.displayName} connection(s):`));
2277
+ for (const conn of existingConnections) {
2278
+ const teamInfo = conn.providerTeamId ? `(Team: ${conn.providerTeamId})` : "(Personal)";
2279
+ console.log(pc11.gray(` - ${teamInfo}`));
2280
+ }
2281
+ console.log("");
2282
+ const { action } = await prompts9({
2283
+ type: "select",
2284
+ name: "action",
2285
+ message: "What would you like to do?",
2286
+ choices: [
2287
+ { title: "Add another account/team", value: "add" },
2288
+ { title: "Cancel", value: "cancel" }
2289
+ ]
2147
2290
  });
2148
- if (!reconnect) {
2149
- console.log(pc10.gray("Keeping existing connection."));
2291
+ if (action !== "add") {
2292
+ console.log(pc11.gray("Keeping existing connections."));
2150
2293
  return;
2151
2294
  }
2152
2295
  }
2153
- console.log(pc10.blue(`
2296
+ console.log(pc11.blue(`
2154
2297
  Connecting to ${providerInfo.displayName}...
2155
2298
  `));
2156
2299
  let connected = false;
@@ -2169,7 +2312,7 @@ Connecting to ${providerInfo.displayName}...
2169
2312
  command: "connect",
2170
2313
  error: truncateMessage(message)
2171
2314
  });
2172
- console.error(pc10.red(`
2315
+ console.error(pc11.red(`
2173
2316
  \u2717 ${message}`));
2174
2317
  process.exit(1);
2175
2318
  }
@@ -2179,24 +2322,24 @@ async function connectionsCommand(options = {}) {
2179
2322
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2180
2323
  const { connections } = await getConnections(accessToken);
2181
2324
  if (connections.length === 0) {
2182
- console.log(pc10.gray("No provider connections found."));
2183
- console.log(pc10.gray("\nConnect to a provider with: keyway connect <provider>"));
2184
- console.log(pc10.gray("Available providers: vercel, railway"));
2325
+ console.log(pc11.gray("No provider connections found."));
2326
+ console.log(pc11.gray("\nConnect to a provider with: keyway connect <provider>"));
2327
+ console.log(pc11.gray("Available providers: vercel, railway"));
2185
2328
  return;
2186
2329
  }
2187
- console.log(pc10.blue("\n\u{1F4E1} Provider Connections\n"));
2330
+ console.log(pc11.blue("\n\u{1F4E1} Provider Connections\n"));
2188
2331
  for (const conn of connections) {
2189
2332
  const providerName = conn.provider.charAt(0).toUpperCase() + conn.provider.slice(1);
2190
- const teamInfo = conn.providerTeamId ? pc10.gray(` (Team: ${conn.providerTeamId})`) : "";
2333
+ const teamInfo = conn.providerTeamId ? pc11.gray(` (Team: ${conn.providerTeamId})`) : "";
2191
2334
  const date = new Date(conn.createdAt).toLocaleDateString();
2192
- console.log(` ${pc10.green("\u25CF")} ${pc10.bold(providerName)}${teamInfo}`);
2193
- console.log(pc10.gray(` Connected: ${date}`));
2194
- console.log(pc10.gray(` ID: ${conn.id}`));
2335
+ console.log(` ${pc11.green("\u25CF")} ${pc11.bold(providerName)}${teamInfo}`);
2336
+ console.log(pc11.gray(` Connected: ${date}`));
2337
+ console.log(pc11.gray(` ID: ${conn.id}`));
2195
2338
  console.log("");
2196
2339
  }
2197
2340
  } catch (error) {
2198
2341
  const message = error instanceof Error ? error.message : "Failed to list connections";
2199
- console.error(pc10.red(`
2342
+ console.error(pc11.red(`
2200
2343
  \u2717 ${message}`));
2201
2344
  process.exit(1);
2202
2345
  }
@@ -2207,22 +2350,22 @@ async function disconnectCommand(provider, options = {}) {
2207
2350
  const { connections } = await getConnections(accessToken);
2208
2351
  const connection = connections.find((c) => c.provider === provider.toLowerCase());
2209
2352
  if (!connection) {
2210
- console.log(pc10.gray(`No connection found for provider: ${provider}`));
2353
+ console.log(pc11.gray(`No connection found for provider: ${provider}`));
2211
2354
  return;
2212
2355
  }
2213
2356
  const providerName = provider.charAt(0).toUpperCase() + provider.slice(1);
2214
- const { confirm } = await prompts8({
2357
+ const { confirm } = await prompts9({
2215
2358
  type: "confirm",
2216
2359
  name: "confirm",
2217
2360
  message: `Disconnect from ${providerName}?`,
2218
2361
  initial: false
2219
2362
  });
2220
2363
  if (!confirm) {
2221
- console.log(pc10.gray("Cancelled."));
2364
+ console.log(pc11.gray("Cancelled."));
2222
2365
  return;
2223
2366
  }
2224
2367
  await deleteConnection(accessToken, connection.id);
2225
- console.log(pc10.green(`
2368
+ console.log(pc11.green(`
2226
2369
  \u2713 Disconnected from ${providerName}`));
2227
2370
  trackEvent(AnalyticsEvents.CLI_DISCONNECT, {
2228
2371
  provider: provider.toLowerCase()
@@ -2233,15 +2376,15 @@ async function disconnectCommand(provider, options = {}) {
2233
2376
  command: "disconnect",
2234
2377
  error: truncateMessage(message)
2235
2378
  });
2236
- console.error(pc10.red(`
2379
+ console.error(pc11.red(`
2237
2380
  \u2717 ${message}`));
2238
2381
  process.exit(1);
2239
2382
  }
2240
2383
  }
2241
2384
 
2242
2385
  // src/cmds/sync.ts
2243
- import pc11 from "picocolors";
2244
- import prompts9 from "prompts";
2386
+ import pc12 from "picocolors";
2387
+ import prompts10 from "prompts";
2245
2388
  function mapToVercelEnvironment(keywayEnv) {
2246
2389
  const mapping = {
2247
2390
  production: "production",
@@ -2285,36 +2428,36 @@ function mapToProviderEnvironment(provider, keywayEnv) {
2285
2428
  function displayDiffSummary(diff, providerName) {
2286
2429
  const totalDiff = diff.onlyInKeyway.length + diff.onlyInProvider.length + diff.different.length;
2287
2430
  if (totalDiff === 0 && diff.same.length > 0) {
2288
- console.log(pc11.green(`
2431
+ console.log(pc12.green(`
2289
2432
  \u2713 Already in sync (${diff.same.length} secrets)`));
2290
2433
  return;
2291
2434
  }
2292
- console.log(pc11.blue("\n\u{1F4CA} Comparison Summary\n"));
2293
- console.log(pc11.gray(` Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets
2435
+ console.log(pc12.blue("\n\u{1F4CA} Comparison Summary\n"));
2436
+ console.log(pc12.gray(` Keyway: ${diff.keywayCount} secrets | ${providerName}: ${diff.providerCount} secrets
2294
2437
  `));
2295
2438
  if (diff.onlyInKeyway.length > 0) {
2296
- console.log(pc11.cyan(` \u2192 ${diff.onlyInKeyway.length} only in Keyway`));
2297
- diff.onlyInKeyway.slice(0, 3).forEach((key) => console.log(pc11.gray(` ${key}`)));
2439
+ console.log(pc12.cyan(` \u2192 ${diff.onlyInKeyway.length} only in Keyway`));
2440
+ diff.onlyInKeyway.slice(0, 3).forEach((key) => console.log(pc12.gray(` ${key}`)));
2298
2441
  if (diff.onlyInKeyway.length > 3) {
2299
- console.log(pc11.gray(` ... and ${diff.onlyInKeyway.length - 3} more`));
2442
+ console.log(pc12.gray(` ... and ${diff.onlyInKeyway.length - 3} more`));
2300
2443
  }
2301
2444
  }
2302
2445
  if (diff.onlyInProvider.length > 0) {
2303
- console.log(pc11.magenta(` \u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
2304
- diff.onlyInProvider.slice(0, 3).forEach((key) => console.log(pc11.gray(` ${key}`)));
2446
+ console.log(pc12.magenta(` \u2190 ${diff.onlyInProvider.length} only in ${providerName}`));
2447
+ diff.onlyInProvider.slice(0, 3).forEach((key) => console.log(pc12.gray(` ${key}`)));
2305
2448
  if (diff.onlyInProvider.length > 3) {
2306
- console.log(pc11.gray(` ... and ${diff.onlyInProvider.length - 3} more`));
2449
+ console.log(pc12.gray(` ... and ${diff.onlyInProvider.length - 3} more`));
2307
2450
  }
2308
2451
  }
2309
2452
  if (diff.different.length > 0) {
2310
- console.log(pc11.yellow(` \u2260 ${diff.different.length} with different values`));
2311
- diff.different.slice(0, 3).forEach((key) => console.log(pc11.gray(` ${key}`)));
2453
+ console.log(pc12.yellow(` \u2260 ${diff.different.length} with different values`));
2454
+ diff.different.slice(0, 3).forEach((key) => console.log(pc12.gray(` ${key}`)));
2312
2455
  if (diff.different.length > 3) {
2313
- console.log(pc11.gray(` ... and ${diff.different.length - 3} more`));
2456
+ console.log(pc12.gray(` ... and ${diff.different.length - 3} more`));
2314
2457
  }
2315
2458
  }
2316
2459
  if (diff.same.length > 0) {
2317
- console.log(pc11.gray(` = ${diff.same.length} identical`));
2460
+ console.log(pc12.gray(` = ${diff.same.length} identical`));
2318
2461
  }
2319
2462
  console.log("");
2320
2463
  }
@@ -2354,130 +2497,216 @@ function projectMatchesRepo(project, repoFullName) {
2354
2497
  }
2355
2498
  return false;
2356
2499
  }
2357
- async function promptProjectSelection(projects, repoFullName) {
2500
+ async function selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, initialProjects) {
2501
+ let projects = initialProjects;
2502
+ while (true) {
2503
+ const result = await promptProjectSelection(projects, repoFullName, providerDisplayName);
2504
+ if (result === "connect_new") {
2505
+ console.log("");
2506
+ await connectCommand(provider, { loginPrompt: false });
2507
+ console.log("");
2508
+ const { projects: allProjects } = await getAllProviderProjects(accessToken, provider.toLowerCase());
2509
+ projects = allProjects.map((p) => ({
2510
+ id: p.id,
2511
+ name: p.name,
2512
+ serviceId: p.serviceId,
2513
+ serviceName: p.serviceName,
2514
+ linkedRepo: p.linkedRepo,
2515
+ environments: p.environments,
2516
+ connectionId: p.connectionId,
2517
+ teamId: p.teamId,
2518
+ teamName: p.teamName
2519
+ }));
2520
+ if (projects.length === 0) {
2521
+ console.error(pc12.red(`No projects found after connecting.`));
2522
+ process.exit(1);
2523
+ }
2524
+ console.log(pc12.green(`Found ${projects.length} projects. Select one:
2525
+ `));
2526
+ continue;
2527
+ }
2528
+ return { project: result, projects };
2529
+ }
2530
+ }
2531
+ async function promptProjectSelection(projects, repoFullName, providerDisplayName) {
2358
2532
  const repoName = repoFullName.split("/")[1]?.toLowerCase() || "";
2533
+ const uniqueTeams = new Set(projects.map((p) => p.teamId || "personal"));
2534
+ const hasMultipleAccounts = uniqueTeams.size > 1;
2359
2535
  const choices = projects.map((p) => {
2360
2536
  const displayName = getProjectDisplayName(p);
2361
2537
  let title = displayName;
2362
2538
  const badges = [];
2539
+ if (hasMultipleAccounts) {
2540
+ if (p.teamName) {
2541
+ badges.push(pc12.cyan(`[${p.teamName}]`));
2542
+ } else if (p.teamId) {
2543
+ const shortTeamId = p.teamId.length > 12 ? p.teamId.slice(0, 12) + "..." : p.teamId;
2544
+ badges.push(pc12.cyan(`[team:${shortTeamId}]`));
2545
+ } else {
2546
+ badges.push(pc12.cyan("[personal]"));
2547
+ }
2548
+ }
2363
2549
  if (p.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
2364
- badges.push(pc11.green("\u2190 linked"));
2550
+ badges.push(pc12.green("\u2190 linked"));
2365
2551
  } else if (p.name.toLowerCase() === repoName || p.serviceName?.toLowerCase() === repoName) {
2366
- badges.push(pc11.green("\u2190 same name"));
2552
+ badges.push(pc12.green("\u2190 same name"));
2367
2553
  } else if (p.linkedRepo) {
2368
- badges.push(pc11.gray(`\u2192 ${p.linkedRepo}`));
2554
+ badges.push(pc12.gray(`\u2192 ${p.linkedRepo}`));
2369
2555
  }
2370
2556
  if (badges.length > 0) {
2371
2557
  title = `${displayName} ${badges.join(" ")}`;
2372
2558
  }
2373
2559
  return { title, value: p.id };
2374
2560
  });
2375
- const { projectChoice } = await prompts9({
2561
+ choices.push({
2562
+ title: pc12.blue(`+ Connect another ${providerDisplayName} account`),
2563
+ value: "__connect_new__"
2564
+ });
2565
+ const { projectChoice } = await prompts10({
2376
2566
  type: "select",
2377
2567
  name: "projectChoice",
2378
2568
  message: "Select a project:",
2379
2569
  choices
2380
2570
  });
2381
2571
  if (!projectChoice) {
2382
- console.log(pc11.gray("Cancelled."));
2572
+ console.log(pc12.gray("Cancelled."));
2383
2573
  process.exit(0);
2384
2574
  }
2575
+ if (projectChoice === "__connect_new__") {
2576
+ return "connect_new";
2577
+ }
2385
2578
  return projects.find((p) => p.id === projectChoice);
2386
2579
  }
2387
2580
  async function syncCommand(provider, options = {}) {
2388
2581
  try {
2389
2582
  if (options.pull && options.allowDelete) {
2390
- console.error(pc11.red("Error: --allow-delete cannot be used with --pull"));
2391
- console.log(pc11.gray("The --allow-delete flag is only for push operations."));
2583
+ console.error(pc12.red("Error: --allow-delete cannot be used with --pull"));
2584
+ console.log(pc12.gray("The --allow-delete flag is only for push operations."));
2392
2585
  process.exit(1);
2393
2586
  }
2394
2587
  const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
2395
2588
  const repoFullName = detectGitRepo();
2396
2589
  if (!repoFullName) {
2397
- console.error(pc11.red("Could not detect Git repository."));
2398
- console.log(pc11.gray("Run this command from a Git repository directory."));
2590
+ console.error(pc12.red("Could not detect Git repository."));
2591
+ console.log(pc12.gray("Run this command from a Git repository directory."));
2399
2592
  process.exit(1);
2400
2593
  }
2401
- console.log(pc11.gray(`Repository: ${repoFullName}`));
2594
+ console.log(pc12.gray(`Repository: ${repoFullName}`));
2402
2595
  const vaultExists = await checkVaultExists(accessToken, repoFullName);
2403
2596
  if (!vaultExists) {
2404
- console.log(pc11.yellow(`
2597
+ console.log(pc12.yellow(`
2405
2598
  No vault found for ${repoFullName}.`));
2406
- const { shouldCreate } = await prompts9({
2599
+ const { shouldCreate } = await prompts10({
2407
2600
  type: "confirm",
2408
2601
  name: "shouldCreate",
2409
2602
  message: "Create vault now?",
2410
2603
  initial: true
2411
2604
  });
2412
2605
  if (!shouldCreate) {
2413
- console.log(pc11.gray("Cancelled. Run `keyway init` to create a vault first."));
2606
+ console.log(pc12.gray("Cancelled. Run `keyway init` to create a vault first."));
2414
2607
  process.exit(0);
2415
2608
  }
2416
- console.log(pc11.gray("\nCreating vault..."));
2609
+ console.log(pc12.gray("\nCreating vault..."));
2417
2610
  try {
2418
2611
  await initVault(repoFullName, accessToken);
2419
- console.log(pc11.green(`\u2713 Vault created for ${repoFullName}
2612
+ console.log(pc12.green(`\u2713 Vault created for ${repoFullName}
2420
2613
  `));
2421
2614
  } catch (error) {
2422
2615
  const message = error instanceof Error ? error.message : "Failed to create vault";
2423
- console.error(pc11.red(`
2616
+ console.error(pc12.red(`
2424
2617
  \u2717 ${message}`));
2425
2618
  process.exit(1);
2426
2619
  }
2427
2620
  }
2428
- let { connections } = await getConnections(accessToken);
2429
- let connection = connections.find((c) => c.provider === provider.toLowerCase());
2430
- if (!connection) {
2431
- const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
2432
- console.log(pc11.yellow(`
2621
+ const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
2622
+ let { projects: allProjects, connections } = await getAllProviderProjects(accessToken, provider.toLowerCase());
2623
+ if (connections.length === 0) {
2624
+ console.log(pc12.yellow(`
2433
2625
  Not connected to ${providerDisplayName}.`));
2434
- const { shouldConnect } = await prompts9({
2626
+ const { shouldConnect } = await prompts10({
2435
2627
  type: "confirm",
2436
2628
  name: "shouldConnect",
2437
2629
  message: `Connect to ${providerDisplayName} now?`,
2438
2630
  initial: true
2439
2631
  });
2440
2632
  if (!shouldConnect) {
2441
- console.log(pc11.gray("Cancelled."));
2633
+ console.log(pc12.gray("Cancelled."));
2442
2634
  process.exit(0);
2443
2635
  }
2444
2636
  await connectCommand(provider, { loginPrompt: false });
2445
- const refreshed = await getConnections(accessToken);
2637
+ const refreshed = await getAllProviderProjects(accessToken, provider.toLowerCase());
2638
+ allProjects = refreshed.projects;
2446
2639
  connections = refreshed.connections;
2447
- connection = connections.find((c) => c.provider === provider.toLowerCase());
2448
- if (!connection) {
2449
- console.error(pc11.red(`
2640
+ if (connections.length === 0) {
2641
+ console.error(pc12.red(`
2450
2642
  Connection to ${providerDisplayName} failed.`));
2451
2643
  process.exit(1);
2452
2644
  }
2453
2645
  console.log("");
2454
2646
  }
2455
- const { projects } = await getConnectionProjects(accessToken, connection.id);
2647
+ let projects = allProjects.map((p) => ({
2648
+ id: p.id,
2649
+ name: p.name,
2650
+ serviceId: p.serviceId,
2651
+ serviceName: p.serviceName,
2652
+ linkedRepo: p.linkedRepo,
2653
+ environments: p.environments,
2654
+ connectionId: p.connectionId,
2655
+ teamId: p.teamId,
2656
+ teamName: p.teamName
2657
+ }));
2658
+ if (options.team) {
2659
+ const teamFilter = options.team.toLowerCase();
2660
+ const filteredProjects = projects.filter(
2661
+ (p) => p.teamId?.toLowerCase() === teamFilter || p.teamName?.toLowerCase() === teamFilter || // Match "personal" for null teamId
2662
+ teamFilter === "personal" && !p.teamId
2663
+ );
2664
+ if (filteredProjects.length === 0) {
2665
+ console.error(pc12.red(`No projects found for team: ${options.team}`));
2666
+ console.log(pc12.gray("Available teams:"));
2667
+ const teams = /* @__PURE__ */ new Set();
2668
+ projects.forEach((p) => {
2669
+ if (p.teamName) teams.add(p.teamName);
2670
+ else if (p.teamId) teams.add(p.teamId);
2671
+ else teams.add("personal");
2672
+ });
2673
+ teams.forEach((t) => console.log(pc12.gray(` - ${t}`)));
2674
+ process.exit(1);
2675
+ }
2676
+ projects = filteredProjects;
2677
+ console.log(pc12.gray(`Filtered to ${projects.length} projects in team: ${options.team}`));
2678
+ }
2456
2679
  if (projects.length === 0) {
2457
- console.error(pc11.red(`No projects found in your ${provider} account.`));
2680
+ console.error(pc12.red(`No projects found in your ${providerDisplayName} account(s).`));
2681
+ if (connections.length > 1) {
2682
+ console.log(pc12.gray(`Checked ${connections.length} connected accounts.`));
2683
+ }
2458
2684
  process.exit(1);
2459
2685
  }
2686
+ if (connections.length > 1 && !options.team) {
2687
+ console.log(pc12.gray(`Searching ${projects.length} projects across ${connections.length} ${providerDisplayName} accounts...`));
2688
+ }
2460
2689
  let selectedProject;
2461
2690
  if (options.project) {
2462
2691
  const found = projects.find(
2463
2692
  (p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase() || p.serviceName?.toLowerCase() === options.project?.toLowerCase()
2464
2693
  );
2465
2694
  if (!found) {
2466
- console.error(pc11.red(`Project not found: ${options.project}`));
2467
- console.log(pc11.gray("Available projects:"));
2468
- projects.forEach((p) => console.log(pc11.gray(` - ${getProjectDisplayName(p)}`)));
2695
+ console.error(pc12.red(`Project not found: ${options.project}`));
2696
+ console.log(pc12.gray("Available projects:"));
2697
+ projects.forEach((p) => console.log(pc12.gray(` - ${getProjectDisplayName(p)}`)));
2469
2698
  process.exit(1);
2470
2699
  }
2471
2700
  selectedProject = found;
2472
2701
  if (!projectMatchesRepo(selectedProject, repoFullName)) {
2473
2702
  console.log("");
2474
- console.log(pc11.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2475
- console.log(pc11.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2476
- console.log(pc11.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2477
- console.log(pc11.yellow(` Current repo: ${repoFullName}`));
2478
- console.log(pc11.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
2703
+ console.log(pc12.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2704
+ console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2705
+ console.log(pc12.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2706
+ console.log(pc12.yellow(` Current repo: ${repoFullName}`));
2707
+ console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
2479
2708
  if (selectedProject.linkedRepo) {
2480
- console.log(pc11.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2709
+ console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2481
2710
  }
2482
2711
  console.log("");
2483
2712
  }
@@ -2486,11 +2715,18 @@ Connection to ${providerDisplayName} failed.`));
2486
2715
  if (autoMatch && (autoMatch.matchType === "linked_repo" || autoMatch.matchType === "exact_name")) {
2487
2716
  selectedProject = autoMatch.project;
2488
2717
  const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
2489
- console.log(pc11.green(`\u2713 Auto-selected project: ${getProjectDisplayName(selectedProject)} (${matchReason})`));
2718
+ let teamInfo = "";
2719
+ if (selectedProject.teamName) {
2720
+ teamInfo = pc12.gray(` (${selectedProject.teamName})`);
2721
+ } else if (selectedProject.teamId && connections.length > 1) {
2722
+ const shortTeamId = selectedProject.teamId.length > 12 ? selectedProject.teamId.slice(0, 12) + "..." : selectedProject.teamId;
2723
+ teamInfo = pc12.gray(` (team:${shortTeamId})`);
2724
+ }
2725
+ console.log(pc12.green(`\u2713 Auto-selected project: ${getProjectDisplayName(selectedProject)}${teamInfo} (${matchReason})`));
2490
2726
  } else if (autoMatch && autoMatch.matchType === "partial_name") {
2491
2727
  const partialDisplayName = getProjectDisplayName(autoMatch.project);
2492
- console.log(pc11.yellow(`Detected project: ${partialDisplayName} (partial match)`));
2493
- const { useDetected } = await prompts9({
2728
+ console.log(pc12.yellow(`Detected project: ${partialDisplayName} (partial match)`));
2729
+ const { useDetected } = await prompts10({
2494
2730
  type: "confirm",
2495
2731
  name: "useDetected",
2496
2732
  message: `Use ${partialDisplayName}?`,
@@ -2499,60 +2735,64 @@ Connection to ${providerDisplayName} failed.`));
2499
2735
  if (useDetected) {
2500
2736
  selectedProject = autoMatch.project;
2501
2737
  } else {
2502
- selectedProject = await promptProjectSelection(projects, repoFullName);
2738
+ const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
2739
+ selectedProject = result.project;
2740
+ projects = result.projects;
2503
2741
  }
2504
2742
  } else if (projects.length === 1) {
2505
2743
  selectedProject = projects[0];
2506
2744
  if (!projectMatchesRepo(selectedProject, repoFullName)) {
2507
2745
  console.log("");
2508
- console.log(pc11.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2509
- console.log(pc11.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2510
- console.log(pc11.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2511
- console.log(pc11.yellow(` Current repo: ${repoFullName}`));
2512
- console.log(pc11.yellow(` Only project: ${getProjectDisplayName(selectedProject)}`));
2746
+ console.log(pc12.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2747
+ console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
2748
+ console.log(pc12.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2749
+ console.log(pc12.yellow(` Current repo: ${repoFullName}`));
2750
+ console.log(pc12.yellow(` Only project: ${getProjectDisplayName(selectedProject)}`));
2513
2751
  if (selectedProject.linkedRepo) {
2514
- console.log(pc11.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2752
+ console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2515
2753
  }
2516
2754
  console.log("");
2517
- const { continueAnyway } = await prompts9({
2755
+ const { continueAnyway } = await prompts10({
2518
2756
  type: "confirm",
2519
2757
  name: "continueAnyway",
2520
2758
  message: "Continue anyway?",
2521
2759
  initial: false
2522
2760
  });
2523
2761
  if (!continueAnyway) {
2524
- console.log(pc11.gray("Cancelled."));
2762
+ console.log(pc12.gray("Cancelled."));
2525
2763
  process.exit(0);
2526
2764
  }
2527
2765
  }
2528
2766
  } else {
2529
- console.log(pc11.yellow(`
2767
+ console.log(pc12.yellow(`
2530
2768
  \u26A0\uFE0F No matching project found for ${repoFullName}`));
2531
- console.log(pc11.gray("Select a project manually:\n"));
2532
- selectedProject = await promptProjectSelection(projects, repoFullName);
2769
+ console.log(pc12.gray("Select a project manually:\n"));
2770
+ const result = await selectProjectWithConnectOption(accessToken, provider, providerDisplayName, repoFullName, projects);
2771
+ selectedProject = result.project;
2772
+ projects = result.projects;
2533
2773
  }
2534
2774
  }
2535
2775
  if (!options.project && !projectMatchesRepo(selectedProject, repoFullName)) {
2536
2776
  const autoMatch = findMatchingProject(projects, repoFullName);
2537
2777
  if (autoMatch && autoMatch.project.id !== selectedProject.id) {
2538
2778
  console.log("");
2539
- console.log(pc11.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2540
- console.log(pc11.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
2541
- console.log(pc11.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2542
- console.log(pc11.yellow(` Current repo: ${repoFullName}`));
2543
- console.log(pc11.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
2779
+ console.log(pc12.yellow("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
2780
+ console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
2781
+ console.log(pc12.yellow("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
2782
+ console.log(pc12.yellow(` Current repo: ${repoFullName}`));
2783
+ console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
2544
2784
  if (selectedProject.linkedRepo) {
2545
- console.log(pc11.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2785
+ console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
2546
2786
  }
2547
2787
  console.log("");
2548
- const { continueAnyway } = await prompts9({
2788
+ const { continueAnyway } = await prompts10({
2549
2789
  type: "confirm",
2550
2790
  name: "continueAnyway",
2551
2791
  message: "Are you sure you want to sync with this project?",
2552
2792
  initial: false
2553
2793
  });
2554
2794
  if (!continueAnyway) {
2555
- console.log(pc11.gray("Cancelled."));
2795
+ console.log(pc12.gray("Cancelled."));
2556
2796
  process.exit(0);
2557
2797
  }
2558
2798
  }
@@ -2566,7 +2806,7 @@ Connection to ${providerDisplayName} failed.`));
2566
2806
  if (needsEnvPrompt || needsDirectionPrompt) {
2567
2807
  if (needsEnvPrompt) {
2568
2808
  const vaultEnvs = await getVaultEnvironments(accessToken, repoFullName);
2569
- const { selectedEnv } = await prompts9({
2809
+ const { selectedEnv } = await prompts10({
2570
2810
  type: "select",
2571
2811
  name: "selectedEnv",
2572
2812
  message: "Keyway environment:",
@@ -2574,7 +2814,7 @@ Connection to ${providerDisplayName} failed.`));
2574
2814
  initial: Math.max(0, vaultEnvs.indexOf("production"))
2575
2815
  });
2576
2816
  if (!selectedEnv) {
2577
- console.log(pc11.gray("Cancelled."));
2817
+ console.log(pc12.gray("Cancelled."));
2578
2818
  process.exit(0);
2579
2819
  }
2580
2820
  keywayEnv = selectedEnv;
@@ -2588,9 +2828,9 @@ Connection to ${providerDisplayName} failed.`));
2588
2828
  providerEnv = mappedEnv;
2589
2829
  } else if (selectedProject.environments.length === 1) {
2590
2830
  providerEnv = selectedProject.environments[0];
2591
- console.log(pc11.gray(`Using ${providerName} environment: ${providerEnv}`));
2831
+ console.log(pc12.gray(`Using ${providerName} environment: ${providerEnv}`));
2592
2832
  } else {
2593
- const { selectedProviderEnv } = await prompts9({
2833
+ const { selectedProviderEnv } = await prompts10({
2594
2834
  type: "select",
2595
2835
  name: "selectedProviderEnv",
2596
2836
  message: `${providerName} environment:`,
@@ -2600,7 +2840,7 @@ Connection to ${providerDisplayName} failed.`));
2600
2840
  ))
2601
2841
  });
2602
2842
  if (!selectedProviderEnv) {
2603
- console.log(pc11.gray("Cancelled."));
2843
+ console.log(pc12.gray("Cancelled."));
2604
2844
  process.exit(0);
2605
2845
  }
2606
2846
  providerEnv = selectedProviderEnv;
@@ -2614,9 +2854,9 @@ Connection to ${providerDisplayName} failed.`));
2614
2854
  if (needsDirectionPrompt) {
2615
2855
  const effectiveKeywayEnv = keywayEnv || "production";
2616
2856
  const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
2617
- console.log(pc11.gray("\nComparing secrets..."));
2857
+ console.log(pc12.gray("\nComparing secrets..."));
2618
2858
  diff = await getSyncDiff(accessToken, repoFullName, {
2619
- connectionId: connection.id,
2859
+ connectionId: selectedProject.connectionId,
2620
2860
  projectId: selectedProject.id,
2621
2861
  serviceId: selectedProject.serviceId,
2622
2862
  // Railway: service ID for service-specific variables
@@ -2636,7 +2876,7 @@ Connection to ${providerDisplayName} failed.`));
2636
2876
  } else if (diff.providerCount === 0 && diff.keywayCount > 0) {
2637
2877
  defaultDirection = 0;
2638
2878
  }
2639
- const { selectedDirection } = await prompts9({
2879
+ const { selectedDirection } = await prompts10({
2640
2880
  type: "select",
2641
2881
  name: "selectedDirection",
2642
2882
  message: "Sync direction:",
@@ -2647,7 +2887,7 @@ Connection to ${providerDisplayName} failed.`));
2647
2887
  initial: defaultDirection
2648
2888
  });
2649
2889
  if (!selectedDirection) {
2650
- console.log(pc11.gray("Cancelled."));
2890
+ console.log(pc12.gray("Cancelled."));
2651
2891
  process.exit(0);
2652
2892
  }
2653
2893
  direction = selectedDirection;
@@ -2659,15 +2899,15 @@ Connection to ${providerDisplayName} failed.`));
2659
2899
  const status = await getSyncStatus(
2660
2900
  accessToken,
2661
2901
  repoFullName,
2662
- connection.id,
2902
+ selectedProject.connectionId,
2663
2903
  selectedProject.id,
2664
2904
  keywayEnv
2665
2905
  );
2666
2906
  if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
2667
- console.log(pc11.yellow(`
2907
+ console.log(pc12.yellow(`
2668
2908
  \u26A0\uFE0F Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`));
2669
- console.log(pc11.gray(` (Use --environment to sync a different environment)`));
2670
- const { importFirst } = await prompts9({
2909
+ console.log(pc12.gray(` (Use --environment to sync a different environment)`));
2910
+ const { importFirst } = await prompts10({
2671
2911
  type: "confirm",
2672
2912
  name: "importFirst",
2673
2913
  message: `Import secrets from ${providerName} first?`,
@@ -2677,7 +2917,7 @@ Connection to ${providerDisplayName} failed.`));
2677
2917
  await executeSyncOperation(
2678
2918
  accessToken,
2679
2919
  repoFullName,
2680
- connection.id,
2920
+ selectedProject.connectionId,
2681
2921
  selectedProject,
2682
2922
  keywayEnv,
2683
2923
  providerEnv,
@@ -2693,7 +2933,7 @@ Connection to ${providerDisplayName} failed.`));
2693
2933
  await executeSyncOperation(
2694
2934
  accessToken,
2695
2935
  repoFullName,
2696
- connection.id,
2936
+ selectedProject.connectionId,
2697
2937
  selectedProject,
2698
2938
  keywayEnv,
2699
2939
  providerEnv,
@@ -2708,7 +2948,7 @@ Connection to ${providerDisplayName} failed.`));
2708
2948
  command: "sync",
2709
2949
  error: truncateMessage(message)
2710
2950
  });
2711
- console.error(pc11.red(`
2951
+ console.error(pc12.red(`
2712
2952
  \u2717 ${message}`));
2713
2953
  process.exit(1);
2714
2954
  }
@@ -2727,49 +2967,49 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2727
2967
  });
2728
2968
  const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
2729
2969
  if (totalChanges === 0) {
2730
- console.log(pc11.green("\n\u2713 Already in sync. No changes needed."));
2970
+ console.log(pc12.green("\n\u2713 Already in sync. No changes needed."));
2731
2971
  return;
2732
2972
  }
2733
- console.log(pc11.blue("\n\u{1F4CB} Sync Preview\n"));
2973
+ console.log(pc12.blue("\n\u{1F4CB} Sync Preview\n"));
2734
2974
  if (preview.toCreate.length > 0) {
2735
- console.log(pc11.green(` + ${preview.toCreate.length} to create`));
2736
- preview.toCreate.slice(0, 5).forEach((key) => console.log(pc11.gray(` ${key}`)));
2975
+ console.log(pc12.green(` + ${preview.toCreate.length} to create`));
2976
+ preview.toCreate.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
2737
2977
  if (preview.toCreate.length > 5) {
2738
- console.log(pc11.gray(` ... and ${preview.toCreate.length - 5} more`));
2978
+ console.log(pc12.gray(` ... and ${preview.toCreate.length - 5} more`));
2739
2979
  }
2740
2980
  }
2741
2981
  if (preview.toUpdate.length > 0) {
2742
- console.log(pc11.yellow(` ~ ${preview.toUpdate.length} to update`));
2743
- preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc11.gray(` ${key}`)));
2982
+ console.log(pc12.yellow(` ~ ${preview.toUpdate.length} to update`));
2983
+ preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
2744
2984
  if (preview.toUpdate.length > 5) {
2745
- console.log(pc11.gray(` ... and ${preview.toUpdate.length - 5} more`));
2985
+ console.log(pc12.gray(` ... and ${preview.toUpdate.length - 5} more`));
2746
2986
  }
2747
2987
  }
2748
2988
  if (preview.toDelete.length > 0) {
2749
- console.log(pc11.red(` - ${preview.toDelete.length} to delete`));
2750
- preview.toDelete.slice(0, 5).forEach((key) => console.log(pc11.gray(` ${key}`)));
2989
+ console.log(pc12.red(` - ${preview.toDelete.length} to delete`));
2990
+ preview.toDelete.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
2751
2991
  if (preview.toDelete.length > 5) {
2752
- console.log(pc11.gray(` ... and ${preview.toDelete.length - 5} more`));
2992
+ console.log(pc12.gray(` ... and ${preview.toDelete.length - 5} more`));
2753
2993
  }
2754
2994
  }
2755
2995
  if (preview.toSkip.length > 0) {
2756
- console.log(pc11.gray(` \u25CB ${preview.toSkip.length} unchanged`));
2996
+ console.log(pc12.gray(` \u25CB ${preview.toSkip.length} unchanged`));
2757
2997
  }
2758
2998
  console.log("");
2759
2999
  if (!skipConfirm) {
2760
3000
  const target = direction === "push" ? providerName : "Keyway";
2761
- const { confirm } = await prompts9({
3001
+ const { confirm } = await prompts10({
2762
3002
  type: "confirm",
2763
3003
  name: "confirm",
2764
3004
  message: `Apply ${totalChanges} changes to ${target}?`,
2765
3005
  initial: true
2766
3006
  });
2767
3007
  if (!confirm) {
2768
- console.log(pc11.gray("Cancelled."));
3008
+ console.log(pc12.gray("Cancelled."));
2769
3009
  return;
2770
3010
  }
2771
3011
  }
2772
- console.log(pc11.blue("\n\u23F3 Syncing...\n"));
3012
+ console.log(pc12.blue("\n\u23F3 Syncing...\n"));
2773
3013
  const result = await executeSync(accessToken, repoFullName, {
2774
3014
  connectionId,
2775
3015
  projectId: project.id,
@@ -2781,11 +3021,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2781
3021
  allowDelete
2782
3022
  });
2783
3023
  if (result.success) {
2784
- console.log(pc11.green("\u2713 Sync complete"));
2785
- console.log(pc11.gray(` Created: ${result.stats.created}`));
2786
- console.log(pc11.gray(` Updated: ${result.stats.updated}`));
3024
+ console.log(pc12.green("\u2713 Sync complete"));
3025
+ console.log(pc12.gray(` Created: ${result.stats.created}`));
3026
+ console.log(pc12.gray(` Updated: ${result.stats.updated}`));
2787
3027
  if (result.stats.deleted > 0) {
2788
- console.log(pc11.gray(` Deleted: ${result.stats.deleted}`));
3028
+ console.log(pc12.gray(` Deleted: ${result.stats.deleted}`));
2789
3029
  }
2790
3030
  trackEvent(AnalyticsEvents.CLI_SYNC, {
2791
3031
  provider,
@@ -2795,7 +3035,7 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2795
3035
  deleted: result.stats.deleted
2796
3036
  });
2797
3037
  } else {
2798
- console.error(pc11.red(`
3038
+ console.error(pc12.red(`
2799
3039
  \u2717 ${result.error}`));
2800
3040
  process.exit(1);
2801
3041
  }
@@ -2803,16 +3043,16 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
2803
3043
 
2804
3044
  // src/cli.ts
2805
3045
  process.on("unhandledRejection", (reason) => {
2806
- console.error(pc12.red("Unhandled error:"), reason);
3046
+ console.error(pc13.red("Unhandled error:"), reason);
2807
3047
  process.exit(1);
2808
3048
  });
2809
3049
  var program = new Command();
2810
3050
  var TAGLINE = "Sync secrets with your team and infra";
2811
3051
  var showBanner = () => {
2812
- const text = pc12.bold(pc12.cyan("Keyway CLI"));
3052
+ const text = pc13.bold(pc13.cyan("Keyway CLI"));
2813
3053
  console.log(`
2814
3054
  ${text}
2815
- ${pc12.gray(TAGLINE)}
3055
+ ${pc13.gray(TAGLINE)}
2816
3056
  `);
2817
3057
  };
2818
3058
  showBanner();
@@ -2844,13 +3084,17 @@ program.command("connections").description("List your provider connections").opt
2844
3084
  program.command("disconnect <provider>").description("Disconnect from a provider").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
2845
3085
  await disconnectCommand(provider, options);
2846
3086
  });
2847
- program.command("sync <provider>").description("Sync secrets with a provider (e.g., vercel)").option("--push", "Export secrets from Keyway to provider").option("--pull", "Import secrets from provider to Keyway").option("-e, --environment <env>", "Keyway environment (default: production)").option("--provider-env <env>", "Provider environment (default: production)").option("--project <project>", "Provider project name or ID").option("--allow-delete", "Allow deleting secrets not in source").option("-y, --yes", "Skip confirmation prompt").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
3087
+ program.command("sync <provider>").description("Sync secrets with a provider (e.g., vercel)").option("--push", "Export secrets from Keyway to provider").option("--pull", "Import secrets from provider to Keyway").option("-e, --environment <env>", "Keyway environment (default: production)").option("--provider-env <env>", "Provider environment (default: production)").option("--project <project>", "Provider project name or ID").option("--team <team>", "Team/org name or ID (for multi-account)").option("--allow-delete", "Allow deleting secrets not in source").option("-y, --yes", "Skip confirmation prompt").option("--no-login-prompt", "Fail instead of prompting to login if unauthenticated").action(async (provider, options) => {
2848
3088
  await syncCommand(provider, options);
2849
3089
  });
3090
+ var ci = program.command("ci").description("CI/CD integration commands");
3091
+ ci.command("setup").description("Setup GitHub Actions integration (adds KEYWAY_TOKEN secret)").option("--repo <repo>", "Repository in owner/repo format (auto-detected)").action(async (options) => {
3092
+ await ciSetupCommand(options);
3093
+ });
2850
3094
  (async () => {
2851
3095
  await warnIfEnvNotGitignored();
2852
3096
  await program.parseAsync();
2853
3097
  })().catch((error) => {
2854
- console.error(pc12.red("Error:"), error.message || error);
3098
+ console.error(pc13.red("Error:"), error.message || error);
2855
3099
  process.exit(1);
2856
3100
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keywaysh/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "One link to all your secrets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,9 +45,11 @@
45
45
  "node": ">=18.0.0"
46
46
  },
47
47
  "dependencies": {
48
+ "@octokit/rest": "^22.0.1",
48
49
  "balanced-match": "^3.0.1",
49
50
  "commander": "^14.0.0",
50
51
  "conf": "^15.0.2",
52
+ "libsodium-wrappers": "^0.7.15",
51
53
  "open": "^11.0.0",
52
54
  "picocolors": "^1.1.1",
53
55
  "posthog-node": "^3.5.0",
@@ -55,6 +57,7 @@
55
57
  },
56
58
  "devDependencies": {
57
59
  "@types/balanced-match": "^3.0.2",
60
+ "@types/libsodium-wrappers": "^0.7.14",
58
61
  "@types/node": "^24.2.0",
59
62
  "@types/prompts": "^2.4.9",
60
63
  "@vitest/coverage-v8": "^3.0.0",