gitops-ai 1.2.1 → 1.2.3
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/README.md +15 -30
- package/dist/commands/bootstrap.js +181 -126
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/sops.js +5 -9
- package/dist/commands/sops.js.map +1 -1
- package/dist/core/bootstrap-runner.js +35 -6
- package/dist/core/bootstrap-runner.js.map +1 -1
- package/dist/core/dependencies.d.ts +4 -0
- package/dist/core/dependencies.js +75 -29
- package/dist/core/dependencies.js.map +1 -1
- package/dist/core/flux.d.ts +5 -2
- package/dist/core/flux.js +38 -46
- package/dist/core/flux.js.map +1 -1
- package/dist/core/k8s-api.d.ts +33 -0
- package/dist/core/k8s-api.js +255 -0
- package/dist/core/k8s-api.js.map +1 -0
- package/dist/core/kubernetes.d.ts +2 -2
- package/dist/core/kubernetes.js +29 -48
- package/dist/core/kubernetes.js.map +1 -1
- package/dist/core/template-sync.js +1 -1
- package/dist/core/template-sync.js.map +1 -1
- package/dist/utils/log.js +35 -4
- package/dist/utils/log.js.map +1 -1
- package/dist/utils/shell.d.ts +7 -0
- package/dist/utils/shell.js +25 -1
- package/dist/utils/shell.js.map +1 -1
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -16,48 +16,33 @@ GitOps-managed Kubernetes infrastructure for AI-powered applications powered by
|
|
|
16
16
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
If you already have **Node.js** on your Mac or Linux machine:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npx gitops-ai bootstrap
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
If Node is not installed yet, use the helper script — it installs Node when missing (on macOS, [Homebrew](https://brew.sh) is required for that step), then starts the wizard:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
curl -sfL https://raw.githubusercontent.com/
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Or, if you already have Node.js >= 18:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npx gitops-ai bootstrap
|
|
28
|
+
curl -sfL https://raw.githubusercontent.com/GitOpsAI/gitops-ai-bootstrapper/refs/heads/main/install.sh | bash
|
|
35
29
|
```
|
|
36
30
|
|
|
37
31
|
The interactive wizard will prompt for your Git provider (GitHub or GitLab), create or use a repository from the [GitOps AI Template](https://github.com/GitOpsAI/gitops-ai-template), and run the full bootstrap.
|
|
38
32
|
|
|
39
33
|
## Requirements
|
|
40
34
|
|
|
41
|
-
| Resource | Minimum
|
|
42
|
-
|
|
43
|
-
| **CPU** | 2+ cores
|
|
44
|
-
| **Memory** | 4+ GB
|
|
45
|
-
| **Disk** | 20+ GB free
|
|
46
|
-
| **OS** | Ubuntu 25.04+ or macOS
|
|
47
|
-
| **Node.js** | 18+ (installed automatically by `install.sh`)
|
|
35
|
+
| Resource | Minimum |
|
|
36
|
+
|-------------|----------------------------------------------------------|
|
|
37
|
+
| **CPU** | 2+ cores |
|
|
38
|
+
| **Memory** | 4+ GB |
|
|
39
|
+
| **Disk** | 20+ GB free |
|
|
40
|
+
| **OS** | Ubuntu 25.04+ or macOS |
|
|
41
|
+
| **Node.js** | 18+ (installed automatically by `install.sh`) |
|
|
42
|
+
| **Docker** | macOS only — Docker Desktop; not used on Linux bootstrap |
|
|
48
43
|
|
|
49
44
|
You will also need a [GitLab PAT](docs/prerequisites.md#1-gitlab-personal-access-token), a [Cloudflare API Token](docs/prerequisites.md#2-cloudflare-api-token) (if using automatic DNS/TLS), and an [OpenAI API Key](docs/prerequisites.md#3-openai-api-key) (if using OpenClaw). See [Prerequisites](docs/prerequisites.md) for full details.
|
|
50
45
|
|
|
51
|
-
### Docker runtime (macOS only)
|
|
52
|
-
|
|
53
|
-
macOS requires a Docker-compatible runtime for k3d. Install one of:
|
|
54
|
-
|
|
55
|
-
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
|
56
|
-
- [OrbStack](https://orbstack.dev/)
|
|
57
|
-
- [Colima](https://github.com/abiosoft/colima)
|
|
58
|
-
|
|
59
|
-
On Linux the bootstrap installs k3s directly -- no Docker required.
|
|
60
|
-
|
|
61
46
|
## Template Repository
|
|
62
47
|
|
|
63
48
|
This CLI bootstraps clusters from the [GitOps AI Template](https://github.com/GitOpsAI/gitops-ai-template) -- a ready-made GitOps repository structure that Flux uses as the single source of truth for your cluster.
|
|
@@ -74,11 +59,11 @@ Keeping the template in a separate repository means:
|
|
|
74
59
|
|
|
75
60
|
The upstream template (and your bootstrapped repo) is organised roughly as:
|
|
76
61
|
|
|
77
|
-
| Path | Role
|
|
78
|
-
|
|
62
|
+
| Path | Role |
|
|
63
|
+
|--------------------------|-----------------------------------------------------------------------------------------------------------------|
|
|
79
64
|
| `templates/<category>/…` | Shared Helm bases and component manifests (e.g. `templates/system/`, `templates/ai/`, `templates/monitoring/`). |
|
|
80
|
-
| `clusters/_template/` | Prototype cluster layout; the CLI copies this to `clusters/<your-cluster-name>/` during bootstrap.
|
|
81
|
-
| `clusters/<name>/` | Your live cluster overlay (`cluster-sync.yaml`, `components/`, encrypted secrets).
|
|
65
|
+
| `clusters/_template/` | Prototype cluster layout; the CLI copies this to `clusters/<your-cluster-name>/` during bootstrap. |
|
|
66
|
+
| `clusters/<name>/` | Your live cluster overlay (`cluster-sync.yaml`, `components/`, encrypted secrets). |
|
|
82
67
|
|
|
83
68
|
See [Architecture](docs/architecture.md) for diagrams and a fuller tree.
|
|
84
69
|
|
|
@@ -8,9 +8,10 @@ import { header, log, summary, nextSteps, finish, handleCancel, withSpinner, for
|
|
|
8
8
|
import { saveInstallPlan, loadInstallPlan, clearInstallPlan } from "../utils/config.js";
|
|
9
9
|
import { execAsync, exec, execSafe, commandExists } from "../utils/shell.js";
|
|
10
10
|
import { isMacOS, isCI } from "../utils/platform.js";
|
|
11
|
-
import { ensureAll } from "../core/dependencies.js";
|
|
11
|
+
import { ensureAll, ensureDockerDaemonReady } from "../core/dependencies.js";
|
|
12
12
|
import { runBootstrap, stripTemplateGitHubDirectory } from "../core/bootstrap-runner.js";
|
|
13
13
|
import * as k8s from "../core/kubernetes.js";
|
|
14
|
+
import * as k8sApi from "../core/k8s-api.js";
|
|
14
15
|
import * as flux from "../core/flux.js";
|
|
15
16
|
import { getProvider, } from "../core/git-provider.js";
|
|
16
17
|
import { COMPONENTS, REQUIRED_COMPONENT_IDS, DNS_TLS_COMPONENT_IDS, MONITORING_COMPONENT_IDS, OPTIONAL_COMPONENTS, SOURCE_TEMPLATE_HOST, SOURCE_PROJECT_PATH, } from "../schemas.js";
|
|
@@ -64,8 +65,11 @@ async function enrichWithUser(state, token, provider) {
|
|
|
64
65
|
// ---------------------------------------------------------------------------
|
|
65
66
|
// Wizard field definitions (Esc / Ctrl+C = go back one field)
|
|
66
67
|
// ---------------------------------------------------------------------------
|
|
67
|
-
function buildFields(detectedIp, hasSavedPlan) {
|
|
68
|
+
function buildFields(detectedIp, hasSavedPlan, savedPlanRaw) {
|
|
68
69
|
const saved = (state, key) => hasSavedPlan && !!state[key];
|
|
70
|
+
const skipAdditionalSettingsToggle = hasSavedPlan &&
|
|
71
|
+
savedPlanRaw != null &&
|
|
72
|
+
savedPlanRaw.enableAdditionalSettings !== undefined;
|
|
69
73
|
return [
|
|
70
74
|
// ── Setup Mode ──────────────────────────────────────────────────────
|
|
71
75
|
{
|
|
@@ -343,64 +347,6 @@ function buildFields(detectedIp, hasSavedPlan) {
|
|
|
343
347
|
? ["Local path", `./${state.repoLocalPath}`]
|
|
344
348
|
: ["Local repo path", resolve(state.repoLocalPath || ".")],
|
|
345
349
|
},
|
|
346
|
-
{
|
|
347
|
-
id: "repoBranch",
|
|
348
|
-
section: "Git Repository",
|
|
349
|
-
skip: (state) => saved(state, "repoBranch"),
|
|
350
|
-
run: async (state) => {
|
|
351
|
-
const v = await p.text({
|
|
352
|
-
message: pc.bold("Git branch for Flux"),
|
|
353
|
-
placeholder: "main",
|
|
354
|
-
defaultValue: state.repoBranch,
|
|
355
|
-
});
|
|
356
|
-
if (p.isCancel(v))
|
|
357
|
-
return back();
|
|
358
|
-
return { ...state, repoBranch: v };
|
|
359
|
-
},
|
|
360
|
-
review: (state) => ["Branch", state.repoBranch],
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
id: "templateTag",
|
|
364
|
-
section: "Git Repository",
|
|
365
|
-
hidden: (state) => !isNewRepo(state),
|
|
366
|
-
skip: (state) => saved(state, "templateTag"),
|
|
367
|
-
run: async (state) => {
|
|
368
|
-
const tags = await fetchTemplateTags();
|
|
369
|
-
if (tags.length > 0) {
|
|
370
|
-
const options = [
|
|
371
|
-
...tags.map((tag, i) => ({
|
|
372
|
-
value: tag,
|
|
373
|
-
label: tag,
|
|
374
|
-
hint: i === 0 ? "latest" : undefined,
|
|
375
|
-
})),
|
|
376
|
-
{
|
|
377
|
-
value: "__manual__",
|
|
378
|
-
label: "Enter manually…",
|
|
379
|
-
hint: "type a tag or branch name",
|
|
380
|
-
},
|
|
381
|
-
];
|
|
382
|
-
const v = await p.select({
|
|
383
|
-
message: pc.bold("Template version (tag) to clone"),
|
|
384
|
-
options,
|
|
385
|
-
initialValue: state.templateTag || tags[0],
|
|
386
|
-
});
|
|
387
|
-
if (p.isCancel(v))
|
|
388
|
-
return back();
|
|
389
|
-
if (v !== "__manual__") {
|
|
390
|
-
return { ...state, templateTag: v };
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
const v = await p.text({
|
|
394
|
-
message: pc.bold("Template tag or branch to clone"),
|
|
395
|
-
placeholder: "main",
|
|
396
|
-
defaultValue: state.templateTag || "main",
|
|
397
|
-
});
|
|
398
|
-
if (p.isCancel(v))
|
|
399
|
-
return back();
|
|
400
|
-
return { ...state, templateTag: v };
|
|
401
|
-
},
|
|
402
|
-
review: (state) => ["Template tag", state.templateTag],
|
|
403
|
-
},
|
|
404
350
|
// ── DNS & TLS ─────────────────────────────────────────────────────────
|
|
405
351
|
{
|
|
406
352
|
id: "manageDnsAndTls",
|
|
@@ -793,6 +739,87 @@ function buildFields(detectedIp, hasSavedPlan) {
|
|
|
793
739
|
},
|
|
794
740
|
review: (state) => ["Network", `${state.clusterPublicIp} allowed: ${state.ingressAllowedIps}`],
|
|
795
741
|
},
|
|
742
|
+
// ── Additional settings (last — rarely needed) ─────────────────────────
|
|
743
|
+
{
|
|
744
|
+
id: "enableAdditionalSettings",
|
|
745
|
+
section: "Additional settings",
|
|
746
|
+
skip: () => skipAdditionalSettingsToggle,
|
|
747
|
+
run: async (state) => {
|
|
748
|
+
const v = await p.confirm({
|
|
749
|
+
message: pc.bold("Setup additional settings?") +
|
|
750
|
+
`\n${pc.dim("Regularly not needed")}`,
|
|
751
|
+
initialValue: state.enableAdditionalSettings,
|
|
752
|
+
});
|
|
753
|
+
if (p.isCancel(v))
|
|
754
|
+
return back();
|
|
755
|
+
return { ...state, enableAdditionalSettings: v };
|
|
756
|
+
},
|
|
757
|
+
review: (state) => [
|
|
758
|
+
"Customize branch & template",
|
|
759
|
+
state.enableAdditionalSettings
|
|
760
|
+
? "Yes"
|
|
761
|
+
: "No — defaults (main / main)",
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
id: "repoBranch",
|
|
766
|
+
section: "Additional settings",
|
|
767
|
+
hidden: (state) => !state.enableAdditionalSettings,
|
|
768
|
+
skip: (state) => saved(state, "repoBranch"),
|
|
769
|
+
run: async (state) => {
|
|
770
|
+
const v = await p.text({
|
|
771
|
+
message: pc.bold("Git branch for Flux"),
|
|
772
|
+
placeholder: "main",
|
|
773
|
+
defaultValue: state.repoBranch,
|
|
774
|
+
});
|
|
775
|
+
if (p.isCancel(v))
|
|
776
|
+
return back();
|
|
777
|
+
return { ...state, repoBranch: v };
|
|
778
|
+
},
|
|
779
|
+
review: (state) => ["Flux Git branch", state.repoBranch],
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
id: "templateTag",
|
|
783
|
+
section: "Additional settings",
|
|
784
|
+
hidden: (state) => !isNewRepo(state) || !state.enableAdditionalSettings,
|
|
785
|
+
skip: (state) => saved(state, "templateTag"),
|
|
786
|
+
run: async (state) => {
|
|
787
|
+
const tags = await fetchTemplateTags();
|
|
788
|
+
if (tags.length > 0) {
|
|
789
|
+
const options = [
|
|
790
|
+
...tags.map((tag, i) => ({
|
|
791
|
+
value: tag,
|
|
792
|
+
label: tag,
|
|
793
|
+
hint: i === 0 ? "latest" : undefined,
|
|
794
|
+
})),
|
|
795
|
+
{
|
|
796
|
+
value: "__manual__",
|
|
797
|
+
label: "Enter manually…",
|
|
798
|
+
hint: "type a tag or branch name",
|
|
799
|
+
},
|
|
800
|
+
];
|
|
801
|
+
const v = await p.select({
|
|
802
|
+
message: pc.bold("Template version (tag) to clone"),
|
|
803
|
+
options,
|
|
804
|
+
initialValue: state.templateTag || tags[0],
|
|
805
|
+
});
|
|
806
|
+
if (p.isCancel(v))
|
|
807
|
+
return back();
|
|
808
|
+
if (v !== "__manual__") {
|
|
809
|
+
return { ...state, templateTag: v };
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const v = await p.text({
|
|
813
|
+
message: pc.bold("Template tag or branch to clone"),
|
|
814
|
+
placeholder: "main",
|
|
815
|
+
defaultValue: state.templateTag || "main",
|
|
816
|
+
});
|
|
817
|
+
if (p.isCancel(v))
|
|
818
|
+
return back();
|
|
819
|
+
return { ...state, templateTag: v };
|
|
820
|
+
},
|
|
821
|
+
review: (state) => ["Template tag", state.templateTag],
|
|
822
|
+
},
|
|
796
823
|
];
|
|
797
824
|
}
|
|
798
825
|
// ---------------------------------------------------------------------------
|
|
@@ -902,7 +929,12 @@ export async function openclawPair() {
|
|
|
902
929
|
handleCancel();
|
|
903
930
|
log.step("Listing pending device requests");
|
|
904
931
|
try {
|
|
905
|
-
|
|
932
|
+
const kc = k8sApi.kubeConfigFromDefault();
|
|
933
|
+
const code = await k8sApi.execInDeploymentContainerTty(kc, "openclaw", "openclaw", "main", ["node", "dist/index.js", "devices", "list"]);
|
|
934
|
+
if (code !== 0) {
|
|
935
|
+
log.error("devices list failed");
|
|
936
|
+
return process.exit(1);
|
|
937
|
+
}
|
|
906
938
|
}
|
|
907
939
|
catch {
|
|
908
940
|
log.error("Failed to list device requests");
|
|
@@ -918,7 +950,12 @@ export async function openclawPair() {
|
|
|
918
950
|
if (p.isCancel(requestId))
|
|
919
951
|
handleCancel();
|
|
920
952
|
log.step(`Approving device ${requestId}`);
|
|
921
|
-
|
|
953
|
+
const kcApprove = k8sApi.kubeConfigFromDefault();
|
|
954
|
+
const approve = await k8sApi.execInDeploymentContainer(kcApprove, "openclaw", "openclaw", "main", ["node", "dist/index.js", "devices", "approve", requestId]);
|
|
955
|
+
if (approve.exitCode !== 0) {
|
|
956
|
+
log.error(approve.stderr || "approve failed");
|
|
957
|
+
return process.exit(1);
|
|
958
|
+
}
|
|
922
959
|
log.success("Device paired successfully");
|
|
923
960
|
}
|
|
924
961
|
// ---------------------------------------------------------------------------
|
|
@@ -937,17 +974,17 @@ export async function bootstrap() {
|
|
|
937
974
|
console.log();
|
|
938
975
|
const version = readPackageVersion();
|
|
939
976
|
const logo = [
|
|
940
|
-
"
|
|
941
|
-
"
|
|
942
|
-
"
|
|
943
|
-
"
|
|
944
|
-
"
|
|
977
|
+
" ▄█████▄ ",
|
|
978
|
+
" ██ ◆ ██ ",
|
|
979
|
+
" ██ ██ ",
|
|
980
|
+
" ▀██ ██▀ ",
|
|
981
|
+
" ▀█▀ ",
|
|
945
982
|
];
|
|
946
983
|
const taglines = [
|
|
947
984
|
"💅 Secure, isolated & flexible GitOps infrastructure",
|
|
948
985
|
"🤖 Manage it yourself — or delegate to AI",
|
|
949
|
-
"🔐 Encrypted secrets, hardened containers,",
|
|
950
|
-
"
|
|
986
|
+
"🔐 Encrypted secrets, hardened containers, continuous delivery",
|
|
987
|
+
"",
|
|
951
988
|
pc.dim(`v${version}`),
|
|
952
989
|
];
|
|
953
990
|
const banner = logo
|
|
@@ -956,7 +993,8 @@ export async function bootstrap() {
|
|
|
956
993
|
p.box(banner, pc.bold("Welcome to GitOps AI Bootstrapper"), {
|
|
957
994
|
contentAlign: "left",
|
|
958
995
|
titleAlign: "center",
|
|
959
|
-
|
|
996
|
+
width: "auto",
|
|
997
|
+
rounded: false,
|
|
960
998
|
formatBorder: (text) => pc.cyan(text),
|
|
961
999
|
});
|
|
962
1000
|
// ── Load saved state ─────────────────────────────────────────────────
|
|
@@ -1019,6 +1057,13 @@ export async function bootstrap() {
|
|
|
1019
1057
|
...MONITORING_COMPONENT_IDS,
|
|
1020
1058
|
...OPTIONAL_COMPONENTS.map((c) => c.id),
|
|
1021
1059
|
];
|
|
1060
|
+
const enableAdditionalFromPlan = prev.enableAdditionalSettings === "true" ||
|
|
1061
|
+
(saved != null &&
|
|
1062
|
+
prev.enableAdditionalSettings === undefined &&
|
|
1063
|
+
((prev.repoBranch != null && prev.repoBranch !== "" && prev.repoBranch !== "main") ||
|
|
1064
|
+
(prev.templateTag != null &&
|
|
1065
|
+
prev.templateTag !== "" &&
|
|
1066
|
+
prev.templateTag !== "main")));
|
|
1022
1067
|
const initialState = {
|
|
1023
1068
|
gitProvider: prev.gitProvider ?? "github",
|
|
1024
1069
|
setupMode: prev.setupMode ?? "new",
|
|
@@ -1029,6 +1074,7 @@ export async function bootstrap() {
|
|
|
1029
1074
|
repoName: prev.repoName ?? "fluxcd_ai",
|
|
1030
1075
|
repoLocalPath: prev.repoLocalPath ?? "",
|
|
1031
1076
|
repoOwner: prev.repoOwner ?? "",
|
|
1077
|
+
enableAdditionalSettings: enableAdditionalFromPlan,
|
|
1032
1078
|
repoBranch: prev.repoBranch ?? "main",
|
|
1033
1079
|
templateTag: prev.templateTag ?? "",
|
|
1034
1080
|
letsencryptEmail: prev.letsencryptEmail ?? "",
|
|
@@ -1040,7 +1086,7 @@ export async function bootstrap() {
|
|
|
1040
1086
|
ingressAllowedIps: prev.ingressAllowedIps ?? "0.0.0.0/0",
|
|
1041
1087
|
clusterPublicIp: prev.clusterPublicIp ?? detectedIp,
|
|
1042
1088
|
};
|
|
1043
|
-
const wizard = await stepWizard(buildFields(detectedIp, !!saved), initialState);
|
|
1089
|
+
const wizard = await stepWizard(buildFields(detectedIp, !!saved, saved), initialState);
|
|
1044
1090
|
// ── Save config ─────────────────────────────────────────────────────
|
|
1045
1091
|
saveInstallPlan({
|
|
1046
1092
|
gitProvider: wizard.gitProvider,
|
|
@@ -1056,6 +1102,7 @@ export async function bootstrap() {
|
|
|
1056
1102
|
repoName: wizard.repoName,
|
|
1057
1103
|
repoLocalPath: wizard.repoLocalPath,
|
|
1058
1104
|
repoOwner: wizard.repoOwner,
|
|
1105
|
+
enableAdditionalSettings: String(wizard.enableAdditionalSettings),
|
|
1059
1106
|
repoBranch: wizard.repoBranch,
|
|
1060
1107
|
templateTag: wizard.templateTag,
|
|
1061
1108
|
cloudflareApiToken: wizard.cloudflareApiToken,
|
|
@@ -1064,60 +1111,67 @@ export async function bootstrap() {
|
|
|
1064
1111
|
selectedComponents: wizard.selectedComponents.join(","),
|
|
1065
1112
|
});
|
|
1066
1113
|
log.success("Configuration saved");
|
|
1067
|
-
// ──
|
|
1114
|
+
// ── CLI tools: explain + confirm + install only when something is missing ──
|
|
1068
1115
|
const toolDescriptions = [
|
|
1069
1116
|
["git", "Version control (repo operations)"],
|
|
1070
|
-
["
|
|
1071
|
-
["helm", "Kubernetes package manager (chart installs)"],
|
|
1072
|
-
["flux-operator", "FluxCD Operator CLI (GitOps reconciliation)"],
|
|
1117
|
+
["flux-operator", "FluxCD Operator CLI (installs Flux into the cluster)"],
|
|
1073
1118
|
["sops", "Mozilla SOPS (secret encryption)"],
|
|
1074
1119
|
["age", "Age encryption (SOPS key backend)"],
|
|
1075
1120
|
];
|
|
1121
|
+
if (isMacOS()) {
|
|
1122
|
+
toolDescriptions.push([
|
|
1123
|
+
"docker",
|
|
1124
|
+
"Docker-compatible runtime for k3d",
|
|
1125
|
+
]);
|
|
1126
|
+
}
|
|
1076
1127
|
if (isMacOS() || isCI()) {
|
|
1077
1128
|
toolDescriptions.push(["k3d", "Lightweight K3s in Docker (local cluster)"]);
|
|
1078
1129
|
}
|
|
1079
|
-
const
|
|
1080
|
-
.filter(([name]) => !commandExists(name))
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
pc.
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1130
|
+
const missingToolNames = toolDescriptions
|
|
1131
|
+
.filter(([name]) => !commandExists(name))
|
|
1132
|
+
.map(([name]) => name);
|
|
1133
|
+
if (missingToolNames.length > 0) {
|
|
1134
|
+
const toolListFormatted = toolDescriptions
|
|
1135
|
+
.map(([name, desc]) => {
|
|
1136
|
+
const status = commandExists(name)
|
|
1137
|
+
? pc.green("installed")
|
|
1138
|
+
: pc.yellow("will install");
|
|
1139
|
+
return ` ${pc.bold(name.padEnd(16))} ${pc.dim(desc)} [${status}]`;
|
|
1140
|
+
})
|
|
1141
|
+
.join("\n");
|
|
1142
|
+
const uninstallMacFormulae = toolDescriptions
|
|
1143
|
+
.map(([name]) => name)
|
|
1144
|
+
.filter((n) => n !== "docker")
|
|
1145
|
+
.join(" ");
|
|
1146
|
+
const uninstallMac = isMacOS() && toolDescriptions.some(([n]) => n === "docker")
|
|
1147
|
+
? `brew uninstall ${uninstallMacFormulae} && brew uninstall --cask docker`
|
|
1148
|
+
: `brew uninstall ${uninstallMacFormulae}`;
|
|
1149
|
+
p.note(`${pc.bold("The following CLI tools are required and will be installed if missing:")}\n\n` +
|
|
1150
|
+
toolListFormatted +
|
|
1151
|
+
"\n\n" +
|
|
1152
|
+
pc.dim("─".repeat(60)) + "\n\n" +
|
|
1153
|
+
pc.bold("Why are these needed?\n") +
|
|
1154
|
+
pc.dim("These tools are used to create and manage your Kubernetes cluster,\n") +
|
|
1155
|
+
pc.dim(`install Flux via flux-operator, encrypt secrets, and interact with ${providerLabel(wizard.gitProvider)}.\n`) +
|
|
1156
|
+
pc.bold("How to uninstall later:\n") +
|
|
1157
|
+
(isMacOS()
|
|
1158
|
+
? ` ${pc.cyan(`brew uninstall ${uninstallMac}`)}\n`
|
|
1159
|
+
: ` ${pc.cyan("sudo rm -f /usr/local/bin/{flux-operator,sops,age,age-keygen}")}\n` +
|
|
1160
|
+
` ${pc.cyan(`sudo apt remove -y git`)} ${pc.dim("(if installed via apt)")}\n`) +
|
|
1161
|
+
pc.dim("\nAlready-installed tools will be skipped. No system tools will be modified."), "Required CLI Tools");
|
|
1162
|
+
const proceed = await p.confirm({
|
|
1163
|
+
message: pc.bold(`Install ${missingToolNames.length} missing tool(s) and continue?`),
|
|
1164
|
+
initialValue: true,
|
|
1165
|
+
});
|
|
1166
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
1167
|
+
log.error("Aborted.");
|
|
1168
|
+
return process.exit(1);
|
|
1169
|
+
}
|
|
1118
1170
|
log.step("Installing CLI tools");
|
|
1119
|
-
|
|
1120
|
-
|
|
1171
|
+
await ensureAll(missingToolNames);
|
|
1172
|
+
}
|
|
1173
|
+
if (isMacOS()) {
|
|
1174
|
+
await ensureDockerDaemonReady();
|
|
1121
1175
|
}
|
|
1122
1176
|
// ── Repo creation phase (new mode only) ─────────────────────────────
|
|
1123
1177
|
let repoRoot;
|
|
@@ -1158,15 +1212,9 @@ export async function bootstrap() {
|
|
|
1158
1212
|
(isNewRepo(wizard) ? "main" : undefined),
|
|
1159
1213
|
};
|
|
1160
1214
|
// ── Check macOS prerequisites ────────────────────────────────────────
|
|
1161
|
-
if (isMacOS()) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
return process.exit(1);
|
|
1165
|
-
}
|
|
1166
|
-
if (!commandExists("docker")) {
|
|
1167
|
-
log.error("Docker is required on macOS (Docker Desktop, OrbStack, or Colima).");
|
|
1168
|
-
return process.exit(1);
|
|
1169
|
-
}
|
|
1215
|
+
if (isMacOS() && !commandExists("brew")) {
|
|
1216
|
+
log.error("Homebrew is required on macOS. Install from https://brew.sh");
|
|
1217
|
+
return process.exit(1);
|
|
1170
1218
|
}
|
|
1171
1219
|
// ── Run bootstrap (cluster + flux + template + sops + git push) ─────
|
|
1172
1220
|
try {
|
|
@@ -1224,8 +1272,15 @@ export async function bootstrap() {
|
|
|
1224
1272
|
summary("Bootstrap Complete", summaryEntries);
|
|
1225
1273
|
const finalSteps = [
|
|
1226
1274
|
`All HelmReleases may take ${pc.yellow("~5 minutes")} to become ready.`,
|
|
1227
|
-
`Check status: ${pc.cyan("kubectl get helmreleases -A")}`,
|
|
1228
1275
|
];
|
|
1276
|
+
if (commandExists("kubectl")) {
|
|
1277
|
+
finalSteps.push(`Check HelmRelease status: ${pc.cyan("kubectl get helmreleases -A")}`);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
finalSteps.push(`Install ${pc.bold("kubectl")} to check HelmRelease status: ${isMacOS()
|
|
1281
|
+
? pc.cyan("brew install kubectl")
|
|
1282
|
+
: pc.cyan("https://kubernetes.io/docs/tasks/tools/")}`);
|
|
1283
|
+
}
|
|
1229
1284
|
if (!commandExists("k9s")) {
|
|
1230
1285
|
finalSteps.push(`Install ${pc.bold("k9s")} for a terminal UI to monitor your cluster: ${isMacOS()
|
|
1231
1286
|
? pc.cyan("brew install derailed/k9s/k9s")
|