@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.
- package/dist/cli.js +410 -166
- 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
|
|
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.
|
|
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
|
|
457
|
-
const response = await fetchWithTimeout(`${API_BASE_URL}/v1/integrations/
|
|
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/
|
|
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(
|
|
2062
|
-
console.log(
|
|
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
|
|
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(
|
|
2204
|
+
console.log(pc11.gray("Cancelled."));
|
|
2072
2205
|
return false;
|
|
2073
2206
|
}
|
|
2074
|
-
console.log(
|
|
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(
|
|
2211
|
+
console.log(pc11.green(`
|
|
2079
2212
|
\u2713 Connected to ${displayName}!`));
|
|
2080
|
-
console.log(
|
|
2213
|
+
console.log(pc11.gray(` Account: ${result.user.username}`));
|
|
2081
2214
|
if (result.user.teamName) {
|
|
2082
|
-
console.log(
|
|
2215
|
+
console.log(pc11.gray(` Team: ${result.user.teamName}`));
|
|
2083
2216
|
}
|
|
2084
2217
|
return true;
|
|
2085
2218
|
} else {
|
|
2086
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
2120
|
-
console.log(
|
|
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(
|
|
2131
|
-
console.log(
|
|
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(
|
|
2136
|
-
console.log(
|
|
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
|
|
2141
|
-
if (
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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 (
|
|
2149
|
-
console.log(
|
|
2291
|
+
if (action !== "add") {
|
|
2292
|
+
console.log(pc11.gray("Keeping existing connections."));
|
|
2150
2293
|
return;
|
|
2151
2294
|
}
|
|
2152
2295
|
}
|
|
2153
|
-
console.log(
|
|
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(
|
|
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(
|
|
2183
|
-
console.log(
|
|
2184
|
-
console.log(
|
|
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(
|
|
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 ?
|
|
2333
|
+
const teamInfo = conn.providerTeamId ? pc11.gray(` (Team: ${conn.providerTeamId})`) : "";
|
|
2191
2334
|
const date = new Date(conn.createdAt).toLocaleDateString();
|
|
2192
|
-
console.log(` ${
|
|
2193
|
-
console.log(
|
|
2194
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
2364
|
+
console.log(pc11.gray("Cancelled."));
|
|
2222
2365
|
return;
|
|
2223
2366
|
}
|
|
2224
2367
|
await deleteConnection(accessToken, connection.id);
|
|
2225
|
-
console.log(
|
|
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(
|
|
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
|
|
2244
|
-
import
|
|
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(
|
|
2431
|
+
console.log(pc12.green(`
|
|
2289
2432
|
\u2713 Already in sync (${diff.same.length} secrets)`));
|
|
2290
2433
|
return;
|
|
2291
2434
|
}
|
|
2292
|
-
console.log(
|
|
2293
|
-
console.log(
|
|
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(
|
|
2297
|
-
diff.onlyInKeyway.slice(0, 3).forEach((key) => console.log(
|
|
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(
|
|
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(
|
|
2304
|
-
diff.onlyInProvider.slice(0, 3).forEach((key) => console.log(
|
|
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(
|
|
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(
|
|
2311
|
-
diff.different.slice(0, 3).forEach((key) => console.log(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
2550
|
+
badges.push(pc12.green("\u2190 linked"));
|
|
2365
2551
|
} else if (p.name.toLowerCase() === repoName || p.serviceName?.toLowerCase() === repoName) {
|
|
2366
|
-
badges.push(
|
|
2552
|
+
badges.push(pc12.green("\u2190 same name"));
|
|
2367
2553
|
} else if (p.linkedRepo) {
|
|
2368
|
-
badges.push(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
2391
|
-
console.log(
|
|
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(
|
|
2398
|
-
console.log(
|
|
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(
|
|
2594
|
+
console.log(pc12.gray(`Repository: ${repoFullName}`));
|
|
2402
2595
|
const vaultExists = await checkVaultExists(accessToken, repoFullName);
|
|
2403
2596
|
if (!vaultExists) {
|
|
2404
|
-
console.log(
|
|
2597
|
+
console.log(pc12.yellow(`
|
|
2405
2598
|
No vault found for ${repoFullName}.`));
|
|
2406
|
-
const { shouldCreate } = await
|
|
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(
|
|
2606
|
+
console.log(pc12.gray("Cancelled. Run `keyway init` to create a vault first."));
|
|
2414
2607
|
process.exit(0);
|
|
2415
2608
|
}
|
|
2416
|
-
console.log(
|
|
2609
|
+
console.log(pc12.gray("\nCreating vault..."));
|
|
2417
2610
|
try {
|
|
2418
2611
|
await initVault(repoFullName, accessToken);
|
|
2419
|
-
console.log(
|
|
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(
|
|
2616
|
+
console.error(pc12.red(`
|
|
2424
2617
|
\u2717 ${message}`));
|
|
2425
2618
|
process.exit(1);
|
|
2426
2619
|
}
|
|
2427
2620
|
}
|
|
2428
|
-
|
|
2429
|
-
let
|
|
2430
|
-
if (
|
|
2431
|
-
|
|
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
|
|
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(
|
|
2633
|
+
console.log(pc12.gray("Cancelled."));
|
|
2442
2634
|
process.exit(0);
|
|
2443
2635
|
}
|
|
2444
2636
|
await connectCommand(provider, { loginPrompt: false });
|
|
2445
|
-
const refreshed = await
|
|
2637
|
+
const refreshed = await getAllProviderProjects(accessToken, provider.toLowerCase());
|
|
2638
|
+
allProjects = refreshed.projects;
|
|
2446
2639
|
connections = refreshed.connections;
|
|
2447
|
-
|
|
2448
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
2467
|
-
console.log(
|
|
2468
|
-
projects.forEach((p) => console.log(
|
|
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(
|
|
2475
|
-
console.log(
|
|
2476
|
-
console.log(
|
|
2477
|
-
console.log(
|
|
2478
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
2493
|
-
const { useDetected } = await
|
|
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
|
-
|
|
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(
|
|
2509
|
-
console.log(
|
|
2510
|
-
console.log(
|
|
2511
|
-
console.log(
|
|
2512
|
-
console.log(
|
|
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(
|
|
2752
|
+
console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2515
2753
|
}
|
|
2516
2754
|
console.log("");
|
|
2517
|
-
const { continueAnyway } = await
|
|
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(
|
|
2762
|
+
console.log(pc12.gray("Cancelled."));
|
|
2525
2763
|
process.exit(0);
|
|
2526
2764
|
}
|
|
2527
2765
|
}
|
|
2528
2766
|
} else {
|
|
2529
|
-
console.log(
|
|
2767
|
+
console.log(pc12.yellow(`
|
|
2530
2768
|
\u26A0\uFE0F No matching project found for ${repoFullName}`));
|
|
2531
|
-
console.log(
|
|
2532
|
-
|
|
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(
|
|
2540
|
-
console.log(
|
|
2541
|
-
console.log(
|
|
2542
|
-
console.log(
|
|
2543
|
-
console.log(
|
|
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(
|
|
2785
|
+
console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2546
2786
|
}
|
|
2547
2787
|
console.log("");
|
|
2548
|
-
const { continueAnyway } = await
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
2831
|
+
console.log(pc12.gray(`Using ${providerName} environment: ${providerEnv}`));
|
|
2592
2832
|
} else {
|
|
2593
|
-
const { selectedProviderEnv } = await
|
|
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(
|
|
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(
|
|
2857
|
+
console.log(pc12.gray("\nComparing secrets..."));
|
|
2618
2858
|
diff = await getSyncDiff(accessToken, repoFullName, {
|
|
2619
|
-
connectionId:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
2670
|
-
const { importFirst } = await
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
2970
|
+
console.log(pc12.green("\n\u2713 Already in sync. No changes needed."));
|
|
2731
2971
|
return;
|
|
2732
2972
|
}
|
|
2733
|
-
console.log(
|
|
2973
|
+
console.log(pc12.blue("\n\u{1F4CB} Sync Preview\n"));
|
|
2734
2974
|
if (preview.toCreate.length > 0) {
|
|
2735
|
-
console.log(
|
|
2736
|
-
preview.toCreate.slice(0, 5).forEach((key) => console.log(
|
|
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(
|
|
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(
|
|
2743
|
-
preview.toUpdate.slice(0, 5).forEach((key) => console.log(
|
|
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(
|
|
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(
|
|
2750
|
-
preview.toDelete.slice(0, 5).forEach((key) => console.log(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
3008
|
+
console.log(pc12.gray("Cancelled."));
|
|
2769
3009
|
return;
|
|
2770
3010
|
}
|
|
2771
3011
|
}
|
|
2772
|
-
console.log(
|
|
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(
|
|
2785
|
-
console.log(
|
|
2786
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
3052
|
+
const text = pc13.bold(pc13.cyan("Keyway CLI"));
|
|
2813
3053
|
console.log(`
|
|
2814
3054
|
${text}
|
|
2815
|
-
${
|
|
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(
|
|
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.
|
|
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",
|