pinnace 0.0.0 → 0.2.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/LICENSE +661 -0
- package/README.md +164 -0
- package/dist/car/car-build.d.ts +26 -0
- package/dist/car/car-build.d.ts.map +1 -0
- package/dist/car/car-build.js +113 -0
- package/dist/car/car-build.js.map +1 -0
- package/dist/ci/ci-emit.d.ts +116 -0
- package/dist/ci/ci-emit.d.ts.map +1 -0
- package/dist/ci/ci-emit.js +190 -0
- package/dist/ci/ci-emit.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +17 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/run.d.ts +94 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +731 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/config/config-resolution.d.ts +186 -0
- package/dist/config/config-resolution.d.ts.map +1 -0
- package/dist/config/config-resolution.js +137 -0
- package/dist/config/config-resolution.js.map +1 -0
- package/dist/deploy/deploy.d.ts +121 -0
- package/dist/deploy/deploy.d.ts.map +1 -0
- package/dist/deploy/deploy.js +150 -0
- package/dist/deploy/deploy.js.map +1 -0
- package/dist/derive/ipns-key-derivation.d.ts +37 -0
- package/dist/derive/ipns-key-derivation.d.ts.map +1 -0
- package/dist/derive/ipns-key-derivation.js +130 -0
- package/dist/derive/ipns-key-derivation.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/node/node-commands.d.ts +115 -0
- package/dist/node/node-commands.d.ts.map +1 -0
- package/dist/node/node-commands.js +231 -0
- package/dist/node/node-commands.js.map +1 -0
- package/dist/provision/cloud-init.d.ts +162 -0
- package/dist/provision/cloud-init.d.ts.map +1 -0
- package/dist/provision/cloud-init.js +460 -0
- package/dist/provision/cloud-init.js.map +1 -0
- package/dist/publisher/key-import.d.ts +105 -0
- package/dist/publisher/key-import.d.ts.map +1 -0
- package/dist/publisher/key-import.js +76 -0
- package/dist/publisher/key-import.js.map +1 -0
- package/dist/publisher/record-sequence.d.ts +90 -0
- package/dist/publisher/record-sequence.d.ts.map +1 -0
- package/dist/publisher/record-sequence.js +250 -0
- package/dist/publisher/record-sequence.js.map +1 -0
- package/dist/rpc/kubo-rpc-client.d.ts +154 -0
- package/dist/rpc/kubo-rpc-client.d.ts.map +1 -0
- package/dist/rpc/kubo-rpc-client.js +207 -0
- package/dist/rpc/kubo-rpc-client.js.map +1 -0
- package/dist/rpc/mock-kubo.d.ts +87 -0
- package/dist/rpc/mock-kubo.d.ts.map +1 -0
- package/dist/rpc/mock-kubo.js +129 -0
- package/dist/rpc/mock-kubo.js.map +1 -0
- package/dist/site/site-management.d.ts +126 -0
- package/dist/site/site-management.d.ts.map +1 -0
- package/dist/site/site-management.js +123 -0
- package/dist/site/site-management.js.map +1 -0
- package/dist/status/status-report.d.ts +145 -0
- package/dist/status/status-report.d.ts.map +1 -0
- package/dist/status/status-report.js +192 -0
- package/dist/status/status-report.js.map +1 -0
- package/package.json +45 -2
- package/src/car/car-build.ts +135 -0
- package/src/ci/ci-emit.ts +284 -0
- package/src/cli/bin.ts +20 -0
- package/src/cli/run.ts +970 -0
- package/src/config/config-resolution.ts +264 -0
- package/src/deploy/deploy.ts +250 -0
- package/src/derive/ipns-key-derivation.ts +173 -0
- package/src/index.ts +146 -0
- package/src/node/node-commands.ts +395 -0
- package/src/provision/cloud-init.ts +646 -0
- package/src/publisher/key-import.ts +141 -0
- package/src/publisher/record-sequence.ts +336 -0
- package/src/rpc/kubo-rpc-client.ts +281 -0
- package/src/rpc/mock-kubo.ts +194 -0
- package/src/site/site-management.ts +241 -0
- package/src/status/status-report.ts +291 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# pinnace
|
|
2
|
+
|
|
3
|
+
Self-host a static website on IPFS across one or more self-owned Kubo nodes, without a paid pinning service. `pinnace` provisions the nodes (generated cloud-init), deploys your site as a content-addressed archive pinned on every node with the same CID, manages mutable `ipns://` names via a master-key-derived per-site key, keeps public-gateway caches warm, and emits CI, all over the nodes' bearer-guarded Kubo RPC API. Hetzner is the first host; other hosts sit behind a provider seam.
|
|
4
|
+
|
|
5
|
+
`pinnace` is both a **CLI** and a **library**: the core owns all logic and the `pinnace` bin is a thin wrapper, so every operation is equally usable as a TypeScript API.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g pinnace
|
|
11
|
+
# or, per-project:
|
|
12
|
+
npm install pinnace
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node >= 22.
|
|
16
|
+
|
|
17
|
+
## Mental model
|
|
18
|
+
|
|
19
|
+
- **node** (box): a self-owned server running the Kubo IPFS daemon, reached ONLY via its Kubo RPC API (`POST /api/v0/...`, bearer-token guarded, fronted by Caddy over HTTPS). Swarm port 4001 is open so public gateways can dial it; the raw RPC (5001) is never exposed.
|
|
20
|
+
- **publisher / replica**: exactly one **publisher** per shared IPNS name holds the derived key, signs + refreshes the record, and exports the raw signed record. Keyless **replicas** pin the same CID and re-announce the publisher's record (falling back to a cached copy if the publisher is down). This is the failover model: the name stays resolvable within the record's validity window even if the publisher dies.
|
|
21
|
+
- **CID / CAR**: your site is built client-side into a CAR (Content Addressable aRchive) whose root is the site's UnixFS directory; the same CAR is imported into every node so they all serve the identical **CID**.
|
|
22
|
+
- **master key -> per-site IPNS key**: one operator-held secret (env-only, never on a node, never in the config file) deterministically derives each site's IPNS key: `HKDF-SHA256(master, info = "pinnace:ipns:v1:" + id)` -> ed25519 seed -> the `k51...` IPNS name. Names are recoverable from the master alone; provisioning is stateless. This is a frozen contract (see the ADRs).
|
|
23
|
+
- **site `id`**: one value per site, used as BOTH its MFS entry (`/sites/<id>`) AND the key-derivation input. Pick anything stable (e.g. `mysite`, or `ronan.eth`).
|
|
24
|
+
- **mode**: per-site `ipfs` (land + pin + MFS only; you point a contenthash at `ipfs://<cid>` per deploy) or `ipns` (also publish/refresh; point it at `ipns://<id>` once).
|
|
25
|
+
- **gateway warming**: re-fetching each site's CID through public gateways so their caches stay hot; sites are auto-discovered from MFS.
|
|
26
|
+
|
|
27
|
+
## Configuration + secrets
|
|
28
|
+
|
|
29
|
+
Every setting resolves **CLI arg > env (via `ldenv`) > `pinnace.json`**.
|
|
30
|
+
|
|
31
|
+
`pinnace.json` holds only NON-secret structure (commit-safe):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"hosts": [
|
|
36
|
+
{ "name": "publisher", "endpoint": "https://ipfs-publisher.example.com", "role": "publisher" },
|
|
37
|
+
{ "name": "replica", "endpoint": "https://ipfs-replica-01.example.com", "role": "replica",
|
|
38
|
+
"publisherEndpoint": "https://ipfs-dash.example.com" }
|
|
39
|
+
],
|
|
40
|
+
"sites": [
|
|
41
|
+
{ "id": "mysite", "mode": "ipns", "sourceDir": "./site" }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Secrets are **env-only, never in the config file** (structurally: the resolver has no file path for them). Each host's bearer token is read from `PINNACE_HOST_<NAME>_TOKEN` (the name upper-cased), and the master from `PINNACE_MASTER`:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# .env.local (git-ignored) — the ONLY place secrets live
|
|
50
|
+
PINNACE_MASTER=<your high-entropy master secret>
|
|
51
|
+
PINNACE_HOST_PUBLISHER_TOKEN=<publisher bearer token>
|
|
52
|
+
PINNACE_HOST_REPLICA_TOKEN=<replica bearer token>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A missing token fails loud (naming the exact env var), never a silent empty token. Point the CLI at a config anywhere with `--config <path>`.
|
|
56
|
+
|
|
57
|
+
## The end-to-end setup
|
|
58
|
+
|
|
59
|
+
The full flow, from zero to a redundant, failover-capable IPNS site. Values below match the config above; substitute your own domains.
|
|
60
|
+
|
|
61
|
+
### 1. Pick tokens + an id
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
export PINNACE_HOST_PUBLISHER_TOKEN=$(openssl rand -hex 32)
|
|
65
|
+
export PINNACE_HOST_REPLICA_TOKEN=$(openssl rand -hex 32)
|
|
66
|
+
# choose one id for the site, used as both the MFS entry and the key input
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Generate cloud-init for each box
|
|
70
|
+
|
|
71
|
+
`provision` emits ready-to-paste cloud-init to stdout (it is per-box and arg-driven; it does not read `pinnace.json`). Give the publisher a `--dashboard-domain`: that vhost serves the exported IPNS records at `/records/` for replicas to fetch.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
# publisher
|
|
75
|
+
pinnace provision --host hetzner --role publisher \
|
|
76
|
+
--api-domain ipfs-publisher.example.com \
|
|
77
|
+
--dashboard-domain ipfs-dash.example.com \
|
|
78
|
+
--acme-email you@example.com \
|
|
79
|
+
--bearer-token "$PINNACE_HOST_PUBLISHER_TOKEN" > cloud-init-publisher.yaml
|
|
80
|
+
|
|
81
|
+
# replica (points at the publisher's DASHBOARD as its records endpoint)
|
|
82
|
+
pinnace provision --host hetzner --role replica \
|
|
83
|
+
--api-domain ipfs-replica-01.example.com \
|
|
84
|
+
--acme-email you@example.com \
|
|
85
|
+
--bearer-token "$PINNACE_HOST_REPLICA_TOKEN" \
|
|
86
|
+
--publisher-endpoint https://ipfs-dash.example.com > cloud-init-replica.yaml
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The emitted cloud-init stands up a hardened node: Kubo as a systemd unit (discoverability tuned so gateways find it), ufw opening 4001 TCP+UDP + 80/443 (never 5001), Caddy HTTPS + bearer API proxy, and it installs a pinned `pinnace` on the box and schedules the on-box agent (`pinnace node republish|mirror|warm|status`) on role-gated systemd timers. The publisher's `republish` timer exports signed records to the dashboard's `/records/`; the replica's `mirror` timer fetches + re-announces them, falling back to cache on outage.
|
|
90
|
+
|
|
91
|
+
### 3. Create the boxes + DNS
|
|
92
|
+
|
|
93
|
+
Create two servers (e.g. Hetzner Debian 13 / CX22), each with its `cloud-init-*.yaml` as user-data. Then point DNS at their IPv4s so Caddy can obtain certificates:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
A ipfs-publisher <publisher IP>
|
|
97
|
+
A ipfs-replica-01 <replica IP>
|
|
98
|
+
A ipfs-dash <publisher IP> # the records/dashboard vhost
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Verify each API answers once DNS + certs are up (bearer required):
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
curl -sS -X POST https://ipfs-publisher.example.com/api/v0/id \
|
|
105
|
+
-H "Authorization: Bearer $PINNACE_HOST_PUBLISHER_TOKEN"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 4. Derive the IPNS id (optional, before first deploy)
|
|
109
|
+
|
|
110
|
+
Print a site's `k51...` id from the master + id with no deploy or network, so you can set a contenthash ahead of time:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
pinnace --config pinnace.json derive mysite
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 5. Deploy + provision the publisher key
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
# build one CAR, import the same CID into every node, place it in MFS
|
|
120
|
+
pinnace --config pinnace.json deploy --mode ipns ./site mysite
|
|
121
|
+
|
|
122
|
+
# import the derived key onto the publisher (and flip its role to publisher)
|
|
123
|
+
pinnace --config pinnace.json promote mysite --host publisher
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
After this the on-box timers run the failover loop automatically: the publisher re-signs + exports the record, replicas mirror + re-announce it, and if the publisher goes down the replicas keep the name alive from their cached record within its validity window.
|
|
127
|
+
|
|
128
|
+
### 6. Check it
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
pinnace --config pinnace.json status # per-site CID / IPNS id / announce / gateway-serves
|
|
132
|
+
curl -sS https://ipfs-dash.example.com/records/mysite.ipns-record # the exported signed record
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Command reference
|
|
136
|
+
|
|
137
|
+
| Command | What it does |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `pinnace provision --host hetzner --role <publisher\|replica> --api-domain <d> --acme-email <e> --bearer-token <t> [--dashboard-domain <d>] [--publisher-endpoint <url>]` | Emit a node's cloud-init YAML to stdout. |
|
|
140
|
+
| `pinnace deploy [--mode ipfs\|ipns] <dir> <id>` | Build one CAR, import the same CID into every configured node, pin + place in MFS; in `ipns` mode publish on the publisher. |
|
|
141
|
+
| `pinnace promote <id> [--host <name>]` | Derive the per-site key from the master and import it onto the host, making it the publisher (also the replica-promotion path). |
|
|
142
|
+
| `pinnace derive <id>` (alias `ipns-id`) | Print a site's `k51...` IPNS id from master + id, no deploy/network. |
|
|
143
|
+
| `pinnace status` | Per-site report across nodes: CID, IPNS id, network-announce, gateway-serves. |
|
|
144
|
+
| `pinnace install-ci --system github --build-command <c> --output-dir <d>` | Emit a deploy CI workflow and report the secrets/vars to set. |
|
|
145
|
+
| `pinnace site <list\|add\|remove> ...` | Manage the sites a node serves (MFS entries + pins). |
|
|
146
|
+
| `pinnace node <republish\|mirror\|warm\|status>` | The on-box agent verbs (run by the box's systemd timers; role-gated). |
|
|
147
|
+
|
|
148
|
+
Global: `--config <path>` selects the `pinnace.json` (default `./pinnace.json`); a named-but-missing path fails loud.
|
|
149
|
+
|
|
150
|
+
## Library use
|
|
151
|
+
|
|
152
|
+
Every operation is exported from the package core, so the same things are callable as a TypeScript API:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import {deriveIpnsId, buildCar, resolveConfig, KuboRpcClient} from 'pinnace';
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Design notes / decisions
|
|
159
|
+
|
|
160
|
+
Durable architectural decisions live in [`docs/adr/`](../../docs/adr/) in the repo, notably: the frozen master-key -> IPNS KDF; that client-side key derivation is NOT client-side record signing (the node signs); and the boundary that Kubo owns pinning + provider-record freshness while the same `pinnace` binary runs the recurring on-box loop.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
AGPL-3.0-only.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The result of building a CAR: its raw bytes and its authoritative root CID. */
|
|
2
|
+
export interface BuiltCar {
|
|
3
|
+
/** The complete CAR file bytes (header carries {@link BuiltCar.rootCid}). */
|
|
4
|
+
readonly carBytes: Uint8Array;
|
|
5
|
+
/** The site's UnixFS directory root CID, as the LAST encoder block. */
|
|
6
|
+
readonly rootCid: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build a CAR for a site source directory, entirely in-process.
|
|
10
|
+
*
|
|
11
|
+
* @param sourceDir the site's built output directory (its CONTENTS become the
|
|
12
|
+
* CAR root — the directory itself is NOT wrapped in an extra segment).
|
|
13
|
+
* @returns the CAR bytes plus the authoritative root CID (the last block the
|
|
14
|
+
* directory encoder emits, mirrored into the CAR header).
|
|
15
|
+
* @throws if no files are found under `sourceDir` (nothing to deploy).
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildCar(sourceDir: string): Promise<BuiltCar>;
|
|
18
|
+
/**
|
|
19
|
+
* Build a CAR for `sourceDir` and write it to `outPath`.
|
|
20
|
+
*
|
|
21
|
+
* A thin convenience over {@link buildCar} for the deploy path that persists a
|
|
22
|
+
* CAR to disk before importing it into each node. Returns the same
|
|
23
|
+
* {@link BuiltCar} (its `carBytes` are exactly the bytes written).
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeCar(sourceDir: string, outPath: string): Promise<BuiltCar>;
|
|
26
|
+
//# sourceMappingURL=car-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"car-build.d.ts","sourceRoot":"","sources":["../../src/car/car-build.ts"],"names":[],"mappings":"AAmCA,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqDnE;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACb,OAAO,CAAC,QAAQ,CAAC,CAInB"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process **CAR** (Content Addressable aRchive) builder — the default and
|
|
3
|
+
* primary deploy artifact (CONTEXT.md `CAR`; spec user stories 4-6).
|
|
4
|
+
*
|
|
5
|
+
* Given a site SOURCE DIRECTORY, this builds a CAR whose root is a real UnixFS
|
|
6
|
+
* DIRECTORY preserving the site structure (`index.html`, `assets/...`). The DAG
|
|
7
|
+
* is built entirely IN-PROCESS via the `ipfs-car` library (no external CLI, no
|
|
8
|
+
* output scraping), so the root CID is authoritative and the build reproducible.
|
|
9
|
+
*
|
|
10
|
+
* The scheme (ported, not copied, from the reference prototype
|
|
11
|
+
* `~/searches/ipfs-hetzner/deploy-car.mjs`):
|
|
12
|
+
*
|
|
13
|
+
* 1. `filesFromPaths([sourceDir])` yields the directory's files with
|
|
14
|
+
* SITE-RELATIVE paths (`index.html`, `assets/s.css`) and NO wrapping
|
|
15
|
+
* segment. We pass them straight in: the CAR root IS the site dir. (An
|
|
16
|
+
* earlier prototype wrongly stripped a leading segment; that bug is fixed
|
|
17
|
+
* here and guarded by a test asserting `index.html` sits at the root.)
|
|
18
|
+
* 2. Pipe the files through `createDirectoryEncoderStream`, buffering every
|
|
19
|
+
* emitted block and capturing the LAST block's CID as the directory root
|
|
20
|
+
* (authoritative — captured from the encoder, NEVER scraped from text).
|
|
21
|
+
* 3. Re-encode the buffered blocks with `new CAREncoderStream([rootCid])` so
|
|
22
|
+
* the CAR HEADER carries the root. That is what makes
|
|
23
|
+
* `dag/import?pin-roots=true` pin the site root on every node.
|
|
24
|
+
*
|
|
25
|
+
* The build BUFFERS blocks in memory (fine for normal static sites; streaming
|
|
26
|
+
* very large sites is explicitly out of scope for v1 — see the spec).
|
|
27
|
+
*/
|
|
28
|
+
import { writeFile } from 'node:fs/promises';
|
|
29
|
+
import { createDirectoryEncoderStream, CAREncoderStream, } from 'ipfs-car';
|
|
30
|
+
import { filesFromPaths } from 'files-from-path';
|
|
31
|
+
/**
|
|
32
|
+
* Build a CAR for a site source directory, entirely in-process.
|
|
33
|
+
*
|
|
34
|
+
* @param sourceDir the site's built output directory (its CONTENTS become the
|
|
35
|
+
* CAR root — the directory itself is NOT wrapped in an extra segment).
|
|
36
|
+
* @returns the CAR bytes plus the authoritative root CID (the last block the
|
|
37
|
+
* directory encoder emits, mirrored into the CAR header).
|
|
38
|
+
* @throws if no files are found under `sourceDir` (nothing to deploy).
|
|
39
|
+
*/
|
|
40
|
+
export async function buildCar(sourceDir) {
|
|
41
|
+
// filesFromPaths([sourceDir]) already yields SITE-RELATIVE paths
|
|
42
|
+
// ("index.html", "assets/s.css"). Do NOT strip a leading segment.
|
|
43
|
+
const files = await filesFromPaths([sourceDir]);
|
|
44
|
+
if (files.length === 0) {
|
|
45
|
+
throw new Error(`no files found under ${sourceDir}`);
|
|
46
|
+
}
|
|
47
|
+
// Pass 1: encode the directory, buffering blocks and capturing the root CID
|
|
48
|
+
// as the LAST emitted block. The header roots are patched in pass 2, so we
|
|
49
|
+
// encode with an empty root list here and discard the resulting stream.
|
|
50
|
+
const blocks = [];
|
|
51
|
+
let rootBlock;
|
|
52
|
+
await createDirectoryEncoderStream(files)
|
|
53
|
+
.pipeThrough(new TransformStream({
|
|
54
|
+
transform(block, controller) {
|
|
55
|
+
// The directory root is the LAST block the encoder emits.
|
|
56
|
+
rootBlock = block;
|
|
57
|
+
blocks.push(block);
|
|
58
|
+
controller.enqueue(block);
|
|
59
|
+
},
|
|
60
|
+
}))
|
|
61
|
+
.pipeThrough(new CAREncoderStream([]))
|
|
62
|
+
.pipeTo(new WritableStream());
|
|
63
|
+
if (!rootBlock) {
|
|
64
|
+
// Unreachable given files.length > 0, but keeps the root authoritative.
|
|
65
|
+
throw new Error(`encoder produced no blocks for ${sourceDir}`);
|
|
66
|
+
}
|
|
67
|
+
const rootCid = rootBlock.cid.toString();
|
|
68
|
+
// Pass 2: re-encode the SAME buffered blocks WITH the known root in the CAR
|
|
69
|
+
// header, so `dag/import?pin-roots=true` pins the site root.
|
|
70
|
+
const chunks = [];
|
|
71
|
+
const pending = blocks.slice();
|
|
72
|
+
const source = new ReadableStream({
|
|
73
|
+
pull(controller) {
|
|
74
|
+
const next = pending.shift();
|
|
75
|
+
if (next)
|
|
76
|
+
controller.enqueue(next);
|
|
77
|
+
else
|
|
78
|
+
controller.close();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
await source.pipeThrough(new CAREncoderStream([rootBlock.cid])).pipeTo(new WritableStream({
|
|
82
|
+
write(chunk) {
|
|
83
|
+
chunks.push(chunk);
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
return { carBytes: concatChunks(chunks), rootCid };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build a CAR for `sourceDir` and write it to `outPath`.
|
|
90
|
+
*
|
|
91
|
+
* A thin convenience over {@link buildCar} for the deploy path that persists a
|
|
92
|
+
* CAR to disk before importing it into each node. Returns the same
|
|
93
|
+
* {@link BuiltCar} (its `carBytes` are exactly the bytes written).
|
|
94
|
+
*/
|
|
95
|
+
export async function writeCar(sourceDir, outPath) {
|
|
96
|
+
const built = await buildCar(sourceDir);
|
|
97
|
+
await writeFile(outPath, built.carBytes);
|
|
98
|
+
return built;
|
|
99
|
+
}
|
|
100
|
+
/** Concatenate CAR stream chunks into one contiguous byte array. */
|
|
101
|
+
function concatChunks(chunks) {
|
|
102
|
+
let total = 0;
|
|
103
|
+
for (const chunk of chunks)
|
|
104
|
+
total += chunk.length;
|
|
105
|
+
const out = new Uint8Array(total);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
for (const chunk of chunks) {
|
|
108
|
+
out.set(chunk, offset);
|
|
109
|
+
offset += chunk.length;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=car-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"car-build.js","sourceRoot":"","sources":["../../src/car/car-build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **CI emitter** behind the `CIProvider` seam (CONTEXT.md `CI provider
|
|
3
|
+
* seam`). `install-ci` asks the core for a deploy pipeline: an emitter WRITES a
|
|
4
|
+
* deploy workflow for a CI system and REPORTS the repo secrets/vars the
|
|
5
|
+
* operator must set. GitHub Actions is the FIRST (and, in v1, only)
|
|
6
|
+
* implementation; other CI systems are Out of Scope but slot in behind this
|
|
7
|
+
* same seam later (spec "Out of Scope"; user stories 16, 17).
|
|
8
|
+
*
|
|
9
|
+
* This module is PURE: {@link emitCi} takes a plain input and returns the
|
|
10
|
+
* workflow file (path + contents) plus the secrets/vars report as data. It does
|
|
11
|
+
* NOT touch the filesystem or the network — the CLI wrapper (`cli-command-
|
|
12
|
+
* wrapper` task) is what writes the file to `.github/workflows/` and prints the
|
|
13
|
+
* report. Keeping the emit pure lets it be snapshot-tested as a string with no
|
|
14
|
+
* fixtures (test-first policy) and reused as a TypeScript API (CONTEXT.md `core
|
|
15
|
+
* vs cli`).
|
|
16
|
+
*
|
|
17
|
+
* BEHAVIOUR PORTED (not copied) from the reference GitHub Action
|
|
18
|
+
* `~/searches/ipfs-hetzner/github-workflow.yml`: the shape is checkout ->
|
|
19
|
+
* setup-node -> install -> build -> a `deploy` step reading the multi-node env
|
|
20
|
+
* contract (`IPFS_API`/`IPFS_TOKEN` comma-separated, publisher first),
|
|
21
|
+
* `SITE_NAME`, `SITE_MODE` -> a run-summary. Two deliberate CORRECTIONS vs the
|
|
22
|
+
* reference: (1) the deploy step invokes `pinnace deploy` (this tool now OWNS
|
|
23
|
+
* deploy) instead of a copied `deploy-car.mjs` script — one implementation, no
|
|
24
|
+
* bash/TS drift; (2) the same env contract carries the MULTI-NODE + per-site
|
|
25
|
+
* `mode` semantics (same CID to all nodes; the publisher publishes the record,
|
|
26
|
+
* replicas mirror) which the deploy core (`deploy-multi-target` +
|
|
27
|
+
* `publisher-replica-model` tasks) enforces — the workflow just passes the env
|
|
28
|
+
* through.
|
|
29
|
+
*/
|
|
30
|
+
/** The CI systems pinnace can emit for. v1 = GitHub Actions ONLY. */
|
|
31
|
+
export type CiSystem = 'github';
|
|
32
|
+
/** The CI systems, in a stable order (help text / iteration / validation). */
|
|
33
|
+
export declare const CI_SYSTEMS: readonly CiSystem[];
|
|
34
|
+
/** One required repo secret or variable the operator must set for the pipeline. */
|
|
35
|
+
export interface RequiredCiSetting {
|
|
36
|
+
/** The env/secret/var NAME as referenced in the workflow (e.g. `IPFS_API`). */
|
|
37
|
+
name: string;
|
|
38
|
+
/**
|
|
39
|
+
* `secret` (masked, e.g. tokens) or `var` (plain repo variable, e.g. URLs /
|
|
40
|
+
* names). GitHub stores these separately; the report tells the operator
|
|
41
|
+
* which panel each belongs in.
|
|
42
|
+
*/
|
|
43
|
+
kind: 'secret' | 'var';
|
|
44
|
+
/** A human-readable note (what it is, format, an example). */
|
|
45
|
+
description: string;
|
|
46
|
+
/**
|
|
47
|
+
* True when the value is a COMMA-SEPARATED list, one per target node with
|
|
48
|
+
* the PUBLISHER FIRST (the multi-node contract). `IPFS_API`/`IPFS_TOKEN` are
|
|
49
|
+
* multi-node; `SITE_NAME`/`SITE_MODE` are single values.
|
|
50
|
+
*/
|
|
51
|
+
multiNode: boolean;
|
|
52
|
+
/** An example value, shown in the report to make the format concrete. */
|
|
53
|
+
example: string;
|
|
54
|
+
}
|
|
55
|
+
/** Inputs to {@link emitCi}: which system + how the site builds. */
|
|
56
|
+
export interface EmitCiInput {
|
|
57
|
+
/** Which CI system to emit for (v1: `github`). */
|
|
58
|
+
system: CiSystem;
|
|
59
|
+
/** The build command that produces the static output dir (e.g. `npm run build`). */
|
|
60
|
+
buildCommand: string;
|
|
61
|
+
/** The directory the build outputs the static site to (e.g. `dist`). */
|
|
62
|
+
outputDir: string;
|
|
63
|
+
/** The branch to deploy on push (default `main`). */
|
|
64
|
+
branch?: string;
|
|
65
|
+
/** The Node.js version the workflow sets up (default `20`). */
|
|
66
|
+
nodeVersion?: string;
|
|
67
|
+
}
|
|
68
|
+
/** A single emitted file: where it goes + what it contains. */
|
|
69
|
+
export interface EmittedFile {
|
|
70
|
+
/** The repo-relative path to write the file at. */
|
|
71
|
+
path: string;
|
|
72
|
+
/** The full file contents. */
|
|
73
|
+
contents: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What an emitter returns: the workflow file plus the secrets/vars the operator
|
|
77
|
+
* must set. `secrets` and `vars` are split so the CLI can tell the operator
|
|
78
|
+
* exactly which GitHub settings panel each value belongs in.
|
|
79
|
+
*/
|
|
80
|
+
export interface EmittedCi {
|
|
81
|
+
/** Which system this was emitted for (echoed for the caller). */
|
|
82
|
+
system: CiSystem;
|
|
83
|
+
/** The deploy workflow file (path + contents). */
|
|
84
|
+
workflow: EmittedFile;
|
|
85
|
+
/** The required masked SECRETS (Settings -> Secrets). */
|
|
86
|
+
secrets: RequiredCiSetting[];
|
|
87
|
+
/** The required repo VARIABLES (Settings -> Variables). */
|
|
88
|
+
vars: RequiredCiSetting[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The `CIProvider` seam: one method that emits a deploy pipeline + its required
|
|
92
|
+
* settings for a given input. v1 has a single implementation
|
|
93
|
+
* ({@link githubCiProvider}); adding GitLab CI / others later means adding
|
|
94
|
+
* another provider here and an entry in {@link CI_SYSTEMS} — deploy/publish
|
|
95
|
+
* logic is untouched (spec user story 21).
|
|
96
|
+
*/
|
|
97
|
+
export interface CIProvider {
|
|
98
|
+
/** The system this provider emits for. */
|
|
99
|
+
readonly system: CiSystem;
|
|
100
|
+
/** Emit the workflow + secrets/vars report for the given input. */
|
|
101
|
+
emit(input: EmitCiInput): EmittedCi;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The GitHub Actions provider — the first implementation of {@link CIProvider}.
|
|
105
|
+
* Emits the deploy workflow to the conventional GitHub path and reports the
|
|
106
|
+
* secrets/vars split into the two GitHub settings panels.
|
|
107
|
+
*/
|
|
108
|
+
export declare const githubCiProvider: CIProvider;
|
|
109
|
+
/**
|
|
110
|
+
* Emit a deploy pipeline for the requested CI system: dispatches to the
|
|
111
|
+
* matching {@link CIProvider} and returns its workflow + secrets/vars report.
|
|
112
|
+
* Throws LOUDLY on an unknown/unimplemented system (the seam exists so callers
|
|
113
|
+
* can add systems later, but v1 only ships `github`) — never a silent no-op.
|
|
114
|
+
*/
|
|
115
|
+
export declare function emitCi(input: EmitCiInput): EmittedCi;
|
|
116
|
+
//# sourceMappingURL=ci-emit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-emit.d.ts","sourceRoot":"","sources":["../../src/ci/ci-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,qEAAqE;AACrE,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC;AAEhC,8EAA8E;AAC9E,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAAe,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IACjC,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IACvB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC3B,kDAAkD;IAClD,MAAM,EAAE,QAAQ,CAAC;IACjB,oFAAoF;IACpF,YAAY,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC3B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACzB,iEAAiE;IACjE,MAAM,EAAE,QAAQ,CAAC;IACjB,kDAAkD;IAClD,QAAQ,EAAE,WAAW,CAAC;IACtB,yDAAyD;IACzD,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,2DAA2D;IAC3D,IAAI,EAAE,iBAAiB,EAAE,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,mEAAmE;IACnE,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;CACpC;AAsID;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,UAa9B,CAAC;AAOF;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAUpD"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **CI emitter** behind the `CIProvider` seam (CONTEXT.md `CI provider
|
|
3
|
+
* seam`). `install-ci` asks the core for a deploy pipeline: an emitter WRITES a
|
|
4
|
+
* deploy workflow for a CI system and REPORTS the repo secrets/vars the
|
|
5
|
+
* operator must set. GitHub Actions is the FIRST (and, in v1, only)
|
|
6
|
+
* implementation; other CI systems are Out of Scope but slot in behind this
|
|
7
|
+
* same seam later (spec "Out of Scope"; user stories 16, 17).
|
|
8
|
+
*
|
|
9
|
+
* This module is PURE: {@link emitCi} takes a plain input and returns the
|
|
10
|
+
* workflow file (path + contents) plus the secrets/vars report as data. It does
|
|
11
|
+
* NOT touch the filesystem or the network — the CLI wrapper (`cli-command-
|
|
12
|
+
* wrapper` task) is what writes the file to `.github/workflows/` and prints the
|
|
13
|
+
* report. Keeping the emit pure lets it be snapshot-tested as a string with no
|
|
14
|
+
* fixtures (test-first policy) and reused as a TypeScript API (CONTEXT.md `core
|
|
15
|
+
* vs cli`).
|
|
16
|
+
*
|
|
17
|
+
* BEHAVIOUR PORTED (not copied) from the reference GitHub Action
|
|
18
|
+
* `~/searches/ipfs-hetzner/github-workflow.yml`: the shape is checkout ->
|
|
19
|
+
* setup-node -> install -> build -> a `deploy` step reading the multi-node env
|
|
20
|
+
* contract (`IPFS_API`/`IPFS_TOKEN` comma-separated, publisher first),
|
|
21
|
+
* `SITE_NAME`, `SITE_MODE` -> a run-summary. Two deliberate CORRECTIONS vs the
|
|
22
|
+
* reference: (1) the deploy step invokes `pinnace deploy` (this tool now OWNS
|
|
23
|
+
* deploy) instead of a copied `deploy-car.mjs` script — one implementation, no
|
|
24
|
+
* bash/TS drift; (2) the same env contract carries the MULTI-NODE + per-site
|
|
25
|
+
* `mode` semantics (same CID to all nodes; the publisher publishes the record,
|
|
26
|
+
* replicas mirror) which the deploy core (`deploy-multi-target` +
|
|
27
|
+
* `publisher-replica-model` tasks) enforces — the workflow just passes the env
|
|
28
|
+
* through.
|
|
29
|
+
*/
|
|
30
|
+
/** The CI systems, in a stable order (help text / iteration / validation). */
|
|
31
|
+
export const CI_SYSTEMS = ['github'];
|
|
32
|
+
/**
|
|
33
|
+
* The multi-node + per-site-mode env contract, shared by the reported
|
|
34
|
+
* secrets/vars and the workflow's deploy step (single source so they never
|
|
35
|
+
* drift). Order is stable: publisher-first multi-node inputs, then the site
|
|
36
|
+
* name/mode. `IPFS_TOKEN` is the sole secret; the rest are plain vars.
|
|
37
|
+
*/
|
|
38
|
+
const IPFS_API_SETTING = {
|
|
39
|
+
name: 'IPFS_API',
|
|
40
|
+
kind: 'var',
|
|
41
|
+
multiNode: true,
|
|
42
|
+
description: 'Kubo RPC endpoint(s). One URL, or comma-separated for MULTIPLE nodes ' +
|
|
43
|
+
'(publisher first, replicas after). Same CID lands on every node.',
|
|
44
|
+
example: 'https://ipfs-a.you.com,https://ipfs-b.you.com',
|
|
45
|
+
};
|
|
46
|
+
const IPFS_TOKEN_SETTING = {
|
|
47
|
+
name: 'IPFS_TOKEN',
|
|
48
|
+
kind: 'secret',
|
|
49
|
+
multiNode: true,
|
|
50
|
+
description: 'Bearer token(s) guarding the RPC API. One token, or comma-separated ' +
|
|
51
|
+
'tokens in the SAME order as IPFS_API (publisher first).',
|
|
52
|
+
example: 'tokenA,tokenB',
|
|
53
|
+
};
|
|
54
|
+
const SITE_NAME_SETTING = {
|
|
55
|
+
name: 'SITE_NAME',
|
|
56
|
+
kind: 'var',
|
|
57
|
+
multiNode: false,
|
|
58
|
+
description: 'The site name / ENS name this deploy targets.',
|
|
59
|
+
example: 'mysite.eth',
|
|
60
|
+
};
|
|
61
|
+
const SITE_MODE_SETTING = {
|
|
62
|
+
name: 'SITE_MODE',
|
|
63
|
+
kind: 'var',
|
|
64
|
+
multiNode: false,
|
|
65
|
+
description: "Per-site mode: 'ipfs' (immutable, ENS ipfs://<cid> per deploy) or " +
|
|
66
|
+
"'ipns' (mutable, publisher publishes the record, replicas mirror). " +
|
|
67
|
+
"Defaults to 'ipns'.",
|
|
68
|
+
example: 'ipns',
|
|
69
|
+
};
|
|
70
|
+
/** All settings in report order (drives both the report split and validation). */
|
|
71
|
+
const ALL_SETTINGS = [
|
|
72
|
+
IPFS_API_SETTING,
|
|
73
|
+
IPFS_TOKEN_SETTING,
|
|
74
|
+
SITE_NAME_SETTING,
|
|
75
|
+
SITE_MODE_SETTING,
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Render the GitHub Actions deploy workflow. Ports the reference Action's shape
|
|
79
|
+
* (checkout -> setup-node -> install -> build -> deploy reading the env
|
|
80
|
+
* contract -> run summary), but the deploy step calls `pinnace deploy` (this
|
|
81
|
+
* tool owns deploy) and the env carries the multi-node + per-site-mode
|
|
82
|
+
* semantics the deploy core enforces. Deterministic: same input -> byte-
|
|
83
|
+
* identical output (snapshot-locked).
|
|
84
|
+
*/
|
|
85
|
+
function renderGithubWorkflow(input) {
|
|
86
|
+
const branch = input.branch ?? 'main';
|
|
87
|
+
const nodeVersion = input.nodeVersion ?? '20';
|
|
88
|
+
const buildCommand = input.buildCommand;
|
|
89
|
+
const outputDir = input.outputDir;
|
|
90
|
+
return `# =============================================================================
|
|
91
|
+
# pinnace: deploy a static site to your self-hosted IPFS node(s) on push.
|
|
92
|
+
#
|
|
93
|
+
# Generated by \`pinnace install-ci --system github\`. Edit BUILD/OUTPUT below
|
|
94
|
+
# for your stack; the deploy step is owned by the \`pinnace\` CLI.
|
|
95
|
+
#
|
|
96
|
+
# Set these in your repo (Settings -> Secrets and variables -> Actions):
|
|
97
|
+
# IPFS_API (variable) one URL, OR comma-separated for MULTIPLE nodes,
|
|
98
|
+
# publisher first (e.g. https://ipfs-a.you.com,https://ipfs-b.you.com)
|
|
99
|
+
# IPFS_TOKEN (secret) matching token(s), comma-separated in the SAME order
|
|
100
|
+
# SITE_NAME (variable) e.g. mysite.eth
|
|
101
|
+
# SITE_MODE (variable) ipfs | ipns (default ipns)
|
|
102
|
+
#
|
|
103
|
+
# Multiple nodes serve the SAME CID (redundancy). In ipns mode the PUBLISHER
|
|
104
|
+
# (first in IPFS_API) publishes the signed record; replicas mirror it.
|
|
105
|
+
# =============================================================================
|
|
106
|
+
name: Deploy to IPFS
|
|
107
|
+
|
|
108
|
+
on:
|
|
109
|
+
push:
|
|
110
|
+
branches: [${branch}]
|
|
111
|
+
workflow_dispatch: {}
|
|
112
|
+
|
|
113
|
+
concurrency:
|
|
114
|
+
group: pinnace-deploy
|
|
115
|
+
cancel-in-progress: true
|
|
116
|
+
|
|
117
|
+
jobs:
|
|
118
|
+
deploy:
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v4
|
|
122
|
+
|
|
123
|
+
- uses: actions/setup-node@v4
|
|
124
|
+
with:
|
|
125
|
+
node-version: ${nodeVersion}
|
|
126
|
+
cache: npm
|
|
127
|
+
|
|
128
|
+
- name: Install deps
|
|
129
|
+
run: npm ci
|
|
130
|
+
|
|
131
|
+
- name: Build
|
|
132
|
+
run: ${buildCommand}
|
|
133
|
+
|
|
134
|
+
- name: Deploy to IPFS node(s)
|
|
135
|
+
id: deploy
|
|
136
|
+
env:
|
|
137
|
+
IPFS_API: \${{ vars.IPFS_API }}
|
|
138
|
+
IPFS_TOKEN: \${{ secrets.IPFS_TOKEN }}
|
|
139
|
+
SITE_NAME: \${{ vars.SITE_NAME }}
|
|
140
|
+
SITE_MODE: \${{ vars.SITE_MODE || 'ipns' }}
|
|
141
|
+
run: |
|
|
142
|
+
npx pinnace deploy --mode "$SITE_MODE" "${outputDir}" "$SITE_NAME"
|
|
143
|
+
|
|
144
|
+
- name: Summary
|
|
145
|
+
if: always()
|
|
146
|
+
run: |
|
|
147
|
+
echo "### IPFS deploy" >> "$GITHUB_STEP_SUMMARY"
|
|
148
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
149
|
+
echo "- CID: \\\`\${{ steps.deploy.outputs.cid }}\\\`" >> "$GITHUB_STEP_SUMMARY"
|
|
150
|
+
echo "- IPNS: \\\`\${{ steps.deploy.outputs.ipns }}\\\`" >> "$GITHUB_STEP_SUMMARY"
|
|
151
|
+
echo "- Preview: https://\${{ steps.deploy.outputs.cid }}.ipfs.dweb.link/" >> "$GITHUB_STEP_SUMMARY"
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The GitHub Actions provider — the first implementation of {@link CIProvider}.
|
|
156
|
+
* Emits the deploy workflow to the conventional GitHub path and reports the
|
|
157
|
+
* secrets/vars split into the two GitHub settings panels.
|
|
158
|
+
*/
|
|
159
|
+
export const githubCiProvider = {
|
|
160
|
+
system: 'github',
|
|
161
|
+
emit(input) {
|
|
162
|
+
return {
|
|
163
|
+
system: 'github',
|
|
164
|
+
workflow: {
|
|
165
|
+
path: '.github/workflows/pinnace-deploy.yml',
|
|
166
|
+
contents: renderGithubWorkflow(input),
|
|
167
|
+
},
|
|
168
|
+
secrets: ALL_SETTINGS.filter((s) => s.kind === 'secret'),
|
|
169
|
+
vars: ALL_SETTINGS.filter((s) => s.kind === 'var'),
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
/** The provider registry (system -> provider). v1 has a single entry. */
|
|
174
|
+
const PROVIDERS = {
|
|
175
|
+
github: githubCiProvider,
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Emit a deploy pipeline for the requested CI system: dispatches to the
|
|
179
|
+
* matching {@link CIProvider} and returns its workflow + secrets/vars report.
|
|
180
|
+
* Throws LOUDLY on an unknown/unimplemented system (the seam exists so callers
|
|
181
|
+
* can add systems later, but v1 only ships `github`) — never a silent no-op.
|
|
182
|
+
*/
|
|
183
|
+
export function emitCi(input) {
|
|
184
|
+
const provider = PROVIDERS[input.system];
|
|
185
|
+
if (!provider) {
|
|
186
|
+
throw new Error(`unsupported CI system '${input.system}'; v1 emits only ${CI_SYSTEMS.join(', ')} (other systems are Out of Scope but the CIProvider seam exists)`);
|
|
187
|
+
}
|
|
188
|
+
return provider.emit(input);
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=ci-emit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ci-emit.js","sourceRoot":"","sources":["../../src/ci/ci-emit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,QAAQ,CAAC,CAAC;AA4E1D;;;;;GAKG;AACH,MAAM,gBAAgB,GAAsB;IAC3C,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,KAAK;IACX,SAAS,EAAE,IAAI;IACf,WAAW,EACV,uEAAuE;QACvE,kEAAkE;IACnE,OAAO,EAAE,+CAA+C;CACxD,CAAC;AAEF,MAAM,kBAAkB,GAAsB;IAC7C,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,QAAQ;IACd,SAAS,EAAE,IAAI;IACf,WAAW,EACV,sEAAsE;QACtE,yDAAyD;IAC1D,OAAO,EAAE,eAAe;CACxB,CAAC;AAEF,MAAM,iBAAiB,GAAsB;IAC5C,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,KAAK;IACX,SAAS,EAAE,KAAK;IAChB,WAAW,EAAE,+CAA+C;IAC5D,OAAO,EAAE,YAAY;CACrB,CAAC;AAEF,MAAM,iBAAiB,GAAsB;IAC5C,IAAI,EAAE,WAAW;IACjB,IAAI,EAAE,KAAK;IACX,SAAS,EAAE,KAAK;IAChB,WAAW,EACV,oEAAoE;QACpE,qEAAqE;QACrE,qBAAqB;IACtB,OAAO,EAAE,MAAM;CACf,CAAC;AAEF,kFAAkF;AAClF,MAAM,YAAY,GAAiC;IAClD,gBAAgB;IAChB,kBAAkB;IAClB,iBAAiB;IACjB,iBAAiB;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,KAAkB;IAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC;IACtC,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC;IAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;IACxC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAElC,OAAO;;;;;;;;;;;;;;;;;;;;iBAoBS,MAAM;;;;;;;;;;;;;;;0BAeG,WAAW;;;;;;;eAOtB,YAAY;;;;;;;;;;oDAUyB,SAAS;;;;;;;;;;CAU5D,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAe;IAC3C,MAAM,EAAE,QAAQ;IAChB,IAAI,CAAC,KAAkB;QACtB,OAAO;YACN,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE;gBACT,IAAI,EAAE,sCAAsC;gBAC5C,QAAQ,EAAE,oBAAoB,CAAC,KAAK,CAAC;aACrC;YACD,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC;YACxD,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC;SAClD,CAAC;IACH,CAAC;CACD,CAAC;AAEF,yEAAyE;AACzE,MAAM,SAAS,GAAiC;IAC/C,MAAM,EAAE,gBAAgB;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,KAAkB;IACxC,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACd,0BAA0B,KAAK,CAAC,MAAM,oBAAoB,UAAU,CAAC,IAAI,CACxE,IAAI,CACJ,kEAAkE,CACnE,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":""}
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `pinnace` CLI bin — a thin wrapper over the library core.
|
|
4
|
+
*
|
|
5
|
+
* It parses/formats only; ALL behaviour lives in the core (imported from
|
|
6
|
+
* `../index.js`). It wires the client verbs (provision, deploy, install-ci,
|
|
7
|
+
* status, derive) over the injectable `RunContext` seam in `./run.ts`; the
|
|
8
|
+
* on-box `pinnace node <verb>` subcommands share this same binary but are wired
|
|
9
|
+
* by their own task. Config/env/core are all injectable there so the dispatch
|
|
10
|
+
* is unit-tested without a process or the operator's real env/config.
|
|
11
|
+
*/
|
|
12
|
+
import { run } from './run.js';
|
|
13
|
+
run(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
14
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=bin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../../src/cli/bin.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AACH,OAAO,EAAC,GAAG,EAAC,MAAM,UAAU,CAAC;AAE7B,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC9B,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAC5B,CAAC,GAAG,EAAE,EAAE;IACP,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CACD,CAAC"}
|