launchline 0.2.1.dev0__tar.gz → 0.4.0__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 (95) hide show
  1. launchline-0.4.0/.github/dependabot.yml +6 -0
  2. launchline-0.4.0/.github/workflows/ci.yml +67 -0
  3. launchline-0.4.0/.github/workflows/release.yml +234 -0
  4. {launchline-0.2.1.dev0 → launchline-0.4.0}/.gitignore +7 -0
  5. {launchline-0.2.1.dev0 → launchline-0.4.0}/.markdownlint.json +1 -0
  6. launchline-0.4.0/.semrelrc +21 -0
  7. {launchline-0.2.1.dev0 → launchline-0.4.0}/CHANGELOG.md +24 -3
  8. {launchline-0.2.1.dev0 → launchline-0.4.0}/PKG-INFO +47 -22
  9. {launchline-0.2.1.dev0 → launchline-0.4.0}/README.md +45 -20
  10. launchline-0.4.0/docs/compatibility.md +39 -0
  11. launchline-0.4.0/docs/menus.md +135 -0
  12. launchline-0.4.0/docs/releasing.md +46 -0
  13. launchline-0.4.0/docs/validation/baseline.json +31 -0
  14. launchline-0.4.0/docs/validation/benchmark.py +45 -0
  15. launchline-0.4.0/docs/validation/current.json +31 -0
  16. launchline-0.4.0/docs/validation/dependency-alerts.json +814 -0
  17. launchline-0.4.0/docs/validation/dependency-triage.md +33 -0
  18. launchline-0.4.0/docs/validation/hosted-ci.md +22 -0
  19. launchline-0.4.0/docs/validation/menu-checkpoint.md +103 -0
  20. launchline-0.4.0/docs/validation/menu-mutation.json +56 -0
  21. launchline-0.4.0/docs/validation/menu-packages.json +12 -0
  22. launchline-0.4.0/docs/validation/menu-performance.json +38 -0
  23. launchline-0.4.0/docs/validation/menu-protocol-limit.json +51 -0
  24. launchline-0.4.0/docs/validation/mutation.json +74 -0
  25. launchline-0.4.0/docs/validation/package-smoke.json +40 -0
  26. launchline-0.4.0/docs/validation/parity.json +77 -0
  27. launchline-0.4.0/docs/validation/report.md +70 -0
  28. launchline-0.4.0/docs/validation/search-comparison.json +152 -0
  29. launchline-0.4.0/docs/validation/semantic-release-dry-run.json +16 -0
  30. launchline-0.4.0/docs/validation/summary.json +69 -0
  31. {launchline-0.2.1.dev0 → launchline-0.4.0}/pyproject.toml +43 -7
  32. launchline-0.4.0/scripts/benchmark_compare.py +64 -0
  33. launchline-0.4.0/scripts/benchmark_menus.py +88 -0
  34. launchline-0.4.0/scripts/mutate_menus.py +207 -0
  35. launchline-0.4.0/scripts/mutation_check.py +196 -0
  36. launchline-0.4.0/scripts/package_smoke.py +122 -0
  37. launchline-0.4.0/scripts/parity_check.py +70 -0
  38. launchline-0.4.0/scripts/prepare_semrel.py +49 -0
  39. launchline-0.4.0/scripts/release_dryrun.py +123 -0
  40. launchline-0.4.0/scripts/release_version.py +28 -0
  41. launchline-0.4.0/scripts/semantic-release-plugins.json +98 -0
  42. launchline-0.4.0/src/launchline/__main__.py +138 -0
  43. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/cli.py +25 -3
  44. launchline-0.4.0/src/launchline/config.py +393 -0
  45. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/exceptions.py +5 -1
  46. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/fuzzy.py +22 -18
  47. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/keys.py +102 -24
  48. launchline-0.4.0/src/launchline/menus.py +284 -0
  49. launchline-0.4.0/src/launchline/providers.py +290 -0
  50. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/runner.py +4 -1
  51. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/ui.py +111 -10
  52. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/conftest.py +31 -0
  53. launchline-0.4.0/tests/fixtures/fuzzy.tsv +405 -0
  54. launchline-0.4.0/tests/fixtures/menu-contract.json +161 -0
  55. launchline-0.4.0/tests/fixtures/menu_provider.py +97 -0
  56. launchline-0.4.0/tests/test_application.py +142 -0
  57. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/test_config.py +49 -77
  58. launchline-0.4.0/tests/test_contract.py +240 -0
  59. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/test_cross_platform.py +114 -60
  60. launchline-0.4.0/tests/test_keys_unix.py +192 -0
  61. launchline-0.4.0/tests/test_menu_application.py +188 -0
  62. launchline-0.4.0/tests/test_menus.py +310 -0
  63. launchline-0.4.0/tests/test_providers.py +122 -0
  64. launchline-0.4.0/tests/test_release_policy.py +109 -0
  65. launchline-0.4.0/tests/test_release_version.py +40 -0
  66. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/test_runner.py +24 -10
  67. launchline-0.4.0/tests/test_terminal_e2e.py +525 -0
  68. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/test_ui.py +228 -112
  69. launchline-0.4.0/uv.lock +446 -0
  70. launchline-0.2.1.dev0/.generated-go-semantic-release-changelog.md +0 -23
  71. launchline-0.2.1.dev0/.github/workflows/ci.yml +0 -40
  72. launchline-0.2.1.dev0/.github/workflows/release.yml +0 -73
  73. launchline-0.2.1.dev0/.semrel/linux_amd64/changelog-generator-default/1.15.0/changelog-generator-default_v1.15.0_linux_amd64 +0 -0
  74. launchline-0.2.1.dev0/.semrel/linux_amd64/commit-analyzer-default/1.15.0/commit-analyzer-cz_v1.15.0_linux_amd64 +0 -0
  75. launchline-0.2.1.dev0/.semrel/linux_amd64/condition-github/1.11.0/condition-github_v1.11.0_linux_amd64 +0 -0
  76. launchline-0.2.1.dev0/.semrel/linux_amd64/files-updater-npm/1.11.0/files-updater-npm_v1.11.0_linux_amd64 +0 -0
  77. launchline-0.2.1.dev0/.semrel/linux_amd64/provider-github/1.19.0/provider-github_v1.19.0_linux_amd64 +0 -0
  78. launchline-0.2.1.dev0/src/launchline/__main__.py +0 -109
  79. launchline-0.2.1.dev0/src/launchline/config.py +0 -271
  80. launchline-0.2.1.dev0/uv.lock +0 -427
  81. {launchline-0.2.1.dev0 → launchline-0.4.0}/.python-version +0 -0
  82. {launchline-0.2.1.dev0 → launchline-0.4.0}/LICENSE +0 -0
  83. {launchline-0.2.1.dev0 → launchline-0.4.0}/assets/launchline.ico +0 -0
  84. {launchline-0.2.1.dev0 → launchline-0.4.0}/assets/launchline.png +0 -0
  85. {launchline-0.2.1.dev0 → launchline-0.4.0}/assets/launchline.svg +0 -0
  86. {launchline-0.2.1.dev0 → launchline-0.4.0}/docs/images/demo.gif +0 -0
  87. {launchline-0.2.1.dev0 → launchline-0.4.0}/docs/images/hero.png +0 -0
  88. {launchline-0.2.1.dev0 → launchline-0.4.0}/docs/images/navigate.png +0 -0
  89. {launchline-0.2.1.dev0 → launchline-0.4.0}/docs/images/search.png +0 -0
  90. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/__init__.py +0 -0
  91. {launchline-0.2.1.dev0 → launchline-0.4.0}/src/launchline/assets/launchline.ico +0 -0
  92. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/__init__.py +0 -0
  93. {launchline-0.2.1.dev0 → launchline-0.4.0}/tests/test_cli.py +0 -0
  94. {launchline-0.2.1.dev0 → launchline-0.4.0}/tools/generate_icon.py +0 -0
  95. {launchline-0.2.1.dev0 → launchline-0.4.0}/tools/generate_screenshots.py +0 -0
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
@@ -0,0 +1,67 @@
1
+ name: CI
2
+ 'on':
3
+ workflow_call: null
4
+ workflow_dispatch: null
5
+ push:
6
+ branches:
7
+ - main
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ permissions:
12
+ contents: read
13
+ concurrency:
14
+ group: ci-${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: true
16
+ jobs:
17
+ check:
18
+ name: Python (${{ matrix.os }}, ${{ matrix.python }})
19
+ runs-on: ${{ matrix.os }}
20
+ timeout-minutes: 15
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ os:
25
+ - ubuntu-latest
26
+ - windows-latest
27
+ - macos-latest
28
+ python:
29
+ - '3.12'
30
+ - '3.13'
31
+ - '3.14'
32
+ steps:
33
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
34
+ with:
35
+ persist-credentials: false
36
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
37
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e
38
+ with:
39
+ python-version: ${{ matrix.python }}
40
+ - run: uv sync --locked --group dev
41
+ - run: uv run poe check
42
+ - run: uv build
43
+ - run: uv run python scripts/package_smoke.py
44
+ - run: uv run poe mutation
45
+ if: matrix.os == 'windows-latest'
46
+ - name: Check seeded menu mutations
47
+ run: uv run poe mutation-menus
48
+ if: matrix.os == 'windows-latest'
49
+ - name: Test results
50
+ if: always()
51
+ uses: mikepenz/action-junit-report@6ef3fdcfcd5f0b5d14df3e659ef19b8c6820f93d
52
+ with:
53
+ report_paths: junit.xml
54
+ annotate_only: true
55
+ check_annotations: false
56
+ check_retries: false
57
+ require_tests: true
58
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
59
+ if: always()
60
+ with:
61
+ name: evidence-${{ matrix.os }}-${{ matrix.python }}
62
+ path: 'junit.xml
63
+
64
+ docs/validation/*json
65
+
66
+ '
67
+ if-no-files-found: error
@@ -0,0 +1,234 @@
1
+ name: Release
2
+ 'on':
3
+ push:
4
+ branches:
5
+ - main
6
+ workflow_dispatch:
7
+ inputs:
8
+ force-patch:
9
+ description: Force patch bump when no releasable commits exist
10
+ type: boolean
11
+ default: false
12
+ permissions:
13
+ contents: read
14
+ concurrency:
15
+ group: release
16
+ cancel-in-progress: false
17
+ jobs:
18
+ target:
19
+ if: github.ref == 'refs/heads/main' && github.actor == 'mikejhill' && github.triggering_actor == 'mikejhill'
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 20
22
+ outputs:
23
+ version: ${{ steps.semrel.outputs.version }}
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
26
+ with:
27
+ persist-credentials: false
28
+ fetch-depth: 0
29
+ ref: ${{ github.sha }}
30
+ - name: Download checksum-pinned semantic-release CLI
31
+ shell: bash
32
+ run: "set -euo pipefail\ndest=\"$RUNNER_TEMP/semantic-release-v2.31.0\"\ncurl --fail --silent --show-error --location\
33
+ \ --proto '=https' --tlsv1.2 \\\n https://github.com/go-semantic-release/semantic-release/releases/download/v2.31.0/semantic-release_v2.31.0_linux_amd64\
34
+ \ \\\n --output \"$dest\"\nprintf '%s %s\\n' 'b8f518b1aeb1d1742f4e6e91a179b707cea4309a64fe3854f465bf724f04f6d3'\
35
+ \ \"$dest\" | sha256sum --check --strict\nchmod 0755 \"$dest\"\n"
36
+ - name: Preload verified runtime plugins
37
+ run: python3 scripts/prepare_semrel.py
38
+ - name: Determine version without publishing
39
+ id: semrel
40
+ shell: bash
41
+ env:
42
+ GITHUB_TOKEN: ${{ github.token }}
43
+ FORCE_PATCH: ${{ inputs.force-patch || false }}
44
+ run: 'set -euo pipefail
45
+
46
+ args=(--dry --allow-initial-development-versions --changelog release-changelog.md --allow-no-changes --plugin-resolver-disable-batch-prefetch)
47
+
48
+ if [ "$FORCE_PATCH" = true ]; then args+=(--force-bump-patch-version); fi
49
+
50
+ "$RUNNER_TEMP/semantic-release-v2.31.0" "${args[@]}"
51
+
52
+ python3 scripts/release_version.py
53
+
54
+ '
55
+ name: Target
56
+ ci:
57
+ name: Python (${{ matrix.os }}, ${{ matrix.python }})
58
+ runs-on: ${{ matrix.os }}
59
+ timeout-minutes: 15
60
+ strategy:
61
+ fail-fast: false
62
+ matrix:
63
+ os:
64
+ - ubuntu-latest
65
+ - windows-latest
66
+ - macos-latest
67
+ python:
68
+ - '3.12'
69
+ - '3.13'
70
+ - '3.14'
71
+ steps:
72
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
73
+ with:
74
+ persist-credentials: false
75
+ ref: ${{ github.sha }}
76
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e
77
+ with:
78
+ python-version: ${{ matrix.python }}
79
+ - run: uv sync --locked --group dev
80
+ - run: uv run poe check
81
+ - run: uv build
82
+ - run: uv run python scripts/package_smoke.py
83
+ - run: uv run poe mutation
84
+ if: matrix.os == 'windows-latest'
85
+ - name: Check seeded menu mutations
86
+ run: uv run poe mutation-menus
87
+ if: matrix.os == 'windows-latest'
88
+ - name: Test results
89
+ if: always()
90
+ uses: mikepenz/action-junit-report@6ef3fdcfcd5f0b5d14df3e659ef19b8c6820f93d
91
+ with:
92
+ report_paths: junit.xml
93
+ annotate_only: true
94
+ check_annotations: false
95
+ check_retries: false
96
+ require_tests: true
97
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
98
+ if: always()
99
+ with:
100
+ name: evidence-${{ matrix.os }}-${{ matrix.python }}
101
+ path: 'junit.xml
102
+
103
+ docs/validation/*json
104
+
105
+ '
106
+ if-no-files-found: error
107
+ needs: target
108
+ if: needs.target.outputs.version != ''
109
+ build:
110
+ needs:
111
+ - target
112
+ - ci
113
+ runs-on: ubuntu-latest
114
+ timeout-minutes: 20
115
+ outputs:
116
+ artifact-id: ${{ steps.dist.outputs.artifact-id }}
117
+ steps:
118
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
119
+ with:
120
+ persist-credentials: false
121
+ fetch-depth: 0
122
+ ref: ${{ github.sha }}
123
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e
124
+ - name: Build tested version without publishing credentials
125
+ run: '[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1
126
+
127
+ uv sync --locked --group dev
128
+
129
+ uv run poe check
130
+
131
+ python3 - <<''PY''
132
+
133
+ import os,pathlib
134
+
135
+ p=pathlib.Path(''README.md'')
136
+
137
+ p.write_text(p.read_text().replace(''(docs/'', ''(https://raw.githubusercontent.com/''+os.environ[''GITHUB_REPOSITORY'']+''/v''+os.environ[''VERSION'']+''/docs/''))
138
+
139
+ PY
140
+
141
+ uv build
142
+
143
+ uv run python scripts/package_smoke.py
144
+
145
+ printf ''Built from reviewed main commit %s\n'' "$GITHUB_SHA" > release-notes.md
146
+
147
+ '
148
+ env:
149
+ VERSION: ${{ needs.target.outputs.version }}
150
+ SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.target.outputs.version }}
151
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
152
+ id: dist
153
+ with:
154
+ name: trusted-release
155
+ path: 'dist/*
156
+
157
+ release-notes.md'
158
+ if-no-files-found: error
159
+ retention-days: 7
160
+ name: Build
161
+ github-release:
162
+ if: github.ref == 'refs/heads/main' && github.actor == 'mikejhill' && github.triggering_actor == 'mikejhill' && needs.build.result
163
+ == 'success'
164
+ needs:
165
+ - target
166
+ - build
167
+ - attest
168
+ runs-on: ubuntu-latest
169
+ timeout-minutes: 20
170
+ permissions:
171
+ contents: write # Create immutable refs and upload tested release artifacts.
172
+ steps:
173
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333
174
+ with:
175
+ artifact-ids: ${{ needs.build.outputs.artifact-id }}
176
+ path: release
177
+ - name: Publish same-run artifacts from trusted merge
178
+ run: "# The approved immutable-v* ruleset must already prohibit tag updates/deletion.\nancestor=$(gh api \"repos/$GITHUB_REPOSITORY/compare/$GITHUB_SHA...main\"\
179
+ \ --jq .merge_base_commit.sha)\ntest \"$ancestor\" = \"$GITHUB_SHA\" || { echo 'Release commit is no longer on main.'\
180
+ \ >&2; exit 1; }\npython3 - <<'PY'\nimport json,os,pathlib,re\nassert re.fullmatch(r'v[0-9]+\\.[0-9]+\\.[0-9]+',os.environ['TAG'])\n\
181
+ assert re.fullmatch(r'[0-9a-f]{40}',os.environ['GITHUB_SHA'])\npathlib.Path('release-ref.json').write_text(json.dumps({'ref':'refs/tags/'+os.environ['TAG'],'sha':os.environ['GITHUB_SHA']}))\n\
182
+ PY\n# Ref creation is atomic: if another process created it first, accept only the same commit.\nif ! gh api --method\
183
+ \ POST \"repos/$GITHUB_REPOSITORY/git/refs\" --input release-ref.json >/dev/null 2>tag-create-error.txt; then\n actual=$(gh\
184
+ \ api \"repos/$GITHUB_REPOSITORY/commits/$TAG\" --jq .sha)\n test \"$actual\" = \"$GITHUB_SHA\" || { echo 'Tag creation\
185
+ \ failed or a conflicting tag exists.' >&2; exit 1; }\nfi\nactual=$(gh api \"repos/$GITHUB_REPOSITORY/commits/$TAG\"\
186
+ \ --jq .sha)\ntest \"$actual\" = \"$GITHUB_SHA\" || { echo 'Tag does not match the trusted merge.' >&2; exit 1; }\n\
187
+ if gh release view \"$TAG\" --repo \"$GITHUB_REPOSITORY\" >/dev/null 2>&1; then\n :\nelse\n gh release create \"\
188
+ $TAG\" --repo \"$GITHUB_REPOSITORY\" --verify-tag --title \"$TAG\" --generate-notes --notes-file release/release-notes.md\n\
189
+ fi\ngh release upload \"$TAG\" release/dist/* --repo \"$GITHUB_REPOSITORY\" --clobber\n"
190
+ env:
191
+ GH_TOKEN: ${{ github.token }}
192
+ TAG: v${{ needs.target.outputs.version }}
193
+ name: Github Release
194
+ pypi:
195
+ if: github.ref == 'refs/heads/main' && github.actor == 'mikejhill' && github.triggering_actor == 'mikejhill' && needs.github-release.result
196
+ == 'success'
197
+ needs:
198
+ - target
199
+ - build
200
+ - github-release
201
+ runs-on: ubuntu-latest
202
+ timeout-minutes: 20
203
+ environment: pypi
204
+ permissions:
205
+ id-token: write # OIDC identity for attestation or trusted PyPI publishing.
206
+ steps:
207
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333
208
+ with:
209
+ artifact-ids: ${{ needs.build.outputs.artifact-id }}
210
+ path: release
211
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086
212
+ with:
213
+ packages-dir: release/dist/
214
+ skip-existing: true
215
+ name: Pypi
216
+ attest:
217
+ name: Attest tested artifacts
218
+ needs: build
219
+ if: github.ref == 'refs/heads/main' && github.actor == 'mikejhill' && github.triggering_actor == 'mikejhill' && needs.build.result
220
+ == 'success'
221
+ runs-on: ubuntu-latest
222
+ timeout-minutes: 10
223
+ permissions:
224
+ contents: read
225
+ id-token: write # OIDC identity for attestation or trusted PyPI publishing.
226
+ attestations: write # Store provenance for these same-run build artifacts.
227
+ steps:
228
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333
229
+ with:
230
+ path: release
231
+ artifact-ids: ${{ needs.build.outputs.artifact-id }}
232
+ - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6
233
+ with:
234
+ subject-path: release/dist/*
@@ -257,3 +257,10 @@ __marimo__/
257
257
 
258
258
  # hatch-vcs generated version file
259
259
  src/launchline/_version.py
260
+
261
+ junit.xml
262
+ release/
263
+
264
+ .semrel/
265
+ .version
266
+ release-changelog.md
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "MD013": false,
3
+ "MD024": { "siblings_only": true },
3
4
  "MD033": false
4
5
  }
@@ -0,0 +1,21 @@
1
+ {
2
+ "plugins": {
3
+ "provider": {
4
+ "name": "github@1.19.0"
5
+ },
6
+ "commit-analyzer": {
7
+ "name": "default@1.15.0"
8
+ },
9
+ "changelog-generator": {
10
+ "name": "default@1.15.0"
11
+ },
12
+ "ci-condition": {
13
+ "name": "default@1.11.0"
14
+ },
15
+ "files-updater": {
16
+ "names": [
17
+ "npm@1.11.0"
18
+ ]
19
+ }
20
+ }
21
+ }
@@ -7,11 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Menus
11
+
12
+ - Add nested menus with Back navigation, lazy JSON providers, explicit refresh/retry,
13
+ configurable parent refresh, and stable selection restoration.
14
+ - Bound generated output, provider timeouts and process cleanup; cache up to 128 menu
15
+ snapshots with inactive least-recently-used eviction. Existing static configs remain valid.
16
+ - Add shared declaration fixtures, actual provider/terminal tests and seeded menu mutations.
17
+ See [menu lifecycle and provider contract](docs/menus.md) for examples and limits.
18
+
19
+ ### Removed
20
+
21
+ - Reverted array syntax for `command` config key; `command` is string-only,
22
+ use `args` for arguments
23
+
24
+ ### Changed
25
+
26
+ - Improved test naming to follow `[unit]_[scenario]_[expectedResult]` pattern
27
+ - Added diagnostic failure messages to all test assertions
28
+ - Release workflow now supports `force-patch` input for non-bumping commits
29
+
30
+ ## [0.2.0] - 2026-03-30
31
+
10
32
  ### Added
11
33
 
12
- - `command` config key now accepts a list of strings (e.g.,
13
- `command = ["ssh", "-t", "my server"]`) for easier argument handling,
14
- especially with values containing spaces
15
34
  - `--config-path` CLI flag to print the active configuration file path
16
35
 
17
36
  ### Fixed
@@ -19,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
38
  - PyPI package page now renders README images correctly (paths are rewritten
20
39
  to absolute URLs during the release build)
21
40
  - Release artifacts no longer accidentally include non-distribution files
41
+ - Release workflow now produces correct version numbers (fixed dirty-tree
42
+ version detection via `SETUPTOOLS_SCM_PRETEND_VERSION`)
22
43
 
23
44
  ## [0.1.0] - 2026-03-30
24
45
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: launchline
3
- Version: 0.2.1.dev0
3
+ Version: 0.4.0
4
4
  Summary: A lightweight terminal launcher for interactive CLI tools.
5
5
  Project-URL: Homepage, https://github.com/mikejhill/launchline
6
6
  Project-URL: Repository, https://github.com/mikejhill/launchline
@@ -24,12 +24,14 @@ Description-Content-Type: text/markdown
24
24
 
25
25
  [![CI](https://github.com/mikejhill/launchline/actions/workflows/ci.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/ci.yml)
26
26
  [![Release](https://github.com/mikejhill/launchline/actions/workflows/release.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/release.yml)
27
+ [![PyPI](https://img.shields.io/pypi/v/launchline)](https://pypi.org/project/launchline/)
28
+ [![Python](https://img.shields.io/pypi/pyversions/launchline)](https://pypi.org/project/launchline/)
27
29
 
28
30
  A lightweight terminal launcher for interactive CLI tools. Define your frequently
29
31
  used commands in a TOML config file, then pick them from a fuzzy-searchable menu
30
32
  instead of remembering flags and paths.
31
33
 
32
- ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/demo.gif)
34
+ ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/images/demo.gif)
33
35
 
34
36
  ## Why
35
37
 
@@ -50,7 +52,7 @@ instead.
50
52
 
51
53
  | Fuzzy search | Arrow-key navigation |
52
54
  | :-------------------------------: | :-----------------------------------: |
53
- | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/navigate.png) |
55
+ | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/images/navigate.png) |
54
56
 
55
57
  ## Installation
56
58
 
@@ -118,29 +120,29 @@ Config file format is [TOML](https://toml.io). The file has an optional
118
120
  | `clear_on_launch` | bool | `true` | Clear terminal before launching an entry |
119
121
  | `ghost_text` | bool | `true` | Show the highlighted entry name as an autocomplete hint on prompt |
120
122
  | `numeric_trigger` | bool | `true` | Pressing a digit immediately launches the matching entry |
123
+ | `refresh_on_return` | boolean | `false` | Regenerate a dynamic root after returning from a submenu |
121
124
 
122
125
  ### `[[entries]]`
123
126
 
124
127
  Each `[[entries]]` table defines one launchable tool:
125
128
 
126
- | Key | Type | Required | Default | Description |
127
- | ------------------- | ------------------------- | -------- | ------- | -------------------------------------------------------------- |
128
- | `name` | string | yes | — | Display name shown in the menu |
129
- | `command` | string or list of strings | yes | — | Executable to run (with optional inline arguments) |
130
- | `args` | list of strings | no | `[]` | Additional arguments appended after any inline command args |
131
- | `description` | string | no | `""` | Short description shown next to the name |
132
- | `working_directory` | string | no | — | Working directory for the subprocess |
133
- | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
129
+ | Key | Type | Required | Default | Description |
130
+ | ------------------- | --------------- | -------- | ------- | -------------------------------------------------------------- |
131
+ | `name` | string | yes | — | Display name shown in the menu |
132
+ | `command` | string | yes | — | Executable to run |
133
+ | `args` | list of strings | no | `[]` | Arguments passed to the command |
134
+ | `description` | string | no | `""` | Short description shown next to the name |
135
+ | `working_directory` | string | no | — | Working directory for the subprocess |
136
+ | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
137
+ | `id` | string | no | `""` | Stable per-menu identity for refreshed selection |
138
+ | `menu` | table | no | - | Submenu declaration, exclusive with command fields |
134
139
 
135
140
  ### Validation Rules
136
141
 
137
- - At least one `[[entries]]` table is required.
138
- - Every entry must have both `name` and `command`.
139
- - `command` may be a string (`"pwsh"`) or a list (`["ssh", "-t", "host"]`).
140
- When a list, the first element is the executable and the rest are arguments.
142
+ - A root requires nonempty `[[entries]]` or a `[provider]` declaration.
143
+ - Every entry needs `name` and either `command` or `menu`.
141
144
  - `on_exit` must be `"restart"` or `"exit"`.
142
- - `args` must be a list (not a bare string). These are appended after any
143
- arguments provided inline in a `command` list.
145
+ - `args` must be a list (not a bare string).
144
146
  - `env` must be a TOML table (not a string or list).
145
147
  - If `working_directory` does not exist at load time, it is silently reset to
146
148
  `None` (a warning is logged).
@@ -171,10 +173,6 @@ name = "PowerShell"
171
173
  command = "pwsh"
172
174
  args = ["-NoProfile"]
173
175
  env = { TERM = "xterm-256color" }
174
-
175
- [[entries]]
176
- name = "Remote Server"
177
- command = ["ssh", "-t", "my server", "bash"]
178
176
  ```
179
177
 
180
178
  ## Usage
@@ -220,3 +218,30 @@ uv run mypy src
220
218
  ## License
221
219
 
222
220
  MIT
221
+
222
+ ## See Also
223
+
224
+ - **[LaunchLine (Rust)](https://github.com/mikejhill/launchline-rust)** — a Rust
225
+ implementation with the shared configuration and keyboard contract,
226
+ ships as a single native binary.
227
+
228
+ ## Compatibility and validation
229
+
230
+ Use `launchline --check-config` to validate configuration without opening the menu.
231
+ Set `NO_COLOR` for a selection marker without ANSI color. See the
232
+ [compatibility matrix](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/compatibility.md), [reproducible test and performance
233
+ results](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/validation/report.md), and [release readiness](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/releasing.md).
234
+
235
+ ### `[provider]`
236
+
237
+ The same table is accepted under `entries.menu.provider`. See the
238
+ [menu lifecycle and provider contract](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/menus.md) for nesting, caching,
239
+ refresh/back behavior, limits, errors and examples.
240
+
241
+ | Field | Type | Default | Description |
242
+ | --- | --- | --- | --- |
243
+ | `command` | string | required | Explicit trusted provider executable |
244
+ | `args` | string array | `[]` | Literal argv |
245
+ | `env` | string table | `{}` | Environment overlay |
246
+ | `working_directory` | string | inherited | Provider working directory |
247
+ | `timeout_seconds` | integer | `10` | Deadline from 1 to 300 seconds |
@@ -2,12 +2,14 @@
2
2
 
3
3
  [![CI](https://github.com/mikejhill/launchline/actions/workflows/ci.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/ci.yml)
4
4
  [![Release](https://github.com/mikejhill/launchline/actions/workflows/release.yml/badge.svg)](https://github.com/mikejhill/launchline/actions/workflows/release.yml)
5
+ [![PyPI](https://img.shields.io/pypi/v/launchline)](https://pypi.org/project/launchline/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/launchline)](https://pypi.org/project/launchline/)
5
7
 
6
8
  A lightweight terminal launcher for interactive CLI tools. Define your frequently
7
9
  used commands in a TOML config file, then pick them from a fuzzy-searchable menu
8
10
  instead of remembering flags and paths.
9
11
 
10
- ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/demo.gif)
12
+ ![LaunchLine demo](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/images/demo.gif)
11
13
 
12
14
  ## Why
13
15
 
@@ -28,7 +30,7 @@ instead.
28
30
 
29
31
  | Fuzzy search | Arrow-key navigation |
30
32
  | :-------------------------------: | :-----------------------------------: |
31
- | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.2.0/docs/images/navigate.png) |
33
+ | ![Search](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/images/search.png) | ![Navigate](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/images/navigate.png) |
32
34
 
33
35
  ## Installation
34
36
 
@@ -96,29 +98,29 @@ Config file format is [TOML](https://toml.io). The file has an optional
96
98
  | `clear_on_launch` | bool | `true` | Clear terminal before launching an entry |
97
99
  | `ghost_text` | bool | `true` | Show the highlighted entry name as an autocomplete hint on prompt |
98
100
  | `numeric_trigger` | bool | `true` | Pressing a digit immediately launches the matching entry |
101
+ | `refresh_on_return` | boolean | `false` | Regenerate a dynamic root after returning from a submenu |
99
102
 
100
103
  ### `[[entries]]`
101
104
 
102
105
  Each `[[entries]]` table defines one launchable tool:
103
106
 
104
- | Key | Type | Required | Default | Description |
105
- | ------------------- | ------------------------- | -------- | ------- | -------------------------------------------------------------- |
106
- | `name` | string | yes | — | Display name shown in the menu |
107
- | `command` | string or list of strings | yes | — | Executable to run (with optional inline arguments) |
108
- | `args` | list of strings | no | `[]` | Additional arguments appended after any inline command args |
109
- | `description` | string | no | `""` | Short description shown next to the name |
110
- | `working_directory` | string | no | — | Working directory for the subprocess |
111
- | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
107
+ | Key | Type | Required | Default | Description |
108
+ | ------------------- | --------------- | -------- | ------- | -------------------------------------------------------------- |
109
+ | `name` | string | yes | — | Display name shown in the menu |
110
+ | `command` | string | yes | — | Executable to run |
111
+ | `args` | list of strings | no | `[]` | Arguments passed to the command |
112
+ | `description` | string | no | `""` | Short description shown next to the name |
113
+ | `working_directory` | string | no | — | Working directory for the subprocess |
114
+ | `env` | table | no | `{}` | Extra environment variables (`KEY = "value"`) |
115
+ | `id` | string | no | `""` | Stable per-menu identity for refreshed selection |
116
+ | `menu` | table | no | - | Submenu declaration, exclusive with command fields |
112
117
 
113
118
  ### Validation Rules
114
119
 
115
- - At least one `[[entries]]` table is required.
116
- - Every entry must have both `name` and `command`.
117
- - `command` may be a string (`"pwsh"`) or a list (`["ssh", "-t", "host"]`).
118
- When a list, the first element is the executable and the rest are arguments.
120
+ - A root requires nonempty `[[entries]]` or a `[provider]` declaration.
121
+ - Every entry needs `name` and either `command` or `menu`.
119
122
  - `on_exit` must be `"restart"` or `"exit"`.
120
- - `args` must be a list (not a bare string). These are appended after any
121
- arguments provided inline in a `command` list.
123
+ - `args` must be a list (not a bare string).
122
124
  - `env` must be a TOML table (not a string or list).
123
125
  - If `working_directory` does not exist at load time, it is silently reset to
124
126
  `None` (a warning is logged).
@@ -149,10 +151,6 @@ name = "PowerShell"
149
151
  command = "pwsh"
150
152
  args = ["-NoProfile"]
151
153
  env = { TERM = "xterm-256color" }
152
-
153
- [[entries]]
154
- name = "Remote Server"
155
- command = ["ssh", "-t", "my server", "bash"]
156
154
  ```
157
155
 
158
156
  ## Usage
@@ -198,3 +196,30 @@ uv run mypy src
198
196
  ## License
199
197
 
200
198
  MIT
199
+
200
+ ## See Also
201
+
202
+ - **[LaunchLine (Rust)](https://github.com/mikejhill/launchline-rust)** — a Rust
203
+ implementation with the shared configuration and keyboard contract,
204
+ ships as a single native binary.
205
+
206
+ ## Compatibility and validation
207
+
208
+ Use `launchline --check-config` to validate configuration without opening the menu.
209
+ Set `NO_COLOR` for a selection marker without ANSI color. See the
210
+ [compatibility matrix](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/compatibility.md), [reproducible test and performance
211
+ results](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/validation/report.md), and [release readiness](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/releasing.md).
212
+
213
+ ### `[provider]`
214
+
215
+ The same table is accepted under `entries.menu.provider`. See the
216
+ [menu lifecycle and provider contract](https://raw.githubusercontent.com/mikejhill/launchline/v0.4.0/docs/menus.md) for nesting, caching,
217
+ refresh/back behavior, limits, errors and examples.
218
+
219
+ | Field | Type | Default | Description |
220
+ | --- | --- | --- | --- |
221
+ | `command` | string | required | Explicit trusted provider executable |
222
+ | `args` | string array | `[]` | Literal argv |
223
+ | `env` | string table | `{}` | Environment overlay |
224
+ | `working_directory` | string | inherited | Provider working directory |
225
+ | `timeout_seconds` | integer | `10` | Deadline from 1 to 300 seconds |
@@ -0,0 +1,39 @@
1
+ # Shared compatibility contract
2
+
3
+ Baseline: Python `904ace1`, Rust `109a68b`. Modernized together on MHILL-LAPTOP.
4
+ This matrix records the supported contract and evidence, not universal parity.
5
+
6
+ | Surface | Baseline gap | Implemented contract | Evidence |
7
+ | --- | --- | --- | --- |
8
+ | Configuration | Python coerced types; Rust discarded invalid args/env | Strict strings, booleans, string arrays/maps; reject controls/NUL and illegal environment keys | Both unit suites; 15 differential real CLI cases in validation/parity.json |
9
+ | Resolution | Same CLI > LAUNCHLINE_CONFIG > default precedence | Explicit missing paths fail; exclusive default creation; missing/file cwd warns and inherits | Config tests; actual CLI and child processes |
10
+ | Search | Equivalent greedy subsequence scoring | Identical score bonuses and stable ties; monotonic search avoids tail scans | Shared 405-case ASCII/Unicode score corpus; paired benchmarks |
11
+ | Numeric selection | Python non-ASCII digit crash | ASCII digits only, existing prefix and shortcut semantics | Keyboard and mutation tests |
12
+ | Editing | Rust UTF-8 byte cursor could panic | Character boundary editing; Emacs keys, arrows, Escape clear/quit | Rust unit UX; Python decoder/UI tests; real PTY flows |
13
+ | CLI | Rust accepted arbitrary log levels; Python config-path tracebacks | Same five uppercase levels; friendly failures; --check-config validates without opening TUI; non-TTY guard | Actual subprocess and wheel smoke tests |
14
+ | Launch | Rust cloned entire environment; POSIX status representation differed | Literal argv, inherited env plus overlay, cwd, child exit code; interrupted child 130; POSIX signal 128+signal | Actual child probe; Python Linux SIGTERM test; Rust Unix test added but not run locally |
15
+ | Terminal | Windows inherited ignore flag; Unix queued input loss | Enable own Ctrl+C; restore terminal; preserve queued UTF-8 input; stop on EOF/read error | Windows PTY both; Linux PTY Python; faithful OS-boundary simulations |
16
+ | Accessibility | Color-only emphasis | NO_COLOR suppresses SGR with visible selection marker; keyboard navigation and resize | PTY output/assertions both; no screen-reader certification |
17
+ | Distribution | Rust archives lacked icons; wrapper used wrong version metadata | Bundle icons; wrapper uses launchline-rust version, verifies SHA256SUMS before atomic cache install | Built native archive and isolated installed wheel; tamper/interruption tests |
18
+ | Release | Python #1 pending; Rust broad permissions | Preserve #1 pin/generated notes; owner + rerun owner gates, exact SHA, read-only build, isolated publishers, provenance | actionlint, zizmor auditor, mocked actual publisher body, no-remote semantic-release fixture |
19
+
20
+ Unknown TOML keys remain ignored for compatibility. Shell expansion is not supported.
21
+ Portable home expansion is `~`/`~/...`; Python also recognizes OS-supported named-user
22
+ forms. Diagnostic wording, argparse/clap help formatting, platform process lookup,
23
+ batch execution, Unicode database versions, terminal escape timing and font widths
24
+ can differ. Cursor accounting uses code points rather than grapheme/display columns;
25
+ wide/combining glyph layout is not claimed equivalent. Python needs CPython >=3.12;
26
+ Rust is native, with a separate Python >=3.9 download wrapper. Binary and interpreter
27
+ startup/memory costs therefore differ. No maximum-performance claim is made.
28
+
29
+ Executed: Windows x64 both; Ubuntu/WSL Python. macOS, ARM targets, Rust Linux/macOS,
30
+ physical terminal/screen-reader combinations and hosted CI have not been executed
31
+ here. See [validation report](validation/report.md) and [release readiness](releasing.md).
32
+
33
+ ## Nested and dynamic menu extension
34
+
35
+ See the [shared lifecycle and provider schema](menus.md) and
36
+ [feature validation matrix](validation/menu-checkpoint.md) for current evidence.
37
+ The earlier matrix above describes the modernization baseline; feature-head hosted
38
+ CI and ordered releases are tracked separately. Windows raw/Kitty input differences
39
+ are explicitly documented rather than asserted equivalent.