@keywaysh/cli 0.1.14 → 0.1.15
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 +365 -163
- 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.15",
|
|
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
|
}
|
|
@@ -2356,30 +2499,42 @@ function projectMatchesRepo(project, repoFullName) {
|
|
|
2356
2499
|
}
|
|
2357
2500
|
async function promptProjectSelection(projects, repoFullName) {
|
|
2358
2501
|
const repoName = repoFullName.split("/")[1]?.toLowerCase() || "";
|
|
2502
|
+
const uniqueTeams = new Set(projects.map((p) => p.teamId || "personal"));
|
|
2503
|
+
const hasMultipleAccounts = uniqueTeams.size > 1;
|
|
2359
2504
|
const choices = projects.map((p) => {
|
|
2360
2505
|
const displayName = getProjectDisplayName(p);
|
|
2361
2506
|
let title = displayName;
|
|
2362
2507
|
const badges = [];
|
|
2508
|
+
if (hasMultipleAccounts) {
|
|
2509
|
+
if (p.teamName) {
|
|
2510
|
+
badges.push(pc12.cyan(`[${p.teamName}]`));
|
|
2511
|
+
} else if (p.teamId) {
|
|
2512
|
+
const shortTeamId = p.teamId.length > 12 ? p.teamId.slice(0, 12) + "..." : p.teamId;
|
|
2513
|
+
badges.push(pc12.cyan(`[team:${shortTeamId}]`));
|
|
2514
|
+
} else {
|
|
2515
|
+
badges.push(pc12.cyan("[personal]"));
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2363
2518
|
if (p.linkedRepo?.toLowerCase() === repoFullName.toLowerCase()) {
|
|
2364
|
-
badges.push(
|
|
2519
|
+
badges.push(pc12.green("\u2190 linked"));
|
|
2365
2520
|
} else if (p.name.toLowerCase() === repoName || p.serviceName?.toLowerCase() === repoName) {
|
|
2366
|
-
badges.push(
|
|
2521
|
+
badges.push(pc12.green("\u2190 same name"));
|
|
2367
2522
|
} else if (p.linkedRepo) {
|
|
2368
|
-
badges.push(
|
|
2523
|
+
badges.push(pc12.gray(`\u2192 ${p.linkedRepo}`));
|
|
2369
2524
|
}
|
|
2370
2525
|
if (badges.length > 0) {
|
|
2371
2526
|
title = `${displayName} ${badges.join(" ")}`;
|
|
2372
2527
|
}
|
|
2373
2528
|
return { title, value: p.id };
|
|
2374
2529
|
});
|
|
2375
|
-
const { projectChoice } = await
|
|
2530
|
+
const { projectChoice } = await prompts10({
|
|
2376
2531
|
type: "select",
|
|
2377
2532
|
name: "projectChoice",
|
|
2378
2533
|
message: "Select a project:",
|
|
2379
2534
|
choices
|
|
2380
2535
|
});
|
|
2381
2536
|
if (!projectChoice) {
|
|
2382
|
-
console.log(
|
|
2537
|
+
console.log(pc12.gray("Cancelled."));
|
|
2383
2538
|
process.exit(0);
|
|
2384
2539
|
}
|
|
2385
2540
|
return projects.find((p) => p.id === projectChoice);
|
|
@@ -2387,97 +2542,133 @@ async function promptProjectSelection(projects, repoFullName) {
|
|
|
2387
2542
|
async function syncCommand(provider, options = {}) {
|
|
2388
2543
|
try {
|
|
2389
2544
|
if (options.pull && options.allowDelete) {
|
|
2390
|
-
console.error(
|
|
2391
|
-
console.log(
|
|
2545
|
+
console.error(pc12.red("Error: --allow-delete cannot be used with --pull"));
|
|
2546
|
+
console.log(pc12.gray("The --allow-delete flag is only for push operations."));
|
|
2392
2547
|
process.exit(1);
|
|
2393
2548
|
}
|
|
2394
2549
|
const accessToken = await ensureLogin({ allowPrompt: options.loginPrompt !== false });
|
|
2395
2550
|
const repoFullName = detectGitRepo();
|
|
2396
2551
|
if (!repoFullName) {
|
|
2397
|
-
console.error(
|
|
2398
|
-
console.log(
|
|
2552
|
+
console.error(pc12.red("Could not detect Git repository."));
|
|
2553
|
+
console.log(pc12.gray("Run this command from a Git repository directory."));
|
|
2399
2554
|
process.exit(1);
|
|
2400
2555
|
}
|
|
2401
|
-
console.log(
|
|
2556
|
+
console.log(pc12.gray(`Repository: ${repoFullName}`));
|
|
2402
2557
|
const vaultExists = await checkVaultExists(accessToken, repoFullName);
|
|
2403
2558
|
if (!vaultExists) {
|
|
2404
|
-
console.log(
|
|
2559
|
+
console.log(pc12.yellow(`
|
|
2405
2560
|
No vault found for ${repoFullName}.`));
|
|
2406
|
-
const { shouldCreate } = await
|
|
2561
|
+
const { shouldCreate } = await prompts10({
|
|
2407
2562
|
type: "confirm",
|
|
2408
2563
|
name: "shouldCreate",
|
|
2409
2564
|
message: "Create vault now?",
|
|
2410
2565
|
initial: true
|
|
2411
2566
|
});
|
|
2412
2567
|
if (!shouldCreate) {
|
|
2413
|
-
console.log(
|
|
2568
|
+
console.log(pc12.gray("Cancelled. Run `keyway init` to create a vault first."));
|
|
2414
2569
|
process.exit(0);
|
|
2415
2570
|
}
|
|
2416
|
-
console.log(
|
|
2571
|
+
console.log(pc12.gray("\nCreating vault..."));
|
|
2417
2572
|
try {
|
|
2418
2573
|
await initVault(repoFullName, accessToken);
|
|
2419
|
-
console.log(
|
|
2574
|
+
console.log(pc12.green(`\u2713 Vault created for ${repoFullName}
|
|
2420
2575
|
`));
|
|
2421
2576
|
} catch (error) {
|
|
2422
2577
|
const message = error instanceof Error ? error.message : "Failed to create vault";
|
|
2423
|
-
console.error(
|
|
2578
|
+
console.error(pc12.red(`
|
|
2424
2579
|
\u2717 ${message}`));
|
|
2425
2580
|
process.exit(1);
|
|
2426
2581
|
}
|
|
2427
2582
|
}
|
|
2428
|
-
|
|
2429
|
-
let
|
|
2430
|
-
if (
|
|
2431
|
-
|
|
2432
|
-
console.log(pc11.yellow(`
|
|
2583
|
+
const providerDisplayName = provider.charAt(0).toUpperCase() + provider.slice(1);
|
|
2584
|
+
let { projects: allProjects, connections } = await getAllProviderProjects(accessToken, provider.toLowerCase());
|
|
2585
|
+
if (connections.length === 0) {
|
|
2586
|
+
console.log(pc12.yellow(`
|
|
2433
2587
|
Not connected to ${providerDisplayName}.`));
|
|
2434
|
-
const { shouldConnect } = await
|
|
2588
|
+
const { shouldConnect } = await prompts10({
|
|
2435
2589
|
type: "confirm",
|
|
2436
2590
|
name: "shouldConnect",
|
|
2437
2591
|
message: `Connect to ${providerDisplayName} now?`,
|
|
2438
2592
|
initial: true
|
|
2439
2593
|
});
|
|
2440
2594
|
if (!shouldConnect) {
|
|
2441
|
-
console.log(
|
|
2595
|
+
console.log(pc12.gray("Cancelled."));
|
|
2442
2596
|
process.exit(0);
|
|
2443
2597
|
}
|
|
2444
2598
|
await connectCommand(provider, { loginPrompt: false });
|
|
2445
|
-
const refreshed = await
|
|
2599
|
+
const refreshed = await getAllProviderProjects(accessToken, provider.toLowerCase());
|
|
2600
|
+
allProjects = refreshed.projects;
|
|
2446
2601
|
connections = refreshed.connections;
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
console.error(pc11.red(`
|
|
2602
|
+
if (connections.length === 0) {
|
|
2603
|
+
console.error(pc12.red(`
|
|
2450
2604
|
Connection to ${providerDisplayName} failed.`));
|
|
2451
2605
|
process.exit(1);
|
|
2452
2606
|
}
|
|
2453
2607
|
console.log("");
|
|
2454
2608
|
}
|
|
2455
|
-
|
|
2609
|
+
let projects = allProjects.map((p) => ({
|
|
2610
|
+
id: p.id,
|
|
2611
|
+
name: p.name,
|
|
2612
|
+
serviceId: p.serviceId,
|
|
2613
|
+
serviceName: p.serviceName,
|
|
2614
|
+
linkedRepo: p.linkedRepo,
|
|
2615
|
+
environments: p.environments,
|
|
2616
|
+
connectionId: p.connectionId,
|
|
2617
|
+
teamId: p.teamId,
|
|
2618
|
+
teamName: p.teamName
|
|
2619
|
+
}));
|
|
2620
|
+
if (options.team) {
|
|
2621
|
+
const teamFilter = options.team.toLowerCase();
|
|
2622
|
+
const filteredProjects = projects.filter(
|
|
2623
|
+
(p) => p.teamId?.toLowerCase() === teamFilter || p.teamName?.toLowerCase() === teamFilter || // Match "personal" for null teamId
|
|
2624
|
+
teamFilter === "personal" && !p.teamId
|
|
2625
|
+
);
|
|
2626
|
+
if (filteredProjects.length === 0) {
|
|
2627
|
+
console.error(pc12.red(`No projects found for team: ${options.team}`));
|
|
2628
|
+
console.log(pc12.gray("Available teams:"));
|
|
2629
|
+
const teams = /* @__PURE__ */ new Set();
|
|
2630
|
+
projects.forEach((p) => {
|
|
2631
|
+
if (p.teamName) teams.add(p.teamName);
|
|
2632
|
+
else if (p.teamId) teams.add(p.teamId);
|
|
2633
|
+
else teams.add("personal");
|
|
2634
|
+
});
|
|
2635
|
+
teams.forEach((t) => console.log(pc12.gray(` - ${t}`)));
|
|
2636
|
+
process.exit(1);
|
|
2637
|
+
}
|
|
2638
|
+
projects = filteredProjects;
|
|
2639
|
+
console.log(pc12.gray(`Filtered to ${projects.length} projects in team: ${options.team}`));
|
|
2640
|
+
}
|
|
2456
2641
|
if (projects.length === 0) {
|
|
2457
|
-
console.error(
|
|
2642
|
+
console.error(pc12.red(`No projects found in your ${providerDisplayName} account(s).`));
|
|
2643
|
+
if (connections.length > 1) {
|
|
2644
|
+
console.log(pc12.gray(`Checked ${connections.length} connected accounts.`));
|
|
2645
|
+
}
|
|
2458
2646
|
process.exit(1);
|
|
2459
2647
|
}
|
|
2648
|
+
if (connections.length > 1 && !options.team) {
|
|
2649
|
+
console.log(pc12.gray(`Searching ${projects.length} projects across ${connections.length} ${providerDisplayName} accounts...`));
|
|
2650
|
+
}
|
|
2460
2651
|
let selectedProject;
|
|
2461
2652
|
if (options.project) {
|
|
2462
2653
|
const found = projects.find(
|
|
2463
2654
|
(p) => p.id === options.project || p.name.toLowerCase() === options.project?.toLowerCase() || p.serviceName?.toLowerCase() === options.project?.toLowerCase()
|
|
2464
2655
|
);
|
|
2465
2656
|
if (!found) {
|
|
2466
|
-
console.error(
|
|
2467
|
-
console.log(
|
|
2468
|
-
projects.forEach((p) => console.log(
|
|
2657
|
+
console.error(pc12.red(`Project not found: ${options.project}`));
|
|
2658
|
+
console.log(pc12.gray("Available projects:"));
|
|
2659
|
+
projects.forEach((p) => console.log(pc12.gray(` - ${getProjectDisplayName(p)}`)));
|
|
2469
2660
|
process.exit(1);
|
|
2470
2661
|
}
|
|
2471
2662
|
selectedProject = found;
|
|
2472
2663
|
if (!projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2473
2664
|
console.log("");
|
|
2474
|
-
console.log(
|
|
2475
|
-
console.log(
|
|
2476
|
-
console.log(
|
|
2477
|
-
console.log(
|
|
2478
|
-
console.log(
|
|
2665
|
+
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"));
|
|
2666
|
+
console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
|
|
2667
|
+
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"));
|
|
2668
|
+
console.log(pc12.yellow(` Current repo: ${repoFullName}`));
|
|
2669
|
+
console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
|
|
2479
2670
|
if (selectedProject.linkedRepo) {
|
|
2480
|
-
console.log(
|
|
2671
|
+
console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2481
2672
|
}
|
|
2482
2673
|
console.log("");
|
|
2483
2674
|
}
|
|
@@ -2486,11 +2677,18 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2486
2677
|
if (autoMatch && (autoMatch.matchType === "linked_repo" || autoMatch.matchType === "exact_name")) {
|
|
2487
2678
|
selectedProject = autoMatch.project;
|
|
2488
2679
|
const matchReason = autoMatch.matchType === "linked_repo" ? `linked to ${repoFullName}` : "exact name match";
|
|
2489
|
-
|
|
2680
|
+
let teamInfo = "";
|
|
2681
|
+
if (selectedProject.teamName) {
|
|
2682
|
+
teamInfo = pc12.gray(` (${selectedProject.teamName})`);
|
|
2683
|
+
} else if (selectedProject.teamId && connections.length > 1) {
|
|
2684
|
+
const shortTeamId = selectedProject.teamId.length > 12 ? selectedProject.teamId.slice(0, 12) + "..." : selectedProject.teamId;
|
|
2685
|
+
teamInfo = pc12.gray(` (team:${shortTeamId})`);
|
|
2686
|
+
}
|
|
2687
|
+
console.log(pc12.green(`\u2713 Auto-selected project: ${getProjectDisplayName(selectedProject)}${teamInfo} (${matchReason})`));
|
|
2490
2688
|
} else if (autoMatch && autoMatch.matchType === "partial_name") {
|
|
2491
2689
|
const partialDisplayName = getProjectDisplayName(autoMatch.project);
|
|
2492
|
-
console.log(
|
|
2493
|
-
const { useDetected } = await
|
|
2690
|
+
console.log(pc12.yellow(`Detected project: ${partialDisplayName} (partial match)`));
|
|
2691
|
+
const { useDetected } = await prompts10({
|
|
2494
2692
|
type: "confirm",
|
|
2495
2693
|
name: "useDetected",
|
|
2496
2694
|
message: `Use ${partialDisplayName}?`,
|
|
@@ -2505,30 +2703,30 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2505
2703
|
selectedProject = projects[0];
|
|
2506
2704
|
if (!projectMatchesRepo(selectedProject, repoFullName)) {
|
|
2507
2705
|
console.log("");
|
|
2508
|
-
console.log(
|
|
2509
|
-
console.log(
|
|
2510
|
-
console.log(
|
|
2511
|
-
console.log(
|
|
2512
|
-
console.log(
|
|
2706
|
+
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"));
|
|
2707
|
+
console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: Project does not match current repository \u2502"));
|
|
2708
|
+
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"));
|
|
2709
|
+
console.log(pc12.yellow(` Current repo: ${repoFullName}`));
|
|
2710
|
+
console.log(pc12.yellow(` Only project: ${getProjectDisplayName(selectedProject)}`));
|
|
2513
2711
|
if (selectedProject.linkedRepo) {
|
|
2514
|
-
console.log(
|
|
2712
|
+
console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2515
2713
|
}
|
|
2516
2714
|
console.log("");
|
|
2517
|
-
const { continueAnyway } = await
|
|
2715
|
+
const { continueAnyway } = await prompts10({
|
|
2518
2716
|
type: "confirm",
|
|
2519
2717
|
name: "continueAnyway",
|
|
2520
2718
|
message: "Continue anyway?",
|
|
2521
2719
|
initial: false
|
|
2522
2720
|
});
|
|
2523
2721
|
if (!continueAnyway) {
|
|
2524
|
-
console.log(
|
|
2722
|
+
console.log(pc12.gray("Cancelled."));
|
|
2525
2723
|
process.exit(0);
|
|
2526
2724
|
}
|
|
2527
2725
|
}
|
|
2528
2726
|
} else {
|
|
2529
|
-
console.log(
|
|
2727
|
+
console.log(pc12.yellow(`
|
|
2530
2728
|
\u26A0\uFE0F No matching project found for ${repoFullName}`));
|
|
2531
|
-
console.log(
|
|
2729
|
+
console.log(pc12.gray("Select a project manually:\n"));
|
|
2532
2730
|
selectedProject = await promptProjectSelection(projects, repoFullName);
|
|
2533
2731
|
}
|
|
2534
2732
|
}
|
|
@@ -2536,23 +2734,23 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2536
2734
|
const autoMatch = findMatchingProject(projects, repoFullName);
|
|
2537
2735
|
if (autoMatch && autoMatch.project.id !== selectedProject.id) {
|
|
2538
2736
|
console.log("");
|
|
2539
|
-
console.log(
|
|
2540
|
-
console.log(
|
|
2541
|
-
console.log(
|
|
2542
|
-
console.log(
|
|
2543
|
-
console.log(
|
|
2737
|
+
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"));
|
|
2738
|
+
console.log(pc12.yellow("\u2502 \u26A0\uFE0F WARNING: You selected a different project \u2502"));
|
|
2739
|
+
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"));
|
|
2740
|
+
console.log(pc12.yellow(` Current repo: ${repoFullName}`));
|
|
2741
|
+
console.log(pc12.yellow(` Selected project: ${getProjectDisplayName(selectedProject)}`));
|
|
2544
2742
|
if (selectedProject.linkedRepo) {
|
|
2545
|
-
console.log(
|
|
2743
|
+
console.log(pc12.yellow(` Project linked to: ${selectedProject.linkedRepo}`));
|
|
2546
2744
|
}
|
|
2547
2745
|
console.log("");
|
|
2548
|
-
const { continueAnyway } = await
|
|
2746
|
+
const { continueAnyway } = await prompts10({
|
|
2549
2747
|
type: "confirm",
|
|
2550
2748
|
name: "continueAnyway",
|
|
2551
2749
|
message: "Are you sure you want to sync with this project?",
|
|
2552
2750
|
initial: false
|
|
2553
2751
|
});
|
|
2554
2752
|
if (!continueAnyway) {
|
|
2555
|
-
console.log(
|
|
2753
|
+
console.log(pc12.gray("Cancelled."));
|
|
2556
2754
|
process.exit(0);
|
|
2557
2755
|
}
|
|
2558
2756
|
}
|
|
@@ -2566,7 +2764,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2566
2764
|
if (needsEnvPrompt || needsDirectionPrompt) {
|
|
2567
2765
|
if (needsEnvPrompt) {
|
|
2568
2766
|
const vaultEnvs = await getVaultEnvironments(accessToken, repoFullName);
|
|
2569
|
-
const { selectedEnv } = await
|
|
2767
|
+
const { selectedEnv } = await prompts10({
|
|
2570
2768
|
type: "select",
|
|
2571
2769
|
name: "selectedEnv",
|
|
2572
2770
|
message: "Keyway environment:",
|
|
@@ -2574,7 +2772,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2574
2772
|
initial: Math.max(0, vaultEnvs.indexOf("production"))
|
|
2575
2773
|
});
|
|
2576
2774
|
if (!selectedEnv) {
|
|
2577
|
-
console.log(
|
|
2775
|
+
console.log(pc12.gray("Cancelled."));
|
|
2578
2776
|
process.exit(0);
|
|
2579
2777
|
}
|
|
2580
2778
|
keywayEnv = selectedEnv;
|
|
@@ -2588,9 +2786,9 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2588
2786
|
providerEnv = mappedEnv;
|
|
2589
2787
|
} else if (selectedProject.environments.length === 1) {
|
|
2590
2788
|
providerEnv = selectedProject.environments[0];
|
|
2591
|
-
console.log(
|
|
2789
|
+
console.log(pc12.gray(`Using ${providerName} environment: ${providerEnv}`));
|
|
2592
2790
|
} else {
|
|
2593
|
-
const { selectedProviderEnv } = await
|
|
2791
|
+
const { selectedProviderEnv } = await prompts10({
|
|
2594
2792
|
type: "select",
|
|
2595
2793
|
name: "selectedProviderEnv",
|
|
2596
2794
|
message: `${providerName} environment:`,
|
|
@@ -2600,7 +2798,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2600
2798
|
))
|
|
2601
2799
|
});
|
|
2602
2800
|
if (!selectedProviderEnv) {
|
|
2603
|
-
console.log(
|
|
2801
|
+
console.log(pc12.gray("Cancelled."));
|
|
2604
2802
|
process.exit(0);
|
|
2605
2803
|
}
|
|
2606
2804
|
providerEnv = selectedProviderEnv;
|
|
@@ -2614,9 +2812,9 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2614
2812
|
if (needsDirectionPrompt) {
|
|
2615
2813
|
const effectiveKeywayEnv = keywayEnv || "production";
|
|
2616
2814
|
const effectiveProviderEnv = providerEnv || mapToProviderEnvironment(provider, effectiveKeywayEnv);
|
|
2617
|
-
console.log(
|
|
2815
|
+
console.log(pc12.gray("\nComparing secrets..."));
|
|
2618
2816
|
diff = await getSyncDiff(accessToken, repoFullName, {
|
|
2619
|
-
connectionId:
|
|
2817
|
+
connectionId: selectedProject.connectionId,
|
|
2620
2818
|
projectId: selectedProject.id,
|
|
2621
2819
|
serviceId: selectedProject.serviceId,
|
|
2622
2820
|
// Railway: service ID for service-specific variables
|
|
@@ -2636,7 +2834,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2636
2834
|
} else if (diff.providerCount === 0 && diff.keywayCount > 0) {
|
|
2637
2835
|
defaultDirection = 0;
|
|
2638
2836
|
}
|
|
2639
|
-
const { selectedDirection } = await
|
|
2837
|
+
const { selectedDirection } = await prompts10({
|
|
2640
2838
|
type: "select",
|
|
2641
2839
|
name: "selectedDirection",
|
|
2642
2840
|
message: "Sync direction:",
|
|
@@ -2647,7 +2845,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2647
2845
|
initial: defaultDirection
|
|
2648
2846
|
});
|
|
2649
2847
|
if (!selectedDirection) {
|
|
2650
|
-
console.log(
|
|
2848
|
+
console.log(pc12.gray("Cancelled."));
|
|
2651
2849
|
process.exit(0);
|
|
2652
2850
|
}
|
|
2653
2851
|
direction = selectedDirection;
|
|
@@ -2659,15 +2857,15 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2659
2857
|
const status = await getSyncStatus(
|
|
2660
2858
|
accessToken,
|
|
2661
2859
|
repoFullName,
|
|
2662
|
-
|
|
2860
|
+
selectedProject.connectionId,
|
|
2663
2861
|
selectedProject.id,
|
|
2664
2862
|
keywayEnv
|
|
2665
2863
|
);
|
|
2666
2864
|
if (status.isFirstSync && direction === "push" && status.vaultIsEmpty && status.providerHasSecrets) {
|
|
2667
|
-
console.log(
|
|
2865
|
+
console.log(pc12.yellow(`
|
|
2668
2866
|
\u26A0\uFE0F Your Keyway vault is empty for "${keywayEnv}", but ${providerName} has ${status.providerSecretCount} secrets.`));
|
|
2669
|
-
console.log(
|
|
2670
|
-
const { importFirst } = await
|
|
2867
|
+
console.log(pc12.gray(` (Use --environment to sync a different environment)`));
|
|
2868
|
+
const { importFirst } = await prompts10({
|
|
2671
2869
|
type: "confirm",
|
|
2672
2870
|
name: "importFirst",
|
|
2673
2871
|
message: `Import secrets from ${providerName} first?`,
|
|
@@ -2677,7 +2875,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2677
2875
|
await executeSyncOperation(
|
|
2678
2876
|
accessToken,
|
|
2679
2877
|
repoFullName,
|
|
2680
|
-
|
|
2878
|
+
selectedProject.connectionId,
|
|
2681
2879
|
selectedProject,
|
|
2682
2880
|
keywayEnv,
|
|
2683
2881
|
providerEnv,
|
|
@@ -2693,7 +2891,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2693
2891
|
await executeSyncOperation(
|
|
2694
2892
|
accessToken,
|
|
2695
2893
|
repoFullName,
|
|
2696
|
-
|
|
2894
|
+
selectedProject.connectionId,
|
|
2697
2895
|
selectedProject,
|
|
2698
2896
|
keywayEnv,
|
|
2699
2897
|
providerEnv,
|
|
@@ -2708,7 +2906,7 @@ Connection to ${providerDisplayName} failed.`));
|
|
|
2708
2906
|
command: "sync",
|
|
2709
2907
|
error: truncateMessage(message)
|
|
2710
2908
|
});
|
|
2711
|
-
console.error(
|
|
2909
|
+
console.error(pc12.red(`
|
|
2712
2910
|
\u2717 ${message}`));
|
|
2713
2911
|
process.exit(1);
|
|
2714
2912
|
}
|
|
@@ -2727,49 +2925,49 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2727
2925
|
});
|
|
2728
2926
|
const totalChanges = preview.toCreate.length + preview.toUpdate.length + preview.toDelete.length;
|
|
2729
2927
|
if (totalChanges === 0) {
|
|
2730
|
-
console.log(
|
|
2928
|
+
console.log(pc12.green("\n\u2713 Already in sync. No changes needed."));
|
|
2731
2929
|
return;
|
|
2732
2930
|
}
|
|
2733
|
-
console.log(
|
|
2931
|
+
console.log(pc12.blue("\n\u{1F4CB} Sync Preview\n"));
|
|
2734
2932
|
if (preview.toCreate.length > 0) {
|
|
2735
|
-
console.log(
|
|
2736
|
-
preview.toCreate.slice(0, 5).forEach((key) => console.log(
|
|
2933
|
+
console.log(pc12.green(` + ${preview.toCreate.length} to create`));
|
|
2934
|
+
preview.toCreate.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
|
|
2737
2935
|
if (preview.toCreate.length > 5) {
|
|
2738
|
-
console.log(
|
|
2936
|
+
console.log(pc12.gray(` ... and ${preview.toCreate.length - 5} more`));
|
|
2739
2937
|
}
|
|
2740
2938
|
}
|
|
2741
2939
|
if (preview.toUpdate.length > 0) {
|
|
2742
|
-
console.log(
|
|
2743
|
-
preview.toUpdate.slice(0, 5).forEach((key) => console.log(
|
|
2940
|
+
console.log(pc12.yellow(` ~ ${preview.toUpdate.length} to update`));
|
|
2941
|
+
preview.toUpdate.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
|
|
2744
2942
|
if (preview.toUpdate.length > 5) {
|
|
2745
|
-
console.log(
|
|
2943
|
+
console.log(pc12.gray(` ... and ${preview.toUpdate.length - 5} more`));
|
|
2746
2944
|
}
|
|
2747
2945
|
}
|
|
2748
2946
|
if (preview.toDelete.length > 0) {
|
|
2749
|
-
console.log(
|
|
2750
|
-
preview.toDelete.slice(0, 5).forEach((key) => console.log(
|
|
2947
|
+
console.log(pc12.red(` - ${preview.toDelete.length} to delete`));
|
|
2948
|
+
preview.toDelete.slice(0, 5).forEach((key) => console.log(pc12.gray(` ${key}`)));
|
|
2751
2949
|
if (preview.toDelete.length > 5) {
|
|
2752
|
-
console.log(
|
|
2950
|
+
console.log(pc12.gray(` ... and ${preview.toDelete.length - 5} more`));
|
|
2753
2951
|
}
|
|
2754
2952
|
}
|
|
2755
2953
|
if (preview.toSkip.length > 0) {
|
|
2756
|
-
console.log(
|
|
2954
|
+
console.log(pc12.gray(` \u25CB ${preview.toSkip.length} unchanged`));
|
|
2757
2955
|
}
|
|
2758
2956
|
console.log("");
|
|
2759
2957
|
if (!skipConfirm) {
|
|
2760
2958
|
const target = direction === "push" ? providerName : "Keyway";
|
|
2761
|
-
const { confirm } = await
|
|
2959
|
+
const { confirm } = await prompts10({
|
|
2762
2960
|
type: "confirm",
|
|
2763
2961
|
name: "confirm",
|
|
2764
2962
|
message: `Apply ${totalChanges} changes to ${target}?`,
|
|
2765
2963
|
initial: true
|
|
2766
2964
|
});
|
|
2767
2965
|
if (!confirm) {
|
|
2768
|
-
console.log(
|
|
2966
|
+
console.log(pc12.gray("Cancelled."));
|
|
2769
2967
|
return;
|
|
2770
2968
|
}
|
|
2771
2969
|
}
|
|
2772
|
-
console.log(
|
|
2970
|
+
console.log(pc12.blue("\n\u23F3 Syncing...\n"));
|
|
2773
2971
|
const result = await executeSync(accessToken, repoFullName, {
|
|
2774
2972
|
connectionId,
|
|
2775
2973
|
projectId: project.id,
|
|
@@ -2781,11 +2979,11 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2781
2979
|
allowDelete
|
|
2782
2980
|
});
|
|
2783
2981
|
if (result.success) {
|
|
2784
|
-
console.log(
|
|
2785
|
-
console.log(
|
|
2786
|
-
console.log(
|
|
2982
|
+
console.log(pc12.green("\u2713 Sync complete"));
|
|
2983
|
+
console.log(pc12.gray(` Created: ${result.stats.created}`));
|
|
2984
|
+
console.log(pc12.gray(` Updated: ${result.stats.updated}`));
|
|
2787
2985
|
if (result.stats.deleted > 0) {
|
|
2788
|
-
console.log(
|
|
2986
|
+
console.log(pc12.gray(` Deleted: ${result.stats.deleted}`));
|
|
2789
2987
|
}
|
|
2790
2988
|
trackEvent(AnalyticsEvents.CLI_SYNC, {
|
|
2791
2989
|
provider,
|
|
@@ -2795,7 +2993,7 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2795
2993
|
deleted: result.stats.deleted
|
|
2796
2994
|
});
|
|
2797
2995
|
} else {
|
|
2798
|
-
console.error(
|
|
2996
|
+
console.error(pc12.red(`
|
|
2799
2997
|
\u2717 ${result.error}`));
|
|
2800
2998
|
process.exit(1);
|
|
2801
2999
|
}
|
|
@@ -2803,16 +3001,16 @@ async function executeSyncOperation(accessToken, repoFullName, connectionId, pro
|
|
|
2803
3001
|
|
|
2804
3002
|
// src/cli.ts
|
|
2805
3003
|
process.on("unhandledRejection", (reason) => {
|
|
2806
|
-
console.error(
|
|
3004
|
+
console.error(pc13.red("Unhandled error:"), reason);
|
|
2807
3005
|
process.exit(1);
|
|
2808
3006
|
});
|
|
2809
3007
|
var program = new Command();
|
|
2810
3008
|
var TAGLINE = "Sync secrets with your team and infra";
|
|
2811
3009
|
var showBanner = () => {
|
|
2812
|
-
const text =
|
|
3010
|
+
const text = pc13.bold(pc13.cyan("Keyway CLI"));
|
|
2813
3011
|
console.log(`
|
|
2814
3012
|
${text}
|
|
2815
|
-
${
|
|
3013
|
+
${pc13.gray(TAGLINE)}
|
|
2816
3014
|
`);
|
|
2817
3015
|
};
|
|
2818
3016
|
showBanner();
|
|
@@ -2844,13 +3042,17 @@ program.command("connections").description("List your provider connections").opt
|
|
|
2844
3042
|
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
3043
|
await disconnectCommand(provider, options);
|
|
2846
3044
|
});
|
|
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) => {
|
|
3045
|
+
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
3046
|
await syncCommand(provider, options);
|
|
2849
3047
|
});
|
|
3048
|
+
var ci = program.command("ci").description("CI/CD integration commands");
|
|
3049
|
+
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) => {
|
|
3050
|
+
await ciSetupCommand(options);
|
|
3051
|
+
});
|
|
2850
3052
|
(async () => {
|
|
2851
3053
|
await warnIfEnvNotGitignored();
|
|
2852
3054
|
await program.parseAsync();
|
|
2853
3055
|
})().catch((error) => {
|
|
2854
|
-
console.error(
|
|
3056
|
+
console.error(pc13.red("Error:"), error.message || error);
|
|
2855
3057
|
process.exit(1);
|
|
2856
3058
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keywaysh/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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",
|