heroku-dash 0.1.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/LICENSE +21 -0
- package/README.md +230 -0
- package/oclif.manifest.json +105 -0
- package/package.json +37 -0
- package/src/api.js +128 -0
- package/src/commands/dash.js +47 -0
- package/src/demo.js +31 -0
- package/src/hierarchy.js +27 -0
- package/src/project.js +72 -0
- package/src/ui/dashboard.js +638 -0
- package/src/ui/details.js +34 -0
- package/src/ui/text.js +8 -0
- package/src/ui/theme.js +86 -0
- package/src/ui/views.js +127 -0
package/src/project.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {execFile} from 'node:child_process'
|
|
2
|
+
import {basename} from 'node:path'
|
|
3
|
+
import {promisify} from 'node:util'
|
|
4
|
+
|
|
5
|
+
const exec = promisify(execFile)
|
|
6
|
+
|
|
7
|
+
export function parseRemotes(text) {
|
|
8
|
+
const remotes = new Map()
|
|
9
|
+
for (const line of text.split('\n')) {
|
|
10
|
+
const [remote, url] = line.trim().split(/\s+/)
|
|
11
|
+
// Also recognize heroku-accounts SSH aliases (heroku.personal, etc.).
|
|
12
|
+
const match = url?.match(/^(?:https:\/\/git\.heroku\.com\/|(?:ssh:\/\/)?git@(?:git\.)?heroku(?:\.[\w-]+)*(?::|\/))([\w-]+)\.git$/)
|
|
13
|
+
if (match) remotes.set(remote, {remote, app: match[1]})
|
|
14
|
+
}
|
|
15
|
+
return [...remotes.values()]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function inspectProject(cwd = process.cwd()) {
|
|
19
|
+
try {
|
|
20
|
+
const {stdout: root} = await exec('git', ['rev-parse', '--show-toplevel'], {cwd})
|
|
21
|
+
const {stdout: remotes} = await exec('git', ['remote', '-v'], {cwd})
|
|
22
|
+
return {name: basename(root.trim()), root: root.trim(), remotes: parseRemotes(remotes)}
|
|
23
|
+
} catch {
|
|
24
|
+
return {name: null, root: null, remotes: []}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function uniquePipeline(pipelines, nameOrId) {
|
|
29
|
+
const matches = pipelines.filter(p => p.id === nameOrId || p.name === nameOrId)
|
|
30
|
+
if (matches.length > 1) throw new Error(`Multiple pipelines named ${nameOrId}; use --pipeline with an ID: ${matches.map(p => p.id).join(', ')}`)
|
|
31
|
+
if (!matches.length) throw new Error(`Pipeline not found: ${nameOrId}`)
|
|
32
|
+
return matches[0]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function resolveContext(api, catalog, options = {}, project = {}) {
|
|
36
|
+
const byApp = async name => {
|
|
37
|
+
const app = await api.get(`/apps/${encodeURIComponent(name)}`)
|
|
38
|
+
const coupling = await api.coupling(app.id)
|
|
39
|
+
return {app, pipeline: coupling?.pipeline ?? null}
|
|
40
|
+
}
|
|
41
|
+
if (options.app) return {...await byApp(options.app), reason: `App: ${options.app}`}
|
|
42
|
+
if (options.pipeline) return {pipeline: uniquePipeline(catalog.pipelines, options.pipeline), reason: 'Explicit pipeline'}
|
|
43
|
+
if (options.remote) {
|
|
44
|
+
const remote = project.remotes?.find(r => r.remote === options.remote)
|
|
45
|
+
if (!remote) throw new Error(`No Heroku Git remote named ${options.remote} in this repository.`)
|
|
46
|
+
return {...await byApp(remote.app), reason: `Git remote: ${remote.remote}`}
|
|
47
|
+
}
|
|
48
|
+
// An explicit team is a browsing scope, not a request to select a local app.
|
|
49
|
+
if (options.team) return {reason: `Team: ${options.team}`}
|
|
50
|
+
const contexts = []
|
|
51
|
+
const warnings = []
|
|
52
|
+
for (const remote of project.remotes ?? []) {
|
|
53
|
+
try {
|
|
54
|
+
contexts.push(await byApp(remote.app))
|
|
55
|
+
} catch (error) {
|
|
56
|
+
warnings.push(`${remote.remote}: ${error.message}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const pipelineIds = new Set(contexts.map(c => c.pipeline?.id).filter(Boolean))
|
|
60
|
+
if (pipelineIds.size === 1) {
|
|
61
|
+
const context = contexts.find(c => c.pipeline)
|
|
62
|
+
return {pipeline: context.pipeline, reason: 'Pipeline from Git remotes', warnings}
|
|
63
|
+
}
|
|
64
|
+
if (pipelineIds.size > 1) return {reason: 'Git remotes span multiple pipelines; choose one, or use --remote.', warnings}
|
|
65
|
+
if (project.name) {
|
|
66
|
+
const matches = catalog.pipelines.filter(p => p.name === project.name)
|
|
67
|
+
if (matches.length === 1) return {pipeline: matches[0], reason: 'Pipeline matches repository name', warnings}
|
|
68
|
+
if (matches.length > 1) return {reason: 'Repository matches multiple pipelines; choose one, or use --pipeline ID.', warnings}
|
|
69
|
+
}
|
|
70
|
+
if (contexts.length === 1) return {...contexts[0], reason: 'App from Git remote', warnings}
|
|
71
|
+
return {reason: 'Choose a team, pipeline, or app to get started.', warnings}
|
|
72
|
+
}
|