@arvoretech/hub 0.8.2 → 0.10.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 +42 -6
- package/dist/{chunk-T2UKYK2B.js → chunk-MDJG7IOU.js} +496 -153
- package/dist/chunk-VMN4KGAK.js +43 -0
- package/dist/config/index.d.ts +213 -0
- package/dist/config/index.js +234 -0
- package/dist/{generate-7YPRUSN5.js → generate-KSB3QH7S.js} +2 -1
- package/dist/hub-config-UATYEV6Q.js +10 -0
- package/dist/index.js +2430 -1310
- package/package.json +16 -3
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/core/hub-config.ts
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
function resolveConfigPath(dir) {
|
|
8
|
+
const tsPath = join(dir, "hub.config.ts");
|
|
9
|
+
if (existsSync(tsPath)) return { path: tsPath, format: "typescript" };
|
|
10
|
+
return { path: join(dir, "hub.yaml"), format: "yaml" };
|
|
11
|
+
}
|
|
12
|
+
async function loadTypeScriptConfig(configPath) {
|
|
13
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
14
|
+
try {
|
|
15
|
+
const mod = await import(fileUrl);
|
|
16
|
+
return mod.default ?? mod;
|
|
17
|
+
} catch {
|
|
18
|
+
const { execFileSync } = await import("child_process");
|
|
19
|
+
const json = execFileSync("npx", ["tsx", "-e", `import c from '${configPath}'; console.log(JSON.stringify(c))`], {
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
cwd: configPath.replace(/\/hub\.config\.ts$/, ""),
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
23
|
+
});
|
|
24
|
+
return JSON.parse(json);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function loadHubConfig(dir) {
|
|
28
|
+
const { path: configPath, format } = resolveConfigPath(dir);
|
|
29
|
+
if (format === "typescript") {
|
|
30
|
+
return loadTypeScriptConfig(configPath);
|
|
31
|
+
}
|
|
32
|
+
const content = await readFile(configPath, "utf-8");
|
|
33
|
+
return parse(content);
|
|
34
|
+
}
|
|
35
|
+
function findHubRoot(startDir = process.cwd()) {
|
|
36
|
+
return startDir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
resolveConfigPath,
|
|
41
|
+
loadHubConfig,
|
|
42
|
+
findHubRoot
|
|
43
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
interface Repo {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
url: string;
|
|
5
|
+
tech?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
display_name?: string;
|
|
8
|
+
env_file?: string;
|
|
9
|
+
commands?: {
|
|
10
|
+
install?: string;
|
|
11
|
+
dev?: string;
|
|
12
|
+
build?: string;
|
|
13
|
+
lint?: string;
|
|
14
|
+
test?: string;
|
|
15
|
+
[key: string]: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
skills?: string[];
|
|
18
|
+
tools?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
interface Service {
|
|
21
|
+
name: string;
|
|
22
|
+
image: string;
|
|
23
|
+
port?: number;
|
|
24
|
+
ports?: number[];
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
interface MCPConfig {
|
|
28
|
+
name: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
instructions?: string;
|
|
31
|
+
package?: string;
|
|
32
|
+
url?: string;
|
|
33
|
+
image?: string;
|
|
34
|
+
env?: Record<string, string>;
|
|
35
|
+
upstreams?: string[];
|
|
36
|
+
autoApprove?: boolean | string[];
|
|
37
|
+
}
|
|
38
|
+
interface IntegrationConfig {
|
|
39
|
+
linear?: {
|
|
40
|
+
team?: string;
|
|
41
|
+
labels?: string[];
|
|
42
|
+
link_pattern?: string;
|
|
43
|
+
};
|
|
44
|
+
github?: {
|
|
45
|
+
org?: string;
|
|
46
|
+
pr_branch_pattern?: string;
|
|
47
|
+
pr_tool?: "cli" | "mcp";
|
|
48
|
+
};
|
|
49
|
+
slack?: {
|
|
50
|
+
channels?: Record<string, string>;
|
|
51
|
+
templates?: Record<string, string>;
|
|
52
|
+
};
|
|
53
|
+
playwright?: {
|
|
54
|
+
base_url?: string;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
interface WorkflowStep {
|
|
58
|
+
step: string;
|
|
59
|
+
agent?: string;
|
|
60
|
+
agents?: (string | {
|
|
61
|
+
agent: string;
|
|
62
|
+
when?: string;
|
|
63
|
+
output?: string;
|
|
64
|
+
})[];
|
|
65
|
+
parallel?: boolean;
|
|
66
|
+
output?: string;
|
|
67
|
+
tools?: string[];
|
|
68
|
+
when?: string;
|
|
69
|
+
actions?: string[];
|
|
70
|
+
mode?: "plan" | "agent";
|
|
71
|
+
}
|
|
72
|
+
interface SecretRef {
|
|
73
|
+
secret: string;
|
|
74
|
+
profile?: string;
|
|
75
|
+
}
|
|
76
|
+
interface BuildDatabaseUrl {
|
|
77
|
+
from_secret: string;
|
|
78
|
+
profile?: string;
|
|
79
|
+
vars?: {
|
|
80
|
+
user?: string;
|
|
81
|
+
password?: string;
|
|
82
|
+
host?: string;
|
|
83
|
+
port?: string;
|
|
84
|
+
database?: string;
|
|
85
|
+
};
|
|
86
|
+
template?: string;
|
|
87
|
+
}
|
|
88
|
+
interface EnvProfile {
|
|
89
|
+
description?: string;
|
|
90
|
+
services?: string[];
|
|
91
|
+
aws_profile?: string;
|
|
92
|
+
secrets?: Record<string, string | SecretRef>;
|
|
93
|
+
build_database_url?: Record<string, BuildDatabaseUrl>;
|
|
94
|
+
}
|
|
95
|
+
interface MiseSettings {
|
|
96
|
+
experimental?: boolean;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
interface PromptCustomization {
|
|
100
|
+
prepend?: string;
|
|
101
|
+
append?: string;
|
|
102
|
+
sections?: Record<string, string>;
|
|
103
|
+
}
|
|
104
|
+
interface HookEntry {
|
|
105
|
+
type: "command" | "prompt";
|
|
106
|
+
command?: string;
|
|
107
|
+
prompt?: string;
|
|
108
|
+
matcher?: string;
|
|
109
|
+
timeout_ms?: number;
|
|
110
|
+
}
|
|
111
|
+
interface MemoryConfig {
|
|
112
|
+
path?: string;
|
|
113
|
+
categories?: string[];
|
|
114
|
+
auto_capture?: boolean;
|
|
115
|
+
embedding_model?: string;
|
|
116
|
+
}
|
|
117
|
+
interface RemoteSource {
|
|
118
|
+
name: string;
|
|
119
|
+
type: "skill" | "steering";
|
|
120
|
+
notion_page?: string;
|
|
121
|
+
url?: string;
|
|
122
|
+
path?: string;
|
|
123
|
+
instructions?: string;
|
|
124
|
+
triggers?: string[];
|
|
125
|
+
}
|
|
126
|
+
interface DesignLibrary {
|
|
127
|
+
name: string;
|
|
128
|
+
mcp?: string;
|
|
129
|
+
url?: string;
|
|
130
|
+
path?: string;
|
|
131
|
+
}
|
|
132
|
+
interface DesignConfig {
|
|
133
|
+
skills?: string[];
|
|
134
|
+
libraries?: DesignLibrary[];
|
|
135
|
+
icons?: string;
|
|
136
|
+
instructions?: string;
|
|
137
|
+
}
|
|
138
|
+
interface HubConfig {
|
|
139
|
+
name: string;
|
|
140
|
+
description?: string;
|
|
141
|
+
version?: string;
|
|
142
|
+
tools?: Record<string, string>;
|
|
143
|
+
mise_settings?: MiseSettings;
|
|
144
|
+
repos: Repo[];
|
|
145
|
+
services?: Service[];
|
|
146
|
+
env?: {
|
|
147
|
+
profiles?: Record<string, EnvProfile>;
|
|
148
|
+
overrides?: Record<string, Record<string, Record<string, string>>>;
|
|
149
|
+
};
|
|
150
|
+
mcps?: MCPConfig[];
|
|
151
|
+
integrations?: IntegrationConfig;
|
|
152
|
+
hooks?: Record<string, HookEntry[]>;
|
|
153
|
+
commands?: Record<string, string>;
|
|
154
|
+
commands_dir?: string;
|
|
155
|
+
memory?: MemoryConfig;
|
|
156
|
+
remote_sources?: RemoteSource[];
|
|
157
|
+
design?: DesignConfig;
|
|
158
|
+
workflow?: {
|
|
159
|
+
task_folder?: string;
|
|
160
|
+
pipeline?: WorkflowStep[];
|
|
161
|
+
prompt?: PromptCustomization;
|
|
162
|
+
enforce_workflow?: boolean;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
declare function defineConfig(config: HubConfig): HubConfig;
|
|
167
|
+
type RepoOverrides = Partial<Omit<Repo, "name" | "path" | "url" | "tech">>;
|
|
168
|
+
declare const repo: {
|
|
169
|
+
nestjs: (name: string, url: string, overrides?: RepoOverrides) => Repo;
|
|
170
|
+
nextjs: (name: string, url: string, overrides?: RepoOverrides) => Repo;
|
|
171
|
+
react: (name: string, url: string, overrides?: RepoOverrides) => Repo;
|
|
172
|
+
elixir: (name: string, url: string, overrides?: RepoOverrides) => Repo;
|
|
173
|
+
go: (name: string, url: string, overrides?: RepoOverrides) => Repo;
|
|
174
|
+
python: (name: string, url: string, overrides?: RepoOverrides) => Repo;
|
|
175
|
+
custom(name: string, url: string, config?: RepoOverrides & {
|
|
176
|
+
tech?: string;
|
|
177
|
+
}): Repo;
|
|
178
|
+
};
|
|
179
|
+
type MCPOverrides = Partial<Omit<MCPConfig, "name">>;
|
|
180
|
+
declare const mcp: {
|
|
181
|
+
postgresql(name: string, overrides?: MCPOverrides): MCPConfig;
|
|
182
|
+
mysql(name: string, overrides?: MCPOverrides): MCPConfig;
|
|
183
|
+
clickhouse(name: string, overrides?: MCPOverrides): MCPConfig;
|
|
184
|
+
datadog(overrides?: MCPOverrides): MCPConfig;
|
|
185
|
+
memory(overrides?: MCPOverrides): MCPConfig;
|
|
186
|
+
sendgrid(overrides?: MCPOverrides): MCPConfig;
|
|
187
|
+
launchdarkly(overrides?: MCPOverrides): MCPConfig;
|
|
188
|
+
tempmail(overrides?: MCPOverrides): MCPConfig;
|
|
189
|
+
awsSecretsManager(overrides?: MCPOverrides): MCPConfig;
|
|
190
|
+
npmRegistry(overrides?: MCPOverrides): MCPConfig;
|
|
191
|
+
runtimeLens(overrides?: MCPOverrides): MCPConfig;
|
|
192
|
+
meetTranscriptions(overrides?: MCPOverrides): MCPConfig;
|
|
193
|
+
googleChat(overrides?: MCPOverrides): MCPConfig;
|
|
194
|
+
playwright(overrides?: MCPOverrides): MCPConfig;
|
|
195
|
+
context7(overrides?: MCPOverrides): MCPConfig;
|
|
196
|
+
proxy(name: string, overrides: MCPOverrides & {
|
|
197
|
+
upstreams: string[];
|
|
198
|
+
}): MCPConfig;
|
|
199
|
+
custom(name: string, overrides?: MCPOverrides): MCPConfig;
|
|
200
|
+
};
|
|
201
|
+
type ServiceOverrides = Partial<Omit<Service, "name" | "image">>;
|
|
202
|
+
declare const service: {
|
|
203
|
+
mysql: (name: string, overrides?: ServiceOverrides) => Service;
|
|
204
|
+
postgres: (name: string, overrides?: ServiceOverrides) => Service;
|
|
205
|
+
redis: (name: string, overrides?: ServiceOverrides) => Service;
|
|
206
|
+
mongo: (name: string, overrides?: ServiceOverrides) => Service;
|
|
207
|
+
rabbitmq: (name: string, overrides?: ServiceOverrides) => Service;
|
|
208
|
+
elasticsearch: (name: string, overrides?: ServiceOverrides) => Service;
|
|
209
|
+
clickhouse: (name: string, overrides?: ServiceOverrides) => Service;
|
|
210
|
+
custom(name: string, image: string, overrides?: ServiceOverrides): Service;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export { type DesignConfig, type EnvProfile, type HookEntry, type HubConfig, type IntegrationConfig, type MCPConfig, type MemoryConfig, type Repo, type Service, type WorkflowStep, defineConfig, mcp, repo, service };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// src/config/define-config.ts
|
|
2
|
+
function defineConfig(config) {
|
|
3
|
+
return config;
|
|
4
|
+
}
|
|
5
|
+
function createRepo(tech, defaults) {
|
|
6
|
+
return (name, url, overrides) => {
|
|
7
|
+
const merged = { ...defaults, ...overrides?.commands };
|
|
8
|
+
const rest = Object.fromEntries(
|
|
9
|
+
Object.entries(overrides ?? {}).filter(([key]) => key !== "commands")
|
|
10
|
+
);
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
path: `./${name}`,
|
|
14
|
+
url,
|
|
15
|
+
tech,
|
|
16
|
+
commands: merged,
|
|
17
|
+
...rest
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
var repo = {
|
|
22
|
+
nestjs: createRepo("nestjs", {
|
|
23
|
+
install: "pnpm install",
|
|
24
|
+
dev: "pnpm dev",
|
|
25
|
+
build: "pnpm build",
|
|
26
|
+
test: "pnpm test",
|
|
27
|
+
lint: "pnpm lint"
|
|
28
|
+
}),
|
|
29
|
+
nextjs: createRepo("nextjs", {
|
|
30
|
+
install: "pnpm install",
|
|
31
|
+
dev: "pnpm dev",
|
|
32
|
+
build: "pnpm build",
|
|
33
|
+
test: "pnpm test",
|
|
34
|
+
lint: "pnpm lint"
|
|
35
|
+
}),
|
|
36
|
+
react: createRepo("react", {
|
|
37
|
+
install: "pnpm install",
|
|
38
|
+
dev: "pnpm dev",
|
|
39
|
+
build: "pnpm build",
|
|
40
|
+
test: "pnpm test",
|
|
41
|
+
lint: "pnpm lint"
|
|
42
|
+
}),
|
|
43
|
+
elixir: createRepo("elixir", {
|
|
44
|
+
install: "mix deps.get",
|
|
45
|
+
dev: "mix phx.server",
|
|
46
|
+
test: "mix test",
|
|
47
|
+
lint: "mix credo"
|
|
48
|
+
}),
|
|
49
|
+
go: createRepo("go", {
|
|
50
|
+
install: "go mod download",
|
|
51
|
+
build: "go build ./...",
|
|
52
|
+
test: "go test ./...",
|
|
53
|
+
lint: "golangci-lint run"
|
|
54
|
+
}),
|
|
55
|
+
python: createRepo("python", {
|
|
56
|
+
install: "pip install -r requirements.txt",
|
|
57
|
+
dev: "python manage.py runserver",
|
|
58
|
+
test: "pytest",
|
|
59
|
+
lint: "ruff check ."
|
|
60
|
+
}),
|
|
61
|
+
custom(name, url, config) {
|
|
62
|
+
const { tech, ...rest } = config ?? {};
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
path: `./${name}`,
|
|
66
|
+
url,
|
|
67
|
+
...tech && { tech },
|
|
68
|
+
...rest
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var mcp = {
|
|
73
|
+
postgresql(name, overrides) {
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
package: "@arvoretech/postgresql-mcp",
|
|
77
|
+
...overrides,
|
|
78
|
+
env: { ...overrides?.env }
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
mysql(name, overrides) {
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
package: "@arvoretech/mysql-mcp",
|
|
85
|
+
...overrides,
|
|
86
|
+
env: { ...overrides?.env }
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
clickhouse(name, overrides) {
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
package: "@arvoretech/clickhouse-mcp",
|
|
93
|
+
...overrides,
|
|
94
|
+
env: { ...overrides?.env }
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
datadog(overrides) {
|
|
98
|
+
return {
|
|
99
|
+
name: "datadog",
|
|
100
|
+
package: "@arvoretech/datadog-mcp",
|
|
101
|
+
...overrides,
|
|
102
|
+
env: { ...overrides?.env }
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
memory(overrides) {
|
|
106
|
+
return {
|
|
107
|
+
name: "team-memory",
|
|
108
|
+
package: "@arvoretech/memory-mcp",
|
|
109
|
+
...overrides,
|
|
110
|
+
env: { ...overrides?.env }
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
sendgrid(overrides) {
|
|
114
|
+
return {
|
|
115
|
+
name: "sendgrid",
|
|
116
|
+
package: "@arvoretech/sendgrid-mcp",
|
|
117
|
+
...overrides,
|
|
118
|
+
env: { ...overrides?.env }
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
launchdarkly(overrides) {
|
|
122
|
+
return {
|
|
123
|
+
name: "launchdarkly",
|
|
124
|
+
package: "@arvoretech/launchdarkly-mcp",
|
|
125
|
+
...overrides,
|
|
126
|
+
env: { ...overrides?.env }
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
tempmail(overrides) {
|
|
130
|
+
return {
|
|
131
|
+
name: "tempmail",
|
|
132
|
+
package: "@arvoretech/tempmail-mcp",
|
|
133
|
+
...overrides,
|
|
134
|
+
env: { ...overrides?.env }
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
awsSecretsManager(overrides) {
|
|
138
|
+
return {
|
|
139
|
+
name: "aws-secrets-manager",
|
|
140
|
+
package: "@arvoretech/aws-secrets-manager-mcp",
|
|
141
|
+
...overrides,
|
|
142
|
+
env: { ...overrides?.env }
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
npmRegistry(overrides) {
|
|
146
|
+
return {
|
|
147
|
+
name: "npm-registry",
|
|
148
|
+
package: "@arvoretech/npm-registry-mcp",
|
|
149
|
+
...overrides
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
runtimeLens(overrides) {
|
|
153
|
+
return {
|
|
154
|
+
name: "runtime-lens",
|
|
155
|
+
package: "runtime-lens",
|
|
156
|
+
...overrides,
|
|
157
|
+
env: { ...overrides?.env }
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
meetTranscriptions(overrides) {
|
|
161
|
+
return {
|
|
162
|
+
name: "meet-transcriptions",
|
|
163
|
+
package: "@arvoretech/meet-transcriptions-mcp",
|
|
164
|
+
...overrides,
|
|
165
|
+
env: { ...overrides?.env }
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
googleChat(overrides) {
|
|
169
|
+
return {
|
|
170
|
+
name: "google-chat",
|
|
171
|
+
package: "@arvoretech/google-chat-mcp",
|
|
172
|
+
...overrides,
|
|
173
|
+
env: { ...overrides?.env }
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
playwright(overrides) {
|
|
177
|
+
return {
|
|
178
|
+
name: "playwright",
|
|
179
|
+
package: "@playwright/mcp",
|
|
180
|
+
...overrides
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
context7(overrides) {
|
|
184
|
+
return {
|
|
185
|
+
name: "context7",
|
|
186
|
+
package: "@upstash/context7-mcp",
|
|
187
|
+
...overrides
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
proxy(name, overrides) {
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
package: "@arvoretech/mcp-proxy",
|
|
194
|
+
...overrides,
|
|
195
|
+
env: { ...overrides?.env }
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
custom(name, overrides) {
|
|
199
|
+
return {
|
|
200
|
+
name,
|
|
201
|
+
...overrides
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
function createService(image, defaultPort) {
|
|
206
|
+
return (name, overrides) => ({
|
|
207
|
+
name,
|
|
208
|
+
image,
|
|
209
|
+
port: overrides?.port ?? defaultPort,
|
|
210
|
+
...overrides
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
var service = {
|
|
214
|
+
mysql: createService("mysql:8", 3306),
|
|
215
|
+
postgres: createService("postgres:16", 5432),
|
|
216
|
+
redis: createService("redis:7", 6379),
|
|
217
|
+
mongo: createService("mongo:7", 27017),
|
|
218
|
+
rabbitmq: createService("rabbitmq:3-management", 5672),
|
|
219
|
+
elasticsearch: createService("elasticsearch:8.12.0", 9200),
|
|
220
|
+
clickhouse: createService("clickhouse/clickhouse-server:24", 8123),
|
|
221
|
+
custom(name, image, overrides) {
|
|
222
|
+
return {
|
|
223
|
+
name,
|
|
224
|
+
image,
|
|
225
|
+
...overrides
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
export {
|
|
230
|
+
defineConfig,
|
|
231
|
+
mcp,
|
|
232
|
+
repo,
|
|
233
|
+
service
|
|
234
|
+
};
|