opencodekit 0.18.4 → 0.18.6

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 (43) hide show
  1. package/dist/index.js +491 -47
  2. package/dist/template/.opencode/AGENTS.md +13 -1
  3. package/dist/template/.opencode/agent/build.md +4 -1
  4. package/dist/template/.opencode/agent/explore.md +25 -58
  5. package/dist/template/.opencode/command/ship.md +7 -5
  6. package/dist/template/.opencode/command/verify.md +63 -12
  7. package/dist/template/.opencode/memory/research/benchmark-framework.md +162 -0
  8. package/dist/template/.opencode/memory/research/effectiveness-audit.md +213 -0
  9. package/dist/template/.opencode/memory.db +0 -0
  10. package/dist/template/.opencode/memory.db-shm +0 -0
  11. package/dist/template/.opencode/memory.db-wal +0 -0
  12. package/dist/template/.opencode/opencode.json +1429 -1678
  13. package/dist/template/.opencode/package.json +1 -1
  14. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +3 -129
  15. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +4 -60
  16. package/dist/template/.opencode/plugin/memory.ts +0 -3
  17. package/dist/template/.opencode/skill/agent-teams/SKILL.md +16 -1
  18. package/dist/template/.opencode/skill/beads/SKILL.md +22 -0
  19. package/dist/template/.opencode/skill/brainstorming/SKILL.md +28 -0
  20. package/dist/template/.opencode/skill/code-navigation/SKILL.md +130 -0
  21. package/dist/template/.opencode/skill/condition-based-waiting/SKILL.md +12 -0
  22. package/dist/template/.opencode/skill/context-management/SKILL.md +122 -113
  23. package/dist/template/.opencode/skill/defense-in-depth/SKILL.md +20 -0
  24. package/dist/template/.opencode/skill/design-system-audit/SKILL.md +113 -112
  25. package/dist/template/.opencode/skill/dispatching-parallel-agents/SKILL.md +8 -0
  26. package/dist/template/.opencode/skill/executing-plans/SKILL.md +156 -132
  27. package/dist/template/.opencode/skill/memory-system/SKILL.md +50 -266
  28. package/dist/template/.opencode/skill/mockup-to-code/SKILL.md +21 -6
  29. package/dist/template/.opencode/skill/receiving-code-review/SKILL.md +8 -0
  30. package/dist/template/.opencode/skill/root-cause-tracing/SKILL.md +15 -0
  31. package/dist/template/.opencode/skill/session-management/SKILL.md +4 -103
  32. package/dist/template/.opencode/skill/subagent-driven-development/SKILL.md +23 -2
  33. package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +17 -1
  34. package/dist/template/.opencode/skill/systematic-debugging/SKILL.md +21 -0
  35. package/dist/template/.opencode/skill/tool-priority/SKILL.md +34 -16
  36. package/dist/template/.opencode/skill/ui-ux-research/SKILL.md +5 -127
  37. package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +36 -0
  38. package/dist/template/.opencode/skill/verification-before-completion/references/VERIFICATION_PROTOCOL.md +133 -29
  39. package/dist/template/.opencode/skill/visual-analysis/SKILL.md +20 -7
  40. package/dist/template/.opencode/skill/writing-plans/SKILL.md +7 -0
  41. package/dist/template/.opencode/tool/context7.ts +9 -1
  42. package/dist/template/.opencode/tool/grepsearch.ts +9 -1
  43. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18,11 +18,35 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
18
 
19
19
  //#endregion
20
20
  //#region package.json
21
- var version = "0.18.4";
21
+ var version = "0.18.6";
22
22
 
23
23
  //#endregion
24
24
  //#region src/utils/errors.ts
25
25
  /**
26
+ * Resolve the .opencode directory path.
27
+ * Handles two cases:
28
+ * 1. Standard project: cwd has .opencode/ subdirectory
29
+ * 2. Global config dir: cwd IS the opencode config dir (has opencode.json directly)
30
+ * Returns null if neither case applies.
31
+ */
32
+ function resolveOpencodePath() {
33
+ const nested = join(process.cwd(), ".opencode");
34
+ if (existsSync(nested)) return nested;
35
+ if (existsSync(join(process.cwd(), "opencode.json"))) return process.cwd();
36
+ return null;
37
+ }
38
+ /**
39
+ * Resolve opencode path or show "not initialized" error and return null.
40
+ */
41
+ function requireOpencodePath() {
42
+ const resolved = resolveOpencodePath();
43
+ if (!resolved) {
44
+ notInitialized();
45
+ return null;
46
+ }
47
+ return resolved;
48
+ }
49
+ /**
26
50
  * Display a styled error message with optional fix suggestion
27
51
  */
