aiox-core 5.0.0 → 5.0.2

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.
Files changed (91) hide show
  1. package/.aiox-core/data/entity-registry.yaml +5297 -1814
  2. package/.aiox-core/data/registry-update-log.jsonl +2 -0
  3. package/.aiox-core/development/templates/service-template/README.md.hbs +158 -158
  4. package/.aiox-core/development/templates/service-template/__tests__/index.test.ts.hbs +237 -237
  5. package/.aiox-core/development/templates/service-template/client.ts.hbs +403 -403
  6. package/.aiox-core/development/templates/service-template/errors.ts.hbs +182 -182
  7. package/.aiox-core/development/templates/service-template/index.ts.hbs +120 -120
  8. package/.aiox-core/development/templates/service-template/package.json.hbs +87 -87
  9. package/.aiox-core/development/templates/service-template/types.ts.hbs +145 -145
  10. package/.aiox-core/development/templates/squad-template/LICENSE +21 -21
  11. package/.aiox-core/infrastructure/scripts/tool-resolver.js +4 -4
  12. package/.aiox-core/infrastructure/templates/aiox-sync.yaml.template +182 -182
  13. package/.aiox-core/infrastructure/templates/coderabbit.yaml.template +279 -279
  14. package/.aiox-core/infrastructure/templates/github-workflows/ci.yml.template +169 -169
  15. package/.aiox-core/infrastructure/templates/github-workflows/pr-automation.yml.template +330 -330
  16. package/.aiox-core/infrastructure/templates/github-workflows/release.yml.template +196 -196
  17. package/.aiox-core/infrastructure/templates/gitignore/gitignore-aiox-base.tmpl +63 -63
  18. package/.aiox-core/infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl +18 -18
  19. package/.aiox-core/infrastructure/templates/gitignore/gitignore-node.tmpl +85 -85
  20. package/.aiox-core/infrastructure/templates/gitignore/gitignore-python.tmpl +145 -145
  21. package/.aiox-core/install-manifest.yaml +58 -58
  22. package/.aiox-core/local-config.yaml.template +71 -71
  23. package/.aiox-core/monitor/hooks/lib/__init__.py +1 -1
  24. package/.aiox-core/monitor/hooks/lib/enrich.py +58 -58
  25. package/.aiox-core/monitor/hooks/lib/send_event.py +47 -47
  26. package/.aiox-core/monitor/hooks/notification.py +29 -29
  27. package/.aiox-core/monitor/hooks/post_tool_use.py +45 -45
  28. package/.aiox-core/monitor/hooks/pre_compact.py +29 -29
  29. package/.aiox-core/monitor/hooks/pre_tool_use.py +40 -40
  30. package/.aiox-core/monitor/hooks/stop.py +29 -29
  31. package/.aiox-core/monitor/hooks/subagent_stop.py +29 -29
  32. package/.aiox-core/monitor/hooks/user_prompt_submit.py +38 -38
  33. package/.aiox-core/product/templates/adr.hbs +125 -125
  34. package/.aiox-core/product/templates/dbdr.hbs +241 -241
  35. package/.aiox-core/product/templates/engine/elicitation.js +2 -3
  36. package/.aiox-core/product/templates/epic.hbs +212 -212
  37. package/.aiox-core/product/templates/pmdr.hbs +186 -186
  38. package/.aiox-core/product/templates/prd-v2.0.hbs +216 -216
  39. package/.aiox-core/product/templates/prd.hbs +201 -201
  40. package/.aiox-core/product/templates/story.hbs +263 -263
  41. package/.aiox-core/product/templates/task.hbs +170 -170
  42. package/.aiox-core/product/templates/tmpl-comment-on-examples.sql +158 -158
  43. package/.aiox-core/product/templates/tmpl-migration-script.sql +91 -91
  44. package/.aiox-core/product/templates/tmpl-rls-granular-policies.sql +104 -104
  45. package/.aiox-core/product/templates/tmpl-rls-kiss-policy.sql +10 -10
  46. package/.aiox-core/product/templates/tmpl-rls-roles.sql +135 -135
  47. package/.aiox-core/product/templates/tmpl-rls-simple.sql +77 -77
  48. package/.aiox-core/product/templates/tmpl-rls-tenant.sql +152 -152
  49. package/.aiox-core/product/templates/tmpl-rollback-script.sql +77 -77
  50. package/.aiox-core/product/templates/tmpl-seed-data.sql +140 -140
  51. package/.aiox-core/product/templates/tmpl-smoke-test.sql +16 -16
  52. package/.aiox-core/product/templates/tmpl-staging-copy-merge.sql +139 -139
  53. package/.aiox-core/product/templates/tmpl-stored-proc.sql +140 -140
  54. package/.aiox-core/product/templates/tmpl-trigger.sql +152 -152
  55. package/.aiox-core/product/templates/tmpl-view-materialized.sql +133 -133
  56. package/.aiox-core/product/templates/tmpl-view.sql +177 -177
  57. package/.aiox-core/scripts/pm.sh +0 -0
  58. package/.claude/hooks/code-intel-pretool.cjs +107 -0
  59. package/.claude/hooks/enforce-architecture-first.py +196 -196
  60. package/.claude/hooks/mind-clone-governance.py +192 -192
  61. package/.claude/hooks/read-protection.py +151 -151
  62. package/.claude/hooks/slug-validation.py +176 -176
  63. package/.claude/hooks/sql-governance.py +182 -182
  64. package/.claude/hooks/write-path-validation.py +194 -194
  65. package/LICENSE +33 -33
  66. package/bin/aiox-graph.js +0 -0
  67. package/bin/aiox-minimal.js +0 -0
  68. package/bin/aiox.js +0 -0
  69. package/docs/guides/aios-workflows/README.md +247 -0
  70. package/docs/guides/aios-workflows/bob-orchestrator-workflow.md +1536 -0
  71. package/package.json +1 -1
  72. package/packages/aiox-install/bin/aiox-install.js +0 -0
  73. package/packages/aiox-install/bin/edmcp.js +0 -0
  74. package/packages/aiox-pro-cli/bin/aiox-pro.js +0 -0
  75. package/packages/installer/src/wizard/pro-setup.js +210 -123
  76. package/pro/README.md +66 -0
  77. package/pro/license/degradation.js +220 -0
  78. package/pro/license/errors.js +450 -0
  79. package/pro/license/feature-gate.js +354 -0
  80. package/pro/license/index.js +181 -0
  81. package/pro/license/license-api.js +679 -0
  82. package/pro/license/license-cache.js +523 -0
  83. package/pro/license/license-crypto.js +303 -0
  84. package/scripts/check-markdown-links.py +352 -352
  85. package/scripts/dashboard-parallel-dev.sh +0 -0
  86. package/scripts/dashboard-parallel-phase3.sh +0 -0
  87. package/scripts/dashboard-parallel-phase4.sh +0 -0
  88. package/scripts/glue/README.md +355 -0
  89. package/scripts/glue/compose-agent-prompt.cjs +362 -0
  90. package/scripts/install-monitor-hooks.sh +0 -0
  91. package/.aiox-core/lib/build.json +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiox-core",
