• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uscreen / dev-service / 23537034377

25 Mar 2026 10:44AM UTC coverage: 95.538% (-1.4%) from 96.938%
23537034377

push

github

mashpie
aded `service status` and auto-detection of compose plugin

221 of 246 branches covered (89.84%)

Branch coverage included in aggregate %.

93 of 104 new or added lines in 4 files covered. (89.42%)

1 existing line in 1 file now uncovered.

1085 of 1121 relevant lines covered (96.79%)

262.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.44
/src/utils.js
1
'use strict'
762✔
2

762✔
3
import { exec, spawn } from 'node:child_process'
762✔
4
import path from 'node:path'
762✔
5
import process from 'node:process'
762✔
6
import { fileURLToPath } from 'node:url'
762✔
7
import chalk from 'chalk'
762✔
8
import fs from 'fs-extra'
762✔
9
import { readPackageSync } from 'read-pkg'
762✔
10

762✔
11
import { COMPOSE_DIR, root } from './constants.js'
762✔
12

762✔
13
const __filename = fileURLToPath(import.meta.url)
762✔
14
const __dirname = path.dirname(__filename)
762✔
15

762✔
16
/**
762✔
17
 * Read package json
762✔
18
 */
762✔
19
export const readPackageJson = () => {
762✔
20
  let packageJson = null
780✔
21
  try {
780✔
22
    packageJson = readPackageSync({ cwd: root })
780✔
23
  }
780✔
24
  catch {}
780✔
25

780✔
26
  if (!packageJson) {
780✔
27
    throw new Error('Missing or invalid package.json')
3✔
28
  }
3✔
29

777✔
30
  const services = packageJson.services || []
777✔
31
  const name = packageJson.name || path.basename(root)
780✔
32

780✔
33
  if (services.length === 0) {
780✔
34
    throw new Error('No services defined')
6✔
35
  }
6✔
36

771✔
37
  return { packageJson, services, name }
771✔
38
}
780✔
39

762✔
40
/**
762✔
41
 * Escape string for use in docker
762✔
42
 */
762✔
43
export const escape = name =>
762✔
44
  name.replace(/^[^a-z0-9]*/i, '').replace(/[^a-z0-9-]/gi, '-')
1,455✔
45

762✔
46
/**
762✔
47
 * Checks if compose directory exists and contains files
762✔
48
 */
762✔
49
export const checkComposeDir = () => {
762✔
50
  return fs.existsSync(COMPOSE_DIR) && fs.readdirSync(COMPOSE_DIR).length > 0
555✔
51
}
555✔
52

762✔
53
/**
762✔
54
 * Get all compose files from compose directory
762✔
55
 */
762✔
56
export const getComposeFiles = () =>
762✔
57
  fs.readdirSync(COMPOSE_DIR).filter(f => f !== '.gitignore')
684✔
58

762✔
59
/**
762✔
60
 * Detect compose command: prefer 'docker compose' plugin, fall back to 'docker-compose'
762✔
61
 */
762✔
62
let _composeCmd = null
762✔
63

762✔
64
export const getComposeCommand = () => {
762✔
65
  if (!_composeCmd) {
273✔
66
    _composeCmd = new Promise((resolve) => {
255✔
67
      exec('docker compose version', (err) => {
255✔
68
        resolve(err ? 'docker-compose' : 'docker compose')
255!
69
      })
255✔
70
    })
255✔
71
  }
255✔
72
  return _composeCmd
273✔
73
}
273✔
74

762✔
75
/**
762✔
76
 * read path to compose file/folder via `docker inspect <containerId>`
762✔
77
 */
762✔
78
const getComposePath = containerId =>
762✔
79
  new Promise((resolve, reject) => {
48✔
80
    exec(`docker inspect ${containerId}`, (err, stdout, stderr) => {
48✔
81
      if (err) {
48!
82
        reject(err)
×
83
      }
×
84

48✔
85
      const errMessage = stderr.toString().trim()
48✔
86
      if (errMessage) {
48!
87
        reject(new Error(errMessage))
×
88
      }
×
89

48✔
90
      const [data] = JSON.parse(stdout)
48✔
91

48✔
92
      const result
48✔
93
        = data
48✔
94
          && data.Config
48✔
95
          && data.Config.Labels
48✔
96
          && data.Config.Labels['com.docker.compose.project.working_dir']
48✔
97

48✔
98
      resolve(result)
48✔
99
    })
48✔
100
  })
