opencode-gemiterm-skills 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import { uninstall, type Scope } from "../installer.ts";
2
+
3
+ interface UninstallOptions {
4
+ scope?: Scope;
5
+ force?: boolean;
6
+ }
7
+
8
+ export async function uninstallCommand(options: UninstallOptions = {}): Promise<void> {
9
+ const scope = options.scope ?? "local";
10
+ const result = await uninstall(scope);
11
+
12
+ if (result.removed.length === 0) {
13
+ console.log("opencode-gemiterm-skills is not installed.");
14
+ return;
15
+ }
16
+
17
+ console.log(`opencode-gemiterm-skills uninstalled ${scope === "global" ? "globally" : "locally"}:`);
18
+ for (const p of result.removed) {
19
+ console.log(` Removed: ${p}`);
20
+ }
21
+ }
@@ -0,0 +1,231 @@
1
+ import { exists, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ export type Scope = "local" | "global";
7
+
8
+ export interface InstallResult {
9
+ scope: Scope;
10
+ skillPaths: string[];
11
+ configPath: string;
12
+ migrated: boolean;
13
+ permissionConfigured: boolean;
14
+ pluginAdded: boolean;
15
+ }
16
+
17
+ export interface UninstallResult {
18
+ scope: Scope;
19
+ removed: string[];
20
+ pluginRemoved: boolean;
21
+ }
22
+
23
+ export interface StatusResult {
24
+ installed: boolean;
25
+ version: string | null;
26
+ scope: Scope | null;
27
+ pluginInConfig: boolean;
28
+ }
29
+
30
+ const SKILL_NAMES = ["gemiterm", "debate-with-gemini"] as const;
31
+ const PACKAGE_NAME = "opencode-gemiterm-skills";
32
+
33
+ function getPackageDir(): string {
34
+ return join(fileURLToPath(new URL("../", import.meta.url)));
35
+ }
36
+
37
+ export async function getPackageVersion(): Promise<string> {
38
+ const content = await Bun.file(join(getPackageDir(), "package.json")).text();
39
+ return JSON.parse(content).version;
40
+ }
41
+
42
+ export function getGlobalConfigPath(): string {
43
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
44
+ if (xdgConfig) {
45
+ return join(xdgConfig, "opencode");
46
+ }
47
+ return join(homedir(), ".config", "opencode");
48
+ }
49
+
50
+ export function getLocalConfigPath(projectDir: string): string {
51
+ return join(projectDir, ".opencode");
52
+ }
53
+
54
+ async function copyDir(src: string, dest: string): Promise<void> {
55
+ await mkdir(dest, { recursive: true });
56
+ for (const entry of await readdir(src, { withFileTypes: true })) {
57
+ const s = join(src, entry.name);
58
+ const d = join(dest, entry.name);
59
+ if (entry.isDirectory()) {
60
+ await copyDir(s, d);
61
+ } else {
62
+ await Bun.write(d, Bun.file(s));
63
+ }
64
+ }
65
+ }
66
+
67
+ async function readJsonConfig(path: string): Promise<Record<string, unknown>> {
68
+ try {
69
+ return JSON.parse(await readFile(path, "utf-8"));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ async function writeJsonConfig(path: string, config: Record<string, unknown>): Promise<void> {
76
+ await mkdir(join(path, ".."), { recursive: true });
77
+ await writeFile(path, JSON.stringify(config, null, 2));
78
+ }
79
+
80
+ async function ensureSkillPermissions(configPath: string, skillNames: readonly string[]): Promise<boolean> {
81
+ const config = await readJsonConfig(configPath);
82
+ if (!config.permission) config.permission = {};
83
+ if (!(config.permission as Record<string, unknown>).skill) (config.permission as Record<string, unknown>).skill = {};
84
+ const skillPerms = (config.permission as Record<string, unknown>).skill as Record<string, unknown>;
85
+ let changed = false;
86
+ for (const name of skillNames) {
87
+ if (skillPerms[name] !== "allow") {
88
+ skillPerms[name] = "allow";
89
+ changed = true;
90
+ }
91
+ }
92
+ if (changed) {
93
+ await mkdir(join(configPath, ".."), { recursive: true });
94
+ await writeFile(configPath, JSON.stringify(config, null, 2));
95
+ }
96
+ return changed;
97
+ }
98
+
99
+ async function addPluginToConfig(configPath: string): Promise<boolean> {
100
+ const config = await readJsonConfig(configPath);
101
+ if (!config.plugin) config.plugin = [];
102
+ const plugins = config.plugin as string[];
103
+ if (plugins.includes(PACKAGE_NAME)) return false;
104
+ plugins.push(PACKAGE_NAME);
105
+ await mkdir(join(configPath, ".."), { recursive: true });
106
+ await writeFile(configPath, JSON.stringify(config, null, 2));
107
+ return true;
108
+ }
109
+
110
+ async function removePluginFromConfig(configPath: string): Promise<boolean> {
111
+ const config = await readJsonConfig(configPath);
112
+ if (!config.plugin) return false;
113
+ const plugins = config.plugin as string[];
114
+ const idx = plugins.indexOf(PACKAGE_NAME);
115
+ if (idx === -1) return false;
116
+ plugins.splice(idx, 1);
117
+ if (plugins.length === 0) delete config.plugin;
118
+ await mkdir(join(configPath, ".."), { recursive: true });
119
+ await writeFile(configPath, JSON.stringify(config, null, 2));
120
+ return true;
121
+ }
122
+
123
+ async function isPluginInConfig(configPath: string): Promise<boolean> {
124
+ const config = await readJsonConfig(configPath);
125
+ if (!config.plugin) return false;
126
+ return (config.plugin as string[]).includes(PACKAGE_NAME);
127
+ }
128
+
129
+ async function checkMigrationNeeded(projectDir: string) {
130
+ const rootConfigPath = join(projectDir, "opencode.json");
131
+ const dotOpencodeConfigPath = join(projectDir, ".opencode", "opencode.json");
132
+ const rootExists = await exists(rootConfigPath);
133
+ if (!rootExists) return { needed: false, rootConfigPath, dotOpencodeConfigPath };
134
+ return { needed: true, rootConfigPath, dotOpencodeConfigPath };
135
+ }
136
+
137
+ async function migrateRootConfig(projectDir: string): Promise<boolean> {
138
+ const { needed, rootConfigPath, dotOpencodeConfigPath } = await checkMigrationNeeded(projectDir);
139
+ if (!needed) return false;
140
+ const rootConfig = await readJsonConfig(rootConfigPath);
141
+ const dotOpencodeExists = await exists(dotOpencodeConfigPath);
142
+ if (dotOpencodeExists) {
143
+ const dotConfig = await readJsonConfig(dotOpencodeConfigPath);
144
+ const merged = { ...rootConfig, ...dotConfig };
145
+ await writeJsonConfig(dotOpencodeConfigPath, merged);
146
+ } else {
147
+ await mkdir(join(projectDir, ".opencode"), { recursive: true });
148
+ await writeJsonConfig(dotOpencodeConfigPath, rootConfig);
149
+ }
150
+ await rm(rootConfigPath);
151
+ return true;
152
+ }
153
+
154
+ export async function install(
155
+ scope: Scope,
156
+ projectDir: string = process.cwd(),
157
+ ): Promise<InstallResult> {
158
+ const version = await getPackageVersion();
159
+ const pkgDir = getPackageDir();
160
+
161
+ const configBase =
162
+ scope === "global" ? getGlobalConfigPath() : getLocalConfigPath(projectDir);
163
+
164
+ const skillPaths: string[] = [];
165
+
166
+ let migrated = false;
167
+ if (scope === "local") {
168
+ migrated = await migrateRootConfig(projectDir);
169
+ }
170
+
171
+ for (const name of SKILL_NAMES) {
172
+ const srcSkillDir = join(pkgDir, "assets", "skills", name);
173
+ const destSkillDir = join(configBase, "skills", name);
174
+ await copyDir(srcSkillDir, destSkillDir);
175
+ skillPaths.push(destSkillDir);
176
+ await Bun.write(join(destSkillDir, ".version"), version);
177
+ }
178
+
179
+ const configPath = join(configBase, "opencode.json");
180
+ const permissionConfigured = await ensureSkillPermissions(configPath, SKILL_NAMES);
181
+ const pluginAdded = await addPluginToConfig(configPath);
182
+
183
+ return { scope, skillPaths, configPath, migrated, permissionConfigured, pluginAdded };
184
+ }
185
+
186
+ export async function uninstall(
187
+ scope: Scope,
188
+ projectDir: string = process.cwd(),
189
+ ): Promise<UninstallResult> {
190
+ const configBase =
191
+ scope === "global" ? getGlobalConfigPath() : getLocalConfigPath(projectDir);
192
+
193
+ const removed: string[] = [];
194
+ for (const name of SKILL_NAMES) {
195
+ const skillPath = join(configBase, "skills", name);
196
+ if (await exists(skillPath)) {
197
+ await rm(skillPath, { recursive: true });
198
+ removed.push(skillPath);
199
+ }
200
+ }
201
+
202
+ const configPath = join(configBase, "opencode.json");
203
+ const pluginRemoved = await removePluginFromConfig(configPath);
204
+
205
+ return { scope, removed, pluginRemoved };
206
+ }
207
+
208
+ export async function status(projectDir: string = process.cwd()): Promise<StatusResult> {
209
+ const version = await getPackageVersion();
210
+
211
+ for (const scope of ["local", "global"] as Scope[]) {
212
+ const configBase =
213
+ scope === "global" ? getGlobalConfigPath() : getLocalConfigPath(projectDir);
214
+ const versionMarker = join(configBase, "skills", SKILL_NAMES[0], ".version");
215
+ const configPath = join(configBase, "opencode.json");
216
+
217
+ try {
218
+ const installedVersion = (await readFile(versionMarker, "utf-8")).trim();
219
+ const pluginInConfig = await isPluginInConfig(configPath);
220
+ return { installed: true, version: installedVersion, scope, pluginInConfig };
221
+ } catch {
222
+ const firstSkillPath = join(configBase, "skills", SKILL_NAMES[0]);
223
+ if (await exists(firstSkillPath)) {
224
+ const pluginInConfig = await isPluginInConfig(configPath);
225
+ return { installed: true, version: null, scope, pluginInConfig };
226
+ }
227
+ }
228
+ }
229
+
230
+ return { installed: false, version: null, scope: null, pluginInConfig: false };
231
+ }