@astrazds/fjgo 1.4.1

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 Andrejs Strazds
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,179 @@
1
+ # fjgo
2
+
3
+ `fjgo` helps coding agents work with repositories hosted on
4
+ [Forgejo](https://forgejo.org/).
5
+
6
+ It can read and update issues, pull requests, releases, Actions runs,
7
+ repository settings, labels, secrets, and more. It also gives agents a safe
8
+ way to use the full Forgejo API when there is no shorter command.
9
+
10
+ ## Why use it?
11
+
12
+ An agent can call the Forgejo API with `curl`, but it has to remember URLs,
13
+ JSON shapes, and authentication rules. That creates extra work and makes
14
+ mistakes more likely.
15
+
16
+ `fjgo` gives the agent:
17
+
18
+ - short commands for common Forgejo jobs;
19
+ - compact output that uses fewer tokens;
20
+ - clear errors and help;
21
+ - safe request previews before making changes;
22
+ - automatic protection against printing passwords or API tokens;
23
+ - access to every operation in the bundled Forgejo API specification.
24
+
25
+ Use normal `git` commands for commits, branches, and local files. Use `fjgo`
26
+ for tasks that need the Forgejo server.
27
+
28
+ ## Install
29
+
30
+ ### Agent Skill
31
+
32
+ Install the Agent Skill globally:
33
+
34
+ ```sh
35
+ npx skills add https://repos.astrazds.net/astrazds/fjgo.git --skill fjgo -g
36
+ ```
37
+
38
+ That is the full setup. You do not need to clone this repository or run
39
+ `npm install`.
40
+
41
+ The skill teaches your agent to run the CLI through `npx -y @astrazds/fjgo`.
42
+ The npm package is scoped because unscoped `fjgo` is blocked on the public
43
+ registry. The installed command name remains `fjgo`. The first run downloads
44
+ the matching native release and saves it in a local cache. Later runs reuse
45
+ that copy.
46
+
47
+ Requirements: Node.js 20 or newer, on Linux or macOS with an x64 or arm64 CPU.
48
+
49
+ Without Node.js, install a native release archive:
50
+
51
+ ```sh
52
+ curl -LO https://repos.astrazds.net/astrazds/fjgo/releases/download/v1.4.1/fjgo_v1.4.1_linux_amd64.tar.gz
53
+ tar -xzf fjgo_v1.4.1_linux_amd64.tar.gz
54
+ install -Dm755 fjgo_v1.4.1_linux_amd64/fjgo ~/.local/bin/fjgo
55
+ ```
56
+
57
+ Use `darwin` instead of `linux`, and `arm64` instead of `amd64`, when that
58
+ matches the machine. Then run `fjgo` directly instead of `npx -y @astrazds/fjgo`.
59
+
60
+ ### Optional ambient hooks
61
+
62
+ The skill and the native binary are enough for on-demand use. If you want
63
+ Forgejo context injected at the start of every agent session, install the
64
+ optional hooks after the CLI is on PATH:
65
+
66
+ ```sh
67
+ npx -y @astrazds/fjgo setup hooks --check
68
+ npx -y @astrazds/fjgo setup hooks
69
+ ```
70
+
71
+ You only need the skill or the hooks. Installing both is fine; the hooks add
72
+ live session context, and the skill remains available on demand.
73
+
74
+ ### Codex plugin
75
+
76
+ This repository is also a validated Codex plugin package. A marketplace can
77
+ point at the repository root to distribute the existing `fjgo` skill with
78
+ plugin presentation metadata and starter prompts. Until a marketplace lists
79
+ it, the Agent Skill command above remains the shortest public installation
80
+ path.
81
+
82
+ The plugin still runs `npx -y @astrazds/fjgo`; it does not bundle another API client,
83
+ install ambient hooks automatically, provide OAuth, or store credentials.
84
+ See [Codex plugin](docs/codex-plugin.md) for marketplace installation, local
85
+ testing, authentication, and maintenance details.
86
+
87
+ ## Connect to Forgejo
88
+
89
+ Set your Forgejo host and access token:
90
+
91
+ ```sh
92
+ export FJGO_HOST=forgejo.example.com
93
+ export FJGO_TOKEN=your_access_token
94
+ ```
95
+
96
+ Create the token in your Forgejo account settings. Give it only the permissions
97
+ needed for your task. Do not paste the token into an agent prompt or commit it
98
+ to a file.
99
+
100
+ ## First use
101
+
102
+ Run these commands inside a repository that has a Forgejo Git remote named
103
+ `origin`:
104
+
105
+ ```sh
106
+ npx -y @astrazds/fjgo -R origin
107
+ npx -y @astrazds/fjgo -R origin doctor
108
+ npx -y @astrazds/fjgo -R origin issue list --state open
109
+ npx -y @astrazds/fjgo -R origin pr list --state open
110
+ npx -y @astrazds/fjgo -R origin run list
111
+ ```
112
+
113
+ `-R origin` reads the repository owner and name from the Git remote. You can
114
+ also choose a repository directly:
115
+
116
+ ```sh
117
+ npx -y @astrazds/fjgo --repo OWNER/REPO repo get
118
+ npx -y @astrazds/fjgo issue list OWNER/REPO --state open
119
+ ```
120
+
121
+ ## Make a change safely
122
+
123
+ Commands that change Forgejo require `--yes`. Preview the request with
124
+ `--dry-run` first:
125
+
126
+ ```sh
127
+ npx -y @astrazds/fjgo -R origin issue create \
128
+ --title "Fix the login page" \
129
+ --body "The login button is not working." \
130
+ --dry-run --yes
131
+ ```
132
+
133
+ If the preview is correct, remove `--dry-run`:
134
+
135
+ ```sh
136
+ npx -y @astrazds/fjgo -R origin issue create \
137
+ --title "Fix the login page" \
138
+ --body "The login button is not working." \
139
+ --yes
140
+ ```
141
+
142
+ More examples:
143
+
144
+ ```sh
145
+ npx -y @astrazds/fjgo -R origin issue view 42 --comments --full
146
+ npx -y @astrazds/fjgo -R origin pr checks 12
147
+ npx -y @astrazds/fjgo -R origin release list
148
+ npx -y @astrazds/fjgo -R origin workflow list
149
+ npx -y @astrazds/fjgo -R origin search issues "login" --state open
150
+ ```
151
+
152
+ Every command has focused help:
153
+
154
+ ```sh
155
+ npx -y @astrazds/fjgo issue --help
156
+ npx -y @astrazds/fjgo issue create --help
157
+ ```
158
+
159
+ ## Learn more
160
+
161
+ - [Wiki manual](https://repos.astrazds.net/astrazds/fjgo/wiki): concise operator
162
+ guidance published through Forgejo from the reviewed sources in `docs/wiki`.
163
+ - [CLI reference](docs/cli-reference.md): authentication, repository selection,
164
+ output, command groups, and the full API escape hatch.
165
+ - [Agent setup prompt](docs/agent-setup-prompt.md): a ready-to-paste setup prompt
166
+ for another coding agent.
167
+ - [Codex plugin](docs/codex-plugin.md): plugin packaging, marketplace
168
+ installation, authentication boundaries, and validation.
169
+ - [Development guide](docs/development.md): build, test, generate code, and make
170
+ releases, including the deterministic offline agent-job benchmark and its
171
+ scenario-run record format. CI runs through `.forgejo/workflows/verify.yml`.
172
+ - [AXI compliance](docs/axi-compliance.md): the agent-friendly interface rules
173
+ followed by `fjgo`.
174
+ - [Field validation](docs/alpha.md): the v1.4.1 live-testing checklist.
175
+ - [Changelog](CHANGELOG.md): release history.
176
+
177
+ ## License
178
+
179
+ MIT
package/bin/fjgo.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../lib/launcher.js";
4
+
5
+ run(process.argv.slice(2)).catch((error) => {
6
+ process.stdout.write(`error: unable to run fjgo: ${error.message}\n`);
7
+ process.stdout.write("help: check network access or install a native fjgo release\n");
8
+ process.exitCode = 1;
9
+ });
@@ -0,0 +1,111 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { gunzipSync } from "node:zlib";
8
+
9
+ const releaseBase = "https://repos.astrazds.net/astrazds/fjgo/releases/download";
10
+
11
+ export function releaseTarget(platform = process.platform, arch = process.arch) {
12
+ const platforms = { darwin: "darwin", linux: "linux" };
13
+ const architectures = { arm64: "arm64", x64: "amd64" };
14
+ if (!platforms[platform] || !architectures[arch]) {
15
+ throw new Error(`unsupported platform ${platform}/${arch}; supported platforms are linux and macOS on x64 or arm64`);
16
+ }
17
+ return { os: platforms[platform], arch: architectures[arch] };
18
+ }
19
+
20
+ export function checksumFor(checksums, archiveName) {
21
+ for (const line of checksums.split(/\r?\n/)) {
22
+ const match = line.trim().match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
23
+ if (match && basename(match[2]) === archiveName) {
24
+ return match[1].toLowerCase();
25
+ }
26
+ }
27
+ throw new Error(`release checksums do not contain ${archiveName}`);
28
+ }
29
+
30
+ function tarString(block, start, length) {
31
+ return block.subarray(start, start + length).toString("utf8").replace(/\0.*$/, "");
32
+ }
33
+
34
+ export function extractBinary(archive, expectedPath) {
35
+ const tar = gunzipSync(archive);
36
+ for (let offset = 0; offset + 512 <= tar.length; ) {
37
+ const header = tar.subarray(offset, offset + 512);
38
+ if (header.every((byte) => byte === 0)) break;
39
+ const name = tarString(header, 0, 100);
40
+ const prefix = tarString(header, 345, 155);
41
+ const path = prefix ? `${prefix}/${name}` : name;
42
+ const sizeText = tarString(header, 124, 12).trim();
43
+ const size = Number.parseInt(sizeText || "0", 8);
44
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error("release archive has an invalid entry size");
45
+ const dataStart = offset + 512;
46
+ const dataEnd = dataStart + size;
47
+ if (dataEnd > tar.length) throw new Error("release archive is truncated");
48
+ if (path === expectedPath) return Buffer.from(tar.subarray(dataStart, dataEnd));
49
+ offset = dataStart + Math.ceil(size / 512) * 512;
50
+ }
51
+ throw new Error(`release archive does not contain ${expectedPath}`);
52
+ }
53
+
54
+ async function download(url, fetchImpl) {
55
+ const response = await fetchImpl(url, { redirect: "follow", signal: AbortSignal.timeout(30_000) });
56
+ if (!response.ok) throw new Error(`download failed with HTTP ${response.status} for ${url}`);
57
+ return Buffer.from(await response.arrayBuffer());
58
+ }
59
+
60
+ export async function ensureBinary(options = {}) {
61
+ const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
62
+ const packageJSON = JSON.parse(await readFile(packagePath, "utf8"));
63
+ const version = options.version ?? packageJSON.version;
64
+ const tag = version.startsWith("v") ? version : `v${version}`;
65
+ const target = releaseTarget(options.platform, options.arch);
66
+ const archiveName = `fjgo_${tag}_${target.os}_${target.arch}.tar.gz`;
67
+ const entryName = `fjgo_${tag}_${target.os}_${target.arch}/fjgo`;
68
+ const cacheRoot = options.cacheDir ?? process.env.FJGO_NPX_CACHE_DIR ?? join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "fjgo");
69
+ const binary = join(cacheRoot, tag, `${target.os}-${target.arch}`, "fjgo");
70
+
71
+ try {
72
+ await chmod(binary, 0o755);
73
+ return binary;
74
+ } catch (error) {
75
+ if (error.code !== "ENOENT") throw error;
76
+ }
77
+
78
+ const base = (options.baseUrl ?? process.env.FJGO_NPX_RELEASE_BASE ?? releaseBase).replace(/\/$/, "");
79
+ const fetchImpl = options.fetchImpl ?? fetch;
80
+ const [archive, checksumsBuffer] = await Promise.all([
81
+ download(`${base}/${tag}/${archiveName}`, fetchImpl),
82
+ download(`${base}/${tag}/checksums.txt`, fetchImpl),
83
+ ]);
84
+ const expected = checksumFor(checksumsBuffer.toString("utf8"), archiveName);
85
+ const actual = createHash("sha256").update(archive).digest("hex");
86
+ if (actual !== expected) throw new Error(`checksum mismatch for ${archiveName}`);
87
+
88
+ const contents = extractBinary(archive, entryName);
89
+ await mkdir(dirname(binary), { recursive: true });
90
+ const temporary = `${binary}.${process.pid}.tmp`;
91
+ try {
92
+ await writeFile(temporary, contents, { mode: 0o755 });
93
+ await rename(temporary, binary);
94
+ } finally {
95
+ await rm(temporary, { force: true });
96
+ }
97
+ return binary;
98
+ }
99
+
100
+ export async function run(args) {
101
+ const binary = await ensureBinary();
102
+ const child = spawn(binary, args, { stdio: "inherit", env: process.env });
103
+ await new Promise((resolve, reject) => {
104
+ child.once("error", reject);
105
+ child.once("exit", (code, signal) => {
106
+ if (signal) process.kill(process.pid, signal);
107
+ else process.exitCode = code ?? 1;
108
+ resolve();
109
+ });
110
+ });
111
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@astrazds/fjgo",
3
+ "version": "1.4.1",
4
+ "description": "Agent-first CLI and API client for Forgejo",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "fjgo": "bin/fjgo.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://repos.astrazds.net/astrazds/fjgo.git"
21
+ },
22
+ "homepage": "https://repos.astrazds.net/astrazds/fjgo",
23
+ "bugs": {
24
+ "url": "https://repos.astrazds.net/astrazds/fjgo/issues"
25
+ },
26
+ "scripts": {
27
+ "test": "node --test test/*.test.js",
28
+ "prepack": "npm test"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "devDependencies": {
34
+ "@openai/codex": "0.145.0"
35
+ }
36
+ }