diff --git a/.env.example b/.env.example index e69de29..bf38c50 100644 --- a/.env.example +++ b/.env.example @@ -0,0 +1,2 @@ +SKULK_BASE_URL= +SKULK_TOKEN= \ No newline at end of file diff --git a/.github/workflows/carmel-judgment.yml b/.github/workflows/carmel-judgment.yml index 6f459b6..874e796 100644 --- a/.github/workflows/carmel-judgment.yml +++ b/.github/workflows/carmel-judgment.yml @@ -33,26 +33,29 @@ jobs: - name: πŸ§ͺ Run tests id: tests run: npm run test:run + continue-on-error: true - name: 😼 Carmel Judgment Stamp if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} uses: actions/github-script@v7 + env: + TESTS_OUTCOME: ${{ steps.tests.conclusion }} with: script: | - const passed = process.env.TESTS_OUTCOME == "success"; - + const passed = process.env.TESTS_OUTCOME === "success"; + const body = passed ? "😼✨ **Carmel Approval Stampβ„’**\n\n> *Adequate work, human.*" : "😼πŸ”₯ **Carmel Chaos Stampβ„’**\n\n> *I sense weakness in these tests.*"; - + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body }); - - if (!passed) core.setFailed("Carmel has rejected this PR."); + if (!passed) core.setFailed("Carmel has rejected this PR."); + - name: 😼 Carmel Observes the Results run: echo "Verdict delivered. " diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..8b9bb4d --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,38 @@ +name: Publish to npm + +on: + push: + tags: + - "v*.*.*" + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + registry-url: "https://registry.npmjs.org" + + - name: Install + run: npm ci + + - name: Test + run: npm run test:run + + - name: Build + run: npm run build + + - name: Publish + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 0255654..13c6551 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ yarn-error.log* .vscode/* /dist/ /.idea/ + +*.tgz diff --git a/README.md b/README.md index 26e767e..b0ebda8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,30 @@ Skulk is a command-line tool for syncing and managing Lab Notes β€” built to work just as well for humans at the keyboard as it does for automation, CI, and agent-driven workflows. +--- +## What Skulk Connects To + +Skulk is the CLI for **The Human Pattern Lab API**. + +By default it targets a Human Pattern Lab API instance. You can override the API endpoint with `--base-url` to use staging or a self-hosted deployment of the same API. + +> Note: `--base-url` is intended for alternate deployments of the Human Pattern Lab API, not arbitrary third-party APIs. + +--- +## Configuration + +### Environment variables + +- `SKULK_TOKEN` β€” API token used to authenticate requests. +- `SKULK_BASE_URL` β€” Base URL for a Human Pattern Lab API instance (overridden by `--base-url`). + +Example: + +```bash +export SKULK_TOKEN="..." +export SKULK_BASE_URL="https://thehumanpatternlab.com/api" +skulk notes sync --dir ./src/labnotes/en +``` --- ## Why Skulk Exists diff --git a/package.json b/package.json index 3bf344e..ca243eb 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,25 @@ { - "name": "@humanpatternlab/skulk", + "name": "@thehumanpatternlab/skulk", "version": "0.1.0", - "private": true, - "description": "Skulk CLI for The Human Pattern Lab", + "private": false, + "description": "CLI for syncing Lab Notes with The Human Pattern Lab API", + "keywords": ["cli", "automation", "human-pattern-lab"], + "author": "Ada Vale", + "license": "MIT", "type": "module", "bin": { "skulk": "dist/index.js" }, + "files": [ + "dist/index.js", + "dist/lab.js", + "dist/cli/**", + "dist/commands/**", + "dist/lib/**", + "dist/sdk/**", + "dist/sync/**", + "dist/utils/**" + ], "scripts": { "dev": "tsx src/index.ts", "build": "tsc", @@ -41,8 +54,5 @@ }, "lint-staged": { "*.{js,ts,md,json}": "prettier --write" - }, - "keywords": [], - "author": "", - "license": "ISC" + } } diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts new file mode 100644 index 0000000..31e24f1 --- /dev/null +++ b/src/__tests__/config.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import { SKULK_BASE_URL, SKULK_TOKEN } from '../lib/config.js'; + +describe('env config', () => { + beforeEach(() => { + delete process.env.SKULK_BASE_URL; + delete process.env.SKULK_TOKEN; + delete process.env.HPL_API_BASE_URL; + delete process.env.HPL_TOKEN; + }); + + it('uses SKULK_TOKEN when set', () => { + process.env.SKULK_TOKEN = 'abc123'; + expect(SKULK_TOKEN()).toBe('abc123'); + }); + + it('uses SKULK_BASE_URL when set', () => { + process.env.SKULK_BASE_URL = 'https://example.com/api'; + expect(SKULK_BASE_URL()).toBe('https://example.com/api'); + }); + + it('override beats SKULK_BASE_URL', () => { + process.env.SKULK_BASE_URL = 'https://example.com/api'; + expect(SKULK_BASE_URL('https://override.com/api')).toBe( + 'https://override.com/api', + ); + }); +}); diff --git a/src/commands/notesSync.js b/src/commands/notesSync.js index 0393f53..37f25ad 100644 --- a/src/commands/notesSync.js +++ b/src/commands/notesSync.js @@ -1,59 +1,79 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); +'use strict'; +Object.defineProperty(exports, '__esModule', { value: true }); exports.notesSyncCommand = notesSyncCommand; -const commander_1 = require("commander"); -const config_js_1 = require("../lib/config.js"); -const http_js_1 = require("../lib/http.js"); -const notes_js_1 = require("../lib/notes.js"); +const commander_1 = require('commander'); +const config_js_1 = require('../lib/config.js'); +const http_js_1 = require('../lib/http.js'); +const notes_js_1 = require('../lib/notes.js'); async function upsertNote(baseUrl, token, note) { - // Adjust this to match your API. - // Recommended: POST /api/lab-notes/upsert OR POST /api/lab-notes (upsert by slug) - return (0, http_js_1.httpJson)({ baseUrl, token }, "POST", "/lab-notes", note); + // Adjust this to match your API. + // Recommended: POST /api/lab-notes/upsert OR POST /api/lab-notes (upsert by slug) + return (0, http_js_1.httpJson)( + { baseUrl, token }, + 'POST', + '/lab-notes', + note, + ); } function notesSyncCommand() { - const notes = new commander_1.Command("notes").description("Lab Notes commands"); - notes - .command("sync") - .description("Sync local markdown notes to the API") - .option("--dir ", "Directory containing markdown notes", "./labnotes/en") - .option("--locale ", "Locale code", "en") - .option("--base-url ", "Override API base URL (ex: https://thehumanpatternlab.com/api)") - .option("--dry-run", "Print what would be sent, but do not call the API", false) - .action(async (opts) => { - const baseUrl = (0, config_js_1.resolveApiBaseUrl)(opts.baseUrl); - const token = (0, config_js_1.resolveToken)(); - const files = (0, notes_js_1.listMarkdownFiles)(opts.dir); - if (files.length === 0) { - console.log(`No .md/.mdx files found in: ${opts.dir}`); - process.exitCode = 1; - return; + const notes = new commander_1.Command('notes').description( + 'Lab Notes commands', + ); + notes + .command('sync') + .description('Sync local markdown notes to the API') + .option( + '--dir ', + 'Directory containing markdown notes', + './labnotes/en', + ) + .option('--locale ', 'Locale code', 'en') + .option( + '--base-url ', + 'Override API base URL (ex: https://thehumanpatternlab.com/api)', + ) + .option( + '--dry-run', + 'Print what would be sent, but do not call the API', + false, + ) + .action(async (opts) => { + const baseUrl = (0, config_js_1.SKULK_BASE_URL)(opts.baseUrl); + const token = (0, config_js_1.SKULK_TOKEN)(); + const files = (0, notes_js_1.listMarkdownFiles)(opts.dir); + if (files.length === 0) { + console.log(`No .md/.mdx files found in: ${opts.dir}`); + process.exitCode = 1; + return; + } + console.log(`Skulk syncing ${files.length} note(s) from ${opts.dir}`); + console.log(`API: ${baseUrl}`); + console.log(`Locale: ${opts.locale}`); + console.log( + opts.dryRun ? 'Mode: DRY RUN (no writes)' : 'Mode: LIVE (writing)', + ); + let ok = 0; + let fail = 0; + for (const file of files) { + try { + const note = (0, notes_js_1.readNote)(file, opts.locale); + if (opts.dryRun) { + console.log( + `\n---\n${note.slug}\n${file}\nfrontmatter keys: ${Object.keys(note.attributes).join(', ')}`, + ); + continue; + } + const res = await upsertNote(baseUrl, token, note); + ok++; + console.log(`βœ… ${note.slug} (${res.action ?? 'ok'})`); + } catch (e) { + fail++; + console.error(`❌ ${file}`); + console.error(String(e)); } - console.log(`Skulk syncing ${files.length} note(s) from ${opts.dir}`); - console.log(`API: ${baseUrl}`); - console.log(`Locale: ${opts.locale}`); - console.log(opts.dryRun ? "Mode: DRY RUN (no writes)" : "Mode: LIVE (writing)"); - let ok = 0; - let fail = 0; - for (const file of files) { - try { - const note = (0, notes_js_1.readNote)(file, opts.locale); - if (opts.dryRun) { - console.log(`\n---\n${note.slug}\n${file}\nfrontmatter keys: ${Object.keys(note.attributes).join(", ")}`); - continue; - } - const res = await upsertNote(baseUrl, token, note); - ok++; - console.log(`βœ… ${note.slug} (${res.action ?? "ok"})`); - } - catch (e) { - fail++; - console.error(`❌ ${file}`); - console.error(String(e)); - } - } - console.log(`\nDone. Success: ${ok}, Failed: ${fail}`); - if (fail > 0) - process.exitCode = 1; + } + console.log(`\nDone. Success: ${ok}, Failed: ${fail}`); + if (fail > 0) process.exitCode = 1; }); - return notes; + return notes; } diff --git a/src/commands/notesSync.ts b/src/commands/notesSync.ts index 80b3455..14b4826 100644 --- a/src/commands/notesSync.ts +++ b/src/commands/notesSync.ts @@ -31,219 +31,247 @@ * @description Syncs markdown Lab Notes to the API via `skulk notes sync`. * Supports human + JSON output modes; JSON mode is stdout-pure. */ -import { Command } from "commander"; -import { resolveApiBaseUrl, resolveToken } from "../lib/config.js"; -import { httpJson } from "../lib/http.js"; -import { listMarkdownFiles, readNote, type NotePayload } from "../lib/notes.js"; -import { getOutputMode, printJson } from "../cli/output.js"; -import { buildSyncReport } from "../cli/outputContract.js"; -import fs from "node:fs"; - +import { Command } from 'commander'; +import { SKULK_BASE_URL, SKULK_TOKEN } from '../lib/config.js'; +import { httpJson } from '../lib/http.js'; +import { listMarkdownFiles, readNote, type NotePayload } from '../lib/notes.js'; +import { getOutputMode, printJson } from '../cli/output.js'; +import { buildSyncReport } from '../cli/outputContract.js'; +import fs from 'node:fs'; type UpsertResponse = { - ok: boolean; - slug: string; - action?: "created" | "updated"; + ok: boolean; + slug: string; + action?: 'created' | 'updated'; }; type LabNoteUpsertPayload = { - slug: string; - title: string; - markdown: string; - locale?: string; - // optional extras if your API supports them: - // subtitle?: string; - // tags?: string[]; - // published?: string; - // status?: string; - // dept?: string; + slug: string; + title: string; + markdown: string; + locale?: string; + // optional extras if your API supports them: + // subtitle?: string; + // tags?: string[]; + // published?: string; + // status?: string; + // dept?: string; }; -async function upsertNote(baseUrl: string, token: string | undefined, note: any, locale?: string) { - const payload: LabNoteUpsertPayload = { - slug: note.slug, - title: note.attributes.title, - markdown: note.markdown, - locale - }; - if (!payload.slug || !payload.title || !payload.markdown) { - throw new Error( - `Invalid note payload: slug/title/markdown missing for ${payload.slug ?? "unknown"}` - ); - } - return httpJson( - { baseUrl, token }, - "POST", - "/lab-notes/upsert", - payload +async function upsertNote( + baseUrl: string, + token: string | undefined, + note: any, + locale?: string, +) { + const payload: LabNoteUpsertPayload = { + slug: note.slug, + title: note.attributes.title, + markdown: note.markdown, + locale, + }; + if (!payload.slug || !payload.title || !payload.markdown) { + throw new Error( + `Invalid note payload: slug/title/markdown missing for ${payload.slug ?? 'unknown'}`, ); + } + return httpJson( + { baseUrl, token }, + 'POST', + '/lab-notes/upsert', + payload, + ); } export function notesSyncCommand() { - const notes = new Command("notes").description("Lab Notes commands"); - - notes - .command("sync") - .description("Sync local markdown notes to the API") - .option("--dir ", "Directory containing markdown notes", "./src/labnotes/en") - //.option("--dir ", "Directory containing markdown notes", "./labnotes/en") - .option("--locale ", "Locale code", "en") - .option("--base-url ", "Override API base URL (ex: https://thehumanpatternlab.com/api)") - .option("--dry-run", "Print what would be sent, but do not call the API", false) - .option("--only ", "Sync only a single note by slug") - .option("--limit ", "Sync only the first N notes", (v) => parseInt(v, 10)) - - .action(async (opts, cmd) => { - const mode = getOutputMode(cmd); // "json" | "human" - - const jsonError = (message: string, extra?: unknown) => { - if (mode === "json") { - process.stderr.write(JSON.stringify({ ok: false, error: { message, extra } }, null, 2) + "\n"); - } else { - console.error(message); - if (extra) console.error(extra); - } - process.exitCode = 1; - }; - - if (!fs.existsSync(opts.dir)) { - jsonError(`Notes directory not found: ${opts.dir}`, { - hint: `Try: skulk notes sync --dir "..\\\\the-human-pattern-lab\\\\src\\\\labnotes\\\\en"` - }); - return; - } + const notes = new Command('notes').description('Lab Notes commands'); + + notes + .command('sync') + .description('Sync local markdown notes to the API') + .option( + '--dir ', + 'Directory containing markdown notes', + './src/labnotes/en', + ) + //.option("--dir ", "Directory containing markdown notes", "./labnotes/en") + .option('--locale ', 'Locale code', 'en') + .option( + '--base-url ', + 'Override API base URL (ex: https://thehumanpatternlab.com/api)', + ) + .option( + '--dry-run', + 'Print what would be sent, but do not call the API', + false, + ) + .option('--only ', 'Sync only a single note by slug') + .option('--limit ', 'Sync only the first N notes', (v) => + parseInt(v, 10), + ) + + .action(async (opts, cmd) => { + const mode = getOutputMode(cmd); // "json" | "human" + + const jsonError = (message: string, extra?: unknown) => { + if (mode === 'json') { + process.stderr.write( + JSON.stringify({ ok: false, error: { message, extra } }, null, 2) + + '\n', + ); + } else { + console.error(message); + if (extra) console.error(extra); + } + process.exitCode = 1; + }; + + if (!fs.existsSync(opts.dir)) { + jsonError(`Notes directory not found: ${opts.dir}`, { + hint: `Try: skulk notes sync --dir "..\\\\the-human-pattern-lab\\\\src\\\\labnotes\\\\en"`, + }); + return; + } - const baseUrl = resolveApiBaseUrl(opts.baseUrl); - const token = resolveToken(); + const baseUrl = SKULK_BASE_URL(opts.baseUrl); + const token = SKULK_TOKEN(); - const files = listMarkdownFiles(opts.dir); - let selectedFiles = files; + const files = listMarkdownFiles(opts.dir); + let selectedFiles = files; - if (opts.only) { - selectedFiles = files.filter((f) => - f.toLowerCase().includes(opts.only.toLowerCase()) - ); + if (opts.only) { + selectedFiles = files.filter((f) => + f.toLowerCase().includes(opts.only.toLowerCase()), + ); + } + + if (opts.limit && Number.isFinite(opts.limit)) { + selectedFiles = selectedFiles.slice(0, opts.limit); + } + + if (selectedFiles.length === 0) { + if (mode === 'json') { + printJson({ + ok: true, + action: 'noop', + message: 'No matching notes found.', + matched: 0, + }); + } else { + console.log('No matching notes found.'); + } + process.exitCode = 0; + return; + } + + // Human-mode header chatter + if (mode === 'human') { + console.log( + `Skulk syncing ${selectedFiles.length} note(s) from ${opts.dir}`, + ); + console.log(`API: ${baseUrl}`); + console.log(`Locale: ${opts.locale}`); + console.log( + opts.dryRun ? 'Mode: DRY RUN (no writes)' : 'Mode: LIVE (writing)', + ); + } + + let ok = 0; + let fail = 0; + + const results: Array<{ + file: string; + slug?: string; + status: 'ok' | 'fail' | 'dry-run'; + action?: 'created' | 'updated'; + error?: string; + }> = []; + + for (const file of selectedFiles) { + try { + const note = readNote(file, opts.locale); + + if (opts.dryRun) { + ok++; + results.push({ + file, + slug: note.slug, + status: 'dry-run', + }); + + if (mode === 'human') { + console.log( + `\n---\n${note.slug}\n${file}\nfrontmatter keys: ${Object.keys(note.attributes).join(', ')}`, + ); } - if (opts.limit && Number.isFinite(opts.limit)) { - selectedFiles = selectedFiles.slice(0, opts.limit); - } + continue; + } + + const res = await upsertNote(baseUrl, token, note, opts.locale); + ok++; + + results.push({ + file, + slug: note.slug, + status: 'ok', + action: res.action, + }); + + if (mode === 'human') { + console.log(`βœ… ${note.slug} (${res.action ?? 'ok'})`); + } + } catch (e) { + fail++; + const msg = String(e); + + results.push({ + file, + status: 'fail', + error: msg, + }); + + if (mode === 'human') { + console.error(`❌ ${file}`); + console.error(msg); + } + } + } + + if (mode === 'json') { + const report = buildSyncReport({ + results, + dryRun: Boolean(opts.dryRun), + locale: opts.locale, + baseUrl, + }); - if (selectedFiles.length === 0) { - if (mode === "json") { - printJson({ ok: true, action: "noop", message: "No matching notes found.", matched: 0 }); - } else { - console.log("No matching notes found."); - } - process.exitCode = 0; - return; - } + printJson(report); - // Human-mode header chatter - if (mode === "human") { - console.log(`Skulk syncing ${selectedFiles.length} note(s) from ${opts.dir}`); - console.log(`API: ${baseUrl}`); - console.log(`Locale: ${opts.locale}`); - console.log(opts.dryRun ? "Mode: DRY RUN (no writes)" : "Mode: LIVE (writing)"); - } + if (!report.ok) process.exitCode = 1; + } else { + const report = buildSyncReport({ + results, + dryRun: Boolean(opts.dryRun), + locale: opts.locale, + baseUrl, + }); - let ok = 0; - let fail = 0; - - const results: Array<{ - file: string; - slug?: string; - status: "ok" | "fail" | "dry-run"; - action?: "created" | "updated"; - error?: string; - }> = []; - - for (const file of selectedFiles) { - try { - const note = readNote(file, opts.locale); - - if (opts.dryRun) { - ok++; - results.push({ - file, - slug: note.slug, - status: "dry-run" - }); - - if (mode === "human") { - console.log( - `\n---\n${note.slug}\n${file}\nfrontmatter keys: ${Object.keys(note.attributes).join(", ")}` - ); - } - - continue; - } - - const res = await upsertNote(baseUrl, token, note, opts.locale); - ok++; - - results.push({ - file, - slug: note.slug, - status: "ok", - action: res.action - }); - - if (mode === "human") { - console.log(`βœ… ${note.slug} (${res.action ?? "ok"})`); - } - } catch (e) { - fail++; - const msg = String(e); - - results.push({ - file, - status: "fail", - error: msg - }); - - if (mode === "human") { - console.error(`❌ ${file}`); - console.error(msg); - } - } - } + const { synced, dryRun, failed } = report.summary; - if (mode === "json") { - const report = buildSyncReport({ - results, - dryRun: Boolean(opts.dryRun), - locale: opts.locale, - baseUrl, - }); - - printJson(report); - - if (!report.ok) process.exitCode = 1; - } else { - const report = buildSyncReport({ - results, - dryRun: Boolean(opts.dryRun), - locale: opts.locale, - baseUrl, - }); - - const { synced, dryRun, failed } = report.summary; - - if (report.dryRun) { - console.log( - `\nDone. ${dryRun} note(s) would be synced (dry-run). Failures: ${failed}` - ); - } else { - console.log( - `\nDone. ${synced} note(s) synced successfully. Failures: ${failed}` - ); - } - - if (!report.ok) process.exitCode = 1; - } - }); + if (report.dryRun) { + console.log( + `\nDone. ${dryRun} note(s) would be synced (dry-run). Failures: ${failed}`, + ); + } else { + console.log( + `\nDone. ${synced} note(s) synced successfully. Failures: ${failed}`, + ); + } + if (!report.ok) process.exitCode = 1; + } + }); - return notes; + return notes; } diff --git a/src/lib/config.ts b/src/lib/config.ts index 9ea8c3d..8b7f51e 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,58 +1,65 @@ -import fs from "node:fs"; -import path from "node:path"; -import os from "node:os"; -import { z } from "zod"; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { z } from 'zod'; /** * Skulk CLI configuration schema * Stored in ~/.humanpatternlab/skulk.json */ const ConfigSchema = z.object({ - apiBaseUrl: z - .string() - .url() - .default("https://thehumanpatternlab.com/api"), + apiBaseUrl: z.string().url().default('https://thehumanpatternlab.com/api'), - token: z.string().optional() + token: z.string().optional(), }); export type SkulkConfig = z.infer; function getConfigPath() { - return path.join(os.homedir(), ".humanpatternlab", "skulk.json"); + return path.join(os.homedir(), '.humanpatternlab', 'skulk.json'); } export function loadConfig(): SkulkConfig { - const p = getConfigPath(); + const p = getConfigPath(); - if (!fs.existsSync(p)) { - return ConfigSchema.parse({}); - } + if (!fs.existsSync(p)) { + return ConfigSchema.parse({}); + } - const raw = fs.readFileSync(p, "utf-8"); - return ConfigSchema.parse(JSON.parse(raw)); + const raw = fs.readFileSync(p, 'utf-8'); + return ConfigSchema.parse(JSON.parse(raw)); } export function saveConfig(partial: Partial) { - const p = getConfigPath(); - fs.mkdirSync(path.dirname(p), { recursive: true }); + const p = getConfigPath(); + fs.mkdirSync(path.dirname(p), { recursive: true }); - const current = loadConfig(); - const next = ConfigSchema.parse({ ...current, ...partial }); + const current = loadConfig(); + const next = ConfigSchema.parse({ ...current, ...partial }); - fs.writeFileSync(p, JSON.stringify(next, null, 2), "utf-8"); + fs.writeFileSync(p, JSON.stringify(next, null, 2), 'utf-8'); } -export function resolveApiBaseUrl(override?: string) { - if (override) return override; +export function SKULK_BASE_URL(override?: string) { + if (override?.trim()) return override.trim(); - if (process.env.HPL_API_BASE_URL) { - return process.env.HPL_API_BASE_URL; - } + // NEW official env var + const env = process.env.SKULK_BASE_URL?.trim(); + if (env) return env; - return loadConfig().apiBaseUrl; + // optional legacy support (remove later if you want) + const legacy = process.env.HPL_API_BASE_URL?.trim(); + if (legacy) return legacy; + + return loadConfig().apiBaseUrl; } -export function resolveToken() { - return process.env.HPL_TOKEN ?? loadConfig().token; +export function SKULK_TOKEN() { + const env = process.env.SKULK_TOKEN?.trim(); + if (env) return env; + + const legacy = process.env.HPL_TOKEN?.trim(); + if (legacy) return legacy; + + return loadConfig().token; }