@izantech/lineup-cli 2.1.0 → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  // src/cli.ts
2
2
  import { readFileSync as readFileSync6 } from "fs";
3
- import path8 from "path";
3
+ import path9 from "path";
4
4
  import process2 from "process";
5
5
  import { Command } from "commander";
6
6
 
@@ -15,7 +15,8 @@ function printTableLine(text) {
15
15
  }
16
16
 
17
17
  // src/lib/operations.ts
18
- import { rmSync as rmSync3 } from "fs";
18
+ import { rmSync as rmSync4 } from "fs";
19
+ import os2 from "os";
19
20
 
20
21
  // src/lib/errors.ts
21
22
  var CliError = class extends Error {
@@ -45,54 +46,114 @@ var LINEUP_PLUGIN_NAME = "lineup";
45
46
  var CLAUDE_LEGACY_PLUGIN = "lineup@izantech";
46
47
  var CLAUDE_LOCAL_MARKETPLACE_NAME = "lineup-local";
47
48
  var CLAUDE_LOCAL_PLUGIN = `${LINEUP_PLUGIN_NAME}@${CLAUDE_LOCAL_MARKETPLACE_NAME}`;
48
- var SUPPORTED_HOSTS = ["claude", "codex"];
49
+ var SUPPORTED_HOSTS = ["claude", "codex", "opencode"];
49
50
  var CODEX_SKILL_DIRS = [
50
51
  "lineup-kick-off",
51
52
  "lineup-configure",
52
53
  "lineup-explain",
53
- "lineup-playbook"
54
+ "lineup-playbook",
55
+ "lineup-digest"
54
56
  ];
55
57
  var CODEX_REQUIRED_FILES = [
56
58
  ".agents/skills/lineup-kick-off/SKILL.md",
57
59
  ".agents/skills/lineup-kick-off/INIT.md",
60
+ ".agents/skills/lineup-kick-off/STAGES-1-3.md",
61
+ ".agents/skills/lineup-kick-off/STAGES-4-5.md",
62
+ ".agents/skills/lineup-kick-off/STAGES-6-7.md",
58
63
  ".agents/skills/lineup-configure/SKILL.md",
59
64
  ".agents/skills/lineup-explain/SKILL.md",
60
- ".agents/skills/lineup-playbook/SKILL.md"
65
+ ".agents/skills/lineup-playbook/SKILL.md",
66
+ ".agents/skills/lineup-digest/SKILL.md"
67
+ ];
68
+ var OPENCODE_SKILL_DIRS = [
69
+ "lineup-kick-off",
70
+ "lineup-configure",
71
+ "lineup-explain",
72
+ "lineup-playbook",
73
+ "lineup-digest"
74
+ ];
75
+ var OPENCODE_REQUIRED_FILES = [
76
+ ".opencode/skills/lineup-kick-off/SKILL.md",
77
+ ".opencode/skills/lineup-kick-off/INIT.md",
78
+ ".opencode/skills/lineup-kick-off/STAGES-1-3.md",
79
+ ".opencode/skills/lineup-kick-off/STAGES-4-5.md",
80
+ ".opencode/skills/lineup-kick-off/STAGES-6-7.md",
81
+ ".opencode/skills/lineup-configure/SKILL.md",
82
+ ".opencode/skills/lineup-explain/SKILL.md",
83
+ ".opencode/skills/lineup-playbook/SKILL.md",
84
+ ".opencode/skills/lineup-digest/SKILL.md"
61
85
  ];
62
86
  var HOST_TEMPLATE_SPECS = [
63
87
  {
64
88
  source: ".lineup-core/skills/kick-off/core.md",
65
89
  targetFor: {
66
90
  claude: "skills/{{SKILL_NAME_KICKOFF}}/SKILL.md",
67
- codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/SKILL.md"
91
+ codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/SKILL.md",
92
+ opencode: ".opencode/skills/{{SKILL_NAME_KICKOFF}}/SKILL.md"
68
93
  }
69
94
  },
70
95
  {
71
96
  source: ".lineup-core/skills/kick-off/init.core.md",
72
97
  targetFor: {
73
98
  claude: "skills/{{SKILL_NAME_KICKOFF}}/INIT.md",
74
- codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/INIT.md"
99
+ codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/INIT.md",
100
+ opencode: ".opencode/skills/{{SKILL_NAME_KICKOFF}}/INIT.md"
101
+ }
102
+ },
103
+ {
104
+ source: ".lineup-core/skills/kick-off/stages-1-3.core.md",
105
+ targetFor: {
106
+ claude: "skills/{{SKILL_NAME_KICKOFF}}/STAGES-1-3.md",
107
+ codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/STAGES-1-3.md",
108
+ opencode: ".opencode/skills/{{SKILL_NAME_KICKOFF}}/STAGES-1-3.md"
109
+ }
110
+ },
111
+ {
112
+ source: ".lineup-core/skills/kick-off/stages-4-5.core.md",
113
+ targetFor: {
114
+ claude: "skills/{{SKILL_NAME_KICKOFF}}/STAGES-4-5.md",
115
+ codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/STAGES-4-5.md",
116
+ opencode: ".opencode/skills/{{SKILL_NAME_KICKOFF}}/STAGES-4-5.md"
117
+ }
118
+ },
119
+ {
120
+ source: ".lineup-core/skills/kick-off/stages-6-7.core.md",
121
+ targetFor: {
122
+ claude: "skills/{{SKILL_NAME_KICKOFF}}/STAGES-6-7.md",
123
+ codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/STAGES-6-7.md",
124
+ opencode: ".opencode/skills/{{SKILL_NAME_KICKOFF}}/STAGES-6-7.md"
75
125
  }
76
126
  },
77
127
  {
78
128
  source: ".lineup-core/skills/configure/core.md",
79
129
  targetFor: {
80
130
  claude: "skills/{{SKILL_NAME_CONFIGURE}}/SKILL.md",
81
- codex: ".agents/skills/{{SKILL_NAME_CONFIGURE}}/SKILL.md"
131
+ codex: ".agents/skills/{{SKILL_NAME_CONFIGURE}}/SKILL.md",
132
+ opencode: ".opencode/skills/{{SKILL_NAME_CONFIGURE}}/SKILL.md"
82
133
  }
83
134
  },
84
135
  {
85
136
  source: ".lineup-core/skills/explain/core.md",
86
137
  targetFor: {
87
138
  claude: "skills/{{SKILL_NAME_EXPLAIN}}/SKILL.md",
88
- codex: ".agents/skills/{{SKILL_NAME_EXPLAIN}}/SKILL.md"
139
+ codex: ".agents/skills/{{SKILL_NAME_EXPLAIN}}/SKILL.md",
140
+ opencode: ".opencode/skills/{{SKILL_NAME_EXPLAIN}}/SKILL.md"
89
141
  }
90
142
  },
91
143
  {
92
144
  source: ".lineup-core/skills/playbook/core.md",
93
145
  targetFor: {
94
146
  claude: "skills/{{SKILL_NAME_PLAYBOOK}}/SKILL.md",
95
- codex: ".agents/skills/{{SKILL_NAME_PLAYBOOK}}/SKILL.md"
147
+ codex: ".agents/skills/{{SKILL_NAME_PLAYBOOK}}/SKILL.md",
148
+ opencode: ".opencode/skills/{{SKILL_NAME_PLAYBOOK}}/SKILL.md"
149
+ }
150
+ },
151
+ {
152
+ source: ".lineup-core/skills/digest/core.md",
153
+ targetFor: {
154
+ claude: "skills/{{SKILL_NAME_DIGEST}}/SKILL.md",
155
+ codex: ".agents/skills/{{SKILL_NAME_DIGEST}}/SKILL.md",
156
+ opencode: ".opencode/skills/{{SKILL_NAME_DIGEST}}/SKILL.md"
96
157
  }
97
158
  }
98
159
  ];
@@ -153,6 +214,12 @@ function claudeHostRoot(homeDir = os.homedir()) {
153
214
  function codexHostRoot(homeDir = os.homedir()) {
154
215
  return path.join(lineupHome(homeDir), "hosts", "codex");
155
216
  }
217
+ function opencodeGlobalSkillsDir(homeDir = os.homedir()) {
218
+ return path.join(homeDir, ".config", "opencode", "skills");
219
+ }
220
+ function opencodeHostRoot(homeDir = os.homedir()) {
221
+ return path.join(lineupHome(homeDir), "hosts", "opencode");
222
+ }
156
223
  function claudeManagedPluginDir(version, homeDir = os.homedir()) {
157
224
  return path.join(claudeMarketplaceRoot(homeDir), "plugins", "lineup", version);
158
225
  }
@@ -171,6 +238,9 @@ function purgeTargets(hosts, homeDir = os.homedir()) {
171
238
  targets.push(path.join(homeDir, ".codex", "lineup", "agents"));
172
239
  targets.push(path.join(homeDir, ".codex", "lineup", "memory"));
173
240
  }
241
+ if (hosts.includes("opencode")) {
242
+ targets.push(path.join(homeDir, ".config", "opencode", "lineup"));
243
+ }
174
244
  return targets;
175
245
  }
176
246
 
@@ -361,7 +431,7 @@ function writeGeneratedFiles(files, outputRoot) {
361
431
  }
362
432
  }
