@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.
Files changed (31) hide show
  1. package/package.json +25 -0
  2. package/src/__tests__/errors.test.ts +71 -0
  3. package/src/__tests__/hooks.test.ts +145 -0
  4. package/src/__tests__/i18n.test.ts +60 -0
  5. package/src/__tests__/remote-registry.test.ts +265 -0
  6. package/src/__tests__/safe-yaml-parser.test.ts +187 -0
  7. package/src/__tests__/snapshots/__snapshots__/error-messages.snapshot.test.ts.snap +27 -0
  8. package/src/__tests__/snapshots/__snapshots__/snippet-schema-output.snapshot.test.ts.snap +78 -0
  9. package/src/__tests__/snapshots/__snapshots__/template-output.snapshot.test.ts.snap +45 -0
  10. package/src/__tests__/snapshots/error-messages.snapshot.test.ts +76 -0
  11. package/src/__tests__/snapshots/snippet-schema-output.snapshot.test.ts +98 -0
  12. package/src/__tests__/snapshots/template-output.snapshot.test.ts +73 -0
  13. package/src/__tests__/snippet-schema.test.ts +180 -0
  14. package/src/__tests__/symlink-checker.test.ts +95 -0
  15. package/src/__tests__/template-engine.test.ts +137 -0
  16. package/src/__tests__/validate-name.test.ts +61 -0
  17. package/src/errors.ts +82 -0
  18. package/src/hooks.ts +63 -0
  19. package/src/i18n/index.ts +32 -0
  20. package/src/i18n/locales/en.ts +91 -0
  21. package/src/i18n/locales/ja.ts +91 -0
  22. package/src/i18n/types.ts +91 -0
  23. package/src/index.ts +84 -0
  24. package/src/lib/symlink-checker.ts +62 -0
  25. package/src/registry.ts +117 -0
  26. package/src/remote-registry.ts +260 -0
  27. package/src/safe-yaml-parser.ts +71 -0
  28. package/src/snippet-schema.ts +117 -0
  29. package/src/template-engine.ts +114 -0
  30. package/src/validate-name.ts +12 -0
  31. 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
+ }
@@ -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
+ }