@justanarthur/just-github-actions-n-workflows-cli 0.0.0-beta.10

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 ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@justanarthur/just-github-actions-n-workflows-cli",
3
+ "version": "0.0.0-beta.10",
4
+ "description": "Release automation toolkit — version bumping, npm publishing, docker publishing, VPS deployment via GitHub Actions composite actions",
5
+ "type": "module",
6
+ "bin": {
7
+ "just-github-actions-n-workflows": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "publish": "bun publish --access public"
11
+ },
12
+ "files": [
13
+ "src/**/*.ts",
14
+ "package.json",
15
+ "README.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/justAnArthur/just-github-actions-n-workflows"
23
+ },
24
+ "dependencies": {
25
+ "@oclif/core": "^4",
26
+ "@inquirer/prompts": "^7"
27
+ },
28
+ "oclif": {
29
+ "commands": "./src/commands",
30
+ "default": "init"
31
+ },
32
+ "properties": {
33
+ "gitCommitScopeRelatedNames": "cli"
34
+ }
35
+ }
@@ -0,0 +1,265 @@
1
+ import { Args, Command, Flags, ux } from "@oclif/core"
2
+ import { checkbox, select } from "@inquirer/prompts"
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+
6
+ import {
7
+ REPO,
8
+ enrichWorkflows,
9
+ fetchTags,
10
+ fetchWorkflowContent,
11
+ fetchWorkflowList,
12
+ type WorkflowEntry,
13
+ } from "../github.js"
14
+
15
+ export default class Init extends Command {
16
+ static override description = "Scaffold workflow files into .github/workflows/ of the current repo"
17
+
18
+ static override examples = [
19
+ "<%= config.bin %> init",
20
+ "<%= config.bin %> init bump-version",
21
+ "<%= config.bin %> init --ref v1.0.0",
22
+ "<%= config.bin %> init --list",
23
+ "<%= config.bin %> init --yes --force",
24
+ ]
25
+
26
+ static override args = {
27
+ workflows: Args.string({
28
+ description: "Specific workflow names to install (space-separated)",
29
+ required: false,
30
+ }),
31
+ }
32
+
33
+ static override strict = false
34
+
35
+ static override flags = {
36
+ list: Flags.boolean({
37
+ char: "l",
38
+ description: "Show available workflows",
39
+ default: false,
40
+ }),
41
+ force: Flags.boolean({
42
+ char: "f",
43
+ description: "Overwrite existing workflow files",
44
+ default: false,
45
+ }),
46
+ yes: Flags.boolean({
47
+ char: "y",
48
+ description: "Skip interactive prompts, install all workflows",
49
+ default: false,
50
+ }),
51
+ ref: Flags.string({
52
+ description: "Git ref to fetch from (branch, tag, or sha)",
53
+ default: "main",
54
+ }),
55
+ }
56
+
57
+ async run(): Promise<void> {
58
+ const { argv, flags } = await this.parse(Init)
59
+ const positional = argv as string[]
60
+
61
+ // --- list mode ---
62
+
63
+ if (flags.list) {
64
+ await this.listWorkflows(flags.ref)
65
+ return
66
+ }
67
+
68
+ // --- interactive / install mode ---
69
+
70
+ this.log()
71
+ this.log(ux.colorize("bold", " just-github-actions-n-workflows"))
72
+ this.log(ux.colorize("dim", " release automation toolkit\n"))
73
+
74
+ const ref = await this.resolveRef(flags, positional)
75
+ const selected = await this.selectWorkflows(ref, flags, positional)
76
+
77
+ if (selected.length === 0) {
78
+ this.log(ux.colorize("yellow", "\n no workflows selected — nothing to do.\n"))
79
+ return
80
+ }
81
+
82
+ const { created, skipped } = await this.installWorkflows(selected, ref, flags)
83
+
84
+ this.log(`\n done — ${ux.colorize("green", `${created} created`)}, ${skipped} skipped\n`)
85
+
86
+ this.printSecretsReminder(selected)
87
+ this.printNextSteps(created, selected)
88
+ }
89
+
90
+ // ── step 1: resolve ref ────────────────────────────────
91
+
92
+ private async resolveRef(
93
+ flags: { ref: string; yes: boolean },
94
+ positional: string[]
95
+ ): Promise<string> {
96
+ if (flags.ref !== "main" || positional.length > 0 || flags.yes) {
97
+ return flags.ref
98
+ }
99
+
100
+ this.log(ux.colorize("bold", " step 1 — select version\n"))
101
+
102
+ const tags = await fetchTags()
103
+
104
+ if (tags.length === 0) return "main"
105
+
106
+ const choices = [
107
+ { name: `main ${ux.colorize("dim", "(latest)")}`, value: "main" },
108
+ ...tags.slice(0, 9).map((tag) => ({ name: tag, value: tag })),
109
+ ]
110
+
111
+ const ref = await select({
112
+ message: "Pick a version",
113
+ choices,
114
+ default: "main",
115
+ })
116
+
117
+ this.log(ux.colorize("green", `\n → using ${ref}\n`))
118
+ return ref
119
+ }
120
+
121
+ // ── step 2: select workflows ───────────────────────────
122
+
123
+ private async selectWorkflows(
124
+ ref: string,
125
+ flags: { yes: boolean },
126
+ positional: string[]
127
+ ): Promise<WorkflowEntry[]> {
128
+ let available = await fetchWorkflowList(ref)
129
+ available = await enrichWorkflows(available, ref)
130
+
131
+ if (positional.length > 0) {
132
+ return positional.map((name) => {
133
+ const key = name.replace(/\.yml$/, "")
134
+ const wf = available.find((w) => w.name === key)
135
+ if (!wf) {
136
+ this.error(`Unknown workflow "${name}". Available: ${available.map((w) => w.name).join(", ")}`)
137
+ }
138
+ return wf!
139
+ })
140
+ }
141
+
142
+ if (flags.yes) return available
143
+
144
+ this.log(ux.colorize("bold", " step 2 — select workflows\n"))
145
+
146
+ const selected = await checkbox({
147
+ message: "Select workflows to install",
148
+ choices: available.map((wf) => {
149
+ const secretNames = wf.secrets?.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ")
150
+ const hint = [
151
+ wf.description,
152
+ secretNames ? `requires: ${secretNames}` : null,
153
+ ].filter(Boolean).join(" · ")
154
+
155
+ return {
156
+ name: `${wf.name.padEnd(26)} ${ux.colorize("dim", hint || wf.file)}`,
157
+ value: wf.name,
158
+ checked: true,
159
+ }
160
+ }),
161
+ })
162
+
163
+ return available.filter((w) => selected.includes(w.name))
164
+ }
165
+
166
+ // ── step 3: install ────────────────────────────────────
167
+
168
+ private async installWorkflows(
169
+ selected: WorkflowEntry[],
170
+ ref: string,
171
+ flags: { force: boolean; yes: boolean }
172
+ ): Promise<{ created: number; skipped: number }> {
173
+ const targetDir = join(process.cwd(), ".github", "workflows")
174
+ mkdirSync(targetDir, { recursive: true })
175
+
176
+ if (!flags.yes) {
177
+ this.log(ux.colorize("bold", " step 3 — install\n"))
178
+ }
179
+
180
+ this.log(ux.colorize("dim", ` fetching from ${REPO} @ ${ref}\n`))
181
+
182
+ let created = 0
183
+ let skipped = 0
184
+
185
+ for (const workflow of selected) {
186
+ const targetPath = join(targetDir, workflow.file)
187
+
188
+ if (!flags.force && existsSync(targetPath)) {
189
+ this.log(` ${ux.colorize("yellow", "skip")} ${workflow.file} ${ux.colorize("dim", "(already exists, use --force)")}`)
190
+ skipped++
191
+ continue
192
+ }
193
+
194
+ try {
195
+ const content = await fetchWorkflowContent(workflow.file, ref)
196
+ writeFileSync(targetPath, content, "utf-8")
197
+ this.log(` ${ux.colorize("green", "create")} .github/workflows/${workflow.file}`)
198
+ created++
199
+ } catch (error: any) {
200
+ this.log(` ${ux.colorize("red", "error")} ${workflow.file}: ${error.message}`)
201
+ }
202
+ }
203
+
204
+ return { created, skipped }
205
+ }
206
+
207
+ // ── step 4: secrets reminder ───────────────────────────
208
+
209
+ private printSecretsReminder(selected: WorkflowEntry[]): void {
210
+ const allSecrets = new Map<string, string>()
211
+ for (const wf of selected) {
212
+ for (const s of wf.secrets || []) {
213
+ const [name, ...descParts] = s.split(/\s+[—–]\s*/)
214
+ if (name && !allSecrets.has(name.trim())) {
215
+ allSecrets.set(name.trim(), descParts.join(" — ").trim())
216
+ }
217
+ }
218
+ }
219
+
220
+ if (allSecrets.size === 0) return
221
+
222
+ this.log(ux.colorize("bold", " required secrets:\n"))
223
+ for (const [name, desc] of allSecrets) {
224
+ this.log(` • ${ux.colorize("cyan", name.padEnd(18))} ${ux.colorize("dim", desc)}`)
225
+ }
226
+ this.log(ux.colorize("dim", `\n set these in your repo → Settings → Secrets → Actions\n`))
227
+ }
228
+
229
+ // ── next steps ─────────────────────────────────────────
230
+
231
+ private printNextSteps(created: number, selected: WorkflowEntry[]): void {
232
+ if (created === 0) return
233
+
234
+ const hasSecrets = selected.some((w) => w.secrets && w.secrets.length > 0)
235
+
236
+ this.log(ux.colorize("bold", " next steps:\n"))
237
+ this.log(` 1. ${hasSecrets ? "set the secrets listed above" : "set the GH_TOKEN secret in your repo settings"}`)
238
+ this.log(` 2. adjust push.branches / push.tags triggers for your repo`)
239
+ this.log(` 3. commit and push:`)
240
+ this.log(ux.colorize("dim", ` git add .github/ && git commit -m "ci: add workflows" && git push`))
241
+ this.log()
242
+ }
243
+
244
+ // ── list mode ──────────────────────────────────────────
245
+
246
+ private async listWorkflows(ref: string): Promise<void> {
247
+ const workflows = await enrichWorkflows(await fetchWorkflowList(ref), ref)
248
+
249
+ this.log(`\n${ux.colorize("bold", "available workflows")} ${ux.colorize("dim", `(${REPO} @ ${ref})`)}\n`)
250
+
251
+ for (const w of workflows) {
252
+ this.log(` ${ux.colorize("cyan", w.name.padEnd(28))} → .github/workflows/${w.file}`)
253
+ if (w.description) {
254
+ this.log(` ${"".padEnd(28)} ${ux.colorize("dim", w.description)}`)
255
+ }
256
+ if (w.secrets?.length) {
257
+ const names = w.secrets.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ")
258
+ this.log(` ${"".padEnd(28)} ${ux.colorize("dim", `secrets: ${names}`)}`)
259
+ }
260
+ this.log()
261
+ }
262
+ }
263
+ }
264
+
265
+
package/src/github.ts ADDED
@@ -0,0 +1,113 @@
1
+ import pkg from "../package.json"
2
+
3
+ const REPO = pkg.repository.url
4
+ .replace(/^https?:\/\/github\.com\//, "")
5
+ .replace(/\.git$/, "")
6
+
7
+ const API_BASE = `https://api.github.com/repos/${REPO}`
8
+ const RAW_BASE = `https://raw.githubusercontent.com/${REPO}`
9
+
10
+ export { REPO }
11
+
12
+ // --- types ---
13
+
14
+ export type WorkflowEntry = {
15
+ name: string
16
+ file: string
17
+ description?: string
18
+ secrets?: string[]
19
+ }
20
+
21
+ // --- parse workflow header ---
22
+ // extracts description and required secrets from the yaml header comment block.
23
+
24
+ export function parseWorkflowHeader(content: string): { description: string; secrets: string[] } {
25
+ const lines = content.split("\n")
26
+ let description = ""
27
+ const secrets: string[] = []
28
+ let inHeader = false
29
+
30
+ for (const line of lines) {
31
+ if (line.startsWith("# ---")) {
32
+ if (inHeader) break
33
+ inHeader = true
34
+ continue
35
+ }
36
+ if (!inHeader) continue
37
+ if (!line.startsWith("#")) break
38
+
39
+ const text = line.replace(/^#\s?/, "").trim()
40
+
41
+ if (/^required secrets:/i.test(text)) continue
42
+ if (/^\w+\s+[—–]/.test(text)) {
43
+ secrets.push(text)
44
+ continue
45
+ }
46
+
47
+ if (!description && text && !/^copy this|^can also|^actions used|^paste this|^this one/i.test(text)) {
48
+ description = text
49
+ }
50
+ }
51
+
52
+ return { description, secrets }
53
+ }
54
+
55
+ // --- api ---
56
+
57
+ export async function fetchTags(): Promise<string[]> {
58
+ const url = `${API_BASE}/tags?per_page=20`
59
+ const res = await fetch(url, {
60
+ headers: { Accept: "application/vnd.github.v3+json" }
61
+ })
62
+ if (!res.ok) return []
63
+ const tags: any[] = await res.json()
64
+ return tags.map((t) => t.name)
65
+ }
66
+
67
+ export async function fetchWorkflowList(gitRef: string): Promise<WorkflowEntry[]> {
68
+ const url = `${API_BASE}/contents/workflows?ref=${gitRef}`
69
+ const res = await fetch(url, {
70
+ headers: { Accept: "application/vnd.github.v3+json" }
71
+ })
72
+
73
+ if (!res.ok) {
74
+ throw new Error(`failed to list workflows from ${REPO} @ ${gitRef} (${res.status})`)
75
+ }
76
+
77
+ const entries: any[] = await res.json()
78
+
79
+ return entries
80
+ .filter((e) => e.type === "file" && e.name.endsWith(".yml"))
81
+ .map((e) => ({
82
+ name: e.name.replace(/\.yml$/, ""),
83
+ file: e.name
84
+ }))
85
+ }
86
+
87
+ export async function fetchWorkflowContent(file: string, gitRef: string): Promise<string> {
88
+ const url = `${RAW_BASE}/${gitRef}/workflows/${file}`
89
+ const res = await fetch(url)
90
+
91
+ if (!res.ok) {
92
+ throw new Error(`failed to fetch workflows/${file} from ${REPO} @ ${gitRef} (${res.status})`)
93
+ }
94
+
95
+ return await res.text()
96
+ }
97
+
98
+ export async function enrichWorkflows(workflows: WorkflowEntry[], gitRef: string): Promise<WorkflowEntry[]> {
99
+ const enriched: WorkflowEntry[] = []
100
+
101
+ for (const wf of workflows) {
102
+ try {
103
+ const content = await fetchWorkflowContent(wf.file, gitRef)
104
+ const { description, secrets } = parseWorkflowHeader(content)
105
+ enriched.push({ ...wf, description, secrets })
106
+ } catch {
107
+ enriched.push(wf)
108
+ }
109
+ }
110
+
111
+ return enriched
112
+ }
113
+
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execute } from "@oclif/core"
4
+
5
+ await execute({ dir: import.meta.url })
6
+
package/src/init.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This file has been replaced by:
4
+ // src/index.ts — oclif entry point
5
+ // src/commands/init.ts — init command
6
+ // src/github.ts — github api helpers
7
+ //
8
+ // You can safely delete this file.