fix(qmd): preserve user models block, adopt-and-remove ledger package, guard symlinks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59796d99d7
commit
7f89ae70bd
6 files changed
+155
-20
No files matched your search
@@ -1,4 +1,3 @@
|
|||||||
# scripts/lib/qmd.ps1
|
|
||||||
function Sync-Qmd {
|
function Sync-Qmd {
|
||||||
param([Parameter(Mandatory=$true)][string]$RepositoryRoot,[Parameter(Mandatory=$true)][string]$HomePath,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,[Parameter(Mandatory=$true)][bool]$Enabled,[switch]$DryRun,[switch]$Update,[switch]$Summary)
|
param([Parameter(Mandatory=$true)][string]$RepositoryRoot,[Parameter(Mandatory=$true)][string]$HomePath,[ValidateSet('Codex','Claude','Both')][Parameter(Mandatory=$true)][string]$Client,[Parameter(Mandatory=$true)][bool]$Enabled,[switch]$DryRun,[switch]$Update,[switch]$Summary)
|
||||||
$arguments = @((Join-Path $PSScriptRoot 'qmd.py'), 'sync', '--root', $RepositoryRoot, '--home', $HomePath, '--client', $Client.ToLowerInvariant(), '--enabled', $Enabled.ToString().ToLowerInvariant())
|
$arguments = @((Join-Path $PSScriptRoot 'qmd.py'), 'sync', '--root', $RepositoryRoot, '--home', $HomePath, '--client', $Client.ToLowerInvariant(), '--enabled', $Enabled.ToString().ToLowerInvariant())
|
||||||
|
|||||||
+35
-10
@@ -17,10 +17,10 @@ DEVICE_TIMEOUT = 600
|
|||||||
WINDOWS = os.name == 'nt'
|
WINDOWS = os.name == 'nt'
|
||||||
|
|
||||||
|
|
||||||
def run(arguments, env=None, timeout=1800):
|
def run(arguments, env=None, timeout=1800, check=True):
|
||||||
result = subprocess.run(arguments, env=env, capture_output=True, text=True, encoding='utf-8', errors='replace',
|
result = subprocess.run(arguments, env=env, capture_output=True, text=True, encoding='utf-8', errors='replace',
|
||||||
timeout=timeout, shell=WINDOWS and arguments[0] in ('npm', 'qmd'))
|
timeout=timeout, shell=WINDOWS and arguments[0] in ('npm', 'qmd'))
|
||||||
if result.returncode != 0:
|
if check and result.returncode != 0:
|
||||||
raise ValueError(f'{" ".join(arguments[:3])} failed: {(result.stderr or result.stdout).strip()[-300:]}')
|
raise ValueError(f'{" ".join(arguments[:3])} failed: {(result.stderr or result.stdout).strip()[-300:]}')
|
||||||
return result.stdout
|
return result.stdout
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class Setup:
|
|||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
if not self.state_path.exists():
|
if not self.state_path.exists():
|
||||||
return {'version': 1, 'clients': [], 'device': None, 'owned_package': False, 'owned_models': [], 'models_block': False, 'files': {}}
|
return {'version': 1, 'clients': [], 'device': None, 'owned_package': False, 'owned_models': [], 'models_block': False, 'previous_models': None, 'files': {}}
|
||||||
state = json.loads(self.state_path.read_text(encoding='utf-8'))
|
state = json.loads(self.state_path.read_text(encoding='utf-8'))
|
||||||
if state.get('version') != 1 or state.get('device') not in (None, 'gpu', 'cpu'):
|
if state.get('version') != 1 or state.get('device') not in (None, 'gpu', 'cpu'):
|
||||||
raise ValueError('Invalid QMD setup state.')
|
raise ValueError('Invalid QMD setup state.')
|
||||||
@@ -73,7 +73,12 @@ class Setup:
|
|||||||
return self.run(['node', str(self.scripts / script), *arguments], env=env, timeout=timeout)
|
return self.run(['node', str(self.scripts / script), *arguments], env=env, timeout=timeout)
|
||||||
|
|
||||||
def package_installed(self):
|
def package_installed(self):
|
||||||
listing = json.loads(self.run(['npm', 'list', '--global', '--depth=0', '--json']) or '{}')
|
# npm list exits non-zero on tree problems (ELSPROBLEMS) but still prints valid JSON.
|
||||||
|
output = self.run(['npm', 'list', '--global', '--depth=0', '--json'], check=False) or '{}'
|
||||||
|
try:
|
||||||
|
listing = json.loads(output)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValueError('npm list did not return JSON.')
|
||||||
return PACKAGE in (listing.get('dependencies') or {})
|
return PACKAGE in (listing.get('dependencies') or {})
|
||||||
|
|
||||||
def require_node(self):
|
def require_node(self):
|
||||||
@@ -87,7 +92,8 @@ class Setup:
|
|||||||
if legacy_state.exists() or legacy.exists():
|
if legacy_state.exists() or legacy.exists():
|
||||||
if legacy.is_symlink():
|
if legacy.is_symlink():
|
||||||
raise ValueError('Legacy semantic retrieval directory must not be a symbolic link.')
|
raise ValueError('Legacy semantic retrieval directory must not be a symbolic link.')
|
||||||
shutil.rmtree(legacy, ignore_errors=True)
|
if legacy.exists():
|
||||||
|
shutil.rmtree(legacy)
|
||||||
legacy_state.unlink(missing_ok=True)
|
legacy_state.unlink(missing_ok=True)
|
||||||
self.say('Removed the previous Python semantic retrieval installation.')
|
self.say('Removed the previous Python semantic retrieval installation.')
|
||||||
ledger_path = self.base / 'extensions.json'
|
ledger_path = self.base / 'extensions.json'
|
||||||
@@ -95,14 +101,19 @@ class Setup:
|
|||||||
ledger = json.loads(ledger_path.read_text(encoding='utf-8'))
|
ledger = json.loads(ledger_path.read_text(encoding='utf-8'))
|
||||||
owned = [r for r in ledger.get('resources', []) if r.get('kind') == 'qmd']
|
owned = [r for r in ledger.get('resources', []) if r.get('kind') == 'qmd']
|
||||||
if owned:
|
if owned:
|
||||||
|
state['owned_package'] = True
|
||||||
|
atomic_json(self.state_path, state) # record ownership before the ledger forgets it
|
||||||
ledger['resources'] = [r for r in ledger['resources'] if r.get('kind') != 'qmd']
|
ledger['resources'] = [r for r in ledger['resources'] if r.get('kind') != 'qmd']
|
||||||
atomic_json(ledger_path, ledger)
|
atomic_json(ledger_path, ledger)
|
||||||
state['owned_package'] = True
|
|
||||||
|
|
||||||
def install_scripts(self, state):
|
def install_scripts(self, state):
|
||||||
|
if self.scripts.is_symlink():
|
||||||
|
raise ValueError('QMD script directory must not be a symbolic link.')
|
||||||
self.scripts.mkdir(parents=True, exist_ok=True)
|
self.scripts.mkdir(parents=True, exist_ok=True)
|
||||||
for name in SCRIPTS:
|
for name in SCRIPTS:
|
||||||
target = self.scripts / name
|
target = self.scripts / name
|
||||||
|
if target.is_symlink():
|
||||||
|
raise ValueError(f'QMD script must not be a symbolic link: {name}')
|
||||||
expected = state['files'].get(name)
|
expected = state['files'].get(name)
|
||||||
if target.exists() and expected and digest(target) != expected:
|
if target.exists() and expected and digest(target) != expected:
|
||||||
raise ValueError(f'Setup-owned QMD script changed: {name}')
|
raise ValueError(f'Setup-owned QMD script changed: {name}')
|
||||||
@@ -119,7 +130,12 @@ class Setup:
|
|||||||
self.install_scripts(state)
|
self.install_scripts(state)
|
||||||
atomic_json(self.state_path, state)
|
atomic_json(self.state_path, state)
|
||||||
if not state['models_block']:
|
if not state['models_block']:
|
||||||
self.node('qmd-config.mjs', 'set-models')
|
output = self.node('qmd-config.mjs', 'set-models')
|
||||||
|
try:
|
||||||
|
previous = json.loads(output.strip().splitlines()[-1])['previous']
|
||||||
|
except (IndexError, KeyError, TypeError, json.JSONDecodeError):
|
||||||
|
raise ValueError('qmd-config set-models returned no previous models.')
|
||||||
|
state['previous_models'] = previous if isinstance(previous, dict) else None
|
||||||
state['models_block'] = True
|
state['models_block'] = True
|
||||||
atomic_json(self.state_path, state)
|
atomic_json(self.state_path, state)
|
||||||
before = {p.name for p in self.models.iterdir()} if self.models.is_dir() else set()
|
before = {p.name for p in self.models.iterdir()} if self.models.is_dir() else set()
|
||||||
@@ -159,10 +175,14 @@ class Setup:
|
|||||||
if re.fullmatch(r'repo-[0-9a-f]{12}', name):
|
if re.fullmatch(r'repo-[0-9a-f]{12}', name):
|
||||||
try:
|
try:
|
||||||
self.run(['qmd', 'collection', 'remove', name])
|
self.run(['qmd', 'collection', 'remove', name])
|
||||||
except ValueError:
|
except (ValueError, subprocess.SubprocessError):
|
||||||
pass
|
pass
|
||||||
if state['models_block'] and self.scripts.joinpath('qmd-config.mjs').exists():
|
if state['models_block'] and self.scripts.joinpath('qmd-config.mjs').exists():
|
||||||
self.node('qmd-config.mjs', 'unset-models')
|
previous = state.get('previous_models')
|
||||||
|
if previous is not None:
|
||||||
|
self.node('qmd-config.mjs', 'restore-models', json.dumps(previous))
|
||||||
|
else:
|
||||||
|
self.node('qmd-config.mjs', 'unset-models')
|
||||||
for name in state['owned_models']:
|
for name in state['owned_models']:
|
||||||
path = self.models / name
|
path = self.models / name
|
||||||
if name == Path(name).name and path.is_file() and not path.is_symlink():
|
if name == Path(name).name and path.is_file() and not path.is_symlink():
|
||||||
@@ -178,6 +198,9 @@ class Setup:
|
|||||||
if not self.state_path.exists():
|
if not self.state_path.exists():
|
||||||
if not self.dry_run:
|
if not self.dry_run:
|
||||||
self.migrate(state)
|
self.migrate(state)
|
||||||
|
if state['owned_package']:
|
||||||
|
self.run(['npm', 'uninstall', '--global', PACKAGE])
|
||||||
|
self.state_path.unlink(missing_ok=True)
|
||||||
return
|
return
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
self.say('DRYRUN disable QMD local search')
|
self.say('DRYRUN disable QMD local search')
|
||||||
@@ -203,7 +226,9 @@ class Setup:
|
|||||||
env['QMD_LLAMA_GPU' if device == 'gpu' else 'QMD_FORCE_CPU'] = 'auto' if device == 'gpu' else '1'
|
env['QMD_LLAMA_GPU' if device == 'gpu' else 'QMD_FORCE_CPU'] = 'auto' if device == 'gpu' else '1'
|
||||||
try:
|
try:
|
||||||
result = json.loads(self.node('qmd-benchmark.mjs', '--device', device, env=env, timeout=DEVICE_TIMEOUT))
|
result = json.loads(self.node('qmd-benchmark.mjs', '--device', device, env=env, timeout=DEVICE_TIMEOUT))
|
||||||
except (ValueError, subprocess.TimeoutExpired) as error:
|
if not isinstance(result, dict):
|
||||||
|
raise ValueError('benchmark returned a non-object result')
|
||||||
|
except (ValueError, subprocess.SubprocessError) as error:
|
||||||
result = {'device': device, 'passed': False, 'error': str(error)[:200]}
|
result = {'device': device, 'passed': False, 'error': str(error)[:200]}
|
||||||
results.append(result)
|
results.append(result)
|
||||||
print(f'QMD benchmark {device.upper()}: ' + ('passed' if result.get('passed') else 'failed')
|
print(f'QMD benchmark {device.upper()}: ' + ('passed' if result.get('passed') else 'failed')
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# scripts/lib/qmd.sh
|
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
sync_qmd() {
|
sync_qmd() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import * as YAML from './fixtures/yaml-shim.mjs';
|
import * as YAML from './fixtures/yaml-shim.mjs';
|
||||||
import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, writePrivateJson, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs';
|
import { collectionName, deviceEnv, idleMinutes, qmdConfigPath, writePrivateJson, readJson, runQmdCli, MODELS } from '../../shared/qmd/qmd-lib.mjs';
|
||||||
import { setModels, unsetModels } from '../../shared/qmd/qmd-config.mjs';
|
import { setModels, unsetModels, previousModels, restoreModels } from '../../shared/qmd/qmd-config.mjs';
|
||||||
|
|
||||||
test('collection name is stable, prefixed, and path-safe', () => {
|
test('collection name is stable, prefixed, and path-safe', () => {
|
||||||
const a = collectionName('/tmp/My Repo/ü');
|
const a = collectionName('/tmp/My Repo/ü');
|
||||||
@@ -75,3 +75,19 @@ test('runQmdCli runs the CLI entry of QMD_PACKAGE_DIR without a shell', () => {
|
|||||||
if (previous === undefined) delete process.env.QMD_PACKAGE_DIR; else process.env.QMD_PACKAGE_DIR = previous;
|
if (previous === undefined) delete process.env.QMD_PACKAGE_DIR; else process.env.QMD_PACKAGE_DIR = previous;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('previousModels captures a user models block, null when absent', () => {
|
||||||
|
assert.deepEqual(previousModels('models:\n embed: mine\ncollections: {}\n', YAML), { embed: 'mine' });
|
||||||
|
assert.equal(previousModels('collections: {}\n', YAML), null);
|
||||||
|
assert.equal(previousModels('', YAML), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restoreModels writes the previous block back and keeps comments and other keys', () => {
|
||||||
|
const original = '# keep\ncollections:\n a: {}\nmodels:\n embed: mine\n';
|
||||||
|
const replaced = setModels(original, YAML);
|
||||||
|
const restored = restoreModels(replaced, YAML, { embed: 'mine' });
|
||||||
|
assert.match(restored, /# keep/);
|
||||||
|
assert.match(restored, /embed: mine/);
|
||||||
|
assert.match(restored, /collections:/);
|
||||||
|
assert.doesNotMatch(restored, /Qwen/);
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# scripts/tests/test-qmd-setup.py
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
spec = importlib.util.spec_from_file_location('qmd_setup', ROOT / 'scripts/lib/qmd.py')
|
spec = importlib.util.spec_from_file_location('qmd_setup', ROOT / 'scripts/lib/qmd.py')
|
||||||
@@ -13,15 +13,20 @@ spec.loader.exec_module(qmd)
|
|||||||
|
|
||||||
|
|
||||||
class FakeRunner:
|
class FakeRunner:
|
||||||
def __init__(self, installed=False, gpu='cuda', gpu_result=None, cpu_result=None):
|
def __init__(self, installed=False, gpu='cuda', gpu_result=None, cpu_result=None, previous=None, fail_pull=False):
|
||||||
self.calls, self.installed, self.gpu = [], installed, gpu
|
self.calls, self.installed, self.gpu = [], installed, gpu
|
||||||
|
self.previous, self.fail_pull = previous, fail_pull
|
||||||
self.results = {'gpu': gpu_result, 'cpu': cpu_result}
|
self.results = {'gpu': gpu_result, 'cpu': cpu_result}
|
||||||
|
|
||||||
def __call__(self, arguments, env=None, timeout=None):
|
def __call__(self, arguments, env=None, timeout=None, check=True):
|
||||||
self.calls.append(list(arguments))
|
self.calls.append(list(arguments))
|
||||||
joined = ' '.join(arguments)
|
joined = ' '.join(arguments)
|
||||||
if arguments[:2] == ['node', '--version']:
|
if arguments[:2] == ['node', '--version']:
|
||||||
return 'v22.4.0'
|
return 'v22.4.0'
|
||||||
|
if arguments[-1:] == ['set-models']:
|
||||||
|
return json.dumps({'previous': self.previous}) + chr(10)
|
||||||
|
if self.fail_pull and arguments[:2] == ['qmd', 'pull']:
|
||||||
|
raise ValueError('pull failed')
|
||||||
if 'npm list' in joined:
|
if 'npm list' in joined:
|
||||||
return json.dumps({'dependencies': {'@tobilu/qmd': {}} if self.installed else {}})
|
return json.dumps({'dependencies': {'@tobilu/qmd': {}} if self.installed else {}})
|
||||||
if 'npm install' in joined:
|
if 'npm install' in joined:
|
||||||
@@ -126,6 +131,73 @@ class SetupTest(unittest.TestCase):
|
|||||||
self.assertEqual([r['kind'] for r in ledger['resources']], ['plugin'], 'plugin record stays so reconcile uninstalls qmd@qmd')
|
self.assertEqual([r['kind'] for r in ledger['resources']], ['plugin'], 'plugin record stays so reconcile uninstalls qmd@qmd')
|
||||||
self.assertTrue(self.state()['owned_package'], 'ledger-owned package is adopted')
|
self.assertTrue(self.state()['owned_package'], 'ledger-owned package is adopted')
|
||||||
|
|
||||||
|
def test_previous_models_are_restored_on_disable(self):
|
||||||
|
runner = FakeRunner(previous={'embed': 'mine'})
|
||||||
|
setup = self.setup(runner)
|
||||||
|
setup.enable(['claude'])
|
||||||
|
self.assertEqual(self.state()['previous_models'], {'embed': 'mine'})
|
||||||
|
setup.disable(['claude'])
|
||||||
|
self.assertIn(['node', str(self.home / '.my-ai-configuration/qmd/qmd-config.mjs'), 'restore-models', '{"embed": "mine"}'], runner.calls)
|
||||||
|
self.assertFalse(any(c[-1:] == ['unset-models'] for c in runner.calls))
|
||||||
|
|
||||||
|
def test_models_block_removed_when_none_existed(self):
|
||||||
|
runner = FakeRunner()
|
||||||
|
setup = self.setup(runner)
|
||||||
|
setup.enable(['claude'])
|
||||||
|
self.assertIsNone(self.state()['previous_models'])
|
||||||
|
setup.disable(['claude'])
|
||||||
|
self.assertTrue(any(c[-1:] == ['unset-models'] for c in runner.calls))
|
||||||
|
self.assertFalse(any('restore-models' in c for c in runner.calls))
|
||||||
|
|
||||||
|
def ledger(self, base):
|
||||||
|
base.mkdir(parents=True, exist_ok=True)
|
||||||
|
(base / 'extensions.json').write_text(json.dumps({'version': 1, 'resources': [
|
||||||
|
{'client': 'shared', 'name': 'QMD', 'kind': 'qmd', 'package': '@tobilu/qmd', 'clients': ['codex']}]}))
|
||||||
|
|
||||||
|
def test_disable_without_state_uninstalls_adopted_package(self):
|
||||||
|
base = self.home / '.my-ai-configuration'
|
||||||
|
self.ledger(base)
|
||||||
|
runner = FakeRunner(installed=True)
|
||||||
|
self.setup(runner).disable(['claude'])
|
||||||
|
self.assertIn(['npm', 'uninstall', '--global', '@tobilu/qmd'], runner.calls)
|
||||||
|
self.assertFalse((base / 'qmd.json').exists())
|
||||||
|
|
||||||
|
def test_ownership_is_persisted_before_ledger_rewrite(self):
|
||||||
|
base = self.home / '.my-ai-configuration'
|
||||||
|
self.ledger(base)
|
||||||
|
real = qmd.atomic_json
|
||||||
|
|
||||||
|
def failing(path, value):
|
||||||
|
if path.name == 'extensions.json':
|
||||||
|
raise OSError('disk full')
|
||||||
|
real(path, value)
|
||||||
|
with mock.patch.object(qmd, 'atomic_json', failing):
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
self.setup(FakeRunner(installed=True)).enable(['claude'])
|
||||||
|
self.assertTrue(self.state()['owned_package'])
|
||||||
|
self.assertEqual(len(json.loads((base / 'extensions.json').read_text())['resources']), 1)
|
||||||
|
|
||||||
|
def test_symlinked_script_directory_is_refused(self):
|
||||||
|
base = self.home / '.my-ai-configuration'
|
||||||
|
base.mkdir(parents=True)
|
||||||
|
target = self.home / 'elsewhere'
|
||||||
|
target.mkdir()
|
||||||
|
try:
|
||||||
|
(base / 'qmd').symlink_to(target, target_is_directory=True)
|
||||||
|
except OSError:
|
||||||
|
self.skipTest('symlinks unavailable')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.setup(FakeRunner()).enable(['claude'])
|
||||||
|
self.assertEqual(list(target.iterdir()), [])
|
||||||
|
|
||||||
|
def test_non_dict_benchmark_result_counts_as_failure(self):
|
||||||
|
runner = FakeRunner(gpu=False, cpu_result=['unexpected'])
|
||||||
|
self.assertFalse(self.setup(runner).benchmark())
|
||||||
|
|
||||||
|
def test_package_installed_parses_listing(self):
|
||||||
|
self.assertTrue(self.setup(FakeRunner(installed=True)).package_installed())
|
||||||
|
self.assertFalse(self.setup(FakeRunner()).package_installed())
|
||||||
|
|
||||||
def test_dry_run_changes_nothing(self):
|
def test_dry_run_changes_nothing(self):
|
||||||
runner = FakeRunner()
|
runner = FakeRunner()
|
||||||
self.setup(runner, dry_run=True).enable(['claude'])
|
self.setup(runner, dry_run=True).enable(['claude'])
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ export function setModels(text, YAML) {
|
|||||||
return document.toString();
|
return document.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function previousModels(text, YAML) {
|
||||||
|
const document = YAML.parseDocument(text || '');
|
||||||
|
if (document.errors.length) throw new Error(`index.yml cannot be parsed: ${document.errors[0].message}`);
|
||||||
|
return document.contents === null ? null : document.toJS().models ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreModels(text, YAML, mapping) {
|
||||||
|
const document = YAML.parseDocument(text || '');
|
||||||
|
if (document.errors.length) throw new Error(`index.yml cannot be parsed: ${document.errors[0].message}`);
|
||||||
|
if (document.contents === null) document.contents = document.createNode({});
|
||||||
|
document.set('models', document.createNode(mapping));
|
||||||
|
return document.toString();
|
||||||
|
}
|
||||||
|
|
||||||
export function unsetModels(text, YAML) {
|
export function unsetModels(text, YAML) {
|
||||||
const document = YAML.parseDocument(text || '');
|
const document = YAML.parseDocument(text || '');
|
||||||
if (document.errors.length) throw new Error(`index.yml cannot be parsed: ${document.errors[0].message}`);
|
if (document.errors.length) throw new Error(`index.yml cannot be parsed: ${document.errors[0].message}`);
|
||||||
@@ -19,16 +33,26 @@ export function unsetModels(text, YAML) {
|
|||||||
return document.contents === null || document.contents.items?.length === 0 ? '' : document.toString();
|
return document.contents === null || document.contents.items?.length === 0 ? '' : document.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(action) {
|
async function main(action, argument) {
|
||||||
if (!['set-models', 'unset-models'].includes(action)) throw new Error('usage: qmd-config.mjs set-models|unset-models');
|
if (!['set-models', 'unset-models', 'restore-models'].includes(action)) throw new Error('usage: qmd-config.mjs set-models|unset-models|restore-models <json>');
|
||||||
const YAML = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'yaml', 'dist', 'index.js')).href);
|
const YAML = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'yaml', 'dist', 'index.js')).href);
|
||||||
const path = qmdConfigPath();
|
const path = qmdConfigPath();
|
||||||
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
||||||
const next = action === 'set-models' ? setModels(current, YAML) : unsetModels(current, YAML);
|
let next;
|
||||||
|
if (action === 'set-models') {
|
||||||
|
const previous = previousModels(current, YAML);
|
||||||
|
// A block equal to ours is a leftover of an interrupted run, not the user's own.
|
||||||
|
console.log(JSON.stringify({ previous: JSON.stringify(previous) === JSON.stringify(MODELS) ? null : previous }));
|
||||||
|
next = setModels(current, YAML);
|
||||||
|
} else if (action === 'restore-models') {
|
||||||
|
const mapping = JSON.parse(argument ?? '');
|
||||||
|
if (mapping === null || typeof mapping !== 'object' || Array.isArray(mapping)) throw new Error('restore-models needs a JSON object');
|
||||||
|
next = restoreModels(current, YAML, mapping);
|
||||||
|
} else next = unsetModels(current, YAML);
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
writeFileSync(path, next);
|
writeFileSync(path, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
|
||||||
main(process.argv[2]).catch((error) => { console.error(`qmd-config: ${error.message}`); process.exit(1); });
|
main(process.argv[2], process.argv[3]).catch((error) => { console.error(`qmd-config: ${error.message}`); process.exit(1); });
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user