28
52
  function showError(message, fix) {
@@ -79,7 +103,8 @@ const InitOptionsSchema = z.object({
79
103
  yes: z.boolean().optional().default(false),
80
104
  backup: z.boolean().optional().default(false),
81
105
  prune: z.boolean().optional().default(false),
82
- pruneAll: z.boolean().optional().default(false)
106
+ pruneAll: z.boolean().optional().default(false),
107
+ projectOnly: z.boolean().optional().default(false)
83
108
  });
84
109
  const UpgradeOptionsSchema = z.object({
85
110
  force: z.boolean().optional().default(false),
@@ -153,6 +178,15 @@ const ToolActionSchema = z.enum([
153
178
  "delete"
154
179
  ]);
155
180
  const ToolOptionsSchema = z.object({ json: z.boolean().optional().default(false) });
181
+ const PatchActionSchema = z.enum([
182
+ "list",
183
+ "create",
184
+ "apply",
185
+ "diff",
186
+ "remove",
187
+ "disable",
188
+ "enable"
189
+ ]);
156
190
  const CompletionShellSchema = z.enum([
157
191
  "bash",
158
192
  "zsh",
@@ -183,11 +217,8 @@ function parseAction(schema, action) {
183
217
  //#region src/commands/agent.ts
184
218
  async function agentCommand(action) {
185
219
  const validatedAction = parseAction(AgentActionSchema, action);
186
- const opencodePath = join(process.cwd(), ".opencode");
187
- if (!existsSync(opencodePath)) {
188
- notInitialized();
189
- return;
190
- }
220
+ const opencodePath = requireOpencodePath();
221
+ if (!opencodePath) return;
191
222
  const agentPath = join(opencodePath, "agent");
192
223
  switch (validatedAction) {
193
224
  case "list":
@@ -464,11 +495,8 @@ async function removeAgent(agentPath, agentNameArg) {
464
495
  //#region src/commands/command.ts
465
496
  async function commandCommand(action) {
466
497
  const validatedAction = parseAction(CommandActionSchema, action);
467
- const opencodePath = join(process.cwd(), ".opencode");
468
- if (!existsSync(opencodePath)) {
469
- notInitialized();
470
- return;
471
- }
498
+ const opencodePath = requireOpencodePath();
499
+ if (!opencodePath) return;
472
500
  const commandDir = join(opencodePath, "command");
473
501
  switch (validatedAction) {
474
502
  case "list":
@@ -973,12 +1001,9 @@ async function getAgentsFromServer() {
973
1001
  return fetchFromServer("/agent");
974
1002
  }
975
1003
  async function configCommand(action) {
976
- const opencodePath = join(process.cwd(), ".opencode");
1004
+ const opencodePath = requireOpencodePath();
1005
+ if (!opencodePath) return;
977
1006
  const configPath = join(opencodePath, "opencode.json");
978
- if (!existsSync(opencodePath)) {
979
- notInitialized();
980
- return;
981
- }
982
1007
  if (!existsSync(configPath)) {
983
1008
  showError("opencode.json not found", "ock init --force");
984
1009
  return;
@@ -2515,6 +2540,22 @@ function getPatchesDir(opencodeDir) {
2515
2540
  return join(opencodeDir, PATCHES_DIR);
2516
2541
  }
2517
2542
  /**
2543
+ * Get the template root directory (from dist/template or dev mode).
2544
+ */
2545
+ function getTemplateRoot$2() {
2546
+ const __dirname = dirname(fileURLToPath(import.meta.url));
2547
+ const possiblePaths = [
2548
+ join(__dirname, "template"),
2549
+ join(__dirname, "..", "..", ".opencode"),
2550
+ join(__dirname, "..", "template")
2551
+ ];
2552
+ for (const path of possiblePaths) {
2553
+ if (existsSync(join(path, ".opencode"))) return path;
2554
+ if (existsSync(join(path, "opencode.json"))) return dirname(path);
2555
+ }
2556
+ return null;
2557
+ }
2558
+ /**
2518
2559
  * Load patch metadata from .patches.json.
2519
2560
  */
2520
2561
  function loadPatchMetadata(opencodeDir) {
@@ -2581,6 +2622,22 @@ function savePatch(opencodeDir, relativePath, templateContent, userContent) {
2581
2622
  return entry;
2582
2623
  }
2583
2624
  /**
2625
+ * Remove a patch for a file.
2626
+ */
2627
+ function removePatch(opencodeDir, relativePath) {
2628
+ const metadata = loadPatchMetadata(opencodeDir);
2629
+ const entry = metadata.patches[relativePath];
2630
+ if (!entry) return false;
2631
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
2632
+ if (existsSync(patchPath)) {
2633
+ const { rmSync } = __require("node:fs");
2634
+ rmSync(patchPath);
2635
+ }
2636
+ delete metadata.patches[relativePath];
2637
+ savePatchMetadata(opencodeDir, metadata);
2638
+ return true;
2639
+ }
2640
+ /**
2584
2641
  * Apply a patch to file content.
2585
2642
  * @returns The patched content, or null if patch failed.
2586
2643
  */
@@ -2595,6 +2652,14 @@ function applyAllPatches(opencodeDir) {
2595
2652
  const patchesDir = getPatchesDir(opencodeDir);
2596
2653
  const results = [];
2597
2654
  for (const [relativePath, entry] of Object.entries(metadata.patches)) {
2655
+ if (entry.disabled) {
2656
+ results.push({
2657
+ success: true,
2658
+ file: relativePath,
2659
+ message: "Skipped (disabled)"
2660
+ });
2661
+ continue;
2662
+ }
2598
2663
  const filePath = join(opencodeDir, relativePath);
2599
2664
  const patchPath = join(patchesDir, entry.patchFile);
2600
2665
  if (!existsSync(filePath)) {
@@ -2637,6 +2702,59 @@ function applyAllPatches(opencodeDir) {
2637
2702
  }
2638
2703
  return results;
2639
2704
  }
2705
+ /**
2706
+ * Check the status of all patches.
2707
+ */
2708
+ function checkPatchStatus(opencodeDir, templateRoot) {
2709
+ const metadata = loadPatchMetadata(opencodeDir);
2710
+ const statuses = [];
2711
+ for (const [relativePath, entry] of Object.entries(metadata.patches)) {
2712
+ if (!existsSync(join(opencodeDir, relativePath))) {
2713
+ statuses.push({
2714
+ relativePath,
2715
+ entry,
2716
+ status: "missing",
2717
+ message: "User file no longer exists"
2718
+ });
2719
+ continue;
2720
+ }
2721
+ if (templateRoot) {
2722
+ const templateFilePath = join(templateRoot, ".opencode", relativePath);
2723
+ if (existsSync(templateFilePath)) {
2724
+ const templateContent = readFileSync(templateFilePath, "utf-8");
2725
+ if (calculateHash(templateContent) !== entry.originalHash) {
2726
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
2727
+ if (existsSync(patchPath)) if (applyPatch$1(templateContent, readFileSync(patchPath, "utf-8")) === false) statuses.push({
2728
+ relativePath,
2729
+ entry,
2730
+ status: "conflict",
2731
+ message: "Template changed and patch cannot apply cleanly"
2732
+ });
2733
+ else statuses.push({
2734
+ relativePath,
2735
+ entry,
2736
+ status: "stale",
2737
+ message: "Template changed but patch can still apply"
2738
+ });
2739
+ else statuses.push({
2740
+ relativePath,
2741
+ entry,
2742
+ status: "missing",
2743
+ message: "Patch file missing"
2744
+ });
2745
+ continue;
2746
+ }
2747
+ }
2748
+ }
2749
+ statuses.push({
2750
+ relativePath,
2751
+ entry,
2752
+ status: "clean",
2753
+ message: "Patch is up to date"
2754
+ });
2755
+ }
2756
+ return statuses;
2757
+ }
2640
2758
 
2641
2759
  //#endregion
2642
2760
  //#region src/commands/init.ts
@@ -2656,6 +2774,34 @@ const EXCLUDED_FILES = [
2656
2774
  "pnpm-lock.yaml"
2657
2775
  ];
2658
2776
  const PRESERVE_USER_DIRS = ["memory/project", "context"];
2777
+ const SHARED_CONFIG_DIRS = [
2778
+ "agent",
2779
+ "command",
2780
+ "skill",
2781
+ "tool"
2782
+ ];
2783
+ /**
2784
+ * Detect if global config has any of the shared dirs populated.
2785
+ * Returns null if no global config or no shared dirs found.
2786
+ */
2787
+ function detectGlobalConfig() {
2788
+ const globalDir = getGlobalConfigDir();
2789
+ if (!existsSync(globalDir)) return null;
2790
+ const coveredDirs = SHARED_CONFIG_DIRS.filter((d) => {
2791
+ const dirPath = join(globalDir, d);
2792
+ if (!existsSync(dirPath)) return false;
2793
+ try {
2794
+ return readdirSync(dirPath).filter((e) => !e.startsWith(".")).length > 0;
2795
+ } catch {
2796
+ return false;
2797
+ }
2798
+ });
2799
+ if (coveredDirs.length === 0) return null;
2800
+ return {
2801
+ dir: globalDir,
2802
+ coveredDirs
2803
+ };
2804
+ }
2659
2805
  /**
2660
2806
  * Get the global OpenCode config directory based on OS.
2661
2807
  * - macOS/Linux: ~/.config/opencode/ (respects XDG_CONFIG_HOME)
@@ -2699,10 +2845,25 @@ async function copyDir(src, dest) {
2699
2845
  else writeFileSync(destPath, readFileSync(srcPath, "utf-8"));
2700
2846
  }
2701
2847
  }
2702
- async function copyOpenCodeOnly(templateRoot, targetDir) {
2848
+ async function copyOpenCodeOnly(templateRoot, targetDir, skipDirs) {
2703
2849
  const opencodeSrc = join(templateRoot, ".opencode");
2704
2850
  const opencodeDest = join(targetDir, ".opencode");
2705
2851
  if (!existsSync(opencodeSrc)) return false;
2852
+ if (skipDirs && skipDirs.length > 0) {
2853
+ const skipSet = new Set(skipDirs);
2854
+ mkdirSync(opencodeDest, { recursive: true });
2855
+ for (const entry of readdirSync(opencodeSrc, { withFileTypes: true })) {
2856
+ if (EXCLUDED_DIRS.includes(entry.name)) continue;
2857
+ if (!entry.isDirectory() && EXCLUDED_FILES.includes(entry.name)) continue;
2858
+ if (entry.isSymbolicLink()) continue;
2859
+ if (entry.isDirectory() && skipSet.has(entry.name)) continue;
2860
+ const srcPath = join(opencodeSrc, entry.name);
2861
+ const destPath = join(opencodeDest, entry.name);
2862
+ if (entry.isDirectory()) await copyDir(srcPath, destPath);
2863
+ else writeFileSync(destPath, readFileSync(srcPath, "utf-8"));
2864
+ }
2865
+ return true;
2866
+ }
2706
2867
  await copyDir(opencodeSrc, opencodeDest);
2707
2868
  return true;
2708
2869
  }
@@ -2998,18 +3159,36 @@ async function initCommand(rawOptions = {}) {
2998
3159
  }
2999
3160
  projectName = name || projectName;
3000
3161
  }
3162
+ let skipDirs = [];
3163
+ if (!options.global) {
3164
+ const globalConfig = detectGlobalConfig();
3165
+ if (globalConfig && options.projectOnly) {
3166
+ skipDirs = globalConfig.coveredDirs;
3167
+ p.log.info(`Using global config from ${color.cyan(globalConfig.dir)}`);
3168
+ p.log.info(`Skipping: ${skipDirs.map((d) => color.dim(d)).join(", ")}`);
3169
+ } else if (globalConfig && !options.yes) {
3170
+ p.log.info(`Global config found at ${color.cyan(globalConfig.dir)}`);
3171
+ p.log.info(`Available globally: ${globalConfig.coveredDirs.map((d) => color.green(d)).join(", ")}`);
3172
+ const useGlobal = await p.confirm({
3173
+ message: "Skip these (use global config)? Only project-scope files will be created locally.",
3174
+ initialValue: true
3175
+ });
3176
+ if (!p.isCancel(useGlobal) && useGlobal) skipDirs = globalConfig.coveredDirs;
3177
+ } 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`);
3178
+ }
3001
3179
  const s = p.spinner();
3002
3180
  if (mode === "scaffold") {
3003
3181
  s.start("Scaffolding project");
3004
3182
  mkdirSync(targetDir, { recursive: true });
3005
3183
  } else if (mode === "add-config") s.start("Adding OpenCodeKit");
3006
3184
  else s.start("Reinitializing");
3007
- if (!await copyOpenCodeOnly(templateRoot, targetDir)) {
3185
+ if (!await copyOpenCodeOnly(templateRoot, targetDir, skipDirs)) {
3008
3186
  s.stop("Failed");
3009
3187
  p.outro(color.red("Template copy failed"));
3010
3188
  process.exit(1);
3011
3189
  }
3012
3190
  s.stop("Done");
3191
+ if (skipDirs.length > 0) p.log.info(`Project-only init: skipped ${skipDirs.map((d) => color.dim(d)).join(", ")} (using global config)`);
3013
3192
  const restoredFileCount = finalizeInstalledFiles(targetDir, getPackageVersion$1(), preservedFiles);
3014
3193
  if (restoredFileCount > 0) p.log.info(`Preserved ${restoredFileCount} user memory files (memory/project/)`);
3015
3194
  if (options.free) {
@@ -3178,11 +3357,8 @@ async function initCommand(rawOptions = {}) {
3178
3357
  //#region src/commands/skill.ts
3179
3358
  async function skillCommand(action) {
3180
3359
  const validatedAction = parseAction(SkillActionSchema, action);
3181
- const opencodePath = join(process.cwd(), ".opencode");
3182
- if (!existsSync(opencodePath)) {
3183
- notInitialized();
3184
- return;
3185
- }
3360
+ const opencodePath = requireOpencodePath();
3361
+ if (!opencodePath) return;
3186
3362
  const skillDir = join(opencodePath, "skill");
3187
3363
  switch (validatedAction) {
3188
3364
  case "list":
@@ -3498,7 +3674,7 @@ function copyDirWithPreserve(src, dest, preserveFiles, preserveDirs, manifest, b
3498
3674
  const destPath = join(dest, entry.name);
3499
3675
  if (entry.isDirectory()) if (preserveDirs.includes(entry.name)) {
3500
3676
  if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
3501
- const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, entry.name);
3677
+ const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, dest, entry.name);
3502
3678
  added.push(...subResult.added);
3503
3679
  updated.push(...subResult.updated);
3504
3680
  preserved.push(...subResult.preserved);
@@ -3524,7 +3700,7 @@ function copyDirWithPreserve(src, dest, preserveFiles, preserveDirs, manifest, b
3524
3700
  preserved
3525
3701
  };
3526
3702
  }
3527
- function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
3703
+ function copyDirPreserveExisting(src, dest, manifest, opencodeDir, basePath = "") {
3528
3704
  const added = [];
3529
3705
  const updated = [];
3530
3706
  const preserved = [];
@@ -3534,7 +3710,7 @@ function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
3534
3710
  const destPath = join(dest, entry.name);
3535
3711
  if (entry.isDirectory()) {
3536
3712
  if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
3537
- const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, join(basePath, entry.name));
3713
+ const subResult = copyDirPreserveExisting(srcPath, destPath, manifest, opencodeDir, join(basePath, entry.name));
3538
3714
  added.push(...subResult.added);
3539
3715
  updated.push(...subResult.updated);
3540
3716
  preserved.push(...subResult.preserved);
@@ -3546,6 +3722,9 @@ function copyDirPreserveExisting(src, dest, manifest, basePath = "") {
3546
3722
  } else if (fileModificationStatus(destPath, relativePath, manifest) === "unmodified") {
3547
3723
  copyFileSync(srcPath, destPath);
3548
3724
  updated.push(relativePath);
3725
+ } else if (loadPatchMetadata(opencodeDir).patches[relativePath]) {
3726
+ copyFileSync(srcPath, destPath);
3727
+ updated.push(relativePath);
3549
3728
  } else preserved.push(relativePath);
3550
3729
  }
3551
3730
  }
@@ -3579,12 +3758,9 @@ function findUpgradeOrphans(installedFiles, templateFiles) {
3579
3758
  async function upgradeCommand(rawOptions = {}) {
3580
3759
  const options = parseOptions(UpgradeOptionsSchema, rawOptions);
3581
3760
  if (process.argv.includes("--quiet")) return;
3582
- const opencodeDir = join(process.cwd(), ".opencode");
3761
+ const opencodeDir = requireOpencodePath();
3762
+ if (!opencodeDir) return;
3583
3763
  const manifest = loadManifest(opencodeDir);
3584
- if (!existsSync(opencodeDir)) {
3585
- notInitialized();
3586
- return;
3587
- }
3588
3764
  p.intro(color.bgCyan(color.black(" Upgrade ")));
3589
3765
  const versionInfo = await checkVersion(opencodeDir);
3590
3766
  console.log();
@@ -3646,7 +3822,10 @@ async function upgradeCommand(rawOptions = {}) {
3646
3822
  }
3647
3823
  if (result.updated.length > 0) p.log.success(`Updated ${result.updated.length} files`);
3648
3824
  if (result.added.length > 0) p.log.success(`Added ${result.added.length} files`);
3649
- if (result.preserved.length > 0) p.log.info(`Preserved ${result.preserved.length} user files`);
3825
+ if (result.preserved.length > 0) {
3826
+ p.log.info(`Preserved ${result.preserved.length} user files`);
3827
+ p.log.info(color.dim(" Tip: Run 'ock patch create <file>' to save customizations as reapplyable patches"));
3828
+ }
3650
3829
  if (patchResults.success > 0) p.log.success(`Reapplied ${patchResults.success} patches`);
3651
3830
  const orphans = findUpgradeOrphans(getAllFiles(opencodeDir), getAllFiles(templateOpencode));
3652
3831
  if (orphans.length > 0) {
@@ -4012,11 +4191,8 @@ function displayChecks(checks) {
4012
4191
  async function statusCommand() {
4013
4192
  if (process.argv.includes("--quiet")) return;
4014
4193
  const cwd = process.cwd();
4015
- const opencodeDir = join(cwd, ".opencode");
4016
- if (!existsSync(opencodeDir)) {
4017
- notInitialized();
4018
- return;
4019
- }
4194
+ const opencodeDir = requireOpencodePath();
4195
+ if (!opencodeDir) return;
4020
4196
  const projectName = basename(cwd);
4021
4197
  p.intro(color.bgCyan(color.black(` ${projectName} `)));
4022
4198
  const agentDir = join(opencodeDir, "agent");
@@ -4053,6 +4229,277 @@ async function statusCommand() {
4053
4229
  p.outro(color.dim(".opencode/"));
4054
4230
  }
4055
4231
 
4232
+ //#endregion
4233
+ //#region src/commands/patch.ts
4234
+ function listPatches(opencodeDir) {
4235
+ const metadata = loadPatchMetadata(opencodeDir);
4236
+ const entries = Object.entries(metadata.patches);
4237
+ if (entries.length === 0) {
4238
+ showEmpty("patches", "ock patch create <file>");
4239
+ return;
4240
+ }
4241
+ const statuses = checkPatchStatus(opencodeDir, getTemplateRoot$2());
4242
+ const statusMap = new Map(statuses.map((s) => [s.relativePath, s]));
4243
+ p.intro(color.bgCyan(color.black(` ${entries.length} patch${entries.length === 1 ? "" : "es"} `)));
4244
+ for (const [relativePath, entry] of entries) {
4245
+ const ps = statusMap.get(relativePath);
4246
+ const statusLabel = ps ? formatStatus(ps.status) : color.dim("unknown");
4247
+ const disabledLabel = entry.disabled ? color.yellow(" [disabled]") : "";
4248
+ const descLabel = entry.description ? color.dim(` — ${entry.description}`) : "";
4249
+ p.log.info(`${color.cyan(relativePath)}${disabledLabel}${descLabel}\n Status: ${statusLabel} Created: ${color.dim(entry.createdAt.slice(0, 10))} Version: ${color.dim(entry.templateVersion)}`);
4250
+ }
4251
+ p.outro(color.dim("Use 'ock patch diff <file>' to view changes"));
4252
+ }
4253
+ function formatStatus(status) {
4254
+ switch (status) {
4255
+ case "clean": return color.green("clean");
4256
+ case "stale": return color.yellow("stale");
4257
+ case "conflict": return color.red("conflict");
4258
+ case "missing": return color.red("missing");
4259
+ default: return color.dim(status);
4260
+ }
4261
+ }
4262
+ async function createPatch$1(opencodeDir) {
4263
+ const fileArg = process.argv[4];
4264
+ if (!fileArg) {
4265
+ p.log.error("Usage: ock patch create <file>");
4266
+ p.log.info(color.dim("File path is relative to .opencode/ (e.g., skill/beads/SKILL.md)"));
4267
+ return;
4268
+ }
4269
+ const relativePath = fileArg;
4270
+ const userFilePath = join(opencodeDir, relativePath);
4271
+ if (!existsSync(userFilePath)) {
4272
+ notFound("file", relativePath);
4273
+ return;
4274
+ }
4275
+ const templateRoot = getTemplateRoot$2();
4276
+ if (!templateRoot) {
4277
+ p.log.error("Cannot find template root — unable to compute diff");
4278
+ p.log.info(color.dim("Make sure ock is installed correctly"));
4279
+ return;
4280
+ }
4281
+ const templateFilePath = join(templateRoot, ".opencode", relativePath);
4282
+ if (!existsSync(templateFilePath)) {
4283
+ p.log.error(`No template file for ${color.cyan(relativePath)}`);
4284
+ p.log.info(color.dim("Only template-originated files can be patched"));
4285
+ return;
4286
+ }
4287
+ const templateContent = readFileSync(templateFilePath, "utf-8");
4288
+ const userContent = readFileSync(userFilePath, "utf-8");
4289
+ if (calculateHash(templateContent) === calculateHash(userContent)) {
4290
+ p.log.warn(`${color.cyan(relativePath)} is identical to template — nothing to patch`);
4291
+ return;
4292
+ }
4293
+ if (loadPatchMetadata(opencodeDir).patches[relativePath]) {
4294
+ const overwrite = await p.confirm({
4295
+ message: `Patch already exists for ${color.cyan(relativePath)}. Overwrite?`,
4296
+ initialValue: false
4297
+ });
4298
+ if (p.isCancel(overwrite) || !overwrite) {
4299
+ p.cancel("Cancelled");
4300
+ return;
4301
+ }
4302
+ }
4303
+ const description = await p.text({
4304
+ message: "Description (optional)",
4305
+ placeholder: "e.g., Custom agent prompt for our team"
4306
+ });
4307
+ if (p.isCancel(description)) {
4308
+ p.cancel("Cancelled");
4309
+ return;
4310
+ }
4311
+ const entry = savePatch(opencodeDir, relativePath, templateContent, userContent);
4312
+ if (description && typeof description === "string" && description.trim()) {
4313
+ entry.description = description.trim();
4314
+ const updatedMetadata = loadPatchMetadata(opencodeDir);
4315
+ updatedMetadata.patches[relativePath] = entry;
4316
+ savePatchMetadata(opencodeDir, updatedMetadata);
4317
+ }
4318
+ p.log.success(`Created patch for ${color.cyan(relativePath)}`);
4319
+ p.log.info(color.dim(`Patch file: ${entry.patchFile}`));
4320
+ }
4321
+ function applyPatches(opencodeDir) {
4322
+ const fileArg = process.argv[4];
4323
+ const metadata = loadPatchMetadata(opencodeDir);
4324
+ if (Object.entries(metadata.patches).length === 0) {
4325
+ showEmpty("patches", "ock patch create <file>");
4326
+ return;
4327
+ }
4328
+ if (fileArg) {
4329
+ const entry = metadata.patches[fileArg];
4330
+ if (!entry) {
4331
+ notFound("patch", fileArg);
4332
+ return;
4333
+ }
4334
+ if (entry.disabled) {
4335
+ p.log.warn(`Patch for ${color.cyan(fileArg)} is disabled — enable it first`);
4336
+ return;
4337
+ }
4338
+ applySinglePatch(opencodeDir, fileArg, entry);
4339
+ return;
4340
+ }
4341
+ const results = applyAllPatches(opencodeDir);
4342
+ const success = results.filter((r) => r.success && r.message !== "Skipped (disabled)").length;
4343
+ const skipped = results.filter((r) => r.message === "Skipped (disabled)").length;
4344
+ const conflicts = results.filter((r) => r.conflict).length;
4345
+ if (success > 0) p.log.success(`Applied ${success} patch${success === 1 ? "" : "es"}`);
4346
+ if (skipped > 0) p.log.info(color.dim(`Skipped ${skipped} disabled patch${skipped === 1 ? "" : "es"}`));
4347
+ if (conflicts > 0) p.log.warn(`${conflicts} conflict${conflicts === 1 ? "" : "s"} — see .rej files in ${color.cyan(".opencode/patches/")}`);
4348
+ if (success === 0 && conflicts === 0 && skipped === 0) p.log.info("No patches to apply");
4349
+ }
4350
+ function applySinglePatch(opencodeDir, relativePath, entry) {
4351
+ const filePath = join(opencodeDir, relativePath);
4352
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
4353
+ if (!existsSync(filePath)) {
4354
+ p.log.error(`Target file missing: ${color.cyan(relativePath)}`);
4355
+ return;
4356
+ }
4357
+ if (!existsSync(patchPath)) {
4358
+ p.log.error(`Patch file missing: ${color.cyan(entry.patchFile)}`);
4359
+ return;
4360
+ }
4361
+ const result = applyPatch$1(readFileSync(filePath, "utf-8"), readFileSync(patchPath, "utf-8"));
4362
+ if (result === false) {
4363
+ p.log.error(`Conflict applying patch to ${color.cyan(relativePath)}`);
4364
+ p.log.info(color.dim("Template may have changed — try 'ock patch create' to recreate"));
4365
+ return;
4366
+ }
4367
+ writeFileSync(filePath, result, "utf-8");
4368
+ p.log.success(`Applied patch to ${color.cyan(relativePath)}`);
4369
+ }
4370
+ function showDiff(opencodeDir) {
4371
+ const fileArg = process.argv[4];
4372
+ if (!fileArg) {
4373
+ const metadata = loadPatchMetadata(opencodeDir);
4374
+ const entries = Object.entries(metadata.patches);
4375
+ if (entries.length === 0) {
4376
+ showEmpty("patches", "ock patch create <file>");
4377
+ return;
4378
+ }
4379
+ for (const [relativePath, entry] of entries) showSingleDiff(opencodeDir, relativePath, entry);
4380
+ return;
4381
+ }
4382
+ const entry = loadPatchMetadata(opencodeDir).patches[fileArg];
4383
+ if (!entry) {
4384
+ notFound("patch", fileArg);
4385
+ return;
4386
+ }
4387
+ showSingleDiff(opencodeDir, fileArg, entry);
4388
+ }
4389
+ function showSingleDiff(opencodeDir, relativePath, entry) {
4390
+ const patchPath = join(getPatchesDir(opencodeDir), entry.patchFile);
4391
+ if (!existsSync(patchPath)) {
4392
+ p.log.error(`Patch file missing: ${color.cyan(entry.patchFile)}`);
4393
+ return;
4394
+ }
4395
+ const patchContent = readFileSync(patchPath, "utf-8");
4396
+ const disabledLabel = entry.disabled ? color.yellow(" [disabled]") : "";
4397
+ console.log(`\n${color.bold(color.cyan(relativePath))}${disabledLabel}`);
4398
+ if (entry.description) console.log(color.dim(` ${entry.description}`));
4399
+ console.log(color.dim("─".repeat(60)));
4400
+ for (const line of patchContent.split("\n")) if (line.startsWith("+++") || line.startsWith("---")) console.log(color.bold(line));
4401
+ else if (line.startsWith("+")) console.log(color.green(line));
4402
+ else if (line.startsWith("-")) console.log(color.red(line));
4403
+ else if (line.startsWith("@@")) console.log(color.cyan(line));
4404
+ else console.log(color.dim(line));
4405
+ console.log();
4406
+ }
4407
+ async function removePatchCmd(opencodeDir) {
4408
+ const fileArg = process.argv[4];
4409
+ if (!fileArg) {
4410
+ p.log.error("Usage: ock patch remove <file>");
4411
+ p.log.info(color.dim("Use 'ock patch list' to see available patches"));
4412
+ return;
4413
+ }
4414
+ if (!loadPatchMetadata(opencodeDir).patches[fileArg]) {
4415
+ notFound("patch", fileArg);
4416
+ return;
4417
+ }
4418
+ const confirm = await p.confirm({
4419
+ message: `Remove patch for ${color.cyan(fileArg)}?`,
4420
+ initialValue: false
4421
+ });
4422
+ if (p.isCancel(confirm) || !confirm) {
4423
+ p.cancel("Cancelled");
4424
+ return;
4425
+ }
4426
+ if (removePatch(opencodeDir, fileArg)) p.log.success(`Removed patch for ${color.cyan(fileArg)}`);
4427
+ else p.log.error(`Failed to remove patch for ${color.cyan(fileArg)}`);
4428
+ }
4429
+ function togglePatch(opencodeDir, disable) {
4430
+ const fileArg = process.argv[4];
4431
+ if (!fileArg) {
4432
+ p.log.error(`Usage: ock patch ${disable ? "disable" : "enable"} <file>`);
4433
+ p.log.info(color.dim("Use 'ock patch list' to see available patches"));
4434
+ return;
4435
+ }
4436
+ const metadata = loadPatchMetadata(opencodeDir);
4437
+ const entry = metadata.patches[fileArg];
4438
+ if (!entry) {
4439
+ notFound("patch", fileArg);
4440
+ return;
4441
+ }
4442
+ if (disable && entry.disabled) {
4443
+ p.log.warn(`Patch for ${color.cyan(fileArg)} is already disabled`);
4444
+ return;
4445
+ }
4446
+ if (!disable && !entry.disabled) {
4447
+ p.log.warn(`Patch for ${color.cyan(fileArg)} is already enabled`);
4448
+ return;
4449
+ }
4450
+ entry.disabled = disable || void 0;
4451
+ metadata.patches[fileArg] = entry;
4452
+ savePatchMetadata(opencodeDir, metadata);
4453
+ if (disable) {
4454
+ p.log.success(`Disabled patch for ${color.cyan(fileArg)}`);
4455
+ p.log.info(color.dim("This patch will be skipped during upgrades"));
4456
+ } else {
4457
+ p.log.success(`Enabled patch for ${color.cyan(fileArg)}`);
4458
+ p.log.info(color.dim("This patch will be applied during upgrades"));
4459
+ }
4460
+ }
4461
+ async function patchCommand(action) {
4462
+ const opencodeDir = requireOpencodePath();
4463
+ if (!opencodeDir) return;
4464
+ const validatedAction = parseAction(PatchActionSchema, action);
4465
+ if (!validatedAction) {
4466
+ listPatches(opencodeDir);
4467
+ return;
4468
+ }
4469
+ switch (validatedAction) {
4470
+ case "list":
4471
+ listPatches(opencodeDir);
4472
+ break;
4473
+ case "create":
4474
+ await createPatch$1(opencodeDir);
4475
+ break;
4476
+ case "apply":
4477
+ applyPatches(opencodeDir);
4478
+ break;
4479
+ case "diff":
4480
+ showDiff(opencodeDir);
4481
+ break;
4482
+ case "remove":
4483
+ await removePatchCmd(opencodeDir);
4484
+ break;
4485
+ case "disable":
4486
+ togglePatch(opencodeDir, true);
4487
+ break;
4488
+ case "enable":
4489
+ togglePatch(opencodeDir, false);
4490
+ break;
4491
+ default: unknownAction(action ?? "", [
4492
+ "list",
4493
+ "create",
4494
+ "apply",
4495
+ "diff",
4496
+ "remove",
4497
+ "disable",
4498
+ "enable"
4499
+ ]);
4500
+ }
4501
+ }
4502
+
4056
4503
  //#endregion
4057
4504
  //#region src/tui/utils/keyboard.ts
4058
4505
  /**
@@ -5029,7 +5476,7 @@ function stripAnsi(str) {
5029
5476
  //#region src/tui/hooks/useData.ts
5030
5477
  function loadProjectData() {
5031
5478
  const cwd = process.cwd();
5032
- const opencodeDir = join(cwd, ".opencode");
5479
+ const opencodeDir = resolveOpencodePath() ?? join(cwd, ".opencode");
5033
5480
  const projectName = basename(cwd);
5034
5481
  const agentDir = join(opencodeDir, "agent");
5035
5482
  let agents = [];
@@ -5058,9 +5505,6 @@ function loadProjectData() {
5058
5505
  mcpServers
5059
5506
  };
5060
5507
  }
5061
- function isInitialized() {
5062
- return existsSync(join(process.cwd(), ".opencode"));
5063
- }
5064
5508
 
5065
5509
  //#endregion
5066
5510
  //#region src/tui/index.ts
@@ -5068,10 +5512,7 @@ function isInitialized() {
5068
5512
  * Launch the TUI dashboard with interactive navigation.
5069
5513
  */
5070
5514
  async function launchTUI() {
5071
- if (!isInitialized()) {
5072
- notInitialized();
5073
- return;
5074
- }
5515
+ if (!requireOpencodePath()) return;
5075
5516
  await mainMenu();
5076
5517
  }
5077
5518
  async function mainMenu() {
@@ -5174,7 +5615,7 @@ const cli = cac("ock");
5174
5615
  cli.option("--verbose", "Enable verbose logging");
5175
5616
  cli.option("--quiet", "Suppress all output");
5176
5617
  cli.version(`${packageVersion}`);
5177
- cli.command("init", "Initialize OpenCodeKit in current directory").option("--force", "Reinitialize even if already exists").option("--beads", "Also initialize .beads/ for multi-agent coordination").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").action(initCommand);
5618
+ cli.command("init", "Initialize OpenCodeKit in current directory").option("--force", "Reinitialize even if already exists").option("--beads", "Also initialize .beads/ for multi-agent coordination").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(initCommand);
5178
5619
  cli.command("agent [action]", "Manage agents (list, add, view)").action(async (action) => {
5179
5620
  if (!action) {
5180
5621
  console.log("\nUsage: ock agent <action>\n");
@@ -5218,6 +5659,9 @@ cli.command("config [action]", "Edit opencode.json (model, mcp, permission, vali
5218
5659
  cli.command("upgrade", "Update .opencode/ templates to latest version").option("--force", "Force upgrade even if already up to date").option("--check", "Check for updates without upgrading").option("--prune", "Manually select orphan files to delete").option("--prune-all", "Auto-delete all orphan files").action(async (options) => {
5219
5660
  await upgradeCommand(options);
5220
5661
  });
5662
+ cli.command("patch [action]", "Manage template patches (list, create, apply, diff, remove, disable, enable)").action(async (action) => {
5663
+ await patchCommand(action);
5664
+ });
5221
5665
  cli.command("completion [shell]", "Generate shell completion script (bash, zsh, fish)").action(async (shell) => {
5222
5666
  await completionCommand(shell);
5223
5667
  });