@trackseries/cli 0.1.0-preview.1 → 0.1.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/README.md CHANGED
@@ -17,7 +17,7 @@ Before the first stable release, install the current preview with:
17
17
  npm install --global @trackseries/cli@next
18
18
  ```
19
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.
20
+ The first `trackseries` invocation downloads and checksum-verifies the matching release artifact for the current operating system and architecture. Later invocations reuse the versioned executable from the platform-specific TrackSeries data directory. npm installation does not execute lifecycle scripts.
21
21
 
22
22
  Supported targets:
23
23
 
@@ -34,10 +34,10 @@ After installation, authenticate and inspect the available commands:
34
34
  ```bash
35
35
  trackseries auth login
36
36
  trackseries auth status
37
- trackseries skill
37
+ trackseries --skill
38
38
  ```
39
39
 
40
- `trackseries skill` prints version-matched instructions designed for coding agents and other automated tools.
40
+ `trackseries --skill` prints version-matched instructions designed for coding agents and other automated tools.
41
41
 
42
42
  ## Updates
43
43
 
@@ -2,15 +2,13 @@
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync } from "node:fs";
5
- import { dirname, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
5
+ import { ensureBinary } from "../scripts/install.js";
7
6
 
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.");
7
+ let executablePath;
8
+ try {
9
+ executablePath = await ensureBinary();
10
+ } catch (error) {
11
+ console.error(error instanceof Error ? error.message : String(error));
14
12
  process.exit(1);
15
13
  }
16
14
 
@@ -19,6 +17,10 @@ const result = spawnSync(executablePath, process.argv.slice(2), {
19
17
  });
20
18
 
21
19
  if (result.error) {
20
+ if (process.platform === "linux" && result.error.code === "ENOENT" && existsSync(executablePath)) {
21
+ console.error("Unable to start the TrackSeries CLI. NixOS and other non-FHS distributions may require nix-ld.");
22
+ process.exit(1);
23
+ }
22
24
  console.error(`Unable to start the TrackSeries CLI: ${result.error.message}`);
23
25
  process.exit(1);
24
26
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@trackseries/cli",
3
- "version": "0.1.0-preview.1",
3
+ "version": "0.1.0",
4
4
  "description": "Install and run the TrackSeries CLI.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "trackseries": "./bin/trackseries.js"
7
+ "trackseries": "bin/trackseries.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/trackseries.js",
@@ -12,7 +12,6 @@
12
12
  "scripts/platform.js"
13
13
  ],
14
14
  "scripts": {
15
- "postinstall": "node scripts/install.js",
16
15
  "test": "node --test"
17
16
  },
18
17
  "engines": {
@@ -5,22 +5,12 @@ import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { unzipSync } from "fflate";
7
7
  import { x as extractTar } from "tar";
8
- import { assertSupportedRuntime, releaseArtifact } from "./platform.js";
8
+ import { assertSupportedRuntime, binaryDirectory, releaseArtifact } from "./platform.js";
9
9
 
10
10
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
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) {
12
+
13
+ async function download(url, timeout) {
24
14
  let lastError;
25
15
 
26
16
  for (let attempt = 1; attempt <= 3; attempt += 1) {
@@ -30,7 +20,7 @@ async function download(url) {
30
20
  headers: {
31
21
  "User-Agent": `@trackseries/cli/${metadata.version}`
32
22
  },
33
- signal: AbortSignal.timeout(60_000)
23
+ signal: AbortSignal.timeout(timeout)
34
24
  });
35
25
  } catch (error) {
36
26
  lastError = error;
@@ -53,58 +43,87 @@ async function download(url) {
53
43
  throw new Error(`Unable to download ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}.`);
54
44
  }
55
45
 
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}.`);
46
+ async function isRegularFile(path) {
47
+ try {
48
+ return (await lstat(path)).isFile();
49
+ } catch (error) {
50
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return false;
51
+ throw error;
66
52
  }
53
+ }
54
+
55
+ export async function ensureBinary() {
56
+ assertSupportedRuntime();
57
+ const artifact = releaseArtifact(metadata.version);
58
+ const releaseBaseUrl =
59
+ process.env.TRACKSERIES_CLI_RELEASE_BASE_URL ?? `https://github.com/TrackSeries/cli/releases/download/cli-v${metadata.version}`;
60
+ const executableName = process.platform === "win32" ? "trackseries.exe" : "trackseries";
61
+ const installedExecutable = resolve(binaryDirectory(metadata.version), executableName);
62
+
63
+ if (await isRegularFile(installedExecutable)) return installedExecutable;
67
64
 
68
- await mkdir(extractionPath, { recursive: true });
69
- await writeFile(archivePath, archive);
65
+ const temporaryDirectory = await mkdtemp(resolve(tmpdir(), "trackseries-cli-"));
66
+ const archivePath = resolve(temporaryDirectory, artifact);
67
+ const extractionPath = resolve(temporaryDirectory, "extracted");
68
+ const pendingExecutable = `${installedExecutable}.${process.pid}.tmp`;
70
69
 
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}.`);
70
+ console.error(`Downloading TrackSeries CLI ${metadata.version} for ${process.platform}-${process.arch}...`);
71
+
72
+ try {
73
+ const [archive, checksumFile] = await Promise.all([
74
+ download(`${releaseBaseUrl}/${artifact}`, 300_000),
75
+ download(`${releaseBaseUrl}/${artifact}.sha256`, 60_000)
76
+ ]);
77
+ const expectedChecksum = checksumFile.toString("utf8").trim().split(/\s+/)[0]?.toLowerCase();
78
+ const actualChecksum = createHash("sha256").update(archive).digest("hex");
79
+
80
+ if (!expectedChecksum || actualChecksum !== expectedChecksum) {
81
+ throw new Error(`Checksum verification failed for ${artifact}.`);
77
82
  }
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
83
 
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
- }
84
+ await mkdir(extractionPath, { recursive: true });
85
+ await writeFile(archivePath, archive);
91
86
 
92
- await mkdir(dirname(installedExecutable), { recursive: true });
93
- await copyFile(extractedExecutable, pendingExecutable);
87
+ if (process.platform === "win32") {
88
+ const executable = unzipSync(archive, {
89
+ filter: (file) => file.name === executableName
90
+ })[executableName];
91
+ if (!executable) {
92
+ throw new Error(`${artifact} does not contain ${executableName}.`);
93
+ }
94
+ await writeFile(resolve(extractionPath, executableName), executable);
95
+ } else {
96
+ await extractTar({
97
+ cwd: extractionPath,
98
+ file: archivePath,
99
+ filter: (path, entry) => (path === executableName || path === `./${executableName}`) && entry.type === "File"
100
+ });
101
+ }
94
102
 
95
- if (process.platform !== "win32") {
96
- await chmod(pendingExecutable, 0o755);
97
- } else {
98
- await rm(installedExecutable, { force: true });
99
- }
103
+ const extractedExecutable = resolve(extractionPath, executableName);
104
+ if (!(await lstat(extractedExecutable)).isFile()) {
105
+ throw new Error(`${artifact} does not contain a regular ${executableName} file.`);
106
+ }
100
107
 
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 });
108
+ await mkdir(dirname(installedExecutable), { recursive: true });
109
+ await copyFile(extractedExecutable, pendingExecutable);
110
+
111
+ if (process.platform !== "win32") {
112
+ await chmod(pendingExecutable, 0o755);
113
+ } else {
114
+ await rm(installedExecutable, { force: true });
115
+ }
116
+
117
+ await rename(pendingExecutable, installedExecutable);
118
+ return installedExecutable;
119
+ } catch (error) {
120
+ const reason = error instanceof Error ? error.message : String(error);
121
+ throw new Error(
122
+ `Unable to install TrackSeries CLI ${metadata.version}: ${reason} Download ${artifact} manually from ${releaseBaseUrl}.`,
123
+ { cause: error }
124
+ );
125
+ } finally {
126
+ await rm(pendingExecutable, { force: true });
127
+ await rm(temporaryDirectory, { force: true, recursive: true });
128
+ }
110
129
  }
@@ -1,3 +1,6 @@
1
+ import { homedir } from "node:os";
2
+ import { posix, win32 } from "node:path";
3
+
1
4
  export function releaseArtifact(version, platform = process.platform, architecture = process.arch) {
2
5
  const platformNames = {
3
6
  darwin: "macos",
@@ -20,3 +23,18 @@ export function assertSupportedRuntime(platform = process.platform, report = pro
20
23
  throw new Error("TrackSeries CLI requires a glibc-based Linux distribution. Alpine and other musl-based systems are not yet supported.");
21
24
  }
22
25
  }
26
+
27
+ export function binaryDirectory(version, platform = process.platform, environment = process.env, home = homedir()) {
28
+ const path = platform === "win32" ? win32 : posix;
29
+ let dataDirectory;
30
+
31
+ if (platform === "win32") {
32
+ dataDirectory = environment.LOCALAPPDATA ?? environment.APPDATA ?? path.join(home, "AppData", "Local");
33
+ } else if (platform === "darwin") {
34
+ dataDirectory = path.join(home, "Library", "Application Support");
35
+ } else {
36
+ dataDirectory = environment.XDG_DATA_HOME ?? path.join(home, ".local", "share");
37
+ }
38
+
39
+ return path.join(dataDirectory, "trackseries", "bin", version);
40
+ }