@stacksolo/cli 0.1.6 → 0.1.7
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/index.js +681 -218
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -635,6 +635,120 @@ async function checkAndFixCloudBuildPermissions(projectId, onLog) {
|
|
|
635
635
|
};
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
// src/gcp/firebase.ts
|
|
639
|
+
import { exec as exec5 } from "child_process";
|
|
640
|
+
import { promisify as promisify5 } from "util";
|
|
641
|
+
var execAsync5 = promisify5(exec5);
|
|
642
|
+
async function isFirebaseInstalled() {
|
|
643
|
+
try {
|
|
644
|
+
await execAsync5("which firebase");
|
|
645
|
+
return true;
|
|
646
|
+
} catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async function checkFirebaseAuth() {
|
|
651
|
+
try {
|
|
652
|
+
const { stdout } = await execAsync5("firebase login:list --json 2>/dev/null");
|
|
653
|
+
const result = JSON.parse(stdout);
|
|
654
|
+
if (result.status === "success" && result.result?.length > 0) {
|
|
655
|
+
return result.result[0].user?.email || result.result[0];
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
} catch {
|
|
659
|
+
try {
|
|
660
|
+
const { stdout } = await execAsync5("firebase projects:list --json 2>/dev/null");
|
|
661
|
+
const result = JSON.parse(stdout);
|
|
662
|
+
if (result.status === "success") {
|
|
663
|
+
return "authenticated";
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async function addFirebaseToProject(projectId) {
|
|
671
|
+
try {
|
|
672
|
+
await execAsync5(`firebase projects:addfirebase ${projectId} 2>&1`);
|
|
673
|
+
return { success: true };
|
|
674
|
+
} catch (error) {
|
|
675
|
+
let errorMessage = "";
|
|
676
|
+
if (error && typeof error === "object" && "stdout" in error) {
|
|
677
|
+
errorMessage = String(error.stdout);
|
|
678
|
+
}
|
|
679
|
+
if (!errorMessage && error && typeof error === "object" && "stderr" in error) {
|
|
680
|
+
errorMessage = String(error.stderr);
|
|
681
|
+
}
|
|
682
|
+
if (!errorMessage) {
|
|
683
|
+
errorMessage = String(error);
|
|
684
|
+
}
|
|
685
|
+
if (errorMessage.includes("already exists") || errorMessage.includes("ALREADY_EXISTS")) {
|
|
686
|
+
return { success: true };
|
|
687
|
+
}
|
|
688
|
+
if (errorMessage.includes("PERMISSION_DENIED")) {
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: "Permission denied. Make sure you have Firebase Admin permissions on the GCP project."
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (errorMessage.includes("NOT_FOUND")) {
|
|
695
|
+
return {
|
|
696
|
+
success: false,
|
|
697
|
+
error: "GCP project not found. Make sure the project ID is correct."
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return { success: false, error: errorMessage || "Failed to add Firebase to project" };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function getFirebaseAuthConsoleUrl(projectId) {
|
|
704
|
+
return `https://console.firebase.google.com/project/${projectId}/authentication`;
|
|
705
|
+
}
|
|
706
|
+
function getBillingConsoleUrl(projectId) {
|
|
707
|
+
return `https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
708
|
+
}
|
|
709
|
+
async function isBillingEnabled(projectId) {
|
|
710
|
+
try {
|
|
711
|
+
const { stdout } = await execAsync5(
|
|
712
|
+
`gcloud billing projects describe ${projectId} --format="value(billingEnabled)" 2>/dev/null`
|
|
713
|
+
);
|
|
714
|
+
return stdout.trim().toLowerCase() === "true";
|
|
715
|
+
} catch {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function generateProjectId(name) {
|
|
720
|
+
const cleanName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
721
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
722
|
+
let suffix = "";
|
|
723
|
+
for (let i = 0; i < 6; i++) {
|
|
724
|
+
suffix += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
725
|
+
}
|
|
726
|
+
const maxNameLen = 30 - 1 - suffix.length;
|
|
727
|
+
const truncatedName = cleanName.slice(0, maxNameLen);
|
|
728
|
+
return `${truncatedName}-${suffix}`;
|
|
729
|
+
}
|
|
730
|
+
function isValidProjectId(projectId) {
|
|
731
|
+
if (!projectId) {
|
|
732
|
+
return { valid: false, error: "Project ID is required" };
|
|
733
|
+
}
|
|
734
|
+
if (projectId.length < 6 || projectId.length > 30) {
|
|
735
|
+
return { valid: false, error: "Project ID must be 6-30 characters" };
|
|
736
|
+
}
|
|
737
|
+
if (!/^[a-z]/.test(projectId)) {
|
|
738
|
+
return { valid: false, error: "Project ID must start with a lowercase letter" };
|
|
739
|
+
}
|
|
740
|
+
if (!/[a-z0-9]$/.test(projectId)) {
|
|
741
|
+
return { valid: false, error: "Project ID must end with a letter or digit" };
|
|
742
|
+
}
|
|
743
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(projectId)) {
|
|
744
|
+
return {
|
|
745
|
+
valid: false,
|
|
746
|
+
error: "Project ID can only contain lowercase letters, digits, and hyphens"
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
return { valid: true };
|
|
750
|
+
}
|
|
751
|
+
|
|
638
752
|
// src/templates/index.ts
|
|
639
753
|
import * as fs from "fs/promises";
|
|
640
754
|
import * as path from "path";
|
|
@@ -3390,6 +3504,386 @@ var UI_FRAMEWORKS = [
|
|
|
3390
3504
|
description: "Simple HTML/CSS/JS - no build step"
|
|
3391
3505
|
}
|
|
3392
3506
|
];
|
|
3507
|
+
async function waitForManualStep(description, url, instruction) {
|
|
3508
|
+
console.log(chalk.yellow(`
|
|
3509
|
+
${description}
|
|
3510
|
+
`));
|
|
3511
|
+
console.log(chalk.white(` ${url}
|
|
3512
|
+
`));
|
|
3513
|
+
console.log(chalk.gray(` ${instruction}
|
|
3514
|
+
`));
|
|
3515
|
+
const { action } = await inquirer.prompt([
|
|
3516
|
+
{
|
|
3517
|
+
type: "list",
|
|
3518
|
+
name: "action",
|
|
3519
|
+
message: "What would you like to do?",
|
|
3520
|
+
choices: [
|
|
3521
|
+
{ name: "Continue (I've completed this step)", value: "continue" },
|
|
3522
|
+
{ name: "Open URL and wait", value: "open" },
|
|
3523
|
+
{ name: "Quit and resume later", value: "quit" }
|
|
3524
|
+
]
|
|
3525
|
+
}
|
|
3526
|
+
]);
|
|
3527
|
+
if (action === "open") {
|
|
3528
|
+
const { exec: exec16 } = await import("child_process");
|
|
3529
|
+
const { promisify: promisify16 } = await import("util");
|
|
3530
|
+
const execAsync16 = promisify16(exec16);
|
|
3531
|
+
try {
|
|
3532
|
+
const platform = process.platform;
|
|
3533
|
+
if (platform === "darwin") {
|
|
3534
|
+
await execAsync16(`open "${url}"`);
|
|
3535
|
+
} else if (platform === "win32") {
|
|
3536
|
+
await execAsync16(`start "${url}"`);
|
|
3537
|
+
} else {
|
|
3538
|
+
await execAsync16(`xdg-open "${url}"`);
|
|
3539
|
+
}
|
|
3540
|
+
console.log(chalk.gray("\n Opened in browser. Complete the step and press Enter.\n"));
|
|
3541
|
+
} catch {
|
|
3542
|
+
console.log(chalk.gray("\n Could not open browser. Please open the URL manually.\n"));
|
|
3543
|
+
}
|
|
3544
|
+
await inquirer.prompt([
|
|
3545
|
+
{
|
|
3546
|
+
type: "input",
|
|
3547
|
+
name: "done",
|
|
3548
|
+
message: "Press Enter when done..."
|
|
3549
|
+
}
|
|
3550
|
+
]);
|
|
3551
|
+
return { continue: true, retry: false };
|
|
3552
|
+
}
|
|
3553
|
+
if (action === "quit") {
|
|
3554
|
+
return { continue: false, retry: false };
|
|
3555
|
+
}
|
|
3556
|
+
return { continue: true, retry: false };
|
|
3557
|
+
}
|
|
3558
|
+
async function handleCreateProject(cwd, options) {
|
|
3559
|
+
console.log(chalk.cyan(BANNER));
|
|
3560
|
+
console.log(chalk.bold(" Create a new GCP + Firebase project\n"));
|
|
3561
|
+
console.log(chalk.gray("\u2500".repeat(75)));
|
|
3562
|
+
console.log(chalk.cyan.bold("\n Step 1: Prerequisites\n"));
|
|
3563
|
+
const gcloudSpinner = ora("Checking gcloud CLI...").start();
|
|
3564
|
+
if (!await isGcloudInstalled()) {
|
|
3565
|
+
gcloudSpinner.fail("gcloud CLI not found");
|
|
3566
|
+
console.log(chalk.red("\n gcloud CLI is required.\n"));
|
|
3567
|
+
console.log(chalk.gray(" Install: https://cloud.google.com/sdk/docs/install\n"));
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
const authInfo = await checkGcloudAuth();
|
|
3571
|
+
if (!authInfo) {
|
|
3572
|
+
gcloudSpinner.fail("Not authenticated to GCP");
|
|
3573
|
+
console.log(chalk.red("\n Please authenticate first:\n"));
|
|
3574
|
+
console.log(chalk.white(" gcloud auth login"));
|
|
3575
|
+
console.log(chalk.white(" gcloud auth application-default login\n"));
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
gcloudSpinner.succeed(`gcloud authenticated as ${chalk.green(authInfo.account)}`);
|
|
3579
|
+
const firebaseSpinner = ora("Checking Firebase CLI...").start();
|
|
3580
|
+
if (!await isFirebaseInstalled()) {
|
|
3581
|
+
firebaseSpinner.fail("Firebase CLI not found");
|
|
3582
|
+
console.log(chalk.red("\n Firebase CLI is required for --create-project.\n"));
|
|
3583
|
+
console.log(chalk.gray(" Install: npm install -g firebase-tools"));
|
|
3584
|
+
console.log(chalk.gray(" Then run: firebase login\n"));
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3587
|
+
const firebaseAuth = await checkFirebaseAuth();
|
|
3588
|
+
if (!firebaseAuth) {
|
|
3589
|
+
firebaseSpinner.fail("Not authenticated to Firebase");
|
|
3590
|
+
console.log(chalk.red("\n Please authenticate first:\n"));
|
|
3591
|
+
console.log(chalk.white(" firebase login\n"));
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
firebaseSpinner.succeed("Firebase CLI authenticated");
|
|
3595
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3596
|
+
console.log(chalk.cyan.bold("\n Step 2: Project Details\n"));
|
|
3597
|
+
let projectName = options.name;
|
|
3598
|
+
if (!projectName) {
|
|
3599
|
+
const defaultName = path4.basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
3600
|
+
const { name } = await inquirer.prompt([
|
|
3601
|
+
{
|
|
3602
|
+
type: "input",
|
|
3603
|
+
name: "name",
|
|
3604
|
+
message: "Project name:",
|
|
3605
|
+
default: defaultName,
|
|
3606
|
+
validate: (input) => {
|
|
3607
|
+
if (!input) return "Project name is required";
|
|
3608
|
+
if (!/^[a-z][a-z0-9-]*$/.test(input)) {
|
|
3609
|
+
return "Must start with letter, only lowercase, numbers, hyphens";
|
|
3610
|
+
}
|
|
3611
|
+
return true;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
]);
|
|
3615
|
+
projectName = name;
|
|
3616
|
+
}
|
|
3617
|
+
const suggestedId = generateProjectId(projectName);
|
|
3618
|
+
const { projectId } = await inquirer.prompt([
|
|
3619
|
+
{
|
|
3620
|
+
type: "input",
|
|
3621
|
+
name: "projectId",
|
|
3622
|
+
message: "GCP Project ID:",
|
|
3623
|
+
default: suggestedId,
|
|
3624
|
+
validate: (input) => {
|
|
3625
|
+
const result = isValidProjectId(input);
|
|
3626
|
+
return result.valid || result.error || "Invalid project ID";
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
]);
|
|
3630
|
+
let region = options.region;
|
|
3631
|
+
if (!region) {
|
|
3632
|
+
const regions = getRegionsForProvider("gcp");
|
|
3633
|
+
const { selectedRegion } = await inquirer.prompt([
|
|
3634
|
+
{
|
|
3635
|
+
type: "list",
|
|
3636
|
+
name: "selectedRegion",
|
|
3637
|
+
message: "Region:",
|
|
3638
|
+
choices: regions.map((r) => ({ name: r.name, value: r.value })),
|
|
3639
|
+
default: "us-central1"
|
|
3640
|
+
}
|
|
3641
|
+
]);
|
|
3642
|
+
region = selectedRegion;
|
|
3643
|
+
}
|
|
3644
|
+
console.log(chalk.gray(`
|
|
3645
|
+
Project: ${chalk.white(projectName)}`));
|
|
3646
|
+
console.log(chalk.gray(` GCP ID: ${chalk.white(projectId)}`));
|
|
3647
|
+
console.log(chalk.gray(` Region: ${chalk.white(region)}`));
|
|
3648
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3649
|
+
console.log(chalk.cyan.bold("\n Step 3: Create GCP Project\n"));
|
|
3650
|
+
const createSpinner = ora("Creating GCP project...").start();
|
|
3651
|
+
const createResult = await createProject(projectId, projectName);
|
|
3652
|
+
if (!createResult.success) {
|
|
3653
|
+
createSpinner.fail("Failed to create project");
|
|
3654
|
+
console.log(chalk.red(`
|
|
3655
|
+
${createResult.error}
|
|
3656
|
+
`));
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
3659
|
+
createSpinner.succeed(`Created GCP project: ${chalk.green(projectId)}`);
|
|
3660
|
+
await setActiveProject(projectId);
|
|
3661
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3662
|
+
console.log(chalk.cyan.bold("\n Step 4: Enable Billing\n"));
|
|
3663
|
+
const billingSpinner = ora("Checking billing status...").start();
|
|
3664
|
+
let billingEnabled = await isBillingEnabled(projectId);
|
|
3665
|
+
if (billingEnabled) {
|
|
3666
|
+
billingSpinner.succeed("Billing is already enabled");
|
|
3667
|
+
} else {
|
|
3668
|
+
billingSpinner.stop();
|
|
3669
|
+
const billingAccounts = await listBillingAccounts();
|
|
3670
|
+
if (billingAccounts.length > 0) {
|
|
3671
|
+
const { billingAction } = await inquirer.prompt([
|
|
3672
|
+
{
|
|
3673
|
+
type: "list",
|
|
3674
|
+
name: "billingAction",
|
|
3675
|
+
message: "Link a billing account:",
|
|
3676
|
+
choices: [
|
|
3677
|
+
...billingAccounts.map((b) => ({
|
|
3678
|
+
name: `${b.name} (${b.id})`,
|
|
3679
|
+
value: b.id
|
|
3680
|
+
})),
|
|
3681
|
+
new inquirer.Separator(),
|
|
3682
|
+
{ name: "Configure manually in GCP Console", value: "__manual__" }
|
|
3683
|
+
]
|
|
3684
|
+
}
|
|
3685
|
+
]);
|
|
3686
|
+
if (billingAction !== "__manual__") {
|
|
3687
|
+
const linkSpinner = ora("Linking billing account...").start();
|
|
3688
|
+
const linked = await linkBillingAccount(projectId, billingAction);
|
|
3689
|
+
if (linked) {
|
|
3690
|
+
linkSpinner.succeed("Billing account linked");
|
|
3691
|
+
billingEnabled = true;
|
|
3692
|
+
} else {
|
|
3693
|
+
linkSpinner.warn("Could not link billing account automatically");
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
if (!billingEnabled) {
|
|
3698
|
+
const billingUrl = getBillingConsoleUrl(projectId);
|
|
3699
|
+
const result = await waitForManualStep(
|
|
3700
|
+
"Billing must be enabled to use GCP services.",
|
|
3701
|
+
billingUrl,
|
|
3702
|
+
"Link a billing account in the GCP Console, then continue."
|
|
3703
|
+
);
|
|
3704
|
+
if (!result.continue) {
|
|
3705
|
+
console.log(chalk.yellow("\n Project created but billing not enabled."));
|
|
3706
|
+
console.log(chalk.gray(` Resume setup later by running: stacksolo init --project-id ${projectId}
|
|
3707
|
+
`));
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3713
|
+
console.log(chalk.cyan.bold("\n Step 5: Enable GCP APIs\n"));
|
|
3714
|
+
const apisSpinner = ora("Enabling required APIs...").start();
|
|
3715
|
+
const apiResult = await enableApis(projectId, REQUIRED_APIS, (api2, success) => {
|
|
3716
|
+
if (success) {
|
|
3717
|
+
apisSpinner.text = `Enabled ${api2}`;
|
|
3718
|
+
}
|
|
3719
|
+
});
|
|
3720
|
+
if (apiResult.failed.length === 0) {
|
|
3721
|
+
apisSpinner.succeed(`Enabled ${apiResult.enabled.length} APIs`);
|
|
3722
|
+
} else {
|
|
3723
|
+
apisSpinner.warn(`Enabled ${apiResult.enabled.length} APIs, ${apiResult.failed.length} failed`);
|
|
3724
|
+
console.log(chalk.yellow(" Failed APIs (may need billing):"));
|
|
3725
|
+
for (const api2 of apiResult.failed) {
|
|
3726
|
+
console.log(chalk.gray(` - ${api2}`));
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3730
|
+
console.log(chalk.cyan.bold("\n Step 6: Add Firebase\n"));
|
|
3731
|
+
const firebaseAddSpinner = ora("Adding Firebase to project...").start();
|
|
3732
|
+
const firebaseResult = await addFirebaseToProject(projectId);
|
|
3733
|
+
if (!firebaseResult.success) {
|
|
3734
|
+
firebaseAddSpinner.fail("Failed to add Firebase");
|
|
3735
|
+
console.log(chalk.red(`
|
|
3736
|
+
${firebaseResult.error}
|
|
3737
|
+
`));
|
|
3738
|
+
console.log(chalk.gray(" You can add Firebase manually later:"));
|
|
3739
|
+
console.log(chalk.white(` firebase projects:addfirebase ${projectId}
|
|
3740
|
+
`));
|
|
3741
|
+
} else {
|
|
3742
|
+
firebaseAddSpinner.succeed("Firebase added to project");
|
|
3743
|
+
}
|
|
3744
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3745
|
+
console.log(chalk.cyan.bold("\n Step 7: Configure Firebase Authentication\n"));
|
|
3746
|
+
const authUrl = getFirebaseAuthConsoleUrl(projectId);
|
|
3747
|
+
const { needsAuth } = await inquirer.prompt([
|
|
3748
|
+
{
|
|
3749
|
+
type: "confirm",
|
|
3750
|
+
name: "needsAuth",
|
|
3751
|
+
message: "Do you need Firebase Authentication?",
|
|
3752
|
+
default: true
|
|
3753
|
+
}
|
|
3754
|
+
]);
|
|
3755
|
+
if (needsAuth) {
|
|
3756
|
+
const authResult = await waitForManualStep(
|
|
3757
|
+
"Enable authentication providers in Firebase Console.",
|
|
3758
|
+
authUrl,
|
|
3759
|
+
'Click "Get Started", then enable Email/Password, Google, or other providers.'
|
|
3760
|
+
);
|
|
3761
|
+
if (!authResult.continue) {
|
|
3762
|
+
console.log(chalk.yellow("\n You can configure auth later at:"));
|
|
3763
|
+
console.log(chalk.gray(` ${authUrl}
|
|
3764
|
+
`));
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3768
|
+
console.log(chalk.cyan.bold("\n Step 8: Configure Permissions\n"));
|
|
3769
|
+
const policySpinner = ora("Checking org policy...").start();
|
|
3770
|
+
const policyStatus = await checkOrgPolicy(projectId);
|
|
3771
|
+
if (policyStatus.hasRestriction && policyStatus.canOverride) {
|
|
3772
|
+
policySpinner.text = "Fixing org policy...";
|
|
3773
|
+
const fixed = await fixOrgPolicy(projectId);
|
|
3774
|
+
if (fixed) {
|
|
3775
|
+
policySpinner.succeed("Org policy configured for public access");
|
|
3776
|
+
} else {
|
|
3777
|
+
policySpinner.warn("Could not update org policy - some features may be limited");
|
|
3778
|
+
}
|
|
3779
|
+
} else if (policyStatus.hasRestriction) {
|
|
3780
|
+
policySpinner.warn("Org policy restricts public access - contact your admin");
|
|
3781
|
+
} else {
|
|
3782
|
+
policySpinner.succeed("No org policy restrictions");
|
|
3783
|
+
}
|
|
3784
|
+
const iamSpinner = ora("Configuring Cloud Build permissions...").start();
|
|
3785
|
+
const iamResult = await checkAndFixCloudBuildPermissions(projectId);
|
|
3786
|
+
if (iamResult.failed.length === 0) {
|
|
3787
|
+
iamSpinner.succeed("Cloud Build permissions configured");
|
|
3788
|
+
} else {
|
|
3789
|
+
iamSpinner.warn("Some permissions could not be set");
|
|
3790
|
+
}
|
|
3791
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3792
|
+
console.log(chalk.cyan.bold("\n Step 9: Generate Project Files\n"));
|
|
3793
|
+
const { projectType } = await inquirer.prompt([
|
|
3794
|
+
{
|
|
3795
|
+
type: "list",
|
|
3796
|
+
name: "projectType",
|
|
3797
|
+
message: "What are you building?",
|
|
3798
|
+
choices: PROJECT_TYPES.map((t) => ({
|
|
3799
|
+
name: `${t.name}
|
|
3800
|
+
${chalk.gray(t.description)}`,
|
|
3801
|
+
value: t.value,
|
|
3802
|
+
short: t.name
|
|
3803
|
+
})),
|
|
3804
|
+
default: "ui-api"
|
|
3805
|
+
}
|
|
3806
|
+
]);
|
|
3807
|
+
let uiFramework;
|
|
3808
|
+
if (projectType === "ui-api" || projectType === "ui-only") {
|
|
3809
|
+
const { selectedFramework } = await inquirer.prompt([
|
|
3810
|
+
{
|
|
3811
|
+
type: "list",
|
|
3812
|
+
name: "selectedFramework",
|
|
3813
|
+
message: "Which UI framework?",
|
|
3814
|
+
choices: UI_FRAMEWORKS.map((f) => ({
|
|
3815
|
+
name: `${f.name}
|
|
3816
|
+
${chalk.gray(f.description)}`,
|
|
3817
|
+
value: f.value,
|
|
3818
|
+
short: f.name
|
|
3819
|
+
})),
|
|
3820
|
+
default: "react"
|
|
3821
|
+
}
|
|
3822
|
+
]);
|
|
3823
|
+
uiFramework = selectedFramework;
|
|
3824
|
+
}
|
|
3825
|
+
const generateSpinner = ora("Generating configuration...").start();
|
|
3826
|
+
const config = generateConfig({
|
|
3827
|
+
projectName,
|
|
3828
|
+
gcpProjectId: projectId,
|
|
3829
|
+
region,
|
|
3830
|
+
projectType,
|
|
3831
|
+
uiFramework,
|
|
3832
|
+
needsDatabase: false,
|
|
3833
|
+
needsBucket: false
|
|
3834
|
+
});
|
|
3835
|
+
if (needsAuth) {
|
|
3836
|
+
config.project.gcpKernel = {
|
|
3837
|
+
name: "kernel",
|
|
3838
|
+
firebaseProjectId: projectId,
|
|
3839
|
+
storageBucket: `${projectId}.firebasestorage.app`
|
|
3840
|
+
};
|
|
3841
|
+
if (!config.project.plugins) {
|
|
3842
|
+
config.project.plugins = [];
|
|
3843
|
+
}
|
|
3844
|
+
if (!config.project.plugins.includes("@stacksolo/plugin-gcp-kernel")) {
|
|
3845
|
+
config.project.plugins.push("@stacksolo/plugin-gcp-kernel");
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
await createStacksoloDir(cwd, {
|
|
3849
|
+
gcpProjectId: projectId,
|
|
3850
|
+
orgPolicyFixed: !policyStatus.hasRestriction || policyStatus.canOverride,
|
|
3851
|
+
apisEnabled: REQUIRED_APIS
|
|
3852
|
+
});
|
|
3853
|
+
await createConfigFile(cwd, config);
|
|
3854
|
+
const scaffoldedFiles = await scaffoldTemplates(cwd, projectType, uiFramework);
|
|
3855
|
+
generateSpinner.succeed("Project files created");
|
|
3856
|
+
try {
|
|
3857
|
+
const registry3 = getRegistry();
|
|
3858
|
+
const configPath = path4.join(cwd, ".stacksolo", "stacksolo.config.json");
|
|
3859
|
+
await registry3.registerProject({
|
|
3860
|
+
name: projectName,
|
|
3861
|
+
gcpProjectId: projectId,
|
|
3862
|
+
region,
|
|
3863
|
+
configPath
|
|
3864
|
+
});
|
|
3865
|
+
} catch {
|
|
3866
|
+
}
|
|
3867
|
+
console.log(chalk.gray("\n\u2500".repeat(75)));
|
|
3868
|
+
console.log(chalk.bold.green("\n Success! Your project is ready.\n"));
|
|
3869
|
+
console.log(chalk.white(" Created:"));
|
|
3870
|
+
console.log(chalk.green(` \u2713 GCP Project: ${projectId}`));
|
|
3871
|
+
console.log(chalk.green(` \u2713 Firebase Project: ${projectId}`));
|
|
3872
|
+
if (needsAuth) {
|
|
3873
|
+
console.log(chalk.green(" \u2713 Firebase Auth enabled"));
|
|
3874
|
+
}
|
|
3875
|
+
console.log(chalk.green(" \u2713 .stacksolo/stacksolo.config.json"));
|
|
3876
|
+
console.log(chalk.gray("\n Next steps:\n"));
|
|
3877
|
+
console.log(chalk.white(" 1. Review your config:"));
|
|
3878
|
+
console.log(chalk.cyan(" cat .stacksolo/stacksolo.config.json"));
|
|
3879
|
+
console.log(chalk.white(" 2. Install dependencies:"));
|
|
3880
|
+
console.log(chalk.cyan(" npm install"));
|
|
3881
|
+
console.log(chalk.white(" 3. Start development:"));
|
|
3882
|
+
console.log(chalk.cyan(" stacksolo dev"));
|
|
3883
|
+
console.log(chalk.white(" 4. Deploy to GCP:"));
|
|
3884
|
+
console.log(chalk.cyan(" stacksolo deploy"));
|
|
3885
|
+
console.log("");
|
|
3886
|
+
}
|
|
3393
3887
|
async function handleRemoteTemplate(cwd, options) {
|
|
3394
3888
|
console.log(chalk.cyan(BANNER));
|
|
3395
3889
|
console.log(chalk.bold(` Initializing from template: ${options.template}
|
|
@@ -3600,8 +4094,11 @@ async function handleRemoteTemplate(cwd, options) {
|
|
|
3600
4094
|
`));
|
|
3601
4095
|
}
|
|
3602
4096
|
}
|
|
3603
|
-
var initCommand = new Command("init").description("Initialize a new StackSolo project").option("-n, --name <name>", "Project name").option("--project-id <id>", "GCP project ID").option("-r, --region <region>", "Region").option("-t, --template <template>", "Project template (function-api, container-api, firebase-app, etc.)").option("-y, --yes", "Skip prompts and use defaults").option("--skip-org-policy", "Skip org policy check and fix").option("--skip-apis", "Skip enabling GCP APIs").option("--list-templates", "List available remote templates").action(async (options) => {
|
|
4097
|
+
var initCommand = new Command("init").description("Initialize a new StackSolo project").option("-n, --name <name>", "Project name").option("--project-id <id>", "GCP project ID").option("-r, --region <region>", "Region").option("-t, --template <template>", "Project template (function-api, container-api, firebase-app, etc.)").option("-y, --yes", "Skip prompts and use defaults").option("--skip-org-policy", "Skip org policy check and fix").option("--skip-apis", "Skip enabling GCP APIs").option("--list-templates", "List available remote templates").option("--create-project", "Create a new GCP + Firebase project").action(async (options) => {
|
|
3604
4098
|
const cwd = process.cwd();
|
|
4099
|
+
if (options.createProject) {
|
|
4100
|
+
return await handleCreateProject(cwd, options);
|
|
4101
|
+
}
|
|
3605
4102
|
if (options.listTemplates) {
|
|
3606
4103
|
const spinner = ora("Fetching available templates...").start();
|
|
3607
4104
|
try {
|
|
@@ -4717,7 +5214,7 @@ function resolveContainer(container, networkName, defaultRegion, networkId, regi
|
|
|
4717
5214
|
}
|
|
4718
5215
|
return {
|
|
4719
5216
|
id: `container-${container.name}`,
|
|
4720
|
-
type: "gcp:cloud_run",
|
|
5217
|
+
type: "gcp-cdktf:cloud_run",
|
|
4721
5218
|
name: container.name,
|
|
4722
5219
|
config: {
|
|
4723
5220
|
name: container.name,
|
|
@@ -4775,7 +5272,7 @@ function resolveFunction(fn, networkName, defaultRegion, networkId, registryId)
|
|
|
4775
5272
|
}
|
|
4776
5273
|
return {
|
|
4777
5274
|
id: `function-${fn.name}`,
|
|
4778
|
-
type: "gcp:cloud_function",
|
|
5275
|
+
type: "gcp-cdktf:cloud_function",
|
|
4779
5276
|
name: fn.name,
|
|
4780
5277
|
config: {
|
|
4781
5278
|
name: fn.name,
|
|
@@ -5091,6 +5588,10 @@ function resolveCdktfConfig(config, projectInfo) {
|
|
|
5091
5588
|
const functionId = `function-${fn.name}`;
|
|
5092
5589
|
functionIds.push(functionId);
|
|
5093
5590
|
functionNames.push(functionName);
|
|
5591
|
+
const functionEnv = {
|
|
5592
|
+
...hasGcpKernel ? { GOOGLE_CLOUD_PROJECT: projectInfo.gcpProjectId } : {},
|
|
5593
|
+
...fn.env
|
|
5594
|
+
};
|
|
5094
5595
|
resources.push({
|
|
5095
5596
|
id: functionId,
|
|
5096
5597
|
type: "gcp-cdktf:cloud_function",
|
|
@@ -5108,7 +5609,8 @@ function resolveCdktfConfig(config, projectInfo) {
|
|
|
5108
5609
|
vpcConnector: connectorName,
|
|
5109
5610
|
allowUnauthenticated: fn.allowUnauthenticated ?? true,
|
|
5110
5611
|
projectId: projectInfo.gcpProjectId,
|
|
5111
|
-
projectName: projectInfo.name
|
|
5612
|
+
projectName: projectInfo.name,
|
|
5613
|
+
environmentVariables: Object.keys(functionEnv).length > 0 ? functionEnv : void 0
|
|
5112
5614
|
},
|
|
5113
5615
|
dependsOn: [connectorId],
|
|
5114
5616
|
network: network.name
|
|
@@ -9197,17 +9699,17 @@ import ora4 from "ora";
|
|
|
9197
9699
|
import * as path14 from "path";
|
|
9198
9700
|
import * as fs11 from "fs/promises";
|
|
9199
9701
|
import { homedir as homedir3 } from "os";
|
|
9200
|
-
import { spawn as spawn4, exec as
|
|
9201
|
-
import { promisify as
|
|
9702
|
+
import { spawn as spawn4, exec as exec11 } from "child_process";
|
|
9703
|
+
import { promisify as promisify11 } from "util";
|
|
9202
9704
|
|
|
9203
9705
|
// src/services/deploy.service.ts
|
|
9204
9706
|
import * as fs7 from "fs/promises";
|
|
9205
9707
|
import * as path9 from "path";
|
|
9206
|
-
import { exec as
|
|
9207
|
-
import { promisify as
|
|
9708
|
+
import { exec as exec6, spawn as spawn2 } from "child_process";
|
|
9709
|
+
import { promisify as promisify6 } from "util";
|
|
9208
9710
|
init_plugin_loader_service();
|
|
9209
9711
|
import { registry as registry2 } from "@stacksolo/core";
|
|
9210
|
-
var
|
|
9712
|
+
var execAsync6 = promisify6(exec6);
|
|
9211
9713
|
async function execStreaming(command, options = {}) {
|
|
9212
9714
|
return new Promise((resolve7, reject) => {
|
|
9213
9715
|
const { cwd, onOutput, timeout = 3e5 } = options;
|
|
@@ -9267,7 +9769,11 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9267
9769
|
};
|
|
9268
9770
|
try {
|
|
9269
9771
|
if (!pluginsLoaded) {
|
|
9270
|
-
|
|
9772
|
+
const pluginsToLoad = [...config.project.plugins || []];
|
|
9773
|
+
if (config.project.gcpKernel && !pluginsToLoad.includes("@stacksolo/plugin-gcp-kernel")) {
|
|
9774
|
+
pluginsToLoad.push("@stacksolo/plugin-gcp-kernel");
|
|
9775
|
+
}
|
|
9776
|
+
await loadPlugins(pluginsToLoad);
|
|
9271
9777
|
pluginsLoaded = true;
|
|
9272
9778
|
}
|
|
9273
9779
|
const resolved = resolveConfig(config);
|
|
@@ -9329,11 +9835,11 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9329
9835
|
await fs7.access(nodeModulesPath);
|
|
9330
9836
|
} catch {
|
|
9331
9837
|
log(`Installing dependencies for ${fnName}...`);
|
|
9332
|
-
await
|
|
9838
|
+
await execAsync6("npm install", { cwd: sourceDir, timeout: 12e4 });
|
|
9333
9839
|
}
|
|
9334
9840
|
if (packageJson2.scripts?.build) {
|
|
9335
9841
|
log(`Building ${fnName} (TypeScript)...`);
|
|
9336
|
-
await
|
|
9842
|
+
await execAsync6("npm run build", { cwd: sourceDir, timeout: 6e4 });
|
|
9337
9843
|
isTypeScriptProject = true;
|
|
9338
9844
|
}
|
|
9339
9845
|
} catch {
|
|
@@ -9342,7 +9848,7 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9342
9848
|
const distDir = path9.join(sourceDir, "dist");
|
|
9343
9849
|
const stagingDir = path9.join(workDir, `staging-${fnName}`);
|
|
9344
9850
|
await fs7.mkdir(stagingDir, { recursive: true });
|
|
9345
|
-
await
|
|
9851
|
+
await execAsync6(`cp -r "${distDir}"/* "${stagingDir}"/`, { timeout: 3e4 });
|
|
9346
9852
|
const pkgContent = await fs7.readFile(packageJsonPath, "utf-8");
|
|
9347
9853
|
const pkg2 = JSON.parse(pkgContent);
|
|
9348
9854
|
if (pkg2.main && pkg2.main.startsWith("dist/")) {
|
|
@@ -9355,10 +9861,10 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9355
9861
|
}
|
|
9356
9862
|
delete pkg2.devDependencies;
|
|
9357
9863
|
await fs7.writeFile(path9.join(stagingDir, "package.json"), JSON.stringify(pkg2, null, 2));
|
|
9358
|
-
await
|
|
9864
|
+
await execAsync6(`cd "${stagingDir}" && zip -r "${sourceZipPath}" .`, { timeout: 6e4 });
|
|
9359
9865
|
await fs7.rm(stagingDir, { recursive: true, force: true });
|
|
9360
9866
|
} else {
|
|
9361
|
-
await
|
|
9867
|
+
await execAsync6(`cd "${sourceDir}" && zip -r "${sourceZipPath}" . -x "*.git*" -x "node_modules/*"`, { timeout: 6e4 });
|
|
9362
9868
|
}
|
|
9363
9869
|
sourceZips.push({ name: fnName, zipPath: sourceZipPath });
|
|
9364
9870
|
}
|
|
@@ -9366,7 +9872,7 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9366
9872
|
if (!preview && gcpKernelResources.length > 0) {
|
|
9367
9873
|
log(`Configuring Docker authentication for gcr.io...`);
|
|
9368
9874
|
try {
|
|
9369
|
-
await
|
|
9875
|
+
await execAsync6(`gcloud auth configure-docker gcr.io --quiet`, { timeout: 3e4 });
|
|
9370
9876
|
} catch (error) {
|
|
9371
9877
|
log(`Warning: Failed to configure Docker auth for GCR: ${error instanceof Error ? error.message : error}`);
|
|
9372
9878
|
}
|
|
@@ -9407,11 +9913,11 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9407
9913
|
await fs7.access(nodeModulesPath);
|
|
9408
9914
|
} catch {
|
|
9409
9915
|
log(`Installing dependencies for GCP Kernel...`);
|
|
9410
|
-
await
|
|
9916
|
+
await execAsync6("npm install", { cwd: kernelSourceDir, timeout: 12e4 });
|
|
9411
9917
|
}
|
|
9412
9918
|
if (pkg2.scripts?.build) {
|
|
9413
9919
|
log(`Building GCP Kernel TypeScript...`);
|
|
9414
|
-
await
|
|
9920
|
+
await execAsync6("npm run build", { cwd: kernelSourceDir, timeout: 6e4 });
|
|
9415
9921
|
}
|
|
9416
9922
|
} catch (buildError) {
|
|
9417
9923
|
log(`Warning: Could not build kernel TypeScript: ${buildError instanceof Error ? buildError.message : buildError}`);
|
|
@@ -9426,7 +9932,7 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9426
9932
|
throw new Error(buildResult.stderr || "Docker build failed");
|
|
9427
9933
|
}
|
|
9428
9934
|
} else {
|
|
9429
|
-
await
|
|
9935
|
+
await execAsync6(`docker build --platform linux/amd64 -t "${kernelImage}" .`, { cwd: kernelSourceDir, timeout: 3e5 });
|
|
9430
9936
|
}
|
|
9431
9937
|
log(`Pushing Docker image: ${kernelImage}`);
|
|
9432
9938
|
if (verbose) {
|
|
@@ -9438,7 +9944,7 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9438
9944
|
throw new Error(pushResult.stderr || "Docker push failed");
|
|
9439
9945
|
}
|
|
9440
9946
|
} else {
|
|
9441
|
-
await
|
|
9947
|
+
await execAsync6(`docker push "${kernelImage}"`, { timeout: 3e5 });
|
|
9442
9948
|
}
|
|
9443
9949
|
log(`GCP Kernel built and pushed successfully`);
|
|
9444
9950
|
}
|
|
@@ -9486,9 +9992,9 @@ async function deployConfig(config, _stateDir, options = {}) {
|
|
|
9486
9992
|
};
|
|
9487
9993
|
await fs7.writeFile(path9.join(workDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
|
|
9488
9994
|
log("Installing npm dependencies...");
|
|
9489
|
-
await
|
|
9995
|
+
await execAsync6("npm install", { cwd: workDir, timeout: 12e4 });
|
|
9490
9996
|
log("Synthesizing Terraform configuration...");
|
|
9491
|
-
await
|
|
9997
|
+
await execAsync6("npx ts-node main.ts", { cwd: workDir, timeout: 6e4 });
|
|
9492
9998
|
const stackDir = path9.join(workDir, "cdktf.out", "stacks", "main");
|
|
9493
9999
|
for (const { name, zipPath } of sourceZips) {
|
|
9494
10000
|
await fs7.copyFile(zipPath, path9.join(stackDir, `${name}-source.zip`));
|
|
@@ -9505,7 +10011,7 @@ terraform {
|
|
|
9505
10011
|
if (verbose) {
|
|
9506
10012
|
await execStreaming("terraform init", { cwd: stackDir, onOutput: verboseLog });
|
|
9507
10013
|
} else {
|
|
9508
|
-
await
|
|
10014
|
+
await execAsync6("terraform init", { cwd: stackDir });
|
|
9509
10015
|
}
|
|
9510
10016
|
if (destroy) {
|
|
9511
10017
|
log("Destroying resources...");
|
|
@@ -9519,7 +10025,7 @@ terraform {
|
|
|
9519
10025
|
throw new Error(result.stderr || "Terraform destroy failed");
|
|
9520
10026
|
}
|
|
9521
10027
|
} else {
|
|
9522
|
-
await
|
|
10028
|
+
await execAsync6("terraform destroy -auto-approve", { cwd: stackDir });
|
|
9523
10029
|
}
|
|
9524
10030
|
return {
|
|
9525
10031
|
success: true,
|
|
@@ -9532,7 +10038,7 @@ terraform {
|
|
|
9532
10038
|
if (verbose) {
|
|
9533
10039
|
await execStreaming("terraform plan", { cwd: stackDir, onOutput: verboseLog });
|
|
9534
10040
|
} else {
|
|
9535
|
-
const { stdout } = await
|
|
10041
|
+
const { stdout } = await execAsync6("terraform plan", { cwd: stackDir });
|
|
9536
10042
|
log(stdout);
|
|
9537
10043
|
}
|
|
9538
10044
|
return {
|
|
@@ -9554,7 +10060,7 @@ terraform {
|
|
|
9554
10060
|
throw new Error(result.stderr || "Terraform apply failed");
|
|
9555
10061
|
}
|
|
9556
10062
|
} else {
|
|
9557
|
-
await
|
|
10063
|
+
await execAsync6("terraform apply -auto-approve", { cwd: stackDir });
|
|
9558
10064
|
}
|
|
9559
10065
|
} catch (applyError) {
|
|
9560
10066
|
const errorStr = String(applyError);
|
|
@@ -9570,7 +10076,7 @@ terraform {
|
|
|
9570
10076
|
const region = config.project.region;
|
|
9571
10077
|
log(`Configuring Docker authentication for ${region}-docker.pkg.dev...`);
|
|
9572
10078
|
try {
|
|
9573
|
-
await
|
|
10079
|
+
await execAsync6(`gcloud auth configure-docker ${region}-docker.pkg.dev --quiet`, { timeout: 3e4 });
|
|
9574
10080
|
} catch (error) {
|
|
9575
10081
|
log(`Warning: Failed to configure Docker auth: ${error instanceof Error ? error.message : error}`);
|
|
9576
10082
|
log("You may need to run: gcloud auth configure-docker " + region + "-docker.pkg.dev");
|
|
@@ -9601,11 +10107,11 @@ terraform {
|
|
|
9601
10107
|
await fs7.access(nodeModulesPath);
|
|
9602
10108
|
} catch {
|
|
9603
10109
|
log(`Installing dependencies for ${containerName}...`);
|
|
9604
|
-
await
|
|
10110
|
+
await execAsync6("npm install", { cwd: sourceDir, timeout: 12e4 });
|
|
9605
10111
|
}
|
|
9606
10112
|
if (pkg2.scripts?.build) {
|
|
9607
10113
|
log(`Building ${containerName}...`);
|
|
9608
|
-
await
|
|
10114
|
+
await execAsync6("npm run build", { cwd: sourceDir, timeout: 6e4 });
|
|
9609
10115
|
}
|
|
9610
10116
|
} catch {
|
|
9611
10117
|
}
|
|
@@ -9620,7 +10126,7 @@ terraform {
|
|
|
9620
10126
|
throw new Error(buildResult.stderr || "Docker build failed");
|
|
9621
10127
|
}
|
|
9622
10128
|
} else {
|
|
9623
|
-
await
|
|
10129
|
+
await execAsync6(`docker build -t "${image}" .`, { cwd: sourceDir, timeout: 3e5 });
|
|
9624
10130
|
}
|
|
9625
10131
|
log(`Pushing Docker image: ${image}`);
|
|
9626
10132
|
if (verbose) {
|
|
@@ -9632,7 +10138,7 @@ terraform {
|
|
|
9632
10138
|
throw new Error(pushResult.stderr || "Docker push failed");
|
|
9633
10139
|
}
|
|
9634
10140
|
} else {
|
|
9635
|
-
await
|
|
10141
|
+
await execAsync6(`docker push "${image}"`, { timeout: 3e5 });
|
|
9636
10142
|
}
|
|
9637
10143
|
log(`Container ${containerName} built and pushed successfully`);
|
|
9638
10144
|
}
|
|
@@ -9648,11 +10154,11 @@ terraform {
|
|
|
9648
10154
|
throw new Error(result.stderr || "Terraform re-apply failed");
|
|
9649
10155
|
}
|
|
9650
10156
|
} else {
|
|
9651
|
-
await
|
|
10157
|
+
await execAsync6("terraform apply -auto-approve", { cwd: stackDir });
|
|
9652
10158
|
}
|
|
9653
10159
|
}
|
|
9654
10160
|
}
|
|
9655
|
-
const { stdout: outputJson } = await
|
|
10161
|
+
const { stdout: outputJson } = await execAsync6("terraform output -json", { cwd: stackDir });
|
|
9656
10162
|
const outputs = JSON.parse(outputJson || "{}");
|
|
9657
10163
|
const outputValues = {};
|
|
9658
10164
|
for (const [key, value] of Object.entries(outputs)) {
|
|
@@ -9703,10 +10209,10 @@ terraform {
|
|
|
9703
10209
|
await fs7.access(nodeModulesPath);
|
|
9704
10210
|
} catch {
|
|
9705
10211
|
log(`Installing dependencies for ${uiName}...`);
|
|
9706
|
-
await
|
|
10212
|
+
await execAsync6("npm install", { cwd: sourceDir, timeout: 12e4 });
|
|
9707
10213
|
}
|
|
9708
10214
|
log(`Building ${uiName} (${detectedFramework})...`);
|
|
9709
|
-
await
|
|
10215
|
+
await execAsync6(buildCommand2, { cwd: sourceDir, timeout: 12e4 });
|
|
9710
10216
|
} else {
|
|
9711
10217
|
distPath = sourceDir;
|
|
9712
10218
|
}
|
|
@@ -9718,7 +10224,7 @@ terraform {
|
|
|
9718
10224
|
continue;
|
|
9719
10225
|
}
|
|
9720
10226
|
log(`Uploading ${uiName} to gs://${bucketName}...`);
|
|
9721
|
-
await
|
|
10227
|
+
await execAsync6(`gsutil -m rsync -r -d "${distPath}" gs://${bucketName}`, { timeout: 3e5 });
|
|
9722
10228
|
log(`UI ${uiName} deployed to gs://${bucketName}`);
|
|
9723
10229
|
}
|
|
9724
10230
|
}
|
|
@@ -9729,7 +10235,7 @@ terraform {
|
|
|
9729
10235
|
log("Provisioning IAP service agent...");
|
|
9730
10236
|
let iapServiceAccount = null;
|
|
9731
10237
|
try {
|
|
9732
|
-
const { stdout: identityOutput } = await
|
|
10238
|
+
const { stdout: identityOutput } = await execAsync6(
|
|
9733
10239
|
`gcloud beta services identity create --service=iap.googleapis.com --project=${gcpProjectId} 2>&1`,
|
|
9734
10240
|
{ timeout: 6e4 }
|
|
9735
10241
|
);
|
|
@@ -9761,7 +10267,7 @@ terraform {
|
|
|
9761
10267
|
const region = config.project.region;
|
|
9762
10268
|
log(`Granting IAP service account invoker role on Cloud Run: ${cloudRunServiceName}`);
|
|
9763
10269
|
try {
|
|
9764
|
-
await
|
|
10270
|
+
await execAsync6(
|
|
9765
10271
|
`gcloud run services add-iam-policy-binding ${cloudRunServiceName} --region=${region} --project=${gcpProjectId} --member="serviceAccount:${iapServiceAccount}" --role="roles/run.invoker"`,
|
|
9766
10272
|
{ timeout: 6e4 }
|
|
9767
10273
|
);
|
|
@@ -9774,7 +10280,7 @@ terraform {
|
|
|
9774
10280
|
}
|
|
9775
10281
|
log(`Enabling IAP on backend: ${backendServiceName}`);
|
|
9776
10282
|
try {
|
|
9777
|
-
await
|
|
10283
|
+
await execAsync6(
|
|
9778
10284
|
`gcloud compute backend-services update ${backendServiceName} --global --project=${gcpProjectId} --iap=enabled`,
|
|
9779
10285
|
{ timeout: 6e4 }
|
|
9780
10286
|
);
|
|
@@ -9782,7 +10288,7 @@ terraform {
|
|
|
9782
10288
|
for (const member of allowedMembers) {
|
|
9783
10289
|
log(`Granting IAP access to: ${member}`);
|
|
9784
10290
|
try {
|
|
9785
|
-
await
|
|
10291
|
+
await execAsync6(
|
|
9786
10292
|
`gcloud iap web add-iam-policy-binding --resource-type=backend-services --service=${backendServiceName} --project=${gcpProjectId} --member="${member}" --role="roles/iap.httpsResourceAccessor"`,
|
|
9787
10293
|
{ timeout: 6e4 }
|
|
9788
10294
|
);
|
|
@@ -9863,8 +10369,8 @@ function generateCdktfMain(config, generated) {
|
|
|
9863
10369
|
// src/services/k8s-deploy.service.ts
|
|
9864
10370
|
import * as fs8 from "fs/promises";
|
|
9865
10371
|
import * as path10 from "path";
|
|
9866
|
-
import { exec as
|
|
9867
|
-
import { promisify as
|
|
10372
|
+
import { exec as exec7 } from "child_process";
|
|
10373
|
+
import { promisify as promisify7 } from "util";
|
|
9868
10374
|
|
|
9869
10375
|
// src/generators/k8s/yaml.ts
|
|
9870
10376
|
function toYaml(obj, indent = 0) {
|
|
@@ -9941,7 +10447,7 @@ function combineYamlDocuments(documents) {
|
|
|
9941
10447
|
}
|
|
9942
10448
|
|
|
9943
10449
|
// src/services/k8s-deploy.service.ts
|
|
9944
|
-
var
|
|
10450
|
+
var execAsync7 = promisify7(exec7);
|
|
9945
10451
|
async function deployToKubernetes(options) {
|
|
9946
10452
|
const { config, resources, imageTag = "latest", dryRun = false, onLog = console.log, onVerbose } = options;
|
|
9947
10453
|
const project = config.project;
|
|
@@ -9976,7 +10482,7 @@ async function deployToKubernetes(options) {
|
|
|
9976
10482
|
log("Applying manifests to cluster...");
|
|
9977
10483
|
const kubectlArgs = k8sConfig.context ? `--context ${k8sConfig.context}` : "";
|
|
9978
10484
|
const kubeconfigArg = k8sConfig.kubeconfig ? `--kubeconfig ${k8sConfig.kubeconfig}` : "";
|
|
9979
|
-
await
|
|
10485
|
+
await execAsync7(
|
|
9980
10486
|
`kubectl apply -f "${manifestDir}" ${kubectlArgs} ${kubeconfigArg}`.trim(),
|
|
9981
10487
|
{ timeout: 12e4 }
|
|
9982
10488
|
);
|
|
@@ -9986,7 +10492,7 @@ async function deployToKubernetes(options) {
|
|
|
9986
10492
|
const deploymentName = deployment.config.name;
|
|
9987
10493
|
verbose?.(` Waiting for ${deploymentName}...`);
|
|
9988
10494
|
try {
|
|
9989
|
-
await
|
|
10495
|
+
await execAsync7(
|
|
9990
10496
|
`kubectl rollout status deployment/${deploymentName} -n ${namespace} --timeout=120s ${kubectlArgs} ${kubeconfigArg}`.trim(),
|
|
9991
10497
|
{ timeout: 13e4 }
|
|
9992
10498
|
);
|
|
@@ -10054,11 +10560,11 @@ async function buildAndPushImages(config, resources, tag = "latest", options = {
|
|
|
10054
10560
|
await generateFunctionDockerfile(fullSourceDir, runtime);
|
|
10055
10561
|
}
|
|
10056
10562
|
verboseLog?.(` docker build -t ${imageUrl} ${fullSourceDir}`);
|
|
10057
|
-
await
|
|
10563
|
+
await execAsync7(`docker build -t "${imageUrl}" "${fullSourceDir}"`, {
|
|
10058
10564
|
timeout: 3e5
|
|
10059
10565
|
});
|
|
10060
10566
|
log(`Pushing ${imageUrl}...`);
|
|
10061
|
-
await
|
|
10567
|
+
await execAsync7(`docker push "${imageUrl}"`, { timeout: 3e5 });
|
|
10062
10568
|
builtImages.push(imageUrl);
|
|
10063
10569
|
}
|
|
10064
10570
|
return {
|
|
@@ -10345,7 +10851,7 @@ async function checkKubernetesConnection(context, kubeconfig) {
|
|
|
10345
10851
|
try {
|
|
10346
10852
|
const contextArg = context ? `--context ${context}` : "";
|
|
10347
10853
|
const kubeconfigArg = kubeconfig ? `--kubeconfig ${kubeconfig}` : "";
|
|
10348
|
-
await
|
|
10854
|
+
await execAsync7(
|
|
10349
10855
|
`kubectl cluster-info ${contextArg} ${kubeconfigArg}`.trim(),
|
|
10350
10856
|
{ timeout: 1e4 }
|
|
10351
10857
|
);
|
|
@@ -10487,8 +10993,8 @@ import path13 from "path";
|
|
|
10487
10993
|
import * as fs10 from "fs/promises";
|
|
10488
10994
|
import chalk4 from "chalk";
|
|
10489
10995
|
import inquirer3 from "inquirer";
|
|
10490
|
-
import { exec as
|
|
10491
|
-
import { promisify as
|
|
10996
|
+
import { exec as exec10 } from "child_process";
|
|
10997
|
+
import { promisify as promisify10 } from "util";
|
|
10492
10998
|
|
|
10493
10999
|
// src/services/terraform-state.service.ts
|
|
10494
11000
|
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
@@ -10583,15 +11089,15 @@ function isResourceInState(gcpResource, state) {
|
|
|
10583
11089
|
}
|
|
10584
11090
|
|
|
10585
11091
|
// src/services/gcp-scanner.service.ts
|
|
10586
|
-
import { exec as
|
|
10587
|
-
import { promisify as
|
|
10588
|
-
var
|
|
11092
|
+
import { exec as exec8 } from "child_process";
|
|
11093
|
+
import { promisify as promisify8 } from "util";
|
|
11094
|
+
var execAsync8 = promisify8(exec8);
|
|
10589
11095
|
function matchesProjectPattern(resourceName, projectName, gcpProjectId) {
|
|
10590
11096
|
return resourceName.startsWith(`${projectName}-`) || resourceName.startsWith(`${gcpProjectId}-${projectName}-`);
|
|
10591
11097
|
}
|
|
10592
11098
|
async function runGcloudCommand(command, timeoutMs = 3e4) {
|
|
10593
11099
|
try {
|
|
10594
|
-
const { stdout } = await
|
|
11100
|
+
const { stdout } = await execAsync8(command, { timeout: timeoutMs });
|
|
10595
11101
|
const trimmed = stdout.trim();
|
|
10596
11102
|
if (!trimmed || trimmed === "[]") {
|
|
10597
11103
|
return [];
|
|
@@ -10811,9 +11317,9 @@ async function scanGcpResources(options) {
|
|
|
10811
11317
|
}
|
|
10812
11318
|
|
|
10813
11319
|
// src/services/terraform-import.service.ts
|
|
10814
|
-
import { exec as
|
|
10815
|
-
import { promisify as
|
|
10816
|
-
var
|
|
11320
|
+
import { exec as exec9 } from "child_process";
|
|
11321
|
+
import { promisify as promisify9 } from "util";
|
|
11322
|
+
var execAsync9 = promisify9(exec9);
|
|
10817
11323
|
function toTerraformName(name) {
|
|
10818
11324
|
return name.replace(/[^a-zA-Z0-9]/g, "-");
|
|
10819
11325
|
}
|
|
@@ -10944,7 +11450,7 @@ async function importResource(conflict, config, stackDir) {
|
|
|
10944
11450
|
const importId = mapping.importIdFormatter(conflict.resource, config);
|
|
10945
11451
|
const tfAddress = mapping.terraformAddressFormatter(conflict.resource);
|
|
10946
11452
|
try {
|
|
10947
|
-
await
|
|
11453
|
+
await execAsync9(`terraform import '${tfAddress}' '${importId}'`, {
|
|
10948
11454
|
cwd: stackDir,
|
|
10949
11455
|
timeout: 6e4
|
|
10950
11456
|
});
|
|
@@ -10985,7 +11491,7 @@ function getImportCommand(resource, config) {
|
|
|
10985
11491
|
}
|
|
10986
11492
|
|
|
10987
11493
|
// src/services/preflight.service.ts
|
|
10988
|
-
var
|
|
11494
|
+
var execAsync10 = promisify10(exec10);
|
|
10989
11495
|
function groupByType(conflicts) {
|
|
10990
11496
|
const groups = {};
|
|
10991
11497
|
for (const conflict of conflicts) {
|
|
@@ -11139,7 +11645,7 @@ async function runKernelPreflightCheck(gcpProjectId, cwd = process.cwd()) {
|
|
|
11139
11645
|
const errors = [];
|
|
11140
11646
|
const warnings = [];
|
|
11141
11647
|
try {
|
|
11142
|
-
await
|
|
11648
|
+
await execAsync10("docker info", { timeout: 1e4 });
|
|
11143
11649
|
checks.push({
|
|
11144
11650
|
name: "Docker",
|
|
11145
11651
|
status: "pass",
|
|
@@ -11174,7 +11680,7 @@ async function runKernelPreflightCheck(gcpProjectId, cwd = process.cwd()) {
|
|
|
11174
11680
|
}
|
|
11175
11681
|
}
|
|
11176
11682
|
try {
|
|
11177
|
-
await
|
|
11683
|
+
await execAsync10("gcloud --version", { timeout: 1e4 });
|
|
11178
11684
|
checks.push({
|
|
11179
11685
|
name: "gcloud CLI",
|
|
11180
11686
|
status: "pass",
|
|
@@ -11190,7 +11696,7 @@ async function runKernelPreflightCheck(gcpProjectId, cwd = process.cwd()) {
|
|
|
11190
11696
|
errors.push("gcloud CLI is not installed");
|
|
11191
11697
|
}
|
|
11192
11698
|
try {
|
|
11193
|
-
const { stdout } = await
|
|
11699
|
+
const { stdout } = await execAsync10('gcloud auth list --filter=status:ACTIVE --format="value(account)"', { timeout: 1e4 });
|
|
11194
11700
|
if (stdout.trim()) {
|
|
11195
11701
|
checks.push({
|
|
11196
11702
|
name: "GCP Authentication",
|
|
@@ -11278,7 +11784,7 @@ async function runKernelPreflightCheck(gcpProjectId, cwd = process.cwd()) {
|
|
|
11278
11784
|
}
|
|
11279
11785
|
if (gcpProjectId) {
|
|
11280
11786
|
try {
|
|
11281
|
-
const { stdout } = await
|
|
11787
|
+
const { stdout } = await execAsync10(
|
|
11282
11788
|
`gcloud services list --project=${gcpProjectId} --format="value(config.name)" --filter="config.name:(run.googleapis.com OR cloudbuild.googleapis.com OR firestore.googleapis.com)"`,
|
|
11283
11789
|
{ timeout: 3e4 }
|
|
11284
11790
|
);
|
|
@@ -11430,7 +11936,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11430
11936
|
const errors = [];
|
|
11431
11937
|
let projectNumber;
|
|
11432
11938
|
try {
|
|
11433
|
-
const { stdout } = await
|
|
11939
|
+
const { stdout } = await execAsync10(
|
|
11434
11940
|
`gcloud projects describe ${gcpProjectId} --format="value(projectNumber)"`
|
|
11435
11941
|
);
|
|
11436
11942
|
projectNumber = stdout.trim();
|
|
@@ -11448,7 +11954,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11448
11954
|
];
|
|
11449
11955
|
for (const api2 of requiredApis) {
|
|
11450
11956
|
try {
|
|
11451
|
-
await
|
|
11957
|
+
await execAsync10(
|
|
11452
11958
|
`gcloud services enable ${api2} --project=${gcpProjectId} --quiet`,
|
|
11453
11959
|
{ timeout: 6e4 }
|
|
11454
11960
|
);
|
|
@@ -11468,7 +11974,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11468
11974
|
];
|
|
11469
11975
|
for (const role of cloudBuildRoles) {
|
|
11470
11976
|
try {
|
|
11471
|
-
await
|
|
11977
|
+
await execAsync10(
|
|
11472
11978
|
`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:${cloudBuildSa}" --role="${role}" --condition=None --quiet`,
|
|
11473
11979
|
{ timeout: 3e4 }
|
|
11474
11980
|
);
|
|
@@ -11484,7 +11990,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11484
11990
|
];
|
|
11485
11991
|
for (const role of serverlessRoles) {
|
|
11486
11992
|
try {
|
|
11487
|
-
await
|
|
11993
|
+
await execAsync10(
|
|
11488
11994
|
`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:${serverlessRobotSa}" --role="${role}" --condition=None --quiet`,
|
|
11489
11995
|
{ timeout: 3e4 }
|
|
11490
11996
|
);
|
|
@@ -11499,7 +12005,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11499
12005
|
];
|
|
11500
12006
|
for (const role of computeRoles) {
|
|
11501
12007
|
try {
|
|
11502
|
-
await
|
|
12008
|
+
await execAsync10(
|
|
11503
12009
|
`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:${defaultComputeSa}" --role="${role}" --condition=None --quiet`,
|
|
11504
12010
|
{ timeout: 3e4 }
|
|
11505
12011
|
);
|
|
@@ -11508,7 +12014,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11508
12014
|
}
|
|
11509
12015
|
}
|
|
11510
12016
|
try {
|
|
11511
|
-
await
|
|
12017
|
+
await execAsync10(
|
|
11512
12018
|
`gcloud artifacts repositories describe gcf-artifacts --location=${region} --project=${gcpProjectId}`,
|
|
11513
12019
|
{ timeout: 3e4 }
|
|
11514
12020
|
);
|
|
@@ -11517,7 +12023,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11517
12023
|
for (const sa of serviceAccounts) {
|
|
11518
12024
|
for (const role of repoRoles) {
|
|
11519
12025
|
try {
|
|
11520
|
-
await
|
|
12026
|
+
await execAsync10(
|
|
11521
12027
|
`gcloud artifacts repositories add-iam-policy-binding gcf-artifacts --location=${region} --project=${gcpProjectId} --member="serviceAccount:${sa}" --role="${role}" --quiet`,
|
|
11522
12028
|
{ timeout: 3e4 }
|
|
11523
12029
|
);
|
|
@@ -11528,7 +12034,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11528
12034
|
}
|
|
11529
12035
|
} catch {
|
|
11530
12036
|
try {
|
|
11531
|
-
await
|
|
12037
|
+
await execAsync10(
|
|
11532
12038
|
`gcloud artifacts repositories create gcf-artifacts --repository-format=docker --location=${region} --project=${gcpProjectId} --description="Cloud Functions artifacts" --quiet`,
|
|
11533
12039
|
{ timeout: 6e4 }
|
|
11534
12040
|
);
|
|
@@ -11538,7 +12044,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11538
12044
|
for (const sa of serviceAccounts) {
|
|
11539
12045
|
for (const role of repoRoles) {
|
|
11540
12046
|
try {
|
|
11541
|
-
await
|
|
12047
|
+
await execAsync10(
|
|
11542
12048
|
`gcloud artifacts repositories add-iam-policy-binding gcf-artifacts --location=${region} --project=${gcpProjectId} --member="serviceAccount:${sa}" --role="${role}" --quiet`,
|
|
11543
12049
|
{ timeout: 3e4 }
|
|
11544
12050
|
);
|
|
@@ -11560,7 +12066,7 @@ async function ensureCloudFunctionsPrerequisites(gcpProjectId, region) {
|
|
|
11560
12066
|
|
|
11561
12067
|
// src/commands/infra/deploy.ts
|
|
11562
12068
|
init_plugin_loader_service();
|
|
11563
|
-
var
|
|
12069
|
+
var execAsync11 = promisify11(exec11);
|
|
11564
12070
|
var STACKSOLO_DIR4 = ".stacksolo";
|
|
11565
12071
|
function sleep(ms) {
|
|
11566
12072
|
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
@@ -12102,7 +12608,7 @@ async function runDeploy(options, retryCount = 0, retryContext = {}, sessionId)
|
|
|
12102
12608
|
console.log(progress.next());
|
|
12103
12609
|
const authSpinner = ora4("Configuring Docker authentication...").start();
|
|
12104
12610
|
try {
|
|
12105
|
-
await
|
|
12611
|
+
await execAsync11(
|
|
12106
12612
|
`gcloud auth configure-docker ${config.project.region}-docker.pkg.dev --quiet`
|
|
12107
12613
|
);
|
|
12108
12614
|
authSpinner.succeed("Docker authentication configured");
|
|
@@ -12123,7 +12629,7 @@ async function runDeploy(options, retryCount = 0, retryContext = {}, sessionId)
|
|
|
12123
12629
|
}
|
|
12124
12630
|
const buildSpinner = ora4(`Building ${container.name}...`).start();
|
|
12125
12631
|
try {
|
|
12126
|
-
await
|
|
12632
|
+
await execAsync11(`docker build -t "${imageTag}" "${sourceDir}"`, {
|
|
12127
12633
|
maxBuffer: 50 * 1024 * 1024
|
|
12128
12634
|
});
|
|
12129
12635
|
buildSpinner.succeed(`Built ${container.name}`);
|
|
@@ -12136,7 +12642,7 @@ async function runDeploy(options, retryCount = 0, retryContext = {}, sessionId)
|
|
|
12136
12642
|
}
|
|
12137
12643
|
const pushSpinner = ora4(`Pushing ${container.name}...`).start();
|
|
12138
12644
|
try {
|
|
12139
|
-
await
|
|
12645
|
+
await execAsync11(`docker push "${imageTag}"`);
|
|
12140
12646
|
pushSpinner.succeed(`Pushed ${container.name}`);
|
|
12141
12647
|
console.log(chalk5.gray(` ${imageTag}
|
|
12142
12648
|
`));
|
|
@@ -12230,7 +12736,7 @@ async function runDeploy(options, retryCount = 0, retryContext = {}, sessionId)
|
|
|
12230
12736
|
const namespace = k8sConfig.namespace || config.project.name;
|
|
12231
12737
|
const releaseName = config.project.name;
|
|
12232
12738
|
try {
|
|
12233
|
-
await
|
|
12739
|
+
await execAsync11(
|
|
12234
12740
|
`helm upgrade --install ${releaseName} ${helmOutputDir} --namespace ${namespace} --create-namespace`
|
|
12235
12741
|
);
|
|
12236
12742
|
console.log(chalk5.green("\n Deployed successfully with Helm!"));
|
|
@@ -12487,7 +12993,7 @@ async function runDeploy(options, retryCount = 0, retryContext = {}, sessionId)
|
|
|
12487
12993
|
`));
|
|
12488
12994
|
const spinner2 = ora4(`Enabling ${apiName}...`).start();
|
|
12489
12995
|
try {
|
|
12490
|
-
await
|
|
12996
|
+
await execAsync11(`gcloud services enable ${apiName} --project=${config.project.gcpProjectId}`);
|
|
12491
12997
|
spinner2.succeed(`Enabled ${apiName}`);
|
|
12492
12998
|
console.log(chalk5.gray(" Waiting 30 seconds for API to propagate...\n"));
|
|
12493
12999
|
await sleep(3e4);
|
|
@@ -12698,11 +13204,11 @@ async function promptFixBuildPermissions() {
|
|
|
12698
13204
|
}
|
|
12699
13205
|
async function checkCloudBuildPermissions(gcpProjectId) {
|
|
12700
13206
|
try {
|
|
12701
|
-
const { stdout: projectNumber } = await
|
|
13207
|
+
const { stdout: projectNumber } = await execAsync11(
|
|
12702
13208
|
`gcloud projects describe ${gcpProjectId} --format="value(projectNumber)"`
|
|
12703
13209
|
);
|
|
12704
13210
|
const trimmedProjectNumber = projectNumber.trim();
|
|
12705
|
-
const { stdout: policy } = await
|
|
13211
|
+
const { stdout: policy } = await execAsync11(
|
|
12706
13212
|
`gcloud projects get-iam-policy ${gcpProjectId} --format="json" --flatten="bindings[].members" --filter="bindings.members:${trimmedProjectNumber}@cloudbuild.gserviceaccount.com AND bindings.role:roles/storage.objectViewer"`
|
|
12707
13213
|
);
|
|
12708
13214
|
return policy.trim().length > 10;
|
|
@@ -12713,7 +13219,7 @@ async function checkCloudBuildPermissions(gcpProjectId) {
|
|
|
12713
13219
|
async function grantCloudBuildPermissions(gcpProjectId, region) {
|
|
12714
13220
|
const spinner = ora4("Getting project number...").start();
|
|
12715
13221
|
try {
|
|
12716
|
-
const { stdout: projectNumber } = await
|
|
13222
|
+
const { stdout: projectNumber } = await execAsync11(
|
|
12717
13223
|
`gcloud projects describe ${gcpProjectId} --format="value(projectNumber)"`
|
|
12718
13224
|
);
|
|
12719
13225
|
const trimmedProjectNumber = projectNumber.trim();
|
|
@@ -12724,7 +13230,7 @@ async function grantCloudBuildPermissions(gcpProjectId, region) {
|
|
|
12724
13230
|
];
|
|
12725
13231
|
spinner.text = "Granting permissions to Cloud Build service account...";
|
|
12726
13232
|
for (const { role } of cloudBuildRoles) {
|
|
12727
|
-
await
|
|
13233
|
+
await execAsync11(
|
|
12728
13234
|
`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:${trimmedProjectNumber}@cloudbuild.gserviceaccount.com" --role="${role}" --quiet`
|
|
12729
13235
|
);
|
|
12730
13236
|
}
|
|
@@ -12735,13 +13241,13 @@ async function grantCloudBuildPermissions(gcpProjectId, region) {
|
|
|
12735
13241
|
];
|
|
12736
13242
|
spinner.text = "Granting permissions to Cloud Functions service account...";
|
|
12737
13243
|
for (const { role } of serverlessRobotRoles) {
|
|
12738
|
-
await
|
|
13244
|
+
await execAsync11(
|
|
12739
13245
|
`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:service-${trimmedProjectNumber}@serverless-robot-prod.iam.gserviceaccount.com" --role="${role}" --quiet`
|
|
12740
13246
|
);
|
|
12741
13247
|
}
|
|
12742
13248
|
spinner.text = "Checking for gcf-artifacts repository...";
|
|
12743
13249
|
try {
|
|
12744
|
-
await
|
|
13250
|
+
await execAsync11(`gcloud artifacts repositories describe gcf-artifacts --location=${region} --project=${gcpProjectId}`);
|
|
12745
13251
|
await grantGcfArtifactsPermissions(gcpProjectId, region);
|
|
12746
13252
|
} catch {
|
|
12747
13253
|
}
|
|
@@ -12759,7 +13265,7 @@ async function grantCloudBuildPermissions(gcpProjectId, region) {
|
|
|
12759
13265
|
async function grantGcfArtifactsPermissions(gcpProjectId, region) {
|
|
12760
13266
|
const spinner = ora4("Granting gcf-artifacts permissions...").start();
|
|
12761
13267
|
try {
|
|
12762
|
-
const { stdout: projectNumber } = await
|
|
13268
|
+
const { stdout: projectNumber } = await execAsync11(
|
|
12763
13269
|
`gcloud projects describe ${gcpProjectId} --format="value(projectNumber)"`
|
|
12764
13270
|
);
|
|
12765
13271
|
const trimmedProjectNumber = projectNumber.trim();
|
|
@@ -12771,7 +13277,7 @@ async function grantGcfArtifactsPermissions(gcpProjectId, region) {
|
|
|
12771
13277
|
spinner.text = "Granting artifactregistry.writer on gcf-artifacts...";
|
|
12772
13278
|
for (const sa of serviceAccounts) {
|
|
12773
13279
|
try {
|
|
12774
|
-
await
|
|
13280
|
+
await execAsync11(
|
|
12775
13281
|
`gcloud artifacts repositories add-iam-policy-binding gcf-artifacts --location=${region} --project=${gcpProjectId} --member="serviceAccount:${sa}" --role="roles/artifactregistry.writer" --quiet`
|
|
12776
13282
|
);
|
|
12777
13283
|
} catch {
|
|
@@ -12856,7 +13362,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12856
13362
|
const regionMatch = fullPath.match(/locations\/([^/]+)/);
|
|
12857
13363
|
const region = regionMatch ? regionMatch[1] : "us-east1";
|
|
12858
13364
|
spinner.text = `Deleting Artifact Registry ${name}...`;
|
|
12859
|
-
await
|
|
13365
|
+
await execAsync11(
|
|
12860
13366
|
`gcloud artifacts repositories delete ${name} --location=${region} --project=${gcpProjectId} --quiet`
|
|
12861
13367
|
);
|
|
12862
13368
|
spinner.succeed(`Deleted Artifact Registry ${name}`);
|
|
@@ -12867,7 +13373,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12867
13373
|
case "serviceaccount/Account": {
|
|
12868
13374
|
spinner.text = `Deleting Service Account ${name}...`;
|
|
12869
13375
|
const email = name.includes("@") ? name : `${name}@${gcpProjectId}.iam.gserviceaccount.com`;
|
|
12870
|
-
await
|
|
13376
|
+
await execAsync11(
|
|
12871
13377
|
`gcloud iam service-accounts delete ${email} --project=${gcpProjectId} --quiet`
|
|
12872
13378
|
);
|
|
12873
13379
|
spinner.succeed(`Deleted Service Account ${name}`);
|
|
@@ -12878,7 +13384,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12878
13384
|
case "globalAddresses":
|
|
12879
13385
|
case "compute/GlobalAddress": {
|
|
12880
13386
|
spinner.text = `Deleting Global Address ${name}...`;
|
|
12881
|
-
await
|
|
13387
|
+
await execAsync11(
|
|
12882
13388
|
`gcloud compute addresses delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12883
13389
|
);
|
|
12884
13390
|
spinner.succeed(`Deleted Global Address ${name}`);
|
|
@@ -12896,7 +13402,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12896
13402
|
}
|
|
12897
13403
|
spinner.text = `Deleting URL Map ${name}...`;
|
|
12898
13404
|
}
|
|
12899
|
-
await
|
|
13405
|
+
await execAsync11(
|
|
12900
13406
|
`gcloud compute url-maps delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12901
13407
|
);
|
|
12902
13408
|
spinner.succeed(`Deleted URL Map ${name}`);
|
|
@@ -12914,7 +13420,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12914
13420
|
}
|
|
12915
13421
|
spinner.text = `Deleting Target HTTP Proxy ${name}...`;
|
|
12916
13422
|
}
|
|
12917
|
-
await
|
|
13423
|
+
await execAsync11(
|
|
12918
13424
|
`gcloud compute target-http-proxies delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12919
13425
|
);
|
|
12920
13426
|
spinner.succeed(`Deleted Target HTTP Proxy ${name}`);
|
|
@@ -12931,7 +13437,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12931
13437
|
}
|
|
12932
13438
|
spinner.text = `Deleting Target HTTPS Proxy ${name}...`;
|
|
12933
13439
|
}
|
|
12934
|
-
await
|
|
13440
|
+
await execAsync11(
|
|
12935
13441
|
`gcloud compute target-https-proxies delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12936
13442
|
);
|
|
12937
13443
|
spinner.succeed(`Deleted Target HTTPS Proxy ${name}`);
|
|
@@ -12942,7 +13448,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12942
13448
|
case "globalForwardingRules":
|
|
12943
13449
|
case "compute/GlobalForwardingRule": {
|
|
12944
13450
|
spinner.text = `Deleting Forwarding Rule ${name}...`;
|
|
12945
|
-
await
|
|
13451
|
+
await execAsync11(
|
|
12946
13452
|
`gcloud compute forwarding-rules delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12947
13453
|
);
|
|
12948
13454
|
spinner.succeed(`Deleted Forwarding Rule ${name}`);
|
|
@@ -12952,7 +13458,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12952
13458
|
case "backendServices":
|
|
12953
13459
|
case "compute/BackendService": {
|
|
12954
13460
|
spinner.text = `Deleting Backend Service ${name}...`;
|
|
12955
|
-
await
|
|
13461
|
+
await execAsync11(
|
|
12956
13462
|
`gcloud compute backend-services delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12957
13463
|
);
|
|
12958
13464
|
spinner.succeed(`Deleted Backend Service ${name}`);
|
|
@@ -12962,7 +13468,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12962
13468
|
case "backendBuckets":
|
|
12963
13469
|
case "compute/BackendBucket": {
|
|
12964
13470
|
spinner.text = `Deleting Backend Bucket ${name}...`;
|
|
12965
|
-
await
|
|
13471
|
+
await execAsync11(
|
|
12966
13472
|
`gcloud compute backend-buckets delete ${name} --project=${gcpProjectId} --quiet`
|
|
12967
13473
|
);
|
|
12968
13474
|
spinner.succeed(`Deleted Backend Bucket ${name}`);
|
|
@@ -12972,7 +13478,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12972
13478
|
case "healthChecks":
|
|
12973
13479
|
case "compute/HealthCheck": {
|
|
12974
13480
|
spinner.text = `Deleting Health Check ${name}...`;
|
|
12975
|
-
await
|
|
13481
|
+
await execAsync11(
|
|
12976
13482
|
`gcloud compute health-checks delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
12977
13483
|
);
|
|
12978
13484
|
spinner.succeed(`Deleted Health Check ${name}`);
|
|
@@ -12984,7 +13490,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12984
13490
|
const regionMatch = fullPath.match(/regions\/([^/]+)/);
|
|
12985
13491
|
const region = regionMatch ? regionMatch[1] : "us-east1";
|
|
12986
13492
|
spinner.text = `Deleting Network Endpoint Group ${name}...`;
|
|
12987
|
-
await
|
|
13493
|
+
await execAsync11(
|
|
12988
13494
|
`gcloud compute network-endpoint-groups delete ${name} --region=${region} --project=${gcpProjectId} --quiet`
|
|
12989
13495
|
);
|
|
12990
13496
|
spinner.succeed(`Deleted Network Endpoint Group ${name}`);
|
|
@@ -12994,7 +13500,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
12994
13500
|
case "networks":
|
|
12995
13501
|
case "compute/Network": {
|
|
12996
13502
|
spinner.text = `Deleting VPC Network ${name}...`;
|
|
12997
|
-
await
|
|
13503
|
+
await execAsync11(
|
|
12998
13504
|
`gcloud compute networks delete ${name} --project=${gcpProjectId} --quiet`
|
|
12999
13505
|
);
|
|
13000
13506
|
spinner.succeed(`Deleted VPC Network ${name}`);
|
|
@@ -13006,7 +13512,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
13006
13512
|
const locationMatch = fullPath.match(/locations\/([^/]+)/);
|
|
13007
13513
|
const location = locationMatch ? locationMatch[1] : "us-east1";
|
|
13008
13514
|
spinner.text = `Deleting Cloud Function ${name}...`;
|
|
13009
|
-
await
|
|
13515
|
+
await execAsync11(
|
|
13010
13516
|
`gcloud functions delete ${name} --region=${location} --project=${gcpProjectId} --gen2 --quiet`
|
|
13011
13517
|
);
|
|
13012
13518
|
spinner.succeed(`Deleted Cloud Function ${name}`);
|
|
@@ -13018,7 +13524,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
13018
13524
|
const locationMatch = fullPath.match(/locations\/([^/]+)/);
|
|
13019
13525
|
const location = locationMatch ? locationMatch[1] : "us-east1";
|
|
13020
13526
|
spinner.text = `Deleting Cloud Run service ${name}...`;
|
|
13021
|
-
await
|
|
13527
|
+
await execAsync11(
|
|
13022
13528
|
`gcloud run services delete ${name} --region=${location} --project=${gcpProjectId} --quiet`
|
|
13023
13529
|
);
|
|
13024
13530
|
spinner.succeed(`Deleted Cloud Run service ${name}`);
|
|
@@ -13028,7 +13534,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
13028
13534
|
case "topics":
|
|
13029
13535
|
case "pubsub/Topic": {
|
|
13030
13536
|
spinner.text = `Deleting Pub/Sub topic ${name}...`;
|
|
13031
|
-
await
|
|
13537
|
+
await execAsync11(`gcloud pubsub topics delete ${name} --project=${gcpProjectId} --quiet`);
|
|
13032
13538
|
spinner.succeed(`Deleted Pub/Sub topic ${name}`);
|
|
13033
13539
|
return true;
|
|
13034
13540
|
}
|
|
@@ -13044,7 +13550,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
13044
13550
|
}
|
|
13045
13551
|
spinner.text = `Deleting SSL Certificate ${name}...`;
|
|
13046
13552
|
}
|
|
13047
|
-
await
|
|
13553
|
+
await execAsync11(
|
|
13048
13554
|
`gcloud compute ssl-certificates delete ${name} --global --project=${gcpProjectId} --quiet`
|
|
13049
13555
|
);
|
|
13050
13556
|
spinner.succeed(`Deleted SSL Certificate ${name}`);
|
|
@@ -13054,7 +13560,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
13054
13560
|
case "buckets":
|
|
13055
13561
|
case "storage/Bucket": {
|
|
13056
13562
|
spinner.text = `Deleting Storage Bucket ${name}...`;
|
|
13057
|
-
await
|
|
13563
|
+
await execAsync11(`gcloud storage rm -r gs://${name} --project=${gcpProjectId}`);
|
|
13058
13564
|
spinner.succeed(`Deleted Storage Bucket ${name}`);
|
|
13059
13565
|
return true;
|
|
13060
13566
|
}
|
|
@@ -13064,7 +13570,7 @@ async function forceDeleteResource(resource, gcpProjectId) {
|
|
|
13064
13570
|
const regionMatch = fullPath.match(/locations\/([^/]+)/);
|
|
13065
13571
|
const region = regionMatch ? regionMatch[1] : "us-east1";
|
|
13066
13572
|
spinner.text = `Deleting VPC Connector ${name}...`;
|
|
13067
|
-
await
|
|
13573
|
+
await execAsync11(
|
|
13068
13574
|
`gcloud compute networks vpc-access connectors delete ${name} --region=${region} --project=${gcpProjectId} --quiet`
|
|
13069
13575
|
);
|
|
13070
13576
|
spinner.succeed(`Deleted VPC Connector ${name}`);
|
|
@@ -13147,7 +13653,7 @@ async function findDependentResources(resourceName, resourceType, gcpProjectId)
|
|
|
13147
13653
|
const dependencies = [];
|
|
13148
13654
|
try {
|
|
13149
13655
|
if (resourceType === "url-map") {
|
|
13150
|
-
const { stdout: httpProxies } = await
|
|
13656
|
+
const { stdout: httpProxies } = await execAsync11(
|
|
13151
13657
|
`gcloud compute target-http-proxies list --project=${gcpProjectId} --format="json" 2>/dev/null || echo "[]"`
|
|
13152
13658
|
);
|
|
13153
13659
|
const proxies = JSON.parse(httpProxies);
|
|
@@ -13160,7 +13666,7 @@ async function findDependentResources(resourceName, resourceType, gcpProjectId)
|
|
|
13160
13666
|
});
|
|
13161
13667
|
}
|
|
13162
13668
|
}
|
|
13163
|
-
const { stdout: httpsProxies } = await
|
|
13669
|
+
const { stdout: httpsProxies } = await execAsync11(
|
|
13164
13670
|
`gcloud compute target-https-proxies list --project=${gcpProjectId} --format="json" 2>/dev/null || echo "[]"`
|
|
13165
13671
|
);
|
|
13166
13672
|
const httpsProxyList = JSON.parse(httpsProxies);
|
|
@@ -13174,7 +13680,7 @@ async function findDependentResources(resourceName, resourceType, gcpProjectId)
|
|
|
13174
13680
|
}
|
|
13175
13681
|
}
|
|
13176
13682
|
} else if (resourceType === "http-proxy" || resourceType === "https-proxy") {
|
|
13177
|
-
const { stdout: rules } = await
|
|
13683
|
+
const { stdout: rules } = await execAsync11(
|
|
13178
13684
|
`gcloud compute forwarding-rules list --global --project=${gcpProjectId} --format="json" 2>/dev/null || echo "[]"`
|
|
13179
13685
|
);
|
|
13180
13686
|
const ruleList = JSON.parse(rules);
|
|
@@ -13188,7 +13694,7 @@ async function findDependentResources(resourceName, resourceType, gcpProjectId)
|
|
|
13188
13694
|
}
|
|
13189
13695
|
}
|
|
13190
13696
|
} else if (resourceType === "ssl-cert") {
|
|
13191
|
-
const { stdout: httpsProxies } = await
|
|
13697
|
+
const { stdout: httpsProxies } = await execAsync11(
|
|
13192
13698
|
`gcloud compute target-https-proxies list --project=${gcpProjectId} --format="json" 2>/dev/null || echo "[]"`
|
|
13193
13699
|
);
|
|
13194
13700
|
const proxyList = JSON.parse(httpsProxies);
|
|
@@ -13223,7 +13729,7 @@ async function grantResourceDeletePermissions(resourcePath, gcpProjectId) {
|
|
|
13223
13729
|
const inquirer6 = await import("inquirer");
|
|
13224
13730
|
let currentAccount = "";
|
|
13225
13731
|
try {
|
|
13226
|
-
const { stdout } = await
|
|
13732
|
+
const { stdout } = await execAsync11("gcloud config get-value account");
|
|
13227
13733
|
currentAccount = stdout.trim();
|
|
13228
13734
|
} catch {
|
|
13229
13735
|
}
|
|
@@ -13273,7 +13779,7 @@ async function grantResourceDeletePermissions(resourcePath, gcpProjectId) {
|
|
|
13273
13779
|
});
|
|
13274
13780
|
child.on("error", reject);
|
|
13275
13781
|
});
|
|
13276
|
-
const { stdout } = await
|
|
13782
|
+
const { stdout } = await execAsync11("gcloud config get-value account");
|
|
13277
13783
|
email = stdout.trim();
|
|
13278
13784
|
console.log(chalk5.green(`
|
|
13279
13785
|
Now authenticated as: ${email}
|
|
@@ -13323,7 +13829,7 @@ async function grantResourceDeletePermissions(resourcePath, gcpProjectId) {
|
|
|
13323
13829
|
roleName = "Editor";
|
|
13324
13830
|
}
|
|
13325
13831
|
spinner.text = `Granting ${roleName} role to ${email}...`;
|
|
13326
|
-
await
|
|
13832
|
+
await execAsync11(
|
|
13327
13833
|
`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="user:${email}" --role="${role}" --quiet`
|
|
13328
13834
|
);
|
|
13329
13835
|
spinner.succeed(`Granted ${roleName} role to ${email}`);
|
|
@@ -14388,9 +14894,9 @@ eventsCommand.action(async () => {
|
|
|
14388
14894
|
// src/commands/infra/inventory.ts
|
|
14389
14895
|
import { Command as Command13 } from "commander";
|
|
14390
14896
|
import chalk14 from "chalk";
|
|
14391
|
-
import { exec as
|
|
14392
|
-
import { promisify as
|
|
14393
|
-
var
|
|
14897
|
+
import { exec as exec12 } from "child_process";
|
|
14898
|
+
import { promisify as promisify12 } from "util";
|
|
14899
|
+
var execAsync12 = promisify12(exec12);
|
|
14394
14900
|
var LABEL_UPDATE_COMMANDS = {
|
|
14395
14901
|
"VPC Network": (projectId, name, labels) => {
|
|
14396
14902
|
const labelStr = Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(",");
|
|
@@ -14431,7 +14937,7 @@ async function updateResourceLabels(projectId, resourceType, resourceName, label
|
|
|
14431
14937
|
command = command.replace(/us-central1/g, region);
|
|
14432
14938
|
}
|
|
14433
14939
|
try {
|
|
14434
|
-
await
|
|
14940
|
+
await execAsync12(command, { timeout: 6e4 });
|
|
14435
14941
|
return { success: true };
|
|
14436
14942
|
} catch (err) {
|
|
14437
14943
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -14461,7 +14967,7 @@ async function shareResource(projectId, resourceType, resourceName, existingLabe
|
|
|
14461
14967
|
}
|
|
14462
14968
|
async function runGcloudCommand2(command, timeoutMs = 3e4) {
|
|
14463
14969
|
try {
|
|
14464
|
-
const { stdout } = await
|
|
14970
|
+
const { stdout } = await execAsync12(command, { timeout: timeoutMs });
|
|
14465
14971
|
const trimmed = stdout.trim();
|
|
14466
14972
|
if (!trimmed || trimmed === "[]") {
|
|
14467
14973
|
return [];
|
|
@@ -14671,7 +15177,7 @@ async function getGcpProjectId(explicitProject) {
|
|
|
14671
15177
|
} catch {
|
|
14672
15178
|
}
|
|
14673
15179
|
try {
|
|
14674
|
-
const { stdout } = await
|
|
15180
|
+
const { stdout } = await execAsync12("gcloud config get-value project 2>/dev/null");
|
|
14675
15181
|
const gcloudProject = stdout.trim();
|
|
14676
15182
|
if (gcloudProject && gcloudProject !== "(unset)") {
|
|
14677
15183
|
return gcloudProject;
|
|
@@ -14851,9 +15357,9 @@ import { Command as Command14 } from "commander";
|
|
|
14851
15357
|
import chalk15 from "chalk";
|
|
14852
15358
|
import * as path19 from "path";
|
|
14853
15359
|
import * as fs16 from "fs/promises";
|
|
14854
|
-
import { exec as
|
|
14855
|
-
import { promisify as
|
|
14856
|
-
var
|
|
15360
|
+
import { exec as exec13 } from "child_process";
|
|
15361
|
+
import { promisify as promisify13 } from "util";
|
|
15362
|
+
var execAsync13 = promisify13(exec13);
|
|
14857
15363
|
var STACKSOLO_DIR7 = ".stacksolo";
|
|
14858
15364
|
var CONFIG_FILENAME5 = "stacksolo.config.json";
|
|
14859
15365
|
var doctorCommand = new Command14("doctor").description("Check system prerequisites and GCP configuration").option("--fix", "Attempt to fix issues automatically").option("--verbose", "Show detailed output for each check").action(async (options) => {
|
|
@@ -14931,7 +15437,7 @@ function displayResult(result, verbose) {
|
|
|
14931
15437
|
}
|
|
14932
15438
|
async function checkNode(_options) {
|
|
14933
15439
|
try {
|
|
14934
|
-
const { stdout } = await
|
|
15440
|
+
const { stdout } = await execAsync13("node --version");
|
|
14935
15441
|
const version = stdout.trim().replace("v", "");
|
|
14936
15442
|
const major = parseInt(version.split(".")[0], 10);
|
|
14937
15443
|
if (major >= 18) {
|
|
@@ -14966,7 +15472,7 @@ async function checkNode(_options) {
|
|
|
14966
15472
|
}
|
|
14967
15473
|
async function checkTerraform(_options) {
|
|
14968
15474
|
try {
|
|
14969
|
-
const { stdout } = await
|
|
15475
|
+
const { stdout } = await execAsync13("terraform version -json");
|
|
14970
15476
|
const data = JSON.parse(stdout);
|
|
14971
15477
|
const version = data.terraform_version;
|
|
14972
15478
|
return {
|
|
@@ -14976,7 +15482,7 @@ async function checkTerraform(_options) {
|
|
|
14976
15482
|
};
|
|
14977
15483
|
} catch {
|
|
14978
15484
|
try {
|
|
14979
|
-
const { stdout } = await
|
|
15485
|
+
const { stdout } = await execAsync13("terraform version");
|
|
14980
15486
|
const match = stdout.match(/Terraform v(\d+\.\d+\.\d+)/);
|
|
14981
15487
|
if (match) {
|
|
14982
15488
|
return {
|
|
@@ -14997,11 +15503,11 @@ async function checkTerraform(_options) {
|
|
|
14997
15503
|
}
|
|
14998
15504
|
async function checkDocker(_options) {
|
|
14999
15505
|
try {
|
|
15000
|
-
const { stdout } = await
|
|
15506
|
+
const { stdout } = await execAsync13("docker --version");
|
|
15001
15507
|
const match = stdout.match(/Docker version (\d+\.\d+\.\d+)/);
|
|
15002
15508
|
const version = match ? match[1] : "unknown";
|
|
15003
15509
|
try {
|
|
15004
|
-
await
|
|
15510
|
+
await execAsync13("docker info", { timeout: 5e3 });
|
|
15005
15511
|
return {
|
|
15006
15512
|
name: "Docker",
|
|
15007
15513
|
status: "ok",
|
|
@@ -15026,7 +15532,7 @@ async function checkDocker(_options) {
|
|
|
15026
15532
|
}
|
|
15027
15533
|
async function checkGcloud(_options) {
|
|
15028
15534
|
try {
|
|
15029
|
-
const { stdout } = await
|
|
15535
|
+
const { stdout } = await execAsync13("gcloud version --format=json");
|
|
15030
15536
|
const data = JSON.parse(stdout);
|
|
15031
15537
|
const version = data["Google Cloud SDK"];
|
|
15032
15538
|
return {
|
|
@@ -15045,7 +15551,7 @@ async function checkGcloud(_options) {
|
|
|
15045
15551
|
}
|
|
15046
15552
|
async function checkGcpAuth(_options) {
|
|
15047
15553
|
try {
|
|
15048
|
-
const { stdout } = await
|
|
15554
|
+
const { stdout } = await execAsync13("gcloud auth list --format=json");
|
|
15049
15555
|
const accounts = JSON.parse(stdout);
|
|
15050
15556
|
if (accounts.length === 0) {
|
|
15051
15557
|
return {
|
|
@@ -15065,7 +15571,7 @@ async function checkGcpAuth(_options) {
|
|
|
15065
15571
|
};
|
|
15066
15572
|
}
|
|
15067
15573
|
try {
|
|
15068
|
-
await
|
|
15574
|
+
await execAsync13("gcloud auth application-default print-access-token", { timeout: 5e3 });
|
|
15069
15575
|
return {
|
|
15070
15576
|
name: "GCP Authentication",
|
|
15071
15577
|
status: "ok",
|
|
@@ -15121,7 +15627,7 @@ async function checkGcpProjectAccess(_options) {
|
|
|
15121
15627
|
try {
|
|
15122
15628
|
const config = parseConfig(configPath);
|
|
15123
15629
|
const projectId = config.project.gcpProjectId;
|
|
15124
|
-
const { stdout } = await
|
|
15630
|
+
const { stdout } = await execAsync13(
|
|
15125
15631
|
`gcloud projects describe ${projectId} --format="value(projectId)"`,
|
|
15126
15632
|
{ timeout: 1e4 }
|
|
15127
15633
|
);
|
|
@@ -15162,7 +15668,7 @@ async function checkRequiredApis(_options) {
|
|
|
15162
15668
|
"cloudbuild.googleapis.com",
|
|
15163
15669
|
"artifactregistry.googleapis.com"
|
|
15164
15670
|
];
|
|
15165
|
-
const { stdout } = await
|
|
15671
|
+
const { stdout } = await execAsync13(
|
|
15166
15672
|
`gcloud services list --project=${projectId} --enabled --format="value(NAME)"`,
|
|
15167
15673
|
{ timeout: 3e4 }
|
|
15168
15674
|
);
|
|
@@ -15390,9 +15896,9 @@ import chalk17 from "chalk";
|
|
|
15390
15896
|
import ora7 from "ora";
|
|
15391
15897
|
import * as path21 from "path";
|
|
15392
15898
|
import * as fs18 from "fs/promises";
|
|
15393
|
-
import { exec as
|
|
15394
|
-
import { promisify as
|
|
15395
|
-
var
|
|
15899
|
+
import { exec as exec14 } from "child_process";
|
|
15900
|
+
import { promisify as promisify14 } from "util";
|
|
15901
|
+
var execAsync14 = promisify14(exec14);
|
|
15396
15902
|
var STACKSOLO_DIR9 = ".stacksolo";
|
|
15397
15903
|
var CONFIG_FILENAME7 = "stacksolo.config.json";
|
|
15398
15904
|
function getConfigPath5() {
|
|
@@ -15414,7 +15920,7 @@ var buildCommand = new Command16("build").description("Build and push container
|
|
|
15414
15920
|
return;
|
|
15415
15921
|
}
|
|
15416
15922
|
try {
|
|
15417
|
-
await
|
|
15923
|
+
await execAsync14("docker --version");
|
|
15418
15924
|
} catch {
|
|
15419
15925
|
console.log(chalk17.red(" Docker CLI not found.\n"));
|
|
15420
15926
|
console.log(chalk17.gray(" Install Docker Desktop: https://www.docker.com/products/docker-desktop\n"));
|
|
@@ -15453,7 +15959,7 @@ var buildCommand = new Command16("build").description("Build and push container
|
|
|
15453
15959
|
if (options.push) {
|
|
15454
15960
|
const authSpinner = ora7("Configuring Docker authentication...").start();
|
|
15455
15961
|
try {
|
|
15456
|
-
await
|
|
15962
|
+
await execAsync14(
|
|
15457
15963
|
`gcloud auth configure-docker ${config.project.region}-docker.pkg.dev --quiet`
|
|
15458
15964
|
);
|
|
15459
15965
|
authSpinner.succeed("Docker authentication configured");
|
|
@@ -15482,7 +15988,7 @@ var buildCommand = new Command16("build").description("Build and push container
|
|
|
15482
15988
|
const buildSpinner = ora7(`Building ${container.name}...`).start();
|
|
15483
15989
|
try {
|
|
15484
15990
|
const buildCmd = `docker build -f "${dockerfilePath}" -t "${imageTag}" "${sourceDir}"`;
|
|
15485
|
-
await
|
|
15991
|
+
await execAsync14(buildCmd, { maxBuffer: 50 * 1024 * 1024 });
|
|
15486
15992
|
buildSpinner.succeed(`Built ${container.name}`);
|
|
15487
15993
|
} catch (error) {
|
|
15488
15994
|
buildSpinner.fail(`Failed to build ${container.name}`);
|
|
@@ -15494,7 +16000,7 @@ var buildCommand = new Command16("build").description("Build and push container
|
|
|
15494
16000
|
if (options.push) {
|
|
15495
16001
|
const pushSpinner = ora7(`Pushing ${container.name}...`).start();
|
|
15496
16002
|
try {
|
|
15497
|
-
await
|
|
16003
|
+
await execAsync14(`docker push "${imageTag}"`);
|
|
15498
16004
|
pushSpinner.succeed(`Pushed ${container.name}`);
|
|
15499
16005
|
console.log(chalk17.gray(` ${imageTag}
|
|
15500
16006
|
`));
|
|
@@ -15665,35 +16171,6 @@ function getPythonVersion(runtime) {
|
|
|
15665
16171
|
if (version === "312") return "3.12";
|
|
15666
16172
|
return "3.12";
|
|
15667
16173
|
}
|
|
15668
|
-
function getFrameworkConfig(framework, port = 3e3) {
|
|
15669
|
-
switch (framework) {
|
|
15670
|
-
case "vue":
|
|
15671
|
-
return {
|
|
15672
|
-
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", String(port)]
|
|
15673
|
-
};
|
|
15674
|
-
case "nuxt":
|
|
15675
|
-
return {
|
|
15676
|
-
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", String(port)]
|
|
15677
|
-
};
|
|
15678
|
-
case "react":
|
|
15679
|
-
return {
|
|
15680
|
-
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", String(port)]
|
|
15681
|
-
};
|
|
15682
|
-
case "next":
|
|
15683
|
-
return {
|
|
15684
|
-
command: ["npm", "run", "dev", "--", "--hostname", "0.0.0.0", "-p", String(port)]
|
|
15685
|
-
};
|
|
15686
|
-
case "svelte":
|
|
15687
|
-
case "sveltekit":
|
|
15688
|
-
return {
|
|
15689
|
-
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", String(port)]
|
|
15690
|
-
};
|
|
15691
|
-
default:
|
|
15692
|
-
return {
|
|
15693
|
-
command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", String(port)]
|
|
15694
|
-
};
|
|
15695
|
-
}
|
|
15696
|
-
}
|
|
15697
16174
|
function isPythonRuntime(runtime) {
|
|
15698
16175
|
return runtime.startsWith("python");
|
|
15699
16176
|
}
|
|
@@ -15704,31 +16181,27 @@ function generateFunctionManifests(options) {
|
|
|
15704
16181
|
const functionName = sanitizeName(options.function.name);
|
|
15705
16182
|
const runtimeConfig = getRuntimeConfig(options.function.runtime, options.function.entryPoint);
|
|
15706
16183
|
const isPython = isPythonRuntime(options.function.runtime);
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
[
|
|
15712
|
-
"
|
|
15713
|
-
"
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
15719
|
-
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
15724
|
-
|
|
15725
|
-
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
// Run the dev server
|
|
15729
|
-
runCmd
|
|
15730
|
-
].join(" && ")
|
|
15731
|
-
];
|
|
16184
|
+
let containerCommand;
|
|
16185
|
+
let containerImage;
|
|
16186
|
+
if (isPython) {
|
|
16187
|
+
containerImage = runtimeConfig.image;
|
|
16188
|
+
containerCommand = [
|
|
16189
|
+
"sh",
|
|
16190
|
+
"-c",
|
|
16191
|
+
[
|
|
16192
|
+
"cp -r /source/* /app/ 2>/dev/null || true",
|
|
16193
|
+
"cd /app",
|
|
16194
|
+
"pip install -r requirements.txt 2>/dev/null || true",
|
|
16195
|
+
"pip install functions-framework",
|
|
16196
|
+
runtimeConfig.command.join(" ")
|
|
16197
|
+
].join(" && ")
|
|
16198
|
+
];
|
|
16199
|
+
} else {
|
|
16200
|
+
containerImage = "node:20-slim";
|
|
16201
|
+
const functionsFrameworkCmd = runtimeConfig.command.join(" ");
|
|
16202
|
+
const command = 'cd /source && if [ -d dist ]; then echo "Running pre-built function from dist/" && find . -maxdepth 1 ! -name node_modules ! -name . -exec cp -r {} /app/ \\; && cd /app && grep -v "workspace:" package.json > package.json.tmp && mv package.json.tmp package.json && npm install --omit=dev && ' + functionsFrameworkCmd + '; else echo "No dist/ folder. Trying dev mode..." && find . -maxdepth 1 ! -name node_modules ! -name . -exec cp -r {} /app/ \\; && cd /app && npm install && (npm run dev 2>/dev/null || ' + functionsFrameworkCmd + "); fi";
|
|
16203
|
+
containerCommand = ["sh", "-c", command];
|
|
16204
|
+
}
|
|
15732
16205
|
const labels = {
|
|
15733
16206
|
"app.kubernetes.io/name": functionName,
|
|
15734
16207
|
"app.kubernetes.io/component": "function",
|
|
@@ -15761,7 +16234,7 @@ function generateFunctionManifests(options) {
|
|
|
15761
16234
|
containers: [
|
|
15762
16235
|
{
|
|
15763
16236
|
name: functionName,
|
|
15764
|
-
image:
|
|
16237
|
+
image: containerImage,
|
|
15765
16238
|
command: containerCommand,
|
|
15766
16239
|
ports: [
|
|
15767
16240
|
{
|
|
@@ -15858,7 +16331,7 @@ function sanitizeName(name) {
|
|
|
15858
16331
|
function generateUIManifests(options) {
|
|
15859
16332
|
const namespace = sanitizeNamespaceName(options.projectName);
|
|
15860
16333
|
const uiName = sanitizeName2(options.ui.name);
|
|
15861
|
-
const
|
|
16334
|
+
const command = 'if [ -d /source/dist ]; then echo "Serving pre-built static files from dist/" && npx serve /source/dist -l ' + options.port + ' -s; else echo "No dist/ folder found. Running dev server..." && cd /source && npm install && npm run dev -- --host 0.0.0.0 --port ' + options.port + "; fi";
|
|
15862
16335
|
const labels = {
|
|
15863
16336
|
"app.kubernetes.io/name": uiName,
|
|
15864
16337
|
"app.kubernetes.io/component": "ui",
|
|
@@ -15892,21 +16365,7 @@ function generateUIManifests(options) {
|
|
|
15892
16365
|
{
|
|
15893
16366
|
name: uiName,
|
|
15894
16367
|
image: "node:20-slim",
|
|
15895
|
-
|
|
15896
|
-
command: [
|
|
15897
|
-
"sh",
|
|
15898
|
-
"-c",
|
|
15899
|
-
[
|
|
15900
|
-
// Copy source files to /app, excluding node_modules (macOS binaries don't work in Linux)
|
|
15901
|
-
"cd /source",
|
|
15902
|
-
"find . -maxdepth 1 ! -name node_modules ! -name . -exec cp -r {} /app/ \\;",
|
|
15903
|
-
"cd /app",
|
|
15904
|
-
// Always install fresh for Linux platform
|
|
15905
|
-
"npm install",
|
|
15906
|
-
// Run the dev server
|
|
15907
|
-
frameworkConfig.command.join(" ")
|
|
15908
|
-
].join(" && ")
|
|
15909
|
-
],
|
|
16368
|
+
command: ["sh", "-c", command],
|
|
15910
16369
|
ports: [
|
|
15911
16370
|
{
|
|
15912
16371
|
containerPort: options.port,
|
|
@@ -16783,6 +17242,7 @@ function generateK8sManifests(options) {
|
|
|
16783
17242
|
const services = [];
|
|
16784
17243
|
const warnings = [];
|
|
16785
17244
|
const projectName = config.project.name;
|
|
17245
|
+
const packageManager = config.project.packageManager;
|
|
16786
17246
|
const portAllocator = createPortAllocator();
|
|
16787
17247
|
const servicePortMap = {};
|
|
16788
17248
|
let kernelUrl;
|
|
@@ -16845,7 +17305,8 @@ function generateK8sManifests(options) {
|
|
|
16845
17305
|
timeout: func.timeout
|
|
16846
17306
|
},
|
|
16847
17307
|
sourceDir,
|
|
16848
|
-
port
|
|
17308
|
+
port,
|
|
17309
|
+
packageManager
|
|
16849
17310
|
});
|
|
16850
17311
|
manifests.push(manifest);
|
|
16851
17312
|
services.push(func.name);
|
|
@@ -16866,7 +17327,8 @@ function generateK8sManifests(options) {
|
|
|
16866
17327
|
framework: ui.framework || "vue"
|
|
16867
17328
|
},
|
|
16868
17329
|
sourceDir,
|
|
16869
|
-
port
|
|
17330
|
+
port,
|
|
17331
|
+
packageManager
|
|
16870
17332
|
});
|
|
16871
17333
|
manifests.push(manifest);
|
|
16872
17334
|
services.push(ui.name);
|
|
@@ -17034,6 +17496,7 @@ function spawnService(service, manager, firebaseProjectId) {
|
|
|
17034
17496
|
};
|
|
17035
17497
|
if (firebaseProjectId) {
|
|
17036
17498
|
env.FIREBASE_PROJECT_ID = firebaseProjectId;
|
|
17499
|
+
env.VITE_FIREBASE_PROJECT_ID = firebaseProjectId;
|
|
17037
17500
|
}
|
|
17038
17501
|
const args = service.type === "ui" ? ["run", "dev", "--", "--port", String(service.port)] : ["run", "dev"];
|
|
17039
17502
|
const proc = spawn6("npm", args, {
|
|
@@ -17062,12 +17525,12 @@ function spawnService(service, manager, firebaseProjectId) {
|
|
|
17062
17525
|
});
|
|
17063
17526
|
return proc;
|
|
17064
17527
|
}
|
|
17065
|
-
async function startFirebaseEmulators(manager) {
|
|
17528
|
+
async function startFirebaseEmulators(manager, projectId = "demo-local") {
|
|
17066
17529
|
const spinner = ora8("Starting Firebase emulators...").start();
|
|
17067
17530
|
try {
|
|
17068
17531
|
const proc = spawn6(
|
|
17069
17532
|
"firebase",
|
|
17070
|
-
["emulators:start", "--only", "firestore,auth", "--project",
|
|
17533
|
+
["emulators:start", "--only", "firestore,auth", "--project", projectId],
|
|
17071
17534
|
{
|
|
17072
17535
|
stdio: ["ignore", "pipe", "pipe"],
|
|
17073
17536
|
shell: true
|
|
@@ -17172,13 +17635,13 @@ async function startLocalEnvironment(options) {
|
|
|
17172
17635
|
services: validServices,
|
|
17173
17636
|
isShuttingDown: false
|
|
17174
17637
|
};
|
|
17638
|
+
const firebaseProjectId = config.project.gcpKernel?.firebaseProjectId || config.project.gcpProjectId;
|
|
17175
17639
|
if (options.includeEmulators !== false) {
|
|
17176
|
-
const emulatorProc = await startFirebaseEmulators(manager);
|
|
17640
|
+
const emulatorProc = await startFirebaseEmulators(manager, firebaseProjectId);
|
|
17177
17641
|
if (emulatorProc) {
|
|
17178
17642
|
manager.processes.set("firebase-emulator", emulatorProc);
|
|
17179
17643
|
}
|
|
17180
17644
|
}
|
|
17181
|
-
const firebaseProjectId = config.project.gcpKernel?.firebaseProjectId || config.project.gcpProjectId;
|
|
17182
17645
|
const spinner = ora8("Starting services...").start();
|
|
17183
17646
|
for (const service of validServices) {
|
|
17184
17647
|
const proc = spawnService(service, manager, firebaseProjectId);
|
|
@@ -18212,9 +18675,9 @@ import chalk20 from "chalk";
|
|
|
18212
18675
|
import ora10 from "ora";
|
|
18213
18676
|
import * as path24 from "path";
|
|
18214
18677
|
import * as fs21 from "fs/promises";
|
|
18215
|
-
import { exec as
|
|
18216
|
-
import { promisify as
|
|
18217
|
-
var
|
|
18678
|
+
import { exec as exec15 } from "child_process";
|
|
18679
|
+
import { promisify as promisify15 } from "util";
|
|
18680
|
+
var execAsync15 = promisify15(exec15);
|
|
18218
18681
|
var STACKSOLO_DIR10 = ".stacksolo";
|
|
18219
18682
|
var CONFIG_FILENAME8 = "stacksolo.config.json";
|
|
18220
18683
|
var installCommand = new Command18("install").description("Install dependencies for all resources").option("-p, --parallel", "Install dependencies in parallel").action(async (options) => {
|
|
@@ -18278,7 +18741,7 @@ var installCommand = new Command18("install").description("Install dependencies
|
|
|
18278
18741
|
const installDir = async (dir) => {
|
|
18279
18742
|
const spinner = ora10(`Installing ${dir.name}...`).start();
|
|
18280
18743
|
try {
|
|
18281
|
-
await
|
|
18744
|
+
await execAsync15("npm install", { cwd: dir.path, timeout: 12e4 });
|
|
18282
18745
|
spinner.succeed(`Installed ${dir.name}`);
|
|
18283
18746
|
return { success: true, name: dir.name };
|
|
18284
18747
|
} catch (error) {
|