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
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Process adapter for the Vcs port: shells out to jj/git. The only module
3
- * outside the build-agent that may import node:child_process; failures are
4
- * translated with the command and directory before they cross the port.
2
+ * Process adapter for the Vcs port: shells out to jj/git. One of only two
3
+ * modules outside the build-agent that may import node:child_process (the
4
+ * other is process-package-manager.ts); failures are translated with the
5
+ * command and directory before they cross the port.
5
6
  */
6
7
  import { execFile } from 'node:child_process';
7
8
  import { promisify } from 'node:util';
@@ -22,7 +23,7 @@ async function runVcsCommand(cwd, command, args) {
22
23
  /**
23
24
  * Build the jj/git process adapter. The revision hash prefers jj's git commit
24
25
  * id (jj auto-commits the working copy), falling back to git HEAD; listings
25
- * honor .gitignore tracked files plus untracked files that are not ignored.
26
+ * honor .gitignore - tracked files plus untracked files that are not ignored.
26
27
  */
27
28
  export function createProcessVcs() {
28
29
  return {
@@ -34,7 +35,7 @@ export function createProcessVcs() {
34
35
  return hash;
35
36
  }
36
37
  catch {
37
- /* jj unavailable or not a jj repo fall through to git */
38
+ /* jj unavailable or not a jj repo - fall through to git */
38
39
  }
39
40
  return (await runVcsCommand(cwd, 'git', ['rev-parse', '--short', 'HEAD'])).trim();
40
41
  },
@@ -46,4 +47,3 @@ export function createProcessVcs() {
46
47
  },
47
48
  };
48
49
  }
49
- //# sourceMappingURL=process-vcs.js.map
@@ -8,7 +8,7 @@ import type { OpsContext } from './context.js';
8
8
  /**
9
9
  * Package the build-agent (Dockerfile + bundled server) into a zip and upload it to
10
10
  * `build/agent/agent-<hash>.zip`. `<hash>` is the reproducible source hash stamped
11
- * into agent-manifest.json when the agent was bundled a hash of the agent's
11
+ * into agent-manifest.json when the agent was bundled - a hash of the agent's
12
12
  * *source*, not the bundle, because bundle bytes vary by build platform/toolchain
13
13
  * while source bytes are identical everywhere. The MicroVM image is then built from
14
14
  * this artifact and keyed by the same hash, so identical source never rebuilds.
@@ -4,6 +4,7 @@
4
4
  * artifacts through the FileSystem port from the composition-root-resolved
5
5
  * `ctx.agentDir`; this module touches no Node API directly.
6
6
  */
7
+ import { REPRODUCIBLE_ZIP_MTIME } from 'blogwright-core';
7
8
  import { zipSync } from 'fflate';
