@theholocron/cli 2.0.0-alpha.9 → 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 +2624 -1461
- 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
package/dist/index.d.mts
CHANGED
|
@@ -1,26 +1,62 @@
|
|
|
1
|
-
import {
|
|
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, LabelDef, LifecycleResult, LifecycleSlot, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, 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>;
|
|
12
|
+
/**
|
|
13
|
+
* The shape a capability config package's default export must satisfy.
|
|
14
|
+
* Config packages let teams share a pre-bundled provider + options across
|
|
15
|
+
* repos (Level 1 of the shareable-configs story, issue #75).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // @acme/holocron-vault/index.ts
|
|
19
|
+
* import type { CapabilityConfigPackage } from '@theholocron/cli'
|
|
20
|
+
* export default {
|
|
21
|
+
* provider: '1password',
|
|
22
|
+
* options: { vault: 'acme-app' },
|
|
23
|
+
* } satisfies CapabilityConfigPackage
|
|
24
|
+
*/
|
|
25
|
+
interface CapabilityConfigPackage {
|
|
26
|
+
provider: string;
|
|
27
|
+
options?: ProviderOptions;
|
|
28
|
+
}
|
|
5
29
|
type SingleEntry = string | [provider: string, options: ProviderOptions];
|
|
6
30
|
type MultiEntry = Array<string | [provider: string, options: ProviderOptions]>;
|
|
7
31
|
type RawProviderEntry = SingleEntry | MultiEntry;
|
|
8
32
|
type RawProvidersConfig = Partial<Record<CapabilityKey, RawProviderEntry>>;
|
|
9
|
-
|
|
33
|
+
type RepoProtection = "balanced" | "strict" | "none";
|
|
34
|
+
interface RepoProperties {
|
|
35
|
+
lifecycle?: "active" | "experimental" | "deprecated";
|
|
36
|
+
open_source?: boolean;
|
|
37
|
+
runtime_environment?: "node" | "browser" | "universal" | "none";
|
|
38
|
+
uses_external_packages?: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface RepoConfig {
|
|
41
|
+
/** "owner/name" — the GitHub repository coordinate. Derived from the git remote when absent. */
|
|
42
|
+
name?: string;
|
|
10
43
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* plus a ruleset that blocks force-push + deletion and requires a pull request (0 reviews).
|
|
14
|
-
*
|
|
15
|
-
* "strict" — everything in "balanced" plus required status checks from `requiredChecks`.
|
|
16
|
-
*
|
|
17
|
-
* "none" — skips repo settings + ruleset entirely.
|
|
18
|
-
*
|
|
19
|
-
* @default "balanced"
|
|
44
|
+
* Branch protection preset applied by `holocron setup`. When omitted,
|
|
45
|
+
* no protection is applied and no `branch_protection_level` property is set.
|
|
20
46
|
*/
|
|
21
|
-
|
|
22
|
-
/** CI check context names required on the default branch (used
|
|
47
|
+
protection?: RepoProtection;
|
|
48
|
+
/** CI check context names required on the default branch (only used when `protection` is "strict"). */
|
|
23
49
|
requiredChecks?: string[];
|
|
50
|
+
/**
|
|
51
|
+
* GitHub teams granted repository access. Synced by `holocron setup`, which
|
|
52
|
+
* also writes `.github/CODEOWNERS` for teams with write-or-higher permission.
|
|
53
|
+
* String shorthand defaults to `push` (Write).
|
|
54
|
+
*/
|
|
55
|
+
teams?: TeamEntry[];
|
|
56
|
+
/** GitHub topics set on the repository. */
|
|
57
|
+
topics?: string[];
|
|
58
|
+
/** GitHub custom properties synced to the org dashboard. */
|
|
59
|
+
properties?: RepoProperties;
|
|
24
60
|
}
|
|
25
61
|
interface AppConfig {
|
|
26
62
|
name: string;
|
|
@@ -31,38 +67,54 @@ interface DoctorConfig {
|
|
|
31
67
|
checks?: string[];
|
|
32
68
|
}
|
|
33
69
|
interface HolocronConfig {
|
|
34
|
-
|
|
70
|
+
/** Project name. Derived from package.json when absent. */
|
|
71
|
+
name?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Repository identity and metadata. When set, `PluginLoader` injects
|
|
75
|
+
* `repo.name` into every plugin's `RuntimeContext.repo` so plugins that
|
|
76
|
+
* need a repo (github, etc.) don't require `--repo` on every invocation.
|
|
77
|
+
* `--repo` on the command line still overrides.
|
|
78
|
+
*/
|
|
79
|
+
repo?: RepoConfig;
|
|
80
|
+
/**
|
|
81
|
+
* CI workflow names to install as thin wrappers during `holocron setup`.
|
|
82
|
+
* Each name maps to a reusable workflow in `theholocron/.github`.
|
|
83
|
+
* Use the object form to pass `with:` inputs to the reusable workflow.
|
|
84
|
+
*
|
|
85
|
+
* Supported values: "lint" | "test" | "typecheck" | "codeql" | "review" |
|
|
86
|
+
* "release" | "stale" | "greetings" | "dependencies" | "bookkeeping" | "audit"
|
|
87
|
+
*
|
|
88
|
+
* `holocron setup` writes `.github/workflows/<name>.yml` for each entry,
|
|
89
|
+
* calling the corresponding `ci-<name>.yml@main` reusable workflow.
|
|
90
|
+
* Files are overwritten on each run — they are generated artifacts.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ["lint", { "name": "release", "with": { "run-build": false } }]
|
|
94
|
+
*/
|
|
95
|
+
workflows?: Array<string | {
|
|
35
96
|
name: string;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* Repo coord — `"owner/name"`. When set, `PluginLoader` injects
|
|
39
|
-
* it into every plugin's `RuntimeContext.repo` so plugins that
|
|
40
|
-
* need a repo (github, etc.) don't require `--repo` on every
|
|
41
|
-
* invocation. `--repo` on the command line still overrides.
|
|
42
|
-
*/
|
|
43
|
-
repo?: string;
|
|
44
|
-
/**
|
|
45
|
-
* Repo-level policy applied by `holocron setup`. Defines merge
|
|
46
|
-
* strategy, branch protection rulesets, and security defaults.
|
|
47
|
-
* Requires `source` capability to be configured.
|
|
48
|
-
*/
|
|
49
|
-
repoPolicy?: RepoPolicyConfig;
|
|
50
|
-
/**
|
|
51
|
-
* CI workflow names to install as thin wrappers during `holocron setup`.
|
|
52
|
-
* Each name maps to a reusable workflow in `theholocron/.github`.
|
|
53
|
-
*
|
|
54
|
-
* Supported values: "lint" | "test" | "typecheck" | "codeql" | "review" |
|
|
55
|
-
* "release" | "stale" | "greetings" | "dependencies" | "bookkeeping-pr" | "audit"
|
|
56
|
-
*
|
|
57
|
-
* `holocron setup` writes `.github/workflows/<name>.yml` for each entry,
|
|
58
|
-
* calling the corresponding `ci-<name>.yml@main` reusable workflow.
|
|
59
|
-
* Files are overwritten on each run — they are generated artifacts.
|
|
60
|
-
*/
|
|
61
|
-
workflows?: string[];
|
|
62
|
-
};
|
|
97
|
+
with?: Record<string, unknown>;
|
|
98
|
+
}>;
|
|
63
99
|
providers: RawProvidersConfig;
|
|
64
100
|
apps?: AppConfig[];
|
|
65
101
|
doctor?: DoctorConfig;
|
|
102
|
+
/**
|
|
103
|
+
* Agent runtime that determines where skills are installed by `holocron setup`.
|
|
104
|
+
* Skills are installed to `.agents/skills/<name>/` (canonical) with a
|
|
105
|
+
* relative symlink at the agent-specific path:
|
|
106
|
+
* - `"claude"` → `.claude/skills/<name>` → `../../.agents/skills/<name>`
|
|
107
|
+
* - `"codex"` | `"gemini"` → logged as unsupported; skipped gracefully.
|
|
108
|
+
*/
|
|
109
|
+
agent?: "claude" | "codex" | "gemini";
|
|
110
|
+
/**
|
|
111
|
+
* Skill names from `@theholocron/skills` to install during `holocron setup`.
|
|
112
|
+
* Installed paths are gitignored and managed by setup — do not commit them.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ["git-safety", "pr-workflow", "commit-standards"]
|
|
116
|
+
*/
|
|
117
|
+
skills?: string[];
|
|
66
118
|
}
|
|
67
119
|
interface ResolvedTuple {
|
|
68
120
|
provider: string;
|
|
@@ -79,10 +131,18 @@ type ResolvedProviderEntry = {
|
|
|
79
131
|
};
|
|
80
132
|
type ResolvedProvidersConfig = Partial<Record<CapabilityKey, ResolvedProviderEntry>>;
|
|
81
133
|
interface ResolvedHolocronConfig {
|
|
82
|
-
|
|
134
|
+
name: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
repo?: RepoConfig;
|
|
137
|
+
workflows?: Array<string | {
|
|
138
|
+
name: string;
|
|
139
|
+
with?: Record<string, unknown>;
|
|
140
|
+
}>;
|
|
83
141
|
providers: ResolvedProvidersConfig;
|
|
84
142
|
apps: AppConfig[];
|
|
85
143
|
doctor: DoctorConfig;
|
|
144
|
+
agent?: "claude" | "codex" | "gemini";
|
|
145
|
+
skills?: string[];
|
|
86
146
|
}
|
|
87
147
|
declare class ConfigError extends Error {
|
|
88
148
|
name: string;
|
|
@@ -96,6 +156,9 @@ declare function resolvePluginPackage(provider: string): string;
|
|
|
96
156
|
declare function resolveEntry(key: CapabilityKey, raw: RawProviderEntry): ResolvedProviderEntry;
|
|
97
157
|
declare function resolveConfig(raw: HolocronConfig): ResolvedHolocronConfig;
|
|
98
158
|
//#endregion
|
|
159
|
+
//#region src/define-config.d.ts
|
|
160
|
+
declare function defineConfig(config: HolocronConfig): HolocronConfig;
|
|
161
|
+
//#endregion
|
|
99
162
|
//#region src/keyring.d.ts
|
|
100
163
|
/**
|
|
101
164
|
* Keyring-backed bootstrap credential store.
|
|
@@ -140,4 +203,20 @@ declare function deleteToken(provider: string): boolean;
|
|
|
140
203
|
*/
|
|
141
204
|
declare function listStoredProviders(): string[];
|
|
142
205
|
//#endregion
|
|
143
|
-
|
|
206
|
+
//#region src/load-config.d.ts
|
|
207
|
+
declare class ConfigFileError extends Error {
|
|
208
|
+
name: string;
|
|
209
|
+
}
|
|
210
|
+
interface LoadedConfig {
|
|
211
|
+
resolved: ResolvedHolocronConfig;
|
|
212
|
+
/** Absolute path to the file the config was read from. */
|
|
213
|
+
filepath: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Read + parse + resolve `holocron.config.*` from the given directory.
|
|
217
|
+
* Search order: json → js → ts. Throws `ConfigFileError` if nothing
|
|
218
|
+
* found, or `ConfigError` if the config is malformed / invalid.
|
|
219
|
+
*/
|
|
220
|
+
declare function loadConfig(cwd: string): Promise<LoadedConfig>;
|
|
221
|
+
//#endregion
|
|
222
|
+
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, LabelDef, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoConfig, RepoProperties, RepoProtection, RepoRef, RepoSettings, type RequestOptions, ResolveTokenConfig, type ResolveTokenInput, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, type RestClient, type RestClientConfig, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, 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,3 +1,328 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { CARDINALITY, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, isMulti } from "./capabilities/index.mjs";
|
|
2
|
+
import { AuthError, createResolveToken as createResolveToken$1, createRestClient } from "@theholocron/http-client";
|
|
3
|
+
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { readFile, stat } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
//#region src/keyring.ts
|
|
10
|
+
/**
|
|
11
|
+
* Keyring-backed bootstrap credential store.
|
|
12
|
+
*
|
|
13
|
+
* Every holocron plugin's bootstrap token (the one it needs before it
|
|
14
|
+
* can talk to its vendor's API) can be stored in the OS keyring under
|
|
15
|
+
* a single reverse-DNS service scope. Managed via `holocron auth`
|
|
16
|
+
* subcommands; consulted at position 4 in every plugin's auth
|
|
17
|
+
* precedence chain (after --token / HOLOCRON_<X>_TOKEN / <native>_TOKEN).
|
|
18
|
+
*
|
|
19
|
+
* See `.notes/tech-auth-bootstrap.spec.md` for the design rationale.
|
|
20
|
+
*
|
|
21
|
+
* Failure model: keyring access is best-effort. Platforms without a
|
|
22
|
+
* supported credential store (some Linux CI images, sandboxed
|
|
23
|
+
* environments) will throw from the underlying library. Every export
|
|
24
|
+
* here catches and returns a null/empty result rather than propagating
|
|
25
|
+
* — the plugin's precedence chain then falls through to
|
|
26
|
+
* env-var-only paths, which is exactly how CI is meant to work.
|
|
27
|
+
*/
|
|
28
|
+
const SERVICE = "com.theholocron.cli";
|
|
29
|
+
/**
|
|
30
|
+
* Store or overwrite a bootstrap token for a provider. Returns true on
|
|
31
|
+
* success, false when the underlying keyring is unsupported or errored.
|
|
32
|
+
*/
|
|
33
|
+
function setToken(provider, token) {
|
|
34
|
+
try {
|
|
35
|
+
new Entry(SERVICE, provider).setPassword(token);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the bootstrap token for a provider. Returns `null` for both
|
|
43
|
+
* "not stored" and "keyring unavailable" — callers can treat them the
|
|
44
|
+
* same way (fall through to env-var precedence).
|
|
45
|
+
*/
|
|
46
|
+
function getToken(provider) {
|
|
47
|
+
try {
|
|
48
|
+
return new Entry(SERVICE, provider).getPassword();
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Delete a stored token. Returns true when a token was removed, false
|
|
55
|
+
* when there was nothing to delete or the keyring is unavailable.
|
|
56
|
+
* Distinguishing the two cases isn't worth the surface area — the
|
|
57
|
+
* command output makes the situation clear either way.
|
|
58
|
+
*/
|
|
59
|
+
function deleteToken(provider) {
|
|
60
|
+
try {
|
|
61
|
+
return new Entry(SERVICE, provider).deletePassword();
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* List provider slugs with a stored token in this service scope.
|
|
68
|
+
* Uses the library's `findCredentials(service)` — supported on all
|
|
69
|
+
* platforms the underlying credential store supports.
|
|
70
|
+
*/
|
|
71
|
+
function listStoredProviders() {
|
|
72
|
+
try {
|
|
73
|
+
return findCredentials(SERVICE).map((c) => c.account);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/auth-resolver.ts
|
|
80
|
+
/** Wraps `createResolveToken` from `@theholocron/http` and injects the
|
|
81
|
+
* system keyring so plugins stay at a one-liner call site. */
|
|
82
|
+
function createResolveToken(config) {
|
|
83
|
+
return createResolveToken$1({
|
|
84
|
+
...config,
|
|
85
|
+
getKeyringToken: getToken
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/config.ts
|
|
90
|
+
/**
|
|
91
|
+
* `holocron.config.json` schema, parser, and provider resolution.
|
|
92
|
+
*
|
|
93
|
+
* ESLint-style entry forms:
|
|
94
|
+
*
|
|
95
|
+
* "source": "github" ← single, short
|
|
96
|
+
* "deployment": ["vercel", { team: "rando" }] ← single, with options
|
|
97
|
+
* "notifications": ["slack", "discord"] ← multi, short
|
|
98
|
+
* "notifications": [
|
|
99
|
+
* ["slack", { channel: "#ops" }],
|
|
100
|
+
* ["discord", { webhook: "env:HOOK" }]
|
|
101
|
+
* ] ← multi, with options
|
|
102
|
+
*
|
|
103
|
+
* Discriminator: an array entry is a `[provider, options]` tuple when
|
|
104
|
+
* the length is 2 AND element[1] is a non-array, non-null object.
|
|
105
|
+
* Otherwise it's a multi-provider list (string[] or tuple[]).
|
|
106
|
+
*
|
|
107
|
+
* Validation rules:
|
|
108
|
+
* - `vault` is REQUIRED (every project has secrets somewhere)
|
|
109
|
+
* - Entries for `'many'` capabilities are normalized to an array of
|
|
110
|
+
* normalized tuples; entries for `'single'` capabilities are
|
|
111
|
+
* normalized to one tuple
|
|
112
|
+
* - Tokens / secret values never appear in config — providers read
|
|
113
|
+
* them from env (or pull from `vault` at runtime)
|
|
114
|
+
*/
|
|
115
|
+
var ConfigError = class extends Error {
|
|
116
|
+
name = "ConfigError";
|
|
117
|
+
};
|
|
118
|
+
const PLUGIN_PREFIX = "@theholocron/holocron-plugin-";
|
|
119
|
+
const COMMUNITY_PREFIX = "holocron-plugin-";
|
|
120
|
+
/**
|
|
121
|
+
* Resolve `"github"` → `"@theholocron/holocron-plugin-github"`.
|
|
122
|
+
* Fully-qualified names (scoped or not) are honored verbatim, which
|
|
123
|
+
* is how third-party plugins published outside the org work.
|
|
124
|
+
*/
|
|
125
|
+
function resolvePluginPackage(provider) {
|
|
126
|
+
if (!provider) throw new ConfigError("provider name is empty");
|
|
127
|
+
if (provider.startsWith("@")) return provider;
|
|
128
|
+
if (provider.startsWith(COMMUNITY_PREFIX)) return provider;
|
|
129
|
+
if (provider.includes("/")) return provider;
|
|
130
|
+
return PLUGIN_PREFIX + provider;
|
|
131
|
+
}
|
|
132
|
+
/** A bare `[provider, options]` tuple, with both elements present? */
|
|
133
|
+
function isOptionsTuple(value) {
|
|
134
|
+
if (!Array.isArray(value)) return false;
|
|
135
|
+
if (value.length !== 2) return false;
|
|
136
|
+
if (typeof value[0] !== "string") return false;
|
|
137
|
+
const opt = value[1];
|
|
138
|
+
return typeof opt === "object" && opt !== null && !Array.isArray(opt);
|
|
139
|
+
}
|
|
140
|
+
function normalizeEntry(entry) {
|
|
141
|
+
if (typeof entry === "string") return {
|
|
142
|
+
provider: entry,
|
|
143
|
+
packageName: resolvePluginPackage(entry),
|
|
144
|
+
options: {}
|
|
145
|
+
};
|
|
146
|
+
const [provider, options] = entry;
|
|
147
|
+
return {
|
|
148
|
+
provider,
|
|
149
|
+
packageName: resolvePluginPackage(provider),
|
|
150
|
+
options
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function resolveEntry(key, raw) {
|
|
154
|
+
const cardinality = CARDINALITY[key];
|
|
155
|
+
if (typeof raw === "string") {
|
|
156
|
+
if (cardinality === "many") throw new ConfigError(`\`${key}\` accepts multiple providers; wrap a single one in an array: ["${raw}"]`);
|
|
157
|
+
return {
|
|
158
|
+
cardinality: "single",
|
|
159
|
+
tuple: normalizeEntry(raw)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(raw)) throw new ConfigError(`\`${key}\` entry must be a string or array, got ${typeof raw}`);
|
|
163
|
+
if (isOptionsTuple(raw)) {
|
|
164
|
+
if (cardinality === "many") return {
|
|
165
|
+
cardinality: "many",
|
|
166
|
+
tuples: [normalizeEntry(raw)]
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
cardinality: "single",
|
|
170
|
+
tuple: normalizeEntry(raw)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (cardinality === "single") throw new ConfigError(`\`${key}\` accepts exactly one provider; got a multi-provider list with ${raw.length} entries`);
|
|
174
|
+
return {
|
|
175
|
+
cardinality: "many",
|
|
176
|
+
tuples: raw.map((entry, idx) => {
|
|
177
|
+
if (typeof entry === "string") return normalizeEntry(entry);
|
|
178
|
+
if (isOptionsTuple(entry)) return normalizeEntry(entry);
|
|
179
|
+
throw new ConfigError(`\`${key}[${idx}]\` must be a provider string or [provider, options] tuple`);
|
|
180
|
+
})
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function resolveConfig(raw) {
|
|
184
|
+
if (!raw.name) throw new ConfigError("`name` is required");
|
|
185
|
+
if (!raw.providers || typeof raw.providers !== "object") throw new ConfigError("`providers` block is required");
|
|
186
|
+
const providers = {};
|
|
187
|
+
for (const [key, entry] of Object.entries(raw.providers)) {
|
|
188
|
+
if (entry === void 0) continue;
|
|
189
|
+
providers[key] = resolveEntry(key, entry);
|
|
190
|
+
}
|
|
191
|
+
for (const required of REQUIRED_CAPABILITIES) if (!providers[required]) throw new ConfigError(`required capability \`${required}\` is missing from providers`);
|
|
192
|
+
return {
|
|
193
|
+
name: raw.name,
|
|
194
|
+
description: raw.description,
|
|
195
|
+
repo: raw.repo,
|
|
196
|
+
workflows: raw.workflows,
|
|
197
|
+
providers,
|
|
198
|
+
apps: raw.apps ?? [],
|
|
199
|
+
doctor: raw.doctor ?? {},
|
|
200
|
+
agent: raw.agent,
|
|
201
|
+
skills: raw.skills
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/define-config.ts
|
|
206
|
+
function defineConfig(config) {
|
|
207
|
+
return config;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/load-config.ts
|
|
211
|
+
/**
|
|
212
|
+
* `holocron.config.{json,js,ts}` file loader.
|
|
213
|
+
*
|
|
214
|
+
* Search order: json → js → ts. JSON is parsed directly; JS is loaded
|
|
215
|
+
* via native dynamic import; TS is loaded via `tsImport` from tsx (a
|
|
216
|
+
* runtime dep) so operators can write typed configs with `defineConfig`
|
|
217
|
+
* without needing a separate build step.
|
|
218
|
+
*
|
|
219
|
+
* All three forms are validated through the same `resolveConfig` path.
|
|
220
|
+
* Implements the lookup-order contract from issue #75 / #81.
|
|
221
|
+
*/
|
|
222
|
+
const execFileAsync = promisify(execFile);
|
|
223
|
+
const CANDIDATE_FILENAMES = [
|
|
224
|
+
"holocron.config.json",
|
|
225
|
+
"holocron.config.js",
|
|
226
|
+
"holocron.config.ts"
|
|
227
|
+
];
|
|
228
|
+
var ConfigFileError = class extends Error {
|
|
229
|
+
name = "ConfigFileError";
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Read + parse + resolve `holocron.config.*` from the given directory.
|
|
233
|
+
* Search order: json → js → ts. Throws `ConfigFileError` if nothing
|
|
234
|
+
* found, or `ConfigError` if the config is malformed / invalid.
|
|
235
|
+
*/
|
|
236
|
+
async function loadConfig(cwd) {
|
|
237
|
+
for (const filename of CANDIDATE_FILENAMES) {
|
|
238
|
+
const fullPath = join(cwd, filename);
|
|
239
|
+
if (await fileExists(fullPath)) {
|
|
240
|
+
if (filename.endsWith(".json")) return {
|
|
241
|
+
resolved: await loadJson(fullPath),
|
|
242
|
+
filepath: fullPath
|
|
243
|
+
};
|
|
244
|
+
if (filename.endsWith(".ts")) return {
|
|
245
|
+
resolved: await loadTs(fullPath),
|
|
246
|
+
filepath: fullPath
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
resolved: await loadJs(fullPath),
|
|
250
|
+
filepath: fullPath
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw new ConfigFileError(`no holocron.config.{json,js,ts} found in ${cwd}. Create one — see the README for the schema.`);
|
|
255
|
+
}
|
|
256
|
+
async function loadJson(filepath) {
|
|
257
|
+
const raw = await readFile(filepath, "utf8");
|
|
258
|
+
let parsed;
|
|
259
|
+
try {
|
|
260
|
+
parsed = JSON.parse(raw);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
throw new ConfigError(`${filepath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
263
|
+
}
|
|
264
|
+
return resolveConfig(await deriveDefaults(dirname(filepath), parsed));
|
|
265
|
+
}
|
|
266
|
+
async function loadJs(filepath) {
|
|
267
|
+
const mod = await import(pathToFileURL(filepath).href);
|
|
268
|
+
return resolveConfig(await deriveDefaults(dirname(filepath), extractRaw(filepath, mod)));
|
|
269
|
+
}
|
|
270
|
+
async function loadTs(filepath) {
|
|
271
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
272
|
+
const mod = await tsImport(pathToFileURL(filepath).href, import.meta.url);
|
|
273
|
+
return resolveConfig(await deriveDefaults(dirname(filepath), extractRaw(filepath, mod)));
|
|
274
|
+
}
|
|
275
|
+
function extractRaw(filepath, mod) {
|
|
276
|
+
const outer = mod.default;
|
|
277
|
+
const raw = outer?.__esModule === true ? outer.default : outer;
|
|
278
|
+
if (raw === void 0 || raw === null) throw new ConfigFileError(`${filepath} must have a default export (use \`export default defineConfig({…})\`)`);
|
|
279
|
+
return raw;
|
|
280
|
+
}
|
|
281
|
+
async function deriveDefaults(configDir, raw) {
|
|
282
|
+
const result = { ...raw };
|
|
283
|
+
if (!result.name) result.name = await readPackageJsonName(configDir) ?? basename(configDir);
|
|
284
|
+
if (result.repo && !result.repo.name) {
|
|
285
|
+
const repoName = await readGitRemote(configDir);
|
|
286
|
+
if (repoName) result.repo = {
|
|
287
|
+
...result.repo,
|
|
288
|
+
name: repoName
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
async function readPackageJsonName(dir) {
|
|
294
|
+
try {
|
|
295
|
+
const content = await readFile(join(dir, "package.json"), "utf8");
|
|
296
|
+
const pkg = JSON.parse(content);
|
|
297
|
+
return typeof pkg.name === "string" ? pkg.name.replace(/^@[^/]+\//, "") : void 0;
|
|
298
|
+
} catch {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function readGitRemote(dir) {
|
|
303
|
+
try {
|
|
304
|
+
const { stdout } = await execFileAsync("git", [
|
|
305
|
+
"remote",
|
|
306
|
+
"get-url",
|
|
307
|
+
"origin"
|
|
308
|
+
], { cwd: dir });
|
|
309
|
+
return parseGitRemoteUrl(stdout.trim());
|
|
310
|
+
} catch {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function parseGitRemoteUrl(url) {
|
|
315
|
+
const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
316
|
+
if (httpsMatch) return httpsMatch[1];
|
|
317
|
+
const sshMatch = url.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
318
|
+
if (sshMatch) return sshMatch[1];
|
|
319
|
+
}
|
|
320
|
+
async function fileExists(path) {
|
|
321
|
+
try {
|
|
322
|
+
return (await stat(path)).isFile();
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
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
|
|
3
|
+
"version": "2.0.0",
|
|
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,26 +34,33 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@napi-rs/keyring": "^1.3.0",
|
|
37
|
+
"@theholocron/github-client": "^1.1.0",
|
|
38
|
+
"@theholocron/http-client": "^1.1.0",
|
|
39
|
+
"chalk": "^5.4.1",
|
|
40
|
+
"ora": "^8.2.0",
|
|
41
|
+
"tsx": "^4.22.4",
|
|
37
42
|
"yargs": "^18.0.0"
|
|
38
43
|
},
|
|
39
44
|
"devDependencies": {
|
|
40
|
-
"@theholocron/eslint-config": "^
|
|
41
|
-
"@theholocron/tsconfig": "^
|
|
42
|
-
"@
|
|
45
|
+
"@theholocron/eslint-config": "^7.3.0",
|
|
46
|
+
"@theholocron/tsconfig": "^7.3.0",
|
|
47
|
+
"@theholocron/vitest-config": "^7.3.0",
|
|
48
|
+
"@types/node": "^26",
|
|
43
49
|
"@types/yargs": "^17.0.35",
|
|
44
|
-
"@vitest/coverage-v8": "^
|
|
45
|
-
"eslint": "^
|
|
46
|
-
"
|
|
50
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
51
|
+
"@vitest/eslint-plugin": "^1.6.23",
|
|
52
|
+
"eslint": "^10.7.0",
|
|
53
|
+
"eslint-plugin-n": "^18.2.2",
|
|
54
|
+
"globals": "^17.7.0",
|
|
47
55
|
"tsdown": "^0.22.3",
|
|
48
56
|
"typescript": "^5.9.3",
|
|
49
|
-
"vitest": "^
|
|
50
|
-
"@theholocron/cli-utils": "0.0.0"
|
|
57
|
+
"vitest": "^4.1.10"
|
|
51
58
|
},
|
|
52
59
|
"publishConfig": {
|
|
53
60
|
"access": "public"
|
|
54
61
|
},
|
|
55
62
|
"scripts": {
|
|
56
|
-
"build": "tsdown",
|
|
63
|
+
"build": "tsdown --config-loader tsx",
|
|
57
64
|
"start": "tsx ./src/cli.ts",
|
|
58
65
|
"lint": "eslint .",
|
|
59
66
|
"typecheck": "tsc --noEmit",
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
//#region src/capabilities/index.ts
|
|
2
|
-
const CARDINALITY = {
|
|
3
|
-
source: "single",
|
|
4
|
-
ci: "single",
|
|
5
|
-
secrets: "single",
|
|
6
|
-
environments: "single",
|
|
7
|
-
issues: "single",
|
|
8
|
-
deployment: "single",
|
|
9
|
-
storage: "single",
|
|
10
|
-
auth: "single",
|
|
11
|
-
vault: "single",
|
|
12
|
-
dns: "single",
|
|
13
|
-
tooling: "many",
|
|
14
|
-
notifications: "many",
|
|
15
|
-
analytics: "many",
|
|
16
|
-
observability: "many"
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* No capabilities are strictly required — repos without secrets (e.g. org
|
|
20
|
-
* community health repos) legitimately omit vault. Plugins validate their
|
|
21
|
-
* own requirements at call time.
|
|
22
|
-
*/
|
|
23
|
-
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
|
-
var WebhookVerificationError = class extends Error {
|
|
41
|
-
name = "WebhookVerificationError";
|
|
42
|
-
};
|
|
43
|
-
function isMulti(key) {
|
|
44
|
-
return CARDINALITY[key] === "many";
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
|
-
export { isMulti as a, WebhookVerificationError as i, ProviderApiError as n, REQUIRED_CAPABILITIES as r, CARDINALITY as t };
|
package/dist/cli.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|