@andrew-codes/twg-axi 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # twg-axi
2
+
3
+ A drop-in wrapper for Atlassian's [`twg`](https://www.npmjs.com/package/twg) CLI that emits
4
+ [TOON](https://toonformat.dev) instead of JSON on stdout.
5
+
6
+ `twg-axi` mirrors `twg` exactly: every command, flag, and argument is forwarded to the real
7
+ `twg` binary unchanged. The only difference is output encoding. Any time `twg` would write raw
8
+ JSON to stdout (`--output json` or `--output jsonl`), `twg-axi` captures that JSON and re-emits
9
+ it as TOON, which is roughly 30-60% fewer tokens for an LLM to read than the equivalent JSON.
10
+ Everything else - plain text output, tables, `--output-summary`'s YAML envelope, errors, exit
11
+ codes, and stdin/stdout/stderr stream behavior - passes through from `twg` unaltered.
12
+
13
+ `twg-axi` does not implement any `twg` functionality itself. It is a thin subprocess wrapper
14
+ around the installed `twg` binary plus a JSON-to-TOON conversion step at the output boundary.
15
+
16
+ ## Requirements
17
+
18
+ - Node.js >= 18
19
+ - The [`twg` CLI](https://www.npmjs.com/package/twg) installed and authenticated on your `PATH`
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ npm install -g @andrew-codes/twg-axi
25
+ ```
26
+
27
+ This installs a `twg-axi` executable alongside `twg`. It does not install or configure `twg`
28
+ itself - `twg-axi` requires `twg` to already be present and authenticated.
29
+
30
+ ## Usage
31
+
32
+ Run `twg-axi` exactly as you would run `twg`, substituting the binary name:
33
+
34
+ ```sh
35
+ twg-axi jira issue list --project ABC --output json
36
+ ```
37
+
38
+ Any flag, subcommand, or argument accepted by `twg` is accepted identically by `twg-axi` and
39
+ forwarded through unchanged. See `twg --help` (or `twg-axi --help`, which is the same thing) for
40
+ the full command surface.
41
+
42
+ ### JSON becomes TOON
43
+
44
+ ```sh
45
+ $ twg jira issue list --project ABC --output json
46
+ {"items":[{"key":"ABC-1","summary":"Fix auth bug"},{"key":"ABC-2","summary":"Add pagination"}]}
47
+
48
+ $ twg-axi jira issue list --project ABC --output json
49
+ items[2]{key,summary}:
50
+ ABC-1,Fix auth bug
51
+ ABC-2,Add pagination
52
+ ```
53
+
54
+ The same applies to `--output jsonl`, where each JSON record is converted to its own TOON block.
55
+
56
+ ### Everything else passes through unchanged
57
+
58
+ Default text output, tables, `--output json --output-summary` (which emits a YAML envelope, not
59
+ raw JSON), error messages, and exit codes are all identical to running `twg` directly.
60
+
61
+ ## How it works
62
+
63
+ `twg-axi` spawns `twg` as a subprocess with the exact argv it was given.
64
+
65
+ - When `--output`/`-o` is not `json` or `jsonl` (or `--output-summary` is set, which makes `twg`
66
+ emit YAML instead of raw JSON), stdin/stdout/stderr are inherited directly from `twg` so
67
+ behavior - including streaming and TTY detection - is identical to running `twg` on its own.
68
+ - When `--output json` or `--output jsonl` is requested without `--output-summary`, `twg-axi`
69
+ captures `twg`'s stdout, parses it as JSON, and re-encodes it with
70
+ [`@toon-format/toon`](https://www.npmjs.com/package/@toon-format/toon) before writing it to
71
+ stdout. If the captured output isn't valid JSON (for example, `twg` exited before writing a
72
+ payload), it is passed through unchanged rather than dropped.
73
+
74
+ Exit codes and signals (`SIGINT`, `SIGTERM`, `SIGHUP`) are forwarded to the underlying `twg`
75
+ process in both cases.
76
+
77
+ ## Contributing
78
+
79
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for local development setup (Yarn PnP /
80
+ zero-installs) and [`docs/release-process.md`](docs/release-process.md) for how releases are
81
+ cut and published.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../cli.js";
3
+ main(process.argv.slice(2))
4
+ .then((code) => {
5
+ process.exitCode = code;
6
+ })
7
+ .catch((error) => {
8
+ const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
9
+ process.stderr.write(`${message}\n`);
10
+ process.exitCode = 1;
11
+ });
package/dist/cli.js ADDED
@@ -0,0 +1,50 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:os";
3
+ import { needsToonConversion, parseOutputMode } from "./output-mode.js";
4
+ import { convertToToon } from "./toon-convert.js";
5
+ // Overridable only so tests can point at a fixture binary instead of the real twg CLI.
6
+ const TWG_BIN = process.env.TWG_AXI_BIN ?? "twg";
7
+ const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
8
+ function reportSpawnFailure(error) {
9
+ if (error.code === "ENOENT") {
10
+ process.stderr.write(`twg-axi: could not find "${TWG_BIN}" on PATH. Install the twg CLI and ensure it is reachable.\n`);
11
+ return 127;
12
+ }
13
+ process.stderr.write(`twg-axi: failed to run "${TWG_BIN}": ${error.message}\n`);
14
+ return 1;
15
+ }
16
+ function exitCodeFor(code, signal) {
17
+ if (signal)
18
+ return 128 + (constants.signals[signal] ?? 0);
19
+ return code ?? 1;
20
+ }
21
+ async function runPassthrough(argv) {
22
+ return new Promise((resolve) => {
23
+ const child = spawn(TWG_BIN, argv, { stdio: "inherit" });
24
+ for (const signal of FORWARDED_SIGNALS) {
25
+ process.on(signal, () => child.kill(signal));
26
+ }
27
+ child.on("error", (error) => resolve(reportSpawnFailure(error)));
28
+ child.on("close", (code, signal) => resolve(exitCodeFor(code, signal)));
29
+ });
30
+ }
31
+ async function runWithConversion(argv, format) {
32
+ return new Promise((resolve) => {
33
+ const child = spawn(TWG_BIN, argv, { stdio: ["inherit", "pipe", "inherit"] });
34
+ const chunks = [];
35
+ for (const signal of FORWARDED_SIGNALS) {
36
+ process.on(signal, () => child.kill(signal));
37
+ }
38
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
39
+ child.on("error", (error) => resolve(reportSpawnFailure(error)));
40
+ child.on("close", (code, signal) => {
41
+ const raw = Buffer.concat(chunks).toString("utf8");
42
+ process.stdout.write(convertToToon(raw, format));
43
+ resolve(exitCodeFor(code, signal));
44
+ });
45
+ });
46
+ }
47
+ export async function main(argv) {
48
+ const mode = parseOutputMode(argv);
49
+ return needsToonConversion(mode) ? runWithConversion(argv, mode.format) : runPassthrough(argv);
50
+ }
@@ -0,0 +1,42 @@
1
+ const SUMMARY_LEVELS = new Set(["stats", "auto", "inline"]);
2
+ /**
3
+ * Scans raw argv for twg's global --output/-o and --output-summary flags without
4
+ * reimplementing twg's own argument parser. Used only to decide whether stdout will
5
+ * carry raw JSON that needs TOON conversion; the argv is always forwarded to twg unchanged.
6
+ */
7
+ export function parseOutputMode(argv) {
8
+ let format = "text";
9
+ let summary = false;
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const token = argv[i];
12
+ if (token === "--output" || token === "-o") {
13
+ format = argv[i + 1] ?? format;
14
+ i++;
15
+ continue;
16
+ }
17
+ if (token.startsWith("--output=")) {
18
+ format = token.slice("--output=".length);
19
+ continue;
20
+ }
21
+ if (token.startsWith("-o") && token.length > 2) {
22
+ format = token.slice(2);
23
+ continue;
24
+ }
25
+ if (token === "--output-summary") {
26
+ summary = true;
27
+ const next = argv[i + 1];
28
+ if (next !== undefined && SUMMARY_LEVELS.has(next))
29
+ i++;
30
+ continue;
31
+ }
32
+ if (token.startsWith("--output-summary=")) {
33
+ summary = true;
34
+ continue;
35
+ }
36
+ }
37
+ return { format, summary };
38
+ }
39
+ /** twg only emits raw JSON on stdout for --output json/jsonl without --output-summary. */
40
+ export function needsToonConversion(mode) {
41
+ return (mode.format === "json" || mode.format === "jsonl") && !mode.summary;
42
+ }
@@ -0,0 +1,23 @@
1
+ import { encode } from "@toon-format/toon";
2
+ /**
3
+ * Converts captured twg stdout to TOON. Falls back to returning the input unchanged
4
+ * when it isn't valid JSON (e.g. twg exited before writing a payload) so the wrapper
5
+ * never fabricates output that didn't come from twg.
6
+ */
7
+ export function convertToToon(raw, format) {
8
+ const text = raw.trim();
9
+ if (!text)
10
+ return raw;
11
+ try {
12
+ if (format === "jsonl") {
13
+ const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
14
+ const blocks = lines.map((line) => encode(JSON.parse(line)));
15
+ return `${blocks.join("\n")}\n`;
16
+ }
17
+ const data = JSON.parse(text);
18
+ return `${encode(data)}\n`;
19
+ }
20
+ catch {
21
+ return raw;
22
+ }
23
+ }
@@ -0,0 +1,68 @@
1
+ # Release process
2
+
3
+ `@andrew-codes/twg-axi` publishes to npm automatically. There is no manual version bump
4
+ and no manual `npm publish`.
5
+
6
+ ## How a release happens
7
+
8
+ Every push to `main` (i.e. every merged PR) runs `.github/workflows/release.yml`:
9
+
10
+ 1. Check out the repo with full history and tags.
11
+ 2. Install with Yarn PnP (`yarn install --immutable`) and run the full verification suite
12
+ (typecheck, tests, lint if present). If any step fails, the workflow stops here - nothing
13
+ is published, versioned, or tagged.
14
+ 3. Walk the commits since the last version tag and compute the next semantic version from
15
+ their messages (see below).
16
+ 4. Bump `package.json` to that version, build, and publish the package to npm under the
17
+ `@andrew-codes` scope.
18
+ 5. Only if publishing succeeds: commit the version bump back to `main` (with a `[skip ci]`
19
+ marker so it doesn't retrigger the workflow) and create + push a git tag for the new
20
+ version, then create a GitHub release for that tag.
21
+
22
+ ## How the version is computed
23
+
24
+ The workflow uses [Conventional Commits](https://www.conventionalcommits.org/) prefixes on
25
+ commit messages to decide the bump type:
26
+
27
+ | Commit message pattern | Bump |
28
+ | --------------------------------------------------------- | ----- |
29
+ | `fix: ...` | patch |
30
+ | `feat: ...` | minor |
31
+ | Any commit with a `BREAKING CHANGE:` footer, or `!` after the type/scope (e.g. `feat!:`) | major |
32
+ | No commits since the last tag match a recognized prefix | patch |
33
+
34
+ Only commits since the most recent version tag are considered. If several commits qualify
35
+ for different bump levels, the highest one wins (major > minor > patch).
36
+
37
+ To land a release at the bump level you want, write your commit messages accordingly:
38
+
39
+ ```
40
+ fix: correct exit code passthrough on SIGTERM
41
+ feat: support --output jsonl passthrough
42
+ feat!: drop Node 16 support
43
+
44
+ BREAKING CHANGE: minimum supported Node.js version is now 18
45
+ ```
46
+
47
+ Commits that don't follow this convention (e.g. `chore:`, `docs:`, or unprefixed messages)
48
+ don't influence the bump; if a release contains only such commits, it still ships as a patch.
49
+
50
+ ## Local development under Yarn PnP / zero-installs
51
+
52
+ This project uses Yarn Plug'n'Play with zero-installs: `.pnp.cjs`, `.yarn/cache`, and
53
+ `.yarn/releases` are committed to the repo, so `yarn install` after a fresh clone does not
54
+ hit the network for anything already in the cache and does not create a `node_modules`
55
+ directory.
56
+
57
+ Practical implications:
58
+
59
+ - Don't `rm -rf node_modules` to "reset" things - there isn't one. If something seems stale,
60
+ `yarn install` again.
61
+ - Adding or upgrading a dependency (`yarn add`, `yarn up`) updates `.pnp.cjs`,
62
+ `yarn.lock`, and `.yarn/cache` together; commit all three.
63
+ - Editors need the Yarn PnP SDK/IDE integration to resolve TypeScript correctly (see
64
+ [`CONTRIBUTING.md`](../CONTRIBUTING.md) for setup).
65
+ - Tools that read `node_modules` directly instead of through Node's module resolution (some
66
+ native binaries, some editor plugins) may need a package marked
67
+ `dependenciesMeta.<name>.unplugged: true` in `package.json` to force it to be extracted to
68
+ a real folder under `.yarn/unplugged` instead of read from the zip cache.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@andrew-codes/twg-axi",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in wrapper for Atlassian's twg CLI that emits TOON instead of JSON on stdout",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Andrew Smith <andrew@andrew.codes>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/andrew-codes/atlassian-twg-axi.git"
11
+ },
12
+ "bin": "./dist/bin/twg-axi.js",
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "docs"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json && chmod +x dist/bin/twg-axi.js",
26
+ "pretest": "yarn build",
27
+ "test": "vitest run",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit",
29
+ "lint": "eslint ."
30
+ },
31
+ "dependencies": {
32
+ "@toon-format/toon": "^4.1.1"
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/js": "^10.0.1",
36
+ "@types/node": "^22.10.0",
37
+ "conventional-commits-parser": "^7.1.2",
38
+ "eslint": "^10.11.0",
39
+ "globals": "^17.12.0",
40
+ "semver": "^7.6.3",
41
+ "typescript": "^5.9.3",
42
+ "typescript-eslint": "^8.70.1",
43
+ "vite": "^8.0.0",
44
+ "vitest": "5.0.1"
45
+ },
46
+ "packageManager": "yarn@4.18.1"
47
+ }