@rbbtsn0w/adg 0.3.0-beta.1 → 0.3.0-beta.3
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/dist/bin/adg.js +18 -9
- package/dist/src/adapters/anthropic.js +8 -16
- package/dist/src/adapters/antigravity.js +18 -0
- package/dist/src/adapters/{openai.js → codex.js} +4 -14
- package/dist/src/adapters/index.js +15 -6
- package/dist/src/adapters/reverse.js +25 -2
- package/dist/src/agents/antigravity.js +185 -0
- package/dist/src/agents/claude.js +1 -4
- package/dist/src/agents/index.js +2 -0
- package/dist/src/commands/install.js +4 -6
- package/dist/src/commands/update.js +6 -3
- package/dist/src/fsutil.js +4 -0
- package/dist/src/package.js +1 -1
- package/dist/src/semver.js +76 -0
- package/dist/src/skills.js +31 -0
- package/dist/src/sources.js +2 -2
- package/dist/src/update-check.js +56 -10
- package/package.json +1 -1
package/dist/bin/adg.js
CHANGED
|
@@ -49,7 +49,7 @@ const FLAGS = {
|
|
|
49
49
|
dir: { type: "string", short: "d", hint: "<dir>", help: "install into an explicit directory" },
|
|
50
50
|
global: { type: "boolean", short: "g", help: "use ~/.agents/plugins (across all projects)" },
|
|
51
51
|
project: { type: "boolean", help: "use <repo>/.agents/plugins (default)" },
|
|
52
|
-
target: { type: "string", short: "t", hint: "claude|codex|all", help: "runtime(s) to adapt for" },
|
|
52
|
+
target: { type: "string", short: "t", hint: "claude|codex|antigravity|all", help: "runtime(s) to adapt for" },
|
|
53
53
|
all: { type: "boolean", short: "a", help: "select all available plugins" },
|
|
54
54
|
plugin: { type: "string", short: "p", multiple: true, hint: "<name>", help: "select a specific plugin (repeatable)" },
|
|
55
55
|
"no-deps": { type: "boolean", short: "n", help: "don't install dependencies" },
|
|
@@ -137,7 +137,7 @@ const PLUGIN_COMMANDS = {
|
|
|
137
137
|
},
|
|
138
138
|
link: {
|
|
139
139
|
summary: "link installed plugins into a runtime",
|
|
140
|
-
synopsis: "adg plugins link --target claude|codex",
|
|
140
|
+
synopsis: "adg plugins link --target claude|codex|antigravity",
|
|
141
141
|
flags: ["target", ...SCOPE],
|
|
142
142
|
},
|
|
143
143
|
migrate: {
|
|
@@ -253,12 +253,20 @@ function reportAgents(agents, verb) {
|
|
|
253
253
|
console.log(ui.warn(`note: \`${r.agent}\` CLI not found — run \`adg plugins link --target ${r.agent}\` after installing it.`));
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
/** Friendly `--target` aliases mapped onto canonical adapter target ids. */
|
|
257
|
+
const TARGET_ALIASES = {
|
|
258
|
+
anthropic: "claude",
|
|
259
|
+
openai: "codex",
|
|
260
|
+
agy: "antigravity",
|
|
261
|
+
gemini: "antigravity",
|
|
262
|
+
};
|
|
256
263
|
function resolveTargets(target) {
|
|
257
264
|
if (!target || target === "all")
|
|
258
265
|
return [...ADAPTER_TARGETS];
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
266
|
+
const t = TARGET_ALIASES[target] ?? target;
|
|
267
|
+
if (ADAPTER_TARGETS.includes(t))
|
|
268
|
+
return [t];
|
|
269
|
+
fail(`invalid --target "${target}" (expected ${[...ADAPTER_TARGETS, "all"].join("|")})`);
|
|
262
270
|
}
|
|
263
271
|
/** Parse a `--only skills,commands` list into validated component types. */
|
|
264
272
|
function resolveComponents(only) {
|
|
@@ -461,9 +469,10 @@ async function runPlugins(rawVerb, rest) {
|
|
|
461
469
|
}
|
|
462
470
|
case "link": {
|
|
463
471
|
const { values } = parseVerb(verb, cmd.flags, rest);
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
472
|
+
if (values.target === undefined || values.target === "all") {
|
|
473
|
+
fail(`plugins link requires a single --target (${ADAPTER_TARGETS.join("|")})`);
|
|
474
|
+
}
|
|
475
|
+
const target = resolveTargets(values.target)[0]; // validates + maps aliases (agy → antigravity); always non-empty
|
|
467
476
|
const res = linkPlugins({ pluginsDir: resolveScopeDir(values), target, global: Boolean(values.global) });
|
|
468
477
|
for (const a of res.actions) {
|
|
469
478
|
console.log(`${ui.ok("linked")} ${ui.name(a.name)} ${ui.meta(`[${res.target}]`)}${a.linkedTo ? ui.meta(` -> ${a.linkedTo}`) : ""}`);
|
|
@@ -584,7 +593,7 @@ Commands:
|
|
|
584
593
|
adg plugins marketplace list [--verbose] [--global | --project | --dir <dir>]
|
|
585
594
|
Group installed plugins by source. --verbose expands each plugin to its
|
|
586
595
|
components (skills, agents, commands, …).
|
|
587
|
-
adg plugins marketplace upgrade [<source>] [--all] [--target claude|codex|all] [--global | --project | --dir <dir>]
|
|
596
|
+
adg plugins marketplace upgrade [<source>] [--all] [--target claude|codex|antigravity|all] [--global | --project | --dir <dir>]
|
|
588
597
|
Re-fetch a source and update its installed plugins (--all also installs
|
|
589
598
|
anything new it now offers). No <source> upgrades every remote source.
|
|
590
599
|
adg plugins marketplace remove <source> [--force] [--global | --project | --dir <dir>]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveProjectedSkills } from "../skills.js";
|
|
3
3
|
import { isExposed } from "../components.js";
|
|
4
4
|
/**
|
|
5
5
|
* Generate a Claude (.claude-plugin/plugin.json) manifest from an ADG manifest.
|
|
@@ -31,24 +31,16 @@ export function toAnthropicManifest(pluginDir, manifest, selection) {
|
|
|
31
31
|
out.hooks = manifest.hooks;
|
|
32
32
|
if (manifest.mcp && isExposed(selection, "mcp"))
|
|
33
33
|
out.mcp = manifest.mcp;
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Claude's array form is already `./skills/<id>` paths, so a strict array is
|
|
35
|
+
// passed through verbatim; an explicit id list (selection or strict:false) is
|
|
36
|
+
// mapped to paths and marks the manifest non-strict.
|
|
37
|
+
const projected = resolveProjectedSkills(pluginDir, manifest, selection, { passthroughArray: true });
|
|
38
|
+
if (projected.explicit) {
|
|
36
39
|
out.strict = false;
|
|
37
|
-
|
|
38
|
-
? selection.skills ?? resolveSkills(pluginDir, manifest)
|
|
39
|
-
: [];
|
|
40
|
-
out.skills = names.map((name) => `./skills/${name}`);
|
|
40
|
+
out.skills = projected.skills.map((name) => `./skills/${name}`);
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
|
-
|
|
44
|
-
if (strict && manifest.skills !== undefined) {
|
|
45
|
-
out.skills = manifest.skills;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
// skill-bundle form: explicit list, strict:false
|
|
49
|
-
out.strict = false;
|
|
50
|
-
out.skills = resolveSkills(pluginDir, manifest).map((name) => `./skills/${name}`);
|
|
51
|
-
}
|
|
43
|
+
out.skills = projected.skills;
|
|
52
44
|
}
|
|
53
45
|
return { defaultPath: join(".claude-plugin", "plugin.json"), manifest: out };
|
|
54
46
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
/** Projection subdirectory that holds the self-contained agy plugin root. */
|
|
3
|
+
export const ANTIGRAVITY_PROJECTION_DIR = ".antigravity-plugin";
|
|
4
|
+
/**
|
|
5
|
+
* Generate an Antigravity (`agy`) plugin.json from an ADG manifest.
|
|
6
|
+
*
|
|
7
|
+
* Antigravity's manifest is minimal: it reads only `name` from a `plugin.json`
|
|
8
|
+
* and discovers components by convention (sibling `skills/`, `agents/`,
|
|
9
|
+
* `commands/`, `hooks/` directories plus a `mcp_config.json`) — all resolved
|
|
10
|
+
* relative to the directory handed to `agy plugin install`, with no manifest
|
|
11
|
+
* path indirection. We therefore project a self-contained agy plugin root under
|
|
12
|
+
* `.antigravity-plugin/`: this pure transform emits its `plugin.json`, while the
|
|
13
|
+
* agent materializes the rest (mcp_config.json + symlinked component dirs), so a
|
|
14
|
+
* partial-install `selection` is not expressible for this target.
|
|
15
|
+
*/
|
|
16
|
+
export function toAntigravityManifest(_pluginDir, manifest, _selection) {
|
|
17
|
+
return { defaultPath: join(ANTIGRAVITY_PROJECTION_DIR, "plugin.json"), manifest: { name: manifest.name } };
|
|
18
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { isExposed } from "../components.js";
|
|
2
|
+
import { resolveProjectedSkills } from "../skills.js";
|
|
4
3
|
/**
|
|
5
4
|
* Generate a Codex (.codex-plugin/plugin.json) manifest from an ADG manifest.
|
|
6
5
|
*
|
|
@@ -14,18 +13,9 @@ import { isExposed } from "../components.js";
|
|
|
14
13
|
* rather than passed through. Codex only consumes skills.
|
|
15
14
|
*/
|
|
16
15
|
export function toCodexManifest(pluginDir, manifest, selection) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
? selection.skills ?? resolveSkills(pluginDir, manifest)
|
|
21
|
-
: [];
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const strict = manifest.strict !== false;
|
|
25
|
-
skills = strict && typeof manifest.skills === "string"
|
|
26
|
-
? manifest.skills
|
|
27
|
-
: resolveSkills(pluginDir, manifest);
|
|
28
|
-
}
|
|
16
|
+
// Codex's array form is bare ids, so a strict skills *array* is resolved to
|
|
17
|
+
// ids rather than passed through; only a declared root string passes through.
|
|
18
|
+
const { skills } = resolveProjectedSkills(pluginDir, manifest, selection, { passthroughArray: false });
|
|
29
19
|
const out = {
|
|
30
20
|
name: manifest.name,
|
|
31
21
|
version: manifest.version,
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { toAnthropicManifest } from "./anthropic.js";
|
|
2
|
-
import { toCodexManifest } from "./
|
|
2
|
+
import { toCodexManifest } from "./codex.js";
|
|
3
|
+
import { toAntigravityManifest } from "./antigravity.js";
|
|
4
|
+
// `anthropic` is kept as a synonym because Claude's plugin.json *is* the
|
|
5
|
+
// "anthropic" manifest shape. There is deliberately no `openai` key: the runtime
|
|
6
|
+
// is Codex, and an `openai` alias would imply OpenAI support that does not exist.
|
|
3
7
|
export const ADAPTERS = {
|
|
4
8
|
claude: toAnthropicManifest,
|
|
5
9
|
anthropic: toAnthropicManifest,
|
|
6
10
|
codex: toCodexManifest,
|
|
7
|
-
|
|
11
|
+
antigravity: toAntigravityManifest,
|
|
12
|
+
agy: toAntigravityManifest,
|
|
13
|
+
gemini: toAntigravityManifest,
|
|
8
14
|
};
|
|
9
|
-
export const ADAPTER_TARGETS = ["claude", "codex"];
|
|
15
|
+
export const ADAPTER_TARGETS = ["claude", "codex", "antigravity"];
|
|
10
16
|
/**
|
|
11
17
|
* Component categories each adapter target can actually express, mirroring what
|
|
12
18
|
* the adapters emit: the Claude manifest carries skills/agents/commands/hooks/mcp
|
|
13
19
|
* (`toAnthropicManifest`), while Codex only consumes skills (`toCodexManifest`).
|
|
14
|
-
* `
|
|
15
|
-
* agents
|
|
20
|
+
* Antigravity (`agy`) discovers the same superset as Claude via convention
|
|
21
|
+
* (skills/agents/commands/hooks dirs + mcp_config.json). `apps` is emitted by
|
|
22
|
+
* none, so it maps to no target. Used to derive which agents a plugin is
|
|
23
|
+
* adaptable to from its exposed component types.
|
|
16
24
|
*/
|
|
17
25
|
export const ADAPTER_COMPONENTS = {
|
|
18
26
|
claude: ["skills", "agents", "commands", "hooks", "mcp"],
|
|
19
27
|
codex: ["skills"],
|
|
28
|
+
antigravity: ["skills", "agents", "commands", "hooks", "mcp"],
|
|
20
29
|
};
|
|
21
|
-
export { toAnthropicManifest, toCodexManifest };
|
|
30
|
+
export { toAnthropicManifest, toCodexManifest, toAntigravityManifest };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ADG_SCHEMA_VERSION } from "../types.js";
|
|
2
2
|
import { validateManifest } from "../manifest.js";
|
|
3
|
+
import { toPosix } from "../fsutil.js";
|
|
3
4
|
/**
|
|
4
5
|
* Reverse-adapt a runtime-native manifest (.claude-plugin/plugin.json or
|
|
5
6
|
* .codex-plugin/plugin.json) into a canonical ADG manifest. This is the inverse
|
|
@@ -7,8 +8,14 @@ import { validateManifest } from "../manifest.js";
|
|
|
7
8
|
*
|
|
8
9
|
* Missing `version` falls back to 0.0.0 (callers may override with a git SHA);
|
|
9
10
|
* skills normalize to the manifest's array/string or the default ./skills/.
|
|
11
|
+
*
|
|
12
|
+
* `kind` disambiguates the two native skills-array conventions: Claude arrays are
|
|
13
|
+
* already `./skills/<id>` paths, while Codex arrays are bare ids. Both canonicalize
|
|
14
|
+
* to ADG's path-array contract so a later cross-adapt (e.g. codex → ADG → claude)
|
|
15
|
+
* emits valid `./skills/<id>` entries instead of leaking bare ids into a Claude
|
|
16
|
+
* manifest.
|
|
10
17
|
*/
|
|
11
|
-
export function fromNativeManifest(raw,
|
|
18
|
+
export function fromNativeManifest(raw, kind) {
|
|
12
19
|
if (typeof raw !== "object" || raw === null) {
|
|
13
20
|
throw new Error("native manifest must be a JSON object");
|
|
14
21
|
}
|
|
@@ -35,9 +42,16 @@ export function fromNativeManifest(raw, _kind) {
|
|
|
35
42
|
else if (typeof n.author === "string") {
|
|
36
43
|
manifest.author = { name: n.author };
|
|
37
44
|
}
|
|
38
|
-
if (typeof n.skills === "string"
|
|
45
|
+
if (typeof n.skills === "string") {
|
|
39
46
|
manifest.skills = n.skills;
|
|
40
47
|
}
|
|
48
|
+
else if (isStringArray(n.skills)) {
|
|
49
|
+
// Codex arrays are bare ids; map them to ADG's `./skills/<id>` path form.
|
|
50
|
+
// Claude arrays are already paths, but a Windows-authored manifest may use
|
|
51
|
+
// backslashes, so normalize separators to keep ADG manifests POSIX-pathed
|
|
52
|
+
// (downstream `resolveSkillEntries` splits on `/`).
|
|
53
|
+
manifest.skills = kind === "codex" ? n.skills.map(toSkillPath) : n.skills.map(toPosix);
|
|
54
|
+
}
|
|
41
55
|
else {
|
|
42
56
|
manifest.skills = "./skills/";
|
|
43
57
|
}
|
|
@@ -51,3 +65,12 @@ function copyIfString(src, dst, key) {
|
|
|
51
65
|
function isStringArray(v) {
|
|
52
66
|
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
53
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Canonicalize a skill reference (bare id or path) to ADG's `./skills/<id>` form.
|
|
70
|
+
* Accepts both `/` and `\` separators so a Windows-authored native manifest
|
|
71
|
+
* (e.g. `skills\\foo`) still yields a valid `./skills/foo` entry.
|
|
72
|
+
*/
|
|
73
|
+
function toSkillPath(ref) {
|
|
74
|
+
const name = ref.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || ref;
|
|
75
|
+
return `./skills/${name}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, rmSync, statSync, symlinkSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { ensureDir, writeJson } from "../fsutil.js";
|
|
6
|
+
import { readManifest } from "../manifest.js";
|
|
7
|
+
import { resolveSkillEntries } from "../skills.js";
|
|
8
|
+
import { isExposed } from "../components.js";
|
|
9
|
+
import { installedPluginDir, lockPath } from "../paths.js";
|
|
10
|
+
import { readLock } from "../lock.js";
|
|
11
|
+
import { ANTIGRAVITY_PROJECTION_DIR } from "../adapters/antigravity.js";
|
|
12
|
+
/**
|
|
13
|
+
* Antigravity (`agy`) agent.
|
|
14
|
+
*
|
|
15
|
+
* Antigravity discovers a plugin by convention relative to the directory handed
|
|
16
|
+
* to `agy plugin install` — `plugin.json` plus sibling `skills/`, `agents/`,
|
|
17
|
+
* `commands/`, `hooks/` dirs and a `mcp_config.json`, with no manifest path
|
|
18
|
+
* indirection. We therefore project a self-contained agy plugin root under
|
|
19
|
+
* `<store>/.antigravity-plugin/`: generated `plugin.json` + `mcp_config.json`
|
|
20
|
+
* (the ADG `mcp/.mcp.json` shape is exactly agy's, so it passes through) and
|
|
21
|
+
* *symlinks* to the real component dirs one level up, so nothing is duplicated
|
|
22
|
+
* on disk. agy follows these symlinks; where the platform forbids them (e.g.
|
|
23
|
+
* Windows without privilege) we fall back to a copy. We then drive
|
|
24
|
+
* `agy plugin install/uninstall` (which owns `~/.gemini/antigravity-cli`).
|
|
25
|
+
*/
|
|
26
|
+
const ID = "antigravity";
|
|
27
|
+
/**
|
|
28
|
+
* Single-directory component fields: agy reads each as a sibling dir named by
|
|
29
|
+
* convention. `skills` is handled separately because it can be a path-list and
|
|
30
|
+
* supports per-skill subsetting.
|
|
31
|
+
*/
|
|
32
|
+
const DIR_FIELDS = ["agents", "commands", "hooks"];
|
|
33
|
+
function geminiHome(env) {
|
|
34
|
+
return env.GEMINI_HOME?.trim() || join(homedir(), ".gemini");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* agy's config/store home: `<GEMINI_HOME>/antigravity-cli` (defaulting to
|
|
38
|
+
* `~/.gemini/antigravity-cli`). Exported so the resolver itself is testable
|
|
39
|
+
* without depending on the host filesystem state.
|
|
40
|
+
*/
|
|
41
|
+
export function antigravityHome(env = process.env) {
|
|
42
|
+
return join(geminiHome(env), "antigravity-cli");
|
|
43
|
+
}
|
|
44
|
+
function available() {
|
|
45
|
+
// `--help` is rejected by `install` (it parses it as a target), so probe the
|
|
46
|
+
// plugin command group with its own `help` subcommand instead.
|
|
47
|
+
return spawnSync("agy", ["plugin", "help"], { stdio: "ignore" }).status === 0;
|
|
48
|
+
}
|
|
49
|
+
function run(args) {
|
|
50
|
+
const r = spawnSync("agy", args, { encoding: "utf8" });
|
|
51
|
+
const ok = r.status === 0;
|
|
52
|
+
// Surface the CLI's own diagnostics on failure instead of swallowing them.
|
|
53
|
+
if (!ok && r.stderr)
|
|
54
|
+
console.error(r.stderr.trim());
|
|
55
|
+
return { ok, out: `${r.stdout ?? ""}${r.stderr ?? ""}` };
|
|
56
|
+
}
|
|
57
|
+
/** Resolve a plugin's on-disk store directory and selection from the lock's provenance. */
|
|
58
|
+
function pluginStore(pluginsDir, name) {
|
|
59
|
+
const entry = readLock(lockPath(pluginsDir)).plugins[name];
|
|
60
|
+
if (!entry)
|
|
61
|
+
return undefined;
|
|
62
|
+
const dir = installedPluginDir(pluginsDir, name, entry.origin);
|
|
63
|
+
return existsSync(dir) ? { dir, selection: entry.selection } : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** First on-disk top segment of a declared component path (e.g. "./agents/" -> "agents"). */
|
|
66
|
+
function componentSegment(value) {
|
|
67
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
68
|
+
if (typeof first !== "string")
|
|
69
|
+
return undefined;
|
|
70
|
+
const seg = first.replace(/^\.?[/\\]/, "").split(/[/\\]/)[0];
|
|
71
|
+
return seg || undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Symlink `linkPath` at `absTarget` (target stored relative so the projection
|
|
75
|
+
* survives the whole plugin dir being moved), copying instead where symlinks are
|
|
76
|
+
* unavailable (e.g. Windows without privilege). Idempotent.
|
|
77
|
+
*/
|
|
78
|
+
function linkOrCopy(linkPath, absTarget) {
|
|
79
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
80
|
+
ensureDir(dirname(linkPath));
|
|
81
|
+
try {
|
|
82
|
+
symlinkSync(relative(dirname(linkPath), absTarget), linkPath, "dir");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
cpSync(absTarget, linkPath, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the agy-native projection under `<dir>/.antigravity-plugin/`: a
|
|
90
|
+
* `plugin.json` (name only), a `mcp_config.json` copied verbatim from the
|
|
91
|
+
* manifest's mcp file when present, and relative symlinks (copy fallback) to the
|
|
92
|
+
* declared component dirs named for agy's convention.
|
|
93
|
+
*
|
|
94
|
+
* An optional `selection` (the plugin's partial install) narrows what is
|
|
95
|
+
* projected: component categories outside it are dropped, and `skills` is pinned
|
|
96
|
+
* to the selected subset. `skills` is also projected per-skill into a real
|
|
97
|
+
* `skills/` dir, so a path-list spanning multiple roots is fully honored rather
|
|
98
|
+
* than collapsing to its first root. Idempotent; safe to re-run.
|
|
99
|
+
*/
|
|
100
|
+
export function writeAntigravityProjection(dir, selection) {
|
|
101
|
+
const manifest = readManifest(dir);
|
|
102
|
+
const stage = join(dir, ANTIGRAVITY_PROJECTION_DIR);
|
|
103
|
+
ensureDir(stage);
|
|
104
|
+
writeJson(join(stage, "plugin.json"), { name: manifest.name });
|
|
105
|
+
const mcpConfig = join(stage, "mcp_config.json");
|
|
106
|
+
rmSync(mcpConfig, { force: true });
|
|
107
|
+
if (manifest.mcp && isExposed(selection, "mcp")) {
|
|
108
|
+
const mcpFile = join(dir, manifest.mcp);
|
|
109
|
+
// The ADG mcp file shape is exactly agy's `mcp_config.json`, so copy it
|
|
110
|
+
// verbatim — preserving formatting and avoiding a parse/re-serialize round-trip.
|
|
111
|
+
if (existsSync(mcpFile))
|
|
112
|
+
cpSync(mcpFile, mcpConfig);
|
|
113
|
+
}
|
|
114
|
+
const skillsDir = join(stage, "skills");
|
|
115
|
+
rmSync(skillsDir, { recursive: true, force: true });
|
|
116
|
+
if (manifest.skills !== undefined && isExposed(selection, "skills")) {
|
|
117
|
+
const pick = selection?.skills;
|
|
118
|
+
for (const e of resolveSkillEntries(dir, manifest)) {
|
|
119
|
+
if (pick && !pick.includes(e.name))
|
|
120
|
+
continue;
|
|
121
|
+
if (!e.skillMd)
|
|
122
|
+
continue;
|
|
123
|
+
const srcSkillDir = dirname(e.skillMd);
|
|
124
|
+
if (existsSync(srcSkillDir) && statSync(srcSkillDir).isDirectory()) {
|
|
125
|
+
linkOrCopy(join(skillsDir, e.name), srcSkillDir);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const field of DIR_FIELDS) {
|
|
130
|
+
const link = join(stage, field);
|
|
131
|
+
rmSync(link, { recursive: true, force: true });
|
|
132
|
+
if (!isExposed(selection, field))
|
|
133
|
+
continue;
|
|
134
|
+
const seg = componentSegment(manifest[field]);
|
|
135
|
+
if (!seg)
|
|
136
|
+
continue;
|
|
137
|
+
const srcDir = join(dir, seg);
|
|
138
|
+
// agy reads the component by its convention name (`field`); point that at
|
|
139
|
+
// the real source dir so a non-conventional source name still resolves.
|
|
140
|
+
if (existsSync(srcDir))
|
|
141
|
+
linkOrCopy(link, srcDir);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export const antigravityAgent = {
|
|
145
|
+
id: ID,
|
|
146
|
+
displayName: "Antigravity",
|
|
147
|
+
adaptTarget: "antigravity",
|
|
148
|
+
detect: (env = process.env) => existsSync(antigravityHome(env)),
|
|
149
|
+
available,
|
|
150
|
+
activate(ctx) {
|
|
151
|
+
if (!available())
|
|
152
|
+
return { agent: ID, affected: [], skipped: true };
|
|
153
|
+
const affected = [];
|
|
154
|
+
for (const p of ctx.plugins) {
|
|
155
|
+
// Isolate each plugin: a malformed manifest or a filesystem error must not
|
|
156
|
+
// abort activation of the remaining (valid) plugins.
|
|
157
|
+
try {
|
|
158
|
+
const store = pluginStore(ctx.pluginsDir, p);
|
|
159
|
+
if (!store)
|
|
160
|
+
continue;
|
|
161
|
+
writeAntigravityProjection(store.dir, store.selection);
|
|
162
|
+
if (run(["plugin", "install", join(store.dir, ANTIGRAVITY_PROJECTION_DIR)]).ok)
|
|
163
|
+
affected.push(p);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error(`failed to enable "${p}" in Antigravity:`, err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { agent: ID, affected, skipped: false };
|
|
170
|
+
},
|
|
171
|
+
deactivate(ctx) {
|
|
172
|
+
if (!available())
|
|
173
|
+
return { agent: ID, affected: [], skipped: true };
|
|
174
|
+
const affected = [];
|
|
175
|
+
for (const p of ctx.plugins) {
|
|
176
|
+
if (run(["plugin", "uninstall", p]).ok)
|
|
177
|
+
affected.push(p);
|
|
178
|
+
}
|
|
179
|
+
return { agent: ID, affected, skipped: false };
|
|
180
|
+
},
|
|
181
|
+
// `agy plugin install` re-imports the source dir, so re-running it is the refresh.
|
|
182
|
+
refresh(ctx) {
|
|
183
|
+
return antigravityAgent.activate(ctx);
|
|
184
|
+
},
|
|
185
|
+
};
|
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, relative } from "node:path";
|
|
5
|
-
import { writeJson } from "../fsutil.js";
|
|
5
|
+
import { toPosix, writeJson } from "../fsutil.js";
|
|
6
6
|
import { readManifest } from "../manifest.js";
|
|
7
7
|
import { installedPluginDir, lockPath } from "../paths.js";
|
|
8
8
|
import { readLock } from "../lock.js";
|
|
@@ -15,9 +15,6 @@ import { readLock } from "../lock.js";
|
|
|
15
15
|
* versions) rather than hand-editing Claude's internal state.
|
|
16
16
|
*/
|
|
17
17
|
const MARKETPLACE = "adg";
|
|
18
|
-
function toPosix(p) {
|
|
19
|
-
return p.split("\\").join("/");
|
|
20
|
-
}
|
|
21
18
|
function claudeHome(env) {
|
|
22
19
|
return env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
23
20
|
}
|
package/dist/src/agents/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { registerAgent } from "./registry.js";
|
|
2
2
|
import { claudeAgent } from "./claude.js";
|
|
3
3
|
import { codexAgent } from "./codex.js";
|
|
4
|
+
import { antigravityAgent } from "./antigravity.js";
|
|
4
5
|
// Built-in agents register on import. Third-party agents can `registerAgent()`
|
|
5
6
|
// their own implementation (stage 2: discover from config without core edits).
|
|
6
7
|
registerAgent(claudeAgent);
|
|
7
8
|
registerAgent(codexAgent);
|
|
9
|
+
registerAgent(antigravityAgent);
|
|
8
10
|
export * from "./types.js";
|
|
9
11
|
export { registerAgent, getAgent, allAgents, detectedAgents, resolveAgents, agentsForComponents } from "./registry.js";
|
|
10
12
|
export { writeClaudeCatalog } from "./claude.js";
|
|
@@ -4,9 +4,9 @@ import { basename, join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { ADAPTER_TARGETS } from "../adapters/index.js";
|
|
5
5
|
import { fromNativeManifest } from "../adapters/reverse.js";
|
|
6
6
|
import { adaptPlugin } from "./adapt.js";
|
|
7
|
-
import { copyPluginDir, writeJson } from "../fsutil.js";
|
|
7
|
+
import { copyPluginDir, toPosix, writeJson } from "../fsutil.js";
|
|
8
8
|
import { folderHash } from "../hash.js";
|
|
9
|
-
import { packageFilter } from "../package.js";
|
|
9
|
+
import { packageFilter, PROJECTION_DIRS } from "../package.js";
|
|
10
10
|
import { lockPath, marketplacePath, marketplaceSourcePath, pluginDir } from "../paths.js";
|
|
11
11
|
import { readLock, upsertEntry, writeLock } from "../lock.js";
|
|
12
12
|
import { ADG_MANIFEST_PATH, readManifest } from "../manifest.js";
|
|
@@ -17,10 +17,8 @@ import { sameSource, COMPONENT_TYPES } from "../types.js";
|
|
|
17
17
|
import { pluginContents, presentComponents } from "../components.js";
|
|
18
18
|
import { skillDescriptionLoader } from "../skills.js";
|
|
19
19
|
import { resolveAgents } from "../agents/index.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return p.split("\\").join("/");
|
|
23
|
-
}
|
|
20
|
+
// Generated runtime projections never count toward a plugin's content hash.
|
|
21
|
+
const HASH_IGNORE = PROJECTION_DIRS;
|
|
24
22
|
/**
|
|
25
23
|
* Install a single local plugin directory into a plugins directory: copy the
|
|
26
24
|
* source, generate adapter manifests, compute the folder hash, and update both
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { folderHash } from "../hash.js";
|
|
3
|
-
import { packageFilter } from "../package.js";
|
|
3
|
+
import { packageFilter, PROJECTION_DIRS } from "../package.js";
|
|
4
4
|
import { lockPath, installedPluginDir } from "../paths.js";
|
|
5
5
|
import { readLock, writeLock } from "../lock.js";
|
|
6
6
|
import { readManifest } from "../manifest.js";
|
|
7
7
|
import { adaptPlugin } from "./adapt.js";
|
|
8
|
+
import { ADAPTER_TARGETS } from "../adapters/index.js";
|
|
8
9
|
import { resolveAgents } from "../agents/index.js";
|
|
9
10
|
/**
|
|
10
11
|
* Re-scan installed plugins in a plugins directory, refreshing each lock
|
|
@@ -29,14 +30,16 @@ export function updateLock(pluginsDir, now = new Date().toISOString(), opts = {}
|
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
31
32
|
const manifest = readManifest(dir);
|
|
32
|
-
const hash = folderHash(dir,
|
|
33
|
+
const hash = folderHash(dir, PROJECTION_DIRS, packageFilter(manifest, { includeProjections: false }));
|
|
33
34
|
const changed = hash !== entry.folderHash || manifest.version !== entry.version;
|
|
34
35
|
if (changed) {
|
|
35
36
|
entry.folderHash = hash;
|
|
36
37
|
entry.version = manifest.version;
|
|
37
38
|
entry.updatedAt = now;
|
|
38
39
|
// Regenerate runtime manifests from the updated source, honoring selection.
|
|
39
|
-
|
|
40
|
+
// Covers every registered adapter target (claude/codex/antigravity) so a
|
|
41
|
+
// projection can't go stale after an update.
|
|
42
|
+
adaptPlugin(dir, [...ADAPTER_TARGETS], entry.selection);
|
|
40
43
|
changedNames.push(name);
|
|
41
44
|
}
|
|
42
45
|
results.push({ name, changed, version: manifest.version, folderHash: hash });
|
package/dist/src/fsutil.js
CHANGED
|
@@ -3,6 +3,10 @@ import { dirname, relative } from "node:path";
|
|
|
3
3
|
export function ensureDir(dir) {
|
|
4
4
|
mkdirSync(dir, { recursive: true });
|
|
5
5
|
}
|
|
6
|
+
/** Normalize a path to forward slashes (stable across Windows and POSIX hosts). */
|
|
7
|
+
export function toPosix(p) {
|
|
8
|
+
return p.split("\\").join("/");
|
|
9
|
+
}
|
|
6
10
|
export function writeJson(file, value) {
|
|
7
11
|
ensureDir(dirname(file));
|
|
8
12
|
writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
|
package/dist/src/package.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
/** Top-level metadata files always shipped with a plugin, matched case-insensitively. */
|
|
12
12
|
const META_RE = /^(README|LICEN[CS]E|CHANGELOG|NOTICE)(\..+)?$/i;
|
|
13
13
|
/** Generated runtime projections — shipped, but excluded from the content hash. */
|
|
14
|
-
export const PROJECTION_DIRS = [".claude-plugin", ".codex-plugin"];
|
|
14
|
+
export const PROJECTION_DIRS = [".claude-plugin", ".codex-plugin", ".antigravity-plugin"];
|
|
15
15
|
/** Extract the first path segment of a manifest component value (e.g. "./skills/" -> "skills"). */
|
|
16
16
|
function topSegment(p) {
|
|
17
17
|
return p.replace(/^\.?[/\\]/, "").split(/[/\\]/)[0] ?? "";
|
package/dist/src/semver.js
CHANGED
|
@@ -19,6 +19,82 @@ export function compare(a, b) {
|
|
|
19
19
|
}
|
|
20
20
|
return 0;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract the dot-separated pre-release identifiers from a version string.
|
|
24
|
+
*
|
|
25
|
+
* Returns the segment after the first `-` (and before any `+` build metadata),
|
|
26
|
+
* with numeric-only identifiers coerced to `number`. A stable version (no
|
|
27
|
+
* pre-release) yields `[]`.
|
|
28
|
+
*/
|
|
29
|
+
export function parsePrerelease(v) {
|
|
30
|
+
// Drop build metadata first so a hyphen inside it (e.g. `1.2.3+build-1`) is
|
|
31
|
+
// not mistaken for the pre-release separator.
|
|
32
|
+
const withoutBuild = v.split("+")[0] ?? "";
|
|
33
|
+
const dashIndex = withoutBuild.indexOf("-");
|
|
34
|
+
if (dashIndex === -1)
|
|
35
|
+
return [];
|
|
36
|
+
const pre = withoutBuild.slice(dashIndex + 1);
|
|
37
|
+
if (pre === "")
|
|
38
|
+
return [];
|
|
39
|
+
return pre.split(".").map((id) => (/^\d+$/.test(id) ? Number(id) : id));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compare two pre-release identifier lists per SemVer §11.
|
|
43
|
+
*
|
|
44
|
+
* A stable version (empty list) outranks any pre-release. Otherwise identifiers
|
|
45
|
+
* are compared left to right: numeric identifiers rank below alphanumeric ones,
|
|
46
|
+
* numerics compare numerically, strings compare by ASCII, and when every shared
|
|
47
|
+
* field is equal the longer list wins.
|
|
48
|
+
*/
|
|
49
|
+
export function comparePrerelease(a, b) {
|
|
50
|
+
if (a.length === 0 && b.length === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
if (a.length === 0)
|
|
53
|
+
return 1; // stable > pre-release
|
|
54
|
+
if (b.length === 0)
|
|
55
|
+
return -1;
|
|
56
|
+
const len = Math.min(a.length, b.length);
|
|
57
|
+
for (let i = 0; i < len; i++) {
|
|
58
|
+
const x = a[i];
|
|
59
|
+
const y = b[i];
|
|
60
|
+
if (x === y)
|
|
61
|
+
continue;
|
|
62
|
+
const xNum = typeof x === "number";
|
|
63
|
+
const yNum = typeof y === "number";
|
|
64
|
+
if (xNum && yNum)
|
|
65
|
+
return x < y ? -1 : 1;
|
|
66
|
+
if (xNum !== yNum)
|
|
67
|
+
return xNum ? -1 : 1; // numeric < alphanumeric
|
|
68
|
+
return x < y ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
// All shared fields equal: the list with more fields has higher precedence.
|
|
71
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Full version comparison including pre-release precedence.
|
|
75
|
+
*
|
|
76
|
+
* Unlike {@link compare}, this honors the pre-release suffix so that, e.g.,
|
|
77
|
+
* `0.3.0-beta.2 < 0.3.0-beta.3 < 0.3.0`. Used by the update check; range
|
|
78
|
+
* matching ({@link satisfies}) deliberately ignores pre-release.
|
|
79
|
+
*/
|
|
80
|
+
export function compareVersions(a, b) {
|
|
81
|
+
const core = compare(parseVersion(a), parseVersion(b));
|
|
82
|
+
if (core !== 0)
|
|
83
|
+
return core;
|
|
84
|
+
return comparePrerelease(parsePrerelease(a), parsePrerelease(b));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Return the pre-release channel of a version — its first non-numeric
|
|
88
|
+
* pre-release identifier (e.g. `0.3.0-beta.2` -> `"beta"`), or `null` for a
|
|
89
|
+
* stable version. Used to pick the matching npm dist-tag.
|
|
90
|
+
*/
|
|
91
|
+
export function prereleaseChannel(v) {
|
|
92
|
+
for (const id of parsePrerelease(v)) {
|
|
93
|
+
if (typeof id === "string")
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
22
98
|
export function satisfies(version, range) {
|
|
23
99
|
const v = parseVersion(version);
|
|
24
100
|
const r = range.trim();
|
package/dist/src/skills.js
CHANGED
|
@@ -42,6 +42,37 @@ export function resolveSkillEntries(pluginDir, manifest) {
|
|
|
42
42
|
export function resolveSkills(pluginDir, manifest) {
|
|
43
43
|
return resolveSkillEntries(pluginDir, manifest).map((e) => e.name);
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Decide the skills a forward adapter should project from an ADG manifest, the
|
|
47
|
+
* one strict/selection decision both adapters share (previously copy-pasted, so
|
|
48
|
+
* it could drift — see the Codex/Claude strict regression that motivated it).
|
|
49
|
+
*
|
|
50
|
+
* In the default strict case the declared skills *root* string is passed through
|
|
51
|
+
* (the runtime discovers skills from the directory); an omitted `skills` is
|
|
52
|
+
* treated as the default `./skills/` root so the strict default stays
|
|
53
|
+
* directory-discovery rather than an explicit enumeration. A `selection` or
|
|
54
|
+
* `strict: false` always yields an explicit resolved id list. A declared `skills`
|
|
55
|
+
* *array* is runtime-dependent: `passthroughArray` keeps it verbatim (Claude,
|
|
56
|
+
* whose entries are already `./skills/<id>` paths), otherwise it resolves to ids
|
|
57
|
+
* (Codex, whose array form is bare ids).
|
|
58
|
+
*/
|
|
59
|
+
export function resolveProjectedSkills(pluginDir, manifest, selection, opts) {
|
|
60
|
+
if (selection) {
|
|
61
|
+
const skills = selection.components.includes("skills")
|
|
62
|
+
? selection.skills ?? resolveSkills(pluginDir, manifest)
|
|
63
|
+
: [];
|
|
64
|
+
return { skills, explicit: true };
|
|
65
|
+
}
|
|
66
|
+
const strict = manifest.strict !== false;
|
|
67
|
+
// An omitted `skills` defaults to the `./skills/` root, so a strict manifest
|
|
68
|
+
// without a declaration keeps directory discovery instead of being forced to
|
|
69
|
+
// an explicit `strict: false` enumeration.
|
|
70
|
+
const declared = manifest.skills ?? "./skills/";
|
|
71
|
+
const passthrough = strict && (typeof declared === "string" || (opts.passthroughArray && Array.isArray(declared)));
|
|
72
|
+
if (passthrough)
|
|
73
|
+
return { skills: declared, explicit: false };
|
|
74
|
+
return { skills: resolveSkills(pluginDir, manifest), explicit: true };
|
|
75
|
+
}
|
|
45
76
|
/** Read a SKILL.md's `description` from its YAML frontmatter (undefined if absent). */
|
|
46
77
|
export function readSkillDescription(skillMd) {
|
|
47
78
|
try {
|
package/dist/src/sources.js
CHANGED
|
@@ -105,11 +105,11 @@ function walkNative(current, out) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
if (existsSync(claude)) {
|
|
108
|
-
out.push({ dir: current, kind: "
|
|
108
|
+
out.push({ dir: current, kind: "claude", manifestFile: claude });
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
if (existsSync(codex)) {
|
|
112
|
-
out.push({ dir: current, kind: "
|
|
112
|
+
out.push({ dir: current, kind: "codex", manifestFile: codex });
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
package/dist/src/update-check.js
CHANGED
|
@@ -10,12 +10,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import https from "node:https";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import { compareVersions, prereleaseChannel } from "./semver.js";
|
|
14
14
|
const PACKAGE_NAME = "@rbbtsn0w/adg";
|
|
15
15
|
// URL-encode the slash in the scoped package name for the npm registry API.
|
|
16
|
-
|
|
16
|
+
// The abbreviated packument (vnd.npm.install-v1+json) is small and exposes
|
|
17
|
+
// `dist-tags`, which we need to follow the caller's release channel (e.g. the
|
|
18
|
+
// `beta` dist-tag for pre-release users, not just `latest`).
|
|
19
|
+
const REGISTRY_URL = `https://registry.npmjs.org/@rbbtsn0w%2Fadg`;
|
|
20
|
+
const REGISTRY_ACCEPT = "application/vnd.npm.install-v1+json";
|
|
17
21
|
const CACHE_FILENAME = "update-check.json";
|
|
18
22
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
23
|
+
// Cap the accumulated response body. The abbreviated packument is small, but a
|
|
24
|
+
// registry that ignores the abbreviated Accept header (or returns an unexpected
|
|
25
|
+
// payload) could stream a much larger body; abort rather than grow unbounded.
|
|
26
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MiB
|
|
19
27
|
/** Resolve the directory that holds the update-check cache file. */
|
|
20
28
|
export function updateCacheDir(env = process.env) {
|
|
21
29
|
const stateHome = env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
@@ -46,6 +54,29 @@ export function writeUpdateCache(cache, env = process.env) {
|
|
|
46
54
|
// Ignore write errors (read-only FS, permissions, etc.)
|
|
47
55
|
}
|
|
48
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Pick the newest version relevant to the caller's release channel from the
|
|
59
|
+
* registry's `dist-tags`.
|
|
60
|
+
*
|
|
61
|
+
* Always considers `latest` (stable). When `currentVersion` is a pre-release
|
|
62
|
+
* (e.g. `0.3.0-beta.2`) it also considers the matching channel tag (e.g.
|
|
63
|
+
* `beta`), so pre-release users are notified of newer pre-releases as well as a
|
|
64
|
+
* newer stable. Returns the max candidate by pre-release-aware comparison, or
|
|
65
|
+
* `undefined` when no usable tag is present.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveLatestForChannel(currentVersion, distTags) {
|
|
68
|
+
if (!distTags)
|
|
69
|
+
return undefined;
|
|
70
|
+
const candidates = [];
|
|
71
|
+
if (typeof distTags.latest === "string")
|
|
72
|
+
candidates.push(distTags.latest);
|
|
73
|
+
const channel = prereleaseChannel(currentVersion);
|
|
74
|
+
if (channel && typeof distTags[channel] === "string")
|
|
75
|
+
candidates.push(distTags[channel]);
|
|
76
|
+
if (candidates.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
return candidates.reduce((best, v) => (compareVersions(v, best) > 0 ? v : best));
|
|
79
|
+
}
|
|
49
80
|
/**
|
|
50
81
|
* Fire-and-forget background fetch of the latest version from the npm registry.
|
|
51
82
|
* The socket is unreffed so Node can exit naturally without waiting for the
|
|
@@ -53,14 +84,26 @@ export function writeUpdateCache(cache, env = process.env) {
|
|
|
53
84
|
*/
|
|
54
85
|
export function scheduleUpdateCacheRefresh(currentVersion, env = process.env) {
|
|
55
86
|
try {
|
|
56
|
-
const req = https.get(REGISTRY_URL, { headers: { "User-Agent": `adg/${currentVersion}`, Accept:
|
|
87
|
+
const req = https.get(REGISTRY_URL, { headers: { "User-Agent": `adg/${currentVersion}`, Accept: REGISTRY_ACCEPT } }, (res) => {
|
|
57
88
|
let body = "";
|
|
58
|
-
|
|
89
|
+
let byteCount = 0;
|
|
90
|
+
res.on("data", (chunk) => {
|
|
91
|
+
byteCount += Buffer.byteLength(chunk);
|
|
92
|
+
if (byteCount > MAX_RESPONSE_BYTES) {
|
|
93
|
+
// Oversized payload: stop reading and abort so we neither buffer
|
|
94
|
+
// unbounded memory nor parse a partial body.
|
|
95
|
+
body = "";
|
|
96
|
+
req.destroy();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
body += String(chunk);
|
|
100
|
+
});
|
|
59
101
|
res.on("end", () => {
|
|
60
102
|
try {
|
|
61
103
|
const data = JSON.parse(body);
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
const latestVersion = resolveLatestForChannel(currentVersion, data["dist-tags"]);
|
|
105
|
+
if (latestVersion !== undefined) {
|
|
106
|
+
writeUpdateCache({ latestVersion, checkedAt: new Date().toISOString() }, env);
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
catch {
|
|
@@ -103,9 +146,7 @@ export function checkForUpdate(currentVersion, env = process.env, refresh = sche
|
|
|
103
146
|
if (!cache)
|
|
104
147
|
return undefined;
|
|
105
148
|
try {
|
|
106
|
-
|
|
107
|
-
const latest = parseVersion(cache.latestVersion);
|
|
108
|
-
return compare(latest, current) > 0 ? cache.latestVersion : undefined;
|
|
149
|
+
return compareVersions(cache.latestVersion, currentVersion) > 0 ? cache.latestVersion : undefined;
|
|
109
150
|
}
|
|
110
151
|
catch {
|
|
111
152
|
return undefined;
|
|
@@ -113,6 +154,11 @@ export function checkForUpdate(currentVersion, env = process.env, refresh = sche
|
|
|
113
154
|
}
|
|
114
155
|
/** Format an update notice for display on stderr. */
|
|
115
156
|
export function formatUpdateNotice(currentVersion, latestVersion) {
|
|
157
|
+
// A pre-release suggestion lives on its channel dist-tag (e.g. `beta`), not
|
|
158
|
+
// `latest`; installing `@latest` would pull the stable release instead of the
|
|
159
|
+
// advertised version. Pin to the exact version so the right artifact installs.
|
|
160
|
+
const channel = prereleaseChannel(latestVersion);
|
|
161
|
+
const installTarget = channel ? latestVersion : "latest";
|
|
116
162
|
return (`\n Update available: ${currentVersion} → ${latestVersion}\n` +
|
|
117
|
-
` Run: npm install -g ${PACKAGE_NAME}
|
|
163
|
+
` Run: npm install -g ${PACKAGE_NAME}@${installTarget}\n`);
|
|
118
164
|
}
|