@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 +21 -0
- package/README.md +84 -0
- package/dist/bin/oasis-k8s.d.ts +2 -0
- package/dist/bin/oasis-k8s.js +4 -0
- package/dist/bin/oasis-k8s.js.map +1 -0
- package/dist/src/axi.d.ts +122 -0
- package/dist/src/axi.js +406 -0
- package/dist/src/axi.js.map +1 -0
- package/dist/src/cli.d.ts +16 -0
- package/dist/src/cli.js +768 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/client.d.ts +158 -0
- package/dist/src/client.js +185 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/config.d.ts +13 -0
- package/dist/src/config.js +20 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/storage.d.ts +22 -0
- package/dist/src/storage.js +88 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/toon.d.ts +19 -0
- package/dist/src/toon.js +148 -0
- package/dist/src/toon.js.map +1 -0
- package/package.json +55 -0
- package/skills/k8s-cli/SKILL.md +63 -0
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 @@
|
|
|
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;
|
package/dist/src/axi.js
ADDED
|
@@ -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
|