Raise SDK floor and resolve vault paths from config
This commit is contained in:
parent
0cd08e78d3
commit
f7346b1afe
22 changed files with 184 additions and 95 deletions
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
dart 3.12.0
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ProjectContext> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ void main() {
|
|||
final tools = registry.mcpTools.map((t) => t.name).toSet();
|
||||
expect(
|
||||
tools,
|
||||
containsAll(
|
||||
{
|
||||
containsAll({
|
||||
'vault_init',
|
||||
'vault_set_secret',
|
||||
'vault_get_secret',
|
||||
|
|
@ -59,8 +58,7 @@ void main() {
|
|||
'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,18 +172,25 @@ 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(
|
||||
fs
|
||||
.file('/meta.json')
|
||||
.writeAsStringSync(
|
||||
'{"rotation":{"generator":"postgres_password","length":16},"notes":"from-file"}',
|
||||
);
|
||||
await tools['vault_set_secret']!.handler({
|
||||
|
|
@ -242,7 +253,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('update command accepts metadata file', () async {
|
||||
fs.file('/meta-update.json').writeAsStringSync(
|
||||
fs
|
||||
.file('/meta-update.json')
|
||||
.writeAsStringSync(
|
||||
'{"rotation":{"generator":"postgres_password","length":24},"notes":"file-metadata"}',
|
||||
);
|
||||
await tools['vault_set_secret']!.handler({
|
||||
|
|
@ -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<ArgumentError>()),
|
||||
);
|
||||
|
||||
|
|
@ -320,7 +336,9 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('rotate command uses rotation metadata and can rotate vault password', () async {
|
||||
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',
|
||||
|
|
@ -364,7 +382,8 @@ void main() {
|
|||
);
|
||||
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<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue