@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.
- package/cli/audit/social-git.js +91 -19
- package/cli/audit/social-git.spec.js +71 -0
- package/cli/audit/social.d.ts +1 -0
- package/cli/audit/social.js +1 -0
- package/cli/index.js +12 -6
- package/cli/navigator-actions.js +10 -12
- package/cli/terminal.js +3 -3
- package/docs/CHANGELOG.md +11 -0
- package/package.json +2 -1
package/cli/audit/social-git.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/cli/audit/social.d.ts
CHANGED
|
@@ -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;
|
package/cli/audit/social.js
CHANGED
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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;
|
package/cli/navigator-actions.js
CHANGED
|
@@ -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
|
|
90
|
-
const
|
|
91
|
-
|
|
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:
|
|
106
|
+
violations: count,
|
|
108
107
|
originalIndex: index,
|
|
109
108
|
tag: resolveGradeBadge(grade, 11),
|
|
110
|
-
label: `${
|
|
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
|
|
121
|
-
const slopCountLabel =
|
|
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:
|
|
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 (${
|
|
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.
|
|
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"
|