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.
- package/dist/commands/backlog.d.ts +15 -0
- package/dist/commands/backlog.js +364 -0
- package/dist/commands/control.d.ts +20 -0
- package/dist/commands/control.js +700 -0
- package/dist/commands/registry.d.ts +20 -0
- package/dist/commands/registry.js +662 -0
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.js +202 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +4 -0
- package/dist/index.js +11 -1
- package/package.json +3 -10
|
@@ -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
|
+
});
|