@aifabrix/builder 2.1.7 → 2.2.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/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +57 -37
- package/lib/cli.js +90 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/config.js +257 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/secrets.js +17 -0
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +144 -0
- package/lib/utils/cli-utils.js +21 -0
- package/lib/utils/compose-generator.js +43 -14
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/token-manager.js +381 -0
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Token Management Utilities
|
|
3
|
+
*
|
|
4
|
+
* Centralized token management for device and client credentials tokens
|
|
5
|
+
* Handles token retrieval, expiration checking, and refresh logic
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Token management utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const yaml = require('js-yaml');
|
|
16
|
+
const config = require('../config');
|
|
17
|
+
const { makeApiCall, refreshDeviceToken: apiRefreshDeviceToken } = require('./api');
|
|
18
|
+
const logger = require('./logger');
|
|
19
|
+
|
|
20
|
+
const SECRETS_FILE = path.join(os.homedir(), '.aifabrix', 'secrets.local.yaml');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load client credentials from secrets.local.yaml
|
|
24
|
+
* Reads using pattern: <app-name>-client-idKeyVault and <app-name>-client-secretKeyVault
|
|
25
|
+
* @param {string} appName - Application name
|
|
26
|
+
* @returns {Promise<{clientId: string, clientSecret: string}|null>} Credentials or null if not found
|
|
27
|
+
*/
|
|
28
|
+
async function loadClientCredentials(appName) {
|
|
29
|
+
if (!appName || typeof appName !== 'string') {
|
|
30
|
+
throw new Error('App name is required and must be a string');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(SECRETS_FILE)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const content = fs.readFileSync(SECRETS_FILE, 'utf8');
|
|
39
|
+
const secrets = yaml.load(content) || {};
|
|
40
|
+
|
|
41
|
+
const clientIdKey = `${appName}-client-idKeyVault`;
|
|
42
|
+
const clientSecretKey = `${appName}-client-secretKeyVault`;
|
|
43
|
+
|
|
44
|
+
const clientId = secrets[clientIdKey];
|
|
45
|
+
const clientSecret = secrets[clientSecretKey];
|
|
46
|
+
|
|
47
|
+
if (!clientId || !clientSecret) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
clientId: clientId,
|
|
53
|
+
clientSecret: clientSecret
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.warn(`Failed to load credentials from secrets.local.yaml: ${error.message}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get device token for controller
|
|
63
|
+
* @param {string} controllerUrl - Controller URL
|
|
64
|
+
* @returns {Promise<{controller: string, token: string, refreshToken: string, expiresAt: string}|null>} Device token info or null
|
|
65
|
+
*/
|
|
66
|
+
async function getDeviceToken(controllerUrl) {
|
|
67
|
+
return await config.getDeviceToken(controllerUrl);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get client token for environment and app
|
|
72
|
+
* @param {string} environment - Environment key
|
|
73
|
+
* @param {string} appName - Application name
|
|
74
|
+
* @returns {Promise<{controller: string, token: string, expiresAt: string}|null>} Client token info or null
|
|
75
|
+
*/
|
|
76
|
+
async function getClientToken(environment, appName) {
|
|
77
|
+
return await config.getClientToken(environment, appName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if token is expired
|
|
82
|
+
* @param {string} expiresAt - ISO timestamp string
|
|
83
|
+
* @returns {boolean} True if token is expired
|
|
84
|
+
*/
|
|
85
|
+
function isTokenExpired(expiresAt) {
|
|
86
|
+
return config.isTokenExpired(expiresAt);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Refresh client token using credentials from secrets.local.yaml
|
|
91
|
+
* Gets new token from API and saves it to config.yaml
|
|
92
|
+
* @param {string} environment - Environment key
|
|
93
|
+
* @param {string} appName - Application name
|
|
94
|
+
* @param {string} controllerUrl - Controller URL
|
|
95
|
+
* @param {string} [clientId] - Optional client ID (if not provided, loads from secrets.local.yaml)
|
|
96
|
+
* @param {string} [clientSecret] - Optional client secret (if not provided, loads from secrets.local.yaml)
|
|
97
|
+
* @returns {Promise<{token: string, expiresAt: string}>} New token and expiration
|
|
98
|
+
* @throws {Error} If credentials are missing or token refresh fails
|
|
99
|
+
*/
|
|
100
|
+
async function refreshClientToken(environment, appName, controllerUrl, clientId, clientSecret) {
|
|
101
|
+
if (!environment || typeof environment !== 'string') {
|
|
102
|
+
throw new Error('Environment is required and must be a string');
|
|
103
|
+
}
|
|
104
|
+
if (!appName || typeof appName !== 'string') {
|
|
105
|
+
throw new Error('App name is required and must be a string');
|
|
106
|
+
}
|
|
107
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
108
|
+
throw new Error('Controller URL is required and must be a string');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Load credentials if not provided
|
|
112
|
+
let credentials = null;
|
|
113
|
+
if (clientId && clientSecret) {
|
|
114
|
+
credentials = { clientId, clientSecret };
|
|
115
|
+
} else {
|
|
116
|
+
credentials = await loadClientCredentials(appName);
|
|
117
|
+
if (!credentials) {
|
|
118
|
+
throw new Error(`Client credentials not found for app '${appName}'. Add them to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Call login API to get new token
|
|
123
|
+
const response = await makeApiCall(`${controllerUrl}/api/v1/auth/token`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
'x-client-id': credentials.clientId,
|
|
128
|
+
'x-client-secret': credentials.clientSecret
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.success) {
|
|
133
|
+
throw new Error(`Failed to refresh token: ${response.error || 'Unknown error'}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const responseData = response.data;
|
|
137
|
+
if (!responseData || !responseData.token) {
|
|
138
|
+
throw new Error('Invalid response: missing token');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const token = responseData.token;
|
|
142
|
+
// Calculate expiration (default to 24 hours if not provided)
|
|
143
|
+
const expiresIn = responseData.expiresIn || 86400;
|
|
144
|
+
const expiresAt = responseData.expiresAt || new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
145
|
+
|
|
146
|
+
// Save token to config.yaml (NEVER save credentials)
|
|
147
|
+
await config.saveClientToken(environment, appName, controllerUrl, token, expiresAt);
|
|
148
|
+
|
|
149
|
+
return { token, expiresAt };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get or refresh client token for environment and app
|
|
154
|
+
* Checks if token exists and is valid, refreshes if expired
|
|
155
|
+
* @param {string} environment - Environment key
|
|
156
|
+
* @param {string} appName - Application name
|
|
157
|
+
* @param {string} controllerUrl - Controller URL
|
|
158
|
+
* @returns {Promise<{token: string, controller: string}>} Token and controller URL
|
|
159
|
+
* @throws {Error} If token cannot be retrieved or refreshed
|
|
160
|
+
*/
|
|
161
|
+
async function getOrRefreshClientToken(environment, appName, controllerUrl) {
|
|
162
|
+
// Try to get existing token
|
|
163
|
+
const tokenInfo = await getClientToken(environment, appName);
|
|
164
|
+
|
|
165
|
+
if (tokenInfo && tokenInfo.controller === controllerUrl && !isTokenExpired(tokenInfo.expiresAt)) {
|
|
166
|
+
// Token exists, is for correct controller, and is not expired
|
|
167
|
+
return {
|
|
168
|
+
token: tokenInfo.token,
|
|
169
|
+
controller: tokenInfo.controller
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Token missing or expired, refresh it
|
|
174
|
+
const refreshed = await refreshClientToken(environment, appName, controllerUrl);
|
|
175
|
+
return {
|
|
176
|
+
token: refreshed.token,
|
|
177
|
+
controller: controllerUrl
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Refresh device token using refresh token
|
|
183
|
+
* Calls API refresh endpoint and saves new token to config
|
|
184
|
+
* @param {string} controllerUrl - Controller URL
|
|
185
|
+
* @param {string} refreshToken - Refresh token
|
|
186
|
+
* @returns {Promise<{token: string, refreshToken: string, expiresAt: string}>} New token info
|
|
187
|
+
* @throws {Error} If refresh fails
|
|
188
|
+
*/
|
|
189
|
+
async function refreshDeviceToken(controllerUrl, refreshToken) {
|
|
190
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
191
|
+
throw new Error('Controller URL is required');
|
|
192
|
+
}
|
|
193
|
+
if (!refreshToken || typeof refreshToken !== 'string') {
|
|
194
|
+
throw new Error('Refresh token is required');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Call API refresh endpoint
|
|
198
|
+
const tokenResponse = await apiRefreshDeviceToken(controllerUrl, refreshToken);
|
|
199
|
+
|
|
200
|
+
const token = tokenResponse.access_token;
|
|
201
|
+
const newRefreshToken = tokenResponse.refresh_token || refreshToken; // Use new refresh token if provided, otherwise keep old one
|
|
202
|
+
const expiresIn = tokenResponse.expires_in || 3600;
|
|
203
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
204
|
+
|
|
205
|
+
// Save new token and refresh token to config
|
|
206
|
+
await config.saveDeviceToken(controllerUrl, token, newRefreshToken, expiresAt);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
token,
|
|
210
|
+
refreshToken: newRefreshToken,
|
|
211
|
+
expiresAt
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get or refresh device token for controller
|
|
217
|
+
* Checks if token exists and is valid, refreshes if expired using refresh token
|
|
218
|
+
* @param {string} controllerUrl - Controller URL
|
|
219
|
+
* @returns {Promise<{token: string, controller: string}|null>} Token and controller URL, or null if not available
|
|
220
|
+
*/
|
|
221
|
+
async function getOrRefreshDeviceToken(controllerUrl) {
|
|
222
|
+
// Try to get existing token
|
|
223
|
+
const tokenInfo = await getDeviceToken(controllerUrl);
|
|
224
|
+
|
|
225
|
+
if (!tokenInfo) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if token is expired
|
|
230
|
+
if (!isTokenExpired(tokenInfo.expiresAt)) {
|
|
231
|
+
// Token is valid
|
|
232
|
+
return {
|
|
233
|
+
token: tokenInfo.token,
|
|
234
|
+
controller: tokenInfo.controller
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Token is expired, try to refresh if refresh token exists
|
|
239
|
+
if (!tokenInfo.refreshToken) {
|
|
240
|
+
// No refresh token available
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const refreshed = await refreshDeviceToken(controllerUrl, tokenInfo.refreshToken);
|
|
246
|
+
return {
|
|
247
|
+
token: refreshed.token,
|
|
248
|
+
controller: controllerUrl
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
// Refresh failed, return null
|
|
252
|
+
logger.warn(`Failed to refresh device token: ${error.message}`);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get deployment authentication configuration with priority:
|
|
259
|
+
* 1. Device token (Bearer) - for user-level audit tracking (preferred)
|
|
260
|
+
* 2. Client token (Bearer) - for application-level authentication
|
|
261
|
+
* 3. Client credentials (x-client-id/x-client-secret) - direct credential authentication
|
|
262
|
+
*
|
|
263
|
+
* @param {string} controllerUrl - Controller URL
|
|
264
|
+
* @param {string} environment - Environment key
|
|
265
|
+
* @param {string} appName - Application name
|
|
266
|
+
* @returns {Promise<{type: 'bearer'|'credentials', token?: string, clientId?: string, clientSecret?: string, controller: string}>} Auth configuration
|
|
267
|
+
* @throws {Error} If no authentication method is available
|
|
268
|
+
*/
|
|
269
|
+
async function getDeploymentAuth(controllerUrl, environment, appName) {
|
|
270
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
271
|
+
throw new Error('Controller URL is required');
|
|
272
|
+
}
|
|
273
|
+
if (!environment || typeof environment !== 'string') {
|
|
274
|
+
throw new Error('Environment is required');
|
|
275
|
+
}
|
|
276
|
+
if (!appName || typeof appName !== 'string') {
|
|
277
|
+
throw new Error('App name is required');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Priority 1: Try device token (for user-level audit)
|
|
281
|
+
const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
282
|
+
if (deviceToken && deviceToken.token) {
|
|
283
|
+
return {
|
|
284
|
+
type: 'bearer',
|
|
285
|
+
token: deviceToken.token,
|
|
286
|
+
controller: deviceToken.controller
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Priority 2: Try client token (application-level)
|
|
291
|
+
try {
|
|
292
|
+
const clientToken = await getOrRefreshClientToken(environment, appName, controllerUrl);
|
|
293
|
+
if (clientToken && clientToken.token) {
|
|
294
|
+
return {
|
|
295
|
+
type: 'bearer',
|
|
296
|
+
token: clientToken.token,
|
|
297
|
+
controller: clientToken.controller
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
// Client token unavailable, continue to credentials
|
|
302
|
+
logger.warn(`Client token unavailable: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Priority 3: Use client credentials directly
|
|
306
|
+
const credentials = await loadClientCredentials(appName);
|
|
307
|
+
if (credentials && credentials.clientId && credentials.clientSecret) {
|
|
308
|
+
return {
|
|
309
|
+
type: 'credentials',
|
|
310
|
+
clientId: credentials.clientId,
|
|
311
|
+
clientSecret: credentials.clientSecret,
|
|
312
|
+
controller: controllerUrl
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
throw new Error(`No authentication method available. Run 'aifabrix login' for device token, or add credentials to ~/.aifabrix/secrets.local.yaml as '${appName}-client-idKeyVault' and '${appName}-client-secretKeyVault'`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Extracts client credentials from authConfig, loading from secrets if needed
|
|
321
|
+
* Used for validation and deployment endpoints that require clientId/clientSecret
|
|
322
|
+
* @async
|
|
323
|
+
* @param {Object} authConfig - Authentication configuration
|
|
324
|
+
* @param {string} appKey - Application key for loading credentials
|
|
325
|
+
* @param {string} envKey - Environment key
|
|
326
|
+
* @param {Object} options - Options with controllerId
|
|
327
|
+
* @returns {Promise<{clientId: string, clientSecret: string}>} Client credentials
|
|
328
|
+
* @throws {Error} If credentials cannot be obtained
|
|
329
|
+
*/
|
|
330
|
+
async function extractClientCredentials(authConfig, appKey, envKey, _options = {}) {
|
|
331
|
+
if (authConfig.type === 'credentials') {
|
|
332
|
+
if (!authConfig.clientId || !authConfig.clientSecret) {
|
|
333
|
+
throw new Error('Client ID and Client Secret are required');
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
clientId: authConfig.clientId,
|
|
337
|
+
clientSecret: authConfig.clientSecret
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (authConfig.type === 'bearer') {
|
|
342
|
+
if (authConfig.clientId && authConfig.clientSecret) {
|
|
343
|
+
return {
|
|
344
|
+
clientId: authConfig.clientId,
|
|
345
|
+
clientSecret: authConfig.clientSecret
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Try to load from secrets.local.yaml
|
|
350
|
+
const credentials = await loadClientCredentials(appKey);
|
|
351
|
+
if (credentials && credentials.clientId && credentials.clientSecret) {
|
|
352
|
+
// Store in authConfig so they're available for deployment step
|
|
353
|
+
authConfig.clientId = credentials.clientId;
|
|
354
|
+
authConfig.clientSecret = credentials.clientSecret;
|
|
355
|
+
return {
|
|
356
|
+
clientId: credentials.clientId,
|
|
357
|
+
clientSecret: credentials.clientSecret
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Construct clientId from controller, environment, and application key
|
|
362
|
+
// (not used, but shown in error message for reference)
|
|
363
|
+
throw new Error(`Client ID and Client Secret are required. Add credentials to ~/.aifabrix/secrets.local.yaml as '${appKey}-client-idKeyVault' and '${appKey}-client-secretKeyVault', or use credentials authentication.`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
throw new Error('Invalid authentication type');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
getDeviceToken,
|
|
371
|
+
getClientToken,
|
|
372
|
+
isTokenExpired,
|
|
373
|
+
refreshClientToken,
|
|
374
|
+
refreshDeviceToken,
|
|
375
|
+
loadClientCredentials,
|
|
376
|
+
getOrRefreshClientToken,
|
|
377
|
+
getOrRefreshDeviceToken,
|
|
378
|
+
getDeploymentAuth,
|
|
379
|
+
extractClientCredentials
|
|
380
|
+
};
|
|
381
|
+
|
package/package.json
CHANGED
|
@@ -2,56 +2,193 @@
|
|
|
2
2
|
|
|
3
3
|
Build, run, and deploy {{displayName}} using `@aifabrix/builder`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Install
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
npm install -g @aifabrix/builder
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
### 2. First Time Setup
|
|
12
16
|
|
|
13
17
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
# Check your environment
|
|
19
|
+
aifabrix doctor
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
# Login to controller
|
|
22
|
+
aifabrix login --method device --environment dev
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
# Register your application (gets you credentials automatically)
|
|
25
|
+
aifabrix app register {{appName}} --environment dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 3. Build & Run Locally
|
|
20
29
|
|
|
21
30
|
```bash
|
|
31
|
+
# Build the Docker image
|
|
32
|
+
aifabrix build {{appName}}
|
|
33
|
+
|
|
34
|
+
# Generate environment variables
|
|
35
|
+
aifabrix resolve {{appName}}
|
|
36
|
+
|
|
37
|
+
# Run locally
|
|
22
38
|
aifabrix run {{appName}}
|
|
23
39
|
```
|
|
24
40
|
|
|
25
|
-
**Access:** http://localhost:{{port}}
|
|
41
|
+
**Access your app:** http://localhost:{{port}}
|
|
26
42
|
|
|
27
|
-
**
|
|
43
|
+
**View logs:**
|
|
28
44
|
```bash
|
|
29
|
-
docker logs {{appName}} -f
|
|
45
|
+
docker logs aifabrix-{{appName}} -f
|
|
30
46
|
```
|
|
31
47
|
|
|
32
48
|
**Stop:**
|
|
33
49
|
```bash
|
|
34
|
-
docker stop {{appName}}
|
|
50
|
+
docker stop aifabrix-{{appName}}
|
|
35
51
|
```
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
### 4. Deploy to Azure
|
|
38
54
|
|
|
39
55
|
```bash
|
|
40
|
-
|
|
56
|
+
# Build with version tag
|
|
57
|
+
aifabrix build {{appName}} --tag v1.0.0
|
|
58
|
+
|
|
59
|
+
# Push to registry
|
|
60
|
+
aifabrix push {{appName}} --registry {{registry}} --tag "v1.0.0,latest"
|
|
61
|
+
|
|
62
|
+
# Deploy to miso-controller
|
|
63
|
+
aifabrix deploy {{appName}} --controller https://controller.aifabrix.ai --environment dev
|
|
41
64
|
```
|
|
42
65
|
|
|
43
|
-
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Using miso-client
|
|
69
|
+
|
|
70
|
+
> [miso-client](https://github.com/esystemsdev/aifabrix-miso-client)
|
|
71
|
+
|
|
72
|
+
After registering your app, you automatically get credentials in your secret file. Use miso-client for login, RBAC, audit logs, etc.
|
|
73
|
+
|
|
74
|
+
**Rotate credentials if needed:**
|
|
75
|
+
```bash
|
|
76
|
+
aifabrix app rotate-secret {{appName}} --environment dev
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Reference
|
|
82
|
+
|
|
83
|
+
### Common Commands
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Development
|
|
87
|
+
aifabrix build {{appName}} # Build app
|
|
88
|
+
aifabrix run {{appName}} # Run locally
|
|
89
|
+
aifabrix dockerfile {{appName}} --force # Generate Dockerfile
|
|
90
|
+
aifabrix resolve {{appName}} # Generate .env file
|
|
91
|
+
|
|
92
|
+
# Deployment
|
|
93
|
+
aifabrix json {{appName}} # Preview deployment JSON
|
|
94
|
+
aifabrix genkey {{appName}} # Generate deployment key
|
|
95
|
+
aifabrix push {{appName}} --registry {{registry}} # Push to ACR
|
|
96
|
+
aifabrix deploy {{appName}} --controller <url> # Deploy to Azure
|
|
97
|
+
|
|
98
|
+
# Management
|
|
99
|
+
aifabrix app register {{appName}} --environment dev
|
|
100
|
+
aifabrix app list --environment dev
|
|
101
|
+
aifabrix app rotate-secret {{appName}} --environment dev
|
|
102
|
+
|
|
103
|
+
# Utilities
|
|
104
|
+
aifabrix doctor # Check environment
|
|
105
|
+
aifabrix login --method device --environment dev # Login
|
|
106
|
+
aifabrix --help # Get help
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Build Options
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
aifabrix build {{appName}} --tag v1.0.0 # Custom tag
|
|
113
|
+
aifabrix build {{appName}} --force-template # Force template regeneration
|
|
114
|
+
aifabrix build {{appName}} --language typescript # Override language detection
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Run Options
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
aifabrix run {{appName}} --port {{port}} # Custom port
|
|
121
|
+
aifabrix run {{appName}} --debug # Debug output
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Push Options
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
aifabrix push {{appName}} --registry {{registry}} --tag v1.0.0
|
|
128
|
+
aifabrix push {{appName}} --registry {{registry}} --tag "v1.0.0,latest,stable"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Deploy Options
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
aifabrix deploy {{appName}} --controller <url> --environment dev
|
|
135
|
+
aifabrix deploy {{appName}} --controller <url> --environment dev --no-poll
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Login Methods
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Device code flow
|
|
142
|
+
aifabrix login --method device --environment dev
|
|
143
|
+
|
|
144
|
+
# Credentials (reads from secrets.local.yaml)
|
|
145
|
+
aifabrix login --method credentials --app {{appName}} --environment dev
|
|
146
|
+
|
|
147
|
+
# Explicit credentials
|
|
148
|
+
aifabrix login --method credentials --app {{appName}} --client-id $CLIENT_ID --client-secret $CLIENT_SECRET --environment dev
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Environment Variables
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
export AIFABRIX_HOME=/custom/path
|
|
155
|
+
export AIFABRIX_SECRETS=/path/to/secrets.yaml
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Troubleshooting
|
|
161
|
+
|
|
162
|
+
- **"Docker not running"** → Start Docker Desktop
|
|
163
|
+
- **"Not logged in"** → Run `aifabrix login` first
|
|
164
|
+
- **"Port already in use"** → Use `--port` flag or change `build.localPort` in `variables.yaml` (default: {{port}})
|
|
165
|
+
- **"Authentication failed"** → Run `aifabrix login` again
|
|
166
|
+
- **"Build fails"** → Check Docker is running and `variables.yaml` → `build.secrets` path is correct
|
|
167
|
+
- **"Can't connect"** → Verify infrastructure is running{{#if hasDatabase}} and PostgreSQL is accessible{{/if}}
|
|
168
|
+
|
|
169
|
+
**Regenerate files:**
|
|
170
|
+
```bash
|
|
171
|
+
aifabrix resolve {{appName}} --force
|
|
172
|
+
aifabrix json {{appName}}
|
|
173
|
+
aifabrix genkey {{appName}}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
44
177
|
|
|
45
178
|
## Prerequisites
|
|
46
179
|
|
|
47
180
|
- `@aifabrix/builder` installed globally
|
|
48
181
|
- Docker Desktop running
|
|
49
|
-
-
|
|
182
|
+
- Azure CLI installed (for push command)
|
|
183
|
+
- Authenticated with controller (for deploy command)
|
|
184
|
+
{{#unless hasAnyService}}
|
|
185
|
+
- Infrastructure running
|
|
186
|
+
{{/unless}}
|
|
50
187
|
{{#if hasDatabase}}
|
|
51
|
-
- PostgreSQL database
|
|
188
|
+
- PostgreSQL database (ensure infrastructure is running)
|
|
52
189
|
{{/if}}
|
|
53
190
|
{{#if hasRedis}}
|
|
54
|
-
- Redis
|
|
191
|
+
- Redis (ensure infrastructure is running)
|
|
55
192
|
{{/if}}
|
|
56
193
|
{{#if hasStorage}}
|
|
57
194
|
- File storage configured
|
|
@@ -60,11 +197,6 @@ aifabrix push {{appName}} --registry {{registry}} --tag latest
|
|
|
60
197
|
- Authentication/RBAC configured
|
|
61
198
|
{{/if}}
|
|
62
199
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
**Build fails:** Check Docker is running and `variables.yaml` → `build.secrets` path is correct
|
|
66
|
-
|
|
67
|
-
**Can't connect:** Verify infrastructure is running (`aifabrix up`){{#if hasDatabase}} and PostgreSQL is accessible{{/if}}
|
|
68
|
-
|
|
69
|
-
**Port in use:** Change `build.localPort` in `variables.yaml` (default: {{port}})
|
|
200
|
+
---
|
|
70
201
|
|
|
202
|
+
**Application**: {{appName}} | **Port**: {{port}} | **Registry**: {{registry}} | **Image**: {{imageName}}:latest
|