kushi-agents 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/config/m365-auth.json.example +56 -0
- package/.github/config/m365-mutable.json.example +11 -0
- package/LICENSE +201 -0
- package/README.md +159 -0
- package/bin/cli.mjs +75 -0
- package/package.json +35 -0
- package/plugin/agents/kushi.agent.md +147 -0
- package/plugin/instructions/answer-from-evidence.instructions.md +73 -0
- package/plugin/instructions/auth-and-retry.instructions.md +116 -0
- package/plugin/instructions/az-auth-conditional.instructions.md +39 -0
- package/plugin/instructions/azure-auth-patterns.instructions.md +226 -0
- package/plugin/instructions/citation-ledger.instructions.md +52 -0
- package/plugin/instructions/engagement-root-resolution.instructions.md +82 -0
- package/plugin/instructions/evidence-thoroughness.instructions.md +62 -0
- package/plugin/instructions/side-by-side-config.instructions.md +56 -0
- package/plugin/instructions/snapshot-vs-stream.instructions.md +87 -0
- package/plugin/instructions/thoroughness-detector.instructions.md +105 -0
- package/plugin/instructions/workiq-first.instructions.md +47 -0
- package/plugin/plugin.json +96 -0
- package/plugin/prompts/aggregate.prompt.md +24 -0
- package/plugin/prompts/ask.prompt.md +16 -0
- package/plugin/prompts/bootstrap.prompt.md +23 -0
- package/plugin/prompts/consolidate.prompt.md +21 -0
- package/plugin/prompts/fde-intake.prompt.md +41 -0
- package/plugin/prompts/fde-report.prompt.md +46 -0
- package/plugin/prompts/fde-triage.prompt.md +46 -0
- package/plugin/prompts/refresh.prompt.md +17 -0
- package/plugin/prompts/state.prompt.md +17 -0
- package/plugin/prompts/status.prompt.md +17 -0
- package/plugin/reference-packs/README.md +74 -0
- package/plugin/reference-packs/fde/README.md +62 -0
- package/plugin/reference-packs/fde/core-fde-reference.md +427 -0
- package/plugin/reference-packs/fde/intake-questions.md +168 -0
- package/plugin/reference-packs/fde/report-doctrine.md +189 -0
- package/plugin/skills/aggregate-project/SKILL.md +72 -0
- package/plugin/skills/ask-project/SKILL.md +162 -0
- package/plugin/skills/bootstrap-project/SKILL.md +129 -0
- package/plugin/skills/build-state/SKILL.md +69 -0
- package/plugin/skills/consolidate-evidence/SKILL.md +47 -0
- package/plugin/skills/fde-intake/SKILL.md +147 -0
- package/plugin/skills/fde-report/SKILL.md +159 -0
- package/plugin/skills/fde-triage/SKILL.md +114 -0
- package/plugin/skills/intro/SKILL.md +449 -0
- package/plugin/skills/project-status/SKILL.md +61 -0
- package/plugin/skills/pull-ado/SKILL.md +77 -0
- package/plugin/skills/pull-crm/SKILL.md +75 -0
- package/plugin/skills/pull-email/SKILL.md +75 -0
- package/plugin/skills/pull-meetings/SKILL.md +77 -0
- package/plugin/skills/pull-onenote/SKILL.md +82 -0
- package/plugin/skills/pull-sharepoint/SKILL.md +85 -0
- package/plugin/skills/pull-teams/SKILL.md +75 -0
- package/plugin/skills/refresh-project/SKILL.md +89 -0
- package/plugin/skills/self-check/SKILL.md +166 -0
- package/plugin/skills/self-check/run.ps1 +517 -0
- package/plugin/skills/self-check/run.sh +33 -0
- package/plugin/templates/fde/intake.md +114 -0
- package/plugin/templates/fde/report-fitness.md +151 -0
- package/plugin/templates/fde/report-long.md +109 -0
- package/plugin/templates/fde/report-short.md +45 -0
- package/plugin/templates/fde/report-stage-readiness.md +70 -0
- package/plugin/templates/fde/report-weekly.md +73 -0
- package/plugin/templates/fde/triage-00-fde-analysis.md +78 -0
- package/plugin/templates/fde/triage-02-risk-analysis.md +76 -0
- package/plugin/templates/fde/triage-03-6Q.md +40 -0
- package/plugin/templates/fde/triage-04-readiness-checklist.md +82 -0
- package/plugin/templates/fde/triage-05-executive-consolidated.md +78 -0
- package/plugin/templates/fde/triage-06-global-opportunity.md +70 -0
- package/plugin/templates/fde/triage-07-validation-warnings.md +60 -0
- package/plugin/templates/init/ado-config.template.yml +21 -0
- package/plugin/templates/init/crm-config.template.yml +16 -0
- package/plugin/templates/init/kushi-projects.template.json +14 -0
- package/plugin/templates/init/m365-auth.template.json +67 -0
- package/plugin/templates/init/m365-mutable.template.json +19 -0
- package/plugin/templates/init/project-contributors.template.yml +27 -0
- package/plugin/templates/init/project-evidence.template.yml +32 -0
- package/plugin/templates/init/project-integrations.template.yml +34 -0
- package/plugin/templates/init/project-user-settings.template.yml +71 -0
- package/plugin/templates/paste-prompt.md +35 -0
- package/plugin/templates/snapshot/ado-item.template.md +45 -0
- package/plugin/templates/snapshot/crm-record.template.md +34 -0
- package/plugin/templates/snapshot/meetings-series-index.template.md +32 -0
- package/plugin/templates/snapshot/onenote-page.template.md +28 -0
- package/plugin/templates/snapshot/sharepoint-file.template.md +31 -0
- package/plugin/templates/snapshot/sharepoint-tree.template.md +39 -0
- package/plugin/templates/snapshot/teams-roster.template.md +27 -0
- package/plugin/templates/state/00_overview.template.md +44 -0
- package/plugin/templates/state/01_decisions.template.md +41 -0
- package/plugin/templates/state/02_stakeholders.template.md +48 -0
- package/plugin/templates/state/03_architecture-and-solution.template.md +56 -0
- package/plugin/templates/state/04_workshops-and-key-meetings.template.md +43 -0
- package/plugin/templates/state/05_action-items.template.md +29 -0
- package/plugin/templates/state/06_risks-and-issues.template.md +43 -0
- package/plugin/templates/state/07_timeline-and-milestones.template.md +45 -0
- package/plugin/templates/state/08_artifacts-and-deliverables.template.md +55 -0
- package/plugin/templates/state/09_open-questions.template.md +62 -0
- package/plugin/templates/state/README.md +41 -0
- package/plugin/templates/weekly/ado-stream.template.md +71 -0
- package/plugin/templates/weekly/consolidated.template.md +98 -0
- package/plugin/templates/weekly/crm-stream.template.md +74 -0
- package/plugin/templates/weekly/email-stream.template.md +103 -0
- package/plugin/templates/weekly/meetings-stream.template.md +182 -0
- package/plugin/templates/weekly/onenote-stream.template.md +106 -0
- package/plugin/templates/weekly/run-log.template.md +88 -0
- package/plugin/templates/weekly/sharepoint-stream.template.md +121 -0
- package/plugin/templates/weekly/teams-stream.template.md +121 -0
- package/src/constants.mjs +49 -0
- package/src/copy-assets.mjs +183 -0
- package/src/main.mjs +262 -0
- package/src/profile-resolver.mjs +168 -0
- package/src/prompt.mjs +42 -0
- package/src/settings.mjs +77 -0
package/src/main.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_DEST,
|
|
7
|
+
CLAWPILOT_DEST_SUBPATH,
|
|
8
|
+
TARGET_VSCODE,
|
|
9
|
+
TARGET_CLAWPILOT,
|
|
10
|
+
CLAWPILOT_AGENT_SOURCE,
|
|
11
|
+
CLAWPILOT_SKILL_DEST,
|
|
12
|
+
PLUGIN_SOURCE_DIR,
|
|
13
|
+
} from './constants.mjs';
|
|
14
|
+
import { promptForDestination } from './prompt.mjs';
|
|
15
|
+
import { copyAssets, copyProjectFiles } from './copy-assets.mjs';
|
|
16
|
+
import { mergeSettings } from './settings.mjs';
|
|
17
|
+
import {
|
|
18
|
+
resolveProfile,
|
|
19
|
+
makeIncludeFilter,
|
|
20
|
+
writeInstalledManifest,
|
|
21
|
+
} from './profile-resolver.mjs';
|
|
22
|
+
|
|
23
|
+
/** Resolve the root of *this* npm package. */
|
|
24
|
+
const PKG_ROOT = path.resolve(
|
|
25
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
26
|
+
'..',
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function getVersion() {
|
|
30
|
+
const pkg = JSON.parse(
|
|
31
|
+
fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf-8'),
|
|
32
|
+
);
|
|
33
|
+
return pkg.version;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readDefaultProfile() {
|
|
37
|
+
const pj = JSON.parse(
|
|
38
|
+
fs.readFileSync(path.join(PKG_ROOT, PLUGIN_SOURCE_DIR, 'plugin.json'), 'utf-8'),
|
|
39
|
+
);
|
|
40
|
+
return pj.default_profile || 'standard';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveProfileForInstall(profileName) {
|
|
44
|
+
const pluginJsonPath = path.join(PKG_ROOT, PLUGIN_SOURCE_DIR, 'plugin.json');
|
|
45
|
+
const name = profileName || readDefaultProfile();
|
|
46
|
+
return resolveProfile(pluginJsonPath, name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Orchestrator: prompt -> copy -> settings -> summary.
|
|
51
|
+
* @param {{ dest?: string, force?: boolean, noSettings?: boolean, target?: string, profile?: string }} options
|
|
52
|
+
*/
|
|
53
|
+
export async function main(options = {}) {
|
|
54
|
+
const version = getVersion();
|
|
55
|
+
const target = options.target === TARGET_CLAWPILOT ? TARGET_CLAWPILOT : TARGET_VSCODE;
|
|
56
|
+
|
|
57
|
+
let resolved;
|
|
58
|
+
try {
|
|
59
|
+
resolved = resolveProfileForInstall(options.profile);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`\n ${err.message}\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
`\n Kushi Agents Installer v${version} [target: ${target}, profile: ${resolved.profile}]\n`,
|
|
67
|
+
);
|
|
68
|
+
console.log(` Profile chain: ${resolved.chain.join(' -> ')}`);
|
|
69
|
+
console.log(` ${resolved.description}\n`);
|
|
70
|
+
|
|
71
|
+
if (target === TARGET_CLAWPILOT) {
|
|
72
|
+
await installClawpilot(options, resolved, version);
|
|
73
|
+
} else {
|
|
74
|
+
await installVscode(options, resolved, version);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Install for VS Code Chat: copies assets into a workspace-local `.kushi/`
|
|
80
|
+
* folder and updates `.vscode/settings.json` so the chat surface picks them up.
|
|
81
|
+
*/
|
|
82
|
+
async function installVscode(options, resolved, version) {
|
|
83
|
+
const projectRoot = process.cwd();
|
|
84
|
+
const hasProjectMarker =
|
|
85
|
+
fs.existsSync(path.join(projectRoot, 'package.json')) ||
|
|
86
|
+
fs.existsSync(path.join(projectRoot, '.git'));
|
|
87
|
+
|
|
88
|
+
if (!hasProjectMarker) {
|
|
89
|
+
console.warn(
|
|
90
|
+
' WARN: This directory does not look like a project root (no package.json or .git found). Proceeding anyway.\n',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let dest;
|
|
95
|
+
if (options.dest) {
|
|
96
|
+
dest = options.dest.replace(/\\/g, '/');
|
|
97
|
+
if (path.isAbsolute(dest)) {
|
|
98
|
+
console.error('\n Please use a relative path (e.g., .kushi).\n');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
dest = dest.replace(/\/+$/, '');
|
|
102
|
+
if (!dest) {
|
|
103
|
+
console.error('\n Please provide a non-empty destination path.\n');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const resolvedPath = path.resolve(projectRoot, dest);
|
|
107
|
+
if (
|
|
108
|
+
!resolvedPath.startsWith(projectRoot + path.sep) &&
|
|
109
|
+
resolvedPath !== projectRoot
|
|
110
|
+
) {
|
|
111
|
+
console.error('\n Destination must be within the current project.\n');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
dest = await promptForDestination(DEFAULT_DEST);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fullDest = path.resolve(projectRoot, dest);
|
|
119
|
+
await confirmOverwriteIfExists(fullDest, dest, options.force);
|
|
120
|
+
|
|
121
|
+
const includeFilter = makeIncludeFilter(resolved);
|
|
122
|
+
const { copied, details } = copyAssets(PKG_ROOT, fullDest, includeFilter);
|
|
123
|
+
|
|
124
|
+
console.log(` Copied ${copied} files to ${dest}/`);
|
|
125
|
+
for (const { dir, count } of details) {
|
|
126
|
+
if (count > 0) {
|
|
127
|
+
console.log(` - ${dir}/ (${count} ${count === 1 ? 'file' : 'files'})`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const manifestPath = writeInstalledManifest(fullDest, resolved, version);
|
|
132
|
+
console.log(` - kushi-install.json (profile manifest)`);
|
|
133
|
+
|
|
134
|
+
if (!options.noSettings) {
|
|
135
|
+
const { created, keysAdded, keysUnchanged } = mergeSettings(
|
|
136
|
+
projectRoot,
|
|
137
|
+
dest,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
console.log(
|
|
141
|
+
`\n ${created ? 'Created' : 'Updated'} .vscode/settings.json`,
|
|
142
|
+
);
|
|
143
|
+
for (const key of keysAdded) {
|
|
144
|
+
console.log(` - ${key} -> added`);
|
|
145
|
+
}
|
|
146
|
+
for (const key of keysUnchanged) {
|
|
147
|
+
console.log(` - ${key} -> unchanged`);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
console.log('\n Skipped .vscode/settings.json (--no-settings)');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { copied: projCopied, skipped: projSkipped } = copyProjectFiles(PKG_ROOT);
|
|
154
|
+
if (projCopied.length > 0) {
|
|
155
|
+
console.log('\n Copied project files to .github/');
|
|
156
|
+
for (const f of projCopied) {
|
|
157
|
+
console.log(` - ${f}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (projSkipped.length > 0) {
|
|
161
|
+
for (const f of projSkipped) {
|
|
162
|
+
console.log(` - ${f} -> unchanged (already exists)`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
printPostInstall(resolved, '@Kushi');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Install for Clawpilot CLI: copies assets into `~/.copilot/m-skills/kushi/`
|
|
171
|
+
* and writes a top-level `SKILL.md` (mirrored from `agents/kushi.agent.md`) so
|
|
172
|
+
* Clawpilot's skill discovery (`m_list_skills`) finds it as a single skill.
|
|
173
|
+
*/
|
|
174
|
+
async function installClawpilot(options, resolved, version) {
|
|
175
|
+
const home = os.homedir();
|
|
176
|
+
const fullDest = options.dest
|
|
177
|
+
? path.resolve(options.dest)
|
|
178
|
+
: path.join(home, ...CLAWPILOT_DEST_SUBPATH.split('/'));
|
|
179
|
+
|
|
180
|
+
const displayDest = fullDest.startsWith(home + path.sep)
|
|
181
|
+
? '~' + path.sep + path.relative(home, fullDest)
|
|
182
|
+
: fullDest;
|
|
183
|
+
|
|
184
|
+
await confirmOverwriteIfExists(fullDest, displayDest, options.force);
|
|
185
|
+
|
|
186
|
+
// If overwriting, wipe profile-dependent dirs first so an upgrade from
|
|
187
|
+
// standard -> core actually removes the standard-only skills.
|
|
188
|
+
if (fs.existsSync(fullDest) && options.force) {
|
|
189
|
+
for (const sub of ['skills', 'prompts', 'instructions', 'templates', 'reference-packs']) {
|
|
190
|
+
const p = path.join(fullDest, sub);
|
|
191
|
+
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fs.mkdirSync(fullDest, { recursive: true });
|
|
196
|
+
|
|
197
|
+
const includeFilter = makeIncludeFilter(resolved);
|
|
198
|
+
const { copied, details } = copyAssets(PKG_ROOT, fullDest, includeFilter);
|
|
199
|
+
|
|
200
|
+
console.log(` Copied ${copied} files to ${displayDest}\\`);
|
|
201
|
+
for (const { dir, count } of details) {
|
|
202
|
+
if (count > 0) {
|
|
203
|
+
console.log(` - ${dir}/ (${count} ${count === 1 ? 'file' : 'files'})`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeInstalledManifest(fullDest, resolved, version);
|
|
208
|
+
console.log(` - kushi-install.json (profile manifest)`);
|
|
209
|
+
|
|
210
|
+
// Mirror agents/kushi.agent.md as top-level SKILL.md for Clawpilot discovery.
|
|
211
|
+
const agentSrc = path.join(PKG_ROOT, PLUGIN_SOURCE_DIR, CLAWPILOT_AGENT_SOURCE);
|
|
212
|
+
const skillDst = path.join(fullDest, CLAWPILOT_SKILL_DEST);
|
|
213
|
+
if (fs.existsSync(agentSrc)) {
|
|
214
|
+
fs.cpSync(agentSrc, skillDst, { force: true });
|
|
215
|
+
console.log(`\n Wrote ${CLAWPILOT_SKILL_DEST} (mirrored from ${CLAWPILOT_AGENT_SOURCE})`);
|
|
216
|
+
} else {
|
|
217
|
+
console.warn(`\n WARN: ${CLAWPILOT_AGENT_SOURCE} not found in package — skipping SKILL.md.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
printPostInstall(resolved, 'kushi');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function printPostInstall(resolved, prefix) {
|
|
224
|
+
console.log(`\n Done. Profile "${resolved.profile}" installed. Available verbs:`);
|
|
225
|
+
for (const verb of resolved.verbs) {
|
|
226
|
+
console.log(` ${prefix} ${verb} <project>`);
|
|
227
|
+
}
|
|
228
|
+
if (!resolved.verbs.includes('refresh')) {
|
|
229
|
+
console.log(
|
|
230
|
+
`\n NOTE: 'refresh' / 'state' / 'bootstrap' are NOT available in profile "${resolved.profile}".`,
|
|
231
|
+
);
|
|
232
|
+
console.log(` Re-install with --profile standard --force to enable them.`);
|
|
233
|
+
}
|
|
234
|
+
console.log('\n Docs: https://gim-home.github.io/kushi/\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function confirmOverwriteIfExists(fullDest, displayDest, force) {
|
|
238
|
+
if (!fs.existsSync(fullDest) || force) return;
|
|
239
|
+
|
|
240
|
+
const { createInterface } = await import('node:readline/promises');
|
|
241
|
+
const rl = createInterface({
|
|
242
|
+
input: process.stdin,
|
|
243
|
+
output: process.stdout,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
let overwrite;
|
|
247
|
+
try {
|
|
248
|
+
const answer = await rl.question(
|
|
249
|
+
` ${displayDest} already exists. Overwrite? (Y/n): `,
|
|
250
|
+
);
|
|
251
|
+
overwrite = answer.trim().toLowerCase() !== 'n';
|
|
252
|
+
} catch {
|
|
253
|
+
process.exit(0);
|
|
254
|
+
} finally {
|
|
255
|
+
rl.close();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!overwrite) {
|
|
259
|
+
console.log('\n Cancelled.\n');
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a profile name into the concrete asset set to install.
|
|
6
|
+
*
|
|
7
|
+
* Walks the `extends:` chain in plugin.json profiles and unions the
|
|
8
|
+
* skills / prompts / instructions / templates / verbs lists.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} pluginJsonPath - absolute path to plugin/plugin.json
|
|
11
|
+
* @param {string} profileName - one of the keys under "profiles"
|
|
12
|
+
* @returns {{
|
|
13
|
+
* profile: string,
|
|
14
|
+
* chain: string[],
|
|
15
|
+
* skills: string[],
|
|
16
|
+
* prompts: string[],
|
|
17
|
+
* instructions: string[] | '*',
|
|
18
|
+
* templates: string[],
|
|
19
|
+
* referencePacks: string[],
|
|
20
|
+
* verbs: string[],
|
|
21
|
+
* description: string
|
|
22
|
+
* }}
|
|
23
|
+
*/
|
|
24
|
+
export function resolveProfile(pluginJsonPath, profileName) {
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf-8'));
|
|
26
|
+
const profiles = raw.profiles || {};
|
|
27
|
+
|
|
28
|
+
if (!profiles[profileName]) {
|
|
29
|
+
const available = Object.keys(profiles).join(', ') || '(none defined)';
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Unknown profile "${profileName}". Available: ${available}.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Walk extends chain from root down so child overrides win on description.
|
|
36
|
+
const chain = [];
|
|
37
|
+
let cursor = profileName;
|
|
38
|
+
const guard = new Set();
|
|
39
|
+
while (cursor) {
|
|
40
|
+
if (guard.has(cursor)) {
|
|
41
|
+
throw new Error(`Circular extends in profile "${cursor}".`);
|
|
42
|
+
}
|
|
43
|
+
guard.add(cursor);
|
|
44
|
+
chain.unshift(cursor);
|
|
45
|
+
cursor = profiles[cursor]?.extends;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const merged = {
|
|
49
|
+
skills: new Set(),
|
|
50
|
+
prompts: new Set(),
|
|
51
|
+
instructions: null, // '*' means all
|
|
52
|
+
templates: new Set(),
|
|
53
|
+
referencePacks: new Set(),
|
|
54
|
+
verbs: new Set(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
for (const name of chain) {
|
|
58
|
+
const p = profiles[name];
|
|
59
|
+
for (const s of p.skills || []) merged.skills.add(s);
|
|
60
|
+
for (const s of p.prompts || []) merged.prompts.add(s);
|
|
61
|
+
for (const s of p.templates || []) merged.templates.add(s);
|
|
62
|
+
for (const s of p.reference_packs || []) merged.referencePacks.add(s);
|
|
63
|
+
for (const s of p.verbs || []) merged.verbs.add(s);
|
|
64
|
+
if (p.instructions === '*' || merged.instructions === '*') {
|
|
65
|
+
merged.instructions = '*';
|
|
66
|
+
} else if (Array.isArray(p.instructions)) {
|
|
67
|
+
if (!merged.instructions) merged.instructions = new Set();
|
|
68
|
+
for (const s of p.instructions) merged.instructions.add(s);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
profile: profileName,
|
|
74
|
+
chain,
|
|
75
|
+
description: profiles[profileName].description || '',
|
|
76
|
+
skills: [...merged.skills],
|
|
77
|
+
prompts: [...merged.prompts],
|
|
78
|
+
instructions:
|
|
79
|
+
merged.instructions === '*' || merged.instructions === null
|
|
80
|
+
? '*'
|
|
81
|
+
: [...merged.instructions],
|
|
82
|
+
templates: [...merged.templates],
|
|
83
|
+
referencePacks: [...merged.referencePacks],
|
|
84
|
+
verbs: [...merged.verbs],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the include-set used by copyDirFiltered: a function that takes a
|
|
90
|
+
* relative path like "skills/pull-email/SKILL.md" and returns true if it
|
|
91
|
+
* should be copied for the given resolved profile.
|
|
92
|
+
*
|
|
93
|
+
* @param {ReturnType<typeof resolveProfile>} resolved
|
|
94
|
+
*/
|
|
95
|
+
export function makeIncludeFilter(resolved) {
|
|
96
|
+
const skillSet = new Set(resolved.skills);
|
|
97
|
+
const promptSet = new Set(resolved.prompts);
|
|
98
|
+
const templateSet = new Set(resolved.templates);
|
|
99
|
+
const packSet = new Set(resolved.referencePacks);
|
|
100
|
+
const allInstr = resolved.instructions === '*';
|
|
101
|
+
const instrSet = allInstr ? null : new Set(resolved.instructions);
|
|
102
|
+
|
|
103
|
+
return function include(assetDir, relPath) {
|
|
104
|
+
// assetDir is one of: agents, instructions, prompts, skills, templates
|
|
105
|
+
if (assetDir === 'agents') return true;
|
|
106
|
+
|
|
107
|
+
if (assetDir === 'skills') {
|
|
108
|
+
const top = relPath.split(/[\\/]/)[0];
|
|
109
|
+
return skillSet.has(top);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (assetDir === 'prompts') {
|
|
113
|
+
// e.g. "bootstrap.prompt.md" -> match "bootstrap"
|
|
114
|
+
const base = relPath.split(/[\\/]/)[0];
|
|
115
|
+
const stem = base.replace(/\.prompt\.md$/, '');
|
|
116
|
+
return promptSet.has(stem);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (assetDir === 'instructions') {
|
|
120
|
+
if (allInstr) return true;
|
|
121
|
+
const base = relPath.split(/[\\/]/)[0];
|
|
122
|
+
const stem = base.replace(/\.instructions\.md$/, '');
|
|
123
|
+
return instrSet.has(stem);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (assetDir === 'templates') {
|
|
127
|
+
const top = relPath.split(/[\\/]/)[0];
|
|
128
|
+
return templateSet.has(top);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (assetDir === 'reference-packs') {
|
|
132
|
+
// Always include the top-level README.md doctrine file.
|
|
133
|
+
if (!relPath.includes(path.sep) && !relPath.includes('/') && /^README\.md$/i.test(relPath)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const top = relPath.split(/[\\/]/)[0];
|
|
137
|
+
return packSet.has(top);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Write a slim manifest into the install destination so the host (and
|
|
146
|
+
* self-check) knows which profile is installed.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} destAbsolute
|
|
149
|
+
* @param {ReturnType<typeof resolveProfile>} resolved
|
|
150
|
+
* @param {string} version
|
|
151
|
+
*/
|
|
152
|
+
export function writeInstalledManifest(destAbsolute, resolved, version) {
|
|
153
|
+
const file = path.join(destAbsolute, 'kushi-install.json');
|
|
154
|
+
const manifest = {
|
|
155
|
+
profile: resolved.profile,
|
|
156
|
+
chain: resolved.chain,
|
|
157
|
+
version,
|
|
158
|
+
skills: resolved.skills.sort(),
|
|
159
|
+
prompts: resolved.prompts.sort(),
|
|
160
|
+
instructions: resolved.instructions === '*' ? '*' : resolved.instructions.sort(),
|
|
161
|
+
templates: resolved.templates.sort(),
|
|
162
|
+
reference_packs: [...(resolved.referencePacks || [])].sort(),
|
|
163
|
+
verbs: resolved.verbs.sort(),
|
|
164
|
+
installed_at: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
|
|
167
|
+
return file;
|
|
168
|
+
}
|
package/src/prompt.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prompt the user for a destination directory.
|
|
6
|
+
* @param {string} defaultDest
|
|
7
|
+
* @returns {Promise<string>}
|
|
8
|
+
*/
|
|
9
|
+
export async function promptForDestination(defaultDest) {
|
|
10
|
+
const { stdin: input, stdout: output } = await import('node:process');
|
|
11
|
+
const rl = createInterface({ input, output });
|
|
12
|
+
|
|
13
|
+
rl.on('close', () => {});
|
|
14
|
+
|
|
15
|
+
let dest;
|
|
16
|
+
try {
|
|
17
|
+
const answer = await rl.question(
|
|
18
|
+
` Where should Kushi assets be installed? (${defaultDest}): `,
|
|
19
|
+
);
|
|
20
|
+
dest = answer.trim() || defaultDest;
|
|
21
|
+
} catch {
|
|
22
|
+
process.exit(0);
|
|
23
|
+
} finally {
|
|
24
|
+
rl.close();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return validatePath(dest);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validatePath(raw) {
|
|
31
|
+
if (path.isAbsolute(raw)) {
|
|
32
|
+
console.error('\n Please use a relative path (e.g., .kushi).\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const resolved = path.resolve(process.cwd(), raw);
|
|
36
|
+
const root = process.cwd();
|
|
37
|
+
if (!resolved.startsWith(root + path.sep) && resolved !== root) {
|
|
38
|
+
console.error('\n Destination must be within the current project.\n');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
return raw.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
42
|
+
}
|
package/src/settings.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { modify, applyEdits, parse, printParseErrorCode } from 'jsonc-parser';
|
|
4
|
+
import { SETTINGS_MAP } from './constants.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merge Nova settings into .vscode/settings.json (JSONC-safe).
|
|
8
|
+
* Creates the file and directory if they don't exist.
|
|
9
|
+
* Additive-only: never removes existing entries.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} projectRoot – the user's project root (cwd)
|
|
12
|
+
* @param {string} destination – relative destination path (e.g. ".nova")
|
|
13
|
+
* @returns {{ created: boolean, keysAdded: string[], keysUnchanged: string[] }}
|
|
14
|
+
*/
|
|
15
|
+
export function mergeSettings(projectRoot, destination) {
|
|
16
|
+
const vscodeDir = path.join(projectRoot, '.vscode');
|
|
17
|
+
const settingsPath = path.join(vscodeDir, 'settings.json');
|
|
18
|
+
|
|
19
|
+
// Ensure .vscode/ exists
|
|
20
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
let content;
|
|
23
|
+
let created = false;
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(settingsPath)) {
|
|
26
|
+
content = fs.readFileSync(settingsPath, 'utf-8');
|
|
27
|
+
|
|
28
|
+
// Validate the JSONC is parseable
|
|
29
|
+
/** @type {import('jsonc-parser').ParseError[]} */
|
|
30
|
+
const errors = [];
|
|
31
|
+
parse(content, errors, { allowTrailingComma: true });
|
|
32
|
+
if (errors.length > 0) {
|
|
33
|
+
const first = errors[0];
|
|
34
|
+
// Compute line number from offset
|
|
35
|
+
const line = content.slice(0, first.offset).split('\n').length;
|
|
36
|
+
console.error(
|
|
37
|
+
`\n ✘ Your settings.json has a syntax error at line ${line} (${printParseErrorCode(first.error)}). Fix it and re-run.\n`,
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
content = '{}';
|
|
43
|
+
created = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const keysAdded = [];
|
|
47
|
+
const keysUnchanged = [];
|
|
48
|
+
|
|
49
|
+
for (const [settingsKey, subdir] of Object.entries(SETTINGS_MAP)) {
|
|
50
|
+
// Always use forward slashes in settings paths
|
|
51
|
+
const entryPath = [destination, subdir].join('/');
|
|
52
|
+
|
|
53
|
+
// Check if the exact path entry already exists
|
|
54
|
+
const parsed = parse(content, undefined, { allowTrailingComma: true });
|
|
55
|
+
const existing = parsed?.[settingsKey];
|
|
56
|
+
|
|
57
|
+
if (existing && typeof existing === 'object' && existing[entryPath] === true) {
|
|
58
|
+
keysUnchanged.push(settingsKey);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Use jsonc-parser's modify to add/merge the entry while preserving comments
|
|
63
|
+
const edits = modify(content, [settingsKey, entryPath], true, {
|
|
64
|
+
formattingOptions: {
|
|
65
|
+
tabSize: 2,
|
|
66
|
+
insertSpaces: true,
|
|
67
|
+
eol: '\n',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
content = applyEdits(content, edits);
|
|
71
|
+
keysAdded.push(settingsKey);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(settingsPath, content, 'utf-8');
|
|
75
|
+
|
|
76
|
+
return { created, keysAdded, keysUnchanged };
|
|
77
|
+
}
|