Skip to content

Commit e260b21

Browse files
committed
config.yamlを読むように変更
1 parent 16f6ea3 commit e260b21

File tree

8 files changed

+166
-41
lines changed

8 files changed

+166
-41
lines changed

.claude/settings.local.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"Bash(npm run dev:*)",
77
"Bash(npm run tauri:build:*)",
88
"Bash(npm run build:*)",
9-
"Bash(npm:*)",
109
"mcp__playwright__browser_navigate",
1110
"mcp__playwright__browser_click",
1211
"mcp__playwright__browser_snapshot",
@@ -22,9 +21,9 @@
2221
"Bash(npx c8 report:*)",
2322
"Bash(rg:*)",
2423
"Bash(npx tsc:*)",
25-
"Bash(git add:*)"
26-
"mcp__playwright__browser_take_screenshot"
27-
]
28-
},
29-
"enableAllProjectMcpServers": false
24+
"Bash(git add:*)",
25+
"mcp__playwright__browser_take_screenshot",
26+
],
27+
"deny": []
28+
}
3029
}

frontend/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
} from './utils/fileSystem';
1313
import {
1414
loadSampleCommandsYaml,
15-
loadSampleSkit
15+
loadSampleSkit,
16+
loadSampleConfig
1617
} from './utils/devFileSystem';
1718
import { Toaster } from 'sonner';
1819
import { DndProvider } from './components/dnd/DndProvider';
@@ -53,6 +54,9 @@ function App() {
5354
}
5455

5556
try {
57+
// 設定ファイルを読み込み、そこからcommands.yamlのパスを取得
58+
await loadSampleConfig();
59+
// 開発環境では設定に関係なく直接commands.yamlを読み込む
5660
const commandsYaml = await loadSampleCommandsYaml();
5761
loadCommandsYaml(commandsYaml);
5862
} catch (e) {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version: 1
2+
projectName: "Sample Project"
3+
commandsSchema: "commands2.yaml"

frontend/src/types/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,9 @@ export interface Skit {
6464
meta: SkitMeta;
6565
commands: SkitCommand[];
6666
}
67+
68+
export interface CommandForgeConfig {
69+
version: number;
70+
projectName?: string;
71+
commandsSchema: string;
72+
}

frontend/src/utils/configLoader.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { readTextFile, exists } from '@tauri-apps/api/fs';
2+
import { join } from '@tauri-apps/api/path';
3+
import { parse } from 'yaml';
4+
import Ajv from 'ajv';
5+
import { CommandForgeConfig } from '../types';
6+
7+
const ajv = new Ajv();
8+
9+
const configSchema = {
10+
type: 'object',
11+
required: ['version', 'commandsSchema'],
12+
properties: {
13+
version: { type: 'number' },
14+
projectName: { type: 'string' },
15+
commandsSchema: { type: 'string' }
16+
}
17+
};
18+
19+
const validateConfig = ajv.compile(configSchema);
20+
21+
/**
22+
* Loads and validates the commandForgeEditor.config.yml file
23+
* @param projectPath The project path
24+
* @returns Promise with the configuration
25+
*/
26+
export async function loadConfigFile(projectPath: string): Promise<CommandForgeConfig> {
27+
try {
28+
// Web環境の場合は例外をスロー
29+
if (import.meta.env.DEV && typeof window !== 'undefined' && !((window as unknown) as { __TAURI__: unknown }).__TAURI__) {
30+
throw new Error('Running in web environment, Tauri API not available');
31+
}
32+
33+
const configPath = await join(projectPath, 'commandForgeEditor.config.yml');
34+
35+
if (!(await exists(configPath))) {
36+
throw new Error(`Configuration file not found at ${configPath}`);
37+
}
38+
39+
const configContent = await readTextFile(configPath);
40+
const config = parse(configContent) as CommandForgeConfig;
41+
42+
if (!validateConfig(config)) {
43+
const errors = validateConfig.errors?.map(err => `${err.instancePath} ${err.message}`).join(', ');
44+
throw new Error(`Invalid configuration: ${errors}`);
45+
}
46+
47+
return config;
48+
} catch (error) {
49+
console.error('Failed to load configuration file:', error);
50+
throw error;
51+
}
52+
}
53+
54+
/**
55+
* Validates a configuration object
56+
* @param config The configuration to validate
57+
* @returns Array of validation errors, empty if valid
58+
*/
59+
export function validateCommandForgeConfig(config: unknown): string[] {
60+
if (!validateConfig(config)) {
61+
return validateConfig.errors?.map(err => `${err.instancePath} ${err.message}`) || ['Unknown validation error'];
62+
}
63+
return [];
64+
}

frontend/src/utils/devFileSystem.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Skit } from '../types';
1+
import { Skit, CommandForgeConfig } from '../types';
2+
import { parse } from 'yaml';
23