363
433
  function prepareClaudePluginSkeleton(sourceRoot, outputRoot) {
364
- const copyPaths = [".claude-plugin", "agents", "tactics", "templates", "examples", "AGENTS.md", "CLAUDE.md"];
434
+ const copyPaths = [".claude-plugin", "agents", "tactics", "templates"];
365
435
  for (const entry of copyPaths) {
366
436
  const from = path3.join(sourceRoot, entry);
367
437
  const to = path3.join(outputRoot, entry);
@@ -435,6 +505,13 @@ function parseInstallPresence(output2) {
435
505
  legacyInstalled: new RegExp(CLAUDE_LEGACY_PLUGIN, "i").test(output2)
436
506
  };
437
507
  }
508
+ function checkPluginOnDisk() {
509
+ const marketplaceManifest = path4.join(claudeMarketplaceRoot(), ".claude-plugin", "marketplace.json");
510
+ if (existsSync3(marketplaceManifest)) {
511
+ return { installed: true, source: "cli-managed" };
512
+ }
513
+ return { installed: false, source: null };
514
+ }
438
515
  function writeMarketplace(root, pluginSource, version) {
439
516
  const dotClaude = path4.join(root, ".claude-plugin");
440
517
  mkdirSync2(dotClaude, { recursive: true });
@@ -479,13 +556,14 @@ async function statusClaude() {
479
556
  try {
480
557
  const result = await runCommand("claude", ["plugin", "list"]);
481
558
  if (result.code !== 0) {
559
+ const disk = checkPluginOnDisk();
482
560
  return {
483
561
  host: "claude",
484
- installed: false,
562
+ installed: disk.installed,
485
563
  version: null,
486
- source: null,
564
+ source: disk.source,
487
565
  last_action: null,
488
- error: result.stderr.trim() || "Failed to run claude plugin list"
566
+ error: disk.installed ? "claude plugin list unavailable; status detected from filesystem" : result.stderr.trim() || "Failed to run claude plugin list"
489
567
  };
490
568
  }
491
569
  const output2 = `${result.stdout}
@@ -641,6 +719,92 @@ function statusCodex(global = true) {
641
719
  };
642
720
  }
643
721
 
722
+ // src/lib/host-opencode.ts
723
+ import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
724
+ import path6 from "path";
725
+ function ensureOpencodeGenerated(sourceRoot, homeDir) {
726
+ const outputRoot = opencodeHostRoot(homeDir);
727
+ const files = generateHostFiles(sourceRoot, "opencode");
728
+ writeGeneratedFiles(files, outputRoot);
729
+ return path6.join(outputRoot, ".opencode", "skills");
730
+ }
731
+ function requiredAbsolutePaths2(baseDir) {
732
+ return OPENCODE_REQUIRED_FILES.map((relative) => path6.join(baseDir, ...relative.replace(/^\.opencode\/skills\//, "").split("/")));
733
+ }
734
+ function validateOpencodeSkillsDir(skillsDir) {
735
+ const missing = requiredAbsolutePaths2(skillsDir).filter((item) => !existsSync5(item));
736
+ if (missing.length > 0) {
737
+ throw new CliError([`OpenCode skills directory is missing required files in ${skillsDir}:`, ...missing.map((item) => `- ${item}`)].join("\n"), {
738
+ code: "opencode_skill_validation_failed"
739
+ });
740
+ }
741
+ }
742
+ function replaceDirectoryAtomic2(sourceDir, targetDir) {
743
+ const parentDir = path6.dirname(targetDir);
744
+ const nonce = `${Date.now()}-${process.pid}`;
745
+ const tempDir = path6.join(parentDir, `.${path6.basename(targetDir)}.tmp-${nonce}`);
746
+ const backupDir = path6.join(parentDir, `.${path6.basename(targetDir)}.bak-${nonce}`);
747
+ mkdirSync4(parentDir, { recursive: true });
748
+ cpSync3(sourceDir, tempDir, { recursive: true });
749
+ let movedTarget = false;
750
+ try {
751
+ if (existsSync5(targetDir)) {
752
+ renameSync2(targetDir, backupDir);
753
+ movedTarget = true;
754
+ }
755
+ renameSync2(tempDir, targetDir);
756
+ if (movedTarget && existsSync5(backupDir)) {
757
+ rmSync2(backupDir, { recursive: true, force: true });
758
+ }
759
+ } catch (error) {
760
+ if (existsSync5(tempDir)) {
761
+ rmSync2(tempDir, { recursive: true, force: true });
762
+ }
763
+ if (movedTarget && !existsSync5(targetDir) && existsSync5(backupDir)) {
764
+ renameSync2(backupDir, targetDir);
765
+ }
766
+ throw error;
767
+ }
768
+ }
769
+ function installOpencode(sourceRoot, homeDir) {
770
+ const sourceSkills = ensureOpencodeGenerated(sourceRoot, homeDir);
771
+ validateOpencodeSkillsDir(sourceSkills);
772
+ const destinationRoot = opencodeGlobalSkillsDir(homeDir);
773
+ mkdirSync4(destinationRoot, { recursive: true });
774
+ for (const dirName of OPENCODE_SKILL_DIRS) {
775
+ const from = path6.join(sourceSkills, dirName);
776
+ const to = path6.join(destinationRoot, dirName);
777
+ replaceDirectoryAtomic2(from, to);
778
+ }
779
+ validateOpencodeSkillsDir(destinationRoot);
780
+ return {
781
+ skills_dir: destinationRoot,
782
+ files_verified: OPENCODE_REQUIRED_FILES.length
783
+ };
784
+ }
785
+ function uninstallOpencode(homeDir) {
786
+ const root = opencodeGlobalSkillsDir(homeDir);
787
+ for (const dirName of OPENCODE_SKILL_DIRS) {
788
+ const target = path6.join(root, dirName);
789
+ if (existsSync5(target)) {
790
+ rmSync2(target, { recursive: true, force: true });
791
+ }
792
+ }
793
+ return { skills_dir: root };
794
+ }
795
+ function statusOpencode(homeDir) {
796
+ const root = opencodeGlobalSkillsDir(homeDir);
797
+ const missing = requiredAbsolutePaths2(root).filter((item) => !existsSync5(item));
798
+ return {
799
+ host: "opencode",
800
+ installed: missing.length === 0,
801
+ version: null,
802
+ source: null,
803
+ last_action: null,
804
+ ...missing.length > 0 ? { error: `Missing ${missing.length} required files.` } : {}
805
+ };
806
+ }
807
+
644
808
  // src/lib/prompts.ts
645
809
  import readline from "readline/promises";
646
810
  import { stdin as input, stdout as output } from "process";
@@ -669,9 +833,10 @@ async function promptHostSelection() {
669
833
  output.write("Select host(s):\n");
670
834
  output.write(" 1. claude\n");
671
835
  output.write(" 2. codex\n");
672
- output.write(" 3. all\n");
836
+ output.write(" 3. opencode\n");
837
+ output.write(" 4. all\n");
673
838
  while (true) {
674
- const answer = await rl.question("Enter selection [1-3]: ");
839
+ const answer = await rl.question("Enter selection [1-4]: ");
675
840
  const normalized = answer.trim().toLowerCase();
676
841
  if (normalized === "1" || normalized === "claude") {
677
842
  return ["claude"];
@@ -679,10 +844,13 @@ async function promptHostSelection() {
679
844
  if (normalized === "2" || normalized === "codex") {
680
845
  return ["codex"];
681
846
  }
682
- if (normalized === "3" || normalized === "all") {
683
- return ["claude", "codex"];
847
+ if (normalized === "3" || normalized === "opencode") {
848
+ return ["opencode"];
684
849
  }
685
- output.write("Invalid selection. Choose 1, 2, or 3.\n");
850
+ if (normalized === "4" || normalized === "all") {
851
+ return ["claude", "codex", "opencode"];
852
+ }
853
+ output.write("Invalid selection. Choose 1, 2, 3, or 4.\n");
686
854
  }
687
855
  } finally {
688
856
  rl.close();
@@ -714,7 +882,7 @@ async function promptUninstallPlan(hosts) {
714
882
  return { proceed: false, purge: false };
715
883
  }
716
884
  const purge = await promptConfirm(
717
- "Also purge Lineup data (~/.claude/lineup/agents, ~/.codex/lineup/agents, ~/.codex/lineup/memory)?",
885
+ "Also purge Lineup data (~/.claude/lineup/agents, ~/.codex/lineup/agents, ~/.codex/lineup/memory, ~/.config/opencode/lineup)?",
718
886
  false
719
887
  );
720
888
  return { proceed: true, purge };
@@ -723,16 +891,16 @@ async function promptUninstallPlan(hosts) {
723
891
  // src/lib/release.ts
724
892
  import { createHash } from "crypto";
725
893
  import {
726
- existsSync as existsSync5,
727
- mkdirSync as mkdirSync4,
894
+ existsSync as existsSync6,
895
+ mkdirSync as mkdirSync5,
728
896
  readdirSync as readdirSync2,
729
897
  readFileSync as readFileSync4,
730
- renameSync as renameSync2,
731
- rmSync as rmSync2,
898
+ renameSync as renameSync3,
899
+ rmSync as rmSync3,
732
900
  writeFileSync as writeFileSync3
733
901
  } from "fs";
734
902
  import { readFile } from "fs/promises";
735
- import path6 from "path";
903
+ import path7 from "path";
736
904
  var OWNER = "izantech";
737
905
  var REPO = "lineup";
738
906
  var API_BASE = `https://api.github.com/repos/${OWNER}/${REPO}`;
@@ -784,13 +952,13 @@ async function downloadBinary(url) {
784
952
  return Buffer.from(arrayBuffer);
785
953
  }
786
954
  function cachePaths(tag) {
787
- const cacheDir = path6.join(lineupCacheDir(), tag);
955
+ const cacheDir = path7.join(lineupCacheDir(), tag);
788
956
  return {
789
957
  cacheDir,
790
- manifestPath: path6.join(cacheDir, "manifest.json"),
791
- tarballPath: path6.join(cacheDir, "release.tar.gz"),
792
- extractDir: path6.join(cacheDir, "extracted"),
793
- sourceRoot: path6.join(cacheDir, "source")
958
+ manifestPath: path7.join(cacheDir, "manifest.json"),
959
+ tarballPath: path7.join(cacheDir, "release.tar.gz"),
960
+ extractDir: path7.join(cacheDir, "extracted"),
961
+ sourceRoot: path7.join(cacheDir, "source")
794
962
  };
795
963
  }
796
964
  function sha256File(filePath) {
@@ -842,15 +1010,29 @@ async function fetchReleaseManifest(tag) {
842
1010
  code: "release_manifest_missing"
843
1011
  });
844
1012
  }
845
- function validateExtractedSource(sourceRoot) {
846
- const required = [
1013
+ function validateExtractedSource(sourceRoot, hosts) {
1014
+ const coreRequired = [
847
1015
  ".lineup-core/skills/kick-off/core.md",
848
- ".lineup-core/hosts/claude.json",
849
- ".lineup-core/hosts/codex.json",
850
1016
  "agents/researcher.md",
851
1017
  "templates/tactic.yaml"
852
1018
  ];
853
- const missing = required.filter((item) => !existsSync5(path6.join(sourceRoot, item)));
1019
+ const hostAdapterFiles = {
1020
+ claude: ".lineup-core/hosts/claude.json",
1021
+ codex: ".lineup-core/hosts/codex.json",
1022
+ opencode: ".lineup-core/hosts/opencode.json"
1023
+ };
1024
+ const required = [...coreRequired];
1025
+ if (hosts && hosts.length > 0) {
1026
+ for (const host of hosts) {
1027
+ const adapterFile = hostAdapterFiles[host];
1028
+ if (adapterFile) {
1029
+ required.push(adapterFile);
1030
+ }
1031
+ }
1032
+ } else {
1033
+ required.push(...Object.values(hostAdapterFiles));
1034
+ }
1035
+ const missing = required.filter((item) => !existsSync6(path7.join(sourceRoot, item)));
854
1036
  if (missing.length > 0) {
855
1037
  throw new CliError(`Release source missing required files:
856
1038
  ${missing.map((item) => `- ${item}`).join("\n")}`, {
@@ -865,16 +1047,16 @@ function chooseExtractedRoot(extractDir) {
865
1047
  code: "extract_failed"
866
1048
  });
867
1049
  }
868
- return path6.join(extractDir, dirs[0].name);
1050
+ return path7.join(extractDir, dirs[0].name);
869
1051
  }
870
1052
  async function extractTarball(tarballPath, extractDir) {
871
- rmSync2(extractDir, { recursive: true, force: true });
872
- mkdirSync4(extractDir, { recursive: true });
1053
+ rmSync3(extractDir, { recursive: true, force: true });
1054
+ mkdirSync5(extractDir, { recursive: true });
873
1055
  const result = await runCommand("tar", ["-xzf", tarballPath, "-C", extractDir]);
874
1056
  assertSuccess(result, `tar -xzf ${tarballPath}`);
875
1057
  }
876
1058
  function loadCachedManifest(manifestPath) {
877
- if (!existsSync5(manifestPath)) {
1059
+ if (!existsSync6(manifestPath)) {
878
1060
  return null;
879
1061
  }
880
1062
  try {
@@ -887,10 +1069,10 @@ function loadCachedManifest(manifestPath) {
887
1069
  async function resolveRelease(input2 = {}) {
888
1070
  const tag = input2.version && input2.version !== "latest" ? input2.version : await resolveLatestTag();
889
1071
  const { cacheDir, manifestPath, tarballPath, extractDir, sourceRoot } = cachePaths(tag);
890
- mkdirSync4(cacheDir, { recursive: true });
1072
+ mkdirSync5(cacheDir, { recursive: true });
891
1073
  const cachedManifest = loadCachedManifest(manifestPath);
892
- if (cachedManifest && existsSync5(sourceRoot)) {
893
- validateExtractedSource(sourceRoot);
1074
+ if (cachedManifest && existsSync6(sourceRoot)) {
1075
+ validateExtractedSource(sourceRoot, input2.hosts);
894
1076
  return {
895
1077
  tag,
896
1078
  sourceRoot,
@@ -905,17 +1087,17 @@ async function resolveRelease(input2 = {}) {
905
1087
  writeFileSync3(tarballPath, tarball);
906
1088
  const digest = sha256File(tarballPath);
907
1089
  if (digest.toLowerCase() !== manifest.sha256.toLowerCase()) {
908
- rmSync2(tarballPath, { force: true });
1090
+ rmSync3(tarballPath, { force: true });
909
1091
  throw new CliError(`Checksum mismatch for ${tag}. expected=${manifest.sha256} actual=${digest}`, {
910
1092
  code: "checksum_mismatch"
911
1093
  });
912
1094
  }
913
1095
  await extractTarball(tarballPath, extractDir);
914
1096
  const extractedRoot = chooseExtractedRoot(extractDir);
915
- rmSync2(sourceRoot, { recursive: true, force: true });
916
- renameSync2(extractedRoot, sourceRoot);
917
- rmSync2(extractDir, { recursive: true, force: true });
918
- validateExtractedSource(sourceRoot);
1097
+ rmSync3(sourceRoot, { recursive: true, force: true });
1098
+ renameSync3(extractedRoot, sourceRoot);
1099
+ rmSync3(extractDir, { recursive: true, force: true });
1100
+ validateExtractedSource(sourceRoot, input2.hosts);
919
1101
  return {
920
1102
  tag,
921
1103
  sourceRoot,
@@ -923,17 +1105,17 @@ async function resolveRelease(input2 = {}) {
923
1105
  manifest
924
1106
  };
925
1107
  }
926
- function resolveLocalRelease(dirPath) {
927
- const resolvedPath = path6.resolve(dirPath);
928
- if (!existsSync5(resolvedPath)) {
1108
+ function resolveLocalRelease(dirPath, hosts) {
1109
+ const resolvedPath = path7.resolve(dirPath);
1110
+ if (!existsSync6(resolvedPath)) {
929
1111
  throw new CliError(`Local directory does not exist: ${resolvedPath}`, {
930
1112
  code: "local_dir_not_found"
931
1113
  });
932
1114
  }
933
- validateExtractedSource(resolvedPath);
1115
+ validateExtractedSource(resolvedPath, hosts);
934
1116
  let tag = "local";
935
1117
  try {
936
- const pkgPath = path6.join(resolvedPath, "cli", "package.json");
1118
+ const pkgPath = path7.join(resolvedPath, "cli", "package.json");
937
1119
  const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
938
1120
  if (parsed.version) {
939
1121
  tag = parsed.version;
@@ -953,8 +1135,8 @@ function resolveLocalRelease(dirPath) {
953
1135
  }
954
1136
 
955
1137
  // src/lib/state.ts
956
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
957
- import path7 from "path";
1138
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1139
+ import path8 from "path";
958
1140
  var STATE_SCHEMA_VERSION = 1;
959
1141
  function defaultState() {
960
1142
  return {
@@ -964,7 +1146,7 @@ function defaultState() {
964
1146
  };
965
1147
  }
966
1148
  function loadState(filePath = lineupStateFile()) {
967
- if (!existsSync6(filePath)) {
1149
+ if (!existsSync7(filePath)) {
968
1150
  return defaultState();
969
1151
  }
970
1152
  try {
@@ -982,7 +1164,7 @@ function saveState(state, filePath = lineupStateFile()) {
982
1164
  hosts: state.hosts
983
1165
  };
984
1166
  const valid = validateInstallerState(payload, filePath);
985
- mkdirSync5(path7.dirname(filePath), { recursive: true });
1167
+ mkdirSync6(path8.dirname(filePath), { recursive: true });
986
1168
  writeFileSync4(filePath, `${JSON.stringify(valid, null, 2)}
987
1169
  `, "utf8");
988
1170
  return valid;
@@ -1016,6 +1198,11 @@ var defaultDeps = {
1016
1198
  statusCodex,
1017
1199
  uninstallCodex,
1018
1200
  codexHostRoot,
1201
+ installOpencode,
1202
+ statusOpencode,
1203
+ uninstallOpencode,
1204
+ opencodeHostRoot,
1205
+ homeDir: os2.homedir,
1019
1206
  lineupStateFile,
1020
1207
  purgeTargets,
1021
1208
  isInteractive,
@@ -1025,7 +1212,7 @@ var defaultDeps = {
1025
1212
  saveState,
1026
1213
  updateHostState,
1027
1214
  removePath: (target) => {
1028
- rmSync3(target, { recursive: true, force: true });
1215
+ rmSync4(target, { recursive: true, force: true });
1029
1216
  },
1030
1217
  asErrorMessage
1031
1218
  };
@@ -1078,7 +1265,7 @@ function createOperations(overrides = {}) {
1078
1265
  return true;
1079
1266
  }
1080
1267
  async function performInstallOrUpdate2(input2) {
1081
- const release = input2.fromDir ? deps.resolveLocalRelease(input2.fromDir) : await deps.resolveRelease({ version: input2.version });
1268
+ const release = input2.fromDir ? deps.resolveLocalRelease(input2.fromDir, input2.hosts) : await deps.resolveRelease({ version: input2.version, hosts: input2.hosts });
1082
1269
  deps.validateSourceBundle(release.sourceRoot);
1083
1270
  const state = deps.loadState();
1084
1271
  const failures = [];
@@ -1127,6 +1314,21 @@ function createOperations(overrides = {}) {
1127
1314
  message: `Codex ${input2.action} complete (${release.tag}).`
1128
1315
  });
1129
1316
  }
1317
+ if (host === "opencode") {
1318
+ const opencodeResult = deps.installOpencode(release.sourceRoot, deps.homeDir());
1319
+ deps.updateHostState(state, "opencode", {
1320
+ installed: true,
1321
+ version: release.tag,
1322
+ source: "cli-managed",
1323
+ skills_dir: opencodeResult.skills_dir,
1324
+ last_action: input2.action
1325
+ });
1326
+ results.push({
1327
+ host,
1328
+ ok: true,
1329
+ message: `OpenCode ${input2.action} complete (${release.tag}).`
1330
+ });
1331
+ }
1130
1332
  } catch (error) {
1131
1333
  const message = deps.asErrorMessage(error);
1132
1334
  failures.push({ host, error: message });
@@ -1200,6 +1402,21 @@ function createOperations(overrides = {}) {
1200
1402
  message: "Codex uninstall complete."
1201
1403
  });
1202
1404
  }
1405
+ if (host === "opencode") {
1406
+ deps.uninstallOpencode(deps.homeDir());
1407
+ deps.updateHostState(state, "opencode", {
1408
+ installed: false,
1409
+ version: null,
1410
+ source: null,
1411
+ skills_dir: null,
1412
+ last_action: "uninstall"
1413
+ });
1414
+ results.push({
1415
+ host,
1416
+ ok: true,
1417
+ message: "OpenCode uninstall complete."
1418
+ });
1419
+ }
1203
1420
  } catch (error) {
1204
1421
  const message = deps.asErrorMessage(error);
1205
1422
  failures.push({ host, error: message });
@@ -1251,6 +1468,17 @@ function createOperations(overrides = {}) {
1251
1468
  } : void 0;
1252
1469
  outputHosts.codex = mergeStatus(stateHost, runtime);
1253
1470
  }
1471
+ if (host === "opencode") {
1472
+ const runtime = deps.statusOpencode(deps.homeDir());
1473
+ const stateHost = state.hosts.opencode ? {
1474
+ host: "opencode",
1475
+ installed: state.hosts.opencode.installed,
1476
+ version: state.hosts.opencode.version ?? null,
1477
+ source: state.hosts.opencode.source ?? null,
1478
+ last_action: state.hosts.opencode.last_action
1479
+ } : void 0;
1480
+ outputHosts.opencode = mergeStatus(stateHost, runtime);
1481
+ }
1254
1482
  }
1255
1483
  return {
1256
1484
  schema_version: state.schema_version,
@@ -1271,13 +1499,14 @@ var readStatus = operations.readStatus;
1271
1499
 
1272
1500
  // src/lib/hosts.ts
1273
1501
  var HOST_SET = /* @__PURE__ */ new Set([...SUPPORTED_HOSTS, "all"]);
1502
+ var HOST_OPTIONS = [...SUPPORTED_HOSTS, "all"];
1274
1503
  function normalizeHostOption(raw) {
1275
1504
  if (!raw) {
1276
1505
  return null;
1277
1506
  }
1278
1507
  const normalized = raw.trim().toLowerCase();
1279
1508
  if (!HOST_SET.has(normalized)) {
1280
- throw new CliError(`Invalid --host value: ${raw}. Expected claude, codex, or all.`, {
1509
+ throw new CliError(`Invalid --host value: ${raw}. Expected ${HOST_OPTIONS.join(", ")}.`, {
1281
1510
  code: "invalid_host"
1282
1511
  });
1283
1512
  }
@@ -1298,7 +1527,7 @@ async function resolveRequestedHosts(rawHost, options) {
1298
1527
  if (interactive) {
1299
1528
  return (options?.prompt ?? promptHostSelection)();
1300
1529
  }
1301
- throw new CliError("No host selected. Use --host claude|codex|all when running non-interactively.", {
1530
+ throw new CliError(`No host selected. Use --host ${HOST_OPTIONS.join("|")} when running non-interactively.`, {
1302
1531
  code: "host_required"
1303
1532
  });
1304
1533
  }
@@ -1388,7 +1617,7 @@ async function runUpdateCommand(options) {
1388
1617
 
1389
1618
  // src/cli.ts
1390
1619
  function packageVersion() {
1391
- const packageJsonPath = path8.join(packageRoot(), "package.json");
1620
+ const packageJsonPath = path9.join(packageRoot(), "package.json");
1392
1621
  const raw = readFileSync6(packageJsonPath, "utf8");
1393
1622
  const parsed = JSON.parse(raw);
1394
1623
  return parsed.version ?? "0.0.0";
@@ -1402,11 +1631,11 @@ function buildProgram(handlers) {
1402
1631
  ...handlers
1403
1632
  };
1404
1633
  const program = new Command();
1405
- program.name("lineup").description("Lineup multi-host manager for Claude Code and Codex").version(packageVersion(), "--cli-version", "output CLI version").showHelpAfterError();
1406
- program.command("install").description("Install Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|all").option("--version <tag>", "Release tag to install", "latest").option("--from-dir <path>", "Install from local directory instead of GitHub release").option("--yes", "Auto-confirm prompts").action(commandHandlers.install);
1407
- program.command("update").description("Update Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|all").option("--version <tag>", "Release tag to install", "latest").option("--from-dir <path>", "Install from local directory instead of GitHub release").option("--yes", "Auto-confirm prompts").action(commandHandlers.update);
1408
- program.command("uninstall").description("Uninstall Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|all").option("--yes", "Auto-confirm prompts").option("--purge", "Purge Lineup data directories").action(commandHandlers.uninstall);
1409
- program.command("status").description("Show Lineup installation status").option("--host <host>", "Target host(s): claude|codex|all").option("--json", "Emit machine-readable JSON output").action(commandHandlers.status);
1634
+ program.name("lineup").description("Lineup multi-host manager for Claude Code, Codex, and OpenCode").version(packageVersion(), "--cli-version", "output CLI version").showHelpAfterError();
1635
+ program.command("install").description("Install Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|opencode|all").option("--version <tag>", "Release tag to install", "latest").option("--from-dir <path>", "Install from local directory instead of GitHub release").option("--yes", "Auto-confirm prompts").action(commandHandlers.install);
1636
+ program.command("update").description("Update Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|opencode|all").option("--version <tag>", "Release tag to install", "latest").option("--from-dir <path>", "Install from local directory instead of GitHub release").option("--yes", "Auto-confirm prompts").action(commandHandlers.update);
1637
+ program.command("uninstall").description("Uninstall Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|opencode|all").option("--yes", "Auto-confirm prompts").option("--purge", "Purge Lineup data directories").action(commandHandlers.uninstall);
1638
+ program.command("status").description("Show Lineup installation status").option("--host <host>", "Target host(s): claude|codex|opencode|all").option("--json", "Emit machine-readable JSON output").action(commandHandlers.status);
1410
1639
  return program;
1411
1640
  }
1412
1641
  function printCliError(error) {
@@ -1433,7 +1662,7 @@ function isDirectExecution(argv) {
1433
1662
  if (!entry) {
1434
1663
  return false;
1435
1664
  }
1436
- return path8.resolve(entry) === path8.resolve(packageRoot(), "dist", "cli.js");
1665
+ return path9.resolve(entry) === path9.resolve(packageRoot(), "dist", "cli.js");
1437
1666
  }
1438
1667
  if (isDirectExecution(process2.argv)) {
1439
1668
  run().catch(handleFatalError);
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  printCliError,
5
5
  resolveExitCode,
6
6
  run
7
- } from "./chunk-RQBQVCEG.js";
7
+ } from "./chunk-G7CR3AA7.js";
8
8
  export {
9
9
  buildProgram,
10
10
  handleFatalError,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { CliHandlers, buildProgram, printCliError, resolveExitCode, run } from './cli.js';
2
2
  import 'commander';
3
3
 
4
- declare const SUPPORTED_HOSTS: readonly ["claude", "codex"];
4
+ declare const SUPPORTED_HOSTS: readonly ["claude", "codex", "opencode"];
5
5
  type HostName = (typeof SUPPORTED_HOSTS)[number];
6
6
 
7
7
  type HostState = {
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  validateHostAdapter,
7
7
  validateInstallerState,
8
8
  validateReleaseManifest
9
- } from "./chunk-RQBQVCEG.js";
9
+ } from "./chunk-G7CR3AA7.js";
10
10
  export {
11
11
  buildProgram,
12
12
  printCliError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@izantech/lineup-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "description": "Lineup multi-host installer and manager for Claude Code and Codex",
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@
8
8
  "properties": {
9
9
  "host": {
10
10
  "type": "string",
11
- "enum": ["claude", "codex", "gemini"]
11
+ "enum": ["claude", "codex", "opencode"]
12
12
  },
13
13
  "vars": {
14
14
  "type": "object",
@@ -18,10 +18,12 @@
18
18
  "SKILL_NAME_CONFIGURE",
19
19
  "SKILL_NAME_EXPLAIN",
20
20
  "SKILL_NAME_PLAYBOOK",
21
+ "SKILL_NAME_DIGEST",
21
22
  "CMD_KICKOFF",
22
23
  "CMD_CONFIGURE",
23
24
  "CMD_EXPLAIN",
24
25
  "CMD_PLAYBOOK",
26
+ "CMD_DIGEST",
25
27
  "KICKOFF_INIT_PATH",
26
28
  "QUESTION_PRIMITIVE",
27
29
  "OVERRIDES_DIR",
@@ -32,17 +34,20 @@
32
34
  "HOST_DEFAULTS_TERM",
33
35
  "HOST_ARTIFACT_LABEL",
34
36
  "HOST_ARTIFACT_LABEL_LOWER",
35
- "AGENTS_DIR"
37
+ "AGENTS_DIR",
38
+ "OLLAMA_CONFIG_PATH"
36
39
  ],
37
40
  "properties": {
38
41
  "SKILL_NAME_KICKOFF": { "type": "string" },
39
42
  "SKILL_NAME_CONFIGURE": { "type": "string" },
40
43
  "SKILL_NAME_EXPLAIN": { "type": "string" },
41
44
  "SKILL_NAME_PLAYBOOK": { "type": "string" },
45
+ "SKILL_NAME_DIGEST": { "type": "string" },
42
46
  "CMD_KICKOFF": { "type": "string" },
43
47
  "CMD_CONFIGURE": { "type": "string" },
44
48
  "CMD_EXPLAIN": { "type": "string" },
45
49
  "CMD_PLAYBOOK": { "type": "string" },
50
+ "CMD_DIGEST": { "type": "string" },
46
51
  "KICKOFF_INIT_PATH": { "type": "string" },
47
52
  "QUESTION_PRIMITIVE": { "type": "string" },
48
53
  "OVERRIDES_DIR": { "type": "string" },
@@ -53,7 +58,8 @@
53
58
  "HOST_DEFAULTS_TERM": { "type": "string" },
54
59
  "HOST_ARTIFACT_LABEL": { "type": "string" },
55
60
  "HOST_ARTIFACT_LABEL_LOWER": { "type": "string" },
56
- "AGENTS_DIR": { "type": "string" }
61
+ "AGENTS_DIR": { "type": "string" },
62
+ "OLLAMA_CONFIG_PATH": { "type": "string" }
57
63
  }
58
64
  }
59
65
  }
@@ -23,6 +23,9 @@
23
23
  },
24
24
  "codex": {
25
25
  "$ref": "#/$defs/hostState"
26
+ },
27
+ "opencode": {
28
+ "$ref": "#/$defs/hostState"
26
29
  }
27
30
  }
28
31
  }
@@ -20,8 +20,12 @@
20
20
  "items": {
21
21
  "type": "object",
22
22
  "additionalProperties": false,
23
- "required": ["type", "agent"],
24
23
  "properties": {
24
+ "tactic": {
25
+ "type": "string",
26
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
27
+ "description": "Name of another tactic to inline at this position"
28
+ },
25
29
  "type": {
26
30
  "type": "string",
27
31
  "enum": [
@@ -49,7 +53,15 @@
49
53
  "type": ["string", "null"],
50
54
  "enum": ["approval", null]
51
55
  }
52
- }
56
+ },
57
+ "if": { "properties": { "tactic": true }, "required": ["tactic"] },
58
+ "then": {
59
+ "allOf": [
60
+ { "not": { "properties": { "type": true }, "required": ["type"] } },
61
+ { "not": { "properties": { "agent": true }, "required": ["agent"] } }
62
+ ]
63
+ },
64
+ "else": { "properties": { "type": true, "agent": true }, "required": ["type", "agent"] }
53
65
  }
54
66
  },
55
67
  "verification": {