@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,317 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from "fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
import { getHarnessAdapters } from "../harness/registry";
|
|
5
|
+
import type { HarnessAdapter, HarnessSkillInstallDefinition, SkillInstallScope } from "../harness/types";
|
|
6
|
+
import { resolveBootstrapWorkspaceRoot, resolveWorkspaceRoot } from "../spec/workspace";
|
|
7
|
+
|
|
8
|
+
export type SkillInstallState = "created" | "updated" | "unchanged" | "skipped" | "failed";
|
|
9
|
+
|
|
10
|
+
export interface SkillInstallResult {
|
|
11
|
+
adapterId: string;
|
|
12
|
+
scope: SkillInstallScope;
|
|
13
|
+
sourcePath: string;
|
|
14
|
+
destinationPath: string;
|
|
15
|
+
state: SkillInstallState;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface InstallCommandOptions {
|
|
20
|
+
help: boolean;
|
|
21
|
+
scope?: SkillInstallScope;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRepoInstructionFile(path: string): boolean {
|
|
25
|
+
return ["AGENTS.md", "CLAUDE.md", "GEMINI.md"].includes(basename(path));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function containsLisaSubstring(content: string): boolean {
|
|
29
|
+
return content.toLowerCase().includes("lisa");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasManagedGuidanceBlock(content: string): boolean {
|
|
33
|
+
return content.includes("<!-- LISA_AGENT_GUIDANCE_START -->")
|
|
34
|
+
&& content.includes("<!-- LISA_AGENT_GUIDANCE_END -->");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isRepositoryWorkspace(path: string): boolean {
|
|
38
|
+
let current = resolve(path);
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
if (existsSync(join(current, ".git")) || existsSync(join(current, ".specs")) || existsSync(join(current, ".lisa"))) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parent = dirname(current);
|
|
46
|
+
if (parent === current) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
current = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printInstallHelp(): void {
|
|
55
|
+
console.log(`Lisa skill install
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
lisa skill install --local
|
|
59
|
+
lisa skill install --global
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--local Install Lisa guidance into the current repository
|
|
63
|
+
--global Install Lisa guidance into global harness config roots
|
|
64
|
+
--help, -h Show this help
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseInstallArgs(args: string[]): InstallCommandOptions {
|
|
69
|
+
const options: InstallCommandOptions = { help: false };
|
|
70
|
+
|
|
71
|
+
for (const arg of args) {
|
|
72
|
+
if (!arg) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (arg === "--help" || arg === "-h") {
|
|
77
|
+
options.help = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (arg === "--local" || arg === "--global") {
|
|
82
|
+
if (options.scope && options.scope !== arg.slice(2)) {
|
|
83
|
+
throw new Error("Choose exactly one install scope: `--local` or `--global`.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
options.scope = arg.slice(2) as SkillInstallScope;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error(`Unknown lisa skill install option: ${arg}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizePortablePath(path: string, basePath: string): string {
|
|
97
|
+
return relative(basePath, path).split("\\").join("/") || ".";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveExistingPath(path: string): string {
|
|
101
|
+
let current = resolve(path);
|
|
102
|
+
|
|
103
|
+
while (!existsSync(current)) {
|
|
104
|
+
const parent = dirname(current);
|
|
105
|
+
if (parent === current) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
current = parent;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return current;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isPathInsideWorkspace(rootPath: string, targetPath: string): boolean {
|
|
116
|
+
const workspaceRealPath = realpathSync(rootPath);
|
|
117
|
+
const targetRealPath = realpathSync(resolveExistingPath(targetPath));
|
|
118
|
+
return targetRealPath === workspaceRealPath || targetRealPath.startsWith(`${workspaceRealPath}/`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveDefinition(adapter: HarnessAdapter, scope: SkillInstallScope): Promise<HarnessSkillInstallDefinition> {
|
|
122
|
+
if (!adapter.getSkillInstallDefinition) {
|
|
123
|
+
throw new Error(`Harness \`${adapter.id}\` does not expose Lisa skill install metadata.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return adapter.getSkillInstallDefinition(scope);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readCanonicalContent(definition: HarnessSkillInstallDefinition): string {
|
|
130
|
+
return readFileSync(definition.sourcePath, "utf8");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveLocalRootInstructionAction(definition: HarnessSkillInstallDefinition): { skip: true; reason: string } | { skip: false } {
|
|
134
|
+
if (!isRepoInstructionFile(definition.destinationPath) || !existsSync(definition.destinationPath)) {
|
|
135
|
+
return { skip: false };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (lstatSync(definition.destinationPath).isSymbolicLink()) {
|
|
139
|
+
return { skip: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const existingContent = readFileSync(definition.destinationPath, "utf8");
|
|
143
|
+
if (hasManagedGuidanceBlock(existingContent)) {
|
|
144
|
+
return { skip: true, reason: "existing file preserved" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (existingContent === readCanonicalContent(definition)) {
|
|
148
|
+
return { skip: false };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (containsLisaSubstring(existingContent)) {
|
|
152
|
+
return { skip: true, reason: "existing file already contains lisa" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { skip: true, reason: "existing file preserved" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unresolvedResult(adapterId: string, scope: SkillInstallScope, reason: string, state: SkillInstallState): SkillInstallResult {
|
|
159
|
+
return {
|
|
160
|
+
adapterId,
|
|
161
|
+
scope,
|
|
162
|
+
sourcePath: "(unresolved)",
|
|
163
|
+
destinationPath: "(unresolved)",
|
|
164
|
+
state,
|
|
165
|
+
reason,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatInstallResult(result: SkillInstallResult, workspaceRoot: string): string {
|
|
170
|
+
const destination = result.scope === "local" && result.destinationPath !== "(unresolved)"
|
|
171
|
+
? normalizePortablePath(result.destinationPath, workspaceRoot)
|
|
172
|
+
: result.destinationPath;
|
|
173
|
+
const source = result.sourcePath === "(unresolved)"
|
|
174
|
+
? result.sourcePath
|
|
175
|
+
: normalizePortablePath(resolve(result.sourcePath), workspaceRoot);
|
|
176
|
+
|
|
177
|
+
if (result.state === "skipped" || result.state === "failed") {
|
|
178
|
+
return `- ${result.adapterId}: ${result.state} ${destination} (${result.reason ?? "unavailable"}; source ${source})`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return `- ${result.adapterId}: ${result.state} ${destination} (source ${source})`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function installLisaSkills(
|
|
185
|
+
scope: SkillInstallScope,
|
|
186
|
+
workspaceRoot: string,
|
|
187
|
+
adapters = getHarnessAdapters(workspaceRoot),
|
|
188
|
+
): Promise<SkillInstallResult[]> {
|
|
189
|
+
const results: SkillInstallResult[] = [];
|
|
190
|
+
|
|
191
|
+
for (const adapter of adapters) {
|
|
192
|
+
let definition: HarnessSkillInstallDefinition | undefined;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
definition = await resolveDefinition(adapter, scope);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
198
|
+
const availability = await adapter.detect();
|
|
199
|
+
results.push(unresolvedResult(adapter.id, scope, availability.available ? message : availability.reason, availability.available ? "failed" : "skipped"));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const availability = await adapter.detect();
|
|
204
|
+
if (!availability.available) {
|
|
205
|
+
results.push({
|
|
206
|
+
adapterId: adapter.id,
|
|
207
|
+
scope,
|
|
208
|
+
sourcePath: definition.sourcePath,
|
|
209
|
+
destinationPath: definition.destinationPath,
|
|
210
|
+
state: "skipped",
|
|
211
|
+
reason: availability.reason,
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (scope === "global" && isPathInsideWorkspace(workspaceRoot, definition.destinationPath)) {
|
|
217
|
+
results.push({
|
|
218
|
+
adapterId: adapter.id,
|
|
219
|
+
scope,
|
|
220
|
+
sourcePath: definition.sourcePath,
|
|
221
|
+
destinationPath: definition.destinationPath,
|
|
222
|
+
state: "failed",
|
|
223
|
+
reason: `Global install target must stay outside the repository: ${definition.destinationPath}`,
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const localRootAction = scope === "local" ? resolveLocalRootInstructionAction(definition) : { skip: false as const };
|
|
229
|
+
if (localRootAction.skip) {
|
|
230
|
+
results.push({
|
|
231
|
+
adapterId: adapter.id,
|
|
232
|
+
scope,
|
|
233
|
+
sourcePath: definition.sourcePath,
|
|
234
|
+
destinationPath: definition.destinationPath,
|
|
235
|
+
state: "skipped",
|
|
236
|
+
reason: localRootAction.reason,
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!adapter.installSkill) {
|
|
242
|
+
results.push(unresolvedResult(adapter.id, scope, `Harness \`${adapter.id}\` cannot install Lisa guidance.`, "failed"));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const installed = await adapter.installSkill(scope);
|
|
248
|
+
results.push({
|
|
249
|
+
adapterId: adapter.id,
|
|
250
|
+
scope,
|
|
251
|
+
sourcePath: installed.sourcePath,
|
|
252
|
+
destinationPath: installed.destinationPath,
|
|
253
|
+
state: installed.status,
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
+
results.push({
|
|
258
|
+
adapterId: adapter.id,
|
|
259
|
+
scope,
|
|
260
|
+
sourcePath: definition.sourcePath,
|
|
261
|
+
destinationPath: definition.destinationPath,
|
|
262
|
+
state: "failed",
|
|
263
|
+
reason: message,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function runSkillInstallCommand(args: string[], cwd = process.cwd()): Promise<number> {
|
|
272
|
+
try {
|
|
273
|
+
const options = parseInstallArgs(args);
|
|
274
|
+
if (options.help) {
|
|
275
|
+
printInstallHelp();
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!options.scope) {
|
|
280
|
+
throw new Error("Install scope required. Use `--local` or `--global`.");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const workspaceRoot = options.scope === "local"
|
|
284
|
+
? resolveBootstrapWorkspaceRoot(cwd)
|
|
285
|
+
: resolveWorkspaceRoot(cwd);
|
|
286
|
+
|
|
287
|
+
if (options.scope === "local" && !isRepositoryWorkspace(workspaceRoot)) {
|
|
288
|
+
throw new Error("`lisa skill install --local` must be run from inside a repository or Lisa workspace.");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const results = await installLisaSkills(options.scope, workspaceRoot);
|
|
292
|
+
const installedCount = results.filter((result) => ["created", "updated", "unchanged"].includes(result.state)).length;
|
|
293
|
+
const skippedCount = results.filter((result) => result.state === "skipped").length;
|
|
294
|
+
const failedCount = results.filter((result) => result.state === "failed").length;
|
|
295
|
+
|
|
296
|
+
console.log("Lisa skill install");
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log(`Scope: ${options.scope}`);
|
|
299
|
+
console.log(`Workspace: ${workspaceRoot}`);
|
|
300
|
+
console.log("");
|
|
301
|
+
console.log(`Installed: ${installedCount}`);
|
|
302
|
+
console.log(`Skipped: ${skippedCount}`);
|
|
303
|
+
if (failedCount > 0) {
|
|
304
|
+
console.log(`Failed: ${failedCount}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log("");
|
|
308
|
+
for (const result of results) {
|
|
309
|
+
console.log(formatInstallResult(result, workspaceRoot));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return failedCount > 0 ? 1 : 0;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
317
|
+
}
|