@chemx/starter-kit 26.9.13-383 → 26.9.14-205

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.
@@ -2,9 +2,13 @@ import fs from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
 
5
- const safeSpawnSync = (command, args, options) => {
5
+ const safeSpawnSync = (command, args, options = {}) => {
6
6
  try {
7
- const res = spawnSync(command, args, options);
7
+ const optsWithDefaults = {
8
+ timeout: 1000,
9
+ ...options
10
+ };
11
+ const res = spawnSync(command, args, optsWithDefaults);
8
12
  if (res.error) {
9
13
  return [null, res.error];
10
14
  }
@@ -193,24 +197,92 @@ export const detectGitHubUser = (preferredUser) => {
193
197
  return 'Architect';
194
198
  };
195
199
 
200
+ export const copyViaOsc52 = (text) => {
201
+ if (typeof text !== 'string' || !text) return false;
202
+ try {
203
+ const base64 = Buffer.from(text, 'utf-8').toString('base64');
204
+ const isTmux = Boolean(process.env.TMUX);
205
+ const isScreen = Boolean(process.env.TERM && process.env.TERM.startsWith('screen'));
206
+
207
+ let seq = `\x1b]52;c;${base64}\x07`;
208
+ if (isTmux) {
209
+ seq = `\x1bPtmux;\x1b\x1b]52;c;${base64}\x07\x1b\\`;
210
+ } else if (isScreen) {
211
+ seq = `\x1bP\x1b]52;c;${base64}\x07\x1b\\`;
212
+ }
213
+
214
+ if (process.stdout.isTTY) {
215
+ process.stdout.write(seq);
216
+ return true;
217
+ }
218
+ if (process.stderr.isTTY) {
219
+ process.stderr.write(seq);
220
+ return true;
221
+ }
222
+ try {
223
+ const fd = fs.openSync('/dev/tty', 'w');
224
+ fs.writeSync(fd, seq);
225
+ fs.closeSync(fd);
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ } catch {
231
+ return false;
232
+ }
233
+ };
234
+
196
235
  export const copyToClipboard = (text) => {
236
+ if (typeof text !== 'string' || !text) return false;
237
+
238
+ let copied = false;
239
+
197
240
  if (process.platform === 'darwin') {
198
- const [res] = safeSpawnSync('pbcopy', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
199
- return res?.status === 0;
200
- }
201
- if (process.platform === 'win32') {
202
- const [res] = safeSpawnSync('clip', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
203
- return res?.status === 0;
204
- }
205
- const [wlWhich] = safeSpawnSync('which', ['wl-copy'], { stdio: 'ignore' });
206
- if (wlWhich?.status === 0) {
207
- const [res] = safeSpawnSync('wl-copy', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
208
- if (res?.status === 0) return true;
209
- }
210
- const [xcWhich] = safeSpawnSync('which', ['xclip'], { stdio: 'ignore' });
211
- if (xcWhich?.status === 0) {
212
- const [res] = safeSpawnSync('xclip', ['-selection', 'clipboard'], { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
213
- if (res?.status === 0) return true;
241
+ const [res] = safeSpawnSync('pbcopy', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 1000 });
242
+ if (res?.status === 0) copied = true;
243
+ } else if (process.platform === 'win32') {
244
+ const [res] = safeSpawnSync('clip', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 1000 });
245
+ if (res?.status === 0) copied = true;
246
+ } else {
247
+ // Linux / BSD / Unix environments
248
+ const isWsl = Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
249
+ const isSommelier = Boolean(process.env.SOMMELIER_VM_IDENTIFIER || process.env.SOMMELIER_VERSION);
250
+
251
+ if (isWsl) {
252
+ const [wslRes] = safeSpawnSync('clip.exe', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 1000 });
253
+ if (wslRes?.status === 0) copied = true;
254
+ }
255
+
256
+ // wl-copy hangs in Sommelier / ChromeOS containers due to lack of unfocused data-control support
257
+ if (!copied && !isSommelier) {
258
+ const [wlWhich] = safeSpawnSync('which', ['wl-copy'], { stdio: 'ignore', timeout: 500 });
259
+ if (wlWhich?.status === 0) {
260
+ const [res] = safeSpawnSync('wl-copy', [], { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 800 });
261
+ if (res?.status === 0) copied = true;
262
+ }
263
+ }
264
+
265
+ if (!copied) {
266
+ const [xcWhich] = safeSpawnSync('which', ['xclip'], { stdio: 'ignore', timeout: 500 });
267
+ if (xcWhich?.status === 0) {
268
+ const [res] = safeSpawnSync('xclip', ['-selection', 'clipboard'], { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 800 });
269
+ if (res?.status === 0) copied = true;
270
+ }
271
+ }
272
+
273
+ if (!copied) {
274
+ const [xsWhich] = safeSpawnSync('which', ['xsel'], { stdio: 'ignore', timeout: 500 });
275
+ if (xsWhich?.status === 0) {
276
+ const [res] = safeSpawnSync('xsel', ['--clipboard', '--input'], { input: text, stdio: ['pipe', 'ignore', 'ignore'], timeout: 800 });
277
+ if (res?.status === 0) copied = true;
278
+ }
279
+ }
214
280
  }
215
- return false;
281
+
282
+ // OSC 52 ANSI escape sequence: universally supported across modern terminal emulators
283
+ // (ChromeOS Terminal, iTerm2, Alacritty, Kitty, WezTerm, VS Code, Windows Terminal, tmux, screen).
284
+ // This provides zero-hang clipboard access over SSH, Crostini containers, Docker, and remote sessions.
285
+ const oscCopied = copyViaOsc52(text);
286
+
287
+ return copied || oscCopied;
216
288
  };
@@ -0,0 +1,71 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert';
3
+ import {
4
+ copyViaOsc52,
5
+ copyToClipboard,
6
+ parseGitRemoteUrl
7
+ } from './social-git.js';
8
+
9
+ test('copyViaOsc52: edge cases and input validation', () => {
10
+ assert.strictEqual(copyViaOsc52(''), false);
11
+ assert.strictEqual(copyViaOsc52(null), false);
12
+ assert.strictEqual(copyViaOsc52(undefined), false);
13
+ assert.strictEqual(copyViaOsc52(123), false);
14
+ });
15
+
16
+ test('copyViaOsc52: generates OSC 52 sequence without throwing', () => {
17
+ const originalWrite = process.stdout.write;
18
+ let written = '';
19
+ process.stdout.write = (chunk) => {
20
+ written += chunk;
21
+ return true;
22
+ };
23
+
24
+ try {
25
+ const isTty = process.stdout.isTTY;
26
+ process.stdout.isTTY = true;
27
+ const res = copyViaOsc52('Hello Chemical X');
28
+ assert.strictEqual(res, true);
29
+ assert.ok(written.includes('\x1b]52;c;'));
30
+ assert.ok(written.includes(Buffer.from('Hello Chemical X').toString('base64')));
31
+ process.stdout.isTTY = isTty;
32
+ } finally {
33
+ process.stdout.write = originalWrite;
34
+ }
35
+ });
36
+
37
+ test('copyToClipboard: handles invalid and empty inputs gracefully', () => {
38
+ assert.strictEqual(copyToClipboard(''), false);
39
+ assert.strictEqual(copyToClipboard(null), false);
40
+ assert.strictEqual(copyToClipboard(undefined), false);
41
+ });
42
+
43
+ test('copyToClipboard: executes without hanging and respects timeout boundaries', () => {
44
+ const startTime = Date.now();
45
+ const res = copyToClipboard('Test refactoring prompt content');
46
+ const elapsed = Date.now() - startTime;
47
+
48
+ assert.ok(elapsed < 1000, `Expected elapsed time < 1000ms, got ${elapsed}ms`);
49
+ assert.strictEqual(typeof res, 'boolean');
50
+ });
51
+
52
+ test('parseGitRemoteUrl: handles SSH and HTTPS URLs correctly', () => {
53
+ const ssh = parseGitRemoteUrl('git@github.com:Chemical-X-Protocol/starter-kit.git');
54
+ assert.deepStrictEqual(ssh, {
55
+ owner: 'Chemical-X-Protocol',
56
+ repo: 'starter-kit',
57
+ nameWithOwner: 'Chemical-X-Protocol/starter-kit',
58
+ url: 'https://github.com/Chemical-X-Protocol/starter-kit'
59
+ });
60
+
61
+ const https = parseGitRemoteUrl('https://github.com/Chemical-X-Protocol/starter-kit.git');
62
+ assert.deepStrictEqual(https, {
63
+ owner: 'Chemical-X-Protocol',
64
+ repo: 'starter-kit',
65
+ nameWithOwner: 'Chemical-X-Protocol/starter-kit',
66
+ url: 'https://github.com/Chemical-X-Protocol/starter-kit'
67
+ });
68
+
69
+ assert.strictEqual(parseGitRemoteUrl(''), null);
70
+ assert.strictEqual(parseGitRemoteUrl(null), null);
71
+ });
@@ -23,6 +23,7 @@ export declare const DEFAULT_DISCUSSION_REPO: string;
23
23
  export declare function parseGitRemoteUrl(url: string): GitRepoInfo | null;
24
24
  export declare function detectGitRepoInfo(cwd?: string): GitRepoInfo;
25
25
  export declare function detectGitHubUser(preferredUser?: string): string;
26
+ export declare function copyViaOsc52(text: string): boolean;
26
27
  export declare function copyToClipboard(text: string): boolean;
27
28
  export declare function resolveBadgeColor(score: number): string;
28
29
  export declare function resolveHotspotTierText(lineCount: number): string;
@@ -10,6 +10,7 @@ export {
10
10
  parseGitRemoteUrl,
11
11
  detectGitRepoInfo,
12
12
  detectGitHubUser,
13
+ copyViaOsc52,
13
14
  copyToClipboard
14
15
  } from './social-git.js';
15
16
 
package/cli/index.js CHANGED
@@ -154,10 +154,14 @@ export const runAudit = async (customDir = null, isCli = false) => {
154
154
 
155
155
  if (hasFailingViolations && (isPromptOnFail || isCopyPrompt)) {
156
156
  const prompt = buildMasterPrompt(report);
157
- const copied = copyToClipboard(prompt);
158
- if (copied) {
159
- process.stdout.write('\n\x1b[1m\x1b[32m✔ AI Agent refactoring prompt copied to clipboard!\x1b[0m\n');
160
- process.stdout.write('\x1b[36mPaste directly into Cursor, Claude, or Windsurf to resolve architectural hazards.\x1b[0m\n\n');
157
+ if (prompt) {
158
+ const copied = copyToClipboard(prompt);
159
+ if (copied) {
160
+ process.stdout.write('\n\x1b[1m\x1b[32m✔ AI Agent refactoring prompt copied to clipboard!\x1b[0m\n');
161
+ process.stdout.write('\x1b[36mPaste directly into Cursor, Claude, or Windsurf to resolve architectural hazards.\x1b[0m\n\n');
162
+ } else if (isCopyPrompt) {
163
+ process.stdout.write('\n\x1b[33m⚠ Could not access system clipboard.\x1b[0m\n\n');
164
+ }
161
165
  }
162
166
  }
163
167
 
@@ -185,9 +189,11 @@ const main = async () => {
185
189
  case 'wrap':
186
190
  await runBuildAudit(rawArgs.slice(1), true);
187
191
  break;
188
- case 'audit':
189
- await runAudit(null, true);
192
+ case 'audit': {
193
+ const posDir = (rawArgs[1] && !rawArgs[1].startsWith('-')) ? rawArgs[1] : null;
194
+ await runAudit(posDir, true);
190
195
  break;
196
+ }
191
197
  case 'init':
192
198
  await runInit(rawArgs[1] || 'src/chemical-x', rawArgs, runAudit);
193
199
  break;
@@ -86,10 +86,9 @@ export const buildActiveGrades = (report) => {
86
86
  const riskWeight = resolvePillarRiskWeight(data);
87
87
  const icon = PILLAR_EMOJIS[pillarName] || '🏛️';
88
88
  const shortName = PILLAR_SHORT_NAMES[pillarName] || pillarName;
89
- const isClean = data.violations === 0;
90
- const countLabel = isClean
91
- ? '0 items'
92
- : `${data.violations} ${data.violations === 1 ? 'item' : 'items'}`;
89
+ const count = data.violations || 0;
90
+ const paddedCount = String(count).padStart(2, '0');
91
+ const countLabel = `(${paddedCount} Items)`;
93
92
 
94
93
  const handlePillarAction = async () => {
95
94
  await showPagedContent(
@@ -104,10 +103,10 @@ export const buildActiveGrades = (report) => {
104
103
  shortName,
105
104
  grade,
106
105
  riskWeight,
107
- violations: data.violations || 0,
106
+ violations: count,
108
107
  originalIndex: index,
109
108
  tag: resolveGradeBadge(grade, 11),
110
- label: `${icon} ${pillarName} (${countLabel})`,
109
+ label: `${countLabel} ${icon} ${pillarName}`,
111
110
  action: handlePillarAction
112
111
  });
113
112
  });
@@ -117,10 +116,8 @@ export const buildActiveGrades = (report) => {
117
116
  const slopGrade = aiSlop.grade || (slopViolations.length > 0 ? 'D' : 'A+');
118
117
  const slopRiskWeight = resolvePillarRiskWeight(aiSlop.breakdown);
119
118
  const slopCount = slopViolations.length;
120
- const isSlopClean = slopCount === 0;
121
- const slopCountLabel = isSlopClean
122
- ? '0 items'
123
- : `${slopCount} ${slopCount === 1 ? 'item' : 'items'}`;
119
+ const paddedSlopCount = String(slopCount).padStart(2, '0');
120
+ const slopCountLabel = `(${paddedSlopCount} Items)`;
124
121
 
125
122
  const handleSlopAction = async () => {
126
123
  await showPagedContent(formatAiSlopSection(report), buildAiSlopPrompt(report));
@@ -135,7 +132,7 @@ export const buildActiveGrades = (report) => {
135
132
  violations: slopCount,
136
133
  originalIndex: pillarEntries.length,
137
134
  tag: resolveGradeBadge(slopGrade, 11),
138
- label: `🤖 AI Slop & Authenticity (${aiSlop.score ?? 100}/100 - ${aiSlop.label ?? 'Pure Artisanal'}) (${slopCountLabel})`,
135
+ label: `${slopCountLabel} 🤖 AI Slop & Authenticity (${aiSlop.score ?? 100}/100 - ${aiSlop.label ?? 'Pure Artisanal'})`,
139
136
  action: handleSlopAction
140
137
  });
141
138
  }
@@ -251,10 +248,11 @@ export const buildDashboardActionGroups = ({ report, onScaffold = null, onRerun
251
248
  const handleExitAction = async () => {};
252
249
 
253
250
  const monolithHotspots = (report?.hotspots || []).filter((h) => h.isMonolith || h.lineCount > 500);
251
+ const monoCount = String(monolithHotspots.length).padStart(2, '0');
254
252
  const hotspotsAction = {
255
253
  key: "hotspots",
256
254
  tag: formatButtonTag("Hotspots", "\x1b[38;5;208m"),
257
- label: `🔥 Top Refactoring Hotspots & Monoliths (${monolithHotspots.length} files)`,
255
+ label: `🔥 Top Refactoring Hotspots & Monoliths (${monoCount} Files)`,
258
256
  action: handleHotspotsAction
259
257
  };
260
258
 
package/cli/terminal.js CHANGED
@@ -6,11 +6,11 @@ export const openBrowser = (url) => {
6
6
  const platform = process.platform;
7
7
  try {
8
8
  if (platform === "darwin") {
9
- spawnSync("open", [url], { stdio: "ignore" });
9
+ spawnSync("open", [url], { stdio: "ignore", timeout: 3000 });
10
10
  } else if (platform === "win32") {
11
- spawnSync("cmd.exe", ["/c", "start", '""', url], { stdio: "ignore" });
11
+ spawnSync("cmd.exe", ["/c", "start", '""', url], { stdio: "ignore", timeout: 3000 });
12
12
  } else {
13
- spawnSync("xdg-open", [url], { stdio: "ignore" });
13
+ spawnSync("xdg-open", [url], { stdio: "ignore", timeout: 3000 });
14
14
  }
15
15
  } catch (err) {
16
16
  const error = err instanceof Error ? err : new Error(String(err));
package/docs/CHANGELOG.md CHANGED
@@ -10,6 +10,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
10
10
  - **Gatekeeper SaaS & Sponsor Verification**: Integrated client license validation in `cli/license.js` with `Xophz Gatekeeper` (`/wp-json/compass/v1/gatekeeper/licenses/validate`), allowing verification of both license keys (`CX-ST-`, `CX-SP-`) and GitHub Sponsor handles.
11
11
  - **Community Edition Flow**: Added reciprocal free Community Edition scaffolding in `cli/scaffold.js` allowing developers to unpack blueprints locally while sharing scorecards with community GitHub Discussions.
12
12
  - **Local Blueprint Fallback**: Added `loadLocalBlueprintFiles` in `cli/license.js` enabling offline and community blueprint scaffolding without remote edge download dependencies.
13
+ - **Co-located Social-Git Test Suite**: Added test suite (`cli/audit/social-git.spec.js`) covering clipboard copying, OSC 52 escape sequences, and remote Git repository parsing.
14
+
15
+ ### Fixed
16
+ - **Zero-Hang Clipboard Copying**: Resolved indefinite process hangs during prompt copying in `cli/audit/social-git.js` by adding default timeouts (`timeout: 1000`) to `safeSpawnSync`, preventing child processes from blocking the Node event loop.
17
+ - **ANSI OSC 52 Universal Clipboard Support**: Added `copyViaOsc52` in `cli/audit/social-git.js`, enabling instant, zero-hang prompt copying directly to system clipboards via ANSI OSC 52 sequences across modern terminal emulators (ChromeOS Terminal, iTerm2, Alacritty, Kitty, WezTerm, VS Code, Windows Terminal, tmux).
18
+ - **ChromeOS Sommelier & Wayland Protection**: Added Sommelier container detection in `cli/audit/social-git.js` to avoid `wl-copy` window focus deadlocks, alongside timeouts for `wl-copy`, `xclip`, `xsel`, and WSL `clip.exe`.
19
+ - **Positional Audit Directory Arguments**: Updated `cli/index.js` to accept positional directory paths for `chemx audit <dir>` and added empty prompt guard clause to skip clipboard writes on pristine Grade A+ codebases.
20
+ - **Browser Launch Timeout**: Added explicit timeout guard (`timeout: 3000`) to `openBrowser` in `cli/terminal.js`.
21
+
22
+ ### Changed
23
+ - **Menu Alignment & Double-Padded Item Counts**: Reordered grade menu options in `cli/navigator-actions.js` to place double-padded count tags `(## Items)` directly following `[ Grade: X ]` badges, ensuring pixel-perfect column alignment across all architectural pillar categories and hotspot files.
13
24
 
14
25
  ## [2026-09-12]
15
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemx/starter-kit",
3
- "version": "26.9.13-383",
3
+ "version": "26.9.14-205",
4
4
  "description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "scripts": {
27
27
  "publish:both": "node scripts/publish-both.mjs",
28
+ "test": "node --test cli/audit/*.spec.js",
28
29
  "typecheck": "tsc --noEmit",
29
30
  "create-capsule": "node cli/index.js",
30
31
  "audit": "node cli/index.js audit"