@kuckit/cli 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1878 -59
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -3,10 +3,11 @@ import { i as generateModule, n as loadTryLoadKuckitConfig, r as addModule, t as
|
|
|
3
3
|
import { program } from "commander";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { spawn } from "child_process";
|
|
7
|
-
import { access, constants as constants$1, mkdir, readFile, writeFile } from "fs/promises";
|
|
6
|
+
import { execSync, spawn } from "child_process";
|
|
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
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
10
11
|
|
|
11
12
|
//#region src/commands/doctor.ts
|
|
12
13
|
const CONFIG_FILES = [
|
|
@@ -77,7 +78,7 @@ function checkModuleExports(packageName, cwd) {
|
|
|
77
78
|
].some((p) => existsSync(p));
|
|
78
79
|
return result;
|
|
79
80
|
}
|
|
80
|
-
async function loadConfig$
|
|
81
|
+
async function loadConfig$9(configPath) {
|
|
81
82
|
try {
|
|
82
83
|
if (configPath.endsWith(".ts")) {
|
|
83
84
|
const { createJiti } = await import("jiti");
|
|
@@ -117,7 +118,7 @@ async function doctor(options) {
|
|
|
117
118
|
});
|
|
118
119
|
let configModules = [];
|
|
119
120
|
if (configPath) {
|
|
120
|
-
const { success, config, error } = await loadConfig$
|
|
121
|
+
const { success, config, error } = await loadConfig$9(configPath);
|
|
121
122
|
if (success && config && typeof config === "object") {
|
|
122
123
|
const cfg = config;
|
|
123
124
|
if (Array.isArray(cfg.modules)) {
|
|
@@ -345,9 +346,9 @@ async function search(keyword, options) {
|
|
|
345
346
|
|
|
346
347
|
//#endregion
|
|
347
348
|
//#region src/commands/db.ts
|
|
348
|
-
const KUCKIT_DIR = ".kuckit";
|
|
349
|
+
const KUCKIT_DIR$9 = ".kuckit";
|
|
349
350
|
const TEMP_CONFIG_NAME = "drizzle.config.ts";
|
|
350
|
-
async function fileExists(path) {
|
|
351
|
+
async function fileExists$9(path) {
|
|
351
352
|
try {
|
|
352
353
|
await access(path, constants$1.F_OK);
|
|
353
354
|
return true;
|
|
@@ -355,10 +356,10 @@ async function fileExists(path) {
|
|
|
355
356
|
return false;
|
|
356
357
|
}
|
|
357
358
|
}
|
|
358
|
-
async function findProjectRoot(cwd) {
|
|
359
|
+
async function findProjectRoot$9(cwd) {
|
|
359
360
|
let dir = cwd;
|
|
360
361
|
while (dir !== dirname$1(dir)) {
|
|
361
|
-
if (await fileExists(join$1(dir, "package.json"))) return dir;
|
|
362
|
+
if (await fileExists$9(join$1(dir, "package.json"))) return dir;
|
|
362
363
|
dir = dirname$1(dir);
|
|
363
364
|
}
|
|
364
365
|
return null;
|
|
@@ -371,7 +372,7 @@ async function getDatabaseUrl(cwd, options) {
|
|
|
371
372
|
join$1(cwd, "apps", "server", ".env"),
|
|
372
373
|
join$1(cwd, ".env.local")
|
|
373
374
|
];
|
|
374
|
-
for (const envPath of envPaths) if (await fileExists(envPath)) try {
|
|
375
|
+
for (const envPath of envPaths) if (await fileExists$9(envPath)) try {
|
|
375
376
|
const match = (await readFile(envPath, "utf-8")).match(/^DATABASE_URL=(.+)$/m);
|
|
376
377
|
if (match) return match[1].replace(/^["']|["']$/g, "");
|
|
377
378
|
} catch {}
|
|
@@ -387,10 +388,10 @@ async function discoverModuleSchemas(cwd) {
|
|
|
387
388
|
let moduleId = null;
|
|
388
389
|
if (moduleSpec.package) {
|
|
389
390
|
const workspacePath = join$1(cwd, "packages", moduleSpec.package.replace(/^@[^/]+\//, ""));
|
|
390
|
-
if (await fileExists(join$1(workspacePath, "package.json"))) packagePath = workspacePath;
|
|
391
|
+
if (await fileExists$9(join$1(workspacePath, "package.json"))) packagePath = workspacePath;
|
|
391
392
|
else {
|
|
392
393
|
const nodeModulesPath = join$1(cwd, "node_modules", moduleSpec.package);
|
|
393
|
-
if (await fileExists(join$1(nodeModulesPath, "package.json"))) packagePath = nodeModulesPath;
|
|
394
|
+
if (await fileExists$9(join$1(nodeModulesPath, "package.json"))) packagePath = nodeModulesPath;
|
|
394
395
|
}
|
|
395
396
|
if (packagePath) try {
|
|
396
397
|
moduleId = JSON.parse(await readFile(join$1(packagePath, "package.json"), "utf-8")).kuckit?.id || moduleSpec.package;
|
|
@@ -405,7 +406,7 @@ async function discoverModuleSchemas(cwd) {
|
|
|
405
406
|
join$1(packagePath, "src", "schema", "index.ts"),
|
|
406
407
|
join$1(packagePath, "src", "schema.ts")
|
|
407
408
|
];
|
|
408
|
-
for (const schemaPath of schemaLocations) if (await fileExists(schemaPath)) {
|
|
409
|
+
for (const schemaPath of schemaLocations) if (await fileExists$9(schemaPath)) {
|
|
409
410
|
schemas.push({
|
|
410
411
|
moduleId: moduleId || "unknown",
|
|
411
412
|
schemaPath
|
|
@@ -414,14 +415,14 @@ async function discoverModuleSchemas(cwd) {
|
|
|
414
415
|
}
|
|
415
416
|
}
|
|
416
417
|
const centralSchemaPath = join$1(cwd, "packages", "db", "src", "schema");
|
|
417
|
-
if (await fileExists(centralSchemaPath)) schemas.push({
|
|
418
|
+
if (await fileExists$9(centralSchemaPath)) schemas.push({
|
|
418
419
|
moduleId: "core",
|
|
419
420
|
schemaPath: centralSchemaPath
|
|
420
421
|
});
|
|
421
422
|
return schemas;
|
|
422
423
|
}
|
|
423
424
|
async function generateTempDrizzleConfig(cwd, databaseUrl, schemas) {
|
|
424
|
-
const kuckitDir = join$1(cwd, KUCKIT_DIR);
|
|
425
|
+
const kuckitDir = join$1(cwd, KUCKIT_DIR$9);
|
|
425
426
|
await mkdir(kuckitDir, { recursive: true });
|
|
426
427
|
const configPath = join$1(kuckitDir, TEMP_CONFIG_NAME);
|
|
427
428
|
const schemaPaths = schemas.map((s) => s.schemaPath);
|
|
@@ -473,7 +474,7 @@ function runDrizzleKit(command, configPath, cwd) {
|
|
|
473
474
|
});
|
|
474
475
|
}
|
|
475
476
|
async function runDbCommand(command, options) {
|
|
476
|
-
const projectRoot = await findProjectRoot(process.cwd());
|
|
477
|
+
const projectRoot = await findProjectRoot$9(process.cwd());
|
|
477
478
|
if (!projectRoot) {
|
|
478
479
|
console.error("Error: Could not find project root (no package.json found)");
|
|
479
480
|
process.exit(1);
|
|
@@ -521,7 +522,7 @@ async function dbStudio(options) {
|
|
|
521
522
|
const DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
522
523
|
const CONFIG_DIR = join(homedir(), ".kuckit");
|
|
523
524
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
524
|
-
function loadConfig() {
|
|
525
|
+
function loadConfig$8() {
|
|
525
526
|
try {
|
|
526
527
|
if (!existsSync(CONFIG_PATH)) return null;
|
|
527
528
|
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
@@ -530,7 +531,7 @@ function loadConfig() {
|
|
|
530
531
|
return null;
|
|
531
532
|
}
|
|
532
533
|
}
|
|
533
|
-
function saveConfig(config) {
|
|
534
|
+
function saveConfig$2(config) {
|
|
534
535
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, {
|
|
535
536
|
recursive: true,
|
|
536
537
|
mode: 448
|
|
@@ -574,7 +575,7 @@ function formatExpiryDate(expiresAt) {
|
|
|
574
575
|
});
|
|
575
576
|
}
|
|
576
577
|
async function authLogin(options) {
|
|
577
|
-
const config = loadConfig();
|
|
578
|
+
const config = loadConfig$8();
|
|
578
579
|
const serverUrl = getServerUrl(config, options.server);
|
|
579
580
|
const scopes = options.scopes?.split(",").map((s) => s.trim()) ?? ["cli"];
|
|
580
581
|
console.log("\nAuthenticating with Kuckit...\n");
|
|
@@ -639,14 +640,16 @@ async function authLogin(options) {
|
|
|
639
640
|
process.exit(1);
|
|
640
641
|
}
|
|
641
642
|
const expiresAt = new Date(Date.now() + pollResult.expiresIn * 1e3).toISOString();
|
|
642
|
-
saveConfig({
|
|
643
|
+
saveConfig$2({
|
|
643
644
|
...config,
|
|
644
645
|
cliToken: pollResult.token,
|
|
645
646
|
tokenExpiresAt: expiresAt,
|
|
647
|
+
permissions: pollResult.permissions,
|
|
646
648
|
serverUrl
|
|
647
649
|
});
|
|
648
650
|
console.log("✓ Successfully authenticated!");
|
|
649
651
|
console.log(` Token expires: ${formatExpiryDate(expiresAt)}`);
|
|
652
|
+
if (pollResult.permissions && pollResult.permissions.length > 0) console.log(` Permissions: ${pollResult.permissions.join(", ")}`);
|
|
650
653
|
console.log("");
|
|
651
654
|
return;
|
|
652
655
|
}
|
|
@@ -670,7 +673,7 @@ async function authLogin(options) {
|
|
|
670
673
|
process.exit(1);
|
|
671
674
|
}
|
|
672
675
|
async function authWhoami(options) {
|
|
673
|
-
const config = loadConfig();
|
|
676
|
+
const config = loadConfig$8();
|
|
674
677
|
if (!config?.cliToken) {
|
|
675
678
|
if (options.json) console.log(JSON.stringify({ authenticated: false }));
|
|
676
679
|
else console.log("Not logged in. Run 'kuckit auth login' to authenticate.");
|
|
@@ -689,7 +692,8 @@ async function authWhoami(options) {
|
|
|
689
692
|
userId: config.userId,
|
|
690
693
|
userName: config.userName,
|
|
691
694
|
serverUrl: config.serverUrl,
|
|
692
|
-
tokenExpiresAt: config.tokenExpiresAt
|
|
695
|
+
tokenExpiresAt: config.tokenExpiresAt,
|
|
696
|
+
permissions: config.permissions
|
|
693
697
|
}));
|
|
694
698
|
else {
|
|
695
699
|
console.log("\nAuthenticated");
|
|
@@ -697,11 +701,12 @@ async function authWhoami(options) {
|
|
|
697
701
|
if (config.userId) console.log(` ID: ${config.userId}`);
|
|
698
702
|
if (config.serverUrl) console.log(` Server: ${config.serverUrl}`);
|
|
699
703
|
if (config.tokenExpiresAt) console.log(` Token expires: ${formatExpiryDate(config.tokenExpiresAt)}`);
|
|
704
|
+
if (config.permissions && config.permissions.length > 0) console.log(` Permissions: ${config.permissions.join(", ")}`);
|
|
700
705
|
console.log("");
|
|
701
706
|
}
|
|
702
707
|
}
|
|
703
708
|
async function authLogout() {
|
|
704
|
-
if (!loadConfig()?.cliToken) {
|
|
709
|
+
if (!loadConfig$8()?.cliToken) {
|
|
705
710
|
console.log("Already logged out.");
|
|
706
711
|
return;
|
|
707
712
|
}
|
|
@@ -728,7 +733,7 @@ function registerAuthCommands(program$1) {
|
|
|
728
733
|
* Exits the process with an error message if not authenticated.
|
|
729
734
|
*/
|
|
730
735
|
function requireAuth() {
|
|
731
|
-
const config = loadConfig();
|
|
736
|
+
const config = loadConfig$8();
|
|
732
737
|
if (!config?.cliToken) {
|
|
733
738
|
console.error("\nError: Not logged in. Run 'kuckit auth login' first.\n");
|
|
734
739
|
process.exit(1);
|
|
@@ -740,43 +745,1857 @@ function requireAuth() {
|
|
|
740
745
|
}
|
|
741
746
|
|
|
742
747
|
//#endregion
|
|
743
|
-
//#region src/
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
748
|
+
//#region src/commands/infra/runner.ts
|
|
749
|
+
/**
|
|
750
|
+
* Run a Pulumi CLI command
|
|
751
|
+
*/
|
|
752
|
+
function runPulumi(args, options) {
|
|
753
|
+
return new Promise((resolve) => {
|
|
754
|
+
const proc = spawn("pulumi", args, {
|
|
755
|
+
cwd: options.cwd,
|
|
756
|
+
stdio: options.stream ? [
|
|
757
|
+
"inherit",
|
|
758
|
+
"pipe",
|
|
759
|
+
"pipe"
|
|
760
|
+
] : [
|
|
761
|
+
"pipe",
|
|
762
|
+
"pipe",
|
|
763
|
+
"pipe"
|
|
764
|
+
],
|
|
765
|
+
shell: true,
|
|
766
|
+
env: {
|
|
767
|
+
...process.env,
|
|
768
|
+
...options.env,
|
|
769
|
+
PULUMI_SKIP_UPDATE_CHECK: "true"
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
let stdout = "";
|
|
773
|
+
let stderr = "";
|
|
774
|
+
proc.stdout?.on("data", (data) => {
|
|
775
|
+
const str = data.toString();
|
|
776
|
+
stdout += str;
|
|
777
|
+
if (options.stream) process.stdout.write(data);
|
|
778
|
+
});
|
|
779
|
+
proc.stderr?.on("data", (data) => {
|
|
780
|
+
const str = data.toString();
|
|
781
|
+
stderr += str;
|
|
782
|
+
if (options.stream) process.stderr.write(data);
|
|
783
|
+
});
|
|
784
|
+
proc.on("close", (code) => {
|
|
785
|
+
resolve({
|
|
786
|
+
code: code ?? 1,
|
|
787
|
+
stdout,
|
|
788
|
+
stderr
|
|
789
|
+
});
|
|
790
|
+
});
|
|
764
791
|
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Run pulumi stack output --json and parse the result
|
|
795
|
+
*/
|
|
796
|
+
async function getPulumiOutputs(options) {
|
|
797
|
+
const result = await runPulumi([
|
|
798
|
+
"stack",
|
|
799
|
+
"output",
|
|
800
|
+
"--json"
|
|
801
|
+
], options);
|
|
802
|
+
if (result.code !== 0) return null;
|
|
803
|
+
try {
|
|
804
|
+
return JSON.parse(result.stdout);
|
|
805
|
+
} catch {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Run pulumi stack --json and parse the result
|
|
811
|
+
*/
|
|
812
|
+
async function getPulumiStackInfo(options) {
|
|
813
|
+
const result = await runPulumi(["stack", "--json"], options);
|
|
814
|
+
if (result.code !== 0) return null;
|
|
815
|
+
try {
|
|
816
|
+
const data = JSON.parse(result.stdout);
|
|
817
|
+
return {
|
|
818
|
+
name: data.name ?? "",
|
|
819
|
+
current: data.current ?? false,
|
|
820
|
+
updateInProgress: data.updateInProgress ?? false,
|
|
821
|
+
resourceCount: data.resourceCount ?? 0,
|
|
822
|
+
lastUpdate: data.lastUpdate,
|
|
823
|
+
url: data.url
|
|
824
|
+
};
|
|
825
|
+
} catch {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Run pulumi stack history --json and parse the result
|
|
831
|
+
*/
|
|
832
|
+
async function getPulumiStackHistory(options, limit = 5) {
|
|
833
|
+
const result = await runPulumi([
|
|
834
|
+
"stack",
|
|
835
|
+
"history",
|
|
836
|
+
"--json",
|
|
837
|
+
"--show-secrets=false"
|
|
838
|
+
], options);
|
|
839
|
+
if (result.code !== 0) return [];
|
|
840
|
+
try {
|
|
841
|
+
const data = JSON.parse(result.stdout);
|
|
842
|
+
if (!Array.isArray(data)) return [];
|
|
843
|
+
return data.slice(0, limit).map((entry) => ({
|
|
844
|
+
version: entry.version ?? 0,
|
|
845
|
+
startTime: entry.startTime ?? "",
|
|
846
|
+
endTime: entry.endTime ?? "",
|
|
847
|
+
result: entry.result ?? "failed",
|
|
848
|
+
resourceChanges: entry.resourceChanges
|
|
849
|
+
}));
|
|
850
|
+
} catch {
|
|
851
|
+
return [];
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Select or create a Pulumi stack
|
|
856
|
+
*/
|
|
857
|
+
async function selectOrCreateStack(stackName, options) {
|
|
858
|
+
if ((await runPulumi([
|
|
859
|
+
"stack",
|
|
860
|
+
"select",
|
|
861
|
+
stackName
|
|
862
|
+
], options)).code === 0) return true;
|
|
863
|
+
return (await runPulumi([
|
|
864
|
+
"stack",
|
|
865
|
+
"init",
|
|
866
|
+
stackName
|
|
867
|
+
], options)).code === 0;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Set a Pulumi config value
|
|
871
|
+
*/
|
|
872
|
+
async function setPulumiConfig(key, value, options) {
|
|
873
|
+
return (await runPulumi([
|
|
874
|
+
"config",
|
|
875
|
+
"set",
|
|
876
|
+
key,
|
|
877
|
+
value
|
|
878
|
+
], options)).code === 0;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Run pulumi up with optional preview mode
|
|
882
|
+
*/
|
|
883
|
+
async function pulumiUp(options) {
|
|
884
|
+
return runPulumi(options.preview ? ["preview"] : ["up", "--yes"], {
|
|
885
|
+
...options,
|
|
886
|
+
stream: true
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Run pulumi destroy
|
|
891
|
+
*/
|
|
892
|
+
async function pulumiDestroy(options) {
|
|
893
|
+
return runPulumi(options.force ? [
|
|
894
|
+
"destroy",
|
|
895
|
+
"--yes",
|
|
896
|
+
"-f"
|
|
897
|
+
] : ["destroy", "--yes"], {
|
|
898
|
+
...options,
|
|
899
|
+
stream: true
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Run pulumi cancel to cancel stuck operations
|
|
904
|
+
*/
|
|
905
|
+
async function pulumiCancel(options) {
|
|
906
|
+
return runPulumi(["cancel", "--yes"], options);
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Run pulumi refresh to sync state with cloud provider
|
|
910
|
+
*/
|
|
911
|
+
async function pulumiRefresh(options) {
|
|
912
|
+
return runPulumi(["refresh", "--yes"], {
|
|
913
|
+
...options,
|
|
914
|
+
stream: true
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Export Pulumi stack state to a file
|
|
919
|
+
*/
|
|
920
|
+
async function pulumiStackExport(filePath, options) {
|
|
921
|
+
return runPulumi([
|
|
922
|
+
"stack",
|
|
923
|
+
"export",
|
|
924
|
+
"--file",
|
|
925
|
+
filePath
|
|
926
|
+
], options);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Run a gcloud CLI command
|
|
930
|
+
*/
|
|
931
|
+
function runGcloud(args, options = {}) {
|
|
932
|
+
return new Promise((resolve) => {
|
|
933
|
+
const proc = spawn("gcloud", args, {
|
|
934
|
+
cwd: options.cwd ?? process.cwd(),
|
|
935
|
+
stdio: options.stream ? [
|
|
936
|
+
"inherit",
|
|
937
|
+
"pipe",
|
|
938
|
+
"pipe"
|
|
939
|
+
] : [
|
|
940
|
+
"pipe",
|
|
941
|
+
"pipe",
|
|
942
|
+
"pipe"
|
|
943
|
+
],
|
|
944
|
+
shell: true
|
|
945
|
+
});
|
|
946
|
+
let stdout = "";
|
|
947
|
+
let stderr = "";
|
|
948
|
+
proc.stdout?.on("data", (data) => {
|
|
949
|
+
const str = data.toString();
|
|
950
|
+
stdout += str;
|
|
951
|
+
if (options.stream) process.stdout.write(data);
|
|
952
|
+
});
|
|
953
|
+
proc.stderr?.on("data", (data) => {
|
|
954
|
+
const str = data.toString();
|
|
955
|
+
stderr += str;
|
|
956
|
+
if (options.stream) process.stderr.write(data);
|
|
957
|
+
});
|
|
958
|
+
proc.on("close", (code) => {
|
|
959
|
+
resolve({
|
|
960
|
+
code: code ?? 1,
|
|
961
|
+
stdout,
|
|
962
|
+
stderr
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Build and push Docker image using Cloud Build
|
|
969
|
+
* No local Docker daemon required!
|
|
970
|
+
*/
|
|
971
|
+
async function buildAndPushImage(options) {
|
|
972
|
+
const tag = options.tag ?? process.env.GIT_SHA ?? "latest";
|
|
973
|
+
const imageUrl = `${options.registryUrl}/kuckit:${tag}`;
|
|
974
|
+
console.log(`Building image: ${imageUrl}`);
|
|
975
|
+
const args = [
|
|
976
|
+
"builds",
|
|
977
|
+
"submit",
|
|
978
|
+
"--tag",
|
|
979
|
+
imageUrl,
|
|
980
|
+
"--project",
|
|
981
|
+
options.project
|
|
982
|
+
];
|
|
983
|
+
if (options.dockerfile && options.dockerfile !== "Dockerfile") args.push("--config", options.dockerfile);
|
|
984
|
+
return {
|
|
985
|
+
code: (await runGcloud(args, {
|
|
986
|
+
stream: true,
|
|
987
|
+
cwd: options.cwd
|
|
988
|
+
})).code,
|
|
989
|
+
imageUrl
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Get the path to the packages/infra directory
|
|
994
|
+
*/
|
|
995
|
+
function getInfraDir(projectRoot) {
|
|
996
|
+
return join$1(projectRoot, "packages", "infra");
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Check if Pulumi CLI is installed
|
|
1000
|
+
*/
|
|
1001
|
+
async function checkPulumiInstalled() {
|
|
1002
|
+
try {
|
|
1003
|
+
return (await runPulumi(["version"], { cwd: process.cwd() })).code === 0;
|
|
1004
|
+
} catch {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Check if gcloud CLI is installed
|
|
1010
|
+
*/
|
|
1011
|
+
async function checkGcloudInstalled() {
|
|
1012
|
+
try {
|
|
1013
|
+
return (await runGcloud(["version"])).code === 0;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* List Cloud Run service revisions
|
|
1020
|
+
*/
|
|
1021
|
+
async function listCloudRunRevisions(serviceName, project, region) {
|
|
1022
|
+
const revisionsResult = await runGcloud([
|
|
1023
|
+
"run",
|
|
1024
|
+
"revisions",
|
|
1025
|
+
"list",
|
|
1026
|
+
"--service",
|
|
1027
|
+
serviceName,
|
|
1028
|
+
"--project",
|
|
1029
|
+
project,
|
|
1030
|
+
"--region",
|
|
1031
|
+
region,
|
|
1032
|
+
"--format",
|
|
1033
|
+
"json"
|
|
1034
|
+
]);
|
|
1035
|
+
if (revisionsResult.code !== 0) return [];
|
|
1036
|
+
const trafficResult = await runGcloud([
|
|
1037
|
+
"run",
|
|
1038
|
+
"services",
|
|
1039
|
+
"describe",
|
|
1040
|
+
serviceName,
|
|
1041
|
+
"--project",
|
|
1042
|
+
project,
|
|
1043
|
+
"--region",
|
|
1044
|
+
region,
|
|
1045
|
+
"--format",
|
|
1046
|
+
"json"
|
|
1047
|
+
]);
|
|
1048
|
+
const trafficMap = {};
|
|
1049
|
+
if (trafficResult.code === 0) try {
|
|
1050
|
+
const traffic = JSON.parse(trafficResult.stdout).status?.traffic ?? [];
|
|
1051
|
+
for (const t of traffic) if (t.revisionName && t.percent) trafficMap[t.revisionName] = t.percent;
|
|
1052
|
+
} catch {}
|
|
1053
|
+
try {
|
|
1054
|
+
return JSON.parse(revisionsResult.stdout).map((rev) => {
|
|
1055
|
+
const name = rev.metadata?.name ?? "";
|
|
1056
|
+
const trafficPercent = trafficMap[name] ?? 0;
|
|
1057
|
+
return {
|
|
1058
|
+
name,
|
|
1059
|
+
createTime: rev.metadata?.creationTimestamp ?? "",
|
|
1060
|
+
active: trafficPercent > 0,
|
|
1061
|
+
trafficPercent
|
|
1062
|
+
};
|
|
1063
|
+
});
|
|
1064
|
+
} catch {
|
|
1065
|
+
return [];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Update Cloud Run traffic routing to a specific revision
|
|
1070
|
+
*/
|
|
1071
|
+
async function updateCloudRunTraffic(serviceName, revisionName, project, region) {
|
|
1072
|
+
return runGcloud([
|
|
1073
|
+
"run",
|
|
1074
|
+
"services",
|
|
1075
|
+
"update-traffic",
|
|
1076
|
+
serviceName,
|
|
1077
|
+
"--to-revisions",
|
|
1078
|
+
`${revisionName}=100`,
|
|
1079
|
+
"--project",
|
|
1080
|
+
project,
|
|
1081
|
+
"--region",
|
|
1082
|
+
region
|
|
1083
|
+
], { stream: true });
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Extract service name from Cloud Run URL
|
|
1087
|
+
* e.g., https://myapp-abc123-uc.a.run.app -> myapp
|
|
1088
|
+
*/
|
|
1089
|
+
function extractServiceNameFromUrl(url) {
|
|
1090
|
+
try {
|
|
1091
|
+
const hostname = new URL(url).hostname;
|
|
1092
|
+
const match = hostname.match(/^([^-]+(?:-[^-]+)*?)-[a-z0-9]+-[a-z]+\.a\.run\.app$/);
|
|
1093
|
+
if (match) return match[1];
|
|
1094
|
+
const parts = hostname.split("-");
|
|
1095
|
+
if (parts.length >= 3) return parts.slice(0, -2).join("-");
|
|
1096
|
+
return null;
|
|
1097
|
+
} catch {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/commands/infra/errors.ts
|
|
1104
|
+
/**
|
|
1105
|
+
* Error parsing for Pulumi and infrastructure operations
|
|
1106
|
+
*/
|
|
1107
|
+
/**
|
|
1108
|
+
* Types of errors that can occur during infrastructure operations
|
|
1109
|
+
*/
|
|
1110
|
+
let PulumiErrorType = /* @__PURE__ */ function(PulumiErrorType$1) {
|
|
1111
|
+
/** Another update is already in progress */
|
|
1112
|
+
PulumiErrorType$1["ConcurrentUpdate"] = "CONCURRENT_UPDATE";
|
|
1113
|
+
/** Resource already exists */
|
|
1114
|
+
PulumiErrorType$1["ResourceConflict"] = "RESOURCE_CONFLICT";
|
|
1115
|
+
/** GCP quota exceeded */
|
|
1116
|
+
PulumiErrorType$1["QuotaExceeded"] = "QUOTA_EXCEEDED";
|
|
1117
|
+
/** Permission denied (IAM issues) */
|
|
1118
|
+
PulumiErrorType$1["PermissionDenied"] = "PERMISSION_DENIED";
|
|
1119
|
+
/** State corruption or pending operations */
|
|
1120
|
+
PulumiErrorType$1["StateCorruption"] = "STATE_CORRUPTION";
|
|
1121
|
+
/** Network or transient failure */
|
|
1122
|
+
PulumiErrorType$1["NetworkError"] = "NETWORK_ERROR";
|
|
1123
|
+
/** Resource not found */
|
|
1124
|
+
PulumiErrorType$1["ResourceNotFound"] = "RESOURCE_NOT_FOUND";
|
|
1125
|
+
/** Invalid configuration */
|
|
1126
|
+
PulumiErrorType$1["InvalidConfig"] = "INVALID_CONFIG";
|
|
1127
|
+
/** Unknown error */
|
|
1128
|
+
PulumiErrorType$1["Unknown"] = "UNKNOWN";
|
|
1129
|
+
return PulumiErrorType$1;
|
|
1130
|
+
}({});
|
|
1131
|
+
/**
|
|
1132
|
+
* Error patterns and their mappings
|
|
1133
|
+
*/
|
|
1134
|
+
const ERROR_PATTERNS = [
|
|
1135
|
+
{
|
|
1136
|
+
pattern: /conflict.*concurrent.*update|another update is in progress|lock/i,
|
|
1137
|
+
type: PulumiErrorType.ConcurrentUpdate,
|
|
1138
|
+
suggestion: "Run: kuckit infra repair --cancel",
|
|
1139
|
+
retryable: false
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
pattern: /already exists|resource.*conflict|duplicate/i,
|
|
1143
|
+
type: PulumiErrorType.ResourceConflict,
|
|
1144
|
+
suggestion: "Run: kuckit infra repair --refresh",
|
|
1145
|
+
retryable: false
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
pattern: /quota.*exceeded|limit.*reached|resource exhausted/i,
|
|
1149
|
+
type: PulumiErrorType.QuotaExceeded,
|
|
1150
|
+
suggestion: "Request quota increase in GCP Console: https://console.cloud.google.com/iam-admin/quotas",
|
|
1151
|
+
retryable: false
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
pattern: /permission.*denied|access.*denied|forbidden|unauthorized|403/i,
|
|
1155
|
+
type: PulumiErrorType.PermissionDenied,
|
|
1156
|
+
suggestion: "Check IAM permissions in GCP Console. Ensure your account has the required roles.",
|
|
1157
|
+
retryable: false
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
pattern: /pending.*operation|state.*corrupt|inconsistent.*state/i,
|
|
1161
|
+
type: PulumiErrorType.StateCorruption,
|
|
1162
|
+
suggestion: "Run: kuckit infra repair --refresh",
|
|
1163
|
+
retryable: false
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
pattern: /network.*error|connection.*refused|timeout|ETIMEDOUT|ECONNRESET/i,
|
|
1167
|
+
type: PulumiErrorType.NetworkError,
|
|
1168
|
+
suggestion: "Check your network connection and try again.",
|
|
1169
|
+
retryable: true
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
pattern: /not found|does not exist|404/i,
|
|
1173
|
+
type: PulumiErrorType.ResourceNotFound,
|
|
1174
|
+
suggestion: "The resource may have been deleted externally. Run: kuckit infra repair --refresh",
|
|
1175
|
+
retryable: false
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
pattern: /invalid.*config|configuration.*error|missing.*required/i,
|
|
1179
|
+
type: PulumiErrorType.InvalidConfig,
|
|
1180
|
+
suggestion: "Check your configuration in .kuckit/infra.json and Pulumi stack config.",
|
|
1181
|
+
retryable: false
|
|
1182
|
+
}
|
|
1183
|
+
];
|
|
1184
|
+
/**
|
|
1185
|
+
* Extract resource name from error output
|
|
1186
|
+
*/
|
|
1187
|
+
function extractResourceName(error) {
|
|
1188
|
+
const urnMatch = error.match(/urn:pulumi:[^:]+:[^:]+::([^:]+:[^:\s]+)/i);
|
|
1189
|
+
if (urnMatch) return urnMatch[1];
|
|
1190
|
+
const gcpMatch = error.match(/projects\/[^/]+\/(?:locations|regions)\/[^/]+\/([^/\s]+\/[^/\s]+)/i);
|
|
1191
|
+
if (gcpMatch) return gcpMatch[1];
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Parse an error output and return a structured ParsedError
|
|
1195
|
+
*/
|
|
1196
|
+
function parseError(error) {
|
|
1197
|
+
for (const { pattern, type, suggestion, retryable } of ERROR_PATTERNS) if (pattern.test(error)) return {
|
|
1198
|
+
type,
|
|
1199
|
+
message: getErrorMessage(type, error),
|
|
1200
|
+
resource: extractResourceName(error),
|
|
1201
|
+
suggestion,
|
|
1202
|
+
retryable,
|
|
1203
|
+
originalError: error
|
|
1204
|
+
};
|
|
1205
|
+
return {
|
|
1206
|
+
type: PulumiErrorType.Unknown,
|
|
1207
|
+
message: "An unknown error occurred during infrastructure operation.",
|
|
1208
|
+
resource: extractResourceName(error),
|
|
1209
|
+
suggestion: "Check the error output above. If the issue persists, run: kuckit infra repair --refresh",
|
|
1210
|
+
retryable: false,
|
|
1211
|
+
originalError: error
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Get a human-readable error message for an error type
|
|
1216
|
+
*/
|
|
1217
|
+
function getErrorMessage(type, originalError) {
|
|
1218
|
+
switch (type) {
|
|
1219
|
+
case PulumiErrorType.ConcurrentUpdate: return "Another infrastructure update is already in progress.";
|
|
1220
|
+
case PulumiErrorType.ResourceConflict: return "A resource with this name already exists.";
|
|
1221
|
+
case PulumiErrorType.QuotaExceeded: return "GCP quota limit has been reached.";
|
|
1222
|
+
case PulumiErrorType.PermissionDenied: return "Permission denied. Check your IAM permissions.";
|
|
1223
|
+
case PulumiErrorType.StateCorruption: return "Infrastructure state is inconsistent.";
|
|
1224
|
+
case PulumiErrorType.NetworkError: return "Network error occurred. This may be a transient issue.";
|
|
1225
|
+
case PulumiErrorType.ResourceNotFound: return "A required resource was not found.";
|
|
1226
|
+
case PulumiErrorType.InvalidConfig: return "Invalid configuration detected.";
|
|
1227
|
+
default: return originalError.split("\n").find((line) => line.trim().length > 0)?.slice(0, 200) ?? "An error occurred.";
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Format a ParsedError for console output
|
|
1232
|
+
*/
|
|
1233
|
+
function formatError(error) {
|
|
1234
|
+
const lines = [`Error: ${error.message}`, ""];
|
|
1235
|
+
if (error.resource) lines.push(`Resource: ${error.resource}`);
|
|
1236
|
+
lines.push(`Suggestion: ${error.suggestion}`);
|
|
1237
|
+
return lines.join("\n");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
//#endregion
|
|
1241
|
+
//#region src/commands/infra/init.ts
|
|
1242
|
+
const KUCKIT_DIR$8 = ".kuckit";
|
|
1243
|
+
const CONFIG_FILE$8 = "infra.json";
|
|
1244
|
+
async function fileExists$8(path) {
|
|
1245
|
+
try {
|
|
1246
|
+
await access(path, constants$1.F_OK);
|
|
1247
|
+
return true;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
async function findProjectRoot$8(cwd) {
|
|
1253
|
+
let dir = cwd;
|
|
1254
|
+
while (dir !== dirname$1(dir)) {
|
|
1255
|
+
if (await fileExists$8(join$1(dir, "package.json"))) return dir;
|
|
1256
|
+
dir = dirname$1(dir);
|
|
1257
|
+
}
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
async function loadExistingConfig(projectRoot) {
|
|
1261
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$8, CONFIG_FILE$8);
|
|
1262
|
+
if (!await fileExists$8(configPath)) return null;
|
|
1263
|
+
try {
|
|
1264
|
+
const content = await readFile(configPath, "utf-8");
|
|
1265
|
+
return JSON.parse(content);
|
|
1266
|
+
} catch {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async function saveConfig$1(projectRoot, config) {
|
|
1271
|
+
const kuckitDir = join$1(projectRoot, KUCKIT_DIR$8);
|
|
1272
|
+
await mkdir(kuckitDir, { recursive: true });
|
|
1273
|
+
await writeFile(join$1(kuckitDir, CONFIG_FILE$8), JSON.stringify(config, null, 2), "utf-8");
|
|
1274
|
+
}
|
|
1275
|
+
async function infraInit(options) {
|
|
1276
|
+
console.log("Initializing kuckit infrastructure...\n");
|
|
1277
|
+
if (!await checkPulumiInstalled()) {
|
|
1278
|
+
console.error("Error: Pulumi CLI is not installed.");
|
|
1279
|
+
console.error("Install it from: https://www.pulumi.com/docs/install/");
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
if (!await checkGcloudInstalled()) {
|
|
1283
|
+
console.error("Error: gcloud CLI is not installed.");
|
|
1284
|
+
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
1285
|
+
process.exit(1);
|
|
1286
|
+
}
|
|
1287
|
+
const projectRoot = await findProjectRoot$8(process.cwd());
|
|
1288
|
+
if (!projectRoot) {
|
|
1289
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
const infraDir = getInfraDir(projectRoot);
|
|
1293
|
+
if (!await fileExists$8(infraDir)) {
|
|
1294
|
+
console.error("Error: packages/infra not found.");
|
|
1295
|
+
console.error("Make sure you have the @kuckit/infra package in your project.");
|
|
1296
|
+
process.exit(1);
|
|
1297
|
+
}
|
|
1298
|
+
const existingConfig = await loadExistingConfig(projectRoot);
|
|
1299
|
+
const provider = options.provider ?? "gcp";
|
|
1300
|
+
if (provider !== "gcp") {
|
|
1301
|
+
console.error(`Error: Provider '${provider}' is not supported. Only 'gcp' is currently supported.`);
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
1304
|
+
let gcpProject = options.project ?? existingConfig?.gcpProject;
|
|
1305
|
+
if (!gcpProject) gcpProject = await input({
|
|
1306
|
+
message: "GCP Project ID:",
|
|
1307
|
+
validate: (value) => value.length > 0 ? true : "Project ID is required"
|
|
1308
|
+
});
|
|
1309
|
+
let region = options.region ?? existingConfig?.region ?? "us-central1";
|
|
1310
|
+
if (!options.region && !existingConfig?.region) region = await input({
|
|
1311
|
+
message: "GCP Region:",
|
|
1312
|
+
default: "us-central1"
|
|
1313
|
+
});
|
|
1314
|
+
let env = options.env ?? existingConfig?.env ?? "dev";
|
|
1315
|
+
if (!options.env && !existingConfig?.env) env = await input({
|
|
1316
|
+
message: "Environment (dev/prod):",
|
|
1317
|
+
default: "dev",
|
|
1318
|
+
validate: (value) => value === "dev" || value === "prod" ? true : "Must be dev or prod"
|
|
1319
|
+
});
|
|
1320
|
+
const stackName = `${gcpProject}-${env}`;
|
|
1321
|
+
console.log("\nConfiguration:");
|
|
1322
|
+
console.log(` Provider: ${provider}`);
|
|
1323
|
+
console.log(` GCP Project: ${gcpProject}`);
|
|
1324
|
+
console.log(` Region: ${region}`);
|
|
1325
|
+
console.log(` Environment: ${env}`);
|
|
1326
|
+
console.log(` Stack: ${stackName}`);
|
|
1327
|
+
console.log("");
|
|
1328
|
+
if (!options.yes) {
|
|
1329
|
+
if (!await confirm({
|
|
1330
|
+
message: "Proceed with infrastructure initialization?",
|
|
1331
|
+
default: true
|
|
1332
|
+
})) {
|
|
1333
|
+
console.log("Aborted.");
|
|
1334
|
+
process.exit(0);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
console.log("\nInstalling infrastructure dependencies...");
|
|
1338
|
+
const { spawn: spawn$1 } = await import("child_process");
|
|
1339
|
+
await new Promise((resolve, reject) => {
|
|
1340
|
+
spawn$1("bun", ["install"], {
|
|
1341
|
+
cwd: infraDir,
|
|
1342
|
+
stdio: "inherit",
|
|
1343
|
+
shell: true
|
|
1344
|
+
}).on("close", (code) => {
|
|
1345
|
+
if (code === 0) resolve();
|
|
1346
|
+
else reject(/* @__PURE__ */ new Error(`bun install failed with code ${code}`));
|
|
1347
|
+
});
|
|
1348
|
+
});
|
|
1349
|
+
console.log("\nBuilding infrastructure package...");
|
|
1350
|
+
await new Promise((resolve, reject) => {
|
|
1351
|
+
spawn$1("bun", ["run", "build"], {
|
|
1352
|
+
cwd: infraDir,
|
|
1353
|
+
stdio: "inherit",
|
|
1354
|
+
shell: true
|
|
1355
|
+
}).on("close", (code) => {
|
|
1356
|
+
if (code === 0) resolve();
|
|
1357
|
+
else reject(/* @__PURE__ */ new Error(`Build failed with code ${code}`));
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
console.log(`\nSelecting Pulumi stack: ${stackName}`);
|
|
1361
|
+
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
1362
|
+
console.error("Error: Failed to select or create Pulumi stack");
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
console.log("Configuring stack...");
|
|
1366
|
+
await setPulumiConfig("gcp:project", gcpProject, { cwd: infraDir });
|
|
1367
|
+
await setPulumiConfig("gcp:region", region, { cwd: infraDir });
|
|
1368
|
+
await setPulumiConfig("env", env, { cwd: infraDir });
|
|
1369
|
+
console.log("\nCreating infrastructure...");
|
|
1370
|
+
console.log("This may take several minutes.\n");
|
|
1371
|
+
const result = await pulumiUp({
|
|
1372
|
+
cwd: infraDir,
|
|
1373
|
+
stream: true
|
|
1374
|
+
});
|
|
1375
|
+
if (result.code !== 0) {
|
|
1376
|
+
const parsed = parseError(result.stderr);
|
|
1377
|
+
console.error("\n" + formatError(parsed));
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1380
|
+
console.log("\nRetrieving outputs...");
|
|
1381
|
+
const outputs = await getPulumiOutputs({ cwd: infraDir });
|
|
1382
|
+
await saveConfig$1(projectRoot, {
|
|
1383
|
+
provider: "gcp",
|
|
1384
|
+
gcpProject,
|
|
1385
|
+
region,
|
|
1386
|
+
projectName: "kuckit-infra",
|
|
1387
|
+
stackName,
|
|
1388
|
+
env,
|
|
1389
|
+
outputs: outputs ? {
|
|
1390
|
+
registryUrl: outputs.registryUrl,
|
|
1391
|
+
databaseConnectionName: outputs.databaseConnectionName,
|
|
1392
|
+
redisHost: outputs.redisHost,
|
|
1393
|
+
secretIds: {
|
|
1394
|
+
dbPassword: outputs.dbPasswordSecretId,
|
|
1395
|
+
redisAuth: outputs.redisAuthSecretId ?? ""
|
|
1396
|
+
}
|
|
1397
|
+
} : void 0,
|
|
1398
|
+
createdAt: existingConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1399
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1400
|
+
});
|
|
1401
|
+
console.log(`\nConfiguration saved to ${KUCKIT_DIR$8}/${CONFIG_FILE$8}`);
|
|
1402
|
+
console.log("\n" + "=".repeat(60));
|
|
1403
|
+
console.log("Infrastructure initialized successfully!");
|
|
1404
|
+
console.log("=".repeat(60));
|
|
1405
|
+
if (outputs) {
|
|
1406
|
+
console.log("\nOutputs:");
|
|
1407
|
+
console.log(` Registry: ${outputs.registryUrl}`);
|
|
1408
|
+
console.log(` Database: ${outputs.databaseConnectionName}`);
|
|
1409
|
+
console.log(` Redis: ${outputs.redisHost}`);
|
|
1410
|
+
}
|
|
1411
|
+
console.log("\nNext steps:");
|
|
1412
|
+
console.log(" 1. Ensure you have a Dockerfile in your project root");
|
|
1413
|
+
console.log(` 2. Run: kuckit infra deploy --env ${env}`);
|
|
1414
|
+
console.log("");
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region src/commands/infra/deploy.ts
|
|
1419
|
+
const KUCKIT_DIR$7 = ".kuckit";
|
|
1420
|
+
const CONFIG_FILE$7 = "infra.json";
|
|
1421
|
+
async function saveConfig(projectRoot, config) {
|
|
1422
|
+
const kuckitDir = join$1(projectRoot, KUCKIT_DIR$7);
|
|
1423
|
+
await mkdir(kuckitDir, { recursive: true });
|
|
1424
|
+
await writeFile(join$1(kuckitDir, CONFIG_FILE$7), JSON.stringify(config, null, 2), "utf-8");
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Get the current git commit short hash
|
|
1428
|
+
* Returns null if not in a git repo or git command fails
|
|
1429
|
+
*/
|
|
1430
|
+
function getGitShortHash() {
|
|
1431
|
+
try {
|
|
1432
|
+
return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
1433
|
+
} catch {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
async function fileExists$7(path) {
|
|
1438
|
+
try {
|
|
1439
|
+
await access(path, constants$1.F_OK);
|
|
1440
|
+
return true;
|
|
1441
|
+
} catch {
|
|
1442
|
+
return false;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function findProjectRoot$7(cwd) {
|
|
1446
|
+
let dir = cwd;
|
|
1447
|
+
while (dir !== dirname$1(dir)) {
|
|
1448
|
+
if (await fileExists$7(join$1(dir, "package.json"))) return dir;
|
|
1449
|
+
dir = dirname$1(dir);
|
|
1450
|
+
}
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
async function loadConfig$7(projectRoot) {
|
|
1454
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$7, CONFIG_FILE$7);
|
|
1455
|
+
if (!await fileExists$7(configPath)) return null;
|
|
1456
|
+
try {
|
|
1457
|
+
const content = await readFile(configPath, "utf-8");
|
|
1458
|
+
return JSON.parse(content);
|
|
1459
|
+
} catch {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
async function infraDeploy(options) {
|
|
1464
|
+
console.log("Deploying kuckit application...\n");
|
|
1465
|
+
if (!await checkPulumiInstalled()) {
|
|
1466
|
+
console.error("Error: Pulumi CLI is not installed.");
|
|
1467
|
+
console.error("Install it from: https://www.pulumi.com/docs/install/");
|
|
1468
|
+
process.exit(1);
|
|
1469
|
+
}
|
|
1470
|
+
if (!await checkGcloudInstalled()) {
|
|
1471
|
+
console.error("Error: gcloud CLI is not installed.");
|
|
1472
|
+
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
const projectRoot = await findProjectRoot$7(process.cwd());
|
|
1476
|
+
if (!projectRoot) {
|
|
1477
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
const config = await loadConfig$7(projectRoot);
|
|
1481
|
+
if (!config) {
|
|
1482
|
+
console.error("Error: No infrastructure configuration found.");
|
|
1483
|
+
console.error("Run: kuckit infra init");
|
|
1484
|
+
process.exit(1);
|
|
1485
|
+
}
|
|
1486
|
+
const env = options.env ?? config.env;
|
|
1487
|
+
if (env !== "dev" && env !== "prod") {
|
|
1488
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
const infraDir = getInfraDir(projectRoot);
|
|
1492
|
+
if (!await fileExists$7(infraDir)) {
|
|
1493
|
+
console.error("Error: packages/infra not found.");
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1496
|
+
const dockerfilePath = join$1(projectRoot, "Dockerfile");
|
|
1497
|
+
if (!options.skipBuild && !options.image && !await fileExists$7(dockerfilePath)) {
|
|
1498
|
+
console.error("Error: Dockerfile not found in project root.");
|
|
1499
|
+
console.error("Create a Dockerfile or use --skip-build with an existing image.");
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
}
|
|
1502
|
+
if (!config.outputs?.registryUrl) {
|
|
1503
|
+
console.error("Error: No registry URL found in configuration.");
|
|
1504
|
+
console.error("Run: kuckit infra init");
|
|
1505
|
+
process.exit(1);
|
|
1506
|
+
}
|
|
1507
|
+
console.log("Configuration:");
|
|
1508
|
+
console.log(` Project: ${config.gcpProject}`);
|
|
1509
|
+
console.log(` Region: ${config.region}`);
|
|
1510
|
+
console.log(` Environment: ${env}`);
|
|
1511
|
+
console.log(` Registry: ${config.outputs.registryUrl}`);
|
|
1512
|
+
if (options.preview) console.log(" Mode: Preview (no changes will be applied)");
|
|
1513
|
+
console.log("");
|
|
1514
|
+
let imageUrl;
|
|
1515
|
+
if (options.image) {
|
|
1516
|
+
imageUrl = options.image;
|
|
1517
|
+
console.log(`Using provided image: ${imageUrl}\n`);
|
|
1518
|
+
} else if (options.skipBuild) {
|
|
1519
|
+
imageUrl = `${config.outputs.registryUrl}/kuckit:latest`;
|
|
1520
|
+
console.log(`Using existing image: ${imageUrl}\n`);
|
|
1521
|
+
} else {
|
|
1522
|
+
console.log("Building and pushing Docker image...");
|
|
1523
|
+
console.log("(Using Cloud Build - no local Docker required)\n");
|
|
1524
|
+
const buildResult = await buildAndPushImage({
|
|
1525
|
+
project: config.gcpProject,
|
|
1526
|
+
registryUrl: config.outputs.registryUrl,
|
|
1527
|
+
tag: process.env.GIT_SHA ?? process.env.GITHUB_SHA ?? getGitShortHash() ?? "latest",
|
|
1528
|
+
cwd: projectRoot
|
|
1529
|
+
});
|
|
1530
|
+
if (buildResult.code !== 0) {
|
|
1531
|
+
console.error("\nError: Docker build failed.");
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
imageUrl = buildResult.imageUrl;
|
|
1535
|
+
console.log(`\nImage built: ${imageUrl}\n`);
|
|
1536
|
+
}
|
|
1537
|
+
if (!options.yes && !options.preview) {
|
|
1538
|
+
if (!await confirm({
|
|
1539
|
+
message: "Proceed with deployment?",
|
|
1540
|
+
default: true
|
|
1541
|
+
})) {
|
|
1542
|
+
console.log("Aborted.");
|
|
1543
|
+
process.exit(0);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
const stackName = config.stackName;
|
|
1547
|
+
console.log(`Selecting Pulumi stack: ${stackName}`);
|
|
1548
|
+
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
1549
|
+
console.error("Error: Failed to select Pulumi stack");
|
|
1550
|
+
process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
console.log("Configuring deployment...");
|
|
1553
|
+
await setPulumiConfig("imageUrl", imageUrl, { cwd: infraDir });
|
|
1554
|
+
if (options.preview) console.log("\nPreviewing changes...\n");
|
|
1555
|
+
else console.log("\nDeploying to Cloud Run...\n");
|
|
1556
|
+
const result = await pulumiUp({
|
|
1557
|
+
cwd: infraDir,
|
|
1558
|
+
stream: true,
|
|
1559
|
+
preview: options.preview
|
|
1560
|
+
});
|
|
1561
|
+
if (result.code !== 0) {
|
|
1562
|
+
const parsed = parseError(result.stderr);
|
|
1563
|
+
console.error("\n" + formatError(parsed));
|
|
1564
|
+
process.exit(1);
|
|
1565
|
+
}
|
|
1566
|
+
if (!options.preview) {
|
|
1567
|
+
console.log("\nRetrieving deployment info...");
|
|
1568
|
+
const outputs = await getPulumiOutputs({ cwd: infraDir });
|
|
1569
|
+
if (outputs) {
|
|
1570
|
+
config.outputs = {
|
|
1571
|
+
...config.outputs,
|
|
1572
|
+
...outputs
|
|
1573
|
+
};
|
|
1574
|
+
config.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1575
|
+
await saveConfig(projectRoot, config);
|
|
1576
|
+
}
|
|
1577
|
+
console.log("\n" + "=".repeat(60));
|
|
1578
|
+
console.log("Deployment successful!");
|
|
1579
|
+
console.log("=".repeat(60));
|
|
1580
|
+
if (outputs?.serviceUrl) console.log(`\nService URL: ${outputs.serviceUrl}`);
|
|
1581
|
+
if (outputs?.migrationJobName) console.log(`Migration Job: ${outputs.migrationJobName}`);
|
|
1582
|
+
console.log("\nUseful commands:");
|
|
1583
|
+
console.log(` View logs: kuckit infra logs --env ${env}`);
|
|
1584
|
+
console.log(` Check status: kuckit infra status --env ${env}`);
|
|
1585
|
+
console.log(` Run migrations: kuckit infra db:migrate --env ${env}`);
|
|
1586
|
+
console.log("");
|
|
1587
|
+
} else {
|
|
1588
|
+
console.log("\nPreview complete. No changes were applied.");
|
|
1589
|
+
console.log("Run without --preview to apply changes.");
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
//#endregion
|
|
1594
|
+
//#region src/commands/infra/destroy.ts
|
|
1595
|
+
const KUCKIT_DIR$6 = ".kuckit";
|
|
1596
|
+
const CONFIG_FILE$6 = "infra.json";
|
|
1597
|
+
async function fileExists$6(path) {
|
|
1598
|
+
try {
|
|
1599
|
+
await access(path, constants$1.F_OK);
|
|
1600
|
+
return true;
|
|
1601
|
+
} catch {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function findProjectRoot$6(cwd) {
|
|
1606
|
+
let dir = cwd;
|
|
1607
|
+
while (dir !== dirname$1(dir)) {
|
|
1608
|
+
if (await fileExists$6(join$1(dir, "package.json"))) return dir;
|
|
1609
|
+
dir = dirname$1(dir);
|
|
1610
|
+
}
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
async function loadConfig$6(projectRoot) {
|
|
1614
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$6, CONFIG_FILE$6);
|
|
1615
|
+
if (!await fileExists$6(configPath)) return null;
|
|
1616
|
+
try {
|
|
1617
|
+
const content = await readFile(configPath, "utf-8");
|
|
1618
|
+
return JSON.parse(content);
|
|
1619
|
+
} catch {
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
async function infraDestroy(options) {
|
|
1624
|
+
const isAppOnly = options.appOnly ?? false;
|
|
1625
|
+
if (isAppOnly) console.log("Destroying Cloud Run application (keeping database and Redis)...\n");
|
|
1626
|
+
else console.log("Destroying kuckit infrastructure...\n");
|
|
1627
|
+
if (!await checkPulumiInstalled()) {
|
|
1628
|
+
console.error("Error: Pulumi CLI is not installed.");
|
|
1629
|
+
console.error("Install it from: https://www.pulumi.com/docs/install/");
|
|
1630
|
+
process.exit(1);
|
|
1631
|
+
}
|
|
1632
|
+
const projectRoot = await findProjectRoot$6(process.cwd());
|
|
1633
|
+
if (!projectRoot) {
|
|
1634
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1635
|
+
process.exit(1);
|
|
1636
|
+
}
|
|
1637
|
+
const config = await loadConfig$6(projectRoot);
|
|
1638
|
+
if (!config) {
|
|
1639
|
+
console.error("Error: No infrastructure configuration found.");
|
|
1640
|
+
console.error("Nothing to destroy.");
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
}
|
|
1643
|
+
const env = options.env ?? config.env;
|
|
1644
|
+
if (env !== "dev" && env !== "prod") {
|
|
1645
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
const infraDir = getInfraDir(projectRoot);
|
|
1649
|
+
if (!await fileExists$6(infraDir)) {
|
|
1650
|
+
console.error("Error: packages/infra not found.");
|
|
1651
|
+
process.exit(1);
|
|
1652
|
+
}
|
|
1653
|
+
console.log("Configuration:");
|
|
1654
|
+
console.log(` Project: ${config.gcpProject}`);
|
|
1655
|
+
console.log(` Region: ${config.region}`);
|
|
1656
|
+
console.log(` Environment: ${env}`);
|
|
1657
|
+
console.log(` Stack: ${config.stackName}`);
|
|
1658
|
+
console.log("");
|
|
1659
|
+
if (isAppOnly) {
|
|
1660
|
+
console.log("WARNING: This will destroy the Cloud Run service.");
|
|
1661
|
+
console.log(" Database and Redis will be preserved.\n");
|
|
1662
|
+
} else {
|
|
1663
|
+
console.log("=".repeat(60));
|
|
1664
|
+
console.log("WARNING: DESTRUCTIVE OPERATION");
|
|
1665
|
+
console.log("=".repeat(60));
|
|
1666
|
+
console.log("");
|
|
1667
|
+
console.log("This will PERMANENTLY destroy:");
|
|
1668
|
+
console.log(" - Cloud Run service");
|
|
1669
|
+
console.log(" - Cloud SQL database (ALL DATA WILL BE LOST)");
|
|
1670
|
+
console.log(" - Memorystore Redis instance");
|
|
1671
|
+
console.log(" - Artifact Registry and all container images");
|
|
1672
|
+
console.log(" - All associated secrets");
|
|
1673
|
+
console.log("");
|
|
1674
|
+
console.log("This action CANNOT be undone.");
|
|
1675
|
+
console.log("");
|
|
1676
|
+
}
|
|
1677
|
+
if (!options.force) {
|
|
1678
|
+
if (!await confirm({
|
|
1679
|
+
message: isAppOnly ? "Are you sure you want to destroy the Cloud Run service?" : "Are you ABSOLUTELY sure you want to destroy ALL infrastructure?",
|
|
1680
|
+
default: false
|
|
1681
|
+
})) {
|
|
1682
|
+
console.log("Aborted.");
|
|
1683
|
+
process.exit(0);
|
|
1684
|
+
}
|
|
1685
|
+
if (!isAppOnly) {
|
|
1686
|
+
if (!await confirm({
|
|
1687
|
+
message: "This will delete ALL data. Type \"yes\" to confirm:",
|
|
1688
|
+
default: false
|
|
1689
|
+
})) {
|
|
1690
|
+
console.log("Aborted.");
|
|
1691
|
+
process.exit(0);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const stackName = config.stackName;
|
|
1696
|
+
console.log(`\nSelecting Pulumi stack: ${stackName}`);
|
|
1697
|
+
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
1698
|
+
console.error("Error: Failed to select Pulumi stack");
|
|
1699
|
+
process.exit(1);
|
|
1700
|
+
}
|
|
1701
|
+
if (isAppOnly) {
|
|
1702
|
+
console.log("\nRemoving Cloud Run service...");
|
|
1703
|
+
console.log("(Database and Redis will be preserved)\n");
|
|
1704
|
+
await setPulumiConfig("imageUrl", "", { cwd: infraDir });
|
|
1705
|
+
const result = await pulumiUp({
|
|
1706
|
+
cwd: infraDir,
|
|
1707
|
+
stream: true
|
|
1708
|
+
});
|
|
1709
|
+
if (result.code !== 0) {
|
|
1710
|
+
const parsed = parseError(result.stderr);
|
|
1711
|
+
console.error("\n" + formatError(parsed));
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
console.log("\n" + "=".repeat(60));
|
|
1715
|
+
console.log("Cloud Run service destroyed successfully!");
|
|
1716
|
+
console.log("=".repeat(60));
|
|
1717
|
+
console.log("\nBase infrastructure preserved:");
|
|
1718
|
+
console.log(" - Cloud SQL database");
|
|
1719
|
+
console.log(" - Memorystore Redis");
|
|
1720
|
+
console.log(" - Artifact Registry");
|
|
1721
|
+
console.log("\nTo redeploy: kuckit infra deploy --env " + env);
|
|
1722
|
+
} else {
|
|
1723
|
+
console.log("\nDestroying all infrastructure...");
|
|
1724
|
+
console.log("This may take several minutes.\n");
|
|
1725
|
+
const result = await pulumiDestroy({
|
|
1726
|
+
cwd: infraDir,
|
|
1727
|
+
force: true
|
|
1728
|
+
});
|
|
1729
|
+
if (result.code !== 0) {
|
|
1730
|
+
const parsed = parseError(result.stderr);
|
|
1731
|
+
console.error("\n" + formatError(parsed));
|
|
1732
|
+
process.exit(1);
|
|
1733
|
+
}
|
|
1734
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$6, CONFIG_FILE$6);
|
|
1735
|
+
try {
|
|
1736
|
+
await unlink(configPath);
|
|
1737
|
+
console.log(`\nRemoved ${KUCKIT_DIR$6}/${CONFIG_FILE$6}`);
|
|
1738
|
+
} catch {}
|
|
1739
|
+
console.log("\n" + "=".repeat(60));
|
|
1740
|
+
console.log("Infrastructure destroyed successfully!");
|
|
1741
|
+
console.log("=".repeat(60));
|
|
1742
|
+
console.log("\nAll resources have been removed.");
|
|
1743
|
+
console.log("To recreate: kuckit infra init");
|
|
1744
|
+
}
|
|
1745
|
+
console.log("");
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
//#endregion
|
|
1749
|
+
//#region src/commands/infra/repair.ts
|
|
1750
|
+
const KUCKIT_DIR$5 = ".kuckit";
|
|
1751
|
+
const CONFIG_FILE$5 = "infra.json";
|
|
1752
|
+
async function fileExists$5(path) {
|
|
1753
|
+
try {
|
|
1754
|
+
await access(path, constants$1.F_OK);
|
|
1755
|
+
return true;
|
|
1756
|
+
} catch {
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function findProjectRoot$5(cwd) {
|
|
1761
|
+
let dir = cwd;
|
|
1762
|
+
while (dir !== dirname$1(dir)) {
|
|
1763
|
+
if (await fileExists$5(join$1(dir, "package.json"))) return dir;
|
|
1764
|
+
dir = dirname$1(dir);
|
|
1765
|
+
}
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
async function loadConfig$5(projectRoot) {
|
|
1769
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$5, CONFIG_FILE$5);
|
|
1770
|
+
if (!await fileExists$5(configPath)) return null;
|
|
1771
|
+
try {
|
|
1772
|
+
const content = await readFile(configPath, "utf-8");
|
|
1773
|
+
return JSON.parse(content);
|
|
1774
|
+
} catch {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
async function infraRepair(options) {
|
|
1779
|
+
console.log("Repairing kuckit infrastructure state...\n");
|
|
1780
|
+
if (!await checkPulumiInstalled()) {
|
|
1781
|
+
console.error("Error: Pulumi CLI is not installed.");
|
|
1782
|
+
console.error("Install it from: https://www.pulumi.com/docs/install/");
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
const projectRoot = await findProjectRoot$5(process.cwd());
|
|
1786
|
+
if (!projectRoot) {
|
|
1787
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1788
|
+
process.exit(1);
|
|
1789
|
+
}
|
|
1790
|
+
const config = await loadConfig$5(projectRoot);
|
|
1791
|
+
if (!config) {
|
|
1792
|
+
console.error("Error: No infrastructure configuration found.");
|
|
1793
|
+
console.error("Run: kuckit infra init");
|
|
1794
|
+
process.exit(1);
|
|
1795
|
+
}
|
|
1796
|
+
const env = options.env ?? config.env;
|
|
1797
|
+
if (env !== "dev" && env !== "prod") {
|
|
1798
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
const infraDir = getInfraDir(projectRoot);
|
|
1802
|
+
if (!await fileExists$5(infraDir)) {
|
|
1803
|
+
console.error("Error: packages/infra not found.");
|
|
1804
|
+
process.exit(1);
|
|
1805
|
+
}
|
|
1806
|
+
const runCancel = options.cancel || !options.cancel && !options.refresh;
|
|
1807
|
+
const runRefresh = options.refresh || !options.cancel && !options.refresh;
|
|
1808
|
+
console.log(`Repairing stack: ${config.stackName}`);
|
|
1809
|
+
console.log(` Environment: ${env}`);
|
|
1810
|
+
console.log(` Operations: ${[runCancel && "cancel", runRefresh && "refresh"].filter(Boolean).join(", ")}`);
|
|
1811
|
+
console.log("");
|
|
1812
|
+
if (!options.force) {
|
|
1813
|
+
if (!await confirm({
|
|
1814
|
+
message: "Proceed with repair?",
|
|
1815
|
+
default: true
|
|
1816
|
+
})) {
|
|
1817
|
+
console.log("Aborted.");
|
|
1818
|
+
process.exit(0);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
console.log(`Selecting Pulumi stack: ${config.stackName}`);
|
|
1822
|
+
if (!await selectOrCreateStack(config.stackName, { cwd: infraDir })) {
|
|
1823
|
+
console.error("Error: Failed to select Pulumi stack");
|
|
1824
|
+
process.exit(1);
|
|
1825
|
+
}
|
|
1826
|
+
console.log("");
|
|
1827
|
+
if (!options.force) {
|
|
1828
|
+
const kuckitDir = join$1(projectRoot, KUCKIT_DIR$5);
|
|
1829
|
+
await mkdir(kuckitDir, { recursive: true });
|
|
1830
|
+
const backupFileName = `state-backup-${env}-${Math.floor(Date.now() / 1e3)}.json`;
|
|
1831
|
+
const backupPath = join$1(kuckitDir, backupFileName);
|
|
1832
|
+
console.log("Backing up state...");
|
|
1833
|
+
if ((await pulumiStackExport(backupPath, { cwd: infraDir })).code === 0) console.log(`State backed up to ${KUCKIT_DIR$5}/${backupFileName}\n`);
|
|
1834
|
+
else console.warn("Warning: Failed to backup state. Continuing anyway...\n");
|
|
1835
|
+
}
|
|
1836
|
+
if (runCancel) {
|
|
1837
|
+
console.log("Cancelling any stuck operations...");
|
|
1838
|
+
const cancelResult = await pulumiCancel({ cwd: infraDir });
|
|
1839
|
+
if (cancelResult.code === 0) if (cancelResult.stdout.includes("no update") || cancelResult.stderr.includes("no update")) console.log("No stuck operation to cancel\n");
|
|
1840
|
+
else console.log("✓ Cancelled stuck operation\n");
|
|
1841
|
+
else if (cancelResult.stderr.includes("no update") || cancelResult.stdout.includes("no update")) console.log("No stuck operation to cancel\n");
|
|
1842
|
+
else {
|
|
1843
|
+
console.error("Warning: Cancel operation failed");
|
|
1844
|
+
console.error(cancelResult.stderr || cancelResult.stdout);
|
|
1845
|
+
console.log("");
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (runRefresh) {
|
|
1849
|
+
console.log("Refreshing state from cloud provider...");
|
|
1850
|
+
const refreshResult = await pulumiRefresh({ cwd: infraDir });
|
|
1851
|
+
if (refreshResult.code === 0) console.log("✓ State refreshed successfully\n");
|
|
1852
|
+
else {
|
|
1853
|
+
console.error("\nError: Refresh operation failed");
|
|
1854
|
+
console.error(refreshResult.stderr);
|
|
1855
|
+
process.exit(1);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
console.log("✓ Infrastructure state repair complete");
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
//#endregion
|
|
1862
|
+
//#region src/commands/infra/db.ts
|
|
1863
|
+
const KUCKIT_DIR$4 = ".kuckit";
|
|
1864
|
+
const CONFIG_FILE$4 = "infra.json";
|
|
1865
|
+
async function fileExists$4(path) {
|
|
1866
|
+
try {
|
|
1867
|
+
await access(path, constants$1.F_OK);
|
|
1868
|
+
return true;
|
|
1869
|
+
} catch {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
async function findProjectRoot$4(cwd) {
|
|
1874
|
+
let dir = cwd;
|
|
1875
|
+
while (dir !== dirname$1(dir)) {
|
|
1876
|
+
if (await fileExists$4(join$1(dir, "package.json"))) return dir;
|
|
1877
|
+
dir = dirname$1(dir);
|
|
1878
|
+
}
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
async function loadConfig$4(projectRoot) {
|
|
1882
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$4, CONFIG_FILE$4);
|
|
1883
|
+
if (!await fileExists$4(configPath)) return null;
|
|
1884
|
+
try {
|
|
1885
|
+
const content = await readFile(configPath, "utf-8");
|
|
1886
|
+
return JSON.parse(content);
|
|
1887
|
+
} catch {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async function getJobContext(options) {
|
|
1892
|
+
if (!await checkGcloudInstalled()) {
|
|
1893
|
+
console.error("Error: gcloud CLI is not installed.");
|
|
1894
|
+
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
}
|
|
1897
|
+
const projectRoot = await findProjectRoot$4(process.cwd());
|
|
1898
|
+
if (!projectRoot) {
|
|
1899
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
const config = await loadConfig$4(projectRoot);
|
|
1903
|
+
if (!config) {
|
|
1904
|
+
console.error("Error: No infrastructure configuration found.");
|
|
1905
|
+
console.error("Run: kuckit infra init");
|
|
1906
|
+
process.exit(1);
|
|
1907
|
+
}
|
|
1908
|
+
const env = options.env ?? config.env;
|
|
1909
|
+
if (env !== "dev" && env !== "prod") {
|
|
1910
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
1911
|
+
process.exit(1);
|
|
1912
|
+
}
|
|
1913
|
+
const jobName = config.outputs?.migrationJobName;
|
|
1914
|
+
if (!jobName) {
|
|
1915
|
+
console.error("Error: No migration job found in configuration.");
|
|
1916
|
+
console.error("Make sure you have deployed the infrastructure with an image:");
|
|
1917
|
+
console.error(" kuckit infra deploy");
|
|
1918
|
+
process.exit(1);
|
|
1919
|
+
}
|
|
1920
|
+
return {
|
|
1921
|
+
config,
|
|
1922
|
+
projectRoot,
|
|
1923
|
+
env,
|
|
1924
|
+
jobName
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
async function executeCloudRunJob(project, region, jobName, commandOverride) {
|
|
1928
|
+
const args = [
|
|
1929
|
+
"run",
|
|
1930
|
+
"jobs",
|
|
1931
|
+
"execute",
|
|
1932
|
+
jobName,
|
|
1933
|
+
"--region",
|
|
1934
|
+
region,
|
|
1935
|
+
"--project",
|
|
1936
|
+
project,
|
|
1937
|
+
"--wait"
|
|
1938
|
+
];
|
|
1939
|
+
if (commandOverride && commandOverride.length > 0) {
|
|
1940
|
+
const escapedArgs = commandOverride.map((arg) => arg.replace(/'/g, "'\\''")).join(",");
|
|
1941
|
+
args.push(`--args='${escapedArgs}'`);
|
|
1942
|
+
}
|
|
1943
|
+
console.log(`Executing Cloud Run Job: ${jobName}`);
|
|
1944
|
+
console.log(` Project: ${project}`);
|
|
1945
|
+
console.log(` Region: ${region}`);
|
|
1946
|
+
console.log("");
|
|
1947
|
+
return (await runGcloud(args, { stream: true })).code === 0;
|
|
1948
|
+
}
|
|
1949
|
+
async function getLatestJobExecution(project, region, jobName) {
|
|
1950
|
+
const listResult = await runGcloud([
|
|
1951
|
+
"run",
|
|
1952
|
+
"jobs",
|
|
1953
|
+
"executions",
|
|
1954
|
+
"list",
|
|
1955
|
+
"--job",
|
|
1956
|
+
jobName,
|
|
1957
|
+
"--region",
|
|
1958
|
+
region,
|
|
1959
|
+
"--project",
|
|
1960
|
+
project,
|
|
1961
|
+
"--limit",
|
|
1962
|
+
"1",
|
|
1963
|
+
"--format",
|
|
1964
|
+
"json"
|
|
1965
|
+
], { stream: false });
|
|
1966
|
+
if (listResult.code !== 0 || !listResult.stdout.trim()) return null;
|
|
1967
|
+
try {
|
|
1968
|
+
const executions = JSON.parse(listResult.stdout);
|
|
1969
|
+
if (executions.length === 0) return null;
|
|
1970
|
+
const execution = executions[0];
|
|
1971
|
+
const executionName = execution.metadata?.name;
|
|
1972
|
+
if (executionName) {
|
|
1973
|
+
const logsResult = await runGcloud([
|
|
1974
|
+
"logging",
|
|
1975
|
+
"read",
|
|
1976
|
+
`resource.type="cloud_run_job" AND resource.labels.job_name="${jobName}" AND labels."run.googleapis.com/execution_name"="${executionName}"`,
|
|
1977
|
+
"--project",
|
|
1978
|
+
project,
|
|
1979
|
+
"--limit",
|
|
1980
|
+
"50",
|
|
1981
|
+
"--format",
|
|
1982
|
+
"value(textPayload)"
|
|
1983
|
+
], { stream: false });
|
|
1984
|
+
return {
|
|
1985
|
+
status: execution.status?.conditions?.[0]?.type ?? "Unknown",
|
|
1986
|
+
logs: logsResult.stdout
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
return { status: execution.status?.conditions?.[0]?.type ?? "Unknown" };
|
|
1990
|
+
} catch {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
async function infraDbPush(options) {
|
|
1995
|
+
console.log("Running database schema push via Cloud Run Job...\n");
|
|
1996
|
+
const ctx = await getJobContext(options);
|
|
1997
|
+
console.log("Configuration:");
|
|
1998
|
+
console.log(` Project: ${ctx.config.gcpProject}`);
|
|
1999
|
+
console.log(` Environment: ${ctx.env}`);
|
|
2000
|
+
console.log(` Job: ${ctx.jobName}`);
|
|
2001
|
+
console.log("");
|
|
2002
|
+
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" : ""}`];
|
|
2003
|
+
if (!await executeCloudRunJob(ctx.config.gcpProject, ctx.config.region, ctx.jobName, commandOverride)) {
|
|
2004
|
+
console.error("\nError: Schema push failed.");
|
|
2005
|
+
console.error("Check the Cloud Run Job logs for details:");
|
|
2006
|
+
console.error(` gcloud run jobs executions list --job ${ctx.jobName} --region ${ctx.config.region}`);
|
|
2007
|
+
process.exit(1);
|
|
2008
|
+
}
|
|
2009
|
+
const execution = await getLatestJobExecution(ctx.config.gcpProject, ctx.config.region, ctx.jobName);
|
|
2010
|
+
if (execution?.logs) {
|
|
2011
|
+
console.log("\nJob output:");
|
|
2012
|
+
console.log(execution.logs);
|
|
2013
|
+
}
|
|
2014
|
+
console.log("\n✓ Schema push complete");
|
|
2015
|
+
}
|
|
2016
|
+
async function infraDbMigrate(options) {
|
|
2017
|
+
console.log("Running database migrations via Cloud Run Job...\n");
|
|
2018
|
+
if (options.dryRun) {
|
|
2019
|
+
console.log("Note: Dry run mode is not supported for Cloud Run Jobs.");
|
|
2020
|
+
console.log("The job will execute actual migrations.\n");
|
|
2021
|
+
}
|
|
2022
|
+
if (options.rollback) {
|
|
2023
|
+
console.error("Error: Rollback is not supported via Cloud Run Jobs.");
|
|
2024
|
+
console.error("Use local drizzle-kit for rollback operations.");
|
|
2025
|
+
process.exit(1);
|
|
2026
|
+
}
|
|
2027
|
+
const ctx = await getJobContext(options);
|
|
2028
|
+
console.log("Configuration:");
|
|
2029
|
+
console.log(` Project: ${ctx.config.gcpProject}`);
|
|
2030
|
+
console.log(` Environment: ${ctx.env}`);
|
|
2031
|
+
console.log(` Job: ${ctx.jobName}`);
|
|
2032
|
+
console.log("");
|
|
2033
|
+
if (!await executeCloudRunJob(ctx.config.gcpProject, ctx.config.region, ctx.jobName)) {
|
|
2034
|
+
console.error("\nError: Migration failed.");
|
|
2035
|
+
console.error("Check the Cloud Run Job logs for details:");
|
|
2036
|
+
console.error(` gcloud run jobs executions list --job ${ctx.jobName} --region ${ctx.config.region}`);
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
const execution = await getLatestJobExecution(ctx.config.gcpProject, ctx.config.region, ctx.jobName);
|
|
2040
|
+
if (execution?.logs) {
|
|
2041
|
+
console.log("\nJob output:");
|
|
2042
|
+
console.log(execution.logs);
|
|
2043
|
+
}
|
|
2044
|
+
console.log("\n✓ Migrations complete");
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
//#endregion
|
|
2048
|
+
//#region src/commands/infra/rollback.ts
|
|
2049
|
+
const KUCKIT_DIR$3 = ".kuckit";
|
|
2050
|
+
const CONFIG_FILE$3 = "infra.json";
|
|
2051
|
+
async function fileExists$3(path) {
|
|
2052
|
+
try {
|
|
2053
|
+
await access(path, constants$1.F_OK);
|
|
2054
|
+
return true;
|
|
2055
|
+
} catch {
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
async function findProjectRoot$3(cwd) {
|
|
2060
|
+
let dir = cwd;
|
|
2061
|
+
while (dir !== dirname$1(dir)) {
|
|
2062
|
+
if (await fileExists$3(join$1(dir, "package.json"))) return dir;
|
|
2063
|
+
dir = dirname$1(dir);
|
|
2064
|
+
}
|
|
2065
|
+
return null;
|
|
2066
|
+
}
|
|
2067
|
+
async function loadConfig$3(projectRoot) {
|
|
2068
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$3, CONFIG_FILE$3);
|
|
2069
|
+
if (!await fileExists$3(configPath)) return null;
|
|
2070
|
+
try {
|
|
2071
|
+
const content = await readFile(configPath, "utf-8");
|
|
2072
|
+
return JSON.parse(content);
|
|
2073
|
+
} catch {
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
function formatRevisionDate(isoDate) {
|
|
2078
|
+
try {
|
|
2079
|
+
return new Date(isoDate).toLocaleString("en-US", {
|
|
2080
|
+
year: "numeric",
|
|
2081
|
+
month: "2-digit",
|
|
2082
|
+
day: "2-digit",
|
|
2083
|
+
hour: "2-digit",
|
|
2084
|
+
minute: "2-digit"
|
|
2085
|
+
});
|
|
2086
|
+
} catch {
|
|
2087
|
+
return isoDate;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function displayRevisions(revisions) {
|
|
2091
|
+
console.log("Available revisions:");
|
|
2092
|
+
for (const rev of revisions) {
|
|
2093
|
+
const marker = rev.active ? " (current)" : "";
|
|
2094
|
+
const traffic = rev.trafficPercent > 0 ? ` [${rev.trafficPercent}%]` : "";
|
|
2095
|
+
console.log(` ${rev.name}${marker}${traffic} - ${formatRevisionDate(rev.createTime)}`);
|
|
2096
|
+
}
|
|
2097
|
+
console.log("");
|
|
2098
|
+
}
|
|
2099
|
+
async function infraRollback(options) {
|
|
2100
|
+
console.log("Rolling back Cloud Run service...\n");
|
|
2101
|
+
if (!await checkGcloudInstalled()) {
|
|
2102
|
+
console.error("Error: gcloud CLI is not installed.");
|
|
2103
|
+
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
2104
|
+
process.exit(1);
|
|
2105
|
+
}
|
|
2106
|
+
const projectRoot = await findProjectRoot$3(process.cwd());
|
|
2107
|
+
if (!projectRoot) {
|
|
2108
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
2109
|
+
process.exit(1);
|
|
2110
|
+
}
|
|
2111
|
+
const config = await loadConfig$3(projectRoot);
|
|
2112
|
+
if (!config) {
|
|
2113
|
+
console.error("Error: No infrastructure configuration found.");
|
|
2114
|
+
console.error("Run: kuckit infra init");
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
}
|
|
2117
|
+
const env = options.env ?? config.env;
|
|
2118
|
+
if (env !== "dev" && env !== "prod") {
|
|
2119
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
2120
|
+
process.exit(1);
|
|
2121
|
+
}
|
|
2122
|
+
const serviceUrl = config.outputs?.serviceUrl;
|
|
2123
|
+
if (!serviceUrl) {
|
|
2124
|
+
console.error("Error: No Cloud Run service deployed yet.");
|
|
2125
|
+
console.error("Run: kuckit infra deploy");
|
|
2126
|
+
process.exit(1);
|
|
2127
|
+
}
|
|
2128
|
+
const serviceName = extractServiceNameFromUrl(serviceUrl);
|
|
2129
|
+
if (!serviceName) {
|
|
2130
|
+
console.error("Error: Could not determine service name from URL:", serviceUrl);
|
|
2131
|
+
process.exit(1);
|
|
2132
|
+
}
|
|
2133
|
+
console.log(`Service: ${serviceName}`);
|
|
2134
|
+
console.log(`Project: ${config.gcpProject}`);
|
|
2135
|
+
console.log(`Region: ${config.region}`);
|
|
2136
|
+
console.log("");
|
|
2137
|
+
console.log("Fetching revisions...");
|
|
2138
|
+
const revisions = await listCloudRunRevisions(serviceName, config.gcpProject, config.region);
|
|
2139
|
+
if (revisions.length === 0) {
|
|
2140
|
+
console.error("Error: No revisions found for service:", serviceName);
|
|
2141
|
+
process.exit(1);
|
|
2142
|
+
}
|
|
2143
|
+
revisions.sort((a, b) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime());
|
|
2144
|
+
displayRevisions(revisions);
|
|
2145
|
+
let targetRevision;
|
|
2146
|
+
if (options.revision) {
|
|
2147
|
+
if (!revisions.find((r) => r.name === options.revision)) {
|
|
2148
|
+
console.error(`Error: Revision '${options.revision}' not found.`);
|
|
2149
|
+
console.error("Available revisions:", revisions.map((r) => r.name).join(", "));
|
|
2150
|
+
process.exit(1);
|
|
2151
|
+
}
|
|
2152
|
+
targetRevision = options.revision;
|
|
2153
|
+
} else if (options.previous) {
|
|
2154
|
+
if (revisions.length < 2) {
|
|
2155
|
+
console.error("Error: No previous revision available.");
|
|
2156
|
+
console.error("Only one revision exists:", revisions[0]?.name);
|
|
2157
|
+
process.exit(1);
|
|
2158
|
+
}
|
|
2159
|
+
const previousRev = revisions.find((r) => !r.active);
|
|
2160
|
+
if (!previousRev) targetRevision = revisions[1]?.name;
|
|
2161
|
+
else targetRevision = previousRev.name;
|
|
2162
|
+
} else {
|
|
2163
|
+
const choices = revisions.filter((r) => !r.active || r.trafficPercent < 100).map((r) => ({
|
|
2164
|
+
name: `${r.name} - ${formatRevisionDate(r.createTime)}${r.active ? " (partial traffic)" : ""}`,
|
|
2165
|
+
value: r.name
|
|
2166
|
+
}));
|
|
2167
|
+
if (choices.length === 0) {
|
|
2168
|
+
console.log("No other revisions available to rollback to.");
|
|
2169
|
+
process.exit(0);
|
|
2170
|
+
}
|
|
2171
|
+
targetRevision = await select({
|
|
2172
|
+
message: "Select revision to rollback to:",
|
|
2173
|
+
choices
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
if (!targetRevision) {
|
|
2177
|
+
console.error("Error: Could not determine target revision.");
|
|
2178
|
+
process.exit(1);
|
|
2179
|
+
}
|
|
2180
|
+
if (revisions.find((r) => r.name === targetRevision)?.trafficPercent === 100) {
|
|
2181
|
+
console.log(`Already routing 100% traffic to ${targetRevision}`);
|
|
2182
|
+
process.exit(0);
|
|
2183
|
+
}
|
|
2184
|
+
console.log(`Rolling back to ${targetRevision}...`);
|
|
2185
|
+
const result = await updateCloudRunTraffic(serviceName, targetRevision, config.gcpProject, config.region);
|
|
2186
|
+
if (result.code !== 0) {
|
|
2187
|
+
console.error("\nError: Failed to update traffic routing");
|
|
2188
|
+
console.error(result.stderr);
|
|
2189
|
+
process.exit(1);
|
|
2190
|
+
}
|
|
2191
|
+
console.log(`\n✓ Traffic routed to ${targetRevision}`);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
//#endregion
|
|
2195
|
+
//#region src/commands/infra/logs.ts
|
|
2196
|
+
const KUCKIT_DIR$2 = ".kuckit";
|
|
2197
|
+
const CONFIG_FILE$2 = "infra.json";
|
|
2198
|
+
async function fileExists$2(path) {
|
|
2199
|
+
try {
|
|
2200
|
+
await access(path, constants$1.F_OK);
|
|
2201
|
+
return true;
|
|
2202
|
+
} catch {
|
|
2203
|
+
return false;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
async function findProjectRoot$2(cwd) {
|
|
2207
|
+
let dir = cwd;
|
|
2208
|
+
while (dir !== dirname$1(dir)) {
|
|
2209
|
+
if (await fileExists$2(join$1(dir, "package.json"))) return dir;
|
|
2210
|
+
dir = dirname$1(dir);
|
|
2211
|
+
}
|
|
2212
|
+
return null;
|
|
2213
|
+
}
|
|
2214
|
+
async function loadConfig$2(projectRoot) {
|
|
2215
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$2, CONFIG_FILE$2);
|
|
2216
|
+
if (!await fileExists$2(configPath)) return null;
|
|
2217
|
+
try {
|
|
2218
|
+
const content = await readFile(configPath, "utf-8");
|
|
2219
|
+
return JSON.parse(content);
|
|
2220
|
+
} catch {
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
function runGcloudStreaming(args) {
|
|
2225
|
+
return new Promise((resolve) => {
|
|
2226
|
+
spawn("gcloud", args, {
|
|
2227
|
+
stdio: "inherit",
|
|
2228
|
+
shell: true
|
|
2229
|
+
}).on("close", (code) => {
|
|
2230
|
+
resolve(code ?? 1);
|
|
2231
|
+
});
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
async function infraLogs(options) {
|
|
2235
|
+
if (!await checkGcloudInstalled()) {
|
|
2236
|
+
console.error("Error: gcloud CLI is not installed.");
|
|
2237
|
+
console.error("Install it from: https://cloud.google.com/sdk/docs/install");
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
const projectRoot = await findProjectRoot$2(process.cwd());
|
|
2241
|
+
if (!projectRoot) {
|
|
2242
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
2243
|
+
process.exit(1);
|
|
2244
|
+
}
|
|
2245
|
+
const config = await loadConfig$2(projectRoot);
|
|
2246
|
+
if (!config) {
|
|
2247
|
+
console.error("Error: No infrastructure configuration found.");
|
|
2248
|
+
console.error("Run: kuckit infra init");
|
|
2249
|
+
process.exit(1);
|
|
2250
|
+
}
|
|
2251
|
+
const env = options.env ?? config.env;
|
|
2252
|
+
if (env !== "dev" && env !== "prod") {
|
|
2253
|
+
console.error(`Error: Invalid environment '${env}'. Must be 'dev' or 'prod'.`);
|
|
2254
|
+
process.exit(1);
|
|
2255
|
+
}
|
|
2256
|
+
const serviceUrl = config.outputs?.serviceUrl;
|
|
2257
|
+
if (!serviceUrl) {
|
|
2258
|
+
console.error("Error: No Cloud Run service URL found.");
|
|
2259
|
+
console.error("The service may not be deployed yet. Run: kuckit infra deploy");
|
|
2260
|
+
process.exit(1);
|
|
2261
|
+
}
|
|
2262
|
+
const serviceName = extractServiceNameFromUrl(serviceUrl);
|
|
2263
|
+
if (!serviceName) {
|
|
2264
|
+
console.error("Error: Could not extract service name from URL.");
|
|
2265
|
+
console.error(`Service URL: ${serviceUrl}`);
|
|
2266
|
+
process.exit(1);
|
|
2267
|
+
}
|
|
2268
|
+
const since = options.since ?? "1h";
|
|
2269
|
+
console.log(`Fetching logs for service: ${serviceName}`);
|
|
2270
|
+
console.log(` Project: ${config.gcpProject}`);
|
|
2271
|
+
console.log(` Region: ${config.region}`);
|
|
2272
|
+
if (options.follow) console.log(" Mode: Following (Ctrl+C to stop)");
|
|
2273
|
+
else console.log(` Since: ${since}`);
|
|
2274
|
+
if (options.severity) console.log(` Severity: >= ${options.severity}`);
|
|
2275
|
+
console.log("");
|
|
2276
|
+
const args = [
|
|
2277
|
+
"run",
|
|
2278
|
+
"services",
|
|
2279
|
+
"logs"
|
|
2280
|
+
];
|
|
2281
|
+
if (options.follow) args.push("tail");
|
|
2282
|
+
else args.push("read");
|
|
2283
|
+
args.push(serviceName);
|
|
2284
|
+
args.push("--project", config.gcpProject);
|
|
2285
|
+
args.push("--region", config.region);
|
|
2286
|
+
if (!options.follow) {
|
|
2287
|
+
args.push("--limit", "100");
|
|
2288
|
+
args.push("--freshness", since);
|
|
2289
|
+
}
|
|
2290
|
+
if (options.severity) {
|
|
2291
|
+
const severity = options.severity.toUpperCase();
|
|
2292
|
+
const validSeverities = [
|
|
2293
|
+
"DEBUG",
|
|
2294
|
+
"INFO",
|
|
2295
|
+
"WARNING",
|
|
2296
|
+
"ERROR",
|
|
2297
|
+
"CRITICAL"
|
|
2298
|
+
];
|
|
2299
|
+
if (!validSeverities.includes(severity)) {
|
|
2300
|
+
console.error(`Error: Invalid severity '${severity}'.`);
|
|
2301
|
+
console.error(`Valid values: ${validSeverities.join(", ")}`);
|
|
2302
|
+
process.exit(1);
|
|
2303
|
+
}
|
|
2304
|
+
args.push("--log-filter", `severity>=${severity}`);
|
|
2305
|
+
}
|
|
2306
|
+
if (await runGcloudStreaming(args) !== 0) {
|
|
2307
|
+
console.error("\nError: Failed to fetch logs.");
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
//#endregion
|
|
2313
|
+
//#region src/commands/infra/status.ts
|
|
2314
|
+
const KUCKIT_DIR$1 = ".kuckit";
|
|
2315
|
+
const CONFIG_FILE$1 = "infra.json";
|
|
2316
|
+
async function fileExists$1(path) {
|
|
2317
|
+
try {
|
|
2318
|
+
await access(path, constants$1.F_OK);
|
|
2319
|
+
return true;
|
|
2320
|
+
} catch {
|
|
2321
|
+
return false;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
async function findProjectRoot$1(cwd) {
|
|
2325
|
+
let dir = cwd;
|
|
2326
|
+
while (dir !== dirname$1(dir)) {
|
|
2327
|
+
if (await fileExists$1(join$1(dir, "package.json"))) return dir;
|
|
2328
|
+
dir = dirname$1(dir);
|
|
2329
|
+
}
|
|
2330
|
+
return null;
|
|
2331
|
+
}
|
|
2332
|
+
async function loadConfig$1(projectRoot) {
|
|
2333
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR$1, CONFIG_FILE$1);
|
|
2334
|
+
if (!await fileExists$1(configPath)) return null;
|
|
2335
|
+
try {
|
|
2336
|
+
const content = await readFile(configPath, "utf-8");
|
|
2337
|
+
return JSON.parse(content);
|
|
2338
|
+
} catch {
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function formatDate(dateStr) {
|
|
2343
|
+
try {
|
|
2344
|
+
return new Date(dateStr).toLocaleString();
|
|
2345
|
+
} catch {
|
|
2346
|
+
return dateStr;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
async function infraStatus(options) {
|
|
2350
|
+
const projectRoot = await findProjectRoot$1(process.cwd());
|
|
2351
|
+
if (!projectRoot) {
|
|
2352
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
2353
|
+
process.exit(1);
|
|
2354
|
+
}
|
|
2355
|
+
const config = await loadConfig$1(projectRoot);
|
|
2356
|
+
if (!config) {
|
|
2357
|
+
console.error("Error: No infrastructure configuration found.");
|
|
2358
|
+
console.error("Run: kuckit infra init");
|
|
2359
|
+
process.exit(1);
|
|
2360
|
+
}
|
|
2361
|
+
const env = options.env ?? config.env ?? "dev";
|
|
2362
|
+
const stackName = `${config.gcpProject}-${env}`;
|
|
2363
|
+
const infraDir = getInfraDir(projectRoot);
|
|
2364
|
+
if (!await fileExists$1(infraDir)) {
|
|
2365
|
+
console.error("Error: packages/infra not found.");
|
|
2366
|
+
process.exit(1);
|
|
2367
|
+
}
|
|
2368
|
+
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
2369
|
+
console.error(`Error: Could not select stack '${stackName}'`);
|
|
2370
|
+
process.exit(1);
|
|
2371
|
+
}
|
|
2372
|
+
const stackInfo = await getPulumiStackInfo({ cwd: infraDir });
|
|
2373
|
+
const history = await getPulumiStackHistory({ cwd: infraDir }, 1);
|
|
2374
|
+
const outputs = await getPulumiOutputs({ cwd: infraDir });
|
|
2375
|
+
let status = "unknown";
|
|
2376
|
+
if (stackInfo?.updateInProgress) status = "updating";
|
|
2377
|
+
else if (stackInfo) status = "active";
|
|
2378
|
+
const resources = [];
|
|
2379
|
+
if (outputs) {
|
|
2380
|
+
if (outputs.registryUrl) resources.push({
|
|
2381
|
+
name: "Artifact Registry",
|
|
2382
|
+
healthy: true
|
|
2383
|
+
});
|
|
2384
|
+
if (outputs.databaseConnectionName) resources.push({
|
|
2385
|
+
name: "Cloud SQL",
|
|
2386
|
+
healthy: true
|
|
2387
|
+
});
|
|
2388
|
+
if (outputs.redisHost) resources.push({
|
|
2389
|
+
name: "Memorystore Redis",
|
|
2390
|
+
healthy: true
|
|
2391
|
+
});
|
|
2392
|
+
if (outputs.serviceUrl) resources.push({
|
|
2393
|
+
name: "Cloud Run Service",
|
|
2394
|
+
healthy: true
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
const lastUpdate = history[0]?.endTime ?? stackInfo?.lastUpdate ?? null;
|
|
2398
|
+
const result = {
|
|
2399
|
+
stack: stackName,
|
|
2400
|
+
status,
|
|
2401
|
+
lastUpdate,
|
|
2402
|
+
resourceCount: stackInfo?.resourceCount ?? 0,
|
|
2403
|
+
resources,
|
|
2404
|
+
outputs
|
|
2405
|
+
};
|
|
2406
|
+
if (options.json) {
|
|
2407
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
console.log("");
|
|
2411
|
+
console.log(`Stack: ${result.stack}`);
|
|
2412
|
+
console.log(`Status: ${result.status}`);
|
|
2413
|
+
if (result.lastUpdate) console.log(`Last updated: ${formatDate(result.lastUpdate)}`);
|
|
2414
|
+
console.log(`Resources: ${result.resourceCount}`);
|
|
2415
|
+
console.log("");
|
|
2416
|
+
if (resources.length > 0) {
|
|
2417
|
+
console.log("Infrastructure:");
|
|
2418
|
+
for (const resource of resources) {
|
|
2419
|
+
const icon = resource.healthy ? "✓" : "✗";
|
|
2420
|
+
console.log(` ${icon} ${resource.name}`);
|
|
2421
|
+
}
|
|
2422
|
+
console.log("");
|
|
2423
|
+
} else {
|
|
2424
|
+
console.log("No resources deployed yet.");
|
|
2425
|
+
console.log("Run: kuckit infra deploy");
|
|
2426
|
+
console.log("");
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
//#endregion
|
|
2431
|
+
//#region src/commands/infra/outputs.ts
|
|
2432
|
+
const KUCKIT_DIR = ".kuckit";
|
|
2433
|
+
const CONFIG_FILE = "infra.json";
|
|
2434
|
+
async function fileExists(path) {
|
|
2435
|
+
try {
|
|
2436
|
+
await access(path, constants$1.F_OK);
|
|
2437
|
+
return true;
|
|
2438
|
+
} catch {
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
async function findProjectRoot(cwd) {
|
|
2443
|
+
let dir = cwd;
|
|
2444
|
+
while (dir !== dirname$1(dir)) {
|
|
2445
|
+
if (await fileExists(join$1(dir, "package.json"))) return dir;
|
|
2446
|
+
dir = dirname$1(dir);
|
|
2447
|
+
}
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
async function loadConfig(projectRoot) {
|
|
2451
|
+
const configPath = join$1(projectRoot, KUCKIT_DIR, CONFIG_FILE);
|
|
2452
|
+
if (!await fileExists(configPath)) return null;
|
|
2453
|
+
try {
|
|
2454
|
+
const content = await readFile(configPath, "utf-8");
|
|
2455
|
+
return JSON.parse(content);
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
async function infraOutputs(options) {
|
|
2461
|
+
const projectRoot = await findProjectRoot(process.cwd());
|
|
2462
|
+
if (!projectRoot) {
|
|
2463
|
+
console.error("Error: Could not find project root (no package.json found)");
|
|
2464
|
+
process.exit(1);
|
|
2465
|
+
}
|
|
2466
|
+
const config = await loadConfig(projectRoot);
|
|
2467
|
+
if (!config) {
|
|
2468
|
+
console.error("Error: No infrastructure configuration found.");
|
|
2469
|
+
console.error("Run: kuckit infra init");
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
}
|
|
2472
|
+
const env = options.env ?? config.env ?? "dev";
|
|
2473
|
+
const stackName = `${config.gcpProject}-${env}`;
|
|
2474
|
+
const infraDir = getInfraDir(projectRoot);
|
|
2475
|
+
if (!await fileExists(infraDir)) {
|
|
2476
|
+
console.error("Error: packages/infra not found.");
|
|
2477
|
+
process.exit(1);
|
|
2478
|
+
}
|
|
2479
|
+
if (!await selectOrCreateStack(stackName, { cwd: infraDir })) {
|
|
2480
|
+
console.error(`Error: Could not select stack '${stackName}'`);
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
const outputs = await getPulumiOutputs({ cwd: infraDir });
|
|
2484
|
+
if (!outputs) {
|
|
2485
|
+
console.error("Error: Could not retrieve stack outputs.");
|
|
2486
|
+
console.error("The stack may not have been deployed yet.");
|
|
2487
|
+
console.error("Run: kuckit infra deploy");
|
|
2488
|
+
process.exit(1);
|
|
2489
|
+
}
|
|
2490
|
+
if (options.json) {
|
|
2491
|
+
console.log(JSON.stringify(outputs, null, 2));
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
console.log("");
|
|
2495
|
+
console.log(`Environment: ${env}`);
|
|
2496
|
+
console.log("");
|
|
2497
|
+
if (outputs.serviceUrl) console.log(`Service URL: ${outputs.serviceUrl}`);
|
|
2498
|
+
if (outputs.registryUrl) console.log(`Registry URL: ${outputs.registryUrl}`);
|
|
2499
|
+
if (outputs.databaseConnectionName) console.log(`Database: ${outputs.databaseConnectionName}`);
|
|
2500
|
+
if (outputs.redisHost) console.log(`Redis Host: ${outputs.redisHost}`);
|
|
2501
|
+
const knownKeys = [
|
|
2502
|
+
"serviceUrl",
|
|
2503
|
+
"registryUrl",
|
|
2504
|
+
"databaseConnectionName",
|
|
2505
|
+
"redisHost"
|
|
2506
|
+
];
|
|
2507
|
+
const additionalKeys = Object.keys(outputs).filter((k) => !knownKeys.includes(k));
|
|
2508
|
+
if (additionalKeys.length > 0) {
|
|
2509
|
+
console.log("");
|
|
2510
|
+
console.log("Additional outputs:");
|
|
2511
|
+
for (const key of additionalKeys) {
|
|
2512
|
+
const value = outputs[key];
|
|
2513
|
+
if (typeof value === "object") console.log(` ${key}: ${JSON.stringify(value)}`);
|
|
2514
|
+
else console.log(` ${key}: ${value}`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
console.log("");
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
//#endregion
|
|
2521
|
+
//#region src/bin.ts
|
|
2522
|
+
program.name("kuckit").description("CLI tools for Kuckit SDK module development").version("0.1.0");
|
|
2523
|
+
program.command("generate").description("Generate Kuckit resources").command("module <name>").description("Generate a new Kuckit module").option("-o, --org <org>", "Organization scope for package name", "").option("-d, --dir <directory>", "Target directory", "packages").action(async (name, options) => {
|
|
2524
|
+
requireAuth();
|
|
2525
|
+
await generateModule(name, options);
|
|
2526
|
+
});
|
|
2527
|
+
program.command("add <package>").description("Install and wire a Kuckit module").option("--skip-install", "Skip package installation", false).action(async (packageName, options) => {
|
|
2528
|
+
requireAuth();
|
|
2529
|
+
await addModule(packageName, options);
|
|
2530
|
+
});
|
|
2531
|
+
program.command("discover").description("Scan node_modules for installed Kuckit modules").option("--json", "Output as JSON (non-interactive)", false).option("--no-interactive", "Disable interactive prompts").action(async (options) => {
|
|
2532
|
+
requireAuth();
|
|
2533
|
+
await discoverModules(options);
|
|
2534
|
+
});
|
|
2535
|
+
program.command("doctor").description("Check Kuckit setup and validate module configuration").option("--json", "Output as JSON", false).action(async (options) => {
|
|
2536
|
+
await doctor(options);
|
|
2537
|
+
});
|
|
2538
|
+
program.command("search <keyword>").description("Search npm registry for Kuckit modules").option("--json", "Output as JSON", false).option("-l, --limit <number>", "Maximum results to return", "10").action(async (keyword, options) => {
|
|
2539
|
+
await search(keyword, {
|
|
2540
|
+
json: options.json,
|
|
2541
|
+
limit: parseInt(options.limit, 10)
|
|
2542
|
+
});
|
|
2543
|
+
});
|
|
2544
|
+
const db = program.command("db").description("Database schema management for module-owned schemas");
|
|
2545
|
+
db.command("push").description("Push all module schemas to database").option("--url <url>", "Database connection URL").action(async (options) => {
|
|
2546
|
+
requireAuth();
|
|
2547
|
+
await dbPush(options);
|
|
2548
|
+
});
|
|
2549
|
+
db.command("generate").description("Generate migration files for module schemas").option("--url <url>", "Database connection URL").action(async (options) => {
|
|
2550
|
+
requireAuth();
|
|
2551
|
+
await dbGenerate(options);
|
|
2552
|
+
});
|
|
2553
|
+
db.command("studio").description("Open Drizzle Studio with all module schemas").option("--url <url>", "Database connection URL").action(async (options) => {
|
|
2554
|
+
requireAuth();
|
|
2555
|
+
await dbStudio(options);
|
|
2556
|
+
});
|
|
2557
|
+
registerAuthCommands(program);
|
|
2558
|
+
const infra = program.command("infra").description("Infrastructure deployment and management");
|
|
2559
|
+
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) => {
|
|
2560
|
+
requireAuth();
|
|
2561
|
+
await infraInit(options);
|
|
2562
|
+
});
|
|
2563
|
+
infra.command("deploy").description("Build and deploy application to Cloud Run").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--preview", "Preview changes without applying", false).option("--skip-build", "Use existing image (skip Docker build)", false).option("--image <url>", "Use specific image URL").option("-y, --yes", "Skip confirmation prompts", false).action(async (options) => {
|
|
2564
|
+
requireAuth();
|
|
2565
|
+
await infraDeploy(options);
|
|
2566
|
+
});
|
|
2567
|
+
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) => {
|
|
2568
|
+
requireAuth();
|
|
2569
|
+
await infraDestroy(options);
|
|
2570
|
+
});
|
|
2571
|
+
infra.command("repair").description("Repair infrastructure state issues").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--refresh", "Refresh state from cloud provider", false).option("--cancel", "Cancel any stuck operations", false).option("--force", "Force repair without confirmation", false).action(async (options) => {
|
|
2572
|
+
requireAuth();
|
|
2573
|
+
await infraRepair(options);
|
|
2574
|
+
});
|
|
2575
|
+
infra.command("db:push").description("Push schema directly to Cloud SQL (dev/fresh databases)").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--force", "Skip confirmation for destructive changes", false).option("--public-ip", "Use public IP (default: private IP, requires VPC access)", false).action(async (options) => {
|
|
2576
|
+
requireAuth();
|
|
2577
|
+
await infraDbPush(options);
|
|
2578
|
+
});
|
|
2579
|
+
infra.command("db:migrate").description("Run database migrations via Cloud SQL Auth Proxy").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--dry-run", "Show migrations without applying", false).option("--rollback", "Rollback the last migration", false).option("--public-ip", "Use public IP (default: private IP, requires VPC access)", false).action(async (options) => {
|
|
2580
|
+
requireAuth();
|
|
2581
|
+
await infraDbMigrate(options);
|
|
2582
|
+
});
|
|
2583
|
+
infra.command("rollback").description("Rollback Cloud Run service to a previous revision").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--revision <name>", "Specific revision name to rollback to").option("--previous", "Rollback to the previous revision", false).action(async (options) => {
|
|
2584
|
+
requireAuth();
|
|
2585
|
+
await infraRollback(options);
|
|
2586
|
+
});
|
|
2587
|
+
infra.command("logs").description("View Cloud Run service logs").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("-f, --follow", "Stream logs in real-time", false).option("--since <duration>", "Show logs since duration (e.g., 1h, 30m)", "1h").option("--severity <level>", "Filter by severity (DEBUG, INFO, WARNING, ERROR)").action(async (options) => {
|
|
2588
|
+
requireAuth();
|
|
2589
|
+
await infraLogs(options);
|
|
2590
|
+
});
|
|
2591
|
+
infra.command("status").description("Show current infrastructure state").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--json", "Output as JSON", false).action(async (options) => {
|
|
2592
|
+
requireAuth();
|
|
2593
|
+
await infraStatus(options);
|
|
2594
|
+
});
|
|
2595
|
+
infra.command("outputs").description("Display infrastructure outputs (URLs, connection strings)").option("-e, --env <env>", "Environment (dev, prod)", "dev").option("--json", "Output as JSON", false).action(async (options) => {
|
|
2596
|
+
requireAuth();
|
|
2597
|
+
await infraOutputs(options);
|
|
2598
|
+
});
|
|
780
2599
|
program.parse();
|
|
781
2600
|
|
|
782
2601
|
//#endregion
|