@getquick/site 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to `@getquick/site` are recorded here. Versions follow
4
4
  [Semantic Versioning](https://semver.org/); each release is tagged `v<version>`.
5
5
 
6
+ ## 0.1.1 — 2026-10-01
7
+
8
+ ### Fixed
9
+
10
+ - The README describes the published package: install, configuration,
11
+ commands, `run()`, and releasing.
12
+ - `exec` no longer crashes the process when a child exits without reading
13
+ its input, and decodes output as UTF-8 so multibyte characters survive
14
+ chunk boundaries.
15
+ - `run()` rejects a call without `stdout` and `stderr` up front instead of
16
+ failing while reporting another error.
17
+ - `release:publish` stops with a clear message when a `git` check fails,
18
+ instead of reading a failed `git status` as a clean tree.
19
+
6
20
  ## 0.1.0 — 2026-09-30
7
21
 
8
22
  First release: `gq-ops` 0.1.0 (`Quick-Release/gq-ops@d972d12`) as the `gq` bin
package/README.md CHANGED
@@ -1,14 +1,146 @@
1
1
  # getquick-site
2
2
 
3
3
  Shared tooling for GETQUICK sites, published to public npm as
4
- `@getquick/site`: one `gq` CLI for releases and version sync, Ploi
5
- provisioning and releases, database sync and backups, Cloudflare CI and
6
- media, Sigillo secret injection, and the `setup`/`doctor`/`verify` runners.
7
- Each site configures it through its own `gq.ops.json`.
4
+ [`@getquick/site`](https://www.npmjs.com/package/@getquick/site): one `gq`
5
+ CLI, configured by each site's own `gq.ops.json`.
8
6
 
9
7
  It replaces [`gq-ops`](https://github.com/Quick-Release/gq-ops) and the
10
8
  vendored `shop-devtools`. The package is being extracted from the Lombardi
11
- site (phase 1 of the GETQUICK blueprint rollout) and isn't published yet.
9
+ site (phase 1 of the GETQUICK blueprint rollout); release and version sync,
10
+ Ploi provisioning and releases, database sync and backups, Cloudflare CI and
11
+ media, Sigillo secret injection, and the `setup`/`doctor`/`verify` runners
12
+ move in over the coming releases. Today it carries `gq-ops`'s commands: Ploi,
13
+ Cloudflare, and GitHub Actions sync.
14
+
15
+ ## Install
16
+
17
+ Pin an exact version in the site's root `devDependencies`; it needs no
18
+ registry login:
19
+
20
+ ```sh
21
+ pnpm add --save-dev --save-exact @getquick/site
22
+ ```
23
+
24
+ ```json
25
+ {
26
+ "scripts": {
27
+ "ops": "gq"
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## Configure a site
33
+
34
+ `gq` walks up from the current directory to the nearest `gq.ops.json`,
35
+ stopping at the enclosing Git repository, and treats that directory as the
36
+ site root. Every site-relative path resolves from there. `--project <dir>` or
37
+ `--config <file>` selects a site explicitly.
38
+
39
+ ```json
40
+ {
41
+ "project": "example-site",
42
+ "ploi": { "serverId": "12345", "siteId": "67890" },
43
+ "cloudflare": {
44
+ "accountId": "0123456789abcdef0123456789abcdef",
45
+ "zoneId": "abcdef0123456789abcdef0123456789",
46
+ "zoneName": "example.com"
47
+ },
48
+ "github": {
49
+ "repository": "Quick-Release/example-site",
50
+ "environment": "production",
51
+ "secrets": ["CLOUDFLARE_API_TOKEN"],
52
+ "variables": ["CLOUDFLARE_ACCOUNT_ID"]
53
+ }
54
+ }
55
+ ```
56
+
57
+ Provider IDs are safe to commit; tokens are not. `gq` reads `PLOI_API_TOKEN`,
58
+ `CLOUDFLARE_API_TOKEN` and optional ID overrides (`PLOI_SERVER_ID`,
59
+ `PLOI_SITE_ID`, `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ZONE_ID`,
60
+ `CLOUDFLARE_ZONE_NAME`) from, in increasing precedence:
61
+
62
+ 1. `${XDG_CONFIG_HOME:-$HOME/.config}/gq/ops.env`
63
+ 2. the site's `.env`
64
+ 3. the process environment, which is where a secret manager such as Sigillo
65
+ injects them per command
66
+ 4. flags (`--server`, `--site`, `--account`, `--zone`)
67
+
68
+ ## Commands
69
+
70
+ ```sh
71
+ gq --version
72
+ gq context show [--json]
73
+
74
+ gq ploi servers list
75
+ gq ploi server show [--server <id>]
76
+ gq ploi sites list [--server <id>]
77
+ gq ploi site show [--server <id>] [--site <id>]
78
+ gq ploi api list [--group <group>] [--search <text>]
79
+ gq ploi api describe <operation-id>
80
+ gq ploi api <operation-id> [--path name=value] [--query name=value]
81
+ [--page <n>] [--per-page <n>] [--data <json> | --data-file <file>]
82
+ [--all] [--max-pages <n>] [--dry-run | --yes]
83
+
84
+ gq cloudflare accounts list
85
+ gq cloudflare zones list [--account <id>]
86
+ gq cloudflare zone show [--zone <id>]
87
+ gq cloudflare dns list [--zone <id>] [--name <hostname>] [--type <type>]
88
+
89
+ gq github actions sync [--dry-run] [--yes]
90
+ ```
91
+
92
+ `--json` prints machine-readable output; `ploi api` always prints the
93
+ provider's JSON. In a terminal, `gq` with no arguments opens a command picker.
94
+
95
+ `ploi api` covers all 225 operations in the Ploi API reference
96
+ ([inventory](docs/research/ploi-api.md)); operation IDs follow the docs' routes,
97
+ such as `sites.log-site`. Every non-GET operation needs `--yes` (or a prompt in
98
+ a terminal); `--dry-run` prints the resolved request without sending it.
99
+ Cloudflare commands are read-only. `github actions sync` pipes each value to
100
+ `gh` on stdin and never prints it.
101
+
102
+ ## Programmatic use
103
+
104
+ The CLI is a thin shell over `run()`, which resolves to an exit code:
105
+
106
+ ```js
107
+ import { run } from "@getquick/site";
108
+
109
+ const code = await run(["ploi", "site", "show", "--json"], {
110
+ cwd, // where discovery starts
111
+ env, // replaces process.env
112
+ fetch, // every provider request
113
+ exec, // every child process: (command, args, { cwd, env, input }) => { code, stdout, stderr }
114
+ stdout, // anything with write()
115
+ stderr,
116
+ });
117
+ ```
118
+
119
+ No command reads `process.env` or `process.cwd()`; `bin/gq.mjs` is the only
120
+ place that passes the real ones.
121
+
122
+ ## Development
123
+
124
+ ```sh
125
+ pnpm install
126
+ pnpm check # Prettier, ESLint, node:test
127
+ ```
128
+
129
+ Tests call `run()` against a fixture site (a temporary Git repository with a
130
+ `gq.ops.json`) with recording fakes for `fetch` and `exec`
131
+ ([`test/support/fixture-site.mjs`](test/support/fixture-site.mjs)). They need
132
+ no network, credentials, or provider accounts.
133
+
134
+ ## Releasing
135
+
136
+ 1. Bump `version` in `package.json`, add its section to `CHANGELOG.md`, and
137
+ commit.
138
+ 2. Tag the commit `v<version>` and push the commit and tag.
139
+ 3. `pnpm release:publish` checks that `HEAD` carries that tag and that
140
+ `pnpm check` passes, then runs `npm publish` under Sigillo's `operations`
141
+ environment (`gq.ops.json`), where `NPM_TOKEN` lives. The token is never
142
+ written to disk. It is a granular token that expires within 90 days, so
143
+ rotate it before then.
12
144
 
13
145
  ## License
14
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getquick/site",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared tooling for GETQUICK sites: the gq CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/exec.mjs CHANGED
@@ -7,12 +7,15 @@ export function exec(command, args, { cwd, env, input } = {}) {
7
7
  const child = spawn(command, args, { cwd, env, stdio: ["pipe", "pipe", "pipe"] });
8
8
  let stdout = "";
9
9
  let stderr = "";
10
- child.stdout.on("data", (chunk) => {
10
+ child.stdout.setEncoding("utf8").on("data", (chunk) => {
11
11
  stdout += chunk;
12
12
  });
13
- child.stderr.on("data", (chunk) => {
13
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
14
14
  stderr += chunk;
15
15
  });
16
+ // A child that exits without reading its input closes the pipe (EPIPE);
17
+ // its exit code, not the write, is the result.
18
+ child.stdin.on("error", () => {});
16
19
  child.once("error", reject);
17
20
  child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
18
21
  child.stdin.end(input === undefined ? undefined : String(input));
package/src/run.mjs CHANGED
@@ -19,6 +19,9 @@ export async function run(
19
19
  } = {},
20
20
  ) {
21
21
  if (typeof cwd !== "string" || cwd === "") throw new TypeError("run() requires a cwd.");
22
+ if (typeof stdout?.write !== "function" || typeof stderr?.write !== "function") {
23
+ throw new TypeError("run() requires stdout and stderr streams.");
24
+ }
22
25
  const io = {
23
26
  out: (line) => stdout.write(`${line}\n`),
24
27
  err: (line) => stderr.write(`${line}\n`),