kushi-agents 4.3.0 → 4.4.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/package.json +2 -4
- package/plugin/agents/kushi.agent.md +2 -2
- package/plugin/instructions/azure-auth-patterns.instructions.md +2 -2
- package/plugin/instructions/bootstrap-status-format.instructions.md +24 -0
- package/plugin/instructions/engagement-root-resolution.instructions.md +3 -3
- package/plugin/instructions/identity-resolution.instructions.md +4 -4
- package/plugin/instructions/multi-user-shared-files.instructions.md +87 -0
- package/plugin/instructions/run-reports.instructions.md +1 -1
- package/plugin/instructions/side-by-side-config.instructions.md +23 -17
- package/plugin/instructions/tracking.instructions.md +1 -1
- package/plugin/instructions/workiq-only.instructions.md +2 -2
- package/plugin/lib/Get-KushiConfig.ps1 +109 -0
- package/plugin/prompts/bootstrap.prompt.md +15 -1
- package/plugin/reference-packs/README.md +1 -1
- package/plugin/skills/ask-project/SKILL.md +1 -1
- package/plugin/skills/bootstrap-project/SKILL.md +8 -6
- package/plugin/skills/intro/SKILL.md +2 -2
- package/plugin/skills/propose-ado-update/SKILL.md +1 -1
- package/plugin/templates/init/azuredevops.template.json +159 -0
- package/plugin/templates/init/dynamics365.template.json +412 -0
- package/{.github/config/m365-mutable.json.example → plugin/templates/init/m365-mutable.example.json} +1 -1
- package/plugin/templates/init/rsi-program-catalog.template.json +107 -0
- package/src/config-loader.mjs +69 -0
- package/src/constants.mjs +54 -18
- package/src/copy-assets.mjs +0 -76
- package/src/main.mjs +30 -26
- package/src/seed-config.mjs +88 -23
- package/src/seed-config.test.mjs +150 -0
- /package/{.github/config/m365-auth.json.example → plugin/templates/init/m365-auth.example.json} +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Tests for seed-config.mjs — verifies the two-tier shared/ + user/ seeding.
|
|
2
|
+
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { seedConfig } from './seed-config.mjs';
|
|
10
|
+
import {
|
|
11
|
+
CONFIG_DOCTRINE_FILES,
|
|
12
|
+
CONFIG_EXAMPLE_FILES,
|
|
13
|
+
CONFIG_USER_SEED_FILES,
|
|
14
|
+
CONFIG_SHARED_SEED_FILES,
|
|
15
|
+
} from './constants.mjs';
|
|
16
|
+
import { loadKushiConfig, resolveKushiConfigPath } from './config-loader.mjs';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const PKG = path.resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
function tmp() {
|
|
22
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'kushi-seed-'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('seedConfig: fresh install populates shared/ and user/ correctly', () => {
|
|
26
|
+
const dest = tmp();
|
|
27
|
+
try {
|
|
28
|
+
const r = seedConfig(PKG, dest);
|
|
29
|
+
|
|
30
|
+
const userTargets = CONFIG_USER_SEED_FILES.map((f) => f.target);
|
|
31
|
+
const sharedTargets = CONFIG_SHARED_SEED_FILES.map((f) => f.target);
|
|
32
|
+
const doctrineTargets = CONFIG_DOCTRINE_FILES.map((f) => f.target);
|
|
33
|
+
const exampleTargets = CONFIG_EXAMPLE_FILES.map((f) => f.target);
|
|
34
|
+
|
|
35
|
+
for (const t of userTargets) assert.ok(r.seededUser.includes(t), `seededUser includes ${t}`);
|
|
36
|
+
for (const t of sharedTargets) assert.ok(r.seededShared.includes(t), `seededShared includes ${t}`);
|
|
37
|
+
for (const t of doctrineTargets) assert.ok(r.doctrine.includes(t), `doctrine includes ${t}`);
|
|
38
|
+
for (const t of exampleTargets) assert.ok(r.examples.includes(t), `examples includes ${t}`);
|
|
39
|
+
|
|
40
|
+
for (const t of userTargets) {
|
|
41
|
+
assert.ok(fs.existsSync(path.join(dest, 'config', 'user', t)), `user/${t} on disk`);
|
|
42
|
+
}
|
|
43
|
+
for (const t of [...sharedTargets, ...doctrineTargets, ...exampleTargets]) {
|
|
44
|
+
assert.ok(fs.existsSync(path.join(dest, 'config', 'shared', t)), `shared/${t} on disk`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
assert.equal(r.gitignore, 'created');
|
|
48
|
+
const gi = fs.readFileSync(path.join(dest, '.gitignore'), 'utf8');
|
|
49
|
+
assert.ok(gi.includes('config/user/'), '.gitignore contains config/user/');
|
|
50
|
+
} finally {
|
|
51
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('seedConfig: re-run preserves user/ and shared/ seed-once files', () => {
|
|
56
|
+
const dest = tmp();
|
|
57
|
+
try {
|
|
58
|
+
seedConfig(PKG, dest);
|
|
59
|
+
const userFile = path.join(dest, 'config', 'user', 'project-evidence.yml');
|
|
60
|
+
const sharedSeed = path.join(dest, 'config', 'shared', 'integrations.yml');
|
|
61
|
+
fs.writeFileSync(userFile, 'alias: edited-by-user\n');
|
|
62
|
+
fs.writeFileSync(sharedSeed, '# team-edited\n');
|
|
63
|
+
|
|
64
|
+
const r2 = seedConfig(PKG, dest);
|
|
65
|
+
|
|
66
|
+
assert.ok(r2.preservedUser.includes('project-evidence.yml'));
|
|
67
|
+
assert.ok(r2.preservedShared.includes('integrations.yml'));
|
|
68
|
+
assert.equal(fs.readFileSync(userFile, 'utf8'), 'alias: edited-by-user\n');
|
|
69
|
+
assert.equal(fs.readFileSync(sharedSeed, 'utf8'), '# team-edited\n');
|
|
70
|
+
assert.equal(r2.gitignore, 'unchanged');
|
|
71
|
+
} finally {
|
|
72
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('seedConfig: doctrine and examples are overwritten on every install', () => {
|
|
77
|
+
const dest = tmp();
|
|
78
|
+
try {
|
|
79
|
+
seedConfig(PKG, dest);
|
|
80
|
+
const doc = path.join(dest, 'config', 'shared', 'azuredevops.json');
|
|
81
|
+
const ex = path.join(dest, 'config', 'shared', 'm365-auth.json.example');
|
|
82
|
+
fs.writeFileSync(doc, '{"tampered":true}');
|
|
83
|
+
fs.writeFileSync(ex, '{}');
|
|
84
|
+
|
|
85
|
+
const r2 = seedConfig(PKG, dest);
|
|
86
|
+
|
|
87
|
+
assert.ok(r2.doctrine.includes('azuredevops.json'));
|
|
88
|
+
assert.ok(r2.examples.includes('m365-auth.json.example'));
|
|
89
|
+
const docRe = fs.readFileSync(doc, 'utf8');
|
|
90
|
+
const exRe = fs.readFileSync(ex, 'utf8');
|
|
91
|
+
assert.ok(!docRe.includes('tampered'), 'doctrine restored');
|
|
92
|
+
assert.ok(exRe.length > 10, 'example restored');
|
|
93
|
+
} finally {
|
|
94
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('seedConfig: .gitignore is appended when file exists without marker', () => {
|
|
99
|
+
const dest = tmp();
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(path.join(dest, '.gitignore'), '# pre-existing\nnode_modules/\n');
|
|
102
|
+
const r = seedConfig(PKG, dest);
|
|
103
|
+
assert.equal(r.gitignore, 'appended');
|
|
104
|
+
const gi = fs.readFileSync(path.join(dest, '.gitignore'), 'utf8');
|
|
105
|
+
assert.ok(gi.includes('node_modules/'), 'preserved existing content');
|
|
106
|
+
assert.ok(gi.includes('config/user/'), 'appended Kushi marker');
|
|
107
|
+
} finally {
|
|
108
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('config-loader: resolves user/ before shared/', () => {
|
|
113
|
+
const workspace = tmp();
|
|
114
|
+
const dest = path.join(workspace, '.kushi');
|
|
115
|
+
try {
|
|
116
|
+
seedConfig(PKG, dest);
|
|
117
|
+
const sharedResolved = resolveKushiConfigPath('azuredevops', { workspace });
|
|
118
|
+
assert.ok(sharedResolved.includes(path.join('config', 'shared', 'azuredevops.json')));
|
|
119
|
+
|
|
120
|
+
const userShadow = path.join(dest, 'config', 'user', 'azuredevops.json');
|
|
121
|
+
fs.writeFileSync(userShadow, '{"override":true}');
|
|
122
|
+
const userResolved = resolveKushiConfigPath('azuredevops', { workspace });
|
|
123
|
+
assert.equal(userResolved, userShadow);
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('config-loader: throws on placeholder by default; -allowPlaceholders bypasses', () => {
|
|
130
|
+
const workspace = tmp();
|
|
131
|
+
const dest = path.join(workspace, '.kushi');
|
|
132
|
+
try {
|
|
133
|
+
seedConfig(PKG, dest);
|
|
134
|
+
assert.throws(() => loadKushiConfig('project-evidence', { workspace, raw: true }),
|
|
135
|
+
/placeholder/i);
|
|
136
|
+
const txt = loadKushiConfig('project-evidence', { workspace, raw: true, allowPlaceholders: true });
|
|
137
|
+
assert.equal(typeof txt, 'string');
|
|
138
|
+
} finally {
|
|
139
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('config-loader: throws with actionable message when missing', () => {
|
|
144
|
+
const dest = tmp();
|
|
145
|
+
try {
|
|
146
|
+
assert.throws(() => loadKushiConfig('nonexistent-config', { workspace: dest }), /not found/i);
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
/package/{.github/config/m365-auth.json.example → plugin/templates/init/m365-auth.example.json}
RENAMED
|
File without changes
|