@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 +21 -0
- package/README.md +179 -0
- package/bin/fjgo.js +9 -0
- package/lib/launcher.js +111 -0
- package/package.json +36 -0
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
|
+
});
|
package/lib/launcher.js
ADDED
|
@@ -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
|
+
}
|