| 1 | #!/usr/bin/env node |
| 2 | // Publishes g1t's Claude Code and Codex plugin marketplace to its own repository, |
| 3 | // flagon-io/g1t-plugins on g1t.sh, so adding the marketplace clones a few |
| 4 | // files rather than this monorepo: |
| 5 | // |
| 6 | // claude plugin marketplace add https://g1t.sh/flagon-io/g1t-plugins.git |
| 7 | // claude plugin install g1t@g1t |
| 8 | // |
| 9 | // This repository stays the source: .claude-plugin/marketplace.json and |
| 10 | // plugins/claude-code/ are where the plugin is changed and tested, and |
| 11 | // .g1t/workflows/plugins-release.yml runs this on every push to main that |
| 12 | // touches them. Nobody commits to g1t-plugins by hand. |
| 13 | // |
| 14 | // node scripts/plugins-release.mjs check # the manifests agree; nothing is written |
| 15 | // node scripts/plugins-release.mjs build [DIR] # the published tree (default target/g1t-plugins) |
| 16 | // node scripts/plugins-release.mjs publish [--dry-run] |
| 17 | // |
| 18 | // What is published, and nothing else: |
| 19 | // |
| 20 | // .claude-plugin/marketplace.json this repository's, its plugin's source rewritten to ./plugins/g1t |
| 21 | // plugins/g1t/ plugins/claude-code/ without its tests (PLUGIN_FILES) |
| 22 | // README.md written here (readme()) |
| 23 | // .agents/plugins/marketplace.json Codex catalog, generated below |
| 24 | // plugins/g1t-codex/ plugins/codex/, with its own version |
| 25 | // LICENSE this repository's |
| 26 | // |
| 27 | // A file added to plugins/claude-code/ that PLUGIN_FILES does not name |
| 28 | // stops the build, so nothing is published or left out without a change |
| 29 | // here. Every published file is scanned for anything that looks like a |
| 30 | // credential. |
| 31 | // |
| 32 | // `publish` clones g1t-plugins, replaces its tree with the build, and |
| 33 | // commits on main only when something changed, naming this repository's |
| 34 | // commit; then tags g1t--v<version> once, the tag Claude Code's own |
| 35 | // `claude plugin tag` makes. Claude Code updates an installed |
| 36 | // plugin when its version changes, so a change to the plugin's files with |
| 37 | // the version already published stops the run: bump the version in |
| 38 | // plugins/claude-code/.claude-plugin/plugin.json and in marketplace.json. |
| 39 | // |
| 40 | // G1T_PLUGINS_REMOTE where to publish (default https://g1t.sh/flagon-io/g1t-plugins.git) |
| 41 | // PLUGINS_PUBLISH_TOKEN a g1t access token that can push there; sent as a |
| 42 | // git header through the environment, never on a |
| 43 | // command line |
| 44 | // G1T_SOURCE_COMMIT this repository's commit, for the commit message |
| 45 | // (default: git rev-parse HEAD) |
| 46 | |
| 47 | import { spawnSync } from "node:child_process"; |
| 48 | import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; |
| 49 | import { tmpdir } from "node:os"; |
| 50 | import { dirname, join, relative } from "node:path"; |
| 51 | import { fileURLToPath } from "node:url"; |
| 52 | |
| 53 | const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); |
| 54 | export const SOURCE = "plugins/claude-code"; |
| 55 | export const CODEX_SOURCE = "plugins/codex"; |
| 56 | export const CODEX_PUBLISHED = "plugins/g1t-codex"; |
| 57 | export const CODEX_FILES = ["plugin.json", "mcp.json", "skills"]; |
| 58 | export const CODEX_INSTALL = "codex plugin marketplace add https://g1t.sh/flagon-io/g1t-plugins.git"; |
| 59 | export const PUBLISHED = "plugins/g1t"; |
| 60 | export const REMOTE = "https://g1t.sh/flagon-io/g1t-plugins.git"; |
| 61 | export const INSTALL = ["claude plugin marketplace add https://g1t.sh/flagon-io/g1t-plugins.git", "claude plugin install g1t@g1t"]; |
| 62 | |
| 63 | // What of plugins/claude-code/ is the plugin: entries published whole. |
| 64 | // DEV_FILES are this repository's only (the hooks' tests and their stand-in |
| 65 | // CLI). Anything else in the folder stops the build. |
| 66 | export const PLUGIN_FILES = [".claude-plugin", ".mcp.json", "commands", "hooks", "skills"]; |
| 67 | export const DEV_FILES = ["test"]; |
| 68 | |
| 69 | // Names no published file may have, and text no published file may hold. |
| 70 | const FORBIDDEN_NAMES = [/^\.env/, /^\.credentials/, /\.(pem|key|p12|pfx)$/i, /^id_(rsa|ed25519|ecdsa)/, /^\.npmrc$/, /^node_modules$/]; |
| 71 | const SECRETS = [ |
| 72 | ["a g1t token", /\bg1t[a-z]*_[0-9a-f]{32,}/], |
| 73 | ["an Anthropic key", /\bsk-ant-[A-Za-z0-9_-]{20,}/], |
| 74 | ["an OpenAI key", /\bsk-(proj-)?[A-Za-z0-9]{32,}/], |
| 75 | ["a GitHub token", /\b(gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})/], |
| 76 | ["an AWS key", /\bAKIA[0-9A-Z]{16}\b/], |
| 77 | ["a Slack token", /\bxox[abpr]-[A-Za-z0-9-]{10,}/], |
| 78 | ["a Stripe key", /\b[rs]k_(live|test)_[A-Za-z0-9]{16,}/], |
| 79 | ["a private key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/], |
| 80 | ]; |
| 81 | |
| 82 | const readJson = (root, file) => JSON.parse(readFileSync(join(root, file), "utf8")); |
| 83 | |
| 84 | /** Every file under `dir` but git's own, as paths relative to it with forward slashes, sorted. */ |
| 85 | export function walk(dir, base = dir) { |
| 86 | const out = []; |
| 87 | for (const name of readdirSync(dir)) { |
| 88 | if (name === ".git") continue; |
| 89 | const path = join(dir, name); |
| 90 | if (statSync(path).isDirectory()) out.push(...walk(path, base)); |
| 91 | else out.push(relative(base, path).replaceAll("\\", "/")); |
| 92 | } |
| 93 | return out.sort(); |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * The marketplace and plugin manifests, checked against each other: one |
| 98 | * plugin, g1t, in a marketplace g1t (so it installs as g1t@g1t wherever it |
| 99 | * is published from), whose source is plugins/claude-code and whose version |
| 100 | * is the plugin's own. Returns them, and the version. |
| 101 | */ |
| 102 | export function manifests(root = ROOT) { |
| 103 | const marketplace = readJson(root, ".claude-plugin/marketplace.json"); |
| 104 | const plugin = readJson(root, `${SOURCE}/.claude-plugin/plugin.json`); |
| 105 | const problems = []; |
| 106 | if (marketplace.name !== "g1t") problems.push(`the marketplace is named ${JSON.stringify(marketplace.name)}, not "g1t": installs are g1t@g1t`); |
| 107 | if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length !== 1) problems.push("the marketplace lists one plugin, g1t"); |
| 108 | const entry = marketplace.plugins?.[0] ?? {}; |
| 109 | if (entry.name !== "g1t" || plugin.name !== "g1t") problems.push(`the plugin is named ${JSON.stringify(entry.name)} in marketplace.json and ${JSON.stringify(plugin.name)} in plugin.json, not "g1t"`); |
| 110 | if (entry.source !== `./${SOURCE}`) problems.push(`marketplace.json's source is ${JSON.stringify(entry.source)}, not "./${SOURCE}"`); |
| 111 | if (!/^\d+\.\d+\.\d+$/.test(plugin.version ?? "")) problems.push(`plugin.json's version ${JSON.stringify(plugin.version)} is not x.y.z`); |
| 112 | if (entry.version !== plugin.version) problems.push(`marketplace.json says version ${entry.version} and plugin.json ${plugin.version}: make them the same`); |
| 113 | if (entry.description !== plugin.description) problems.push("marketplace.json's description is not plugin.json's"); |
| 114 | if (problems.length) throw new Error(`The plugin's manifests disagree:\n- ${problems.join("\n- ")}`); |
| 115 | return { marketplace, plugin, version: plugin.version }; |
| 116 | } |
| 117 | |
| 118 | /** The marketplace manifest as published: its plugin's source is ./plugins/g1t. */ |
| 119 | export function publishedMarketplace(marketplace) { |
| 120 | return { ...marketplace, plugins: marketplace.plugins.map((p) => ({ ...p, source: `./${PUBLISHED}` })) }; |
| 121 | } |
| 122 | |
| 123 | export function readme(version) { |
| 124 | return `# g1t plugins |
| 125 | |
| 126 | g1t's plugin marketplace for Claude Code and Codex. Choose your client below. |
| 127 | |
| 128 | ## Claude Code |
| 129 | |
| 130 | The Claude Code plugin, \`g1t\` (${version}), |
| 131 | records your Claude Code sessions on g1t, gives Claude the issue it is |
| 132 | working on, claims and hands off work, and adds g1t's MCP server, skills |
| 133 | and commands. |
| 134 | |
| 135 | \`\`\`sh |
| 136 | ${INSTALL.join("\n")} |
| 137 | \`\`\` |
| 138 | |
| 139 | Then start Claude Code again, run \`/mcp\` and sign **plugin:g1t:g1t** in. |
| 140 | The plugin's hooks need the [g1t CLI](https://docs.g1t.sh/guides/cli/) |
| 141 | 0.3.2 or later and Node 18 or later. |
| 142 | |
| 143 | The guide, with what each hook records and never records and how to opt |
| 144 | out: https://docs.g1t.sh/guides/claude-code-plugin/ |
| 145 | |
| 146 | This repository is published automatically from |
| 147 | [flagon-io/g1t](https://g1t.sh/flagon-io/g1t) (\`.claude-plugin/\` and |
| 148 | \`${SOURCE}/\`). Changes and issues go there. |
| 149 | `; |
| 150 | } |
| 151 | |
| 152 | /** Codex's catalog uses paths relative to the marketplace root. */ |
| 153 | export function codexMarketplace() { |
| 154 | return { name: "g1t", interface: { displayName: "g1t" }, plugins: [{ |
| 155 | name: "g1t", source: { source: "local", path: `./${CODEX_PUBLISHED}` }, |
| 156 | policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: "Developer Tools", |
| 157 | }] }; |
| 158 | } |
| 159 | |
| 160 | function codexManifest(root) { |
| 161 | const plugin = readJson(root, `${CODEX_SOURCE}/plugin.json`); |
| 162 | if (plugin.name !== "g1t" || !/^\d+\.\d+\.\d+$/.test(plugin.version ?? "")) throw new Error("Invalid Codex plugin identity or version"); |
| 163 | const extra = readdirSync(join(root, CODEX_SOURCE)).filter(name => !CODEX_FILES.includes(name)); |
| 164 | if (extra.length) throw new Error(`Unlisted Codex plugin files: ${extra.join(", ")}`); |
| 165 | return plugin; |
| 166 | } |
| 167 | |
| 168 | /** A release's tag: `claude plugin tag`'s {name}--v{version}. */ |
| 169 | export const tagName = (version) => `g1t--v${version}`; |
| 170 | |
| 171 | /** Why the folder cannot be published as it is: entries PLUGIN_FILES and DEV_FILES do not name. */ |
| 172 | function unlisted(root) { |
| 173 | return readdirSync(join(root, SOURCE)).filter((name) => !PLUGIN_FILES.includes(name) && !DEV_FILES.includes(name)); |
| 174 | } |
| 175 | |
| 176 | /** Credentials and forbidden files in a built tree, as "path: what". */ |
| 177 | export function scan(dir) { |
| 178 | const found = []; |
| 179 | for (const file of walk(dir)) { |
| 180 | for (const part of file.split("/")) if (FORBIDDEN_NAMES.some((re) => re.test(part))) found.push(`${file}: a file that is never published`); |
| 181 | const text = readFileSync(join(dir, file), "utf8"); |
| 182 | for (const [what, re] of SECRETS) if (re.test(text)) found.push(`${file}: looks like ${what}`); |
| 183 | } |
| 184 | return found; |
| 185 | } |
| 186 | |
| 187 | /** The files the build writes, relative to its folder: the only ones it may contain. */ |
| 188 | export function expectedFiles(root = ROOT) { |
| 189 | const plugin = PLUGIN_FILES.flatMap((name) => { |
| 190 | const path = join(root, SOURCE, name); |
| 191 | return statSync(path).isDirectory() ? walk(path).map((f) => `${PUBLISHED}/${name}/${f}`) : [`${PUBLISHED}/${name}`]; |
| 192 | }); |
| 193 | const codex = walk(join(root, CODEX_SOURCE)).map(f => `${CODEX_PUBLISHED}/${f}`); |
| 194 | return [".claude-plugin/marketplace.json", ".agents/plugins/marketplace.json", "LICENSE", "README.md", ...plugin, ...codex].sort(); |
| 195 | } |
| 196 | |
| 197 | /** Writes the published tree to `out`, replacing what was there, and checks it. */ |
| 198 | export function build(out = join(ROOT, "target", "g1t-plugins"), root = ROOT) { |
| 199 | const { marketplace, version } = manifests(root); |
| 200 | codexManifest(root); |
| 201 | const extra = unlisted(root); |
| 202 | if (extra.length) throw new Error(`${SOURCE}/ has ${extra.join(", ")}, which scripts/plugins-release.mjs neither publishes nor keeps back: add each to PLUGIN_FILES or DEV_FILES.`); |
| 203 | for (const name of PLUGIN_FILES) if (!existsSync(join(root, SOURCE, name))) throw new Error(`${SOURCE}/${name} is missing`); |
| 204 | |
| 205 | rmSync(out, { recursive: true, force: true }); |
| 206 | mkdirSync(join(out, ".claude-plugin"), { recursive: true }); |
| 207 | writeFileSync(join(out, ".claude-plugin", "marketplace.json"), `${JSON.stringify(publishedMarketplace(marketplace), null, 2)}\n`); |
| 208 | for (const name of PLUGIN_FILES) cpSync(join(root, SOURCE, name), join(out, PUBLISHED, name), { recursive: true }); |
| 209 | mkdirSync(join(out, ".agents/plugins"), { recursive: true }); |
| 210 | writeFileSync(join(out, ".agents/plugins/marketplace.json"), JSON.stringify(codexMarketplace(), null, 2) + "\n"); |
| 211 | for (const name of CODEX_FILES) cpSync(join(root, CODEX_SOURCE, name), join(out, CODEX_PUBLISHED, name), { recursive: true }); |
| 212 | writeFileSync(join(out, "README.md"), readme(version) + ` |
| 213 | ## Codex |
| 214 | |
| 215 | Add this marketplace with: |
| 216 | |
| 217 | \`\`\`sh |
| 218 | ${CODEX_INSTALL} |
| 219 | codex plugin add g1t@g1t |
| 220 | \`\`\` |
| 221 | |
| 222 | In the ChatGPT desktop app's Plugins directory, choose the g1t marketplace, |
| 223 | install g1t and authorize its MCP connection. Restart the app if needed. |
| 224 | The Codex plugin (${codexManifest(root).version}) bundles the remote MCP server and |
| 225 | an issue/session workflow skill. It records sessions through MCP calls, not |
| 226 | Claude Code hooks. Disable a duplicate standalone g1t MCP connection if installed. |
| 227 | Direct MCP setup and ChatGPT instructions: https://docs.g1t.sh/guides/bring-your-own-agent/ |
| 228 | `); |
| 229 | cpSync(join(root, "LICENSE"), join(out, "LICENSE")); |
| 230 | verify(out, root); |
| 231 | return { out, version }; |
| 232 | } |
| 233 | |
| 234 | /** Checks a built tree: exactly the expected files, manifests that agree, nothing secret. */ |
| 235 | export function verify(dir, root = ROOT) { |
| 236 | const { version } = manifests(root); |
| 237 | const problems = []; |
| 238 | const files = walk(dir); |
| 239 | const expected = expectedFiles(root); |
| 240 | const more = files.filter((f) => !expected.includes(f)); |
| 241 | const fewer = expected.filter((f) => !files.includes(f)); |
| 242 | if (more.length) problems.push(`files that are not the plugin's: ${more.join(", ")}`); |
| 243 | if (fewer.length) problems.push(`missing: ${fewer.join(", ")}`); |
| 244 | const marketplace = readJson(dir, ".claude-plugin/marketplace.json"); |
| 245 | const entry = marketplace.plugins?.[0] ?? {}; |
| 246 | if (entry.source !== `./${PUBLISHED}`) problems.push(`the published source is ${JSON.stringify(entry.source)}`); |
| 247 | const plugin = readJson(dir, `${PUBLISHED}/.claude-plugin/plugin.json`); |
| 248 | if (marketplace.name !== "g1t" || entry.name !== "g1t" || plugin.name !== "g1t") problems.push("the names are not g1t@g1t"); |
| 249 | if (entry.version !== version || plugin.version !== version) problems.push(`versions: marketplace ${entry.version}, plugin ${plugin.version}, source ${version}`); |
| 250 | for (const file of [`${PUBLISHED}/.mcp.json`, `${PUBLISHED}/hooks/hooks.json`]) readJson(dir, file); |
| 251 | const codex = readJson(dir, `${CODEX_PUBLISHED}/plugin.json`); |
| 252 | if (codex.name !== "g1t" || codex.version !== codexManifest(root).version) problems.push("Codex plugin identity or version differs from source"); |
| 253 | if (JSON.stringify(readJson(dir, ".agents/plugins/marketplace.json")) !== JSON.stringify(codexMarketplace())) problems.push("Codex marketplace differs from expected catalog"); |
| 254 | const mcp = readJson(dir, `${CODEX_PUBLISHED}/mcp.json`); |
| 255 | if (mcp.mcpServers?.g1t?.url !== "https://mcp.g1t.sh" || mcp.mcpServers?.g1t?.type !== "streamable-http") problems.push("Invalid Codex MCP server"); |
| 256 | problems.push(...scan(dir)); |
| 257 | if (problems.length) throw new Error(`The published tree is wrong:\n- ${problems.join("\n- ")}`); |
| 258 | } |
| 259 | |
| 260 | function git(cwd, args, { env = {}, allowFail = false } = {}) { |
| 261 | const r = spawnSync("git", args, { cwd, encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...env } }); |
| 262 | if (r.status !== 0 && !allowFail) throw new Error(`git ${args[0]} failed (${r.status}): ${(r.stderr || r.stdout).trim()}`); |
| 263 | return r; |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * The token as a git header, through git's environment config |
| 268 | * (GIT_CONFIG_COUNT), so it is on no command line and in no file. |
| 269 | */ |
| 270 | export function authEnv(token) { |
| 271 | if (!token) return {}; |
| 272 | const basic = Buffer.from(`g1t:${token}`).toString("base64"); |
| 273 | return { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "http.extraHeader", GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}` }; |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * Publishes the build to `remote`'s main and tags v<version>. Returns |
| 278 | * { changed, commit, tagged }. An empty repository gets its first commit. |
| 279 | */ |
| 280 | export function publish({ remote = process.env.G1T_PLUGINS_REMOTE || REMOTE, token = process.env.PLUGINS_PUBLISH_TOKEN, source, dryRun = false, root = ROOT, work } = {}) { |
| 281 | source ??= process.env.G1T_SOURCE_COMMIT || git(root, ["rev-parse", "HEAD"]).stdout.trim(); |
| 282 | const env = authEnv(token); |
| 283 | work ??= mkdtempSync(join(tmpdir(), "g1t-plugins-")); |
| 284 | const built = join(work, "build"); |
| 285 | const clone = join(work, "clone"); |
| 286 | const { version } = build(built, root); |
| 287 | |
| 288 | git(work, ["clone", "--quiet", remote, clone], { env }); |
| 289 | const hasMain = git(clone, ["rev-parse", "--verify", "--quiet", "refs/remotes/origin/main"], { allowFail: true }).status === 0; |
| 290 | if (hasMain) git(clone, ["checkout", "--quiet", "-B", "main", "origin/main"]); |
| 291 | else git(clone, ["checkout", "--quiet", "--orphan", "main"]); |
| 292 | |
| 293 | const codexBefore = hasMain && existsSync(join(clone, CODEX_PUBLISHED, "plugin.json")) ? readJson(clone, `${CODEX_PUBLISHED}/plugin.json`).version : null; |
| 294 | const before = hasMain && existsSync(join(clone, PUBLISHED, ".claude-plugin", "plugin.json")) ? readJson(clone, `${PUBLISHED}/.claude-plugin/plugin.json`).version : null; |
| 295 | for (const name of readdirSync(clone)) if (name !== ".git") rmSync(join(clone, name), { recursive: true, force: true }); |
| 296 | cpSync(built, clone, { recursive: true }); |
| 297 | verify(clone, root); |
| 298 | |
| 299 | git(clone, ["add", "--all"]); |
| 300 | const changed = git(clone, ["status", "--porcelain"]).stdout.trim() !== ""; |
| 301 | const pluginChanged = hasMain ? git(clone, ["diff", "--cached", "--quiet", "--", PUBLISHED], { allowFail: true }).status !== 0 : true; |
| 302 | if (pluginChanged && before === version) { |
| 303 | throw new Error(`The plugin's files changed, but ${version} is already published, and Claude Code updates installed plugins only when the version changes. Bump "version" in ${SOURCE}/.claude-plugin/plugin.json and .claude-plugin/marketplace.json.`); |
| 304 | } |
| 305 | |
| 306 | const codexChanged = hasMain ? git(clone, ["diff", "--cached", "--quiet", "--", CODEX_PUBLISHED], { allowFail: true }).status !== 0 : true; |
| 307 | if (codexChanged && codexBefore === codexManifest(root).version) throw new Error("Codex plugin files changed without a version bump in plugins/codex/plugin.json"); |
| 308 | const tag = tagName(version); |
| 309 | const remoteTag = git(clone, ["ls-remote", "--tags", "origin", `refs/tags/${tag}`], { env }).stdout.trim(); |
| 310 | let commit = hasMain ? git(clone, ["rev-parse", "HEAD"]).stdout.trim() : null; |
| 311 | if (changed) { |
| 312 | const ident = ["-c", "user.name=g1t", "-c", "user.email=g1t@users.noreply.g1t.sh"]; |
| 313 | git(clone, [...ident, "commit", "--quiet", "-m", `g1t ${version}, from flagon-io/g1t@${source.slice(0, 12)}\n\nPublished by scripts/plugins-release.mjs from https://g1t.sh/flagon-io/g1t/commit/${source}.\nChange the plugin there; this repository is overwritten.`]); |
| 314 | commit = git(clone, ["rev-parse", "HEAD"]).stdout.trim(); |
| 315 | } |
| 316 | const tagged = !remoteTag && commit != null; |
| 317 | if (dryRun) return { changed, commit, tagged, version, dir: clone }; |
| 318 | if (changed) git(clone, ["push", "--quiet", "origin", "main:refs/heads/main"], { env }); |
| 319 | if (tagged) git(clone, ["push", "--quiet", "origin", `${commit}:refs/tags/${tag}`], { env }); |
| 320 | return { changed, commit, tagged, version, dir: clone }; |
| 321 | } |
| 322 | |
| 323 | if (process.argv[1]?.replaceAll("\\", "/").endsWith("scripts/plugins-release.mjs")) { |
| 324 | const [command, ...rest] = process.argv.slice(2); |
| 325 | try { |
| 326 | if (command === "check") { |
| 327 | const { version } = manifests(); |
| 328 | const dir = mkdtempSync(join(tmpdir(), "g1t-plugins-check-")); |
| 329 | build(dir); |
| 330 | rmSync(dir, { recursive: true, force: true }); |
| 331 | console.log(`g1t@g1t ${version}: the manifests agree and the published tree is only the plugin`); |
| 332 | } else if (command === "build") { |
| 333 | const { out, version } = build(rest[0] ? join(process.cwd(), rest[0]) : undefined); |
| 334 | console.log(`Built g1t@g1t ${version} in ${out}:\n${walk(out).map((f) => ` ${f}`).join("\n")}`); |
| 335 | } else if (command === "publish") { |
| 336 | const dryRun = rest.includes("--dry-run"); |
| 337 | const r = publish({ dryRun }); |
| 338 | const where = process.env.G1T_PLUGINS_REMOTE || REMOTE; |
| 339 | console.log(r.changed ? `${dryRun ? "Would publish" : "Published"} g1t ${r.version} to ${where} as ${r.commit}` : `${where} already has g1t ${r.version}; nothing to publish`); |
| 340 | if (r.tagged) console.log(`${dryRun ? "Would tag" : "Tagged"} ${tagName(r.version)}`); |
| 341 | if (process.env.GITHUB_STEP_SUMMARY) writeFileSync(process.env.GITHUB_STEP_SUMMARY, `- g1t@g1t ${r.version}: ${r.changed ? `published as \`${r.commit}\`` : "unchanged"}${r.tagged ? `, tagged \`${tagName(r.version)}\`` : ""}\n`, { flag: "a" }); |
| 342 | } else { |
| 343 | console.error("usage: node scripts/plugins-release.mjs check | build [DIR] | publish [--dry-run]"); |
| 344 | process.exit(2); |
| 345 | } |
| 346 | } catch (e) { |
| 347 | console.error(e.message); |
| 348 | process.exit(1); |
| 349 | } |
| 350 | } |