@minhpnq1807/contextos 0.5.32 → 0.5.34
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 +7 -0
- package/bin/ctx.js +31 -17
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/ruler-sync.js +41 -2
- package/plugins/ctx/lib/skillshare-sync.js +65 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.34
|
|
4
|
+
|
|
5
|
+
- **Real-time streaming output during install/setup:** Replaced `captureSetupOutput` (buffered) with `streamSetupOutput` — now prints each line immediately with `│ ` prefix as it arrives, eliminating the perceived "hang" during long-running downloads and installs.
|
|
6
|
+
- **Fix codex CLI output missing `│` prefix:** Changed `runCodex` from `stdio: "inherit"` to `stdio: ["ignore", "pipe", "pipe"]`. Output now flows through `console.log` → `streamSetupOutput` → `│ ` prefix, ensuring lines like "Added marketplace..." are consistently formatted.
|
|
7
|
+
- **Async streaming for skillshare/ruler install:** Replaced blocking `execSync`/`runShell` calls in `installSkillshare` and `installRuler` with async `spawn` + line-by-line streaming. Download progress from PowerShell/curl/npm is now visible in real time instead of being buffered until completion.
|
|
8
|
+
- **Fix `skillshare init` hang on Windows:** `skillshare init` is interactive by default (prompts for copy source, git, skill install). With stdin routed to NUL (deadlock prevention), the Go binary hangs waiting for terminal input that never arrives. Fixed by passing `--no-copy --no-git --no-skill --all-targets` flags for fully non-interactive initialization.
|
|
9
|
+
|
|
3
10
|
## 0.5.32
|
|
4
11
|
|
|
5
12
|
- **Fix Windows terminal hang during skillshare/ruler install:** `execSync` with `stdio: "pipe"` creates a stdin pipe whose write-end is held by Node while it blocks on `waitpid`. If the child process (PowerShell installer, npm, etc.) reads from stdin, it blocks waiting for data/EOF that never comes — classic deadlock. Fixed by normalizing `stdio: "pipe"` to `["ignore", "pipe", "pipe"]` in both `runCommand` and `runShell`. This routes stdin to NUL (`/dev/null`) for immediate EOF, while still capturing stdout/stderr through pipes for `◇`/`│` formatting.
|
package/bin/ctx.js
CHANGED
|
@@ -97,19 +97,23 @@ function normalizeInstallAgent(agent) {
|
|
|
97
97
|
if (normalized === "antigravity") return "agy";
|
|
98
98
|
return normalized;
|
|
99
99
|
}
|
|
100
|
-
|
|
101
100
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
101
|
+
* Intercept console.log and process.stderr.write from an async fn,
|
|
102
|
+
* printing each line immediately with "│ " prefix for real-time feedback.
|
|
103
|
+
* Returns the collected lines array (for callers that inspect it).
|
|
104
104
|
*/
|
|
105
|
-
async function
|
|
105
|
+
async function streamSetupOutput(fn) {
|
|
106
106
|
const lines = [];
|
|
107
107
|
const origLog = console.log;
|
|
108
108
|
const origStderrWrite = process.stderr.write;
|
|
109
|
-
|
|
109
|
+
const emit = (text) => {
|
|
110
|
+
lines.push(text);
|
|
111
|
+
origLog(`│ ${text}`);
|
|
112
|
+
};
|
|
113
|
+
console.log = (...args) => emit(args.map(String).join(" "));
|
|
110
114
|
process.stderr.write = (chunk) => {
|
|
111
115
|
const text = String(chunk).replace(/\r/g, "").trim();
|
|
112
|
-
if (text)
|
|
116
|
+
if (text) emit(text);
|
|
113
117
|
return true;
|
|
114
118
|
};
|
|
115
119
|
try {
|
|
@@ -363,8 +367,23 @@ function tryRunCodex(args) {
|
|
|
363
367
|
|
|
364
368
|
function runCodex(args) {
|
|
365
369
|
try {
|
|
366
|
-
execFileSync("codex", args, {
|
|
370
|
+
const stdout = execFileSync("codex", args, {
|
|
371
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
372
|
+
encoding: "utf8",
|
|
373
|
+
shell: true
|
|
374
|
+
});
|
|
375
|
+
if (stdout && stdout.trim()) {
|
|
376
|
+
for (const line of stdout.trim().split(/\r?\n/)) {
|
|
377
|
+
console.log(line);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
367
380
|
} catch (error) {
|
|
381
|
+
// Log any output captured before the error
|
|
382
|
+
if (error.stdout && error.stdout.trim()) {
|
|
383
|
+
for (const line of error.stdout.trim().split(/\r?\n/)) {
|
|
384
|
+
console.log(line);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
368
387
|
const status = typeof error.status === "number" ? error.status : 1;
|
|
369
388
|
throw new Error(`codex ${args.join(" ")} failed with exit code ${status}. Make sure Codex CLI is installed and authenticated.`);
|
|
370
389
|
}
|
|
@@ -561,8 +580,7 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
561
580
|
|
|
562
581
|
for (const agent of options.agents) {
|
|
563
582
|
console.log(`◇ Setting up ${agent}...`);
|
|
564
|
-
|
|
565
|
-
for (const line of lines) console.log(`│ ${line}`);
|
|
583
|
+
await streamSetupOutput(() => install({ agent, copy: false }));
|
|
566
584
|
}
|
|
567
585
|
|
|
568
586
|
if (options.syncRules) {
|
|
@@ -570,8 +588,7 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
570
588
|
const syncAgents = options.agents.map((agent) => agent === "agy" ? "antigravity" : agent).join(",");
|
|
571
589
|
const syncArgs = ["--rules", "--agents", syncAgents];
|
|
572
590
|
if (options.yes) syncArgs.push("--yes");
|
|
573
|
-
|
|
574
|
-
for (const line of lines) console.log(`│ ${line}`);
|
|
591
|
+
await streamSetupOutput(() => syncRules({ cwd, rootDir, args: syncArgs }));
|
|
575
592
|
}
|
|
576
593
|
|
|
577
594
|
if (options.syncSkills) {
|
|
@@ -579,7 +596,7 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
579
596
|
const skillAgents = options.agents.map((agent) => agent === "agy" ? "antigravity" : agent).join(",");
|
|
580
597
|
const syncArgs = ["--skills", "--agents", skillAgents];
|
|
581
598
|
if (options.yes) syncArgs.push("--yes");
|
|
582
|
-
|
|
599
|
+
await streamSetupOutput(() => syncSkills({
|
|
583
600
|
cwd,
|
|
584
601
|
args: syncArgs,
|
|
585
602
|
rebuildSkillEmbeddings: async ({ cwd: skillCwd, sourceDir }) => warmSkillEmbeddings({
|
|
@@ -589,7 +606,6 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
589
606
|
skills: scanSkills({ cwd: skillCwd, roots: [sourceDir] })
|
|
590
607
|
})
|
|
591
608
|
}));
|
|
592
|
-
for (const line of lines) console.log(`│ ${line}`);
|
|
593
609
|
}
|
|
594
610
|
|
|
595
611
|
console.log("");
|
|
@@ -620,8 +636,7 @@ try {
|
|
|
620
636
|
if (explicitAgent) {
|
|
621
637
|
// Direct mode: ctx install --agent <name>
|
|
622
638
|
console.log(`◇ Installing ${explicitAgent}...`);
|
|
623
|
-
|
|
624
|
-
for (const line of lines) console.log(`│ ${line}`);
|
|
639
|
+
await streamSetupOutput(() => install({ copy, agent: explicitAgent }));
|
|
625
640
|
console.log("");
|
|
626
641
|
} else {
|
|
627
642
|
// Interactive mode: ctx install
|
|
@@ -634,8 +649,7 @@ try {
|
|
|
634
649
|
} else {
|
|
635
650
|
for (const agent of selected) {
|
|
636
651
|
console.log(`◇ Installing ${agent}...`);
|
|
637
|
-
|
|
638
|
-
for (const line of lines) console.log(`│ ${line}`);
|
|
652
|
+
await streamSetupOutput(() => install({ copy, agent }));
|
|
639
653
|
console.log("");
|
|
640
654
|
}
|
|
641
655
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
-
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
import { defaultDataRoot } from "./workspace-data.js";
|
|
9
9
|
|
|
@@ -98,7 +98,46 @@ export async function installRuler({ run = runCommand, yes = false, dryRun = fal
|
|
|
98
98
|
if (!accepted) {
|
|
99
99
|
throw new Error("Ruler is required for ctx sync --rules. Install it with `npm install -g @intellectronica/ruler` or rerun with --yes.");
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
if (dryRun) {
|
|
102
|
+
run("npm", ["install", "-g", "@intellectronica/ruler"], { stdio: "pipe", dryRun });
|
|
103
|
+
} else {
|
|
104
|
+
console.log("Installing ruler...");
|
|
105
|
+
await spawnCommand("npm", ["install", "-g", "@intellectronica/ruler"]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Spawn a child process and stream stdout/stderr line-by-line in real time.
|
|
111
|
+
* stdin is closed immediately to prevent deadlocks on Windows.
|
|
112
|
+
*/
|
|
113
|
+
function spawnCommand(command, args = []) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const child = spawn(command, args, {
|
|
116
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
117
|
+
shell: true
|
|
118
|
+
});
|
|
119
|
+
const streamLines = (stream) => {
|
|
120
|
+
let buffer = "";
|
|
121
|
+
stream.on("data", (chunk) => {
|
|
122
|
+
buffer += chunk.toString();
|
|
123
|
+
const lines = buffer.split(/\r?\n/);
|
|
124
|
+
buffer = lines.pop() || "";
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (line.trim()) console.log(line);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
stream.on("end", () => {
|
|
130
|
+
if (buffer.trim()) console.log(buffer.trim());
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
if (child.stdout) streamLines(child.stdout);
|
|
134
|
+
if (child.stderr) streamLines(child.stderr);
|
|
135
|
+
child.on("close", (code) => {
|
|
136
|
+
if (code === 0) resolve();
|
|
137
|
+
else reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
138
|
+
});
|
|
139
|
+
child.on("error", reject);
|
|
140
|
+
});
|
|
102
141
|
}
|
|
103
142
|
|
|
104
143
|
export function ensureRulerInit({ cwd = process.cwd(), run = runCommand, dryRun = false } = {}) {
|
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
6
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_AGENTS = ["codex", "claude", "antigravity"];
|
|
9
9
|
const INSTALL_SH_URL = "https://raw.githubusercontent.com/runkids/skillshare/main/install.sh";
|
|
@@ -127,17 +127,31 @@ export async function installSkillshare({
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const osName = detectOS(platform);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
|
|
131
|
+
if (dryRun) {
|
|
132
|
+
// dry-run keeps the old sync path
|
|
133
|
+
if (osName === "windows") {
|
|
134
|
+
runShellCommand(`powershell -NoProfile -ExecutionPolicy Bypass -Command "irm ${INSTALL_PS_URL} | iex"`, { stdio: "pipe", dryRun });
|
|
135
|
+
} else {
|
|
136
|
+
runShellCommand(`curl -fsSL ${INSTALL_SH_URL} | sh`, { stdio: "pipe", dryRun });
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
console.log("Installing skillshare...");
|
|
140
|
+
if (osName === "windows") {
|
|
141
|
+
await spawnShellStreaming("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `irm ${INSTALL_PS_URL} | iex`]);
|
|
142
|
+
} else {
|
|
143
|
+
await spawnShellStreaming("sh", ["-c", `curl -fsSL ${INSTALL_SH_URL} | sh`]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The installer adds to the system PATH, but the current Node process
|
|
148
|
+
// still has the old PATH. Inject the known install dir so subsequent
|
|
149
|
+
// skillshare calls in this session can resolve the binary.
|
|
150
|
+
if (!dryRun && osName === "windows") {
|
|
135
151
|
const winInstallDir = path.join(os.homedir(), "AppData", "Local", "Programs", "skillshare");
|
|
136
|
-
if (!
|
|
152
|
+
if (!process.env.PATH.includes(winInstallDir)) {
|
|
137
153
|
process.env.PATH = `${winInstallDir}${path.delimiter}${process.env.PATH}`;
|
|
138
154
|
}
|
|
139
|
-
} else {
|
|
140
|
-
runShellCommand(`curl -fsSL ${INSTALL_SH_URL} | sh`, { stdio: "pipe", dryRun });
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
const check = checkSkillshareInstalled({ run });
|
|
@@ -147,6 +161,44 @@ export async function installSkillshare({
|
|
|
147
161
|
return check;
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Spawn a child process and stream its stdout/stderr line-by-line in real time
|
|
166
|
+
* via console.log (which will be intercepted by streamSetupOutput for │ prefix).
|
|
167
|
+
* stdin is closed immediately to prevent deadlocks.
|
|
168
|
+
*/
|
|
169
|
+
function spawnShellStreaming(command, args = []) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const child = spawn(command, args, {
|
|
172
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
173
|
+
shell: false
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const streamLines = (stream) => {
|
|
177
|
+
let buffer = "";
|
|
178
|
+
stream.on("data", (chunk) => {
|
|
179
|
+
buffer += chunk.toString();
|
|
180
|
+
const lines = buffer.split(/\r?\n/);
|
|
181
|
+
buffer = lines.pop() || "";
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
if (line.trim()) console.log(line);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
stream.on("end", () => {
|
|
187
|
+
if (buffer.trim()) console.log(buffer.trim());
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (child.stdout) streamLines(child.stdout);
|
|
192
|
+
if (child.stderr) streamLines(child.stderr);
|
|
193
|
+
|
|
194
|
+
child.on("close", (code) => {
|
|
195
|
+
if (code === 0) resolve();
|
|
196
|
+
else reject(new Error(`${command} exited with code ${code}`));
|
|
197
|
+
});
|
|
198
|
+
child.on("error", reject);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
150
202
|
export function detectExistingSkills({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
151
203
|
return skillRoots({ cwd, home })
|
|
152
204
|
.map((root) => ({ path: root, count: countSkillFiles(root) }))
|
|
@@ -360,7 +412,10 @@ export async function syncSkills({
|
|
|
360
412
|
logger("[ctx] No existing skills found.");
|
|
361
413
|
}
|
|
362
414
|
|
|
363
|
-
|
|
415
|
+
// --no-copy --no-git --no-skill --all-targets: fully non-interactive init.
|
|
416
|
+
// skillshare init is interactive by default; with stdin routed to NUL
|
|
417
|
+
// (deadlock prevention) the Go binary hangs waiting for terminal input.
|
|
418
|
+
run("skillshare", ["init", "--no-copy", "--no-git", "--no-skill", "--all-targets"], { cwd, stdio: "pipe", dryRun: options.dryRun });
|
|
364
419
|
logger(statusLine("Initializing skillshare...", options.dryRun ? "dry-run" : "✓ initialized"));
|
|
365
420
|
|
|
366
421
|
if (existing.length && !options.noCollect) {
|