sinapse-ai 1.12.2 → 1.13.0
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/.claude/CLAUDE.md +2 -2
- package/.claude/rules/agent-memory-imports.md +2 -0
- package/.sinapse-ai/cli/commands/pro/index.js +8 -48
- package/.sinapse-ai/constitution.md +6 -2
- package/.sinapse-ai/data/entity-registry.yaml +12 -36
- package/.sinapse-ai/development/agents/architect/MEMORY.md +35 -0
- package/.sinapse-ai/development/agents/data-engineer/MEMORY.md +38 -0
- package/.sinapse-ai/development/agents/developer/MEMORY.md +36 -0
- package/.sinapse-ai/development/agents/devops/MEMORY.md +39 -0
- package/.sinapse-ai/development/agents/product-lead/MEMORY.md +36 -0
- package/.sinapse-ai/development/agents/project-lead/MEMORY.md +36 -0
- package/.sinapse-ai/development/agents/quality-gate/MEMORY.md +35 -0
- package/.sinapse-ai/development/agents/sprint-lead/MEMORY.md +36 -0
- package/.sinapse-ai/development/templates/chrome-brain/knowledge-base/chrome-brain.md +2 -2
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +15 -0
- package/.sinapse-ai/infrastructure/scripts/validate-agents.js +159 -8
- package/.sinapse-ai/install-manifest.yaml +12 -148
- package/AGENTS.md +13 -2
- package/CHANGELOG.md +122 -2
- package/README.en.md +1 -1
- package/README.md +6 -26
- package/bin/cli.js +2 -7
- package/bin/commands/install.js +19 -257
- package/bin/commands/update.js +18 -78
- package/bin/lib/command-generator.js +261 -0
- package/bin/lib/prompts.js +2 -65
- package/bin/modules/chrome-brain-installer.js +2 -1
- package/bin/postinstall.js +6 -6
- package/docs/getting-started.md +0 -1
- package/docs/pt/getting-started.md +1 -1
- package/docs/security/dependabot-triage.md +17 -17
- package/package.json +6 -3
- package/packages/installer/src/wizard/i18n.js +3 -204
- package/packages/installer/src/wizard/index.js +1 -1
- package/packages/installer/src/wizard/questions.js +5 -368
- package/packages/installer/src/wizard/wizard.js +0 -9
- package/scripts/generate-install-manifest.js +6 -0
- package/scripts/validate-changelog-tags.js +97 -0
- package/scripts/validate-no-personal-leaks.js +184 -28
- package/scripts/validate-schemas.js +55 -10
- package/scripts/validate-story-meta.js +25 -12
- package/squads/claude-code-mastery/agents/claude-mastery-chief.md +1 -1
- package/squads/claude-code-mastery/agents/hooks-architect.md +12 -20
- package/squads/claude-code-mastery/tasks/audit-setup.md +1 -1
- package/squads/squad-product/agents/product-orqx.md +0 -1
- package/squads/squad-product/agents/ps-client-product-manager.md +0 -1
- package/squads/squad-product/agents/ps-delivery-manager.md +0 -1
- package/squads/squad-product/agents/ps-discovery-lead.md +0 -1
- package/squads/squad-product/agents/ps-product-analyst.md +0 -1
- package/squads/squad-product/agents/ps-product-ops-specialist.md +0 -1
- package/squads/squad-product/agents/ps-product-strategist.md +0 -1
- package/.sinapse-ai/core/grounding/README.md +0 -83
- package/.sinapse-ai/core/grounding/brand.cjs +0 -29
- package/.sinapse-ai/core/grounding/config-loader.cjs +0 -52
- package/.sinapse-ai/core/grounding/design-system.cjs +0 -29
- package/.sinapse-ai/core/grounding/vault.cjs +0 -36
- package/.sinapse-ai/development/scripts/approval-workflow.js +0 -643
- package/.sinapse-ai/development/scripts/backup-manager.js +0 -607
- package/.sinapse-ai/development/scripts/branch-manager.js +0 -390
- package/.sinapse-ai/development/scripts/code-quality-improver.js +0 -1329
- package/.sinapse-ai/development/scripts/commit-message-generator.js +0 -850
- package/.sinapse-ai/development/scripts/conflict-resolver.js +0 -675
- package/.sinapse-ai/development/scripts/dependency-analyzer.js +0 -638
- package/.sinapse-ai/development/scripts/diff-generator.js +0 -352
- package/.sinapse-ai/development/scripts/git-wrapper.js +0 -462
- package/.sinapse-ai/development/scripts/modification-validator.js +0 -555
- package/.sinapse-ai/development/scripts/performance-analyzer.js +0 -758
- package/.sinapse-ai/development/scripts/refactoring-suggester.js +0 -1148
- package/.sinapse-ai/development/scripts/rollback-handler.js +0 -531
- package/.sinapse-ai/development/scripts/security-checker.js +0 -359
- package/.sinapse-ai/development/scripts/template-engine.js +0 -240
- package/.sinapse-ai/development/scripts/template-validator.js +0 -279
- package/.sinapse-ai/development/scripts/test-generator.js +0 -844
- package/.sinapse-ai/development/scripts/transaction-manager.js +0 -590
- package/.sinapse-ai/development/scripts/yaml-validator.js +0 -397
- package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +0 -162
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +0 -211
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +0 -194
- package/bin/lib/register-grounding-hooks.js +0 -145
- package/docs/guides/agents/traces/execution-traces.zip +0 -0
- package/docs/guides/grounding-setup.md +0 -154
- package/docs/guides/workflows/SINAPSE-WORKFLOWS.zip +0 -0
- package/packages/installer/src/pro/pro-scaffolder.js +0 -449
- package/packages/installer/src/wizard/grounding-config.js +0 -131
- package/packages/installer/src/wizard/pro-setup.js +0 -1439
- package/packages/installer/templates/README.md +0 -16
- package/packages/installer/templates/brand-routing.example.json +0 -12
- package/packages/installer/templates/ds-routing.example.json +0 -12
- package/packages/installer/templates/vault-routing.example.json +0 -12
|
@@ -1,1439 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pro Installation Wizard with License Gate
|
|
3
|
-
*
|
|
4
|
-
* 3-step wizard: (1) License Gate, (2) Install/Scaffold, (3) Verify
|
|
5
|
-
* Supports interactive mode, CI mode (SINAPSE_PRO_KEY/SINAPSE_PRO_EMAIL env vars), and lazy import.
|
|
6
|
-
*
|
|
7
|
-
* License Gate supports two activation methods:
|
|
8
|
-
* - Email + Password authentication (recommended, PRO-11)
|
|
9
|
-
* - License key (legacy, PRO-6)
|
|
10
|
-
*
|
|
11
|
-
* @module wizard/pro-setup
|
|
12
|
-
* @story INS-3.2 — Implement Pro Installation Wizard with License Gate
|
|
13
|
-
* @story PRO-11 — Email Authentication & Buyer-Based Pro Activation
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const { createSpinner, showSuccess, showError, showWarning, showInfo } = require('./feedback');
|
|
19
|
-
const { colors, status } = require('../utils/sinapse-colors');
|
|
20
|
-
const { t, tf } = require('./i18n');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Gold color for Pro branding.
|
|
24
|
-
* Falls back gracefully if chalk hex is unavailable.
|
|
25
|
-
*/
|
|
26
|
-
let gold;
|
|
27
|
-
try {
|
|
28
|
-
const chalk = require('chalk');
|
|
29
|
-
gold = chalk.hex('#FFD700').bold;
|
|
30
|
-
} catch {
|
|
31
|
-
gold = (text) => text;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* License server base URL (same source of truth as license-api.js CONFIG.BASE_URL).
|
|
36
|
-
*/
|
|
37
|
-
const LICENSE_SERVER_URL = process.env.SINAPSE_LICENSE_API_URL || 'https://sinapse-license-server.vercel.app';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Inline License Client — lightweight HTTP client for pre-bootstrap license checks.
|
|
41
|
-
*
|
|
42
|
-
* Used when @sinapse-fullstack/pro is not yet installed (first install scenario).
|
|
43
|
-
* Implements the same interface subset as LicenseApiClient using Node.js native https.
|
|
44
|
-
*/
|
|
45
|
-
class InlineLicenseClient {
|
|
46
|
-
constructor(baseUrl = LICENSE_SERVER_URL) {
|
|
47
|
-
this.baseUrl = baseUrl;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Make an HTTPS request and return parsed JSON.
|
|
52
|
-
* @param {string} method - HTTP method
|
|
53
|
-
* @param {string} urlPath - URL path (e.g., '/api/v1/auth/check-email')
|
|
54
|
-
* @param {Object} [body] - JSON body for POST requests
|
|
55
|
-
* @param {Object} [headers] - Additional headers
|
|
56
|
-
* @returns {Promise<Object>} Parsed JSON response
|
|
57
|
-
*/
|
|
58
|
-
_request(method, urlPath, body, headers = {}) {
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
const https = require('https');
|
|
61
|
-
const url = new URL(urlPath, this.baseUrl);
|
|
62
|
-
|
|
63
|
-
const options = {
|
|
64
|
-
hostname: url.hostname,
|
|
65
|
-
port: url.port || 443,
|
|
66
|
-
path: url.pathname + url.search,
|
|
67
|
-
method,
|
|
68
|
-
headers: {
|
|
69
|
-
'Content-Type': 'application/json',
|
|
70
|
-
'User-Agent': 'sinapse-installer',
|
|
71
|
-
...headers,
|
|
72
|
-
},
|
|
73
|
-
timeout: 15000,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const req = https.request(options, (res) => {
|
|
77
|
-
let data = '';
|
|
78
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
79
|
-
res.on('end', () => {
|
|
80
|
-
try {
|
|
81
|
-
const parsed = JSON.parse(data);
|
|
82
|
-
if (res.statusCode >= 400) {
|
|
83
|
-
const err = new Error(parsed.message || `HTTP ${res.statusCode}`);
|
|
84
|
-
err.code = parsed.code;
|
|
85
|
-
reject(err);
|
|
86
|
-
} else {
|
|
87
|
-
resolve(parsed);
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
reject(new Error(`Invalid JSON response (HTTP ${res.statusCode})`));
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
req.on('error', (err) => {
|
|
96
|
-
const networkErr = new Error(err.message);
|
|
97
|
-
networkErr.code = 'NETWORK_ERROR';
|
|
98
|
-
reject(networkErr);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
req.on('timeout', () => {
|
|
102
|
-
req.destroy();
|
|
103
|
-
const timeoutErr = new Error('Request timeout');
|
|
104
|
-
timeoutErr.code = 'NETWORK_ERROR';
|
|
105
|
-
reject(timeoutErr);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (body) {
|
|
109
|
-
req.write(JSON.stringify(body));
|
|
110
|
-
}
|
|
111
|
-
req.end();
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** @returns {Promise<boolean>} true if license server is reachable */
|
|
116
|
-
async isOnline() {
|
|
117
|
-
try {
|
|
118
|
-
await this._request('GET', '/health');
|
|
119
|
-
return true;
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Check if email is a buyer and has an account.
|
|
127
|
-
* @param {string} email
|
|
128
|
-
* @returns {Promise<{isBuyer: boolean, hasAccount: boolean}>}
|
|
129
|
-
*/
|
|
130
|
-
async checkEmail(email) {
|
|
131
|
-
return this._request('POST', '/api/v1/auth/check-email', { email });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Login with email and password.
|
|
136
|
-
* @param {string} email
|
|
137
|
-
* @param {string} password
|
|
138
|
-
* @returns {Promise<{sessionToken: string, emailVerified: boolean}>}
|
|
139
|
-
*/
|
|
140
|
-
async login(email, password) {
|
|
141
|
-
return this._request('POST', '/api/v1/auth/login', { email, password });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Create a new account.
|
|
146
|
-
* @param {string} email
|
|
147
|
-
* @param {string} password
|
|
148
|
-
* @returns {Promise<Object>}
|
|
149
|
-
*/
|
|
150
|
-
async signup(email, password) {
|
|
151
|
-
return this._request('POST', '/api/v1/auth/signup', { email, password });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Activate Pro using an authenticated session.
|
|
156
|
-
* @param {string} token - Session token
|
|
157
|
-
* @param {string} machineId - Machine fingerprint
|
|
158
|
-
* @param {string} version - sinapse-ai version
|
|
159
|
-
* @returns {Promise<Object>} Activation result
|
|
160
|
-
*/
|
|
161
|
-
async activateByAuth(token, machineId, version) {
|
|
162
|
-
return this._request('POST', '/api/v1/auth/activate-pro', {
|
|
163
|
-
machineId,
|
|
164
|
-
version,
|
|
165
|
-
}, {
|
|
166
|
-
Authorization: `Bearer ${token}`,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Activate Pro using a license key (legacy flow).
|
|
172
|
-
* @param {string} licenseKey - License key
|
|
173
|
-
* @param {string} machineId - Machine fingerprint
|
|
174
|
-
* @param {string} version - sinapse-ai version
|
|
175
|
-
* @returns {Promise<Object>} Activation result
|
|
176
|
-
*/
|
|
177
|
-
async activate(licenseKey, machineId, version) {
|
|
178
|
-
return this._request('POST', '/api/v1/licenses/activate', {
|
|
179
|
-
key: licenseKey,
|
|
180
|
-
machineId,
|
|
181
|
-
version,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Check if user's email has been verified.
|
|
187
|
-
* @param {string} sessionToken - Session token
|
|
188
|
-
* @returns {Promise<{verified: boolean}>}
|
|
189
|
-
*/
|
|
190
|
-
async checkEmailVerified(sessionToken) {
|
|
191
|
-
return this._request('GET', '/api/v1/auth/email-verified', null, {
|
|
192
|
-
Authorization: `Bearer ${sessionToken}`,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Resend verification email.
|
|
198
|
-
* @param {string} email - User email
|
|
199
|
-
* @returns {Promise<Object>}
|
|
200
|
-
*/
|
|
201
|
-
async resendVerification(email) {
|
|
202
|
-
return this._request('POST', '/api/v1/auth/resend-verification', { email });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* License key format: PRO-XXXX-XXXX-XXXX-XXXX
|
|
208
|
-
*/
|
|
209
|
-
const LICENSE_KEY_PATTERN = /^PRO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Maximum retry attempts for license validation.
|
|
213
|
-
*/
|
|
214
|
-
const MAX_RETRIES = 3;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Email verification polling interval in milliseconds.
|
|
218
|
-
*/
|
|
219
|
-
const VERIFY_POLL_INTERVAL_MS = 5000;
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Email verification polling timeout in milliseconds (10 minutes).
|
|
223
|
-
*/
|
|
224
|
-
const VERIFY_POLL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Minimum password length.
|
|
228
|
-
*/
|
|
229
|
-
const MIN_PASSWORD_LENGTH = 8;
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Email format regex.
|
|
233
|
-
*/
|
|
234
|
-
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Detect CI environment.
|
|
238
|
-
*
|
|
239
|
-
* @returns {boolean} true if running in CI or non-interactive terminal
|
|
240
|
-
*/
|
|
241
|
-
function isCIEnvironment() {
|
|
242
|
-
return process.env.CI === 'true' || !process.stdout.isTTY;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Mask a license key for safe display.
|
|
247
|
-
* Shows first and last segments, masks middle two.
|
|
248
|
-
* Example: PRO-ABCD-****-****-WXYZ
|
|
249
|
-
*
|
|
250
|
-
* @param {string} key - License key
|
|
251
|
-
* @returns {string} Masked key
|
|
252
|
-
*/
|
|
253
|
-
function maskLicenseKey(key) {
|
|
254
|
-
if (!key || typeof key !== 'string') {
|
|
255
|
-
return '****';
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const trimmed = key.trim().toUpperCase();
|
|
259
|
-
|
|
260
|
-
if (!LICENSE_KEY_PATTERN.test(trimmed)) {
|
|
261
|
-
return '****';
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const parts = trimmed.split('-');
|
|
265
|
-
return `${parts[0]}-${parts[1]}-****-****-${parts[4]}`;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Validate license key format before sending to API.
|
|
270
|
-
*
|
|
271
|
-
* @param {string} key - License key
|
|
272
|
-
* @returns {boolean} true if format is valid
|
|
273
|
-
*/
|
|
274
|
-
function validateKeyFormat(key) {
|
|
275
|
-
if (!key || typeof key !== 'string') {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
return LICENSE_KEY_PATTERN.test(key.trim().toUpperCase());
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Show the Pro branding header.
|
|
283
|
-
*/
|
|
284
|
-
function showProHeader() {
|
|
285
|
-
const title = t('proWizardTitle');
|
|
286
|
-
const subtitle = t('proWizardSubtitle');
|
|
287
|
-
const maxLen = Math.max(title.length, subtitle.length) + 10;
|
|
288
|
-
const pad = (str) => {
|
|
289
|
-
const totalPad = maxLen - str.length;
|
|
290
|
-
const left = Math.floor(totalPad / 2);
|
|
291
|
-
const right = totalPad - left;
|
|
292
|
-
return ' '.repeat(left) + str + ' '.repeat(right);
|
|
293
|
-
};
|
|
294
|
-
console.log('');
|
|
295
|
-
console.log(gold(' ╔' + '═'.repeat(maxLen) + '╗'));
|
|
296
|
-
console.log(gold(' ║' + pad(title) + '║'));
|
|
297
|
-
console.log(gold(' ║' + pad(subtitle) + '║'));
|
|
298
|
-
console.log(gold(' ╚' + '═'.repeat(maxLen) + '╝'));
|
|
299
|
-
console.log('');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Show step indicator.
|
|
304
|
-
*
|
|
305
|
-
* @param {number} current - Current step (1-based)
|
|
306
|
-
* @param {number} total - Total steps
|
|
307
|
-
* @param {string} label - Step label
|
|
308
|
-
*/
|
|
309
|
-
function showStep(current, total, label) {
|
|
310
|
-
const progress = `[${current}/${total}]`;
|
|
311
|
-
console.log(gold(`\n ${progress} ${label}`));
|
|
312
|
-
console.log(colors.dim(' ' + '─'.repeat(44)));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Try to load a pro license module via multiple resolution paths.
|
|
317
|
-
*
|
|
318
|
-
* Resolution order:
|
|
319
|
-
* 1. Relative path (framework-dev mode: ../../../../pro/license/{name})
|
|
320
|
-
* 2. @sinapse-fullstack/pro package (brownfield: node_modules/@sinapse-fullstack/pro/license/{name})
|
|
321
|
-
* 3. Absolute path via sinapse-ai in node_modules (brownfield upgrade)
|
|
322
|
-
* 4. Absolute path via @sinapse-fullstack/pro in user project (npx context)
|
|
323
|
-
*
|
|
324
|
-
* Path 4 is critical for npx execution: when running `npx sinapse-ai install`,
|
|
325
|
-
* require() resolves from the npx temp directory, not process.cwd(). After
|
|
326
|
-
* bootstrap installs @sinapse-fullstack/pro in the user's project, only an
|
|
327
|
-
* absolute path to process.cwd()/node_modules/@sinapse-fullstack/pro/... works.
|
|
328
|
-
*
|
|
329
|
-
* @param {string} moduleName - Module filename without extension (e.g., 'license-api')
|
|
330
|
-
* @returns {Object|null} Loaded module or null
|
|
331
|
-
*/
|
|
332
|
-
function loadProModule(moduleName) {
|
|
333
|
-
const path = require('path');
|
|
334
|
-
|
|
335
|
-
// 1. Framework-dev mode (cloned repo with pro/ submodule)
|
|
336
|
-
try {
|
|
337
|
-
return require(`../../../../pro/license/${moduleName}`);
|
|
338
|
-
} catch { /* not available */ }
|
|
339
|
-
|
|
340
|
-
// 2. @sinapse-fullstack/pro package (works when sinapse-ai is a local dependency)
|
|
341
|
-
try {
|
|
342
|
-
return require(`@sinapse-fullstack/pro/license/${moduleName}`);
|
|
343
|
-
} catch { /* not available */ }
|
|
344
|
-
|
|
345
|
-
// 3. sinapse-ai in node_modules (brownfield upgrade from >= v4.2.15)
|
|
346
|
-
try {
|
|
347
|
-
const absPath = path.join(process.cwd(), 'node_modules', 'sinapse-ai', 'pro', 'license', moduleName);
|
|
348
|
-
return require(absPath);
|
|
349
|
-
} catch { /* not available */ }
|
|
350
|
-
|
|
351
|
-
// 4. @sinapse-fullstack/pro in user project (npx context — require resolves from
|
|
352
|
-
// temp dir, so we need absolute path to where bootstrap installed the package)
|
|
353
|
-
try {
|
|
354
|
-
const absPath = path.join(process.cwd(), 'node_modules', '@sinapse-fullstack', 'pro', 'license', moduleName);
|
|
355
|
-
return require(absPath);
|
|
356
|
-
} catch { /* not available */ }
|
|
357
|
-
|
|
358
|
-
return null;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Try to load the license API client via lazy import.
|
|
363
|
-
* Attempts multiple resolution paths for framework-dev, greenfield, and brownfield.
|
|
364
|
-
*
|
|
365
|
-
* @returns {{ LicenseApiClient: Function, licenseApi: Object }|null} License API or null
|
|
366
|
-
*/
|
|
367
|
-
function loadLicenseApi() {
|
|
368
|
-
return loadProModule('license-api');
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Try to load the feature gate via lazy import.
|
|
373
|
-
* Attempts multiple resolution paths for framework-dev, greenfield, and brownfield.
|
|
374
|
-
*
|
|
375
|
-
* @returns {{ featureGate: Object }|null} Feature gate or null
|
|
376
|
-
*/
|
|
377
|
-
function loadFeatureGate() {
|
|
378
|
-
return loadProModule('feature-gate');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Get a license API client instance.
|
|
383
|
-
*
|
|
384
|
-
* Prefers the full LicenseApiClient from @sinapse-fullstack/pro when available.
|
|
385
|
-
* Falls back to InlineLicenseClient (native https) for pre-bootstrap scenarios.
|
|
386
|
-
*
|
|
387
|
-
* @returns {Object} Client instance with isOnline, checkEmail, login, signup, activateByAuth
|
|
388
|
-
*/
|
|
389
|
-
function getLicenseClient() {
|
|
390
|
-
const loader = module.exports._testing ? module.exports._testing.loadLicenseApi : loadLicenseApi;
|
|
391
|
-
const licenseModule = loader();
|
|
392
|
-
|
|
393
|
-
if (licenseModule) {
|
|
394
|
-
const { LicenseApiClient } = licenseModule;
|
|
395
|
-
return new LicenseApiClient();
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Fallback: use inline client for pre-bootstrap (no @sinapse-fullstack/pro yet)
|
|
399
|
-
return new InlineLicenseClient();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Try to load the pro scaffolder via lazy import.
|
|
404
|
-
*
|
|
405
|
-
* @returns {{ scaffoldProContent: Function }|null} Scaffolder or null
|
|
406
|
-
*/
|
|
407
|
-
function loadProScaffolder() {
|
|
408
|
-
try {
|
|
409
|
-
return require('../pro/pro-scaffolder');
|
|
410
|
-
} catch {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Step 1: License Gate — authenticate and validate license.
|
|
417
|
-
*
|
|
418
|
-
* Supports two activation methods:
|
|
419
|
-
* 1. Email + Password authentication (recommended, PRO-11)
|
|
420
|
-
* 2. License key (legacy, PRO-6)
|
|
421
|
-
*
|
|
422
|
-
* In CI mode, reads from SINAPSE_PRO_EMAIL + SINAPSE_PRO_PASSWORD or SINAPSE_PRO_KEY env vars.
|
|
423
|
-
* In interactive mode, prompts user to choose method.
|
|
424
|
-
*
|
|
425
|
-
* @param {Object} [options={}] - Options
|
|
426
|
-
* @param {string} [options.key] - Pre-provided key (from CLI args or env)
|
|
427
|
-
* @param {string} [options.email] - Pre-provided email (from CLI args or env)
|
|
428
|
-
* @param {string} [options.password] - Pre-provided password (from CLI args or env)
|
|
429
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
430
|
-
*/
|
|
431
|
-
async function stepLicenseGate(options = {}) {
|
|
432
|
-
showStep(1, 3, t('proLicenseActivation'));
|
|
433
|
-
|
|
434
|
-
const isCI = isCIEnvironment();
|
|
435
|
-
|
|
436
|
-
// CI mode: check env vars
|
|
437
|
-
if (isCI) {
|
|
438
|
-
return stepLicenseGateCI(options);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Pre-provided key (from CLI args)
|
|
442
|
-
if (options.key) {
|
|
443
|
-
return stepLicenseGateWithKey(options.key);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Pre-provided email credentials (from CLI args)
|
|
447
|
-
if (options.email && options.password) {
|
|
448
|
-
return authenticateWithEmail(options.email, options.password);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Interactive mode: prompt for method
|
|
452
|
-
const inquirer = require('inquirer');
|
|
453
|
-
|
|
454
|
-
const { method } = await inquirer.prompt([
|
|
455
|
-
{
|
|
456
|
-
type: 'list',
|
|
457
|
-
name: 'method',
|
|
458
|
-
message: colors.primary(t('proHowActivate')),
|
|
459
|
-
choices: [
|
|
460
|
-
{
|
|
461
|
-
name: t('proLoginOrCreate'),
|
|
462
|
-
value: 'email',
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
name: t('proEnterKey'),
|
|
466
|
-
value: 'key',
|
|
467
|
-
},
|
|
468
|
-
],
|
|
469
|
-
},
|
|
470
|
-
]);
|
|
471
|
-
|
|
472
|
-
if (method === 'email') {
|
|
473
|
-
return stepLicenseGateWithEmail();
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return stepLicenseGateWithKeyInteractive();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* CI mode license gate — reads from env vars.
|
|
481
|
-
*
|
|
482
|
-
* Priority: SINAPSE_PRO_EMAIL + SINAPSE_PRO_PASSWORD > SINAPSE_PRO_KEY
|
|
483
|
-
*
|
|
484
|
-
* @param {Object} options - Options with possible pre-provided credentials
|
|
485
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
486
|
-
*/
|
|
487
|
-
async function stepLicenseGateCI(options) {
|
|
488
|
-
const email = options.email || process.env.SINAPSE_PRO_EMAIL;
|
|
489
|
-
const password = options.password || process.env.SINAPSE_PRO_PASSWORD;
|
|
490
|
-
const key = options.key || process.env.SINAPSE_PRO_KEY;
|
|
491
|
-
|
|
492
|
-
// Prefer email auth over key
|
|
493
|
-
if (email && password) {
|
|
494
|
-
return authenticateWithEmail(email, password);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (key) {
|
|
498
|
-
return stepLicenseGateWithKey(key);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return {
|
|
502
|
-
success: false,
|
|
503
|
-
error: t('proCISetEnv'),
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Interactive email/password license gate flow.
|
|
509
|
-
*
|
|
510
|
-
* New flow (PRO-11 v2):
|
|
511
|
-
* 1. Email → checkEmail API → { isBuyer, hasAccount }
|
|
512
|
-
* 2. NOT buyer → "No Pro access found" → STOP
|
|
513
|
-
* 3. IS buyer + HAS account → Password → Login (with retry) → Activate
|
|
514
|
-
* 4. IS buyer + NO account → Password + Confirm → Signup → Verify email → Login → Activate
|
|
515
|
-
*
|
|
516
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
517
|
-
*/
|
|
518
|
-
async function stepLicenseGateWithEmail() {
|
|
519
|
-
const inquirer = require('inquirer');
|
|
520
|
-
|
|
521
|
-
// Step 1: Get email
|
|
522
|
-
const { email } = await inquirer.prompt([
|
|
523
|
-
{
|
|
524
|
-
type: 'input',
|
|
525
|
-
name: 'email',
|
|
526
|
-
message: colors.primary(t('proEmailLabel')),
|
|
527
|
-
validate: (input) => {
|
|
528
|
-
if (!input || !input.trim()) {
|
|
529
|
-
return t('proEmailRequired');
|
|
530
|
-
}
|
|
531
|
-
if (!EMAIL_PATTERN.test(input.trim())) {
|
|
532
|
-
return t('proEmailInvalid');
|
|
533
|
-
}
|
|
534
|
-
return true;
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
]);
|
|
538
|
-
|
|
539
|
-
const trimmedEmail = email.trim();
|
|
540
|
-
|
|
541
|
-
// Step 2: Check buyer status + account existence
|
|
542
|
-
const client = getLicenseClient();
|
|
543
|
-
|
|
544
|
-
// Check connectivity
|
|
545
|
-
const online = await client.isOnline();
|
|
546
|
-
if (!online) {
|
|
547
|
-
return {
|
|
548
|
-
success: false,
|
|
549
|
-
error: t('proServerUnreachable'),
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const checkSpinner = createSpinner(t('proVerifyingAccess'));
|
|
554
|
-
checkSpinner.start();
|
|
555
|
-
|
|
556
|
-
let checkResult;
|
|
557
|
-
try {
|
|
558
|
-
checkResult = await client.checkEmail(trimmedEmail);
|
|
559
|
-
} catch (checkError) {
|
|
560
|
-
checkSpinner.fail(tf('proVerificationFailed', { message: checkError.message }));
|
|
561
|
-
return { success: false, error: checkError.message };
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Step 2a: NOT a buyer → stop
|
|
565
|
-
if (!checkResult.isBuyer) {
|
|
566
|
-
checkSpinner.fail(t('proNoAccess'));
|
|
567
|
-
console.log('');
|
|
568
|
-
showInfo(t('proContactSupport'));
|
|
569
|
-
showInfo(' Issues: https://github.com/caioimori/sinapse-ai/issues');
|
|
570
|
-
showInfo(' ' + t('proPurchase'));
|
|
571
|
-
return { success: false, error: t('proEmailNotBuyer') };
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Step 2b: IS a buyer
|
|
575
|
-
if (checkResult.hasAccount) {
|
|
576
|
-
checkSpinner.succeed(t('proAccessConfirmedAccount'));
|
|
577
|
-
// Flow 3: Existing account → Login with password (retry loop)
|
|
578
|
-
return loginWithRetry(client, trimmedEmail);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
checkSpinner.succeed(t('proAccessConfirmedCreate'));
|
|
582
|
-
// Flow 4: New account → Create account flow
|
|
583
|
-
return createAccountFlow(client, trimmedEmail);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Login flow with password retry (max 3 attempts).
|
|
588
|
-
*
|
|
589
|
-
* @param {object} client - LicenseApiClient instance
|
|
590
|
-
* @param {string} email - Verified buyer email
|
|
591
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
592
|
-
*/
|
|
593
|
-
async function loginWithRetry(client, email) {
|
|
594
|
-
const inquirer = require('inquirer');
|
|
595
|
-
|
|
596
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
597
|
-
const { password } = await inquirer.prompt([
|
|
598
|
-
{
|
|
599
|
-
type: 'password',
|
|
600
|
-
name: 'password',
|
|
601
|
-
message: colors.primary(t('proPasswordLabel')),
|
|
602
|
-
mask: '*',
|
|
603
|
-
validate: (input) => {
|
|
604
|
-
if (!input || input.length < MIN_PASSWORD_LENGTH) {
|
|
605
|
-
return tf('proPasswordMin', { min: MIN_PASSWORD_LENGTH });
|
|
606
|
-
}
|
|
607
|
-
return true;
|
|
608
|
-
},
|
|
609
|
-
},
|
|
610
|
-
]);
|
|
611
|
-
|
|
612
|
-
const spinner = createSpinner(t('proAuthenticating'));
|
|
613
|
-
spinner.start();
|
|
614
|
-
|
|
615
|
-
try {
|
|
616
|
-
const loginResult = await client.login(email, password);
|
|
617
|
-
spinner.succeed(t('proAuthSuccess'));
|
|
618
|
-
|
|
619
|
-
// Wait for email verification if needed
|
|
620
|
-
if (!loginResult.emailVerified) {
|
|
621
|
-
const verifyResult = await waitForEmailVerification(client, loginResult.sessionToken, email);
|
|
622
|
-
if (!verifyResult.success) {
|
|
623
|
-
return verifyResult;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Activate Pro
|
|
628
|
-
return activateProByAuth(client, loginResult.sessionToken);
|
|
629
|
-
} catch (loginError) {
|
|
630
|
-
if (loginError.code === 'EMAIL_NOT_VERIFIED') {
|
|
631
|
-
// Email not verified — poll by retrying login until verified
|
|
632
|
-
spinner.info(t('proEmailNotVerified'));
|
|
633
|
-
console.log(colors.dim(' ' + t('proCheckingEvery')));
|
|
634
|
-
|
|
635
|
-
const startTime = Date.now();
|
|
636
|
-
while (Date.now() - startTime < VERIFY_POLL_TIMEOUT_MS) {
|
|
637
|
-
await new Promise((resolve) => setTimeout(resolve, VERIFY_POLL_INTERVAL_MS));
|
|
638
|
-
try {
|
|
639
|
-
const retryLogin = await client.login(email, password);
|
|
640
|
-
showSuccess(t('proEmailVerified'));
|
|
641
|
-
if (!retryLogin.emailVerified) {
|
|
642
|
-
const verifyResult = await waitForEmailVerification(client, retryLogin.sessionToken, email);
|
|
643
|
-
if (!verifyResult.success) return verifyResult;
|
|
644
|
-
}
|
|
645
|
-
return activateProByAuth(client, retryLogin.sessionToken);
|
|
646
|
-
} catch (retryError) {
|
|
647
|
-
if (retryError.code !== 'EMAIL_NOT_VERIFIED') {
|
|
648
|
-
return { success: false, error: retryError.message };
|
|
649
|
-
}
|
|
650
|
-
// Still not verified, continue polling
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
showError(t('proVerificationTimeout'));
|
|
655
|
-
showInfo(t('proRunAgain'));
|
|
656
|
-
return { success: false, error: t('proVerificationTimeout') };
|
|
657
|
-
} else if (loginError.code === 'INVALID_CREDENTIALS') {
|
|
658
|
-
const remaining = MAX_RETRIES - attempt;
|
|
659
|
-
if (remaining > 0) {
|
|
660
|
-
spinner.fail(`Incorrect password. ${remaining} attempt${remaining > 1 ? 's' : ''} remaining.`);
|
|
661
|
-
showInfo('Forgot your password? Visit https://sinapse-license-server.vercel.app/reset-password');
|
|
662
|
-
} else {
|
|
663
|
-
spinner.fail('Maximum login attempts reached.');
|
|
664
|
-
showInfo('Forgot your password? Visit https://sinapse-license-server.vercel.app/reset-password');
|
|
665
|
-
showInfo('Or open an issue: https://github.com/caioimori/sinapse-ai/issues');
|
|
666
|
-
return { success: false, error: 'Maximum login attempts reached.' };
|
|
667
|
-
}
|
|
668
|
-
} else if (loginError.code === 'AUTH_RATE_LIMITED') {
|
|
669
|
-
spinner.fail(loginError.message);
|
|
670
|
-
return { success: false, error: loginError.message };
|
|
671
|
-
} else {
|
|
672
|
-
spinner.fail(tf('proAuthFailed', { message: loginError.message }));
|
|
673
|
-
return { success: false, error: loginError.message };
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
return { success: false, error: t('proMaxAttempts') };
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Create account flow for new buyers.
|
|
683
|
-
*
|
|
684
|
-
* Asks for password, creates account, waits for email verification.
|
|
685
|
-
*
|
|
686
|
-
* @param {object} client - LicenseApiClient instance
|
|
687
|
-
* @param {string} email - Verified buyer email
|
|
688
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
689
|
-
*/
|
|
690
|
-
async function createAccountFlow(client, email) {
|
|
691
|
-
const inquirer = require('inquirer');
|
|
692
|
-
|
|
693
|
-
console.log('');
|
|
694
|
-
showInfo(t('proCreateAccount'));
|
|
695
|
-
|
|
696
|
-
// Ask for password with confirmation
|
|
697
|
-
const { newPassword } = await inquirer.prompt([
|
|
698
|
-
{
|
|
699
|
-
type: 'password',
|
|
700
|
-
name: 'newPassword',
|
|
701
|
-
message: colors.primary(t('proChoosePassword')),
|
|
702
|
-
mask: '*',
|
|
703
|
-
validate: (input) => {
|
|
704
|
-
if (!input || input.length < MIN_PASSWORD_LENGTH) {
|
|
705
|
-
return tf('proPasswordMin', { min: MIN_PASSWORD_LENGTH });
|
|
706
|
-
}
|
|
707
|
-
return true;
|
|
708
|
-
},
|
|
709
|
-
},
|
|
710
|
-
]);
|
|
711
|
-
|
|
712
|
-
const { confirmPassword } = await inquirer.prompt([
|
|
713
|
-
{
|
|
714
|
-
type: 'password',
|
|
715
|
-
name: 'confirmPassword',
|
|
716
|
-
message: colors.primary(t('proConfirmPassword')),
|
|
717
|
-
mask: '*',
|
|
718
|
-
validate: (input) => {
|
|
719
|
-
if (input !== newPassword) {
|
|
720
|
-
return t('proPasswordsNoMatch');
|
|
721
|
-
}
|
|
722
|
-
return true;
|
|
723
|
-
},
|
|
724
|
-
},
|
|
725
|
-
]);
|
|
726
|
-
|
|
727
|
-
// Create account
|
|
728
|
-
const spinner = createSpinner(t('proCreatingAccount'));
|
|
729
|
-
spinner.start();
|
|
730
|
-
|
|
731
|
-
let sessionToken;
|
|
732
|
-
try {
|
|
733
|
-
await client.signup(email, confirmPassword);
|
|
734
|
-
spinner.succeed(t('proAccountCreated'));
|
|
735
|
-
} catch (signupError) {
|
|
736
|
-
if (signupError.code === 'EMAIL_ALREADY_REGISTERED') {
|
|
737
|
-
spinner.info(t('proAccountExists'));
|
|
738
|
-
return loginWithRetry(client, email);
|
|
739
|
-
}
|
|
740
|
-
spinner.fail(tf('proAccountFailed', { message: signupError.message }));
|
|
741
|
-
return { success: false, error: signupError.message };
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Wait for email verification
|
|
745
|
-
console.log('');
|
|
746
|
-
showInfo(t('proCheckEmail'));
|
|
747
|
-
|
|
748
|
-
// Login after signup to get session token
|
|
749
|
-
try {
|
|
750
|
-
const loginResult = await client.login(email, confirmPassword);
|
|
751
|
-
sessionToken = loginResult.sessionToken;
|
|
752
|
-
} catch {
|
|
753
|
-
// Login might fail if email not verified yet — that's OK, we'll poll
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (sessionToken) {
|
|
757
|
-
const verifyResult = await waitForEmailVerification(client, sessionToken, email);
|
|
758
|
-
if (!verifyResult.success) {
|
|
759
|
-
return verifyResult;
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
// Need to wait for verification then login
|
|
763
|
-
showInfo(t('proWaitingVerification'));
|
|
764
|
-
showInfo(t('proAfterVerifying'));
|
|
765
|
-
|
|
766
|
-
// Poll by trying to login periodically
|
|
767
|
-
const startTime = Date.now();
|
|
768
|
-
while (Date.now() - startTime < VERIFY_POLL_TIMEOUT_MS) {
|
|
769
|
-
await new Promise((resolve) => setTimeout(resolve, VERIFY_POLL_INTERVAL_MS));
|
|
770
|
-
try {
|
|
771
|
-
const loginResult = await client.login(email, confirmPassword);
|
|
772
|
-
sessionToken = loginResult.sessionToken;
|
|
773
|
-
if (loginResult.emailVerified) {
|
|
774
|
-
showSuccess('Email verified!');
|
|
775
|
-
break;
|
|
776
|
-
}
|
|
777
|
-
// Got session but not verified yet — use the verification polling
|
|
778
|
-
const verifyResult = await waitForEmailVerification(client, sessionToken, email);
|
|
779
|
-
if (!verifyResult.success) {
|
|
780
|
-
return verifyResult;
|
|
781
|
-
}
|
|
782
|
-
break;
|
|
783
|
-
} catch {
|
|
784
|
-
// Still waiting for verification
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (!sessionToken) {
|
|
789
|
-
showError(t('proVerificationTimeout'));
|
|
790
|
-
showInfo(t('proRunAgain'));
|
|
791
|
-
return { success: false, error: t('proVerificationTimeout') };
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
// Activate Pro
|
|
796
|
-
return activateProByAuth(client, sessionToken);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Authenticate with email and password (CI mode / pre-provided credentials).
|
|
801
|
-
*
|
|
802
|
-
* For interactive mode, use stepLicenseGateWithEmail() instead (buyer-first flow).
|
|
803
|
-
* This function is used when credentials are pre-provided (CI, CLI args).
|
|
804
|
-
*
|
|
805
|
-
* @param {string} email - User email
|
|
806
|
-
* @param {string} password - User password
|
|
807
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
808
|
-
*/
|
|
809
|
-
async function authenticateWithEmail(email, password) {
|
|
810
|
-
const client = getLicenseClient();
|
|
811
|
-
|
|
812
|
-
// Check connectivity
|
|
813
|
-
const online = await client.isOnline();
|
|
814
|
-
if (!online) {
|
|
815
|
-
return {
|
|
816
|
-
success: false,
|
|
817
|
-
error: t('proServerUnreachable'),
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// CI mode: check buyer first, then try login or auto-signup
|
|
822
|
-
const checkSpinner = createSpinner(t('proVerifyingAccessShort'));
|
|
823
|
-
checkSpinner.start();
|
|
824
|
-
|
|
825
|
-
try {
|
|
826
|
-
const checkResult = await client.checkEmail(email);
|
|
827
|
-
if (!checkResult.isBuyer) {
|
|
828
|
-
checkSpinner.fail(t('proNoAccess'));
|
|
829
|
-
return { success: false, error: t('proEmailNotBuyer') };
|
|
830
|
-
}
|
|
831
|
-
checkSpinner.succeed(t('proAccessConfirmed'));
|
|
832
|
-
} catch {
|
|
833
|
-
checkSpinner.info(t('proBuyerCheckUnavailable'));
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Try login
|
|
837
|
-
const spinner = createSpinner(t('proAuthenticating'));
|
|
838
|
-
spinner.start();
|
|
839
|
-
|
|
840
|
-
let sessionToken;
|
|
841
|
-
let emailVerified;
|
|
842
|
-
|
|
843
|
-
try {
|
|
844
|
-
const loginResult = await client.login(email, password);
|
|
845
|
-
sessionToken = loginResult.sessionToken;
|
|
846
|
-
emailVerified = loginResult.emailVerified;
|
|
847
|
-
spinner.succeed(t('proAuthSuccess'));
|
|
848
|
-
} catch (loginError) {
|
|
849
|
-
if (loginError.code === 'INVALID_CREDENTIALS') {
|
|
850
|
-
spinner.info(t('proLoginFailedSignup'));
|
|
851
|
-
try {
|
|
852
|
-
await client.signup(email, password);
|
|
853
|
-
showSuccess(t('proAccountCreatedVerify'));
|
|
854
|
-
emailVerified = false;
|
|
855
|
-
const loginAfterSignup = await client.login(email, password);
|
|
856
|
-
sessionToken = loginAfterSignup.sessionToken;
|
|
857
|
-
} catch (signupError) {
|
|
858
|
-
if (signupError.code === 'EMAIL_ALREADY_REGISTERED') {
|
|
859
|
-
showError(t('proAccountExistsWrongPw'));
|
|
860
|
-
return { success: false, error: t('proAccountExistsWrongPw') };
|
|
861
|
-
}
|
|
862
|
-
return { success: false, error: signupError.message };
|
|
863
|
-
}
|
|
864
|
-
} else {
|
|
865
|
-
spinner.fail(tf('proAuthFailed', { message: loginError.message }));
|
|
866
|
-
return { success: false, error: loginError.message };
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
if (!sessionToken) {
|
|
871
|
-
return { success: false, error: t('proAuthFailedShort') };
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Wait for email verification if needed
|
|
875
|
-
if (!emailVerified) {
|
|
876
|
-
const verifyResult = await waitForEmailVerification(client, sessionToken, email);
|
|
877
|
-
if (!verifyResult.success) {
|
|
878
|
-
return verifyResult;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Activate Pro
|
|
883
|
-
return activateProByAuth(client, sessionToken);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Wait for email verification with polling.
|
|
888
|
-
*
|
|
889
|
-
* Polls the server every 5 seconds for up to 10 minutes.
|
|
890
|
-
* User can press R to resend verification email.
|
|
891
|
-
*
|
|
892
|
-
* @param {object} client - LicenseApiClient instance
|
|
893
|
-
* @param {string} sessionToken - Session token (accessToken)
|
|
894
|
-
* @param {string} email - User email for resend functionality
|
|
895
|
-
* @returns {Promise<Object>} Result with { success }
|
|
896
|
-
*/
|
|
897
|
-
async function waitForEmailVerification(client, sessionToken, email) {
|
|
898
|
-
console.log('');
|
|
899
|
-
showInfo(t('proWaitingVerification'));
|
|
900
|
-
showInfo(t('proCheckEmail'));
|
|
901
|
-
console.log(colors.dim(' ' + t('proCheckingEvery')));
|
|
902
|
-
|
|
903
|
-
if (!isCIEnvironment()) {
|
|
904
|
-
console.log(colors.dim(' ' + t('proPressResend')));
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const startTime = Date.now();
|
|
908
|
-
let resendHint = false;
|
|
909
|
-
|
|
910
|
-
// Set up keyboard listener for resend (non-CI only)
|
|
911
|
-
let keyListener;
|
|
912
|
-
if (!isCIEnvironment() && process.stdin.setRawMode) {
|
|
913
|
-
process.stdin.setRawMode(true);
|
|
914
|
-
process.stdin.resume();
|
|
915
|
-
keyListener = (key) => {
|
|
916
|
-
if (key.toString().toLowerCase() === 'r') {
|
|
917
|
-
resendHint = true;
|
|
918
|
-
}
|
|
919
|
-
// Ctrl+C
|
|
920
|
-
if (key.toString() === '\u0003') {
|
|
921
|
-
cleanupKeyListener();
|
|
922
|
-
process.exit(0);
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
process.stdin.on('data', keyListener);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function cleanupKeyListener() {
|
|
929
|
-
if (keyListener) {
|
|
930
|
-
process.stdin.removeListener('data', keyListener);
|
|
931
|
-
if (process.stdin.setRawMode) {
|
|
932
|
-
process.stdin.setRawMode(false);
|
|
933
|
-
}
|
|
934
|
-
process.stdin.pause();
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
try {
|
|
939
|
-
while (Date.now() - startTime < VERIFY_POLL_TIMEOUT_MS) {
|
|
940
|
-
// Handle resend request
|
|
941
|
-
if (resendHint) {
|
|
942
|
-
resendHint = false;
|
|
943
|
-
try {
|
|
944
|
-
await client.resendVerification(email);
|
|
945
|
-
showInfo(t('proVerificationResent'));
|
|
946
|
-
} catch (error) {
|
|
947
|
-
showWarning(tf('proCouldNotResend', { message: error.message }));
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Poll verification status
|
|
952
|
-
try {
|
|
953
|
-
const status = await client.checkEmailVerified(sessionToken);
|
|
954
|
-
if (status.verified) {
|
|
955
|
-
showSuccess(t('proEmailVerified'));
|
|
956
|
-
return { success: true };
|
|
957
|
-
}
|
|
958
|
-
} catch {
|
|
959
|
-
// Polling failure is non-fatal, continue
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Wait before next poll
|
|
963
|
-
await new Promise((resolve) => setTimeout(resolve, VERIFY_POLL_INTERVAL_MS));
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// Timeout
|
|
967
|
-
showError(t('proVerificationTimeout'));
|
|
968
|
-
showInfo(t('proRunAgainRetry'));
|
|
969
|
-
return { success: false, error: t('proVerificationTimeout') };
|
|
970
|
-
} finally {
|
|
971
|
-
cleanupKeyListener();
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Activate Pro using an authenticated session.
|
|
977
|
-
*
|
|
978
|
-
* @param {object} client - LicenseApiClient instance
|
|
979
|
-
* @param {string} sessionToken - Authenticated session token
|
|
980
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
981
|
-
*/
|
|
982
|
-
async function activateProByAuth(client, sessionToken) {
|
|
983
|
-
const spinner = createSpinner(t('proValidatingSubscription'));
|
|
984
|
-
spinner.start();
|
|
985
|
-
|
|
986
|
-
try {
|
|
987
|
-
// Generate machine fingerprint
|
|
988
|
-
const os = require('os');
|
|
989
|
-
const crypto = require('crypto');
|
|
990
|
-
const machineId = crypto
|
|
991
|
-
.createHash('sha256')
|
|
992
|
-
.update(`${os.hostname()}-${os.platform()}-${os.arch()}`)
|
|
993
|
-
.digest('hex')
|
|
994
|
-
.substring(0, 32);
|
|
995
|
-
|
|
996
|
-
// Read sinapse-ai version
|
|
997
|
-
let sinapseCoreVersion = 'unknown';
|
|
998
|
-
try {
|
|
999
|
-
const path = require('path');
|
|
1000
|
-
const fs = require('fs');
|
|
1001
|
-
const pkgPath = path.join(__dirname, '..', '..', '..', '..', 'package.json');
|
|
1002
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1003
|
-
sinapseCoreVersion = pkg.version || 'unknown';
|
|
1004
|
-
} catch {
|
|
1005
|
-
// Keep 'unknown'
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const activationResult = await client.activateByAuth(sessionToken, machineId, sinapseCoreVersion);
|
|
1009
|
-
|
|
1010
|
-
spinner.succeed(tf('proSubscriptionConfirmed', { key: maskLicenseKey(activationResult.key) }));
|
|
1011
|
-
return { success: true, key: activationResult.key, activationResult };
|
|
1012
|
-
} catch (error) {
|
|
1013
|
-
if (error.code === 'NOT_A_BUYER') {
|
|
1014
|
-
spinner.fail(t('proNoSubscription'));
|
|
1015
|
-
showInfo(t('proPurchaseAt'));
|
|
1016
|
-
return { success: false, error: error.message };
|
|
1017
|
-
}
|
|
1018
|
-
if (error.code === 'SEAT_LIMIT_EXCEEDED') {
|
|
1019
|
-
spinner.fail(error.message);
|
|
1020
|
-
showInfo(t('proSeatLimit'));
|
|
1021
|
-
return { success: false, error: error.message };
|
|
1022
|
-
}
|
|
1023
|
-
if (error.code === 'ALREADY_ACTIVATED') {
|
|
1024
|
-
// License already exists — treat as success (re-install scenario)
|
|
1025
|
-
spinner.succeed(t('proAlreadyActivated'));
|
|
1026
|
-
return { success: true, key: 'existing', activationResult: { reactivation: true } };
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
spinner.fail(tf('proActivationFailed', { message: error.message }));
|
|
1030
|
-
return { success: false, error: error.message };
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Interactive license key gate (legacy flow).
|
|
1036
|
-
*
|
|
1037
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
1038
|
-
*/
|
|
1039
|
-
async function stepLicenseGateWithKeyInteractive() {
|
|
1040
|
-
const inquirer = require('inquirer');
|
|
1041
|
-
|
|
1042
|
-
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
1043
|
-
const { licenseKey } = await inquirer.prompt([
|
|
1044
|
-
{
|
|
1045
|
-
type: 'password',
|
|
1046
|
-
name: 'licenseKey',
|
|
1047
|
-
message: colors.primary(t('proEnterKeyPrompt')),
|
|
1048
|
-
mask: '*',
|
|
1049
|
-
validate: (input) => {
|
|
1050
|
-
if (!input || !input.trim()) {
|
|
1051
|
-
return t('proKeyRequired');
|
|
1052
|
-
}
|
|
1053
|
-
if (!validateKeyFormat(input)) {
|
|
1054
|
-
return t('proKeyInvalid');
|
|
1055
|
-
}
|
|
1056
|
-
return true;
|
|
1057
|
-
},
|
|
1058
|
-
},
|
|
1059
|
-
]);
|
|
1060
|
-
|
|
1061
|
-
const key = licenseKey.trim().toUpperCase();
|
|
1062
|
-
const result = await validateKeyWithApi(key);
|
|
1063
|
-
|
|
1064
|
-
if (result.success) {
|
|
1065
|
-
showSuccess(tf('proKeyValidated', { key: maskLicenseKey(key) }));
|
|
1066
|
-
return { success: true, key, activationResult: result.data };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
const remaining = MAX_RETRIES - attempt;
|
|
1070
|
-
if (remaining > 0) {
|
|
1071
|
-
showError(`${result.error} (${remaining} attempt${remaining > 1 ? 's' : ''} remaining)`);
|
|
1072
|
-
} else {
|
|
1073
|
-
showError(`${result.error} — no attempts remaining.`);
|
|
1074
|
-
return { success: false, error: result.error };
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
return { success: false, error: 'Maximum attempts reached.' };
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Validate with pre-provided license key (CI or CLI arg).
|
|
1083
|
-
*
|
|
1084
|
-
* @param {string} key - License key
|
|
1085
|
-
* @returns {Promise<Object>} Result with { success, key, activationResult }
|
|
1086
|
-
*/
|
|
1087
|
-
async function stepLicenseGateWithKey(key) {
|
|
1088
|
-
if (!validateKeyFormat(key)) {
|
|
1089
|
-
return {
|
|
1090
|
-
success: false,
|
|
1091
|
-
error: tf('proInvalidKeyFormat', { key: maskLicenseKey(key) }),
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
const spinner = createSpinner(tf('proValidatingKey', { key: maskLicenseKey(key) }));
|
|
1096
|
-
spinner.start();
|
|
1097
|
-
|
|
1098
|
-
const result = await validateKeyWithApi(key);
|
|
1099
|
-
|
|
1100
|
-
if (result.success) {
|
|
1101
|
-
spinner.succeed(tf('proKeyValidated', { key: maskLicenseKey(key) }));
|
|
1102
|
-
return { success: true, key, activationResult: result.data };
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
spinner.fail(result.error);
|
|
1106
|
-
return { success: false, error: result.error };
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Validate a key against the license API.
|
|
1111
|
-
*
|
|
1112
|
-
* @param {string} key - License key
|
|
1113
|
-
* @returns {Promise<Object>} Result with { success, data?, error? }
|
|
1114
|
-
*/
|
|
1115
|
-
async function validateKeyWithApi(key) {
|
|
1116
|
-
const client = getLicenseClient();
|
|
1117
|
-
|
|
1118
|
-
try {
|
|
1119
|
-
// Check if API is reachable
|
|
1120
|
-
const online = await client.isOnline();
|
|
1121
|
-
|
|
1122
|
-
if (!online) {
|
|
1123
|
-
return {
|
|
1124
|
-
success: false,
|
|
1125
|
-
error: t('proServerUnreachable'),
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Generate a simple machine fingerprint
|
|
1130
|
-
const os = require('os');
|
|
1131
|
-
const crypto = require('crypto');
|
|
1132
|
-
const machineId = crypto
|
|
1133
|
-
.createHash('sha256')
|
|
1134
|
-
.update(`${os.hostname()}-${os.platform()}-${os.arch()}`)
|
|
1135
|
-
.digest('hex')
|
|
1136
|
-
.substring(0, 32);
|
|
1137
|
-
|
|
1138
|
-
// Read sinapse-ai version
|
|
1139
|
-
let sinapseCoreVersion = 'unknown';
|
|
1140
|
-
try {
|
|
1141
|
-
const path = require('path');
|
|
1142
|
-
const fs = require('fs');
|
|
1143
|
-
const pkgPath = path.join(__dirname, '..', '..', '..', '..', 'package.json');
|
|
1144
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1145
|
-
sinapseCoreVersion = pkg.version || 'unknown';
|
|
1146
|
-
} catch {
|
|
1147
|
-
// Keep 'unknown'
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const activationResult = await client.activate(key, machineId, sinapseCoreVersion);
|
|
1151
|
-
|
|
1152
|
-
return { success: true, data: activationResult };
|
|
1153
|
-
} catch (error) {
|
|
1154
|
-
// Handle specific error codes from license-api
|
|
1155
|
-
if (error.code === 'INVALID_KEY') {
|
|
1156
|
-
return { success: false, error: t('proInvalidKey') };
|
|
1157
|
-
}
|
|
1158
|
-
if (error.code === 'EXPIRED_KEY') {
|
|
1159
|
-
return { success: false, error: t('proExpiredKey') };
|
|
1160
|
-
}
|
|
1161
|
-
if (error.code === 'SEAT_LIMIT_EXCEEDED') {
|
|
1162
|
-
return { success: false, error: t('proMaxActivations') };
|
|
1163
|
-
}
|
|
1164
|
-
if (error.code === 'RATE_LIMITED') {
|
|
1165
|
-
return { success: false, error: t('proRateLimited') };
|
|
1166
|
-
}
|
|
1167
|
-
if (error.code === 'NETWORK_ERROR') {
|
|
1168
|
-
return {
|
|
1169
|
-
success: false,
|
|
1170
|
-
error: t('proServerUnreachable'),
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
return {
|
|
1175
|
-
success: false,
|
|
1176
|
-
error: tf('proValidationFailed', { message: error.message || 'Unknown error' }),
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
/**
|
|
1182
|
-
* Step 2: Install/Scaffold — copy pro content into the project.
|
|
1183
|
-
*
|
|
1184
|
-
* @param {string} targetDir - Project root directory
|
|
1185
|
-
* @param {Object} [options={}] - Options
|
|
1186
|
-
* @returns {Promise<Object>} Result with { success, scaffoldResult }
|
|
1187
|
-
*/
|
|
1188
|
-
async function stepInstallScaffold(targetDir, options = {}) {
|
|
1189
|
-
showStep(2, 3, t('proContentInstallation'));
|
|
1190
|
-
|
|
1191
|
-
const path = require('path');
|
|
1192
|
-
const fs = require('fs');
|
|
1193
|
-
|
|
1194
|
-
// Resolve pro source directory from multiple locations:
|
|
1195
|
-
// 1. Bundled in sinapse-ai package (pro/ submodule — npx and local dev)
|
|
1196
|
-
// 2. @sinapse-fullstack/pro in node_modules (legacy brownfield)
|
|
1197
|
-
const bundledProDir = path.resolve(__dirname, '..', '..', '..', '..', 'pro');
|
|
1198
|
-
const npmProDir = path.join(targetDir, 'node_modules', '@sinapse-fullstack', 'pro');
|
|
1199
|
-
|
|
1200
|
-
let proSourceDir;
|
|
1201
|
-
if (fs.existsSync(bundledProDir) && fs.existsSync(path.join(bundledProDir, 'squads'))) {
|
|
1202
|
-
proSourceDir = bundledProDir;
|
|
1203
|
-
} else if (fs.existsSync(npmProDir)) {
|
|
1204
|
-
proSourceDir = npmProDir;
|
|
1205
|
-
} else {
|
|
1206
|
-
return {
|
|
1207
|
-
success: false,
|
|
1208
|
-
error: t('proPackageNotFound'),
|
|
1209
|
-
};
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Step 2c: Scaffold pro content
|
|
1213
|
-
const scaffolderModule = loadProScaffolder();
|
|
1214
|
-
|
|
1215
|
-
if (!scaffolderModule) {
|
|
1216
|
-
showWarning(t('proScaffolderNotAvailable'));
|
|
1217
|
-
return { success: false, error: t('proScaffolderNotFound') };
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const { scaffoldProContent } = scaffolderModule;
|
|
1221
|
-
|
|
1222
|
-
const spinner = createSpinner(t('proScaffolding'));
|
|
1223
|
-
spinner.start();
|
|
1224
|
-
|
|
1225
|
-
try {
|
|
1226
|
-
const scaffoldResult = await scaffoldProContent(targetDir, proSourceDir, {
|
|
1227
|
-
onProgress: (progress) => {
|
|
1228
|
-
spinner.text = tf('proScaffoldingProgress', { message: progress.message });
|
|
1229
|
-
},
|
|
1230
|
-
force: options.force || false,
|
|
1231
|
-
});
|
|
1232
|
-
|
|
1233
|
-
if (scaffoldResult.success) {
|
|
1234
|
-
spinner.succeed(tf('proContentInstalled', { count: scaffoldResult.copiedFiles.length }));
|
|
1235
|
-
|
|
1236
|
-
if (scaffoldResult.warnings.length > 0) {
|
|
1237
|
-
for (const warning of scaffoldResult.warnings) {
|
|
1238
|
-
showWarning(warning);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
return { success: true, scaffoldResult };
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
spinner.fail(t('proScaffoldFailed'));
|
|
1246
|
-
for (const error of scaffoldResult.errors) {
|
|
1247
|
-
showError(error);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
return { success: false, error: scaffoldResult.errors.join('; '), scaffoldResult };
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
spinner.fail(tf('proScaffoldError', { message: error.message }));
|
|
1253
|
-
return { success: false, error: error.message };
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
/**
|
|
1258
|
-
* Step 3: Verify — check installed pro content and list features.
|
|
1259
|
-
*
|
|
1260
|
-
* @param {Object} [scaffoldResult] - Result from step 2
|
|
1261
|
-
* @returns {Promise<Object>} Verification result
|
|
1262
|
-
*/
|
|
1263
|
-
async function stepVerify(scaffoldResult) {
|
|
1264
|
-
showStep(3, 3, t('proVerification'));
|
|
1265
|
-
|
|
1266
|
-
const result = {
|
|
1267
|
-
success: true,
|
|
1268
|
-
features: [],
|
|
1269
|
-
squads: [],
|
|
1270
|
-
configs: [],
|
|
1271
|
-
};
|
|
1272
|
-
|
|
1273
|
-
// Show scaffolded content summary
|
|
1274
|
-
if (scaffoldResult && scaffoldResult.copiedFiles) {
|
|
1275
|
-
const files = scaffoldResult.copiedFiles;
|
|
1276
|
-
|
|
1277
|
-
// Categorize files
|
|
1278
|
-
result.squads = files.filter((f) => f.startsWith('squads/'));
|
|
1279
|
-
result.configs = files.filter(
|
|
1280
|
-
(f) => f.endsWith('.yaml') || f.endsWith('.json'),
|
|
1281
|
-
);
|
|
1282
|
-
|
|
1283
|
-
showInfo(tf('proFilesInstalled', { count: files.length }));
|
|
1284
|
-
|
|
1285
|
-
if (result.squads.length > 0) {
|
|
1286
|
-
// Extract unique squad names
|
|
1287
|
-
const squadNames = [...new Set(
|
|
1288
|
-
result.squads
|
|
1289
|
-
.map((f) => f.split('/')[1])
|
|
1290
|
-
.filter(Boolean),
|
|
1291
|
-
)];
|
|
1292
|
-
showSuccess(tf('proSquads', { names: squadNames.join(', ') }));
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
if (result.configs.length > 0) {
|
|
1296
|
-
showSuccess(tf('proConfigs', { count: result.configs.length }));
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// Check feature gate if available
|
|
1301
|
-
const featureModule = loadFeatureGate();
|
|
1302
|
-
|
|
1303
|
-
if (featureModule) {
|
|
1304
|
-
const { featureGate } = featureModule;
|
|
1305
|
-
featureGate.reload();
|
|
1306
|
-
|
|
1307
|
-
const available = featureGate.listAvailable();
|
|
1308
|
-
result.features = available;
|
|
1309
|
-
|
|
1310
|
-
if (available.length > 0) {
|
|
1311
|
-
showSuccess(tf('proFeaturesUnlocked', { count: available.length }));
|
|
1312
|
-
for (const feature of available.slice(0, 5)) {
|
|
1313
|
-
console.log(colors.dim(` ${feature}`));
|
|
1314
|
-
}
|
|
1315
|
-
if (available.length > 5) {
|
|
1316
|
-
console.log(colors.dim(` ... and ${available.length - 5} more`));
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// Final status
|
|
1322
|
-
console.log('');
|
|
1323
|
-
console.log(gold(' ════════════════════════════════════════════════'));
|
|
1324
|
-
console.log(status.celebrate(t('proInstallComplete')));
|
|
1325
|
-
console.log(gold(' ════════════════════════════════════════════════'));
|
|
1326
|
-
console.log('');
|
|
1327
|
-
|
|
1328
|
-
return result;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
/**
|
|
1332
|
-
* Run the full Pro Installation Wizard.
|
|
1333
|
-
*
|
|
1334
|
-
* Main entry point. Orchestrates the 3-step flow:
|
|
1335
|
-
* 1. License Gate (validate key)
|
|
1336
|
-
* 2. Install/Scaffold (copy pro content)
|
|
1337
|
-
* 3. Verify (list installed features)
|
|
1338
|
-
*
|
|
1339
|
-
* @param {Object} [options={}] - Wizard options
|
|
1340
|
-
* @param {string} [options.key] - Pre-provided license key
|
|
1341
|
-
* @param {string} [options.targetDir] - Project root (default: process.cwd())
|
|
1342
|
-
* @param {boolean} [options.force] - Force overwrite existing content
|
|
1343
|
-
* @param {boolean} [options.quiet] - Suppress non-essential output
|
|
1344
|
-
* @returns {Promise<Object>} Wizard result
|
|
1345
|
-
*/
|
|
1346
|
-
async function runProWizard(options = {}) {
|
|
1347
|
-
const targetDir = options.targetDir || process.cwd();
|
|
1348
|
-
const isCI = isCIEnvironment();
|
|
1349
|
-
|
|
1350
|
-
const result = {
|
|
1351
|
-
success: false,
|
|
1352
|
-
licenseValidated: false,
|
|
1353
|
-
scaffolded: false,
|
|
1354
|
-
verified: false,
|
|
1355
|
-
};
|
|
1356
|
-
|
|
1357
|
-
// Show branding (skip in CI or quiet mode)
|
|
1358
|
-
if (!isCI && !options.quiet) {
|
|
1359
|
-
showProHeader();
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
// Step 1: License Gate (uses InlineLicenseClient if @sinapse-fullstack/pro not yet installed)
|
|
1363
|
-
const licenseResult = await stepLicenseGate({
|
|
1364
|
-
key: options.key || process.env.SINAPSE_PRO_KEY,
|
|
1365
|
-
email: options.email || process.env.SINAPSE_PRO_EMAIL,
|
|
1366
|
-
password: options.password || process.env.SINAPSE_PRO_PASSWORD,
|
|
1367
|
-
});
|
|
1368
|
-
|
|
1369
|
-
if (!licenseResult.success) {
|
|
1370
|
-
showError(licenseResult.error);
|
|
1371
|
-
|
|
1372
|
-
if (!isCI) {
|
|
1373
|
-
showInfo(t('proNeedHelp'));
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
result.error = licenseResult.error;
|
|
1377
|
-
return result;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
result.licenseValidated = true;
|
|
1381
|
-
|
|
1382
|
-
// Step 2: Install/Scaffold
|
|
1383
|
-
const scaffoldResult = await stepInstallScaffold(targetDir, {
|
|
1384
|
-
force: options.force,
|
|
1385
|
-
});
|
|
1386
|
-
|
|
1387
|
-
if (!scaffoldResult.success) {
|
|
1388
|
-
result.error = scaffoldResult.error;
|
|
1389
|
-
return result;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
result.scaffolded = true;
|
|
1393
|
-
|
|
1394
|
-
// Step 3: Verify
|
|
1395
|
-
const verifyResult = await stepVerify(scaffoldResult.scaffoldResult);
|
|
1396
|
-
result.verified = verifyResult.success;
|
|
1397
|
-
result.features = verifyResult.features;
|
|
1398
|
-
result.squads = verifyResult.squads;
|
|
1399
|
-
result.success = true;
|
|
1400
|
-
|
|
1401
|
-
return result;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
module.exports = {
|
|
1405
|
-
runProWizard,
|
|
1406
|
-
stepLicenseGate,
|
|
1407
|
-
stepInstallScaffold,
|
|
1408
|
-
stepVerify,
|
|
1409
|
-
maskLicenseKey,
|
|
1410
|
-
validateKeyFormat,
|
|
1411
|
-
isCIEnvironment,
|
|
1412
|
-
showProHeader,
|
|
1413
|
-
// Internal helpers exported for testing
|
|
1414
|
-
_testing: {
|
|
1415
|
-
validateKeyWithApi,
|
|
1416
|
-
authenticateWithEmail,
|
|
1417
|
-
waitForEmailVerification,
|
|
1418
|
-
activateProByAuth,
|
|
1419
|
-
loginWithRetry,
|
|
1420
|
-
createAccountFlow,
|
|
1421
|
-
stepLicenseGateCI,
|
|
1422
|
-
stepLicenseGateWithKey,
|
|
1423
|
-
stepLicenseGateWithKeyInteractive,
|
|
1424
|
-
stepLicenseGateWithEmail,
|
|
1425
|
-
loadProModule,
|
|
1426
|
-
loadLicenseApi,
|
|
1427
|
-
loadFeatureGate,
|
|
1428
|
-
loadProScaffolder,
|
|
1429
|
-
getLicenseClient,
|
|
1430
|
-
InlineLicenseClient,
|
|
1431
|
-
LICENSE_SERVER_URL,
|
|
1432
|
-
MAX_RETRIES,
|
|
1433
|
-
LICENSE_KEY_PATTERN,
|
|
1434
|
-
EMAIL_PATTERN,
|
|
1435
|
-
MIN_PASSWORD_LENGTH,
|
|
1436
|
-
VERIFY_POLL_INTERVAL_MS,
|
|
1437
|
-
VERIFY_POLL_TIMEOUT_MS,
|
|
1438
|
-
},
|
|
1439
|
-
};
|