redeploy 0.2.17__tar.gz → 0.2.18__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.
- {redeploy-0.2.17 → redeploy-0.2.18}/PKG-INFO +108 -6
- {redeploy-0.2.17 → redeploy-0.2.18}/README.md +107 -5
- {redeploy-0.2.17 → redeploy-0.2.18}/pyproject.toml +1 -1
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/__init__.py +1 -1
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/apply/executor.py +18 -5
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/cli.py +323 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/markpact/__init__.py +11 -1
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/markpact/compiler.py +7 -2
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/markpact/models.py +3 -1
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/markpact/parser.py +111 -4
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/models.py +1 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_executor.py +56 -0
- redeploy-0.2.18/redeploy/tests/test_markpact_parser.py +93 -0
- redeploy-0.2.18/redeploy/tests/test_resume.py +271 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy.egg-info/PKG-INFO +108 -6
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy.egg-info/SOURCES.txt +1 -1
- redeploy-0.2.17/redeploy/checkpoint.py +0 -234
- redeploy-0.2.17/redeploy/tests/test_markpact_parser.py +0 -41
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/apply/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/apply/state.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/audit.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/data_sync.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/detect/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/detect/detector.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/detect/probes.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/detect/remote.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/detect/templates.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/detect/workflow.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/discovery.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl/loader.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl/parser.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/context.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/decorators.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/docker_steps.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/exceptions.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/runner.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/dsl_python/steps.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/fleet.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/iac/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/iac/base.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/iac/docker_compose.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/iac/parsers/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/iac/parsers/compose.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/iac/registry.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/observe.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/parse.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/patterns.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plan/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plan/planner.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plugins/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plugins/builtin/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plugins/builtin/browser_reload.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plugins/builtin/notify.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/plugins/builtin/systemd_reload.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/spec_loader.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/ssh.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/steps.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_audit.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_cli.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_data_sync.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_discovery.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_dsl.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_examples.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_fleet.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_git_integration.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_iac.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_manifest.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_markpact_compiler.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_models.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_observe.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_parse.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_patterns.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_planner_edge.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_plugins.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_probes.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_public_api.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_spec_loader.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_ssh.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_templates.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_verify.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_version.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_version_changelog.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_version_cli.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_version_manifest.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_version_sources.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/tests/test_workflow.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/verify.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/bump.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/changelog.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/commits.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/diff.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/git_integration.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/git_transaction.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/manifest.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/__init__.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/base.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/json_.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/plain.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/regex.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/toml_.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/sources/yaml_.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version/transaction.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy/version.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy.egg-info/dependency_links.txt +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy.egg-info/entry_points.txt +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy.egg-info/requires.txt +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/redeploy.egg-info/top_level.txt +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/setup.cfg +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/tests/test_iac.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/tests/test_parse.py +0 -0
- {redeploy-0.2.17 → redeploy-0.2.18}/tests/test_redeploy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redeploy
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
4
4
|
Summary: Infrastructure migration toolkit: detect → plan → apply
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -27,17 +27,17 @@ Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
|
27
27
|
|
|
28
28
|
## AI Cost Tracking
|
|
29
29
|
|
|
30
|
-
    
|
|
31
|
+
  
|
|
32
32
|
|
|
33
|
-
- 🤖 **LLM usage:** $5.
|
|
34
|
-
- 👤 **Human dev:** ~$
|
|
33
|
+
- 🤖 **LLM usage:** $5.5500 (37 commits)
|
|
34
|
+
- 👤 **Human dev:** ~$1255 (12.6h @ $100/h, 30min dedup)
|
|
35
35
|
|
|
36
36
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
    
