Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/remove-cargo-dist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@googleworkspace/cli": minor
---

Remove cargo-dist; use native Node.js fetch for npm binary installer

Replaces the cargo-dist generated release pipeline and npm package with:
- A custom GitHub Actions release workflow with matrix cross-compilation
- A zero-dependency npm installer using native `fetch()` (Node 18+)
- Removes axios, rimraf, detect-libc, console.table, and axios-proxy-builder dependencies from the published npm package
427 changes: 131 additions & 296 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

44 changes: 0 additions & 44 deletions dist-workspace.toml

This file was deleted.

2 changes: 2 additions & 0 deletions npm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Downloaded binary (created during npm postinstall)
bin/
112 changes: 112 additions & 0 deletions npm/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node

"use strict";

const fs = require("fs");
const path = require("path");
const os = require("os");
const { pipeline } = require("stream/promises");
const { createWriteStream, mkdirSync, rmSync } = require("fs");
const { execSync } = require("child_process");
const { getPlatform } = require("./platform");

const INSTALL_DIR = path.join(__dirname, "bin");

/**
* Get the GitHub release download URL base for the current package version.
*/
function getDownloadUrl(artifactName) {
const { version } = require("./package.json");
return `https://github.com/googleworkspace/cli/releases/download/v${version}/${artifactName}`;
}

