agnostic-ai 0.62.0 → 0.64.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/README.md CHANGED
@@ -13,10 +13,26 @@ Or install it globally:
13
13
  npm install -g agnostic-ai
14
14
  ```
15
15
 
16
- This package is a thin wrapper: it downloads the prebuilt Go binary for your platform from [GitHub Releases](https://github.com/Chemaclass/agnostic-ai/releases) and runs it. Supported platforms are macOS, Linux, and Windows on x64 and arm64.
16
+ npm is one route of several. The same binary installs through Homebrew on macOS and Linux:
17
17
 
18
- The download normally happens on install. Under npm's install-script gating (`--ignore-scripts`, or npm 11's default prompt), it happens on first run instead. Pin a different binary version with `AGNOSTIC_AI_VERSION=v0.45.0`.
18
+ ```bash
19
+ brew install --cask Chemaclass/tap/agnostic-ai
20
+ ```
21
+
22
+ Or through the install script, which needs no package manager:
23
+
24
+ ```bash
25
+ curl -fsSL https://raw.githubusercontent.com/Chemaclass/agnostic-ai/main/scripts/install.sh | bash
26
+ ```
27
+
28
+ Windows, Go, and manual download are covered in [all install options](https://agnostic-ai.org/docs/installation/).
29
+
30
+ This package is a thin wrapper around the prebuilt Go binary. The binary ships inside a platform package (`@agnostic-ai/darwin-arm64` and five siblings), declared as optional dependencies. npm reads each one's `os` and `cpu` and installs only the one that matches your machine. Supported platforms are macOS, Linux, and Windows on x64 and arm64.
31
+
32
+ Nothing is downloaded and no install script runs, so the package works under `--ignore-scripts`, behind a proxy, and from an offline npm mirror. To pin a version, pin the package: `npm install -g agnostic-ai@<version>`.
33
+
34
+ Set `AGNOSTIC_AI_BINARY` to an absolute path to run a binary this package does not ship, such as one you built yourself.
19
35
 
20
- Full docs, targets, and configuration: [github.com/Chemaclass/agnostic-ai](https://github.com/Chemaclass/agnostic-ai).
36
+ Full docs, targets, and configuration: [agnostic-ai.org](https://agnostic-ai.org).
21
37
 
22
38
  MIT licensed.
@@ -2,14 +2,80 @@
2
2
  'use strict'
3
3
 
4
4
  const { spawnSync } = require('node:child_process')
5
- const { ensureBinary } = require('../lib/download')
5
+ const os = require('node:os')
6
+ const path = require('node:path')
7
+ const { entryPoint, packageName, platformFor } = require('../lib/platforms')
6
8
 
7
- async function main() {
9
+ const DOCS = 'https://agnostic-ai.org/docs/installation/'
10
+
11
+ // A global install resolves its dependencies from the global tree, a project
12
+ // install from the project's, and `npm install` without `-g` only ever writes
13
+ // the second. Telling a globally installed CLI to run the project command
14
+ // leaves it exactly as broken as it was, so the repair hint has to know which
15
+ // tree it is sitting in.
16
+ //
17
+ // npm puts a global package under its own prefix: `<prefix>/lib/node_modules`
18
+ // on macOS and Linux, `<prefix>\node_modules` on Windows, where <prefix> is
19
+ // npm's own directory (commonly `.../npm`). A project install sits in a
20
+ // `node_modules` whose parent is the project. Unrecognised layouts fall back
21
+ // to the project command, which is the common case, and the message names the
22
+ // other form either way.
23
+ function isGlobalInstall(dir) {
24
+ const parts = path.resolve(dir).split(path.sep)
25
+ const i = parts.lastIndexOf('node_modules')
26
+ if (i < 1) return false
27
+ return parts[i - 1] === 'lib' || parts[i - 1] === 'npm'
28
+ }
29
+
30
+ // The binary ships inside a platform package that npm installed as an optional
31
+ // dependency, so finding it is a module lookup, not a download. Nothing here
32
+ // touches the network.
33
+ function resolveBinary() {
34
+ // Escape hatch for a binary this package does not ship: a local build, an
35
+ // unsupported CPU, an air-gapped machine that already has one on disk.
36
+ const override = process.env.AGNOSTIC_AI_BINARY
37
+ if (override) return override
38
+
39
+ const platform = platformFor(process.platform, process.arch)
40
+ if (!platform) {
41
+ throw new Error(
42
+ `no prebuilt binary for ${process.platform}/${process.arch}. Build one with ` +
43
+ '`go install github.com/chemaclass/agnostic-ai/cmd/agnostic-ai@latest` and point ' +
44
+ `AGNOSTIC_AI_BINARY at it, or pick another route: ${DOCS}`
45
+ )
46
+ }
47
+
48
+ try {
49
+ return require.resolve(entryPoint(platform))
50
+ } catch {
51
+ // npm reuses a lockfile written on another platform without re-resolving
52
+ // optional dependencies, which leaves the matching package absent
53
+ // (npm/cli#4828). `--no-optional` and `--omit=optional` do the same on
54
+ // purpose. Both look like a successful install until the CLI runs.
55
+ //
56
+ // --include=optional is not decoration: the second cause is an
57
+ // `omit=optional` sitting in the user's npm config, and a plain reinstall
58
+ // obeys it and skips the package again.
59
+ const isGlobal = isGlobalInstall(__dirname)
60
+ const scope = isGlobal ? 'installed globally' : 'installed in this project'
61
+ const fix = isGlobal
62
+ ? 'npm install -g agnostic-ai --force --include=optional'
63
+ : 'npm install agnostic-ai --force --include=optional'
64
+ const other = isGlobal ? 'drop -g for a project install' : 'add -g for a global install'
65
+ throw new Error(
66
+ `${packageName(platform)} is not installed, so there is no binary to run. npm installs ` +
67
+ 'it as an optional dependency; a lockfile copied from another platform or an install ' +
68
+ `run with --omit=optional skips it. This copy is ${scope}, so reinstall with ` +
69
+ `\`${fix}\` (${other}), or set AGNOSTIC_AI_BINARY to a binary you already have. ` +
70
+ `Other routes: ${DOCS}`
71
+ )
72
+ }
73
+ }
74
+
75
+ function main() {
8
76
  let binary
9
77
  try {
10
- // Normally already on disk from postinstall; this covers an install that
11
- // ran with --ignore-scripts or without network.
12
- binary = await ensureBinary({ log: (m) => console.error(`agnostic-ai: ${m}`) })
78
+ binary = resolveBinary()
13
79
  } catch (err) {
14
80
  console.error(`agnostic-ai: ${err.message}`)
15
81
  process.exit(1)
@@ -20,8 +86,12 @@ async function main() {
20
86
  console.error(`agnostic-ai: ${result.error.message}`)
21
87
  process.exit(1)
22
88
  }
23
- // A signalled child reports null status; 128+signal is the shell convention.
24
- process.exit(result.status === null ? 128 : result.status)
89
+ // A signalled child reports a null status; 128+signal is the shell
90
+ // convention, so a caller can tell a SIGINT (130) from a SIGKILL (137).
91
+ if (result.status === null) {
92
+ process.exit(128 + (os.constants.signals[result.signal] || 0))
93
+ }
94
+ process.exit(result.status)
25
95
  }
26
96
 
27
97
  main()
@@ -0,0 +1,64 @@
1
+ 'use strict'
2
+
3
+ // The one table the whole npm distribution reads: the bin shim uses it to find
4
+ // the binary at runtime, scripts/build-platform-packages.js uses it to emit the
5
+ // packages at release time, and the parent package.json pins the same six names
6
+ // in optionalDependencies.
7
+ //
8
+ // Keeping it in one file is the point. npm skips an optional dependency whose
9
+ // `os` or `cpu` does not match the machine, and it skips it silently, so a pair
10
+ // that disagrees with what the shim asks for installs nothing and leaves the CLI
11
+ // missing with no error anywhere. Two copies of this table is how that happens.
12
+
13
+ // Published under the project's npm organization. The dedicated scope keeps
14
+ // ownership separate from a maintainer account, and makes the project name in
15
+ // each package redundant: `@agnostic-ai/win32-x64` already identifies both.
16
+ const SCOPE = '@agnostic-ai'
17
+
18
+ const BINARY = 'agnostic-ai'
19
+
20
+ // `os` and `cpu` are npm's own spellings, matched against process.platform and
21
+ // process.arch. `goos` and `goarch` are Go's, and name the release archive the
22
+ // binary is extracted from. The two vocabularies differ on win32/windows and
23
+ // x64/amd64, which is the mapping this table exists to hold.
24
+ const PLATFORMS = [
25
+ { os: 'darwin', cpu: 'arm64', goos: 'darwin', goarch: 'arm64' },
26
+ { os: 'darwin', cpu: 'x64', goos: 'darwin', goarch: 'amd64' },
27
+ { os: 'linux', cpu: 'arm64', goos: 'linux', goarch: 'arm64' },
28
+ { os: 'linux', cpu: 'x64', goos: 'linux', goarch: 'amd64' },
29
+ { os: 'win32', cpu: 'arm64', goos: 'windows', goarch: 'arm64' },
30
+ { os: 'win32', cpu: 'x64', goos: 'windows', goarch: 'amd64' },
31
+ ]
32
+
33
+ function packageName(platform) {
34
+ return `${SCOPE}/${platform.os}-${platform.cpu}`
35
+ }
36
+
37
+ function binaryName(platform) {
38
+ return platform.os === 'win32' ? `${BINARY}.exe` : BINARY
39
+ }
40
+
41
+ // What the shim hands require.resolve(). A subpath of the platform package, so
42
+ // Node walks the same node_modules lookup npm just populated.
43
+ function entryPoint(platform) {
44
+ return `${packageName(platform)}/${binaryName(platform)}`
45
+ }
46
+
47
+ function platformFor(nodeOs, nodeCpu) {
48
+ return PLATFORMS.find((p) => p.os === nodeOs && p.cpu === nodeCpu)
49
+ }
50
+
51
+ // Exact pins, never a range. A platform package carries a binary built from one
52
+ // commit, so a caret would let npm pair a new CLI with an old binary.
53
+ function optionalDependencies(version) {
54
+ return Object.fromEntries(PLATFORMS.map((p) => [packageName(p), version]))
55
+ }
56
+
57
+ module.exports = {
58
+ PLATFORMS,
59
+ binaryName,
60
+ entryPoint,
61
+ optionalDependencies,
62
+ packageName,
63
+ platformFor,
64
+ }
package/package.json CHANGED
@@ -1,19 +1,30 @@
1
1
  {
2
2
  "name": "agnostic-ai",
3
- "version": "0.62.0",
3
+ "version": "0.64.1",
4
4
  "description": "One spec, every AI CLI. Sync agents, skills, rules, hooks, and MCP servers to Claude Code, Codex, Gemini, Cursor, Copilot, and 20 more.",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "cli",
8
+ "agents",
9
+ "skills",
10
+ "rules",
11
+ "hooks",
12
+ "mcp",
13
+ "prompts",
14
+ "agents-md",
8
15
  "claude",
16
+ "claude-code",
9
17
  "codex",
10
18
  "cursor",
11
19
  "copilot",
12
- "agents",
13
- "skills",
14
- "mcp"
20
+ "gemini",
21
+ "windsurf",
22
+ "cline",
23
+ "opencode",
24
+ "ai-tools",
25
+ "developer-tools"
15
26
  ],
16
- "homepage": "https://github.com/Chemaclass/agnostic-ai#readme",
27
+ "homepage": "https://agnostic-ai.org",
17
28
  "bugs": {
18
29
  "url": "https://github.com/Chemaclass/agnostic-ai/issues"
19
30
  },
@@ -28,25 +39,22 @@
28
39
  "agnostic-ai": "bin/agnostic-ai.js"
29
40
  },
