kushi-agents 4.2.3 → 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/.github/copilot-instructions.kushi.md +4 -2
- package/package.json +2 -4
- package/plugin/agents/kushi.agent.md +7 -4
- 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/aggregate.prompt.md +15 -4
- package/plugin/prompts/apply-ado.prompt.md +14 -5
- package/plugin/prompts/ask.prompt.md +12 -2
- package/plugin/prompts/bootstrap.prompt.md +27 -3
- package/plugin/prompts/consolidate.prompt.md +12 -2
- package/plugin/prompts/fde-intake.prompt.md +14 -5
- package/plugin/prompts/fde-report.prompt.md +15 -5
- package/plugin/prompts/fde-triage.prompt.md +14 -5
- package/plugin/prompts/propose-ado.prompt.md +14 -5
- package/plugin/prompts/refresh.prompt.md +12 -2
- package/plugin/prompts/state.prompt.md +11 -2
- package/plugin/prompts/status.prompt.md +11 -2
- 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
package/src/constants.mjs
CHANGED
|
@@ -20,21 +20,69 @@ export const CLAWPILOT_SKILL_DEST = 'SKILL.md';
|
|
|
20
20
|
export const PLUGIN_SOURCE_DIR = 'plugin';
|
|
21
21
|
|
|
22
22
|
/** Asset subdirectories under plugin/ that get copied. */
|
|
23
|
-
export const ASSET_DIRS = ['agents', 'instructions', 'prompts', 'skills', 'templates', 'reference-packs'];
|
|
23
|
+
export const ASSET_DIRS = ['agents', 'instructions', 'prompts', 'skills', 'templates', 'reference-packs', 'lib'];
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* User-editable config directory under the install destination.
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* v4.4.0 introduces a two-tier split inside config/:
|
|
29
|
+
* - shared/ → team-owned, safe to commit. Doctrine + team defaults.
|
|
30
|
+
* - user/ → per-contributor identity & local paths. Gitignored.
|
|
31
|
+
*
|
|
32
|
+
* Skills read configs via the Get-KushiConfig helper which checks
|
|
33
|
+
* user/ first, then shared/.
|
|
28
34
|
*/
|
|
29
35
|
export const CONFIG_DIR = 'config';
|
|
36
|
+
export const CONFIG_SHARED_SUBDIR = 'shared';
|
|
37
|
+
export const CONFIG_USER_SUBDIR = 'user';
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
40
|
+
* Per-contributor config files (USER tier).
|
|
41
|
+
* Seeded only if absent; preserved on reinstall/upgrade.
|
|
42
|
+
* Written to <dest>/config/user/. Gitignored by installer.
|
|
34
43
|
*/
|
|
35
|
-
export const
|
|
44
|
+
export const CONFIG_USER_SEED_FILES = [
|
|
36
45
|
{ template: 'init/project-evidence.template.yml', target: 'project-evidence.yml' },
|
|
37
|
-
{ template: 'init/
|
|
46
|
+
{ template: 'init/m365-auth.template.json', target: 'm365-auth.json' },
|
|
47
|
+
{ template: 'init/m365-mutable.template.json', target: 'm365-mutable.json' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Team-shared config files (SHARED tier, seed-once).
|
|
52
|
+
* Seeded only if absent; preserved on reinstall.
|
|
53
|
+
* Written to <dest>/config/shared/. Safe to commit.
|
|
54
|
+
*/
|
|
55
|
+
export const CONFIG_SHARED_SEED_FILES = [
|
|
56
|
+
{ template: 'init/integrations.template.yml', target: 'integrations.yml' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Doctrine config files (SHARED tier, overwrite-always).
|
|
61
|
+
* Versioned by Kushi; overwritten on every install. User hand-edits are lost.
|
|
62
|
+
* Written to <dest>/config/shared/.
|
|
63
|
+
*/
|
|
64
|
+
export const CONFIG_DOCTRINE_FILES = [
|
|
65
|
+
{ template: 'init/azuredevops.template.json', target: 'azuredevops.json' },
|
|
66
|
+
{ template: 'init/dynamics365.template.json', target: 'dynamics365.json' },
|
|
67
|
+
{ template: 'init/rsi-program-catalog.template.json', target: 'rsi-program-catalog.json' },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Example/reference scaffolds (SHARED tier, overwrite-always).
|
|
72
|
+
* Written to <dest>/config/shared/.
|
|
73
|
+
*/
|
|
74
|
+
export const CONFIG_EXAMPLE_FILES = [
|
|
75
|
+
{ template: 'init/m365-auth.example.json', target: 'm365-auth.json.example' },
|
|
76
|
+
{ template: 'init/m365-mutable.example.json', target: 'm365-mutable.json.example' },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Lines written to <dest>/.gitignore (created if missing, appended otherwise).
|
|
81
|
+
* Marks the per-user tier so contributors don't accidentally commit identity/paths.
|
|
82
|
+
*/
|
|
83
|
+
export const CONFIG_GITIGNORE_LINES = [
|
|
84
|
+
'# Kushi per-user config — never commit (created by kushi-agents installer)',
|
|
85
|
+
'config/user/',
|
|
38
86
|
];
|
|
39
87
|
|
|
40
88
|
/** Files to exclude from consumer installs (relative to plugin/). */
|
|
@@ -43,18 +91,6 @@ export const EXCLUDED_FILES = [];
|
|
|
43
91
|
/** Directories to exclude from consumer installs (relative to plugin/). */
|
|
44
92
|
export const EXCLUDED_DIRS = [];
|
|
45
93
|
|
|
46
|
-
/** Files copied to the TARGET project's .github/ directory. */
|
|
47
|
-
export const PROJECT_GITHUB_FILES = [];
|
|
48
|
-
|
|
49
|
-
/** Directories copied to the TARGET project's .github/ directory. */
|
|
50
|
-
export const PROJECT_GITHUB_DIRS = ['config'];
|
|
51
|
-
|
|
52
|
-
/** Project-level .github files that must never be copied into consumer projects. */
|
|
53
|
-
export const PROJECT_GITHUB_EXCLUDED_FILES = [
|
|
54
|
-
'config/m365-auth.json',
|
|
55
|
-
'config/m365-mutable.json'
|
|
56
|
-
];
|
|
57
|
-
|
|
58
94
|
/**
|
|
59
95
|
* Files / directories that indicate the cwd is a sane install target.
|
|
60
96
|
*
|
package/src/copy-assets.mjs
CHANGED
|
@@ -5,9 +5,6 @@ import {
|
|
|
5
5
|
EXCLUDED_FILES,
|
|
6
6
|
EXCLUDED_DIRS,
|
|
7
7
|
PLUGIN_SOURCE_DIR,
|
|
8
|
-
PROJECT_GITHUB_FILES,
|
|
9
|
-
PROJECT_GITHUB_DIRS,
|
|
10
|
-
PROJECT_GITHUB_EXCLUDED_FILES,
|
|
11
8
|
} from './constants.mjs';
|
|
12
9
|
|
|
13
10
|
/**
|
|
@@ -97,31 +94,6 @@ function copyDirFiltered(src, dest, assetDir, relPrefix = '', includeFilter) {
|
|
|
97
94
|
* @param {string} projectDir
|
|
98
95
|
* @param {string} [relPrefix='']
|
|
99
96
|
*/
|
|
100
|
-
function copyProjectDirFiltered(src, dest, projectDir, relPrefix = '') {
|
|
101
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
102
|
-
|
|
103
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
104
|
-
const srcPath = path.join(src, entry.name);
|
|
105
|
-
const destPath = path.join(dest, entry.name);
|
|
106
|
-
const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
107
|
-
const excludeKey = `${projectDir}/${relPath}`;
|
|
108
|
-
|
|
109
|
-
if (entry.isDirectory()) {
|
|
110
|
-
copyProjectDirFiltered(srcPath, destPath, projectDir, relPrefix ? `${relPrefix}/${entry.name}` : entry.name);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (PROJECT_GITHUB_EXCLUDED_FILES.includes(excludeKey)) continue;
|
|
115
|
-
|
|
116
|
-
fs.cpSync(srcPath, destPath, { force: true });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Count files recursively in a directory.
|
|
122
|
-
* @param {string} dir
|
|
123
|
-
* @returns {number}
|
|
124
|
-
*/
|
|
125
97
|
function countFiles(dir) {
|
|
126
98
|
let count = 0;
|
|
127
99
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -133,51 +105,3 @@ function countFiles(dir) {
|
|
|
133
105
|
}
|
|
134
106
|
return count;
|
|
135
107
|
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Copy project-level files (hooks, copilot-instructions) to the target
|
|
139
|
-
* project's .github/ directory. Does not overwrite existing files.
|
|
140
|
-
*
|
|
141
|
-
* @param {string} sourcePkgDir – root of the npm package
|
|
142
|
-
* @returns {{ copied: string[], skipped: string[] }}
|
|
143
|
-
*/
|
|
144
|
-
export function copyProjectFiles(sourcePkgDir) {
|
|
145
|
-
const projectGitHub = path.join(process.cwd(), '.github');
|
|
146
|
-
const srcGitHub = path.join(sourcePkgDir, '.github');
|
|
147
|
-
|
|
148
|
-
const copied = [];
|
|
149
|
-
const skipped = [];
|
|
150
|
-
|
|
151
|
-
for (const file of PROJECT_GITHUB_FILES) {
|
|
152
|
-
const src = path.join(srcGitHub, file);
|
|
153
|
-
const dest = path.join(projectGitHub, file);
|
|
154
|
-
|
|
155
|
-
if (!fs.existsSync(src)) continue;
|
|
156
|
-
|
|
157
|
-
if (fs.existsSync(dest)) {
|
|
158
|
-
skipped.push(`.github/${file}`);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
163
|
-
fs.cpSync(src, dest);
|
|
164
|
-
copied.push(`.github/${file}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
for (const dir of PROJECT_GITHUB_DIRS) {
|
|
168
|
-
const src = path.join(srcGitHub, dir);
|
|
169
|
-
const dest = path.join(projectGitHub, dir);
|
|
170
|
-
|
|
171
|
-
if (!fs.existsSync(src)) continue;
|
|
172
|
-
|
|
173
|
-
if (fs.existsSync(dest)) {
|
|
174
|
-
skipped.push(`.github/${dir}/`);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
copyProjectDirFiltered(src, dest, dir);
|
|
179
|
-
copied.push(`.github/${dir}/`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return { copied, skipped };
|
|
183
|
-
}
|
package/src/main.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
PROJECT_MARKERS,
|
|
14
14
|
} from './constants.mjs';
|
|
15
15
|
import { promptForDestination } from './prompt.mjs';
|
|
16
|
-
import { copyAssets
|
|
16
|
+
import { copyAssets } from './copy-assets.mjs';
|
|
17
17
|
import { mergeSettings } from './settings.mjs';
|
|
18
18
|
import { mergeCopilotInstructions } from './copilot-instructions.mjs';
|
|
19
19
|
import { seedConfig } from './seed-config.mjs';
|
|
@@ -134,12 +134,8 @@ async function installVscode(options, resolved, version) {
|
|
|
134
134
|
const manifestPath = writeInstalledManifest(fullDest, resolved, version);
|
|
135
135
|
console.log(` - kushi-install.json (profile manifest)`);
|
|
136
136
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
console.log(`\n Config (.kushi/config/ — user-editable, preserved across upgrades):`);
|
|
140
|
-
for (const f of seeded) console.log(` - ${f} -> seeded from template`);
|
|
141
|
-
for (const f of preserved) console.log(` - ${f} -> preserved (already exists)`);
|
|
142
|
-
}
|
|
137
|
+
const seedRes = seedConfig(PKG_ROOT, fullDest);
|
|
138
|
+
printSeedReport(seedRes, `${dest}/`);
|
|
143
139
|
|
|
144
140
|
if (!options.noSettings) {
|
|
145
141
|
const { created, keysAdded, keysUnchanged } = mergeSettings(
|
|
@@ -176,19 +172,6 @@ async function installVscode(options, resolved, version) {
|
|
|
176
172
|
console.log('\n Skipped .github/copilot-instructions.md (--no-instructions)');
|
|
177
173
|
}
|
|
178
174
|
|
|
179
|
-
const { copied: projCopied, skipped: projSkipped } = copyProjectFiles(PKG_ROOT);
|
|
180
|
-
if (projCopied.length > 0) {
|
|
181
|
-
console.log('\n Copied project files to .github/');
|
|
182
|
-
for (const f of projCopied) {
|
|
183
|
-
console.log(` - ${f}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (projSkipped.length > 0) {
|
|
187
|
-
for (const f of projSkipped) {
|
|
188
|
-
console.log(` - ${f} -> unchanged (already exists)`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
175
|
printPostInstall(resolved, '@Kushi');
|
|
193
176
|
}
|
|
194
177
|
|
|
@@ -233,12 +216,8 @@ async function installClawpilot(options, resolved, version) {
|
|
|
233
216
|
writeInstalledManifest(fullDest, resolved, version);
|
|
234
217
|
console.log(` - kushi-install.json (profile manifest)`);
|
|
235
218
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
console.log(`\n Config (${displayDest}\\config\\ — user-editable, preserved across upgrades):`);
|
|
239
|
-
for (const f of seeded) console.log(` - ${f} -> seeded from template`);
|
|
240
|
-
for (const f of preserved) console.log(` - ${f} -> preserved (already exists)`);
|
|
241
|
-
}
|
|
219
|
+
const seedRes = seedConfig(PKG_ROOT, fullDest);
|
|
220
|
+
printSeedReport(seedRes, `${displayDest}\\`);
|
|
242
221
|
|
|
243
222
|
// Mirror agents/kushi.agent.md as top-level SKILL.md for Clawpilot discovery.
|
|
244
223
|
const agentSrc = path.join(PKG_ROOT, PLUGIN_SOURCE_DIR, CLAWPILOT_AGENT_SOURCE);
|
|
@@ -253,6 +232,31 @@ async function installClawpilot(options, resolved, version) {
|
|
|
253
232
|
printPostInstall(resolved, 'kushi');
|
|
254
233
|
}
|
|
255
234
|
|
|
235
|
+
function printSeedReport(r, dispDestSlash) {
|
|
236
|
+
const total =
|
|
237
|
+
r.seededUser.length + r.seededShared.length +
|
|
238
|
+
r.preservedUser.length + r.preservedShared.length +
|
|
239
|
+
r.doctrine.length + r.examples.length;
|
|
240
|
+
if (total === 0 && r.gitignore === 'unchanged') return;
|
|
241
|
+
|
|
242
|
+
console.log(`\n Config (${dispDestSlash}config/):`);
|
|
243
|
+
if (r.seededShared.length || r.preservedShared.length || r.doctrine.length || r.examples.length) {
|
|
244
|
+
console.log(` shared/ (team-owned, safe to commit)`);
|
|
245
|
+
for (const f of r.seededShared) console.log(` - ${f} -> seeded (team default; preserved on reinstall)`);
|
|
246
|
+
for (const f of r.preservedShared) console.log(` - ${f} -> preserved (already exists)`);
|
|
247
|
+
for (const f of r.doctrine) console.log(` - ${f} -> updated (Kushi-versioned doctrine; do not hand-edit)`);
|
|
248
|
+
for (const f of r.examples) console.log(` - ${f} -> refreshed (reference scaffold)`);
|
|
249
|
+
}
|
|
250
|
+
if (r.seededUser.length || r.preservedUser.length) {
|
|
251
|
+
console.log(` user/ (per-contributor; gitignored)`);
|
|
252
|
+
for (const f of r.seededUser) console.log(` - ${f} -> seeded (edit before first bootstrap)`);
|
|
253
|
+
for (const f of r.preservedUser) console.log(` - ${f} -> preserved (already exists)`);
|
|
254
|
+
}
|
|
255
|
+
if (r.gitignore === 'created' || r.gitignore === 'appended') {
|
|
256
|
+
console.log(` .gitignore ${r.gitignore === 'created' ? 'created' : 'appended'} -> config/user/ excluded`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
256
260
|
function printPostInstall(resolved, prefix) {
|
|
257
261
|
console.log(`\n Done. Profile "${resolved.profile}" installed. Available verbs:`);
|
|
258
262
|
for (const verb of resolved.verbs) {
|
package/src/seed-config.mjs
CHANGED
|
@@ -2,43 +2,108 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
CONFIG_DIR,
|
|
5
|
-
|
|
5
|
+
CONFIG_SHARED_SUBDIR,
|
|
6
|
+
CONFIG_USER_SUBDIR,
|
|
7
|
+
CONFIG_USER_SEED_FILES,
|
|
8
|
+
CONFIG_SHARED_SEED_FILES,
|
|
9
|
+
CONFIG_DOCTRINE_FILES,
|
|
10
|
+
CONFIG_EXAMPLE_FILES,
|
|
11
|
+
CONFIG_GITIGNORE_LINES,
|
|
6
12
|
PLUGIN_SOURCE_DIR,
|
|
7
13
|
} from './constants.mjs';
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
|
-
* Seed <dest>/config/ with
|
|
16
|
+
* Seed <dest>/config/ with config files using a two-tier shared/ + user/ layout.
|
|
11
17
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
18
|
+
* <dest>/config/
|
|
19
|
+
* shared/ ← team-owned, safe to commit (doctrine + team defaults + .example scaffolds)
|
|
20
|
+
* user/ ← per-contributor identity & local paths (gitignored)
|
|
21
|
+
*
|
|
22
|
+
* Policies:
|
|
23
|
+
* - USER SEED (user/, seed-once): copied only if absent. Preserved on reinstall.
|
|
24
|
+
* - SHARED SEED (shared/, seed-once): copied only if absent. Preserved on reinstall.
|
|
25
|
+
* - DOCTRINE (shared/, overwrite-always): always rewritten — Kushi-versioned.
|
|
26
|
+
* - EXAMPLES (shared/, overwrite-always): always rewritten — reference scaffolds.
|
|
27
|
+
*
|
|
28
|
+
* Also writes <dest>/.gitignore with `config/user/` to prevent accidental commits.
|
|
16
29
|
*
|
|
17
30
|
* @param {string} sourcePkgDir – root of the npm package
|
|
18
31
|
* @param {string} destAbsolute – absolute install destination (e.g. <workspace>/.kushi)
|
|
19
|
-
* @returns {{
|
|
32
|
+
* @returns {{
|
|
33
|
+
* seededUser: string[], // user/ files freshly created
|
|
34
|
+
* seededShared: string[], // shared/ seed-once files freshly created
|
|
35
|
+
* preservedUser: string[], // user/ files left untouched (already existed)
|
|
36
|
+
* preservedShared: string[],// shared/ seed-once files left untouched
|
|
37
|
+
* doctrine: string[], // shared/ doctrine files written (always)
|
|
38
|
+
* examples: string[], // shared/ .example files written (always)
|
|
39
|
+
* gitignore: 'created' | 'appended' | 'unchanged' | 'skipped',
|
|
40
|
+
* }}
|
|
20
41
|
*/
|
|
21
42
|
export function seedConfig(sourcePkgDir, destAbsolute) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const preserved = [];
|
|
43
|
+
const sharedDir = path.join(destAbsolute, CONFIG_DIR, CONFIG_SHARED_SUBDIR);
|
|
44
|
+
const userDir = path.join(destAbsolute, CONFIG_DIR, CONFIG_USER_SUBDIR);
|
|
45
|
+
fs.mkdirSync(sharedDir, { recursive: true });
|
|
46
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
const result = {
|
|
49
|
+
seededUser: [],
|
|
50
|
+
seededShared: [],
|
|
51
|
+
preservedUser: [],
|
|
52
|
+
preservedShared: [],
|
|
53
|
+
doctrine: [],
|
|
54
|
+
examples: [],
|
|
55
|
+
gitignore: 'unchanged',
|
|
56
|
+
};
|
|
31
57
|
|
|
32
|
-
|
|
58
|
+
const seedOnce = (specs, destDir, seededList, preservedList) => {
|
|
59
|
+
for (const { template, target } of specs) {
|
|
60
|
+
const src = path.join(sourcePkgDir, PLUGIN_SOURCE_DIR, 'templates', template);
|
|
61
|
+
const dst = path.join(destDir, target);
|
|
62
|
+
if (!fs.existsSync(src)) continue;
|
|
63
|
+
if (fs.existsSync(dst)) {
|
|
64
|
+
preservedList.push(target);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
fs.cpSync(src, dst);
|
|
68
|
+
seededList.push(target);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
33
71
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
72
|
+
const overwriteAlways = (specs, destDir, list) => {
|
|
73
|
+
for (const { template, target } of specs) {
|
|
74
|
+
const src = path.join(sourcePkgDir, PLUGIN_SOURCE_DIR, 'templates', template);
|
|
75
|
+
const dst = path.join(destDir, target);
|
|
76
|
+
if (!fs.existsSync(src)) continue;
|
|
77
|
+
fs.cpSync(src, dst, { force: true });
|
|
78
|
+
list.push(target);
|
|
37
79
|
}
|
|
80
|
+
};
|
|
38
81
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
// user/ tier
|
|
83
|
+
seedOnce(CONFIG_USER_SEED_FILES, userDir, result.seededUser, result.preservedUser);
|
|
84
|
+
// shared/ tier
|
|
85
|
+
seedOnce(CONFIG_SHARED_SEED_FILES, sharedDir, result.seededShared, result.preservedShared);
|
|
86
|
+
overwriteAlways(CONFIG_DOCTRINE_FILES, sharedDir, result.doctrine);
|
|
87
|
+
overwriteAlways(CONFIG_EXAMPLE_FILES, sharedDir, result.examples);
|
|
42
88
|
|
|
43
|
-
|
|
89
|
+
// .gitignore at <dest>/.gitignore — append our marker block if not already present
|
|
90
|
+
result.gitignore = ensureGitignore(destAbsolute, CONFIG_GITIGNORE_LINES);
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureGitignore(destAbsolute, lines) {
|
|
96
|
+
const target = path.join(destAbsolute, '.gitignore');
|
|
97
|
+
const marker = lines[0];
|
|
98
|
+
const block = lines.join('\n') + '\n';
|
|
99
|
+
|
|
100
|
+
if (!fs.existsSync(target)) {
|
|
101
|
+
fs.writeFileSync(target, block, 'utf8');
|
|
102
|
+
return 'created';
|
|
103
|
+
}
|
|
104
|
+
const existing = fs.readFileSync(target, 'utf8');
|
|
105
|
+
if (existing.includes(marker)) return 'unchanged';
|
|
106
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
107
|
+
fs.writeFileSync(target, existing + sep + block, 'utf8');
|
|
108
|
+
return 'appended';
|
|
44
109
|
}
|
|
@@ -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
|