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
|
@@ -0,0 +1,284 @@
|
|
|
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
|
+
|
|
31
|
+
/** The CI systems pinnace can emit for. v1 = GitHub Actions ONLY. */
|
|
32
|
+
export type CiSystem = 'github';
|
|
33
|
+
|
|
34
|
+
/** The CI systems, in a stable order (help text / iteration / validation). */
|
|
35
|
+
export const CI_SYSTEMS: readonly CiSystem[] = ['github'];
|
|
36
|
+
|
|
37
|
+
/** One required repo secret or variable the operator must set for the pipeline. */
|
|
38
|
+
export interface RequiredCiSetting {
|
|
39
|
+
/** The env/secret/var NAME as referenced in the workflow (e.g. `IPFS_API`). */
|
|
40
|
+
name: string;
|
|
41
|
+
/**
|
|
42
|
+
* `secret` (masked, e.g. tokens) or `var` (plain repo variable, e.g. URLs /
|
|
43
|
+
* names). GitHub stores these separately; the report tells the operator
|
|
44
|
+
* which panel each belongs in.
|
|
45
|
+
*/
|
|
46
|
+
kind: 'secret' | 'var';
|
|
47
|
+
/** A human-readable note (what it is, format, an example). */
|
|
48
|
+
description: string;
|
|
49
|
+
/**
|
|
50
|
+
* True when the value is a COMMA-SEPARATED list, one per target node with
|
|
51
|
+
* the PUBLISHER FIRST (the multi-node contract). `IPFS_API`/`IPFS_TOKEN` are
|
|
52
|
+
* multi-node; `SITE_NAME`/`SITE_MODE` are single values.
|
|
53
|
+
*/
|
|
54
|
+
multiNode: boolean;
|
|
55
|
+
/** An example value, shown in the report to make the format concrete. */
|
|
56
|
+
example: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Inputs to {@link emitCi}: which system + how the site builds. */
|
|
60
|
+
export interface EmitCiInput {
|
|
61
|
+
/** Which CI system to emit for (v1: `github`). */
|
|
62
|
+
system: CiSystem;
|
|
63
|
+
/** The build command that produces the static output dir (e.g. `npm run build`). */
|
|
64
|
+
buildCommand: string;
|
|
65
|
+
/** The directory the build outputs the static site to (e.g. `dist`). */
|
|
66
|
+
outputDir: string;
|
|
67
|
+
/** The branch to deploy on push (default `main`). */
|
|
68
|
+
branch?: string;
|
|
69
|
+
/** The Node.js version the workflow sets up (default `20`). */
|
|
70
|
+
nodeVersion?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A single emitted file: where it goes + what it contains. */
|
|
74
|
+
export interface EmittedFile {
|
|
75
|
+
/** The repo-relative path to write the file at. */
|
|
76
|
+
path: string;
|
|
77
|
+
/** The full file contents. */
|
|
78
|
+
contents: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What an emitter returns: the workflow file plus the secrets/vars the operator
|
|
83
|
+
* must set. `secrets` and `vars` are split so the CLI can tell the operator
|
|
84
|
+
* exactly which GitHub settings panel each value belongs in.
|
|
85
|
+
*/
|
|
86
|
+
export interface EmittedCi {
|
|
87
|
+
/** Which system this was emitted for (echoed for the caller). */
|
|
88
|
+
system: CiSystem;
|
|
89
|
+
/** The deploy workflow file (path + contents). */
|
|
90
|
+
workflow: EmittedFile;
|
|
91
|
+
/** The required masked SECRETS (Settings -> Secrets). */
|
|
92
|
+
secrets: RequiredCiSetting[];
|
|
93
|
+
/** The required repo VARIABLES (Settings -> Variables). */
|
|
94
|
+
vars: RequiredCiSetting[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The `CIProvider` seam: one method that emits a deploy pipeline + its required
|
|
99
|
+
* settings for a given input. v1 has a single implementation
|
|
100
|
+
* ({@link githubCiProvider}); adding GitLab CI / others later means adding
|
|
101
|
+
* another provider here and an entry in {@link CI_SYSTEMS} — deploy/publish
|
|
102
|
+
* logic is untouched (spec user story 21).
|
|
103
|
+
*/
|
|
104
|
+
export interface CIProvider {
|
|
105
|
+
/** The system this provider emits for. */
|
|
106
|
+
readonly system: CiSystem;
|
|
107
|
+
/** Emit the workflow + secrets/vars report for the given input. */
|
|
108
|
+
emit(input: EmitCiInput): EmittedCi;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The multi-node + per-site-mode env contract, shared by the reported
|
|
113
|
+
* secrets/vars and the workflow's deploy step (single source so they never
|
|
114
|
+
* drift). Order is stable: publisher-first multi-node inputs, then the site
|
|
115
|
+
* name/mode. `IPFS_TOKEN` is the sole secret; the rest are plain vars.
|
|
116
|
+
*/
|
|
117
|
+
const IPFS_API_SETTING: RequiredCiSetting = {
|
|
118
|
+
name: 'IPFS_API',
|
|
119
|
+
kind: 'var',
|
|
120
|
+
multiNode: true,
|
|
121
|
+
description:
|
|
122
|
+
'Kubo RPC endpoint(s). One URL, or comma-separated for MULTIPLE nodes ' +
|
|
123
|
+
'(publisher first, replicas after). Same CID lands on every node.',
|
|
124
|
+
example: 'https://ipfs-a.you.com,https://ipfs-b.you.com',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const IPFS_TOKEN_SETTING: RequiredCiSetting = {
|
|
128
|
+
name: 'IPFS_TOKEN',
|
|
129
|
+
kind: 'secret',
|
|
130
|
+
multiNode: true,
|
|
131
|
+
description:
|
|
132
|
+
'Bearer token(s) guarding the RPC API. One token, or comma-separated ' +
|
|
133
|
+
'tokens in the SAME order as IPFS_API (publisher first).',
|
|
134
|
+
example: 'tokenA,tokenB',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const SITE_NAME_SETTING: RequiredCiSetting = {
|
|
138
|
+
name: 'SITE_NAME',
|
|
139
|
+
kind: 'var',
|
|
140
|
+
multiNode: false,
|
|
141
|
+
description: 'The site name / ENS name this deploy targets.',
|
|
142
|
+
example: 'mysite.eth',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const SITE_MODE_SETTING: RequiredCiSetting = {
|
|
146
|
+
name: 'SITE_MODE',
|
|
147
|
+
kind: 'var',
|
|
148
|
+
multiNode: false,
|
|
149
|
+
description:
|
|
150
|
+
"Per-site mode: 'ipfs' (immutable, ENS ipfs://<cid> per deploy) or " +
|
|
151
|
+
"'ipns' (mutable, publisher publishes the record, replicas mirror). " +
|
|
152
|
+
"Defaults to 'ipns'.",
|
|
153
|
+
example: 'ipns',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** All settings in report order (drives both the report split and validation). */
|
|
157
|
+
const ALL_SETTINGS: readonly RequiredCiSetting[] = [
|
|
158
|
+
IPFS_API_SETTING,
|
|
159
|
+
IPFS_TOKEN_SETTING,
|
|
160
|
+
SITE_NAME_SETTING,
|
|
161
|
+
SITE_MODE_SETTING,
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render the GitHub Actions deploy workflow. Ports the reference Action's shape
|
|
166
|
+
* (checkout -> setup-node -> install -> build -> deploy reading the env
|
|
167
|
+
* contract -> run summary), but the deploy step calls `pinnace deploy` (this
|
|
168
|
+
* tool owns deploy) and the env carries the multi-node + per-site-mode
|
|
169
|
+
* semantics the deploy core enforces. Deterministic: same input -> byte-
|
|
170
|
+
* identical output (snapshot-locked).
|
|
171
|
+
*/
|
|
172
|
+
function renderGithubWorkflow(input: EmitCiInput): string {
|
|
173
|
+
const branch = input.branch ?? 'main';
|
|
174
|
+
const nodeVersion = input.nodeVersion ?? '20';
|
|
175
|
+
const buildCommand = input.buildCommand;
|
|
176
|
+
const outputDir = input.outputDir;
|
|
177
|
+
|
|
178
|
+
return `# =============================================================================
|
|
179
|
+
# pinnace: deploy a static site to your self-hosted IPFS node(s) on push.
|
|
180
|
+
#
|
|
181
|
+
# Generated by \`pinnace install-ci --system github\`. Edit BUILD/OUTPUT below
|
|
182
|
+
# for your stack; the deploy step is owned by the \`pinnace\` CLI.
|
|
183
|
+
#
|
|
184
|
+
# Set these in your repo (Settings -> Secrets and variables -> Actions):
|
|
185
|
+
# IPFS_API (variable) one URL, OR comma-separated for MULTIPLE nodes,
|
|
186
|
+
# publisher first (e.g. https://ipfs-a.you.com,https://ipfs-b.you.com)
|
|
187
|
+
# IPFS_TOKEN (secret) matching token(s), comma-separated in the SAME order
|
|
188
|
+
# SITE_NAME (variable) e.g. mysite.eth
|
|
189
|
+
# SITE_MODE (variable) ipfs | ipns (default ipns)
|
|
190
|
+
#
|
|
191
|
+
# Multiple nodes serve the SAME CID (redundancy). In ipns mode the PUBLISHER
|
|
192
|
+
# (first in IPFS_API) publishes the signed record; replicas mirror it.
|
|
193
|
+
# =============================================================================
|
|
194
|
+
name: Deploy to IPFS
|
|
195
|
+
|
|
196
|
+
on:
|
|
197
|
+
push:
|
|
198
|
+
branches: [${branch}]
|
|
199
|
+
workflow_dispatch: {}
|
|
200
|
+
|
|
201
|
+
concurrency:
|
|
202
|
+
group: pinnace-deploy
|
|
203
|
+
cancel-in-progress: true
|
|
204
|
+
|
|
205
|
+
jobs:
|
|
206
|
+
deploy:
|
|
207
|
+
runs-on: ubuntu-latest
|
|
208
|
+
steps:
|
|
209
|
+
- uses: actions/checkout@v4
|
|
210
|
+
|
|
211
|
+
- uses: actions/setup-node@v4
|
|
212
|
+
with:
|
|
213
|
+
node-version: ${nodeVersion}
|
|
214
|
+
cache: npm
|
|
215
|
+
|
|
216
|
+
- name: Install deps
|
|
217
|
+
run: npm ci
|
|
218
|
+
|
|
219
|
+
- name: Build
|
|
220
|
+
run: ${buildCommand}
|
|
221
|
+
|
|
222
|
+
- name: Deploy to IPFS node(s)
|
|
223
|
+
id: deploy
|
|
224
|
+
env:
|
|
225
|
+
IPFS_API: \${{ vars.IPFS_API }}
|
|
226
|
+
IPFS_TOKEN: \${{ secrets.IPFS_TOKEN }}
|
|
227
|
+
SITE_NAME: \${{ vars.SITE_NAME }}
|
|
228
|
+
SITE_MODE: \${{ vars.SITE_MODE || 'ipns' }}
|
|
229
|
+
run: |
|
|
230
|
+
npx pinnace deploy --mode "$SITE_MODE" "${outputDir}" "$SITE_NAME"
|
|
231
|
+
|
|
232
|
+
- name: Summary
|
|
233
|
+
if: always()
|
|
234
|
+
run: |
|
|
235
|
+
echo "### IPFS deploy" >> "$GITHUB_STEP_SUMMARY"
|
|
236
|
+
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
237
|
+
echo "- CID: \\\`\${{ steps.deploy.outputs.cid }}\\\`" >> "$GITHUB_STEP_SUMMARY"
|
|
238
|
+
echo "- IPNS: \\\`\${{ steps.deploy.outputs.ipns }}\\\`" >> "$GITHUB_STEP_SUMMARY"
|
|
239
|
+
echo "- Preview: https://\${{ steps.deploy.outputs.cid }}.ipfs.dweb.link/" >> "$GITHUB_STEP_SUMMARY"
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The GitHub Actions provider — the first implementation of {@link CIProvider}.
|
|
245
|
+
* Emits the deploy workflow to the conventional GitHub path and reports the
|
|
246
|
+
* secrets/vars split into the two GitHub settings panels.
|
|
247
|
+
*/
|
|
248
|
+
export const githubCiProvider: CIProvider = {
|
|
249
|
+
system: 'github',
|
|
250
|
+
emit(input: EmitCiInput): EmittedCi {
|
|
251
|
+
return {
|
|
252
|
+
system: 'github',
|
|
253
|
+
workflow: {
|
|
254
|
+
path: '.github/workflows/pinnace-deploy.yml',
|
|
255
|
+
contents: renderGithubWorkflow(input),
|
|
256
|
+
},
|
|
257
|
+
secrets: ALL_SETTINGS.filter((s) => s.kind === 'secret'),
|
|
258
|
+
vars: ALL_SETTINGS.filter((s) => s.kind === 'var'),
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** The provider registry (system -> provider). v1 has a single entry. */
|
|
264
|
+
const PROVIDERS: Record<CiSystem, CIProvider> = {
|
|
265
|
+
github: githubCiProvider,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Emit a deploy pipeline for the requested CI system: dispatches to the
|
|
270
|
+
* matching {@link CIProvider} and returns its workflow + secrets/vars report.
|
|
271
|
+
* Throws LOUDLY on an unknown/unimplemented system (the seam exists so callers
|
|
272
|
+
* can add systems later, but v1 only ships `github`) — never a silent no-op.
|
|
273
|
+
*/
|
|
274
|
+
export function emitCi(input: EmitCiInput): EmittedCi {
|
|
275
|
+
const provider = PROVIDERS[input.system];
|
|
276
|
+
if (!provider) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`unsupported CI system '${input.system}'; v1 emits only ${CI_SYSTEMS.join(
|
|
279
|
+
', ',
|
|
280
|
+
)} (other systems are Out of Scope but the CIProvider seam exists)`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return provider.emit(input);
|
|
284
|
+
}
|
package/src/cli/bin.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
|
|
14
|
+
run(process.argv.slice(2)).then(
|
|
15
|
+
(code) => process.exit(code),
|
|
16
|
+
(err) => {
|
|
17
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
},
|
|
20
|
+
);
|