settingspec 0.4.0__tar.gz → 0.5.1__tar.gz

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.
Files changed (28) hide show
  1. {settingspec-0.4.0 → settingspec-0.5.1}/Cargo.lock +67 -1
  2. {settingspec-0.4.0 → settingspec-0.5.1}/Cargo.toml +2 -1
  3. {settingspec-0.4.0 → settingspec-0.5.1}/PKG-INFO +34 -11
  4. {settingspec-0.4.0 → settingspec-0.5.1}/README.md +33 -10
  5. {settingspec-0.4.0 → settingspec-0.5.1}/src/cli.rs +110 -35
  6. {settingspec-0.4.0 → settingspec-0.5.1}/LICENSE-APACHE +0 -0
  7. {settingspec-0.4.0 → settingspec-0.5.1}/LICENSE-MIT +0 -0
  8. {settingspec-0.4.0 → settingspec-0.5.1}/pyproject.toml +0 -0
  9. {settingspec-0.4.0 → settingspec-0.5.1}/src/envfile.rs +0 -0
  10. {settingspec-0.4.0 → settingspec-0.5.1}/src/error.rs +0 -0
  11. {settingspec-0.4.0 → settingspec-0.5.1}/src/filter.rs +0 -0
  12. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/common.rs +0 -0
  13. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/env.rs +0 -0
  14. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/javascript.rs +0 -0
  15. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/json.rs +0 -0
  16. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/lua.rs +0 -0
  17. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/mod.rs +0 -0
  18. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/python.rs +0 -0
  19. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/toml.rs +0 -0
  20. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/tree.rs +0 -0
  21. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/typescript.rs +0 -0
  22. {settingspec-0.4.0 → settingspec-0.5.1}/src/generator/yaml.rs +0 -0
  23. {settingspec-0.4.0 → settingspec-0.5.1}/src/lib.rs +0 -0
  24. {settingspec-0.4.0 → settingspec-0.5.1}/src/main.rs +0 -0
  25. {settingspec-0.4.0 → settingspec-0.5.1}/src/model.rs +0 -0
  26. {settingspec-0.4.0 → settingspec-0.5.1}/src/parser.rs +0 -0
  27. {settingspec-0.4.0 → settingspec-0.5.1}/src/profile.rs +0 -0
  28. {settingspec-0.4.0 → settingspec-0.5.1}/src/resolver.rs +0 -0
@@ -282,6 +282,15 @@ dependencies = [
282
282
  "generic-array",
283
283
  ]
284
284
 
285
+ [[package]]
286
+ name = "block2"
287
+ version = "0.6.2"
288
+ source = "registry+https://github.com/rust-lang/crates.io-index"
289
+ checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
290
+ dependencies = [
291
+ "objc2",
292
+ ]
293
+
285
294
  [[package]]
286
295
  name = "blowfish"
287
296
  version = "0.9.1"
@@ -324,6 +333,12 @@ version = "1.0.5"
324
333
  source = "registry+https://github.com/rust-lang/crates.io-index"
325
334
  checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
326
335
 
336
+ [[package]]
337
+ name = "cfg_aliases"
338
+ version = "0.2.2"
339
+ source = "registry+https://github.com/rust-lang/crates.io-index"
340
+ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
341
+
327
342
  [[package]]
328
343
  name = "chacha20"
329
344
  version = "0.9.1"
@@ -519,6 +534,17 @@ dependencies = [
519
534
  "cipher",
520
535
  ]
521
536
 
537
+ [[package]]
538
+ name = "ctrlc"
539
+ version = "3.5.2"
540
+ source = "registry+https://github.com/rust-lang/crates.io-index"
541
+ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
542
+ dependencies = [
543
+ "dispatch2",
544
+ "nix",
545
+ "windows-sys",
546
+ ]
547
+
522
548
  [[package]]
523
549
  name = "curve25519-dalek"
524
550
  version = "4.1.3"
@@ -584,6 +610,18 @@ dependencies = [
584
610
  "crypto-common 0.2.2",
585
611
  ]
586
612
 
613
+ [[package]]
614
+ name = "dispatch2"
615
+ version = "0.3.1"
616
+ source = "registry+https://github.com/rust-lang/crates.io-index"
617
+ checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
618
+ dependencies = [
619
+ "bitflags",
620
+ "block2",
621
+ "libc",
622
+ "objc2",
623
+ ]
624
+
587
625
  [[package]]
588
626
  name = "displaydoc"
589
627
  version = "0.2.7"
@@ -1180,6 +1218,18 @@ dependencies = [
1180
1218
  "sha3",
1181
1219
  ]
