@theholocron/cli 2.0.0-alpha.73 → 2.0.0-alpha.75

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +194 -9
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -16,6 +16,7 @@ import { execFile, execFileSync, spawnSync } from "node:child_process";
16
16
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { promisify } from "node:util";
19
+ import { homedir } from "node:os";
19
20
  //#region src/capabilities/index.ts
20
21
  const CARDINALITY = {
21
22
  source: "single",
@@ -483,9 +484,12 @@ var PluginLoader = class {
483
484
  throw new LoaderError(`failed to import \`${tuple.packageName}\` for capability \`${key}\`: ${err instanceof Error ? err.message : String(err)}`);
484
485
  });
485
486
  if (isPluginModule(mod)) {
487
+ const effectiveToken = this.context.cliTokens?.[tuple.provider] ?? this.context.cliToken;
486
488
  const factory = mod.createPlugin({
487
489
  ...this.projectDefaults(),
488
490
  ...this.context,
491
+ ...effectiveToken !== void 0 ? { cliToken: effectiveToken } : {},
492
+ cliTokens: void 0,
489
493
  ...tuple.options
490
494
  }).capabilities[key];
491
495
  if (typeof factory !== "function") throw new LoaderError(`\`${tuple.packageName}\` does not implement the \`${key}\` capability`);
@@ -913,7 +917,7 @@ var lint_default$1 = "name: Lint\n\non: # yamllint disable-line rule:truthy\n w
913
917
  var release_default$1 = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing — no NPM_TOKEN required.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n secrets:\n SYNC_TOKEN:\n description: >\n Optional PAT with Contents write access. Required when the default\n branch is protected by a ruleset — github.token cannot push through\n rulesets, but a PAT with admin bypass can. Falls back to github.token.\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n concurrency:\n group: release-${{ github.ref }}\n cancel-in-progress: false\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n # Use SYNC_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.SYNC_TOKEN || github.token }}\n\n - run: npm install -g npm@11 sigstore\n name: Upgrade npm for OIDC support\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - run: npx semantic-release\n name: Release\n env:\n # Prefer SYNC_TOKEN (a PAT with Contents write) when available —\n # the built-in github.token cannot push to protected default branches\n # because rulesets block non-PAT pushes. Falls back to github.token\n # for repos without branch protection.\n GITHUB_TOKEN: ${{ secrets.SYNC_TOKEN || github.token }}\n NPM_CONFIG_PROVENANCE: true\n";
914
918
  //#endregion
915
919
  //#region src/templates/workflows/review.yml
916
- var review_default$1 = "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# super-linter (lint.yml) is the CI gate covering push + PR events.\n# Gitleaks and YAML are intentionally duplicated: super-linter gates\n# merges; ReviewDog surfaces exact line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\nconcurrency:\n group: review-${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: true\n\njobs:\n reviewdog:\n name: Review PRs\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n checks: write\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-check\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('yamllint.config.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-check\n yamllint_flags: -c ${{ github.workspace }}/yamllint.config.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-check\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-check\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-check\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-check\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-check\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-check\n";
920
+ var review_default$1 = "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# super-linter (lint.yml) is the CI gate covering push + PR events.\n# Gitleaks and YAML are intentionally duplicated: super-linter gates\n# merges; ReviewDog surfaces exact line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\nconcurrency:\n group: review-${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: true\n\njobs:\n reviewdog:\n name: Review PRs\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n checks: write\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / gitleaks\"\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('yamllint.config.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / yamllint\"\n yamllint_flags: -c ${{ github.workspace }}/yamllint.config.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / actionlint\"\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-check\n tool_name: \"Review / eslint\"\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / tsc\"\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-check\n tool_name: \"Review / alex\"\n";
917
921
  //#endregion
918
922
  //#region src/templates/workflows/stale.yml
919
923
  var stale_default$1 = "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue is marked stale\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days of inactivity after stale label before closing\n type: number\n required: false\n default: 5\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-pr-milestones: true\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n";
@@ -4072,15 +4076,181 @@ async function fileExists(path) {
4072
4076
  }
4073
4077
  }
