@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 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
- * Capture all console.log and process.stderr.write output from an async fn.
103
- * Returns an array of clean text lines (no \r, no trailing whitespace).
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 captureSetupOutput(fn) {
105
+ async function streamSetupOutput(fn) {
106
106
  const lines = [];
107
107
  const origLog = console.log;
108
108
  const origStderrWrite = process.stderr.write;
109
- console.log = (...args) => lines.push(args.map(String).join(" "));
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) lines.push(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, { stdio: "inherit", shell: true });
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
- const lines = await captureSetupOutput(() => install({ agent, copy: false }));
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
- const lines = await captureSetupOutput(() => syncRules({ cwd, rootDir, args: syncArgs }));
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
- const lines = await captureSetupOutput(() => syncSkills({
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
- const lines = await captureSetupOutput(() => install({ copy, agent: explicitAgent }));
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
- const lines = await captureSetupOutput(() => install({ copy, agent }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctx",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "description": "Inject task-relevant AGENTS.md rules into Codex through plugin hooks.",
5
5
  "author": {
6
6
  "name": "ContextOS"
@@ -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
- run("npm", ["install", "-g", "@intellectronica/ruler"], { stdio: "pipe", dryRun });
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
- if (osName === "windows") {
131
- runShellCommand(`powershell -NoProfile -ExecutionPolicy Bypass -Command "irm ${INSTALL_PS_URL} | iex"`, { stdio: "pipe", dryRun });
132
- // The installer adds to the system PATH, but the current Node process
133
- // still has the old PATH. Inject the known install dir so subsequent
134
- // skillshare calls in this session can resolve the binary.
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 (!dryRun && !process.env.PATH.includes(winInstallDir)) {
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
- run("skillshare", ["init"], { cwd, stdio: "pipe", dryRun: options.dryRun });
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) {