blogwright 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.
Files changed (70) hide show
  1. package/README.md +11 -11
  2. package/agent/agent-manifest.json +1 -1
  3. package/agent/server.js +37 -19
  4. package/dist/adapters/fetch-ping.d.ts +1 -1
  5. package/dist/adapters/fetch-ping.js +3 -4
  6. package/dist/adapters/node-module-loader.d.ts +11 -0
  7. package/dist/adapters/node-module-loader.js +146 -0
  8. package/dist/adapters/process-package-manager.d.ts +41 -0
  9. package/dist/adapters/process-package-manager.js +116 -0
  10. package/dist/adapters/process-vcs.d.ts +5 -4
  11. package/dist/adapters/process-vcs.js +6 -6
  12. package/dist/agent-package.d.ts +1 -1
  13. package/dist/agent-package.js +4 -4
  14. package/dist/bin.js +13 -3
  15. package/dist/cli.d.ts +68 -1
  16. package/dist/cli.js +270 -89
  17. package/dist/commands.d.ts +70 -3
  18. package/dist/commands.js +180 -32
  19. package/dist/config-block.d.ts +34 -0
  20. package/dist/config-block.js +262 -0
  21. package/dist/context.d.ts +76 -6
  22. package/dist/context.js +98 -19
  23. package/dist/deploy.d.ts +2 -2
  24. package/dist/deploy.js +14 -14
  25. package/dist/graph.d.ts +27 -16
  26. package/dist/graph.js +1 -2
  27. package/dist/init.d.ts +37 -3
  28. package/dist/init.js +146 -23
  29. package/dist/known-commands.d.ts +63 -0
  30. package/dist/known-commands.js +78 -0
  31. package/dist/logger.js +0 -1
  32. package/dist/microvms.d.ts +2 -2
  33. package/dist/microvms.js +3 -4
  34. package/dist/nodes.d.ts +5 -3
  35. package/dist/nodes.js +97 -31
  36. package/dist/plugin-commands.d.ts +298 -0
  37. package/dist/plugin-commands.js +990 -0
  38. package/dist/plugins.d.ts +194 -0
  39. package/dist/plugins.js +523 -0
  40. package/dist/ports.d.ts +89 -1
  41. package/dist/ports.js +0 -1
  42. package/dist/render.d.ts +55 -0
  43. package/dist/render.js +89 -2
  44. package/dist/repo.d.ts +3 -3
  45. package/dist/repo.js +8 -9
  46. package/dist/rkey.js +0 -1
  47. package/dist/seo.d.ts +1 -1
  48. package/dist/seo.js +1 -2
  49. package/package.json +6 -6
  50. package/dist/adapters/fetch-ping.js.map +0 -1
  51. package/dist/adapters/process-vcs.js.map +0 -1
  52. package/dist/agent-package.js.map +0 -1
  53. package/dist/bin.js.map +0 -1
  54. package/dist/cli.js.map +0 -1
  55. package/dist/commands.js.map +0 -1
  56. package/dist/context.js.map +0 -1
  57. package/dist/deploy.js.map +0 -1
  58. package/dist/graph.js.map +0 -1
  59. package/dist/init.js.map +0 -1
  60. package/dist/logger.js.map +0 -1
  61. package/dist/microvms.js.map +0 -1
  62. package/dist/nodes.js.map +0 -1
  63. package/dist/ports.js.map +0 -1
  64. package/dist/render.js.map +0 -1
  65. package/dist/repo.js.map +0 -1
  66. package/dist/rkey.js.map +0 -1
  67. package/dist/seo.js.map +0 -1
  68. package/dist/test-support.d.ts +0 -45
  69. package/dist/test-support.js +0 -126
  70. package/dist/test-support.js.map +0 -1