1182
1220
 
1221
+ [[package]]
1222
+ name = "nix"
1223
+ version = "0.31.3"
1224
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1225
+ checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
1226
+ dependencies = [
1227
+ "bitflags",
1228
+ "cfg-if",
1229
+ "cfg_aliases",
1230
+ "libc",
1231
+ ]
1232
+
1183
1233
  [[package]]
1184
1234
  name = "nom"
1185
1235
  version = "8.0.0"
@@ -1240,6 +1290,21 @@ dependencies = [
1240
1290
  "libm",
1241
1291
  ]
1242
1292
 
1293
+ [[package]]
1294
+ name = "objc2"
1295
+ version = "0.6.4"
1296
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1297
+ checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
1298
+ dependencies = [
1299
+ "objc2-encode",
1300
+ ]
1301
+
1302
+ [[package]]
1303
+ name = "objc2-encode"
1304
+ version = "4.1.0"
1305
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1306
+ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
1307
+
1243
1308
  [[package]]
1244
1309
  name = "once_cell"
1245
1310
  version = "1.21.4"
@@ -1758,7 +1823,7 @@ dependencies = [
1758
1823
 
1759
1824
  [[package]]
1760
1825
  name = "settingspec"
1761
- version = "0.4.0"
1826
+ version = "0.5.1"
1762
1827
  dependencies = [
1763
1828
  "age",
1764
1829
  "anyhow",
@@ -1766,6 +1831,7 @@ dependencies = [
1766
1831
  "assert_fs",
1767
1832
  "clap",
1768
1833
  "clap_complete",
1834
+ "ctrlc",
1769
1835
  "dotenvy",
1770
1836
  "indexmap",
1771
1837
  "predicates",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "settingspec"
3
- version = "0.4.0"
3
+ version = "0.5.1"
4
4
  edition = "2024"
5
5
  description = "One file for all environments, all languages, all submodules."
6
6
  readme = "README.md"
@@ -40,6 +40,7 @@ serde_yaml = "0.9"
40
40
  indexmap = { version = "2", features = ["serde"] }
41
41
  dotenvy = "0.15.7"
42
42
  age = { version = "0.12.1", features = ["armor", "ssh"] }
43
+ ctrlc = "3.5.2"
43
44
 
44
45
  [dev-dependencies]
45
46
  assert_fs = "1.1.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: settingspec
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -19,7 +19,7 @@ Project-URL: Source Code, https://github.com/sayanarijit/settingspec
19
19
  <picture>
20
20
  <source media="(prefers-color-scheme: dark)" srcset="https://s15.gifyu.com/images/buXVO.png">
21
21
  <source media="(prefers-color-scheme: light)" srcset="https://s15.gifyu.com/images/buXZJ.png">
22
- <img alt="The settingspec cover" width="600" src="https://s15.gifyu.com/images/buXZJ.png">
22
+ <img alt="The settingspec cover" width="100%" src="https://s15.gifyu.com/images/buXZJ.png">
23
23
  </picture>
24
24
  </p>
25
25
 
@@ -28,16 +28,17 @@ Project-URL: Source Code, https://github.com/sayanarijit/settingspec
28
28
  [![PyPI Version](https://img.shields.io/pypi/v/settingspec)](https://pypi.org/project/settingspec)
29
29
  [![NPM Version](https://img.shields.io/npm/v/%40sayanarijit%2Fsettingspec)](https://www.npmjs.com/package/@sayanarijit/settingspec)
30
30
 
31
- # Table of contents
31
+ # SettingSpec
32
32
 
33
- 1. [Concepts](#concepts)
34
- 1. [Single file convenience](#single-file-convenience)
35
- 2. [Declarative profiles as environments](#declarative-profiles-as-environments)
36
- 3. [Language independent](#language-independent)
37
- 4. [Easy secrets](#easy-secrets)
38
- 1. [SecretSpec](#secretspec)
39
- 2. [Dotenv](#dotenv)
40
- 2. [Learn more](#learn-more)
33
+ 1. [Concepts](#concepts)
34
+ 1. [Single file convenience](#single-file-convenience)
35
+ 2. [Declarative profiles as environments](#declarative-profiles-as-environments)
36
+ 3. [Language independent](#language-independent)
37
+ 4. [Easy secrets](#easy-secrets)
38
+ 1. [SecretSpec](#secretspec)
39
+ 2. [Dotenv](#dotenv)
40
+ 3. [Cottage or agebox](#cottage-or-agebox)
41
+ 2. [Learn more](#learn-more)
41
42
 
42
43
  ## Concepts
43
44
 
@@ -109,6 +110,12 @@ Now export the final settings as declared.
109
110
  settingspec export
110
111
  ```
111
112
 
113
+ Or keep watching to export when `settingspec.toml` changes.
114
+
115
+ ```bash
116
+ settingspec watch
117
+ ```
118
+
112
119
  Or directly run the program with exported settings.
113
120
 
114
121
  ```bash
@@ -182,6 +189,22 @@ envfile.prod = "-" # Read from stdin, encrypted or not
182
189
  secret1.default.env = "SECRET1" # Load value from $SECRET1
183
190
  ```
184
191
 
192
+ #### Cottage or agebox
193
+
194
+ Too simple? Use [Cottage](https://cottage-cli.pages.dev) or [agebox](https://github.com/slok/agebox).
195
+
196
+ ```toml
197
+ [spec]
198
+ envfile.default = ".env.cott.age"
199
+
200
+ # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
201
+ # decryption.key.env = "COTTAGE_IDENTITY"
202
+ decryption.key.path = "~/.config/cottage/identity:~/.ssh"
203
+
204
+ [settings]
205
+ secret.default.env = "SECRET"
206
+ ```
207
+
185
208
  ## Learn more
186
209
 
187
210
  Check out the [settingspec.toml](./settingspec.toml) example file and [full specification](./SPECIFICATION.md) for more details.
@@ -2,7 +2,7 @@
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="https://s15.gifyu.com/images/buXVO.png">
4
4
  <source media="(prefers-color-scheme: light)" srcset="https://s15.gifyu.com/images/buXZJ.png">
5
- <img alt="The settingspec cover" width="600" src="https://s15.gifyu.com/images/buXZJ.png">
5
+ <img alt="The settingspec cover" width="100%" src="https://s15.gifyu.com/images/buXZJ.png">
6
6
  </picture>
7
7
  </p>
8
8
 
@@ -11,16 +11,17 @@
11
11
  [![PyPI Version](https://img.shields.io/pypi/v/settingspec)](https://pypi.org/project/settingspec)
12
12
  [![NPM Version](https://img.shields.io/npm/v/%40sayanarijit%2Fsettingspec)](https://www.npmjs.com/package/@sayanarijit/settingspec)
13
13
 
14
- # Table of contents
14
+ # SettingSpec
15
15
 
16
- 1. [Concepts](#concepts)
17
- 1. [Single file convenience](#single-file-convenience)
18
- 2. [Declarative profiles as environments](#declarative-profiles-as-environments)
19
- 3. [Language independent](#language-independent)
20
- 4. [Easy secrets](#easy-secrets)
21
- 1. [SecretSpec](#secretspec)
22
- 2. [Dotenv](#dotenv)
23
- 2. [Learn more](#learn-more)
16
+ 1. [Concepts](#concepts)
17
+ 1. [Single file convenience](#single-file-convenience)
18
+ 2. [Declarative profiles as environments](#declarative-profiles-as-environments)
19
+ 3. [Language independent](#language-independent)
20
+ 4. [Easy secrets](#easy-secrets)
21
+ 1. [SecretSpec](#secretspec)
22
+ 2. [Dotenv](#dotenv)
23
+ 3. [Cottage or agebox](#cottage-or-agebox)
24
+ 2. [Learn more](#learn-more)
24
25
 
25
26
  ## Concepts
26
27
 
@@ -92,6 +93,12 @@ Now export the final settings as declared.
92
93
  settingspec export
93
94
  ```
94
95
 
96
+ Or keep watching to export when `settingspec.toml` changes.
97
+
98
+ ```bash
99
+ settingspec watch
100
+ ```
101
+
95
102
  Or directly run the program with exported settings.
96
103
 
97
104
  ```bash
@@ -165,6 +172,22 @@ envfile.prod = "-" # Read from stdin, encrypted or not
165
172
  secret1.default.env = "SECRET1" # Load value from $SECRET1
166
173
  ```
167
174
 
175
+ #### Cottage or agebox
176
+
177
+ Too simple? Use [Cottage](https://cottage-cli.pages.dev) or [agebox](https://github.com/slok/agebox).
178
+
179
+ ```toml
180
+ [spec]
181
+ envfile.default = ".env.cott.age"
182
+
183
+ # decryption.key.env = "AGEBOX_PRIVATE_KEYS"
184
+ # decryption.key.env = "COTTAGE_IDENTITY"
185
+ decryption.key.path = "~/.config/cottage/identity:~/.ssh"
186
+
187
+ [settings]
188
+ secret.default.env = "SECRET"
189
+ ```
190
+
168
191
  ## Learn more
169
192
 
170
193
  Check out the [settingspec.toml](./settingspec.toml) example file and [full specification](./SPECIFICATION.md) for more details.
@@ -7,8 +7,29 @@ use crate::parser::parse_config_str;
7
7
  use crate::profile::resolve_active_profile;
8
8
  use crate::resolver::resolve_settings;
9
9
  use clap::{Parser, Subcommand};
10
- use std::collections::HashMap;
10
+ use std::collections::{HashMap, HashSet};
11
+ use std::io::Write;
11
12
  use std::path::{Path, PathBuf};
13
+ use std::sync::mpsc;
14
+ use std::time::Duration;
15
+
16
+ #[derive(Default)]
17
+ struct CleanupGuard {
18
+ keep: bool,
19
+ paths: HashSet<PathBuf>,
20
+ }
21
+
22
+ impl Drop for CleanupGuard {
23
+ fn drop(&mut self) {
24
+ if !self.keep {
25
+ self.paths.iter().for_each(|path| {
26
+ if std::fs::exists(path).unwrap_or(false) {
27
+ let _ = std::fs::remove_file(path);
28
+ }
29
+ });
30
+ }
31
+ }
32
+ }
12
33
 
13
34
  #[derive(Parser, Debug)]
14
35
  #[command(
@@ -34,6 +55,8 @@ pub enum Commands {
34
55
  #[arg(last = true, required = true)]
35
56
  command: Vec<String>,
36
57
  },
58
+ /// Watches settingspec.toml and regenerates exports whenever it changes
59
+ Watch,
37
60
  }
38
61
 
39
62
  pub const INIT_CONFIG: &str = r#"[spec]
@@ -145,9 +168,83 @@ pub fn run_cli() -> Result<()> {
145
168
  let ctx = load_and_resolve()?;
146
169
  execute_run(&ctx, &command)
147
170
  }
171
+ Commands::Watch => run_watch(),
148
172
  }
149
173
  }
150
174
 
175
+ fn run_watch() -> Result<()> {
176
+ let ctx = load_and_resolve()?;
177
+ let config_path = ctx.project_root.join("settingspec.toml");
178
+ let mut guard = CleanupGuard::default();
179
+
180
+ let (tx, rx) = mpsc::channel();
181
+ let (txsig, rxsig) = mpsc::channel();
182
+
183
+ ctrlc::set_handler(move || txsig.send(()).expect("Could not send signal on channel."))
184
+ .expect("Error setting Ctrl-C handler");
185
+
186
+ std::thread::spawn(move || {
187
+ let _ = tx.send(true); // Trigger initial export
188
+
189
+ let mut modified = std::fs::metadata(&config_path)
190
+ .ok()
191
+ .and_then(|m| m.modified().ok());
192
+
193
+ loop {
194
+ std::thread::sleep(Duration::from_secs(1));
195
+ if rxsig.try_recv().is_ok() {
196
+ let _ = tx.send(false);
197
+ break;
198
+ } else {
199
+ let new_modified = std::fs::metadata(&config_path)
200
+ .ok()
201
+ .and_then(|m| m.modified().ok());
202
+ if new_modified != modified {
203
+ modified = new_modified;
204
+ let _ = tx.send(true);
205
+ }
206
+ }
207
+ }
208
+ });
209
+
210
+ while rx.recv().unwrap_or(false) {
211
+ match load_and_resolve() {
212
+ Ok(mut ctx) => {
213
+ guard.keep = ctx.doc.spec.export.keep;
214
+ if ctx.doc.spec.export.file.is_empty() {
215
+ eprintln!("No export files configured, skipping export.");
216
+ continue;
217
+ }
218
+ ctx.doc.spec.export.stdout = None;
219
+ ctx.doc.spec.export.stdin = None;
220
+ ctx.doc.spec.export.env = None;
221
+
222
+ eprintln!("Exporting files...");
223
+ if let Err(e) = perform_export(&ctx) {
224
+ eprintln!("Error during export: {}", e);
225
+ }
226
+ ctx.doc
227
+ .spec
228
+ .export
229
+ .file
230
+ .iter()
231
+ .for_each(|(file_path_str, filter)| {
232
+ if !matches!(filter, ExportFilter::Disabled) {
233
+ guard.paths.insert(PathBuf::from(file_path_str));
234
+ println!("{file_path_str}");
235
+ }
236
+ });
237
+ }
238
+ Err(e) => {
239
+ eprintln!("Error loading configuration: {}", e);
240
+ }
241
+ }
242
+ }
243
+
244
+ eprintln!("Exiting watch mode.");
245
+ Ok(())
246
+ }
247
+
151
248
  fn perform_export(ctx: &ExecutionContext) -> Result<Vec<PathBuf>> {
152
249
  auto_append_gitignore(ctx)?;
153
250
 
@@ -189,28 +286,10 @@ fn execute_run(ctx: &ExecutionContext, command_args: &[String]) -> Result<()> {
189
286
  }
190
287
 
191
288
  // Export all files
192
- let generated_files = perform_export(ctx)?;
193
-
194
- // Setup cleanup guard
195
- struct CleanupGuard {
196
- files: Vec<PathBuf>,
197
- keep: bool,
198
- }
199
-
200
- impl Drop for CleanupGuard {
201
- fn drop(&mut self) {
202
- if !self.keep {
203
- for f in &self.files {
204
- let _ = std::fs::remove_file(f);
205
- }
206
- }
207
- }
208
- }
209
-
210
- let _guard = CleanupGuard {
211
- files: generated_files,
289
+ let _guard = perform_export(ctx).map(|files| CleanupGuard {
212
290
  keep: ctx.doc.spec.export.keep,
213
- };
291
+ paths: files.into_iter().collect(),
292
+ })?;
214
293
 
215
294
  let stdin_payload = if let Some(ref stdin_fmt) = ctx.doc.spec.export.stdin {
216
295
  Some(generate_by_format(stdin_fmt, &ctx.resolved_settings)?)
@@ -250,7 +329,6 @@ fn execute_run(ctx: &ExecutionContext, command_args: &[String]) -> Result<()> {
250
329
  let child_stdin = child.stdin.take();
251
330
  let writer = std::thread::spawn(move || {
252
331
  if let Some(mut stdin) = child_stdin {
253
- use std::io::Write;
254
332
  let _ = stdin.write_all(payload.as_bytes());
255
333
  }
256
334
  });
@@ -372,16 +450,14 @@ pub fn auto_append_gitignore(ctx: &ExecutionContext) -> Result<()> {
372
450
  return Ok(());
373
451
  }
374
452
 
375
- let current_dir = &ctx.project_root;
376
- let git_root = match find_git_root(current_dir) {
377
- Some(root) => root,
378
- None => return Ok(()),
379
- };
453
+ if find_git_root(&ctx.project_root).is_none() {
454
+ return Ok(());
455
+ }
380
456
 
381
- let git_root_canon = git_root.canonicalize().unwrap_or_else(|_| git_root.clone());
382
- let current_dir_canon = current_dir
457
+ let project_root = &ctx.project_root;
458
+ let project_root_canon = project_root
383
459
  .canonicalize()
384
- .unwrap_or_else(|_| current_dir.clone());
460
+ .unwrap_or_else(|_| project_root.clone());
385
461
 
386
462
  let mut entries_to_add = Vec::new();
387
463
 
@@ -392,13 +468,13 @@ pub fn auto_append_gitignore(ctx: &ExecutionContext) -> Result<()> {
392
468
 
393
469
  let target_path = Path::new(file_path_str);
394
470
  let full_target = if target_path.is_relative() {
395
- current_dir_canon.join(target_path)
471
+ project_root_canon.join(target_path)
396
472
  } else {
397
473
  target_path.to_path_buf()
398
474
  };
399
475
 
400
476
  let normalized = normalize_path(&full_target);
401
- let rel_path = match normalized.strip_prefix(&git_root_canon) {
477
+ let rel_path = match normalized.strip_prefix(&project_root_canon) {
402
478
  Ok(rel) => rel,
403
479
  Err(_) => continue,
404
480
  };
@@ -422,7 +498,7 @@ pub fn auto_append_gitignore(ctx: &ExecutionContext) -> Result<()> {
422
498
  return Ok(());
423
499
  }
424
500
 
425
- let gitignore_path = git_root.join(".gitignore");
501
+ let gitignore_path = project_root.join(".gitignore");
426
502
  let existing_content = if gitignore_path.exists() {
427
503
  std::fs::read_to_string(&gitignore_path)?
428
504
  } else {
@@ -449,7 +525,6 @@ pub fn auto_append_gitignore(ctx: &ExecutionContext) -> Result<()> {
449
525
  to_append.push('\n');
450
526
  }
451
527
 
452
- use std::io::Write;
453
528
  let mut file = std::fs::OpenOptions::new()
454
529
  .create(true)
455
530
  .append(true)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes