@osovv/vvcode 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +90 -13
  2. package/dist/vvcode.js +444 -35
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,14 +8,20 @@
8
8
  bun add -g @osovv/vvcode
9
9
  ```
10
10
 
11
- Or for local development:
11
+ For local development from this repo:
12
12
 
13
13
  ```bash
14
14
  bun install
15
15
  bun run cli -- --help
16
16
  ```
17
17
 
18
- ## Runtime config
18
+ ## Config layout
19
+
20
+ Global config path:
21
+
22
+ ```text
23
+ ~/.config/vvcode/config.jsonc
24
+ ```
19
25
 
20
26
  Project config path:
21
27
 
@@ -23,30 +29,101 @@ Project config path:
23
29
  .vvcode/config.jsonc
24
30
  ```
25
31
 
26
- Default global config path:
32
+ The current behavior is:
27
33
 
28
- ```text
29
- ~/.config/vvcode/config.jsonc
34
+ 1. Global config is the source of shared presets and profiles.
35
+ 2. Project config is optional for launch if a project root already exists.
36
+ 3. `vvcode init` creates a minimal project config scaffold.
37
+ 4. `vvcode preset add <name>` copies a preset from global config into the project config.
38
+ 5. `vvcode preset remove <name>` removes a copied project preset and prunes unused copied profiles.
39
+
40
+ ## Quick start
41
+
42
+ Initialize a project-local config:
43
+
44
+ ```bash
45
+ vvcode init
46
+ ```
47
+
48
+ See which presets are available:
49
+
50
+ ```bash
51
+ vvcode preset list
52
+ ```
53
+
54
+ Copy a global preset into the current project:
55
+
56
+ ```bash
57
+ vvcode preset add openai
58
+ ```
59
+
60
+ Launch OpenCode with a preset:
61
+
62
+ ```bash
63
+ vvcode opencode --preset openai
64
+ ```
65
+
66
+ Or use the shorthand form:
67
+
68
+ ```bash
69
+ vvcode --preset openai
70
+ ```
71
+
72
+ ## Commands
73
+
74
+ Show help:
75
+
76
+ ```bash
77
+ vvcode help
78
+ vvcode --help
79
+ ```
80
+
81
+ Initialize project config:
82
+
83
+ ```bash
84
+ vvcode init
85
+ vvcode init --project-root /path/to/project
30
86
  ```
31
87
 
32
- ## Usage
88
+ Launch OpenCode:
33
89
 
34
90
  ```bash
35
91
  vvcode opencode --preset <name>
92
+ vvcode --preset <name>
93
+ ```
94
+
95
+ By default `vvcode` starts the shell with:
96
+
97
+ ```bash
98
+ opencode
99
+ ```
100
+
101
+ You can override the shell command, for example:
102
+
103
+ ```bash
104
+ OPENCODE_SHELL_COMMAND='bun x opencode-ai' vvcode --preset openai
36
105
  ```
37
106
 
38
107
  Options:
39
108
 
40
- - `--preset`, `-p` (required)
41
- - `--global-config`, `-c` (optional)
42
- - `--project-root`, `-r` (optional)
109
+ 1. `--preset`, `-p` for the preset name.
110
+ 2. `--global-config`, `-c` to override the global config file path.
111
+ 3. `--project-root`, `-r` to launch from a different project root.
43
112
 
44
- You can also omit `opencode` because it is the default subcommand:
113
+ Manage presets:
45
114
 
46
115
  ```bash
