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.
Files changed (83) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +164 -0
  3. package/dist/car/car-build.d.ts +26 -0
  4. package/dist/car/car-build.d.ts.map +1 -0
  5. package/dist/car/car-build.js +113 -0
  6. package/dist/car/car-build.js.map +1 -0
  7. package/dist/ci/ci-emit.d.ts +116 -0
  8. package/dist/ci/ci-emit.d.ts.map +1 -0
  9. package/dist/ci/ci-emit.js +190 -0
  10. package/dist/ci/ci-emit.js.map +1 -0
  11. package/dist/cli/bin.d.ts +3 -0
  12. package/dist/cli/bin.d.ts.map +1 -0
  13. package/dist/cli/bin.js +17 -0
  14. package/dist/cli/bin.js.map +1 -0
  15. package/dist/cli/run.d.ts +94 -0
  16. package/dist/cli/run.d.ts.map +1 -0
  17. package/dist/cli/run.js +731 -0
  18. package/dist/cli/run.js.map +1 -0
  19. package/dist/config/config-resolution.d.ts +186 -0
  20. package/dist/config/config-resolution.d.ts.map +1 -0
  21. package/dist/config/config-resolution.js +137 -0
  22. package/dist/config/config-resolution.js.map +1 -0
  23. package/dist/deploy/deploy.d.ts +121 -0
  24. package/dist/deploy/deploy.d.ts.map +1 -0
  25. package/dist/deploy/deploy.js +150 -0
  26. package/dist/deploy/deploy.js.map +1 -0
  27. package/dist/derive/ipns-key-derivation.d.ts +37 -0
  28. package/dist/derive/ipns-key-derivation.d.ts.map +1 -0
  29. package/dist/derive/ipns-key-derivation.js +130 -0
  30. package/dist/derive/ipns-key-derivation.js.map +1 -0
  31. package/dist/index.d.ts +27 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +29 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/node/node-commands.d.ts +115 -0
  36. package/dist/node/node-commands.d.ts.map +1 -0
  37. package/dist/node/node-commands.js +231 -0
  38. package/dist/node/node-commands.js.map +1 -0
  39. package/dist/provision/cloud-init.d.ts +162 -0
  40. package/dist/provision/cloud-init.d.ts.map +1 -0
  41. package/dist/provision/cloud-init.js +460 -0
  42. package/dist/provision/cloud-init.js.map +1 -0
  43. package/dist/publisher/key-import.d.ts +105 -0
  44. package/dist/publisher/key-import.d.ts.map +1 -0
  45. package/dist/publisher/key-import.js +76 -0
  46. package/dist/publisher/key-import.js.map +1 -0
  47. package/dist/publisher/record-sequence.d.ts +90 -0
  48. package/dist/publisher/record-sequence.d.ts.map +1 -0
  49. package/dist/publisher/record-sequence.js +250 -0
  50. package/dist/publisher/record-sequence.js.map +1 -0
  51. package/dist/rpc/kubo-rpc-client.d.ts +154 -0
  52. package/dist/rpc/kubo-rpc-client.d.ts.map +1 -0
  53. package/dist/rpc/kubo-rpc-client.js +207 -0
  54. package/dist/rpc/kubo-rpc-client.js.map +1 -0
  55. package/dist/rpc/mock-kubo.d.ts +87 -0
  56. package/dist/rpc/mock-kubo.d.ts.map +1 -0
  57. package/dist/rpc/mock-kubo.js +129 -0
  58. package/dist/rpc/mock-kubo.js.map +1 -0
  59. package/dist/site/site-management.d.ts +126 -0
  60. package/dist/site/site-management.d.ts.map +1 -0
  61. package/dist/site/site-management.js +123 -0
  62. package/dist/site/site-management.js.map +1 -0
  63. package/dist/status/status-report.d.ts +145 -0
  64. package/dist/status/status-report.d.ts.map +1 -0
  65. package/dist/status/status-report.js +192 -0
  66. package/dist/status/status-report.js.map +1 -0
  67. package/package.json +45 -2
  68. package/src/car/car-build.ts +135 -0
  69. package/src/ci/ci-emit.ts +284 -0
  70. package/src/cli/bin.ts +20 -0
  71. package/src/cli/run.ts +970 -0
  72. package/src/config/config-resolution.ts +264 -0
  73. package/src/deploy/deploy.ts +250 -0
  74. package/src/derive/ipns-key-derivation.ts +173 -0
  75. package/src/index.ts +146 -0
  76. package/src/node/node-commands.ts +395 -0
  77. package/src/provision/cloud-init.ts +646 -0
  78. package/src/publisher/key-import.ts +141 -0
  79. package/src/publisher/record-sequence.ts +336 -0
  80. package/src/rpc/kubo-rpc-client.ts +281 -0
  81. package/src/rpc/mock-kubo.ts +194 -0
  82. package/src/site/site-management.ts +241 -0
  83. 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
+ );