package/dist/graph.d.ts CHANGED
@@ -1,20 +1,31 @@
1
- import type { OpsContext } from './context.js';
2
- /** A node in the infrastructure dependency graph. */
3
- export interface ResourceNode {
4
- id: string;
5
- dependsOn: string[];
6
- /** Human label for logging. */
7
- title: string;
8
- /** Does the resource already exist? (Also hydrates outputs into ctx.state.) */
9
- read(ctx: OpsContext): Promise<boolean>;
10
- create(ctx: OpsContext): Promise<void>;
11
- /** Reconcile an existing resource (optional). */
12
- update?(ctx: OpsContext): Promise<void>;
13
- delete(ctx: OpsContext): Promise<void>;
1
+ import type { ResourceNode, ResourceOutputs } from 'blogwright-core';
2
+ /**
3
+ * The structural minimum the engine below (`topoSort`, `applyGraph`,
4
+ * `destroyGraph`) actually reads off a node's context: a logger it calls
5
+ * `step`/`ok`/`warn` on, a way to persist state, and the state's resources
6
+ * map (`destroyGraph` deletes an entry from it). Exported so a caller
7
+ * running this engine over a different context - `OpsContext` (`context.ts`)
8
+ * and core's `PluginContext` (`blogwright-core`) both do already - knows
9
+ * exactly what that context must supply, without this module depending on
10
+ * either one. Neither `OpsContext` nor `PluginContext` is named here on
11
+ * purpose: this is the structural minimum both happen to satisfy, not a
12
+ * fixed supertype of them (see the doc comment on core's `ResourceNode` for
13
+ * why no such supertype is worth naming).
14
+ */
15
+ export interface GraphContext {
16
+ logger: {
17
+ step(msg: string): void;
18
+ ok(msg: string): void;
19
+ warn(msg: string): void;
20
+ };
21
+ state: {
22
+ resources: Record<string, ResourceOutputs>;
23
+ };
24
+ save(): Promise<void>;
14
25
  }
15
26
  /** Topologically order nodes so dependencies come before dependents (Kahn's algorithm). */
16
- export declare function topoSort(nodes: ResourceNode[]): ResourceNode[];
27
+ export declare function topoSort<Ctx>(nodes: ResourceNode<Ctx>[]): ResourceNode<Ctx>[];
17
28
  /** Reconcile the graph in dependency order (create missing, update existing). */
18
- export declare function applyGraph(nodes: ResourceNode[], ctx: OpsContext): Promise<void>;
29
+ export declare function applyGraph<Ctx extends GraphContext>(nodes: ResourceNode<Ctx>[], ctx: Ctx): Promise<void>;
19
30
  /** Tear down the graph in reverse dependency order. */
20
- export declare function destroyGraph(nodes: ResourceNode[], ctx: OpsContext): Promise<void>;
31
+ export declare function destroyGraph<Ctx extends GraphContext>(nodes: ResourceNode<Ctx>[], ctx: Ctx): Promise<void>;
package/dist/graph.js CHANGED
@@ -77,9 +77,8 @@ export async function destroyGraph(nodes, ctx) {
77
77
  await node.delete(ctx);
78
78
  delete ctx.state.resources[node.id];
79
79
  // The state lives in the bucket that is itself being deleted, so persisting it may
80
- // fail (NoSuchBucket) once the bucket node is gone never let that abort teardown.
80
+ // fail (NoSuchBucket) once the bucket node is gone - never let that abort teardown.
81
81
  await ctx.save().catch(() => undefined);
82
82
  ctx.logger.ok(`deleted ${node.title}`);
83
83
  }
84
84
  }
85
- //# sourceMappingURL=graph.js.map
package/dist/init.d.ts CHANGED
@@ -1,4 +1,38 @@
1
- import { type FileSystem, type Terminal } from 'blogwright-core';
1
+ import { type FileSystem, type Plugin, type Terminal } from 'blogwright-core';
2
2
  import type { Logger } from './logger.js';
