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,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rsiProgramCatalog": {
|
|
3
|
+
"version": "2026-05-08",
|
|
4
|
+
"description": "Committed RSI program-name reference catalog for HLS-related classification during 6Q and report authoring.",
|
|
5
|
+
"classificationGuidance": {
|
|
6
|
+
"directMatch": "Use when the ask clearly aligns to one or more catalog program names.",
|
|
7
|
+
"remoteChance": "Use when the ask has only a weak or partial overlap and should be labeled as a remote chance rather than a direct fit.",
|
|
8
|
+
"none": "If no plausible direct or remote-chance program match exists, mark the classification as None."
|
|
9
|
+
},
|
|
10
|
+
"segments": [
|
|
11
|
+
{
|
|
12
|
+
"segment": "HLS MedTech",
|
|
13
|
+
"programNames": [
|
|
14
|
+
"RSI | [HLS MedTech] AI factory digital twins",
|
|
15
|
+
"RSI | [HLS MedTech] AI-enabled guided selling and customer service",
|
|
16
|
+
"RSI | [HLS MedTech] Augmented device research with optimized data & AI",
|
|
17
|
+
"RSI | [HLS MedTech] Care experience personalization through connected devices",
|
|
18
|
+
"RSI | [HLS MedTech] Connected Devices",
|
|
19
|
+
"RSI | [HLS MedTech] Emerging Use Case",
|
|
20
|
+
"RSI | [HLS MedTech] MedTech Sales and Field Service",
|
|
21
|
+
"RSI | [HLS MedTech] Optimized Supply chain Augmented end to end supply chain management",
|
|
22
|
+
"RSI | [HLS MedTech] Oracle Database on Azure",
|
|
23
|
+
"RSI | [HLS MedTech] SAP on Azure",
|
|
24
|
+
"RSI | [HLS MedTech] Unified Threat and Identity Protection"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"segment": "HLS Payor",
|
|
29
|
+
"programNames": [
|
|
30
|
+
"RSI | [HLS Payor] Accelerate claims administration, prior authorization, billing, and enrollment processes through automation",
|
|
31
|
+
"RSI | [HLS Payor] AI-Powered Contact Center",
|
|
32
|
+
"RSI | [HLS Payor] Care Management",
|
|
33
|
+
"RSI | [HLS Payor] Core Operations Modernization",
|
|
34
|
+
"RSI | [HLS Payor] Defend against ransomware and breaches",
|
|
35
|
+
"RSI | [HLS Payor] Differentiate with integrated and AI-powered cybersecurity",
|
|
36
|
+
"RSI | [HLS Payor] Digital Clinical Quality Measures",
|
|
37
|
+
"RSI | [HLS Payor] Emerging Use Case",
|
|
38
|
+
"RSI | [HLS Payor] Enhance team communication and collaboration across all functional areas (clinical/non-clinical)",
|
|
39
|
+
"RSI | [HLS Payor] Interoperability Platforms",
|
|
40
|
+
"RSI | [HLS Payor] M365 Copilot for Payors",
|
|
41
|
+
"RSI | [HLS Payor] Personalize the Member Experience",
|
|
42
|
+
"RSI | [HLS Payor] Prior Authorization",
|
|
43
|
+
"RSI | [HLS Payor] Protect identities and access",
|
|
44
|
+
"RSI | [HLS Payor] Revenue Cycle Management",
|
|
45
|
+
"RSI | [HLS Payor] TriZetto on Azure",
|
|
46
|
+
"RSI | [HLS Payor] Unify member, provider, and clinical data to enhance member outcomes and drive operational efficiency"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"segment": "HLS Pharma",
|
|
51
|
+
"programNames": [
|
|
52
|
+
"RSI | [HLS Pharma] Agentic Trials Execution",
|
|
53
|
+
"RSI | [HLS Pharma] AI factory digital twins",
|
|
54
|
+
"RSI | [HLS Pharma] AI-enabled guided selling and customer service",
|
|
55
|
+
"RSI | [HLS Pharma] Augmented end to end supply chain management",
|
|
56
|
+
"RSI | [HLS Pharma] Augmented research and drug discovery with optimized data and AI",
|
|
57
|
+
"RSI | [HLS Pharma] Clinical Trials Matching",
|
|
58
|
+
"RSI | [HLS Pharma] Custom Sales Agent",
|
|
59
|
+
"RSI | [HLS Pharma] Emerging Use Case",
|
|
60
|
+
"RSI | [HLS Pharma] Lab Automation",
|
|
61
|
+
"RSI | [HLS Pharma] Optimized Supply chain",
|
|
62
|
+
"RSI | [HLS Pharma] Oracle Database on Azure",
|
|
63
|
+
"RSI | [HLS Pharma] Pharmacovigilance & Drug Safety",
|
|
64
|
+
"RSI | [HLS Pharma] Real-World Evidence",
|
|
65
|
+
"RSI | [HLS Pharma] Regulated Documents Generation",
|
|
66
|
+
"RSI | [HLS Pharma] SAP on Azure",
|
|
67
|
+
"RSI | [HLS Pharma] Scientific Data Mining and Sharing",
|
|
68
|
+
"RSI | [HLS Pharma] Scientific Workflow Automation (MSFT Discovery)",
|
|
69
|
+
"RSI | [HLS Pharma] Streamlined clinical trial development & submission",
|
|
70
|
+
"RSI | [HLS Pharma] Study Protocols",
|
|
71
|
+
"RSI | [HLS Pharma] Trusted Research Environment",
|
|
72
|
+
"RSI | [HLS Pharma] Unified Threat and Identity Protection"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"segment": "HLS Provider",
|
|
77
|
+
"programNames": [
|
|
78
|
+
"RSI | [HLS Provider] Accelerate clinical research and patient care",
|
|
79
|
+
"RSI | [HLS Provider] AI Transformation: Smart Hospitals / Hospital of the Future (including Digital Twin)",
|
|
80
|
+
"RSI | [HLS Provider] Defend against ransomware, breaches, isolated protection",
|
|
81
|
+
"RSI | [HLS Provider] Differentiate with integrated and AI-powered cybersecurity",
|
|
82
|
+
"RSI | [HLS Provider] EHR on Azure: Cloud Migration",
|
|
83
|
+
"RSI | [HLS Provider] EHR on Azure: Developer and testing training environment",
|
|
84
|
+
"RSI | [HLS Provider] EHR on Azure: Isolated protection against ransomware attacks",
|
|
85
|
+
"RSI | [HLS Provider] EHR on Azure: Optimize Operations and Data Insights",
|
|
86
|
+
"RSI | [HLS Provider] Emerging Use Case",
|
|
87
|
+
"RSI | [HLS Provider] Enhance care management with actionable insights",
|
|
88
|
+
"RSI | [HLS Provider] Enhance care team communication and collaboration",
|
|
89
|
+
"RSI | [HLS Provider] Imaging: Accelerate radiology report creation and the discovery of medical insights",
|
|
90
|
+
"RSI | [HLS Provider] Imaging: Cloud Migration",
|
|
91
|
+
"RSI | [HLS Provider] Imaging: Transform image sharing and patient care coordination",
|
|
92
|
+
"RSI | [HLS Provider] Population Health Platforms",
|
|
93
|
+
"RSI | [HLS Provider] Protecting identities and access",
|
|
94
|
+
"RSI | [HLS Provider] Streamline clinical documentation and workflows (Dragon)",
|
|
95
|
+
"RSI | [HLS Provider] Trusted Research Environment"
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"segment": "HLS Cross-Segment",
|
|
100
|
+
"programNames": [
|
|
101
|
+
"RSI | [HLS] Personalize patient engagement",
|
|
102
|
+
"RSI | [HLS] Emerging Use Case"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Node-side twin of Get-KushiConfig.ps1.
|
|
6
|
+
*
|
|
7
|
+
* Lookup order:
|
|
8
|
+
* 1. <workspace>/.kushi/config/user/<name>.<ext>
|
|
9
|
+
* 2. <workspace>/.kushi/config/shared/<name>.<ext>
|
|
10
|
+
*
|
|
11
|
+
* @param {string} name Logical config name (e.g. 'm365-auth', 'project-evidence').
|
|
12
|
+
* @param {object} [opts]
|
|
13
|
+
* @param {string} [opts.workspace=process.cwd()] Workspace root.
|
|
14
|
+
* @param {string} [opts.ext] Override extension (json | yml | yaml).
|
|
15
|
+
* @param {boolean} [opts.raw=false] Return raw text instead of parsed object.
|
|
16
|
+
* @param {boolean} [opts.pathOnly=false] Return only the resolved file path.
|
|
17
|
+
* @param {boolean} [opts.allowPlaceholders=false] Skip __FILL_ME_IN__/<auto> check.
|
|
18
|
+
* @returns {string|object} Parsed object (or raw text, or path).
|
|
19
|
+
*/
|
|
20
|
+
export function loadKushiConfig(name, opts = {}) {
|
|
21
|
+
const workspace = opts.workspace || process.cwd();
|
|
22
|
+
const ext = opts.ext || defaultExt(name);
|
|
23
|
+
const fileName = `${name}.${ext}`;
|
|
24
|
+
|
|
25
|
+
const configRoot = path.join(workspace, '.kushi', 'config');
|
|
26
|
+
const userPath = path.join(configRoot, 'user', fileName);
|
|
27
|
+
const sharedPath = path.join(configRoot, 'shared', fileName);
|
|
28
|
+
|
|
29
|
+
let resolved = null;
|
|
30
|
+
if (fs.existsSync(userPath)) resolved = userPath;
|
|
31
|
+
else if (fs.existsSync(sharedPath)) resolved = sharedPath;
|
|
32
|
+
|
|
33
|
+
if (!resolved) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Kushi config '${name}' not found. Looked in:\n` +
|
|
36
|
+
` - ${userPath}\n` +
|
|
37
|
+
` - ${sharedPath}\n` +
|
|
38
|
+
`Run \`npx kushi-agents@latest\` to (re)seed config files.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (opts.pathOnly) return resolved;
|
|
43
|
+
|
|
44
|
+
const raw = fs.readFileSync(resolved, 'utf8');
|
|
45
|
+
|
|
46
|
+
if (!opts.allowPlaceholders) {
|
|
47
|
+
if (raw.includes('__FILL_ME_IN__') || raw.includes('<auto>')) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Kushi config '${resolved}' still has template placeholders. ` +
|
|
50
|
+
`Edit it with real values, or pass {allowPlaceholders: true}.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (opts.raw) return raw;
|
|
56
|
+
if (ext === 'json') return JSON.parse(raw);
|
|
57
|
+
// YAML: return raw — Node has no built-in parser. Caller installs js-yaml if needed.
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultExt(name) {
|
|
62
|
+
if (/^(project-evidence|integrations)$/.test(name)) return 'yml';
|
|
63
|
+
return 'json';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve path only — convenience wrapper. */
|
|
67
|
+
export function resolveKushiConfigPath(name, opts = {}) {
|
|
68
|
+
return loadKushiConfig(name, { ...opts, pathOnly: true });
|
|
69
|
+
}
|
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
|
}
|