lsh-framework 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -317,6 +317,35 @@ lsh --version
317
317
  lsh self version
318
318
  ```
319
319
 
320
+ ### Shell Completion (Optional but Recommended)
321
+
322
+ **New in v1.3.2:** Enable intelligent Tab completion for bash/zsh!
323
+
324
+ **Bash:**
325
+ ```bash
326
+ # Add to ~/.bashrc
327
+ echo 'source <(lsh completion bash)' >> ~/.bashrc
328
+ source ~/.bashrc
329
+ ```
330
+
331
+ **Zsh:**
332
+ ```bash
333
+ # Quick setup
334
+ mkdir -p ~/.zsh/completions
335
+ lsh completion zsh > ~/.zsh/completions/_lsh
336
+ echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
337
+ echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
338
+ source ~/.zshrc
339
+ ```
340
+
341
+ Now you can use Tab to:
342
+ - Complete command names: `lsh pu<Tab>` → `lsh push`
343
+ - Discover options: `lsh push <Tab>` → `-f --file -e --env --force -h --help`
344
+ - Complete environments: `lsh push --env <Tab>` → `dev staging production`
345
+ - Complete formats: `lsh list --format <Tab>` → `env json yaml toml export`
346
+
347
+ See [Shell Completion Guide](docs/features/SHELL_COMPLETION.md) for more details.
348
+
320
349
  ### Initial Setup
321
350
 
322
351
  ```bash
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { Command } from 'commander';
7
7
  import selfCommand from './commands/self.js';
8
8
  import { registerInitCommands } from './commands/init.js';
9
9
  import { registerDoctorCommands } from './commands/doctor.js';
10
+ import { registerCompletionCommands } from './commands/completion.js';
10
11
  import { init_daemon } from './services/daemon/daemon.js';
11
12
  import { init_supabase } from './services/supabase/supabase.js';
12
13
  import { init_cron } from './services/cron/cron.js';
