opencode-pilot 0.3.0 → 0.5.0
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 +18 -1
- package/examples/config.yaml +70 -45
- package/examples/templates/devcontainer.md +9 -0
- package/package.json +1 -1
- package/service/poll-service.js +3 -3
- package/service/poller.js +23 -7
- package/service/presets/github.yaml +37 -0
- package/service/presets/index.js +157 -0
- package/service/presets/linear.yaml +19 -0
- package/service/repo-config.js +79 -3
- package/test/unit/poller.test.js +59 -0
- package/test/unit/repo-config.test.js +291 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Automation daemon for [OpenCode](https://github.com/sst/opencode) - polls for wo
|
|
|
9
9
|
- **Polling automation** - Automatically start sessions from GitHub issues, Linear tickets, etc.
|
|
10
10
|
- **Readiness evaluation** - Check labels, dependencies, and priority before starting work
|
|
11
11
|
- **Template-based prompts** - Customize prompts with placeholders for issue data
|
|
12
|
+
- **Built-in presets** - Common patterns like "my GitHub issues" work out of the box
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -44,10 +45,26 @@ See [examples/config.yaml](examples/config.yaml) for a complete example with all
|
|
|
44
45
|
|
|
45
46
|
### Key Sections
|
|
46
47
|
|
|
47
|
-
- **`
|
|
48
|
+
- **`defaults`** - Default values applied to all sources
|
|
49
|
+
- **`sources`** - What to poll (presets, shorthand, or full config)
|
|
48
50
|
- **`tools`** - Field mappings to normalize different MCP APIs
|
|
49
51
|
- **`repos`** - Repository paths and settings (use YAML anchors to share config)
|
|
50
52
|
|
|
53
|
+
### Source Syntax
|
|
54
|
+
|
|
55
|
+
Three ways to configure sources, from simplest to most flexible:
|
|
56
|
+
|
|
57
|
+
1. **Presets** - Built-in definitions for common patterns (`github/my-issues`, `github/review-requests`, etc.)
|
|
58
|
+
2. **GitHub shorthand** - Simple `github: "query"` syntax for custom GitHub searches
|
|
59
|
+
3. **Full syntax** - Complete control with `tool`, `args`, and `item` for any MCP source
|
|
60
|
+
|
|
61
|
+
### Available Presets
|
|
62
|
+
|
|
63
|
+
- `github/my-issues` - Issues assigned to me
|
|
64
|
+
- `github/review-requests` - PRs needing my review
|
|
65
|
+
- `github/my-prs-feedback` - My PRs with change requests
|
|
66
|
+
- `linear/my-issues` - Linear tickets (requires `teamId`, `assigneeId`)
|
|
67
|
+
|
|
51
68
|
### Prompt Templates
|
|
52
69
|
|
|
53
70
|
Create prompt templates as markdown files in `~/.config/opencode-pilot/templates/`. Templates support placeholders like `{title}`, `{body}`, `{number}`, `{html_url}`, etc.
|
package/examples/config.yaml
CHANGED
|
@@ -4,63 +4,88 @@
|
|
|
4
4
|
# Also create templates/ directory with prompt template files
|
|
5
5
|
|
|
6
6
|
# =============================================================================
|
|
7
|
-
#
|
|
7
|
+
# DEFAULTS - Applied to all sources (source values override these)
|
|
8
8
|
# =============================================================================
|
|
9
|
-
|
|
9
|
+
defaults:
|
|
10
|
+
agent: plan # Default agent for all sources
|
|
11
|
+
prompt: default # Default prompt template
|
|
10
12
|
|
|
11
13
|
# =============================================================================
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
linear:
|
|
19
|
-
mappings:
|
|
20
|
-
body: title # Use title as body
|
|
21
|
-
number: "url:/([A-Z0-9]+-[0-9]+)/" # Extract PROJ-123 from URL
|
|
22
|
-
|
|
23
|
-
# =============================================================================
|
|
24
|
-
# SOURCES - What to poll (generic MCP tool references)
|
|
14
|
+
# SOURCES - What to poll
|
|
15
|
+
#
|
|
16
|
+
# Three syntax options:
|
|
17
|
+
# 1. Presets: Use built-in presets for common patterns
|
|
18
|
+
# 2. Shorthand: Use `github: "query"` for GitHub sources
|
|
19
|
+
# 3. Full: Specify tool, args, item for custom sources
|
|
25
20
|
# =============================================================================
|
|
26
21
|
sources:
|
|
22
|
+
# ----- PRESET SYNTAX (recommended for common patterns) -----
|
|
23
|
+
|
|
27
24
|
# GitHub issues assigned to me
|
|
28
|
-
-
|
|
29
|
-
tool:
|
|
30
|
-
mcp: github
|
|
31
|
-
name: search_issues
|
|
32
|
-
args:
|
|
33
|
-
q: "is:issue assignee:@me state:open"
|
|
34
|
-
item:
|
|
35
|
-
id: "{html_url}"
|
|
25
|
+
- preset: github/my-issues
|
|
36
26
|
prompt: worktree
|
|
37
|
-
agent: plan
|
|
38
27
|
|
|
39
|
-
# GitHub PRs needing review
|
|
40
|
-
-
|
|
41
|
-
tool:
|
|
42
|
-
mcp: github
|
|
43
|
-
name: search_issues
|
|
44
|
-
args:
|
|
45
|
-
q: "is:pr review-requested:@me state:open"
|
|
46
|
-
item:
|
|
47
|
-
id: "{html_url}"
|
|
28
|
+
# GitHub PRs needing my review
|
|
29
|
+
- preset: github/review-requests
|
|
48
30
|
prompt: review
|
|
49
|
-
agent: plan
|
|
50
31
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
32
|
+
# My PRs that have change requests
|
|
33
|
+
- preset: github/my-prs-feedback
|
|
34
|
+
prompt: review-feedback
|
|
35
|
+
|
|
36
|
+
# Linear issues (requires teamId and assigneeId)
|
|
37
|
+
# Find IDs via: opencode-pilot discover linear
|
|
38
|
+
- preset: linear/my-issues
|
|
58
39
|
args:
|
|
59
40
|
teamId: "your-team-uuid" # Replace with actual team UUID
|
|
60
41
|
assigneeId: "your-user-uuid" # Replace with actual user UUID
|
|
61
|
-
status: "
|
|
62
|
-
item:
|
|
63
|
-
id: "linear:{id}"
|
|
42
|
+
status: "In Progress"
|
|
64
43
|
working_dir: ~/code/myproject
|
|
65
44
|
prompt: worktree
|
|
66
|
-
|
|
45
|
+
|
|
46
|
+
# ----- SHORTHAND SYNTAX (for custom GitHub queries) -----
|
|
47
|
+
|
|
48
|
+
# Custom GitHub query with shorthand
|
|
49
|
+
# - name: urgent-issues
|
|
50
|
+
# github: "is:issue assignee:@me label:urgent state:open"
|
|
51
|
+
# prompt: worktree
|
|
52
|
+
|
|
53
|
+
# ----- FULL SYNTAX (for non-GitHub sources or full control) -----
|
|
54
|
+
|
|
55
|
+
# Apple Reminders example (requires tools config below)
|
|
56
|
+
# - name: agent-tasks
|
|
57
|
+
# tool:
|
|
58
|
+
# mcp: apple-reminders
|
|
59
|
+
# name: list_reminders
|
|
60
|
+
# args:
|
|
61
|
+
# list_name: "Agent Tasks"
|
|
62
|
+
# completed: false
|
|
63
|
+
# item:
|
|
64
|
+
# id: "reminder:{id}"
|
|
65
|
+
# prompt: agent-planning
|
|
66
|
+
# working_dir: ~
|
|
67
|
+
|
|
68
|
+
# =============================================================================
|
|
69
|
+
# TOOLS - Field mappings for custom MCP servers (OPTIONAL)
|
|
70
|
+
#
|
|
71
|
+
# Note: GitHub and Linear presets include built-in mappings.
|
|
72
|
+
# Only needed for custom sources or to override preset defaults.
|
|
73
|
+
#
|
|
74
|
+
# Each tool provider can specify:
|
|
75
|
+
# response_key: Key to extract array from response (e.g., "reminders")
|
|
76
|
+
# mappings: Field name translations (e.g., { body: "notes" })
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# tools:
|
|
79
|
+
# apple-reminders:
|
|
80
|
+
# response_key: reminders
|
|
81
|
+
# mappings:
|
|
82
|
+
# title: name
|
|
83
|
+
# body: notes
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Available presets (no tools config needed):
|
|
87
|
+
# github/my-issues - Issues assigned to me
|
|
88
|
+
# github/review-requests - PRs needing my review
|
|
89
|
+
# github/my-prs-feedback - My PRs with change requests
|
|
90
|
+
# linear/my-issues - Linear tickets (requires teamId, assigneeId)
|
|
91
|
+
# =============================================================================
|
package/package.json
CHANGED
package/service/poll-service.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 5. Track processed items to avoid duplicates
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { loadRepoConfig, getRepoConfig, getAllSources,
|
|
12
|
+
import { loadRepoConfig, getRepoConfig, getAllSources, getToolProviderConfig } from "./repo-config.js";
|
|
13
13
|
import { createPoller, pollGenericSource } from "./poller.js";
|
|
14
14
|
import { evaluateReadiness, sortByPriority } from "./readiness.js";
|
|
15
15
|
import { executeAction, buildCommand } from "./actions.js";
|
|
@@ -100,8 +100,8 @@ export async function pollOnce(options = {}) {
|
|
|
100
100
|
// Fetch items from source
|
|
101
101
|
if (!skipMcp) {
|
|
102
102
|
try {
|
|
103
|
-
const
|
|
104
|
-
items = await pollGenericSource(source, {
|
|
103
|
+
const toolProviderConfig = getToolProviderConfig(source.tool.mcp);
|
|
104
|
+
items = await pollGenericSource(source, { toolProviderConfig });
|
|
105
105
|
debug(`Fetched ${items.length} items from ${sourceName}`);
|
|
106
106
|
} catch (err) {
|
|
107
107
|
console.error(`[poll] Error fetching from ${sourceName}: ${err.message}`);
|
package/service/poller.js
CHANGED
|
@@ -107,14 +107,28 @@ export function transformItems(items, idTemplate) {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Parse JSON text as an array with error handling
|
|
110
|
+
* @param {string} text - JSON text to parse
|
|
111
|
+
* @param {string} sourceName - Source name for error logging
|
|
112
|
+
* @param {string} [responseKey] - Key to extract array from response object
|
|
113
|
+
* @returns {Array} Parsed array of items
|
|
110
114
|
*/
|
|
111
|
-
function parseJsonArray(text, sourceName) {
|
|
115
|
+
export function parseJsonArray(text, sourceName, responseKey) {
|
|
112
116
|
try {
|
|
113
117
|
const data = JSON.parse(text);
|
|
118
|
+
|
|
119
|
+
// If already an array, return it
|
|
114
120
|
if (Array.isArray(data)) return data;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
121
|
+
|
|
122
|
+
// If response_key is configured, use it to extract the array
|
|
123
|
+
if (responseKey) {
|
|
124
|
+
const items = data[responseKey];
|
|
125
|
+
if (Array.isArray(items)) return items;
|
|
126
|
+
// response_key was specified but not found or not an array
|
|
127
|
+
console.error(`[poller] Response key '${responseKey}' not found or not an array in ${sourceName} response`);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No response_key - wrap single object as array
|
|
118
132
|
return [data];
|
|
119
133
|
} catch (err) {
|
|
120
134
|
console.error(`[poller] Failed to parse ${sourceName} response:`, err.message);
|
|
@@ -206,13 +220,15 @@ function createTimeout(ms, operation) {
|
|
|
206
220
|
* @param {object} [options] - Additional options
|
|
207
221
|
* @param {number} [options.timeout] - Timeout in ms (default: 30000)
|
|
208
222
|
* @param {string} [options.opencodeConfigPath] - Path to opencode.json for MCP config
|
|
209
|
-
* @param {object} [options.
|
|
223
|
+
* @param {object} [options.toolProviderConfig] - Tool provider config (response_key, mappings)
|
|
210
224
|
* @returns {Promise<Array>} Array of items from the source with IDs and mappings applied
|
|
211
225
|
*/
|
|
212
226
|
export async function pollGenericSource(source, options = {}) {
|
|
213
227
|
const toolConfig = getToolConfig(source);
|
|
214
228
|
const timeout = options.timeout || DEFAULT_MCP_TIMEOUT;
|
|
215
|
-
const
|
|
229
|
+
const toolProviderConfig = options.toolProviderConfig || {};
|
|
230
|
+
const responseKey = toolProviderConfig.response_key;
|
|
231
|
+
const mappings = toolProviderConfig.mappings || null;
|
|
216
232
|
const mcpConfig = getMcpConfig(toolConfig.mcpServer, options.opencodeConfigPath);
|
|
217
233
|
const client = new Client({ name: "opencode-pilot", version: "1.0.0" });
|
|
218
234
|
|
|
@@ -235,7 +251,7 @@ export async function pollGenericSource(source, options = {}) {
|
|
|
235
251
|
const text = result.content?.[0]?.text;
|
|
236
252
|
if (!text) return [];
|
|
237
253
|
|
|
238
|
-
const rawItems = parseJsonArray(text, source.name);
|
|
254
|
+
const rawItems = parseJsonArray(text, source.name, responseKey);
|
|
239
255
|
|
|
240
256
|
// Apply field mappings before transforming
|
|
241
257
|
const mappedItems = mappings
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# GitHub source presets
|
|
2
|
+
|
|
3
|
+
# Provider-level config (applies to all GitHub presets)
|
|
4
|
+
_provider:
|
|
5
|
+
response_key: items
|
|
6
|
+
mappings: {}
|
|
7
|
+
|
|
8
|
+
# Presets
|
|
9
|
+
my-issues:
|
|
10
|
+
name: my-issues
|
|
11
|
+
tool:
|
|
12
|
+
mcp: github
|
|
13
|
+
name: search_issues
|
|
14
|
+
args:
|
|
15
|
+
q: "is:issue assignee:@me state:open"
|
|
16
|
+
item:
|
|
17
|
+
id: "{html_url}"
|
|
18
|
+
|
|
19
|
+
review-requests:
|
|
20
|
+
name: review-requests
|
|
21
|
+
tool:
|
|
22
|
+
mcp: github
|
|
23
|
+
name: search_issues
|
|
24
|
+
args:
|
|
25
|
+
q: "is:pr review-requested:@me state:open"
|
|
26
|
+
item:
|
|
27
|
+
id: "{html_url}"
|
|
28
|
+
|
|
29
|
+
my-prs-feedback:
|
|
30
|
+
name: my-prs-feedback
|
|
31
|
+
tool:
|
|
32
|
+
mcp: github
|
|
33
|
+
name: search_issues
|
|
34
|
+
args:
|
|
35
|
+
q: "is:pr author:@me state:open review:changes_requested"
|
|
36
|
+
item:
|
|
37
|
+
id: "{html_url}"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presets/index.js - Built-in source presets for common patterns
|
|
3
|
+
*
|
|
4
|
+
* Presets reduce config verbosity by providing sensible defaults
|
|
5
|
+
* for common polling sources like GitHub issues and Linear tickets.
|
|
6
|
+
*
|
|
7
|
+
* Presets are loaded from YAML files in this directory.
|
|
8
|
+
* Each file can include a _provider key with provider-level config
|
|
9
|
+
* (response_key, mappings) that applies to all presets for that provider.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import YAML from "yaml";
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
// Cache for loaded provider data
|
|
20
|
+
const providerCache = {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load provider data from a YAML file
|
|
24
|
+
* @param {string} provider - Provider name (e.g., "github", "linear")
|
|
25
|
+
* @returns {object} Provider data with presets and _provider config
|
|
26
|
+
*/
|
|
27
|
+
function loadProviderData(provider) {
|
|
28
|
+
if (providerCache[provider]) {
|
|
29
|
+
return providerCache[provider];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const filePath = path.join(__dirname, `${provider}.yaml`);
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
providerCache[provider] = { presets: {}, providerConfig: null };
|
|
35
|
+
return providerCache[provider];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
40
|
+
const data = YAML.parse(content) || {};
|
|
41
|
+
|
|
42
|
+
// Extract _provider config and presets
|
|
43
|
+
const { _provider, ...presets } = data;
|
|
44
|
+
|
|
45
|
+
providerCache[provider] = {
|
|
46
|
+
presets,
|
|
47
|
+
providerConfig: _provider || null,
|
|
48
|
+
};
|
|
49
|
+
return providerCache[provider];
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`Warning: Failed to load presets from ${filePath}: ${err.message}`);
|
|
52
|
+
providerCache[provider] = { presets: {}, providerConfig: null };
|
|
53
|
+
return providerCache[provider];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the full presets registry from YAML files
|
|
59
|
+
* Format: "provider/preset-name" -> preset config
|
|
60
|
+
*/
|
|
61
|
+
function buildPresetsRegistry() {
|
|
62
|
+
const registry = {};
|
|
63
|
+
const providers = ["github", "linear"];
|
|
64
|
+
|
|
65
|
+
for (const provider of providers) {
|
|
66
|
+
const { presets } = loadProviderData(provider);
|
|
67
|
+
for (const [name, config] of Object.entries(presets)) {
|
|
68
|
+
registry[`${provider}/${name}`] = config;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return registry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Load presets once at module initialization
|
|
76
|
+
const PRESETS = buildPresetsRegistry();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a preset by name
|
|
80
|
+
* @param {string} presetName - Preset identifier (e.g., "github/my-issues")
|
|
81
|
+
* @returns {object|null} Preset configuration or null if not found
|
|
82
|
+
*/
|
|
83
|
+
export function getPreset(presetName) {
|
|
84
|
+
return PRESETS[presetName] || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get provider config (response_key, mappings) for a provider
|
|
89
|
+
* @param {string} provider - Provider name (e.g., "github", "linear")
|
|
90
|
+
* @returns {object|null} Provider config or null if not found
|
|
91
|
+
*/
|
|
92
|
+
export function getProviderConfig(provider) {
|
|
93
|
+
const { providerConfig } = loadProviderData(provider);
|
|
94
|
+
return providerConfig;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* List all available preset names
|
|
99
|
+
* @returns {string[]} Array of preset names
|
|
100
|
+
*/
|
|
101
|
+
export function listPresets() {
|
|
102
|
+
return Object.keys(PRESETS);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Expand a preset into a full source configuration
|
|
107
|
+
* User config is merged on top of preset defaults
|
|
108
|
+
* @param {string} presetName - Preset identifier
|
|
109
|
+
* @param {object} userConfig - User's source config (overrides preset)
|
|
110
|
+
* @returns {object} Merged source configuration
|
|
111
|
+
* @throws {Error} If preset is unknown
|
|
112
|
+
*/
|
|
113
|
+
export function expandPreset(presetName, userConfig) {
|
|
114
|
+
const preset = getPreset(presetName);
|
|
115
|
+
if (!preset) {
|
|
116
|
+
throw new Error(`Unknown preset: ${presetName}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Deep merge: preset as base, user config on top
|
|
120
|
+
return {
|
|
121
|
+
...preset,
|
|
122
|
+
...userConfig,
|
|
123
|
+
// Merge nested objects specially
|
|
124
|
+
tool: userConfig.tool || preset.tool,
|
|
125
|
+
args: {
|
|
126
|
+
...preset.args,
|
|
127
|
+
...(userConfig.args || {}),
|
|
128
|
+
},
|
|
129
|
+
item: userConfig.item || preset.item,
|
|
130
|
+
// Remove the preset key from final output
|
|
131
|
+
preset: undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Expand GitHub shorthand syntax into full source config
|
|
137
|
+
* @param {string} query - GitHub search query
|
|
138
|
+
* @param {object} userConfig - Rest of user's source config
|
|
139
|
+
* @returns {object} Full source configuration
|
|
140
|
+
*/
|
|
141
|
+
export function expandGitHubShorthand(query, userConfig) {
|
|
142
|
+
return {
|
|
143
|
+
...userConfig,
|
|
144
|
+
tool: {
|
|
145
|
+
mcp: "github",
|
|
146
|
+
name: "search_issues",
|
|
147
|
+
},
|
|
148
|
+
args: {
|
|
149
|
+
q: query,
|
|
150
|
+
},
|
|
151
|
+
item: {
|
|
152
|
+
id: "{html_url}",
|
|
153
|
+
},
|
|
154
|
+
// Remove the github key from final output
|
|
155
|
+
github: undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Linear source presets
|
|
2
|
+
|
|
3
|
+
# Provider-level config (applies to all Linear presets)
|
|
4
|
+
_provider:
|
|
5
|
+
response_key: nodes
|
|
6
|
+
mappings:
|
|
7
|
+
body: title
|
|
8
|
+
number: "url:/([A-Z0-9]+-[0-9]+)/"
|
|
9
|
+
|
|
10
|
+
# Presets
|
|
11
|
+
my-issues:
|
|
12
|
+
name: my-issues
|
|
13
|
+
tool:
|
|
14
|
+
mcp: linear
|
|
15
|
+
name: list_issues
|
|
16
|
+
args: {}
|
|
17
|
+
# teamId and assigneeId are required - user must provide
|
|
18
|
+
item:
|
|
19
|
+
id: "linear:{id}"
|
package/service/repo-config.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Manages configuration stored in ~/.config/opencode-pilot/config.yaml
|
|
5
5
|
* Supports:
|
|
6
|
+
* - defaults: default values applied to all sources
|
|
6
7
|
* - repos: per-repository settings (use YAML anchors for sharing)
|
|
7
|
-
* - sources: polling sources with generic tool references
|
|
8
|
+
* - sources: polling sources with generic tool references, presets, or shorthand
|
|
8
9
|
* - tools: field mappings for normalizing MCP responses
|
|
9
10
|
* - templates: prompt templates stored as markdown files
|
|
10
11
|
*/
|
|
@@ -14,6 +15,7 @@ import path from "path";
|
|
|
14
15
|
import os from "os";
|
|
15
16
|
import YAML from "yaml";
|
|
16
17
|
import { getNestedValue } from "./utils.js";
|
|
18
|
+
import { expandPreset, expandGitHubShorthand, getProviderConfig } from "./presets/index.js";
|
|
17
19
|
|
|
18
20
|
// Default config path
|
|
19
21
|
const DEFAULT_CONFIG_PATH = path.join(
|
|
@@ -100,13 +102,53 @@ export function getRepoConfig(repoKey) {
|
|
|
100
102
|
return repoConfig;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Normalize a single source config
|
|
107
|
+
* Expands presets, shorthand syntax, and applies defaults
|
|
108
|
+
* @param {object} source - Raw source config
|
|
109
|
+
* @param {object} defaults - Default values to apply
|
|
110
|
+
* @returns {object} Normalized source config
|
|
111
|
+
*/
|
|
112
|
+
function normalizeSource(source, defaults) {
|
|
113
|
+
let normalized = { ...source };
|
|
114
|
+
|
|
115
|
+
// Expand preset if present
|
|
116
|
+
if (source.preset) {
|
|
117
|
+
normalized = expandPreset(source.preset, source);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Expand GitHub shorthand if present
|
|
121
|
+
if (source.github) {
|
|
122
|
+
normalized = expandGitHubShorthand(source.github, source);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Apply defaults (source values take precedence)
|
|
126
|
+
return {
|
|
127
|
+
...defaults,
|
|
128
|
+
...normalized,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
/**
|
|
104
133
|
* Get all top-level sources (for polling)
|
|
105
|
-
*
|
|
134
|
+
* Expands presets and shorthand syntax, applies defaults
|
|
135
|
+
* @returns {Array} Array of normalized source configurations
|
|
106
136
|
*/
|
|
107
137
|
export function getSources() {
|
|
108
138
|
const config = getRawConfig();
|
|
109
|
-
|
|
139
|
+
const rawSources = config.sources || [];
|
|
140
|
+
const defaults = config.defaults || {};
|
|
141
|
+
|
|
142
|
+
return rawSources.map((source) => normalizeSource(source, defaults));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get defaults section from config
|
|
147
|
+
* @returns {object} Defaults configuration or empty object
|
|
148
|
+
*/
|
|
149
|
+
export function getDefaults() {
|
|
150
|
+
const config = getRawConfig();
|
|
151
|
+
return config.defaults || {};
|
|
110
152
|
}
|
|
111
153
|
|
|
112
154
|
/**
|
|
@@ -134,6 +176,40 @@ export function getToolMappings(provider) {
|
|
|
134
176
|
return toolConfig.mappings;
|
|
135
177
|
}
|
|
136
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Get full tool provider configuration (response_key, mappings, etc.)
|
|
181
|
+
* Checks user config first, then falls back to preset provider defaults
|
|
182
|
+
* @param {string} provider - Tool provider name (e.g., "github", "linear", "apple-reminders")
|
|
183
|
+
* @returns {object|null} Tool config including response_key and mappings, or null if not configured
|
|
184
|
+
*/
|
|
185
|
+
export function getToolProviderConfig(provider) {
|
|
186
|
+
const config = getRawConfig();
|
|
187
|
+
const tools = config.tools || {};
|
|
188
|
+
const userToolConfig = tools[provider];
|
|
189
|
+
|
|
190
|
+
// Get preset provider config as fallback
|
|
191
|
+
const presetProviderConfig = getProviderConfig(provider);
|
|
192
|
+
|
|
193
|
+
// If user has config, merge with preset defaults (user takes precedence)
|
|
194
|
+
if (userToolConfig) {
|
|
195
|
+
if (presetProviderConfig) {
|
|
196
|
+
return {
|
|
197
|
+
...presetProviderConfig,
|
|
198
|
+
...userToolConfig,
|
|
199
|
+
// Deep merge mappings
|
|
200
|
+
mappings: {
|
|
201
|
+
...(presetProviderConfig.mappings || {}),
|
|
202
|
+
...(userToolConfig.mappings || {}),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return userToolConfig;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fall back to preset provider config
|
|
210
|
+
return presetProviderConfig;
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
/**
|
|
138
214
|
* Load a template from the templates directory
|
|
139
215
|
* @param {string} templateName - Template name (without .md extension)
|
package/test/unit/poller.test.js
CHANGED
|
@@ -312,6 +312,65 @@ describe('poller.js', () => {
|
|
|
312
312
|
});
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
describe('parseJsonArray', () => {
|
|
316
|
+
test('parses direct array response', async () => {
|
|
317
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
318
|
+
|
|
319
|
+
const text = JSON.stringify([{ id: '1' }, { id: '2' }]);
|
|
320
|
+
const result = parseJsonArray(text, 'test');
|
|
321
|
+
|
|
322
|
+
assert.strictEqual(result.length, 2);
|
|
323
|
+
assert.strictEqual(result[0].id, '1');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('extracts array using response_key', async () => {
|
|
327
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
328
|
+
|
|
329
|
+
const text = JSON.stringify({
|
|
330
|
+
reminders: [
|
|
331
|
+
{ id: 'reminder-1', name: 'Task 1', completed: false },
|
|
332
|
+
{ id: 'reminder-2', name: 'Task 2', completed: false },
|
|
333
|
+
{ id: 'reminder-3', name: 'Task 3', completed: false }
|
|
334
|
+
],
|
|
335
|
+
count: 3
|
|
336
|
+
});
|
|
337
|
+
const result = parseJsonArray(text, 'agent-tasks', 'reminders');
|
|
338
|
+
|
|
339
|
+
assert.strictEqual(result.length, 3);
|
|
340
|
+
assert.strictEqual(result[0].id, 'reminder-1');
|
|
341
|
+
assert.strictEqual(result[0].name, 'Task 1');
|
|
342
|
+
assert.strictEqual(result[1].id, 'reminder-2');
|
|
343
|
+
assert.strictEqual(result[2].id, 'reminder-3');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('wraps single object as array when no response_key', async () => {
|
|
347
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
348
|
+
|
|
349
|
+
const text = JSON.stringify({ id: '1', title: 'Single item' });
|
|
350
|
+
const result = parseJsonArray(text, 'test');
|
|
351
|
+
|
|
352
|
+
assert.strictEqual(result.length, 1);
|
|
353
|
+
assert.strictEqual(result[0].id, '1');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('returns empty array for invalid JSON', async () => {
|
|
357
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
358
|
+
|
|
359
|
+
const result = parseJsonArray('not valid json', 'test');
|
|
360
|
+
|
|
361
|
+
assert.strictEqual(result.length, 0);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('returns empty array when response_key not found', async () => {
|
|
365
|
+
const { parseJsonArray } = await import('../../service/poller.js');
|
|
366
|
+
|
|
367
|
+
const text = JSON.stringify({ items: [{ id: '1' }] });
|
|
368
|
+
const result = parseJsonArray(text, 'test', 'reminders');
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(result.length, 0);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
315
374
|
describe('transformItems with mappings', () => {
|
|
316
375
|
test('applies mappings to all items', async () => {
|
|
317
376
|
const { transformItems, applyMappings } = await import('../../service/poller.js');
|
|
@@ -314,6 +314,84 @@ sources: []
|
|
|
314
314
|
|
|
315
315
|
assert.strictEqual(mappings, null);
|
|
316
316
|
});
|
|
317
|
+
|
|
318
|
+
test('getToolProviderConfig returns full tool config with response_key', async () => {
|
|
319
|
+
writeFileSync(configPath, `
|
|
320
|
+
tools:
|
|
321
|
+
apple-reminders:
|
|
322
|
+
response_key: reminders
|
|
323
|
+
mappings:
|
|
324
|
+
body: notes
|
|
325
|
+
|
|
326
|
+
sources: []
|
|
327
|
+
`);
|
|
328
|
+
|
|
329
|
+
const { loadRepoConfig, getToolProviderConfig } = await import('../../service/repo-config.js');
|
|
330
|
+
loadRepoConfig(configPath);
|
|
331
|
+
|
|
332
|
+
const toolConfig = getToolProviderConfig('apple-reminders');
|
|
333
|
+
|
|
334
|
+
assert.strictEqual(toolConfig.response_key, 'reminders');
|
|
335
|
+
assert.deepStrictEqual(toolConfig.mappings, { body: 'notes' });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('getToolProviderConfig returns config without response_key', async () => {
|
|
339
|
+
writeFileSync(configPath, `
|
|
340
|
+
tools:
|
|
341
|
+
github:
|
|
342
|
+
mappings:
|
|
343
|
+
url: html_url
|
|
344
|
+
|
|
345
|
+
sources: []
|
|
346
|
+
`);
|
|
347
|
+
|
|
348
|
+
const { loadRepoConfig, getToolProviderConfig } = await import('../../service/repo-config.js');
|
|
349
|
+
loadRepoConfig(configPath);
|
|
350
|
+
|
|
351
|
+
const toolConfig = getToolProviderConfig('github');
|
|
352
|
+
|
|
353
|
+
// GitHub preset has response_key: items, user config doesn't override it
|
|
354
|
+
assert.strictEqual(toolConfig.response_key, 'items');
|
|
355
|
+
assert.deepStrictEqual(toolConfig.mappings, { url: 'html_url' });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('getToolProviderConfig falls back to preset provider config', async () => {
|
|
359
|
+
writeFileSync(configPath, `
|
|
360
|
+
sources: []
|
|
361
|
+
`);
|
|
362
|
+
|
|
363
|
+
const { loadRepoConfig, getToolProviderConfig } = await import('../../service/repo-config.js');
|
|
364
|
+
loadRepoConfig(configPath);
|
|
365
|
+
|
|
366
|
+
// Linear preset has provider config with response_key and mappings
|
|
367
|
+
const toolConfig = getToolProviderConfig('linear');
|
|
368
|
+
|
|
369
|
+
assert.strictEqual(toolConfig.response_key, 'nodes');
|
|
370
|
+
assert.strictEqual(toolConfig.mappings.body, 'title');
|
|
371
|
+
assert.strictEqual(toolConfig.mappings.number, 'url:/([A-Z0-9]+-[0-9]+)/');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('getToolProviderConfig merges user config with preset defaults', async () => {
|
|
375
|
+
writeFileSync(configPath, `
|
|
376
|
+
tools:
|
|
377
|
+
linear:
|
|
378
|
+
mappings:
|
|
379
|
+
custom_field: some_source
|
|
380
|
+
|
|
381
|
+
sources: []
|
|
382
|
+
`);
|
|
383
|
+
|
|
384
|
+
const { loadRepoConfig, getToolProviderConfig } = await import('../../service/repo-config.js');
|
|
385
|
+
loadRepoConfig(configPath);
|
|
386
|
+
|
|
387
|
+
const toolConfig = getToolProviderConfig('linear');
|
|
388
|
+
|
|
389
|
+
// Should have preset response_key
|
|
390
|
+
assert.strictEqual(toolConfig.response_key, 'nodes');
|
|
391
|
+
// Should have preset mappings plus user mappings
|
|
392
|
+
assert.strictEqual(toolConfig.mappings.body, 'title');
|
|
393
|
+
assert.strictEqual(toolConfig.mappings.custom_field, 'some_source');
|
|
394
|
+
});
|
|
317
395
|
});
|
|
318
396
|
|
|
319
397
|
describe('repo resolution for sources', () => {
|
|
@@ -438,4 +516,217 @@ repos:
|
|
|
438
516
|
assert.strictEqual(repoKey, null);
|
|
439
517
|
});
|
|
440
518
|
});
|
|
519
|
+
|
|
520
|
+
describe('presets', () => {
|
|
521
|
+
test('expands github/my-issues preset', async () => {
|
|
522
|
+
writeFileSync(configPath, `
|
|
523
|
+
sources:
|
|
524
|
+
- preset: github/my-issues
|
|
525
|
+
prompt: worktree
|
|
526
|
+
`);
|
|
527
|
+
|
|
528
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
529
|
+
loadRepoConfig(configPath);
|
|
530
|
+
const sources = getSources();
|
|
531
|
+
|
|
532
|
+
assert.strictEqual(sources.length, 1);
|
|
533
|
+
assert.strictEqual(sources[0].name, 'my-issues');
|
|
534
|
+
assert.deepStrictEqual(sources[0].tool, { mcp: 'github', name: 'search_issues' });
|
|
535
|
+
assert.strictEqual(sources[0].args.q, 'is:issue assignee:@me state:open');
|
|
536
|
+
assert.strictEqual(sources[0].item.id, '{html_url}');
|
|
537
|
+
assert.strictEqual(sources[0].prompt, 'worktree');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test('expands github/review-requests preset', async () => {
|
|
541
|
+
writeFileSync(configPath, `
|
|
542
|
+
sources:
|
|
543
|
+
- preset: github/review-requests
|
|
544
|
+
`);
|
|
545
|
+
|
|
546
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
547
|
+
loadRepoConfig(configPath);
|
|
548
|
+
const sources = getSources();
|
|
549
|
+
|
|
550
|
+
assert.strictEqual(sources[0].name, 'review-requests');
|
|
551
|
+
assert.strictEqual(sources[0].args.q, 'is:pr review-requested:@me state:open');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('expands github/my-prs-feedback preset', async () => {
|
|
555
|
+
writeFileSync(configPath, `
|
|
556
|
+
sources:
|
|
557
|
+
- preset: github/my-prs-feedback
|
|
558
|
+
`);
|
|
559
|
+
|
|
560
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
561
|
+
loadRepoConfig(configPath);
|
|
562
|
+
const sources = getSources();
|
|
563
|
+
|
|
564
|
+
assert.strictEqual(sources[0].name, 'my-prs-feedback');
|
|
565
|
+
assert.strictEqual(sources[0].args.q, 'is:pr author:@me state:open review:changes_requested');
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('expands linear/my-issues preset with required args', async () => {
|
|
569
|
+
writeFileSync(configPath, `
|
|
570
|
+
sources:
|
|
571
|
+
- preset: linear/my-issues
|
|
572
|
+
args:
|
|
573
|
+
teamId: "team-uuid-123"
|
|
574
|
+
assigneeId: "user-uuid-456"
|
|
575
|
+
`);
|
|
576
|
+
|
|
577
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
578
|
+
loadRepoConfig(configPath);
|
|
579
|
+
const sources = getSources();
|
|
580
|
+
|
|
581
|
+
assert.strictEqual(sources[0].name, 'my-issues');
|
|
582
|
+
assert.deepStrictEqual(sources[0].tool, { mcp: 'linear', name: 'list_issues' });
|
|
583
|
+
assert.strictEqual(sources[0].args.teamId, 'team-uuid-123');
|
|
584
|
+
assert.strictEqual(sources[0].args.assigneeId, 'user-uuid-456');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('user config overrides preset values', async () => {
|
|
588
|
+
writeFileSync(configPath, `
|
|
589
|
+
sources:
|
|
590
|
+
- preset: github/my-issues
|
|
591
|
+
name: custom-name
|
|
592
|
+
args:
|
|
593
|
+
q: "is:issue assignee:@me state:open label:urgent"
|
|
594
|
+
agent: plan
|
|
595
|
+
`);
|
|
596
|
+
|
|
597
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
598
|
+
loadRepoConfig(configPath);
|
|
599
|
+
const sources = getSources();
|
|
600
|
+
|
|
601
|
+
assert.strictEqual(sources[0].name, 'custom-name');
|
|
602
|
+
assert.strictEqual(sources[0].args.q, 'is:issue assignee:@me state:open label:urgent');
|
|
603
|
+
assert.strictEqual(sources[0].agent, 'plan');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test('throws error for unknown preset', async () => {
|
|
607
|
+
writeFileSync(configPath, `
|
|
608
|
+
sources:
|
|
609
|
+
- preset: unknown/preset
|
|
610
|
+
`);
|
|
611
|
+
|
|
612
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
613
|
+
loadRepoConfig(configPath);
|
|
614
|
+
|
|
615
|
+
assert.throws(() => getSources(), /Unknown preset: unknown\/preset/);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe('shorthand syntax', () => {
|
|
620
|
+
test('expands github shorthand to full source', async () => {
|
|
621
|
+
writeFileSync(configPath, `
|
|
622
|
+
sources:
|
|
623
|
+
- name: my-issues
|
|
624
|
+
github: "is:issue assignee:@me state:open"
|
|
625
|
+
prompt: worktree
|
|
626
|
+
`);
|
|
627
|
+
|
|
628
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
629
|
+
loadRepoConfig(configPath);
|
|
630
|
+
const sources = getSources();
|
|
631
|
+
|
|
632
|
+
assert.strictEqual(sources.length, 1);
|
|
633
|
+
assert.strictEqual(sources[0].name, 'my-issues');
|
|
634
|
+
assert.deepStrictEqual(sources[0].tool, { mcp: 'github', name: 'search_issues' });
|
|
635
|
+
assert.strictEqual(sources[0].args.q, 'is:issue assignee:@me state:open');
|
|
636
|
+
assert.strictEqual(sources[0].item.id, '{html_url}');
|
|
637
|
+
assert.strictEqual(sources[0].prompt, 'worktree');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('shorthand works with other source fields', async () => {
|
|
641
|
+
writeFileSync(configPath, `
|
|
642
|
+
sources:
|
|
643
|
+
- name: urgent-issues
|
|
644
|
+
github: "is:issue assignee:@me label:urgent"
|
|
645
|
+
agent: plan
|
|
646
|
+
working_dir: ~/code/myproject
|
|
647
|
+
`);
|
|
648
|
+
|
|
649
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
650
|
+
loadRepoConfig(configPath);
|
|
651
|
+
const sources = getSources();
|
|
652
|
+
|
|
653
|
+
assert.strictEqual(sources[0].agent, 'plan');
|
|
654
|
+
assert.strictEqual(sources[0].working_dir, '~/code/myproject');
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe('defaults section', () => {
|
|
659
|
+
test('applies defaults to sources without those fields', async () => {
|
|
660
|
+
writeFileSync(configPath, `
|
|
661
|
+
defaults:
|
|
662
|
+
agent: plan
|
|
663
|
+
prompt: default
|
|
664
|
+
|
|
665
|
+
sources:
|
|
666
|
+
- preset: github/my-issues
|
|
667
|
+
- preset: github/review-requests
|
|
668
|
+
prompt: review
|
|
669
|
+
`);
|
|
670
|
+
|
|
671
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
672
|
+
loadRepoConfig(configPath);
|
|
673
|
+
const sources = getSources();
|
|
674
|
+
|
|
675
|
+
// First source gets both defaults
|
|
676
|
+
assert.strictEqual(sources[0].agent, 'plan');
|
|
677
|
+
assert.strictEqual(sources[0].prompt, 'default');
|
|
678
|
+
|
|
679
|
+
// Second source overrides prompt but gets agent default
|
|
680
|
+
assert.strictEqual(sources[1].agent, 'plan');
|
|
681
|
+
assert.strictEqual(sources[1].prompt, 'review');
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('source values override defaults', async () => {
|
|
685
|
+
writeFileSync(configPath, `
|
|
686
|
+
defaults:
|
|
687
|
+
agent: plan
|
|
688
|
+
model: claude-3-sonnet
|
|
689
|
+
|
|
690
|
+
sources:
|
|
691
|
+
- preset: github/my-issues
|
|
692
|
+
agent: architect
|
|
693
|
+
`);
|
|
694
|
+
|
|
695
|
+
const { loadRepoConfig, getSources } = await import('../../service/repo-config.js');
|
|
696
|
+
loadRepoConfig(configPath);
|
|
697
|
+
const sources = getSources();
|
|
698
|
+
|
|
699
|
+
assert.strictEqual(sources[0].agent, 'architect');
|
|
700
|
+
assert.strictEqual(sources[0].model, 'claude-3-sonnet');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test('getDefaults returns defaults section', async () => {
|
|
704
|
+
writeFileSync(configPath, `
|
|
705
|
+
defaults:
|
|
706
|
+
agent: plan
|
|
707
|
+
prompt: default
|
|
708
|
+
working_dir: ~/code
|
|
709
|
+
`);
|
|
710
|
+
|
|
711
|
+
const { loadRepoConfig, getDefaults } = await import('../../service/repo-config.js');
|
|
712
|
+
loadRepoConfig(configPath);
|
|
713
|
+
const defaults = getDefaults();
|
|
714
|
+
|
|
715
|
+
assert.strictEqual(defaults.agent, 'plan');
|
|
716
|
+
assert.strictEqual(defaults.prompt, 'default');
|
|
717
|
+
assert.strictEqual(defaults.working_dir, '~/code');
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('getDefaults returns empty object when no defaults', async () => {
|
|
721
|
+
writeFileSync(configPath, `
|
|
722
|
+
sources: []
|
|
723
|
+
`);
|
|
724
|
+
|
|
725
|
+
const { loadRepoConfig, getDefaults } = await import('../../service/repo-config.js');
|
|
726
|
+
loadRepoConfig(configPath);
|
|
727
|
+
const defaults = getDefaults();
|
|
728
|
+
|
|
729
|
+
assert.deepStrictEqual(defaults, {});
|
|
730
|
+
});
|
|
731
|
+
});
|
|
441
732
|
});
|