agy-plugin-porter 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/bin/agy-porter.mjs +91 -0
- package/lib/consent.mjs +122 -0
- package/lib/detector.mjs +92 -0
- package/lib/hook-compiler.mjs +208 -0
- package/lib/index.mjs +32 -0
- package/lib/manifest-sanitizer.mjs +187 -0
- package/lib/mod-compiler.mjs +210 -0
- package/lib/mod-lexer.mjs +143 -0
- package/lib/mod-scan.mjs +169 -0
- package/lib/mod-transform.mjs +84 -0
- package/lib/mode-compiler.mjs +195 -0
- package/lib/rules-compiler.mjs +205 -0
- package/lib/skill-compiler.mjs +135 -0
- package/lib/source.mjs +81 -0
- package/lib/static-auditor.mjs +179 -0
- package/lib/tool-map.mjs +46 -0
- package/lib/transpiler-engine.mjs +190 -0
- package/lib/yaml-lite.mjs +222 -0
- package/package.json +73 -4
- package/scripts/hook-shim.mjs +418 -0
- package/scripts/mod-runtime/claude-code.mjs +20 -0
- package/scripts/mod-runtime/elements.mjs +35 -0
- package/scripts/mod-runtime/engine.mjs +129 -0
- package/scripts/mod-runtime/hook.mjs +24 -0
- package/scripts/mod-runtime/instance.mjs +250 -0
- package/scripts/mod-runtime/lifecycle.mjs +45 -0
- package/scripts/mod-runtime/locate.mjs +48 -0
- package/scripts/mod-runtime/mcp-entry.mjs +55 -0
- package/scripts/mod-runtime/server.mjs +178 -0
- package/scripts/mod-runtime/tool-calls.mjs +83 -0
- package/scripts/mod-runtime/ui-draw.js +122 -0
- package/scripts/mod-runtime/ui.html +61 -0
- package/scripts/mod-runtime/ui.js +149 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// detect -> stage -> compile -> audit -> write. For Claude Code and Gemini CLI
|
|
2
|
+
// sources, staging is `agy plugin install` run against a throwaway home, and
|
|
3
|
+
// the porter post-processes what agy staged (docs/agy-baseline.md). Cursor,
|
|
4
|
+
// Roo/Kilo and Codex sources, which agy doesn't detect, start from an empty
|
|
5
|
+
// plugin dir. agy only ever runs with HOME/USERPROFILE pointed at a temp dir,
|
|
6
|
+
// so the user's real profile is never read or written.
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { basename, join, resolve, sep } from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { detect } from './detector.mjs';
|
|
13
|
+
import { compileHooks, writeHookPort } from './hook-compiler.mjs';
|
|
14
|
+
import { sanitizeManifest, sourceManifest } from './manifest-sanitizer.mjs';
|
|
15
|
+
import { compileMods, userConfigDefaults } from './mod-compiler.mjs';
|
|
16
|
+
import { compileModes } from './mode-compiler.mjs';
|
|
17
|
+
import { compileRules } from './rules-compiler.mjs';
|
|
18
|
+
import { compileSkills, slugifyName } from './skill-compiler.mjs';
|
|
19
|
+
import { removeTree } from './source.mjs';
|
|
20
|
+
import { auditPlugin } from './static-auditor.mjs';
|
|
21
|
+
|
|
22
|
+
const AGY_STAGED = ['claude', 'gemini'];
|
|
23
|
+
|
|
24
|
+
export function throwawayEnv(home) {
|
|
25
|
+
return { ...process.env, HOME: home, USERPROFILE: home, NO_COLOR: '1' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Spawns agy. An `agy` that names a .mjs/.js file runs under this Node (tests use a stand-in).
|
|
29
|
+
export function spawnAgy(agy, args, options) {
|
|
30
|
+
return /\.m?js$/i.test(agy) ? spawnSync(process.execPath, [agy, ...args], options) : spawnSync(agy, args, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function runAgy(args, { agy = 'agy', home }) {
|
|
34
|
+
const run = spawnAgy(agy, args, { encoding: 'utf8', env: throwawayEnv(home), timeout: 120_000 });
|
|
35
|
+
if (run.error) throw new Error(`can't run ${agy}: ${run.error.message}`);
|
|
36
|
+
return run;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Runs `agy plugin install <srcDir>` in a throwaway home and returns the staged dir.
|
|
40
|
+
export function stageWithAgy(srcDir, { agy, workDir }) {
|
|
41
|
+
const home = join(workDir, 'home');
|
|
42
|
+
mkdirSync(home, { recursive: true });
|
|
43
|
+
const run = runAgy(['plugin', 'install', srcDir], { agy, home });
|
|
44
|
+
const pluginsDir = join(home, '.gemini', 'config', 'plugins');
|
|
45
|
+
const staged = existsSync(pluginsDir) ? readdirSync(pluginsDir).filter((d) => statSync(join(pluginsDir, d)).isDirectory()) : [];
|
|
46
|
+
if (run.status !== 0 || staged.length !== 1) {
|
|
47
|
+
throw new Error(`agy plugin install failed (exit ${run.status}): ${(run.stdout + run.stderr).trim()}`);
|
|
48
|
+
}
|
|
49
|
+
return join(pluginsDir, staged[0]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const removeEmptyDirs = (dir) => {
|
|
53
|
+
for (const entry of readdirSync(dir)) {
|
|
54
|
+
const full = join(dir, entry);
|
|
55
|
+
if (statSync(full).isDirectory()) {
|
|
56
|
+
removeEmptyDirs(full);
|
|
57
|
+
if (readdirSync(full).length === 0) rmSync(full, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const merge = (into, part) => {
|
|
63
|
+
into.ported.push(...part.ported);
|
|
64
|
+
into.unported.push(...part.unported);
|
|
65
|
+
into.warnings.push(...part.warnings);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function defaultSlug(srcDir, ecosystems) {
|
|
69
|
+
const name = sourceManifest(srcDir, ecosystems).name;
|
|
70
|
+
return slugifyName(typeof name === 'string' && name ? name : basename(resolve(srcDir)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeCopyFilter(rootDir, skipRe) {
|
|
74
|
+
let rootReal = '';
|
|
75
|
+
try { rootReal = realpathSync(rootDir); } catch { rootReal = resolve(rootDir); }
|
|
76
|
+
return (p) => {
|
|
77
|
+
if (skipRe.test(p)) return false;
|
|
78
|
+
try {
|
|
79
|
+
const lst = lstatSync(p);
|
|
80
|
+
if (lst.isSymbolicLink()) {
|
|
81
|
+
const targetReal = realpathSync(p);
|
|
82
|
+
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep)) return false;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Ports srcDir into outDir. Returns the port report. `esbuild` overrides the optional
|
|
92
|
+
// esbuild the mod compiler loads for TS/TSX mods (null: act as if it's missing).
|
|
93
|
+
export function transpile({ srcDir, outDir, slug, agy = 'agy', force = false, validate = false, esbuild }) {
|
|
94
|
+
srcDir = resolve(srcDir);
|
|
95
|
+
const detection = detect(srcDir);
|
|
96
|
+
const { ecosystems } = detection;
|
|
97
|
+
if (ecosystems.length === 0) throw new Error(`no plugin found in ${srcDir} (looked for Claude Code, Gemini CLI, Cursor, Roo/Kilo, Codex and Antigravity layouts)`);
|
|
98
|
+
slug = slug ?? defaultSlug(srcDir, ecosystems);
|
|
99
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(slug)) throw new Error(`invalid slug "${slug}": use lowercase letters, digits, ".", "_" and "-"`);
|
|
100
|
+
outDir = resolve(outDir);
|
|
101
|
+
if (existsSync(outDir) && readdirSync(outDir).length && !force) throw new Error(`${outDir} isn't empty (pass force to replace it)`);
|
|
102
|
+
|
|
103
|
+
const report = { slug, ecosystems, ported: [], unported: [], warnings: [], flags: [], tier: null, tierReasons: [] };
|
|
104
|
+
const workDir = mkdtempSync(join(tmpdir(), 'agy-porter-'));
|
|
105
|
+
try {
|
|
106
|
+
const pluginDir = join(workDir, 'plugin');
|
|
107
|
+
const stagedPaths = [];
|
|
108
|
+
if (ecosystems.includes('antigravity')) {
|
|
109
|
+
cpSync(srcDir, pluginDir, { recursive: true, filter: makeCopyFilter(srcDir, /[\\/](\.git|node_modules)$/) });
|
|
110
|
+
report.ported.push('native plugin (copied as is)');
|
|
111
|
+
} else if (ecosystems.some((e) => AGY_STAGED.includes(e))) {
|
|
112
|
+
let stageFrom = srcDir;
|
|
113
|
+
if (detection.manifestless) {
|
|
114
|
+
// agy only detects Claude plugins by their manifest, so stage a copy with one.
|
|
115
|
+
stageFrom = join(workDir, 'src');
|
|
116
|
+
cpSync(srcDir, stageFrom, { recursive: true, filter: makeCopyFilter(srcDir, /[\\/](\.git|node_modules)$/) });
|
|
117
|
+
mkdirSync(join(stageFrom, '.claude-plugin'));
|
|
118
|
+
writeFileSync(join(stageFrom, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: slug }));
|
|
119
|
+
report.warnings.push('no .claude-plugin/plugin.json: ported as a Claude Code plugin from its default folders');
|
|
120
|
+
}
|
|
121
|
+
const staged = stageWithAgy(stageFrom, { agy, workDir });
|
|
122
|
+
// agy may write the resolved path (macOS /var -> /private/var, Windows 8.3 names).
|
|
123
|
+
stagedPaths.push(...new Set([staged, realpathSync.native(staged)]));
|
|
124
|
+
cpSync(staged, pluginDir, { recursive: true, filter: makeCopyFilter(staged, /[\\/]\.git$/) });
|
|
125
|
+
} else {
|
|
126
|
+
mkdirSync(pluginDir);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!ecosystems.includes('antigravity')) {
|
|
130
|
+
merge(report, sanitizeManifest(pluginDir, { srcDir, ecosystems, slug, stagedPaths }));
|
|
131
|
+
const hooks = compileHooks(detection.hookSources, { slug });
|
|
132
|
+
writeHookPort(pluginDir, hooks);
|
|
133
|
+
merge(report, hooks.report);
|
|
134
|
+
// UI mods become sidecars (it merges its hooks into hooks.json, so it runs after
|
|
135
|
+
// writeHookPort). The raw module files agy staged are dropped either way.
|
|
136
|
+
if (detection.uiMods.length) {
|
|
137
|
+
const manifest = sourceManifest(srcDir, ecosystems);
|
|
138
|
+
merge(report, compileMods(detection.uiMods, {
|
|
139
|
+
srcDir, pluginDir, slug, esbuild,
|
|
140
|
+
sourceName: typeof manifest.name === 'string' ? manifest.name : slug,
|
|
141
|
+
title: typeof manifest.displayName === 'string' ? manifest.displayName : undefined,
|
|
142
|
+
version: typeof manifest.version === 'string' ? manifest.version : undefined,
|
|
143
|
+
options: userConfigDefaults(manifest.userConfig),
|
|
144
|
+
}));
|
|
145
|
+
if (typeof manifest.types === 'string') rmSync(join(pluginDir, manifest.types), { force: true });
|
|
146
|
+
}
|
|
147
|
+
merge(report, compileSkills(pluginDir, { srcDir, ecosystems }));
|
|
148
|
+
merge(report, compileRules(pluginDir, { srcDir, ecosystems }));
|
|
149
|
+
merge(report, compileModes(pluginDir, { srcDir, ecosystems, slug }));
|
|
150
|
+
removeEmptyDirs(pluginDir);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const audit = auditPlugin(pluginDir);
|
|
154
|
+
report.flags = audit.flags;
|
|
155
|
+
report.warnings.push(...audit.warnings);
|
|
156
|
+
report.tier = audit.tier;
|
|
157
|
+
report.tierReasons = audit.reasons;
|
|
158
|
+
|
|
159
|
+
removeTree(outDir);
|
|
160
|
+
mkdirSync(outDir, { recursive: true });
|
|
161
|
+
cpSync(pluginDir, outDir, { recursive: true });
|
|
162
|
+
if (validate) report.validation = validateWithAgy(outDir, { agy, workDir });
|
|
163
|
+
return report;
|
|
164
|
+
} finally {
|
|
165
|
+
removeTree(workDir);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// `agy plugin validate --json` in a throwaway home.
|
|
170
|
+
export function validateWithAgy(pluginDir, { agy = 'agy', workDir } = {}) {
|
|
171
|
+
const ownDir = !workDir;
|
|
172
|
+
const dir = workDir ?? mkdtempSync(join(tmpdir(), 'agy-porter-validate-'));
|
|
173
|
+
const home = join(dir, 'validate-home');
|
|
174
|
+
mkdirSync(home, { recursive: true });
|
|
175
|
+
try {
|
|
176
|
+
const run = runAgy(['plugin', 'validate', pluginDir, '--json'], { agy, home });
|
|
177
|
+
let result = null;
|
|
178
|
+
try { result = JSON.parse(run.stdout); } catch {}
|
|
179
|
+
if (!result) return { ok: false, errors: [], warnings: [], output: (run.stdout + run.stderr).trim() };
|
|
180
|
+
const findings = result.findings ?? [];
|
|
181
|
+
return {
|
|
182
|
+
ok: run.status === 0,
|
|
183
|
+
components: result.components,
|
|
184
|
+
errors: findings.filter((f) => f.severity === 'error'),
|
|
185
|
+
warnings: findings.filter((f) => f.severity === 'warning'),
|
|
186
|
+
};
|
|
187
|
+
} finally {
|
|
188
|
+
if (ownDir) removeTree(dir);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// A YAML subset, enough for Markdown frontmatter and `.roomodes`: block maps and
|
|
2
|
+
// sequences, flow `[a, b]` / `{k: v}` (on one line or several), quoted and plain scalars, `|` / `>` block
|
|
3
|
+
// scalars, multi-line plain scalars and comments. Anchors, tags and multiple
|
|
4
|
+
// documents are not supported. Like agy's loader, it tolerates `: ` inside an
|
|
5
|
+
// unquoted value (`description: Runs checks: lint`).
|
|
6
|
+
|
|
7
|
+
const KEY = /^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'|[^\s"'#\-[{][^:#]*?|-[^\s:#][^:#]*?)\s*:(?=\s|$)/;
|
|
8
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
9
|
+
|
|
10
|
+
function unquote(s) {
|
|
11
|
+
if (s.startsWith('"')) {
|
|
12
|
+
try { return JSON.parse(s); } catch { return s.slice(1, -1); }
|
|
13
|
+
}
|
|
14
|
+
if (s.startsWith("'")) return s.slice(1, -1).replace(/''/g, "'");
|
|
15
|
+
return s;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripComment(s) {
|
|
19
|
+
let quote = null;
|
|
20
|
+
for (let i = 0; i < s.length; i++) {
|
|
21
|
+
const c = s[i];
|
|
22
|
+
if (quote) {
|
|
23
|
+
if (c === quote && !(quote === '"' && s[i - 1] === '\\')) quote = null;
|
|
24
|
+
} else if (c === '"' || c === "'") {
|
|
25
|
+
if (i === 0 || /[\s[{,]/.test(s[i - 1])) quote = c;
|
|
26
|
+
} else if (c === '#' && (i === 0 || /\s/.test(s[i - 1]))) {
|
|
27
|
+
return s.slice(0, i).trimEnd();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function splitFlow(s) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
let depth = 0;
|
|
36
|
+
let quote = null;
|
|
37
|
+
let start = 0;
|
|
38
|
+
for (let i = 0; i < s.length; i++) {
|
|
39
|
+
const c = s[i];
|
|
40
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
41
|
+
if (c === '"' || c === "'") quote = c;
|
|
42
|
+
else if (c === '[' || c === '{') depth++;
|
|
43
|
+
else if (c === ']' || c === '}') depth--;
|
|
44
|
+
else if (c === ',' && depth === 0) { parts.push(s.slice(start, i)); start = i + 1; }
|
|
45
|
+
}
|
|
46
|
+
parts.push(s.slice(start));
|
|
47
|
+
return parts.map((p) => p.trim()).filter((p) => p !== '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// True once every `[` and `{` outside quotes is closed.
|
|
51
|
+
function flowClosed(s) {
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let quote = null;
|
|
54
|
+
for (const c of s) {
|
|
55
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
56
|
+
if (c === '"' || c === "'") quote = c;
|
|
57
|
+
else if (c === '[' || c === '{') depth++;
|
|
58
|
+
else if (c === ']' || c === '}') depth--;
|
|
59
|
+
}
|
|
60
|
+
return depth <= 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseScalar(raw) {
|
|
64
|
+
const s = stripComment(raw.trim());
|
|
65
|
+
if (s === '') return null;
|
|
66
|
+
if (s.startsWith('"') || s.startsWith("'")) return unquote(s);
|
|
67
|
+
if (s.startsWith('[') && s.endsWith(']')) return splitFlow(s.slice(1, -1)).map(parseScalar);
|
|
68
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
69
|
+
const obj = {};
|
|
70
|
+
for (const part of splitFlow(s.slice(1, -1))) {
|
|
71
|
+
const m = KEY.exec(part);
|
|
72
|
+
if (m) {
|
|
73
|
+
const k = unquote(m[1]);
|
|
74
|
+
if (!UNSAFE_KEYS.has(k)) obj[k] = parseScalar(part.slice(m[0].length));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return obj;
|
|
78
|
+
}
|
|
79
|
+
if (/^(true|True|TRUE)$/.test(s)) return true;
|
|
80
|
+
if (/^(false|False|FALSE)$/.test(s)) return false;
|
|
81
|
+
if (/^(null|Null|NULL|~)$/.test(s)) return null;
|
|
82
|
+
if (/^[-+]?\d+(\.\d+)?$/.test(s)) return Number(s);
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseYaml(text) {
|
|
87
|
+
const lines = String(text).replace(/^/, '').replace(/\r\n?/g, '\n').split('\n')
|
|
88
|
+
.map((raw) => ({ raw, indent: raw.match(/^ */)[0].length, text: raw.trim() }));
|
|
89
|
+
let i = 0;
|
|
90
|
+
if (lines[0]?.text === '---') i = 1;
|
|
91
|
+
const blank = (l) => l.text === '' || l.text.startsWith('#');
|
|
92
|
+
const skip = () => { while (i < lines.length && blank(lines[i])) i++; };
|
|
93
|
+
const isSeq = (t) => t === '-' || t.startsWith('- ');
|
|
94
|
+
|
|
95
|
+
function blockScalar(header, parentIndent) {
|
|
96
|
+
const folded = header.startsWith('>');
|
|
97
|
+
const chomp = header.includes('-') ? 'strip' : header.includes('+') ? 'keep' : 'clip';
|
|
98
|
+
const body = [];
|
|
99
|
+
while (i < lines.length && (lines[i].text === '' || lines[i].indent > parentIndent)) body.push(lines[i++]);
|
|
100
|
+
const first = body.find((l) => l.text !== '');
|
|
101
|
+
const ind = first ? first.indent : 0;
|
|
102
|
+
const rows = body.map((l) => (l.text === '' ? '' : l.raw.slice(ind)));
|
|
103
|
+
let out;
|
|
104
|
+
if (folded) {
|
|
105
|
+
out = '';
|
|
106
|
+
rows.forEach((row, k) => {
|
|
107
|
+
if (k === 0) out = row;
|
|
108
|
+
else if (row === '' || rows[k - 1] === '' || /^\s/.test(row)) out += `\n${row}`;
|
|
109
|
+
else out += ` ${row}`;
|
|
110
|
+
});
|
|
111
|
+
out = out.replace(/\n(?=\n)/g, '');
|
|
112
|
+
} else out = rows.join('\n');
|
|
113
|
+
const trimmed = out.replace(/\n+$/, '');
|
|
114
|
+
if (chomp === 'strip') return trimmed;
|
|
115
|
+
if (chomp === 'keep') return out.endsWith('\n') ? out : `${out}\n`;
|
|
116
|
+
return trimmed === '' ? '' : `${trimmed}\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A flow `[...]` or `{...}` that spans lines (`allowed-tools:\n [\n "Read",\n ]`):
|
|
120
|
+
// joins lines until its brackets close.
|
|
121
|
+
function flowValue(first) {
|
|
122
|
+
let text = stripComment(first);
|
|
123
|
+
while (!flowClosed(text) && i < lines.length) text += ` ${stripComment(lines[i++].text)}`;
|
|
124
|
+
if (!flowClosed(text)) throw new Error(`yaml: unclosed ${text[0]} at the end of the document`);
|
|
125
|
+
return parseScalar(text);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A value on the same line as its key or dash, with continuation lines for plain scalars.
|
|
129
|
+
function inlineValue(rest, indent) {
|
|
130
|
+
if (/^[|>][-+0-9]*$/.test(stripComment(rest))) return blockScalar(rest, indent);
|
|
131
|
+
if (/^[[{]/.test(rest) && !flowClosed(stripComment(rest))) return flowValue(rest);
|
|
132
|
+
const value = parseScalar(rest);
|
|
133
|
+
if (typeof value !== 'string' || /^["'[{]/.test(rest)) return value;
|
|
134
|
+
let joined = value;
|
|
135
|
+
while (i < lines.length && lines[i].text !== '' && lines[i].indent > indent && !KEY.test(lines[i].text) && !isSeq(lines[i].text)) {
|
|
136
|
+
joined += ` ${stripComment(lines[i++].text)}`;
|
|
137
|
+
}
|
|
138
|
+
return joined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function node(indent) {
|
|
142
|
+
skip();
|
|
143
|
+
if (i >= lines.length || lines[i].indent < indent) return null;
|
|
144
|
+
if (/^[[{]/.test(lines[i].text)) return flowValue(lines[i++].text);
|
|
145
|
+
return isSeq(lines[i].text) ? seq(lines[i].indent) : map(lines[i].indent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function seq(indent) {
|
|
149
|
+
const items = [];
|
|
150
|
+
for (skip(); i < lines.length && lines[i].indent === indent && isSeq(lines[i].text); skip()) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
const rest = line.text.slice(1).trimStart();
|
|
153
|
+
if (rest === '' || rest.startsWith('#')) { i++; items.push(node(indent + 1)); continue; }
|
|
154
|
+
if (isSeq(rest) || KEY.test(rest)) {
|
|
155
|
+
// `- - x` or `- key: v`: reparse the rest as a node at its own column.
|
|
156
|
+
const col = line.raw.indexOf(rest, line.indent + 1);
|
|
157
|
+
lines[i] = { raw: ' '.repeat(col) + rest, indent: col, text: rest };
|
|
158
|
+
items.push(node(col));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
i++;
|
|
162
|
+
items.push(inlineValue(rest, indent));
|
|
163
|
+
}
|
|
164
|
+
return items;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function map(indent) {
|
|
168
|
+
const obj = {};
|
|
169
|
+
for (skip(); i < lines.length && lines[i].indent === indent; skip()) {
|
|
170
|
+
const m = KEY.exec(lines[i].text);
|
|
171
|
+
if (!m) throw new Error(`yaml: can't parse line ${i + 1}: ${lines[i].text}`);
|
|
172
|
+
const key = unquote(m[1].trim());
|
|
173
|
+
const rest = lines[i].text.slice(m[0].length).trim();
|
|
174
|
+
i++;
|
|
175
|
+
if (rest === '' || rest.startsWith('#')) {
|
|
176
|
+
skip();
|
|
177
|
+
const next = lines[i];
|
|
178
|
+
const val = next && (next.indent > indent || (next.indent === indent && isSeq(next.text))) ? node(next.indent) : null;
|
|
179
|
+
if (!UNSAFE_KEYS.has(key)) obj[key] = val;
|
|
180
|
+
} else {
|
|
181
|
+
const val = inlineValue(rest, indent);
|
|
182
|
+
if (!UNSAFE_KEYS.has(key)) obj[key] = val;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return obj;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = node(0);
|
|
189
|
+
skip();
|
|
190
|
+
if (i < lines.length && lines[i].text !== '---') throw new Error(`yaml: unexpected indentation at line ${i + 1}`);
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Splits `---\n<yaml>\n---\n<body>`. Returns {data, body, hasFrontmatter}.
|
|
195
|
+
export function splitFrontmatter(text) {
|
|
196
|
+
const src = String(text).replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
197
|
+
const m = /^---\n([\s\S]*?\n)?---[ \t]*(?:\n|$)/.exec(src);
|
|
198
|
+
if (!m) return { data: {}, body: src, hasFrontmatter: false };
|
|
199
|
+
return { data: parseYaml(m[1] ?? '') ?? {}, body: src.slice(m[0].length), hasFrontmatter: true };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function scalar(value) {
|
|
203
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
204
|
+
const s = String(value);
|
|
205
|
+
const plain = /^[A-Za-z0-9_./*][^\n:#]*$/.test(s) && s === s.trim()
|
|
206
|
+
&& !/^(true|false|null|yes|no|on|off|~|[-+]?\d+(\.\d+)?)$/i.test(s) && !/[[\]{},&*!|>'"%@`]/.test(s[0]);
|
|
207
|
+
return plain ? s : JSON.stringify(s);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Writes frontmatter from plain values; arrays become block lists, null/undefined are skipped.
|
|
211
|
+
export function formatFrontmatter(data, body) {
|
|
212
|
+
const lines = ['---'];
|
|
213
|
+
for (const [key, value] of Object.entries(data)) {
|
|
214
|
+
if (value === undefined || value === null) continue;
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
lines.push(value.length ? `${key}:` : `${key}: []`);
|
|
217
|
+
value.forEach((v) => lines.push(` - ${scalar(v)}`));
|
|
218
|
+
} else lines.push(`${key}: ${scalar(value)}`);
|
|
219
|
+
}
|
|
220
|
+
lines.push('---', '');
|
|
221
|
+
return `${lines.join('\n')}${body.replace(/^\n+/, '\n')}`;
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-plugin-porter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Ports Claude Code, Gemini CLI, Cursor, Roo/Kilo and Codex plugins to native Antigravity plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Arthur Cavallari",
|
|
9
|
+
"url": "https://github.com/arthurcavallari"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/arthurcavallari/agy-plugin-porter#readme",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/arthurcavallari/agy-plugin-porter.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/arthurcavallari/agy-plugin-porter/issues"
|
|
18
|
+
},
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "github",
|
|
21
|
+
"url": "https://github.com/sponsors/arthurcavallari"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"antigravity",
|
|
25
|
+
"google-antigravity",
|
|
26
|
+
"agy",
|
|
27
|
+
"agy-plugin",
|
|
28
|
+
"antigravity-plugin",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"claude-code-plugin",
|
|
31
|
+
"gemini-cli",
|
|
32
|
+
"cursor",
|
|
33
|
+
"roo-code",
|
|
34
|
+
"codex",
|
|
35
|
+
"plugin-porter",
|
|
36
|
+
"hooks",
|
|
37
|
+
"mcp"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20.0.0"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"agy-porter": "bin/agy-porter.mjs"
|
|
44
|
+
},
|
|
45
|
+
"optionalDependencies": {
|
|
46
|
+
"esbuild": "0.28.2"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"bin",
|
|
50
|
+
"lib",
|
|
51
|
+
"scripts/hook-shim.mjs",
|
|
52
|
+
"scripts/mod-runtime",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"exports": {
|
|
57
|
+
".": "./lib/index.mjs",
|
|
58
|
+
"./consent": "./lib/consent.mjs",
|
|
59
|
+
"./detector": "./lib/detector.mjs",
|
|
60
|
+
"./hook-compiler": "./lib/hook-compiler.mjs",
|
|
61
|
+
"./manifest-sanitizer": "./lib/manifest-sanitizer.mjs",
|
|
62
|
+
"./mod-compiler": "./lib/mod-compiler.mjs",
|
|
63
|
+
"./skill-compiler": "./lib/skill-compiler.mjs",
|
|
64
|
+
"./rules-compiler": "./lib/rules-compiler.mjs",
|
|
65
|
+
"./mode-compiler": "./lib/mode-compiler.mjs",
|
|
66
|
+
"./static-auditor": "./lib/static-auditor.mjs",
|
|
67
|
+
"./transpiler-engine": "./lib/transpiler-engine.mjs",
|
|
68
|
+
"./package.json": "./package.json"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"test": "node --test test/yaml-lite.test.mjs test/detector.test.mjs test/hook-compiler.test.mjs test/hook-shim.test.mjs test/compilers.test.mjs test/transpiler.test.mjs test/mod-scan.test.mjs test/mod-compiler.test.mjs test/mod-continuation.test.mjs test/mod-runtime.test.mjs test/mod-stage.test.mjs test/mod-e2e.test.mjs test/consent.test.mjs test/session-start.test.mjs test/plugin-build.test.mjs test/plugin-mcp.test.mjs test/cockpit.test.mjs",
|
|
72
|
+
"test:pack": "node scripts/pack-smoke.mjs",
|
|
73
|
+
"build:plugin": "node scripts/build-plugin.mjs"
|
|
74
|
+
}
|
|
75
|
+
}
|