aranea-sdk-cli 0.3.0 → 0.3.1

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.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * AraneaSDK CLI - Auth Command
3
+ *
4
+ * Firebase Auth token management for CLI operations
5
+ *
6
+ * Usage:
7
+ * aranea-sdk auth login # Interactive login
8
+ * aranea-sdk auth token # Show current token info
9
+ * aranea-sdk auth refresh # Refresh token
10
+ * aranea-sdk auth logout # Clear saved credentials
11
+ *
12
+ * Token Storage:
13
+ * ~/.aranea-sdk/credentials.json
14
+ *
15
+ * Note: ID tokens expire after 1 hour. Use `auth refresh` to get a new token.
16
+ */
17
+ import { Command } from 'commander';
18
+ /**
19
+ * Get valid token, refreshing if necessary
20
+ * Exported for use by other commands
21
+ */
22
+ export declare function getValidToken(): Promise<string | null>;
23
+ /**
24
+ * Auth command
25
+ */
26
+ export declare const authCommand: Command;
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+ /**
3
+ * AraneaSDK CLI - Auth Command
4
+ *
5
+ * Firebase Auth token management for CLI operations
6
+ *
7
+ * Usage:
8
+ * aranea-sdk auth login # Interactive login
9
+ * aranea-sdk auth token # Show current token info
10
+ * aranea-sdk auth refresh # Refresh token
11
+ * aranea-sdk auth logout # Clear saved credentials
12
+ *
13
+ * Token Storage:
14
+ * ~/.aranea-sdk/credentials.json
15
+ *
16
+ * Note: ID tokens expire after 1 hour. Use `auth refresh` to get a new token.
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ var __importDefault = (this && this.__importDefault) || function (mod) {
52
+ return (mod && mod.__esModule) ? mod : { "default": mod };
53
+ };
54
+ Object.defineProperty(exports, "__esModule", { value: true });
55
+ exports.authCommand = void 0;
56
+ exports.getValidToken = getValidToken;
57
+ const commander_1 = require("commander");
58
+ const fs = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ const os = __importStar(require("os"));
61
+ const readline = __importStar(require("readline"));
62
+ const chalk_1 = __importDefault(require("chalk"));
63
+ // Firebase Auth REST API configuration
64
+ const FIREBASE_API_KEY = 'AIzaSyChVMfLPZ9fL_LQBK2TtLbM2iONPhyU1NY'; // mobesorder project
65
+ const AUTH_ENDPOINT = 'https://identitytoolkit.googleapis.com/v1';
66
+ // Credentials storage
67
+ const CONFIG_DIR = path.join(os.homedir(), '.aranea-sdk');
68
+ const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
69
+ /**
70
+ * Ensure config directory exists
71
+ */
72
+ function ensureConfigDir() {
73
+ if (!fs.existsSync(CONFIG_DIR)) {
74
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
75
+ }
76
+ }
77
+ /**
78
+ * Load saved credentials
79
+ */
80
+ function loadCredentials() {
81
+ try {
82
+ if (fs.existsSync(CREDENTIALS_FILE)) {
83
+ const data = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
84
+ return JSON.parse(data);
85
+ }
86
+ }
87
+ catch (error) {
88
+ // Ignore parse errors
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * Save credentials to disk
94
+ */
95
+ function saveCredentials(credentials) {
96
+ ensureConfigDir();
97
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
98
+ mode: 0o600,
99
+ });
100
+ }
101
+ /**
102
+ * Clear saved credentials
103
+ */
104
+ function clearCredentials() {
105
+ if (fs.existsSync(CREDENTIALS_FILE)) {
106
+ fs.unlinkSync(CREDENTIALS_FILE);
107
+ }
108
+ }
109
+ /**
110
+ * Check if token is expired or about to expire (within 5 minutes)
111
+ */
112
+ function isTokenExpired(credentials) {
113
+ const bufferMs = 5 * 60 * 1000; // 5 minutes buffer
114
+ return Date.now() >= credentials.expiresAt - bufferMs;
115
+ }
116
+ /**
117
+ * Format remaining time
118
+ */
119
+ function formatRemainingTime(expiresAt) {
120
+ const remaining = expiresAt - Date.now();
121
+ if (remaining <= 0) {
122
+ return chalk_1.default.red('Expired');
123
+ }
124
+ const minutes = Math.floor(remaining / 60000);
125
+ const seconds = Math.floor((remaining % 60000) / 1000);
126
+ if (minutes > 30) {
127
+ return chalk_1.default.green(`${minutes}m ${seconds}s`);
128
+ }
129
+ else if (minutes > 5) {
130
+ return chalk_1.default.yellow(`${minutes}m ${seconds}s`);
131
+ }
132
+ else {
133
+ return chalk_1.default.red(`${minutes}m ${seconds}s`);
134
+ }
135
+ }
136
+ /**
137
+ * Prompt for password (hidden input)
138
+ */
139
+ async function promptPassword(prompt) {
140
+ return new Promise((resolve) => {
141
+ const rl = readline.createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout,
144
+ });
145
+ // Hide password input
146
+ const stdin = process.stdin;
147
+ const wasRaw = stdin.isRaw || false;
148
+ process.stdout.write(prompt);
149
+ if (stdin.isTTY) {
150
+ stdin.setRawMode(true);
151
+ }
152
+ let password = '';
153
+ const onData = (char) => {
154
+ const c = char.toString();
155
+ switch (c) {
156
+ case '\n':
157
+ case '\r':
158
+ case '\u0004': // Ctrl+D
159
+ if (stdin.isTTY) {
160
+ stdin.setRawMode(wasRaw);
161
+ }
162
+ stdin.removeListener('data', onData);
163
+ rl.close();
164
+ process.stdout.write('\n');
165
+ resolve(password);
166
+ break;
167
+ case '\u0003': // Ctrl+C
168
+ process.exit();
169
+ break;
170
+ case '\u007F': // Backspace
171
+ if (password.length > 0) {
172
+ password = password.slice(0, -1);
173
+ process.stdout.write('\b \b');
174
+ }
175
+ break;
176
+ default:
177
+ password += c;
178
+ process.stdout.write('*');
179
+ break;
180
+ }
181
+ };
182
+ stdin.on('data', onData);
183
+ });
184
+ }
185
+ /**
186
+ * Prompt for email
187
+ */
188
+ async function promptEmail() {
189
+ return new Promise((resolve) => {
190
+ const rl = readline.createInterface({
191
+ input: process.stdin,
192
+ output: process.stdout,
193
+ });
194
+ rl.question('Email: ', (answer) => {
195
+ rl.close();
196
+ resolve(answer.trim());
197
+ });
198
+ });
199
+ }
200
+ /**
201
+ * Login with email and password
202
+ */
203
+ async function loginWithEmailPassword(email, password) {
204
+ const response = await fetch(`${AUTH_ENDPOINT}/accounts:signInWithPassword?key=${FIREBASE_API_KEY}`, {
205
+ method: 'POST',
206
+ headers: { 'Content-Type': 'application/json' },
207
+ body: JSON.stringify({
208
+ email,
209
+ password,
210
+ returnSecureToken: true,
211
+ }),
212
+ });
213
+ if (!response.ok) {
214
+ const errorBody = await response.json();
215
+ const errorMessage = errorBody.error?.message || 'Login failed';
216
+ throw new Error(translateFirebaseError(errorMessage));
217
+ }
218
+ const data = (await response.json());
219
+ const expiresIn = parseInt(data.expiresIn, 10) * 1000; // Convert to ms
220
+ return {
221
+ idToken: data.idToken,
222
+ refreshToken: data.refreshToken,
223
+ email: data.email,
224
+ expiresAt: Date.now() + expiresIn,
225
+ localId: data.localId,
226
+ };
227
+ }
228
+ /**
229
+ * Refresh ID token
230
+ */
231
+ async function refreshIdToken(refreshToken) {
232
+ const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`, {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
235
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
236
+ });
237
+ if (!response.ok) {
238
+ const errorBody = await response.json();
239
+ throw new Error(errorBody.error?.message || 'Token refresh failed');
240
+ }
241
+ const data = (await response.json());
242
+ const expiresIn = parseInt(data.expires_in, 10) * 1000;
243
+ // Load existing credentials to preserve email
244
+ const existing = loadCredentials();
245
+ return {
246
+ idToken: data.id_token,
247
+ refreshToken: data.refresh_token,
248
+ email: existing?.email || '',
249
+ expiresAt: Date.now() + expiresIn,
250
+ localId: data.user_id,
251
+ };
252
+ }
253
+ /**
254
+ * Translate Firebase error messages to user-friendly Japanese
255
+ */
256
+ function translateFirebaseError(errorCode) {
257
+ const translations = {
258
+ EMAIL_NOT_FOUND: 'メールアドレスが見つかりません',
259
+ INVALID_PASSWORD: 'パスワードが正しくありません',
260
+ INVALID_EMAIL: 'メールアドレスの形式が正しくありません',
261
+ USER_DISABLED: 'このアカウントは無効化されています',
262
+ TOO_MANY_ATTEMPTS_TRY_LATER: 'ログイン試行回数が多すぎます。しばらく待ってから再試行してください',
263
+ INVALID_LOGIN_CREDENTIALS: 'メールアドレスまたはパスワードが正しくありません',
264
+ TOKEN_EXPIRED: 'トークンの有効期限が切れています。再度ログインしてください',
265
+ INVALID_REFRESH_TOKEN: 'リフレッシュトークンが無効です。再度ログインしてください',
266
+ };
267
+ return translations[errorCode] || errorCode;
268
+ }
269
+ /**
270
+ * Get valid token, refreshing if necessary
271
+ * Exported for use by other commands
272
+ */
273
+ async function getValidToken() {
274
+ const credentials = loadCredentials();
275
+ if (!credentials) {
276
+ return null;
277
+ }
278
+ if (isTokenExpired(credentials)) {
279
+ try {
280
+ const newCredentials = await refreshIdToken(credentials.refreshToken);
281
+ saveCredentials(newCredentials);
282
+ return newCredentials.idToken;
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ }
288
+ return credentials.idToken;
289
+ }
290
+ /**
291
+ * Auth command
292
+ */
293
+ exports.authCommand = new commander_1.Command('auth')
294
+ .description('Firebase Auth token management')
295
+ .addHelpText('after', `
296
+ Token Lifecycle:
297
+ - ID tokens expire after 1 hour
298
+ - Use 'auth refresh' to get a new token
299
+ - Tokens are auto-saved to ~/.aranea-sdk/credentials.json
300
+
301
+ Examples:
302
+ aranea-sdk auth login # Interactive login
303
+ aranea-sdk auth token # Show current token info
304
+ aranea-sdk auth refresh # Refresh expired token
305
+ aranea-sdk auth logout # Clear saved credentials
306
+ `);
307
+ // auth login
308
+ exports.authCommand
309
+ .command('login')
310
+ .description('Login with email and password')
311
+ .option('-e, --email <email>', 'Email address')
312
+ .action(async (options) => {
313
+ console.log(chalk_1.default.cyan('\n=== AraneaSDK CLI Login ===\n'));
314
+ try {
315
+ // Get email
316
+ const email = options.email || (await promptEmail());
317
+ if (!email) {
318
+ console.error(chalk_1.default.red('Email is required'));
319
+ process.exit(1);
320
+ }
321
+ // Get password
322
+ const password = await promptPassword('Password: ');
323
+ if (!password) {
324
+ console.error(chalk_1.default.red('Password is required'));
325
+ process.exit(1);
326
+ }
327
+ console.log('\nLogging in...');
328
+ const credentials = await loginWithEmailPassword(email, password);
329
+ saveCredentials(credentials);
330
+ console.log(chalk_1.default.green('\n✓ Login successful!'));
331
+ console.log(` Email: ${credentials.email}`);
332
+ console.log(` User ID: ${credentials.localId}`);
333
+ console.log(` Token expires: ${formatRemainingTime(credentials.expiresAt)}`);
334
+ console.log(`\nCredentials saved to: ${CREDENTIALS_FILE}`);
335
+ console.log(chalk_1.default.yellow('\nNote: ID tokens expire after 1 hour. Use "aranea-sdk auth refresh" to renew.'));
336
+ }
337
+ catch (error) {
338
+ console.error(chalk_1.default.red(`\n✖ Login failed: ${error.message}`));
339
+ process.exit(1);
340
+ }
341
+ });
342
+ // auth token
343
+ exports.authCommand
344
+ .command('token')
345
+ .description('Show current token information')
346
+ .option('--raw', 'Output raw token value only')
347
+ .action(async (options) => {
348
+ const credentials = loadCredentials();
349
+ if (!credentials) {
350
+ if (options.raw) {
351
+ process.exit(1);
352
+ }
353
+ console.log(chalk_1.default.yellow('\nNo saved credentials found.'));
354
+ console.log('Run "aranea-sdk auth login" to authenticate.');
355
+ process.exit(1);
356
+ }
357
+ if (options.raw) {
358
+ // Output just the token for scripting
359
+ if (isTokenExpired(credentials)) {
360
+ try {
361
+ const newCredentials = await refreshIdToken(credentials.refreshToken);
362
+ saveCredentials(newCredentials);
363
+ console.log(newCredentials.idToken);
364
+ }
365
+ catch {
366
+ process.exit(1);
367
+ }
368
+ }
369
+ else {
370
+ console.log(credentials.idToken);
371
+ }
372
+ return;
373
+ }
374
+ console.log(chalk_1.default.cyan('\n=== AraneaSDK CLI Token Info ===\n'));
375
+ console.log(` Email: ${credentials.email}`);
376
+ console.log(` User ID: ${credentials.localId}`);
377
+ console.log(` Expires in: ${formatRemainingTime(credentials.expiresAt)}`);
378
+ console.log(` Expires at: ${new Date(credentials.expiresAt).toLocaleString('ja-JP')}`);
379
+ if (isTokenExpired(credentials)) {
380
+ console.log(chalk_1.default.red('\n⚠ Token is expired or about to expire.'));
381
+ console.log('Run "aranea-sdk auth refresh" to get a new token.');
382
+ }
383
+ else {
384
+ console.log(chalk_1.default.green('\n✓ Token is valid'));
385
+ }
386
+ console.log(`\nToken (first 50 chars): ${credentials.idToken.substring(0, 50)}...`);
387
+ });
388
+ // auth refresh
389
+ exports.authCommand
390
+ .command('refresh')
391
+ .description('Refresh the ID token')
392
+ .action(async () => {
393
+ const credentials = loadCredentials();
394
+ if (!credentials) {
395
+ console.log(chalk_1.default.yellow('\nNo saved credentials found.'));
396
+ console.log('Run "aranea-sdk auth login" to authenticate.');
397
+ process.exit(1);
398
+ }
399
+ console.log('Refreshing token...');
400
+ try {
401
+ const newCredentials = await refreshIdToken(credentials.refreshToken);
402
+ saveCredentials(newCredentials);
403
+ console.log(chalk_1.default.green('\n✓ Token refreshed successfully!'));
404
+ console.log(` Email: ${newCredentials.email}`);
405
+ console.log(` Expires in: ${formatRemainingTime(newCredentials.expiresAt)}`);
406
+ console.log(` Expires at: ${new Date(newCredentials.expiresAt).toLocaleString('ja-JP')}`);
407
+ }
408
+ catch (error) {
409
+ console.error(chalk_1.default.red(`\n✖ Token refresh failed: ${error.message}`));
410
+ console.log('You may need to run "aranea-sdk auth login" again.');
411
+ process.exit(1);
412
+ }
413
+ });
414
+ // auth logout
415
+ exports.authCommand
416
+ .command('logout')
417
+ .description('Clear saved credentials')
418
+ .action(() => {
419
+ const credentials = loadCredentials();
420
+ if (!credentials) {
421
+ console.log(chalk_1.default.yellow('\nNo saved credentials found.'));
422
+ return;
423
+ }
424
+ clearCredentials();
425
+ console.log(chalk_1.default.green('\n✓ Credentials cleared.'));
426
+ console.log(` Removed: ${CREDENTIALS_FILE}`);
427
+ });
@@ -2,13 +2,17 @@
2
2
  * schema command
3
3
  *
4
4
  * Usage:
5
- * aranea-sdk schema get --type "aranea_ar-is04a"
6
- * aranea-sdk schema list
5
+ * aranea-sdk schema list [--endpoint staging|production]
6
+ * aranea-sdk schema get --type "aranea_ar-is04a" [--endpoint staging|production]
7
7
  * aranea-sdk schema validate --type "aranea_ar-is04a" --file state.json
8
8
  * aranea-sdk schema validate-schema --file schema.json
9
- * aranea-sdk schema push --file schema.json [--token TOKEN]
10
- * aranea-sdk schema promote --type "aranea_ar-is04a" [--token TOKEN]
11
- * aranea-sdk schema info --type "aranea_ar-is04a"
9
+ * aranea-sdk schema push --file schema.json [--token TOKEN] [--endpoint staging|production]
10
+ * aranea-sdk schema promote --type "aranea_ar-is04a" [--token TOKEN] [--endpoint staging|production] [--confirm]
11
+ * aranea-sdk schema info --type "aranea_ar-is04a" [--endpoint staging|production]
12
+ *
13
+ * Environment:
14
+ * - Default: staging (for safety)
15
+ * - Override: ARANEA_ENV=production or --endpoint production
12
16
  */
13
17
  import { Command } from 'commander';
14
18
  export declare const schemaCommand: Command;
@@ -3,13 +3,17 @@
3
3
  * schema command
4
4
  *
5
5
  * Usage:
6
- * aranea-sdk schema get --type "aranea_ar-is04a"
7
- * aranea-sdk schema list
6
+ * aranea-sdk schema list [--endpoint staging|production]
7
+ * aranea-sdk schema get --type "aranea_ar-is04a" [--endpoint staging|production]
8
8
  * aranea-sdk schema validate --type "aranea_ar-is04a" --file state.json
9
9
  * aranea-sdk schema validate-schema --file schema.json
10
- * aranea-sdk schema push --file schema.json [--token TOKEN]
11
- * aranea-sdk schema promote --type "aranea_ar-is04a" [--token TOKEN]
12
- * aranea-sdk schema info --type "aranea_ar-is04a"
10
+ * aranea-sdk schema push --file schema.json [--token TOKEN] [--endpoint staging|production]
11
+ * aranea-sdk schema promote --type "aranea_ar-is04a" [--token TOKEN] [--endpoint staging|production] [--confirm]
12
+ * aranea-sdk schema info --type "aranea_ar-is04a" [--endpoint staging|production]
13
+ *
14
+ * Environment:
15
+ * - Default: staging (for safety)
16
+ * - Override: ARANEA_ENV=production or --endpoint production
13
17
  */
14
18
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
19
  if (k2 === undefined) k2 = k;
@@ -55,29 +59,118 @@ const ora_1 = __importDefault(require("ora"));
55
59
  const axios_1 = __importDefault(require("axios"));
56
60
  const fs = __importStar(require("fs"));
57
61
  const path = __importStar(require("path"));
58
- // API Configuration
59
- const SCHEMA_API_BASE = 'https://asia-northeast1-mobesorder.cloudfunctions.net/araneaSchemaAPI';
62
+ const readline = __importStar(require("readline"));
63
+ const config_1 = require("../config");
64
+ // Helper: Get API base URL for environment
65
+ function getSchemaApiBase(env) {
66
+ return (0, config_1.getEndpoint)(env).schemaAPI;
67
+ }
68
+ // Helper: Get auth token from options or environment
69
+ function getAuthToken(options) {
70
+ return options.token || process.env.ARANEA_AUTH_TOKEN || process.env.FIREBASE_AUTH_TOKEN || null;
71
+ }
72
+ // Helper: Prompt for confirmation
73
+ async function promptConfirm(message) {
74
+ const rl = readline.createInterface({
75
+ input: process.stdin,
76
+ output: process.stdout,
77
+ });
78
+ return new Promise((resolve) => {
79
+ rl.question(`${message} (y/N): `, (answer) => {
80
+ rl.close();
81
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
82
+ });
83
+ });
84
+ }
60
85
  // API Functions
61
- async function fetchSchemaList() {
62
- const response = await axios_1.default.get(`${SCHEMA_API_BASE}?action=list`);
86
+ async function fetchSchemaList(apiBase) {
87
+ const response = await axios_1.default.get(`${apiBase}?action=list`);
63
88
  return response.data;
64
89
  }
65
- async function fetchSchema(type) {
66
- const response = await axios_1.default.get(`${SCHEMA_API_BASE}?action=get&type=${encodeURIComponent(type)}`);
90
+ async function fetchSchema(apiBase, type) {
91
+ const response = await axios_1.default.get(`${apiBase}?action=get&type=${encodeURIComponent(type)}`);
67
92
  return response.data;
68
93
  }
94
+ // Helper: Validate schema file structure (enhanced)
95
+ function validateSchemaStructure(schema) {
96
+ const errors = [];
97
+ const warnings = [];
98
+ // Required top-level fields
99
+ const requiredFields = ['type', 'stateSchema'];
100
+ for (const field of requiredFields) {
101
+ if (!schema[field]) {
102
+ errors.push(`Missing required field: ${field}`);
103
+ }
104
+ }
105
+ // Recommended fields (warn if missing)
106
+ const recommendedFields = ['displayName', 'description', 'productType', 'capabilities'];
107
+ for (const field of recommendedFields) {
108
+ if (!schema[field]) {
109
+ warnings.push(`Recommended field missing: ${field}`);
110
+ }
111
+ }
112
+ // Type validation
113
+ if (schema.type && typeof schema.type !== 'string') {
114
+ errors.push('Field "type" must be a string');
115
+ }
116
+ // ProductType validation
117
+ if (schema.productType && !/^\d{3}$/.test(schema.productType)) {
118
+ errors.push('Field "productType" must be a 3-digit string (e.g., "001")');
119
+ }
120
+ // stateSchema validation
121
+ if (schema.stateSchema) {
122
+ if (typeof schema.stateSchema !== 'object') {
123
+ errors.push('Field "stateSchema" must be an object');
124
+ }
125
+ else {
126
+ if (schema.stateSchema.type !== 'object') {
127
+ errors.push('Field "stateSchema.type" must be "object"');
128
+ }
129
+ if (!schema.stateSchema.properties || typeof schema.stateSchema.properties !== 'object') {
130
+ warnings.push('stateSchema.properties is missing or not an object');
131
+ }
132
+ }
133
+ }
134
+ // configSchema validation (optional but if present, must be object)
135
+ if (schema.configSchema && typeof schema.configSchema !== 'object') {
136
+ errors.push('Field "configSchema" must be an object');
137
+ }
138
+ // commandSchema validation (optional but if present, must be object)
139
+ if (schema.commandSchema && typeof schema.commandSchema !== 'object') {
140
+ errors.push('Field "commandSchema" must be an object');
141
+ }
142
+ // capabilities validation (optional but if present, must be array)
143
+ if (schema.capabilities && !Array.isArray(schema.capabilities)) {
144
+ errors.push('Field "capabilities" must be an array');
145
+ }
146
+ // features validation (optional but if present, must be array)
147
+ if (schema.features && !Array.isArray(schema.features)) {
148
+ errors.push('Field "features" must be an array');
149
+ }
150
+ // Warn about client-controlled fields that will be ignored
151
+ if (schema.revisionNumber !== undefined) {
152
+ warnings.push('revisionNumber will be ignored (server-controlled)');
153
+ }
154
+ if (schema.state !== undefined) {
155
+ warnings.push('state will be set to "development" on push (server-controlled)');
156
+ }
157
+ return { valid: errors.length === 0, errors, warnings };
158
+ }
69
159
  exports.schemaCommand = new commander_1.Command('schema')
70
- .description('Type schema retrieval and validation');
160
+ .description('Type schema retrieval and validation (default: staging)');
71
161
  // schema list
72
162
  exports.schemaCommand
73
163
  .command('list')
74
164
  .description('List available Type schemas')
75
- .action(async () => {
76
- const spinner = (0, ora_1.default)('Fetching schema list from API...').start();
165
+ .option('-e, --endpoint <env>', 'Environment (staging|production)', 'staging')
166
+ .action(async (options) => {
167
+ const env = (0, config_1.resolveEnvironment)(options.endpoint);
168
+ const apiBase = getSchemaApiBase(env);
169
+ const spinner = (0, ora_1.default)(`Fetching schema list from ${env}...`).start();
77
170
  try {
78
- const result = await fetchSchemaList();
171
+ const result = await fetchSchemaList(apiBase);
79
172
  spinner.stop();
80
- console.log(chalk_1.default.bold('\n=== AraneaDevice Type Schema List ===\n'));
173
+ console.log(chalk_1.default.bold(`\n=== AraneaDevice Type Schema List (${env}) ===\n`));
81
174
  if (!result.ok || result.count === 0) {
82
175
  console.log(chalk_1.default.yellow('No schemas found.'));
83
176
  console.log(chalk_1.default.gray('Run schema registration script to initialize schemas.'));
@@ -87,13 +180,14 @@ exports.schemaCommand
87
180
  console.log(chalk_1.default.cyan('Registered Types:'));
88
181
  console.log('');
89
182
  for (const schema of result.schemas) {
90
- console.log(` ${chalk_1.default.green(schema.type)}`);
91
- console.log(` ${schema.displayName} - ${schema.description}`);
92
- console.log(` ProductType: ${schema.productType}`);
93
- console.log(` Capabilities: ${schema.capabilities.join(', ')}`);
183
+ const stateIcon = schema.state === 'production' ? chalk_1.default.green('P') : chalk_1.default.yellow('D');
184
+ console.log(` [${stateIcon}] ${chalk_1.default.green(schema.type)}`);
185
+ console.log(` ${schema.displayName} - ${schema.description}`);
186
+ console.log(` ProductType: ${schema.productType}`);
94
187
  console.log('');
95
188
  }
96
189
  console.log(chalk_1.default.gray(`Total: ${result.count} types`));
190
+ console.log(chalk_1.default.gray(`Legend: [${chalk_1.default.green('P')}]=Production [${chalk_1.default.yellow('D')}]=Development`));
97
191
  console.log('');
98
192
  }
99
193
  catch (error) {
@@ -113,11 +207,14 @@ exports.schemaCommand
113
207
  .command('get')
114
208
  .description('Get detailed schema for a specific Type')
115
209
  .requiredOption('-t, --type <type>', 'Type name to retrieve')
210
+ .option('-e, --endpoint <env>', 'Environment (staging|production)', 'staging')
116
211
  .option('--json', 'Output as JSON')
117
212
  .action(async (options) => {
118
- const spinner = (0, ora_1.default)(`Fetching schema for ${options.type}...`).start();
213
+ const env = (0, config_1.resolveEnvironment)(options.endpoint);
214
+ const apiBase = getSchemaApiBase(env);
215
+ const spinner = (0, ora_1.default)(`Fetching schema for ${options.type} from ${env}...`).start();
119
216
  try {
120
- const result = await fetchSchema(options.type);
217
+ const result = await fetchSchema(apiBase, options.type);
121
218
  spinner.stop();
122
219
  if (!result.ok) {
123
220
  console.error(chalk_1.default.red(`\nSchema "${options.type}" not found`));
@@ -136,12 +233,19 @@ exports.schemaCommand
136
233
  console.log(JSON.stringify(schema, null, 2));
137
234
  return;
138
235
  }
139
- console.log(chalk_1.default.bold(`\n=== ${schema.type} Schema ===\n`));
236
+ console.log(chalk_1.default.bold(`\n=== ${schema.type} Schema (${env}) ===\n`));
140
237
  console.log(`DisplayName: ${chalk_1.default.cyan(schema.displayName)}`);
141
238
  console.log(`Description: ${schema.description}`);
142
239
  console.log(`ProductType: ${schema.productType}`);
143
240
  console.log(`Capabilities: ${schema.capabilities.join(', ')}`);
144
241
  console.log(`Version: ${schema.version}`);
242
+ if (schema.state) {
243
+ const stateColor = schema.state === 'production' ? chalk_1.default.green : chalk_1.default.yellow;
244
+ console.log(`State: ${stateColor(schema.state)}`);
245
+ }
246
+ if (schema.revisionNumber) {
247
+ console.log(`Revision: ${schema.revisionNumber}`);
248
+ }
145
249
  console.log('');
146
250
  // Features
147
251
  if (schema.features && schema.features.length > 0) {
@@ -186,14 +290,6 @@ exports.schemaCommand
186
290
  if (desc) {
187
291
  console.log(` ${desc}`);
188
292
  }
189
- // Show properties if any
190
- if (def.properties) {
191
- Object.entries(def.properties).forEach(([propName, propDef]) => {
192
- const propType = propDef.type || 'any';
193
- const propDesc = propDef.description || '';
194
- console.log(` ${chalk_1.default.cyan(propName)}: ${chalk_1.default.yellow(propType)} ${propDesc}`);
195
- });
196
- }
197
293
  });
198
294
  }
199
295
  console.log('');
@@ -201,7 +297,6 @@ exports.schemaCommand
201
297
  // Sample State Report
202
298
  console.log(chalk_1.default.bold('Sample State Report Request:'));
203
299
  const sampleState = {};
204
- // Generate sample values from state schema
205
300
  Object.entries(stateProps).forEach(([name, def]) => {
206
301
  const type = def.type;
207
302
  if (type === 'boolean') {
@@ -246,20 +341,6 @@ exports.schemaCommand
246
341
  spinner.fail('Failed to fetch schema');
247
342
  if (error.response?.status === 404) {
248
343
  console.error(chalk_1.default.red(`\nSchema "${options.type}" not found`));
249
- // Try to show available types
250
- try {
251
- const listResult = await fetchSchemaList();
252
- if (listResult.ok && listResult.count > 0) {
253
- console.log('');
254
- console.log(chalk_1.default.yellow('Available Types:'));
255
- listResult.schemas.forEach((s) => {
256
- console.log(` - ${s.type}`);
257
- });
258
- }
259
- }
260
- catch {
261
- // Ignore
262
- }
263
344
  }
264
345
  else {
265
346
  console.error(chalk_1.default.red(`\nError: ${error.message}`));
@@ -268,19 +349,20 @@ exports.schemaCommand
268
349
  process.exit(1);
269
350
  }
270
351
  });
271
- // schema validate
352
+ // schema validate (State Report validation)
272
353
  exports.schemaCommand
273
354
  .command('validate')
274
355
  .description('Validate a state report JSON against schema')
275
356
  .requiredOption('-t, --type <type>', 'Type name')
276
357
  .requiredOption('-f, --file <path>', 'JSON file path')
358
+ .option('-e, --endpoint <env>', 'Environment (staging|production)', 'staging')
277
359
  .action(async (options) => {
360
+ const env = (0, config_1.resolveEnvironment)(options.endpoint);
361
+ const apiBase = getSchemaApiBase(env);
278
362
  const spinner = (0, ora_1.default)('Validating...').start();
279
363
  try {
280
- const fs = require('fs');
281
- const path = require('path');
282
364
  // Fetch schema from API
283
- const schemaResult = await fetchSchema(options.type);
365
+ const schemaResult = await fetchSchema(apiBase, options.type);
284
366
  if (!schemaResult.ok) {
285
367
  spinner.fail(`Schema "${options.type}" not found`);
286
368
  process.exit(1);
@@ -320,7 +402,6 @@ exports.schemaCommand
320
402
  issues.push(`"${fieldName}": expected number but got ${actualType}`);
321
403
  }
322
404
  else {
323
- // Check range
324
405
  if (fieldDef.minimum !== undefined && value < fieldDef.minimum) {
325
406
  issues.push(`"${fieldName}": value ${value} is below minimum ${fieldDef.minimum}`);
326
407
  }
@@ -387,55 +468,6 @@ exports.schemaCommand
387
468
  process.exit(1);
388
469
  }
389
470
  });
390
- // Helper: Get auth token from options or environment
391
- function getAuthToken(options) {
392
- return options.token || process.env.ARANEA_AUTH_TOKEN || process.env.FIREBASE_AUTH_TOKEN || null;
393
- }
394
- // Helper: Validate schema file structure
395
- function validateSchemaStructure(schema) {
396
- const errors = [];
397
- // Required top-level fields
398
- const requiredFields = ['type', 'stateSchema'];
399
- for (const field of requiredFields) {
400
- if (!schema[field]) {
401
- errors.push(`Missing required field: ${field}`);
402
- }
403
- }
404
- // Type validation
405
- if (schema.type && typeof schema.type !== 'string') {
406
- errors.push('Field "type" must be a string');
407
- }
408
- // ProductType validation
409
- if (schema.productType && !/^\d{3}$/.test(schema.productType)) {
410
- errors.push('Field "productType" must be a 3-digit string (e.g., "001")');
411
- }
412
- // stateSchema validation
413
- if (schema.stateSchema) {
414
- if (typeof schema.stateSchema !== 'object') {
415
- errors.push('Field "stateSchema" must be an object');
416
- }
417
- else if (schema.stateSchema.type !== 'object') {
418
- errors.push('Field "stateSchema.type" must be "object"');
419
- }
420
- }
421
- // configSchema validation (optional but if present, must be object)
422
- if (schema.configSchema && typeof schema.configSchema !== 'object') {
423
- errors.push('Field "configSchema" must be an object');
424
- }
425
- // commandSchema validation (optional but if present, must be object)
426
- if (schema.commandSchema && typeof schema.commandSchema !== 'object') {
427
- errors.push('Field "commandSchema" must be an object');
428
- }
429
- // capabilities validation (optional but if present, must be array)
430
- if (schema.capabilities && !Array.isArray(schema.capabilities)) {
431
- errors.push('Field "capabilities" must be an array');
432
- }
433
- // features validation (optional but if present, must be array)
434
- if (schema.features && !Array.isArray(schema.features)) {
435
- errors.push('Field "features" must be an array');
436
- }
437
- return { valid: errors.length === 0, errors };
438
- }
439
471
  // schema validate-schema - validate a schema definition file
440
472
  exports.schemaCommand
441
473
  .command('validate-schema')
@@ -458,14 +490,14 @@ exports.schemaCommand
458
490
  spinner.fail(`Invalid JSON: ${parseError.message}`);
459
491
  process.exit(1);
460
492
  }
461
- const { valid, errors } = validateSchemaStructure(schema);
493
+ const { valid, errors, warnings } = validateSchemaStructure(schema);
462
494
  spinner.stop();
463
495
  console.log(chalk_1.default.bold('\n=== Schema File Validation ===\n'));
464
496
  console.log(`File: ${filePath}`);
465
497
  console.log(`Type: ${schema.type || '(not specified)'}`);
466
498
  console.log('');
467
499
  if (valid) {
468
- console.log(chalk_1.default.green('Schema structure is valid'));
500
+ console.log(chalk_1.default.green('Schema structure is valid'));
469
501
  console.log('');
470
502
  // Show summary
471
503
  console.log(chalk_1.default.bold('Schema Summary:'));
@@ -478,17 +510,27 @@ exports.schemaCommand
478
510
  console.log(` Config Fields: ${configFields.length} (${configFields.join(', ') || 'none'})`);
479
511
  const cmdFields = Object.keys(schema.commandSchema?.properties || {});
480
512
  console.log(` Command Fields: ${cmdFields.length} (${cmdFields.join(', ') || 'none'})`);
481
- console.log('');
482
- console.log(chalk_1.default.cyan('Ready to push with: aranea-sdk schema push --file ' + options.file));
483
513
  }
484
514
  else {
485
- console.log(chalk_1.default.red('Schema validation failed'));
515
+ console.log(chalk_1.default.red('Schema validation failed'));
486
516
  console.log('');
487
517
  console.log(chalk_1.default.red('Errors:'));
488
518
  errors.forEach((err) => {
489
519
  console.log(chalk_1.default.red(` - ${err}`));
490
520
  });
491
521
  }
522
+ if (warnings.length > 0) {
523
+ console.log('');
524
+ console.log(chalk_1.default.yellow('Warnings:'));
525
+ warnings.forEach((warn) => {
526
+ console.log(chalk_1.default.yellow(` - ${warn}`));
527
+ });
528
+ }
529
+ console.log('');
530
+ if (valid) {
531
+ console.log(chalk_1.default.cyan('Ready to push with: aranea-sdk schema push --file ' + options.file));
532
+ console.log(chalk_1.default.gray('Note: Use --endpoint production for production environment'));
533
+ }
492
534
  console.log('');
493
535
  if (!valid) {
494
536
  process.exit(1);
@@ -505,8 +547,13 @@ exports.schemaCommand
505
547
  .description('Push a schema to Firestore (development state)')
506
548
  .requiredOption('-f, --file <path>', 'Schema JSON file path')
507
549
  .option('-t, --token <token>', 'Firebase Auth ID token')
550
+ .option('-e, --endpoint <env>', 'Environment (staging|production)', 'staging')
508
551
  .option('-d, --dry-run', 'Show what would be pushed without actually pushing')
509
552
  .action(async (options) => {
553
+ const env = (0, config_1.resolveEnvironment)(options.endpoint);
554
+ const apiBase = getSchemaApiBase(env);
555
+ // Warn if production
556
+ (0, config_1.warnIfProduction)(env, 'schema push');
510
557
  const spinner = (0, ora_1.default)('Preparing schema push...').start();
511
558
  try {
512
559
  const filePath = path.resolve(options.file);
@@ -524,13 +571,20 @@ exports.schemaCommand
524
571
  process.exit(1);
525
572
  }
526
573
  // Validate schema structure
527
- const { valid, errors } = validateSchemaStructure(schema);
574
+ const { valid, errors, warnings } = validateSchemaStructure(schema);
528
575
  if (!valid) {
529
576
  spinner.fail('Schema validation failed');
530
577
  console.log('');
531
578
  errors.forEach((err) => console.log(chalk_1.default.red(` - ${err}`)));
532
579
  process.exit(1);
533
580
  }
581
+ // Show warnings
582
+ if (warnings.length > 0 && !options.dryRun) {
583
+ spinner.stop();
584
+ console.log(chalk_1.default.yellow('\nWarnings:'));
585
+ warnings.forEach((warn) => console.log(chalk_1.default.yellow(` - ${warn}`)));
586
+ spinner.start();
587
+ }
534
588
  const token = getAuthToken(options);
535
589
  if (!token && !options.dryRun) {
536
590
  spinner.fail('Authentication required');
@@ -540,12 +594,12 @@ exports.schemaCommand
540
594
  console.log(' export ARANEA_AUTH_TOKEN=<TOKEN>');
541
595
  console.log(' export FIREBASE_AUTH_TOKEN=<TOKEN>');
542
596
  console.log('');
597
+ console.log(chalk_1.default.gray('Token valid for 1 hour. Refresh manually if expired.'));
543
598
  console.log(chalk_1.default.gray('Or use --dry-run to see what would be pushed'));
544
599
  process.exit(1);
545
600
  }
546
- spinner.text = `Pushing schema: ${schema.type}...`;
547
- // Prepare the document data
548
- const now = new Date().toISOString();
601
+ spinner.text = `Pushing schema: ${schema.type} to ${env}...`;
602
+ // Prepare the document data (NO revisionNumber - server will handle it)
549
603
  const docData = {
550
604
  type: schema.type,
551
605
  displayName: schema.displayName || schema.type,
@@ -559,18 +613,16 @@ exports.schemaCommand
559
613
  configSchema: schema.configSchema || {},
560
614
  commandSchema: schema.commandSchema,
561
615
  version: schema.version || 1,
562
- state: 'development', // Always push to development
563
- revisionNumber: 1, // Will be incremented if already exists
564
- updatedAt: now,
565
- updatedBy: 'cli',
616
+ // Note: state and revisionNumber are server-controlled
566
617
  };
567
618
  if (options.dryRun) {
568
619
  spinner.stop();
569
- console.log(chalk_1.default.bold('\n=== Dry Run: Schema Push ===\n'));
620
+ console.log(chalk_1.default.bold(`\n=== Dry Run: Schema Push (${env}) ===\n`));
570
621
  console.log(chalk_1.default.yellow('Would push the following schema:'));
571
622
  console.log('');
572
623
  console.log(` Target: araneaSDK/typeSettings/schemas/${schema.type}`);
573
- console.log(` State: ${chalk_1.default.cyan('development')}`);
624
+ console.log(` Environment: ${env}`);
625
+ console.log(` State: ${chalk_1.default.cyan('development')} (server-assigned)`);
574
626
  console.log('');
575
627
  console.log('Document Data:');
576
628
  console.log(chalk_1.default.gray(JSON.stringify(docData, null, 2)));
@@ -579,7 +631,7 @@ exports.schemaCommand
579
631
  return;
580
632
  }
581
633
  // Call the API to push
582
- const response = await axios_1.default.post(`${SCHEMA_API_BASE}`, {
634
+ const response = await axios_1.default.post(`${apiBase}`, {
583
635
  action: 'push',
584
636
  schema: docData,
585
637
  }, {
@@ -589,12 +641,12 @@ exports.schemaCommand
589
641
  },
590
642
  });
591
643
  if (response.data.ok) {
592
- spinner.succeed(`Schema "${schema.type}" pushed to development`);
644
+ spinner.succeed(`Schema "${schema.type}" pushed to ${env} (development)`);
593
645
  console.log('');
594
646
  console.log(` Path: ${response.data.path || `araneaSDK/typeSettings/schemas/${schema.type}`}`);
595
- console.log(` Revision: ${response.data.revisionNumber || 1}`);
647
+ console.log(` Revision: ${response.data.revisionNumber || '(new)'}`);
596
648
  console.log('');
597
- console.log(chalk_1.default.cyan(`To promote to production: aranea-sdk schema promote --type ${schema.type}`));
649
+ console.log(chalk_1.default.cyan(`To promote to production: aranea-sdk schema promote --type ${schema.type} --endpoint ${env}`));
598
650
  }
599
651
  else {
600
652
  spinner.fail(`Push failed: ${response.data.error || 'Unknown error'}`);
@@ -604,6 +656,7 @@ exports.schemaCommand
604
656
  catch (error) {
605
657
  if (error.response?.status === 401) {
606
658
  spinner.fail('Authentication failed - invalid or expired token');
659
+ console.log(chalk_1.default.yellow('\nToken is valid for 1 hour. Please refresh your token.'));
607
660
  }
608
661
  else if (error.response?.status === 403) {
609
662
  spinner.fail('Permission denied - insufficient privileges');
@@ -623,42 +676,67 @@ exports.schemaCommand
623
676
  .description('Promote a schema from development to production')
624
677
  .requiredOption('-t, --type <type>', 'Type name to promote')
625
678
  .option('--token <token>', 'Firebase Auth ID token')
679
+ .option('-e, --endpoint <env>', 'Environment (staging|production)', 'staging')
626
680
  .option('-d, --dry-run', 'Show what would be promoted without actually promoting')
681
+ .option('-y, --confirm', 'Skip confirmation prompt')
627
682
  .action(async (options) => {
628
- const spinner = (0, ora_1.default)(`Promoting schema: ${options.type}...`).start();
683
+ const env = (0, config_1.resolveEnvironment)(options.endpoint);
684
+ const apiBase = getSchemaApiBase(env);
685
+ // Always warn for promote
686
+ console.log(chalk_1.default.yellow(`\nPromoting schema to PRODUCTION state in ${env} environment\n`));
687
+ const spinner = (0, ora_1.default)(`Fetching schema info: ${options.type}...`).start();
629
688
  try {
630
- const token = getAuthToken(options);
631
- if (!token && !options.dryRun) {
632
- spinner.fail('Authentication required');
633
- console.log('');
634
- console.log(chalk_1.default.yellow('Provide a token using one of:'));
635
- console.log(' --token <TOKEN>');
636
- console.log(' export ARANEA_AUTH_TOKEN=<TOKEN>');
637
- console.log(' export FIREBASE_AUTH_TOKEN=<TOKEN>');
689
+ // First, fetch current schema info
690
+ const schemaResult = await fetchSchema(apiBase, options.type);
691
+ if (!schemaResult.ok) {
692
+ spinner.fail(`Schema "${options.type}" not found`);
638
693
  process.exit(1);
639
694
  }
695
+ const schema = schemaResult.schema;
696
+ spinner.stop();
697
+ // Show current state
698
+ console.log(chalk_1.default.bold('Current Schema State:'));
699
+ console.log(` Type: ${schema.type}`);
700
+ console.log(` DisplayName: ${schema.displayName}`);
701
+ console.log(` Current State: ${schema.state === 'production' ? chalk_1.default.green(schema.state) : chalk_1.default.yellow(schema.state || 'development')}`);
702
+ console.log(` Revision: ${schema.revisionNumber || schema.version}`);
703
+ console.log(` Environment: ${env}`);
704
+ console.log('');
705
+ if (schema.state === 'production') {
706
+ console.log(chalk_1.default.yellow('Schema is already in production state.'));
707
+ return;
708
+ }
640
709
  if (options.dryRun) {
641
- // Fetch current schema info
642
- const schemaResult = await fetchSchema(options.type);
643
- spinner.stop();
644
- console.log(chalk_1.default.bold('\n=== Dry Run: Schema Promote ===\n'));
645
- if (!schemaResult.ok) {
646
- console.log(chalk_1.default.red(`Schema "${options.type}" not found`));
647
- process.exit(1);
648
- }
649
- const schema = schemaResult.schema;
650
- console.log(chalk_1.default.yellow('Would promote the following schema:'));
651
- console.log('');
652
- console.log(` Type: ${schema.type}`);
653
- console.log(` Current State: ${schema.state || 'unknown'}`);
654
- console.log(` New State: ${chalk_1.default.green('production')}`);
655
- console.log(` Revision: ${schema.revisionNumber || schema.version}`);
710
+ console.log(chalk_1.default.bold('=== Dry Run: Would Promote ===\n'));
711
+ console.log(` From: ${chalk_1.default.yellow('development')}`);
712
+ console.log(` To: ${chalk_1.default.green('production')}`);
656
713
  console.log('');
657
714
  console.log(chalk_1.default.gray('Remove --dry-run to actually promote'));
658
715
  return;
659
716
  }
717
+ // Confirmation step
718
+ if (!options.confirm) {
719
+ console.log(chalk_1.default.red('This will promote the schema to PRODUCTION state.'));
720
+ console.log(chalk_1.default.red('Existing devices using this schema type will be affected.'));
721
+ console.log('');
722
+ const confirmed = await promptConfirm('Are you sure you want to proceed?');
723
+ if (!confirmed) {
724
+ console.log(chalk_1.default.yellow('\nPromotion cancelled.'));
725
+ return;
726
+ }
727
+ }
728
+ const token = getAuthToken(options);
729
+ if (!token) {
730
+ console.log(chalk_1.default.red('\nAuthentication required'));
731
+ console.log(chalk_1.default.yellow('Provide a token using one of:'));
732
+ console.log(' --token <TOKEN>');
733
+ console.log(' export ARANEA_AUTH_TOKEN=<TOKEN>');
734
+ console.log(' export FIREBASE_AUTH_TOKEN=<TOKEN>');
735
+ process.exit(1);
736
+ }
737
+ const promoteSpinner = (0, ora_1.default)(`Promoting schema: ${options.type}...`).start();
660
738
  // Call the API to promote
661
- const response = await axios_1.default.post(`${SCHEMA_API_BASE}`, {
739
+ const response = await axios_1.default.post(`${apiBase}`, {
662
740
  action: 'promote',
663
741
  type: options.type,
664
742
  }, {
@@ -668,7 +746,7 @@ exports.schemaCommand
668
746
  },
669
747
  });
670
748
  if (response.data.ok) {
671
- spinner.succeed(`Schema "${options.type}" promoted to production`);
749
+ promoteSpinner.succeed(`Schema "${options.type}" promoted to production`);
672
750
  console.log('');
673
751
  console.log(` Previous State: ${response.data.previousState || 'development'}`);
674
752
  console.log(` New State: ${chalk_1.default.green('production')}`);
@@ -676,25 +754,26 @@ exports.schemaCommand
676
754
  console.log('');
677
755
  }
678
756
  else {
679
- spinner.fail(`Promote failed: ${response.data.error || 'Unknown error'}`);
757
+ promoteSpinner.fail(`Promote failed: ${response.data.error || 'Unknown error'}`);
680
758
  process.exit(1);
681
759
  }
682
760
  }
683
761
  catch (error) {
684
762
  if (error.response?.status === 401) {
685
- spinner.fail('Authentication failed - invalid or expired token');
763
+ console.log(chalk_1.default.red('\nAuthentication failed - invalid or expired token'));
764
+ console.log(chalk_1.default.yellow('Token is valid for 1 hour. Please refresh your token.'));
686
765
  }
687
766
  else if (error.response?.status === 403) {
688
- spinner.fail('Permission denied - insufficient privileges');
767
+ console.log(chalk_1.default.red('\nPermission denied - insufficient privileges'));
689
768
  }
690
769
  else if (error.response?.status === 404) {
691
- spinner.fail(`Schema "${options.type}" not found`);
770
+ console.log(chalk_1.default.red(`\nSchema "${options.type}" not found`));
692
771
  }
693
772
  else if (error.response?.data?.error) {
694
- spinner.fail(`Promote failed: ${error.response.data.error}`);
773
+ console.log(chalk_1.default.red(`\nPromote failed: ${error.response.data.error}`));
695
774
  }
696
775
  else {
697
- spinner.fail(`Promote failed: ${error.message}`);
776
+ console.log(chalk_1.default.red(`\nPromote failed: ${error.message}`));
698
777
  }
699
778
  process.exit(1);
700
779
  }
@@ -704,12 +783,15 @@ exports.schemaCommand
704
783
  .command('info')
705
784
  .description('Show detailed schema information with revision history')
706
785
  .requiredOption('-t, --type <type>', 'Type name')
786
+ .option('-e, --endpoint <env>', 'Environment (staging|production)', 'staging')
707
787
  .option('--json', 'Output as JSON')
708
788
  .action(async (options) => {
709
- const spinner = (0, ora_1.default)(`Fetching schema info: ${options.type}...`).start();
789
+ const env = (0, config_1.resolveEnvironment)(options.endpoint);
790
+ const apiBase = getSchemaApiBase(env);
791
+ const spinner = (0, ora_1.default)(`Fetching schema info: ${options.type} from ${env}...`).start();
710
792
  try {
711
793
  // Fetch schema from API with extended info
712
- const response = await axios_1.default.get(`${SCHEMA_API_BASE}`, {
794
+ const response = await axios_1.default.get(`${apiBase}`, {
713
795
  params: {
714
796
  action: 'info',
715
797
  type: options.type,
@@ -728,10 +810,10 @@ exports.schemaCommand
728
810
  const schema = response.data.schema;
729
811
  const revisions = response.data.revisions || [];
730
812
  if (options.json) {
731
- console.log(JSON.stringify({ schema, revisions }, null, 2));
813
+ console.log(JSON.stringify({ schema, revisions, environment: env }, null, 2));
732
814
  return;
733
815
  }
734
- console.log(chalk_1.default.bold(`\n=== Schema Info: ${schema.type} ===\n`));
816
+ console.log(chalk_1.default.bold(`\n=== Schema Info: ${schema.type} (${env}) ===\n`));
735
817
  // Basic info
736
818
  console.log(chalk_1.default.cyan('Basic Information:'));
737
819
  console.log(` Type: ${schema.type}`);
@@ -778,9 +860,9 @@ exports.schemaCommand
778
860
  // Actions
779
861
  console.log(chalk_1.default.gray('Actions:'));
780
862
  if (schema.state !== 'production') {
781
- console.log(chalk_1.default.gray(` Promote: aranea-sdk schema promote --type ${schema.type}`));
863
+ console.log(chalk_1.default.gray(` Promote: aranea-sdk schema promote --type ${schema.type} --endpoint ${env}`));
782
864
  }
783
- console.log(chalk_1.default.gray(` Get full schema: aranea-sdk schema get --type ${schema.type} --json`));
865
+ console.log(chalk_1.default.gray(` Get full schema: aranea-sdk schema get --type ${schema.type} --endpoint ${env} --json`));
784
866
  console.log('');
785
867
  }
786
868
  catch (error) {
package/dist/config.d.ts CHANGED
@@ -1,17 +1,52 @@
1
1
  /**
2
2
  * AraneaSDK CLI Configuration
3
+ *
4
+ * Environment switching:
5
+ * - ARANEA_ENV environment variable (production | staging)
6
+ * - --endpoint option on commands
7
+ * - Default: staging (safety first)
3
8
  */
9
+ export type Environment = 'production' | 'staging';
4
10
  export interface Endpoint {
5
11
  gate: string;
6
12
  state: string;
7
13
  command: string;
8
14
  mqtt?: string;
15
+ schemaAPI: string;
16
+ knowledgeAPI: string;
17
+ metatronAPI: string;
18
+ webhookBase: string;
9
19
  }
10
- export declare const ENDPOINTS: Record<string, Endpoint>;
20
+ export declare const ENDPOINTS: Record<Environment, Endpoint>;
11
21
  export declare const TEST_TENANT: {
12
22
  tid: string;
13
23
  primaryLacisId: string;
14
24
  primaryEmail: string;
15
25
  primaryCic: string;
16
26
  };
27
+ /**
28
+ * Get current environment from ARANEA_ENV or default to staging
29
+ */
30
+ export declare function getCurrentEnvironment(): Environment;
31
+ /**
32
+ * Resolve environment from option or environment variable
33
+ * @param optionValue - Value from --endpoint option
34
+ * @returns Resolved environment
35
+ */
36
+ export declare function resolveEnvironment(optionValue?: string): Environment;
37
+ /**
38
+ * Get endpoint configuration for environment
39
+ */
17
40
  export declare function getEndpoint(env: string): Endpoint;
41
+ /**
42
+ * CelestialGlobe webhook configuration
43
+ */
44
+ export interface WebhookConfig {
45
+ ingestUrl: string;
46
+ stateReportUrl: string;
47
+ }
48
+ export declare function getWebhookConfig(env: Environment, fid: string): WebhookConfig;
49
+ /**
50
+ * Display environment warning for production operations
51
+ */
52
+ export declare function warnIfProduction(env: Environment, operation: string): void;
package/dist/config.js CHANGED
@@ -1,22 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
3
  * AraneaSDK CLI Configuration
4
+ *
5
+ * Environment switching:
6
+ * - ARANEA_ENV environment variable (production | staging)
7
+ * - --endpoint option on commands
8
+ * - Default: staging (safety first)
4
9
  */
5
10
  Object.defineProperty(exports, "__esModule", { value: true });
6
11
  exports.TEST_TENANT = exports.ENDPOINTS = void 0;
12
+ exports.getCurrentEnvironment = getCurrentEnvironment;
13
+ exports.resolveEnvironment = resolveEnvironment;
7
14
  exports.getEndpoint = getEndpoint;
15
+ exports.getWebhookConfig = getWebhookConfig;
16
+ exports.warnIfProduction = warnIfProduction;
8
17
  exports.ENDPOINTS = {
9
18
  production: {
10
19
  gate: 'https://asia-northeast1-mobesorder.cloudfunctions.net/araneaDeviceGate',
11
20
  state: 'https://asia-northeast1-mobesorder.cloudfunctions.net/deviceStateReport',
12
21
  command: 'https://asia-northeast1-mobesorder.cloudfunctions.net/araneaDeviceCommand',
13
22
  mqtt: 'wss://aranea-mqtt-bridge-1010551946141.asia-northeast1.run.app',
23
+ schemaAPI: 'https://asia-northeast1-mobesorder.cloudfunctions.net/araneaSchemaAPI',
24
+ knowledgeAPI: 'https://asia-northeast1-mobesorder.cloudfunctions.net/araneaKnowledgeManagement',
25
+ metatronAPI: 'https://asia-northeast1-mobesorder.cloudfunctions.net/araneaMetatronQuery',
26
+ webhookBase: 'https://us-central1-mobesorder.cloudfunctions.net',
14
27
  },
15
28
  staging: {
16
29
  gate: 'https://asia-northeast1-mobesorder-staging.cloudfunctions.net/araneaDeviceGate',
17
30
  state: 'https://asia-northeast1-mobesorder-staging.cloudfunctions.net/deviceStateReport',
18
31
  command: 'https://asia-northeast1-mobesorder-staging.cloudfunctions.net/araneaDeviceCommand',
19
32
  mqtt: 'wss://aranea-mqtt-bridge-staging.asia-northeast1.run.app',
33
+ schemaAPI: 'https://asia-northeast1-mobesorder-staging.cloudfunctions.net/araneaSchemaAPI',
34
+ knowledgeAPI: 'https://asia-northeast1-mobesorder-staging.cloudfunctions.net/araneaKnowledgeManagement',
35
+ metatronAPI: 'https://asia-northeast1-mobesorder-staging.cloudfunctions.net/araneaMetatronQuery',
36
+ webhookBase: 'https://us-central1-mobesorder-staging.cloudfunctions.net',
20
37
  },
21
38
  };
22
39
  exports.TEST_TENANT = {
@@ -25,10 +42,60 @@ exports.TEST_TENANT = {
25
42
  primaryEmail: 'dev@araneadevice.dev',
26
43
  primaryCic: '022029',
27
44
  };
45
+ /**
46
+ * Get current environment from ARANEA_ENV or default to staging
47
+ */
48
+ function getCurrentEnvironment() {
49
+ const env = process.env.ARANEA_ENV?.toLowerCase();
50
+ if (env === 'production' || env === 'prod') {
51
+ return 'production';
52
+ }
53
+ // Default to staging for safety
54
+ return 'staging';
55
+ }
56
+ /**
57
+ * Resolve environment from option or environment variable
58
+ * @param optionValue - Value from --endpoint option
59
+ * @returns Resolved environment
60
+ */
61
+ function resolveEnvironment(optionValue) {
62
+ if (optionValue) {
63
+ const normalized = optionValue.toLowerCase();
64
+ if (normalized === 'production' || normalized === 'prod') {
65
+ return 'production';
66
+ }
67
+ if (normalized === 'staging' || normalized === 'stg') {
68
+ return 'staging';
69
+ }
70
+ throw new Error(`Unknown environment: ${optionValue}. Use 'production' or 'staging'.`);
71
+ }
72
+ return getCurrentEnvironment();
73
+ }
74
+ /**
75
+ * Get endpoint configuration for environment
76
+ */
28
77
  function getEndpoint(env) {
78
+ const normalized = env.toLowerCase();
79
+ if (normalized === 'production' || normalized === 'prod') {
80
+ return exports.ENDPOINTS.production;
81
+ }
82
+ if (normalized === 'staging' || normalized === 'stg') {
83
+ return exports.ENDPOINTS.staging;
84
+ }
85
+ throw new Error(`Unknown environment: ${env}. Use 'production' or 'staging'.`);
86
+ }
87
+ function getWebhookConfig(env, fid) {
29
88
  const endpoint = exports.ENDPOINTS[env];
30
- if (!endpoint) {
31
- throw new Error(`Unknown environment: ${env}. Use 'production' or 'staging'.`);
89
+ return {
90
+ ingestUrl: `${endpoint.webhookBase}/celestialGlobe_ingest?fid=${fid}&source=omada`,
91
+ stateReportUrl: `${endpoint.webhookBase}/deviceStateReport`,
92
+ };
93
+ }
94
+ /**
95
+ * Display environment warning for production operations
96
+ */
97
+ function warnIfProduction(env, operation) {
98
+ if (env === 'production') {
99
+ console.warn(`\x1b[33m⚠️ WARNING: ${operation} targeting PRODUCTION environment\x1b[0m`);
32
100
  }
33
- return endpoint;
34
101
  }
package/dist/index.js CHANGED
@@ -23,11 +23,12 @@ const register_1 = require("./commands/register");
23
23
  const mqtt_1 = require("./commands/mqtt");
24
24
  const knowledge_1 = require("./commands/knowledge");
25
25
  const metatron_1 = require("./commands/metatron");
26
+ const auth_1 = require("./commands/auth");
26
27
  const program = new commander_1.Command();
27
28
  program
28
29
  .name('aranea-sdk')
29
30
  .description('AraneaSDK CLI - デバイス開発支援ツール')
30
- .version('0.3.0');
31
+ .version('0.3.1');
31
32
  // test コマンド
32
33
  program.addCommand(test_1.testCommand);
33
34
  // simulate コマンド
@@ -44,4 +45,6 @@ program.addCommand(mqtt_1.mqttCommand);
44
45
  program.addCommand(knowledge_1.knowledgeCommand);
45
46
  // metatron コマンド (v0.2.1)
46
47
  program.addCommand(metatron_1.metatronCommand);
48
+ // auth コマンド (v0.3.1)
49
+ program.addCommand(auth_1.authCommand);
47
50
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aranea-sdk-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "AraneaSDK CLI - ESP32 IoTデバイス開発支援ツール",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",