@drkmttr/adhere 0.14.0 → 0.17.5
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 +294 -146
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/presets/effect/config/tests-provide-values-directly/RULE.md +12 -1
- package/presets/effect/data/brand-meaningful-primitives/RULE.md +14 -2
- package/presets/effect/data/variants-are-tagged-unions/RULE.md +17 -4
- package/presets/effect/services/dependencies-through-layers/RULE.md +16 -3
- package/presets/effect/services/no-mutable-state/RULE.md +7 -1
- package/skills/adhere/SKILL.md +180 -31
- package/skills/adhere-setup/SKILL.md +142 -16
- package/src/cli.ts +56 -23
- package/src/config.ts +49 -16
- package/src/index.ts +14 -0
- package/src/markdown.ts +9 -5
- package/src/providers/cloudflare.ts +84 -0
- package/src/providers/jev.ts +66 -0
- package/src/providers/openai.ts +89 -0
- package/src/providers/post.ts +53 -0
- package/src/providers/provider.ts +131 -0
- package/src/providers/providers.test.ts +276 -0
- package/src/providers/systemOne.ts +95 -0
- package/src/services/AdhereConfig.ts +2 -4
- package/src/services/Credentials.ts +101 -51
- package/src/services/Jev.http.ts +165 -170
- package/src/services/Jev.provider.test.ts +207 -0
- package/src/services/Jev.reads.test.ts +439 -0
- package/src/services/Jev.ts +205 -113
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1040 -0
- package/src/services/SourceReads.ts +437 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +440 -265
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
- package/presets/effect/services/operations-have-no-requirements/RULE.md +0 -50
- package/presets/effect/services/test-layers-are-in-memory/RULE.md +0 -29
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { API, Snapshot } from "typescript/async";
|
|
5
|
+
|
|
6
|
+
const Message = Schema.Struct({
|
|
7
|
+
id: Schema.optionalKey(Schema.Union([Schema.Number, Schema.String, Schema.Null])),
|
|
8
|
+
method: Schema.optionalKey(Schema.String),
|
|
9
|
+
params: Schema.optionalKey(Schema.Unknown),
|
|
10
|
+
result: Schema.optionalKey(Schema.Unknown),
|
|
11
|
+
error: Schema.optionalKey(Schema.Struct({ message: Schema.String })),
|
|
12
|
+
});
|
|
13
|
+
const decodeMessage = Schema.decodeUnknownSync(Schema.fromJsonString(Message));
|
|
14
|
+
const Initialize = Schema.Struct({
|
|
15
|
+
capabilities: Schema.Struct({
|
|
16
|
+
positionEncoding: Schema.optionalKey(Schema.String),
|
|
17
|
+
documentSymbolProvider: Schema.Unknown,
|
|
18
|
+
}),
|
|
19
|
+
serverInfo: Schema.Struct({ version: Schema.String }),
|
|
20
|
+
});
|
|
21
|
+
const Session = Schema.Struct({ pipe: Schema.String });
|
|
22
|
+
|
|
23
|
+
interface Pending {
|
|
24
|
+
readonly resolve: (value: unknown) => void;
|
|
25
|
+
readonly reject: (error: Error) => void;
|
|
26
|
+
readonly timer: ReturnType<typeof setTimeout>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One native LSP process; its SDK API session connects to a pipe, never another compiler. */
|
|
30
|
+
export class NativeLsp {
|
|
31
|
+
private readonly process: ChildProcessWithoutNullStreams;
|
|
32
|
+
private readonly exited: Promise<void>;
|
|
33
|
+
private readonly pending = new Map<number, Pending>();
|
|
34
|
+
private readonly sdkPending = new Set<(error: unknown) => void>();
|
|
35
|
+
private document: string | undefined;
|
|
36
|
+
private input = Buffer.alloc(0);
|
|
37
|
+
private stderr = "";
|
|
38
|
+
private nextId = 0;
|
|
39
|
+
private failure: Error | undefined;
|
|
40
|
+
private attached: API<true> | undefined;
|
|
41
|
+
private closing: Promise<void> | undefined;
|
|
42
|
+
|
|
43
|
+
private constructor(executable: string, root: string) {
|
|
44
|
+
this.process = spawn(executable, ["--lsp", "--stdio"], { cwd: root, stdio: "pipe" });
|
|
45
|
+
this.exited = new Promise((resolve) => {
|
|
46
|
+
this.process.once("error", (error) => this.fail(error));
|
|
47
|
+
this.process.once("exit", (code, signal) =>
|
|
48
|
+
this.fail(new Error(`Native TypeScript LSP exited (${signal ?? code}). ${this.stderr}`)),
|
|
49
|
+
);
|
|
50
|
+
this.process.once("close", (code, signal) => {
|
|
51
|
+
this.fail(new Error(`Native TypeScript LSP closed (${signal ?? code}). ${this.stderr}`));
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
for (const stream of [this.process.stdin, this.process.stdout, this.process.stderr])
|
|
56
|
+
stream.on("error", (error: Error) => this.fail(error));
|
|
57
|
+
this.process.stderr.on("data", (data: Buffer) => {
|
|
58
|
+
this.stderr = (this.stderr + data.toString("utf8")).slice(-4096);
|
|
59
|
+
});
|
|
60
|
+
this.process.stdout.on("data", (data: Buffer) => {
|
|
61
|
+
try {
|
|
62
|
+
this.input = Buffer.concat([this.input, data]);
|
|
63
|
+
for (;;) {
|
|
64
|
+
const end = this.input.indexOf("\r\n\r\n");
|
|
65
|
+
if (end < 0) break;
|
|
66
|
+
const length = /^Content-Length:\s*(\d+)$/im.exec(
|
|
67
|
+
this.input.subarray(0, end).toString("ascii"),
|
|
68
|
+
);
|
|
69
|
+
if (!length) throw new Error("Native TypeScript LSP sent an invalid frame header.");
|
|
70
|
+
const last = end + 4 + Number(length[1]);
|
|
71
|
+
if (this.input.length < last) break;
|
|
72
|
+
const message = decodeMessage(this.input.subarray(end + 4, last).toString("utf8"));
|
|
73
|
+
this.input = this.input.subarray(last);
|
|
74
|
+
if (message.method !== undefined) {
|
|
75
|
+
if (message.id !== undefined && message.id !== null) {
|
|
76
|
+
const items = (message.params as { readonly items?: unknown[] } | undefined)?.items;
|
|
77
|
+
const response =
|
|
78
|
+
message.method === "workspace/configuration" && Array.isArray(items)
|
|
79
|
+
? { result: items.map(() => null) }
|
|
80
|
+
: {
|
|
81
|
+
error: {
|
|
82
|
+
code: -32601,
|
|
83
|
+
message: `Unsupported client request: ${message.method}`,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
void this.write({ id: message.id, ...response }).catch((error: Error) =>
|
|
87
|
+
this.fail(error),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (typeof message.id !== "number") continue;
|
|
93
|
+
const pending = this.pending.get(message.id);
|
|
94
|
+
if (!pending) continue;
|
|
95
|
+
this.pending.delete(message.id);
|
|
96
|
+
clearTimeout(pending.timer);
|
|
97
|
+
if (message.error) pending.reject(new Error(message.error.message));
|
|
98
|
+
else pending.resolve(message.result);
|
|
99
|
+
}
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
this.fail(cause instanceof Error ? cause : new Error(String(cause)));
|
|
102
|
+
this.process.kill();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static async open(executable: string, root: string, version: string): Promise<NativeLsp> {
|
|
108
|
+
const connection = new NativeLsp(executable, root);
|
|
109
|
+
try {
|
|
110
|
+
const initialized = Schema.decodeUnknownSync(Initialize)(
|
|
111
|
+
await connection.request("initialize", {
|
|
112
|
+
processId: process.pid,
|
|
113
|
+
rootUri: pathToFileURL(root).href,
|
|
114
|
+
capabilities: {
|
|
115
|
+
general: { positionEncodings: ["utf-16"] },
|
|
116
|
+
textDocument: { documentSymbol: { hierarchicalDocumentSymbolSupport: true } },
|
|
117
|
+
},
|
|
118
|
+
clientInfo: { name: "adhere" },
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
if (
|
|
122
|
+
initialized.serverInfo.version !== version ||
|
|
123
|
+
initialized.capabilities.positionEncoding !== "utf-16" ||
|
|
124
|
+
!initialized.capabilities.documentSymbolProvider
|
|
125
|
+
)
|
|
126
|
+
throw new Error(`Native TypeScript ${version} with UTF-16 document symbols is required.`);
|
|
127
|
+
await connection.notify("initialized", {});
|
|
128
|
+
const session = Schema.decodeUnknownSync(Session)(
|
|
129
|
+
await connection.request("custom/initializeAPISession", {}),
|
|
130
|
+
);
|
|
131
|
+
const { API } = await import("typescript/async");
|
|
132
|
+
await connection.sdk("API attachment", async () => {
|
|
133
|
+
const attached = await API.fromLSPConnection({ pipe: session.pipe });
|
|
134
|
+
// A guarded attachment may finish after timeout/exit; it still belongs to this owner.
|
|
135
|
+
if (connection.failure || connection.closing) {
|
|
136
|
+
void attached.close().catch(() => {});
|
|
137
|
+
throw connection.failure ?? new Error("Native TypeScript LSP is closing.");
|
|
138
|
+
}
|
|
139
|
+
connection.attached = attached;
|
|
140
|
+
});
|
|
141
|
+
return connection;
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
await connection.close();
|
|
144
|
+
throw cause;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get api(): API<true> {
|
|
149
|
+
if (!this.attached) throw new Error("The native TypeScript API session is not initialized.");
|
|
150
|
+
return this.attached;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get pid(): number | undefined {
|
|
154
|
+
return this.process.pid;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async openDocument(file: string, text: string): Promise<void> {
|
|
158
|
+
if (this.document !== undefined)
|
|
159
|
+
throw new Error(`Native TypeScript is already reading ${this.document}.`);
|
|
160
|
+
await this.sdk("didOpen", () =>
|
|
161
|
+
this.notify("textDocument/didOpen", {
|
|
162
|
+
textDocument: {
|
|
163
|
+
uri: pathToFileURL(file).href,
|
|
164
|
+
languageId: file.endsWith(".tsx") ? "typescriptreact" : "typescript",
|
|
165
|
+
version: 1,
|
|
166
|
+
text,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
this.document = file;
|
|
171
|
+
// Order didOpen before the separate API pipe captures its snapshot, without type/hover work.
|
|
172
|
+
await this.request("textDocument/documentSymbol", {
|
|
173
|
+
textDocument: { uri: pathToFileURL(file).href },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async closeDocument(file: string): Promise<void> {
|
|
178
|
+
await this.sdk("didClose", () =>
|
|
179
|
+
this.notify("textDocument/didClose", { textDocument: { uri: pathToFileURL(file).href } }),
|
|
180
|
+
);
|
|
181
|
+
this.document = undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
snapshot(): Promise<Snapshot> {
|
|
185
|
+
return this.sdk("getCurrentLanguageServerSnapshot", async () => {
|
|
186
|
+
// The LSP document owns discovery and text; do not retain a second API openFiles lease.
|
|
187
|
+
const snapshot = await this.api.getCurrentLanguageServerSnapshot();
|
|
188
|
+
if (this.failure || this.closing) {
|
|
189
|
+
void snapshot.dispose().catch(() => {});
|
|
190
|
+
throw this.failure ?? new Error("Native TypeScript LSP is closing.");
|
|
191
|
+
}
|
|
192
|
+
return snapshot;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// SDK pipe closure does not reject pending requests; bound each phase, not the whole audit.
|
|
197
|
+
sdk<A>(phase: string, work: () => Promise<A>): Promise<A> {
|
|
198
|
+
if (this.failure || this.closing)
|
|
199
|
+
return Promise.reject(this.failure ?? new Error("Native TypeScript LSP is closing."));
|
|
200
|
+
return new Promise<A>((resolve, reject) => {
|
|
201
|
+
const finish = () => {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
this.sdkPending.delete(refuse);
|
|
204
|
+
};
|
|
205
|
+
const refuse = (error: unknown) => {
|
|
206
|
+
finish();
|
|
207
|
+
reject(error);
|
|
208
|
+
};
|
|
209
|
+
const timer = setTimeout(
|
|
210
|
+
() => this.fail(new Error(`Native TypeScript SDK timed out: ${phase}`)),
|
|
211
|
+
30_000,
|
|
212
|
+
);
|
|
213
|
+
this.sdkPending.add(refuse);
|
|
214
|
+
try {
|
|
215
|
+
// Both handlers stay attached after refusal so late SDK errors are never unhandled.
|
|
216
|
+
void work().then((value) => {
|
|
217
|
+
finish();
|
|
218
|
+
resolve(value);
|
|
219
|
+
}, refuse);
|
|
220
|
+
} catch (cause) {
|
|
221
|
+
refuse(cause);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
close(): Promise<void> {
|
|
227
|
+
return (this.closing ??= (async () => {
|
|
228
|
+
const kill = setTimeout(() => this.process.kill("SIGKILL"), 2000);
|
|
229
|
+
try {
|
|
230
|
+
if (this.attached) await Promise.race([this.attached.close(), this.exited]).catch(() => {});
|
|
231
|
+
if (this.process.exitCode === null && this.process.signalCode === null) {
|
|
232
|
+
await this.request("shutdown").catch(() => {});
|
|
233
|
+
await this.notify("exit").catch(() => {});
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
this.document = undefined;
|
|
237
|
+
this.process.stdin.end();
|
|
238
|
+
await this.exited;
|
|
239
|
+
clearTimeout(kill);
|
|
240
|
+
this.fail(new Error("Native TypeScript LSP is closed."));
|
|
241
|
+
}
|
|
242
|
+
})());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private fail(error: Error): void {
|
|
246
|
+
this.failure ??= error;
|
|
247
|
+
for (const pending of this.pending.values()) {
|
|
248
|
+
clearTimeout(pending.timer);
|
|
249
|
+
pending.reject(error);
|
|
250
|
+
}
|
|
251
|
+
this.pending.clear();
|
|
252
|
+
for (const refuse of this.sdkPending) refuse(this.failure);
|
|
253
|
+
this.sdkPending.clear();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private write(message: object): Promise<void> {
|
|
257
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
258
|
+
const body = Buffer.from(JSON.stringify({ jsonrpc: "2.0", ...message }), "utf8");
|
|
259
|
+
const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii");
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
this.process.stdin.write(Buffer.concat([header, body]), (error) =>
|
|
262
|
+
error ? reject(error) : resolve(),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private notify(method: string, params?: unknown): Promise<void> {
|
|
268
|
+
return this.write({ method, ...(params === undefined ? {} : { params }) });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private request(method: string, params?: unknown): Promise<unknown> {
|
|
272
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
273
|
+
const id = ++this.nextId;
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const timer = setTimeout(() => {
|
|
276
|
+
this.pending.delete(id);
|
|
277
|
+
reject(new Error(`Native TypeScript LSP timed out: ${method}`));
|
|
278
|
+
}, 30_000);
|
|
279
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
280
|
+
void this.write({ id, method, ...(params === undefined ? {} : { params }) }).catch(
|
|
281
|
+
(error: Error) => {
|
|
282
|
+
this.pending.delete(id);
|
|
283
|
+
clearTimeout(timer);
|
|
284
|
+
reject(error);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|