litopencode 0.0.0 → 0.0.2
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/README.md +17 -7
- package/dist/cli/args.d.ts +3 -0
- package/dist/cli/args.js +58 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +34 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +129 -0
- package/dist/cli/json.d.ts +4 -0
- package/dist/cli/json.js +30 -0
- package/dist/cli/types.d.ts +49 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +28 -167
- package/dist/features.js +3 -3
- package/dist/skills.js +3 -3
- package/docs/migration.md +2 -1
- package/docs/release-checklist.md +7 -5
- package/package.json +8 -2
- package/skills/doctor-installer/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -24,12 +24,28 @@
|
|
|
24
24
|
- **Two-agent OpenCode surface** -- Keep the visible agent switcher focused on <code>lit-plan</code> and <code>lit-loop</code>.
|
|
25
25
|
- **Durable goal ledger** -- Persist resumable progress under <code>.litopencode/litgoal/lit-loop/ledger.jsonl</code>.
|
|
26
26
|
- **OpenCode-native plugin hooks** -- Register config, tools, command activation, tool guards, and dispose lifecycle hooks.
|
|
27
|
-
- **
|
|
27
|
+
- **npx installer/doctor CLI** -- Install the plugin into OpenCode with a branded terminal flow, or preview the exact config patch with dry-run.
|
|
28
28
|
- **Role aliases and specialists** -- Keep deeper planning, implementation, verification, QA, review, research, and specialist roles available without crowding the default UI.
|
|
29
29
|
- **Release guardrails** -- Verify scanner, lockstep, packed payload, source gates, and dry-pack behavior before publication.
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
|
|
33
|
+
### Install Into OpenCode
|
|
34
|
+
|
|
35
|
+
npx litopencode install
|
|
36
|
+
|
|
37
|
+
The installer writes a version-pinned plugin entry such as <code>litopencode@0.0.2</code> into OpenCode config. Then restart OpenCode. The agent switcher should show <code>lit-plan</code> and <code>lit-loop</code>.
|
|
38
|
+
|
|
39
|
+
For a preview without writing <code>opencode.json</code>:
|
|
40
|
+
|
|
41
|
+
npx litopencode install --dry-run
|
|
42
|
+
|
|
43
|
+
For health checks:
|
|
44
|
+
|
|
45
|
+
npx litopencode doctor
|
|
46
|
+
|
|
47
|
+
By default, the installer targets <code>~/.config/opencode/opencode.json</code>, or <code>$XDG_CONFIG_HOME/opencode/opencode.json</code> when <code>XDG_CONFIG_HOME</code> is set. Use <code>--root <dir></code> for a custom OpenCode config directory.
|
|
48
|
+
|
|
33
49
|
### Local Source Probe
|
|
34
50
|
|
|
35
51
|
npm install
|
|
@@ -41,12 +57,6 @@
|
|
|
41
57
|
node bin/litopencode doctor --root .
|
|
42
58
|
node bin/litopencode install --dry-run --root .
|
|
43
59
|
|
|
44
|
-
### OpenCode Plugin Install Preview
|
|
45
|
-
|
|
46
|
-
node bin/litopencode install --dry-run --root ~/.config/opencode
|
|
47
|
-
|
|
48
|
-
<code>install --dry-run</code> prints the exact <code>opencode.json</code> plugin mutation without writing files. <code>doctor</code> reports package, config, and runtime path status without creating runtime state.
|
|
49
|
-
|
|
50
60
|
## Package Surface
|
|
51
61
|
|
|
52
62
|
| Surface | Value |
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function defaultOpenCodeRoot() {
|
|
4
|
+
const configHome = process.env.XDG_CONFIG_HOME;
|
|
5
|
+
if (configHome && configHome.length > 0)
|
|
6
|
+
return path.join(configHome, "opencode");
|
|
7
|
+
return path.join(os.homedir(), ".config", "opencode");
|
|
8
|
+
}
|
|
9
|
+
export function parseArgs(argv) {
|
|
10
|
+
let command = argv[0];
|
|
11
|
+
let root = defaultOpenCodeRoot();
|
|
12
|
+
let dryRun = false;
|
|
13
|
+
if (command === "--help" || command === "-h") {
|
|
14
|
+
command = "help";
|
|
15
|
+
}
|
|
16
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
17
|
+
const arg = argv[index];
|
|
18
|
+
if (arg === "--root" || arg === "--workdir") {
|
|
19
|
+
const value = argv[index + 1];
|
|
20
|
+
if (!value)
|
|
21
|
+
throw new Error(`${arg} requires a value`);
|
|
22
|
+
root = value;
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === "--dry-run") {
|
|
27
|
+
dryRun = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === "--help" || arg === "-h") {
|
|
31
|
+
command = "help";
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
35
|
+
}
|
|
36
|
+
return { command, root, dryRun };
|
|
37
|
+
}
|
|
38
|
+
export function helpText() {
|
|
39
|
+
return [
|
|
40
|
+
"litopencode",
|
|
41
|
+
"",
|
|
42
|
+
"Install LitOpenCode into OpenCode with a polished npx flow.",
|
|
43
|
+
"",
|
|
44
|
+
"Quick start:",
|
|
45
|
+
" npx litopencode install",
|
|
46
|
+
"",
|
|
47
|
+
"Usage:",
|
|
48
|
+
" litopencode install [--dry-run] [--root <dir>]",
|
|
49
|
+
" litopencode doctor [--root <dir>]",
|
|
50
|
+
"",
|
|
51
|
+
"Commands:",
|
|
52
|
+
" install Add litopencode to opencode.json with a branded installer UI.",
|
|
53
|
+
" doctor Report package, config, and runtime path status without writing files.",
|
|
54
|
+
"",
|
|
55
|
+
"Default root:",
|
|
56
|
+
" ~/.config/opencode, or $XDG_CONFIG_HOME/opencode when XDG_CONFIG_HOME is set."
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { readPackageMetadata } from "./json.js";
|
|
4
|
+
export async function doctor(root) {
|
|
5
|
+
const metadata = await readPackageMetadata();
|
|
6
|
+
const loaded = await loadConfig(root);
|
|
7
|
+
const runtimeExists = await fs
|
|
8
|
+
.stat(loaded.paths.runtimeDir)
|
|
9
|
+
.then(() => true)
|
|
10
|
+
.catch((error) => {
|
|
11
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
12
|
+
return false;
|
|
13
|
+
throw error;
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
exitCode: 0,
|
|
17
|
+
stdout: JSON.stringify({
|
|
18
|
+
package: metadata,
|
|
19
|
+
config: {
|
|
20
|
+
source: loaded.source,
|
|
21
|
+
path: loaded.paths.configFile,
|
|
22
|
+
enabled: loaded.config.enabled,
|
|
23
|
+
logLevel: loaded.config.logLevel
|
|
24
|
+
},
|
|
25
|
+
state: {
|
|
26
|
+
runtimeDir: loaded.paths.runtimeDir,
|
|
27
|
+
runtimeExists,
|
|
28
|
+
stateFile: loaded.paths.stateFile,
|
|
29
|
+
logFile: loaded.paths.logFile,
|
|
30
|
+
ledgerFile: loaded.paths.ledgerFile
|
|
31
|
+
}
|
|
32
|
+
}, null, 2)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRuntimePaths } from "../state.js";
|
|
4
|
+
import { readJsonObjectIfPresent, readPackageMetadata } from "./json.js";
|
|
5
|
+
const pluginName = "litopencode";
|
|
6
|
+
const reset = "\u001b[0m";
|
|
7
|
+
function useColor() {
|
|
8
|
+
return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
9
|
+
}
|
|
10
|
+
function tint(value, code) {
|
|
11
|
+
if (!useColor())
|
|
12
|
+
return value;
|
|
13
|
+
return code + value + reset;
|
|
14
|
+
}
|
|
15
|
+
function padLabel(label) {
|
|
16
|
+
return label.padEnd(24, " ");
|
|
17
|
+
}
|
|
18
|
+
function headerLine(value, code) {
|
|
19
|
+
return "| " + tint(value.padEnd(58, " "), code) + " |";
|
|
20
|
+
}
|
|
21
|
+
function pluginSpec(metadata) {
|
|
22
|
+
return metadata.name + "@" + metadata.version;
|
|
23
|
+
}
|
|
24
|
+
function isLitOpenCodeEntry(value) {
|
|
25
|
+
return typeof value === "string" && (value === pluginName || value.startsWith(pluginName + "@"));
|
|
26
|
+
}
|
|
27
|
+
function describePluginMutation(config, target) {
|
|
28
|
+
const pluginValue = config?.plugin;
|
|
29
|
+
const pluginIsArray = Array.isArray(pluginValue);
|
|
30
|
+
const existingIndex = pluginIsArray ? pluginValue.findIndex(isLitOpenCodeEntry) : -1;
|
|
31
|
+
const alreadyPresent = pluginIsArray && pluginValue.includes(target);
|
|
32
|
+
const currentCount = pluginIsArray ? pluginValue.length : 0;
|
|
33
|
+
if (alreadyPresent) {
|
|
34
|
+
return {
|
|
35
|
+
changed: false,
|
|
36
|
+
plugin: { alreadyPresent: true, currentCount, resultCount: currentCount, add: [] },
|
|
37
|
+
patch: []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const add = [target];
|
|
41
|
+
if (pluginIsArray) {
|
|
42
|
+
if (existingIndex >= 0) {
|
|
43
|
+
return {
|
|
44
|
+
changed: true,
|
|
45
|
+
plugin: { alreadyPresent: true, currentCount, resultCount: currentCount, add },
|
|
46
|
+
patch: [{ op: "replace", path: `/plugin/${existingIndex}`, value: target }]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
changed: true,
|
|
51
|
+
plugin: { alreadyPresent: false, currentCount, resultCount: currentCount + 1, add },
|
|
52
|
+
patch: [{ op: "add", path: "/plugin/-", value: target }]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const op = config !== null && Object.hasOwn(config, "plugin") ? "replace" : "add";
|
|
56
|
+
return {
|
|
57
|
+
changed: true,
|
|
58
|
+
plugin: { alreadyPresent: false, currentCount, resultCount: 1, add },
|
|
59
|
+
patch: [{ op, path: "/plugin", value: [target] }]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function applyPluginMutation(config, mutation, target) {
|
|
63
|
+
const next = config === null ? { "$schema": "https://opencode.ai/config.json" } : { ...config };
|
|
64
|
+
if (!mutation.changed)
|
|
65
|
+
return next;
|
|
66
|
+
const pluginValue = next.plugin;
|
|
67
|
+
if (Array.isArray(pluginValue)) {
|
|
68
|
+
const existingIndex = pluginValue.findIndex(isLitOpenCodeEntry);
|
|
69
|
+
if (existingIndex >= 0) {
|
|
70
|
+
next.plugin = pluginValue.map((value, index) => (index === existingIndex ? target : value));
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
next.plugin = [...pluginValue, target];
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
next.plugin = [target];
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
function renderInstallReport(report) {
|
|
80
|
+
const status = report.changed ? "Complete" : "Already installed";
|
|
81
|
+
const action = report.changed ? "plugin[] updated" : "no write needed";
|
|
82
|
+
const ok = tint("ok", "\u001b[38;5;82m");
|
|
83
|
+
return [
|
|
84
|
+
"+------------------------------------------------------------+",
|
|
85
|
+
headerLine("LitOpenCode", "\u001b[1;38;5;81m"),
|
|
86
|
+
headerLine("OpenCode plugin installer", "\u001b[38;5;245m"),
|
|
87
|
+
"+------------------------------------------------------------+",
|
|
88
|
+
"",
|
|
89
|
+
" " + padLabel("Package") + " " + report.package.name + "@" + report.package.version,
|
|
90
|
+
" " + padLabel("Config") + " " + report.path,
|
|
91
|
+
" " + padLabel("Plugin entries") + " " + report.plugin.currentCount + " -> " + report.plugin.resultCount,
|
|
92
|
+
"",
|
|
93
|
+
" [1/4] " + padLabel("Resolve package") + " " + ok,
|
|
94
|
+
" [2/4] " + padLabel("Read OpenCode config") + " " + ok,
|
|
95
|
+
" [3/4] " + padLabel("Register plugin") + " " + ok,
|
|
96
|
+
" [4/4] " + padLabel("Verify install") + " " + ok,
|
|
97
|
+
"",
|
|
98
|
+
" Status " + status,
|
|
99
|
+
" Result " + action,
|
|
100
|
+
"",
|
|
101
|
+
" Next",
|
|
102
|
+
" Restart OpenCode, then press Tab.",
|
|
103
|
+
" Agents: lit-plan / lit-loop"
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
export async function install(root, dryRun) {
|
|
107
|
+
const metadata = await readPackageMetadata();
|
|
108
|
+
const paths = createRuntimePaths(root);
|
|
109
|
+
const before = await readJsonObjectIfPresent(paths.opencodeConfigFile);
|
|
110
|
+
const target = pluginSpec(metadata);
|
|
111
|
+
const mutation = describePluginMutation(before, target);
|
|
112
|
+
const report = {
|
|
113
|
+
dryRun,
|
|
114
|
+
path: paths.opencodeConfigFile,
|
|
115
|
+
plugin: mutation.plugin,
|
|
116
|
+
patch: mutation.patch,
|
|
117
|
+
changed: mutation.changed,
|
|
118
|
+
package: metadata
|
|
119
|
+
};
|
|
120
|
+
if (dryRun) {
|
|
121
|
+
return { exitCode: 0, stdout: JSON.stringify(report, null, 2) };
|
|
122
|
+
}
|
|
123
|
+
if (mutation.changed) {
|
|
124
|
+
const next = applyPluginMutation(before, mutation, target);
|
|
125
|
+
await fs.mkdir(path.dirname(paths.opencodeConfigFile), { recursive: true });
|
|
126
|
+
await fs.writeFile(paths.opencodeConfigFile, JSON.stringify(next, null, 2) + "\n");
|
|
127
|
+
}
|
|
128
|
+
return { exitCode: 0, stdout: renderInstallReport(report) };
|
|
129
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PackageMetadata } from "./types.ts";
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function readPackageMetadata(): Promise<PackageMetadata>;
|
|
4
|
+
export declare function readJsonObjectIfPresent(filePath: string): Promise<Record<string, unknown> | null>;
|
package/dist/cli/json.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
export async function readPackageMetadata() {
|
|
8
|
+
const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
9
|
+
const parsed = JSON.parse(await fs.readFile(packagePath, "utf8"));
|
|
10
|
+
if (!isRecord(parsed) || typeof parsed.name !== "string" || typeof parsed.version !== "string") {
|
|
11
|
+
throw new Error(`Malformed package metadata at ${packagePath}`);
|
|
12
|
+
}
|
|
13
|
+
return { name: parsed.name, version: parsed.version };
|
|
14
|
+
}
|
|
15
|
+
export async function readJsonObjectIfPresent(filePath) {
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
22
|
+
return null;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (!isRecord(parsed)) {
|
|
27
|
+
throw new Error(`Malformed JSON at ${filePath}: expected an object.`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type CliResult = {
|
|
2
|
+
readonly exitCode: number;
|
|
3
|
+
readonly stdout?: string;
|
|
4
|
+
readonly stderr?: string;
|
|
5
|
+
};
|
|
6
|
+
export type PackageMetadata = {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly version: string;
|
|
9
|
+
};
|
|
10
|
+
export type ParsedArgs = {
|
|
11
|
+
readonly command?: string;
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly dryRun: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type JsonPatchOperation = {
|
|
16
|
+
readonly op: "add";
|
|
17
|
+
readonly path: "/plugin";
|
|
18
|
+
readonly value: readonly string[];
|
|
19
|
+
} | {
|
|
20
|
+
readonly op: "replace";
|
|
21
|
+
readonly path: "/plugin";
|
|
22
|
+
readonly value: readonly string[];
|
|
23
|
+
} | {
|
|
24
|
+
readonly op: "add";
|
|
25
|
+
readonly path: "/plugin/-";
|
|
26
|
+
readonly value: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly op: "replace";
|
|
29
|
+
readonly path: `/plugin/${number}`;
|
|
30
|
+
readonly value: string;
|
|
31
|
+
};
|
|
32
|
+
export type PluginMutation = {
|
|
33
|
+
readonly changed: boolean;
|
|
34
|
+
readonly plugin: {
|
|
35
|
+
readonly alreadyPresent: boolean;
|
|
36
|
+
readonly currentCount: number;
|
|
37
|
+
readonly resultCount: number;
|
|
38
|
+
readonly add: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
readonly patch: readonly JsonPatchOperation[];
|
|
41
|
+
};
|
|
42
|
+
export type InstallReport = {
|
|
43
|
+
readonly dryRun: boolean;
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly plugin: PluginMutation["plugin"];
|
|
46
|
+
readonly patch: readonly JsonPatchOperation[];
|
|
47
|
+
readonly changed: boolean;
|
|
48
|
+
readonly package: PackageMetadata;
|
|
49
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
type CliResult
|
|
2
|
-
exitCode: number;
|
|
3
|
-
stdout?: string;
|
|
4
|
-
stderr?: string;
|
|
5
|
-
};
|
|
1
|
+
import type { CliResult } from "./cli/types.ts";
|
|
6
2
|
export declare function runCli(argv?: readonly string[]): Promise<CliResult>;
|
|
7
3
|
export declare function main(argv?: readonly string[]): Promise<void>;
|
|
8
|
-
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,173 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { LitOpenCodeConfigError
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { helpText, parseArgs } from "./cli/args.js";
|
|
2
|
+
import { doctor } from "./cli/doctor.js";
|
|
3
|
+
import { install } from "./cli/install.js";
|
|
4
|
+
import { LitOpenCodeConfigError } from "./config.js";
|
|
5
|
+
const installFrames = [
|
|
6
|
+
{ bar: "[##........]", label: "resolving litopencode package" },
|
|
7
|
+
{ bar: "[####......]", label: "opening OpenCode config" },
|
|
8
|
+
{ bar: "[######....]", label: "patching plugin registry" },
|
|
9
|
+
{ bar: "[########..]", label: "checking lit-plan / lit-loop" },
|
|
10
|
+
{ bar: "[##########]", label: "sealing installer state" }
|
|
11
|
+
];
|
|
12
|
+
function sleep(milliseconds) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
8
14
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
function parseArgs(argv) {
|
|
18
|
-
const parsed = {
|
|
19
|
-
command: argv[0],
|
|
20
|
-
root: process.cwd(),
|
|
21
|
-
dryRun: false
|
|
22
|
-
};
|
|
23
|
-
if (parsed.command === "--help" || parsed.command === "-h") {
|
|
24
|
-
parsed.command = "help";
|
|
25
|
-
}
|
|
26
|
-
for (let index = 1; index < argv.length; index += 1) {
|
|
27
|
-
const arg = argv[index];
|
|
28
|
-
if (arg === "--root" || arg === "--workdir") {
|
|
29
|
-
const value = argv[index + 1];
|
|
30
|
-
if (!value)
|
|
31
|
-
throw new Error(`${arg} requires a value`);
|
|
32
|
-
parsed.root = value;
|
|
33
|
-
index += 1;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (arg === "--dry-run") {
|
|
37
|
-
parsed.dryRun = true;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (arg === "--help" || arg === "-h") {
|
|
41
|
-
parsed.command = "help";
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
45
|
-
}
|
|
46
|
-
return parsed;
|
|
47
|
-
}
|
|
48
|
-
function helpText() {
|
|
49
|
-
return [
|
|
50
|
-
"litopencode",
|
|
51
|
-
"",
|
|
52
|
-
"Usage:",
|
|
53
|
-
" litopencode doctor [--root <dir>]",
|
|
54
|
-
" litopencode install --dry-run [--root <dir>]",
|
|
55
|
-
"",
|
|
56
|
-
"Commands:",
|
|
57
|
-
" doctor Report package, config, and runtime path status without writing files.",
|
|
58
|
-
" install Print the exact opencode.json mutation for adding this plugin."
|
|
59
|
-
].join("\n");
|
|
60
|
-
}
|
|
61
|
-
async function readJsonObjectIfPresent(filePath) {
|
|
62
|
-
let raw;
|
|
63
|
-
try {
|
|
64
|
-
raw = await fs.readFile(filePath, "utf8");
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
68
|
-
return null;
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
const parsed = JSON.parse(raw);
|
|
72
|
-
if (!isRecord(parsed)) {
|
|
73
|
-
throw new Error(`Malformed JSON at ${filePath}: expected an object.`);
|
|
74
|
-
}
|
|
75
|
-
return parsed;
|
|
76
|
-
}
|
|
77
|
-
function describePluginMutation(config) {
|
|
78
|
-
const pluginValue = config?.plugin;
|
|
79
|
-
const pluginIsArray = Array.isArray(pluginValue);
|
|
80
|
-
const alreadyPresent = pluginIsArray && pluginValue.includes("litopencode");
|
|
81
|
-
const currentCount = pluginIsArray ? pluginValue.length : 0;
|
|
82
|
-
if (alreadyPresent) {
|
|
83
|
-
return {
|
|
84
|
-
changed: false,
|
|
85
|
-
plugin: {
|
|
86
|
-
alreadyPresent: true,
|
|
87
|
-
currentCount,
|
|
88
|
-
resultCount: currentCount,
|
|
89
|
-
add: []
|
|
90
|
-
},
|
|
91
|
-
patch: []
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
const add = ["litopencode"];
|
|
95
|
-
const resultCount = currentCount + add.length;
|
|
96
|
-
if (pluginIsArray) {
|
|
97
|
-
return {
|
|
98
|
-
changed: true,
|
|
99
|
-
plugin: {
|
|
100
|
-
alreadyPresent: false,
|
|
101
|
-
currentCount,
|
|
102
|
-
resultCount,
|
|
103
|
-
add
|
|
104
|
-
},
|
|
105
|
-
patch: [{ op: "add", path: "/plugin/-", value: "litopencode" }]
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const op = config !== null && Object.hasOwn(config, "plugin") ? "replace" : "add";
|
|
109
|
-
return {
|
|
110
|
-
changed: true,
|
|
111
|
-
plugin: {
|
|
112
|
-
alreadyPresent: false,
|
|
113
|
-
currentCount,
|
|
114
|
-
resultCount: 1,
|
|
115
|
-
add
|
|
116
|
-
},
|
|
117
|
-
patch: [{ op, path: "/plugin", value: ["litopencode"] }]
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async function doctor(root) {
|
|
121
|
-
const metadata = await readPackageMetadata();
|
|
122
|
-
const loaded = await loadConfig(root);
|
|
123
|
-
const runtimeExists = await fs
|
|
124
|
-
.stat(loaded.paths.runtimeDir)
|
|
125
|
-
.then(() => true)
|
|
126
|
-
.catch((error) => {
|
|
127
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
128
|
-
return false;
|
|
129
|
-
throw error;
|
|
130
|
-
});
|
|
131
|
-
return {
|
|
132
|
-
exitCode: 0,
|
|
133
|
-
stdout: JSON.stringify({
|
|
134
|
-
package: metadata,
|
|
135
|
-
config: {
|
|
136
|
-
source: loaded.source,
|
|
137
|
-
path: loaded.paths.configFile,
|
|
138
|
-
enabled: loaded.config.enabled,
|
|
139
|
-
logLevel: loaded.config.logLevel
|
|
140
|
-
},
|
|
141
|
-
state: {
|
|
142
|
-
runtimeDir: loaded.paths.runtimeDir,
|
|
143
|
-
runtimeExists,
|
|
144
|
-
stateFile: loaded.paths.stateFile,
|
|
145
|
-
logFile: loaded.paths.logFile,
|
|
146
|
-
ledgerFile: loaded.paths.ledgerFile
|
|
147
|
-
}
|
|
148
|
-
}, null, 2)
|
|
149
|
-
};
|
|
15
|
+
function shouldAnimateInstall(argv, result) {
|
|
16
|
+
return (process.stdout.isTTY === true &&
|
|
17
|
+
process.env.CI === undefined &&
|
|
18
|
+
argv[0] === "install" &&
|
|
19
|
+
!argv.includes("--dry-run") &&
|
|
20
|
+
result.exitCode === 0 &&
|
|
21
|
+
result.stdout !== undefined);
|
|
150
22
|
}
|
|
151
|
-
async function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
};
|
|
23
|
+
async function renderInstallEffect() {
|
|
24
|
+
process.stdout.write("\n");
|
|
25
|
+
for (const frame of installFrames) {
|
|
26
|
+
process.stdout.write("\r\u001b[2K " + frame.bar + " " + frame.label);
|
|
27
|
+
await sleep(95);
|
|
157
28
|
}
|
|
158
|
-
|
|
159
|
-
const before = await readJsonObjectIfPresent(paths.opencodeConfigFile);
|
|
160
|
-
const mutation = describePluginMutation(before);
|
|
161
|
-
return {
|
|
162
|
-
exitCode: 0,
|
|
163
|
-
stdout: JSON.stringify({
|
|
164
|
-
dryRun: true,
|
|
165
|
-
path: paths.opencodeConfigFile,
|
|
166
|
-
plugin: mutation.plugin,
|
|
167
|
-
patch: mutation.patch,
|
|
168
|
-
changed: mutation.changed
|
|
169
|
-
}, null, 2)
|
|
170
|
-
};
|
|
29
|
+
process.stdout.write("\r\u001b[2K");
|
|
171
30
|
}
|
|
172
31
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
173
32
|
let parsed;
|
|
@@ -196,6 +55,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
196
55
|
}
|
|
197
56
|
export async function main(argv = process.argv.slice(2)) {
|
|
198
57
|
const result = await runCli(argv);
|
|
58
|
+
if (shouldAnimateInstall(argv, result))
|
|
59
|
+
await renderInstallEffect();
|
|
199
60
|
if (result.stdout)
|
|
200
61
|
process.stdout.write(`${result.stdout}\n`);
|
|
201
62
|
if (result.stderr)
|
package/dist/features.js
CHANGED
|
@@ -112,7 +112,7 @@ export const litOpenCodeFeatures = Object.freeze([
|
|
|
112
112
|
{
|
|
113
113
|
id: "doctor-install",
|
|
114
114
|
title: "Doctor And Installer CLI",
|
|
115
|
-
summary: "Reports package, config, and state health and previews the OpenCode plugin config mutation.",
|
|
115
|
+
summary: "Reports package, config, and state health and installs or previews the OpenCode plugin config mutation.",
|
|
116
116
|
bindings: [
|
|
117
117
|
{
|
|
118
118
|
kind: "cli",
|
|
@@ -122,9 +122,9 @@ export const litOpenCodeFeatures = Object.freeze([
|
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
kind: "cli",
|
|
125
|
-
id: "litopencode install
|
|
125
|
+
id: "npx litopencode install",
|
|
126
126
|
surface: "litopencode CLI install command",
|
|
127
|
-
description: "
|
|
127
|
+
description: "Adds or updates a version-pinned litopencode entry in opencode.json with branded progress output; --dry-run prints the mutation only."
|
|
128
128
|
}
|
|
129
129
|
],
|
|
130
130
|
verification: ["node --test test/cli.test.mjs", "node --test test/config-state.test.mjs"]
|
package/dist/skills.js
CHANGED
|
@@ -35,11 +35,11 @@ export const litOpenCodeRuntimeSkills = Object.freeze([
|
|
|
35
35
|
{
|
|
36
36
|
id: "doctor-installer",
|
|
37
37
|
title: "Doctor Installer",
|
|
38
|
-
summary: "Use the CLI to
|
|
38
|
+
summary: "Use the CLI to install the plugin into OpenCode or preview the plugin mutation without writing files.",
|
|
39
39
|
featureIds: ["doctor-install"],
|
|
40
|
-
discovery: "Run litopencode doctor or litopencode install --dry-run.",
|
|
40
|
+
discovery: "Run npx litopencode install, litopencode doctor, or litopencode install --dry-run.",
|
|
41
41
|
safety: [
|
|
42
|
-
"Default installation
|
|
42
|
+
"Default installation writes only the version-pinned OpenCode opencode.json plugin entry.",
|
|
43
43
|
"Malformed config fails closed with a typed config error."
|
|
44
44
|
]
|
|
45
45
|
},
|
package/docs/migration.md
CHANGED
|
@@ -13,8 +13,9 @@ This guide describes the supported migration shape for moving an OpenCode workfl
|
|
|
13
13
|
- Use the `LITOPENCODE_` prefix for LitOpenCode-owned environment variables.
|
|
14
14
|
- Keep OpenCode plugin configuration in `opencode.json`.
|
|
15
15
|
- Keep LitOpenCode runtime config in `.litopencode/config.json` when project-local config is needed.
|
|
16
|
+
- Use `npx litopencode install` to add a version-pinned entry such as `litopencode@0.0.2` to the default OpenCode config at `~/.config/opencode/opencode.json`.
|
|
16
17
|
- Use `litopencode doctor --root <workspace>` to inspect package metadata, config source, runtime paths, and ledger location without writing files.
|
|
17
|
-
- Use `litopencode install --dry-run --root <workspace>` to preview the `opencode.json` plugin mutation
|
|
18
|
+
- Use `litopencode install --dry-run --root <workspace>` to preview the `opencode.json` plugin mutation without writing files.
|
|
18
19
|
|
|
19
20
|
## Durable State
|
|
20
21
|
|
|
@@ -41,7 +41,7 @@ After the source gates pass, verify the packed artifact in a temporary directory
|
|
|
41
41
|
```sh
|
|
42
42
|
tmp="$(mktemp -d)"
|
|
43
43
|
npm pack --pack-destination "$tmp"
|
|
44
|
-
tar -xzf "$tmp"/litopencode-0.0.
|
|
44
|
+
tar -xzf "$tmp"/litopencode-0.0.2.tgz -C "$tmp"
|
|
45
45
|
node --input-type=module -e "import('$tmp/package/dist/index.js').then((m) => console.log(m.default?.id ?? m.pluginId))"
|
|
46
46
|
(
|
|
47
47
|
cd "$tmp/package"
|
|
@@ -73,21 +73,23 @@ mkdir "$tmp/consumer"
|
|
|
73
73
|
(
|
|
74
74
|
cd "$tmp/consumer"
|
|
75
75
|
npm init -y
|
|
76
|
-
npm install "$tmp"/litopencode-0.0.
|
|
77
|
-
npm ls litopencode
|
|
76
|
+
npm install "$tmp"/litopencode-0.0.2.tgz
|
|
77
|
+
npm ls litopencode --all
|
|
78
78
|
node --input-type=module -e "import('litopencode').then((m) => console.log(m.default.id))"
|
|
79
79
|
node_modules/.bin/litopencode --help
|
|
80
80
|
node_modules/.bin/litopencode doctor --root .
|
|
81
81
|
node_modules/.bin/litopencode install --dry-run --root .
|
|
82
|
+
node_modules/.bin/litopencode install --root .
|
|
82
83
|
)
|
|
83
84
|
rm -rf "$tmp"
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
Expected results:
|
|
87
88
|
|
|
88
|
-
- `@opencode-ai/plugin` is
|
|
89
|
+
- `@opencode-ai/plugin` is declared as an optional peer for TypeScript host typings without forcing npx/global installs to pull its transitive runtime tree
|
|
89
90
|
- package import prints `litopencode`
|
|
90
|
-
- CLI help, doctor,
|
|
91
|
+
- CLI help, doctor, install dry-run, and write-enabled install exit 0
|
|
92
|
+
- write-enabled install creates or updates `opencode.json` without echoing unrelated config secrets
|
|
91
93
|
- temporary project cleanup removes the probe directory
|
|
92
94
|
|
|
93
95
|
## OpenCode Host Probe
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litopencode",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "LitOpenCode OpenCode plugin bootstrap package.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,10 +19,16 @@
|
|
|
19
19
|
"test": "node --test",
|
|
20
20
|
"typecheck": "node tools/run-typecheck.mjs"
|
|
21
21
|
},
|
|
22
|
-
"
|
|
22
|
+
"peerDependencies": {
|
|
23
23
|
"@opencode-ai/plugin": "^1.17.6"
|
|
24
24
|
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"@opencode-ai/plugin": {
|
|
27
|
+
"optional": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
25
30
|
"devDependencies": {
|
|
31
|
+
"@opencode-ai/plugin": "^1.17.6",
|
|
26
32
|
"@types/node": "^24.12.2",
|
|
27
33
|
"typescript": "^5.9.3"
|
|
28
34
|
}
|
|
@@ -5,13 +5,15 @@ Use this LitOpenCode skill when a contributor needs the static CLI health and in
|
|
|
5
5
|
## Covers
|
|
6
6
|
|
|
7
7
|
- Inspect package metadata, config source, runtime paths, and state presence.
|
|
8
|
-
-
|
|
8
|
+
- Install or update the version-pinned OpenCode plugin entry with a bounded branded terminal flow.
|
|
9
|
+
- Preview OpenCode plugin configuration changes without writing files when `--dry-run` is set.
|
|
9
10
|
- Keep malformed config handling fail-closed.
|
|
10
11
|
- Keep installer output bounded and avoid leaking existing user config content.
|
|
11
12
|
|
|
12
13
|
## OpenCode Surfaces
|
|
13
14
|
|
|
14
15
|
- CLI: `litopencode doctor`
|
|
16
|
+
- CLI: `npx litopencode install`
|
|
15
17
|
- CLI: `litopencode install --dry-run`
|
|
16
18
|
- Runtime feature id: `doctor-install`
|
|
17
19
|
|
|
@@ -19,4 +21,4 @@ Use this LitOpenCode skill when a contributor needs the static CLI health and in
|
|
|
19
21
|
|
|
20
22
|
- This file is static documentation.
|
|
21
23
|
- Do not execute commands from this file automatically.
|
|
22
|
-
- Treat
|
|
24
|
+
- Treat installer output as a bounded summary, not as a full config dump.
|