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.
- package/README.md +11 -11
- package/agent/agent-manifest.json +1 -1
- package/agent/server.js +37 -19
- package/dist/adapters/fetch-ping.d.ts +1 -1
- package/dist/adapters/fetch-ping.js +3 -4
- package/dist/adapters/node-module-loader.d.ts +11 -0
- package/dist/adapters/node-module-loader.js +146 -0
- package/dist/adapters/process-package-manager.d.ts +41 -0
- package/dist/adapters/process-package-manager.js +116 -0
- package/dist/adapters/process-vcs.d.ts +5 -4
- package/dist/adapters/process-vcs.js +6 -6
- package/dist/agent-package.d.ts +1 -1
- package/dist/agent-package.js +4 -4
- package/dist/bin.js +13 -3
- package/dist/cli.d.ts +68 -1
- package/dist/cli.js +270 -89
- package/dist/commands.d.ts +70 -3
- package/dist/commands.js +180 -32
- package/dist/config-block.d.ts +34 -0
- package/dist/config-block.js +262 -0
- package/dist/context.d.ts +76 -6
- package/dist/context.js +98 -19
- package/dist/deploy.d.ts +2 -2
- package/dist/deploy.js +14 -14
- package/dist/graph.d.ts +27 -16
- package/dist/graph.js +1 -2
- package/dist/init.d.ts +37 -3
- package/dist/init.js +146 -23
- package/dist/known-commands.d.ts +63 -0
- package/dist/known-commands.js +78 -0
- package/dist/logger.js +0 -1
- package/dist/microvms.d.ts +2 -2
- package/dist/microvms.js +3 -4
- package/dist/nodes.d.ts +5 -3
- package/dist/nodes.js +97 -31
- package/dist/plugin-commands.d.ts +298 -0
- package/dist/plugin-commands.js +990 -0
- package/dist/plugins.d.ts +194 -0
- package/dist/plugins.js +523 -0
- package/dist/ports.d.ts +89 -1
- package/dist/ports.js +0 -1
- package/dist/render.d.ts +55 -0
- package/dist/render.js +89 -2
- package/dist/repo.d.ts +3 -3
- package/dist/repo.js +8 -9
- package/dist/rkey.js +0 -1
- package/dist/seo.d.ts +1 -1
- package/dist/seo.js +1 -2
- package/package.json +6 -6
- package/dist/adapters/fetch-ping.js.map +0 -1
- package/dist/adapters/process-vcs.js.map +0 -1
- package/dist/agent-package.js.map +0 -1
- package/dist/bin.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands.js.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/deploy.js.map +0 -1
- package/dist/graph.js.map +0 -1
- package/dist/init.js.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/microvms.js.map +0 -1
- package/dist/nodes.js.map +0 -1
- package/dist/ports.js.map +0 -1
- package/dist/render.js.map +0 -1
- package/dist/repo.js.map +0 -1
- package/dist/rkey.js.map +0 -1
- package/dist/seo.js.map +0 -1
- package/dist/test-support.d.ts +0 -45
- package/dist/test-support.js +0 -126
- 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.
|
|
3
|
-
* outside the build-agent that may import node:child_process
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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
|
package/dist/agent-package.d.ts
CHANGED
|
@@ -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
|
|
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.
|
package/dist/agent-package.js
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
100
|
-
|
|
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 === '
|
|
103
|
-
|
|
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 === '
|
|
106
|
-
return
|
|
305
|
+
if (command === 'preview') {
|
|
306
|
+
return runPreview(positionals, values, terminal, logger, makeContext, makeDiscoveryPorts);
|
|
107
307
|
}
|
|
108
308
|
if (!KNOWN_COMMANDS.has(command)) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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(
|
|
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(
|
|
393
|
+
logger.info(await helpText(makeDiscoveryPorts()));
|
|
216
394
|
return 1;
|
|
217
395
|
}
|
|
218
|
-
const ctx = await
|
|
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(
|
|
430
|
+
logger.info(await helpText(makeDiscoveryPorts()));
|
|
249
431
|
return 1;
|
|
250
432
|
}
|
|
251
433
|
return 0;
|
|
252
434
|
}
|
|
253
|
-
//# sourceMappingURL=cli.js.map
|