aranea-sdk-cli 0.3.17 → 0.5.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,700 @@
1
+ "use strict";
2
+ /**
3
+ * control command
4
+ *
5
+ * Control型(UIコンポーネント定義)の管理コマンド
6
+ *
7
+ * 安全設計:
8
+ * - list, show, sync: 許可
9
+ * - add: 類似チェック付きで許可
10
+ * - modify, delete: 禁止(Admin UIのみ)
11
+ *
12
+ * @see doc/APPS/araneaSDK/headDesign/59_METATRON_CONTROL_SUGGESTION.md Section 8
13
+ *
14
+ * Usage:
15
+ * aranea-sdk control list [--category <category>] [--type <dataType>]
16
+ * aranea-sdk control show <controlId>
17
+ * aranea-sdk control sync [--dry-run]
18
+ * aranea-sdk control add --id <id> --name <name> --category <category> [--dry-run]
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ var __importDefault = (this && this.__importDefault) || function (mod) {
54
+ return (mod && mod.__esModule) ? mod : { "default": mod };
55
+ };
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.controlCommand = void 0;
58
+ const commander_1 = require("commander");
59
+ const chalk_1 = __importDefault(require("chalk"));
60
+ const ora_1 = __importDefault(require("ora"));
61
+ const admin = __importStar(require("firebase-admin"));
62
+ const readline = __importStar(require("readline"));
63
+ const path = __importStar(require("path"));
64
+ const fs = __importStar(require("fs"));
65
+ // Firebase initialization helper
66
+ let firebaseInitialized = false;
67
+ function initializeFirebase() {
68
+ if (firebaseInitialized)
69
+ return;
70
+ // Try to find service account key in common locations
71
+ const keyPaths = [
72
+ process.env.GOOGLE_APPLICATION_CREDENTIALS,
73
+ path.resolve(__dirname, '../../../../KEY/mobesorder-firebase-adminsdk-fbsvc-8b62efc60c.json'),
74
+ path.resolve(process.cwd(), 'KEY/mobesorder-firebase-adminsdk-fbsvc-8b62efc60c.json'),
75
+ ].filter(Boolean);
76
+ let keyPath;
77
+ for (const p of keyPaths) {
78
+ if (fs.existsSync(p)) {
79
+ keyPath = p;
80
+ break;
81
+ }
82
+ }
83
+ if (!keyPath) {
84
+ throw new Error('Firebase service account key not found.\n' +
85
+ 'Set GOOGLE_APPLICATION_CREDENTIALS environment variable or place key in KEY/ directory.');
86
+ }
87
+ admin.initializeApp({
88
+ credential: admin.credential.cert(keyPath),
89
+ projectId: 'mobesorder',
90
+ });
91
+ firebaseInitialized = true;
92
+ }
93
+ function getFirestore() {
94
+ initializeFirebase();
95
+ return admin.firestore();
96
+ }
97
+ // Built-in control definitions (synced from ControlRegistry)
98
+ const BUILTIN_CONTROLS = [
99
+ // Boolean Controls
100
+ {
101
+ id: 'slide-switch',
102
+ displayName: 'スライドスイッチ',
103
+ description: 'ON/OFF切り替え用のスライドスイッチ',
104
+ category: 'Boolean',
105
+ subcategory: 'Switch',
106
+ dataTypes: ['boolean'],
107
+ protocols: ['mqtt', 'http', 'websocket'],
108
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
109
+ suggestionRules: {
110
+ fieldNamePatterns: ['^(power|switch|toggle|on_off|enable|active|isOn)$'],
111
+ fieldLabelPatterns: ['電源', 'スイッチ', 'ON/OFF', 'オン', 'オフ', '有効', '無効'],
112
+ useCaseKeywords: ['電源制御', 'スイッチ操作', 'トグル'],
113
+ priority: 90,
114
+ exclusiveDataTypes: ['boolean'],
115
+ },
116
+ defaultProps: { labelTrue: 'ON', labelFalse: 'OFF', size: 'md' },
117
+ tags: ['switch', 'toggle', 'on/off', 'スイッチ', '電源'],
118
+ iconName: 'ToggleOn',
119
+ enabled: true,
120
+ isSystem: true,
121
+ },
122
+ {
123
+ id: 'status-led',
124
+ displayName: 'ステータスLED',
125
+ description: '状態表示用のLEDインジケータ(読取専用)',
126
+ category: 'Boolean',
127
+ subcategory: 'Indicator',
128
+ dataTypes: ['boolean'],
129
+ protocols: ['mqtt', 'http', 'websocket'],
130
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
131
+ suggestionRules: {
132
+ fieldNamePatterns: ['^(status|state|is_.*|has_.*)$'],
133
+ fieldLabelPatterns: ['ステータス', '状態', 'インジケータ', 'ランプ'],
134
+ useCaseKeywords: ['状態表示', 'インジケータ', 'モニタリング'],
135
+ priority: 85,
136
+ exclusiveDataTypes: ['boolean'],
137
+ },
138
+ defaultProps: { operationMode: 'read', size: 'md' },
139
+ tags: ['led', 'indicator', 'status', 'ランプ', 'インジケータ'],
140
+ iconName: 'Circle',
141
+ enabled: true,
142
+ isSystem: true,
143
+ },
144
+ {
145
+ id: 'push-button',
146
+ displayName: '押しボタン',
147
+ description: 'アクション実行用の押しボタン',
148
+ category: 'Boolean',
149
+ subcategory: 'Button',
150
+ dataTypes: ['boolean'],
151
+ protocols: ['mqtt', 'http', 'websocket'],
152
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
153
+ suggestionRules: {
154
+ fieldNamePatterns: ['^(button|trigger|action|execute|run)$'],
155
+ fieldLabelPatterns: ['ボタン', 'トリガー', '実行', 'アクション'],
156
+ useCaseKeywords: ['ワンショット', 'トリガー', 'アクション実行'],
157
+ priority: 80,
158
+ exclusiveDataTypes: ['boolean'],
159
+ },
160
+ defaultProps: { size: 'md' },
161
+ tags: ['button', 'push', 'momentary', 'ボタン', 'プッシュ'],
162
+ iconName: 'TouchApp',
163
+ enabled: true,
164
+ isSystem: true,
165
+ },
166
+ // Numeric Controls
167
+ {
168
+ id: 'slider',
169
+ displayName: 'スライダー',
170
+ description: '範囲内の数値を選択するスライダー',
171
+ category: 'Numeric',
172
+ subcategory: 'Slider',
173
+ dataTypes: ['number'],
174
+ protocols: ['mqtt', 'http', 'websocket', 'modbus', 'opcua'],
175
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
176
+ suggestionRules: {
177
+ fieldNamePatterns: ['^(level|brightness|volume|speed|rate|percent|ratio)$'],
178
+ fieldLabelPatterns: ['レベル', '明るさ', '音量', '速度', '割合', 'パーセント'],
179
+ useCaseKeywords: ['調整', 'レベル制御', 'パーセンテージ'],
180
+ priority: 85,
181
+ exclusiveDataTypes: ['number'],
182
+ },
183
+ defaultProps: { min: 0, max: 100, step: 1, size: 'md' },
184
+ tags: ['slider', 'range', 'スライダー', '範囲', 'レベル'],
185
+ iconName: 'Tune',
186
+ enabled: true,
187
+ isSystem: true,
188
+ },
189
+ {
190
+ id: 'circular-gauge',
191
+ displayName: '円形ゲージ',
192
+ description: '円形のゲージで数値を表示(読取専用)',
193
+ category: 'Numeric',
194
+ subcategory: 'Gauge',
195
+ dataTypes: ['number'],
196
+ protocols: ['mqtt', 'http', 'websocket', 'modbus', 'opcua', 'bacnet'],
197
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
198
+ suggestionRules: {
199
+ fieldNamePatterns: ['^(temperature|humidity|pressure|voltage|current|power|energy)$'],
200
+ fieldLabelPatterns: ['温度', '湿度', '気圧', '電圧', '電流', '電力', 'エネルギー', 'センサー値'],
201
+ useCaseKeywords: ['センサー表示', 'メーター', 'ゲージ', '計測値'],
202
+ priority: 90,
203
+ exclusiveDataTypes: ['number'],
204
+ },
205
+ defaultProps: { operationMode: 'read', min: 0, max: 100, size: 'md' },
206
+ tags: ['gauge', 'meter', 'dial', 'ゲージ', 'メーター', '円形'],
207
+ iconName: 'Speed',
208
+ enabled: true,
209
+ isSystem: true,
210
+ },
211
+ // String Controls
212
+ {
213
+ id: 'text-input',
214
+ displayName: 'テキスト入力',
215
+ description: '単一行テキスト入力フィールド',
216
+ category: 'String',
217
+ subcategory: 'Input',
218
+ dataTypes: ['string'],
219
+ protocols: ['mqtt', 'http', 'websocket'],
220
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
221
+ suggestionRules: {
222
+ fieldNamePatterns: ['^(name|label|title|description|comment|note)$'],
223
+ fieldLabelPatterns: ['名前', 'ラベル', 'タイトル', '説明', 'コメント', 'メモ'],
224
+ useCaseKeywords: ['テキスト入力', '名前設定', 'ラベル設定'],
225
+ priority: 80,
226
+ exclusiveDataTypes: ['string'],
227
+ },
228
+ defaultProps: { size: 'md' },
229
+ tags: ['text', 'input', 'field', 'テキスト', '入力'],
230
+ iconName: 'TextFields',
231
+ enabled: true,
232
+ isSystem: true,
233
+ },
234
+ {
235
+ id: 'dropdown',
236
+ displayName: 'ドロップダウン',
237
+ description: '選択肢からひとつを選ぶドロップダウン',
238
+ category: 'String',
239
+ subcategory: 'Select',
240
+ dataTypes: ['string', 'enum'],
241
+ protocols: ['mqtt', 'http', 'websocket'],
242
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
243
+ suggestionRules: {
244
+ fieldNamePatterns: ['^(mode|type|category|option|select|choice)$'],
245
+ fieldLabelPatterns: ['モード', 'タイプ', 'カテゴリ', 'オプション', '選択', '種類'],
246
+ useCaseKeywords: ['選択', 'モード切替', 'オプション選択'],
247
+ priority: 85,
248
+ exclusiveDataTypes: ['string', 'enum'],
249
+ },
250
+ defaultProps: { size: 'md' },
251
+ tags: ['dropdown', 'select', 'choice', 'ドロップダウン', '選択'],
252
+ iconName: 'ArrowDropDown',
253
+ enabled: true,
254
+ isSystem: true,
255
+ },
256
+ {
257
+ id: 'label-display',
258
+ displayName: 'ラベル表示',
259
+ description: 'テキストを読取専用で表示',
260
+ category: 'String',
261
+ subcategory: 'Display',
262
+ dataTypes: ['string'],
263
+ protocols: ['mqtt', 'http', 'websocket'],
264
+ platforms: ['aranea', 'switchbot', 'tuya', 'ewelink'],
265
+ suggestionRules: {
266
+ fieldNamePatterns: ['^(message|info|text|display|output)$'],
267
+ fieldLabelPatterns: ['メッセージ', '情報', 'テキスト', '表示', '出力'],
268
+ useCaseKeywords: ['情報表示', 'ステータステキスト', 'メッセージ表示'],
269
+ priority: 75,
270
+ exclusiveDataTypes: ['string'],
271
+ },
272
+ defaultProps: { operationMode: 'read', size: 'md' },
273
+ tags: ['label', 'display', 'text', 'ラベル', '表示'],
274
+ iconName: 'Label',
275
+ enabled: true,
276
+ isSystem: true,
277
+ },
278
+ ];
279
+ // Helper: Prompt for confirmation
280
+ async function promptConfirm(message) {
281
+ const rl = readline.createInterface({
282
+ input: process.stdin,
283
+ output: process.stdout,
284
+ });
285
+ return new Promise((resolve) => {
286
+ rl.question(`${message} (y/N): `, (answer) => {
287
+ rl.close();
288
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
289
+ });
290
+ });
291
+ }
292
+ // Calculate Levenshtein distance
293
+ function levenshteinDistance(a, b) {
294
+ const matrix = [];
295
+ for (let i = 0; i <= a.length; i++) {
296
+ matrix[i] = [i];
297
+ }
298
+ for (let j = 0; j <= b.length; j++) {
299
+ matrix[0][j] = j;
300
+ }
301
+ for (let i = 1; i <= a.length; i++) {
302
+ for (let j = 1; j <= b.length; j++) {
303
+ if (a[i - 1] === b[j - 1]) {
304
+ matrix[i][j] = matrix[i - 1][j - 1];
305
+ }
306
+ else {
307
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
308
+ }
309
+ }
310
+ }
311
+ return matrix[a.length][b.length];
312
+ }
313
+ // Calculate string similarity (0-1)
314
+ function stringSimilarity(a, b) {
315
+ const maxLen = Math.max(a.length, b.length);
316
+ if (maxLen === 0)
317
+ return 1;
318
+ return 1 - levenshteinDistance(a.toLowerCase(), b.toLowerCase()) / maxLen;
319
+ }
320
+ // Calculate control similarity
321
+ function calculateControlSimilarity(newControl, existing) {
322
+ let score = 0;
323
+ let weight = 0;
324
+ // displayName similarity (30%)
325
+ if (newControl.displayName && existing.displayName) {
326
+ score += stringSimilarity(newControl.displayName, existing.displayName) * 0.3;
327
+ weight += 0.3;
328
+ }
329
+ // dataTypes overlap (30%)
330
+ if (newControl.dataTypes && existing.dataTypes) {
331
+ const overlap = newControl.dataTypes.filter(t => existing.dataTypes.includes(t)).length;
332
+ score += (overlap / Math.max(newControl.dataTypes.length, existing.dataTypes.length)) * 0.3;
333
+ weight += 0.3;
334
+ }
335
+ // category match (20%)
336
+ if (newControl.category && existing.category) {
337
+ score += (newControl.category === existing.category ? 1 : 0) * 0.2;
338
+ weight += 0.2;
339
+ }
340
+ // tags overlap (20%)
341
+ if (newControl.tags && existing.tags) {
342
+ const overlap = newControl.tags.filter(t => existing.tags.includes(t)).length;
343
+ score += (overlap / Math.max(newControl.tags.length, existing.tags.length)) * 0.2;
344
+ weight += 0.2;
345
+ }
346
+ return weight > 0 ? score / weight : 0;
347
+ }
348
+ // Detect similar controls
349
+ async function detectSimilarControls(db, newControl) {
350
+ const snapshot = await db.collection('araneaControlDefinitions')
351
+ .where('isDeleted', '==', false)
352
+ .get();
353
+ const matches = [];
354
+ for (const doc of snapshot.docs) {
355
+ const existing = { id: doc.id, ...doc.data() };
356
+ const similarity = calculateControlSimilarity(newControl, existing);
357
+ if (similarity >= 0.7) {
358
+ matches.push({ ...existing, similarity });
359
+ }
360
+ }
361
+ return matches.sort((a, b) => b.similarity - a.similarity);
362
+ }
363
+ // Command implementation
364
+ exports.controlCommand = new commander_1.Command('control')
365
+ .description('Control型(UIコンポーネント定義)の管理');
366
+ // control list
367
+ exports.controlCommand
368
+ .command('list')
369
+ .description('利用可能なコントロール一覧を表示')
370
+ .option('-c, --category <category>', 'カテゴリでフィルタ')
371
+ .option('-t, --type <dataType>', 'データ型でフィルタ')
372
+ .option('--include-deleted', '削除済みも表示')
373
+ .action(async (options) => {
374
+ const spinner = (0, ora_1.default)('コントロール一覧を取得中...').start();
375
+ try {
376
+ initializeFirebase();
377
+ const db = getFirestore();
378
+ let query = db.collection('araneaControlDefinitions');
379
+ if (!options.includeDeleted) {
380
+ query = query.where('isDeleted', '==', false);
381
+ }
382
+ const snapshot = await query.get();
383
+ if (snapshot.empty) {
384
+ spinner.warn('コントロールが見つかりません');
385
+ console.log(chalk_1.default.yellow('\nヒント: `aranea-sdk control sync` でビルトインコントロールを同期してください'));
386
+ return;
387
+ }
388
+ let controls = snapshot.docs.map(doc => ({
389
+ id: doc.id,
390
+ ...doc.data(),
391
+ }));
392
+ // Apply filters
393
+ if (options.category) {
394
+ controls = controls.filter(c => c.category === options.category);
395
+ }
396
+ if (options.type) {
397
+ controls = controls.filter(c => c.dataTypes.includes(options.type));
398
+ }
399
+ spinner.succeed(`${controls.length} 件のコントロールを取得しました`);
400
+ console.log('\n' + chalk_1.default.bold('=== コントロール一覧 ==='));
401
+ console.log('');
402
+ // Group by category
403
+ const byCategory = controls.reduce((acc, c) => {
404
+ if (!acc[c.category])
405
+ acc[c.category] = [];
406
+ acc[c.category].push(c);
407
+ return acc;
408
+ }, {});
409
+ for (const [category, items] of Object.entries(byCategory)) {
410
+ console.log(chalk_1.default.cyan.bold(`[${category}]`));
411
+ for (const c of items) {
412
+ const systemBadge = c.isSystem ? chalk_1.default.gray('[System]') : chalk_1.default.blue('[Custom]');
413
+ const enabledBadge = c.enabled ? chalk_1.default.green('✓') : chalk_1.default.red('✗');
414
+ const refCount = c.referenceCount > 0 ? chalk_1.default.yellow(`(${c.referenceCount} refs)`) : '';
415
+ console.log(` ${enabledBadge} ${c.id} ${systemBadge} ${refCount}`);
416
+ console.log(` ${c.displayName} - ${c.description}`);
417
+ console.log(` DataTypes: ${c.dataTypes.join(', ')}`);
418
+ }
419
+ console.log('');
420
+ }
421
+ }
422
+ catch (error) {
423
+ spinner.fail('エラーが発生しました');
424
+ console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
425
+ process.exit(1);
426
+ }
427
+ });
428
+ // control show
429
+ exports.controlCommand
430
+ .command('show <controlId>')
431
+ .description('コントロールの詳細を表示')
432
+ .action(async (controlId) => {
433
+ const spinner = (0, ora_1.default)(`コントロール "${controlId}" を取得中...`).start();
434
+ try {
435
+ initializeFirebase();
436
+ const db = getFirestore();
437
+ const doc = await db.collection('araneaControlDefinitions').doc(controlId).get();
438
+ if (!doc.exists) {
439
+ spinner.fail(`コントロール "${controlId}" が見つかりません`);
440
+ process.exit(1);
441
+ }
442
+ const control = { id: doc.id, ...doc.data() };
443
+ spinner.succeed('コントロール詳細を取得しました');
444
+ console.log('');
445
+ console.log(chalk_1.default.bold('=== コントロール詳細 ==='));
446
+ console.log('');
447
+ console.log(`${chalk_1.default.cyan('ID:')} ${control.id}`);
448
+ console.log(`${chalk_1.default.cyan('表示名:')} ${control.displayName}`);
449
+ console.log(`${chalk_1.default.cyan('説明:')} ${control.description}`);
450
+ console.log(`${chalk_1.default.cyan('カテゴリ:')} ${control.category}${control.subcategory ? ` / ${control.subcategory}` : ''}`);
451
+ console.log(`${chalk_1.default.cyan('データ型:')} ${control.dataTypes.join(', ')}`);
452
+ console.log(`${chalk_1.default.cyan('プロトコル:')} ${control.protocols.join(', ')}`);
453
+ console.log(`${chalk_1.default.cyan('プラットフォーム:')} ${control.platforms.join(', ')}`);
454
+ console.log(`${chalk_1.default.cyan('タグ:')} ${control.tags.join(', ')}`);
455
+ console.log(`${chalk_1.default.cyan('アイコン:')} ${control.iconName}`);
456
+ console.log(`${chalk_1.default.cyan('有効:')} ${control.enabled ? 'Yes' : 'No'}`);
457
+ console.log('');
458
+ console.log(chalk_1.default.bold('--- 安全情報 ---'));
459
+ console.log(`${chalk_1.default.cyan('システム定義:')} ${control.isSystem ? 'Yes (変更不可)' : 'No'}`);
460
+ console.log(`${chalk_1.default.cyan('参照カウント:')} ${control.referenceCount}`);
461
+ console.log(`${chalk_1.default.cyan('削除済み:')} ${control.isDeleted ? 'Yes' : 'No'}`);
462
+ if (control.createdByLacisId) {
463
+ console.log(`${chalk_1.default.cyan('作成者:')} ${control.createdByLacisId}`);
464
+ }
465
+ console.log('');
466
+ console.log(chalk_1.default.bold('--- サジェストルール ---'));
467
+ console.log(JSON.stringify(control.suggestionRules, null, 2));
468
+ console.log('');
469
+ console.log(chalk_1.default.bold('--- デフォルトProps ---'));
470
+ console.log(JSON.stringify(control.defaultProps, null, 2));
471
+ }
472
+ catch (error) {
473
+ spinner.fail('エラーが発生しました');
474
+ console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
475
+ process.exit(1);
476
+ }
477
+ });
478
+ // control sync
479
+ exports.controlCommand
480
+ .command('sync')
481
+ .description('ビルトインコントロールをFirestoreに同期')
482
+ .option('--dry-run', '実際の書き込みを行わずプレビュー')
483
+ .action(async (options) => {
484
+ const spinner = (0, ora_1.default)('ビルトインコントロールを同期中...').start();
485
+ try {
486
+ initializeFirebase();
487
+ const db = getFirestore();
488
+ const collectionRef = db.collection('araneaControlDefinitions');
489
+ // Get existing controls
490
+ const existingSnapshot = await collectionRef.get();
491
+ const existingData = new Map();
492
+ existingSnapshot.forEach(doc => {
493
+ existingData.set(doc.id, { id: doc.id, ...doc.data() });
494
+ });
495
+ spinner.info(`既存コントロール: ${existingData.size} 件`);
496
+ let createdCount = 0;
497
+ let updatedCount = 0;
498
+ let skippedCount = 0;
499
+ for (const def of BUILTIN_CONTROLS) {
500
+ const existing = existingData.get(def.id);
501
+ if (!existing) {
502
+ // Create new
503
+ console.log(chalk_1.default.green(` [CREATE] ${def.id}: ${def.displayName}`));
504
+ if (!options.dryRun) {
505
+ await collectionRef.doc(def.id).set({
506
+ ...def,
507
+ referenceCount: 0,
508
+ isDeleted: false,
509
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
510
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
511
+ });
512
+ }
513
+ createdCount++;
514
+ }
515
+ else if (existing.isSystem === false) {
516
+ // Skip custom controls
517
+ console.log(chalk_1.default.gray(` [SKIP] ${def.id}: カスタム定義のため同期対象外`));
518
+ skippedCount++;
519
+ }
520
+ else {
521
+ // Update system control
522
+ console.log(chalk_1.default.blue(` [UPDATE] ${def.id}: ${def.displayName}`));
523
+ if (!options.dryRun) {
524
+ await collectionRef.doc(def.id).update({
525
+ ...def,
526
+ // Preserve these fields
527
+ referenceCount: existing.referenceCount || 0,
528
+ isDeleted: false,
529
+ createdAt: existing.createdAt,
530
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
531
+ });
532
+ }
533
+ updatedCount++;
534
+ }
535
+ }
536
+ spinner.succeed('同期完了');
537
+ console.log('');
538
+ console.log(chalk_1.default.bold('=== 同期結果 ==='));
539
+ console.log(` 新規作成: ${createdCount} 件`);
540
+ console.log(` 更新: ${updatedCount} 件`);
541
+ console.log(` スキップ: ${skippedCount} 件`);
542
+ if (options.dryRun) {
543
+ console.log('');
544
+ console.log(chalk_1.default.yellow('(Dry Run モード - 実際の変更は行われていません)'));
545
+ }
546
+ }
547
+ catch (error) {
548
+ spinner.fail('エラーが発生しました');
549
+ console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
550
+ process.exit(1);
551
+ }
552
+ });
553
+ // control add (with similarity check)
554
+ exports.controlCommand
555
+ .command('add')
556
+ .description('カスタムコントロールを追加(類似チェック付き)')
557
+ .requiredOption('--id <id>', 'コントロールID')
558
+ .requiredOption('--name <name>', '表示名')
559
+ .requiredOption('--category <category>', 'カテゴリ (Boolean, Numeric, String)')
560
+ .option('--description <desc>', '説明')
561
+ .option('--data-types <types>', 'データ型 (カンマ区切り)', 'string')
562
+ .option('--dry-run', '実際の書き込みを行わずプレビュー')
563
+ .action(async (options) => {
564
+ const spinner = (0, ora_1.default)('コントロール追加の準備中...').start();
565
+ try {
566
+ initializeFirebase();
567
+ const db = getFirestore();
568
+ // Check if ID already exists
569
+ const existingDoc = await db.collection('araneaControlDefinitions').doc(options.id).get();
570
+ if (existingDoc.exists) {
571
+ spinner.fail(`コントロールID "${options.id}" は既に存在します`);
572
+ process.exit(1);
573
+ }
574
+ // Build new control
575
+ const newControl = {
576
+ id: options.id,
577
+ displayName: options.name,
578
+ description: options.description || '',
579
+ category: options.category,
580
+ dataTypes: options.dataTypes.split(',').map((t) => t.trim()),
581
+ tags: [],
582
+ };
583
+ // Check for similar controls
584
+ spinner.text = '類似コントロールを検索中...';
585
+ const similarControls = await detectSimilarControls(db, newControl);
586
+ spinner.stop();
587
+ if (similarControls.length > 0) {
588
+ console.log('');
589
+ console.log(chalk_1.default.yellow.bold('⚠️ 類似するコントロールが見つかりました:'));
590
+ console.log('');
591
+ for (let i = 0; i < Math.min(5, similarControls.length); i++) {
592
+ const sc = similarControls[i];
593
+ const similarity = Math.round(sc.similarity * 100);
594
+ console.log(` ${i + 1}. ${chalk_1.default.cyan(sc.id)} (${sc.displayName}) - 類似度: ${chalk_1.default.yellow(`${similarity}%`)}`);
595
+ console.log(` ${sc.description}`);
596
+ console.log(` DataTypes: ${sc.dataTypes.join(', ')}`);
597
+ console.log('');
598
+ }
599
+ if (!options.dryRun) {
600
+ const confirmed = await promptConfirm('本当に新規作成しますか?');
601
+ if (!confirmed) {
602
+ console.log(chalk_1.default.gray('キャンセルしました'));
603
+ process.exit(0);
604
+ }
605
+ }
606
+ }
607
+ if (options.dryRun) {
608
+ console.log('');
609
+ console.log(chalk_1.default.bold('=== 作成予定のコントロール ==='));
610
+ console.log(JSON.stringify(newControl, null, 2));
611
+ console.log('');
612
+ console.log(chalk_1.default.yellow('(Dry Run モード - 実際の作成は行われていません)'));
613
+ return;
614
+ }
615
+ // Create the control
616
+ const controlData = {
617
+ ...newControl,
618
+ protocols: ['mqtt', 'http'],
619
+ platforms: ['aranea'],
620
+ suggestionRules: { priority: 50 },
621
+ defaultProps: {},
622
+ iconName: 'Extension',
623
+ enabled: true,
624
+ isSystem: false,
625
+ // Note: createdByLacisId should be set from auth context in production
626
+ referenceCount: 0,
627
+ isDeleted: false,
628
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
629
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
630
+ };
631
+ await db.collection('araneaControlDefinitions').doc(options.id).set(controlData);
632
+ console.log('');
633
+ console.log(chalk_1.default.green.bold(`✓ コントロール "${options.id}" を作成しました`));
634
+ }
635
+ catch (error) {
636
+ spinner.fail('エラーが発生しました');
637
+ console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
638
+ process.exit(1);
639
+ }
640
+ });
641
+ // ============================================================
642
+ // FORBIDDEN COMMANDS - These must show error messages
643
+ // ============================================================
644
+ // control modify (FORBIDDEN)
645
+ exports.controlCommand
646
+ .command('modify <controlId>')
647
+ .description('コントロールを変更(禁止)')
648
+ .action(async (controlId) => {
649
+ console.log('');
650
+ console.log(chalk_1.default.red.bold('❌ エラー: Control変更はAdmin UIからのみ実行可能です。'));
651
+ console.log('');
652
+ console.log(chalk_1.default.yellow('理由:'));
653
+ console.log(' - システム定義コントロールの変更は禁止されています');
654
+ console.log(' - カスタムコントロールは所有者のみ変更可能です');
655
+ console.log(' - 変更にはUI上での確認ダイアログが必要です');
656
+ console.log('');
657
+ console.log(chalk_1.default.cyan('代わりに以下にアクセスしてください:'));
658
+ console.log(` ${chalk_1.default.underline('https://mobesorder.web.app/admin/aranea/controls')}`);
659
+ console.log('');
660
+ process.exit(1);
661
+ });
662
+ // control delete (FORBIDDEN)
663
+ exports.controlCommand
664
+ .command('delete <controlId>')
665
+ .description('コントロールを削除(禁止)')
666
+ .action(async (controlId) => {
667
+ console.log('');
668
+ console.log(chalk_1.default.red.bold('❌ エラー: Control削除はAdmin UIからのみ実行可能です。'));
669
+ console.log('');
670
+ console.log(chalk_1.default.yellow('理由:'));
671
+ console.log(' - システム定義コントロールは削除できません');
672
+ console.log(' - 削除には参照チェックが必要です(使用中のスキーマがある場合は削除不可)');
673
+ console.log(' - 削除には確認ダイアログと論理削除処理が必要です');
674
+ console.log('');
675
+ console.log(chalk_1.default.cyan('代わりに以下にアクセスしてください:'));
676
+ console.log(` ${chalk_1.default.underline('https://mobesorder.web.app/admin/aranea/controls')}`);
677
+ console.log('');
678
+ process.exit(1);
679
+ });
680
+ // Alias commands that also need to be blocked
681
+ exports.controlCommand
682
+ .command('update <controlId>')
683
+ .description('コントロールを更新(禁止)')
684
+ .action(async () => {
685
+ console.log('');
686
+ console.log(chalk_1.default.red.bold('❌ エラー: Control更新はAdmin UIからのみ実行可能です。'));
687
+ console.log('');
688
+ console.log(chalk_1.default.cyan('詳細は `aranea-sdk control modify --help` を参照してください'));
689
+ process.exit(1);
690
+ });
691
+ exports.controlCommand
692
+ .command('remove <controlId>')
693
+ .description('コントロールを削除(禁止)')
694
+ .action(async () => {
695
+ console.log('');
696
+ console.log(chalk_1.default.red.bold('❌ エラー: Control削除はAdmin UIからのみ実行可能です。'));
697
+ console.log('');
698
+ console.log(chalk_1.default.cyan('詳細は `aranea-sdk control delete --help` を参照してください'));
699
+ process.exit(1);
700
+ });