8
9
  const IMAGE_PACKAGE_JSON = JSON.stringify({
9
10
  name: 'site-builder',
@@ -13,7 +14,7 @@ const IMAGE_PACKAGE_JSON = JSON.stringify({
13
14
  /**
14
15
  * Package the build-agent (Dockerfile + bundled server) into a zip and upload it to
15
16
  * `build/agent/agent-<hash>.zip`. `<hash>` is the reproducible source hash stamped
16
- * into agent-manifest.json when the agent was bundled a hash of the agent's
17
+ * into agent-manifest.json when the agent was bundled - a hash of the agent's
17
18
  * *source*, not the bundle, because bundle bytes vary by build platform/toolchain
18
19
  * while source bytes are identical everywhere. The MicroVM image is then built from
19
20
  * this artifact and keyed by the same hash, so identical source never rebuilds.
@@ -34,16 +35,15 @@ export async function packageAndUploadAgent(ctx) {
34
35
  }
35
36
  const hash = manifest.hash;
36
37
  if (!hash || !/^[0-9a-f]{12}$/.test(hash)) {
37
- throw new Error(`agent-manifest.json in ${dir} has no valid hash rebuild the agent`);
38
+ throw new Error(`agent-manifest.json in ${dir} has no valid hash - rebuild the agent`);
38
39
  }
39
40
  const entries = {
40
41
  Dockerfile: dockerfile,
41
42
  'server.js': server,
42
43
  'package.json': new TextEncoder().encode(IMAGE_PACKAGE_JSON),
43
44
  };
44
- const zip = zipSync(entries, { level: 6, mtime: new Date('1980-01-01T00:00:00Z') });
45
+ const zip = zipSync(entries, { level: 6, mtime: REPRODUCIBLE_ZIP_MTIME });
45
46
  const key = `build/agent/agent-${hash}.zip`;
46
47
  await ctx.clients.s3.putObject(ctx.names.bucket, key, zip, 'application/zip');
47
48
  return { key, hash };
48
49
  }
49
- //# sourceMappingURL=agent-package.js.map
package/dist/bin.js CHANGED
@@ -1,9 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { createNodeTerminal } from 'blogwright-core';
2
+ import { createNodeFileSystem, createNodeTerminal } from 'blogwright-core';
3
+ import { createNodeModuleLoader } from './adapters/node-module-loader.js';
4
+ import { createProcessPackageManager } from './adapters/process-package-manager.js';
3
5
  import { main } from './cli.js';
6
+ import { createContext } from './context.js';
4
7
  import { createLogger } from './logger.js';
5
8
  const makeTerminal = (opts) => createNodeTerminal(opts);
6
- main(process.argv.slice(2), makeTerminal)
9
+ // Plugin dispatch needs `fs`/`loader` for discovery before any environment
10
+ // (and therefore any OpsContext) is known - see cli.ts's `DiscoveryPortsFactory`.
11
+ const makeDiscoveryPorts = () => ({ fs: createNodeFileSystem(), loader: createNodeModuleLoader() });
12
+ // `blogwright plugin add`/`plugin remove` install and uninstall through the
13
+ // PackageManager port, likewise before any OpsContext exists - see cli.ts's
14
+ // `PackageManagerFactory`. The adapter resolves the repo and the manager
15
+ // governing it itself, so it needs nothing here but a FileSystem.
16
+ const makePackages = () => createProcessPackageManager(createNodeFileSystem());
17
+ main(process.argv.slice(2), makeTerminal, createContext, makeDiscoveryPorts, makePackages)
7
18
  .then((code) => {
8
19
  process.exitCode = code;
9
20
  })
@@ -11,4 +22,3 @@ main(process.argv.slice(2), makeTerminal)
11
22
  createLogger(makeTerminal({ plain: true })).error(err instanceof Error ? err.message : String(err));
12
23
  process.exitCode = 1;
13
24
  });
14
- //# sourceMappingURL=bin.js.map
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,73 @@
1
1
  import { type Terminal } from 'blogwright-core';
2
+ import { type ContextOptions, type OpsContext } from './context.js';
3
+ import type { PackageManager, Ports } from './ports.js';
2
4
  /** Builds the Terminal after flag parsing, so --plain shapes the whole session. */
3
5
  export type TerminalFactory = (opts: {
4
6
  plain: boolean;
5
7
  }) => Terminal;
6
- export declare function main(argv: string[], makeTerminal: TerminalFactory): Promise<number>;
8
+ /**
9
+ * Builds the OpsContext for a dispatched command. `bin.ts` defaults this to
10
+ * `createContext` (the real, AWS- and disk-reaching composition root); tests
11
+ * supply `createTestContext` instead so dispatch can be asserted without a
12
+ * module mock or an env-var override.
13
+ */
14
+ export type ContextFactory = (opts: ContextOptions) => Promise<OpsContext>;
15
+ /**
16
+ * Builds the `fs`/`loader` ports plugin dispatch needs for discovery -
17
+ * BEFORE any environment is known and therefore before any `OpsContext`
18
+ * exists at all. `bin.ts` defaults this to the real Node adapters
19
+ * (`createNodeFileSystem`/`createNodeModuleLoader`). Declared as a required
20
+ * parameter, not defaulted inside this module, for the same reason
21
+ * `ContextFactory` is: the composition root (`bin.ts`) is the only place
22
+ * real adapters get constructed.
23
+ *
24
+ * Only four kinds of path ever call this factory, and therefore ever run
25
+ * `discover` (`plugins.ts`) or touch `ports.loader` at all:
26
+ *
27
+ * 1. Generic plugin dispatch (`runPlugin`, `plugin-commands.ts`) - a
28
+ * command that is neither a built-in nor `plugin` itself.
29
+ * 2. `blogwright --help` and a bare invocation, plus every other USAGE
30
+ * print site in this module (`helpText`, below) - `main`'s own
31
+ * unknown-command default and `runPreview`'s two - so an error path
32
+ * never shows help that is stale about what is installed.
33
+ * 3. `blogwright plugin list` (task 17), which names plugins that failed
34
+ * to load - only a load attempt can discover that.
35
+ * 4. `blogwright init`, whose wizard asks each discovered plugin's
36
+ * questions and writes their blocks into the config file it produces
37
+ * - but only on an interactive terminal. A non-interactive invocation
38
+ * still calls this factory (it needs the `FileSystem` half to build
39
+ * `initSite`'s arguments) but refuses before ever calling `discover`,
40
+ * so it never resolves or imports a single plugin module for a
41
+ * command that was always going to decline (see the `init` branch's
42
+ * own comment, below).
43
+ *
44
+ * Every other built-in command pays nothing for discovery - `deploy`,
45
+ * `bootstrap` and `status` among them (the three a laziness test in
46
+ * `cli.test.ts` pins directly), and likewise `rollback`, `delete`,
47
+ * `destroy`, `history`, `logs` and `preview` dispatched successfully. A
48
+ * plugin NAMESPACE is not a built-in and never was: dispatching one runs
49
+ * discovery by construction (case 1), which is the whole reason a bundled
50
+ * plugin's actions can reach `--help`, and its own refusals, once no
51
+ * branch in this module knows any namespace by name (task 29).
52
+ */
53
+ export type DiscoveryPortsFactory = () => Pick<Ports, 'fs' | 'loader'>;
54
+ /**
55
+ * Builds the `PackageManager` port `blogwright plugin add`/`plugin remove`
56
+ * install and uninstall through - the only two commands in the CLI that reach
57
+ * it, and the only two that ever call this factory.
58
+ *
59
+ * A SEPARATE factory rather than a third member of {@link DiscoveryPortsFactory}'s
60
+ * result, because the two seams answer different questions: that one supplies
61
+ * the pair every discovery-running path needs (`--help`, `init`, plugin
62
+ * dispatch, `plugin list`), and widening it would hand a package manager to
63
+ * four paths that must never install anything. Declared as a required
64
+ * parameter, not defaulted inside this module, for exactly the reason
65
+ * `ContextFactory` and `DiscoveryPortsFactory` are: the composition root
66
+ * (`bin.ts`) is the only place real adapters get constructed.
67
+ *
68
+ * Passed DOWN to `runPluginNamespace` as the factory itself rather than as a
69
+ * built port, so `blogwright plugin list` - and every refusal `add`/`remove`
70
+ * can end at - constructs no package manager at all.
71
+ */
72
+ export type PackageManagerFactory = () => PackageManager;
73
+ export declare function main(argv: string[], makeTerminal: TerminalFactory, makeContext: ContextFactory, makeDiscoveryPorts: DiscoveryPortsFactory, makePackages: PackageManagerFactory): Promise<number>;
package/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { parseArgs } from 'node:util';
2
- import { createNodeFileSystem } from 'blogwright-core';
3
- import * as pds from 'blogwright-pds';
2
+ import { FileNotFoundError, findRepoRoot, RepoRootNotFoundError, } from 'blogwright-core';
4
3
  import * as commands from './commands.js';
5
- import { createContext } from './context.js';
4
+ import { cliPackageDir, cliVersion } from './context.js';
6
5
  import { initSite } from './init.js';
6
+ import { KNOWN_COMMANDS } from './known-commands.js';
7
7
  import { createLogger } from './logger.js';
8
- const USAGE = `blogwright full operations for a blog site on AWS (S3 + CloudFront, MicroVM builds)
8
+ import { genericLifecycleActions, runPlugin, runPluginNamespace } from './plugin-commands.js';
9
+ import { discover } from './plugins.js';
10
+ const USAGE = `blogwright - full operations for a blog site on AWS (S3 + CloudFront, MicroVM builds)
9
11
 
10
12
  Usage:
11
13
  blogwright <command> [env] [options]
@@ -27,21 +29,12 @@ Commands:
27
29
  preview list List active previews
28
30
  preview teardown --yes Tear down the whole preview stack
29
31
 
30
- pds keygen Generate the OAuth client key: private JWK into
31
- Secrets Manager, public documents into public/oauth/
32
- (commit + release those before pds login)
33
- pds login --identifier <handle-or-did>
34
- Interactive OAuth bootstrap: prints an authorize URL,
35
- then expects the pasted /oauth/callback redirect URL;
36
- the session is stored in Secrets Manager and refreshed
37
- automatically on every sync
38
- pds secret status Show secret metadata (never the value)
39
- pds secret delete --yes Delete the secret (logs out and discards the key)
40
- pds init Create/update the standard.site publication record and
41
- write the site verification files (commit them)
42
- pds sync Reconcile site.standard.document records with the
43
- content collection (production only; also runs after
44
- every successful production deploy)
32
+ plugin add <name> Install a plugin, pinned to this CLI's own
33
+ version (analytics -> blogwright-analytics)
34
+ plugin list List installed plugins: namespace, package,
35
+ version and the config key each owns
36
+ plugin remove <name> Uninstall a plugin, asking first whether to tear
37
+ down the resources it provisioned
45
38
 
46
39
  Options:
47
40
  --env <name> Environment (default: production; also accepted positionally)
@@ -50,7 +43,7 @@ Options:
50
43
  --endpoint <url> AWS endpoint override (e.g. http://localhost:4566 for floci)
51
44
  --id <preview> Preview id for preview deploy/destroy (also accepted positionally)
52
45
  --plain Minimal machine-friendly output (no colour, no live status,
53
- no prompts) for CI systems and agents; also automatic when
46
+ no prompts) - for CI systems and agents; also automatic when
54
47
  output is piped. NO_COLOR disables colour only.
55
48
  --refresh Re-upload every file on deploy, even unchanged ones, so
56
49
  metadata fixes (content types, object tags) reach live
@@ -58,18 +51,150 @@ Options:
58
51
  --yes Confirm destructive operations
59
52
  --help Show this help
60
53
  `;
54
+ /**
55
+ * Render one discovered plugin's help section: its `description` as a
56
+ * header, then one line per command built from that command's `action` and
57
+ * `summary` (§CLI → Plugin dispatch: "`blogwright --help` appends one
58
+ * section per discovered plugin, built from its `description` and its
59
+ * commands' `summary` fields").
60
+ *
61
+ * Then the generic lifecycle verbs the plugin answers -
62
+ * `genericLifecycleActions` (`plugin-commands.ts`), which returns them only
63
+ * for a plugin contributing `nodes`, and never one the plugin declares
64
+ * itself (already listed above). Built from the SAME source the dispatcher
65
+ * builds its synthetic command from, so `--help` cannot advertise a verb
66
+ * `runPlugin` would refuse, nor hide one it answers: a nodes-only plugin
67
+ * declares no commands at all, and listing `plugin.commands` alone rendered
68
+ * it here as a bare description line with nothing under it while all three
69
+ * verbs worked.
70
+ */
71
+ function renderPluginSection(plugin) {
72
+ const commandLines = plugin.commands.map((command) => ` ${command.action} - ${command.summary}`);
73
+ const lifecycleLines = genericLifecycleActions(plugin).map((command) => ` ${command.action} - ${command.summary}`);
74
+ return [` ${plugin.name} - ${plugin.description}`, ...commandLines, ...lifecycleLines].join('\n');
75
+ }
76
+ /**
77
+ * Render one plugin that failed to load: the package it came from and why,
78
+ * with no stack trace, so one broken plugin's `Error` never leaks its
79
+ * `.stack` into `--help` output.
80
+ */
81
+ function renderPluginFailure(failure) {
82
+ return ` ${failure.packageName}: ${failure.reason}`;
83
+ }
84
+ /**
85
+ * Build the full help text from the static `USAGE` base plus one section
86
+ * per discovered plugin, and one line per plugin that failed to load.
87
+ * Returns `base` completely UNCHANGED - not even a trailing blank line
88
+ * added - when `discovered` carries neither a plugin nor a failure, so a
89
+ * repo with no plugins installed sees byte-identical output to today's
90
+ * constant (the pin `cli.test.ts` inherits from task 07).
91
+ *
92
+ * Plugin sections are ordered by `Plugin.name`, never by `discovered`'s own
93
+ * array order: `discover`'s candidate set is built from two
94
+ * `dependencies`/`devDependencies` maps (`plugins.ts`'s `collectCandidates`),
95
+ * and object-key iteration order is an implementation detail of those
96
+ * manifests, not something `--help` output should vary with.
97
+ */
98
+ function buildHelp(base, discovered) {
99
+ if (discovered.plugins.length === 0 && discovered.failures.length === 0)
100
+ return base;
101
+ const blocks = [];
102
+ if (discovered.plugins.length > 0) {
103
+ // Plain string comparison, not `localeCompare` (which would construct an
104
+ // `Intl.Collator` on every call): plugin names are constrained to
105
+ // `PLUGIN_NAME_PATTERN` (lowercase ASCII alphanumerics and dashes), so
106
+ // code-unit ordering already sorts them the same way.
107
+ const byName = [...discovered.plugins].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
108
+ blocks.push(['Plugins:', ...byName.map(renderPluginSection)].join('\n\n'));
109
+ }
110
+ if (discovered.failures.length > 0) {
111
+ blocks.push(['Plugins that failed to load:', ...discovered.failures.map(renderPluginFailure)].join('\n'));
112
+ }
113
+ return `${base}\n${blocks.join('\n\n')}\n`;
114
+ }
115
+ /**
116
+ * True for the exact failure {@link findRepoRoot} (`blogwright-core`)
117
+ * raises when no `.git`/`.jj` is found above the start directory - the
118
+ * ONLY error that function throws (see `repo-root.ts`). Matched by message
119
+ * prefix, not merely `instanceof Error`, so a genuinely unexpected error
120
+ * from a future change to that function still propagates instead of being
121
+ * swallowed here.
122
+ */
123
+ function isNoRepoRootError(err) {
124
+ return err instanceof RepoRootNotFoundError;
125
+ }
126
+ /**
127
+ * True for the one discovery precondition `discover` (`plugins.ts`) throws
128
+ * for that `helpText` chooses to tolerate: the repo's own (or the CLI's
129
+ * own) `package.json` missing entirely -
130
+ * `readDependencyManifest`'s `FileNotFoundError`-caused branch, the only
131
+ * one of its three throw shapes wrapping that error as `cause`. An
132
+ * unparseable or non-object `package.json` is deliberately NOT matched
133
+ * here: unlike a missing file - the ordinary state of a checkout before
134
+ * its first `npm init` - a malformed one is an actual defect in the repo
135
+ * worth surfacing as an error, not a "nothing set up yet" state `--help`
136
+ * should paper over.
137
+ */
138
+ function isMissingPackageJsonError(err) {
139
+ return err instanceof Error && err.cause instanceof FileNotFoundError;
140
+ }
141
+ /**
142
+ * Discover installed plugins and build the help text every USAGE print site
143
+ * in this module now shows - the base `USAGE` constant unchanged when
144
+ * nothing is discovered, plus a section per plugin and failure otherwise.
145
+ *
146
+ * Called fresh on every invocation, never memoised across print sites: each
147
+ * of the four call sites below (`main`'s own `--help`/bare-invocation
148
+ * branch and unknown-command default, and `runPreview`'s two) reaches this
149
+ * only on the specific path it is on, so a command that never prints help -
150
+ * `deploy`, `bootstrap`, `status` among them - never calls `discover` and
151
+ * never touches `ports.loader` at all. Task 29 removed the fifth: the
152
+ * hardcoded namespace branch that printed this text for an unknown action
153
+ * of its own. Generic dispatch answers that namespace now, and
154
+ * `runPlugin`'s refusal lists the matched plugin's OWN actions
155
+ * (`renderActions`, `plugin-commands.ts`) instead of the whole of USAGE.
156
+ * `blogwright plugin list` (task 17) and `blogwright init` (task 14) are
157
+ * the other two paths that pay for discovery; every other built-in command
158
+ * does not. See `DiscoveryPortsFactory`'s doc comment for why the ports
159
+ * themselves come from the caller rather than being built here.
160
+ *
161
+ * `findRepoRoot` and `discover` are documented to throw ONLY for their own
162
+ * repo-level preconditions - `plugins.ts`'s own module comment names those
163
+ * two (an unreadable repo root `package.json` or CLI `package.json`) as the
164
+ * things it deliberately throws for, in contrast to every candidate-level
165
+ * problem, which becomes a `failures` entry instead. `--help` running
166
+ * outside any repo, or inside one with no root `package.json` yet, is
167
+ * exactly the "nothing set up" state a first-time run looks like - so
168
+ * BOTH calls are individually guarded to fall back to plain `USAGE` for
169
+ * their own documented precondition failure, one precondition earlier than
170
+ * `buildHelp` already does the same for a single broken plugin. Anything
171
+ * else - a malformed `package.json`, or any error `findRepoRoot`/`discover`
172
+ * are not documented to throw - still propagates to `bin.ts`'s error path,
173
+ * exactly as it would have before this function existed.
174
+ */
175
+ async function helpText(ports) {
176
+ let repoRoot;
177
+ try {
178
+ repoRoot = await findRepoRoot(ports.fs);
179
+ }
180
+ catch (err) {
181
+ if (isNoRepoRootError(err))
182
+ return USAGE;
183
+ throw err;
184
+ }
185
+ let discovered;
186
+ try {
187
+ discovered = await discover(repoRoot, cliPackageDir(), ports);
188
+ }
189
+ catch (err) {
190
+ if (isMissingPackageJsonError(err))
191
+ return USAGE;
192
+ throw err;
193
+ }
194
+ return buildHelp(USAGE, discovered);
195
+ }
61
196
  const HASH_COMMANDS = new Set(['rollback', 'logs']);
62
- const KNOWN_COMMANDS = new Set([
63
- 'bootstrap',
64
- 'deploy',
65
- 'rollback',
66
- 'delete',
67
- 'destroy',
68
- 'history',
69
- 'logs',
70
- 'status',
71
- ]);
72
- export async function main(argv, makeTerminal) {
197
+ export async function main(argv, makeTerminal, makeContext, makeDiscoveryPorts, makePackages) {
73
198
  const { values, positionals } = parseArgs({
74
199
  args: argv,
75
200
  allowPositionals: true,
@@ -80,6 +205,17 @@ export async function main(argv, makeTerminal) {
80
205
  endpoint: { type: 'string' },
81
206
  hash: { type: 'string' },
82
207
  id: { type: 'string' },
208
+ // Declared here even though no built-in command reads it. `main`
209
+ // parses ONE flag table for every invocation, so a flag only a
210
+ // plugin action takes still has to be pulled out of the positionals
211
+ // before dispatch - otherwise `--identifier alice.example` would be
212
+ // read as an environment. It reaches the plugin as the ordinary
213
+ // `--identifier <value>` token pair `serialiseFlags`
214
+ // (`plugin-commands.ts`) renders, so this module forwards it without
215
+ // knowing what it means, and the action that does know declares the
216
+ // flag in its own `summary`. Do not delete it as unused: nothing in
217
+ // this file refers to it by name, and dropping it turns the value
218
+ // into a stray positional rather than an error.
83
219
  identifier: { type: 'string' },
84
220
  plain: { type: 'boolean', default: false },
85
221
  refresh: { type: 'boolean', default: false },
@@ -91,24 +227,101 @@ export async function main(argv, makeTerminal) {
91
227
  const logger = createLogger(terminal);
92
228
  const command = positionals[0];
93
229
  if (!command || values.help) {
94
- logger.info(USAGE);
230
+ logger.info(await helpText(makeDiscoveryPorts()));
95
231
  // Asking for help is success; invoking with no command at all is not.
96
232
  return values.help || command ? 0 : 1;
97
233
  }
98
234
  if (command === 'init') {
99
- // Runs before any context exists — there is no config to load yet.
100
- return initSite(createNodeFileSystem(), terminal, logger);
235
+ const discoveryPorts = makeDiscoveryPorts();
236
+ if (!terminal.isInteractive) {
237
+ // `initSite`'s own first check refuses immediately on a
238
+ // non-interactive terminal, before it ever touches `fs` or `plugins`.
239
+ // Skip discovery entirely here rather than resolving - and dynamically
240
+ // importing - every installed plugin's module for a command that was
241
+ // always going to decline; `makeDiscoveryPorts()` above is cheap
242
+ // (adapter construction only, no I/O), unlike `discover` itself.
243
+ return initSite(discoveryPorts.fs, terminal, logger, []);
244
+ }
245
+ // Runs before any context exists - there is no config to load yet.
246
+ // `init` is one of the four discovery-running paths `DiscoveryPortsFactory`
247
+ // enumerates above: the wizard asks each discovered plugin's questions
248
+ // too, so discovery runs over the SAME factory plugin dispatch uses
249
+ // below, not a second, ad hoc seam. `blogwright init` is also exactly
250
+ // the situation `helpText` above already tolerates a discovery
251
+ // precondition failure for: a resolved repo root, a readable
252
+ // package.json there, may not hold at all, since this is the wizard
253
+ // that BOOTSTRAPS a repo. Mirrors `helpText`'s two-step try/catch
254
+ // exactly, narrowed the same way with the same two helpers: a missing
255
+ // repo root or a missing package.json is tolerated - a real first-run
256
+ // state, not a defect - and treated as "no plugins installed" so the
257
+ // plain four-question flow still completes; anything else - a
258
+ // malformed package.json, say, which IS a defect worth surfacing -
259
+ // propagates unchanged rather than silently discarding a plugin's
260
+ // config block.
261
+ let repoRoot;
262
+ try {
263
+ repoRoot = await findRepoRoot(discoveryPorts.fs);
264
+ }
265
+ catch (err) {
266
+ if (!isNoRepoRootError(err))
267
+ throw err;
268
+ repoRoot = process.cwd();
269
+ }
270
+ let plugins = [];
271
+ try {
272
+ plugins = (await discover(repoRoot, cliPackageDir(), discoveryPorts)).plugins;
273
+ }
274
+ catch (err) {
275
+ if (!isMissingPackageJsonError(err))
276
+ throw err;
277
+ logger.warn(`${err.message} - continuing with no plugins discovered`);
278
+ }
279
+ return initSite(discoveryPorts.fs, terminal, logger, plugins, repoRoot);
101
280
  }
102
- if (command === 'preview') {
103
- return runPreview(positionals, values, terminal, logger);
281
+ if (command === 'plugin') {
282
+ // Dispatched HERE, beside `init` and ahead of the built-in switch, and
283
+ // never from the switch itself: the switch runs past the `makeContext`
284
+ // call below, which loads the environment's config and calls
285
+ // `sts.getAccountId()`. `blogwright plugin list` and `plugin add` are what
286
+ // an operator runs BEFORE the repo is configured, so dispatching them
287
+ // after that build would answer both with `no config found for
288
+ // environment "production"` on exactly the repo they exist to serve -
289
+ // and `plugin add` would additionally need a working AWS session to
290
+ // install a package, which is absurd. `plugin` stays in `KNOWN_COMMANDS`
291
+ // regardless, so no installed plugin can claim the name
292
+ // (`RESERVED_COMMANDS` derives from that set).
293
+ //
294
+ // `makeContext` crosses as the FACTORY it is, never a built context: the
295
+ // one path in this namespace that needs an `OpsContext` is `plugin
296
+ // remove`'s answered-yes teardown, which builds it there, after the
297
+ // question - so the placement above still holds for every other action.
298
+ return runPluginNamespace(positionals.slice(1), terminal, logger, makeDiscoveryPorts(), {
299
+ values,
300
+ makePackages,
301
+ cliVersion,
302
+ makeContext,
303
+ });
104
304
  }
105
- if (command === 'pds') {
106
- return runPds(positionals, values, terminal, logger);
305
+ if (command === 'preview') {
306
+ return runPreview(positionals, values, terminal, logger, makeContext, makeDiscoveryPorts);
107
307
  }
108
308
  if (!KNOWN_COMMANDS.has(command)) {
109
- logger.error(`unknown command: ${command}`);
110
- logger.info(USAGE);
111
- return 1;
309
+ // Not a built-in and not `plugin` itself: the only remaining possibility
310
+ // is an installed plugin's namespace. `runPlugin` runs discovery itself
311
+ // (built-in commands below never call `makeDiscoveryPorts` or trigger
312
+ // it) and reports an unknown plugin or action on its own, so there is no
313
+ // further fallback here.
314
+ //
315
+ // TASK 29 - this is now the ONLY route into a plugin's namespace. The
316
+ // hardcoded branch that used to sit immediately above this membership
317
+ // test, ahead of it precisely so it would win, is gone: the bundled
318
+ // plugin the CLI ships in its own `dependencies` is discovered and
319
+ // dispatched here exactly like an installed one, which is why the name
320
+ // it claims stays out of `RESERVED_COMMANDS` (`known-commands.ts`).
321
+ // This module knows no namespace by name any more, and adding one back
322
+ // would re-open the split where `--help` advertised verbs the
323
+ // hand-rolled branch refused.
324
+ return runPlugin(command, positionals.slice(1), values, terminal, logger, makeContext, makeDiscoveryPorts());
112
325
  }
113
326
  // Positional layout: rollback/logs take <hash> first, then optional env.
114
327
  let hash;
@@ -121,7 +334,7 @@ export async function main(argv, makeTerminal) {
121
334
  envPositional = positionals[1];
122
335
  }
123
336
  const env = values.env ?? envPositional ?? 'production';
124
- const ctx = await createContext({
337
+ const ctx = await makeContext({
125
338
  env,
126
339
  configPath: values.config,
127
340
  domain: values.domain,
@@ -158,64 +371,29 @@ export async function main(argv, makeTerminal) {
158
371
  await commands.status(ctx);
159
372
  break;
160
373
  default:
374
+ // Unreachable: past the `KNOWN_COMMANDS` membership test above,
375
+ // `command` is one of the nine names in that set, and the `plugin`
376
+ // branch has already claimed the only one without a case here. Kept
377
+ // wired to the same help text as every other USAGE print site - like
378
+ // `runPreview`'s own unreachable default below - so it stays correct
379
+ // if a name is ever added to `KNOWN_COMMANDS` without a case.
161
380
  logger.error(`unknown command: ${command}`);
162
- logger.info(USAGE);
381
+ logger.info(await helpText(makeDiscoveryPorts()));
163
382
  return 1;
164
383
  }
165
384
  return 0;
166
385
  }
167
- /** Handle `blogwright pds <action> [env]` (and `pds secret <action> [env]`). */
168
- async function runPds(positionals, values, terminal, logger) {
169
- // `pds secret set production` — the secret sub-action shifts positionals by one.
170
- const secret = positionals[1] === 'secret';
171
- const action = secret ? `secret ${positionals[2] ?? ''}`.trim() : positionals[1];
172
- const envPositional = positionals[secret ? 3 : 2];
173
- const known = new Set(['keygen', 'login', 'init', 'sync', 'secret status', 'secret delete']);
174
- if (!action || !known.has(action)) {
175
- logger.error(`unknown pds action: ${action ?? '(none)'}`);
176
- logger.info(USAGE);
177
- return 1;
178
- }
179
- const ctx = await createContext({
180
- env: values.env ?? envPositional ?? 'production',
181
- configPath: values.config,
182
- domain: values.domain,
183
- endpointOverride: values.endpoint,
184
- ports: { terminal },
185
- });
186
- switch (action) {
187
- case 'keygen':
188
- await pds.keygen(ctx);
189
- break;
190
- case 'login':
191
- await pds.login(ctx, { identifier: values.identifier });
192
- break;
193
- case 'secret status':
194
- await pds.secretStatus(ctx);
195
- break;
196
- case 'secret delete':
197
- await pds.secretDelete(ctx, { yes: values.yes });
198
- break;
199
- case 'init':
200
- await pds.init(ctx);
201
- break;
202
- case 'sync':
203
- await pds.sync(ctx);
204
- break;
205
- }
206
- return 0;
207
- }
208
386
  /** Handle `blogwright preview <action> [id]` (always the shared `preview` stack). */
209
387
  const PREVIEW_ACTIONS = new Set(['bootstrap', 'deploy', 'destroy', 'list', 'teardown']);
210
- async function runPreview(positionals, values, terminal, logger) {
388
+ async function runPreview(positionals, values, terminal, logger, makeContext, makeDiscoveryPorts) {
211
389
  const action = positionals[1];
212
390
  const id = values.id ?? positionals[2];
213
391
  if (!action || !PREVIEW_ACTIONS.has(action)) {
214
392
  logger.error(`unknown preview action: ${action ?? '(none)'}`);
215
- logger.info(USAGE);
393
+ logger.info(await helpText(makeDiscoveryPorts()));
216
394
  return 1;
217
395
  }
218
- const ctx = await createContext({
396
+ const ctx = await makeContext({
219
397
  env: 'preview',
220
398
  preview: true,
221
399
  configPath: values.config,
@@ -244,10 +422,13 @@ async function runPreview(positionals, values, terminal, logger) {
244
422
  await commands.previewTeardown(ctx, { yes: values.yes });
245
423
  break;
246
424
  default:
425
+ // Unreachable - `action` is guaranteed to be a `PREVIEW_ACTIONS` member
426
+ // past the guard above, which already returns for anything else. Kept
427
+ // wired to the same help text as every other USAGE print site so it
428
+ // stays correct if that guarantee is ever loosened.
247
429
  logger.error(`unknown preview action: ${action ?? '(none)'}`);
248
- logger.info(USAGE);
430
+ logger.info(await helpText(makeDiscoveryPorts()));
249
431
  return 1;
250
432
  }
251
433
  return 0;
252
434
  }
253
- //# sourceMappingURL=cli.js.map