@nyxa/automation 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.
- package/README.md +559 -0
- package/dist/cancellation.d.ts +1 -0
- package/dist/cancellation.js +5 -0
- package/dist/claude-code.d.ts +19 -0
- package/dist/claude-code.js +182 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +487 -0
- package/dist/codex-output.d.ts +6 -0
- package/dist/codex-output.js +167 -0
- package/dist/codex.d.ts +17 -0
- package/dist/codex.js +228 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.js +138 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +51 -0
- package/dist/events.d.ts +55 -0
- package/dist/events.js +51 -0
- package/dist/filesystem.d.ts +2 -0
- package/dist/filesystem.js +13 -0
- package/dist/harness-process.d.ts +39 -0
- package/dist/harness-process.js +359 -0
- package/dist/harness-prompt.d.ts +2 -0
- package/dist/harness-prompt.js +14 -0
- package/dist/harness.d.ts +44 -0
- package/dist/harness.js +68 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +305 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +1 -0
- package/dist/kimi-code.d.ts +16 -0
- package/dist/kimi-code.js +299 -0
- package/dist/opencode.d.ts +15 -0
- package/dist/opencode.js +164 -0
- package/dist/run-coordination.d.ts +10 -0
- package/dist/run-coordination.js +96 -0
- package/dist/run-journal.d.ts +14 -0
- package/dist/run-journal.js +129 -0
- package/dist/schema.d.ts +167 -0
- package/dist/schema.js +516 -0
- package/dist/standard-input.d.ts +1 -0
- package/dist/standard-input.js +7 -0
- package/dist/type-utils.d.ts +1 -0
- package/dist/type-utils.js +1 -0
- package/dist/workflow.d.ts +160 -0
- package/dist/workflow.js +530 -0
- package/package.json +51 -0
package/dist/opencode.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { AutomationError } from "./errors.js";
|
|
2
|
+
import { createHarnessPreflight, harnessProcessExitError, runHarnessProcess, } from "./harness-process.js";
|
|
3
|
+
import { createHarness, harnessProcessControls, } from "./harness.js";
|
|
4
|
+
import { promptWithOutputContract } from "./harness-prompt.js";
|
|
5
|
+
const HARNESS_NAME = "OpenCode";
|
|
6
|
+
export const OPENCODE_HARNESS_PREFLIGHT = {
|
|
7
|
+
harnessName: HARNESS_NAME,
|
|
8
|
+
minimumVersion: [1, 18, 4],
|
|
9
|
+
unsupportedVersionMessage: "OpenCode CLI 1.18.4 or later is required.",
|
|
10
|
+
versionPattern: /(?:\bopencode\s+)?(\d+)\.(\d+)\.(\d+)(?:-([0-9a-z.-]+))?(?:\+[0-9a-z.-]+)?(?:\s|$)/iu,
|
|
11
|
+
};
|
|
12
|
+
const preflightOpenCode = createHarnessPreflight(OPENCODE_HARNESS_PREFLIGHT);
|
|
13
|
+
export function opencode(options = {}) {
|
|
14
|
+
const configuredOptions = Object.freeze({ ...options });
|
|
15
|
+
return createHarness({
|
|
16
|
+
name: HARNESS_NAME,
|
|
17
|
+
async run(request) {
|
|
18
|
+
if (request.access !== "full") {
|
|
19
|
+
throw new AutomationError("HARNESS_CAPABILITY_UNSUPPORTED", "OpenCode cannot guarantee read or write-workspace Access Policies because its CLI does not sandbox shell commands to the Workspace.");
|
|
20
|
+
}
|
|
21
|
+
const executable = await preflightOpenCode(configuredOptions.executable ?? "opencode", request.environment, harnessProcessControls(request));
|
|
22
|
+
return executeOpenCode(executable, configuredOptions, request);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function executeOpenCode(executable, options, request) {
|
|
27
|
+
const arguments_ = ["run", "--format", "json"];
|
|
28
|
+
if (options.model !== undefined) {
|
|
29
|
+
arguments_.push("--model", options.model);
|
|
30
|
+
}
|
|
31
|
+
if (options.effort !== undefined) {
|
|
32
|
+
arguments_.push("--variant", options.effort);
|
|
33
|
+
}
|
|
34
|
+
if (request.approval === "auto") {
|
|
35
|
+
arguments_.push("--auto");
|
|
36
|
+
}
|
|
37
|
+
if (request.nativeSessionId !== undefined) {
|
|
38
|
+
arguments_.push("--session", request.nativeSessionId);
|
|
39
|
+
}
|
|
40
|
+
arguments_.push(promptWithOutputContract(request));
|
|
41
|
+
await request.start();
|
|
42
|
+
let nativeSessionId;
|
|
43
|
+
let nativeFailure;
|
|
44
|
+
const textParts = [];
|
|
45
|
+
const result = await runHarnessProcess(executable, arguments_, request.workingDirectory, HARNESS_NAME, {
|
|
46
|
+
environment: request.environment,
|
|
47
|
+
...harnessProcessControls(request),
|
|
48
|
+
async onStdoutLine(line) {
|
|
49
|
+
if (line.trim().length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const event = parseOpenCodeEvent(line);
|
|
53
|
+
await request.emitNativeEvent(event);
|
|
54
|
+
const eventSessionId = opencodeSessionId(event);
|
|
55
|
+
if (nativeSessionId !== undefined &&
|
|
56
|
+
eventSessionId !== nativeSessionId) {
|
|
57
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode emitted events for different Sessions.");
|
|
58
|
+
}
|
|
59
|
+
if (request.nativeSessionId !== undefined &&
|
|
60
|
+
eventSessionId !== request.nativeSessionId) {
|
|
61
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode resumed a different Session than requested.");
|
|
62
|
+
}
|
|
63
|
+
nativeSessionId = eventSessionId;
|
|
64
|
+
const text = completedOpenCodeText(event);
|
|
65
|
+
if (text !== undefined) {
|
|
66
|
+
textParts.push(text);
|
|
67
|
+
}
|
|
68
|
+
nativeFailure ??= opencodeFailureDiagnostic(event);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (result.exitCode !== 0) {
|
|
72
|
+
throw harnessProcessExitError(HARNESS_NAME, {
|
|
73
|
+
...result,
|
|
74
|
+
stderr: result.stderr.trim().length === 0 && nativeFailure !== undefined
|
|
75
|
+
? nativeFailure
|
|
76
|
+
: result.stderr,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (nativeSessionId === undefined) {
|
|
80
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode completed without a Session identifier.");
|
|
81
|
+
}
|
|
82
|
+
if (textParts.length === 0) {
|
|
83
|
+
if (request.output !== undefined) {
|
|
84
|
+
request.sessionReady?.(nativeSessionId);
|
|
85
|
+
throw new AutomationError("RUN_OUTPUT_INVALID", "The OpenCode Run completed without structured output.", { cause: new TypeError("Missing completed OpenCode text event.") });
|
|
86
|
+
}
|
|
87
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode completed without a textual result.");
|
|
88
|
+
}
|
|
89
|
+
const response = textParts.join("\n");
|
|
90
|
+
if (request.output === undefined) {
|
|
91
|
+
return { nativeSessionId, value: response };
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return {
|
|
95
|
+
nativeSessionId,
|
|
96
|
+
value: JSON.parse(response),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (cause) {
|
|
100
|
+
request.sessionReady?.(nativeSessionId);
|
|
101
|
+
throw new AutomationError("RUN_OUTPUT_INVALID", "The OpenCode Run returned malformed structured output.", { cause });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function parseOpenCodeEvent(line) {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(line);
|
|
107
|
+
}
|
|
108
|
+
catch (cause) {
|
|
109
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode emitted malformed JSONL.", { cause });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function opencodeSessionId(event) {
|
|
113
|
+
if (typeof event !== "object" || event === null) {
|
|
114
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode emitted an event without a valid Session identifier.");
|
|
115
|
+
}
|
|
116
|
+
const sessionId = event.sessionID;
|
|
117
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
118
|
+
throw new AutomationError("HARNESS_PROTOCOL_ERROR", "OpenCode emitted an event without a valid Session identifier.");
|
|
119
|
+
}
|
|
120
|
+
return sessionId;
|
|
121
|
+
}
|
|
122
|
+
function completedOpenCodeText(event) {
|
|
123
|
+
if (typeof event !== "object" || event === null) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const candidate = event;
|
|
127
|
+
if (candidate.type !== "text" ||
|
|
128
|
+
candidate.part?.type !== "text" ||
|
|
129
|
+
candidate.part.time?.end === undefined ||
|
|
130
|
+
typeof candidate.part.text !== "string") {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return candidate.part.text;
|
|
134
|
+
}
|
|
135
|
+
function opencodeFailureDiagnostic(event) {
|
|
136
|
+
if (typeof event !== "object" ||
|
|
137
|
+
event === null ||
|
|
138
|
+
event.type !== "error") {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const error = event.error;
|
|
142
|
+
if (typeof error === "string") {
|
|
143
|
+
return error;
|
|
144
|
+
}
|
|
145
|
+
if (typeof error === "object" && error !== null) {
|
|
146
|
+
const data = error.data;
|
|
147
|
+
if (typeof data === "object" && data !== null) {
|
|
148
|
+
const message = data.message;
|
|
149
|
+
if (typeof message === "string") {
|
|
150
|
+
return message;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const message = error.message;
|
|
154
|
+
if (typeof message === "string") {
|
|
155
|
+
return message;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return JSON.stringify(error);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AccessPolicy } from "./harness.js";
|
|
2
|
+
type Release = () => void;
|
|
3
|
+
export declare class RunConcurrencyLimiter {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(limit: number | undefined);
|
|
6
|
+
acquire(signal?: AbortSignal): Promise<Release>;
|
|
7
|
+
}
|
|
8
|
+
export declare function isConcurrencyLimit(value: unknown): value is number;
|
|
9
|
+
export declare function acquireWorkspaceAccess(workspaceRoot: string, access: AccessPolicy): Release;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { throwIfAborted } from "./cancellation.js";
|
|
2
|
+
import { AutomationError } from "./errors.js";
|
|
3
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
4
|
+
const workspaceAccesses = new Map();
|
|
5
|
+
export class RunConcurrencyLimiter {
|
|
6
|
+
#limit;
|
|
7
|
+
#waiters = [];
|
|
8
|
+
#active = 0;
|
|
9
|
+
constructor(limit) {
|
|
10
|
+
const resolvedLimit = limit ?? DEFAULT_CONCURRENCY;
|
|
11
|
+
if (!isConcurrencyLimit(resolvedLimit)) {
|
|
12
|
+
throw new TypeError("Workflow concurrency must be a positive integer.");
|
|
13
|
+
}
|
|
14
|
+
this.#limit = resolvedLimit;
|
|
15
|
+
}
|
|
16
|
+
async acquire(signal) {
|
|
17
|
+
throwIfAborted(signal);
|
|
18
|
+
if (this.#active < this.#limit) {
|
|
19
|
+
this.#active += 1;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
const cleanup = () => signal?.removeEventListener("abort", cancel);
|
|
24
|
+
const waiter = {
|
|
25
|
+
grant() {
|
|
26
|
+
cleanup();
|
|
27
|
+
resolve();
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const cancel = () => {
|
|
31
|
+
const index = this.#waiters.indexOf(waiter);
|
|
32
|
+
if (index === -1) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.#waiters.splice(index, 1);
|
|
36
|
+
cleanup();
|
|
37
|
+
reject(signal?.reason);
|
|
38
|
+
};
|
|
39
|
+
this.#waiters.push(waiter);
|
|
40
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
41
|
+
if (signal?.aborted === true) {
|
|
42
|
+
cancel();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
let released = false;
|
|
47
|
+
return () => {
|
|
48
|
+
if (released) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
released = true;
|
|
52
|
+
const next = this.#waiters.shift();
|
|
53
|
+
if (next === undefined) {
|
|
54
|
+
this.#active -= 1;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
next.grant();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function isConcurrencyLimit(value) {
|
|
63
|
+
return Number.isSafeInteger(value) && value >= 1;
|
|
64
|
+
}
|
|
65
|
+
export function acquireWorkspaceAccess(workspaceRoot, access) {
|
|
66
|
+
const current = workspaceAccesses.get(workspaceRoot);
|
|
67
|
+
const write = access === "write-workspace" || access === "full";
|
|
68
|
+
if ((write && current !== undefined) ||
|
|
69
|
+
(!write && current?.writer === true)) {
|
|
70
|
+
throw new AutomationError("WORKSPACE_CONCURRENCY_CONFLICT", "The Workspace is already used by a concurrent Run with incompatible access.");
|
|
71
|
+
}
|
|
72
|
+
const state = current ?? { readers: 0, writer: false };
|
|
73
|
+
if (write) {
|
|
74
|
+
state.writer = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
state.readers += 1;
|
|
78
|
+
}
|
|
79
|
+
workspaceAccesses.set(workspaceRoot, state);
|
|
80
|
+
let released = false;
|
|
81
|
+
return () => {
|
|
82
|
+
if (released) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
released = true;
|
|
86
|
+
if (write) {
|
|
87
|
+
state.writer = false;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
state.readers -= 1;
|
|
91
|
+
}
|
|
92
|
+
if (!state.writer && state.readers === 0) {
|
|
93
|
+
workspaceAccesses.delete(workspaceRoot);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AutomationEvent } from "./events.js";
|
|
2
|
+
export type RunJournalMode = "full" | "metadata";
|
|
3
|
+
export interface RunJournalContent {
|
|
4
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
5
|
+
readonly input?: unknown;
|
|
6
|
+
readonly prompt?: string;
|
|
7
|
+
readonly result?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export declare class RunJournal {
|
|
10
|
+
#private;
|
|
11
|
+
private constructor();
|
|
12
|
+
static create(workflowId: string, mode: RunJournalMode, workspaceRoot: string): Promise<RunJournal>;
|
|
13
|
+
append(event: AutomationEvent, content?: RunJournalContent): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { mkdir, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AutomationError, contextualizeAutomationError, } from "./errors.js";
|
|
5
|
+
import { isWithinDirectory } from "./filesystem.js";
|
|
6
|
+
export class RunJournal {
|
|
7
|
+
#filePath;
|
|
8
|
+
#mode;
|
|
9
|
+
#runStartedAt = new Map();
|
|
10
|
+
#executionStartedAt;
|
|
11
|
+
constructor(filePath, mode, executionStartedAt) {
|
|
12
|
+
this.#filePath = filePath;
|
|
13
|
+
this.#mode = mode;
|
|
14
|
+
this.#executionStartedAt = executionStartedAt;
|
|
15
|
+
}
|
|
16
|
+
static async create(workflowId, mode, workspaceRoot) {
|
|
17
|
+
try {
|
|
18
|
+
const [directory, canonicalWorkspaceRoot] = await Promise.all([
|
|
19
|
+
canonicalizePotentialPath(path.join(userStateDirectory(), "automation", "run-journals")),
|
|
20
|
+
canonicalizePotentialPath(workspaceRoot),
|
|
21
|
+
]);
|
|
22
|
+
if (isWithinDirectory(canonicalWorkspaceRoot, directory)) {
|
|
23
|
+
throw new AutomationError("RUN_JOURNAL_UNAVAILABLE", "Run Journal directory must be outside the Workspace. Disable recording or configure a user state directory outside it.");
|
|
24
|
+
}
|
|
25
|
+
await mkdir(directory, { mode: 0o700, recursive: true });
|
|
26
|
+
const filePath = path.join(directory, `${workflowId}.jsonl`);
|
|
27
|
+
await writeFile(filePath, "", { flag: "wx", mode: 0o600 });
|
|
28
|
+
return new RunJournal(filePath, mode, Date.now());
|
|
29
|
+
}
|
|
30
|
+
catch (cause) {
|
|
31
|
+
throw contextualizeAutomationError(cause, "RUN_JOURNAL_UNAVAILABLE", "Could not initialize the Run Journal.", { workflowId });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async append(event, content) {
|
|
35
|
+
const entry = this.#entry(event, content);
|
|
36
|
+
await writeFile(this.#filePath, `${JSON.stringify(entry)}\n`, {
|
|
37
|
+
flag: "a",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
#entry(event, content) {
|
|
41
|
+
const timestamp = Date.parse(event.timestamp);
|
|
42
|
+
const publicMetadata = redactSensitiveEventContent(event);
|
|
43
|
+
const lifecycle = this.#lifecycle(event, timestamp);
|
|
44
|
+
return {
|
|
45
|
+
...publicMetadata,
|
|
46
|
+
...lifecycle,
|
|
47
|
+
...(this.#mode === "full" ? sensitiveEventContent(event, content) : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
#lifecycle(event, timestamp) {
|
|
51
|
+
if (event.type === "workflow.started") {
|
|
52
|
+
return { state: "started" };
|
|
53
|
+
}
|
|
54
|
+
if (event.type === "workflow.completed") {
|
|
55
|
+
return {
|
|
56
|
+
durationMs: Math.max(0, timestamp - this.#executionStartedAt),
|
|
57
|
+
state: "completed",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (event.type === "workflow.failed") {
|
|
61
|
+
return {
|
|
62
|
+
durationMs: Math.max(0, timestamp - this.#executionStartedAt),
|
|
63
|
+
state: "failed",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (event.type === "run.started") {
|
|
67
|
+
this.#runStartedAt.set(event.runId, timestamp);
|
|
68
|
+
return { state: "started" };
|
|
69
|
+
}
|
|
70
|
+
if (event.type === "run.completed" || event.type === "run.failed") {
|
|
71
|
+
return {
|
|
72
|
+
durationMs: Math.max(0, timestamp - (this.#runStartedAt.get(event.runId) ?? timestamp)),
|
|
73
|
+
state: event.type === "run.completed" ? "completed" : "failed",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function canonicalizePotentialPath(targetPath) {
|
|
80
|
+
let existingPath = path.resolve(targetPath);
|
|
81
|
+
const missingSegments = [];
|
|
82
|
+
while (true) {
|
|
83
|
+
try {
|
|
84
|
+
return path.join(await realpath(existingPath), ...missingSegments);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code !== "ENOENT") {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
const parent = path.dirname(existingPath);
|
|
91
|
+
if (parent === existingPath) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
missingSegments.unshift(path.basename(existingPath));
|
|
95
|
+
existingPath = parent;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function sensitiveEventContent(event, content) {
|
|
100
|
+
return {
|
|
101
|
+
...(event.type === "run.native" ? { native: event.native } : {}),
|
|
102
|
+
...(event.type === "workflow.completed" ? { result: event.result } : {}),
|
|
103
|
+
...content,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function redactSensitiveEventContent(event) {
|
|
107
|
+
if (event.type === "workflow.completed") {
|
|
108
|
+
const { result: _result, ...metadata } = event;
|
|
109
|
+
return metadata;
|
|
110
|
+
}
|
|
111
|
+
if (event.type === "run.native") {
|
|
112
|
+
const { native: _native, ...metadata } = event;
|
|
113
|
+
return metadata;
|
|
114
|
+
}
|
|
115
|
+
return { ...event };
|
|
116
|
+
}
|
|
117
|
+
function userStateDirectory() {
|
|
118
|
+
const xdgStateHome = process.env.XDG_STATE_HOME;
|
|
119
|
+
if (xdgStateHome !== undefined && path.isAbsolute(xdgStateHome)) {
|
|
120
|
+
return xdgStateHome;
|
|
121
|
+
}
|
|
122
|
+
if (process.platform === "win32") {
|
|
123
|
+
return process.env.LOCALAPPDATA ?? path.join(homedir(), "AppData", "Local");
|
|
124
|
+
}
|
|
125
|
+
if (process.platform === "darwin") {
|
|
126
|
+
return path.join(homedir(), "Library", "Application Support");
|
|
127
|
+
}
|
|
128
|
+
return path.join(homedir(), ".local", "state");
|
|
129
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { JsonPrimitive } from "./json.js";
|
|
2
|
+
export type SchemaPathSegment = number | string;
|
|
3
|
+
export type SchemaIssueCode = "invalid_enum_value" | "invalid_literal" | "invalid_string" | "invalid_type" | "invalid_union" | "too_big" | "too_small" | "unrecognized_key";
|
|
4
|
+
export interface SchemaIssue {
|
|
5
|
+
readonly code: SchemaIssueCode;
|
|
6
|
+
readonly path: readonly SchemaPathSegment[];
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly [detail: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
export type SafeParseResult<TValue> = {
|
|
11
|
+
readonly success: true;
|
|
12
|
+
readonly data: TValue;
|
|
13
|
+
} | {
|
|
14
|
+
readonly success: false;
|
|
15
|
+
readonly error: SchemaValidationError;
|
|
16
|
+
};
|
|
17
|
+
export type JSONSchema = Readonly<Record<string, unknown>>;
|
|
18
|
+
declare const schemaOutput: unique symbol;
|
|
19
|
+
declare const schemaOptional: unique symbol;
|
|
20
|
+
declare const schemaParser: unique symbol;
|
|
21
|
+
declare const schemaJson: unique symbol;
|
|
22
|
+
interface ParseResult<TValue> {
|
|
23
|
+
readonly value?: TValue;
|
|
24
|
+
readonly issues: readonly SchemaIssue[];
|
|
25
|
+
}
|
|
26
|
+
export interface Schema<TValue, TOptional extends boolean = false> {
|
|
27
|
+
readonly [schemaOutput]: TValue;
|
|
28
|
+
readonly [schemaOptional]: TOptional;
|
|
29
|
+
parse(value: unknown): TValue;
|
|
30
|
+
safeParse(value: unknown): SafeParseResult<TValue>;
|
|
31
|
+
describe(description: string): Schema<TValue, TOptional>;
|
|
32
|
+
nullable(): Schema<TValue | null, TOptional>;
|
|
33
|
+
optional(): Schema<TValue | undefined, true>;
|
|
34
|
+
toJSONSchema(): JSONSchema;
|
|
35
|
+
}
|
|
36
|
+
type AnySchema = Schema<any, boolean>;
|
|
37
|
+
type InferAnySchema<TSchema extends AnySchema> = TSchema[typeof schemaOutput];
|
|
38
|
+
export type InferSchema<TSchema extends AnySchema> = TSchema[typeof schemaOutput];
|
|
39
|
+
export declare class SchemaValidationError extends Error {
|
|
40
|
+
readonly issues: readonly SchemaIssue[];
|
|
41
|
+
constructor(issues: readonly SchemaIssue[]);
|
|
42
|
+
}
|
|
43
|
+
declare abstract class BaseSchema<TValue, TOptional extends boolean = false> implements Schema<TValue, TOptional> {
|
|
44
|
+
#private;
|
|
45
|
+
readonly [schemaOutput]: TValue;
|
|
46
|
+
readonly [schemaOptional]: TOptional;
|
|
47
|
+
protected constructor(optional: TOptional, description?: string);
|
|
48
|
+
protected get description(): string | undefined;
|
|
49
|
+
parse(value: unknown): TValue;
|
|
50
|
+
safeParse(value: unknown): SafeParseResult<TValue>;
|
|
51
|
+
describe(description: string): this;
|
|
52
|
+
nullable(): Schema<TValue | null, TOptional>;
|
|
53
|
+
optional(): Schema<TValue | undefined, true>;
|
|
54
|
+
toJSONSchema(): JSONSchema;
|
|
55
|
+
protected abstract cloneWithDescription(description: string): BaseSchema<TValue, TOptional>;
|
|
56
|
+
abstract [schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<TValue>;
|
|
57
|
+
abstract [schemaJson](): JSONSchema;
|
|
58
|
+
}
|
|
59
|
+
interface StringConstraints {
|
|
60
|
+
readonly maximumLength: number | undefined;
|
|
61
|
+
readonly minimumLength: number | undefined;
|
|
62
|
+
readonly pattern: string | undefined;
|
|
63
|
+
}
|
|
64
|
+
export declare class StringSchema extends BaseSchema<string> {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(constraints?: StringConstraints, description?: string);
|
|
67
|
+
minLength(minimumLength: number): StringSchema;
|
|
68
|
+
maxLength(maximumLength: number): StringSchema;
|
|
69
|
+
pattern(pattern: string): StringSchema;
|
|
70
|
+
protected cloneWithDescription(description: string): StringSchema;
|
|
71
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<string>;
|
|
72
|
+
[schemaJson](): JSONSchema;
|
|
73
|
+
}
|
|
74
|
+
interface NumberConstraints {
|
|
75
|
+
readonly maximum: number | undefined;
|
|
76
|
+
readonly minimum: number | undefined;
|
|
77
|
+
readonly integer: boolean;
|
|
78
|
+
}
|
|
79
|
+
export declare class NumberSchema<TInteger extends boolean = false> extends BaseSchema<number> {
|
|
80
|
+
#private;
|
|
81
|
+
constructor(integer?: TInteger, constraints?: Omit<NumberConstraints, "integer">, description?: string);
|
|
82
|
+
minimum(minimum: number): NumberSchema<TInteger>;
|
|
83
|
+
maximum(maximum: number): NumberSchema<TInteger>;
|
|
84
|
+
protected cloneWithDescription(description: string): NumberSchema<TInteger>;
|
|
85
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<number>;
|
|
86
|
+
[schemaJson](): JSONSchema;
|
|
87
|
+
}
|
|
88
|
+
export declare class BooleanSchema extends BaseSchema<boolean> {
|
|
89
|
+
constructor(description?: string);
|
|
90
|
+
protected cloneWithDescription(description: string): BooleanSchema;
|
|
91
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<boolean>;
|
|
92
|
+
[schemaJson](): JSONSchema;
|
|
93
|
+
}
|
|
94
|
+
export declare class NullSchema extends BaseSchema<null> {
|
|
95
|
+
constructor(description?: string);
|
|
96
|
+
protected cloneWithDescription(description: string): NullSchema;
|
|
97
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<null>;
|
|
98
|
+
[schemaJson](): JSONSchema;
|
|
99
|
+
}
|
|
100
|
+
export declare class LiteralSchema<TValue extends JsonPrimitive> extends BaseSchema<TValue> {
|
|
101
|
+
#private;
|
|
102
|
+
constructor(expected: TValue, description?: string);
|
|
103
|
+
protected cloneWithDescription(description: string): LiteralSchema<TValue>;
|
|
104
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<TValue>;
|
|
105
|
+
[schemaJson](): JSONSchema;
|
|
106
|
+
}
|
|
107
|
+
export declare class EnumSchema<TValues extends readonly JsonPrimitive[]> extends BaseSchema<TValues[number]> {
|
|
108
|
+
#private;
|
|
109
|
+
constructor(values: TValues, description?: string);
|
|
110
|
+
protected cloneWithDescription(description: string): EnumSchema<TValues>;
|
|
111
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<TValues[number]>;
|
|
112
|
+
[schemaJson](): JSONSchema;
|
|
113
|
+
}
|
|
114
|
+
interface ArrayConstraints {
|
|
115
|
+
readonly maximumItems: number | undefined;
|
|
116
|
+
readonly minimumItems: number | undefined;
|
|
117
|
+
}
|
|
118
|
+
export declare class ArraySchema<TItemSchema extends AnySchema> extends BaseSchema<InferAnySchema<TItemSchema>[]> {
|
|
119
|
+
#private;
|
|
120
|
+
constructor(item: TItemSchema, constraints?: ArrayConstraints, description?: string);
|
|
121
|
+
minItems(minimumItems: number): ArraySchema<TItemSchema>;
|
|
122
|
+
maxItems(maximumItems: number): ArraySchema<TItemSchema>;
|
|
123
|
+
protected cloneWithDescription(description: string): ArraySchema<TItemSchema>;
|
|
124
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<InferAnySchema<TItemSchema>[]>;
|
|
125
|
+
[schemaJson](): JSONSchema;
|
|
126
|
+
}
|
|
127
|
+
type SchemaShape = Readonly<Record<string, AnySchema>>;
|
|
128
|
+
type OptionalShapeKeys<TShape extends SchemaShape> = {
|
|
129
|
+
[TKey in keyof TShape]-?: TShape[TKey] extends Schema<any, true> ? TKey : never;
|
|
130
|
+
}[keyof TShape];
|
|
131
|
+
type RequiredShapeKeys<TShape extends SchemaShape> = Exclude<keyof TShape, OptionalShapeKeys<TShape>>;
|
|
132
|
+
type Simplify<TValue> = {
|
|
133
|
+
[TKey in keyof TValue]: TValue[TKey];
|
|
134
|
+
};
|
|
135
|
+
type ObjectSchemaValue<TShape extends SchemaShape> = Simplify<{
|
|
136
|
+
-readonly [TKey in RequiredShapeKeys<TShape>]: InferAnySchema<TShape[TKey]>;
|
|
137
|
+
} & {
|
|
138
|
+
-readonly [TKey in OptionalShapeKeys<TShape>]?: Exclude<InferAnySchema<TShape[TKey]>, undefined>;
|
|
139
|
+
}>;
|
|
140
|
+
export declare class ObjectSchema<TShape extends SchemaShape> extends BaseSchema<ObjectSchemaValue<TShape>> {
|
|
141
|
+
#private;
|
|
142
|
+
constructor(shape: TShape, description?: string);
|
|
143
|
+
protected cloneWithDescription(description: string): ObjectSchema<TShape>;
|
|
144
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<ObjectSchemaValue<TShape>>;
|
|
145
|
+
[schemaJson](): JSONSchema;
|
|
146
|
+
}
|
|
147
|
+
type UnionSchemaValue<TSchemas extends readonly AnySchema[]> = InferAnySchema<TSchemas[number]>;
|
|
148
|
+
export declare class UnionSchema<TSchemas extends readonly AnySchema[]> extends BaseSchema<UnionSchemaValue<TSchemas>> {
|
|
149
|
+
#private;
|
|
150
|
+
constructor(schemas: TSchemas, description?: string);
|
|
151
|
+
protected cloneWithDescription(description: string): UnionSchema<TSchemas>;
|
|
152
|
+
[schemaParser](value: unknown, path: readonly SchemaPathSegment[]): ParseResult<UnionSchemaValue<TSchemas>>;
|
|
153
|
+
[schemaJson](): JSONSchema;
|
|
154
|
+
}
|
|
155
|
+
export declare const schema: Readonly<{
|
|
156
|
+
array: <TItemSchema extends AnySchema>(item: TItemSchema) => ArraySchema<TItemSchema>;
|
|
157
|
+
boolean: () => BooleanSchema;
|
|
158
|
+
enum: <const TValues extends readonly JsonPrimitive[]>(values: TValues) => EnumSchema<TValues>;
|
|
159
|
+
integer: () => NumberSchema<true>;
|
|
160
|
+
literal: <const TValue extends JsonPrimitive>(value: TValue) => LiteralSchema<TValue>;
|
|
161
|
+
null: () => NullSchema;
|
|
162
|
+
number: () => NumberSchema;
|
|
163
|
+
object: <const TShape extends SchemaShape>(shape: TShape) => ObjectSchema<TShape>;
|
|
164
|
+
string: () => StringSchema;
|
|
165
|
+
union: <const TSchemas extends readonly AnySchema[]>(schemas: TSchemas) => UnionSchema<TSchemas>;
|
|
166
|
+
}>;
|
|
167
|
+
export {};
|