@aas-dev/stack 0.0.0-stage → 0.1.1

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,379 @@
1
+ // plugin/aas.mjs
2
+ const markers = [
3
+ "@ts-ignore",
4
+ "@ts-expect-error",
5
+ "@ts-nocheck",
6
+ "eslint-disable",
7
+ "eslint-enable",
8
+ "oxlint-disable",
9
+ "oxlint-enable"
10
+ ];
11
+ const errorNames = /* @__PURE__ */ new Set([
12
+ "Error",
13
+ "AggregateError",
14
+ "EvalError",
15
+ "RangeError",
16
+ "ReferenceError",
17
+ "SyntaxError",
18
+ "TypeError",
19
+ "URIError"
20
+ ]);
21
+ const rawSpecifiers = /* @__PURE__ */ new Set([
22
+ "child_process",
23
+ "fs",
24
+ "http",
25
+ "https",
26
+ "node:child_process",
27
+ "node:fs",
28
+ "node:fs/promises",
29
+ "node:http",
30
+ "node:https"
31
+ ]);
32
+ const socketSpecifiers = /* @__PURE__ */ new Set([
33
+ "net",
34
+ "node:net",
35
+ "node:tls",
36
+ "tls"
37
+ ]);
38
+ const consoleMethods = /* @__PURE__ */ new Set([
39
+ "debug",
40
+ "error",
41
+ "info",
42
+ "log",
43
+ "warn"
44
+ ]);
45
+ const effectRunMethods = /* @__PURE__ */ new Set([
46
+ "runFork",
47
+ "runPromise",
48
+ "runPromiseExit",
49
+ "runSync"
50
+ ]);
51
+ const bannedTestMembers = /* @__PURE__ */ new Set([
52
+ "only",
53
+ "skip",
54
+ "skipIf",
55
+ "todo"
56
+ ]);
57
+ const testAliases = /* @__PURE__ */ new Set([
58
+ "fdescribe",
59
+ "fit",
60
+ "ftest",
61
+ "xdescribe",
62
+ "xit",
63
+ "xtest"
64
+ ]);
65
+ const report = (context, node, message) => {
66
+ context.report({
67
+ node,
68
+ message
69
+ });
70
+ };
71
+ const literalName = (property, computed) => {
72
+ if (!computed && property.type === "Identifier") return property.name;
73
+ if (computed && (property.type === "Literal" || property.type === "StringLiteral") && typeof property.value === "string") return property.value;
74
+ return "";
75
+ };
76
+ const isProcessEnv = (node) => {
77
+ if (node.type !== "MemberExpression") return false;
78
+ return node.object.type === "Identifier" && node.object.name === "process" && literalName(node.property, node.computed) === "env";
79
+ };
80
+ const hasName = (node, name) => {
81
+ if (node === null || node === void 0 || typeof node !== "object") return false;
82
+ if (node.type === "Identifier" && node.name === name) return true;
83
+ return hasName(node.callee, name) || hasName(node.object, name) || hasName(node.property, name) || hasName(node.expression, name);
84
+ };
85
+ const blessedSuper = (node) => hasName(node, "TaggedError") || hasName(node, "Service");
86
+ const specifier = (node) => {
87
+ if (node === null || node === void 0) return "";
88
+ if ((node.type === "Literal" || node.type === "StringLiteral") && typeof node.value === "string") return node.value;
89
+ return "";
90
+ };
91
+ const chainParts = (callee) => {
92
+ const parts = [];
93
+ let current = callee;
94
+ while (current.type === "MemberExpression" || current.type === "ChainExpression") {
95
+ if (current.type === "ChainExpression") {
96
+ current = current.expression;
97
+ continue;
98
+ }
99
+ if (current.computed || current.property.type !== "Identifier") return [];
100
+ parts.push(current.property.name);
101
+ current = current.object;
102
+ }
103
+ if (current.type !== "Identifier") return [];
104
+ parts.push(current.name);
105
+ parts.reverse();
106
+ return parts;
107
+ };
108
+ const isItEffect = (parts) => {
109
+ if (parts[0] !== "it" || parts[1] !== "effect") return false;
110
+ let index = 2;
111
+ while (index < parts.length) {
112
+ const part = parts[index];
113
+ if (part !== void 0 && bannedTestMembers.has(part)) return false;
114
+ index += 1;
115
+ }
116
+ return true;
117
+ };
118
+ const flagModuleBinding = (context, statement) => {
119
+ if (statement === null || statement === void 0) return;
120
+ if (statement.type === "VariableDeclaration" && statement.kind !== "const") report(context, statement, "module-scope let and var are banned; keep state in a Context.Service");
121
+ const binding = statement.init ?? statement.left;
122
+ if ((statement.type === "ForStatement" || statement.type === "ForInStatement" || statement.type === "ForOfStatement") && binding !== null && binding !== void 0 && binding.type === "VariableDeclaration" && binding.kind !== "const") report(context, binding, "module-scope let and var are banned; keep state in a Context.Service");
123
+ };
124
+ const noThrow = {
125
+ meta: { type: "problem" },
126
+ create(context) {
127
+ return { ThrowStatement(node) {
128
+ report(context, node, "throw is banned; yield a Schema.TaggedError");
129
+ } };
130
+ }
131
+ };
132
+ const noTry = {
133
+ meta: { type: "problem" },
134
+ create(context) {
135
+ return { TryStatement(node) {
136
+ report(context, node, "try is banned; failures stay on the Effect channel");
137
+ } };
138
+ }
139
+ };
140
+ const noErrorClass = {
141
+ meta: { type: "problem" },
142
+ create(context) {
143
+ return { NewExpression(node) {
144
+ const callee = node.callee;
145
+ const name = callee.type === "Identifier" ? callee.name : callee.type === "MemberExpression" ? literalName(callee.property, callee.computed) : "";
146
+ if (errorNames.has(name)) report(context, node, "new Error is banned; define a Schema.TaggedError");
147
+ } };
148
+ }
149
+ };
150
+ const noProcessEnv = {
151
+ meta: { type: "problem" },
152
+ create(context) {
153
+ return { MemberExpression(node) {
154
+ if (isProcessEnv(node)) report(context, node, "process.env is banned; declare the variable in .env.schema and read it with Config");
155
+ } };
156
+ }
157
+ };
158
+ const noTypeAssertion = {
159
+ meta: { type: "problem" },
160
+ create(context) {
161
+ return {
162
+ TSAsExpression(node) {
163
+ report(context, node, "type assertions are banned, including as const; parse with Schema or use a const type parameter");
164
+ },
165
+ TSTypeAssertion(node) {
166
+ report(context, node, "angle-bracket assertions are banned; parse with Schema or use a const type parameter");
167
+ }
168
+ };
169
+ }
170
+ };
171
+ const noExplicitAny = {
172
+ meta: { type: "problem" },
173
+ create(context) {
174
+ return { TSAnyKeyword(node) {
175
+ report(context, node, "any is banned; use unknown and decode it with Schema");
176
+ } };
177
+ }
178
+ };
179
+ const noNonNullAssertion = {
180
+ meta: { type: "problem" },
181
+ create(context) {
182
+ return { TSNonNullExpression(node) {
183
+ report(context, node, "non-null assertions are banned; narrow the value or decode it with Schema");
184
+ } };
185
+ }
186
+ };
187
+ const textOf = (context) => {
188
+ const source = context.sourceCode;
189
+ if (typeof source.text === "string") return source.text;
190
+ if (typeof source.getText === "function") return source.getText();
191
+ return "";
192
+ };
193
+ const commentStart = (comment) => {
194
+ const range = comment.range;
195
+ if (Array.isArray(range)) {
196
+ const start = range[0];
197
+ return typeof start === "number" ? start : -1;
198
+ }
199
+ if (range !== null && range !== void 0 && typeof range.start === "number") return range.start;
200
+ if (typeof comment.start === "number") return comment.start;
201
+ return -1;
202
+ };
203
+ const isShebang = (comment, text) => {
204
+ if (comment.type === "Shebang" || comment.type === "Hashbang") return true;
205
+ return text.startsWith("#!") && commentStart(comment) === 0;
206
+ };
207
+ const noComments = {
208
+ meta: { type: "problem" },
209
+ create(context) {
210
+ const text = textOf(context);
211
+ return { Program(node) {
212
+ for (const comment of context.sourceCode.getAllComments()) {
213
+ if (isShebang(comment, text)) continue;
214
+ const message = "comments are banned; use clearer names or docs";
215
+ if (comment.loc === void 0 || comment.loc === null) report(context, node, message);
216
+ else context.report({
217
+ loc: comment.loc,
218
+ message
219
+ });
220
+ }
221
+ } };
222
+ }
223
+ };
224
+ const noBypassComment = {
225
+ meta: { type: "problem" },
226
+ create(context) {
227
+ return { Program(node) {
228
+ for (const comment of context.sourceCode.getAllComments()) {
229
+ let matched = "";
230
+ for (const marker of markers) if (comment.value.includes(marker)) {
231
+ matched = marker;
232
+ break;
233
+ }
234
+ if (matched === "") continue;
235
+ const message = `directive comments are banned (${matched})`;
236
+ if (comment.loc === void 0 || comment.loc === null) report(context, node, message);
237
+ else context.report({
238
+ loc: comment.loc,
239
+ message
240
+ });
241
+ }
242
+ } };
243
+ }
244
+ };
245
+ const effectTests = {
246
+ meta: { type: "problem" },
247
+ create(context) {
248
+ const filename = typeof context.filename === "string" ? context.filename : "";
249
+ const isTest = filename.endsWith(".test.ts");
250
+ return {
251
+ ImportDeclaration(node) {
252
+ const source = specifier(node.source);
253
+ if (source === "vitest/config") {
254
+ if (!filename.endsWith("/vitest.config.ts") && !filename.endsWith("\\vitest.config.ts")) report(context, node, "import tests from @effect/vitest");
255
+ return;
256
+ }
257
+ if (source === "vitest" || source.startsWith("vitest/") || source.startsWith("@vitest/")) report(context, node, "import tests from @effect/vitest");
258
+ },
259
+ CallExpression(node) {
260
+ if (!isTest) return;
261
+ const parts = chainParts(node.callee);
262
+ const root = parts[0];
263
+ if (root === void 0) {
264
+ if (node.callee.type === "Identifier" && (node.callee.name === "it" || node.callee.name === "test" || testAliases.has(node.callee.name))) report(context, node, "tests use it.effect from @effect/vitest");
265
+ return;
266
+ }
267
+ if (isItEffect(parts)) return;
268
+ if (root === "it" || root === "test" || testAliases.has(root)) {
269
+ report(context, node, "tests use it.effect from @effect/vitest");
270
+ return;
271
+ }
272
+ if (root === "describe") {
273
+ let index = 1;
274
+ while (index < parts.length) {
275
+ const part = parts[index];
276
+ if (part !== void 0 && bannedTestMembers.has(part)) {
277
+ report(context, node, "skipped, focused, and todo tests are banned");
278
+ return;
279
+ }
280
+ index += 1;
281
+ }
282
+ }
283
+ }
284
+ };
285
+ }
286
+ };
287
+ const isAdapterFile = (context) => {
288
+ return (typeof context.filename === "string" ? context.filename : "").endsWith(".adapter.ts");
289
+ };
290
+ const rawMessage = (source) => {
291
+ if (socketSpecifiers.has(source)) return "node:net and node:tls belong in a .adapter.ts file; other raw IO stays banned";
292
+ return "raw IO imports are banned; use Effect FileSystem, HttpClient, or ChildProcess";
293
+ };
294
+ const bannedRaw = (context, node, source) => {
295
+ if (socketSpecifiers.has(source)) {
296
+ if (isAdapterFile(context)) return;
297
+ report(context, node, rawMessage(source));
298
+ return;
299
+ }
300
+ if (rawSpecifiers.has(source)) report(context, node, rawMessage(source));
301
+ };
302
+ const ruleNames = Object.keys({
303
+ "effect-tests": effectTests,
304
+ "no-bypass-comment": noBypassComment,
305
+ "no-comments": noComments,
306
+ "no-effect-run": {
307
+ meta: { type: "problem" },
308
+ create(context) {
309
+ return { CallExpression(node) {
310
+ const callee = node.callee;
311
+ if (callee.type !== "MemberExpression") return;
312
+ if (callee.object.type === "Identifier" && callee.object.name === "Effect" && effectRunMethods.has(literalName(callee.property, callee.computed))) report(context, node, "Effect.run* is banned; stay inside Effect and use it.effect in tests");
313
+ } };
314
+ }
315
+ },
316
+ "no-error-class": noErrorClass,
317
+ "no-explicit-any": noExplicitAny,
318
+ "no-module-let": {
319
+ meta: { type: "problem" },
320
+ create(context) {
321
+ return { Program(node) {
322
+ for (const statement of node.body) {
323
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
324
+ flagModuleBinding(context, declaration);
325
+ }
326
+ } };
327
+ }
328
+ },
329
+ "no-non-null-assertion": noNonNullAssertion,
330
+ "no-plain-class": {
331
+ meta: { type: "problem" },
332
+ create(context) {
333
+ return {
334
+ ClassDeclaration(node) {
335
+ if (!blessedSuper(node.superClass)) report(context, node, "plain classes are banned; extend Schema.TaggedError or Context.Service");
336
+ },
337
+ ClassExpression(node) {
338
+ if (!blessedSuper(node.superClass)) report(context, node, "plain classes are banned; extend Schema.TaggedError or Context.Service");
339
+ }
340
+ };
341
+ }
342
+ },
343
+ "no-process-env": noProcessEnv,
344
+ "no-raw-io": {
345
+ meta: { type: "problem" },
346
+ create(context) {
347
+ return {
348
+ ImportDeclaration(node) {
349
+ bannedRaw(context, node, specifier(node.source));
350
+ },
351
+ ImportExpression(node) {
352
+ bannedRaw(context, node, specifier(node.source));
353
+ },
354
+ CallExpression(node) {
355
+ const callee = node.callee;
356
+ if (callee.type === "Identifier" && callee.name === "fetch") {
357
+ report(context, node, "global fetch is banned; use Effect HttpClient");
358
+ return;
359
+ }
360
+ if (callee.type === "Identifier" && callee.name === "require") {
361
+ bannedRaw(context, node, specifier(node.arguments[0]));
362
+ return;
363
+ }
364
+ if (callee.type !== "MemberExpression") return;
365
+ const name = literalName(callee.property, callee.computed);
366
+ if (callee.object.type === "Identifier" && callee.object.name === "console" && consoleMethods.has(name)) {
367
+ report(context, node, "console is banned; use Effect Console");
368
+ return;
369
+ }
370
+ if (callee.object.type === "Identifier" && (callee.object.name === "globalThis" || callee.object.name === "global" || callee.object.name === "window") && name === "fetch") report(context, node, "global fetch is banned; use Effect HttpClient");
371
+ }
372
+ };
373
+ }
374
+ },
375
+ "no-throw": noThrow,
376
+ "no-try": noTry,
377
+ "no-type-assertion": noTypeAssertion
378
+ });
379
+ export { ruleNames };
@@ -0,0 +1 @@
1
+ export declare const commandBypassesVerify: (command: string) => boolean;
package/dist/policy.js ADDED
@@ -0,0 +1,124 @@
1
+ // src/policy.ts
2
+ const noVerifySubcommands = /* @__PURE__ */ new Set([
3
+ "am",
4
+ "cherry-pick",
5
+ "commit",
6
+ "merge",
7
+ "pull",
8
+ "rebase"
9
+ ]);
10
+ const globalOptionsWithValue = /* @__PURE__ */ new Set([
11
+ "-C",
12
+ "-c",
13
+ "--config-env",
14
+ "--exec-path",
15
+ "--git-dir",
16
+ "--list-cmds",
17
+ "--namespace",
18
+ "--super-prefix",
19
+ "--work-tree"
20
+ ]);
21
+ const isGitToken = (token) => token === "git" || token.endsWith("/git");
22
+ const tokenAt = (tokens, index) => {
23
+ if (index < 0 || index >= tokens.length) return "";
24
+ const token = tokens[index];
25
+ if (token === void 0 || token === "") return "";
26
+ return token;
27
+ };
28
+ const tokenize = (command) => {
29
+ const tokens = [];
30
+ let current = "";
31
+ let quote = "";
32
+ let index = 0;
33
+ while (index < command.length) {
34
+ const char = command.charAt(index);
35
+ if (quote === "" && (char === " " || char === " " || char === "\n")) {
36
+ if (current !== "") {
37
+ tokens.push(current);
38
+ current = "";
39
+ }
40
+ index += 1;
41
+ continue;
42
+ }
43
+ if (quote === "" && (char === "'" || char === "\"")) {
44
+ quote = char;
45
+ index += 1;
46
+ continue;
47
+ }
48
+ if (quote !== "" && char === quote) {
49
+ quote = "";
50
+ index += 1;
51
+ continue;
52
+ }
53
+ current += char;
54
+ index += 1;
55
+ }
56
+ if (current !== "") tokens.push(current);
57
+ return tokens;
58
+ };
59
+ const subcommandOf = (tokens) => {
60
+ let gitAt = -1;
61
+ let index = 0;
62
+ while (index < tokens.length) {
63
+ if (isGitToken(tokenAt(tokens, index))) {
64
+ gitAt = index;
65
+ break;
66
+ }
67
+ index += 1;
68
+ }
69
+ if (gitAt < 0) return {
70
+ name: "",
71
+ at: -1
72
+ };
73
+ index = gitAt + 1;
74
+ while (index < tokens.length) {
75
+ const token = tokenAt(tokens, index);
76
+ if (token === "--") return {
77
+ name: "",
78
+ at: -1
79
+ };
80
+ if (token.startsWith("-")) {
81
+ const option = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
82
+ if (globalOptionsWithValue.has(option) && !token.includes("=")) {
83
+ index += 2;
84
+ continue;
85
+ }
86
+ index += 1;
87
+ continue;
88
+ }
89
+ return {
90
+ name: token,
91
+ at: index
92
+ };
93
+ }
94
+ return {
95
+ name: "",
96
+ at: -1
97
+ };
98
+ };
99
+ const shortClusterHasN = (tokens, from) => {
100
+ let index = from;
101
+ while (index < tokens.length) {
102
+ const token = tokenAt(tokens, index);
103
+ if (token.startsWith("-") && !token.startsWith("--") && token.includes("n")) return true;
104
+ index += 1;
105
+ }
106
+ return false;
107
+ };
108
+ const tokensBypass = (tokens) => {
109
+ const sub = subcommandOf(tokens);
110
+ if (noVerifySubcommands.has(sub.name) && shortClusterHasN(tokens, sub.at + 1)) return true;
111
+ let index = 0;
112
+ while (index < tokens.length) {
113
+ const token = tokenAt(tokens, index);
114
+ if ((token.includes(" ") || token.includes("\n")) && commandBypassesVerify(token)) return true;
115
+ index += 1;
116
+ }
117
+ return false;
118
+ };
119
+ const skipsHook = (command) => command.includes("--no-verify") || command.includes("LEFTHOOK=0") || command.includes("HUSKY=0") || command.includes("VP_GIT_HOOKS=0") || command.includes("VITE_GIT_HOOKS=0") || command.includes("vp hooks disable") || command.includes("vp.hooks.disabled");
120
+ const commandBypassesVerify = (command) => {
121
+ if (skipsHook(command)) return true;
122
+ return tokensBypass(tokenize(command));
123
+ };
124
+ export { commandBypassesVerify };
@@ -0,0 +1,3 @@
1
+ import { Effect } from "effect";
2
+ import { Stdio } from "effect/Stdio";
3
+ export declare const main: (cwd: string) => Effect.Effect<number, import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | Stdio>;
@@ -0,0 +1,49 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { writeStdout } from "./run.js";
3
+ import { verify } from "./verify.js";
4
+ import { doctor } from "./doctor.js";
5
+ import { explain } from "./explain.js";
6
+ import { init, sync } from "./scaffold.js";
7
+ import { Cause, Effect } from "effect";
8
+ import { Stdio } from "effect/Stdio";
9
+ // src/program.ts
10
+ const usage = "usage: aas verify | aas init | aas sync | aas explain [rule] | aas doctor";
11
+ const main = (cwd) => Effect.gen(function* () {
12
+ const args = yield* (yield* Stdio).args;
13
+ const command = args[0];
14
+ const rule = args[1];
15
+ return yield* dispatch(command, rule, cwd).pipe(Effect.catchTag("CommandFailed", (error) => Effect.gen(function* () {
16
+ yield* writeStdout(error.lines.join("\n"));
17
+ return 1;
18
+ })), Effect.catchCause((cause) => Effect.gen(function* () {
19
+ yield* writeStdout(Cause.pretty(cause));
20
+ return 1;
21
+ })));
22
+ });
23
+ const dispatch = (command, rule, cwd) => Effect.gen(function* () {
24
+ if (command === "verify") {
25
+ yield* verify(cwd);
26
+ return 0;
27
+ }
28
+ if (command === "init") {
29
+ yield* init(cwd);
30
+ return 0;
31
+ }
32
+ if (command === "sync") {
33
+ yield* sync(cwd);
34
+ return 0;
35
+ }
36
+ if (command === "explain") {
37
+ yield* explain(rule);
38
+ return 0;
39
+ }
40
+ if (command === "doctor") {
41
+ const report = yield* doctor(cwd);
42
+ yield* writeStdout(report.text);
43
+ if (!report.ok) return yield* new CommandFailed({ lines: [] });
44
+ return 0;
45
+ }
46
+ yield* writeStdout(usage);
47
+ return yield* new CommandFailed({ lines: [] });
48
+ });
49
+ export { main };
package/dist/run.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { Effect, FileSystem } from "effect";
3
+ import { Stdio } from "effect/Stdio";
4
+ import { ChildProcessSpawner } from "effect/process";
5
+ export declare const succeeded: (code: ChildProcessSpawner.ExitCode) => boolean;
6
+ export declare const findBin: (name: string) => Effect.Effect<string, CommandFailed | import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
7
+ export declare const runTool: (input: {
8
+ readonly bin: string;
9
+ readonly args: ReadonlyArray<string>;
10
+ readonly cwd: string;
11
+ }) => Effect.Effect<{
12
+ stdout: string;
13
+ stderr: string;
14
+ exitCode: ChildProcessSpawner.ExitCode;
15
+ }, import("effect/PlatformError").PlatformError, ChildProcessSpawner.ChildProcessSpawner>;
16
+ export declare const writeStdout: (text: string) => Effect.Effect<void, import("effect/PlatformError").PlatformError, Stdio>;
17
+ export declare const diagnosticsFrom: (text: string) => string[];
18
+ export declare const failLines: (name: string, text: string) => string[];
package/dist/run.js ADDED
@@ -0,0 +1,99 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { Effect, FileSystem, Stream } from "effect";
3
+ import { Stdio } from "effect/Stdio";
4
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
5
+ // src/run.ts
6
+ const succeeded = (code) => code === ChildProcessSpawner.ExitCode(0);
7
+ const findBin = (name) => Effect.gen(function* () {
8
+ const fs = yield* FileSystem.FileSystem;
9
+ const segments = import.meta.dirname.split("/");
10
+ let index = segments.length;
11
+ while (index > 1) {
12
+ index -= 1;
13
+ const candidate = `${segments.slice(0, index).join("/")}/node_modules/.bin/${name}`;
14
+ if (yield* fs.exists(candidate)) return candidate;
15
+ }
16
+ return yield* new CommandFailed({ lines: [`${name}:1: missing executable ${name}`] });
17
+ });
18
+ const runTool = (input) => Effect.gen(function* () {
19
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
20
+ return yield* Effect.scoped(Effect.gen(function* () {
21
+ const handle = yield* spawner.spawn(ChildProcess.make(input.bin, Array.from(input.args), {
22
+ cwd: input.cwd,
23
+ env: {
24
+ CI: "1",
25
+ NO_COLOR: "1"
26
+ },
27
+ extendEnv: true
28
+ }));
29
+ const [stdout, stderr] = yield* Effect.all([Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))], { concurrency: 2 });
30
+ return {
31
+ stdout,
32
+ stderr,
33
+ exitCode: yield* handle.exitCode
34
+ };
35
+ }));
36
+ });
37
+ const writeStdout = (text) => Effect.gen(function* () {
38
+ if (text.length === 0) return;
39
+ const stdio = yield* Stdio;
40
+ const body = text.endsWith("\n") ? text : `${text}\n`;
41
+ yield* Stream.run(Stream.make(body), stdio.stdout());
42
+ });
43
+ const lintLine = /^(.*?):(\d+):\d+:\s*(.*)$/u;
44
+ const tscLine = /^(.*?)\((\d+),\d+\):\s*(.*)$/u;
45
+ const formatLine = /^(\S+\.[A-Za-z0-9]+)(?:\s+\(\d+ms\))?$/u;
46
+ const escape = String.fromCharCode(27);
47
+ const stripAnsi = (text) => {
48
+ let result = "";
49
+ let index = 0;
50
+ while (index < text.length) {
51
+ if (text.charAt(index) === escape && text.charAt(index + 1) === "[") {
52
+ index += 2;
53
+ while (index < text.length) {
54
+ const code = text.charCodeAt(index);
55
+ index += 1;
56
+ if (code >= 64 && code <= 126) break;
57
+ }
58
+ continue;
59
+ }
60
+ result += text.charAt(index);
61
+ index += 1;
62
+ }
63
+ return result;
64
+ };
65
+ const pushUnique = (lines, line) => {
66
+ if (!lines.includes(line)) lines.push(line);
67
+ };
68
+ const diagnosticsFrom = (text) => {
69
+ const lines = [];
70
+ for (const raw of stripAnsi(text).split("\n")) {
71
+ const trimmed = raw.trim();
72
+ if (trimmed === "") continue;
73
+ const lint = lintLine.exec(trimmed);
74
+ const lintFile = lint?.[1];
75
+ const lintNo = lint?.[2];
76
+ const lintMessage = lint?.[3];
77
+ if (lintFile !== void 0 && lintNo !== void 0 && lintMessage !== void 0) {
78
+ pushUnique(lines, `${lintFile}:${lintNo}: ${lintMessage}`);
79
+ continue;
80
+ }
81
+ const tsc = tscLine.exec(trimmed);
82
+ const tscFile = tsc?.[1];
83
+ const tscNo = tsc?.[2];
84
+ const tscMessage = tsc?.[3];
85
+ if (tscFile !== void 0 && tscNo !== void 0 && tscMessage !== void 0) {
86
+ pushUnique(lines, `${tscFile}:${tscNo}: ${tscMessage}`);
87
+ continue;
88
+ }
89
+ const formattedFile = formatLine.exec(trimmed)?.[1];
90
+ if (formattedFile !== void 0 && !formattedFile.includes("://")) pushUnique(lines, `${formattedFile}:1: format`);
91
+ }
92
+ return lines;
93
+ };
94
+ const failLines = (name, text) => {
95
+ const lines = diagnosticsFrom(text);
96
+ if (lines.length > 0) return lines;
97
+ return [`${name}:1: ${text.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "failed"}`];
98
+ };
99
+ export { diagnosticsFrom, failLines, findBin, runTool, succeeded, writeStdout };
@@ -0,0 +1,5 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { Effect, FileSystem } from "effect";
3
+ export declare const writeTemplates: (cwd: string) => Effect.Effect<void, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
4
+ export declare const init: (cwd: string) => Effect.Effect<void, CommandFailed | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | import("effect/Stdio").Stdio>;
5
+ export declare const sync: typeof writeTemplates;