@trackseries/cli 0.1.0-preview.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 +60 -0
- package/bin/trackseries.js +30 -0
- package/package.json +36 -0
- package/scripts/install.js +110 -0
- package/scripts/platform.js +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# TrackSeries CLI
|
|
2
|
+
|
|
3
|
+
The TrackSeries CLI provides terminal and agent-friendly access to your TrackSeries account. This public repository contains the npm installer and downloadable releases; the application source is maintained separately.
|
|
4
|
+
|
|
5
|
+
## Install with npm
|
|
6
|
+
|
|
7
|
+
Node.js 20 or newer is required. Install the latest stable release with:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --global @trackseries/cli
|
|
11
|
+
trackseries auth login
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Before the first stable release, install the current preview with:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --global @trackseries/cli@next
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The npm installer downloads and checksum-verifies the matching release artifact for the current operating system and architecture. The installed `trackseries` command is a standalone executable and does not use Node.js while running.
|
|
21
|
+
|
|
22
|
+
Supported targets:
|
|
23
|
+
|
|
24
|
+
- glibc-based Linux x64 and ARM64
|
|
25
|
+
- macOS x64 and ARM64
|
|
26
|
+
- Windows x64 and ARM64
|
|
27
|
+
|
|
28
|
+
## Direct download
|
|
29
|
+
|
|
30
|
+
Archives and SHA-256 checksums are available from [GitHub Releases](https://github.com/TrackSeries/cli/releases).
|
|
31
|
+
|
|
32
|
+
After installation, authenticate and inspect the available commands:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
trackseries auth login
|
|
36
|
+
trackseries auth status
|
|
37
|
+
trackseries skill
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`trackseries skill` prints version-matched instructions designed for coding agents and other automated tools.
|
|
41
|
+
|
|
42
|
+
## Updates
|
|
43
|
+
|
|
44
|
+
Install the latest published version with:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install --global @trackseries/cli@latest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Preview releases are published under the npm `next` tag:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install --global @trackseries/cli@next
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Security
|
|
57
|
+
|
|
58
|
+
Authentication uses the OAuth device authorization flow. Credentials are stored in the platform-specific TrackSeries configuration directory and are never stored in this repository or the npm package.
|
|
59
|
+
|
|
60
|
+
Release archives are verified against their accompanying SHA-256 checksum before installation. The initial binaries are not code-signed, so Windows SmartScreen or macOS Gatekeeper may require manual approval.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const executableName = process.platform === "win32" ? "trackseries.exe" : "trackseries";
|
|
10
|
+
const executablePath = resolve(packageRoot, "vendor", executableName);
|
|
11
|
+
|
|
12
|
+
if (!existsSync(executablePath)) {
|
|
13
|
+
console.error("The TrackSeries CLI executable is missing. Reinstall @trackseries/cli.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = spawnSync(executablePath, process.argv.slice(2), {
|
|
18
|
+
stdio: "inherit"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result.error) {
|
|
22
|
+
console.error(`Unable to start the TrackSeries CLI: ${result.error.message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (result.signal) {
|
|
27
|
+
process.kill(process.pid, result.signal);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trackseries/cli",
|
|
3
|
+
"version": "0.1.0-preview.1",
|
|
4
|
+
"description": "Install and run the TrackSeries CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"trackseries": "./bin/trackseries.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/trackseries.js",
|
|
11
|
+
"scripts/install.js",
|
|
12
|
+
"scripts/platform.js"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node scripts/install.js",
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"fflate": "0.8.3",
|
|
23
|
+
"tar": "7.5.22"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/TrackSeries/cli.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/TrackSeries/cli#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/TrackSeries/cli/issues"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { unzipSync } from "fflate";
|
|
7
|
+
import { x as extractTar } from "tar";
|
|
8
|
+
import { assertSupportedRuntime, releaseArtifact } from "./platform.js";
|
|
9
|
+
|
|
10
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const metadata = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
12
|
+
assertSupportedRuntime();
|
|
13
|
+
const artifact = releaseArtifact(metadata.version);
|
|
14
|
+
const releaseBaseUrl =
|
|
15
|
+
process.env.TRACKSERIES_CLI_RELEASE_BASE_URL ?? `https://github.com/TrackSeries/cli/releases/download/cli-v${metadata.version}`;
|
|
16
|
+
const temporaryDirectory = await mkdtemp(resolve(tmpdir(), "trackseries-cli-"));
|
|
17
|
+
const archivePath = resolve(temporaryDirectory, artifact);
|
|
18
|
+
const extractionPath = resolve(temporaryDirectory, "extracted");
|
|
19
|
+
const executableName = process.platform === "win32" ? "trackseries.exe" : "trackseries";
|
|
20
|
+
const installedExecutable = resolve(packageRoot, "vendor", executableName);
|
|
21
|
+
const pendingExecutable = `${installedExecutable}.${process.pid}.tmp`;
|
|
22
|
+
|
|
23
|
+
async function download(url) {
|
|
24
|
+
let lastError;
|
|
25
|
+
|
|
26
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
27
|
+
let response;
|
|
28
|
+
try {
|
|
29
|
+
response = await fetch(url, {
|
|
30
|
+
headers: {
|
|
31
|
+
"User-Agent": `@trackseries/cli/${metadata.version}`
|
|
32
|
+
},
|
|
33
|
+
signal: AbortSignal.timeout(60_000)
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
lastError = error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (response?.ok) {
|
|
40
|
+
return Buffer.from(await response.arrayBuffer());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (response) {
|
|
44
|
+
lastError = new Error(`HTTP ${response.status}`);
|
|
45
|
+
if (response.status < 500 && response.status !== 408 && response.status !== 429) break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (attempt < 3) {
|
|
49
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, attempt * 1_000));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error(`Unable to download ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const [archive, checksumFile] = await Promise.all([
|
|
58
|
+
download(`${releaseBaseUrl}/${artifact}`),
|
|
59
|
+
download(`${releaseBaseUrl}/${artifact}.sha256`)
|
|
60
|
+
]);
|
|
61
|
+
const expectedChecksum = checksumFile.toString("utf8").trim().split(/\s+/)[0]?.toLowerCase();
|
|
62
|
+
const actualChecksum = createHash("sha256").update(archive).digest("hex");
|
|
63
|
+
|
|
64
|
+
if (!expectedChecksum || actualChecksum !== expectedChecksum) {
|
|
65
|
+
throw new Error(`Checksum verification failed for ${artifact}.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await mkdir(extractionPath, { recursive: true });
|
|
69
|
+
await writeFile(archivePath, archive);
|
|
70
|
+
|
|
71
|
+
if (process.platform === "win32") {
|
|
72
|
+
const executable = unzipSync(archive, {
|
|
73
|
+
filter: (file) => file.name === executableName
|
|
74
|
+
})[executableName];
|
|
75
|
+
if (!executable) {
|
|
76
|
+
throw new Error(`${artifact} does not contain ${executableName}.`);
|
|
77
|
+
}
|
|
78
|
+
await writeFile(resolve(extractionPath, executableName), executable);
|
|
79
|
+
} else {
|
|
80
|
+
await extractTar({
|
|
81
|
+
cwd: extractionPath,
|
|
82
|
+
file: archivePath,
|
|
83
|
+
filter: (path, entry) => (path === executableName || path === `./${executableName}`) && entry.type === "File"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const extractedExecutable = resolve(extractionPath, executableName);
|
|
88
|
+
if (!(await lstat(extractedExecutable)).isFile()) {
|
|
89
|
+
throw new Error(`${artifact} does not contain a regular ${executableName} file.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await mkdir(dirname(installedExecutable), { recursive: true });
|
|
93
|
+
await copyFile(extractedExecutable, pendingExecutable);
|
|
94
|
+
|
|
95
|
+
if (process.platform !== "win32") {
|
|
96
|
+
await chmod(pendingExecutable, 0o755);
|
|
97
|
+
} else {
|
|
98
|
+
await rm(installedExecutable, { force: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await rename(pendingExecutable, installedExecutable);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Unable to install TrackSeries CLI ${metadata.version}. Download ${artifact} manually from ${releaseBaseUrl}.`,
|
|
105
|
+
{ cause: error }
|
|
106
|
+
);
|
|
107
|
+
} finally {
|
|
108
|
+
await rm(pendingExecutable, { force: true });
|
|
109
|
+
await rm(temporaryDirectory, { force: true, recursive: true });
|
|
110
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function releaseArtifact(version, platform = process.platform, architecture = process.arch) {
|
|
2
|
+
const platformNames = {
|
|
3
|
+
darwin: "macos",
|
|
4
|
+
linux: "linux",
|
|
5
|
+
win32: "windows"
|
|
6
|
+
};
|
|
7
|
+
const architectures = new Set(["arm64", "x64"]);
|
|
8
|
+
const platformName = platformNames[platform];
|
|
9
|
+
|
|
10
|
+
if (!platformName || !architectures.has(architecture)) {
|
|
11
|
+
throw new Error(`TrackSeries CLI does not support ${platform}-${architecture}.`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const extension = platform === "win32" ? "zip" : "tar.gz";
|
|
15
|
+
return `trackseries-${version}-${platformName}-${architecture}.${extension}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function assertSupportedRuntime(platform = process.platform, report = process.report?.getReport()) {
|
|
19
|
+
if (platform === "linux" && !report?.header?.glibcVersionRuntime) {
|
|
20
|
+
throw new Error("TrackSeries CLI requires a glibc-based Linux distribution. Alpine and other musl-based systems are not yet supported.");
|
|
21
|
+
}
|
|
22
|
+
}
|