kracked-core 1.0.0 → 1.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/src/scaffold.mjs CHANGED
@@ -9,7 +9,18 @@ import { fileURLToPath } from 'node:url';
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const TEMPLATES_ROOT = path.join(__dirname, '..', 'templates');
11
11
 
12
- const SKILL_NAMES = ['kracked-boot', 'kracked-sdd', 'kracked-wrap', 'kracked-explain'];
12
+ // Written into every loader kracked-core generates. `uninstall` requires this
13
+ // exact string before deleting a shared-name file (AGENTS.md / CLAUDE.md), so
14
+ // another tool's loader is never removed. Never change it without a migration.
15
+ export const OWNED_MARKER = '<!-- kracked-core:owned -->';
16
+
17
+ export const SKILL_NAMES = [
18
+ 'kracked-boot',
19
+ 'kracked-sdd',
20
+ 'kracked-wrap',
21
+ 'kracked-explain',
22
+ 'kracked-identity',
23
+ ];
13
24
 
14
25
  /** Read a template file, throwing a clear error if Track A hasn't shipped it yet. */
15
26
  function readTemplate(relPath) {
@@ -89,8 +100,10 @@ export async function writeGlobalMemory({ tokens, ask, report }) {
89
100
  export function registerProject({ tokens }) {
90
101
  const globalDir = path.join(os.homedir(), '.kracked');
91
102
  const projectsPath = path.join(globalDir, 'projects.md');
103
+ // Escape pipes — a path or name containing `|` would add phantom columns.
104
+ const cell = (v) => String(v ?? '').replace(/\|/g, '\\|');
92
105
  const stack = tokens.STACK && tokens.STACK.trim() ? tokens.STACK : '—';
93
- const row = `| ${tokens.PROJECT_NAME} | ${tokens.PROJECT_PATH} | ${stack} | active |`;
106
+ const row = `| ${cell(tokens.PROJECT_NAME)} | ${cell(tokens.PROJECT_PATH)} | ${cell(stack)} | active |`;
94
107
 
95
108
  fs.mkdirSync(globalDir, { recursive: true });
96
109
 
@@ -142,38 +155,66 @@ export async function writeProjectMemory({ projectDir, tokens, ask, report }) {
142
155
  await writeFileWithConflictCheck(destPath, content, ask, report);
143
156
  }
144
157
 
145
- // sdd/tracker.md + sdd/specs/.gitkeep
146
- const trackerDest = path.join(krackedDir, 'sdd', 'tracker.md');
147
- const trackerContent = applyTokens(readTemplate('project/sdd/tracker.md'), tokens);
148
- await writeFileWithConflictCheck(trackerDest, trackerContent, ask, report);
158
+ // SDD docs: tracker + the artifact folders, each with a template that
159
+ // explains what belongs there. An empty folder teaches nothing.
160
+ const sddDocs = [
161
+ 'sdd/tracker.md',
162
+ 'sdd/README.md',
163
+ 'sdd/specs/_TEMPLATE.md',
164
+ 'sdd/epics/_TEMPLATE.md',
165
+ 'sdd/stories/_TEMPLATE.md',
166
+ 'sdd/architecture/_TEMPLATE.md',
167
+ 'sdd/architecture/decisions/_TEMPLATE.md',
168
+ ];
149
169
 
150
- const gitkeepDest = path.join(krackedDir, 'sdd', 'specs', '.gitkeep');
151
- if (!fs.existsSync(gitkeepDest)) {
152
- fs.mkdirSync(path.dirname(gitkeepDest), { recursive: true });
153
- fs.writeFileSync(gitkeepDest, '', 'utf8');
154
- report({ path: gitkeepDest, action: 'written' });
170
+ for (const rel of sddDocs) {
171
+ const destPath = path.join(krackedDir, rel);
172
+ const content = applyTokens(readTemplate(`project/${rel}`), tokens);
173
+ await writeFileWithConflictCheck(destPath, content, ask, report);
155
174
  }
156
175
  }
157
176
 
158
177
  /** Write AGENTS.md, CLAUDE.md (shim), and .agents/rules/kracked.md. */
159
- export async function writeLoaders({ projectDir, tokens, ask, report }) {
178
+ export async function writeLoaders({ projectDir, tokens, ask, report, editors }) {
160
179
  const agentsContent = applyTokens(readTemplate('loaders/AGENTS.md'), tokens);
161
180
  await writeFileWithConflictCheck(path.join(projectDir, 'AGENTS.md'), agentsContent, ask, report);
162
181
 
163
182
  const claudeContent = applyTokens(readTemplate('loaders/CLAUDE.md'), tokens);
164
183
  await writeFileWithConflictCheck(path.join(projectDir, 'CLAUDE.md'), claudeContent, ask, report);
165
184
 
166
- const krackedRulesContent = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
167
- await writeFileWithConflictCheck(
168
- path.join(projectDir, '.agents', 'rules', 'kracked.md'),
169
- krackedRulesContent,
170
- ask,
171
- report
172
- );
185
+ // Roo reads .roo/rules/ only — it does NOT read .agents/rules/, so without
186
+ // this mirror the pointer is invisible to Roo.
187
+ if (editors && editors.includes('roo')) {
188
+ const rooRules = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
189
+ await writeFileWithConflictCheck(
190
+ path.join(projectDir, '.roo', 'rules', 'kracked.md'),
191
+ rooRules,
192
+ ask,
193
+ report
194
+ );
195
+ }
196
+
197
+ // Kilo only loads a rules file if it's declared in kilo.jsonc's `instructions`
198
+ // array. Merge into an existing config rather than clobbering the user's.
199
+ if (editors && editors.includes('kilo')) {
200
+ await writeKiloConfig({ projectDir, report });
201
+ }
202
+
203
+ // Only write the Antigravity pointer when Antigravity is actually in use —
204
+ // `update` was creating .agents/ in Claude-only projects.
205
+ if (!editors || editors.includes('antigravity')) {
206
+ const krackedRulesContent = applyTokens(readTemplate('loaders/antigravity-rules.md'), tokens);
207
+ await writeFileWithConflictCheck(
208
+ path.join(projectDir, '.agents', 'rules', 'kracked.md'),
209
+ krackedRulesContent,
210
+ ask,
211
+ report
212
+ );
213
+ }
173
214
  }
174
215
 
175
216
  /**
176
- * Write the 4 skills to BOTH .claude/skills/<name>/SKILL.md and
217
+ * Write every skill to BOTH .claude/skills/<name>/SKILL.md and
177
218
  * .agents/skills/<name>/SKILL.md — but only for the harnesses selected.
178
219
  * `editors` is an array that may contain 'antigravity' and/or 'claude'.
179
220
  */
@@ -190,3 +231,41 @@ export async function writeSkills({ projectDir, tokens, editors, ask, report })
190
231
  }
191
232
  }
192
233
  }
234
+
235
+ /**
236
+ * Add our rules file to kilo.jsonc's `instructions` array. Kilo does not
237
+ * auto-load `.kilo/rules/` — a file is only read if it's listed here, so
238
+ * without this the pointer is silently ignored.
239
+ * Merges into an existing config; never overwrites the user's settings.
240
+ */
241
+ async function writeKiloConfig({ projectDir, report }) {
242
+ const configPath = path.join(projectDir, 'kilo.jsonc');
243
+ const entry = '.agents/rules/kracked.md';
244
+
245
+ if (fs.existsSync(configPath)) {
246
+ const body = fs.readFileSync(configPath, 'utf8');
247
+ if (body.includes(entry)) {
248
+ report({ path: configPath, action: 'already configured' });
249
+ return;
250
+ }
251
+ // Append to the existing instructions array if there is one, else add it.
252
+ let updated;
253
+ if (/"instructions"\s*:\s*\[/.test(body)) {
254
+ updated = body.replace(/("instructions"\s*:\s*\[)/, `$1\n "${entry}",`);
255
+ } else {
256
+ updated = body.replace(/^\s*\{/, `{\n "instructions": ["${entry}"],`);
257
+ }
258
+ fs.writeFileSync(configPath, updated, 'utf8');
259
+ report({ path: configPath, action: 'updated (added instructions entry)' });
260
+ return;
261
+ }
262
+
263
+ const fresh = `{
264
+ // Kilo reads AGENTS.md automatically. This entry additionally loads the
265
+ // kracked-core rules pointer. Add your own settings alongside it.
266
+ "instructions": ["${entry}"]
267
+ }
268
+ `;
269
+ fs.writeFileSync(configPath, fresh, 'utf8');
270
+ report({ path: configPath, action: 'written' });
271
+ }
@@ -0,0 +1,214 @@
1
+ // `kracked-core uninstall` — remove what the installer wrote.
2
+ //
3
+ // Deleting a user's memory is unrecoverable, so this is deliberately cautious:
4
+ // it shows exactly what it will remove, asks per layer, defaults to No on the
5
+ // global layer, and never deletes a file it didn't recognise as its own.
6
+
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { stdout } from 'node:process';
11
+
12
+ import { select, confirm } from './prompt.mjs';
13
+ import { OWNED_MARKER, SKILL_NAMES } from './scaffold.mjs';
14
+
15
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
16
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
17
+
18
+ /**
19
+ * Files/dirs the installer writes into a project.
20
+ *
21
+ * CRITICAL: when the "project" IS the home directory, `<project>/.kracked` and
22
+ * `~/.kracked` are the same folder — so listing it as a project target would
23
+ * delete the user's entire global memory behind the mild "Remove these project
24
+ * files?" prompt, never reaching the two-step global confirmation below.
25
+ * Global memory is only ever removable via the global layer.
26
+ */
27
+ function projectTargets(projectDir) {
28
+ const globalDir = path.resolve(path.join(os.homedir(), '.kracked'));
29
+
30
+ const targets = [
31
+ { path: path.join(projectDir, '.kracked'), label: '.kracked/', kind: 'dir' },
32
+ { path: path.join(projectDir, 'AGENTS.md'), label: 'AGENTS.md', kind: 'file' },
33
+ { path: path.join(projectDir, 'CLAUDE.md'), label: 'CLAUDE.md', kind: 'file' },
34
+ {
35
+ path: path.join(projectDir, '.agents', 'rules', 'kracked.md'),
36
+ label: '.agents/rules/kracked.md',
37
+ kind: 'file',
38
+ },
39
+ ];
40
+
41
+ // Roo's rules mirror. kilo.jsonc is deliberately NOT listed — it may hold the
42
+ // user's own settings, so removing it wholesale would destroy their config.
43
+ targets.push({
44
+ path: path.join(projectDir, '.roo', 'rules', 'kracked.md'),
45
+ label: '.roo/rules/kracked.md',
46
+ kind: 'file',
47
+ });
48
+
49
+ // "Write alongside" during init creates these; nothing else knew about them,
50
+ // so they were orphaned forever.
51
+ for (const base of ['AGENTS.md', 'CLAUDE.md']) {
52
+ targets.push({
53
+ path: path.join(projectDir, `${base}.kracked-new`),
54
+ label: `${base}.kracked-new`,
55
+ kind: 'file',
56
+ });
57
+ }
58
+
59
+ for (const skill of SKILL_NAMES) {
60
+ targets.push({
61
+ path: path.join(projectDir, '.claude', 'skills', skill),
62
+ label: `.claude/skills/${skill}/`,
63
+ kind: 'dir',
64
+ });
65
+ targets.push({
66
+ path: path.join(projectDir, '.agents', 'skills', skill),
67
+ label: `.agents/skills/${skill}/`,
68
+ kind: 'dir',
69
+ });
70
+ }
71
+
72
+ return targets.filter((t) => {
73
+ if (!fs.existsSync(t.path)) return false;
74
+ // Never let a project target resolve onto global memory.
75
+ if (path.resolve(t.path) === globalDir) return false;
76
+ return true;
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Only the two SHARED loader filenames can belong to another system —
82
+ * AGENTS.md and CLAUDE.md are conventions other tools use too, so their
83
+ * contents decide ownership. Everything else lives at a kracked-core-specific
84
+ * path (.kracked/, kracked.md, skills/kracked-*) and is ours by definition.
85
+ */
86
+ function isOurs(filePath) {
87
+ const base = path.basename(filePath);
88
+ if (base !== 'AGENTS.md' && base !== 'CLAUDE.md') return true;
89
+
90
+ try {
91
+ // Require the sentinel the installer writes. Anything else — including a
92
+ // file that merely MENTIONS kracked-core, or another tool using the shared
93
+ // `@AGENTS.md` import convention — is someone else's and must survive.
94
+ // Fail closed: an orphaned file is recoverable, a deleted one is not.
95
+ const body = fs.readFileSync(filePath, 'utf8');
96
+ if (body.includes(OWNED_MARKER)) return true;
97
+
98
+ // Pre-sentinel installs (<=1.2.0) have no marker. Recognise their exact
99
+ // generated shape so an upgrade path doesn't orphan loaders forever.
100
+ // Deliberately narrow: a bare `@AGENTS.md` (a shared convention other tools
101
+ // use) is NOT enough on its own.
102
+ const legacyClaude = /^@AGENTS\.md\s*\n[\s\S]*canonical instructions live in `AGENTS\.md`/.test(body);
103
+ const legacyAgents = /^# AGENTS\.md — [\s\S]*This is the canonical loader[\s\S]*kracked-boot/.test(body);
104
+ return legacyClaude || legacyAgents;
105
+ } catch {
106
+ return false; // unreadable — leave it alone rather than risk deleting someone else's
107
+ }
108
+ }
109
+
110
+ function remove(target) {
111
+ if (target.kind === 'dir') {
112
+ fs.rmSync(target.path, { recursive: true, force: true });
113
+ } else {
114
+ fs.rmSync(target.path, { force: true });
115
+ }
116
+ }
117
+
118
+ /** Drop now-empty .claude/.agents shells so uninstall leaves no litter. */
119
+ function pruneEmptyParents(projectDir) {
120
+ const candidates = [
121
+ path.join(projectDir, '.claude', 'skills'),
122
+ path.join(projectDir, '.claude'),
123
+ path.join(projectDir, '.agents', 'rules'),
124
+ path.join(projectDir, '.roo', 'rules'),
125
+ path.join(projectDir, '.roo'),
126
+ path.join(projectDir, '.agents', 'skills'),
127
+ path.join(projectDir, '.agents'),
128
+ ];
129
+ for (const dir of candidates) {
130
+ try {
131
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
132
+ } catch {
133
+ // Non-empty or not ours to remove — leaving it is the safe outcome.
134
+ }
135
+ }
136
+ }
137
+
138
+ export async function runUninstall() {
139
+ if (!process.stdin.isTTY) {
140
+ process.stderr.write(
141
+ 'kracked-core uninstall needs an interactive terminal.\n' +
142
+ 'Run it directly in a terminal, or delete the files manually — see\n' +
143
+ 'https://github.com/Krackeddevs-Org/kracked-core/blob/main/docs/UNINSTALL.md\n'
144
+ );
145
+ process.exitCode = 1;
146
+ return;
147
+ }
148
+
149
+ const projectDir = process.cwd();
150
+ const globalDir = path.join(os.homedir(), '.kracked');
151
+
152
+ stdout.write(`${bold('kracked-core uninstall')}\n\n`);
153
+
154
+ // ---- Project layer ----
155
+ const found = projectTargets(projectDir);
156
+
157
+ if (found.length === 0) {
158
+ stdout.write(`No kracked-core files found in ${projectDir}\n`);
159
+ } else {
160
+ // Loaders another system owns must never be deleted.
161
+ const foreign = found.filter((t) => t.kind === 'file' && !isOurs(t.path));
162
+ const ours = found.filter((t) => !foreign.includes(t));
163
+
164
+ stdout.write(`Found in ${dim(projectDir)}:\n`);
165
+ for (const t of ours) stdout.write(` ${t.label}\n`);
166
+ if (foreign.length) {
167
+ stdout.write('\n Not ours — will NOT be touched:\n');
168
+ for (const t of foreign) stdout.write(` ${t.label} ${dim('(another system wrote this)')}\n`);
169
+ }
170
+ stdout.write('\n');
171
+
172
+ const choice = await select('Remove these project files?', [
173
+ { label: 'No, keep everything', value: 'no' },
174
+ { label: 'Yes, remove them', value: 'yes', hint: 'this cannot be undone' },
175
+ ], 0);
176
+
177
+ if (choice === 'yes') {
178
+ for (const t of ours) remove(t);
179
+ pruneEmptyParents(projectDir);
180
+ stdout.write(`\n Removed ${ours.length} item(s) from this project.\n`);
181
+ } else {
182
+ stdout.write('\n Kept project files.\n');
183
+ }
184
+ }
185
+
186
+ // ---- Global layer ----
187
+ stdout.write('\n');
188
+ if (!fs.existsSync(globalDir)) {
189
+ stdout.write(`No global memory at ${globalDir}\n`);
190
+ } else {
191
+ stdout.write(`${bold('Global memory')} — ${dim(globalDir)}\n`);
192
+ stdout.write(
193
+ ' This holds your agent\'s identity, your preferences, and every lesson it has\n' +
194
+ ' learned across ALL projects. Removing it cannot be undone, and it is not\n' +
195
+ ' recreated by reinstalling — the content is yours, not the package\'s.\n\n'
196
+ );
197
+
198
+ const wipe = await confirm('Remove global memory too?', false);
199
+ if (wipe) {
200
+ // Deleting months of accumulated lessons deserves a second, explicit yes.
201
+ const sure = await confirm('Are you sure? Your lessons and preferences will be gone.', false);
202
+ if (sure) {
203
+ fs.rmSync(globalDir, { recursive: true, force: true });
204
+ stdout.write('\n Removed global memory.\n');
205
+ } else {
206
+ stdout.write('\n Kept global memory.\n');
207
+ }
208
+ } else {
209
+ stdout.write('\n Kept global memory.\n');
210
+ }
211
+ }
212
+
213
+ stdout.write('\nDone.\n');
214
+ }
package/src/update.mjs ADDED
@@ -0,0 +1,150 @@
1
+ // `kracked-core update` — refresh the package-owned files without touching
2
+ // anything the user has written.
3
+ //
4
+ // The distinction that makes this safe: SKILLS and LOADERS are package files
5
+ // (we wrote them, we can replace them). MEMORY is the user's (identity,
6
+ // preferences, lessons, project docs) — never overwritten here, ever.
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import os from 'node:os';
11
+ import { stdout } from 'node:process';
12
+ import { createRequire } from 'node:module';
13
+
14
+ import { select } from './prompt.mjs';
15
+ import { writeLoaders, writeSkills, SKILL_NAMES } from './scaffold.mjs';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
19
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
20
+
21
+ function installedVersion() {
22
+ try {
23
+ return require('../package.json').version;
24
+ } catch {
25
+ return 'unknown';
26
+ }
27
+ }
28
+
29
+ /** Which harnesses this project already uses, so update doesn't add new ones. */
30
+ function detectInstalledHarnesses(projectDir) {
31
+ const editors = [];
32
+ if (fs.existsSync(path.join(projectDir, '.claude', 'skills'))) editors.push('claude');
33
+ if (fs.existsSync(path.join(projectDir, '.agents', 'skills'))) editors.push('antigravity');
34
+ // A CLAUDE.md with no skills dir still means Claude Code is in play.
35
+ if (!editors.includes('claude') && fs.existsSync(path.join(projectDir, 'CLAUDE.md'))) {
36
+ editors.push('claude');
37
+ }
38
+ return editors;
39
+ }
40
+
41
+ /** Read the agent + user name back out of an installed AGENTS.md. */
42
+ function recoverTokens(projectDir) {
43
+ const agentsPath = path.join(projectDir, 'AGENTS.md');
44
+ const tokens = { AGENT_NAME: 'KC', USER_NAME: os.userInfo().username || 'you' };
45
+
46
+ try {
47
+ const body = fs.readFileSync(agentsPath, 'utf8');
48
+ const agent = body.match(/You are \*\*([^*]+)\*\*/);
49
+ if (agent) tokens.AGENT_NAME = agent[1].trim();
50
+ const user = body.match(/how ([^\s]+) likes to work/);
51
+ if (user) tokens.USER_NAME = user[1].trim();
52
+ } catch {
53
+ // No AGENTS.md to read — fall back to defaults above.
54
+ }
55
+
56
+ return tokens;
57
+ }
58
+
59
+ /** Which skills exist here but aren't in this version — i.e. new ones to add. */
60
+ function missingSkills(projectDir, editors) {
61
+ const missing = [];
62
+ for (const skill of SKILL_NAMES) {
63
+ const inClaude = editors.includes('claude')
64
+ && fs.existsSync(path.join(projectDir, '.claude', 'skills', skill, 'SKILL.md'));
65
+ const inAgents = editors.includes('antigravity')
66
+ && fs.existsSync(path.join(projectDir, '.agents', 'skills', skill, 'SKILL.md'));
67
+ const expected = (editors.includes('claude') ? 1 : 0) + (editors.includes('antigravity') ? 1 : 0);
68
+ const present = (inClaude ? 1 : 0) + (inAgents ? 1 : 0);
69
+ if (present < expected) missing.push(skill);
70
+ }
71
+ return missing;
72
+ }
73
+
74
+ export async function runUpdate() {
75
+ if (!process.stdin.isTTY) {
76
+ process.stderr.write(
77
+ 'kracked-core update needs an interactive terminal.\n' +
78
+ 'Run it directly in a terminal, not piped or in a script.\n'
79
+ );
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const projectDir = process.cwd();
85
+ const version = installedVersion();
86
+
87
+ stdout.write(`${bold('kracked-core update')} ${dim(`v${version}`)}\n\n`);
88
+
89
+ if (!fs.existsSync(path.join(projectDir, '.kracked'))) {
90
+ stdout.write(
91
+ `No kracked-core install found in ${projectDir}\n\n` +
92
+ 'Run `npx kracked-core init` to set it up first.\n'
93
+ );
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+
98
+ const editors = detectInstalledHarnesses(projectDir);
99
+ if (editors.length === 0) {
100
+ stdout.write('Found .kracked/ but no editor files. Run `npx kracked-core init` instead.\n');
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+
105
+ const tokens = {
106
+ ...recoverTokens(projectDir),
107
+ PROJECT_NAME: path.basename(projectDir),
108
+ PROJECT_PATH: projectDir,
109
+ DATE: new Date().toISOString().slice(0, 10),
110
+ STACK: '',
111
+ RUN_COMMANDS: '',
112
+ };
113
+
114
+ const newSkills = missingSkills(projectDir, editors);
115
+
116
+ stdout.write(`Agent: ${tokens.AGENT_NAME}\n`);
117
+ stdout.write(`Editors: ${editors.join(', ')}\n`);
118
+ if (newSkills.length) {
119
+ stdout.write(`New in this version: ${newSkills.join(', ')}\n`);
120
+ }
121
+
122
+ stdout.write(`\n${bold('Will be refreshed')} ${dim('(package files — safe to replace)')}\n`);
123
+ stdout.write(' the 5 skills, AGENTS.md, CLAUDE.md, .agents/rules/kracked.md\n');
124
+ stdout.write(`\n${bold('Will NOT be touched')} ${dim('(yours)')}\n`);
125
+ stdout.write(' ~/.kracked/ — identity, preferences, lessons\n');
126
+ stdout.write(' .kracked/ — project memory, specs, epics, stories, architecture\n\n');
127
+
128
+ const choice = await select('Refresh package files to this version?', [
129
+ { label: 'Yes, update', value: 'yes' },
130
+ { label: 'No, cancel', value: 'no' },
131
+ ], 0);
132
+
133
+ if (choice === 'no') {
134
+ stdout.write('\nCancelled — nothing changed.\n');
135
+ return;
136
+ }
137
+
138
+ // Package files are ours, so overwrite silently rather than prompting per
139
+ // file. User memory is never in this set.
140
+ const created = [];
141
+ const report = (entry) => created.push(entry);
142
+ const overwrite = async () => 'overwrite';
143
+
144
+ stdout.write('\nUpdating...\n');
145
+ await writeLoaders({ projectDir, tokens, ask: overwrite, report, editors });
146
+ await writeSkills({ projectDir, tokens, editors, ask: overwrite, report });
147
+
148
+ stdout.write(`\n Refreshed ${created.length} file(s).\n`);
149
+ stdout.write('\nRestart your editor so it picks up the updated skills.\n');
150
+ }