3
- /** Run the wizard. Returns a process exit code; never throws for expected refusals. */
4
- export declare function initSite(fs: FileSystem, terminal: Terminal, logger: Logger, root?: string): Promise<number>;
3
+ /**
4
+ * One question, and how to ask it: shown text, a prefilled default, whether
5
+ * an empty answer must re-prompt, and an optional validator. Exported - and
6
+ * `ask` below along with it - because `plugin-commands.ts`'s `io.ask` (the
7
+ * surface an `init?(io)` contributor asks its own questions through) reuses
8
+ * this exact prompt/validate/retry loop rather than writing a second one; no
9
+ * plugin path may reach `node:readline` itself. Structurally the same shape
10
+ * as core's `PluginQuestion` (`blogwright-core`'s `plugin.ts`), so a
11
+ * contributor's question passes straight through with no conversion.
12
+ */
13
+ export interface Question {
14
+ prompt: string;
15
+ defaultValue?: string | undefined;
16
+ required?: boolean | undefined;
17
+ validate?: ((answer: string) => string | undefined) | undefined;
18
+ }
19
+ /**
20
+ * Ask `q.prompt` over `terminal`, retrying up to `MAX_ATTEMPTS` times on a
21
+ * required-but-empty answer or a `validate` failure, and resolving with
22
+ * `undefined` for an unanswered optional question. Throws once every attempt
23
+ * is spent. The one prompt/validate/retry loop every wizard-shaped question -
24
+ * `blogwright init`'s own four and any plugin's `init?(io)` contributor -
25
+ * asks through.
26
+ */
27
+ export declare function ask(terminal: Terminal, logger: Logger, q: Question): Promise<string | undefined>;
28
+ /**
29
+ * Run the wizard. Returns a process exit code; never throws for expected
30
+ * refusals (non-interactive, an existing config file). `plugins` is every
31
+ * plugin the caller has ALREADY discovered (`cli.ts`, over
32
+ * `DiscoveryPortsFactory`) - this function asks their questions and writes
33
+ * their blocks but never runs discovery itself. A plugin's `init(io)`
34
+ * contributor throwing propagates unchanged, rejecting this call before the
35
+ * single `fs.writeText` below ever runs, so the config file stays exactly
36
+ * what it was - absent, on this path - rather than a partial write.
37
+ */
38
+ export declare function initSite(fs: FileSystem, terminal: Terminal, logger: Logger, plugins: readonly Plugin[], root?: string): Promise<number>;
package/dist/init.js CHANGED
@@ -1,14 +1,28 @@
1
1
  /*
2
- * `blogwright init` the first-run wizard. Asks the four questions a new site
3
- * needs, writes a commented config/production.jsonc, and prints the path to a
4
- * live site. Runs before any context exists (there is no config to load yet),
5
- * so it takes its ports directly.
2
+ * `blogwright init` - the first-run wizard. Asks the four core questions a
3
+ * new site needs, then each already-discovered plugin's own `init(io)`
4
+ * questions (in deterministic, name-sorted order), and writes ONE commented
5
+ * config/production.jsonc carrying every answered block. Runs before any
6
+ * context exists (there is no config to load yet), so it takes its ports
7
+ * directly. Plugin discovery itself is the composition root's job
8
+ * (`cli.ts`), never this module's: `initSite` takes the already-discovered
9
+ * plugins as a plain array, never a `ModuleLoader`, so this stays a domain
10
+ * module.
6
11
  */
7
- import { colors, findRepoRoot } from 'blogwright-core';
12
+ import { colors, findRepoRoot, parseConfig, } from 'blogwright-core';
13
+ import { renderConfigBlock } from './config-block.js';
8
14
  const SITE_NAME_PATTERN = /^[a-z0-9-]+$/;
9
15
  const GITHUB_REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/;
10
16
  const MAX_ATTEMPTS = 3;
11
- async function ask(terminal, logger, q) {
17
+ /**
18
+ * Ask `q.prompt` over `terminal`, retrying up to `MAX_ATTEMPTS` times on a
19
+ * required-but-empty answer or a `validate` failure, and resolving with
20
+ * `undefined` for an unanswered optional question. Throws once every attempt
21
+ * is spent. The one prompt/validate/retry loop every wizard-shaped question -
22
+ * `blogwright init`'s own four and any plugin's `init?(io)` contributor -
23
+ * asks through.
24
+ */
25
+ export async function ask(terminal, logger, q) {
12
26
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
13
27
  const suffix = q.defaultValue ? ` [${q.defaultValue}]` : '';
14
28
  const answer = (await terminal.question(`${q.prompt}${suffix}: `)).trim() || q.defaultValue;
@@ -25,44 +39,140 @@ async function ask(terminal, logger, q) {
25
39
  }
26
40
  return answer;
27
41
  }
28
- throw new Error(`no valid answer after ${MAX_ATTEMPTS} attempts giving up`);
42
+ throw new Error(`no valid answer after ${MAX_ATTEMPTS} attempts - giving up`);
43
+ }
44
+ /**
45
+ * Build the `io` an `init?(io)` contributor asks its own questions through,
46
+ * entirely over the `ask` loop above - never a second prompt/validate/retry
47
+ * loop. Mirrors `plugin-commands.ts`'s own `buildInitIo` (the other path
48
+ * that reaches a plugin's contributor, `blogwright <plugin> init`) rather
49
+ * than importing it: that module already imports `ask` from here, so the
50
+ * reverse import would be a cycle between the two (see `known-commands.ts`'s
51
+ * module comment for what that class of cycle already broke once in this
52
+ * package). `ask` resolves `undefined` for an unanswered optional question;
53
+ * `PluginInitIo.ask` promises a `string` always, per the no-null rule, so
54
+ * the empty string stands in for "declined" here too.
55
+ */
56
+ function buildInitIo(terminal, logger) {
57
+ return {
58
+ isInteractive: terminal.isInteractive,
59
+ logger,
60
+ ask: async (question) => (await ask(terminal, logger, question)) ?? '',
61
+ };
62
+ }
63
+ /**
64
+ * Ask one plugin's `init(io)` contributor its questions and render what it
65
+ * returns as a `"key": { ... }` block in `renderConfigBlock`'s style, or
66
+ * `undefined` when the operator answered nothing (an empty array). A
67
+ * contributor with no `configKey` to file its answers under is a
68
+ * plugin-authoring bug, not an operator refusal - raised naming the plugin,
69
+ * the same check `plugin-commands.ts`'s `runGenericInit` makes for the other
70
+ * path that reaches a contributor, so both refuse identically.
71
+ *
72
+ * This DOES abort the whole wizard - a misconfigured plugin among several
73
+ * stops every other plugin's block (and the core entries) from being
74
+ * written at all, unlike a candidate-level `plugins.ts` discovery failure,
75
+ * which is collected and never blocks an unrelated command. The
76
+ * inconsistency is deliberate, not overlooked: this is a plugin-authoring
77
+ * bug the operator cannot fix by re-running (unlike a transient discovery
78
+ * failure), `runGenericInit` - the sibling path - has no "continue past it"
79
+ * option either, since it operates on exactly one plugin, and this task's
80
+ * own contract is for the two paths to reach a contributor identically.
81
+ * Diverging here would need `plugin-commands.ts` to diverge too, which is
82
+ * outside this module's ownership.
83
+ */
84
+ async function askPluginBlock(plugin, contributor, io) {
85
+ const configKey = plugin.configKey;
86
+ if (!configKey) {
87
+ throw new Error(`plugin "${plugin.name}" declares an init(io) contributor but no configKey - there is ` +
88
+ 'nothing to file its answered block under');
89
+ }
90
+ const entries = await contributor(io);
91
+ if (entries.length === 0)
92
+ return undefined;
93
+ return renderConfigBlock(configKey, entries.map((entry) => ({ prop: entry.property, comment: entry.comment })));
29
94
  }
