@workrail/cli 0.5.10 → 0.6.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 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.10",
3
+ "version": "0.6.1",
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
+ e780027df5fdf683bf4b5c31f08757febee1798e240a6b3b7f207015702d2ff0 workrail-darwin-arm64
2
+ a16c4bc401f4db140f14fdb4d5301a2a674c6c7cfcfb973cf1a26cf0b2e4e5d7 workrail-darwin-x64
3
+ 41bb3e231f6d19a8f98e1218933e2293652585ea2a08d5b2375149fb488c70d5 workrail-gh-collector-darwin-arm64
4
+ cd83d1f8e41805aef6d9613dac9b5f4ff59e7c3d68eba983825b4f34e2a215d1 workrail-gh-collector-darwin-x64
5
+ 6243c3a132512df846634511b90096f03e36cd2c9a5dc11c102290c41606650b workrail-gh-collector-linux-arm64
6
+ 533ac74bee15efb6bb44b17238f62daac20444fcb31a47b8c453e437b51e378c workrail-gh-collector-linux-x64
7
+ 40afea1437fbe3f032c03934ac4aee50db633d1d4d9d9eae08e5e52655fb6602 workrail-gh-collector-windows-x64.exe
8
+ 7f77ef1eaeb422c6f283705df0c68155b9c935767cb8b328e8ebb3ea83227d64 workrail-linux-arm64
9
+ 151c902f90a3cdf8711f588d8f8fd8fe399d41bfffdbfd24f60a603d3850b424 workrail-linux-x64
10
+ 383e4b719bc2d3868d00da09f4e41647943d64f00fe5a80dc11a80086c265bd3 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
+ );