@givolt/i3-composer-agent 0.6.12 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +3 -2
- package/.claude-plugin/marketplace.json +2 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +3 -1
- package/CHANGELOG.md +34 -0
- package/NOTICE.md +6 -1
- package/README.md +116 -138
- package/bin/agent-targets.json +726 -0
- package/bin/i3-composer-agent.cjs +89 -18
- package/bin/install.cjs +164 -0
- package/bin/skills-registry.LICENSE +21 -0
- package/bin/wizard.cjs +66 -0
- package/docs/antigravity-setup.md +1 -1
- package/docs/codebuddy-setup.md +55 -8
- package/docs/codex-setup.md +13 -2
- package/docs/comparison.md +1 -1
- package/hooks/sdd-cache-post.sh +0 -0
- package/hooks/sdd-cache-pre.sh +0 -0
- package/package.json +75 -17
- package/plugin.json +4 -2
- package/rebrand/INSTALLATION-SPEC.md +46 -0
- package/rebrand/MAINTENANCE.md +93 -31
- package/rebrand/NOTICE.md +14 -0
- package/rebrand/PR-WORKFLOW.md +34 -0
- package/rebrand/UNIFICATION-SPEC.md +33 -0
- package/rebrand/VALIDATION.md +32 -0
- package/rebrand/VERSIONING.md +33 -0
- package/rebrand/apply.cjs +80 -7
- package/rebrand/brand.json +6 -2
- package/rebrand/catalog.cjs +56 -0
- package/rebrand/fork-patches.json +12 -0
- package/rebrand/overlay/.github/workflows/test-plugin-install.yml +177 -0
- package/rebrand/overlay/CHANGELOG.md +34 -0
- package/rebrand/overlay/bin/agent-targets.json +726 -0
- package/rebrand/overlay/bin/i3-composer-agent.cjs +190 -0
- package/rebrand/overlay/bin/install.cjs +164 -0
- package/rebrand/overlay/bin/skills-registry.LICENSE +21 -0
- package/rebrand/overlay/bin/wizard.cjs +66 -0
- package/rebrand/overlay/scripts/validate-brand.cjs +44 -0
- package/rebrand/overlay/tests/bundle.test.cjs +85 -0
- package/rebrand/overlay/tests/installer.test.cjs +193 -0
- package/rebrand/package.template.json +82 -0
- package/rebrand/rebrand.sh +4 -0
- package/rebrand/record-upstream.cjs +37 -0
- package/rebrand/templates/README.md +83 -137
- package/rebrand/templates/codebuddy-setup.md +55 -8
- package/rebrand/templates/codex-setup.md +14 -3
- package/rebrand/tests/pipeline.test.cjs +185 -0
- package/rebrand/upstream.json +171 -0
- package/scripts/validate-brand.cjs +11 -3
- package/skills/idea-refine/scripts/idea-refine.sh +0 -0
- package/tests/bundle.test.cjs +11 -5
- package/tests/installer.test.cjs +193 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const cli = path.join(root, 'bin/i3-composer-agent.cjs');
|
|
11
|
+
const upstream = require('../rebrand/upstream.json');
|
|
12
|
+
function workspace(t) {
|
|
13
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-agent-install-'));
|
|
14
|
+
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
15
|
+
return fs.realpathSync(directory);
|
|
16
|
+
}
|
|
17
|
+
function run(directory, args) {
|
|
18
|
+
return spawnSync(process.execPath, [cli, ...args], { cwd: directory, encoding: 'utf8' });
|
|
19
|
+
}
|
|
20
|
+
test('native CodeBuddy installation includes short commands, personas and usable references', t => {
|
|
21
|
+
const directory = workspace(t);
|
|
22
|
+
const result = run(directory, ['--codebuddy', '--scope', 'project', '--yes']);
|
|
23
|
+
assert.equal(result.status, 0, result.stderr);
|
|
24
|
+
const base = path.join(directory, '.codebuddy');
|
|
25
|
+
assert.equal(fs.readdirSync(path.join(base, 'skills')).length, upstream.skills.length);
|
|
26
|
+
assert.equal(fs.readdirSync(path.join(base, 'commands')).length, upstream.commands.length);
|
|
27
|
+
assert.equal(fs.readdirSync(path.join(base, 'agents')).length, upstream.personas.length);
|
|
28
|
+
const command = fs.readFileSync(path.join(base, 'commands/spec.md'), 'utf8');
|
|
29
|
+
assert.match(command, /Invoke the spec-driven-development skill/);
|
|
30
|
+
assert.doesNotMatch(command, /i3-composer-agent:/);
|
|
31
|
+
const ship = fs.readFileSync(path.join(base, 'commands/ship.md'), 'utf8');
|
|
32
|
+
assert.match(ship, /\.codebuddy\/agents\//);
|
|
33
|
+
assert.doesNotMatch(ship, /\.claude\/agents\//);
|
|
34
|
+
assert.ok(fs.existsSync(path.join(base, 'references/definition-of-done.md')));
|
|
35
|
+
assert.equal(fs.readFileSync(path.join(base, 'i3-composer-agent-LICENSE'), 'utf8'), fs.readFileSync(path.join(root, 'LICENSE'), 'utf8'));
|
|
36
|
+
assert.match(fs.readFileSync(path.join(base, 'i3-composer-agent-NOTICE.md'), 'utf8'), /Addy Osmani/);
|
|
37
|
+
assert.equal(fs.existsSync(path.join(directory, 'AGENTS.md')), false);
|
|
38
|
+
assert.equal(fs.existsSync(path.join(directory, 'i3-composer-agent')), false);
|
|
39
|
+
assert.match(result.stdout, /\/spec/);
|
|
40
|
+
});
|
|
41
|
+
test('multiple tools retain their own destinations and share universal skills without duplicate work', t => {
|
|
42
|
+
const directory = workspace(t);
|
|
43
|
+
const result = run(directory, ['--agents', 'codebuddy,codex,cursor,claude-code', '--scope', 'project', '--yes']);
|
|
44
|
+
assert.equal(result.status, 0, result.stderr);
|
|
45
|
+
for (const base of ['.codebuddy', '.agents', '.claude']) {
|
|
46
|
+
assert.equal(fs.readdirSync(path.join(directory, base, 'skills')).length, upstream.skills.length);
|
|
47
|
+
assert.ok(fs.existsSync(path.join(directory, base, 'references/testing-patterns.md')));
|
|
48
|
+
}
|
|
49
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(directory, '.agents/.i3-composer-agent.json')));
|
|
50
|
+
assert.deepEqual(manifest.agents.sort(), ['codex', 'cursor']);
|
|
51
|
+
assert.equal(fs.existsSync(path.join(directory, '.agents/commands')), false);
|
|
52
|
+
});
|
|
53
|
+
test('installation dry run describes scope and destinations without changing project files', t => {
|
|
54
|
+
const directory = workspace(t);
|
|
55
|
+
const result = run(directory, ['--agents', 'codebuddy,codex', '--scope', 'project', '--dry-run']);
|
|
56
|
+
assert.equal(result.status, 0, result.stderr);
|
|
57
|
+
const plan = JSON.parse(result.stdout);
|
|
58
|
+
assert.equal(plan.mode, 'install');
|
|
59
|
+
assert.equal(plan.scope, 'project');
|
|
60
|
+
assert.equal(plan.targets.length, 2);
|
|
61
|
+
assert.deepEqual(fs.readdirSync(directory), []);
|
|
62
|
+
});
|
|
63
|
+
test('all copied Markdown reference links and stable skill frontmatter remain valid', t => {
|
|
64
|
+
const directory = workspace(t);
|
|
65
|
+
assert.equal(run(directory, ['--codebuddy', '--scope', 'project', '--yes']).status, 0);
|
|
66
|
+
const base = path.join(directory, '.codebuddy');
|
|
67
|
+
function visit(folder) {
|
|
68
|
+
for (const entry of fs.readdirSync(folder, { withFileTypes: true })) {
|
|
69
|
+
const file = path.join(folder, entry.name);
|
|
70
|
+
if (entry.isDirectory()) visit(file);
|
|
71
|
+
else if (entry.name.endsWith('.md')) {
|
|
72
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
73
|
+
for (const match of content.matchAll(/\]\(([^)]+)\)/g)) {
|
|
74
|
+
const relative = match[1].split('#')[0];
|
|
75
|
+
if (relative.includes('references/') && !/^[a-z]+:/i.test(relative)) {
|
|
76
|
+
assert.ok(fs.existsSync(path.resolve(path.dirname(file), relative)), `${file}: ${relative}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
visit(path.join(base, 'skills'));
|
|
83
|
+
for (const id of upstream.skills) {
|
|
84
|
+
const head = content => content.match(/^---\r?\n[\s\S]*?\r?\n---/)[0];
|
|
85
|
+
assert.equal(head(fs.readFileSync(path.join(base, 'skills', id, 'SKILL.md'), 'utf8')), head(fs.readFileSync(path.join(root, 'skills', id, 'SKILL.md'), 'utf8')));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
test('existing assets are protected; confirmed replacement backs them up and preserves unrelated settings', t => {
|
|
89
|
+
const directory = workspace(t);
|
|
90
|
+
const base = path.join(directory, '.codebuddy');
|
|
91
|
+
fs.mkdirSync(path.join(base, 'commands'), { recursive: true });
|
|
92
|
+
fs.writeFileSync(path.join(base, 'commands/spec.md'), 'my custom command');
|
|
93
|
+
fs.writeFileSync(path.join(base, 'settings.json'), '{"keep":"my settings"}\n');
|
|
94
|
+
const args = ['--codebuddy', '--scope', 'project', '--yes'];
|
|
95
|
+
assert.equal(run(directory, args).status, 1);
|
|
96
|
+
assert.equal(fs.existsSync(path.join(base, 'skills')), false);
|
|
97
|
+
const result = run(directory, [...args, '--force']);
|
|
98
|
+
assert.equal(result.status, 0, result.stderr);
|
|
99
|
+
const backups = fs.readdirSync(base).filter(file => file.startsWith('.i3-composer-agent-backup-'));
|
|
100
|
+
assert.equal(backups.length, 1);
|
|
101
|
+
assert.equal(fs.readFileSync(path.join(base, backups[0], 'commands/spec.md'), 'utf8'), 'my custom command');
|
|
102
|
+
assert.equal(fs.readFileSync(path.join(base, 'settings.json'), 'utf8'), '{"keep":"my settings"}\n');
|
|
103
|
+
assert.equal(fs.existsSync(path.join(base, backups[0], 'settings.json')), false);
|
|
104
|
+
});
|
|
105
|
+
test('global installation uses injected home and config roots without touching the real user configuration', t => {
|
|
106
|
+
const directory = workspace(t);
|
|
107
|
+
const { planInstallation, executeInstallation } = require('../bin/install.cjs');
|
|
108
|
+
const plan = planInstallation({ agents: ['codebuddy', 'codex', 'claude-code', 'opencode'], scope: 'global', home: directory, env: { XDG_CONFIG_HOME: path.join(directory, 'config'), CLAUDE_CONFIG_DIR: path.join(directory, 'custom-claude') } });
|
|
109
|
+
assert.ok(plan.targets.every(target => target.skillsDirectory.startsWith(directory + path.sep)));
|
|
110
|
+
assert.ok(plan.targets.some(target => target.skillsDirectory === path.join(directory, 'custom-claude/skills')));
|
|
111
|
+
assert.ok(plan.targets.some(target => target.skillsDirectory === path.join(directory, 'config/opencode/skills')));
|
|
112
|
+
executeInstallation(plan);
|
|
113
|
+
for (const target of plan.targets) {
|
|
114
|
+
assert.equal(fs.readdirSync(target.skillsDirectory).length, upstream.skills.length);
|
|
115
|
+
assert.ok(fs.existsSync(path.join(target.referencesDirectory, 'definition-of-done.md')));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
test('the pinned tool catalog plans all project choices and rejects unsupported global choices before writing', t => {
|
|
119
|
+
const directory = workspace(t);
|
|
120
|
+
const { registry, planInstallation } = require('../bin/install.cjs');
|
|
121
|
+
assert.equal(registry.agents.length, 79);
|
|
122
|
+
assert.equal(registry.commit, '48dc9e8eb8aa19040e91cb90ba0b642c9cbad57e');
|
|
123
|
+
const plan = planInstallation({ agents: registry.agents.map(agent => agent.id), scope: 'project', projectDir: directory });
|
|
124
|
+
assert.equal(plan.agents.length, registry.agents.length);
|
|
125
|
+
assert.ok(plan.targets.length < plan.agents.length);
|
|
126
|
+
assert.equal(new Set(plan.assets.map(asset => asset.destination)).size, plan.assets.length);
|
|
127
|
+
for (const agent of registry.agents) {
|
|
128
|
+
if (agent.globalSkillsDir) assert.ok(planInstallation({ agents: [agent.id], scope: 'global', home: directory, env: {} }).targets[0].skillsDirectory.startsWith(directory + path.sep));
|
|
129
|
+
else assert.throws(() => planInstallation({ agents: [agent.id], scope: 'global', home: directory, env: {} }), /project installation only/);
|
|
130
|
+
}
|
|
131
|
+
assert.deepEqual(fs.readdirSync(directory), []);
|
|
132
|
+
});
|
|
133
|
+
test('failed installation restores prior assets and removes all newly written files', t => {
|
|
134
|
+
const directory = workspace(t);
|
|
135
|
+
const { planInstallation, executeInstallation } = require('../bin/install.cjs');
|
|
136
|
+
const prior = path.join(directory, '.codebuddy/skills/api-and-interface-design');
|
|
137
|
+
fs.mkdirSync(prior, { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(prior, 'custom.md'), 'prior skill content');
|
|
139
|
+
const plan = planInstallation({ agents: ['codebuddy', 'codex'], scope: 'project', projectDir: directory });
|
|
140
|
+
const realRename = fs.renameSync;
|
|
141
|
+
let failed = false;
|
|
142
|
+
fs.renameSync = (source, destination) => {
|
|
143
|
+
if (!failed && destination === plan.assets[1].destination) { failed = true; throw new Error('simulated disk failure'); }
|
|
144
|
+
return realRename(source, destination);
|
|
145
|
+
};
|
|
146
|
+
try { assert.throws(() => executeInstallation(plan, { force: true }), /simulated disk failure/); }
|
|
147
|
+
finally { fs.renameSync = realRename; }
|
|
148
|
+
assert.equal(fs.readFileSync(path.join(prior, 'custom.md'), 'utf8'), 'prior skill content');
|
|
149
|
+
assert.deepEqual(fs.readdirSync(path.dirname(prior)), ['api-and-interface-design']);
|
|
150
|
+
assert.deepEqual(fs.readdirSync(path.join(directory, '.codebuddy')), ['skills']);
|
|
151
|
+
assert.equal(fs.existsSync(path.join(directory, '.agents')), false);
|
|
152
|
+
});
|
|
153
|
+
test('replacing a skill symlink backs up the link and leaves its external source untouched', t => {
|
|
154
|
+
const directory = workspace(t);
|
|
155
|
+
const external = path.join(directory, 'external-skill');
|
|
156
|
+
const skills = path.join(directory, '.codebuddy/skills');
|
|
157
|
+
fs.mkdirSync(external);
|
|
158
|
+
fs.writeFileSync(path.join(external, 'custom.md'), 'external source');
|
|
159
|
+
fs.mkdirSync(skills, { recursive: true });
|
|
160
|
+
fs.symlinkSync(external, path.join(skills, 'spec-driven-development'), 'dir');
|
|
161
|
+
const result = run(directory, ['--codebuddy', '--scope', 'project', '--yes', '--force']);
|
|
162
|
+
assert.equal(result.status, 0, result.stderr);
|
|
163
|
+
assert.deepEqual(fs.readdirSync(external), ['custom.md']);
|
|
164
|
+
assert.equal(fs.lstatSync(path.join(skills, 'spec-driven-development')).isSymbolicLink(), false);
|
|
165
|
+
const base = path.dirname(skills);
|
|
166
|
+
const backup = fs.readdirSync(base).find(file => file.startsWith('.i3-composer-agent-backup-'));
|
|
167
|
+
assert.equal(fs.lstatSync(path.join(base, backup, 'skills/spec-driven-development')).isSymbolicLink(), true);
|
|
168
|
+
});
|
|
169
|
+
test('package destinations, shared directory symlinks and ambiguous noninteractive flags fail without writes', t => {
|
|
170
|
+
const directory = workspace(t);
|
|
171
|
+
for (const args of [ ['--codebuddy'], ['--codebuddy', '--yes'], ['--agents', 'unknown', '--scope', 'project', '--yes'], ['--codebuddy', '--scope', 'invalid', '--yes'], ['--codebuddy', '--scope', 'project', '--global', '--yes'] ]) {
|
|
172
|
+
assert.equal(run(directory, args).status, 1);
|
|
173
|
+
assert.deepEqual(fs.readdirSync(directory), []);
|
|
174
|
+
}
|
|
175
|
+
assert.equal(run(directory, ['--codebuddy', '--scope', 'project', '--yes', '--project-dir', root]).status, 1);
|
|
176
|
+
fs.mkdirSync(path.join(directory, '.codebuddy'));
|
|
177
|
+
fs.symlinkSync(path.join(root, 'skills'), path.join(directory, '.codebuddy/skills'), 'dir');
|
|
178
|
+
assert.equal(run(directory, ['--codebuddy', '--scope', 'project', '--yes', '--force']).status, 1);
|
|
179
|
+
assert.deepEqual(fs.readdirSync(path.join(directory, '.codebuddy')), ['skills']);
|
|
180
|
+
});
|
|
181
|
+
test('explicit plugin preparation remains available and does not install native host files', t => {
|
|
182
|
+
const directory = workspace(t);
|
|
183
|
+
const result = run(directory, ['--prepare', '--codebuddy']);
|
|
184
|
+
assert.equal(result.status, 0, result.stderr);
|
|
185
|
+
assert.match(result.stdout, /\/plugin install/);
|
|
186
|
+
assert.equal(fs.existsSync(path.join(directory, 'i3-composer-agent/plugin.json')), true);
|
|
187
|
+
assert.equal(fs.existsSync(path.join(directory, '.codebuddy')), false);
|
|
188
|
+
const listing = run(directory, ['--list-agents']);
|
|
189
|
+
assert.equal(listing.status, 0, listing.stderr);
|
|
190
|
+
assert.equal(listing.stdout.trim().split('\n').length, 79);
|
|
191
|
+
assert.match(listing.stdout, /codebuddy/);
|
|
192
|
+
assert.match(listing.stdout, /windsurf/);
|
|
193
|
+
});
|