@litfamily/litopencode 1.0.29 → 1.0.31
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/CHANGELOG.md +22 -0
- package/README-Ko-KR.md +7 -7
- package/README.md +7 -7
- package/dist/activation-primary-prompts.js +15 -6
- package/dist/activation-prompt-utils.d.ts +2 -2
- package/dist/activation-prompt-utils.js +9 -3
- package/dist/activation-reentry.d.ts +29 -0
- package/dist/activation-reentry.js +71 -0
- package/dist/activation-routing.d.ts +1 -1
- package/dist/activation-routing.js +40 -7
- package/dist/activation-workflow-prompts.d.ts +2 -0
- package/dist/activation-workflow-prompts.js +32 -7
- package/dist/activation.d.ts +6 -1
- package/dist/activation.js +34 -7
- package/dist/cli/args.js +14 -0
- package/dist/cli/auto-update.js +2 -4
- package/dist/cli/command-plan.d.ts +40 -0
- package/dist/cli/command-plan.js +62 -0
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +65 -4
- package/dist/cli/host-detect.d.ts +58 -0
- package/dist/cli/host-detect.js +199 -0
- package/dist/cli/install-report.js +3 -1
- package/dist/cli/install.d.ts +9 -1
- package/dist/cli/install.js +134 -31
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +5 -3
- package/dist/cli/office-dependencies.d.ts +2 -0
- package/dist/cli/office-dependencies.js +7 -3
- package/dist/cli/scientific-visualization-dependencies.js +1 -1
- package/dist/cli/tui-config.d.ts +27 -2
- package/dist/cli/tui-config.js +77 -2
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli.js +9 -6
- package/dist/commands.d.ts +10 -1
- package/dist/commands.js +17 -3
- package/dist/features.d.ts +27 -1
- package/dist/features.js +40 -8
- package/dist/host/server.d.ts +7 -0
- package/dist/host/server.js +12 -0
- package/dist/host/tui.d.ts +7 -0
- package/dist/host/tui.js +12 -0
- package/dist/server.js +9 -0
- package/dist/skill-renames.d.ts +1 -1
- package/dist/skills.d.ts +9 -2
- package/dist/skills.js +22 -8
- package/dist/tui/mission-source-v2.d.ts +51 -0
- package/dist/tui/mission-source-v2.js +52 -0
- package/dist/v2/agents.d.ts +105 -0
- package/dist/v2/agents.js +185 -0
- package/dist/v2/commands.d.ts +44 -0
- package/dist/v2/commands.js +65 -0
- package/dist/v2/context.d.ts +67 -0
- package/dist/v2/context.js +17 -0
- package/dist/v2/events.d.ts +55 -0
- package/dist/v2/events.js +193 -0
- package/dist/v2/hooks.d.ts +39 -0
- package/dist/v2/hooks.js +61 -0
- package/dist/v2/server.d.ts +2 -0
- package/dist/v2/server.js +68 -0
- package/dist/v2/session-hooks.d.ts +64 -0
- package/dist/v2/session-hooks.js +230 -0
- package/dist/v2/tools.d.ts +26 -0
- package/dist/v2/tools.js +49 -0
- package/dist/v2/tui.d.ts +63 -0
- package/dist/v2/tui.js +259 -0
- package/docs/assets/cover-motion-still.webp +0 -0
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/cover.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/readme/litfamily-machines.png +0 -0
- package/docs/migration.md +4 -3
- package/docs/reference-Ko-KR.md +109 -7
- package/docs/reference.md +122 -9
- package/package.json +10 -5
- package/skills/deep-interview/SKILL.md +1 -1
- package/skills/lit-brainstorm/SKILL.md +299 -0
- package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
- package/skills/lit-brainstorm/references/lenses.md +106 -0
- package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
- package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
- package/skills/lit-docx/SKILL.md +1 -1
- package/skills/lit-docx/run.py +42 -4
- package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
- package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
- package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
- package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
- package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
- package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
- package/skills/lit-humanizer/scripts/detect.mjs +1 -1
- package/skills/lit-pptx/SKILL.md +1 -1
- package/skills/lit-pptx/command.mjs +94 -0
- package/skills/lit-pptx/run.mjs +16 -6
- package/skills/lit-pptx/scripts/compile-deck.js +2 -2
- package/skills/lit-typographic-motion/SKILL.md +5 -2
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
- package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
- package/skills/lit-typographic-motion/engine/command.mjs +94 -0
- package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
- package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
- package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
- package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
- package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
- package/skills/lit-typographic-motion/engine/look.mjs +113 -10
- package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
- package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
- package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
- package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
- package/skills/lit-typographic-motion/fonts/pins.json +3 -1
- package/skills/lit-typographic-motion/gate.mjs +34 -1
- package/skills/lit-typographic-motion/probe.mjs +8 -3
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
- package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
- package/skills/lit-typographic-motion/references/cover.md +423 -0
- package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
- package/skills/lit-typographic-motion/references/runtime.md +17 -2
- package/skills/lit-typographic-motion/references/stage.md +3 -0
- package/skills/lit-typographic-motion/references/treatment.md +4 -0
- package/skills/lit-typographic-motion/render.mjs +30 -6
- package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
- package/skills/lit-typographic-motion/runtime.mjs +12 -13
- package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
- package/skills/managed-skill-manifest.json +176 -32
- package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/skills/review-work/SKILL.md +27 -18
- package/skills/skill-rename-aliases.json +2 -1
- package/tools/check-pack-payload.mjs +4 -1
- package/tools/payload-substance-allowlist.json +1 -1
- package/tools/payload-substance-parity.json +6 -4
- package/tools/qa-cover-recipes.mjs +167 -0
- package/tools/version-manifests.json +6 -0
package/dist/cli/install.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { CliResult, InstallModelSelection, InstallPermissionMode, InstallProgress, ModelPromptMode, PermissionPromptMode } from "./types.ts";
|
|
2
|
-
|
|
2
|
+
import { type HostChoice } from "./host-detect.ts";
|
|
3
|
+
/**
|
|
4
|
+
* OpenCode 2 merges its own `plugins` key with the OpenCode 1 `plugin` list and refuses a second plugin
|
|
5
|
+
* with the same id, so a LitOpenCode entry under `plugins` is removed when OpenCode 2 is a target. The
|
|
6
|
+
* one registration stays in `plugin`, which both hosts read.
|
|
7
|
+
*/
|
|
8
|
+
export declare function withoutV2PluginEntries(config: Record<string, unknown> | null): Record<string, unknown> | null;
|
|
9
|
+
export declare function assertNoHostOnlyKeysAdded(before: Record<string, unknown> | null, next: Record<string, unknown> | null): void;
|
|
10
|
+
export declare function install(root: string, dryRun: boolean, modelSelection?: InstallModelSelection | undefined, modelPrompt?: ModelPromptMode, permissionMode?: InstallPermissionMode, permissionModeExplicit?: boolean, permissionPrompt?: PermissionPromptMode, progress?: InstallProgress, outputStylePrompt?: PermissionPromptMode, host?: HostChoice): Promise<CliResult>;
|
package/dist/cli/install.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
4
|
import { createRuntimePaths } from "../state.js";
|
|
7
5
|
import { describeCommandAliasMutation, ensureCommandAliases, inspectCommandAliases } from "./command-aliases.js";
|
|
6
|
+
import { execCommand } from "./command-plan.js";
|
|
8
7
|
import { openCodeHostCapabilities } from "./host-capabilities.js";
|
|
9
8
|
import { applyHostLimits, describeHostLimitMutation, ensureHostLimits } from "./host-limits.js";
|
|
10
9
|
import { assertExistingModelRoutesSafe, migrateRetiredEfforts, describeInstallRoutes, describeLitConfigMutation, ensureLitConfig, OpenCodeConfigCommentError, readExistingOutputStyle, readInstallModelClassification, readInstallModelSelection, renderOpenCodeConfig, writeOpenCodeConfig } from "./install-config.js";
|
|
@@ -16,8 +15,8 @@ import { litOpenCodeConfigFileForOpenCodeRoot } from "../state.js";
|
|
|
16
15
|
import { describeNativeSkillMutation, ensureNativeSkills, inspectNativeSkills } from "./native-skills.js";
|
|
17
16
|
import { applyPluginMutation, describePluginMutation, isLitOpenCodeEntry } from "./plugin-mutation.js";
|
|
18
17
|
import { prewarmMotionRuntime } from "./motion-runtime.js";
|
|
19
|
-
import { applyTuiWrites, backupTuiFiles, inspectTuiPlugin, planTuiInstall } from "./tui-config.js";
|
|
20
|
-
|
|
18
|
+
import { applyCliJsonCleanup, applyTuiWrites, backupTuiFiles, inspectTuiPlugin, planCliJsonCleanup, planTuiInstall, skippedTuiPlan } from "./tui-config.js";
|
|
19
|
+
import { confirmUnplacedOpenCode, detectOpenCodeHosts, targetsV1, targetsV2 } from "./host-detect.js";
|
|
21
20
|
function withProgress(progress, label, task) {
|
|
22
21
|
return progress === undefined ? task() : progress.run(label, task);
|
|
23
22
|
}
|
|
@@ -107,9 +106,18 @@ async function openCodeInstallTarget(metadata) {
|
|
|
107
106
|
const spec = pluginSpec(metadata);
|
|
108
107
|
return { value: spec, label: spec };
|
|
109
108
|
}
|
|
110
|
-
async function
|
|
109
|
+
async function openCodeVersion(command) {
|
|
111
110
|
try {
|
|
112
|
-
await
|
|
111
|
+
const { stdout } = await execCommand(command, ["--version"], { timeout: 15_000, maxBuffer: 64 * 1024 });
|
|
112
|
+
return String(stdout);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function installWithOpenCode(target, command = "opencode") {
|
|
119
|
+
try {
|
|
120
|
+
await execCommand(command, ["plugin", target.value, "--global", "--force"], {
|
|
113
121
|
timeout: 120_000,
|
|
114
122
|
maxBuffer: 1024 * 1024 * 4
|
|
115
123
|
});
|
|
@@ -147,14 +155,41 @@ async function normalizeOpenCodeEntries(root, target) {
|
|
|
147
155
|
await writeOpenCodeConfig(configFile, { ...config, plugin: nextPlugin });
|
|
148
156
|
}
|
|
149
157
|
}
|
|
158
|
+
function v2PluginEntryId(value) {
|
|
159
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value))
|
|
160
|
+
return value.package;
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* OpenCode 2 merges its own `plugins` key with the OpenCode 1 `plugin` list and refuses a second plugin
|
|
165
|
+
* with the same id, so a LitOpenCode entry under `plugins` is removed when OpenCode 2 is a target. The
|
|
166
|
+
* one registration stays in `plugin`, which both hosts read.
|
|
167
|
+
*/
|
|
168
|
+
export function withoutV2PluginEntries(config) {
|
|
169
|
+
if (config === null || !Array.isArray(config.plugins))
|
|
170
|
+
return config;
|
|
171
|
+
const kept = config.plugins.filter((entry) => !isLitOpenCodeEntry(v2PluginEntryId(entry)));
|
|
172
|
+
return kept.length === config.plugins.length ? config : { ...config, plugins: kept };
|
|
173
|
+
}
|
|
174
|
+
// OpenCode 1 refuses to start when the shared config holds the OpenCode 2 `permissions` key, and OpenCode 2
|
|
175
|
+
// keeps `policies` there too. LitOpenCode never writes either; this check makes that a hard rule.
|
|
176
|
+
const hostOnlyKeys = ["permissions", "policies"];
|
|
177
|
+
export function assertNoHostOnlyKeysAdded(before, next) {
|
|
178
|
+
for (const key of hostOnlyKeys) {
|
|
179
|
+
if (next !== null && key in next && JSON.stringify(next[key]) !== JSON.stringify(before?.[key])) {
|
|
180
|
+
throw new Error(`Refusing to write the OpenCode 2 \`${key}\` key into the shared config.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
150
184
|
// Works out every OpenCode config edit the install will make before anything is written, and returns
|
|
151
185
|
// the refusal when one of them would delete a user comment. On the default root OpenCode registers the
|
|
152
186
|
// plugin itself; LitOpenCode then only cleans up stale entries and sets host limits.
|
|
153
|
-
async function commentLossRefusal(root, configFile, target, before, mutation, reconfigureHostLimits, useOpenCodeInstaller) {
|
|
187
|
+
async function commentLossRefusal(root, configFile, target, before, mutation, reconfigureHostLimits, useOpenCodeInstaller, stripV2Plugins = false) {
|
|
154
188
|
const planned = [];
|
|
189
|
+
const v2 = (next) => (stripV2Plugins ? withoutV2PluginEntries(next) : next);
|
|
155
190
|
if (!useOpenCodeInstaller) {
|
|
156
191
|
const next = applyPluginMutation(before, mutation, target.value);
|
|
157
|
-
planned.push({ file: configFile, next: reconfigureHostLimits ? applyHostLimits(next) : next });
|
|
192
|
+
planned.push({ file: configFile, next: v2(reconfigureHostLimits ? applyHostLimits(next) : next) });
|
|
158
193
|
}
|
|
159
194
|
else {
|
|
160
195
|
for (const file of openCodeConfigCandidates(root)) {
|
|
@@ -170,7 +205,7 @@ async function commentLossRefusal(root, configFile, target, before, mutation, re
|
|
|
170
205
|
if (JSON.stringify(normalized) !== JSON.stringify(registered))
|
|
171
206
|
next = { ...config, plugin: normalized };
|
|
172
207
|
}
|
|
173
|
-
planned.push({ file, next: file === configFile && reconfigureHostLimits ? applyHostLimits(next) : next });
|
|
208
|
+
planned.push({ file, next: v2(file === configFile && reconfigureHostLimits ? applyHostLimits(next) : next) });
|
|
174
209
|
}
|
|
175
210
|
}
|
|
176
211
|
try {
|
|
@@ -186,7 +221,7 @@ async function commentLossRefusal(root, configFile, target, before, mutation, re
|
|
|
186
221
|
}
|
|
187
222
|
return undefined;
|
|
188
223
|
}
|
|
189
|
-
async function verifyInstalledSurfaces(root, configFile, target, metadata, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle) {
|
|
224
|
+
async function verifyInstalledSurfaces(root, configFile, target, metadata, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle, host = { mode: "v1", binaries: [], evidence: { cliJson: false, tuiJson: false } }) {
|
|
190
225
|
const config = await readJsonObjectIfPresent(configFile);
|
|
191
226
|
const pluginEntries = Array.isArray(config?.plugin) ? config.plugin : [];
|
|
192
227
|
const pluginIds = pluginEntries.map((entry) => typeof entry === "string" ? entry : Array.isArray(entry) && typeof entry[0] === "string" ? entry[0] : "");
|
|
@@ -195,12 +230,20 @@ async function verifyInstalledSurfaces(root, configFile, target, metadata, selec
|
|
|
195
230
|
const litConfig = await describeLitConfigMutation(root, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle);
|
|
196
231
|
const commandAliases = await inspectCommandAliases(root);
|
|
197
232
|
const nativeSkills = await inspectNativeSkills(root, metadata);
|
|
198
|
-
const tui = await inspectTuiPlugin(root, target.value);
|
|
199
233
|
const failures = [];
|
|
200
234
|
if (targetCount !== 1 || staleLitEntries.length > 0)
|
|
201
235
|
failures.push("plugin registration");
|
|
202
|
-
if (
|
|
203
|
-
|
|
236
|
+
if (targetsV1(host.mode)) {
|
|
237
|
+
const tui = await inspectTuiPlugin(root, target.value);
|
|
238
|
+
if (!tui.ok)
|
|
239
|
+
failures.push("TUI plugin registration in " + tui.path);
|
|
240
|
+
}
|
|
241
|
+
if (targetsV2(host.mode)) {
|
|
242
|
+
if (withoutV2PluginEntries(config) !== config)
|
|
243
|
+
failures.push("duplicate LitOpenCode entry under plugins");
|
|
244
|
+
if ((await planCliJsonCleanup(root)).removed > 0)
|
|
245
|
+
failures.push("LitOpenCode entry left in cli.json");
|
|
246
|
+
}
|
|
204
247
|
if (litConfig.changed)
|
|
205
248
|
failures.push("LitOpenCode routes");
|
|
206
249
|
if (!commandAliases.ok)
|
|
@@ -214,7 +257,7 @@ async function verifyInstalledSurfaces(root, configFile, target, metadata, selec
|
|
|
214
257
|
if (failures.length > 0)
|
|
215
258
|
throw new Error("Install verification failed: " + failures.join("; "));
|
|
216
259
|
}
|
|
217
|
-
export async function install(root, dryRun, modelSelection = undefined, modelPrompt = "auto", permissionMode = "safe", permissionModeExplicit = false, permissionPrompt = "auto", progress, outputStylePrompt = "auto") {
|
|
260
|
+
export async function install(root, dryRun, modelSelection = undefined, modelPrompt = "auto", permissionMode = "safe", permissionModeExplicit = false, permissionPrompt = "auto", progress, outputStylePrompt = "auto", host = "auto") {
|
|
218
261
|
const retiredNotice = await migrateRetiredEfforts(root, dryRun);
|
|
219
262
|
if (retiredNotice !== undefined)
|
|
220
263
|
process.stdout.write(retiredNotice + "\n");
|
|
@@ -233,33 +276,66 @@ export async function install(root, dryRun, modelSelection = undefined, modelPro
|
|
|
233
276
|
await activeProgress.start(pluginSpec(metadata));
|
|
234
277
|
}
|
|
235
278
|
const spec = pluginSpec(metadata);
|
|
236
|
-
const
|
|
279
|
+
const located = await withProgress(activeProgress, "Resolve package", async () => {
|
|
237
280
|
const paths = createRuntimePaths(root);
|
|
238
281
|
const useOpenCodeInstaller = shouldUseOpenCodeInstaller(root);
|
|
239
282
|
const target = useOpenCodeInstaller ? await openCodeInstallTarget(metadata) : { value: spec, label: spec };
|
|
240
|
-
|
|
283
|
+
// A custom root says nothing about which host reads it, so only its config files count there.
|
|
284
|
+
const detection = await detectOpenCodeHosts({
|
|
285
|
+
root,
|
|
286
|
+
override: host,
|
|
287
|
+
...(useOpenCodeInstaller ? {} : { resolvePackage: async () => undefined })
|
|
288
|
+
});
|
|
289
|
+
return { paths, useOpenCodeInstaller, target, detection };
|
|
241
290
|
});
|
|
242
|
-
const
|
|
243
|
-
const
|
|
291
|
+
const resolveFor = (detection) => {
|
|
292
|
+
const forV1 = targetsV1(detection.mode);
|
|
293
|
+
const forV2 = targetsV2(detection.mode);
|
|
294
|
+
// `opencode plugin` is the OpenCode 1 command; OpenCode 2 installs the package itself on first load.
|
|
295
|
+
const runInstaller = located.useOpenCodeInstaller && forV1;
|
|
296
|
+
const opencodeIsV2 = [...detection.binaries, ...(detection.unplaced ?? [])].some((found) => found.command === "opencode" && found.major === 2);
|
|
297
|
+
if (runInstaller && detection.v1Command === undefined && opencodeIsV2) {
|
|
298
|
+
throw new Error("Found no OpenCode 1 command: `opencode` is OpenCode 2, and `opencode plugin` would change OpenCode 2's config. Install for OpenCode 2 with --host v2, or put OpenCode 1 on PATH as `opencode`.");
|
|
299
|
+
}
|
|
300
|
+
return { ...located, detection, runInstaller, forV1, forV2 };
|
|
301
|
+
};
|
|
302
|
+
const inspect = async (current) => {
|
|
303
|
+
const configFile = current.useOpenCodeInstaller
|
|
244
304
|
? await openCodeManagedConfigFile(root)
|
|
245
|
-
:
|
|
305
|
+
: current.paths.opencodeConfigFile;
|
|
246
306
|
const before = await readJsonObjectIfPresent(configFile);
|
|
247
307
|
return {
|
|
248
308
|
configFile,
|
|
249
309
|
before,
|
|
250
|
-
mutation: describePluginMutation(before,
|
|
310
|
+
mutation: describePluginMutation(before, current.target.value),
|
|
251
311
|
litopencodeConfig: await describeLitConfigMutation(root, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle),
|
|
252
312
|
commandAliases: await describeCommandAliasMutation(root),
|
|
253
313
|
nativeSkills: await describeNativeSkillMutation(root, metadata),
|
|
254
314
|
hostLimits: describeHostLimitMutation(before, selectedModel !== undefined),
|
|
255
|
-
tui: await planTuiInstall(root,
|
|
315
|
+
tui: current.forV1 ? await planTuiInstall(root, current.target.value) : skippedTuiPlan(root),
|
|
316
|
+
cliJson: current.forV2 ? await planCliJsonCleanup(root) : { path: path.join(path.resolve(root), "cli.json"), removed: 0 },
|
|
256
317
|
routes: dryRun
|
|
257
318
|
? await describeInstallRoutes(root, before, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle)
|
|
258
319
|
: undefined
|
|
259
320
|
};
|
|
260
|
-
}
|
|
261
|
-
const
|
|
262
|
-
|
|
321
|
+
};
|
|
322
|
+
const refusalFor = async (current, plan) => (await commentLossRefusal(root, plan.configFile, current.target, plan.before, plan.mutation, plan.hostLimits.reconfigure, current.runInstaller, current.forV2)) ?? plan.tui.blocked ?? plan.cliJson.blocked;
|
|
323
|
+
let resolved = resolveFor(located.detection);
|
|
324
|
+
let inspected = await withProgress(activeProgress, "Read OpenCode config", async () => inspect(resolved));
|
|
325
|
+
let blocked = await refusalFor(resolved, inspected);
|
|
326
|
+
// A real install that passed every refusal is about to run `opencode plugin`. An `opencode` only guessed
|
|
327
|
+
// to be OpenCode 1 answers `--version` first, so an OpenCode 2 installed outside npm gets the OpenCode 2
|
|
328
|
+
// setup instead. A dry run and a refused install start nothing.
|
|
329
|
+
if (blocked === undefined && !dryRun && resolved.runInstaller) {
|
|
330
|
+
const confirmed = await confirmUnplacedOpenCode(resolved.detection, openCodeVersion);
|
|
331
|
+
if (confirmed !== resolved.detection) {
|
|
332
|
+
resolved = resolveFor(confirmed);
|
|
333
|
+
inspected = await inspect(resolved);
|
|
334
|
+
blocked = await refusalFor(resolved, inspected);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const { configFile, before, mutation, litopencodeConfig, commandAliases, nativeSkills, hostLimits, routes, tui, cliJson } = inspected;
|
|
338
|
+
const v2PluginsChanged = resolved.forV2 && withoutV2PluginEntries(before) !== before;
|
|
263
339
|
const report = {
|
|
264
340
|
dryRun,
|
|
265
341
|
path: configFile,
|
|
@@ -269,12 +345,20 @@ export async function install(root, dryRun, modelSelection = undefined, modelPro
|
|
|
269
345
|
nativeSkills,
|
|
270
346
|
tui: tui.report,
|
|
271
347
|
patch: mutation.patch,
|
|
272
|
-
changed: mutation.changed || litopencodeConfig.changed || commandAliases.changed || nativeSkills.changed || hostLimits.changed || tui.report.changed,
|
|
348
|
+
changed: mutation.changed || litopencodeConfig.changed || commandAliases.changed || nativeSkills.changed || hostLimits.changed || tui.report.changed || cliJson.removed > 0 || v2PluginsChanged,
|
|
273
349
|
...(blocked === undefined ? {} : { blocked }),
|
|
274
350
|
routes,
|
|
275
351
|
hostLimits: { path: configFile, ...hostLimits },
|
|
276
352
|
capabilities: openCodeHostCapabilities(),
|
|
277
|
-
package: { name: metadata.name, version: metadata.version }
|
|
353
|
+
package: { name: metadata.name, version: metadata.version },
|
|
354
|
+
host: {
|
|
355
|
+
mode: resolved.detection.mode,
|
|
356
|
+
binaries: resolved.detection.binaries,
|
|
357
|
+
...(resolved.detection.unplaced === undefined ? {} : { unplaced: resolved.detection.unplaced }),
|
|
358
|
+
evidence: resolved.detection.evidence,
|
|
359
|
+
...(resolved.detection.override === undefined ? {} : { override: resolved.detection.override })
|
|
360
|
+
},
|
|
361
|
+
cliJson: { path: cliJson.path, removed: cliJson.removed, changed: cliJson.removed > 0 }
|
|
278
362
|
};
|
|
279
363
|
if (dryRun) {
|
|
280
364
|
return {
|
|
@@ -295,30 +379,49 @@ export async function install(root, dryRun, modelSelection = undefined, modelPro
|
|
|
295
379
|
await ensureCommandAliases(root);
|
|
296
380
|
});
|
|
297
381
|
await withProgress(activeProgress, "Register plugin", async () => {
|
|
298
|
-
if (resolved.
|
|
382
|
+
if (resolved.runInstaller) {
|
|
299
383
|
// The package's `./tui` export makes `opencode plugin` add the TUI entry itself, so back up
|
|
300
384
|
// first, then normalize whatever it wrote without a second backup.
|
|
301
385
|
await backupTuiFiles(tui);
|
|
302
|
-
|
|
386
|
+
// Both config candidates as they were, so an OpenCode 2 key added to either one is caught.
|
|
387
|
+
const candidates = openCodeConfigCandidates(root);
|
|
388
|
+
const snapshots = new Map(await Promise.all(candidates.map(async (file) => [file, await readJsonObjectIfPresent(file)])));
|
|
389
|
+
await installWithOpenCode(resolved.target, resolved.detection.v1Command ?? "opencode");
|
|
303
390
|
const installed = await readJsonObjectIfPresent(configFile);
|
|
304
391
|
if (!Array.isArray(installed?.plugin) || !installed.plugin.some((entry) => (Array.isArray(entry) ? entry[0] : entry) === resolved.target.value))
|
|
305
392
|
throw new Error("OpenCode did not register the requested package target");
|
|
306
393
|
await normalizeOpenCodeEntries(root, resolved.target);
|
|
394
|
+
if (resolved.forV2) {
|
|
395
|
+
for (const file of candidates) {
|
|
396
|
+
const current = await readJsonObjectIfPresent(file);
|
|
397
|
+
const stripped = withoutV2PluginEntries(current);
|
|
398
|
+
if (stripped === current || stripped === null)
|
|
399
|
+
continue;
|
|
400
|
+
assertNoHostOnlyKeysAdded(snapshots.get(file) ?? null, stripped);
|
|
401
|
+
await writeOpenCodeConfig(file, stripped);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
307
404
|
await ensureHostLimits(configFile, await readJsonObjectIfPresent(configFile), hostLimits.reconfigure);
|
|
308
405
|
const settled = await planTuiInstall(root, resolved.target.value);
|
|
309
406
|
if (settled.blocked !== undefined)
|
|
310
407
|
throw new OpenCodeConfigCommentError(settled.blocked);
|
|
311
408
|
await applyTuiWrites(settled.writes, { backup: false });
|
|
409
|
+
for (const file of candidates)
|
|
410
|
+
assertNoHostOnlyKeysAdded(snapshots.get(file) ?? null, await readJsonObjectIfPresent(file));
|
|
312
411
|
return;
|
|
313
412
|
}
|
|
314
413
|
await applyTuiWrites(tui.writes);
|
|
315
414
|
const withPlugin = applyPluginMutation(before, mutation, resolved.target.value);
|
|
316
|
-
const
|
|
317
|
-
|
|
415
|
+
const limited = hostLimits.reconfigure ? applyHostLimits(withPlugin) : withPlugin;
|
|
416
|
+
const next = resolved.forV2 ? withoutV2PluginEntries(limited) : limited;
|
|
417
|
+
assertNoHostOnlyKeysAdded(before, next);
|
|
418
|
+
if (next === null || JSON.stringify(next) === JSON.stringify(before))
|
|
318
419
|
return;
|
|
319
420
|
await writeOpenCodeConfig(configFile, next);
|
|
320
421
|
});
|
|
321
|
-
|
|
422
|
+
if (resolved.forV2)
|
|
423
|
+
await applyCliJsonCleanup(cliJson);
|
|
424
|
+
await withProgress(activeProgress, "Verify install", async () => verifyInstalledSurfaces(root, configFile, resolved.target, metadata, selectedModel, selectedPermissionMode, selectedPermissionExplicit, selectedOutputStyle, resolved.detection));
|
|
322
425
|
await activeProgress?.complete();
|
|
323
426
|
// MO-A-42: the installer attempts the motion pre-warm; a failure never fails the install.
|
|
324
427
|
const motionReceipt = wrapReceiptLine(prewarmMotionRuntime(metadata.packageRoot));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LitOpenCodeRuntimeSkillId } from "../skills.ts";
|
|
2
|
-
export type ManagedSkillId = "lit-comprehend" | "lit-handoff" | "lit-scientific-visualization" | "lit-diagram-drawer" | "lit-pptx" | "lit-typographic-motion" | "lit-docx" | "readme-studio" | "frontend-ui-ux" | "visual-qa" | "lit-plan" | "browser-drive" | "lit-humanizer" | "autoresearch" | "autoconference" | "wikify";
|
|
2
|
+
export type ManagedSkillId = "lit-brainstorm" | "lit-comprehend" | "lit-handoff" | "lit-scientific-visualization" | "lit-diagram-drawer" | "lit-pptx" | "lit-typographic-motion" | "lit-docx" | "readme-studio" | "frontend-ui-ux" | "visual-qa" | "lit-plan" | "browser-drive" | "lit-humanizer" | "autoresearch" | "autoconference" | "wikify";
|
|
3
3
|
export type CanonicalManagedAsset = {
|
|
4
4
|
readonly path: string;
|
|
5
5
|
readonly sha256: string;
|
|
@@ -6,7 +6,8 @@ if (managedSkillManifest.schemaVersion !== 1) {
|
|
|
6
6
|
throw new Error(`Unsupported managed-skill manifest schema: ${String(managedSkillManifest.schemaVersion)}`);
|
|
7
7
|
}
|
|
8
8
|
export function managedSkillDefinition(id) {
|
|
9
|
-
if (id !== "lit-
|
|
9
|
+
if (id !== "lit-brainstorm" &&
|
|
10
|
+
id !== "lit-comprehend" &&
|
|
10
11
|
id !== "lit-handoff" &&
|
|
11
12
|
id !== "lit-scientific-visualization" &&
|
|
12
13
|
id !== "lit-diagram-drawer" &&
|
|
@@ -79,7 +80,7 @@ export function isRecursivelyManagedSkill(id) {
|
|
|
79
80
|
// id without writing its trigger is a compile error, not a silent fallback.
|
|
80
81
|
const skillDiscoveryDescriptions = Object.freeze({
|
|
81
82
|
"workflow-loop": "Trigger when the user wants a multi-step LitOpenCode work loop started, resumed, or kept running — bare `lit`, `/lit`, `/litwork`, \"keep going\", \"continue the loop\" — or when a request needs goal binding, ordered execution, and ledger-backed progress across turns instead of one answer. Also trigger when one request hands over a whole feature, fix, or ticket to carry through to done — build it, add tests, verify against each acceptance condition and on the real user-facing path, clean up temporary artifacts, then report what changed (\"start to finish\", \"end to end\", \"see it through\", \"처음부터 끝까지\", \"끝까지 해줘\"). Do not trigger for a question, an explanation, or a one-line snippet.",
|
|
82
|
-
|
|
83
|
+
litgoal: "Trigger when work needs a finish line that outlives the current turn — litgoal sets and keeps it, lit-loop and start-work run toward it: binding a goal with checkable criteria, reading or appending the `.litopencode/litgoal` ledger, resuming a schema-3 bounded-authority work record, or answering \"what was I working on\" from durable state rather than chat memory.",
|
|
83
84
|
"agent-roster": "Trigger when deciding which OpenCode agent should do the work, and again the moment work is about to be split across several lanes at once — before delegating through the task tool, when a request fans out into planning, implementation, review, research, or verification lanes, or when the user asks which agents exist and what each is permitted to touch. Delegation here is depth-one, so lanes need non-overlapping slices, explicit per-lane state, and a completion claim that aggregates every lane's evidence rather than summarising it.",
|
|
84
85
|
"lit-plan": "Trigger when the user asks for a plan, checklist, approach, or design before implementation, or when a request is large or risky enough that execution must not start until scope, non-goals, ordered action/output/verification items, and a falsifiable DoneClaim are agreed. Do not trigger to perform the work itself.",
|
|
85
86
|
"start-work": "Trigger when an approved plan or an explicit go-ahead already exists and implementation should begin or resume — \"start work\", \"execute the plan\", \"pick up where we left off\" — so the loop runs in verifiable slices with delegation lanes, evidence, and an independent verifier before any completion claim.",
|
|
@@ -92,6 +93,7 @@ const skillDiscoveryDescriptions = Object.freeze({
|
|
|
92
93
|
"lit-fetch": "Trigger when a public HTTP(S) page, README, raw file, or API response must actually be fetched, so retrieval runs through the SSRF guards and returns a typed verdict, and so authentication, paywall, rate-limit, consent, and challenge boundaries become stop conditions instead of improvised workarounds.",
|
|
93
94
|
"release-guardrails": "Trigger before any release-shaped action — version bump, tag, publish, changelog, \"is this ready to ship\" — so vocabulary scanning, version lockstep, and a real dry-pack manifest run before the claim is made and publication stays behind explicit user approval.",
|
|
94
95
|
"lit-init": "Trigger when a repository needs agent-facing guidance created or refreshed: \"set up AGENTS.md\", \"document this repo for agents\", onboarding an unfamiliar codebase, or a restructure that left directory guidance stale and misleading.",
|
|
96
|
+
"lit-brainstorm": "Trigger when the question is still open and the user wants options before a decision — \"what could we call it\", \"any ideas for\", \"how else could we do this\", \"brainstorm\", \"ideation\", `lit idea`, `lit brainstorm` — so many different options are generated, clustered, scored against stated criteria, and narrowed to a short list with trade-offs before one hand-off to deep-interview or lit-plan. Do not trigger to plan a chosen thing, to interview toward one spec, or to implement.",
|
|
95
97
|
"lit-crucible": "Trigger before planning when the approach itself is uncertain, contested, or expensive to reverse — competing designs, hidden coupling, a request to pressure-test assumptions or find what will go wrong — so independent read-only lanes attack the idea and only surviving evidence-backed insights reach lit-plan.",
|
|
96
98
|
refactor: "Trigger when code should change shape without changing behavior: extracting, renaming, deduplicating, splitting an overgrown module, or paying down structure the user calls messy or hard to follow — so the behavior boundary is pinned by before/after checks and feature work is not smuggled into the diff.",
|
|
97
99
|
"lit-burnoff": "Trigger when text or code reads as machine-written and should not — comments restating the code, hedging filler, decorative headings, invented emphasis — or when the user asks to \"clean this up\", \"make it less AI\", or strip generated padding, while facts, APIs, tests, and accessibility behavior stay intact.",
|
|
@@ -113,7 +115,7 @@ const skillDiscoveryDescriptions = Object.freeze({
|
|
|
113
115
|
"lit-scientific-visualization": "Trigger only for explicit `/lit-scientific-visualization`, an exact bare `lit-scientific-visualization` message, or native selection by the exact `lit-scientific-visualization` id.",
|
|
114
116
|
"lit-diagram-drawer": "Select this skill from OpenCode's native skill picker when the user needs an architecture map, workflow, schema, decision path, schedule, or conceptual chart. Route product pages and interface layouts to frontend-ui-ux; route measured scientific data and statistical figures to lit-scientific-visualization. Do not claim a slash command or bare-chat route.",
|
|
115
117
|
"lit-pptx": "Select for a PowerPoint deck or presentation, including a bounded lit request for slides. Keep editable Markdown and PPTX together, run the QA gate, and inspect the render.",
|
|
116
|
-
"lit-typographic-motion": "Select for a new film or video. Write the treatment first, then render on the stage path (a captured page) or the type path (words alone), gate it and look at the frames;
|
|
118
|
+
"lit-typographic-motion": "Select for a new film or video. Write the treatment first, then render on the stage path (a captured page) or the type path (words alone), gate it and look at the frames; a README cover loop takes cover mode. Editing existing footage or embedding a video in a page is outside this route.",
|
|
117
119
|
"lit-docx": "Select for a Word report, proposal, or document, including a bounded lit report request. Keep editable Markdown and DOCX together; select publisher profiles when specified and inspect rendered pages.",
|
|
118
120
|
"readme-studio": "Select by native skill name for factual README writing and local cover composition. Inspect actual session image capability; never infer generation support or authorize publication from selection.",
|
|
119
121
|
"frontend-ui-ux": "Trigger the moment interface work is described, not after it starts — \"design a settings page\", \"redesign the dashboard\", \"build a login screen\", or any making verb aimed at a UI, screen, layout, component, or stylesheet — so compact direction and existing tokens shape the authorized build. Evolve the contract with implementation and inspect renders; review and plan intent remains read-only. Also on the exact `frontend-ui-ux` id. The SKILL.md reference router names which of the sixteen shipped reference documents answers which question.",
|
|
@@ -3,21 +3,25 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { findCommandOnPath } from "./host-detect.js";
|
|
6
7
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
7
8
|
const cacheRoot = path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"), "litopencode", "office");
|
|
8
9
|
function cached(label, lock, marker) {
|
|
9
10
|
const digest = createHash("sha256").update(readFileSync(path.join(packageRoot, lock))).digest("hex").slice(0, 16);
|
|
10
11
|
return existsSync(path.join(cacheRoot, `${label}-${digest}`, marker));
|
|
11
12
|
}
|
|
13
|
+
/** A venv keeps its interpreter in `Scripts` on Windows and in `bin` elsewhere. */
|
|
14
|
+
export const venvPythonMarker = (platform = process.platform) => (platform === "win32" ? "Scripts/python.exe" : "bin/python");
|
|
12
15
|
export function inspectOfficeDependencies() {
|
|
13
|
-
const onPath = (name) => (
|
|
16
|
+
const onPath = (name) => findCommandOnPath(name) !== undefined;
|
|
17
|
+
const python = venvPythonMarker();
|
|
14
18
|
return {
|
|
15
19
|
pptx: {
|
|
16
20
|
nodeReady: cached("node", "skills/lit-pptx/package-lock.json", "node_modules/pptxgenjs"),
|
|
17
|
-
pythonReady: cached("python-pptx", "skills/lit-pptx/requirements.lock",
|
|
21
|
+
pythonReady: cached("python-pptx", "skills/lit-pptx/requirements.lock", python)
|
|
18
22
|
},
|
|
19
23
|
docx: {
|
|
20
|
-
pythonReady: cached("python-docx", "skills/lit-docx/requirements.lock",
|
|
24
|
+
pythonReady: cached("python-docx", "skills/lit-docx/requirements.lock", python)
|
|
21
25
|
},
|
|
22
26
|
optional: { pandoc: onPath("pandoc"), xelatex: onPath("xelatex"), soffice: onPath("soffice") }
|
|
23
27
|
};
|
|
@@ -15,7 +15,7 @@ const optionalModules = Object.freeze([
|
|
|
15
15
|
]);
|
|
16
16
|
function selectedInterpreter() {
|
|
17
17
|
const explicit = process.env.LITOPENCODE_SCIENTIFIC_PYTHON?.trim();
|
|
18
|
-
return explicit && explicit.length > 0 ? explicit : "python3";
|
|
18
|
+
return explicit && explicit.length > 0 ? explicit : process.platform === "win32" ? "python" : "python3";
|
|
19
19
|
}
|
|
20
20
|
function unavailableReport(command) {
|
|
21
21
|
return {
|
package/dist/cli/tui-config.d.ts
CHANGED
|
@@ -37,8 +37,6 @@ export declare function applyTuiWrites(writes: readonly PlannedWrite[], options?
|
|
|
37
37
|
}): Promise<void>;
|
|
38
38
|
/** Backs up the TUI files a plan is about to change, for writers outside LitOpenCode such as `opencode plugin`. */
|
|
39
39
|
export declare function backupTuiFiles(plan: TuiInstallPlan): Promise<void>;
|
|
40
|
-
/** Removes LitOpenCode's own TUI entries, and only those, from tui.json and tui.jsonc. */
|
|
41
|
-
export declare function removeTuiPlugin(root: string, dryRun: boolean): Promise<TuiRemovalReport>;
|
|
42
40
|
export type TuiInspection = {
|
|
43
41
|
readonly path: string;
|
|
44
42
|
readonly expected: string;
|
|
@@ -49,4 +47,31 @@ export type TuiInspection = {
|
|
|
49
47
|
readonly error?: string;
|
|
50
48
|
};
|
|
51
49
|
export declare function inspectTuiPlugin(root: string, expected: string): Promise<TuiInspection>;
|
|
50
|
+
export declare const cliConfigFile = "cli.json";
|
|
51
|
+
export type CliJsonCleanupPlan = {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly removed: number;
|
|
54
|
+
readonly write?: PlannedWrite;
|
|
55
|
+
readonly blocked?: string;
|
|
56
|
+
};
|
|
57
|
+
/** Plans the removal of LitOpenCode's own entries from the `plugins` list of cli.json, without writing. */
|
|
58
|
+
export declare function planCliJsonCleanup(root: string): Promise<CliJsonCleanupPlan>;
|
|
59
|
+
/** Writes a cli.json cleanup plan, backing the file up first. Returns the backup path. */
|
|
60
|
+
export declare function applyCliJsonCleanup(plan: CliJsonCleanupPlan): Promise<string | undefined>;
|
|
61
|
+
/** The TUI part of an install that targets OpenCode 2 only: nothing to write in tui.json. */
|
|
62
|
+
export declare function skippedTuiPlan(root: string): TuiInstallPlan;
|
|
63
|
+
/**
|
|
64
|
+
* `tui remove`: takes LitOpenCode's own entries out of tui.json/tui.jsonc and, unless only OpenCode 1 is
|
|
65
|
+
* targeted, out of cli.json, each with a backup. Both files are read and planned first, so a malformed or
|
|
66
|
+
* comment-losing file refuses before anything is written. A dry run only counts.
|
|
67
|
+
*/
|
|
68
|
+
export declare function removeTuiEntries(root: string, dryRun: boolean, options: {
|
|
69
|
+
readonly cliJson: boolean;
|
|
70
|
+
}): Promise<TuiRemovalReport & {
|
|
71
|
+
readonly cliJson?: {
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly removed: number;
|
|
74
|
+
readonly backup?: string;
|
|
75
|
+
};
|
|
76
|
+
}>;
|
|
52
77
|
export {};
|
package/dist/cli/tui-config.js
CHANGED
|
@@ -132,8 +132,8 @@ export async function backupTuiFiles(plan) {
|
|
|
132
132
|
for (const write of plan.writes)
|
|
133
133
|
await backup(write);
|
|
134
134
|
}
|
|
135
|
-
/**
|
|
136
|
-
|
|
135
|
+
/** Plans the removal of LitOpenCode's own TUI entries, and only those, from tui.json and tui.jsonc, without writing. */
|
|
136
|
+
async function planTuiRemoval(root) {
|
|
137
137
|
const files = await Promise.all(tuiConfigCandidates(root).map(readTuiFile));
|
|
138
138
|
const writes = [];
|
|
139
139
|
for (const file of files) {
|
|
@@ -146,6 +146,9 @@ export async function removeTuiPlugin(root, dryRun) {
|
|
|
146
146
|
const kept = plugins.filter((_value, index) => !owned.includes(index));
|
|
147
147
|
writes.push({ write: render(file, { ...file.config, plugin: kept }), removed: owned.length });
|
|
148
148
|
}
|
|
149
|
+
return writes;
|
|
150
|
+
}
|
|
151
|
+
async function applyTuiRemoval(writes, dryRun) {
|
|
149
152
|
const reports = [];
|
|
150
153
|
for (const { write, removed } of writes) {
|
|
151
154
|
if (dryRun) {
|
|
@@ -196,3 +199,75 @@ export async function inspectTuiPlugin(root, expected) {
|
|
|
196
199
|
ok: present && entries === 1
|
|
197
200
|
};
|
|
198
201
|
}
|
|
202
|
+
// OpenCode 2 keeps its terminal settings in one global cli.json, and its first TUI start copies the
|
|
203
|
+
// tui.json plugin list there. A LitOpenCode entry in cli.json is loaded after the TUI the server plugin
|
|
204
|
+
// brings and wins by id, so a stale pin would shadow the current TUI. OpenCode 2 loads LitOpenCode's TUI
|
|
205
|
+
// from the server plugin on its own; the installer and `tui remove` therefore take LitOpenCode out of
|
|
206
|
+
// cli.json, keeping every other entry, the comments and a backup.
|
|
207
|
+
export const cliConfigFile = "cli.json";
|
|
208
|
+
function cliEntryId(value) {
|
|
209
|
+
if (typeof value === "string")
|
|
210
|
+
return value;
|
|
211
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
212
|
+
const pkg = value.package;
|
|
213
|
+
return typeof pkg === "string" ? pkg : "";
|
|
214
|
+
}
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
/** Plans the removal of LitOpenCode's own entries from the `plugins` list of cli.json, without writing. */
|
|
218
|
+
export async function planCliJsonCleanup(root) {
|
|
219
|
+
const file = path.join(path.resolve(root), cliConfigFile);
|
|
220
|
+
const current = await readTuiFile(file);
|
|
221
|
+
const plugins = Array.isArray(current.config?.plugins) ? current.config.plugins : [];
|
|
222
|
+
const kept = [];
|
|
223
|
+
for (const value of plugins) {
|
|
224
|
+
const id = cliEntryId(value);
|
|
225
|
+
if (id !== "" && (isLitOpenCodeEntry(id) || (await isLitOpenCodePathEntry(root, id))))
|
|
226
|
+
continue;
|
|
227
|
+
kept.push(value);
|
|
228
|
+
}
|
|
229
|
+
const removed = plugins.length - kept.length;
|
|
230
|
+
if (removed === 0 || current.config === null)
|
|
231
|
+
return { path: file, removed: 0 };
|
|
232
|
+
try {
|
|
233
|
+
return { path: file, removed, write: render(current, { ...current.config, plugins: kept }) };
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!(error instanceof OpenCodeConfigCommentError))
|
|
237
|
+
throw error;
|
|
238
|
+
return { path: file, removed, blocked: error.message };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Writes a cli.json cleanup plan, backing the file up first. Returns the backup path. */
|
|
242
|
+
export async function applyCliJsonCleanup(plan) {
|
|
243
|
+
if (plan.write === undefined)
|
|
244
|
+
return undefined;
|
|
245
|
+
const backupPath = await backup(plan.write);
|
|
246
|
+
await writeTextAtomically(plan.write.path, plan.write.text);
|
|
247
|
+
return backupPath;
|
|
248
|
+
}
|
|
249
|
+
/** The TUI part of an install that targets OpenCode 2 only: nothing to write in tui.json. */
|
|
250
|
+
export function skippedTuiPlan(root) {
|
|
251
|
+
return {
|
|
252
|
+
report: { path: tuiConfigCandidates(root)[0], alreadyPresent: false, changed: false, add: [], currentCount: 0, resultCount: 0, cleaned: [] },
|
|
253
|
+
writes: []
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* `tui remove`: takes LitOpenCode's own entries out of tui.json/tui.jsonc and, unless only OpenCode 1 is
|
|
258
|
+
* targeted, out of cli.json, each with a backup. Both files are read and planned first, so a malformed or
|
|
259
|
+
* comment-losing file refuses before anything is written. A dry run only counts.
|
|
260
|
+
*/
|
|
261
|
+
export async function removeTuiEntries(root, dryRun, options) {
|
|
262
|
+
const tuiPlan = await planTuiRemoval(root);
|
|
263
|
+
const cliPlan = options.cliJson ? await planCliJsonCleanup(root) : undefined;
|
|
264
|
+
if (cliPlan?.blocked !== undefined)
|
|
265
|
+
throw new OpenCodeConfigCommentError(cliPlan.blocked);
|
|
266
|
+
const tui = await applyTuiRemoval(tuiPlan, dryRun);
|
|
267
|
+
if (cliPlan === undefined)
|
|
268
|
+
return tui;
|
|
269
|
+
if (dryRun || cliPlan.write === undefined)
|
|
270
|
+
return { ...tui, cliJson: { path: cliPlan.path, removed: cliPlan.removed } };
|
|
271
|
+
const backupPath = await applyCliJsonCleanup(cliPlan);
|
|
272
|
+
return { ...tui, cliJson: { path: cliPlan.path, removed: cliPlan.removed, ...(backupPath === undefined ? {} : { backup: backupPath }) } };
|
|
273
|
+
}
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type PackageMetadata = {
|
|
|
10
10
|
readonly version: string;
|
|
11
11
|
readonly packageRoot: string;
|
|
12
12
|
};
|
|
13
|
+
import type { HostBinary, HostChoice, HostDetection, HostMode } from "./host-detect.ts";
|
|
13
14
|
export type ParsedArgs = {
|
|
14
15
|
readonly command?: string;
|
|
15
16
|
readonly root: string;
|
|
@@ -27,6 +28,7 @@ export type ParsedArgs = {
|
|
|
27
28
|
readonly outputStylePrompt: PermissionPromptMode;
|
|
28
29
|
readonly loop: LoopArgs;
|
|
29
30
|
readonly tuiAction?: string;
|
|
31
|
+
readonly host: HostChoice;
|
|
30
32
|
};
|
|
31
33
|
export type LoopArgs = {
|
|
32
34
|
readonly projectRoot: string;
|
|
@@ -190,4 +192,19 @@ export type InstallReport = {
|
|
|
190
192
|
readonly routes?: InstallRouteReport;
|
|
191
193
|
readonly capabilities: OpenCodeHostCapabilities;
|
|
192
194
|
readonly package: Pick<PackageMetadata, "name" | "version">;
|
|
195
|
+
readonly host?: {
|
|
196
|
+
readonly mode: HostMode;
|
|
197
|
+
readonly binaries: readonly HostBinary[];
|
|
198
|
+
readonly unplaced?: HostDetection["unplaced"];
|
|
199
|
+
readonly evidence: {
|
|
200
|
+
readonly cliJson: boolean;
|
|
201
|
+
readonly tuiJson: boolean;
|
|
202
|
+
};
|
|
203
|
+
readonly override?: string;
|
|
204
|
+
};
|
|
205
|
+
readonly cliJson?: {
|
|
206
|
+
readonly path: string;
|
|
207
|
+
readonly removed: number;
|
|
208
|
+
readonly changed: boolean;
|
|
209
|
+
};
|
|
193
210
|
};
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { defaultOpenAiRoutes, legacyFastAliasHelp, modelEffortHelp, openaiModelH
|
|
|
10
10
|
import { fetchPublicSource } from "./lit-fetch.js";
|
|
11
11
|
import { readPackageMetadata } from "./cli/json.js";
|
|
12
12
|
import { runMotionRuntime } from "./cli/motion-runtime.js";
|
|
13
|
-
import {
|
|
13
|
+
import { removeTuiEntries } from "./cli/tui-config.js";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
function helpText() {
|
|
@@ -86,7 +86,8 @@ function helpText() {
|
|
|
86
86
|
" ships a per-channel capture playbook, and keeps blocked codes separate from fail codes.",
|
|
87
87
|
" lit-diagram-drawer creates conceptual, editorial, and technical diagrams with packaged type guides,",
|
|
88
88
|
" local verifiers, bounded importers, and exports that never install tools.",
|
|
89
|
-
" lit-typographic-motion directs films from a treatment: a captured stage page or the type engine
|
|
89
|
+
" lit-typographic-motion directs films from a treatment: a captured stage page or the type engine,",
|
|
90
|
+
" and seamless README cover loops gated on the decoded WebP.",
|
|
90
91
|
" Typographic-motion engine adapted from mexicat/pdoom-video (MIT, Giacomo Magnanini), commit `ca251e3`.",
|
|
91
92
|
" Each skill resolves its own helpers inside its own installed tree; these skills add no command,",
|
|
92
93
|
" MCP, agent, authentication, or write authority.",
|
|
@@ -138,9 +139,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
138
139
|
if (isLoopCommand(parsed.command))
|
|
139
140
|
return await runLoopCommand(parsed.command, parsed.loop, parsed.dryRun);
|
|
140
141
|
if (parsed.command === "doctor")
|
|
141
|
-
return await doctor(parsed.root);
|
|
142
|
-
if (parsed.command === "tui")
|
|
143
|
-
|
|
142
|
+
return await doctor(parsed.root, parsed.host);
|
|
143
|
+
if (parsed.command === "tui") {
|
|
144
|
+
const removed = await removeTuiEntries(parsed.root, parsed.dryRun, { cliJson: parsed.host !== "v1" });
|
|
145
|
+
return { exitCode: 0, stdout: JSON.stringify(removed, null, 2) };
|
|
146
|
+
}
|
|
144
147
|
if (parsed.command === "fetch-public") {
|
|
145
148
|
if (!parsed.fetchPublicUrl)
|
|
146
149
|
return { exitCode: 2, stderr: "fetch-public requires a URL" };
|
|
@@ -154,7 +157,7 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
154
157
|
return { exitCode: output.ok ? 0 : 1, stdout: renderFetchPublicText(output) };
|
|
155
158
|
}
|
|
156
159
|
if (parsed.command === "install") {
|
|
157
|
-
return await install(parsed.root, parsed.dryRun, parsed.modelSelection, parsed.modelPrompt, parsed.permissionMode, parsed.permissionModeExplicit, parsed.permissionPrompt, options.installProgress, parsed.outputStylePrompt);
|
|
160
|
+
return await install(parsed.root, parsed.dryRun, parsed.modelSelection, parsed.modelPrompt, parsed.permissionMode, parsed.permissionModeExplicit, parsed.permissionPrompt, options.installProgress, parsed.outputStylePrompt, parsed.host);
|
|
158
161
|
}
|
|
159
162
|
return { exitCode: 2, stderr: `Unknown command: ${parsed.command}` };
|
|
160
163
|
}
|