@intentius/chant-lexicon-k8s 0.30.0 → 0.32.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.
Files changed (137) hide show
  1. package/dist/api/classify.d.ts +65 -0
  2. package/dist/api/classify.d.ts.map +1 -0
  3. package/dist/api/connect.d.ts +58 -0
  4. package/dist/api/connect.d.ts.map +1 -0
  5. package/dist/api/fake-cluster.d.ts +55 -0
  6. package/dist/api/fake-cluster.d.ts.map +1 -0
  7. package/dist/api/operation-surface.d.ts +64 -0
  8. package/dist/api/operation-surface.d.ts.map +1 -0
  9. package/dist/api/owner-chain.d.ts +46 -0
  10. package/dist/api/owner-chain.d.ts.map +1 -0
  11. package/dist/api/sweep-types.d.ts +17 -0
  12. package/dist/api/sweep-types.d.ts.map +1 -0
  13. package/dist/codegen/generate-operations.d.ts +29 -0
  14. package/dist/codegen/generate-operations.d.ts.map +1 -0
  15. package/dist/codegen/generate.d.ts.map +1 -1
  16. package/dist/config.d.ts +17 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/crd/parser.d.ts.map +1 -1
  19. package/dist/crd/types.d.ts +7 -0
  20. package/dist/crd/types.d.ts.map +1 -1
  21. package/dist/deep-observe-hooks.d.ts +86 -0
  22. package/dist/deep-observe-hooks.d.ts.map +1 -0
  23. package/dist/deep-observe.d.ts +146 -0
  24. package/dist/deep-observe.d.ts.map +1 -0
  25. package/dist/describe-resources.d.ts +58 -26
  26. package/dist/describe-resources.d.ts.map +1 -1
  27. package/dist/export-resources.d.ts +31 -1
  28. package/dist/export-resources.d.ts.map +1 -1
  29. package/dist/integrity.json +2 -2
  30. package/dist/kube/apply.d.ts +26 -0
  31. package/dist/kube/apply.d.ts.map +1 -0
  32. package/dist/kube/connect.d.ts +30 -0
  33. package/dist/kube/connect.d.ts.map +1 -0
  34. package/dist/kube/delete.d.ts +27 -0
  35. package/dist/kube/delete.d.ts.map +1 -0
  36. package/dist/kube/describe.d.ts +19 -0
  37. package/dist/kube/describe.d.ts.map +1 -0
  38. package/dist/kube/events.d.ts +34 -0
  39. package/dist/kube/events.d.ts.map +1 -0
  40. package/dist/kube/flags.d.ts +40 -0
  41. package/dist/kube/flags.d.ts.map +1 -0
  42. package/dist/kube/get.d.ts +21 -0
  43. package/dist/kube/get.d.ts.map +1 -0
  44. package/dist/kube/group.d.ts +19 -0
  45. package/dist/kube/group.d.ts.map +1 -0
  46. package/dist/kube/logs.d.ts +16 -0
  47. package/dist/kube/logs.d.ts.map +1 -0
  48. package/dist/kube/project.d.ts +59 -0
  49. package/dist/kube/project.d.ts.map +1 -0
  50. package/dist/kube/render.d.ts +75 -0
  51. package/dist/kube/render.d.ts.map +1 -0
  52. package/dist/kube/source.d.ts +19 -0
  53. package/dist/kube/source.d.ts.map +1 -0
  54. package/dist/kube/target.d.ts +52 -0
  55. package/dist/kube/target.d.ts.map +1 -0
  56. package/dist/kube/testing.d.ts +21 -0
  57. package/dist/kube/testing.d.ts.map +1 -0
  58. package/dist/kube/top.d.ts +23 -0
  59. package/dist/kube/top.d.ts.map +1 -0
  60. package/dist/kube/verdict.d.ts +45 -0
  61. package/dist/kube/verdict.d.ts.map +1 -0
  62. package/dist/kube/wait.d.ts +23 -0
  63. package/dist/kube/wait.d.ts.map +1 -0
  64. package/dist/manifest.json +1 -1
  65. package/dist/op/activities/index.d.ts +7 -4
  66. package/dist/op/activities/index.d.ts.map +1 -1
  67. package/dist/op/activities/kubectl.d.ts +120 -2
  68. package/dist/op/activities/kubectl.d.ts.map +1 -1
  69. package/dist/op/activities/wait-for-ready.d.ts +30 -3
  70. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  71. package/dist/plugin.d.ts.map +1 -1
  72. package/dist/spec/parse.d.ts +42 -0
  73. package/dist/spec/parse.d.ts.map +1 -1
  74. package/package.json +5 -2
  75. package/src/api/classify.test.ts +133 -0
  76. package/src/api/classify.ts +144 -0
  77. package/src/api/connect.ts +104 -0
  78. package/src/api/fake-cluster.ts +218 -0
  79. package/src/api/operation-surface.test.ts +116 -0
  80. package/src/api/operation-surface.ts +129 -0
  81. package/src/api/owner-chain.test.ts +171 -0
  82. package/src/api/owner-chain.ts +144 -0
  83. package/src/api/sweep-types.ts +36 -0
  84. package/src/codegen/generate-operations.ts +56 -0
  85. package/src/codegen/generate.ts +9 -0
  86. package/src/config.ts +17 -0
  87. package/src/crd/parser.ts +8 -0
  88. package/src/crd/types.ts +7 -0
  89. package/src/deep-observe-hooks.ts +125 -0
  90. package/src/deep-observe.test.ts +668 -0
  91. package/src/deep-observe.ts +299 -0
  92. package/src/describe-resources.test.ts +619 -191
  93. package/src/describe-resources.ts +218 -117
  94. package/src/export-resources-io.test.ts +76 -51
  95. package/src/export-resources.ts +47 -35
  96. package/src/generated/operations.json +2156 -0
  97. package/src/kube/apply.test.ts +105 -0
  98. package/src/kube/apply.ts +101 -0
  99. package/src/kube/connect.ts +54 -0
  100. package/src/kube/delete.test.ts +123 -0
  101. package/src/kube/delete.ts +174 -0
  102. package/src/kube/describe.test.ts +106 -0
  103. package/src/kube/describe.ts +195 -0
  104. package/src/kube/events.test.ts +115 -0
  105. package/src/kube/events.ts +160 -0
  106. package/src/kube/flags.test.ts +65 -0
  107. package/src/kube/flags.ts +114 -0
  108. package/src/kube/get.test.ts +195 -0
  109. package/src/kube/get.ts +190 -0
  110. package/src/kube/group.test.ts +33 -0
  111. package/src/kube/group.ts +125 -0
  112. package/src/kube/logs.test.ts +126 -0
  113. package/src/kube/logs.ts +96 -0
  114. package/src/kube/project.test.ts +57 -0
  115. package/src/kube/project.ts +106 -0
  116. package/src/kube/render.test.ts +142 -0
  117. package/src/kube/render.ts +252 -0
  118. package/src/kube/source.test.ts +138 -0
  119. package/src/kube/source.ts +139 -0
  120. package/src/kube/target.test.ts +67 -0
  121. package/src/kube/target.ts +89 -0
  122. package/src/kube/testing.ts +42 -0
  123. package/src/kube/top.test.ts +149 -0
  124. package/src/kube/top.ts +120 -0
  125. package/src/kube/verdict.test.ts +48 -0
  126. package/src/kube/verdict.ts +77 -0
  127. package/src/kube/wait.test.ts +125 -0
  128. package/src/kube/wait.ts +160 -0
  129. package/src/lifecycle-integration.test.ts +132 -92
  130. package/src/op/activities/index.ts +7 -3
  131. package/src/op/activities/kubectl.test.ts +420 -0
  132. package/src/op/activities/kubectl.ts +379 -13
  133. package/src/op/activities/wait-for-ready.test.ts +94 -0
  134. package/src/op/activities/wait-for-ready.ts +66 -15
  135. package/src/plugin.test.ts +58 -1
  136. package/src/plugin.ts +29 -0
  137. package/src/spec/parse.ts +93 -1
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `chant kube describe` (chant #1079) — a generic, multi-section human view:
3
+ * identity, chant's own verdict + provenance (when inside a project), a
4
+ * pretty-printed spec/status, and the object's Events (reusing `./events.ts`'s
5
+ * `listEvents`/`filterInvolved`, the same machinery `chant kube events`
6
+ * itself uses — one implementation, not two). This is not kubectl's
7
+ * per-kind template set (`OldReplicaSets`/`NewReplicaSet` for a Deployment,
8
+ * per-container detail for a Pod, and so on for every built-in kind) — a
9
+ * generic describe covering the fields every object carries, plus the
10
+ * context only chant can add.
11
+ */
12
+
13
+ import type { K8sObject } from "@intentius/chant-k8s-client";
14
+ import { formatUnobserved } from "@intentius/chant/observation";
15
+ import { defaultK8sConnector, type K8sConnector } from "../api/connect";
16
+ import { classifyApiFailure } from "../api/classify";
17
+ import { statusFromObject } from "../describe-resources";
18
+ import { kubeConnect } from "./connect";
19
+ import { parseKubeFlags, connectOptionsFrom } from "./flags";
20
+ import { loadKubeProjectContext, relativeProvenance, type KubeProjectContext } from "./project";
21
+ import { resolveKubeTarget, matchLiveObject, isTargetError, type KubeTarget } from "./target";
22
+ import { verdictFor } from "./verdict";
23
+ import { relativeAge } from "./render";
24
+ import { listEvents, filterInvolved, renderEvents } from "./events";
25
+
26
+ export interface DescribeDeps {
27
+ connect?: K8sConnector;
28
+ loadProject?: (cwd?: string) => Promise<KubeProjectContext | undefined>;
29
+ }
30
+
31
+ function targetLabel(target: KubeTarget): string {
32
+ return "apiVersion" in target.selector ? target.selector.kind : target.selector.resource;
33
+ }
34
+
35
+ function keyValueBlock(m: Record<string, string> | undefined): string {
36
+ if (!m || Object.keys(m).length === 0) return "<none>";
37
+ const entries = Object.entries(m);
38
+ return entries.map(([k, v], i) => (i === 0 ? `${k}=${v}` : ` ${k}=${v}`)).join("\n");
39
+ }
40
+
41
+ function indentedJson(value: unknown): string {
42
+ return JSON.stringify(value, null, 2)
43
+ .split("\n")
44
+ .map((l) => ` ${l}`)
45
+ .join("\n");
46
+ }
47
+
48
+ async function renderBlock(
49
+ client: Parameters<typeof listEvents>[0],
50
+ obj: K8sObject,
51
+ info: { apiVersion: string; kind: string },
52
+ project: KubeProjectContext | undefined,
53
+ ): Promise<string> {
54
+ const lines: string[] = [];
55
+ lines.push(`Name: ${obj.metadata?.name ?? ""}`);
56
+ if (obj.metadata?.namespace) lines.push(`Namespace: ${obj.metadata.namespace}`);
57
+ lines.push(`API Version: ${obj.apiVersion ?? info.apiVersion}`);
58
+ lines.push(`Kind: ${obj.kind ?? info.kind}`);
59
+ lines.push(`Labels: ${keyValueBlock(obj.metadata?.labels)}`);
60
+ lines.push(`Annotations: ${keyValueBlock(obj.metadata?.annotations)}`);
61
+ lines.push(`Created: ${obj.metadata?.creationTimestamp ?? "<unknown>"} (${relativeAge(obj.metadata?.creationTimestamp)} ago)`);
62
+ lines.push(`Status: ${statusFromObject(obj)}`);
63
+
64
+ lines.push("Chant:");
65
+ if (!project) {
66
+ lines.push(" Verdict: unavailable (not running inside a chant project)");
67
+ } else {
68
+ const match = matchLiveObject(project, {
69
+ apiVersion: obj.apiVersion ?? info.apiVersion,
70
+ kind: obj.kind ?? info.kind,
71
+ name: obj.metadata?.name ?? "",
72
+ namespace: obj.metadata?.namespace,
73
+ });
74
+ lines.push(` Verdict: ${verdictFor(obj, match)}`);
75
+ if (match) {
76
+ const prov = relativeProvenance(project, match.entity);
77
+ lines.push(` Entity: ${match.entityName}`);
78
+ lines.push(` Source: ${prov?.sourceFile ?? "(unknown)"}${prov?.composite ? ` (via ${prov.composite})` : ""}`);
79
+ }
80
+ }
81
+
82
+ if (obj.spec !== undefined) {
83
+ lines.push("Spec:");
84
+ lines.push(indentedJson(obj.spec));
85
+ }
86
+ if (obj.status !== undefined) {
87
+ lines.push("Status:");
88
+ lines.push(indentedJson(obj.status));
89
+ }
90
+
91
+ try {
92
+ const events = await listEvents(client, { namespace: obj.metadata?.namespace });
93
+ const involved = filterInvolved(events, { kind: obj.kind ?? info.kind, name: obj.metadata?.name ?? "" });
94
+ lines.push("Events:");
95
+ lines.push(renderEvents(involved));
96
+ } catch {
97
+ lines.push("Events: <could not be read>");
98
+ }
99
+
100
+ return lines.join("\n");
101
+ }
102
+
103
+ export async function runDescribe(rawArgs: string[], deps: DescribeDeps = {}): Promise<number> {
104
+ const connect = deps.connect ?? defaultK8sConnector;
105
+ const loadProject = deps.loadProject ?? loadKubeProjectContext;
106
+
107
+ let flags;
108
+ try {
109
+ flags = parseKubeFlags(rawArgs);
110
+ } catch (err) {
111
+ console.error(err instanceof Error ? err.message : String(err));
112
+ return 1;
113
+ }
114
+
115
+ const project = await loadProject();
116
+ const target = resolveKubeTarget(flags.positional, flags.values.namespace, project);
117
+ if (isTargetError(target)) {
118
+ console.error(`error: ${target.error}`);
119
+ return 1;
120
+ }
121
+
122
+ const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
123
+ if (connected.kind === "unobserved") {
124
+ console.error(formatUnobserved(targetLabel(target), { reason: connected.reason, detail: connected.detail }));
125
+ return 1;
126
+ }
127
+ const { client } = connected;
128
+
129
+ let info;
130
+ try {
131
+ info = await client.resolve(target.selector);
132
+ } catch (err) {
133
+ const outcome = classifyApiFailure(err);
134
+ console.error(
135
+ formatUnobserved(targetLabel(target), {
136
+ reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
137
+ detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
138
+ }),
139
+ );
140
+ return 1;
141
+ }
142
+ if (!info) {
143
+ console.error(`error: the server doesn't have a resource type "${targetLabel(target)}"`);
144
+ return 1;
145
+ }
146
+
147
+ const namespace = flags.flags.allNamespaces ? undefined : (target.namespace ?? client.defaultNamespace);
148
+ const ref = { apiVersion: info.apiVersion, kind: info.kind };
149
+
150
+ let objects: K8sObject[];
151
+ if (target.name) {
152
+ try {
153
+ const obj = await client.readIfPresent({ ...ref, name: target.name, ...(namespace ? { namespace } : {}) });
154
+ if (!obj) {
155
+ console.error(`Error from server (NotFound): ${info.name} "${target.name}" not found`);
156
+ return 1;
157
+ }
158
+ objects = [obj];
159
+ } catch (err) {
160
+ const outcome = classifyApiFailure(err);
161
+ console.error(
162
+ formatUnobserved(`${info.kind}/${target.name}`, {
163
+ reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
164
+ detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
165
+ }),
166
+ );
167
+ return 1;
168
+ }
169
+ } else {
170
+ try {
171
+ objects = await client.list(ref, {
172
+ ...(namespace ? { namespace } : {}),
173
+ ...(flags.values.selector ? { labelSelector: flags.values.selector } : {}),
174
+ });
175
+ } catch (err) {
176
+ const outcome = classifyApiFailure(err);
177
+ console.error(
178
+ formatUnobserved(info.kind, {
179
+ reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
180
+ detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
181
+ }),
182
+ );
183
+ return 1;
184
+ }
185
+ }
186
+
187
+ if (objects.length === 0) {
188
+ console.log("No resources found.");
189
+ return 0;
190
+ }
191
+
192
+ const blocks = await Promise.all(objects.map((obj) => renderBlock(client, obj, info, project)));
193
+ console.log(blocks.join("\n\n\n"));
194
+ return 0;
195
+ }
@@ -0,0 +1,115 @@
1
+ import { describe, test, expect, vi, afterEach } from "vitest";
2
+ import { ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
3
+ import { fakeCluster } from "../api/fake-cluster";
4
+ import { runEvents } from "./events";
5
+
6
+ afterEach(() => vi.restoreAllMocks());
7
+
8
+ function spyConsole() {
9
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
10
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
11
+ return { log, error };
12
+ }
13
+
14
+ function event(name: string, reason: string, involved: { kind: string; name: string }, lastTimestamp: string) {
15
+ return {
16
+ apiVersion: "v1",
17
+ kind: "Event",
18
+ metadata: { name, namespace: "prod", uid: `uid-${name}` },
19
+ involvedObject: involved,
20
+ reason,
21
+ type: "Normal",
22
+ message: `${reason} happened`,
23
+ lastTimestamp,
24
+ };
25
+ }
26
+
27
+ describe("chant kube events (#1079)", () => {
28
+ test("happy path: lists events oldest first", async () => {
29
+ const cluster = fakeCluster({
30
+ respond: (req) =>
31
+ req.path === "/api/v1/namespaces/prod/events"
32
+ ? {
33
+ body: {
34
+ items: [
35
+ event("e2", "Started", { kind: "Pod", name: "web" }, "2026-01-01T00:02:00Z"),
36
+ event("e1", "Scheduled", { kind: "Pod", name: "web" }, "2026-01-01T00:00:00Z"),
37
+ ],
38
+ },
39
+ }
40
+ : undefined,
41
+ });
42
+ const { log } = spyConsole();
43
+
44
+ const code = await runEvents(["-n", "prod"], { connect: cluster.connector });
45
+
46
+ expect(code).toBe(0);
47
+ const out = log.mock.calls[0][0] as string;
48
+ const scheduledIdx = out.indexOf("Scheduled");
49
+ const startedIdx = out.indexOf("Started");
50
+ expect(scheduledIdx).toBeGreaterThan(-1);
51
+ expect(startedIdx).toBeGreaterThan(scheduledIdx);
52
+ });
53
+
54
+ test("--for filters by involvedObject kind/name", async () => {
55
+ const cluster = fakeCluster({
56
+ respond: (req) =>
57
+ req.path === "/api/v1/namespaces/prod/events"
58
+ ? {
59
+ body: {
60
+ items: [
61
+ event("e1", "Started", { kind: "Pod", name: "web" }, "2026-01-01T00:00:00Z"),
62
+ event("e2", "Started", { kind: "Pod", name: "other" }, "2026-01-01T00:00:00Z"),
63
+ ],
64
+ },
65
+ }
66
+ : undefined,
67
+ });
68
+ const { log } = spyConsole();
69
+
70
+ await runEvents(["-n", "prod", "--for=Pod/web"], { connect: cluster.connector });
71
+
72
+ const out = log.mock.calls[0][0] as string;
73
+ expect(out).toContain("Pod/web");
74
+ expect(out).not.toContain("Pod/other");
75
+ });
76
+
77
+ test("binding mismatch refuses loudly", async () => {
78
+ const { error } = spyConsole();
79
+ const connect = async () => {
80
+ throw new ClusterBindingMismatchError("prod", "prod-eks", "dev-eks");
81
+ };
82
+
83
+ const code = await runEvents(["--env", "prod"], { connect });
84
+
85
+ expect(code).toBe(1);
86
+ expect(error.mock.calls[0][0]).toContain("no binding for this environment");
87
+ });
88
+
89
+ test("unknown flag is rejected before any connection", async () => {
90
+ const { error } = spyConsole();
91
+ const connect = vi.fn();
92
+
93
+ const code = await runEvents(["--bogus"], { connect });
94
+
95
+ expect(code).toBe(1);
96
+ expect(error.mock.calls[0][0]).toContain("Unknown flag");
97
+ expect(connect).not.toHaveBeenCalled();
98
+ });
99
+
100
+ test("tri-state: an RBAC-denied list renders NOT-OBSERVED, not an empty list", async () => {
101
+ const cluster = fakeCluster({
102
+ respond: (req) =>
103
+ req.path === "/api/v1/namespaces/prod/events"
104
+ ? { status: 403, body: { kind: "Status", reason: "Forbidden", message: "events is forbidden" } }
105
+ : undefined,
106
+ });
107
+ const { error, log } = spyConsole();
108
+
109
+ const code = await runEvents(["-n", "prod"], { connect: cluster.connector });
110
+
111
+ expect(code).toBe(1);
112
+ expect(error.mock.calls[0][0]).toContain("no credentials");
113
+ expect(log).not.toHaveBeenCalled();
114
+ });
115
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `chant kube events` (chant #1079) — the core `v1 Event` kind is addressed
3
+ * exactly like any other read (`client.list`), no client extension needed.
4
+ * The one thing the typed client's `ListOptions` does not carry is a field
5
+ * selector, so `--for <kind>/<name>` (kubectl's own `kubectl events
6
+ * --for=kind/name` shape) filters client-side against `involvedObject` after
7
+ * the list comes back, rather than asking the server to. Event volume in a
8
+ * single namespace is small enough that this is a fine trade for not
9
+ * widening the client's surface for one caller.
10
+ */
11
+
12
+ import type { K8sClient, K8sObject } from "@intentius/chant-k8s-client";
13
+ import { formatUnobserved } from "@intentius/chant/observation";
14
+ import { defaultK8sConnector, type K8sConnector } from "../api/connect";
15
+ import { classifyApiFailure } from "../api/classify";
16
+ import { kubeConnect } from "./connect";
17
+ import { parseKubeFlags, connectOptionsFrom } from "./flags";
18
+ import { renderTable, renderJson, renderYaml, relativeAge, parseOutput } from "./render";
19
+
20
+ export interface EventsDeps {
21
+ connect?: K8sConnector;
22
+ }
23
+
24
+ interface InvolvedObject {
25
+ kind?: string;
26
+ name?: string;
27
+ namespace?: string;
28
+ }
29
+
30
+ function involvedOf(event: K8sObject): InvolvedObject {
31
+ return (event.involvedObject as InvolvedObject | undefined) ?? {};
32
+ }
33
+
34
+ function eventTimestamp(event: K8sObject): string | undefined {
35
+ return (
36
+ (event.lastTimestamp as string | undefined) ??
37
+ (event.eventTime as string | undefined) ??
38
+ event.metadata?.creationTimestamp
39
+ );
40
+ }
41
+
42
+ /** Events whose `involvedObject` names `ref`, oldest first — kubectl's own default order. */
43
+ export function filterInvolved(events: readonly K8sObject[], ref: { kind: string; name: string }): K8sObject[] {
44
+ return events.filter((e) => {
45
+ const involved = involvedOf(e);
46
+ return involved.kind === ref.kind && involved.name === ref.name;
47
+ });
48
+ }
49
+
50
+ function sortByTime(events: readonly K8sObject[]): K8sObject[] {
51
+ return [...events].sort((a, b) => {
52
+ const ta = eventTimestamp(a);
53
+ const tb = eventTimestamp(b);
54
+ if (!ta && !tb) return 0;
55
+ if (!ta) return -1;
56
+ if (!tb) return 1;
57
+ return Date.parse(ta) - Date.parse(tb);
58
+ });
59
+ }
60
+
61
+ /**
62
+ * List Events for `namespace` (or every namespace, when `namespace` is
63
+ * undefined), sorted oldest first. Shared by `runEvents` and `describe`'s own
64
+ * Events section, so the two verbs agree on what "the events for this object"
65
+ * means.
66
+ */
67
+ export async function listEvents(
68
+ client: K8sClient,
69
+ options: { namespace?: string; labelSelector?: string } = {},
70
+ ): Promise<K8sObject[]> {
71
+ const events = await client.list(
72
+ { apiVersion: "v1", kind: "Event" },
73
+ { ...(options.namespace ? { namespace: options.namespace } : {}), ...(options.labelSelector ? { labelSelector: options.labelSelector } : {}) },
74
+ );
75
+ return sortByTime(events);
76
+ }
77
+
78
+ function eventRow(e: K8sObject): string[] {
79
+ const involved = involvedOf(e);
80
+ const count = typeof e.count === "number" ? e.count : undefined;
81
+ return [
82
+ relativeAge(eventTimestamp(e)),
83
+ (e.type as string | undefined) ?? "",
84
+ (e.reason as string | undefined) ?? "",
85
+ `${involved.kind ?? ""}/${involved.name ?? ""}`,
86
+ `${(e.message as string | undefined) ?? ""}${count && count > 1 ? ` (x${count})` : ""}`,
87
+ ];
88
+ }
89
+
90
+ /** Render an events table — reused by `describe`'s Events section. */
91
+ export function renderEvents(events: readonly K8sObject[]): string {
92
+ if (events.length === 0) return "No events found.";
93
+ return renderTable(["LAST SEEN", "TYPE", "REASON", "OBJECT", "MESSAGE"], events.map(eventRow));
94
+ }
95
+
96
+ function parseFor(value: string | undefined): { kind: string; name: string } | undefined {
97
+ if (!value) return undefined;
98
+ const slash = value.indexOf("/");
99
+ if (slash === -1) throw new Error(`--for expects KIND/NAME, e.g. --for=Pod/web-abc123`);
100
+ return { kind: value.slice(0, slash), name: value.slice(slash + 1) };
101
+ }
102
+
103
+ export async function runEvents(rawArgs: string[], deps: EventsDeps = {}): Promise<number> {
104
+ const connect = deps.connect ?? defaultK8sConnector;
105
+
106
+ let flags;
107
+ try {
108
+ flags = parseKubeFlags(rawArgs, { value: { "--for": "for" } });
109
+ } catch (err) {
110
+ console.error(err instanceof Error ? err.message : String(err));
111
+ return 1;
112
+ }
113
+
114
+ let output;
115
+ let forRef;
116
+ try {
117
+ output = parseOutput(flags.values.output);
118
+ forRef = parseFor(flags.values.for);
119
+ } catch (err) {
120
+ console.error(err instanceof Error ? err.message : String(err));
121
+ return 1;
122
+ }
123
+
124
+ const connected = await kubeConnect(connectOptionsFrom(flags.values), connect);
125
+ if (connected.kind === "unobserved") {
126
+ console.error(formatUnobserved("events", { reason: connected.reason, detail: connected.detail }));
127
+ return 1;
128
+ }
129
+ const { client } = connected;
130
+
131
+ const namespace = flags.flags.allNamespaces ? undefined : (flags.values.namespace ?? client.defaultNamespace);
132
+
133
+ let events: K8sObject[];
134
+ try {
135
+ events = await listEvents(client, { namespace, ...(flags.values.selector ? { labelSelector: flags.values.selector } : {}) });
136
+ } catch (err) {
137
+ const outcome = classifyApiFailure(err);
138
+ console.error(
139
+ formatUnobserved("events", {
140
+ reason: outcome.kind === "unobserved" ? outcome.reason : "read-failed",
141
+ detail: outcome.kind === "unobserved" ? outcome.detail : String(err),
142
+ }),
143
+ );
144
+ return 1;
145
+ }
146
+
147
+ if (forRef) events = filterInvolved(events, forRef);
148
+
149
+ switch (output.kind) {
150
+ case "json":
151
+ console.log(renderJson(events));
152
+ return 0;
153
+ case "yaml":
154
+ console.log(renderYaml(events));
155
+ return 0;
156
+ default:
157
+ console.log(renderEvents(events));
158
+ return 0;
159
+ }
160
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { parseKubeFlags, connectOptionsFrom, parseDurationSeconds, parseDurationMs } from "./flags";
3
+
4
+ describe("parseKubeFlags (chant #1079)", () => {
5
+ test("splits positional args from the common flag set", () => {
6
+ const { positional, values, flags } = parseKubeFlags(["deployment", "web", "-n", "prod", "-A"]);
7
+ expect(positional).toEqual(["deployment", "web"]);
8
+ expect(values.namespace).toBe("prod");
9
+ expect(flags.allNamespaces).toBe(true);
10
+ });
11
+
12
+ test("accepts a joined --flag=value form (#1127 discipline)", () => {
13
+ const { values } = parseKubeFlags(["--namespace=prod", "--output=json"]);
14
+ expect(values).toEqual({ namespace: "prod", output: "json" });
15
+ });
16
+
17
+ test("merges a verb's own flag spec with the common set", () => {
18
+ const { values, flags } = parseKubeFlags(["--tail", "50", "--previous"], {
19
+ value: { "--tail": "tail" },
20
+ boolean: { "--previous": "previous" },
21
+ });
22
+ expect(values.tail).toBe("50");
23
+ expect(flags.previous).toBe(true);
24
+ });
25
+
26
+ test("an unrecognized flag throws the shared unknown-flag error", () => {
27
+ expect(() => parseKubeFlags(["--bogus"])).toThrow(/Unknown flag: --bogus/);
28
+ });
29
+
30
+ test("a value flag given as a boolean via = throws (#1127)", () => {
31
+ expect(() => parseKubeFlags(["--all-namespaces=yes"])).toThrow(/boolean flag/);
32
+ });
33
+
34
+ test("a value flag with nothing after it is an error, not a silent positional swallow", () => {
35
+ expect(() => parseKubeFlags(["--namespace"])).toThrow(/requires a value/);
36
+ });
37
+
38
+ test("a bare '-' is a positional, not a flag (kubectl's stdin convention)", () => {
39
+ const { positional } = parseKubeFlags(["apply", "-"]);
40
+ expect(positional).toEqual(["apply", "-"]);
41
+ });
42
+ });
43
+
44
+ describe("connectOptionsFrom", () => {
45
+ test("maps env/context/kubeconfig to ConnectOptions", () => {
46
+ expect(connectOptionsFrom({ env: "prod" })).toEqual({ environment: "prod" });
47
+ expect(connectOptionsFrom({ context: "prod-eks" })).toEqual({ context: "prod-eks" });
48
+ expect(connectOptionsFrom({ kubeconfig: "/tmp/kc" })).toEqual({ client: { kubeconfigPath: "/tmp/kc" } });
49
+ expect(connectOptionsFrom({})).toEqual({});
50
+ });
51
+ });
52
+
53
+ describe("parseDurationSeconds / parseDurationMs", () => {
54
+ test("parses bare seconds and suffixed durations", () => {
55
+ expect(parseDurationSeconds("45")).toBe(45);
56
+ expect(parseDurationSeconds("45s")).toBe(45);
57
+ expect(parseDurationSeconds("90m")).toBe(5400);
58
+ expect(parseDurationSeconds("2h")).toBe(7200);
59
+ expect(parseDurationMs("1h")).toBe(3_600_000);
60
+ });
61
+
62
+ test("rejects a nonsense duration", () => {
63
+ expect(() => parseDurationSeconds("soon")).toThrow(/expected a duration/);
64
+ });
65
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Flag parsing shared by every `chant kube <verb>` (chant #1079).
3
+ *
4
+ * `CommandGroupContext.rawArgs` is unparsed argv — core has no vocabulary for
5
+ * a lexicon's own verbs (chant #1078) — so every verb here does its own
6
+ * parsing, reusing `splitJoinedFlags`/`unknownFlagError` for the same
7
+ * `--flag=value` splitting and rejection discipline core's own parser
8
+ * applies (chant #1127). This module adds the flag vocabulary every verb
9
+ * shares (namespace, selector, output, cluster targeting) so each verb file
10
+ * only has to declare what is actually its own (`--tail`, `--for`, ...).
11
+ */
12
+
13
+ import { splitJoinedFlags, unknownFlagError } from "@intentius/chant/cli/command-group";
14
+ import type { ConnectOptions } from "../api/connect";
15
+
16
+ /** A verb's own flag vocabulary, merged with the common set below. */
17
+ export interface FlagSpec {
18
+ /** Value flags this verb accepts, alias → canonical key (e.g. `{ "--tail": "tail" }`). */
19
+ value?: Record<string, string>;
20
+ /** Boolean flags this verb accepts, alias → canonical key. */
21
+ boolean?: Record<string, string>;
22
+ }
23
+
24
+ /** Every `chant kube` verb accepts these — kubectl's own names. */
25
+ const COMMON_VALUE: Record<string, string> = {
26
+ "-n": "namespace",
27
+ "--namespace": "namespace",
28
+ "-l": "selector",
29
+ "--selector": "selector",
30
+ "-o": "output",
31
+ "--output": "output",
32
+ "--context": "context",
33
+ "--env": "env",
34
+ "--kubeconfig": "kubeconfig",
35
+ };
36
+
37
+ const COMMON_BOOLEAN: Record<string, string> = {
38
+ "-A": "allNamespaces",
39
+ "--all-namespaces": "allNamespaces",
40
+ };
41
+
42
+ export interface ParsedFlags {
43
+ /** Non-flag tokens, in order — the verb's own positional arguments (kind, name, ...). */
44
+ positional: string[];
45
+ /** Resolved value-flag key → the value given. */
46
+ values: Record<string, string>;
47
+ /** Resolved boolean-flag key → true (absent keys are false). */
48
+ flags: Record<string, boolean>;
49
+ }
50
+
51
+ /**
52
+ * Parse `rawArgs` against the common flag set plus `spec`. Throws
53
+ * {@link unknownFlagError}'s shape on anything starting with `-` that
54
+ * neither set recognizes, and on a value flag given with nothing after it.
55
+ */
56
+ export function parseKubeFlags(rawArgs: string[], spec: FlagSpec = {}): ParsedFlags {
57
+ const valueFlags: Record<string, string> = { ...COMMON_VALUE, ...(spec.value ?? {}) };
58
+ const booleanFlags: Record<string, string> = { ...COMMON_BOOLEAN, ...(spec.boolean ?? {}) };
59
+ const args = splitJoinedFlags(rawArgs, new Set(Object.keys(booleanFlags)));
60
+
61
+ const positional: string[] = [];
62
+ const values: Record<string, string> = {};
63
+ const flags: Record<string, boolean> = {};
64
+
65
+ for (let i = 0; i < args.length; i++) {
66
+ const arg = args[i];
67
+ if (!arg.startsWith("-") || arg === "-") {
68
+ positional.push(arg);
69
+ continue;
70
+ }
71
+ if (arg in booleanFlags) {
72
+ flags[booleanFlags[arg]] = true;
73
+ continue;
74
+ }
75
+ if (arg in valueFlags) {
76
+ const value = args[++i];
77
+ if (value === undefined) throw new Error(`${arg} requires a value`);
78
+ values[valueFlags[arg]] = value;
79
+ continue;
80
+ }
81
+ throw unknownFlagError(arg);
82
+ }
83
+
84
+ return { positional, values, flags };
85
+ }
86
+
87
+ /** Build the typed client's `ConnectOptions` from a parsed common flag set. */
88
+ export function connectOptionsFrom(values: Record<string, string>): ConnectOptions {
89
+ const options: ConnectOptions = {};
90
+ if (values.env !== undefined) options.environment = values.env;
91
+ if (values.context !== undefined) options.context = values.context;
92
+ if (values.kubeconfig !== undefined) options.client = { kubeconfigPath: values.kubeconfig };
93
+ return options;
94
+ }
95
+
96
+ /** kubectl duration shorthand: "3600" (bare seconds), "1h", "90m", "45s". Shared by `logs --since` and `wait --timeout`. */
97
+ export function parseDurationSeconds(value: string): number {
98
+ const match = /^(\d+)(s|m|h)?$/.exec(value.trim());
99
+ if (!match) throw new Error(`expected a duration like "1h", "90m", "45s", or a bare second count, got "${value}"`);
100
+ const n = Number(match[1]);
101
+ switch (match[2]) {
102
+ case "h":
103
+ return n * 3600;
104
+ case "m":
105
+ return n * 60;
106
+ default:
107
+ return n;
108
+ }
109
+ }
110
+
111
+ /** {@link parseDurationSeconds} in milliseconds. */
112
+ export function parseDurationMs(value: string): number {
113
+ return parseDurationSeconds(value) * 1000;
114
+ }