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