Skip to content

Commit 3b7e2df

Browse files
committed
Interface parity: fix shell args, add encrypt toggle, add vault:delete and env:remove
Shell fixes: - Fix diff command mapping broken --from/--to to correct --env (collect) - Add unmask flag to history command - Add overwrite, skip-existing, dry-run flags to import command - Add overwrite, dry-run flags to copy command Web UI: - Add encrypt toggle to SecretDialog for create/update operations - Wire secure param through API client, composable, and controller New CLI commands: - vault:delete: Remove vault config with confirmation and default vault protection - env:remove: Remove custom environments with system env protection
1 parent e275122 commit 3b7e2df

8 files changed

Lines changed: 212 additions & 21 deletions

File tree

src/Commands/EnvRemoveCommand.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace STS\Keep\Commands;
4+
5+
use STS\Keep\Data\Settings;
6+
use STS\Keep\Facades\Keep;
7+
8+
use function Laravel\Prompts\confirm;
9+
use function Laravel\Prompts\select;
10+
11+
class EnvRemoveCommand extends BaseCommand
12+
{
13+
private const SYSTEM_ENVS = ['local', 'staging', 'production'];
14+
15+
protected $signature = 'env:remove
16+
{name? : The environment to remove}
17+
{--force : Skip confirmation prompt}';
18+
19+
protected $description = 'Remove a custom environment';
20+
21+
protected function process()
22+
{
23+
$settings = Settings::load();
24+
$envs = $settings->envs();
25+
26+
$customEnvs = array_values(array_diff($envs, self::SYSTEM_ENVS));
27+
28+
if (empty($customEnvs)) {
29+
$this->info('No custom environments to remove.');
30+
$this->neutral('System environments (local, staging, production) cannot be removed.');
31+
32+
return self::SUCCESS;
33+
}
34+
35+
$envName = $this->argument('name') ?? select(
36+
label: 'Which environment do you want to remove?',
37+
options: $customEnvs
38+
);
39+
40+
if (in_array($envName, self::SYSTEM_ENVS)) {
41+
$this->error("Cannot remove system environment '{$envName}'.");
42+
43+
return self::FAILURE;
44+
}
45+
46+
if (! in_array($envName, $envs)) {
47+
$this->error("Environment '{$envName}' not found.");
48+
49+
return self::FAILURE;
50+
}
51+
52+
$this->line('Current environments: '.implode(', ', $envs));
53+
54+
if (! $this->option('force')) {
55+
$confirmed = confirm(
56+
label: "Are you sure you want to remove the '{$envName}' environment?",
57+
default: false,
58+
hint: 'This removes the environment from settings only — secrets in remote vaults are not affected'
59+
);
60+
61+
if (! $confirmed) {
62+
$this->neutral('Environment removal cancelled.');
63+
64+
return self::SUCCESS;
65+
}
66+
}
67+
68+
$settings->withEnvs(
69+
array_values(array_filter($envs, fn ($e) => $e !== $envName))
70+
)->save();
71+
72+
$this->newLine();
73+
$this->success("Environment <secret-name>{$envName}</secret-name> has been removed.");
74+
}
75+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace STS\Keep\Commands;
4+
5+
use STS\Keep\Facades\Keep;
6+
7+
use function Laravel\Prompts\confirm;
8+
use function Laravel\Prompts\select;
9+
use function Laravel\Prompts\table;
10+
11+
class VaultDeleteCommand extends BaseCommand
12+
{
13+
protected $signature = 'vault:delete
14+
{vault? : The vault slug to delete}
15+
{--force : Skip confirmation prompt}';
16+
17+
protected $description = 'Delete a vault configuration';
18+
19+
protected function process()
20+
{
21+
$configuredVaults = Keep::getConfiguredVaults();
22+
23+
if ($configuredVaults->isEmpty()) {
24+
$this->info('No vaults are configured.');
25+
26+
return self::SUCCESS;
27+
}
28+
29+
$slug = $this->argument('vault') ?? select(
30+
label: 'Which vault do you want to delete?',
31+
options: $configuredVaults->keys()->toArray()
32+
);
33+
34+
if (! $configuredVaults->has($slug)) {
35+
$this->error("Vault '{$slug}' not found.");
36+
37+
return self::FAILURE;
38+
}
39+
40+
$defaultVault = Keep::getDefaultVault();
41+
42+
if ($slug === $defaultVault) {
43+
$this->error('Cannot delete the default vault.');
44+
$this->line('Change the default vault first with: keep init');
45+
46+
return self::FAILURE;
47+
}
48+
49+
$config = $configuredVaults->get($slug);
50+
51+
$this->newLine();
52+
$this->line('Vault to be deleted:');
53+
table(['Slug', 'Name', 'Driver'], [
54+
[$slug, $config->name(), $config->driver()],
55+
]);
56+
57+
if (! $this->option('force')) {
58+
$confirmed = confirm(
59+
label: 'Are you sure you want to delete this vault configuration?',
60+
default: false,
61+
hint: 'This removes the local config only — secrets in the remote vault are not affected'
62+
);
63+
64+
if (! $confirmed) {
65+
$this->neutral('Vault deletion cancelled.');
66+
67+
return self::SUCCESS;
68+
}
69+
}
70+
71+
$vaultFile = getcwd().'/.keep/vaults/'.$slug.'.json';
72+
73+
if (! $this->filesystem->exists($vaultFile)) {
74+
$this->error("Vault configuration file not found: {$vaultFile}");
75+
76+
return self::FAILURE;
77+
}
78+
79+
$this->filesystem->delete($vaultFile);
80+
81+
$this->newLine();
82+
$this->success("Vault <secret-name>{$slug}</secret-name> configuration has been deleted.");
83+
}
84+
}

src/KeepApplication.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ public function __construct(protected KeepInstall $install)
3535

3636
Commands\VaultAddCommand::class,
3737
Commands\VaultEditCommand::class,
38+
Commands\VaultDeleteCommand::class,
3839
Commands\VaultListCommand::class,
3940

4041
Commands\EnvAddCommand::class,
42+
Commands\EnvRemoveCommand::class,
4143

4244
Commands\WorkspaceCommand::class,
4345

src/Server/Controllers/SecretController.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public function create(): array
4040

4141
try {
4242
$vault = $this->getVault();
43-
$vault->set($this->body['key'], $this->body['value']);
43+
$secure = ($this->body['secure'] ?? true) !== false;
44+
$vault->set($this->body['key'], $this->body['value'], $secure);
4445
} catch (\InvalidArgumentException $e) {
4546
return $this->error($e->getMessage());
4647
}
@@ -58,8 +59,9 @@ public function update(string $key): array
5859
}
5960

