chati-dev 4.0.4 → 4.0.6
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/bin/chati.js +31 -0
- package/framework/hooks/license-guard.js +140 -0
- package/framework/hooks/settings.json +4 -0
- package/framework/orchestrator/chati.md +33 -0
- package/package.json +1 -1
- package/src/license/client.js +138 -0
- package/src/license/commands.js +96 -0
- package/src/license/machine-id.js +24 -0
- package/src/telemetry/sender.js +1 -1
- package/src/wizard/index.js +39 -1
package/bin/chati.js
CHANGED
|
@@ -338,6 +338,11 @@ Intelligence:
|
|
|
338
338
|
npx chati-dev registry [stats|check] Entity registry
|
|
339
339
|
npx chati-dev health System health check
|
|
340
340
|
|
|
341
|
+
License:
|
|
342
|
+
npx chati-dev activate [--key=CHATI-XXXX] Activate license on this machine
|
|
343
|
+
npx chati-dev deactivate Remove license key locally
|
|
344
|
+
npx chati-dev license Show license status
|
|
345
|
+
|
|
341
346
|
Telemetry:
|
|
342
347
|
npx chati-dev telemetry Show telemetry status
|
|
343
348
|
npx chati-dev telemetry enable Enable anonymous telemetry
|
|
@@ -347,6 +352,32 @@ Telemetry:
|
|
|
347
352
|
break;
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
case 'activate': {
|
|
356
|
+
const keyFlag = args.find(a => a.startsWith('--key='));
|
|
357
|
+
const keyArg = keyFlag ? keyFlag.replace('--key=', '') : null;
|
|
358
|
+
const { runActivate } = await import('../src/license/commands.js');
|
|
359
|
+
await runActivate(targetDir, keyArg);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
case 'deactivate': {
|
|
364
|
+
const { runDeactivate } = await import('../src/license/commands.js');
|
|
365
|
+
runDeactivate(targetDir);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'license': {
|
|
370
|
+
const licSubCmd = args[1] || 'status';
|
|
371
|
+
if (licSubCmd === 'deactivate') {
|
|
372
|
+
const { runDeactivate } = await import('../src/license/commands.js');
|
|
373
|
+
runDeactivate(targetDir);
|
|
374
|
+
} else {
|
|
375
|
+
const { runLicenseStatus } = await import('../src/license/commands.js');
|
|
376
|
+
await runLicenseStatus(targetDir);
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
350
381
|
default: {
|
|
351
382
|
console.error(`Unknown command: ${command}`);
|
|
352
383
|
console.error("Run 'npx chati-dev --help' for usage.");
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* License Guard Hook — UserPromptSubmit
|
|
4
|
+
*
|
|
5
|
+
* Validates license status before each user prompt turn.
|
|
6
|
+
* Uses a 24h cache stored in ~/.chati-dev/license.yaml (global, all projects).
|
|
7
|
+
*
|
|
8
|
+
* Behavior:
|
|
9
|
+
* - No key configured → allow (orchestrator handles inline activation)
|
|
10
|
+
* - Cache VALID < 24h → allow silently
|
|
11
|
+
* - Cache EXPIRED/INVALID < 24h → block with renewal message
|
|
12
|
+
* - Cache stale (> 24h) → call API, refresh cache, then allow or block
|
|
13
|
+
* - API unreachable → fail open (allow), never block work
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { homedir } from 'os';
|
|
19
|
+
|
|
20
|
+
const API_BASE = 'https://chati.dev/api';
|
|
21
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
22
|
+
|
|
23
|
+
const GLOBAL_DIR = join(homedir(), '.chati-dev');
|
|
24
|
+
const LICENSE_PATH = join(GLOBAL_DIR, 'license.yaml');
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
let input = '';
|
|
28
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Read global license file
|
|
32
|
+
if (!existsSync(LICENSE_PATH)) {
|
|
33
|
+
allow(); // No license configured — orchestrator handles inline activation
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const licenseRaw = readFileSync(LICENSE_PATH, 'utf-8');
|
|
38
|
+
const licenseKey = readYamlField(licenseRaw, 'key');
|
|
39
|
+
|
|
40
|
+
if (!licenseKey || licenseKey === 'null') {
|
|
41
|
+
allow();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check cache validity
|
|
46
|
+
const status = readYamlField(licenseRaw, 'status');
|
|
47
|
+
const checkedAt = readYamlField(licenseRaw, 'checked_at');
|
|
48
|
+
const age = checkedAt ? Date.now() - new Date(checkedAt).getTime() : Infinity;
|
|
49
|
+
|
|
50
|
+
if (age < CACHE_TTL_MS) {
|
|
51
|
+
if (status === 'VALID') { allow(); return; }
|
|
52
|
+
if (status === 'EXPIRED' || status === 'INVALID') {
|
|
53
|
+
block(buildMessage(status, readYamlField(licenseRaw, 'reason')));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Cache stale — call API
|
|
59
|
+
try {
|
|
60
|
+
const machineId = await computeMachineId();
|
|
61
|
+
const res = await fetch(
|
|
62
|
+
`${API_BASE}/license/validate?key=${encodeURIComponent(licenseKey)}&machine_id=${encodeURIComponent(machineId)}`,
|
|
63
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
64
|
+
);
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
|
|
67
|
+
// Persist updated status (keep existing key)
|
|
68
|
+
const existing = parseYaml(licenseRaw);
|
|
69
|
+
const updated = {
|
|
70
|
+
...existing,
|
|
71
|
+
status: data.status,
|
|
72
|
+
plan: data.plan ?? '',
|
|
73
|
+
days_remaining: data.days_remaining ?? '',
|
|
74
|
+
expires_at: data.expires_at ?? '',
|
|
75
|
+
reason: data.reason ?? '',
|
|
76
|
+
checked_at: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
79
|
+
writeFileSync(LICENSE_PATH, dumpYaml(updated));
|
|
80
|
+
|
|
81
|
+
if (data.status === 'VALID') { allow(); return; }
|
|
82
|
+
block(buildMessage(data.status, data.reason));
|
|
83
|
+
} catch {
|
|
84
|
+
allow(); // Fail open — API unreachable
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
allow(); // Parse error — fail open
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function allow() {
|
|
92
|
+
process.stdout.write(JSON.stringify({ decision: 'allow' }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function block(reason) {
|
|
96
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildMessage(status, reason) {
|
|
100
|
+
if (status === 'EXPIRED') {
|
|
101
|
+
return `chati.dev license expired. Renew at https://chati.dev/pricing\nAfter renewing: npx chati-dev activate --key=YOUR-KEY`;
|
|
102
|
+
}
|
|
103
|
+
return `chati.dev license invalid${reason ? ` (${reason})` : ''}.\nRun: npx chati-dev activate --key=YOUR-KEY\nOr visit https://chati.dev/pricing`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readYamlField(raw, field) {
|
|
107
|
+
const match = raw.match(new RegExp(`^${field}:\\s*(.+)$`, 'm'));
|
|
108
|
+
return match ? match[1].trim().replace(/^["']|["']$/g, '') : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Minimal YAML object parser for simple key:value files */
|
|
112
|
+
function parseYaml(raw) {
|
|
113
|
+
const obj = {};
|
|
114
|
+
for (const line of raw.split('\n')) {
|
|
115
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
116
|
+
if (m) obj[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
117
|
+
}
|
|
118
|
+
return obj;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Minimal YAML serializer for simple flat objects */
|
|
122
|
+
function dumpYaml(obj) {
|
|
123
|
+
return Object.entries(obj)
|
|
124
|
+
.map(([k, v]) => `${k}: ${v == null ? '' : String(v).includes(':') ? `'${v}'` : v}`)
|
|
125
|
+
.join('\n') + '\n';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function computeMachineId() {
|
|
129
|
+
const { createHash } = await import('crypto');
|
|
130
|
+
const os = await import('os');
|
|
131
|
+
const ifaces = Object.values(os.networkInterfaces()).flat();
|
|
132
|
+
const mac = ifaces.find(i => i && !i.internal && i.mac !== '00:00:00:00:00:00')?.mac ?? 'no-mac';
|
|
133
|
+
const raw = [os.hostname(), os.platform(), os.arch(), os.cpus()[0]?.model ?? '', os.userInfo().username, mac].join('|');
|
|
134
|
+
return createHash('sha256').update(raw).digest('hex').substring(0, 16);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
import { fileURLToPath } from 'url';
|
|
138
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
139
|
+
main();
|
|
140
|
+
}
|
|
@@ -18,6 +18,39 @@ You are the **Chati.dev Orchestrator**, the single entry point for the Chati.dev
|
|
|
18
18
|
|
|
19
19
|
When the user invokes `/chati`, execute this sequence:
|
|
20
20
|
|
|
21
|
+
### Pre-Flight: License Check
|
|
22
|
+
|
|
23
|
+
FIRST — before anything else:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
1. Read ~/.chati-dev/license.yaml
|
|
27
|
+
|
|
28
|
+
If file is MISSING or status = "MISSING":
|
|
29
|
+
Say:
|
|
30
|
+
"Welcome to chati.dev! Before we start, I need to activate your license.
|
|
31
|
+
|
|
32
|
+
Option 1 — Paste your license key below.
|
|
33
|
+
Option 2 — Get a free 14-day trial: https://chati.dev/pricing
|
|
34
|
+
|
|
35
|
+
Enter your key (or press Enter if you just received one via email):"
|
|
36
|
+
|
|
37
|
+
→ User provides key
|
|
38
|
+
→ Run: `npx chati-dev activate --key={key}`
|
|
39
|
+
→ Re-read ~/.chati-dev/license.yaml
|
|
40
|
+
→ If status = VALID: "✓ Activated! Let's build." → proceed to Step 1
|
|
41
|
+
→ If error: show error message, ask to try again (max 3 attempts, then stop)
|
|
42
|
+
|
|
43
|
+
If status = "EXPIRED":
|
|
44
|
+
Say:
|
|
45
|
+
"Your chati.dev license has expired.
|
|
46
|
+
Renew at https://chati.dev/pricing
|
|
47
|
+
After renewing, run: npx chati-dev activate --key=YOUR-KEY"
|
|
48
|
+
→ STOP. Do not proceed.
|
|
49
|
+
|
|
50
|
+
If status = "VALID":
|
|
51
|
+
→ Proceed silently. Zero mention of license in normal flow.
|
|
52
|
+
```
|
|
53
|
+
|
|
21
54
|
### Step 1: Load Context
|
|
22
55
|
```
|
|
23
56
|
1. Read .chati/session.yaml
|
package/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { getMachineId, getMachineName } from './machine-id.js';
|
|
6
|
+
|
|
7
|
+
const API_BASE = 'https://chati.dev/api';
|
|
8
|
+
export const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Global storage — ~/.chati-dev/license.yaml
|
|
12
|
+
// One activation covers ALL projects on this machine.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export function getGlobalDir() {
|
|
16
|
+
return join(homedir(), '.chati-dev');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getGlobalLicensePath() {
|
|
20
|
+
return join(getGlobalDir(), 'license.yaml');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readGlobal() {
|
|
24
|
+
const p = getGlobalLicensePath();
|
|
25
|
+
if (!existsSync(p)) return {};
|
|
26
|
+
try { return yaml.load(readFileSync(p, 'utf-8')) || {}; } catch { return {}; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeGlobal(data) {
|
|
30
|
+
mkdirSync(getGlobalDir(), { recursive: true });
|
|
31
|
+
writeFileSync(getGlobalLicensePath(), yaml.dump(data, { lineWidth: -1 }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Key helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Returns license key or null. projectDir param accepted but unused (global storage). */
|
|
39
|
+
export function getLicenseKey(_projectDir) {
|
|
40
|
+
const key = readGlobal().key;
|
|
41
|
+
return key && key !== 'null' ? key : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function setLicenseKey(_projectDir, key) {
|
|
45
|
+
const data = readGlobal();
|
|
46
|
+
data.key = key ?? null;
|
|
47
|
+
writeGlobal(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Status cache (stored in same global file)
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export function getLicenseStatus(_projectDir) {
|
|
55
|
+
const data = readGlobal();
|
|
56
|
+
return Object.keys(data).length ? data : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function saveLicenseStatus(_projectDir, status) {
|
|
60
|
+
const existing = readGlobal();
|
|
61
|
+
writeGlobal({ ...existing, ...status });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// API operations
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Activate a license key on this machine.
|
|
70
|
+
* Saves key + status to ~/.chati-dev/license.yaml.
|
|
71
|
+
* projectDir param accepted but unused.
|
|
72
|
+
*/
|
|
73
|
+
export async function activateLicense(_projectDir, key) {
|
|
74
|
+
const machineId = getMachineId();
|
|
75
|
+
const machineName = getMachineName();
|
|
76
|
+
|
|
77
|
+
const res = await fetch(`${API_BASE}/license/activate`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ key, machine_id: machineId, machine_name: machineName }),
|
|
81
|
+
signal: AbortSignal.timeout(10000),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
if (!res.ok) throw new Error(data.error || 'Activation failed');
|
|
86
|
+
|
|
87
|
+
writeGlobal({
|
|
88
|
+
key,
|
|
89
|
+
status: 'VALID',
|
|
90
|
+
plan: data.plan ?? 'trial',
|
|
91
|
+
days_remaining: data.days_remaining ?? null,
|
|
92
|
+
expires_at: data.expires_at ?? null,
|
|
93
|
+
checked_at: new Date().toISOString(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate current license against API and refresh cache.
|
|
101
|
+
*/
|
|
102
|
+
export async function validateLicense(_projectDir) {
|
|
103
|
+
const key = getLicenseKey();
|
|
104
|
+
if (!key) return { status: 'MISSING' };
|
|
105
|
+
|
|
106
|
+
const machineId = getMachineId();
|
|
107
|
+
const res = await fetch(
|
|
108
|
+
`${API_BASE}/license/validate?key=${encodeURIComponent(key)}&machine_id=${encodeURIComponent(machineId)}`,
|
|
109
|
+
{ signal: AbortSignal.timeout(5000) }
|
|
110
|
+
);
|
|
111
|
+
const data = await res.json();
|
|
112
|
+
|
|
113
|
+
const result = {
|
|
114
|
+
key,
|
|
115
|
+
status: data.status,
|
|
116
|
+
plan: data.plan ?? null,
|
|
117
|
+
days_remaining: data.days_remaining ?? null,
|
|
118
|
+
expires_at: data.expires_at ?? null,
|
|
119
|
+
reason: data.reason ?? null,
|
|
120
|
+
checked_at: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
writeGlobal(result);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Remove license locally (does not deactivate machine slot on server).
|
|
129
|
+
*/
|
|
130
|
+
export function deactivateLicense(_projectDir) {
|
|
131
|
+
const data = readGlobal();
|
|
132
|
+
writeGlobal({
|
|
133
|
+
...data,
|
|
134
|
+
key: null,
|
|
135
|
+
status: 'MISSING',
|
|
136
|
+
checked_at: new Date().toISOString(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License CLI command handlers.
|
|
3
|
+
* Used by bin/chati.js for: activate, deactivate, license
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { text, isCancel } from '@clack/prompts';
|
|
7
|
+
import { activateLicense, deactivateLicense, getLicenseKey, getLicenseStatus, validateLicense } from './client.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// activate
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export async function runActivate(projectDir, keyArg) {
|
|
14
|
+
let key = keyArg;
|
|
15
|
+
|
|
16
|
+
if (!key) {
|
|
17
|
+
const input = await text({
|
|
18
|
+
message: 'Enter your license key (format: CHATI-XXXX-XXXX-XXXX):',
|
|
19
|
+
placeholder: 'CHATI-XXXX-XXXX-XXXX',
|
|
20
|
+
validate(v) {
|
|
21
|
+
if (!v || !v.trim()) return 'Key is required';
|
|
22
|
+
if (!v.trim().startsWith('CHATI-')) return 'Key must start with CHATI-';
|
|
23
|
+
return undefined;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (isCancel(input)) {
|
|
27
|
+
console.log('Activation cancelled.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
key = input.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`Activating ${key}...`);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await activateLicense(projectDir, key);
|
|
37
|
+
const plan = result.plan ?? 'trial';
|
|
38
|
+
const days = result.days_remaining ?? '?';
|
|
39
|
+
console.log(`\n✓ Activated! Plan: ${plan} — ${days} day(s) remaining.`);
|
|
40
|
+
console.log(` Key saved to chati.dev/config.yaml`);
|
|
41
|
+
console.log(` Run /chati to start.\n`);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(`\n✗ Activation failed: ${err.message}`);
|
|
44
|
+
console.error(` Visit https://chati.dev/pricing to get a key.\n`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// deactivate
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export function runDeactivate(projectDir) {
|
|
54
|
+
const key = getLicenseKey(projectDir);
|
|
55
|
+
if (!key) {
|
|
56
|
+
console.log('No license key found locally. Nothing to remove.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
deactivateLicense(projectDir);
|
|
60
|
+
console.log(`\n✓ License key removed locally.`);
|
|
61
|
+
console.log(` To free up a machine slot, visit https://chati.dev/dashboard\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// license status
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export async function runLicenseStatus(projectDir) {
|
|
69
|
+
const key = getLicenseKey(projectDir);
|
|
70
|
+
|
|
71
|
+
if (!key) {
|
|
72
|
+
console.log('\nLicense: Not activated');
|
|
73
|
+
console.log(' Get a free 14-day trial at https://chati.dev/pricing\n');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\nChecking license...`);
|
|
78
|
+
|
|
79
|
+
let status;
|
|
80
|
+
try {
|
|
81
|
+
status = await validateLicense(projectDir);
|
|
82
|
+
} catch {
|
|
83
|
+
// Fallback to cache
|
|
84
|
+
status = getLicenseStatus(projectDir) ?? { status: 'UNKNOWN' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('\nLicense Status');
|
|
88
|
+
console.log('='.repeat(30));
|
|
89
|
+
console.log(` Key: ${key}`);
|
|
90
|
+
console.log(` Status: ${status.status}`);
|
|
91
|
+
if (status.plan) console.log(` Plan: ${status.plan}`);
|
|
92
|
+
if (status.days_remaining != null) console.log(` Days remaining: ${status.days_remaining}`);
|
|
93
|
+
if (status.expires_at) console.log(` Expires: ${status.expires_at}`);
|
|
94
|
+
if (status.reason) console.log(` Reason: ${status.reason}`);
|
|
95
|
+
console.log();
|
|
96
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a stable 16-char machine fingerprint.
|
|
6
|
+
* Based on: hostname, platform, arch, CPU model, username, MAC address.
|
|
7
|
+
*/
|
|
8
|
+
export function getMachineId() {
|
|
9
|
+
const ifaces = Object.values(os.networkInterfaces()).flat();
|
|
10
|
+
const mac = ifaces.find(i => i && !i.internal && i.mac !== '00:00:00:00:00:00')?.mac ?? 'no-mac';
|
|
11
|
+
const raw = [
|
|
12
|
+
os.hostname(),
|
|
13
|
+
os.platform(),
|
|
14
|
+
os.arch(),
|
|
15
|
+
os.cpus()[0]?.model ?? '',
|
|
16
|
+
os.userInfo().username,
|
|
17
|
+
mac,
|
|
18
|
+
].join('|');
|
|
19
|
+
return crypto.createHash('sha256').update(raw).digest('hex').substring(0, 16);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getMachineName() {
|
|
23
|
+
return os.hostname();
|
|
24
|
+
}
|
package/src/telemetry/sender.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Default Endpoint
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
|
-
export const DEFAULT_ENDPOINT = 'https://chati
|
|
12
|
+
export const DEFAULT_ENDPOINT = 'https://chati.dev/api/telemetry';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Sender
|
package/src/wizard/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from
|
|
|
11
11
|
import { sendEvents } from '../telemetry/sender.js';
|
|
12
12
|
import { getTelemetryConfig } from '../telemetry/config.js';
|
|
13
13
|
import { t } from './i18n.js';
|
|
14
|
+
import { getLicenseKey, activateLicense } from '../license/client.js';
|
|
14
15
|
import { DEFAULT_MCPS } from '../config/mcp-configs.js';
|
|
15
16
|
import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
|
|
16
17
|
|
|
@@ -29,11 +30,14 @@ export async function runWizard(targetDir, options = {}) {
|
|
|
29
30
|
logoText = 'chati.dev';
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Step 1: Logo +
|
|
33
|
+
// Step 1: Logo + License Activation (first thing, before anything else)
|
|
33
34
|
logBanner(logoText, VERSION);
|
|
34
35
|
|
|
35
36
|
p.intro('Setting up chati.dev');
|
|
36
37
|
|
|
38
|
+
await runLicenseActivationStep();
|
|
39
|
+
|
|
40
|
+
// Step 2: Language Selection
|
|
37
41
|
const language = options.language || await stepLanguage();
|
|
38
42
|
|
|
39
43
|
// Step 2: Terms of Use (must accept to continue)
|
|
@@ -195,3 +199,37 @@ export async function runWizard(targetDir, options = {}) {
|
|
|
195
199
|
return { success: false, error: err.message };
|
|
196
200
|
}
|
|
197
201
|
}
|
|
202
|
+
|
|
203
|
+
async function runLicenseActivationStep() {
|
|
204
|
+
const existingKey = getLicenseKey();
|
|
205
|
+
if (existingKey) return; // already activated, proceed silently
|
|
206
|
+
|
|
207
|
+
console.log('\n ┌─ License Activation ─────────────────────────────────────────┐');
|
|
208
|
+
console.log(' │ Your license key works across ALL projects on this machine. │');
|
|
209
|
+
console.log(' │ Get a free 14-day trial at: https://chati.dev/pricing │');
|
|
210
|
+
console.log(' └──────────────────────────────────────────────────────────────┘\n');
|
|
211
|
+
|
|
212
|
+
const keyInput = await p.text({
|
|
213
|
+
message: 'Enter your license key (press Enter to skip — activate later with: npx chati-dev activate):',
|
|
214
|
+
placeholder: 'CHATI-XXXX-XXXX-XXXX',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (p.isCancel(keyInput) || !keyInput?.trim()) {
|
|
218
|
+
console.log(' Skipped. Run: npx chati-dev activate --key=YOUR-KEY when ready.\n');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const activateSpinner = createSpinner('Activating license...');
|
|
223
|
+
activateSpinner.start();
|
|
224
|
+
try {
|
|
225
|
+
const result = await activateLicense(null, keyInput.trim());
|
|
226
|
+
activateSpinner.stop();
|
|
227
|
+
const plan = result.plan ?? 'trial';
|
|
228
|
+
const days = result.days_remaining ?? '?';
|
|
229
|
+
console.log(` ✓ License activated! Plan: ${plan} — ${days} day(s) remaining.\n`);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
activateSpinner.stop();
|
|
232
|
+
console.log(` ✗ Activation failed: ${err.message}`);
|
|
233
|
+
console.log(' Run: npx chati-dev activate --key=YOUR-KEY to try again.\n');
|
|
234
|
+
}
|
|
235
|
+
}
|