@minhpnq1807/contextos 0.5.23 → 0.5.25
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/CHANGELOG.md +10 -0
- package/bin/ctx.js +34 -16
- package/package.json +1 -1
- package/plugins/ctx/lib/antigravity-hooks.js +11 -2
- package/plugins/ctx/lib/antigravity-mcp.js +6 -1
- package/plugins/ctx/lib/claude-hooks.js +6 -1
- package/plugins/ctx/lib/global-hooks.js +13 -4
- package/plugins/ctx/lib/multi-select.js +142 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.25
|
|
4
|
+
|
|
5
|
+
- **Fix Windows JSON parse crash:** All `readJsonFile`/`readHooksFile` helpers now catch corrupt JSON and warn instead of crashing, allowing fresh config to be generated automatically.
|
|
6
|
+
- **Fix Windows shell quoting:** `shellQuote` now uses double-quotes on Windows (`process.platform === "win32"`) instead of POSIX single-quotes which are not recognized by cmd.exe/PowerShell.
|
|
7
|
+
- **Fix Codex CLI invocation on Windows:** `runCodex`/`tryRunCodex` now pass `shell: true` to `execFileSync` so Windows can resolve `codex.cmd` via PATH.
|
|
8
|
+
|
|
9
|
+
## 0.5.24
|
|
10
|
+
|
|
11
|
+
- **Interactive agent selection:** Replaces the comma-separated text input in `ctx setup` with an interactive multi-select prompt — use ↑/↓ to navigate, Space to toggle agents on/off, and Enter to confirm.
|
|
12
|
+
|
|
3
13
|
## 0.5.23
|
|
4
14
|
|
|
5
15
|
- **Fix Windows install paths:** Replaces all `process.env.HOME || process.cwd()` fallbacks with `os.homedir()` across `ctx.js`, `claude-hooks.js`, `antigravity-hooks.js`, `claude-mcp.js`, `antigravity-mcp.js`, and `ruler-sync.js`. On Windows, `HOME` is not set, causing `.codex/`, `.claude/`, and `.gemini/` directories (with full `node_modules` and source code) to be created inside the project tree instead of the user's home directory.
|
package/bin/ctx.js
CHANGED
|
@@ -30,6 +30,7 @@ import { syncSkills } from "../plugins/ctx/lib/skillshare-sync.js";
|
|
|
30
30
|
import { scanSkills, warmSkillEmbeddings } from "../plugins/ctx/lib/skill-discoverer.js";
|
|
31
31
|
import { parsePassthroughArgs, runPassthrough } from "../plugins/ctx/lib/passthrough.js";
|
|
32
32
|
import { parseAgentList, parseSetupArgs, setupSummaryLines } from "../plugins/ctx/lib/setup-wizard.js";
|
|
33
|
+
import { multiSelect } from "../plugins/ctx/lib/multi-select.js";
|
|
33
34
|
import { syncWorkflows, warmWorkflowEmbeddings } from "../plugins/ctx/lib/workflow-discoverer.js";
|
|
34
35
|
|
|
35
36
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -331,7 +332,7 @@ async function warmInstallEmbeddings() {
|
|
|
331
332
|
|
|
332
333
|
function tryRunCodex(args) {
|
|
333
334
|
try {
|
|
334
|
-
execFileSync("codex", args, { stdio: "ignore" });
|
|
335
|
+
execFileSync("codex", args, { stdio: "ignore", shell: true });
|
|
335
336
|
} catch {
|
|
336
337
|
// Best effort cleanup for repeat installs.
|
|
337
338
|
}
|
|
@@ -339,7 +340,7 @@ function tryRunCodex(args) {
|
|
|
339
340
|
|
|
340
341
|
function runCodex(args) {
|
|
341
342
|
try {
|
|
342
|
-
execFileSync("codex", args, { stdio: "inherit" });
|
|
343
|
+
execFileSync("codex", args, { stdio: "inherit", shell: true });
|
|
343
344
|
} catch (error) {
|
|
344
345
|
const status = typeof error.status === "number" ? error.status : 1;
|
|
345
346
|
throw new Error(`codex ${args.join(" ")} failed with exit code ${status}. Make sure Codex CLI is installed and authenticated.`);
|
|
@@ -493,23 +494,40 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
493
494
|
|
|
494
495
|
if (interactive) {
|
|
495
496
|
const rl = readline.createInterface({ input, output });
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
497
|
+
const proceed = await askSetupYesNo(rl, "Install to this directory?", true);
|
|
498
|
+
if (!proceed) {
|
|
499
|
+
rl.close();
|
|
500
|
+
console.log("Setup cancelled.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (!options.agentsProvided) {
|
|
504
|
+
rl.close();
|
|
505
|
+
const selected = await multiSelect({
|
|
506
|
+
message: "Select agents to install:",
|
|
507
|
+
options: [
|
|
508
|
+
{ label: "Codex", value: "codex", selected: options.agents.includes("codex") },
|
|
509
|
+
{ label: "Claude", value: "claude", selected: options.agents.includes("claude") },
|
|
510
|
+
{ label: "Antigravity (agy)", value: "agy", selected: options.agents.includes("agy") }
|
|
511
|
+
]
|
|
512
|
+
});
|
|
513
|
+
options.agents = selected;
|
|
514
|
+
const rl2 = readline.createInterface({ input, output });
|
|
515
|
+
try {
|
|
516
|
+
options.inject = await askSetupYesNo(rl2, "Enable prompt context injection?", options.inject);
|
|
517
|
+
options.syncRules = await askSetupYesNo(rl2, "Sync project rules and MCP servers through Ruler?", options.syncRules);
|
|
518
|
+
options.syncSkills = await askSetupYesNo(rl2, "Sync skills through skillshare?", options.syncSkills);
|
|
519
|
+
} finally {
|
|
520
|
+
rl2.close();
|
|
501
521
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
options.agents = parseAgentList(agents);
|
|
505
|
-
} else {
|
|
522
|
+
} else {
|
|
523
|
+
try {
|
|
506
524
|
console.log(`◇ Install for agents:\n│ ${options.agents.join(", ")}`);
|
|
525
|
+
options.inject = await askSetupYesNo(rl, "Enable prompt context injection?", options.inject);
|
|
526
|
+
options.syncRules = await askSetupYesNo(rl, "Sync project rules and MCP servers through Ruler?", options.syncRules);
|
|
527
|
+
options.syncSkills = await askSetupYesNo(rl, "Sync skills through skillshare?", options.syncSkills);
|
|
528
|
+
} finally {
|
|
529
|
+
rl.close();
|
|
507
530
|
}
|
|
508
|
-
options.inject = await askSetupYesNo(rl, "Enable prompt context injection?", options.inject);
|
|
509
|
-
options.syncRules = await askSetupYesNo(rl, "Sync project rules and MCP servers through Ruler?", options.syncRules);
|
|
510
|
-
options.syncSkills = await askSetupYesNo(rl, "Sync skills through skillshare?", options.syncSkills);
|
|
511
|
-
} finally {
|
|
512
|
-
rl.close();
|
|
513
531
|
}
|
|
514
532
|
}
|
|
515
533
|
|
package/package.json
CHANGED
|
@@ -3,14 +3,23 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
function shellQuote(value) {
|
|
6
|
-
|
|
6
|
+
const s = String(value);
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
return `"${s.replaceAll('"', '\\"')}"`;
|
|
9
|
+
}
|
|
10
|
+
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
function readJsonFile(filePath, fallback) {
|
|
10
14
|
if (!fs.existsSync(filePath)) return fallback;
|
|
11
15
|
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
12
16
|
if (!raw) return fallback;
|
|
13
|
-
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
console.warn(`[ctx] warning: corrupt JSON in ${filePath}, overwriting with defaults`);
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
function commandFor(installRoot, scriptName, { injectPromptContext = true } = {}) {
|
|
@@ -6,7 +6,12 @@ function readJsonFile(filePath, fallback) {
|
|
|
6
6
|
if (!fs.existsSync(filePath)) return fallback;
|
|
7
7
|
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
8
8
|
if (!raw) return fallback;
|
|
9
|
-
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
console.warn(`[ctx] warning: corrupt JSON in ${filePath}, overwriting with defaults`);
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export function antigravityMcpConfigPaths() {
|
|
@@ -8,7 +8,12 @@ function readJsonFile(filePath, fallback) {
|
|
|
8
8
|
if (!fs.existsSync(filePath)) return fallback;
|
|
9
9
|
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
10
10
|
if (!raw) return fallback;
|
|
11
|
-
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
console.warn(`[ctx] warning: corrupt JSON in ${filePath}, overwriting with defaults`);
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export function claudeHome() {
|
|
@@ -6,16 +6,25 @@ const QUIET_CODE_REVIEW_GRAPH_STATUS_COMMAND =
|
|
|
6
6
|
"git rev-parse --git-dir >/dev/null 2>&1 && code-review-graph status >/dev/null 2>&1 || true";
|
|
7
7
|
|
|
8
8
|
function shellQuote(value) {
|
|
9
|
-
|
|
9
|
+
const s = String(value);
|
|
10
|
+
if (process.platform === "win32") {
|
|
11
|
+
return `"${s.replaceAll('"', '\\"')}"`;
|
|
12
|
+
}
|
|
13
|
+
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
function readHooksFile(hooksPath) {
|
|
13
17
|
if (!fs.existsSync(hooksPath)) return { hooks: {} };
|
|
14
18
|
const raw = fs.readFileSync(hooksPath, "utf8").trim();
|
|
15
19
|
if (!raw) return { hooks: {} };
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (!parsed.hooks || typeof parsed.hooks !== "object") parsed.hooks = {};
|
|
23
|
+
return parsed;
|
|
24
|
+
} catch {
|
|
25
|
+
console.warn(`[ctx] warning: corrupt JSON in ${hooksPath}, overwriting with defaults`);
|
|
26
|
+
return { hooks: {} };
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
function isContextOSHookEntry(entry) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive multi-select prompt using raw stdin.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* const selected = await multiSelect({
|
|
6
|
+
* message: "Select agents:",
|
|
7
|
+
* options: [
|
|
8
|
+
* { label: "Codex", value: "codex", selected: true },
|
|
9
|
+
* { label: "Claude", value: "claude", selected: true },
|
|
10
|
+
* { label: "Antigravity (agy)", value: "agy", selected: true }
|
|
11
|
+
* ]
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const KEYS = {
|
|
16
|
+
UP: ["\x1B[A", "\x1Bk"], // Arrow Up, Alt+k
|
|
17
|
+
DOWN: ["\x1B[B", "\x1Bj"], // Arrow Down, Alt+j
|
|
18
|
+
SPACE: [" "],
|
|
19
|
+
ENTER: ["\r", "\n"],
|
|
20
|
+
CTRL_C: ["\x03"],
|
|
21
|
+
// j/k vim-style (single char)
|
|
22
|
+
J: ["j"],
|
|
23
|
+
K: ["k"]
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DIM = "\x1B[2m";
|
|
27
|
+
const RESET = "\x1B[0m";
|
|
28
|
+
const CYAN = "\x1B[36m";
|
|
29
|
+
const GREEN = "\x1B[32m";
|
|
30
|
+
const BOLD = "\x1B[1m";
|
|
31
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
32
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
33
|
+
|
|
34
|
+
function matchKey(data, keySet) {
|
|
35
|
+
return keySet.some((k) => data === k);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{ message: string, options: Array<{label: string, value: string, selected?: boolean}> }} config
|
|
40
|
+
* @returns {Promise<string[]>} selected values
|
|
41
|
+
*/
|
|
42
|
+
export function multiSelect({ message, options }) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
if (!process.stdin.isTTY) {
|
|
45
|
+
// Non-interactive: return all pre-selected
|
|
46
|
+
resolve(options.filter((o) => o.selected).map((o) => o.value));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let cursor = 0;
|
|
51
|
+
const selections = options.map((o) => o.selected !== false);
|
|
52
|
+
|
|
53
|
+
function render() {
|
|
54
|
+
// Move cursor up to overwrite previous render (except first time)
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push(`${CYAN}◇${RESET} ${message}`);
|
|
57
|
+
lines.push(`${DIM} Use ↑/↓ to navigate, Space to toggle, Enter to confirm${RESET}`);
|
|
58
|
+
for (let i = 0; i < options.length; i++) {
|
|
59
|
+
const isCursor = i === cursor;
|
|
60
|
+
const isSelected = selections[i];
|
|
61
|
+
const checkbox = isSelected ? `${GREEN}◉${RESET}` : `${DIM}○${RESET}`;
|
|
62
|
+
const label = isCursor ? `${BOLD}${CYAN}${options[i].label}${RESET}` : options[i].label;
|
|
63
|
+
const pointer = isCursor ? `${CYAN}❯${RESET}` : " ";
|
|
64
|
+
lines.push(` ${pointer} ${checkbox} ${label}`);
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let prevLineCount = 0;
|
|
70
|
+
|
|
71
|
+
function draw() {
|
|
72
|
+
const lines = render();
|
|
73
|
+
// Clear previous output
|
|
74
|
+
if (prevLineCount > 0) {
|
|
75
|
+
process.stdout.write(`\x1B[${prevLineCount}A`); // move up
|
|
76
|
+
for (let i = 0; i < prevLineCount; i++) {
|
|
77
|
+
process.stdout.write("\x1B[2K"); // clear line
|
|
78
|
+
if (i < prevLineCount - 1) process.stdout.write("\x1B[1B"); // move down
|
|
79
|
+
}
|
|
80
|
+
process.stdout.write(`\x1B[${prevLineCount - 1}A`); // back to top
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
83
|
+
prevLineCount = lines.length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
process.stdout.write(HIDE_CURSOR);
|
|
87
|
+
|
|
88
|
+
const wasRaw = process.stdin.isRaw;
|
|
89
|
+
process.stdin.setRawMode(true);
|
|
90
|
+
process.stdin.resume();
|
|
91
|
+
process.stdin.setEncoding("utf8");
|
|
92
|
+
|
|
93
|
+
function cleanup() {
|
|
94
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
95
|
+
process.stdin.pause();
|
|
96
|
+
process.stdin.removeListener("data", onData);
|
|
97
|
+
process.stdout.write(SHOW_CURSOR);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function onData(data) {
|
|
101
|
+
if (matchKey(data, KEYS.CTRL_C)) {
|
|
102
|
+
cleanup();
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (matchKey(data, KEYS.UP) || matchKey(data, KEYS.K)) {
|
|
107
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
108
|
+
draw();
|
|
109
|
+
} else if (matchKey(data, KEYS.DOWN) || matchKey(data, KEYS.J)) {
|
|
110
|
+
cursor = (cursor + 1) % options.length;
|
|
111
|
+
draw();
|
|
112
|
+
} else if (matchKey(data, KEYS.SPACE)) {
|
|
113
|
+
selections[cursor] = !selections[cursor];
|
|
114
|
+
draw();
|
|
115
|
+
} else if (matchKey(data, KEYS.ENTER)) {
|
|
116
|
+
cleanup();
|
|
117
|
+
const selected = options
|
|
118
|
+
.filter((_, i) => selections[i])
|
|
119
|
+
.map((o) => o.value);
|
|
120
|
+
// Print final summary
|
|
121
|
+
const summary = selected.length > 0
|
|
122
|
+
? selected.join(", ")
|
|
123
|
+
: "(none)";
|
|
124
|
+
// Overwrite prompt area with final state
|
|
125
|
+
if (prevLineCount > 0) {
|
|
126
|
+
process.stdout.write(`\x1B[${prevLineCount}A`);
|
|
127
|
+
for (let i = 0; i < prevLineCount; i++) {
|
|
128
|
+
process.stdout.write("\x1B[2K");
|
|
129
|
+
if (i < prevLineCount - 1) process.stdout.write("\x1B[1B");
|
|
130
|
+
}
|
|
131
|
+
process.stdout.write(`\x1B[${prevLineCount - 1}A`);
|
|
132
|
+
}
|
|
133
|
+
process.stdout.write(`${CYAN}◇${RESET} ${message}\n`);
|
|
134
|
+
process.stdout.write(`${DIM}│${RESET} ${GREEN}${summary}${RESET}\n`);
|
|
135
|
+
resolve(selected);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
process.stdin.on("data", onData);
|
|
140
|
+
draw();
|
|
141
|
+
});
|
|
142
|
+
}
|