@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 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://api.kuckit.dev";
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$1(projectRoot) {
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$1(projectRoot);
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$1(projectRoot);
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$1(projectRoot);
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$1(projectRoot);
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