Mega Snake
A development environment automation platform for teams using Java/Gradle in VS Code. It creates consistent local setups, shell configuration, and workspace tooling so developers can start coding quickly without repeating manual environment setup.
Why Mega Snake?
New contributors often lose time on first-day setup: matching Java and Gradle versions, configuring VS Code correctly, and wiring repetitive local scripts. Mega Snake solves this by automating the same environment steps for everyone.
- Start faster: bootstrap a ready-to-code Java workspace in VS Code with one CLI flow.
- Reduce setup drift: keep local Java/Gradle/tooling configuration consistent across developers.
- Automate recurring tasks: run common Git, release, and utility workflows from one CLI.
Installation
Via PyPI (Recommended for End Users)
Install mega-snake from PyPI using either uv or pipx:
Using uv:
uv tool install mega-snake
Using pipx:
pipx install mega-snake
Post-Installation Setup
After installation, add the shell initialization script to your shell configuration:
For bash/zsh, add this line to ~/.bashrc or ~/.zshrc:
. "$(mgsnake shell-path bash)"
For PowerShell, add this line to your PowerShell profile (usually $PROFILE):
. (mgsnake shell-path pwsh)
Then restart your terminal or source the configuration file to activate the mgsnake command.
Usage
Terminal Support
The mgsnake CLI works on:
- Windows: PowerShell
- macOS/Linux: bash or zsh
Basic Usage
After installation and shell profile configuration, use the mgsnake command:
```bash
# Show help
mgsnake --help
# Execute commands with specific log level
mgsnake --log-level DEBUG <command>
```
Log Levels
Available log levels (from least to most verbose):
- ERROR: Only errors
- WARNING: Errors and warnings
- INFO: Normal operational messages (default)
- DEBUG: Detailed information for debugging
- NOTSET: All messages
Example Commands
```bash
# Create a working environment
mgsnake working-env
# Check GraphQL schema
mgsnake graphql-schema
# Show branch details with debug info
mgsnake --log-level DEBUG remote-branches-details
```
Note: Each command has its own help. Use
mgsnake <command> --help(or the shorter-h) for specific details.
Prefer command aliases for daily use
Many command names are intentionally descriptive. For faster terminal workflows, use the aliases shown next to each command in COMMANDS.md.
# Full command
mgsnake working-env
# Alias
mgsnake cwe
Available Commands
See COMMANDS.md for the full list of available commands and their usage.
The same reference is readable without leaving the terminal, which is usually faster than opening the file:
# The whole reference, paged
mgsnake man
# A single command (aliases work too)
mgsnake man diff-tree
mgsnake man dt
That file is generated, not hand-written: mgsnake generate-docs introspects the registered commands (synopsis,
aliases, options, defaults) and appends a per-command prose fragment from src/mega_snake/resources/docs/. This way
the option tables can never drift away from the code.
# Regenerate the reference after changing any command
mgsnake generate-docs
# Verify it is up to date (exits non-zero and prints a diff when stale)
mgsnake generate-docs --check
generate-docs runs anywhere: it needs no workspace, no Git repository and no shell setup. If you contribute a
command or change its options, regenerate COMMANDS.md and commit it along with your change.
Automated dependency vulnerability scanning
This repository combines two free, open-source tools to keep dependencies up to date and flag vulnerabilities:
- Dependabot (
.github/dependabot.yml): opens weekly pull requests to update outdatedpip/uvdependencies and GitHub Actions. mgsnake scan-dependencies: audits the locked dependencies and files a GitHub issue for every new vulnerability finding. Any repo can reuse this by consumingmgsnake, regardless of its stack.
The scheduled/PR workflow that runs mgsnake scan-dependencies in CI lives at
.github/workflows/dependency-scan.yml. It runs weekly, on pull requests that
touch pyproject.toml/uv.lock, and on demand via workflow_dispatch. Consuming repos on other ecosystems should
adapt this workflow to install the right auditor (e.g. osv-scanner) and pass --ecosystem if auto-detection isn't
sufficient; .github/dependabot.yml and the workflow itself are inherently per-repo (GitHub reads them from the repo
where they live) and cannot be consumed remotely from the mega-snake package.
Available Commands
Config Environment
graphql-schema
Builds a consolidated GraphQL schema and introspection JSON from schema files in a directory.
Synopsis: mgsnake graphql-schema [OPTIONS] SCHEMA_PATH
Aliases: graphql, gql, cgs
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
schema_path— Directory containing the schema files; all files in it (subdirectories included) are merged into the consolidated schema
Frontend tooling (Apollo and most IDE plugins) cannot work from the raw SDL alone — it needs a full
introspection result to provide autocompletion and type checking. That is why this command emits two
files rather than one: the consolidated .graphql schema, and the .json introspection payload
those tools consume.
Output
Writes schema.graphql (consolidated SDL) and schema.json (introspection) to the
workspace_temp folder and opens both in VS Code.
Notes
Every file under the given directory — subdirectories included — is merged, so the directory is the unit of composition, not an entry-point file. Keep only schema files in it.
init-local-config
Creates or updates the local configuration file used for developer-specific shell settings.
Synopsis: mgsnake init-local-config [OPTIONS]
Aliases: iload, ilc
| Option | Description |
|---|---|
-o, --override |
Override the current local configuration file with a new one |
-h, --help |
Show this message and exit. |
Developers usually have machine-specific tokens, paths and aliases that must never be committed.
This command generates a shell-specific file (.sh or .ps1) that is ignored by Git and sourced
by the main environment, so the project can stay "convention over configuration" while still leaving
room for per-machine configuration.
Custom shell function definitions are supported, not just environment variables.
Notes
Reload it in the current session with mgsnake reload-config.
This happens automatically: the command exits with status 29 when it succeeds, and the mgsnake
shell function installed by the init script reads that status and reloads the file for you.
A child process cannot change its parent's environment, so the reload has to happen in your shell.
If you invoke the executable directly — bypassing the function, or from a script — you will see the
29 and no reload will run.
maven-project-setup
Creates or updates Maven tasks and log watchers in the current code-workspace when pom.xml is present.
Synopsis: mgsnake maven-project-setup [OPTIONS]
Aliases: mps
| Option | Description |
|---|---|
-o, --override |
Recreate existing Maven tasks and log watchers |
-h, --help |
Show this message and exit. |
Adds the task definitions under the tasks section of the current .code-workspace file —
clean install, test, verify, dependency:tree and spring-boot:run — together with the
matching log watchers, so each task's output lands in a watched log file.
Notes
Requires a pom.xml in the current directory. Existing tasks and watchers are left alone unless
--override is passed.
set-gradle
Detects installed Gradle versions and sets the default Gradle version for the workspace and shell config.
Synopsis: mgsnake set-gradle [OPTIONS]
Aliases: gradle, sg
| Option | Description |
|---|---|
-o, --override |
Override the current Gradle version |
-h, --help |
Show this message and exit. |
Writes java.import.gradle.home and the GRADLE_HOME entry of terminal.integrated.env.<os> in the
.code-workspace file, keeping the Gradle the IDE imports with and the one your integrated terminal
calls on the same version.
Notes
As with set-java, the .code-workspace file is read with a comment-preserving loader, so your
annotations are not stripped.
Also as with set-java, a successful run exits with status 29, which the mgsnake shell function
(see shell-path) reads to re-source the local environment files, so the new GRADLE_HOME applies
to the current session.
set-java
Detects installed Java versions and sets the default Java version for the workspace and shell config.
Synopsis: mgsnake set-java [OPTIONS]
Aliases: java, sj
| Option | Description |
|---|---|
-o, --override |
Override the current Java version |
-h, --help |
Show this message and exit. |
Writes java.configuration.runtimes and the JAVA_HOME entry of terminal.integrated.env.<os> in
the .code-workspace file, so the IDE's language server and the integrated terminal always agree on
which JDK is in use. It also configures the Java formatter settings.
Notes
The .code-workspace file is JSON with comments. It is read with a comment-preserving loader, so
the annotations you leave in it survive the update.
Because it rewrites a local environment file, the command exits with status 29 on success. The
mgsnake shell function installed by the init script (see shell-path) reads that status and
re-sources the local environment files, so the new JAVA_HOME applies to the current session
without opening a new terminal.
set-maven
Detects Maven installation (or uses --maven-home) and sets Maven paths for VS Code and local shell config.
Synopsis: mgsnake set-maven [OPTIONS]
Aliases: maven, sm
| Option | Description |
|---|---|
-m, --maven-home TEXT |
Explicit Maven home directory |
-h, --help |
Show this message and exit. |
Sets M2_HOME in both the workspace terminal settings and the local shell config, and points VS
Code's Maven executable path at the detected installation.
Notes
Intended for pom.xml-based projects. Run maven-project-setup afterwards to add the matching VS
Code task definitions.
A successful run exits with status 29, which the mgsnake shell function (see shell-path) reads
to re-source the local environment files, so the new M2_HOME applies to the current session.
maven-project-setup writes no environment file and therefore exits 0.
working-env
Sets up the VS Code workspace with recommended extensions, default settings, tasks, launch configurations, and git exclusions. Only the stacks found in the repository are configured: the Java, Gradle and Maven steps — along with their tasks, launch configurations, log watchers and extensions — are skipped unless a build file reveals them, or --stack asks for them explicitly.
Synopsis: mgsnake working-env [OPTIONS]
Aliases: cwe, env
| Option | Description |
|---|---|
-s, --stack [java|gradle|maven|python|node|all] |
Configure this stack regardless of what the repository looks like, instead of detecting it from the build files in the current directory. Repeat the option to select several stacks, or pass 'all' to configure every one of them. A build tool implies its language, so 'gradle' and 'maven' both bring 'java' along. |
-h, --help |
Show this message and exit. |
The "zero-config start" command: a single run should leave the IDE ready to code, so it is the one to reach for on a freshly cloned repository.
On top of what the synopsis lists, it also sets up log watchers and GitHub query definitions, and it
runs the version configuration of every stack it finds — set-java, set-gradle and set-maven
are only needed afterwards when you want to switch versions.
Output
| Path | What lands there |
|---|---|
<name>.code-workspace |
The workspace file itself, in the current directory — created when none exists, updated in place otherwise. Everything below lives inside it: settings, extensions.recommendations, tasks, launch and the log-viewer watchers. |
workspace_temp/ |
The working folder, created after confirmation when missing. Log files and the output of the other commands go here. |
.git/info/exclude |
Gains .vscode/, the working folder and /*.code-workspace, so none of the generated state is ever offered as a commit. Appended to, never rewritten. |
A stack that is not active contributes nothing to any of them — and a workspace with no active task
or launch configuration gets no tasks or launch block at all, rather than an empty one.
Examples
# Configure whatever the repository looks like (the usual case)
mgsnake working-env
# A Java repository whose build file lives in a subfolder
mgsnake working-env --stack maven
# A polyglot repository: several stacks at once
mgsnake working-env -s gradle -s node
# Everything, like the command used to behave before stack detection
mgsnake working-env --stack all
Notes
Everything the command writes belongs to a stack, and only the stacks the repository shows a marker file for are configured. A workspace on a repository with no JVM build file is never asked for a JDK, and gets none of the Java tasks, launch configurations, log watchers or extensions:
| Stack | Marker files in the current directory | Brings along |
|---|---|---|
gradle |
build.gradle, build.gradle.kts, settings.gradle, settings.gradle.kts |
java |
maven |
pom.xml |
java |
python |
pyproject.toml, setup.py, requirements.txt, Pipfile, uv.lock |
— |
node |
package.json, tsconfig.json, deno.json, package-lock.json, pnpm-lock.yaml, yarn.lock |
— |
java |
(none — it comes from a build tool, or from --stack java) |
— |
common |
(always active) | — |
A stack may also be opt-in: it has no --stack key, all does not reach it, and it activates only
when its marker file is found. snake is the one that exists today — it carries the launch
configuration that debugs the mega-snake CLI itself, behind a .mgsnake-dev marker, so it stays
out of every workspace that did not ask for it by creating that file.
Only the current directory is inspected. A repository that keeps its build file in a subfolder — or
one that uses a build tool this command does not know — needs --stack, which replaces the
detection entirely. The per-tool commands stay unconditional either way, so set-java still works
in a folder where the Java stack was skipped.
Skipping a stack never removes anything: recommended extensions, tasks and launch configurations
already present in the .code-workspace file are left untouched, so a workspace configured before
a stack was dropped keeps working. Remove those entries by hand when you want them gone. A Python
repository that used to see PYTHON DEBUG (Snake) in a freshly generated workspace and no longer
does has not lost anything by mistake — that entry only ever belonged to the opt-in snake stack
above, and this is the paragraph that explains its absence.
Requires a valid Git repository. Developer-specific overrides are loaded before the defaults are
written, so anything you set through init-local-config wins over the values this command
generates.
Because it writes the local environment files, a successful run exits with status 29. The
mgsnake shell function installed by the init script (see shell-path) reads that status and
re-sources them, so the environment it just configured applies to the current session.
Configuration
config
Reads and writes the persistent settings mgsnake remembers between runs, in two scopes: repo (stored inside the current clone's git directory) and global (stored in the user's config directory). Reads resolve through environment variable > repo > global, so an exported variable always wins and existing environment-based workflows keep working. Credential-shaped names are refused: secrets stay in environment variables only.
Synopsis: mgsnake config [OPTIONS] COMMAND [ARGS]...
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
The repository already had three configuration layers and not one of them could be written to:
config.properties ships inside the wheel and is replaced on every install, AppProperties is a
per-process singleton kept deliberately immutable, and the local config/env files are a shell
prelude the CLI writes once and never reads back. This command is the missing middle piece — state
the CLI both writes and reads — and it is what lets the Jira commands stop asking for a project key
on every single call.
Subcommands:
| Subcommand | What it does |
|---|---|
get KEY |
Prints the resolved value. Only the value reaches stdout, so $(mgsnake config get jira.project_key) is safe. Exits 1 when nothing defines it. |
set KEY VALUE [--global] |
Stores the setting, atomically. |
unset KEY [--global] |
Removes it from that one scope only. |
list [--scope repo|global|all] |
Prints key=value lines of what is on disk. |
export [--shell] [--scope] |
Prints export statements for the global scope, meant to be evaluated from the shell profile. |
Output
Two files, one per scope, both plain sorted JSON:
<git-dir>/mgsnake/state.json— thereposcope, the default target ofsetandunset. Living inside.gitmeans it is never committed without touching.gitignoreor.git/info/exclude, it is per-clone, and it dies with the clone.workspace_tempwas rejected for this: it is explicitly disposable, and state that evaporates is not state.~/.config/mgsnake/state.json(%APPDATA%\mgsnake\state.jsonon Windows) — theglobalscope.
Writes go through a temporary file in the same directory followed by a rename, so an interrupted run cannot leave behind a half-written file that would then break every other command.
Examples
mgsnake config set jira.domain azltech.atlassian.net --global
mgsnake config set jira.email dev@example.com --global
mgsnake config set jira.project_key TAROTAPP
mgsnake jira-board # no arguments needed any more
# In the shell profile: the user-wide settings only, which is what `export` defaults to.
eval "$(mgsnake config export --shell bash)"
Notes
Reads resolve through environment variable → repo → global. The environment sits on top on
purpose: every workflow that exported JIRA_DOMAIN and friends keeps working untouched, so
adopting the store can be gradual.
export is a session bootstrap, not a synchronisation mechanism, and the difference matters
because what it writes becomes an environment variable — the layer that outranks both scopes. So
for any key a clone also defines in its repo scope, evaluating export from the shell profile
inverts the precedence above for the rest of the session:
mgsnake config set jira.domain companyA.atlassian.net --global
cd ~/clients/companyB && mgsnake config set jira.domain companyB.atlassian.net # repo scope
eval "$(mgsnake config export --shell bash)" # in ~/.bashrc: JIRA_DOMAIN=companyA...
cd ~/clients/companyB && mgsnake jira-issues # ...so this talks to companyA. Exit 0.
Defaulting to --scope global is what keeps this narrow — the alternative, exporting repo, pins
one clone's board id and project key onto every other clone — but it does not close it, and it
cannot be closed from inside export: a shell profile runs in whatever directory the terminal
happened to open in, so filtering against "the current repository" would make the exported set
depend on where the terminal was launched, which is a worse failure because it is not reproducible.
The rule to work by is therefore: export only the keys no clone overrides. In practice that is
jira.domain and jira.email, which is exactly what the global scope is for.
Two conventions keep the file readable. A key ending in .cached is written by a command rather
than by you — jira.field.sprint.cached is what jira-issues worked out on its own — and the
bare key beside it is yours alone: nothing in the CLI writes jira.field.sprint, and when both
exist the bare one wins. That is why pinning a value actually sticks; see jira-issues for the
full story. Removing a .cached key is always safe, since the command that wrote it will work the
value out again.
Credentials are refused, not warned about, on the way in and on the way out. Any name matching
token, secret, password, passwd, credential or api_key fails config set with an error
and nothing is written; JIRA_API_TOKEN and GITHUB_TOKEN stay in the environment, because a
plaintext credential in a state file is worse than an exported variable precisely because it
persists and is forgotten. config get refuses to print one too — its first precedence layer is the
environment, so without that guard mgsnake config get jira.api_token would echo the live token to
stdout, and mgsnake config get path would echo $PATH. config get also insists on a real dotted
setting name for the same reason: it is a settings reader, not a general-purpose way to dump the
environment into a command substitution.
The file also carries a mgsnake.state_version marker, written the first time anything is stored
in that scope. It is what lets a one-time migration tell state written by an older version from
state you wrote yourself — a distinction the keys alone cannot make. It is metadata rather than a
setting, so config list and config export leave it out; it is plainly there if you read the file.
Names must be lowercase and dotted (jira.field.story_points), which is what keeps the file
navigable instead of turning into a flat junk drawer.
The whole group runs before any initialization, so it works outside a configured environment and
even before MEGA_SNAKE_SHELL exists. Outside a git repository the repo scope simply does not
exist: reads fall back to the global one, and writes say so and suggest --global.
An unusable state file (invalid JSON, or valid JSON that is not an object) is never silently discarded, and it is never grounds for a hang either — the two failure modes this mechanism is built to avoid. Four different behaviors apply, depending on the subcommand:
set,unsetandliston one explicit scope (--scope repoor--scope global) ask about that exact file, so from an interactive terminal they offer to back the broken file up next to itself (renamed, never deleted) and start a fresh empty one. Declining, or running non-interactively (a script, CI, a closed stdin), fails loudly instead, naming the broken file for manual repair.getandexportnever prompt, on any terminal. Both are meant to be consumed by a shell —$(mgsnake config get ...), andexportspecificallyeval'd from a shell profile on every new terminal — so a prompt on a corrupt file would hang a script's stdout capture, or a terminal's startup, instead of failing it in milliseconds.getandlist --scope all/export --scope allmerge both scopes, so a single broken one degrades instead of blocking a setting that only lives in the healthy scope — a warning naming the broken file is still printed once per run, but nothing is offered to fix it from there.exporton one explicit scope (--scope repoor--scope global) still fails loudly on a broken file, same asset/unset/list— it is only the prompt thatexportnever gets, never the failure itself.
A state file that cannot be read at all (wrong permissions, an I/O error) follows the same two
rules — an explicit scope fails, a merged read degrades with a warning — but is never offered
the backup-and-reset, on any terminal. Unreadable is not the same as corrupt: the contents are very
likely intact behind the wrong permissions, so renaming the file aside and starting over would
throw away recoverable settings to fix something chmod solves. The message names the file and
says so.
Settings the Jira commands read: jira.domain, jira.email, jira.project_key, jira.board_id,
jira.field.story_points and jira.field.sprint. The last three are written by the commands
themselves as a cache; removing them just forces a fresh resolution.
export covers the global scope by default, and that default is load-bearing. An environment
variable outranks every scope, and a shell profile runs in whatever directory the terminal happened
to open in — so exporting the repo scope from there would pin one clone's jira.project_key and
jira.board_id onto the whole session, and every other clone would then resolve them from the
environment. mgsnake jira-issues in a second repository would download the first one's board, with
no warning and exit code 0. --scope repo and --scope all are still there for anyone who wants
exactly that, per shell rather than per profile.
Dependency Audit
scan-dependencies
Scans the project's locked dependencies for known vulnerabilities against the OSV advisory database, then files a GitHub issue for each new finding (package, installed version, recommended version, severity and advisory link), skipping findings that were already reported. The ecosystem (Python/uv, Java/Gradle/Maven, Node, or a generic OSV-Scanner audit) is auto-detected from the project's lockfiles, or can be forced with --ecosystem.
Synopsis: mgsnake scan-dependencies [OPTIONS]
Aliases: sdep, audit
| Option | Description |
|---|---|
--dry-run |
Scan and print findings without creating GitHub issues. |
--ecosystem [python|java|node|osv] |
Force the ecosystem/auditor instead of auto-detecting it from the project's lockfiles. |
-h, --help |
Show this message and exit. |
The ecosystem is detected from the project's lockfiles, first match wins:
| Marker file | Ecosystem | Auditor |
|---|---|---|
uv.lock |
Python/uv | pip-audit |
build.gradle, build.gradle.kts, pom.xml |
Java (Gradle/Maven) | OSV-Scanner |
package-lock.json |
Node | OSV-Scanner |
| (nothing matches) | generic fallback | OSV-Scanner |
OSV-Scanner covers the non-Python ecosystems because it reads the same OSV advisory database as
pip-audit, so findings stay comparable across stacks instead of depending on one tool per
ecosystem.
Any repo can reuse this by consuming mgsnake, regardless of its stack.
Notes
Issue de-duplication is by exact title and considers closed issues too, so a vulnerability you already triaged and closed is not filed again on the next run.
Diff Tree
diff-tree
Creates a diff tree of changes and a commit list between two points in history. The comparison runs from master (or the commit given with --origin-hash) up to the current HEAD (or the commit given with --target-hash), which makes it possible to inspect a past range instead of only the current work.
Synopsis: mgsnake diff-tree [OPTIONS]
Aliases: dt, tree
| Option | Description |
|---|---|
-o, --origin-hash TEXT |
Commit hash to compare against instead of master |
-t, --target-hash TEXT |
Commit hash to compare up to, instead of the current HEAD. Requires --scope c (the default), since the index and the working tree only exist for HEAD; any other scope is rejected. |
-d, --delete-original-files |
Delete the generated copy of the original files in the diff tree |
-s, --scope [c|s|u] |
Changes to include: (c)ommitted only [default], committed and (s)taged, or also (u)nstaged and untracked. Only 'c' is compatible with --target-hash: the other two read the index and the working tree, which exist only for HEAD, so passing them together is rejected instead of silently ignoring one of them. [default: c] |
-h, --help |
Show this message and exit. |
Useful for code reviews, progress comments on a ticket, and release notes: it answers "what did I
touch since master?" without scrolling through git log.
Both ends of the comparison move independently: --origin-hash sets where it starts, --target-hash
sets where it ends. With both, the range is fully explicit and no longer anchored to the current checkout,
which is what makes it possible to reconstruct a past release from the two commits that bound it.
Output
Writes three files to workspace_temp/diff_tree/ and opens each one in VS Code:
diff_tree.txt— the visual tree of the affected paths. Every entry is tagged with a marker for what happened to it (added, modified, deleted, renamed, copied, type-changed, unmerged), and the file closes with a per-marker legend and its totals.diff_changes.txt— the Git-style patch for those files.diff_commit.txt— the commit list (hash, date, message), newest first.
Alongside them it reconstructs the affected paths as a real directory tree under
workspace_temp/diff_tree/diff_tree_dummy_repo/, which is what the tree above is rendered from.
Each file there holds its contents as of the origin of the comparison — the "before" version,
so you can open it next to your working copy. Files you added have no before-version and are left
empty, and binary files carry a placeholder instead of their bytes rather than dumping raw data
into a text snapshot.
The tree and the patch follow --scope. The commit list cannot, since uncommitted work has no
commits, so pending files are prepended instead as Unstaged files: and Staged files: sections
above the newest commit — each one only when the scope covers it.
Examples
# Everything on this branch that master does not have
mgsnake dt
# A past release, reconstructed from the two commits that bound it
mgsnake dt -o 85652b7 -t 79108b6
Notes
The output directory is wiped and recreated on every run, so nothing from a previous comparison survives into the current one — including the reconstructed tree, which is rebuilt from scratch.
No remote is required: when the repository has none, the command asks for the local main branch to compare against. With a remote, the main branch is resolved from it and the command offers to fetch and prune it first, so the comparison is against a main branch as fresh as you want it.
The rejection of an incompatible --target-hash/--scope pair happens before the output directory
is touched, so a rejected invocation leaves the previous run's files intact instead of wiping them
and then aborting.
Documentation
generate-docs
Generates the Markdown command reference by introspecting the registered CLI commands, rendering their help and options, and appending the command-specific fragment bodies.
Synopsis: mgsnake generate-docs [OPTIONS]
| Option | Description |
|---|---|
--output FILE |
Write the generated command reference to this file. [default: COMMANDS.md] |
--check |
Render in memory, compare with the output file, and exit with an error when it is stale. |
-h, --help |
Show this message and exit. |
Useful when you want a single, drift-resistant command reference: the generator pulls the public
CLI shape from Click itself and only uses these fragments for the extra narrative that --help
should not duplicate.
Output
Writes a Markdown command reference to the target file (default: COMMANDS.md).
Notes
This command is intentionally no_init: it does not require MEGA_SNAKE_SHELL, a workspace, or a
git repository, and it resolves the packaged fragments through importlib.resources.
man
Renders the command reference in the terminal and pages it, showing the whole document or a single command when one is named. The content is built from the live CLI metadata and the packaged fragments, so it never depends on a generated file being present.
Synopsis: mgsnake man [OPTIONS] [COMMAND]
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
command— command name or alias to display. Defaults to the full reference.
--help answers "what are the flags?"; this answers "how does this command actually work?". It is
the reading form of the same reference published as COMMANDS.md, without leaving the terminal and
without a browser.
Nothing is installed into /usr/share/man and mandb is never invoked. uv tool install and
pipx place the package in an isolated environment and copy nothing to the system man path, so
man mgsnake would simply not resolve. Carrying the reader inside the CLI is what makes the
reference available on every platform this tool supports, including PowerShell, where a system
man does not exist at all.
Examples
# The whole reference, grouped by module
mgsnake man
# One command
mgsnake man diff-tree
# Aliases work too — this is the same page as above
mgsnake man dt
Notes
The document is rendered in memory from the live Click metadata and the packaged fragments, never
read from COMMANDS.md. That file lives in the repository and is not shipped inside the wheel, so
reading it would leave installed users with a command that only works in a source checkout.
Paging goes through the shell's pager (less on Unix, honouring PAGER). Styling is dropped
automatically when the pager cannot display it.
On Windows the document is printed in full instead of being paged. Click 8.4.x cannot write text to the temporary-file pager it selects for an interactive Windows console, so the command falls back to plain output rather than failing. The content is identical; only the scrolling is the terminal's job there.
Git & Release Management
remote-branches-cleanup
Builds the branch inventory (local, remote and paired branches) and iterates over the fully merged ones asking which to delete, removing each selected branch from the sides where it exists
Synopsis: mgsnake remote-branches-cleanup [OPTIONS]
Aliases: rbc
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
Works from the same branch inventory as remote-branches-details — every local branch paired with
its remote counterpart, judged against the remote main branch — except that here it is never
written anywhere: it is built in memory and consumed on the spot, so the deletion always acts on
the repository as it is right now, not on a report that may have been generated hours ago.
A selected branch is deleted from the sides where it actually exists: the remote copy when the branch has a remote side, the local copy when it has a local one. Neither side is assumed, because a branch you never checked out has no local reference and a branch whose remote counterpart was deleted on merge has no remote one — attempting the missing half would report a deletion failure for something that was already gone.
Local deletion uses git branch -D rather than git branch -d: the branch has been confirmed merged
into the remote main branch, which is the question that matters, while -d refuses whenever the
local main copy is behind and has not seen the merge yet.
Output
It writes no file. What it produces is a change to the repository, so the run itself is the output: one prompt per candidate, then the deletions you approved.
Each prompt identifies the branch before you decide on it — name, last commit date, author, commit
hash and subject — and states its Location: local, remote, or local and remote. That last
line is the one to read, because it is exactly what will be deleted for that branch. Three answers
are accepted: yes marks it for deletion, no skips it, and finalize ends the review right
there, keeping everything selected so far and never asking about the remaining branches.
Nothing is deleted while you are answering. The deletions run once the review is over, each one
reported as it happens, and the remote-tracking references are pruned afterwards when anything was
removed from the remote. Answering no to everything is a legitimate outcome and leaves the
repository untouched.
Notes
It takes no options: run it and follow the prompts. The command offers to fetch/prune first, so the
inventory is fresh. Deletion is git push <remote> --delete <branch> plus git branch -D <branch>,
and cannot be undone from here. A branch that fails to delete from the remote keeps its local copy
and does not stop the run. A remote is only required when a selected branch has a remote side, so a
local-only cleanup works in a repository without remotes.
remote-branches-details
Creates a detailed markdown report of the repository's branches — local, remote and paired — filtered by merge status against the main branch
Synopsis: mgsnake remote-branches-details [OPTIONS]
Aliases: rbd
| Option | Description |
|---|---|
-f, --filter-by [m|u|a] |
filter branches by merge status against main branch: 'M' - fully merged branches (every existing side is merged) 'U' - not fully merged branches 'A' - all branches (default) |
-h, --help |
Show this message and exit. |
Every local branch is paired with its remote counterpart (through its configured upstream) into a
single logical branch, so the report describes each branch once with both sides: a branch never
checked out shows only its remote side, and a branch whose remote copy was deleted on merge shows
only its local one. Those local leftovers are the most common form of dead branch — once a pull
request is merged the hosting platform usually deletes the branch, git fetch --prune drops the
remote-tracking reference, and the local branch lingers indefinitely.
A side counts as merged when it was merged, fast-forwarded, rebased, or squashed into the main branch — the last two are detected by patch id, so branches that were squash-merged through a PR are correctly reported as merged instead of lingering as unmerged noise. Comparison is always against the remote main branch when one exists, never the possibly stale local copy. A branch is fully merged only when every side it exists on is merged.
Before enumerating anything, the command offers to fetch and prune the remote so the inventory is as fresh as you want it to be.
Output
Creates workspace_temp/remote_branches.md and opens it in VS Code. The file is rewritten from
scratch on every run, so it always describes one single inventory rather than accumulating past
ones.
It opens with the context the report was built from — remote, main branch with both its local and remote hashes, the filter that was applied, and the generation timestamp — so a report kept around can still be read later without guessing which repository state produced it. Then comes one table row per branch, newest commit first, with:
- Status —
merged,remote merged,local mergedorunmerged. The middle two are the ones worth looking at: they mean the two sides disagree, so the branch is not yet safe to delete. - Track / Sync —
local_onlyorremote_onlywhen the branch lives on one side only, otherwise git's own tracking markers ([ahead 1, behind 2]and>,<,=,<>), and[gone]for a branch whose upstream was pruned. - Local hash / Remote hash — both tips, abbreviated, with
-where that side does not exist. They are shown side by side precisely because they can diverge, which is what the status columns above are summarizing. - Last commit, Author, Subject — of whichever side the branch has, to identify the work at a glance.
- Main ancestor — the commit the branch and the main branch last had in common.
When nothing matches, the report is still written and says so in prose instead of leaving an empty table to interpret.
Notes
A remote is not required: without one, the command asks for the local main branch and reports the
local branches against it. A --format option to customize the columns and output shape is
planned; for now the table is fixed. remote-branches-cleanup builds this same inventory in memory
for its interactive deletion — this report is for inspection.
Jira
jira-board
Resolves a Jira project key to its Agile board and prints {"boardId": ..., "cloudDomain": ...} to stdout, and nothing else, so the output can be captured with command substitution. The board id is cached in the current clone once resolved, so later runs answer without any HTTP call. boardId is an integer, not a string as the shell version emitted it.
Synopsis: mgsnake jira-board [OPTIONS] [PROJECT_KEY]
Aliases: jb
| Option | Description |
|---|---|
--refresh |
Ignore the cached board id and resolve it from Jira again. Use it after the project's boards changed, or to pick a different board when the project has several. |
-h, --help |
Show this message and exit. |
project_key— Jira project key. Defaults to the stored jira.project_key.
Resolving a board takes two round trips — project key to project id, then project id to board — and the answer almost never changes. That is why the result is cached per clone: the first run pays for the lookup and every later one answers from disk, with no HTTP call and, therefore, no credentials needed at all.
Output
Nothing on disk except the cache entry: jira.board_id is written to the repository scope of the
state store (see config), but only when the resolved project matches the stored
jira.project_key. Passing a different project key explicitly neither reads nor writes that cache,
so one clone's board can never be served for somebody else's project.
Examples
mgsnake jira-board # uses the stored project key
mgsnake jira-board TAROTAPP | jq .boardId
mgsnake jira-board --refresh # after the project's boards changed
Notes
Breaking change. boardId is now a number. The shell version emitted it through jq --arg, so
it was the string "1", while getSprintInfo turned around and used it as a number — the two
disagreed with each other. Any jq filter comparing it against a string needs adjusting.
An unknown project is an error naming the key. The shell version let jq -r '.id' return the string
null, asked Jira for ?projectKeyOrId=null, and printed {"boardId": "null"} without a word.
When a project has several boards you are asked which one, and the answer is cached. The prompt goes
to stderr so it cannot corrupt a captured stdout — although in a $(...) capture you will not see
it, so run the command once on its own (or with --refresh) to make the choice.
Requires jira.domain, jira.email and JIRA_API_TOKEN; see the config reference. On a corporate
machine with a TLS-inspecting proxy, point REQUESTS_CA_BUNDLE at the corporate CA bundle.
jira-issues
Downloads every issue of a Jira project's Agile board (epics, stories, tasks, subtasks), projects them into the compact schema the Jira skills consume, flags the ones that belong to an active sprint, and writes the result as a JSON array. The story points and sprint custom fields are resolved by name for the current Jira instance instead of being hardcoded, so the output is correct on any tenant. Progress goes to the console; only the file receives the data.
Synopsis: mgsnake jira-issues [OPTIONS] [PROJECT_KEY]
Aliases: ji
| Option | Description |
|---|---|
-o, --output TEXT |
Destination file. Defaults to jira_board_issues.json inside the working path. |
-r, --refresh |
Ignore every cached Jira lookup -- the board id and the story points / sprint custom field ids -- and resolve them from the instance again. Use it when the Jira side changed: a board recreated, or a custom field re-created by a migration, which the cache would otherwise keep answering with a stale id and no warning. |
-q, --quiet |
Silence the progress messages. |
-h, --help |
Show this message and exit. |
project_key— Jira project key. Defaults to the stored jira.project_key.
The whole board goes to a file rather than through the MCP server on purpose: the skills need to
slice the same dataset many times over (by epic, by assignee, by status, by sprint), and paying for
a fresh remote round trip per question is both slow and rate-limited. One download, then jq.
Output
A JSON array written atomically to workspace_temp/jira_board_issues.json, or to --output. Every
entry has this shape:
{
"id": "10001",
"link": "https://<domain>/rest/api/2/issue/10001",
"key": "TAROTAPP-1",
"fields": {
"summary": …, "statuscategorychangedate": …, "created": …, "resolutiondate": …,
"lastViewed": …, "updated": …, "description": …,
"issuetype": { "name": …, "subtask": …, "entityId": …, "hierarchyLevel": … },
"parent": { "id": …, "key": … },
"project": { "id": …, "key": …, "name": … },
"status": { "id": …, "name": …, "statusCategory": { "id": …, "key": …, "name": … } },
"workratio": …, "issuerestriction": …,
"priority": { "id": …, "name": … },
"labels": [ … ],
"storyPoints": …,
"assignee": { "accountId": …, "displayName": …, "emailAddress": …, "timeZone": … },
"creator": { … same shape … },
"reporter": { … same shape … },
"votes": { "votes": …, "hasVoted": … },
"attachment": [ { "id": …, "filename": …, "mimeType": …, "size": …, "contentUrl": …, "author": { … } } ],
"attachmentsCount": …,
"comment": [ { "id": …, "created": …, "updated": …, "jsdPublic": …, "body": …, "author": { … }, "updateAuthor": { … } } ],
"commentCount": …,
"sprint": [ { "id": …, "name": …, "state": …, "startDate": …, "endDate": …, "completeDate": … } ]
},
"activeSprint": true
}
A nested object that Jira returned as null becomes an object whose values are all null, never
null itself — parent above all, since .fields.parent.key == null is the documented way to find
orphaned stories and it throws the moment parent itself is null.
Examples
mgsnake jira-issues # stored project key, default destination
mgsnake jira-issues TAROTAPP -o /tmp/board.json
mgsnake jira-issues --quiet # for scripts and CI
# What is in the current sprint, with points and assignee
jq -r '.[] | select(.activeSprint)
| "\(.key)\t\(.fields.storyPoints // "-")\t\(.fields.assignee.displayName // "unassigned")\t\(.fields.summary)"' \
workspace_temp/jira_board_issues.json
# Stories with no epic
jq -r '.[] | select(.fields.parent.key == null) | .key' workspace_temp/jira_board_issues.json
# Points per assignee in the active sprint
jq '[.[] | select(.activeSprint)] | group_by(.fields.assignee.displayName)
| map({assignee: .[0].fields.assignee.displayName, points: (map(.fields.storyPoints // 0) | add)})' \
workspace_temp/jira_board_issues.json
Notes
The story points and sprint custom fields are looked up by name (Story Points, or
Story point estimate on team-managed projects, and Sprint) and cached per clone. Their ids are
allocated per Jira instance, so the hardcoded customfield_10016/customfield_10020 of the shell
version projected null on any other tenant without saying anything. If the names cannot be found
at all, those ids are used as a last resort and a warning says so — and that last-resort id is
deliberately not cached, so the warning keeps appearing on every run instead of being silenced by
a cache entry that looks exactly like a resolved one.
The same restraint applies when two fields share a display name, which is ordinary on instances
that went through a Server-to-Cloud migration or that hold both a company-managed and a
team-managed project. Story points are looked up under Story Points first and Story point estimate second, and a name declared exactly once is preferred over one declared twice whatever
that order says — the order ranks how likely a name is to be the right field, not how trustworthy
the answer is, and a certainty beats a coin flip. Only when every candidate name is ambiguous does
the first declaration win, with a warning naming every candidate and nothing cached, because then
either id is a guess. To settle it, pin the id yourself:
mgsnake config set jira.field.sprint customfield_10020, which the warning spells out for you.
A pin and a cache entry live in different keys, and that separation is what makes pinning work at all:
| Key | Written by | Read |
|---|---|---|
jira.field.sprint |
you, with config set |
always, and it wins |
jira.field.sprint.cached |
the command itself | only when there is no pin, and not under --refresh |
Sharing one key looked tidy and quietly broke all three ways a pin can be used: the resolver wrote
the id it worked out on top of the pin as soon as a lookup succeeded, --refresh deleted the pin it
could not confirm, and — worst of the three — a pin was only read if the other field happened to
be cached too, so pinning the ambiguous field left the value sitting in the state file, unread,
while the guess kept being used. Nothing writes the bare key now except you. To undo a pin, remove
it: mgsnake config unset jira.field.sprint.
If you ran an earlier version of this command, the bare key may still hold what it wrote back then
as a cache, not a pin. The first run after upgrading moves it onto .cached automatically — reported
with an info message naming both keys — so it keeps behaving as a cache (re-resolved on --refresh)
instead of silently freezing on the value it happened to hold.
A pin you create yourself is never moved. The move is decided by a version marker the state file
carries, not by how the keys look: a legacy cache and a fresh pin are the same key holding the same
kind of value, so there is nothing in their shape to tell apart. Writing any setting stamps the
marker, so a pin made with config set is already stamped before the migration ever looks, and the
migration runs at most once per clone.
--refresh (-r) is the escape hatch for the opposite case: an id that did resolve, was cached,
and later changed on the Jira side — a board recreated, a custom field re-created by a migration. A
stale cached id is the one failure here that says nothing at all — storyPoints and sprint come
out null on every issue with a successful exit — so if the projection looks empty and no warning
explains it, re-run with --refresh. It re-resolves the board id and both field ids, and it is
symmetric: a cached id the refresh cannot confirm is dropped rather than left behind, so the next
run resolves it again instead of quietly answering with the entry you just asked it to distrust.
Pins are untouched — --refresh distrusts what the tool worked out, never what you decided — so if
a run keeps returning the same id despite the flag, check for a pin with
mgsnake config list | grep jira.field.
If the values you get differ from the old script's, the new ones are the correct ones.
With --output the working path is left alone entirely: nothing is created, nothing is prompted for
and nothing is excluded from git. Without it, the default destination lives inside the working path,
so the command offers to create the folder when it is missing.
The download reads the board's own filter, so "every issue of the board" means exactly what Jira means by it — including issues that live outside the project when the filter says so.
Progress goes to the console and the data only to the file, so --quiet is safe to combine with
anything. Every failure exits 1 with the reason on stderr.
On a corporate machine with a TLS-inspecting proxy the request layer will not see the corporate root
CA, because it validates against its own bundled certificate store rather than the system one. Point
REQUESTS_CA_BUNDLE at the corporate bundle:
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/corporate-ca-bundle.crt
Never disable verification instead.
jira-sprint
Prints the active sprints of a Jira project's Agile board to stdout as a JSON array, and nothing else, so the output can be captured with command substitution. The array is always an array: one active sprint yields a one-element list, and a board with none (kanban, or a sprint that was never started) yields an empty list and a successful exit.
Synopsis: mgsnake jira-sprint [OPTIONS] [PROJECT_KEY]
Aliases: js
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
project_key— Jira project key. Defaults to the stored jira.project_key.
Answers "what is the team working on right now?" for the board behind a project, in the shape the Jira skills consume.
Output
Nothing on disk. The board lookup it performs first may populate the cached jira.board_id, exactly
as jira-board does.
Each entry carries id, name, startDate, endDate, cloudDomain and boardId — the same keys
the shell version produced, with boardId now a number.
Examples
mgsnake jira-sprint | jq '.[0].name'
mgsnake jira-sprint TAROTAPP | jq -r '.[] | "\(.id) \(.name)"'
Notes
Breaking change. The result is a JSON array. getSprintInfo.sh piped .values[] through jq
without wrapping it, so a single active sprint came out as a bare object and two came out as two
concatenated objects — which is not a JSON document at all and blows up in json.load. Filters that
assumed a single object need jq '.[0]'.
A board with no active sprint (a kanban board, or a sprint that was never started) prints [] and
exits 0. That is an answer, not a failure.
Boards are per project, so this always resolves the board first; with a warm cache that costs no extra request.
The sprint listing is paged through to the end. Jira's Agile API pages with startAt/isLast and
never sends a continuation token, so a board with a long sprint history cannot hide an active sprint
on page two — which would otherwise show up in jira-issues as every one of that sprint's issues
being flagged activeSprint: false.
Light Weight
create-release
Creates a GitHub release and tag: the new tag is the latest release's version with one of its components incremented, and the publication is delegated to the gh CLI.
Synopsis: mgsnake create-release [OPTIONS] {p|r|l} [NOTES] [BRANCH]
Aliases: release, cr
| Option | Description |
|---|---|
-p, --tag-pattern TEXT |
Pattern describing this project's release tags, where $1, $2 and $3 stand for the major, minor and patch numbers and everything else is literal ($$ is a literal $). Defaults to v$1.$2.$3, or to the release_tag_pattern property when the project sets one. The pattern must match the latest release's tag, or the command stops. |
-s, --tag-suffix TEXT |
Pre-release label appended to the new tag (v1.2.4-.N). Only valid for the 'p' and 'r' release types: a 'l' release takes over the latest pointer, which GitHub only ever grants to a plain version, so the two are mutually exclusive. |
-v, --version-part [patch|minor|major] |
Which component of the latest release's version to increment: 'patch' (the last number), 'minor' (the middle one, resetting the patch to zero) or 'major' (the first one, resetting the other two to zero). [default: patch] |
-h, --help |
Show this message and exit. |
release_type— 'p' (prerelease) | 'l' (latest) | 'r' (regular release)notes— release notesbranch— branch to create the release from. Default is the current branch.
The release type decides how visible the release is once published. A pre-release is announced
as unfinished, so it never becomes the version GitHub offers by default — the usual choice for a
build meant for testing. A latest release is the opposite: it takes over the latest pointer
and becomes what users land on, which is why the command asks for confirmation first. A plain
release publishes without touching that pointer, so an older version stays the recommended one;
if GitHub moves it anyway, the command puts it back where it was.
The new tag is derived, never typed: the command reads the latest release, increments one component of its version, and uses the result. That is what keeps the sequence continuous — the next release always follows the one actually published, so two people cutting releases from different checkouts cannot invent conflicting numbers.
--version-part chooses which component moves, and everything to its right restarts:
From v1.2.3 |
Result | When to use it |
|---|---|---|
--version-part patch (default) |
v1.2.4 |
Fixes and changes that keep the same behaviour |
--version-part minor |
v1.3.0 |
New functionality that stays backwards compatible |
--version-part major |
v2.0.0 |
Breaking changes |
Resetting is what keeps the order monotonic: a minor bump that produced v1.3.3 would sit above the
patches that follow it.
--tag-suffix marks the result as a pre-release build of that version — v1.2.4-beta.0 — with a
counter that grows so the same version can be built repeatedly. The base version is derived the same
way as for a plain release, so a pre-release always announces a version that has not shipped yet:
v1.2.5-beta.0 precedes v1.2.5, never trails v1.2.4. Pre-release tags do not raise the ceiling
either, so one beta cannot push the next release past it. It is rejected for the l type:
GitHub only grants the latest pointer to a plain version, so asking for a suffixed latest release
is something the platform cannot honour.
Publishing is delegated to the gh CLI, which means it reuses the GitHub
authentication you already have — there is no token to configure here.
Examples
# A patch release from the current branch
mgsnake cr l
# A minor release with notes, cut from a specific branch
mgsnake cr l "Adds the man command" release/2.1 --version-part minor
# A prerelease build of the next patch: v1.2.4-beta.0, then -beta.1, ...
mgsnake cr p --tag-suffix beta
# A prerelease, which never takes over the latest pointer
mgsnake cr p
Notes
Light-weight: it runs from anywhere, no workspace required. When branch is omitted the release is
cut from the current branch.
The new tag is derived from the highest vX.Y.Z tag in the repository, not from the latest
pointer alone. Prereleases and r releases publish tags without moving that pointer, so it can sit
below tags that already exist; taking the maximum on every derivation is what makes the guarantee
hold unconditionally — a new tag can never land below one that was already published. The latest
release still decides whether the version is usable at all (see the note below).
Tag patterns
The tag format is not hard-coded. A pattern describes the tags this project already uses, with
$1, $2 and $3 standing for the major, minor and patch numbers; everything else is literal, and
$$ is a literal $. The same string parses the current tag and renders the next one, so the two
can never disagree.
| Pattern | Latest tag | Next patch |
|---|---|---|
v$1.$2.$3 (default) |
v1.2.3 |
v1.2.4 |
$1.$2.$3 |
1.2.3 |
1.2.4 |
rel-$1_$2_$3 |
rel-1_2_3 |
rel-1_2_4 |
Set it per invocation with --tag-pattern, or per project with the release_tag_pattern property.
All three placeholders are required, since --version-part names exactly those three components.
The pattern must match the tag of the latest release, and the command stops when it does not — a pattern that describes nothing in the repository would otherwise fail much later, with nothing pointing at it as the cause. Only tags the pattern recognises count towards the next version, so tags left over from a different scheme never raise the ceiling.
expired-certs-jks
Analyze certificates in a Java KeyStore (JKS) file and report their validity status
Synopsis: mgsnake expired-certs-jks [OPTIONS] JKS_PATH
Aliases: ecj
| Option | Description |
|---|---|
-p, --password TEXT |
Custom password for the JKS file [default: changeit; required] |
-v, --verbose |
Print the full certificate details of the expired certificates |
-h, --help |
Show this message and exit. |
jks_path— Path to the Java KeyStore file to analyze.
Lists every alias in the keystore with its validity dates and raises a warning for the expired ones, so you find out before a local dev environment breaks on an expired SSL certificate.
Examples
mgsnake expired-certs-jks /path/to/keystore.jks
mgsnake expired-certs-jks /path/to/keystore.jks --password mypassword
Notes
Parsing relies on keytool -v -list and expects its standard English date format
(Mon Jan 01 00:00:00 UTC 2026), which depends on the system locale and the installed Java
version. An alias without date information is warned about and skipped; a date in an unexpected
format aborts the run with an error rather than reporting a wrong status. For expired
certificates the command prints the keytool commands to delete and re-import them.
load-env
Exports the variables declared in an environment file into the current shell session. The file is a plain list of KEY=value lines: no export keyword, one pair per line, # starts a comment, and surrounding single or double quotes around a value are stripped.
Synopsis: mgsnake load-env [OPTIONS] [ENV_FILE]
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
env_file— Path to the environment file to load. When omitted, the local environment file (seelocal-env-path, e.g..mgsnake.envunder workspace_temp) is loaded if it exists; otherwise.envin the current directory is loaded instead.
Exports the variables declared in an environment file into the shell session that runs the command,
so a project's settings can be picked up without restarting the terminal or writing an export for
each one.
The file format is deliberately plain, and is not a shell script:
# Comments start with a hash
DATABASE_URL=postgres://localhost:5432/dev
API_TOKEN="quoted values work too"
One KEY=value per line, no export keyword, blank lines and # comments ignored, and a matching
pair of surrounding single or double quotes stripped from the value. Because the file is parsed
rather than executed, it holds values only: command substitutions and variable references are not
expanded.
Examples
# No argument: loads the local environment file (see `local-env-path`) if it exists,
# otherwise falls back to .env in the current directory
mgsnake load-env
# Load a specific file
mgsnake load-env config/staging.env
Notes
A process cannot change the environment of the process that started it, which is a guarantee of the
operating system rather than a limitation of this tool. The command therefore does no work itself:
it reports the request through its exit status, and the mgsnake shell function installed by the
init script performs the exports inside the session that asked for it. That function finds the file
name by re-reading the arguments it was given, so global options are handled normally and
mgsnake --log-level DEBUG load-env staging.env loads staging.env.
That function is the reason this works, so the command is only useful once
config_setup.sh / config_setup.ps1 is sourced from the shell profile — see shell-path. Run
without it, the command exits with its status and nothing happens.
Once the shell has performed the exports the request is fulfilled, so the function reports success
to whoever called it. The command is therefore safe inside && chains and under set -e; only a
direct invocation that bypasses the function (command mgsnake load-env) shows the raw status.
A missing file is not an error: nothing is exported and the command stays silent, so an optional
.env can be loaded unconditionally from a startup script.
Called with no env_file, the shell first looks for the local environment file (the path
local-env-path prints, e.g. .mgsnake.env under workspace_temp) and loads that if it exists;
only when it does not does it fall back to .env in the current directory. That fallback applies
when you type mgsnake load-env yourself with no argument; a future release will let it be turned
on or off.
config_setup.sh / config_setup.ps1 also load the local environment file automatically every time
a new session starts, but they do it by resolving local-env-path themselves and passing it in
explicitly, precisely so that automatic, unattended call never takes the .env-in-the-current-
directory fallback — only a local environment file that actually exists gets loaded at startup.
The environment file created by init-local-config is already loaded by the generated configuration
file, so it needs no explicit call here. Use this command for the other ones.
local-config-path
Prints to stdout the path of the local configuration file (.sh or .ps1 depending on the active shell).
Synopsis: mgsnake local-config-path [OPTIONS]
Aliases: lcp
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
Resolves the local configuration file created by init-local-config, picking the .sh or .ps1
variant according to the active shell.
Notes
Its stdout is consumed by command substitution inside config_setup.sh:
local_config_file=$(mgsnake local-config-path)
so the command prints the path and nothing else. Diagnostics go to stderr precisely so this stays parseable at any log level.
local-env-path
Prints to stdout the path of the local environment file.
Synopsis: mgsnake local-env-path [OPTIONS]
Aliases: lep
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
Resolves the local environment file created by init-local-config — the KEY=value file whose
variables the generated configuration file exports on every shell startup.
Notes
Its stdout is consumed by command substitution inside config_setup.sh:
local_env_file=$(mgsnake local-env-path)
so the command prints the path and nothing else. Diagnostics go to stderr precisely so this stays parseable at any log level.
msg
Prints a message to the console in a custom format and logs it into the workspace configuration log file.
Synopsis: mgsnake msg [OPTIONS] MESSAGE
Aliases: message
| Option | Description |
|---|---|
-p, --prologue TEXT |
An optional starting message printed before the message. |
-e, --epilog TEXT |
An optional ending message printed after the message. |
-t, --type-msg [s|i|w|e|a|t] |
The type of message to be printed: 'S' - Success 'I' - Information -- default 'W' - Warning 'E' - Error 'A' - Advice -- use for Debugging 'T' - Tip |
-h, --help |
Show this message and exit. |
message— The message to print and log.
Exposes the internal logging mechanism to the shell. It exists so that the packaged shell scripts
(config_setup.sh / config_setup.ps1) print success, warning and error messages in exactly the
same format as the Python commands, instead of each one inventing its own echo.
Notes
The message is both printed to the console and written to the workspace log file.
reload-config
Re-sources the local configuration file (and the environment file it loads) into the current shell session, so edits to it take effect without opening a new terminal.
Synopsis: mgsnake reload-config [OPTIONS]
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
Applies edits to the local configuration file without opening a new terminal. Sourcing that file is what makes its functions, aliases and exported variables available, and a shell only does it at startup — so after editing it, the running session keeps the old definitions until something re-sources it. This is that something.
Notes
A process cannot change the environment of the process that started it, which is a guarantee of the
operating system rather than a limitation of this tool. The command therefore does no work itself:
it reports the request through its exit status, and the mgsnake shell function installed by the
init script performs the sourcing inside the session that asked for it.
That function is the reason this works, so the command is only useful once
config_setup.sh / config_setup.ps1 is sourced from the shell profile — see shell-path. Run
without it, the command exits with its status and nothing happens.
Once the shell has re-sourced the file the request is fulfilled, so the function reports success to
whoever called it. Only a direct invocation that bypasses the function (command mgsnake reload-config) shows the raw status.
Commands that rewrite the local files (working-env, set-java, set-gradle, set-maven,
init-local-config) already emit the same request when they finish, so running this afterwards is
usually unnecessary. Reach for it after editing the file by hand.
Reloading the configuration file also reloads the environment file, because the generated configuration file loads it on the way through.
shell-path
Prints to stdout the path of the packaged shell initialization script (config_setup.sh or config_setup.ps1) to be sourced from the shell profile.
Synopsis: mgsnake shell-path [OPTIONS] {bash|zsh|powershell|pwsh}
Aliases: sp
| Option | Description |
|---|---|
-h, --help |
Show this message and exit. |
shell— The shell to be initialized.
Add the matching line to your shell profile — this is what makes mgsnake shell integration active
in every new session.
Examples
For bash/zsh, in ~/.bashrc or ~/.zshrc:
. "$(mgsnake shell-path bash)"
For PowerShell, in your profile ($PROFILE):
. (mgsnake shell-path pwsh)
Notes
Sourcing that script sets MEGA_SNAKE_SHELL and defines the private helpers behind reload-config
and load-env. Because the profile calls this command before that variable exists, it is the one
command that runs with no initialization at all — which is also why it prints the bare path and
nothing else.
It also defines mgsnake itself as a thin shell function around the real executable. That function
is what makes the environment auto-reload work: a command that rewrites one of the local environment
files exits with status 29, and only the parent shell can act on it — a child process cannot
change its parent's environment. The function forwards every argument and calls the executable
through command mgsnake (or its resolved path on PowerShell), so there is no recursion. A served
29/30 signal is reported to the caller as 0 once the function has carried it out — the signal
is a request, and propagating it would make every environment command look like a failure to a
set -e script or an && chain; every other status passes through unchanged. The only visible
difference is that type mgsnake reports a function.
Metadata
Release files for mega_snake 0.1.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| mega_snake-0.1.7.tar.gz | 492.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mega_snake-0.1.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 743.1 kB
Release files / mega_snake-0.1.7.tar.gz
| Download URL | mega_snake-0.1.7.tar.gz |
|---|---|
| Size | 492.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
77e71e3846a9e21f7c61b7857a87314cde4acec5c64693dd8130bd20e76fb8a0
|
|
BLAKE2b-256 checksum How to use checksums |
52973a4749ac053acc94963697fd840f917d61dfc93715abaa4fbbc264946980
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / mega_snake-0.1.7-py3-none-any.whl
| Download URL | mega_snake-0.1.7-py3-none-any.whl |
|---|---|
| Size | 250.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4280ae1493b6085cfa5f6979a1a76e63370ab4762beed6eccf41b7a962e5b958
|
|
BLAKE2b-256 checksum How to use checksums |
7171b19039265a2b20a92079583854da050fbbd34e92cf932fba18a120c41cab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|