oh-my-opencode-slim 2.0.5 → 2.1.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.
Files changed (52) hide show
  1. package/README.ja-JP.md +47 -21
  2. package/README.ko-KR.md +45 -19
  3. package/README.md +56 -32
  4. package/README.zh-CN.md +48 -24
  5. package/dist/agents/council.d.ts +1 -1
  6. package/dist/agents/index.d.ts +7 -2
  7. package/dist/agents/orchestrator.d.ts +1 -1
  8. package/dist/cli/custom-skills-registry.d.ts +18 -0
  9. package/dist/cli/custom-skills.d.ts +3 -19
  10. package/dist/cli/index.js +1089 -185
  11. package/dist/cli/providers.d.ts +5 -9
  12. package/dist/cli/skills.d.ts +2 -2
  13. package/dist/companion/manager.d.ts +7 -0
  14. package/dist/config/loader.d.ts +5 -2
  15. package/dist/config/schema.d.ts +4 -0
  16. package/dist/council/council-manager.d.ts +1 -1
  17. package/dist/hooks/auto-update-checker/skill-sync.d.ts +59 -1
  18. package/dist/hooks/filter-available-skills/index.d.ts +1 -2
  19. package/dist/hooks/foreground-fallback/index.d.ts +5 -1
  20. package/dist/hooks/image-hook.d.ts +1 -1
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/loop-command/index.d.ts +13 -0
  23. package/dist/hooks/phase-reminder/index.d.ts +1 -2
  24. package/dist/hooks/task-session-manager/index.d.ts +1 -2
  25. package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
  26. package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
  27. package/dist/hooks/types.d.ts +3 -1
  28. package/dist/index.js +2116 -670
  29. package/dist/interview/dashboard-manager.d.ts +21 -0
  30. package/dist/interview/manager.d.ts +0 -14
  31. package/dist/interview/service.d.ts +7 -0
  32. package/dist/interview/session-server.d.ts +21 -0
  33. package/dist/interview/types.d.ts +3 -1
  34. package/dist/loop/loop-session.d.ts +64 -0
  35. package/dist/multiplexer/factory.d.ts +5 -5
  36. package/dist/multiplexer/herdr/index.d.ts +31 -0
  37. package/dist/multiplexer/index.d.ts +1 -0
  38. package/dist/multiplexer/session-manager.d.ts +1 -0
  39. package/dist/multiplexer/types.d.ts +4 -4
  40. package/dist/tui.js +82 -39
  41. package/dist/utils/background-job-board.d.ts +17 -1
  42. package/dist/utils/session.d.ts +1 -1
  43. package/oh-my-opencode-slim.schema.json +6 -1
  44. package/package.json +1 -1
  45. package/src/skills/clonedeps/SKILL.md +2 -2
  46. package/src/skills/clonedeps/codemap.md +23 -32
  47. package/src/skills/codemap/SKILL.md +2 -2
  48. package/src/skills/codemap.md +63 -36
  49. package/src/skills/loop-engineering/SKILL.md +30 -0
  50. package/src/skills/reflect/SKILL.md +133 -0
  51. package/src/skills/release-smoke-test/SKILL.md +159 -0
  52. package/src/skills/simplify/SKILL.md +6 -6
package/dist/cli/index.js CHANGED
@@ -90,8 +90,8 @@ function isPwshAvailable() {
90
90
  });
91
91
  return result.status === 0;
92
92
  }
