okstra 0.2.0 → 0.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 +1451 -17
- package/bin/okstra +7 -5
- package/package.json +5 -7
- package/runtime/BUILD.json +2 -2
- package/runtime/skills/okstra-context-loader/SKILL.md +140 -0
- package/runtime/skills/okstra-convergence/SKILL.md +289 -0
- package/runtime/skills/okstra-history/SKILL.md +137 -0
- package/runtime/skills/okstra-report-finder/SKILL.md +87 -0
- package/runtime/skills/okstra-report-writer/SKILL.md +256 -0
- package/runtime/skills/okstra-run/SKILL.md +231 -0
- package/runtime/skills/okstra-schedule/SKILL.md +627 -0
- package/runtime/skills/okstra-setup/SKILL.md +138 -0
- package/runtime/skills/okstra-status/SKILL.md +230 -0
- package/runtime/skills/okstra-team-contract/SKILL.md +402 -0
- package/runtime/skills/okstra-time-summary/SKILL.md +138 -0
- package/src/check-project.mjs +188 -0
- package/src/doctor.mjs +15 -0
- package/src/install.mjs +91 -2
- package/src/paths.mjs +2 -2
- package/src/uninstall.mjs +59 -6
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolvePaths } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const USAGE = `okstra check-project — verify that the current project has okstra setup
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
okstra check-project Resolve PROJECT_ROOT from cwd, look for
|
|
10
|
+
.project-docs/okstra/project.json,
|
|
11
|
+
print JSON status to stdout.
|
|
12
|
+
okstra check-project --cwd <dir> Use <dir> as the search starting point
|
|
13
|
+
instead of process cwd.
|
|
14
|
+
okstra check-project --json Same as default (kept for symmetry with
|
|
15
|
+
'paths --json').
|
|
16
|
+
okstra check-project --quiet Suppress stdout on success; exit code
|
|
17
|
+
alone reports state.
|
|
18
|
+
|
|
19
|
+
Exit codes:
|
|
20
|
+
0 project.json found, projectId present
|
|
21
|
+
1 project.json missing or unreadable (project setup not done)
|
|
22
|
+
2 PROJECT_ROOT could not be resolved from cwd (no project marker
|
|
23
|
+
ancestor; user should pass --cwd or run from within a project)
|
|
24
|
+
|
|
25
|
+
User-facing skills should call this after 'ensure-installed' and refuse
|
|
26
|
+
to proceed if the exit code is non-zero, directing the user to
|
|
27
|
+
'/okstra-setup' first.
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
function runProcess(cmd, args, env) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const child = spawn(cmd, args, {
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
env: { ...process.env, ...env },
|
|
35
|
+
});
|
|
36
|
+
let stdout = "";
|
|
37
|
+
let stderr = "";
|
|
38
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
39
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
40
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
41
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fileExists(p) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(p);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args) {
|
|
55
|
+
const opts = { cwd: process.cwd(), quiet: false, json: true };
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const a = args[i];
|
|
58
|
+
if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
59
|
+
else if (a === "--json") opts.json = true;
|
|
60
|
+
else if (a === "--cwd") {
|
|
61
|
+
const next = args[i + 1];
|
|
62
|
+
if (!next || next.startsWith("--")) throw new Error("--cwd requires a path");
|
|
63
|
+
opts.cwd = next;
|
|
64
|
+
i++;
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`unknown argument '${a}'`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return opts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function emit(opts, payload) {
|
|
73
|
+
if (opts.quiet) return;
|
|
74
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function run(args) {
|
|
78
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
79
|
+
process.stdout.write(USAGE);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const opts = parseArgs(args);
|
|
84
|
+
const paths = await resolvePaths();
|
|
85
|
+
|
|
86
|
+
const probe = await runProcess(
|
|
87
|
+
"python3",
|
|
88
|
+
[
|
|
89
|
+
"-c",
|
|
90
|
+
[
|
|
91
|
+
"import json, sys",
|
|
92
|
+
"from okstra_project import resolve_project_root, project_json_path, ResolverError",
|
|
93
|
+
"try:",
|
|
94
|
+
" pr = resolve_project_root(explicit_root='', cwd=sys.argv[1])",
|
|
95
|
+
" print('PROJECT_ROOT', pr)",
|
|
96
|
+
" print('PROJECT_JSON', project_json_path(pr))",
|
|
97
|
+
"except ResolverError as e:",
|
|
98
|
+
" print('RESOLVER_ERROR', e)",
|
|
99
|
+
].join("\n"),
|
|
100
|
+
opts.cwd,
|
|
101
|
+
],
|
|
102
|
+
{ PYTHONPATH: paths.pythonpath },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (probe.code !== 0) {
|
|
106
|
+
emit(opts, {
|
|
107
|
+
ok: false,
|
|
108
|
+
stage: "python",
|
|
109
|
+
reason: `python invocation failed: ${probe.stderr.trim() || probe.stdout.trim()}`,
|
|
110
|
+
});
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lines = probe.stdout.trim().split("\n");
|
|
115
|
+
const tagOf = (key) =>
|
|
116
|
+
lines
|
|
117
|
+
.find((l) => l.startsWith(key + " "))
|
|
118
|
+
?.slice(key.length + 1)
|
|
119
|
+
.trim() ?? null;
|
|
120
|
+
|
|
121
|
+
const resolverError = tagOf("RESOLVER_ERROR");
|
|
122
|
+
if (resolverError) {
|
|
123
|
+
emit(opts, {
|
|
124
|
+
ok: false,
|
|
125
|
+
stage: "resolve",
|
|
126
|
+
reason: resolverError,
|
|
127
|
+
cwd: opts.cwd,
|
|
128
|
+
});
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const projectRoot = tagOf("PROJECT_ROOT");
|
|
133
|
+
const projectJsonPath = tagOf("PROJECT_JSON");
|
|
134
|
+
if (!projectRoot || !projectJsonPath) {
|
|
135
|
+
emit(opts, {
|
|
136
|
+
ok: false,
|
|
137
|
+
stage: "parse",
|
|
138
|
+
reason: "could not parse python output",
|
|
139
|
+
raw: probe.stdout,
|
|
140
|
+
});
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!(await fileExists(projectJsonPath))) {
|
|
145
|
+
emit(opts, {
|
|
146
|
+
ok: false,
|
|
147
|
+
stage: "project_json_missing",
|
|
148
|
+
reason: `${projectJsonPath} not found — run /okstra-setup in this project first`,
|
|
149
|
+
projectRoot,
|
|
150
|
+
projectJsonPath,
|
|
151
|
+
});
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let projectId = null;
|
|
156
|
+
try {
|
|
157
|
+
const data = JSON.parse(await fs.readFile(projectJsonPath, "utf8"));
|
|
158
|
+
projectId = typeof data?.projectId === "string" ? data.projectId : null;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
emit(opts, {
|
|
161
|
+
ok: false,
|
|
162
|
+
stage: "project_json_invalid",
|
|
163
|
+
reason: `failed to parse ${projectJsonPath}: ${err.message}`,
|
|
164
|
+
projectRoot,
|
|
165
|
+
projectJsonPath,
|
|
166
|
+
});
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!projectId) {
|
|
171
|
+
emit(opts, {
|
|
172
|
+
ok: false,
|
|
173
|
+
stage: "project_json_invalid",
|
|
174
|
+
reason: `${projectJsonPath} missing projectId field`,
|
|
175
|
+
projectRoot,
|
|
176
|
+
projectJsonPath,
|
|
177
|
+
});
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
emit(opts, {
|
|
182
|
+
ok: true,
|
|
183
|
+
projectRoot,
|
|
184
|
+
projectJsonPath,
|
|
185
|
+
projectId,
|
|
186
|
+
});
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { resolvePaths } from "./paths.mjs";
|
|
5
6
|
|
|
7
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
8
|
+
const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
|
|
9
|
+
|
|
6
10
|
const USAGE = `okstra doctor — diagnose the installed runtime
|
|
7
11
|
|
|
8
12
|
Usage:
|
|
@@ -15,6 +19,7 @@ Checks:
|
|
|
15
19
|
- okstra_ctl module importable
|
|
16
20
|
- bash entrypoints present and executable in $HOME/.okstra/bin
|
|
17
21
|
- agents/ directory exists inside the okstra package
|
|
22
|
+
- canonical skills present at $HOME/.claude/skills/<name>/SKILL.md
|
|
18
23
|
- version stamp matches package version
|
|
19
24
|
`;
|
|
20
25
|
|
|
@@ -104,6 +109,16 @@ export async function run(args) {
|
|
|
104
109
|
await check("bin: okstra-codex-exec.sh", () => checkBashEntry(paths.bin, "okstra-codex-exec.sh")),
|
|
105
110
|
await check("bin: okstra-gemini-exec.sh", () => checkBashEntry(paths.bin, "okstra-gemini-exec.sh")),
|
|
106
111
|
await check("bin: okstra-central.sh", () => checkBashEntry(paths.bin, "okstra-central.sh")),
|
|
112
|
+
...(await Promise.all(
|
|
113
|
+
REQUIRED_SKILLS.map((name) =>
|
|
114
|
+
check(`skill: ${name}`, async () => {
|
|
115
|
+
const p = join(CLAUDE_SKILLS_DIR, name, "SKILL.md");
|
|
116
|
+
return (await pathExists(p))
|
|
117
|
+
? { ok: true, detail: p }
|
|
118
|
+
: { ok: false, detail: `not found: ${p}` };
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
)),
|
|
107
122
|
await check("version stamp", async () =>
|
|
108
123
|
paths.version === paths.package
|
|
109
124
|
? { ok: true, detail: paths.version }
|
package/src/install.mjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { join, relative, resolve as resolveAbs } from "node:path";
|
|
4
5
|
import { getPackageRoot } from "./version.mjs";
|
|
5
6
|
import { resolvePaths } from "./paths.mjs";
|
|
6
7
|
|
|
8
|
+
const SKILLS_MANIFEST_REL = "installed-skills.json";
|
|
9
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
10
|
+
|
|
7
11
|
const PYTHON_PACKAGES = ["okstra_project", "okstra_ctl", "okstra_token_usage", "lib"];
|
|
8
12
|
const BIN_ENTRYPOINTS = [
|
|
9
13
|
"okstra.sh",
|
|
@@ -25,13 +29,16 @@ Usage:
|
|
|
25
29
|
okstra install --refresh Re-copy even files that match by hash
|
|
26
30
|
|
|
27
31
|
Effect (copy mode):
|
|
28
|
-
${"$HOME"}/.okstra/lib/python <-
|
|
29
|
-
${"$HOME"}/.okstra/bin <-
|
|
32
|
+
${"$HOME"}/.okstra/lib/python <- runtime/python
|
|
33
|
+
${"$HOME"}/.okstra/bin <- runtime/bin
|
|
34
|
+
${"$HOME"}/.claude/skills/<name> <- runtime/skills/<name> (per skill)
|
|
35
|
+
${"$HOME"}/.okstra/installed-skills.json <- manifest of installed skills
|
|
30
36
|
${"$HOME"}/.okstra/version <- installed package version stamp
|
|
31
37
|
|
|
32
38
|
Effect (link mode):
|
|
33
39
|
${"$HOME"}/.okstra/lib/python/<pkg> -> <repo>/scripts/<pkg> (symlink)
|
|
34
40
|
${"$HOME"}/.okstra/bin/<name>.sh -> <repo>/scripts/<name>.sh
|
|
41
|
+
${"$HOME"}/.claude/skills/<name> -> <repo>/skills/<name> (symlink dir)
|
|
35
42
|
${"$HOME"}/.okstra/dev-link <- <repo> path stamp
|
|
36
43
|
${"$HOME"}/.okstra/version <- installed package version stamp
|
|
37
44
|
|
|
@@ -208,6 +215,9 @@ async function installLinkMode(repoPath, paths, opts) {
|
|
|
208
215
|
if (!quiet) process.stdout.write(` bin/${name}: ${action}\n`);
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
const skillResult = await installSkillsLink(repoAbs, { dryRun, quiet });
|
|
219
|
+
await writeSkillsManifest(paths.home, skillResult.installed, { dryRun });
|
|
220
|
+
|
|
211
221
|
if (!dryRun) {
|
|
212
222
|
await writeFileAtomic(join(paths.home, "dev-link"), repoAbs + "\n", 0o644);
|
|
213
223
|
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
|
@@ -229,6 +239,79 @@ async function fileExists(p) {
|
|
|
229
239
|
}
|
|
230
240
|
}
|
|
231
241
|
|
|
242
|
+
async function listSkillDirs(skillsRoot) {
|
|
243
|
+
try {
|
|
244
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
245
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (err.code === "ENOENT") return [];
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function writeSkillsManifest(home, names, opts) {
|
|
253
|
+
const { dryRun = false } = opts ?? {};
|
|
254
|
+
const data = {
|
|
255
|
+
version: 1,
|
|
256
|
+
installedAt: new Date().toISOString(),
|
|
257
|
+
skills: Array.from(new Set(names)).sort(),
|
|
258
|
+
};
|
|
259
|
+
if (dryRun) {
|
|
260
|
+
process.stdout.write(`[dry-run] write skills manifest: ${data.skills.length} entries\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
await writeFileAtomic(
|
|
264
|
+
join(home, SKILLS_MANIFEST_REL),
|
|
265
|
+
JSON.stringify(data, null, 2) + "\n",
|
|
266
|
+
0o644,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function installSkillsCopy(runtimeRoot, opts) {
|
|
271
|
+
const { refresh, dryRun, quiet } = opts;
|
|
272
|
+
const srcRoot = join(runtimeRoot, "skills");
|
|
273
|
+
const names = await listSkillDirs(srcRoot);
|
|
274
|
+
if (names.length === 0) {
|
|
275
|
+
if (!quiet) process.stdout.write(" skills: runtime/skills empty — skipped\n");
|
|
276
|
+
return { installed: [] };
|
|
277
|
+
}
|
|
278
|
+
let copied = 0;
|
|
279
|
+
let skipped = 0;
|
|
280
|
+
for (const name of names) {
|
|
281
|
+
const r = await copyTreeIfChanged(
|
|
282
|
+
join(srcRoot, name),
|
|
283
|
+
join(CLAUDE_SKILLS_DIR, name),
|
|
284
|
+
{ refresh, dryRun, mode: 0o644 },
|
|
285
|
+
);
|
|
286
|
+
copied += r.copied;
|
|
287
|
+
skipped += r.skipped;
|
|
288
|
+
}
|
|
289
|
+
if (!quiet) {
|
|
290
|
+
process.stdout.write(
|
|
291
|
+
` skills: copied=${copied} skipped=${skipped} -> ${CLAUDE_SKILLS_DIR}/ (${names.length} skills)\n`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return { installed: names };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function installSkillsLink(repoAbs, opts) {
|
|
298
|
+
const { dryRun, quiet } = opts;
|
|
299
|
+
const srcRoot = join(repoAbs, "skills");
|
|
300
|
+
const names = await listSkillDirs(srcRoot);
|
|
301
|
+
if (names.length === 0) {
|
|
302
|
+
if (!quiet) process.stdout.write(" skills: <repo>/skills missing — skipped\n");
|
|
303
|
+
return { installed: [] };
|
|
304
|
+
}
|
|
305
|
+
if (!dryRun) await fs.mkdir(CLAUDE_SKILLS_DIR, { recursive: true });
|
|
306
|
+
for (const name of names) {
|
|
307
|
+
const src = join(srcRoot, name);
|
|
308
|
+
const dst = join(CLAUDE_SKILLS_DIR, name);
|
|
309
|
+
const action = await ensureSymlink(src, dst, { dryRun });
|
|
310
|
+
if (!quiet) process.stdout.write(` skills/${name}: ${action}\n`);
|
|
311
|
+
}
|
|
312
|
+
return { installed: names };
|
|
313
|
+
}
|
|
314
|
+
|
|
232
315
|
function parseInstallArgs(args) {
|
|
233
316
|
const result = {
|
|
234
317
|
dryRun: false,
|
|
@@ -309,6 +392,9 @@ export async function runInstall(args) {
|
|
|
309
392
|
);
|
|
310
393
|
}
|
|
311
394
|
|
|
395
|
+
const skillResult = await installSkillsCopy(runtimeRoot, opts);
|
|
396
|
+
await writeSkillsManifest(paths.home, skillResult.installed, { dryRun: opts.dryRun });
|
|
397
|
+
|
|
312
398
|
if (!opts.dryRun) {
|
|
313
399
|
await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
|
|
314
400
|
}
|
|
@@ -344,6 +430,9 @@ export async function runEnsureInstalled(args) {
|
|
|
344
430
|
}
|
|
345
431
|
if (!(await dirExists(paths.pythonpath))) reasons.push(`missing ${paths.pythonpath}`);
|
|
346
432
|
if (!(await dirExists(paths.agents))) reasons.push(`missing agents dir ${paths.agents}`);
|
|
433
|
+
if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
|
|
434
|
+
reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
|
|
435
|
+
}
|
|
347
436
|
|
|
348
437
|
if (reasons.length === 0) {
|
|
349
438
|
if (!quiet) process.stdout.write(`okstra runtime OK (package ${paths.package})\n`);
|
package/src/paths.mjs
CHANGED
|
@@ -14,8 +14,8 @@ Usage:
|
|
|
14
14
|
Fields:
|
|
15
15
|
agents Absolute path to agents/. Default: <workspace>/agents.
|
|
16
16
|
workspace Directory containing prompts/, templates/, validators/,
|
|
17
|
-
agents/. Equals
|
|
18
|
-
or the dev-link repo path in dev mode.
|
|
17
|
+
agents/. Equals the package's runtime/ directory in copy
|
|
18
|
+
mode, or the dev-link repo path in dev mode.
|
|
19
19
|
pythonpath $HOME/.okstra/lib/python
|
|
20
20
|
bin $HOME/.okstra/bin
|
|
21
21
|
home $HOME/.okstra
|
package/src/uninstall.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { resolvePaths } from "./paths.mjs";
|
|
4
5
|
|
|
@@ -11,13 +12,35 @@ const BIN_ENTRYPOINTS = [
|
|
|
11
12
|
"okstra-error-log.py",
|
|
12
13
|
];
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const FALLBACK_SKILL_NAMES = [
|
|
16
|
+
"okstra-setup",
|
|
17
|
+
"okstra-run",
|
|
18
|
+
"okstra-status",
|
|
19
|
+
"okstra-history",
|
|
20
|
+
"okstra-schedule",
|
|
21
|
+
"okstra-context-loader",
|
|
22
|
+
"okstra-team-contract",
|
|
23
|
+
"okstra-convergence",
|
|
24
|
+
"okstra-report-writer",
|
|
25
|
+
"okstra-report-finder",
|
|
26
|
+
"okstra-time-summary",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
30
|
+
const SKILLS_MANIFEST_REL = "installed-skills.json";
|
|
31
|
+
|
|
32
|
+
const USAGE = `okstra uninstall — remove installed runtime from ~/.okstra and ~/.claude/skills
|
|
15
33
|
|
|
16
34
|
Usage:
|
|
17
|
-
okstra uninstall Remove lib
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
okstra uninstall Remove ~/.okstra/{lib, bin/<known>, version,
|
|
36
|
+
dev-link, installed-skills.json} AND
|
|
37
|
+
~/.claude/skills/<name> for every entry in
|
|
38
|
+
the install manifest (fallback: hard-coded
|
|
39
|
+
okstra-* names). Preserves user data:
|
|
40
|
+
recent.jsonl, active.jsonl, projects/,
|
|
41
|
+
archive/, state.json, .locks/
|
|
42
|
+
okstra uninstall --purge Remove the entire ~/.okstra directory AND
|
|
43
|
+
~/.claude/skills/<okstra-*> (DESTRUCTIVE).
|
|
21
44
|
Requires -y or an interactive confirmation
|
|
22
45
|
okstra uninstall --dry-run Print the plan without touching disk
|
|
23
46
|
okstra uninstall -y Skip confirmation prompt for --purge
|
|
@@ -79,7 +102,9 @@ export async function runUninstall(args) {
|
|
|
79
102
|
|
|
80
103
|
if (opts.purge) {
|
|
81
104
|
if (!opts.yes && !opts.dryRun) {
|
|
82
|
-
const ok = await promptConfirm(
|
|
105
|
+
const ok = await promptConfirm(
|
|
106
|
+
`purge entire ${paths.home} AND ~/.claude/skills/{okstra-*,okstra-setup}? user data will be lost.`,
|
|
107
|
+
);
|
|
83
108
|
if (!ok) {
|
|
84
109
|
process.stdout.write("aborted.\n");
|
|
85
110
|
return 1;
|
|
@@ -87,6 +112,10 @@ export async function runUninstall(args) {
|
|
|
87
112
|
}
|
|
88
113
|
if (!opts.quiet) process.stdout.write(`purging ${paths.home}\n`);
|
|
89
114
|
await removePath(paths.home, opts);
|
|
115
|
+
// Skills live outside ~/.okstra — purge those too with the fallback list.
|
|
116
|
+
for (const name of FALLBACK_SKILL_NAMES) {
|
|
117
|
+
await removePath(join(CLAUDE_SKILLS_DIR, name), opts);
|
|
118
|
+
}
|
|
90
119
|
return 0;
|
|
91
120
|
}
|
|
92
121
|
|
|
@@ -112,6 +141,19 @@ export async function runUninstall(args) {
|
|
|
112
141
|
}
|
|
113
142
|
}
|
|
114
143
|
}
|
|
144
|
+
|
|
145
|
+
// Remove the skills we own. Manifest is authoritative; fall back to the
|
|
146
|
+
// hard-coded okstra-* names if it is missing (e.g. an install from an old
|
|
147
|
+
// version that did not write the manifest).
|
|
148
|
+
const skillNames = await readSkillsManifest(paths.home);
|
|
149
|
+
if (!opts.quiet) {
|
|
150
|
+
process.stdout.write(` skills: removing ${skillNames.length} entries from ${CLAUDE_SKILLS_DIR}\n`);
|
|
151
|
+
}
|
|
152
|
+
for (const name of skillNames) {
|
|
153
|
+
await removePath(join(CLAUDE_SKILLS_DIR, name), opts);
|
|
154
|
+
}
|
|
155
|
+
await removePath(join(paths.home, SKILLS_MANIFEST_REL), opts);
|
|
156
|
+
|
|
115
157
|
await removePath(join(paths.home, "version"), opts);
|
|
116
158
|
await removePath(join(paths.home, "dev-link"), opts);
|
|
117
159
|
|
|
@@ -120,3 +162,14 @@ export async function runUninstall(args) {
|
|
|
120
162
|
}
|
|
121
163
|
return 0;
|
|
122
164
|
}
|
|
165
|
+
|
|
166
|
+
async function readSkillsManifest(home) {
|
|
167
|
+
try {
|
|
168
|
+
const raw = await fs.readFile(join(home, SKILLS_MANIFEST_REL), "utf8");
|
|
169
|
+
const data = JSON.parse(raw);
|
|
170
|
+
if (Array.isArray(data?.skills)) return data.skills;
|
|
171
|
+
} catch {
|
|
172
|
+
/* fall through */
|
|
173
|
+
}
|
|
174
|
+
return FALLBACK_SKILL_NAMES;
|
|
175
|
+
}
|