@tbsten/mir-core 0.0.1-alpha02
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/package.json +25 -0
- package/src/__tests__/errors.test.ts +71 -0
- package/src/__tests__/hooks.test.ts +145 -0
- package/src/__tests__/i18n.test.ts +60 -0
- package/src/__tests__/remote-registry.test.ts +265 -0
- package/src/__tests__/safe-yaml-parser.test.ts +187 -0
- package/src/__tests__/snapshots/__snapshots__/error-messages.snapshot.test.ts.snap +27 -0
- package/src/__tests__/snapshots/__snapshots__/snippet-schema-output.snapshot.test.ts.snap +78 -0
- package/src/__tests__/snapshots/__snapshots__/template-output.snapshot.test.ts.snap +45 -0
- package/src/__tests__/snapshots/error-messages.snapshot.test.ts +76 -0
- package/src/__tests__/snapshots/snippet-schema-output.snapshot.test.ts +98 -0
- package/src/__tests__/snapshots/template-output.snapshot.test.ts +73 -0
- package/src/__tests__/snippet-schema.test.ts +180 -0
- package/src/__tests__/symlink-checker.test.ts +95 -0
- package/src/__tests__/template-engine.test.ts +137 -0
- package/src/__tests__/validate-name.test.ts +61 -0
- package/src/errors.ts +82 -0
- package/src/hooks.ts +63 -0
- package/src/i18n/index.ts +32 -0
- package/src/i18n/locales/en.ts +91 -0
- package/src/i18n/locales/ja.ts +91 -0
- package/src/i18n/types.ts +91 -0
- package/src/index.ts +84 -0
- package/src/lib/symlink-checker.ts +62 -0
- package/src/registry.ts +117 -0
- package/src/remote-registry.ts +260 -0
- package/src/safe-yaml-parser.ts +71 -0
- package/src/snippet-schema.ts +117 -0
- package/src/template-engine.ts +114 -0
- package/src/validate-name.ts +12 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MessageCatalog } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const en: MessageCatalog = {
|
|
4
|
+
// errors
|
|
5
|
+
"error.snippet-not-found": "Snippet \"{name}\" not found. Use `mir list` to see available snippets or specify `--registry` for another registry",
|
|
6
|
+
"error.snippet-already-exists": "Snippet \"{name}\" already exists. Use `--force` to overwrite or specify a different name",
|
|
7
|
+
"error.registry-not-found": "Registry \"{name}\" not found. Configure registries in `~/.mir/mirconfig.yaml`",
|
|
8
|
+
"error.registry-remote": "Cannot publish to remote registry",
|
|
9
|
+
"error.registry-remote-named": "Cannot publish to registry \"{name}\" because it is a remote registry",
|
|
10
|
+
"error.path-traversal": "Security error: File path \"{path}\" references outside the output directory. Check your template files",
|
|
11
|
+
"error.file-conflict": "File \"{path}\" already exists. Use `--no-interactive` to overwrite all files or `--out-dir` to specify another directory",
|
|
12
|
+
"error.validation": "Validation error",
|
|
13
|
+
"error.invalid-snippet-name": "Invalid snippet name \"{name}\". Only alphanumeric characters and hyphens are allowed, and must start with an alphanumeric character",
|
|
14
|
+
"error.variable-empty": "No value provided for variable \"{key}\"",
|
|
15
|
+
"error.variable-required": "Variable \"{key}\" is required. Specify it with {hint}",
|
|
16
|
+
"error.exit-hook": "Installation was cancelled",
|
|
17
|
+
"error.hook-input-required": "Variable \"{key}\" requires input, but interactive mode is not yet supported. Please specify a default value",
|
|
18
|
+
"error.no-snippets": "No snippets available to select",
|
|
19
|
+
"error.remote-fetch": "Failed to fetch from remote registry: {url}",
|
|
20
|
+
"error.remote-fetch-status": "Failed to fetch from remote registry: {url} (HTTP {status})",
|
|
21
|
+
"error.invalid-manifest": "Invalid manifest from remote registry: {url}",
|
|
22
|
+
"error.fetch-timeout": "Timeout: Connection to {url} did not complete within {timeout} seconds",
|
|
23
|
+
"error.symlink-detected": "Symbolic link detected: {path}",
|
|
24
|
+
"error.symlink-in-snippet": "Snippet contains symbolic links: {paths}",
|
|
25
|
+
"error.safe-mode-overwrite": "Overwriting existing files is not allowed in safe mode: {path}",
|
|
26
|
+
"error.file-not-found": "File \"{path}\" not found",
|
|
27
|
+
"error.file-read-failed": "Failed to read file \"{path}\"",
|
|
28
|
+
"error.snippet-not-found-details": "\nPossible causes:\n1. Typo in snippet name\n2. Snippet not registered in registry\n3. No access permission to registry\n\nWays to check:\n• mir list - Show available snippets\n• mir search <keyword> - Search by keyword\n• mir info <name> - Show snippet details",
|
|
29
|
+
|
|
30
|
+
// create
|
|
31
|
+
"create.success": "Created snippet \"{name}\"",
|
|
32
|
+
|
|
33
|
+
// publish
|
|
34
|
+
"publish.success": "Published snippet \"{name}\" to registry",
|
|
35
|
+
"publish.cancelled": "Publish cancelled",
|
|
36
|
+
"publish.confirm-overwrite": "Snippet \"{name}\" already exists. Overwrite?",
|
|
37
|
+
|
|
38
|
+
// install
|
|
39
|
+
"install.success": "Installed snippet \"{name}\"",
|
|
40
|
+
"install.skip": "Skipped: {path}",
|
|
41
|
+
"install.confirm-overwrite": "File \"{path}\" already exists. Overwrite? (y/n/a): ",
|
|
42
|
+
"install.dry-run-files": "[dry-run] Files to be generated:",
|
|
43
|
+
"install.dry-run-complete": "[dry-run] No actual file writes were performed.",
|
|
44
|
+
"install.multiple-snippets": "Installing multiple snippets...",
|
|
45
|
+
"install.snippet-n-of-m": "{current} / {total}: {name}",
|
|
46
|
+
"install.completed-multiple": "Installed {count} snippet(s)",
|
|
47
|
+
"install.failed-multiple": "Failed to install {count} snippet(s)",
|
|
48
|
+
"install.safe-mode-hooks-skipped": "[safe] Skipped hooks execution",
|
|
49
|
+
"install.symlink-warning": "Symbolic link detected: {path}",
|
|
50
|
+
|
|
51
|
+
// sync
|
|
52
|
+
"sync.no-new-vars": "No new variables to add",
|
|
53
|
+
"sync.success": "Added {count} variable(s)",
|
|
54
|
+
|
|
55
|
+
// search
|
|
56
|
+
"search.query-required": "Search query is required",
|
|
57
|
+
"search.no-results": "No snippets found matching \"{query}\"",
|
|
58
|
+
|
|
59
|
+
// error specific
|
|
60
|
+
"error.no-failed-snippets": "No failed snippet history",
|
|
61
|
+
|
|
62
|
+
// clone
|
|
63
|
+
"clone.success": "Cloned snippet \"{name}\" to \"{alias}\"",
|
|
64
|
+
|
|
65
|
+
// preview
|
|
66
|
+
"preview.title": "Preview: {name}",
|
|
67
|
+
"preview.confirm": "Do you want to install this snippet?",
|
|
68
|
+
|
|
69
|
+
// prompt
|
|
70
|
+
"prompt.snippet-name": "Snippet name: ",
|
|
71
|
+
"prompt.select-snippet": "Select a snippet",
|
|
72
|
+
"prompt.select": "Select: ",
|
|
73
|
+
"prompt.enter-number": "Please enter a number",
|
|
74
|
+
"prompt.other-manual": "Other (manual input)",
|
|
75
|
+
"prompt.use-default": "Press Enter to use {value}",
|
|
76
|
+
"prompt.use-default-value": "Press Enter to use default \"{value}\"",
|
|
77
|
+
"prompt.yes-no-all": "(y/n/a): ",
|
|
78
|
+
"prompt.yes-no": "(y/N): ",
|
|
79
|
+
|
|
80
|
+
// batch-summary
|
|
81
|
+
"batch-summary.results": "Installation results:",
|
|
82
|
+
"batch-summary.success": "Success",
|
|
83
|
+
"batch-summary.failure": "Failed",
|
|
84
|
+
"batch-summary.skipped": "Skipped",
|
|
85
|
+
"batch-summary.counts": "Success: {success}/{total}, Failed: {failure}/{total}, Skipped: {skipped}/{total}",
|
|
86
|
+
"batch-summary.retry-hint": "💡 To retry failed snippets: mir install --retry-failed",
|
|
87
|
+
|
|
88
|
+
// general
|
|
89
|
+
"general.variables": "Variables:",
|
|
90
|
+
"general.default": "(default)",
|
|
91
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MessageCatalog } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const ja: MessageCatalog = {
|
|
4
|
+
// errors
|
|
5
|
+
"error.snippet-not-found": "Snippet \"{name}\" が見つかりません。`mir list` で利用可能な snippet を確認するか、`--registry` で別の registry を指定してください",
|
|
6
|
+
"error.snippet-already-exists": "Snippet \"{name}\" は既に存在します。`--force` で上書きするか、別の名前を指定してください",
|
|
7
|
+
"error.registry-not-found": "Registry \"{name}\" が見つかりません。`~/.mir/mirconfig.yaml` で registry を設定してください",
|
|
8
|
+
"error.registry-remote": "リモート registry には publish できません",
|
|
9
|
+
"error.registry-remote-named": "Registry \"{name}\" はリモート registry のため publish できません",
|
|
10
|
+
"error.path-traversal": "セキュリティエラー: ファイルパス \"{path}\" が出力範囲外を参照しています。テンプレートファイルを確認してください",
|
|
11
|
+
"error.file-conflict": "ファイル \"{path}\" は既に存在します。`--no-interactive` で全て上書きするか、`--out-dir` で別ディレクトリを指定してください",
|
|
12
|
+
"error.validation": "バリデーションエラー",
|
|
13
|
+
"error.invalid-snippet-name": "不正な snippet 名 \"{name}\" です。英数字とハイフンのみ使用可能で、先頭は英数字にしてください",
|
|
14
|
+
"error.variable-empty": "変数 \"{key}\" の値が入力されませんでした",
|
|
15
|
+
"error.variable-required": "変数 \"{key}\" の値が必要です。{hint} で指定してください",
|
|
16
|
+
"error.exit-hook": "install が中止されました",
|
|
17
|
+
"error.hook-input-required": "変数 \"{key}\" の入力が必要ですが、interactive mode は未対応です。default 値を指定してください",
|
|
18
|
+
"error.no-snippets": "選択可能な snippet がありません",
|
|
19
|
+
"error.remote-fetch": "リモート registry の取得に失敗しました: {url}",
|
|
20
|
+
"error.remote-fetch-status": "リモート registry の取得に失敗しました: {url} (HTTP {status})",
|
|
21
|
+
"error.invalid-manifest": "リモート registry のマニフェストが不正です: {url}",
|
|
22
|
+
"error.fetch-timeout": "タイムアウト: {url} への接続が {timeout} 秒以内に完了しませんでした",
|
|
23
|
+
"error.symlink-detected": "シンボリックリンクが検出されました: {path}",
|
|
24
|
+
"error.symlink-in-snippet": "Snippet にシンボリックリンクが含まれています: {paths}",
|
|
25
|
+
"error.safe-mode-overwrite": "safe モードでは既存ファイルの上書きは許可されていません: {path}",
|
|
26
|
+
"error.file-not-found": "ファイル \"{path}\" が見つかりません",
|
|
27
|
+
"error.file-read-failed": "ファイル \"{path}\" の読み込みに失敗しました",
|
|
28
|
+
"error.snippet-not-found-details": "\n可能な原因:\n1. Snippet 名の入力ミス\n2. Registry に登録されていない\n3. Registry へのアクセス権限がない\n\n確認方法:\n• mir list - 利用可能な snippet を表示\n• mir search <keyword> - キーワードで検索\n• mir info <name> - snippet の詳細情報を表示",
|
|
29
|
+
|
|
30
|
+
// create
|
|
31
|
+
"create.success": "Snippet \"{name}\" を作成しました",
|
|
32
|
+
|
|
33
|
+
// publish
|
|
34
|
+
"publish.success": "Snippet \"{name}\" を registry に登録しました",
|
|
35
|
+
"publish.cancelled": "publish をキャンセルしました",
|
|
36
|
+
"publish.confirm-overwrite": "Snippet \"{name}\" は既に存在します。上書きしますか?",
|
|
37
|
+
|
|
38
|
+
// install
|
|
39
|
+
"install.success": "Snippet \"{name}\" をインストールしました",
|
|
40
|
+
"install.skip": "スキップ: {path}",
|
|
41
|
+
"install.confirm-overwrite": "ファイル \"{path}\" は既に存在します。上書きしますか? (y/n/a): ",
|
|
42
|
+
"install.dry-run-files": "[dry-run] 生成されるファイル:",
|
|
43
|
+
"install.dry-run-complete": "[dry-run] 実際のファイル書き込みは実行されていません。",
|
|
44
|
+
"install.multiple-snippets": "複数の snippet をインストール中...",
|
|
45
|
+
"install.snippet-n-of-m": "{current} / {total}: {name}",
|
|
46
|
+
"install.completed-multiple": "{count} 個の snippet をインストールしました",
|
|
47
|
+
"install.failed-multiple": "{count} 個の snippet のインストールに失敗しました",
|
|
48
|
+
"install.safe-mode-hooks-skipped": "[safe] hooks の実行をスキップしました",
|
|
49
|
+
"install.symlink-warning": "シンボリックリンクが検出されました: {path}",
|
|
50
|
+
|
|
51
|
+
// sync
|
|
52
|
+
"sync.no-new-vars": "追加する変数はありません",
|
|
53
|
+
"sync.success": "{count} 件の変数を追加しました",
|
|
54
|
+
|
|
55
|
+
// search
|
|
56
|
+
"search.query-required": "検索キーワードが必要です",
|
|
57
|
+
"search.no-results": "\"{query}\" に一致する snippet が見つかりません",
|
|
58
|
+
|
|
59
|
+
// error specific
|
|
60
|
+
"error.no-failed-snippets": "失敗した snippet の履歴がありません",
|
|
61
|
+
|
|
62
|
+
// clone
|
|
63
|
+
"clone.success": "Snippet \"{name}\" を \"{alias}\" として複製しました",
|
|
64
|
+
|
|
65
|
+
// preview
|
|
66
|
+
"preview.title": "プレビュー: {name}",
|
|
67
|
+
"preview.confirm": "この snippet をインストールしますか?",
|
|
68
|
+
|
|
69
|
+
// prompt
|
|
70
|
+
"prompt.snippet-name": "snippet 名: ",
|
|
71
|
+
"prompt.select-snippet": "snippet を選択してください",
|
|
72
|
+
"prompt.select": "選択: ",
|
|
73
|
+
"prompt.enter-number": "番号を入力してください",
|
|
74
|
+
"prompt.other-manual": "その他 (手動入力)",
|
|
75
|
+
"prompt.use-default": "Enter で {value} を使用",
|
|
76
|
+
"prompt.use-default-value": "Enter でデフォルト値 \"{value}\" を使用",
|
|
77
|
+
"prompt.yes-no-all": "(y/n/a): ",
|
|
78
|
+
"prompt.yes-no": "(y/N): ",
|
|
79
|
+
|
|
80
|
+
// batch-summary
|
|
81
|
+
"batch-summary.results": "インストール結果:",
|
|
82
|
+
"batch-summary.success": "成功",
|
|
83
|
+
"batch-summary.failure": "失敗",
|
|
84
|
+
"batch-summary.skipped": "スキップ",
|
|
85
|
+
"batch-summary.counts": "成功: {success}/{total}, 失敗: {failure}/{total}, スキップ: {skipped}/{total}",
|
|
86
|
+
"batch-summary.retry-hint": "💡 失敗した snippet をもう一度インストールするには: mir install --retry-failed",
|
|
87
|
+
|
|
88
|
+
// general
|
|
89
|
+
"general.variables": "Variables:",
|
|
90
|
+
"general.default": "(default)",
|
|
91
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export interface MessageCatalog {
|
|
2
|
+
// errors
|
|
3
|
+
"error.snippet-not-found": string;
|
|
4
|
+
"error.snippet-already-exists": string;
|
|
5
|
+
"error.registry-not-found": string;
|
|
6
|
+
"error.registry-remote": string;
|
|
7
|
+
"error.registry-remote-named": string;
|
|
8
|
+
"error.path-traversal": string;
|
|
9
|
+
"error.file-conflict": string;
|
|
10
|
+
"error.validation": string;
|
|
11
|
+
"error.invalid-snippet-name": string;
|
|
12
|
+
"error.variable-empty": string;
|
|
13
|
+
"error.variable-required": string;
|
|
14
|
+
"error.exit-hook": string;
|
|
15
|
+
"error.hook-input-required": string;
|
|
16
|
+
"error.no-snippets": string;
|
|
17
|
+
"error.remote-fetch": string;
|
|
18
|
+
"error.remote-fetch-status": string;
|
|
19
|
+
"error.invalid-manifest": string;
|
|
20
|
+
"error.fetch-timeout": string;
|
|
21
|
+
"error.symlink-detected": string;
|
|
22
|
+
"error.symlink-in-snippet": string;
|
|
23
|
+
"error.safe-mode-overwrite": string;
|
|
24
|
+
"error.file-not-found": string;
|
|
25
|
+
"error.file-read-failed": string;
|
|
26
|
+
"error.snippet-not-found-details": string;
|
|
27
|
+
|
|
28
|
+
// create
|
|
29
|
+
"create.success": string;
|
|
30
|
+
|
|
31
|
+
// publish
|
|
32
|
+
"publish.success": string;
|
|
33
|
+
"publish.cancelled": string;
|
|
34
|
+
"publish.confirm-overwrite": string;
|
|
35
|
+
|
|
36
|
+
// install
|
|
37
|
+
"install.success": string;
|
|
38
|
+
"install.skip": string;
|
|
39
|
+
"install.confirm-overwrite": string;
|
|
40
|
+
"install.dry-run-files": string;
|
|
41
|
+
"install.dry-run-complete": string;
|
|
42
|
+
"install.multiple-snippets": string;
|
|
43
|
+
"install.snippet-n-of-m": string;
|
|
44
|
+
"install.completed-multiple": string;
|
|
45
|
+
"install.failed-multiple": string;
|
|
46
|
+
"install.safe-mode-hooks-skipped": string;
|
|
47
|
+
"install.symlink-warning": string;
|
|
48
|
+
|
|
49
|
+
// sync
|
|
50
|
+
"sync.no-new-vars": string;
|
|
51
|
+
"sync.success": string;
|
|
52
|
+
|
|
53
|
+
// search
|
|
54
|
+
"search.query-required": string;
|
|
55
|
+
"search.no-results": string;
|
|
56
|
+
|
|
57
|
+
// error specific
|
|
58
|
+
"error.no-failed-snippets": string;
|
|
59
|
+
|
|
60
|
+
// clone
|
|
61
|
+
"clone.success": string;
|
|
62
|
+
|
|
63
|
+
// preview
|
|
64
|
+
"preview.title": string;
|
|
65
|
+
"preview.confirm": string;
|
|
66
|
+
|
|
67
|
+
// prompt
|
|
68
|
+
"prompt.snippet-name": string;
|
|
69
|
+
"prompt.select-snippet": string;
|
|
70
|
+
"prompt.select": string;
|
|
71
|
+
"prompt.enter-number": string;
|
|
72
|
+
"prompt.other-manual": string;
|
|
73
|
+
"prompt.use-default": string;
|
|
74
|
+
"prompt.use-default-value": string;
|
|
75
|
+
"prompt.yes-no-all": string;
|
|
76
|
+
"prompt.yes-no": string;
|
|
77
|
+
|
|
78
|
+
// batch-summary
|
|
79
|
+
"batch-summary.results": string;
|
|
80
|
+
"batch-summary.success": string;
|
|
81
|
+
"batch-summary.failure": string;
|
|
82
|
+
"batch-summary.skipped": string;
|
|
83
|
+
"batch-summary.counts": string;
|
|
84
|
+
"batch-summary.retry-hint": string;
|
|
85
|
+
|
|
86
|
+
// general
|
|
87
|
+
"general.variables": string;
|
|
88
|
+
"general.default": string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type MessageKey = keyof MessageCatalog;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// errors
|
|
2
|
+
export {
|
|
3
|
+
MirError,
|
|
4
|
+
ValidationError,
|
|
5
|
+
SnippetNotFoundError,
|
|
6
|
+
SnippetAlreadyExistsError,
|
|
7
|
+
RegistryNotFoundError,
|
|
8
|
+
RegistryRemoteError,
|
|
9
|
+
PathTraversalError,
|
|
10
|
+
FileConflictError,
|
|
11
|
+
RemoteRegistryFetchError,
|
|
12
|
+
InvalidManifestError,
|
|
13
|
+
} from "./errors.js";
|
|
14
|
+
|
|
15
|
+
// snippet-schema
|
|
16
|
+
export {
|
|
17
|
+
parseSnippetYaml,
|
|
18
|
+
serializeSnippetYaml,
|
|
19
|
+
validateSnippetDefinition,
|
|
20
|
+
} from "./snippet-schema.js";
|
|
21
|
+
export type {
|
|
22
|
+
VariableSchema,
|
|
23
|
+
VariableDefinition,
|
|
24
|
+
Action,
|
|
25
|
+
SnippetDefinition,
|
|
26
|
+
} from "./snippet-schema.js";
|
|
27
|
+
|
|
28
|
+
// validate-name
|
|
29
|
+
export { validateSnippetName } from "./validate-name.js";
|
|
30
|
+
|
|
31
|
+
// safe-yaml-parser
|
|
32
|
+
export {
|
|
33
|
+
safeParseYaml,
|
|
34
|
+
checkNoRefInSchema,
|
|
35
|
+
YAML_MAX_SIZE_BYTES,
|
|
36
|
+
} from "./safe-yaml-parser.js";
|
|
37
|
+
|
|
38
|
+
// template-engine
|
|
39
|
+
export {
|
|
40
|
+
expandTemplate,
|
|
41
|
+
expandPath,
|
|
42
|
+
extractVariables,
|
|
43
|
+
extractVariablesFromDirectory,
|
|
44
|
+
expandTemplateDirectory,
|
|
45
|
+
} from "./template-engine.js";
|
|
46
|
+
|
|
47
|
+
// hooks
|
|
48
|
+
export { ExitHookError, executeHooks } from "./hooks.js";
|
|
49
|
+
export type { HookExecutionOptions } from "./hooks.js";
|
|
50
|
+
|
|
51
|
+
// registry
|
|
52
|
+
export {
|
|
53
|
+
listRegistrySnippets,
|
|
54
|
+
snippetExistsInRegistry,
|
|
55
|
+
readSnippetFromRegistry,
|
|
56
|
+
listTemplateFiles,
|
|
57
|
+
readTemplateFile,
|
|
58
|
+
copySnippetToRegistry,
|
|
59
|
+
removeSnippetFromRegistry,
|
|
60
|
+
} from "./registry.js";
|
|
61
|
+
|
|
62
|
+
// remote-registry
|
|
63
|
+
export {
|
|
64
|
+
fetchRegistryManifest,
|
|
65
|
+
listRemoteSnippets,
|
|
66
|
+
searchRemoteSnippets,
|
|
67
|
+
fetchSnippetDefinition,
|
|
68
|
+
fetchRemoteFiles,
|
|
69
|
+
fetchRemoteSnippet,
|
|
70
|
+
expandRemoteTemplateFiles,
|
|
71
|
+
} from "./remote-registry.js";
|
|
72
|
+
export type { RegistryManifest, RemoteSnippet, FetchOptions } from "./remote-registry.js";
|
|
73
|
+
|
|
74
|
+
// symlink-checker
|
|
75
|
+
export {
|
|
76
|
+
isSymbolicLink,
|
|
77
|
+
findSymlinksInDirectory,
|
|
78
|
+
} from "./lib/symlink-checker.js";
|
|
79
|
+
export type { SymlinkCheckResult } from "./lib/symlink-checker.js";
|
|
80
|
+
|
|
81
|
+
// i18n
|
|
82
|
+
export { setLocale, getLocale, t } from "./i18n/index.js";
|
|
83
|
+
export type { Locale } from "./i18n/index.js";
|
|
84
|
+
export type { MessageKey, MessageCatalog } from "./i18n/types.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SymlinkCheckResult {
|
|
5
|
+
hasSymlinks: boolean;
|
|
6
|
+
symlinkPaths: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 指定ファイルがシンボリックリンクかどうかを確認する
|
|
11
|
+
* lstat を使用することで、リンク先ではなくリンク自体の情報を取得する
|
|
12
|
+
*/
|
|
13
|
+
export function isSymbolicLink(filePath: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const stat = fs.lstatSync(filePath);
|
|
16
|
+
return stat.isSymbolicLink();
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ディレクトリ内のシンボリックリンクを再帰的に検索する
|
|
24
|
+
* @param dirPath 検索対象ディレクトリ
|
|
25
|
+
* @returns シンボリックリンクの有無と、見つかったシンボリックリンクのパス一覧
|
|
26
|
+
*/
|
|
27
|
+
export function findSymlinksInDirectory(dirPath: string): SymlinkCheckResult {
|
|
28
|
+
const symlinkPaths: string[] = [];
|
|
29
|
+
|
|
30
|
+
function walkDir(currentPath: string, relativePath: string): void {
|
|
31
|
+
let entries: fs.Dirent[];
|
|
32
|
+
try {
|
|
33
|
+
entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
40
|
+
const relPath = relativePath
|
|
41
|
+
? path.join(relativePath, entry.name)
|
|
42
|
+
: entry.name;
|
|
43
|
+
|
|
44
|
+
// isSymbolicLink() は entry レベルで確認可能だが、
|
|
45
|
+
// lstat で確実に判定するため個別確認
|
|
46
|
+
if (isSymbolicLink(fullPath)) {
|
|
47
|
+
symlinkPaths.push(relPath);
|
|
48
|
+
} else if (entry.isDirectory()) {
|
|
49
|
+
walkDir(fullPath, relPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(dirPath)) {
|
|
55
|
+
walkDir(dirPath, "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
hasSymlinks: symlinkPaths.length > 0,
|
|
60
|
+
symlinkPaths,
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseSnippetYaml, type SnippetDefinition } from "./snippet-schema.js";
|
|
4
|
+
|
|
5
|
+
export function listRegistrySnippets(registryPath: string): string[] {
|
|
6
|
+
if (!fs.existsSync(registryPath)) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const entries = fs.readdirSync(registryPath);
|
|
10
|
+
return entries
|
|
11
|
+
.filter((e) => e.endsWith(".yaml"))
|
|
12
|
+
.map((e) => e.replace(/\.yaml$/, ""));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function snippetExistsInRegistry(
|
|
16
|
+
registryPath: string,
|
|
17
|
+
name: string,
|
|
18
|
+
): boolean {
|
|
19
|
+
const yamlPath = path.join(registryPath, `${name}.yaml`);
|
|
20
|
+
return fs.existsSync(yamlPath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readSnippetFromRegistry(
|
|
24
|
+
registryPath: string,
|
|
25
|
+
name: string,
|
|
26
|
+
): SnippetDefinition {
|
|
27
|
+
const yamlPath = path.join(registryPath, `${name}.yaml`);
|
|
28
|
+
const content = fs.readFileSync(yamlPath, "utf-8");
|
|
29
|
+
return parseSnippetYaml(content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listTemplateFiles(
|
|
33
|
+
registryPath: string,
|
|
34
|
+
name: string,
|
|
35
|
+
): string[] {
|
|
36
|
+
const dirPath = path.join(registryPath, name);
|
|
37
|
+
if (!fs.existsSync(dirPath)) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return listFilesRecursive(dirPath, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listFilesRecursive(basePath: string, relativePath: string): string[] {
|
|
44
|
+
const fullPath = relativePath
|
|
45
|
+
? path.join(basePath, relativePath)
|
|
46
|
+
: basePath;
|
|
47
|
+
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
48
|
+
const files: string[] = [];
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const entryRelative = relativePath
|
|
52
|
+
? path.join(relativePath, entry.name)
|
|
53
|
+
: entry.name;
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
files.push(...listFilesRecursive(basePath, entryRelative));
|
|
56
|
+
} else {
|
|
57
|
+
files.push(entryRelative);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readTemplateFile(
|
|
65
|
+
registryPath: string,
|
|
66
|
+
name: string,
|
|
67
|
+
filePath: string,
|
|
68
|
+
): string {
|
|
69
|
+
const fullPath = path.join(registryPath, name, filePath);
|
|
70
|
+
return fs.readFileSync(fullPath, "utf-8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function copySnippetToRegistry(
|
|
74
|
+
sourceDir: string,
|
|
75
|
+
sourceYamlPath: string,
|
|
76
|
+
registryPath: string,
|
|
77
|
+
name: string,
|
|
78
|
+
): void {
|
|
79
|
+
fs.mkdirSync(registryPath, { recursive: true });
|
|
80
|
+
|
|
81
|
+
const destYaml = path.join(registryPath, `${name}.yaml`);
|
|
82
|
+
fs.copyFileSync(sourceYamlPath, destYaml);
|
|
83
|
+
|
|
84
|
+
const destDir = path.join(registryPath, name);
|
|
85
|
+
copyDirectoryRecursive(sourceDir, destDir);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function copyDirectoryRecursive(src: string, dest: string): void {
|
|
89
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
90
|
+
|
|
91
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const srcPath = path.join(src, entry.name);
|
|
94
|
+
const destPath = path.join(dest, entry.name);
|
|
95
|
+
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
copyDirectoryRecursive(srcPath, destPath);
|
|
98
|
+
} else {
|
|
99
|
+
fs.copyFileSync(srcPath, destPath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function removeSnippetFromRegistry(
|
|
105
|
+
registryPath: string,
|
|
106
|
+
name: string,
|
|
107
|
+
): void {
|
|
108
|
+
const yamlPath = path.join(registryPath, `${name}.yaml`);
|
|
109
|
+
if (fs.existsSync(yamlPath)) {
|
|
110
|
+
fs.unlinkSync(yamlPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dirPath = path.join(registryPath, name);
|
|
114
|
+
if (fs.existsSync(dirPath)) {
|
|
115
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
}
|