devstuff 1.13.1__py3-none-any.whl
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.
- dev_setup/__init__.py +6 -0
- dev_setup/__main__.py +9 -0
- dev_setup/base.py +74 -0
- dev_setup/catalog.py +188 -0
- dev_setup/cli.py +49 -0
- dev_setup/commands/__init__.py +0 -0
- dev_setup/commands/add_cmd.py +334 -0
- dev_setup/commands/catalog_cmd.py +37 -0
- dev_setup/commands/delete_cmd.py +36 -0
- dev_setup/commands/docs_cmd.py +34 -0
- dev_setup/commands/functions_cmd.py +87 -0
- dev_setup/commands/help_cmd.py +59 -0
- dev_setup/commands/install_cmd.py +129 -0
- dev_setup/commands/list_cmd.py +76 -0
- dev_setup/commands/remove_cmd.py +53 -0
- dev_setup/commands/run_cmd.py +66 -0
- dev_setup/commands/update_cmd.py +149 -0
- dev_setup/function_runner.py +133 -0
- dev_setup/functions.schema.json +106 -0
- dev_setup/functions.yaml +111 -0
- dev_setup/functions_catalog.py +217 -0
- dev_setup/functions_registry.py +149 -0
- dev_setup/generic.py +719 -0
- dev_setup/registry.py +97 -0
- dev_setup/tools.yaml +679 -0
- dev_setup/ui.py +111 -0
- devstuff-1.13.1.dist-info/METADATA +617 -0
- devstuff-1.13.1.dist-info/RECORD +30 -0
- devstuff-1.13.1.dist-info/WHEEL +4 -0
- devstuff-1.13.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import questionary
|
|
8
|
+
|
|
9
|
+
from dev_setup import registry, ui
|
|
10
|
+
from dev_setup.base import Tool
|
|
11
|
+
from dev_setup.generic import UpdateStatus
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("update")
|
|
15
|
+
@click.option("--verbose", "-v", is_flag=True, help="Stream update output to the terminal.")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--version", "target_version", default=None,
|
|
18
|
+
help="Update to a specific version instead of latest. Only valid with a single package.",
|
|
19
|
+
)
|
|
20
|
+
@click.argument("packages", nargs=-1)
|
|
21
|
+
def update_cmd(packages: tuple[str, ...], verbose: bool, target_version: str | None) -> None:
|
|
22
|
+
"""Update packages. Interactive picker with recommended updates when called with no arguments."""
|
|
23
|
+
from dev_setup import generic
|
|
24
|
+
generic._verbose = verbose
|
|
25
|
+
|
|
26
|
+
if not packages:
|
|
27
|
+
if target_version:
|
|
28
|
+
ui.error("--version requires a package key.")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
_update_interactive()
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
if target_version and len(packages) > 1:
|
|
34
|
+
ui.error("--version can only be used with a single package.")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
failed = False
|
|
38
|
+
for key in packages:
|
|
39
|
+
if not registry.exists(key):
|
|
40
|
+
ui.error(f"Unknown package: '{key}'")
|
|
41
|
+
failed = True
|
|
42
|
+
continue
|
|
43
|
+
if not _update_one(registry.get(key), target_version): # type: ignore[arg-type]
|
|
44
|
+
failed = True
|
|
45
|
+
|
|
46
|
+
if failed:
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _update_one(tool: Tool, version: str | None) -> bool:
|
|
51
|
+
ui.section(f"Update {tool.name}")
|
|
52
|
+
|
|
53
|
+
if not tool.is_installed():
|
|
54
|
+
ui.warn(f"{tool.name} is not installed — nothing to update.")
|
|
55
|
+
ui.dim(f"Install first: devstuff install {tool.key}")
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
if tool.install_type in ("bash", "script"):
|
|
59
|
+
ui.warn(f"Updating {tool.name} re-runs its full installer (may use sudo).")
|
|
60
|
+
if not ui.confirm(f"Continue updating {tool.name}?", default=False):
|
|
61
|
+
ui.dim("Skipped.")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
before = tool.get_version()
|
|
65
|
+
try:
|
|
66
|
+
after = tool.update(version=version) or tool.get_version() # type: ignore[attr-defined]
|
|
67
|
+
if before and after and before == after:
|
|
68
|
+
ui.success(f"{tool.name} already up to date: {after}")
|
|
69
|
+
elif before and after:
|
|
70
|
+
ui.success(f"{tool.name} updated: {before} → {after}")
|
|
71
|
+
elif after:
|
|
72
|
+
ui.success(f"{tool.name} updated: {after}")
|
|
73
|
+
else:
|
|
74
|
+
ui.success(f"{tool.name} updated")
|
|
75
|
+
return True
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
ui.error(f"Failed to update {tool.name}: {exc}")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _collect_update_candidates() -> list[tuple[Tool, UpdateStatus]]:
|
|
82
|
+
"""Return (tool, UpdateStatus) for every installed tool, probed concurrently.
|
|
83
|
+
|
|
84
|
+
Pure data-gathering, kept free of any UI/prompt code so it can be exercised
|
|
85
|
+
directly without a terminal.
|
|
86
|
+
"""
|
|
87
|
+
tools = registry.all_tools()
|
|
88
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
89
|
+
installed_flags = list(pool.map(lambda t: t.is_installed(), tools))
|
|
90
|
+
installed = [t for t, flag in zip(tools, installed_flags, strict=True) if flag]
|
|
91
|
+
if not installed:
|
|
92
|
+
return []
|
|
93
|
+
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
94
|
+
statuses = list(pool.map(lambda t: t.check_for_update(), installed)) # type: ignore[attr-defined]
|
|
95
|
+
return list(zip(installed, statuses, strict=True))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _update_interactive() -> None:
|
|
99
|
+
ui.print_banner()
|
|
100
|
+
|
|
101
|
+
with ui.spinner("Checking installed packages for available updates..."):
|
|
102
|
+
candidates = _collect_update_candidates()
|
|
103
|
+
|
|
104
|
+
if not candidates:
|
|
105
|
+
ui.info("No installed packages to update.")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
key_width = max((len(t.key) for t, _ in candidates), default=12) + 2
|
|
109
|
+
|
|
110
|
+
choices: list = []
|
|
111
|
+
for t, status in sorted(candidates, key=lambda c: c[0].key):
|
|
112
|
+
if status.available is True:
|
|
113
|
+
tag = f"update available: {status.current or '?'} → {status.latest}"
|
|
114
|
+
mark = "⬆ "
|
|
115
|
+
elif status.available is False:
|
|
116
|
+
tag = f"up to date ({status.current or status.latest})"
|
|
117
|
+
mark = " "
|
|
118
|
+
else:
|
|
119
|
+
tag = "unknown — reinstall to check"
|
|
120
|
+
mark = " "
|
|
121
|
+
title = [
|
|
122
|
+
("class:check", mark),
|
|
123
|
+
("class:text", f"{t.key:<{key_width}}{tag}"),
|
|
124
|
+
]
|
|
125
|
+
choices.append(questionary.Choice(title=title, value=t.key, checked=status.available is True))
|
|
126
|
+
|
|
127
|
+
selected = questionary.checkbox(
|
|
128
|
+
"Select packages to update:",
|
|
129
|
+
choices=choices,
|
|
130
|
+
instruction="(Space toggle · Enter confirm · pre-checked items have a known update)",
|
|
131
|
+
style=ui._STYLE,
|
|
132
|
+
).ask()
|
|
133
|
+
|
|
134
|
+
if not selected:
|
|
135
|
+
ui.info("No packages selected.")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if not ui.confirm(f"Update {len(selected)} package(s)?"):
|
|
139
|
+
ui.warn("Aborted.")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
failed = False
|
|
143
|
+
for key in selected:
|
|
144
|
+
tool = registry.get(key)
|
|
145
|
+
if tool and not _update_one(tool, None):
|
|
146
|
+
failed = True
|
|
147
|
+
|
|
148
|
+
if failed:
|
|
149
|
+
sys.exit(1)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
from dev_setup.base import patch_bashrc, remove_bashrc_block
|
|
10
|
+
from dev_setup.functions_registry import FunctionDef, FunctionParam
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ParamResolutionError(RuntimeError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_params(
|
|
18
|
+
params: list[FunctionParam],
|
|
19
|
+
args: tuple[str, ...],
|
|
20
|
+
*,
|
|
21
|
+
prompt: Callable[[FunctionParam], str] | None = None,
|
|
22
|
+
) -> list[str]:
|
|
23
|
+
"""Resolve one value per declared param, filling any gap via `prompt` or a catalog
|
|
24
|
+
default. When `prompt` is None (the eval codepath, which must keep stdout clean),
|
|
25
|
+
missing required params raise instead of being asked for interactively.
|
|
26
|
+
|
|
27
|
+
A value counts as "provided" only if it's non-empty — an explicitly empty positional
|
|
28
|
+
arg (`dev-setup run key ""`) is treated the same as a missing one for a required
|
|
29
|
+
param, rather than silently running with an empty value.
|
|
30
|
+
"""
|
|
31
|
+
positional = list(args[: len(params)])
|
|
32
|
+
positional += [""] * (len(params) - len(positional))
|
|
33
|
+
|
|
34
|
+
values: list[str] = []
|
|
35
|
+
missing: list[str] = []
|
|
36
|
+
for p, value in zip(params, positional, strict=True):
|
|
37
|
+
if not value and prompt is not None:
|
|
38
|
+
value = prompt(p)
|
|
39
|
+
if not value and p.default:
|
|
40
|
+
value = p.default
|
|
41
|
+
if not value and p.required:
|
|
42
|
+
missing.append(p.name)
|
|
43
|
+
values.append(value)
|
|
44
|
+
|
|
45
|
+
if missing:
|
|
46
|
+
raise ParamResolutionError("Missing required parameter(s): " + ", ".join(missing))
|
|
47
|
+
return values
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _positional_prelude(params: list[FunctionParam]) -> str:
|
|
51
|
+
"""`name="$1"`-style assignments mapping real argv positions to the script's named vars.
|
|
52
|
+
|
|
53
|
+
Used for `script`-type subprocess runs and bashrc-registered functions, both of which
|
|
54
|
+
receive their values as real shell positional arguments at call time.
|
|
55
|
+
"""
|
|
56
|
+
return "\n".join(f'{p.name}="${i + 1}"' for i, p in enumerate(params))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _literal_prelude(params: list[FunctionParam], values: list[str]) -> str:
|
|
60
|
+
"""`name='resolved value'` assignments for eval mode.
|
|
61
|
+
|
|
62
|
+
`eval "$(dev-setup run ...)"` has no argv of its own — the values dev-setup resolved
|
|
63
|
+
from its own CLI args must be baked into the printed text as shell-quoted literals.
|
|
64
|
+
"""
|
|
65
|
+
return "\n".join(f"{p.name}={shlex.quote(v)}" for p, v in zip(params, values, strict=True))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def render_eval_script(fn: FunctionDef, args: tuple[str, ...]) -> str:
|
|
69
|
+
"""Build the text printed for `register: eval` mode. Never prompts — stdout must stay
|
|
70
|
+
clean for `eval "$(...)"` capture, so missing required params raise instead."""
|
|
71
|
+
values = resolve_params(fn.params, args)
|
|
72
|
+
prelude = _literal_prelude(fn.params, values)
|
|
73
|
+
return f"{prelude}\n{fn.script}" if prelude else fn.script
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _bashrc_prelude(fn: FunctionDef) -> list[str]:
|
|
77
|
+
"""Per-param lines for a bashrc-registered function: `name="$N"` plus, for required
|
|
78
|
+
params with no default, a guard that fails loudly if the caller left it blank.
|
|
79
|
+
|
|
80
|
+
dev-setup itself is never in the loop when an enabled function is called directly by
|
|
81
|
+
the user's shell — `resolve_params` can't help here — so this is the only place
|
|
82
|
+
"required" can be enforced for this mode.
|
|
83
|
+
"""
|
|
84
|
+
lines: list[str] = []
|
|
85
|
+
for i, p in enumerate(fn.params):
|
|
86
|
+
lines.append(f'local {p.name}="${i + 1}"')
|
|
87
|
+
if p.required and not p.default:
|
|
88
|
+
lines.append(f'if [ -z "${p.name}" ]; then')
|
|
89
|
+
lines.append(f' echo "{fn.key}: missing required argument: {p.name}" >&2')
|
|
90
|
+
lines.append(" return 1")
|
|
91
|
+
lines.append("fi")
|
|
92
|
+
return lines
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render_bashrc_function(fn: FunctionDef) -> str:
|
|
96
|
+
"""Build the `name() { ... }` block registered into ~/.bashrc.
|
|
97
|
+
|
|
98
|
+
Blank lines are dropped from the body (blank-line-free is semantically identical
|
|
99
|
+
in bash) because `remove_bashrc_block` treats the first blank line after the
|
|
100
|
+
marker as the end of the block — a blank line inside the function would make
|
|
101
|
+
`disable` orphan everything after it, closing brace included.
|
|
102
|
+
"""
|
|
103
|
+
body_lines = [f" {line}" for line in _bashrc_prelude(fn)]
|
|
104
|
+
body_lines += [f" {line}" for line in fn.script.rstrip("\n").splitlines() if line.strip()]
|
|
105
|
+
return f"{fn.key}() {{\n" + "\n".join(body_lines) + "\n}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def enable_bashrc_function(fn: FunctionDef) -> bool:
|
|
109
|
+
return patch_bashrc(f"dev-setup-fn:{fn.key}", render_bashrc_function(fn))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def disable_bashrc_function(fn: FunctionDef) -> bool:
|
|
113
|
+
return remove_bashrc_block(f"dev-setup-fn:{fn.key}")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run_script_function(
|
|
117
|
+
fn: FunctionDef,
|
|
118
|
+
args: tuple[str, ...],
|
|
119
|
+
*,
|
|
120
|
+
prompt: Callable[[FunctionParam], str] | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Execute a `script`-type function as a subprocess. Raises on nonzero exit."""
|
|
123
|
+
values = resolve_params(fn.params, args, prompt=prompt)
|
|
124
|
+
prelude = _positional_prelude(fn.params)
|
|
125
|
+
content = f"{prelude}\n{fn.script}" if prelude else fn.script
|
|
126
|
+
|
|
127
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
|
|
128
|
+
f.write(content)
|
|
129
|
+
tmp = f.name
|
|
130
|
+
try:
|
|
131
|
+
subprocess.run(["bash", tmp, *values], check=True)
|
|
132
|
+
finally:
|
|
133
|
+
os.unlink(tmp)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/thesawdawg/dev-setup-py/functions.schema.json",
|
|
4
|
+
"title": "devstuff functions catalog",
|
|
5
|
+
"description": "Schema for functions.yaml (bundled at src/dev_setup/functions.yaml, user overrides at ~/.config/dev-setup/functions.yaml). Defines reusable shell functions/scripts invoked via `devstuff run <key>` or, for bashrc-registered functions, called directly by name after `devstuff functions enable <key>`. Mirrors the validation in functions_catalog.py — keep both in sync.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "functions"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"version": {
|
|
11
|
+
"const": 1,
|
|
12
|
+
"description": "Catalog schema version. Currently always 1."
|
|
13
|
+
},
|
|
14
|
+
"functions": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"description": "Map of function key -> function definition.",
|
|
17
|
+
"propertyNames": {
|
|
18
|
+
"pattern": "^[a-z0-9][a-z0-9_-]*$",
|
|
19
|
+
"description": "Lowercase letters, digits, hyphens, underscores; must start with a letter or digit."
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": { "$ref": "#/definitions/function" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"definitions": {
|
|
25
|
+
"function": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["type", "script"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Display name shown in `devstuff functions list`. Defaults to the catalog key if omitted."
|
|
33
|
+
},
|
|
34
|
+
"description": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Short description shown in `devstuff functions list`. Defaults to \"\"."
|
|
37
|
+
},
|
|
38
|
+
"category": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Group shown in `devstuff functions list` (functions are listed grouped by category, like tools). Freeform, not a fixed enum. Defaults to \"custom\"."
|
|
41
|
+
},
|
|
42
|
+
"type": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": ["script", "shell-eval"],
|
|
45
|
+
"description": "Execution model. 'script': runs as a subprocess (like a tool's install_script) — for anything that just calls other binaries/apps and doesn't need to change your shell's state; params are resolved positionally and missing required ones are prompted for interactively. 'shell-eval': for things that must mutate the *calling* shell (env vars, cd, aliases, agents like ssh-agent) — a devstuff subprocess can't do this on its own, so see 'register' for how it actually takes effect."
|
|
46
|
+
},
|
|
47
|
+
"register": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"enum": ["bashrc", "eval"],
|
|
50
|
+
"description": "Only meaningful when type is 'shell-eval' — rejected for type 'script'. 'bashrc' (default): `devstuff functions enable <key>` patches a real shell function into ~/.bashrc (idempotent); afterward you call it directly by name in a new shell (`devstuff run` refuses to run these itself and points at `functions enable` instead). Required params without a default get a runtime guard baked into the generated function that fails loudly (prints to stderr, returns 1) if left blank. 'eval': `devstuff run <key> [args]` prints resolved shell code to stdout ONLY, for `eval \"$(devstuff run key args)\"` — no prompting, since anything else on stdout would corrupt the eval capture; a missing required param is reported on stderr and exits non-zero instead."
|
|
51
|
+
},
|
|
52
|
+
"params": {
|
|
53
|
+
"type": "array",
|
|
54
|
+
"description": "Named parameters, resolved positionally from CLI args in the order declared here. Each becomes a named shell variable in the script body (e.g. \"$key_path\", not positional \"$1\") via a runner-injected prelude. An explicitly empty value counts as missing for a required param, same as not passing it at all.",
|
|
55
|
+
"items": { "$ref": "#/definitions/param" }
|
|
56
|
+
},
|
|
57
|
+
"script": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Required. The bash script body, referencing params by name (e.g. \"$key_path\"). For bashrc-registered functions, keep this free of blank lines if you hand-edit — devstuff already strips them when rendering, since a blank line would make `functions disable` stop removing the block early and orphan the rest (closing brace included)."
|
|
60
|
+
},
|
|
61
|
+
"help_cmd": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Optional help command shown alongside the function in `devstuff functions list`."
|
|
64
|
+
},
|
|
65
|
+
"docs_url": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"format": "uri",
|
|
68
|
+
"description": "Optional documentation URL for this function."
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"if": {
|
|
72
|
+
"properties": { "type": { "const": "shell-eval" } },
|
|
73
|
+
"required": ["type"]
|
|
74
|
+
},
|
|
75
|
+
"else": {
|
|
76
|
+
"properties": { "register": false }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"param": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"required": ["name"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"name": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
|
87
|
+
"description": "Shell variable name this param is bound to in the script body. Must be a valid shell identifier; must be unique within the function."
|
|
88
|
+
},
|
|
89
|
+
"description": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Defaults to \"\". Shown as the prompt label when this param is missing and can be interactively prompted for (type: script only — shell-eval never prompts)."
|
|
92
|
+
},
|
|
93
|
+
"required": {
|
|
94
|
+
"type": "boolean",
|
|
95
|
+
"default": true,
|
|
96
|
+
"description": "Defaults to true. Whether this param must resolve to a non-empty value. For type: script / register: eval, an unresolved required param raises/exits before anything runs. For a bashrc-registered function, a required param with no default instead gets a runtime bash guard that fails at call time."
|
|
97
|
+
},
|
|
98
|
+
"default": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"default": "",
|
|
101
|
+
"description": "Defaults to \"\". Fallback value used when no value is supplied and (for type: script) no interactive prompt filled it in. A required param WITH a default never triggers the bashrc runtime guard or a resolution error, since the default always satisfies it."
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
dev_setup/functions.yaml
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
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
|