@chemx/starter-kit 26.9.9-786 → 26.9.11-481
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/ast-visitors.js +243 -0
- package/cli/audit/discussion-store.js +39 -0
- package/cli/audit/history.d.ts +69 -0
- package/cli/audit/history.js +397 -0
- package/cli/audit/metrics.js +179 -0
- package/cli/audit/prompts.js +174 -0
- package/cli/audit/reporter-ascii.d.ts +4 -0
- package/cli/audit/reporter-ascii.js +99 -0
- package/cli/audit/reporter-grades.js +275 -0
- package/cli/audit/reporter-markdown.js +137 -0
- package/cli/audit/reporter-utils.js +125 -0
- package/cli/audit/reporter.js +497 -0
- package/cli/audit/rules-helpers.js +109 -0
- package/cli/audit/rules-registry.js +97 -0
- package/cli/audit/rules.js +123 -0
- package/cli/audit/social-constants.js +4 -0
- package/cli/audit/social-gh.js +126 -0
- package/cli/audit/social-git.js +214 -0
- package/cli/audit/social-http.js +222 -0
- package/cli/audit/social-publisher.js +193 -0
- package/cli/audit/social.d.ts +69 -0
- package/cli/audit/social.js +303 -0
- package/cli/audit/types.d.ts +140 -0
- package/cli/audit.d.ts +3 -24
- package/cli/audit.js +207 -210
- package/cli/help.d.ts +1 -0
- package/cli/help.js +136 -0
- package/cli/index.js +128 -566
- package/cli/installer-templates.js +91 -0
- package/cli/installer.d.ts +21 -0
- package/cli/installer.js +104 -0
- package/cli/license.js +320 -0
- package/cli/navigator-actions.js +274 -0
- package/cli/navigator-banner.js +98 -0
- package/cli/navigator-conversion.js +100 -0
- package/cli/navigator-grades.js +87 -0
- package/cli/navigator-menu.js +127 -0
- package/cli/navigator-paged.js +86 -0
- package/cli/navigator-share.js +129 -0
- package/cli/navigator.d.ts +43 -0
- package/cli/navigator.js +126 -0
- package/cli/scaffold.js +148 -0
- package/cli/terminal.js +126 -0
- package/docs/CHANGELOG.md +101 -0
- package/package.json +11 -10
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const buildPreCommitHookScript = (minGrade = 'B', minScore = 80) => `#!/bin/sh
|
|
2
|
+
# Chemical X Protocol: Pre-Commit Line Budget & Architecture Gatekeeper
|
|
3
|
+
# Free architectural guardrail preventing context bloat and monolith sprawl.
|
|
4
|
+
|
|
5
|
+
MIN_GRADE="\${CHEMX_MIN_GRADE:-${minGrade}}"
|
|
6
|
+
MIN_SCORE="\${CHEMX_MIN_SCORE:-${minScore}}"
|
|
7
|
+
MAX_LINES="\${CHEMX_MAX_LINES:-500}"
|
|
8
|
+
MAX_MOLECULE_LINES="\${CHEMX_MAX_MOLECULE_LINES:-100}"
|
|
9
|
+
|
|
10
|
+
REPO_ROOT="\$(git rev-parse --show-toplevel 2>/dev/null)"
|
|
11
|
+
[ -z "\$REPO_ROOT" ] && exit 0
|
|
12
|
+
cd "\$REPO_ROOT" || exit 1
|
|
13
|
+
|
|
14
|
+
STAGED_FILES=\$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(jsx?|tsx?|vue|svelte)\$' | grep -vE '(\\.(d\\.ts|min\\.|test\\.|spec\\.))')
|
|
15
|
+
[ -z "\$STAGED_FILES" ] && exit 0
|
|
16
|
+
|
|
17
|
+
FAILED=0
|
|
18
|
+
ERRORS=""
|
|
19
|
+
for F in \$STAGED_FILES; do
|
|
20
|
+
[ ! -f "\$F" ] && continue
|
|
21
|
+
L=\$(wc -l < "\$F" | tr -d ' ')
|
|
22
|
+
case "\$F" in
|
|
23
|
+
*molecules*|*/m-*|*m-*)
|
|
24
|
+
[ "\$L" -gt "\$MAX_MOLECULE_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n \\033[31m✕\\033[0m \$F (\$L LOC > \$MAX_MOLECULE_LINES molecule limit)" ;;
|
|
25
|
+
*)
|
|
26
|
+
[ "\$L" -gt "\$MAX_LINES" ] && FAILED=1 && ERRORS="\${ERRORS}\\n \\033[31m✕\\033[0m \$F (\$L LOC > \$MAX_LINES file budget)" ;;
|
|
27
|
+
esac
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
if [ "\$FAILED" -eq 1 ]; then
|
|
31
|
+
printf "\\n\\033[1m\\033[31m[Chemical X] Commit Blocked: Staged files exceed architectural line budgets\\033[0m\\n\$ERRORS\\n\\n"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
AUDIT_BIN=""
|
|
35
|
+
if [ -f "./cli/index.js" ]; then
|
|
36
|
+
AUDIT_BIN="node ./cli/index.js"
|
|
37
|
+
elif [ -x "./node_modules/.bin/chemx" ]; then
|
|
38
|
+
AUDIT_BIN="./node_modules/.bin/chemx"
|
|
39
|
+
elif command -v chemx >/dev/null 2>&1; then
|
|
40
|
+
AUDIT_BIN="chemx"
|
|
41
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
42
|
+
AUDIT_BIN="npx chemx"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ -n "\$AUDIT_BIN" ]; then
|
|
46
|
+
printf "\\033[38;2;98;201;255m[Chemical X] Verifying architectural health (Min Grade: %s, Min Score: %s)...\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
|
|
47
|
+
if ! \$AUDIT_BIN audit --min-grade="\$MIN_GRADE" --min-score="\$MIN_SCORE" --prompt-on-fail; then
|
|
48
|
+
printf "\\n\\033[1m\\033[31m[Chemical X] Commit Blocked: Codebase falls below required Grade %s (Score %s)\\033[0m\\n" "\$MIN_GRADE" "\$MIN_SCORE"
|
|
49
|
+
printf "\\033[36m💡 Tip: Want crystalline drop-in templates? Run 'npm create chemx' or sponsor at https://github.com/sponsors/Chemical-X-Protocol\\033[0m\\n\\n"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
[ "\$FAILED" -eq 1 ] && exit 1
|
|
55
|
+
printf "\\033[32m✔ [Chemical X] Pre-commit architectural guardrails passed.\\033[0m\\n"
|
|
56
|
+
exit 0
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
export const buildGitHubWorkflowScript = (minGrade = 'B', minScore = 80) => `name: Chemical X Architectural Gatekeeper
|
|
60
|
+
|
|
61
|
+
on:
|
|
62
|
+
push:
|
|
63
|
+
branches: [main, master]
|
|
64
|
+
pull_request:
|
|
65
|
+
branches: [main, master]
|
|
66
|
+
|
|
67
|
+
permissions:
|
|
68
|
+
contents: read
|
|
69
|
+
pull-requests: write
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
molecular-audit:
|
|
73
|
+
name: Chemical X Architecture & Line Budget Audit
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- name: Checkout Code
|
|
77
|
+
uses: actions/checkout@v4
|
|
78
|
+
- name: Setup Node.js
|
|
79
|
+
uses: actions/setup-node@v4
|
|
80
|
+
with:
|
|
81
|
+
node-version: 20
|
|
82
|
+
cache: npm
|
|
83
|
+
- name: Run Chemical X Architectural Audit
|
|
84
|
+
run: npx chemx audit --min-grade=\${{ vars.CHEMX_MIN_GRADE || '${minGrade}' }} --min-score=\${{ vars.CHEMX_MIN_SCORE || ${minScore} }} --markdown --output=AUDIT_REPORT.md
|
|
85
|
+
- name: Upload Audit Scorecard Artifact
|
|
86
|
+
if: always()
|
|
87
|
+
uses: actions/upload-artifact@v4
|
|
88
|
+
with:
|
|
89
|
+
name: chemical-x-audit-report
|
|
90
|
+
path: AUDIT_REPORT.md
|
|
91
|
+
`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface InstallOptions {
|
|
2
|
+
minGrade?: string;
|
|
3
|
+
minScore?: number;
|
|
4
|
+
maxLines?: number;
|
|
5
|
+
maxMoleculeLines?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ProjectConfig {
|
|
9
|
+
minGrade: string;
|
|
10
|
+
minScore: number;
|
|
11
|
+
maxLineCount: number;
|
|
12
|
+
maxMoleculeLineCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export declare function buildPreCommitHookScript(minGrade?: string, minScore?: number): string;
|
|
16
|
+
export declare function buildGitHubWorkflowScript(minGrade?: string, minScore?: number): string;
|
|
17
|
+
export declare function installPreCommitHook(targetDir?: string, options?: InstallOptions): boolean;
|
|
18
|
+
export declare function installGitHubWorkflow(targetDir?: string, options?: InstallOptions): boolean;
|
|
19
|
+
export declare function saveProjectConfig(targetDir?: string, config?: Partial<ProjectConfig>): void;
|
|
20
|
+
export declare function areGuardrailsInstalled(targetDir?: string): boolean;
|
|
21
|
+
export declare function runInstallWizard(targetDir?: string): Promise<void>;
|
package/cli/installer.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { hasGum, gumChoose, gumInput, promptQuestion } from './terminal.js';
|
|
4
|
+
import { buildPreCommitHookScript, buildGitHubWorkflowScript } from './installer-templates.js';
|
|
5
|
+
|
|
6
|
+
export { buildPreCommitHookScript, buildGitHubWorkflowScript } from './installer-templates.js';
|
|
7
|
+
|
|
8
|
+
export const installPreCommitHook = (targetDir = '.', options = {}) => {
|
|
9
|
+
const gitHooksDir = path.resolve(targetDir, '.git', 'hooks');
|
|
10
|
+
if (!fs.existsSync(gitHooksDir)) fs.mkdirSync(gitHooksDir, { recursive: true });
|
|
11
|
+
const hookPath = path.join(gitHooksDir, 'pre-commit');
|
|
12
|
+
fs.writeFileSync(hookPath, buildPreCommitHookScript(options.minGrade, options.minScore), { mode: 0o755 });
|
|
13
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m Installed git pre-commit hook: .git/hooks/pre-commit (chmod +x)\n`);
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const installGitHubWorkflow = (targetDir = '.', options = {}) => {
|
|
18
|
+
const wfDir = path.resolve(targetDir, '.github', 'workflows');
|
|
19
|
+
if (!fs.existsSync(wfDir)) fs.mkdirSync(wfDir, { recursive: true });
|
|
20
|
+
const wfPath = path.join(wfDir, 'chemx-audit.yml');
|
|
21
|
+
fs.writeFileSync(wfPath, buildGitHubWorkflowScript(options.minGrade, options.minScore), 'utf-8');
|
|
22
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m Installed GitHub Actions CI workflow: .github/workflows/chemx-audit.yml\n`);
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const saveProjectConfig = (targetDir = '.', config = {}) => {
|
|
27
|
+
const chemxDir = path.resolve(targetDir, '.chemx');
|
|
28
|
+
if (!fs.existsSync(chemxDir)) fs.mkdirSync(chemxDir, { recursive: true });
|
|
29
|
+
fs.writeFileSync(path.join(chemxDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
30
|
+
process.stdout.write(` \x1b[32m✔\x1b[0m Saved project settings to: .chemx/config.json\n`);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const areGuardrailsInstalled = (targetDir = '.') => {
|
|
34
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
35
|
+
const wfPath = path.join(resolvedTarget, '.github', 'workflows', 'chemx-audit.yml');
|
|
36
|
+
const hasWf = fs.existsSync(wfPath);
|
|
37
|
+
|
|
38
|
+
const gitPath = path.join(resolvedTarget, '.git');
|
|
39
|
+
let hasGit = false;
|
|
40
|
+
let hooksDir = null;
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(gitPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.statSync(gitPath);
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
hasGit = true;
|
|
47
|
+
hooksDir = path.join(gitPath, 'hooks');
|
|
48
|
+
} else if (stat.isFile()) {
|
|
49
|
+
hasGit = true;
|
|
50
|
+
const gitContent = fs.readFileSync(gitPath, 'utf-8');
|
|
51
|
+
const match = gitContent.match(/gitdir:\s*(.+)/);
|
|
52
|
+
if (match) {
|
|
53
|
+
hooksDir = path.resolve(resolvedTarget, match[1].trim(), 'hooks');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
hasGit = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!hasGit) {
|
|
62
|
+
return hasWf;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let hasHook = false;
|
|
66
|
+
if (hooksDir) {
|
|
67
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
68
|
+
if (fs.existsSync(hookPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const hookContent = fs.readFileSync(hookPath, 'utf-8');
|
|
71
|
+
hasHook = hookContent.includes('Chemical X') || hookContent.includes('chemx');
|
|
72
|
+
} catch {
|
|
73
|
+
hasHook = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return hasWf && hasHook;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const runInstallWizard = async (targetDir = '.') => {
|
|
82
|
+
const isGit = fs.existsSync(path.resolve(targetDir, '.git'));
|
|
83
|
+
process.stdout.write('\n\x1b[1m\x1b[38;2;98;201;255mChemical X: Architecture Guardrail Installer\x1b[0m\n\n');
|
|
84
|
+
const targetChoice = hasGum()
|
|
85
|
+
? gumChoose(['1. Install All Guardrails (Git Pre-Commit Hook + GitHub CI Workflow)', '2. Git Pre-Commit Hook only (.git/hooks/pre-commit)', '3. GitHub Actions CI Workflow only (.github/workflows/chemx-audit.yml)', '4. Cancel'])
|
|
86
|
+
: await promptQuestion('Select target: [1] All, [2] Hook, [3] CI, [4] Cancel (default: 1): ');
|
|
87
|
+
if (targetChoice?.includes('Cancel') || targetChoice === '4') return;
|
|
88
|
+
|
|
89
|
+
const minGrade = (hasGum() ? gumInput('Minimum required Grade [A+, A, B, C, D] (default: B):', 'B') : await promptQuestion('Minimum required Grade [default: B]: ')) || 'B';
|
|
90
|
+
const minScore = parseInt((hasGum() ? gumInput('Minimum required Score [0-100] (default: 80):', '80') : await promptQuestion('Minimum required Score [default: 80]: ')) || '80', 10);
|
|
91
|
+
const opts = { minGrade: minGrade.trim().toUpperCase(), minScore };
|
|
92
|
+
|
|
93
|
+
process.stdout.write('\n\x1b[1mInstalling guardrails...\x1b[0m\n');
|
|
94
|
+
const shouldHook = !targetChoice.includes('CI only') && targetChoice !== '3';
|
|
95
|
+
const shouldWf = !targetChoice.includes('Hook only') && targetChoice !== '2';
|
|
96
|
+
|
|
97
|
+
if (shouldHook) {
|
|
98
|
+
if (isGit) installPreCommitHook(targetDir, opts);
|
|
99
|
+
else process.stdout.write(' \x1b[33m⚠\x1b[0m Skipped .git/hooks (current directory is not a git repository root).\n');
|
|
100
|
+
}
|
|
101
|
+
if (shouldWf) installGitHubWorkflow(targetDir, opts);
|
|
102
|
+
saveProjectConfig(targetDir, { minGrade: opts.minGrade, minScore: opts.minScore, maxLineCount: 500, maxMoleculeLineCount: 100 });
|
|
103
|
+
process.stdout.write('\n\x1b[1m\x1b[32m✔ Chemical X guardrails installed successfully!\x1b[0m\n\n');
|
|
104
|
+
};
|
package/cli/license.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import {
|
|
5
|
+
hasGum,
|
|
6
|
+
gumChoose,
|
|
7
|
+
gumInput,
|
|
8
|
+
promptQuestion,
|
|
9
|
+
openBrowser
|
|
10
|
+
} from './terminal.js';
|
|
11
|
+
|
|
12
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.chemical-x');
|
|
13
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
14
|
+
export const DEVICE_FILE = path.join(CONFIG_DIR, 'device_id');
|
|
15
|
+
|
|
16
|
+
export const API_BASE = process.env.CHEMICAL_X_API_URL || 'https://chemicalx.xophz.com';
|
|
17
|
+
export const URL_LEARN = 'https://chemicalx.xophz.com';
|
|
18
|
+
export const URL_SPONSOR = 'https://github.com/sponsors/Chemical-X-Protocol';
|
|
19
|
+
export const URL_STANDARD = 'https://mycompassconsulting.com/buy/chemical-x/standard';
|
|
20
|
+
export const URL_MASTER = 'https://mycompassconsulting.com/buy/chemical-x/master';
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
23
|
+
try {
|
|
24
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getOrCreateDeviceId = () => {
|
|
29
|
+
if (fs.existsSync(DEVICE_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
const id = fs.readFileSync(DEVICE_FILE, 'utf-8').trim();
|
|
32
|
+
if (id) return id;
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
const newId = `cli_${Math.random().toString(36).substring(2, 12)}_${Date.now()}`;
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(DEVICE_FILE, newId, 'utf-8');
|
|
38
|
+
} catch {}
|
|
39
|
+
return newId;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getCachedLicenseKey = () => {
|
|
43
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
48
|
+
return data.licenseKey || null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const saveLicenseKey = (licenseKey) => {
|
|
55
|
+
try {
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
CONFIG_FILE,
|
|
58
|
+
JSON.stringify({ licenseKey, updatedAt: new Date().toISOString() }, null, 2),
|
|
59
|
+
'utf-8'
|
|
60
|
+
);
|
|
61
|
+
} catch {}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const resolveFlagLicense = (args) => {
|
|
65
|
+
const cliFlagIdx = args.indexOf('--license');
|
|
66
|
+
const hasFlag = cliFlagIdx !== -1 && Boolean(args[cliFlagIdx + 1]);
|
|
67
|
+
if (!hasFlag) return null;
|
|
68
|
+
return args[cliFlagIdx + 1].trim();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const obtainLicenseKey = async (rawArgs = [], onRunAudit = null) => {
|
|
72
|
+
const flagKey = resolveFlagLicense(rawArgs);
|
|
73
|
+
const effectiveKey = flagKey || getCachedLicenseKey();
|
|
74
|
+
|
|
75
|
+
if (effectiveKey) {
|
|
76
|
+
return effectiveKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const useGum = hasGum();
|
|
80
|
+
|
|
81
|
+
if (useGum) {
|
|
82
|
+
const choice = gumChoose(
|
|
83
|
+
[
|
|
84
|
+
'1. Visit chemicalx.xophz.com to learn more',
|
|
85
|
+
'2. Buy Standard Vault: Single Dev License ($27) -> Launch Checkout',
|
|
86
|
+
'3. Buy Team Power Puff: Unlimited Lifetime ($97) -> Launch Checkout',
|
|
87
|
+
'4. Enter License Key (CX-XXXX-XXXX-XXXX)',
|
|
88
|
+
'5. Run Free Public Audit (npx chemx audit)',
|
|
89
|
+
'6. Exit'
|
|
90
|
+
],
|
|
91
|
+
'Chemical X Scaffolding Requires a Paid License:'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (choice.startsWith('1.')) {
|
|
95
|
+
process.stdout.write(`\x1b[36mOpening Chemical X Portal in default browser:\x1b[0m ${URL_LEARN}\n`);
|
|
96
|
+
openBrowser(URL_LEARN);
|
|
97
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
98
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (choice.startsWith('2.')) {
|
|
102
|
+
process.stdout.write(`\x1b[36mOpening Standard Vault checkout (Single Dev License) in default browser:\x1b[0m ${URL_STANDARD}\n`);
|
|
103
|
+
openBrowser(URL_STANDARD);
|
|
104
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
105
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (choice.startsWith('3.')) {
|
|
109
|
+
process.stdout.write(`\x1b[36mOpening Team Power Puff checkout (Unlimited Lifetime) in default browser:\x1b[0m ${URL_MASTER}\n`);
|
|
110
|
+
openBrowser(URL_MASTER);
|
|
111
|
+
process.stdout.write('\nOnce completed, paste your Sponsor / VIP License Key below.\n');
|
|
112
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (choice.startsWith('4.')) {
|
|
116
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (choice.startsWith('5.') && onRunAudit) {
|
|
120
|
+
await onRunAudit(null, true);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isExitChoice = choice.startsWith('6.') || !choice;
|
|
124
|
+
if (isExitChoice) {
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.stdout.write('\x1b[1mChemical X Scaffolding Requires a Paid License:\x1b[0m\n');
|
|
132
|
+
process.stdout.write(' [1] Visit chemicalx.xophz.com to learn more\n');
|
|
133
|
+
process.stdout.write(' [2] Buy Standard Vault: Single Dev License ($27) - Opens browser\n');
|
|
134
|
+
process.stdout.write(' [3] Buy Team Power Puff: Unlimited Lifetime ($97) - Opens browser\n');
|
|
135
|
+
process.stdout.write(' [4] Enter License Key\n');
|
|
136
|
+
process.stdout.write(' [5] Run Free Public Audit (npx chemx audit)\n');
|
|
137
|
+
process.stdout.write(' [6] Exit\n\n');
|
|
138
|
+
|
|
139
|
+
const selection = await promptQuestion('Select option [1-6] (default: 1): ');
|
|
140
|
+
const effectiveChoice = selection.trim() || '1';
|
|
141
|
+
|
|
142
|
+
if (effectiveChoice === '1') {
|
|
143
|
+
process.stdout.write(`Opening: ${URL_LEARN}\n`);
|
|
144
|
+
openBrowser(URL_LEARN);
|
|
145
|
+
return promptQuestion('Enter License Key after review (or press Enter to exit): ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (effectiveChoice === '2') {
|
|
149
|
+
process.stdout.write(`Opening: ${URL_STANDARD}\n`);
|
|
150
|
+
openBrowser(URL_STANDARD);
|
|
151
|
+
return promptQuestion('Enter License Key after purchase: ');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (effectiveChoice === '3') {
|
|
155
|
+
process.stdout.write(`Opening: ${URL_MASTER}\n`);
|
|
156
|
+
openBrowser(URL_MASTER);
|
|
157
|
+
return promptQuestion('Enter License Key after purchase: ');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (effectiveChoice === '4') {
|
|
161
|
+
return promptQuestion('Enter License Key (CX-XXXX-XXXX-XXXX): ');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (effectiveChoice === '5' && onRunAudit) {
|
|
165
|
+
await onRunAudit(null, true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (effectiveChoice === '6') {
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return promptQuestion('Enter Chemical X Sponsor License Key (CX-XXXX-XXXX-XXXX): ');
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const checkOrPromptEvaluation = async (actionLabel = 'generate capsule') => {
|
|
176
|
+
const cachedKey = getCachedLicenseKey();
|
|
177
|
+
if (cachedKey) {
|
|
178
|
+
return { licensed: true, key: cachedKey };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const useGum = hasGum();
|
|
182
|
+
if (useGum) {
|
|
183
|
+
spawnSync(
|
|
184
|
+
'gum',
|
|
185
|
+
[
|
|
186
|
+
'style',
|
|
187
|
+
'--border=rounded',
|
|
188
|
+
'--border-foreground=81',
|
|
189
|
+
'--padding=0 1',
|
|
190
|
+
'--bold',
|
|
191
|
+
'⚡ Chemical X: Evaluation Mode (Unlicensed)\n' +
|
|
192
|
+
'Support vibe coding standards: chemicalx.xophz.com\n' +
|
|
193
|
+
'Single Dev ($27) | Team Power Puff ($97: Unlimited Lifetime)'
|
|
194
|
+
],
|
|
195
|
+
{ stdio: 'inherit' }
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const choice = gumChoose(
|
|
199
|
+
[
|
|
200
|
+
`1. ⚡ Continue in Evaluation Mode (Press Enter to ${actionLabel})`,
|
|
201
|
+
'2. 🔑 Enter License Key (Team Power Puff or Single Dev)',
|
|
202
|
+
'3. 💎 Buy License ($27 Solo / $97 Team Power Puff Unlimited Lifetime)',
|
|
203
|
+
'4. 🚪 Cancel'
|
|
204
|
+
],
|
|
205
|
+
`Evaluation Mode: ${actionLabel}`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (choice.startsWith('1.') || !choice) {
|
|
209
|
+
return { licensed: false, proceed: true };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (choice.startsWith('2.')) {
|
|
213
|
+
const enteredKey = gumInput('License Key (CX-XXXX-XXXX-XXXX):', 'CX-XXXX-XXXX-XXXX');
|
|
214
|
+
if (enteredKey && enteredKey !== 'CX-XXXX-XXXX-XXXX') {
|
|
215
|
+
saveLicenseKey(enteredKey.trim().toUpperCase());
|
|
216
|
+
process.stdout.write('\x1b[32m✔ License saved to ~/.chemical-x/config.json!\x1b[0m\n\n');
|
|
217
|
+
return { licensed: true, key: enteredKey.trim().toUpperCase() };
|
|
218
|
+
}
|
|
219
|
+
return { licensed: false, proceed: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (choice.startsWith('3.')) {
|
|
223
|
+
openBrowser(URL_MASTER);
|
|
224
|
+
process.stdout.write(`\x1b[36mOpened checkout in default browser:\x1b[0m ${URL_MASTER}\n`);
|
|
225
|
+
const keyAfterBuy = gumInput('Enter License Key once purchased (or press Enter to skip):');
|
|
226
|
+
if (keyAfterBuy) {
|
|
227
|
+
saveLicenseKey(keyAfterBuy.trim().toUpperCase());
|
|
228
|
+
process.stdout.write('\x1b[32m✔ License saved to ~/.chemical-x/config.json!\x1b[0m\n\n');
|
|
229
|
+
return { licensed: true, key: keyAfterBuy.trim().toUpperCase() };
|
|
230
|
+
}
|
|
231
|
+
return { licensed: false, proceed: true };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (choice.startsWith('4.')) {
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { licensed: false, proceed: true };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
process.stdout.write(
|
|
242
|
+
'\n\x1b[38;5;208;1m⚡ Chemical X: Evaluation Mode (Unlicensed)\x1b[0m\n' +
|
|
243
|
+
'Support vibe coding standards: chemicalx.xophz.com\n' +
|
|
244
|
+
` [1] Continue in Evaluation Mode (Press Enter to ${actionLabel})\n` +
|
|
245
|
+
' [2] Enter License Key (Team Power Puff or Single Dev)\n' +
|
|
246
|
+
' [3] Buy License ($27 Solo / $97 Team Power Puff Unlimited Lifetime)\n' +
|
|
247
|
+
' [4] Cancel\n\n'
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const sel = await promptQuestion('Select option [1-4] (default: 1): ');
|
|
251
|
+
const choice = sel.trim() || '1';
|
|
252
|
+
|
|
253
|
+
if (choice === '1') {
|
|
254
|
+
return { licensed: false, proceed: true };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (choice === '2') {
|
|
258
|
+
const entered = await promptQuestion('Enter License Key (CX-XXXX-XXXX-XXXX): ');
|
|
259
|
+
if (entered) {
|
|
260
|
+
saveLicenseKey(entered.trim().toUpperCase());
|
|
261
|
+
process.stdout.write('\x1b[32m✔ License saved to ~/.chemical-x/config.json!\x1b[0m\n\n');
|
|
262
|
+
return { licensed: true, key: entered.trim().toUpperCase() };
|
|
263
|
+
}
|
|
264
|
+
return { licensed: false, proceed: true };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (choice === '3') {
|
|
268
|
+
openBrowser(URL_MASTER);
|
|
269
|
+
const entered = await promptQuestion('Enter License Key after purchase (or Enter to skip): ');
|
|
270
|
+
if (entered) {
|
|
271
|
+
saveLicenseKey(entered.trim().toUpperCase());
|
|
272
|
+
process.stdout.write('\x1b[32m✔ License saved to ~/.chemical-x/config.json!\x1b[0m\n\n');
|
|
273
|
+
return { licensed: true, key: entered.trim().toUpperCase() };
|
|
274
|
+
}
|
|
275
|
+
return { licensed: false, proceed: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (choice === '4') {
|
|
279
|
+
process.exit(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { licensed: false, proceed: true };
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const fetchStarterKitFiles = async (licenseKey) => {
|
|
286
|
+
const normalizedKey = licenseKey.trim().toUpperCase();
|
|
287
|
+
const deviceId = getOrCreateDeviceId();
|
|
288
|
+
|
|
289
|
+
process.stdout.write(`\nVerifying license via edge: ${API_BASE}...\n`);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch(`${API_BASE}/api/starter-kit/download`, {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: { 'Content-Type': 'application/json' },
|
|
295
|
+
body: JSON.stringify({ licenseKey: normalizedKey, deviceId })
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const responseData = await res.json();
|
|
299
|
+
const isSuccess = Boolean(res.ok && responseData.valid);
|
|
300
|
+
|
|
301
|
+
if (!isSuccess) {
|
|
302
|
+
process.stderr.write(
|
|
303
|
+
`\x1b[31m✕ License Verification Failed: ${responseData.error || 'Invalid key.'}\x1b[0m\n`
|
|
304
|
+
);
|
|
305
|
+
process.stderr.write(`Purchase key at: ${URL_STANDARD}\n\n`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
saveLicenseKey(normalizedKey);
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`\x1b[32m✔ Verified License for @${responseData.githubUser || 'sponsor'}\x1b[0m\n\n`
|
|
312
|
+
);
|
|
313
|
+
return responseData.files || {};
|
|
314
|
+
} catch (err) {
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
`\x1b[31m✕ Network Error: Failed to reach edge server (${err.message}).\x1b[0m\n`
|
|
317
|
+
);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
};
|