@osovv/vvcode 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +78 -13
  2. package/dist/vvcode.js +295 -13
  3. package/package.json +1 -1
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,89 @@ 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>
36
93
  ```
37
94
 
38
95
  Options:
39
96
 
40
- - `--preset`, `-p` (required)
41
- - `--global-config`, `-c` (optional)
42
- - `--project-root`, `-r` (optional)
97
+ 1. `--preset`, `-p` for the preset name.
98
+ 2. `--global-config`, `-c` to override the global config file path.
99
+ 3. `--project-root`, `-r` to launch from a different project root.
43
100
 
44
- You can also omit `opencode` because it is the default subcommand:
101
+ Manage presets:
45
102
 
46
103
  ```bash
47
- vvcode --preset <name>
104
+ vvcode preset list
105
+ vvcode preset add <preset-name>
106
+ vvcode preset remove <preset-name>
48
107
  ```
49
108
 
109
+ `preset list` prints one preset per line with its source label:
110
+
111
+ 1. `global`
112
+ 2. `project`
113
+ 3. `project-overrides-global`
114
+
50
115
  ## Runtime artifacts
51
116
 
52
117
  - `.vvcode/local/runtime/overlays/`
@@ -60,9 +125,9 @@ vvcode --preset <name>
60
125
  ## Packaging notes
61
126
 
62
127
  - Package name: `@osovv/vvcode`
63
- - CLI bin: `bin/vvcode`
128
+ - CLI bin: `vvcode`
64
129
  - Published runtime entry: `dist/vvcode.js`
65
130
  - Build command: `bun run build`
66
131
  - Local dev command: `bun run cli -- --help`
67
- - `prepack` builds `dist` automatically before publish/pack
132
+ - `prepack` builds `dist` automatically before publish or pack
68
133
  - 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.0",
551
551
  description: "Packaged CLI for launching OpenCode with vvcode runtime semantics.",
552
552
  license: "MIT",
553
553
  homepage: "https://github.com/osovv/vvcode",
@@ -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";
@@ -1730,6 +1758,7 @@ async function launchOpenCodeProcess(request) {
1730
1758
  // src/cli/vvcode.ts
1731
1759
  var CLI_LOG_PREFIX = "[VvcodeCli]";
1732
1760
  var BLOCK_RUN_OPENCODE_COMMAND = "BLOCK_RUN_OPENCODE_COMMAND";
1761
+ var KNOWN_TOP_LEVEL_COMMANDS = new Set(["help", "init", "opencode", "preset"]);
1733
1762
  function resolveDefaultGlobalConfigPath() {
1734
1763
  return join3(homedir(), ".config", "vvcode", "config.jsonc");
1735
1764
  }
@@ -1738,7 +1767,7 @@ function createVvcodeCommand(dependencyOverrides = {}) {
1738
1767
  const opencodeArgs = {
1739
1768
  preset: {
1740
1769
  type: "string",
1741
- description: "Preset name defined in .vvcode/config.jsonc.",
1770
+ description: "Preset name defined in global or project vvcode config.",
1742
1771
  alias: ["p"],
1743
1772
  required: true
1744
1773
  },
@@ -1755,15 +1784,63 @@ function createVvcodeCommand(dependencyOverrides = {}) {
1755
1784
  default: dependencies.cwd()
1756
1785
  }
1757
1786
  };
1787
+ const configManagementArgs = {
1788
+ "global-config": {
1789
+ type: "string",
1790
+ description: "Path to the global vvcode config file.",
1791
+ alias: ["c"],
1792
+ default: dependencies.defaultGlobalConfigPath()
1793
+ },
1794
+ "project-root": {
1795
+ type: "string",
1796
+ description: "Project root where .vvcode/config.jsonc should be managed.",
1797
+ alias: ["r"],
1798
+ default: dependencies.cwd()
1799
+ }
1800
+ };
1758
1801
  return defineCommand({
1759
1802
  meta: {
1760
1803
  name: "vvcode",
1761
1804
  version: package_default.version,
1762
1805
  description: "Packaged CLI for launching OpenCode with vvcode runtime semantics."
1763
1806
  },
1764
- args: opencodeArgs,
1765
- default: "opencode",
1766
1807
  subCommands: {
1808
+ help: defineCommand({
1809
+ meta: {
1810
+ name: "help",
1811
+ description: "Show vvcode command help."
1812
+ },
1813
+ args: {
1814
+ command: {
1815
+ type: "positional",
1816
+ description: "Optional subcommand name to show help for.",
1817
+ required: false
1818
+ }
1819
+ },
1820
+ async run({ args }) {
1821
+ const helpArgs = typeof args.command === "string" && args.command.length > 0 ? [args.command, "--help"] : ["--help"];
1822
+ await runMain(createVvcodeCommand(dependencyOverrides), { rawArgs: helpArgs });
1823
+ }
1824
+ }),
1825
+ init: defineCommand({
1826
+ meta: {
1827
+ name: "init",
1828
+ description: "Create a project-local .vvcode/config.jsonc scaffold."
1829
+ },
1830
+ args: {
1831
+ "project-root": configManagementArgs["project-root"]
1832
+ },
1833
+ async run({ args }) {
1834
+ const configPath = resolveProjectConfigPath(String(args.projectRoot));
1835
+ const existingConfig = await readOptionalRawVvcodeConfig(configPath);
1836
+ if (existingConfig !== null) {
1837
+ dependencies.writeStdout(`vvcode: project config already exists at ${configPath}`);
1838
+ return;
1839
+ }
1840
+ await writeRawVvcodeConfig(configPath, createEmptyRawVvcodeConfig());
1841
+ dependencies.writeStdout(`vvcode: initialized project config at ${configPath}`);
1842
+ }
1843
+ }),
1767
1844
  opencode: defineCommand({
1768
1845
  meta: {
1769
1846
  name: "opencode",
@@ -1787,13 +1864,151 @@ function createVvcodeCommand(dependencyOverrides = {}) {
1787
1864
  }
1788
1865
  return launchResult;
1789
1866
  }
1867
+ }),
1868
+ preset: defineCommand({
1869
+ meta: {
1870
+ name: "preset",
1871
+ description: "Inspect or manage project presets copied from global vvcode config."
1872
+ },
1873
+ default: "list",
1874
+ subCommands: {
1875
+ list: defineCommand({
1876
+ meta: {
1877
+ name: "list",
1878
+ description: "List available presets from global and project config."
1879
+ },
1880
+ args: configManagementArgs,
1881
+ async run({ args }) {
1882
+ const snapshot = await loadProjectConfigSnapshot(String(args.globalConfig), String(args.projectRoot));
1883
+ const presetNames = collectPresetNames(snapshot);
1884
+ if (presetNames.length === 0) {
1885
+ dependencies.writeStdout("vvcode: no presets found in global or project config");
1886
+ return;
1887
+ }
1888
+ for (const presetName of presetNames) {
1889
+ dependencies.writeStdout(`${presetName} ${describePresetSource(snapshot, presetName)}`);
1890
+ }
1891
+ }
1892
+ }),
1893
+ add: defineCommand({
1894
+ meta: {
1895
+ name: "add",
1896
+ description: "Copy one preset and its referenced profiles from global config into the project config."
1897
+ },
1898
+ args: {
1899
+ name: {
1900
+ type: "positional",
1901
+ description: "Preset name to copy into the project config.",
1902
+ required: true
1903
+ },
1904
+ ...configManagementArgs
1905
+ },
1906
+ async run({ args }) {
1907
+ const presetName = String(args.name);
1908
+ const snapshot = await loadProjectConfigSnapshot(String(args.globalConfig), String(args.projectRoot));
1909
+ const globalPresets = toObjectBucket(snapshot.globalConfig?.presets);
1910
+ const globalProfiles = toObjectBucket(snapshot.globalConfig?.profiles);
1911
+ if (!(presetName in globalPresets) || !isRecord2(globalPresets[presetName])) {
1912
+ throw new Error(`Preset "${presetName}" was not found in global config ${snapshot.globalConfigPath}`);
1913
+ }
1914
+ const projectConfig = snapshot.projectConfig ?? createEmptyRawVvcodeConfig();
1915
+ const projectPresets = toMutableBucket(projectConfig, "presets");
1916
+ const projectProfiles = toMutableBucket(projectConfig, "profiles");
1917
+ if (presetName in projectPresets) {
1918
+ dependencies.writeStdout(`vvcode: preset "${presetName}" already exists in ${snapshot.projectConfigPath}`);
1919
+ return;
1920
+ }
1921
+ const globalPreset = globalPresets[presetName];
1922
+ const copiedProfiles = [];
1923
+ const normalizedPreset = {};
1924
+ for (const [slotName, profileNameValue] of Object.entries(globalPreset)) {
1925
+ if (typeof profileNameValue !== "string" || profileNameValue.length === 0) {
1926
+ throw new Error(`Global preset "${presetName}" slot "${slotName}" must reference a non-empty profile name`);
1927
+ }
1928
+ const profileName = profileNameValue;
1929
+ const globalProfile = globalProfiles[profileName];
1930
+ if (!isRecord2(globalProfile)) {
1931
+ throw new Error(`Global preset "${presetName}" references missing profile "${profileName}"`);
1932
+ }
1933
+ if (profileName in projectProfiles) {
1934
+ if (!areJsonValuesEqual(projectProfiles[profileName], globalProfile)) {
1935
+ throw new Error(`Project config already defines profile "${profileName}" with different settings; remove it or rename the project profile before adding preset "${presetName}"`);
1936
+ }
1937
+ } else {
1938
+ projectProfiles[profileName] = globalProfile;
1939
+ copiedProfiles.push(profileName);
1940
+ }
1941
+ normalizedPreset[slotName] = profileName;
1942
+ }
1943
+ projectPresets[presetName] = normalizedPreset;
1944
+ await writeRawVvcodeConfig(snapshot.projectConfigPath, projectConfig);
1945
+ dependencies.writeStdout(`vvcode: added preset "${presetName}" to ${snapshot.projectConfigPath} (copied ${copiedProfiles.length} profiles)`);
1946
+ }
1947
+ }),
1948
+ remove: defineCommand({
1949
+ meta: {
1950
+ name: "remove",
1951
+ description: "Remove one preset from the project config and prune unreferenced copied profiles."
1952
+ },
1953
+ args: {
1954
+ name: {
1955
+ type: "positional",
1956
+ description: "Preset name to remove from the project config.",
1957
+ required: true
1958
+ },
1959
+ "project-root": configManagementArgs["project-root"]
1960
+ },
1961
+ async run({ args }) {
1962
+ const presetName = String(args.name);
1963
+ const projectRoot = resolve(String(args.projectRoot));
1964
+ const projectConfigPath = resolveProjectConfigPath(projectRoot);
1965
+ const projectConfig = await readOptionalRawVvcodeConfig(projectConfigPath);
1966
+ if (projectConfig === null) {
1967
+ throw new Error(`Project config was not found at ${projectConfigPath}`);
1968
+ }
1969
+ const projectPresets = toMutableBucket(projectConfig, "presets");
1970
+ const projectProfiles = toMutableBucket(projectConfig, "profiles");
1971
+ const removedPreset = projectPresets[presetName];
1972
+ if (!isRecord2(removedPreset)) {
1973
+ throw new Error(`Preset "${presetName}" was not found in project config ${projectConfigPath}`);
1974
+ }
1975
+ delete projectPresets[presetName];
1976
+ const stillReferencedProfiles = new Set;
1977
+ for (const presetValue of Object.values(projectPresets)) {
1978
+ if (!isRecord2(presetValue)) {
1979
+ continue;
1980
+ }
1981
+ for (const profileNameValue of Object.values(presetValue)) {
1982
+ if (typeof profileNameValue === "string" && profileNameValue.length > 0) {
1983
+ stillReferencedProfiles.add(profileNameValue);
1984
+ }
1985
+ }
1986
+ }
1987
+ const removedProfiles = [];
1988
+ for (const profileNameValue of Object.values(removedPreset)) {
1989
+ if (typeof profileNameValue !== "string" || profileNameValue.length === 0) {
1990
+ continue;
1991
+ }
1992
+ if (stillReferencedProfiles.has(profileNameValue)) {
1993
+ continue;
1994
+ }
1995
+ if (profileNameValue in projectProfiles) {
1996
+ delete projectProfiles[profileNameValue];
1997
+ removedProfiles.push(profileNameValue);
1998
+ }
1999
+ }
2000
+ await writeRawVvcodeConfig(projectConfigPath, projectConfig);
2001
+ dependencies.writeStdout(`vvcode: removed preset "${presetName}" from ${projectConfigPath} (removed ${removedProfiles.length} profiles)`);
2002
+ }
2003
+ })
2004
+ }
1790
2005
  })
1791
2006
  }
1792
2007
  });
1793
2008
  }
1794
2009
  async function runVvcodeCli(rawArgs = process.argv.slice(2), dependencyOverrides = {}) {
1795
2010
  const vvcodeCommand = createVvcodeCommand(dependencyOverrides);
1796
- await runMain(vvcodeCommand, { rawArgs });
2011
+ await runMain(vvcodeCommand, { rawArgs: normalizeRawArgs(rawArgs) });
1797
2012
  }
1798
2013
  function resolveVvcodeCliDependencies(dependencyOverrides) {
1799
2014
  return {
@@ -1801,6 +2016,7 @@ function resolveVvcodeCliDependencies(dependencyOverrides) {
1801
2016
  cwd: dependencyOverrides.cwd ?? (() => process.cwd()),
1802
2017
  defaultGlobalConfigPath: dependencyOverrides.defaultGlobalConfigPath ?? resolveDefaultGlobalConfigPath,
1803
2018
  writeStderr: dependencyOverrides.writeStderr ?? ((message) => console.error(message)),
2019
+ writeStdout: dependencyOverrides.writeStdout ?? ((message) => console.log(message)),
1804
2020
  setExitCode: dependencyOverrides.setExitCode ?? ((code) => {
1805
2021
  process.exitCode = code;
1806
2022
  })
@@ -1820,6 +2036,72 @@ function formatLaunchFailure(launchResult) {
1820
2036
  }
1821
2037
  return failureFields.join(" ");
1822
2038
  }
2039
+ function normalizeRawArgs(rawArgs) {
2040
+ if (rawArgs.length === 0) {
2041
+ return ["help"];
2042
+ }
2043
+ if (KNOWN_TOP_LEVEL_COMMANDS.has(rawArgs[0])) {
2044
+ return rawArgs;
2045
+ }
2046
+ if (rawArgs.some((arg) => arg === "-p" || arg === "--preset" || arg.startsWith("--preset="))) {
2047
+ return ["opencode", ...rawArgs];
2048
+ }
2049
+ return rawArgs;
2050
+ }
2051
+ async function loadProjectConfigSnapshot(globalConfigPath, projectRoot) {
2052
+ const normalizedProjectRoot = resolve(projectRoot);
2053
+ const projectConfigPath = resolveProjectConfigPath(normalizedProjectRoot);
2054
+ const [globalConfig, projectConfig] = await Promise.all([
2055
+ readOptionalRawVvcodeConfig(globalConfigPath),
2056
+ readOptionalRawVvcodeConfig(projectConfigPath)
2057
+ ]);
2058
+ return {
2059
+ projectRoot: normalizedProjectRoot,
2060
+ projectConfigPath,
2061
+ globalConfigPath: resolve(globalConfigPath),
2062
+ globalConfig,
2063
+ projectConfig
2064
+ };
2065
+ }
2066
+ function collectPresetNames(snapshot) {
2067
+ const presetNames = new Set;
2068
+ for (const presetName of Object.keys(toObjectBucket(snapshot.globalConfig?.presets))) {
2069
+ presetNames.add(presetName);
2070
+ }
2071
+ for (const presetName of Object.keys(toObjectBucket(snapshot.projectConfig?.presets))) {
2072
+ presetNames.add(presetName);
2073
+ }
2074
+ return [...presetNames].sort((left, right) => left.localeCompare(right));
2075
+ }
2076
+ function describePresetSource(snapshot, presetName) {
2077
+ const hasGlobalPreset = presetName in toObjectBucket(snapshot.globalConfig?.presets);
2078
+ const hasProjectPreset = presetName in toObjectBucket(snapshot.projectConfig?.presets);
2079
+ if (hasProjectPreset && hasGlobalPreset) {
2080
+ return "project-overrides-global";
2081
+ }
2082
+ if (hasProjectPreset) {
2083
+ return "project";
2084
+ }
2085
+ return "global";
2086
+ }
2087
+ function toMutableBucket(config, key) {
2088
+ const bucket = config[key];
2089
+ if (isRecord2(bucket)) {
2090
+ return bucket;
2091
+ }
2092
+ const nextBucket = {};
2093
+ config[key] = nextBucket;
2094
+ return nextBucket;
2095
+ }
2096
+ function toObjectBucket(value) {
2097
+ return isRecord2(value) ? value : {};
2098
+ }
2099
+ function isRecord2(value) {
2100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2101
+ }
2102
+ function areJsonValuesEqual(left, right) {
2103
+ return JSON.stringify(left) === JSON.stringify(right);
2104
+ }
1823
2105
 
1824
2106
  // src/cli/vvcode-entry.ts
1825
2107
  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.0",
4
4
  "description": "Packaged CLI for launching OpenCode with vvcode runtime semantics.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/osovv/vvcode",