@gajae-gateway/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ import { type RuntimeConfig } from "@gajae-gateway/protocol";
2
+ export declare const FALLBACK_PATH_DIRS: readonly ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
3
+ export type LoginPathRunner = (command: readonly string[]) => string | PromiseLike<string>;
4
+ export type PlistWriter = (path: string, contents: string) => void | PromiseLike<void>;
5
+ export interface ResolvePathOptions {
6
+ readonly binDir: string;
7
+ /** The operator home used for managed PATH entries and `~/` expansion. */
8
+ readonly home: string;
9
+ readonly runtime?: RuntimeConfig;
10
+ readonly env?: NodeJS.ProcessEnv;
11
+ /** A supplied PATH bypasses shell discovery and is useful for pure tests. */
12
+ readonly loginPath?: string;
13
+ /** Test seam for the `${SHELL} -lc` invocation used by the default path. */
14
+ readonly loginPathRunner?: LoginPathRunner;
15
+ }
16
+ export interface InstallServicesOptions {
17
+ readonly binDir: string;
18
+ readonly launchAgentsDir?: string;
19
+ readonly env?: NodeJS.ProcessEnv;
20
+ /** Test seam for the `${SHELL} -lc` invocation used by the default path. */
21
+ readonly loginPathRunner?: LoginPathRunner;
22
+ /** Test seam for counting or inspecting the plist writes. */
23
+ readonly writeFile?: PlistWriter;
24
+ }
25
+ interface ServiceSpec {
26
+ readonly id: string;
27
+ readonly label: string;
28
+ readonly binary: string;
29
+ readonly args: readonly string[];
30
+ }
31
+ export declare function mergePathEntries(binDir: string, userHome: string, loginPath: string): string[];
32
+ export declare function defaultLoginPathRunner(command: readonly string[]): Promise<string>;
33
+ export declare function discoverLoginPath(env?: NodeJS.ProcessEnv, runner?: LoginPathRunner): Promise<string>;
34
+ export declare function resolvePath(options: ResolvePathOptions): Promise<string>;
35
+ export declare function escapeXml(value: string): string;
36
+ export declare function renderLaunchAgent(spec: ServiceSpec, binDir: string, home: string, path: string): string;
37
+ export declare function installServices(options: InstallServicesOptions): Promise<readonly string[]>;
38
+ export declare function serviceUsage(): string;
39
+ export {};
40
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../src/services.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,aAAa,EAAsB,MAAM,yBAAyB,CAAC;AAIrG,eAAO,MAAM,kBAAkB,uEAAwE,CAAC;AAGxG,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;AAC3F,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;AAEvF,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;CAC3C;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC;CACjC;AAED,UAAU,WAAW;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAwCD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9F;AAED,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAYxF;AAED,wBAAsB,iBAAiB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,MAAM,GAAE,eAAwC,GAC9C,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAW9E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO/C;AAMD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBvG;AA6BD,wBAAsB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CA6BjG;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@gajae-gateway/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Owner CLI for the gajaeway gateway: status, sessions, chat, monitors, and worker lanes over the gateway socket. Requires the Bun runtime (uses Bun.file/Bun.spawn).",
6
+ "license": "AGPL-3.0-only",
7
+ "author": "Yeachan Heo",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Yeachan-Heo/gajae-way.git",
11
+ "directory": "packages/cli"
12
+ },
13
+ "homepage": "https://github.com/Yeachan-Heo/gajae-way/tree/main/packages/cli#readme",
14
+ "bugs": "https://github.com/Yeachan-Heo/gajae-way/issues",
15
+ "keywords": [
16
+ "gajaeway",
17
+ "cli",
18
+ "gateway"
19
+ ],
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "main": "./dist/main.js",
25
+ "types": "./dist/main.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "bun": "./src/main.ts",
29
+ "types": "./dist/main.d.ts",
30
+ "default": "./dist/main.js"
31
+ }
32
+ },
33
+ "bin": {
34
+ "gajaeway": "./dist/main.js"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "rm -rf dist && bun build src/main.ts --outdir dist --target bun --format esm --external @gajae-gateway/protocol --external @gajae-gateway/sdk && tsc -p tsconfig.build.json",
41
+ "prepublishOnly": "bun run build"
42
+ },
43
+ "dependencies": {
44
+ "@gajae-gateway/sdk": "0.1.0",
45
+ "@gajae-gateway/protocol": "0.1.0"
46
+ },
47
+ "engines": {
48
+ "bun": ">=1.4.0"
49
+ }
50
+ }
package/src/list.ts ADDED
@@ -0,0 +1,187 @@
1
+ import type { MonitorRecord } from "@gajae-gateway/protocol";
2
+ import { originKey } from "@gajae-gateway/protocol";
3
+
4
+ /**
5
+ * Presentation layer for `gajaeway monitors list` / `sessions list`.
6
+ *
7
+ * Rendering contract:
8
+ * - Default output is a padded column table, one record per line, so agent
9
+ * shells with a ~1KB visible-output cap can read it without an artifact
10
+ * refetch or an external jq/python filter.
11
+ * - `--json` keeps the machine shape: the gateway envelope verbatim when no
12
+ * projection flag is used, and the same envelope with a projected/paged
13
+ * array when `--fields`/`--limit`/`--offset` are present.
14
+ * - `--fields` selects columns by name; an unknown name is a usage error that
15
+ * lists every valid name.
16
+ */
17
+
18
+ /** Widest rendered cell before the value is elided; keeps rows terminal-sized. */
19
+ const MAX_CELL_WIDTH = 40;
20
+
21
+ export interface Column<T> {
22
+ readonly name: string;
23
+ /** Raw value used verbatim by `--json` projection. */
24
+ readonly value: (row: T, index: number) => unknown;
25
+ }
26
+
27
+ export interface ListOptions {
28
+ readonly json: boolean;
29
+ readonly fields?: readonly string[];
30
+ readonly limit?: number;
31
+ readonly offset: number;
32
+ }
33
+
34
+ export function columnNames<T>(columns: readonly Column<T>[]): string[] {
35
+ return columns.map((column) => column.name);
36
+ }
37
+
38
+ function positiveInteger(flag: string, raw: string | undefined): number {
39
+ const value = Number(raw);
40
+ if (raw === undefined || raw === "" || !Number.isInteger(value) || value < 0)
41
+ throw new Error(`${flag} expects a non-negative integer, got: ${raw ?? "(missing)"}`);
42
+ return value;
43
+ }
44
+
45
+ export function parseListOptions<T>(args: readonly string[], columns: readonly Column<T>[]): ListOptions {
46
+ const valid = columnNames(columns);
47
+ let json = false;
48
+ let fields: string[] | undefined;
49
+ let limit: number | undefined;
50
+ let offset = 0;
51
+ for (let i = 0; i < args.length; i++) {
52
+ const arg = args[i] as string;
53
+ if (arg === "--json") json = true;
54
+ else if (arg === "--fields") {
55
+ const raw = args[++i];
56
+ if (!raw) throw new Error(`--fields expects a comma-separated column list (valid: ${valid.join(", ")})`);
57
+ fields = raw
58
+ .split(",")
59
+ .map((field) => field.trim())
60
+ .filter((field) => field.length > 0);
61
+ if (fields.length === 0)
62
+ throw new Error(`--fields expects a comma-separated column list (valid: ${valid.join(", ")})`);
63
+ const unknown = fields.filter((field) => !valid.includes(field));
64
+ if (unknown.length > 0) throw new Error(`unknown field(s): ${unknown.join(", ")} (valid: ${valid.join(", ")})`);
65
+ } else if (arg === "--limit") limit = positiveInteger("--limit", args[++i]);
66
+ else if (arg === "--offset") offset = positiveInteger("--offset", args[++i]);
67
+ else throw new Error(`unknown option: ${arg}`);
68
+ }
69
+ return { json, ...(fields ? { fields } : {}), ...(limit === undefined ? {} : { limit }), offset };
70
+ }
71
+
72
+ export function selectColumns<T>(columns: readonly Column<T>[], fields?: readonly string[]): Column<T>[] {
73
+ if (!fields) return [...columns];
74
+ return fields.map((field) => columns.find((column) => column.name === field) as Column<T>);
75
+ }
76
+
77
+ /** Rows survive paging with their original index so `index` columns stay stable. */
78
+ export function pageRows<T>(rows: readonly T[], options: ListOptions): Array<{ row: T; index: number }> {
79
+ const entries = rows.map((row, index) => ({ row, index }));
80
+ const start = Math.min(options.offset, entries.length);
81
+ return options.limit === undefined ? entries.slice(start) : entries.slice(start, start + options.limit);
82
+ }
83
+
84
+ /** `lastActivityAt` reads as `LAST_ACTIVITY_AT`, not as an unreadable letter run. */
85
+ export function headerLabel(name: string): string {
86
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
87
+ }
88
+
89
+ export function formatCell(value: unknown): string {
90
+ if (value === null || value === undefined) return "-";
91
+ const text = Array.isArray(value) ? (value.length === 0 ? "-" : value.join(",")) : String(value);
92
+ if (text === "") return "-";
93
+ return text.length > MAX_CELL_WIDTH ? `${text.slice(0, MAX_CELL_WIDTH - 1)}…` : text;
94
+ }
95
+
96
+ export function renderTable<T>(
97
+ columns: readonly Column<T>[],
98
+ entries: ReadonlyArray<{ row: T; index: number }>,
99
+ ): string[] {
100
+ if (columns.length === 0) return ["(no columns)"];
101
+ const header = columns.map((column) => headerLabel(column.name));
102
+ const body = entries.map((entry) => columns.map((column) => formatCell(column.value(entry.row, entry.index))));
103
+ if (body.length === 0) return [header.join(" "), "(none)"];
104
+ const widths = header.map((cell, index) =>
105
+ Math.max(cell.length, ...body.map((row) => (row[index] as string).length)),
106
+ );
107
+ const line = (cells: string[]): string =>
108
+ cells
109
+ .map((cell, index) => (index === cells.length - 1 ? cell : cell.padEnd(widths[index] as number)))
110
+ .join(" ")
111
+ .trimEnd();
112
+ return [line(header), ...body.map(line)];
113
+ }
114
+
115
+ export function projectRows<T>(
116
+ columns: readonly Column<T>[],
117
+ entries: ReadonlyArray<{ row: T; index: number }>,
118
+ ): Array<Record<string, unknown>> {
119
+ return entries.map((entry) =>
120
+ Object.fromEntries(columns.map((column) => [column.name, column.value(entry.row, entry.index)])),
121
+ );
122
+ }
123
+
124
+ function triggerSummary(monitor: MonitorRecord): string {
125
+ const trigger = monitor.trigger;
126
+ switch (trigger.kind) {
127
+ case "cron":
128
+ return trigger.schedule;
129
+ case "webhook":
130
+ return `webhook:${trigger.route}`;
131
+ case "watcher":
132
+ return `watch:${trigger.root}`;
133
+ case "script":
134
+ return `script:${trigger.intervalMs}ms`;
135
+ }
136
+ }
137
+
138
+ export const MONITOR_COLUMNS: readonly Column<MonitorRecord>[] = [
139
+ { name: "id", value: (monitor) => monitor.monitorId.slice(0, 8) },
140
+ { name: "name", value: (monitor) => monitor.name },
141
+ { name: "schedule", value: triggerSummary },
142
+ { name: "events", value: (monitor) => [...monitor.eventTypes] },
143
+ { name: "target", value: (monitor) => (monitor.channelTarget ? originKey(monitor.channelTarget.origin) : null) },
144
+ { name: "enabled", value: (monitor) => monitor.enabled },
145
+ ];
146
+
147
+ export interface SessionListRow {
148
+ readonly origin: Parameters<typeof originKey>[0];
149
+ readonly epoch: number;
150
+ readonly createdAt: string;
151
+ readonly lastActivityAt: string | null;
152
+ readonly bootstrap: { readonly pending: boolean; readonly byteCount: number; readonly truncated: boolean };
153
+ }
154
+
155
+ export const SESSION_COLUMNS: readonly Column<SessionListRow>[] = [
156
+ { name: "index", value: (_session, index) => index },
157
+ { name: "origin", value: (session) => originKey(session.origin) },
158
+ { name: "epoch", value: (session) => session.epoch },
159
+ {
160
+ name: "bootstrap",
161
+ value: (session) =>
162
+ session.bootstrap.pending
163
+ ? "pending"
164
+ : `${session.bootstrap.byteCount}B${session.bootstrap.truncated ? "/trunc" : ""}`,
165
+ },
166
+ { name: "createdAt", value: (session) => session.createdAt },
167
+ { name: "lastActivityAt", value: (session) => session.lastActivityAt },
168
+ ];
169
+
170
+ /**
171
+ * Shared `list` output path: table by default, envelope-preserving JSON under
172
+ * `--json`. `envelopeKey` is the array field of the gateway result so the
173
+ * machine shape is identical to the pre-table output.
174
+ */
175
+ export function renderList<T>(
176
+ columns: readonly Column<T>[],
177
+ rows: readonly T[],
178
+ options: ListOptions,
179
+ envelope: { readonly key: string; readonly result: unknown },
180
+ ): string[] {
181
+ const entries = pageRows(rows, options);
182
+ const selected = selectColumns(columns, options.fields);
183
+ if (!options.json) return renderTable(selected, entries);
184
+ if (!options.fields && options.limit === undefined && options.offset === 0) return [JSON.stringify(envelope.result)];
185
+ const projected = options.fields ? projectRows(selected, entries) : entries.map((entry) => entry.row);
186
+ return [JSON.stringify({ ...(envelope.result as object), [envelope.key]: projected })];
187
+ }