@givolt/i3-composer-agent 2.0.0 → 2.1.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.
Files changed (158) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +37 -33
  6. package/bin/agent-targets.json +11 -0
  7. package/bin/asset-fingerprint.cjs +38 -0
  8. package/bin/cleanup-backups.cjs +16 -0
  9. package/bin/i3-composer-agent.cjs +28 -12
  10. package/bin/install.cjs +74 -28
  11. package/bin/native-content.cjs +29 -0
  12. package/bin/wizard.cjs +3 -1
  13. package/docs/antigravity-setup.md +1 -1
  14. package/docs/codebuddy-setup.md +16 -3
  15. package/docs/cursor-setup.md +2 -2
  16. package/docs/getting-started.md +0 -2
  17. package/docs/opencode-setup.md +4 -4
  18. package/docs/workbuddy-setup.md +106 -0
  19. package/package.json +36 -20
  20. package/plugin.json +1 -1
  21. package/CHANGELOG.md +0 -51
  22. package/NOTICE.md +0 -15
  23. package/docs/advanced-per-agent-configuration.md +0 -70
  24. package/docs/comparison.md +0 -131
  25. package/docs/developer-onboarding.md +0 -116
  26. package/docs/skill-anatomy.md +0 -203
  27. package/evals/README.md +0 -101
  28. package/evals/cases/i3-api-and-interface-design.json +0 -45
  29. package/evals/cases/i3-browser-testing-with-devtools.json +0 -44
  30. package/evals/cases/i3-ci-cd-and-automation.json +0 -45
  31. package/evals/cases/i3-code-review-and-quality.json +0 -49
  32. package/evals/cases/i3-code-simplification.json +0 -45
  33. package/evals/cases/i3-constraint-driven-development.json +0 -79
  34. package/evals/cases/i3-context-engineering.json +0 -44
  35. package/evals/cases/i3-debugging-and-error-recovery.json +0 -58
  36. package/evals/cases/i3-deprecation-and-migration.json +0 -52
  37. package/evals/cases/i3-documentation-and-adrs.json +0 -44
  38. package/evals/cases/i3-doubt-driven-development.json +0 -43
  39. package/evals/cases/i3-frontend-ui-engineering.json +0 -52
  40. package/evals/cases/i3-git-workflow-and-versioning.json +0 -47
  41. package/evals/cases/i3-idea-refine.json +0 -43
  42. package/evals/cases/i3-incremental-implementation.json +0 -57
  43. package/evals/cases/i3-interview-me.json +0 -42
  44. package/evals/cases/i3-observability-and-instrumentation.json +0 -44
  45. package/evals/cases/i3-performance-optimization.json +0 -53
  46. package/evals/cases/i3-planning-and-task-breakdown.json +0 -45
  47. package/evals/cases/i3-security-and-hardening.json +0 -53
  48. package/evals/cases/i3-shipping-and-launch.json +0 -56
  49. package/evals/cases/i3-source-driven-development.json +0 -44
  50. package/evals/cases/i3-spec-driven-development.json +0 -72
  51. package/evals/cases/i3-test-driven-development.json +0 -73
  52. package/evals/cases/i3-using-agent-skills.json +0 -44
  53. package/evals/fixtures/i3-api-and-interface-design/service-brief.md +0 -19
  54. package/evals/fixtures/i3-browser-testing-with-devtools/README.md +0 -5
  55. package/evals/fixtures/i3-browser-testing-with-devtools/index.html +0 -24
  56. package/evals/fixtures/i3-browser-testing-with-devtools/server.js +0 -15
  57. package/evals/fixtures/i3-ci-cd-and-automation/package.json +0 -8
  58. package/evals/fixtures/i3-ci-cd-and-automation/src/slug.js +0 -3
  59. package/evals/fixtures/i3-ci-cd-and-automation/test/slug.test.js +0 -9
  60. package/evals/fixtures/i3-code-review-and-quality/user-search.diff +0 -16
  61. package/evals/fixtures/i3-code-simplification/config-parser.js +0 -46
  62. package/evals/fixtures/i3-code-simplification/config-parser.test.js +0 -15
  63. package/evals/fixtures/i3-context-engineering/context-audit.md +0 -15
  64. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.js +0 -8
  65. package/evals/fixtures/i3-debugging-and-error-recovery/pagination.test.js +0 -9
  66. package/evals/fixtures/i3-debugging-and-error-recovery/time-pressure.md +0 -6
  67. package/evals/fixtures/i3-deprecation-and-migration/api-inventory.md +0 -9
  68. package/evals/fixtures/i3-documentation-and-adrs/decision-context.md +0 -16
  69. package/evals/fixtures/i3-doubt-driven-development/migration-plan.md +0 -19
  70. package/evals/fixtures/i3-frontend-ui-engineering/Button.tsx +0 -7
  71. package/evals/fixtures/i3-frontend-ui-engineering/design-system.md +0 -11
  72. package/evals/fixtures/i3-git-workflow-and-versioning/.eval/working-tree.patch +0 -21
  73. package/evals/fixtures/i3-git-workflow-and-versioning/app.js +0 -7
  74. package/evals/fixtures/i3-git-workflow-and-versioning/app.test.js +0 -9
  75. package/evals/fixtures/i3-incremental-implementation/reports.js +0 -7
  76. package/evals/fixtures/i3-incremental-implementation/reports.test.js +0 -12
  77. package/evals/fixtures/i3-incremental-implementation/tasks/plan.md +0 -8
  78. package/evals/fixtures/i3-observability-and-instrumentation/operations.md +0 -11
  79. package/evals/fixtures/i3-observability-and-instrumentation/payment-retry.js +0 -14
  80. package/evals/fixtures/i3-performance-optimization/benchmark.js +0 -15
  81. package/evals/fixtures/i3-performance-optimization/products.js +0 -14
  82. package/evals/fixtures/i3-planning-and-task-breakdown/notifications-spec.md +0 -18
  83. package/evals/fixtures/i3-security-and-hardening/webhook.js +0 -11
  84. package/evals/fixtures/i3-security-and-hardening/webhook.test.js +0 -13
  85. package/evals/fixtures/i3-shipping-and-launch/authority-pressure.md +0 -6
  86. package/evals/fixtures/i3-shipping-and-launch/launch-status.md +0 -11
  87. package/evals/fixtures/i3-source-driven-development/framework-task.md +0 -10
  88. package/evals/fixtures/i3-spec-driven-development/billing-brief.md +0 -16
  89. package/evals/fixtures/i3-test-driven-development/BUG.md +0 -10
  90. package/evals/fixtures/i3-test-driven-development/README.md +0 -28
  91. package/evals/fixtures/i3-test-driven-development/package.json +0 -9
  92. package/evals/fixtures/i3-test-driven-development/src/split.js +0 -8
  93. package/evals/fixtures/i3-test-driven-development/test/split.test.js +0 -13
  94. package/evals/fixtures/i3-using-agent-skills/incident.md +0 -6
  95. package/evals/fixtures/incremental-implementation-pressure/draft-export.js +0 -17
  96. package/evals/fixtures/incremental-implementation-pressure/scenario.md +0 -9
  97. package/evals/fixtures/spec-driven-development-decomposition/portal-brief.md +0 -22
  98. package/evals/fixtures/test-driven-development-ecosystem/README.md +0 -9
  99. package/evals/fixtures/test-driven-development-ecosystem/ledger.py +0 -15
  100. package/evals/fixtures/test-driven-development-ecosystem/test_ledger.py +0 -19
  101. package/evals/plugin/code-review-fires/graders/off-by-one.md +0 -7
  102. package/evals/plugin/code-review-fires/graders/severity-labels.md +0 -4
  103. package/evals/plugin/code-review-fires/graders/skill-fired.md +0 -5
  104. package/evals/plugin/code-review-fires/prompt.md +0 -31
  105. package/evals/plugin/code-review-stays-quiet/graders/not-fired.md +0 -8
  106. package/evals/plugin/code-review-stays-quiet/graders/tdd-fired.md +0 -6
  107. package/evals/plugin/code-review-stays-quiet/prompt.md +0 -8
  108. package/evals/plugin/code-review-stays-quiet-on-commit-message/graders/not-fired.md +0 -8
  109. package/evals/plugin/code-review-stays-quiet-on-commit-message/prompt.md +0 -25
  110. package/evals/skill-impact.md +0 -6
  111. package/rebrand/INSTALLATION-SPEC.md +0 -47
  112. package/rebrand/MAINTENANCE.md +0 -114
  113. package/rebrand/NOTICE.md +0 -15
  114. package/rebrand/PR-WORKFLOW.md +0 -34
  115. package/rebrand/PREFIX-SPEC.md +0 -16
  116. package/rebrand/UNIFICATION-SPEC.md +0 -33
  117. package/rebrand/VALIDATION.md +0 -32
  118. package/rebrand/VERSIONING.md +0 -35
  119. package/rebrand/apply.cjs +0 -217
  120. package/rebrand/brand.json +0 -13
  121. package/rebrand/catalog.cjs +0 -70
  122. package/rebrand/fork-patches.json +0 -27
  123. package/rebrand/identifiers.cjs +0 -41
  124. package/rebrand/overlay/.github/workflows/test-plugin-install.yml +0 -177
  125. package/rebrand/overlay/CHANGELOG.md +0 -51
  126. package/rebrand/overlay/bin/agent-targets.json +0 -726
  127. package/rebrand/overlay/bin/i3-composer-agent.cjs +0 -190
  128. package/rebrand/overlay/bin/install.cjs +0 -180
  129. package/rebrand/overlay/bin/skills-registry.LICENSE +0 -21
  130. package/rebrand/overlay/bin/wizard.cjs +0 -66
  131. package/rebrand/overlay/scripts/validate-brand.cjs +0 -44
  132. package/rebrand/overlay/tests/bundle.test.cjs +0 -85
  133. package/rebrand/overlay/tests/installer.test.cjs +0 -241
  134. package/rebrand/package.template.json +0 -82
  135. package/rebrand/rebrand.sh +0 -4
  136. package/rebrand/record-upstream.cjs +0 -37
  137. package/rebrand/templates/README.md +0 -130
  138. package/rebrand/templates/codebuddy-setup.md +0 -89
  139. package/rebrand/templates/codex-setup.md +0 -45
  140. package/rebrand/tests/pipeline.test.cjs +0 -253
  141. package/rebrand/upstream.json +0 -171
  142. package/scripts/floor-guard-reference-test.js +0 -291
  143. package/scripts/lib/skill-lint-test.js +0 -650
  144. package/scripts/lib/skill-lint.js +0 -586
  145. package/scripts/run-evals-test.js +0 -623
  146. package/scripts/run-evals.js +0 -638
  147. package/scripts/validate-artifact-paths-test.js +0 -179
  148. package/scripts/validate-artifact-paths.js +0 -130
  149. package/scripts/validate-brand.cjs +0 -44
  150. package/scripts/validate-commands-test.js +0 -216
  151. package/scripts/validate-commands.js +0 -223
  152. package/scripts/validate-reference-links-test.js +0 -495
  153. package/scripts/validate-reference-links.js +0 -187
  154. package/scripts/validate-skills.js +0 -69
  155. package/scripts/validate-versions-test.js +0 -32
  156. package/scripts/validate-versions.js +0 -35
  157. package/tests/bundle.test.cjs +0 -85
  158. package/tests/installer.test.cjs +0 -241
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * validate-skills.js
4
- *
5
- * CLI that validates every skill in skills/ against the rules in
6
- * docs/skill-anatomy.md. The rules themselves live in scripts/lib/skill-lint.js
7
- * (a single source of truth, importable and unit-testable); this file is a thin
8
- * wrapper that walks the skills directory, runs the linter, prints the report,
9
- * and sets the exit code.
10
- *
11
- * Exit codes: 0 = all clear, 1 = one or more errors
12
- */
13
-
14
- 'use strict';
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
-
19
- const { lintSkill } = require('./lib/skill-lint');
20
-
21
- const SKILLS_DIR = path.resolve(__dirname, '..', 'skills');
22
-
23
- // ─── Main ────────────────────────────────────────────────────────────────────
24
-
25
- function main() {
26
- if (!fs.existsSync(SKILLS_DIR)) {
27
- console.error(`ERROR: skills directory not found at ${SKILLS_DIR}`);
28
- process.exit(1);
29
- }
30
-
31
- const skillDirs = fs.readdirSync(SKILLS_DIR)
32
- .filter(d => fs.statSync(path.join(SKILLS_DIR, d)).isDirectory())
33
- .sort();
34
-
35
- const knownSkills = new Set(skillDirs);
36
-
37
- let totalErrors = 0;
38
- let totalWarnings = 0;
39
-
40
- for (const dirName of skillDirs) {
41
- const { errors, warnings, exempt } = lintSkill(dirName, SKILLS_DIR, knownSkills);
42
- totalErrors += errors.length;
43
- totalWarnings += warnings.length;
44
-
45
- if (errors.length === 0 && warnings.length === 0) {
46
- const tag = exempt ? ' (section checks exempt)' : '';
47
- console.log(` ✓ ${dirName}${tag}`);
48
- } else {
49
- const icon = errors.length > 0 ? ' ✗ ' : ' ⚠ ';
50
- console.log(`${icon} ${dirName}`);
51
- for (const msg of errors) console.log(` ERROR: ${msg}`);
52
- for (const msg of warnings) console.log(` WARN: ${msg}`);
53
- }
54
- }
55
-
56
- const status = totalErrors > 0 ? 'FAILED' : totalWarnings > 0 ? 'PASSED WITH WARNINGS' : 'PASSED';
57
- console.log(`\n${skillDirs.length} skills checked — ${totalErrors} error(s), ${totalWarnings} warning(s) — ${status}`);
58
-
59
- if (totalErrors > 0) process.exit(1);
60
- }
61
-
62
- // Surface unexpected failures (fs errors, bad symlinks, …) as a structured
63
- // one-line CI error instead of an uncaught stack trace.
64
- try {
65
- main();
66
- } catch (err) {
67
- console.error(`\nERROR: validate-skills failed unexpectedly: ${err.message}`);
68
- process.exit(1);
69
- }
@@ -1,32 +0,0 @@
1
- "use strict";
2
-
3
- const assert = require("node:assert/strict");
4
- const { readFileSync } = require("node:fs");
5
- const test = require("node:test");
6
-
7
- const manifestPaths = [
8
- "package.json",
9
- "plugin.json",
10
- ".codex-plugin/plugin.json",
11
- ".claude-plugin/plugin.json",
12
- ".claude-plugin/marketplace.json",
13
- ".agents/plugins/marketplace.json",
14
- ];
15
-
16
- function readManifestVersion(manifestPath) {
17
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
18
- return manifest.version ?? manifest.plugins?.[0]?.version;
19
- }
20
-
21
- test("all plugin manifests use the root plugin.json version", () => {
22
- const expectedVersion = readManifestVersion("plugin.json");
23
- assert.ok(expectedVersion, "plugin.json must define a version");
24
-
25
- for (const manifestPath of manifestPaths) {
26
- assert.equal(
27
- readManifestVersion(manifestPath),
28
- expectedVersion,
29
- `${manifestPath} must use version ${expectedVersion}`,
30
- );
31
- }
32
- });
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- "use strict";
4
-
5
- const { readFileSync } = require("node:fs");
6
-
7
- const manifestPaths = [
8
- "package.json",
9
- "plugin.json",
10
- ".codex-plugin/plugin.json",
11
- ".claude-plugin/plugin.json",
12
- ".claude-plugin/marketplace.json",
13
- ".agents/plugins/marketplace.json",
14
- ];
15
-
16
- function readManifestVersion(manifestPath) {
17
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
18
- return manifest.version ?? manifest.plugins?.[0]?.version;
19
- }
20
-
21
- const expectedVersion = readManifestVersion("plugin.json");
22
- if (!expectedVersion) {
23
- throw new Error("plugin.json is missing a version field");
24
- }
25
-
26
- for (const manifestPath of manifestPaths) {
27
- const version = readManifestVersion(manifestPath);
28
- if (version !== expectedVersion) {
29
- throw new Error(
30
- `${manifestPath} has version ${version ?? "<missing>"}; expected ${expectedVersion}`,
31
- );
32
- }
33
- }
34
-
35
- console.log(`All plugin manifests use version ${expectedVersion}.`);
@@ -1,85 +0,0 @@
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 upstream = require('../rebrand/upstream.json');
11
- const cli = path.join(root, 'bin/i3-composer-agent.cjs');
12
- function run(args) { return spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' }); }
13
- function workspace(t) {
14
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'i3-agent-bundle-'));
15
- t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
16
- return directory;
17
- }
18
- test('help and version identify the package without preparing files', () => {
19
- const help = run(['--help']).stdout;
20
- assert.match(help, /@givolt\/i3-composer-agent/);
21
- assert.ok(help.includes(`i3 Composer Agent v${require('../package.json').version}`));
22
- assert.ok(help.includes(`Based on agent-skills v${upstream.upstreamVersion}`));
23
- assert.equal(run(['--version']).stdout.trim(), require('../package.json').version);
24
- });
25
- test('dry run and invalid arguments leave the target untouched', t => {
26
- const target = path.join(workspace(t), 'plugin');
27
- assert.equal(run(['--codex', '--dir', target, '--dry-run']).status, 0);
28
- assert.equal(fs.existsSync(target), false);
29
- assert.equal(run(['--codebuddy', '--codex', '--dir', target]).status, 1);
30
- assert.equal(run(['--extract', '--dir']).status, 1);
31
- assert.equal(fs.existsSync(target), false);
32
- });
33
- test('complete bundle preserves reference links and all host assets at a path with spaces', t => {
34
- const target = path.join(workspace(t), 'plugin with spaces');
35
- const result = run(['--codebuddy', '--dir', target]);
36
- assert.equal(result.status, 0, result.stderr);
37
- assert.ok(result.stdout.includes(`i3 Composer Agent v${require('../package.json').version}`));
38
- assert.ok(result.stdout.includes(`Based on agent-skills v${upstream.upstreamVersion}`));
39
- assert.match(result.stdout, /\/plugin install i3-composer-agent@i3-composer-agent/);
40
- assert.equal(fs.readdirSync(path.join(target, 'skills')).length, upstream.skills.length);
41
- assert.equal(fs.readdirSync(path.join(target, '.claude/commands')).length, upstream.commands.length);
42
- assert.equal(fs.readdirSync(path.join(target, 'agents')).filter(name => name.endsWith('.md')).length, upstream.personas.length);
43
- assert.equal(JSON.parse(fs.readFileSync(path.join(target, '.codex-plugin/plugin.json'), 'utf8')).name, 'i3-composer-agent');
44
- const check = spawnSync(process.execPath, [path.join(target, 'scripts/validate-reference-links.js')], { cwd: target, encoding: 'utf8' });
45
- assert.equal(check.status, 0, check.stdout + check.stderr);
46
- assert.equal(fs.existsSync(path.join(target, 'AGENTS.md')), false);
47
- assert.equal(fs.existsSync(path.join(target, 'CLAUDE.md')), false);
48
- });
49
- test('existing unrelated files cannot be replaced even with force', t => {
50
- const target = path.join(workspace(t), 'existing');
51
- fs.mkdirSync(target);
52
- fs.writeFileSync(path.join(target, 'plugin.json'), '{"name":"another-plugin"}\n');
53
- fs.writeFileSync(path.join(target, 'keep.txt'), 'business data');
54
- assert.equal(run(['--extract', '--dir', target]).status, 1);
55
- assert.equal(run(['--extract', '--dir', target, '--force']).status, 1);
56
- assert.equal(fs.readFileSync(path.join(target, 'keep.txt'), 'utf8'), 'business data');
57
- });
58
- test('installed packages beneath node_modules remain fully extractable', t => {
59
- const parent = workspace(t);
60
- const installed = path.join(parent, 'node_modules', '@givolt', 'i3-composer-agent');
61
- assert.equal(run(['--extract', '--dir', installed]).status, 0);
62
- const target = path.join(parent, 'plugin');
63
- const result = spawnSync(process.execPath, [path.join(installed, 'bin/i3-composer-agent.cjs'), '--extract', '--dir', target], { encoding: 'utf8' });
64
- assert.equal(result.status, 0, result.stderr);
65
- assert.equal(JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8')).name, '@givolt/i3-composer-agent');
66
- assert.equal(fs.readdirSync(path.join(target, 'skills')).length, upstream.skills.length);
67
- });
68
- test('replacement preserves the complete previous bundle in a backup', t => {
69
- const parent = workspace(t);
70
- const target = path.join(parent, 'plugin');
71
- assert.equal(run(['--extract', '--dir', target]).status, 0);
72
- fs.writeFileSync(path.join(target, 'custom-note.txt'), 'preserve me');
73
- const result = run(['--extract', '--dir', target, '--force']);
74
- assert.equal(result.status, 0, result.stderr);
75
- const backups = fs.readdirSync(parent).filter(name => name.startsWith('plugin.backup-'));
76
- assert.equal(backups.length, 1);
77
- assert.equal(fs.readFileSync(path.join(parent, backups[0], 'custom-note.txt'), 'utf8'), 'preserve me');
78
- assert.equal(fs.existsSync(path.join(target, 'custom-note.txt')), false);
79
- });
80
- test('source directories and aliases cannot be overwritten', t => {
81
- assert.equal(run(['--extract', '--dir', root, '--force']).status, 1);
82
- const alias = path.join(workspace(t), 'source-alias');
83
- fs.symlinkSync(root, alias, 'dir');
84
- assert.equal(run(['--extract', '--dir', alias, '--force']).status, 1);
85
- });
@@ -1,241 +0,0 @@
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/i3-spec.md'), 'utf8');
29
- assert.match(command, /Invoke the i3-spec-driven-development skill/);
30
- assert.doesNotMatch(command, /i3-composer-agent:/);
31
- const ship = fs.readFileSync(path.join(base, 'commands/i3-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, /\/i3-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.map(id => `i3-${id}`)) {
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
- function legacyInstallation(directory) {
89
- const base = path.join(directory, '.codebuddy');
90
- fs.mkdirSync(path.join(base, 'commands'), { recursive: true });
91
- fs.mkdirSync(path.join(base, 'skills/spec-driven-development'), { recursive: true });
92
- fs.writeFileSync(path.join(base, 'commands/spec.md'), 'original owned command');
93
- fs.writeFileSync(path.join(base, 'skills/spec-driven-development/SKILL.md'), 'original owned Skill');
94
- fs.writeFileSync(path.join(base, 'commands/build.md'), 'unowned custom command');
95
- fs.writeFileSync(path.join(base, '.i3-composer-agent.json'), JSON.stringify({ name: 'i3-composer-agent', version: '1.0.1', assets: ['commands/spec.md', 'skills/spec-driven-development'] }));
96
- return base;
97
- }
98
- test('prefix migration retires only owned legacy paths and preserves their backups', t => {
99
- const directory = workspace(t);
100
- const base = legacyInstallation(directory);
101
- const args = ['--codebuddy', '--scope', 'project', '--yes'];
102
- assert.equal(run(directory, args).status, 1);
103
- assert.equal(fs.readFileSync(path.join(base, 'commands/spec.md'), 'utf8'), 'original owned command');
104
- const result = run(directory, [...args, '--force']);
105
- assert.equal(result.status, 0, result.stderr);
106
- assert.equal(fs.existsSync(path.join(base, 'commands/spec.md')), false);
107
- assert.equal(fs.existsSync(path.join(base, 'skills/spec-driven-development')), false);
108
- assert.ok(fs.existsSync(path.join(base, 'commands/i3-spec.md')));
109
- assert.match(fs.readFileSync(path.join(base, 'skills/i3-spec-driven-development/SKILL.md'), 'utf8'), /^name: i3-spec-driven-development$/m);
110
- assert.equal(fs.readFileSync(path.join(base, 'commands/build.md'), 'utf8'), 'unowned custom command');
111
- const backup = fs.readdirSync(base).find(file => file.startsWith('.i3-composer-agent-backup-'));
112
- assert.equal(fs.readFileSync(path.join(base, backup, 'commands/spec.md'), 'utf8'), 'original owned command');
113
- assert.equal(fs.readFileSync(path.join(base, backup, 'skills/spec-driven-development/SKILL.md'), 'utf8'), 'original owned Skill');
114
- const manifest = JSON.parse(fs.readFileSync(path.join(base, '.i3-composer-agent.json'), 'utf8'));
115
- assert.equal(manifest.entryPrefix, 'i3-');
116
- assert.ok(!manifest.assets.includes('commands/spec.md'));
117
- });
118
- test('a failure after retiring legacy paths rolls the migration back', t => {
119
- const directory = workspace(t);
120
- const base = legacyInstallation(directory);
121
- const before = fs.readFileSync(path.join(base, '.i3-composer-agent.json'), 'utf8');
122
- const { planInstallation, executeInstallation } = require('../bin/install.cjs');
123
- const plan = planInstallation({ agents: ['codebuddy'], scope: 'project', projectDir: directory });
124
- const realRename = fs.renameSync;
125
- fs.renameSync = (source, destination) => {
126
- if (destination === path.join(base, '.i3-composer-agent.json') && source.includes('-stage-')) throw new Error('simulated manifest failure');
127
- return realRename(source, destination);
128
- };
129
- try { assert.throws(() => executeInstallation(plan, { force: true }), /simulated manifest failure/); }
130
- finally { fs.renameSync = realRename; }
131
- assert.equal(fs.readFileSync(path.join(base, '.i3-composer-agent.json'), 'utf8'), before);
132
- assert.equal(fs.readFileSync(path.join(base, 'commands/spec.md'), 'utf8'), 'original owned command');
133
- assert.equal(fs.readFileSync(path.join(base, 'skills/spec-driven-development/SKILL.md'), 'utf8'), 'original owned Skill');
134
- assert.equal(fs.existsSync(path.join(base, 'commands/i3-spec.md')), false);
135
- });
136
- test('existing assets are protected; confirmed replacement backs them up and preserves unrelated settings', t => {
137
- const directory = workspace(t);
138
- const base = path.join(directory, '.codebuddy');
139
- fs.mkdirSync(path.join(base, 'commands'), { recursive: true });
140
- fs.writeFileSync(path.join(base, 'commands/i3-spec.md'), 'my custom command');
141
- fs.writeFileSync(path.join(base, 'settings.json'), '{"keep":"my settings"}\n');
142
- const args = ['--codebuddy', '--scope', 'project', '--yes'];
143
- assert.equal(run(directory, args).status, 1);
144
- assert.equal(fs.existsSync(path.join(base, 'skills')), false);
145
- const result = run(directory, [...args, '--force']);
146
- assert.equal(result.status, 0, result.stderr);
147
- const backups = fs.readdirSync(base).filter(file => file.startsWith('.i3-composer-agent-backup-'));
148
- assert.equal(backups.length, 1);
149
- assert.equal(fs.readFileSync(path.join(base, backups[0], 'commands/i3-spec.md'), 'utf8'), 'my custom command');
150
- assert.equal(fs.readFileSync(path.join(base, 'settings.json'), 'utf8'), '{"keep":"my settings"}\n');
151
- assert.equal(fs.existsSync(path.join(base, backups[0], 'settings.json')), false);
152
- });
153
- test('global installation uses injected home and config roots without touching the real user configuration', t => {
154
- const directory = workspace(t);
155
- const { planInstallation, executeInstallation } = require('../bin/install.cjs');
156
- 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') } });
157
- assert.ok(plan.targets.every(target => target.skillsDirectory.startsWith(directory + path.sep)));
158
- assert.ok(plan.targets.some(target => target.skillsDirectory === path.join(directory, 'custom-claude/skills')));
159
- assert.ok(plan.targets.some(target => target.skillsDirectory === path.join(directory, 'config/opencode/skills')));
160
- executeInstallation(plan);
161
- for (const target of plan.targets) {
162
- assert.equal(fs.readdirSync(target.skillsDirectory).length, upstream.skills.length);
163
- assert.ok(fs.existsSync(path.join(target.referencesDirectory, 'definition-of-done.md')));
164
- }
165
- });
166
- test('the pinned tool catalog plans all project choices and rejects unsupported global choices before writing', t => {
167
- const directory = workspace(t);
168
- const { registry, planInstallation } = require('../bin/install.cjs');
169
- assert.equal(registry.agents.length, 79);
170
- assert.equal(registry.commit, '48dc9e8eb8aa19040e91cb90ba0b642c9cbad57e');
171
- const plan = planInstallation({ agents: registry.agents.map(agent => agent.id), scope: 'project', projectDir: directory });
172
- assert.equal(plan.agents.length, registry.agents.length);
173
- assert.ok(plan.targets.length < plan.agents.length);
174
- assert.equal(new Set(plan.assets.map(asset => asset.destination)).size, plan.assets.length);
175
- for (const agent of registry.agents) {
176
- if (agent.globalSkillsDir) assert.ok(planInstallation({ agents: [agent.id], scope: 'global', home: directory, env: {} }).targets[0].skillsDirectory.startsWith(directory + path.sep));
177
- else assert.throws(() => planInstallation({ agents: [agent.id], scope: 'global', home: directory, env: {} }), /project installation only/);
178
- }
179
- assert.deepEqual(fs.readdirSync(directory), []);
180
- });
181
- test('failed installation restores prior assets and removes all newly written files', t => {
182
- const directory = workspace(t);
183
- const { planInstallation, executeInstallation } = require('../bin/install.cjs');
184
- const prior = path.join(directory, '.codebuddy/skills/i3-api-and-interface-design');
185
- fs.mkdirSync(prior, { recursive: true });
186
- fs.writeFileSync(path.join(prior, 'custom.md'), 'prior skill content');
187
- const plan = planInstallation({ agents: ['codebuddy', 'codex'], scope: 'project', projectDir: directory });
188
- const realRename = fs.renameSync;
189
- let failed = false;
190
- fs.renameSync = (source, destination) => {
191
- if (!failed && destination === plan.assets[1].destination) { failed = true; throw new Error('simulated disk failure'); }
192
- return realRename(source, destination);
193
- };
194
- try { assert.throws(() => executeInstallation(plan, { force: true }), /simulated disk failure/); }
195
- finally { fs.renameSync = realRename; }
196
- assert.equal(fs.readFileSync(path.join(prior, 'custom.md'), 'utf8'), 'prior skill content');
197
- assert.deepEqual(fs.readdirSync(path.dirname(prior)), ['i3-api-and-interface-design']);
198
- assert.deepEqual(fs.readdirSync(path.join(directory, '.codebuddy')), ['skills']);
199
- assert.equal(fs.existsSync(path.join(directory, '.agents')), false);
200
- });
201
- test('replacing a skill symlink backs up the link and leaves its external source untouched', t => {
202
- const directory = workspace(t);
203
- const external = path.join(directory, 'external-skill');
204
- const skills = path.join(directory, '.codebuddy/skills');
205
- fs.mkdirSync(external);
206
- fs.writeFileSync(path.join(external, 'custom.md'), 'external source');
207
- fs.mkdirSync(skills, { recursive: true });
208
- fs.symlinkSync(external, path.join(skills, 'i3-spec-driven-development'), 'dir');
209
- const result = run(directory, ['--codebuddy', '--scope', 'project', '--yes', '--force']);
210
- assert.equal(result.status, 0, result.stderr);
211
- assert.deepEqual(fs.readdirSync(external), ['custom.md']);
212
- assert.equal(fs.lstatSync(path.join(skills, 'i3-spec-driven-development')).isSymbolicLink(), false);
213
- const base = path.dirname(skills);
214
- const backup = fs.readdirSync(base).find(file => file.startsWith('.i3-composer-agent-backup-'));
215
- assert.equal(fs.lstatSync(path.join(base, backup, 'skills/i3-spec-driven-development')).isSymbolicLink(), true);
216
- });
217
- test('package destinations, shared directory symlinks and ambiguous noninteractive flags fail without writes', t => {
218
- const directory = workspace(t);
219
- for (const args of [ ['--codebuddy'], ['--codebuddy', '--yes'], ['--agents', 'unknown', '--scope', 'project', '--yes'], ['--codebuddy', '--scope', 'invalid', '--yes'], ['--codebuddy', '--scope', 'project', '--global', '--yes'] ]) {
220
- assert.equal(run(directory, args).status, 1);
221
- assert.deepEqual(fs.readdirSync(directory), []);
222
- }
223
- assert.equal(run(directory, ['--codebuddy', '--scope', 'project', '--yes', '--project-dir', root]).status, 1);
224
- fs.mkdirSync(path.join(directory, '.codebuddy'));
225
- fs.symlinkSync(path.join(root, 'skills'), path.join(directory, '.codebuddy/skills'), 'dir');
226
- assert.equal(run(directory, ['--codebuddy', '--scope', 'project', '--yes', '--force']).status, 1);
227
- assert.deepEqual(fs.readdirSync(path.join(directory, '.codebuddy')), ['skills']);
228
- });
229
- test('explicit plugin preparation remains available and does not install native host files', t => {
230
- const directory = workspace(t);
231
- const result = run(directory, ['--prepare', '--codebuddy']);
232
- assert.equal(result.status, 0, result.stderr);
233
- assert.match(result.stdout, /\/plugin install/);
234
- assert.equal(fs.existsSync(path.join(directory, 'i3-composer-agent/plugin.json')), true);
235
- assert.equal(fs.existsSync(path.join(directory, '.codebuddy')), false);
236
- const listing = run(directory, ['--list-agents']);
237
- assert.equal(listing.status, 0, listing.stderr);
238
- assert.equal(listing.stdout.trim().split('\n').length, 79);
239
- assert.match(listing.stdout, /codebuddy/);
240
- assert.match(listing.stdout, /windsurf/);
241
- });