@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 +21 -0
- package/README.md +81 -0
- package/dist/bin/twg-axi.js +11 -0
- package/dist/cli.js +50 -0
- package/dist/output-mode.js +42 -0
- package/dist/toon-convert.js +23 -0
- package/docs/release-process.md +68 -0
- package/package.json +47 -0
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
|
+
}
|