agy-plugin-porter 0.0.0-stage → 0.1.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 +100 -2
- package/bin/agy-porter.mjs +91 -0
- package/lib/detector.mjs +91 -0
- package/lib/hook-compiler.mjs +172 -0
- package/lib/index.mjs +29 -0
- package/lib/manifest-sanitizer.mjs +151 -0
- package/lib/mode-compiler.mjs +185 -0
- package/lib/rules-compiler.mjs +144 -0
- package/lib/skill-compiler.mjs +132 -0
- package/lib/source.mjs +58 -0
- package/lib/static-auditor.mjs +140 -0
- package/lib/tool-map.mjs +47 -0
- package/lib/transpiler-engine.mjs +158 -0
- package/lib/yaml-lite.mjs +190 -0
- package/package.json +62 -4
- package/scripts/hook-shim.mjs +244 -0
package/lib/tool-map.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Source tool names -> agy tool names. Claude's comes from `agy plugin validate`
|
|
2
|
+
// 1.4.5 (docs/agy-baseline.md, "Tool names"); where the CLI and the IDE name a
|
|
3
|
+
// tool differently, both are listed. scripts/hook-shim.mjs keeps its own reverse
|
|
4
|
+
// copy because it ships alone inside each ported plugin.
|
|
5
|
+
export const TOOLS = {
|
|
6
|
+
claude: {
|
|
7
|
+
Bash: ['run_command'],
|
|
8
|
+
Read: ['view_file', 'read_file'],
|
|
9
|
+
Write: ['write_to_file', 'write_file'],
|
|
10
|
+
Edit: ['replace_file_content', 'edit_file'],
|
|
11
|
+
MultiEdit: ['multi_replace_file_content', 'edit_file'],
|
|
12
|
+
Grep: ['grep_search'],
|
|
13
|
+
Glob: ['find_by_name'],
|
|
14
|
+
LS: ['list_dir'],
|
|
15
|
+
WebFetch: ['read_url_content'],
|
|
16
|
+
WebSearch: ['search_web'],
|
|
17
|
+
NotebookEdit: ['notebook_edit'],
|
|
18
|
+
Task: ['invoke_subagent'],
|
|
19
|
+
Agent: ['invoke_subagent'],
|
|
20
|
+
},
|
|
21
|
+
gemini: {
|
|
22
|
+
run_shell_command: ['run_command'],
|
|
23
|
+
read_file: ['view_file', 'read_file'],
|
|
24
|
+
write_file: ['write_to_file', 'write_file'],
|
|
25
|
+
replace: ['replace_file_content', 'edit_file'],
|
|
26
|
+
glob: ['find_by_name'],
|
|
27
|
+
search_file_content: ['grep_search'],
|
|
28
|
+
list_directory: ['list_dir'],
|
|
29
|
+
web_fetch: ['read_url_content'],
|
|
30
|
+
google_web_search: ['search_web'],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const AGY_TOOLS = new Set(Object.values(TOOLS).flatMap((m) => Object.values(m).flat()));
|
|
35
|
+
|
|
36
|
+
// Roo/Kilo/Cline mode tool groups.
|
|
37
|
+
export const ROO_GROUPS = {
|
|
38
|
+
read: ['view_file', 'read_file', 'list_dir', 'grep_search', 'find_by_name'],
|
|
39
|
+
edit: ['write_to_file', 'write_file', 'replace_file_content', 'edit_file', 'multi_replace_file_content'],
|
|
40
|
+
command: ['run_command'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function unmappedToolReason(name) {
|
|
44
|
+
if (name.startsWith('mcp__')) return 'MCP tool names are not mapped yet (agy MCP step names are unverified)';
|
|
45
|
+
if (name === 'TodoWrite' || /^Task(Create|Get|List|Update|Output|Stop)$/.test(name)) return 'agy has no built-in tool that does the same job';
|
|
46
|
+
return 'no agy tool with this name';
|
|
47
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
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, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { basename, join, posix, resolve } 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 { compileModes } from './mode-compiler.mjs';
|
|
16
|
+
import { compileRules } from './rules-compiler.mjs';
|
|
17
|
+
import { compileSkills, slugifyName } from './skill-compiler.mjs';
|
|
18
|
+
import { auditPlugin } from './static-auditor.mjs';
|
|
19
|
+
|
|
20
|
+
const AGY_STAGED = ['claude', 'gemini'];
|
|
21
|
+
|
|
22
|
+
export function throwawayEnv(home) {
|
|
23
|
+
return { ...process.env, HOME: home, USERPROFILE: home, NO_COLOR: '1' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runAgy(args, { agy = 'agy', home }) {
|
|
27
|
+
const run = spawnSync(agy, args, { encoding: 'utf8', env: throwawayEnv(home), timeout: 120_000 });
|
|
28
|
+
if (run.error) throw new Error(`can't run ${agy}: ${run.error.message}`);
|
|
29
|
+
return run;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Runs `agy plugin install <srcDir>` in a throwaway home and returns the staged dir.
|
|
33
|
+
export function stageWithAgy(srcDir, { agy, workDir }) {
|
|
34
|
+
const home = join(workDir, 'home');
|
|
35
|
+
mkdirSync(home, { recursive: true });
|
|
36
|
+
const run = runAgy(['plugin', 'install', srcDir], { agy, home });
|
|
37
|
+
const pluginsDir = join(home, '.gemini', 'config', 'plugins');
|
|
38
|
+
const staged = existsSync(pluginsDir) ? readdirSync(pluginsDir).filter((d) => statSync(join(pluginsDir, d)).isDirectory()) : [];
|
|
39
|
+
if (run.status !== 0 || staged.length !== 1) {
|
|
40
|
+
throw new Error(`agy plugin install failed (exit ${run.status}): ${(run.stdout + run.stderr).trim()}`);
|
|
41
|
+
}
|
|
42
|
+
return join(pluginsDir, staged[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const removeEmptyDirs = (dir) => {
|
|
46
|
+
for (const entry of readdirSync(dir)) {
|
|
47
|
+
const full = join(dir, entry);
|
|
48
|
+
if (statSync(full).isDirectory()) {
|
|
49
|
+
removeEmptyDirs(full);
|
|
50
|
+
if (readdirSync(full).length === 0) rmSync(full, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const merge = (into, part) => {
|
|
56
|
+
into.ported.push(...part.ported);
|
|
57
|
+
into.unported.push(...part.unported);
|
|
58
|
+
into.warnings.push(...part.warnings);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function defaultSlug(srcDir, ecosystems) {
|
|
62
|
+
const name = sourceManifest(srcDir, ecosystems).name;
|
|
63
|
+
return slugifyName(typeof name === 'string' && name ? name : basename(resolve(srcDir)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Ports srcDir into outDir. Returns the port report.
|
|
67
|
+
export function transpile({ srcDir, outDir, slug, agy = 'agy', force = false, validate = false }) {
|
|
68
|
+
srcDir = resolve(srcDir);
|
|
69
|
+
const detection = detect(srcDir);
|
|
70
|
+
const { ecosystems } = detection;
|
|
71
|
+
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)`);
|
|
72
|
+
slug = slug ?? defaultSlug(srcDir, ecosystems);
|
|
73
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(slug)) throw new Error(`invalid slug "${slug}": use lowercase letters, digits, ".", "_" and "-"`);
|
|
74
|
+
outDir = resolve(outDir);
|
|
75
|
+
if (existsSync(outDir) && readdirSync(outDir).length && !force) throw new Error(`${outDir} isn't empty (pass force to replace it)`);
|
|
76
|
+
|
|
77
|
+
const report = { slug, ecosystems, ported: [], unported: [], warnings: [], flags: [], tier: null, tierReasons: [] };
|
|
78
|
+
const workDir = mkdtempSync(join(tmpdir(), 'agy-porter-'));
|
|
79
|
+
try {
|
|
80
|
+
const pluginDir = join(workDir, 'plugin');
|
|
81
|
+
const stagedPaths = [];
|
|
82
|
+
if (ecosystems.includes('antigravity')) {
|
|
83
|
+
cpSync(srcDir, pluginDir, { recursive: true, filter: (p) => !/[\\/](\.git|node_modules)$/.test(p) });
|
|
84
|
+
report.ported.push('native plugin (copied as is)');
|
|
85
|
+
} else if (ecosystems.some((e) => AGY_STAGED.includes(e))) {
|
|
86
|
+
let stageFrom = srcDir;
|
|
87
|
+
if (detection.manifestless) {
|
|
88
|
+
// agy only detects Claude plugins by their manifest, so stage a copy with one.
|
|
89
|
+
stageFrom = join(workDir, 'src');
|
|
90
|
+
cpSync(srcDir, stageFrom, { recursive: true, filter: (p) => !/[\\/](\.git|node_modules)$/.test(p) });
|
|
91
|
+
mkdirSync(join(stageFrom, '.claude-plugin'));
|
|
92
|
+
writeFileSync(join(stageFrom, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: slug }));
|
|
93
|
+
report.warnings.push('no .claude-plugin/plugin.json: ported as a Claude Code plugin from its default folders');
|
|
94
|
+
}
|
|
95
|
+
const staged = stageWithAgy(stageFrom, { agy, workDir });
|
|
96
|
+
// agy may write the resolved path (macOS /var -> /private/var, Windows 8.3 names).
|
|
97
|
+
stagedPaths.push(...new Set([staged, realpathSync.native(staged)]));
|
|
98
|
+
cpSync(staged, pluginDir, { recursive: true, filter: (p) => !/[\\/]\.git$/.test(p) });
|
|
99
|
+
} else {
|
|
100
|
+
mkdirSync(pluginDir);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!ecosystems.includes('antigravity')) {
|
|
104
|
+
merge(report, sanitizeManifest(pluginDir, { srcDir, ecosystems, slug, stagedPaths }));
|
|
105
|
+
const hooks = compileHooks(detection.hookSources, { slug });
|
|
106
|
+
writeHookPort(pluginDir, hooks);
|
|
107
|
+
merge(report, hooks.report);
|
|
108
|
+
// UI mods are reported, never copied: drop the module files agy staged.
|
|
109
|
+
for (const mod of detection.uiMods) {
|
|
110
|
+
const base = posix.dirname(mod.path.split('#')[0]);
|
|
111
|
+
for (const m of mod.modules) rmSync(join(pluginDir, posix.join(base, String(m))), { force: true });
|
|
112
|
+
const types = sourceManifest(srcDir, ecosystems).types;
|
|
113
|
+
if (typeof types === 'string') rmSync(join(pluginDir, types), { force: true });
|
|
114
|
+
}
|
|
115
|
+
merge(report, compileSkills(pluginDir, { srcDir, ecosystems }));
|
|
116
|
+
merge(report, compileRules(pluginDir, { srcDir, ecosystems }));
|
|
117
|
+
merge(report, compileModes(pluginDir, { srcDir, ecosystems, slug }));
|
|
118
|
+
removeEmptyDirs(pluginDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const audit = auditPlugin(pluginDir);
|
|
122
|
+
report.flags = audit.flags;
|
|
123
|
+
report.warnings.push(...audit.warnings);
|
|
124
|
+
report.tier = audit.tier;
|
|
125
|
+
report.tierReasons = audit.reasons;
|
|
126
|
+
|
|
127
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
128
|
+
mkdirSync(outDir, { recursive: true });
|
|
129
|
+
cpSync(pluginDir, outDir, { recursive: true });
|
|
130
|
+
if (validate) report.validation = validateWithAgy(outDir, { agy, workDir });
|
|
131
|
+
return report;
|
|
132
|
+
} finally {
|
|
133
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// `agy plugin validate --json` in a throwaway home.
|
|
138
|
+
export function validateWithAgy(pluginDir, { agy = 'agy', workDir } = {}) {
|
|
139
|
+
const ownDir = !workDir;
|
|
140
|
+
const dir = workDir ?? mkdtempSync(join(tmpdir(), 'agy-porter-validate-'));
|
|
141
|
+
const home = join(dir, 'validate-home');
|
|
142
|
+
mkdirSync(home, { recursive: true });
|
|
143
|
+
try {
|
|
144
|
+
const run = runAgy(['plugin', 'validate', pluginDir, '--json'], { agy, home });
|
|
145
|
+
let result = null;
|
|
146
|
+
try { result = JSON.parse(run.stdout); } catch {}
|
|
147
|
+
if (!result) return { ok: false, errors: [], warnings: [], output: (run.stdout + run.stderr).trim() };
|
|
148
|
+
const findings = result.findings ?? [];
|
|
149
|
+
return {
|
|
150
|
+
ok: run.status === 0,
|
|
151
|
+
components: result.components,
|
|
152
|
+
errors: findings.filter((f) => f.severity === 'error'),
|
|
153
|
+
warnings: findings.filter((f) => f.severity !== 'error'),
|
|
154
|
+
};
|
|
155
|
+
} finally {
|
|
156
|
+
if (ownDir) rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// A YAML subset, enough for Markdown frontmatter and `.roomodes`: block maps and
|
|
2
|
+
// sequences, flow `[a, b]` / `{k: v}`, 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
|
+
|
|
9
|
+
function unquote(s) {
|
|
10
|
+
if (s.startsWith('"')) {
|
|
11
|
+
try { return JSON.parse(s); } catch { return s.slice(1, -1); }
|
|
12
|
+
}
|
|
13
|
+
if (s.startsWith("'")) return s.slice(1, -1).replace(/''/g, "'");
|
|
14
|
+
return s;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stripComment(s) {
|
|
18
|
+
let quote = null;
|
|
19
|
+
for (let i = 0; i < s.length; i++) {
|
|
20
|
+
const c = s[i];
|
|
21
|
+
if (quote) {
|
|
22
|
+
if (c === quote && !(quote === '"' && s[i - 1] === '\\')) quote = null;
|
|
23
|
+
} else if (c === '"' || c === "'") {
|
|
24
|
+
if (i === 0 || /[\s[{,]/.test(s[i - 1])) quote = c;
|
|
25
|
+
} else if (c === '#' && (i === 0 || /\s/.test(s[i - 1]))) {
|
|
26
|
+
return s.slice(0, i).trimEnd();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitFlow(s) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let quote = null;
|
|
36
|
+
let start = 0;
|
|
37
|
+
for (let i = 0; i < s.length; i++) {
|
|
38
|
+
const c = s[i];
|
|
39
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
40
|
+
if (c === '"' || c === "'") quote = c;
|
|
41
|
+
else if (c === '[' || c === '{') depth++;
|
|
42
|
+
else if (c === ']' || c === '}') depth--;
|
|
43
|
+
else if (c === ',' && depth === 0) { parts.push(s.slice(start, i)); start = i + 1; }
|
|
44
|
+
}
|
|
45
|
+
parts.push(s.slice(start));
|
|
46
|
+
return parts.map((p) => p.trim()).filter((p) => p !== '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseScalar(raw) {
|
|
50
|
+
const s = stripComment(raw.trim());
|
|
51
|
+
if (s === '') return null;
|
|
52
|
+
if (s.startsWith('"') || s.startsWith("'")) return unquote(s);
|
|
53
|
+
if (s.startsWith('[') && s.endsWith(']')) return splitFlow(s.slice(1, -1)).map(parseScalar);
|
|
54
|
+
if (s.startsWith('{') && s.endsWith('}')) {
|
|
55
|
+
const obj = {};
|
|
56
|
+
for (const part of splitFlow(s.slice(1, -1))) {
|
|
57
|
+
const m = KEY.exec(part);
|
|
58
|
+
if (m) obj[unquote(m[1])] = parseScalar(part.slice(m[0].length));
|
|
59
|
+
}
|
|
60
|
+
return obj;
|
|
61
|
+
}
|
|
62
|
+
if (/^(true|True|TRUE)$/.test(s)) return true;
|
|
63
|
+
if (/^(false|False|FALSE)$/.test(s)) return false;
|
|
64
|
+
if (/^(null|Null|NULL|~)$/.test(s)) return null;
|
|
65
|
+
if (/^[-+]?\d+(\.\d+)?$/.test(s)) return Number(s);
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseYaml(text) {
|
|
70
|
+
const lines = String(text).replace(/^/, '').replace(/\r\n?/g, '\n').split('\n')
|
|
71
|
+
.map((raw) => ({ raw, indent: raw.match(/^ */)[0].length, text: raw.trim() }));
|
|
72
|
+
let i = 0;
|
|
73
|
+
if (lines[0]?.text === '---') i = 1;
|
|
74
|
+
const blank = (l) => l.text === '' || l.text.startsWith('#');
|
|
75
|
+
const skip = () => { while (i < lines.length && blank(lines[i])) i++; };
|
|
76
|
+
const isSeq = (t) => t === '-' || t.startsWith('- ');
|
|
77
|
+
|
|
78
|
+
function blockScalar(header, parentIndent) {
|
|
79
|
+
const folded = header.startsWith('>');
|
|
80
|
+
const chomp = header.includes('-') ? 'strip' : header.includes('+') ? 'keep' : 'clip';
|
|
81
|
+
const body = [];
|
|
82
|
+
while (i < lines.length && (lines[i].text === '' || lines[i].indent > parentIndent)) body.push(lines[i++]);
|
|
83
|
+
const first = body.find((l) => l.text !== '');
|
|
84
|
+
const ind = first ? first.indent : 0;
|
|
85
|
+
const rows = body.map((l) => (l.text === '' ? '' : l.raw.slice(ind)));
|
|
86
|
+
let out;
|
|
87
|
+
if (folded) {
|
|
88
|
+
out = '';
|
|
89
|
+
rows.forEach((row, k) => {
|
|
90
|
+
if (k === 0) out = row;
|
|
91
|
+
else if (row === '' || rows[k - 1] === '' || /^\s/.test(row)) out += `\n${row}`;
|
|
92
|
+
else out += ` ${row}`;
|
|
93
|
+
});
|
|
94
|
+
out = out.replace(/\n(?=\n)/g, '');
|
|
95
|
+
} else out = rows.join('\n');
|
|
96
|
+
const trimmed = out.replace(/\n+$/, '');
|
|
97
|
+
if (chomp === 'strip') return trimmed;
|
|
98
|
+
if (chomp === 'keep') return out.endsWith('\n') ? out : `${out}\n`;
|
|
99
|
+
return trimmed === '' ? '' : `${trimmed}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A value on the same line as its key or dash, with continuation lines for plain scalars.
|
|
103
|
+
function inlineValue(rest, indent) {
|
|
104
|
+
if (/^[|>][-+0-9]*$/.test(stripComment(rest))) return blockScalar(rest, indent);
|
|
105
|
+
const value = parseScalar(rest);
|
|
106
|
+
if (typeof value !== 'string' || /^["'[{]/.test(rest)) return value;
|
|
107
|
+
let joined = value;
|
|
108
|
+
while (i < lines.length && lines[i].text !== '' && lines[i].indent > indent && !KEY.test(lines[i].text) && !isSeq(lines[i].text)) {
|
|
109
|
+
joined += ` ${stripComment(lines[i++].text)}`;
|
|
110
|
+
}
|
|
111
|
+
return joined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function node(indent) {
|
|
115
|
+
skip();
|
|
116
|
+
if (i >= lines.length || lines[i].indent < indent) return null;
|
|
117
|
+
return isSeq(lines[i].text) ? seq(lines[i].indent) : map(lines[i].indent);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function seq(indent) {
|
|
121
|
+
const items = [];
|
|
122
|
+
for (skip(); i < lines.length && lines[i].indent === indent && isSeq(lines[i].text); skip()) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
const rest = line.text.slice(1).trimStart();
|
|
125
|
+
if (rest === '' || rest.startsWith('#')) { i++; items.push(node(indent + 1)); continue; }
|
|
126
|
+
if (isSeq(rest) || KEY.test(rest)) {
|
|
127
|
+
// `- - x` or `- key: v`: reparse the rest as a node at its own column.
|
|
128
|
+
const col = line.raw.indexOf(rest, line.indent + 1);
|
|
129
|
+
lines[i] = { raw: ' '.repeat(col) + rest, indent: col, text: rest };
|
|
130
|
+
items.push(node(col));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
i++;
|
|
134
|
+
items.push(inlineValue(rest, indent));
|
|
135
|
+
}
|
|
136
|
+
return items;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function map(indent) {
|
|
140
|
+
const obj = {};
|
|
141
|
+
for (skip(); i < lines.length && lines[i].indent === indent; skip()) {
|
|
142
|
+
const m = KEY.exec(lines[i].text);
|
|
143
|
+
if (!m) throw new Error(`yaml: can't parse line ${i + 1}: ${lines[i].text}`);
|
|
144
|
+
const key = unquote(m[1].trim());
|
|
145
|
+
const rest = lines[i].text.slice(m[0].length).trim();
|
|
146
|
+
i++;
|
|
147
|
+
if (rest === '' || rest.startsWith('#')) {
|
|
148
|
+
skip();
|
|
149
|
+
const next = lines[i];
|
|
150
|
+
obj[key] = next && (next.indent > indent || (next.indent === indent && isSeq(next.text))) ? node(next.indent) : null;
|
|
151
|
+
} else obj[key] = inlineValue(rest, indent);
|
|
152
|
+
}
|
|
153
|
+
return obj;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = node(0);
|
|
157
|
+
skip();
|
|
158
|
+
if (i < lines.length && lines[i].text !== '---') throw new Error(`yaml: unexpected indentation at line ${i + 1}`);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Splits `---\n<yaml>\n---\n<body>`. Returns {data, body, hasFrontmatter}.
|
|
163
|
+
export function splitFrontmatter(text) {
|
|
164
|
+
const src = String(text).replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
165
|
+
const m = /^---\n([\s\S]*?\n)?---[ \t]*(?:\n|$)/.exec(src);
|
|
166
|
+
if (!m) return { data: {}, body: src, hasFrontmatter: false };
|
|
167
|
+
return { data: parseYaml(m[1] ?? '') ?? {}, body: src.slice(m[0].length), hasFrontmatter: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function scalar(value) {
|
|
171
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
172
|
+
const s = String(value);
|
|
173
|
+
const plain = /^[A-Za-z0-9_./*][^\n:#]*$/.test(s) && s === s.trim()
|
|
174
|
+
&& !/^(true|false|null|yes|no|on|off|~|[-+]?\d+(\.\d+)?)$/i.test(s) && !/[[\]{},&*!|>'"%@`]/.test(s[0]);
|
|
175
|
+
return plain ? s : JSON.stringify(s);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Writes frontmatter from plain values; arrays become block lists, null/undefined are skipped.
|
|
179
|
+
export function formatFrontmatter(data, body) {
|
|
180
|
+
const lines = ['---'];
|
|
181
|
+
for (const [key, value] of Object.entries(data)) {
|
|
182
|
+
if (value === undefined || value === null) continue;
|
|
183
|
+
if (Array.isArray(value)) {
|
|
184
|
+
lines.push(value.length ? `${key}:` : `${key}: []`);
|
|
185
|
+
value.forEach((v) => lines.push(` - ${scalar(v)}`));
|
|
186
|
+
} else lines.push(`${key}: ${scalar(value)}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push('---', '');
|
|
189
|
+
return `${lines.join('\n')}${body.replace(/^\n+/, '\n')}`;
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-plugin-porter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.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
|
+
"keywords": [
|
|
20
|
+
"antigravity",
|
|
21
|
+
"google-antigravity",
|
|
22
|
+
"agy",
|
|
23
|
+
"agy-plugin",
|
|
24
|
+
"antigravity-plugin",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"claude-code-plugin",
|
|
27
|
+
"gemini-cli",
|
|
28
|
+
"cursor",
|
|
29
|
+
"roo-code",
|
|
30
|
+
"codex",
|
|
31
|
+
"plugin-porter",
|
|
32
|
+
"hooks",
|
|
33
|
+
"mcp"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"agy-porter": "bin/agy-porter.mjs"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"bin",
|
|
43
|
+
"lib",
|
|
44
|
+
"scripts/hook-shim.mjs",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"exports": {
|
|
49
|
+
".": "./lib/index.mjs",
|
|
50
|
+
"./detector": "./lib/detector.mjs",
|
|
51
|
+
"./hook-compiler": "./lib/hook-compiler.mjs",
|
|
52
|
+
"./manifest-sanitizer": "./lib/manifest-sanitizer.mjs",
|
|
53
|
+
"./skill-compiler": "./lib/skill-compiler.mjs",
|
|
54
|
+
"./rules-compiler": "./lib/rules-compiler.mjs",
|
|
55
|
+
"./mode-compiler": "./lib/mode-compiler.mjs",
|
|
56
|
+
"./static-auditor": "./lib/static-auditor.mjs",
|
|
57
|
+
"./transpiler-engine": "./lib/transpiler-engine.mjs",
|
|
58
|
+
"./package.json": "./package.json"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"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",
|
|
62
|
+
"test:pack": "node scripts/pack-smoke.mjs"
|
|
63
|
+
}
|
|
64
|
+
}
|