@@ -141,6 +142,8 @@ function findSimilarCommands(input, validCommands) {
141
142
  await init_supabase(program);
142
143
  await init_daemon(program);
143
144
  await init_cron(program);
145
+ // Shell completion
146
+ registerCompletionCommands(program);
144
147
  // Self-management commands
145
148
  program.addCommand(selfCommand);
146
149
  // Pre-parse check for unknown commands
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Shell Completion Commands
3
+ * Generate shell completion scripts for bash and zsh
4
+ */
5
+ /**
6
+ * Generate bash completion script
7
+ */
8
+ function generateBashCompletion() {
9
+ return `# lsh bash completion script
10
+ # Source this file or add to ~/.bashrc:
11
+ # source <(lsh completion bash)
12
+ # Or save to completion directory:
13
+ # lsh completion bash > /etc/bash_completion.d/lsh
14
+
15
+ _lsh_completion() {
16
+ local cur prev words cword
17
+ _init_completion || return
18
+
19
+ local commands="help init doctor push pull list ls env key create sync status info get set delete supabase daemon cron self completion"
20
+ local global_opts="-V --version -v --verbose -d --debug -h --help"
21
+
22
+ # If we're completing the first word (command)
23
+ if [ $` + `{cword} -eq 1 ]; then
24
+ COMPREPLY=( $` + `(compgen -W "$` + `{commands} $` + `{global_opts}" -- "$` + `{cur}") )
25
+ return 0
26
+ fi
27
+
28
+ # Get the command (first argument)
29
+ local command="$` + `{words[1]}"
30
+
31
+ case "$` + `{command}" in
32
+ get)
33
+ case "$` + `{prev}" in
34
+ -f|--file)
35
+ COMPREPLY=( $` + `(compgen -f -- "$` + `{cur}") )
36
+ return 0
37
+ ;;
38
+ --format)
39
+ COMPREPLY=( $(compgen -W "env json yaml toml export" -- "$cur") )
40
+ return 0
41
+ ;;
42
+ *)
43
+ local opts="-f --file --all --export --format --exact -h --help"
44
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
45
+ return 0
46
+ ;;
47
+ esac
48
+ ;;
49
+ set)
50
+ case "$prev" in
51
+ -f|--file)
52
+ COMPREPLY=( $(compgen -f -- "$cur") )
53
+ return 0
54
+ ;;
55
+ *)
56
+ local opts="-f --file --stdin -h --help"
57
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
58
+ return 0
59
+ ;;
60
+ esac
61
+ ;;
62
+ push|pull)
63
+ case "$prev" in
64
+ -f|--file)
65
+ COMPREPLY=( $(compgen -f -- "$cur") )
66
+ return 0
67
+ ;;
68
+ -e|--env)
69
+ COMPREPLY=( $(compgen -W "dev development staging production prod test" -- "$cur") )
70
+ return 0
71
+ ;;
72
+ *)
73
+ local opts="-f --file -e --env --force -h --help"
74
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
75
+ return 0
76
+ ;;
77
+ esac
78
+ ;;
79
+ sync)
80
+ case "$prev" in
81
+ -f|--file)
82
+ COMPREPLY=( $(compgen -f -- "$cur") )
83
+ return 0
84
+ ;;
85
+ -e|--env)
86
+ COMPREPLY=( $(compgen -W "dev development staging production prod test" -- "$cur") )
87
+ return 0
88
+ ;;
89
+ *)
90
+ local opts="-f --file -e --env --dry-run --legacy --load --force -h --help"
91
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
92
+ return 0
93
+ ;;
94
+ esac
95
+ ;;
96
+ list|ls)
97
+ case "$prev" in
98
+ -f|--file)
99
+ COMPREPLY=( $(compgen -f -- "$cur") )
100
+ return 0
101
+ ;;
102
+ --format)
103
+ COMPREPLY=( $(compgen -W "env json yaml toml export" -- "$cur") )
104
+ return 0
105
+ ;;
106
+ *)
107
+ local opts="-f --file --keys-only --format --no-mask -h --help"
108
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
109
+ return 0
110
+ ;;
111
+ esac
112
+ ;;
113
+ env)
114
+ case "$prev" in
115
+ --format)
116
+ COMPREPLY=( $(compgen -W "env json yaml toml export" -- "$cur") )
117
+ return 0
118
+ ;;
119
+ env)
120
+ COMPREPLY=( $(compgen -W "dev development staging production prod test" -- "$cur") )
121
+ return 0
122
+ ;;
123
+ *)
124
+ local opts="--all-files --format -h --help"
125
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
126
+ return 0
127
+ ;;
128
+ esac
129
+ ;;
130
+ key)
131
+ local opts="--export -h --help"
132
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
133
+ return 0
134
+ ;;
135
+ create)
136
+ case "$prev" in
137
+ -f|--file)
138
+ COMPREPLY=( $(compgen -f -- "$cur") )
139
+ return 0
140
+ ;;
141
+ *)
142
+ local opts="-f --file -t --template -h --help"
143
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
144
+ return 0
145
+ ;;
146
+ esac
147
+ ;;
148
+ delete)
149
+ case "$prev" in
150
+ -f|--file)
151
+ COMPREPLY=( $(compgen -f -- "$cur") )
152
+ return 0
153
+ ;;
154
+ *)
155
+ local opts="-f --file -y --yes -h --help"
156
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
157
+ return 0
158
+ ;;
159
+ esac
160
+ ;;
161
+ status|info)
162
+ case "$prev" in
163
+ -f|--file)
164
+ COMPREPLY=( $(compgen -f -- "$cur") )
165
+ return 0
166
+ ;;
167
+ -e|--env)
168
+ COMPREPLY=( $(compgen -W "dev development staging production prod test" -- "$cur") )
169
+ return 0
170
+ ;;
171
+ *)
172
+ local opts="-f --file -e --env -h --help"
173
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
174
+ return 0
175
+ ;;
176
+ esac
177
+ ;;
178
+ init|doctor)
179
+ local opts="-h --help"
180
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
181
+ return 0
182
+ ;;
183
+ completion)
184
+ local opts="bash zsh -h --help"
185
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
186
+ return 0
187
+ ;;
188
+ supabase|daemon|cron|self)
189
+ # These have subcommands, just complete help for now
190
+ local opts="-h --help"
191
+ COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
192
+ return 0
193
+ ;;
194
+ *)
195
+ # Default: complete global options
196
+ COMPREPLY=( $(compgen -W "$global_opts" -- "$cur") )
197
+ return 0
198
+ ;;
199
+ esac
200
+ }
201
+
202
+ complete -F _lsh_completion lsh
203
+ `;
204
+ }
205
+ /**
206
+ * Generate zsh completion script
207
+ */
208
+ function generateZshCompletion() {
209
+ return `#compdef lsh
210
+ # lsh zsh completion script
211
+ # Install to: ~/.zsh/completions/_lsh
212
+ # Or source directly:
213
+ # source <(lsh completion zsh)
214
+ # Make sure ~/.zsh/completions is in your fpath:
215
+ # fpath=(~/.zsh/completions $fpath)
216
+ # autoload -Uz compinit && compinit
217
+
218
+ _lsh() {
219
+ local -a commands
220
+ local -a global_opts
221
+ local state line
222
+
223
+ global_opts=(
224
+ '(-V --version)'{-V,--version}'[Output version number]'
225
+ '(-v --verbose)'{-v,--verbose}'[Verbose output]'
226
+ '(-d --debug)'{-d,--debug}'[Debug mode]'
227
+ '(-h --help)'{-h,--help}'[Display help]'
228
+ )
229
+
230
+ commands=(
231
+ 'help:Show detailed help'
232
+ 'init:Interactive setup wizard'
233
+ 'doctor:Health check and troubleshooting'
234
+ 'push:Push local .env to encrypted cloud storage'
235
+ 'pull:Pull .env from encrypted cloud storage'
236
+ 'list:List secrets in the current local .env file'
237
+ 'ls:List secrets (alias for list)'
238
+ 'env:List all stored environments'
239
+ 'key:Generate a new encryption key'
240
+ 'create:Create a new .env file'
241
+ 'sync:Automatically set up and synchronize secrets'
242
+ 'status:Get detailed secrets status'
243
+ 'info:Show current directory context'
244
+ 'get:Get a specific secret value'
245
+ 'set:Set a specific secret value'
246
+ 'delete:Delete .env file'
247
+ 'completion:Generate shell completion scripts'
248
+ 'supabase:Supabase database management'
249
+ 'daemon:LSH daemon management'
250
+ 'cron:Cron job management'
251
+ 'self:Manage and update LSH'
252
+ )
253
+
254
+ _arguments -C \
255
+ $global_opts \
256
+ '1: :->command' \
257
+ '*:: :->args'
258
+
259
+ case $state in
260
+ command)
261
+ _describe -t commands 'lsh commands' commands
262
+ ;;
263
+ args)
264
+ case $line[1] in
265
+ get)
266
+ _arguments \
267
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
268
+ '--all[Get all secrets]' \
269
+ '--export[Output in export format]' \
270
+ '--format[Output format]:format:(env json yaml toml export)' \
271
+ '--exact[Require exact key match]' \
272
+ '(-h --help)'{-h,--help}'[Display help]' \
273
+ '1:key:'
274
+ ;;
275
+ set)
276
+ _arguments \
277
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
278
+ '--stdin[Read from stdin]' \
279
+ '(-h --help)'{-h,--help}'[Display help]' \
280
+ '1:key:' \
281
+ '2:value:'
282
+ ;;
283
+ push|pull)
284
+ _arguments \
285
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
286
+ '(-e --env)'{-e,--env}'[Environment name]:environment:(dev development staging production prod test)' \
287
+ '--force[Force operation]' \
288
+ '(-h --help)'{-h,--help}'[Display help]'
289
+ ;;
290
+ sync)
291
+ _arguments \
292
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
293
+ '(-e --env)'{-e,--env}'[Environment name]:environment:(dev development staging production prod test)' \
294
+ '--dry-run[Show what would be done]' \
295
+ '--legacy[Use legacy sync mode]' \
296
+ '--load[Output eval-able export commands]' \
297
+ '--force[Force sync]' \
298
+ '(-h --help)'{-h,--help}'[Display help]'
299
+ ;;
300
+ list|ls)
301
+ _arguments \
302
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
303
+ '--keys-only[Show only keys]' \
304
+ '--format[Output format]:format:(env json yaml toml export)' \
305
+ '--no-mask[Show full values]' \
306
+ '(-h --help)'{-h,--help}'[Display help]'
307
+ ;;
308
+ env)
309
+ _arguments \
310
+ '--all-files[List all tracked files]' \
311
+ '--format[Output format]:format:(env json yaml toml export)' \
312
+ '(-h --help)'{-h,--help}'[Display help]' \
313
+ '1:environment:(dev development staging production prod test)'
314
+ ;;
315
+ key)
316
+ _arguments \
317
+ '--export[Output in export format]' \
318
+ '(-h --help)'{-h,--help}'[Display help]'
319
+ ;;
320
+ create)
321
+ _arguments \
322
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
323
+ '(-t --template)'{-t,--template}'[Create with template]' \
324
+ '(-h --help)'{-h,--help}'[Display help]'
325
+ ;;
326
+ delete)
327
+ _arguments \
328
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
329
+ '(-y --yes)'{-y,--yes}'[Skip confirmation]' \
330
+ '(-h --help)'{-h,--help}'[Display help]'
331
+ ;;
332
+ status|info)
333
+ _arguments \
334
+ '(-f --file)'{-f,--file}'[Path to .env file]:file:_files' \
335
+ '(-e --env)'{-e,--env}'[Environment name]:environment:(dev development staging production prod test)' \
336
+ '(-h --help)'{-h,--help}'[Display help]'
337
+ ;;
338
+ completion)
339
+ _arguments \
340
+ '(-h --help)'{-h,--help}'[Display help]' \
341
+ '1:shell:(bash zsh)'
342
+ ;;
343
+ init|doctor|help)
344
+ _arguments \
345
+ '(-h --help)'{-h,--help}'[Display help]'
346
+ ;;
347
+ esac
348
+ ;;
349
+ esac
350
+ }
351
+
352
+ _lsh "$@"
353
+ `;
354
+ }
355
+ /**
356
+ * Register completion commands
357
+ */
358
+ export function registerCompletionCommands(program) {
359
+ program
360
+ .command('completion <shell>')
361
+ .description('Generate shell completion script (bash or zsh)')
362
+ .action((shell) => {
363
+ const shellLower = shell.toLowerCase();
364
+ if (shellLower === 'bash') {
365
+ console.log(generateBashCompletion());
366
+ }
367
+ else if (shellLower === 'zsh') {
368
+ console.log(generateZshCompletion());
369
+ }
370
+ else {
371
+ console.error(`❌ Unknown shell: ${shell}`);
372
+ console.error('Supported shells: bash, zsh');
373
+ console.error('');
374
+ console.error('Usage:');
375
+ console.error(' lsh completion bash > ~/.lsh-completion.bash');
376
+ console.error(' lsh completion zsh > ~/.zsh/completions/_lsh');
377
+ process.exit(1);
378
+ }
379
+ });
380
+ }
381
+ export default registerCompletionCommands;
@@ -543,6 +543,42 @@ API_KEY=
543
543
  process.exit(1);
544
544
  }