93
- function escapePowerShellPath(path2) {
94
- return path2.replace(/'/g, "''");
93
+ function escapePowerShellPath(path3) {
94
+ return path3.replace(/'/g, "''");
95
95
  }
96
96
  function getWindowsZipExtractor() {
97
97
  const buildNumber = getWindowsBuildNumber();
@@ -163,17 +163,17 @@ import * as path from "node:path";
163
163
  // src/cli/config-io.ts
164
164
  init_compat();
165
165
  import {
166
- copyFileSync as copyFileSync2,
167
- existsSync as existsSync3,
168
- mkdirSync as mkdirSync3,
166
+ copyFileSync,
167
+ existsSync as existsSync2,
168
+ mkdirSync as mkdirSync2,
169
169
  readFileSync,
170
170
  renameSync,
171
171
  rmSync,
172
- statSync as statSync2,
172
+ statSync,
173
173
  writeFileSync
174
174
  } from "node:fs";
175
175
  import { homedir as homedir2 } from "node:os";
176
- import { dirname as dirname3, join as join3 } from "node:path";
176
+ import { dirname as dirname2, join as join2 } from "node:path";
177
177
 
178
178
  // src/cli/paths.ts
179
179
  import { existsSync, mkdirSync } from "node:fs";
@@ -279,10 +279,6 @@ function ensureOpenCodeConfigDir() {
279
279
  }
280
280
 
281
281
  // src/config/constants.ts
282
- var AGENT_ALIASES = {
283
- explore: "explorer",
284
- "frontend-ui-ux-engineer": "designer"
285
- };
286
282
  var SUBAGENT_NAMES = [
287
283
  "explorer",
288
284
  "librarian",
@@ -344,7 +340,7 @@ var CouncilConfigSchema = z.object({
344
340
  default_preset: z.string().default("default"),
345
341
  councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
346
342
  councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
347
- master: z.unknown().optional().describe("DEPRECATED ignored. Council agent synthesizes directly.")
343
+ master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
348
344
  }).transform((data) => {
349
345
  const deprecated = [];
350
346
  if (data.master !== undefined)
@@ -410,7 +406,13 @@ var AgentOverrideConfigSchema = z2.object({
410
406
  options: z2.record(z2.string(), z2.unknown()).optional(),
411
407
  displayName: z2.string().min(1).optional()
412
408
  }).strict();
413
- var MultiplexerTypeSchema = z2.enum(["auto", "tmux", "zellij", "none"]);
409
+ var MultiplexerTypeSchema = z2.enum([
410
+ "auto",
411
+ "tmux",
412
+ "zellij",
413
+ "herdr",
414
+ "none"
415
+ ]);
414
416
  var MultiplexerLayoutSchema = z2.enum([
415
417
  "main-horizontal",
416
418
  "main-vertical",
@@ -479,24 +481,13 @@ var AcpAgentConfigSchema = z2.object({
479
481
  permissionMode: AcpAgentPermissionModeSchema.default("ask")
480
482
  }).strict();
481
483
  var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
482
- function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
484
+ function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
483
485
  for (const [name, override] of Object.entries(overrides)) {
484
- const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
485
- if (!isBuiltInOrAlias) {
486
- continue;
487
- }
488
- if (override.prompt !== undefined) {
489
- ctx.addIssue({
490
- code: z2.ZodIssueCode.custom,
491
- path: [...pathPrefix, name, "prompt"],
492
- message: "prompt is only supported for custom agents"
493
- });
494
- }
495
- if (override.orchestratorPrompt !== undefined) {
486
+ if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
496
487
  ctx.addIssue({
497
488
  code: z2.ZodIssueCode.custom,
498
489
  path: [...pathPrefix, name, "orchestratorPrompt"],
499
- message: "orchestratorPrompt is only supported for custom agents"
490
+ message: "orchestratorPrompt is not supported for the orchestrator agent"
500
491
  });
501
492
  }
502
493
  }
@@ -504,6 +495,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
504
495
  var PluginConfigSchema = z2.object({
505
496
  preset: z2.string().optional(),
506
497
  setDefaultAgent: z2.boolean().optional(),
498
+ compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout when enabled."),
507
499
  autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
508
500
  presets: z2.record(z2.string(), PresetSchema).optional(),
509
501
  agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
@@ -522,11 +514,14 @@ var PluginConfigSchema = z2.object({
522
514
  acpAgents: AcpAgentsConfigSchema.optional()
523
515
  }).superRefine((value, ctx) => {
524
516
  if (value.agents) {
525
- validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
517
+ rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
526
518
  }
527
519
  if (value.presets) {
528
520
  for (const [presetName, preset] of Object.entries(value.presets)) {
529
- validateCustomOnlyPromptFields(preset, ctx, ["presets", presetName]);
521
+ rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
522
+ "presets",
523
+ presetName
524
+ ]);
530
525
  }
531
526
  }
532
527
  });
@@ -543,16 +538,7 @@ var DEFAULT_AGENT_MCPS = {
543
538
  councillor: []
544
539
  };
545
540
 
546
- // src/cli/custom-skills.ts
547
- import {
548
- copyFileSync,
549
- existsSync as existsSync2,
550
- mkdirSync as mkdirSync2,
551
- readdirSync,
552
- statSync
553
- } from "node:fs";
554
- import { dirname as dirname2, join as join2 } from "node:path";
555
- import { fileURLToPath } from "node:url";
541
+ // src/cli/custom-skills-registry.ts
556
542
  var CUSTOM_SKILLS = [
557
543
  {
558
544
  name: "simplify",
@@ -590,6 +576,12 @@ var CUSTOM_SKILLS = [
590
576
  allowedAgents: ["orchestrator"],
591
577
  sourcePath: "src/skills/oh-my-opencode-slim"
592
578
  },
579
+ {
580
+ name: "release-smoke-test",
581
+ description: "Validate packed release candidates and bugfixes before public publish",
582
+ allowedAgents: ["orchestrator"],
583
+ sourcePath: "src/skills/release-smoke-test"
584
+ },
593
585
  {
594
586
  name: "worktrees",
595
587
  description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
@@ -597,45 +589,6 @@ var CUSTOM_SKILLS = [
597
589
  sourcePath: "src/skills/worktrees"
598
590
  }
599
591
  ];
600
- function getCustomSkillsDir() {
601
- return join2(getConfigDir(), "skills");
602
- }
603
- function copyDirRecursive(src, dest) {
604
- if (!existsSync2(dest)) {
605
- mkdirSync2(dest, { recursive: true });
606
- }
607
- const entries = readdirSync(src);
608
- for (const entry of entries) {
609
- const srcPath = join2(src, entry);
610
- const destPath = join2(dest, entry);
611
- const stat = statSync(srcPath);
612
- if (stat.isDirectory()) {
613
- copyDirRecursive(srcPath, destPath);
614
- } else {
615
- const destDir = dirname2(destPath);
616
- if (!existsSync2(destDir)) {
617
- mkdirSync2(destDir, { recursive: true });
618
- }
619
- copyFileSync(srcPath, destPath);
620
- }
621
- }
622
- }
623
- function installCustomSkill(skill) {
624
- try {
625
- const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
626
- const sourcePath = join2(packageRoot, skill.sourcePath);
627
- const targetPath = join2(getCustomSkillsDir(), skill.name);
628
- if (!existsSync2(sourcePath)) {
629
- console.error(`Custom skill source not found: ${sourcePath}`);
630
- return false;
631
- }
632
- copyDirRecursive(sourcePath, targetPath);
633
- return true;
634
- } catch (error) {
635
- console.error(`Failed to install custom skill: ${skill.name}`, error);
636
- return false;
637
- }
638
- }
639
592
 
640
593
  // src/cli/providers.ts
641
594
  var SCHEMA_URL = "https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-slim.schema.json";
@@ -677,12 +630,11 @@ var MODEL_MAPPINGS = {
677
630
  fixer: { model: "zai-coding-plan/glm-5", variant: "low" }
678
631
  },
679
632
  "opencode-go": {
680
- orchestrator: { model: "opencode-go/glm-5.1" },
681
- oracle: { model: "opencode-go/deepseek-v4-pro", variant: "max" },
682
- council: { model: "opencode-go/deepseek-v4-pro", variant: "high" },
683
- librarian: { model: "opencode-go/minimax-m2.7" },
684
- explorer: { model: "opencode-go/minimax-m2.7" },
685
- designer: { model: "opencode-go/kimi-k2.6", variant: "medium" },
633
+ orchestrator: { model: "opencode-go/glm-5.2" },
634
+ oracle: { model: "opencode-go/qwen3.7-max", variant: "max" },
635
+ librarian: { model: "opencode-go/deepseek-v4-flash" },
636
+ explorer: { model: "opencode-go/deepseek-v4-flash" },
637
+ designer: { model: "opencode-go/kimi-k2.7-code", variant: "medium" },
686
638
  fixer: { model: "opencode-go/deepseek-v4-flash", variant: "high" },
687
639
  observer: { model: "opencode-go/kimi-k2.6" }
688
640
  }
@@ -788,10 +740,10 @@ function normalizePathForMatch(path) {
788
740
  return path.replaceAll("\\", "/");
789
741
  }
790
742
  function findPackageRoot(startPath) {
791
- let currentPath = dirname3(startPath);
743
+ let currentPath = dirname2(startPath);
792
744
  while (true) {
793
- const packageJsonPath = join3(currentPath, "package.json");
794
- if (existsSync3(packageJsonPath)) {
745
+ const packageJsonPath = join2(currentPath, "package.json");
746
+ if (existsSync2(packageJsonPath)) {
795
747
  try {
796
748
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
797
749
  if (packageJson.name === PACKAGE_NAME) {
@@ -799,7 +751,7 @@ function findPackageRoot(startPath) {
799
751
  }
800
752
  } catch {}
801
753
  }
802
- const parentPath = dirname3(currentPath);
754
+ const parentPath = dirname2(currentPath);
803
755
  if (parentPath === currentPath) {
804
756
  return null;
805
757
  }
@@ -810,8 +762,8 @@ function isLocalPackageRootEntry(entry) {
810
762
  if (!entry || entry.startsWith("file://")) {
811
763
  return false;
812
764
  }
813
- const packageJsonPath = join3(entry, "package.json");
814
- if (!existsSync3(packageJsonPath)) {
765
+ const packageJsonPath = join2(entry, "package.json");
766
+ if (!existsSync2(packageJsonPath)) {
815
767
  return false;
816
768
  }
817
769
  try {
@@ -883,8 +835,8 @@ function getRequestedPackageTag(packageRoot) {
883
835
  }
884
836
  function getVersionFromPackageRoot(packageRoot) {
885
837
  try {
886
- const packageJsonPath = join3(packageRoot, "package.json");
887
- if (!existsSync3(packageJsonPath))
838
+ const packageJsonPath = join2(packageRoot, "package.json");
839
+ if (!existsSync2(packageJsonPath))
888
840
  return;
889
841
  const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
890
842
  return pkg.version;
@@ -893,13 +845,13 @@ function getVersionFromPackageRoot(packageRoot) {
893
845
  }
894
846
  }
895
847
  function getOpenCodePluginCacheDir(version) {
896
- const cacheDir = process.env.XDG_CACHE_HOME?.trim() || join3(homedir2(), ".cache");
848
+ const cacheDir = process.env.XDG_CACHE_HOME?.trim() || join2(homedir2(), ".cache");
897
849
  const suffix = version ? `${PACKAGE_NAME}@${version}` : `${PACKAGE_NAME}@latest`;
898
- return join3(cacheDir, "opencode", "packages", suffix);
850
+ return join2(cacheDir, "opencode", "packages", suffix);
899
851
  }
900
852
  function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
901
853
  try {
902
- writeFileSync(join3(cacheDir, "package.json"), JSON.stringify({
854
+ writeFileSync(join2(cacheDir, "package.json"), JSON.stringify({
903
855
  name: `${PACKAGE_NAME}-cache`,
904
856
  private: true,
905
857
  dependencies: {
@@ -916,16 +868,16 @@ function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
916
868
  }
917
869
  }
918
870
  function removeOpenCodePluginCacheArtifacts(cacheDir) {
919
- rmSync(join3(cacheDir, "node_modules", PACKAGE_NAME), {
871
+ rmSync(join2(cacheDir, "node_modules", PACKAGE_NAME), {
920
872
  recursive: true,
921
873
  force: true
922
874
  });
923
- rmSync(join3(cacheDir, "bun.lock"), { force: true });
924
- rmSync(join3(cacheDir, "bun.lockb"), { force: true });
875
+ rmSync(join2(cacheDir, "bun.lock"), { force: true });
876
+ rmSync(join2(cacheDir, "bun.lockb"), { force: true });
925
877
  }
926
878
  function verifyOpenCodePluginCache(cacheDir) {
927
- const pluginPackageJsonPath = join3(cacheDir, "node_modules", PACKAGE_NAME, "package.json");
928
- if (!existsSync3(pluginPackageJsonPath)) {
879
+ const pluginPackageJsonPath = join2(cacheDir, "node_modules", PACKAGE_NAME, "package.json");
880
+ if (!existsSync2(pluginPackageJsonPath)) {
929
881
  return {
930
882
  success: false,
931
883
  configPath: cacheDir,
@@ -965,7 +917,7 @@ async function warmOpenCodePluginCache() {
965
917
  const cacheVersion = pinnedVersion ?? requestedTag ?? runningVersion;
966
918
  const cacheDir = getOpenCodePluginCacheDir(cacheVersion);
967
919
  try {
968
- mkdirSync3(cacheDir, { recursive: true });
920
+ mkdirSync2(cacheDir, { recursive: true });
969
921
  } catch (err) {
970
922
  return {
971
923
  success: false,
@@ -1011,9 +963,9 @@ function stripJsonComments(json) {
1011
963
  }
1012
964
  function parseConfigFile(path) {
1013
965
  try {
1014
- if (!existsSync3(path))
966
+ if (!existsSync2(path))
1015
967
  return { config: null };
1016
- const stat = statSync2(path);
968
+ const stat = statSync(path);
1017
969
  if (stat.size === 0)
1018
970
  return { config: null };
1019
971
  const content = readFileSync(path, "utf-8");
@@ -1042,8 +994,8 @@ function writeConfig(configPath, config) {
1042
994
  const bakPath = `${configPath}.bak`;
1043
995
  const content = `${JSON.stringify(config, null, 2)}
1044
996
  `;
1045
- if (existsSync3(configPath)) {
1046
- copyFileSync2(configPath, bakPath);
997
+ if (existsSync2(configPath)) {
998
+ copyFileSync(configPath, bakPath);
1047
999
  }
1048
1000
  writeFileSync(tmpPath, content);
1049
1001
  renameSync(tmpPath, configPath);
@@ -1129,8 +1081,8 @@ function writeLiteConfig(installConfig, targetPath) {
1129
1081
  const bakPath = `${configPath}.bak`;
1130
1082
  const content = `${JSON.stringify(config, null, 2)}
1131
1083
  `;
1132
- if (existsSync3(configPath)) {
1133
- copyFileSync2(configPath, bakPath);
1084
+ if (existsSync2(configPath)) {
1085
+ copyFileSync(configPath, bakPath);
1134
1086
  }
1135
1087
  writeFileSync(tmpPath, content);
1136
1088
  renameSync(tmpPath, configPath);
@@ -1288,6 +1240,7 @@ function mergePluginConfigs(base, override) {
1288
1240
  ...base,
1289
1241
  ...override,
1290
1242
  agents: deepMerge(base.agents, override.agents),
1243
+ presets: deepMerge(base.presets, override.presets),
1291
1244
  tmux: deepMerge(base.tmux, override.tmux),
1292
1245
  multiplexer: deepMerge(base.multiplexer, override.multiplexer),
1293
1246
  interview: deepMerge(base.interview, override.interview),
@@ -1513,9 +1466,949 @@ Options:
1513
1466
  // src/cli/install.ts
1514
1467
  import { existsSync as existsSync7 } from "node:fs";
1515
1468
  import { createInterface } from "node:readline/promises";
1469
+ import { fileURLToPath } from "node:url";
1470
+
1471
+ // src/hooks/auto-update-checker/skill-sync.ts
1472
+ import * as crypto from "node:crypto";
1473
+ import {
1474
+ copyFileSync as copyFileSync2,
1475
+ existsSync as existsSync4,
1476
+ lstatSync,
1477
+ mkdirSync as mkdirSync3,
1478
+ readdirSync,
1479
+ readFileSync as readFileSync4,
1480
+ renameSync as renameSync2,
1481
+ rmSync as rmSync2,
1482
+ unlinkSync,
1483
+ writeFileSync as writeFileSync2
1484
+ } from "node:fs";
1485
+ import * as os from "node:os";
1486
+ import * as path2 from "node:path";
1487
+
1488
+ // src/utils/logger.ts
1489
+ import { appendFile } from "node:fs/promises";
1490
+ var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
1491
+ var logFile = null;
1492
+ var writeChain = Promise.resolve();
1493
+ function log(message, data) {
1494
+ const target = logFile;
1495
+ if (!target)
1496
+ return;
1497
+ try {
1498
+ const timestamp = new Date().toISOString();
1499
+ let dataStr = "";
1500
+ if (data !== undefined) {
1501
+ try {
1502
+ dataStr = JSON.stringify(data);
1503
+ } catch {
1504
+ dataStr = "[unserializable]";
1505
+ }
1506
+ }
1507
+ const logEntry = `[${timestamp}] ${message} ${dataStr}
1508
+ `;
1509
+ writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
1510
+ } catch {}
1511
+ }
1512
+
1513
+ // src/hooks/auto-update-checker/skill-sync.ts
1514
+ var localProcessToken = globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN;
1515
+ if (!localProcessToken) {
1516
+ localProcessToken = crypto.randomUUID();
1517
+ globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN = localProcessToken;
1518
+ }
1519
+ var PROCESS_TOKEN = localProcessToken;
1520
+ var ACQUIRED_LOCKS = new Set;
1521
+ var LEGACY_MANAGED_SKILL_HASHES = {};
1522
+ function validateManifest(data) {
1523
+ if (typeof data !== "object" || data === null)
1524
+ return false;
1525
+ const d = data;
1526
+ if (d.schemaVersion !== 1)
1527
+ return false;
1528
+ if (typeof d.skills !== "object" || d.skills === null)
1529
+ return false;
1530
+ const allowedStatuses = new Set([
1531
+ "managed",
1532
+ "customized",
1533
+ "deleted",
1534
+ "conflict"
1535
+ ]);
1536
+ const skillsObj = d.skills;
1537
+ for (const key of Object.keys(skillsObj)) {
1538
+ const entry = skillsObj[key];
1539
+ if (typeof entry !== "object" || entry === null)
1540
+ return false;
1541
+ if (typeof entry.status !== "string" || !allowedStatuses.has(entry.status))
1542
+ return false;
1543
+ if (typeof entry.packageVersion !== "string")
1544
+ return false;
1545
+ if (typeof entry.sourceHash !== "string")
1546
+ return false;
1547
+ if (typeof entry.lastManagedHash !== "string")
1548
+ return false;
1549
+ if (typeof entry.lastSeenHash !== "string")
1550
+ return false;
1551
+ if (entry.stagedPath !== undefined && typeof entry.stagedPath !== "string")
1552
+ return false;
1553
+ if (typeof entry.updatedAt !== "string")
1554
+ return false;
1555
+ }
1556
+ return true;
1557
+ }
1558
+ function copyDirRecursive(src, dest) {
1559
+ const stat = lstatSync(src);
1560
+ if (stat.isSymbolicLink()) {
1561
+ return;
1562
+ }
1563
+ if (stat.isDirectory()) {
1564
+ mkdirSync3(dest, { recursive: true });
1565
+ const entries = readdirSync(src);
1566
+ for (const entry of entries) {
1567
+ copyDirRecursive(path2.join(src, entry), path2.join(dest, entry));
1568
+ }
1569
+ } else if (stat.isFile()) {
1570
+ const destDir = path2.dirname(dest);
1571
+ if (!existsSync4(destDir)) {
1572
+ mkdirSync3(destDir, { recursive: true });
1573
+ }
1574
+ copyFileSync2(src, dest);
1575
+ }
1576
+ }
1577
+ function computeDirectoryHash(dirPath) {
1578
+ const hash = crypto.createHash("sha256");
1579
+ const entriesToHash = [];
1580
+ function traverse(currentDir) {
1581
+ const entries = readdirSync(currentDir);
1582
+ for (const entry of entries) {
1583
+ const absolutePath = path2.join(currentDir, entry);
1584
+ const stat = lstatSync(absolutePath);
1585
+ const relativePath = path2.relative(dirPath, absolutePath);
1586
+ if (stat.isSymbolicLink()) {
1587
+ continue;
1588
+ }
1589
+ if (stat.isDirectory()) {
1590
+ entriesToHash.push({
1591
+ relativePath,
1592
+ absolutePath,
1593
+ kind: "directory",
1594
+ mode: stat.mode
1595
+ });
1596
+ traverse(absolutePath);
1597
+ } else if (stat.isFile()) {
1598
+ entriesToHash.push({
1599
+ relativePath,
1600
+ absolutePath,
1601
+ kind: "file",
1602
+ mode: stat.mode
1603
+ });
1604
+ }
1605
+ }
1606
+ }
1607
+ traverse(dirPath);
1608
+ entriesToHash.sort((a, b) => {
1609
+ if (a.relativePath < b.relativePath)
1610
+ return -1;
1611
+ if (a.relativePath > b.relativePath)
1612
+ return 1;
1613
+ return 0;
1614
+ });
1615
+ for (const entry of entriesToHash) {
1616
+ hash.update(entry.kind);
1617
+ hash.update("\x00");
1618
+ hash.update(entry.relativePath);
1619
+ hash.update("\x00");
1620
+ hash.update(String(entry.mode & 4095));
1621
+ hash.update("\x00");
1622
+ if (entry.kind === "file") {
1623
+ const content = readFileSync4(entry.absolutePath);
1624
+ hash.update(content);
1625
+ }
1626
+ }
1627
+ return hash.digest("hex");
1628
+ }
1629
+ function isPidRunning(pid) {
1630
+ try {
1631
+ process.kill(pid, 0);
1632
+ return true;
1633
+ } catch (err) {
1634
+ return err.code === "EPERM";
1635
+ }
1636
+ }
1637
+ var CROSS_HOST_LOCK_EXPIRY_MS = 5 * 60 * 1000;
1638
+ function acquireLock(lockDir) {
1639
+ const metadataPath = path2.join(lockDir, "owner.json");
1640
+ const currentHost = os.hostname();
1641
+ const currentPid = process.pid;
1642
+ const writeMetadata = () => {
1643
+ try {
1644
+ const metadata = {
1645
+ pid: currentPid,
1646
+ host: currentHost,
1647
+ time: Date.now(),
1648
+ token: PROCESS_TOKEN
1649
+ };
1650
+ writeFileSync2(metadataPath, JSON.stringify(metadata), "utf-8");
1651
+ } catch {}
1652
+ };
1653
+ try {
1654
+ mkdirSync3(lockDir);
1655
+ writeMetadata();
1656
+ ACQUIRED_LOCKS.add(path2.resolve(lockDir));
1657
+ return true;
1658
+ } catch (err) {
1659
+ if (err.code !== "EEXIST") {
1660
+ throw err;
1661
+ }
1662
+ }
1663
+ try {
1664
+ let shouldSteal = false;
1665
+ let ageMs = 0;
1666
+ if (existsSync4(metadataPath)) {
1667
+ try {
1668
+ const content = readFileSync4(metadataPath, "utf-8");
1669
+ const metadata = JSON.parse(content);
1670
+ ageMs = Date.now() - metadata.time;
1671
+ if (metadata.host === currentHost) {
1672
+ if (!isPidRunning(metadata.pid)) {
1673
+ log(`[skill-sync] Lock owner process ${metadata.pid} is not running on this host. Recovery path.`);
1674
+ shouldSteal = true;
1675
+ }
1676
+ } else {
1677
+ if (ageMs > CROSS_HOST_LOCK_EXPIRY_MS) {
1678
+ log(`[skill-sync] Lock owned by different host ${metadata.host} has expired (${Math.round(ageMs / 1000)}s old). Reclaiming lock.`);
1679
+ shouldSteal = true;
1680
+ } else {
1681
+ log(`[skill-sync] Lock is owned by different host ${metadata.host}; failing closed.`);
1682
+ }
1683
+ }
1684
+ } catch {
1685
+ shouldSteal = true;
1686
+ }
1687
+ } else {
1688
+ const stat = lstatSync(lockDir);
1689
+ ageMs = Date.now() - stat.mtimeMs;
1690
+ if (ageMs > 30000) {
1691
+ shouldSteal = true;
1692
+ }
1693
+ }
1694
+ if (!shouldSteal)
1695
+ return false;
1696
+ log(`[skill-sync] Stealing/recovering lock directory.`);
1697
+ rmSync2(lockDir, { recursive: true, force: true });
1698
+ mkdirSync3(lockDir);
1699
+ writeMetadata();
1700
+ ACQUIRED_LOCKS.add(path2.resolve(lockDir));
1701
+ return true;
1702
+ } catch (err) {
1703
+ log(`[skill-sync] Failed to check/recover lock at ${lockDir}:`, err);
1704
+ return false;
1705
+ }
1706
+ }
1707
+ function releaseLock(lockDir) {
1708
+ const resolvedPath = path2.resolve(lockDir);
1709
+ try {
1710
+ let isOurLock = false;
1711
+ const metadataPath = path2.join(lockDir, "owner.json");
1712
+ if (existsSync4(metadataPath)) {
1713
+ try {
1714
+ const content = readFileSync4(metadataPath, "utf-8");
1715
+ const metadata = JSON.parse(content);
1716
+ if (metadata.host === os.hostname() && metadata.pid === process.pid && metadata.token === PROCESS_TOKEN) {
1717
+ isOurLock = true;
1718
+ } else {
1719
+ isOurLock = false;
1720
+ }
1721
+ } catch (err) {
1722
+ log(`[skill-sync] Lock owner.json is unreadable/corrupt:`, err);
1723
+ isOurLock = false;
1724
+ }
1725
+ } else if (ACQUIRED_LOCKS.has(resolvedPath)) {
1726
+ isOurLock = true;
1727
+ }
1728
+ if (isOurLock) {
1729
+ if (existsSync4(lockDir)) {
1730
+ rmSync2(lockDir, { recursive: true, force: true });
1731
+ }
1732
+ } else if (existsSync4(lockDir)) {
1733
+ log(`[skill-sync] Skipping lock directory removal: lock is not owned by this process/token or owner.json check failed.`);
1734
+ }
1735
+ } catch (err) {
1736
+ log(`[skill-sync] Failed to release lock at ${lockDir}:`, err);
1737
+ } finally {
1738
+ ACQUIRED_LOCKS.delete(resolvedPath);
1739
+ }
1740
+ }
1741
+ function atomicReplaceDir(sourceDir, destDir) {
1742
+ const parentDir = path2.dirname(destDir);
1743
+ if (!existsSync4(parentDir)) {
1744
+ mkdirSync3(parentDir, { recursive: true });
1745
+ }
1746
+ const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1747
+ const stagingDir = path2.join(parentDir, `.staging-${path2.basename(destDir)}-${uniqueSuffix}`);
1748
+ const backupDir = path2.join(parentDir, `.backup-${path2.basename(destDir)}-${uniqueSuffix}`);
1749
+ let backupCreated = false;
1750
+ try {
1751
+ copyDirRecursive(sourceDir, stagingDir);
1752
+ if (existsSync4(destDir)) {
1753
+ renameSync2(destDir, backupDir);
1754
+ backupCreated = true;
1755
+ }
1756
+ renameSync2(stagingDir, destDir);
1757
+ if (backupCreated) {
1758
+ rmSync2(backupDir, { recursive: true, force: true });
1759
+ }
1760
+ } catch (err) {
1761
+ log(`[skill-sync] Error during atomic replace for ${destDir}. Rolling back:`, err);
1762
+ if (backupCreated) {
1763
+ try {
1764
+ if (existsSync4(destDir)) {
1765
+ rmSync2(destDir, { recursive: true, force: true });
1766
+ }
1767
+ renameSync2(backupDir, destDir);
1768
+ } catch (rollbackErr) {
1769
+ log(`[skill-sync] Critical error during rollback for ${destDir}:`, rollbackErr);
1770
+ }
1771
+ }
1772
+ try {
1773
+ if (existsSync4(stagingDir)) {
1774
+ rmSync2(stagingDir, { recursive: true, force: true });
1775
+ }
1776
+ } catch {}
1777
+ throw err;
1778
+ }
1779
+ }
1780
+ function matchesArtifactPattern(entry, prefix, skillName) {
1781
+ if (!entry.startsWith(prefix))
1782
+ return false;
1783
+ const rest = entry.slice(prefix.length);
1784
+ if (!rest.startsWith(`${skillName}-`))
1785
+ return false;
1786
+ const suffix = rest.slice(skillName.length + 1);
1787
+ const firstPart = suffix.split("-")[0];
1788
+ const timestamp = Number(firstPart);
1789
+ if (Number.isNaN(timestamp) || timestamp <= 0)
1790
+ return false;
1791
+ return true;
1792
+ }
1793
+ function recoverOrphanArtifacts(destSkillsDir, skillName) {
1794
+ if (!existsSync4(destSkillsDir))
1795
+ return false;
1796
+ let hadArtifacts = false;
1797
+ let entries = [];
1798
+ try {
1799
+ entries = readdirSync(destSkillsDir);
1800
+ } catch {
1801
+ return false;
1802
+ }
1803
+ const backups = [];
1804
+ const stagings = [];
1805
+ for (const entry of entries) {
1806
+ if (matchesArtifactPattern(entry, ".backup-", skillName)) {
1807
+ backups.push(path2.join(destSkillsDir, entry));
1808
+ hadArtifacts = true;
1809
+ } else if (matchesArtifactPattern(entry, ".staging-", skillName)) {
1810
+ stagings.push(path2.join(destSkillsDir, entry));
1811
+ hadArtifacts = true;
1812
+ }
1813
+ }
1814
+ const destPath = path2.join(destSkillsDir, skillName);
1815
+ if (backups.length > 0) {
1816
+ backups.sort();
1817
+ const mostRecentBackup = backups[backups.length - 1];
1818
+ if (!existsSync4(destPath)) {
1819
+ backups.pop();
1820
+ try {
1821
+ renameSync2(mostRecentBackup, destPath);
1822
+ log(`[skill-sync] Recovered backup for ${skillName} back to destination.`);
1823
+ } catch (err) {
1824
+ log(`[skill-sync] Failed to restore backup for ${skillName}:`, err);
1825
+ }
1826
+ }
1827
+ for (const backup of backups) {
1828
+ try {
1829
+ rmSync2(backup, { recursive: true, force: true });
1830
+ } catch (err) {
1831
+ log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
1832
+ }
1833
+ }
1834
+ }
1835
+ for (const staging of stagings) {
1836
+ try {
1837
+ rmSync2(staging, { recursive: true, force: true });
1838
+ } catch (err) {
1839
+ log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
1840
+ }
1841
+ }
1842
+ return hadArtifacts;
1843
+ }
1844
+ function removeManagedStagedPath(stagedPath, manifestDir, skillName) {
1845
+ try {
1846
+ const absoluteStagedPath = path2.resolve(stagedPath);
1847
+ const absoluteAllowedRoot = path2.resolve(path2.join(manifestDir, "skill-updates"));
1848
+ const relative2 = path2.relative(absoluteAllowedRoot, absoluteStagedPath);
1849
+ const isUnderRoot = relative2 && !relative2.startsWith("..") && !path2.isAbsolute(relative2);
1850
+ if (isUnderRoot) {
1851
+ if (existsSync4(absoluteStagedPath)) {
1852
+ rmSync2(absoluteStagedPath, { recursive: true, force: true });
1853
+ log(`[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`);
1854
+ }
1855
+ } else {
1856
+ log(`[skill-sync] Refusing to delete staged path for ${skillName}: path ${absoluteStagedPath} is not under managed root ${absoluteAllowedRoot}`);
1857
+ }
1858
+ } catch (err) {
1859
+ log(`[skill-sync] Error while trying to verify and remove staged path for ${skillName} (${stagedPath}):`, err);
1860
+ }
1861
+ }
1862
+ function syncBundledSkillsFromPackage(packageRoot, options = {}) {
1863
+ const installed = [];
1864
+ const skippedExisting = [];
1865
+ const failed = [];
1866
+ const staged = [];
1867
+ const adopted = [];
1868
+ const customized = [];
1869
+ const sourceSkillsDir = path2.join(packageRoot, "src", "skills");
1870
+ try {
1871
+ const stat = lstatSync(sourceSkillsDir);
1872
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
1873
+ log(`[skill-sync] Source skills directory is not a valid directory: ${sourceSkillsDir}`);
1874
+ return {
1875
+ installed,
1876
+ skippedExisting,
1877
+ failed,
1878
+ staged,
1879
+ adopted,
1880
+ customized
1881
+ };
1882
+ }
1883
+ } catch {
1884
+ log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
1885
+ return {
1886
+ installed,
1887
+ skippedExisting,
1888
+ failed,
1889
+ staged,
1890
+ adopted,
1891
+ customized
1892
+ };
1893
+ }
1894
+ let packageVersion = "unknown";
1895
+ try {
1896
+ const pkgJsonPath = path2.join(packageRoot, "package.json");
1897
+ if (existsSync4(pkgJsonPath)) {
1898
+ const content = readFileSync4(pkgJsonPath, "utf-8");
1899
+ const pkg = JSON.parse(content);
1900
+ if (pkg.version) {
1901
+ packageVersion = pkg.version;
1902
+ }
1903
+ }
1904
+ } catch (err) {
1905
+ log(`[skill-sync] Failed to read package version from ${packageRoot}:`, err);
1906
+ }
1907
+ const manifestDir = path2.join(getConfigDir(), ".oh-my-opencode-slim");
1908
+ const lockDir = path2.join(manifestDir, "skills.lock");
1909
+ try {
1910
+ mkdirSync3(manifestDir, { recursive: true });
1911
+ } catch (err) {
1912
+ log(`[skill-sync] Failed to create manifest directory: ${manifestDir}`, err);
1913
+ }
1914
+ if (!acquireLock(lockDir)) {
1915
+ log("[skill-sync] Failed to acquire lock for skill synchronization. Skipping.");
1916
+ return {
1917
+ installed,
1918
+ skippedExisting,
1919
+ failed: ["__lock__"],
1920
+ staged,
1921
+ adopted,
1922
+ customized
1923
+ };
1924
+ }
1925
+ try {
1926
+ const manifestPath = path2.join(manifestDir, "skills-manifest.json");
1927
+ let manifest = {
1928
+ schemaVersion: 1,
1929
+ updatedAt: new Date().toISOString(),
1930
+ skills: {}
1931
+ };
1932
+ let isManifestCorrupt = false;
1933
+ if (existsSync4(manifestPath)) {
1934
+ try {
1935
+ const content = readFileSync4(manifestPath, "utf-8");
1936
+ const parsed = JSON.parse(content);
1937
+ if (validateManifest(parsed)) {
1938
+ manifest = parsed;
1939
+ } else {
1940
+ throw new Error("Manifest validation failed");
1941
+ }
1942
+ } catch (err) {
1943
+ log("[skill-sync] Manifest is corrupt/unreadable. Failing closed.", err);
1944
+ isManifestCorrupt = true;
1945
+ }
1946
+ }
1947
+ const destSkillsDir = path2.join(getConfigDir(), "skills");
1948
+ try {
1949
+ if (!existsSync4(destSkillsDir)) {
1950
+ mkdirSync3(destSkillsDir, { recursive: true });
1951
+ }
1952
+ } catch (err) {
1953
+ log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
1954
+ }
1955
+ const skillsToProcess = (options.skills ?? CUSTOM_SKILLS).map((s) => ({
1956
+ name: s.name,
1957
+ sourcePath: s.sourcePath
1958
+ }));
1959
+ for (const skill of skillsToProcess) {
1960
+ try {
1961
+ const sourcePath = path2.join(packageRoot, skill.sourcePath);
1962
+ try {
1963
+ const stat = lstatSync(sourcePath);
1964
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
1965
+ continue;
1966
+ }
1967
+ const skillMdPath = path2.join(sourcePath, "SKILL.md");
1968
+ const skillMdStat = lstatSync(skillMdPath);
1969
+ if (skillMdStat.isSymbolicLink() || !skillMdStat.isFile()) {
1970
+ continue;
1971
+ }
1972
+ } catch {
1973
+ continue;
1974
+ }
1975
+ const destPath = path2.join(destSkillsDir, skill.name);
1976
+ const hadArtifacts = recoverOrphanArtifacts(destSkillsDir, skill.name);
1977
+ let destExists = false;
1978
+ let destIsDir = false;
1979
+ try {
1980
+ const destStat = lstatSync(destPath);
1981
+ destExists = true;
1982
+ destIsDir = destStat.isDirectory() && !destStat.isSymbolicLink();
1983
+ } catch {}
1984
+ if (destExists && !destIsDir) {
1985
+ log(`[skill-sync] Skill ${skill.name} destination is a file or symlink (conflict). Skipping.`);
1986
+ skippedExisting.push(skill.name);
1987
+ const sourceHash2 = computeDirectoryHash(sourcePath);
1988
+ const entry2 = manifest.skills[skill.name];
1989
+ if (entry2?.stagedPath) {
1990
+ removeManagedStagedPath(entry2.stagedPath, manifestDir, skill.name);
1991
+ }
1992
+ manifest.skills[skill.name] = {
1993
+ status: "conflict",
1994
+ packageVersion,
1995
+ sourceHash: sourceHash2,
1996
+ lastManagedHash: "",
1997
+ lastSeenHash: "",
1998
+ updatedAt: new Date().toISOString()
1999
+ };
2000
+ continue;
2001
+ }
2002
+ const sourceHash = computeDirectoryHash(sourcePath);
2003
+ if (isManifestCorrupt) {
2004
+ if (!destExists) {
2005
+ try {
2006
+ atomicReplaceDir(sourcePath, destPath);
2007
+ installed.push(skill.name);
2008
+ manifest.skills[skill.name] = {
2009
+ status: "managed",
2010
+ packageVersion,
2011
+ sourceHash,
2012
+ lastManagedHash: sourceHash,
2013
+ lastSeenHash: sourceHash,
2014
+ updatedAt: new Date().toISOString()
2015
+ };
2016
+ } catch (err) {
2017
+ log(`[skill-sync] Failed to install missing skill ${skill.name} (corrupt manifest mode):`, err);
2018
+ failed.push(skill.name);
2019
+ }
2020
+ } else {
2021
+ log(`[skill-sync] Skipping existing skill ${skill.name} because manifest is corrupt.`);
2022
+ skippedExisting.push(skill.name);
2023
+ const destHash2 = computeDirectoryHash(destPath);
2024
+ if (destHash2 === sourceHash) {
2025
+ manifest.skills[skill.name] = {
2026
+ status: "managed",
2027
+ packageVersion,
2028
+ sourceHash,
2029
+ lastManagedHash: sourceHash,
2030
+ lastSeenHash: sourceHash,
2031
+ updatedAt: new Date().toISOString()
2032
+ };
2033
+ } else {
2034
+ try {
2035
+ const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
2036
+ if (existsSync4(stagedSkillDir)) {
2037
+ rmSync2(stagedSkillDir, { recursive: true, force: true });
2038
+ }
2039
+ mkdirSync3(stagedSkillDir, { recursive: true });
2040
+ copyDirRecursive(sourcePath, stagedSkillDir);
2041
+ manifest.skills[skill.name] = {
2042
+ status: "customized",
2043
+ packageVersion,
2044
+ sourceHash,
2045
+ lastManagedHash: "",
2046
+ lastSeenHash: destHash2,
2047
+ stagedPath: stagedSkillDir,
2048
+ updatedAt: new Date().toISOString()
2049
+ };
2050
+ staged.push(skill.name);
2051
+ customized.push(skill.name);
2052
+ } catch (err) {
2053
+ log(`[skill-sync] Failed to stage update for customized skill ${skill.name} during recovery:`, err);
2054
+ manifest.skills[skill.name] = {
2055
+ status: "customized",
2056
+ packageVersion: "unknown",
2057
+ sourceHash: "",
2058
+ lastManagedHash: "",
2059
+ lastSeenHash: destHash2,
2060
+ updatedAt: new Date().toISOString()
2061
+ };
2062
+ }
2063
+ }
2064
+ }
2065
+ continue;
2066
+ }
2067
+ const entry = manifest.skills[skill.name];
2068
+ if (!destExists) {
2069
+ if (entry && entry.status === "deleted") {
2070
+ log(`[skill-sync] Skill ${skill.name} was deleted by user. Skipping.`);
2071
+ skippedExisting.push(skill.name);
2072
+ continue;
2073
+ }
2074
+ if (entry && entry.status !== "deleted") {
2075
+ if (hadArtifacts) {
2076
+ log(`[skill-sync] Managed skill ${skill.name} has backup/staging artifacts. Skipping delete, re-installing.`);
2077
+ try {
2078
+ atomicReplaceDir(sourcePath, destPath);
2079
+ installed.push(skill.name);
2080
+ manifest.skills[skill.name] = {
2081
+ status: "managed",
2082
+ packageVersion,
2083
+ sourceHash,
2084
+ lastManagedHash: sourceHash,
2085
+ lastSeenHash: sourceHash,
2086
+ updatedAt: new Date().toISOString()
2087
+ };
2088
+ } catch (err) {
2089
+ log(`[skill-sync] Failed to re-install skill ${skill.name}:`, err);
2090
+ failed.push(skill.name);
2091
+ }
2092
+ continue;
2093
+ } else {
2094
+ if (entry.stagedPath) {
2095
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2096
+ delete entry.stagedPath;
2097
+ }
2098
+ const rawEntry = entry;
2099
+ delete rawEntry.stagedVersion;
2100
+ delete rawEntry.stagedHash;
2101
+ entry.status = "deleted";
2102
+ entry.updatedAt = new Date().toISOString();
2103
+ log(`[skill-sync] Skill ${skill.name} was deleted by user (detected now). Skipping.`);
2104
+ skippedExisting.push(skill.name);
2105
+ continue;
2106
+ }
2107
+ }
2108
+ try {
2109
+ atomicReplaceDir(sourcePath, destPath);
2110
+ installed.push(skill.name);
2111
+ manifest.skills[skill.name] = {
2112
+ status: "managed",
2113
+ packageVersion,
2114
+ sourceHash,
2115
+ lastManagedHash: sourceHash,
2116
+ lastSeenHash: sourceHash,
2117
+ updatedAt: new Date().toISOString()
2118
+ };
2119
+ log(`[skill-sync] Successfully installed missing skill: ${skill.name}`);
2120
+ } catch (err) {
2121
+ log(`[skill-sync] Failed to install skill ${skill.name}:`, err);
2122
+ failed.push(skill.name);
2123
+ }
2124
+ continue;
2125
+ }
2126
+ const destHash = computeDirectoryHash(destPath);
2127
+ if (entry) {
2128
+ if (entry.status === "managed") {
2129
+ if (destHash === entry.lastManagedHash) {
2130
+ if (destHash === sourceHash) {
2131
+ entry.packageVersion = packageVersion;
2132
+ entry.sourceHash = sourceHash;
2133
+ entry.lastManagedHash = sourceHash;
2134
+ entry.lastSeenHash = sourceHash;
2135
+ entry.updatedAt = new Date().toISOString();
2136
+ skippedExisting.push(skill.name);
2137
+ } else {
2138
+ try {
2139
+ atomicReplaceDir(sourcePath, destPath);
2140
+ installed.push(skill.name);
2141
+ manifest.skills[skill.name] = {
2142
+ status: "managed",
2143
+ packageVersion,
2144
+ sourceHash,
2145
+ lastManagedHash: sourceHash,
2146
+ lastSeenHash: sourceHash,
2147
+ updatedAt: new Date().toISOString()
2148
+ };
2149
+ log(`[skill-sync] Updated managed skill: ${skill.name}`);
2150
+ } catch (err) {
2151
+ log(`[skill-sync] Failed to update managed skill ${skill.name}:`, err);
2152
+ failed.push(skill.name);
2153
+ }
2154
+ }
2155
+ } else {
2156
+ if (destHash === sourceHash) {
2157
+ manifest.skills[skill.name] = {
2158
+ status: "managed",
2159
+ packageVersion,
2160
+ sourceHash,
2161
+ lastManagedHash: sourceHash,
2162
+ lastSeenHash: sourceHash,
2163
+ updatedAt: new Date().toISOString()
2164
+ };
2165
+ skippedExisting.push(skill.name);
2166
+ } else {
2167
+ try {
2168
+ const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
2169
+ if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
2170
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2171
+ }
2172
+ if (existsSync4(stagedSkillDir)) {
2173
+ rmSync2(stagedSkillDir, { recursive: true, force: true });
2174
+ }
2175
+ mkdirSync3(stagedSkillDir, { recursive: true });
2176
+ copyDirRecursive(sourcePath, stagedSkillDir);
2177
+ entry.status = "customized";
2178
+ entry.lastSeenHash = destHash;
2179
+ entry.stagedPath = stagedSkillDir;
2180
+ entry.sourceHash = sourceHash;
2181
+ entry.packageVersion = packageVersion;
2182
+ entry.updatedAt = new Date().toISOString();
2183
+ staged.push(skill.name);
2184
+ customized.push(skill.name);
2185
+ skippedExisting.push(skill.name);
2186
+ log(`[skill-sync] Skill ${skill.name} is customized. Staged update at ${stagedSkillDir}`);
2187
+ } catch (err) {
2188
+ log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
2189
+ failed.push(skill.name);
2190
+ }
2191
+ }
2192
+ }
2193
+ } else if (entry.status === "customized") {
2194
+ if (destHash === sourceHash) {
2195
+ if (entry.stagedPath) {
2196
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2197
+ }
2198
+ entry.status = "managed";
2199
+ entry.lastManagedHash = sourceHash;
2200
+ entry.lastSeenHash = sourceHash;
2201
+ entry.sourceHash = sourceHash;
2202
+ entry.packageVersion = packageVersion;
2203
+ delete entry.stagedPath;
2204
+ entry.updatedAt = new Date().toISOString();
2205
+ adopted.push(skill.name);
2206
+ skippedExisting.push(skill.name);
2207
+ log(`[skill-sync] Customized skill ${skill.name} converged with current version. Adopted back to managed.`);
2208
+ } else {
2209
+ entry.lastSeenHash = destHash;
2210
+ entry.updatedAt = new Date().toISOString();
2211
+ if (destHash !== sourceHash && entry.sourceHash !== sourceHash) {
2212
+ try {
2213
+ const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
2214
+ if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
2215
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2216
+ }
2217
+ if (existsSync4(stagedSkillDir)) {
2218
+ rmSync2(stagedSkillDir, { recursive: true, force: true });
2219
+ }
2220
+ mkdirSync3(stagedSkillDir, { recursive: true });
2221
+ copyDirRecursive(sourcePath, stagedSkillDir);
2222
+ entry.stagedPath = stagedSkillDir;
2223
+ entry.sourceHash = sourceHash;
2224
+ entry.packageVersion = packageVersion;
2225
+ staged.push(skill.name);
2226
+ customized.push(skill.name);
2227
+ skippedExisting.push(skill.name);
2228
+ log(`[skill-sync] Staged new update for customized skill ${skill.name} at ${stagedSkillDir}`);
2229
+ } catch (err) {
2230
+ log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
2231
+ failed.push(skill.name);
2232
+ }
2233
+ } else {
2234
+ customized.push(skill.name);
2235
+ skippedExisting.push(skill.name);
2236
+ }
2237
+ }
2238
+ } else if (entry.status === "deleted") {
2239
+ if (destHash === sourceHash) {
2240
+ entry.status = "managed";
2241
+ entry.packageVersion = packageVersion;
2242
+ entry.sourceHash = sourceHash;
2243
+ entry.lastManagedHash = sourceHash;
2244
+ entry.lastSeenHash = sourceHash;
2245
+ entry.updatedAt = new Date().toISOString();
2246
+ skippedExisting.push(skill.name);
2247
+ adopted.push(skill.name);
2248
+ log(`[skill-sync] Skill ${skill.name} re-created by user (matching current). Adopted as managed.`);
2249
+ } else {
2250
+ try {
2251
+ const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
2252
+ if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
2253
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2254
+ }
2255
+ if (existsSync4(stagedSkillDir)) {
2256
+ rmSync2(stagedSkillDir, { recursive: true, force: true });
2257
+ }
2258
+ mkdirSync3(stagedSkillDir, { recursive: true });
2259
+ copyDirRecursive(sourcePath, stagedSkillDir);
2260
+ entry.status = "customized";
2261
+ entry.packageVersion = packageVersion;
2262
+ entry.sourceHash = sourceHash;
2263
+ entry.lastManagedHash = sourceHash;
2264
+ entry.lastSeenHash = destHash;
2265
+ entry.stagedPath = stagedSkillDir;
2266
+ entry.updatedAt = new Date().toISOString();
2267
+ staged.push(skill.name);
2268
+ customized.push(skill.name);
2269
+ skippedExisting.push(skill.name);
2270
+ log(`[skill-sync] Skill ${skill.name} re-created by user (custom). Marked customized and staged.`);
2271
+ } catch (err) {
2272
+ log(`[skill-sync] Failed to stage update for deleted/recreated skill ${skill.name}:`, err);
2273
+ failed.push(skill.name);
2274
+ }
2275
+ }
2276
+ } else if (entry.status === "conflict") {
2277
+ if (destHash === sourceHash) {
2278
+ if (entry.stagedPath) {
2279
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2280
+ }
2281
+ entry.status = "managed";
2282
+ entry.packageVersion = packageVersion;
2283
+ entry.sourceHash = sourceHash;
2284
+ entry.lastManagedHash = sourceHash;
2285
+ entry.lastSeenHash = sourceHash;
2286
+ delete entry.stagedPath;
2287
+ entry.updatedAt = new Date().toISOString();
2288
+ adopted.push(skill.name);
2289
+ } else {
2290
+ try {
2291
+ const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
2292
+ if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
2293
+ removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
2294
+ }
2295
+ if (existsSync4(stagedSkillDir)) {
2296
+ rmSync2(stagedSkillDir, { recursive: true, force: true });
2297
+ }
2298
+ mkdirSync3(stagedSkillDir, { recursive: true });
2299
+ copyDirRecursive(sourcePath, stagedSkillDir);
2300
+ entry.status = "customized";
2301
+ entry.packageVersion = packageVersion;
2302
+ entry.sourceHash = sourceHash;
2303
+ entry.lastManagedHash = sourceHash;
2304
+ entry.lastSeenHash = destHash;
2305
+ entry.stagedPath = stagedSkillDir;
2306
+ entry.updatedAt = new Date().toISOString();
2307
+ staged.push(skill.name);
2308
+ customized.push(skill.name);
2309
+ skippedExisting.push(skill.name);
2310
+ log(`[skill-sync] Conflicted skill ${skill.name} recovered as customized and staged at ${stagedSkillDir}`);
2311
+ } catch (err) {
2312
+ log(`[skill-sync] Failed to stage update for conflicted skill ${skill.name}:`, err);
2313
+ failed.push(skill.name);
2314
+ }
2315
+ }
2316
+ }
2317
+ } else {
2318
+ if (destHash === sourceHash) {
2319
+ manifest.skills[skill.name] = {
2320
+ status: "managed",
2321
+ packageVersion,
2322
+ sourceHash,
2323
+ lastManagedHash: sourceHash,
2324
+ lastSeenHash: sourceHash,
2325
+ updatedAt: new Date().toISOString()
2326
+ };
2327
+ skippedExisting.push(skill.name);
2328
+ adopted.push(skill.name);
2329
+ log(`[skill-sync] Adopted existing matching skill: ${skill.name}`);
2330
+ } else if (LEGACY_MANAGED_SKILL_HASHES[skill.name]?.includes(destHash)) {
2331
+ try {
2332
+ atomicReplaceDir(sourcePath, destPath);
2333
+ installed.push(skill.name);
2334
+ manifest.skills[skill.name] = {
2335
+ status: "managed",
2336
+ packageVersion,
2337
+ sourceHash,
2338
+ lastManagedHash: sourceHash,
2339
+ lastSeenHash: sourceHash,
2340
+ updatedAt: new Date().toISOString()
2341
+ };
2342
+ log(`[skill-sync] Adopted and updated legacy skill: ${skill.name}`);
2343
+ } catch (err) {
2344
+ log(`[skill-sync] Failed to update legacy skill ${skill.name}:`, err);
2345
+ failed.push(skill.name);
2346
+ }
2347
+ } else {
2348
+ try {
2349
+ const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
2350
+ if (existsSync4(stagedSkillDir)) {
2351
+ rmSync2(stagedSkillDir, { recursive: true, force: true });
2352
+ }
2353
+ mkdirSync3(stagedSkillDir, { recursive: true });
2354
+ copyDirRecursive(sourcePath, stagedSkillDir);
2355
+ manifest.skills[skill.name] = {
2356
+ status: "customized",
2357
+ packageVersion,
2358
+ sourceHash,
2359
+ lastManagedHash: "",
2360
+ lastSeenHash: destHash,
2361
+ stagedPath: stagedSkillDir,
2362
+ updatedAt: new Date().toISOString()
2363
+ };
2364
+ staged.push(skill.name);
2365
+ customized.push(skill.name);
2366
+ skippedExisting.push(skill.name);
2367
+ log(`[skill-sync] Skill ${skill.name} is customized (no manifest entry). Staged update at ${stagedSkillDir}`);
2368
+ } catch (err) {
2369
+ log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
2370
+ failed.push(skill.name);
2371
+ }
2372
+ }
2373
+ }
2374
+ } catch (err) {
2375
+ log(`[skill-sync] Failed processing skill ${skill.name}:`, err);
2376
+ failed.push(skill.name);
2377
+ }
2378
+ }
2379
+ let manifestWriteFailed = false;
2380
+ manifest.updatedAt = new Date().toISOString();
2381
+ const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
2382
+ try {
2383
+ writeFileSync2(tempManifestPath, JSON.stringify(manifest, null, 2), "utf-8");
2384
+ renameSync2(tempManifestPath, manifestPath);
2385
+ } catch (err) {
2386
+ log("[skill-sync] Failed to write skills manifest atomically:", err);
2387
+ manifestWriteFailed = true;
2388
+ try {
2389
+ if (existsSync4(tempManifestPath)) {
2390
+ unlinkSync(tempManifestPath);
2391
+ }
2392
+ } catch {}
2393
+ }
2394
+ if (manifestWriteFailed) {
2395
+ failed.push("__manifest__");
2396
+ }
2397
+ } finally {
2398
+ releaseLock(lockDir);
2399
+ }
2400
+ return {
2401
+ installed,
2402
+ skippedExisting,
2403
+ failed,
2404
+ staged,
2405
+ adopted,
2406
+ customized
2407
+ };
2408
+ }
1516
2409
 
1517
2410
  // src/cli/background-subagents.ts
1518
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
2411
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
1519
2412
  import { homedir as homedir3 } from "node:os";
1520
2413
  import { dirname as dirname4, join as join5 } from "node:path";
1521
2414
  var ENV_NAME = "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS";
@@ -1604,57 +2497,30 @@ function upsertBackgroundSubagentsBlock(content, block) {
1604
2497
  }
1605
2498
  function writeBackgroundSubagentsBlock(targetPath) {
1606
2499
  const block = getBackgroundSubagentsBlock(targetPath);
1607
- const content = existsSync5(targetPath) ? readFileSync4(targetPath, "utf8") : "";
2500
+ const content = existsSync5(targetPath) ? readFileSync5(targetPath, "utf8") : "";
1608
2501
  const nextContent = upsertBackgroundSubagentsBlock(content, block);
1609
2502
  mkdirSync4(dirname4(targetPath), { recursive: true });
1610
- writeFileSync2(targetPath, nextContent);
2503
+ writeFileSync3(targetPath, nextContent);
1611
2504
  }
1612
2505
 
1613
2506
  // src/companion/updater.ts
1614
2507
  init_compat();
1615
- import { createHash } from "node:crypto";
2508
+ import { createHash as createHash2 } from "node:crypto";
1616
2509
  import {
1617
2510
  chmodSync,
1618
2511
  copyFileSync as copyFileSync3,
1619
2512
  existsSync as existsSync6,
1620
2513
  mkdirSync as mkdirSync5,
1621
2514
  mkdtempSync,
1622
- readFileSync as readFileSync5,
1623
- renameSync as renameSync2,
1624
- rmSync as rmSync2,
1625
- statSync as statSync4,
1626
- writeFileSync as writeFileSync3
2515
+ readFileSync as readFileSync6,
2516
+ renameSync as renameSync3,
2517
+ rmSync as rmSync3,
2518
+ statSync as statSync3,
2519
+ writeFileSync as writeFileSync4
1627
2520
  } from "node:fs";
1628
2521
  import { homedir as homedir4, platform, tmpdir } from "node:os";
1629
- import * as path2 from "node:path";
2522
+ import * as path3 from "node:path";
1630
2523
  import { setTimeout as delay } from "node:timers/promises";
1631
-
1632
- // src/utils/logger.ts
1633
- import { appendFile } from "node:fs/promises";
1634
- var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
1635
- var logFile = null;
1636
- var writeChain = Promise.resolve();
1637
- function log(message, data) {
1638
- const target = logFile;
1639
- if (!target)
1640
- return;
1641
- try {
1642
- const timestamp = new Date().toISOString();
1643
- let dataStr = "";
1644
- if (data !== undefined) {
1645
- try {
1646
- dataStr = JSON.stringify(data);
1647
- } catch {
1648
- dataStr = "[unserializable]";
1649
- }
1650
- }
1651
- const logEntry = `[${timestamp}] ${message} ${dataStr}
1652
- `;
1653
- writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
1654
- } catch {}
1655
- }
1656
-
1657
- // src/companion/updater.ts
1658
2524
  var DOWNLOAD_TIMEOUT_MS = 30000;
1659
2525
  var LOCK_TIMEOUT_MS = 2000;
1660
2526
  var STALE_LOCK_MS = 5 * 60000;
@@ -1692,8 +2558,8 @@ function getCompanionTarget() {
1692
2558
  }
1693
2559
  function getCompanionBinaryPath() {
1694
2560
  const xdg = process.env.XDG_DATA_HOME?.trim();
1695
- const base = xdg && path2.isAbsolute(xdg) ? xdg : path2.join(homedir4(), ".local", "share");
1696
- return path2.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
2561
+ const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(homedir4(), ".local", "share");
2562
+ return path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
1697
2563
  }
1698
2564
  async function ensureCompanionVersion(options) {
1699
2565
  const { config, dryRun = false } = options;
@@ -1790,7 +2656,7 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
1790
2656
  } finally {
1791
2657
  clearTimeout(timeout);
1792
2658
  }
1793
- const checksum = createHash("sha256").update(Buffer.from(buffer)).digest("hex");
2659
+ const checksum = createHash2("sha256").update(Buffer.from(buffer)).digest("hex");
1794
2660
  if (checksum !== expectedChecksum) {
1795
2661
  return {
1796
2662
  status: "failed",
@@ -1800,10 +2666,10 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
1800
2666
  }
1801
2667
  let tempDir = "";
1802
2668
  try {
1803
- tempDir = mkdtempSync(path2.join(tmpdir(), "companion-install-"));
1804
- const archivePath = path2.join(tempDir, archiveName);
1805
- writeFileSync3(archivePath, Buffer.from(buffer));
1806
- const extractedDir = path2.join(tempDir, "extracted");
2669
+ tempDir = mkdtempSync(path3.join(tmpdir(), "companion-install-"));
2670
+ const archivePath = path3.join(tempDir, archiveName);
2671
+ writeFileSync4(archivePath, Buffer.from(buffer));
2672
+ const extractedDir = path3.join(tempDir, "extracted");
1807
2673
  mkdirSync5(extractedDir, { recursive: true });
1808
2674
  if (isWindows) {
1809
2675
  const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
@@ -1821,7 +2687,7 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
1821
2687
  }
1822
2688
  }
1823
2689
  const binaryName = isWindows ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion";
1824
- const extractedBinaryPath = path2.join(extractedDir, binaryName);
2690
+ const extractedBinaryPath = path3.join(extractedDir, binaryName);
1825
2691
  if (!existsSync6(extractedBinaryPath)) {
1826
2692
  return {
1827
2693
  status: "failed",
@@ -1829,14 +2695,14 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
1829
2695
  error: `Binary ${binaryName} not found in extracted archive`
1830
2696
  };
1831
2697
  }
1832
- const binDir = path2.dirname(finalBinaryPath);
2698
+ const binDir = path3.dirname(finalBinaryPath);
1833
2699
  mkdirSync5(binDir, { recursive: true });
1834
2700
  const tmpFinalPath = `${finalBinaryPath}.tmp`;
1835
2701
  copyFileSync3(extractedBinaryPath, tmpFinalPath);
1836
2702
  if (!isWindows) {
1837
2703
  chmodSync(tmpFinalPath, 493);
1838
2704
  }
1839
- renameSync2(tmpFinalPath, finalBinaryPath);
2705
+ renameSync3(tmpFinalPath, finalBinaryPath);
1840
2706
  writeInstallMetadata(finalBinaryPath, {
1841
2707
  version: manifest.version,
1842
2708
  tag: manifest.tag,
@@ -1859,14 +2725,14 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
1859
2725
  } finally {
1860
2726
  if (tempDir) {
1861
2727
  try {
1862
- rmSync2(tempDir, { recursive: true, force: true });
2728
+ rmSync3(tempDir, { recursive: true, force: true });
1863
2729
  } catch {}
1864
2730
  }
1865
2731
  }
1866
2732
  }
1867
2733
  function readInstallMetadata(binaryPath) {
1868
2734
  try {
1869
- const parsed = JSON.parse(readFileSync5(metadataPath(binaryPath), "utf8"));
2735
+ const parsed = JSON.parse(readFileSync6(metadataPath(binaryPath), "utf8"));
1870
2736
  if (parsed?.version && parsed.tag && parsed.target) {
1871
2737
  return parsed;
1872
2738
  }
@@ -1874,7 +2740,7 @@ function readInstallMetadata(binaryPath) {
1874
2740
  return null;
1875
2741
  }
1876
2742
  function writeInstallMetadata(binaryPath, metadata) {
1877
- writeFileSync3(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
2743
+ writeFileSync4(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
1878
2744
  }
1879
2745
  function metadataPath(binaryPath) {
1880
2746
  return `${binaryPath}.json`;
@@ -1883,7 +2749,7 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
1883
2749
  const lock = `${binaryPath}.lock`;
1884
2750
  const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
1885
2751
  const staleAfterMs = staleMs ?? STALE_LOCK_MS;
1886
- mkdirSync5(path2.dirname(binaryPath), { recursive: true });
2752
+ mkdirSync5(path3.dirname(binaryPath), { recursive: true });
1887
2753
  while (Date.now() <= deadline) {
1888
2754
  try {
1889
2755
  mkdirSync5(lock);
@@ -1891,7 +2757,7 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
1891
2757
  return await run();
1892
2758
  } finally {
1893
2759
  try {
1894
- rmSync2(lock, { recursive: true, force: true });
2760
+ rmSync3(lock, { recursive: true, force: true });
1895
2761
  } catch {}
1896
2762
  }
1897
2763
  } catch (err) {
@@ -1899,9 +2765,9 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
1899
2765
  if (code !== "EEXIST")
1900
2766
  throw err;
1901
2767
  try {
1902
- const ageMs = Date.now() - statSync4(lock).mtimeMs;
2768
+ const ageMs = Date.now() - statSync3(lock).mtimeMs;
1903
2769
  if (ageMs > staleAfterMs) {
1904
- rmSync2(lock, { recursive: true, force: true });
2770
+ rmSync3(lock, { recursive: true, force: true });
1905
2771
  log("[companion] removed stale install lock", lock);
1906
2772
  continue;
1907
2773
  }
@@ -1988,7 +2854,7 @@ async function installCompanion(config) {
1988
2854
  // src/cli/system.ts
1989
2855
  init_compat();
1990
2856
  import { spawnSync as spawnSync2 } from "node:child_process";
1991
- import { statSync as statSync5 } from "node:fs";
2857
+ import { statSync as statSync4 } from "node:fs";
1992
2858
  var cachedOpenCodePath = null;
1993
2859
  function resolvePathCommand(command) {
1994
2860
  try {
@@ -2061,7 +2927,7 @@ function resolveOpenCodePath() {
2061
2927
  if (opencodePath === "opencode")
2062
2928
  continue;
2063
2929
  try {
2064
- const stat = statSync5(opencodePath);
2930
+ const stat = statSync4(opencodePath);
2065
2931
  if (stat.isFile()) {
2066
2932
  cachedOpenCodePath = opencodePath;
2067
2933
  return opencodePath;
@@ -2110,8 +2976,8 @@ async function getOpenCodeVersion() {
2110
2976
  return null;
2111
2977
  }
2112
2978
  function getOpenCodePath() {
2113
- const path3 = resolveOpenCodePath();
2114
- return path3 === "opencode" ? null : path3;
2979
+ const path4 = resolveOpenCodePath();
2980
+ return path4 === "opencode" ? null : path4;
2115
2981
  }
2116
2982
  // src/cli/install.ts
2117
2983
  var GREEN = "\x1B[32m";
@@ -2194,11 +3060,11 @@ async function checkOpenCodeInstalled() {
2194
3060
  return { ok: false };
2195
3061
  }
2196
3062
  const version = await getOpenCodeVersion();
2197
- const path3 = getOpenCodePath();
3063
+ const path4 = getOpenCodePath();
2198
3064
  const detectedVersion = version ?? "";
2199
- const pathInfo = path3 ? ` (${DIM}${path3}${RESET})` : "";
3065
+ const pathInfo = path4 ? ` (${DIM}${path4}${RESET})` : "";
2200
3066
  printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
2201
- return { ok: true, version: version ?? undefined, path: path3 ?? undefined };
3067
+ return { ok: true, version: version ?? undefined, path: path4 ?? undefined };
2202
3068
  }
2203
3069
  async function configureBackgroundSubagents(config) {
2204
3070
  if (isBackgroundSubagentsEnabled(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS)) {
@@ -2260,9 +3126,9 @@ async function shouldInstallCompanion(config) {
2260
3126
  if (config.companion === "no")
2261
3127
  return false;
2262
3128
  if (config.dryRun) {
2263
- printInfo("Dry run mode - would ask to install the desktop companion (default: yes).");
2264
- config.companion = "yes";
2265
- return true;
3129
+ printInfo("Dry run mode - would ask to install the desktop companion (default: no).");
3130
+ config.companion = "no";
3131
+ return false;
2266
3132
  }
2267
3133
  if (!process.stdin.isTTY) {
2268
3134
  printInfo("Skipped desktop companion prompt in non-TTY mode. Use --companion=yes to install it.");
@@ -2271,7 +3137,7 @@ async function shouldInstallCompanion(config) {
2271
3137
  }
2272
3138
  console.log();
2273
3139
  printInfo("The optional desktop companion shows live agent activity.");
2274
- const shouldInstall = await confirm("Install and enable the desktop companion?", true);
3140
+ const shouldInstall = await confirm("Install and enable the desktop companion?", false);
2275
3141
  config.companion = shouldInstall ? "yes" : "no";
2276
3142
  if (!shouldInstall) {
2277
3143
  printInfo("Desktop companion install skipped.");
@@ -2390,25 +3256,63 @@ ${JSON.stringify(liteConfig, null, 2)}
2390
3256
  }
2391
3257
  }
2392
3258
  if (config.installCustomSkills) {
2393
- printStep(step++, totalSteps, "Installing custom skills...");
3259
+ printStep(step++, totalSteps, "Synchronizing custom skills...");
2394
3260
  if (config.dryRun) {
2395
- printInfo("Dry run mode - would install custom skills:");
3261
+ printInfo("Dry run mode - would synchronize custom skills:");
2396
3262
  for (const skill of CUSTOM_SKILLS) {
2397
3263
  printInfo(` - ${skill.name}`);
2398
3264
  }
2399
3265
  } else {
2400
- let customSkillsInstalled = 0;
2401
- for (const skill of CUSTOM_SKILLS) {
2402
- printInfo(`Installing ${skill.name}...`);
2403
- if (installCustomSkill(skill)) {
2404
- printSuccess(`Installed: ${skill.name}`);
2405
- customSkillsInstalled++;
2406
- } else {
2407
- printInfo(`Skipped: ${skill.name} (already installed)`);
3266
+ try {
3267
+ const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
3268
+ const result = syncBundledSkillsFromPackage(packageRoot);
3269
+ const categorizedSkipped = new Set([
3270
+ ...result.staged,
3271
+ ...result.adopted,
3272
+ ...result.customized
3273
+ ]);
3274
+ const preservedSkills = result.skippedExisting.filter((skill) => !categorizedSkipped.has(skill));
3275
+ if (result.installed.length > 0) {
3276
+ for (const skill of result.installed) {
3277
+ printSuccess(`Installed/Updated: ${skill}`);
3278
+ }
3279
+ }
3280
+ if (preservedSkills.length > 0) {
3281
+ for (const skill of preservedSkills) {
3282
+ printInfo(`Skipped/Preserved: ${skill}`);
3283
+ }
3284
+ }
3285
+ if (result.failed.length > 0) {
3286
+ for (const skill of result.failed) {
3287
+ if (skill === "__lock__") {
3288
+ printError("Lock acquisition failed");
3289
+ } else if (skill === "__manifest__") {
3290
+ printError("Manifest write failed");
3291
+ } else {
3292
+ printError(`Failed: ${skill}`);
3293
+ }
3294
+ }
3295
+ }
3296
+ if (result.staged.length > 0) {
3297
+ for (const skill of result.staged) {
3298
+ printInfo(`Staged for review: ${skill}`);
3299
+ }
3300
+ }
3301
+ if (result.adopted.length > 0) {
3302
+ for (const skill of result.adopted) {
3303
+ printInfo(`Adopted: ${skill}`);
3304
+ }
3305
+ }
3306
+ if (result.customized.length > 0) {
3307
+ for (const skill of result.customized) {
3308
+ printInfo(`Customized: ${skill}`);
3309
+ }
2408
3310
  }
3311
+ const realFailed = result.failed.filter((skill) => skill !== "__lock__" && skill !== "__manifest__");
3312
+ printSuccess(`Skill synchronization complete: ${result.installed.length} installed/updated, ${preservedSkills.length} skipped/preserved, ${result.staged.length} staged, ${result.adopted.length} adopted, ${result.customized.length} customized, ${realFailed.length} failed.`);
3313
+ } catch (err) {
3314
+ printError(`Failed to synchronize custom skills: ${err}`);
2409
3315
  }
2410
- const totalCustom = CUSTOM_SKILLS.length;
2411
- printSuccess(`${customSkillsInstalled}/${totalCustom} custom skills processed`);
2412
3316
  }
2413
3317
  }
2414
3318
  const statusMsg = isUpdate ? "Configuration updated!" : "Installation complete!";
@@ -2532,7 +3436,7 @@ Usage:
2532
3436
  Options:
2533
3437
  --skills=yes|no Install bundled skills (default: yes)
2534
3438
  --companion=ask|yes|no Install desktop companion binary and enable config
2535
- (default: ask; prompt defaults to yes)
3439
+ (default: ask; prompt defaults to no)
2536
3440
  --preset=<name> Active generated config preset (default: openai)
2537
3441
  --background-subagents=ask|yes|no
2538
3442
  Persist required OpenCode background subagent env