@nahisaho/satori 0.27.1 → 0.28.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/bin/satori.js +384 -14
- package/package.json +1 -1
package/bin/satori.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
4
5
|
const path = require('node:path');
|
|
5
6
|
|
|
6
7
|
const COMMAND = process.argv[2];
|
|
@@ -9,6 +10,7 @@ const FLAGS = process.argv.slice(3);
|
|
|
9
10
|
|
|
10
11
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
11
12
|
const SOURCE_DIR = path.join(PACKAGE_ROOT, 'src', '.github');
|
|
13
|
+
const CUSTOM_PIPELINES_PATH = path.join(process.env.HOME || os.homedir(), '.satori', 'custom-pipelines.json');
|
|
12
14
|
|
|
13
15
|
function copyDirSync(src, dest) {
|
|
14
16
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -73,8 +75,11 @@ Usage:
|
|
|
73
75
|
satori init [--force] [--dry-run] Install .github/ skills into current directory
|
|
74
76
|
satori skill search <query> Search skills by keyword
|
|
75
77
|
satori skill info <name> Show detailed skill information
|
|
78
|
+
satori skill recommend <name> Get similar/related skills
|
|
76
79
|
satori pipeline suggest Interactive pipeline recommendation
|
|
77
80
|
satori pipeline list List all available pipelines
|
|
81
|
+
satori pipeline custom <action> Manage custom pipelines
|
|
82
|
+
satori docs generate [--preview] Generate docs/ and docs/qiita/ files
|
|
78
83
|
satori validate [--verbose] Validate all SKILL.md files
|
|
79
84
|
satori stats Show skill/TU coverage statistics
|
|
80
85
|
satori help Show this help message
|
|
@@ -84,6 +89,12 @@ Options:
|
|
|
84
89
|
--force Overwrite existing .github/ directory
|
|
85
90
|
--dry-run Preview what would be installed without making changes
|
|
86
91
|
--verbose Show detailed validation output
|
|
92
|
+
--preview Show docs generation summary without writing files
|
|
93
|
+
|
|
94
|
+
Custom Pipelines:
|
|
95
|
+
satori pipeline custom list List custom pipelines
|
|
96
|
+
satori pipeline custom add <path> Add custom pipeline from file
|
|
97
|
+
satori pipeline custom remove <id> Remove custom pipeline
|
|
87
98
|
`);
|
|
88
99
|
}
|
|
89
100
|
|
|
@@ -473,6 +484,54 @@ const PIPELINES = [
|
|
|
473
484
|
},
|
|
474
485
|
];
|
|
475
486
|
|
|
487
|
+
// ── Synonym Dictionary ──
|
|
488
|
+
const SYNONYM_DICT = {
|
|
489
|
+
// 機械学習・AI
|
|
490
|
+
ml: ['machine learning', '機械学習', 'ML'],
|
|
491
|
+
ai: ['artificial intelligence', '人工知能', 'AI', 'AI'],
|
|
492
|
+
dl: ['deep learning', '深層学習', 'DL'],
|
|
493
|
+
'neural network': ['NN', 'ニューラルネットワーク'],
|
|
494
|
+
|
|
495
|
+
// バイオインフォマティクス
|
|
496
|
+
bioinfo: ['バイオインフォマティクス', 'bioinformatics'],
|
|
497
|
+
genomics: ['ゲノミクス', 'ゲノム', 'genomics'],
|
|
498
|
+
seq: ['シーケンシング', 'sequencing'],
|
|
499
|
+
rna: ['RNA', 'RNA-seq', 'トランスクリプトーム'],
|
|
500
|
+
protein: ['プロテイン', 'タンパク質'],
|
|
501
|
+
|
|
502
|
+
// 創薬・化学
|
|
503
|
+
'drug discovery': ['創薬', '創薬', 'drug-discovery'],
|
|
504
|
+
admet: ['ADMET', '薬物動態'],
|
|
505
|
+
docking: ['ドッキング', 'molecular docking'],
|
|
506
|
+
cheminformatics: ['ケモインフォマティクス'],
|
|
507
|
+
|
|
508
|
+
// データ分析
|
|
509
|
+
'data analysis': ['データ解析', 'data analysis'],
|
|
510
|
+
statistics: ['統計', '統計学'],
|
|
511
|
+
visualization: ['可視化', 'ビジュアル'],
|
|
512
|
+
pipeline: ['パイプライン'],
|
|
513
|
+
|
|
514
|
+
// 医療・臨床
|
|
515
|
+
clinical: ['臨床', 'クリニカル'],
|
|
516
|
+
precision: ['精密医療', '精密'],
|
|
517
|
+
oncology: ['腫瘍学', 'がん'],
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
function normalizeKeyword(keyword) {
|
|
521
|
+
const lower = keyword.toLowerCase().trim();
|
|
522
|
+
|
|
523
|
+
// 同義語チェック
|
|
524
|
+
for (const [key, synonyms] of Object.entries(SYNONYM_DICT)) {
|
|
525
|
+
for (const syn of synonyms) {
|
|
526
|
+
if (lower.includes(syn.toLowerCase()) || syn.toLowerCase().includes(lower)) {
|
|
527
|
+
return key;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return lower;
|
|
533
|
+
}
|
|
534
|
+
|
|
476
535
|
function pipelineSuggest() {
|
|
477
536
|
const readline = require('node:readline');
|
|
478
537
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -486,15 +545,20 @@ function pipelineSuggest() {
|
|
|
486
545
|
|
|
487
546
|
const input = await ask('何を解析しますか? キーワードや研究テーマを入力してください:\n> ');
|
|
488
547
|
const query = input.toLowerCase();
|
|
548
|
+
const normalizedQuery = normalizeKeyword(query);
|
|
489
549
|
|
|
490
|
-
// Score each pipeline by keyword match
|
|
550
|
+
// Score each pipeline by keyword match (with synonym support)
|
|
491
551
|
const scored = PIPELINES.map((p) => {
|
|
492
552
|
let score = 0;
|
|
493
553
|
for (const kw of p.keywords) {
|
|
554
|
+
const normalizedKw = normalizeKeyword(kw);
|
|
555
|
+
// 完全一致:2点、同義語マッチ:1.5点、部分一致:1点
|
|
494
556
|
if (query.includes(kw.toLowerCase())) score += 2;
|
|
557
|
+
else if (normalizedQuery === normalizedKw) score += 1.5;
|
|
558
|
+
else if (normalizedKw.includes(normalizedQuery) || query.includes(normalizedKw)) score += 1;
|
|
495
559
|
}
|
|
496
560
|
// Partial match on name
|
|
497
|
-
if (query.includes(p.name.toLowerCase()) || p.name.toLowerCase().includes(query)) score +=
|
|
561
|
+
if (query.includes(p.name.toLowerCase()) || p.name.toLowerCase().includes(query)) score += 0.5;
|
|
498
562
|
return { ...p, score };
|
|
499
563
|
})
|
|
500
564
|
.filter((p) => p.score > 0)
|
|
@@ -656,7 +720,7 @@ function validate() {
|
|
|
656
720
|
|
|
657
721
|
// ── Stats ──
|
|
658
722
|
|
|
659
|
-
function
|
|
723
|
+
function collectStats() {
|
|
660
724
|
const skillsDir = path.join(SOURCE_DIR, 'skills');
|
|
661
725
|
|
|
662
726
|
if (!fs.existsSync(skillsDir)) {
|
|
@@ -690,21 +754,142 @@ function stats() {
|
|
|
690
754
|
}
|
|
691
755
|
}
|
|
692
756
|
|
|
693
|
-
const
|
|
757
|
+
const pipelineBreakdown = {
|
|
758
|
+
domain: PIPELINES.filter((p) => typeof p.id === 'number').length,
|
|
759
|
+
cross: PIPELINES.filter((p) => p.domain === 'cross-domain').length,
|
|
760
|
+
industry: PIPELINES.filter((p) => p.domain === 'industry').length,
|
|
761
|
+
methodology: PIPELINES.filter((p) => p.domain === 'methodology').length,
|
|
762
|
+
};
|
|
763
|
+
|
|
694
764
|
const pkg = require(path.join(PACKAGE_ROOT, 'package.json'));
|
|
765
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
766
|
+
const coverage = ((tuLinked / totalSkills) * 100).toFixed(1);
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
version: pkg.version,
|
|
770
|
+
date,
|
|
771
|
+
totalSkills,
|
|
772
|
+
pipelinesCount: PIPELINES.length,
|
|
773
|
+
pipelineBreakdown,
|
|
774
|
+
tuLinked,
|
|
775
|
+
tuKeysCount: allTuKeys.size,
|
|
776
|
+
totalCodeBlocks,
|
|
777
|
+
coverage,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function stats() {
|
|
782
|
+
const summary = collectStats();
|
|
695
783
|
|
|
696
784
|
console.log(`
|
|
697
|
-
📊 SATORI v${
|
|
698
|
-
|
|
699
|
-
スキル総数: ${totalSkills}
|
|
700
|
-
パイプライン数: ${
|
|
701
|
-
TU 連携スキル: ${tuLinked} (${coverage}%)
|
|
702
|
-
TU 未連携: ${totalSkills - tuLinked}
|
|
703
|
-
ユニーク TU キー: ${
|
|
704
|
-
コードブロック総数: ${totalCodeBlocks}
|
|
785
|
+
📊 SATORI v${summary.version} — 統計
|
|
786
|
+
|
|
787
|
+
スキル総数: ${summary.totalSkills}
|
|
788
|
+
パイプライン数: ${summary.pipelinesCount}
|
|
789
|
+
TU 連携スキル: ${summary.tuLinked} (${summary.coverage}%)
|
|
790
|
+
TU 未連携: ${summary.totalSkills - summary.tuLinked}
|
|
791
|
+
ユニーク TU キー: ${summary.tuKeysCount}
|
|
792
|
+
コードブロック総数: ${summary.totalCodeBlocks}
|
|
705
793
|
`);
|
|
706
794
|
}
|
|
707
795
|
|
|
796
|
+
// ── Docs Generate ──
|
|
797
|
+
|
|
798
|
+
function updateReverseIndexDoc(content, summary) {
|
|
799
|
+
const pipelineLine = `| パイプライン数 | **${summary.pipelinesCount}** (ドメイン ${summary.pipelineBreakdown.domain} + クロスドメイン ${summary.pipelineBreakdown.cross} + インダストリー ${summary.pipelineBreakdown.industry} + メソドロジー ${summary.pipelineBreakdown.methodology}) |`;
|
|
800
|
+
|
|
801
|
+
return content
|
|
802
|
+
.replace(/\n\| SATORI バージョン \| \*\*v[^*]+\*\* \|/g, `\n| SATORI バージョン | **v${summary.version}** |`)
|
|
803
|
+
.replace(/\n\| 生成日 \| .* \|/g, `\n| 生成日 | ${summary.date} |`)
|
|
804
|
+
.replace(/\n\| スキル数 \| \*\*\d+\*\* \|/g, `\n| スキル数 | **${summary.totalSkills}** |`)
|
|
805
|
+
.replace(/\n\| パイプライン数 \| .* \|/g, `\n${pipelineLine}`)
|
|
806
|
+
.replace(
|
|
807
|
+
/\n\| ToolUniverse 連携スキル数 \| \*\*\d+\*\* \|/g,
|
|
808
|
+
`\n| ToolUniverse 連携スキル数 | **${summary.tuLinked}** |`,
|
|
809
|
+
)
|
|
810
|
+
.replace(
|
|
811
|
+
/\n\| ToolUniverse キー数(ユニーク) \| \*\*\d+\*\* \|/g,
|
|
812
|
+
`\n| ToolUniverse キー数(ユニーク) | **${summary.tuKeysCount}** |`,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function updatePipelineExamplesDoc(content, summary) {
|
|
817
|
+
const pipelineLine = `| 掲載パイプライン数 | ${summary.pipelineBreakdown.domain} ドメイン + ${summary.pipelineBreakdown.cross} クロスドメイン + ${summary.pipelineBreakdown.industry} インダストリー + ${summary.pipelineBreakdown.methodology} メソドロジー = **${summary.pipelinesCount}** |`;
|
|
818
|
+
|
|
819
|
+
return content
|
|
820
|
+
.replace(
|
|
821
|
+
/^> \*\*SATORI v[^*]+\*\* — .*$/m,
|
|
822
|
+
`> **SATORI v${summary.version}** — ${summary.totalSkills} スキル + ${summary.pipelinesCount} パイプラインの連携レシピ集`,
|
|
823
|
+
)
|
|
824
|
+
.replace(/\n\| 生成日 \| .* \|/g, `\n| 生成日 | ${summary.date} |`)
|
|
825
|
+
.replace(/\n\| 対象バージョン \| .* \|/g, `\n| 対象バージョン | v${summary.version} |`)
|
|
826
|
+
.replace(/\n\| 掲載パイプライン数 \| .* \|/g, `\n${pipelineLine}`)
|
|
827
|
+
.replace(
|
|
828
|
+
/\n\| スキル総数 \| \d+ \(.*\) \|/g,
|
|
829
|
+
`\n| スキル総数 | ${summary.totalSkills} (\`src/.github/skills/scientific-*/SKILL.md\`) |`,
|
|
830
|
+
)
|
|
831
|
+
.replace(/\n\| ToolUniverse キー数 \| .* \|/g, `\n| ToolUniverse キー数 | ${summary.tuKeysCount} (ユニーク) |`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function updateQiitaReverseIndexDoc(content, summary) {
|
|
835
|
+
const title = `title: 【SATORI v${summary.version}】${summary.totalSkills}スキル×${summary.pipelinesCount}パイプライン逆引き辞書 完全索引`;
|
|
836
|
+
const intro = `**[SATORI](https://github.com/nahisaho/satori)** は **GitHub Copilot** 上で動作する、${summary.totalSkills} の専門スキルと ${summary.pipelinesCount} の統合パイプライン(${summary.pipelineBreakdown.domain} ドメイン + ${summary.pipelineBreakdown.cross} クロスドメイン + ${summary.pipelineBreakdown.industry} インダストリー + ${summary.pipelineBreakdown.methodology} メソドロジー)により、仮説構築から論文出版まで、あらゆる科学研究ワークフローを自動化するフレームワークです。`;
|
|
837
|
+
|
|
838
|
+
return content.replace(/^title: .+$/m, title).replace(/^\*\*\[SATORI\][\s\S]*?フレームワークです。$/m, intro);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function updateQiitaPipelineExamplesDoc(content, summary) {
|
|
842
|
+
const title = `title: 【SATORI v${summary.version}】${summary.totalSkills}スキル×${summary.pipelinesCount}パイプラインで実現する科学研究自動化 完全ガイド`;
|
|
843
|
+
const intro = `**[SATORI](https://github.com/nahisaho/satori)** は **GitHub Copilot** 上で動作する、${summary.totalSkills} の専門スキルを組み合わせて構築した ${summary.pipelinesCount} 個のパイプライン(${summary.pipelineBreakdown.domain} ドメイン + ${summary.pipelineBreakdown.cross} クロスドメイン + ${summary.pipelineBreakdown.industry} インダストリー + ${summary.pipelineBreakdown.methodology} メソドロジー)により、仮説構築から論文出版まで、あらゆる科学研究ワークフローを自動化するフレームワークです。`;
|
|
844
|
+
|
|
845
|
+
return content.replace(/^title: .+$/m, title).replace(/^\*\*\[SATORI\][\s\S]*?フレームワークです。$/m, intro);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function applyDocUpdate(filePath, updater, summary, preview) {
|
|
849
|
+
if (!fs.existsSync(filePath)) {
|
|
850
|
+
console.error(`Error: ドキュメントが見つかりません: ${filePath}`);
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
854
|
+
const updated = updater(content, summary);
|
|
855
|
+
if (updated !== content && !preview) {
|
|
856
|
+
fs.writeFileSync(filePath, updated);
|
|
857
|
+
}
|
|
858
|
+
return updated !== content;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function docsGenerate() {
|
|
862
|
+
const summary = collectStats();
|
|
863
|
+
const docsFlags = process.argv.slice(4);
|
|
864
|
+
const preview = docsFlags.includes('--preview');
|
|
865
|
+
|
|
866
|
+
const targets = [
|
|
867
|
+
{ path: path.join(PACKAGE_ROOT, 'docs', 'SATORI_REVERSE_INDEX.md'), update: updateReverseIndexDoc },
|
|
868
|
+
{ path: path.join(PACKAGE_ROOT, 'docs', 'SATORI_PIPELINE_EXAMPLES.md'), update: updatePipelineExamplesDoc },
|
|
869
|
+
{
|
|
870
|
+
path: path.join(PACKAGE_ROOT, 'docs', 'qiita', 'SATORI_REVERSE_INDEX_QIITA.md'),
|
|
871
|
+
update: updateQiitaReverseIndexDoc,
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
path: path.join(PACKAGE_ROOT, 'docs', 'qiita', 'SATORI_PIPELINE_EXAMPLES_QIITA.md'),
|
|
875
|
+
update: updateQiitaPipelineExamplesDoc,
|
|
876
|
+
},
|
|
877
|
+
];
|
|
878
|
+
|
|
879
|
+
let updatedCount = 0;
|
|
880
|
+
for (const target of targets) {
|
|
881
|
+
if (applyDocUpdate(target.path, target.update, summary, preview)) {
|
|
882
|
+
updatedCount++;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (preview) {
|
|
887
|
+
console.log(`✔ docs generate (preview) 完了: ${updatedCount} 件更新予定`);
|
|
888
|
+
} else {
|
|
889
|
+
console.log(`✔ docs generate 完了: ${updatedCount} 件更新`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
708
893
|
// ── Skill Search / Info ──
|
|
709
894
|
|
|
710
895
|
function loadAllSkills() {
|
|
@@ -895,6 +1080,178 @@ function skillInfo() {
|
|
|
895
1080
|
console.log(`ファイル: src/.github/skills/${dirName}/SKILL.md`);
|
|
896
1081
|
}
|
|
897
1082
|
|
|
1083
|
+
function skillRecommend() {
|
|
1084
|
+
const name = process.argv[4];
|
|
1085
|
+
if (!name) {
|
|
1086
|
+
console.error('Error: スキル名を指定してください。');
|
|
1087
|
+
console.log('Usage: satori skill recommend <name>');
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const skillsDir = path.join(SOURCE_DIR, 'skills');
|
|
1092
|
+
const dirName = name.startsWith('scientific-') ? name : `scientific-${name}`;
|
|
1093
|
+
const filePath = path.join(skillsDir, dirName, 'SKILL.md');
|
|
1094
|
+
|
|
1095
|
+
if (!fs.existsSync(filePath)) {
|
|
1096
|
+
console.error(`Error: スキル "${name}" が見つかりません。`);
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// スキル名から短縮形を取得
|
|
1101
|
+
const shortName = dirName.replace('scientific-', '');
|
|
1102
|
+
|
|
1103
|
+
// 全パイプラインから、このスキルを使用するパイプラインを検出
|
|
1104
|
+
const usedInPipelines = PIPELINES.filter((p) => p.skills.includes(shortName));
|
|
1105
|
+
|
|
1106
|
+
// これらのパイプラインで使用される他のスキルをカウント
|
|
1107
|
+
const skillCooccurrence = {};
|
|
1108
|
+
for (const p of usedInPipelines) {
|
|
1109
|
+
const skillNames = p.skills.split(' → ').map((s) => s.trim());
|
|
1110
|
+
for (const sk of skillNames) {
|
|
1111
|
+
if (sk !== shortName && !skillCooccurrence[sk]) {
|
|
1112
|
+
skillCooccurrence[sk] = 0;
|
|
1113
|
+
}
|
|
1114
|
+
if (sk !== shortName) {
|
|
1115
|
+
skillCooccurrence[sk]++;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// スコア降順にソート
|
|
1121
|
+
const recommended = Object.entries(skillCooccurrence)
|
|
1122
|
+
.map(([skill, count]) => ({ skill, count }))
|
|
1123
|
+
.sort((a, b) => b.count - a.count)
|
|
1124
|
+
.slice(0, 5);
|
|
1125
|
+
|
|
1126
|
+
console.log(`\n🎯 "${name}" に関連するスキル\n`);
|
|
1127
|
+
|
|
1128
|
+
if (usedInPipelines.length > 0) {
|
|
1129
|
+
console.log(`このスキルが使用されるパイプライン: ${usedInPipelines.length} 件\n`);
|
|
1130
|
+
console.log('関連スキル:');
|
|
1131
|
+
for (let i = 0; i < recommended.length; i++) {
|
|
1132
|
+
const { skill, count } = recommended[i];
|
|
1133
|
+
console.log(` ${i + 1}. ${skill} (${count} パイプラインで併用)`);
|
|
1134
|
+
}
|
|
1135
|
+
console.log('');
|
|
1136
|
+
console.log('詳細は `satori skill info <related-skill>` で確認できます。');
|
|
1137
|
+
} else {
|
|
1138
|
+
console.log('❌ このスキルが使用されるパイプラインが見つかりませんでした。');
|
|
1139
|
+
console.log('すべてのパイプラインは `satori pipeline list` で確認できます。');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// ── Custom Pipeline Management ──
|
|
1144
|
+
|
|
1145
|
+
function loadCustomPipelines() {
|
|
1146
|
+
if (!fs.existsSync(CUSTOM_PIPELINES_PATH)) {
|
|
1147
|
+
return [];
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const content = fs.readFileSync(CUSTOM_PIPELINES_PATH, 'utf-8');
|
|
1151
|
+
const data = JSON.parse(content);
|
|
1152
|
+
return data.customPipelines || [];
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
console.error('Warning: Failed to load custom pipelines:', err.message);
|
|
1155
|
+
return [];
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function saveCustomPipelines(pipelines) {
|
|
1160
|
+
const dir = path.dirname(CUSTOM_PIPELINES_PATH);
|
|
1161
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1162
|
+
fs.writeFileSync(CUSTOM_PIPELINES_PATH, JSON.stringify({ customPipelines: pipelines }, null, 2));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function pipelineCustom() {
|
|
1166
|
+
const action = process.argv[4];
|
|
1167
|
+
const customPipelines = loadCustomPipelines();
|
|
1168
|
+
|
|
1169
|
+
if (action === 'list') {
|
|
1170
|
+
listCustomPipelines(customPipelines);
|
|
1171
|
+
} else if (action === 'add') {
|
|
1172
|
+
addCustomPipeline(customPipelines);
|
|
1173
|
+
} else if (action === 'remove') {
|
|
1174
|
+
removeCustomPipeline(customPipelines);
|
|
1175
|
+
} else {
|
|
1176
|
+
console.error(`Unknown custom pipeline action: ${action || '(none)'}`);
|
|
1177
|
+
console.log('Usage: satori pipeline custom list | add <file> | remove <id>');
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function listCustomPipelines(pipelines) {
|
|
1183
|
+
if (pipelines.length === 0) {
|
|
1184
|
+
console.log('\n📋 カスタムパイプライン: 0 件\n');
|
|
1185
|
+
console.log('新しいカスタムパイプラインを追加するには:');
|
|
1186
|
+
console.log(' satori pipeline custom add <file>');
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
console.log(`\n📋 カスタムパイプライン一覧 (${pipelines.length} 件)\n`);
|
|
1191
|
+
for (const p of pipelines) {
|
|
1192
|
+
console.log(` 🔧 [${p.id}] ${p.name}`);
|
|
1193
|
+
console.log(` スキル連鎖: ${p.skills}`);
|
|
1194
|
+
console.log('');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function addCustomPipeline(pipelines) {
|
|
1199
|
+
const filePath = process.argv[5];
|
|
1200
|
+
if (!filePath) {
|
|
1201
|
+
console.error('Error: ファイルパスを指定してください。');
|
|
1202
|
+
console.log('Usage: satori pipeline custom add <file>');
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (!fs.existsSync(filePath)) {
|
|
1207
|
+
console.error(`Error: ファイルが見つかりません: ${filePath}`);
|
|
1208
|
+
process.exit(1);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
try {
|
|
1212
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1213
|
+
const pipelineData = JSON.parse(content);
|
|
1214
|
+
|
|
1215
|
+
// バリデーション
|
|
1216
|
+
if (!pipelineData.id || !pipelineData.name || !pipelineData.skills) {
|
|
1217
|
+
console.error('Error: パイプラインは id, name, skills を含む必要があります。');
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 重複チェック
|
|
1222
|
+
if (pipelines.some((p) => p.id === pipelineData.id)) {
|
|
1223
|
+
console.error(`Error: ID "${pipelineData.id}" は既に存在します。`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
pipelines.push(pipelineData);
|
|
1228
|
+
saveCustomPipelines(pipelines);
|
|
1229
|
+
console.log(`✔ カスタムパイプライン "${pipelineData.name}" を追加しました。`);
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
console.error('Error: パイプラインファイルの解析に失敗しました:', err.message);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function removeCustomPipeline(pipelines) {
|
|
1237
|
+
const id = process.argv[5];
|
|
1238
|
+
if (!id) {
|
|
1239
|
+
console.error('Error: パイプライン ID を指定してください。');
|
|
1240
|
+
console.log('Usage: satori pipeline custom remove <id>');
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const index = pipelines.findIndex((p) => p.id === id);
|
|
1245
|
+
if (index === -1) {
|
|
1246
|
+
console.error(`Error: パイプライン "${id}" が見つかりません。`);
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const removed = pipelines.splice(index, 1)[0];
|
|
1251
|
+
saveCustomPipelines(pipelines);
|
|
1252
|
+
console.log(`✔ カスタムパイプライン "${removed.name}" を削除しました。`);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
898
1255
|
switch (COMMAND) {
|
|
899
1256
|
case 'init':
|
|
900
1257
|
init();
|
|
@@ -904,9 +1261,11 @@ switch (COMMAND) {
|
|
|
904
1261
|
skillSearch();
|
|
905
1262
|
} else if (SUBCOMMAND === 'info') {
|
|
906
1263
|
skillInfo();
|
|
1264
|
+
} else if (SUBCOMMAND === 'recommend') {
|
|
1265
|
+
skillRecommend();
|
|
907
1266
|
} else {
|
|
908
1267
|
console.error(`Unknown skill subcommand: ${SUBCOMMAND || '(none)'}`);
|
|
909
|
-
console.log('Usage: satori skill search <query> | satori skill info <name>');
|
|
1268
|
+
console.log('Usage: satori skill search <query> | satori skill info <name> | satori skill recommend <name>');
|
|
910
1269
|
process.exit(1);
|
|
911
1270
|
}
|
|
912
1271
|
break;
|
|
@@ -915,9 +1274,20 @@ switch (COMMAND) {
|
|
|
915
1274
|
pipelineSuggest();
|
|
916
1275
|
} else if (SUBCOMMAND === 'list') {
|
|
917
1276
|
pipelineList();
|
|
1277
|
+
} else if (SUBCOMMAND === 'custom') {
|
|
1278
|
+
pipelineCustom();
|
|
918
1279
|
} else {
|
|
919
1280
|
console.error(`Unknown pipeline subcommand: ${SUBCOMMAND || '(none)'}`);
|
|
920
|
-
console.log('Usage: satori pipeline suggest | satori pipeline list');
|
|
1281
|
+
console.log('Usage: satori pipeline suggest | satori pipeline list | satori pipeline custom list|add|remove');
|
|
1282
|
+
process.exit(1);
|
|
1283
|
+
}
|
|
1284
|
+
break;
|
|
1285
|
+
case 'docs':
|
|
1286
|
+
if (SUBCOMMAND === 'generate') {
|
|
1287
|
+
docsGenerate();
|
|
1288
|
+
} else {
|
|
1289
|
+
console.error(`Unknown docs subcommand: ${SUBCOMMAND || '(none)'}`);
|
|
1290
|
+
console.log('Usage: satori docs generate [--preview]');
|
|
921
1291
|
process.exit(1);
|
|
922
1292
|
}
|
|
923
1293
|
break;
|