patchline 1.0.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 ADDED
@@ -0,0 +1,60 @@
1
+ # Patchline
2
+
3
+ Streamline OpenCode plugin updates by inspecting config and cache state, pinning versions, and invalidating stale cache entries.
4
+
5
+ Patchline does not install plugins or run OpenCode. After running Patchline, launch OpenCode to reinstall or refresh plugins.
6
+
7
+ ## Install
8
+
9
+ ### Homebrew
10
+
11
+ ```
12
+ brew install AksharP5/tap/patchline
13
+ ```
14
+
15
+ ### From source
16
+
17
+ ```
18
+ go install github.com/AksharP5/Patchline/cmd/patchline@latest
19
+ ```
20
+
21
+ ### npm
22
+
23
+ ```
24
+ npm i -g patchline
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```
30
+ patchline list
31
+ patchline outdated
32
+ patchline sync
33
+ patchline upgrade <plugin> --to 1.2.3
34
+ patchline upgrade <plugin> --major|--minor|--patch
35
+ patchline upgrade --all --minor
36
+ patchline snapshot <plugin>
37
+ patchline rollback <plugin>
38
+ patchline version
39
+ ```
40
+
41
+ ## Common flags
42
+
43
+ - `--project <dir>`: project root to scan for `opencode.json`.
44
+ - `--global-config <file>`: override the global config path.
45
+ - `--cache-dir <dir>`: override the OpenCode plugin cache directory.
46
+ - `--snapshot-dir <dir>`: override where snapshots are stored.
47
+ - `--local-dir <dir>`: add an extra local plugin directory (repeatable).
48
+ - `--offline`: skip npm registry calls.
49
+
50
+ ## Status meanings
51
+
52
+ - `missing`: declared in config, but no cache entry was found.
53
+ - `mismatch`: cache entry exists but does not match the pinned version.
54
+ - `outdated`: installed version is behind the npm registry latest.
55
+ - `local/unmanaged`: plugin is a local file and not managed by npm.
56
+
57
+ ## Troubleshooting
58
+
59
+ - If plugins show as `missing`, run OpenCode to install them.
60
+ - If Patchline cannot find your config or cache, pass `--global-config` or `--cache-dir`.
package/bin/patchline ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawnSync } = require("node:child_process");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ const { getInstalledBinaryName, resolvePlatform } = require("../lib/installer");
8
+
9
+ function run() {
10
+ let goos;
11
+ try {
12
+ goos = resolvePlatform(process.platform);
13
+ } catch (error) {
14
+ console.error(error.message);
15
+ process.exit(1);
16
+ }
17
+
18
+ const binaryPath = path.join(__dirname, getInstalledBinaryName(goos));
19
+ if (!fs.existsSync(binaryPath)) {
20
+ console.error("Patchline binary not found. Reinstall the package to download it.");
21
+ process.exit(1);
22
+ }
23
+
24
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
25
+ stdio: "inherit",
26
+ });
27
+
28
+ if (result.error) {
29
+ console.error(result.error.message);
30
+ process.exit(1);
31
+ }
32
+
33
+ process.exit(result.status ?? 1);
34
+ }
35
+
36
+ run();
@@ -0,0 +1,139 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+
5
+ const platformMap = {
6
+ darwin: "darwin",
7
+ linux: "linux",
8
+ win32: "windows",
9
+ };
10
+
11
+ const archMap = {
12
+ x64: "amd64",
13
+ arm64: "arm64",
14
+ };
15
+
16
+ function resolvePlatform(platform) {
17
+ const resolved = platformMap[platform];
18
+ if (!resolved) {
19
+ throw new Error(`unsupported platform: ${platform}`);
20
+ }
21
+ return resolved;
22
+ }
23
+
24
+ function resolveArch(arch) {
25
+ const resolved = archMap[arch];
26
+ if (!resolved) {
27
+ throw new Error(`unsupported architecture: ${arch}`);
28
+ }
29
+ return resolved;
30
+ }
31
+
32
+ function getArchiveExtension(goos) {
33
+ if (goos === "windows") {
34
+ return "zip";
35
+ }
36
+ return "tar.gz";
37
+ }
38
+
39
+ function getArchiveName(version, goos, goarch) {
40
+ const ext = getArchiveExtension(goos);
41
+ return `patchline_${version}_${goos}_${goarch}.${ext}`;
42
+ }
43
+
44
+ function getDownloadUrl(version, goos, goarch) {
45
+ const archiveName = getArchiveName(version, goos, goarch);
46
+ return `https://github.com/AksharP5/Patchline/releases/download/v${version}/${archiveName}`;
47
+ }
48
+
49
+ function getChecksumsUrl(version) {
50
+ return `https://github.com/AksharP5/Patchline/releases/download/v${version}/checksums.txt`;
51
+ }
52
+
53
+ function getBinaryName(goos) {
54
+ if (goos === "windows") {
55
+ return "patchline.exe";
56
+ }
57
+ return "patchline";
58
+ }
59
+
60
+ function getInstalledBinaryName(goos) {
61
+ if (goos === "windows") {
62
+ return "patchline-bin.exe";
63
+ }
64
+ return "patchline-bin";
65
+ }
66
+
67
+ function getCandidateBinaryPaths(rootDir, version, goos, goarch) {
68
+ const binaryName = getBinaryName(goos);
69
+ return [
70
+ path.join(rootDir, binaryName),
71
+ path.join(rootDir, `patchline_${version}_${goos}_${goarch}`, binaryName),
72
+ ];
73
+ }
74
+
75
+ function parseChecksums(text) {
76
+ const result = {};
77
+ for (const line of text.split(/\r?\n/)) {
78
+ const trimmed = line.trim();
79
+ if (!trimmed) {
80
+ continue;
81
+ }
82
+ const parts = trimmed.split(/\s+/);
83
+ if (parts.length < 2) {
84
+ continue;
85
+ }
86
+ const hash = parts[0];
87
+ const filename = parts[1].replace(/^\*?\.\//, "").replace(/^\*/, "");
88
+ result[filename] = hash;
89
+ }
90
+ return result;
91
+ }
92
+
93
+ function calculateSha256(filePath) {
94
+ return new Promise((resolve, reject) => {
95
+ const hash = crypto.createHash("sha256");
96
+ const stream = fs.createReadStream(filePath);
97
+
98
+ stream.on("data", (chunk) => {
99
+ hash.update(chunk);
100
+ });
101
+ stream.on("end", () => resolve(hash.digest("hex")));
102
+ stream.on("error", reject);
103
+ });
104
+ }
105
+
106
+ async function verifyChecksum(checksums, filename, filePath) {
107
+ const expected = checksums[filename];
108
+ if (!expected) {
109
+ throw new Error(`checksum not found for ${filename}`);
110
+ }
111
+ const actual = await calculateSha256(filePath);
112
+ if (actual !== expected) {
113
+ throw new Error(`checksum mismatch for ${filename}`);
114
+ }
115
+ }
116
+
117
+ function shouldSkipDownload(env) {
118
+ const flag = env.PATCHLINE_SKIP_DOWNLOAD;
119
+ if (!flag) {
120
+ return false;
121
+ }
122
+ const normalized = flag.toLowerCase();
123
+ return normalized === "1" || normalized === "true";
124
+ }
125
+
126
+ module.exports = {
127
+ resolvePlatform,
128
+ resolveArch,
129
+ getArchiveExtension,
130
+ getArchiveName,
131
+ getDownloadUrl,
132
+ getChecksumsUrl,
133
+ getBinaryName,
134
+ getInstalledBinaryName,
135
+ getCandidateBinaryPaths,
136
+ parseChecksums,
137
+ verifyChecksum,
138
+ shouldSkipDownload,
139
+ };
@@ -0,0 +1,32 @@
1
+ const fs = require("node:fs");
2
+
3
+ const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
4
+
5
+ function normalizeTag(tag) {
6
+ if (!tag) {
7
+ throw new Error("version tag is required");
8
+ }
9
+ const trimmed = tag.trim();
10
+ const version = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
11
+ if (!version) {
12
+ throw new Error("version tag is required");
13
+ }
14
+ if (!semverPattern.test(version)) {
15
+ throw new Error(`invalid semver: ${version}`);
16
+ }
17
+ return version;
18
+ }
19
+
20
+ function updatePackageVersion(packagePath, tag) {
21
+ const version = normalizeTag(tag);
22
+ const raw = fs.readFileSync(packagePath, "utf8");
23
+ const pkg = JSON.parse(raw);
24
+ pkg.version = version;
25
+ fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
26
+ return version;
27
+ }
28
+
29
+ module.exports = {
30
+ normalizeTag,
31
+ updatePackageVersion,
32
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "patchline",
3
+ "version": "1.0.1",
4
+ "description": "Streamline OpenCode plugin updates.",
5
+ "license": "UNLICENSED",
6
+ "bin": {
7
+ "patchline": "bin/patchline"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node scripts/install.js",
11
+ "test": "node --test test/*.test.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "scripts",
16
+ "lib",
17
+ "README.md",
18
+ "package.json"
19
+ ],
20
+ "dependencies": {
21
+ "extract-zip": "^2.0.1",
22
+ "tar": "^6.2.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/AksharP5/Patchline.git"
30
+ },
31
+ "homepage": "https://github.com/AksharP5/Patchline"
32
+ }
@@ -0,0 +1,133 @@
1
+ const fs = require("node:fs");
2
+ const fsPromises = require("node:fs/promises");
3
+ const https = require("node:https");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { pipeline } = require("node:stream/promises");
7
+
8
+ const extractZip = require("extract-zip");
9
+ const tar = require("tar");
10
+
11
+ const {
12
+ getArchiveExtension,
13
+ getArchiveName,
14
+ getBinaryName,
15
+ getCandidateBinaryPaths,
16
+ getChecksumsUrl,
17
+ getDownloadUrl,
18
+ getInstalledBinaryName,
19
+ parseChecksums,
20
+ resolveArch,
21
+ resolvePlatform,
22
+ shouldSkipDownload,
23
+ verifyChecksum,
24
+ } = require("../lib/installer");
25
+
26
+ const packageJson = require("../package.json");
27
+
28
+ function request(url) {
29
+ return new Promise((resolve, reject) => {
30
+ const req = https.get(url, (res) => {
31
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
32
+ res.resume();
33
+ resolve(request(res.headers.location));
34
+ return;
35
+ }
36
+
37
+ if (res.statusCode !== 200) {
38
+ res.resume();
39
+ reject(new Error(`download failed: ${res.statusCode} ${res.statusMessage}`));
40
+ return;
41
+ }
42
+
43
+ resolve(res);
44
+ });
45
+
46
+ req.on("error", reject);
47
+ });
48
+ }
49
+
50
+ async function downloadFile(url, destination) {
51
+ const response = await request(url);
52
+ await pipeline(response, fs.createWriteStream(destination));
53
+ }
54
+
55
+ async function downloadText(url) {
56
+ const response = await request(url);
57
+ const chunks = [];
58
+ for await (const chunk of response) {
59
+ chunks.push(chunk);
60
+ }
61
+ return Buffer.concat(chunks).toString("utf8");
62
+ }
63
+
64
+ async function extractArchive(archivePath, extractDir, extension) {
65
+ if (extension === "zip") {
66
+ await extractZip(archivePath, { dir: extractDir });
67
+ return;
68
+ }
69
+ await tar.x({ file: archivePath, cwd: extractDir });
70
+ }
71
+
72
+ async function findExistingPath(paths) {
73
+ for (const candidate of paths) {
74
+ try {
75
+ await fsPromises.access(candidate);
76
+ return candidate;
77
+ } catch (error) {
78
+ continue;
79
+ }
80
+ }
81
+ return "";
82
+ }
83
+
84
+ async function install() {
85
+ if (shouldSkipDownload(process.env)) {
86
+ console.log("Skipping Patchline download because PATCHLINE_SKIP_DOWNLOAD is set.");
87
+ return;
88
+ }
89
+
90
+ const version = packageJson.version;
91
+ const goos = resolvePlatform(process.platform);
92
+ const goarch = resolveArch(process.arch);
93
+ const extension = getArchiveExtension(goos);
94
+ const archiveName = getArchiveName(version, goos, goarch);
95
+ const url = getDownloadUrl(version, goos, goarch);
96
+ const checksumsUrl = getChecksumsUrl(version);
97
+
98
+ const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), "patchline-"));
99
+ const archivePath = path.join(tempRoot, archiveName);
100
+ const extractDir = path.join(tempRoot, "extract");
101
+
102
+ await fsPromises.mkdir(extractDir, { recursive: true });
103
+ await downloadFile(url, archivePath);
104
+
105
+ const checksumsText = await downloadText(checksumsUrl);
106
+ const checksums = parseChecksums(checksumsText);
107
+ await verifyChecksum(checksums, archiveName, archivePath);
108
+
109
+ await extractArchive(archivePath, extractDir, extension);
110
+
111
+ const binaryName = getBinaryName(goos);
112
+ const candidates = getCandidateBinaryPaths(extractDir, version, goos, goarch);
113
+ const extractedBinaryPath = await findExistingPath(candidates);
114
+
115
+ if (!extractedBinaryPath) {
116
+ throw new Error(`binary ${binaryName} not found in archive`);
117
+ }
118
+
119
+ const installedBinaryName = getInstalledBinaryName(goos);
120
+ const targetPath = path.join(__dirname, "..", "bin", installedBinaryName);
121
+
122
+ await fsPromises.mkdir(path.dirname(targetPath), { recursive: true });
123
+ await fsPromises.copyFile(extractedBinaryPath, targetPath);
124
+
125
+ if (goos !== "windows") {
126
+ await fsPromises.chmod(targetPath, 0o755);
127
+ }
128
+ }
129
+
130
+ install().catch((error) => {
131
+ console.error(error.message);
132
+ process.exit(1);
133
+ });
@@ -0,0 +1,17 @@
1
+ const path = require("node:path");
2
+
3
+ const { updatePackageVersion } = require("../lib/npm-version");
4
+
5
+ const tag = process.env.NPM_VERSION || process.env.GITHUB_REF_NAME;
6
+ if (!tag) {
7
+ console.error("NPM_VERSION or GITHUB_REF_NAME is required to set the npm version.");
8
+ process.exit(1);
9
+ }
10
+
11
+ try {
12
+ const version = updatePackageVersion(path.join(__dirname, "..", "package.json"), tag);
13
+ console.log(`Prepared npm package version ${version}.`);
14
+ } catch (error) {
15
+ console.error(error.message);
16
+ process.exit(1);
17
+ }