@ejklock/living-docs 0.0.0-stage → 0.26.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/README.md +34 -2
- package/bin/living-docs.js +18 -0
- package/lib/asset.js +117 -0
- package/package.json +24 -4
- package/scripts/postinstall.js +9 -0
package/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# living-docs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
npm wrapper, published as `@ejklock/living-docs` (the installed command stays `living-docs`), for the [`living-docs`](https://github.com/ejklock/living-docs-skill) CLI, the
|
|
4
|
+
deterministic layer of Living Docs authoring (`new`, `set`, `supersede`, `index`, `check`, `fmt`, `read`).
|
|
5
|
+
|
|
6
|
+
The package has no dependencies. It downloads the release binary for your platform from the
|
|
7
|
+
GitHub release matching the package version and verifies its sha256 before using it.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @ejklock/living-docs
|
|
13
|
+
living-docs --version
|
|
14
|
+
|
|
15
|
+
npx @ejklock/living-docs check --plain docs
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Supported platforms: macOS and Linux on x64 and arm64 (the four release targets
|
|
19
|
+
`aarch64-apple-darwin`, `x86_64-apple-darwin`, `x86_64-unknown-linux-gnu`,
|
|
20
|
+
`aarch64-unknown-linux-gnu`).
|
|
21
|
+
|
|
22
|
+
With `--ignore-scripts`, the binary is downloaded and verified on the first run instead.
|
|
23
|
+
|
|
24
|
+
## Environment
|
|
25
|
+
|
|
26
|
+
- `LIVING_DOCS_DOWNLOAD_BASE`: replaces `https://github.com/ejklock/living-docs-skill/releases/download/`
|
|
27
|
+
(a mirror holding `v<version>/living-docs-<target>` and its `.sha256`).
|
|
28
|
+
|
|
29
|
+
## Fallback
|
|
30
|
+
|
|
31
|
+
If the download or the checksum fails, install the binary with the script instead:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -fsSL https://raw.githubusercontent.com/ejklock/living-docs-skill/main/install.sh | bash
|
|
35
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
const { FALLBACK_HINT, ensureBinary } = require('../lib/asset');
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const binary = await ensureBinary();
|
|
9
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
|
|
10
|
+
if (result.error) throw result.error;
|
|
11
|
+
if (result.signal) process.kill(process.pid, result.signal);
|
|
12
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main().catch((error) => {
|
|
16
|
+
process.stderr.write(`living-docs: ${error.message}\n${FALLBACK_HINT}\n`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
package/lib/asset.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const VENDOR_DIR = path.join(PACKAGE_ROOT, 'vendor');
|
|
9
|
+
const BINARY_PATH = path.join(VENDOR_DIR, 'living-docs');
|
|
10
|
+
const DEFAULT_DOWNLOAD_BASE = 'https://github.com/ejklock/living-docs-skill/releases/download/';
|
|
11
|
+
|
|
12
|
+
const TARGETS = {
|
|
13
|
+
'darwin-arm64': 'aarch64-apple-darwin',
|
|
14
|
+
'darwin-x64': 'x86_64-apple-darwin',
|
|
15
|
+
'linux-x64': 'x86_64-unknown-linux-gnu',
|
|
16
|
+
'linux-arm64': 'aarch64-unknown-linux-gnu',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FALLBACK_HINT =
|
|
20
|
+
'Install the binary another way instead: curl -fsSL https://raw.githubusercontent.com/ejklock/living-docs-skill/main/install.sh | bash (or run ./install.sh from a checkout).';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The "<platform>-<arch>" key of the running host; LIVING_DOCS_PLATFORM overrides it.
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function platformKey() {
|
|
27
|
+
return process.env.LIVING_DOCS_PLATFORM || `${process.platform}-${process.arch}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The release target triple for the running host.
|
|
32
|
+
* @returns {string} the target triple
|
|
33
|
+
* @throws {Error} naming the four supported targets when the host is not one of them
|
|
34
|
+
*/
|
|
35
|
+
function resolveTarget() {
|
|
36
|
+
const key = platformKey();
|
|
37
|
+
const target = TARGETS[key];
|
|
38
|
+
if (target) return target;
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unsupported platform ${key}. Supported targets: ${Object.values(TARGETS).join(', ')}. ${FALLBACK_HINT}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @returns {string} the package version, which is the release tag without its "v" */
|
|
45
|
+
function packageVersion() {
|
|
46
|
+
return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The URL directory holding this version's release assets.
|
|
51
|
+
* LIVING_DOCS_DOWNLOAD_BASE replaces the GitHub releases location.
|
|
52
|
+
* @returns {string} ends with a slash
|
|
53
|
+
*/
|
|
54
|
+
function releaseUrl() {
|
|
55
|
+
const base = process.env.LIVING_DOCS_DOWNLOAD_BASE || DEFAULT_DOWNLOAD_BASE;
|
|
56
|
+
return `${base.replace(/\/*$/, '/')}v${packageVersion()}/`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchBytes(url) {
|
|
60
|
+
const response = await fetch(url);
|
|
61
|
+
if (!response.ok) throw new Error(`GET ${url} failed with HTTP ${response.status}`);
|
|
62
|
+
return Buffer.from(await response.arrayBuffer());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Throws unless the bytes hash to the digest in a `sha256sum`-style line ("<hex> <name>").
|
|
67
|
+
* @param {Buffer} bytes
|
|
68
|
+
* @param {string} checksumText
|
|
69
|
+
* @param {string} assetName
|
|
70
|
+
*/
|
|
71
|
+
function verifySha256(bytes, checksumText, assetName) {
|
|
72
|
+
const expected = checksumText.trim().split(/\s+/)[0].toLowerCase();
|
|
73
|
+
const actual = crypto.createHash('sha256').update(bytes).digest('hex');
|
|
74
|
+
if (expected !== actual) {
|
|
75
|
+
throw new Error(`Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeExecutableAtomically(bytes) {
|
|
80
|
+
fs.mkdirSync(VENDOR_DIR, { recursive: true });
|
|
81
|
+
const partial = path.join(VENDOR_DIR, `.living-docs.${process.pid}.partial`);
|
|
82
|
+
try {
|
|
83
|
+
fs.writeFileSync(partial, bytes, { mode: 0o755 });
|
|
84
|
+
fs.chmodSync(partial, 0o755);
|
|
85
|
+
fs.renameSync(partial, BINARY_PATH);
|
|
86
|
+
} finally {
|
|
87
|
+
fs.rmSync(partial, { force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Downloads the host's release asset, verifies its sha256 and moves it into place.
|
|
93
|
+
* Nothing is left on disk when any step fails.
|
|
94
|
+
* @returns {Promise<string>} the binary path
|
|
95
|
+
*/
|
|
96
|
+
async function download() {
|
|
97
|
+
const assetName = `living-docs-${resolveTarget()}`;
|
|
98
|
+
const url = `${releaseUrl()}${assetName}`;
|
|
99
|
+
const [bytes, checksum] = await Promise.all([
|
|
100
|
+
fetchBytes(url),
|
|
101
|
+
fetchBytes(`${url}.sha256`).then((buffer) => buffer.toString('utf8')),
|
|
102
|
+
]);
|
|
103
|
+
verifySha256(bytes, checksum, assetName);
|
|
104
|
+
writeExecutableAtomically(bytes);
|
|
105
|
+
return BINARY_PATH;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The binary path, downloading it first when it is missing.
|
|
110
|
+
* @returns {Promise<string>}
|
|
111
|
+
*/
|
|
112
|
+
async function ensureBinary() {
|
|
113
|
+
if (fs.existsSync(BINARY_PATH)) return BINARY_PATH;
|
|
114
|
+
return download();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { FALLBACK_HINT, TARGETS, BINARY_PATH, resolveTarget, releaseUrl, download, ensureBinary };
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ejklock/living-docs",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.26.1",
|
|
4
|
+
"description": "Deterministic CLI for living-docs authoring: ADRs, issues, research, indexes and checks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ejklock/living-docs-skill.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ejklock/living-docs-skill#readme",
|
|
11
|
+
"bin": {
|
|
12
|
+
"living-docs": "bin/living-docs.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node scripts/postinstall.js"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"lib",
|
|
23
|
+
"scripts",
|
|
24
|
+
"README.md"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { FALLBACK_HINT, download } = require('../lib/asset');
|
|
5
|
+
|
|
6
|
+
download().catch((error) => {
|
|
7
|
+
process.stderr.write(`living-docs: could not install the binary.\n${error.message}\n${FALLBACK_HINT}\n`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|