47
- vvcode --preset <name>
116
+ vvcode preset list
117
+ vvcode preset add <preset-name>
118
+ vvcode preset remove <preset-name>
48
119
  ```
49
120
 
121
+ `preset list` prints one preset per line with its source label:
122
+
123
+ 1. `global`
124
+ 2. `project`
125
+ 3. `project-overrides-global`
126
+
50
127
  ## Runtime artifacts
51
128
 
52
129
  - `.vvcode/local/runtime/overlays/`
@@ -60,9 +137,9 @@ vvcode --preset <name>
60
137
  ## Packaging notes
61
138
 
62
139
  - Package name: `@osovv/vvcode`
63
- - CLI bin: `bin/vvcode`
140
+ - CLI bin: `vvcode`
64
141
  - Published runtime entry: `dist/vvcode.js`
65
142
  - Build command: `bun run build`
66
143
  - Local dev command: `bun run cli -- --help`
67
- - `prepack` builds `dist` automatically before publish/pack
144
+ - `prepack` builds `dist` automatically before publish or pack
68
145
  - Scoped publish target: `npm publish --access=public`
package/dist/vvcode.js CHANGED
@@ -543,11 +543,11 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
543
543
 
544
544
  // src/cli/vvcode.ts
545
545
  import { homedir } from "os";
546
- import { join as join3 } from "path";
546
+ import { join as join3, resolve } from "path";
547
547
  // package.json
548
548
  var package_default = {
549
549
  name: "@osovv/vvcode",
550
- version: "0.4.1",
550
+ version: "0.5.1",
551
551
  description: "Packaged CLI for launching OpenCode with vvcode runtime semantics.",
552
552
  license: "MIT",
553
553
  homepage: "https://github.com/osovv/vvcode",
@@ -565,7 +565,7 @@ var package_default = {
565
565
  "dist"
566
566
  ],
567
567
  bin: {
568
- vvcode: "./bin/vvcode"
568
+ vvcode: "bin/vvcode"
569
569
  },
570
570
  publishConfig: {
571
571
  access: "public"
@@ -602,10 +602,6 @@ var package_default = {
602
602
  ]
603
603
  };
604
604
 
605
- // src/cli/pivv-opencode.ts
606
- import { rm } from "fs/promises";
607
- import { spawn } from "child_process";
608
-
609
605
  // src/config/load-pivv-config.ts
610
606
  class PivvConfigError extends Error {
611
607
  code;
@@ -628,9 +624,6 @@ async function loadPivvConfig(globalConfigPath, projectRoot) {
628
624
  for (const sourceCandidate of sourceCandidates) {
629
625
  const sourcePath = sourceCandidate.path;
630
626
  const sourceExists = await pathExists(sourcePath);
631
- if (!sourceExists && sourceCandidate.sourceType === "project") {
632
- throw new PivvConfigError("CONFIG_NOT_FOUND", `Project config not found at ${sourcePath}`);
633
- }
634
627
  if (!sourceExists) {
635
628
  continue;
636
629
  }
@@ -674,6 +667,37 @@ async function findProjectRoot(projectRoot) {
674
667
  }
675
668
  throw new PivvConfigError("CONFIG_NOT_FOUND", `Unable to resolve project root from ${projectRoot}`);
676
669
  }
670
+ function resolveProjectConfigPath(projectRoot) {
671
+ return joinPath(resolvePath(projectRoot), PROJECT_CONFIG_PATH);
672
+ }
673
+ function createEmptyRawVvcodeConfig() {
674
+ return {
675
+ profiles: {},
676
+ presets: {},
677
+ memory: {},
678
+ overlay: {}
679
+ };
680
+ }
681
+ async function readOptionalRawVvcodeConfig(configPath) {
682
+ const normalizedPath = resolvePath(configPath);
683
+ if (!await pathExists(normalizedPath)) {
684
+ return null;
685
+ }
686
+ return readJsoncConfig(normalizedPath);
687
+ }
688
+ async function writeRawVvcodeConfig(configPath, config) {
689
+ const normalizedPath = resolvePath(configPath);
690
+ const fsPromises = await getFsPromises();
691
+ const stableConfig = {
692
+ profiles: isRecord(config.profiles) ? config.profiles : {},
693
+ presets: isRecord(config.presets) ? config.presets : {},
694
+ memory: isRecord(config.memory) ? config.memory : {},
695
+ overlay: isRecord(config.overlay) ? config.overlay : {}
696
+ };
697
+ await fsPromises.mkdir(getParentPath(normalizedPath), { recursive: true });
698
+ await fsPromises.writeFile(normalizedPath, `${JSON.stringify(stableConfig, null, 2)}
699
+ `, "utf-8");
700
+ }
677
701
  function normalizeConfig(rawConfig, projectRoot, configSources) {
678
702
  if (!isRecord(rawConfig)) {
679
703
  throw new PivvConfigError("CONFIG_INVALID", "Config root must be an object");
@@ -899,6 +923,10 @@ function normalizePath(path) {
899
923
  return withSingleSlashes;
900
924
  }
901
925
 
926
+ // src/cli/pivv-opencode.ts
927
+ import { rm } from "fs/promises";
928
+ import { spawn } from "child_process";
929
+
902
930
  // src/opencode/build-runtime-overlay.ts
903
931
  import { mkdir, writeFile } from "fs/promises";
904
932
  import { join } from "path";
@@ -1331,7 +1359,8 @@ function createTraceWriteError(message, cause) {
1331
1359
  // src/cli/pivv-opencode.ts
1332
1360
  var LAUNCHER_LOG_PREFIX = "[VvcodeLauncher]";
1333
1361
  var BLOCK_LOAD_ACTIVE_PRESET = "BLOCK_LOAD_ACTIVE_PRESET";
1334
- var OPENCODE_ENTRYPOINT = "bun x opencode-ai";
1362
+ var DEFAULT_OPENCODE_SHELL_COMMAND = "opencode";
1363
+ var OPENCODE_SHELL_COMMAND_ENV = "OPENCODE_SHELL_COMMAND";
1335
1364
  function buildRuntimeEnvironment(runtimeOverlay, baseEnvironment = process.env) {
1336
1365
  const runtimeEnvironment = {};
1337
1366
  for (const [key, value] of Object.entries(baseEnvironment)) {
@@ -1340,7 +1369,12 @@ function buildRuntimeEnvironment(runtimeOverlay, baseEnvironment = process.env)
1340
1369
  }
1341
1370
  }
1342
1371
  runtimeEnvironment.OPENCODE_CONFIG_DIR = runtimeOverlay.overlayDirPath;
1343
- runtimeEnvironment.OPENCODE_CONFIG_CONTENT = JSON.stringify(runtimeOverlay);
1372
+ runtimeEnvironment.OPENCODE_CONFIG_CONTENT = JSON.stringify({
1373
+ provider: normalizeProviderConfig(runtimeOverlay.providers),
1374
+ agent: runtimeOverlay.agents,
1375
+ command: normalizeCommandConfig(runtimeOverlay.commands),
1376
+ tools: runtimeOverlay.tools
1377
+ });
1344
1378
  return runtimeEnvironment;
1345
1379
  }
1346
1380
  async function runPivvOpencode(cliArgs) {
@@ -1355,6 +1389,8 @@ async function runPivvOpencode(cliArgs) {
1355
1389
  let aliasModelCount = 0;
1356
1390
  let providerIds = [];
1357
1391
  let lifecycleStage = "config.load";
1392
+ const openCodeShellCommand = resolveOpenCodeShellCommand();
1393
+ const openCodeEntrypoint = openCodeShellCommand.entrypoint;
1358
1394
  if (!traceRecorder) {
1359
1395
  try {
1360
1396
  resolvedProjectRoot = await findProjectRoot(cliArgs.projectRoot);
@@ -1373,7 +1409,7 @@ async function runPivvOpencode(cliArgs) {
1373
1409
  preset: cliArgs.preset,
1374
1410
  projectRoot: resolvedProjectRoot,
1375
1411
  cwd: resolvedProjectRoot,
1376
- entrypoint: OPENCODE_ENTRYPOINT
1412
+ entrypoint: openCodeEntrypoint
1377
1413
  }
1378
1414
  });
1379
1415
  try {
@@ -1387,7 +1423,7 @@ async function runPivvOpencode(cliArgs) {
1387
1423
  preset: cliArgs.preset,
1388
1424
  projectRoot: resolvedProjectRoot,
1389
1425
  cwd: resolvedProjectRoot,
1390
- entrypoint: OPENCODE_ENTRYPOINT
1426
+ entrypoint: openCodeEntrypoint
1391
1427
  }
1392
1428
  });
1393
1429
  const projectConfig = await loadPivvConfig(cliArgs.globalConfigPath, cliArgs.projectRoot);
@@ -1402,7 +1438,7 @@ async function runPivvOpencode(cliArgs) {
1402
1438
  preset: cliArgs.preset,
1403
1439
  projectRoot: resolvedProjectRoot,
1404
1440
  cwd: resolvedProjectRoot,
1405
- entrypoint: OPENCODE_ENTRYPOINT
1441
+ entrypoint: openCodeEntrypoint
1406
1442
  }
1407
1443
  });
1408
1444
  lifecycleStage = "preset.resolve";
@@ -1416,7 +1452,7 @@ async function runPivvOpencode(cliArgs) {
1416
1452
  preset: cliArgs.preset,
1417
1453
  coreSlotsResolved: 0,
1418
1454
  cwd: resolvedProjectRoot,
1419
- entrypoint: OPENCODE_ENTRYPOINT
1455
+ entrypoint: openCodeEntrypoint
1420
1456
  }
1421
1457
  });
1422
1458
  const resolvedPreset = resolvePreset(cliArgs.preset, projectConfig);
@@ -1431,7 +1467,7 @@ async function runPivvOpencode(cliArgs) {
1431
1467
  profileCount: resolvedPreset.profileCount,
1432
1468
  coreSlotsResolved: Object.keys(resolvedPreset.coreProfiles).length,
1433
1469
  cwd: resolvedProjectRoot,
1434
- entrypoint: OPENCODE_ENTRYPOINT
1470
+ entrypoint: openCodeEntrypoint
1435
1471
  }
1436
1472
  });
1437
1473
  lifecycleStage = "overlay.build";
@@ -1446,7 +1482,7 @@ async function runPivvOpencode(cliArgs) {
1446
1482
  aliasModelCount,
1447
1483
  providerIds,
1448
1484
  cwd: resolvedProjectRoot,
1449
- entrypoint: OPENCODE_ENTRYPOINT
1485
+ entrypoint: openCodeEntrypoint
1450
1486
  }
1451
1487
  });
1452
1488
  const runtimeOverlay = await buildRuntimeOverlay(resolvedPreset, projectConfig, baseOpenCodeInputs);
@@ -1465,7 +1501,7 @@ async function runPivvOpencode(cliArgs) {
1465
1501
  aliasModelCount,
1466
1502
  providerIds,
1467
1503
  cwd: resolvedProjectRoot,
1468
- entrypoint: OPENCODE_ENTRYPOINT
1504
+ entrypoint: openCodeEntrypoint
1469
1505
  }
1470
1506
  });
1471
1507
  lifecycleStage = "launch";
@@ -1483,7 +1519,7 @@ async function runPivvOpencode(cliArgs) {
1483
1519
  aliasModelCount,
1484
1520
  providerIds,
1485
1521
  cwd: resolvedProjectRoot,
1486
- entrypoint: OPENCODE_ENTRYPOINT
1522
+ entrypoint: openCodeEntrypoint
1487
1523
  }
1488
1524
  });
1489
1525
  const launchResult = await launchOpenCodeProcess({
@@ -1491,7 +1527,8 @@ async function runPivvOpencode(cliArgs) {
1491
1527
  projectRoot: resolvedProjectRoot,
1492
1528
  runtimeOverlay,
1493
1529
  runtimeEnvironment,
1494
- traceContext
1530
+ traceContext,
1531
+ openCodeShellCommand
1495
1532
  });
1496
1533
  if (launchResult.exitCode !== 0) {
1497
1534
  const cleanupResult = await cleanupRuntimeOverlay(runtimeOverlayPath);
@@ -1503,7 +1540,8 @@ async function runPivvOpencode(cliArgs) {
1503
1540
  exitCode: launchResult.exitCode,
1504
1541
  cleanupResult,
1505
1542
  aliasModelCount,
1506
- providerIds
1543
+ providerIds,
1544
+ entrypoint: openCodeEntrypoint
1507
1545
  });
1508
1546
  return failureResult;
1509
1547
  }
@@ -1520,7 +1558,7 @@ async function runPivvOpencode(cliArgs) {
1520
1558
  aliasModelCount,
1521
1559
  providerIds,
1522
1560
  cwd: resolvedProjectRoot,
1523
- entrypoint: OPENCODE_ENTRYPOINT
1561
+ entrypoint: openCodeEntrypoint
1524
1562
  }
1525
1563
  });
1526
1564
  const runFinishTrace = await persistLaunchTrace(traceRecorder, traceContext, {
@@ -1535,7 +1573,7 @@ async function runPivvOpencode(cliArgs) {
1535
1573
  projectRoot: resolvedProjectRoot,
1536
1574
  runtimeOverlayPath,
1537
1575
  cwd: resolvedProjectRoot,
1538
- entrypoint: OPENCODE_ENTRYPOINT
1576
+ entrypoint: openCodeEntrypoint
1539
1577
  }
1540
1578
  });
1541
1579
  console.info(`${LAUNCHER_LOG_PREFIX}[runPivvOpencode][${BLOCK_LOAD_ACTIVE_PRESET}] launch completed`, {
@@ -1565,7 +1603,7 @@ async function runPivvOpencode(cliArgs) {
1565
1603
  preset: cliArgs.preset,
1566
1604
  projectRoot: resolvedProjectRoot,
1567
1605
  cwd: resolvedProjectRoot,
1568
- entrypoint: OPENCODE_ENTRYPOINT
1606
+ entrypoint: openCodeEntrypoint
1569
1607
  }
1570
1608
  });
1571
1609
  }
@@ -1581,7 +1619,7 @@ async function runPivvOpencode(cliArgs) {
1581
1619
  projectRoot: resolvedProjectRoot,
1582
1620
  coreSlotsResolved: 0,
1583
1621
  cwd: resolvedProjectRoot,
1584
- entrypoint: OPENCODE_ENTRYPOINT
1622
+ entrypoint: openCodeEntrypoint
1585
1623
  }
1586
1624
  });
1587
1625
  }
@@ -1599,7 +1637,7 @@ async function runPivvOpencode(cliArgs) {
1599
1637
  aliasModelCount,
1600
1638
  providerIds,
1601
1639
  cwd: resolvedProjectRoot,
1602
- entrypoint: OPENCODE_ENTRYPOINT
1640
+ entrypoint: openCodeEntrypoint
1603
1641
  }
1604
1642
  });
1605
1643
  }
@@ -1612,7 +1650,8 @@ async function runPivvOpencode(cliArgs) {
1612
1650
  code: failureCode,
1613
1651
  cleanupResult,
1614
1652
  aliasModelCount,
1615
- providerIds
1653
+ providerIds,
1654
+ entrypoint: openCodeEntrypoint
1616
1655
  });
1617
1656
  }
1618
1657
  }
@@ -1672,7 +1711,7 @@ async function finalizeLaunchFailure(traceRecorder, traceContext, context) {
1672
1711
  aliasModelCount: context.aliasModelCount,
1673
1712
  providerIds: context.providerIds,
1674
1713
  cwd: context.projectRoot,
1675
- entrypoint: OPENCODE_ENTRYPOINT
1714
+ entrypoint: context.entrypoint
1676
1715
  }
1677
1716
  });
1678
1717
  const runFailTrace = await persistLaunchTrace(traceRecorder, traceContext, {
@@ -1689,7 +1728,7 @@ async function finalizeLaunchFailure(traceRecorder, traceContext, context) {
1689
1728
  code: context.code,
1690
1729
  exitCode: context.exitCode,
1691
1730
  cwd: context.projectRoot,
1692
- entrypoint: OPENCODE_ENTRYPOINT
1731
+ entrypoint: context.entrypoint
1693
1732
  }
1694
1733
  });
1695
1734
  console.info(`${LAUNCHER_LOG_PREFIX}[runPivvOpencode][${BLOCK_LOAD_ACTIVE_PRESET}] launch failed`, {
@@ -1711,7 +1750,7 @@ async function finalizeLaunchFailure(traceRecorder, traceContext, context) {
1711
1750
  }
1712
1751
  async function launchOpenCodeProcess(request) {
1713
1752
  return await new Promise((resolve, reject) => {
1714
- const launchedProcess = spawn("bun", ["x", "opencode-ai"], {
1753
+ const launchedProcess = spawn(request.openCodeShellCommand.executable, request.openCodeShellCommand.args, {
1715
1754
  cwd: request.projectRoot,
1716
1755
  env: request.runtimeEnvironment,
1717
1756
  stdio: "inherit"
@@ -1726,10 +1765,127 @@ async function launchOpenCodeProcess(request) {
1726
1765
  });
1727
1766
  });
1728
1767
  }
1768
+ function resolveOpenCodeShellCommand(baseEnvironment = process.env) {
1769
+ const configuredCommand = baseEnvironment[OPENCODE_SHELL_COMMAND_ENV]?.trim();
1770
+ const entrypoint = configuredCommand && configuredCommand.length > 0 ? configuredCommand : DEFAULT_OPENCODE_SHELL_COMMAND;
1771
+ const tokens = splitShellCommand(entrypoint);
1772
+ if (tokens.length === 0) {
1773
+ return {
1774
+ executable: DEFAULT_OPENCODE_SHELL_COMMAND,
1775
+ args: [],
1776
+ entrypoint: DEFAULT_OPENCODE_SHELL_COMMAND
1777
+ };
1778
+ }
1779
+ return {
1780
+ executable: tokens[0],
1781
+ args: tokens.slice(1),
1782
+ entrypoint
1783
+ };
1784
+ }
1785
+ function splitShellCommand(command) {
1786
+ const tokens = [];
1787
+ let current = "";
1788
+ let quote = null;
1789
+ for (let index = 0;index < command.length; index += 1) {
1790
+ const character = command[index];
1791
+ if (quote === null) {
1792
+ if (character === '"' || character === "'") {
1793
+ quote = character === '"' ? "double" : "single";
1794
+ continue;
1795
+ }
1796
+ if (/\s/.test(character)) {
1797
+ if (current.length > 0) {
1798
+ tokens.push(current);
1799
+ current = "";
1800
+ }
1801
+ continue;
1802
+ }
1803
+ } else if (quote === "double" && character === '"' || quote === "single" && character === "'") {
1804
+ quote = null;
1805
+ continue;
1806
+ }
1807
+ current += character;
1808
+ }
1809
+ if (current.length > 0) {
1810
+ tokens.push(current);
1811
+ }
1812
+ return tokens;
1813
+ }
1814
+ function normalizeCommandConfig(commands) {
1815
+ const normalizedCommands = {};
1816
+ for (const [commandName, commandConfig] of Object.entries(commands)) {
1817
+ const template = typeof commandConfig.prompt === "string" ? commandConfig.prompt : undefined;
1818
+ if (!template) {
1819
+ continue;
1820
+ }
1821
+ normalizedCommands[commandName] = {
1822
+ template,
1823
+ ...typeof commandConfig.description === "string" ? { description: commandConfig.description } : {},
1824
+ ...typeof commandConfig.agent === "string" ? { agent: commandConfig.agent } : {},
1825
+ ...typeof commandConfig.model === "string" ? { model: commandConfig.model } : {},
1826
+ ...typeof commandConfig.subtask === "boolean" ? { subtask: commandConfig.subtask } : {}
1827
+ };
1828
+ }
1829
+ return normalizedCommands;
1830
+ }
1831
+ function normalizeProviderConfig(providers) {
1832
+ const normalizedProviders = {};
1833
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
1834
+ const normalizedProvider = {};
1835
+ for (const key of ["api", "name", "env", "id", "npm", "whitelist", "blacklist", "options"]) {
1836
+ if (key in providerConfig) {
1837
+ normalizedProvider[key] = providerConfig[key];
1838
+ }
1839
+ }
1840
+ if (isRecord2(providerConfig.models)) {
1841
+ const normalizedModels = {};
1842
+ for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
1843
+ if (!isRecord2(modelConfig)) {
1844
+ continue;
1845
+ }
1846
+ const normalizedModel = {};
1847
+ for (const key of [
1848
+ "id",
1849
+ "name",
1850
+ "family",
1851
+ "release_date",
1852
+ "attachment",
1853
+ "reasoning",
1854
+ "temperature",
1855
+ "tool_call",
1856
+ "interleaved",
1857
+ "cost",
1858
+ "limit",
1859
+ "modalities",
1860
+ "experimental",
1861
+ "status",
1862
+ "options",
1863
+ "headers",
1864
+ "variants"
1865
+ ]) {
1866
+ if (key in modelConfig) {
1867
+ normalizedModel[key] = modelConfig[key];
1868
+ }
1869
+ }
1870
+ if (isRecord2(modelConfig.provider)) {
1871
+ normalizedModel.provider = modelConfig.provider;
1872
+ }
1873
+ normalizedModels[modelId] = normalizedModel;
1874
+ }
1875
+ normalizedProvider.models = normalizedModels;
1876
+ }
1877
+ normalizedProviders[providerId] = normalizedProvider;
1878
+ }
1879
+ return normalizedProviders;
1880
+ }
1881
+ function isRecord2(value) {
1882
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1883
+ }
1729
1884
 
1730
1885
  // src/cli/vvcode.ts
1731
1886
  var CLI_LOG_PREFIX = "[VvcodeCli]";
1732
1887
  var BLOCK_RUN_OPENCODE_COMMAND = "BLOCK_RUN_OPENCODE_COMMAND";
1888
+ var KNOWN_TOP_LEVEL_COMMANDS = new Set(["help", "init", "opencode", "preset"]);
1733
1889
  function resolveDefaultGlobalConfigPath() {
1734
1890
  return join3(homedir(), ".config", "vvcode", "config.jsonc");
1735
1891
  }
@@ -1738,7 +1894,7 @@ function createVvcodeCommand(dependencyOverrides = {}) {
1738
1894
  const opencodeArgs = {
1739
1895
  preset: {
1740
1896
  type: "string",
1741
- description: "Preset name defined in .vvcode/config.jsonc.",
1897
+ description: "Preset name defined in global or project vvcode config.",
1742
1898
  alias: ["p"],
1743
1899
  required: true
1744
1900
  },
@@ -1755,15 +1911,63 @@ function createVvcodeCommand(dependencyOverrides = {}) {
1755
1911
  default: dependencies.cwd()
1756
1912
  }
1757
1913
  };
1914
+ const configManagementArgs = {
1915
+ "global-config": {
1916
+ type: "string",
1917
+ description: "Path to the global vvcode config file.",
1918
+ alias: ["c"],
1919
+ default: dependencies.defaultGlobalConfigPath()
1920
+ },
1921
+ "project-root": {
1922
+ type: "string",
1923
+ description: "Project root where .vvcode/config.jsonc should be managed.",
1924
+ alias: ["r"],
1925
+ default: dependencies.cwd()
1926
+ }
1927
+ };
1758
1928
  return defineCommand({
1759
1929
  meta: {
1760
1930
  name: "vvcode",
1761
1931
  version: package_default.version,
1762
1932
  description: "Packaged CLI for launching OpenCode with vvcode runtime semantics."
1763
1933
  },
1764
- args: opencodeArgs,
1765
- default: "opencode",
1766
1934
  subCommands: {
1935
+ help: defineCommand({
1936
+ meta: {
1937
+ name: "help",
1938
+ description: "Show vvcode command help."
1939
+ },
1940
+ args: {
1941
+ command: {
1942
+ type: "positional",
1943
+ description: "Optional subcommand name to show help for.",
1944
+ required: false
1945
+ }
1946
+ },
1947
+ async run({ args }) {
1948
+ const helpArgs = typeof args.command === "string" && args.command.length > 0 ? [args.command, "--help"] : ["--help"];
1949
+ await runMain(createVvcodeCommand(dependencyOverrides), { rawArgs: helpArgs });
1950
+ }
1951
+ }),
1952
+ init: defineCommand({
1953
+ meta: {
1954
+ name: "init",
1955
+ description: "Create a project-local .vvcode/config.jsonc scaffold."
1956
+ },
1957
+ args: {
1958
+ "project-root": configManagementArgs["project-root"]
1959
+ },
1960
+ async run({ args }) {
1961
+ const configPath = resolveProjectConfigPath(String(args.projectRoot));
1962
+ const existingConfig = await readOptionalRawVvcodeConfig(configPath);
1963
+ if (existingConfig !== null) {
1964
+ dependencies.writeStdout(`vvcode: project config already exists at ${configPath}`);
1965
+ return;
1966
+ }
1967
+ await writeRawVvcodeConfig(configPath, createEmptyRawVvcodeConfig());
1968
+ dependencies.writeStdout(`vvcode: initialized project config at ${configPath}`);
1969
+ }
1970
+ }),
1767
1971
  opencode: defineCommand({
1768
1972
  meta: {
1769
1973
  name: "opencode",
@@ -1787,13 +1991,151 @@ function createVvcodeCommand(dependencyOverrides = {}) {
1787
1991
  }
1788
1992
  return launchResult;
1789
1993
  }
1994
+ }),
1995
+ preset: defineCommand({
1996
+ meta: {
1997
+ name: "preset",
1998
+ description: "Inspect or manage project presets copied from global vvcode config."
1999
+ },
2000
+ default: "list",
2001
+ subCommands: {
2002
+ list: defineCommand({
2003
+ meta: {
2004
+ name: "list",
2005
+ description: "List available presets from global and project config."
2006
+ },
2007
+ args: configManagementArgs,
2008
+ async run({ args }) {
2009
+ const snapshot = await loadProjectConfigSnapshot(String(args.globalConfig), String(args.projectRoot));
2010
+ const presetNames = collectPresetNames(snapshot);
2011
+ if (presetNames.length === 0) {
2012
+ dependencies.writeStdout("vvcode: no presets found in global or project config");
2013
+ return;
2014
+ }
2015
+ for (const presetName of presetNames) {
2016
+ dependencies.writeStdout(`${presetName} ${describePresetSource(snapshot, presetName)}`);
2017
+ }
2018
+ }
2019
+ }),
2020
+ add: defineCommand({
2021
+ meta: {
2022
+ name: "add",
2023
+ description: "Copy one preset and its referenced profiles from global config into the project config."
2024
+ },
2025
+ args: {
2026
+ name: {
2027
+ type: "positional",
2028
+ description: "Preset name to copy into the project config.",
2029
+ required: true
2030
+ },
2031
+ ...configManagementArgs
2032
+ },
2033
+ async run({ args }) {
2034
+ const presetName = String(args.name);
2035
+ const snapshot = await loadProjectConfigSnapshot(String(args.globalConfig), String(args.projectRoot));
2036
+ const globalPresets = toObjectBucket(snapshot.globalConfig?.presets);
2037
+ const globalProfiles = toObjectBucket(snapshot.globalConfig?.profiles);
2038
+ if (!(presetName in globalPresets) || !isRecord3(globalPresets[presetName])) {
2039
+ throw new Error(`Preset "${presetName}" was not found in global config ${snapshot.globalConfigPath}`);
2040
+ }
2041
+ const projectConfig = snapshot.projectConfig ?? createEmptyRawVvcodeConfig();
2042
+ const projectPresets = toMutableBucket(projectConfig, "presets");
2043
+ const projectProfiles = toMutableBucket(projectConfig, "profiles");
2044
+ if (presetName in projectPresets) {
2045
+ dependencies.writeStdout(`vvcode: preset "${presetName}" already exists in ${snapshot.projectConfigPath}`);
2046
+ return;
2047
+ }
2048
+ const globalPreset = globalPresets[presetName];
2049
+ const copiedProfiles = [];
2050
+ const normalizedPreset = {};
2051
+ for (const [slotName, profileNameValue] of Object.entries(globalPreset)) {
2052
+ if (typeof profileNameValue !== "string" || profileNameValue.length === 0) {
2053
+ throw new Error(`Global preset "${presetName}" slot "${slotName}" must reference a non-empty profile name`);
2054
+ }
2055
+ const profileName = profileNameValue;
2056
+ const globalProfile = globalProfiles[profileName];
2057
+ if (!isRecord3(globalProfile)) {
2058
+ throw new Error(`Global preset "${presetName}" references missing profile "${profileName}"`);
2059
+ }
2060
+ if (profileName in projectProfiles) {
2061
+ if (!areJsonValuesEqual(projectProfiles[profileName], globalProfile)) {
2062
+ throw new Error(`Project config already defines profile "${profileName}" with different settings; remove it or rename the project profile before adding preset "${presetName}"`);
2063
+ }
2064
+ } else {
2065
+ projectProfiles[profileName] = globalProfile;
2066
+ copiedProfiles.push(profileName);
2067
+ }
2068
+ normalizedPreset[slotName] = profileName;
2069
+ }
2070
+ projectPresets[presetName] = normalizedPreset;
2071
+ await writeRawVvcodeConfig(snapshot.projectConfigPath, projectConfig);
2072
+ dependencies.writeStdout(`vvcode: added preset "${presetName}" to ${snapshot.projectConfigPath} (copied ${copiedProfiles.length} profiles)`);
2073
+ }
2074
+ }),
2075
+ remove: defineCommand({
2076
+ meta: {
2077
+ name: "remove",
2078
+ description: "Remove one preset from the project config and prune unreferenced copied profiles."
2079
+ },
2080
+ args: {
2081
+ name: {
2082
+ type: "positional",
2083
+ description: "Preset name to remove from the project config.",
2084
+ required: true
2085
+ },
2086
+ "project-root": configManagementArgs["project-root"]
2087
+ },
2088
+ async run({ args }) {
2089
+ const presetName = String(args.name);
2090
+ const projectRoot = resolve(String(args.projectRoot));
2091
+ const projectConfigPath = resolveProjectConfigPath(projectRoot);
2092
+ const projectConfig = await readOptionalRawVvcodeConfig(projectConfigPath);
2093
+ if (projectConfig === null) {
2094
+ throw new Error(`Project config was not found at ${projectConfigPath}`);
2095
+ }
2096
+ const projectPresets = toMutableBucket(projectConfig, "presets");
2097
+ const projectProfiles = toMutableBucket(projectConfig, "profiles");
2098
+ const removedPreset = projectPresets[presetName];
2099
+ if (!isRecord3(removedPreset)) {
2100
+ throw new Error(`Preset "${presetName}" was not found in project config ${projectConfigPath}`);
2101
+ }
2102
+ delete projectPresets[presetName];
2103
+ const stillReferencedProfiles = new Set;
2104
+ for (const presetValue of Object.values(projectPresets)) {
2105
+ if (!isRecord3(presetValue)) {
2106
+ continue;
2107
+ }
2108
+ for (const profileNameValue of Object.values(presetValue)) {
2109
+ if (typeof profileNameValue === "string" && profileNameValue.length > 0) {
2110
+ stillReferencedProfiles.add(profileNameValue);
2111
+ }
2112
+ }
2113
+ }
2114
+ const removedProfiles = [];
2115
+ for (const profileNameValue of Object.values(removedPreset)) {
2116
+ if (typeof profileNameValue !== "string" || profileNameValue.length === 0) {
2117
+ continue;
2118
+ }
2119
+ if (stillReferencedProfiles.has(profileNameValue)) {
2120
+ continue;
2121
+ }
2122
+ if (profileNameValue in projectProfiles) {
2123
+ delete projectProfiles[profileNameValue];
2124
+ removedProfiles.push(profileNameValue);
2125
+ }
2126
+ }
2127
+ await writeRawVvcodeConfig(projectConfigPath, projectConfig);
2128
+ dependencies.writeStdout(`vvcode: removed preset "${presetName}" from ${projectConfigPath} (removed ${removedProfiles.length} profiles)`);
2129
+ }
2130
+ })
2131
+ }
1790
2132
  })
1791
2133
  }
1792
2134
  });
1793
2135
  }
1794
2136
  async function runVvcodeCli(rawArgs = process.argv.slice(2), dependencyOverrides = {}) {
1795
2137
  const vvcodeCommand = createVvcodeCommand(dependencyOverrides);
1796
- await runMain(vvcodeCommand, { rawArgs });
2138
+ await runMain(vvcodeCommand, { rawArgs: normalizeRawArgs(rawArgs) });
1797
2139
  }
1798
2140
  function resolveVvcodeCliDependencies(dependencyOverrides) {
1799
2141
  return {
@@ -1801,6 +2143,7 @@ function resolveVvcodeCliDependencies(dependencyOverrides) {
1801
2143
  cwd: dependencyOverrides.cwd ?? (() => process.cwd()),
1802
2144
  defaultGlobalConfigPath: dependencyOverrides.defaultGlobalConfigPath ?? resolveDefaultGlobalConfigPath,
1803
2145
  writeStderr: dependencyOverrides.writeStderr ?? ((message) => console.error(message)),
2146
+ writeStdout: dependencyOverrides.writeStdout ?? ((message) => console.log(message)),
1804
2147
  setExitCode: dependencyOverrides.setExitCode ?? ((code) => {
1805
2148
  process.exitCode = code;
1806
2149
  })
@@ -1820,6 +2163,72 @@ function formatLaunchFailure(launchResult) {
1820
2163
  }
1821
2164
  return failureFields.join(" ");
1822
2165
  }
2166
+ function normalizeRawArgs(rawArgs) {
2167
+ if (rawArgs.length === 0) {
2168
+ return ["help"];
2169
+ }
2170
+ if (KNOWN_TOP_LEVEL_COMMANDS.has(rawArgs[0])) {
2171
+ return rawArgs;
2172
+ }
2173
+ if (rawArgs.some((arg) => arg === "-p" || arg === "--preset" || arg.startsWith("--preset="))) {
2174
+ return ["opencode", ...rawArgs];
2175
+ }
2176
+ return rawArgs;
2177
+ }
2178
+ async function loadProjectConfigSnapshot(globalConfigPath, projectRoot) {
2179
+ const normalizedProjectRoot = resolve(projectRoot);
2180
+ const projectConfigPath = resolveProjectConfigPath(normalizedProjectRoot);
2181
+ const [globalConfig, projectConfig] = await Promise.all([
2182
+ readOptionalRawVvcodeConfig(globalConfigPath),
2183
+ readOptionalRawVvcodeConfig(projectConfigPath)
2184
+ ]);
2185
+ return {
2186
+ projectRoot: normalizedProjectRoot,
2187
+ projectConfigPath,
2188
+ globalConfigPath: resolve(globalConfigPath),
2189
+ globalConfig,
2190
+ projectConfig
2191
+ };
2192
+ }
2193
+ function collectPresetNames(snapshot) {
2194
+ const presetNames = new Set;
2195
+ for (const presetName of Object.keys(toObjectBucket(snapshot.globalConfig?.presets))) {
2196
+ presetNames.add(presetName);
2197
+ }
2198
+ for (const presetName of Object.keys(toObjectBucket(snapshot.projectConfig?.presets))) {
2199
+ presetNames.add(presetName);
2200
+ }
2201
+ return [...presetNames].sort((left, right) => left.localeCompare(right));
2202
+ }
2203
+ function describePresetSource(snapshot, presetName) {
2204
+ const hasGlobalPreset = presetName in toObjectBucket(snapshot.globalConfig?.presets);
2205
+ const hasProjectPreset = presetName in toObjectBucket(snapshot.projectConfig?.presets);
2206
+ if (hasProjectPreset && hasGlobalPreset) {
2207
+ return "project-overrides-global";
2208
+ }
2209
+ if (hasProjectPreset) {
2210
+ return "project";
2211
+ }
2212
+ return "global";
2213
+ }
2214
+ function toMutableBucket(config, key) {
2215
+ const bucket = config[key];
2216
+ if (isRecord3(bucket)) {
2217
+ return bucket;
2218
+ }
2219
+ const nextBucket = {};
2220
+ config[key] = nextBucket;
2221
+ return nextBucket;
2222
+ }
2223
+ function toObjectBucket(value) {
2224
+ return isRecord3(value) ? value : {};
2225
+ }
2226
+ function isRecord3(value) {
2227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2228
+ }
2229
+ function areJsonValuesEqual(left, right) {
2230
+ return JSON.stringify(left) === JSON.stringify(right);
2231
+ }
1823
2232
 
1824
2233
  // src/cli/vvcode-entry.ts
1825
2234
  await runVvcodeCli();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osovv/vvcode",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Packaged CLI for launching OpenCode with vvcode runtime semantics.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/osovv/vvcode",
@@ -18,7 +18,7 @@
18
18
  "dist"
19
19
  ],
20
20
  "bin": {
21
- "vvcode": "./bin/vvcode"
21
+ "vvcode": "bin/vvcode"
22
22
  },
23
23
  "publishConfig": {
24
24
  "access": "public"