@kuckit/cli 1.0.4 → 2.0.1
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 +1293 -695
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +476 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/{discover-module-B4oRIuSK.js → provider-DbqTBb6C.js} +66 -1
- package/dist/provider-DbqTBb6C.js.map +1 -0
- package/package.json +3 -2
package/dist/bin.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as isLegacyConfig, c as loadTryLoadKuckitConfig, i as isGcpConfig, l as addModule, o as migrateLegacyConfig, s as discoverModules, u as generateModule } from "./provider-DbqTBb6C.js";
|
|
3
3
|
import { program } from "commander";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
-
import {
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
7
|
import { access, constants as constants$1, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
8
8
|
import { dirname as dirname$1, join as join$1 } from "path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
11
|
+
import { accessSync as accessSync$1, constants as constants$2 } from "fs";
|
|
11
12
|
|
|
12
13
|
//#region src/commands/doctor.ts
|
|
13
14
|
const CONFIG_FILES = [
|
|
@@ -99,7 +100,7 @@ function checkModuleExports(packageName, cwd) {
|
|
|
99
100
|
].some((p) => existsSync(p));
|
|
100
101
|
return result;
|
|
101
102
|
}
|
|
102
|
-
async function loadConfig$
|
|
103
|
+
async function loadConfig$8(configPath) {
|
|
103
104
|
try {
|
|
104
105
|
if (configPath.endsWith(".ts")) {
|
|
105
106
|
const { createJiti } = await import("jiti");
|
|
@@ -139,7 +140,7 @@ async function doctor(options) {
|
|
|
139
140
|
});
|
|
140
141
|
let configModules = [];
|
|
141
142
|
if (configPath) {
|
|
142
|
-
const { success, config, error } = await loadConfig$
|
|
143
|
+
const { success, config, error } = await loadConfig$8(configPath);
|
|
143
144
|
if (success && config && typeof config === "object") {
|
|
144
145
|
const cfg = config;
|
|
145
146
|
if (Array.isArray(cfg.modules)) {
|
|
@@ -462,9 +463,9 @@ async function search(keyword, options) {
|
|
|
462
463
|
|
|
463
464
|
//#endregion
|
|
464
465
|
//#region src/commands/db.ts
|
|
465
|
-
const KUCKIT_DIR$
|
|
466
|
+
const KUCKIT_DIR$8 = ".kuckit";
|
|
466
467
|
const TEMP_CONFIG_NAME = "drizzle.config.ts";
|
|
467
|
-
async function fileExists$
|
|
468
|
+
async function fileExists$11(path) {
|
|
468
469
|
try {
|
|
469
470
|
await access(path, constants$1.F_OK);
|
|
470
471
|
return true;
|
|
@@ -472,10 +473,10 @@ async function fileExists$9(path) {
|
|
|
472
473
|
return false;
|
|
473
474
|
}
|
|
474
475
|
}
|
|
475
|
-
async function findProjectRoot$
|
|
476
|
+
async function findProjectRoot$11(cwd) {
|
|
476
477
|
let dir = cwd;
|
|
477
478
|
while (dir !== dirname$1(dir)) {
|
|
478
|
-
if (await fileExists$
|
|
479
|
+
if (await fileExists$11(join$1(dir, "package.json"))) return dir;
|
|
479
480
|
dir = dirname$1(dir);
|
|
480
481
|
}
|
|
481
482
|
return null;
|
|
@@ -488,7 +489,7 @@ async function getDatabaseUrl(cwd, options) {
|
|
|
488
489
|
join$1(cwd, "apps", "server", ".env"),
|
|
489
490
|
join$1(cwd, ".env.local")
|
|
490
491
|
];
|
|
491
|
-
for (const envPath of envPaths) if (await fileExists$
|
|
492
|
+
for (const envPath of envPaths) if (await fileExists$11(envPath)) try {
|
|
492
493
|
const match = (await readFile(envPath, "utf-8")).match(/^DATABASE_URL=(.+)$/m);
|
|
493
494
|
if (match) return match[1].replace(/^["']|["']$/g, "");
|
|
494
495
|
} catch {}
|
|
@@ -504,10 +505,10 @@ async function discoverModuleSchemas(cwd) {
|
|
|
504
505
|
let moduleId = null;
|
|
505
506
|
if (moduleSpec.package) {
|
|
506
507
|
const workspacePath = join$1(cwd, "packages", moduleSpec.package.replace(/^@[^/]+\//, ""));
|
|
507
|
-
if (await fileExists$
|
|
508
|
+
if (await fileExists$11(join$1(workspacePath, "package.json"))) packagePath = workspacePath;
|
|
508
509
|
else {
|
|
509
510
|
const nodeModulesPath = join$1(cwd, "node_modules", moduleSpec.package);
|
|
510
|
-
if (await fileExists$
|
|
511
|
+
if (await fileExists$11(join$1(nodeModulesPath, "package.json"))) packagePath = nodeModulesPath;
|
|
511
512
|
}
|
|
512
513
|
if (packagePath) try {
|
|
513
514
|
moduleId = JSON.parse(await readFile(join$1(packagePath, "package.json"), "utf-8")).kuckit?.id || moduleSpec.package;
|
|
@@ -522,7 +523,7 @@ async function discoverModuleSchemas(cwd) {
|
|
|
522
523
|
join$1(packagePath, "src", "schema", "index.ts"),
|
|
523
524
|
join$1(packagePath, "src", "schema.ts")
|
|
524
525
|
];
|
|
525
|
-
for (const schemaPath of schemaLocations) if (await fileExists$
|
|
526
|
+
for (const schemaPath of schemaLocations) if (await fileExists$11(schemaPath)) {
|
|
526
527
|
schemas.push({
|
|
527
528
|
moduleId: moduleId || "unknown",
|
|
528
529
|
schemaPath
|
|
@@ -531,14 +532,14 @@ async function discoverModuleSchemas(cwd) {
|
|
|
531
532
|
}
|
|
532
533
|
}
|
|
533
534
|
const centralSchemaPath = join$1(cwd, "packages", "db", "src", "schema");
|
|
534
|
-
if (await fileExists$
|
|
535
|
+
if (await fileExists$11(centralSchemaPath)) schemas.push({
|
|
535
536
|
moduleId: "core",
|
|
536
537
|
schemaPath: centralSchemaPath
|
|
537
538
|
});
|
|
538
539
|
return schemas;
|
|
539
540
|
}
|
|
540
541
|
async function generateTempDrizzleConfig(cwd, databaseUrl, schemas) {
|
|
541
|
-
const kuckitDir = join$1(cwd, KUCKIT_DIR$
|
|
542
|
+
const kuckitDir = join$1(cwd, KUCKIT_DIR$8);
|
|
542
543
|
await mkdir(kuckitDir, { recursive: true });
|
|
543
544
|
const configPath = join$1(kuckitDir, TEMP_CONFIG_NAME);
|
|
544
545
|
const schemaPaths = schemas.map((s) => s.schemaPath);
|
|
@@ -590,7 +591,7 @@ function runDrizzleKit(command, configPath, cwd) {
|
|
|
590
591
|
});
|
|
591
592
|
}
|
|
592
593
|
async function runDbCommand(command, options) {
|
|
593
|
-
const projectRoot = await findProjectRoot$
|
|
594
|
+
const projectRoot = await findProjectRoot$11(process.cwd());
|
|
594
595
|
if (!projectRoot) {
|
|
595
596
|
console.error("Error: Could not find project root (no package.json found)");
|
|
596
597
|
process.exit(1);
|
|
@@ -638,7 +639,7 @@ async function dbStudio(options) {
|
|
|
638
639
|
const DEFAULT_SERVER_URL = "https://dev-app-nyh7i73bea-uc.a.run.app/";
|
|
639
640
|
const CONFIG_DIR = join(homedir(), ".kuckit");
|
|
640
641
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
641
|
-
function loadConfig$
|
|
642
|
+
function loadConfig$7() {
|
|
642
643
|
try {
|
|
643
644
|
if (!existsSync(CONFIG_PATH)) return null;
|
|
644
645
|
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
@@ -647,7 +648,7 @@ function loadConfig$8() {
|
|
|
647
648
|
return null;
|
|
648
649
|
}
|
|
649
650
|
}
|
|
650
|
-
function saveConfig
|
|
651
|
+
function saveConfig(config) {
|
|
651
652
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, {
|
|
652
653
|
recursive: true,
|
|
653
654
|
mode: 448
|
|
@@ -691,7 +692,7 @@ function formatExpiryDate(expiresAt) {
|
|
|
691
692
|
});
|
|
692
693
|
}
|
|
693
694
|
async function authLogin(options) {
|
|
694
|
-
const config = loadConfig$
|
|
695
|
+
const config = loadConfig$7();
|
|
695
696
|
const serverUrl = getServerUrl(config, options.server);
|
|
696
697
|
const scopes = options.scopes?.split(",").map((s) => s.trim()) ?? ["cli"];
|
|
697
698
|
console.log("\nAuthenticating with Kuckit...\n");
|
|
@@ -756,7 +757,7 @@ async function authLogin(options) {
|
|
|
756
757
|
process.exit(1);
|
|
757
758
|
}
|
|
758
759
|
const expiresAt = new Date(Date.now() + pollResult.expiresIn * 1e3).toISOString();
|
|
759
|
-
saveConfig
|
|
760
|
+
saveConfig({
|
|
760
761
|
...config,
|
|
761
762
|
cliToken: pollResult.token,
|
|
762
763
|
tokenExpiresAt: expiresAt,
|
|
@@ -789,7 +790,7 @@ async function authLogin(options) {
|
|
|
789
790
|
process.exit(1);
|
|
790
791
|
}
|
|
791
792
|
async function authWhoami(options) {
|
|
792
|
-
const config = loadConfig$
|
|
793
|
+
const config = loadConfig$7();
|
|
793
794
|
if (!config?.cliToken) {
|
|
794
795
|
if (options.json) console.log(JSON.stringify({ authenticated: false }));
|
|
795
796
|
else console.log("Not logged in. Run 'kuckit auth login' to authenticate.");
|
|
@@ -822,7 +823,7 @@ async function authWhoami(options) {
|
|
|
822
823
|
}
|
|
823
824
|
}
|
|
824
825
|
async function authLogout() {
|
|
825
|
-
if (!loadConfig$
|
|
826
|
+
if (!loadConfig$7()?.cliToken) {
|
|
826
827
|
console.log("Already logged out.");
|
|
827
828
|
return;
|
|
828
829
|
}
|
|
@@ -849,7 +850,7 @@ function registerAuthCommands(program$1) {
|
|
|
849
850
|
* Exits the process with an error message if not authenticated.
|
|
850
851
|
*/
|
|
851
852
|
function requireAuth() {
|
|
852
|
-
const config = loadConfig$
|
|
853
|
+
const config = loadConfig$7();
|
|
853
854
|
if (!config?.cliToken) {
|
|
854
855
|
console.error("\nError: Not logged in. Run 'kuckit auth login' first.\n");
|
|
855
856
|
process.exit(1);
|
|
@@ -861,293 +862,1008 @@ function requireAuth() {
|
|
|
861
862
|
}
|
|
862
863
|
|
|
863
864
|
//#endregion
|
|
864
|
-
//#region src/
|
|
865
|
+
//#region src/lib/package-manager.ts
|
|
865
866
|
/**
|
|
866
|
-
*
|
|
867
|
+
* Detect the package manager used in a project
|
|
868
|
+
*
|
|
869
|
+
* @param cwd - Directory to check for lock files
|
|
870
|
+
* @returns The detected package manager, defaults to 'npm' if none found
|
|
867
871
|
*/
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
],
|
|
881
|
-
shell: true,
|
|
882
|
-
env: {
|
|
883
|
-
...process.env,
|
|
884
|
-
...options.env,
|
|
885
|
-
PULUMI_SKIP_UPDATE_CHECK: "true"
|
|
886
|
-
}
|
|
887
|
-
});
|
|
888
|
-
let stdout = "";
|
|
889
|
-
let stderr = "";
|
|
890
|
-
proc.stdout?.on("data", (data) => {
|
|
891
|
-
const str = data.toString();
|
|
892
|
-
stdout += str;
|
|
893
|
-
if (options.stream) process.stdout.write(data);
|
|
894
|
-
});
|
|
895
|
-
proc.stderr?.on("data", (data) => {
|
|
896
|
-
const str = data.toString();
|
|
897
|
-
stderr += str;
|
|
898
|
-
if (options.stream) process.stderr.write(data);
|
|
899
|
-
});
|
|
900
|
-
proc.on("close", (code) => {
|
|
901
|
-
resolve({
|
|
902
|
-
code: code ?? 1,
|
|
903
|
-
stdout,
|
|
904
|
-
stderr
|
|
905
|
-
});
|
|
906
|
-
});
|
|
907
|
-
});
|
|
872
|
+
function detectPackageManager(cwd) {
|
|
873
|
+
for (const [file, pm] of Object.entries({
|
|
874
|
+
"bun.lock": "bun",
|
|
875
|
+
"bun.lockb": "bun",
|
|
876
|
+
"package-lock.json": "npm",
|
|
877
|
+
"yarn.lock": "yarn",
|
|
878
|
+
"pnpm-lock.yaml": "pnpm"
|
|
879
|
+
})) try {
|
|
880
|
+
accessSync$1(join$1(cwd, file), constants$2.F_OK);
|
|
881
|
+
return pm;
|
|
882
|
+
} catch {}
|
|
883
|
+
return "npm";
|
|
908
884
|
}
|
|
909
885
|
/**
|
|
910
|
-
*
|
|
886
|
+
* Get the install command for a package manager
|
|
887
|
+
*
|
|
888
|
+
* @param pm - Package manager
|
|
889
|
+
* @param packageName - Package to install
|
|
890
|
+
* @param options - Install options
|
|
891
|
+
* @returns The full install command
|
|
911
892
|
*/
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
"
|
|
916
|
-
"--
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
return JSON.parse(result.stdout);
|
|
921
|
-
} catch {
|
|
922
|
-
return null;
|
|
893
|
+
function getInstallCommand(pm, packageName, options = {}) {
|
|
894
|
+
const devFlag = options.dev ? " -D" : "";
|
|
895
|
+
switch (pm) {
|
|
896
|
+
case "bun": return `bun add${devFlag} ${packageName}`;
|
|
897
|
+
case "yarn": return `yarn add${options.dev ? " --dev" : ""} ${packageName}`;
|
|
898
|
+
case "pnpm": return `pnpm add${devFlag} ${packageName}`;
|
|
899
|
+
case "npm":
|
|
900
|
+
default: return `npm install${options.dev ? " --save-dev" : ""} ${packageName}`;
|
|
923
901
|
}
|
|
924
902
|
}
|
|
903
|
+
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region src/commands/infra/provider-loader.ts
|
|
906
|
+
const KUCKIT_DIR$7 = ".kuckit";
|
|
907
|
+
const CONFIG_FILE$7 = "infra.json";
|
|
925
908
|
/**
|
|
926
|
-
*
|
|
909
|
+
* Default provider packages by provider ID
|
|
927
910
|
*/
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
return {
|
|
934
|
-
name: data.name ?? "",
|
|
935
|
-
current: data.current ?? false,
|
|
936
|
-
updateInProgress: data.updateInProgress ?? false,
|
|
937
|
-
resourceCount: data.resourceCount ?? 0,
|
|
938
|
-
lastUpdate: data.lastUpdate,
|
|
939
|
-
url: data.url
|
|
940
|
-
};
|
|
941
|
-
} catch {
|
|
942
|
-
return null;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
911
|
+
const DEFAULT_PROVIDER_PACKAGES = {
|
|
912
|
+
gcp: "@kuckit/infra-gcp",
|
|
913
|
+
aws: "@kuckit/infra-aws",
|
|
914
|
+
azure: "@kuckit/infra-azure"
|
|
915
|
+
};
|
|
945
916
|
/**
|
|
946
|
-
*
|
|
917
|
+
* Load the stored infrastructure config from .kuckit/infra.json
|
|
947
918
|
*/
|
|
948
|
-
async function
|
|
949
|
-
const
|
|
950
|
-
"stack",
|
|
951
|
-
"history",
|
|
952
|
-
"--json",
|
|
953
|
-
"--show-secrets=false"
|
|
954
|
-
], options);
|
|
955
|
-
if (result.code !== 0) return [];
|
|
919
|
+
async function loadStoredConfig(projectRoot) {
|
|
920
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$7, CONFIG_FILE$7);
|
|
956
921
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
endTime: entry.endTime ?? "",
|
|
963
|
-
result: entry.result ?? "failed",
|
|
964
|
-
resourceChanges: entry.resourceChanges
|
|
965
|
-
}));
|
|
922
|
+
await access(configPath, constants$1.F_OK);
|
|
923
|
+
const content = await readFile(configPath, "utf-8");
|
|
924
|
+
const parsed = JSON.parse(content);
|
|
925
|
+
if (isLegacyConfig(parsed)) return migrateLegacyConfig(parsed);
|
|
926
|
+
return parsed;
|
|
966
927
|
} catch {
|
|
967
|
-
return
|
|
928
|
+
return null;
|
|
968
929
|
}
|
|
969
930
|
}
|
|
970
931
|
/**
|
|
971
|
-
*
|
|
972
|
-
*/
|
|
973
|
-
async function selectOrCreateStack(stackName, options) {
|
|
974
|
-
if ((await runPulumi([
|
|
975
|
-
"stack",
|
|
976
|
-
"select",
|
|
977
|
-
stackName
|
|
978
|
-
], options)).code === 0) return true;
|
|
979
|
-
return (await runPulumi([
|
|
980
|
-
"stack",
|
|
981
|
-
"init",
|
|
982
|
-
stackName
|
|
983
|
-
], options)).code === 0;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Set a Pulumi config value
|
|
987
|
-
*/
|
|
988
|
-
async function setPulumiConfig(key, value, options) {
|
|
989
|
-
return (await runPulumi([
|
|
990
|
-
"config",
|
|
991
|
-
"set",
|
|
992
|
-
key,
|
|
993
|
-
value
|
|
994
|
-
], options)).code === 0;
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Run pulumi up with optional preview mode
|
|
998
|
-
*/
|
|
999
|
-
async function pulumiUp(options) {
|
|
1000
|
-
return runPulumi(options.preview ? ["preview"] : ["up", "--yes"], {
|
|
1001
|
-
...options,
|
|
1002
|
-
stream: true
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
/**
|
|
1006
|
-
* Run pulumi destroy
|
|
1007
|
-
*/
|
|
1008
|
-
async function pulumiDestroy(options) {
|
|
1009
|
-
return runPulumi(options.force ? [
|
|
1010
|
-
"destroy",
|
|
1011
|
-
"--yes",
|
|
1012
|
-
"-f"
|
|
1013
|
-
] : ["destroy", "--yes"], {
|
|
1014
|
-
...options,
|
|
1015
|
-
stream: true
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Run pulumi cancel to cancel stuck operations
|
|
1020
|
-
*/
|
|
1021
|
-
async function pulumiCancel(options) {
|
|
1022
|
-
return runPulumi(["cancel", "--yes"], options);
|
|
1023
|
-
}
|
|
1024
|
-
/**
|
|
1025
|
-
* Run pulumi refresh to sync state with cloud provider
|
|
932
|
+
* Save infrastructure config to .kuckit/infra.json
|
|
1026
933
|
*/
|
|
1027
|
-
async function
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
934
|
+
async function saveStoredConfig(projectRoot, config) {
|
|
935
|
+
const { mkdir: mkdir$1, writeFile: writeFile$1 } = await import("fs/promises");
|
|
936
|
+
const kuckitDir = join$1(projectRoot, KUCKIT_DIR$7);
|
|
937
|
+
await mkdir$1(kuckitDir, { recursive: true });
|
|
938
|
+
const configPath = join$1(kuckitDir, CONFIG_FILE$7);
|
|
939
|
+
config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
940
|
+
await writeFile$1(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
1032
941
|
}
|
|
1033
942
|
/**
|
|
1034
|
-
*
|
|
943
|
+
* Error thrown when provider package is not installed
|
|
1035
944
|
*/
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}
|
|
945
|
+
var ProviderNotInstalledError = class extends Error {
|
|
946
|
+
constructor(packageName, installCommand) {
|
|
947
|
+
super(`Provider package '${packageName}' is not installed.`);
|
|
948
|
+
this.packageName = packageName;
|
|
949
|
+
this.installCommand = installCommand;
|
|
950
|
+
this.name = "ProviderNotInstalledError";
|
|
951
|
+
}
|
|
952
|
+
};
|
|
1044
953
|
/**
|
|
1045
|
-
*
|
|
954
|
+
* Error thrown when provider package is invalid
|
|
1046
955
|
*/
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
"pipe"
|
|
1055
|
-
] : [
|
|
1056
|
-
"pipe",
|
|
1057
|
-
"pipe",
|
|
1058
|
-
"pipe"
|
|
1059
|
-
],
|
|
1060
|
-
shell: true
|
|
1061
|
-
});
|
|
1062
|
-
let stdout = "";
|
|
1063
|
-
let stderr = "";
|
|
1064
|
-
proc.stdout?.on("data", (data) => {
|
|
1065
|
-
const str = data.toString();
|
|
1066
|
-
stdout += str;
|
|
1067
|
-
if (options.stream) process.stdout.write(data);
|
|
1068
|
-
});
|
|
1069
|
-
proc.stderr?.on("data", (data) => {
|
|
1070
|
-
const str = data.toString();
|
|
1071
|
-
stderr += str;
|
|
1072
|
-
if (options.stream) process.stderr.write(data);
|
|
1073
|
-
});
|
|
1074
|
-
proc.on("close", (code) => {
|
|
1075
|
-
resolve({
|
|
1076
|
-
code: code ?? 1,
|
|
1077
|
-
stdout,
|
|
1078
|
-
stderr
|
|
1079
|
-
});
|
|
1080
|
-
});
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
956
|
+
var InvalidProviderError = class extends Error {
|
|
957
|
+
constructor(packageName, reason) {
|
|
958
|
+
super(`Package '${packageName}' is not a valid Kuckit infrastructure provider: ${reason}`);
|
|
959
|
+
this.packageName = packageName;
|
|
960
|
+
this.name = "InvalidProviderError";
|
|
961
|
+
}
|
|
962
|
+
};
|
|
1083
963
|
/**
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
964
|
+
* Load a provider from an npm package
|
|
965
|
+
*
|
|
966
|
+
* @param packageName - npm package name (e.g., '@kuckit/infra-gcp')
|
|
967
|
+
* @param projectRoot - Optional project root for package manager detection
|
|
968
|
+
* @returns The provider instance
|
|
969
|
+
* @throws ProviderNotInstalledError if package not found
|
|
970
|
+
* @throws InvalidProviderError if package doesn't export a valid provider
|
|
1086
971
|
*/
|
|
1087
|
-
async function
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
"
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
"
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (options.dockerfile && options.dockerfile !== "Dockerfile") args.push("--config", options.dockerfile);
|
|
1100
|
-
return {
|
|
1101
|
-
code: (await runGcloud(args, {
|
|
1102
|
-
stream: true,
|
|
1103
|
-
cwd: options.cwd
|
|
1104
|
-
})).code,
|
|
1105
|
-
imageUrl
|
|
1106
|
-
};
|
|
972
|
+
async function loadProviderFromPackage(packageName, projectRoot) {
|
|
973
|
+
try {
|
|
974
|
+
const module = await import(packageName);
|
|
975
|
+
if (!module.provider) throw new InvalidProviderError(packageName, "does not export a 'provider' object");
|
|
976
|
+
const provider = module.provider;
|
|
977
|
+
if (!provider.id || !provider.label || !provider.getInfraDir || !provider.init || !provider.deploy) throw new InvalidProviderError(packageName, "missing required methods (id, label, getInfraDir, init, deploy)");
|
|
978
|
+
return provider;
|
|
979
|
+
} catch (error) {
|
|
980
|
+
if (error instanceof ProviderNotInstalledError || error instanceof InvalidProviderError) throw error;
|
|
981
|
+
if (error instanceof Error && (error.message.includes("Cannot find module") || error.message.includes("Cannot find package"))) throw new ProviderNotInstalledError(packageName, getInstallCommand(detectPackageManager(projectRoot ?? process.cwd()), packageName, { dev: true }));
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
1107
984
|
}
|
|
1108
985
|
/**
|
|
1109
|
-
* Get the
|
|
986
|
+
* Get the provider package name for a given provider ID
|
|
1110
987
|
*/
|
|
1111
|
-
function
|
|
1112
|
-
return
|
|
988
|
+
function getProviderPackage(providerId) {
|
|
989
|
+
return DEFAULT_PROVIDER_PACKAGES[providerId] ?? `@kuckit/infra-${providerId}`;
|
|
1113
990
|
}
|
|
1114
991
|
/**
|
|
1115
|
-
* Check if
|
|
992
|
+
* Check if a provider package is available
|
|
1116
993
|
*/
|
|
1117
|
-
async function
|
|
994
|
+
async function isProviderAvailable(providerId) {
|
|
995
|
+
const packageName = getProviderPackage(providerId);
|
|
1118
996
|
try {
|
|
1119
|
-
|
|
997
|
+
await import(packageName);
|
|
998
|
+
return true;
|
|
1120
999
|
} catch {
|
|
1121
1000
|
return false;
|
|
1122
1001
|
}
|
|
1123
1002
|
}
|
|
1124
1003
|
/**
|
|
1125
|
-
*
|
|
1004
|
+
* List available provider packages
|
|
1126
1005
|
*/
|
|
1127
|
-
async function
|
|
1006
|
+
async function listAvailableProviders() {
|
|
1007
|
+
return await Promise.all(Object.entries(DEFAULT_PROVIDER_PACKAGES).map(async ([id, pkg]) => ({
|
|
1008
|
+
id,
|
|
1009
|
+
package: pkg,
|
|
1010
|
+
available: await isProviderAvailable(id)
|
|
1011
|
+
})));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/commands/infra/init.ts
|
|
1016
|
+
async function fileExists$10(path) {
|
|
1128
1017
|
try {
|
|
1129
|
-
|
|
1018
|
+
await access(path, constants$1.F_OK);
|
|
1019
|
+
return true;
|
|
1130
1020
|
} catch {
|
|
1131
1021
|
return false;
|
|
1132
1022
|
}
|
|
1133
1023
|
}
|
|
1024
|
+
async function findProjectRoot$10(cwd) {
|
|
1025
|
+
let dir = cwd;
|
|
1026
|
+
while (dir !== dirname$1(dir)) {
|
|
1027
|
+
if (await fileExists$10(join$1(dir, "package.json"))) return dir;
|
|
1028
|
+
dir = dirname$1(dir);
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1134
1032
|
/**
|
|
1135
|
-
*
|
|
1033
|
+
* Check if legacy packages/infra directory exists
|
|
1136
1034
|
*/
|
|
1137
|
-
async function
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
"
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
"
|
|
1149
|
-
"
|
|
1150
|
-
|
|
1035
|
+
async function hasLegacyInfra(projectRoot) {
|
|
1036
|
+
return fileExists$10(join$1(projectRoot, "packages", "infra", "package.json"));
|
|
1037
|
+
}
|
|
1038
|
+
async function infraInit(options) {
|
|
1039
|
+
console.log("Initializing kuckit infrastructure...\n");
|
|
1040
|
+
const projectRoot = await findProjectRoot$10(process.cwd());
|
|
1041
|
+
if (!projectRoot) {
|
|
1042
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
if (await hasLegacyInfra(projectRoot)) {
|
|
1046
|
+
console.log("⚠️ Legacy infrastructure detected: packages/infra");
|
|
1047
|
+
console.log("");
|
|
1048
|
+
console.log("Kuckit now uses provider packages (@kuckit/infra-gcp) instead of");
|
|
1049
|
+
console.log("local Pulumi code. Your existing Pulumi state will be preserved.");
|
|
1050
|
+
console.log("");
|
|
1051
|
+
console.log("After migration, you can remove packages/infra.");
|
|
1052
|
+
console.log("See docs/MIGRATION.md for details.");
|
|
1053
|
+
console.log("");
|
|
1054
|
+
}
|
|
1055
|
+
const existingConfig = await loadStoredConfig(projectRoot);
|
|
1056
|
+
let providerId = options.provider ?? existingConfig?.provider ?? "gcp";
|
|
1057
|
+
if (!options.provider && !existingConfig) {
|
|
1058
|
+
const installedProviders = (await listAvailableProviders()).filter((p) => p.available);
|
|
1059
|
+
if (installedProviders.length === 0) {
|
|
1060
|
+
const installCmd = getInstallCommand(detectPackageManager(projectRoot), "@kuckit/infra-gcp", { dev: true });
|
|
1061
|
+
console.error("Error: No infrastructure providers installed.");
|
|
1062
|
+
console.error("");
|
|
1063
|
+
console.error("Kuckit requires a cloud provider package to deploy infrastructure.");
|
|
1064
|
+
console.error("Install a provider to get started:");
|
|
1065
|
+
console.error("");
|
|
1066
|
+
console.error(` ${installCmd}`);
|
|
1067
|
+
console.error("");
|
|
1068
|
+
console.error("Available providers:");
|
|
1069
|
+
console.error(" @kuckit/infra-gcp - Google Cloud Platform (Cloud Run, Cloud SQL, Redis)");
|
|
1070
|
+
console.error(" @kuckit/infra-aws - Amazon Web Services (coming soon)");
|
|
1071
|
+
console.error(" @kuckit/infra-azure - Microsoft Azure (coming soon)");
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
providerId = installedProviders[0].id;
|
|
1075
|
+
}
|
|
1076
|
+
const providerPackage = getProviderPackage(providerId);
|
|
1077
|
+
let provider;
|
|
1078
|
+
try {
|
|
1079
|
+
provider = await loadProviderFromPackage(providerPackage, projectRoot);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
if (error instanceof ProviderNotInstalledError) {
|
|
1082
|
+
console.error(`Error: ${error.message}`);
|
|
1083
|
+
console.error("");
|
|
1084
|
+
console.error("Install the provider package:");
|
|
1085
|
+
console.error(` ${error.installCommand}`);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
if (error instanceof InvalidProviderError) {
|
|
1089
|
+
console.error(`Error: ${error.message}`);
|
|
1090
|
+
console.error("");
|
|
1091
|
+
console.error("This may be a corrupted or incompatible package version.");
|
|
1092
|
+
console.error("Try reinstalling the provider package.");
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Failed to load provider"}`);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
console.log("Checking prerequisites...");
|
|
1099
|
+
const prereqResult = await provider.checkPrerequisites();
|
|
1100
|
+
if (!prereqResult.ok) {
|
|
1101
|
+
console.error("\nError: Missing required tools:");
|
|
1102
|
+
for (const missing of prereqResult.missing) {
|
|
1103
|
+
console.error(` - ${missing.name}`);
|
|
1104
|
+
console.error(` Install from: ${missing.installUrl}`);
|
|
1105
|
+
}
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
if (provider.validateProject) {
|
|
1109
|
+
const validation = await provider.validateProject(projectRoot);
|
|
1110
|
+
if (!validation.valid) {
|
|
1111
|
+
console.error("\nError: Project validation failed:");
|
|
1112
|
+
for (const issue of validation.issues.filter((i) => i.severity === "error")) console.error(` - ${issue.message}`);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
for (const issue of validation.issues.filter((i) => i.severity === "warning")) console.warn(`Warning: ${issue.message}`);
|
|
1116
|
+
}
|
|
1117
|
+
const prompts = await provider.getInitPrompts(existingConfig ?? void 0);
|
|
1118
|
+
const responses = {};
|
|
1119
|
+
if (options.project && providerId === "gcp") responses.gcpProject = options.project;
|
|
1120
|
+
for (const prompt of prompts) {
|
|
1121
|
+
if (responses[prompt.name] !== void 0) continue;
|
|
1122
|
+
if (prompt.type === "input") responses[prompt.name] = await input({
|
|
1123
|
+
message: prompt.message,
|
|
1124
|
+
default: prompt.default,
|
|
1125
|
+
validate: prompt.validate
|
|
1126
|
+
});
|
|
1127
|
+
else if (prompt.type === "select" && prompt.choices) responses[prompt.name] = await select({
|
|
1128
|
+
message: prompt.message,
|
|
1129
|
+
choices: prompt.choices.map((c) => ({
|
|
1130
|
+
value: c.value,
|
|
1131
|
+
name: c.label
|
|
1132
|
+
})),
|
|
1133
|
+
default: prompt.default
|
|
1134
|
+
});
|
|
1135
|
+
else if (prompt.type === "confirm") responses[prompt.name] = await confirm({
|
|
1136
|
+
message: prompt.message,
|
|
1137
|
+
default: prompt.default
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
let region = options.region ?? existingConfig?.region ?? "us-central1";
|
|
1141
|
+
if (!options.region && !existingConfig?.region) region = await input({
|
|
1142
|
+
message: "Deployment region:",
|
|
1143
|
+
default: "us-central1"
|
|
1144
|
+
});
|
|
1145
|
+
let env = options.env ?? existingConfig?.env ?? "dev";
|
|
1146
|
+
if (!options.env && !existingConfig?.env) env = await select({
|
|
1147
|
+
message: "Environment:",
|
|
1148
|
+
choices: [
|
|
1149
|
+
{
|
|
1150
|
+
value: "dev",
|
|
1151
|
+
name: "Development"
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
value: "staging",
|
|
1155
|
+
name: "Staging"
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
value: "prod",
|
|
1159
|
+
name: "Production"
|
|
1160
|
+
}
|
|
1161
|
+
],
|
|
1162
|
+
default: "dev"
|
|
1163
|
+
});
|
|
1164
|
+
const packageJsonPath = join$1(projectRoot, "package.json");
|
|
1165
|
+
let projectName = "kuckit-app";
|
|
1166
|
+
try {
|
|
1167
|
+
const { readFile: readFile$1 } = await import("fs/promises");
|
|
1168
|
+
projectName = JSON.parse(await readFile$1(packageJsonPath, "utf-8")).name?.replace(/^@[^/]+\//, "") ?? "kuckit-app";
|
|
1169
|
+
} catch {
|
|
1170
|
+
projectName = projectRoot.split("/").pop() ?? "kuckit-app";
|
|
1171
|
+
}
|
|
1172
|
+
const stackName = `${responses.gcpProject ?? ""}-${env}`;
|
|
1173
|
+
console.log("\nConfiguration:");
|
|
1174
|
+
console.log(` Provider: ${provider.label}`);
|
|
1175
|
+
for (const [key, value] of Object.entries(responses)) console.log(` ${key}: ${value}`);
|
|
1176
|
+
console.log(` Region: ${region}`);
|
|
1177
|
+
console.log(` Environment: ${env}`);
|
|
1178
|
+
console.log(` Stack: ${stackName}`);
|
|
1179
|
+
console.log("");
|
|
1180
|
+
if (!options.yes) {
|
|
1181
|
+
if (!await confirm({
|
|
1182
|
+
message: "Proceed with infrastructure initialization?",
|
|
1183
|
+
default: true
|
|
1184
|
+
})) {
|
|
1185
|
+
console.log("Aborted.");
|
|
1186
|
+
process.exit(0);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const dockerfilePath = join$1(projectRoot, "Dockerfile");
|
|
1190
|
+
if (provider.generateDockerfile && !await fileExists$10(dockerfilePath)) {
|
|
1191
|
+
console.log("\nGenerating Dockerfile...");
|
|
1192
|
+
const dockerfileContent = await provider.generateDockerfile(projectRoot);
|
|
1193
|
+
if (dockerfileContent) {
|
|
1194
|
+
const { writeFile: writeFile$1 } = await import("fs/promises");
|
|
1195
|
+
await writeFile$1(dockerfilePath, dockerfileContent, "utf-8");
|
|
1196
|
+
console.log("Dockerfile created.");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
console.log("\nInitializing infrastructure...");
|
|
1200
|
+
console.log("This may take several minutes.\n");
|
|
1201
|
+
const result = await provider.init({
|
|
1202
|
+
env,
|
|
1203
|
+
region,
|
|
1204
|
+
projectRoot,
|
|
1205
|
+
yes: options.yes,
|
|
1206
|
+
providerOptions: responses
|
|
1207
|
+
});
|
|
1208
|
+
if (!result.success) {
|
|
1209
|
+
console.error(`\nError: ${result.message}`);
|
|
1210
|
+
if (result.error) console.error(` ${result.error.code}: ${result.error.message}`);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
const baseConfig = {
|
|
1214
|
+
env,
|
|
1215
|
+
region,
|
|
1216
|
+
projectName,
|
|
1217
|
+
stackName
|
|
1218
|
+
};
|
|
1219
|
+
await saveStoredConfig(projectRoot, {
|
|
1220
|
+
...provider.buildConfig(responses, baseConfig),
|
|
1221
|
+
providerPackage,
|
|
1222
|
+
outputs: result.outputs,
|
|
1223
|
+
createdAt: existingConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1224
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1225
|
+
});
|
|
1226
|
+
console.log("\nConfiguration saved to .kuckit/infra.json");
|
|
1227
|
+
console.log("\n" + "=".repeat(60));
|
|
1228
|
+
console.log("Infrastructure initialized successfully!");
|
|
1229
|
+
console.log("=".repeat(60));
|
|
1230
|
+
if (result.outputs) {
|
|
1231
|
+
console.log("\nOutputs:");
|
|
1232
|
+
const outputs = result.outputs;
|
|
1233
|
+
if (outputs.registryUrl) console.log(` Registry: ${outputs.registryUrl}`);
|
|
1234
|
+
if (outputs.databaseConnectionName) console.log(` Database: ${outputs.databaseConnectionName}`);
|
|
1235
|
+
if (outputs.redisHost) console.log(` Redis: ${outputs.redisHost}`);
|
|
1236
|
+
}
|
|
1237
|
+
console.log("\nNext steps:");
|
|
1238
|
+
console.log(" 1. Ensure you have a Dockerfile in your project root");
|
|
1239
|
+
console.log(` 2. Run: kuckit infra deploy --env ${env}`);
|
|
1240
|
+
console.log("");
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region src/commands/infra/deploy.ts
|
|
1245
|
+
async function fileExists$9(path) {
|
|
1246
|
+
try {
|
|
1247
|
+
await access(path, constants$1.F_OK);
|
|
1248
|
+
return true;
|
|
1249
|
+
} catch {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async function findProjectRoot$9(cwd) {
|
|
1254
|
+
let dir = cwd;
|
|
1255
|
+
while (dir !== dirname$1(dir)) {
|
|
1256
|
+
if (await fileExists$9(join$1(dir, "package.json"))) return dir;
|
|
1257
|
+
dir = dirname$1(dir);
|
|
1258
|
+
}
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
async function infraDeploy(options) {
|
|
1262
|
+
console.log("Deploying kuckit application...\n");
|
|
1263
|
+
const projectRoot = await findProjectRoot$9(process.cwd());
|
|
1264
|
+
if (!projectRoot) {
|
|
1265
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
}
|
|
1268
|
+
const config = await loadStoredConfig(projectRoot);
|
|
1269
|
+
if (!config) {
|
|
1270
|
+
console.error("Error: No infrastructure configuration found.");
|
|
1271
|
+
console.error("Run: kuckit infra init");
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
const env = options.env ?? config.env;
|
|
1275
|
+
if (env !== "dev" && env !== "staging" && env !== "prod") {
|
|
1276
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev', 'staging', or 'prod'.`);
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
const providerPackage = config.providerPackage ?? getProviderPackage(config.provider);
|
|
1280
|
+
let provider;
|
|
1281
|
+
try {
|
|
1282
|
+
provider = await loadProviderFromPackage(providerPackage, projectRoot);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
if (error instanceof ProviderNotInstalledError) {
|
|
1285
|
+
console.error(`Error: ${error.message}`);
|
|
1286
|
+
console.error("");
|
|
1287
|
+
console.error("Install the provider package:");
|
|
1288
|
+
console.error(` ${error.installCommand}`);
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
}
|
|
1291
|
+
if (error instanceof InvalidProviderError) {
|
|
1292
|
+
console.error(`Error: ${error.message}`);
|
|
1293
|
+
console.error("");
|
|
1294
|
+
console.error("This may be a corrupted or incompatible package version.");
|
|
1295
|
+
console.error("Try reinstalling the provider package.");
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Failed to load provider"}`);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
const prereqResult = await provider.checkPrerequisites();
|
|
1302
|
+
if (!prereqResult.ok) {
|
|
1303
|
+
console.error("\nError: Missing required tools:");
|
|
1304
|
+
for (const missing of prereqResult.missing) {
|
|
1305
|
+
console.error(` - ${missing.name}`);
|
|
1306
|
+
console.error(` Install from: ${missing.installUrl}`);
|
|
1307
|
+
}
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
const dockerfilePath = join$1(projectRoot, "Dockerfile");
|
|
1311
|
+
if (!options.skipBuild && !options.image && !await fileExists$9(dockerfilePath)) if (provider.generateDockerfile) {
|
|
1312
|
+
console.log("Generating Dockerfile...");
|
|
1313
|
+
const dockerfileContent = await provider.generateDockerfile(projectRoot);
|
|
1314
|
+
if (dockerfileContent) {
|
|
1315
|
+
const { writeFile: writeFile$1 } = await import("fs/promises");
|
|
1316
|
+
await writeFile$1(dockerfilePath, dockerfileContent, "utf-8");
|
|
1317
|
+
console.log("Dockerfile created.\n");
|
|
1318
|
+
} else {
|
|
1319
|
+
console.error("Error: Dockerfile not found in project root.");
|
|
1320
|
+
console.error("Create a Dockerfile or use --skip-build with an existing image.");
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
} else {
|
|
1324
|
+
console.error("Error: Dockerfile not found in project root.");
|
|
1325
|
+
console.error("Create a Dockerfile or use --skip-build with an existing image.");
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
if (!config.outputs?.registryUrl) {
|
|
1329
|
+
console.error("Error: No registry URL found in configuration.");
|
|
1330
|
+
console.error("Run: kuckit infra init");
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
let projectDisplay = config.projectName;
|
|
1334
|
+
if (isGcpConfig(config)) projectDisplay = config.providerConfig.gcpProject;
|
|
1335
|
+
console.log("Configuration:");
|
|
1336
|
+
console.log(` Provider: ${provider.label}`);
|
|
1337
|
+
console.log(` Project: ${projectDisplay}`);
|
|
1338
|
+
console.log(` Region: ${config.region}`);
|
|
1339
|
+
console.log(` Environment: ${env}`);
|
|
1340
|
+
console.log(` Registry: ${config.outputs.registryUrl}`);
|
|
1341
|
+
if (options.preview) console.log(" Mode: Preview (no changes will be applied)");
|
|
1342
|
+
console.log("");
|
|
1343
|
+
if (!options.yes && !options.preview) {
|
|
1344
|
+
if (!await confirm({
|
|
1345
|
+
message: "Proceed with deployment?",
|
|
1346
|
+
default: true
|
|
1347
|
+
})) {
|
|
1348
|
+
console.log("Aborted.");
|
|
1349
|
+
process.exit(0);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const result = await provider.deploy({
|
|
1353
|
+
env,
|
|
1354
|
+
projectRoot,
|
|
1355
|
+
preview: options.preview,
|
|
1356
|
+
skipBuild: options.skipBuild,
|
|
1357
|
+
image: options.image,
|
|
1358
|
+
yes: options.yes
|
|
1359
|
+
}, config);
|
|
1360
|
+
if (!result.success) {
|
|
1361
|
+
console.error(`\nError: ${result.message}`);
|
|
1362
|
+
if (result.error) console.error(` ${result.error.code}: ${result.error.message}`);
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
if (!options.preview && result.outputs) await saveStoredConfig(projectRoot, {
|
|
1366
|
+
...config,
|
|
1367
|
+
outputs: result.outputs,
|
|
1368
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1369
|
+
});
|
|
1370
|
+
if (!options.preview) {
|
|
1371
|
+
console.log("\n" + "=".repeat(60));
|
|
1372
|
+
console.log("Deployment successful!");
|
|
1373
|
+
console.log("=".repeat(60));
|
|
1374
|
+
if (result.outputs) {
|
|
1375
|
+
const outputs = result.outputs;
|
|
1376
|
+
if (outputs.serviceUrl) console.log(`\nService URL: ${outputs.serviceUrl}`);
|
|
1377
|
+
if (outputs.migrationJobName) console.log(`Migration Job: ${outputs.migrationJobName}`);
|
|
1378
|
+
if (outputs.customDomain) {
|
|
1379
|
+
console.log(`\nCustom Domain: ${outputs.customDomain}`);
|
|
1380
|
+
console.log(`Status: ${outputs.customDomainStatus ?? "UNKNOWN"}`);
|
|
1381
|
+
const records = outputs.customDomainRecords;
|
|
1382
|
+
if (records && records.length > 0) {
|
|
1383
|
+
console.log("\nRequired DNS Records:");
|
|
1384
|
+
for (const record of records) console.log(` ${record.type} ${record.name} → ${record.rrdata}`);
|
|
1385
|
+
console.log("\nNote: If using Cloudflare:");
|
|
1386
|
+
console.log(" 1. Set DNS records to DNS-only (gray cloud) until status is READY");
|
|
1387
|
+
console.log(" 2. Certificate provisioning can take 15-60 minutes");
|
|
1388
|
+
console.log(" 3. Once READY, you can enable Cloudflare proxy (orange cloud)");
|
|
1389
|
+
console.log(" 4. Ensure Cloudflare SSL mode is set to \"Full (strict)\"");
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
console.log("\nUseful commands:");
|
|
1394
|
+
console.log(` View logs: kuckit infra logs --env ${env}`);
|
|
1395
|
+
console.log(` Check status: kuckit infra status --env ${env}`);
|
|
1396
|
+
console.log(` Run migrations: kuckit infra db:migrate --env ${env}`);
|
|
1397
|
+
console.log("");
|
|
1398
|
+
} else {
|
|
1399
|
+
console.log("\nPreview complete. No changes were applied.");
|
|
1400
|
+
console.log("Run without --preview to apply changes.");
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
//#endregion
|
|
1405
|
+
//#region src/commands/infra/up.ts
|
|
1406
|
+
async function fileExists$8(path) {
|
|
1407
|
+
try {
|
|
1408
|
+
await access(path, constants$1.F_OK);
|
|
1409
|
+
return true;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async function findProjectRoot$8(cwd) {
|
|
1415
|
+
let dir = cwd;
|
|
1416
|
+
while (dir !== dirname$1(dir)) {
|
|
1417
|
+
if (await fileExists$8(join$1(dir, "package.json"))) return dir;
|
|
1418
|
+
dir = dirname$1(dir);
|
|
1419
|
+
}
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
async function infraUp(options) {
|
|
1423
|
+
console.log("🚀 Kuckit Infrastructure Up\n");
|
|
1424
|
+
const projectRoot = await findProjectRoot$8(process.cwd());
|
|
1425
|
+
if (!projectRoot) {
|
|
1426
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
}
|
|
1429
|
+
const existingConfig = await loadStoredConfig(projectRoot);
|
|
1430
|
+
if (!existingConfig) {
|
|
1431
|
+
const providerPackage = getProviderPackage(options.provider ?? "gcp");
|
|
1432
|
+
let providerLabel = "cloud";
|
|
1433
|
+
try {
|
|
1434
|
+
providerLabel = (await loadProviderFromPackage(providerPackage, projectRoot)).label;
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
if (error instanceof ProviderNotInstalledError) {
|
|
1437
|
+
console.error(`Error: ${error.message}`);
|
|
1438
|
+
console.error("");
|
|
1439
|
+
console.error("Install the provider package first:");
|
|
1440
|
+
console.error(` ${error.installCommand}`);
|
|
1441
|
+
console.error("");
|
|
1442
|
+
console.error("Then run this command again.");
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
console.log("No infrastructure configuration found. Starting initialization...\n");
|
|
1447
|
+
if (!options.yes) {
|
|
1448
|
+
if (!await confirm({
|
|
1449
|
+
message: `This will create ${providerLabel} infrastructure (VPC, Database, Redis, Registry). Continue?`,
|
|
1450
|
+
default: true
|
|
1451
|
+
})) {
|
|
1452
|
+
console.log("Aborted.");
|
|
1453
|
+
process.exit(0);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
await infraInit({
|
|
1457
|
+
provider: options.provider,
|
|
1458
|
+
project: options.project,
|
|
1459
|
+
region: options.region,
|
|
1460
|
+
env: options.env,
|
|
1461
|
+
yes: options.yes
|
|
1462
|
+
});
|
|
1463
|
+
if (!await loadStoredConfig(projectRoot)) {
|
|
1464
|
+
console.error("Error: Initialization completed but no configuration found.");
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
console.log("\n" + "─".repeat(60) + "\n");
|
|
1468
|
+
console.log("Infrastructure initialized! Proceeding to deployment...\n");
|
|
1469
|
+
} else {
|
|
1470
|
+
console.log(`Using existing configuration for ${existingConfig.env} environment.`);
|
|
1471
|
+
console.log(`Provider: ${existingConfig.provider}`);
|
|
1472
|
+
console.log(`Region: ${existingConfig.region}\n`);
|
|
1473
|
+
}
|
|
1474
|
+
await infraDeploy({
|
|
1475
|
+
env: options.env ?? existingConfig?.env ?? "dev",
|
|
1476
|
+
preview: options.preview,
|
|
1477
|
+
yes: options.yes
|
|
1478
|
+
});
|
|
1479
|
+
console.log("\n" + "═".repeat(60));
|
|
1480
|
+
console.log("✅ Infrastructure up and running!");
|
|
1481
|
+
console.log("═".repeat(60));
|
|
1482
|
+
console.log("\nUseful commands:");
|
|
1483
|
+
console.log(` kuckit infra status - Check current status`);
|
|
1484
|
+
console.log(` kuckit infra logs - View application logs`);
|
|
1485
|
+
console.log(` kuckit infra outputs - Show service URLs and secrets`);
|
|
1486
|
+
console.log("");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/commands/infra/eject.ts
|
|
1491
|
+
async function fileExists$7(path) {
|
|
1492
|
+
try {
|
|
1493
|
+
await access(path, constants$1.F_OK);
|
|
1494
|
+
return true;
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async function findProjectRoot$7(cwd) {
|
|
1500
|
+
let dir = cwd;
|
|
1501
|
+
while (dir !== dirname$1(dir)) {
|
|
1502
|
+
if (await fileExists$7(join$1(dir, "package.json"))) return dir;
|
|
1503
|
+
dir = dirname$1(dir);
|
|
1504
|
+
}
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
async function infraEject(options) {
|
|
1508
|
+
console.log("Ejecting infrastructure code...\n");
|
|
1509
|
+
const projectRoot = await findProjectRoot$7(process.cwd());
|
|
1510
|
+
if (!projectRoot) {
|
|
1511
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1512
|
+
process.exit(1);
|
|
1513
|
+
}
|
|
1514
|
+
const config = await loadStoredConfig(projectRoot);
|
|
1515
|
+
if (!config) {
|
|
1516
|
+
console.error("Error: No infrastructure configuration found.");
|
|
1517
|
+
console.error("Run: kuckit infra init");
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
if (config.localInfraDir) {
|
|
1521
|
+
console.error(`Error: Infrastructure already ejected to ${config.localInfraDir}`);
|
|
1522
|
+
console.error("Remove localInfraDir from .kuckit/infra.json to re-eject.");
|
|
1523
|
+
process.exit(1);
|
|
1524
|
+
}
|
|
1525
|
+
const providerPackage = config.providerPackage ?? getProviderPackage(config.provider);
|
|
1526
|
+
let provider;
|
|
1527
|
+
try {
|
|
1528
|
+
provider = await loadProviderFromPackage(providerPackage, projectRoot);
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
if (error instanceof ProviderNotInstalledError) {
|
|
1531
|
+
console.error(`Error: ${error.message}`);
|
|
1532
|
+
console.error("");
|
|
1533
|
+
console.error("Install the provider package:");
|
|
1534
|
+
console.error(` ${error.installCommand}`);
|
|
1535
|
+
process.exit(1);
|
|
1536
|
+
}
|
|
1537
|
+
if (error instanceof InvalidProviderError) {
|
|
1538
|
+
console.error(`Error: ${error.message}`);
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
}
|
|
1541
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Failed to load provider"}`);
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
if (!provider.eject) {
|
|
1545
|
+
console.error(`Error: Provider '${provider.id}' does not support ejecting.`);
|
|
1546
|
+
process.exit(1);
|
|
1547
|
+
}
|
|
1548
|
+
const targetDir = options.dir ?? "infra";
|
|
1549
|
+
const absoluteTargetDir = join$1(projectRoot, targetDir);
|
|
1550
|
+
if (await fileExists$7(absoluteTargetDir)) {
|
|
1551
|
+
if (!options.force) {
|
|
1552
|
+
console.error(`Error: Directory '${targetDir}' already exists.`);
|
|
1553
|
+
console.error("Use --force to overwrite, or specify a different directory with --dir.");
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
console.log("Configuration:");
|
|
1558
|
+
console.log(` Provider: ${provider.label}`);
|
|
1559
|
+
console.log(` Target directory: ${targetDir}`);
|
|
1560
|
+
console.log("");
|
|
1561
|
+
if (!options.force) {
|
|
1562
|
+
console.log("This will:");
|
|
1563
|
+
console.log(` 1. Copy provider Pulumi code to ${targetDir}/`);
|
|
1564
|
+
console.log(" 2. Update .kuckit/infra.json to use local infrastructure");
|
|
1565
|
+
console.log(" 3. Future deployments will use your local copy");
|
|
1566
|
+
console.log("");
|
|
1567
|
+
if (!await confirm({
|
|
1568
|
+
message: "Proceed with eject?",
|
|
1569
|
+
default: true
|
|
1570
|
+
})) {
|
|
1571
|
+
console.log("Aborted.");
|
|
1572
|
+
process.exit(0);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const result = await provider.eject({
|
|
1576
|
+
targetDir: absoluteTargetDir,
|
|
1577
|
+
projectRoot,
|
|
1578
|
+
force: options.force
|
|
1579
|
+
});
|
|
1580
|
+
if (!result.success) {
|
|
1581
|
+
console.error(`\nError: ${result.message}`);
|
|
1582
|
+
if (result.error) console.error(` ${result.error.code}: ${result.error.message}`);
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
await saveStoredConfig(projectRoot, {
|
|
1586
|
+
...config,
|
|
1587
|
+
localInfraDir: targetDir,
|
|
1588
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1589
|
+
});
|
|
1590
|
+
console.log("\n" + "=".repeat(60));
|
|
1591
|
+
console.log("Infrastructure ejected successfully!");
|
|
1592
|
+
console.log("=".repeat(60));
|
|
1593
|
+
console.log(`\nYour infrastructure code is now in: ${targetDir}/`);
|
|
1594
|
+
console.log("");
|
|
1595
|
+
console.log("Next steps:");
|
|
1596
|
+
console.log(` 1. Review and customize the Pulumi code in ${targetDir}/`);
|
|
1597
|
+
console.log(" 2. Install Pulumi dependencies: cd " + targetDir + " && bun install");
|
|
1598
|
+
console.log(" 3. Deploy with: kuckit infra deploy");
|
|
1599
|
+
console.log("");
|
|
1600
|
+
console.log("The CLI will now use your local infrastructure instead of the provider package.");
|
|
1601
|
+
console.log("");
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
//#endregion
|
|
1605
|
+
//#region src/commands/infra/runner.ts
|
|
1606
|
+
/**
|
|
1607
|
+
* Run a Pulumi CLI command
|
|
1608
|
+
*/
|
|
1609
|
+
function runPulumi(args, options) {
|
|
1610
|
+
return new Promise((resolve) => {
|
|
1611
|
+
const proc = spawn("pulumi", args, {
|
|
1612
|
+
cwd: options.cwd,
|
|
1613
|
+
stdio: options.stream ? [
|
|
1614
|
+
"inherit",
|
|
1615
|
+
"pipe",
|
|
1616
|
+
"pipe"
|
|
1617
|
+
] : [
|
|
1618
|
+
"pipe",
|
|
1619
|
+
"pipe",
|
|
1620
|
+
"pipe"
|
|
1621
|
+
],
|
|
1622
|
+
shell: true,
|
|
1623
|
+
env: {
|
|
1624
|
+
...process.env,
|
|
1625
|
+
...options.env,
|
|
1626
|
+
PULUMI_SKIP_UPDATE_CHECK: "true"
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
let stdout = "";
|
|
1630
|
+
let stderr = "";
|
|
1631
|
+
proc.stdout?.on("data", (data) => {
|
|
1632
|
+
const str = data.toString();
|
|
1633
|
+
stdout += str;
|
|
1634
|
+
if (options.stream) process.stdout.write(data);
|
|
1635
|
+
});
|
|
1636
|
+
proc.stderr?.on("data", (data) => {
|
|
1637
|
+
const str = data.toString();
|
|
1638
|
+
stderr += str;
|
|
1639
|
+
if (options.stream) process.stderr.write(data);
|
|
1640
|
+
});
|
|
1641
|
+
proc.on("close", (code) => {
|
|
1642
|
+
resolve({
|
|
1643
|
+
code: code ?? 1,
|
|
1644
|
+
stdout,
|
|
1645
|
+
stderr
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Run pulumi stack output --json and parse the result
|
|
1652
|
+
*/
|
|
1653
|
+
async function getPulumiOutputs(options) {
|
|
1654
|
+
const result = await runPulumi([
|
|
1655
|
+
"stack",
|
|
1656
|
+
"output",
|
|
1657
|
+
"--json"
|
|
1658
|
+
], options);
|
|
1659
|
+
if (result.code !== 0) return null;
|
|
1660
|
+
try {
|
|
1661
|
+
return JSON.parse(result.stdout);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Run pulumi stack --json and parse the result
|
|
1668
|
+
*/
|
|
1669
|
+
async function getPulumiStackInfo(options) {
|
|
1670
|
+
const result = await runPulumi(["stack", "--json"], options);
|
|
1671
|
+
if (result.code !== 0) return null;
|
|
1672
|
+
try {
|
|
1673
|
+
const data = JSON.parse(result.stdout);
|
|
1674
|
+
return {
|
|
1675
|
+
name: data.name ?? "",
|
|
1676
|
+
current: data.current ?? false,
|
|
1677
|
+
updateInProgress: data.updateInProgress ?? false,
|
|
1678
|
+
resourceCount: data.resourceCount ?? 0,
|
|
1679
|
+
lastUpdate: data.lastUpdate,
|
|
1680
|
+
url: data.url
|
|
1681
|
+
};
|
|
1682
|
+
} catch {
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Run pulumi stack history --json and parse the result
|
|
1688
|
+
*/
|
|
1689
|
+
async function getPulumiStackHistory(options, limit = 5) {
|
|
1690
|
+
const result = await runPulumi([
|
|
1691
|
+
"stack",
|
|
1692
|
+
"history",
|
|
1693
|
+
"--json",
|
|
1694
|
+
"--show-secrets=false"
|
|
1695
|
+
], options);
|
|
1696
|
+
if (result.code !== 0) return [];
|
|
1697
|
+
try {
|
|
1698
|
+
const data = JSON.parse(result.stdout);
|
|
1699
|
+
if (!Array.isArray(data)) return [];
|
|
1700
|
+
return data.slice(0, limit).map((entry) => ({
|
|
1701
|
+
version: entry.version ?? 0,
|
|
1702
|
+
startTime: entry.startTime ?? "",
|
|
1703
|
+
endTime: entry.endTime ?? "",
|
|
1704
|
+
result: entry.result ?? "failed",
|
|
1705
|
+
resourceChanges: entry.resourceChanges
|
|
1706
|
+
}));
|
|
1707
|
+
} catch {
|
|
1708
|
+
return [];
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Select or create a Pulumi stack
|
|
1713
|
+
*/
|
|
1714
|
+
async function selectOrCreateStack(stackName, options) {
|
|
1715
|
+
if ((await runPulumi([
|
|
1716
|
+
"stack",
|
|
1717
|
+
"select",
|
|
1718
|
+
stackName
|
|
1719
|
+
], options)).code === 0) return true;
|
|
1720
|
+
return (await runPulumi([
|
|
1721
|
+
"stack",
|
|
1722
|
+
"init",
|
|
1723
|
+
stackName
|
|
1724
|
+
], options)).code === 0;
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Set a Pulumi config value
|
|
1728
|
+
*/
|
|
1729
|
+
async function setPulumiConfig(key, value, options) {
|
|
1730
|
+
return (await runPulumi([
|
|
1731
|
+
"config",
|
|
1732
|
+
"set",
|
|
1733
|
+
key,
|
|
1734
|
+
value
|
|
1735
|
+
], options)).code === 0;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Run pulumi up with optional preview mode
|
|
1739
|
+
*/
|
|
1740
|
+
async function pulumiUp(options) {
|
|
1741
|
+
return runPulumi(options.preview ? ["preview"] : ["up", "--yes"], {
|
|
1742
|
+
...options,
|
|
1743
|
+
stream: true
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Run pulumi destroy
|
|
1748
|
+
*/
|
|
1749
|
+
async function pulumiDestroy(options) {
|
|
1750
|
+
return runPulumi(options.force ? [
|
|
1751
|
+
"destroy",
|
|
1752
|
+
"--yes",
|
|
1753
|
+
"-f"
|
|
1754
|
+
] : ["destroy", "--yes"], {
|
|
1755
|
+
...options,
|
|
1756
|
+
stream: true
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Run pulumi cancel to cancel stuck operations
|
|
1761
|
+
*/
|
|
1762
|
+
async function pulumiCancel(options) {
|
|
1763
|
+
return runPulumi(["cancel", "--yes"], options);
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Run pulumi refresh to sync state with cloud provider
|
|
1767
|
+
*/
|
|
1768
|
+
async function pulumiRefresh(options) {
|
|
1769
|
+
return runPulumi(["refresh", "--yes"], {
|
|
1770
|
+
...options,
|
|
1771
|
+
stream: true
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Export Pulumi stack state to a file
|
|
1776
|
+
*/
|
|
1777
|
+
async function pulumiStackExport(filePath, options) {
|
|
1778
|
+
return runPulumi([
|
|
1779
|
+
"stack",
|
|
1780
|
+
"export",
|
|
1781
|
+
"--file",
|
|
1782
|
+
filePath
|
|
1783
|
+
], options);
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Run a gcloud CLI command
|
|
1787
|
+
*/
|
|
1788
|
+
function runGcloud(args, options = {}) {
|
|
1789
|
+
return new Promise((resolve) => {
|
|
1790
|
+
const proc = spawn("gcloud", args, {
|
|
1791
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1792
|
+
stdio: options.stream ? [
|
|
1793
|
+
"inherit",
|
|
1794
|
+
"pipe",
|
|
1795
|
+
"pipe"
|
|
1796
|
+
] : [
|
|
1797
|
+
"pipe",
|
|
1798
|
+
"pipe",
|
|
1799
|
+
"pipe"
|
|
1800
|
+
],
|
|
1801
|
+
shell: true
|
|
1802
|
+
});
|
|
1803
|
+
let stdout = "";
|
|
1804
|
+
let stderr = "";
|
|
1805
|
+
proc.stdout?.on("data", (data) => {
|
|
1806
|
+
const str = data.toString();
|
|
1807
|
+
stdout += str;
|
|
1808
|
+
if (options.stream) process.stdout.write(data);
|
|
1809
|
+
});
|
|
1810
|
+
proc.stderr?.on("data", (data) => {
|
|
1811
|
+
const str = data.toString();
|
|
1812
|
+
stderr += str;
|
|
1813
|
+
if (options.stream) process.stderr.write(data);
|
|
1814
|
+
});
|
|
1815
|
+
proc.on("close", (code) => {
|
|
1816
|
+
resolve({
|
|
1817
|
+
code: code ?? 1,
|
|
1818
|
+
stdout,
|
|
1819
|
+
stderr
|
|
1820
|
+
});
|
|
1821
|
+
});
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Get the path to the packages/infra directory
|
|
1826
|
+
*/
|
|
1827
|
+
function getInfraDir$1(projectRoot) {
|
|
1828
|
+
return join$1(projectRoot, "packages", "infra");
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Check if Pulumi CLI is installed
|
|
1832
|
+
*/
|
|
1833
|
+
async function checkPulumiInstalled() {
|
|
1834
|
+
try {
|
|
1835
|
+
return (await runPulumi(["version"], { cwd: process.cwd() })).code === 0;
|
|
1836
|
+
} catch {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Check if gcloud CLI is installed
|
|
1842
|
+
*/
|
|
1843
|
+
async function checkGcloudInstalled() {
|
|
1844
|
+
try {
|
|
1845
|
+
return (await runGcloud(["version"])).code === 0;
|
|
1846
|
+
} catch {
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* List Cloud Run service revisions
|
|
1852
|
+
*/
|
|
1853
|
+
async function listCloudRunRevisions(serviceName, project, region) {
|
|
1854
|
+
const revisionsResult = await runGcloud([
|
|
1855
|
+
"run",
|
|
1856
|
+
"revisions",
|
|
1857
|
+
"list",
|
|
1858
|
+
"--service",
|
|
1859
|
+
serviceName,
|
|
1860
|
+
"--project",
|
|
1861
|
+
project,
|
|
1862
|
+
"--region",
|
|
1863
|
+
region,
|
|
1864
|
+
"--format",
|
|
1865
|
+
"json"
|
|
1866
|
+
]);
|
|
1151
1867
|
if (revisionsResult.code !== 0) return [];
|
|
1152
1868
|
const trafficResult = await runGcloud([
|
|
1153
1869
|
"run",
|
|
@@ -1282,429 +1998,75 @@ const ERROR_PATTERNS = [
|
|
|
1282
1998
|
pattern: /network.*error|connection.*refused|timeout|ETIMEDOUT|ECONNRESET/i,
|
|
1283
1999
|
type: PulumiErrorType.NetworkError,
|
|
1284
2000
|
suggestion: "Check your network connection and try again.",
|
|
1285
|
-
retryable: true
|
|
1286
|
-
},
|
|
1287
|
-
{
|
|
1288
|
-
pattern: /not found|does not exist|404/i,
|
|
1289
|
-
type: PulumiErrorType.ResourceNotFound,
|
|
1290
|
-
suggestion: "The resource may have been deleted externally. Run: kuckit infra repair --refresh",
|
|
1291
|
-
retryable: false
|
|
1292
|
-
},
|
|
1293
|
-
{
|
|
1294
|
-
pattern: /invalid.*config|configuration.*error|missing.*required/i,
|
|
1295
|
-
type: PulumiErrorType.InvalidConfig,
|
|
1296
|
-
suggestion: "Check your configuration in .kuckit/infra.json and Pulumi stack config.",
|
|
1297
|
-
retryable: false
|
|
1298
|
-
}
|
|
1299
|
-
];
|
|
1300
|
-
/**
|
|
1301
|
-
* Extract resource name from error output
|
|
1302
|
-
*/
|
|
1303
|
-
function extractResourceName(error) {
|
|
1304
|
-
const urnMatch = error.match(/urn:pulumi:[^:]+:[^:]+::([^:]+:[^:\s]+)/i);
|
|
1305
|
-
if (urnMatch) return urnMatch[1];
|
|
1306
|
-
const gcpMatch = error.match(/projects\/[^/]+\/(?:locations|regions)\/[^/]+\/([^/\s]+\/[^/\s]+)/i);
|
|
1307
|
-
if (gcpMatch) return gcpMatch[1];
|
|
1308
|
-
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Parse an error output and return a structured ParsedError
|
|
1311
|
-
*/
|
|
1312
|
-
function parseError(error) {
|
|
1313
|
-
for (const { pattern, type, suggestion, retryable } of ERROR_PATTERNS) if (pattern.test(error)) return {
|
|
1314
|
-
type,
|
|
1315
|
-
message: getErrorMessage(type, error),
|
|
1316
|
-
resource: extractResourceName(error),
|
|
1317
|
-
suggestion,
|
|
1318
|
-
retryable,
|
|
1319
|
-
originalError: error
|
|
1320
|
-
};
|
|
1321
|
-
return {
|
|
1322
|
-
type: PulumiErrorType.Unknown,
|
|
1323
|
-
message: "An unknown error occurred during infrastructure operation.",
|
|
1324
|
-
resource: extractResourceName(error),
|
|
1325
|
-
suggestion: "Check the error output above. If the issue persists, run: kuckit infra repair --refresh",
|
|
1326
|
-
retryable: false,
|
|
1327
|
-
originalError: error
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
/**
|
|
1331
|
-
* Get a human-readable error message for an error type
|
|
1332
|
-
*/
|
|
1333
|
-
function getErrorMessage(type, originalError) {
|
|
1334
|
-
switch (type) {
|
|
1335
|
-
case PulumiErrorType.ConcurrentUpdate: return "Another infrastructure update is already in progress.";
|
|
1336
|
-
case PulumiErrorType.ResourceConflict: return "A resource with this name already exists.";
|
|
1337
|
-
case PulumiErrorType.QuotaExceeded: return "GCP quota limit has been reached.";
|
|
1338
|
-
case PulumiErrorType.PermissionDenied: return "Permission denied. Check your IAM permissions.";
|
|
1339
|
-
case PulumiErrorType.StateCorruption: return "Infrastructure state is inconsistent.";
|
|
1340
|
-
case PulumiErrorType.NetworkError: return "Network error occurred. This may be a transient issue.";
|
|
1341
|
-
case PulumiErrorType.ResourceNotFound: return "A required resource was not found.";
|
|
1342
|
-
case PulumiErrorType.InvalidConfig: return "Invalid configuration detected.";
|
|
1343
|
-
default: return originalError.split("\n").find((line) => line.trim().length > 0)?.slice(0, 200) ?? "An error occurred.";
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
/**
|
|
1347
|
-
* Format a ParsedError for console output
|
|
1348
|
-
*/
|
|
1349
|
-
function formatError(error) {
|
|
1350
|
-
const lines = [`Error: ${error.message}`, ""];
|
|
1351
|
-
if (error.resource) lines.push(`Resource: ${error.resource}`);
|
|
1352
|
-
lines.push(`Suggestion: ${error.suggestion}`);
|
|
1353
|
-
return lines.join("\n");
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
//#endregion
|
|
1357
|
-
//#region src/commands/infra/init.ts
|
|
1358
|
-
const KUCKIT_DIR$8 = ".kuckit";
|
|
1359
|
-
const CONFIG_FILE$8 = "infra.json";
|
|
1360
|
-
async function fileExists$8(path) {
|
|
1361
|
-
try {
|
|
1362
|
-
await access(path, constants$1.F_OK);
|
|
1363
|
-
return true;
|
|
1364
|
-
} catch {
|
|
1365
|
-
return false;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
async function findProjectRoot$8(cwd) {
|
|
1369
|
-
let dir = cwd;
|
|
1370
|
-
while (dir !== dirname$1(dir)) {
|
|
1371
|
-
if (await fileExists$8(join$1(dir, "package.json"))) return dir;
|
|
1372
|
-
dir = dirname$1(dir);
|
|
1373
|
-
}
|
|
1374
|
-
return null;
|
|
1375
|
-
}
|
|
1376
|
-
async function loadExistingConfig(projectRoot) {
|
|
1377
|
-
const configPath = join$1(projectRoot, KUCKIT_DIR$8, CONFIG_FILE$8);
|
|
1378
|
-
if (!await fileExists$8(configPath)) return null;
|
|
1379
|
-
try {
|
|
1380
|
-
const content = await readFile(configPath, "utf-8");
|
|
1381
|
-
return JSON.parse(content);
|
|
1382
|
-
} catch {
|
|
1383
|
-
return null;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
async function saveConfig$1(projectRoot, config) {
|
|
1387
|
-
const kuckitDir = join$1(projectRoot, KUCKIT_DIR$8);
|
|
1388
|
-
await mkdir(kuckitDir, { recursive: true });
|
|
1389
|
-
await writeFile(join$1(kuckitDir, CONFIG_FILE$8), JSON.stringify(config, null, 2), "utf-8");
|
|
1390
|
-
}
|
|
1391
|
-
async function infraInit(options) {
|
|
1392
|
-
console.log("Initializing kuckit infrastructure...\n");
|
|
1393
|
-
if (!await checkPulumiInstalled()) {
|
|
1394
|
-
console.error("Error: Pulumi CLI is not installed.");
|
|
1395
|
-
console.error("Install it from: https://www.pulumi.com/docs/install/");
|
|
1396
|
-
process.exit(1);
|
|
1397
|
-
}
|
|
1398
|
-
if (!await checkGcloudInstalled()) {
|
|
1399
|
-
console.error("Error: gcloud CLI is not installed.");
|
|
1400
|
-
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
1401
|
-
process.exit(1);
|
|
1402
|
-
}
|
|
1403
|
-
const projectRoot = await findProjectRoot$8(process.cwd());
|
|
1404
|
-
if (!projectRoot) {
|
|
1405
|
-
console.error("Error: Could not find project root (no package.json found)");
|
|
1406
|
-
process.exit(1);
|
|
1407
|
-
}
|
|
1408
|
-
const infraDir = getInfraDir(projectRoot);
|
|
1409
|
-
if (!await fileExists$8(infraDir)) {
|
|
1410
|
-
console.error("Error: packages/infra not found.");
|
|
1411
|
-
console.error("Make sure you have the @kuckit/infra package in your project.");
|
|
1412
|
-
process.exit(1);
|
|
1413
|
-
}
|
|
1414
|
-
const existingConfig = await loadExistingConfig(projectRoot);
|
|
1415
|
-
const provider = options.provider ?? "gcp";
|
|
1416
|
-
if (provider !== "gcp") {
|
|
1417
|
-
console.error(`Error: Provider '${provider}' is not supported. Only 'gcp' is currently supported.`);
|
|
1418
|
-
process.exit(1);
|
|
1419
|
-
}
|
|
1420
|
-
let gcpProject = options.project ?? existingConfig?.gcpProject;
|
|
1421
|
-
if (!gcpProject) gcpProject = await input({
|
|
1422
|
-
message: "GCP Project ID:",
|
|
1423
|
-
validate: (value) => value.length > 0 ? true : "Project ID is required"
|
|
1424
|
-
});
|
|
1425
|
-
let region = options.region ?? existingConfig?.region ?? "us-central1";
|
|
1426
|
-
if (!options.region && !existingConfig?.region) region = await input({
|
|
1427
|
-
message: "GCP Region:",
|
|
1428
|
-
default: "us-central1"
|
|
1429
|
-
});
|
|
1430
|
-
let env = options.env ?? existingConfig?.env ?? "dev";
|
|
1431
|
-
if (!options.env && !existingConfig?.env) env = await input({
|
|
1432
|
-
message: "Environment (dev/prod):",
|
|
1433
|
-
default: "dev",
|
|
1434
|
-
validate: (value) => value === "dev" || value === "prod" ? true : "Must be dev or prod"
|
|
1435
|
-
});
|
|
1436
|
-
const stackName = `${gcpProject}-${env}`;
|
|
1437
|
-
console.log("\nConfiguration:");
|
|
1438
|
-
console.log(` Provider: ${provider}`);
|
|
1439
|
-
console.log(` GCP Project: ${gcpProject}`);
|
|
1440
|
-
console.log(` Region: ${region}`);
|
|
1441
|
-
console.log(` Environment: ${env}`);
|
|
1442
|
-
console.log(` Stack: ${stackName}`);
|
|
1443
|
-
console.log("");
|
|
1444
|
-
if (!options.yes) {
|
|
1445
|
-
if (!await confirm({
|
|
1446
|
-
message: "Proceed with infrastructure initialization?",
|
|
1447
|
-
default: true
|
|
1448
|
-
})) {
|
|
1449
|
-
console.log("Aborted.");
|
|
1450
|
-
process.exit(0);
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
console.log("\nInstalling infrastructure dependencies...");
|
|
1454
|
-
const { spawn: spawn$1 } = await import("child_process");
|
|
1455
|
-
await new Promise((resolve, reject) => {
|
|
1456
|
-
spawn$1("bun", ["install"], {
|
|
1457
|
-
cwd: infraDir,
|
|
1458
|
-
stdio: "inherit",
|
|
1459
|
-
shell: true
|
|
1460
|
-
}).on("close", (code) => {
|
|
1461
|
-
if (code === 0) resolve();
|
|
1462
|
-
else reject(/* @__PURE__ */ new Error(`bun install failed with code ${code}`));
|
|
1463
|
-
});
|
|
1464
|
-
});
|
|
1465
|
-
console.log("\nBuilding infrastructure package...");
|
|
1466
|
-
await new Promise((resolve, reject) => {
|
|
1467
|
-
spawn$1("bun", ["run", "build"], {
|
|
1468
|
-
cwd: infraDir,
|
|
1469
|
-
stdio: "inherit",
|
|
1470
|
-
shell: true
|
|
1471
|
-
}).on("close", (code) => {
|
|
1472
|
-
if (code === 0) resolve();
|
|
1473
|
-
else reject(/* @__PURE__ */ new Error(`Build failed with code ${code}`));
|
|
1474
|
-
});
|
|
1475
|
-
});
|
|
1476
|
-
console.log(`\nSelecting Pulumi stack: ${stackName}`);
|
|
1477
|
-
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
1478
|
-
console.error("Error: Failed to select or create Pulumi stack");
|
|
1479
|
-
process.exit(1);
|
|
1480
|
-
}
|
|
1481
|
-
console.log("Configuring stack...");
|
|
1482
|
-
await setPulumiConfig("gcp:project", gcpProject, { cwd: infraDir });
|
|
1483
|
-
await setPulumiConfig("gcp:region", region, { cwd: infraDir });
|
|
1484
|
-
await setPulumiConfig("env", env, { cwd: infraDir });
|
|
1485
|
-
console.log("\nCreating infrastructure...");
|
|
1486
|
-
console.log("This may take several minutes.\n");
|
|
1487
|
-
const result = await pulumiUp({
|
|
1488
|
-
cwd: infraDir,
|
|
1489
|
-
stream: true
|
|
1490
|
-
});
|
|
1491
|
-
if (result.code !== 0) {
|
|
1492
|
-
const parsed = parseError(result.stderr);
|
|
1493
|
-
console.error("\n" + formatError(parsed));
|
|
1494
|
-
process.exit(1);
|
|
1495
|
-
}
|
|
1496
|
-
console.log("\nRetrieving outputs...");
|
|
1497
|
-
const outputs = await getPulumiOutputs({ cwd: infraDir });
|
|
1498
|
-
await saveConfig$1(projectRoot, {
|
|
1499
|
-
provider: "gcp",
|
|
1500
|
-
gcpProject,
|
|
1501
|
-
region,
|
|
1502
|
-
projectName: "kuckit-infra",
|
|
1503
|
-
stackName,
|
|
1504
|
-
env,
|
|
1505
|
-
outputs: outputs ? {
|
|
1506
|
-
registryUrl: outputs.registryUrl,
|
|
1507
|
-
databaseConnectionName: outputs.databaseConnectionName,
|
|
1508
|
-
redisHost: outputs.redisHost,
|
|
1509
|
-
secretIds: {
|
|
1510
|
-
dbPassword: outputs.dbPasswordSecretId,
|
|
1511
|
-
redisAuth: outputs.redisAuthSecretId ?? ""
|
|
1512
|
-
}
|
|
1513
|
-
} : void 0,
|
|
1514
|
-
createdAt: existingConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1515
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1516
|
-
});
|
|
1517
|
-
console.log(`\nConfiguration saved to ${KUCKIT_DIR$8}/${CONFIG_FILE$8}`);
|
|
1518
|
-
console.log("\n" + "=".repeat(60));
|
|
1519
|
-
console.log("Infrastructure initialized successfully!");
|
|
1520
|
-
console.log("=".repeat(60));
|
|
1521
|
-
if (outputs) {
|
|
1522
|
-
console.log("\nOutputs:");
|
|
1523
|
-
console.log(` Registry: ${outputs.registryUrl}`);
|
|
1524
|
-
console.log(` Database: ${outputs.databaseConnectionName}`);
|
|
1525
|
-
console.log(` Redis: ${outputs.redisHost}`);
|
|
2001
|
+
retryable: true
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
pattern: /not found|does not exist|404/i,
|
|
2005
|
+
type: PulumiErrorType.ResourceNotFound,
|
|
2006
|
+
suggestion: "The resource may have been deleted externally. Run: kuckit infra repair --refresh",
|
|
2007
|
+
retryable: false
|
|
2008
|
+
},
|
|
2009
|
+
{
|
|
2010
|
+
pattern: /invalid.*config|configuration.*error|missing.*required/i,
|
|
2011
|
+
type: PulumiErrorType.InvalidConfig,
|
|
2012
|
+
suggestion: "Check your configuration in .kuckit/infra.json and Pulumi stack config.",
|
|
2013
|
+
retryable: false
|
|
1526
2014
|
}
|
|
1527
|
-
|
|
1528
|
-
console.log(" 1. Ensure you have a Dockerfile in your project root");
|
|
1529
|
-
console.log(` 2. Run: kuckit infra deploy --env ${env}`);
|
|
1530
|
-
console.log("");
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
//#endregion
|
|
1534
|
-
//#region src/commands/infra/deploy.ts
|
|
1535
|
-
const KUCKIT_DIR$7 = ".kuckit";
|
|
1536
|
-
const CONFIG_FILE$7 = "infra.json";
|
|
1537
|
-
async function saveConfig(projectRoot, config) {
|
|
1538
|
-
const kuckitDir = join$1(projectRoot, KUCKIT_DIR$7);
|
|
1539
|
-
await mkdir(kuckitDir, { recursive: true });
|
|
1540
|
-
await writeFile(join$1(kuckitDir, CONFIG_FILE$7), JSON.stringify(config, null, 2), "utf-8");
|
|
1541
|
-
}
|
|
2015
|
+
];
|
|
1542
2016
|
/**
|
|
1543
|
-
*
|
|
1544
|
-
* Returns null if not in a git repo or git command fails
|
|
2017
|
+
* Extract resource name from error output
|
|
1545
2018
|
*/
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
async function fileExists$7(path) {
|
|
1554
|
-
try {
|
|
1555
|
-
await access(path, constants$1.F_OK);
|
|
1556
|
-
return true;
|
|
1557
|
-
} catch {
|
|
1558
|
-
return false;
|
|
1559
|
-
}
|
|
2019
|
+
function extractResourceName(error) {
|
|
2020
|
+
const urnMatch = error.match(/urn:pulumi:[^:]+:[^:]+::([^:]+:[^:\s]+)/i);
|
|
2021
|
+
if (urnMatch) return urnMatch[1];
|
|
2022
|
+
const gcpMatch = error.match(/projects\/[^/]+\/(?:locations|regions)\/[^/]+\/([^/\s]+\/[^/\s]+)/i);
|
|
2023
|
+
if (gcpMatch) return gcpMatch[1];
|
|
1560
2024
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2025
|
+
/**
|
|
2026
|
+
* Parse an error output and return a structured ParsedError
|
|
2027
|
+
*/
|
|
2028
|
+
function parseError(error) {
|
|
2029
|
+
for (const { pattern, type, suggestion, retryable } of ERROR_PATTERNS) if (pattern.test(error)) return {
|
|
2030
|
+
type,
|
|
2031
|
+
message: getErrorMessage(type, error),
|
|
2032
|
+
resource: extractResourceName(error),
|
|
2033
|
+
suggestion,
|
|
2034
|
+
retryable,
|
|
2035
|
+
originalError: error
|
|
2036
|
+
};
|
|
2037
|
+
return {
|
|
2038
|
+
type: PulumiErrorType.Unknown,
|
|
2039
|
+
message: "An unknown error occurred during infrastructure operation.",
|
|
2040
|
+
resource: extractResourceName(error),
|
|
2041
|
+
suggestion: "Check the error output above. If the issue persists, run: kuckit infra repair --refresh",
|
|
2042
|
+
retryable: false,
|
|
2043
|
+
originalError: error
|
|
2044
|
+
};
|
|
1568
2045
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
return
|
|
1575
|
-
|
|
1576
|
-
return
|
|
2046
|
+
/**
|
|
2047
|
+
* Get a human-readable error message for an error type
|
|
2048
|
+
*/
|
|
2049
|
+
function getErrorMessage(type, originalError) {
|
|
2050
|
+
switch (type) {
|
|
2051
|
+
case PulumiErrorType.ConcurrentUpdate: return "Another infrastructure update is already in progress.";
|
|
2052
|
+
case PulumiErrorType.ResourceConflict: return "A resource with this name already exists.";
|
|
2053
|
+
case PulumiErrorType.QuotaExceeded: return "GCP quota limit has been reached.";
|
|
2054
|
+
case PulumiErrorType.PermissionDenied: return "Permission denied. Check your IAM permissions.";
|
|
2055
|
+
case PulumiErrorType.StateCorruption: return "Infrastructure state is inconsistent.";
|
|
2056
|
+
case PulumiErrorType.NetworkError: return "Network error occurred. This may be a transient issue.";
|
|
2057
|
+
case PulumiErrorType.ResourceNotFound: return "A required resource was not found.";
|
|
2058
|
+
case PulumiErrorType.InvalidConfig: return "Invalid configuration detected.";
|
|
2059
|
+
default: return originalError.split("\n").find((line) => line.trim().length > 0)?.slice(0, 200) ?? "An error occurred.";
|
|
1577
2060
|
}
|
|
1578
2061
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
console.error("Error: gcloud CLI is not installed.");
|
|
1588
|
-
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
1589
|
-
process.exit(1);
|
|
1590
|
-
}
|
|
1591
|
-
const projectRoot = await findProjectRoot$7(process.cwd());
|
|
1592
|
-
if (!projectRoot) {
|
|
1593
|
-
console.error("Error: Could not find project root (no package.json found)");
|
|
1594
|
-
process.exit(1);
|
|
1595
|
-
}
|
|
1596
|
-
const config = await loadConfig$7(projectRoot);
|
|
1597
|
-
if (!config) {
|
|
1598
|
-
console.error("Error: No infrastructure configuration found.");
|
|
1599
|
-
console.error("Run: kuckit infra init");
|
|
1600
|
-
process.exit(1);
|
|
1601
|
-
}
|
|
1602
|
-
const env = options.env ?? config.env;
|
|
1603
|
-
if (env !== "dev" && env !== "prod") {
|
|
1604
|
-
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1605
|
-
process.exit(1);
|
|
1606
|
-
}
|
|
1607
|
-
const infraDir = getInfraDir(projectRoot);
|
|
1608
|
-
if (!await fileExists$7(infraDir)) {
|
|
1609
|
-
console.error("Error: packages/infra not found.");
|
|
1610
|
-
process.exit(1);
|
|
1611
|
-
}
|
|
1612
|
-
const dockerfilePath = join$1(projectRoot, "Dockerfile");
|
|
1613
|
-
if (!options.skipBuild && !options.image && !await fileExists$7(dockerfilePath)) {
|
|
1614
|
-
console.error("Error: Dockerfile not found in project root.");
|
|
1615
|
-
console.error("Create a Dockerfile or use --skip-build with an existing image.");
|
|
1616
|
-
process.exit(1);
|
|
1617
|
-
}
|
|
1618
|
-
if (!config.outputs?.registryUrl) {
|
|
1619
|
-
console.error("Error: No registry URL found in configuration.");
|
|
1620
|
-
console.error("Run: kuckit infra init");
|
|
1621
|
-
process.exit(1);
|
|
1622
|
-
}
|
|
1623
|
-
console.log("Configuration:");
|
|
1624
|
-
console.log(` Project: ${config.gcpProject}`);
|
|
1625
|
-
console.log(` Region: ${config.region}`);
|
|
1626
|
-
console.log(` Environment: ${env}`);
|
|
1627
|
-
console.log(` Registry: ${config.outputs.registryUrl}`);
|
|
1628
|
-
if (options.preview) console.log(" Mode: Preview (no changes will be applied)");
|
|
1629
|
-
console.log("");
|
|
1630
|
-
let imageUrl;
|
|
1631
|
-
if (options.image) {
|
|
1632
|
-
imageUrl = options.image;
|
|
1633
|
-
console.log(`Using provided image: ${imageUrl}\n`);
|
|
1634
|
-
} else if (options.skipBuild) {
|
|
1635
|
-
imageUrl = `${config.outputs.registryUrl}/kuckit:latest`;
|
|
1636
|
-
console.log(`Using existing image: ${imageUrl}\n`);
|
|
1637
|
-
} else {
|
|
1638
|
-
console.log("Building and pushing Docker image...");
|
|
1639
|
-
console.log("(Using Cloud Build - no local Docker required)\n");
|
|
1640
|
-
const buildResult = await buildAndPushImage({
|
|
1641
|
-
project: config.gcpProject,
|
|
1642
|
-
registryUrl: config.outputs.registryUrl,
|
|
1643
|
-
tag: process.env.GIT_SHA ?? process.env.GITHUB_SHA ?? getGitShortHash() ?? "latest",
|
|
1644
|
-
cwd: projectRoot
|
|
1645
|
-
});
|
|
1646
|
-
if (buildResult.code !== 0) {
|
|
1647
|
-
console.error("\nError: Docker build failed.");
|
|
1648
|
-
process.exit(1);
|
|
1649
|
-
}
|
|
1650
|
-
imageUrl = buildResult.imageUrl;
|
|
1651
|
-
console.log(`\nImage built: ${imageUrl}\n`);
|
|
1652
|
-
}
|
|
1653
|
-
if (!options.yes && !options.preview) {
|
|
1654
|
-
if (!await confirm({
|
|
1655
|
-
message: "Proceed with deployment?",
|
|
1656
|
-
default: true
|
|
1657
|
-
})) {
|
|
1658
|
-
console.log("Aborted.");
|
|
1659
|
-
process.exit(0);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
const stackName = config.stackName;
|
|
1663
|
-
console.log(`Selecting Pulumi stack: ${stackName}`);
|
|
1664
|
-
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
1665
|
-
console.error("Error: Failed to select Pulumi stack");
|
|
1666
|
-
process.exit(1);
|
|
1667
|
-
}
|
|
1668
|
-
console.log("Configuring deployment...");
|
|
1669
|
-
await setPulumiConfig("imageUrl", imageUrl, { cwd: infraDir });
|
|
1670
|
-
if (config.outputs?.serviceUrl) await setPulumiConfig("appUrl", config.outputs.serviceUrl, { cwd: infraDir });
|
|
1671
|
-
if (options.preview) console.log("\nPreviewing changes...\n");
|
|
1672
|
-
else console.log("\nDeploying to Cloud Run...\n");
|
|
1673
|
-
const result = await pulumiUp({
|
|
1674
|
-
cwd: infraDir,
|
|
1675
|
-
stream: true,
|
|
1676
|
-
preview: options.preview
|
|
1677
|
-
});
|
|
1678
|
-
if (result.code !== 0) {
|
|
1679
|
-
const parsed = parseError(result.stderr);
|
|
1680
|
-
console.error("\n" + formatError(parsed));
|
|
1681
|
-
process.exit(1);
|
|
1682
|
-
}
|
|
1683
|
-
if (!options.preview) {
|
|
1684
|
-
console.log("\nRetrieving deployment info...");
|
|
1685
|
-
const outputs = await getPulumiOutputs({ cwd: infraDir });
|
|
1686
|
-
if (outputs) {
|
|
1687
|
-
config.outputs = {
|
|
1688
|
-
...config.outputs,
|
|
1689
|
-
...outputs
|
|
1690
|
-
};
|
|
1691
|
-
config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1692
|
-
await saveConfig(projectRoot, config);
|
|
1693
|
-
}
|
|
1694
|
-
console.log("\n" + "=".repeat(60));
|
|
1695
|
-
console.log("Deployment successful!");
|
|
1696
|
-
console.log("=".repeat(60));
|
|
1697
|
-
if (outputs?.serviceUrl) console.log(`\nService URL: ${outputs.serviceUrl}`);
|
|
1698
|
-
if (outputs?.migrationJobName) console.log(`Migration Job: ${outputs.migrationJobName}`);
|
|
1699
|
-
console.log("\nUseful commands:");
|
|
1700
|
-
console.log(` View logs: kuckit infra logs --env ${env}`);
|
|
1701
|
-
console.log(` Check status: kuckit infra status --env ${env}`);
|
|
1702
|
-
console.log(` Run migrations: kuckit infra db:migrate --env ${env}`);
|
|
1703
|
-
console.log("");
|
|
1704
|
-
} else {
|
|
1705
|
-
console.log("\nPreview complete. No changes were applied.");
|
|
1706
|
-
console.log("Run without --preview to apply changes.");
|
|
1707
|
-
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Format a ParsedError for console output
|
|
2064
|
+
*/
|
|
2065
|
+
function formatError(error) {
|
|
2066
|
+
const lines = [`Error: ${error.message}`, ""];
|
|
2067
|
+
if (error.resource) lines.push(`Resource: ${error.resource}`);
|
|
2068
|
+
lines.push(`Suggestion: ${error.suggestion}`);
|
|
2069
|
+
return lines.join("\n");
|
|
1708
2070
|
}
|
|
1709
2071
|
|
|
1710
2072
|
//#endregion
|
|
@@ -1762,7 +2124,7 @@ async function infraDestroy(options) {
|
|
|
1762
2124
|
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1763
2125
|
process.exit(1);
|
|
1764
2126
|
}
|
|
1765
|
-
const infraDir = getInfraDir(projectRoot);
|
|
2127
|
+
const infraDir = getInfraDir$1(projectRoot);
|
|
1766
2128
|
if (!await fileExists$6(infraDir)) {
|
|
1767
2129
|
console.error("Error: packages/infra not found.");
|
|
1768
2130
|
process.exit(1);
|
|
@@ -1915,7 +2277,7 @@ async function infraRepair(options) {
|
|
|
1915
2277
|
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1916
2278
|
process.exit(1);
|
|
1917
2279
|
}
|
|
1918
|
-
const infraDir = getInfraDir(projectRoot);
|
|
2280
|
+
const infraDir = getInfraDir$1(projectRoot);
|
|
1919
2281
|
if (!await fileExists$5(infraDir)) {
|
|
1920
2282
|
console.error("Error: packages/infra not found.");
|
|
1921
2283
|
process.exit(1);
|
|
@@ -2022,6 +2384,12 @@ async function getJobContext(options) {
|
|
|
2022
2384
|
console.error("Run: kuckit infra init");
|
|
2023
2385
|
process.exit(1);
|
|
2024
2386
|
}
|
|
2387
|
+
if (!isGcpConfig(config)) {
|
|
2388
|
+
console.error("Error: Database commands only support GCP provider.");
|
|
2389
|
+
console.error(`Current provider: ${config.provider}`);
|
|
2390
|
+
process.exit(1);
|
|
2391
|
+
}
|
|
2392
|
+
const gcpProject = config.providerConfig.gcpProject;
|
|
2025
2393
|
const env = options.env ?? config.env;
|
|
2026
2394
|
if (env !== "dev" && env !== "prod") {
|
|
2027
2395
|
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
@@ -2036,6 +2404,7 @@ async function getJobContext(options) {
|
|
|
2036
2404
|
}
|
|
2037
2405
|
return {
|
|
2038
2406
|
config,
|
|
2407
|
+
gcpProject,
|
|
2039
2408
|
projectRoot,
|
|
2040
2409
|
env,
|
|
2041
2410
|
jobName
|
|
@@ -2112,18 +2481,18 @@ async function infraDbPush(options) {
|
|
|
2112
2481
|
console.log("Running database schema push via Cloud Run Job...\n");
|
|
2113
2482
|
const ctx = await getJobContext(options);
|
|
2114
2483
|
console.log("Configuration:");
|
|
2115
|
-
console.log(` Project: ${ctx.
|
|
2484
|
+
console.log(` Project: ${ctx.gcpProject}`);
|
|
2116
2485
|
console.log(` Environment: ${ctx.env}`);
|
|
2117
2486
|
console.log(` Job: ${ctx.jobName}`);
|
|
2118
2487
|
console.log("");
|
|
2119
2488
|
const commandOverride = [`export NODE_PATH=/app/node_modules && cd /app && node /app/node_modules/drizzle-kit/bin.cjs push --config=packages/db/drizzle.config.ts${options.force ? " --force" : ""}`];
|
|
2120
|
-
if (!await executeCloudRunJob(ctx.
|
|
2489
|
+
if (!await executeCloudRunJob(ctx.gcpProject, ctx.config.region, ctx.jobName, commandOverride)) {
|
|
2121
2490
|
console.error("\nError: Schema push failed.");
|
|
2122
2491
|
console.error("Check the Cloud Run Job logs for details:");
|
|
2123
2492
|
console.error(` gcloud run jobs executions list --job ${ctx.jobName} --region ${ctx.config.region}`);
|
|
2124
2493
|
process.exit(1);
|
|
2125
2494
|
}
|
|
2126
|
-
const execution = await getLatestJobExecution(ctx.
|
|
2495
|
+
const execution = await getLatestJobExecution(ctx.gcpProject, ctx.config.region, ctx.jobName);
|
|
2127
2496
|
if (execution?.logs) {
|
|
2128
2497
|
console.log("\nJob output:");
|
|
2129
2498
|
console.log(execution.logs);
|
|
@@ -2143,17 +2512,17 @@ async function infraDbMigrate(options) {
|
|
|
2143
2512
|
}
|
|
2144
2513
|
const ctx = await getJobContext(options);
|
|
2145
2514
|
console.log("Configuration:");
|
|
2146
|
-
console.log(` Project: ${ctx.
|
|
2515
|
+
console.log(` Project: ${ctx.gcpProject}`);
|
|
2147
2516
|
console.log(` Environment: ${ctx.env}`);
|
|
2148
2517
|
console.log(` Job: ${ctx.jobName}`);
|
|
2149
2518
|
console.log("");
|
|
2150
|
-
if (!await executeCloudRunJob(ctx.
|
|
2519
|
+
if (!await executeCloudRunJob(ctx.gcpProject, ctx.config.region, ctx.jobName)) {
|
|
2151
2520
|
console.error("\nError: Migration failed.");
|
|
2152
2521
|
console.error("Check the Cloud Run Job logs for details:");
|
|
2153
2522
|
console.error(` gcloud run jobs executions list --job ${ctx.jobName} --region ${ctx.config.region}`);
|
|
2154
2523
|
process.exit(1);
|
|
2155
2524
|
}
|
|
2156
|
-
const execution = await getLatestJobExecution(ctx.
|
|
2525
|
+
const execution = await getLatestJobExecution(ctx.gcpProject, ctx.config.region, ctx.jobName);
|
|
2157
2526
|
if (execution?.logs) {
|
|
2158
2527
|
console.log("\nJob output:");
|
|
2159
2528
|
console.log(execution.logs);
|
|
@@ -2475,9 +2844,8 @@ async function infraStatus(options) {
|
|
|
2475
2844
|
console.error("Run: kuckit infra init");
|
|
2476
2845
|
process.exit(1);
|
|
2477
2846
|
}
|
|
2478
|
-
const
|
|
2479
|
-
const
|
|
2480
|
-
const infraDir = getInfraDir(projectRoot);
|
|
2847
|
+
const stackName = config.stackName;
|
|
2848
|
+
const infraDir = getInfraDir$1(projectRoot);
|
|
2481
2849
|
if (!await fileExists$1(infraDir)) {
|
|
2482
2850
|
console.error("Error: packages/infra not found.");
|
|
2483
2851
|
process.exit(1);
|
|
@@ -2587,8 +2955,8 @@ async function infraOutputs(options) {
|
|
|
2587
2955
|
process.exit(1);
|
|
2588
2956
|
}
|
|
2589
2957
|
const env = options.env ?? config.env ?? "dev";
|
|
2590
|
-
const stackName =
|
|
2591
|
-
const infraDir = getInfraDir(projectRoot);
|
|
2958
|
+
const stackName = config.stackName;
|
|
2959
|
+
const infraDir = getInfraDir$1(projectRoot);
|
|
2592
2960
|
if (!await fileExists(infraDir)) {
|
|
2593
2961
|
console.error("Error: packages/infra not found.");
|
|
2594
2962
|
process.exit(1);
|
|
@@ -2634,6 +3002,210 @@ async function infraOutputs(options) {
|
|
|
2634
3002
|
console.log("");
|
|
2635
3003
|
}
|
|
2636
3004
|
|
|
3005
|
+
//#endregion
|
|
3006
|
+
//#region src/commands/infra/config.ts
|
|
3007
|
+
/**
|
|
3008
|
+
* Known configuration keys with their descriptions
|
|
3009
|
+
*/
|
|
3010
|
+
const KNOWN_CONFIG_KEYS = {
|
|
3011
|
+
appUrl: {
|
|
3012
|
+
description: "Application URL (used for CORS and redirects)",
|
|
3013
|
+
example: "https://app.example.com"
|
|
3014
|
+
},
|
|
3015
|
+
region: {
|
|
3016
|
+
description: "Deployment region",
|
|
3017
|
+
example: "us-central1"
|
|
3018
|
+
},
|
|
3019
|
+
env: {
|
|
3020
|
+
description: "Environment (dev, staging, prod)",
|
|
3021
|
+
example: "prod"
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
function getProjectRoot() {
|
|
3025
|
+
return process.cwd();
|
|
3026
|
+
}
|
|
3027
|
+
async function ensureConfigExists(projectRoot) {
|
|
3028
|
+
const config = await loadStoredConfig(projectRoot);
|
|
3029
|
+
if (!config) throw new Error("No infrastructure configuration found. Run `kuckit infra init` first.");
|
|
3030
|
+
return config;
|
|
3031
|
+
}
|
|
3032
|
+
function getStackName(config, env) {
|
|
3033
|
+
return `${config.projectName}-${env}`;
|
|
3034
|
+
}
|
|
3035
|
+
async function getInfraDir(projectRoot, config) {
|
|
3036
|
+
if (config.localInfraDir) return config.localInfraDir;
|
|
3037
|
+
return (await loadProviderFromPackage(config.providerPackage ?? getProviderPackage(config.provider), projectRoot)).getInfraDir(projectRoot);
|
|
3038
|
+
}
|
|
3039
|
+
async function selectStack(infraDir, stackName) {
|
|
3040
|
+
return selectOrCreateStack(stackName, { cwd: infraDir });
|
|
3041
|
+
}
|
|
3042
|
+
async function syncToPulumi(projectRoot, config, env, key, value) {
|
|
3043
|
+
const infraDir = await getInfraDir(projectRoot, config);
|
|
3044
|
+
const stackName = getStackName(config, env);
|
|
3045
|
+
const pulumiOptions = { cwd: infraDir };
|
|
3046
|
+
if (!await selectStack(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3047
|
+
await setPulumiConfig(key, value, pulumiOptions);
|
|
3048
|
+
}
|
|
3049
|
+
async function getPulumiConfigValue(projectRoot, config, env, key) {
|
|
3050
|
+
const infraDir = await getInfraDir(projectRoot, config);
|
|
3051
|
+
const stackName = getStackName(config, env);
|
|
3052
|
+
const pulumiOptions = { cwd: infraDir };
|
|
3053
|
+
if (!await selectStack(infraDir, stackName)) return null;
|
|
3054
|
+
try {
|
|
3055
|
+
const result = await runPulumi([
|
|
3056
|
+
"config",
|
|
3057
|
+
"get",
|
|
3058
|
+
key
|
|
3059
|
+
], pulumiOptions);
|
|
3060
|
+
return result.code === 0 ? result.stdout.trim() : null;
|
|
3061
|
+
} catch {
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async function getAllPulumiConfig(projectRoot, config, env) {
|
|
3066
|
+
const infraDir = await getInfraDir(projectRoot, config);
|
|
3067
|
+
const stackName = getStackName(config, env);
|
|
3068
|
+
const pulumiOptions = { cwd: infraDir };
|
|
3069
|
+
if (!await selectStack(infraDir, stackName)) return {};
|
|
3070
|
+
try {
|
|
3071
|
+
const result = await runPulumi(["config", "--json"], pulumiOptions);
|
|
3072
|
+
if (result.code === 0 && result.stdout) {
|
|
3073
|
+
const parsed = JSON.parse(result.stdout);
|
|
3074
|
+
const configMap = {};
|
|
3075
|
+
for (const [fullKey, data] of Object.entries(parsed)) {
|
|
3076
|
+
const key = fullKey.includes(":") ? fullKey.split(":")[1] : fullKey;
|
|
3077
|
+
if (key) configMap[key] = data.value;
|
|
3078
|
+
}
|
|
3079
|
+
return configMap;
|
|
3080
|
+
}
|
|
3081
|
+
return {};
|
|
3082
|
+
} catch {
|
|
3083
|
+
return {};
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
async function unsetPulumiConfig(projectRoot, config, env, key) {
|
|
3087
|
+
const infraDir = await getInfraDir(projectRoot, config);
|
|
3088
|
+
const stackName = getStackName(config, env);
|
|
3089
|
+
const pulumiOptions = { cwd: infraDir };
|
|
3090
|
+
if (!await selectStack(infraDir, stackName)) throw new Error(`Could not select stack '${stackName}'. Run 'kuckit infra init --env ${env}' first.`);
|
|
3091
|
+
await runPulumi([
|
|
3092
|
+
"config",
|
|
3093
|
+
"rm",
|
|
3094
|
+
key
|
|
3095
|
+
], pulumiOptions);
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Set a configuration value
|
|
3099
|
+
*/
|
|
3100
|
+
async function infraConfigSet(key, value, options = {}) {
|
|
3101
|
+
const projectRoot = getProjectRoot();
|
|
3102
|
+
const config = await ensureConfigExists(projectRoot);
|
|
3103
|
+
const env = options.env ?? config.env ?? "dev";
|
|
3104
|
+
if (["region", "projectName"].includes(key)) {
|
|
3105
|
+
if (key === "region") config.region = value;
|
|
3106
|
+
else if (key === "projectName") config.projectName = value;
|
|
3107
|
+
await saveStoredConfig(projectRoot, config);
|
|
3108
|
+
console.log(`✓ Set ${key}=${value} in .kuckit/infra.json`);
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
if (key.startsWith("provider.")) {
|
|
3112
|
+
const providerKey = key.replace("provider.", "");
|
|
3113
|
+
if (isGcpConfig(config) && providerKey === "gcpProject") {
|
|
3114
|
+
config.providerConfig.gcpProject = value;
|
|
3115
|
+
await saveStoredConfig(projectRoot, config);
|
|
3116
|
+
console.log(`✓ Set ${key}=${value} in .kuckit/infra.json`);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
await syncToPulumi(projectRoot, config, env, key, value);
|
|
3121
|
+
console.log(`✓ Set ${key}=${value} for env '${env}'`);
|
|
3122
|
+
if (key === "appUrl") console.log(`\nNote: Run 'kuckit infra deploy --env ${env}' to apply this change.`);
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Get a configuration value
|
|
3126
|
+
*/
|
|
3127
|
+
async function infraConfigGet(key, options = {}) {
|
|
3128
|
+
const projectRoot = getProjectRoot();
|
|
3129
|
+
const config = await ensureConfigExists(projectRoot);
|
|
3130
|
+
const env = options.env ?? config.env ?? "dev";
|
|
3131
|
+
const localKeys = {
|
|
3132
|
+
region: (c) => c.region,
|
|
3133
|
+
projectName: (c) => c.projectName,
|
|
3134
|
+
provider: (c) => c.provider,
|
|
3135
|
+
"provider.gcpProject": (c) => isGcpConfig(c) ? c.providerConfig.gcpProject : void 0
|
|
3136
|
+
};
|
|
3137
|
+
if (key in localKeys) {
|
|
3138
|
+
const getter = localKeys[key];
|
|
3139
|
+
if (getter) {
|
|
3140
|
+
const value$1 = getter(config);
|
|
3141
|
+
if (value$1 !== void 0) {
|
|
3142
|
+
console.log(value$1);
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
const value = await getPulumiConfigValue(projectRoot, config, env, key);
|
|
3148
|
+
if (value !== null) console.log(value);
|
|
3149
|
+
else {
|
|
3150
|
+
console.error(`Config key '${key}' is not set for env '${env}'.`);
|
|
3151
|
+
process.exit(1);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* List all configuration values
|
|
3156
|
+
*/
|
|
3157
|
+
async function infraConfigList(options = {}) {
|
|
3158
|
+
const projectRoot = getProjectRoot();
|
|
3159
|
+
const config = await ensureConfigExists(projectRoot);
|
|
3160
|
+
const env = options.env ?? config.env ?? "dev";
|
|
3161
|
+
const localConfig = {
|
|
3162
|
+
provider: config.provider,
|
|
3163
|
+
region: config.region,
|
|
3164
|
+
projectName: config.projectName
|
|
3165
|
+
};
|
|
3166
|
+
if (isGcpConfig(config)) localConfig["provider.gcpProject"] = config.providerConfig.gcpProject;
|
|
3167
|
+
const pulumiConfig = await getAllPulumiConfig(projectRoot, config, env);
|
|
3168
|
+
const allConfig = {
|
|
3169
|
+
...localConfig,
|
|
3170
|
+
...pulumiConfig
|
|
3171
|
+
};
|
|
3172
|
+
if (options.json) {
|
|
3173
|
+
console.log(JSON.stringify({
|
|
3174
|
+
env,
|
|
3175
|
+
config: allConfig
|
|
3176
|
+
}, null, 2));
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
console.log(`\nInfrastructure Configuration (env: ${env})\n`);
|
|
3180
|
+
console.log("Shared (.kuckit/infra.json):");
|
|
3181
|
+
for (const [key, value] of Object.entries(localConfig)) console.log(` ${key}: ${value}`);
|
|
3182
|
+
if (Object.keys(pulumiConfig).length > 0) {
|
|
3183
|
+
console.log(`\nEnvironment '${env}' Config:`);
|
|
3184
|
+
for (const [key, value] of Object.entries(pulumiConfig)) if (!(key in localConfig)) {
|
|
3185
|
+
const desc = KNOWN_CONFIG_KEYS[key]?.description;
|
|
3186
|
+
console.log(` ${key}: ${value}${desc ? ` (${desc})` : ""}`);
|
|
3187
|
+
}
|
|
3188
|
+
} else console.log(`\nEnvironment '${env}' Config: (none set)`);
|
|
3189
|
+
console.log("\nAvailable keys:");
|
|
3190
|
+
for (const [key, info] of Object.entries(KNOWN_CONFIG_KEYS)) if (!(key in allConfig)) console.log(` ${key}: ${info.description} (e.g., ${info.example})`);
|
|
3191
|
+
console.log(`\nTip: Use --env to configure other environments (dev, staging, prod)`);
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* Unset a configuration value
|
|
3195
|
+
*/
|
|
3196
|
+
async function infraConfigUnset(key, options = {}) {
|
|
3197
|
+
const projectRoot = getProjectRoot();
|
|
3198
|
+
const config = await ensureConfigExists(projectRoot);
|
|
3199
|
+
const env = options.env ?? config.env ?? "dev";
|
|
3200
|
+
if ([
|
|
3201
|
+
"provider",
|
|
3202
|
+
"region",
|
|
3203
|
+
"projectName"
|
|
3204
|
+
].includes(key)) throw new Error(`Cannot unset required key '${key}'.`);
|
|
3205
|
+
await unsetPulumiConfig(projectRoot, config, env, key);
|
|
3206
|
+
console.log(`✓ Unset ${key} for env '${env}'`);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
2637
3209
|
//#endregion
|
|
2638
3210
|
//#region src/bin.ts
|
|
2639
3211
|
program.name("kuckit").description("CLI tools for Kuckit SDK module development").version("0.1.0");
|
|
@@ -2673,6 +3245,10 @@ db.command("studio").description("Open Drizzle Studio with all module schemas").
|
|
|
2673
3245
|
});
|
|
2674
3246
|
registerAuthCommands(program);
|
|
2675
3247
|
const infra = program.command("infra").description("Infrastructure deployment and management");
|
|
3248
|
+
infra.command("up").description("Initialize (if needed) and deploy infrastructure in one command").option("-p, --provider <provider>", "Cloud provider (gcp)", "gcp").option("--project <id>", "GCP project ID").option("--region <region>", "Deployment region", "us-central1").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--preview", "Preview changes without applying", false).option("-y, --yes", "Skip confirmation prompts", false).action(async (options) => {
|
|
3249
|
+
requireAuth();
|
|
3250
|
+
await infraUp(options);
|
|
3251
|
+
});
|
|
2676
3252
|
infra.command("init").description("Initialize base infrastructure (no Docker required)").option("-p, --provider <provider>", "Cloud provider (gcp)", "gcp").option("--project <id>", "GCP project ID").option("--region <region>", "Deployment region", "us-central1").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("-y, --yes", "Skip confirmation prompts", false).action(async (options) => {
|
|
2677
3253
|
requireAuth();
|
|
2678
3254
|
await infraInit(options);
|
|
@@ -2681,6 +3257,10 @@ infra.command("deploy").description("Build and deploy application to Cloud Run")
|
|
|
2681
3257
|
requireAuth();
|
|
2682
3258
|
await infraDeploy(options);
|
|
2683
3259
|
});
|
|
3260
|
+
infra.command("eject").description("Eject provider infrastructure code to local project for customization").option("-d, --dir <directory>", "Target directory for ejected code", "infra").option("--force", "Overwrite existing directory", false).action(async (options) => {
|
|
3261
|
+
requireAuth();
|
|
3262
|
+
await infraEject(options);
|
|
3263
|
+
});
|
|
2684
3264
|
infra.command("destroy").description("Destroy infrastructure resources").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--app-only", "Only destroy Cloud Run, keep DB/Redis", false).option("--force", "Skip confirmation prompt", false).action(async (options) => {
|
|
2685
3265
|
requireAuth();
|
|
2686
3266
|
await infraDestroy(options);
|
|
@@ -2713,7 +3293,25 @@ infra.command("outputs").description("Display infrastructure outputs (URLs, conn
|
|
|
2713
3293
|
requireAuth();
|
|
2714
3294
|
await infraOutputs(options);
|
|
2715
3295
|
});
|
|
3296
|
+
const configCmd = infra.command("config").description("Manage infrastructure configuration");
|
|
3297
|
+
configCmd.command("set <key> <value>").description("Set a configuration value").option("-e, --env <env>", "Environment (dev, prod)", "dev").action(async (key, value, options) => {
|
|
3298
|
+
requireAuth();
|
|
3299
|
+
await infraConfigSet(key, value, options);
|
|
3300
|
+
});
|
|
3301
|
+
configCmd.command("get <key>").description("Get a configuration value").option("-e, --env <env>", "Environment (dev, prod)", "dev").action(async (key, options) => {
|
|
3302
|
+
requireAuth();
|
|
3303
|
+
await infraConfigGet(key, options);
|
|
3304
|
+
});
|
|
3305
|
+
configCmd.command("list").description("List all configuration values").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--json", "Output as JSON", false).action(async (options) => {
|
|
3306
|
+
requireAuth();
|
|
3307
|
+
await infraConfigList(options);
|
|
3308
|
+
});
|
|
3309
|
+
configCmd.command("unset <key>").description("Remove a configuration value").option("-e, --env <env>", "Environment (dev, prod)", "dev").action(async (key, options) => {
|
|
3310
|
+
requireAuth();
|
|
3311
|
+
await infraConfigUnset(key, options);
|
|
3312
|
+
});
|
|
2716
3313
|
program.parse();
|
|
2717
3314
|
|
|
2718
3315
|
//#endregion
|
|
2719
|
-
export { };
|
|
3316
|
+
export { };
|
|
3317
|
+
//# sourceMappingURL=bin.js.map
|