95
+ /**
96
+ * Ask every plugin in `plugins` that declares an `init(io)` contributor, in
97
+ * deterministic order - sorted by `name`, never discovery or argument order
98
+ * - and return each answered block. A plugin declining (an empty array) or
99
+ * carrying no contributor at all contributes nothing: never a stray entry,
100
+ * never a stray comma once `renderConfig` composes the result.
101
+ */
102
+ async function collectPluginBlocks(plugins, terminal, logger) {
103
+ const io = buildInitIo(terminal, logger);
104
+ const sorted = [...plugins].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
105
+ const blocks = [];
106
+ for (const plugin of sorted) {
107
+ const contributor = plugin.init;
108
+ if (!contributor)
109
+ continue;
110
+ const block = await askPluginBlock(plugin, contributor, io);
111
+ if (block)
112
+ blocks.push(block);
113
+ }
114
+ return blocks;
115
+ }
116
+ /** Render one top-level item with its own trailing comma, or none when it is last. */
117
+ function renderTopLevelItem(item, last) {
118
+ const comma = last ? '' : ',';
119
+ if ('block' in item)
120
+ return `${item.block}${comma}`;
121
+ return ` ${item.prop}${comma}${item.comment ? ` // ${item.comment}` : ''}`;
122
+ }
123
+ /**
124
+ * Render the whole `config/production.jsonc` body: the four core entries in
125
+ * their fixed, commented style, followed by every plugin block already
126
+ * rendered by `collectPluginBlocks` - each its own top-level property, comma
127
+ * discipline shared with the core entries via `renderTopLevelItem` so no
128
+ * block ever leaves (or follows) a stray comma. `pluginBlocks` empty
129
+ * reproduces exactly the entries-only output this wizard has always
130
+ * written - byte for byte, pinned by `init.test.ts`.
131
+ */
30
132
  function renderConfig(opts) {
31
- const entries = [
133
+ const items = [
32
134
  { prop: `"region": "${opts.region}"` },
33
135
  {
34
136
  prop: `"siteName": "${opts.siteName}"`,
35
- comment: 'stable slug in every AWS resource name never change it',
137
+ comment: 'stable slug in every AWS resource name - never change it',
36
138
  },
37
139
  ];
38
140
  if (opts.domain)
39
- entries.push({ prop: `"domain": "${opts.domain}"` });
141
+ items.push({ prop: `"domain": "${opts.domain}"` });
40
142
  if (opts.githubRepo) {
41
- entries.push({
143
+ items.push({
42
144
  prop: `"githubRepo": "${opts.githubRepo}"`,
43
145
  comment: 'enables the GitHub OIDC deploy role',
44
146
  });
45
147
  }
46
- const body = entries.map((e, i) => {
47
- const comma = i < entries.length - 1 ? ',' : '';
48
- return ` ${e.prop}${comma}${e.comment ? ` // ${e.comment}` : ''}`;
49
- });
50
- return ['// config/production.jsonc — created by `blogwright init`', '{', ...body, '}', ''].join('\n');
148
+ for (const block of opts.pluginBlocks)
149
+ items.push({ block });
150
+ const body = items.map((item, i) => renderTopLevelItem(item, i === items.length - 1));
151
+ return ['// config/production.jsonc - created by `blogwright init`', '{', ...body, '}', ''].join('\n');
51
152
  }
52
- /** Run the wizard. Returns a process exit code; never throws for expected refusals. */
53
- export async function initSite(fs, terminal, logger, root) {
153
+ /**
154
+ * Run the wizard. Returns a process exit code; never throws for expected
155
+ * refusals (non-interactive, an existing config file). `plugins` is every
156
+ * plugin the caller has ALREADY discovered (`cli.ts`, over
157
+ * `DiscoveryPortsFactory`) - this function asks their questions and writes
158
+ * their blocks but never runs discovery itself. A plugin's `init(io)`
159
+ * contributor throwing propagates unchanged, rejecting this call before the
160
+ * single `fs.writeText` below ever runs, so the config file stays exactly
161
+ * what it was - absent, on this path - rather than a partial write.
162
+ */
163
+ export async function initSite(fs, terminal, logger, plugins, root) {
54
164
  if (!terminal.isInteractive) {
55
165
  logger.error('init is an interactive wizard; in CI or plain mode create config/production.jsonc ' +
56
- 'by hand instead (see README only "region" and "siteName" are required)');
166
+ 'by hand instead (see README - only "region" and "siteName" are required)');
57
167
  return 1;
58
168
  }
59
169
  const repoRoot = root ?? (await findRepoRoot(fs).catch(() => process.cwd()));
60
170
  const configPath = `${repoRoot}/config/production.jsonc`;
61
171
  if (await fs.exists(configPath)) {
62
- logger.error(`${configPath} already exists edit it directly, or pass --config elsewhere`);
172
+ logger.error(`${configPath} already exists - edit it directly, or pass --config elsewhere`);
63
173
  return 1;
64
174
  }
65
- logger.info(colors.bold('Welcome to blogwright four questions and you are live.'));
175
+ logger.info(colors.bold('Welcome to blogwright - four questions and you are live.'));
66
176
  const siteName = await ask(terminal, logger, {
67
177
  prompt: 'site name (lowercase slug, names every AWS resource)',
68
178
  required: true,
@@ -80,7 +190,21 @@ export async function initSite(fs, terminal, logger, root) {
80
190
  prompt: 'GitHub repo for CI deploys, owner/repo (blank to skip)',
81
191
  validate: (v) => (GITHUB_REPO_PATTERN.test(v) ? undefined : 'expected owner/repo'),
82
192
  });
83
- await fs.writeText(configPath, renderConfig({ region: region, siteName: siteName, domain, githubRepo }));
193
+ const pluginBlocks = await collectPluginBlocks(plugins, terminal, logger);
194
+ const rendered = renderConfig({
195
+ region: region,
196
+ siteName: siteName,
197
+ domain,
198
+ githubRepo,
199
+ pluginBlocks,
200
+ });
201
+ // Re-parsed before it is trusted onto disk, mirroring `plugin-commands.ts`'s
202
+ // `runGenericInit` (the sibling path composing a plugin's block into an
203
+ // existing file): a bug in this composition must never reach the operator
204
+ // as an unloadable config file, which is the one thing this whole feature
205
+ // promises never to do.
206
+ parseConfig(rendered);
207
+ await fs.writeText(configPath, rendered);
84
208
  logger.ok(`wrote ${configPath}`);
85
209
  logger.info('');
86
210
  logger.info(colors.bold('Next steps:'));
@@ -89,4 +213,3 @@ export async function initSite(fs, terminal, logger, root) {
89
213
  logger.info(colors.dim(' (bootstrap prints ACM validation CNAMEs when a domain is set)'));
90
214
  return 0;
91
215
  }
92
- //# sourceMappingURL=init.js.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * A LEAF. This module must never import anything.
3
+ *
4
+ * Task 09 moved `KNOWN_COMMANDS` and `RESERVED_COMMANDS` here out of `cli.ts`
5
+ * because `plugins.ts` needs the reserved set and `cli.ts` imports `plugins.ts`
6
+ * for dispatch - a cycle between the composition root and a domain module. It
7
+ * did not throw, because the set was only read inside a function body, but
8
+ * adding one ordinary top-level derivation to `plugins.ts` under that cycle
9
+ * produced `ReferenceError: Cannot access 'RESERVED_COMMANDS' before
10
+ * initialization` and killed every command including `--help`.
11
+ *
12
+ * A single import added here re-opens that fault, silently, for whichever
13
+ * module happens to be entered first. Keep it dependency-free.
14
+ */
15
+ /**
16
+ * The CLI's command-name registries, isolated in their own leaf module with
17
+ * no imports of its own. Both `cli.ts` (the dispatcher - it pulls in every
18
+ * built-in command implementation, and since task 29 no plugin package by
19
+ * name: `blogwright-pds` reaches it only transitively, through the
20
+ * post-deploy sync `commands.ts` imports) and
21
+ * `plugins.ts` (a domain module that must stay inside the port boundary -
22
+ * see DEVELOPMENT.md §Hexagonal architecture) need to read the same set of
23
+ * reserved names, and neither may import the other: `plugins.ts` importing
24
+ * `cli.ts` for this alone becomes a real cycle the moment `cli.ts` imports
25
+ * `discover` from `plugins.ts` too (task 10 adds exactly that edge, to
26
+ * dispatch `blogwright plugin list`) - two modules each waiting on the
27
+ * other to finish initialising before either can run. This module gives
28
+ * both callers a shared, dependency-free home instead.
29
+ *
30
+ * `KNOWN_COMMANDS` is the eight names `cli.ts`'s `main` dispatches through
31
+ * its `switch`, after the `KNOWN_COMMANDS.has(command)` membership test:
32
+ * `bootstrap`, `deploy`, `rollback`, `delete`, `destroy`, `history`, `logs`,
33
+ * `status`.
34
+ *
35
+ * `RESERVED_COMMANDS` is every name a plugin may never claim as its own -
36
+ * see `discover`'s namespace-collision check in `plugins.ts`. It is
37
+ * deliberately NOT `KNOWN_COMMANDS` alone: `init` (dispatched in `cli.ts`
38
+ * ahead of the `KNOWN_COMMANDS` membership test) and `preview` (likewise)
39
+ * never enter that set, so deriving from it alone would under-reserve by
40
+ * two and let a plugin shadow either one. `plugin` is named explicitly for
41
+ * the same reason, one task early: `cli.ts` does not yet dispatch a
42
+ * `blogwright plugin` namespace (task 10 adds `'plugin'` to
43
+ * `KNOWN_COMMANDS`), but the name is reserved from the moment any plugin
44
+ * could collide with it, and the union keeps this set correct both before
45
+ * and after that command exists - no second edit required either way.
46
+ *
47
+ * `pds` is deliberately absent from `RESERVED_COMMANDS`, and adding it here
48
+ * would break `blogwright pds` outright. There is no built-in `pds` command:
49
+ * task 29 deleted `cli.ts`'s hardcoded `command === 'pds'` branch, and the
50
+ * namespace is now served by the bundled `blogwright-pds` package, which
51
+ * declares the plugin name `pds` and is discovered like any other plugin.
52
+ * Reserving the name would make `discover`'s reserved-name check
53
+ * (`plugins.ts`) reject that package - a load `failures` entry, never an
54
+ * installed plugin - so `blogwright pds <action>` would exit 1 with `no
55
+ * built-in command or installed plugin claims "pds"`, `--help` would list
56
+ * none of its six actions, and `blogwright plugin list` would blame a
57
+ * built-in command that does not exist. This is the one name in this file
58
+ * whose reservation would REMOVE a working command rather than protect one;
59
+ * it stays unreserved on purpose, pinned by a test in `plugins.test.ts` and
60
+ * `cli.test.ts`. See `plugins.ts`'s module comment for the full reasoning.
61
+ */
62
+ export declare const KNOWN_COMMANDS: Set<string>;
63
+ export declare const RESERVED_COMMANDS: ReadonlySet<string>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A LEAF. This module must never import anything.
3
+ *
4
+ * Task 09 moved `KNOWN_COMMANDS` and `RESERVED_COMMANDS` here out of `cli.ts`
5
+ * because `plugins.ts` needs the reserved set and `cli.ts` imports `plugins.ts`
6
+ * for dispatch - a cycle between the composition root and a domain module. It
7
+ * did not throw, because the set was only read inside a function body, but
8
+ * adding one ordinary top-level derivation to `plugins.ts` under that cycle
9
+ * produced `ReferenceError: Cannot access 'RESERVED_COMMANDS' before
10
+ * initialization` and killed every command including `--help`.
11
+ *
12
+ * A single import added here re-opens that fault, silently, for whichever
13
+ * module happens to be entered first. Keep it dependency-free.
14
+ */
15
+ /**
16
+ * The CLI's command-name registries, isolated in their own leaf module with
17
+ * no imports of its own. Both `cli.ts` (the dispatcher - it pulls in every
18
+ * built-in command implementation, and since task 29 no plugin package by
19
+ * name: `blogwright-pds` reaches it only transitively, through the
20
+ * post-deploy sync `commands.ts` imports) and
21
+ * `plugins.ts` (a domain module that must stay inside the port boundary -
22
+ * see DEVELOPMENT.md §Hexagonal architecture) need to read the same set of
23
+ * reserved names, and neither may import the other: `plugins.ts` importing
24
+ * `cli.ts` for this alone becomes a real cycle the moment `cli.ts` imports
25
+ * `discover` from `plugins.ts` too (task 10 adds exactly that edge, to
26
+ * dispatch `blogwright plugin list`) - two modules each waiting on the
27
+ * other to finish initialising before either can run. This module gives
28
+ * both callers a shared, dependency-free home instead.
29
+ *
30
+ * `KNOWN_COMMANDS` is the eight names `cli.ts`'s `main` dispatches through
31
+ * its `switch`, after the `KNOWN_COMMANDS.has(command)` membership test:
32
+ * `bootstrap`, `deploy`, `rollback`, `delete`, `destroy`, `history`, `logs`,
33
+ * `status`.
34
+ *
35
+ * `RESERVED_COMMANDS` is every name a plugin may never claim as its own -
36
+ * see `discover`'s namespace-collision check in `plugins.ts`. It is
37
+ * deliberately NOT `KNOWN_COMMANDS` alone: `init` (dispatched in `cli.ts`
38
+ * ahead of the `KNOWN_COMMANDS` membership test) and `preview` (likewise)
39
+ * never enter that set, so deriving from it alone would under-reserve by
40
+ * two and let a plugin shadow either one. `plugin` is named explicitly for
41
+ * the same reason, one task early: `cli.ts` does not yet dispatch a
42
+ * `blogwright plugin` namespace (task 10 adds `'plugin'` to
43
+ * `KNOWN_COMMANDS`), but the name is reserved from the moment any plugin
44
+ * could collide with it, and the union keeps this set correct both before
45
+ * and after that command exists - no second edit required either way.
46
+ *
47
+ * `pds` is deliberately absent from `RESERVED_COMMANDS`, and adding it here
48
+ * would break `blogwright pds` outright. There is no built-in `pds` command:
49
+ * task 29 deleted `cli.ts`'s hardcoded `command === 'pds'` branch, and the
50
+ * namespace is now served by the bundled `blogwright-pds` package, which
51
+ * declares the plugin name `pds` and is discovered like any other plugin.
52
+ * Reserving the name would make `discover`'s reserved-name check
53
+ * (`plugins.ts`) reject that package - a load `failures` entry, never an
54
+ * installed plugin - so `blogwright pds <action>` would exit 1 with `no
55
+ * built-in command or installed plugin claims "pds"`, `--help` would list
56
+ * none of its six actions, and `blogwright plugin list` would blame a
57
+ * built-in command that does not exist. This is the one name in this file
58
+ * whose reservation would REMOVE a working command rather than protect one;
59
+ * it stays unreserved on purpose, pinned by a test in `plugins.test.ts` and
60
+ * `cli.test.ts`. See `plugins.ts`'s module comment for the full reasoning.
61
+ */
62
+ export const KNOWN_COMMANDS = new Set([
63
+ 'bootstrap',
64
+ 'deploy',
65
+ 'rollback',
66
+ 'delete',
67
+ 'destroy',
68
+ 'history',
69
+ 'logs',
70
+ 'status',
71
+ 'plugin',
72
+ ]);
73
+ export const RESERVED_COMMANDS = new Set([
74
+ ...KNOWN_COMMANDS,
75
+ 'init',
76
+ 'preview',
77
+ 'plugin',
78
+ ]);
package/dist/logger.js CHANGED
@@ -31,4 +31,3 @@ export async function confirm(terminal, question, opts = {}) {
31
31
  return defaultYes;
32
32
  return answer === 'y' || answer === 'yes';
33
33
  }
34
- //# sourceMappingURL=logger.js.map
@@ -3,12 +3,12 @@ import type { OpsContext } from './context.js';
3
3
  /**
4
4
  * Active MicroVMs launched from *this* environment's builder image. Matches on the image
5
5
  * ARN recorded in state, falling back to the (env-scoped) image name so a stack whose
6
- * state predates the ARN still resolves and a sibling env's VMs are never touched.
6
+ * state predates the ARN still resolves - and a sibling env's VMs are never touched.
7
7
  */
8
8
  export declare function runningStackMicrovms(ctx: OpsContext): Promise<Microvm[]>;
9
9
  /**
10
10
  * Guard a destroy/teardown against running builder MicroVMs (deleting the image 400s while
11
- * any are alive). Lists them, then interactively offers to terminate default yes or to
11
+ * any are alive). Lists them, then interactively offers to terminate - default yes - or to
12
12
  * wait, which cancels the destroy. Non-interactive callers get the default (terminate) so
13
13
  * automation isn't blocked. Returns true to proceed with the destroy, false to abort.
14
14
  */
package/dist/microvms.js CHANGED
@@ -8,7 +8,7 @@ function isActive(vm) {
8
8
  /**
9
9
  * Active MicroVMs launched from *this* environment's builder image. Matches on the image
10
10
  * ARN recorded in state, falling back to the (env-scoped) image name so a stack whose
11
- * state predates the ARN still resolves and a sibling env's VMs are never touched.
11
+ * state predates the ARN still resolves - and a sibling env's VMs are never touched.
12
12
  */
13
13
  export async function runningStackMicrovms(ctx) {
14
14
  const imageArn = ctx.state.resources['microvm-image']?.arn;
@@ -22,7 +22,7 @@ export async function runningStackMicrovms(ctx) {
22
22
  }
23
23
  /**
24
24
  * Guard a destroy/teardown against running builder MicroVMs (deleting the image 400s while
25
- * any are alive). Lists them, then interactively offers to terminate default yes or to
25
+ * any are alive). Lists them, then interactively offers to terminate - default yes - or to
26
26
  * wait, which cancels the destroy. Non-interactive callers get the default (terminate) so
27
27
  * automation isn't blocked. Returns true to proceed with the destroy, false to abort.
28
28
  */
@@ -37,7 +37,7 @@ export async function clearRunningMicrovms(ctx) {
37
37
  defaultYes: true,
38
38
  });
39
39
  if (!proceed) {
40
- ctx.logger.info('Leaving MicroVMs running destroy cancelled.');
40
+ ctx.logger.info('Leaving MicroVMs running - destroy cancelled.');
41
41
  return false;
42
42
  }
43
43
  for (const vm of running) {
@@ -53,4 +53,3 @@ export async function clearRunningMicrovms(ctx) {
53
53
  ctx.logger.ok('MicroVMs terminated');
54
54
  return true;
55
55
  }
56
- //# sourceMappingURL=microvms.js.map
package/dist/nodes.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { type ResourceNode as CoreResourceNode } from 'blogwright-core';
1
2
  import type { OpsContext } from './context.js';
2
- import type { ResourceNode } from './graph.js';
3
+ type ResourceNode = CoreResourceNode<OpsContext>;
3
4
  export type BuilderImageAction = 'create' | 'update' | 'skip';
4
5
  /**
5
6
  * Decide what a builder-image reconcile should do: create when the image is missing or
@@ -17,14 +18,14 @@ export declare function builderImageAction(image: {
17
18
  * Create, rebuild, or leave the MicroVM builder image, depending on what's deployed:
18
19
  * create it if missing, rebuild it if the agent bundle (or its log group) changed or the
19
20
  * last build is unhealthy, otherwise no-op. Idempotent and cheap in the common case (a
20
- * single GetMicrovmImage + hash compare), so it's safe to run before every deploy which
21
+ * single GetMicrovmImage + hash compare), so it's safe to run before every deploy - which
21
22
  * is how build-agent changes propagate through CI without a separate `bootstrap`.
22
23
  */
23
24
  export declare function reconcileBuilderImage(ctx: OpsContext): Promise<void>;
24
25
  /**
25
26
  * The workflow's OIDC subject claim, scoped per environment to match how each one
26
27
  * deploys: previews from any PR ref; staging from pushes to main; production from the
27
- * `production` GitHub Environment (release-gated see production.yml), which lets
28
+ * `production` GitHub Environment (release-gated - see production.yml), which lets
28
29
  * deploys be gated behind environment protection rules.
29
30
  */
30
31
  export declare function oidcSubClaim(repo: string, env: string, preview: boolean): string;
@@ -32,3 +33,4 @@ export declare function oidcSubClaim(repo: string, env: string, preview: boolean
32
33
  export declare function oidcRolePolicyStatements(ctx: OpsContext): object[];
33
34
  /** Build the full node set for the current context (production or preview stack). */
34
35
  export declare function buildNodes(ctx: OpsContext): ResourceNode[];
36
+ export {};