kracked-core 1.1.0 → 1.5.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 +73 -2
- package/bin/kracked-core.mjs +36 -2
- package/package.json +1 -1
- package/src/detect.mjs +34 -12
- package/src/prompt.mjs +110 -6
- package/src/scaffold.mjs +113 -19
- package/src/status.mjs +125 -0
- package/src/uninstall.mjs +214 -0
- package/src/update.mjs +151 -0
- package/src/wizard.mjs +89 -4
- package/templates/loaders/AGENTS.md +1 -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 +1 -0
- package/templates/skills/kracked-sdd/SKILL.md +36 -1
package/src/status.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `kracked-core status` — what's installed here, and is it current?
|
|
2
|
+
//
|
|
3
|
+
// Answers the question a version number alone can't: the npm registry tells you
|
|
4
|
+
// what's available, package.json tells you what you just ran, but neither tells
|
|
5
|
+
// you which version actually wrote the files in this project.
|
|
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
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
import { readVersionStamp, SKILL_NAMES } from './scaffold.mjs';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
17
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
18
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
19
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
20
|
+
|
|
21
|
+
function runningVersion() {
|
|
22
|
+
try {
|
|
23
|
+
return require('../package.json').version;
|
|
24
|
+
} catch {
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Ask npm what the current published version is. Network-optional. */
|
|
30
|
+
async function latestPublished() {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch('https://registry.npmjs.org/kracked-core', {
|
|
33
|
+
signal: AbortSignal.timeout(4000),
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) return null;
|
|
36
|
+
const body = await res.json();
|
|
37
|
+
return body['dist-tags']?.latest ?? null;
|
|
38
|
+
} catch {
|
|
39
|
+
return null; // offline, or npm unreachable — not an error worth failing on
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Compare semver-ish strings. Returns -1, 0, or 1. */
|
|
44
|
+
function compareVersions(a, b) {
|
|
45
|
+
const pa = String(a).split('.').map(Number);
|
|
46
|
+
const pb = String(b).split('.').map(Number);
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
49
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runStatus() {
|
|
55
|
+
const projectDir = process.cwd();
|
|
56
|
+
const globalDir = path.join(os.homedir(), '.kracked');
|
|
57
|
+
const running = runningVersion();
|
|
58
|
+
|
|
59
|
+
stdout.write(`${bold('kracked-core status')}\n\n`);
|
|
60
|
+
|
|
61
|
+
// --- This project ---
|
|
62
|
+
const installed = readVersionStamp(projectDir);
|
|
63
|
+
const hasProject = fs.existsSync(path.join(projectDir, '.kracked'));
|
|
64
|
+
|
|
65
|
+
stdout.write(`${bold('This project')} ${dim(projectDir)}\n`);
|
|
66
|
+
if (!hasProject) {
|
|
67
|
+
stdout.write(' not set up — run `npx kracked-core@latest init`\n');
|
|
68
|
+
} else {
|
|
69
|
+
stdout.write(` installed version: ${installed || dim('unknown (installed before 1.5.0)')}\n`);
|
|
70
|
+
|
|
71
|
+
const skillDirs = ['.claude/skills', '.agents/skills'].filter((d) =>
|
|
72
|
+
fs.existsSync(path.join(projectDir, d))
|
|
73
|
+
);
|
|
74
|
+
for (const dir of skillDirs) {
|
|
75
|
+
const present = SKILL_NAMES.filter((s) =>
|
|
76
|
+
fs.existsSync(path.join(projectDir, dir, s, 'SKILL.md'))
|
|
77
|
+
).length;
|
|
78
|
+
stdout.write(` ${dir}: ${present}/${SKILL_NAMES.length} skills\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Global memory ---
|
|
83
|
+
stdout.write(`\n${bold('Global memory')} ${dim(globalDir)}\n`);
|
|
84
|
+
if (!fs.existsSync(globalDir)) {
|
|
85
|
+
stdout.write(' not set up\n');
|
|
86
|
+
} else {
|
|
87
|
+
const files = ['identity.md', 'preferences.md', 'lessons.md', 'projects.md'];
|
|
88
|
+
const present = files.filter((f) => fs.existsSync(path.join(globalDir, f)));
|
|
89
|
+
stdout.write(` ${present.length}/${files.length} files present\n`);
|
|
90
|
+
|
|
91
|
+
// Lesson count is the signal that memory is actually accumulating.
|
|
92
|
+
try {
|
|
93
|
+
const lessons = fs.readFileSync(path.join(globalDir, 'lessons.md'), 'utf8');
|
|
94
|
+
const count = lessons.split('\n').filter((l) => /^- /.test(l)).length;
|
|
95
|
+
stdout.write(` lessons learned: ${count}\n`);
|
|
96
|
+
} catch {
|
|
97
|
+
// No lessons file — already reflected in the count above.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- Version check ---
|
|
102
|
+
stdout.write(`\n${bold('Version')}\n`);
|
|
103
|
+
stdout.write(` running now: ${running}\n`);
|
|
104
|
+
|
|
105
|
+
const latest = await latestPublished();
|
|
106
|
+
if (!latest) {
|
|
107
|
+
stdout.write(` latest on npm: ${dim("couldn't check (offline?)")}\n`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
stdout.write(` latest on npm: ${latest}\n\n`);
|
|
112
|
+
|
|
113
|
+
const compareTo = installed || running;
|
|
114
|
+
const cmp = compareVersions(compareTo, latest);
|
|
115
|
+
|
|
116
|
+
if (cmp >= 0) {
|
|
117
|
+
stdout.write(` ${green('Up to date.')}\n`);
|
|
118
|
+
} else {
|
|
119
|
+
stdout.write(
|
|
120
|
+
` ${yellow(`Update available: ${compareTo} → ${latest}`)}\n` +
|
|
121
|
+
' Run `npx kracked-core@latest update` to refresh skills and loaders.\n' +
|
|
122
|
+
' Your memory is never touched by an update.\n'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -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,151 @@
|
|
|
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, writeVersionStamp, 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
|
+
writeVersionStamp({ projectDir, version, report });
|
|
148
|
+
|
|
149
|
+
stdout.write(`\n Refreshed ${created.length} file(s).\n`);
|
|
150
|
+
stdout.write('\nRestart your editor so it picks up the updated skills.\n');
|
|
151
|
+
}
|
package/src/wizard.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Follows the wizard flow in CONTRACT.md exactly — question order matters.
|
|
3
3
|
|
|
4
4
|
import { stdin, stdout } from 'node:process';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import os from 'node:os';
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
scanExistingProject,
|
|
14
15
|
stackSummaryLine,
|
|
15
16
|
detectExistingAgentSetup,
|
|
17
|
+
sanitizeScanned,
|
|
16
18
|
} from './detect.mjs';
|
|
17
19
|
import { select, checkbox, confirm, input } from './prompt.mjs';
|
|
18
20
|
import {
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
writeProjectMemory,
|
|
22
24
|
writeLoaders,
|
|
23
25
|
writeSkills,
|
|
26
|
+
writeVersionStamp,
|
|
24
27
|
} from './scaffold.mjs';
|
|
25
28
|
|
|
26
29
|
/** Expand a leading ~ to the home directory. Leaves other paths untouched. */
|
|
@@ -33,11 +36,45 @@ function expandTilde(inputPath) {
|
|
|
33
36
|
return inputPath;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/** The version of kracked-core doing the installing. */
|
|
40
|
+
function pkgVersion() {
|
|
41
|
+
try {
|
|
42
|
+
return createRequire(import.meta.url)('../package.json').version;
|
|
43
|
+
} catch {
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
/** Format today's date as YYYY-MM-DD. */
|
|
37
49
|
function isoDate() {
|
|
38
50
|
return new Date().toISOString().slice(0, 10);
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
// Installing into these scatters loader files across the filesystem, and
|
|
54
|
+
// installing into the home dir makes `<project>/.kracked` collide with global
|
|
55
|
+
// memory. A typo like `/` should not silently create a tree.
|
|
56
|
+
const FORBIDDEN_DIRS = ['/', '/etc', '/usr', '/bin', '/sbin', '/var', '/System', '/Library'];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Refuse obviously wrong install targets before anything is written.
|
|
60
|
+
* Returns an error string, or null when the path is fine.
|
|
61
|
+
*/
|
|
62
|
+
function rejectUnsafeProjectDir(dir) {
|
|
63
|
+
const resolved = path.resolve(dir);
|
|
64
|
+
|
|
65
|
+
if (FORBIDDEN_DIRS.includes(resolved)) {
|
|
66
|
+
return `${resolved} is a system directory — pick a project folder instead.`;
|
|
67
|
+
}
|
|
68
|
+
if (resolved === path.resolve(os.homedir())) {
|
|
69
|
+
return (
|
|
70
|
+
'That\'s your home directory. Installing here would put loader files loose in\n' +
|
|
71
|
+
' your home folder and collide with global memory at ~/.kracked.\n' +
|
|
72
|
+
' Make a project folder first, then run this inside it.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
/**
|
|
42
79
|
* Turn detected package.json scripts into a ready-to-read "how to run it" block.
|
|
43
80
|
* The scan already knows these commands — writing them into project.md is the
|
|
@@ -136,11 +173,11 @@ async function wizardFlow() {
|
|
|
136
173
|
stdout.write('kracked-core — set up memory for your AI coding agent\n\n');
|
|
137
174
|
|
|
138
175
|
// 1. Agent name
|
|
139
|
-
const agentName = (await input('What should we call your agent?', 'KC')) || 'KC';
|
|
176
|
+
const agentName = sanitizeScanned(await input('What should we call your agent?', 'KC'), 40) || 'KC';
|
|
140
177
|
|
|
141
178
|
// 2. User name
|
|
142
179
|
const systemUser = os.userInfo().username || 'you';
|
|
143
|
-
const userName = (await input('Your name?', systemUser)) || systemUser;
|
|
180
|
+
const userName = sanitizeScanned(await input('Your name?', systemUser), 40) || systemUser;
|
|
144
181
|
|
|
145
182
|
// 3. Global memory — always installed. It holds identity, preferences and
|
|
146
183
|
// cross-project lessons, and boot depends on it; making it optional only
|
|
@@ -165,6 +202,35 @@ async function wizardFlow() {
|
|
|
165
202
|
const expanded = expandTilde(pathAnswer);
|
|
166
203
|
projectDir = path.resolve(expanded === '' ? '.' : expanded);
|
|
167
204
|
|
|
205
|
+
const unsafe = rejectUnsafeProjectDir(projectDir);
|
|
206
|
+
if (unsafe) {
|
|
207
|
+
stdout.write(`\n ${unsafe}\n\nStopped — nothing was written.\n`);
|
|
208
|
+
process.exitCode = 1;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Already installed here? 27 individual conflict prompts is not a sane
|
|
213
|
+
// default — offer the three real intents up front instead.
|
|
214
|
+
if (fs.existsSync(path.join(projectDir, '.kracked'))) {
|
|
215
|
+
stdout.write('\n kracked-core is already set up in this project.\n\n');
|
|
216
|
+
const action = await select('What would you like to do?', [
|
|
217
|
+
{ label: 'Refresh package files', value: 'refresh', hint: 'skills + loaders, keeps all your memory' },
|
|
218
|
+
{ label: 'Reinstall everything', value: 'reinstall', hint: 'asks per file before replacing' },
|
|
219
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
220
|
+
], 0);
|
|
221
|
+
|
|
222
|
+
if (action === 'cancel') {
|
|
223
|
+
stdout.write('\nCancelled — nothing changed.\n');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (action === 'refresh') {
|
|
227
|
+
const { runUpdate } = await import('./update.mjs');
|
|
228
|
+
process.chdir(projectDir);
|
|
229
|
+
await runUpdate();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
168
234
|
// Another agent system already here? Ask before touching its loaders.
|
|
169
235
|
if (fs.existsSync(projectDir)) {
|
|
170
236
|
const proceed = await warnOnExistingSetup(projectDir);
|
|
@@ -205,7 +271,7 @@ async function wizardFlow() {
|
|
|
205
271
|
const confirmed = await confirm('Does this look right?', true);
|
|
206
272
|
if (!confirmed) {
|
|
207
273
|
const manualStack = await input('Describe the stack yourself:', stackLine);
|
|
208
|
-
stackLine = manualStack;
|
|
274
|
+
stackLine = sanitizeScanned(manualStack, 80) || stackLine;
|
|
209
275
|
}
|
|
210
276
|
}
|
|
211
277
|
}
|
|
@@ -236,8 +302,26 @@ async function wizardFlow() {
|
|
|
236
302
|
checked: !claudeTaken,
|
|
237
303
|
hint: claudeTaken ? 'CLAUDE.md already in use here' : 'writes CLAUDE.md + .claude/',
|
|
238
304
|
},
|
|
305
|
+
{
|
|
306
|
+
label: 'Kilo Code (VS Code)',
|
|
307
|
+
value: 'kilo',
|
|
308
|
+
checked: false,
|
|
309
|
+
hint: 'reads AGENTS.md + .agents/skills; adds kilo.jsonc',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
label: 'Roo Code (VS Code)',
|
|
313
|
+
value: 'roo',
|
|
314
|
+
checked: false,
|
|
315
|
+
hint: 'reads AGENTS.md + .agents/skills; adds .roo/rules',
|
|
316
|
+
},
|
|
239
317
|
]);
|
|
240
318
|
|
|
319
|
+
// Kilo and Roo both read AGENTS.md and .agents/skills/ natively, so picking
|
|
320
|
+
// either implies the shared .agents/ footprint.
|
|
321
|
+
if ((editors.includes('kilo') || editors.includes('roo')) && !editors.includes('antigravity')) {
|
|
322
|
+
editors.push('antigravity');
|
|
323
|
+
}
|
|
324
|
+
|
|
241
325
|
// 6. Write files
|
|
242
326
|
const projectName = projectSummary?.name || (setUpProject ? path.basename(projectDir) : 'unnamed-project');
|
|
243
327
|
|
|
@@ -266,8 +350,9 @@ async function wizardFlow() {
|
|
|
266
350
|
|
|
267
351
|
if (setUpProject) {
|
|
268
352
|
await writeProjectMemory({ projectDir, tokens, ask: conflictAsk, report: reporter });
|
|
269
|
-
await writeLoaders({ projectDir, tokens, ask: conflictAsk, report: reporter });
|
|
353
|
+
await writeLoaders({ projectDir, tokens, ask: conflictAsk, report: reporter, editors });
|
|
270
354
|
await writeSkills({ projectDir, tokens, editors, ask: conflictAsk, report: reporter });
|
|
355
|
+
writeVersionStamp({ projectDir, version: pkgVersion(), report: reporter });
|
|
271
356
|
}
|
|
272
357
|
|
|
273
358
|
printSummary(created, agentName, setUpProject);
|