4074
4078
  //#endregion
4079
+ //#region src/token-args.ts
4080
+ var TokenParseError = class extends Error {
4081
+ name = "TokenParseError";
4082
+ };
4083
+ /**
4084
+ * Converts raw --token CLI values into a typed result.
4085
+ *
4086
+ * Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
4087
+ * Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
4088
+ * Mixed: --token github=ghp_xxx --token v_yyy
4089
+ * → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
4090
+ *
4091
+ * Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
4092
+ */
4093
+ function parseTokenArgs(tokens) {
4094
+ if (tokens.length === 0) return {};
4095
+ const cliTokens = {};
4096
+ const bare = [];
4097
+ for (const raw of tokens) {
4098
+ const eqIdx = raw.indexOf("=");
4099
+ if (eqIdx === -1) {
4100
+ bare.push(raw);
4101
+ continue;
4102
+ }
4103
+ const vendor = raw.slice(0, eqIdx);
4104
+ const value = raw.slice(eqIdx + 1);
4105
+ if (vendor.trim() === "") throw new TokenParseError(`invalid --token value "${raw}": vendor name must not be empty`);
4106
+ if (/\s/.test(vendor)) throw new TokenParseError(`invalid --token value "${raw}": vendor name must not contain whitespace`);
4107
+ if (value === "") throw new TokenParseError(`invalid --token value "${raw}": token value must not be empty`);
4108
+ cliTokens[vendor] = value;
4109
+ }
4110
+ if (bare.length > 1) throw new TokenParseError(`only one bare --token value is allowed; got ${bare.length.toString()} — use vendor=value form for multiple tokens`);
4111
+ const result = {};
4112
+ if (bare.length === 1) result.cliToken = bare[0];
4113
+ if (Object.keys(cliTokens).length > 0) result.cliTokens = cliTokens;
4114
+ return result;
4115
+ }
4116
+ //#endregion
4117
+ //#region src/update-notifier.ts
4118
+ const PACKAGE_NAME = "@theholocron/cli";
4119
+ const CACHE_TTL_MS = 1440 * 60 * 1e3;
4120
+ const FETCH_TIMEOUT_MS = 3e3;
4121
+ function getCacheDir() {
4122
+ return process.env["HOLOCRON_CACHE_DIR"] ?? join(homedir(), ".cache", "holocron");
4123
+ }
4124
+ function getCachePath() {
4125
+ return join(getCacheDir(), "update-check.json");
4126
+ }
4127
+ function readCache() {
4128
+ try {
4129
+ return JSON.parse(readFileSync(getCachePath(), "utf8"));
4130
+ } catch {
4131
+ return null;
4132
+ }
4133
+ }
4134
+ function writeCache(entry) {
4135
+ try {
4136
+ mkdirSync(getCacheDir(), { recursive: true });
4137
+ writeFileSync(getCachePath(), JSON.stringify(entry));
4138
+ } catch {}
4139
+ }
4140
+ async function fetchLatestVersion(channel) {
4141
+ try {
4142
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
4143
+ if (!res.ok) return null;
4144
+ const data = await res.json();
4145
+ return data["dist-tags"][channel] ?? data["dist-tags"]["latest"] ?? null;
4146
+ } catch {
4147
+ return null;
4148
+ }
4149
+ }
4150
+ function getChannel(version) {
4151
+ return /^[^-]+-([a-zA-Z]+)/.exec(version)?.[1] ?? "latest";
4152
+ }
4153
+ function isUpdateAvailable(current, latest) {
4154
+ const normalize = (v) => v.replace(/^v/, "");
4155
+ const c = normalize(current);
4156
+ const l = normalize(latest);
4157
+ if (c === l) return false;
4158
+ const splitPre = (v) => {
4159
+ const idx = v.indexOf("-");
4160
+ return idx === -1 ? [v, ""] : [v.slice(0, idx), v.slice(idx + 1)];
4161
+ };
4162
+ const [cRelease, cPre] = splitPre(c);
4163
+ const [lRelease, lPre] = splitPre(l);
4164
+ const parseRelease = (r) => r.split(".").map(Number);
4165
+ const cParts = parseRelease(cRelease);
4166
+ const lParts = parseRelease(lRelease);
4167
+ for (let i = 0; i < Math.max(cParts.length, lParts.length); i++) {
4168
+ const cv = cParts[i] ?? 0;
4169
+ const lv = lParts[i] ?? 0;
4170
+ if (lv > cv) return true;
4171
+ if (lv < cv) return false;
4172
+ }
4173
+ if (!lPre && cPre) return true;
4174
+ if (lPre && !cPre) return false;
4175
+ const cPreParts = cPre.split(".");
4176
+ const lPreParts = lPre.split(".");
4177
+ for (let i = 0; i < Math.max(cPreParts.length, lPreParts.length); i++) {
4178
+ const cv = cPreParts[i] ?? "";
4179
+ const lv = lPreParts[i] ?? "";
4180
+ const cvNum = Number(cv);
4181
+ const lvNum = Number(lv);
4182
+ if (!isNaN(cvNum) && !isNaN(lvNum)) {
4183
+ if (lvNum > cvNum) return true;
4184
+ if (lvNum < cvNum) return false;
4185
+ } else {
4186
+ if (lv > cv) return true;
4187
+ if (lv < cv) return false;
4188
+ }
4189
+ }
4190
+ return false;
4191
+ }
4192
+ function formatNotice(current, latest) {
4193
+ const installCmd = `npm install -g ${PACKAGE_NAME}`;
4194
+ const raw1 = `Update available: ${current} → ${latest}`;
4195
+ const raw2 = `Run ${installCmd} to update`;
4196
+ const width = Math.max(raw1.length, raw2.length) + 4;
4197
+ const bar = chalk.yellow("─".repeat(width));
4198
+ const border = chalk.yellow("│");
4199
+ const pad = (raw, styled) => `${border} ${styled}${" ".repeat(width - 2 - raw.length)} ${border}`;
4200
+ return [
4201
+ "",
4202
+ chalk.yellow(`╭${bar}╮`),
4203
+ pad(raw1, `Update available: ${chalk.dim(current)} → ${chalk.green(latest)}`),
4204
+ pad(raw2, `Run ${chalk.cyan(installCmd)} to update`),
4205
+ chalk.yellow(`╰${bar}╯`),
4206
+ ""
4207
+ ].join("\n");
4208
+ }
4209
+ async function checkForUpdates(currentVersion) {
4210
+ if (process.env["CI"] || process.env["NO_UPDATE_NOTIFIER"]) return null;
4211
+ const channel = getChannel(currentVersion);
4212
+ const cache = readCache();
4213
+ const now = Date.now();
4214
+ let latestVersion = null;
4215
+ if (cache && now - cache.checkedAt < CACHE_TTL_MS) latestVersion = cache.latestVersion;
4216
+ else {
4217
+ latestVersion = await fetchLatestVersion(channel);
4218
+ if (latestVersion) writeCache({
4219
+ latestVersion,
4220
+ checkedAt: now
4221
+ });
4222
+ }
4223
+ if (!latestVersion || !isUpdateAvailable(currentVersion, latestVersion)) return null;
4224
+ return () => {
4225
+ process.stderr.write(formatNotice(currentVersion, latestVersion) + "\n");
4226
+ };
4227
+ }
4228
+ //#endregion
4075
4229
  //#region src/cli.ts
