@workrail/cli 0.5.9 → 0.6.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
@@ -36,43 +36,43 @@ The CLI automatically downloads the correct binary for your platform during inst
36
36
 
37
37
  ## Quick Start
38
38
 
39
- 1. **Authenticate** (opens browser):
39
+ 1. **Run guided setup**:
40
40
 
41
41
  ```bash
42
- workrail auth login
42
+ workrail setup
43
43
  ```
44
44
 
45
- 2. **Link your repository** to a Workrail project:
45
+ On a new installation, the same guided flow starts automatically in an
46
+ interactive terminal. It connects your account, links repositories to your
47
+ Default Project, offers optional GitHub context and autosync, and runs your
48
+ first sync.
46
49
 
47
- ```bash
48
- workrail link
49
- ```
50
-
51
- 3. **Sync your work** (captures commit metadata, not source code):
50
+ 2. **Sync your work** (captures metadata, not source code):
52
51
 
53
52
  ```bash
54
53
  workrail sync
55
54
  ```
56
55
 
57
- 4. **Create an entry manually** (for non-code achievements):
56
+ 3. **Create an entry manually** (for non-code achievements):
58
57
 
59
58
  ```bash
60
59
  workrail entry add
61
60
  ```
62
61
 
63
- 5. **Enable autosync** (optional - runs sync on a schedule):
62
+ 4. **Enable autosync** (optional - runs sync on a schedule):
64
63
 
65
64
  ```bash
66
65
  workrail autosync enable
67
66
  ```
68
67
 
