opencode-skills-deck 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,8 +5,13 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // packages/plugin/src/index.ts
8
+ import { fileURLToPath } from "node:url";
8
9
  import { Plugin } from "@opencode/plugin";
9
10
 
11
+ // packages/core/src/config.ts
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
10
15
  // node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/external.js
11
16
  var external_exports = {};
12
17
  __export(external_exports, {
@@ -3621,8 +3626,8 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
3621
3626
  const _normalized = cached(() => normalizeDef(def));
3622
3627
  const memo2 = globalConfig.memoizer;
3623
3628
  const generateFastpass = (shape) => {
3624
- const normalized = _normalized.value;
3625
- const syms = normalized.symbolKeys;
3629
+ const normalized2 = _normalized.value;
3630
+ const syms = normalized2.symbolKeys;
3626
3631
  const doc = new Doc(["payload", "ctx"], { shape, inst, memo: memo2, syms });
3627
3632
  const parseStr = (k) => `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
3628
3633
  const prefixStr = (id, k) => `
@@ -3640,11 +3645,11 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
3640
3645
  doc.write(`const input = payload.value;`);
3641
3646
  const ids = /* @__PURE__ */ Object.create(null);
3642
3647
  let counter = 0;
3643
- for (const key of normalized.allKeys) {
3648
+ for (const key of normalized2.allKeys) {
3644
3649
  ids[key] = `key_${counter++}`;
3645
3650
  }
3646
3651
  doc.write(memo2 ? `const newResult = memo.alloc(inst, payload, {}, ctx);` : `const newResult = {};`);
3647
- for (const key of normalized.allKeys) {
3652
+ for (const key of normalized2.allKeys) {
3648
3653
  if (key === "__proto__")
3649
3654
  continue;
3650
3655
  const id = ids[key];
@@ -5074,7 +5079,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5074
5079
  var NONE = 0;
5075
5080
  var ASSUMED = 1;
5076
5081
  var PROVEN = 2;
5077
- function isRecursive(inst, stack, resolve) {
5082
+ function isRecursive(inst, stack, resolve2) {
5078
5083
  const cached2 = recursive.get(inst);
5079
5084
  if (cached2 !== void 0)
5080
5085
  return cached2 ? PROVEN : NONE;
@@ -5084,7 +5089,7 @@ function isRecursive(inst, stack, resolve) {
5084
5089
  let result = NONE;
5085
5090
  const check2 = (child) => {
5086
5091
  if (result !== PROVEN && child?._zod) {
5087
- const answer = isRecursive(child, stack, resolve);
5092
+ const answer = isRecursive(child, stack, resolve2);
5088
5093
  if (answer > result)
5089
5094
  result = answer;
5090
5095
  }
@@ -5095,7 +5100,7 @@ function isRecursive(inst, stack, resolve) {
5095
5100
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5096
5101
  if (spread && !desc.enumerable)
5097
5102
  continue;
5098
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
5103
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve2) : NONE;
5099
5104
  if (child > answer)
5100
5105
  answer = child;
5101
5106
  }
@@ -5159,7 +5164,7 @@ function isRecursive(inst, stack, resolve) {
5159
5164
  break;
5160
5165
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5161
5166
  case "lazy": {
5162
- const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
5167
+ const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
5163
5168
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5164
5169
  break;
5165
5170
  }
@@ -19483,23 +19488,23 @@ function fromJSONSchema(schema, params) {
19483
19488
  if (typeof schema === "boolean") {
19484
19489
  return schema ? z.any() : z.never();
19485
19490
  }
19486
- let normalized;
19491
+ let normalized2;
19487
19492
  try {
19488
- normalized = JSON.parse(JSON.stringify(schema));
19493
+ normalized2 = JSON.parse(JSON.stringify(schema));
19489
19494
  } catch {
19490
19495
  throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
19491
19496
  }
19492
- const version2 = detectVersion(normalized, params?.defaultTarget);
19493
- const defs = normalized.$defs || normalized.definitions || {};
19497
+ const version2 = detectVersion(normalized2, params?.defaultTarget);
19498
+ const defs = normalized2.$defs || normalized2.definitions || {};
19494
19499
  const ctx = {
19495
19500
  version: version2,
19496
19501
  defs,
19497
19502
  refs: /* @__PURE__ */ new Map(),
19498
19503
  processing: /* @__PURE__ */ new Set(),
19499
- rootSchema: normalized,
19504
+ rootSchema: normalized2,
19500
19505
  registry: params?.registry ?? globalRegistry
19501
19506
  };
19502
- return convertSchema(normalized, ctx);
19507
+ return convertSchema(normalized2, ctx);
19503
19508
  }
19504
19509
 
19505
19510
  // node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/visit.js
@@ -19745,60 +19750,290 @@ var configSchema = external_exports.object({
19745
19750
  repos: external_exports.array(repoSchema).default([])
19746
19751
  });
19747
19752
  var defaultConfig = configSchema.parse({});
19753
+ function configFilePath(paths) {
19754
+ return join(paths.configDir, "config.json");
19755
+ }
19756
+ function parseConfig(raw) {
19757
+ return configSchema.parse(raw);
19758
+ }
19759
+ function loadConfig(paths) {
19760
+ try {
19761
+ const text = readFileSync(configFilePath(paths), "utf8");
19762
+ return parseConfig(JSON.parse(text));
19763
+ } catch {
19764
+ return defaultConfig;
19765
+ }
19766
+ }
19767
+ function fillTemplate(template, values) {
19768
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
19769
+ const value = values[key];
19770
+ return value === void 0 ? match : String(value);
19771
+ });
19772
+ }
19748
19773
 
19749
19774
  // packages/core/src/exec.ts
19775
+ import { spawn } from "node:child_process";
19750
19776
  var MAX_BUFFER = 16 * 1024 * 1024;
19777
+ var DEFAULT_TIMEOUT_MS = 3e4;
19778
+ function createRunner(options = {}) {
19779
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19780
+ return (command, args, options2) => new Promise((resolve2, reject) => {
19781
+ const child = spawn(command, args, {
19782
+ cwd: options2?.cwd,
19783
+ env: process.env,
19784
+ stdio: ["ignore", "pipe", "pipe"]
19785
+ });
19786
+ let stdout = "";
19787
+ let stderr = "";
19788
+ let overflow = false;
19789
+ let settled = false;
19790
+ const fail = (error62) => {
19791
+ if (settled) return;
19792
+ settled = true;
19793
+ clearTimeout(timer);
19794
+ reject(error62);
19795
+ };
19796
+ const succeed = (result) => {
19797
+ if (settled) return;
19798
+ settled = true;
19799
+ clearTimeout(timer);
19800
+ resolve2(result);
19801
+ };
19802
+ const timer = setTimeout(() => {
19803
+ child.kill("SIGTERM");
19804
+ fail(new Error(`${command} ${args.join(" ")} timed out after ${timeoutMs}ms`));
19805
+ }, timeoutMs);
19806
+ timer.unref?.();
19807
+ child.stdout?.setEncoding("utf8");
19808
+ child.stderr?.setEncoding("utf8");
19809
+ child.stdout?.on("data", (chunk) => {
19810
+ if (overflow) return;
19811
+ stdout += chunk;
19812
+ if (stdout.length > MAX_BUFFER) {
19813
+ overflow = true;
19814
+ child.kill();
19815
+ }
19816
+ });
19817
+ child.stderr?.on("data", (chunk) => {
19818
+ if (overflow) return;
19819
+ stderr += chunk;
19820
+ if (stderr.length > MAX_BUFFER) {
19821
+ overflow = true;
19822
+ child.kill();
19823
+ }
19824
+ });
19825
+ child.on("error", (error62) => {
19826
+ fail(new Error(`${command} ${args.join(" ")} failed: ${error62.message}`));
19827
+ });
19828
+ child.on("close", (code) => {
19829
+ if (settled) return;
19830
+ if (overflow) {
19831
+ fail(new Error(`${command} ${args.join(" ")} failed: \u51FA\u529B\u304C\u5927\u304D\u3059\u304E\u307E\u3059`));
19832
+ return;
19833
+ }
19834
+ if (code === 0) {
19835
+ succeed({ stdout, stderr });
19836
+ return;
19837
+ }
19838
+ const detail = stderr.trim() || `exit code ${code ?? "unknown"}`;
19839
+ fail(new Error(`${command} ${args.join(" ")} failed: ${detail}`));
19840
+ });
19841
+ });
19842
+ }
19751
19843
 
19752
19844
  // packages/core/src/paths.ts
19753
19845
  import { homedir } from "node:os";
19754
- import { join } from "node:path";
19846
+ import { posix, win32 } from "node:path";
19755
19847
  var APP_DIR = "opencode-skills-deck";
19848
+ function pathFor(platform) {
19849
+ return platform === "win32" ? win32 : posix;
19850
+ }
19851
+ function styleOf(path) {
19852
+ return /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\") ? "win32" : "linux";
19853
+ }
19756
19854
  function configRootOf(platform, env, home) {
19757
- return env.XDG_CONFIG_HOME ?? (platform === "win32" ? env.APPDATA ?? join(home, "AppData", "Roaming") : join(home, ".config"));
19855
+ const path = pathFor(platform);
19856
+ return env.XDG_CONFIG_HOME ?? (platform === "win32" ? env.APPDATA ?? path.join(home, "AppData", "Roaming") : path.join(home, ".config"));
19758
19857
  }
19759
19858
  function stateRootOf(platform, env, home) {
19760
- return env.XDG_STATE_HOME ?? (platform === "win32" ? env.LOCALAPPDATA ?? join(home, "AppData", "Local") : join(home, ".local", "state"));
19859
+ const path = pathFor(platform);
19860
+ return env.XDG_STATE_HOME ?? (platform === "win32" ? env.LOCALAPPDATA ?? path.join(home, "AppData", "Local") : path.join(home, ".local", "state"));
19861
+ }
19862
+ function opencodeConfigDir(options = {}) {
19863
+ const platform = options.platform ?? process.platform;
19864
+ const env = options.env ?? process.env;
19865
+ const home = options.home ?? homedir();
19866
+ return pathFor(platform).join(configRootOf(platform, env, home), "opencode");
19867
+ }
19868
+ function opencodeSkillsDir(options = {}) {
19869
+ return pathFor(options.platform ?? process.platform).join(opencodeConfigDir(options), "skills");
19761
19870
  }
19762
19871
  function repoSessionSlug(repo) {
19763
19872
  return repo.replace(/[^a-zA-Z0-9._-]+/g, "__");
19764
19873
  }
19765
19874
  function sessionsDirFor(paths, repo) {
19766
- return repo ? join(paths.sessionsDir, repoSessionSlug(repo)) : paths.sessionsDir;
19875
+ if (!repo) return paths.sessionsDir;
19876
+ return pathFor(styleOf(paths.sessionsDir)).join(paths.sessionsDir, repoSessionSlug(repo));
19767
19877
  }
19768
19878
  function resolvePaths(options = {}) {
19769
19879
  const platform = options.platform ?? process.platform;
19770
19880
  const env = options.env ?? process.env;
19771
19881
  const home = options.home ?? homedir();
19882
+ const path = pathFor(platform);
19772
19883
  const configRoot = configRootOf(platform, env, home);
19773
19884
  const stateRoot = stateRootOf(platform, env, home);
19774
- const configDir = join(configRoot, APP_DIR);
19775
- const stateDir = join(stateRoot, APP_DIR);
19885
+ const configDir = path.join(configRoot, APP_DIR);
19886
+ const stateDir = path.join(stateRoot, APP_DIR);
19776
19887
  return {
19777
19888
  configDir,
19778
19889
  stateDir,
19779
- sessionsDir: join(stateDir, "sessions"),
19780
- issueCacheDir: join(stateDir, "issues")
19890
+ sessionsDir: path.join(stateDir, "sessions"),
19891
+ issueCacheDir: path.join(stateDir, "issues")
19781
19892
  };
19782
19893
  }
19783
19894
 
19895
+ // packages/core/src/repo.ts
19896
+ import { join as join2, posix as posix2, win32 as win322 } from "node:path";
19897
+ var REMOTE_PATTERNS = [
19898
+ /^git@[^:]+:(?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?$/,
19899
+ /^(?:ssh|git|https?):\/\/(?:[^@/]+@)?[^/]+\/(?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/
19900
+ ];
19901
+ function parseRemoteUrl(url2) {
19902
+ const trimmed = url2.trim();
19903
+ for (const pattern of REMOTE_PATTERNS) {
19904
+ const match = pattern.exec(trimmed);
19905
+ const owner = match?.groups?.owner;
19906
+ const name = match?.groups?.name;
19907
+ if (owner && name) return { owner, name };
19908
+ }
19909
+ return null;
19910
+ }
19911
+ function slugify2(input2, maxLength = 40) {
19912
+ const base = input2.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
19913
+ if (!base) return "";
19914
+ return base.slice(0, maxLength).replace(/-+$/g, "");
19915
+ }
19916
+ function branchName(issueNumber, title) {
19917
+ const slug = slugify2(title);
19918
+ return slug ? `issue/${issueNumber}-${slug}` : `issue/${issueNumber}`;
19919
+ }
19920
+ function issueWorktreePath(repoRoot, issueNumber) {
19921
+ return join2(repoRoot, ".worktrees", `issue-${issueNumber}`);
19922
+ }
19923
+ function pathStyleOf(path) {
19924
+ return /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\") ? win322 : posix2;
19925
+ }
19926
+ async function resolveRepoRoot(directory, runner) {
19927
+ let worktreeRoot;
19928
+ try {
19929
+ const { stdout } = await runner("git", ["rev-parse", "--show-toplevel"], { cwd: directory });
19930
+ worktreeRoot = stdout.trim();
19931
+ if (!worktreeRoot) return null;
19932
+ } catch {
19933
+ return null;
19934
+ }
19935
+ try {
19936
+ const { stdout } = await runner("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
19937
+ cwd: worktreeRoot
19938
+ });
19939
+ const common = stdout.trim();
19940
+ if (common.endsWith(".git")) return pathStyleOf(common).dirname(common);
19941
+ } catch {
19942
+ try {
19943
+ const { stdout } = await runner("git", ["rev-parse", "--git-common-dir"], {
19944
+ cwd: worktreeRoot
19945
+ });
19946
+ const common = pathStyleOf(worktreeRoot).resolve(worktreeRoot, stdout.trim());
19947
+ if (common.endsWith(".git")) return pathStyleOf(common).dirname(common);
19948
+ } catch {
19949
+ }
19950
+ }
19951
+ return worktreeRoot;
19952
+ }
19953
+ async function readRepoInfo(directory, runner) {
19954
+ const root = await resolveRepoRoot(directory, runner);
19955
+ if (!root) return null;
19956
+ let remote;
19957
+ try {
19958
+ const { stdout } = await runner("git", ["remote", "get-url", "origin"], { cwd: root });
19959
+ remote = stdout.trim();
19960
+ } catch {
19961
+ return null;
19962
+ }
19963
+ const ref = parseRemoteUrl(remote);
19964
+ if (!ref) return null;
19965
+ let defaultBranch2 = "main";
19966
+ try {
19967
+ const { stdout } = await runner("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], {
19968
+ cwd: root
19969
+ });
19970
+ const value = stdout.trim().replace(/^refs\/remotes\/origin\//, "");
19971
+ if (value) defaultBranch2 = value;
19972
+ } catch {
19973
+ }
19974
+ let branch = defaultBranch2;
19975
+ try {
19976
+ const { stdout } = await runner("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
19977
+ const current = stdout.trim();
19978
+ if (current) branch = current;
19979
+ } catch {
19980
+ }
19981
+ return { root, ref, defaultBranch: defaultBranch2, branch };
19982
+ }
19983
+ async function resolveRepoContexts(input2) {
19984
+ const contexts = [];
19985
+ if (input2.config.repos.length === 0) {
19986
+ const info = await readRepoInfo(input2.directory, input2.runner);
19987
+ if (info) {
19988
+ contexts.push({
19989
+ repo: `${info.ref.owner}/${info.ref.name}`,
19990
+ root: info.root,
19991
+ info,
19992
+ configured: false
19993
+ });
19994
+ }
19995
+ return contexts;
19996
+ }
19997
+ for (const entry of input2.config.repos) {
19998
+ const root = entry.root ?? input2.directory;
19999
+ const info = await readRepoInfo(root, input2.runner);
20000
+ if (info) {
20001
+ contexts.push({
20002
+ repo: `${info.ref.owner}/${info.ref.name}`,
20003
+ root: info.root,
20004
+ info,
20005
+ configured: true
20006
+ });
20007
+ continue;
20008
+ }
20009
+ contexts.push({ repo: entry.repo, root, info: null, configured: true });
20010
+ }
20011
+ const seen = /* @__PURE__ */ new Set();
20012
+ return contexts.filter((context) => {
20013
+ if (seen.has(context.repo)) return false;
20014
+ seen.add(context.repo);
20015
+ return true;
20016
+ });
20017
+ }
20018
+
19784
20019
  // packages/core/src/state.ts
19785
20020
  import {
19786
20021
  existsSync,
19787
20022
  mkdirSync,
19788
20023
  readdirSync,
19789
- readFileSync,
20024
+ readFileSync as readFileSync2,
19790
20025
  renameSync,
19791
20026
  rmSync,
19792
20027
  watch,
19793
20028
  writeFileSync
19794
20029
  } from "node:fs";
19795
- import { join as join2 } from "node:path";
20030
+ import { join as join3 } from "node:path";
19796
20031
  function sessionFilePath(dir, key) {
19797
- return join2(dir, `${key}.json`);
20032
+ return join3(dir, `${key}.json`);
19798
20033
  }
19799
20034
  function readSession(dir, key) {
19800
20035
  try {
19801
- const parsed = JSON.parse(readFileSync(sessionFilePath(dir, key), "utf8"));
20036
+ const parsed = JSON.parse(readFileSync2(sessionFilePath(dir, key), "utf8"));
19802
20037
  return typeof parsed?.key === "string" && typeof parsed?.status === "string" ? parsed : null;
19803
20038
  } catch {
19804
20039
  return null;
@@ -19815,9 +20050,9 @@ function writeSession(dir, record2) {
19815
20050
  var PREVIEW_MAX_LINES = 3;
19816
20051
  var PREVIEW_MAX_CHARS = 500;
19817
20052
  function truncatePreview(text, lines = PREVIEW_MAX_LINES, chars = PREVIEW_MAX_CHARS) {
19818
- const normalized = text.replace(/\r\n?/g, "\n").split(String.fromCharCode(0)).join("").trimEnd();
19819
- if (!normalized) return "";
19820
- const tail = normalized.split("\n").slice(-lines).join("\n");
20053
+ const normalized2 = text.replace(/\r\n?/g, "\n").split(String.fromCharCode(0)).join("").trimEnd();
20054
+ if (!normalized2) return "";
20055
+ const tail = normalized2.split("\n").slice(-lines).join("\n");
19821
20056
  return tail.length > chars ? tail.slice(tail.length - chars) : tail;
19822
20057
  }
19823
20058
  function appendPreview(current, chunk) {
@@ -19825,10 +20060,1077 @@ function appendPreview(current, chunk) {
19825
20060
  }
19826
20061
  var SESSION_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
19827
20062
 
20063
+ // packages/core/src/worktree.ts
20064
+ import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
20065
+ import { join as join4, resolve, sep } from "node:path";
20066
+ async function defaultBranch(input2) {
20067
+ if (input2.configured) return input2.configured;
20068
+ try {
20069
+ const { stdout } = await input2.runner("git", ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], {
20070
+ cwd: input2.repoRoot
20071
+ });
20072
+ const value = stdout.trim().replace(/^refs\/remotes\/origin\//, "");
20073
+ if (value) return value;
20074
+ } catch {
20075
+ }
20076
+ return "main";
20077
+ }
20078
+ async function ensureWorktreesExcluded(input2) {
20079
+ const { stdout } = await input2.runner("git", ["rev-parse", "--git-path", "info/exclude"], {
20080
+ cwd: input2.repoRoot
20081
+ });
20082
+ const path = join4(input2.repoRoot, stdout.trim());
20083
+ const existing = existsSync2(path) ? readFileSync3(path, "utf8") : "";
20084
+ if (existing.split("\n").some((line) => line.trim() === ".worktrees/")) return;
20085
+ const suffix = existing.endsWith("\n") || existing === "" ? "" : "\n";
20086
+ writeFileSync2(path, `${existing}${suffix}.worktrees/
20087
+ `);
20088
+ }
20089
+ async function readWorktreeEntries(input2) {
20090
+ const { stdout } = await input2.runner("git", ["worktree", "list", "--porcelain"], {
20091
+ cwd: input2.repoRoot
20092
+ });
20093
+ const entries = [];
20094
+ let current = null;
20095
+ for (const line of stdout.split("\n")) {
20096
+ if (line.startsWith("worktree ")) {
20097
+ if (current) entries.push(current);
20098
+ current = {
20099
+ directory: line.slice("worktree ".length).trim(),
20100
+ branch: null,
20101
+ detached: false,
20102
+ bare: false
20103
+ };
20104
+ continue;
20105
+ }
20106
+ if (!current) continue;
20107
+ if (line.startsWith("branch ")) {
20108
+ current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "").trim();
20109
+ } else if (line === "detached") {
20110
+ current.detached = true;
20111
+ } else if (line === "bare") {
20112
+ current.bare = true;
20113
+ }
20114
+ }
20115
+ if (current) entries.push(current);
20116
+ return entries;
20117
+ }
20118
+ async function registeredWorktrees(input2) {
20119
+ return (await readWorktreeEntries(input2)).map((entry) => entry.directory);
20120
+ }
20121
+ function normalized(path) {
20122
+ try {
20123
+ return realpathSync(path).replace(/\/+$/, "");
20124
+ } catch {
20125
+ return resolve(path).replace(/\/+$/, "");
20126
+ }
20127
+ }
20128
+ async function branchExists(input2) {
20129
+ try {
20130
+ await input2.runner("git", ["show-ref", "--verify", "--quiet", `refs/heads/${input2.branch}`], {
20131
+ cwd: input2.repoRoot
20132
+ });
20133
+ return true;
20134
+ } catch {
20135
+ return false;
20136
+ }
20137
+ }
20138
+ async function ensureWorktree(input2) {
20139
+ const directory = issueWorktreePath(input2.repoRoot, input2.issueNumber);
20140
+ const branch = branchName(input2.issueNumber, input2.title);
20141
+ const base = await defaultBranch({
20142
+ repoRoot: input2.repoRoot,
20143
+ runner: input2.runner,
20144
+ configured: input2.baseBranch
20145
+ });
20146
+ await ensureWorktreesExcluded({ repoRoot: input2.repoRoot, runner: input2.runner });
20147
+ const registered = await registeredWorktrees({ repoRoot: input2.repoRoot, runner: input2.runner });
20148
+ if (registered.includes(directory)) return { directory, branch, base, created: false };
20149
+ if (await branchExists({ repoRoot: input2.repoRoot, runner: input2.runner, branch })) {
20150
+ await input2.runner("git", ["worktree", "add", directory, branch], { cwd: input2.repoRoot });
20151
+ return { directory, branch, base, created: true };
20152
+ }
20153
+ try {
20154
+ await input2.runner("git", ["fetch", "--quiet", "origin", base], { cwd: input2.repoRoot });
20155
+ } catch {
20156
+ }
20157
+ try {
20158
+ await input2.runner("git", ["worktree", "add", "-b", branch, directory, `origin/${base}`], {
20159
+ cwd: input2.repoRoot
20160
+ });
20161
+ } catch {
20162
+ await input2.runner("git", ["worktree", "add", "-b", branch, directory, base], { cwd: input2.repoRoot });
20163
+ }
20164
+ return { directory, branch, base, created: true };
20165
+ }
20166
+ async function removeWorktree(input2) {
20167
+ const registered = await registeredWorktrees({ repoRoot: input2.repoRoot, runner: input2.runner });
20168
+ if (!registered.includes(input2.directory)) return { removed: false, reason: "missing" };
20169
+ const { stdout } = await input2.runner("git", ["status", "--porcelain"], { cwd: input2.directory });
20170
+ if (stdout.trim() !== "") return { removed: false, reason: "dirty" };
20171
+ await input2.runner("git", ["worktree", "remove", input2.directory], { cwd: input2.repoRoot });
20172
+ return { removed: true };
20173
+ }
20174
+ function worktreeIssueNumber(directory) {
20175
+ const name = normalized(directory).split(/[\\/]/).at(-1) ?? "";
20176
+ const match = /^issue-(\d+)$/.exec(name);
20177
+ if (!match) return null;
20178
+ const value = Number(match[1]);
20179
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
20180
+ }
20181
+ function isInsideWorktrees(repoRoot, directory) {
20182
+ const root = normalized(join4(repoRoot, ".worktrees"));
20183
+ return normalized(directory).startsWith(`${root}${sep}`);
20184
+ }
20185
+ async function readWorktreeStatus(input2) {
20186
+ const entries = await readWorktreeEntries(input2);
20187
+ const entry = entries.find((candidate) => normalized(candidate.directory) === normalized(input2.directory));
20188
+ const exists = existsSync2(input2.directory);
20189
+ let dirty = false;
20190
+ if (exists) {
20191
+ try {
20192
+ const { stdout } = await input2.runner("git", ["status", "--porcelain"], {
20193
+ cwd: input2.directory
20194
+ });
20195
+ dirty = stdout.trim() !== "";
20196
+ } catch {
20197
+ dirty = true;
20198
+ }
20199
+ }
20200
+ return {
20201
+ directory: input2.directory,
20202
+ branch: entry?.branch ?? null,
20203
+ registered: Boolean(entry),
20204
+ exists,
20205
+ dirty,
20206
+ issueNumber: worktreeIssueNumber(input2.directory)
20207
+ };
20208
+ }
20209
+ async function listIssueWorktrees(input2) {
20210
+ const entries = await readWorktreeEntries(input2);
20211
+ const result = [];
20212
+ for (const entry of entries) {
20213
+ if (!isInsideWorktrees(input2.repoRoot, entry.directory)) continue;
20214
+ const issueNumber = worktreeIssueNumber(entry.directory);
20215
+ if (issueNumber === null) continue;
20216
+ let dirty = false;
20217
+ try {
20218
+ const { stdout } = await input2.runner("git", ["status", "--porcelain"], {
20219
+ cwd: entry.directory
20220
+ });
20221
+ dirty = stdout.trim() !== "";
20222
+ } catch {
20223
+ dirty = true;
20224
+ }
20225
+ result.push({
20226
+ directory: entry.directory,
20227
+ branch: entry.branch,
20228
+ registered: true,
20229
+ exists: existsSync2(entry.directory),
20230
+ dirty,
20231
+ issueNumber
20232
+ });
20233
+ }
20234
+ return result;
20235
+ }
20236
+ function selectOrphanWorktrees(worktrees, openIssueNumbers) {
20237
+ return worktrees.filter(
20238
+ (worktree) => worktree.issueNumber === null || !openIssueNumbers.has(worktree.issueNumber)
20239
+ );
20240
+ }
20241
+
20242
+ // packages/core/src/issue.ts
20243
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
20244
+ import { dirname, join as join5 } from "node:path";
20245
+ var SUMMARY_HEADING = /summary|要約|概要/i;
20246
+ var CRITERIA_HEADING = /acceptance criteria|受け入れ基準|完了条件|acceptance/i;
20247
+ var BULLET = /^\s*(?:[-*+]|\d+\.)\s+(?:\[[ xX]\]\s*)?(.+)$/;
20248
+ function parseIssueBody(body) {
20249
+ const sections = [{ title: "", lines: [] }];
20250
+ for (const line of body.replace(/\r\n?/g, "\n").split("\n")) {
20251
+ const heading = /^##\s+(.+?)\s*$/.exec(line);
20252
+ if (heading) {
20253
+ sections.push({ title: heading[1] ?? "", lines: [] });
20254
+ continue;
20255
+ }
20256
+ sections[sections.length - 1]?.lines.push(line);
20257
+ }
20258
+ const criteriaSection = sections.find((section) => CRITERIA_HEADING.test(section.title));
20259
+ const summarySection = sections.find((section) => SUMMARY_HEADING.test(section.title));
20260
+ const fallback = sections.find(
20261
+ (section) => section !== criteriaSection && section.lines.some((line) => line.trim() !== "")
20262
+ );
20263
+ const summarySource = summarySection ?? fallback;
20264
+ const summary = collapse((summarySource?.lines ?? []).join("\n"));
20265
+ const criteria = (criteriaSection?.lines ?? []).map((line) => BULLET.exec(line)?.[1]?.trim()).filter((line) => Boolean(line));
20266
+ return { summary, criteria };
20267
+ }
20268
+ function collapse(text) {
20269
+ return text.split("\n").map((line) => line.trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
20270
+ }
20271
+ function toIssue(raw) {
20272
+ const parsed = parseIssueBody(raw.body ?? "");
20273
+ return {
20274
+ number: raw.number,
20275
+ title: raw.title,
20276
+ labels: (raw.labels ?? []).map((label) => label.name),
20277
+ state: raw.state ?? "OPEN",
20278
+ updatedAt: raw.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString(),
20279
+ summary: parsed.summary,
20280
+ criteria: parsed.criteria
20281
+ };
20282
+ }
20283
+ async function fetchIssues(input2) {
20284
+ const args = [
20285
+ "issue",
20286
+ "list",
20287
+ "--repo",
20288
+ `${input2.repo.owner}/${input2.repo.name}`,
20289
+ "--state",
20290
+ "open",
20291
+ "--limit",
20292
+ String(input2.config.issueLimit),
20293
+ "--json",
20294
+ "number,title,labels,body,state,updatedAt"
20295
+ ];
20296
+ if (input2.config.issueLabelFilter) args.push("--label", input2.config.issueLabelFilter);
20297
+ const { stdout } = await input2.runner("gh", args, { cwd: input2.cwd });
20298
+ const rows = JSON.parse(stdout || "[]");
20299
+ return rows.map(toIssue).sort((left, right) => left.number - right.number);
20300
+ }
20301
+ function issueCachePath(paths, repo) {
20302
+ return join5(paths.issueCacheDir, `${repo.owner}__${repo.name}.json`);
20303
+ }
20304
+ function normalizeCachedIssue(value) {
20305
+ if (!value || typeof value !== "object") return null;
20306
+ const issue2 = value;
20307
+ if (typeof issue2.number !== "number" || typeof issue2.title !== "string") return null;
20308
+ return {
20309
+ number: issue2.number,
20310
+ title: issue2.title,
20311
+ labels: Array.isArray(issue2.labels) ? issue2.labels.filter((label) => typeof label === "string") : [],
20312
+ state: typeof issue2.state === "string" ? issue2.state : "OPEN",
20313
+ updatedAt: typeof issue2.updatedAt === "string" ? issue2.updatedAt : (/* @__PURE__ */ new Date(0)).toISOString(),
20314
+ summary: typeof issue2.summary === "string" ? issue2.summary : "",
20315
+ criteria: Array.isArray(issue2.criteria) ? issue2.criteria.filter((criterion) => typeof criterion === "string") : []
20316
+ };
20317
+ }
20318
+ function readIssueCache(path) {
20319
+ try {
20320
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
20321
+ if (!Array.isArray(parsed.issues) || typeof parsed.fetchedAt !== "number") {
20322
+ rmSync2(path, { force: true });
20323
+ return null;
20324
+ }
20325
+ const issues = parsed.issues.map(normalizeCachedIssue);
20326
+ if (issues.some((issue2) => issue2 === null)) {
20327
+ rmSync2(path, { force: true });
20328
+ return null;
20329
+ }
20330
+ return { fetchedAt: parsed.fetchedAt, issues };
20331
+ } catch {
20332
+ try {
20333
+ rmSync2(path, { force: true });
20334
+ } catch {
20335
+ }
20336
+ return null;
20337
+ }
20338
+ }
20339
+ function writeIssueCache(path, cache) {
20340
+ mkdirSync2(dirname(path), { recursive: true });
20341
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
20342
+ writeFileSync3(temporary, `${JSON.stringify(cache, null, 2)}
20343
+ `);
20344
+ renameSync2(temporary, path);
20345
+ }
20346
+ var CACHE_TTL_MS = 6e4;
20347
+ function classifyGhError(error62) {
20348
+ const message = error62 instanceof Error ? error62.message : String(error62);
20349
+ if (/not authenticated|authentication|401|gh auth login/i.test(message)) {
20350
+ return { code: "auth", message };
20351
+ }
20352
+ if (/rate limit|secondary rate|403/i.test(message)) {
20353
+ return { code: "rate_limit", message };
20354
+ }
20355
+ if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|fetch failed|network/i.test(message)) {
20356
+ return { code: "network", message };
20357
+ }
20358
+ if (/^gh\b|gh issue/i.test(message)) return { code: "gh", message };
20359
+ return { code: "unknown", message };
20360
+ }
20361
+ async function fetchIssue(input2) {
20362
+ const { stdout } = await input2.runner(
20363
+ "gh",
20364
+ [
20365
+ "issue",
20366
+ "view",
20367
+ String(input2.number),
20368
+ "--repo",
20369
+ `${input2.repo.owner}/${input2.repo.name}`,
20370
+ "--json",
20371
+ "number,title,labels,body,state,updatedAt"
20372
+ ],
20373
+ { cwd: input2.cwd }
20374
+ );
20375
+ return toIssue(JSON.parse(stdout));
20376
+ }
20377
+ function availableLabels(issues) {
20378
+ const seen = /* @__PURE__ */ new Map();
20379
+ for (const issue2 of issues) {
20380
+ for (const label of issue2.labels) {
20381
+ const key = label.toLocaleLowerCase();
20382
+ if (!seen.has(key)) seen.set(key, label);
20383
+ }
20384
+ }
20385
+ return [...seen.values()].sort((left, right) => left.localeCompare(right));
20386
+ }
20387
+ async function loadIssuesResult(input2) {
20388
+ const path = issueCachePath(input2.paths, input2.repo);
20389
+ const now = input2.now ?? Date.now();
20390
+ const cache = readIssueCache(path);
20391
+ if (!input2.force && cache && now - cache.fetchedAt < CACHE_TTL_MS) {
20392
+ return { issues: cache.issues, cached: true, stale: false, fetchedAt: cache.fetchedAt, error: null };
20393
+ }
20394
+ try {
20395
+ const issues = await fetchIssues({
20396
+ repo: input2.repo,
20397
+ config: input2.config,
20398
+ runner: input2.runner,
20399
+ cwd: input2.cwd
20400
+ });
20401
+ writeIssueCache(path, { fetchedAt: now, issues });
20402
+ return { issues, cached: false, stale: false, fetchedAt: now, error: null };
20403
+ } catch (error62) {
20404
+ const failure3 = classifyGhError(error62);
20405
+ if (cache) {
20406
+ return {
20407
+ issues: cache.issues,
20408
+ cached: true,
20409
+ stale: true,
20410
+ fetchedAt: cache.fetchedAt,
20411
+ error: failure3
20412
+ };
20413
+ }
20414
+ return { issues: [], cached: false, stale: false, fetchedAt: null, error: failure3 };
20415
+ }
20416
+ }
20417
+
20418
+ // packages/core/src/skills.ts
20419
+ import { createHash } from "node:crypto";
20420
+ import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
20421
+ import { join as join6, relative, sep as sep2 } from "node:path";
20422
+ var MANIFEST_NAME = ".skills-deck.json";
20423
+ function hashSkillDir(dir) {
20424
+ const hash2 = createHash("sha256");
20425
+ const walk = (current) => {
20426
+ const entries = readdirSync2(current, { withFileTypes: true }).sort(
20427
+ (left, right) => left.name.localeCompare(right.name)
20428
+ );
20429
+ for (const entry of entries) {
20430
+ const absolute = join6(current, entry.name);
20431
+ const key = relative(dir, absolute).split(sep2).join("/");
20432
+ if (entry.isDirectory()) {
20433
+ hash2.update(`d:${key}
20434
+ `);
20435
+ walk(absolute);
20436
+ } else if (entry.isFile()) {
20437
+ hash2.update(`f:${key}
20438
+ `);
20439
+ hash2.update(readFileSync5(absolute));
20440
+ hash2.update("\n");
20441
+ }
20442
+ }
20443
+ };
20444
+ walk(dir);
20445
+ return hash2.digest("hex");
20446
+ }
20447
+ function readManifest(targetDir) {
20448
+ try {
20449
+ const parsed = JSON.parse(readFileSync5(join6(targetDir, MANIFEST_NAME), "utf8"));
20450
+ if (!parsed || typeof parsed.version !== "string" || typeof parsed.skills !== "object") return null;
20451
+ return parsed;
20452
+ } catch {
20453
+ return null;
20454
+ }
20455
+ }
20456
+ function writeManifest(targetDir, manifest) {
20457
+ writeFileSync4(join6(targetDir, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}
20458
+ `);
20459
+ }
20460
+ function readBundledVersion(sourceDir) {
20461
+ try {
20462
+ return readFileSync5(join6(sourceDir, "VERSION"), "utf8").trim() || "unknown";
20463
+ } catch {
20464
+ return "unknown";
20465
+ }
20466
+ }
20467
+ function skillNames(sourceDir) {
20468
+ if (!existsSync3(sourceDir)) return [];
20469
+ return readdirSync2(sourceDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => existsSync3(join6(sourceDir, name, "SKILL.md"))).sort();
20470
+ }
20471
+ function bundledSkillNames(sourceDir) {
20472
+ return skillNames(sourceDir);
20473
+ }
20474
+ function installBundledSkills(input2) {
20475
+ const result = { installed: [], updated: [], skipped: [], unchanged: [] };
20476
+ if (!existsSync3(input2.sourceDir)) return result;
20477
+ mkdirSync3(input2.targetDir, { recursive: true });
20478
+ const version2 = readBundledVersion(input2.sourceDir);
20479
+ const manifest = readManifest(input2.targetDir) ?? { version: version2, skills: {} };
20480
+ const now = new Date(input2.now ?? Date.now()).toISOString();
20481
+ for (const name of skillNames(input2.sourceDir)) {
20482
+ const source = join6(input2.sourceDir, name);
20483
+ const target = join6(input2.targetDir, name);
20484
+ const sourceHash = hashSkillDir(source);
20485
+ if (!existsSync3(target)) {
20486
+ cpSync(source, target, { recursive: true });
20487
+ manifest.skills[name] = { hash: sourceHash, updatedAt: now };
20488
+ result.installed.push(name);
20489
+ continue;
20490
+ }
20491
+ const recorded = manifest.skills[name];
20492
+ const targetHash = hashSkillDir(target);
20493
+ if (!recorded) {
20494
+ if (targetHash === sourceHash) {
20495
+ manifest.skills[name] = { hash: targetHash, updatedAt: now };
20496
+ result.unchanged.push(name);
20497
+ } else {
20498
+ result.skipped.push(name);
20499
+ }
20500
+ continue;
20501
+ }
20502
+ if (recorded.hash !== targetHash) {
20503
+ result.skipped.push(name);
20504
+ continue;
20505
+ }
20506
+ if (sourceHash === targetHash) {
20507
+ result.unchanged.push(name);
20508
+ continue;
20509
+ }
20510
+ rmSync3(target, { recursive: true, force: true });
20511
+ cpSync(source, target, { recursive: true });
20512
+ manifest.skills[name] = { hash: sourceHash, updatedAt: now };
20513
+ result.updated.push(name);
20514
+ }
20515
+ manifest.version = version2;
20516
+ writeManifest(input2.targetDir, manifest);
20517
+ return result;
20518
+ }
20519
+
19828
20520
  // packages/core/src/version.ts
19829
20521
  var SUPPORTED_OPENCODE_MIN = "2.3.0";
19830
20522
  var SUPPORTED_OPENCODE_MAX_EXCLUSIVE = "3.0.0";
19831
20523
  var SUPPORTED_OPENCODE_RANGE = `>=${SUPPORTED_OPENCODE_MIN} <${SUPPORTED_OPENCODE_MAX_EXCLUSIVE}`;
20524
+ var VERSION = /(\d+)\.(\d+)\.(\d+)/;
20525
+ function parseVersion(input2) {
20526
+ if (!input2) return null;
20527
+ const match = VERSION.exec(input2);
20528
+ if (!match) return null;
20529
+ const major = Number(match[1]);
20530
+ const minor = Number(match[2]);
20531
+ const patch = Number(match[3]);
20532
+ if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) return null;
20533
+ return [major, minor, patch];
20534
+ }
20535
+ function compareVersions(left, right) {
20536
+ for (let index = 0; index < 3; index += 1) {
20537
+ const a = left[index] ?? 0;
20538
+ const b = right[index] ?? 0;
20539
+ if (a !== b) return a < b ? -1 : 1;
20540
+ }
20541
+ return 0;
20542
+ }
20543
+ function isSupportedVersion(input2) {
20544
+ const parsed = parseVersion(input2);
20545
+ if (!parsed) return false;
20546
+ return compareVersions(parsed, [2, 3, 0]) >= 0 && compareVersions(parsed, [3, 0, 0]) < 0;
20547
+ }
20548
+ function checkOpencodeVersion(input2) {
20549
+ const parsed = parseVersion(input2);
20550
+ if (!parsed) {
20551
+ return {
20552
+ ok: false,
20553
+ version: null,
20554
+ message: `OpenCode \u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\uFF08\u5BFE\u5FDC: ${SUPPORTED_OPENCODE_RANGE}\uFF09`
20555
+ };
20556
+ }
20557
+ const version2 = parsed.join(".");
20558
+ if (isSupportedVersion(version2)) return { ok: true, version: version2, message: "" };
20559
+ return {
20560
+ ok: false,
20561
+ version: version2,
20562
+ message: `OpenCode ${version2} \u306F\u672A\u5BFE\u5FDC\u3067\u3059\uFF08\u5BFE\u5FDC: ${SUPPORTED_OPENCODE_RANGE}\uFF09\u3002\u52D5\u4F5C\u306F\u4FDD\u8A3C\u3055\u308C\u307E\u305B\u3093`
20563
+ };
20564
+ }
20565
+
20566
+ // packages/plugin/src/contract.ts
20567
+ import { Rpc } from "@opencode/plugin/rpc";
20568
+ var repoViewSchema = external_exports.object({
20569
+ /** `owner/name`。 */
20570
+ repo: external_exports.string(),
20571
+ /** メインの作業ツリー(リンクされた worktree から解決してもここに戻す)。 */
20572
+ root: external_exports.string(),
20573
+ branch: external_exports.string(),
20574
+ defaultBranch: external_exports.string(),
20575
+ /** `config.json` の `repos` から来たか。 */
20576
+ configured: external_exports.boolean()
20577
+ });
20578
+ var issueViewSchema = external_exports.object({
20579
+ number: external_exports.number().int(),
20580
+ title: external_exports.string(),
20581
+ labels: external_exports.array(external_exports.string()),
20582
+ state: external_exports.string(),
20583
+ updatedAt: external_exports.string(),
20584
+ summary: external_exports.string(),
20585
+ criteria: external_exports.array(external_exports.string())
20586
+ });
20587
+ var worktreeViewSchema = external_exports.object({
20588
+ directory: external_exports.string(),
20589
+ branch: external_exports.string().nullable(),
20590
+ issueNumber: external_exports.number().int().nullable(),
20591
+ registered: external_exports.boolean(),
20592
+ exists: external_exports.boolean(),
20593
+ dirty: external_exports.boolean(),
20594
+ /** そのディレクトリを使っているアプリ内セッションの数。 */
20595
+ sessions: external_exports.number().int()
20596
+ });
20597
+ var syncViewSchema = external_exports.object({
20598
+ state: external_exports.enum(["fresh", "cached", "stale", "error"]),
20599
+ fetchedAt: external_exports.number().nullable(),
20600
+ message: external_exports.string().nullable(),
20601
+ code: external_exports.enum(["auth", "rate_limit", "network", "gh", "unknown"]).nullable()
20602
+ });
20603
+ var versionViewSchema = external_exports.object({
20604
+ app: external_exports.string().nullable(),
20605
+ supported: external_exports.boolean(),
20606
+ message: external_exports.string()
20607
+ });
20608
+ var promptsSchema = external_exports.object({
20609
+ /** `draftPromptIssue`。`{number}` を置換して使う。 */
20610
+ issue: external_exports.string(),
20611
+ /** `draftPromptSkill`。`{skill}` を置換して使う。 */
20612
+ skill: external_exports.string()
20613
+ });
20614
+ var skillsViewSchema = external_exports.object({
20615
+ /** 同梱スキルの名前。 */
20616
+ available: external_exports.array(external_exports.string()),
20617
+ /** 実際に配置した(または配置済みだった)スキルの名前。 */
20618
+ installed: external_exports.array(external_exports.string())
20619
+ });
20620
+ var boardSnapshotSchema = external_exports.object({
20621
+ /** 表示中の Repo。解決できなければ null。 */
20622
+ repo: repoViewSchema.nullable(),
20623
+ repos: external_exports.array(repoViewSchema),
20624
+ issues: external_exports.array(issueViewSchema),
20625
+ worktrees: external_exports.array(worktreeViewSchema),
20626
+ /** 検索・ラベル絞り込みのデータ面。 */
20627
+ labels: external_exports.array(external_exports.string()),
20628
+ sync: syncViewSchema,
20629
+ prompts: promptsSchema,
20630
+ version: versionViewSchema,
20631
+ skills: skillsViewSchema,
20632
+ /** Repo が無い・設定が壊れているなど、snapshot 全体の失敗。 */
20633
+ error: external_exports.string().nullable()
20634
+ });
20635
+ var failureSchema = external_exports.object({
20636
+ code: external_exports.enum(["repo", "issue", "gh", "git", "worktree", "session", "unknown"]),
20637
+ message: external_exports.string()
20638
+ });
20639
+ var ControlTower = Rpc.define({
20640
+ id: "opencode-skills-deck.control-tower",
20641
+ methods: {
20642
+ boardSnapshot: {
20643
+ input: external_exports.object({
20644
+ /** `owner/name`。省略時はセッションのディレクトリから解決した Repo。 */
20645
+ repo: external_exports.string().optional(),
20646
+ /** gh を呼び直す。 */
20647
+ force: external_exports.boolean().optional()
20648
+ }).optional(),
20649
+ output: boardSnapshotSchema
20650
+ },
20651
+ issueDetail: {
20652
+ input: external_exports.object({
20653
+ repo: external_exports.string().optional(),
20654
+ number: external_exports.number().int()
20655
+ }),
20656
+ output: external_exports.object({
20657
+ issue: issueViewSchema.nullable(),
20658
+ worktree: worktreeViewSchema.nullable(),
20659
+ error: failureSchema.nullable()
20660
+ })
20661
+ },
20662
+ repoList: {
20663
+ input: external_exports.object({ repo: external_exports.string().optional() }).optional(),
20664
+ output: external_exports.object({
20665
+ repos: external_exports.array(repoViewSchema),
20666
+ active: repoViewSchema.nullable()
20667
+ })
20668
+ },
20669
+ repoSwitch: {
20670
+ input: external_exports.object({ repo: external_exports.string() }),
20671
+ output: boardSnapshotSchema
20672
+ },
20673
+ dispatchPrepare: {
20674
+ input: external_exports.object({
20675
+ repo: external_exports.string().optional(),
20676
+ number: external_exports.number().int()
20677
+ }),
20678
+ output: external_exports.discriminatedUnion("ok", [
20679
+ external_exports.object({
20680
+ ok: external_exports.literal(true),
20681
+ issue: issueViewSchema,
20682
+ worktree: worktreeViewSchema,
20683
+ base: external_exports.string(),
20684
+ created: external_exports.boolean(),
20685
+ draft: external_exports.string()
20686
+ }),
20687
+ external_exports.object({ ok: external_exports.literal(false), error: failureSchema })
20688
+ ])
20689
+ },
20690
+ /**
20691
+ * New session 用に、worktree を用意して**その worktree で動くセッション**を作る(#40)。
20692
+ * アプリ 2.3 の `layout.draft` は directory を実行先に使わないため、セッションは server 側で作る。
20693
+ * 送信はしない。プロンプトは GUI が新しいセッションの composer に入れる。
20694
+ */
20695
+ sessionStart: {
20696
+ input: external_exports.object({
20697
+ repo: external_exports.string().optional(),
20698
+ number: external_exports.number().int()
20699
+ }),
20700
+ output: external_exports.discriminatedUnion("ok", [
20701
+ external_exports.object({
20702
+ ok: external_exports.literal(true),
20703
+ sessionID: external_exports.string(),
20704
+ directory: external_exports.string(),
20705
+ worktree: worktreeViewSchema,
20706
+ draft: external_exports.string()
20707
+ }),
20708
+ external_exports.object({ ok: external_exports.literal(false), error: failureSchema })
20709
+ ])
20710
+ },
20711
+ worktreeCleanup: {
20712
+ input: external_exports.object({
20713
+ repo: external_exports.string().optional(),
20714
+ number: external_exports.number().int().optional(),
20715
+ directory: external_exports.string().optional(),
20716
+ /** GUI 上でその worktree のタブが開いているか。 */
20717
+ inUse: external_exports.boolean().optional()
20718
+ }),
20719
+ output: external_exports.object({
20720
+ removed: external_exports.boolean(),
20721
+ reason: external_exports.enum(["removed", "missing", "dirty", "in_use", "outside", "unknown"]),
20722
+ message: external_exports.string().nullable()
20723
+ })
20724
+ },
20725
+ worktreeOrphans: {
20726
+ input: external_exports.object({ repo: external_exports.string().optional() }).optional(),
20727
+ output: external_exports.object({
20728
+ orphans: external_exports.array(worktreeViewSchema),
20729
+ error: failureSchema.nullable()
20730
+ })
20731
+ },
20732
+ sessionResume: {
20733
+ input: external_exports.object({
20734
+ repo: external_exports.string().optional(),
20735
+ number: external_exports.number().int()
20736
+ }),
20737
+ output: external_exports.discriminatedUnion("ok", [
20738
+ external_exports.object({
20739
+ ok: external_exports.literal(true),
20740
+ worktree: worktreeViewSchema.nullable(),
20741
+ draft: external_exports.string(),
20742
+ existing: external_exports.boolean()
20743
+ }),
20744
+ external_exports.object({ ok: external_exports.literal(false), error: failureSchema })
20745
+ ])
20746
+ }
20747
+ },
20748
+ events: {
20749
+ boardChanged: {
20750
+ schema: external_exports.object({
20751
+ repo: external_exports.string(),
20752
+ fetchedAt: external_exports.number()
20753
+ })
20754
+ }
20755
+ }
20756
+ });
20757
+
20758
+ // packages/plugin/src/rpc-handlers.ts
20759
+ function toRepoView(context) {
20760
+ return {
20761
+ repo: context.repo,
20762
+ root: context.root,
20763
+ branch: context.info?.branch ?? "?",
20764
+ defaultBranch: context.info?.defaultBranch ?? "main",
20765
+ configured: context.configured
20766
+ };
20767
+ }
20768
+ function toIssueView(issue2) {
20769
+ return {
20770
+ number: issue2.number,
20771
+ title: issue2.title,
20772
+ labels: issue2.labels,
20773
+ state: issue2.state,
20774
+ updatedAt: issue2.updatedAt,
20775
+ summary: issue2.summary,
20776
+ criteria: issue2.criteria
20777
+ };
20778
+ }
20779
+ function toSyncView(result) {
20780
+ let state;
20781
+ if (result.error) state = result.cached ? "stale" : "error";
20782
+ else state = result.cached ? "cached" : "fresh";
20783
+ return {
20784
+ state,
20785
+ fetchedAt: result.fetchedAt,
20786
+ message: result.error?.message ?? null,
20787
+ code: result.error?.code ?? null
20788
+ };
20789
+ }
20790
+ function versionView(appVersion) {
20791
+ const check2 = checkOpencodeVersion(appVersion);
20792
+ return { app: check2.version, supported: check2.ok, message: check2.message };
20793
+ }
20794
+ function failure2(code, error62) {
20795
+ return { code, message: error62 instanceof Error ? error62.message : String(error62) };
20796
+ }
20797
+ async function resolveActive(input2) {
20798
+ const contexts = await resolveRepoContexts({
20799
+ config: input2.ctx.config,
20800
+ directory: input2.ctx.directory,
20801
+ runner: input2.ctx.runner
20802
+ });
20803
+ const active = input2.repo ? contexts.find((context) => context.repo === input2.repo) ?? null : contexts[0] ?? null;
20804
+ return { repos: contexts.map(toRepoView), active, contexts };
20805
+ }
20806
+ async function toWorktreeView(ctx, status) {
20807
+ const sessions = ctx.sessionsIn ? await ctx.sessionsIn(status.directory) : 0;
20808
+ return {
20809
+ directory: status.directory,
20810
+ branch: status.branch,
20811
+ issueNumber: status.issueNumber,
20812
+ registered: status.registered,
20813
+ exists: status.exists,
20814
+ dirty: status.dirty,
20815
+ sessions
20816
+ };
20817
+ }
20818
+ async function worktreeForIssue(ctx, repoRoot, issueNumber) {
20819
+ const directory = issueWorktreePath(repoRoot, issueNumber);
20820
+ const status = await readWorktreeStatus({
20821
+ repoRoot,
20822
+ directory,
20823
+ runner: ctx.runner
20824
+ });
20825
+ return toWorktreeView(ctx, status);
20826
+ }
20827
+ async function snapshot(input2) {
20828
+ const { repos, active } = await resolveActive(input2);
20829
+ const base = {
20830
+ repo: active ? toRepoView(active) : null,
20831
+ repos,
20832
+ issues: [],
20833
+ worktrees: [],
20834
+ labels: [],
20835
+ sync: { state: "error", fetchedAt: null, message: null, code: null },
20836
+ prompts: {
20837
+ issue: input2.ctx.config.draftPromptIssue,
20838
+ skill: input2.ctx.config.draftPromptSkill
20839
+ },
20840
+ version: versionView(input2.ctx.appVersion),
20841
+ skills: input2.ctx.skills,
20842
+ error: null
20843
+ };
20844
+ if (!active) {
20845
+ return {
20846
+ ...base,
20847
+ error: repos.length === 0 ? "Could not resolve a repo context. Open a git repository or set repos in config.json." : `Repo ${input2.repo ?? ""} was not found.`
20848
+ };
20849
+ }
20850
+ const result = await loadIssuesResult({
20851
+ paths: input2.ctx.paths,
20852
+ repo: repoRef(active),
20853
+ config: input2.ctx.config,
20854
+ runner: input2.ctx.runner,
20855
+ cwd: active.root,
20856
+ force: input2.force,
20857
+ now: input2.ctx.now
20858
+ });
20859
+ const worktrees = await listIssueWorktrees({
20860
+ repoRoot: active.root,
20861
+ runner: input2.ctx.runner
20862
+ });
20863
+ const views = [];
20864
+ for (const worktree of worktrees) views.push(await toWorktreeView(input2.ctx, worktree));
20865
+ return {
20866
+ ...base,
20867
+ issues: [...result.issues].sort((left, right) => left.number - right.number).map(toIssueView),
20868
+ worktrees: views,
20869
+ labels: availableLabels(result.issues),
20870
+ sync: toSyncView(result)
20871
+ };
20872
+ }
20873
+ function emitChanged(ctx, repo) {
20874
+ ctx.emit?.("boardChanged", { repo, fetchedAt: ctx.now });
20875
+ }
20876
+ function createControlTowerHandlers(getContext) {
20877
+ return {
20878
+ async boardSnapshot(input2) {
20879
+ return snapshot({ ctx: await getContext(), repo: input2?.repo, force: input2?.force });
20880
+ },
20881
+ async issueDetail(input2) {
20882
+ const ctx = await getContext();
20883
+ const { active } = await resolveActive({ ctx, repo: input2.repo });
20884
+ if (!active) return { issue: null, worktree: null, error: failure2("repo", "Repo was not found.") };
20885
+ const result = await loadIssuesResult({
20886
+ paths: ctx.paths,
20887
+ repo: repoRef(active),
20888
+ config: ctx.config,
20889
+ runner: ctx.runner,
20890
+ cwd: active.root,
20891
+ now: ctx.now
20892
+ });
20893
+ let issue2 = result.issues.find((candidate) => candidate.number === input2.number);
20894
+ if (!issue2) {
20895
+ try {
20896
+ issue2 = await fetchIssue({
20897
+ repo: repoRef(active),
20898
+ number: input2.number,
20899
+ runner: ctx.runner,
20900
+ cwd: active.root
20901
+ });
20902
+ } catch (error62) {
20903
+ return {
20904
+ issue: null,
20905
+ worktree: null,
20906
+ error: failure2("issue", classifyGhError(error62).message)
20907
+ };
20908
+ }
20909
+ }
20910
+ return {
20911
+ issue: toIssueView(issue2),
20912
+ worktree: await worktreeForIssue(ctx, active.root, input2.number),
20913
+ error: null
20914
+ };
20915
+ },
20916
+ async repoList(input2) {
20917
+ const ctx = await getContext();
20918
+ const { repos, active } = await resolveActive({ ctx, repo: input2?.repo });
20919
+ return { repos, active: active ? toRepoView(active) : null };
20920
+ },
20921
+ async repoSwitch(input2) {
20922
+ return snapshot({ ctx: await getContext(), repo: input2.repo });
20923
+ },
20924
+ async dispatchPrepare(input2) {
20925
+ const ctx = await getContext();
20926
+ const { active } = await resolveActive({ ctx, repo: input2.repo });
20927
+ if (!active) return { ok: false, error: failure2("repo", "Repo was not found.") };
20928
+ let issue2;
20929
+ try {
20930
+ const result = await loadIssuesResult({
20931
+ paths: ctx.paths,
20932
+ repo: repoRef(active),
20933
+ config: ctx.config,
20934
+ runner: ctx.runner,
20935
+ cwd: active.root,
20936
+ now: ctx.now
20937
+ });
20938
+ issue2 = result.issues.find((candidate) => candidate.number === input2.number);
20939
+ } catch (error62) {
20940
+ return { ok: false, error: failure2("gh", classifyGhError(error62).message) };
20941
+ }
20942
+ if (!issue2) {
20943
+ try {
20944
+ issue2 = await fetchIssue({
20945
+ repo: repoRef(active),
20946
+ number: input2.number,
20947
+ runner: ctx.runner,
20948
+ cwd: active.root
20949
+ });
20950
+ } catch (error62) {
20951
+ return { ok: false, error: failure2("issue", classifyGhError(error62).message) };
20952
+ }
20953
+ }
20954
+ try {
20955
+ const plan = await ensureWorktree({
20956
+ repoRoot: active.root,
20957
+ issueNumber: issue2.number,
20958
+ title: issue2.title,
20959
+ baseBranch: ctx.config.baseBranch,
20960
+ runner: ctx.runner
20961
+ });
20962
+ emitChanged(ctx, active.repo);
20963
+ return {
20964
+ ok: true,
20965
+ issue: toIssueView(issue2),
20966
+ worktree: await worktreeForIssue(ctx, active.root, issue2.number),
20967
+ base: plan.base,
20968
+ created: plan.created,
20969
+ draft: fillTemplate(ctx.config.draftPromptIssue, { number: issue2.number })
20970
+ };
20971
+ } catch (error62) {
20972
+ return { ok: false, error: failure2("git", error62) };
20973
+ }
20974
+ },
20975
+ async sessionStart(input2) {
20976
+ const ctx = await getContext();
20977
+ const { active } = await resolveActive({ ctx, repo: input2.repo });
20978
+ if (!active) return { ok: false, error: failure2("repo", "Repo was not found.") };
20979
+ let issue2;
20980
+ try {
20981
+ const result = await loadIssuesResult({
20982
+ paths: ctx.paths,
20983
+ repo: repoRef(active),
20984
+ config: ctx.config,
20985
+ runner: ctx.runner,
20986
+ cwd: active.root,
20987
+ now: ctx.now
20988
+ });
20989
+ issue2 = result.issues.find((candidate) => candidate.number === input2.number);
20990
+ } catch (error62) {
20991
+ return { ok: false, error: failure2("gh", classifyGhError(error62).message) };
20992
+ }
20993
+ if (!issue2) {
20994
+ try {
20995
+ issue2 = await fetchIssue({
20996
+ repo: repoRef(active),
20997
+ number: input2.number,
20998
+ runner: ctx.runner,
20999
+ cwd: active.root
21000
+ });
21001
+ } catch (error62) {
21002
+ return { ok: false, error: failure2("issue", classifyGhError(error62).message) };
21003
+ }
21004
+ }
21005
+ try {
21006
+ const plan = await ensureWorktree({
21007
+ repoRoot: active.root,
21008
+ issueNumber: issue2.number,
21009
+ title: issue2.title,
21010
+ baseBranch: ctx.config.baseBranch,
21011
+ runner: ctx.runner
21012
+ });
21013
+ if (!ctx.createSession) {
21014
+ return { ok: false, error: failure2("session", "Sessions cannot be created here.") };
21015
+ }
21016
+ const sessionID = await ctx.createSession(plan.directory);
21017
+ emitChanged(ctx, active.repo);
21018
+ return {
21019
+ ok: true,
21020
+ sessionID,
21021
+ directory: plan.directory,
21022
+ worktree: await worktreeForIssue(ctx, active.root, issue2.number),
21023
+ draft: fillTemplate(ctx.config.draftPromptIssue, { number: issue2.number })
21024
+ };
21025
+ } catch (error62) {
21026
+ return { ok: false, error: failure2("session", error62) };
21027
+ }
21028
+ },
21029
+ async worktreeCleanup(input2) {
21030
+ const ctx = await getContext();
21031
+ const { active } = await resolveActive({ ctx, repo: input2.repo });
21032
+ if (!active) {
21033
+ return { removed: false, reason: "unknown", message: "Repo was not found." };
21034
+ }
21035
+ const directory = input2.directory ?? (input2.number !== void 0 ? issueWorktreePath(active.root, input2.number) : null);
21036
+ if (!directory) {
21037
+ return {
21038
+ removed: false,
21039
+ reason: "unknown",
21040
+ message: "Pass number or directory."
21041
+ };
21042
+ }
21043
+ if (!isInsideWorktrees(active.root, directory)) {
21044
+ return {
21045
+ removed: false,
21046
+ reason: "outside",
21047
+ message: "Only worktrees under .worktrees/ can be cleaned."
21048
+ };
21049
+ }
21050
+ if (input2.inUse) {
21051
+ return { removed: false, reason: "in_use", message: "A tab for this worktree is open." };
21052
+ }
21053
+ const result = await removeWorktree({
21054
+ repoRoot: active.root,
21055
+ directory,
21056
+ runner: ctx.runner
21057
+ });
21058
+ if (result.removed) emitChanged(ctx, active.repo);
21059
+ return {
21060
+ removed: result.removed,
21061
+ reason: result.removed ? "removed" : result.reason ?? "unknown",
21062
+ message: result.reason === "dirty" ? "Uncommitted changes; the worktree cannot be cleaned." : result.reason === "missing" ? "The worktree was not found." : null
21063
+ };
21064
+ },
21065
+ async worktreeOrphans(input2) {
21066
+ const ctx = await getContext();
21067
+ const { active } = await resolveActive({ ctx, repo: input2?.repo });
21068
+ if (!active) {
21069
+ return { orphans: [], error: failure2("repo", "Repo was not found.") };
21070
+ }
21071
+ const result = await loadIssuesResult({
21072
+ paths: ctx.paths,
21073
+ repo: repoRef(active),
21074
+ config: ctx.config,
21075
+ runner: ctx.runner,
21076
+ cwd: active.root,
21077
+ now: ctx.now
21078
+ });
21079
+ if (result.error && !result.cached) {
21080
+ return { orphans: [], error: failure2("gh", result.error.message) };
21081
+ }
21082
+ const worktrees = await listIssueWorktrees({
21083
+ repoRoot: active.root,
21084
+ runner: ctx.runner
21085
+ });
21086
+ const orphans = selectOrphanWorktrees(worktrees, new Set(result.issues.map((issue2) => issue2.number)));
21087
+ const views = [];
21088
+ for (const orphan of orphans) views.push(await toWorktreeView(ctx, orphan));
21089
+ return {
21090
+ orphans: views,
21091
+ error: result.error ? failure2("gh", result.error.message) : null
21092
+ };
21093
+ },
21094
+ async sessionResume(input2) {
21095
+ const ctx = await getContext();
21096
+ const { active } = await resolveActive({ ctx, repo: input2.repo });
21097
+ if (!active) return { ok: false, error: failure2("repo", "Repo was not found.") };
21098
+ let issue2;
21099
+ try {
21100
+ const result = await loadIssuesResult({
21101
+ paths: ctx.paths,
21102
+ repo: repoRef(active),
21103
+ config: ctx.config,
21104
+ runner: ctx.runner,
21105
+ cwd: active.root,
21106
+ now: ctx.now
21107
+ });
21108
+ issue2 = result.issues.find((candidate) => candidate.number === input2.number);
21109
+ } catch {
21110
+ issue2 = void 0;
21111
+ }
21112
+ const draft = fillTemplate(ctx.config.draftPromptIssue, { number: input2.number });
21113
+ if (!issue2) {
21114
+ return {
21115
+ ok: false,
21116
+ error: failure2("issue", `Issue #${input2.number} was not found.`)
21117
+ };
21118
+ }
21119
+ const worktree = await worktreeForIssue(ctx, active.root, input2.number);
21120
+ return {
21121
+ ok: true,
21122
+ worktree,
21123
+ draft,
21124
+ existing: worktree.registered && worktree.exists
21125
+ };
21126
+ }
21127
+ };
21128
+ }
21129
+ function repoRef(context) {
21130
+ if (context.info) return context.info.ref;
21131
+ const [owner = "unknown", name = context.repo] = context.repo.split("/");
21132
+ return { owner, name };
21133
+ }
19832
21134
 
19833
21135
  // packages/plugin/src/status.ts
19834
21136
  function sessionIDOf(event) {
@@ -19909,103 +21211,151 @@ var KEY = process.env.SKILLS_DECK_KEY;
19909
21211
  var REPO = process.env.SKILLS_DECK_REPO ?? "";
19910
21212
  var KIND = process.env.SKILLS_DECK_KIND === "dialogue" ? "dialogue" : "issue";
19911
21213
  var PREVIEW_FLUSH_MS = 400;
21214
+ function setupChildSession(ctx) {
21215
+ const paths = resolvePaths();
21216
+ const sessionsDir = sessionsDirFor(paths, REPO || null);
21217
+ const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
21218
+ const patch = (update) => {
21219
+ const current = readSession(sessionsDir, KEY);
21220
+ if (!current) return;
21221
+ writeSession(sessionsDir, mergeRecord(current, update, process.pid, stamp()));
21222
+ };
21223
+ const initial = readSession(sessionsDir, KEY);
21224
+ const record2 = initial ?? {
21225
+ key: KEY,
21226
+ kind: KIND,
21227
+ repo: REPO,
21228
+ label: KEY,
21229
+ pid: process.pid,
21230
+ status: "input_wait",
21231
+ reason: "idle",
21232
+ createdAt: stamp(),
21233
+ updatedAt: stamp()
21234
+ };
21235
+ writeSession(sessionsDir, {
21236
+ ...record2,
21237
+ pid: process.pid,
21238
+ status: "input_wait",
21239
+ reason: "idle",
21240
+ updatedAt: stamp()
21241
+ });
21242
+ let boundSessionID = record2.opencodeSessionID;
21243
+ let preview = record2.preview ?? "";
21244
+ let previewDirty = false;
21245
+ let flushTimer;
21246
+ const flushPreview = () => {
21247
+ if (!previewDirty) return;
21248
+ previewDirty = false;
21249
+ patch({ preview });
21250
+ };
21251
+ const scheduleFlush = () => {
21252
+ if (flushTimer) return;
21253
+ flushTimer = setTimeout(() => {
21254
+ flushTimer = void 0;
21255
+ flushPreview();
21256
+ }, PREVIEW_FLUSH_MS);
21257
+ flushTimer.unref?.();
21258
+ };
21259
+ const controller = new AbortController();
21260
+ const onExit = () => {
21261
+ try {
21262
+ flushPreview();
21263
+ patch({ status: "exited", exitedAt: stamp() });
21264
+ } catch {
21265
+ }
21266
+ };
21267
+ process.once("exit", onExit);
21268
+ void (async () => {
21269
+ try {
21270
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
21271
+ const typed = event;
21272
+ const sessionID = sessionIDOf(typed);
21273
+ if (!boundSessionID && sessionID) {
21274
+ boundSessionID = sessionID;
21275
+ patch({ opencodeSessionID: sessionID });
21276
+ } else if (boundSessionID && sessionID && sessionID !== boundSessionID) {
21277
+ continue;
21278
+ }
21279
+ const update = mapEvent(typed);
21280
+ if (update) {
21281
+ if (update.status === "exited") {
21282
+ preview = "";
21283
+ previewDirty = false;
21284
+ if (flushTimer) clearTimeout(flushTimer);
21285
+ flushTimer = void 0;
21286
+ patch({ ...update, preview: "" });
21287
+ } else if (typed.type === "session.execution.started") {
21288
+ preview = "";
21289
+ previewDirty = false;
21290
+ patch({ ...update, preview: "" });
21291
+ } else {
21292
+ patch(update);
21293
+ }
21294
+ }
21295
+ const delta = textDeltaOf(typed);
21296
+ if (delta) {
21297
+ preview = appendPreview(preview, delta);
21298
+ previewDirty = true;
21299
+ scheduleFlush();
21300
+ }
21301
+ }
21302
+ } catch {
21303
+ }
21304
+ })();
21305
+ return () => {
21306
+ controller.abort();
21307
+ process.off("exit", onExit);
21308
+ };
21309
+ }
19912
21310
  var index_default = Plugin.define({
19913
21311
  id: "opencode-skills-deck",
19914
21312
  async setup(ctx) {
19915
- if (!KEY) return;
19916
21313
  const paths = resolvePaths();
19917
- const sessionsDir = sessionsDirFor(paths, REPO || null);
19918
- const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
19919
- const patch = (update) => {
19920
- const current = readSession(sessionsDir, KEY);
19921
- if (!current) return;
19922
- writeSession(sessionsDir, mergeRecord(current, update, process.pid, stamp()));
19923
- };
19924
- const initial = readSession(sessionsDir, KEY);
19925
- const record2 = initial ?? {
19926
- key: KEY,
19927
- kind: KIND,
19928
- repo: REPO,
19929
- label: KEY,
19930
- pid: process.pid,
19931
- status: "input_wait",
19932
- reason: "idle",
19933
- createdAt: stamp(),
19934
- updatedAt: stamp()
19935
- };
19936
- writeSession(sessionsDir, {
19937
- ...record2,
19938
- pid: process.pid,
19939
- status: "input_wait",
19940
- reason: "idle",
19941
- updatedAt: stamp()
19942
- });
19943
- let boundSessionID = record2.opencodeSessionID;
19944
- let preview = record2.preview ?? "";
19945
- let previewDirty = false;
19946
- let flushTimer;
19947
- const flushPreview = () => {
19948
- if (!previewDirty) return;
19949
- previewDirty = false;
19950
- patch({ preview });
19951
- };
19952
- const scheduleFlush = () => {
19953
- if (flushTimer) return;
19954
- flushTimer = setTimeout(() => {
19955
- flushTimer = void 0;
19956
- flushPreview();
19957
- }, PREVIEW_FLUSH_MS);
19958
- flushTimer.unref?.();
19959
- };
19960
- const controller = new AbortController();
19961
- const onExit = () => {
21314
+ const config2 = loadConfig(paths);
21315
+ const sourceDir = fileURLToPath(new URL("../resources/skills", import.meta.url));
21316
+ const available = bundledSkillNames(sourceDir);
21317
+ let installed = available;
21318
+ if (config2.installSkillsOnStart) {
19962
21319
  try {
19963
- flushPreview();
19964
- patch({ status: "exited", exitedAt: stamp() });
21320
+ const result = installBundledSkills({
21321
+ sourceDir,
21322
+ targetDir: opencodeSkillsDir()
21323
+ });
21324
+ installed = [
21325
+ .../* @__PURE__ */ new Set([...result.installed, ...result.updated, ...result.unchanged, ...result.skipped])
21326
+ ];
19965
21327
  } catch {
19966
- }
19967
- };
19968
- process.once("exit", onExit);
19969
- void (async () => {
19970
- try {
19971
- for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
19972
- const typed = event;
19973
- const sessionID = sessionIDOf(typed);
19974
- if (!boundSessionID && sessionID) {
19975
- boundSessionID = sessionID;
19976
- patch({ opencodeSessionID: sessionID });
19977
- } else if (boundSessionID && sessionID && sessionID !== boundSessionID) {
19978
- continue;
19979
- }
19980
- const update = mapEvent(typed);
19981
- if (update) {
19982
- if (update.status === "exited") {
19983
- preview = "";
19984
- previewDirty = false;
19985
- if (flushTimer) clearTimeout(flushTimer);
19986
- flushTimer = void 0;
19987
- patch({ ...update, preview: "" });
19988
- } else if (typed.type === "session.execution.started") {
19989
- preview = "";
19990
- previewDirty = false;
19991
- patch({ ...update, preview: "" });
19992
- } else {
19993
- patch(update);
19994
- }
19995
- }
19996
- const delta = textDeltaOf(typed);
19997
- if (delta) {
19998
- preview = appendPreview(preview, delta);
19999
- previewDirty = true;
20000
- scheduleFlush();
20001
- }
21328
+ installed = [];
21329
+ }
21330
+ }
21331
+ let registration;
21332
+ registration = await ctx.rpc.register(
21333
+ ControlTower,
21334
+ createControlTowerHandlers(async () => ({
21335
+ directory: ctx.location.directory,
21336
+ config: config2,
21337
+ paths,
21338
+ runner: createRunner(),
21339
+ now: Date.now(),
21340
+ appVersion: ctx.app.version ?? null,
21341
+ skills: { available, installed },
21342
+ sessionsIn: async (directory) => {
21343
+ const response = await ctx.session.list({ directory });
21344
+ return response.data.length;
21345
+ },
21346
+ createSession: async (directory) => {
21347
+ const info = await ctx.session.create({ location: { directory } });
21348
+ return info.id;
21349
+ },
21350
+ emit: (event, data) => {
21351
+ void registration?.events.emit(event, data);
20002
21352
  }
20003
- } catch {
20004
- }
20005
- })();
21353
+ }))
21354
+ );
21355
+ const childCleanup = KEY ? setupChildSession(ctx) : void 0;
20006
21356
  return () => {
20007
- controller.abort();
20008
- process.off("exit", onExit);
21357
+ childCleanup?.();
21358
+ void registration?.dispose();
20009
21359
  };
20010
21360
  }
20011
21361
  });