@orxataguy/tyr 1.0.9 → 1.0.10-beta.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/package.json +4 -1
- package/src/core/Container.ts +7 -0
- package/src/lib/SetupManager.ts +328 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orxataguy/tyr",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tyr": "./bin/tyr.js"
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"release:patch": "npm version patch && git push --follow-tags",
|
|
23
23
|
"release:minor": "npm version minor && git push --follow-tags",
|
|
24
24
|
"release:major": "npm version major && git push --follow-tags",
|
|
25
|
+
"release:beta": "npm version prerelease --preid=beta && git push --follow-tags && npm publish --tag beta",
|
|
26
|
+
"release:beta:minor": "npm version preminor --preid=beta && git push --follow-tags && npm publish --tag beta",
|
|
27
|
+
"release:beta:major": "npm version premajor --preid=beta && git push --follow-tags && npm publish --tag beta",
|
|
25
28
|
"config": "node bin/tyr.js install"
|
|
26
29
|
},
|
|
27
30
|
"keywords": [
|
package/src/core/Container.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { DockerManager } from '../lib/DockerManager.js';
|
|
|
5
5
|
import { GitManager } from '../lib/GitManager.js';
|
|
6
6
|
import { SystemManager } from '../lib/SystemManager.js';
|
|
7
7
|
import { SQLManager } from '../lib/SQLManager.js';
|
|
8
|
+
import { MongoManager } from '../lib/MongoManager.js';
|
|
8
9
|
import { WebManager } from '../lib/WebManager.js';
|
|
9
10
|
import { WorkspaceManager } from '../lib/WorkspaceManager.js';
|
|
10
11
|
import { JiraManager } from '../lib/JiraManager.js';
|
|
12
|
+
import { SetupManager } from '../lib/SetupManager.js';
|
|
11
13
|
import { Logger, createLogger } from './Logger.js';
|
|
12
14
|
|
|
13
15
|
export type { Logger };
|
|
@@ -21,9 +23,11 @@ export interface ServiceContainer {
|
|
|
21
23
|
git: GitManager;
|
|
22
24
|
sys: SystemManager;
|
|
23
25
|
db: SQLManager;
|
|
26
|
+
mongo: MongoManager;
|
|
24
27
|
web: WebManager;
|
|
25
28
|
workspace: WorkspaceManager;
|
|
26
29
|
jira: JiraManager;
|
|
30
|
+
setup: SetupManager;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export class Container {
|
|
@@ -37,6 +41,7 @@ export class Container {
|
|
|
37
41
|
const logger = createLogger(isDebug);
|
|
38
42
|
const shell = new ShellManager();
|
|
39
43
|
const db = new SQLManager();
|
|
44
|
+
const mongo = new MongoManager();
|
|
40
45
|
const web = new WebManager(logger);
|
|
41
46
|
const fs = new FileSystemManager(logger);
|
|
42
47
|
|
|
@@ -44,6 +49,7 @@ export class Container {
|
|
|
44
49
|
logger,
|
|
45
50
|
shell,
|
|
46
51
|
db,
|
|
52
|
+
mongo,
|
|
47
53
|
web,
|
|
48
54
|
fs,
|
|
49
55
|
pkg: new PackageManager(shell, logger),
|
|
@@ -52,6 +58,7 @@ export class Container {
|
|
|
52
58
|
sys: new SystemManager(shell, logger),
|
|
53
59
|
workspace: new WorkspaceManager(shell, fs, logger),
|
|
54
60
|
jira: new JiraManager(web, shell, logger),
|
|
61
|
+
setup: new SetupManager(shell, fs, logger),
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { ShellManager } from './ShellManager.js';
|
|
3
|
+
import { FileSystemManager } from './FileSystemManager.js';
|
|
4
|
+
import { Logger } from '../core/Logger.js';
|
|
5
|
+
import { TyrError } from '../core/TyrError.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @class SetupManager
|
|
9
|
+
* @description Utilities for bootstrapping a development environment from a
|
|
10
|
+
* remote repository. Covers OS detection, dependency checks, docker-compose
|
|
11
|
+
* introspection, compose/buildx installation, Makefile generation, and
|
|
12
|
+
* migration-strategy detection.
|
|
13
|
+
*/
|
|
14
|
+
export class SetupManager {
|
|
15
|
+
private shell: ShellManager;
|
|
16
|
+
private fs: FileSystemManager;
|
|
17
|
+
private logger: Logger;
|
|
18
|
+
|
|
19
|
+
constructor(shell: ShellManager, fs: FileSystemManager, logger: Logger) {
|
|
20
|
+
this.shell = shell;
|
|
21
|
+
this.fs = fs;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── OS / package manager ────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @method detectPkgMgr
|
|
29
|
+
* @description Detects the system package manager including apk (Alpine Linux).
|
|
30
|
+
* Checks: apk → apt → brew → dnf on Unix; winget → choco → scoop on Windows.
|
|
31
|
+
* @returns {Promise<string>} Manager name ('apk','apt','brew','dnf','winget',
|
|
32
|
+
* 'choco','scoop') or 'unknown' if none found.
|
|
33
|
+
* @example
|
|
34
|
+
* const mgr = await setup.detectPkgMgr();
|
|
35
|
+
* logger.info(`Package manager: ${mgr}`);
|
|
36
|
+
*/
|
|
37
|
+
public async detectPkgMgr(): Promise<string> {
|
|
38
|
+
const candidates: [string, string][] =
|
|
39
|
+
process.platform === 'win32'
|
|
40
|
+
? [['winget', 'winget'], ['choco', 'choco'], ['scoop', 'scoop']]
|
|
41
|
+
: [['apk', 'apk'], ['apt-get', 'apt'], ['brew', 'brew'], ['dnf', 'dnf']];
|
|
42
|
+
|
|
43
|
+
for (const [bin, name] of candidates) {
|
|
44
|
+
const found = await this.shell.exec(
|
|
45
|
+
`which ${bin} 2>/dev/null && echo yes || echo no`
|
|
46
|
+
).catch(() => 'no');
|
|
47
|
+
if (found.trim() === 'yes') return name;
|
|
48
|
+
}
|
|
49
|
+
return 'unknown';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @method sysInstall
|
|
54
|
+
* @description Installs a system-level package using the detected package manager.
|
|
55
|
+
* @param {string} pkgMgr - Package manager name as returned by detectPkgMgr().
|
|
56
|
+
* @param {string} pkg - Package name to install (e.g. 'git', 'nodejs', 'docker').
|
|
57
|
+
* @example
|
|
58
|
+
* await setup.sysInstall('apk', 'git');
|
|
59
|
+
*/
|
|
60
|
+
public async sysInstall(pkgMgr: string, pkg: string): Promise<void> {
|
|
61
|
+
const cmds: Record<string, string> = {
|
|
62
|
+
apk: `apk add --no-cache ${pkg}`,
|
|
63
|
+
apt: `apt-get install -y ${pkg}`,
|
|
64
|
+
brew: `brew install ${pkg}`,
|
|
65
|
+
dnf: `sudo dnf install -y ${pkg}`,
|
|
66
|
+
winget: `winget install ${pkg}`,
|
|
67
|
+
choco: `choco install -y ${pkg}`,
|
|
68
|
+
scoop: `scoop install ${pkg}`,
|
|
69
|
+
};
|
|
70
|
+
const cmd = cmds[pkgMgr];
|
|
71
|
+
if (!cmd) throw new TyrError(`Unsupported package manager: ${pkgMgr}`);
|
|
72
|
+
try {
|
|
73
|
+
await this.shell.exec(cmd);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
throw new TyrError(`Could not install package '${pkg}' with ${pkgMgr}.`, e, `Run the install command manually: ${cmd}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @method binExists
|
|
81
|
+
* @description Checks whether a binary is available on PATH.
|
|
82
|
+
* @param {string} bin - Binary name to look for (e.g. 'git', 'docker').
|
|
83
|
+
* @returns {Promise<boolean>} True if the binary is found.
|
|
84
|
+
* @example
|
|
85
|
+
* if (!await setup.binExists('docker')) fail('Docker is required.');
|
|
86
|
+
*/
|
|
87
|
+
public async binExists(bin: string): Promise<boolean> {
|
|
88
|
+
const result = await this.shell.exec(
|
|
89
|
+
`which ${bin} 2>/dev/null && echo yes || echo no`
|
|
90
|
+
).catch(() => 'no');
|
|
91
|
+
return result.trim() === 'yes';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── docker-compose file introspection ───────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @method findComposeFile
|
|
98
|
+
* @description Locates a docker-compose file in the given directory.
|
|
99
|
+
* Checks: docker-compose.yml, docker-compose.yaml,
|
|
100
|
+
* docker-compose.override.yml, compose.yml, compose.yaml.
|
|
101
|
+
* @param {string} dir - Directory to search in.
|
|
102
|
+
* @returns {string|null} Absolute path of the compose file, or null.
|
|
103
|
+
* @example
|
|
104
|
+
* const file = setup.findComposeFile('/path/to/repo');
|
|
105
|
+
* if (!file) logger.warn('No compose file found.');
|
|
106
|
+
*/
|
|
107
|
+
public findComposeFile(dir: string): string | null {
|
|
108
|
+
const candidates = [
|
|
109
|
+
'docker-compose.yml',
|
|
110
|
+
'docker-compose.yaml',
|
|
111
|
+
'docker-compose.override.yml',
|
|
112
|
+
'compose.yml',
|
|
113
|
+
'compose.yaml',
|
|
114
|
+
];
|
|
115
|
+
for (const f of candidates) {
|
|
116
|
+
const full = path.join(dir, f);
|
|
117
|
+
if (this.fs.exists(full)) return full;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @method extractPorts
|
|
124
|
+
* @description Extracts unique host ports from a docker-compose file.
|
|
125
|
+
* Handles all four YAML port formats:
|
|
126
|
+
* - "HOST:CONTAINER" (quoted or unquoted)
|
|
127
|
+
* - HOST:CONTAINER (numeric pair)
|
|
128
|
+
* - PORT (single plain number)
|
|
129
|
+
* - published: PORT (long-form mapping)
|
|
130
|
+
* @param {string} composeFile - Absolute path to the compose file.
|
|
131
|
+
* @returns {Promise<number[]>} Sorted list of unique host port numbers.
|
|
132
|
+
* @example
|
|
133
|
+
* const ports = await setup.extractPorts('/repo/docker-compose.yml');
|
|
134
|
+
* // [3000, 5432, 8080]
|
|
135
|
+
*/
|
|
136
|
+
public async extractPorts(composeFile: string): Promise<number[]> {
|
|
137
|
+
const portSet = new Set<number>();
|
|
138
|
+
|
|
139
|
+
const addPorts = (raw: string) => {
|
|
140
|
+
for (const line of raw.split('\n')) {
|
|
141
|
+
const n = parseInt(line.trim(), 10);
|
|
142
|
+
if (!isNaN(n) && n > 0) portSet.add(n);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Format 1+2: - "HOST:CONTAINER" or - HOST:CONTAINER
|
|
147
|
+
addPorts(await this.shell.exec(
|
|
148
|
+
`grep -E '^\\s*-\\s+"?[0-9]+:[0-9]+"?' "${composeFile}" 2>/dev/null | grep -oE '[0-9]+:[0-9]+' | cut -d: -f1 || true`
|
|
149
|
+
).catch(() => ''));
|
|
150
|
+
|
|
151
|
+
// Format 3: - PORT (single number)
|
|
152
|
+
addPorts(await this.shell.exec(
|
|
153
|
+
`grep -E '^\\s*-\\s+"?[0-9]+"?\\s*$' "${composeFile}" 2>/dev/null | grep -oE '[0-9]+' || true`
|
|
154
|
+
).catch(() => ''));
|
|
155
|
+
|
|
156
|
+
// Format 4: published: PORT
|
|
157
|
+
addPorts(await this.shell.exec(
|
|
158
|
+
`grep -E '^\\s+published:\\s*[0-9]+' "${composeFile}" 2>/dev/null | grep -oE '[0-9]+' || true`
|
|
159
|
+
).catch(() => ''));
|
|
160
|
+
|
|
161
|
+
return Array.from(portSet).sort((a, b) => a - b);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @method getAppService
|
|
166
|
+
* @description Detects the main application service in a docker-compose file.
|
|
167
|
+
* Tries preferred names first (app, web, backend, api, server, worker),
|
|
168
|
+
* then falls back to the first service defined.
|
|
169
|
+
* @param {string} composeFile - Absolute path to the compose file.
|
|
170
|
+
* @returns {Promise<string>} Service name, or empty string if not found.
|
|
171
|
+
* @example
|
|
172
|
+
* const svc = await setup.getAppService('/repo/docker-compose.yml');
|
|
173
|
+
* // 'api'
|
|
174
|
+
*/
|
|
175
|
+
public async getAppService(composeFile: string): Promise<string> {
|
|
176
|
+
const preferred = ['app', 'web', 'backend', 'api', 'server', 'worker'];
|
|
177
|
+
for (const svc of preferred) {
|
|
178
|
+
const r = await this.shell.exec(
|
|
179
|
+
`grep -qE "^ ${svc}:" "${composeFile}" 2>/dev/null && echo yes || echo no`
|
|
180
|
+
).catch(() => 'no');
|
|
181
|
+
if (r.trim() === 'yes') return svc;
|
|
182
|
+
}
|
|
183
|
+
const first = await this.shell.exec(
|
|
184
|
+
`grep -E "^ [a-zA-Z]" "${composeFile}" 2>/dev/null | head -1 | sed 's/://;s/^[[:space:]]*//'`
|
|
185
|
+
).catch(() => '');
|
|
186
|
+
return first.trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── docker compose / buildx installation ────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @method getComposeCmd
|
|
193
|
+
* @description Resolves the available docker compose command.
|
|
194
|
+
* Prefers the compose plugin ('docker compose'), falls back to the
|
|
195
|
+
* standalone binary ('docker-compose'), and installs it from GitHub
|
|
196
|
+
* (or pip as a last resort) if neither is found.
|
|
197
|
+
* @returns {Promise<string|null>} Command string, or null if unavailable.
|
|
198
|
+
* @example
|
|
199
|
+
* const cmd = await setup.getComposeCmd();
|
|
200
|
+
* if (!cmd) fail('docker compose is required.');
|
|
201
|
+
*/
|
|
202
|
+
public async getComposeCmd(): Promise<string | null> {
|
|
203
|
+
const pluginOk = await this.shell.exec('docker compose version 2>/dev/null')
|
|
204
|
+
.then(() => true).catch(() => false);
|
|
205
|
+
if (pluginOk) return 'docker compose';
|
|
206
|
+
|
|
207
|
+
if (await this.binExists('docker-compose')) return 'docker-compose';
|
|
208
|
+
|
|
209
|
+
this.logger.warn('docker-compose no encontrado. Intentando instalar...');
|
|
210
|
+
|
|
211
|
+
const rawArch = await this.shell.exec('uname -m').catch(() => 'x86_64');
|
|
212
|
+
const osName = await this.shell.exec("uname -s | tr '[:upper:]' '[:lower:]'").catch(() => 'linux');
|
|
213
|
+
const bin = '/usr/local/bin/docker-compose';
|
|
214
|
+
const url = `https://github.com/docker/compose/releases/latest/download/docker-compose-${osName.trim()}-${rawArch.trim()}`;
|
|
215
|
+
|
|
216
|
+
this.logger.info('Descargando docker-compose desde GitHub...');
|
|
217
|
+
try {
|
|
218
|
+
await this.shell.exec(`curl -fsSL "${url}" -o "${bin}" && chmod +x "${bin}"`);
|
|
219
|
+
if (await this.shell.exec('docker-compose version 2>/dev/null').then(() => true).catch(() => false)) {
|
|
220
|
+
this.logger.success('docker-compose instalado correctamente.');
|
|
221
|
+
return 'docker-compose';
|
|
222
|
+
}
|
|
223
|
+
} catch { /* fall through to pip */ }
|
|
224
|
+
|
|
225
|
+
const pip = await this.shell.exec('which pip3 2>/dev/null || which pip 2>/dev/null').catch(() => '');
|
|
226
|
+
if (pip.trim()) {
|
|
227
|
+
try {
|
|
228
|
+
await this.shell.exec(
|
|
229
|
+
`${pip.trim()} install --quiet docker-compose --break-system-packages 2>/dev/null || ${pip.trim()} install --quiet docker-compose`
|
|
230
|
+
);
|
|
231
|
+
if (await this.binExists('docker-compose')) {
|
|
232
|
+
this.logger.success('docker-compose instalado via pip.');
|
|
233
|
+
return 'docker-compose';
|
|
234
|
+
}
|
|
235
|
+
} catch { /* fall through */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.logger.warn('No se pudo instalar docker-compose automaticamente.');
|
|
239
|
+
this.logger.warn('Instalalo manualmente: https://docs.docker.com/compose/install/');
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @method ensureBuildx
|
|
245
|
+
* @description Installs the docker buildx plugin if it is not already available.
|
|
246
|
+
* Fetches the latest release URL dynamically from the GitHub API.
|
|
247
|
+
* @example
|
|
248
|
+
* await setup.ensureBuildx();
|
|
249
|
+
*/
|
|
250
|
+
public async ensureBuildx(): Promise<void> {
|
|
251
|
+
const ok = await this.shell.exec('docker buildx version 2>/dev/null')
|
|
252
|
+
.then(() => true).catch(() => false);
|
|
253
|
+
if (ok) return;
|
|
254
|
+
|
|
255
|
+
this.logger.info('Instalando docker buildx plugin...');
|
|
256
|
+
const rawArch = await this.shell.exec('uname -m').catch(() => 'x86_64');
|
|
257
|
+
const arch = rawArch.trim().replace('x86_64', 'amd64').replace('aarch64', 'arm64');
|
|
258
|
+
const osName = await this.shell.exec("uname -s | tr '[:upper:]' '[:lower:]'").catch(() => 'linux');
|
|
259
|
+
const pluginDir = '/usr/local/lib/docker/cli-plugins';
|
|
260
|
+
|
|
261
|
+
const urlRaw = await this.shell.exec(
|
|
262
|
+
`wget -qO- https://api.github.com/repos/docker/buildx/releases/latest ` +
|
|
263
|
+
`| grep "browser_download_url" | grep '"${osName.trim()}-${arch}"' | cut -d'"' -f4 || true`
|
|
264
|
+
).catch(() => '');
|
|
265
|
+
|
|
266
|
+
if (!urlRaw.trim()) {
|
|
267
|
+
this.logger.warn('No se pudo obtener la URL de docker buildx. El build puede mostrar advertencias.');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await this.shell.exec(
|
|
272
|
+
`mkdir -p "${pluginDir}" && wget -qO "${pluginDir}/docker-buildx" "${urlRaw.trim()}" && chmod +x "${pluginDir}/docker-buildx"`
|
|
273
|
+
);
|
|
274
|
+
this.logger.success('docker buildx instalado.');
|
|
275
|
+
} catch {
|
|
276
|
+
this.logger.warn('No se pudo instalar docker buildx. El build puede mostrar advertencias.');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Makefile generation ─────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @method generateMakefile
|
|
284
|
+
* @description Creates a Makefile with docker compose shortcuts in the repo
|
|
285
|
+
* directory. If a Makefile already exists, writes Makefile.dev instead.
|
|
286
|
+
* Does nothing if the target file is already present.
|
|
287
|
+
* @param {string} repoDir - Absolute path to the cloned repository.
|
|
288
|
+
* @example
|
|
289
|
+
* await setup.generateMakefile('/workspace/my-repo');
|
|
290
|
+
*/
|
|
291
|
+
public async generateMakefile(repoDir: string): Promise<void> {
|
|
292
|
+
const hasExisting = this.fs.exists(path.join(repoDir, 'Makefile'));
|
|
293
|
+
const makefileName = hasExisting ? 'Makefile.dev' : 'Makefile';
|
|
294
|
+
const makefilePath = path.join(repoDir, makefileName);
|
|
295
|
+
const invokePrefix = hasExisting ? 'make -f Makefile.dev' : 'make';
|
|
296
|
+
|
|
297
|
+
if (this.fs.exists(makefilePath)) {
|
|
298
|
+
this.logger.info(`${makefileName} ya existe. No se sobreescribe.`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const TAB = '\t';
|
|
303
|
+
const content = [
|
|
304
|
+
`# ${makefileName} generado por setup-dev`,
|
|
305
|
+
'',
|
|
306
|
+
'up:', TAB + 'docker-compose up -d', '',
|
|
307
|
+
'build:', TAB + 'docker-compose up -d --build', '',
|
|
308
|
+
'stop:', TAB + 'docker-compose down --remove-orphans', '',
|
|
309
|
+
'restart:', TAB + 'docker-compose restart', '',
|
|
310
|
+
'logs:', TAB + 'docker-compose logs -f', '',
|
|
311
|
+
'ps:', TAB + 'docker-compose ps', '',
|
|
312
|
+
].join('\n');
|
|
313
|
+
|
|
314
|
+
await this.fs.write(makefilePath, content);
|
|
315
|
+
this.logger.success(`${makefileName} generado en: ${makefilePath}`);
|
|
316
|
+
this.logger.info(` ${invokePrefix} up -> arrancar contenedores`);
|
|
317
|
+
this.logger.info(` ${invokePrefix} build -> rebuild y arrancar`);
|
|
318
|
+
this.logger.info(` ${invokePrefix} stop -> parar y eliminar (sin orphans)`);
|
|
319
|
+
this.logger.info(` ${invokePrefix} logs -> ver logs en tiempo real`);
|
|
320
|
+
this.logger.info(` ${invokePrefix} ps -> estado de los contenedores`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const SetupManagerTests = {
|
|
325
|
+
detectPkgMgr: {},
|
|
326
|
+
binExists: { bin: 'git' },
|
|
327
|
+
findComposeFile: { dir: '/tmp' },
|
|
328
|
+
};
|