6061
$vault = $this->getVault();
61-
$vault->set(urldecode($key), $this->body['value']);
62-
62+
$secure = ($this->body['secure'] ?? true) !== false;
63+
$vault->set(urldecode($key), $this->body['value'], $secure);
64+
6365
return $this->success([
6466
'success' => true,
6567
'message' => "Secret '{$key}' updated in vault '{$this->body['vault']}' environment '{$this->body['env']}'"

src/Server/frontend/src/components/SecretDialog.vue

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,19 @@
3434
/>
3535
</div>
3636

37-
<div class="text-xs text-muted-foreground">
38-
Vault: <span class="font-medium">{{ vault }}</span> •
39-
Env: <span class="font-medium">{{ env }}</span>
37+
<div class="flex items-center justify-between">
38+
<div class="text-xs text-muted-foreground">
39+
Vault: <span class="font-medium">{{ vault }}</span> •
40+
Env: <span class="font-medium">{{ env }}</span>
41+
</div>
42+
<label class="flex items-center space-x-2 text-sm cursor-pointer">
43+
<input
44+
type="checkbox"
45+
v-model="form.secure"
46+
class="rounded border-border"
47+
/>
48+
<span class="text-muted-foreground">Encrypt</span>
49+
</label>
4050
</div>
4151

4252
<div v-if="saveError" class="p-3 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900 rounded-md">
@@ -84,7 +94,8 @@ const { createSecret, updateSecret } = useSecrets()
8494
8595
const form = reactive({
8696
key: '',
87-
value: ''
97+
value: '',
98+
secure: true
8899
})
89100
90101
const keyError = ref('')
@@ -153,10 +164,10 @@ async function save() {
153164
154165
try {
155166
if (props.secret) {
156-
await updateSecret(form.key, form.value, props.vault, props.env)
167+
await updateSecret(form.key, form.value, props.vault, props.env, form.secure)
157168
toast.success('Secret updated', `Secret '${form.key}' has been updated successfully`)
158169
} else {
159-
await createSecret(form.key, form.value, props.vault, props.env)
170+
await createSecret(form.key, form.value, props.vault, props.env, form.secure)
160171
toast.success('Secret created', `Secret '${form.key}' has been created successfully`)
161172
}
162173
emit('success')

src/Server/frontend/src/composables/useSecrets.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export function useSecrets() {
2222
}
2323
}
2424

25-
async function createSecret(key, value, vault, env) {
25+
async function createSecret(key, value, vault, env, secure = true) {
2626
loading.value = true
2727
error.value = null
2828
try {
29-
const response = await api.createSecret(key, value, vault, env)
29+
const response = await api.createSecret(key, value, vault, env, secure)
3030
// Don't call loadSecrets here - let the component handle refresh
3131
return response
3232
} catch (err) {
@@ -37,11 +37,11 @@ export function useSecrets() {
3737
}
3838
}
3939

40-
async function updateSecret(key, value, vault, env) {
40+
async function updateSecret(key, value, vault, env, secure = true) {
4141
loading.value = true
4242
error.value = null
4343
try {
44-
const response = await api.updateSecret(key, value, vault, env)
44+
const response = await api.updateSecret(key, value, vault, env, secure)
4545
// Don't call loadSecrets here - let the component handle refresh
4646
return response
4747
} catch (err) {

src/Server/frontend/src/services/api.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,17 @@ class ApiClient {
5757
return this.request(`/secrets/${encodeURIComponent(key)}?vault=${vault}&env=${env}&unmask=${unmask}`)
5858
}
5959

60-
async createSecret(key, value, vault, env) {
60+
async createSecret(key, value, vault, env, secure = true) {
6161
return this.request('/secrets', {
6262
method: 'POST',
63-
body: JSON.stringify({ key, value, vault, env })
63+
body: JSON.stringify({ key, value, vault, env, secure })
6464
})
6565
}
6666

67-
async updateSecret(key, value, vault, env) {
67+
async updateSecret(key, value, vault, env, secure = true) {
6868
return this.request(`/secrets/${encodeURIComponent(key)}`, {
6969
method: 'PUT',
70-
body: JSON.stringify({ value, vault, env })
70+
body: JSON.stringify({ value, vault, env, secure })
7171
})
7272
}
7373

src/Shell/ArgumentProcessor.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ArgumentProcessor
2121
],
2222
'history' => [
2323
'arguments' => ['key'],
24+
'flags' => ['unmask'],
2425
],
2526
'delete' => [
2627
'arguments' => ['key'],
@@ -31,9 +32,11 @@ class ArgumentProcessor
3132
'options' => [
3233
1 => '--to', // Second positional becomes --to option
3334
],
35+
'flags' => ['overwrite', 'dry-run'],
3436
],
3537
'import' => [
3638
'arguments' => ['from'],
39+
'flags' => ['overwrite', 'skip-existing', 'dry-run'],
3740
],
3841
'rename' => [
3942
'arguments' => ['old', 'new'],
@@ -50,10 +53,7 @@ class ArgumentProcessor
5053
'arguments' => ['name'],
5154
],
5255
'diff' => [
53-
'options' => [
54-
0 => '--from',
55-
1 => '--to',
56-
],
56+
'collect' => '--env',
5757
'flags' => ['unmask'],
5858
],
5959
'verify' => [
@@ -92,6 +92,11 @@ public static function process(string $command, array $positionals, array &$inpu
9292
if (isset($config['options'])) {
9393
self::processOptions($positionals, $config['options'], $input);
9494
}
95+
96+
// Collect remaining positionals into a single comma-separated option
97+
if (isset($config['collect'])) {
98+
self::processCollect($positionals, $config['collect'], $config['flags'] ?? [], $input);
99+
}
95100
}
96101

97102
/**
@@ -129,4 +134,16 @@ private static function processOptions(array $positionals, array $options, array
129134
}
130135
}
131136
}
137+
138+
/**
139+
* Collect all non-flag positionals into a single comma-separated option
140+
*/
141+
private static function processCollect(array $positionals, string $option, array $flags, array &$input): void
142+
{
143+
$values = array_filter($positionals, fn ($p) => !in_array($p, $flags));
144+
145+
if (!empty($values)) {
146+
$input[$option] = implode(',', $values);
147+
}
148+
}
132149
}

0 commit comments

Comments
 (0)