@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.
- package/dist/cli.mjs +194 -9
- 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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
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.
|
|
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",
|