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/README.md +41 -3
- package/bin/kracked-core.mjs +29 -2
- package/package.json +1 -1
- package/src/detect.mjs +67 -12
- package/src/prompt.mjs +339 -0
- package/src/scaffold.mjs +99 -20
- package/src/uninstall.mjs +214 -0
- package/src/update.mjs +150 -0
- package/src/wizard.mjs +167 -82
- package/templates/loaders/AGENTS.md +2 -0
- package/templates/loaders/CLAUDE.md +1 -0
- package/templates/loaders/antigravity-rules.md +1 -0
- package/templates/project/sdd/README.md +46 -0
- package/templates/project/sdd/architecture/.gitkeep +0 -0
- package/templates/project/sdd/architecture/_TEMPLATE.md +51 -0
- package/templates/project/sdd/architecture/decisions/_TEMPLATE.md +40 -0
- package/templates/project/sdd/epics/.gitkeep +0 -0
- package/templates/project/sdd/epics/_TEMPLATE.md +34 -0
- package/templates/project/sdd/specs/_TEMPLATE.md +44 -0
- package/templates/project/sdd/stories/.gitkeep +0 -0
- package/templates/project/sdd/stories/_TEMPLATE.md +33 -0
- package/templates/skills/kracked-boot/SKILL.md +13 -5
- package/templates/skills/kracked-identity/SKILL.md +70 -0
- package/templates/skills/kracked-sdd/SKILL.md +36 -1
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
|
-
|
|
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
|
-
//
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
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
|
+
}
|