From f7346b1afeaf2eae50ed0f9c3f37db4682650795 Mon Sep 17 00:00:00 2001 From: Chris Hendrickson Date: Mon, 4 May 2026 21:58:26 -0400 Subject: [PATCH] Raise SDK floor and resolve vault paths from config --- .tool-versions | 1 + CONTRIBUTING.md | 2 +- README.md | 2 +- docs/config.md | 3 + packages/cli/pubspec.yaml | 2 +- packages/core/lib/src/config.dart | 42 +++++ packages/core/pubspec.yaml | 2 +- packages/core/test/dew_core_test.dart | 20 +++ packages/kanban/pubspec.yaml | 2 +- packages/mcp/pubspec.yaml | 2 +- .../lib/src/commands/delete_command.dart | 4 +- .../vault/lib/src/commands/get_command.dart | 4 +- .../vault/lib/src/commands/init_command.dart | 4 +- .../vault/lib/src/commands/list_command.dart | 4 +- .../lib/src/commands/rename_command.dart | 4 +- .../lib/src/commands/rotate_command.dart | 4 +- .../vault/lib/src/commands/set_command.dart | 4 +- .../lib/src/commands/update_command.dart | 4 +- packages/vault/pubspec.yaml | 2 +- packages/vault/test/dew_vault_test.dart | 163 ++++++++++-------- pubspec.lock | 2 +- pubspec.yaml | 2 +- 22 files changed, 184 insertions(+), 95 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..ecfcaf1 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +dart 3.12.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a555995..1796cab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thank you for your interest in contributing! This guide covers everything you ne ## Prerequisites -- **Dart SDK ^3.11.4** — verify with `dart --version` +- **Dart SDK ^3.12.0** — verify with `dart --version` - **Melos** (optional, for workspace scripts) — `dart pub global activate melos` ## Clone & setup diff --git a/README.md b/README.md index 9d85594..3127a2f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The TUI auto-refreshes when ticket files change on disk, so it stays in sync whe dart pub global activate dew ``` -Requires Dart SDK ^3.11.4. +Requires Dart SDK ^3.12.0. ## Quick start diff --git a/docs/config.md b/docs/config.md index 09c0d19..bd8d8be 100644 --- a/docs/config.md +++ b/docs/config.md @@ -10,6 +10,9 @@ your-project/ └── dew.yaml ``` +Path-like values in `dew.yaml` are resolved relative to `.project/dew.yaml` +unless they are absolute (for example, paths under `dew.vault`). + ## Full Schema ```yaml diff --git a/packages/cli/pubspec.yaml b/packages/cli/pubspec.yaml index 9e80349..9a3f222 100644 --- a/packages/cli/pubspec.yaml +++ b/packages/cli/pubspec.yaml @@ -6,7 +6,7 @@ issue_tracker: https://github.com/artificerchris/dew/issues resolution: workspace environment: - sdk: ^3.11.4 + sdk: ^3.12.0 dependencies: args: ^2.7.0 diff --git a/packages/core/lib/src/config.dart b/packages/core/lib/src/config.dart index 7dc9b0d..0f82c05 100644 --- a/packages/core/lib/src/config.dart +++ b/packages/core/lib/src/config.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:path/path.dart' as p; @@ -34,16 +36,42 @@ class ProjectContext { final String root; final DewConfig config; final FileSystem fs; + final String configFilePath; const ProjectContext({ required this.root, required this.config, required this.fs, + required this.configFilePath, }); /// Typed path helpers for this project's well-known directories. ProjectDirs get dirs => ProjectDirs(root); + /// Path to `.project/dew.yaml` used to bootstrap this context. + String get configPath => configFilePath; + + /// Resolves configuration values that are file paths relative to this project's + /// `.project/dew.yaml` location. + String resolveConfigPath(String value) { + final expanded = _expandTilde(value); + if (p.isAbsolute(expanded)) return p.normalize(expanded); + final segments = p.split(p.normalize(expanded)); + if (segments.isNotEmpty && segments.first == '.project') { + return p.normalize( + p.joinAll( + [ + p.dirname(configPath), + '..', + '.project', + ...segments.skip(1), + ], + ), + ); + } + return p.normalize(p.join(p.dirname(configPath), expanded)); + } + /// Walks up from [from] (defaults to [fs.currentDirectory]) until a /// `.project/dew.yaml` is found. static Future find({ @@ -54,11 +82,13 @@ class ProjectContext { while (true) { final configFile = fs.file(p.join(dir.path, '.project', 'dew.yaml')); if (await configFile.exists()) { + final path = configFile.path; final yaml = loadYaml(await configFile.readAsString()) as YamlMap; return ProjectContext( root: dir.path, config: DewConfig.fromYaml(yaml), fs: fs, + configFilePath: path, ); } final parent = dir.parent; @@ -72,3 +102,15 @@ class ProjectContext { } } } + +String _expandTilde(String input) { + if (!input.startsWith('~')) return input; + final home = Platform.environment['HOME'] ?? + Platform.environment['USERPROFILE'] ?? + ''; + if (home.isEmpty) return input; + if (input.length == 1) return home; + final rest = input.substring(1); + if (rest.startsWith('/')) return p.join(home, rest.substring(1)); + return p.join(home, rest); +} diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 0fa0a55..652cf73 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -6,7 +6,7 @@ issue_tracker: https://github.com/artificerchris/dew/issues resolution: workspace environment: - sdk: ^3.11.4 + sdk: ^3.12.0 # Add regular dependencies here. dependencies: diff --git a/packages/core/test/dew_core_test.dart b/packages/core/test/dew_core_test.dart index 574854f..82935eb 100644 --- a/packages/core/test/dew_core_test.dart +++ b/packages/core/test/dew_core_test.dart @@ -72,5 +72,25 @@ dew: final ctx = await ProjectContext.find(fs: fs, from: fs.directory('/sub')); expect(ctx.root, '/'); }); + + test('resolveConfigPath resolves paths relative to .project/dew.yaml', () async { + final fs = MemoryFileSystem(); + fs.directory('/foo/.project').createSync(recursive: true); + fs.file('/foo/.project/dew.yaml').writeAsStringSync(configYaml); + + final ctx = await ProjectContext.find(fs: fs, from: fs.directory('/foo/.project/child')); + expect( + ctx.resolveConfigPath('vault'), + '/foo/.project/vault', + ); + expect( + ctx.resolveConfigPath('.project/vault'), + '/foo/.project/vault', + ); + expect( + ctx.resolveConfigPath('/tmp/abs'), + '/tmp/abs', + ); + }); }); } diff --git a/packages/kanban/pubspec.yaml b/packages/kanban/pubspec.yaml index d035b52..182ae2c 100644 --- a/packages/kanban/pubspec.yaml +++ b/packages/kanban/pubspec.yaml @@ -6,7 +6,7 @@ issue_tracker: https://github.com/artificerchris/dew/issues resolution: workspace environment: - sdk: ^3.11.4 + sdk: ^3.12.0 # Add regular dependencies here. dependencies: diff --git a/packages/mcp/pubspec.yaml b/packages/mcp/pubspec.yaml index db5cc44..645a810 100644 --- a/packages/mcp/pubspec.yaml +++ b/packages/mcp/pubspec.yaml @@ -6,7 +6,7 @@ issue_tracker: https://github.com/artificerchris/dew/issues resolution: workspace environment: - sdk: ^3.11.4 + sdk: ^3.12.0 # Add regular dependencies here. dependencies: diff --git a/packages/vault/lib/src/commands/delete_command.dart b/packages/vault/lib/src/commands/delete_command.dart index 13daadb..2966a11 100644 --- a/packages/vault/lib/src/commands/delete_command.dart +++ b/packages/vault/lib/src/commands/delete_command.dart @@ -41,8 +41,8 @@ class DeleteCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); await store.delete(secretName); diff --git a/packages/vault/lib/src/commands/get_command.dart b/packages/vault/lib/src/commands/get_command.dart index 238abae..c518b2c 100644 --- a/packages/vault/lib/src/commands/get_command.dart +++ b/packages/vault/lib/src/commands/get_command.dart @@ -37,8 +37,8 @@ class GetCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); diff --git a/packages/vault/lib/src/commands/init_command.dart b/packages/vault/lib/src/commands/init_command.dart index 6403917..a2d9da9 100644 --- a/packages/vault/lib/src/commands/init_command.dart +++ b/packages/vault/lib/src/commands/init_command.dart @@ -46,8 +46,8 @@ class VaultInitCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final store = VaultStore( - storageDir: resolveProjectPath(context.root, storageDir), - passwordFilePath: resolveProjectPath(context.root, passwordFile), + storageDir: context.resolveConfigPath(storageDir), + passwordFilePath: context.resolveConfigPath(passwordFile), fs: context.fs, ); await store.initialize(); diff --git a/packages/vault/lib/src/commands/list_command.dart b/packages/vault/lib/src/commands/list_command.dart index 0ebeed2..0b6c181 100644 --- a/packages/vault/lib/src/commands/list_command.dart +++ b/packages/vault/lib/src/commands/list_command.dart @@ -33,8 +33,8 @@ class ListCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); final names = await store.listSecretNames(); diff --git a/packages/vault/lib/src/commands/rename_command.dart b/packages/vault/lib/src/commands/rename_command.dart index e796ad5..bcaae78 100644 --- a/packages/vault/lib/src/commands/rename_command.dart +++ b/packages/vault/lib/src/commands/rename_command.dart @@ -46,8 +46,8 @@ class RenameCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); await store.rename(from, to); diff --git a/packages/vault/lib/src/commands/rotate_command.dart b/packages/vault/lib/src/commands/rotate_command.dart index 71bc5d0..ecb9879 100644 --- a/packages/vault/lib/src/commands/rotate_command.dart +++ b/packages/vault/lib/src/commands/rotate_command.dart @@ -38,8 +38,8 @@ class RotateCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); diff --git a/packages/vault/lib/src/commands/set_command.dart b/packages/vault/lib/src/commands/set_command.dart index 924ebcc..863b5c8 100644 --- a/packages/vault/lib/src/commands/set_command.dart +++ b/packages/vault/lib/src/commands/set_command.dart @@ -46,8 +46,8 @@ class SetCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); diff --git a/packages/vault/lib/src/commands/update_command.dart b/packages/vault/lib/src/commands/update_command.dart index 626d866..5e2232b 100644 --- a/packages/vault/lib/src/commands/update_command.dart +++ b/packages/vault/lib/src/commands/update_command.dart @@ -55,8 +55,8 @@ class UpdateCommand extends DewCommand with DewToolCommand { final context = await ProjectContext.find(fs: _fs); final config = context.config.vault; final store = VaultStore( - storageDir: resolveProjectPath(context.root, config.storageDir), - passwordFilePath: resolveProjectPath(context.root, config.passwordFile), + storageDir: context.resolveConfigPath(config.storageDir), + passwordFilePath: context.resolveConfigPath(config.passwordFile), fs: context.fs, ); diff --git a/packages/vault/pubspec.yaml b/packages/vault/pubspec.yaml index 5a8a037..d1adfb7 100644 --- a/packages/vault/pubspec.yaml +++ b/packages/vault/pubspec.yaml @@ -6,7 +6,7 @@ issue_tracker: https://github.com/artificerchris/dew/issues resolution: workspace environment: - sdk: ^3.11.4 + sdk: ^3.12.0 dependencies: dew_core: ^0.1.0 diff --git a/packages/vault/test/dew_vault_test.dart b/packages/vault/test/dew_vault_test.dart index ab995b5..e5aa9dd 100644 --- a/packages/vault/test/dew_vault_test.dart +++ b/packages/vault/test/dew_vault_test.dart @@ -48,19 +48,17 @@ void main() { final tools = registry.mcpTools.map((t) => t.name).toSet(); expect( tools, - containsAll( - { - 'vault_init', - 'vault_set_secret', - 'vault_get_secret', - 'vault_update_secret', - 'vault_rename_secret', - 'vault_rotate_secret', - 'vault_generate_secret', - 'vault_list_secrets', - 'vault_delete_secret', - }, - ), + containsAll({ + 'vault_init', + 'vault_set_secret', + 'vault_get_secret', + 'vault_update_secret', + 'vault_rename_secret', + 'vault_rotate_secret', + 'vault_generate_secret', + 'vault_list_secrets', + 'vault_delete_secret', + }), ); }); }); @@ -151,11 +149,17 @@ void main() { ); expect(initResult['message'], 'Vault initialised.'); expect(initResult['initialized'], isTrue); - expect(initResult['password_file'], '/.project/secrets/dew.vault.password'); + expect( + initResult['password_file'], + '/.project/secrets/dew.vault.password', + ); expect(initResult['storage_dir'], '/.project/vault'); expect(await fs.directory('/.project/vault').exists(), isTrue); - expect(await fs.file('/.project/secrets/dew.vault.password').exists(), isTrue); + expect( + await fs.file('/.project/secrets/dew.vault.password').exists(), + isTrue, + ); }); test('init command accepts custom password and storage paths', () async { @@ -168,20 +172,27 @@ void main() { ); expect( customInit['password_file'], - '/.custom/secrets/custom.vault.password', + '/.project/.custom/secrets/custom.vault.password', + ); + expect(customInit['storage_dir'], '/.project/.custom/vault-store'); + expect( + await fs.directory('/.project/.custom/vault-store').exists(), + isTrue, ); expect( - customInit['storage_dir'], - '/.custom/vault-store', + await fs + .file('/.project/.custom/secrets/custom.vault.password') + .exists(), + isTrue, ); - expect(await fs.directory('/.custom/vault-store').exists(), isTrue); - expect(await fs.file('/.custom/secrets/custom.vault.password').exists(), isTrue); }); test('set command accepts metadata from file', () async { - fs.file('/meta.json').writeAsStringSync( - '{"rotation":{"generator":"postgres_password","length":16},"notes":"from-file"}', - ); + fs + .file('/meta.json') + .writeAsStringSync( + '{"rotation":{"generator":"postgres_password","length":16},"notes":"from-file"}', + ); await tools['vault_set_secret']!.handler({ 'name': 'META_FILE_SECRET', 'file': '/seed.txt', @@ -242,9 +253,11 @@ void main() { }); test('update command accepts metadata file', () async { - fs.file('/meta-update.json').writeAsStringSync( - '{"rotation":{"generator":"postgres_password","length":24},"notes":"file-metadata"}', - ); + fs + .file('/meta-update.json') + .writeAsStringSync( + '{"rotation":{"generator":"postgres_password","length":24},"notes":"file-metadata"}', + ); await tools['vault_set_secret']!.handler({ 'name': 'META_UPDATE_SECRET', 'file': '/seed.txt', @@ -289,7 +302,10 @@ void main() { expect(listResult['secrets'], isNot(contains('LEGACY_KEY'))); await expectLater( - tools['vault_get_secret']!.handler({'name': 'LEGACY_KEY', 'format': 'json'}), + tools['vault_get_secret']!.handler({ + 'name': 'LEGACY_KEY', + 'format': 'json', + }), throwsA(isA()), ); @@ -320,51 +336,54 @@ void main() { ); }); - test('rotate command uses rotation metadata and can rotate vault password', () async { - await tools['vault_set_secret']!.handler({ - 'name': 'service_db_password', - 'file': '/seed.txt', - 'metadata': - '{"rotation":{"generator":"postgres_password","length":12}}', - }); - await tools['vault_set_secret']!.handler({ - 'name': 'service_api_key', - 'file': '/new-seed.txt', - 'metadata': - '{"rotation":{"generator":"postgres_password","length":12}}', - }); - - final before = _decodeToolJson( - await tools['vault_get_secret']!.handler({ + test( + 'rotate command uses rotation metadata and can rotate vault password', + () async { + await tools['vault_set_secret']!.handler({ 'name': 'service_db_password', - 'format': 'json', - }), - ); - expect(before['value'], 'super-secret'); + 'file': '/seed.txt', + 'metadata': + '{"rotation":{"generator":"postgres_password","length":12}}', + }); + await tools['vault_set_secret']!.handler({ + 'name': 'service_api_key', + 'file': '/new-seed.txt', + 'metadata': + '{"rotation":{"generator":"postgres_password","length":12}}', + }); - final secretRotated = _decodeToolJson( - await tools['vault_rotate_secret']!.handler({ - 'name': 'service_db_password', - 'format': 'json', - }), - ); - expect(secretRotated['scope'], 'secret'); - expect(secretRotated['name'], 'service_db_password'); + final before = _decodeToolJson( + await tools['vault_get_secret']!.handler({ + 'name': 'service_db_password', + 'format': 'json', + }), + ); + expect(before['value'], 'super-secret'); - final after = _decodeToolJson( - await tools['vault_get_secret']!.handler({ - 'name': 'service_db_password', - 'format': 'json', - }), - ); - expect(after['value'], isNot('super-secret')); + final secretRotated = _decodeToolJson( + await tools['vault_rotate_secret']!.handler({ + 'name': 'service_db_password', + 'format': 'json', + }), + ); + expect(secretRotated['scope'], 'secret'); + expect(secretRotated['name'], 'service_db_password'); - final allRotated = _decodeToolJson( - await tools['vault_rotate_secret']!.handler({'format': 'json'}), - ); - expect(allRotated['scope'], 'vault'); - expect(allRotated['rotated_count'], 2); - }); + final after = _decodeToolJson( + await tools['vault_get_secret']!.handler({ + 'name': 'service_db_password', + 'format': 'json', + }), + ); + expect(after['value'], isNot('super-secret')); + + final allRotated = _decodeToolJson( + await tools['vault_rotate_secret']!.handler({'format': 'json'}), + ); + expect(allRotated['scope'], 'vault'); + expect(allRotated['rotated_count'], 2); + }, + ); }); group('vault crypto helpers', () { @@ -373,10 +392,14 @@ void main() { const value = 'very-sensitive'; final encoded = VaultCrypto.encryptToEnvelope(value, password: password); expect(encoded['version'], 1); - final decoded = VaultCrypto.decryptFromEnvelope(encoded, password: password); + final decoded = VaultCrypto.decryptFromEnvelope( + encoded, + password: password, + ); expect(decoded, value); expect( - () => VaultCrypto.decryptFromEnvelope(encoded, password: 'bad-password'), + () => + VaultCrypto.decryptFromEnvelope(encoded, password: 'bad-password'), throwsA(isA()), ); }); diff --git a/pubspec.lock b/pubspec.lock index 47bd6bf..164b82d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -618,4 +618,4 @@ packages: source: hosted version: "2.2.4" sdks: - dart: ">=3.11.4 <4.0.0" + dart: ">=3.12.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 30260b0..8691b20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/artificerchris/dew publish_to: none environment: - sdk: ^3.11.4 + sdk: ^3.12.0 workspace: - packages/cli - packages/core