69
- 6. **View your dashboard** at [workrail.dev](https://workrail.dev)
68
+ 5. **View your dashboard** at [workrail.dev](https://workrail.dev)
70
69
 
71
70
  ## Common Commands
72
71
 
73
72
  | Command | Description |
74
73
  | --------------------------- | --------------------------------------- |
75
74
  | `workrail auth login` | Authenticate via browser OAuth |
75
+ | `workrail setup` | Run or revisit guided onboarding |
76
76
  | `workrail auth status` | Check authentication status |
77
77
  | `workrail link` | Link current repo to a Workrail project |
78
78
  | `workrail sync` | Sync commits and create entries |
@@ -86,7 +86,15 @@ The CLI automatically downloads the correct binary for your platform during inst
86
86
 
87
87
  ## Privacy
88
88
 
89
- Workrail syncs **metadata only** (commit hashes, timestamps, messages). Your source code never leaves your machine. [Read more about our privacy practices](https://workrail.dev/privacy).
89
+ Workrail syncs **metadata only** (commit hashes, timestamps, messages). Your
90
+ source code never leaves your machine.
91
+
92
+ Optional GitHub context uses your locally authenticated GitHub CLI to add
93
+ repository-scoped PR and issue titles, descriptions, labels, dates, links, and
94
+ deterministic commit relationships. It does not request source, diffs, comments,
95
+ reviews, checks, or authentication tokens.
96
+
97
+ [Read more about our privacy practices](https://workrail.dev/privacy).
90
98
 
91
99
  ## Updating
92
100
 
package/bin/workrail.js CHANGED
@@ -3,8 +3,11 @@
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
5
  const https = require("node:https");
6
- const crypto = require("node:crypto");
7
6
  const { spawn } = require("node:child_process");
7
+ const {
8
+ loadPackagedChecksums,
9
+ verifyArtifact,
10
+ } = require("../lib/integrity.cjs");
8
11
 
9
12
  function resolveBinaryName() {
10
13
  const platform = process.platform;
@@ -19,6 +22,10 @@ function resolveBinaryName() {
19
22
  return null;
20
23
  }
21
24
 
25
+ function resolveHelperBinaryName(binaryName) {
26
+ return binaryName.replace(/^workrail-/, "workrail-gh-collector-");
27
+ }
28
+
22
29
  const binName = resolveBinaryName();
23
30
  if (!binName) {
24
31
  console.error(`Unsupported platform: ${process.platform} ${process.arch}`);
@@ -26,6 +33,14 @@ if (!binName) {
26
33
  }
27
34
 
28
35
  const binPath = path.join(__dirname, "native", binName);
36
+ const helperName = resolveHelperBinaryName(binName);
37
+ const helperPath = path.join(
38
+ __dirname,
39
+ "native",
40
+ process.platform === "win32"
41
+ ? "workrail-gh-collector.exe"
42
+ : "workrail-gh-collector",
43
+ );
29
44
 
30
45
  function getPackageVersion() {
31
46
  const pkgPath = path.join(__dirname, "..", "package.json");
@@ -39,24 +54,6 @@ function getPackageVersion() {
39
54
  return null;
40
55
  }
41
56
 
42
- function sha256File(filePath) {
43
- const hash = crypto.createHash("sha256");
44
- hash.update(fs.readFileSync(filePath));
45
- return hash.digest("hex");
46
- }
47
-
48
- function parseChecksums(text) {
49
- const map = new Map();
50
- for (const line of text.split("\n")) {
51
- const trimmed = line.trim();
52
- if (!trimmed) continue;
53
- const parts = trimmed.split(/\s+/);
54
- if (parts.length < 2) continue;
55
- map.set(parts[parts.length - 1], parts[0]);
56
- }
57
- return map;
58
- }
59
-
60
57
  function download(url) {
61
58
  return new Promise((resolve, reject) => {
62
59
  https
@@ -82,8 +79,11 @@ function download(url) {
82
79
  });
83
80
  }
84
81
 
85
- async function ensureBinaryInstalled(binaryPath, binaryName) {
86
- if (fs.existsSync(binaryPath)) return;
82
+ async function ensureBinaryInstalled(binaryPath, binaryName, checksums) {
83
+ if (fs.existsSync(binaryPath)) {
84
+ verifyArtifact(binaryPath, binaryName, checksums);
85
+ return;
86
+ }
87
87
 
88
88
  const version = getPackageVersion();
89
89
  if (!version) {
@@ -96,7 +96,6 @@ async function ensureBinaryInstalled(binaryPath, binaryName) {
96
96
  process.env.WORKRAIL_BINARY_BASE_URL ||
97
97
  `https://cli.workrail.dev/workrail/v${version}`;
98
98
  const binUrl = `${baseUrl}/${binaryName}`;
99
- const checksumsUrl = `${baseUrl}/checksums.txt`;
100
99
 
101
100
  fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
102
101
  console.error(
@@ -104,27 +103,18 @@ async function ensureBinaryInstalled(binaryPath, binaryName) {
104
103
  );
105
104
 
106
105
  const binaryBytes = await download(binUrl);
107
- fs.writeFileSync(binaryPath, binaryBytes);
106
+ const temporaryPath = `${binaryPath}.download`;
107
+ fs.writeFileSync(temporaryPath, binaryBytes, { mode: 0o600 });
108
108
 
109
109
  try {
110
- const checksumsText = (await download(checksumsUrl)).toString("utf8");
111
- const checksums = parseChecksums(checksumsText);
112
- const expected = checksums.get(binaryName);
113
- if (!expected) {
114
- throw new Error(`No checksum entry found for ${binaryName}`);
115
- }
116
- const actual = sha256File(binaryPath);
117
- if (actual !== expected) {
118
- throw new Error(
119
- `Checksum mismatch for ${binaryName}. expected=${expected} actual=${actual}`,
120
- );
121
- }
110
+ verifyArtifact(temporaryPath, binaryName, checksums);
111
+ fs.renameSync(temporaryPath, binaryPath);
122
112
  if (process.platform !== "win32") {
123
113
  fs.chmodSync(binaryPath, 0o755);
124
114
  }
125
115
  } catch (error) {
126
116
  try {
127
- fs.unlinkSync(binaryPath);
117
+ fs.unlinkSync(temporaryPath);
128
118
  } catch {}
129
119
  throw error;
130
120
  }
@@ -140,7 +130,9 @@ function printInstallHelp() {
140
130
 
141
131
  async function main() {
142
132
  try {
143
- await ensureBinaryInstalled(binPath, binName);
133
+ const checksums = loadPackagedChecksums(path.join(__dirname, ".."));
134
+ await ensureBinaryInstalled(binPath, binName, checksums);
135
+ await ensureBinaryInstalled(helperPath, helperName, checksums);
144
136
  } catch (error) {
145
137
  console.error(
146
138
  `[workrail] Failed to install native binary: ${error instanceof Error ? error.message : String(error)}`,
@@ -154,7 +146,13 @@ async function main() {
154
146
  process.exit(1);
155
147
  }
156
148
 
157
- const child = spawn(binPath, process.argv.slice(2), { stdio: "inherit" });
149
+ const child = spawn(binPath, process.argv.slice(2), {
150
+ stdio: "inherit",
151
+ env: {
152
+ ...process.env,
153
+ PATH: `${path.dirname(helperPath)}${path.delimiter}${process.env.PATH || ""}`,
154
+ },
155
+ });
158
156
 
159
157
  child.on("error", (err) => {
160
158
  console.error(`Failed to run Workrail binary: ${err.message}`);
@@ -0,0 +1,59 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+
5
+ function parseChecksums(text) {
6
+ const checksums = new Map();
7
+ for (const [index, line] of text.split("\n").entries()) {
8
+ const trimmed = line.trim();
9
+ if (!trimmed) continue;
10
+ const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?([^/\\]+)$/);
11
+ if (!match) {
12
+ throw new Error(`Invalid checksum manifest line ${index + 1}`);
13
+ }
14
+ checksums.set(match[2], match[1].toLowerCase());
15
+ }
16
+ if (checksums.size === 0) throw new Error("Checksum manifest is empty");
17
+ return checksums;
18
+ }
19
+
20
+ function loadPackagedChecksums(packageRoot) {
21
+ const manifestPath = path.join(packageRoot, "release-checksums.txt");
22
+ try {
23
+ return parseChecksums(fs.readFileSync(manifestPath, "utf8"));
24
+ } catch (error) {
25
+ throw new Error(
26
+ `Trusted release checksum manifest is unavailable: ${error instanceof Error ? error.message : String(error)}`,
27
+ );
28
+ }
29
+ }
30
+
31
+ function sha256File(filePath) {
32
+ const hash = crypto.createHash("sha256");
33
+ hash.update(fs.readFileSync(filePath));
34
+ return hash.digest("hex");
35
+ }
36
+
37
+ function verifyArtifact(filePath, artifactName, checksums) {
38
+ const expected = checksums.get(artifactName);
39
+ if (!expected) throw new Error(`No trusted checksum for ${artifactName}`);
40
+ const actual = sha256File(filePath);
41
+ if (actual !== expected) {
42
+ throw new Error(
43
+ `Checksum mismatch for ${artifactName}. expected=${expected} actual=${actual}`,
44
+ );
45
+ }
46
+ }
47
+
48
+ function verifyRequiredArtifacts(distRoot, artifactNames, checksums) {
49
+ for (const artifactName of artifactNames) {
50
+ verifyArtifact(path.join(distRoot, artifactName), artifactName, checksums);
51
+ }
52
+ }
53
+
54
+ module.exports = {
55
+ loadPackagedChecksums,
56
+ parseChecksums,
57
+ verifyArtifact,
58
+ verifyRequiredArtifacts,
59
+ };
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "@workrail/cli",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "Workrail CLI - Track your engineering work and build a career record",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
7
7
  "workrail": "bin/workrail.js"
8
8
  },
9
9
  "scripts": {
10
+ "prepack": "node ./scripts/prepare-release.mjs",
10
11
  "postinstall": "node ./scripts/postinstall.mjs"
11
12
  },
12
13
  "files": [
13
14
  "bin",
15
+ "lib",
14
16
  "scripts",
17
+ "release-checksums.txt",
15
18
  "README.md"
16
19
  ],
17
20
  "publishConfig": {
@@ -0,0 +1,10 @@
1
+ 6b5aa61e2bb1733a3b5c8a66684586c015c32c565f77ea51b6273c7d66d89c25 workrail-darwin-arm64
2
+ 20325c936a2032d5ee35fa02357e232ecd67c246e670319f78e980d3ea6ab0ac workrail-darwin-x64
3
+ 2a51c9e382cb0c184dd42fabcc72e7e6d6589657934b73f8154191d19afcb0c3 workrail-gh-collector-darwin-arm64
4
+ 3393e24280097c2430d088d8c4c0407f0d7c912cdade416652b0e55b3c617737 workrail-gh-collector-darwin-x64
5
+ cd98378b00252c1a3337a402961ddb80f76a4c826b7b40c45b5e76dde32e58cb workrail-gh-collector-linux-arm64
6
+ 4f2d59e4e95d59d22cfb4ea6fc1533d9fde9b5964fa6eae91d0ab1b5857b5c14 workrail-gh-collector-linux-x64
7
+ 315139c4108cb6ae327fb57fecc6f1a0f4fdc5317c92674f5c189cef4ba03ee5 workrail-gh-collector-windows-x64.exe
8
+ 9b039c7d533c722f1230a9391015ec9daed98649d59bb85365003edbcccaadb0 workrail-linux-arm64
9
+ d10d8af7660c2f92c5a69669023685086d431f0fabb95c5ba6e375f9b8748be9 workrail-linux-x64
10
+ 2402bafae25d263bf1a3aa667a124b07fe35a1857f2d45b0550b9e6ea8d35584 workrail-windows-x64.exe
@@ -1,16 +1,40 @@
1
- import crypto from "node:crypto";
2
1
  import fs from "node:fs";
3
2
  import https from "node:https";
4
3
  import path from "node:path";
4
+ import { createRequire } from "node:module";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
+ const require = createRequire(import.meta.url);
10
+ const {
11
+ loadPackagedChecksums,
12
+ verifyArtifact,
13
+ } = require("../lib/integrity.cjs");
9
14
 
10
15
  const pkgPath = path.join(__dirname, "..", "package.json");
11
16
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
12
17
  const version = pkg.version;
13
18
 
19
+ function isMonorepoSourceCheckout() {
20
+ const packageRoot = path.resolve(__dirname, "..");
21
+ const repositoryRoot = path.resolve(packageRoot, "../..");
22
+ try {
23
+ const repositoryPackage = JSON.parse(
24
+ fs.readFileSync(path.join(repositoryRoot, "package.json"), "utf8"),
25
+ );
26
+ return (
27
+ repositoryPackage.private === true &&
28
+ Array.isArray(repositoryPackage.workspaces) &&
29
+ repositoryPackage.workspaces.includes("packages/*") &&
30
+ fs.realpathSync(packageRoot) ===
31
+ path.join(fs.realpathSync(repositoryRoot), "packages", "cli-wrapper")
32
+ );
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
14
38
  const baseUrl =
15
39
  process.env.WORKRAIL_BINARY_BASE_URL ||
16
40
  `https://cli.workrail.dev/workrail/v${version}`;
@@ -28,6 +52,10 @@ function resolveBinaryName() {
28
52
  return null;
29
53
  }
30
54
 
55
+ function resolveHelperBinaryName(binaryName) {
56
+ return binaryName.replace(/^workrail-/, "workrail-gh-collector-");
57
+ }
58
+
31
59
  function downloadToFile(url, dest) {
32
60
  return new Promise((resolve, reject) => {
33
61
  const file = fs.createWriteStream(dest);
@@ -44,45 +72,14 @@ function downloadToFile(url, dest) {
44
72
  });
45
73
  }
46
74
 
47
- function downloadText(url) {
48
- return new Promise((resolve, reject) => {
49
- let data = "";
50
- https
51
- .get(url, (res) => {
52
- if (res.statusCode !== 200) {
53
- reject(new Error(`Download failed (${res.statusCode}) for ${url}`));
54
- return;
55
- }
56
- res.setEncoding("utf8");
57
- res.on("data", (chunk) => (data += chunk));
58
- res.on("end", () => resolve(data));
59
- })
60
- .on("error", (err) => reject(err));
61
- });
62
- }
63
-
64
- function sha256File(filePath) {
65
- const hash = crypto.createHash("sha256");
66
- const data = fs.readFileSync(filePath);
67
- hash.update(data);
68
- return hash.digest("hex");
69
- }
70
-
71
- function parseChecksums(text) {
72
- const map = new Map();
73
- for (const line of text.split("\n")) {
74
- const trimmed = line.trim();
75
- if (!trimmed) continue;
76
- const parts = trimmed.split(/\s+/);
77
- if (parts.length < 2) continue;
78
- const hash = parts[0];
79
- const file = parts[parts.length - 1];
80
- map.set(file, hash);
75
+ async function main() {
76
+ if (isMonorepoSourceCheckout()) {
77
+ console.log(
78
+ "[workrail] Source checkout detected; skipping native binary installation.",
79
+ );
80
+ return;
81
81
  }
82
- return map;
83
- }
84
82
 
85
- async function main() {
86
83
  const binaryName = resolveBinaryName();
87
84
  if (!binaryName) {
88
85
  console.warn(
@@ -94,32 +91,36 @@ async function main() {
94
91
  const nativeDir = path.join(__dirname, "..", "bin", "native");
95
92
  fs.mkdirSync(nativeDir, { recursive: true });
96
93
 
97
- const binUrl = `${baseUrl}/${binaryName}`;
98
- const checksumsUrl = `${baseUrl}/checksums.txt`;
99
- const targetPath = path.join(nativeDir, binaryName);
94
+ const checksums = loadPackagedChecksums(path.join(__dirname, ".."));
95
+ const helperBinaryName = resolveHelperBinaryName(binaryName);
96
+ const artifacts = [
97
+ { remoteName: binaryName, targetName: binaryName },
98
+ {
99
+ remoteName: helperBinaryName,
100
+ targetName:
101
+ process.platform === "win32"
102
+ ? "workrail-gh-collector.exe"
103
+ : "workrail-gh-collector",
104
+ },
105
+ ];
100
106
 
101
107
  console.log(
102
108
  `📦 Installing Workrail CLI for ${process.platform} (${process.arch})...`,
103
109
  );
104
- await downloadToFile(binUrl, targetPath);
105
-
106
- console.log("🔒 Verifying download...");
107
- const checksums = parseChecksums(await downloadText(checksumsUrl));
108
- const expected = checksums.get(binaryName);
109
-
110
- if (!expected) {
111
- throw new Error(`No checksum entry found for ${binaryName}`);
112
- }
113
-
114
- const actual = sha256File(targetPath);
115
- if (actual !== expected) {
116
- throw new Error(
117
- `Checksum mismatch for ${binaryName}. expected=${expected} actual=${actual}`,
118
- );
119
- }
120
-
121
- if (process.platform !== "win32") {
122
- fs.chmodSync(targetPath, 0o755);
110
+ for (const artifact of artifacts) {
111
+ const targetPath = path.join(nativeDir, artifact.targetName);
112
+ const temporaryPath = `${targetPath}.download`;
113
+ try {
114
+ await downloadToFile(`${baseUrl}/${artifact.remoteName}`, temporaryPath);
115
+ console.log(`🔒 Verifying ${artifact.remoteName}...`);
116
+ verifyArtifact(temporaryPath, artifact.remoteName, checksums);
117
+ fs.renameSync(temporaryPath, targetPath);
118
+ } finally {
119
+ if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
120
+ }
121
+ if (process.platform !== "win32") {
122
+ fs.chmodSync(targetPath, 0o755);
123
+ }
123
124
  }
124
125
 
125
126
  console.log("✅ Workrail CLI installed successfully!");
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const {
8
+ parseChecksums,
9
+ verifyRequiredArtifacts,
10
+ } = require("../lib/integrity.cjs");
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const packageRoot = path.resolve(__dirname, "..");
13
+ const repoRoot = path.resolve(packageRoot, "../..");
14
+
15
+ const packageJson = JSON.parse(
16
+ fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"),
17
+ );
18
+ const denoJson = JSON.parse(
19
+ fs.readFileSync(path.join(repoRoot, "apps/cli/deno.json"), "utf8"),
20
+ );
21
+ if (packageJson.version !== denoJson.version) {
22
+ throw new Error(
23
+ `CLI version mismatch: npm=${packageJson.version} deno=${denoJson.version}`,
24
+ );
25
+ }
26
+
27
+ const source = path.join(repoRoot, "apps/cli/dist/checksums.txt");
28
+ const contents = fs.readFileSync(source, "utf8");
29
+ const checksums = parseChecksums(contents);
30
+ const required = [
31
+ "workrail-darwin-arm64",
32
+ "workrail-darwin-x64",
33
+ "workrail-linux-arm64",
34
+ "workrail-linux-x64",
35
+ "workrail-windows-x64.exe",
36
+ "workrail-gh-collector-darwin-arm64",
37
+ "workrail-gh-collector-darwin-x64",
38
+ "workrail-gh-collector-linux-arm64",
39
+ "workrail-gh-collector-linux-x64",
40
+ "workrail-gh-collector-windows-x64.exe",
41
+ ];
42
+ for (const artifact of required) {
43
+ if (!checksums.has(artifact))
44
+ throw new Error(`Missing checksum for ${artifact}`);
45
+ }
46
+ verifyRequiredArtifacts(path.dirname(source), required, checksums);
47
+
48
+ fs.writeFileSync(path.join(packageRoot, "release-checksums.txt"), contents, {
49
+ mode: 0o644,
50
+ });
51
+ console.log(
52
+ `Embedded trusted checksums for Workrail CLI v${packageJson.version}`,
53
+ );