48✔
101

762✔
102
/**
762✔
103
 * Get paths to compose files/folders from running containers
762✔
104
 */
762✔
105
export const getComposePaths = () =>
762✔
106
  new Promise((resolve, reject) => {
237✔
107
    exec('docker ps -q', (err, stdout, stderr) => {
237✔
108
      if (err) {
237✔
109
        reject(err)
6✔
110
      }
6✔
111

237✔
112
      const errMessage = stderr.toString().trim()
237✔
113
      if (errMessage) {
237✔
114
        reject(new Error(errMessage))
6✔
115
      }
6✔
116

237✔
117
      const containers = stdout.split(/\n/).filter(r => r)
237✔
118

237✔
119
      Promise.all(containers.map(getComposePath))
237✔
120
        .then((ps) => {
237✔
121
          const paths = Array.from(new Set(ps)).filter(p => p)
237✔
122

237✔
123
          resolve(paths)
237✔
124
        })
237✔
125
        .catch(reject)
237✔
126
    })
237✔
127
  })
237✔
128

762✔
129
/**
762✔
130
 * Removes all content from compose directory
762✔
131
 */
762✔
132
export const resetComposeDir = () => {
762✔
133
  fs.removeSync(COMPOSE_DIR)
288✔
134
  fs.ensureDirSync(COMPOSE_DIR)
288✔
135
  fs.writeFileSync(path.resolve(COMPOSE_DIR, '.gitignore'), '*', 'utf8')
288✔
136
}
288✔
137

762✔
138
/**
762✔
139
 * Show error message & exit
762✔
140
 */
762✔
141
export const error = (e) => {
762✔
142
  console.error(chalk.red(`ERROR: ${e.message}\n`))
177✔
143
  process.exit(e.code || 1)
177✔
144
}
177✔
145

762✔
146
/**
762✔
147
 * Show warn message
762✔
148
 */
762✔
149
export const warning = (message) => {
762✔
150
  console.error(chalk.yellow(`WARNING: ${message}\n`))
12✔
151
}
12✔
152

762✔
153
/**
762✔
154
 * spawns a child process and returns a promise
762✔
155
 */
762✔
156
export const run = (command, parameters = [], cwd = null, stdio = [0, 1, 2]) =>
762✔
157
  new Promise((resolve, reject) => {
1,248✔
158
    const c = spawn(command, parameters, {
1,248✔
159
      cwd,
1,248✔
160
      stdio
1,248✔
161
    })
1,248✔
162
    c.on('close', (code) => {
1,248✔
163
      if (code === 0) {
1,248✔
164
        return resolve(code)
702✔
165
      }
702✔
166
      const e = new Error(`Running "${command}" returns exit code ${code}`)
546✔
167
      e.code = code
546✔
168
      reject(e)
546✔
169
    })
1,248✔
170
  })
1,248✔
171

762✔
172
/**
762✔
173
 * executes docker command
762✔
174
 */
762✔
175
export const docker = async (...params) => {
762✔
176
  return run('docker', params, __dirname, [null, null, null])
978✔
177
}
978✔
178

762✔
179
/**
762✔
180
 * executes docker-compose command
762✔
181
 */
762✔
182
export const compose = async (...params) => {
762✔
183
  if (!checkComposeDir()) {
324✔
184
    throw new Error('No services found. Try running `service install`')
54✔
185
  }
54✔
186

270✔
187
  const { name } = await readPackageJson()
270✔
188
  const projectname = escape(name)
270✔
189
  const files = getComposeFiles()
270✔
190

270✔
191
  const ps = []
270✔
192
  ps.push('-p', projectname)
270✔
193
  for (const f of files) {
270✔
194
    ps.push('-f', f)
516✔
195
  }
516✔
196

270✔
197
  ps.push(...params)
270✔
198

270✔
199
  const cmd = await getComposeCommand()
270✔
200

270✔
201
  if (cmd === 'docker compose') {
270✔
202
    return run('docker', ['compose', ...ps], COMPOSE_DIR)
270✔
203
  }
270✔
UNCOV
204
  return run('docker-compose', ps, COMPOSE_DIR)
×
205
}
324✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc