blogwright-core 0.3.2 → 0.4.0-beta.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 +1 -1
- package/dist/adapters/memory-fs.js +0 -1
- package/dist/adapters/node-fs.js +0 -1
- package/dist/adapters/node-terminal.d.ts +2 -2
- package/dist/adapters/node-terminal.js +1 -2
- package/dist/adapters/script-terminal.js +0 -1
- package/dist/aws/acm.js +0 -1
- package/dist/aws/cloudfront.d.ts +4 -4
- package/dist/aws/cloudfront.js +7 -10
- package/dist/aws/credentials.d.ts +1 -1
- package/dist/aws/credentials.js +1 -2
- package/dist/aws/endpoint.d.ts +22 -2
- package/dist/aws/endpoint.js +21 -3
- package/dist/aws/errors.js +1 -2
- package/dist/aws/form.js +0 -1
- package/dist/aws/iam.d.ts +1 -1
- package/dist/aws/iam.js +1 -2
- package/dist/aws/logs.d.ts +24 -4
- package/dist/aws/logs.js +19 -10
- package/dist/aws/microvms.d.ts +1 -1
- package/dist/aws/microvms.js +2 -3
- package/dist/aws/route53.d.ts +1 -1
- package/dist/aws/route53.js +2 -3
- package/dist/aws/s3.js +4 -2
- package/dist/aws/secretsmanager.js +0 -1
- package/dist/aws/signer.d.ts +2 -2
- package/dist/aws/signer.js +7 -7
- package/dist/aws/sts.d.ts +1 -1
- package/dist/aws/sts.js +1 -2
- package/dist/aws/xml.d.ts +2 -2
- package/dist/aws/xml.js +2 -3
- package/dist/clients.d.ts +11 -1
- package/dist/clients.js +3 -2
- package/dist/colors.js +1 -2
- package/dist/config.d.ts +47 -4
- package/dist/config.js +29 -29
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +360 -0
- package/dist/plugin.js +85 -0
- package/dist/ports.js +0 -1
- package/dist/repo-root.d.ts +3 -0
- package/dist/repo-root.js +7 -2
- package/dist/state.d.ts +24 -3
- package/dist/state.js +38 -12
- package/dist/tags.js +0 -1
- package/dist/util.d.ts +23 -0
- package/dist/util.js +23 -1
- package/package.json +1 -1
- package/dist/adapters/memory-fs.js.map +0 -1
- package/dist/adapters/node-fs.js.map +0 -1
- package/dist/adapters/node-terminal.js.map +0 -1
- package/dist/adapters/script-terminal.js.map +0 -1
- package/dist/aws/acm.js.map +0 -1
- package/dist/aws/cloudfront.js.map +0 -1
- package/dist/aws/credentials.js.map +0 -1
- package/dist/aws/endpoint.js.map +0 -1
- package/dist/aws/errors.js.map +0 -1
- package/dist/aws/form.js.map +0 -1
- package/dist/aws/iam.js.map +0 -1
- package/dist/aws/logs.js.map +0 -1
- package/dist/aws/microvms.js.map +0 -1
- package/dist/aws/route53.js.map +0 -1
- package/dist/aws/s3.js.map +0 -1
- package/dist/aws/secretsmanager.js.map +0 -1
- package/dist/aws/signer.js.map +0 -1
- package/dist/aws/sts.js.map +0 -1
- package/dist/aws/xml.js.map +0 -1
- package/dist/clients.js.map +0 -1
- package/dist/colors.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ports.js.map +0 -1
- package/dist/repo-root.js.map +0 -1
- package/dist/state.js.map +0 -1
- package/dist/tags.js.map +0 -1
- package/dist/util.js.map +0 -1
package/dist/aws/xml.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal XML helpers for the handful of AWS REST-XML / query responses this CLI
|
|
3
|
-
* reads (S3, STS, IAM, CloudFront). Not a general XML parser
|
|
3
|
+
* reads (S3, STS, IAM, CloudFront). Not a general XML parser - it extracts tag
|
|
4
4
|
* contents by name, which is sufficient for the flat response shapes we consume.
|
|
5
5
|
*/
|
|
6
6
|
const ENTITIES = {
|
|
@@ -41,7 +41,7 @@ export function textTag(xml, name) {
|
|
|
41
41
|
return inner === undefined ? undefined : decodeEntities(inner.trim());
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
* Like {@link textTag} but verbatim
|
|
44
|
+
* Like {@link textTag} but verbatim - no trimming. For values where whitespace
|
|
45
45
|
* is data, like S3 object keys (a trimmed key would target a different object).
|
|
46
46
|
*/
|
|
47
47
|
export function rawTextTag(xml, name) {
|
|
@@ -62,4 +62,3 @@ export function allTags(xml, name) {
|
|
|
62
62
|
function escapeName(name) {
|
|
63
63
|
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
64
|
}
|
|
65
|
-
//# sourceMappingURL=xml.js.map
|
package/dist/clients.d.ts
CHANGED
|
@@ -19,12 +19,22 @@ export interface ClientBundleOptions {
|
|
|
19
19
|
export interface AwsClients {
|
|
20
20
|
region: string;
|
|
21
21
|
signing: SigningClient;
|
|
22
|
+
/**
|
|
23
|
+
* The us-east-1 signer, exposed so a plugin can build clients for services
|
|
24
|
+
* core does not enumerate over the credentials, endpoint override and
|
|
25
|
+
* transport the host already resolved - a `SigningClient`'s region is fixed
|
|
26
|
+
* at construction and both of those are private, so a hand-built one would
|
|
27
|
+
* re-resolve credentials and ignore a transport a test injected. Core builds
|
|
28
|
+
* no additional client from it: `logsUsEast1`, `acm`, `cloudfront` and
|
|
29
|
+
* `route53` below are the only ones it signs for.
|
|
30
|
+
*/
|
|
31
|
+
signingUsEast1: SigningClient;
|
|
22
32
|
s3: S3Client;
|
|
23
33
|
sts: StsClient;
|
|
24
34
|
iam: IamClient;
|
|
25
35
|
logs: LogsClient;
|
|
26
36
|
/**
|
|
27
|
-
* Logs client pinned to us-east-1 for CloudFront vended log delivery
|
|
37
|
+
* Logs client pinned to us-east-1 for CloudFront vended log delivery -
|
|
28
38
|
* PutDeliverySource with the CloudFront LogType exists only there, the same
|
|
29
39
|
* global-service quirk as CloudFront's ACM certificates.
|
|
30
40
|
*/
|
package/dist/clients.js
CHANGED
|
@@ -19,11 +19,13 @@ export function createClients(opts) {
|
|
|
19
19
|
...(opts.transport ? { transport: opts.transport } : {}),
|
|
20
20
|
};
|
|
21
21
|
const signing = new SigningClient({ region: opts.region, ...base });
|
|
22
|
-
// ACM for CloudFront must be us-east-1 regardless of the primary region
|
|
22
|
+
// ACM for CloudFront must be us-east-1 regardless of the primary region; the
|
|
23
|
+
// same signer is handed out as `signingUsEast1` for plugin-supplied services.
|
|
23
24
|
const usEast1 = new SigningClient({ region: 'us-east-1', ...base });
|
|
24
25
|
return {
|
|
25
26
|
region: opts.region,
|
|
26
27
|
signing,
|
|
28
|
+
signingUsEast1: usEast1,
|
|
27
29
|
s3: new S3Client(signing),
|
|
28
30
|
sts: new StsClient(signing),
|
|
29
31
|
iam: new IamClient(signing),
|
|
@@ -36,4 +38,3 @@ export function createClients(opts) {
|
|
|
36
38
|
secrets: new SecretsManagerClient(signing),
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
|
-
//# sourceMappingURL=clients.js.map
|
package/dist/colors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* ANSI colour helpers shared by the CLI and feature packages. Pure string
|
|
3
|
-
* composition
|
|
3
|
+
* composition - writing (and stripping for non-interactive sessions) is the
|
|
4
4
|
* logger's job.
|
|
5
5
|
*/
|
|
6
6
|
const ESCAPE = '\u001B';
|
|
@@ -21,4 +21,3 @@ const COLOR_CODES = new RegExp(`${ESCAPE}\\[[0-9;]*m`, 'g');
|
|
|
21
21
|
export function stripColors(text) {
|
|
22
22
|
return text.replace(COLOR_CODES, '');
|
|
23
23
|
}
|
|
24
|
-
//# sourceMappingURL=colors.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -23,7 +23,17 @@ export interface SeoConfig {
|
|
|
23
23
|
*/
|
|
24
24
|
sitemap: 'auto' | 'on' | 'off';
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* AT Protocol / standard.site publishing (see `blogwright pds`). Inert when
|
|
28
|
+
* absent.
|
|
29
|
+
*
|
|
30
|
+
* A *shape*, not a contract core enforces: `blogwright-pds` owns this key end
|
|
31
|
+
* to end - it validates the block (`validatePdsConfig`) and derives the
|
|
32
|
+
* `<siteName>/atproto` default `secretName` (`resolvePdsSecretName`). Core
|
|
33
|
+
* parses the block through untouched. The type stays here only because
|
|
34
|
+
* `OpsConfig.pds` is still declared, which is what lets the CLI's deploy-role
|
|
35
|
+
* statement read it until that statement moves to the plugin.
|
|
36
|
+
*/
|
|
27
37
|
export interface PdsConfig {
|
|
28
38
|
/** Publication display name (site.standard.publication `name`). */
|
|
29
39
|
name: string;
|
|
@@ -35,8 +45,12 @@ export interface PdsConfig {
|
|
|
35
45
|
* the DID document during OAuth.
|
|
36
46
|
*/
|
|
37
47
|
handleResolver?: string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Secrets Manager secret holding the OAuth client key + session. Optional
|
|
50
|
+
* because core no longer defaults it: `blogwright-pds` resolves an absent
|
|
51
|
+
* one to `<siteName>/atproto`.
|
|
52
|
+
*/
|
|
53
|
+
secretName?: string | undefined;
|
|
40
54
|
}
|
|
41
55
|
/**
|
|
42
56
|
* Repo-relative paths the pds commands read/write in the consuming site.
|
|
@@ -85,7 +99,7 @@ export interface OpsConfig {
|
|
|
85
99
|
/** Extra path prefixes excluded from the source zip (on top of .gitignore). */
|
|
86
100
|
sourceIgnore: string[];
|
|
87
101
|
/**
|
|
88
|
-
* Paths zipped into the deploy source even when gitignored
|
|
102
|
+
* Paths zipped into the deploy source even when gitignored - for artifacts a
|
|
89
103
|
* pre-deploy step builds outside the MicroVM (a wasm bundle, generated
|
|
90
104
|
* assets). Each entry must exist and be non-empty at deploy time, so a
|
|
91
105
|
* forgotten pre-build fails fast instead of shipping a broken site.
|
|
@@ -121,8 +135,27 @@ export declare function stripJsonComments(input: string): string;
|
|
|
121
135
|
* JSON.parse does not. Run after comment stripping.
|
|
122
136
|
*/
|
|
123
137
|
export declare function stripTrailingCommas(input: string): string;
|
|
138
|
+
/**
|
|
139
|
+
* Parse a JSONC config document into both its validated `OpsConfig` and the
|
|
140
|
+
* raw parsed document. `raw` carries every top-level property the file has,
|
|
141
|
+
* including a plugin's own key - `OpsConfig` has no index signature to read
|
|
142
|
+
* one off `config`, so a plugin's block survives the parse only here.
|
|
143
|
+
* `parseConfig` keeps its existing signature as this function's `config`
|
|
144
|
+
* half, so no existing caller changes.
|
|
145
|
+
*/
|
|
146
|
+
export declare function parseConfigDocument(text: string): {
|
|
147
|
+
config: OpsConfig;
|
|
148
|
+
raw: Readonly<Record<string, unknown>>;
|
|
149
|
+
};
|
|
124
150
|
/** Parse + validate a JSONC config document, merged over defaults. */
|
|
125
151
|
export declare function parseConfig(text: string): OpsConfig;
|
|
152
|
+
/**
|
|
153
|
+
* Read a plugin's own block out of a config document's raw form - the
|
|
154
|
+
* `unknown` the CLI hands straight to that plugin's `validateConfig`. Not a
|
|
155
|
+
* function a plugin calls itself; only the CLI's dispatch path reads a
|
|
156
|
+
* block this way, over the `raw` half {@link parseConfigDocument} returns.
|
|
157
|
+
*/
|
|
158
|
+
export declare function pluginBlock(raw: Readonly<Record<string, unknown>>, key: string): unknown;
|
|
126
159
|
export declare function mergeConfig(raw: Partial<OpsConfig>): OpsConfig;
|
|
127
160
|
export interface Names {
|
|
128
161
|
env: string;
|
|
@@ -130,6 +163,16 @@ export interface Names {
|
|
|
130
163
|
prefix: string;
|
|
131
164
|
buildRole: string;
|
|
132
165
|
execRole: string;
|
|
166
|
+
/**
|
|
167
|
+
* The GitHub Actions OIDC deploy role. Derived here rather than inside the
|
|
168
|
+
* CLI's own `githubOidcRoleNode` because it has two readers: that node, and
|
|
169
|
+
* `blogwright-pds`, whose resource node attaches its own named inline policy
|
|
170
|
+
* to the same role (`packages/pds/src/nodes.ts`). Only the role's ARN reaches
|
|
171
|
+
* the site's state, so a plugin cannot recover the name from there - and a
|
|
172
|
+
* second, private derivation is exactly the duplication DEVELOPMENT.md
|
|
173
|
+
* §Limits and bounds bans for a derived AWS name.
|
|
174
|
+
*/
|
|
175
|
+
githubRole: string;
|
|
133
176
|
microvmImage: string;
|
|
134
177
|
microvmLogGroup: string;
|
|
135
178
|
cloudfrontLogGroup: string;
|
package/dist/config.js
CHANGED
|
@@ -120,14 +120,35 @@ export function stripTrailingCommas(input) {
|
|
|
120
120
|
}
|
|
121
121
|
return out;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse a JSONC config document into both its validated `OpsConfig` and the
|
|
125
|
+
* raw parsed document. `raw` carries every top-level property the file has,
|
|
126
|
+
* including a plugin's own key - `OpsConfig` has no index signature to read
|
|
127
|
+
* one off `config`, so a plugin's block survives the parse only here.
|
|
128
|
+
* `parseConfig` keeps its existing signature as this function's `config`
|
|
129
|
+
* half, so no existing caller changes.
|
|
130
|
+
*/
|
|
131
|
+
export function parseConfigDocument(text) {
|
|
132
|
+
const raw = JSON.parse(stripTrailingCommas(stripJsonComments(text)));
|
|
133
|
+
const config = mergeConfig(raw);
|
|
134
|
+
return { config, raw };
|
|
135
|
+
}
|
|
123
136
|
/** Parse + validate a JSONC config document, merged over defaults. */
|
|
124
137
|
export function parseConfig(text) {
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
return parseConfigDocument(text).config;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read a plugin's own block out of a config document's raw form - the
|
|
142
|
+
* `unknown` the CLI hands straight to that plugin's `validateConfig`. Not a
|
|
143
|
+
* function a plugin calls itself; only the CLI's dispatch path reads a
|
|
144
|
+
* block this way, over the `raw` half {@link parseConfigDocument} returns.
|
|
145
|
+
*/
|
|
146
|
+
export function pluginBlock(raw, key) {
|
|
147
|
+
return raw[key];
|
|
127
148
|
}
|
|
128
149
|
export function mergeConfig(raw) {
|
|
129
150
|
if (!raw.siteName) {
|
|
130
|
-
throw new Error('config.siteName is required
|
|
151
|
+
throw new Error('config.siteName is required - a stable lowercase slug used in every derived AWS resource name');
|
|
131
152
|
}
|
|
132
153
|
const cfg = {
|
|
133
154
|
...DEFAULT_CONFIG,
|
|
@@ -142,12 +163,6 @@ export function mergeConfig(raw) {
|
|
|
142
163
|
seo: { ...DEFAULT_CONFIG.seo, ...raw.seo },
|
|
143
164
|
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
144
165
|
};
|
|
145
|
-
if (raw.pds) {
|
|
146
|
-
cfg.pds = {
|
|
147
|
-
...raw.pds,
|
|
148
|
-
secretName: raw.pds.secretName ?? `${cfg.siteName}/atproto`,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
166
|
validateConfig(cfg);
|
|
152
167
|
return cfg;
|
|
153
168
|
}
|
|
@@ -188,25 +203,10 @@ function validateConfig(cfg) {
|
|
|
188
203
|
throw new Error(`config.paths.${key} must be repo-relative without "..", got "${value}"`);
|
|
189
204
|
}
|
|
190
205
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let resolver;
|
|
196
|
-
try {
|
|
197
|
-
resolver = new URL(cfg.pds.handleResolver);
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
throw new Error(`config.pds.handleResolver must be a URL, got "${cfg.pds.handleResolver}"`);
|
|
201
|
-
}
|
|
202
|
-
if (resolver.protocol !== 'https:') {
|
|
203
|
-
throw new Error(`config.pds.handleResolver must be https, got "${cfg.pds.handleResolver}"`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (!/^[\w/+=.@-]+$/.test(cfg.pds.secretName)) {
|
|
207
|
-
throw new Error(`config.pds.secretName has invalid characters: "${cfg.pds.secretName}"`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
206
|
+
// No plugin-block branch here, by design: a plugin's block is that plugin's
|
|
207
|
+
// own to judge. Core validates only the keys it declares, so an unknown - or
|
|
208
|
+
// malformed - plugin block parses through untouched and is checked by that
|
|
209
|
+
// plugin's `validateConfig`, off `parseConfigDocument`'s raw half.
|
|
210
210
|
}
|
|
211
211
|
/** Derive deterministic, environment-prefixed resource names. */
|
|
212
212
|
export function deriveNames(env, accountId, cfg) {
|
|
@@ -225,6 +225,7 @@ export function deriveNames(env, accountId, cfg) {
|
|
|
225
225
|
prefix,
|
|
226
226
|
buildRole: `${prefix}-build-role`,
|
|
227
227
|
execRole: `${prefix}-exec-role`,
|
|
228
|
+
githubRole: `${prefix}-gh`,
|
|
228
229
|
microvmImage,
|
|
229
230
|
microvmLogGroup: `/aws/lambda/microvms/${microvmImage}`,
|
|
230
231
|
cloudfrontLogGroup: `/${cfg.siteName}/${env}/cloudfront`,
|
|
@@ -233,4 +234,3 @@ export function deriveNames(env, accountId, cfg) {
|
|
|
233
234
|
deliveryDestination: `${prefix}-cf-dest`,
|
|
234
235
|
};
|
|
235
236
|
}
|
|
236
|
-
//# sourceMappingURL=config.js.map
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -20,9 +20,9 @@ export * from './aws/xml.js';
|
|
|
20
20
|
export * from './clients.js';
|
|
21
21
|
export * from './colors.js';
|
|
22
22
|
export * from './config.js';
|
|
23
|
+
export * from './plugin.js';
|
|
23
24
|
export * from './ports.js';
|
|
24
25
|
export * from './repo-root.js';
|
|
25
26
|
export * from './state.js';
|
|
26
27
|
export * from './tags.js';
|
|
27
28
|
export * from './util.js';
|
|
28
|
-
//# sourceMappingURL=index.js.map
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin service-provider interface (SPI) vocabulary. A plugin is a
|
|
3
|
+
* package that depends on `blogwright-core` and never on the CLI - it never
|
|
4
|
+
* imports from `blogwright` (the CLI package), only from this module and the
|
|
5
|
+
* rest of core. The CLI's own `OpsContext` (`packages/cli/src/context.ts`) is
|
|
6
|
+
* deliberately wider than what a plugin is handed: it carries CLI-private
|
|
7
|
+
* concerns - `agentDir` (the build-agent artifact directory) and the full
|
|
8
|
+
* `Ports` (`vcs`, `ping`, and any port a later CLI feature adds) - that a
|
|
9
|
+
* plugin must never see, because seeing them would let a plugin depend on
|
|
10
|
+
* types this module cannot even name. `PluginContext<TConfig>` below is the
|
|
11
|
+
* narrow slice the CLI carves out of `OpsContext` at the dispatch boundary
|
|
12
|
+
* for a plugin command to run against.
|
|
13
|
+
*/
|
|
14
|
+
import type { AwsClients } from './clients.js';
|
|
15
|
+
import type { Names, OpsConfig } from './config.js';
|
|
16
|
+
import type { FileSystem, Terminal } from './ports.js';
|
|
17
|
+
import type { OpsState, ResourceOutputs, StateStore } from './state.js';
|
|
18
|
+
/**
|
|
19
|
+
* Leveled logger surface a plugin command reports through. Restates the CLI's
|
|
20
|
+
* `Logger` (`packages/cli/src/logger.ts`) and pds's `PdsLogger`
|
|
21
|
+
* (`packages/pds/src/context.ts`) so this module never imports either.
|
|
22
|
+
*/
|
|
23
|
+
export interface PluginLogger {
|
|
24
|
+
info(msg: string): void;
|
|
25
|
+
step(msg: string): void;
|
|
26
|
+
ok(msg: string): void;
|
|
27
|
+
warn(msg: string): void;
|
|
28
|
+
error(msg: string): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The two ports core owns that a plugin may cross: repo files and the
|
|
32
|
+
* operator's terminal. Deliberately narrower than the CLI's six-member
|
|
33
|
+
* `Ports` (`packages/cli/src/ports.ts`) - `vcs` and `ping` are CLI-private
|
|
34
|
+
* ports declared in the CLI, and any port a later CLI feature adds joins
|
|
35
|
+
* them - so a `PluginContext` declared in core cannot name any of those
|
|
36
|
+
* types. A separate declaration from the CLI's `Ports`, rather than a
|
|
37
|
+
* `Pick`, keeps this module free of any import from the CLI.
|
|
38
|
+
*/
|
|
39
|
+
export interface PluginPorts {
|
|
40
|
+
fs: FileSystem;
|
|
41
|
+
terminal: Terminal;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A **read-only** view of the site's recorded outputs (`state/<env>.json`).
|
|
45
|
+
* A plugin reads it to find resources the site owns - the analytics
|
|
46
|
+
* log-delivery node reads the site's CloudFront distribution ARN through it
|
|
47
|
+
* - but can never write through it: every property is `readonly`, all the
|
|
48
|
+
* way into the map values, so `ctx.siteState.resources['x'] = {}` does not
|
|
49
|
+
* typecheck. This is one of the two state surfaces a plugin sees (see
|
|
50
|
+
* {@link PluginContext.siteState}); the plugin's own, writable state is
|
|
51
|
+
* `PluginContext.state`. Any `OpsState` (`state.ts`) satisfies this type
|
|
52
|
+
* structurally - a mutable property is assignable to a `readonly` one of the
|
|
53
|
+
* same type - so the CLI needs no wrapper object to hand its site state to a
|
|
54
|
+
* plugin this way.
|
|
55
|
+
*/
|
|
56
|
+
export interface SiteState {
|
|
57
|
+
readonly resources: Readonly<Record<string, Readonly<ResourceOutputs>>>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The narrow slice of the host CLI's context a plugin command receives,
|
|
61
|
+
* generic over `TConfig` - the shape of the config block the plugin owns
|
|
62
|
+
* (the block its own `validateConfig` returned). It names exactly sixteen
|
|
63
|
+
* members and nothing else; the enumeration is exhaustive on purpose,
|
|
64
|
+
* because plugins are written against this type before they exercise it. A
|
|
65
|
+
* field left out here surfaces as a compile error many features downstream,
|
|
66
|
+
* not a runtime one.
|
|
67
|
+
*
|
|
68
|
+
* Every member is required except {@link PluginContext.tags}.
|
|
69
|
+
*/
|
|
70
|
+
export interface PluginContext<TConfig = never> {
|
|
71
|
+
/** The environment name (e.g. `production`), the same value `names` was derived from. */
|
|
72
|
+
env: string;
|
|
73
|
+
/** The site's domain, when the site has one configured. */
|
|
74
|
+
domain: string | undefined;
|
|
75
|
+
/** True for the shared preview stack (host-routed, per-PR prefixes). */
|
|
76
|
+
preview: boolean;
|
|
77
|
+
/** The full, validated site config. A plugin's own block is `pluginConfig`, not a key read off this - `OpsConfig` has no index signature to read a plugin's key from. */
|
|
78
|
+
config: OpsConfig;
|
|
79
|
+
/**
|
|
80
|
+
* The plugin's own config block, as returned by its `validateConfig`. A
|
|
81
|
+
* plugin reads its settings from here, never from `ctx.config`: `OpsConfig`
|
|
82
|
+
* (`config.ts`) has no index signature, so `ctx.config.<pluginKey>` does
|
|
83
|
+
* not compile, and casting or widening to `any` around that is banned by
|
|
84
|
+
* DEVELOPMENT.md §Code style. Returning the validated block also gives a
|
|
85
|
+
* plugin's own defaults somewhere to live, so every reader keeps a total
|
|
86
|
+
* type instead of re-checking for `undefined`.
|
|
87
|
+
*/
|
|
88
|
+
pluginConfig: TConfig;
|
|
89
|
+
/**
|
|
90
|
+
* Deterministic, environment-prefixed resource names the site derived.
|
|
91
|
+
* `names.deliverySource` is load-bearing for the analytics plugin: it hangs
|
|
92
|
+
* its CloudFront log delivery off the source name the site already owns,
|
|
93
|
+
* rather than deriving a second one.
|
|
94
|
+
*/
|
|
95
|
+
names: Names;
|
|
96
|
+
/**
|
|
97
|
+
* The AWS account id the site's resources live in. Both the pds and
|
|
98
|
+
* analytics plugins build IAM resource ARNs from it directly (for example
|
|
99
|
+
* `arn:aws:secretsmanager:<region>:<accountId>:secret:<name>-*`).
|
|
100
|
+
*/
|
|
101
|
+
accountId: string;
|
|
102
|
+
/** The signed AWS service clients the host built, shared with every node this context reaches. */
|
|
103
|
+
clients: AwsClients;
|
|
104
|
+
/** The two ports core owns. See {@link PluginPorts} for what stays CLI-side. */
|
|
105
|
+
ports: PluginPorts;
|
|
106
|
+
/**
|
|
107
|
+
* Tags applied to every AWS resource this stack creates. Optional - not
|
|
108
|
+
* merely possibly-empty - under `exactOptionalPropertyTypes`, because
|
|
109
|
+
* `PdsContext` (`packages/pds/src/context.ts`) already declares it this
|
|
110
|
+
* way and `createTestContext` (`packages/pds/src/test-support.ts`) builds
|
|
111
|
+
* a complete feature context without one; a required `tags` here would
|
|
112
|
+
* propagate through the pds migration's `Pick` and break every existing
|
|
113
|
+
* pds test.
|
|
114
|
+
*/
|
|
115
|
+
tags?: Record<string, string> | undefined;
|
|
116
|
+
/** Leveled logger a plugin command reports progress, successes, and failures through. */
|
|
117
|
+
logger: PluginLogger;
|
|
118
|
+
/**
|
|
119
|
+
* The plugin's own scoped `StateStore` (`state/<env>.<plugin>.json`), the
|
|
120
|
+
* store `state`/`save()` persist through. A plugin node must never call
|
|
121
|
+
* `store.save()` itself - see {@link PluginContext.record}.
|
|
122
|
+
*/
|
|
123
|
+
store: StateStore;
|
|
124
|
+
/**
|
|
125
|
+
* The plugin's **own** state, loaded from its scoped `store`. Typed as
|
|
126
|
+
* core's `OpsState` - not a bare outputs map - because the engine reaches
|
|
127
|
+
* through it: `destroyGraph` does `delete ctx.state.resources[node.id]`
|
|
128
|
+
* (`packages/cli/src/graph.ts`). This is the only state a plugin may
|
|
129
|
+
* write, and only through {@link PluginContext.record}; contrast with the
|
|
130
|
+
* read-only {@link PluginContext.siteState}.
|
|
131
|
+
*/
|
|
132
|
+
state: OpsState;
|
|
133
|
+
/**
|
|
134
|
+
* The read-only view of the site's own recorded outputs. See
|
|
135
|
+
* {@link SiteState}.
|
|
136
|
+
*/
|
|
137
|
+
siteState: SiteState;
|
|
138
|
+
/**
|
|
139
|
+
* Record a resource node's outputs (ARNs, ids, domains) under `nodeId` in
|
|
140
|
+
* the plugin's own `state`. This is the *only* way a plugin's resource
|
|
141
|
+
* nodes may record outputs - it mirrors what the CLI's own nodes do through
|
|
142
|
+
* the private `output(ctx, id)` helper (`packages/cli/src/nodes.ts`),
|
|
143
|
+
* but a plugin node must never call `store.save()` directly: the engine
|
|
144
|
+
* (`applyGraph`) saves the in-memory state after every node, and a node's
|
|
145
|
+
* own direct write would be clobbered by that next save.
|
|
146
|
+
*/
|
|
147
|
+
record(nodeId: string, outputs: ResourceOutputs): void;
|
|
148
|
+
/**
|
|
149
|
+
* Persist the plugin's own working state through `store`. `applyGraph`
|
|
150
|
+
* calls this after every node (`packages/cli/src/graph.ts`); a plugin
|
|
151
|
+
* node itself never calls `store.save()` - see {@link PluginContext.record}.
|
|
152
|
+
*/
|
|
153
|
+
save(): Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One command a plugin's namespace answers, generic over the same `TConfig`
|
|
157
|
+
* as the owning {@link Plugin}. `action` is a plain string, not a nested
|
|
158
|
+
* sub-namespace, so a multi-word action such as `secret status` (declared
|
|
159
|
+
* by the bundled pds plugin, `packages/pds/src/plugin.ts`) is one
|
|
160
|
+
* `PluginCommand`, not two levels of dispatch; the host matches the longest
|
|
161
|
+
* declared action first against the remaining positionals. Declared with a
|
|
162
|
+
* method signature (`run(...)`), not an arrow-typed property
|
|
163
|
+
* (`run: (...) => ...`) - that is what makes
|
|
164
|
+
* `PluginCommand<A>[]` and `PluginCommand<B>[]` both assignable to
|
|
165
|
+
* `PluginCommand<unknown>[]` with no cast; see {@link Plugin} for why the
|
|
166
|
+
* host's registry depends on it.
|
|
167
|
+
*/
|
|
168
|
+
export interface PluginCommand<TConfig = never> {
|
|
169
|
+
/** The action name, e.g. `sync` or `secret status`. */
|
|
170
|
+
action: string;
|
|
171
|
+
/** One line, shown next to the action in help output. */
|
|
172
|
+
summary: string;
|
|
173
|
+
/**
|
|
174
|
+
* Run the command. `args` is whatever positionals/flags remain once
|
|
175
|
+
* `action` - and, by the usual positional/`--env` rule every built-in
|
|
176
|
+
* command already follows, the environment - are consumed.
|
|
177
|
+
*/
|
|
178
|
+
run(ctx: PluginContext<TConfig>, args: string[]): Promise<void>;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A node in the infrastructure dependency graph, generic over `Ctx` - the
|
|
182
|
+
* context its four methods receive. `Ctx` defaults to {@link PluginContext},
|
|
183
|
+
* so a plugin writes plain `ResourceNode` for a node typed against its own
|
|
184
|
+
* narrow context; the CLI instantiates `ResourceNode<OpsContext>`
|
|
185
|
+
* (`packages/cli/src/nodes.ts`) for its own, wider one.
|
|
186
|
+
*
|
|
187
|
+
* The parameter is deliberately **unconstrained** - there is no
|
|
188
|
+
* `Ctx extends PluginContext` here, and there cannot be. The CLI's
|
|
189
|
+
* `OpsContext` (`packages/cli/src/context.ts`) does not satisfy
|
|
190
|
+
* `PluginContext`: it lacks `pluginConfig`, `siteState` and `record` - the
|
|
191
|
+
* same three members task 01's `PluginContext composition` test
|
|
192
|
+
* (`packages/cli/src/context.test.ts`) names off the `TS2739` its own gate
|
|
193
|
+
* rests on. Under a `Ctx extends PluginContext` bound the CLI stops
|
|
194
|
+
* compiling with `TS2344`, because the argument fails the constraint: its
|
|
195
|
+
* fifteen node factories annotate `ResourceNode` bare, so the error surfaces
|
|
196
|
+
* at the one alias they resolve through (`nodes.ts`'s
|
|
197
|
+
* `type ResourceNode = CoreResourceNode<OpsContext>`) and at each remaining
|
|
198
|
+
* explicit instantiation - twelve diagnostics in all, measured, not
|
|
199
|
+
* estimated.
|
|
200
|
+
* Nor is there a supertype of the two contexts worth naming as a bound -
|
|
201
|
+
* `OpsContext` carries CLI-private concerns (`agentDir`, the six-member
|
|
202
|
+
* `Ports`) that `PluginContext` must never see, and `PluginContext` carries
|
|
203
|
+
* `pluginConfig`/`siteState`/`record` that `OpsContext` has no use for.
|
|
204
|
+
* `ResourceNode<OpsContext>` and `ResourceNode<PluginContext>` are therefore
|
|
205
|
+
* two unrelated instantiations of the same generic type, with nothing
|
|
206
|
+
* converting between them.
|
|
207
|
+
*
|
|
208
|
+
* What lets one engine run both - `topoSort`, `applyGraph` and
|
|
209
|
+
* `destroyGraph` (`packages/cli/src/graph.ts`) - is that the engine's own
|
|
210
|
+
* generic constraint is not `PluginContext` either. It is the structural
|
|
211
|
+
* minimum the engine actually reads off a node's context (a logger, a way
|
|
212
|
+
* to persist state, and the state's resources map - `graph.ts`'s exported
|
|
213
|
+
* `GraphContext`), which both `OpsContext` and `PluginContext` satisfy
|
|
214
|
+
* structurally, without either one naming the other or this module.
|
|
215
|
+
*/
|
|
216
|
+
export interface ResourceNode<Ctx = PluginContext> {
|
|
217
|
+
id: string;
|
|
218
|
+
dependsOn: string[];
|
|
219
|
+
/** Human label for logging. */
|
|
220
|
+
title: string;
|
|
221
|
+
/** Does the resource already exist? (Also hydrates outputs into ctx.state.) */
|
|
222
|
+
read(ctx: Ctx): Promise<boolean>;
|
|
223
|
+
create(ctx: Ctx): Promise<void>;
|
|
224
|
+
/** Reconcile an existing resource (optional). */
|
|
225
|
+
update?(ctx: Ctx): Promise<void>;
|
|
226
|
+
delete(ctx: Ctx): Promise<void>;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* One question a plugin's `init` contributor asks, generalising
|
|
230
|
+
* `packages/cli/src/init.ts`'s `Question`. An unanswered optional
|
|
231
|
+
* question (`required` unset or `false`) resolves to the empty string,
|
|
232
|
+
* never `undefined` - DEVELOPMENT.md §Error handling bans `undefined`
|
|
233
|
+
* standing in for a domain value.
|
|
234
|
+
*/
|
|
235
|
+
export interface PluginQuestion {
|
|
236
|
+
/** The prompt text shown to the operator. */
|
|
237
|
+
prompt: string;
|
|
238
|
+
/** Prefilled answer, shown in the prompt and used verbatim when the operator presses enter with no input. */
|
|
239
|
+
defaultValue?: string | undefined;
|
|
240
|
+
/** When true, an empty answer re-prompts instead of being accepted. */
|
|
241
|
+
required?: boolean | undefined;
|
|
242
|
+
/** Return a problem message to re-prompt with, or nothing when the answer is acceptable. */
|
|
243
|
+
validate?: ((answer: string) => string | undefined) | undefined;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The init contributor's surface: the narrow, terminal-shaped slice an
|
|
247
|
+
* `init?(io)` contributor needs to ask its own questions, without depending
|
|
248
|
+
* on the CLI's `Terminal`/`Logger` types directly. It lives here, in core,
|
|
249
|
+
* because both the config-block splice (`blogwright <plugin> init`) and the
|
|
250
|
+
* first-run wizard (`blogwright init`) reach a plugin's contributor through
|
|
251
|
+
* it and need one shared shape.
|
|
252
|
+
*/
|
|
253
|
+
export interface PluginInitIo {
|
|
254
|
+
/** True when the session has an interactive TTY, mirroring `Terminal.isInteractive`. */
|
|
255
|
+
readonly isInteractive: boolean;
|
|
256
|
+
/** Leveled logger for the contributor to report through. */
|
|
257
|
+
logger: PluginLogger;
|
|
258
|
+
/** Ask one question and resolve with the answer - the empty string for an unanswered optional question, never `undefined`. */
|
|
259
|
+
ask(question: PluginQuestion): Promise<string>;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* One property/comment pair an `init?(io)` contributor returns, mirroring
|
|
263
|
+
* the entry shape `renderConfig` builds (`packages/cli/src/init.ts`).
|
|
264
|
+
* `property` is the rendered `"key": value` text; `comment` is an optional
|
|
265
|
+
* trailing `//` note. A contributor the operator declines returns an empty
|
|
266
|
+
* array, never `undefined`.
|
|
267
|
+
*/
|
|
268
|
+
export interface ConfigBlockEntry {
|
|
269
|
+
property: string;
|
|
270
|
+
comment?: string | undefined;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Namespace pattern shared by `Plugin.name` and {@link PluginManifest}'s
|
|
274
|
+
* `plugin` field, so the JSON Schema fragment
|
|
275
|
+
* (`.specs/changes/2026-07-26-cli_plugin_system.md` §Type changes) and this
|
|
276
|
+
* runtime check cannot drift apart. Lowercase alphanumerics and dashes.
|
|
277
|
+
*/
|
|
278
|
+
export declare const PLUGIN_NAME_PATTERN: RegExp;
|
|
279
|
+
/**
|
|
280
|
+
* The `blogwright` field a plugin package declares in its own
|
|
281
|
+
* `package.json` - not in blogwright's own config. Discovery reads this to
|
|
282
|
+
* decide a candidate dependency is a plugin at all; `plugin` is the CLI
|
|
283
|
+
* namespace the package claims, checked against {@link PLUGIN_NAME_PATTERN}
|
|
284
|
+
* the same way `Plugin.name` is.
|
|
285
|
+
*/
|
|
286
|
+
export interface PluginManifest {
|
|
287
|
+
plugin: string;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* A plugin's default export: the whole SPI surface, generic over `TConfig` -
|
|
291
|
+
* the shape of the config block it owns (see {@link PluginContext.pluginConfig}
|
|
292
|
+
* and its doc comment on why the block is read from there, never from
|
|
293
|
+
* `ctx.config`). A plugin declares exactly these members and nothing else:
|
|
294
|
+
* no lifecycle hooks, no plugin-to-plugin dependencies, no contributed
|
|
295
|
+
* ports or adapters, and no merged config schema beyond the one owned key.
|
|
296
|
+
*
|
|
297
|
+
* The default type argument is `never`, not `unknown`, so a plugin that
|
|
298
|
+
* owns no `configKey` writes bare `Plugin` with no argument. Inside such a
|
|
299
|
+
* plugin every property read off `ctx.pluginConfig` -
|
|
300
|
+
* `ctx.pluginConfig.anything` - is `TS2339: Property 'anything' does not
|
|
301
|
+
* exist on type 'never'` (verified against this repo's tsc 6.0.3): that is
|
|
302
|
+
* the check worth having, because under `unknown` the same plugin would
|
|
303
|
+
* instead have to narrow a value it has no schema for, for no benefit. The
|
|
304
|
+
* one unsoundness `never` leaves - recorded here rather than designed
|
|
305
|
+
* around, because no plugin reads its config this way, and the
|
|
306
|
+
* property-level check above is what a mistaken read actually looks like -
|
|
307
|
+
* is the *whole-field* assignment `const n: number = ctx.pluginConfig`,
|
|
308
|
+
* which compiles, because `never` is assignable to every type. Widening the
|
|
309
|
+
* default to `unknown` would trade that property-level check away for a
|
|
310
|
+
* narrowing no plugin has a schema for, and must not be done to "fix" this.
|
|
311
|
+
*
|
|
312
|
+
* The host's registry is `Plugin<unknown>[]`: a `Plugin<PdsConfig>` and a
|
|
313
|
+
* `Plugin<AnalyticsConfig>` both join it, because `commands[].run` and
|
|
314
|
+
* `nodes` are method-declared (not arrow-typed properties) and therefore
|
|
315
|
+
* bivariant in their parameter types - see the type-level test in
|
|
316
|
+
* `plugin.test.ts`. The host consequently builds a `PluginContext<unknown>`
|
|
317
|
+
* and dispatches every plugin through that one registry with no cast. It
|
|
318
|
+
* never constructs a `PluginContext<never>`: nothing inhabits `never`, so
|
|
319
|
+
* `pluginConfig` would have no value to put there, and reaching one would
|
|
320
|
+
* take the `as` cast DEVELOPMENT.md §Code style bans. For a plugin that
|
|
321
|
+
* owns no `configKey` the host puts an empty object in `pluginConfig`
|
|
322
|
+
* instead of `undefined`, per the no-null rule - and such a plugin cannot
|
|
323
|
+
* read it either way, since it never names a `TConfig` to narrow it with.
|
|
324
|
+
*/
|
|
325
|
+
export interface Plugin<TConfig = never> {
|
|
326
|
+
/** The CLI namespace this plugin claims (`analytics` answers `blogwright analytics <action>`). Lowercase alphanumerics and dashes - see {@link PLUGIN_NAME_PATTERN}. */
|
|
327
|
+
name: string;
|
|
328
|
+
/** One line, shown in `blogwright --help`. */
|
|
329
|
+
description: string;
|
|
330
|
+
/** The actions this namespace accepts. */
|
|
331
|
+
commands: PluginCommand<TConfig>[];
|
|
332
|
+
/** Resource-graph nodes this plugin contributes, if any. Never invoked by {@link validatePlugin}. */
|
|
333
|
+
nodes?(ctx: PluginContext<TConfig>): ResourceNode[];
|
|
334
|
+
/** The single top-level config key this plugin owns, if any. */
|
|
335
|
+
configKey?: string;
|
|
336
|
+
/**
|
|
337
|
+
* Validate this plugin's raw config block and **return it**, applying the
|
|
338
|
+
* plugin's own defaults, raising in the repo's own error vocabulary when
|
|
339
|
+
* it does not hold. Never invoked by {@link validatePlugin}.
|
|
340
|
+
*/
|
|
341
|
+
validateConfig?(raw: unknown): TConfig;
|
|
342
|
+
/**
|
|
343
|
+
* The init contributor: ask questions through `io` and return the config
|
|
344
|
+
* block to write - an empty array when the operator declines. Never
|
|
345
|
+
* invoked by {@link validatePlugin}.
|
|
346
|
+
*/
|
|
347
|
+
init?(io: PluginInitIo): Promise<ConfigBlockEntry[]>;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Turn an arbitrary imported module into a trusted {@link Plugin}, or raise
|
|
351
|
+
* naming `packageName` and stating what would fix it. This is the *only*
|
|
352
|
+
* boundary between a package discovery resolved off disk and the rest of
|
|
353
|
+
* the host: validation never invokes anything the module supplies - not
|
|
354
|
+
* `nodes`, not `init`, not `validateConfig`, not any command's `run` - only
|
|
355
|
+
* property reads and `typeof`/`Array.isArray` narrowing, so a malformed or
|
|
356
|
+
* hostile module cannot run code merely by being validated. No raised
|
|
357
|
+
* message echoes a value read off the module; only `packageName`, supplied
|
|
358
|
+
* by the caller, appears in the text.
|
|
359
|
+
*/
|
|
360
|
+
export declare function validatePlugin(module: unknown, packageName: string): Plugin;
|