4076
4230
  const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
4231
+ /** Parses --token values and returns the context spread, or null on parse error (exits with code 1). */
4232
+ function tokenContext(rawTokens) {
4233
+ if (!rawTokens?.length) return {};
4234
+ try {
4235
+ return parseTokenArgs(rawTokens);
4236
+ } catch (err) {
4237
+ if (err instanceof TokenParseError) {
4238
+ console.error(`--token: ${err.message}`);
4239
+ process.exitCode = 1;
4240
+ return null;
4241
+ }
4242
+ throw err;
4243
+ }
4244
+ }
4245
+ const updateCheckPromise = checkForUpdates(CLI_VERSION);
4077
4246
  await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [options]").option("dry-run", {
4078
4247
  type: "boolean",
4079
4248
  default: false,
4080
4249
  describe: "Print what would be mutated without calling capability mutators. Commands branch on this; read-only commands ignore it."
4081
4250
  }).option("token", {
4082
4251
  type: "string",
4083
- describe: "Vendor token passed to plugins as `cliToken`, taking precedence over env vars. Unambiguous for single-plugin commands; for multi-plugin flows pass per-vendor env vars instead."
4252
+ array: true,
4253
+ describe: "Vendor token override for plugins. Bare form: --token <value> (fallback for all plugins, single-plugin commands). Keyed form: --token vendor=value (targets a specific provider; repeat for each). Example: --token github=ghp_xxx --token vercel=v_yyy"
4084
4254
  }).option("cwd", {
4085
4255
  type: "string",
4086
4256
  default: process.cwd(),
@@ -4091,26 +4261,30 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4091
4261
  type: "string",
4092
4262
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
4093
4263
  }), async (argv) => {
4264
+ const tokens = tokenContext(argv.token);
4265
+ if (!tokens) return;
4094
4266
  if ((await runDoctor({
4095
4267
  loaded: await loadConfig(argv.cwd),
4096
4268
  context: {
4097
4269
  repoRoot: argv.cwd,
4098
4270
  dryRun: argv.dryRun,
4099
4271
  ...argv.repo ? { repo: argv.repo } : {},
4100
- ...argv.token ? { cliToken: argv.token } : {}
4272
+ ...tokens
4101
4273
  }
4102
4274
  })).summary.fail > 0) process.exitCode = 1;
4103
4275
  }).command("setup", "Apply infra setup actions across every configured capability", (y) => y.option("repo", {
4104
4276
  type: "string",
4105
4277
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
4106
4278
  }), async (argv) => {
4279
+ const tokens = tokenContext(argv.token);
4280
+ if (!tokens) return;
4107
4281
  if ((await runSetup({
4108
4282
  loaded: await loadConfig(argv.cwd),
4109
4283
  context: {
4110
4284
  repoRoot: argv.cwd,
4111
4285
  dryRun: argv.dryRun,
4112
4286
  ...argv.repo ? { repo: argv.repo } : {},
4113
- ...argv.token ? { cliToken: argv.token } : {}
4287
+ ...tokens
4114
4288
  }
4115
4289
  })).summary.fail > 0) process.exitCode = 1;
4116
4290
  }).command("skills", "Manage agent skills from the @theholocron/skills registry", (y) => y.command("install", "Copy skills from @theholocron/skills into .agents/ with agent symlinks", () => {}, async (argv) => {
@@ -4163,6 +4337,8 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4163
4337
  default: "repo",
4164
4338
  describe: "Scope: \"repo\" (default), \"env=<name>\", or \"org=<name>\""
4165
4339
  }), async (argv) => {
4340
+ const tokens = tokenContext(argv.token);
4341
+ if (!tokens) return;
4166
4342
  const scopeArg = argv.scope;
4167
4343
  const scope = parseScope(scopeArg);
4168
4344
  if ((await runSecretSet({
@@ -4170,7 +4346,7 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4170
4346
  context: {
4171
4347
  repoRoot: argv.cwd,
4172
4348
  dryRun: argv.dryRun,
4173
- ...argv.token ? { cliToken: argv.token } : {}
4349
+ ...tokens
4174
4350
  },
4175
4351
  name: argv.name,
4176
4352
  ...argv.value ? { value: argv.value } : {},
@@ -4190,12 +4366,14 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4190
4366
  default: ["production", "preview"],
4191
4367
  describe: "Deployment targets to sync to. Defaults to production + preview."
4192
4368
  }), async (argv) => {
4369
+ const tokens = tokenContext(argv.token);
4370
+ if (!tokens) return;
4193
4371
  if ((await runSecretsSync({
4194
4372
  loaded: await loadConfig(argv.cwd),
4195
4373
  context: {
4196
4374
  repoRoot: argv.cwd,
4197
4375
  dryRun: argv.dryRun,
4198
- ...argv.token ? { cliToken: argv.token } : {}
4376
+ ...tokens
4199
4377
  },
4200
4378
  environmentId: argv.environmentId,
4201
4379
  ...argv.projectId ? { projectId: argv.projectId } : {},
@@ -4214,12 +4392,14 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4214
4392
  choices: ["production", "staging"],
4215
4393
  describe: "Named environment to deploy into. Omit for a branch preview."
4216
4394
  }), async (argv) => {
4395
+ const tokens = tokenContext(argv.token);
4396
+ if (!tokens) return;
4217
4397
  if ((await runDeploy({
4218
4398
  loaded: await loadConfig(argv.cwd),
4219
4399
  context: {
4220
4400
  repoRoot: argv.cwd,
4221
4401
  dryRun: argv.dryRun,
4222
- ...argv.token ? { cliToken: argv.token } : {}
4402
+ ...tokens
4223
4403
  },
4224
4404
  projectId: argv.projectId,
4225
4405
  branch: argv.branch,
@@ -4257,13 +4437,15 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4257
4437
  type: "string",
4258
4438
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
4259
4439
  }), async (argv) => {
4440
+ const tokens = tokenContext(argv.token);
4441
+ if (!tokens) return;
4260
4442
  if ((await runSync({
4261
4443
  loaded: await loadConfig(argv.cwd),
4262
4444
  context: {
4263
4445
  repoRoot: argv.cwd,
4264
4446
  dryRun: argv.dryRun,
4265
4447
  ...argv.repo ? { repo: argv.repo } : {},
4266
- ...argv.token ? { cliToken: argv.token } : {}
4448
+ ...tokens
4267
4449
  },
4268
4450
  ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
4269
4451
  })).summary.fail > 0) process.exitCode = 1;
@@ -4286,7 +4468,9 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4286
4468
  describe: "Write generated files to this local directory instead of pushing (for validation)"
4287
4469
  }), async (argv) => {
4288
4470
  const outputDir = argv["output-dir"];
4289
- const token = outputDir ? "no-token-needed" : argv.token ?? process.env.GITHUB_TOKEN ?? process.env.HOLOCRON_GITHUB_TOKEN;
4471
+ const parsed = tokenContext(argv.token);
4472
+ if (!parsed) return;
4473
+ const token = outputDir ? "no-token-needed" : parsed.cliTokens?.["github"] ?? parsed.cliToken ?? process.env.GITHUB_TOKEN ?? process.env.HOLOCRON_GITHUB_TOKEN;
4290
4474
  if (!token) {
4291
4475
  console.error("sync-github: GitHub token required — pass --token or set GITHUB_TOKEN");
4292
4476
  process.exitCode = 1;
@@ -4421,6 +4605,7 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4421
4605
  }).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
4422
4606
  await runAuthList();
4423
4607
  }).demandCommand(1, "Run `holocron auth --help` to see available auth subcommands."), () => {}).demandCommand(1, "Run `holocron --help` to see available commands.").strict().help().parse();
4608
+ (await updateCheckPromise)?.();
4424
4609
  /**
4425
4610
  * Parse `--scope` strings: `repo` | `env=NAME` | `org=NAME`.
4426
4611
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.73",
3
+ "version": "2.0.0-alpha.75",
4
4
  "description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",