@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 CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@dockstat/repo-cli",
3
- "version": "1.0.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) => `![${b.name}](./${outputDir}/${b.name}.svg)`).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, "&amp;")
6
+ .replace(/</g, "&lt;")
7
+ .replace(/>/g, "&gt;")
8
+ .replace(/"/g, "&quot;")
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 }