@keynv/cli 0.1.0-rc.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1783 -977
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
-
import 'crypto';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
4
|
import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { isAbsolute, resolve, join, dirname, basename } from 'path';
|
|
5
|
+
import { hostname, platform, homedir } from 'os';
|
|
6
|
+
import { relative, isAbsolute, resolve, join, dirname, basename } from 'path';
|
|
7
7
|
import { Entry } from '@napi-rs/keyring';
|
|
8
8
|
import { styleText } from 'util';
|
|
9
9
|
import j2, { stdin, stdout } from 'process';
|
|
@@ -579,16 +579,16 @@ var require_lib = __commonJS({
|
|
|
579
579
|
}
|
|
580
580
|
var TypeAssertionError = class extends Error {
|
|
581
581
|
constructor({ errors } = {}) {
|
|
582
|
-
let
|
|
582
|
+
let errorMessage2 = `Type mismatch`;
|
|
583
583
|
if (errors && errors.length > 0) {
|
|
584
|
-
|
|
584
|
+
errorMessage2 += `
|
|
585
585
|
`;
|
|
586
586
|
for (const error of errors) {
|
|
587
|
-
|
|
587
|
+
errorMessage2 += `
|
|
588
588
|
- ${error}`;
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
|
-
super(
|
|
591
|
+
super(errorMessage2);
|
|
592
592
|
}
|
|
593
593
|
};
|
|
594
594
|
function assert(val, validator) {
|
|
@@ -1056,7 +1056,7 @@ var require_lib = __commonJS({
|
|
|
1056
1056
|
var VERSION, AGENT;
|
|
1057
1057
|
var init_version = __esm({
|
|
1058
1058
|
"src/version.ts"() {
|
|
1059
|
-
VERSION = "0.1.0
|
|
1059
|
+
VERSION = "0.1.0" ;
|
|
1060
1060
|
AGENT = `keynv-cli/${VERSION}`;
|
|
1061
1061
|
}
|
|
1062
1062
|
});
|
|
@@ -1146,10 +1146,10 @@ var init_parser = __esm({
|
|
|
1146
1146
|
"../../packages/core/dist/reference/parser.js"() {
|
|
1147
1147
|
PROJECT_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
1148
1148
|
ENV_RE = /^[a-z0-9][a-z0-9-]{0,23}$/;
|
|
1149
|
-
KEY_RE = /^[a-
|
|
1149
|
+
KEY_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
1150
1150
|
TRAILING_BOUNDARY = "(?!\\w|\\.[a-z0-9])";
|
|
1151
|
-
TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-
|
|
1152
|
-
ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-
|
|
1151
|
+
TEXT_FIND_SOURCE = `(?<![\\w.@/])@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
|
|
1152
|
+
ARGV_FIND_SOURCE = `@[a-z0-9][a-z0-9-]{0,47}\\.[a-z0-9][a-z0-9-]{0,23}\\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}${TRAILING_BOUNDARY}`;
|
|
1153
1153
|
TEXT_FIND_RE = new RegExp(TEXT_FIND_SOURCE, "g");
|
|
1154
1154
|
ARGV_FIND_RE = new RegExp(ARGV_FIND_SOURCE, "g");
|
|
1155
1155
|
}
|
|
@@ -1695,15 +1695,15 @@ var init_parseUtil = __esm({
|
|
|
1695
1695
|
message: issueData.message
|
|
1696
1696
|
};
|
|
1697
1697
|
}
|
|
1698
|
-
let
|
|
1698
|
+
let errorMessage2 = "";
|
|
1699
1699
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
1700
1700
|
for (const map of maps) {
|
|
1701
|
-
|
|
1701
|
+
errorMessage2 = map(fullIssue, { data, defaultError: errorMessage2 }).message;
|
|
1702
1702
|
}
|
|
1703
1703
|
return {
|
|
1704
1704
|
...issueData,
|
|
1705
1705
|
path: fullPath,
|
|
1706
|
-
message:
|
|
1706
|
+
message: errorMessage2
|
|
1707
1707
|
};
|
|
1708
1708
|
};
|
|
1709
1709
|
EMPTY_PATH = [];
|
|
@@ -5415,6 +5415,12 @@ var init_payload_schemas = __esm({
|
|
|
5415
5415
|
}).strict(),
|
|
5416
5416
|
"project.deleted": external_exports.object({ project_id: projectId, name: external_exports.string() }).strict(),
|
|
5417
5417
|
"project.dek_rotated": external_exports.object({ project_id: projectId }).strict(),
|
|
5418
|
+
"environment.created": external_exports.object({
|
|
5419
|
+
project_id: projectId,
|
|
5420
|
+
environment: external_exports.string().min(1),
|
|
5421
|
+
tier: external_exports.enum(["production", "non-production"]),
|
|
5422
|
+
require_approval: external_exports.boolean()
|
|
5423
|
+
}).strict(),
|
|
5418
5424
|
"member.added": external_exports.object({ project_id: projectId, target_user_id: userId, role }).strict(),
|
|
5419
5425
|
"member.removed": external_exports.object({ project_id: projectId, target_user_id: userId }).strict(),
|
|
5420
5426
|
"member.role_changed": external_exports.object({ project_id: projectId, target_user_id: userId, role }).strict(),
|
|
@@ -5530,7 +5536,15 @@ function clearCredentialsFile() {
|
|
|
5530
5536
|
if (existsSync(legacy)) rmSync(legacy, { force: true });
|
|
5531
5537
|
try {
|
|
5532
5538
|
entry().deletePassword();
|
|
5533
|
-
|
|
5539
|
+
return true;
|
|
5540
|
+
} catch (err) {
|
|
5541
|
+
process.stderr.write(
|
|
5542
|
+
`keynv: warning \u2014 could not remove OS keychain entry (${err instanceof Error ? err.message : String(err)}).
|
|
5543
|
+
You may need to remove the '${SERVICE}' / '${KEY_ACCOUNT}' entry manually
|
|
5544
|
+
via your OS credential manager (Windows Credential Manager, macOS Keychain, or libsecret).
|
|
5545
|
+
`
|
|
5546
|
+
);
|
|
5547
|
+
return false;
|
|
5534
5548
|
}
|
|
5535
5549
|
}
|
|
5536
5550
|
var SERVICE, KEY_ACCOUNT;
|
|
@@ -5573,8 +5587,9 @@ async function saveCredentials(creds) {
|
|
|
5573
5587
|
cache = creds;
|
|
5574
5588
|
}
|
|
5575
5589
|
function clearCredentials() {
|
|
5576
|
-
clearCredentialsFile();
|
|
5590
|
+
const ok = clearCredentialsFile();
|
|
5577
5591
|
cache = null;
|
|
5592
|
+
return ok;
|
|
5578
5593
|
}
|
|
5579
5594
|
var cache;
|
|
5580
5595
|
var init_store = __esm({
|
|
@@ -5584,6 +5599,9 @@ var init_store = __esm({
|
|
|
5584
5599
|
});
|
|
5585
5600
|
|
|
5586
5601
|
// src/client/http.ts
|
|
5602
|
+
function isClientError(err) {
|
|
5603
|
+
return err instanceof Error && typeof err.status === "number";
|
|
5604
|
+
}
|
|
5587
5605
|
function clientError(status, code, message, details) {
|
|
5588
5606
|
const err = Object.assign(new Error(message), { status, code, details });
|
|
5589
5607
|
return err;
|
|
@@ -5598,6 +5616,7 @@ function buildUrl(base, path, query) {
|
|
|
5598
5616
|
return url.toString();
|
|
5599
5617
|
}
|
|
5600
5618
|
async function tryRefresh(creds) {
|
|
5619
|
+
if (creds.auth_kind === "cli_token" || creds.refresh_token.length === 0) return null;
|
|
5601
5620
|
const res = await fetch(buildUrl(creds.server_url, "/v1/auth/refresh"), {
|
|
5602
5621
|
method: "POST",
|
|
5603
5622
|
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
@@ -5637,7 +5656,16 @@ var init_http = __esm({
|
|
|
5637
5656
|
return this.creds;
|
|
5638
5657
|
}
|
|
5639
5658
|
async ensureHydrated() {
|
|
5640
|
-
if (this.hydrated)
|
|
5659
|
+
if (this.hydrated) {
|
|
5660
|
+
try {
|
|
5661
|
+
await this.hydrated;
|
|
5662
|
+
} catch (err) {
|
|
5663
|
+
process.stderr.write(
|
|
5664
|
+
`${err instanceof Error ? err.message : `keynv: credential load failed \u2014 ${String(err)}`}
|
|
5665
|
+
`
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5641
5669
|
}
|
|
5642
5670
|
async setCredentials(creds) {
|
|
5643
5671
|
this.creds = creds;
|
|
@@ -5659,21 +5687,40 @@ var init_http = __esm({
|
|
|
5659
5687
|
headers.authorization = `Bearer ${this.creds.access_token}`;
|
|
5660
5688
|
}
|
|
5661
5689
|
const url = this.creds ? buildUrl(this.creds.server_url, path, opts.query) : path;
|
|
5662
|
-
let res
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5690
|
+
let res;
|
|
5691
|
+
try {
|
|
5692
|
+
res = await fetch(url, {
|
|
5693
|
+
method: opts.method ?? "GET",
|
|
5694
|
+
headers,
|
|
5695
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
5696
|
+
});
|
|
5697
|
+
} catch (err) {
|
|
5698
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
5699
|
+
const serverUrl = this.creds?.server_url ?? url;
|
|
5700
|
+
throw new Error(
|
|
5701
|
+
`keynv: cannot reach server '${serverUrl}' \u2014 ${cause}
|
|
5702
|
+
Check the server is running: curl ${serverUrl}/v1/health`
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5667
5705
|
if (res.status === 401 && this.creds && opts.authed !== false) {
|
|
5668
5706
|
const refreshed = await tryRefresh(this.creds);
|
|
5669
5707
|
if (refreshed) {
|
|
5670
5708
|
this.creds = refreshed;
|
|
5671
5709
|
headers.authorization = `Bearer ${refreshed.access_token}`;
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5710
|
+
try {
|
|
5711
|
+
res = await fetch(url, {
|
|
5712
|
+
method: opts.method ?? "GET",
|
|
5713
|
+
headers,
|
|
5714
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
5715
|
+
});
|
|
5716
|
+
} catch (err) {
|
|
5717
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
5718
|
+
const serverUrl = this.creds?.server_url ?? url;
|
|
5719
|
+
throw new Error(
|
|
5720
|
+
`keynv: cannot reach server '${serverUrl}' \u2014 ${cause}
|
|
5721
|
+
Check the server is running: curl ${serverUrl}/v1/health`
|
|
5722
|
+
);
|
|
5723
|
+
}
|
|
5677
5724
|
}
|
|
5678
5725
|
}
|
|
5679
5726
|
if (res.status === 204) return void 0;
|
|
@@ -5712,8 +5759,38 @@ function fmtError(err) {
|
|
|
5712
5759
|
const status = err.status ? ` (${err.status})` : "";
|
|
5713
5760
|
return `keynv:${code}${status} ${err.message}`;
|
|
5714
5761
|
}
|
|
5762
|
+
function handleExecError(stderr, err) {
|
|
5763
|
+
if (isClientError(err)) {
|
|
5764
|
+
const code = err.code ? `[${err.code}] ` : "";
|
|
5765
|
+
stderr.write(`keynv: ${code}${err.message}
|
|
5766
|
+
`);
|
|
5767
|
+
} else if (err instanceof Error) {
|
|
5768
|
+
stderr.write(`keynv: ${err.message}
|
|
5769
|
+
`);
|
|
5770
|
+
} else {
|
|
5771
|
+
stderr.write(`keynv: unexpected error
|
|
5772
|
+
`);
|
|
5773
|
+
}
|
|
5774
|
+
return 1;
|
|
5775
|
+
}
|
|
5715
5776
|
var init_format = __esm({
|
|
5716
5777
|
"src/ui/format.ts"() {
|
|
5778
|
+
init_http();
|
|
5779
|
+
}
|
|
5780
|
+
});
|
|
5781
|
+
function walkUp(startDir, predicate) {
|
|
5782
|
+
let dir = resolve(startDir);
|
|
5783
|
+
for (let i = 0; i < 64; i++) {
|
|
5784
|
+
const result = predicate(dir);
|
|
5785
|
+
if (result !== null && result !== void 0) return result;
|
|
5786
|
+
const parent = dirname(dir);
|
|
5787
|
+
if (parent === dir) return null;
|
|
5788
|
+
dir = parent;
|
|
5789
|
+
}
|
|
5790
|
+
return null;
|
|
5791
|
+
}
|
|
5792
|
+
var init_fs = __esm({
|
|
5793
|
+
"src/util/fs.ts"() {
|
|
5717
5794
|
}
|
|
5718
5795
|
});
|
|
5719
5796
|
function parseEnvFile(content, filename) {
|
|
@@ -5742,7 +5819,7 @@ function parseEnvFile(content, filename) {
|
|
|
5742
5819
|
`invalid key '${name}' (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
|
|
5743
5820
|
);
|
|
5744
5821
|
}
|
|
5745
|
-
|
|
5822
|
+
const valueRaw = body.slice(eq + 1);
|
|
5746
5823
|
let value;
|
|
5747
5824
|
const valueLeading = valueRaw.replace(/^\s+/, "");
|
|
5748
5825
|
const firstCh = valueLeading.charAt(0);
|
|
@@ -5774,20 +5851,14 @@ function parseEnvFile(content, filename) {
|
|
|
5774
5851
|
return entries;
|
|
5775
5852
|
}
|
|
5776
5853
|
function findEnvFile(startDir) {
|
|
5777
|
-
|
|
5778
|
-
for (let i = 0; i < 64; i++) {
|
|
5854
|
+
return walkUp(startDir, (dir) => {
|
|
5779
5855
|
const candidate = join(dir, ENV_FILE_BASENAME);
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
} catch {
|
|
5784
|
-
}
|
|
5856
|
+
try {
|
|
5857
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
5858
|
+
} catch {
|
|
5785
5859
|
}
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
dir = parent;
|
|
5789
|
-
}
|
|
5790
|
-
return null;
|
|
5860
|
+
return null;
|
|
5861
|
+
});
|
|
5791
5862
|
}
|
|
5792
5863
|
function loadEnvFile(opts) {
|
|
5793
5864
|
if (opts.disabled) return null;
|
|
@@ -5812,6 +5883,7 @@ var MAX_FILE_BYTES, KEY_RE2, ENV_FILE_BASENAME, EnvFileParseError, EnvFileNotFou
|
|
|
5812
5883
|
var init_envFile = __esm({
|
|
5813
5884
|
"src/exec/envFile.ts"() {
|
|
5814
5885
|
init_dist();
|
|
5886
|
+
init_fs();
|
|
5815
5887
|
MAX_FILE_BYTES = 1e6;
|
|
5816
5888
|
KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5817
5889
|
ENV_FILE_BASENAME = ".keynv.env";
|
|
@@ -5848,16 +5920,129 @@ var init_envFile = __esm({
|
|
|
5848
5920
|
}
|
|
5849
5921
|
});
|
|
5850
5922
|
|
|
5851
|
-
// src/
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5923
|
+
// src/init/heuristics.ts
|
|
5924
|
+
function shannonEntropyBits(s) {
|
|
5925
|
+
if (s.length === 0) return 0;
|
|
5926
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5927
|
+
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
5928
|
+
let h2 = 0;
|
|
5929
|
+
for (const c2 of counts.values()) {
|
|
5930
|
+
const p2 = c2 / s.length;
|
|
5931
|
+
h2 -= p2 * Math.log2(p2);
|
|
5932
|
+
}
|
|
5933
|
+
return h2;
|
|
5858
5934
|
}
|
|
5859
|
-
|
|
5860
|
-
|
|
5935
|
+
function nameMatchesLiteralPrefix(name) {
|
|
5936
|
+
const upper = name.toUpperCase();
|
|
5937
|
+
return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
|
|
5938
|
+
}
|
|
5939
|
+
function nameMatchesSecretSuffix(name) {
|
|
5940
|
+
const upper = name.toUpperCase();
|
|
5941
|
+
for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
|
|
5942
|
+
if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
|
|
5943
|
+
return { matched: true, hint };
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
return { matched: false, hint: "" };
|
|
5947
|
+
}
|
|
5948
|
+
function classifyEntry(name, value) {
|
|
5949
|
+
const upper = name.toUpperCase();
|
|
5950
|
+
if (NAME_FRAMEWORK_MANAGED.has(upper)) {
|
|
5951
|
+
return { verdict: "skip", hint: "framework/shell-managed" };
|
|
5952
|
+
}
|
|
5953
|
+
if (NAME_LITERAL_EXACT.has(upper)) {
|
|
5954
|
+
return { verdict: "literal", hint: "common config var" };
|
|
5955
|
+
}
|
|
5956
|
+
if (nameMatchesLiteralPrefix(name)) {
|
|
5957
|
+
return { verdict: "literal", hint: "public env, build-time bundled" };
|
|
5958
|
+
}
|
|
5959
|
+
if (value.length === 0) {
|
|
5960
|
+
return { verdict: "literal", hint: "empty" };
|
|
5961
|
+
}
|
|
5962
|
+
for (const { re, hint } of VALUE_PATTERNS) {
|
|
5963
|
+
if (re.test(value)) return { verdict: "secret", hint };
|
|
5964
|
+
}
|
|
5965
|
+
const suffix = nameMatchesSecretSuffix(name);
|
|
5966
|
+
if (suffix.matched) {
|
|
5967
|
+
return { verdict: "secret", hint: suffix.hint };
|
|
5968
|
+
}
|
|
5969
|
+
if (NAME_DB_URL.test(name)) {
|
|
5970
|
+
return { verdict: "secret", hint: "database URL" };
|
|
5971
|
+
}
|
|
5972
|
+
if (value.length >= 32) {
|
|
5973
|
+
const bits = shannonEntropyBits(value);
|
|
5974
|
+
if (bits >= 3.5) {
|
|
5975
|
+
return { verdict: "secret", hint: `${value.length}-char random-looking string` };
|
|
5976
|
+
}
|
|
5977
|
+
}
|
|
5978
|
+
return { verdict: "ambiguous", hint: "" };
|
|
5979
|
+
}
|
|
5980
|
+
function previewValue(value, max = 40) {
|
|
5981
|
+
if (value.length === 0) return "(empty)";
|
|
5982
|
+
if (value.length <= max) return value;
|
|
5983
|
+
return `${value.slice(0, max - 1)}\u2026`;
|
|
5984
|
+
}
|
|
5985
|
+
var NAME_FRAMEWORK_MANAGED, NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
|
|
5986
|
+
var init_heuristics = __esm({
|
|
5987
|
+
"src/init/heuristics.ts"() {
|
|
5988
|
+
NAME_FRAMEWORK_MANAGED = /* @__PURE__ */ new Set([
|
|
5989
|
+
"NODE_ENV",
|
|
5990
|
+
"PORT",
|
|
5991
|
+
"HOST",
|
|
5992
|
+
"HOSTNAME",
|
|
5993
|
+
"PATH",
|
|
5994
|
+
"HOME",
|
|
5995
|
+
"USER",
|
|
5996
|
+
"SHELL",
|
|
5997
|
+
"PWD",
|
|
5998
|
+
"CI"
|
|
5999
|
+
]);
|
|
6000
|
+
NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
|
|
6001
|
+
"DEBUG",
|
|
6002
|
+
"LOG_LEVEL",
|
|
6003
|
+
"LOGLEVEL",
|
|
6004
|
+
"TZ",
|
|
6005
|
+
"LANG",
|
|
6006
|
+
"LC_ALL",
|
|
6007
|
+
"NODE_OPTIONS",
|
|
6008
|
+
"NPM_CONFIG_LOGLEVEL",
|
|
6009
|
+
"TS_NODE_PROJECT"
|
|
6010
|
+
]);
|
|
6011
|
+
NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
|
|
6012
|
+
NAME_SECRET_SUFFIXES = [
|
|
6013
|
+
{ suffix: "PRIVATE_KEY", hint: "private key" },
|
|
6014
|
+
{ suffix: "API_KEY", hint: "API key" },
|
|
6015
|
+
{ suffix: "ACCESS_KEY", hint: "access key" },
|
|
6016
|
+
{ suffix: "SECRET_KEY", hint: "secret key" },
|
|
6017
|
+
{ suffix: "SECRET", hint: "secret" },
|
|
6018
|
+
{ suffix: "PASSWORD", hint: "password" },
|
|
6019
|
+
{ suffix: "PASSPHRASE", hint: "passphrase" },
|
|
6020
|
+
{ suffix: "TOKEN", hint: "token" },
|
|
6021
|
+
{ suffix: "KEY", hint: "key" },
|
|
6022
|
+
{ suffix: "CREDENTIALS", hint: "credentials" },
|
|
6023
|
+
{ suffix: "AUTH", hint: "auth" },
|
|
6024
|
+
{ suffix: "DSN", hint: "connection string" }
|
|
6025
|
+
];
|
|
6026
|
+
NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
|
|
6027
|
+
VALUE_PATTERNS = [
|
|
6028
|
+
{ re: /^sk-proj-/, hint: "OpenAI project key" },
|
|
6029
|
+
{ re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
|
|
6030
|
+
{ re: /^sk_live_/, hint: "Stripe live key" },
|
|
6031
|
+
{ re: /^sk_test_/, hint: "Stripe test key" },
|
|
6032
|
+
{ re: /^pk_live_/, hint: "Stripe publishable \u2014 often public, double-check" },
|
|
6033
|
+
{ re: /^xoxb-/, hint: "Slack bot token" },
|
|
6034
|
+
{ re: /^xoxp-/, hint: "Slack user token" },
|
|
6035
|
+
{ re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
|
|
6036
|
+
{ re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
|
|
6037
|
+
{ re: /^gho_/, hint: "GitHub OAuth token" },
|
|
6038
|
+
{ re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
|
|
6039
|
+
{ re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
|
|
6040
|
+
{ re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
|
|
6041
|
+
{ re: /^ya29\./, hint: "Google OAuth access token" },
|
|
6042
|
+
{ re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
|
|
6043
|
+
{ re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
|
|
6044
|
+
{ re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
|
|
6045
|
+
];
|
|
5861
6046
|
}
|
|
5862
6047
|
});
|
|
5863
6048
|
|
|
@@ -7015,23 +7200,109 @@ ${c2}
|
|
|
7015
7200
|
} }).prompt();
|
|
7016
7201
|
}
|
|
7017
7202
|
});
|
|
7203
|
+
function aiContextBody() {
|
|
7204
|
+
return `## keynv (secrets)
|
|
7205
|
+
|
|
7206
|
+
This project uses [keynv](https://keynv.dev) for secrets. There is a \`.keynv.env\` file at the project root containing **alias references** like \`OPENAI_API_KEY=@projectname.dev.openai-key\` \u2014 these are NOT real values, they are references to secrets stored in the keynv vault.
|
|
7207
|
+
|
|
7208
|
+
### Mental model
|
|
7209
|
+
|
|
7210
|
+
- **\`.keynv.env\`** \u2014 checked into git, contains aliases. Safe to read, edit, commit.
|
|
7211
|
+
- **Vault** \u2014 holds the real values. The \`keynv\` CLI reads it on demand.
|
|
7212
|
+
- **\`keynv exec -- <command>\`** \u2014 forks the command in a subprocess where the real values are injected into env vars. The parent process (where you, the AI agent, run) NEVER sees the real values.
|
|
7213
|
+
- **Redactor** \u2014 pipes subprocess stdout/stderr through a masker before they reach this terminal. Even if a script accidentally echoes a secret, you see \`<REDACTED:...>\` here.
|
|
7214
|
+
|
|
7215
|
+
### What you should do
|
|
7216
|
+
|
|
7217
|
+
When the user asks about secrets or env vars, follow this decision tree:
|
|
7218
|
+
|
|
7219
|
+
| User intent | Run / suggest |
|
|
7220
|
+
|---|---|
|
|
7221
|
+
| "Add a new API key" / "set up a secret" | \`keynv secret create\` (interactive prompt picks project + env + key) |
|
|
7222
|
+
| "I have a \`.env\`, migrate it" | \`keynv init\` (you're seeing this file because that already happened) |
|
|
7223
|
+
| "Run the app / dev server / tests" | \`keynv exec -- <their existing command>\` (auto-loads \`.keynv.env\`) |
|
|
7224
|
+
| "Show me the value of X" | \`keynv secret get @alias\` \u2014 but copy to clipboard, do NOT print the value into chat / terminal |
|
|
7225
|
+
| "Rotate this key" | \`keynv secret rotate @alias\` |
|
|
7226
|
+
| "Add a new env var to the project" | Edit \`.keynv.env\` and add \`NAME=@project.env.key\` (after creating the secret with \`keynv secret create\`) |
|
|
7227
|
+
| "Who has access?" | \`keynv member list <project>\` |
|
|
7228
|
+
|
|
7229
|
+
### Hard rules \u2014 do not violate
|
|
7230
|
+
|
|
7231
|
+
1. **Never print resolved secret values** to the chat, terminal, log, or any file. The user's editor / scrollback / chat history is an attack surface.
|
|
7232
|
+
2. **Never write a \`.env\` file** (or any file) containing real secret values. The whole point of keynv is that those don't exist on disk.
|
|
7233
|
+
3. **Never edit \`.keynv.env\` to substitute resolved values for aliases.** The file should only ever contain \`@project.env.key\` references and plain non-secret literals.
|
|
7234
|
+
4. **Use \`keynv exec --\` to run commands that need env vars.** Don't try to load \`.keynv.env\` yourself by parsing it manually; the wrapper handles resolution + redaction + subprocess isolation.
|
|
7235
|
+
5. **If a tool fails with "blocked by keynv-guard" or similar**, do NOT try to work around it. Tell the user \u2014 that's a sign of a misconfiguration, not an obstacle to bypass.
|
|
7236
|
+
6. **Treat the alias literal (\`@project.env.key\`) as the canonical reference**, never the resolved value. When you write code, configs, or commands that need a secret, write the alias.
|
|
7237
|
+
|
|
7238
|
+
### Quick reference
|
|
7239
|
+
|
|
7240
|
+
\`\`\`bash
|
|
7241
|
+
keynv # interactive TUI menu (pick projects, secrets, members)
|
|
7242
|
+
keynv exec -- <cmd> # run cmd with .keynv.env loaded
|
|
7243
|
+
keynv secret create # walk through creating a new secret
|
|
7244
|
+
keynv secret list <project> # list aliases (no values)
|
|
7245
|
+
keynv whoami # who am I logged in as
|
|
7246
|
+
keynv --help # full command list
|
|
7247
|
+
\`\`\`
|
|
7248
|
+
|
|
7249
|
+
If \`.keynv.env\` is missing or empty, the user probably hasn't run \`keynv init\` yet \u2014 suggest they do so before they paste a secret into a \`.env\`.`;
|
|
7250
|
+
}
|
|
7251
|
+
function renderKeynvBlock() {
|
|
7252
|
+
return `${KEYNV_BLOCK_START}
|
|
7253
|
+
${aiContextBody()}
|
|
7254
|
+
${KEYNV_BLOCK_END}`;
|
|
7255
|
+
}
|
|
7256
|
+
function writeAiContext(rootPath) {
|
|
7257
|
+
const path = join(rootPath, AGENTS_FILE_BASENAME);
|
|
7258
|
+
const block = renderKeynvBlock();
|
|
7259
|
+
if (!existsSync(path)) {
|
|
7260
|
+
const initial = `# Agent guidance for this project
|
|
7261
|
+
|
|
7262
|
+
${block}
|
|
7263
|
+
`;
|
|
7264
|
+
writeFileSync(path, initial);
|
|
7265
|
+
return "created";
|
|
7266
|
+
}
|
|
7267
|
+
const existing = readFileSync(path, "utf8");
|
|
7268
|
+
const startIdx = existing.indexOf(KEYNV_BLOCK_START);
|
|
7269
|
+
const endIdx = existing.indexOf(KEYNV_BLOCK_END);
|
|
7270
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
7271
|
+
const before = existing.slice(0, startIdx);
|
|
7272
|
+
const after = existing.slice(endIdx + KEYNV_BLOCK_END.length);
|
|
7273
|
+
const next = `${before}${block}${after}`;
|
|
7274
|
+
if (next === existing) return "unchanged";
|
|
7275
|
+
writeFileSync(path, next);
|
|
7276
|
+
return "updated";
|
|
7277
|
+
}
|
|
7278
|
+
const sep = existing.length === 0 ? "" : existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
7279
|
+
const trailingNewline = existing.endsWith("\n") ? "" : "\n";
|
|
7280
|
+
writeFileSync(path, `${existing}${trailingNewline}${sep}${block}
|
|
7281
|
+
`);
|
|
7282
|
+
return "appended";
|
|
7283
|
+
}
|
|
7284
|
+
var AGENTS_FILE_BASENAME, KEYNV_BLOCK_START, KEYNV_BLOCK_END;
|
|
7285
|
+
var init_aiContext = __esm({
|
|
7286
|
+
"src/init/aiContext.ts"() {
|
|
7287
|
+
AGENTS_FILE_BASENAME = "AGENTS.md";
|
|
7288
|
+
KEYNV_BLOCK_START = "<!-- keynv:start -- generated by `keynv init`; safe to leave intact, will be refreshed on re-run -->";
|
|
7289
|
+
KEYNV_BLOCK_END = "<!-- keynv:end -->";
|
|
7290
|
+
}
|
|
7291
|
+
});
|
|
7018
7292
|
function findProjectRoot(startDir) {
|
|
7019
|
-
|
|
7020
|
-
for (let i = 0; i < 64; i++) {
|
|
7293
|
+
const result = walkUp(startDir, (dir) => {
|
|
7021
7294
|
for (const marker of PROJECT_MARKERS) {
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
return buildRoot(dir, marker);
|
|
7295
|
+
if (existsSync(join(dir, marker))) {
|
|
7296
|
+
return { dir, marker };
|
|
7025
7297
|
}
|
|
7026
7298
|
}
|
|
7027
7299
|
if (existsSync(join(dir, GIT_MARKER))) {
|
|
7028
|
-
return
|
|
7300
|
+
return { dir, marker: GIT_MARKER };
|
|
7029
7301
|
}
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
return null;
|
|
7302
|
+
return null;
|
|
7303
|
+
});
|
|
7304
|
+
if (!result) return null;
|
|
7305
|
+
return buildRoot(result.dir, result.marker);
|
|
7035
7306
|
}
|
|
7036
7307
|
function buildRoot(dir, marker) {
|
|
7037
7308
|
let suggestedName = basename(dir);
|
|
@@ -7045,16 +7316,20 @@ function buildRoot(dir, marker) {
|
|
|
7045
7316
|
}
|
|
7046
7317
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
7047
7318
|
scripts = Object.fromEntries(
|
|
7048
|
-
Object.entries(pkg.scripts).filter(
|
|
7049
|
-
([, v2]) => typeof v2 === "string"
|
|
7050
|
-
)
|
|
7319
|
+
Object.entries(pkg.scripts).filter(([, v2]) => typeof v2 === "string")
|
|
7051
7320
|
);
|
|
7052
7321
|
}
|
|
7053
7322
|
} catch {
|
|
7054
7323
|
invalid = true;
|
|
7055
7324
|
}
|
|
7056
7325
|
}
|
|
7057
|
-
return {
|
|
7326
|
+
return {
|
|
7327
|
+
path: dir,
|
|
7328
|
+
suggestedName,
|
|
7329
|
+
marker,
|
|
7330
|
+
packageJsonScripts: scripts,
|
|
7331
|
+
packageJsonInvalid: invalid
|
|
7332
|
+
};
|
|
7058
7333
|
}
|
|
7059
7334
|
function findEnvFiles(rootDir) {
|
|
7060
7335
|
let entries;
|
|
@@ -7112,6 +7387,7 @@ function hasExistingKeynvEnv(rootDir) {
|
|
|
7112
7387
|
var PROJECT_MARKERS, GIT_MARKER, ENV_GLOB, ENV_EXAMPLE, KEYNV_ENV_BASENAME;
|
|
7113
7388
|
var init_detect = __esm({
|
|
7114
7389
|
"src/init/detect.ts"() {
|
|
7390
|
+
init_fs();
|
|
7115
7391
|
PROJECT_MARKERS = [
|
|
7116
7392
|
"package.json",
|
|
7117
7393
|
"pyproject.toml",
|
|
@@ -7124,132 +7400,11 @@ var init_detect = __esm({
|
|
|
7124
7400
|
];
|
|
7125
7401
|
GIT_MARKER = ".git";
|
|
7126
7402
|
ENV_GLOB = /^\.env(\.[A-Za-z0-9_-]+)?$/;
|
|
7127
|
-
ENV_EXAMPLE = /^\.env\.(example|sample|template)$/;
|
|
7403
|
+
ENV_EXAMPLE = /^\.env\.(example|sample|template|dist|defaults)$/;
|
|
7128
7404
|
KEYNV_ENV_BASENAME = ".keynv.env";
|
|
7129
7405
|
}
|
|
7130
7406
|
});
|
|
7131
7407
|
|
|
7132
|
-
// src/init/heuristics.ts
|
|
7133
|
-
function shannonEntropyBits(s) {
|
|
7134
|
-
if (s.length === 0) return 0;
|
|
7135
|
-
const counts = /* @__PURE__ */ new Map();
|
|
7136
|
-
for (const ch of s) counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
7137
|
-
let h2 = 0;
|
|
7138
|
-
for (const c2 of counts.values()) {
|
|
7139
|
-
const p2 = c2 / s.length;
|
|
7140
|
-
h2 -= p2 * Math.log2(p2);
|
|
7141
|
-
}
|
|
7142
|
-
return h2;
|
|
7143
|
-
}
|
|
7144
|
-
function nameMatchesLiteralPrefix(name) {
|
|
7145
|
-
const upper = name.toUpperCase();
|
|
7146
|
-
return NAME_LITERAL_PREFIX.some((p2) => upper.startsWith(p2));
|
|
7147
|
-
}
|
|
7148
|
-
function nameMatchesSecretSuffix(name) {
|
|
7149
|
-
const upper = name.toUpperCase();
|
|
7150
|
-
for (const { suffix, hint } of NAME_SECRET_SUFFIXES) {
|
|
7151
|
-
if (upper.endsWith(suffix) || upper.endsWith(`_${suffix}`)) {
|
|
7152
|
-
return { matched: true, hint };
|
|
7153
|
-
}
|
|
7154
|
-
}
|
|
7155
|
-
return { matched: false, hint: "" };
|
|
7156
|
-
}
|
|
7157
|
-
function classifyEntry(name, value) {
|
|
7158
|
-
const upper = name.toUpperCase();
|
|
7159
|
-
if (NAME_LITERAL_EXACT.has(upper)) {
|
|
7160
|
-
return { verdict: "literal", hint: "common config var" };
|
|
7161
|
-
}
|
|
7162
|
-
if (nameMatchesLiteralPrefix(name)) {
|
|
7163
|
-
return { verdict: "literal", hint: "public env (build-time bundled)" };
|
|
7164
|
-
}
|
|
7165
|
-
if (value.length === 0) {
|
|
7166
|
-
return { verdict: "literal", hint: "empty" };
|
|
7167
|
-
}
|
|
7168
|
-
for (const { re, hint } of VALUE_PATTERNS) {
|
|
7169
|
-
if (re.test(value)) return { verdict: "secret", hint };
|
|
7170
|
-
}
|
|
7171
|
-
const suffix = nameMatchesSecretSuffix(name);
|
|
7172
|
-
if (suffix.matched) {
|
|
7173
|
-
return { verdict: "secret", hint: suffix.hint };
|
|
7174
|
-
}
|
|
7175
|
-
if (NAME_DB_URL.test(name)) {
|
|
7176
|
-
return { verdict: "secret", hint: "database URL" };
|
|
7177
|
-
}
|
|
7178
|
-
if (value.length >= 32) {
|
|
7179
|
-
const bits = shannonEntropyBits(value);
|
|
7180
|
-
if (bits >= 3.5) {
|
|
7181
|
-
return { verdict: "secret", hint: `${value.length}-char random-looking string` };
|
|
7182
|
-
}
|
|
7183
|
-
}
|
|
7184
|
-
return { verdict: "ambiguous", hint: "" };
|
|
7185
|
-
}
|
|
7186
|
-
function previewValue(value, max = 40) {
|
|
7187
|
-
if (value.length === 0) return "(empty)";
|
|
7188
|
-
if (value.length <= max) return value;
|
|
7189
|
-
return `${value.slice(0, max - 1)}\u2026`;
|
|
7190
|
-
}
|
|
7191
|
-
var NAME_LITERAL_EXACT, NAME_LITERAL_PREFIX, NAME_SECRET_SUFFIXES, NAME_DB_URL, VALUE_PATTERNS;
|
|
7192
|
-
var init_heuristics = __esm({
|
|
7193
|
-
"src/init/heuristics.ts"() {
|
|
7194
|
-
NAME_LITERAL_EXACT = /* @__PURE__ */ new Set([
|
|
7195
|
-
"NODE_ENV",
|
|
7196
|
-
"PORT",
|
|
7197
|
-
"HOST",
|
|
7198
|
-
"HOSTNAME",
|
|
7199
|
-
"DEBUG",
|
|
7200
|
-
"LOG_LEVEL",
|
|
7201
|
-
"LOGLEVEL",
|
|
7202
|
-
"TZ",
|
|
7203
|
-
"LANG",
|
|
7204
|
-
"LC_ALL",
|
|
7205
|
-
"PATH",
|
|
7206
|
-
"HOME",
|
|
7207
|
-
"USER",
|
|
7208
|
-
"SHELL",
|
|
7209
|
-
"PWD",
|
|
7210
|
-
"CI",
|
|
7211
|
-
"NODE_OPTIONS",
|
|
7212
|
-
"NPM_CONFIG_LOGLEVEL",
|
|
7213
|
-
"TS_NODE_PROJECT"
|
|
7214
|
-
]);
|
|
7215
|
-
NAME_LITERAL_PREFIX = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_", "EXPO_PUBLIC_"];
|
|
7216
|
-
NAME_SECRET_SUFFIXES = [
|
|
7217
|
-
{ suffix: "PRIVATE_KEY", hint: "private key" },
|
|
7218
|
-
{ suffix: "API_KEY", hint: "API key" },
|
|
7219
|
-
{ suffix: "ACCESS_KEY", hint: "access key" },
|
|
7220
|
-
{ suffix: "SECRET_KEY", hint: "secret key" },
|
|
7221
|
-
{ suffix: "SECRET", hint: "secret" },
|
|
7222
|
-
{ suffix: "PASSWORD", hint: "password" },
|
|
7223
|
-
{ suffix: "PASSPHRASE", hint: "passphrase" },
|
|
7224
|
-
{ suffix: "TOKEN", hint: "token" },
|
|
7225
|
-
{ suffix: "KEY", hint: "key" },
|
|
7226
|
-
{ suffix: "CREDENTIALS", hint: "credentials" },
|
|
7227
|
-
{ suffix: "AUTH", hint: "auth" },
|
|
7228
|
-
{ suffix: "DSN", hint: "connection string" }
|
|
7229
|
-
];
|
|
7230
|
-
NAME_DB_URL = /^(DATABASE|DB|POSTGRES|MYSQL|MONGO|REDIS)_URL$/i;
|
|
7231
|
-
VALUE_PATTERNS = [
|
|
7232
|
-
{ re: /^sk-proj-/, hint: "OpenAI project key" },
|
|
7233
|
-
{ re: /^sk-[A-Za-z0-9]{20,}/, hint: "OpenAI key" },
|
|
7234
|
-
{ re: /^sk_live_/, hint: "Stripe live key" },
|
|
7235
|
-
{ re: /^sk_test_/, hint: "Stripe test key" },
|
|
7236
|
-
{ re: /^pk_live_/, hint: "Stripe publishable (often public, double-check)" },
|
|
7237
|
-
{ re: /^xoxb-/, hint: "Slack bot token" },
|
|
7238
|
-
{ re: /^xoxp-/, hint: "Slack user token" },
|
|
7239
|
-
{ re: /^ghp_[A-Za-z0-9]{30,}/, hint: "GitHub personal access token" },
|
|
7240
|
-
{ re: /^github_pat_/, hint: "GitHub fine-grained PAT" },
|
|
7241
|
-
{ re: /^gho_/, hint: "GitHub OAuth token" },
|
|
7242
|
-
{ re: /^AKIA[0-9A-Z]{16}$/, hint: "AWS access key id" },
|
|
7243
|
-
{ re: /^ASIA[0-9A-Z]{16}$/, hint: "AWS temporary key id" },
|
|
7244
|
-
{ re: /^AIza[0-9A-Za-z_-]{35}$/, hint: "Google API key" },
|
|
7245
|
-
{ re: /^ya29\./, hint: "Google OAuth access token" },
|
|
7246
|
-
{ re: /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, hint: "JWT" },
|
|
7247
|
-
{ re: /^-----BEGIN [A-Z ]+PRIVATE KEY-----/, hint: "PEM private key" },
|
|
7248
|
-
{ re: /^([a-z]+):\/\/[^@]+:[^@]+@/i, hint: "connection string with credentials" }
|
|
7249
|
-
];
|
|
7250
|
-
}
|
|
7251
|
-
});
|
|
7252
|
-
|
|
7253
7408
|
// src/init/scriptWrap.ts
|
|
7254
7409
|
function analyzeScript(name, command) {
|
|
7255
7410
|
const trimmed = command.trim();
|
|
@@ -7482,14 +7637,14 @@ async function runInitFlow(client, opts) {
|
|
|
7482
7637
|
R2.warn(`package.json at ${root.path} is not valid JSON \u2014 script wrapping will be skipped.`);
|
|
7483
7638
|
}
|
|
7484
7639
|
const envFiles = findEnvFiles(root.path);
|
|
7485
|
-
|
|
7640
|
+
const intoExisting = hasExistingKeynvEnv(root.path);
|
|
7641
|
+
if (envFiles.length === 0 && !intoExisting) {
|
|
7486
7642
|
R2.info(
|
|
7487
7643
|
`No .env files found in ${root.path}. There's nothing to migrate yet \u2014 create a .keynv.env by hand or run \`keynv exec\` once you have one.`
|
|
7488
7644
|
);
|
|
7489
7645
|
ye("Nothing to do.");
|
|
7490
7646
|
return { exitCode: 0 };
|
|
7491
7647
|
}
|
|
7492
|
-
const intoExisting = hasExistingKeynvEnv(root.path);
|
|
7493
7648
|
Se(
|
|
7494
7649
|
[
|
|
7495
7650
|
`Project root: ${root.path}`,
|
|
@@ -7499,43 +7654,79 @@ async function runInitFlow(client, opts) {
|
|
|
7499
7654
|
].filter(Boolean).join("\n"),
|
|
7500
7655
|
"Detected"
|
|
7501
7656
|
);
|
|
7502
|
-
const merged = mergeEnvFiles(envFiles);
|
|
7503
|
-
if (merged.length === 0) {
|
|
7504
|
-
R2.info("All env files were empty (only comments/blanks). Nothing to upload.");
|
|
7505
|
-
ye("Done.");
|
|
7506
|
-
return { exitCode: 0 };
|
|
7507
|
-
}
|
|
7508
7657
|
const projectChoice = await pickOrCreateProject(client, root.suggestedName);
|
|
7509
7658
|
if (projectChoice === null) {
|
|
7510
7659
|
me("No project selected.");
|
|
7511
7660
|
return { exitCode: 130 };
|
|
7512
7661
|
}
|
|
7513
|
-
const
|
|
7514
|
-
if (
|
|
7515
|
-
me("No
|
|
7662
|
+
const fileMapping = await pickFileEnvMapping(envFiles, projectChoice);
|
|
7663
|
+
if (fileMapping === null) {
|
|
7664
|
+
me("No env mapping selected.");
|
|
7516
7665
|
return { exitCode: 130 };
|
|
7517
7666
|
}
|
|
7518
|
-
const
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
}
|
|
7533
|
-
|
|
7534
|
-
|
|
7667
|
+
const distinctEnvs = [...new Set(fileMapping.map((m) => m.envName))];
|
|
7668
|
+
const perEnv = parseAndMergePerEnv(fileMapping);
|
|
7669
|
+
const skipped = [];
|
|
7670
|
+
for (const env2 of distinctEnvs) {
|
|
7671
|
+
const before = perEnv.get(env2) ?? [];
|
|
7672
|
+
const kept = [];
|
|
7673
|
+
for (const e2 of before) {
|
|
7674
|
+
if (classifyEntry(e2.name, e2.value).verdict === "skip") {
|
|
7675
|
+
skipped.push({ env: env2, entry: e2 });
|
|
7676
|
+
} else {
|
|
7677
|
+
kept.push(e2);
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
perEnv.set(env2, kept);
|
|
7681
|
+
}
|
|
7682
|
+
if (skipped.length > 0) {
|
|
7683
|
+
const names = [...new Set(skipped.map((s) => s.entry.name))];
|
|
7684
|
+
R2.info(
|
|
7685
|
+
`Skipped ${skipped.length} framework/shell-managed entr${skipped.length === 1 ? "y" : "ies"}: ${names.join(", ")}`
|
|
7686
|
+
);
|
|
7687
|
+
}
|
|
7688
|
+
for (const env2 of distinctEnvs) {
|
|
7689
|
+
const shadowed = (perEnv.get(env2) ?? []).filter((e2) => e2.shadowedBy.length > 0);
|
|
7690
|
+
if (shadowed.length === 0) continue;
|
|
7691
|
+
const detail = shadowed.map((e2) => ` [${env2}] ${e2.name}: ${e2.source} \u2192 ${e2.shadowedBy[e2.shadowedBy.length - 1]}`).join("\n");
|
|
7692
|
+
R2.warn(
|
|
7693
|
+
`Some keys appear in multiple env files mapped to the same env; using the last value (dotenv convention):
|
|
7694
|
+
${detail}`
|
|
7695
|
+
);
|
|
7696
|
+
}
|
|
7697
|
+
const totalEntries = [...perEnv.values()].reduce((n, arr) => n + arr.length, 0);
|
|
7698
|
+
if (totalEntries === 0) {
|
|
7699
|
+
R2.info(
|
|
7700
|
+
"All env files were empty or only contained framework-managed vars. Nothing to upload."
|
|
7701
|
+
);
|
|
7702
|
+
ye("Done.");
|
|
7703
|
+
return { exitCode: 0 };
|
|
7704
|
+
}
|
|
7705
|
+
const choices = [];
|
|
7706
|
+
for (const env2 of distinctEnvs) {
|
|
7707
|
+
for (const e2 of perEnv.get(env2) ?? []) {
|
|
7708
|
+
const c2 = classifyEntry(e2.name, e2.value);
|
|
7709
|
+
const hint = c2.hint || (e2.isAlias ? "looks like an alias literal" : "no signal");
|
|
7710
|
+
const preview2 = e2.isAlias ? e2.value : previewValue(e2.value, 28);
|
|
7711
|
+
const envTag = distinctEnvs.length > 1 ? `[${env2}] ` : "";
|
|
7712
|
+
choices.push({
|
|
7713
|
+
composite: `${env2}|${e2.name}`,
|
|
7714
|
+
env: env2,
|
|
7715
|
+
name: e2.name,
|
|
7716
|
+
value: e2.value,
|
|
7717
|
+
isAlias: e2.isAlias,
|
|
7718
|
+
verdict: c2.verdict,
|
|
7719
|
+
label: `${envTag}${e2.name} ${preview2}`,
|
|
7720
|
+
hint
|
|
7721
|
+
});
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
const initialSecretSelection = choices.filter((c2) => c2.verdict === "secret" && !c2.isAlias).map((c2) => c2.composite);
|
|
7725
|
+
const selectedComposites = unwrap(
|
|
7535
7726
|
await ve({
|
|
7536
7727
|
message: "Mark which keys are secrets (vault-uploaded). Unchecked keys stay as literals.",
|
|
7537
7728
|
options: choices.map((c2) => ({
|
|
7538
|
-
value: c2.
|
|
7729
|
+
value: c2.composite,
|
|
7539
7730
|
label: c2.label,
|
|
7540
7731
|
hint: c2.isAlias ? `${c2.hint} \u2014 already aliased; will pass through` : c2.hint
|
|
7541
7732
|
})),
|
|
@@ -7543,50 +7734,26 @@ async function runInitFlow(client, opts) {
|
|
|
7543
7734
|
required: false
|
|
7544
7735
|
})
|
|
7545
7736
|
);
|
|
7546
|
-
const selected = new Set(
|
|
7737
|
+
const selected = new Set(selectedComposites);
|
|
7738
|
+
const defaultEnv = pickDefaultEnv(distinctEnvs);
|
|
7547
7739
|
let scriptWrapSelection = [];
|
|
7548
|
-
|
|
7740
|
+
const scriptPlan = root.packageJsonScripts ? planScriptWrap(root.packageJsonScripts) : null;
|
|
7549
7741
|
if (!opts.noScripts && scriptPlan && scriptPlan.recommended.length > 0) {
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
})),
|
|
7559
|
-
...scriptPlan.unknown.map((a) => ({
|
|
7560
|
-
value: a.name,
|
|
7561
|
-
label: `${a.name} \u2192 ${a.wrapped}`,
|
|
7562
|
-
hint: `unknown tool \u2014 opt in if you know it reads env`
|
|
7563
|
-
}))
|
|
7564
|
-
],
|
|
7565
|
-
initialValues: scriptPlan.recommended.map((a) => a.name),
|
|
7566
|
-
required: false
|
|
7567
|
-
})
|
|
7568
|
-
);
|
|
7569
|
-
scriptWrapSelection = wrapAnswer;
|
|
7570
|
-
} else if (opts.noScripts) {
|
|
7571
|
-
scriptPlan = null;
|
|
7572
|
-
}
|
|
7573
|
-
const envFileFateRaw = unwrap(
|
|
7574
|
-
await Ee({
|
|
7575
|
-
message: "After upload, what about the original .env files?",
|
|
7576
|
-
options: [
|
|
7577
|
-
{ value: "delete", label: "Delete (recommended \u2014 eliminates the leak vector)" },
|
|
7578
|
-
{ value: "gitignore", label: "Keep but make sure .gitignore covers them" }
|
|
7579
|
-
],
|
|
7580
|
-
initialValue: "delete"
|
|
7581
|
-
})
|
|
7582
|
-
);
|
|
7742
|
+
scriptWrapSelection = scriptPlan.recommended.map((a) => a.name);
|
|
7743
|
+
}
|
|
7744
|
+
const perEnvCounts = distinctEnvs.map((env2) => {
|
|
7745
|
+
const entries = perEnv.get(env2) ?? [];
|
|
7746
|
+
const sec = entries.filter((e2) => selected.has(`${env2}|${e2.name}`)).length;
|
|
7747
|
+
const lit = entries.length - sec;
|
|
7748
|
+
return ` ${env2}: ${sec} secrets, ${lit} literals${env2 === defaultEnv ? " (default \u2014 written to .keynv.env)" : ""}`;
|
|
7749
|
+
}).join("\n");
|
|
7583
7750
|
const planSummary = [
|
|
7584
|
-
`Project:
|
|
7585
|
-
`
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
`Script wraps:
|
|
7589
|
-
|
|
7751
|
+
`Project: ${projectChoice.name}${projectChoice.created ? " (will be created)" : ""}`,
|
|
7752
|
+
`Environments: ${distinctEnvs.join(", ")}`,
|
|
7753
|
+
"Per-env breakdown:",
|
|
7754
|
+
perEnvCounts,
|
|
7755
|
+
`Script wraps: ${scriptWrapSelection.length}`,
|
|
7756
|
+
"Original .env: delete after upload",
|
|
7590
7757
|
opts.dryRun ? "Dry-run: no changes will be made." : ""
|
|
7591
7758
|
].filter(Boolean).join("\n");
|
|
7592
7759
|
Se(planSummary, "About to apply");
|
|
@@ -7599,82 +7766,114 @@ async function runInitFlow(client, opts) {
|
|
|
7599
7766
|
ye("Dry-run complete \u2014 no changes were made.");
|
|
7600
7767
|
return { exitCode: 0 };
|
|
7601
7768
|
}
|
|
7602
|
-
|
|
7603
|
-
if (
|
|
7604
|
-
|
|
7605
|
-
s.start(`Creating project "${projectChoice.name}"`);
|
|
7606
|
-
try {
|
|
7607
|
-
const created = await client.request("/v1/projects", {
|
|
7608
|
-
method: "POST",
|
|
7609
|
-
body: {
|
|
7610
|
-
name: projectChoice.name,
|
|
7611
|
-
environments: [{ name: envName, tier: "non-production", require_approval: false }]
|
|
7612
|
-
}
|
|
7613
|
-
});
|
|
7614
|
-
projectId2 = created.id;
|
|
7615
|
-
s.stop(`Created project ${created.name}`);
|
|
7616
|
-
} catch (err) {
|
|
7617
|
-
s.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
|
|
7618
|
-
return { exitCode: 1 };
|
|
7619
|
-
}
|
|
7620
|
-
} else {
|
|
7621
|
-
projectId2 = projectChoice.id;
|
|
7622
|
-
}
|
|
7623
|
-
const uploaded = [];
|
|
7769
|
+
const projectId2 = await ensureProjectAndEnvs(client, projectChoice, distinctEnvs);
|
|
7770
|
+
if (projectId2 === null) return { exitCode: 1 };
|
|
7771
|
+
const uploadedByEnv = /* @__PURE__ */ new Map();
|
|
7624
7772
|
const failed = [];
|
|
7625
|
-
|
|
7773
|
+
const totalToUpload = selected.size;
|
|
7774
|
+
if (totalToUpload > 0) {
|
|
7626
7775
|
const s = ft();
|
|
7627
|
-
s.start(`Uploading ${
|
|
7776
|
+
s.start(`Uploading ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
|
|
7628
7777
|
let i = 0;
|
|
7629
|
-
for (const
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7778
|
+
for (const env2 of distinctEnvs) {
|
|
7779
|
+
const envUploaded = /* @__PURE__ */ new Map();
|
|
7780
|
+
uploadedByEnv.set(env2, envUploaded);
|
|
7781
|
+
for (const e2 of perEnv.get(env2) ?? []) {
|
|
7782
|
+
if (!selected.has(`${env2}|${e2.name}`)) continue;
|
|
7783
|
+
i++;
|
|
7784
|
+
s.message(`Uploading (${i}/${totalToUpload}) [${env2}] ${e2.name}`);
|
|
7785
|
+
const aliasKey = e2.name.toLowerCase().replace(/_/g, "-");
|
|
7786
|
+
try {
|
|
7787
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
7788
|
+
method: "POST",
|
|
7789
|
+
body: { env: env2, key: aliasKey, value: e2.value }
|
|
7790
|
+
});
|
|
7791
|
+
const alias2 = buildAlias({
|
|
7792
|
+
project: projectChoice.name,
|
|
7793
|
+
environment: env2,
|
|
7794
|
+
key: aliasKey
|
|
7795
|
+
});
|
|
7796
|
+
if (alias2 === null) {
|
|
7797
|
+
failed.push({
|
|
7798
|
+
env: env2,
|
|
7799
|
+
name: e2.name,
|
|
7800
|
+
reason: `produced an invalid alias for project=${projectChoice.name} env=${env2} key=${aliasKey}`
|
|
7801
|
+
});
|
|
7802
|
+
} else {
|
|
7803
|
+
envUploaded.set(e2.name, alias2.literal);
|
|
7804
|
+
}
|
|
7805
|
+
} catch (err) {
|
|
7641
7806
|
failed.push({
|
|
7642
|
-
|
|
7643
|
-
|
|
7807
|
+
env: env2,
|
|
7808
|
+
name: e2.name,
|
|
7809
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
7644
7810
|
});
|
|
7645
|
-
} else {
|
|
7646
|
-
uploaded.push({ name: entry2.name, aliasLiteral: alias2.literal });
|
|
7647
7811
|
}
|
|
7648
|
-
} catch (err) {
|
|
7649
|
-
failed.push({ name: entry2.name, reason: err instanceof Error ? err.message : String(err) });
|
|
7650
7812
|
}
|
|
7651
7813
|
}
|
|
7652
7814
|
if (failed.length === 0) {
|
|
7653
|
-
s.stop(`Uploaded ${
|
|
7815
|
+
s.stop(`Uploaded ${totalToUpload} secret${totalToUpload === 1 ? "" : "s"}`);
|
|
7654
7816
|
} else {
|
|
7655
|
-
s.error(
|
|
7656
|
-
|
|
7817
|
+
s.error(
|
|
7818
|
+
`${totalToUpload - failed.length}/${totalToUpload} uploaded; ${failed.length} failed`
|
|
7819
|
+
);
|
|
7820
|
+
for (const f of failed) R2.warn(` [${f.env}] ${f.name}: ${f.reason}`);
|
|
7657
7821
|
}
|
|
7658
7822
|
}
|
|
7659
|
-
const
|
|
7660
|
-
const
|
|
7823
|
+
const defaultUploaded = uploadedByEnv.get(defaultEnv) ?? /* @__PURE__ */ new Map();
|
|
7824
|
+
const defaultLiterals = (perEnv.get(defaultEnv) ?? []).filter(
|
|
7825
|
+
(e2) => !selected.has(`${defaultEnv}|${e2.name}`)
|
|
7826
|
+
);
|
|
7661
7827
|
const keynvEnvPath = join(root.path, ".keynv.env");
|
|
7662
|
-
let writtenLines;
|
|
7663
7828
|
try {
|
|
7664
|
-
|
|
7665
|
-
uploadedAliases:
|
|
7666
|
-
literals,
|
|
7829
|
+
const lines = composeKeynvEnv({
|
|
7830
|
+
uploadedAliases: defaultUploaded,
|
|
7831
|
+
literals: defaultLiterals,
|
|
7667
7832
|
mergeWithExisting: intoExisting ? readFileSync(keynvEnvPath, "utf8") : null
|
|
7668
7833
|
});
|
|
7669
|
-
writeFileSync(keynvEnvPath, `${
|
|
7834
|
+
writeFileSync(keynvEnvPath, `${lines.join("\n")}
|
|
7670
7835
|
`);
|
|
7671
7836
|
R2.success(
|
|
7672
|
-
`${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${
|
|
7837
|
+
`${intoExisting ? "Updated" : "Wrote"} ${keynvEnvPath} (${defaultUploaded.size + defaultLiterals.length} entries from "${defaultEnv}")`
|
|
7673
7838
|
);
|
|
7674
7839
|
} catch (err) {
|
|
7675
7840
|
R2.error(`Failed to write .keynv.env: ${err instanceof Error ? err.message : String(err)}`);
|
|
7676
7841
|
return { exitCode: 1 };
|
|
7677
7842
|
}
|
|
7843
|
+
const otherEnvsWithAliases = distinctEnvs.filter(
|
|
7844
|
+
(env2) => env2 !== defaultEnv && (uploadedByEnv.get(env2)?.size ?? 0) > 0
|
|
7845
|
+
);
|
|
7846
|
+
for (const env2 of otherEnvsWithAliases) {
|
|
7847
|
+
const envUploaded = uploadedByEnv.get(env2) ?? /* @__PURE__ */ new Map();
|
|
7848
|
+
const envLiterals = (perEnv.get(env2) ?? []).filter(
|
|
7849
|
+
(e2) => !selected.has(`${env2}|${e2.name}`)
|
|
7850
|
+
);
|
|
7851
|
+
const envFilePath = join(root.path, `.keynv.${env2}.env`);
|
|
7852
|
+
try {
|
|
7853
|
+
const lines = composeKeynvEnv({
|
|
7854
|
+
uploadedAliases: envUploaded,
|
|
7855
|
+
literals: envLiterals,
|
|
7856
|
+
mergeWithExisting: null
|
|
7857
|
+
});
|
|
7858
|
+
writeFileSync(envFilePath, `${lines.join("\n")}
|
|
7859
|
+
`);
|
|
7860
|
+
R2.success(
|
|
7861
|
+
`Wrote .keynv.${env2}.env (${envUploaded.size} secret alias${envUploaded.size === 1 ? "" : "es"} from "${env2}")`
|
|
7862
|
+
);
|
|
7863
|
+
} catch (err) {
|
|
7864
|
+
R2.warn(
|
|
7865
|
+
`Could not write .keynv.${env2}.env: ${err instanceof Error ? err.message : String(err)}`
|
|
7866
|
+
);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
try {
|
|
7870
|
+
const outcome = writeAiContext(root.path);
|
|
7871
|
+
if (outcome === "created") R2.success("Wrote AGENTS.md (so AI agents understand keynv)");
|
|
7872
|
+
else if (outcome === "updated") R2.success("Refreshed keynv section in AGENTS.md");
|
|
7873
|
+
else if (outcome === "appended") R2.success("Appended keynv section to AGENTS.md");
|
|
7874
|
+
} catch (err) {
|
|
7875
|
+
R2.warn(`Could not write AGENTS.md: ${err instanceof Error ? err.message : String(err)}`);
|
|
7876
|
+
}
|
|
7678
7877
|
if (scriptWrapSelection.length > 0 && root.packageJsonScripts) {
|
|
7679
7878
|
try {
|
|
7680
7879
|
updatePackageJsonScripts(
|
|
@@ -7682,66 +7881,37 @@ async function runInitFlow(client, opts) {
|
|
|
7682
7881
|
root.packageJsonScripts,
|
|
7683
7882
|
scriptWrapSelection
|
|
7684
7883
|
);
|
|
7685
|
-
R2.success(
|
|
7884
|
+
R2.success(
|
|
7885
|
+
`Wrapped ${scriptWrapSelection.length} script${scriptWrapSelection.length === 1 ? "" : "s"} in package.json`
|
|
7886
|
+
);
|
|
7686
7887
|
} catch (err) {
|
|
7687
7888
|
R2.warn(
|
|
7688
7889
|
`Could not update package.json scripts: ${err instanceof Error ? err.message : String(err)}`
|
|
7689
7890
|
);
|
|
7690
7891
|
}
|
|
7691
7892
|
}
|
|
7692
|
-
|
|
7693
|
-
for (const f of envFiles) {
|
|
7694
|
-
try {
|
|
7695
|
-
unlinkSync(f.path);
|
|
7696
|
-
R2.success(`Removed ${f.name}`);
|
|
7697
|
-
} catch (err) {
|
|
7698
|
-
R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7699
|
-
}
|
|
7700
|
-
}
|
|
7701
|
-
} else {
|
|
7702
|
-
const gitignorePath = join(root.path, ".gitignore");
|
|
7893
|
+
for (const f of envFiles) {
|
|
7703
7894
|
try {
|
|
7704
|
-
|
|
7705
|
-
R2.success(`
|
|
7895
|
+
unlinkSync(f.path);
|
|
7896
|
+
R2.success(`Removed ${f.name}`);
|
|
7706
7897
|
} catch (err) {
|
|
7707
|
-
R2.warn(`Could not
|
|
7898
|
+
R2.warn(`Could not remove ${f.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
7708
7899
|
}
|
|
7709
7900
|
}
|
|
7901
|
+
const otherEnvs = distinctEnvs.filter((e2) => e2 !== defaultEnv);
|
|
7902
|
+
if (otherEnvs.length > 0) {
|
|
7903
|
+
const lines = otherEnvs.map((env2) => {
|
|
7904
|
+
const count = uploadedByEnv.get(env2)?.size ?? 0;
|
|
7905
|
+
const hasFile = count > 0;
|
|
7906
|
+
return hasFile ? ` ${env2}: ${count} secret${count === 1 ? "" : "s"} \u2192 .keynv.${env2}.env (use \`keynv exec --from .keynv.${env2}.env -- <cmd>\`)` : ` ${env2}: 0 secrets in vault (no alias file written)`;
|
|
7907
|
+
});
|
|
7908
|
+
Se(lines.join("\n"), "Secrets in other envs");
|
|
7909
|
+
}
|
|
7710
7910
|
ye(
|
|
7711
|
-
failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "
|
|
7911
|
+
failed.length > 0 ? `Done with ${failed.length} failure(s) \u2014 see warnings above.` : `Done. Try: ${scriptWrapSelection.includes("dev") ? "pnpm dev" : "keynv exec -- <your command>"}`
|
|
7712
7912
|
);
|
|
7713
7913
|
return { exitCode: failed.length > 0 ? 1 : 0 };
|
|
7714
7914
|
}
|
|
7715
|
-
function mergeEnvFiles(files) {
|
|
7716
|
-
const map = /* @__PURE__ */ new Map();
|
|
7717
|
-
for (const f of files) {
|
|
7718
|
-
let entries;
|
|
7719
|
-
try {
|
|
7720
|
-
entries = parseEnvFile(readFileSync(f.path, "utf8"), f.path);
|
|
7721
|
-
} catch (err) {
|
|
7722
|
-
R2.warn(`${f.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
|
|
7723
|
-
continue;
|
|
7724
|
-
}
|
|
7725
|
-
for (const e2 of entries) {
|
|
7726
|
-
const existing = map.get(e2.name);
|
|
7727
|
-
if (existing) {
|
|
7728
|
-
existing.shadowedBy.push(f.name);
|
|
7729
|
-
existing.value = e2.value;
|
|
7730
|
-
existing.isAlias = e2.isAlias;
|
|
7731
|
-
} else {
|
|
7732
|
-
map.set(e2.name, {
|
|
7733
|
-
name: e2.name,
|
|
7734
|
-
value: e2.value,
|
|
7735
|
-
isAlias: e2.isAlias,
|
|
7736
|
-
source: f.name,
|
|
7737
|
-
sourceLine: e2.line,
|
|
7738
|
-
shadowedBy: []
|
|
7739
|
-
});
|
|
7740
|
-
}
|
|
7741
|
-
}
|
|
7742
|
-
}
|
|
7743
|
-
return [...map.values()];
|
|
7744
|
-
}
|
|
7745
7915
|
async function pickOrCreateProject(client, suggestedName) {
|
|
7746
7916
|
const projects = await listProjects(client);
|
|
7747
7917
|
const value = unwrap(
|
|
@@ -7761,53 +7931,156 @@ async function pickOrCreateProject(client, suggestedName) {
|
|
|
7761
7931
|
validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,47}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 48 chars"
|
|
7762
7932
|
})
|
|
7763
7933
|
);
|
|
7764
|
-
return { id: "", name, created: true };
|
|
7934
|
+
return { id: "", name, created: true, existingEnvs: [] };
|
|
7765
7935
|
}
|
|
7766
7936
|
const match = projects.find((p2) => p2.id === value);
|
|
7767
7937
|
if (!match) return null;
|
|
7768
|
-
|
|
7938
|
+
const detail = await client.request(`/v1/projects/${match.id}`);
|
|
7939
|
+
return {
|
|
7940
|
+
id: match.id,
|
|
7941
|
+
name: match.name,
|
|
7942
|
+
created: false,
|
|
7943
|
+
existingEnvs: detail.environments
|
|
7944
|
+
};
|
|
7769
7945
|
}
|
|
7770
|
-
async function
|
|
7771
|
-
|
|
7772
|
-
const
|
|
7773
|
-
if (
|
|
7774
|
-
const
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7946
|
+
async function pickFileEnvMapping(files, project) {
|
|
7947
|
+
if (files.length === 0) return [];
|
|
7948
|
+
const existingEnvNames = new Set(project.existingEnvs.map((e2) => e2.name));
|
|
7949
|
+
if (files.length === 1) {
|
|
7950
|
+
const onlyFile = files[0];
|
|
7951
|
+
if (!onlyFile) return [];
|
|
7952
|
+
const suggested = suggestedEnvForSuffix(onlyFile.suffix);
|
|
7953
|
+
return [{ file: onlyFile, envName: suggested }];
|
|
7954
|
+
}
|
|
7955
|
+
const assignments = [];
|
|
7956
|
+
for (const f of files) {
|
|
7957
|
+
const suggested = suggestedEnvForSuffix(f.suffix);
|
|
7958
|
+
const opts = [];
|
|
7959
|
+
opts.push({
|
|
7960
|
+
value: suggested,
|
|
7961
|
+
label: suggested,
|
|
7962
|
+
hint: existingEnvNames.has(suggested) ? "existing env" : project.created ? "will be created with project" : "will be added to project"
|
|
7963
|
+
});
|
|
7964
|
+
for (const e2 of project.existingEnvs) {
|
|
7965
|
+
if (e2.name === suggested) continue;
|
|
7966
|
+
opts.push({ value: e2.name, label: e2.name, hint: e2.tier });
|
|
7967
|
+
}
|
|
7968
|
+
opts.push({ value: "__custom", label: "+ Custom env name\u2026" });
|
|
7969
|
+
let envName = unwrap(
|
|
7970
|
+
await Ee({
|
|
7971
|
+
message: `Map ${f.name} to which keynv env?`,
|
|
7972
|
+
options: opts,
|
|
7973
|
+
initialValue: suggested
|
|
7779
7974
|
})
|
|
7780
7975
|
);
|
|
7781
|
-
|
|
7976
|
+
if (envName === "__custom") {
|
|
7977
|
+
envName = unwrap(
|
|
7978
|
+
await Re({
|
|
7979
|
+
message: "New env name",
|
|
7980
|
+
validate: (v2) => v2 && /^[a-z0-9][a-z0-9-]{0,23}$/.test(v2) ? void 0 : "lowercase letters, digits, dashes; up to 24 chars"
|
|
7981
|
+
})
|
|
7982
|
+
);
|
|
7983
|
+
}
|
|
7984
|
+
assignments.push({ file: f, envName });
|
|
7782
7985
|
}
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7986
|
+
return assignments;
|
|
7987
|
+
}
|
|
7988
|
+
function parseAndMergePerEnv(mapping) {
|
|
7989
|
+
const acc = /* @__PURE__ */ new Map();
|
|
7990
|
+
for (const { file, envName } of mapping) {
|
|
7991
|
+
let entries;
|
|
7992
|
+
try {
|
|
7993
|
+
entries = parseEnvFile(readFileSync(file.path, "utf8"), file.path);
|
|
7994
|
+
} catch (err) {
|
|
7995
|
+
R2.warn(`${file.name}: ${err instanceof Error ? err.message : String(err)} \u2014 skipping file`);
|
|
7996
|
+
continue;
|
|
7997
|
+
}
|
|
7998
|
+
let envMap = acc.get(envName);
|
|
7999
|
+
if (!envMap) {
|
|
8000
|
+
envMap = /* @__PURE__ */ new Map();
|
|
8001
|
+
acc.set(envName, envMap);
|
|
8002
|
+
}
|
|
8003
|
+
for (const e2 of entries) {
|
|
8004
|
+
const existing = envMap.get(e2.name);
|
|
8005
|
+
if (existing) {
|
|
8006
|
+
existing.shadowedBy.push(file.name);
|
|
8007
|
+
existing.value = e2.value;
|
|
8008
|
+
existing.isAlias = e2.isAlias;
|
|
8009
|
+
} else {
|
|
8010
|
+
envMap.set(e2.name, {
|
|
8011
|
+
name: e2.name,
|
|
8012
|
+
value: e2.value,
|
|
8013
|
+
isAlias: e2.isAlias,
|
|
8014
|
+
source: file.name,
|
|
8015
|
+
sourceLine: e2.line,
|
|
8016
|
+
shadowedBy: []
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
7789
8020
|
}
|
|
7790
|
-
|
|
7791
|
-
|
|
8021
|
+
const out = /* @__PURE__ */ new Map();
|
|
8022
|
+
for (const [env2, m] of acc) out.set(env2, [...m.values()]);
|
|
8023
|
+
return out;
|
|
8024
|
+
}
|
|
8025
|
+
function pickDefaultEnv(envs) {
|
|
8026
|
+
if (envs.length === 0) return "dev";
|
|
8027
|
+
if (envs.includes("dev")) return "dev";
|
|
8028
|
+
return envs[0];
|
|
8029
|
+
}
|
|
8030
|
+
async function ensureProjectAndEnvs(client, projectChoice, distinctEnvs) {
|
|
8031
|
+
if (projectChoice.created) {
|
|
8032
|
+
const s2 = ft();
|
|
8033
|
+
s2.start(`Creating project "${projectChoice.name}" with ${distinctEnvs.length} env(s)`);
|
|
8034
|
+
try {
|
|
8035
|
+
const created = await client.request("/v1/projects", {
|
|
8036
|
+
method: "POST",
|
|
8037
|
+
body: {
|
|
8038
|
+
name: projectChoice.name,
|
|
8039
|
+
environments: distinctEnvs.map((name) => envBodyFor(name))
|
|
8040
|
+
}
|
|
8041
|
+
});
|
|
8042
|
+
s2.stop(`Created project ${created.name}`);
|
|
8043
|
+
return created.id;
|
|
8044
|
+
} catch (err) {
|
|
8045
|
+
s2.error(`Failed to create project: ${err instanceof Error ? err.message : String(err)}`);
|
|
8046
|
+
return null;
|
|
8047
|
+
}
|
|
7792
8048
|
}
|
|
7793
|
-
const
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
}
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
8049
|
+
const existing = new Set(projectChoice.existingEnvs.map((e2) => e2.name));
|
|
8050
|
+
const missing = distinctEnvs.filter((e2) => !existing.has(e2));
|
|
8051
|
+
if (missing.length === 0) return projectChoice.id;
|
|
8052
|
+
const s = ft();
|
|
8053
|
+
s.start(`Adding ${missing.length} missing env(s) to project "${projectChoice.name}"`);
|
|
8054
|
+
for (const envName of missing) {
|
|
8055
|
+
try {
|
|
8056
|
+
await client.request(`/v1/projects/${projectChoice.id}/environments`, {
|
|
8057
|
+
method: "POST",
|
|
8058
|
+
body: envBodyFor(envName)
|
|
8059
|
+
});
|
|
8060
|
+
} catch (err) {
|
|
8061
|
+
s.error(
|
|
8062
|
+
`Failed to add env "${envName}": ${err instanceof Error ? err.message : String(err)}`
|
|
8063
|
+
);
|
|
8064
|
+
return null;
|
|
8065
|
+
}
|
|
8066
|
+
}
|
|
8067
|
+
s.stop(`Added env(s): ${missing.join(", ")}`);
|
|
8068
|
+
return projectChoice.id;
|
|
8069
|
+
}
|
|
8070
|
+
function envBodyFor(name) {
|
|
8071
|
+
const isProd = name === "prod" || name === "production";
|
|
8072
|
+
return {
|
|
8073
|
+
name,
|
|
8074
|
+
tier: isProd ? "production" : "non-production",
|
|
8075
|
+
require_approval: isProd
|
|
8076
|
+
};
|
|
7805
8077
|
}
|
|
7806
8078
|
function composeKeynvEnv(opts) {
|
|
7807
8079
|
const { uploadedAliases, literals, mergeWithExisting } = opts;
|
|
7808
8080
|
const lines = [];
|
|
7809
8081
|
if (mergeWithExisting !== null) {
|
|
7810
|
-
const
|
|
8082
|
+
const normalized = mergeWithExisting.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
8083
|
+
const trimmed = normalized.replace(/\n+$/, "");
|
|
7811
8084
|
if (trimmed.length > 0) {
|
|
7812
8085
|
lines.push(...trimmed.split("\n"));
|
|
7813
8086
|
lines.push("");
|
|
@@ -7834,7 +8107,7 @@ function composeKeynvEnv(opts) {
|
|
|
7834
8107
|
}
|
|
7835
8108
|
}
|
|
7836
8109
|
if (mergeWithExisting !== null) {
|
|
7837
|
-
lines.push(
|
|
8110
|
+
lines.push("# <<< keynv init <<<");
|
|
7838
8111
|
}
|
|
7839
8112
|
return lines;
|
|
7840
8113
|
}
|
|
@@ -7853,21 +8126,12 @@ function updatePackageJsonScripts(path, originalScripts, selectedNames) {
|
|
|
7853
8126
|
writeFileSync(path, trailingNewline ? `${out}
|
|
7854
8127
|
` : out);
|
|
7855
8128
|
}
|
|
7856
|
-
function ensureGitignoreEntries(path, basenames) {
|
|
7857
|
-
let existing = "";
|
|
7858
|
-
if (existsSync(path)) existing = readFileSync(path, "utf8");
|
|
7859
|
-
const existingLines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
7860
|
-
const toAdd = basenames.filter((n) => !existingLines.has(n));
|
|
7861
|
-
if (toAdd.length === 0) return;
|
|
7862
|
-
const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
7863
|
-
const block = ["", "# added by keynv init", ...toAdd, ""].join("\n");
|
|
7864
|
-
writeFileSync(path, `${existing}${sep}${block}`);
|
|
7865
|
-
}
|
|
7866
8129
|
var init_init = __esm({
|
|
7867
8130
|
"src/ui/flows/init.ts"() {
|
|
7868
|
-
init_dist();
|
|
7869
8131
|
init_dist5();
|
|
8132
|
+
init_dist();
|
|
7870
8133
|
init_envFile();
|
|
8134
|
+
init_aiContext();
|
|
7871
8135
|
init_detect();
|
|
7872
8136
|
init_heuristics();
|
|
7873
8137
|
init_scriptWrap();
|
|
@@ -7876,30 +8140,122 @@ var init_init = __esm({
|
|
|
7876
8140
|
}
|
|
7877
8141
|
});
|
|
7878
8142
|
|
|
7879
|
-
// src/ui/helpers/
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
8143
|
+
// src/ui/helpers/tty.ts
|
|
8144
|
+
var tty_exports = {};
|
|
8145
|
+
__export(tty_exports, {
|
|
8146
|
+
isInteractive: () => isInteractive
|
|
8147
|
+
});
|
|
8148
|
+
function isInteractive() {
|
|
8149
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
7885
8150
|
}
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
8151
|
+
var init_tty = __esm({
|
|
8152
|
+
"src/ui/helpers/tty.ts"() {
|
|
8153
|
+
}
|
|
8154
|
+
});
|
|
8155
|
+
function sleep(ms) {
|
|
8156
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
8157
|
+
}
|
|
8158
|
+
async function errorMessage(res, fallback) {
|
|
8159
|
+
const text = await res.text();
|
|
8160
|
+
if (!text) return fallback;
|
|
8161
|
+
try {
|
|
8162
|
+
const parsed = JSON.parse(text);
|
|
8163
|
+
return parsed.error?.message ?? fallback;
|
|
8164
|
+
} catch {
|
|
8165
|
+
return fallback;
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
function openBrowser(url) {
|
|
8169
|
+
try {
|
|
8170
|
+
const os = platform();
|
|
8171
|
+
const command = os === "win32" ? "cmd" : os === "darwin" ? "open" : "xdg-open";
|
|
8172
|
+
const args = os === "win32" ? ["/c", "start", "", url] : [url];
|
|
8173
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
8174
|
+
child.unref();
|
|
8175
|
+
return true;
|
|
8176
|
+
} catch {
|
|
8177
|
+
return false;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
async function runBrowserAuth(serverUrl) {
|
|
8181
|
+
const startRes = await fetch(new URL("/v1/auth/cli/browser/start", serverUrl).toString(), {
|
|
8182
|
+
method: "POST",
|
|
8183
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
8184
|
+
body: JSON.stringify({ device_name: hostname() })
|
|
7896
8185
|
});
|
|
7897
|
-
if (
|
|
7898
|
-
|
|
8186
|
+
if (!startRes.ok) {
|
|
8187
|
+
throw new BrowserAuthError(
|
|
8188
|
+
await errorMessage(startRes, `Browser auth failed to start (${startRes.status}).`)
|
|
8189
|
+
);
|
|
8190
|
+
}
|
|
8191
|
+
const start = await startRes.json();
|
|
8192
|
+
const opened = openBrowser(start.verification_uri_complete);
|
|
8193
|
+
if (opened) {
|
|
8194
|
+
process.stderr.write(
|
|
8195
|
+
`
|
|
8196
|
+
Your code: ${start.user_code}
|
|
8197
|
+
Complete auth in your browser, then return here.
|
|
8198
|
+
|
|
8199
|
+
`
|
|
8200
|
+
);
|
|
8201
|
+
} else {
|
|
8202
|
+
process.stderr.write(
|
|
8203
|
+
`
|
|
8204
|
+
Could not open a browser automatically.
|
|
8205
|
+
Open this URL manually:
|
|
8206
|
+
|
|
8207
|
+
${start.verification_uri_complete}
|
|
8208
|
+
|
|
8209
|
+
Your code: ${start.user_code}
|
|
8210
|
+
Waiting for you to complete auth in the browser...
|
|
8211
|
+
|
|
8212
|
+
`
|
|
8213
|
+
);
|
|
8214
|
+
}
|
|
8215
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
8216
|
+
const intervalMs = Math.max(1, start.interval) * 1e3;
|
|
8217
|
+
while (Date.now() < deadline) {
|
|
8218
|
+
await sleep(intervalMs);
|
|
8219
|
+
const pollRes = await fetch(new URL("/v1/auth/cli/browser/poll", serverUrl).toString(), {
|
|
8220
|
+
method: "POST",
|
|
8221
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
8222
|
+
body: JSON.stringify({ device_code: start.device_code })
|
|
8223
|
+
});
|
|
8224
|
+
if (pollRes.status === 202) continue;
|
|
8225
|
+
if (!pollRes.ok) {
|
|
8226
|
+
throw new BrowserAuthError(
|
|
8227
|
+
await errorMessage(pollRes, `Browser auth failed (${pollRes.status}).`)
|
|
8228
|
+
);
|
|
8229
|
+
}
|
|
8230
|
+
const data = await pollRes.json();
|
|
8231
|
+
return {
|
|
8232
|
+
auth_kind: "session",
|
|
8233
|
+
server_url: serverUrl,
|
|
8234
|
+
user_id: data.user.id,
|
|
8235
|
+
email: data.user.email,
|
|
8236
|
+
org_id: data.user.org_id,
|
|
8237
|
+
org_role: data.user.org_role,
|
|
8238
|
+
access_token: data.access_token,
|
|
8239
|
+
refresh_token: data.refresh_token,
|
|
8240
|
+
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
8241
|
+
};
|
|
8242
|
+
}
|
|
8243
|
+
throw new BrowserAuthError("Browser auth timed out. Run `keynv login` to try again.");
|
|
7899
8244
|
}
|
|
7900
|
-
var
|
|
7901
|
-
|
|
7902
|
-
|
|
8245
|
+
var BrowserAuthError;
|
|
8246
|
+
var init_browser_auth = __esm({
|
|
8247
|
+
"src/client/browser-auth.ts"() {
|
|
8248
|
+
init_version();
|
|
8249
|
+
BrowserAuthError = class extends Error {
|
|
8250
|
+
};
|
|
8251
|
+
}
|
|
8252
|
+
});
|
|
8253
|
+
|
|
8254
|
+
// src/client/defaults.ts
|
|
8255
|
+
var DEFAULT_SERVER_URL;
|
|
8256
|
+
var init_defaults = __esm({
|
|
8257
|
+
"src/client/defaults.ts"() {
|
|
8258
|
+
DEFAULT_SERVER_URL = "https://api.keynv.dev";
|
|
7903
8259
|
}
|
|
7904
8260
|
});
|
|
7905
8261
|
|
|
@@ -7927,6 +8283,33 @@ var init_pickEnv = __esm({
|
|
|
7927
8283
|
init_dist5();
|
|
7928
8284
|
}
|
|
7929
8285
|
});
|
|
8286
|
+
|
|
8287
|
+
// src/ui/helpers/pickSecret.ts
|
|
8288
|
+
async function listSecrets(client, projectId2) {
|
|
8289
|
+
const data = await client.request(
|
|
8290
|
+
`/v1/projects/${projectId2}/secrets`
|
|
8291
|
+
);
|
|
8292
|
+
return data.secrets;
|
|
8293
|
+
}
|
|
8294
|
+
async function pickSecret(client, projectId2, message = "Secret:") {
|
|
8295
|
+
const secrets = await listSecrets(client, projectId2);
|
|
8296
|
+
if (secrets.length === 0) return null;
|
|
8297
|
+
const value = await Ee({
|
|
8298
|
+
message,
|
|
8299
|
+
options: secrets.map((s) => ({
|
|
8300
|
+
value: s.alias,
|
|
8301
|
+
label: s.alias,
|
|
8302
|
+
hint: `v${s.version}`
|
|
8303
|
+
}))
|
|
8304
|
+
});
|
|
8305
|
+
if (q(value)) return null;
|
|
8306
|
+
return secrets.find((s) => s.alias === value) ?? null;
|
|
8307
|
+
}
|
|
8308
|
+
var init_pickSecret = __esm({
|
|
8309
|
+
"src/ui/helpers/pickSecret.ts"() {
|
|
8310
|
+
init_dist5();
|
|
8311
|
+
}
|
|
8312
|
+
});
|
|
7930
8313
|
async function runSecretsFlow(client, project) {
|
|
7931
8314
|
let target = project;
|
|
7932
8315
|
if (!target) {
|
|
@@ -7990,7 +8373,10 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
7990
8373
|
}
|
|
7991
8374
|
} else if (choice === "rotate") {
|
|
7992
8375
|
const newValue = unwrap(
|
|
7993
|
-
await Ce({
|
|
8376
|
+
await Ce({
|
|
8377
|
+
message: "New value",
|
|
8378
|
+
validate: (v2) => v2?.length ? void 0 : "required"
|
|
8379
|
+
})
|
|
7994
8380
|
);
|
|
7995
8381
|
const data = await client.request(`${path}/rotate`, {
|
|
7996
8382
|
method: "POST",
|
|
@@ -7998,9 +8384,7 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
7998
8384
|
});
|
|
7999
8385
|
R2.success(`Rotated ${data.alias} \u2192 v${data.version}`);
|
|
8000
8386
|
} else if (choice === "delete") {
|
|
8001
|
-
const confirmed = unwrap(
|
|
8002
|
-
await ue({ message: `Delete ${alias2}?`, initialValue: false })
|
|
8003
|
-
);
|
|
8387
|
+
const confirmed = unwrap(await ue({ message: `Delete ${alias2}?`, initialValue: false }));
|
|
8004
8388
|
if (!confirmed) continue;
|
|
8005
8389
|
await client.request(path, { method: "DELETE" });
|
|
8006
8390
|
R2.success(`Deleted ${alias2}`);
|
|
@@ -8009,11 +8393,13 @@ async function runSecretMenu(client, project, alias2) {
|
|
|
8009
8393
|
}
|
|
8010
8394
|
}
|
|
8011
8395
|
async function copyToClipboard(value) {
|
|
8012
|
-
const
|
|
8013
|
-
const cmd =
|
|
8396
|
+
const platform2 = process.platform;
|
|
8397
|
+
const cmd = platform2 === "darwin" ? ["pbcopy", []] : platform2 === "win32" ? ["clip", []] : ["xclip", ["-selection", "clipboard"]];
|
|
8014
8398
|
return new Promise((resolve3) => {
|
|
8015
8399
|
try {
|
|
8016
|
-
const child = spawn(cmd[0], cmd[1], {
|
|
8400
|
+
const child = spawn(cmd[0], cmd[1], {
|
|
8401
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
8402
|
+
});
|
|
8017
8403
|
child.on("error", () => resolve3(false));
|
|
8018
8404
|
child.on("exit", (code) => resolve3(code === 0));
|
|
8019
8405
|
child.stdin.end(value);
|
|
@@ -8040,7 +8426,7 @@ async function promptNewSecret(client, project) {
|
|
|
8040
8426
|
}),
|
|
8041
8427
|
value: () => Ce({
|
|
8042
8428
|
message: "Value (hidden)",
|
|
8043
|
-
validate: (v2) => v2
|
|
8429
|
+
validate: (v2) => v2?.length ? void 0 : "required"
|
|
8044
8430
|
})
|
|
8045
8431
|
},
|
|
8046
8432
|
{
|
|
@@ -8069,8 +8455,8 @@ async function createSecretInteractive(client, project) {
|
|
|
8069
8455
|
}
|
|
8070
8456
|
var init_secret = __esm({
|
|
8071
8457
|
"src/ui/flows/secret.ts"() {
|
|
8072
|
-
init_dist();
|
|
8073
8458
|
init_dist5();
|
|
8459
|
+
init_dist();
|
|
8074
8460
|
init_cancel();
|
|
8075
8461
|
init_pickEnv();
|
|
8076
8462
|
init_pickProject();
|
|
@@ -24764,7 +25150,7 @@ var require_named_placeholders = __commonJS({
|
|
|
24764
25150
|
}
|
|
24765
25151
|
return s;
|
|
24766
25152
|
}
|
|
24767
|
-
function
|
|
25153
|
+
function join6(tree) {
|
|
24768
25154
|
if (tree.length === 1) {
|
|
24769
25155
|
return tree;
|
|
24770
25156
|
}
|
|
@@ -24790,7 +25176,7 @@ var require_named_placeholders = __commonJS({
|
|
|
24790
25176
|
if (cache2 && (tree = cache2.get(query))) {
|
|
24791
25177
|
return toArrayParams(tree, paramsObj);
|
|
24792
25178
|
}
|
|
24793
|
-
tree =
|
|
25179
|
+
tree = join6(parse(query));
|
|
24794
25180
|
if (cache2) {
|
|
24795
25181
|
cache2.set(query, tree);
|
|
24796
25182
|
}
|
|
@@ -28403,7 +28789,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
28403
28789
|
var nodeCrypto = __require("crypto");
|
|
28404
28790
|
module.exports = {
|
|
28405
28791
|
postgresMd5PasswordHash,
|
|
28406
|
-
randomBytes,
|
|
28792
|
+
randomBytes: randomBytes2,
|
|
28407
28793
|
deriveKey,
|
|
28408
28794
|
sha256,
|
|
28409
28795
|
hashByName,
|
|
@@ -28413,7 +28799,7 @@ var require_utils_webcrypto = __commonJS({
|
|
|
28413
28799
|
var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
|
|
28414
28800
|
var subtleCrypto = webCrypto.subtle;
|
|
28415
28801
|
var textEncoder = new TextEncoder();
|
|
28416
|
-
function
|
|
28802
|
+
function randomBytes2(length) {
|
|
28417
28803
|
return webCrypto.getRandomValues(Buffer.alloc(length));
|
|
28418
28804
|
}
|
|
28419
28805
|
async function md5(string) {
|
|
@@ -28809,11 +29195,11 @@ var require_pg_connection_string = __commonJS({
|
|
|
28809
29195
|
config.client_encoding = result.searchParams.get("encoding");
|
|
28810
29196
|
return config;
|
|
28811
29197
|
}
|
|
28812
|
-
const
|
|
29198
|
+
const hostname2 = dummyHost ? "" : result.hostname;
|
|
28813
29199
|
if (!config.host) {
|
|
28814
|
-
config.host = decodeURIComponent(
|
|
28815
|
-
} else if (
|
|
28816
|
-
result.pathname =
|
|
29200
|
+
config.host = decodeURIComponent(hostname2);
|
|
29201
|
+
} else if (hostname2 && /^%2f/i.test(hostname2)) {
|
|
29202
|
+
result.pathname = hostname2 + result.pathname;
|
|
28817
29203
|
}
|
|
28818
29204
|
if (!config.port) {
|
|
28819
29205
|
config.port = result.port;
|
|
@@ -39919,9 +40305,9 @@ var require_cluster = __commonJS({
|
|
|
39919
40305
|
}
|
|
39920
40306
|
});
|
|
39921
40307
|
}
|
|
39922
|
-
resolveSrv(
|
|
40308
|
+
resolveSrv(hostname2) {
|
|
39923
40309
|
return new Promise((resolve3, reject) => {
|
|
39924
|
-
this.options.resolveSrv(
|
|
40310
|
+
this.options.resolveSrv(hostname2, (err, records) => {
|
|
39925
40311
|
if (err) {
|
|
39926
40312
|
return reject(err);
|
|
39927
40313
|
}
|
|
@@ -39943,14 +40329,14 @@ var require_cluster = __commonJS({
|
|
|
39943
40329
|
});
|
|
39944
40330
|
});
|
|
39945
40331
|
}
|
|
39946
|
-
dnsLookup(
|
|
40332
|
+
dnsLookup(hostname2) {
|
|
39947
40333
|
return new Promise((resolve3, reject) => {
|
|
39948
|
-
this.options.dnsLookup(
|
|
40334
|
+
this.options.dnsLookup(hostname2, (err, address) => {
|
|
39949
40335
|
if (err) {
|
|
39950
|
-
debug2("failed to resolve hostname %s to IP: %s",
|
|
40336
|
+
debug2("failed to resolve hostname %s to IP: %s", hostname2, err.message);
|
|
39951
40337
|
reject(err);
|
|
39952
40338
|
} else {
|
|
39953
|
-
debug2("resolved hostname %s to IP %s",
|
|
40339
|
+
debug2("resolved hostname %s to IP %s", hostname2, address);
|
|
39954
40340
|
resolve3(address);
|
|
39955
40341
|
}
|
|
39956
40342
|
});
|
|
@@ -42275,85 +42661,27 @@ var init_audit2 = __esm({
|
|
|
42275
42661
|
});
|
|
42276
42662
|
|
|
42277
42663
|
// src/ui/flows/login.ts
|
|
42278
|
-
async function runLoginFlow(client) {
|
|
42279
|
-
const
|
|
42280
|
-
{
|
|
42281
|
-
server: () => Re({
|
|
42282
|
-
message: "Server URL",
|
|
42283
|
-
placeholder: "http://localhost:8080",
|
|
42284
|
-
defaultValue: "http://localhost:8080"
|
|
42285
|
-
}),
|
|
42286
|
-
email: () => Re({
|
|
42287
|
-
message: "Email",
|
|
42288
|
-
validate: (v2) => v2 && v2.includes("@") ? void 0 : "enter an email"
|
|
42289
|
-
}),
|
|
42290
|
-
password: () => Ce({
|
|
42291
|
-
message: "Password",
|
|
42292
|
-
validate: (v2) => v2 && v2.length >= 1 ? void 0 : "required"
|
|
42293
|
-
})
|
|
42294
|
-
},
|
|
42295
|
-
{
|
|
42296
|
-
onCancel: () => {
|
|
42297
|
-
throw new UserCancelled();
|
|
42298
|
-
}
|
|
42299
|
-
}
|
|
42300
|
-
);
|
|
42301
|
-
const server = answers.server || "http://localhost:8080";
|
|
42664
|
+
async function runLoginFlow(client, options = {}) {
|
|
42665
|
+
const server = options.server ?? DEFAULT_SERVER_URL;
|
|
42302
42666
|
const s = ft();
|
|
42303
|
-
s.start("
|
|
42304
|
-
let
|
|
42667
|
+
s.start("Waiting for authorization");
|
|
42668
|
+
let creds;
|
|
42305
42669
|
try {
|
|
42306
|
-
|
|
42307
|
-
method: "POST",
|
|
42308
|
-
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
42309
|
-
body: JSON.stringify({ email: answers.email, password: answers.password })
|
|
42310
|
-
});
|
|
42670
|
+
creds = await runBrowserAuth(server);
|
|
42311
42671
|
} catch (err) {
|
|
42312
|
-
s.error("
|
|
42313
|
-
me(err instanceof Error ? err.message : "
|
|
42314
|
-
return false;
|
|
42315
|
-
}
|
|
42316
|
-
if (!res.ok) {
|
|
42317
|
-
let msg = `login failed (${res.status})`;
|
|
42318
|
-
try {
|
|
42319
|
-
const parsed = await res.json();
|
|
42320
|
-
msg = parsed.error?.message ?? msg;
|
|
42321
|
-
} catch {
|
|
42322
|
-
}
|
|
42323
|
-
s.error(msg);
|
|
42672
|
+
s.error("Browser login failed");
|
|
42673
|
+
me(err instanceof Error ? err.message : "Unable to authenticate.");
|
|
42324
42674
|
return false;
|
|
42325
42675
|
}
|
|
42326
|
-
|
|
42327
|
-
|
|
42328
|
-
server_url: server,
|
|
42329
|
-
user_id: data.user.id,
|
|
42330
|
-
email: data.user.email,
|
|
42331
|
-
org_id: data.user.org_id,
|
|
42332
|
-
org_role: data.user.org_role,
|
|
42333
|
-
access_token: data.access_token,
|
|
42334
|
-
refresh_token: data.refresh_token,
|
|
42335
|
-
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
42336
|
-
});
|
|
42337
|
-
await client.setCredentials({
|
|
42338
|
-
server_url: server,
|
|
42339
|
-
user_id: data.user.id,
|
|
42340
|
-
email: data.user.email,
|
|
42341
|
-
org_id: data.user.org_id,
|
|
42342
|
-
org_role: data.user.org_role,
|
|
42343
|
-
access_token: data.access_token,
|
|
42344
|
-
refresh_token: data.refresh_token,
|
|
42345
|
-
access_expires_at: new Date(Date.now() + data.expires_in * 1e3).toISOString()
|
|
42346
|
-
});
|
|
42347
|
-
s.stop(`Logged in as ${data.user.email}`);
|
|
42676
|
+
await client.setCredentials(creds);
|
|
42677
|
+
s.stop(`Logged in as ${creds.email}`);
|
|
42348
42678
|
return true;
|
|
42349
42679
|
}
|
|
42350
42680
|
var init_login = __esm({
|
|
42351
42681
|
"src/ui/flows/login.ts"() {
|
|
42352
42682
|
init_dist5();
|
|
42353
|
-
|
|
42354
|
-
|
|
42355
|
-
init_version();
|
|
42356
|
-
init_cancel();
|
|
42683
|
+
init_browser_auth();
|
|
42684
|
+
init_defaults();
|
|
42357
42685
|
}
|
|
42358
42686
|
});
|
|
42359
42687
|
|
|
@@ -42412,7 +42740,7 @@ async function addMemberInteractive(client, project) {
|
|
|
42412
42740
|
{
|
|
42413
42741
|
email: () => Re({
|
|
42414
42742
|
message: "Email",
|
|
42415
|
-
validate: (v2) => v2
|
|
42743
|
+
validate: (v2) => v2?.includes("@") ? void 0 : "enter an email"
|
|
42416
42744
|
}),
|
|
42417
42745
|
role: () => Ee({
|
|
42418
42746
|
message: "Role",
|
|
@@ -42511,8 +42839,11 @@ function printDetail(detail) {
|
|
|
42511
42839
|
const envLines = detail.environments.map(
|
|
42512
42840
|
(e2) => ` ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})`
|
|
42513
42841
|
);
|
|
42514
|
-
Se(
|
|
42515
|
-
|
|
42842
|
+
Se(
|
|
42843
|
+
`${detail.name} (${detail.id})
|
|
42844
|
+
${envLines.join("\n") || " (no environments)"}`,
|
|
42845
|
+
"Project"
|
|
42846
|
+
);
|
|
42516
42847
|
}
|
|
42517
42848
|
async function createProjectInteractive(client) {
|
|
42518
42849
|
const answers = await he(
|
|
@@ -42571,6 +42902,7 @@ async function runMenu() {
|
|
|
42571
42902
|
ge(`keynv ${VERSION}`);
|
|
42572
42903
|
const client = new ApiClient();
|
|
42573
42904
|
await client.ensureHydrated();
|
|
42905
|
+
let didLogin = false;
|
|
42574
42906
|
if (!client.isLoggedIn) {
|
|
42575
42907
|
R2.info("Not logged in.");
|
|
42576
42908
|
try {
|
|
@@ -42579,6 +42911,7 @@ async function runMenu() {
|
|
|
42579
42911
|
ye("Login cancelled.");
|
|
42580
42912
|
return 1;
|
|
42581
42913
|
}
|
|
42914
|
+
didLogin = true;
|
|
42582
42915
|
} catch (err) {
|
|
42583
42916
|
if (err instanceof UserCancelled) {
|
|
42584
42917
|
me("Login cancelled.");
|
|
@@ -42590,6 +42923,20 @@ async function runMenu() {
|
|
|
42590
42923
|
const u = client.currentUser;
|
|
42591
42924
|
if (u) R2.message(`${u.email} (${u.org_role}) @ ${u.server_url}`);
|
|
42592
42925
|
}
|
|
42926
|
+
if (didLogin) {
|
|
42927
|
+
const root = findProjectRoot(process.cwd());
|
|
42928
|
+
const alreadyInitialized = root !== null && hasExistingKeynvEnv(root.path);
|
|
42929
|
+
if (!alreadyInitialized) {
|
|
42930
|
+
const setup = await ue({
|
|
42931
|
+
message: "Set up this project now?",
|
|
42932
|
+
initialValue: true
|
|
42933
|
+
});
|
|
42934
|
+
if (!q(setup) && setup) {
|
|
42935
|
+
const { runInitFlow: runInitFlow2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
42936
|
+
await runInitFlow2(client, { cwd: process.cwd(), dryRun: false, noScripts: false });
|
|
42937
|
+
}
|
|
42938
|
+
}
|
|
42939
|
+
}
|
|
42593
42940
|
while (true) {
|
|
42594
42941
|
let choice;
|
|
42595
42942
|
try {
|
|
@@ -42656,15 +43003,16 @@ var init_menu = __esm({
|
|
|
42656
43003
|
"src/ui/menu.ts"() {
|
|
42657
43004
|
init_dist5();
|
|
42658
43005
|
init_http();
|
|
43006
|
+
init_detect();
|
|
42659
43007
|
init_store();
|
|
42660
|
-
|
|
42661
|
-
init_cancel();
|
|
43008
|
+
init_version();
|
|
42662
43009
|
init_audit2();
|
|
42663
43010
|
init_login();
|
|
42664
43011
|
init_member();
|
|
42665
43012
|
init_project();
|
|
42666
43013
|
init_secret();
|
|
42667
|
-
|
|
43014
|
+
init_format();
|
|
43015
|
+
init_cancel();
|
|
42668
43016
|
}
|
|
42669
43017
|
});
|
|
42670
43018
|
|
|
@@ -43345,11 +43693,11 @@ var reducers = {
|
|
|
43345
43693
|
return { ...state, options: [{ name: `-c`, value: String(command) }] };
|
|
43346
43694
|
}
|
|
43347
43695
|
},
|
|
43348
|
-
setError: (state, segment, segmentIndex,
|
|
43696
|
+
setError: (state, segment, segmentIndex, errorMessage2) => {
|
|
43349
43697
|
if (segment === SpecialToken.EndOfInput || segment === SpecialToken.EndOfPartialInput) {
|
|
43350
|
-
return { ...state, errorMessage: `${
|
|
43698
|
+
return { ...state, errorMessage: `${errorMessage2}.` };
|
|
43351
43699
|
} else {
|
|
43352
|
-
return { ...state, errorMessage: `${
|
|
43700
|
+
return { ...state, errorMessage: `${errorMessage2} ("${segment}").` };
|
|
43353
43701
|
}
|
|
43354
43702
|
},
|
|
43355
43703
|
setOptionArityError: (state, segment) => {
|
|
@@ -44382,44 +44730,48 @@ var AuditListCommand = class extends Command {
|
|
|
44382
44730
|
sinceId = options_exports.String("--since-id");
|
|
44383
44731
|
json = options_exports.Boolean("--json", false);
|
|
44384
44732
|
async execute() {
|
|
44385
|
-
|
|
44386
|
-
|
|
44387
|
-
|
|
44388
|
-
|
|
44389
|
-
|
|
44390
|
-
|
|
44391
|
-
|
|
44392
|
-
|
|
44733
|
+
try {
|
|
44734
|
+
const client = new ApiClient();
|
|
44735
|
+
const data = await client.request(
|
|
44736
|
+
"/v1/audit",
|
|
44737
|
+
{
|
|
44738
|
+
query: {
|
|
44739
|
+
event_type: this.eventType,
|
|
44740
|
+
limit: this.limit,
|
|
44741
|
+
since_id: this.sinceId
|
|
44742
|
+
}
|
|
44393
44743
|
}
|
|
44394
|
-
|
|
44395
|
-
|
|
44396
|
-
|
|
44397
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
44744
|
+
);
|
|
44745
|
+
if (this.json) {
|
|
44746
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
44398
44747
|
`);
|
|
44399
|
-
|
|
44400
|
-
|
|
44401
|
-
|
|
44402
|
-
|
|
44403
|
-
|
|
44404
|
-
|
|
44405
|
-
|
|
44406
|
-
|
|
44407
|
-
|
|
44408
|
-
|
|
44409
|
-
|
|
44410
|
-
|
|
44411
|
-
|
|
44412
|
-
|
|
44413
|
-
|
|
44414
|
-
|
|
44415
|
-
|
|
44748
|
+
return 0;
|
|
44749
|
+
}
|
|
44750
|
+
if (data.entries.length === 0) {
|
|
44751
|
+
this.context.stdout.write("no audit entries\n");
|
|
44752
|
+
return 0;
|
|
44753
|
+
}
|
|
44754
|
+
this.context.stdout.write(
|
|
44755
|
+
`${table(
|
|
44756
|
+
["id", "ts", "actor", "agent", "event"],
|
|
44757
|
+
data.entries.map((e2) => [
|
|
44758
|
+
String(e2.id),
|
|
44759
|
+
e2.ts,
|
|
44760
|
+
e2.actor_user_id ?? "(none)",
|
|
44761
|
+
e2.actor_agent,
|
|
44762
|
+
e2.event_type
|
|
44763
|
+
])
|
|
44764
|
+
)}
|
|
44416
44765
|
`
|
|
44417
|
-
|
|
44418
|
-
|
|
44419
|
-
|
|
44766
|
+
);
|
|
44767
|
+
if (data.next_cursor) {
|
|
44768
|
+
this.context.stdout.write(`(next: --since-id ${data.next_cursor})
|
|
44420
44769
|
`);
|
|
44770
|
+
}
|
|
44771
|
+
return 0;
|
|
44772
|
+
} catch (err) {
|
|
44773
|
+
return handleExecError(this.context.stderr, err);
|
|
44421
44774
|
}
|
|
44422
|
-
return 0;
|
|
44423
44775
|
}
|
|
44424
44776
|
};
|
|
44425
44777
|
var AuditVerifyCommand = class extends Command {
|
|
@@ -44429,23 +44781,27 @@ var AuditVerifyCommand = class extends Command {
|
|
|
44429
44781
|
});
|
|
44430
44782
|
json = options_exports.Boolean("--json", false);
|
|
44431
44783
|
async execute() {
|
|
44432
|
-
|
|
44433
|
-
|
|
44434
|
-
|
|
44435
|
-
this.
|
|
44784
|
+
try {
|
|
44785
|
+
const client = new ApiClient();
|
|
44786
|
+
const data = await client.request("/v1/audit/verify", { method: "POST" });
|
|
44787
|
+
if (this.json) {
|
|
44788
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
44436
44789
|
`);
|
|
44437
|
-
|
|
44438
|
-
|
|
44439
|
-
|
|
44440
|
-
|
|
44790
|
+
return data.ok ? 0 : 1;
|
|
44791
|
+
}
|
|
44792
|
+
if (data.ok) {
|
|
44793
|
+
this.context.stdout.write(`OK: ${data.checked} entries verified
|
|
44441
44794
|
`);
|
|
44442
|
-
|
|
44443
|
-
|
|
44444
|
-
|
|
44445
|
-
|
|
44795
|
+
return 0;
|
|
44796
|
+
}
|
|
44797
|
+
this.context.stdout.write(
|
|
44798
|
+
`FAIL: chain broken at id ${data.broken_at_id} (${data.reason}); ${data.checked} entries verified before break
|
|
44446
44799
|
`
|
|
44447
|
-
|
|
44448
|
-
|
|
44800
|
+
);
|
|
44801
|
+
return 1;
|
|
44802
|
+
} catch (err) {
|
|
44803
|
+
return handleExecError(this.context.stderr, err);
|
|
44804
|
+
}
|
|
44449
44805
|
}
|
|
44450
44806
|
};
|
|
44451
44807
|
|
|
@@ -44486,11 +44842,10 @@ async function resolveAllAliases(client, argv2, extraStrings = []) {
|
|
|
44486
44842
|
return resolved;
|
|
44487
44843
|
}
|
|
44488
44844
|
function substitute(text, resolved) {
|
|
44845
|
+
const sorted = [...resolved].sort((a, b2) => b2.alias.literal.length - a.alias.literal.length);
|
|
44489
44846
|
let out = text;
|
|
44490
|
-
for (const r of
|
|
44491
|
-
|
|
44492
|
-
out = out.split(r.alias.literal).join(r.value);
|
|
44493
|
-
}
|
|
44847
|
+
for (const r of sorted) {
|
|
44848
|
+
out = out.split(r.alias.literal).join(r.value);
|
|
44494
44849
|
}
|
|
44495
44850
|
return out;
|
|
44496
44851
|
}
|
|
@@ -44593,10 +44948,18 @@ var BUILTIN_LINE_PATTERNS = BUILTIN_PATTERNS.filter((p2) => !p2.multiline);
|
|
|
44593
44948
|
|
|
44594
44949
|
// ../../packages/redactor/dist/entropy.js
|
|
44595
44950
|
var TOKEN_BOUNDARY_RE = /[\s,;:'"<>(){}[\]=]+/;
|
|
44951
|
+
var DEFAULT_EXCLUDE_PREFIXES = [
|
|
44952
|
+
"sha1-",
|
|
44953
|
+
"sha256:",
|
|
44954
|
+
"sha256-",
|
|
44955
|
+
"sha384-",
|
|
44956
|
+
"sha512-"
|
|
44957
|
+
];
|
|
44596
44958
|
var DEFAULTS = {
|
|
44597
44959
|
enabled: true,
|
|
44598
44960
|
minLength: 24,
|
|
44599
|
-
minBitsPerChar: 4.5
|
|
44961
|
+
minBitsPerChar: 4.5,
|
|
44962
|
+
excludePrefixes: DEFAULT_EXCLUDE_PREFIXES
|
|
44600
44963
|
};
|
|
44601
44964
|
function shannonEntropy(s) {
|
|
44602
44965
|
if (s.length === 0)
|
|
@@ -44629,9 +44992,13 @@ function findEntropyMatches(text, opts = {}) {
|
|
|
44629
44992
|
const end = i;
|
|
44630
44993
|
const token = text.slice(start, end);
|
|
44631
44994
|
if (token.length >= cfg.minLength) {
|
|
44632
|
-
const
|
|
44633
|
-
|
|
44634
|
-
|
|
44995
|
+
const lower = token.toLowerCase();
|
|
44996
|
+
const excluded = cfg.excludePrefixes.some((p2) => lower.startsWith(p2));
|
|
44997
|
+
if (!excluded) {
|
|
44998
|
+
const h2 = shannonEntropy(token);
|
|
44999
|
+
if (h2 >= cfg.minBitsPerChar) {
|
|
45000
|
+
matches.push({ start, end, token });
|
|
45001
|
+
}
|
|
44635
45002
|
}
|
|
44636
45003
|
}
|
|
44637
45004
|
}
|
|
@@ -44775,7 +45142,19 @@ var ENV_ALLOWLIST = [
|
|
|
44775
45142
|
"PWD",
|
|
44776
45143
|
"OLDPWD",
|
|
44777
45144
|
"TMPDIR",
|
|
44778
|
-
"SSH_AUTH_SOCK"
|
|
45145
|
+
"SSH_AUTH_SOCK",
|
|
45146
|
+
// Windows-specific
|
|
45147
|
+
"USERPROFILE",
|
|
45148
|
+
"USERNAME",
|
|
45149
|
+
"COMPUTERNAME",
|
|
45150
|
+
"TEMP",
|
|
45151
|
+
"TMP",
|
|
45152
|
+
"SYSTEMROOT",
|
|
45153
|
+
"SYSTEMDRIVE",
|
|
45154
|
+
"WINDIR",
|
|
45155
|
+
"COMSPEC",
|
|
45156
|
+
"APPDATA",
|
|
45157
|
+
"LOCALAPPDATA"
|
|
44779
45158
|
];
|
|
44780
45159
|
function spawnPrivileged(opts) {
|
|
44781
45160
|
const startedAt = Date.now();
|
|
@@ -44788,7 +45167,32 @@ function spawnPrivileged(opts) {
|
|
|
44788
45167
|
for (const [k2, v2] of Object.entries(opts.injectedEnv)) env2[k2] = v2;
|
|
44789
45168
|
}
|
|
44790
45169
|
const stdio = ["inherit", "pipe", "pipe"];
|
|
44791
|
-
const
|
|
45170
|
+
const WIN_BUILTINS = /* @__PURE__ */ new Set([
|
|
45171
|
+
"echo",
|
|
45172
|
+
"dir",
|
|
45173
|
+
"type",
|
|
45174
|
+
"copy",
|
|
45175
|
+
"del",
|
|
45176
|
+
"move",
|
|
45177
|
+
"ren",
|
|
45178
|
+
"md",
|
|
45179
|
+
"mkdir",
|
|
45180
|
+
"rd",
|
|
45181
|
+
"rmdir",
|
|
45182
|
+
"cd",
|
|
45183
|
+
"cls",
|
|
45184
|
+
"set",
|
|
45185
|
+
"pause",
|
|
45186
|
+
"find",
|
|
45187
|
+
"where"
|
|
45188
|
+
]);
|
|
45189
|
+
let spawnCmd = opts.command;
|
|
45190
|
+
let spawnArgs = opts.args;
|
|
45191
|
+
if (process.platform === "win32" && WIN_BUILTINS.has(opts.command.toLowerCase())) {
|
|
45192
|
+
spawnCmd = process.env.COMSPEC ?? "cmd.exe";
|
|
45193
|
+
spawnArgs = ["/d", "/s", "/c", opts.command, ...opts.args];
|
|
45194
|
+
}
|
|
45195
|
+
const child = spawn(spawnCmd, spawnArgs, {
|
|
44792
45196
|
env: env2,
|
|
44793
45197
|
stdio,
|
|
44794
45198
|
detached: false
|
|
@@ -44855,10 +45259,7 @@ to load a specific file, or set KEYNV_ENV_FILE in the environment.
|
|
|
44855
45259
|
spelled \`--from\` to avoid the collision.)
|
|
44856
45260
|
`,
|
|
44857
45261
|
examples: [
|
|
44858
|
-
[
|
|
44859
|
-
"Auto-load .keynv.env from cwd or parents",
|
|
44860
|
-
"$0 exec -- next dev"
|
|
44861
|
-
],
|
|
45262
|
+
["Auto-load .keynv.env from cwd or parents", "$0 exec -- next dev"],
|
|
44862
45263
|
[
|
|
44863
45264
|
"Run mysql with the alias substituted at fork-exec time",
|
|
44864
45265
|
"$0 exec -- mysql -p@billing.dev.db_pass -h db.example.com"
|
|
@@ -44922,7 +45323,18 @@ spelled \`--from\` to avoid the collision.)
|
|
|
44922
45323
|
`);
|
|
44923
45324
|
return 2;
|
|
44924
45325
|
}
|
|
44925
|
-
|
|
45326
|
+
this.context.stderr.write(
|
|
45327
|
+
`keynv: unexpected error loading env file: ${err instanceof Error ? err.message : String(err)}
|
|
45328
|
+
`
|
|
45329
|
+
);
|
|
45330
|
+
return 2;
|
|
45331
|
+
}
|
|
45332
|
+
if (!envFileLoaded && !this.noEnvFile && !this.envFile && !process.env.KEYNV_ENV_FILE) {
|
|
45333
|
+
this.context.stderr.write(
|
|
45334
|
+
`keynv: no ${ENV_FILE_BASENAME} found in this directory or any parent.
|
|
45335
|
+
Run \`keynv init\` in your project root to migrate secrets and create one.
|
|
45336
|
+
`
|
|
45337
|
+
);
|
|
44926
45338
|
}
|
|
44927
45339
|
const viaEnvSpecs = [];
|
|
44928
45340
|
for (const spec of this.viaEnv ?? []) {
|
|
@@ -44994,11 +45406,18 @@ spelled \`--from\` to avoid the collision.)
|
|
|
44994
45406
|
injectedEnv[spec.name] = value;
|
|
44995
45407
|
}
|
|
44996
45408
|
if (envFileLoaded && !this.quiet) {
|
|
44997
|
-
const
|
|
44998
|
-
const
|
|
45409
|
+
const aliasEntries = envFileLoaded.entries.filter((e2) => e2.isAlias);
|
|
45410
|
+
const plainEntries = envFileLoaded.entries.filter((e2) => !e2.isAlias);
|
|
45411
|
+
const displayPath = relative(process.cwd(), envFileLoaded.path) || envFileLoaded.path;
|
|
45412
|
+
const parts = [];
|
|
45413
|
+
if (aliasEntries.length > 0) {
|
|
45414
|
+
parts.push(aliasEntries.map((e2) => `${e2.name}=${e2.value}`).join(", ") + " (vault)");
|
|
45415
|
+
}
|
|
45416
|
+
if (plainEntries.length > 0) {
|
|
45417
|
+
parts.push(plainEntries.map((e2) => e2.name).join(", ") + " (plain)");
|
|
45418
|
+
}
|
|
44999
45419
|
this.context.stderr.write(
|
|
45000
|
-
`keynv: loaded ${
|
|
45001
|
-
`
|
|
45420
|
+
`keynv: loaded ${displayPath}` + (parts.length > 0 ? ` \u2014 ${parts.join("; ")}` : "") + "\n"
|
|
45002
45421
|
);
|
|
45003
45422
|
}
|
|
45004
45423
|
const timeoutS = this.timeout ? Number.parseInt(this.timeout, 10) : void 0;
|
|
@@ -45038,10 +45457,158 @@ function signalNumber(sig) {
|
|
|
45038
45457
|
}
|
|
45039
45458
|
|
|
45040
45459
|
// src/commands/init.ts
|
|
45460
|
+
init_dist();
|
|
45041
45461
|
init_http();
|
|
45042
|
-
|
|
45462
|
+
init_envFile();
|
|
45463
|
+
init_heuristics();
|
|
45043
45464
|
init_init();
|
|
45044
45465
|
init_cancel();
|
|
45466
|
+
init_tty();
|
|
45467
|
+
|
|
45468
|
+
// src/commands/project.ts
|
|
45469
|
+
init_http();
|
|
45470
|
+
init_format();
|
|
45471
|
+
function isProjectId(input) {
|
|
45472
|
+
return input.startsWith("p_") || /^[a-f0-9]{20,}$/i.test(input);
|
|
45473
|
+
}
|
|
45474
|
+
async function resolveProjectId(client, input) {
|
|
45475
|
+
if (isProjectId(input)) {
|
|
45476
|
+
return input;
|
|
45477
|
+
}
|
|
45478
|
+
const data = await client.request("/v1/projects");
|
|
45479
|
+
const match = data.projects.find((p2) => p2.name === input);
|
|
45480
|
+
if (!match) throw new Error(`project not found: ${input}`);
|
|
45481
|
+
return match.id;
|
|
45482
|
+
}
|
|
45483
|
+
var ProjectListCommand = class extends Command {
|
|
45484
|
+
static paths = [["project", "list"]];
|
|
45485
|
+
static usage = Command.Usage({
|
|
45486
|
+
description: "List projects visible to the current user."
|
|
45487
|
+
});
|
|
45488
|
+
json = options_exports.Boolean("--json", false);
|
|
45489
|
+
async execute() {
|
|
45490
|
+
try {
|
|
45491
|
+
const client = new ApiClient();
|
|
45492
|
+
const data = await client.request("/v1/projects");
|
|
45493
|
+
if (this.json) {
|
|
45494
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45495
|
+
`);
|
|
45496
|
+
return 0;
|
|
45497
|
+
}
|
|
45498
|
+
if (data.projects.length === 0) {
|
|
45499
|
+
this.context.stdout.write("no projects\n");
|
|
45500
|
+
return 0;
|
|
45501
|
+
}
|
|
45502
|
+
this.context.stdout.write(
|
|
45503
|
+
`${table(
|
|
45504
|
+
["name", "id", "created_at"],
|
|
45505
|
+
data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
|
|
45506
|
+
)}
|
|
45507
|
+
`
|
|
45508
|
+
);
|
|
45509
|
+
return 0;
|
|
45510
|
+
} catch (err) {
|
|
45511
|
+
return handleExecError(this.context.stderr, err);
|
|
45512
|
+
}
|
|
45513
|
+
}
|
|
45514
|
+
};
|
|
45515
|
+
var ProjectCreateCommand = class extends Command {
|
|
45516
|
+
static paths = [["project", "create"]];
|
|
45517
|
+
static usage = Command.Usage({
|
|
45518
|
+
description: "Create a new project with one or more environments.",
|
|
45519
|
+
examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
|
|
45520
|
+
});
|
|
45521
|
+
name = options_exports.String();
|
|
45522
|
+
envs = options_exports.Array("--env", {
|
|
45523
|
+
description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
|
|
45524
|
+
});
|
|
45525
|
+
async execute() {
|
|
45526
|
+
try {
|
|
45527
|
+
const envs = (this.envs ?? ["dev"]).map((spec) => {
|
|
45528
|
+
const [name, tier, approval] = spec.split(":");
|
|
45529
|
+
return {
|
|
45530
|
+
name: name ?? "",
|
|
45531
|
+
tier: tier ?? "non-production",
|
|
45532
|
+
require_approval: approval === "approval"
|
|
45533
|
+
};
|
|
45534
|
+
});
|
|
45535
|
+
const client = new ApiClient();
|
|
45536
|
+
const result = await client.request("/v1/projects", {
|
|
45537
|
+
method: "POST",
|
|
45538
|
+
body: { name: this.name, environments: envs }
|
|
45539
|
+
});
|
|
45540
|
+
this.context.stdout.write(`created project ${result.name} (${result.id})
|
|
45541
|
+
`);
|
|
45542
|
+
for (const e2 of envs) {
|
|
45543
|
+
this.context.stdout.write(
|
|
45544
|
+
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45545
|
+
`
|
|
45546
|
+
);
|
|
45547
|
+
}
|
|
45548
|
+
return 0;
|
|
45549
|
+
} catch (err) {
|
|
45550
|
+
return handleExecError(this.context.stderr, err);
|
|
45551
|
+
}
|
|
45552
|
+
}
|
|
45553
|
+
};
|
|
45554
|
+
var ProjectDescribeCommand = class extends Command {
|
|
45555
|
+
static paths = [["project", "describe"]];
|
|
45556
|
+
static usage = Command.Usage({
|
|
45557
|
+
description: "Show metadata for one project.",
|
|
45558
|
+
examples: [
|
|
45559
|
+
["By ID", "$0 project describe p_go6rqgwz0wlokdsl55ikn"],
|
|
45560
|
+
["By name", "$0 project describe myproject"]
|
|
45561
|
+
]
|
|
45562
|
+
});
|
|
45563
|
+
id = options_exports.String();
|
|
45564
|
+
json = options_exports.Boolean("--json", false);
|
|
45565
|
+
async execute() {
|
|
45566
|
+
try {
|
|
45567
|
+
const client = new ApiClient();
|
|
45568
|
+
const projectId2 = await resolveProjectId(client, this.id);
|
|
45569
|
+
const data = await client.request(`/v1/projects/${projectId2}`);
|
|
45570
|
+
if (this.json) {
|
|
45571
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45572
|
+
`);
|
|
45573
|
+
return 0;
|
|
45574
|
+
}
|
|
45575
|
+
this.context.stdout.write(`project: ${data.name} (${data.id})
|
|
45576
|
+
`);
|
|
45577
|
+
for (const e2 of data.environments) {
|
|
45578
|
+
this.context.stdout.write(
|
|
45579
|
+
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45580
|
+
`
|
|
45581
|
+
);
|
|
45582
|
+
}
|
|
45583
|
+
return 0;
|
|
45584
|
+
} catch (err) {
|
|
45585
|
+
return handleExecError(this.context.stderr, err);
|
|
45586
|
+
}
|
|
45587
|
+
}
|
|
45588
|
+
};
|
|
45589
|
+
var ProjectDeleteCommand = class extends Command {
|
|
45590
|
+
static paths = [["project", "delete"]];
|
|
45591
|
+
static usage = Command.Usage({ description: "Soft-delete a project." });
|
|
45592
|
+
id = options_exports.String();
|
|
45593
|
+
force = options_exports.Boolean("--force", false);
|
|
45594
|
+
async execute() {
|
|
45595
|
+
if (!this.force) {
|
|
45596
|
+
this.context.stderr.write("keynv: refusing to delete without --force\n");
|
|
45597
|
+
return 2;
|
|
45598
|
+
}
|
|
45599
|
+
try {
|
|
45600
|
+
const client = new ApiClient();
|
|
45601
|
+
await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
|
|
45602
|
+
this.context.stdout.write(`deleted project ${this.id}
|
|
45603
|
+
`);
|
|
45604
|
+
return 0;
|
|
45605
|
+
} catch (err) {
|
|
45606
|
+
return handleExecError(this.context.stderr, err);
|
|
45607
|
+
}
|
|
45608
|
+
}
|
|
45609
|
+
};
|
|
45610
|
+
|
|
45611
|
+
// src/commands/init.ts
|
|
45045
45612
|
var InitCommand = class extends Command {
|
|
45046
45613
|
static paths = [["init"]];
|
|
45047
45614
|
static usage = Command.Usage({
|
|
@@ -45055,6 +45622,11 @@ package.json scripts with \`keynv exec\`.
|
|
|
45055
45622
|
Safe to re-run: existing .keynv.env entries are preserved; new
|
|
45056
45623
|
entries are appended below a marker.
|
|
45057
45624
|
|
|
45625
|
+
--dry-run prints the secrets that would be uploaded and the alias
|
|
45626
|
+
mappings that would be written to .keynv.env, then exits without
|
|
45627
|
+
touching any files or making any network calls. Use it to preview
|
|
45628
|
+
what init will do before committing.
|
|
45629
|
+
|
|
45058
45630
|
Requires an interactive terminal (clack TUI). For scripted
|
|
45059
45631
|
migration, use the lower-level \`keynv project\` and \`keynv secret\`
|
|
45060
45632
|
commands directly.
|
|
@@ -45062,28 +45634,47 @@ commands directly.
|
|
|
45062
45634
|
examples: [
|
|
45063
45635
|
["Walk the current project", "$0 init"],
|
|
45064
45636
|
["Preview without writing or uploading", "$0 init --dry-run"],
|
|
45065
|
-
["Skip the package.json script-wrapping step", "$0 init --no-scripts"]
|
|
45637
|
+
["Skip the package.json script-wrapping step", "$0 init --no-scripts"],
|
|
45638
|
+
["Non-interactive (CI/CD)", "$0 init --env-file .env --project myproject --env dev"]
|
|
45066
45639
|
]
|
|
45067
45640
|
});
|
|
45068
45641
|
dryRun = options_exports.Boolean("--dry-run", false, {
|
|
45069
|
-
description: "
|
|
45642
|
+
description: "Scan .env files and print the secrets that would be uploaded and the alias mappings that would be written \u2014 no files written, no vault calls made."
|
|
45070
45643
|
});
|
|
45071
45644
|
noScripts = options_exports.Boolean("--no-scripts", false, {
|
|
45072
45645
|
description: "Skip the package.json script-wrapping step."
|
|
45073
45646
|
});
|
|
45647
|
+
envFile = options_exports.String("--env-file", {
|
|
45648
|
+
description: "Path to .env file to migrate (non-interactive mode)."
|
|
45649
|
+
});
|
|
45650
|
+
project = options_exports.String("--project", {
|
|
45651
|
+
description: "Project name or ID (non-interactive mode)."
|
|
45652
|
+
});
|
|
45653
|
+
env = options_exports.String("--env", {
|
|
45654
|
+
description: "Environment name for --env-file (default: dev)."
|
|
45655
|
+
});
|
|
45656
|
+
secret = options_exports.Array("--secret", {
|
|
45657
|
+
description: "KEY=value secret to upload (non-interactive). Can be specified multiple times."
|
|
45658
|
+
});
|
|
45074
45659
|
async execute() {
|
|
45075
|
-
if (!isInteractive()) {
|
|
45076
|
-
this.context.stderr.write(
|
|
45077
|
-
"keynv init requires an interactive terminal. Use the lower-level commands (`keynv project`, `keynv secret`) for scripted setup.\n"
|
|
45078
|
-
);
|
|
45079
|
-
return 1;
|
|
45080
|
-
}
|
|
45081
45660
|
const client = new ApiClient();
|
|
45082
45661
|
await client.ensureHydrated();
|
|
45083
45662
|
if (!client.isLoggedIn) {
|
|
45084
45663
|
this.context.stderr.write("keynv: not logged in. Run `keynv login` first.\n");
|
|
45085
45664
|
return 1;
|
|
45086
45665
|
}
|
|
45666
|
+
const hasEnvFile = this.envFile != null;
|
|
45667
|
+
const hasSecrets = this.secret != null && this.secret.length > 0;
|
|
45668
|
+
const isNonInteractive = hasEnvFile || hasSecrets;
|
|
45669
|
+
if (isNonInteractive) {
|
|
45670
|
+
return this.runNonInteractive(client);
|
|
45671
|
+
}
|
|
45672
|
+
if (!isInteractive()) {
|
|
45673
|
+
this.context.stderr.write(
|
|
45674
|
+
"keynv init requires an interactive terminal. Use --env-file or --secret for scripted setup.\n"
|
|
45675
|
+
);
|
|
45676
|
+
return 1;
|
|
45677
|
+
}
|
|
45087
45678
|
try {
|
|
45088
45679
|
const outcome = await runInitFlow(client, {
|
|
45089
45680
|
cwd: process.cwd(),
|
|
@@ -45099,9 +45690,102 @@ commands directly.
|
|
|
45099
45690
|
return 1;
|
|
45100
45691
|
}
|
|
45101
45692
|
}
|
|
45693
|
+
async runNonInteractive(client) {
|
|
45694
|
+
const projectName = this.project;
|
|
45695
|
+
if (!projectName) {
|
|
45696
|
+
this.context.stderr.write("keynv: --project is required in non-interactive mode.\n");
|
|
45697
|
+
return 1;
|
|
45698
|
+
}
|
|
45699
|
+
const resolved = await resolveProjectId(client, projectName);
|
|
45700
|
+
if (!resolved) {
|
|
45701
|
+
this.context.stderr.write(`keynv: project not found: ${projectName}
|
|
45702
|
+
`);
|
|
45703
|
+
return 1;
|
|
45704
|
+
}
|
|
45705
|
+
const projectId2 = resolved;
|
|
45706
|
+
const envName = this.env ?? "dev";
|
|
45707
|
+
const secrets = [];
|
|
45708
|
+
if (this.envFile) {
|
|
45709
|
+
try {
|
|
45710
|
+
const content = readFileSync(this.envFile, "utf8");
|
|
45711
|
+
const entries = parseEnvFile(content, this.envFile);
|
|
45712
|
+
for (const e2 of entries) {
|
|
45713
|
+
if (classifyEntry(e2.name, e2.value).verdict === "secret") {
|
|
45714
|
+
secrets.push({ name: e2.name, value: e2.value });
|
|
45715
|
+
}
|
|
45716
|
+
}
|
|
45717
|
+
} catch (err) {
|
|
45718
|
+
this.context.stderr.write(
|
|
45719
|
+
`keynv: cannot read ${this.envFile}: ${err instanceof Error ? err.message : String(err)}
|
|
45720
|
+
`
|
|
45721
|
+
);
|
|
45722
|
+
return 1;
|
|
45723
|
+
}
|
|
45724
|
+
}
|
|
45725
|
+
if (this.secret) {
|
|
45726
|
+
for (const spec of this.secret) {
|
|
45727
|
+
const eq = spec.indexOf("=");
|
|
45728
|
+
if (eq <= 0) {
|
|
45729
|
+
this.context.stderr.write(`keynv: invalid --secret '${spec}', expected KEY=value
|
|
45730
|
+
`);
|
|
45731
|
+
return 1;
|
|
45732
|
+
}
|
|
45733
|
+
const name = spec.slice(0, eq);
|
|
45734
|
+
const value = spec.slice(eq + 1);
|
|
45735
|
+
secrets.push({ name, value });
|
|
45736
|
+
}
|
|
45737
|
+
}
|
|
45738
|
+
if (secrets.length === 0) {
|
|
45739
|
+
this.context.stdout.write("keynv: nothing to migrate.\n");
|
|
45740
|
+
return 0;
|
|
45741
|
+
}
|
|
45742
|
+
if (this.dryRun) {
|
|
45743
|
+
this.context.stdout.write(
|
|
45744
|
+
`keynv: dry-run \u2014 would upload ${secrets.length} secret(s) to project ${projectName} (${projectId2}) in env ${envName}
|
|
45745
|
+
`
|
|
45746
|
+
);
|
|
45747
|
+
for (const { name } of secrets) {
|
|
45748
|
+
const aliasKey = name.toLowerCase().replace(/_/g, "-");
|
|
45749
|
+
this.context.stdout.write(` ${name}=@${projectName}.${envName}.${aliasKey}
|
|
45750
|
+
`);
|
|
45751
|
+
}
|
|
45752
|
+
return 0;
|
|
45753
|
+
}
|
|
45754
|
+
let uploaded = 0;
|
|
45755
|
+
const failed = [];
|
|
45756
|
+
for (const { name, value } of secrets) {
|
|
45757
|
+
const aliasKey = name.toLowerCase().replace(/_/g, "-");
|
|
45758
|
+
const alias2 = buildAlias({ project: projectName, environment: envName, key: aliasKey });
|
|
45759
|
+
if (!alias2) {
|
|
45760
|
+
failed.push(`${name} (invalid alias key: ${aliasKey})`);
|
|
45761
|
+
continue;
|
|
45762
|
+
}
|
|
45763
|
+
try {
|
|
45764
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
45765
|
+
method: "POST",
|
|
45766
|
+
body: { env: envName, key: aliasKey, value }
|
|
45767
|
+
});
|
|
45768
|
+
uploaded++;
|
|
45769
|
+
} catch (err) {
|
|
45770
|
+
failed.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
45771
|
+
}
|
|
45772
|
+
}
|
|
45773
|
+
this.context.stdout.write(
|
|
45774
|
+
`keynv: uploaded ${uploaded}/${secrets.length} secret(s) to ${projectName}.${envName}
|
|
45775
|
+
`
|
|
45776
|
+
);
|
|
45777
|
+
if (failed.length > 0) {
|
|
45778
|
+
for (const f of failed) this.context.stderr.write(` failed: ${f}
|
|
45779
|
+
`);
|
|
45780
|
+
return 1;
|
|
45781
|
+
}
|
|
45782
|
+
return 0;
|
|
45783
|
+
}
|
|
45102
45784
|
};
|
|
45103
45785
|
|
|
45104
45786
|
// src/commands/login.ts
|
|
45787
|
+
init_browser_auth();
|
|
45788
|
+
init_defaults();
|
|
45105
45789
|
init_http();
|
|
45106
45790
|
init_store();
|
|
45107
45791
|
init_format();
|
|
@@ -45162,25 +45846,63 @@ var LoginCommand = class extends Command {
|
|
|
45162
45846
|
static usage = Command.Usage({
|
|
45163
45847
|
description: "Authenticate against a keynv server.",
|
|
45164
45848
|
examples: [
|
|
45165
|
-
["
|
|
45849
|
+
["Browser login", "$0 login"],
|
|
45850
|
+
["Self-hosted browser login", "$0 login --server http://localhost:8080"],
|
|
45851
|
+
["Headless token login", "$0 login --server http://... --token kt_..."],
|
|
45166
45852
|
["Non-interactive", "$0 login --server http://... --email a@b.com --password ..."]
|
|
45167
45853
|
]
|
|
45168
45854
|
});
|
|
45169
45855
|
server = options_exports.String("--server", { description: "Server base URL." });
|
|
45856
|
+
token = options_exports.String("--token", { description: "CLI token for headless auth." });
|
|
45170
45857
|
email = options_exports.String("--email", { description: "Email address." });
|
|
45171
45858
|
password = options_exports.String("--password", {
|
|
45172
45859
|
description: "Password (use stdin to avoid argv leak)."
|
|
45173
45860
|
});
|
|
45174
45861
|
async execute() {
|
|
45175
|
-
const
|
|
45176
|
-
|
|
45862
|
+
const finalServerUrl = this.server ?? DEFAULT_SERVER_URL;
|
|
45863
|
+
try {
|
|
45864
|
+
new URL(finalServerUrl);
|
|
45865
|
+
} catch {
|
|
45866
|
+
this.context.stderr.write(`keynv: invalid server URL '${finalServerUrl}'
|
|
45867
|
+
`);
|
|
45868
|
+
return 1;
|
|
45869
|
+
}
|
|
45870
|
+
if (this.token) {
|
|
45871
|
+
return this.loginWithToken(finalServerUrl, this.token);
|
|
45872
|
+
}
|
|
45873
|
+
if (!this.email && !this.password) {
|
|
45874
|
+
this.context.stdout.write(`Opening browser for ${finalServerUrl} ...
|
|
45875
|
+
`);
|
|
45876
|
+
try {
|
|
45877
|
+
const creds = await runBrowserAuth(finalServerUrl);
|
|
45878
|
+
await saveCredentials(creds);
|
|
45879
|
+
this.context.stdout.write(`logged in as ${creds.email} (${creds.org_role})
|
|
45880
|
+
`);
|
|
45881
|
+
return 0;
|
|
45882
|
+
} catch (err) {
|
|
45883
|
+
this.context.stderr.write(
|
|
45884
|
+
`keynv: ${err instanceof Error ? err.message : "browser login failed"}
|
|
45885
|
+
`
|
|
45886
|
+
);
|
|
45887
|
+
return 1;
|
|
45888
|
+
}
|
|
45889
|
+
}
|
|
45177
45890
|
const email2 = this.email ?? await promptLine("email: ");
|
|
45178
45891
|
const password = this.password ?? await promptHidden("password: ");
|
|
45179
|
-
|
|
45180
|
-
|
|
45181
|
-
|
|
45182
|
-
|
|
45183
|
-
|
|
45892
|
+
let res;
|
|
45893
|
+
try {
|
|
45894
|
+
res = await fetch(new URL("/v1/auth/login", finalServerUrl).toString(), {
|
|
45895
|
+
method: "POST",
|
|
45896
|
+
headers: { "content-type": "application/json", "x-keynv-agent": AGENT },
|
|
45897
|
+
body: JSON.stringify({ email: email2, password })
|
|
45898
|
+
});
|
|
45899
|
+
} catch (err) {
|
|
45900
|
+
this.context.stderr.write(
|
|
45901
|
+
`keynv: cannot reach server '${finalServerUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
45902
|
+
`
|
|
45903
|
+
);
|
|
45904
|
+
return 1;
|
|
45905
|
+
}
|
|
45184
45906
|
if (!res.ok) {
|
|
45185
45907
|
const text = await res.text();
|
|
45186
45908
|
let msg = `login failed (${res.status})`;
|
|
@@ -45204,6 +45926,7 @@ var LoginCommand = class extends Command {
|
|
|
45204
45926
|
const data = await res.json();
|
|
45205
45927
|
try {
|
|
45206
45928
|
await saveCredentials({
|
|
45929
|
+
auth_kind: "session",
|
|
45207
45930
|
server_url: finalServerUrl,
|
|
45208
45931
|
user_id: data.user.id,
|
|
45209
45932
|
email: data.user.email,
|
|
@@ -45221,6 +45944,63 @@ var LoginCommand = class extends Command {
|
|
|
45221
45944
|
return 1;
|
|
45222
45945
|
}
|
|
45223
45946
|
this.context.stdout.write(`logged in as ${data.user.email} (${data.user.org_role})
|
|
45947
|
+
`);
|
|
45948
|
+
return 0;
|
|
45949
|
+
}
|
|
45950
|
+
async loginWithToken(serverUrl, token) {
|
|
45951
|
+
let res;
|
|
45952
|
+
try {
|
|
45953
|
+
res = await fetch(new URL("/v1/whoami", serverUrl).toString(), {
|
|
45954
|
+
headers: { authorization: `Bearer ${token}`, "x-keynv-agent": AGENT }
|
|
45955
|
+
});
|
|
45956
|
+
} catch (err) {
|
|
45957
|
+
this.context.stderr.write(
|
|
45958
|
+
`keynv: cannot reach server '${serverUrl}' \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
45959
|
+
`
|
|
45960
|
+
);
|
|
45961
|
+
return 1;
|
|
45962
|
+
}
|
|
45963
|
+
if (!res.ok) {
|
|
45964
|
+
const text = await res.text();
|
|
45965
|
+
let msg = `token login failed (${res.status})`;
|
|
45966
|
+
try {
|
|
45967
|
+
const parsed = JSON.parse(text);
|
|
45968
|
+
msg = parsed.error?.message ?? msg;
|
|
45969
|
+
this.context.stderr.write(
|
|
45970
|
+
`${fmtError({
|
|
45971
|
+
status: res.status,
|
|
45972
|
+
...parsed.error?.code ? { code: parsed.error.code } : {},
|
|
45973
|
+
message: msg
|
|
45974
|
+
})}
|
|
45975
|
+
`
|
|
45976
|
+
);
|
|
45977
|
+
} catch {
|
|
45978
|
+
this.context.stderr.write(`keynv: ${msg}
|
|
45979
|
+
`);
|
|
45980
|
+
}
|
|
45981
|
+
return 1;
|
|
45982
|
+
}
|
|
45983
|
+
const data = await res.json();
|
|
45984
|
+
try {
|
|
45985
|
+
await saveCredentials({
|
|
45986
|
+
auth_kind: "cli_token",
|
|
45987
|
+
server_url: serverUrl,
|
|
45988
|
+
user_id: data.id,
|
|
45989
|
+
email: data.email,
|
|
45990
|
+
org_id: data.org_id,
|
|
45991
|
+
org_role: data.org_role,
|
|
45992
|
+
access_token: token,
|
|
45993
|
+
refresh_token: "",
|
|
45994
|
+
access_expires_at: "9999-12-31T23:59:59.999Z"
|
|
45995
|
+
});
|
|
45996
|
+
} catch (err) {
|
|
45997
|
+
this.context.stderr.write(
|
|
45998
|
+
`keynv: failed to persist credentials: ${err instanceof Error ? err.message : String(err)}
|
|
45999
|
+
`
|
|
46000
|
+
);
|
|
46001
|
+
return 1;
|
|
46002
|
+
}
|
|
46003
|
+
this.context.stdout.write(`logged in as ${data.email} (${data.org_role})
|
|
45224
46004
|
`);
|
|
45225
46005
|
return 0;
|
|
45226
46006
|
}
|
|
@@ -45257,48 +46037,46 @@ var WhoamiCommand = class extends Command {
|
|
|
45257
46037
|
});
|
|
45258
46038
|
json = options_exports.Boolean("--json", false);
|
|
45259
46039
|
async execute() {
|
|
45260
|
-
|
|
45261
|
-
|
|
45262
|
-
|
|
45263
|
-
|
|
45264
|
-
|
|
45265
|
-
|
|
45266
|
-
|
|
45267
|
-
|
|
45268
|
-
this.
|
|
46040
|
+
try {
|
|
46041
|
+
const client = new ApiClient();
|
|
46042
|
+
await client.ensureHydrated();
|
|
46043
|
+
if (!client.isLoggedIn) {
|
|
46044
|
+
this.context.stderr.write("keynv: not logged in. Run `keynv login`.\n");
|
|
46045
|
+
return 1;
|
|
46046
|
+
}
|
|
46047
|
+
const data = await client.request("/v1/whoami");
|
|
46048
|
+
if (this.json) {
|
|
46049
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45269
46050
|
`);
|
|
45270
|
-
|
|
45271
|
-
|
|
45272
|
-
|
|
46051
|
+
return 0;
|
|
46052
|
+
}
|
|
46053
|
+
this.context.stdout.write(`user: ${data.email} (${data.id})
|
|
45273
46054
|
`);
|
|
45274
|
-
|
|
46055
|
+
const orgDisplay = data.org_name ? `${data.org_name} (${data.org_id})` : data.org_id;
|
|
46056
|
+
this.context.stdout.write(`org: ${orgDisplay}
|
|
45275
46057
|
`);
|
|
45276
|
-
|
|
46058
|
+
this.context.stdout.write(`role: ${data.org_role}
|
|
45277
46059
|
`);
|
|
45278
|
-
|
|
45279
|
-
|
|
45280
|
-
|
|
45281
|
-
|
|
45282
|
-
|
|
45283
|
-
|
|
46060
|
+
if (data.memberships.length === 0) {
|
|
46061
|
+
this.context.stdout.write("memberships: (none)\n");
|
|
46062
|
+
} else {
|
|
46063
|
+
this.context.stdout.write("memberships:\n");
|
|
46064
|
+
for (const m of data.memberships) {
|
|
46065
|
+
const proj = m.project_name ? `${m.project_name} (${m.project_id})` : m.project_id;
|
|
46066
|
+
this.context.stdout.write(` ${proj}: ${m.role}
|
|
45284
46067
|
`);
|
|
46068
|
+
}
|
|
45285
46069
|
}
|
|
46070
|
+
return 0;
|
|
46071
|
+
} catch (err) {
|
|
46072
|
+
return handleExecError(this.context.stderr, err);
|
|
45286
46073
|
}
|
|
45287
|
-
return 0;
|
|
45288
46074
|
}
|
|
45289
46075
|
};
|
|
45290
46076
|
|
|
45291
46077
|
// src/commands/member.ts
|
|
45292
46078
|
init_http();
|
|
45293
46079
|
init_format();
|
|
45294
|
-
async function findProjectIdByName(client, name) {
|
|
45295
|
-
const data = await client.request(
|
|
45296
|
-
"/v1/projects"
|
|
45297
|
-
);
|
|
45298
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
45299
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
45300
|
-
return match.id;
|
|
45301
|
-
}
|
|
45302
46080
|
var MemberAddCommand = class extends Command {
|
|
45303
46081
|
static paths = [["member", "add"]];
|
|
45304
46082
|
static usage = Command.Usage({
|
|
@@ -45314,15 +46092,19 @@ var MemberAddCommand = class extends Command {
|
|
|
45314
46092
|
this.context.stderr.write("keynv: --role must be one of lead, developer, reader\n");
|
|
45315
46093
|
return 1;
|
|
45316
46094
|
}
|
|
45317
|
-
|
|
45318
|
-
|
|
45319
|
-
|
|
45320
|
-
|
|
45321
|
-
|
|
45322
|
-
|
|
45323
|
-
|
|
46095
|
+
try {
|
|
46096
|
+
const client = new ApiClient();
|
|
46097
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
46098
|
+
await client.request(`/v1/projects/${projectId2}/members`, {
|
|
46099
|
+
method: "POST",
|
|
46100
|
+
body: { email: this.email, role: role2 }
|
|
46101
|
+
});
|
|
46102
|
+
this.context.stdout.write(`added ${this.email} to ${this.project} as ${role2}
|
|
45324
46103
|
`);
|
|
45325
|
-
|
|
46104
|
+
return 0;
|
|
46105
|
+
} catch (err) {
|
|
46106
|
+
return handleExecError(this.context.stderr, err);
|
|
46107
|
+
}
|
|
45326
46108
|
}
|
|
45327
46109
|
};
|
|
45328
46110
|
var MemberRemoveCommand = class extends Command {
|
|
@@ -45331,21 +46113,25 @@ var MemberRemoveCommand = class extends Command {
|
|
|
45331
46113
|
project = options_exports.String();
|
|
45332
46114
|
email = options_exports.String();
|
|
45333
46115
|
async execute() {
|
|
45334
|
-
|
|
45335
|
-
|
|
45336
|
-
|
|
45337
|
-
|
|
45338
|
-
|
|
45339
|
-
|
|
46116
|
+
try {
|
|
46117
|
+
const client = new ApiClient();
|
|
46118
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
46119
|
+
const members = await client.request(`/v1/projects/${projectId2}/members`);
|
|
46120
|
+
const target = members.members.find((m) => m.email === this.email);
|
|
46121
|
+
if (!target) {
|
|
46122
|
+
this.context.stderr.write(`keynv: ${this.email} is not a member of ${this.project}
|
|
45340
46123
|
`);
|
|
45341
|
-
|
|
45342
|
-
|
|
45343
|
-
|
|
45344
|
-
|
|
45345
|
-
|
|
45346
|
-
|
|
46124
|
+
return 1;
|
|
46125
|
+
}
|
|
46126
|
+
await client.request(`/v1/projects/${projectId2}/members/${target.user_id}`, {
|
|
46127
|
+
method: "DELETE"
|
|
46128
|
+
});
|
|
46129
|
+
this.context.stdout.write(`removed ${this.email} from ${this.project}
|
|
45347
46130
|
`);
|
|
45348
|
-
|
|
46131
|
+
return 0;
|
|
46132
|
+
} catch (err) {
|
|
46133
|
+
return handleExecError(this.context.stderr, err);
|
|
46134
|
+
}
|
|
45349
46135
|
}
|
|
45350
46136
|
};
|
|
45351
46137
|
var MemberListCommand = class extends Command {
|
|
@@ -45354,134 +46140,30 @@ var MemberListCommand = class extends Command {
|
|
|
45354
46140
|
project = options_exports.String();
|
|
45355
46141
|
json = options_exports.Boolean("--json", false);
|
|
45356
46142
|
async execute() {
|
|
45357
|
-
|
|
45358
|
-
|
|
45359
|
-
|
|
45360
|
-
|
|
45361
|
-
this.
|
|
45362
|
-
|
|
45363
|
-
return 0;
|
|
45364
|
-
}
|
|
45365
|
-
if (data.members.length === 0) {
|
|
45366
|
-
this.context.stdout.write("no members\n");
|
|
45367
|
-
return 0;
|
|
45368
|
-
}
|
|
45369
|
-
this.context.stdout.write(
|
|
45370
|
-
`${table(
|
|
45371
|
-
["email", "role", "granted_at"],
|
|
45372
|
-
data.members.map((m) => [m.email, m.role, m.granted_at])
|
|
45373
|
-
)}
|
|
45374
|
-
`
|
|
45375
|
-
);
|
|
45376
|
-
return 0;
|
|
45377
|
-
}
|
|
45378
|
-
};
|
|
45379
|
-
|
|
45380
|
-
// src/commands/project.ts
|
|
45381
|
-
init_http();
|
|
45382
|
-
init_format();
|
|
45383
|
-
var ProjectListCommand = class extends Command {
|
|
45384
|
-
static paths = [["project", "list"]];
|
|
45385
|
-
static usage = Command.Usage({
|
|
45386
|
-
description: "List projects visible to the current user."
|
|
45387
|
-
});
|
|
45388
|
-
json = options_exports.Boolean("--json", false);
|
|
45389
|
-
async execute() {
|
|
45390
|
-
const client = new ApiClient();
|
|
45391
|
-
const data = await client.request("/v1/projects");
|
|
45392
|
-
if (this.json) {
|
|
45393
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45394
|
-
`);
|
|
45395
|
-
return 0;
|
|
45396
|
-
}
|
|
45397
|
-
if (data.projects.length === 0) {
|
|
45398
|
-
this.context.stdout.write("no projects\n");
|
|
45399
|
-
return 0;
|
|
45400
|
-
}
|
|
45401
|
-
this.context.stdout.write(
|
|
45402
|
-
`${table(
|
|
45403
|
-
["name", "id", "created_at"],
|
|
45404
|
-
data.projects.map((p2) => [p2.name, p2.id, p2.created_at])
|
|
45405
|
-
)}
|
|
45406
|
-
`
|
|
45407
|
-
);
|
|
45408
|
-
return 0;
|
|
45409
|
-
}
|
|
45410
|
-
};
|
|
45411
|
-
var ProjectCreateCommand = class extends Command {
|
|
45412
|
-
static paths = [["project", "create"]];
|
|
45413
|
-
static usage = Command.Usage({
|
|
45414
|
-
description: "Create a new project with one or more environments.",
|
|
45415
|
-
examples: [["Two envs", "$0 project create billing --env dev --env prod:production:approval"]]
|
|
45416
|
-
});
|
|
45417
|
-
name = options_exports.String();
|
|
45418
|
-
envs = options_exports.Array("--env", {
|
|
45419
|
-
description: 'Environment spec: name[:tier[:approval]]. Tier \u2208 {production,non-production}; "approval" sets require_approval=true.'
|
|
45420
|
-
});
|
|
45421
|
-
async execute() {
|
|
45422
|
-
const envs = (this.envs ?? ["dev"]).map((spec) => {
|
|
45423
|
-
const [name, tier, approval] = spec.split(":");
|
|
45424
|
-
return {
|
|
45425
|
-
name: name ?? "",
|
|
45426
|
-
tier: tier ?? "non-production",
|
|
45427
|
-
require_approval: approval === "approval"
|
|
45428
|
-
};
|
|
45429
|
-
});
|
|
45430
|
-
const client = new ApiClient();
|
|
45431
|
-
const result = await client.request("/v1/projects", {
|
|
45432
|
-
method: "POST",
|
|
45433
|
-
body: { name: this.name, environments: envs }
|
|
45434
|
-
});
|
|
45435
|
-
this.context.stdout.write(`created project ${result.name} (${result.id})
|
|
46143
|
+
try {
|
|
46144
|
+
const client = new ApiClient();
|
|
46145
|
+
const projectId2 = await resolveProjectId(client, this.project);
|
|
46146
|
+
const data = await client.request(`/v1/projects/${projectId2}/members`);
|
|
46147
|
+
if (this.json) {
|
|
46148
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45436
46149
|
`);
|
|
45437
|
-
|
|
46150
|
+
return 0;
|
|
46151
|
+
}
|
|
46152
|
+
if (data.members.length === 0) {
|
|
46153
|
+
this.context.stdout.write("no members\n");
|
|
46154
|
+
return 0;
|
|
46155
|
+
}
|
|
45438
46156
|
this.context.stdout.write(
|
|
45439
|
-
|
|
46157
|
+
`${table(
|
|
46158
|
+
["email", "role", "granted_at"],
|
|
46159
|
+
data.members.map((m) => [m.email, m.role, m.granted_at])
|
|
46160
|
+
)}
|
|
45440
46161
|
`
|
|
45441
46162
|
);
|
|
45442
|
-
}
|
|
45443
|
-
return 0;
|
|
45444
|
-
}
|
|
45445
|
-
};
|
|
45446
|
-
var ProjectDescribeCommand = class extends Command {
|
|
45447
|
-
static paths = [["project", "describe"]];
|
|
45448
|
-
static usage = Command.Usage({ description: "Show metadata for one project." });
|
|
45449
|
-
id = options_exports.String();
|
|
45450
|
-
json = options_exports.Boolean("--json", false);
|
|
45451
|
-
async execute() {
|
|
45452
|
-
const client = new ApiClient();
|
|
45453
|
-
const data = await client.request(`/v1/projects/${this.id}`);
|
|
45454
|
-
if (this.json) {
|
|
45455
|
-
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45456
|
-
`);
|
|
45457
46163
|
return 0;
|
|
46164
|
+
} catch (err) {
|
|
46165
|
+
return handleExecError(this.context.stderr, err);
|
|
45458
46166
|
}
|
|
45459
|
-
this.context.stdout.write(`project: ${data.name} (${data.id})
|
|
45460
|
-
`);
|
|
45461
|
-
for (const e2 of data.environments) {
|
|
45462
|
-
this.context.stdout.write(
|
|
45463
|
-
` env: ${e2.name} (tier=${e2.tier}, approval=${e2.require_approval})
|
|
45464
|
-
`
|
|
45465
|
-
);
|
|
45466
|
-
}
|
|
45467
|
-
return 0;
|
|
45468
|
-
}
|
|
45469
|
-
};
|
|
45470
|
-
var ProjectDeleteCommand = class extends Command {
|
|
45471
|
-
static paths = [["project", "delete"]];
|
|
45472
|
-
static usage = Command.Usage({ description: "Soft-delete a project." });
|
|
45473
|
-
id = options_exports.String();
|
|
45474
|
-
force = options_exports.Boolean("--force", false);
|
|
45475
|
-
async execute() {
|
|
45476
|
-
if (!this.force) {
|
|
45477
|
-
this.context.stderr.write("keynv: refusing to delete without --force\n");
|
|
45478
|
-
return 2;
|
|
45479
|
-
}
|
|
45480
|
-
const client = new ApiClient();
|
|
45481
|
-
await client.request(`/v1/projects/${this.id}`, { method: "DELETE" });
|
|
45482
|
-
this.context.stdout.write(`deleted project ${this.id}
|
|
45483
|
-
`);
|
|
45484
|
-
return 0;
|
|
45485
46167
|
}
|
|
45486
46168
|
};
|
|
45487
46169
|
var RedactCommand = class extends Command {
|
|
@@ -45526,11 +46208,9 @@ var RedactStreamCommand = class extends Command {
|
|
|
45526
46208
|
static usage = Command.Usage({
|
|
45527
46209
|
description: "Stream stdin through the line-buffered redactor to stdout.",
|
|
45528
46210
|
details: `
|
|
45529
|
-
|
|
45530
|
-
|
|
45531
|
-
|
|
45532
|
-
line structure. Multi-line patterns are NOT applied here (see the
|
|
45533
|
-
streaming-mode limitation in @keynv/redactor).
|
|
46211
|
+
Pipe any output through the line-buffered redactor to stdout.
|
|
46212
|
+
Preserves original line structure. Multi-line patterns are NOT applied
|
|
46213
|
+
here (see the streaming-mode limitation in the redactor package).
|
|
45534
46214
|
`
|
|
45535
46215
|
});
|
|
45536
46216
|
async execute() {
|
|
@@ -45545,18 +46225,13 @@ streaming-mode limitation in @keynv/redactor).
|
|
|
45545
46225
|
// src/commands/secret.ts
|
|
45546
46226
|
init_dist();
|
|
45547
46227
|
init_http();
|
|
46228
|
+
init_secret();
|
|
45548
46229
|
init_format();
|
|
45549
|
-
init_tty();
|
|
45550
46230
|
init_cancel();
|
|
45551
46231
|
init_pickProject();
|
|
45552
46232
|
init_pickSecret();
|
|
45553
|
-
|
|
45554
|
-
|
|
45555
|
-
const data = await client.request("/v1/projects");
|
|
45556
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
45557
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
45558
|
-
return match.id;
|
|
45559
|
-
}
|
|
46233
|
+
init_tty();
|
|
46234
|
+
var ALIAS_FORMAT_HINT = "Format: @<project>.<env>.<KEY> (project/env: lowercase kebab-case; key: letters, digits, _ or -)";
|
|
45560
46235
|
function missingAlias(stderr) {
|
|
45561
46236
|
stderr.write("keynv: missing <alias> (TTY required for interactive prompt).\n");
|
|
45562
46237
|
return 1;
|
|
@@ -45587,45 +46262,50 @@ var SecretCreateCommand = class extends Command {
|
|
|
45587
46262
|
});
|
|
45588
46263
|
stdin = options_exports.Boolean("--stdin", false);
|
|
45589
46264
|
async execute() {
|
|
45590
|
-
|
|
45591
|
-
|
|
45592
|
-
|
|
45593
|
-
|
|
45594
|
-
|
|
45595
|
-
if (!
|
|
45596
|
-
|
|
45597
|
-
|
|
45598
|
-
|
|
45599
|
-
|
|
45600
|
-
|
|
45601
|
-
|
|
45602
|
-
|
|
45603
|
-
|
|
46265
|
+
try {
|
|
46266
|
+
const client = new ApiClient();
|
|
46267
|
+
await client.ensureHydrated();
|
|
46268
|
+
let alias2 = this.alias;
|
|
46269
|
+
let value = this.value;
|
|
46270
|
+
if (!alias2) {
|
|
46271
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46272
|
+
try {
|
|
46273
|
+
const built = await promptNewSecret(client);
|
|
46274
|
+
if (!built) return 1;
|
|
46275
|
+
alias2 = built.alias;
|
|
46276
|
+
value = built.value;
|
|
46277
|
+
} catch (err) {
|
|
46278
|
+
if (err instanceof UserCancelled) return 130;
|
|
46279
|
+
throw err;
|
|
46280
|
+
}
|
|
45604
46281
|
}
|
|
45605
|
-
|
|
45606
|
-
|
|
45607
|
-
|
|
45608
|
-
|
|
46282
|
+
const parsed = parseAlias(alias2);
|
|
46283
|
+
if (!parsed) {
|
|
46284
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46285
|
+
${ALIAS_FORMAT_HINT}
|
|
45609
46286
|
`);
|
|
45610
|
-
|
|
45611
|
-
}
|
|
45612
|
-
if (this.stdin) {
|
|
45613
|
-
const chunks = [];
|
|
45614
|
-
for await (const chunk of this.context.stdin) {
|
|
45615
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46287
|
+
return 1;
|
|
45616
46288
|
}
|
|
45617
|
-
|
|
45618
|
-
|
|
45619
|
-
|
|
45620
|
-
|
|
45621
|
-
|
|
45622
|
-
|
|
45623
|
-
|
|
45624
|
-
|
|
45625
|
-
|
|
45626
|
-
|
|
46289
|
+
if (this.stdin) {
|
|
46290
|
+
const chunks = [];
|
|
46291
|
+
for await (const chunk of this.context.stdin) {
|
|
46292
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46293
|
+
}
|
|
46294
|
+
value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
46295
|
+
} else if (value === void 0) {
|
|
46296
|
+
value = await promptHidden("value: ");
|
|
46297
|
+
}
|
|
46298
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46299
|
+
await client.request(`/v1/projects/${projectId2}/secrets`, {
|
|
46300
|
+
method: "POST",
|
|
46301
|
+
body: { env: parsed.environment, key: parsed.key, value }
|
|
46302
|
+
});
|
|
46303
|
+
this.context.stdout.write(`created ${parsed.literal}
|
|
45627
46304
|
`);
|
|
45628
|
-
|
|
46305
|
+
return 0;
|
|
46306
|
+
} catch (err) {
|
|
46307
|
+
return handleExecError(this.context.stderr, err);
|
|
46308
|
+
}
|
|
45629
46309
|
}
|
|
45630
46310
|
};
|
|
45631
46311
|
var SecretGetCommand = class extends Command {
|
|
@@ -45636,39 +46316,44 @@ var SecretGetCommand = class extends Command {
|
|
|
45636
46316
|
alias = options_exports.String({ required: false });
|
|
45637
46317
|
json = options_exports.Boolean("--json", false);
|
|
45638
46318
|
async execute() {
|
|
45639
|
-
|
|
45640
|
-
|
|
45641
|
-
|
|
45642
|
-
|
|
45643
|
-
if (!
|
|
45644
|
-
|
|
45645
|
-
|
|
45646
|
-
|
|
45647
|
-
|
|
45648
|
-
|
|
46319
|
+
try {
|
|
46320
|
+
const client = new ApiClient();
|
|
46321
|
+
await client.ensureHydrated();
|
|
46322
|
+
let alias2 = this.alias;
|
|
46323
|
+
if (!alias2) {
|
|
46324
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46325
|
+
try {
|
|
46326
|
+
alias2 = await pickAliasInteractive(client) ?? void 0;
|
|
46327
|
+
} catch (err) {
|
|
46328
|
+
if (err instanceof UserCancelled) return 130;
|
|
46329
|
+
throw err;
|
|
46330
|
+
}
|
|
46331
|
+
if (!alias2) return 1;
|
|
45649
46332
|
}
|
|
45650
|
-
|
|
45651
|
-
|
|
45652
|
-
|
|
45653
|
-
|
|
45654
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46333
|
+
const parsed = parseAlias(alias2);
|
|
46334
|
+
if (!parsed) {
|
|
46335
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46336
|
+
${ALIAS_FORMAT_HINT}
|
|
45655
46337
|
`);
|
|
45656
|
-
|
|
45657
|
-
|
|
45658
|
-
|
|
45659
|
-
|
|
45660
|
-
|
|
45661
|
-
);
|
|
45662
|
-
if (this.json) {
|
|
45663
|
-
this.context.stdout.write(
|
|
45664
|
-
`${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
|
|
45665
|
-
`
|
|
46338
|
+
return 1;
|
|
46339
|
+
}
|
|
46340
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46341
|
+
const data = await client.request(
|
|
46342
|
+
`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`
|
|
45666
46343
|
);
|
|
45667
|
-
|
|
45668
|
-
|
|
46344
|
+
if (this.json) {
|
|
46345
|
+
this.context.stdout.write(
|
|
46346
|
+
`${JSON.stringify({ alias: data.alias, version: data.version, value: data.value })}
|
|
46347
|
+
`
|
|
46348
|
+
);
|
|
46349
|
+
} else {
|
|
46350
|
+
this.context.stdout.write(`${data.value}
|
|
45669
46351
|
`);
|
|
46352
|
+
}
|
|
46353
|
+
return 0;
|
|
46354
|
+
} catch (err) {
|
|
46355
|
+
return handleExecError(this.context.stderr, err);
|
|
45670
46356
|
}
|
|
45671
|
-
return 0;
|
|
45672
46357
|
}
|
|
45673
46358
|
};
|
|
45674
46359
|
var SecretListCommand = class extends Command {
|
|
@@ -45679,42 +46364,47 @@ var SecretListCommand = class extends Command {
|
|
|
45679
46364
|
project = options_exports.String({ required: false });
|
|
45680
46365
|
json = options_exports.Boolean("--json", false);
|
|
45681
46366
|
async execute() {
|
|
45682
|
-
|
|
45683
|
-
|
|
45684
|
-
|
|
45685
|
-
|
|
45686
|
-
if (!
|
|
45687
|
-
|
|
45688
|
-
|
|
45689
|
-
|
|
45690
|
-
|
|
45691
|
-
|
|
45692
|
-
|
|
45693
|
-
|
|
45694
|
-
|
|
45695
|
-
|
|
45696
|
-
|
|
46367
|
+
try {
|
|
46368
|
+
const client = new ApiClient();
|
|
46369
|
+
await client.ensureHydrated();
|
|
46370
|
+
let projectName = this.project;
|
|
46371
|
+
if (!projectName) {
|
|
46372
|
+
if (!isInteractive()) {
|
|
46373
|
+
this.context.stderr.write("keynv: missing <project>.\n");
|
|
46374
|
+
return 1;
|
|
46375
|
+
}
|
|
46376
|
+
try {
|
|
46377
|
+
const picked = await pickProject(client, "Project");
|
|
46378
|
+
if (!picked) return 1;
|
|
46379
|
+
projectName = picked.name;
|
|
46380
|
+
} catch (err) {
|
|
46381
|
+
if (err instanceof UserCancelled) return 130;
|
|
46382
|
+
throw err;
|
|
46383
|
+
}
|
|
45697
46384
|
}
|
|
45698
|
-
|
|
45699
|
-
|
|
45700
|
-
|
|
45701
|
-
|
|
45702
|
-
|
|
46385
|
+
const resolvedProjectName = projectName.startsWith("@") ? projectName.slice(1).split(".")[0] ?? projectName : projectName;
|
|
46386
|
+
const projectId2 = await resolveProjectId(client, resolvedProjectName);
|
|
46387
|
+
const data = await client.request(`/v1/projects/${projectId2}/secrets`);
|
|
46388
|
+
if (this.json) {
|
|
46389
|
+
this.context.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
45703
46390
|
`);
|
|
46391
|
+
return 0;
|
|
46392
|
+
}
|
|
46393
|
+
if (data.secrets.length === 0) {
|
|
46394
|
+
this.context.stdout.write("no secrets\n");
|
|
46395
|
+
return 0;
|
|
46396
|
+
}
|
|
46397
|
+
this.context.stdout.write(
|
|
46398
|
+
`${table(
|
|
46399
|
+
["alias", "version", "created_at"],
|
|
46400
|
+
data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
|
|
46401
|
+
)}
|
|
46402
|
+
`
|
|
46403
|
+
);
|
|
45704
46404
|
return 0;
|
|
46405
|
+
} catch (err) {
|
|
46406
|
+
return handleExecError(this.context.stderr, err);
|
|
45705
46407
|
}
|
|
45706
|
-
if (data.secrets.length === 0) {
|
|
45707
|
-
this.context.stdout.write("no secrets\n");
|
|
45708
|
-
return 0;
|
|
45709
|
-
}
|
|
45710
|
-
this.context.stdout.write(
|
|
45711
|
-
`${table(
|
|
45712
|
-
["alias", "version", "created_at"],
|
|
45713
|
-
data.secrets.map((s) => [s.alias, String(s.version), s.created_at])
|
|
45714
|
-
)}
|
|
45715
|
-
`
|
|
45716
|
-
);
|
|
45717
|
-
return 0;
|
|
45718
46408
|
}
|
|
45719
46409
|
};
|
|
45720
46410
|
var SecretRotateCommand = class extends Command {
|
|
@@ -45724,45 +46414,50 @@ var SecretRotateCommand = class extends Command {
|
|
|
45724
46414
|
value = options_exports.String("--value");
|
|
45725
46415
|
stdin = options_exports.Boolean("--stdin", false);
|
|
45726
46416
|
async execute() {
|
|
45727
|
-
|
|
45728
|
-
|
|
45729
|
-
|
|
45730
|
-
|
|
45731
|
-
if (!
|
|
45732
|
-
|
|
45733
|
-
|
|
45734
|
-
|
|
45735
|
-
|
|
45736
|
-
|
|
46417
|
+
try {
|
|
46418
|
+
const client = new ApiClient();
|
|
46419
|
+
await client.ensureHydrated();
|
|
46420
|
+
let alias2 = this.alias;
|
|
46421
|
+
if (!alias2) {
|
|
46422
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46423
|
+
try {
|
|
46424
|
+
alias2 = await pickAliasInteractive(client) ?? void 0;
|
|
46425
|
+
} catch (err) {
|
|
46426
|
+
if (err instanceof UserCancelled) return 130;
|
|
46427
|
+
throw err;
|
|
46428
|
+
}
|
|
46429
|
+
if (!alias2) return 1;
|
|
45737
46430
|
}
|
|
45738
|
-
|
|
45739
|
-
|
|
45740
|
-
|
|
45741
|
-
|
|
45742
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46431
|
+
const parsed = parseAlias(alias2);
|
|
46432
|
+
if (!parsed) {
|
|
46433
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46434
|
+
${ALIAS_FORMAT_HINT}
|
|
45743
46435
|
`);
|
|
45744
|
-
|
|
45745
|
-
}
|
|
45746
|
-
let value;
|
|
45747
|
-
if (this.stdin) {
|
|
45748
|
-
const chunks = [];
|
|
45749
|
-
for await (const chunk of this.context.stdin) {
|
|
45750
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46436
|
+
return 1;
|
|
45751
46437
|
}
|
|
45752
|
-
value
|
|
45753
|
-
|
|
45754
|
-
|
|
45755
|
-
|
|
45756
|
-
|
|
45757
|
-
|
|
45758
|
-
|
|
45759
|
-
|
|
45760
|
-
|
|
45761
|
-
|
|
45762
|
-
|
|
45763
|
-
|
|
46438
|
+
let value;
|
|
46439
|
+
if (this.stdin) {
|
|
46440
|
+
const chunks = [];
|
|
46441
|
+
for await (const chunk of this.context.stdin) {
|
|
46442
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
46443
|
+
}
|
|
46444
|
+
value = Buffer.concat(chunks).toString("utf8").replace(/\n$/, "");
|
|
46445
|
+
} else if (this.value !== void 0) {
|
|
46446
|
+
value = this.value;
|
|
46447
|
+
} else {
|
|
46448
|
+
value = await promptHidden("new value: ");
|
|
46449
|
+
}
|
|
46450
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46451
|
+
const data = await client.request(
|
|
46452
|
+
`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}/rotate`,
|
|
46453
|
+
{ method: "POST", body: { new_value: value } }
|
|
46454
|
+
);
|
|
46455
|
+
this.context.stdout.write(`rotated ${data.alias} \u2192 v${data.version}
|
|
45764
46456
|
`);
|
|
45765
|
-
|
|
46457
|
+
return 0;
|
|
46458
|
+
} catch (err) {
|
|
46459
|
+
return handleExecError(this.context.stderr, err);
|
|
46460
|
+
}
|
|
45766
46461
|
}
|
|
45767
46462
|
};
|
|
45768
46463
|
var SecretDeleteCommand = class extends Command {
|
|
@@ -45770,31 +46465,170 @@ var SecretDeleteCommand = class extends Command {
|
|
|
45770
46465
|
static usage = Command.Usage({ description: "Soft-delete a secret." });
|
|
45771
46466
|
alias = options_exports.String({ required: false });
|
|
45772
46467
|
async execute() {
|
|
45773
|
-
|
|
45774
|
-
|
|
45775
|
-
|
|
45776
|
-
|
|
45777
|
-
if (!
|
|
45778
|
-
|
|
45779
|
-
|
|
45780
|
-
|
|
45781
|
-
|
|
45782
|
-
|
|
46468
|
+
try {
|
|
46469
|
+
const client = new ApiClient();
|
|
46470
|
+
await client.ensureHydrated();
|
|
46471
|
+
let alias2 = this.alias;
|
|
46472
|
+
if (!alias2) {
|
|
46473
|
+
if (!isInteractive()) return missingAlias(this.context.stderr);
|
|
46474
|
+
try {
|
|
46475
|
+
alias2 = await pickAliasInteractive(client) ?? void 0;
|
|
46476
|
+
} catch (err) {
|
|
46477
|
+
if (err instanceof UserCancelled) return 130;
|
|
46478
|
+
throw err;
|
|
46479
|
+
}
|
|
46480
|
+
if (!alias2) return 1;
|
|
45783
46481
|
}
|
|
45784
|
-
|
|
45785
|
-
|
|
45786
|
-
|
|
45787
|
-
|
|
45788
|
-
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46482
|
+
const parsed = parseAlias(alias2);
|
|
46483
|
+
if (!parsed) {
|
|
46484
|
+
this.context.stderr.write(`keynv: invalid alias '${alias2}'.
|
|
46485
|
+
${ALIAS_FORMAT_HINT}
|
|
45789
46486
|
`);
|
|
45790
|
-
|
|
46487
|
+
return 1;
|
|
46488
|
+
}
|
|
46489
|
+
const projectId2 = await resolveProjectId(client, parsed.project);
|
|
46490
|
+
await client.request(`/v1/projects/${projectId2}/secrets/${parsed.environment}/${parsed.key}`, {
|
|
46491
|
+
method: "DELETE"
|
|
46492
|
+
});
|
|
46493
|
+
this.context.stdout.write(`deleted ${parsed.literal}
|
|
46494
|
+
`);
|
|
46495
|
+
return 0;
|
|
46496
|
+
} catch (err) {
|
|
46497
|
+
return handleExecError(this.context.stderr, err);
|
|
45791
46498
|
}
|
|
45792
|
-
|
|
45793
|
-
|
|
45794
|
-
|
|
45795
|
-
|
|
45796
|
-
|
|
46499
|
+
}
|
|
46500
|
+
};
|
|
46501
|
+
function generateSecret(bytes = 32) {
|
|
46502
|
+
return randomBytes(bytes).toString("base64");
|
|
46503
|
+
}
|
|
46504
|
+
var ServerInitCommand = class extends Command {
|
|
46505
|
+
static paths = [["server", "init"]];
|
|
46506
|
+
static usage = Command.Usage({
|
|
46507
|
+
description: "Generate secrets and deployment files for a new keynv server.",
|
|
46508
|
+
details: `
|
|
46509
|
+
Interactive onboarding wizard that generates the configuration
|
|
46510
|
+
a fresh keynv server needs: JWT signing secret, master encryption
|
|
46511
|
+
key, and deployment templates.
|
|
46512
|
+
|
|
46513
|
+
Outputs:
|
|
46514
|
+
\u2022 A .keynv-server.env file you feed into your deployment
|
|
46515
|
+
(Docker Compose, Coolify, or bare-metal).
|
|
46516
|
+
\u2022 Optionally a docker-compose.yml in the current directory.
|
|
46517
|
+
|
|
46518
|
+
Non-interactive with --yes to skip prompts.
|
|
46519
|
+
`,
|
|
46520
|
+
examples: [
|
|
46521
|
+
["Interactive wizard", "$0 server init"],
|
|
46522
|
+
["Non-interactive (CI/scripts)", "$0 server init --yes"],
|
|
46523
|
+
["Docker Compose output", "$0 server init --compose"],
|
|
46524
|
+
["Custom output path", "$0 server init --out ./config/env"]
|
|
46525
|
+
]
|
|
46526
|
+
});
|
|
46527
|
+
yes = options_exports.Boolean("--yes", false, {
|
|
46528
|
+
description: "Skip all prompts (non-interactive)."
|
|
46529
|
+
});
|
|
46530
|
+
compose = options_exports.Boolean("--compose", false, {
|
|
46531
|
+
description: "Also write a docker-compose.yml to the current directory."
|
|
46532
|
+
});
|
|
46533
|
+
out = options_exports.String("--out", {
|
|
46534
|
+
description: "Write server env file to this path (default: .keynv-server.env)."
|
|
46535
|
+
});
|
|
46536
|
+
async execute() {
|
|
46537
|
+
const jwtSecret = generateSecret(48);
|
|
46538
|
+
const masterKey = generateSecret(32);
|
|
46539
|
+
const webSessionSecret = generateSecret(48);
|
|
46540
|
+
const outPath = this.out ?? ".keynv-server.env";
|
|
46541
|
+
const contents = [
|
|
46542
|
+
"# Generated by `keynv server init`.",
|
|
46543
|
+
"# Keep this file secret \u2014 it contains cryptographic key material.",
|
|
46544
|
+
`KEYNV_JWT_SECRET=${jwtSecret}`,
|
|
46545
|
+
`KEYNV_MASTER_KEY=${masterKey}`,
|
|
46546
|
+
"",
|
|
46547
|
+
"# Web dashboard session secret (required for Next.js JWE session tokens).",
|
|
46548
|
+
`KEYNV_WEB_SESSION_SECRET=${webSessionSecret}`,
|
|
46549
|
+
"",
|
|
46550
|
+
"# Server configuration (uncomment and adjust):",
|
|
46551
|
+
"# KEYNV_PORT=8080",
|
|
46552
|
+
"# KEYNV_WEB_URL=https://keynv.example.com",
|
|
46553
|
+
"# KEYNV_PUBLIC_REGISTRATION=false",
|
|
46554
|
+
"# KEYNV_DB_PATH=./keynv.db",
|
|
46555
|
+
"# KEYNV_MASTER_KEY_FILE=./master.key",
|
|
46556
|
+
"# KEYNV_RATE_LIMIT_PER_MINUTE=120",
|
|
46557
|
+
"# KEYNV_ARGON2_MEMORY_KIB=46080",
|
|
46558
|
+
"# KEYNV_ARGON2_TIME_COST=3"
|
|
46559
|
+
].join("\n");
|
|
46560
|
+
const { writeFileSync: writeFileSync4 } = await import('fs');
|
|
46561
|
+
writeFileSync4(outPath, `${contents}
|
|
46562
|
+
`);
|
|
46563
|
+
this.context.stdout.write(`Wrote ${outPath}
|
|
46564
|
+
|
|
45797
46565
|
`);
|
|
46566
|
+
this.context.stdout.write("Generated secrets:\n");
|
|
46567
|
+
this.context.stdout.write(
|
|
46568
|
+
` KEYNV_JWT_SECRET ${jwtSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
46569
|
+
`
|
|
46570
|
+
);
|
|
46571
|
+
this.context.stdout.write(
|
|
46572
|
+
` KEYNV_MASTER_KEY ${masterKey.slice(0, 8)}... (base64, 32 bytes)
|
|
46573
|
+
`
|
|
46574
|
+
);
|
|
46575
|
+
this.context.stdout.write(
|
|
46576
|
+
` KEYNV_WEB_SESSION_SECRET ${webSessionSecret.slice(0, 8)}... (base64, 48 bytes)
|
|
46577
|
+
`
|
|
46578
|
+
);
|
|
46579
|
+
this.context.stdout.write("\nNext steps:\n");
|
|
46580
|
+
this.context.stdout.write(` 1. Feed ${outPath} into your deployment:
|
|
46581
|
+
`);
|
|
46582
|
+
this.context.stdout.write(" docker compose --env-file .keynv-server.env up -d\n");
|
|
46583
|
+
this.context.stdout.write(" or load it into Coolify as environment variables.\n");
|
|
46584
|
+
this.context.stdout.write(" 2. Bootstrap the first owner:\n");
|
|
46585
|
+
this.context.stdout.write(
|
|
46586
|
+
" docker exec -it <server-container> node dist/bootstrap.js \\\n"
|
|
46587
|
+
);
|
|
46588
|
+
this.context.stdout.write(" --owner-email alice@example.com \\\n");
|
|
46589
|
+
this.context.stdout.write(' --owner-password "<a long random password>" \\\n');
|
|
46590
|
+
this.context.stdout.write(' --org-name "Acme Inc"\n');
|
|
46591
|
+
this.context.stdout.write(" 3. Install the CLI and log in:\n");
|
|
46592
|
+
this.context.stdout.write(" npm install -g @keynv/cli\n");
|
|
46593
|
+
this.context.stdout.write(" keynv login --server https://keynv.example.com\n");
|
|
46594
|
+
this.context.stdout.write(" 4. Onboard your first project:\n");
|
|
46595
|
+
this.context.stdout.write(" cd your-project && keynv init\n");
|
|
46596
|
+
if (this.compose) {
|
|
46597
|
+
const composeContent = `# keynv server + Litestream sidecar
|
|
46598
|
+
# Generated by \`keynv server init\`.
|
|
46599
|
+
# Load with: docker compose --env-file .keynv-server.env up -d
|
|
46600
|
+
|
|
46601
|
+
services:
|
|
46602
|
+
server:
|
|
46603
|
+
image: ghcr.io/keynv-labs/keynv-server:latest
|
|
46604
|
+
restart: unless-stopped
|
|
46605
|
+
ports:
|
|
46606
|
+
- '8080:8080'
|
|
46607
|
+
env_file:
|
|
46608
|
+
- .keynv-server.env
|
|
46609
|
+
volumes:
|
|
46610
|
+
- keynv-data:/data
|
|
46611
|
+
|
|
46612
|
+
litestream:
|
|
46613
|
+
image: litestream/litestream:latest
|
|
46614
|
+
restart: unless-stopped
|
|
46615
|
+
volumes:
|
|
46616
|
+
- keynv-data:/data
|
|
46617
|
+
command: replicate
|
|
46618
|
+
environment:
|
|
46619
|
+
LITESTREAM_DB_PATH: /data/keynv.db
|
|
46620
|
+
LITESTREAM_REPLICA_URL: s3://your-bucket/keynv
|
|
46621
|
+
AWS_ACCESS_KEY_ID: \${LITESTREAM_AWS_ACCESS_KEY_ID}
|
|
46622
|
+
AWS_SECRET_ACCESS_KEY: \${LITESTREAM_AWS_SECRET_ACCESS_KEY}
|
|
46623
|
+
|
|
46624
|
+
volumes:
|
|
46625
|
+
keynv-data:
|
|
46626
|
+
`;
|
|
46627
|
+
writeFileSync4("docker-compose.yml", composeContent);
|
|
46628
|
+
this.context.stdout.write(
|
|
46629
|
+
"\nWrote docker-compose.yml (update Litestream S3 config before deploying).\n"
|
|
46630
|
+
);
|
|
46631
|
+
}
|
|
45798
46632
|
return 0;
|
|
45799
46633
|
}
|
|
45800
46634
|
};
|
|
@@ -46150,18 +46984,9 @@ var TESTERS = [
|
|
|
46150
46984
|
sshTester,
|
|
46151
46985
|
httpTester
|
|
46152
46986
|
];
|
|
46153
|
-
function findTester(type) {
|
|
46154
|
-
return TESTERS.find((t) => t.type === type) ?? null;
|
|
46155
|
-
}
|
|
46156
46987
|
|
|
46157
46988
|
// src/commands/test.ts
|
|
46158
46989
|
init_http();
|
|
46159
|
-
async function findProjectIdByName3(client, name) {
|
|
46160
|
-
const data = await client.request("/v1/projects");
|
|
46161
|
-
const match = data.projects.find((p2) => p2.name === name);
|
|
46162
|
-
if (!match) throw new Error(`unknown project: ${name}`);
|
|
46163
|
-
return match.id;
|
|
46164
|
-
}
|
|
46165
46990
|
function parseTargets(specs) {
|
|
46166
46991
|
const out = {};
|
|
46167
46992
|
for (const spec of specs) {
|
|
@@ -46215,12 +47040,17 @@ the error message.
|
|
|
46215
47040
|
);
|
|
46216
47041
|
return 2;
|
|
46217
47042
|
}
|
|
46218
|
-
const tester =
|
|
47043
|
+
const tester = TESTERS.find((t) => t.type === this.as);
|
|
46219
47044
|
if (!tester) {
|
|
46220
47045
|
this.context.stderr.write(`keynv: unknown tester '${this.as}'
|
|
46221
47046
|
`);
|
|
46222
47047
|
return 1;
|
|
46223
47048
|
}
|
|
47049
|
+
const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
|
|
47050
|
+
if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
|
|
47051
|
+
this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
|
|
47052
|
+
return 2;
|
|
47053
|
+
}
|
|
46224
47054
|
let target;
|
|
46225
47055
|
try {
|
|
46226
47056
|
target = parseTargets(this.targets ?? []);
|
|
@@ -46237,7 +47067,7 @@ the error message.
|
|
|
46237
47067
|
}
|
|
46238
47068
|
let value;
|
|
46239
47069
|
try {
|
|
46240
|
-
const projectId2 = await
|
|
47070
|
+
const projectId2 = await resolveProjectId(client, parsedAlias.project);
|
|
46241
47071
|
const data = await client.request(
|
|
46242
47072
|
`/v1/projects/${projectId2}/secrets/${parsedAlias.environment}/${parsedAlias.key}`
|
|
46243
47073
|
);
|
|
@@ -46248,11 +47078,6 @@ the error message.
|
|
|
46248
47078
|
`);
|
|
46249
47079
|
return 1;
|
|
46250
47080
|
}
|
|
46251
|
-
const timeoutMs = this.timeout ? Number.parseInt(this.timeout, 10) * 1e3 : void 0;
|
|
46252
|
-
if (this.timeout && (timeoutMs === void 0 || Number.isNaN(timeoutMs))) {
|
|
46253
|
-
this.context.stderr.write("keynv: invalid --timeout (expected integer seconds)\n");
|
|
46254
|
-
return 2;
|
|
46255
|
-
}
|
|
46256
47081
|
const result = await runTest({
|
|
46257
47082
|
tester,
|
|
46258
47083
|
secret: { alias: parsedAlias.literal, value },
|
|
@@ -46294,25 +47119,6 @@ the error message.
|
|
|
46294
47119
|
}
|
|
46295
47120
|
};
|
|
46296
47121
|
|
|
46297
|
-
// src/commands/ui.ts
|
|
46298
|
-
init_menu();
|
|
46299
|
-
init_tty();
|
|
46300
|
-
var UICommand = class extends Command {
|
|
46301
|
-
static paths = [["ui"]];
|
|
46302
|
-
static usage = Command.Usage({
|
|
46303
|
-
description: "Open the interactive menu (also runs by default when no args are given)."
|
|
46304
|
-
});
|
|
46305
|
-
async execute() {
|
|
46306
|
-
if (!isInteractive()) {
|
|
46307
|
-
this.context.stdout.write(
|
|
46308
|
-
"keynv \u2014 AI-safe secrets management.\nRun `keynv --help` for the full command list, or run `keynv` in an interactive\nterminal to open the menu.\n"
|
|
46309
|
-
);
|
|
46310
|
-
return 0;
|
|
46311
|
-
}
|
|
46312
|
-
return runMenu();
|
|
46313
|
-
}
|
|
46314
|
-
};
|
|
46315
|
-
|
|
46316
47122
|
// src/index.ts
|
|
46317
47123
|
init_format();
|
|
46318
47124
|
init_version();
|
|
@@ -46346,7 +47152,7 @@ cli.register(InitCommand);
|
|
|
46346
47152
|
cli.register(RedactCommand);
|
|
46347
47153
|
cli.register(RedactStreamCommand);
|
|
46348
47154
|
cli.register(TestCommand);
|
|
46349
|
-
cli.register(
|
|
47155
|
+
cli.register(ServerInitCommand);
|
|
46350
47156
|
var argv = process.argv.slice(2);
|
|
46351
47157
|
if (argv.length === 0) {
|
|
46352
47158
|
const { runMenu: runMenu2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));
|