|
|
41
41
|
|
|
42
42
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
43
43
|
|
|
@@ -428,6 +428,108 @@ extra_steps:
|
|
|
428
428
|
|
|
429
429
|
The script is base64-encoded and executed via SSH with automatic temp file cleanup. Use `command` field for multiline script content (YAML `|` preserves newlines).
|
|
430
430
|
|
|
431
|
+
### Script References (`command_ref`)
|
|
432
|
+
|
|
433
|
+
Instead of duplicating scripts in YAML, reference a script defined in a markdown codeblock:
|
|
434
|
+
|
|
435
|
+
```yaml
|
|
436
|
+
extra_steps:
|
|
437
|
+
- id: configure_kiosk
|
|
438
|
+
action: inline_script
|
|
439
|
+
description: "Execute kiosk script from markdown"
|
|
440
|
+
command_ref: "#kiosk-browser-configuration-script"
|
|
441
|
+
risk: medium
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
In your migration markdown file, define the script in a section:
|
|
445
|
+
|
|
446
|
+
```markdown
|
|
447
|
+
## Kiosk Browser Configuration Script
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
#!/bin/bash
|
|
451
|
+
# Auto-detect browser...
|
|
452
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
453
|
+
chromium-browser --kiosk http://localhost:8100
|
|
454
|
+
fi
|
|
455
|
+
```
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Benefits:**
|
|
459
|
+
- Single source of truth — script lives in one place (markdown codeblock)
|
|
460
|
+
- No duplication between markdown documentation and YAML
|
|
461
|
+
- Easy to read and maintain
|
|
462
|
+
- Changes to the codeblock automatically apply to the deployment
|
|
463
|
+
|
|
464
|
+
**Reference formats:**
|
|
465
|
+
- `"#section-id"` — script from section in current spec file
|
|
466
|
+
- `"./file.md#section-id"` — script from section in specific file
|
|
467
|
+
|
|
468
|
+
The section ID is derived from the heading: spaces become hyphens, lowercase.
|
|
469
|
+
Example: `## Kiosk Browser Configuration Script` → `#kiosk-browser-configuration-script`
|
|
470
|
+
|
|
471
|
+
### Execute Script by Reference (`redeploy exec`)
|
|
472
|
+
|
|
473
|
+
Run a single script from markdown without running the full migration:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# Execute script from codeblock on remote host
|
|
477
|
+
redeploy exec '#kiosk-browser-configuration-script' \
|
|
478
|
+
--host pi@192.168.188.108 \
|
|
479
|
+
--file migration.podman-rpi5-resume.md
|
|
480
|
+
|
|
481
|
+
# With file in reference
|
|
482
|
+
redeploy exec './migration.md#install-deps' --host root@server.com
|
|
483
|
+
|
|
484
|
+
# Using markpact:ref (more explicit)
|
|
485
|
+
redeploy exec 'kiosk-script-id' --host pi@192.168.188.108 --file migration.md
|
|
486
|
+
|
|
487
|
+
# Dry-run to preview script
|
|
488
|
+
redeploy exec '#backup-script' --host pi@192.168.188.108 --file ops.md --dry-run
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
This is useful for:
|
|
492
|
+
- One-off operations defined in markdown docs
|
|
493
|
+
- Testing individual scripts before full migration
|
|
494
|
+
- Running maintenance tasks
|
|
495
|
+
|
|
496
|
+
### Execute Multiple Scripts (`redeploy exec-multi`)
|
|
497
|
+
|
|
498
|
+
Test multiple scripts at once:
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
# Execute multiple scripts by ref
|
|
502
|
+
redeploy exec-multi 'kiosk-script,install-deps,cleanup' \
|
|
503
|
+
--host pi@192.168.188.108 \
|
|
504
|
+
--file migration.md
|
|
505
|
+
|
|
506
|
+
# Mix of markpact:ref and section headings
|
|
507
|
+
redeploy exec-multi 'script1,#section2,script3' \
|
|
508
|
+
--host root@server.com \
|
|
509
|
+
--file deploy.md \
|
|
510
|
+
--dry-run
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Marking Codeblocks with `markpact:ref`
|
|
514
|
+
|
|
515
|
+
For more explicit script identification, use `markpact:ref <id>` in codeblock:
|
|
516
|
+
|
|
517
|
+
```markdown
|
|
518
|
+
```bash markpact:ref kiosk-browser-configuration-script
|
|
519
|
+
#!/bin/bash
|
|
520
|
+
# Auto-detect browser...
|
|
521
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
522
|
+
chromium-browser --kiosk http://localhost:8100
|
|
523
|
+
fi
|
|
524
|
+
```
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Benefits of `markpact:ref`:
|
|
528
|
+
- Explicit ID assignment (not derived from heading)
|
|
529
|
+
- Multiple scripts per section
|
|
530
|
+
- Can reference by simple ID instead of full heading
|
|
531
|
+
- Self-documenting in markdown
|
|
532
|
+
|
|
431
533
|
|
|
432
534
|
|
|
433
535
|
Place in project root — `redeploy run` (no args) uses it automatically.
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $5.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $5.5500 (37 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1255 (12.6h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-21 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
Infrastructure migration and device deploy toolkit — VPS, Raspberry Pi kiosk, Podman Quadlet, k3s.
|
|
19
19
|
|
|
@@ -404,6 +404,108 @@ extra_steps:
|
|
|
404
404
|
|
|
405
405
|
The script is base64-encoded and executed via SSH with automatic temp file cleanup. Use `command` field for multiline script content (YAML `|` preserves newlines).
|
|
406
406
|
|
|
407
|
+
### Script References (`command_ref`)
|
|
408
|
+
|
|
409
|
+
Instead of duplicating scripts in YAML, reference a script defined in a markdown codeblock:
|
|
410
|
+
|
|
411
|
+
```yaml
|
|
412
|
+
extra_steps:
|
|
413
|
+
- id: configure_kiosk
|
|
414
|
+
action: inline_script
|
|
415
|
+
description: "Execute kiosk script from markdown"
|
|
416
|
+
command_ref: "#kiosk-browser-configuration-script"
|
|
417
|
+
risk: medium
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
In your migration markdown file, define the script in a section:
|
|
421
|
+
|
|
422
|
+
```markdown
|
|
423
|
+
## Kiosk Browser Configuration Script
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
#!/bin/bash
|
|
427
|
+
# Auto-detect browser...
|
|
428
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
429
|
+
chromium-browser --kiosk http://localhost:8100
|
|
430
|
+
fi
|
|
431
|
+
```
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Benefits:**
|
|
435
|
+
- Single source of truth — script lives in one place (markdown codeblock)
|
|
436
|
+
- No duplication between markdown documentation and YAML
|
|
437
|
+
- Easy to read and maintain
|
|
438
|
+
- Changes to the codeblock automatically apply to the deployment
|
|
439
|
+
|
|
440
|
+
**Reference formats:**
|
|
441
|
+
- `"#section-id"` — script from section in current spec file
|
|
442
|
+
- `"./file.md#section-id"` — script from section in specific file
|
|
443
|
+
|
|
444
|
+
The section ID is derived from the heading: spaces become hyphens, lowercase.
|
|
445
|
+
Example: `## Kiosk Browser Configuration Script` → `#kiosk-browser-configuration-script`
|
|
446
|
+
|
|
447
|
+
### Execute Script by Reference (`redeploy exec`)
|
|
448
|
+
|
|
449
|
+
Run a single script from markdown without running the full migration:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
# Execute script from codeblock on remote host
|
|
453
|
+
redeploy exec '#kiosk-browser-configuration-script' \
|
|
454
|
+
--host pi@192.168.188.108 \
|
|
455
|
+
--file migration.podman-rpi5-resume.md
|
|
456
|
+
|
|
457
|
+
# With file in reference
|
|
458
|
+
redeploy exec './migration.md#install-deps' --host root@server.com
|
|
459
|
+
|
|
460
|
+
# Using markpact:ref (more explicit)
|
|
461
|
+
redeploy exec 'kiosk-script-id' --host pi@192.168.188.108 --file migration.md
|
|
462
|
+
|
|
463
|
+
# Dry-run to preview script
|
|
464
|
+
redeploy exec '#backup-script' --host pi@192.168.188.108 --file ops.md --dry-run
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
This is useful for:
|
|
468
|
+
- One-off operations defined in markdown docs
|
|
469
|
+
- Testing individual scripts before full migration
|
|
470
|
+
- Running maintenance tasks
|
|
471
|
+
|
|
472
|
+
### Execute Multiple Scripts (`redeploy exec-multi`)
|
|
473
|
+
|
|
474
|
+
Test multiple scripts at once:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
# Execute multiple scripts by ref
|
|
478
|
+
redeploy exec-multi 'kiosk-script,install-deps,cleanup' \
|
|
479
|
+
--host pi@192.168.188.108 \
|
|
480
|
+
--file migration.md
|
|
481
|
+
|
|
482
|
+
# Mix of markpact:ref and section headings
|
|
483
|
+
redeploy exec-multi 'script1,#section2,script3' \
|
|
484
|
+
--host root@server.com \
|
|
485
|
+
--file deploy.md \
|
|
486
|
+
--dry-run
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Marking Codeblocks with `markpact:ref`
|
|
490
|
+
|
|
491
|
+
For more explicit script identification, use `markpact:ref <id>` in codeblock:
|
|
492
|
+
|
|
493
|
+
```markdown
|
|
494
|
+
```bash markpact:ref kiosk-browser-configuration-script
|
|
495
|
+
#!/bin/bash
|
|
496
|
+
# Auto-detect browser...
|
|
497
|
+
if command -v chromium-browser >/dev/null 2>&1; then
|
|
498
|
+
chromium-browser --kiosk http://localhost:8100
|
|
499
|
+
fi
|
|
500
|
+
```
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Benefits of `markpact:ref`:
|
|
504
|
+
- Explicit ID assignment (not derived from heading)
|
|
505
|
+
- Multiple scripts per section
|
|
506
|
+
- Can reference by simple ID instead of full heading
|
|
507
|
+
- Self-documenting in markdown
|
|
508
|
+
|
|
407
509
|
|
|
408
510
|
|
|
409
511
|
Place in project root — `redeploy run` (no args) uses it automatically.
|
|
@@ -7,7 +7,7 @@ Public API (stable from 0.2.0, semver guaranteed)::
|
|
|
7
7
|
|
|
8
8
|
Everything not listed in ``__all__`` is internal and may change without notice.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.18"
|
|
11
11
|
|
|
12
12
|
# ── Core models ───────────────────────────────────────────────────────────────
|
|
13
13
|
from .models import ( # noqa: F401
|
|
@@ -143,10 +143,14 @@ class Executor:
|
|
|
143
143
|
self._audit_path = audit_path
|
|
144
144
|
self._t0: float = 0.0
|
|
145
145
|
|
|
146
|
+
# ── spec path for command_ref resolution ─────────────────────────────
|
|
147
|
+
if spec_path and not plan.spec_path:
|
|
148
|
+
plan.spec_path = spec_path
|
|
149
|
+
|
|
146
150
|
# ── resume / checkpoint ──────────────────────────────────────────────
|
|
147
151
|
self._resume = resume
|
|
148
152
|
self._from_step = from_step
|
|
149
|
-
spec_id = spec_path or plan.target_file or plan.infra_file or plan.app
|
|
153
|
+
spec_id = spec_path or plan.spec_path or plan.target_file or plan.infra_file or plan.app
|
|
150
154
|
if state_path is None and not dry_run:
|
|
151
155
|
state_path = default_state_path(spec_id, plan.host)
|
|
152
156
|
self._state_path: Optional[Path] = (
|
|
@@ -578,9 +582,10 @@ class Executor:
|
|
|
578
582
|
command_ref formats:
|
|
579
583
|
- "./file.md#section-id" - script from section in specific file
|
|
580
584
|
- "#section-id" - script from section in current spec file
|
|
585
|
+
- "#kiosk-browser-configuration-script" - markpact:ref block
|
|
581
586
|
"""
|
|
582
587
|
from pathlib import Path
|
|
583
|
-
from ..markpact.parser import extract_script_from_markdown
|
|
588
|
+
from ..markpact.parser import extract_script_from_markdown, extract_script_by_ref
|
|
584
589
|
|
|
585
590
|
# Parse command_ref
|
|
586
591
|
if "#" in command_ref:
|
|
@@ -597,12 +602,20 @@ class Executor:
|
|
|
597
602
|
if not file_path.exists():
|
|
598
603
|
raise StepError(step, f"Command ref file not found: {file_path}")
|
|
599
604
|
|
|
600
|
-
# Extract script from markdown
|
|
605
|
+
# Extract script from markdown - try both methods:
|
|
606
|
+
# 1. First try markpact:ref codeblock (new format)
|
|
607
|
+
# 2. Then try section heading (old format)
|
|
601
608
|
markdown_content = file_path.read_text(encoding="utf-8")
|
|
602
|
-
|
|
609
|
+
|
|
610
|
+
# Try markpact:ref format first
|
|
611
|
+
script = extract_script_by_ref(markdown_content, section_id, language="bash")
|
|
612
|
+
|
|
613
|
+
# Fallback to section heading format
|
|
614
|
+
if script is None:
|
|
615
|
+
script = extract_script_from_markdown(markdown_content, section_id, language="bash")
|
|
603
616
|
|
|
604
617
|
if script is None:
|
|
605
|
-
raise StepError(step, f"Could not find bash script
|
|
618
|
+
raise StepError(step, f"Could not find bash script for ref '{section_id}' in {file_path} (tried markpact:ref and section heading)")
|
|
606
619
|
|
|
607
620
|
return script
|
|
608
621
|
|
|
@@ -743,6 +743,237 @@ def plugin_cmd(ctx, subcommand, name):
|
|
|
743
743
|
console.print(f"\n{doc}")
|
|
744
744
|
|
|
745
745
|
|
|
746
|
+
# ── exec (execute script from markdown by reference) ───────────────────────────
|
|
747
|
+
|
|
748
|
+
@cli.command("exec")
|
|
749
|
+
@click.argument("ref", required=True)
|
|
750
|
+
@click.option("--host", required=True, help="SSH host (user@ip) or 'local'")
|
|
751
|
+
@click.option("--file", "markdown_file", default=None, type=click.Path(),
|
|
752
|
+
help="Markdown file (required if ref doesn't specify file)")
|
|
753
|
+
@click.option("--dry-run", is_flag=True, help="Show script without executing")
|
|
754
|
+
@click.option("--timeout", default=300, help="Step timeout in seconds")
|
|
755
|
+
@click.pass_context
|
|
756
|
+
def exec_cmd(ctx, ref, host, markdown_file, dry_run, timeout):
|
|
757
|
+
"""Execute a script from a markdown codeblock by reference.
|
|
758
|
+
|
|
759
|
+
REF format: #section-id or ./file.md#section-id or just ref-id (for markpact:ref)
|
|
760
|
+
|
|
761
|
+
Extracts the script from a ```bash codeblock in the specified section
|
|
762
|
+
or from a codeblock marked with markpact:ref <id>.
|
|
763
|
+
|
|
764
|
+
\b
|
|
765
|
+
Examples:
|
|
766
|
+
redeploy exec '#kiosk-browser-configuration-script' --host pi@192.168.188.108 --file migration.md
|
|
767
|
+
redeploy exec './migration.md#kiosk-script' --host pi@192.168.188.108
|
|
768
|
+
redeploy exec '#install-deps' --host root@server.com --file deploy.md --dry-run
|
|
769
|
+
redeploy exec 'kiosk-browser-configuration-script' --host pi@192.168.188.108 --file migration.md
|
|
770
|
+
"""
|
|
771
|
+
from rich.console import Console
|
|
772
|
+
from pathlib import Path
|
|
773
|
+
from .markpact.parser import extract_script_from_markdown, extract_script_by_ref
|
|
774
|
+
from .detect.remote import RemoteProbe
|
|
775
|
+
from .apply.executor import Executor, MigrationPlan, MigrationStep, StepAction, StepStatus
|
|
776
|
+
|
|
777
|
+
console = Console()
|
|
778
|
+
|
|
779
|
+
# Parse reference
|
|
780
|
+
if "#" in ref:
|
|
781
|
+
file_part, section_id = ref.split("#", 1)
|
|
782
|
+
if file_part:
|
|
783
|
+
md_path = Path(file_part)
|
|
784
|
+
elif markdown_file:
|
|
785
|
+
md_path = Path(markdown_file)
|
|
786
|
+
else:
|
|
787
|
+
console.print("[red]✗ No file specified in ref and no --file provided[/red]")
|
|
788
|
+
sys.exit(1)
|
|
789
|
+
ref_id = section_id
|
|
790
|
+
use_section_lookup = True
|
|
791
|
+
else:
|
|
792
|
+
ref_id = ref
|
|
793
|
+
if not markdown_file:
|
|
794
|
+
console.print("[red]✗ --file required when ref doesn't specify file[/red]")
|
|
795
|
+
sys.exit(1)
|
|
796
|
+
md_path = Path(markdown_file)
|
|
797
|
+
use_section_lookup = False
|
|
798
|
+
|
|
799
|
+
if not md_path.exists():
|
|
800
|
+
console.print(f"[red]✗ File not found: {md_path}[/red]")
|
|
801
|
+
sys.exit(1)
|
|
802
|
+
|
|
803
|
+
# Extract script from markdown
|
|
804
|
+
console.print(f"[dim]Reading {md_path}...[/dim]")
|
|
805
|
+
md_content = md_path.read_text(encoding="utf-8")
|
|
806
|
+
|
|
807
|
+
# Try markpact:ref lookup first (more specific), then fallback to section lookup
|
|
808
|
+
script = extract_script_by_ref(md_content, ref_id, language="bash")
|
|
809
|
+
lookup_method = "markpact:ref"
|
|
810
|
+
|
|
811
|
+
if script is None and use_section_lookup:
|
|
812
|
+
script = extract_script_from_markdown(md_content, ref_id, language="bash")
|
|
813
|
+
lookup_method = "section heading"
|
|
814
|
+
|
|
815
|
+
if script is None:
|
|
816
|
+
console.print(f"[red]✗ Could not find bash script with ref '#{ref_id}'[/red]")
|
|
817
|
+
console.print("[dim] Make sure there's either:[/dim]")
|
|
818
|
+
console.print("[dim] - A codeblock: ```bash markpact:ref {ref_id}[/dim]")
|
|
819
|
+
console.print("[dim] - Or a ## section heading and ```bash codeblock[/dim]")
|
|
820
|
+
sys.exit(1)
|
|
821
|
+
|
|
822
|
+
console.print(f"[green]✓[/green] Found script ({len(script)} chars) via {lookup_method} '#{ref_id}'")
|
|
823
|
+
|
|
824
|
+
if dry_run:
|
|
825
|
+
console.print(f"\n[bold]Script content (dry-run):[/bold]")
|
|
826
|
+
console.print("```bash")
|
|
827
|
+
console.print(script)
|
|
828
|
+
console.print("```")
|
|
829
|
+
return
|
|
830
|
+
|
|
831
|
+
# Execute script on remote host
|
|
832
|
+
console.print(f"\n[bold]Executing on {host}...[/bold]")
|
|
833
|
+
|
|
834
|
+
# Create minimal plan with single step
|
|
835
|
+
step = MigrationStep(
|
|
836
|
+
id=f"exec_{ref_id}",
|
|
837
|
+
action=StepAction.INLINE_SCRIPT,
|
|
838
|
+
description=f"Execute script from #{ref_id}",
|
|
839
|
+
command=script,
|
|
840
|
+
timeout=timeout,
|
|
841
|
+
)
|
|
842
|
+
plan = MigrationPlan(
|
|
843
|
+
host=host,
|
|
844
|
+
app="exec",
|
|
845
|
+
from_strategy="unknown",
|
|
846
|
+
to_strategy="unknown",
|
|
847
|
+
steps=[step],
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
executor = Executor(plan, dry_run=False)
|
|
851
|
+
ok = executor.run()
|
|
852
|
+
|
|
853
|
+
if not ok:
|
|
854
|
+
sys.exit(1)
|
|
855
|
+
|
|
856
|
+
console.print(f"[green]✓ Script executed successfully[/green]")
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
# ── exec-multi (execute multiple scripts by ref) ───────────────────────────────
|
|
860
|
+
|
|
861
|
+
@cli.command("exec-multi")
|
|
862
|
+
@click.argument("refs")
|
|
863
|
+
@click.option("--host", required=True, help="SSH host (user@ip) or 'local'")
|
|
864
|
+
@click.option("--file", "markdown_file", required=True, type=click.Path(),
|
|
865
|
+
help="Markdown file containing scripts")
|
|
866
|
+
@click.option("--dry-run", is_flag=True, help="Show scripts without executing")
|
|
867
|
+
@click.option("--timeout", default=300, help="Step timeout in seconds")
|
|
868
|
+
@click.option("--parallel", is_flag=True, help="Execute scripts in parallel (default: sequential)")
|
|
869
|
+
@click.pass_context
|
|
870
|
+
def exec_multi_cmd(ctx, refs, host, markdown_file, dry_run, timeout, parallel):
|
|
871
|
+
"""Execute multiple scripts from markdown codeblocks by reference.
|
|
872
|
+
|
|
873
|
+
REFS format: comma-separated list of ref ids (markpact:ref or section headings)
|
|
874
|
+
|
|
875
|
+
\b
|
|
876
|
+
Examples:
|
|
877
|
+
redeploy exec-multi 'kiosk-script,install-deps,cleanup' \
|
|
878
|
+
--host pi@192.168.188.108 --file migration.md
|
|
879
|
+
|
|
880
|
+
redeploy exec-multi '#section1,#section2' \
|
|
881
|
+
--host root@server.com --file deploy.md --dry-run
|
|
882
|
+
"""
|
|
883
|
+
from rich.console import Console
|
|
884
|
+
from rich.table import Table
|
|
885
|
+
from pathlib import Path
|
|
886
|
+
from .markpact.parser import extract_script_from_markdown, extract_script_by_ref
|
|
887
|
+
from .apply.executor import Executor, MigrationPlan, MigrationStep, StepAction, StepStatus
|
|
888
|
+
|
|
889
|
+
console = Console()
|
|
890
|
+
md_path = Path(markdown_file)
|
|
891
|
+
|
|
892
|
+
if not md_path.exists():
|
|
893
|
+
console.print(f"[red]✗ File not found: {md_path}[/red]")
|
|
894
|
+
sys.exit(1)
|
|
895
|
+
|
|
896
|
+
# Parse refs
|
|
897
|
+
ref_list = [r.strip() for r in refs.split(",")]
|
|
898
|
+
console.print(f"[bold]Loading {len(ref_list)} scripts from {md_path}...[/bold]")
|
|
899
|
+
|
|
900
|
+
md_content = md_path.read_text(encoding="utf-8")
|
|
901
|
+
|
|
902
|
+
# Extract all scripts
|
|
903
|
+
scripts: list[tuple[str, str, str]] = [] # (ref_id, script, lookup_method)
|
|
904
|
+
for ref_id in ref_list:
|
|
905
|
+
# Try markpact:ref first
|
|
906
|
+
script = extract_script_by_ref(md_content, ref_id, language="bash")
|
|
907
|
+
lookup_method = "markpact:ref"
|
|
908
|
+
|
|
909
|
+
if script is None and ref_id.startswith("#"):
|
|
910
|
+
# Try section lookup
|
|
911
|
+
script = extract_script_from_markdown(md_content, ref_id[1:], language="bash")
|
|
912
|
+
lookup_method = "section"
|
|
913
|
+
|
|
914
|
+
if script is None:
|
|
915
|
+
console.print(f"[red]✗ Could not find script: {ref_id}[/red]")
|
|
916
|
+
sys.exit(1)
|
|
917
|
+
|
|
918
|
+
scripts.append((ref_id, script, lookup_method))
|
|
919
|
+
console.print(f" [green]✓[/green] {ref_id} ({len(script)} chars, {lookup_method})")
|
|
920
|
+
|
|
921
|
+
if dry_run:
|
|
922
|
+
console.print(f"\n[bold]Scripts (dry-run):[/bold]")
|
|
923
|
+
for ref_id, script, _ in scripts:
|
|
924
|
+
console.print(f"\n--- {ref_id} ---")
|
|
925
|
+
console.print("```bash")
|
|
926
|
+
console.print(script[:500] + ("..." if len(script) > 500 else ""))
|
|
927
|
+
console.print("```")
|
|
928
|
+
return
|
|
929
|
+
|
|
930
|
+
# Execute scripts
|
|
931
|
+
console.print(f"\n[bold]Executing on {host}...[/bold]")
|
|
932
|
+
|
|
933
|
+
results = []
|
|
934
|
+
for ref_id, script, _ in scripts:
|
|
935
|
+
console.print(f" Running {ref_id}...", end=" ")
|
|
936
|
+
|
|
937
|
+
step = MigrationStep(
|
|
938
|
+
id=f"exec_{ref_id}",
|
|
939
|
+
action=StepAction.INLINE_SCRIPT,
|
|
940
|
+
description=f"Execute script from {ref_id}",
|
|
941
|
+
command=script,
|
|
942
|
+
timeout=timeout,
|
|
943
|
+
)
|
|
944
|
+
plan = MigrationPlan(
|
|
945
|
+
host=host,
|
|
946
|
+
app="exec-multi",
|
|
947
|
+
from_strategy="unknown",
|
|
948
|
+
to_strategy="unknown",
|
|
949
|
+
steps=[step],
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
executor = Executor(plan, dry_run=False)
|
|
953
|
+
ok = executor.run()
|
|
954
|
+
results.append((ref_id, ok))
|
|
955
|
+
|
|
956
|
+
if ok:
|
|
957
|
+
console.print("[green]✓[/green]")
|
|
958
|
+
else:
|
|
959
|
+
console.print("[red]✗[/red]")
|
|
960
|
+
|
|
961
|
+
# Summary table
|
|
962
|
+
console.print(f"\n[bold]Results:[/bold]")
|
|
963
|
+
t = Table(show_header=True, box=None)
|
|
964
|
+
t.add_column("Script")
|
|
965
|
+
t.add_column("Status")
|
|
966
|
+
for ref_id, ok in results:
|
|
967
|
+
status = "[green]✓ OK[/green]" if ok else "[red]✗ Failed[/red]"
|
|
968
|
+
t.add_row(ref_id, status)
|
|
969
|
+
console.print(t)
|
|
970
|
+
|
|
971
|
+
if not all(ok for _, ok in results):
|
|
972
|
+
sys.exit(1)
|
|
973
|
+
|
|
974
|
+
console.print(f"\n[green]✓ All {len(scripts)} scripts executed successfully[/green]")
|
|
975
|
+
|
|
976
|
+
|
|
746
977
|
# ── plan ──────────────────────────────────────────────────────────────────────
|
|
747
978
|
|
|
748
979
|
@cli.command()
|
|
@@ -1047,6 +1278,98 @@ def run(ctx, spec_file, dry_run, plan_only, do_detect, plan_out, output,
|
|
|
1047
1278
|
sys.exit(1)
|
|
1048
1279
|
|
|
1049
1280
|
|
|
1281
|
+
# ── state (resume checkpoints) ───────────────────────────────────────────────
|
|
1282
|
+
|
|
1283
|
+
@cli.command("state")
|
|
1284
|
+
@click.argument("action", type=click.Choice(["show", "clear", "ls"]),
|
|
1285
|
+
default="show")
|
|
1286
|
+
@click.argument("spec_file", default=None, required=False, type=click.Path())
|
|
1287
|
+
@click.option("--host", default=None, help="Override host (default: from spec)")
|
|
1288
|
+
@click.option("--state-file", default=None, type=click.Path(),
|
|
1289
|
+
help="Explicit checkpoint path")
|
|
1290
|
+
@click.pass_context
|
|
1291
|
+
def state_cmd(ctx, action, spec_file, host, state_file):
|
|
1292
|
+
"""Inspect or clear resume checkpoints.
|
|
1293
|
+
|
|
1294
|
+
\b
|
|
1295
|
+
Examples:
|
|
1296
|
+
redeploy state show migration.yaml # current checkpoint
|
|
1297
|
+
redeploy state clear migration.yaml # delete checkpoint
|
|
1298
|
+
redeploy state ls # list all checkpoints in CWD
|
|
1299
|
+
"""
|
|
1300
|
+
from rich.console import Console
|
|
1301
|
+
from rich.table import Table
|
|
1302
|
+
from .apply.state import DEFAULT_STATE_DIR, ResumeState, default_state_path
|
|
1303
|
+
|
|
1304
|
+
console = Console()
|
|
1305
|
+
|
|
1306
|
+
if action == "ls":
|
|
1307
|
+
base = DEFAULT_STATE_DIR
|
|
1308
|
+
if not base.exists():
|
|
1309
|
+
console.print(f"[dim]no checkpoints under {base}/[/dim]")
|
|
1310
|
+
return
|
|
1311
|
+
files = sorted(base.glob("*.yaml"))
|
|
1312
|
+
if not files:
|
|
1313
|
+
console.print(f"[dim]no checkpoints under {base}/[/dim]")
|
|
1314
|
+
return
|
|
1315
|
+
t = Table(show_header=True, box=None, padding=(0, 2))
|
|
1316
|
+
t.add_column("File", style="bold")
|
|
1317
|
+
t.add_column("Spec")
|
|
1318
|
+
t.add_column("Host", style="cyan")
|
|
1319
|
+
t.add_column("Done")
|
|
1320
|
+
t.add_column("Failed", style="red")
|
|
1321
|
+
t.add_column("Updated", style="dim")
|
|
1322
|
+
for f in files:
|
|
1323
|
+
try:
|
|
1324
|
+
st = ResumeState.load(f)
|
|
1325
|
+
t.add_row(
|
|
1326
|
+
f.name, st.spec_path or "?", st.host,
|
|
1327
|
+
f"{st.completed_count}/{st.total_steps}",
|
|
1328
|
+
st.failed_step_id or "—",
|
|
1329
|
+
st.updated_at,
|
|
1330
|
+
)
|
|
1331
|
+
except Exception as e:
|
|
1332
|
+
t.add_row(f.name, "[red]parse error[/red]", "—", "—", "—", str(e)[:40])
|
|
1333
|
+
console.print(t)
|
|
1334
|
+
return
|
|
1335
|
+
|
|
1336
|
+
# show / clear need a path
|
|
1337
|
+
if state_file:
|
|
1338
|
+
path = Path(state_file)
|
|
1339
|
+
else:
|
|
1340
|
+
if not spec_file:
|
|
1341
|
+
console.print("[red]✗ provide SPEC or --state-file[/red]")
|
|
1342
|
+
sys.exit(2)
|
|
1343
|
+
spec = _load_spec_or_exit(console, spec_file)
|
|
1344
|
+
target_host = host or spec.source.host or "local"
|
|
1345
|
+
path = default_state_path(spec_file, target_host)
|
|
1346
|
+
|
|
1347
|
+
if not path.exists():
|
|
1348
|
+
console.print(f"[dim]no checkpoint at {path}[/dim]")
|
|
1349
|
+
return
|
|
1350
|
+
|
|
1351
|
+
if action == "clear":
|
|
1352
|
+
path.unlink()
|
|
1353
|
+
console.print(f"[green]✓ removed {path}[/green]")
|
|
1354
|
+
return
|
|
1355
|
+
|
|
1356
|
+
# show
|
|
1357
|
+
st = ResumeState.load(path)
|
|
1358
|
+
console.print(f"[bold]checkpoint[/bold] {path}")
|
|
1359
|
+
console.print(f" spec: {st.spec_path}")
|
|
1360
|
+
console.print(f" host: {st.host}")
|
|
1361
|
+
console.print(f" progress: {st.completed_count}/{st.total_steps} "
|
|
1362
|
+
f"({st.remaining} pending)")
|
|
1363
|
+
console.print(f" started: {st.started_at}")
|
|
1364
|
+
console.print(f" updated: {st.updated_at}")
|
|
1365
|
+
if st.failed_step_id:
|
|
1366
|
+
console.print(f" [red]failed: {st.failed_step_id}[/red]")
|
|
1367
|
+
if st.failed_error:
|
|
1368
|
+
console.print(f" [red]error: {st.failed_error[:200]}[/red]")
|
|
1369
|
+
if st.completed_step_ids:
|
|
1370
|
+
console.print(f" done: {', '.join(st.completed_step_ids)}")
|
|
1371
|
+
|
|
1372
|
+
|
|
1050
1373
|
# ── helper ────────────────────────────────────────────────────────────────────
|
|
1051
1374
|
|
|
1052
1375
|
def _find_manifest_path() -> str:
|