opencodekit 0.23.0 → 0.23.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.
Files changed (23) hide show
  1. package/dist/index.js +354 -825
  2. package/dist/template/.opencode/AGENTS.md +15 -0
  3. package/dist/template/.opencode/command/init.md +198 -34
  4. package/dist/template/.opencode/context/fallow.md +137 -0
  5. package/dist/template/.opencode/dcp-prompts/overrides/compress-range.md +89 -0
  6. package/dist/template/.opencode/opencode.json +110 -315
  7. package/dist/template/.opencode/plugin/README.md +10 -0
  8. package/dist/template/.opencode/plugin/memory/compile.ts +171 -186
  9. package/dist/template/.opencode/plugin/memory/index-generator.ts +118 -133
  10. package/dist/template/.opencode/plugin/memory/lint.ts +253 -275
  11. package/dist/template/.opencode/plugin/memory/tools.ts +224 -268
  12. package/dist/template/.opencode/plugin/memory/validate.ts +154 -164
  13. package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-preview.ts +13 -30
  14. package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-shared.ts +25 -0
  15. package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search.ts +17 -34
  16. package/dist/template/.opencode/plugin/session-summary.ts +542 -0
  17. package/dist/template/.opencode/plugin/srcwalk.ts +775 -661
  18. package/dist/template/.opencode/skill/condition-based-waiting/example.ts +15 -2
  19. package/dist/template/.opencode/skill/fallow/SKILL.md +409 -0
  20. package/dist/template/.opencode/skill/fallow/references/cli-reference.md +1905 -0
  21. package/dist/template/.opencode/skill/fallow/references/gotchas.md +644 -0
  22. package/dist/template/.opencode/skill/fallow/references/patterns.md +791 -0
  23. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,15 +4,15 @@ import * as p from "@clack/prompts";
4
4
  import { cac } from "cac";
5
5
  import color from "picocolors";
6
6
  import { createHash, createHmac } from "node:crypto";
7
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
8
8
  import { homedir, hostname, platform, release } from "node:os";
9
9
  import { basename, dirname, join, relative } from "node:path";
10
10
  import envPaths from "env-paths";
11
11
  import machineId from "node-machine-id";
12
12
  import { z } from "zod";
13
- import { execSync, spawn } from "node:child_process";
14
13
  import { fileURLToPath } from "node:url";
15
14
  import { applyPatch, createPatch } from "diff";
15
+ import { spawn } from "node:child_process";
16
16
  import * as readline from "node:readline";
17
17
 
18
18
  //#region \0rolldown/runtime.js
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "0.23.0";
23
+ var version = "0.23.2";
24
24
 
25
25
  //#endregion
26
26
  //#region src/utils/license.ts
