litopencode 0.0.1 → 0.0.3

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
@@ -34,7 +34,7 @@
34
34
 
35
35
  npx litopencode install
36
36
 
37
- Then restart OpenCode. The agent switcher should show <code>lit-plan</code> and <code>lit-loop</code>.
37
+ The installer delegates the default install path to OpenCode's own plugin installer and registers a version-pinned entry such as <code>litopencode@0.0.3</code>. Then restart OpenCode. The agent switcher should show <code>lit-plan</code> and <code>lit-loop</code>.
38
38
 
39
39
  For a preview without writing <code>opencode.json</code>:
40
40
 
@@ -72,7 +72,7 @@ By default, the installer targets <code>~/.config/opencode/opencode.json</code>,
72
72
 
73
73
  ## OpenCode Plugin
74
74
 
75
- The default export is an OpenCode <code>PluginModule</code>. The packed artifact exports compiled JavaScript from <code>dist/index.js</code>, so installed npm consumers do not rely on Node's TypeScript stripping behavior for files under <code>node_modules</code>.
75
+ The default export is an OpenCode plugin function, and <code>litopencode/server</code> points at the same compiled server entrypoint for OpenCode's plugin installer. The packed artifact exports compiled JavaScript from <code>dist/index.js</code>, so installed npm consumers do not rely on Node's TypeScript stripping behavior for files under <code>node_modules</code>.
76
76
 
77
77
  The server surface registers:
78
78
 
@@ -1,12 +1,63 @@
1
+ import { execFile } from "node:child_process";
1
2
  import fs from "node:fs/promises";
3
+ import os from "node:os";
2
4
  import path from "node:path";
5
+ import { promisify } from "node:util";
3
6
  import { createRuntimePaths } from "../state.js";
4
7
  import { readJsonObjectIfPresent, readPackageMetadata } from "./json.js";