34
/**
45
* 開発環境用: サンプルcommands.yamlファイルを読み込む
@@ -43,4 +44,27 @@ export async function loadSampleSkit(): Promise<Record<string, Skit>> {
4344
console.error('Failed to load sample-skit.json:', error);
4445
throw error;
4546
}
47+
}
48+
49+
/**
50+
* 開発環境用: サンプルcommandForgeEditor.config.ymlファイルを読み込む
51+
* @returns Promise with the configuration
52+
*/
53+
export async function loadSampleConfig(): Promise<CommandForgeConfig> {
54+
try {
55+
console.log('Loading commandForgeEditor.config.yml for web environment');
56+
57+
// Web環境でfetchを使用してファイルをロード
58+
const response = await fetch('/src/sample/commandForgeEditor.config.yml');
59+
if (!response.ok) {
60+
throw new Error(`Failed to fetch commandForgeEditor.config.yml: ${response.status}`);
61+
}
62+
const content = await response.text();
63+
const config = parse(content) as CommandForgeConfig;
64+
console.log('Successfully loaded commandForgeEditor.config.yml');
65+
return config;
66+
} catch (error) {
67+
console.error('Failed to load commandForgeEditor.config.yml:', error);
68+
throw error;
69+
}
4670
}

frontend/src/utils/fileSystem.test.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {
1111
createNewSkit
1212
} from './fileSystem';
1313
import * as validation from './validation';
14+
import * as configLoader from './configLoader';
1415
import { Skit } from '../types';
1516

1617
vi.mock('@tauri-apps/api/fs');
1718
vi.mock('@tauri-apps/api/path');
1819
vi.mock('@tauri-apps/api/dialog');
1920
vi.mock('./validation');
21+
vi.mock('./configLoader');
2022

