@wkronmiller/lisa 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/LICENSE +21 -0
- package/README.md +407 -0
- package/bin/lisa-runtime.js +8797 -0
- package/bin/lisa.js +21 -0
- package/completion.ts +58 -0
- package/install.ps1 +51 -0
- package/install.sh +93 -0
- package/lisa.ts +6 -0
- package/package.json +66 -0
- package/skills/README.md +28 -0
- package/skills/claude-code/CLAUDE.md +151 -0
- package/skills/codex/AGENTS.md +151 -0
- package/skills/gemini/GEMINI.md +151 -0
- package/skills/opencode/AGENTS.md +152 -0
- package/src/cli.ts +85 -0
- package/src/harness/base-adapter.ts +47 -0
- package/src/harness/claude-code.ts +106 -0
- package/src/harness/codex.ts +80 -0
- package/src/harness/command.ts +173 -0
- package/src/harness/gemini.ts +74 -0
- package/src/harness/opencode.ts +84 -0
- package/src/harness/registry.ts +29 -0
- package/src/harness/runner.ts +19 -0
- package/src/harness/types.ts +73 -0
- package/src/output-mode.ts +32 -0
- package/src/skill/artifacts.ts +174 -0
- package/src/skill/cli.ts +29 -0
- package/src/skill/install.ts +317 -0
- package/src/spec/agent-guidance.ts +466 -0
- package/src/spec/cli.ts +151 -0
- package/src/spec/commands/check.ts +1 -0
- package/src/spec/commands/config.ts +146 -0
- package/src/spec/commands/diff.ts +1 -0
- package/src/spec/commands/generate.ts +1 -0
- package/src/spec/commands/guide.ts +1 -0
- package/src/spec/commands/harness-list.ts +36 -0
- package/src/spec/commands/implement.ts +1 -0
- package/src/spec/commands/import.ts +1 -0
- package/src/spec/commands/init.ts +1 -0
- package/src/spec/commands/status.ts +87 -0
- package/src/spec/config.ts +63 -0
- package/src/spec/diff.ts +791 -0
- package/src/spec/extensions/benchmark.ts +347 -0
- package/src/spec/extensions/registry.ts +59 -0
- package/src/spec/extensions/types.ts +56 -0
- package/src/spec/grammar/index.ts +14 -0
- package/src/spec/grammar/parser.ts +443 -0
- package/src/spec/grammar/types.ts +70 -0
- package/src/spec/grammar/validator.ts +104 -0
- package/src/spec/loader.ts +174 -0
- package/src/spec/local-config.ts +59 -0
- package/src/spec/parser.ts +226 -0
- package/src/spec/path-utils.ts +73 -0
- package/src/spec/planner.ts +299 -0
- package/src/spec/prompt-renderer.ts +318 -0
- package/src/spec/skill-content.ts +119 -0
- package/src/spec/types.ts +239 -0
- package/src/spec/validator.ts +443 -0
- package/src/spec/workflows/check.ts +1534 -0
- package/src/spec/workflows/diff.ts +209 -0
- package/src/spec/workflows/generate.ts +1270 -0
- package/src/spec/workflows/guide.ts +190 -0
- package/src/spec/workflows/implement.ts +797 -0
- package/src/spec/workflows/import.ts +986 -0
- package/src/spec/workflows/init.ts +548 -0
- package/src/spec/workflows/status.ts +22 -0
- package/src/spec/workspace.ts +541 -0
- package/uninstall.ps1 +21 -0
- package/uninstall.sh +22 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join, resolve, relative } from "path";
|
|
4
|
+
|
|
5
|
+
import { resolveXdgConfigHome, resolveXdgDataHome, resolveXdgStateHome } from "../skill/artifacts";
|
|
6
|
+
|
|
7
|
+
export type LisaStorageMode = "repo" | "external";
|
|
8
|
+
export type SpecWriteScope = "repo" | "worktree";
|
|
9
|
+
|
|
10
|
+
export interface ResolvedWorkspaceLayout {
|
|
11
|
+
workspacePath: string;
|
|
12
|
+
storageMode: LisaStorageMode;
|
|
13
|
+
repoSpecRoot: string;
|
|
14
|
+
worktreeSpecRoot?: string;
|
|
15
|
+
runtimeRoot: string;
|
|
16
|
+
snapshotRoot: string;
|
|
17
|
+
configPath: string;
|
|
18
|
+
repoConfigPath: string;
|
|
19
|
+
worktreeConfigPath?: string;
|
|
20
|
+
localConfigRoot: string;
|
|
21
|
+
metadataPath?: string;
|
|
22
|
+
globalPackRoots: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SpecWorkspaceSnapshot {
|
|
26
|
+
workspacePath: string;
|
|
27
|
+
storageMode: LisaStorageMode;
|
|
28
|
+
rootPath: string;
|
|
29
|
+
exists: boolean;
|
|
30
|
+
configPath: string;
|
|
31
|
+
hasConfig: boolean;
|
|
32
|
+
backendSpecCount: number;
|
|
33
|
+
frontendSpecCount: number;
|
|
34
|
+
benchmarkSpecCount: number;
|
|
35
|
+
environmentCount: number;
|
|
36
|
+
repoSpecRoot: string;
|
|
37
|
+
worktreeSpecRoot?: string;
|
|
38
|
+
runtimeRoot: string;
|
|
39
|
+
snapshotRoot: string;
|
|
40
|
+
metadataPath?: string;
|
|
41
|
+
globalPackRoots: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ExternalWorkspaceMetadata {
|
|
45
|
+
mode: "external";
|
|
46
|
+
workspacePath: string;
|
|
47
|
+
repoIdentityPath: string;
|
|
48
|
+
worktreeIdentityPath: string;
|
|
49
|
+
globalPackRoots?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BOOTSTRAP_MARKER_FILES = [
|
|
53
|
+
"package.json",
|
|
54
|
+
"Cargo.toml",
|
|
55
|
+
"go.mod",
|
|
56
|
+
"pyproject.toml",
|
|
57
|
+
"Makefile",
|
|
58
|
+
"bunfig.toml",
|
|
59
|
+
"tsconfig.json",
|
|
60
|
+
] as const;
|
|
61
|
+
|
|
62
|
+
const BOOTSTRAP_DESCRIPTOR_FILES = [
|
|
63
|
+
"AGENTS.md",
|
|
64
|
+
"README.md",
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
const BOOTSTRAP_MARKER_DIRECTORIES = [
|
|
68
|
+
"src",
|
|
69
|
+
"tests",
|
|
70
|
+
"docs",
|
|
71
|
+
"scripts",
|
|
72
|
+
"config",
|
|
73
|
+
"docker",
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
function countFiles(path: string, predicate: (entry: string) => boolean): number {
|
|
77
|
+
if (!existsSync(path)) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return readdirSync(path).filter(predicate).length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function decodeOutput(bytes: Uint8Array<ArrayBufferLike>): string {
|
|
85
|
+
return new TextDecoder().decode(bytes);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hashPath(path: string): string {
|
|
89
|
+
return createHash("sha1").update(path).digest("hex");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPathInsideRoot(rootPath: string, targetPath: string): boolean {
|
|
93
|
+
const relativePath = relative(rootPath, targetPath);
|
|
94
|
+
return relativePath !== ".."
|
|
95
|
+
&& !relativePath.startsWith("../")
|
|
96
|
+
&& !relativePath.startsWith("..\\")
|
|
97
|
+
&& !/^[A-Za-z]:[\\/]/.test(relativePath)
|
|
98
|
+
&& !relativePath.startsWith("\\\\");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function safeRealpath(path: string): string {
|
|
102
|
+
return existsSync(path) ? realpathSync(path) : resolve(path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function runGitCommand(cwd: string, args: string[]): { exitCode: number; stdout: string; stderr: string } {
|
|
106
|
+
const proc = Bun.spawnSync({
|
|
107
|
+
cmd: ["git", ...args],
|
|
108
|
+
cwd,
|
|
109
|
+
stdout: "pipe",
|
|
110
|
+
stderr: "pipe",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
exitCode: proc.exitCode,
|
|
115
|
+
stdout: decodeOutput(proc.stdout).trim(),
|
|
116
|
+
stderr: decodeOutput(proc.stderr).trim(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveGitCommonDir(workspacePath: string): string | undefined {
|
|
121
|
+
const result = runGitCommand(workspacePath, ["rev-parse", "--git-common-dir"]);
|
|
122
|
+
if (result.exitCode !== 0 || result.stdout.length === 0) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return resolve(workspacePath, result.stdout);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveRepoIdentityPath(workspacePath: string): string {
|
|
130
|
+
return safeRealpath(resolveGitCommonDir(workspacePath) ?? workspacePath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveWorktreeIdentityPath(workspacePath: string): string {
|
|
134
|
+
return safeRealpath(workspacePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveExternalMetadataPath(workspacePath: string): string {
|
|
138
|
+
const repoId = hashPath(resolveRepoIdentityPath(workspacePath));
|
|
139
|
+
const worktreeId = hashPath(resolveWorktreeIdentityPath(workspacePath));
|
|
140
|
+
return join(resolveXdgConfigHome(), "lisa", "workspaces", repoId, `${worktreeId}.json`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeGlobalPackRoot(root: string): string {
|
|
144
|
+
const resolvedRoot = resolve(root);
|
|
145
|
+
const specRoot = resolvePackSpecRoot(resolvedRoot);
|
|
146
|
+
|
|
147
|
+
for (const candidate of [resolvedRoot, specRoot]) {
|
|
148
|
+
if (!existsSync(candidate)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
if (!statSync(candidate).isDirectory()) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
readdirSync(candidate);
|
|
157
|
+
return resolvedRoot;
|
|
158
|
+
} catch {
|
|
159
|
+
throw new Error(`Configured global pack root is not readable: ${root}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new Error(`Configured global pack root does not exist: ${root}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readExternalWorkspaceMetadata(workspacePath: string): ExternalWorkspaceMetadata | undefined {
|
|
167
|
+
const metadataPath = resolveExternalMetadataPath(workspacePath);
|
|
168
|
+
if (!existsSync(metadataPath)) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let raw: Partial<ExternalWorkspaceMetadata>;
|
|
173
|
+
try {
|
|
174
|
+
raw = JSON.parse(readFileSync(metadataPath, "utf8")) as Partial<ExternalWorkspaceMetadata>;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
throw new Error(`Malformed Lisa external workspace metadata at ${metadataPath}: ${message}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (raw.mode !== "external") {
|
|
181
|
+
throw new Error(`Unsupported Lisa workspace metadata mode in ${metadataPath}.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const repoIdentityPath = resolveRepoIdentityPath(workspacePath);
|
|
185
|
+
if (raw.repoIdentityPath && safeRealpath(raw.repoIdentityPath) !== repoIdentityPath) {
|
|
186
|
+
throw new Error(`Lisa external workspace metadata at ${metadataPath} does not match this repository.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const worktreeIdentityPath = resolveWorktreeIdentityPath(workspacePath);
|
|
190
|
+
if (raw.worktreeIdentityPath && safeRealpath(raw.worktreeIdentityPath) !== worktreeIdentityPath) {
|
|
191
|
+
throw new Error(`Lisa external workspace metadata at ${metadataPath} does not match this worktree.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const globalPackRoots = Array.isArray(raw.globalPackRoots)
|
|
195
|
+
? raw.globalPackRoots
|
|
196
|
+
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
|
197
|
+
.map((entry) => normalizeGlobalPackRoot(entry.trim()))
|
|
198
|
+
: [];
|
|
199
|
+
return {
|
|
200
|
+
mode: "external",
|
|
201
|
+
workspacePath,
|
|
202
|
+
repoIdentityPath,
|
|
203
|
+
worktreeIdentityPath,
|
|
204
|
+
globalPackRoots,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveExternalRoots(workspacePath: string, globalPackRoots: string[] = []): ResolvedWorkspaceLayout {
|
|
209
|
+
const repoIdentityPath = resolveRepoIdentityPath(workspacePath);
|
|
210
|
+
const worktreeIdentityPath = resolveWorktreeIdentityPath(workspacePath);
|
|
211
|
+
const repoId = hashPath(repoIdentityPath);
|
|
212
|
+
const worktreeId = hashPath(worktreeIdentityPath);
|
|
213
|
+
const repoRoot = join(resolveXdgDataHome(), "lisa", "repos", repoId, "repo");
|
|
214
|
+
const worktreeRoot = join(resolveXdgStateHome(), "lisa", "worktrees", repoId, worktreeId, "specs");
|
|
215
|
+
const runtimeRoot = join(resolveXdgStateHome(), "lisa", "worktrees", repoId, worktreeId, "runtime");
|
|
216
|
+
const snapshotRoot = join(resolveXdgStateHome(), "lisa", "worktrees", repoId, worktreeId, "snapshots");
|
|
217
|
+
const repoSpecRoot = join(repoRoot, ".specs");
|
|
218
|
+
const worktreeSpecRoot = join(worktreeRoot, ".specs");
|
|
219
|
+
const repoConfigPath = join(repoSpecRoot, "config.yaml");
|
|
220
|
+
const worktreeConfigPath = join(worktreeSpecRoot, "config.yaml");
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
workspacePath,
|
|
224
|
+
storageMode: "external",
|
|
225
|
+
repoSpecRoot,
|
|
226
|
+
worktreeSpecRoot,
|
|
227
|
+
runtimeRoot,
|
|
228
|
+
snapshotRoot,
|
|
229
|
+
configPath: existsSync(worktreeConfigPath) ? worktreeConfigPath : repoConfigPath,
|
|
230
|
+
repoConfigPath,
|
|
231
|
+
worktreeConfigPath,
|
|
232
|
+
localConfigRoot: runtimeRoot,
|
|
233
|
+
metadataPath: resolveExternalMetadataPath(workspacePath),
|
|
234
|
+
globalPackRoots,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
239
|
+
let current = resolve(cwd);
|
|
240
|
+
|
|
241
|
+
while (true) {
|
|
242
|
+
if (existsSync(join(current, ".git")) || existsSync(join(current, ".specs")) || existsSync(join(current, ".lisa"))) {
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const parent = dirname(current);
|
|
247
|
+
if (parent === current) {
|
|
248
|
+
return cwd;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
current = parent;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function resolveBootstrapWorkspaceRoot(cwd = process.cwd()): string {
|
|
256
|
+
const target = resolve(cwd);
|
|
257
|
+
const structureMarkerCount = BOOTSTRAP_MARKER_DIRECTORIES.filter((marker) => existsSync(join(target, marker))).length;
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
existsSync(join(target, ".git"))
|
|
261
|
+
|| existsSync(join(target, ".specs"))
|
|
262
|
+
|| existsSync(join(target, ".lisa"))
|
|
263
|
+
|| BOOTSTRAP_MARKER_FILES.some((marker) => existsSync(join(target, marker)))
|
|
264
|
+
|| structureMarkerCount >= 2
|
|
265
|
+
|| (structureMarkerCount >= 1 && BOOTSTRAP_DESCRIPTOR_FILES.some((marker) => existsSync(join(target, marker))))
|
|
266
|
+
) {
|
|
267
|
+
return target;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return resolveWorkspaceRoot(target);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function resolveRepoWorkspaceLayout(workspacePath: string): ResolvedWorkspaceLayout {
|
|
274
|
+
const repoSpecRoot = join(workspacePath, ".specs");
|
|
275
|
+
return {
|
|
276
|
+
workspacePath,
|
|
277
|
+
storageMode: "repo",
|
|
278
|
+
repoSpecRoot,
|
|
279
|
+
runtimeRoot: join(workspacePath, ".lisa"),
|
|
280
|
+
snapshotRoot: join(workspacePath, ".lisa", "snapshots"),
|
|
281
|
+
configPath: join(repoSpecRoot, "config.yaml"),
|
|
282
|
+
repoConfigPath: join(repoSpecRoot, "config.yaml"),
|
|
283
|
+
localConfigRoot: repoSpecRoot,
|
|
284
|
+
globalPackRoots: [],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function resolveWorkspaceLayoutFromRoot(workspacePath: string): ResolvedWorkspaceLayout {
|
|
289
|
+
const resolvedWorkspacePath = resolve(workspacePath);
|
|
290
|
+
const externalMetadata = readExternalWorkspaceMetadata(resolvedWorkspacePath);
|
|
291
|
+
if (externalMetadata) {
|
|
292
|
+
return resolveExternalRoots(resolvedWorkspacePath, externalMetadata.globalPackRoots ?? []);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return resolveRepoWorkspaceLayout(resolvedWorkspacePath);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function resolveWorkspaceLayout(cwd = process.cwd()): ResolvedWorkspaceLayout {
|
|
299
|
+
return resolveWorkspaceLayoutFromRoot(resolveWorkspaceRoot(cwd));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function assertSafeLisaStorageWritePath(rootPath: string, targetPath: string, label = "Lisa storage"): void {
|
|
303
|
+
const resolvedRoot = resolve(rootPath);
|
|
304
|
+
const resolvedTarget = resolve(targetPath);
|
|
305
|
+
if (!isPathInsideRoot(resolvedRoot, resolvedTarget)) {
|
|
306
|
+
throw new Error(`${label} writes must stay inside ${resolvedRoot}.`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let existingRootBoundary = resolvedRoot;
|
|
310
|
+
while (!existsSync(existingRootBoundary)) {
|
|
311
|
+
const parentPath = dirname(existingRootBoundary);
|
|
312
|
+
if (parentPath === existingRootBoundary) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
existingRootBoundary = parentPath;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const rootRealPath = existsSync(existingRootBoundary) ? realpathSync(existingRootBoundary) : undefined;
|
|
319
|
+
let existingPath = resolvedTarget;
|
|
320
|
+
while (!existsSync(existingPath)) {
|
|
321
|
+
const parentPath = dirname(existingPath);
|
|
322
|
+
if (parentPath === existingPath) {
|
|
323
|
+
throw new Error(`Could not resolve a safe parent directory for ${targetPath}`);
|
|
324
|
+
}
|
|
325
|
+
existingPath = parentPath;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let cursor = existingPath;
|
|
329
|
+
while (true) {
|
|
330
|
+
if (lstatSync(cursor).isSymbolicLink()) {
|
|
331
|
+
throw new Error(`Refusing to write through symlinked Lisa storage path: ${cursor}`);
|
|
332
|
+
}
|
|
333
|
+
if (cursor === existingRootBoundary) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
const parentPath = dirname(cursor);
|
|
337
|
+
if (parentPath === cursor) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
cursor = parentPath;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!rootRealPath) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const existingRealPath = realpathSync(existingPath);
|
|
348
|
+
if (!isPathInsideRoot(rootRealPath, existingRealPath)) {
|
|
349
|
+
throw new Error(`${label} writes must stay inside ${resolvedRoot}.`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (existsSync(resolvedTarget)) {
|
|
353
|
+
const targetRealPath = realpathSync(resolvedTarget);
|
|
354
|
+
if (!isPathInsideRoot(rootRealPath, targetRealPath)) {
|
|
355
|
+
throw new Error(`${label} writes must stay inside ${resolvedRoot}.`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function listLayerFiles(path: string, predicate: (entry: string) => boolean): string[] {
|
|
361
|
+
if (!existsSync(path)) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return readdirSync(path).filter(predicate).sort().map((entry) => join(path, entry));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolvePackSpecRoot(root: string): string {
|
|
369
|
+
return root.endsWith("/.specs") || root.endsWith("\\.specs") ? root : join(root, ".specs");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function listEffectiveWorkspaceFiles(layout: ResolvedWorkspaceLayout): {
|
|
373
|
+
configPath?: string;
|
|
374
|
+
documentPaths: string[];
|
|
375
|
+
environmentPaths: string[];
|
|
376
|
+
} {
|
|
377
|
+
if (layout.storageMode === "repo") {
|
|
378
|
+
return {
|
|
379
|
+
configPath: existsSync(layout.configPath) ? layout.configPath : undefined,
|
|
380
|
+
documentPaths: [
|
|
381
|
+
...listLayerFiles(join(layout.repoSpecRoot, "backend"), (entry) => entry.endsWith(".md")),
|
|
382
|
+
...listLayerFiles(join(layout.repoSpecRoot, "frontend"), (entry) => entry.endsWith(".md")),
|
|
383
|
+
],
|
|
384
|
+
environmentPaths: listLayerFiles(join(layout.repoSpecRoot, "environments"), (entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const layerRoots = [
|
|
389
|
+
...layout.globalPackRoots.map((root) => resolvePackSpecRoot(root)),
|
|
390
|
+
layout.repoSpecRoot,
|
|
391
|
+
layout.worktreeSpecRoot,
|
|
392
|
+
].filter((root): root is string => Boolean(root));
|
|
393
|
+
const selected = new Map<string, string>();
|
|
394
|
+
|
|
395
|
+
for (const layerRoot of layerRoots) {
|
|
396
|
+
const configPath = join(layerRoot, "config.yaml");
|
|
397
|
+
if (existsSync(configPath)) {
|
|
398
|
+
selected.set("config.yaml", configPath);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
for (const [subdir, matcher] of [
|
|
402
|
+
["backend", (entry: string) => entry.endsWith(".md")],
|
|
403
|
+
["frontend", (entry: string) => entry.endsWith(".md")],
|
|
404
|
+
["environments", (entry: string) => entry.endsWith(".yaml") || entry.endsWith(".yml")],
|
|
405
|
+
] as const) {
|
|
406
|
+
for (const path of listLayerFiles(join(layerRoot, subdir), matcher)) {
|
|
407
|
+
selected.set(`${subdir}/${relative(join(layerRoot, subdir), path).split("\\").join("/")}`, path);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
configPath: selected.get("config.yaml"),
|
|
414
|
+
documentPaths: Array.from(selected.entries())
|
|
415
|
+
.filter(([key]) => key.startsWith("backend/") || key.startsWith("frontend/"))
|
|
416
|
+
.map(([, path]) => path)
|
|
417
|
+
.sort(),
|
|
418
|
+
environmentPaths: Array.from(selected.entries())
|
|
419
|
+
.filter(([key]) => key.startsWith("environments/"))
|
|
420
|
+
.map(([, path]) => path)
|
|
421
|
+
.sort(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function resolveEffectiveProjectSpecPath(layout: ResolvedWorkspaceLayout): string | undefined {
|
|
426
|
+
if (layout.storageMode === "repo") {
|
|
427
|
+
const projectPath = join(layout.repoSpecRoot, "project.md");
|
|
428
|
+
return existsSync(projectPath) ? projectPath : undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const layerRoots = [
|
|
432
|
+
...layout.globalPackRoots.map((root) => resolvePackSpecRoot(root)),
|
|
433
|
+
layout.repoSpecRoot,
|
|
434
|
+
layout.worktreeSpecRoot,
|
|
435
|
+
].filter((root): root is string => Boolean(root));
|
|
436
|
+
|
|
437
|
+
let selected: string | undefined;
|
|
438
|
+
for (const layerRoot of layerRoots) {
|
|
439
|
+
const projectPath = join(layerRoot, "project.md");
|
|
440
|
+
if (existsSync(projectPath)) {
|
|
441
|
+
selected = projectPath;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return selected;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function toLogicalPath(layout: ResolvedWorkspaceLayout, targetPath: string): string {
|
|
449
|
+
const normalizedTarget = resolve(targetPath);
|
|
450
|
+
|
|
451
|
+
for (const globalPackRoot of layout.globalPackRoots) {
|
|
452
|
+
const specRoot = resolve(resolvePackSpecRoot(globalPackRoot));
|
|
453
|
+
const prefix = `${specRoot}${normalizedTarget === specRoot ? "" : "/"}`;
|
|
454
|
+
if (normalizedTarget === specRoot || normalizedTarget.startsWith(prefix)) {
|
|
455
|
+
return join(".specs", relative(specRoot, normalizedTarget)).split("\\").join("/");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const repoPrefix = `${resolve(layout.repoSpecRoot)}${normalizedTarget === resolve(layout.repoSpecRoot) ? "" : "/"}`;
|
|
460
|
+
if (normalizedTarget === resolve(layout.repoSpecRoot) || normalizedTarget.startsWith(repoPrefix)) {
|
|
461
|
+
return join(".specs", relative(layout.repoSpecRoot, normalizedTarget)).split("\\").join("/");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (layout.worktreeSpecRoot) {
|
|
465
|
+
const worktreeRoot = resolve(layout.worktreeSpecRoot);
|
|
466
|
+
const worktreePrefix = `${worktreeRoot}${normalizedTarget === worktreeRoot ? "" : "/"}`;
|
|
467
|
+
if (normalizedTarget === worktreeRoot || normalizedTarget.startsWith(worktreePrefix)) {
|
|
468
|
+
return join(".specs", relative(layout.worktreeSpecRoot, normalizedTarget)).split("\\").join("/");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const runtimeRoot = resolve(layout.runtimeRoot);
|
|
473
|
+
const runtimePrefix = `${runtimeRoot}${normalizedTarget === runtimeRoot ? "" : "/"}`;
|
|
474
|
+
if (normalizedTarget === runtimeRoot || normalizedTarget.startsWith(runtimePrefix)) {
|
|
475
|
+
return join(".lisa", relative(layout.runtimeRoot, normalizedTarget)).split("\\").join("/");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return relative(layout.workspacePath, normalizedTarget).split("\\").join("/") || normalizedTarget;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function resolveSpecWriteRoot(cwd = process.cwd(), scope: SpecWriteScope = "repo"): string {
|
|
482
|
+
const layout = resolveWorkspaceLayout(cwd);
|
|
483
|
+
if (layout.storageMode !== "external" || scope === "repo") {
|
|
484
|
+
return layout.repoSpecRoot;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return layout.worktreeSpecRoot ?? layout.repoSpecRoot;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function initializeExternalWorkspace(cwd = process.cwd(), globalPackRoots: string[] = []): ResolvedWorkspaceLayout {
|
|
491
|
+
const workspacePath = resolveBootstrapWorkspaceRoot(cwd);
|
|
492
|
+
const normalizedGlobalPackRoots = globalPackRoots.map((root) => normalizeGlobalPackRoot(root));
|
|
493
|
+
const layout = resolveExternalRoots(workspacePath, normalizedGlobalPackRoots);
|
|
494
|
+
const metadata: ExternalWorkspaceMetadata = {
|
|
495
|
+
mode: "external",
|
|
496
|
+
workspacePath,
|
|
497
|
+
repoIdentityPath: resolveRepoIdentityPath(workspacePath),
|
|
498
|
+
worktreeIdentityPath: resolveWorktreeIdentityPath(workspacePath),
|
|
499
|
+
globalPackRoots: normalizedGlobalPackRoots,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
mkdirSync(dirname(layout.metadataPath as string), { recursive: true });
|
|
503
|
+
mkdirSync(layout.repoSpecRoot, { recursive: true });
|
|
504
|
+
mkdirSync(layout.worktreeSpecRoot as string, { recursive: true });
|
|
505
|
+
mkdirSync(layout.runtimeRoot, { recursive: true });
|
|
506
|
+
mkdirSync(layout.snapshotRoot, { recursive: true });
|
|
507
|
+
writeFileSync(layout.metadataPath as string, `${JSON.stringify(metadata, null, 2)}\n`);
|
|
508
|
+
return layout;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function inspectSpecWorkspace(cwd = process.cwd()): SpecWorkspaceSnapshot {
|
|
512
|
+
const layout = resolveWorkspaceLayout(cwd);
|
|
513
|
+
const effective = listEffectiveWorkspaceFiles(layout);
|
|
514
|
+
const isBaseSpec = (entry: string) => entry.endsWith(".md") && !entry.includes(".bench.");
|
|
515
|
+
const isBenchmarkSpec = (entry: string) => entry.endsWith(".md") && entry.includes(".bench.");
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
workspacePath: layout.workspacePath,
|
|
519
|
+
storageMode: layout.storageMode,
|
|
520
|
+
rootPath: layout.repoSpecRoot,
|
|
521
|
+
exists: effective.documentPaths.length > 0 || effective.environmentPaths.length > 0 || existsSync(layout.repoSpecRoot),
|
|
522
|
+
configPath: layout.configPath,
|
|
523
|
+
hasConfig: Boolean(effective.configPath),
|
|
524
|
+
backendSpecCount: effective.documentPaths
|
|
525
|
+
.filter((path) => toLogicalPath(layout, path).startsWith(".specs/backend/"))
|
|
526
|
+
.filter(isBaseSpec)
|
|
527
|
+
.length,
|
|
528
|
+
frontendSpecCount: effective.documentPaths
|
|
529
|
+
.filter((path) => toLogicalPath(layout, path).startsWith(".specs/frontend/"))
|
|
530
|
+
.filter(isBaseSpec)
|
|
531
|
+
.length,
|
|
532
|
+
benchmarkSpecCount: effective.documentPaths.filter((path) => isBenchmarkSpec(toLogicalPath(layout, path))).length,
|
|
533
|
+
environmentCount: effective.environmentPaths.length,
|
|
534
|
+
repoSpecRoot: layout.repoSpecRoot,
|
|
535
|
+
worktreeSpecRoot: layout.worktreeSpecRoot,
|
|
536
|
+
runtimeRoot: layout.runtimeRoot,
|
|
537
|
+
snapshotRoot: layout.snapshotRoot,
|
|
538
|
+
metadataPath: layout.metadataPath,
|
|
539
|
+
globalPackRoots: layout.globalPackRoots,
|
|
540
|
+
};
|
|
541
|
+
}
|
package/uninstall.ps1
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Uninstall script for Lisa CLI (Windows)
|
|
2
|
+
|
|
3
|
+
$ErrorActionPreference = "Stop"
|
|
4
|
+
|
|
5
|
+
Write-Host "Uninstalling Lisa CLI..."
|
|
6
|
+
|
|
7
|
+
if (Get-Command bun -ErrorAction SilentlyContinue) {
|
|
8
|
+
Write-Host "Unlinking lisa command (bun)..."
|
|
9
|
+
bun unlink @wkronmiller/lisa 2>$null
|
|
10
|
+
bun unlink @th0rgal/lisa 2>$null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (Get-Command npm -ErrorAction SilentlyContinue) {
|
|
14
|
+
Write-Host "Removing global package (npm)..."
|
|
15
|
+
npm uninstall -g @wkronmiller/lisa 2>$null
|
|
16
|
+
npm uninstall -g @th0rgal/lisa 2>$null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Write-Host ""
|
|
20
|
+
Write-Host "Uninstall complete!"
|
|
21
|
+
Write-Host "You may also want to remove the cloned repository."
|
package/uninstall.sh
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Uninstall script for Lisa CLI
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "Uninstalling Lisa CLI..."
|
|
7
|
+
|
|
8
|
+
if command -v bun &> /dev/null; then
|
|
9
|
+
echo "Unlinking lisa command (bun)..."
|
|
10
|
+
bun unlink @wkronmiller/lisa 2>/dev/null || true
|
|
11
|
+
bun unlink @th0rgal/lisa 2>/dev/null || true
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
if command -v npm &> /dev/null; then
|
|
15
|
+
echo "Removing global package (npm)..."
|
|
16
|
+
npm uninstall -g @wkronmiller/lisa 2>/dev/null || true
|
|
17
|
+
npm uninstall -g @th0rgal/lisa 2>/dev/null || true
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
echo ""
|
|
21
|
+
echo "Uninstall complete!"
|
|
22
|
+
echo "You may also want to remove the cloned repository."
|