@theholocron/cli 2.0.0-alpha.8 → 2.0.0
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 +141 -9
- package/dist/capabilities/index.d.mts +568 -2
- package/dist/capabilities/index.mjs +31 -1
- package/dist/cli.mjs +2641 -1345
- package/dist/index.d.mts +122 -43
- package/dist/index.mjs +328 -3
- package/package.json +17 -10
- package/dist/capabilities-DapaKOlX.mjs +0 -47
- package/dist/cli.d.mts +0 -1
- package/dist/index-jxPVFH7-.d.mts +0 -535
- package/dist/keyring-DwNEmrBc.mjs +0 -184
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { r as REQUIRED_CAPABILITIES, t as CARDINALITY } from "./capabilities-DapaKOlX.mjs";
|
|
2
|
-
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
3
|
-
//#region src/config.ts
|
|
4
|
-
/**
|
|
5
|
-
* `holocron.config.json` schema, parser, and provider resolution.
|
|
6
|
-
*
|
|
7
|
-
* ESLint-style entry forms:
|
|
8
|
-
*
|
|
9
|
-
* "source": "github" ← single, short
|
|
10
|
-
* "deployment": ["vercel", { team: "rando" }] ← single, with options
|
|
11
|
-
* "notifications": ["slack", "discord"] ← multi, short
|
|
12
|
-
* "notifications": [
|
|
13
|
-
* ["slack", { channel: "#ops" }],
|
|
14
|
-
* ["discord", { webhook: "env:HOOK" }]
|
|
15
|
-
* ] ← multi, with options
|
|
16
|
-
*
|
|
17
|
-
* Discriminator: an array entry is a `[provider, options]` tuple when
|
|
18
|
-
* the length is 2 AND element[1] is a non-array, non-null object.
|
|
19
|
-
* Otherwise it's a multi-provider list (string[] or tuple[]).
|
|
20
|
-
*
|
|
21
|
-
* Validation rules:
|
|
22
|
-
* - `vault` is REQUIRED (every project has secrets somewhere)
|
|
23
|
-
* - Entries for `'many'` capabilities are normalized to an array of
|
|
24
|
-
* normalized tuples; entries for `'single'` capabilities are
|
|
25
|
-
* normalized to one tuple
|
|
26
|
-
* - Tokens / secret values never appear in config — providers read
|
|
27
|
-
* them from env (or pull from `vault` at runtime)
|
|
28
|
-
*/
|
|
29
|
-
var ConfigError = class extends Error {
|
|
30
|
-
name = "ConfigError";
|
|
31
|
-
};
|
|
32
|
-
const PLUGIN_PREFIX = "@theholocron/holocron-plugin-";
|
|
33
|
-
const COMMUNITY_PREFIX = "holocron-plugin-";
|
|
34
|
-
/**
|
|
35
|
-
* Resolve `"github"` → `"@theholocron/holocron-plugin-github"`.
|
|
36
|
-
* Fully-qualified names (scoped or not) are honored verbatim, which
|
|
37
|
-
* is how third-party plugins published outside the org work.
|
|
38
|
-
*/
|
|
39
|
-
function resolvePluginPackage(provider) {
|
|
40
|
-
if (!provider) throw new ConfigError("provider name is empty");
|
|
41
|
-
if (provider.startsWith("@")) return provider;
|
|
42
|
-
if (provider.startsWith(COMMUNITY_PREFIX)) return provider;
|
|
43
|
-
if (provider.includes("/")) return provider;
|
|
44
|
-
return PLUGIN_PREFIX + provider;
|
|
45
|
-
}
|
|
46
|
-
/** A bare `[provider, options]` tuple, with both elements present? */
|
|
47
|
-
function isOptionsTuple(value) {
|
|
48
|
-
if (!Array.isArray(value)) return false;
|
|
49
|
-
if (value.length !== 2) return false;
|
|
50
|
-
if (typeof value[0] !== "string") return false;
|
|
51
|
-
const opt = value[1];
|
|
52
|
-
return typeof opt === "object" && opt !== null && !Array.isArray(opt);
|
|
53
|
-
}
|
|
54
|
-
function normalizeEntry(entry) {
|
|
55
|
-
if (typeof entry === "string") return {
|
|
56
|
-
provider: entry,
|
|
57
|
-
packageName: resolvePluginPackage(entry),
|
|
58
|
-
options: {}
|
|
59
|
-
};
|
|
60
|
-
const [provider, options] = entry;
|
|
61
|
-
return {
|
|
62
|
-
provider,
|
|
63
|
-
packageName: resolvePluginPackage(provider),
|
|
64
|
-
options
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function resolveEntry(key, raw) {
|
|
68
|
-
const cardinality = CARDINALITY[key];
|
|
69
|
-
if (typeof raw === "string") {
|
|
70
|
-
if (cardinality === "many") throw new ConfigError(`\`${key}\` accepts multiple providers; wrap a single one in an array: ["${raw}"]`);
|
|
71
|
-
return {
|
|
72
|
-
cardinality: "single",
|
|
73
|
-
tuple: normalizeEntry(raw)
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (!Array.isArray(raw)) throw new ConfigError(`\`${key}\` entry must be a string or array, got ${typeof raw}`);
|
|
77
|
-
if (isOptionsTuple(raw)) {
|
|
78
|
-
if (cardinality === "many") return {
|
|
79
|
-
cardinality: "many",
|
|
80
|
-
tuples: [normalizeEntry(raw)]
|
|
81
|
-
};
|
|
82
|
-
return {
|
|
83
|
-
cardinality: "single",
|
|
84
|
-
tuple: normalizeEntry(raw)
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (cardinality === "single") throw new ConfigError(`\`${key}\` accepts exactly one provider; got a multi-provider list with ${raw.length} entries`);
|
|
88
|
-
return {
|
|
89
|
-
cardinality: "many",
|
|
90
|
-
tuples: raw.map((entry, idx) => {
|
|
91
|
-
if (typeof entry === "string") return normalizeEntry(entry);
|
|
92
|
-
if (isOptionsTuple(entry)) return normalizeEntry(entry);
|
|
93
|
-
throw new ConfigError(`\`${key}[${idx}]\` must be a provider string or [provider, options] tuple`);
|
|
94
|
-
})
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function resolveConfig(raw) {
|
|
98
|
-
if (!raw.project?.name) throw new ConfigError("`project.name` is required");
|
|
99
|
-
if (!raw.providers || typeof raw.providers !== "object") throw new ConfigError("`providers` block is required");
|
|
100
|
-
const providers = {};
|
|
101
|
-
for (const [key, entry] of Object.entries(raw.providers)) {
|
|
102
|
-
if (entry === void 0) continue;
|
|
103
|
-
providers[key] = resolveEntry(key, entry);
|
|
104
|
-
}
|
|
105
|
-
for (const required of REQUIRED_CAPABILITIES) if (!providers[required]) throw new ConfigError(`required capability \`${required}\` is missing from providers`);
|
|
106
|
-
return {
|
|
107
|
-
project: raw.project,
|
|
108
|
-
providers,
|
|
109
|
-
apps: raw.apps ?? [],
|
|
110
|
-
doctor: raw.doctor ?? {}
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
//#endregion
|
|
114
|
-
//#region src/keyring.ts
|
|
115
|
-
/**
|
|
116
|
-
* Keyring-backed bootstrap credential store.
|
|
117
|
-
*
|
|
118
|
-
* Every holocron plugin's bootstrap token (the one it needs before it
|
|
119
|
-
* can talk to its vendor's API) can be stored in the OS keyring under
|
|
120
|
-
* a single reverse-DNS service scope. Managed via `holocron auth`
|
|
121
|
-
* subcommands; consulted at position 4 in every plugin's auth
|
|
122
|
-
* precedence chain (after --token / HOLOCRON_<X>_TOKEN / <native>_TOKEN).
|
|
123
|
-
*
|
|
124
|
-
* See `.notes/tech-auth-bootstrap.spec.md` for the design rationale.
|
|
125
|
-
*
|
|
126
|
-
* Failure model: keyring access is best-effort. Platforms without a
|
|
127
|
-
* supported credential store (some Linux CI images, sandboxed
|
|
128
|
-
* environments) will throw from the underlying library. Every export
|
|
129
|
-
* here catches and returns a null/empty result rather than propagating
|
|
130
|
-
* — the plugin's precedence chain then falls through to
|
|
131
|
-
* env-var-only paths, which is exactly how CI is meant to work.
|
|
132
|
-
*/
|
|
133
|
-
const SERVICE = "com.theholocron.cli";
|
|
134
|
-
/**
|
|
135
|
-
* Store or overwrite a bootstrap token for a provider. Returns true on
|
|
136
|
-
* success, false when the underlying keyring is unsupported or errored.
|
|
137
|
-
*/
|
|
138
|
-
function setToken(provider, token) {
|
|
139
|
-
try {
|
|
140
|
-
new Entry(SERVICE, provider).setPassword(token);
|
|
141
|
-
return true;
|
|
142
|
-
} catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Read the bootstrap token for a provider. Returns `null` for both
|
|
148
|
-
* "not stored" and "keyring unavailable" — callers can treat them the
|
|
149
|
-
* same way (fall through to env-var precedence).
|
|
150
|
-
*/
|
|
151
|
-
function getToken(provider) {
|
|
152
|
-
try {
|
|
153
|
-
return new Entry(SERVICE, provider).getPassword();
|
|
154
|
-
} catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Delete a stored token. Returns true when a token was removed, false
|
|
160
|
-
* when there was nothing to delete or the keyring is unavailable.
|
|
161
|
-
* Distinguishing the two cases isn't worth the surface area — the
|
|
162
|
-
* command output makes the situation clear either way.
|
|
163
|
-
*/
|
|
164
|
-
function deleteToken(provider) {
|
|
165
|
-
try {
|
|
166
|
-
return new Entry(SERVICE, provider).deletePassword();
|
|
167
|
-
} catch {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* List provider slugs with a stored token in this service scope.
|
|
173
|
-
* Uses the library's `findCredentials(service)` — supported on all
|
|
174
|
-
* platforms the underlying credential store supports.
|
|
175
|
-
*/
|
|
176
|
-
function listStoredProviders() {
|
|
177
|
-
try {
|
|
178
|
-
return findCredentials(SERVICE).map((c) => c.account);
|
|
179
|
-
} catch {
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
//#endregion
|
|
184
|
-
export { ConfigError as a, resolvePluginPackage as c, setToken as i, getToken as n, resolveConfig as o, listStoredProviders as r, resolveEntry as s, deleteToken as t };
|