2123
describe('fileSystem', () => {
2224
beforeEach(() => {
@@ -77,38 +79,37 @@ describe('fileSystem', () => {
7779

7880
describe('loadCommandsYaml', () => {
7981
it('should load from project path when available', async () => {
82+
vi.mocked(configLoader.loadConfigFile).mockResolvedValue({
83+
version: 1,
84+
commandsSchema: 'commands.yaml'
85+
});
8086
vi.mocked(path.join).mockResolvedValue('/project/commands.yaml');
8187
vi.mocked(fs.exists).mockResolvedValue(true);
8288
vi.mocked(fs.readTextFile).mockResolvedValue('version: 1\ncommands: []');
8389

8490
const result = await loadCommandsYaml('/project');
8591
expect(result).toBe('version: 1\ncommands: []');
92+
expect(configLoader.loadConfigFile).toHaveBeenCalledWith('/project');
8693
expect(path.join).toHaveBeenCalledWith('/project', 'commands.yaml');
8794
});
8895

89-
it('should fall back to resource path when project path not found', async () => {
90-
vi.mocked(path.join).mockResolvedValue('/project/commands.yaml');
91-
vi.mocked(fs.exists).mockResolvedValue(false);
92-
vi.mocked(path.resolveResource).mockResolvedValue('/resources/commands.yaml');
93-
vi.mocked(fs.readTextFile).mockResolvedValue('default commands');
96+
it('should fail when config file not found', async () => {
97+
vi.mocked(configLoader.loadConfigFile).mockRejectedValue(
98+
new Error('Configuration file not found')
99+
);
94100

95-
const result = await loadCommandsYaml('/project');
96-
expect(result).toBe('default commands');
97-
expect(path.resolveResource).toHaveBeenCalledWith('commands.yaml');
101+
await expect(loadCommandsYaml('/project')).rejects.toThrow('Configuration file not found');
102+
expect(configLoader.loadConfigFile).toHaveBeenCalledWith('/project');
98103
});
99104

100-
it('should load from resource path when no project path', async () => {
101-
vi.mocked(path.resolveResource).mockResolvedValue('/resources/commands.yaml');
102-
vi.mocked(fs.readTextFile).mockResolvedValue('default commands');
103-
104-
const result = await loadCommandsYaml();
105-
expect(result).toBe('default commands');
105+
it('should throw error when no project path', async () => {
106+
await expect(loadCommandsYaml()).rejects.toThrow('Project path is required to load commands.yaml');
106107
});
107108

108109
it('should throw error on failure', async () => {
109-
vi.mocked(path.resolveResource).mockRejectedValue(new Error('File not found'));
110+
vi.mocked(configLoader.loadConfigFile).mockRejectedValue(new Error('File not found'));
110111

111-
await expect(loadCommandsYaml()).rejects.toThrow('File not found');
112+
await expect(loadCommandsYaml('/project')).rejects.toThrow('File not found');
112113
});
113114

114115
it('should throw error in web environment', async () => {
@@ -202,6 +203,11 @@ describe('fileSystem', () => {
202203
it('should apply default background colors from commands.yaml', async () => {
203204
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
204205

206+
vi.mocked(configLoader.loadConfigFile).mockResolvedValue({
207+
version: 1,
208+
commandsSchema: 'commands.yaml'
209+
});
210+
205211
vi.mocked(validation.validateCommandsYaml).mockReturnValue({
206212
config: {
207213
version: 1,

frontend/src/utils/fileSystem.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join, resolveResource } from '@tauri-apps/api/path';
33
import { open } from '@tauri-apps/api/dialog';
44
import { Skit, CommandDefinition, SkitCommand } from '../types';
55
import { validateSkitData, validateCommandsYaml } from './validation';
6+
import { loadConfigFile } from './configLoader';
67
// 未使用のimportを削除
78

89
/**
@@ -29,7 +30,7 @@ export async function selectProjectFolder(): Promise<string | null> {
2930
}
3031

3132
/**
32-
* Loads commands.yaml file from either project path or resources
33+
* Loads commands.yaml file based on configuration
3334
* @param projectPath Optional project path
3435
* @returns Promise with the commands.yaml content
3536
*/
@@ -40,16 +41,20 @@ export async function loadCommandsYaml(projectPath: string | null = null): Promi
4041
throw new Error('Running in web environment, Tauri API not available');
4142
}
4243

43-
let commandsYamlPath;
44+
if (!projectPath) {
45+
throw new Error('Project path is required to load commands.yaml');
46+
}
4447

45-
if (projectPath) {
46-
commandsYamlPath = await join(projectPath, 'commands.yaml');
47-
if (await exists(commandsYamlPath)) {
48-
return await readTextFile(commandsYamlPath);
49-
}
48+
// Load configuration file first
49+
const config = await loadConfigFile(projectPath);
50+
51+
// Get commands.yaml path from config
52+
const commandsYamlPath = await join(projectPath, config.commandsSchema);
53+
54+
if (!(await exists(commandsYamlPath))) {
55+
throw new Error(`Commands schema file not found at ${commandsYamlPath}`);
5056
}
5157

52-
commandsYamlPath = await resolveResource('commands.yaml');
5358
return await readTextFile(commandsYamlPath);
5459
} catch (error) {
5560
console.error('Failed to load commands.yaml:', error);
@@ -81,12 +86,12 @@ export async function loadSkits(projectPath: string | null = null): Promise<Reco
8186
return {}; // Return empty object since directory was just created
8287
}
8388

84-
return await loadSkitsFromPath(skitsPath);
89+
return await loadSkitsFromPath(skitsPath, projectPath);
8590
}
8691

8792
skitsPath = await resolveResource('skits');
8893
// as it should be part of the bundled resources
89-
return await loadSkitsFromPath(skitsPath);
94+
return await loadSkitsFromPath(skitsPath, null);
9095
} catch (error) {
9196
console.error('Failed to load skits:', error);
9297
return {}; // Return empty object on error instead of throwing
@@ -96,22 +101,36 @@ export async function loadSkits(projectPath: string | null = null): Promise<Reco
96101
/**
97102
* Helper function to load skits from a specific path
98103
* @param skitsPath Path to the skits directory
104+
* @param projectPath Optional project path for loading config
99105
* @returns Promise with a record of skits
100106
*/
101-
async function loadSkitsFromPath(skitsPath: string): Promise<Record<string, Skit>> {
107+
async function loadSkitsFromPath(skitsPath: string, projectPath: string | null): Promise<Record<string, Skit>> {
102108
try {
103109
const skitFiles = await readDir(skitsPath);
104110
const skits: Record<string, Skit> = {};
105111

106112
// Load commands.yaml to get defaultBackgroundColor for each command type
107113
let commandsConfig;
108114
try {
109-
const commandsYamlPath = await join(skitsPath, '../commands.yaml');
110-
if (await exists(commandsYamlPath)) {
111-
const commandsYaml = await readTextFile(commandsYamlPath);
112-
const { validateCommandsYaml } = await import('./validation');
113-
const { config } = validateCommandsYaml(commandsYaml);
114-
commandsConfig = config;
115+
if (projectPath) {
116+
// Load config to get commands.yaml path
117+
const config = await loadConfigFile(projectPath);
118+
const commandsYamlPath = await join(projectPath, config.commandsSchema);
119+
if (await exists(commandsYamlPath)) {
120+
const commandsYaml = await readTextFile(commandsYamlPath);
121+
const { validateCommandsYaml } = await import('./validation');
122+
const { config: yamlConfig } = validateCommandsYaml(commandsYaml);
123+
commandsConfig = yamlConfig;
124+
}
125+
} else {
126+
// Fallback for bundled resources
127+
const commandsYamlPath = await join(skitsPath, '../commands.yaml');
128+
if (await exists(commandsYamlPath)) {
129+
const commandsYaml = await readTextFile(commandsYamlPath);
130+
const { validateCommandsYaml } = await import('./validation');
131+
const { config: yamlConfig } = validateCommandsYaml(commandsYaml);
132+
commandsConfig = yamlConfig;
133+
}
115134
}
116135
} catch (error) {
117136
console.error('Failed to load commands.yaml for background colors:', error);

0 commit comments

Comments
 (0)