@oasissys/k8s-cli 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oasis Systems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # k8s-cli
2
+
3
+ `oasis-k8s` — an AXI-compliant CLI for read-only Kubernetes access across the Oasis clusters.
4
+ List namespaces, nodes, pods, deployments, services, and events, read pod logs, and describe objects — with token-efficient [TOON](https://toonformat.dev/) output built for autonomous agents.
5
+
6
+ Extracted from the `oasis-core` Claude Code plugin so it can be installed, versioned, and released on its own.
7
+
8
+ ## Quick Start
9
+
10
+ Install the k8s-cli skill in the [Agent Skills](https://agentskills.io) format with [`npx skills`](https://github.com/vercel-labs/skills):
11
+
12
+ ```sh
13
+ npx skills add oasissys/k8s-cli --skill k8s-cli -g
14
+ ```
15
+
16
+ That is the entire setup — the skill teaches your agent the CLI, and it runs without a global install as `npx -y @oasissys/k8s-cli` (Node 20+ required). Installing the skill itself needs GitHub access to this private repo.
17
+
18
+ `-g` installs the skill for all projects (`~/.claude/skills/`); drop it to install for the current project only.
19
+
20
+ ## Other ways to install
21
+
22
+ ### Zero setup
23
+
24
+ Any capable agent can run the CLI directly with nothing installed. Just tell your agent:
25
+
26
+ ```
27
+ Execute `npx -y @oasissys/k8s-cli` to get Oasis Kubernetes tools.
28
+ ```
29
+
30
+ ### Global install + session hook
31
+
32
+ Want the CLI on PATH and the saved clusters fed into every agent session?
33
+
34
+ ```sh
35
+ npm install -g @oasissys/k8s-cli
36
+ oasis-k8s setup
37
+ ```
38
+
39
+ `setup` registers a `SessionStart` hook for **Claude Code**, **Codex**, and **OpenCode** that surfaces the saved clusters at session start — restart your agent session after running it.
40
+ If you use the `oasis-core` Claude Code plugin, it already ships this hook; skip `setup` or remove one of the two.
41
+
42
+ ### From a clone (development)
43
+
44
+ ```sh
45
+ pnpm install && pnpm run build && pnpm link --global
46
+ ```
47
+
48
+ ## Register clusters
49
+
50
+ Clusters live in `~/.oasis.json` (shared with the other oasis CLIs and the Oasis dashboard) and are verified against the apiserver's `/version` before being saved.
51
+ The CLI talks straight to each kube-apiserver with a ServiceAccount bearer token — no kubeconfig, no certs.
52
+
53
+ ```sh
54
+ oasis-k8s add jeddah --url https://<host>:6443 --token <token>
55
+ oasis-k8s clusters # list saved clusters and the active one
56
+ oasis-k8s use jordan # switch the active cluster
57
+ oasis-k8s rm jeddah # remove a saved cluster
58
+ ```
59
+
60
+ ## Use
61
+
62
+ ```sh
63
+ oasis-k8s # live view: saved clusters + active
64
+ oasis-k8s pods --namespace default # pods in one namespace
65
+ oasis-k8s nodes # nodes (status + kubelet version)
66
+ oasis-k8s logs my-pod --namespace default # tail a pod's logs (200 lines)
67
+ oasis-k8s describe pod my-pod --namespace default # dump one object as JSON
68
+ oasis-k8s deployments --cluster jordan # target a cluster without switching
69
+ ```
70
+
71
+ Every command supports `--help`. Output is TOON on stdout, diagnostics on stderr, exit codes 0/1/2 (success/error/usage).
72
+ Every read command is a GET — the CLI never mutates cluster state.
73
+
74
+ ## Development
75
+
76
+ ```sh
77
+ pnpm run dev -- pods --namespace default # run from source
78
+ pnpm test # vitest
79
+ pnpm run lint # eslint
80
+ pnpm run build:skill # regenerate SKILL.md
81
+ ```
82
+
83
+ Releases are cut by release-please from conventional commits on `main`.
84
+ Do not hand-edit `CHANGELOG.md`, `.release-please-manifest.json`, or `skills/k8s-cli/SKILL.md` — they are generated.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../src/cli.js";
3
+ run();
4
+ //# sourceMappingURL=oasis-k8s.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oasis-k8s.js","sourceRoot":"","sources":["../../bin/oasis-k8s.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEpC,GAAG,EAAE,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared harness for AXI (Agent eXperience Interface) CLIs.
3
+ *
4
+ * Encodes the cross-cutting AXI standards so each service CLI only has to
5
+ * declare its commands:
6
+ * - stdout carries all agent-consumable output (data + errors), stderr is
7
+ * for diagnostics only, exit codes are 0/1/2 (success/error/usage) (§6)
8
+ * - structured errors on stdout with an actionable `help` line (§6)
9
+ * - unknown flags/commands are rejected by name with the valid set inline (§6)
10
+ * - `bin:` identity line with the home directory collapsed to `~` (§10)
11
+ *
12
+ * See https://toonformat.dev/reference/spec.html for the output format.
13
+ */
14
+ import { type ServiceId } from "./config.js";
15
+ export declare const EXIT: {
16
+ readonly OK: 0;
17
+ readonly ERROR: 1;
18
+ readonly USAGE: 2;
19
+ };
20
+ /** Collapse the user's home directory prefix to `~` for display (§10). */
21
+ export declare function collapseHome(p: string): string;
22
+ /**
23
+ * Best-effort absolute path of the running CLI, home-collapsed (§10).
24
+ * `OASIS_CLI_BIN` lets a launcher advertise the on-PATH name it was invoked as
25
+ * instead of the bundled entrypoint.
26
+ */
27
+ export declare function binPath(): string;
28
+ /**
29
+ * Print a TOON document to stdout.
30
+ *
31
+ * A top-level `help: string[]` is rendered as a readable block — one command
32
+ * template per indented line, unquoted — instead of a comma-joined primitive
33
+ * array, since suggestions contain colons, commas, and backticks that would
34
+ * otherwise force heavy quoting. This matches the AXI help convention.
35
+ */
36
+ export declare function emit(doc: unknown): void;
37
+ /** Print raw text to stdout (for console logs, help blocks — already formatted). */
38
+ export declare function emitRaw(text: string): void;
39
+ /**
40
+ * Emit a structured error on stdout and exit. `help` is the specific command
41
+ * that resolves the problem — never "see --help" (§9).
42
+ */
43
+ export declare function fail(message: string, help?: string | string[], code?: number): never;
44
+ export type FlagKind = "bool" | "value" | "multi";
45
+ export interface ParsedArgs {
46
+ positionals: string[];
47
+ /** bool → true; value → last string; multi → string[] */
48
+ flags: Record<string, string | string[] | true>;
49
+ }
50
+ /**
51
+ * Parse `--flag`, `--flag value`, and `--flag=value`. Every recognized flag is
52
+ * declared in `known` with its kind. Unknown flags are rejected by name with
53
+ * the command's valid set inline, so the agent self-corrects in one turn (§6).
54
+ *
55
+ * `--help` is always allowed and surfaces as `flags["--help"] === true`.
56
+ */
57
+ export declare function parseArgs(argv: string[], known: Record<string, FlagKind>, usageContext: string): ParsedArgs;
58
+ export declare function flagStr(parsed: ParsedArgs, name: string): string | undefined;
59
+ export declare function flagBool(parsed: ParsedArgs, name: string): boolean;
60
+ /**
61
+ * Register the `<cli> --ambient` SessionStart hook for Claude Code, Codex,
62
+ * and OpenCode. Idempotent and path-repairing: re-running after a reinstall
63
+ * updates the path. Reports every target so the agent/user can see exactly
64
+ * what changed (§7).
65
+ */
66
+ export declare function runSetup(binName: string): void;
67
+ export interface AuthOptions {
68
+ /** Executable name for help/suggestions, e.g. "oasis-jenkins". */
69
+ binName: string;
70
+ /** Human service label, e.g. "Jenkins". */
71
+ label: string;
72
+ /** Whether the service accepts a `--url` override (Jenkins does). */
73
+ hasUrl?: boolean;
74
+ /** Probe that throws if the credentials are rejected by the live service. */
75
+ verify: (creds: {
76
+ username: string;
77
+ password: string;
78
+ url?: string;
79
+ }) => Promise<void>;
80
+ }
81
+ export declare function runAuth(service: ServiceId, parsed: ParsedArgs, opts: AuthOptions): Promise<void>;
82
+ export declare function flagInt(parsed: ParsedArgs, name: string): number | undefined;
83
+ export declare function flagList(parsed: ParsedArgs, name: string): string[];
84
+ /** Parse a repeatable, comma-separated `--fields a,b --fields c` into ["a","b","c"]. */
85
+ export declare function fieldList(parsed: ParsedArgs, name?: string): string[];
86
+ /**
87
+ * Resolve which columns a list view should emit. Defaults are the minimal
88
+ * schema; `requested` (from --fields) adds columns, validated against the full
89
+ * `available` set — an unknown field fails loud with the valid set (§2, §6).
90
+ * Output preserves `available` order for stable tables.
91
+ */
92
+ export declare function resolveColumns(available: string[], defaults: string[], requested: string[], usage: string): string[];
93
+ /** Project a full row down to the selected columns, preserving column order. */
94
+ export declare function pick(row: Record<string, unknown>, cols: string[]): Record<string, unknown>;
95
+ export interface SkillCommand {
96
+ cmd: string;
97
+ summary: string;
98
+ }
99
+ export interface SkillSpec {
100
+ /** Skill name (frontmatter + directory). */
101
+ name: string;
102
+ /** Executable base name, e.g. "oasis-jenkins". */
103
+ binName: string;
104
+ /** Trigger-shaped one-line frontmatter description. */
105
+ description: string;
106
+ /** Opening paragraph(s) of the body. */
107
+ intro: string;
108
+ /** Command table rows. */
109
+ commands: SkillCommand[];
110
+ /** Shell example lines (no fence). */
111
+ examples: string[];
112
+ /** Trailing bullet notes (no leading dash). */
113
+ notes?: string[];
114
+ /** Optional extra section inserted before Commands (markdown lines). */
115
+ extra?: string[];
116
+ }
117
+ /**
118
+ * Render a SKILL.md from a SkillSpec. The CLI owns the spec, so the committed
119
+ * skill file is generated — never hand-edited — and `build-cli.sh --check`
120
+ * fails CI if the committed copy drifts from this output (§7).
121
+ */
122
+ export declare function renderSkillDoc(spec: SkillSpec): string;
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Shared harness for AXI (Agent eXperience Interface) CLIs.
3
+ *
4
+ * Encodes the cross-cutting AXI standards so each service CLI only has to
5
+ * declare its commands:
6
+ * - stdout carries all agent-consumable output (data + errors), stderr is
7
+ * for diagnostics only, exit codes are 0/1/2 (success/error/usage) (§6)
8
+ * - structured errors on stdout with an actionable `help` line (§6)
9
+ * - unknown flags/commands are rejected by name with the valid set inline (§6)
10
+ * - `bin:` identity line with the home directory collapsed to `~` (§10)
11
+ *
12
+ * See https://toonformat.dev/reference/spec.html for the output format.
13
+ */
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { encode } from "./toon.js";
18
+ import { loadCreds, saveCreds, removeCreds } from "./storage.js";
19
+ import { getServiceBaseUrl } from "./config.js";
20
+ export const EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
21
+ /** Collapse the user's home directory prefix to `~` for display (§10). */
22
+ export function collapseHome(p) {
23
+ const home = os.homedir();
24
+ return p === home || p.startsWith(home + path.sep) ? "~" + p.slice(home.length) : p;
25
+ }
26
+ /**
27
+ * Best-effort absolute path of the running CLI, home-collapsed (§10).
28
+ * `OASIS_CLI_BIN` lets a launcher advertise the on-PATH name it was invoked as
29
+ * instead of the bundled entrypoint.
30
+ */
31
+ export function binPath() {
32
+ const raw = process.env.OASIS_CLI_BIN || process.argv[1] || "oasis-cli";
33
+ // A bare command name (no path separator) is an on-PATH launcher name — a
34
+ // `bin/` shim advertising how it was invoked. Surface it verbatim rather than
35
+ // resolving it against the cwd, which would fabricate a bogus absolute path.
36
+ if (!raw.includes("/") && !raw.includes("\\"))
37
+ return raw;
38
+ try {
39
+ return collapseHome(path.resolve(raw));
40
+ }
41
+ catch {
42
+ return raw;
43
+ }
44
+ }
45
+ /**
46
+ * Print a TOON document to stdout.
47
+ *
48
+ * A top-level `help: string[]` is rendered as a readable block — one command
49
+ * template per indented line, unquoted — instead of a comma-joined primitive
50
+ * array, since suggestions contain colons, commas, and backticks that would
51
+ * otherwise force heavy quoting. This matches the AXI help convention.
52
+ */
53
+ export function emit(doc) {
54
+ if (doc && typeof doc === "object" && !Array.isArray(doc) && Array.isArray(doc.help)) {
55
+ const { help, ...rest } = doc;
56
+ const body = encode(rest);
57
+ const helpBlock = help.length > 0
58
+ ? `help[${help.length}]:\n` + help.map(h => ` ${String(h)}`).join("\n")
59
+ : "";
60
+ const out = [body, helpBlock].filter(Boolean).join("\n");
61
+ process.stdout.write(out + "\n");
62
+ return;
63
+ }
64
+ process.stdout.write(encode(doc) + "\n");
65
+ }
66
+ /** Print raw text to stdout (for console logs, help blocks — already formatted). */
67
+ export function emitRaw(text) {
68
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
69
+ }
70
+ /**
71
+ * Emit a structured error on stdout and exit. `help` is the specific command
72
+ * that resolves the problem — never "see --help" (§9).
73
+ */
74
+ export function fail(message, help, code = EXIT.ERROR) {
75
+ const doc = { error: message };
76
+ if (help !== undefined)
77
+ doc.help = Array.isArray(help) ? help : [help];
78
+ emit(doc);
79
+ process.exit(code);
80
+ }
81
+ /**
82
+ * Parse `--flag`, `--flag value`, and `--flag=value`. Every recognized flag is
83
+ * declared in `known` with its kind. Unknown flags are rejected by name with
84
+ * the command's valid set inline, so the agent self-corrects in one turn (§6).
85
+ *
86
+ * `--help` is always allowed and surfaces as `flags["--help"] === true`.
87
+ */
88
+ export function parseArgs(argv, known, usageContext) {
89
+ const spec = { "--help": "bool", ...known };
90
+ const positionals = [];
91
+ const flags = {};
92
+ for (let i = 0; i < argv.length; i++) {
93
+ const arg = argv[i];
94
+ if (!arg.startsWith("--")) {
95
+ positionals.push(arg);
96
+ continue;
97
+ }
98
+ const eq = arg.indexOf("=");
99
+ const name = eq === -1 ? arg : arg.slice(0, eq);
100
+ const kind = spec[name];
101
+ if (!kind) {
102
+ const valid = Object.keys(spec).join(", ");
103
+ fail(`unknown flag ${name} for \`${usageContext}\``, `valid flags: ${valid}`, EXIT.USAGE);
104
+ }
105
+ if (kind === "bool") {
106
+ if (eq !== -1) {
107
+ fail(`flag ${name} does not take a value`, `use: ${name}`, EXIT.USAGE);
108
+ }
109
+ flags[name] = true;
110
+ continue;
111
+ }
112
+ // value / multi
113
+ let value;
114
+ if (eq !== -1) {
115
+ value = arg.slice(eq + 1);
116
+ }
117
+ else {
118
+ const next = argv[i + 1];
119
+ if (next === undefined || next.startsWith("--")) {
120
+ fail(`flag ${name} requires a value`, `use: ${name} <value>`, EXIT.USAGE);
121
+ }
122
+ value = next;
123
+ i++;
124
+ }
125
+ if (kind === "multi") {
126
+ const existing = flags[name];
127
+ flags[name] = Array.isArray(existing) ? [...existing, value] : [value];
128
+ }
129
+ else {
130
+ flags[name] = value;
131
+ }
132
+ }
133
+ return { positionals, flags };
134
+ }
135
+ export function flagStr(parsed, name) {
136
+ const v = parsed.flags[name];
137
+ return typeof v === "string" ? v : undefined;
138
+ }
139
+ export function flagBool(parsed, name) {
140
+ return parsed.flags[name] === true;
141
+ }
142
+ function upsertJsonHook(file, command, marker) {
143
+ let data = {};
144
+ let existed = false;
145
+ try {
146
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
147
+ if (parsed && typeof parsed === "object") {
148
+ data = parsed;
149
+ existed = true;
150
+ }
151
+ }
152
+ catch { /* new file */ }
153
+ const hooks = data.hooks && typeof data.hooks === "object" ? data.hooks : {};
154
+ data.hooks = hooks;
155
+ const arr = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
156
+ let status = "installed";
157
+ let found = false;
158
+ for (const group of arr) {
159
+ if (!group || !Array.isArray(group.hooks))
160
+ continue;
161
+ for (const h of group.hooks) {
162
+ if (h && typeof h.command === "string" && h.command.includes(marker)) {
163
+ found = true;
164
+ if (h.command === command)
165
+ status = "unchanged";
166
+ else {
167
+ h.command = command;
168
+ status = "updated";
169
+ }
170
+ }
171
+ }
172
+ }
173
+ if (!found) {
174
+ arr.push({ hooks: [{ type: "command", command }] });
175
+ status = existed ? "updated" : "installed";
176
+ }
177
+ hooks.SessionStart = arr;
178
+ if (status !== "unchanged") {
179
+ fs.mkdirSync(path.dirname(file), { recursive: true });
180
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
181
+ }
182
+ return status;
183
+ }
184
+ function setupCodexConfig() {
185
+ const file = path.join(os.homedir(), ".codex", "config.toml");
186
+ let content = "";
187
+ try {
188
+ content = fs.readFileSync(file, "utf-8");
189
+ }
190
+ catch { /* new file */ }
191
+ if (/^\s*hooks\s*=\s*true/m.test(content)) {
192
+ return { target: "codex-config.toml", path: file, status: "unchanged" };
193
+ }
194
+ let next;
195
+ if (/^\s*\[features\]/m.test(content)) {
196
+ next = content.replace(/^(\s*\[features\]\s*\n)/m, `$1hooks = true\n`);
197
+ }
198
+ else {
199
+ const sep = content && !content.endsWith("\n") ? "\n" : "";
200
+ next = `${content}${sep}\n[features]\nhooks = true\n`;
201
+ }
202
+ fs.mkdirSync(path.dirname(file), { recursive: true });
203
+ fs.writeFileSync(file, next);
204
+ return { target: "codex-config.toml", path: file, status: content ? "updated" : "installed", note: "enabled [features].hooks" };
205
+ }
206
+ function openCodePlugin(binAbs, binName) {
207
+ // Fail-safe OpenCode plugin: injects the CLI's ambient snapshot as session
208
+ // context. Wrapped so a schema mismatch can never break OpenCode startup.
209
+ return [
210
+ `// Auto-generated by \`${binName} setup\`. Injects Oasis ambient context at session start.`,
211
+ `import { execFile } from "node:child_process";`,
212
+ ``,
213
+ `export const OasisAmbient = async () => {`,
214
+ ` const run = () => new Promise((resolve) => {`,
215
+ ` execFile("node", [${JSON.stringify(binAbs)}, "--ambient"], { timeout: 4000 }, (_e, out) => resolve(out || ""));`,
216
+ ` });`,
217
+ ` return {`,
218
+ ` // OpenCode injects the returned text into the session's system context.`,
219
+ ` "chat.system": async (_input, output) => {`,
220
+ ` try { const t = await run(); if (t.trim()) output.parts.push(t); } catch {}`,
221
+ ` },`,
222
+ ` };`,
223
+ `};`,
224
+ ``,
225
+ ].join("\n");
226
+ }
227
+ function setupOpenCode(binAbs, binName) {
228
+ const file = path.join(os.homedir(), ".config", "opencode", "plugin", `${binName}.js`);
229
+ const content = openCodePlugin(binAbs, binName);
230
+ let existing = "";
231
+ try {
232
+ existing = fs.readFileSync(file, "utf-8");
233
+ }
234
+ catch { /* new */ }
235
+ if (existing === content)
236
+ return { target: "opencode-plugin", path: file, status: "unchanged", note: "unverified against a live OpenCode" };
237
+ fs.mkdirSync(path.dirname(file), { recursive: true });
238
+ fs.writeFileSync(file, content);
239
+ return { target: "opencode-plugin", path: file, status: existing ? "updated" : "installed", note: "unverified against a live OpenCode" };
240
+ }
241
+ /**
242
+ * Register the `<cli> --ambient` SessionStart hook for Claude Code, Codex,
243
+ * and OpenCode. Idempotent and path-repairing: re-running after a reinstall
244
+ * updates the path. Reports every target so the agent/user can see exactly
245
+ * what changed (§7).
246
+ */
247
+ export function runSetup(binName) {
248
+ const binAbs = path.resolve(process.argv[1] || binName);
249
+ const command = `node "${binAbs}" --ambient`;
250
+ const marker = path.basename(binAbs); // e.g. oasis-bitbucket.js
251
+ const results = [];
252
+ try {
253
+ const claudeSettings = path.join(os.homedir(), ".claude", "settings.json");
254
+ results.push({ target: "claude-settings.json", path: claudeSettings, status: upsertJsonHook(claudeSettings, command, marker) });
255
+ }
256
+ catch (e) {
257
+ results.push({ target: "claude-code", path: "~/.claude", status: "skipped", note: e instanceof Error ? e.message : String(e) });
258
+ }
259
+ try {
260
+ const codexHooks = path.join(os.homedir(), ".codex", "hooks.json");
261
+ results.push({ target: "codex-hooks.json", path: codexHooks, status: upsertJsonHook(codexHooks, command, marker) });
262
+ results.push(setupCodexConfig());
263
+ }
264
+ catch (e) {
265
+ results.push({ target: "codex", path: "~/.codex", status: "skipped", note: e instanceof Error ? e.message : String(e) });
266
+ }
267
+ try {
268
+ results.push(setupOpenCode(binAbs, binName));
269
+ }
270
+ catch (e) {
271
+ results.push({ target: "opencode", path: "~/.config/opencode", status: "skipped", note: e instanceof Error ? e.message : String(e) });
272
+ }
273
+ emit({
274
+ setup: binName,
275
+ hook: command,
276
+ note: "If the oasis-core Claude Code plugin is installed, its hooks.json already runs this ambient hook — remove one of the two to avoid duplication",
277
+ targets: results,
278
+ help: [
279
+ "Re-run this after reinstalling to repair the path",
280
+ "Codex also needs [features].hooks = true (set above) and a Codex restart",
281
+ ],
282
+ });
283
+ }
284
+ export async function runAuth(service, parsed, opts) {
285
+ const setHint = `${opts.binName} auth --user <u> --password <p>${opts.hasUrl ? " [--url <url>]" : ""}`;
286
+ if (flagBool(parsed, "--clear")) {
287
+ const had = !!loadCreds(service)?.username;
288
+ await removeCreds(service);
289
+ emit({ auth: service, status: had ? "cleared" : "already cleared (no-op)" });
290
+ return;
291
+ }
292
+ const user = flagStr(parsed, "--user");
293
+ const password = flagStr(parsed, "--password");
294
+ const url = opts.hasUrl ? flagStr(parsed, "--url") : undefined;
295
+ // No creds supplied → report current status.
296
+ if (!user && !password) {
297
+ const creds = loadCreds(service);
298
+ if (!creds?.username) {
299
+ emit({ auth: service, status: "not configured", help: [`Run \`${setHint}\` to sign in`] });
300
+ return;
301
+ }
302
+ emit({ auth: service, status: "configured", user: creds.username, server: getServiceBaseUrl(service, creds.url) });
303
+ return;
304
+ }
305
+ const missing = [!user && "--user", !password && "--password"].filter(Boolean);
306
+ if (missing.length)
307
+ fail(`missing required flag(s): ${missing.join(", ")}`, setHint, EXIT.USAGE);
308
+ const creds = { username: user, password: password, ...(url ? { url } : {}) };
309
+ try {
310
+ await opts.verify(creds);
311
+ }
312
+ catch (e) {
313
+ const reason = e instanceof Error ? e.message : String(e);
314
+ fail(`${opts.label} rejected these credentials (${reason})`, `Check the username/password${opts.hasUrl ? "/url" : ""} and re-run \`${setHint}\``);
315
+ }
316
+ await saveCreds(service, creds);
317
+ emit({ auth: service, status: "saved", user, server: getServiceBaseUrl(service, url) });
318
+ }
319
+ export function flagInt(parsed, name) {
320
+ const v = flagStr(parsed, name);
321
+ if (v === undefined)
322
+ return undefined;
323
+ const n = Number.parseInt(v, 10);
324
+ if (!Number.isFinite(n))
325
+ fail(`flag ${name} expects an integer, got '${v}'`, undefined, EXIT.USAGE);
326
+ return n;
327
+ }
328
+ export function flagList(parsed, name) {
329
+ const v = parsed.flags[name];
330
+ if (v === undefined)
331
+ return [];
332
+ return Array.isArray(v) ? v : [v];
333
+ }
334
+ // ─── Field selection (§2 minimal schemas + --fields escape hatch) ────
335
+ /** Parse a repeatable, comma-separated `--fields a,b --fields c` into ["a","b","c"]. */
336
+ export function fieldList(parsed, name = "--fields") {
337
+ return flagList(parsed, name).flatMap(v => v.split(",")).map(s => s.trim()).filter(Boolean);
338
+ }
339
+ /**
340
+ * Resolve which columns a list view should emit. Defaults are the minimal
341
+ * schema; `requested` (from --fields) adds columns, validated against the full
342
+ * `available` set — an unknown field fails loud with the valid set (§2, §6).
343
+ * Output preserves `available` order for stable tables.
344
+ */
345
+ export function resolveColumns(available, defaults, requested, usage) {
346
+ for (const f of requested) {
347
+ if (!available.includes(f)) {
348
+ fail(`unknown field '${f}' for \`${usage}\``, `available fields: ${available.join(", ")}`, EXIT.USAGE);
349
+ }
350
+ }
351
+ const want = new Set([...defaults, ...requested]);
352
+ return available.filter(f => want.has(f));
353
+ }
354
+ /** Project a full row down to the selected columns, preserving column order. */
355
+ export function pick(row, cols) {
356
+ const out = {};
357
+ for (const c of cols)
358
+ out[c] = row[c];
359
+ return out;
360
+ }
361
+ /**
362
+ * Render a SKILL.md from a SkillSpec. The CLI owns the spec, so the committed
363
+ * skill file is generated — never hand-edited — and `build-cli.sh --check`
364
+ * fails CI if the committed copy drifts from this output (§7).
365
+ */
366
+ export function renderSkillDoc(spec) {
367
+ const F = "```";
368
+ const bin = spec.binName;
369
+ const out = [];
370
+ out.push("---");
371
+ out.push(`name: ${spec.name}`);
372
+ out.push(`description: ${spec.description}`);
373
+ out.push("---");
374
+ out.push("");
375
+ out.push(`# ${bin}`);
376
+ out.push("");
377
+ out.push(spec.intro);
378
+ out.push(`Invoke as \`${bin}\` (on PATH after a global install; no install: \`npx -y @oasissys/${spec.name}\`). TOON output; exit 0/1/2; every command has \`--help\`.`);
379
+ if (spec.extra && spec.extra.length) {
380
+ out.push("");
381
+ out.push(...spec.extra);
382
+ }
383
+ out.push("");
384
+ out.push("## Commands");
385
+ out.push("");
386
+ out.push("| Command | What it does |");
387
+ out.push("|---|---|");
388
+ for (const c of spec.commands) {
389
+ out.push(`| \`${bin} ${c.cmd}\` | ${c.summary} |`);
390
+ }
391
+ out.push("");
392
+ out.push("## Examples");
393
+ out.push("");
394
+ out.push(F + "sh");
395
+ out.push(...spec.examples);
396
+ out.push(F);
397
+ if (spec.notes && spec.notes.length) {
398
+ out.push("");
399
+ out.push("## Notes");
400
+ for (const n of spec.notes)
401
+ out.push(`- ${n}`);
402
+ }
403
+ out.push("");
404
+ return out.join("\n");
405
+ }
406
+ //# sourceMappingURL=axi.js.map