@intellectronica/ruler 0.2.5 → 0.2.7
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/README.md +15 -1
- package/dist/agents/JulesAgent.js +55 -0
- package/dist/cli/commands.js +16 -2
- package/dist/core/ConfigLoader.js +18 -3
- package/dist/core/FileSystemUtils.js +23 -1
- package/dist/lib.js +27 -8
- package/dist/paths/mcp.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
40
40
|
| GitHub Copilot | `.github/copilot-instructions.md` |
|
|
41
41
|
| Claude Code | `CLAUDE.md` |
|
|
42
42
|
| OpenAI Codex CLI | `AGENTS.md` |
|
|
43
|
+
| Jules | `AGENTS.md` |
|
|
43
44
|
| Cursor | `.cursor/rules/ruler_cursor_instructions.mdc` |
|
|
44
45
|
| Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
|
|
45
46
|
| Cline | `.clinerules` |
|
|
@@ -78,13 +79,20 @@ npx @intellectronica/ruler apply
|
|
|
78
79
|
- `.ruler/ruler.toml`: The main configuration file for Ruler
|
|
79
80
|
- `.ruler/mcp.json`: An example MCP server configuration
|
|
80
81
|
|
|
82
|
+
Additionally, you can create a global configuration to use when no local `.ruler/` directory is found:
|
|
83
|
+
```bash
|
|
84
|
+
ruler init --global
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The global configuration will be created to `$XDG_CONFIG_HOME/ruler` (default: `~/.config/ruler`).
|
|
88
|
+
|
|
81
89
|
## Core Concepts
|
|
82
90
|
|
|
83
91
|
### The `.ruler/` Directory
|
|
84
92
|
|
|
85
93
|
This is your central hub for all AI agent instructions:
|
|
86
94
|
|
|
87
|
-
- **Rule Files (`*.md`)**: Discovered recursively from `.ruler/` and alphabetically concatenated
|
|
95
|
+
- **Rule Files (`*.md`)**: Discovered recursively from `.ruler/` or `$XDG_CONFIG_HOME/ruler` and alphabetically concatenated
|
|
88
96
|
- **Concatenation Marker**: Each file's content is prepended with `--- Source: <relative_path_to_md_file> ---` for traceability
|
|
89
97
|
- **`ruler.toml`**: Master configuration for Ruler's behavior, agent selection, and output paths
|
|
90
98
|
- **`mcp.json`**: Shared MCP server settings
|
|
@@ -128,6 +136,8 @@ This is your central hub for all AI agent instructions:
|
|
|
128
136
|
ruler apply [options]
|
|
129
137
|
```
|
|
130
138
|
|
|
139
|
+
The `apply` command looks for `.ruler/` in the current directory tree, reading the first match. If no such directory is found, it will look for a global configuration in `$XDG_CONFIG_HOME/ruler`.
|
|
140
|
+
|
|
131
141
|
### Options
|
|
132
142
|
|
|
133
143
|
| Option | Description |
|
|
@@ -140,6 +150,7 @@ ruler apply [options]
|
|
|
140
150
|
| `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
|
|
141
151
|
| `--gitignore` | Enable automatic .gitignore updates (default: true) |
|
|
142
152
|
| `--no-gitignore` | Disable automatic .gitignore updates |
|
|
153
|
+
| `--local-only` | Do not look for configuration in `$XDG_CONFIG_HOME` |
|
|
143
154
|
| `--verbose` / `-v` | Display detailed output during execution |
|
|
144
155
|
|
|
145
156
|
### Common Examples
|
|
@@ -226,6 +237,9 @@ output_path = ".idx/airules.md"
|
|
|
226
237
|
[agents.gemini-cli]
|
|
227
238
|
enabled = true
|
|
228
239
|
|
|
240
|
+
[agents.jules]
|
|
241
|
+
enabled = true
|
|
242
|
+
|
|
229
243
|
# Agent-specific MCP configuration
|
|
230
244
|
[agents.cursor.mcp]
|
|
231
245
|
enabled = true
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.JulesAgent = void 0;
|
|
37
|
+
const FileSystemUtils_1 = require("../core/FileSystemUtils");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class JulesAgent {
|
|
40
|
+
getIdentifier() {
|
|
41
|
+
return 'jules';
|
|
42
|
+
}
|
|
43
|
+
getName() {
|
|
44
|
+
return 'Jules';
|
|
45
|
+
}
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, rulerMcpJson, agentConfig) {
|
|
47
|
+
const outputPath = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
const absolutePath = path.resolve(projectRoot, outputPath);
|
|
49
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(absolutePath, concatenatedRules);
|
|
50
|
+
}
|
|
51
|
+
getDefaultOutputPath(projectRoot) {
|
|
52
|
+
return path.join(projectRoot, 'AGENTS.md');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.JulesAgent = JulesAgent;
|
package/dist/cli/commands.js
CHANGED
|
@@ -41,6 +41,7 @@ const yargs_1 = __importDefault(require("yargs"));
|
|
|
41
41
|
const helpers_1 = require("yargs/helpers");
|
|
42
42
|
const lib_1 = require("../lib");
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
44
45
|
const fs_1 = require("fs");
|
|
45
46
|
const constants_1 = require("../constants");
|
|
46
47
|
/**
|
|
@@ -90,6 +91,11 @@ function run() {
|
|
|
90
91
|
description: 'Preview changes without writing files',
|
|
91
92
|
default: false,
|
|
92
93
|
});
|
|
94
|
+
y.option('local-only', {
|
|
95
|
+
type: 'boolean',
|
|
96
|
+
description: 'Only search for local .ruler directories, ignore global config',
|
|
97
|
+
default: false,
|
|
98
|
+
});
|
|
93
99
|
}, async (argv) => {
|
|
94
100
|
const projectRoot = argv['project-root'];
|
|
95
101
|
const agents = argv.agents
|
|
@@ -102,6 +108,7 @@ function run() {
|
|
|
102
108
|
: undefined;
|
|
103
109
|
const verbose = argv.verbose;
|
|
104
110
|
const dryRun = argv['dry-run'];
|
|
111
|
+
const localOnly = argv['local-only'];
|
|
105
112
|
// Determine gitignore preference: CLI > TOML > Default (enabled)
|
|
106
113
|
// yargs handles --no-gitignore by setting gitignore to false
|
|
107
114
|
let gitignorePreference;
|
|
@@ -112,7 +119,7 @@ function run() {
|
|
|
112
119
|
gitignorePreference = undefined; // Let TOML/default decide
|
|
113
120
|
}
|
|
114
121
|
try {
|
|
115
|
-
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun);
|
|
122
|
+
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun, localOnly);
|
|
116
123
|
console.log('Ruler apply completed successfully.');
|
|
117
124
|
}
|
|
118
125
|
catch (err) {
|
|
@@ -126,10 +133,17 @@ function run() {
|
|
|
126
133
|
type: 'string',
|
|
127
134
|
description: 'Project root directory',
|
|
128
135
|
default: process.cwd(),
|
|
136
|
+
}).option('global', {
|
|
137
|
+
type: 'boolean',
|
|
138
|
+
description: 'Initialize in global config directory (XDG_CONFIG_HOME/ruler)',
|
|
139
|
+
default: false,
|
|
129
140
|
});
|
|
130
141
|
}, async (argv) => {
|
|
131
142
|
const projectRoot = argv['project-root'];
|
|
132
|
-
const
|
|
143
|
+
const isGlobal = argv['global'];
|
|
144
|
+
const rulerDir = isGlobal
|
|
145
|
+
? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'ruler')
|
|
146
|
+
: path.join(projectRoot, '.ruler');
|
|
133
147
|
await fs_1.promises.mkdir(rulerDir, { recursive: true });
|
|
134
148
|
const instructionsPath = path.join(rulerDir, 'instructions.md');
|
|
135
149
|
const tomlPath = path.join(rulerDir, 'ruler.toml');
|
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.loadConfig = loadConfig;
|
|
40
40
|
const fs_1 = require("fs");
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
42
43
|
const toml_1 = __importDefault(require("@iarna/toml"));
|
|
43
44
|
const zod_1 = require("zod");
|
|
44
45
|
const constants_1 = require("../constants");
|
|
@@ -78,9 +79,23 @@ const rulerConfigSchema = zod_1.z.object({
|
|
|
78
79
|
*/
|
|
79
80
|
async function loadConfig(options) {
|
|
80
81
|
const { projectRoot, configPath, cliAgents } = options;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
let configFile;
|
|
83
|
+
if (configPath) {
|
|
84
|
+
configFile = path.resolve(configPath);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Try local .ruler/ruler.toml first
|
|
88
|
+
const localConfigFile = path.join(projectRoot, '.ruler', 'ruler.toml');
|
|
89
|
+
try {
|
|
90
|
+
await fs_1.promises.access(localConfigFile);
|
|
91
|
+
configFile = localConfigFile;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// If local config doesn't exist, try global config
|
|
95
|
+
const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
96
|
+
configFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
84
99
|
let raw = {};
|
|
85
100
|
try {
|
|
86
101
|
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
@@ -40,11 +40,20 @@ exports.backupFile = backupFile;
|
|
|
40
40
|
exports.ensureDirExists = ensureDirExists;
|
|
41
41
|
const fs_1 = require("fs");
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
/**
|
|
45
|
+
* Gets the XDG config directory path, falling back to ~/.config if XDG_CONFIG_HOME is not set.
|
|
46
|
+
*/
|
|
47
|
+
function getXdgConfigDir() {
|
|
48
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
49
|
+
}
|
|
43
50
|
/**
|
|
44
51
|
* Searches upwards from startPath to find a directory named .ruler.
|
|
52
|
+
* If not found locally and checkGlobal is true, checks for global config at XDG_CONFIG_HOME/ruler.
|
|
45
53
|
* Returns the path to the .ruler directory, or null if not found.
|
|
46
54
|
*/
|
|
47
|
-
async function findRulerDir(startPath) {
|
|
55
|
+
async function findRulerDir(startPath, checkGlobal = true) {
|
|
56
|
+
// First, search upwards from startPath for local .ruler directory
|
|
48
57
|
let current = startPath;
|
|
49
58
|
while (current) {
|
|
50
59
|
const candidate = path.join(current, '.ruler');
|
|
@@ -63,6 +72,19 @@ async function findRulerDir(startPath) {
|
|
|
63
72
|
}
|
|
64
73
|
current = parent;
|
|
65
74
|
}
|
|
75
|
+
// If no local .ruler found and checkGlobal is true, check global config directory
|
|
76
|
+
if (checkGlobal) {
|
|
77
|
+
const globalConfigDir = path.join(getXdgConfigDir(), 'ruler');
|
|
78
|
+
try {
|
|
79
|
+
const stat = await fs_1.promises.stat(globalConfigDir);
|
|
80
|
+
if (stat.isDirectory()) {
|
|
81
|
+
return globalConfigDir;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(`[ruler] Error checking global config directory ${globalConfigDir}:`, err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
66
88
|
return null;
|
|
67
89
|
}
|
|
68
90
|
/**
|
package/dist/lib.js
CHANGED
|
@@ -50,6 +50,7 @@ const AiderAgent_1 = require("./agents/AiderAgent");
|
|
|
50
50
|
const FirebaseAgent_1 = require("./agents/FirebaseAgent");
|
|
51
51
|
const OpenHandsAgent_1 = require("./agents/OpenHandsAgent");
|
|
52
52
|
const GeminiCliAgent_1 = require("./agents/GeminiCliAgent");
|
|
53
|
+
const JulesAgent_1 = require("./agents/JulesAgent");
|
|
53
54
|
const merge_1 = require("./mcp/merge");
|
|
54
55
|
const validate_1 = require("./mcp/validate");
|
|
55
56
|
const mcp_1 = require("./paths/mcp");
|
|
@@ -100,6 +101,7 @@ const agents = [
|
|
|
100
101
|
new FirebaseAgent_1.FirebaseAgent(),
|
|
101
102
|
new OpenHandsAgent_1.OpenHandsAgent(),
|
|
102
103
|
new GeminiCliAgent_1.GeminiCliAgent(),
|
|
104
|
+
new JulesAgent_1.JulesAgent(),
|
|
103
105
|
];
|
|
104
106
|
/**
|
|
105
107
|
* Applies ruler configurations for all supported AI agents.
|
|
@@ -110,7 +112,7 @@ const agents = [
|
|
|
110
112
|
* @param projectRoot Root directory of the project
|
|
111
113
|
* @param includedAgents Optional list of agent name filters (case-insensitive substrings)
|
|
112
114
|
*/
|
|
113
|
-
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false) {
|
|
115
|
+
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false, localOnly = false) {
|
|
114
116
|
// Load configuration (default_agents, per-agent overrides, CLI filters)
|
|
115
117
|
(0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
|
|
116
118
|
if (configPath) {
|
|
@@ -137,13 +139,13 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
config.agentConfigs = mappedConfigs;
|
|
140
|
-
const rulerDir = await FileSystemUtils.findRulerDir(projectRoot);
|
|
142
|
+
const rulerDir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
|
|
141
143
|
if (!rulerDir) {
|
|
142
144
|
throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
|
|
143
145
|
}
|
|
144
146
|
(0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
|
|
145
147
|
const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
|
|
146
|
-
(0, constants_1.logVerbose)(`Found ${files.length} markdown files in
|
|
148
|
+
(0, constants_1.logVerbose)(`Found ${files.length} markdown files in ruler configuration directory`, verbose);
|
|
147
149
|
const concatenated = (0, RuleProcessor_1.concatenateRules)(files);
|
|
148
150
|
(0, constants_1.logVerbose)(`Concatenated rules length: ${concatenated.length} characters`, verbose);
|
|
149
151
|
const mcpFile = path.join(rulerDir, 'mcp.json');
|
|
@@ -187,6 +189,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
187
189
|
}
|
|
188
190
|
// Collect all generated file paths for .gitignore
|
|
189
191
|
const generatedPaths = [];
|
|
192
|
+
let agentsMdWritten = false;
|
|
190
193
|
for (const agent of selected) {
|
|
191
194
|
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
192
195
|
console.log(`${actionPrefix} Applying rules for ${agent.getName()}...`);
|
|
@@ -196,10 +199,20 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
196
199
|
const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
|
|
197
200
|
(0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
|
|
198
201
|
generatedPaths.push(...outputPaths);
|
|
202
|
+
// Also add the backup file paths to the gitignore list
|
|
203
|
+
const backupPaths = outputPaths.map((p) => `${p}.bak`);
|
|
204
|
+
generatedPaths.push(...backupPaths);
|
|
199
205
|
if (dryRun) {
|
|
200
206
|
(0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose);
|
|
201
207
|
}
|
|
202
208
|
else {
|
|
209
|
+
if (agent.getIdentifier() === 'jules' ||
|
|
210
|
+
agent.getIdentifier() === 'codex') {
|
|
211
|
+
if (agentsMdWritten) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
agentsMdWritten = true;
|
|
215
|
+
}
|
|
203
216
|
await agent.applyRulerConfig(concatenated, projectRoot, rulerMcpJson, agentConfig);
|
|
204
217
|
}
|
|
205
218
|
const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
|
|
@@ -207,6 +220,13 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
207
220
|
(agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
|
|
208
221
|
const rulerMcpFile = path.join(rulerDir, 'mcp.json');
|
|
209
222
|
if (dest && mcpEnabledForAgent) {
|
|
223
|
+
// Include MCP config file in .gitignore only if it's within the project directory
|
|
224
|
+
if (dest.startsWith(projectRoot)) {
|
|
225
|
+
const relativeDest = path.relative(projectRoot, dest);
|
|
226
|
+
generatedPaths.push(relativeDest);
|
|
227
|
+
// Also add the backup for the MCP file
|
|
228
|
+
generatedPaths.push(`${relativeDest}.bak`);
|
|
229
|
+
}
|
|
210
230
|
if (agent.getIdentifier() === 'openhands') {
|
|
211
231
|
// *** Special handling for Open Hands ***
|
|
212
232
|
if (dryRun) {
|
|
@@ -215,8 +235,7 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
215
235
|
else {
|
|
216
236
|
await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(rulerMcpFile, dest);
|
|
217
237
|
}
|
|
218
|
-
//
|
|
219
|
-
generatedPaths.push(dest);
|
|
238
|
+
// Open Hands config file is already included above
|
|
220
239
|
}
|
|
221
240
|
else {
|
|
222
241
|
if (rulerMcpJson) {
|
|
@@ -252,9 +271,9 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
252
271
|
gitignoreEnabled = true; // Default enabled
|
|
253
272
|
}
|
|
254
273
|
if (gitignoreEnabled && generatedPaths.length > 0) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
const uniquePaths = [...new Set(generatedPaths)];
|
|
275
|
+
// Add wildcard pattern for backup files
|
|
276
|
+
uniquePaths.push('*.bak');
|
|
258
277
|
if (uniquePaths.length > 0) {
|
|
259
278
|
const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
|
|
260
279
|
if (dryRun) {
|
package/dist/paths/mcp.js
CHANGED
|
@@ -71,6 +71,9 @@ async function getNativeMcpPath(adapterName, projectRoot) {
|
|
|
71
71
|
// For Open Hands, we target the main config file, not a separate mcp.json
|
|
72
72
|
candidates.push(path.join(projectRoot, '.openhands', 'config.toml'));
|
|
73
73
|
break;
|
|
74
|
+
case 'Gemini CLI':
|
|
75
|
+
candidates.push(path.join(projectRoot, '.gemini', 'settings.json'));
|
|
76
|
+
break;
|
|
74
77
|
default:
|
|
75
78
|
return null;
|
|
76
79
|
}
|