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