@cat-factory/cli 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import { type CliOptions } from './args.js';
2
+ import { type HostShell } from './host-shell.js';
3
+ import { type Io } from './io.js';
4
+ import { type HostState, type OfferId } from './k3s-probe.js';
5
+ /** The namespace + ServiceAccount + long-lived token Secret the guided setup creates. */
6
+ export declare const CAT_FACTORY_NAMESPACE = "cat-factory";
7
+ export declare const SERVICE_ACCOUNT_NAME = "cat-factory";
8
+ export declare const TOKEN_SECRET_NAME = "cat-factory-token";
9
+ /** The apiserver port k3d/kind is asked to publish (the kube default). */
10
+ export declare const DEFAULT_API_PORT = 6443;
11
+ /**
12
+ * Watchdog budget (ms) for `k3d cluster create` / `kind create cluster`. These pull node images on
13
+ * the first run and routinely take 30–90s, so they need far more than the {@link HostShell} default
14
+ * (10s) — otherwise the watchdog SIGKILLs the create mid-flight and the whole create path fails.
15
+ */
16
+ export declare const CLUSTER_CREATE_TIMEOUT_MS = 300000;
17
+ /** A single command to run through the {@link HostShell}, optionally with stdin `input`. */
18
+ export interface Command {
19
+ cmd: string;
20
+ args: string[];
21
+ input?: string;
22
+ /** Per-command watchdog override (ms). Absent ⇒ the {@link HostShell} default applies. */
23
+ timeoutMs?: number;
24
+ }
25
+ /**
26
+ * The resolved local-k3s connection produced by provisioning: the apiserver URL read from the
27
+ * kubeconfig plus the minted ServiceAccount token. Consumed by the hand-off (slice 3 builds the
28
+ * `kubernetes` handler from it). `insecureSkipTlsVerify` is always true — a local k3s/k3d/kind
29
+ * apiserver self-signs its cert.
30
+ */
31
+ export interface ResolvedConnection {
32
+ engine: 'local-k3s';
33
+ /** The provisioned/created cluster name (create paths only; absent for reuse). */
34
+ clusterName?: string;
35
+ apiServerUrl: string;
36
+ apiToken: string;
37
+ insecureSkipTlsVerify: true;
38
+ }
39
+ /** Raised when a provisioning command fails; carries an actionable message (never the token). */
40
+ export declare class ProvisionError extends Error {
41
+ }
42
+ /**
43
+ * The reduced-privilege RBAC the local-k3s environment backend needs, applied via
44
+ * `kubectl apply -f -` (idempotent). It grants the ServiceAccount:
45
+ * - cluster-wide `namespaces` create/delete (the ephemeral-env backend stands up one per PR),
46
+ * - the per-PR resource kinds the backend applies (the manifest allow-list), cluster-wide so
47
+ * they land in whichever per-PR namespace is created,
48
+ * - `pods` + `pods/proxy` (so the SAME token can also back the Kubernetes runner backend).
49
+ * It deliberately does NOT bind `cluster-admin` (see docs/initiatives/local-k3s-guided-setup.md).
50
+ * The cluster-scoped grant is required because per-PR namespaces don't exist yet when the token is
51
+ * minted. A long-lived token Secret is created (k8s >= 1.24 no longer auto-creates one) and read
52
+ * back rather than a short-lived `kubectl create token`.
53
+ *
54
+ * Credential-bearing kinds (`secrets`, `serviceaccounts`) are granted WITHOUT cluster-wide
55
+ * `list`/`watch` — that would let the token enumerate and read every Secret (and thus every other
56
+ * ServiceAccount token) in the cluster, a privilege-escalation path that would make this grant
57
+ * effectively cluster-admin on a single-node cluster. Only single-object create/get/patch/delete
58
+ * (by name) is granted — enough to deploy per-PR app config. (Residual: a `get` on a KNOWN secret
59
+ * name in any namespace is still possible; acceptable for a local dev cluster, and the reason the
60
+ * token must be kept private.)
61
+ */
62
+ export declare const RBAC_MANIFEST = "apiVersion: v1\nkind: Namespace\nmetadata:\n name: cat-factory\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: cat-factory\n namespace: cat-factory\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: cat-factory-env\nrules:\n - apiGroups: ['']\n resources: ['namespaces']\n verbs: ['create', 'get', 'list', 'watch', 'delete']\n - apiGroups: ['']\n resources: ['services', 'configmaps', 'persistentvolumeclaims', 'pods']\n verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']\n - apiGroups: ['']\n resources: ['secrets', 'serviceaccounts']\n verbs: ['create', 'get', 'patch', 'update', 'delete']\n - apiGroups: ['']\n resources: ['pods/proxy']\n verbs: ['create', 'get']\n - apiGroups: ['apps']\n resources: ['deployments', 'statefulsets', 'replicasets']\n verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']\n - apiGroups: ['batch']\n resources: ['jobs']\n verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']\n - apiGroups: ['networking.k8s.io']\n resources: ['ingresses']\n verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']\n - apiGroups: ['gateway.networking.k8s.io']\n resources: ['gateways', 'httproutes']\n verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: cat-factory-env\nroleRef:\n apiGroup: rbac.authorization.k8s.io\n kind: ClusterRole\n name: cat-factory-env\nsubjects:\n - kind: ServiceAccount\n name: cat-factory\n namespace: cat-factory\n---\napiVersion: v1\nkind: Secret\nmetadata:\n name: cat-factory-token\n namespace: cat-factory\n annotations:\n kubernetes.io/service-account.name: cat-factory\ntype: kubernetes.io/service-account-token\n";
63
+ /** `k3d cluster create <name> --api-port <port>` — publishes the apiserver on the host port. */
64
+ export declare function k3dCreateCommand(name: string, apiPort?: number): Command;
65
+ /** `kind create cluster --name <name>`. */
66
+ export declare function kindCreateCommand(name: string): Command;
67
+ /** The kubeconfig context name k3d/kind assigns to a cluster it creates. */
68
+ export declare function contextName(runtime: 'k3d' | 'kind', name: string): string;
69
+ /** `kubectl apply -f -` with the RBAC manifest on stdin (idempotent), targeting `context`. */
70
+ export declare function applyRbacCommand(context?: string): Command;
71
+ /** Read the base64 token out of the SA token Secret via jsonpath, targeting `context`. */
72
+ export declare function readTokenCommand(context?: string): Command;
73
+ /** Read the target context's apiserver URL from the kubeconfig. */
74
+ export declare function readApiServerCommand(context?: string): Command;
75
+ /** Decode the base64 token the Secret exposes; empty until the token controller populates it. */
76
+ export declare function decodeToken(base64: string): string;
77
+ /** Normalize the wildcard bind address k3d writes for the apiserver to a dialable loopback host. */
78
+ export declare function normalizeApiServerUrl(url: string): string;
79
+ /**
80
+ * Whether the (context, apiserver URL) pair looks like a LOCAL cluster. Used to refuse silently
81
+ * mutating a remote/production cluster in `--yes` mode: the `use-existing` offer fires for ANY
82
+ * reachable kubeconfig, which may point at a shared cluster. A local-looking context name OR a
83
+ * loopback/Docker-host apiserver is treated as local.
84
+ */
85
+ export declare function looksLocalCluster(context: string | undefined, apiServerUrl: string): boolean;
86
+ /** Injectable dependencies for the provisioner (mirrors the k3s-command deps). */
87
+ export interface ProvisionDeps {
88
+ shell: HostShell;
89
+ io: Io;
90
+ /** Delay between token-Secret read attempts (real setTimeout; a no-op in tests). */
91
+ sleep?: (ms: number) => Promise<void>;
92
+ }
93
+ /**
94
+ * Provision (or reuse) a local cluster for the chosen offer, create the least-privilege
95
+ * ServiceAccount + RBAC, mint a long-lived token, and read the apiserver URL — returning the
96
+ * resolved `local-k3s` connection. Every MUTATING step (cluster create, RBAC apply) is behind an
97
+ * explicit confirm unless `--yes`. Idempotent: an existing cluster/SA is reused, not duplicated
98
+ * (`kubectl apply` and `k3d/kind` reuse). `install-k3s` is NOT handled here — it is guidance-only.
99
+ */
100
+ export declare function provisionCluster(chosen: Exclude<OfferId, 'install-k3s'>, state: HostState, options: CliOptions, deps: ProvisionDeps): Promise<ResolvedConnection>;
101
+ //# sourceMappingURL=k3s-provision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k3s-provision.d.ts","sourceRoot":"","sources":["../src/k3s-provision.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAmB,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAE,KAAK,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,KAAK,EAAE,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAE7D,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,gBAAgB,CAAA;AAClD,eAAO,MAAM,oBAAoB,gBAAgB,CAAA;AACjD,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AACpD,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,OAAO,CAAA;AAEpC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,SAAU,CAAA;AAEhD,4FAA4F;AAC5F,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,WAAW,CAAA;IACnB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,qBAAqB,EAAE,IAAI,CAAA;CAC5B;AAED,iGAAiG;AACjG,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,o1DA8DzB,CAAA;AAMD,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAyB,GAAG,OAAO,CAM1F;AAED,2CAA2C;AAC3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEzE;AAYD,8FAA8F;AAC9F,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAgB1D;AAED,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElD;AAeD,oGAAoG;AACpG,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAY5F;AAMD,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,CAAA;IAChB,EAAE,EAAE,EAAE,CAAA;IACN,oFAAoF;IACpF,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACtC;AA4DD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC,EACvC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA0E7B"}
@@ -0,0 +1,315 @@
1
+ import { OPTION_DEFAULTS } from './args.js';
2
+ import {} from './host-shell.js';
3
+ import {} from './io.js';
4
+ import {} from './k3s-probe.js';
5
+ /** The namespace + ServiceAccount + long-lived token Secret the guided setup creates. */
6
+ export const CAT_FACTORY_NAMESPACE = 'cat-factory';
7
+ export const SERVICE_ACCOUNT_NAME = 'cat-factory';
8
+ export const TOKEN_SECRET_NAME = 'cat-factory-token';
9
+ /** The apiserver port k3d/kind is asked to publish (the kube default). */
10
+ export const DEFAULT_API_PORT = 6443;
11
+ /**
12
+ * Watchdog budget (ms) for `k3d cluster create` / `kind create cluster`. These pull node images on
13
+ * the first run and routinely take 30–90s, so they need far more than the {@link HostShell} default
14
+ * (10s) — otherwise the watchdog SIGKILLs the create mid-flight and the whole create path fails.
15
+ */
16
+ export const CLUSTER_CREATE_TIMEOUT_MS = 300_000;
17
+ /** Raised when a provisioning command fails; carries an actionable message (never the token). */
18
+ export class ProvisionError extends Error {
19
+ }
20
+ /**
21
+ * The reduced-privilege RBAC the local-k3s environment backend needs, applied via
22
+ * `kubectl apply -f -` (idempotent). It grants the ServiceAccount:
23
+ * - cluster-wide `namespaces` create/delete (the ephemeral-env backend stands up one per PR),
24
+ * - the per-PR resource kinds the backend applies (the manifest allow-list), cluster-wide so
25
+ * they land in whichever per-PR namespace is created,
26
+ * - `pods` + `pods/proxy` (so the SAME token can also back the Kubernetes runner backend).
27
+ * It deliberately does NOT bind `cluster-admin` (see docs/initiatives/local-k3s-guided-setup.md).
28
+ * The cluster-scoped grant is required because per-PR namespaces don't exist yet when the token is
29
+ * minted. A long-lived token Secret is created (k8s >= 1.24 no longer auto-creates one) and read
30
+ * back rather than a short-lived `kubectl create token`.
31
+ *
32
+ * Credential-bearing kinds (`secrets`, `serviceaccounts`) are granted WITHOUT cluster-wide
33
+ * `list`/`watch` — that would let the token enumerate and read every Secret (and thus every other
34
+ * ServiceAccount token) in the cluster, a privilege-escalation path that would make this grant
35
+ * effectively cluster-admin on a single-node cluster. Only single-object create/get/patch/delete
36
+ * (by name) is granted — enough to deploy per-PR app config. (Residual: a `get` on a KNOWN secret
37
+ * name in any namespace is still possible; acceptable for a local dev cluster, and the reason the
38
+ * token must be kept private.)
39
+ */
40
+ export const RBAC_MANIFEST = `apiVersion: v1
41
+ kind: Namespace
42
+ metadata:
43
+ name: ${CAT_FACTORY_NAMESPACE}
44
+ ---
45
+ apiVersion: v1
46
+ kind: ServiceAccount
47
+ metadata:
48
+ name: ${SERVICE_ACCOUNT_NAME}
49
+ namespace: ${CAT_FACTORY_NAMESPACE}
50
+ ---
51
+ apiVersion: rbac.authorization.k8s.io/v1
52
+ kind: ClusterRole
53
+ metadata:
54
+ name: cat-factory-env
55
+ rules:
56
+ - apiGroups: ['']
57
+ resources: ['namespaces']
58
+ verbs: ['create', 'get', 'list', 'watch', 'delete']
59
+ - apiGroups: ['']
60
+ resources: ['services', 'configmaps', 'persistentvolumeclaims', 'pods']
61
+ verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']
62
+ - apiGroups: ['']
63
+ resources: ['secrets', 'serviceaccounts']
64
+ verbs: ['create', 'get', 'patch', 'update', 'delete']
65
+ - apiGroups: ['']
66
+ resources: ['pods/proxy']
67
+ verbs: ['create', 'get']
68
+ - apiGroups: ['apps']
69
+ resources: ['deployments', 'statefulsets', 'replicasets']
70
+ verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']
71
+ - apiGroups: ['batch']
72
+ resources: ['jobs']
73
+ verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']
74
+ - apiGroups: ['networking.k8s.io']
75
+ resources: ['ingresses']
76
+ verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']
77
+ - apiGroups: ['gateway.networking.k8s.io']
78
+ resources: ['gateways', 'httproutes']
79
+ verbs: ['create', 'get', 'list', 'watch', 'patch', 'update', 'delete']
80
+ ---
81
+ apiVersion: rbac.authorization.k8s.io/v1
82
+ kind: ClusterRoleBinding
83
+ metadata:
84
+ name: cat-factory-env
85
+ roleRef:
86
+ apiGroup: rbac.authorization.k8s.io
87
+ kind: ClusterRole
88
+ name: cat-factory-env
89
+ subjects:
90
+ - kind: ServiceAccount
91
+ name: ${SERVICE_ACCOUNT_NAME}
92
+ namespace: ${CAT_FACTORY_NAMESPACE}
93
+ ---
94
+ apiVersion: v1
95
+ kind: Secret
96
+ metadata:
97
+ name: ${TOKEN_SECRET_NAME}
98
+ namespace: ${CAT_FACTORY_NAMESPACE}
99
+ annotations:
100
+ kubernetes.io/service-account.name: ${SERVICE_ACCOUNT_NAME}
101
+ type: kubernetes.io/service-account-token
102
+ `;
103
+ // ---------------------------------------------------------------------------
104
+ // Pure command planners — no shell-out, so they are unit-testable in isolation.
105
+ // ---------------------------------------------------------------------------
106
+ /** `k3d cluster create <name> --api-port <port>` — publishes the apiserver on the host port. */
107
+ export function k3dCreateCommand(name, apiPort = DEFAULT_API_PORT) {
108
+ return {
109
+ cmd: 'k3d',
110
+ args: ['cluster', 'create', name, '--api-port', String(apiPort)],
111
+ timeoutMs: CLUSTER_CREATE_TIMEOUT_MS,
112
+ };
113
+ }
114
+ /** `kind create cluster --name <name>`. */
115
+ export function kindCreateCommand(name) {
116
+ return {
117
+ cmd: 'kind',
118
+ args: ['create', 'cluster', '--name', name],
119
+ timeoutMs: CLUSTER_CREATE_TIMEOUT_MS,
120
+ };
121
+ }
122
+ /** The kubeconfig context name k3d/kind assigns to a cluster it creates. */
123
+ export function contextName(runtime, name) {
124
+ return `${runtime}-${name}`;
125
+ }
126
+ /**
127
+ * Append an explicit `--context <ctx>` when one is supplied, so a command targets a SPECIFIC
128
+ * kubeconfig context instead of mutating (and leaving mutated) the global current-context. Absent
129
+ * ⇒ the command operates on whatever context is already current (used for the reuse path, where
130
+ * the current context IS the target).
131
+ */
132
+ function withContext(args, context) {
133
+ return context ? [...args, '--context', context] : args;
134
+ }
135
+ /** `kubectl apply -f -` with the RBAC manifest on stdin (idempotent), targeting `context`. */
136
+ export function applyRbacCommand(context) {
137
+ return { cmd: 'kubectl', args: withContext(['apply', '-f', '-'], context), input: RBAC_MANIFEST };
138
+ }
139
+ /** Read the base64 token out of the SA token Secret via jsonpath, targeting `context`. */
140
+ export function readTokenCommand(context) {
141
+ return {
142
+ cmd: 'kubectl',
143
+ args: withContext([
144
+ '-n',
145
+ CAT_FACTORY_NAMESPACE,
146
+ 'get',
147
+ 'secret',
148
+ TOKEN_SECRET_NAME,
149
+ '-o',
150
+ 'jsonpath={.data.token}',
151
+ ], context),
152
+ };
153
+ }
154
+ /** Read the target context's apiserver URL from the kubeconfig. */
155
+ export function readApiServerCommand(context) {
156
+ return {
157
+ cmd: 'kubectl',
158
+ args: withContext(['config', 'view', '--minify', '-o', 'jsonpath={.clusters[0].cluster.server}'], context),
159
+ };
160
+ }
161
+ /** Decode the base64 token the Secret exposes; empty until the token controller populates it. */
162
+ export function decodeToken(base64) {
163
+ return Buffer.from(base64.trim(), 'base64').toString('utf8').trim();
164
+ }
165
+ /** kubeconfig context names that unambiguously denote a LOCAL cluster. */
166
+ const LOCAL_CONTEXT_PREFIXES = ['k3d-', 'kind-'];
167
+ const LOCAL_CONTEXT_NAMES = ['minikube', 'docker-desktop', 'orbstack', 'colima', 'rancher-desktop'];
168
+ /** Hostnames that denote a LOCAL apiserver (loopback / the Docker host alias). */
169
+ const LOCAL_API_HOSTS = [
170
+ 'localhost',
171
+ '127.0.0.1',
172
+ '0.0.0.0',
173
+ '::1',
174
+ '[::1]',
175
+ 'host.docker.internal',
176
+ ];
177
+ /** Normalize the wildcard bind address k3d writes for the apiserver to a dialable loopback host. */
178
+ export function normalizeApiServerUrl(url) {
179
+ return url.replace('//0.0.0.0:', '//127.0.0.1:');
180
+ }
181
+ /**
182
+ * Whether the (context, apiserver URL) pair looks like a LOCAL cluster. Used to refuse silently
183
+ * mutating a remote/production cluster in `--yes` mode: the `use-existing` offer fires for ANY
184
+ * reachable kubeconfig, which may point at a shared cluster. A local-looking context name OR a
185
+ * loopback/Docker-host apiserver is treated as local.
186
+ */
187
+ export function looksLocalCluster(context, apiServerUrl) {
188
+ const ctx = (context ?? '').toLowerCase();
189
+ if (LOCAL_CONTEXT_PREFIXES.some((p) => ctx.startsWith(p)))
190
+ return true;
191
+ if (LOCAL_CONTEXT_NAMES.includes(ctx))
192
+ return true;
193
+ let host = '';
194
+ try {
195
+ host = new URL(apiServerUrl).hostname.toLowerCase();
196
+ }
197
+ catch {
198
+ host = '';
199
+ }
200
+ if (LOCAL_API_HOSTS.includes(host))
201
+ return true;
202
+ return host.endsWith('.local') || host.endsWith('.localhost') || host.endsWith('.nip.io');
203
+ }
204
+ const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
205
+ /** Run a command, throwing a {@link ProvisionError} with the captured stderr on a non-zero exit. */
206
+ async function runOrThrow(shell, command, what, hint) {
207
+ const result = await shell.run(command.cmd, command.args, {
208
+ input: command.input,
209
+ timeoutMs: command.timeoutMs,
210
+ });
211
+ if (result.code !== 0) {
212
+ const detail = (result.stderr || result.stdout).trim();
213
+ const base = detail || `${command.cmd} exited ${result.code}`;
214
+ throw new ProvisionError(`${what} failed: ${base}${hint ? hint(base) : ''}`);
215
+ }
216
+ return result;
217
+ }
218
+ /** An extra hint appended to a create failure that looks like an apiserver-port collision. */
219
+ function portCollisionHint(detail) {
220
+ return /already (allocated|in use)|address already in use|port is already/i.test(detail)
221
+ ? ` (the apiserver port ${DEFAULT_API_PORT} may already be in use — free it, or remove the conflicting cluster, then re-run)`
222
+ : '';
223
+ }
224
+ /** Ask to run a mutating step; `--yes` proceeds without prompting. Declining throws. */
225
+ async function confirmStep(io, options, prompt) {
226
+ if (options.yes)
227
+ return;
228
+ const ok = await io.confirm(prompt, true);
229
+ if (!ok)
230
+ throw new ProvisionError('Cancelled — nothing further was changed on your host.');
231
+ }
232
+ /**
233
+ * Read the ServiceAccount token from its Secret, retrying while the token controller populates it.
234
+ * A freshly-applied `kubernetes.io/service-account-token` Secret has an empty `.data.token` for a
235
+ * moment; poll until it's non-empty (or give up with an actionable error).
236
+ */
237
+ async function readSaToken(deps, context) {
238
+ const sleep = deps.sleep ?? realSleep;
239
+ const attempts = 20;
240
+ for (let i = 0; i < attempts; i++) {
241
+ const result = await runOrThrow(deps.shell, readTokenCommand(context), 'Reading the ServiceAccount token');
242
+ const token = decodeToken(result.stdout);
243
+ if (token.length > 0)
244
+ return token;
245
+ if (i < attempts - 1)
246
+ await sleep(500);
247
+ }
248
+ throw new ProvisionError(`The ServiceAccount token Secret "${TOKEN_SECRET_NAME}" never populated. Re-run \`cat-factory k3s\` once the cluster is settled.`);
249
+ }
250
+ /**
251
+ * Provision (or reuse) a local cluster for the chosen offer, create the least-privilege
252
+ * ServiceAccount + RBAC, mint a long-lived token, and read the apiserver URL — returning the
253
+ * resolved `local-k3s` connection. Every MUTATING step (cluster create, RBAC apply) is behind an
254
+ * explicit confirm unless `--yes`. Idempotent: an existing cluster/SA is reused, not duplicated
255
+ * (`kubectl apply` and `k3d/kind` reuse). `install-k3s` is NOT handled here — it is guidance-only.
256
+ */
257
+ export async function provisionCluster(chosen, state, options, deps) {
258
+ const { io, shell } = deps;
259
+ // The kubeconfig context every subsequent command targets. Create paths get an explicit
260
+ // `--context` (so we never mutate the user's global current-context); reuse operates on the
261
+ // already-current context (`undefined`).
262
+ let targetContext;
263
+ let createdName;
264
+ if (chosen === 'create-k3d' || chosen === 'create-kind') {
265
+ const clusterName = options.clusterName ?? OPTION_DEFAULTS.k3sClusterName;
266
+ const runtime = chosen === 'create-kind' ? 'kind' : 'k3d';
267
+ const existing = runtime === 'kind' ? state.detections.kindClusters : state.detections.k3dClusters;
268
+ if (existing.includes(clusterName)) {
269
+ io.info(`Reusing the existing ${runtime} cluster "${clusterName}".`);
270
+ }
271
+ else {
272
+ await confirmStep(io, options, `Create a local ${runtime} cluster "${clusterName}"?`);
273
+ io.info(`Creating the ${runtime} cluster "${clusterName}" (this can take a minute)…`);
274
+ const create = runtime === 'kind' ? kindCreateCommand(clusterName) : k3dCreateCommand(clusterName);
275
+ await runOrThrow(shell, create, `Creating the ${runtime} cluster`, portCollisionHint);
276
+ }
277
+ targetContext = contextName(runtime, clusterName);
278
+ createdName = clusterName;
279
+ }
280
+ else {
281
+ // use-existing: operate on the current context (its name is what the probe detected).
282
+ targetContext = undefined;
283
+ }
284
+ // Read the apiserver URL first (a read-only op): it names the target in the confirm below and
285
+ // gates the reuse path against accidentally mutating a non-local cluster.
286
+ const apiServer = await runOrThrow(shell, readApiServerCommand(targetContext), 'Reading the apiserver URL');
287
+ const apiServerUrl = normalizeApiServerUrl(apiServer.stdout.trim());
288
+ if (apiServerUrl.length === 0) {
289
+ throw new ProvisionError('Could not read the apiserver URL from your kubeconfig.');
290
+ }
291
+ const contextLabel = state.detections.clusterContext;
292
+ const targetDescription = createdName
293
+ ? `the ${chosen === 'create-kind' ? 'kind' : 'k3d'} cluster "${createdName}" (${apiServerUrl})`
294
+ : contextLabel
295
+ ? `context "${contextLabel}" (${apiServerUrl})`
296
+ : `the current cluster (${apiServerUrl})`;
297
+ // Refuse to silently mutate a cluster that doesn't look local when running non-interactively —
298
+ // `use-existing` fires for ANY reachable kubeconfig, which could be a shared/remote cluster.
299
+ if (chosen === 'use-existing' && options.yes && !looksLocalCluster(contextLabel, apiServerUrl)) {
300
+ throw new ProvisionError(`Refusing to auto-provision in --yes mode: ${targetDescription} does not look like a local cluster. Re-run without --yes to confirm explicitly, or point kubeconfig at a local k3d/kind/k3s cluster.`);
301
+ }
302
+ await confirmStep(io, options, `Apply the cat-factory ServiceAccount + RBAC to ${targetDescription}?`);
303
+ io.info('Applying the ServiceAccount + RBAC…');
304
+ await runOrThrow(shell, applyRbacCommand(targetContext), 'Applying the RBAC manifest');
305
+ io.info('Minting the ServiceAccount token…');
306
+ const apiToken = await readSaToken(deps, targetContext);
307
+ return {
308
+ engine: 'local-k3s',
309
+ clusterName: createdName,
310
+ apiServerUrl,
311
+ apiToken,
312
+ insecureSkipTlsVerify: true,
313
+ };
314
+ }
315
+ //# sourceMappingURL=k3s-provision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k3s-provision.js","sourceRoot":"","sources":["../src/k3s-provision.ts"],"names":[],"mappings":"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"}
package/dist/k3s.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { type CliOptions } from './args.js';
2
+ import { type HostShell } from './host-shell.js';
3
+ import { type Io } from './io.js';
4
+ import { type HostState, type OfferId } from './k3s-probe.js';
5
+ import { type ResolvedConnection } from './k3s-provision.js';
6
+ /** The k3s command's injectable dependencies (defaults to the real console IO + host shell). */
7
+ export interface K3sDeps {
8
+ io?: Io;
9
+ shell?: HostShell;
10
+ }
11
+ /** What {@link setupK3s} resolves to (returned for tests + programmatic callers). */
12
+ export interface K3sResult {
13
+ state: HostState;
14
+ chosen: OfferId;
15
+ /** The resolved connection when a provisioning path ran + succeeded (not for `install-k3s`). */
16
+ connection?: ResolvedConnection;
17
+ }
18
+ /** The one-liner k3s install command (needs sudo — printed, never run for the user). */
19
+ export declare const K3S_INSTALL_COMMAND = "curl -sfL https://get.k3s.io | sh -";
20
+ /**
21
+ * `cat-factory k3s` — guided local-cluster setup: probe → offer → provision → hand-off.
22
+ *
23
+ * Probes the host over the {@link HostShell} seam, reports what was found, lets the user pick a
24
+ * setup path, then (for the k3d/kind/existing-cluster paths) provisions the cluster + a
25
+ * least-privilege ServiceAccount and prints the resolved connection values to wire into the
26
+ * Settings → Infrastructure → Local k3s form. `install-k3s` needs sudo, so it is guidance-only
27
+ * (the command is printed, never run). `@clack/prompts` is reached only through {@link Io}.
28
+ */
29
+ export declare function setupK3s(options: CliOptions, deps?: K3sDeps): Promise<K3sResult>;
30
+ //# sourceMappingURL=k3s.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k3s.d.ts","sourceRoot":"","sources":["../src/k3s.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAmB,MAAM,WAAW,CAAA;AAC5D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACjE,OAAO,EAAmB,KAAK,EAAE,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,KAAK,SAAS,EAAc,KAAK,OAAO,EAAa,MAAM,gBAAgB,CAAA;AACpF,OAAO,EAIL,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAA;AAE3B,gGAAgG;AAChG,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,EAAE,EAAE,CAAA;IACP,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAED,qFAAqF;AACrF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;IACf,gGAAgG;IAChG,UAAU,CAAC,EAAE,kBAAkB,CAAA;CAChC;AAED,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,wCAAwC,CAAA;AAExE;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,GAAE,OAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAkC1F"}
package/dist/k3s.js ADDED
@@ -0,0 +1,140 @@
1
+ import { OPTION_DEFAULTS } from './args.js';
2
+ import { createNodeShell } from './host-shell.js';
3
+ import { createConsoleIo } from './io.js';
4
+ import { probeHost } from './k3s-probe.js';
5
+ import { CAT_FACTORY_NAMESPACE, provisionCluster, ProvisionError, } from './k3s-provision.js';
6
+ /** The one-liner k3s install command (needs sudo — printed, never run for the user). */
7
+ export const K3S_INSTALL_COMMAND = 'curl -sfL https://get.k3s.io | sh -';
8
+ /**
9
+ * `cat-factory k3s` — guided local-cluster setup: probe → offer → provision → hand-off.
10
+ *
11
+ * Probes the host over the {@link HostShell} seam, reports what was found, lets the user pick a
12
+ * setup path, then (for the k3d/kind/existing-cluster paths) provisions the cluster + a
13
+ * least-privilege ServiceAccount and prints the resolved connection values to wire into the
14
+ * Settings → Infrastructure → Local k3s form. `install-k3s` needs sudo, so it is guidance-only
15
+ * (the command is printed, never run). `@clack/prompts` is reached only through {@link Io}.
16
+ */
17
+ export async function setupK3s(options, deps = {}) {
18
+ const io = deps.io ?? createConsoleIo();
19
+ const shell = deps.shell ?? createNodeShell();
20
+ const preferred = options.k3sRuntime ?? OPTION_DEFAULTS.k3sRuntime;
21
+ io.info('\ncat-factory — guided local k3s / k3d setup\n');
22
+ io.info('Probing your machine for a usable Kubernetes cluster…');
23
+ const state = await probeHost(shell, preferred);
24
+ io.info(renderReport(state));
25
+ const chosen = await chooseOffer(state, options, io);
26
+ // The k3s install needs sudo — we only ever print the command, never provision on the user's behalf.
27
+ if (chosen === 'install-k3s') {
28
+ printInstallGuidance(state, io);
29
+ return { state, chosen };
30
+ }
31
+ let connection;
32
+ try {
33
+ connection = await provisionCluster(chosen, state, options, { io, shell });
34
+ }
35
+ catch (err) {
36
+ // A declined confirm or a failed command is an expected, non-fatal outcome — report and stop.
37
+ if (err instanceof ProvisionError) {
38
+ io.warn(err.message);
39
+ return { state, chosen };
40
+ }
41
+ throw err;
42
+ }
43
+ printConnectionSummary(connection, io);
44
+ return { state, chosen, connection };
45
+ }
46
+ /** Render the human-readable findings report from the classified host state. */
47
+ function renderReport(state) {
48
+ const d = state.detections;
49
+ const tool = (name, t) => ` ${t.installed ? '✓' : '·'} ${name}${t.installed && t.version ? ` (${t.version})` : t.installed ? '' : ' — not found'}`;
50
+ const lines = [
51
+ '',
52
+ 'Detected:',
53
+ tool('kubectl', d.kubectl),
54
+ tool('k3d', d.k3d),
55
+ tool('kind', d.kind),
56
+ tool('k3s', d.k3s),
57
+ ` ${d.docker.running ? '✓' : '·'} docker${d.docker.running
58
+ ? ' (running)'
59
+ : d.docker.installed
60
+ ? ' — installed but not running'
61
+ : ' — not found'}`,
62
+ d.reachableCluster
63
+ ? ` ✓ reachable cluster${d.clusterContext ? ` (context: ${d.clusterContext})` : ''}`
64
+ : ' · no reachable cluster via your kubeconfig',
65
+ ];
66
+ if (d.k3dClusters.length > 0)
67
+ lines.push(` • existing k3d clusters: ${d.k3dClusters.join(', ')}`);
68
+ if (d.kindClusters.length > 0)
69
+ lines.push(` • existing kind clusters: ${d.kindClusters.join(', ')}`);
70
+ return lines.join('\n');
71
+ }
72
+ /** Pick an offer: `--yes` takes the recommendation; otherwise prompt over the available offers. */
73
+ async function chooseOffer(state, options, io) {
74
+ const available = state.offers.filter((o) => o.available);
75
+ // `install-k3s` is always available, so `available` is never empty.
76
+ if (options.yes || available.length === 1)
77
+ return state.recommended;
78
+ // Surface why an unavailable path is off, so the choice is informed.
79
+ for (const o of state.offers) {
80
+ if (!o.available && o.reason)
81
+ io.info(` (unavailable: ${o.label} — ${o.reason})`);
82
+ }
83
+ return io.select('How would you like to set up the cluster?', available.map((o) => ({ value: o.id, label: offerLabel(o) })), state.recommended);
84
+ }
85
+ /** Label an offer for the menu, tagging the recommended one. */
86
+ function offerLabel(o) {
87
+ return o.recommended ? `${o.label} (recommended)` : o.label;
88
+ }
89
+ /**
90
+ * Print the k3s install guidance. The install needs sudo, so the command is only ever PRINTED —
91
+ * cat-factory never runs it for the user.
92
+ */
93
+ function printInstallGuidance(state, io) {
94
+ // Don't tell a user who already has k3s to re-install it — point them at starting the service.
95
+ io.info(state.detections.k3s.installed
96
+ ? [
97
+ '',
98
+ 'k3s is already installed. Start it (needs sudo), e.g.:',
99
+ '',
100
+ ' sudo systemctl start k3s # or: sudo k3s server',
101
+ '',
102
+ 'Then re-run `cat-factory k3s` — it will detect the running cluster and provision the handler.',
103
+ ].join('\n')
104
+ : [
105
+ '',
106
+ 'Install k3s (single-node) — run this yourself (needs sudo):',
107
+ '',
108
+ ` ${K3S_INSTALL_COMMAND}`,
109
+ '',
110
+ 'Then re-run `cat-factory k3s` — it will detect the new cluster and provision the handler.',
111
+ ].join('\n'));
112
+ }
113
+ /**
114
+ * Print the resolved connection so the user can wire it into the Settings → Infrastructure →
115
+ * Local k3s form (Test → Save). The apiserver token is shown ONCE here (the user's own local
116
+ * cluster credential, to paste) — it is never written to disk or a log by cat-factory. Slice 3
117
+ * replaces this with a deep-linked, pre-filled form.
118
+ */
119
+ function printConnectionSummary(connection, io) {
120
+ io.info([
121
+ '',
122
+ connection.clusterName
123
+ ? `Cluster "${connection.clusterName}" is ready and wired for cat-factory.`
124
+ : 'The existing cluster is wired for cat-factory.',
125
+ '',
126
+ 'Open Settings → Infrastructure → Kubernetes → Local k3s and enter:',
127
+ ` • API server URL: ${connection.apiServerUrl}`,
128
+ ` • Skip TLS verification: yes (local self-signed cert)`,
129
+ ` • Namespace template: cf-env-{{pullNumber}}`,
130
+ ` • Ingress host template: {{branch}}.127.0.0.1.nip.io`,
131
+ ` • ServiceAccount: ${CAT_FACTORY_NAMESPACE}/${CAT_FACTORY_NAMESPACE}`,
132
+ '',
133
+ 'Then paste this ServiceAccount token into the "API token" field and click Test → Save:',
134
+ '',
135
+ ` ${connection.apiToken}`,
136
+ '',
137
+ 'Keep the token private — it grants access to your local cluster.',
138
+ ].join('\n'));
139
+ }
140
+ //# sourceMappingURL=k3s.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"k3s.js","sourceRoot":"","sources":["../src/k3s.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  export declare const LOCAL_SERVER_VERSION = "^0.33.0";
2
- export declare const APP_VERSION = "^0.63.1";
2
+ export declare const APP_VERSION = "^0.64.0";
3
3
  export declare const NUXT_VERSION = "^4.4.8";
4
4
  export declare const TYPES_NODE_VERSION = "^26.0.1";
5
5
  export declare const TYPESCRIPT_VERSION = "7.0.1-rc";
package/dist/templates.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // workspace version. Bump these when cutting a CLI release against newer libraries. (The two
11
11
  // libraries version independently.)
12
12
  export const LOCAL_SERVER_VERSION = '^0.33.0';
13
- export const APP_VERSION = '^0.63.1';
13
+ export const APP_VERSION = '^0.64.0';
14
14
  export const NUXT_VERSION = '^4.4.8';
15
15
  export const TYPES_NODE_VERSION = '^26.0.1';
16
16
  export const TYPESCRIPT_VERSION = '7.0.1-rc';