devstuff 2.0.0__tar.gz → 2.1.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.
- {devstuff-2.0.0 → devstuff-2.1.0}/CHANGELOG.md +6 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/CLAUDE.md +8 -1
- {devstuff-2.0.0 → devstuff-2.1.0}/PKG-INFO +30 -1
- {devstuff-2.0.0 → devstuff-2.1.0}/README.md +29 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/pyproject.toml +1 -1
- devstuff-2.1.0/src/dev_setup/functions.yaml +240 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_functions.py +125 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/uv.lock +1 -1
- devstuff-2.0.0/src/dev_setup/functions.yaml +0 -111
- {devstuff-2.0.0 → devstuff-2.1.0}/.claude/skills/run-stuff/SKILL.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/.claude/skills/run-stuff/smoke.sh +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/.github/workflows/bump.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/.github/workflows/ci.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/.github/workflows/publish.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/.github/workflows/test-installs.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/.gitignore +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/dev/Dockerfile +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/dev/Dockerfile.ci +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/dev/Makefile +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/dev/docker-compose.yml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/devstuff +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/README.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/agent/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/agent/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/agent/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/ansible-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/ansible-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/ansible-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/bat-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/bat-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/bat-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/commitizen-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/commitizen-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/commitizen-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/docker-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/docker-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/docker-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/lazygit-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/lazygit-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/lazygit-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/precommit-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/precommit-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/precommit-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/starship-config/development-plan.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/starship-config/specifications.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/docs/specs/starship-config/stack-decisions.md +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/install.sh +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/__main__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/approval.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/bridges.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/completion.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/config.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/loop.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/ollama.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/preflight.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/primitives.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/registry.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/sandbox.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/session.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/transcript.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent_tools.schema.json +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/agent_tools.yaml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/base.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/cli.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/add_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/agent_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/catalog_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/configure_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/delete_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/docs_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/doctor_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/functions_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/help_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/install_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/list_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/remove_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/run_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/skills_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/commands/update_cmd.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/ansible/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/ansible/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/ansible/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/ansible/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/ansible/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/ansible/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/bat/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/bat/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/bat/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/bat/preview.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/bat/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/bat/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/commitizen/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/commitizen/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/commitizen/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/commitizen/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/commitizen/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/commitizen/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/docker/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/docker/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/docker/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/docker/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/docker/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/docker/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/lazygit/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/lazygit/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/lazygit/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/lazygit/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/lazygit/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/lazygit/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/precommit/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/precommit/detect.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/precommit/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/precommit/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/precommit/validate.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/precommit/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/starship/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/starship/fonts.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/starship/model.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/starship/preview.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/starship/render.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/configure/starship/wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/doctor.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/function_runner.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/functions.schema.json +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/functions_catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/functions_registry.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/generic.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/registry.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/tools.yaml +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/src/dev_setup/ui.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/integration/__init__.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/integration/conftest.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/integration/test_agent_smoke.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/integration/test_agent_tui.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/integration/test_commitizen_config.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/integration/test_tools.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_agent.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_agent_completion.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_agent_loop.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_agent_sandbox.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_agent_tools.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_agent_wizard.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_catalog.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_ansible.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_bat.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_commitizen.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_docker.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_lazygit.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_precommit.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_configure_starship.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_doctor.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_generic.py +0 -0
- {devstuff-2.0.0 → devstuff-2.1.0}/tests/test_registry.py +0 -0
|
@@ -165,7 +165,14 @@ another CLI should guard on `command -v <tool>` and point at `devstuff install <
|
|
|
165
165
|
error rather than let a raw "command not found" surface — see `validate-yaml`/`aws-saml-reauth`
|
|
166
166
|
in `functions.yaml`. If that CLI is only reachable via nvm (like `pi`), source
|
|
167
167
|
`"$HOME/.nvm/nvm.sh"` first (see `acc-check`) — `script`-type functions run via a non-login,
|
|
168
|
-
non-interactive `bash <tmpfile>`, so `~/.bashrc`/nvm's shell init never runs on their own.
|
|
168
|
+
non-interactive `bash <tmpfile>`, so `~/.bashrc`/nvm's shell init never runs on their own. When
|
|
169
|
+
the CLI isn't a catalog tool at all, name the distro package instead (`whats-on-port` points at
|
|
170
|
+
`apt-get install iproute2`) — pointing at `devstuff install ss` would be a lie.
|
|
171
|
+
|
|
172
|
+
**`run_cmd` flattens every non-zero exit to 1**, so a `script` function cannot signal a *result*
|
|
173
|
+
through its exit code — only whether it ran. A "found nothing" answer should therefore exit 0 and
|
|
174
|
+
say so, or the user gets a red "command failed" banner under a correct result; keep non-zero for
|
|
175
|
+
"could not perform the lookup" (see `whats-on-port`'s comment on this).
|
|
169
176
|
|
|
170
177
|
Not yet built: an `add` wizard and `catalog import`/`export` for functions, analogous to the
|
|
171
178
|
ones tools already have.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devstuff
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Development environment setup CLI for Linux
|
|
5
5
|
Project-URL: Repository, https://github.com/thesawdawg/dev-setup-py
|
|
6
6
|
Author-email: Sawyer <sawyerksu@gmail.com>
|
|
@@ -1105,9 +1105,38 @@ ones tools already have — for now, custom functions are hand-edited YAML at
|
|
|
1105
1105
|
| `ssh-agent-key` | auth | shell-eval (bashrc) | Start ssh-agent in the current shell and add a key to it | `key_path` |
|
|
1106
1106
|
| `validate-docker-compose` | validation | script | Validate a docker-compose.yml file in the current directory | — |
|
|
1107
1107
|
| `validate-yaml` | validation | script | Validate a YAML file's syntax using `yq` | `file` |
|
|
1108
|
+
| `whats-on-port` | network | script | Find which process is listening on a port | `port`, `protocol` (optional: `tcp`/`udp`/`all`) |
|
|
1108
1109
|
| `acc-check` | web-dev | script | Run the pi coding agent's `/dogfood` skill against a web URL | `url`, `instruction` (optional) |
|
|
1109
1110
|
| `aws-saml-reauth` | web-dev | script | Reauthorize the AWS CLI via `saml2aws login --force` | `profile` (optional) |
|
|
1110
1111
|
|
|
1112
|
+
#### `whats-on-port`
|
|
1113
|
+
|
|
1114
|
+
```bash
|
|
1115
|
+
devstuff run whats-on-port 8080 # tcp and udp
|
|
1116
|
+
devstuff run whats-on-port 5432 tcp # one protocol
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
Prints the matching sockets, then the PID, user, elapsed time and **untruncated** command
|
|
1120
|
+
line of every process holding them — plural because `SO_REUSEPORT` lets several processes
|
|
1121
|
+
share one port, and "which of these five node processes" is usually the actual question.
|
|
1122
|
+
|
|
1123
|
+
It uses `ss` rather than `lsof` or `fuser` for one measured reason: **run unprivileged
|
|
1124
|
+
against another user's socket, `lsof` and `fuser` print nothing and exit as though the port
|
|
1125
|
+
were free.** `ss` still lists the socket, just without the `users:((...))` field — so the
|
|
1126
|
+
function can tell "nothing there" apart from "something there I'm not allowed to name", and
|
|
1127
|
+
says which. When the process is hidden it retries under `sudo -n`, and only if that needs no
|
|
1128
|
+
password; a diagnostic shouldn't stop to prompt for one. If sudo isn't available it says
|
|
1129
|
+
what to re-run.
|
|
1130
|
+
|
|
1131
|
+
"Nothing is listening" is reported as a success, not a failure, along with the two reasons a
|
|
1132
|
+
bind can still fail afterwards: a container port published with `userland-proxy: false` is
|
|
1133
|
+
NAT-forwarded with no host socket to find at all, and sockets in another network namespace
|
|
1134
|
+
are invisible. If Docker is running, published container ports are matched and named too —
|
|
1135
|
+
`docker-proxy`'s own command line doesn't say which container it belongs to.
|
|
1136
|
+
|
|
1137
|
+
`ss` comes from `iproute2`, which isn't a devstuff package and isn't on every minimal image,
|
|
1138
|
+
so a missing `ss` names the apt package instead of surfacing "command not found".
|
|
1139
|
+
|
|
1111
1140
|
---
|
|
1112
1141
|
|
|
1113
1142
|
## Built-in packages
|
|
@@ -1078,9 +1078,38 @@ ones tools already have — for now, custom functions are hand-edited YAML at
|
|
|
1078
1078
|
| `ssh-agent-key` | auth | shell-eval (bashrc) | Start ssh-agent in the current shell and add a key to it | `key_path` |
|
|
1079
1079
|
| `validate-docker-compose` | validation | script | Validate a docker-compose.yml file in the current directory | — |
|
|
1080
1080
|
| `validate-yaml` | validation | script | Validate a YAML file's syntax using `yq` | `file` |
|
|
1081
|
+
| `whats-on-port` | network | script | Find which process is listening on a port | `port`, `protocol` (optional: `tcp`/`udp`/`all`) |
|
|
1081
1082
|
| `acc-check` | web-dev | script | Run the pi coding agent's `/dogfood` skill against a web URL | `url`, `instruction` (optional) |
|
|
1082
1083
|
| `aws-saml-reauth` | web-dev | script | Reauthorize the AWS CLI via `saml2aws login --force` | `profile` (optional) |
|
|
1083
1084
|
|
|
1085
|
+
#### `whats-on-port`
|
|
1086
|
+
|
|
1087
|
+
```bash
|
|
1088
|
+
devstuff run whats-on-port 8080 # tcp and udp
|
|
1089
|
+
devstuff run whats-on-port 5432 tcp # one protocol
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
Prints the matching sockets, then the PID, user, elapsed time and **untruncated** command
|
|
1093
|
+
line of every process holding them — plural because `SO_REUSEPORT` lets several processes
|
|
1094
|
+
share one port, and "which of these five node processes" is usually the actual question.
|
|
1095
|
+
|
|
1096
|
+
It uses `ss` rather than `lsof` or `fuser` for one measured reason: **run unprivileged
|
|
1097
|
+
against another user's socket, `lsof` and `fuser` print nothing and exit as though the port
|
|
1098
|
+
were free.** `ss` still lists the socket, just without the `users:((...))` field — so the
|
|
1099
|
+
function can tell "nothing there" apart from "something there I'm not allowed to name", and
|
|
1100
|
+
says which. When the process is hidden it retries under `sudo -n`, and only if that needs no
|
|
1101
|
+
password; a diagnostic shouldn't stop to prompt for one. If sudo isn't available it says
|
|
1102
|
+
what to re-run.
|
|
1103
|
+
|
|
1104
|
+
"Nothing is listening" is reported as a success, not a failure, along with the two reasons a
|
|
1105
|
+
bind can still fail afterwards: a container port published with `userland-proxy: false` is
|
|
1106
|
+
NAT-forwarded with no host socket to find at all, and sockets in another network namespace
|
|
1107
|
+
are invisible. If Docker is running, published container ports are matched and named too —
|
|
1108
|
+
`docker-proxy`'s own command line doesn't say which container it belongs to.
|
|
1109
|
+
|
|
1110
|
+
`ss` comes from `iproute2`, which isn't a devstuff package and isn't on every minimal image,
|
|
1111
|
+
so a missing `ss` names the apt package instead of surfacing "command not found".
|
|
1112
|
+
|
|
1084
1113
|
---
|
|
1085
1114
|
|
|
1086
1115
|
## Built-in packages
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# yaml-language-server: $schema=./functions.schema.json
|
|
2
|
+
version: 1
|
|
3
|
+
functions:
|
|
4
|
+
ssh-agent-key:
|
|
5
|
+
name: SSH Agent + Add Key
|
|
6
|
+
description: Start ssh-agent in the current shell and add a key to it
|
|
7
|
+
category: auth
|
|
8
|
+
type: shell-eval
|
|
9
|
+
register: bashrc
|
|
10
|
+
params:
|
|
11
|
+
- name: key_path
|
|
12
|
+
description: Path to the SSH private key
|
|
13
|
+
required: true
|
|
14
|
+
script: |
|
|
15
|
+
eval "$(ssh-agent -s)"
|
|
16
|
+
ssh-add "$key_path"
|
|
17
|
+
docs_url: https://www.ssh.com/academy/ssh/agent
|
|
18
|
+
|
|
19
|
+
validate-docker-compose:
|
|
20
|
+
name: Validate Docker Compose
|
|
21
|
+
description: Validate a docker-compose.yml file in the current directory
|
|
22
|
+
category: validation
|
|
23
|
+
type: script
|
|
24
|
+
script: |
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
27
|
+
echo "docker is required. Install it first: devstuff install docker" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
file=""
|
|
31
|
+
for candidate in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
|
|
32
|
+
if [ -f "$candidate" ]; then
|
|
33
|
+
file="$candidate"
|
|
34
|
+
break
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
if [ -z "$file" ]; then
|
|
38
|
+
echo "No docker-compose.yml/docker-compose.yaml/compose.yml/compose.yaml found in $(pwd)" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
echo "Validating $file..."
|
|
42
|
+
docker compose -f "$file" config --quiet
|
|
43
|
+
echo "$file is valid"
|
|
44
|
+
docs_url: https://docs.docker.com/reference/cli/docker/compose/config/
|
|
45
|
+
|
|
46
|
+
validate-yaml:
|
|
47
|
+
name: Validate YAML
|
|
48
|
+
description: Validate a YAML file's syntax using yq
|
|
49
|
+
category: validation
|
|
50
|
+
type: script
|
|
51
|
+
params:
|
|
52
|
+
- name: file
|
|
53
|
+
description: Path to the YAML file to validate
|
|
54
|
+
required: true
|
|
55
|
+
script: |
|
|
56
|
+
set -euo pipefail
|
|
57
|
+
if ! command -v yq >/dev/null 2>&1; then
|
|
58
|
+
echo "yq is required. Install it first: devstuff install yq" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
if [ ! -f "$file" ]; then
|
|
62
|
+
echo "File not found: $file" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
yq eval '.' "$file" > /dev/null
|
|
66
|
+
echo "$file is valid YAML"
|
|
67
|
+
docs_url: https://mikefarah.gitbook.io/yq
|
|
68
|
+
|
|
69
|
+
acc-check:
|
|
70
|
+
name: Accessibility/QA Check
|
|
71
|
+
description: Run the pi coding agent's /dogfood skill against a web URL
|
|
72
|
+
category: web-dev
|
|
73
|
+
type: script
|
|
74
|
+
params:
|
|
75
|
+
- name: url
|
|
76
|
+
description: URL to review
|
|
77
|
+
required: true
|
|
78
|
+
- name: instruction
|
|
79
|
+
description: Additional scope or instruction for the review (optional)
|
|
80
|
+
required: false
|
|
81
|
+
script: |
|
|
82
|
+
set -euo pipefail
|
|
83
|
+
. "$HOME/.nvm/nvm.sh" 2>/dev/null || true
|
|
84
|
+
if ! command -v pi >/dev/null 2>&1; then
|
|
85
|
+
echo "pi is required. Install it first: devstuff install pi" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
pi -p "/dogfood $url $instruction"
|
|
89
|
+
docs_url: https://pi.dev/docs/latest
|
|
90
|
+
|
|
91
|
+
whats-on-port:
|
|
92
|
+
name: What's On This Port
|
|
93
|
+
description: Find which process is listening on a port (and whether you can see it)
|
|
94
|
+
category: network
|
|
95
|
+
type: script
|
|
96
|
+
params:
|
|
97
|
+
- name: port
|
|
98
|
+
description: Port number to look up (1-65535)
|
|
99
|
+
required: true
|
|
100
|
+
- name: protocol
|
|
101
|
+
description: tcp, udp, or all (default all)
|
|
102
|
+
required: false
|
|
103
|
+
default: all
|
|
104
|
+
script: |
|
|
105
|
+
# Deliberately no `set -e`: this script's whole job is to tell "in use" from
|
|
106
|
+
# "free" from "cannot tell", which is exit-code work that -e would abort.
|
|
107
|
+
set -uo pipefail
|
|
108
|
+
|
|
109
|
+
case "$port" in
|
|
110
|
+
''|*[!0-9]*)
|
|
111
|
+
echo "Port must be a number between 1 and 65535, got: ${port:-<empty>}" >&2
|
|
112
|
+
exit 2
|
|
113
|
+
;;
|
|
114
|
+
esac
|
|
115
|
+
if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
|
|
116
|
+
echo "Port must be between 1 and 65535, got: $port" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
case "${protocol:-all}" in
|
|
121
|
+
tcp) flags="-tlpnH" ;;
|
|
122
|
+
udp) flags="-ulpnH" ;;
|
|
123
|
+
all|"") flags="-tulpnH"; protocol="all" ;;
|
|
124
|
+
*) echo "Protocol must be tcp, udp or all, got: $protocol" >&2; exit 2 ;;
|
|
125
|
+
esac
|
|
126
|
+
|
|
127
|
+
# ss ships in iproute2, which is not in the devstuff catalog and is not on every
|
|
128
|
+
# minimal image (this repo's own CI container has lsof but no ss), so say what to
|
|
129
|
+
# install rather than letting "command not found" surface.
|
|
130
|
+
if ! command -v ss >/dev/null 2>&1; then
|
|
131
|
+
echo "ss is required to look up sockets (it is part of iproute2)." >&2
|
|
132
|
+
echo "Install it with: sudo apt-get install -y iproute2" >&2
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# `ss` exits 0 whether or not the filter matched anything, so an empty result is
|
|
137
|
+
# the only signal that the port is free.
|
|
138
|
+
sockets="$(ss $flags "sport = :$port" 2>/dev/null || true)"
|
|
139
|
+
|
|
140
|
+
# Unprivileged `ss` still lists a socket owned by another user but silently drops
|
|
141
|
+
# the users:((...)) field, so the port reads as taken with nothing named. Retry
|
|
142
|
+
# under sudo only when it needs no password — a diagnostic should not stop to ask
|
|
143
|
+
# for one. (lsof and fuser are worse here: both report *nothing* and exit as if
|
|
144
|
+
# the port were free, which is why this uses ss.)
|
|
145
|
+
escalated=""
|
|
146
|
+
if [ -n "$sockets" ] && ! printf '%s' "$sockets" | grep -q 'pid='; then
|
|
147
|
+
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
|
|
148
|
+
sockets="$(sudo -n ss $flags "sport = :$port" 2>/dev/null || printf '%s' "$sockets")"
|
|
149
|
+
escalated="yes"
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# "Nothing found" is an answer, not a failure: `devstuff run` flattens every
|
|
154
|
+
# non-zero code to 1, so exiting non-zero here would only buy a scary "command
|
|
155
|
+
# failed" banner under a correct result. Non-zero is kept for the cases where
|
|
156
|
+
# the lookup could not be performed at all (bad input, no ss).
|
|
157
|
+
if [ -z "$sockets" ]; then
|
|
158
|
+
echo "Nothing is listening on port $port ($protocol)."
|
|
159
|
+
echo
|
|
160
|
+
echo "A bind can still fail after this: a container port published without the"
|
|
161
|
+
echo "userland proxy is forwarded by NAT with no host socket to find, and sockets"
|
|
162
|
+
echo "in another network namespace are invisible from here."
|
|
163
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
164
|
+
echo "Sockets you have no permission to see are not listed either — to rule"
|
|
165
|
+
echo "that out: sudo devstuff run whats-on-port $port"
|
|
166
|
+
fi
|
|
167
|
+
exit 0
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
echo "Port $port ($protocol) is in use:"
|
|
171
|
+
echo
|
|
172
|
+
printf '%s\n' "$sockets" | sed 's/^/ /'
|
|
173
|
+
|
|
174
|
+
# One port can have several holders (SO_REUSEPORT), so this reports every PID
|
|
175
|
+
# rather than assuming a single answer.
|
|
176
|
+
pids="$(printf '%s\n' "$sockets" | grep -oE 'pid=[0-9]+' | cut -d= -f2 | sort -un)"
|
|
177
|
+
if [ -n "$pids" ]; then
|
|
178
|
+
echo
|
|
179
|
+
echo "Holding process(es):"
|
|
180
|
+
printf ' %-8s %-10s %-10s %s\n' PID USER ELAPSED COMMAND
|
|
181
|
+
for pid in $pids; do
|
|
182
|
+
# The full command line is never truncated: it is usually the thing that
|
|
183
|
+
# identifies which of five node processes this actually is.
|
|
184
|
+
ps -o pid=,user=,etime=,args= -p "$pid" 2>/dev/null \
|
|
185
|
+
| awk '{printf " %-8s %-10s %-10s ", $1, $2, $3; $1=$2=$3=""; sub(/^ +/, ""); print}'
|
|
186
|
+
done
|
|
187
|
+
if [ -n "$escalated" ]; then
|
|
188
|
+
echo
|
|
189
|
+
echo " (the process details above needed root — re-read via sudo)"
|
|
190
|
+
fi
|
|
191
|
+
else
|
|
192
|
+
echo
|
|
193
|
+
echo "The socket exists, but its owning process belongs to another user and is"
|
|
194
|
+
echo "not visible from here. Re-run as root to name it:"
|
|
195
|
+
echo " sudo devstuff run whats-on-port $port"
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Counted on the listener's side only: matching both sport and dport counts each
|
|
199
|
+
# connection twice, once per end of a loopback pair.
|
|
200
|
+
conns="$(ss -tnpH "sport = :$port" 2>/dev/null | grep -c ESTAB || true)"
|
|
201
|
+
if [ "${conns:-0}" -gt 0 ]; then
|
|
202
|
+
echo
|
|
203
|
+
echo "$conns established connection(s) to this port — list them with:"
|
|
204
|
+
echo " ss -tnp 'sport = :$port'"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# Best-effort: a published container port is held by docker-proxy, whose command
|
|
208
|
+
# line does not say which container it belongs to. Silent on any failure, so no
|
|
209
|
+
# daemon (or no docker at all) costs nothing.
|
|
210
|
+
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
|
211
|
+
containers="$(docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null | grep -E ":$port->" || true)"
|
|
212
|
+
if [ -n "$containers" ]; then
|
|
213
|
+
echo
|
|
214
|
+
echo "Published by container(s):"
|
|
215
|
+
printf '%s\n' "$containers" | sed 's/^/ /'
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
docs_url: https://man7.org/linux/man-pages/man8/ss.8.html
|
|
219
|
+
|
|
220
|
+
aws-saml-reauth:
|
|
221
|
+
name: AWS SAML Reauth
|
|
222
|
+
description: Reauthorize the AWS CLI via saml2aws
|
|
223
|
+
category: web-dev
|
|
224
|
+
type: script
|
|
225
|
+
params:
|
|
226
|
+
- name: profile
|
|
227
|
+
description: AWS profile to reauthenticate (optional, uses saml2aws default if omitted)
|
|
228
|
+
required: false
|
|
229
|
+
script: |
|
|
230
|
+
set -euo pipefail
|
|
231
|
+
if ! command -v saml2aws >/dev/null 2>&1; then
|
|
232
|
+
echo "saml2aws is required. Install it first: devstuff install saml2aws" >&2
|
|
233
|
+
exit 1
|
|
234
|
+
fi
|
|
235
|
+
if [ -n "$profile" ]; then
|
|
236
|
+
saml2aws login --force --profile "$profile"
|
|
237
|
+
else
|
|
238
|
+
saml2aws login --force
|
|
239
|
+
fi
|
|
240
|
+
docs_url: https://github.com/Versent/saml2aws
|
|
@@ -381,3 +381,128 @@ def test_captured_failure_carries_output_on_the_exception():
|
|
|
381
381
|
with pytest.raises(subprocess.CalledProcessError) as exc:
|
|
382
382
|
runner.run_script_function(fn, (), capture=True)
|
|
383
383
|
assert "why it failed" in exc.value.stdout
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# -- whats-on-port ------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _bundled(key: str) -> FunctionDef:
|
|
390
|
+
return FunctionDef.from_dict(catalog.load_bundled_catalog()[key], key=key)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def test_whats_on_port_is_declared_the_way_it_is_documented(isolated_catalog):
|
|
394
|
+
fns = catalog.load_bundled_catalog()
|
|
395
|
+
fn = fns["whats-on-port"]
|
|
396
|
+
assert fn["type"] == "script"
|
|
397
|
+
assert fn["category"] == "network"
|
|
398
|
+
port, protocol = fn["params"]
|
|
399
|
+
assert (port["name"], port["required"]) == ("port", True)
|
|
400
|
+
# Optional with a real default, so `devstuff run whats-on-port 8080` never prompts
|
|
401
|
+
# for a second argument.
|
|
402
|
+
assert (protocol["name"], protocol["required"], protocol["default"]) == (
|
|
403
|
+
"protocol", False, "all",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@pytest.mark.parametrize("key", sorted(catalog.load_bundled_catalog()))
|
|
408
|
+
def test_every_bundled_script_is_valid_bash(key):
|
|
409
|
+
"""`bash -n` over the script exactly as the runner assembles it — prelude included,
|
|
410
|
+
since that is what defines the param variables the body reads under `set -u`."""
|
|
411
|
+
import subprocess
|
|
412
|
+
|
|
413
|
+
fn = _bundled(key)
|
|
414
|
+
prelude = runner._positional_prelude(fn.params)
|
|
415
|
+
content = f"{prelude}\n{fn.script}" if prelude else fn.script
|
|
416
|
+
result = subprocess.run(
|
|
417
|
+
["bash", "-n"], input=content, capture_output=True, text=True, check=False
|
|
418
|
+
)
|
|
419
|
+
assert result.returncode == 0, f"{key}: {result.stderr}"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# The behavioural tests below bind a real socket and shell out to `ss`. No sudo and no
|
|
423
|
+
# network — but `ss` is not on every image (this repo's own CI container has lsof and
|
|
424
|
+
# no ss), which is the same gap the function itself guards against.
|
|
425
|
+
needs_ss = pytest.mark.skipif(
|
|
426
|
+
__import__("shutil").which("ss") is None, reason="ss (iproute2) is not installed"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@pytest.fixture()
|
|
431
|
+
def listening_port():
|
|
432
|
+
"""A real TCP listener owned by this process, and its port."""
|
|
433
|
+
import socket
|
|
434
|
+
|
|
435
|
+
sock = socket.socket()
|
|
436
|
+
sock.bind(("127.0.0.1", 0))
|
|
437
|
+
sock.listen(1)
|
|
438
|
+
try:
|
|
439
|
+
yield sock.getsockname()[1]
|
|
440
|
+
finally:
|
|
441
|
+
sock.close()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@pytest.fixture()
|
|
445
|
+
def free_port():
|
|
446
|
+
"""A port nothing holds: bound to claim it from the ephemeral range, then released.
|
|
447
|
+
|
|
448
|
+
`listening_port + 1` would do almost always, which is exactly the kind of almost
|
|
449
|
+
that fails in CI once a month.
|
|
450
|
+
"""
|
|
451
|
+
import socket
|
|
452
|
+
|
|
453
|
+
sock = socket.socket()
|
|
454
|
+
sock.bind(("127.0.0.1", 0))
|
|
455
|
+
port = sock.getsockname()[1]
|
|
456
|
+
sock.close()
|
|
457
|
+
return port
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _run(port, protocol="") -> str:
|
|
461
|
+
args = (str(port), protocol) if protocol else (str(port),)
|
|
462
|
+
return runner.run_script_function(_bundled("whats-on-port"), args, capture=True)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@needs_ss
|
|
466
|
+
def test_it_names_the_process_holding_the_port(listening_port):
|
|
467
|
+
import os
|
|
468
|
+
|
|
469
|
+
output = _run(listening_port)
|
|
470
|
+
assert f"Port {listening_port} (all) is in use" in output
|
|
471
|
+
# The point of the whole function: the PID, not just "something is there".
|
|
472
|
+
assert str(os.getpid()) in output
|
|
473
|
+
assert "Holding process(es):" in output
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@needs_ss
|
|
477
|
+
def test_a_free_port_is_a_successful_answer_not_a_failure(free_port):
|
|
478
|
+
"""`run_cmd` flattens every non-zero exit to 1, so exiting non-zero for "found
|
|
479
|
+
nothing" would only paint a red banner under a correct result."""
|
|
480
|
+
output = _run(free_port) # capture=True raises CalledProcessError on non-zero
|
|
481
|
+
assert f"Nothing is listening on port {free_port}" in output
|
|
482
|
+
# And it does not leave the user thinking a bind will now succeed.
|
|
483
|
+
assert "NAT" in output
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@needs_ss
|
|
487
|
+
def test_the_protocol_filter_excludes_the_other_protocol(listening_port):
|
|
488
|
+
assert "is in use" in _run(listening_port, "tcp")
|
|
489
|
+
assert "Nothing is listening" in _run(listening_port, "udp")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@pytest.mark.parametrize("port", ["abc", "0", "70000", "-1", "80.5"])
|
|
493
|
+
def test_an_impossible_port_is_rejected_before_anything_runs(port):
|
|
494
|
+
import subprocess
|
|
495
|
+
|
|
496
|
+
with pytest.raises(subprocess.CalledProcessError) as exc:
|
|
497
|
+
_run(port)
|
|
498
|
+
assert exc.value.returncode == 2
|
|
499
|
+
assert "between 1 and 65535" in exc.value.stdout + exc.value.stderr
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_an_unknown_protocol_is_rejected():
|
|
503
|
+
import subprocess
|
|
504
|
+
|
|
505
|
+
with pytest.raises(subprocess.CalledProcessError) as exc:
|
|
506
|
+
_run(8080, "sctp")
|
|
507
|
+
assert exc.value.returncode == 2
|
|
508
|
+
assert "tcp, udp or all" in exc.value.stdout + exc.value.stderr
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# yaml-language-server: $schema=./functions.schema.json
|
|
2
|
-
version: 1
|
|
3
|
-
functions:
|
|
4
|
-
ssh-agent-key:
|
|
5
|
-
name: SSH Agent + Add Key
|
|
6
|
-
description: Start ssh-agent in the current shell and add a key to it
|
|
7
|
-
category: auth
|
|
8
|
-
type: shell-eval
|
|
9
|
-
register: bashrc
|
|
10
|
-
params:
|
|
11
|
-
- name: key_path
|
|
12
|
-
description: Path to the SSH private key
|
|
13
|
-
required: true
|
|
14
|
-
script: |
|
|
15
|
-
eval "$(ssh-agent -s)"
|
|
16
|
-
ssh-add "$key_path"
|
|
17
|
-
docs_url: https://www.ssh.com/academy/ssh/agent
|
|
18
|
-
|
|
19
|
-
validate-docker-compose:
|
|
20
|
-
name: Validate Docker Compose
|
|
21
|
-
description: Validate a docker-compose.yml file in the current directory
|
|
22
|
-
category: validation
|
|
23
|
-
type: script
|
|
24
|
-
script: |
|
|
25
|
-
set -euo pipefail
|
|
26
|
-
if ! command -v docker >/dev/null 2>&1; then
|
|
27
|
-
echo "docker is required. Install it first: devstuff install docker" >&2
|
|
28
|
-
exit 1
|
|
29
|
-
fi
|
|
30
|
-
file=""
|
|
31
|
-
for candidate in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do
|
|
32
|
-
if [ -f "$candidate" ]; then
|
|
33
|
-
file="$candidate"
|
|
34
|
-
break
|
|
35
|
-
fi
|
|
36
|
-
done
|
|
37
|
-
if [ -z "$file" ]; then
|
|
38
|
-
echo "No docker-compose.yml/docker-compose.yaml/compose.yml/compose.yaml found in $(pwd)" >&2
|
|
39
|
-
exit 1
|
|
40
|
-
fi
|
|
41
|
-
echo "Validating $file..."
|
|
42
|
-
docker compose -f "$file" config --quiet
|
|
43
|
-
echo "$file is valid"
|
|
44
|
-
docs_url: https://docs.docker.com/reference/cli/docker/compose/config/
|
|
45
|
-
|
|
46
|
-
validate-yaml:
|
|
47
|
-
name: Validate YAML
|
|
48
|
-
description: Validate a YAML file's syntax using yq
|
|
49
|
-
category: validation
|
|
50
|
-
type: script
|
|
51
|
-
params:
|
|
52
|
-
- name: file
|
|
53
|
-
description: Path to the YAML file to validate
|
|
54
|
-
required: true
|
|
55
|
-
script: |
|
|
56
|
-
set -euo pipefail
|
|
57
|
-
if ! command -v yq >/dev/null 2>&1; then
|
|
58
|
-
echo "yq is required. Install it first: devstuff install yq" >&2
|
|
59
|
-
exit 1
|
|
60
|
-
fi
|
|
61
|
-
if [ ! -f "$file" ]; then
|
|
62
|
-
echo "File not found: $file" >&2
|
|
63
|
-
exit 1
|
|
64
|
-
fi
|
|
65
|
-
yq eval '.' "$file" > /dev/null
|
|
66
|
-
echo "$file is valid YAML"
|
|
67
|
-
docs_url: https://mikefarah.gitbook.io/yq
|
|
68
|
-
|
|
69
|
-
acc-check:
|
|
70
|
-
name: Accessibility/QA Check
|
|
71
|
-
description: Run the pi coding agent's /dogfood skill against a web URL
|
|
72
|
-
category: web-dev
|
|
73
|
-
type: script
|
|
74
|
-
params:
|
|
75
|
-
- name: url
|
|
76
|
-
description: URL to review
|
|
77
|
-
required: true
|
|
78
|
-
- name: instruction
|
|
79
|
-
description: Additional scope or instruction for the review (optional)
|
|
80
|
-
required: false
|
|
81
|
-
script: |
|
|
82
|
-
set -euo pipefail
|
|
83
|
-
. "$HOME/.nvm/nvm.sh" 2>/dev/null || true
|
|
84
|
-
if ! command -v pi >/dev/null 2>&1; then
|
|
85
|
-
echo "pi is required. Install it first: devstuff install pi" >&2
|
|
86
|
-
exit 1
|
|
87
|
-
fi
|
|
88
|
-
pi -p "/dogfood $url $instruction"
|
|
89
|
-
docs_url: https://pi.dev/docs/latest
|
|
90
|
-
|
|
91
|
-
aws-saml-reauth:
|
|
92
|
-
name: AWS SAML Reauth
|
|
93
|
-
description: Reauthorize the AWS CLI via saml2aws
|
|
94
|
-
category: web-dev
|
|
95
|
-
type: script
|
|
96
|
-
params:
|
|
97
|
-
- name: profile
|
|
98
|
-
description: AWS profile to reauthenticate (optional, uses saml2aws default if omitted)
|
|
99
|
-
required: false
|
|
100
|
-
script: |
|
|
101
|
-
set -euo pipefail
|
|
102
|
-
if ! command -v saml2aws >/dev/null 2>&1; then
|
|
103
|
-
echo "saml2aws is required. Install it first: devstuff install saml2aws" >&2
|
|
104
|
-
exit 1
|
|
105
|
-
fi
|
|
106
|
-
if [ -n "$profile" ]; then
|
|
107
|
-
saml2aws login --force --profile "$profile"
|
|
108
|
-
else
|
|
109
|
-
saml2aws login --force
|
|
110
|
-
fi
|
|
111
|
-
docs_url: https://github.com/Versent/saml2aws
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|