@danypops/jittor 0.9.0 → 0.11.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.
- package/README.md +9 -6
- package/extension/src/benchmark-tui.ts +4 -0
- package/extension/src/capabilities/codex-recovery.ts +127 -0
- package/extension/src/capabilities/http-headers.ts +5 -0
- package/extension/src/capabilities/local-run-telemetry.ts +104 -0
- package/extension/src/capabilities/provider-response-telemetry.ts +96 -0
- package/extension/src/footer.ts +10 -53
- package/extension/src/index.ts +100 -276
- package/extension/src/session-identity.ts +20 -0
- package/extension/src/tui.ts +20 -11
- package/package.json +1 -1
- package/src/adapters/sqlite-metric-store.ts +11 -2
- package/src/adapters/sqlite-session-identity-store.ts +45 -0
- package/src/cli-commands/benchmarks.ts +140 -0
- package/src/cli-commands/compaction.ts +17 -0
- package/src/cli-commands/context.ts +49 -0
- package/src/cli-commands/metrics.ts +296 -0
- package/src/cli-commands/op.ts +40 -0
- package/src/cli-commands/route-args.ts +15 -0
- package/src/cli-commands/router.ts +207 -0
- package/src/cli-commands/service-daemon.ts +72 -0
- package/src/cli-commands/session.ts +42 -0
- package/src/cli-commands/support.ts +33 -0
- package/src/cli.ts +42 -766
- package/src/constants.ts +7 -0
- package/src/daemon.ts +13 -3
- package/src/db.ts +15 -1
- package/src/domain/task-cost.ts +44 -9
- package/src/operations/benchmark-operations.ts +12 -0
- package/src/operations/context-operations.ts +30 -0
- package/src/operations/metrics-operations.ts +77 -0
- package/src/operations/model-ranking-operations.ts +16 -0
- package/src/operations/router-operations.ts +19 -0
- package/src/operations/session-identity-operations.ts +15 -0
- package/src/operations/session-scope.ts +31 -0
- package/src/operations/types.ts +3 -0
- package/src/ports/metric-store.ts +2 -0
- package/src/ports/router-controller.ts +9 -9
- package/src/ports/session-identity-store.ts +5 -0
- package/src/providers/telemetry-sources.ts +2 -1
- package/src/router.ts +124 -67
- package/src/service.ts +60 -118
- package/src/session-identity-service.ts +55 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { EXPECTED_OPERATION_NAMES, type OperationName } from "../service.ts";
|
|
2
|
+
import type { CliDependencies } from "./support.ts";
|
|
3
|
+
|
|
4
|
+
export const OP_USAGE_LINES = [" op <operation> [--input <json>]"];
|
|
5
|
+
|
|
6
|
+
function parseOpArgs(args: string[]): { operation: OperationName; input: Record<string, unknown> } | null {
|
|
7
|
+
const [operation, ...rest] = args;
|
|
8
|
+
if (operation === undefined || !EXPECTED_OPERATION_NAMES.includes(operation as OperationName)) return null;
|
|
9
|
+
let input: Record<string, unknown> = {};
|
|
10
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
11
|
+
if (rest[index] !== "--input") return null;
|
|
12
|
+
const raw = rest[++index];
|
|
13
|
+
if (raw === undefined) return null;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
17
|
+
input = parsed as Record<string, unknown>;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { operation: operation as OperationName, input };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runOpCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
26
|
+
const parsed = parseOpArgs(action === undefined ? [] : [action, ...rest]);
|
|
27
|
+
if (!parsed) return usage();
|
|
28
|
+
try {
|
|
29
|
+
// The escape hatch dispatches a dynamically named operation; OperationInputs/OperationOutputs
|
|
30
|
+
// are only known statically per literal operation name, so this one call site is intentionally
|
|
31
|
+
// untyped at the boundary. parseOpArgs already restricts `operation` to EXPECTED_OPERATION_NAMES.
|
|
32
|
+
const call = deps.client.call as (operation: OperationName, input: Record<string, unknown>) => Promise<unknown>;
|
|
33
|
+
const result = await call(parsed.operation, parsed.input);
|
|
34
|
+
deps.stdout(JSON.stringify(result));
|
|
35
|
+
return 0;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
deps.stderr(error instanceof Error ? error.message : String(error));
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ModelCandidate } from "../domain/model-ranking.ts";
|
|
2
|
+
import type { Route } from "../policy.ts";
|
|
3
|
+
|
|
4
|
+
/** Shared `provider/model@thinking` parsing for router and benchmark CLI arguments -- a Route and a ModelCandidate are structurally identical at this boundary. */
|
|
5
|
+
export function parseCandidate(raw: string): ModelCandidate | null {
|
|
6
|
+
const separator = raw.indexOf("/");
|
|
7
|
+
const thinkingSeparator = raw.lastIndexOf("@");
|
|
8
|
+
if (separator <= 0 || thinkingSeparator <= separator + 1 || thinkingSeparator === raw.length - 1) return null;
|
|
9
|
+
return { provider: raw.slice(0, separator), model: raw.slice(separator + 1, thinkingSeparator), thinking: raw.slice(thinkingSeparator + 1) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseRoute(raw: string | undefined): Route | null {
|
|
13
|
+
if (raw === undefined) return null;
|
|
14
|
+
return parseCandidate(raw);
|
|
15
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { CLI_AVAILABLE_ROUTES_MAX } from "../constants.ts";
|
|
2
|
+
import type { PolicyDecision, Route } from "../policy.ts";
|
|
3
|
+
import type { RouteOverride, RouterStatus, TelemetryPollResult } from "../ports/router-controller.ts";
|
|
4
|
+
import { callAndPrint, humanField, type CliDependencies } from "./support.ts";
|
|
5
|
+
import { parseRoute } from "./route-args.ts";
|
|
6
|
+
|
|
7
|
+
export const ROUTER_USAGE_LINES = [
|
|
8
|
+
" telemetry poll [--json]",
|
|
9
|
+
" compaction estimate [--json]",
|
|
10
|
+
" router <status|decide|pause|resume|clear-override> [--session-id <id>] [--session-secret <secret>] [--json]",
|
|
11
|
+
" router override --route <provider/model@thinking> [--expires-at <ms>] [--session-id <id>] [--session-secret <secret>] [--json]",
|
|
12
|
+
" router current-route --route <provider/model@thinking> [--session-id <id>] [--session-secret <secret>] [--json]",
|
|
13
|
+
" router available-routes [--route <provider/model@thinking> ...] [--session-id <id>] [--session-secret <secret>] [--json]",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
interface SessionScope { session_id?: string; session_secret?: string }
|
|
17
|
+
|
|
18
|
+
function sessionScopeInput(sessionId: string | undefined, sessionSecret: string | undefined): SessionScope {
|
|
19
|
+
return { ...(sessionId ? { session_id: sessionId } : {}), ...(sessionSecret ? { session_secret: sessionSecret } : {}) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RouterOverrideArgs { input: RouteOverride & SessionScope; json: boolean }
|
|
23
|
+
|
|
24
|
+
function parseRouterOverrideArgs(args: string[]): RouterOverrideArgs | null {
|
|
25
|
+
let json = false;
|
|
26
|
+
let route: Route | null = null;
|
|
27
|
+
let expiresAt: number | null = null;
|
|
28
|
+
let sessionId: string | undefined;
|
|
29
|
+
let sessionSecret: string | undefined;
|
|
30
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
31
|
+
const argument = args[index];
|
|
32
|
+
if (argument === "--json") { json = true; continue; }
|
|
33
|
+
if (!["--route", "--expires-at", "--session-id", "--session-secret"].includes(argument ?? "")) return null;
|
|
34
|
+
const raw = args[++index];
|
|
35
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
36
|
+
if (argument === "--session-id") sessionId = raw;
|
|
37
|
+
else if (argument === "--session-secret") sessionSecret = raw;
|
|
38
|
+
else if (argument === "--route") {
|
|
39
|
+
route = parseRoute(raw);
|
|
40
|
+
if (!route) return null;
|
|
41
|
+
} else {
|
|
42
|
+
const parsed = Number(raw);
|
|
43
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
44
|
+
expiresAt = parsed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!route) return null;
|
|
48
|
+
return { input: { route, expiresAt, ...sessionScopeInput(sessionId, sessionSecret) }, json };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface RouterRouteArgs { input: Route & SessionScope; json: boolean }
|
|
52
|
+
|
|
53
|
+
function parseRouterRouteArgs(args: string[]): RouterRouteArgs | null {
|
|
54
|
+
let json = false;
|
|
55
|
+
let route: Route | null = null;
|
|
56
|
+
let sessionId: string | undefined;
|
|
57
|
+
let sessionSecret: string | undefined;
|
|
58
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
59
|
+
const argument = args[index];
|
|
60
|
+
if (argument === "--json") { json = true; continue; }
|
|
61
|
+
if (!["--route", "--session-id", "--session-secret"].includes(argument ?? "")) return null;
|
|
62
|
+
const raw = args[++index];
|
|
63
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
64
|
+
if (argument === "--session-id") sessionId = raw;
|
|
65
|
+
else if (argument === "--session-secret") sessionSecret = raw;
|
|
66
|
+
else {
|
|
67
|
+
route = parseRoute(raw);
|
|
68
|
+
if (!route) return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!route) return null;
|
|
72
|
+
return { input: { ...route, ...sessionScopeInput(sessionId, sessionSecret) }, json };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface RouterAvailableRoutesArgs { input: { routes: Route[] } & SessionScope; json: boolean }
|
|
76
|
+
|
|
77
|
+
function parseRouterAvailableRoutesArgs(args: string[]): RouterAvailableRoutesArgs | null {
|
|
78
|
+
let json = false;
|
|
79
|
+
let sessionId: string | undefined;
|
|
80
|
+
let sessionSecret: string | undefined;
|
|
81
|
+
const routes: Route[] = [];
|
|
82
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
83
|
+
const argument = args[index];
|
|
84
|
+
if (argument === "--json") { json = true; continue; }
|
|
85
|
+
if (!["--route", "--session-id", "--session-secret"].includes(argument ?? "")) return null;
|
|
86
|
+
const raw = args[++index];
|
|
87
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
88
|
+
if (argument === "--session-id") sessionId = raw;
|
|
89
|
+
else if (argument === "--session-secret") sessionSecret = raw;
|
|
90
|
+
else {
|
|
91
|
+
const route = parseRoute(raw);
|
|
92
|
+
if (!route) return null;
|
|
93
|
+
if (routes.length >= CLI_AVAILABLE_ROUTES_MAX) return null;
|
|
94
|
+
routes.push(route);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { input: { routes, ...sessionScopeInput(sessionId, sessionSecret) }, json };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseRouterScopeArgs(args: string[]): { input: SessionScope; json: boolean } | null {
|
|
101
|
+
let json = false;
|
|
102
|
+
let sessionId: string | undefined;
|
|
103
|
+
let sessionSecret: string | undefined;
|
|
104
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
105
|
+
const argument = args[index];
|
|
106
|
+
if (argument === "--json") { json = true; continue; }
|
|
107
|
+
if (!["--session-id", "--session-secret"].includes(argument ?? "")) return null;
|
|
108
|
+
const raw = args[++index];
|
|
109
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
110
|
+
if (argument === "--session-id") sessionId = raw;
|
|
111
|
+
else sessionSecret = raw;
|
|
112
|
+
}
|
|
113
|
+
return { input: sessionScopeInput(sessionId, sessionSecret), json };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function parseJsonOnlyArgs(args: string[]): { json: boolean } | null {
|
|
117
|
+
let json = false;
|
|
118
|
+
for (const argument of args) {
|
|
119
|
+
if (argument !== "--json") return null;
|
|
120
|
+
json = true;
|
|
121
|
+
}
|
|
122
|
+
return { json };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatRoute(route: Route): string {
|
|
126
|
+
return `${humanField(route.provider)}/${humanField(route.model)} · ${humanField(route.thinking)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatTelemetryPoll(result: TelemetryPollResult): string {
|
|
130
|
+
if (result.sources.length === 0) return "Telemetry: no sources configured";
|
|
131
|
+
return ["Telemetry:", ...result.sources.map((source) => {
|
|
132
|
+
const freshness = !source.ok ? `failed${source.error ? ` (${humanField(source.error)})` : ""}` : "ok";
|
|
133
|
+
return `- ${humanField(source.id)} (${humanField(source.provider)}): ${freshness} · ${source.metrics} metric(s)`;
|
|
134
|
+
})].join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatRouterStatus(status: RouterStatus): string {
|
|
138
|
+
const lines = [
|
|
139
|
+
`Router: ${status.ready ? "ready" : "not ready"}${status.paused ? " · paused" : ""}`,
|
|
140
|
+
`Current route: ${status.currentRoute ? formatRoute(status.currentRoute) : "none"}`,
|
|
141
|
+
`Available routes: ${status.availableRoutes.length.toLocaleString()}`,
|
|
142
|
+
`Override: ${status.override ? `${formatRoute(status.override.route)}${status.override.expiresAt === null ? "" : ` (expires ${new Date(status.override.expiresAt).toISOString()})`}` : "none"}`,
|
|
143
|
+
];
|
|
144
|
+
if (status.lastDecision) lines.push(`Last decision: ${status.lastDecision.action} · pressure ${Number.isFinite(status.lastDecision.pressure) ? status.lastDecision.pressure.toFixed(3) : "∞"} · ${humanField(status.lastDecision.reason)}`);
|
|
145
|
+
lines.push(formatTelemetryPoll({ sources: status.sources, observedAt: Date.now() }));
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatPolicyDecision(decision: PolicyDecision): string {
|
|
150
|
+
const lines = [`Decision: ${decision.action} · pressure ${Number.isFinite(decision.pressure) ? decision.pressure.toFixed(3) : "∞"} · ${humanField(decision.reason)}`];
|
|
151
|
+
if (decision.route) lines.push(`Route: ${formatRoute(decision.route)}`);
|
|
152
|
+
if (decision.delayMs !== undefined) lines.push(`Delay: ${decision.delayMs}ms`);
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function runTelemetryCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
157
|
+
if (action !== "poll") return usage();
|
|
158
|
+
const parsed = parseJsonOnlyArgs(rest);
|
|
159
|
+
if (!parsed) return usage();
|
|
160
|
+
return callAndPrint(deps, "telemetry.poll", {}, parsed.json, formatTelemetryPoll);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function runRouterCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
164
|
+
switch (action) {
|
|
165
|
+
case "status": {
|
|
166
|
+
const parsed = parseRouterScopeArgs(rest);
|
|
167
|
+
if (!parsed) return usage();
|
|
168
|
+
return callAndPrint(deps, "router.status", parsed.input, parsed.json, formatRouterStatus);
|
|
169
|
+
}
|
|
170
|
+
case "decide": {
|
|
171
|
+
const parsed = parseRouterScopeArgs(rest);
|
|
172
|
+
if (!parsed) return usage();
|
|
173
|
+
return callAndPrint(deps, "router.decide", parsed.input, parsed.json, formatPolicyDecision);
|
|
174
|
+
}
|
|
175
|
+
case "pause": {
|
|
176
|
+
const parsed = parseRouterScopeArgs(rest);
|
|
177
|
+
if (!parsed) return usage();
|
|
178
|
+
return callAndPrint(deps, "router.pause", parsed.input, parsed.json, formatRouterStatus);
|
|
179
|
+
}
|
|
180
|
+
case "resume": {
|
|
181
|
+
const parsed = parseRouterScopeArgs(rest);
|
|
182
|
+
if (!parsed) return usage();
|
|
183
|
+
return callAndPrint(deps, "router.resume", parsed.input, parsed.json, formatRouterStatus);
|
|
184
|
+
}
|
|
185
|
+
case "clear-override": {
|
|
186
|
+
const parsed = parseRouterScopeArgs(rest);
|
|
187
|
+
if (!parsed) return usage();
|
|
188
|
+
return callAndPrint(deps, "router.clear_override", parsed.input, parsed.json, formatRouterStatus);
|
|
189
|
+
}
|
|
190
|
+
case "override": {
|
|
191
|
+
const parsed = parseRouterOverrideArgs(rest);
|
|
192
|
+
if (!parsed) return usage();
|
|
193
|
+
return callAndPrint(deps, "router.override", parsed.input, parsed.json, formatRouterStatus);
|
|
194
|
+
}
|
|
195
|
+
case "current-route": {
|
|
196
|
+
const parsed = parseRouterRouteArgs(rest);
|
|
197
|
+
if (!parsed) return usage();
|
|
198
|
+
return callAndPrint(deps, "router.current_route", parsed.input, parsed.json, formatRouterStatus);
|
|
199
|
+
}
|
|
200
|
+
case "available-routes": {
|
|
201
|
+
const parsed = parseRouterAvailableRoutesArgs(rest);
|
|
202
|
+
if (!parsed) return usage();
|
|
203
|
+
return callAndPrint(deps, "router.available_routes", parsed.input, parsed.json, formatRouterStatus);
|
|
204
|
+
}
|
|
205
|
+
default: return usage();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { SYSTEMD_UNIT_NAME } from "../constants.ts";
|
|
6
|
+
import { resolveJittorPaths } from "../state.ts";
|
|
7
|
+
import { callAndPrint, type CliDependencies } from "./support.ts";
|
|
8
|
+
import { parseJsonOnlyArgs } from "./router.ts";
|
|
9
|
+
|
|
10
|
+
export const SERVICE_USAGE_LINES = [" service <install|start|stop|restart|status|checkpoint>"];
|
|
11
|
+
|
|
12
|
+
export interface SystemdUnitOptions {
|
|
13
|
+
bunBin: string;
|
|
14
|
+
cliPath: string;
|
|
15
|
+
codexAuthFile?: string;
|
|
16
|
+
openRouterBenchmarks?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function renderSystemdUnit(options: SystemdUnitOptions): string {
|
|
20
|
+
return `[Unit]
|
|
21
|
+
Description=Jittor token optimizing router
|
|
22
|
+
After=default.target network-online.target
|
|
23
|
+
Wants=network-online.target
|
|
24
|
+
|
|
25
|
+
[Service]
|
|
26
|
+
Type=simple
|
|
27
|
+
ExecStart=${options.bunBin} ${options.cliPath} serve
|
|
28
|
+
${options.codexAuthFile ? `Environment="JITTOR_CODEX_AUTH_FILE=${options.codexAuthFile.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"\n` : ""}${options.openRouterBenchmarks ? "Environment=JITTOR_OPENROUTER_BENCHMARKS=1\n" : ""}Restart=always
|
|
29
|
+
RestartSec=2
|
|
30
|
+
NoNewPrivileges=true
|
|
31
|
+
PrivateTmp=true
|
|
32
|
+
|
|
33
|
+
[Install]
|
|
34
|
+
WantedBy=default.target
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function systemctl(...args: string[]): void {
|
|
39
|
+
execFileSync("systemctl", ["--user", ...args], { stdio: "inherit" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** cliPath is the caller's own entrypoint file -- resolved from the real CLI script's `import.meta.url`, never this module's own, so the installed unit's ExecStart always points at the actual runnable CLI. */
|
|
43
|
+
export function installService(cliPath: string): void {
|
|
44
|
+
const unitPath = resolveJittorPaths().systemdUnit;
|
|
45
|
+
mkdirSync(dirname(unitPath), { recursive: true });
|
|
46
|
+
const codexAuthFile = join(process.env["CODEX_HOME"] ?? join(homedir(), ".codex"), "auth.json");
|
|
47
|
+
writeFileSync(unitPath, renderSystemdUnit({
|
|
48
|
+
bunBin: process.execPath,
|
|
49
|
+
cliPath,
|
|
50
|
+
...(existsSync(codexAuthFile) ? { codexAuthFile } : {}),
|
|
51
|
+
openRouterBenchmarks: process.env["JITTOR_OPENROUTER_BENCHMARKS"] === "1",
|
|
52
|
+
}));
|
|
53
|
+
systemctl("daemon-reload");
|
|
54
|
+
systemctl("enable", SYSTEMD_UNIT_NAME);
|
|
55
|
+
systemctl("restart", SYSTEMD_UNIT_NAME);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runServiceCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
59
|
+
switch (action) {
|
|
60
|
+
case "install": deps.installService(); return 0;
|
|
61
|
+
case "start": deps.systemctl("start", SYSTEMD_UNIT_NAME); return 0;
|
|
62
|
+
case "stop": deps.systemctl("stop", SYSTEMD_UNIT_NAME); return 0;
|
|
63
|
+
case "restart": deps.systemctl("restart", SYSTEMD_UNIT_NAME); return 0;
|
|
64
|
+
case "status": deps.systemctl("status", SYSTEMD_UNIT_NAME); return 0;
|
|
65
|
+
case "checkpoint": {
|
|
66
|
+
const parsed = parseJsonOnlyArgs(rest);
|
|
67
|
+
if (!parsed) return usage();
|
|
68
|
+
return callAndPrint(deps, "service.checkpoint", {}, parsed.json, () => "Checkpoint complete");
|
|
69
|
+
}
|
|
70
|
+
default: return usage();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { callAndPrint, humanField, type CliDependencies } from "./support.ts";
|
|
2
|
+
|
|
3
|
+
export const SESSION_USAGE_LINES = [
|
|
4
|
+
" session register --session-id <id> [--json]",
|
|
5
|
+
" session release --session-id <id> [--session-secret <secret>] [--json]",
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
interface SessionArgs { input: { session_id: string; session_secret?: string }; json: boolean }
|
|
9
|
+
|
|
10
|
+
function parseSessionArgs(args: string[]): SessionArgs | null {
|
|
11
|
+
let json = false;
|
|
12
|
+
let sessionId: string | undefined;
|
|
13
|
+
let sessionSecret: string | undefined;
|
|
14
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
15
|
+
const argument = args[index];
|
|
16
|
+
if (argument === "--json") { json = true; continue; }
|
|
17
|
+
if (!["--session-id", "--session-secret"].includes(argument ?? "")) return null;
|
|
18
|
+
const raw = args[++index];
|
|
19
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
20
|
+
if (argument === "--session-id") sessionId = raw;
|
|
21
|
+
else sessionSecret = raw;
|
|
22
|
+
}
|
|
23
|
+
if (sessionId === undefined) return null;
|
|
24
|
+
return { input: { session_id: sessionId, ...(sessionSecret ? { session_secret: sessionSecret } : {}) }, json };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatSessionRegistration(result: { sessionId: string; secret: string }): string {
|
|
28
|
+
return `Session registered: ${humanField(result.sessionId)} · secret ${humanField(result.secret)} (shown once; keep it to mutate this session's router state)`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatSessionRelease(result: { released: boolean }): string {
|
|
32
|
+
return result.released ? "Session released" : "Session was not registered, or the secret did not match";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runSessionCommand(action: string | undefined, rest: string[], deps: CliDependencies, usage: () => number): Promise<number> {
|
|
36
|
+
if (action !== "register" && action !== "release") return usage();
|
|
37
|
+
const parsed = parseSessionArgs(rest);
|
|
38
|
+
if (!parsed) return usage();
|
|
39
|
+
return action === "register"
|
|
40
|
+
? callAndPrint(deps, "session.register", parsed.input, parsed.json, formatSessionRegistration)
|
|
41
|
+
: callAndPrint(deps, "session.release", parsed.input, parsed.json, formatSessionRelease);
|
|
42
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { HUMAN_TEXT_FIELD_MAX_CHARACTERS } from "../constants.ts";
|
|
2
|
+
import type { JittorClient } from "../client.ts";
|
|
3
|
+
import type { OperationInputs, OperationName, OperationOutputs } from "../service.ts";
|
|
4
|
+
|
|
5
|
+
export interface CliDependencies {
|
|
6
|
+
client: Pick<JittorClient, "call">;
|
|
7
|
+
stdout(line: string): void;
|
|
8
|
+
stderr(line: string): void;
|
|
9
|
+
systemctl(...args: string[]): void;
|
|
10
|
+
installService(): void;
|
|
11
|
+
serve(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function humanField(value: string): string {
|
|
15
|
+
return value.length <= HUMAN_TEXT_FIELD_MAX_CHARACTERS ? value : `${value.slice(0, HUMAN_TEXT_FIELD_MAX_CHARACTERS - 1)}…`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function callAndPrint<Name extends OperationName>(
|
|
19
|
+
deps: CliDependencies,
|
|
20
|
+
operation: Name,
|
|
21
|
+
input: OperationInputs[Name],
|
|
22
|
+
json: boolean,
|
|
23
|
+
formatHuman: (result: OperationOutputs[Name]) => string,
|
|
24
|
+
): Promise<number> {
|
|
25
|
+
try {
|
|
26
|
+
const result = await deps.client.call(operation, input);
|
|
27
|
+
deps.stdout(json ? JSON.stringify(result) : formatHuman(result));
|
|
28
|
+
return 0;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
deps.stderr(error instanceof Error ? error.message : String(error));
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
}
|