@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,76 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { failLines, findBin, runTool, succeeded, writeStdout } from "./run.js";
3
+ import { managedFiles, readTemplate } from "./templates.js";
4
+ import { mergeAllowBuilds } from "./workspace.js";
5
+ import { ensureVerifyScript } from "./package-script.js";
6
+ import { Effect, FileSystem } from "effect";
7
+ // src/scaffold.ts
8
+ const parentOf = (path) => {
9
+ const slash = path.lastIndexOf("/");
10
+ if (slash <= 0) return "";
11
+ return path.slice(0, slash);
12
+ };
13
+ const writeTemplates = (cwd) => Effect.gen(function* () {
14
+ const fs = yield* FileSystem.FileSystem;
15
+ for (const file of managedFiles) {
16
+ const template = yield* readTemplate(file.template);
17
+ const target = `${cwd}/${file.path}`;
18
+ const parent = parentOf(target);
19
+ if (parent !== "") yield* fs.makeDirectory(parent, { recursive: true });
20
+ const previous = (yield* fs.exists(target)) ? yield* fs.readFileString(target) : "";
21
+ const text = file.template === "pnpm-workspace.yaml" && previous !== "" ? mergeAllowBuilds(template, previous) : template;
22
+ yield* fs.writeFileString(target, text);
23
+ }
24
+ });
25
+ const installHooks = (cwd) => Effect.gen(function* () {
26
+ const git = yield* runTool({
27
+ bin: "git",
28
+ args: ["rev-parse", "--git-dir"],
29
+ cwd
30
+ });
31
+ if (!succeeded(git.exitCode)) return;
32
+ const hooksPath = yield* runTool({
33
+ bin: "git",
34
+ args: [
35
+ "config",
36
+ "--get",
37
+ "core.hooksPath"
38
+ ],
39
+ cwd
40
+ });
41
+ if (succeeded(hooksPath.exitCode)) {
42
+ yield* writeStdout(`core.hooksPath is ${hooksPath.stdout.trim()}; vp hooks enable skipped`);
43
+ return;
44
+ }
45
+ const vp = yield* findBin("vp");
46
+ const installed = yield* runTool({
47
+ bin: vp,
48
+ args: ["hooks", "enable"],
49
+ cwd
50
+ });
51
+ if (!succeeded(installed.exitCode)) return yield* new CommandFailed({ lines: failOr(installed.stdout, installed.stderr) });
52
+ });
53
+ const failOr = (stdout, stderr) => {
54
+ const text = `${stdout}\n${stderr}`.trim();
55
+ return [`vp:1: ${text === "" ? "vp hooks enable failed" : text.split("\n")[0] ?? "failed"}`];
56
+ };
57
+ const refreshLockfile = (cwd) => Effect.gen(function* () {
58
+ const fs = yield* FileSystem.FileSystem;
59
+ if (!(yield* fs.exists(`${cwd}/pnpm-lock.yaml`))) return;
60
+ const state = `${cwd}/node_modules/.pnpm-workspace-state-v1.json`;
61
+ if (yield* fs.exists(state)) yield* fs.remove(state);
62
+ const installed = yield* runTool({
63
+ bin: "pnpm",
64
+ args: ["install"],
65
+ cwd
66
+ });
67
+ if (!succeeded(installed.exitCode)) return yield* new CommandFailed({ lines: failLines("pnpm", `${installed.stdout}\n${installed.stderr}`) });
68
+ });
69
+ const init = (cwd) => Effect.gen(function* () {
70
+ yield* writeTemplates(cwd);
71
+ yield* ensureVerifyScript(cwd);
72
+ yield* installHooks(cwd);
73
+ yield* refreshLockfile(cwd);
74
+ });
75
+ const sync = writeTemplates;
76
+ export { init, sync, writeTemplates };
@@ -0,0 +1,6 @@
1
+ import { Effect, FileSystem } from "effect";
2
+ export declare const managedFiles: ReadonlyArray<{
3
+ readonly template: string;
4
+ readonly path: string;
5
+ }>;
6
+ export declare const readTemplate: (name: string) => Effect.Effect<string, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
@@ -0,0 +1,37 @@
1
+ import { Effect, FileSystem } from "effect";
2
+ // src/templates.ts
3
+ const managedFiles = [
4
+ {
5
+ template: "hooks.json",
6
+ path: ".cursor/hooks.json"
7
+ },
8
+ {
9
+ template: "pre-commit",
10
+ path: ".vite-hooks/pre-commit"
11
+ },
12
+ {
13
+ template: "pnpm-workspace.yaml",
14
+ path: "pnpm-workspace.yaml"
15
+ },
16
+ {
17
+ template: "AGENTS.md",
18
+ path: "AGENTS.md"
19
+ },
20
+ {
21
+ template: "verify.yml",
22
+ path: ".github/workflows/verify.yml"
23
+ },
24
+ {
25
+ template: "vite.config.ts",
26
+ path: "vite.config.ts"
27
+ },
28
+ {
29
+ template: "tsconfig.json",
30
+ path: "tsconfig.json"
31
+ }
32
+ ];
33
+ const templateRoot = `${import.meta.dirname}/../templates`;
34
+ const readTemplate = (name) => Effect.gen(function* () {
35
+ return yield* (yield* FileSystem.FileSystem).readFileString(`${templateRoot}/${name}`);
36
+ });
37
+ export { managedFiles, readTemplate };
@@ -0,0 +1,4 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { Effect, FileSystem } from "effect";
3
+ export declare const driftLines: (cwd: string) => Effect.Effect<string[], import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
4
+ export declare const verify: (cwd: string) => Effect.Effect<undefined, CommandFailed | import("effect/PlatformError").PlatformError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem>;
package/dist/verify.js ADDED
@@ -0,0 +1,190 @@
1
+ import { CommandFailed } from "./errors.js";
2
+ import { diagnosticsFrom, failLines, findBin, runTool, succeeded } from "./run.js";
3
+ import { managedFiles, readTemplate } from "./templates.js";
4
+ import { workspaceDrift } from "./workspace.js";
5
+ import { Effect, FileSystem, Result, Schema } from "effect";
6
+ // src/verify.ts
7
+ const driftLines = (cwd) => Effect.gen(function* () {
8
+ const fs = yield* FileSystem.FileSystem;
9
+ const lines = [];
10
+ for (const file of managedFiles) {
11
+ const expected = yield* readTemplate(file.template);
12
+ const target = `${cwd}/${file.path}`;
13
+ const present = yield* fs.exists(target);
14
+ const actual = present ? yield* fs.readFileString(target) : "";
15
+ if (file.template === "pnpm-workspace.yaml") {
16
+ const reason = present ? workspaceDrift(expected, actual) : "drifted from the aas template; run aas sync";
17
+ if (reason !== "") lines.push(`${file.path}:1: ${reason}`);
18
+ continue;
19
+ }
20
+ if (!present || actual !== expected) lines.push(`${file.path}:1: drifted from the aas template; run aas sync`);
21
+ }
22
+ return lines;
23
+ });
24
+ const checkDrift = (cwd) => Effect.gen(function* () {
25
+ const lines = yield* driftLines(cwd);
26
+ if (lines.length > 0) return yield* new CommandFailed({ lines });
27
+ });
28
+ const patchTypeScript = (tsc) => Effect.gen(function* () {
29
+ const at = tsc.lastIndexOf("/node_modules/.bin/");
30
+ if (at <= 0) return yield* new CommandFailed({ lines: [`tsc:1: unable to locate the typescript package for ${tsc}`] });
31
+ const effectTsgo = yield* findBin("effect-tsgo");
32
+ yield* runChecked({
33
+ bin: effectTsgo,
34
+ args: ["patch", "--typescript"],
35
+ cwd: tsc.slice(0, at),
36
+ label: "effect-tsgo"
37
+ });
38
+ });
39
+ const runChecked = (input) => Effect.gen(function* () {
40
+ const result = yield* runTool(input);
41
+ if (succeeded(result.exitCode)) return;
42
+ return yield* new CommandFailed({ lines: failLines(input.label, `${result.stdout}\n${result.stderr}`) });
43
+ });
44
+ const packTargets = (cwd) => Effect.gen(function* () {
45
+ const fs = yield* FileSystem.FileSystem;
46
+ const targets = [];
47
+ const packagesDir = `${cwd}/packages`;
48
+ if (yield* fs.exists(packagesDir)) {
49
+ const sorted = [...yield* fs.readDirectory(packagesDir)].sort();
50
+ for (const name of sorted) {
51
+ const dir = `${packagesDir}/${name}`;
52
+ if (yield* fs.exists(`${dir}/src/index.ts`)) targets.push(dir);
53
+ }
54
+ }
55
+ if (targets.length === 0 && (yield* fs.exists(`${cwd}/src/index.ts`))) targets.push(cwd);
56
+ if (targets.includes(`${cwd}/packages/cli`)) {
57
+ const rest = targets.filter((dir) => dir !== `${cwd}/packages/cli`);
58
+ return [`${cwd}/packages/cli`, ...rest];
59
+ }
60
+ return targets;
61
+ });
62
+ const verify = (cwd) => Effect.gen(function* () {
63
+ yield* checkDrift(cwd);
64
+ const fs = yield* FileSystem.FileSystem;
65
+ const vp = yield* findBin("vp");
66
+ const tsc = yield* findBin("tsc");
67
+ if (yield* fs.exists(`${cwd}/.env.schema`)) {
68
+ const varlock = yield* findBin("varlock");
69
+ yield* runChecked({
70
+ bin: varlock,
71
+ args: ["load"],
72
+ cwd,
73
+ label: "varlock"
74
+ });
75
+ }
76
+ yield* runChecked({
77
+ bin: vp,
78
+ args: [
79
+ "fmt",
80
+ "--check",
81
+ "."
82
+ ],
83
+ cwd,
84
+ label: "fmt"
85
+ });
86
+ yield* runChecked({
87
+ bin: vp,
88
+ args: [
89
+ "lint",
90
+ "--format",
91
+ "unix"
92
+ ],
93
+ cwd,
94
+ label: "lint"
95
+ });
96
+ const targets = yield* packTargets(cwd);
97
+ for (const target of targets) yield* runChecked({
98
+ bin: vp,
99
+ args: [
100
+ "pack",
101
+ "--logLevel",
102
+ "error"
103
+ ],
104
+ cwd: target,
105
+ label: "pack"
106
+ });
107
+ if (yield* fs.exists(`${cwd}/tsconfig.json`)) {
108
+ yield* patchTypeScript(tsc);
109
+ yield* runChecked({
110
+ bin: tsc,
111
+ args: [
112
+ "--noEmit",
113
+ "--pretty",
114
+ "false",
115
+ "-p",
116
+ "tsconfig.json"
117
+ ],
118
+ cwd,
119
+ label: "tsc"
120
+ });
121
+ }
122
+ const reportPath = `${cwd}/.vitest/aas-report.json`;
123
+ const tested = yield* runTool({
124
+ bin: vp,
125
+ args: [
126
+ "test",
127
+ "run",
128
+ "--reporter=json",
129
+ "--outputFile",
130
+ ".vitest/aas-report.json"
131
+ ],
132
+ cwd
133
+ });
134
+ const report = (yield* fs.exists(reportPath)) ? yield* fs.readFileString(reportPath) : "";
135
+ yield* fs.remove(reportPath).pipe(Effect.ignore);
136
+ if (!succeeded(tested.exitCode)) {
137
+ const parsed = testFailures(report === "" ? tested.stdout : report, cwd);
138
+ return yield* new CommandFailed({ lines: parsed.length > 0 ? parsed : failLines("test", `${tested.stdout}\n${tested.stderr}`) });
139
+ }
140
+ });
141
+ const Assertion = Schema.Struct({
142
+ status: Schema.String,
143
+ title: Schema.optionalKey(Schema.String),
144
+ failureMessages: Schema.optionalKey(Schema.Array(Schema.String))
145
+ });
146
+ const TestFile = Schema.Struct({
147
+ name: Schema.optionalKey(Schema.String),
148
+ status: Schema.optionalKey(Schema.String),
149
+ message: Schema.optionalKey(Schema.String),
150
+ assertionResults: Schema.optionalKey(Schema.Array(Assertion))
151
+ });
152
+ const Report = Schema.Struct({ testResults: Schema.optionalKey(Schema.Array(TestFile)) });
153
+ const decodeReport = (text) => Schema.decodeResult(Schema.fromJsonString(Report))(text);
154
+ const failureLine = /([A-Za-z0-9_./-]+\.ts):(\d+):\d+/u;
155
+ const relativeTo = (file, cwd) => {
156
+ const prefix = `${cwd}/`;
157
+ return file.startsWith(prefix) ? file.slice(prefix.length) : file;
158
+ };
159
+ const firstLine = (text) => text.split("\n")[0] ?? "failed";
160
+ const testFailures = (text, cwd) => {
161
+ const start = text.indexOf("{");
162
+ const end = text.lastIndexOf("}");
163
+ if (start < 0 || end <= start) return diagnosticsFrom(text);
164
+ const decoded = decodeReport(text.slice(start, end + 1));
165
+ if (Result.isFailure(decoded)) return diagnosticsFrom(text);
166
+ const results = decoded.success.testResults;
167
+ if (results === void 0) return diagnosticsFrom(text);
168
+ const lines = [];
169
+ for (const result of results) {
170
+ const assertions = result.assertionResults ?? [];
171
+ let failedAssertions = 0;
172
+ for (const assertion of assertions) {
173
+ if (assertion.status !== "failed") continue;
174
+ failedAssertions += 1;
175
+ const message = assertion.failureMessages?.[0];
176
+ const located = message === void 0 ? null : failureLine.exec(message);
177
+ const locatedFile = located?.[1];
178
+ const file = relativeTo(locatedFile ?? result.name ?? "test", cwd);
179
+ const line = located?.[2] ?? "1";
180
+ const reason = message === void 0 ? assertion.title ?? "failed" : firstLine(message);
181
+ lines.push(`${file}:${line}: ${reason}`);
182
+ }
183
+ if (failedAssertions === 0 && result.status === "failed") {
184
+ const file = relativeTo(result.name ?? "test", cwd);
185
+ lines.push(`${file}:1: ${firstLine(result.message ?? "failed")}`);
186
+ }
187
+ }
188
+ return lines;
189
+ };
190
+ export { driftLines, verify };
@@ -0,0 +1,4 @@
1
+ export declare const scalarEntries: (text: string, key: string) => (readonly [string, string])[] | undefined;
2
+ export declare const workspaceDrift: (template: string, actual: string) => "" | "drifted from the aas template; run aas sync";
3
+ export declare const mergeAllowBuilds: (template: string, previous: string) => string;
4
+ export declare const sameScalars: (left: ReadonlyArray<readonly [string, string]>, right: ReadonlyArray<readonly [string, string]>) => boolean;
@@ -0,0 +1,76 @@
1
+ // src/workspace.ts
2
+ const unquote = (raw) => {
3
+ const trimmed = raw.trim();
4
+ if (trimmed.length >= 2 && (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
5
+ return trimmed;
6
+ };
7
+ const linesOf = (text) => text.split("\n");
8
+ const sectionRange = (lines, key) => {
9
+ const start = lines.findIndex((line) => line === `${key}:`);
10
+ if (start < 0) return;
11
+ let end = start + 1;
12
+ while (end < lines.length) {
13
+ const line = lines[end] ?? "";
14
+ if (line === "" || line.startsWith(" ") || line.startsWith("#")) {
15
+ end += 1;
16
+ continue;
17
+ }
18
+ break;
19
+ }
20
+ return {
21
+ start,
22
+ end
23
+ };
24
+ };
25
+ const sectionBody = (text, key) => {
26
+ const lines = linesOf(text);
27
+ const range = sectionRange(lines, key);
28
+ if (range === void 0) return;
29
+ return lines.slice(range.start + 1, range.end).join("\n");
30
+ };
31
+ const remainder = (text, key) => {
32
+ const lines = linesOf(text);
33
+ const range = sectionRange(lines, key);
34
+ if (range === void 0) return text;
35
+ return [...lines.slice(0, range.start), ...lines.slice(range.end)].join("\n");
36
+ };
37
+ const entryPattern = /^("?)([A-Za-z0-9@/_.+*-]+)\1:\s*(.+)$/u;
38
+ const scalarEntries = (text, key) => {
39
+ const body = sectionBody(text, key);
40
+ if (body === void 0) return;
41
+ const entries = [];
42
+ for (const line of body.split("\n")) {
43
+ const trimmed = line.trim();
44
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
45
+ if (trimmed.startsWith("-")) return;
46
+ const matched = entryPattern.exec(trimmed);
47
+ const name = matched?.[2];
48
+ const value = matched?.[3];
49
+ if (name === void 0 || value === void 0) return;
50
+ entries.push([name, unquote(value)]);
51
+ }
52
+ return entries;
53
+ };
54
+ const drift = "drifted from the aas template; run aas sync";
55
+ const workspaceDrift = (template, actual) => {
56
+ if (remainder(template, "allowBuilds") !== remainder(actual, "allowBuilds")) return drift;
57
+ const expected = scalarEntries(template, "allowBuilds");
58
+ const found = scalarEntries(actual, "allowBuilds");
59
+ if (expected === void 0 || found === void 0) return drift;
60
+ for (const [key, value] of expected) {
61
+ const match = found.find((entry) => entry[0] === key);
62
+ if (match === void 0 || match[1] !== value) return drift;
63
+ }
64
+ return "";
65
+ };
66
+ const mergeAllowBuilds = (template, previous) => {
67
+ const expected = scalarEntries(template, "allowBuilds");
68
+ const found = scalarEntries(previous, "allowBuilds");
69
+ if (expected === void 0 || found === void 0) return template;
70
+ const extras = found.filter((entry) => !expected.some((required) => required[0] === entry[0])).sort((left, right) => left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0);
71
+ if (extras.length === 0) return template;
72
+ return `${template.endsWith("\n") ? template.slice(0, -1) : template}\n${extras.map(([key, value]) => ` ${key}: ${value}`).join("\n")}\n`;
73
+ };
74
+ const pairText = (entries) => entries.map(([key, value]) => `${key}=${value}`).sort().join("\n");
75
+ const sameScalars = (left, right) => pairText(left) === pairText(right);
76
+ export { mergeAllowBuilds, sameScalars, scalarEntries, workspaceDrift };
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@aas-dev/stack",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "The aas command. One installable toolchain: Vite+, Effect tsgo, and the fence.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://origin.cursor.com/git/kristoffer-aas/aas-stack.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "bin": {
12
+ "aas": "./bin/aas.js"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "dist",
17
+ "plugin",
18
+ "templates",
19
+ "tsconfig.base.json"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./config": {
28
+ "types": "./dist/config.d.ts",
29
+ "import": "./dist/config.js"
30
+ },
31
+ "./tsconfig.base.json": "./tsconfig.base.json"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@effect/platform-node": "4.0.6",
38
+ "@effect/tsgo": "0.56.0",
39
+ "effect": "4.0.6",
40
+ "typescript": "7.0.2",
41
+ "varlock": "1.23.0",
42
+ "vite-plus": "1.4.0"
43
+ },
44
+ "devDependencies": {
45
+ "@effect/vitest": "4.0.6",
46
+ "@types/node": "22.20.5"
47
+ },
48
+ "engines": {
49
+ "node": ">=22.18.0",
50
+ "pnpm": "11.3.0"
51
+ },
52
+ "scripts": {
53
+ "typecheck": "tsc --noEmit --pretty false -p tsconfig.json",
54
+ "test": "vp test run",
55
+ "build": "vp pack --logLevel error"
56
+ }
6
57
  }
@@ -0,0 +1,8 @@
1
+ export declare const ruleNames: ReadonlyArray<string>
2
+
3
+ declare const plugin: {
4
+ readonly meta: { readonly name: string }
5
+ readonly rules: Readonly<Record<string, object>>
6
+ }
7
+
8
+ export default plugin