5
- const pluginId = "litopencode";
6
- function describePluginMutation(config) {
8
+ const pluginName = "litopencode";
9
+ const reset = "\u001b[0m";
10
+ const execFileAsync = promisify(execFile);
11
+ function useColor() {
12
+ return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
13
+ }
14
+ function tint(value, code) {
15
+ if (!useColor())
16
+ return value;
17
+ return code + value + reset;
18
+ }
19
+ function padLabel(label) {
20
+ return label.padEnd(24, " ");
21
+ }
22
+ function headerLine(value, code) {
23
+ return "| " + tint(value.padEnd(58, " "), code) + " |";
24
+ }
25
+ function pluginSpec(metadata) {
26
+ return metadata.name + "@" + metadata.version;
27
+ }
28
+ function isLitOpenCodeEntry(value) {
29
+ return typeof value === "string" && (value === pluginName || value.startsWith(pluginName + "@"));
30
+ }
31
+ function defaultOpenCodeRoot() {
32
+ const configHome = process.env.XDG_CONFIG_HOME;
33
+ if (configHome && configHome.length > 0)
34
+ return path.join(configHome, "opencode");
35
+ return path.join(os.homedir(), ".config", "opencode");
36
+ }
37
+ function shouldUseOpenCodeInstaller(root) {
38
+ return path.resolve(root) === path.resolve(defaultOpenCodeRoot());
39
+ }
40
+ async function installWithOpenCode(target) {
41
+ try {
42
+ await execFileAsync("opencode", ["plugin", target, "--global", "--force"], {
43
+ timeout: 120_000,
44
+ maxBuffer: 1024 * 1024 * 4
45
+ });
46
+ }
47
+ catch (error) {
48
+ const detail = error instanceof Error && "stderr" in error && typeof error.stderr === "string"
49
+ ? error.stderr.trim()
50
+ : error instanceof Error
51
+ ? error.message
52
+ : String(error);
53
+ throw new Error("OpenCode plugin install failed for " + target + (detail ? ":\n" + detail : ""));
54
+ }
55
+ }
56
+ function describePluginMutation(config, target) {
7
57
  const pluginValue = config?.plugin;
8
58
  const pluginIsArray = Array.isArray(pluginValue);
9
- const alreadyPresent = pluginIsArray && pluginValue.includes(pluginId);
59
+ const existingIndex = pluginIsArray ? pluginValue.findIndex(isLitOpenCodeEntry) : -1;
60
+ const alreadyPresent = pluginIsArray && pluginValue.includes(target);
10
61
  const currentCount = pluginIsArray ? pluginValue.length : 0;
11
62
  if (alreadyPresent) {
12
63
  return {
@@ -15,56 +66,78 @@ function describePluginMutation(config) {
15
66
  patch: []
16
67
  };
17
68
  }
18
- const add = [pluginId];
69
+ const add = [target];
19
70
  if (pluginIsArray) {
71
+ if (existingIndex >= 0) {
72
+ return {
73
+ changed: true,
74
+ plugin: { alreadyPresent: true, currentCount, resultCount: currentCount, add },
75
+ patch: [{ op: "replace", path: `/plugin/${existingIndex}`, value: target }]
76
+ };
77
+ }
20
78
  return {
21
79
  changed: true,
22
80
  plugin: { alreadyPresent: false, currentCount, resultCount: currentCount + 1, add },
23
- patch: [{ op: "add", path: "/plugin/-", value: pluginId }]
81
+ patch: [{ op: "add", path: "/plugin/-", value: target }]
24
82
  };
25
83
  }
26
84
  const op = config !== null && Object.hasOwn(config, "plugin") ? "replace" : "add";
27
85
  return {
28
86
  changed: true,
29
87
  plugin: { alreadyPresent: false, currentCount, resultCount: 1, add },
30
- patch: [{ op, path: "/plugin", value: [pluginId] }]
88
+ patch: [{ op, path: "/plugin", value: [target] }]
31
89
  };
32
90
  }
33
- function applyPluginMutation(config, mutation) {
91
+ function applyPluginMutation(config, mutation, target) {
34
92
  const next = config === null ? { "$schema": "https://opencode.ai/config.json" } : { ...config };
35
93
  if (!mutation.changed)
36
94
  return next;
37
95
  const pluginValue = next.plugin;
38
96
  if (Array.isArray(pluginValue)) {
39
- next.plugin = [...pluginValue, pluginId];
97
+ const existingIndex = pluginValue.findIndex(isLitOpenCodeEntry);
98
+ if (existingIndex >= 0) {
99
+ next.plugin = pluginValue.map((value, index) => (index === existingIndex ? target : value));
100
+ return next;
101
+ }
102
+ next.plugin = [...pluginValue, target];
40
103
  return next;
41
104
  }
42
- next.plugin = [pluginId];
105
+ next.plugin = [target];
43
106
  return next;
44
107
  }
45
108
  function renderInstallReport(report) {
46
109
  const status = report.changed ? "Complete" : "Already installed";
47
- const action = report.changed ? "Added litopencode to plugin[]" : "No config change needed";
110
+ const action = report.changed ? "plugin[] updated" : "no write needed";
111
+ const ok = tint("ok", "\u001b[38;5;82m");
48
112
  return [
49
- "LitOpenCode Installer",
50
- "=====================",
113
+ "+------------------------------------------------------------+",
114
+ headerLine("LitOpenCode", "\u001b[1;38;5;81m"),
115
+ headerLine("OpenCode plugin installer", "\u001b[38;5;245m"),
116
+ "+------------------------------------------------------------+",
117
+ "",
118
+ " " + padLabel("Package") + " " + report.package.name + "@" + report.package.version,
119
+ " " + padLabel("Config") + " " + report.path,
120
+ " " + padLabel("Plugin entries") + " " + report.plugin.currentCount + " -> " + report.plugin.resultCount,
51
121
  "",
52
- "[-] Resolving package " + `${report.package.name}@${report.package.version}`,
53
- "[\\] Reading OpenCode config " + report.path,
54
- "[|] Planning plugin patch " + `${report.plugin.currentCount} -> ${report.plugin.resultCount}`,
55
- "[/] Writing config " + action,
122
+ " [1/4] " + padLabel("Resolve package") + " " + ok,
123
+ " [2/4] " + padLabel("Read OpenCode config") + " " + ok,
124
+ " [3/4] " + padLabel("Register plugin") + " " + ok,
125
+ " [4/4] " + padLabel("Verify install") + " " + ok,
56
126
  "",
57
- status,
127
+ " Status " + status,
128
+ " Result " + action,
58
129
  "",
59
- "Next:",
60
- " Restart OpenCode and press tab to switch between lit-plan and lit-loop."
130
+ " Next",
131
+ " Restart OpenCode, then press Tab.",
132
+ " Agents: lit-plan / lit-loop"
61
133
  ].join("\n");
62
134
  }
63
135
  export async function install(root, dryRun) {
64
136
  const metadata = await readPackageMetadata();
65
137
  const paths = createRuntimePaths(root);
66
138
  const before = await readJsonObjectIfPresent(paths.opencodeConfigFile);
67
- const mutation = describePluginMutation(before);
139
+ const target = pluginSpec(metadata);
140
+ const mutation = describePluginMutation(before, target);
68
141
  const report = {
69
142
  dryRun,
70
143
  path: paths.opencodeConfigFile,
@@ -76,8 +149,11 @@ export async function install(root, dryRun) {
76
149
  if (dryRun) {
77
150
  return { exitCode: 0, stdout: JSON.stringify(report, null, 2) };
78
151
  }
79
- if (mutation.changed) {
80
- const next = applyPluginMutation(before, mutation);
152
+ if (shouldUseOpenCodeInstaller(root)) {
153
+ await installWithOpenCode(target);
154
+ }
155
+ else if (mutation.changed) {
156
+ const next = applyPluginMutation(before, mutation, target);
81
157
  await fs.mkdir(path.dirname(paths.opencodeConfigFile), { recursive: true });
82
158
  await fs.writeFile(paths.opencodeConfigFile, JSON.stringify(next, null, 2) + "\n");
83
159
  }
@@ -15,15 +15,19 @@ export type ParsedArgs = {
15
15
  export type JsonPatchOperation = {
16
16
  readonly op: "add";
17
17
  readonly path: "/plugin";
18
- readonly value: readonly ["litopencode"];
18
+ readonly value: readonly string[];
19
19
  } | {
20
20
  readonly op: "replace";
21
21
  readonly path: "/plugin";
22
- readonly value: readonly ["litopencode"];
22
+ readonly value: readonly string[];
23
23
  } | {
24
24
  readonly op: "add";
25
25
  readonly path: "/plugin/-";
26
- readonly value: "litopencode";
26
+ readonly value: string;
27
+ } | {
28
+ readonly op: "replace";
29
+ readonly path: `/plugin/${number}`;
30
+ readonly value: string;
27
31
  };
28
32
  export type PluginMutation = {
29
33
  readonly changed: boolean;
package/dist/cli.js CHANGED
@@ -2,6 +2,32 @@ import { helpText, parseArgs } from "./cli/args.js";
2
2
  import { doctor } from "./cli/doctor.js";
3
3
  import { install } from "./cli/install.js";
4
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));
14
+ }
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);
22
+ }
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);
28
+ }
29
+ process.stdout.write("\r\u001b[2K");
30
+ }
5
31
  export async function runCli(argv = process.argv.slice(2)) {
6
32
  let parsed;
7
33
  try {
@@ -29,6 +55,8 @@ export async function runCli(argv = process.argv.slice(2)) {
29
55
  }
30
56
  export async function main(argv = process.argv.slice(2)) {
31
57
  const result = await runCli(argv);
58
+ if (shouldAnimateInstall(argv, result))
59
+ await renderInstallEffect();
32
60
  if (result.stdout)
33
61
  process.stdout.write(`${result.stdout}\n`);
34
62
  if (result.stderr)
package/dist/features.js CHANGED
@@ -124,7 +124,7 @@ export const litOpenCodeFeatures = Object.freeze([
124
124
  kind: "cli",
125
125
  id: "npx litopencode install",
126
126
  surface: "litopencode CLI install command",
127
- description: "Adds litopencode to opencode.json with branded progress output; --dry-run prints the mutation only."
127
+ description: "Delegates default setup to OpenCode's plugin installer, or adds a version-pinned litopencode entry for custom roots; --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/index.d.ts CHANGED
@@ -7,9 +7,9 @@ export { litOpenCodeTools, litTool, litworkTool } from "./tools.ts";
7
7
  export { findLitOpenCodeFeature, litOpenCodeFeatures, type LitOpenCodeBindingKind, type LitOpenCodeFeature, type LitOpenCodeFeatureBinding, type LitOpenCodeFeatureId } from "./features.ts";
8
8
  export { findLitOpenCodeRuntimeSkill, litOpenCodeRuntimeSkills, type LitOpenCodeRuntimeSkill, type LitOpenCodeRuntimeSkillId } from "./skills.ts";
9
9
  export declare const pluginId = "litopencode";
10
- export declare const server: (input?: PluginInput) => Promise<Hooks>;
11
- declare const pluginModule: {
10
+ declare const litOpenCodePlugin: (input?: PluginInput) => Promise<Hooks>;
11
+ export declare const pluginModule: {
12
12
  id: string;
13
13
  server: (input?: PluginInput) => Promise<Hooks>;
14
14
  };
15
- export default pluginModule;
15
+ export default litOpenCodePlugin;
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { litOpenCodeTools, litTool, litworkTool } from "./tools.js";
12
12
  export { findLitOpenCodeFeature, litOpenCodeFeatures } from "./features.js";
13
13
  export { findLitOpenCodeRuntimeSkill, litOpenCodeRuntimeSkills } from "./skills.js";
14
14
  export const pluginId = "litopencode";
15
- export const server = async (input) => {
15
+ const litOpenCodePlugin = async (input) => {
16
16
  const root = input?.worktree ?? input?.directory ?? ".";
17
17
  const loaded = await loadConfig(root);
18
18
  const logger = createLogger(loaded.paths);
@@ -38,8 +38,8 @@ export const server = async (input) => {
38
38
  });
39
39
  return hooks;
40
40
  };
41
- const pluginModule = {
41
+ export const pluginModule = {
42
42
  id: pluginId,
43
- server
43
+ server: litOpenCodePlugin
44
44
  };
45
- export default pluginModule;
45
+ export default litOpenCodePlugin;
package/dist/skills.js CHANGED
@@ -39,7 +39,7 @@ export const litOpenCodeRuntimeSkills = Object.freeze([
39
39
  featureIds: ["doctor-install"],
40
40
  discovery: "Run npx litopencode install, litopencode doctor, or litopencode install --dry-run.",
41
41
  safety: [
42
- "Default installation writes only the OpenCode opencode.json plugin entry.",
42
+ "Default installation delegates to the OpenCode plugin installer; custom roots write only the version-pinned 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,7 +13,7 @@ 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 `litopencode` to the default OpenCode config at `~/.config/opencode/opencode.json`.
16
+ - Use `npx litopencode install` to delegate default setup to `opencode plugin litopencode@0.0.3 --global --force` and register the version-pinned plugin entry.
17
17
  - Use `litopencode doctor --root <workspace>` to inspect package metadata, config source, runtime paths, and ledger location without writing files.
18
18
  - Use `litopencode install --dry-run --root <workspace>` to preview the `opencode.json` plugin mutation without writing files.
19
19
 
@@ -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.1.tgz -C "$tmp"
44
+ tar -xzf "$tmp"/litopencode-0.0.3.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,7 +73,7 @@ mkdir "$tmp/consumer"
73
73
  (
74
74
  cd "$tmp/consumer"
75
75
  npm init -y
76
- npm install "$tmp"/litopencode-0.0.1.tgz
76
+ npm install "$tmp"/litopencode-0.0.3.tgz
77
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
@@ -94,7 +94,7 @@ Expected results:
94
94
 
95
95
  ## OpenCode Host Probe
96
96
 
97
- Use the installed temp-project package, not the source tree, to import `litopencode`, call the default plugin module\'s `server()` function, invoke the config hook, command hook, `lit` and `litwork` tools, and before/after tool guard hooks. Expected results:
97
+ Use the installed temp-project package, not the source tree, to import `litopencode`, import `litopencode/server`, call the plugin function, invoke the config hook, command hook, `lit` and `litwork` tools, and before/after tool guard hooks. Expected results:
98
98
 
99
99
  - `server()` exposes config, tool, command activation, dispose, and non-enumerable tool guard hooks
100
100
  - config registers the LitOpenCode agent roster
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litopencode",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "LitOpenCode OpenCode plugin bootstrap package.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,14 @@
8
8
  },
9
9
  "types": "./dist/index.d.ts",
10
10
  "exports": {
11
- ".": "./dist/index.js"
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./server": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
12
19
  },
13
20
  "scripts": {
14
21
  "build": "node tools/run-build.mjs",
@@ -5,7 +5,7 @@ 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
- - Install the OpenCode plugin entry with a bounded branded terminal flow.
8
+ - Install or update the version-pinned OpenCode plugin through the OpenCode plugin installer with a bounded branded terminal flow.
9
9
  - Preview OpenCode plugin configuration changes without writing files when `--dry-run` is set.
10
10
  - Keep malformed config handling fail-closed.
11
11
  - Keep installer output bounded and avoid leaking existing user config content.