30
41
  "files": [
31
- "bin/",
32
- "lib/",
33
- "scripts/",
42
+ "bin/agnostic-ai.js",
43
+ "lib/platforms.js",
34
44
  "README.md"
35
45
  ],
36
46
  "scripts": {
37
- "postinstall": "node scripts/postinstall.js",
38
- "test": "node lib/download_test.js"
47
+ "test": "node lib/platforms_test.js && node scripts/build-platform-packages_test.js"
39
48
  },
40
49
  "engines": {
41
50
  "node": ">=18"
42
51
  },
43
- "os": [
44
- "darwin",
45
- "linux",
46
- "win32"
47
- ],
48
- "cpu": [
49
- "x64",
50
- "arm64"
51
- ]
52
+ "optionalDependencies": {
53
+ "@agnostic-ai/darwin-arm64": "0.64.1",
54
+ "@agnostic-ai/darwin-x64": "0.64.1",
55
+ "@agnostic-ai/linux-arm64": "0.64.1",
56
+ "@agnostic-ai/linux-x64": "0.64.1",
57
+ "@agnostic-ai/win32-arm64": "0.64.1",
58
+ "@agnostic-ai/win32-x64": "0.64.1"
59
+ }
52
60
  }
package/lib/download.js DELETED
@@ -1,122 +0,0 @@
1
- 'use strict'
2
-
3
- // Fetches the agnostic-ai release binary for the current platform. Shared by
4
- // the postinstall hook and the bin shim, so a failed install still recovers on
5
- // first run instead of leaving a broken command.
6
-
7
- const { execFileSync } = require('node:child_process')
8
- const crypto = require('node:crypto')
9
- const fs = require('node:fs')
10
- const https = require('node:https')
11
- const os = require('node:os')
12
- const path = require('node:path')
13
-
14
- const REPO = 'Chemaclass/agnostic-ai'
15
- const BINARY = process.platform === 'win32' ? 'agnostic-ai.exe' : 'agnostic-ai'
16
-
17
- const PLATFORMS = { darwin: 'darwin', linux: 'linux', win32: 'windows' }
18
- const ARCHS = { x64: 'amd64', arm64: 'arm64' }
19
-
20
- function target() {
21
- const goos = PLATFORMS[process.platform]
22
- const goarch = ARCHS[process.arch]
23
- if (!goos || !goarch) {
24
- throw new Error(
25
- `agnostic-ai has no prebuilt binary for ${process.platform}/${process.arch}. ` +
26
- 'Build from source: go install github.com/chemaclass/agnostic-ai/cmd/agnostic-ai@latest'
27
- )
28
- }
29
- return { goos, goarch, ext: goos === 'windows' ? 'zip' : 'tar.gz' }
30
- }
31
-
32
- function assetName() {
33
- const { goos, goarch, ext } = target()
34
- return `agnostic-ai_${goos}_${goarch}.${ext}`
35
- }
36
-
37
- function binaryPath() {
38
- return path.join(__dirname, '..', 'bin', BINARY)
39
- }
40
-
41
- function get(url) {
42
- return new Promise((resolve, reject) => {
43
- https
44
- .get(url, { headers: { 'user-agent': 'agnostic-ai-npm' } }, (res) => {
45
- // GitHub redirects release assets to a signed object-store URL.
46
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
47
- res.resume()
48
- resolve(get(res.headers.location))
49
- return
50
- }
51
- if (res.statusCode !== 200) {
52
- res.resume()
53
- reject(new Error(`GET ${url} failed with HTTP ${res.statusCode}`))
54
- return
55
- }
56
- const chunks = []
57
- res.on('data', (c) => chunks.push(c))
58
- res.on('end', () => resolve(Buffer.concat(chunks)))
59
- res.on('error', reject)
60
- })
61
- .on('error', reject)
62
- })
63
- }
64
-
65
- // The published package carries the release version; a checkout carries the
66
- // 0.0.0-dev placeholder, which has no matching release, so fall back to latest.
67
- async function resolveVersion() {
68
- if (process.env.AGNOSTIC_AI_VERSION) return process.env.AGNOSTIC_AI_VERSION
69
-
70
- const { version } = require('../package.json')
71
- if (version && !version.startsWith('0.0.0')) return `v${version}`
72
-
73
- const body = await get(`https://api.github.com/repos/${REPO}/releases/latest`)
74
- const tag = JSON.parse(body.toString('utf8')).tag_name
75
- if (!tag) throw new Error('could not resolve the latest agnostic-ai release')
76
- return tag
77
- }
78
-
79
- function downloadUrl(version, asset) {
80
- return `https://github.com/${REPO}/releases/download/${version}/${asset}`
81
- }
82
-
83
- async function verifyChecksum(archive, asset, version) {
84
- const sums = (await get(downloadUrl(version, 'checksums.txt'))).toString('utf8')
85
- const line = sums.split('\n').find((l) => l.trim().endsWith(asset))
86
- if (!line) throw new Error(`${asset} missing from checksums.txt`)
87
-
88
- const expected = line.trim().split(/\s+/)[0]
89
- const actual = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex')
90
- if (actual !== expected) throw new Error(`checksum mismatch for ${asset}`)
91
- }
92
-
93
- async function ensureBinary({ log = () => {} } = {}) {
94
- const dest = binaryPath()
95
- if (fs.existsSync(dest)) return dest
96
-
97
- const version = await resolveVersion()
98
- const asset = assetName()
99
- log(`downloading agnostic-ai ${version} (${asset})`)
100
-
101
- const work = fs.mkdtempSync(path.join(os.tmpdir(), 'agnostic-ai-'))
102
- try {
103
- const archive = path.join(work, asset)
104
- fs.writeFileSync(archive, await get(downloadUrl(version, asset)))
105
- await verifyChecksum(archive, asset, version)
106
-
107
- // bsdtar reads both tar.gz and zip, and ships with macOS and Windows 10
108
- // 1803+; Linux only ever gets the tar.gz here, so GNU tar is fine too.
109
- execFileSync('tar', ['-xf', archive, '-C', work, BINARY], { stdio: 'ignore' })
110
-
111
- fs.mkdirSync(path.dirname(dest), { recursive: true })
112
- fs.copyFileSync(path.join(work, BINARY), dest)
113
- fs.chmodSync(dest, 0o755)
114
- } finally {
115
- fs.rmSync(work, { recursive: true, force: true })
116
- }
117
-
118
- log(`installed ${dest}`)
119
- return dest
120
- }
121
-
122
- module.exports = { assetName, binaryPath, downloadUrl, ensureBinary, resolveVersion, target }
@@ -1,62 +0,0 @@
1
- 'use strict'
2
-
3
- // Run: node npm/lib/download_test.js (or npm test inside npm/)
4
- //
5
- // Covers the pure mapping logic. The download path itself is exercised
6
- // end to end by .github/workflows/install.yml against a real release.
7
-
8
- const assert = require('node:assert')
9
- const path = require('node:path')
10
- const { assetName, binaryPath, downloadUrl, resolveVersion, target } = require('./download')
11
-
12
- const tests = {
13
- 'asset name matches the release archive for this platform'() {
14
- const { goos, goarch } = target()
15
- const expected = goos === 'windows'
16
- ? `agnostic-ai_${goos}_${goarch}.zip`
17
- : `agnostic-ai_${goos}_${goarch}.tar.gz`
18
- assert.strictEqual(assetName(), expected)
19
- },
20
-
21
- 'windows gets a zip, every other platform a tar.gz'() {
22
- assert.strictEqual(target().ext, process.platform === 'win32' ? 'zip' : 'tar.gz')
23
- },
24
-
25
- 'download url points at the tagged release asset'() {
26
- assert.strictEqual(
27
- downloadUrl('v0.45.0', 'agnostic-ai_linux_amd64.tar.gz'),
28
- 'https://github.com/Chemaclass/agnostic-ai/releases/download/v0.45.0/agnostic-ai_linux_amd64.tar.gz'
29
- )
30
- },
31
-
32
- 'binary path lands in the package bin dir'() {
33
- assert.strictEqual(path.dirname(binaryPath()), path.join(__dirname, '..', 'bin'))
34
- assert.match(path.basename(binaryPath()), /^agnostic-ai(\.exe)?$/)
35
- },
36
-
37
- async 'env override wins over the package version'() {
38
- process.env.AGNOSTIC_AI_VERSION = 'v1.2.3'
39
- try {
40
- assert.strictEqual(await resolveVersion(), 'v1.2.3')
41
- } finally {
42
- delete process.env.AGNOSTIC_AI_VERSION
43
- }
44
- },
45
- }
46
-
47
- async function run() {
48
- let failed = 0
49
- for (const [name, fn] of Object.entries(tests)) {
50
- try {
51
- await fn()
52
- console.log(`ok ${name}`)
53
- } catch (err) {
54
- failed++
55
- console.error(`FAIL ${name}\n ${err.message}`)
56
- }
57
- }
58
- console.log(`\n${Object.keys(tests).length - failed} passed, ${failed} failed`)
59
- process.exit(failed === 0 ? 0 : 1)
60
- }
61
-
62
- run()
@@ -1,12 +0,0 @@
1
- 'use strict'
2
-
3
- // Downloads the binary at install time. A failure here is a warning, never an
4
- // install error: the bin shim retries on first run, so an offline install or a
5
- // blocked proxy does not break `npm install` for the whole project.
6
-
7
- const { ensureBinary } = require('../lib/download')
8
-
9
- ensureBinary({ log: (m) => console.log(`agnostic-ai: ${m}`) }).catch((err) => {
10
- console.warn(`agnostic-ai: ${err.message}`)
11
- console.warn('agnostic-ai: the binary will be fetched on first run instead')
12
- })