pi-chalin 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.
@@ -0,0 +1,282 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { resolveChalinPaths, type ChalinPathsOptions } from "./paths.ts";
5
+
6
+ export interface ProjectSnapshot {
7
+ version: 1;
8
+ createdAt: string;
9
+ cwd: string;
10
+ cacheKey: string;
11
+ stack: string[];
12
+ signals: string[];
13
+ packageManagers: string[];
14
+ testCommands: string[];
15
+ buildCommands: string[];
16
+ entrypoints: string[];
17
+ highSignalFiles: string[];
18
+ git?: {
19
+ branch?: string;
20
+ head?: string;
21
+ changedFiles: string[];
22
+ recentCommits: string[];
23
+ };
24
+ }
25
+
26
+ interface StackSignal {
27
+ file: string;
28
+ stack: string;
29
+ packageManager?: string;
30
+ testCommands?: string[];
31
+ buildCommands?: string[];
32
+ }
33
+
34
+ const STACK_SIGNALS: StackSignal[] = [
35
+ { file: "package.json", stack: "node" },
36
+ { file: "go.mod", stack: "go", testCommands: ["go test ./..."], buildCommands: ["go build ./..."] },
37
+ { file: "Cargo.toml", stack: "rust", packageManager: "cargo", testCommands: ["cargo test"], buildCommands: ["cargo build"] },
38
+ { file: "pyproject.toml", stack: "python", packageManager: "python", testCommands: ["pytest"], buildCommands: ["python -m build"] },
39
+ { file: "requirements.txt", stack: "python", packageManager: "python", testCommands: ["pytest"] },
40
+ { file: "pom.xml", stack: "java", packageManager: "maven", testCommands: ["mvn test"], buildCommands: ["mvn package"] },
41
+ { file: "build.gradle", stack: "java", packageManager: "gradle", testCommands: ["gradle test"], buildCommands: ["gradle build"] },
42
+ { file: "Gemfile", stack: "ruby", packageManager: "bundler", testCommands: ["bundle exec rspec"] },
43
+ { file: "composer.json", stack: "php", packageManager: "composer", testCommands: ["composer test"] },
44
+ { file: "Makefile", stack: "make" },
45
+ ];
46
+
47
+ const HIGH_SIGNAL_ROOT_FILES = [
48
+ "README.md",
49
+ "AGENTS.md",
50
+ "Makefile",
51
+ "Taskfile.yml",
52
+ "Dockerfile",
53
+ "docker-compose.yml",
54
+ "package.json",
55
+ "go.mod",
56
+ "Cargo.toml",
57
+ "pyproject.toml",
58
+ "pom.xml",
59
+ "build.gradle",
60
+ "tsconfig.json",
61
+ "vite.config.ts",
62
+ "vitest.config.ts",
63
+ "next.config.js",
64
+ "nuxt.config.ts",
65
+ ];
66
+
67
+ const ENTRYPOINT_DIRS = ["cmd", "src", "app", "pages", "components", "internal", "pkg", "lib", "server", "api"];
68
+ const TEST_DIRS = ["test", "tests", "__tests__", "spec", "e2e"];
69
+
70
+ export function buildProjectSnapshot(options: ChalinPathsOptions & { maxAgeMs?: number }): ProjectSnapshot {
71
+ const cwd = path.resolve(options.cwd ?? process.cwd());
72
+ const cachePath = projectSnapshotCachePath({ cwd });
73
+ const cacheKey = computeCacheKey(cwd);
74
+ const maxAgeMs = options.maxAgeMs ?? snapshotMaxAgeMs();
75
+ const cached = readCachedSnapshot(cachePath);
76
+ if (cached && cached.cacheKey === cacheKey && Date.now() - Date.parse(cached.createdAt) <= maxAgeMs) return cached;
77
+
78
+ const snapshot = createProjectSnapshot(cwd, cacheKey);
79
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
80
+ fs.writeFileSync(cachePath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
81
+ return snapshot;
82
+ }
83
+
84
+ export function formatProjectSnapshot(snapshot: ProjectSnapshot): string {
85
+ return [
86
+ `stack: ${snapshot.stack.join(", ") || "unknown"}`,
87
+ `signals: ${snapshot.signals.slice(0, 12).join(", ") || "none"}`,
88
+ snapshot.packageManagers.length ? `package managers: ${snapshot.packageManagers.join(", ")}` : undefined,
89
+ snapshot.testCommands.length ? `test commands: ${snapshot.testCommands.join(" | ")}` : undefined,
90
+ snapshot.buildCommands.length ? `build commands: ${snapshot.buildCommands.join(" | ")}` : undefined,
91
+ snapshot.git?.branch ? `git: ${snapshot.git.branch} @ ${snapshot.git.head ?? "unknown"}` : undefined,
92
+ snapshot.git?.changedFiles.length ? `changed files: ${snapshot.git.changedFiles.slice(0, 10).join(", ")}` : undefined,
93
+ snapshot.git?.recentCommits.length ? `recent commits: ${snapshot.git.recentCommits.slice(0, 5).join(" | ")}` : undefined,
94
+ snapshot.entrypoints.length ? `entrypoints: ${snapshot.entrypoints.slice(0, 10).join(", ")}` : undefined,
95
+ snapshot.highSignalFiles.length ? `high-signal files: ${snapshot.highSignalFiles.slice(0, 12).join(", ")}` : undefined,
96
+ ].filter((line): line is string => Boolean(line)).join("\n");
97
+ }
98
+
99
+ export function projectSnapshotCachePath(options: ChalinPathsOptions): string {
100
+ return path.join(resolveChalinPaths(options).projectRoot, ".pi-chalin", "cache", "project-snapshot.json");
101
+ }
102
+
103
+ function createProjectSnapshot(cwd: string, cacheKey: string): ProjectSnapshot {
104
+ const rootEntries = safeReaddir(cwd);
105
+ const rootFiles = new Set(rootEntries.filter((entry) => safeStat(path.join(cwd, entry))?.isFile()));
106
+ const rootDirs = new Set(rootEntries.filter((entry) => safeStat(path.join(cwd, entry))?.isDirectory()));
107
+ const stack = new Set<string>();
108
+ const signals = new Set<string>();
109
+ const packageManagers = new Set<string>();
110
+ const testCommands = new Set<string>();
111
+ const buildCommands = new Set<string>();
112
+
113
+ for (const signal of STACK_SIGNALS) {
114
+ if (!rootFiles.has(signal.file)) continue;
115
+ stack.add(signal.stack);
116
+ signals.add(signal.file);
117
+ if (signal.packageManager) packageManagers.add(signal.packageManager);
118
+ for (const command of signal.testCommands ?? []) testCommands.add(command);
119
+ for (const command of signal.buildCommands ?? []) buildCommands.add(command);
120
+ }
121
+
122
+ if (rootFiles.has("package.json")) addPackageJsonSignals(cwd, stack, packageManagers, testCommands, buildCommands);
123
+ for (const lock of ["bun.lock", "bun.lockb", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"]) {
124
+ if (rootFiles.has(lock)) signals.add(lock);
125
+ }
126
+
127
+ const entrypoints = discoverEntrypoints(cwd, rootDirs);
128
+ const highSignalFiles = discoverHighSignalFiles(cwd, rootFiles, rootDirs);
129
+ return {
130
+ version: 1,
131
+ createdAt: new Date().toISOString(),
132
+ cwd,
133
+ cacheKey,
134
+ stack: [...stack].sort(),
135
+ signals: [...signals].sort(),
136
+ packageManagers: [...packageManagers].sort(),
137
+ testCommands: [...testCommands].slice(0, 8),
138
+ buildCommands: [...buildCommands].slice(0, 8),
139
+ entrypoints,
140
+ highSignalFiles,
141
+ git: gitSnapshot(cwd),
142
+ };
143
+ }
144
+
145
+ function addPackageJsonSignals(cwd: string, stack: Set<string>, packageManagers: Set<string>, testCommands: Set<string>, buildCommands: Set<string>): void {
146
+ try {
147
+ const parsed = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8")) as {
148
+ scripts?: Record<string, string>;
149
+ dependencies?: Record<string, string>;
150
+ devDependencies?: Record<string, string>;
151
+ packageManager?: string;
152
+ };
153
+ const deps = new Set(Object.keys({ ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) }));
154
+ for (const dep of deps) {
155
+ if (["vue", "nuxt", "react", "next", "svelte", "astro"].includes(dep)) stack.add(dep);
156
+ if (["vitest", "jest", "playwright"].includes(dep)) stack.add(dep);
157
+ if (dep === "typescript") stack.add("typescript");
158
+ }
159
+ const manager = parsed.packageManager?.split("@")[0];
160
+ packageManagers.add(manager || packageManagerFromLock(cwd) || "bun");
161
+ for (const [name] of Object.entries(parsed.scripts ?? {})) {
162
+ const packageManager = manager || packageManagerFromLock(cwd) || "bun";
163
+ if (/^(test|test:|vitest|jest)/.test(name)) testCommands.add(`${packageManager} run ${name}`);
164
+ if (/^(build|typecheck|lint)$/.test(name)) buildCommands.add(`${packageManager} run ${name}`);
165
+ }
166
+ } catch {
167
+ stack.add("node");
168
+ packageManagers.add(packageManagerFromLock(cwd) || "bun");
169
+ }
170
+ }
171
+
172
+ function packageManagerFromLock(cwd: string): string | undefined {
173
+ if (fs.existsSync(path.join(cwd, "bun.lock")) || fs.existsSync(path.join(cwd, "bun.lockb"))) return "bun";
174
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
175
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
176
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
177
+ return undefined;
178
+ }
179
+
180
+ function discoverEntrypoints(cwd: string, rootDirs: Set<string>): string[] {
181
+ const candidates = [
182
+ "main.go",
183
+ "cmd/*/main.go",
184
+ "src/index.ts",
185
+ "src/index.js",
186
+ "src/main.ts",
187
+ "src/main.js",
188
+ "app/page.tsx",
189
+ "pages/index.vue",
190
+ "server/index.ts",
191
+ ];
192
+ const found = new Set<string>();
193
+ for (const pattern of candidates) {
194
+ if (pattern.includes("*")) {
195
+ const [prefix, suffix] = pattern.split("*") as [string, string];
196
+ const dir = path.join(cwd, prefix);
197
+ for (const entry of safeReaddir(dir)) {
198
+ const candidate = path.join(prefix, entry, suffix).replaceAll(path.sep, "/");
199
+ if (fs.existsSync(path.join(cwd, candidate))) found.add(candidate);
200
+ }
201
+ } else if (fs.existsSync(path.join(cwd, pattern))) found.add(pattern);
202
+ }
203
+ for (const dir of ENTRYPOINT_DIRS) {
204
+ if (!rootDirs.has(dir)) continue;
205
+ for (const entry of safeReaddir(path.join(cwd, dir)).slice(0, 5)) found.add(path.join(dir, entry).replaceAll(path.sep, "/"));
206
+ }
207
+ return [...found].slice(0, 16);
208
+ }
209
+
210
+ function discoverHighSignalFiles(cwd: string, rootFiles: Set<string>, rootDirs: Set<string>): string[] {
211
+ const result = new Set<string>();
212
+ for (const file of HIGH_SIGNAL_ROOT_FILES) if (rootFiles.has(file)) result.add(file);
213
+ for (const dir of [...ENTRYPOINT_DIRS, ...TEST_DIRS, "docs", ".github"]) {
214
+ if (!rootDirs.has(dir)) continue;
215
+ result.add(`${dir}/`);
216
+ for (const entry of safeReaddir(path.join(cwd, dir)).slice(0, 5)) result.add(path.join(dir, entry).replaceAll(path.sep, "/"));
217
+ }
218
+ return [...result].slice(0, 32);
219
+ }
220
+
221
+ function gitSnapshot(cwd: string): ProjectSnapshot["git"] | undefined {
222
+ if (!fs.existsSync(path.join(cwd, ".git"))) return undefined;
223
+ const branch = git(cwd, ["branch", "--show-current"]).trim() || undefined;
224
+ const head = git(cwd, ["rev-parse", "--short", "HEAD"]).trim() || undefined;
225
+ const changedFiles = git(cwd, ["diff", "--name-status", "HEAD~1...HEAD"]).trim().split("\n").filter(Boolean).slice(0, 20);
226
+ const recentCommits = git(cwd, ["log", "--oneline", "-5"]).trim().split("\n").filter(Boolean);
227
+ return { branch, head, changedFiles, recentCommits };
228
+ }
229
+
230
+ function computeCacheKey(cwd: string): string {
231
+ const parts = [
232
+ git(cwd, ["rev-parse", "HEAD"]).trim(),
233
+ ...HIGH_SIGNAL_ROOT_FILES.map((file) => `${file}:${mtime(path.join(cwd, file))}`),
234
+ ...STACK_SIGNALS.map((signal) => `${signal.file}:${mtime(path.join(cwd, signal.file))}`),
235
+ ];
236
+ return stableHash(parts.join("|"));
237
+ }
238
+
239
+ function git(cwd: string, args: string[]): string {
240
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8", timeout: 2500 });
241
+ return result.status === 0 ? result.stdout : "";
242
+ }
243
+
244
+ function readCachedSnapshot(cachePath: string): ProjectSnapshot | undefined {
245
+ try {
246
+ const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8")) as ProjectSnapshot;
247
+ return parsed.version === 1 ? parsed : undefined;
248
+ } catch {
249
+ return undefined;
250
+ }
251
+ }
252
+
253
+ function safeReaddir(dir: string): string[] {
254
+ try {
255
+ return fs.readdirSync(dir).filter((entry) => !["node_modules", ".git", ".pi-chalin", "dist", "coverage"].includes(entry));
256
+ } catch {
257
+ return [];
258
+ }
259
+ }
260
+
261
+ function safeStat(filePath: string): fs.Stats | undefined {
262
+ try {
263
+ return fs.statSync(filePath);
264
+ } catch {
265
+ return undefined;
266
+ }
267
+ }
268
+
269
+ function mtime(filePath: string): number {
270
+ return safeStat(filePath)?.mtimeMs ?? 0;
271
+ }
272
+
273
+ function snapshotMaxAgeMs(): number {
274
+ const parsed = Number(process.env.PI_CHALIN_SNAPSHOT_MAX_AGE_MS);
275
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 10 * 60 * 1000;
276
+ }
277
+
278
+ function stableHash(input: string): string {
279
+ let hash = 5381;
280
+ for (let index = 0; index < input.length; index++) hash = (hash * 33) ^ input.charCodeAt(index);
281
+ return (hash >>> 0).toString(16);
282
+ }
@@ -0,0 +1,4 @@
1
+ declare module "sql.js-fts5/dist/sql-asm.js" {
2
+ const initSqlJs: () => Promise<any>;
3
+ export default initSqlJs;
4
+ }