@theholocron/cli 5.0.0-alpha.17 → 5.0.0-alpha.170
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 +224 -53
- package/dist/cli.mjs +3390 -1777
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +221 -49
- package/dist/index.mjs +141 -49
- package/dist/plugin/capabilities.d.mts +208 -51
- package/dist/plugin/capabilities.mjs +2 -1
- package/package.json +19 -13
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { CARDINALITY, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, isMulti } from "./plugin/capabilities.mjs";
|
|
2
2
|
import { AuthError, createResolveToken as createResolveToken$1, createRestClient } from "@theholocron/http-client";
|
|
3
|
-
import { createEnvLookup } from "@theholocron/env-utils";
|
|
3
|
+
import { createEnvLookup, withEnvPrefix } from "@theholocron/env-utils";
|
|
4
4
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
7
5
|
import { basename, dirname, join } from "node:path";
|
|
8
|
-
import {
|
|
6
|
+
import { readPackageJson } from "@theholocron/pkg-utils";
|
|
9
7
|
import { loadTasksConfig } from "@theholocron/astromech/config";
|
|
10
8
|
import { ConfigFileError, ConfigFileError as ConfigFileError$1, loadConfigFile } from "@theholocron/datapad";
|
|
11
|
-
|
|
9
|
+
import { readGitRemote } from "@theholocron/git-utils";
|
|
10
|
+
//#region src/env/env.ts
|
|
11
|
+
/** Singleton env for simple global lookups throughout the CLI. */
|
|
12
|
+
const env = createEnvLookup();
|
|
13
|
+
withEnvPrefix(env, "HOLOCRON");
|
|
12
14
|
/**
|
|
13
15
|
* Create an injectable env for commands that accept a fake env in tests.
|
|
14
16
|
* Pass `input.env` when available, falls back to `process.env`.
|
|
@@ -17,7 +19,7 @@ function makeEnv(source) {
|
|
|
17
19
|
return createEnvLookup(source);
|
|
18
20
|
}
|
|
19
21
|
//#endregion
|
|
20
|
-
//#region src/auth/keyring.ts
|
|
22
|
+
//#region src/auth/keyring/keyring.ts
|
|
21
23
|
/**
|
|
22
24
|
* Keyring-backed bootstrap credential store.
|
|
23
25
|
*
|
|
@@ -87,7 +89,7 @@ function listStoredProviders() {
|
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
//#endregion
|
|
90
|
-
//#region src/auth/auth-resolver.ts
|
|
92
|
+
//#region src/auth/auth-resolver/auth-resolver.ts
|
|
91
93
|
/** Wraps `createResolveToken` from `@theholocron/http` and injects the
|
|
92
94
|
* system keyring so plugins stay at a one-liner call site. */
|
|
93
95
|
function createResolveToken(config) {
|
|
@@ -113,7 +115,114 @@ function createFeatureResolver(config) {
|
|
|
113
115
|
};
|
|
114
116
|
}
|
|
115
117
|
//#endregion
|
|
116
|
-
//#region src/
|
|
118
|
+
//#region src/commands/setup/derived-properties/derived-properties.ts
|
|
119
|
+
/**
|
|
120
|
+
* Repo archetype, in priority order:
|
|
121
|
+
* 1. `*-template` repo name → `template` (independent of package shape).
|
|
122
|
+
* 2. Monorepo whose primary published artifact is a CLI (some workspace
|
|
123
|
+
* package has `bin`) → `platform` — matches `holocron` itself: the CLI is
|
|
124
|
+
* the deliverable, the plugin packages are supporting infrastructure for
|
|
125
|
+
* it, not separate products.
|
|
126
|
+
* 3. Monorepo with no CLI but at least one non-private workspace package →
|
|
127
|
+
* `library` — matches `clients`/`configs`/`utils`/`themes`/
|
|
128
|
+
* `observability`: a family of published packages, none of them a CLI.
|
|
129
|
+
* Checked *before* falling into the root-package branch below, because
|
|
130
|
+
* these repos' own root `package.json` is private and often carries
|
|
131
|
+
* `@theholocron/astro-config` for a docs site built from the monorepo
|
|
132
|
+
* root — without this check they'd wrongly resolve to `docs` off that
|
|
133
|
+
* root-level signal alone, when the docs site is a secondary concern of
|
|
134
|
+
* a library collection, not what the repo fundamentally is.
|
|
135
|
+
* 4. Single-package repo with `package.json#bin` → `cli`.
|
|
136
|
+
* 5. Docs-only site (astro/starlight present, root package private, no
|
|
137
|
+
* publishable exports) → `docs`.
|
|
138
|
+
* 6. Private, non-publishable, non-docs → `app`.
|
|
139
|
+
* 7. Otherwise, a single publishable package → `library`.
|
|
140
|
+
*/
|
|
141
|
+
function deriveProfile(input) {
|
|
142
|
+
if (/-template$/.test(input.repoName)) return "template";
|
|
143
|
+
if (input.isMonorepo) {
|
|
144
|
+
if (input.workspacePackageJsons.some((pkg) => Boolean(pkg.bin))) return "platform";
|
|
145
|
+
if (input.workspacePackageJsons.some((pkg) => pkg.private !== true)) return "library";
|
|
146
|
+
}
|
|
147
|
+
const pkg = input.rootPackageJson;
|
|
148
|
+
if (pkg?.bin) return "cli";
|
|
149
|
+
const deps = {
|
|
150
|
+
...pkg?.dependencies,
|
|
151
|
+
...pkg?.devDependencies
|
|
152
|
+
};
|
|
153
|
+
const isDocsSite = Boolean(deps["@theholocron/astro-config"] || deps["astro"]);
|
|
154
|
+
if (pkg?.private && isDocsSite) return "docs";
|
|
155
|
+
if (pkg?.private) return "app";
|
|
156
|
+
return "library";
|
|
157
|
+
}
|
|
158
|
+
const KNOWN_STACK_DEPS = [
|
|
159
|
+
"next",
|
|
160
|
+
"vite",
|
|
161
|
+
"astro",
|
|
162
|
+
"tsdown",
|
|
163
|
+
"rollup",
|
|
164
|
+
"webpack",
|
|
165
|
+
"storybook",
|
|
166
|
+
"vitest",
|
|
167
|
+
"playwright",
|
|
168
|
+
"turbo"
|
|
169
|
+
];
|
|
170
|
+
/**
|
|
171
|
+
* Detected build/framework tooling — the literal "does this repo need
|
|
172
|
+
* next/vite/tsdown" signal `holocron.config.ts` has no field for today
|
|
173
|
+
* (framework choice isn't a config concept, unlike `providers`/`tasks`).
|
|
174
|
+
* Curated table, not exhaustive by design — add to `KNOWN_STACK_DEPS` as new
|
|
175
|
+
* frameworks show up rather than trying to detect anything on npm.
|
|
176
|
+
*/
|
|
177
|
+
function deriveStack(pkg) {
|
|
178
|
+
const deps = {
|
|
179
|
+
...pkg?.dependencies,
|
|
180
|
+
...pkg?.devDependencies
|
|
181
|
+
};
|
|
182
|
+
return KNOWN_STACK_DEPS.filter((name) => name in deps);
|
|
183
|
+
}
|
|
184
|
+
/** The provider capability keys actually wired in `providers: {}` — zero heuristics. */
|
|
185
|
+
function deriveCapabilities(providers) {
|
|
186
|
+
return Object.keys(providers ?? {}).sort();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* `source` + `ci` are the minimal baseline every repo `holocron repo setup`
|
|
190
|
+
* touches wires — every `holocron.config.ts` in this org already satisfies
|
|
191
|
+
* this, so `non-compliant` here means drift (a provider manually removed
|
|
192
|
+
* after setup), not an unmet aspirational policy. Deliberately not a richer
|
|
193
|
+
* per-profile policy yet (e.g. "a `library` must have `deployment`") — that's
|
|
194
|
+
* a Phase B (GitHub App) refinement once it can post *why* on a check run.
|
|
195
|
+
*/
|
|
196
|
+
const REQUIRED_BASELINE = ["source", "ci"];
|
|
197
|
+
function deriveCompliance(capabilities) {
|
|
198
|
+
return REQUIRED_BASELINE.every((key) => capabilities.includes(key)) ? "compliant" : "non-compliant";
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The Phase B refinement `deriveCompliance()`'s own doc comment names —
|
|
202
|
+
* *why* a repo is non-compliant, not just that it is. `[]` when compliant.
|
|
203
|
+
* Same `REQUIRED_BASELINE` `deriveCompliance()` checks against — one
|
|
204
|
+
* table, not two independently-maintained baselines.
|
|
205
|
+
*/
|
|
206
|
+
function missingCapabilities(capabilities) {
|
|
207
|
+
return REQUIRED_BASELINE.filter((key) => !capabilities.includes(key));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Reads each workspace package's actual `package.json` content (not just
|
|
211
|
+
* `readWorkspacePackages()`'s `{ slug, name, dir }` — `deriveProfile()`'s
|
|
212
|
+
* `platform` detection needs `bin`, which that helper doesn't carry).
|
|
213
|
+
* Missing/invalid files are skipped, matching `readWorkspacePackages()`'s
|
|
214
|
+
* own soft-fail behavior.
|
|
215
|
+
*/
|
|
216
|
+
async function readWorkspacePackageJsons(repoRoot, workspacePackages) {
|
|
217
|
+
const results = [];
|
|
218
|
+
for (const { slug, dir = "packages" } of workspacePackages) {
|
|
219
|
+
const pkg = await readPackageJson(join(repoRoot, dir, slug));
|
|
220
|
+
if (pkg) results.push(pkg);
|
|
221
|
+
}
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/config/config/config.ts
|
|
117
226
|
var ConfigError = class extends Error {
|
|
118
227
|
name = "ConfigError";
|
|
119
228
|
};
|
|
@@ -217,11 +326,12 @@ function resolveConfig(raw) {
|
|
|
217
326
|
docs,
|
|
218
327
|
env: raw.env,
|
|
219
328
|
log: raw.log,
|
|
220
|
-
telemetry: raw.telemetry
|
|
329
|
+
telemetry: raw.telemetry,
|
|
330
|
+
eslint: raw.eslint
|
|
221
331
|
};
|
|
222
332
|
}
|
|
223
333
|
//#endregion
|
|
224
|
-
//#region src/config/compose.ts
|
|
334
|
+
//#region src/config/compose/compose.ts
|
|
225
335
|
function taskName(e) {
|
|
226
336
|
return typeof e === "string" ? e : e.name;
|
|
227
337
|
}
|
|
@@ -309,12 +419,12 @@ function compose(...args) {
|
|
|
309
419
|
};
|
|
310
420
|
}
|
|
311
421
|
//#endregion
|
|
312
|
-
//#region src/config/define-config.ts
|
|
422
|
+
//#region src/config/define-config/define-config.ts
|
|
313
423
|
function defineConfig(config) {
|
|
314
424
|
return config;
|
|
315
425
|
}
|
|
316
426
|
//#endregion
|
|
317
|
-
//#region src/config/load-config.ts
|
|
427
|
+
//#region src/config/load-config/load-config.ts
|
|
318
428
|
/**
|
|
319
429
|
* `holocron.config.{ts,js,mjs,cjs,json}` loader.
|
|
320
430
|
*
|
|
@@ -326,34 +436,39 @@ function defineConfig(config) {
|
|
|
326
436
|
* Probe order is TS-first (`.ts` → `.js` → `.mjs` → `.cjs` → `.json`) —
|
|
327
437
|
* see ADR-0010 / issue #75 / #81.
|
|
328
438
|
*/
|
|
329
|
-
const execFileAsync = promisify(execFile);
|
|
330
439
|
/**
|
|
331
|
-
* Read + parse + resolve `holocron.config.*` from the given
|
|
440
|
+
* Read + parse + resolve `holocron.config.*` starting from the given
|
|
441
|
+
* directory, walking up through ancestors to the nearest `.git` boundary
|
|
442
|
+
* when `cwd` itself has no match — every command is usable from any
|
|
443
|
+
* subdirectory of a repo, not only its exact root (see
|
|
444
|
+
* `@theholocron/datapad`'s own doc comment for the stop conditions).
|
|
332
445
|
* Throws {@link ConfigFileError} when nothing is found or a file cannot
|
|
333
|
-
* be loaded, or `ConfigError` when the config
|
|
446
|
+
* be loaded, or `ConfigError` when the config fails schema checks.
|
|
334
447
|
*/
|
|
335
448
|
async function loadConfig(cwd) {
|
|
336
449
|
const found = await loadConfigFile({
|
|
337
450
|
cwd,
|
|
338
|
-
name: "holocron"
|
|
451
|
+
name: "holocron",
|
|
452
|
+
walkUp: true
|
|
339
453
|
});
|
|
340
454
|
if (!found) throw new ConfigFileError$1(`no holocron.config.{ts,js,mjs,cjs,json} found in ${cwd}. Create one — see the README for the schema.`);
|
|
341
455
|
return {
|
|
342
|
-
resolved: resolveConfig(await deriveDefaults(
|
|
456
|
+
resolved: resolveConfig(await deriveDefaults(found)),
|
|
343
457
|
filepath: found.filepath
|
|
344
458
|
};
|
|
345
459
|
}
|
|
346
|
-
async function deriveDefaults(
|
|
347
|
-
const
|
|
460
|
+
async function deriveDefaults(found) {
|
|
461
|
+
const configDir = dirname(found.filepath);
|
|
462
|
+
const result = { ...found.config };
|
|
348
463
|
if (!result.name) result.name = await readPackageJsonName(configDir) ?? basename(configDir);
|
|
349
464
|
if (result.repo && !result.repo.name) {
|
|
350
|
-
const
|
|
351
|
-
if (
|
|
465
|
+
const remote = await readGitRemote(configDir);
|
|
466
|
+
if (remote?.host === "github.com") result.repo = {
|
|
352
467
|
...result.repo,
|
|
353
|
-
name:
|
|
468
|
+
name: remote.slug
|
|
354
469
|
};
|
|
355
470
|
}
|
|
356
|
-
const manifest = await loadTasksConfig(configDir);
|
|
471
|
+
const manifest = await loadTasksConfig(configDir, { holocron: found });
|
|
357
472
|
if (manifest.tasks) result.tasks = manifest.tasks;
|
|
358
473
|
if (manifest.syncScripts !== void 0) result.syncScripts = manifest.syncScripts;
|
|
359
474
|
if (manifest.holocronScript !== void 0) result.holocronScript = manifest.holocronScript;
|
|
@@ -362,31 +477,8 @@ async function deriveDefaults(configDir, raw) {
|
|
|
362
477
|
return result;
|
|
363
478
|
}
|
|
364
479
|
async function readPackageJsonName(dir) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const pkg = JSON.parse(content);
|
|
368
|
-
return typeof pkg.name === "string" ? pkg.name.replace(/^@[^/]+\//, "") : void 0;
|
|
369
|
-
} catch {
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
async function readGitRemote(dir) {
|
|
374
|
-
try {
|
|
375
|
-
const { stdout } = await execFileAsync("git", [
|
|
376
|
-
"remote",
|
|
377
|
-
"get-url",
|
|
378
|
-
"origin"
|
|
379
|
-
], { cwd: dir });
|
|
380
|
-
return parseGitRemoteUrl(stdout.trim());
|
|
381
|
-
} catch {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
function parseGitRemoteUrl(url) {
|
|
386
|
-
const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
387
|
-
if (httpsMatch) return httpsMatch[1];
|
|
388
|
-
const sshMatch = url.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
389
|
-
if (sshMatch) return sshMatch[1];
|
|
480
|
+
const name = (await readPackageJson(dir))?.name;
|
|
481
|
+
return name === void 0 ? void 0 : name.replace(/^@[^/]+\//, "");
|
|
390
482
|
}
|
|
391
483
|
//#endregion
|
|
392
|
-
export { AuthError, CARDINALITY, ConfigError, ConfigFileError, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, compose, createFeatureResolver, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
|
|
484
|
+
export { AuthError, CARDINALITY, ConfigError, ConfigFileError, ProviderApiError, REQUIRED_CAPABILITIES, WebhookVerificationError, compose, createFeatureResolver, createResolveToken, createRestClient, defineConfig, deleteToken, deriveCapabilities, deriveCompliance, deriveProfile, deriveStack, getToken, isMulti, listStoredProviders, loadConfig, missingCapabilities, readWorkspacePackageJsons, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProviderApiError } from "@theholocron/http-client";
|
|
2
|
-
//#region src/plugin/capabilities.d.ts
|
|
2
|
+
//#region src/plugin/capabilities/capabilities.type.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Capability interfaces — the contracts that providers implement.
|
|
5
5
|
*
|
|
@@ -12,33 +12,8 @@ import { ProviderApiError } from "@theholocron/http-client";
|
|
|
12
12
|
* See `docs/wiki/specifications/tech-architecture.spec.md` for the design narrative
|
|
13
13
|
* (issue: #74).
|
|
14
14
|
*/
|
|
15
|
-
type CapabilityKey = "source" | "ci" | "secrets" | "environments" | "issues" | "deployment" | "storage" | "auth" | "vault" | "dns" | "tooling" | "notifications" | "analytics" | "errors" | "logs" | "wiki" | "workers";
|
|
15
|
+
type CapabilityKey = "source" | "ci" | "secrets" | "environments" | "issues" | "deployment" | "storage" | "auth" | "vault" | "dns" | "tooling" | "notifications" | "analytics" | "errors" | "logs" | "monitoring" | "wiki" | "workers";
|
|
16
16
|
type Cardinality = "single" | "many";
|
|
17
|
-
declare const CARDINALITY: {
|
|
18
|
-
readonly source: "single";
|
|
19
|
-
readonly ci: "single";
|
|
20
|
-
readonly secrets: "single";
|
|
21
|
-
readonly environments: "single";
|
|
22
|
-
readonly issues: "single";
|
|
23
|
-
readonly deployment: "single";
|
|
24
|
-
readonly storage: "single";
|
|
25
|
-
readonly auth: "single";
|
|
26
|
-
readonly vault: "single";
|
|
27
|
-
readonly dns: "single";
|
|
28
|
-
readonly tooling: "many";
|
|
29
|
-
readonly notifications: "many";
|
|
30
|
-
readonly analytics: "many";
|
|
31
|
-
readonly errors: "single";
|
|
32
|
-
readonly logs: "single";
|
|
33
|
-
readonly wiki: "single";
|
|
34
|
-
readonly workers: "single";
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* No capabilities are strictly required — repos without secrets (e.g. org
|
|
38
|
-
* community health repos) legitimately omit vault. Plugins validate their
|
|
39
|
-
* own requirements at call time.
|
|
40
|
-
*/
|
|
41
|
-
declare const REQUIRED_CAPABILITIES: readonly CapabilityKey[];
|
|
42
17
|
interface ProviderIdentity {
|
|
43
18
|
readonly key: CapabilityKey;
|
|
44
19
|
readonly providerName: string;
|
|
@@ -364,9 +339,35 @@ interface DeploymentRecord {
|
|
|
364
339
|
target?: DeploymentTrigger;
|
|
365
340
|
status: "queued" | "building" | "ready" | "error" | "cancelled";
|
|
366
341
|
createdAt?: string;
|
|
342
|
+
/** The provider's own reason when `status` is `"error"` (e.g. Vercel's `Command "npm install" exited with 1`). */
|
|
343
|
+
errorMessage?: string;
|
|
344
|
+
}
|
|
345
|
+
/** Config for `Deployment.deployFunction()` — source files, not a Git ref. */
|
|
346
|
+
interface DeployFunctionConfig {
|
|
347
|
+
/**
|
|
348
|
+
* Files to deploy, keyed by path relative to the project root (e.g.
|
|
349
|
+
* `"api/webhook.js"`, `"package.json"`). Content is plain text — the
|
|
350
|
+
* provider handles any wire-format encoding.
|
|
351
|
+
*/
|
|
352
|
+
files: Record<string, string>;
|
|
353
|
+
/**
|
|
354
|
+
* Named deployment target. Omit for a preview deployment — same
|
|
355
|
+
* semantics as `triggerDeployment`'s `target`.
|
|
356
|
+
*/
|
|
357
|
+
target?: DeploymentTrigger;
|
|
358
|
+
}
|
|
359
|
+
interface DeployFunctionResult {
|
|
360
|
+
deploymentId: string;
|
|
361
|
+
url: string;
|
|
367
362
|
}
|
|
368
363
|
interface Deployment extends ProviderIdentity {
|
|
369
364
|
readonly key: "deployment";
|
|
365
|
+
/**
|
|
366
|
+
* Custom domain declared in the deployment provider's configuration.
|
|
367
|
+
* `holocron repo setup` attaches it to the configured project and hands any
|
|
368
|
+
* returned verification record to the `dns` capability.
|
|
369
|
+
*/
|
|
370
|
+
readonly domain?: string;
|
|
370
371
|
listProjects(): Promise<DeploymentProject[]>;
|
|
371
372
|
/** Create if missing, otherwise return existing. Idempotent. */
|
|
372
373
|
ensureProject(input: {
|
|
@@ -390,6 +391,16 @@ interface Deployment extends ProviderIdentity {
|
|
|
390
391
|
target?: DeploymentTrigger;
|
|
391
392
|
}): Promise<DeploymentRecord>;
|
|
392
393
|
getDeployment(deploymentId: string): Promise<DeploymentRecord>;
|
|
394
|
+
/**
|
|
395
|
+
* Deploy (or update) a project directly from source files, bypassing
|
|
396
|
+
* Git entirely — the non-git counterpart to `triggerDeployment`, for a
|
|
397
|
+
* consumer with no repo to link (a webhook receiver shipped as an npm
|
|
398
|
+
* package, for instance, rather than deployed from its own Git
|
|
399
|
+
* history). `projectId` may be a project name for providers that
|
|
400
|
+
* accept either. Optional — providers without a files-based deploy API
|
|
401
|
+
* omit this (e.g. Cloudflare Pages, git-source only today).
|
|
402
|
+
*/
|
|
403
|
+
deployFunction?(projectId: string, config: DeployFunctionConfig): Promise<DeployFunctionResult>;
|
|
393
404
|
/**
|
|
394
405
|
* List preview deployments for a specific branch alias (e.g. "repo-pr-42").
|
|
395
406
|
* Optional — providers that don't support listing by branch omit this.
|
|
@@ -402,10 +413,18 @@ interface Deployment extends ProviderIdentity {
|
|
|
402
413
|
deletePreviewDeployments?(projectId: string, deploymentIds: string[]): Promise<number>;
|
|
403
414
|
/**
|
|
404
415
|
* Add a custom domain (or wildcard) to the project. Idempotent — no-op
|
|
405
|
-
* when the domain is already present. Optional: providers without a
|
|
406
|
-
* domain API omit this
|
|
416
|
+
* when the domain is already present. Optional: providers without a
|
|
417
|
+
* custom domain API omit this.
|
|
418
|
+
*
|
|
419
|
+
* Returns the DNS record needed to finish verification — `null` when
|
|
420
|
+
* the domain is already verified, or when the provider's API doesn't
|
|
421
|
+
* surface DNS-challenge details at all (e.g. Cloudflare Pages, whose
|
|
422
|
+
* custom-domain flow doesn't expose the same per-request verification
|
|
423
|
+
* handshake Vercel's does). Callers hand a non-null result straight
|
|
424
|
+
* to `dns.upsertRecord(result.zone, result.record)` — the exact
|
|
425
|
+
* pattern `setup` already uses for `Wiki.dnsRecord()`.
|
|
407
426
|
*/
|
|
408
|
-
ensureCustomDomain?(projectId: string, hostname: string): Promise<
|
|
427
|
+
ensureCustomDomain?(projectId: string, hostname: string): Promise<DnsRecordRequest | null>;
|
|
409
428
|
}
|
|
410
429
|
interface StorageBranch {
|
|
411
430
|
id: string;
|
|
@@ -517,9 +536,6 @@ interface ParseWebhookInput {
|
|
|
517
536
|
/** The signing secret the auth provider issued for this webhook endpoint. */
|
|
518
537
|
signingSecret: string;
|
|
519
538
|
}
|
|
520
|
-
declare class WebhookVerificationError extends Error {
|
|
521
|
-
name: string;
|
|
522
|
-
}
|
|
523
539
|
interface EnsureResult {
|
|
524
540
|
/** True when the resource already existed (idempotent no-op). */
|
|
525
541
|
alreadyExists: boolean;
|
|
@@ -573,11 +589,31 @@ interface DnsRecord {
|
|
|
573
589
|
priority?: number;
|
|
574
590
|
proxied?: boolean;
|
|
575
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* A DNS record a capability wants created, plus which zone it belongs
|
|
594
|
+
* to — the shape `Wiki.dnsRecord()` and `Deployment.ensureCustomDomain()`
|
|
595
|
+
* both hand off to `Dns.upsertRecord()`. Not tied to either capability;
|
|
596
|
+
* any provider that needs "here's a record, go create it" returns this.
|
|
597
|
+
*/
|
|
598
|
+
interface DnsRecordRequest {
|
|
599
|
+
/** Zone apex passed as the first argument to `dns.upsertRecord`. */
|
|
600
|
+
zone: string;
|
|
601
|
+
record: DnsRecord;
|
|
602
|
+
}
|
|
576
603
|
interface Dns extends ProviderIdentity {
|
|
577
604
|
readonly key: "dns";
|
|
578
605
|
listRecords(domain: string): Promise<DnsRecord[]>;
|
|
579
606
|
upsertRecord(domain: string, record: DnsRecord): Promise<DnsRecord>;
|
|
580
607
|
deleteRecord(domain: string, id: string): Promise<void>;
|
|
608
|
+
/**
|
|
609
|
+
* A `holocron doctor` smoke check with no specific domain to ask about —
|
|
610
|
+
* `listRecords` needs one, but doctor doesn't have one on hand. Verifies
|
|
611
|
+
* the token and reports how many zones it can see.
|
|
612
|
+
*/
|
|
613
|
+
whoami?(): Promise<{
|
|
614
|
+
ok: boolean;
|
|
615
|
+
zones: number;
|
|
616
|
+
}>;
|
|
581
617
|
}
|
|
582
618
|
interface Errors extends ProviderIdentity {
|
|
583
619
|
readonly key: "errors";
|
|
@@ -614,6 +650,74 @@ interface Logs extends ProviderIdentity {
|
|
|
614
650
|
alreadyExists: boolean;
|
|
615
651
|
}>;
|
|
616
652
|
}
|
|
653
|
+
type MonitoringAssertionSource = "status_code" | "response_time" | "body" | "header";
|
|
654
|
+
type MonitoringAssertionComparison = "equals" | "not_equals" | "contains" | "not_contains" | "greater_than" | "less_than";
|
|
655
|
+
interface MonitoringAssertion {
|
|
656
|
+
source: MonitoringAssertionSource;
|
|
657
|
+
comparison: MonitoringAssertionComparison;
|
|
658
|
+
/** Header name / JSON path — required when `source` needs one (e.g. `"header"`). */
|
|
659
|
+
property?: string;
|
|
660
|
+
target?: string;
|
|
661
|
+
}
|
|
662
|
+
type MonitoringHttpMethod = "GET" | "POST" | "PUT" | "HEAD" | "DELETE" | "PATCH";
|
|
663
|
+
interface MonitoringCheckInput {
|
|
664
|
+
/** Also the idempotency key — `ensureCheck` finds-or-creates by this name. */
|
|
665
|
+
name: string;
|
|
666
|
+
url: string;
|
|
667
|
+
method?: MonitoringHttpMethod;
|
|
668
|
+
/** Minutes between runs. Adapters round to their own supported values. */
|
|
669
|
+
frequency?: number;
|
|
670
|
+
assertions?: MonitoringAssertion[];
|
|
671
|
+
activated?: boolean;
|
|
672
|
+
/**
|
|
673
|
+
* Provider-specific region/datacenter ids to run the check from (e.g.
|
|
674
|
+
* Checkly's `"us-east-1"`). Required by some providers — there's no
|
|
675
|
+
* universal default; an account with none configured rejects a check
|
|
676
|
+
* that omits this entirely.
|
|
677
|
+
*/
|
|
678
|
+
locations?: string[];
|
|
679
|
+
/**
|
|
680
|
+
* Inverts pass/fail: set this when the check's expected-healthy
|
|
681
|
+
* response is itself an HTTP error status (e.g. asserting a 404/405
|
|
682
|
+
* on an endpoint that should reject certain requests). Without it,
|
|
683
|
+
* most providers classify any 4xx/5xx response as a failure at the
|
|
684
|
+
* platform level *before* evaluating `assertions` at all — a custom
|
|
685
|
+
* assertion that matches the "expected" error status does not
|
|
686
|
+
* override that baseline classification on its own.
|
|
687
|
+
*/
|
|
688
|
+
shouldFail?: boolean;
|
|
689
|
+
/**
|
|
690
|
+
* Raw request body to send with the check — e.g. simulating a webhook
|
|
691
|
+
* delivery to verify signature/auth checking is wired correctly
|
|
692
|
+
* end-to-end, rather than just confirming the endpoint is reachable.
|
|
693
|
+
* Omitted entirely by default (no body sent).
|
|
694
|
+
*/
|
|
695
|
+
body?: string;
|
|
696
|
+
}
|
|
697
|
+
interface MonitoringCheck {
|
|
698
|
+
id: string;
|
|
699
|
+
name: string;
|
|
700
|
+
}
|
|
701
|
+
interface Monitoring extends ProviderIdentity {
|
|
702
|
+
readonly key: "monitoring";
|
|
703
|
+
/**
|
|
704
|
+
* Checks this provider is configured to ensure exist — populated from
|
|
705
|
+
* the provider's own config (`holocron.config.ts`'s provider tuple),
|
|
706
|
+
* the same place `Deployment.domain` comes from. `holocron repo setup`
|
|
707
|
+
* loops this list and calls `ensureCheck` for each; empty when none
|
|
708
|
+
* are configured.
|
|
709
|
+
*/
|
|
710
|
+
readonly checks: readonly MonitoringCheckInput[];
|
|
711
|
+
/** Auth sanity-check. Throws ProviderApiError on auth failure. */
|
|
712
|
+
whoami?(): Promise<{
|
|
713
|
+
ok: boolean;
|
|
714
|
+
}>;
|
|
715
|
+
/** Idempotent find-or-create-or-update by `input.name`. */
|
|
716
|
+
ensureCheck(input: MonitoringCheckInput): Promise<{
|
|
717
|
+
check: MonitoringCheck;
|
|
718
|
+
alreadyExists: boolean;
|
|
719
|
+
}>;
|
|
720
|
+
}
|
|
617
721
|
interface ToolingDoctorReport {
|
|
618
722
|
ok: boolean;
|
|
619
723
|
message: string;
|
|
@@ -651,18 +755,6 @@ interface WikiProvisionOpts {
|
|
|
651
755
|
/** Project display name used in the wiki title (e.g. "Holocron"). */
|
|
652
756
|
name?: string;
|
|
653
757
|
}
|
|
654
|
-
/**
|
|
655
|
-
* DNS record that `setup` should create for a wiki custom domain.
|
|
656
|
-
* Returned by `Wiki.dnsRecord()` when a custom domain is configured.
|
|
657
|
-
*/
|
|
658
|
-
interface WikiDnsRecord {
|
|
659
|
-
/** Zone apex passed as the first argument to `dns.upsertRecord`. */
|
|
660
|
-
zone: string;
|
|
661
|
-
/** Full hostname for the CNAME (e.g. "wiki.theholocron.dev"). */
|
|
662
|
-
cname: string;
|
|
663
|
-
/** CNAME target (e.g. "holocron.docs.buildwithfern.com"). */
|
|
664
|
-
target: string;
|
|
665
|
-
}
|
|
666
758
|
/**
|
|
667
759
|
* Reverse-proxy configuration returned by `Wiki.proxyConfig()`.
|
|
668
760
|
* Used by `setup` to deploy a Worker that forwards traffic to the wiki
|
|
@@ -691,7 +783,7 @@ interface Wiki extends ProviderIdentity {
|
|
|
691
783
|
* Returns null when no custom domain is set.
|
|
692
784
|
* Called by `setup` to provision the CNAME via the `dns` capability.
|
|
693
785
|
*/
|
|
694
|
-
dnsRecord?():
|
|
786
|
+
dnsRecord?(): DnsRecordRequest | null;
|
|
695
787
|
/**
|
|
696
788
|
* Reverse-proxy config when the wiki provider requires a Worker-level
|
|
697
789
|
* proxy in addition to the CNAME. Returns null when no proxy is needed.
|
|
@@ -699,12 +791,34 @@ interface Wiki extends ProviderIdentity {
|
|
|
699
791
|
*/
|
|
700
792
|
proxyConfig?(): WikiProxyConfig | null;
|
|
701
793
|
}
|
|
794
|
+
/** Config for `Workers.deployScript()` — an arbitrary Worker, not a generated reverse-proxy. */
|
|
795
|
+
interface DeployScriptConfig {
|
|
796
|
+
/** The full Worker script source — a single ES module (`export default { fetch(...) {...} }`). */
|
|
797
|
+
code: string;
|
|
798
|
+
/**
|
|
799
|
+
* Secret bindings — `env.<name>` in the deployed Worker. Values are
|
|
800
|
+
* write-only; the provider never echoes them back, here or anywhere else.
|
|
801
|
+
*/
|
|
802
|
+
secrets?: Record<string, string>;
|
|
803
|
+
/**
|
|
804
|
+
* Route pattern(s) this Worker should serve (e.g.
|
|
805
|
+
* `"sentinel.theholocron.dev/*"`). Omit for a Worker reached only via
|
|
806
|
+
* its `*.workers.dev` URL.
|
|
807
|
+
*/
|
|
808
|
+
routes?: string[];
|
|
809
|
+
}
|
|
810
|
+
interface DeployScriptResult {
|
|
811
|
+
/** The deployed script's name at the provider. */
|
|
812
|
+
scriptName: string;
|
|
813
|
+
}
|
|
702
814
|
/**
|
|
703
|
-
* Edge Worker
|
|
815
|
+
* Edge Worker management capability.
|
|
704
816
|
*
|
|
705
|
-
* Deploys and manages Worker scripts
|
|
706
|
-
*
|
|
707
|
-
*
|
|
817
|
+
* Deploys and manages Worker scripts. `upsertProxy` is the existing,
|
|
818
|
+
* narrower case `setup` uses to wire wiki custom-domain reverse-proxies;
|
|
819
|
+
* `deployScript` is the general case — any Worker, any code, its own
|
|
820
|
+
* secrets — for a consumer that isn't a reverse-proxy (a webhook
|
|
821
|
+
* receiver, for instance).
|
|
708
822
|
*/
|
|
709
823
|
interface Workers extends ProviderIdentity {
|
|
710
824
|
readonly key: "workers";
|
|
@@ -714,6 +828,17 @@ interface Workers extends ProviderIdentity {
|
|
|
714
828
|
* `config.headers` on each request.
|
|
715
829
|
*/
|
|
716
830
|
upsertProxy(hostname: string, config: WikiProxyConfig): Promise<void>;
|
|
831
|
+
/** Deploy (or update) an arbitrary Worker script, optionally with secrets and routes. */
|
|
832
|
+
deployScript(name: string, config: DeployScriptConfig): Promise<DeployScriptResult>;
|
|
833
|
+
/**
|
|
834
|
+
* A `holocron doctor` smoke check — every other method either mutates or
|
|
835
|
+
* needs a specific script/zone doctor has no way to supply up front.
|
|
836
|
+
* Reports how many Worker scripts the token can see.
|
|
837
|
+
*/
|
|
838
|
+
whoami?(): Promise<{
|
|
839
|
+
ok: boolean;
|
|
840
|
+
scripts: number;
|
|
841
|
+
}>;
|
|
717
842
|
}
|
|
718
843
|
interface CapabilityImpls {
|
|
719
844
|
source: Source;
|
|
@@ -731,12 +856,44 @@ interface CapabilityImpls {
|
|
|
731
856
|
analytics: Analytics;
|
|
732
857
|
errors: Errors;
|
|
733
858
|
logs: Logs;
|
|
859
|
+
monitoring: Monitoring;
|
|
734
860
|
wiki: Wiki;
|
|
735
861
|
workers: Workers;
|
|
736
862
|
}
|
|
737
863
|
type CardinalityFor<K extends CapabilityKey> = (typeof CARDINALITY)[K];
|
|
738
864
|
/** Resolved runtime shape: single → one impl; many → array. */
|
|
739
865
|
type ResolvedCapability<K extends CapabilityKey> = CardinalityFor<K> extends "many" ? CapabilityImpls[K][] : CapabilityImpls[K];
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region src/plugin/capabilities/capabilities.d.ts
|
|
868
|
+
declare const CARDINALITY: {
|
|
869
|
+
readonly source: "single";
|
|
870
|
+
readonly ci: "single";
|
|
871
|
+
readonly secrets: "single";
|
|
872
|
+
readonly environments: "single";
|
|
873
|
+
readonly issues: "single";
|
|
874
|
+
readonly deployment: "single";
|
|
875
|
+
readonly storage: "single";
|
|
876
|
+
readonly auth: "single";
|
|
877
|
+
readonly vault: "single";
|
|
878
|
+
readonly dns: "single";
|
|
879
|
+
readonly tooling: "many";
|
|
880
|
+
readonly notifications: "many";
|
|
881
|
+
readonly analytics: "many";
|
|
882
|
+
readonly errors: "single";
|
|
883
|
+
readonly logs: "single";
|
|
884
|
+
readonly monitoring: "single";
|
|
885
|
+
readonly wiki: "single";
|
|
886
|
+
readonly workers: "single";
|
|
887
|
+
};
|
|
888
|
+
/**
|
|
889
|
+
* No capabilities are strictly required — repos without secrets (e.g. org
|
|
890
|
+
* community health repos) legitimately omit vault. Plugins validate their
|
|
891
|
+
* own requirements at call time.
|
|
892
|
+
*/
|
|
893
|
+
declare const REQUIRED_CAPABILITIES: readonly CapabilityKey[];
|
|
894
|
+
declare class WebhookVerificationError extends Error {
|
|
895
|
+
name: string;
|
|
896
|
+
}
|
|
740
897
|
declare function isMulti<K extends CapabilityKey>(key: K): CardinalityFor<K> extends "many" ? true : false;
|
|
741
898
|
//#endregion
|
|
742
|
-
export { 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, Errors, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, Logs, NormalizedAuthUser, Notifications, PagesConfig, ParseWebhookInput, ProviderApiError, ProviderIdentity, PullRequest, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, Wiki,
|
|
899
|
+
export { Analytics, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConnectionStringOptions, CreateAuthUserInput, DeployFunctionConfig, DeployFunctionResult, DeployScriptConfig, DeployScriptResult, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordRequest, DnsRecordType, EnsureResult, Environment, EnvironmentReviewer, Environments, Errors, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, Logs, Monitoring, MonitoringAssertion, MonitoringAssertionComparison, MonitoringAssertionSource, MonitoringCheck, MonitoringCheckInput, MonitoringHttpMethod, NormalizedAuthUser, Notifications, PagesConfig, ParseWebhookInput, ProviderApiError, ProviderIdentity, PullRequest, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, Wiki, WikiProvisionOpts, WikiProxyConfig, Workers, isMulti };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProviderApiError } from "@theholocron/http-client";
|
|
2
|
-
//#region src/plugin/capabilities.ts
|
|
2
|
+
//#region src/plugin/capabilities/capabilities.ts
|
|
3
3
|
const CARDINALITY = {
|
|
4
4
|
source: "single",
|
|
5
5
|
ci: "single",
|
|
@@ -16,6 +16,7 @@ const CARDINALITY = {
|
|
|
16
16
|
analytics: "many",
|
|
17
17
|
errors: "single",
|
|
18
18
|
logs: "single",
|
|
19
|
+
monitoring: "single",
|
|
19
20
|
wiki: "single",
|
|
20
21
|
workers: "single"
|
|
21
22
|
};
|