@@ -264,16 +264,28 @@ async function activateCommand(keyArg) {
264
264
  //#endregion
265
265
  //#region src/utils/errors.ts
266
266
  /**
267
+ * Get the global OpenCode config directory based on OS.
268
+ * - macOS/Linux: ~/.config/opencode/ (respects XDG_CONFIG_HOME)
269
+ * - Windows: %APPDATA%\opencode\ or %LOCALAPPDATA%\opencode\
270
+ */
271
+ function getGlobalConfigDir() {
272
+ if (platform() === "win32") return join(process.env.APPDATA || process.env.LOCALAPPDATA || join(homedir(), "AppData", "Roaming"), "opencode");
273
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
274
+ }
275
+ /**
267
276
  * Resolve the .opencode directory path.
268
- * Handles two cases:
277
+ * Handles three cases:
269
278
  * 1. Standard project: cwd has .opencode/ subdirectory
270
279
  * 2. Global config dir: cwd IS the opencode config dir (has opencode.json directly)
271
- * Returns null if neither case applies.
280
+ * 3. Global config dir at ~/.config/opencode/ (fallback)
281
+ * Returns null if none apply.
272
282
  */
273
283
  function resolveOpencodePath() {
274
284
  const nested = join(process.cwd(), ".opencode");
275
285
  if (existsSync(nested)) return nested;
276
286
  if (existsSync(join(process.cwd(), "opencode.json"))) return process.cwd();
287
+ const globalDir = getGlobalConfigDir();
288
+ if (existsSync(join(globalDir, "opencode.json"))) return globalDir;
277
289
  return null;
278
290
  }
279
291
  /**
@@ -321,9 +333,6 @@ function showWarning(message, suggestion) {
321
333
  p.log.warn(`${color.yellow("!")} ${message}`);
322
334
  if (suggestion) p.log.info(` ${color.dim(`→ ${suggestion}`)}`);
323
335
  }
324
- /**
325
- * Display empty state with suggestion
326
- */
327
336
  function showEmpty(resource, createCmd) {
328
337
  p.log.info(color.dim(`No ${resource} found`));
329
338
  if (createCmd) p.log.info(color.dim(`→ Run: ${color.cyan(createCmd)}`));
@@ -338,13 +347,7 @@ function showEmpty(resource, createCmd) {
338
347
  const InitOptionsSchema = z.object({
339
348
  force: z.boolean().optional().default(false),
340
349
  global: z.boolean().optional().default(false),
341
- free: z.boolean().optional().default(false),
342
- recommend: z.boolean().optional().default(false),
343
- yes: z.boolean().optional().default(false),
344
- backup: z.boolean().optional().default(false),
345
- prune: z.boolean().optional().default(false),
346
- pruneAll: z.boolean().optional().default(false),
347
- projectOnly: z.boolean().optional().default(false)
350
+ yes: z.boolean().optional().default(false)
348
351
  });
349
352
  const UpgradeOptionsSchema = z.object({
350
353
  force: z.boolean().optional().default(false),
@@ -2752,825 +2755,116 @@ function fileModificationStatus(filePath, relativePath, manifest) {
2752
2755
  }
2753
2756
 
2754
2757
  //#endregion
2755
- //#region src/utils/patch.ts
2756
- /**
2757
- * Patch utilities for saving/applying user modifications to template files.
2758
- * Uses unified diff format for git-friendly, human-readable patches.
2759
- */
2760
- const PATCHES_DIR = "patches";
2761
- const PATCHES_JSON = ".patches.json";
2762
- const METADATA_VERSION = "1.0.0";
2763
- /**
2764
- * Calculate SHA-256 hash of content.
2765
- */
2766
- function calculateHash(content) {
2767
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
2768
- }
2769
- /**
2770
- * Convert a relative file path to a safe patch filename.
2771
- * e.g., "agent/build.md" -> "agent-build.md.patch"
2772
- */
2773
- function pathToPatchFilename(relativePath) {
2774
- return `${relativePath.replace(/\//g, "-")}.patch`;
2775
- }
2776
- /**
2777
- * Get the patches directory path.
2778
- */
2779
- function getPatchesDir(opencodeDir) {
2780
- return join(opencodeDir, PATCHES_DIR);
2781
- }
2782
- /**
2783
- * Get the template root directory (from dist/template or dev mode).
2784
- */
2758
+ //#region src/commands/init.ts
2759
+ const EXCLUDED_DIRS = [
2760
+ "node_modules",
2761
+ ".git",
2762
+ "dist",
2763
+ ".DS_Store",
2764
+ "coverage",
2765
+ ".next",
2766
+ ".turbo"
2767
+ ];
2768
+ const EXCLUDED_FILES = [
2769
+ "bun.lock",
2770
+ "package-lock.json",
2771
+ "yarn.lock",
2772
+ "pnpm-lock.yaml"
2773
+ ];
2785
2774
  function getTemplateRoot$2() {
2786
2775
  const __dirname = dirname(fileURLToPath(import.meta.url));
2787
- const possiblePaths = [
2788
- join(__dirname, "template"),
2789
- join(__dirname, "..", "..", ".opencode"),
2790
- join(__dirname, "..", "template")
2791
- ];
2792
- for (const path of possiblePaths) {
2793
- if (existsSync(join(path, ".opencode"))) return path;
2794
- if (existsSync(join(path, "opencode.json"))) return dirname(path);
2795
- }
2776
+ const possiblePaths = [join(__dirname, "template"), join(__dirname, "..", "..", ".opencode")];
2777
+ for (const path of possiblePaths) if (existsSync(join(path, ".opencode"))) return path;
2796
2778
  return null;
2797
2779
  }
2798
- /**
2799
- * Load patch metadata from .patches.json.
2800
- */
2801
- function loadPatchMetadata(opencodeDir) {
2802
- const metadataPath = join(getPatchesDir(opencodeDir), PATCHES_JSON);
2803
- if (!existsSync(metadataPath)) return {
2804
- version: METADATA_VERSION,
2805
- patches: {}
2806
- };
2807
- try {
2808
- const content = readFileSync(metadataPath, "utf-8");
2809
- return JSON.parse(content);
2810
- } catch {
2811
- return {
2812
- version: METADATA_VERSION,
2813
- patches: {}
2814
- };
2815
- }
2816
- }
2817
- /**
2818
- * Save patch metadata to .patches.json.
2819
- */
2820
- function savePatchMetadata(opencodeDir, metadata) {
2821
- const patchesDir = getPatchesDir(opencodeDir);
2822
- if (!existsSync(patchesDir)) mkdirSync(patchesDir, { recursive: true });
2823
- writeFileSync(join(patchesDir, PATCHES_JSON), JSON.stringify(metadata, null, 2));
2824
- }
2825
- /**
2826
- * Get the current OpenCodeKit version from package.json.
2827
- */
2828
2780
  function getPackageVersion$2() {
2829
2781
  const __dirname = dirname(fileURLToPath(import.meta.url));
2830
2782
  const pkgPaths = [join(__dirname, "..", "..", "package.json"), join(__dirname, "..", "package.json")];
2831
- for (const pkgPath of pkgPaths) if (existsSync(pkgPath)) try {
2832
- return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "unknown";
2833
- } catch {}
2783
+ for (const pkgPath of pkgPaths) {
2784
+ if (!existsSync(pkgPath)) continue;
2785
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
2786
+ }
2834
2787
  return "unknown";
2835
2788
  }
2836
- /**
2837
- * Generate a unified diff patch between template and user file.
2838
- */
2839
- function generatePatch(templateContent, userContent, relativePath) {
2840
- return createPatch(relativePath, templateContent, userContent, "template", "modified");
2841
- }
2842
- /**
2843
- * Save a patch for a modified template file.
2844
- * @returns The patch entry that was saved.
2845
- */
2846
- function savePatch(opencodeDir, relativePath, templateContent, userContent) {
2847
- const metadata = loadPatchMetadata(opencodeDir);
2848
- const patchesDir = getPatchesDir(opencodeDir);
2849
- if (!existsSync(patchesDir)) mkdirSync(patchesDir, { recursive: true });
2850
- const patchContent = generatePatch(templateContent, userContent, relativePath);
2851
- const patchFilename = pathToPatchFilename(relativePath);
2852
- writeFileSync(join(patchesDir, patchFilename), patchContent);
2853
- const entry = {
2854
- originalHash: calculateHash(templateContent),
2855
- currentHash: calculateHash(userContent),
2856
- patchFile: patchFilename,
2857
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2858
- templateVersion: getPackageVersion$2()
2859
- };
2860
- metadata.patches[relativePath] = entry;
2861
- savePatchMetadata(opencodeDir, metadata);
2862
- return entry;
2863
- }
2864
- /**
2865
- * Remove a patch for a file.
2866
- */
2867
- function removePatch(opencodeDir, relativePath) {
2868
- const metadata = loadPatchMetadata(opencodeDir);
2869
- const entry = metadata.patches[relativePath];
2870
- if (!entry) return false;
2871
- const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
2872
- if (existsSync(patchPath)) {
2873
- const { rmSync } = __require("node:fs");
2874
- rmSync(patchPath);
2789
+ async function copyDir(src, dest) {
2790
+ const { mkdir, readdir } = await import("node:fs/promises");
2791
+ await mkdir(dest, { recursive: true });
2792
+ for (const entry of await readdir(src, { withFileTypes: true })) {
2793
+ if (EXCLUDED_DIRS.includes(entry.name)) continue;
2794
+ if (!entry.isDirectory() && EXCLUDED_FILES.includes(entry.name)) continue;
2795
+ const srcPath = join(src, entry.name);
2796
+ const destPath = join(dest, entry.name);
2797
+ if (entry.isSymbolicLink()) {} else if (entry.isDirectory()) await copyDir(srcPath, destPath);
2798
+ else writeFileSync(destPath, readFileSync(srcPath, "utf-8"));
2875
2799
  }
2876
- delete metadata.patches[relativePath];
2877
- savePatchMetadata(opencodeDir, metadata);
2878
- return true;
2879
- }
2880
- /**
2881
- * Apply a patch to file content.
2882
- * @returns The patched content, or null if patch failed.
2883
- */
2884
- function applyPatch$1(originalContent, patchContent) {
2885
- return applyPatch(originalContent, patchContent);
2886
2800
  }
2887
- /**
2888
- * Apply all saved patches after an upgrade.
2889
- */
2890
- function applyAllPatches(opencodeDir) {
2891
- const metadata = loadPatchMetadata(opencodeDir);
2892
- const patchesDir = getPatchesDir(opencodeDir);
2893
- const results = [];
2894
- for (const [relativePath, entry] of Object.entries(metadata.patches)) {
2895
- if (entry.disabled) {
2896
- results.push({
2897
- success: true,
2898
- file: relativePath,
2899
- message: "Skipped (disabled)"
2900
- });
2901
- continue;
2902
- }
2903
- const filePath = join(opencodeDir, relativePath);
2904
- const patchPath = join(patchesDir, entry.patchFile);
2905
- if (!existsSync(filePath)) {
2906
- results.push({
2907
- success: false,
2908
- file: relativePath,
2909
- message: "File no longer exists"
2910
- });
2911
- continue;
2801
+ async function initCommand(rawOptions = {}) {
2802
+ const options = parseOptions(InitOptionsSchema, rawOptions);
2803
+ if (process.argv.includes("--quiet")) return;
2804
+ p.intro(color.bgCyan(color.black(" OpenCodeKit ")));
2805
+ if (options.global) {
2806
+ const globalDir = getGlobalConfigDir();
2807
+ const osName = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
2808
+ p.log.info(`Installing to global config (${osName})`);
2809
+ p.log.info(`Target: ${color.cyan(globalDir)}`);
2810
+ if (existsSync(globalDir) && !options.force) {
2811
+ p.log.warn(`Global config already exists at ${globalDir}`);
2812
+ p.log.info(`Use ${color.cyan("--force")} to overwrite`);
2813
+ p.outro("Nothing to do");
2814
+ return;
2912
2815
  }
2913
- if (!existsSync(patchPath)) {
2914
- results.push({
2915
- success: false,
2916
- file: relativePath,
2917
- message: "Patch file missing"
2918
- });
2919
- continue;
2816
+ const templateRoot = getTemplateRoot$2();
2817
+ if (!templateRoot) {
2818
+ p.log.error("Template not found. Please reinstall opencodekit.");
2819
+ p.outro(color.red("Failed"));
2820
+ process.exit(1);
2920
2821
  }
2921
- const currentContent = readFileSync(filePath, "utf-8");
2922
- const patchContent = readFileSync(patchPath, "utf-8");
2923
- const patched = applyPatch$1(currentContent, patchContent);
2924
- if (patched === false) {
2925
- writeFileSync(`${patchPath}.rej`, patchContent);
2926
- results.push({
2927
- success: false,
2928
- file: relativePath,
2929
- message: "Patch conflict - saved to .rej file",
2930
- conflict: true
2931
- });
2932
- } else {
2933
- writeFileSync(filePath, patched);
2934
- metadata.patches[relativePath].currentHash = calculateHash(patched);
2935
- savePatchMetadata(opencodeDir, metadata);
2936
- results.push({
2937
- success: true,
2938
- file: relativePath,
2939
- message: "Patch applied successfully"
2940
- });
2822
+ const s = p.spinner();
2823
+ s.start("Copying to global config");
2824
+ const opencodeSrc = join(templateRoot, ".opencode");
2825
+ if (!existsSync(opencodeSrc)) {
2826
+ s.stop("Failed");
2827
+ p.log.error("Template .opencode/ not found");
2828
+ p.outro(color.red("Failed"));
2829
+ process.exit(1);
2941
2830
  }
2831
+ await copyDir(opencodeSrc, globalDir);
2832
+ s.stop("Done");
2833
+ writeFileSync(join(globalDir, ".version"), getPackageVersion$2());
2834
+ generateManifest(globalDir, getPackageVersion$2());
2835
+ p.note(`Global config installed at:\n${globalDir}\n\nThis provides default agents, skills, and tools\nfor all OpenCode projects on this machine.`, "Global Installation Complete");
2836
+ p.outro(color.green("Ready!"));
2837
+ return;
2942
2838
  }
2943
- return results;
2944
- }
2945
- /**
2946
- * Check the status of all patches.
2947
- */
2948
- function checkPatchStatus(opencodeDir, templateRoot) {
2949
- const metadata = loadPatchMetadata(opencodeDir);
2950
- const statuses = [];
2951
- for (const [relativePath, entry] of Object.entries(metadata.patches)) {
2952
- if (!existsSync(join(opencodeDir, relativePath))) {
2953
- statuses.push({
2954
- relativePath,
2955
- entry,
2956
- status: "missing",
2957
- message: "User file no longer exists"
2958
- });
2959
- continue;
2960
- }
2961
- if (templateRoot) {
2962
- const templateFilePath = join(templateRoot, ".opencode", relativePath);
2963
- if (existsSync(templateFilePath)) {
2964
- const templateContent = readFileSync(templateFilePath, "utf-8");
2965
- if (calculateHash(templateContent) !== entry.originalHash) {
2966
- const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
2967
- if (existsSync(patchPath)) if (applyPatch$1(templateContent, readFileSync(patchPath, "utf-8")) === false) statuses.push({
2968
- relativePath,
2969
- entry,
2970
- status: "conflict",
2971
- message: "Template changed and patch cannot apply cleanly"
2972
- });
2973
- else statuses.push({
2974
- relativePath,
2975
- entry,
2976
- status: "stale",
2977
- message: "Template changed but patch can still apply"
2978
- });
2979
- else statuses.push({
2980
- relativePath,
2981
- entry,
2982
- status: "missing",
2983
- message: "Patch file missing"
2984
- });
2985
- continue;
2986
- }
2987
- }
2988
- }
2989
- statuses.push({
2990
- relativePath,
2991
- entry,
2992
- status: "clean",
2993
- message: "Patch is up to date"
2994
- });
2995
- }
2996
- return statuses;
2997
- }
2998
-
2999
- //#endregion
3000
- //#region src/commands/init.ts
3001
- const EXCLUDED_DIRS = [
3002
- "node_modules",
3003
- ".git",
3004
- "dist",
3005
- ".DS_Store",
3006
- "coverage",
3007
- ".next",
3008
- ".turbo"
3009
- ];
3010
- const EXCLUDED_FILES = [
3011
- "bun.lock",
3012
- "package-lock.json",
3013
- "yarn.lock",
3014
- "pnpm-lock.yaml"
3015
- ];
3016
- const PRESERVE_USER_DIRS = ["memory/project", "context"];
3017
- const SHARED_CONFIG_DIRS = [
3018
- "agent",
3019
- "command",
3020
- "skill",
3021
- "tool"
3022
- ];
3023
- /**
3024
- * Detect if global config has any of the shared dirs populated.
3025
- * Returns null if no global config or no shared dirs found.
3026
- */
3027
- function detectGlobalConfig() {
3028
- const globalDir = getGlobalConfigDir();
3029
- if (!existsSync(globalDir)) return null;
3030
- const coveredDirs = SHARED_CONFIG_DIRS.filter((d) => {
3031
- const dirPath = join(globalDir, d);
3032
- if (!existsSync(dirPath)) return false;
3033
- try {
3034
- return readdirSync(dirPath).filter((e) => !e.startsWith(".")).length > 0;
3035
- } catch {
3036
- return false;
3037
- }
3038
- });
3039
- if (coveredDirs.length === 0) return null;
3040
- return {
3041
- dir: globalDir,
3042
- coveredDirs
3043
- };
3044
- }
3045
- /**
3046
- * Get the global OpenCode config directory based on OS.
3047
- * - macOS/Linux: ~/.config/opencode/ (respects XDG_CONFIG_HOME)
3048
- * - Windows: %APPDATA%\opencode\ or %LOCALAPPDATA%\opencode\
3049
- */
3050
- function getGlobalConfigDir() {
3051
- if (platform() === "win32") return join(process.env.APPDATA || process.env.LOCALAPPDATA || join(homedir(), "AppData", "Roaming"), "opencode");
3052
- return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
3053
- }
3054
- function detectMode(targetDir) {
3055
- if (existsSync(join(targetDir, ".opencode"))) return "already-initialized";
3056
- if (existsSync(targetDir)) {
3057
- if (readdirSync(targetDir).some((e) => !e.startsWith(".") && !EXCLUDED_DIRS.includes(e) && e !== "node_modules")) return "add-config";
3058
- }
3059
- return "scaffold";
3060
- }
3061
- function getTemplateRoot$1() {
3062
- const __dirname = dirname(fileURLToPath(import.meta.url));
3063
- const possiblePaths = [join(__dirname, "template"), join(__dirname, "..", "..", ".opencode")];
3064
- for (const path of possiblePaths) if (existsSync(join(path, ".opencode"))) return path;
3065
- return null;
3066
- }
3067
- function getPackageVersion$1() {
3068
- const __dirname = dirname(fileURLToPath(import.meta.url));
3069
- const pkgPaths = [join(__dirname, "..", "..", "package.json"), join(__dirname, "..", "package.json")];
3070
- for (const pkgPath of pkgPaths) {
3071
- if (!existsSync(pkgPath)) continue;
3072
- return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
3073
- }
3074
- return "unknown";
3075
- }
3076
- async function copyDir(src, dest) {
3077
- const { mkdir, readdir } = await import("node:fs/promises");
3078
- await mkdir(dest, { recursive: true });
3079
- for (const entry of await readdir(src, { withFileTypes: true })) {
3080
- if (EXCLUDED_DIRS.includes(entry.name)) continue;
3081
- if (!entry.isDirectory() && EXCLUDED_FILES.includes(entry.name)) continue;
3082
- const srcPath = join(src, entry.name);
3083
- const destPath = join(dest, entry.name);
3084
- if (entry.isSymbolicLink()) {} else if (entry.isDirectory()) await copyDir(srcPath, destPath);
3085
- else writeFileSync(destPath, readFileSync(srcPath, "utf-8"));
3086
- }
3087
- }
3088
- async function copyOpenCodeOnly(templateRoot, targetDir, skipDirs) {
3089
- const opencodeSrc = join(templateRoot, ".opencode");
3090
- const opencodeDest = join(targetDir, ".opencode");
3091
- if (!existsSync(opencodeSrc)) return false;
3092
- if (skipDirs && skipDirs.length > 0) {
3093
- const skipSet = new Set(skipDirs);
3094
- mkdirSync(opencodeDest, { recursive: true });
3095
- for (const entry of readdirSync(opencodeSrc, { withFileTypes: true })) {
3096
- if (EXCLUDED_DIRS.includes(entry.name)) continue;
3097
- if (!entry.isDirectory() && EXCLUDED_FILES.includes(entry.name)) continue;
3098
- if (entry.isSymbolicLink()) continue;
3099
- if (entry.isDirectory() && skipSet.has(entry.name)) continue;
3100
- const srcPath = join(opencodeSrc, entry.name);
3101
- const destPath = join(opencodeDest, entry.name);
3102
- if (entry.isDirectory()) await copyDir(srcPath, destPath);
3103
- else writeFileSync(destPath, readFileSync(srcPath, "utf-8"));
3104
- }
3105
- return true;
3106
- }
3107
- await copyDir(opencodeSrc, opencodeDest);
3108
- return true;
3109
- }
3110
- const MODEL_PRESETS = {
3111
- free: {
3112
- model: "opencode/glm-5-free",
3113
- agents: {
3114
- build: "opencode/minimax-m2.5-free",
3115
- plan: "opencode/minimax-m2.5-free",
3116
- review: "opencode/minimax-m2.5-free",
3117
- explore: "opencode/glm-5-free",
3118
- general: "opencode/glm-5-free",
3119
- vision: "opencode/minimax-m2.5-free",
3120
- scout: "opencode/glm-5-free",
3121
- painter: "opencode/minimax-m2.5-free"
3122
- }
3123
- },
3124
- recommend: {
3125
- model: "github-copilot/gpt-5.4",
3126
- agents: {
3127
- build: "github-copilot/gpt-5.4",
3128
- plan: "github-copilot/gpt-5.4",
3129
- review: "github-copilot/gpt-5.3-codex",
3130
- explore: "github-copilot/claude-haiku-4.5",
3131
- general: "github-copilot/gpt-5.3-codex",
3132
- vision: "github-copilot/gemini-3.1-pro-preview",
3133
- scout: "github-copilot/claude-sonnet-4.6",
3134
- painter: "proxypal/gemini-3.1-flash-image"
3135
- }
3136
- }
3137
- };
3138
- function applyModelPreset(targetDir, preset) {
3139
- const configPath = join(targetDir, ".opencode", "opencode.json");
3140
- if (!existsSync(configPath)) return;
3141
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
3142
- const presetConfig = MODEL_PRESETS[preset];
3143
- config.model = presetConfig.model;
3144
- if (config.agent) {
3145
- for (const [agentName, model] of Object.entries(presetConfig.agents)) if (config.agent[agentName]) config.agent[agentName].model = model;
3146
- }
3147
- writeFileSync(configPath, JSON.stringify(config, null, 2));
3148
- }
3149
- const AGENT_DESCRIPTIONS = {
3150
- build: "Main coding agent (complex tasks)",
3151
- plan: "Planning and design agent",
3152
- review: "Code review and debugging",
3153
- explore: "Fast codebase search",
3154
- general: "Quick, simple tasks",
3155
- painter: "Image generation and editing",
3156
- vision: "Visual analysis (quality)",
3157
- scout: "External research/docs",
3158
- compaction: "Context summarization"
3159
- };
3160
- async function promptCustomModels(targetDir) {
3161
- const configPath = join(targetDir, ".opencode", "opencode.json");
3162
- if (!existsSync(configPath)) return;
3163
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
3164
- p.log.info(color.dim("Enter model IDs (e.g., github-copilot/gpt-5.4, proxypal/gemini-3.1-flash-image)"));
3165
- p.log.info(color.dim("Press Enter to keep current value\n"));
3166
- const mainModel = await p.text({
3167
- message: "Main session model",
3168
- placeholder: config.model || "github-copilot/gpt-5.4",
3169
- defaultValue: config.model
3170
- });
3171
- if (p.isCancel(mainModel)) {
3172
- p.log.warn("Cancelled - keeping defaults");
3173
- return;
3174
- }
3175
- if (mainModel) config.model = mainModel;
3176
- const agents = Object.keys(AGENT_DESCRIPTIONS);
3177
- for (const agent of agents) {
3178
- if (!config.agent?.[agent]) continue;
3179
- const currentModel = config.agent[agent].model || config.model;
3180
- const agentModel = await p.text({
3181
- message: `${agent} - ${AGENT_DESCRIPTIONS[agent]}`,
3182
- placeholder: currentModel,
3183
- defaultValue: currentModel
3184
- });
3185
- if (p.isCancel(agentModel)) {
3186
- p.log.warn("Cancelled - saving partial config");
3187
- break;
3188
- }
3189
- if (agentModel) config.agent[agent].model = agentModel;
3190
- }
3191
- writeFileSync(configPath, JSON.stringify(config, null, 2));
3192
- }
3193
- function getAffectedFiles(dir, prefix = "") {
3194
- if (!existsSync(dir)) return [];
3195
- const files = [];
3196
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
3197
- if (EXCLUDED_DIRS.includes(entry.name)) continue;
3198
- const path = prefix ? `${prefix}/${entry.name}` : entry.name;
3199
- if (entry.isDirectory()) files.push(...getAffectedFiles(join(dir, entry.name), path));
3200
- else files.push(path);
3201
- }
3202
- return files;
3203
- }
3204
- function backupOpenCode(targetDir) {
3205
- const opencodeDir = join(targetDir, ".opencode");
3206
- if (!existsSync(opencodeDir)) return null;
3207
- const backupDir = join(targetDir, `.opencode.bak-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`);
3208
- renameSync(opencodeDir, backupDir);
3209
- return backupDir;
3210
- }
3211
- function getTemplateFiles(templateRoot) {
3212
- const opencodeSrc = join(templateRoot, ".opencode");
3213
- if (!existsSync(opencodeSrc)) return /* @__PURE__ */ new Set();
3214
- return new Set(getAffectedFiles(opencodeSrc));
3215
- }
3216
- function findOrphans(targetDir, templateFiles) {
3217
- const opencodeDir = join(targetDir, ".opencode");
3218
- if (!existsSync(opencodeDir)) return [];
3219
- return getAffectedFiles(opencodeDir).filter((f) => f !== MANIFEST_FILE && !templateFiles.has(f));
3220
- }
3221
- /**
3222
- * Check if an orphan file is a modified template file (exists in template but content differs).
3223
- * Returns the template content if it's a modified template file, null otherwise.
3224
- */
3225
- function getModifiedTemplateContent(templateRoot, orphanPath) {
3226
- const templateFilePath = join(templateRoot, ".opencode", orphanPath);
3227
- if (!existsSync(templateFilePath)) return null;
3228
- return readFileSync(templateFilePath, "utf-8");
3229
- }
3230
- /**
3231
- * Auto-detect and save patches for modified template files among orphans.
3232
- * Returns the list of orphans that were NOT saved as patches (true orphans).
3233
- */
3234
- async function autoSavePatchesForOrphans(targetDir, templateRoot, orphans) {
3235
- const opencodeDir = join(targetDir, ".opencode");
3236
- const savedPatches = [];
3237
- const trueOrphans = [];
3238
- for (const orphan of orphans) {
3239
- const templateContent = getModifiedTemplateContent(templateRoot, orphan);
3240
- if (!templateContent) {
3241
- trueOrphans.push(orphan);
3242
- continue;
3243
- }
3244
- const userContent = readFileSync(join(opencodeDir, orphan), "utf-8");
3245
- if (templateContent === userContent) {
3246
- trueOrphans.push(orphan);
3247
- continue;
3248
- }
3249
- try {
3250
- savePatch(opencodeDir, orphan, templateContent, userContent);
3251
- savedPatches.push(orphan);
3252
- } catch {
3253
- trueOrphans.push(orphan);
3254
- }
3255
- }
3256
- return {
3257
- savedPatches,
3258
- trueOrphans
3259
- };
3260
- }
3261
- /**
3262
- * Save existing user files from preserve directories before reinit.
3263
- * Returns a map of relative paths to file contents.
3264
- */
3265
- function preserveUserFiles(targetDir) {
3266
- const opencodeDir = join(targetDir, ".opencode");
3267
- const preserved = /* @__PURE__ */ new Map();
3268
- function collectFiles(currentDir, relativeDir) {
3269
- for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
3270
- const filePath = join(currentDir, entry.name);
3271
- const relativePath = join(relativeDir, entry.name);
3272
- if (entry.isDirectory()) {
3273
- collectFiles(filePath, relativePath);
3274
- continue;
3275
- }
3276
- if (!entry.isFile()) continue;
3277
- preserved.set(relativePath, readFileSync(filePath, "utf-8"));
3278
- }
3279
- }
3280
- for (const relDir of PRESERVE_USER_DIRS) {
3281
- const dirPath = join(opencodeDir, relDir);
3282
- if (!existsSync(dirPath)) continue;
3283
- collectFiles(dirPath, relDir);
3284
- }
3285
- return preserved;
3286
- }
3287
- /**
3288
- * Restore preserved user files after fresh template copy.
3289
- */
3290
- function restoreUserFiles(targetDir, preserved) {
3291
- const opencodeDir = join(targetDir, ".opencode");
3292
- for (const [relativePath, content] of preserved) {
3293
- const filePath = join(opencodeDir, relativePath);
3294
- mkdirSync(dirname(filePath), { recursive: true });
3295
- writeFileSync(filePath, content);
3296
- }
3297
- }
3298
- function finalizeInstalledFiles(targetDir, version, preservedFiles) {
3299
- generateManifest(join(targetDir, ".opencode"), version);
3300
- if (!preservedFiles || preservedFiles.size === 0) return 0;
3301
- restoreUserFiles(targetDir, preservedFiles);
3302
- return preservedFiles.size;
3303
- }
3304
- async function initCommand(rawOptions = {}) {
3305
- const options = parseOptions(InitOptionsSchema, rawOptions);
3306
- if (process.argv.includes("--quiet")) return;
3307
- p.intro(color.bgCyan(color.black(" OpenCodeKit ")));
3308
- if (options.global) {
3309
- const globalDir = getGlobalConfigDir();
3310
- const os = platform();
3311
- const osName = os === "win32" ? "Windows" : os === "darwin" ? "macOS" : "Linux";
3312
- p.log.info(`Installing to global config (${osName})`);
3313
- p.log.info(`Target: ${color.cyan(globalDir)}`);
3314
- const templateRoot = getTemplateRoot$1();
3315
- if (!templateRoot) {
3316
- p.log.error("Template not found. Please reinstall opencodekit.");
3317
- p.outro(color.red("Failed"));
3318
- process.exit(1);
3319
- }
3320
- if (existsSync(globalDir) && !options.force) {
3321
- p.log.warn(`Global config already exists at ${globalDir}`);
3322
- p.log.info(`Use ${color.cyan("--force")} to overwrite`);
3323
- p.outro("Nothing to do");
3324
- return;
3325
- }
3326
- const s = p.spinner();
3327
- s.start("Copying to global config");
3328
- const opencodeSrc = join(templateRoot, ".opencode");
3329
- if (!existsSync(opencodeSrc)) {
3330
- s.stop("Failed");
3331
- p.log.error("Template .opencode/ not found");
3332
- p.outro(color.red("Failed"));
3333
- process.exit(1);
3334
- }
3335
- await copyDir(opencodeSrc, globalDir);
3336
- s.stop("Done");
3337
- p.note(`Global config installed at:\n${globalDir}\n\nThis provides default agents, skills, and tools\nfor all OpenCode projects on this machine.`, "Global Installation Complete");
3338
- p.outro(color.green("Ready!"));
3339
- return;
3340
- }
3341
- const targetDir = process.cwd();
3342
- const mode = detectMode(targetDir);
3343
- if (mode === "already-initialized" && !options.force) {
3344
- p.log.warn("Already initialized (.opencode/ exists)");
3345
- p.log.info(`Use ${color.cyan("--force")} to reinitialize`);
2839
+ const localDir = join(process.cwd(), ".opencode");
2840
+ p.log.info(`Installing to project: ${color.cyan(localDir)}`);
2841
+ if (existsSync(localDir) && !options.force) {
2842
+ p.log.warn("Project already initialized (.opencode/ exists)");
2843
+ p.log.info(`Use ${color.cyan("--force")} to overwrite`);
3346
2844
  p.outro("Nothing to do");
3347
2845
  return;
3348
2846
  }
3349
- let preservedFiles;
3350
- if (mode === "already-initialized" && options.force) {
3351
- const affected = getAffectedFiles(join(targetDir, ".opencode"));
3352
- if (affected.length > 0 && !options.yes) {
3353
- p.log.warn(`${affected.length} files will be overwritten:`);
3354
- const preview = affected.slice(0, 10);
3355
- for (const file of preview) p.log.info(color.dim(` .opencode/${file}`));
3356
- if (affected.length > 10) p.log.info(color.dim(` ... and ${affected.length - 10} more`));
3357
- if (!options.backup) {
3358
- const shouldBackup = await p.confirm({
3359
- message: "Backup existing .opencode before overwriting?",
3360
- initialValue: true
3361
- });
3362
- if (p.isCancel(shouldBackup)) {
3363
- p.cancel("Cancelled");
3364
- process.exit(0);
3365
- }
3366
- if (shouldBackup) options.backup = true;
3367
- }
3368
- const proceed = await p.confirm({
3369
- message: options.backup ? "Proceed? (existing config will be backed up)" : "Proceed without backup?",
3370
- initialValue: options.backup
3371
- });
3372
- if (p.isCancel(proceed) || !proceed) {
3373
- p.cancel("Cancelled");
3374
- process.exit(0);
3375
- }
3376
- }
3377
- if (options.backup) {
3378
- preservedFiles = preserveUserFiles(targetDir);
3379
- const backupPath = backupOpenCode(targetDir);
3380
- if (backupPath) p.log.info(`Backed up to ${color.cyan(basename(backupPath))}`);
3381
- } else preservedFiles = preserveUserFiles(targetDir);
3382
- }
3383
- const templateRoot = getTemplateRoot$1();
2847
+ const templateRoot = getTemplateRoot$2();
3384
2848
  if (!templateRoot) {
3385
2849
  p.log.error("Template not found. Please reinstall opencodekit.");
3386
2850
  p.outro(color.red("Failed"));
3387
2851
  process.exit(1);
3388
2852
  }
3389
- let projectName = basename(targetDir);
3390
- if (mode === "scaffold") {
3391
- const name = await p.text({
3392
- message: "Project name",
3393
- placeholder: projectName,
3394
- defaultValue: projectName
3395
- });
3396
- if (p.isCancel(name)) {
3397
- p.cancel("Cancelled");
3398
- process.exit(0);
3399
- }
3400
- projectName = name || projectName;
3401
- }
3402
- let skipDirs = [];
3403
- if (!options.global) {
3404
- const globalConfig = detectGlobalConfig();
3405
- if (globalConfig && options.projectOnly) {
3406
- skipDirs = globalConfig.coveredDirs;
3407
- p.log.info(`Using global config from ${color.cyan(globalConfig.dir)}`);
3408
- p.log.info(`Skipping: ${skipDirs.map((d) => color.dim(d)).join(", ")}`);
3409
- } else if (globalConfig && !options.yes) {
3410
- p.log.info(`Global config found at ${color.cyan(globalConfig.dir)}`);
3411
- p.log.info(`Available globally: ${globalConfig.coveredDirs.map((d) => color.green(d)).join(", ")}`);
3412
- const useGlobal = await p.confirm({
3413
- message: "Skip these (use global config)? Only project-scope files will be created locally.",
3414
- initialValue: true
3415
- });
3416
- if (!p.isCancel(useGlobal) && useGlobal) skipDirs = globalConfig.coveredDirs;
3417
- } else if (globalConfig && options.yes) p.log.info(`Global config found at ${color.cyan(globalConfig.dir)} — use ${color.bold("--project-only")} to skip shared dirs`);
3418
- }
3419
2853
  const s = p.spinner();
3420
- if (mode === "scaffold") {
3421
- s.start("Scaffolding project");
3422
- mkdirSync(targetDir, { recursive: true });
3423
- } else if (mode === "add-config") s.start("Adding OpenCodeKit");
3424
- else s.start("Reinitializing");
3425
- if (!await copyOpenCodeOnly(templateRoot, targetDir, skipDirs)) {
2854
+ s.start("Copying to project");
2855
+ const opencodeSrc = join(templateRoot, ".opencode");
2856
+ if (!existsSync(opencodeSrc)) {
3426
2857
  s.stop("Failed");
3427
- p.outro(color.red("Template copy failed"));
3428
- process.exit(1);
3429
- }
3430
- s.stop("Done");
3431
- if (skipDirs.length > 0) p.log.info(`Project-only init: skipped ${skipDirs.map((d) => color.dim(d)).join(", ")} (using global config)`);
3432
- const restoredFileCount = finalizeInstalledFiles(targetDir, getPackageVersion$1(), preservedFiles);
3433
- if (restoredFileCount > 0) p.log.info(`Preserved ${restoredFileCount} user memory files (memory/project/)`);
3434
- if (options.free) {
3435
- applyModelPreset(targetDir, "free");
3436
- p.log.info("Applied free model preset");
3437
- } else if (options.recommend) {
3438
- applyModelPreset(targetDir, "recommend");
3439
- p.log.info("Applied recommended model preset");
3440
- } else if (options.yes) {
3441
- applyModelPreset(targetDir, "free");
3442
- p.log.info("Applied free model preset (default)");
3443
- } else {
3444
- const preset = await p.select({
3445
- message: "Choose model preset",
3446
- options: [
3447
- {
3448
- value: "free",
3449
- label: "Free models",
3450
- hint: "minimax, glm, grok (no API costs)"
3451
- },
3452
- {
3453
- value: "recommend",
3454
- label: "Recommended models",
3455
- hint: "gpt-5.4, gpt-5.3-codex, sonnet-4.6, gemini-3.1"
3456
- },
3457
- {
3458
- value: "custom",
3459
- label: "Custom",
3460
- hint: "configure each agent individually"
3461
- },
3462
- {
3463
- value: "skip",
3464
- label: "Skip",
3465
- hint: "keep template defaults"
3466
- }
3467
- ]
3468
- });
3469
- if (!p.isCancel(preset)) {
3470
- if (preset === "custom") {
3471
- await promptCustomModels(targetDir);
3472
- p.log.info("Applied custom model configuration");
3473
- } else if (preset !== "skip") {
3474
- applyModelPreset(targetDir, preset);
3475
- p.log.info(`Applied ${preset} model preset`);
3476
- }
3477
- }
3478
- }
3479
- const opencodeDir = join(targetDir, ".opencode");
3480
- if (existsSync(join(opencodeDir, "package.json"))) {
3481
- const installSpinner = p.spinner();
3482
- installSpinner.start("Installing dependencies");
3483
- try {
3484
- execSync("npm install --no-fund --no-audit", {
3485
- cwd: opencodeDir,
3486
- stdio: "ignore"
3487
- });
3488
- installSpinner.stop("Dependencies installed");
3489
- } catch {
3490
- installSpinner.stop("Failed to install (run manually: cd .opencode && npm install)");
3491
- }
3492
- }
3493
- if (mode === "already-initialized" && options.force && !options.backup) {
3494
- const orphans = findOrphans(targetDir, getTemplateFiles(templateRoot));
3495
- if (orphans.length > 0) {
3496
- p.log.warn(`Found ${orphans.length} orphan files not in template`);
3497
- const { savedPatches, trueOrphans } = await autoSavePatchesForOrphans(targetDir, templateRoot, orphans);
3498
- if (savedPatches.length > 0) {
3499
- p.log.success(`Auto-saved ${savedPatches.length} patches for modified template files`);
3500
- for (const patch of savedPatches) console.log(` ${color.green("✓")} ${patch}`);
3501
- p.log.info(color.dim("These patches will be reapplied after template files are updated."));
3502
- }
3503
- if (trueOrphans.length === 0) {} else if (options.pruneAll) {
3504
- const pruneSpinner = p.spinner();
3505
- pruneSpinner.start("Removing orphan files");
3506
- for (const orphan of trueOrphans) rmSync(join(opencodeDir, orphan));
3507
- pruneSpinner.stop(`Removed ${trueOrphans.length} orphan files`);
3508
- } else if (options.prune) {
3509
- const selected = await p.multiselect({
3510
- message: "Select orphan files to delete",
3511
- options: trueOrphans.map((o) => ({
3512
- value: o,
3513
- label: o
3514
- })),
3515
- required: false
3516
- });
3517
- if (!p.isCancel(selected) && selected.length > 0) {
3518
- const pruneSpinner = p.spinner();
3519
- pruneSpinner.start("Deleting files");
3520
- for (const file of selected) rmSync(join(opencodeDir, file));
3521
- pruneSpinner.stop(`Deleted ${selected.length} files`);
3522
- }
3523
- } else if (!options.yes && trueOrphans.length > 0) {
3524
- const preview = trueOrphans.slice(0, 5);
3525
- for (const file of preview) p.log.info(color.dim(` .opencode/${file}`));
3526
- if (trueOrphans.length > 5) p.log.info(color.dim(` ... and ${trueOrphans.length - 5} more`));
3527
- const orphanAction = await p.select({
3528
- message: "How to handle orphan files?",
3529
- options: [
3530
- {
3531
- value: "keep",
3532
- label: "Keep all",
3533
- hint: "leave orphan files"
3534
- },
3535
- {
3536
- value: "select",
3537
- label: "Select",
3538
- hint: "choose which to delete"
3539
- },
3540
- {
3541
- value: "delete",
3542
- label: "Delete all",
3543
- hint: "remove all orphans"
3544
- }
3545
- ]
3546
- });
3547
- if (!p.isCancel(orphanAction)) {
3548
- if (orphanAction === "delete") {
3549
- const pruneSpinner = p.spinner();
3550
- pruneSpinner.start("Removing orphan files");
3551
- for (const orphan of trueOrphans) rmSync(join(opencodeDir, orphan));
3552
- pruneSpinner.stop(`Removed ${trueOrphans.length} orphan files`);
3553
- } else if (orphanAction === "select") {
3554
- const selected = await p.multiselect({
3555
- message: "Select orphan files to delete",
3556
- options: trueOrphans.map((o) => ({
3557
- value: o,
3558
- label: o
3559
- })),
3560
- required: false
3561
- });
3562
- if (!p.isCancel(selected) && selected.length > 0) {
3563
- const pruneSpinner = p.spinner();
3564
- pruneSpinner.start("Deleting files");
3565
- for (const file of selected) rmSync(join(opencodeDir, file));
3566
- pruneSpinner.stop(`Deleted ${selected.length} files`);
3567
- }
3568
- }
3569
- }
3570
- }
3571
- }
2858
+ p.log.error("Template .opencode/ not found");
2859
+ p.outro(color.red("Failed"));
2860
+ process.exit(1);
3572
2861
  }
3573
- p.outro(color.green("Ready to code!"));
2862
+ await copyDir(opencodeSrc, localDir);
2863
+ s.stop("Done");
2864
+ writeFileSync(join(localDir, ".version"), getPackageVersion$2());
2865
+ generateManifest(localDir, getPackageVersion$2());
2866
+ p.note(`OpenCodeKit config installed at:\n${localDir}\n\nThis provides default agents, skills, and tools\nfor this project.`, "Project Installation Complete");
2867
+ p.outro(color.green("Ready!"));
3574
2868
  }
3575
2869
 
3576
2870
  //#endregion
@@ -3913,6 +3207,251 @@ async function removeSkill(skillDir, skillNameArg) {
3913
3207
  p.log.success(`Removed skill "${skill.name}"`);
3914
3208
  }
3915
3209
 
3210
+ //#endregion
3211
+ //#region src/utils/patch.ts
3212
+ /**
3213
+ * Patch utilities for saving/applying user modifications to template files.
3214
+ * Uses unified diff format for git-friendly, human-readable patches.
3215
+ */
3216
+ const PATCHES_DIR = "patches";
3217
+ const PATCHES_JSON = ".patches.json";
3218
+ const METADATA_VERSION = "1.0.0";
3219
+ /**
3220
+ * Calculate SHA-256 hash of content.
3221
+ */
3222
+ function calculateHash(content) {
3223
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
3224
+ }
3225
+ /**
3226
+ * Convert a relative file path to a safe patch filename.
3227
+ * e.g., "agent/build.md" -> "agent-build.md.patch"
3228
+ */
3229
+ function pathToPatchFilename(relativePath) {
3230
+ return `${relativePath.replace(/\//g, "-")}.patch`;
3231
+ }
3232
+ /**
3233
+ * Get the patches directory path.
3234
+ */
3235
+ function getPatchesDir(opencodeDir) {
3236
+ return join(opencodeDir, PATCHES_DIR);
3237
+ }
3238
+ /**
3239
+ * Get the template root directory (from dist/template or dev mode).
3240
+ */
3241
+ function getTemplateRoot$1() {
3242
+ const __dirname = dirname(fileURLToPath(import.meta.url));
3243
+ const possiblePaths = [
3244
+ join(__dirname, "template"),
3245
+ join(__dirname, "..", "..", ".opencode"),
3246
+ join(__dirname, "..", "template")
3247
+ ];
3248
+ for (const path of possiblePaths) {
3249
+ if (existsSync(join(path, ".opencode"))) return path;
3250
+ if (existsSync(join(path, "opencode.json"))) return dirname(path);
3251
+ }
3252
+ return null;
3253
+ }
3254
+ /**
3255
+ * Load patch metadata from .patches.json.
3256
+ */
3257
+ function loadPatchMetadata(opencodeDir) {
3258
+ const metadataPath = join(getPatchesDir(opencodeDir), PATCHES_JSON);
3259
+ if (!existsSync(metadataPath)) return {
3260
+ version: METADATA_VERSION,
3261
+ patches: {}
3262
+ };
3263
+ try {
3264
+ const content = readFileSync(metadataPath, "utf-8");
3265
+ return JSON.parse(content);
3266
+ } catch {
3267
+ return {
3268
+ version: METADATA_VERSION,
3269
+ patches: {}
3270
+ };
3271
+ }
3272
+ }
3273
+ /**
3274
+ * Save patch metadata to .patches.json.
3275
+ */
3276
+ function savePatchMetadata(opencodeDir, metadata) {
3277
+ const patchesDir = getPatchesDir(opencodeDir);
3278
+ if (!existsSync(patchesDir)) mkdirSync(patchesDir, { recursive: true });
3279
+ writeFileSync(join(patchesDir, PATCHES_JSON), JSON.stringify(metadata, null, 2));
3280
+ }
3281
+ /**
3282
+ * Get the current OpenCodeKit version from package.json.
3283
+ */
3284
+ function getPackageVersion$1() {
3285
+ const __dirname = dirname(fileURLToPath(import.meta.url));
3286
+ const pkgPaths = [join(__dirname, "..", "..", "package.json"), join(__dirname, "..", "package.json")];
3287
+ for (const pkgPath of pkgPaths) if (existsSync(pkgPath)) try {
3288
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "unknown";
3289
+ } catch {}
3290
+ return "unknown";
3291
+ }
3292
+ /**
3293
+ * Generate a unified diff patch between template and user file.
3294
+ */
3295
+ function generatePatch(templateContent, userContent, relativePath) {
3296
+ return createPatch(relativePath, templateContent, userContent, "template", "modified");
3297
+ }
3298
+ /**
3299
+ * Save a patch for a modified template file.
3300
+ * @returns The patch entry that was saved.
3301
+ */
3302
+ function savePatch(opencodeDir, relativePath, templateContent, userContent) {
3303
+ const metadata = loadPatchMetadata(opencodeDir);
3304
+ const patchesDir = getPatchesDir(opencodeDir);
3305
+ if (!existsSync(patchesDir)) mkdirSync(patchesDir, { recursive: true });
3306
+ const patchContent = generatePatch(templateContent, userContent, relativePath);
3307
+ const patchFilename = pathToPatchFilename(relativePath);
3308
+ writeFileSync(join(patchesDir, patchFilename), patchContent);
3309
+ const entry = {
3310
+ originalHash: calculateHash(templateContent),
3311
+ currentHash: calculateHash(userContent),
3312
+ patchFile: patchFilename,
3313
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3314
+ templateVersion: getPackageVersion$1()
3315
+ };
3316
+ metadata.patches[relativePath] = entry;
3317
+ savePatchMetadata(opencodeDir, metadata);
3318
+ return entry;
3319
+ }
3320
+ /**
3321
+ * Remove a patch for a file.
3322
+ */
3323
+ function removePatch(opencodeDir, relativePath) {
3324
+ const metadata = loadPatchMetadata(opencodeDir);
3325
+ const entry = metadata.patches[relativePath];
3326
+ if (!entry) return false;
3327
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
3328
+ if (existsSync(patchPath)) {
3329
+ const { rmSync } = __require("node:fs");
3330
+ rmSync(patchPath);
3331
+ }
3332
+ delete metadata.patches[relativePath];
3333
+ savePatchMetadata(opencodeDir, metadata);
3334
+ return true;
3335
+ }
3336
+ /**
3337
+ * Apply a patch to file content.
3338
+ * @returns The patched content, or null if patch failed.
3339
+ */
3340
+ function applyPatch$1(originalContent, patchContent) {
3341
+ return applyPatch(originalContent, patchContent);
3342
+ }
3343
+ /**
3344
+ * Apply all saved patches after an upgrade.
3345
+ */
3346
+ function applyAllPatches(opencodeDir) {
3347
+ const metadata = loadPatchMetadata(opencodeDir);
3348
+ const patchesDir = getPatchesDir(opencodeDir);
3349
+ const results = [];
3350
+ for (const [relativePath, entry] of Object.entries(metadata.patches)) {
3351
+ if (entry.disabled) {
3352
+ results.push({
3353
+ success: true,
3354
+ file: relativePath,
3355
+ message: "Skipped (disabled)"
3356
+ });
3357
+ continue;
3358
+ }
3359
+ const filePath = join(opencodeDir, relativePath);
3360
+ const patchPath = join(patchesDir, entry.patchFile);
3361
+ if (!existsSync(filePath)) {
3362
+ results.push({
3363
+ success: false,
3364
+ file: relativePath,
3365
+ message: "File no longer exists"
3366
+ });
3367
+ continue;
3368
+ }
3369
+ if (!existsSync(patchPath)) {
3370
+ results.push({
3371
+ success: false,
3372
+ file: relativePath,
3373
+ message: "Patch file missing"
3374
+ });
3375
+ continue;
3376
+ }
3377
+ const currentContent = readFileSync(filePath, "utf-8");
3378
+ const patchContent = readFileSync(patchPath, "utf-8");
3379
+ const patched = applyPatch$1(currentContent, patchContent);
3380
+ if (patched === false) {
3381
+ writeFileSync(`${patchPath}.rej`, patchContent);
3382
+ results.push({
3383
+ success: false,
3384
+ file: relativePath,
3385
+ message: "Patch conflict - saved to .rej file",
3386
+ conflict: true
3387
+ });
3388
+ } else {
3389
+ writeFileSync(filePath, patched);
3390
+ metadata.patches[relativePath].currentHash = calculateHash(patched);
3391
+ savePatchMetadata(opencodeDir, metadata);
3392
+ results.push({
3393
+ success: true,
3394
+ file: relativePath,
3395
+ message: "Patch applied successfully"
3396
+ });
3397
+ }
3398
+ }
3399
+ return results;
3400
+ }
3401
+ /**
3402
+ * Check the status of all patches.
3403
+ */
3404
+ function checkPatchStatus(opencodeDir, templateRoot) {
3405
+ const metadata = loadPatchMetadata(opencodeDir);
3406
+ const statuses = [];
3407
+ for (const [relativePath, entry] of Object.entries(metadata.patches)) {
3408
+ if (!existsSync(join(opencodeDir, relativePath))) {
3409
+ statuses.push({
3410
+ relativePath,
3411
+ entry,
3412
+ status: "missing",
3413
+ message: "User file no longer exists"
3414
+ });
3415
+ continue;
3416
+ }
3417
+ if (templateRoot) {
3418
+ const templateFilePath = join(templateRoot, ".opencode", relativePath);
3419
+ if (existsSync(templateFilePath)) {
3420
+ const templateContent = readFileSync(templateFilePath, "utf-8");
3421
+ if (calculateHash(templateContent) !== entry.originalHash) {
3422
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
3423
+ if (existsSync(patchPath)) if (applyPatch$1(templateContent, readFileSync(patchPath, "utf-8")) === false) statuses.push({
3424
+ relativePath,
3425
+ entry,
3426
+ status: "conflict",
3427
+ message: "Template changed and patch cannot apply cleanly"
3428
+ });
3429
+ else statuses.push({
3430
+ relativePath,
3431
+ entry,
3432
+ status: "stale",
3433
+ message: "Template changed but patch can still apply"
3434
+ });
3435
+ else statuses.push({
3436
+ relativePath,
3437
+ entry,
3438
+ status: "missing",
3439
+ message: "Patch file missing"
3440
+ });
3441
+ continue;
3442
+ }
3443
+ }
3444
+ }
3445
+ statuses.push({
3446
+ relativePath,
3447
+ entry,
3448
+ status: "clean",
3449
+ message: "Patch is up to date"
3450
+ });
3451
+ }
3452
+ return statuses;
3453
+ }
3454
+
3916
3455
  //#endregion
3917
3456
  //#region src/commands/upgrade.ts
3918
3457
  const PRESERVE_FILES = ["opencode.json", ".env"];
@@ -4532,7 +4071,7 @@ function listPatches(opencodeDir) {
4532
4071
  showEmpty("patches", "ock patch create <file>");
4533
4072
  return;
4534
4073
  }
4535
- const statuses = checkPatchStatus(opencodeDir, getTemplateRoot$2());
4074
+ const statuses = checkPatchStatus(opencodeDir, getTemplateRoot$1());
4536
4075
  const statusMap = new Map(statuses.map((s) => [s.relativePath, s]));
4537
4076
  p.intro(color.bgCyan(color.black(` ${entries.length} patch${entries.length === 1 ? "" : "es"} `)));
4538
4077
  for (const [relativePath, entry] of entries) {
@@ -4566,7 +4105,7 @@ async function createPatch$1(opencodeDir) {
4566
4105
  notFound("file", relativePath);
4567
4106
  return;
4568
4107
  }
4569
- const templateRoot = getTemplateRoot$2();
4108
+ const templateRoot = getTemplateRoot$1();
4570
4109
  if (!templateRoot) {
4571
4110
  p.log.error("Cannot find template root — unable to compute diff");
4572
4111
  p.log.info(color.dim("Make sure ock is installed correctly"));
@@ -5922,7 +5461,7 @@ async function ensureLicenseFor(commandName) {
5922
5461
  cli.option("--verbose", "Enable verbose logging");
5923
5462
  cli.option("--quiet", "Suppress all output");
5924
5463
  cli.version(`${packageVersion}`);
5925
- cli.command("init", "Initialize OpenCodeKit in current directory").option("--force", "Reinitialize even if already exists").option("--global", "Install to global OpenCode config (~/.config/opencode/)").option("--free", "Use free models (default)").option("--recommend", "Use recommended premium models").option("-y, --yes", "Skip prompts, use defaults (for CI)").option("--backup", "Backup existing .opencode before overwriting").option("--prune", "Manually select orphan files to delete").option("--prune-all", "Auto-delete all orphan files").option("--project-only", "Only init project-scope files (skip if global config has agents/skills/commands/tools)").action(async (options) => {
5464
+ cli.command("init", "Initialize OpenCodeKit in current directory").option("--force", "Reinitialize even if already exists").option("--global", "Install to global OpenCode config (~/.config/opencode/)").option("-y, --yes", "Skip prompts, use defaults (for CI)").action(async (options) => {
5926
5465
  if (!await ensureLicenseFor("init")) return;
5927
5466
  await initCommand(options);
5928
5467
  });
@@ -5932,17 +5471,6 @@ cli.command("activate [key]", "Activate paid license key").action(async (key) =>
5932
5471
  cli.command("license [action]", "Manage license (status, deactivate)").action(async (action) => {
5933
5472
  await licenseCommand(action);
5934
5473
  });
5935
- cli.command("agent [action]", "Manage agents (list, add, view)").action(async (action) => {
5936
- if (!action) {
5937
- console.log("\nUsage: ock agent <action>\n");
5938
- console.log("Actions:");
5939
- console.log(" list List all agents");
5940
- console.log(" add Create a new agent");
5941
- console.log(" view View agent details\n");
5942
- return;
5943
- }
5944
- await agentCommand(action);
5945
- });
5946
5474
  cli.command("command [action]", "Manage slash commands (list, create, show, delete)").action(async (action) => {
5947
5475
  if (!action) {
5948
5476
  console.log("\nUsage: ock command <action>\n");
@@ -5955,14 +5483,15 @@ cli.command("command [action]", "Manage slash commands (list, create, show, dele
5955
5483
  }
5956
5484
  await commandCommand(action);
5957
5485
  });
5958
- cli.command("agent [action]", "Manage agents (list, create, view, remove)").action(async (action) => {
5486
+ cli.command("agent [action]", "Manage agents (list, create, show, delete, edit)").action(async (action) => {
5959
5487
  if (!action) {
5960
5488
  console.log("\nUsage: ock agent <action>\n");
5961
5489
  console.log("Actions:");
5962
5490
  console.log(" list List all agents");
5963
5491
  console.log(" create Create a new agent");
5964
- console.log(" view View agent details");
5965
- console.log(" remove Remove an agent\n");
5492
+ console.log(" show View agent details");
5493
+ console.log(" delete Remove an agent");
5494
+ console.log(" edit Edit an agent\n");
5966
5495
  return;
5967
5496
  }
5968
5497
  await agentCommand(action);