545
545
  });
546
+ /**
547
+ * Detect if file should use 'export' prefix based on file type
548
+ */
549
+ function shouldUseExport(filePath) {
550
+ const filename = path.basename(filePath);
551
+ const ext = path.extname(filePath);
552
+ // Shell script files - use export
553
+ if (['.sh', '.bash', '.zsh'].includes(ext)) {
554
+ return true;
555
+ }
556
+ // Shell profile/rc files - use export
557
+ if (['.bashrc', '.zshrc', '.profile', '.bash_profile', '.zprofile'].includes(filename)) {
558
+ return true;
559
+ }
560
+ // .envrc files (direnv) - use export
561
+ if (filename === '.envrc' || filename.endsWith('.envrc')) {
562
+ return true;
563
+ }
564
+ // .env files - do NOT use export
565
+ if (filename === '.env' || filename.startsWith('.env.')) {
566
+ return false;
567
+ }
568
+ // Default: no export (safest for most env files)
569
+ return false;
570
+ }
571
+ /**
572
+ * Format a line based on file type
573
+ */
574
+ function formatEnvLine(key, value, filePath) {
575
+ const needsQuotes = /[\s#]/.test(value);
576
+ const quotedValue = needsQuotes ? `"${value}"` : value;
577
+ const useExport = shouldUseExport(filePath);
578
+ return useExport
579
+ ? `export ${key}=${quotedValue}`
580
+ : `${key}=${quotedValue}`;
581
+ }
546
582
  /**
547
583
  * Set a single secret value
548
584
  */
@@ -563,12 +599,11 @@ API_KEY=
563
599
  newLines.push(line);
564
600
  continue;
565
601
  }
