feat(vault): scaffold vault package and command registration
This commit is contained in:
parent
07e8c98c7c
commit
1281bd4092
22 changed files with 766 additions and 3 deletions
169
docs/features/vault.md
Normal file
169
docs/features/vault.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Dew Vault Secret Manager
|
||||||
|
|
||||||
|
Dew Vault is a secret manager for your projects. It helps you keep secrets out of
|
||||||
|
version control by storing each secret as an encrypted file under `.project/vault`.
|
||||||
|
By default the vault password is stored in `.project/secrets/dew.vault.password`.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Vault settings live in `.project/dew.yaml` under `dew.vault`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dew:
|
||||||
|
vault:
|
||||||
|
password_file: .project/secrets/dew.vault.password
|
||||||
|
storage_dir: .project/vault
|
||||||
|
generators:
|
||||||
|
postgres_password:
|
||||||
|
type: random_password
|
||||||
|
description: Generate random PostgreSQL passwords.
|
||||||
|
config:
|
||||||
|
length: 64
|
||||||
|
include_symbols: true
|
||||||
|
jwt_secret:
|
||||||
|
type: random_token
|
||||||
|
description: Generate JWT signing secrets.
|
||||||
|
config:
|
||||||
|
encoding: base64
|
||||||
|
bytes: 48
|
||||||
|
service_uuid:
|
||||||
|
type: uuid_v4
|
||||||
|
description: Generate stable-looking unique IDs.
|
||||||
|
```
|
||||||
|
|
||||||
|
`generators` maps a generator name (for example `postgres_password`) to a built-in
|
||||||
|
generator definition. Values under `config` are defaults and can be overridden per
|
||||||
|
run or stored in secret rotation metadata.
|
||||||
|
|
||||||
|
Built-in generator types are resolved inside Dew, so secrets can be generated
|
||||||
|
without depending on host binaries.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Most commands support `--format [default|json]` (default is `default`) for
|
||||||
|
machine-friendly automation.
|
||||||
|
|
||||||
|
### Initialize Vault
|
||||||
|
|
||||||
|
Initialize the vault storage and metadata.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault init
|
||||||
|
|
||||||
|
dew vault init --password-file .project/secrets/dew.vault.password
|
||||||
|
```
|
||||||
|
|
||||||
|
### List all secrets
|
||||||
|
|
||||||
|
List stored secrets.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault list
|
||||||
|
dew vault list --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set a secret
|
||||||
|
|
||||||
|
`set` stores or replaces a secret and optional metadata.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault set <secret-name> # Prompts for secret value
|
||||||
|
dew vault set <secret-name> --env ENV_VAR_NAME # Uses value from environment variable
|
||||||
|
dew vault set <secret-name> --file /path/to/secret.txt # Uses value from file
|
||||||
|
echo "secret value" | dew vault set <secret-name> # Uses piped stdin
|
||||||
|
|
||||||
|
# Include metadata for automated rotation requirements
|
||||||
|
dew vault set DB_PASSWORD --metadata '{"rotation":{"enabled":true,"generator":"postgres_password","length":64}}'
|
||||||
|
dew vault set DB_PASSWORD --metadata-file .project/vault/db_password.meta.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get a secret
|
||||||
|
|
||||||
|
`get` retrieves a secret by name.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault get <secret-name>
|
||||||
|
dew vault get <secret-name> --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update a secret
|
||||||
|
|
||||||
|
`update` patches secret metadata and/or value. Omit value source flags to edit
|
||||||
|
metadata only.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault update <secret-name> --env ROLLED_PASSWORD
|
||||||
|
dew vault update <secret-name> --metadata '{"rotation":{"enabled":false}}'
|
||||||
|
dew vault update <secret-name> --metadata-file .project/vault/db_password.meta.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rename a secret
|
||||||
|
|
||||||
|
`rename` changes a secret identifier while preserving value and metadata.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault rename OLD_NAME NEW_NAME
|
||||||
|
dew vault rename OLD_NAME NEW_NAME --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generate a secret value
|
||||||
|
|
||||||
|
`generate` runs a built-in generator without writing to the vault by default.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault generate postgres_password --length 64 --include_symbols
|
||||||
|
dew vault generate jwt_secret --bytes 64 --encoding base64
|
||||||
|
dew vault generate postgres_password --service payments --username app_user --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
Pipe generated output directly into `set` when needed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault generate postgres_password --service payments | dew vault set DB_PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rotate secrets
|
||||||
|
|
||||||
|
`rotate` rewraps secrets with a new vault password when run without a name.
|
||||||
|
When run with a secret name, it rotates only that secret. For a secret with rotation
|
||||||
|
metadata (`rotation.enabled: true` and `rotation.generator`), Dew invokes that
|
||||||
|
configured built-in generator using the provided rotation values.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault rotate
|
||||||
|
dew vault rotate <secret-name>
|
||||||
|
dew vault rotate <secret-name> --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete a secret
|
||||||
|
|
||||||
|
`delete` removes a secret and metadata from the vault.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dew vault delete <secret-name>
|
||||||
|
dew vault delete <secret-name> --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metadata format for rotation-aware secrets
|
||||||
|
|
||||||
|
Attach arbitrary metadata and include rotation policy details. Example shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rotation": {
|
||||||
|
"enabled": true,
|
||||||
|
"generator": "postgres_password",
|
||||||
|
"service": "payments",
|
||||||
|
"username": "app_user",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
"notes": "Rotate monthly and update app config via sidecar"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rotation flow:
|
||||||
|
|
||||||
|
1. Define a built-in generator in `dew.yaml` under `dew.vault.generators`.
|
||||||
|
2. Attach `rotation.generator` and generator args to the secret metadata.
|
||||||
|
3. Run `dew vault rotate <secret-name>` to rotate one secret, or `dew vault rotate`
|
||||||
|
to rotate all configured secrets.
|
||||||
|
|
@ -2,11 +2,13 @@ import 'package:args/command_runner.dart';
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:dew_kanban/dew_kanban.dart' as kanban;
|
import 'package:dew_kanban/dew_kanban.dart' as kanban;
|
||||||
import 'package:dew_mcp/dew_mcp.dart' as mcp;
|
import 'package:dew_mcp/dew_mcp.dart' as mcp;
|
||||||
|
import 'package:dew_vault/dew_vault.dart' as vault;
|
||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
final commandRegistry = CommandRegistry();
|
final commandRegistry = CommandRegistry();
|
||||||
|
|
||||||
kanban.registerCommands(commandRegistry);
|
kanban.registerCommands(commandRegistry);
|
||||||
|
vault.registerCommands(commandRegistry);
|
||||||
mcp.registerCommands(commandRegistry);
|
mcp.registerCommands(commandRegistry);
|
||||||
|
|
||||||
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ dependencies:
|
||||||
args: ^2.7.0
|
args: ^2.7.0
|
||||||
dew_core: ^0.1.0
|
dew_core: ^0.1.0
|
||||||
dew_kanban: ^0.1.0
|
dew_kanban: ^0.1.0
|
||||||
|
dew_vault: ^0.1.0
|
||||||
dew_mcp: ^0.1.0
|
dew_mcp: ^0.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ import 'package:args/command_runner.dart';
|
||||||
import 'package:dew_core/dew_core.dart';
|
import 'package:dew_core/dew_core.dart';
|
||||||
import 'package:dew_kanban/dew_kanban.dart' as kanban;
|
import 'package:dew_kanban/dew_kanban.dart' as kanban;
|
||||||
import 'package:dew_mcp/dew_mcp.dart' as mcp;
|
import 'package:dew_mcp/dew_mcp.dart' as mcp;
|
||||||
|
import 'package:dew_vault/dew_vault.dart' as vault;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
/// Builds the same CommandRunner as bin/dew.dart without actually running it.
|
/// Builds the same CommandRunner as bin/dew.dart without actually running it.
|
||||||
CommandRunner<void> buildRunner() {
|
CommandRunner<void> buildRunner() {
|
||||||
final commandRegistry = CommandRegistry();
|
final commandRegistry = CommandRegistry();
|
||||||
kanban.registerCommands(commandRegistry);
|
kanban.registerCommands(commandRegistry);
|
||||||
|
vault.registerCommands(commandRegistry);
|
||||||
mcp.registerCommands(commandRegistry);
|
mcp.registerCommands(commandRegistry);
|
||||||
|
|
||||||
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
final runner = CommandRunner<void>('dew', 'A project management tool.');
|
||||||
|
|
@ -24,9 +26,9 @@ void main() {
|
||||||
expect(buildRunner, returnsNormally);
|
expect(buildRunner, returnsNormally);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('has kanban, init, and mcp commands registered', () {
|
test('has kanban, vault, init, and mcp commands registered', () {
|
||||||
final runner = buildRunner();
|
final runner = buildRunner();
|
||||||
expect(runner.commands.keys, containsAll(['kanban', 'init', 'mcp']));
|
expect(runner.commands.keys, containsAll(['kanban', 'vault', 'init', 'mcp']));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('--help flag does not throw', () async {
|
test('--help flag does not throw', () async {
|
||||||
|
|
|
||||||
9
packages/vault/CHANGELOG.md
Normal file
9
packages/vault/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 — 2026-05-03
|
||||||
|
|
||||||
|
Initial feature stub package for vault support.
|
||||||
|
|
||||||
|
- Added `dew vault` command registration and MCP tool wiring.
|
||||||
|
- Added vault subcommands for init/list/get/set/update/rename/rotate/generate/delete.
|
||||||
|
- Added `VaultInitHook` scaffold for `.project/vault` and `.project/secrets`.
|
||||||
21
packages/vault/LICENSE
Normal file
21
packages/vault/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 artificery-dev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
16
packages/vault/README.md
Normal file
16
packages/vault/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# dew_vault
|
||||||
|
|
||||||
|
Vault feature package for the [Dew](https://github.com/artificerchris/dew) project
|
||||||
|
management tool.
|
||||||
|
|
||||||
|
This package provides the `dew vault` command surface and registers Vault commands
|
||||||
|
as MCP tools through `DewToolCommand`.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This package is currently a scaffold/stub to establish the command and MCP
|
||||||
|
wiring. It does not yet implement full encrypted secret storage or rotation logic.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
19
packages/vault/lib/dew_vault.dart
Normal file
19
packages/vault/lib/dew_vault.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
library;
|
||||||
|
|
||||||
|
export 'src/dew_vault_base.dart';
|
||||||
|
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
|
||||||
|
import 'package:dew_vault/src/dew_vault_base.dart';
|
||||||
|
import 'package:dew_vault/src/vault_init_hook.dart';
|
||||||
|
|
||||||
|
/// Registers all Vault commands and init hooks into [registry].
|
||||||
|
void registerCommands(
|
||||||
|
CommandRegistry registry, {
|
||||||
|
FileSystem fs = const LocalFileSystem(),
|
||||||
|
}) {
|
||||||
|
registry.register(VaultCommand());
|
||||||
|
registry.registerInitHook(VaultInitHook(fs: fs));
|
||||||
|
}
|
||||||
30
packages/vault/lib/src/command_output.dart
Normal file
30
packages/vault/lib/src/command_output.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
String renderVaultOutput({
|
||||||
|
String format = 'default',
|
||||||
|
required String message,
|
||||||
|
Map<String, dynamic>? json,
|
||||||
|
}) {
|
||||||
|
if (format == 'json') {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'message': message,
|
||||||
|
if (json != null) ...json,
|
||||||
|
};
|
||||||
|
return const JsonEncoder.withIndent(' ').convert(payload);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
String requireStringArg(Map<String, dynamic> args, String key) {
|
||||||
|
final value = args[key];
|
||||||
|
if (value == null || value.toString().trim().isEmpty) {
|
||||||
|
throw ArgumentError('Missing required argument: --$key');
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String formatFromArgs(Map<String, dynamic> args) {
|
||||||
|
final value = args['format'];
|
||||||
|
if (value == null || value.toString().trim().isEmpty) return 'default';
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
42
packages/vault/lib/src/commands/delete_command.dart
Normal file
42
packages/vault/lib/src/commands/delete_command.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class DeleteCommand extends DewCommand with DewToolCommand {
|
||||||
|
DeleteCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'name',
|
||||||
|
abbr: 'n',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Secret name.',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'delete';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Delete a secret.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_delete_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final secretName = requireStringArg(args, 'name');
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Delete stub executed.',
|
||||||
|
json: {'secret': secretName},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
packages/vault/lib/src/commands/generate_command.dart
Normal file
51
packages/vault/lib/src/commands/generate_command.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class GenerateCommand extends DewCommand with DewToolCommand {
|
||||||
|
GenerateCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'generator',
|
||||||
|
abbr: 'g',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Generator ID.',
|
||||||
|
)
|
||||||
|
..addMultiOption(
|
||||||
|
'arg',
|
||||||
|
help: 'Generator option as key=value. Repeat as needed.',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'generate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Generate a new secret value using a built-in generator.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_generate_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final generator = requireStringArg(args, 'generator');
|
||||||
|
final overrides = args['arg'] as List<dynamic>?;
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Generate stub output.',
|
||||||
|
json: {
|
||||||
|
'generator': generator,
|
||||||
|
'options': overrides == null ? const <String>[] : overrides,
|
||||||
|
'value': '<generated>',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
37
packages/vault/lib/src/commands/get_command.dart
Normal file
37
packages/vault/lib/src/commands/get_command.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class GetCommand extends DewCommand with DewToolCommand {
|
||||||
|
GetCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption('name', abbr: 'n', mandatory: true, help: 'Secret name.')
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'get';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Get a secret value.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_get_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final secretName = requireStringArg(args, 'name');
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Get stub value: [redacted].',
|
||||||
|
json: {'secret': secretName},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
packages/vault/lib/src/commands/init_command.dart
Normal file
51
packages/vault/lib/src/commands/init_command.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class VaultInitCommand extends DewCommand with DewToolCommand {
|
||||||
|
VaultInitCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'password-file',
|
||||||
|
abbr: 'p',
|
||||||
|
defaultsTo: '.project/secrets/dew.vault.password',
|
||||||
|
help: 'Path to the vault password file to record in config.',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'storage-dir',
|
||||||
|
defaultsTo: '.project/vault',
|
||||||
|
help: 'Directory where encrypted secret files are stored.',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'init';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Initialise vault directories and defaults.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_init';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final passwordFile = requireStringArg(args, 'password-file');
|
||||||
|
final storageDir = requireStringArg(args, 'storage-dir');
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Vault init stub completed.',
|
||||||
|
json: {
|
||||||
|
'password_file': passwordFile,
|
||||||
|
'storage_dir': storageDir,
|
||||||
|
'initialized': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/vault/lib/src/commands/list_command.dart
Normal file
34
packages/vault/lib/src/commands/list_command.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class ListCommand extends DewCommand with DewToolCommand {
|
||||||
|
ListCommand() {
|
||||||
|
argParser.addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'list';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'List vault secrets.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_list_secrets';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'No secrets found (stubbed vault).',
|
||||||
|
json: {'secrets': <String>[], 'count': 0},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
47
packages/vault/lib/src/commands/rename_command.dart
Normal file
47
packages/vault/lib/src/commands/rename_command.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class RenameCommand extends DewCommand with DewToolCommand {
|
||||||
|
RenameCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'from',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Current secret name.',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'to',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'New secret name.',
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'rename';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Rename a secret while preserving value and metadata.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_rename_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final from = requireStringArg(args, 'from');
|
||||||
|
final to = requireStringArg(args, 'to');
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Rename stub executed.',
|
||||||
|
json: {'from': from, 'to': to},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
packages/vault/lib/src/commands/rotate_command.dart
Normal file
38
packages/vault/lib/src/commands/rotate_command.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class RotateCommand extends DewCommand with DewToolCommand {
|
||||||
|
RotateCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption('name', help: 'Secret name to rotate; omit to rotate vault password.')
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'rotate';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description =
|
||||||
|
'Rotate vault password or a single secret value (stub).';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_rotate_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final target = args['name']?.toString();
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Rotate stub executed.',
|
||||||
|
json: {'target': target ?? '<all>', 'scope': target == null ? 'vault' : 'secret'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
packages/vault/lib/src/commands/set_command.dart
Normal file
52
packages/vault/lib/src/commands/set_command.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class SetCommand extends DewCommand with DewToolCommand {
|
||||||
|
SetCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'name',
|
||||||
|
abbr: 'n',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Secret name.',
|
||||||
|
)
|
||||||
|
..addOption('env', help: 'Use value from an environment variable.')
|
||||||
|
..addOption('file', help: 'Use value from file path.')
|
||||||
|
..addOption('metadata', help: 'JSON object to save as metadata.')
|
||||||
|
..addOption('metadata-file', help: 'Path to JSON metadata file.')
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'set';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Set a secret value and optional metadata.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_set_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final secretName = requireStringArg(args, 'name');
|
||||||
|
final source =
|
||||||
|
args['env'] ?? args['file'] ?? args['metadata'] ?? args['metadata-file'];
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Set stub executed.',
|
||||||
|
json: {
|
||||||
|
'secret': secretName,
|
||||||
|
'source': source == null ? 'interactive' : source.toString(),
|
||||||
|
'status': 'set',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
packages/vault/lib/src/commands/update_command.dart
Normal file
50
packages/vault/lib/src/commands/update_command.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import '../command_output.dart';
|
||||||
|
|
||||||
|
class UpdateCommand extends DewCommand with DewToolCommand {
|
||||||
|
UpdateCommand() {
|
||||||
|
argParser
|
||||||
|
..addOption(
|
||||||
|
'name',
|
||||||
|
abbr: 'n',
|
||||||
|
mandatory: true,
|
||||||
|
help: 'Secret name.',
|
||||||
|
)
|
||||||
|
..addOption('env', help: 'Use value from an environment variable.')
|
||||||
|
..addOption('file', help: 'Use value from file path.')
|
||||||
|
..addOption('metadata', help: 'JSON object to save as metadata.')
|
||||||
|
..addOption('metadata-file', help: 'Path to JSON metadata file.')
|
||||||
|
..addOption(
|
||||||
|
'format',
|
||||||
|
defaultsTo: 'default',
|
||||||
|
allowed: ['default', 'json'],
|
||||||
|
help: 'Output format for this command.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'update';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Update a secret value and/or metadata.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String toolName = 'vault_update_secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String> callAsTool(Map<String, dynamic> args) async {
|
||||||
|
final format = formatFromArgs(args);
|
||||||
|
final secretName = requireStringArg(args, 'name');
|
||||||
|
final mode = args['env'] != null || args['file'] != null ? 'value-update' : 'metadata-only';
|
||||||
|
return renderVaultOutput(
|
||||||
|
format: format,
|
||||||
|
message: 'Update stub executed.',
|
||||||
|
json: {
|
||||||
|
'secret': secretName,
|
||||||
|
'mode': mode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
packages/vault/lib/src/dew_vault_base.dart
Normal file
35
packages/vault/lib/src/dew_vault_base.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
|
||||||
|
import 'commands/delete_command.dart';
|
||||||
|
import 'commands/generate_command.dart';
|
||||||
|
import 'commands/get_command.dart';
|
||||||
|
import 'commands/init_command.dart';
|
||||||
|
import 'commands/list_command.dart';
|
||||||
|
import 'commands/rename_command.dart';
|
||||||
|
import 'commands/rotate_command.dart';
|
||||||
|
import 'commands/set_command.dart';
|
||||||
|
import 'commands/update_command.dart';
|
||||||
|
|
||||||
|
/// Top-level CLI command for all Vault operations.
|
||||||
|
class VaultCommand extends DewCommand {
|
||||||
|
VaultCommand() {
|
||||||
|
addSubcommand(VaultInitCommand());
|
||||||
|
addSubcommand(ListCommand());
|
||||||
|
addSubcommand(SetCommand());
|
||||||
|
addSubcommand(GetCommand());
|
||||||
|
addSubcommand(UpdateCommand());
|
||||||
|
addSubcommand(RenameCommand());
|
||||||
|
addSubcommand(RotateCommand());
|
||||||
|
addSubcommand(GenerateCommand());
|
||||||
|
addSubcommand(DeleteCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name = 'vault';
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String description = 'Manage project secrets with a local vault.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> run() async => printUsage();
|
||||||
|
}
|
||||||
39
packages/vault/lib/src/vault_init_hook.dart
Normal file
39
packages/vault/lib/src/vault_init_hook.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:dew_core/dew_core.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
class VaultInitHook implements DewInitHook {
|
||||||
|
final FileSystem _fs;
|
||||||
|
|
||||||
|
VaultInitHook({FileSystem fs = const LocalFileSystem()}) : _fs = fs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onInit(
|
||||||
|
String projectRoot,
|
||||||
|
DewConfig config,
|
||||||
|
DewInitOptions options,
|
||||||
|
) async {
|
||||||
|
final vaultDir = p.join(projectRoot, '.project', 'vault');
|
||||||
|
final secretDir = p.join(projectRoot, '.project', 'secrets');
|
||||||
|
|
||||||
|
await _createDir(vaultDir, options.gitkeep);
|
||||||
|
await _createDir(secretDir, options.gitkeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createDir(String path, bool withGitkeep) async {
|
||||||
|
final dir = _fs.directory(path);
|
||||||
|
final existed = await dir.exists();
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
final relPath = p.join('.project', p.basename(path));
|
||||||
|
if (existed) {
|
||||||
|
print(' found $relPath/');
|
||||||
|
} else {
|
||||||
|
print(' created $relPath/');
|
||||||
|
if (withGitkeep) {
|
||||||
|
await _fs.file(p.join(path, '.gitkeep')).writeAsString('');
|
||||||
|
print(' created $relPath/.gitkeep');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/vault/pubspec.yaml
Normal file
18
packages/vault/pubspec.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
name: dew_vault
|
||||||
|
description: Vault feature package for the Dew project management tool.
|
||||||
|
version: 0.2.0
|
||||||
|
repository: https://github.com/artificerchris/dew
|
||||||
|
issue_tracker: https://github.com/artificerchris/dew/issues
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.11.4
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
dew_core: ^0.2.0
|
||||||
|
file: ^7.0.1
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^6.0.0
|
||||||
|
test: ^1.25.6
|
||||||
|
|
@ -6,12 +6,12 @@ publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.4
|
sdk: ^3.11.4
|
||||||
|
|
||||||
workspace:
|
workspace:
|
||||||
- packages/cli
|
- packages/cli
|
||||||
- packages/core
|
- packages/core
|
||||||
- packages/kanban
|
- packages/kanban
|
||||||
- packages/mcp
|
- packages/mcp
|
||||||
|
- packages/vault
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
file: ^7.0.1
|
file: ^7.0.1
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue