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 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
- - **Installer/doctor CLI** -- Inspect package, config, and runtime state with safe dry-run install output.
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 &lt;dir&gt;</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 |
@@ -0,0 +1,3 @@
1
+ import type { ParsedArgs } from "./types.ts";
2
+ export declare function parseArgs(argv: readonly string[]): ParsedArgs;
3
+ export declare function helpText(): string;
@@ -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,2 @@
1
+ import type { CliResult } from "./types.ts";
2
+ export declare function doctor(root: string): Promise<CliResult>;
@@ -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,2 @@
1
+ import type { CliResult } from "./types.ts";
2
+ export declare function install(root: string, dryRun: boolean): Promise<CliResult>;
@@ -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>;
@@ -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 fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { LitOpenCodeConfigError, loadConfig } from "./config.js";
5
- import { createRuntimePaths } from "./state.js";
6
- function isRecord(value) {
7
- return typeof value === "object" && value !== null && !Array.isArray(value);
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
- async function readPackageMetadata() {
10
- const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
11
- const parsed = JSON.parse(await fs.readFile(packagePath, "utf8"));
12
- if (!isRecord(parsed) || typeof parsed.name !== "string" || typeof parsed.version !== "string") {
13
- throw new Error(`Malformed package metadata at ${packagePath}`);
14
- }
15
- return { name: parsed.name, version: parsed.version };
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 install(root, dryRun) {
152
- if (!dryRun) {
153
- return {
154
- exitCode: 2,
155
- stderr: "litopencode install currently requires --dry-run; no files were written."
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
- const paths = createRuntimePaths(root);
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 --dry-run",
125
+ id: "npx litopencode install",
126
126
  surface: "litopencode CLI install command",
127
- description: "Prints the opencode.json plugin mutation without modifying the workspace."
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 inspect health and preview plugin installation without writing files.",
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 remains dry-run only.",
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 before applying it manually or through a future write-enabled installer.
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.0.tgz -C "$tmp"
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.0.tgz
77
- npm ls litopencode @opencode-ai/plugin --all
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 installed as a runtime dependency below `litopencode`
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, and install dry-run exit 0
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.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
- "dependencies": {
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
- - Preview OpenCode plugin configuration changes without writing files.
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 dry-run output as a summary of intended mutation, not as a full config dump.
24
+ - Treat installer output as a bounded summary, not as a full config dump.