@theholocron/cli 2.0.0-alpha.20 → 2.0.0-alpha.22
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/README.md +29 -24
- package/dist/capabilities/index.d.mts +2 -12
- package/dist/capabilities/index.mjs +1 -16
- package/dist/cli.mjs +275 -191
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +82 -71
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- editorconfig-checker-disable-file -->
|
|
2
|
+
|
|
1
3
|
# `@theholocron/cli`
|
|
2
4
|
|
|
3
5
|
The Holocron CLI — a pluggable, capability-based orchestrator for
|
|
@@ -16,29 +18,31 @@ Holocron reads `holocron.config.{json,js,ts}` from the project root
|
|
|
16
18
|
(priority: json → js → ts).
|
|
17
19
|
|
|
18
20
|
**JSON** (simplest):
|
|
21
|
+
|
|
19
22
|
```jsonc
|
|
20
23
|
// holocron.config.json
|
|
21
24
|
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
"project": { "name": "my-app" },
|
|
26
|
+
"providers": {
|
|
27
|
+
"vault": ["1password", { "vault": "my-app" }],
|
|
28
|
+
"source": "github",
|
|
29
|
+
},
|
|
27
30
|
}
|
|
28
31
|
```
|
|
29
32
|
|
|
30
33
|
**JS/TS** — use `defineConfig` for autocomplete and type-checking:
|
|
34
|
+
|
|
31
35
|
```ts
|
|
32
36
|
// holocron.config.ts
|
|
33
|
-
import { defineConfig } from
|
|
37
|
+
import { defineConfig } from "@theholocron/cli";
|
|
34
38
|
|
|
35
39
|
export default defineConfig({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
})
|
|
40
|
+
project: { name: "my-app" },
|
|
41
|
+
providers: {
|
|
42
|
+
vault: ["1password", { vault: "my-app" }],
|
|
43
|
+
source: "github",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
### Shareable configs
|
|
@@ -50,18 +54,19 @@ top (project wins):
|
|
|
50
54
|
|
|
51
55
|
```ts
|
|
52
56
|
providers: {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
vault: '@acme/holocron-vault', // preset only
|
|
58
|
+
source: ['@acme/holocron-github', { repo: 'x' }], // preset + override
|
|
55
59
|
}
|
|
56
60
|
```
|
|
57
61
|
|
|
58
62
|
A capability config package exports a `CapabilityConfigPackage` default:
|
|
63
|
+
|
|
59
64
|
```ts
|
|
60
|
-
import type { CapabilityConfigPackage } from
|
|
65
|
+
import type { CapabilityConfigPackage } from "@theholocron/cli";
|
|
61
66
|
export default {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
} satisfies CapabilityConfigPackage
|
|
67
|
+
provider: "1password",
|
|
68
|
+
options: { vault: "acme-app" },
|
|
69
|
+
} satisfies CapabilityConfigPackage;
|
|
65
70
|
```
|
|
66
71
|
|
|
67
72
|
**Level 2 — whole-config presets.** Because the config file can be
|
|
@@ -69,23 +74,23 @@ JS/TS, a shared base is just an import:
|
|
|
69
74
|
|
|
70
75
|
```ts
|
|
71
76
|
// holocron.config.ts
|
|
72
|
-
import { acmeConfig } from
|
|
73
|
-
export default acmeConfig
|
|
77
|
+
import { acmeConfig } from "@acme/holocron-config";
|
|
78
|
+
export default acmeConfig;
|
|
74
79
|
```
|
|
75
80
|
|
|
76
81
|
## What's in here
|
|
77
82
|
|
|
78
83
|
- `src/capabilities/` — the 14 capability interfaces that providers
|
|
79
|
-
|
|
84
|
+
implement
|
|
80
85
|
- `src/config.ts` — config schema, `defineConfig`, `resolveConfig`,
|
|
81
|
-
|
|
86
|
+
`CapabilityConfigPackage`
|
|
82
87
|
- `src/load-config.ts` — `loadConfig` — reads JSON/JS/TS config files
|
|
83
88
|
- `src/define-config.ts` — `defineConfig` typed pass-through
|
|
84
89
|
- `src/loader.ts` — `PluginLoader` — dynamic-imports plugins, resolves
|
|
85
|
-
|
|
90
|
+
capability config packages, builds the capability registry
|
|
86
91
|
- `src/cli.ts` — yargs entry, dispatches subcommands
|
|
87
92
|
- `src/commands/` — `setup`, `doctor`, `deploy`, `secret set`,
|
|
88
|
-
|
|
93
|
+
`secrets sync`, `npm publish-initial`
|
|
89
94
|
|
|
90
95
|
## Status
|
|
91
96
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ProviderApiError } from "@theholocron/http-client";
|
|
2
|
+
|
|
1
3
|
//#region src/capabilities/index.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Capability interfaces — the contracts that providers implement.
|
|
@@ -39,18 +41,6 @@ interface ProviderIdentity {
|
|
|
39
41
|
readonly key: CapabilityKey;
|
|
40
42
|
readonly providerName: string;
|
|
41
43
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Surfaced from every capability call that hits a vendor API. Wraps
|
|
44
|
-
* the underlying error with `status` (HTTP) and `details` so
|
|
45
|
-
* orchestrators (`holocron setup`, `doctor`) can soft-skip rather
|
|
46
|
-
* than abort.
|
|
47
|
-
*/
|
|
48
|
-
declare class ProviderApiError extends Error {
|
|
49
|
-
readonly status: number | undefined;
|
|
50
|
-
readonly details?: unknown | undefined;
|
|
51
|
-
name: string;
|
|
52
|
-
constructor(message: string, status: number | undefined, details?: unknown | undefined);
|
|
53
|
-
}
|
|
54
44
|
interface Ruleset {
|
|
55
45
|
id: number;
|
|
56
46
|
name: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProviderApiError } from "@theholocron/http-client";
|
|
1
2
|
//#region src/capabilities/index.ts
|
|
2
3
|
const CARDINALITY = {
|
|
3
4
|
source: "single",
|
|
@@ -21,22 +22,6 @@ const CARDINALITY = {
|
|
|
21
22
|
* own requirements at call time.
|
|
22
23
|
*/
|
|
23
24
|
const REQUIRED_CAPABILITIES = [];
|
|
24
|
-
/**
|
|
25
|
-
* Surfaced from every capability call that hits a vendor API. Wraps
|
|
26
|
-
* the underlying error with `status` (HTTP) and `details` so
|
|
27
|
-
* orchestrators (`holocron setup`, `doctor`) can soft-skip rather
|
|
28
|
-
* than abort.
|
|
29
|
-
*/
|
|
30
|
-
var ProviderApiError = class extends Error {
|
|
31
|
-
status;
|
|
32
|
-
details;
|
|
33
|
-
name = "ProviderApiError";
|
|
34
|
-
constructor(message, status, details) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.status = status;
|
|
37
|
-
this.details = details;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
25
|
var WebhookVerificationError = class extends Error {
|
|
41
26
|
name = "WebhookVerificationError";
|
|
42
27
|
};
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path, { basename, dirname, join } from "node:path";
|
|
3
4
|
import yargs from "yargs";
|
|
4
5
|
import { hideBin } from "yargs/helpers";
|
|
6
|
+
import { ProviderApiError } from "@theholocron/http-client";
|
|
5
7
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
6
|
-
import path, { dirname, join } from "node:path";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
9
|
import { spawnSync } from "node:child_process";
|
|
9
10
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -31,22 +32,6 @@ const CARDINALITY = {
|
|
|
31
32
|
* own requirements at call time.
|
|
32
33
|
*/
|
|
33
34
|
const REQUIRED_CAPABILITIES = [];
|
|
34
|
-
/**
|
|
35
|
-
* Surfaced from every capability call that hits a vendor API. Wraps
|
|
36
|
-
* the underlying error with `status` (HTTP) and `details` so
|
|
37
|
-
* orchestrators (`holocron setup`, `doctor`) can soft-skip rather
|
|
38
|
-
* than abort.
|
|
39
|
-
*/
|
|
40
|
-
var ProviderApiError = class extends Error {
|
|
41
|
-
status;
|
|
42
|
-
details;
|
|
43
|
-
name = "ProviderApiError";
|
|
44
|
-
constructor(message, status, details) {
|
|
45
|
-
super(message);
|
|
46
|
-
this.status = status;
|
|
47
|
-
this.details = details;
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
35
|
//#endregion
|
|
51
36
|
//#region src/config.ts
|
|
52
37
|
/**
|
|
@@ -687,6 +672,161 @@ async function runNpmBumpVersions(input) {
|
|
|
687
672
|
};
|
|
688
673
|
}
|
|
689
674
|
//#endregion
|
|
675
|
+
//#region src/commands/upgrade-node.ts
|
|
676
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
677
|
+
"node_modules",
|
|
678
|
+
".git",
|
|
679
|
+
"dist",
|
|
680
|
+
"coverage",
|
|
681
|
+
"build",
|
|
682
|
+
".turbo",
|
|
683
|
+
".next",
|
|
684
|
+
"out"
|
|
685
|
+
]);
|
|
686
|
+
function patchPackageJson(content, from, to) {
|
|
687
|
+
let pkg;
|
|
688
|
+
try {
|
|
689
|
+
pkg = JSON.parse(content);
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
let changed = false;
|
|
694
|
+
const engines = pkg.engines;
|
|
695
|
+
if (engines?.node === `>=${from}.0.0`) {
|
|
696
|
+
engines.node = `>=${to}.0.0`;
|
|
697
|
+
changed = true;
|
|
698
|
+
}
|
|
699
|
+
for (const field of ["devDependencies", "dependencies"]) {
|
|
700
|
+
const deps = pkg[field];
|
|
701
|
+
if (!deps?.["@types/node"]) continue;
|
|
702
|
+
if (deps["@types/node"] === `^${from}.0.0`) {
|
|
703
|
+
deps["@types/node"] = `^${to}.0.0`;
|
|
704
|
+
changed = true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return changed ? JSON.stringify(pkg, null, 2) + "\n" : null;
|
|
708
|
+
}
|
|
709
|
+
function patchYaml(content, from, to) {
|
|
710
|
+
const updated = content.replace(/node-version:\s+['"]?(\d+)['"]?/g, (match, ver) => ver === String(from) ? match.replace(String(from), String(to)) : match);
|
|
711
|
+
return updated !== content ? updated : null;
|
|
712
|
+
}
|
|
713
|
+
function patchPinFile(content, from, to) {
|
|
714
|
+
const trimmed = content.trim();
|
|
715
|
+
if (trimmed === String(from) || trimmed.startsWith(`${from}.`)) return `${to}\n`;
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
function patchDockerfile(content, from, to) {
|
|
719
|
+
const updated = content.replace(/^(FROM\s+node:)(\d+)/gm, (match, prefix, ver) => ver === String(from) ? `${prefix}${to}` : match);
|
|
720
|
+
return updated !== content ? updated : null;
|
|
721
|
+
}
|
|
722
|
+
function patchToolVersions(content, from, to) {
|
|
723
|
+
const updated = content.replace(/^(nodejs\s+)(\d+)/gm, (match, prefix, ver) => ver === String(from) ? `${prefix}${to}` : match);
|
|
724
|
+
return updated !== content ? updated : null;
|
|
725
|
+
}
|
|
726
|
+
const PATTERNS = [
|
|
727
|
+
{
|
|
728
|
+
matches: (n) => n === "package.json",
|
|
729
|
+
patch: patchPackageJson
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
matches: (n) => n.endsWith(".yml") || n.endsWith(".yaml"),
|
|
733
|
+
patch: patchYaml
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
matches: (n) => n === ".nvmrc" || n === ".node-version",
|
|
737
|
+
patch: patchPinFile
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
matches: (n) => n === "Dockerfile" || n.startsWith("Dockerfile."),
|
|
741
|
+
patch: patchDockerfile
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
matches: (n) => n === ".tool-versions",
|
|
745
|
+
patch: patchToolVersions
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
function detectFrom(cwd, _readFile) {
|
|
749
|
+
for (const name of [".nvmrc", ".node-version"]) try {
|
|
750
|
+
const major = parseInt(_readFile(join(cwd, name)).trim(), 10);
|
|
751
|
+
if (!isNaN(major)) return major;
|
|
752
|
+
} catch {}
|
|
753
|
+
try {
|
|
754
|
+
const node = JSON.parse(_readFile(join(cwd, "package.json"))).engines?.node;
|
|
755
|
+
if (node) {
|
|
756
|
+
const m = node.match(/(\d+)/);
|
|
757
|
+
if (m) return parseInt(m[1], 10);
|
|
758
|
+
}
|
|
759
|
+
} catch {}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
function defaultWalkFiles(dir) {
|
|
763
|
+
const results = [];
|
|
764
|
+
function walk(current) {
|
|
765
|
+
let entries;
|
|
766
|
+
try {
|
|
767
|
+
entries = readdirSync(current);
|
|
768
|
+
} catch {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
for (const entry of entries) {
|
|
772
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
773
|
+
const abs = join(current, entry);
|
|
774
|
+
try {
|
|
775
|
+
if (statSync(abs).isDirectory()) walk(abs);
|
|
776
|
+
else results.push(abs);
|
|
777
|
+
} catch {}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
walk(dir);
|
|
781
|
+
return results;
|
|
782
|
+
}
|
|
783
|
+
async function runUpgradeNode(input) {
|
|
784
|
+
const print = input.print ?? ((line) => console.log(line));
|
|
785
|
+
const cwd = input.cwd ?? process.cwd();
|
|
786
|
+
const { to, dryRun = false, extra = [] } = input;
|
|
787
|
+
const _readFile = input.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
788
|
+
const _writeFile = input.writeFile ?? ((p, c) => writeFileSync(p, c));
|
|
789
|
+
const _walkFiles = input.walkFiles ?? defaultWalkFiles;
|
|
790
|
+
const from = input.from ?? detectFrom(cwd, _readFile);
|
|
791
|
+
if (from === null) return {
|
|
792
|
+
status: "fail",
|
|
793
|
+
updated: [],
|
|
794
|
+
message: "could not detect current Node version — pass --from <major>"
|
|
795
|
+
};
|
|
796
|
+
if (from === to) {
|
|
797
|
+
print(`Already at Node.js ${to} — nothing to do.`);
|
|
798
|
+
return {
|
|
799
|
+
status: "ok",
|
|
800
|
+
updated: []
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
print(`Upgrading Node.js ${from} → ${to}${dryRun ? " (dry-run)" : ""}…`);
|
|
804
|
+
const updated = [];
|
|
805
|
+
const scanned = [..._walkFiles(cwd), ...extra.map((p) => join(cwd, p))];
|
|
806
|
+
for (const abs of scanned) {
|
|
807
|
+
const name = basename(abs);
|
|
808
|
+
const pattern = PATTERNS.find((p) => p.matches(name));
|
|
809
|
+
if (!pattern) continue;
|
|
810
|
+
let content;
|
|
811
|
+
try {
|
|
812
|
+
content = _readFile(abs);
|
|
813
|
+
} catch {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const patched = pattern.patch(content, from, to);
|
|
817
|
+
if (patched === null) continue;
|
|
818
|
+
const rel = abs.startsWith(cwd + "/") ? abs.slice(cwd.length + 1) : abs;
|
|
819
|
+
if (!dryRun) _writeFile(abs, patched);
|
|
820
|
+
print(` ${dryRun ? "~" : "✓"} ${rel}`);
|
|
821
|
+
updated.push(rel);
|
|
822
|
+
}
|
|
823
|
+
if (updated.length === 0) print(` · no files contained Node.js ${from} pins`);
|
|
824
|
+
return {
|
|
825
|
+
status: dryRun ? "dry-run" : "ok",
|
|
826
|
+
updated
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
690
830
|
//#region src/templates/index.ts
|
|
691
831
|
/**
|
|
692
832
|
* All reusable workflow and composite action content bundled as string
|
|
@@ -1027,6 +1167,7 @@ jobs:
|
|
|
1027
1167
|
name: Run Super Linter
|
|
1028
1168
|
env:
|
|
1029
1169
|
GITHUB_TOKEN: \${{ github.token }}
|
|
1170
|
+
DEFAULT_BRANCH: \${{ github.event.pull_request.base.ref || github.event.repository.default_branch }}
|
|
1030
1171
|
ANNOTATE_ONLY: true
|
|
1031
1172
|
DISABLE_COMMENTS: false
|
|
1032
1173
|
IGNORE_GITIGNORED_FILES: true
|
|
@@ -2253,48 +2394,19 @@ function deriveDefaults(input) {
|
|
|
2253
2394
|
//#endregion
|
|
2254
2395
|
//#region src/commands/plugin-create/templates/auth.ts
|
|
2255
2396
|
function render$17(inputs) {
|
|
2256
|
-
return
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
export class AuthError extends Error {
|
|
2271
|
-
override name = "AuthError";
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
export interface ResolveTokenInput {
|
|
2275
|
-
/** From \`--token\` CLI flag. */
|
|
2276
|
-
cliToken?: string;
|
|
2277
|
-
/** Env vars; passed in for testability. Defaults to \`process.env\`. */
|
|
2278
|
-
env?: NodeJS.ProcessEnv;
|
|
2279
|
-
/** Keyring lookup fn; passed in for testability. Defaults to \`getToken(provider)\`. */
|
|
2280
|
-
keyring?: (provider: string) => string | null;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
export function resolveToken(input: ResolveTokenInput = {}): string {
|
|
2284
|
-
const env = input.env ?? process.env;
|
|
2285
|
-
const keyring = input.keyring ?? getKeyringToken;
|
|
2286
|
-
// Bracket access so numeric-prefixed slugs (e.g., env.HOLOCRON_1PASSWORD_TOKEN
|
|
2287
|
-
// which is invalid JS) still produce syntactically valid code.
|
|
2288
|
-
const token =
|
|
2289
|
-
input.cliToken || env["${inputs.tokenEnv}"] || env["${inputs.vendorEnv}"] || keyring("${inputs.slug}");
|
|
2290
|
-
if (!token) {
|
|
2291
|
-
throw new AuthError(
|
|
2292
|
-
"no ${inputs.vendorName} token found. Pass --token <TOKEN>, set ${inputs.tokenEnv} / ${inputs.vendorEnv}, " +
|
|
2293
|
-
"or run: holocron auth set ${inputs.slug} <TOKEN>"
|
|
2294
|
-
);
|
|
2295
|
-
}
|
|
2296
|
-
return token;
|
|
2297
|
-
}
|
|
2397
|
+
return `import { AuthError, createResolveToken, type ResolveTokenInput } from "@theholocron/cli";
|
|
2398
|
+
|
|
2399
|
+
export { AuthError };
|
|
2400
|
+
export type { ResolveTokenInput };
|
|
2401
|
+
|
|
2402
|
+
export const resolveToken = createResolveToken({
|
|
2403
|
+
\tenvName: "${inputs.tokenEnv}",
|
|
2404
|
+
\tvendorEnvName: "${inputs.vendorEnv}",
|
|
2405
|
+
\tkeyringService: "${inputs.slug}",
|
|
2406
|
+
\terrorMessage:
|
|
2407
|
+
\t\t"no ${inputs.vendorName} token found. Pass --token <TOKEN>, set ${inputs.tokenEnv} / ${inputs.vendorEnv}, " +
|
|
2408
|
+
\t\t"or run: holocron auth set ${inputs.slug} <TOKEN>",
|
|
2409
|
+
});
|
|
2298
2410
|
`;
|
|
2299
2411
|
}
|
|
2300
2412
|
//#endregion
|
|
@@ -2720,148 +2832,79 @@ Not yet published; capability methods are stubs.
|
|
|
2720
2832
|
//#endregion
|
|
2721
2833
|
//#region src/commands/plugin-create/templates/rest.ts
|
|
2722
2834
|
function render$7(inputs) {
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
export interface RequestOptions {
|
|
2741
|
-
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
2742
|
-
body?: unknown;
|
|
2743
|
-
query?: Record<string, string>;
|
|
2744
|
-
/** Treat this response as void even if 200 is returned. */
|
|
2745
|
-
expectNoContent?: boolean;
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
export class ${clientClass} {
|
|
2749
|
-
private readonly token: string;
|
|
2750
|
-
private readonly fetchImpl: typeof fetch;
|
|
2751
|
-
readonly baseUrl: string;
|
|
2752
|
-
|
|
2753
|
-
constructor(opts: RestClientOptions) {
|
|
2754
|
-
this.token = opts.token;
|
|
2755
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
2756
|
-
// Manual trailing-slash trim — CodeQL flags regex on library
|
|
2757
|
-
// input as polynomial ReDoS. O(n) loop, no backtracking risk.
|
|
2758
|
-
let url = opts.baseUrl ?? "${inputs.baseUrl}";
|
|
2759
|
-
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
2760
|
-
this.baseUrl = url;
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
async request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
|
2764
|
-
const url = new URL(\`\${this.baseUrl}\${path.startsWith("/") ? path : "/" + path}\`);
|
|
2765
|
-
for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
|
|
2766
|
-
const fullUrl = url.toString();
|
|
2767
|
-
|
|
2768
|
-
const headers: Record<string, string> = {
|
|
2769
|
-
authorization: \`Bearer \${this.token}\`,
|
|
2770
|
-
accept: "application/json",
|
|
2771
|
-
};
|
|
2772
|
-
const init: RequestInit = {
|
|
2773
|
-
method: opts.method ?? "GET",
|
|
2774
|
-
headers,
|
|
2775
|
-
};
|
|
2776
|
-
if (opts.body !== undefined) {
|
|
2777
|
-
headers["content-type"] = "application/json";
|
|
2778
|
-
init.body = JSON.stringify(opts.body);
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
let res: Response;
|
|
2782
|
-
try {
|
|
2783
|
-
res = await this.fetchImpl(fullUrl, init);
|
|
2784
|
-
} catch (err) {
|
|
2785
|
-
const detail = err instanceof Error ? \`\${err.name}: \${err.message}\` : String(err);
|
|
2786
|
-
throw new ProviderApiError(\`${inputs.vendorName} \${init.method} \${path} failed: \${detail}\`, 0, undefined);
|
|
2787
|
-
}
|
|
2788
|
-
if (!res.ok) {
|
|
2789
|
-
const body = await res.text().catch(() => "");
|
|
2790
|
-
throw new ProviderApiError(\`${inputs.vendorName} \${init.method} \${path} → \${res.status}\`, res.status, body);
|
|
2791
|
-
}
|
|
2792
|
-
if (opts.expectNoContent || res.status === 204) return undefined as T;
|
|
2793
|
-
const text = await res.text();
|
|
2794
|
-
if (!text) return undefined as T;
|
|
2795
|
-
return JSON.parse(text) as T;
|
|
2796
|
-
}
|
|
2835
|
+
return `import { createRestClient, type RequestOptions, type RestClient } from "@theholocron/cli";
|
|
2836
|
+
|
|
2837
|
+
export type { RequestOptions, RestClient };
|
|
2838
|
+
|
|
2839
|
+
export function ${`create${inputs.vendorName}RestClient`}(opts: {
|
|
2840
|
+
\ttoken: string;
|
|
2841
|
+
\tbaseUrl?: string;
|
|
2842
|
+
\tfetch?: typeof fetch;
|
|
2843
|
+
}): RestClient {
|
|
2844
|
+
\treturn createRestClient({
|
|
2845
|
+
\t\tbaseUrl: opts.baseUrl ?? "${inputs.baseUrl}",
|
|
2846
|
+
\t\ttoken: opts.token,
|
|
2847
|
+
\t\tvendor: "${inputs.vendorName}",
|
|
2848
|
+
\t\tfetch: opts.fetch,
|
|
2849
|
+
\t});
|
|
2797
2850
|
}
|
|
2798
2851
|
`;
|
|
2799
2852
|
}
|
|
2800
2853
|
//#endregion
|
|
2801
2854
|
//#region src/commands/plugin-create/templates/rest-test.ts
|
|
2802
2855
|
function render$6(inputs) {
|
|
2803
|
-
const
|
|
2856
|
+
const factoryName = `create${inputs.vendorName}RestClient`;
|
|
2804
2857
|
return `import { ProviderApiError } from "@theholocron/cli";
|
|
2805
2858
|
import { describe, expect, it } from "vitest";
|
|
2806
2859
|
|
|
2807
|
-
import { ${
|
|
2860
|
+
import { ${factoryName} } from "../rest.js";
|
|
2808
2861
|
import { stubFetch } from "./helpers.js";
|
|
2809
2862
|
|
|
2810
|
-
describe("${
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
} catch (err) {
|
|
2856
|
-
expect(err).toBeInstanceOf(ProviderApiError);
|
|
2857
|
-
expect((err as ProviderApiError).status).toBe(0);
|
|
2858
|
-
}
|
|
2859
|
-
});
|
|
2860
|
-
|
|
2861
|
-
it("trims trailing slashes from the base URL", () => {
|
|
2862
|
-
const client = new ${clientClass}({ token: "t", baseUrl: "${inputs.baseUrl}//" });
|
|
2863
|
-
expect(client.baseUrl).toBe("${inputs.baseUrl}");
|
|
2864
|
-
});
|
|
2863
|
+
describe("${factoryName}", () => {
|
|
2864
|
+
\tit("sends bearer + accept headers and returns the parsed body", async () => {
|
|
2865
|
+
\t\tconst stub = stubFetch([{ status: 200, body: { ok: true } }]);
|
|
2866
|
+
\t\tconst client = ${factoryName}({ token: "t", fetch: stub.fetch });
|
|
2867
|
+
\t\tconst res = await client.request<{ ok: boolean }>("/me");
|
|
2868
|
+
\t\texpect(res.ok).toBe(true);
|
|
2869
|
+
\t\texpect(stub.calls[0]?.headers["authorization"]).toBe("Bearer t");
|
|
2870
|
+
\t\texpect(stub.calls[0]?.headers["accept"]).toBe("application/json");
|
|
2871
|
+
\t});
|
|
2872
|
+
|
|
2873
|
+
\tit("serializes body as JSON and sets content-type when present", async () => {
|
|
2874
|
+
\t\tconst stub = stubFetch([{ status: 200, body: {} }]);
|
|
2875
|
+
\t\tconst client = ${factoryName}({ token: "t", fetch: stub.fetch });
|
|
2876
|
+
\t\tawait client.request<unknown>("/resource", { method: "POST", body: { name: "demo" } });
|
|
2877
|
+
\t\texpect(stub.calls[0]?.method).toBe("POST");
|
|
2878
|
+
\t\texpect(stub.calls[0]?.headers["content-type"]).toBe("application/json");
|
|
2879
|
+
\t\texpect(stub.calls[0]?.body).toEqual({ name: "demo" });
|
|
2880
|
+
\t});
|
|
2881
|
+
|
|
2882
|
+
\tit("returns undefined on 204", async () => {
|
|
2883
|
+
\t\tconst stub = stubFetch([{ status: 204 }]);
|
|
2884
|
+
\t\tconst client = ${factoryName}({ token: "t", fetch: stub.fetch });
|
|
2885
|
+
\t\texpect(await client.request<unknown>("/whatever")).toBeUndefined();
|
|
2886
|
+
\t});
|
|
2887
|
+
|
|
2888
|
+
\tit("throws ProviderApiError with the HTTP status on non-2xx", async () => {
|
|
2889
|
+
\t\tconst stub = stubFetch([{ status: 401, body: { messages: ["invalid"] } }]);
|
|
2890
|
+
\t\tconst client = ${factoryName}({ token: "bad", fetch: stub.fetch });
|
|
2891
|
+
\t\tconst err = await client.request<unknown>("/me").catch((e: unknown) => e);
|
|
2892
|
+
\t\texpect(err).toBeInstanceOf(ProviderApiError);
|
|
2893
|
+
\t\texpect((err as ProviderApiError).status).toBe(401);
|
|
2894
|
+
\t});
|
|
2895
|
+
|
|
2896
|
+
\tit("wraps transport-level failures with status 0", async () => {
|
|
2897
|
+
\t\tconst throwing: typeof fetch = async () => { throw new TypeError("fetch failed"); };
|
|
2898
|
+
\t\tconst client = ${factoryName}({ token: "t", fetch: throwing });
|
|
2899
|
+
\t\tconst err = await client.request<unknown>("/me").catch((e: unknown) => e);
|
|
2900
|
+
\t\texpect(err).toBeInstanceOf(ProviderApiError);
|
|
2901
|
+
\t\texpect((err as ProviderApiError).status).toBe(0);
|
|
2902
|
+
\t});
|
|
2903
|
+
|
|
2904
|
+
\tit("trims trailing slashes from the base URL", () => {
|
|
2905
|
+
\t\tconst client = ${factoryName}({ token: "t", baseUrl: "${inputs.baseUrl}//" });
|
|
2906
|
+
\t\texpect(client.baseUrl).toBe("${inputs.baseUrl}");
|
|
2907
|
+
\t});
|
|
2865
2908
|
});
|
|
2866
2909
|
`;
|
|
2867
2910
|
}
|
|
@@ -3523,6 +3566,15 @@ function vaultProviderName(loader) {
|
|
|
3523
3566
|
}
|
|
3524
3567
|
//#endregion
|
|
3525
3568
|
//#region src/commands/setup.ts
|
|
3569
|
+
const ALEX_CONFIG = JSON.stringify({ allow: [
|
|
3570
|
+
"dead",
|
|
3571
|
+
"failure",
|
|
3572
|
+
"failures",
|
|
3573
|
+
"hook",
|
|
3574
|
+
"hooks",
|
|
3575
|
+
"husky",
|
|
3576
|
+
"period"
|
|
3577
|
+
] }, null, 2) + "\n";
|
|
3526
3578
|
const DEPENDABOT_CONFIG = `\
|
|
3527
3579
|
# AUTO-GENERATED by holocron — run \`holocron setup\` to regenerate.
|
|
3528
3580
|
version: 2
|
|
@@ -3746,6 +3798,13 @@ async function runSetup(input) {
|
|
|
3746
3798
|
}));
|
|
3747
3799
|
print(formatStep(steps[steps.length - 1]));
|
|
3748
3800
|
}
|
|
3801
|
+
if (loader.has("source")) {
|
|
3802
|
+
const source = loader.get("source");
|
|
3803
|
+
steps.push(await runStep("source", "write .alexrc.json", dryRun, async () => {
|
|
3804
|
+
await source.writeRepoFile(".alexrc.json", ALEX_CONFIG);
|
|
3805
|
+
}));
|
|
3806
|
+
print(formatStep(steps[steps.length - 1]));
|
|
3807
|
+
}
|
|
3749
3808
|
if (loader.has("environments")) {
|
|
3750
3809
|
const envs = loader.get("environments");
|
|
3751
3810
|
print(" → environments");
|
|
@@ -4183,7 +4242,32 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
|
|
|
4183
4242
|
}
|
|
4184
4243
|
throw err;
|
|
4185
4244
|
}
|
|
4186
|
-
}).command("
|
|
4245
|
+
}).command("upgrade", "Upgrade toolchain version pins across the repo", (y) => y.command("node <to>", "Scan the repo and update every Node.js version pin to a new major", (yy) => yy.positional("to", {
|
|
4246
|
+
type: "number",
|
|
4247
|
+
demandOption: true,
|
|
4248
|
+
describe: "Target Node.js major version (e.g., 22)"
|
|
4249
|
+
}).option("from", {
|
|
4250
|
+
type: "number",
|
|
4251
|
+
describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
|
|
4252
|
+
}), async (argv) => {
|
|
4253
|
+
let extra = [];
|
|
4254
|
+
try {
|
|
4255
|
+
const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
|
|
4256
|
+
const upgradeNode = JSON.parse(raw).upgrade?.node;
|
|
4257
|
+
if (Array.isArray(upgradeNode?.extra)) extra = upgradeNode.extra;
|
|
4258
|
+
} catch {}
|
|
4259
|
+
const report = await runUpgradeNode({
|
|
4260
|
+
to: argv.to,
|
|
4261
|
+
...argv.from != null ? { from: argv.from } : {},
|
|
4262
|
+
cwd: argv.cwd,
|
|
4263
|
+
dryRun: argv.dryRun,
|
|
4264
|
+
extra
|
|
4265
|
+
});
|
|
4266
|
+
if (report.status === "fail") {
|
|
4267
|
+
if (report.message) console.error(`upgrade node: ${report.message}`);
|
|
4268
|
+
process.exitCode = 1;
|
|
4269
|
+
}
|
|
4270
|
+
}).demandCommand(1, "Run `holocron upgrade --help` to see available upgrade subcommands."), () => {}).command("auth <subcommand>", "Manage bootstrap credentials in the OS keyring", (y) => y.command("set <provider> [token]", "Verify + store a bootstrap token for a provider", (yy) => yy.positional("provider", {
|
|
4187
4271
|
type: "string",
|
|
4188
4272
|
demandOption: true
|
|
4189
4273
|
}).positional("token", { type: "string" }), async (argv) => {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Analytics, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, EnsureResult, Environment, EnvironmentReviewer, Environments, Issue, IssueSearchFilter, Issues, LifecycleResult, LifecycleSlot, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, isMulti } from "./capabilities/index.mjs";
|
|
2
|
+
import { AuthError, RequestOptions, ResolveTokenConfig as ResolveTokenConfig$1, ResolveTokenInput, RestClient, RestClientConfig, createRestClient } from "@theholocron/http-client";
|
|
2
3
|
|
|
4
|
+
//#region src/auth-resolver.d.ts
|
|
5
|
+
type ResolveTokenConfig = Omit<ResolveTokenConfig$1, "getKeyringToken">;
|
|
6
|
+
/** Wraps `createResolveToken` from `@theholocron/http` and injects the
|
|
7
|
+
* system keyring so plugins stay at a one-liner call site. */
|
|
8
|
+
declare function createResolveToken(config: ResolveTokenConfig): (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
|
|
9
|
+
//#endregion
|
|
3
10
|
//#region src/config.d.ts
|
|
4
11
|
type ProviderOptions = Record<string, unknown>;
|
|
5
12
|
/**
|
|
@@ -183,4 +190,4 @@ interface LoadedConfig {
|
|
|
183
190
|
*/
|
|
184
191
|
declare function loadConfig(cwd: string): Promise<LoadedConfig>;
|
|
185
192
|
//#endregion
|
|
186
|
-
export { Analytics, AppConfig, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoPolicyConfig, RepoRef, RepoSettings, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
|
|
193
|
+
export { Analytics, AppConfig, Auth, AuthDescription, AuthError, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoPolicyConfig, RepoRef, RepoSettings, type RequestOptions, ResolveTokenConfig, type ResolveTokenInput, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, type RestClient, type RestClientConfig, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,89 @@
|
|
|
1
1
|
import { CARDINALITY, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, isMulti } from "./capabilities/index.mjs";
|
|
2
|
+
import { AuthError, createResolveToken as createResolveToken$1, createRestClient } from "@theholocron/http-client";
|
|
2
3
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
3
4
|
import { readFile, stat } from "node:fs/promises";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
//#region src/keyring.ts
|
|
8
|
+
/**
|
|
9
|
+
* Keyring-backed bootstrap credential store.
|
|
10
|
+
*
|
|
11
|
+
* Every holocron plugin's bootstrap token (the one it needs before it
|
|
12
|
+
* can talk to its vendor's API) can be stored in the OS keyring under
|
|
13
|
+
* a single reverse-DNS service scope. Managed via `holocron auth`
|
|
14
|
+
* subcommands; consulted at position 4 in every plugin's auth
|
|
15
|
+
* precedence chain (after --token / HOLOCRON_<X>_TOKEN / <native>_TOKEN).
|
|
16
|
+
*
|
|
17
|
+
* See `.notes/tech-auth-bootstrap.spec.md` for the design rationale.
|
|
18
|
+
*
|
|
19
|
+
* Failure model: keyring access is best-effort. Platforms without a
|
|
20
|
+
* supported credential store (some Linux CI images, sandboxed
|
|
21
|
+
* environments) will throw from the underlying library. Every export
|
|
22
|
+
* here catches and returns a null/empty result rather than propagating
|
|
23
|
+
* — the plugin's precedence chain then falls through to
|
|
24
|
+
* env-var-only paths, which is exactly how CI is meant to work.
|
|
25
|
+
*/
|
|
26
|
+
const SERVICE = "com.theholocron.cli";
|
|
27
|
+
/**
|
|
28
|
+
* Store or overwrite a bootstrap token for a provider. Returns true on
|
|
29
|
+
* success, false when the underlying keyring is unsupported or errored.
|
|
30
|
+
*/
|
|
31
|
+
function setToken(provider, token) {
|
|
32
|
+
try {
|
|
33
|
+
new Entry(SERVICE, provider).setPassword(token);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read the bootstrap token for a provider. Returns `null` for both
|
|
41
|
+
* "not stored" and "keyring unavailable" — callers can treat them the
|
|
42
|
+
* same way (fall through to env-var precedence).
|
|
43
|
+
*/
|
|
44
|
+
function getToken(provider) {
|
|
45
|
+
try {
|
|
46
|
+
return new Entry(SERVICE, provider).getPassword();
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Delete a stored token. Returns true when a token was removed, false
|
|
53
|
+
* when there was nothing to delete or the keyring is unavailable.
|
|
54
|
+
* Distinguishing the two cases isn't worth the surface area — the
|
|
55
|
+
* command output makes the situation clear either way.
|
|
56
|
+
*/
|
|
57
|
+
function deleteToken(provider) {
|
|
58
|
+
try {
|
|
59
|
+
return new Entry(SERVICE, provider).deletePassword();
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* List provider slugs with a stored token in this service scope.
|
|
66
|
+
* Uses the library's `findCredentials(service)` — supported on all
|
|
67
|
+
* platforms the underlying credential store supports.
|
|
68
|
+
*/
|
|
69
|
+
function listStoredProviders() {
|
|
70
|
+
try {
|
|
71
|
+
return findCredentials(SERVICE).map((c) => c.account);
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/auth-resolver.ts
|
|
78
|
+
/** Wraps `createResolveToken` from `@theholocron/http` and injects the
|
|
79
|
+
* system keyring so plugins stay at a one-liner call site. */
|
|
80
|
+
function createResolveToken(config) {
|
|
81
|
+
return createResolveToken$1({
|
|
82
|
+
...config,
|
|
83
|
+
getKeyringToken: getToken
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
6
87
|
//#region src/config.ts
|
|
7
88
|
/**
|
|
8
89
|
* `holocron.config.json` schema, parser, and provider resolution.
|
|
@@ -119,76 +200,6 @@ function defineConfig(config) {
|
|
|
119
200
|
return config;
|
|
120
201
|
}
|
|
121
202
|
//#endregion
|
|
122
|
-
//#region src/keyring.ts
|
|
123
|
-
/**
|
|
124
|
-
* Keyring-backed bootstrap credential store.
|
|
125
|
-
*
|
|
126
|
-
* Every holocron plugin's bootstrap token (the one it needs before it
|
|
127
|
-
* can talk to its vendor's API) can be stored in the OS keyring under
|
|
128
|
-
* a single reverse-DNS service scope. Managed via `holocron auth`
|
|
129
|
-
* subcommands; consulted at position 4 in every plugin's auth
|
|
130
|
-
* precedence chain (after --token / HOLOCRON_<X>_TOKEN / <native>_TOKEN).
|
|
131
|
-
*
|
|
132
|
-
* See `.notes/tech-auth-bootstrap.spec.md` for the design rationale.
|
|
133
|
-
*
|
|
134
|
-
* Failure model: keyring access is best-effort. Platforms without a
|
|
135
|
-
* supported credential store (some Linux CI images, sandboxed
|
|
136
|
-
* environments) will throw from the underlying library. Every export
|
|
137
|
-
* here catches and returns a null/empty result rather than propagating
|
|
138
|
-
* — the plugin's precedence chain then falls through to
|
|
139
|
-
* env-var-only paths, which is exactly how CI is meant to work.
|
|
140
|
-
*/
|
|
141
|
-
const SERVICE = "com.theholocron.cli";
|
|
142
|
-
/**
|
|
143
|
-
* Store or overwrite a bootstrap token for a provider. Returns true on
|
|
144
|
-
* success, false when the underlying keyring is unsupported or errored.
|
|
145
|
-
*/
|
|
146
|
-
function setToken(provider, token) {
|
|
147
|
-
try {
|
|
148
|
-
new Entry(SERVICE, provider).setPassword(token);
|
|
149
|
-
return true;
|
|
150
|
-
} catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Read the bootstrap token for a provider. Returns `null` for both
|
|
156
|
-
* "not stored" and "keyring unavailable" — callers can treat them the
|
|
157
|
-
* same way (fall through to env-var precedence).
|
|
158
|
-
*/
|
|
159
|
-
function getToken(provider) {
|
|
160
|
-
try {
|
|
161
|
-
return new Entry(SERVICE, provider).getPassword();
|
|
162
|
-
} catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Delete a stored token. Returns true when a token was removed, false
|
|
168
|
-
* when there was nothing to delete or the keyring is unavailable.
|
|
169
|
-
* Distinguishing the two cases isn't worth the surface area — the
|
|
170
|
-
* command output makes the situation clear either way.
|
|
171
|
-
*/
|
|
172
|
-
function deleteToken(provider) {
|
|
173
|
-
try {
|
|
174
|
-
return new Entry(SERVICE, provider).deletePassword();
|
|
175
|
-
} catch {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* List provider slugs with a stored token in this service scope.
|
|
181
|
-
* Uses the library's `findCredentials(service)` — supported on all
|
|
182
|
-
* platforms the underlying credential store supports.
|
|
183
|
-
*/
|
|
184
|
-
function listStoredProviders() {
|
|
185
|
-
try {
|
|
186
|
-
return findCredentials(SERVICE).map((c) => c.account);
|
|
187
|
-
} catch {
|
|
188
|
-
return [];
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
//#endregion
|
|
192
203
|
//#region src/load-config.ts
|
|
193
204
|
/**
|
|
194
205
|
* `holocron.config.{json,js,ts}` file loader.
|
|
@@ -265,4 +276,4 @@ async function fileExists(path) {
|
|
|
265
276
|
}
|
|
266
277
|
}
|
|
267
278
|
//#endregion
|
|
268
|
-
export { CARDINALITY, ConfigError, ConfigFileError, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
|
|
279
|
+
export { AuthError, CARDINALITY, ConfigError, ConfigFileError, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
|
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.22",
|
|
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",
|
|
@@ -34,22 +34,23 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@napi-rs/keyring": "^1.3.0",
|
|
37
|
+
"@theholocron/http-client": "^0.1.0",
|
|
37
38
|
"tsx": "^4.22.4",
|
|
38
39
|
"yargs": "^18.0.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@theholocron/eslint-config": "^5.
|
|
42
|
-
"@theholocron/tsconfig": "^
|
|
43
|
-
"@
|
|
42
|
+
"@theholocron/eslint-config": "^5.2.0",
|
|
43
|
+
"@theholocron/tsconfig": "^6.0.0",
|
|
44
|
+
"@theholocron/vitest-config": "^5.2.0",
|
|
45
|
+
"@types/node": "^26",
|
|
44
46
|
"@types/yargs": "^17.0.35",
|
|
45
|
-
"@vitest/coverage-v8": "^3.2.6",
|
|
46
47
|
"@vitest/eslint-plugin": "^1.6.23",
|
|
47
48
|
"eslint": "^10.7.0",
|
|
48
49
|
"eslint-plugin-n": "^18.2.2",
|
|
49
50
|
"globals": "^17.7.0",
|
|
50
51
|
"tsdown": "^0.22.3",
|
|
51
52
|
"typescript": "^5.9.3",
|
|
52
|
-
"vitest": "^
|
|
53
|
+
"vitest": "^4.1.10",
|
|
53
54
|
"@theholocron/cli-utils": "0.0.0"
|
|
54
55
|
},
|
|
55
56
|
"publishConfig": {
|