/**
* Download a file using native fetch (Node 18+).
*/
async function download(url, dest) {
const res = await fetch(url, { redirect: "follow" });
Comment thread
jpoehnelt marked this conversation as resolved.
Comment thread
jpoehnelt marked this conversation as resolved.

if (!res.ok) {
throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`);
}

const fileStream = createWriteStream(dest);
// Convert web ReadableStream to Node stream and pipe
const { Readable } = require("stream");
const nodeStream = Readable.fromWeb(res.body);
await pipeline(nodeStream, fileStream);
Comment thread
jpoehnelt marked this conversation as resolved.
}

/**
* Extract the archive to the install directory.
*/
function extract(archivePath, destDir) {
const ext = path.extname(archivePath);
const isZip = archivePath.endsWith(".zip");
const isTar = archivePath.includes(".tar.");

if (isTar) {
execSync(`tar xf "${archivePath}" --strip-components 1 -C "${destDir}"`, {
stdio: "pipe",
});
} else if (isZip) {
if (process.platform === "win32") {
execSync(
`powershell.exe -NoProfile -NonInteractive -Command "Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${destDir}' -Force"`,
{ stdio: "pipe" },
);
} else {
execSync(`unzip -q -o "${archivePath}" -d "${destDir}"`, {
stdio: "pipe",
});
}
Comment thread
jpoehnelt marked this conversation as resolved.
Outdated
Comment thread
jpoehnelt marked this conversation as resolved.
} else {
throw new Error(`Unsupported archive format: ${archivePath}`);
}
}

async function install() {
const platform = getPlatform();
const url = getDownloadUrl(platform.artifact);

// Check if already installed
const binPath = path.join(INSTALL_DIR, platform.binary);
if (fs.existsSync(binPath)) {
console.error(`gws is already installed, skipping installation.`);
return;
}

// Clean and create install directory
if (fs.existsSync(INSTALL_DIR)) {
rmSync(INSTALL_DIR, { recursive: true, force: true });
}
mkdirSync(INSTALL_DIR, { recursive: true });

// Download to a temp file
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gws-"));
const archiveName = path.basename(platform.artifact);
const tmpFile = path.join(tmpDir, archiveName);

try {
console.error(`Downloading gws from ${url}`);
await download(url, tmpFile);

console.error(`Extracting to ${INSTALL_DIR}`);
extract(tmpFile, INSTALL_DIR);

// Make binary executable on Unix
if (process.platform !== "win32") {
fs.chmodSync(binPath, 0o755);
}

console.error(`gws has been installed!`);
} finally {
// Clean up temp files
rmSync(tmpDir, { recursive: true, force: true });
}
}

install().catch((err) => {
console.error(`Error installing gws: ${err.message}`);
Comment thread
jpoehnelt marked this conversation as resolved.
Outdated
process.exit(1);
});
79 changes: 79 additions & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"name": "@googleworkspace/cli",
"description": "Google Workspace CLI — dynamic command surface from Discovery Service",
"version": "0.22.3",
"license": "Apache-2.0",
"author": "Justin Poehnelt",
"repository": {
"type": "git",
"url": "https://github.com/googleworkspace/cli.git"
},
"homepage": "https://github.com/googleworkspace/cli",
"bugs": {
"url": "https://github.com/googleworkspace/cli/issues"
},
"bin": {
"gws": "run.js"
},
"scripts": {
"postinstall": "node install.js"
},
"engines": {
"node": ">=18"
},
"preferUnplugged": true,
"keywords": [
"cli",
"google-workspace",
"google",
"google-api",
"google-drive",
"google-gmail",
"google-sheets",
"google-calendar",
"google-docs",
"google-chat",
"google-admin",
"gsuite",
"discovery-api",
"ai-agent",
"agent-skills",
"automation",
"oauth2",
"rust"
],
"publishConfig": {
"provenance": true,
"registry": "https://wombat-dressing-room.appspot.com"
},
"supportedPlatforms": {
"aarch64-apple-darwin": {
"artifact": "google-workspace-cli-aarch64-apple-darwin.tar.gz",
"binary": "gws"
},
"x86_64-apple-darwin": {
"artifact": "google-workspace-cli-x86_64-apple-darwin.tar.gz",
"binary": "gws"
},
"aarch64-unknown-linux-gnu": {
"artifact": "google-workspace-cli-aarch64-unknown-linux-gnu.tar.gz",
"binary": "gws"
},
"aarch64-unknown-linux-musl": {
"artifact": "google-workspace-cli-aarch64-unknown-linux-musl.tar.gz",
"binary": "gws"
},
"x86_64-unknown-linux-gnu": {
"artifact": "google-workspace-cli-x86_64-unknown-linux-gnu.tar.gz",
"binary": "gws"
},
"x86_64-unknown-linux-musl": {
"artifact": "google-workspace-cli-x86_64-unknown-linux-musl.tar.gz",
"binary": "gws"
},
"x86_64-pc-windows-msvc": {
"artifact": "google-workspace-cli-x86_64-pc-windows-msvc.zip",
"binary": "gws.exe"
}
}
}
83 changes: 83 additions & 0 deletions npm/platform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node

"use strict";

const os = require("os");
const path = require("path");
const fs = require("fs");
const { execSync } = require("child_process");

const { supportedPlatforms } = require("./package.json");

/**
* Map Node.js os.type() and os.arch() to Rust-style target triples.
*/
function getPlatformKey() {
const rawOs = os.type();
const rawArch = os.arch();

let osType;
switch (rawOs) {
case "Windows_NT":
osType = "pc-windows-msvc";
break;
case "Darwin":
osType = "apple-darwin";
break;
case "Linux":
osType = "unknown-linux-gnu";
break;
default:
throw new Error(`Unsupported operating system: ${rawOs}`);
}

let arch;
switch (rawArch) {
case "x64":
arch = "x86_64";
break;
case "arm64":
arch = "aarch64";
break;
default:
throw new Error(`Unsupported architecture: ${rawArch}`);
}

// On Linux, try to detect musl libc
if (rawOs === "Linux") {
try {
const lddOutput = execSync("ldd --version 2>&1 || true", {
encoding: "utf8",
});
if (lddOutput.toLowerCase().includes("musl")) {
osType = "unknown-linux-musl";
}
} catch {
// If ldd fails, assume glibc
}
}

const key = `${arch}-${osType}`;

if (!supportedPlatforms[key]) {
// Try musl fallback on Linux if glibc binary is not available
if (rawOs === "Linux") {
const muslKey = `${arch}-unknown-linux-musl`;
if (supportedPlatforms[muslKey]) {
return muslKey;
}
}
throw new Error(
`Unsupported platform: ${key}\nSupported platforms: ${Object.keys(supportedPlatforms).join(", ")}`,
);
}

return key;
}

function getPlatform() {
const key = getPlatformKey();
return supportedPlatforms[key];
}

module.exports = { getPlatform, getPlatformKey };
22 changes: 22 additions & 0 deletions npm/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env node

"use strict";

const path = require("path");
const { spawnSync } = require("child_process");
const { getPlatform } = require("./platform");

const platform = getPlatform();
const binPath = path.join(__dirname, "bin", platform.binary);

const result = spawnSync(binPath, process.argv.slice(2), {
cwd: process.cwd(),
stdio: "inherit",
});

if (result.error) {
console.error(`Error running gws: ${result.error.message}`);
process.exit(1);
}

process.exit(result.status);
Comment thread
jpoehnelt marked this conversation as resolved.
Outdated
9 changes: 8 additions & 1 deletion scripts/version-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ done
sed -i.bak -E "s/(google-workspace = \{ version = \")[^\"]+/\1${VERSION}/" crates/google-workspace-cli/Cargo.toml
rm -f crates/google-workspace-cli/Cargo.toml.bak

# Update npm installer package.json version
node -e "
const pkg = require('./npm/package.json');
pkg.version = '${VERSION}';
require('fs').writeFileSync('./npm/package.json', JSON.stringify(pkg, null, 2) + '\n');
"

# Update Cargo.lock to match
cargo generate-lockfile

Expand All @@ -38,5 +45,5 @@ fi
cargo run -- generate-skills --output-dir skills

# Stage the changed files so changesets/action commits them
git add crates/*/Cargo.toml Cargo.lock flake.nix flake.lock skills/
git add crates/*/Cargo.toml Cargo.lock flake.nix flake.lock skills/ npm/package.json

Loading