okstra 0.3.0 → 0.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.kr.md +157 -0
- package/README.md +103 -1245
- package/bin/okstra +23 -6
- package/docs/kr/architecture.md +900 -0
- package/docs/kr/cli.md +400 -0
- package/package.json +4 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/skills/okstra-history/SKILL.md +19 -0
- package/runtime/skills/okstra-report-finder/SKILL.md +19 -0
- package/runtime/skills/okstra-run/SKILL.md +11 -3
- package/runtime/skills/okstra-schedule/SKILL.md +22 -0
- package/runtime/skills/{setup-okstra → okstra-setup}/SKILL.md +3 -3
- package/runtime/skills/okstra-status/SKILL.md +22 -0
- package/runtime/skills/okstra-time-summary/SKILL.md +19 -0
- package/src/check-project.mjs +188 -0
- package/src/doctor.mjs +1 -1
- package/src/install.mjs +16 -2
- package/src/setup.mjs +243 -0
- package/src/uninstall.mjs +2 -2
|
@@ -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
|
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { resolvePaths } from "./paths.mjs";
|
|
6
6
|
|
|
7
7
|
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
8
|
-
const REQUIRED_SKILLS = ["setup
|
|
8
|
+
const REQUIRED_SKILLS = ["okstra-setup", "okstra-run"];
|
|
9
9
|
|
|
10
10
|
const USAGE = `okstra doctor — diagnose the installed runtime
|
|
11
11
|
|
package/src/install.mjs
CHANGED
|
@@ -226,6 +226,13 @@ async function installLinkMode(repoPath, paths, opts) {
|
|
|
226
226
|
process.stdout.write(` dev-link stamp: ${repoAbs}\n`);
|
|
227
227
|
process.stdout.write(` version stamp: ${paths.package}\n`);
|
|
228
228
|
process.stdout.write("done.\n");
|
|
229
|
+
process.stdout.write(
|
|
230
|
+
"\nNext step: register the current project.\n" +
|
|
231
|
+
" cd <your project> && npx -y okstra@latest setup --project-id <id>\n" +
|
|
232
|
+
" (or run /okstra-setup inside a Claude Code session)\n" +
|
|
233
|
+
"\nTip: to use a bare 'okstra' command instead of npx, run:\n" +
|
|
234
|
+
" npm i -g okstra\n",
|
|
235
|
+
);
|
|
229
236
|
}
|
|
230
237
|
return 0;
|
|
231
238
|
}
|
|
@@ -401,6 +408,13 @@ export async function runInstall(args) {
|
|
|
401
408
|
if (!opts.quiet) {
|
|
402
409
|
process.stdout.write(` version stamp: ${paths.package}\n`);
|
|
403
410
|
process.stdout.write("done.\n");
|
|
411
|
+
process.stdout.write(
|
|
412
|
+
"\nNext step: register the current project.\n" +
|
|
413
|
+
" cd <your project> && npx -y okstra@latest setup --project-id <id>\n" +
|
|
414
|
+
" (or run /okstra-setup inside a Claude Code session)\n" +
|
|
415
|
+
"\nTip: to use a bare 'okstra' command instead of npx, run:\n" +
|
|
416
|
+
" npm i -g okstra\n",
|
|
417
|
+
);
|
|
404
418
|
}
|
|
405
419
|
return 0;
|
|
406
420
|
}
|
|
@@ -430,8 +444,8 @@ export async function runEnsureInstalled(args) {
|
|
|
430
444
|
}
|
|
431
445
|
if (!(await dirExists(paths.pythonpath))) reasons.push(`missing ${paths.pythonpath}`);
|
|
432
446
|
if (!(await dirExists(paths.agents))) reasons.push(`missing agents dir ${paths.agents}`);
|
|
433
|
-
if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "setup
|
|
434
|
-
reasons.push(`missing ${CLAUDE_SKILLS_DIR}/setup
|
|
447
|
+
if (!(await fileExists(join(CLAUDE_SKILLS_DIR, "okstra-setup", "SKILL.md")))) {
|
|
448
|
+
reasons.push(`missing ${CLAUDE_SKILLS_DIR}/okstra-setup/SKILL.md`);
|
|
435
449
|
}
|
|
436
450
|
|
|
437
451
|
if (reasons.length === 0) {
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { resolvePaths } from "./paths.mjs";
|
|
5
|
+
|
|
6
|
+
const USAGE = `okstra setup — register the current project with okstra
|
|
7
|
+
|
|
8
|
+
Writes <PROJECT_ROOT>/.project-docs/okstra/project.json. This is the
|
|
9
|
+
project-level companion to 'okstra install' (which is machine-level).
|
|
10
|
+
Inside a Claude Code session this is also exposed as the /okstra-setup
|
|
11
|
+
slash command.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
okstra setup --project-id <id> Register with explicit projectId
|
|
15
|
+
okstra setup Use existing project.json or
|
|
16
|
+
prompt for projectId (TTY only)
|
|
17
|
+
okstra setup --project-root <path> Override PROJECT_ROOT resolution
|
|
18
|
+
okstra setup --yes Skip prompts; require all inputs
|
|
19
|
+
on the command line
|
|
20
|
+
|
|
21
|
+
Behavior:
|
|
22
|
+
- If project.json already exists, the projectId must match (okstra refuses
|
|
23
|
+
to silently rename a project). Delete the file manually if you really
|
|
24
|
+
want to change projectId.
|
|
25
|
+
- If --project-id is omitted and stdin is a TTY, you are prompted.
|
|
26
|
+
- If --project-id is omitted and stdin is not a TTY, the command exits
|
|
27
|
+
with an error (use --project-id for CI / scripts).
|
|
28
|
+
|
|
29
|
+
Exit codes:
|
|
30
|
+
0 project.json present and valid after the run
|
|
31
|
+
1 I/O / python failure or projectId mismatch
|
|
32
|
+
2 PROJECT_ROOT could not be resolved
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function runProcess(cmd, args, env) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const child = spawn(cmd, args, {
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
env: { ...process.env, ...env },
|
|
40
|
+
});
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
child.stdout.on("data", (b) => (stdout += b.toString()));
|
|
44
|
+
child.stderr.on("data", (b) => (stderr += b.toString()));
|
|
45
|
+
child.on("error", (err) => resolve({ code: -1, stdout, stderr: err.message }));
|
|
46
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseArgs(args) {
|
|
51
|
+
const opts = {
|
|
52
|
+
projectId: null,
|
|
53
|
+
projectRoot: null,
|
|
54
|
+
yes: false,
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const a = args[i];
|
|
58
|
+
if (a === "--yes" || a === "-y") opts.yes = true;
|
|
59
|
+
else if (a === "--project-id") {
|
|
60
|
+
const next = args[i + 1];
|
|
61
|
+
if (!next || next.startsWith("--")) throw new Error("--project-id requires a value");
|
|
62
|
+
opts.projectId = next;
|
|
63
|
+
i++;
|
|
64
|
+
} else if (a === "--project-root") {
|
|
65
|
+
const next = args[i + 1];
|
|
66
|
+
if (!next || next.startsWith("--")) throw new Error("--project-root requires a path");
|
|
67
|
+
opts.projectRoot = next;
|
|
68
|
+
i++;
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`unknown argument '${a}'`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return opts;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fileExists(p) {
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(p);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function prompt(question) {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
88
|
+
rl.question(question, (answer) => {
|
|
89
|
+
rl.close();
|
|
90
|
+
resolve(answer.trim());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateProjectId(id) {
|
|
96
|
+
if (!id) return "project-id is empty";
|
|
97
|
+
if (!/[A-Za-z0-9]/.test(id)) return "project-id must contain at least one alphanumeric character";
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveProjectRoot(paths, explicit) {
|
|
102
|
+
const probe = await runProcess(
|
|
103
|
+
"python3",
|
|
104
|
+
[
|
|
105
|
+
"-c",
|
|
106
|
+
[
|
|
107
|
+
"import sys",
|
|
108
|
+
"from okstra_project import resolve_project_root, project_json_path, ResolverError",
|
|
109
|
+
"try:",
|
|
110
|
+
" pr = resolve_project_root(explicit_root=sys.argv[1], cwd=sys.argv[2])",
|
|
111
|
+
" print('PROJECT_ROOT', pr)",
|
|
112
|
+
" print('PROJECT_JSON', project_json_path(pr))",
|
|
113
|
+
"except ResolverError as e:",
|
|
114
|
+
" print('RESOLVER_ERROR', e)",
|
|
115
|
+
].join("\n"),
|
|
116
|
+
explicit || "",
|
|
117
|
+
process.cwd(),
|
|
118
|
+
],
|
|
119
|
+
{ PYTHONPATH: paths.pythonpath },
|
|
120
|
+
);
|
|
121
|
+
if (probe.code !== 0) {
|
|
122
|
+
throw new Error(`python invocation failed: ${probe.stderr.trim() || probe.stdout.trim()}`);
|
|
123
|
+
}
|
|
124
|
+
const lines = probe.stdout.trim().split("\n");
|
|
125
|
+
const tagOf = (key) =>
|
|
126
|
+
lines
|
|
127
|
+
.find((l) => l.startsWith(key + " "))
|
|
128
|
+
?.slice(key.length + 1)
|
|
129
|
+
.trim() ?? null;
|
|
130
|
+
const resolverError = tagOf("RESOLVER_ERROR");
|
|
131
|
+
if (resolverError) {
|
|
132
|
+
const err = new Error(resolverError);
|
|
133
|
+
err.code = "RESOLVER";
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
projectRoot: tagOf("PROJECT_ROOT"),
|
|
138
|
+
projectJsonPath: tagOf("PROJECT_JSON"),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function upsert(paths, projectRoot, projectId) {
|
|
143
|
+
const probe = await runProcess(
|
|
144
|
+
"python3",
|
|
145
|
+
[
|
|
146
|
+
"-c",
|
|
147
|
+
[
|
|
148
|
+
"import json, sys",
|
|
149
|
+
"from pathlib import Path",
|
|
150
|
+
"from okstra_project import upsert_project_json, ResolverError",
|
|
151
|
+
"try:",
|
|
152
|
+
" result = upsert_project_json(Path(sys.argv[1]), sys.argv[2])",
|
|
153
|
+
" print('OK', json.dumps(result))",
|
|
154
|
+
"except ResolverError as e:",
|
|
155
|
+
" print('ERROR', e)",
|
|
156
|
+
].join("\n"),
|
|
157
|
+
projectRoot,
|
|
158
|
+
projectId,
|
|
159
|
+
],
|
|
160
|
+
{ PYTHONPATH: paths.pythonpath },
|
|
161
|
+
);
|
|
162
|
+
if (probe.code !== 0) {
|
|
163
|
+
throw new Error(`python invocation failed: ${probe.stderr.trim() || probe.stdout.trim()}`);
|
|
164
|
+
}
|
|
165
|
+
const out = probe.stdout.trim();
|
|
166
|
+
if (out.startsWith("OK ")) {
|
|
167
|
+
return JSON.parse(out.slice(3));
|
|
168
|
+
}
|
|
169
|
+
if (out.startsWith("ERROR ")) {
|
|
170
|
+
throw new Error(out.slice(6));
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`unexpected upsert output: ${out}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function run(args) {
|
|
176
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
177
|
+
process.stdout.write(USAGE);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let opts;
|
|
182
|
+
try {
|
|
183
|
+
opts = parseArgs(args);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
process.stderr.write(`error: ${err.message}\n\n${USAGE}`);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const paths = await resolvePaths();
|
|
190
|
+
|
|
191
|
+
let resolved;
|
|
192
|
+
try {
|
|
193
|
+
resolved = await resolveProjectRoot(paths, opts.projectRoot);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
process.stderr.write(`error: could not resolve PROJECT_ROOT: ${err.message}\n`);
|
|
196
|
+
return err.code === "RESOLVER" ? 2 : 1;
|
|
197
|
+
}
|
|
198
|
+
const { projectRoot, projectJsonPath } = resolved;
|
|
199
|
+
|
|
200
|
+
let existing = null;
|
|
201
|
+
if (await fileExists(projectJsonPath)) {
|
|
202
|
+
try {
|
|
203
|
+
existing = JSON.parse(await fs.readFile(projectJsonPath, "utf8"));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
process.stderr.write(`error: failed to parse ${projectJsonPath}: ${err.message}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let projectId = opts.projectId;
|
|
211
|
+
if (!projectId && existing?.projectId) {
|
|
212
|
+
projectId = existing.projectId;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!projectId) {
|
|
216
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
217
|
+
process.stderr.write(
|
|
218
|
+
`error: --project-id is required (no existing project.json, not a TTY)\n`,
|
|
219
|
+
);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
process.stderr.write(`PROJECT_ROOT: ${projectRoot}\n`);
|
|
223
|
+
const answer = await prompt("project-id (e.g. INV-1234, fontsninja): ");
|
|
224
|
+
projectId = answer;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const invalid = validateProjectId(projectId);
|
|
228
|
+
if (invalid) {
|
|
229
|
+
process.stderr.write(`error: ${invalid}\n`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let result;
|
|
234
|
+
try {
|
|
235
|
+
result = await upsert(paths, projectRoot, projectId);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
process.stderr.write(`error: ${err.message}\n`);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
process.stdout.write(JSON.stringify({ ok: true, ...result, projectJsonPath }, null, 2) + "\n");
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
package/src/uninstall.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const BIN_ENTRYPOINTS = [
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
const FALLBACK_SKILL_NAMES = [
|
|
16
|
-
"setup
|
|
16
|
+
"okstra-setup",
|
|
17
17
|
"okstra-run",
|
|
18
18
|
"okstra-status",
|
|
19
19
|
"okstra-history",
|
|
@@ -103,7 +103,7 @@ export async function runUninstall(args) {
|
|
|
103
103
|
if (opts.purge) {
|
|
104
104
|
if (!opts.yes && !opts.dryRun) {
|
|
105
105
|
const ok = await promptConfirm(
|
|
106
|
-
`purge entire ${paths.home} AND ~/.claude/skills/{okstra-*,setup
|
|
106
|
+
`purge entire ${paths.home} AND ~/.claude/skills/{okstra-*,okstra-setup}? user data will be lost.`,
|
|
107
107
|
);
|
|
108
108
|
if (!ok) {
|
|
109
109
|
process.stdout.write("aborted.\n");
|