566
- const match = line.match(/^([^=]+)=(.*)$/);
602
+ // Match both with and without export
603
+ const match = line.match(/^(?:export\s+)?([^=]+)=(.*)$/);
567
604
  if (match && match[1].trim() === key) {
568
- // Quote values with spaces or special characters
569
- const needsQuotes = /[\s#]/.test(value);
570
- const quotedValue = needsQuotes ? `"${value}"` : value;
571
- newLines.push(`${key}=${quotedValue}`);
605
+ // Use appropriate format for this file type
606
+ newLines.push(formatEnvLine(key, value, envPath));
572
607
  found = true;
573
608
  }
574
609
  else {
@@ -579,9 +614,8 @@ API_KEY=
579
614
  }
580
615
  // If key wasn't found, append it
581
616
  if (!found) {
582
- const needsQuotes = /[\s#]/.test(value);
583
- const quotedValue = needsQuotes ? `"${value}"` : value;
584
- content = content.trimRight() + `\n${key}=${quotedValue}\n`;
617
+ const formattedLine = formatEnvLine(key, value, envPath);
618
+ content = content.trimRight() + `\n${formattedLine}\n`;
585
619
  }
586
620
  fs.writeFileSync(envPath, content, 'utf8');
587
621
  console.log(`✅ Set ${key}`);
@@ -638,8 +672,8 @@ API_KEY=
638
672
  // Skip comments and empty lines
639
673
  if (trimmed.startsWith('#') || !trimmed)
640
674
  continue;
641
- // Parse KEY=VALUE format
642
- const match = trimmed.match(/^([^=]+)=(.*)$/);
675
+ // Parse KEY=VALUE format (with or without export)
676
+ const match = trimmed.match(/^(?:export\s+)?([^=]+)=(.*)$/);
643
677
  if (!match) {
644
678
  errors.push(`Invalid format: ${trimmed}`);
645
679
  continue;
@@ -671,15 +705,14 @@ API_KEY=
671
705
  newLines.push(line);
672
706
  continue;
673
707
  }
674
- const match = line.match(/^([^=]+)=(.*)$/);
708
+ // Match both with and without export
709
+ const match = line.match(/^(?:export\s+)?([^=]+)=(.*)$/);
675
710
  if (match) {
676
711
  const key = match[1].trim();
677
712
  if (newKeys.has(key)) {
678
- // Update existing key
713
+ // Update existing key with appropriate format
679
714
  const value = newKeys.get(key);
680
- const needsQuotes = /[\s#]/.test(value);
681
- const quotedValue = needsQuotes ? `"${value}"` : value;
682
- newLines.push(`${key}=${quotedValue}`);
715
+ newLines.push(formatEnvLine(key, value, envPath));
683
716
  newKeys.delete(key); // Mark as processed
684
717
  hasContent = true;
685
718
  }
@@ -696,13 +729,12 @@ API_KEY=
696
729
  }
697
730
  // Add new keys that weren't in the existing file
698
731
  for (const [key, value] of newKeys.entries()) {
699
- const needsQuotes = /[\s#]/.test(value);
700
- const quotedValue = needsQuotes ? `"${value}"` : value;
732
+ const formattedLine = formatEnvLine(key, value, envPath);
701
733
  if (hasContent) {
702
- newLines.push(`${key}=${quotedValue}`);
734
+ newLines.push(formattedLine);
703
735
  }
704
736
  else {
705
- newLines.push(`${key}=${quotedValue}`);
737
+ newLines.push(formattedLine);
706
738
  hasContent = true;
707
739
  }
708
740
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsh-framework",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Simple, cross-platform encrypted secrets manager with automatic sync and multi-environment support. Just run lsh sync and start managing your secrets.",
5
5
  "main": "dist/app.js",
6
6
  "bin": {