@kuckit/cli 5.0.1 ā 6.0.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/bin.js +416 -26
- package/dist/bin.js.map +1 -1
- package/package.json +3 -2
package/dist/bin.js
CHANGED
|
@@ -10,6 +10,7 @@ import { dirname as dirname$1, join as join$1 } from "path";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { accessSync as accessSync$1, constants as constants$2 } from "fs";
|
|
12
12
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
13
|
+
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
|
|
13
14
|
|
|
14
15
|
//#region src/commands/doctor.ts
|
|
15
16
|
const CONFIG_FILES$1 = [
|
|
@@ -643,7 +644,7 @@ async function dbStudio(options) {
|
|
|
643
644
|
|
|
644
645
|
//#endregion
|
|
645
646
|
//#region src/lib/credentials.ts
|
|
646
|
-
const DEFAULT_SERVER_URL = "https://
|
|
647
|
+
const DEFAULT_SERVER_URL = "https://dev-app-nyh7i73bea-uc.a.run.app/";
|
|
647
648
|
const CONFIG_DIR = join(homedir(), ".kuckit");
|
|
648
649
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
649
650
|
function loadConfig$7() {
|
|
@@ -2117,7 +2118,7 @@ function runGcloud(args, options = {}) {
|
|
|
2117
2118
|
/**
|
|
2118
2119
|
* Get the path to the packages/infra directory
|
|
2119
2120
|
*/
|
|
2120
|
-
function getInfraDir$
|
|
2121
|
+
function getInfraDir$2(projectRoot) {
|
|
2121
2122
|
return join$1(projectRoot, "packages", "infra");
|
|
2122
2123
|
}
|
|
2123
2124
|
/**
|
|
@@ -2418,7 +2419,7 @@ async function infraDestroy(options) {
|
|
|
2418
2419
|
process.exit(1);
|
|
2419
2420
|
}
|
|
2420
2421
|
const stackName = computeStackName(config, env);
|
|
2421
|
-
const infraDir = getInfraDir$
|
|
2422
|
+
const infraDir = getInfraDir$2(projectRoot);
|
|
2422
2423
|
if (!await fileExists$6(infraDir)) {
|
|
2423
2424
|
console.error("Error: packages/infra not found.");
|
|
2424
2425
|
process.exit(1);
|
|
@@ -2572,7 +2573,7 @@ async function infraRepair(options) {
|
|
|
2572
2573
|
process.exit(1);
|
|
2573
2574
|
}
|
|
2574
2575
|
const stackName = computeStackName(config, env);
|
|
2575
|
-
const infraDir = getInfraDir$
|
|
2576
|
+
const infraDir = getInfraDir$2(projectRoot);
|
|
2576
2577
|
if (!await fileExists$5(infraDir)) {
|
|
2577
2578
|
console.error("Error: packages/infra not found.");
|
|
2578
2579
|
process.exit(1);
|
|
@@ -3157,7 +3158,7 @@ async function infraStatus(options) {
|
|
|
3157
3158
|
process.exit(1);
|
|
3158
3159
|
}
|
|
3159
3160
|
const stackName = computeStackName(config, options.env ?? config.env);
|
|
3160
|
-
const infraDir = getInfraDir$
|
|
3161
|
+
const infraDir = getInfraDir$2(projectRoot);
|
|
3161
3162
|
if (!await fileExists$1(infraDir)) {
|
|
3162
3163
|
console.error("Error: packages/infra not found.");
|
|
3163
3164
|
process.exit(1);
|
|
@@ -3268,7 +3269,7 @@ async function infraOutputs(options) {
|
|
|
3268
3269
|
}
|
|
3269
3270
|
const env = options.env ?? config.env ?? "dev";
|
|
3270
3271
|
const stackName = computeStackName(config, env);
|
|
3271
|
-
const infraDir = getInfraDir$
|
|
3272
|
+
const infraDir = getInfraDir$2(projectRoot);
|
|
3272
3273
|
if (!await fileExists(infraDir)) {
|
|
3273
3274
|
console.error("Error: packages/infra not found.");
|
|
3274
3275
|
process.exit(1);
|
|
@@ -3333,10 +3334,10 @@ const KNOWN_CONFIG_KEYS = {
|
|
|
3333
3334
|
example: "prod"
|
|
3334
3335
|
}
|
|
3335
3336
|
};
|
|
3336
|
-
function getProjectRoot() {
|
|
3337
|
+
function getProjectRoot$1() {
|
|
3337
3338
|
return process.cwd();
|
|
3338
3339
|
}
|
|
3339
|
-
async function ensureConfigExists(projectRoot, env) {
|
|
3340
|
+
async function ensureConfigExists$1(projectRoot, env) {
|
|
3340
3341
|
const config = await loadStoredConfig(projectRoot, env);
|
|
3341
3342
|
if (!config) throw new Error("No infrastructure configuration found. Run `kuckit infra init` first.");
|
|
3342
3343
|
return config;
|
|
@@ -3344,25 +3345,25 @@ async function ensureConfigExists(projectRoot, env) {
|
|
|
3344
3345
|
function getStackName(config, env) {
|
|
3345
3346
|
return `${config.projectName}-${env}`;
|
|
3346
3347
|
}
|
|
3347
|
-
async function getInfraDir(projectRoot, config) {
|
|
3348
|
+
async function getInfraDir$1(projectRoot, config) {
|
|
3348
3349
|
if (config.localInfraDir) return config.localInfraDir;
|
|
3349
3350
|
return (await loadProviderFromPackage(config.providerPackage ?? getProviderPackage(config.provider), projectRoot)).getInfraDir(projectRoot);
|
|
3350
3351
|
}
|
|
3351
|
-
async function selectStack(infraDir, stackName) {
|
|
3352
|
+
async function selectStack$1(infraDir, stackName) {
|
|
3352
3353
|
return selectOrCreateStack(stackName, { cwd: infraDir });
|
|
3353
3354
|
}
|
|
3354
3355
|
async function syncToPulumi(projectRoot, config, env, key, value) {
|
|
3355
|
-
const infraDir = await getInfraDir(projectRoot, config);
|
|
3356
|
+
const infraDir = await getInfraDir$1(projectRoot, config);
|
|
3356
3357
|
const stackName = getStackName(config, env);
|
|
3357
3358
|
const pulumiOptions = { cwd: infraDir };
|
|
3358
|
-
if (!await selectStack(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3359
|
+
if (!await selectStack$1(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3359
3360
|
await setPulumiConfig(key, value, pulumiOptions);
|
|
3360
3361
|
}
|
|
3361
3362
|
async function getPulumiConfigValue(projectRoot, config, env, key) {
|
|
3362
|
-
const infraDir = await getInfraDir(projectRoot, config);
|
|
3363
|
+
const infraDir = await getInfraDir$1(projectRoot, config);
|
|
3363
3364
|
const stackName = getStackName(config, env);
|
|
3364
3365
|
const pulumiOptions = { cwd: infraDir };
|
|
3365
|
-
if (!await selectStack(infraDir, stackName)) return null;
|
|
3366
|
+
if (!await selectStack$1(infraDir, stackName)) return null;
|
|
3366
3367
|
try {
|
|
3367
3368
|
const result = await runPulumi([
|
|
3368
3369
|
"config",
|
|
@@ -3375,10 +3376,10 @@ async function getPulumiConfigValue(projectRoot, config, env, key) {
|
|
|
3375
3376
|
}
|
|
3376
3377
|
}
|
|
3377
3378
|
async function getAllPulumiConfig(projectRoot, config, env) {
|
|
3378
|
-
const infraDir = await getInfraDir(projectRoot, config);
|
|
3379
|
+
const infraDir = await getInfraDir$1(projectRoot, config);
|
|
3379
3380
|
const stackName = getStackName(config, env);
|
|
3380
3381
|
const pulumiOptions = { cwd: infraDir };
|
|
3381
|
-
if (!await selectStack(infraDir, stackName)) return {};
|
|
3382
|
+
if (!await selectStack$1(infraDir, stackName)) return {};
|
|
3382
3383
|
try {
|
|
3383
3384
|
const result = await runPulumi(["config", "--json"], pulumiOptions);
|
|
3384
3385
|
if (result.code === 0 && result.stdout) {
|
|
@@ -3396,10 +3397,10 @@ async function getAllPulumiConfig(projectRoot, config, env) {
|
|
|
3396
3397
|
}
|
|
3397
3398
|
}
|
|
3398
3399
|
async function unsetPulumiConfig(projectRoot, config, env, key) {
|
|
3399
|
-
const infraDir = await getInfraDir(projectRoot, config);
|
|
3400
|
+
const infraDir = await getInfraDir$1(projectRoot, config);
|
|
3400
3401
|
const stackName = getStackName(config, env);
|
|
3401
3402
|
const pulumiOptions = { cwd: infraDir };
|
|
3402
|
-
if (!await selectStack(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3403
|
+
if (!await selectStack$1(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3403
3404
|
await runPulumi([
|
|
3404
3405
|
"config",
|
|
3405
3406
|
"rm",
|
|
@@ -3410,9 +3411,9 @@ async function unsetPulumiConfig(projectRoot, config, env, key) {
|
|
|
3410
3411
|
* Set a configuration value
|
|
3411
3412
|
*/
|
|
3412
3413
|
async function infraConfigSet(key, value, options = {}) {
|
|
3413
|
-
const projectRoot = getProjectRoot();
|
|
3414
|
+
const projectRoot = getProjectRoot$1();
|
|
3414
3415
|
const env = options.env;
|
|
3415
|
-
const config = await ensureConfigExists(projectRoot, env);
|
|
3416
|
+
const config = await ensureConfigExists$1(projectRoot, env);
|
|
3416
3417
|
const resolvedEnv = env ?? config.env ?? "dev";
|
|
3417
3418
|
if (["region", "projectName"].includes(key)) {
|
|
3418
3419
|
if (key === "region") config.region = value;
|
|
@@ -3438,9 +3439,9 @@ async function infraConfigSet(key, value, options = {}) {
|
|
|
3438
3439
|
* Get a configuration value
|
|
3439
3440
|
*/
|
|
3440
3441
|
async function infraConfigGet(key, options = {}) {
|
|
3441
|
-
const projectRoot = getProjectRoot();
|
|
3442
|
+
const projectRoot = getProjectRoot$1();
|
|
3442
3443
|
const env = options.env;
|
|
3443
|
-
const config = await ensureConfigExists(projectRoot, env);
|
|
3444
|
+
const config = await ensureConfigExists$1(projectRoot, env);
|
|
3444
3445
|
const resolvedEnv = env ?? config.env ?? "dev";
|
|
3445
3446
|
const localKeys = {
|
|
3446
3447
|
region: (c) => c.region,
|
|
@@ -3469,9 +3470,9 @@ async function infraConfigGet(key, options = {}) {
|
|
|
3469
3470
|
* List all configuration values
|
|
3470
3471
|
*/
|
|
3471
3472
|
async function infraConfigList(options = {}) {
|
|
3472
|
-
const projectRoot = getProjectRoot();
|
|
3473
|
+
const projectRoot = getProjectRoot$1();
|
|
3473
3474
|
const env = options.env;
|
|
3474
|
-
const config = await ensureConfigExists(projectRoot, env);
|
|
3475
|
+
const config = await ensureConfigExists$1(projectRoot, env);
|
|
3475
3476
|
const resolvedEnv = env ?? config.env ?? "dev";
|
|
3476
3477
|
const localConfig = {
|
|
3477
3478
|
provider: config.provider,
|
|
@@ -3509,9 +3510,9 @@ async function infraConfigList(options = {}) {
|
|
|
3509
3510
|
* Unset a configuration value
|
|
3510
3511
|
*/
|
|
3511
3512
|
async function infraConfigUnset(key, options = {}) {
|
|
3512
|
-
const projectRoot = getProjectRoot();
|
|
3513
|
+
const projectRoot = getProjectRoot$1();
|
|
3513
3514
|
const env = options.env;
|
|
3514
|
-
const config = await ensureConfigExists(projectRoot, env);
|
|
3515
|
+
const config = await ensureConfigExists$1(projectRoot, env);
|
|
3515
3516
|
const resolvedEnv = env ?? config.env ?? "dev";
|
|
3516
3517
|
if ([
|
|
3517
3518
|
"provider",
|
|
@@ -3522,6 +3523,378 @@ async function infraConfigUnset(key, options = {}) {
|
|
|
3522
3523
|
console.log(`ā Unset ${key} for env '${resolvedEnv}'`);
|
|
3523
3524
|
}
|
|
3524
3525
|
|
|
3526
|
+
//#endregion
|
|
3527
|
+
//#region src/lib/secret-manager.ts
|
|
3528
|
+
/**
|
|
3529
|
+
* Sync a secret to GCP Secret Manager.
|
|
3530
|
+
* Creates the secret if it doesn't exist, then adds a new version.
|
|
3531
|
+
*/
|
|
3532
|
+
async function syncSecret(options) {
|
|
3533
|
+
const { projectId, secretId, value } = options;
|
|
3534
|
+
await createSecretIfNotExists(projectId, secretId);
|
|
3535
|
+
await addSecretVersion(projectId, secretId, value);
|
|
3536
|
+
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Create a secret if it doesn't exist.
|
|
3539
|
+
*/
|
|
3540
|
+
async function createSecretIfNotExists(projectId, secretId) {
|
|
3541
|
+
const client = new SecretManagerServiceClient();
|
|
3542
|
+
const name = `projects/${projectId}/secrets/${secretId}`;
|
|
3543
|
+
try {
|
|
3544
|
+
await client.getSecret({ name });
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
if (isGrpcError(error) && error.code === 5) await client.createSecret({
|
|
3547
|
+
parent: `projects/${projectId}`,
|
|
3548
|
+
secretId,
|
|
3549
|
+
secret: { replication: { automatic: {} } }
|
|
3550
|
+
});
|
|
3551
|
+
else throw wrapAuthError(error);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
/**
|
|
3555
|
+
* Add a new version to an existing secret.
|
|
3556
|
+
* @returns The version name (e.g., "projects/.../secrets/.../versions/1")
|
|
3557
|
+
*/
|
|
3558
|
+
async function addSecretVersion(projectId, secretId, value) {
|
|
3559
|
+
const client = new SecretManagerServiceClient();
|
|
3560
|
+
const parent = `projects/${projectId}/secrets/${secretId}`;
|
|
3561
|
+
try {
|
|
3562
|
+
const [version] = await client.addSecretVersion({
|
|
3563
|
+
parent,
|
|
3564
|
+
payload: { data: Buffer.from(value, "utf8") }
|
|
3565
|
+
});
|
|
3566
|
+
return version.name ?? "";
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
throw wrapAuthError(error);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
function isGrpcError(error) {
|
|
3572
|
+
return error !== null && typeof error === "object" && "code" in error && typeof error.code === "number";
|
|
3573
|
+
}
|
|
3574
|
+
/**
|
|
3575
|
+
* Wrap authentication errors with a helpful message.
|
|
3576
|
+
*/
|
|
3577
|
+
function wrapAuthError(error) {
|
|
3578
|
+
if (isGrpcError(error)) {
|
|
3579
|
+
if (error.code === 16) return /* @__PURE__ */ new Error(`GCP authentication failed. Please run \`gcloud auth application-default login\` or ensure your service account credentials are configured.
|
|
3580
|
+
Original error: ${error.message}`);
|
|
3581
|
+
if (error.code === 7) return /* @__PURE__ */ new Error(`GCP permission denied. Ensure your account has the "Secret Manager Secret Accessor" and "Secret Manager Admin" roles.
|
|
3582
|
+
Original error: ${error.message}`);
|
|
3583
|
+
}
|
|
3584
|
+
if (error instanceof Error) return error;
|
|
3585
|
+
return new Error(String(error));
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
//#endregion
|
|
3589
|
+
//#region src/commands/infra/env.ts
|
|
3590
|
+
/** Default pattern to detect secret environment variables by name */
|
|
3591
|
+
const DEFAULT_SECRET_PATTERN = /SECRET|KEY|TOKEN|PASSWORD|WEBHOOK/i;
|
|
3592
|
+
function getProjectRoot() {
|
|
3593
|
+
return process.cwd();
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Parse a .env file into key-value pairs
|
|
3597
|
+
* Handles:
|
|
3598
|
+
* - Empty lines and comments (#)
|
|
3599
|
+
* - Quoted values (single and double quotes)
|
|
3600
|
+
* - Inline comments after values
|
|
3601
|
+
*/
|
|
3602
|
+
function parseEnvFile(content) {
|
|
3603
|
+
const vars = {};
|
|
3604
|
+
for (const line of content.split("\n")) {
|
|
3605
|
+
const trimmed = line.trim();
|
|
3606
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
3607
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
3608
|
+
if (match) {
|
|
3609
|
+
const [, key, rawValue] = match;
|
|
3610
|
+
if (!key) continue;
|
|
3611
|
+
let value = rawValue ?? "";
|
|
3612
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
3613
|
+
else {
|
|
3614
|
+
const commentIndex = value.indexOf(" #");
|
|
3615
|
+
if (commentIndex !== -1) value = value.substring(0, commentIndex);
|
|
3616
|
+
}
|
|
3617
|
+
vars[key.trim()] = value.trim();
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return vars;
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* Classify env vars into secrets and plain vars based on pattern
|
|
3624
|
+
*/
|
|
3625
|
+
function classifyEnvVars(vars, secretPattern) {
|
|
3626
|
+
const secrets = {};
|
|
3627
|
+
const plain = {};
|
|
3628
|
+
for (const [key, value] of Object.entries(vars)) if (secretPattern.test(key)) secrets[key] = value;
|
|
3629
|
+
else plain[key] = value;
|
|
3630
|
+
return {
|
|
3631
|
+
secrets,
|
|
3632
|
+
plain
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* Convert env var name to GCP Secret Manager ID format
|
|
3637
|
+
* GCP Secret IDs must match: [a-zA-Z_0-9]+
|
|
3638
|
+
* We convert to lowercase with underscores for consistency
|
|
3639
|
+
*/
|
|
3640
|
+
function toSecretId(envVarName, env) {
|
|
3641
|
+
return `${envVarName.toLowerCase().replace(/[^a-z0-9_]/g, "_")}_${env}`;
|
|
3642
|
+
}
|
|
3643
|
+
async function ensureConfigExists(projectRoot, env) {
|
|
3644
|
+
const config = await loadStoredConfig(projectRoot, env);
|
|
3645
|
+
if (!config) throw new Error("No infrastructure configuration found. Run `kuckit infra init` first.");
|
|
3646
|
+
return config;
|
|
3647
|
+
}
|
|
3648
|
+
async function getInfraDir(projectRoot, config) {
|
|
3649
|
+
if (config.localInfraDir) return config.localInfraDir;
|
|
3650
|
+
return (await loadProviderFromPackage(config.providerPackage ?? getProviderPackage(config.provider), projectRoot)).getInfraDir(projectRoot);
|
|
3651
|
+
}
|
|
3652
|
+
async function selectStack(infraDir, stackName) {
|
|
3653
|
+
return selectOrCreateStack(stackName, { cwd: infraDir });
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Find the .env file to use
|
|
3657
|
+
* Priority:
|
|
3658
|
+
* 1. Explicit --file option
|
|
3659
|
+
* 2. .env.{env} (e.g., .env.dev)
|
|
3660
|
+
* 3. .env
|
|
3661
|
+
*/
|
|
3662
|
+
async function findEnvFile(projectRoot, env, explicitFile) {
|
|
3663
|
+
if (explicitFile) {
|
|
3664
|
+
const explicitPath = join$1(projectRoot, explicitFile);
|
|
3665
|
+
try {
|
|
3666
|
+
await access(explicitPath, constants$1.F_OK);
|
|
3667
|
+
return explicitPath;
|
|
3668
|
+
} catch {
|
|
3669
|
+
throw new Error(`Specified .env file not found: ${explicitFile}`);
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
const envSpecificPath = join$1(projectRoot, `.env.${env}`);
|
|
3673
|
+
try {
|
|
3674
|
+
await access(envSpecificPath, constants$1.F_OK);
|
|
3675
|
+
return envSpecificPath;
|
|
3676
|
+
} catch {}
|
|
3677
|
+
const defaultPath = join$1(projectRoot, ".env");
|
|
3678
|
+
try {
|
|
3679
|
+
await access(defaultPath, constants$1.F_OK);
|
|
3680
|
+
return defaultPath;
|
|
3681
|
+
} catch {
|
|
3682
|
+
throw new Error(`No .env file found. Tried:\n - .env.${env}\n - .env\n\nCreate one or specify with --file.`);
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Sync environment variables from .env file to GCP Secret Manager and Pulumi config
|
|
3687
|
+
*
|
|
3688
|
+
* 1. Load and parse .env file
|
|
3689
|
+
* 2. Classify variables (secrets vs plain)
|
|
3690
|
+
* 3. Sync secrets to GCP Secret Manager
|
|
3691
|
+
* 4. Update Pulumi config with extraEnvVars and extraSecretEnvVars
|
|
3692
|
+
*/
|
|
3693
|
+
async function infraEnvSync(options) {
|
|
3694
|
+
const projectRoot = getProjectRoot();
|
|
3695
|
+
const env = options.env;
|
|
3696
|
+
const secretPattern = options.secretPattern ?? DEFAULT_SECRET_PATTERN;
|
|
3697
|
+
console.log(`\nš Syncing environment variables for '${env}'...\n`);
|
|
3698
|
+
const config = await ensureConfigExists(projectRoot, env);
|
|
3699
|
+
if (!isGcpConfig(config)) throw new Error("Environment sync is currently only supported for GCP provider.");
|
|
3700
|
+
const gcpProject = config.providerConfig.gcpProject;
|
|
3701
|
+
const envFilePath = await findEnvFile(projectRoot, env, options.file);
|
|
3702
|
+
console.log(`š Loading: ${envFilePath}`);
|
|
3703
|
+
const allVars = parseEnvFile(await readFile(envFilePath, "utf-8"));
|
|
3704
|
+
if (Object.keys(allVars).length === 0) {
|
|
3705
|
+
console.log("ā ļø No environment variables found in file.");
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
console.log(` Found ${Object.keys(allVars).length} variables\n`);
|
|
3709
|
+
const { secrets, plain } = classifyEnvVars(allVars, secretPattern);
|
|
3710
|
+
console.log(`š Classification:`);
|
|
3711
|
+
console.log(` ⢠Plain env vars: ${Object.keys(plain).length}`);
|
|
3712
|
+
console.log(` ⢠Secrets: ${Object.keys(secrets).length}\n`);
|
|
3713
|
+
if (Object.keys(secrets).length > 0) {
|
|
3714
|
+
console.log(`š Syncing secrets to GCP Secret Manager (project: ${gcpProject})...`);
|
|
3715
|
+
const extraSecretEnvVars = {};
|
|
3716
|
+
for (const [name, value] of Object.entries(secrets)) {
|
|
3717
|
+
const secretId = toSecretId(name, env);
|
|
3718
|
+
console.log(` ⢠${name} ā ${secretId}`);
|
|
3719
|
+
try {
|
|
3720
|
+
await syncSecret({
|
|
3721
|
+
projectId: gcpProject,
|
|
3722
|
+
secretId,
|
|
3723
|
+
value
|
|
3724
|
+
});
|
|
3725
|
+
extraSecretEnvVars[name] = secretId;
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3728
|
+
throw new Error(`Failed to sync secret '${name}': ${message}`);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
console.log(` ā All secrets synced\n`);
|
|
3732
|
+
await updatePulumiConfig(projectRoot, config, env, "extraSecretEnvVars", extraSecretEnvVars);
|
|
3733
|
+
}
|
|
3734
|
+
if (Object.keys(plain).length > 0) {
|
|
3735
|
+
console.log(`š Setting plain environment variables in Pulumi config...`);
|
|
3736
|
+
for (const name of Object.keys(plain)) console.log(` ⢠${name}`);
|
|
3737
|
+
await updatePulumiConfig(projectRoot, config, env, "extraEnvVars", plain);
|
|
3738
|
+
console.log(` ā Config updated\n`);
|
|
3739
|
+
}
|
|
3740
|
+
console.log(`ā
Environment sync complete!`);
|
|
3741
|
+
console.log(`\nNext steps:`);
|
|
3742
|
+
console.log(` ⢠Run 'kuckit infra deploy --env ${env}' to apply changes`);
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Update Pulumi config with an object value using --json flag
|
|
3746
|
+
*/
|
|
3747
|
+
async function updatePulumiConfig(projectRoot, config, env, key, value) {
|
|
3748
|
+
const infraDir = await getInfraDir(projectRoot, config);
|
|
3749
|
+
const stackName = computeStackName(config, env);
|
|
3750
|
+
const pulumiOptions = { cwd: infraDir };
|
|
3751
|
+
if (!await selectStack(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3752
|
+
const result = await runPulumi([
|
|
3753
|
+
"config",
|
|
3754
|
+
"set",
|
|
3755
|
+
key,
|
|
3756
|
+
`'${JSON.stringify(value).replace(/'/g, "'\\''")}'`
|
|
3757
|
+
], pulumiOptions);
|
|
3758
|
+
if (result.code !== 0) throw new Error(`Failed to set Pulumi config '${key}': ${result.stderr}`);
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* Read an object value from Pulumi config
|
|
3762
|
+
*/
|
|
3763
|
+
async function getPulumiConfigObject(projectRoot, config, env, key) {
|
|
3764
|
+
const infraDir = await getInfraDir(projectRoot, config);
|
|
3765
|
+
const stackName = computeStackName(config, env);
|
|
3766
|
+
const pulumiOptions = { cwd: infraDir };
|
|
3767
|
+
if (!await selectStack(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3768
|
+
const result = await runPulumi([
|
|
3769
|
+
"config",
|
|
3770
|
+
"get",
|
|
3771
|
+
key
|
|
3772
|
+
], pulumiOptions);
|
|
3773
|
+
if (result.code === 0 && result.stdout.trim()) try {
|
|
3774
|
+
return JSON.parse(result.stdout.trim());
|
|
3775
|
+
} catch {
|
|
3776
|
+
return {};
|
|
3777
|
+
}
|
|
3778
|
+
return {};
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* List all configured environment variables and secrets for an environment
|
|
3782
|
+
*/
|
|
3783
|
+
async function infraEnvList(options) {
|
|
3784
|
+
const projectRoot = getProjectRoot();
|
|
3785
|
+
const env = options.env;
|
|
3786
|
+
const config = await ensureConfigExists(projectRoot, env);
|
|
3787
|
+
if (!isGcpConfig(config)) throw new Error("Environment commands are currently only supported for GCP provider.");
|
|
3788
|
+
const extraEnvVars = await getPulumiConfigObject(projectRoot, config, env, "extraEnvVars");
|
|
3789
|
+
const extraSecretEnvVars = await getPulumiConfigObject(projectRoot, config, env, "extraSecretEnvVars");
|
|
3790
|
+
const hasEnvVars = Object.keys(extraEnvVars).length > 0;
|
|
3791
|
+
const hasSecrets = Object.keys(extraSecretEnvVars).length > 0;
|
|
3792
|
+
if (options.json) {
|
|
3793
|
+
console.log(JSON.stringify({
|
|
3794
|
+
env,
|
|
3795
|
+
envVars: extraEnvVars,
|
|
3796
|
+
secrets: extraSecretEnvVars
|
|
3797
|
+
}, null, 2));
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
console.log(`\nEnvironment Variables (env: ${env})\n`);
|
|
3801
|
+
if (hasEnvVars) {
|
|
3802
|
+
console.log("Plain Environment Variables:");
|
|
3803
|
+
for (const [key, value] of Object.entries(extraEnvVars)) console.log(` ${key}=${value}`);
|
|
3804
|
+
} else console.log("Plain Environment Variables: (none)");
|
|
3805
|
+
console.log("");
|
|
3806
|
+
if (hasSecrets) {
|
|
3807
|
+
console.log("Secrets (stored in GCP Secret Manager):");
|
|
3808
|
+
for (const [key, secretId] of Object.entries(extraSecretEnvVars)) console.log(` ${key} ā ${secretId}`);
|
|
3809
|
+
} else console.log("Secrets: (none)");
|
|
3810
|
+
console.log(`\nTip: Use 'kuckit infra env add KEY=value --env ${env}' to add variables`);
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* Add an environment variable or secret
|
|
3814
|
+
*/
|
|
3815
|
+
async function infraEnvAdd(keyValue, options) {
|
|
3816
|
+
const projectRoot = getProjectRoot();
|
|
3817
|
+
const env = options.env;
|
|
3818
|
+
const equalsIndex = keyValue.indexOf("=");
|
|
3819
|
+
if (equalsIndex === -1) throw new Error(`Invalid format. Expected KEY=value, got: ${keyValue}`);
|
|
3820
|
+
const key = keyValue.substring(0, equalsIndex).trim();
|
|
3821
|
+
const value = keyValue.substring(equalsIndex + 1);
|
|
3822
|
+
if (!key) throw new Error("Key cannot be empty.");
|
|
3823
|
+
const config = await ensureConfigExists(projectRoot, env);
|
|
3824
|
+
if (!isGcpConfig(config)) throw new Error("Environment commands are currently only supported for GCP provider.");
|
|
3825
|
+
const gcpProject = config.providerConfig.gcpProject;
|
|
3826
|
+
if (options.secret) {
|
|
3827
|
+
const secretId = toSecretId(key, env);
|
|
3828
|
+
console.log(`\nš Syncing secret '${key}' to GCP Secret Manager...`);
|
|
3829
|
+
console.log(` Project: ${gcpProject}`);
|
|
3830
|
+
console.log(` Secret ID: ${secretId}`);
|
|
3831
|
+
try {
|
|
3832
|
+
await syncSecret({
|
|
3833
|
+
projectId: gcpProject,
|
|
3834
|
+
secretId,
|
|
3835
|
+
value
|
|
3836
|
+
});
|
|
3837
|
+
console.log(` ā Secret synced`);
|
|
3838
|
+
} catch (error) {
|
|
3839
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3840
|
+
throw new Error(`Failed to sync secret: ${message}`);
|
|
3841
|
+
}
|
|
3842
|
+
await updatePulumiConfig(projectRoot, config, env, "extraSecretEnvVars", {
|
|
3843
|
+
...await getPulumiConfigObject(projectRoot, config, env, "extraSecretEnvVars"),
|
|
3844
|
+
[key]: secretId
|
|
3845
|
+
});
|
|
3846
|
+
console.log(` ā Added to Pulumi config\n`);
|
|
3847
|
+
console.log(`ā
Secret '${key}' added successfully!`);
|
|
3848
|
+
} else {
|
|
3849
|
+
console.log(`\nš Adding environment variable '${key}'...`);
|
|
3850
|
+
await updatePulumiConfig(projectRoot, config, env, "extraEnvVars", {
|
|
3851
|
+
...await getPulumiConfigObject(projectRoot, config, env, "extraEnvVars"),
|
|
3852
|
+
[key]: value
|
|
3853
|
+
});
|
|
3854
|
+
console.log(` ā Added to Pulumi config\n`);
|
|
3855
|
+
console.log(`ā
Environment variable '${key}' added successfully!`);
|
|
3856
|
+
}
|
|
3857
|
+
console.log(`\nNext steps:`);
|
|
3858
|
+
console.log(` ⢠Run 'kuckit infra deploy --env ${env}' to apply changes`);
|
|
3859
|
+
}
|
|
3860
|
+
/**
|
|
3861
|
+
* Remove an environment variable or secret
|
|
3862
|
+
*/
|
|
3863
|
+
async function infraEnvRemove(key, options) {
|
|
3864
|
+
const projectRoot = getProjectRoot();
|
|
3865
|
+
const env = options.env;
|
|
3866
|
+
if (!key || !key.trim()) throw new Error("Key cannot be empty.");
|
|
3867
|
+
key = key.trim();
|
|
3868
|
+
const config = await ensureConfigExists(projectRoot, env);
|
|
3869
|
+
if (!isGcpConfig(config)) throw new Error("Environment commands are currently only supported for GCP provider.");
|
|
3870
|
+
const extraEnvVars = await getPulumiConfigObject(projectRoot, config, env, "extraEnvVars");
|
|
3871
|
+
const extraSecretEnvVars = await getPulumiConfigObject(projectRoot, config, env, "extraSecretEnvVars");
|
|
3872
|
+
let found = false;
|
|
3873
|
+
if (key in extraEnvVars) {
|
|
3874
|
+
console.log(`\nš Removing environment variable '${key}'...`);
|
|
3875
|
+
const { [key]: _,...remaining } = extraEnvVars;
|
|
3876
|
+
await updatePulumiConfig(projectRoot, config, env, "extraEnvVars", remaining);
|
|
3877
|
+
console.log(` ā Removed from Pulumi config`);
|
|
3878
|
+
found = true;
|
|
3879
|
+
}
|
|
3880
|
+
if (key in extraSecretEnvVars) {
|
|
3881
|
+
console.log(`\nš Removing secret '${key}'...`);
|
|
3882
|
+
const { [key]: __,...remaining } = extraSecretEnvVars;
|
|
3883
|
+
await updatePulumiConfig(projectRoot, config, env, "extraSecretEnvVars", remaining);
|
|
3884
|
+
console.log(` ā Removed from Pulumi config`);
|
|
3885
|
+
console.log(` Note: The secret value remains in GCP Secret Manager (not deleted)`);
|
|
3886
|
+
found = true;
|
|
3887
|
+
}
|
|
3888
|
+
if (!found) {
|
|
3889
|
+
console.log(`\nā ļø Key '${key}' not found in environment '${env}'.`);
|
|
3890
|
+
console.log(`\nUse 'kuckit infra env list --env ${env}' to see configured variables.`);
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
console.log(`\nā
Removed '${key}' successfully!`);
|
|
3894
|
+
console.log(`\nNext steps:`);
|
|
3895
|
+
console.log(` ⢠Run 'kuckit infra deploy --env ${env}' to apply changes`);
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3525
3898
|
//#endregion
|
|
3526
3899
|
//#region src/bin.ts
|
|
3527
3900
|
program.name("kuckit").description("CLI tools for Kuckit SDK module development").version("0.1.0");
|
|
@@ -3635,6 +4008,23 @@ configCmd.command("unset <key>").description("Remove a configuration value").opt
|
|
|
3635
4008
|
requireAuth();
|
|
3636
4009
|
await infraConfigUnset(key, options);
|
|
3637
4010
|
});
|
|
4011
|
+
const envCmd = infra.command("env").description("Environment variable management");
|
|
4012
|
+
envCmd.command("sync").description("Sync .env file to GCP Secret Manager and Pulumi config").option("-e, --env <env>", "Environment (dev, staging, prod)", "dev").option("-f, --file <path>", "Path to .env file (default: .env.{env} or .env)").action(async (options) => {
|
|
4013
|
+
requireAuth();
|
|
4014
|
+
await infraEnvSync(options);
|
|
4015
|
+
});
|
|
4016
|
+
envCmd.command("list").description("List all configured environment variables and secrets").requiredOption("-e, --env <env>", "Environment (dev, staging, prod)").option("--json", "Output as JSON", false).action(async (options) => {
|
|
4017
|
+
requireAuth();
|
|
4018
|
+
await infraEnvList(options);
|
|
4019
|
+
});
|
|
4020
|
+
envCmd.command("add <KEY=value>").description("Add an environment variable or secret").requiredOption("-e, --env <env>", "Environment (dev, staging, prod)").option("-s, --secret", "Store as secret in GCP Secret Manager", false).action(async (keyValue, options) => {
|
|
4021
|
+
requireAuth();
|
|
4022
|
+
await infraEnvAdd(keyValue, options);
|
|
4023
|
+
});
|
|
4024
|
+
envCmd.command("remove <KEY>").description("Remove an environment variable or secret").requiredOption("-e, --env <env>", "Environment (dev, staging, prod)").action(async (key, options) => {
|
|
4025
|
+
requireAuth();
|
|
4026
|
+
await infraEnvRemove(key, options);
|
|
4027
|
+
});
|
|
3638
4028
|
program.parse();
|
|
3639
4029
|
|
|
3640
4030
|
//#endregion
|