3
- "version": "5.0.0",
3
+ "version": "5.0.2",
4
4
  "description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
5
5
  "bin": {
6
6
  "aiox": "bin/aiox.js",
File without changes
File without changes
File without changes
@@ -36,6 +36,173 @@ try {
36
36
  */
37
37
  const LICENSE_SERVER_URL = process.env.AIOX_LICENSE_API_URL || 'https://aiox-license-server.vercel.app';
38
38
 
39
+ /**
40
+ * Inline License Client — lightweight HTTP client for pre-bootstrap license checks.
41
+ *
42
+ * Used when @aiox-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': 'aiox-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 - aiox-core 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 - aiox-core 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
+
39
206
  /**
40
207
  * License key format: PRO-XXXX-XXXX-XXXX-XXXX
41
208
  */
@@ -211,6 +378,27 @@ function loadFeatureGate() {
211
378
  return loadProModule('feature-gate');
212
379
  }
213
380
 
381
+ /**
382
+ * Get a license API client instance.
383
+ *
384
+ * Prefers the full LicenseApiClient from @aiox-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 @aiox-fullstack/pro yet)
399
+ return new InlineLicenseClient();
400
+ }
401
+
214
402
  /**
215
403
  * Try to load the pro scaffolder via lazy import.
216
404
  *
@@ -351,18 +539,7 @@ async function stepLicenseGateWithEmail() {
351
539
  const trimmedEmail = email.trim();
352
540
 
353
541
  // Step 2: Check buyer status + account existence
354
- const loader = module.exports._testing ? module.exports._testing.loadLicenseApi : loadLicenseApi;
355
- const licenseModule = loader();
356
-
357
- if (!licenseModule) {
358
- return {
359
- success: false,
360
- error: t('proModuleNotAvailable'),
361
- };
362
- }
363
-
364
- const { LicenseApiClient } = licenseModule;
365
- const client = new LicenseApiClient();
542
+ const client = getLicenseClient();
366
543
 
367
544
  // Check connectivity
368
545
  const online = await client.isOnline();
@@ -630,18 +807,7 @@ async function createAccountFlow(client, email) {
630
807
  * @returns {Promise<Object>} Result with { success, key, activationResult }
631
808
  */
632
809
  async function authenticateWithEmail(email, password) {
633
- const loader = module.exports._testing ? module.exports._testing.loadLicenseApi : loadLicenseApi;
634
- const licenseModule = loader();
635
-
636
- if (!licenseModule) {
637
- return {
638
- success: false,
639
- error: t('proModuleNotAvailable'),
640
- };
641
- }
642
-
643
- const { LicenseApiClient } = licenseModule;
644
- const client = new LicenseApiClient();
810
+ const client = getLicenseClient();
645
811
 
646
812
  // Check connectivity
647
813
  const online = await client.isOnline();
@@ -947,19 +1113,7 @@ async function stepLicenseGateWithKey(key) {
947
1113
  * @returns {Promise<Object>} Result with { success, data?, error? }
948
1114
  */
949
1115
  async function validateKeyWithApi(key) {
950
- // Use exports._testing for testability (allows mock injection)
951
- const loader = module.exports._testing ? module.exports._testing.loadLicenseApi : loadLicenseApi;
952
- const licenseModule = loader();
953
-
954
- if (!licenseModule) {
955
- return {
956
- success: false,
957
- error: t('proModuleNotAvailable'),
958
- };
959
- }
960
-
961
- const { LicenseApiClient } = licenseModule;
962
- const client = new LicenseApiClient();
1116
+ const client = getLicenseClient();
963
1117
 
964
1118
  try {
965
1119
  // Check if API is reachable
@@ -1036,50 +1190,23 @@ async function stepInstallScaffold(targetDir, options = {}) {
1036
1190
 
1037
1191
  const path = require('path');
1038
1192
  const fs = require('fs');
1039
- const { execSync } = require('child_process');
1040
-
1041
- const proSourceDir = path.join(targetDir, 'node_modules', '@aiox-fullstack', 'pro');
1042
-
1043
- // Step 2a: Ensure package.json exists (greenfield projects)
1044
- const packageJsonPath = path.join(targetDir, 'package.json');
1045
- if (!fs.existsSync(packageJsonPath)) {
1046
- const initSpinner = createSpinner(t('proInitPackageJson'));
1047
- initSpinner.start();
1048
- try {
1049
- execSync('npm init -y', { cwd: targetDir, stdio: 'pipe' });
1050
- initSpinner.succeed(t('proPackageJsonCreated'));
1051
- } catch (err) {
1052
- initSpinner.fail(t('proPackageJsonFailed'));
1053
- return { success: false, error: tf('proNpmInitFailed', { message: err.message }) };
1054
- }
1055
- }
1056
1193
 
1057
- // Step 2b: Install @aiox-fullstack/pro if not present
1058
- if (!fs.existsSync(proSourceDir)) {
1059
- const installSpinner = createSpinner(t('proInstallingPackage'));
1060
- installSpinner.start();
1061
- try {
1062
- execSync('npm install @aiox-fullstack/pro', {
1063
- cwd: targetDir,
1064
- stdio: 'pipe',
1065
- timeout: 120000,
1066
- });
1067
- installSpinner.succeed(t('proPackageInstalled'));
1068
- } catch (err) {
1069
- installSpinner.fail(t('proPackageInstallFailed'));
1070
- return {
1071
- success: false,
1072
- error: tf('proNpmInstallFailed', { message: err.message }),
1073
- };
1074
- }
1075
-
1076
- // Validate installation
1077
- if (!fs.existsSync(proSourceDir)) {
1078
- return {
1079
- success: false,
1080
- error: t('proPackageNotFound'),
1081
- };
1082
- }
1194
+ // Resolve pro source directory from multiple locations:
1195
+ // 1. Bundled in aiox-core package (pro/ submodule — npx and local dev)
1196
+ // 2. @aiox-fullstack/pro in node_modules (legacy brownfield)
1197
+ const bundledProDir = path.resolve(__dirname, '..', '..', '..', '..', 'pro');
1198
+ const npmProDir = path.join(targetDir, 'node_modules', '@aiox-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
+ };
1083
1210
  }
1084
1211
 
1085
1212
  // Step 2c: Scaffold pro content
@@ -1232,49 +1359,7 @@ async function runProWizard(options = {}) {
1232
1359
  showProHeader();
1233
1360
  }
1234
1361
 
1235
- // Pre-check: If license module is not available (brownfield upgrade from older version),
1236
- // install @aiox-fullstack/pro first to get the license API, then proceed with gate.
1237
- if (!loadLicenseApi()) {
1238
- const fs = require('fs');
1239
- const path = require('path');
1240
- const { execSync } = require('child_process');
1241
-
1242
- showInfo(t('proModuleBootstrap'));
1243
-
1244
- // Ensure package.json exists
1245
- const packageJsonPath = path.join(targetDir, 'package.json');
1246
- if (!fs.existsSync(packageJsonPath)) {
1247
- execSync('npm init -y', { cwd: targetDir, stdio: 'pipe' });
1248
- }
1249
-
1250
- // Install @aiox-fullstack/pro to get license module
1251
- const proDir = path.join(targetDir, 'node_modules', '@aiox-fullstack', 'pro');
1252
- if (!fs.existsSync(proDir)) {
1253
- const installSpinner = createSpinner(t('proInstallingPackage'));
1254
- installSpinner.start();
1255
- try {
1256
- execSync('npm install @aiox-fullstack/pro', {
1257
- cwd: targetDir,
1258
- stdio: 'pipe',
1259
- timeout: 120000,
1260
- });
1261
- installSpinner.succeed(t('proPackageInstalled'));
1262
- } catch (err) {
1263
- installSpinner.fail(t('proPackageInstallFailed'));
1264
- result.error = tf('proNpmInstallFailed', { message: err.message });
1265
- return result;
1266
- }
1267
- }
1268
-
1269
- // Clear require cache so loadLicenseApi() picks up newly installed module
1270
- Object.keys(require.cache).forEach((key) => {
1271
- if (key.includes('license-api') || key.includes('@aiox-fullstack')) {
1272
- delete require.cache[key];
1273
- }
1274
- });
1275
- }
1276
-
1277
- // Step 1: License Gate
1362
+ // Step 1: License Gate (uses InlineLicenseClient if @aiox-fullstack/pro not yet installed)
1278
1363
  const licenseResult = await stepLicenseGate({
1279
1364
  key: options.key || process.env.AIOX_PRO_KEY,
1280
1365
  email: options.email || process.env.AIOX_PRO_EMAIL,
@@ -1341,6 +1426,8 @@ module.exports = {
1341
1426
  loadLicenseApi,
1342
1427
  loadFeatureGate,
1343
1428
  loadProScaffolder,
1429
+ getLicenseClient,
1430
+ InlineLicenseClient,
1344
1431
  LICENSE_SERVER_URL,
1345
1432
  MAX_RETRIES,
1346
1433
  LICENSE_KEY_PATTERN,
package/pro/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # AIOX Pro
2
+
3
+ Premium features for [Synkra AIOX](https://github.com/SynkraAI/aiox-core).
4
+
5
+ ## Overview
6
+
7
+ AIOX Pro extends the open-source AIOX framework with premium capabilities:
8
+
9
+ - **Premium Squads** - Pre-built squads for SaaS, e-commerce, and fintech
10
+ - **Persistent Memory** - Cross-session context and memory analytics
11
+ - **Usage Metrics** - Dashboards, team analytics, and cost tracking
12
+ - **Enterprise Integrations** - ClickUp, Google Drive, GitLab, Jira, Azure DevOps
13
+
14
+ ## Installation
15
+
16
+ AIOX Pro requires an active license and is distributed via npm.
17
+
18
+ ```bash
19
+ # Install
20
+ npm install @aiox-fullstack/pro
21
+
22
+ # Activate license
23
+ aiox pro activate --key PRO-XXXX-XXXX-XXXX-XXXX
24
+ ```
25
+
26
+ ## Usage with aiox-core
27
+
28
+ AIOX Pro is designed to work as a git submodule of `aiox-core`:
29
+
30
+ ```bash
31
+ # Clone aiox-core with pro submodule
32
+ git clone --recurse-submodules https://github.com/SynkraAI/aiox-core.git
33
+
34
+ # Or add to existing clone
35
+ cd aiox-core
36
+ git submodule update --init pro
37
+ ```
38
+
39
+ ## Activate License
40
+
41
+ ```bash
42
+ aiox pro activate --key PRO-XXXX-XXXX-XXXX-XXXX
43
+ aiox pro status
44
+ ```
45
+
46
+ ## Structure
47
+
48
+ ```
49
+ aiox-pro/
50
+ ├── squads/ # Premium squad definitions
51
+ ├── memory/ # Persistent memory modules
52
+ ├── metrics/ # Usage dashboards and analytics
53
+ ├── integrations/ # Enterprise integration adapters
54
+ ├── license/ # License infrastructure
55
+ └── pro-config.yaml # Pro extension configuration
56
+ ```
57
+
58
+ ## Requirements
59
+
60
+ - Node.js >= 18
61
+ - aiox-core >= 3.12.0
62
+ - Active AIOX Pro license
63
+
64
+ ## License
65
+
66
+ Proprietary - Copyright (c) 2026 SynkraAI. All rights reserved.
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Graceful Degradation Module
3
+ *
4
+ * Utilities for handling pro feature unavailability with user-friendly
5
+ * messages that preserve data and provide clear next steps.
6
+ *
7
+ * Per ADR-PRO-003 and AC-8:
8
+ * - Never show upgrade prompts on core features
9
+ * - Never delete or corrupt user data
10
+ * - Always provide actionable next steps
11
+ * - Keep messages non-intrusive
12
+ *
13
+ * @module pro/license/degradation
14
+ * @see ADR-PRO-003 - Feature Gating & Licensing
15
+ * @see Story PRO-6 - License Key & Feature Gating System
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { featureGate } = require('./feature-gate');
21
+ const { ProFeatureError } = require('./errors');
22
+
23
+ /**
24
+ * Default degradation message template.
25
+ * @private Reserved for future customization support.
26
+ */
27
+ const _DEFAULT_MESSAGE_TEMPLATE = `
28
+ {featureName} requires an active AIOS Pro license.
29
+
30
+ Your data and configurations are preserved.
31
+
32
+ Activate: aios pro activate --key <KEY>
33
+ Purchase: https://synkra.ai/pro
34
+ `;
35
+
36
+ /**
37
+ * Handle pro feature gracefully - return fallback instead of throwing.
38
+ *
39
+ * Use this when you want to provide a degraded experience instead
40
+ * of completely blocking the feature.
41
+ *
42
+ * @param {string} featureId - Feature ID to check
43
+ * @param {Function} proAction - Function to execute if feature is available
44
+ * @param {Function} [fallbackAction] - Function to execute if not available
45
+ * @param {object} [options] - Options
46
+ * @param {boolean} [options.silent=false] - Don't log degradation message
47
+ * @returns {*} Result of proAction or fallbackAction
48
+ */
49
+ function withGracefulDegradation(featureId, proAction, fallbackAction, options = {}) {
50
+ if (featureGate.isAvailable(featureId)) {
51
+ return proAction();
52
+ }
53
+
54
+ if (!options.silent) {
55
+ const featureName = getFeatureFriendlyName(featureId);
56
+ logDegradationMessage(featureName, featureId);
57
+ }
58
+
59
+ if (typeof fallbackAction === 'function') {
60
+ return fallbackAction();
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Execute action only if pro feature is available.
68
+ *
69
+ * Unlike featureGate.require(), this doesn't throw - it just
70
+ * returns undefined if the feature isn't available.
71
+ *
72
+ * @param {string} featureId - Feature ID to check
73
+ * @param {Function} action - Action to execute if available
74
+ * @returns {*|undefined} Result of action or undefined
75
+ */
76
+ function ifProAvailable(featureId, action) {
77
+ if (featureGate.isAvailable(featureId)) {
78
+ return action();
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ /**
84
+ * Get a user-friendly name for a feature.
85
+ *
86
+ * @param {string} featureId - Feature ID
87
+ * @returns {string} Friendly name
88
+ */
89
+ function getFeatureFriendlyName(featureId) {
90
+ const all = featureGate.listAll();
91
+ const feature = all.find((f) => f.id === featureId);
92
+ return feature ? feature.name : featureId;
93
+ }
94
+
95
+ /**
96
+ * Log a degradation message to the console.
97
+ *
98
+ * This is used internally when a pro feature is accessed without
99
+ * a license. Messages are non-intrusive and informative.
100
+ *
101
+ * @param {string} featureName - Human-friendly feature name
102
+ * @param {string} _featureId - Feature ID for reference (reserved for logging)
103
+ */
104
+ function logDegradationMessage(featureName, _featureId) {
105
+ console.log('');
106
+ console.log(` ${featureName} requires an active AIOS Pro license.`);
107
+ console.log('');
108
+ console.log(' Your data and configurations are preserved.');
109
+ console.log('');
110
+ console.log(' Activate: aios pro activate --key <KEY>');
111
+ console.log(' Purchase: https://synkra.ai/pro');
112
+ console.log('');
113
+ }
114
+
115
+ /**
116
+ * Create a degradation-aware wrapper for a pro module.
117
+ *
118
+ * This creates a proxy that catches ProFeatureError and provides
119
+ * a graceful degradation experience.
120
+ *
121
+ * @param {object} proModule - Pro module with methods
122
+ * @param {object} fallbacks - Map of method names to fallback functions
123
+ * @returns {Proxy} Wrapped module
124
+ *
125
+ * @example
126
+ * const safePremiumSquads = createDegradationWrapper(
127
+ * PremiumSquads,
128
+ * {
129
+ * listTemplates: () => ['basic'], // Fallback to basic templates
130
+ * exportSquad: () => null, // No export without license
131
+ * }
132
+ * );
133
+ */
134
+ function createDegradationWrapper(proModule, fallbacks = {}) {
135
+ return new Proxy(proModule, {
136
+ get(target, prop) {
137
+ const original = target[prop];
138
+
139
+ if (typeof original !== 'function') {
140
+ return original;
141
+ }
142
+
143
+ return function (...args) {
144
+ try {
145
+ return original.apply(target, args);
146
+ } catch (error) {
147
+ if (error instanceof ProFeatureError) {
148
+ if (fallbacks[prop]) {
149
+ logDegradationMessage(error.friendlyName, error.featureId);
150
+ return fallbacks[prop](...args);
151
+ }
152
+ // Re-throw if no fallback
153
+ throw error;
154
+ }
155
+ // Re-throw non-license errors
156
+ throw error;
157
+ }
158
+ };
159
+ },
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Check if the system is in degraded mode (expired/no license).
165
+ *
166
+ * @returns {boolean} true if pro features are unavailable
167
+ */
168
+ function isInDegradedMode() {
169
+ const state = featureGate.getLicenseState();
170
+ return state === 'Expired' || state === 'Not Activated';
171
+ }
172
+
173
+ /**
174
+ * Get degradation status summary.
175
+ *
176
+ * @returns {{ degraded: boolean, reason: string, action: string }}
177
+ */
178
+ function getDegradationStatus() {
179
+ const state = featureGate.getLicenseState();
180
+
181
+ if (state === 'Active') {
182
+ return {
183
+ degraded: false,
184
+ reason: 'License is active',
185
+ action: null,
186
+ };
187
+ }
188
+
189
+ if (state === 'Grace') {
190
+ return {
191
+ degraded: false,
192
+ reason: 'License in grace period - revalidate soon',
193
+ action: 'aios pro validate',
194
+ };
195
+ }
196
+
197
+ if (state === 'Expired') {
198
+ return {
199
+ degraded: true,
200
+ reason: 'License has expired',
201
+ action: 'aios pro activate --key <KEY>',
202
+ };
203
+ }
204
+
205
+ return {
206
+ degraded: true,
207
+ reason: 'No license activated',
208
+ action: 'aios pro activate --key <KEY>',
209
+ };
210
+ }
211
+
212
+ module.exports = {
213
+ withGracefulDegradation,
214
+ ifProAvailable,
215
+ getFeatureFriendlyName,
216
+ logDegradationMessage,
217
+ createDegradationWrapper,
218
+ isInDegradedMode,
219
+ getDegradationStatus,
220
+ };