@dockstat/repo-cli 1.0.0 ā 1.0.1
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 +6 -1
- package/src/commands/badges.ts +141 -0
- package/src/commands/bundle.ts +119 -0
- package/src/commands/init.ts +39 -0
- package/src/commands/serve.ts +38 -0
- package/src/index.ts +20 -0
- package/src/types.ts +35 -0
- package/src/utils/badge.ts +102 -0
- package/src/utils/contentType.ts +15 -0
- package/src/utils/extract.ts +15 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/repo.ts +11 -0
- package/src/utils/validation.ts +9 -0
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dockstat/repo-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A CLI helper for administering DockStat Repositories",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
7
12
|
"bin": {
|
|
8
13
|
"dockstat-repo": "./dist/dockstore-repo-cli"
|
|
9
14
|
},
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// cli/commands/badges.ts
|
|
2
|
+
import { Command } from "@commander-js/extra-typings"
|
|
3
|
+
import { COLORS, createBadge, type IconName } from "../utils/badge"
|
|
4
|
+
import { log } from "../utils/logger"
|
|
5
|
+
import { loadRepo } from "../utils/repo"
|
|
6
|
+
|
|
7
|
+
export const badgesCommand = new Command("badges")
|
|
8
|
+
.description("Generate SVG badges for the repository")
|
|
9
|
+
.option("-o, --output <dir>", "Output directory for badges", ".badges")
|
|
10
|
+
.option("--style <style>", "Badge style (flat, flat-square)", "flat")
|
|
11
|
+
.option("--plugins", "Generate plugins count badge", true)
|
|
12
|
+
.option("--themes", "Generate themes count badge", true)
|
|
13
|
+
.option("--stacks", "Generate stacks count badge", true)
|
|
14
|
+
.option("--version", "Generate version badge", true)
|
|
15
|
+
.option("--type", "Generate repository type badge", true)
|
|
16
|
+
.option("--status", "Generate build status badge", true)
|
|
17
|
+
.action(async (options, cmd) => {
|
|
18
|
+
const globalOptions = cmd.optsWithGlobals() as unknown as { root: string }
|
|
19
|
+
|
|
20
|
+
const repoData = await loadRepo(globalOptions.root)
|
|
21
|
+
if (!repoData) {
|
|
22
|
+
console.error(`ā Repository file not found: ${globalOptions.root}`)
|
|
23
|
+
console.error(" Run 'init' first to create a repository.")
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { config, content } = repoData
|
|
28
|
+
const style = options.style as "flat" | "flat-square"
|
|
29
|
+
const outputDir = options.output
|
|
30
|
+
|
|
31
|
+
console.log("\nš·ļø DockStat Badge Generator\n")
|
|
32
|
+
|
|
33
|
+
const badges: { name: string; svg: string }[] = []
|
|
34
|
+
|
|
35
|
+
if (options.plugins) {
|
|
36
|
+
const count = content.plugins.length
|
|
37
|
+
badges.push({
|
|
38
|
+
name: "plugins",
|
|
39
|
+
svg: createBadge({
|
|
40
|
+
label: "plugins",
|
|
41
|
+
message: count.toString(),
|
|
42
|
+
color: count > 0 ? COLORS.blue : COLORS.lightgrey,
|
|
43
|
+
icon: "puzzle",
|
|
44
|
+
style,
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (options.themes) {
|
|
50
|
+
const count = content.themes.length
|
|
51
|
+
badges.push({
|
|
52
|
+
name: "themes",
|
|
53
|
+
svg: createBadge({
|
|
54
|
+
label: "themes",
|
|
55
|
+
message: count.toString(),
|
|
56
|
+
color: count > 0 ? COLORS.purple : COLORS.lightgrey,
|
|
57
|
+
icon: "palette",
|
|
58
|
+
style,
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.stacks) {
|
|
64
|
+
const count = content.stacks.length
|
|
65
|
+
badges.push({
|
|
66
|
+
name: "stacks",
|
|
67
|
+
svg: createBadge({
|
|
68
|
+
label: "stacks",
|
|
69
|
+
message: count.toString(),
|
|
70
|
+
color: count > 0 ? COLORS.teal : COLORS.lightgrey,
|
|
71
|
+
icon: "layers",
|
|
72
|
+
style,
|
|
73
|
+
}),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (options.type) {
|
|
78
|
+
const typeConfig: Record<string, { color: string; icon: IconName }> = {
|
|
79
|
+
github: { color: COLORS.grey, icon: "github" },
|
|
80
|
+
gitlab: { color: COLORS.orange, icon: "gitlab" },
|
|
81
|
+
gitea: { color: COLORS.green, icon: "server" },
|
|
82
|
+
http: { color: COLORS.blue, icon: "globe" },
|
|
83
|
+
local: { color: COLORS.lightgrey, icon: "folder" },
|
|
84
|
+
}
|
|
85
|
+
const cfg = typeConfig[config.type] ?? {
|
|
86
|
+
color: COLORS.grey,
|
|
87
|
+
icon: "server" as IconName,
|
|
88
|
+
}
|
|
89
|
+
badges.push({
|
|
90
|
+
name: "type",
|
|
91
|
+
svg: createBadge({
|
|
92
|
+
label: "repo",
|
|
93
|
+
message: config.type,
|
|
94
|
+
color: cfg.color,
|
|
95
|
+
icon: cfg.icon,
|
|
96
|
+
style,
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.status) {
|
|
102
|
+
const isStrict = config.policy === "strict"
|
|
103
|
+
badges.push({
|
|
104
|
+
name: "policy",
|
|
105
|
+
svg: createBadge({
|
|
106
|
+
label: "policy",
|
|
107
|
+
message: config.policy,
|
|
108
|
+
color: isStrict ? COLORS.green : COLORS.yellow,
|
|
109
|
+
icon: isStrict ? "shieldCheck" : "shield",
|
|
110
|
+
style,
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const badge of badges) {
|
|
116
|
+
const path = `${outputDir}/${badge.name}.svg`
|
|
117
|
+
await Bun.write(path, badge.svg)
|
|
118
|
+
log("ā
", `Created ${badge.name} badge`, path)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mdSnippet = badges.map((b) => ``).join(" ")
|
|
122
|
+
|
|
123
|
+
const mdPath = `${outputDir}/README.md`
|
|
124
|
+
const mdContent = `# Repository Badges
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
\`\`\`markdown
|
|
129
|
+
${mdSnippet}
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
## Preview
|
|
133
|
+
|
|
134
|
+
${mdSnippet}
|
|
135
|
+
`
|
|
136
|
+
|
|
137
|
+
await Bun.write(mdPath, mdContent)
|
|
138
|
+
log("š", "Created badge documentation", mdPath)
|
|
139
|
+
|
|
140
|
+
console.log(`\n⨠Generated ${badges.length} badge(s) in ${outputDir}/\n`)
|
|
141
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// cli/commands/bundle.ts
|
|
2
|
+
import { Command } from "@commander-js/extra-typings"
|
|
3
|
+
import { PluginMeta } from "@dockstat/typings/schemas"
|
|
4
|
+
import { Glob } from "bun"
|
|
5
|
+
import type { BuildResult } from "../types"
|
|
6
|
+
import { extractMeta } from "../utils/extract"
|
|
7
|
+
import { log, printSummary } from "../utils/logger"
|
|
8
|
+
import { loadRepo, saveRepo } from "../utils/repo"
|
|
9
|
+
import { ajv, validateMeta } from "../utils/validation"
|
|
10
|
+
|
|
11
|
+
export const bundleCommand = new Command("bundle")
|
|
12
|
+
.description("Bundles all plugins and updates the repository manifest")
|
|
13
|
+
.option("--schema <path>", "Output path for plugin meta schema")
|
|
14
|
+
.option("--minify", "Minify bundled output", true)
|
|
15
|
+
.option("--sourcemap", "Generate sourcemaps", true)
|
|
16
|
+
.action(async (options, cmd) => {
|
|
17
|
+
const globalOptions = cmd.optsWithGlobals() as unknown as { root: string }
|
|
18
|
+
|
|
19
|
+
const repoData = await loadRepo(globalOptions.root)
|
|
20
|
+
if (!repoData) {
|
|
21
|
+
console.error(`ā Repository file not found: ${globalOptions.root}`)
|
|
22
|
+
console.error(" Run 'init' first to create a repository.")
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { dir: pluginDir, bundle: bundleDir } = repoData.config.plugins
|
|
27
|
+
|
|
28
|
+
console.log("\nš DockStat Plugin Bundler\n")
|
|
29
|
+
|
|
30
|
+
const glob = new Glob(`${pluginDir}/*/index.ts`)
|
|
31
|
+
const pluginPaths = [...glob.scanSync()]
|
|
32
|
+
|
|
33
|
+
if (pluginPaths.length === 0) {
|
|
34
|
+
console.log("ā ļø No plugins found")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log("š", `Found ${pluginPaths.length} plugin(s)`)
|
|
39
|
+
console.log()
|
|
40
|
+
|
|
41
|
+
const results: BuildResult[] = []
|
|
42
|
+
|
|
43
|
+
for (const pluginPath of pluginPaths) {
|
|
44
|
+
const name = pluginPath.replace("/index.ts", "").replace(`${pluginDir}/`, "")
|
|
45
|
+
const outdir = pluginPath.replace("/index.ts", `/${bundleDir}`)
|
|
46
|
+
|
|
47
|
+
log("š¦", `Building ${name}...`)
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const build = await Bun.build({
|
|
51
|
+
entrypoints: [pluginPath],
|
|
52
|
+
outdir,
|
|
53
|
+
minify: options.minify,
|
|
54
|
+
sourcemap: options.sourcemap ? "external" : "none",
|
|
55
|
+
splitting: false,
|
|
56
|
+
env: `${name.toUpperCase()}_*`,
|
|
57
|
+
banner: "/* Bundled by DockStat */",
|
|
58
|
+
target: "bun",
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (build.logs?.length) {
|
|
62
|
+
for (const entry of build.logs) {
|
|
63
|
+
console.log(` ā ļø [${entry.level}] ${entry.message}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const imported = await import(`${process.cwd()}/${outdir}/index.js`)
|
|
68
|
+
|
|
69
|
+
if (!imported.default) {
|
|
70
|
+
throw new Error("No default export found. Ensure plugin uses pluginBuilder.")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const plugin = imported.default
|
|
74
|
+
const meta =
|
|
75
|
+
typeof plugin.build === "function" ? extractMeta(plugin.build()) : extractMeta(plugin)
|
|
76
|
+
|
|
77
|
+
if (!validateMeta(meta)) {
|
|
78
|
+
const errors = ajv.errorsText(validateMeta.errors, { separator: "\n - " })
|
|
79
|
+
throw new Error(`Invalid metadata:\n - ${errors}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
log("ā
", name, `${outdir}/index.js`)
|
|
83
|
+
results.push({ name, success: true, meta })
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
86
|
+
console.error(`ā ${name}: ${message.split("\n")[0]}`)
|
|
87
|
+
results.push({ name, success: false, error: message })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Write schema if requested
|
|
92
|
+
if (options.schema) {
|
|
93
|
+
console.log()
|
|
94
|
+
log("š", "Writing schema", options.schema)
|
|
95
|
+
await Bun.write(options.schema, JSON.stringify(PluginMeta, null, 2))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Update repo file
|
|
99
|
+
// biome-ignore lint/style/noNonNullAssertion: Is checked
|
|
100
|
+
const successfulPlugins = results.filter((r) => r.success && r.meta).map((r) => r.meta!)
|
|
101
|
+
repoData.content.plugins = successfulPlugins
|
|
102
|
+
await saveRepo(globalOptions.root, repoData)
|
|
103
|
+
log("š", `Updated ${globalOptions.root}`, `${successfulPlugins.length} plugin(s)`)
|
|
104
|
+
|
|
105
|
+
const succeeded = results.filter((r) => r.success).length
|
|
106
|
+
const failed = results.filter((r) => !r.success).length
|
|
107
|
+
|
|
108
|
+
printSummary(succeeded, failed)
|
|
109
|
+
|
|
110
|
+
if (failed > 0) {
|
|
111
|
+
console.log("\nā Failed plugins:")
|
|
112
|
+
for (const r of results.filter((r) => !r.success)) {
|
|
113
|
+
console.log(` - ${r.name}: ${r.error?.split("\n")[0]}`)
|
|
114
|
+
}
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log("\n⨠All plugins built successfully!\n")
|
|
119
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// cli/commands/init.ts
|
|
2
|
+
import { Command } from "@commander-js/extra-typings"
|
|
3
|
+
import { repo } from "@dockstat/utils"
|
|
4
|
+
import type { RepoFile } from "../types"
|
|
5
|
+
|
|
6
|
+
export const initCommand = new Command("init")
|
|
7
|
+
.description("Initializes a new repository")
|
|
8
|
+
.option("-t, --themes-dir <path>", "Themes directory", "./content/themes")
|
|
9
|
+
.option("-p, --plugin-dir <path>", "Plugins directory", "./content/plugins")
|
|
10
|
+
.option("--plugin-bundle <dir>", "Plugin bundle output directory", "bundle")
|
|
11
|
+
.option("-s, --stack-dir <path>", "Stacks directory", "./content/stacks")
|
|
12
|
+
.requiredOption("-n, --name <name>", "The name of the repository")
|
|
13
|
+
.option("-r, --relaxed", "Use relaxed verification", false)
|
|
14
|
+
.option("-a, --verification-api <URL>", "Verification API base URL", undefined)
|
|
15
|
+
.requiredOption("-v, --variant <type>", "Repository type (github, gitlab, gitea, http, local)")
|
|
16
|
+
.action(async (options, cmd) => {
|
|
17
|
+
const globalOptions = cmd.optsWithGlobals() as unknown as { root: string }
|
|
18
|
+
|
|
19
|
+
if (!repo.isRepoType(options.variant)) {
|
|
20
|
+
console.error("ā Invalid variant. Use: github, gitlab, gitea, http, or local")
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data: RepoFile = {
|
|
25
|
+
config: {
|
|
26
|
+
name: options.name,
|
|
27
|
+
policy: options.relaxed ? "relaxed" : "strict",
|
|
28
|
+
type: options.variant,
|
|
29
|
+
verification_api: (options.verificationApi && String(options.verificationApi)) || null,
|
|
30
|
+
themes: { dir: options.themesDir },
|
|
31
|
+
plugins: { dir: options.pluginDir, bundle: options.pluginBundle },
|
|
32
|
+
stacks: { dir: options.stackDir },
|
|
33
|
+
},
|
|
34
|
+
content: { plugins: [], themes: [], stacks: [] },
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await Bun.write(globalOptions.root, JSON.stringify(data, null, 2))
|
|
38
|
+
console.log(`ā
Created repository config: ${globalOptions.root}`)
|
|
39
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { extname } from "node:path"
|
|
2
|
+
import { Command } from "@commander-js/extra-typings"
|
|
3
|
+
import { contentType } from "../utils/contentType"
|
|
4
|
+
|
|
5
|
+
export const serveCommand = new Command("serve")
|
|
6
|
+
.description(
|
|
7
|
+
"Serves the current working directory via Bun.serve - Really rudimentair and only recommended for testing or for deployments behind a reverse proxy"
|
|
8
|
+
)
|
|
9
|
+
.option("-p, --port <port>", "The port on which the server should listen", "8080")
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
Bun.serve({
|
|
12
|
+
port: options.port,
|
|
13
|
+
async fetch(req) {
|
|
14
|
+
const url = new URL(req.url)
|
|
15
|
+
|
|
16
|
+
console.log("Received request:", req.url)
|
|
17
|
+
|
|
18
|
+
let filePath = decodeURIComponent(url.pathname)
|
|
19
|
+
|
|
20
|
+
if (filePath.charAt(0) === "/") {
|
|
21
|
+
filePath = filePath.replace("/", "")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.debug("File Path:", filePath)
|
|
25
|
+
|
|
26
|
+
const file = Bun.file(filePath)
|
|
27
|
+
if (!(await file.exists())) {
|
|
28
|
+
return new Response("Not found", { status: 404 })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new Response(file, {
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": contentType(extname(filePath)),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "@commander-js/extra-typings"
|
|
2
|
+
import { badgesCommand } from "./commands/badges"
|
|
3
|
+
import { bundleCommand } from "./commands/bundle"
|
|
4
|
+
import { initCommand } from "./commands/init"
|
|
5
|
+
import { serveCommand } from "./commands/serve"
|
|
6
|
+
|
|
7
|
+
const program = new Command()
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name("dockstat-repo")
|
|
11
|
+
.description("A CLI helper for managing DockStat repositories")
|
|
12
|
+
.version("0.0.1")
|
|
13
|
+
.option("-r, --root <repo.json>", "Defines the repository root file", "repo.json")
|
|
14
|
+
|
|
15
|
+
program.addCommand(initCommand)
|
|
16
|
+
program.addCommand(bundleCommand)
|
|
17
|
+
program.addCommand(badgesCommand)
|
|
18
|
+
program.addCommand(serveCommand)
|
|
19
|
+
|
|
20
|
+
program.parse()
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// cli/types.ts
|
|
2
|
+
import type { CreateRepoType, PluginMetaType } from "@dockstat/typings/types"
|
|
3
|
+
|
|
4
|
+
export interface Opts extends Omit<CreateRepoType, "source"> {
|
|
5
|
+
root: string
|
|
6
|
+
themes: { dir: string }
|
|
7
|
+
plugins: { dir: string; bundle: string }
|
|
8
|
+
stacks: { dir: string }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RepoFile {
|
|
12
|
+
config: Omit<Opts, "root">
|
|
13
|
+
content: {
|
|
14
|
+
plugins: PluginMetaType[]
|
|
15
|
+
themes: unknown[]
|
|
16
|
+
stacks: unknown[]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BuildResult {
|
|
21
|
+
name: string
|
|
22
|
+
success: boolean
|
|
23
|
+
meta?: PluginMetaType
|
|
24
|
+
error?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// cli/types.ts (add or update)
|
|
28
|
+
export interface BadgeOptions {
|
|
29
|
+
label: string
|
|
30
|
+
message: string
|
|
31
|
+
color: string
|
|
32
|
+
labelColor?: string
|
|
33
|
+
style?: "flat" | "flat-square"
|
|
34
|
+
icon?: string
|
|
35
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { BadgeOptions } from "../types"
|
|
2
|
+
|
|
3
|
+
function escapeXml(str: string): string {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function measureText(text: string): number {
|
|
12
|
+
const avgCharWidth = 6.8
|
|
13
|
+
return Math.ceil(text.length * avgCharWidth) + 14
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 16x16 icon paths (from Lucide/Heroicons style)
|
|
17
|
+
export const ICONS = {
|
|
18
|
+
plugin: `<path d="M12 2v4m0 12v4M2 12h4m12 0h4m-5.66-5.66l2.83-2.83m-11.31 0l2.83 2.83m5.65 5.66l2.83 2.83m-11.31 0l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>`,
|
|
19
|
+
puzzle: `<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.077.877.528 1.073 1.01a2.5 2.5 0 1 0 3.259-3.259c-.482-.196-.933-.558-1.01-1.073-.05-.336.062-.676.303-.917l1.525-1.525A2.402 2.402 0 0 1 12 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.878.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
20
|
+
palette: `<path d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75c.969 0 1.781-.774 1.781-1.781 0-.466-.18-.903-.506-1.22a1.766 1.766 0 0 1-.507-1.219c0-.969.774-1.78 1.781-1.78h2.101c3.206 0 5.85-2.644 5.85-5.85 0-4.965-4.365-8.15-9.75-8.15Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="7.5" cy="10.5" r="1" fill="currentColor"/><circle cx="10.5" cy="7.5" r="1" fill="currentColor"/><circle cx="13.5" cy="7.5" r="1" fill="currentColor"/><circle cx="16.5" cy="10.5" r="1" fill="currentColor"/>`,
|
|
21
|
+
layers: `<path d="m12 2 9 4.5-9 4.5-9-4.5L12 2Zm0 0v4.5m9 4.5-9 4.5-9-4.5m18 5-9 4.5-9-4.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
22
|
+
github: `<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.341-3.369-1.341-.454-1.155-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.268 2.75 1.026A9.578 9.578 0 0 1 12 6.836a9.59 9.59 0 0 1 2.504.337c1.909-1.294 2.747-1.026 2.747-1.026.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10Z" fill="currentColor"/>`,
|
|
23
|
+
gitlab: `<path d="m22 13.29-3.5-10.74a.86.86 0 0 0-.81-.55.88.88 0 0 0-.84.61l-2.36 7.25H9.51L7.15 2.61a.88.88 0 0 0-.84-.61.86.86 0 0 0-.81.55L2 13.29a1.72 1.72 0 0 0 .61 1.92l9.06 6.59a.35.35 0 0 0 .66 0l9.06-6.59a1.72 1.72 0 0 0 .61-1.92Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
24
|
+
server: `<rect x="3" y="3" width="18" height="6" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="3" y="15" width="18" height="6" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="7" cy="6" r="1" fill="currentColor"/><circle cx="7" cy="18" r="1" fill="currentColor"/>`,
|
|
25
|
+
folder: `<path d="M3 6.75A2.25 2.25 0 0 1 5.25 4.5h3.879a1.5 1.5 0 0 1 1.06.44l1.122 1.12a1.5 1.5 0 0 0 1.06.44h6.379A2.25 2.25 0 0 1 21 8.75v9a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 17.75v-11Z" fill="none" stroke="currentColor" stroke-width="1.5"/>`,
|
|
26
|
+
globe: `<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3 12h18M12 3a15.3 15.3 0 0 1 4 9 15.3 15.3 0 0 1-4 9 15.3 15.3 0 0 1-4-9 15.3 15.3 0 0 1 4-9Z" fill="none" stroke="currentColor" stroke-width="1.5"/>`,
|
|
27
|
+
shield: `<path d="M12 3.5 3.5 7v4.5c0 5.25 3.625 10.125 8.5 11.5 4.875-1.375 8.5-6.25 8.5-11.5V7L12 3.5Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
28
|
+
shieldCheck: `<path d="M12 3.5 3.5 7v4.5c0 5.25 3.625 10.125 8.5 11.5 4.875-1.375 8.5-6.25 8.5-11.5V7L12 3.5Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="m9 12 2 2 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
29
|
+
check: `<path d="M4.5 12.75l6 6 9-13.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
30
|
+
tag: `<path d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="6" r="1" fill="currentColor"/>`,
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
export type IconName = keyof typeof ICONS
|
|
34
|
+
|
|
35
|
+
export interface BadgeOptionsWithIcon extends BadgeOptions {
|
|
36
|
+
icon?: IconName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createBadge(options: BadgeOptionsWithIcon): string {
|
|
40
|
+
const { label, message, color, labelColor = "#1e293b", style = "flat", icon } = options
|
|
41
|
+
|
|
42
|
+
const iconSize = 14
|
|
43
|
+
const iconPadding = icon ? iconSize + 6 : 0
|
|
44
|
+
const labelWidth = measureText(label) + iconPadding
|
|
45
|
+
const messageWidth = measureText(message)
|
|
46
|
+
const totalWidth = labelWidth + messageWidth + 4
|
|
47
|
+
const height = 26
|
|
48
|
+
const radius = style === "flat-square" ? 4 : 13
|
|
49
|
+
|
|
50
|
+
const uid = Math.random().toString(36).slice(2, 8)
|
|
51
|
+
|
|
52
|
+
const iconSvg = icon
|
|
53
|
+
? `<g transform="translate(8, ${(height - iconSize) / 2})" color="#fff" opacity="0.9">
|
|
54
|
+
<svg width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="none">
|
|
55
|
+
${ICONS[icon]}
|
|
56
|
+
</svg>
|
|
57
|
+
</g>`
|
|
58
|
+
: ""
|
|
59
|
+
|
|
60
|
+
const labelX = icon ? iconPadding + (labelWidth - iconPadding) / 2 : labelWidth / 2
|
|
61
|
+
|
|
62
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
|
|
63
|
+
<title>${escapeXml(label)}: ${escapeXml(message)}</title>
|
|
64
|
+
<defs>
|
|
65
|
+
<linearGradient id="grad-${uid}" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
66
|
+
<stop offset="0%" stop-color="#fff" stop-opacity="0.12"/>
|
|
67
|
+
<stop offset="100%" stop-color="#000" stop-opacity="0.08"/>
|
|
68
|
+
</linearGradient>
|
|
69
|
+
<filter id="shadow-${uid}" x="-5%" y="-5%" width="110%" height="120%">
|
|
70
|
+
<feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-opacity="0.2"/>
|
|
71
|
+
</filter>
|
|
72
|
+
</defs>
|
|
73
|
+
<g filter="url(#shadow-${uid})">
|
|
74
|
+
<rect width="${totalWidth}" height="${height}" rx="${radius}" fill="${labelColor}"/>
|
|
75
|
+
<rect x="${labelWidth}" width="${messageWidth + 4}" height="${height}" rx="${radius}" fill="${color}"/>
|
|
76
|
+
<rect x="${labelWidth}" width="${Math.min(radius, messageWidth)}" height="${height}" fill="${color}"/>
|
|
77
|
+
<rect width="${totalWidth}" height="${height}" rx="${radius}" fill="url(#grad-${uid})"/>
|
|
78
|
+
</g>
|
|
79
|
+
${iconSvg}
|
|
80
|
+
<g fill="#fff" text-anchor="middle" font-family="system-ui,-apple-system,BlinkMacSystemFont,sans-serif" font-weight="600" font-size="11">
|
|
81
|
+
<text x="${labelX}" y="17" opacity="0.95">${escapeXml(label)}</text>
|
|
82
|
+
<text x="${labelWidth + messageWidth / 2 + 2}" y="17">${escapeXml(message)}</text>
|
|
83
|
+
</g>
|
|
84
|
+
</svg>`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const COLORS = {
|
|
88
|
+
green: "#10b981",
|
|
89
|
+
brightgreen: "#22c55e",
|
|
90
|
+
yellow: "#f59e0b",
|
|
91
|
+
yellowgreen: "#84cc16",
|
|
92
|
+
orange: "#f97316",
|
|
93
|
+
red: "#ef4444",
|
|
94
|
+
blue: "#3b82f6",
|
|
95
|
+
lightgrey: "#64748b",
|
|
96
|
+
grey: "#475569",
|
|
97
|
+
purple: "#8b5cf6",
|
|
98
|
+
pink: "#ec4899",
|
|
99
|
+
cyan: "#06b6d4",
|
|
100
|
+
indigo: "#6366f1",
|
|
101
|
+
teal: "#14b8a6",
|
|
102
|
+
} as const
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function contentType(ext: string) {
|
|
2
|
+
switch (ext) {
|
|
3
|
+
case ".json":
|
|
4
|
+
return "application/json"
|
|
5
|
+
case ".yaml":
|
|
6
|
+
case ".yml":
|
|
7
|
+
return "application/yaml"
|
|
8
|
+
case ".txt":
|
|
9
|
+
case ".log":
|
|
10
|
+
case ".md":
|
|
11
|
+
return "text/plain"
|
|
12
|
+
default:
|
|
13
|
+
return "text/plain"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// cli/utils/extract.ts
|
|
2
|
+
import type { PluginMetaType } from "@dockstat/typings/types"
|
|
3
|
+
|
|
4
|
+
export function extractMeta(plugin: Record<string, unknown>): PluginMetaType {
|
|
5
|
+
return {
|
|
6
|
+
name: plugin.name,
|
|
7
|
+
description: plugin.description,
|
|
8
|
+
version: plugin.version,
|
|
9
|
+
repository: plugin.repository,
|
|
10
|
+
repoType: plugin.repoType,
|
|
11
|
+
manifest: plugin.manifest,
|
|
12
|
+
author: plugin.author,
|
|
13
|
+
tags: plugin.tags,
|
|
14
|
+
} as PluginMetaType
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// cli/utils/logger.ts
|
|
2
|
+
export function log(icon: string, message: string, detail?: string) {
|
|
3
|
+
const detailStr = detail ? ` ā ${detail}` : ""
|
|
4
|
+
console.log(`${icon} ${message}${detailStr}`)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function logError(title: string, error: unknown) {
|
|
8
|
+
console.error(`\n${"=".repeat(70)}`)
|
|
9
|
+
console.error(`ā ${title}`)
|
|
10
|
+
console.error("=".repeat(70))
|
|
11
|
+
|
|
12
|
+
if (error instanceof AggregateError) {
|
|
13
|
+
console.error(`\nBuild errors (${error.errors.length}):`)
|
|
14
|
+
for (const err of error.errors) {
|
|
15
|
+
if (err && typeof err === "object") {
|
|
16
|
+
const buildErr = err as {
|
|
17
|
+
message?: string
|
|
18
|
+
position?: { file?: string; line?: number; column?: number }
|
|
19
|
+
level?: string
|
|
20
|
+
}
|
|
21
|
+
const pos = buildErr.position
|
|
22
|
+
const location = pos ? `${pos.file || "unknown"}:${pos.line || 0}:${pos.column || 0}` : ""
|
|
23
|
+
console.error(` - [${buildErr.level || "error"}] ${buildErr.message || String(err)}`)
|
|
24
|
+
if (location) console.error(` at ${location}`)
|
|
25
|
+
} else {
|
|
26
|
+
console.error(` - ${String(err)}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} else if (error instanceof Error) {
|
|
30
|
+
console.error(`\nMessage: ${error.message}`)
|
|
31
|
+
if (error.cause) console.error(`Cause: ${JSON.stringify(error.cause, null, 2)}`)
|
|
32
|
+
if (error.stack) console.error(`\nStack:\n${error.stack}`)
|
|
33
|
+
} else {
|
|
34
|
+
console.error(String(error))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.error(`\n${"=".repeat(70)}\n`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function printSummary(succeeded: number, failed: number) {
|
|
41
|
+
console.log(`\n${"=".repeat(50)}`)
|
|
42
|
+
console.log(`š Summary: ${succeeded} succeeded, ${failed} failed`)
|
|
43
|
+
console.log("=".repeat(50))
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RepoFile } from "../types"
|
|
2
|
+
|
|
3
|
+
export async function loadRepo(path: string): Promise<RepoFile | null> {
|
|
4
|
+
const file = Bun.file(path)
|
|
5
|
+
if (!(await file.exists())) return null
|
|
6
|
+
return file.json()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function saveRepo(path: string, data: RepoFile): Promise<void> {
|
|
10
|
+
await Bun.write(path, JSON.stringify(data, null, 2))
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { WrappedPluginMeta } from "@dockstat/typings/schemas"
|
|
2
|
+
import Ajv from "ajv"
|
|
3
|
+
import addFormats from "ajv-formats"
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ allErrors: true, strict: false })
|
|
6
|
+
addFormats(ajv)
|
|
7
|
+
|
|
8
|
+
export const validateMeta = ajv.compile(WrappedPluginMeta)
|
|
9
|
+
export { ajv }
|