Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"scripts": {
"prepack": "bun run build",
"cli": "TOKENMAXXING_ENV=development bun src/index.ts",
"build": "bun run typecheck && bun build src/index.ts --target node --outfile dist/index.js && chmod +x dist/index.js",
"build": "bun run typecheck && bun script/build-node.ts",
"build:native-packages": "bun script/build-service-runners.ts",
"build:service-runners": "bun script/build-service-runners.ts",
"publish:release": "bun script/publish.ts",
Expand Down
29 changes: 29 additions & 0 deletions apps/cli/script/build-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { chmod } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const cliDir = fileURLToPath(new URL("..", import.meta.url));
const outfile = join(cliDir, "dist", "index.js");

const result = spawnSync(
process.execPath,
["build", "src/index.ts", "--target", "node", "--outfile", outfile],
{
cwd: cliDir,
stdio: "inherit",
},
);

if (result.error !== undefined) {
console.error(result.error.message);
process.exit(1);
}

if (result.status !== 0) {
process.exit(typeof result.status === "number" ? result.status : 1);
}

if (process.platform !== "win32") {
await chmod(outfile, 0o755);
}
22 changes: 22 additions & 0 deletions apps/cli/script/install-native.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,28 @@ describe("native preinstall package selection", () => {
}
});

it("fallback launcher resolves the preinstall binary when optional packages are absent", () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "tokenmaxxing-native-launcher-"));
try {
const binaryPath = path.join(temp, "bin", "tokenmaxxing.exe");
fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
fs.writeFileSync(binaryPath, "#!/bin/sh\nexit 0\n");

expect(
findNativeBinary({
arch: "arm64",
packageDir: temp,
platform: "darwin",
}),
).toEqual({
packageName: "@851-labs/tokenmaxxing",
path: fs.realpathSync(binaryPath),
});
} finally {
fs.rmSync(temp, { force: true, recursive: true });
}
});

it("fallback launcher recovery message explains shadowed and script-blocked installs", () => {
const message = recoveryMessage({ arch: "arm64", platform: "darwin" });

Expand Down
8 changes: 8 additions & 0 deletions apps/cli/script/native-bin-launcher.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ function findNativeBinary(options = {}) {
}
}

const fallbackPath = path.join(options.packageDir ?? __dirname, "bin", "tokenmaxxing.exe");
if (fs.existsSync(fallbackPath)) {
return {
packageName: "@851-labs/tokenmaxxing",
path: fs.realpathSync(fallbackPath),
};
}

return null;
}

Expand Down
27 changes: 27 additions & 0 deletions apps/cli/script/publish-main-package.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { cp, mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import packageJson from "../package.json";
import { createMainPackageJson } from "./publish-manifest";

const cliDir = fileURLToPath(new URL("..", import.meta.url));
const repoDir = resolve(cliDir, "../..");

async function writeMainPackage(outDir: string): Promise<void> {
const packageDir = join(outDir, packageJson.name);
await mkdir(packageDir, { recursive: true });
await cp(join(repoDir, "LICENSE"), join(packageDir, "LICENSE"));
await cp(join(cliDir, "README.md"), join(packageDir, "README.md"));
await cp(join(cliDir, "script", "install-native.mjs"), join(packageDir, "install-native.mjs"));
await cp(
join(cliDir, "script", "native-bin-launcher.cjs"),
join(packageDir, "native-bin-launcher.cjs"),
);
await writeFile(
join(packageDir, "package.json"),
`${JSON.stringify(createMainPackageJson(), null, 2)}\n`,
);
}

export { writeMainPackage };
8 changes: 4 additions & 4 deletions apps/cli/script/publish-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ function createMainPackageJson() {
license: packageJson.license,
repository: packageJson.repository,
bin: {
tokenmaxxing: "./bin/tokenmaxxing.exe",
tokenmaxxing: "./native-bin-launcher.cjs",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore installer fallback for skipped optional deps

When optionalDependencies are omitted/skipped but lifecycle scripts still run, install-native.mjs materializes the downloaded host binary at bin/tokenmaxxing.exe, but this new bin target invokes native-bin-launcher.cjs instead. The launcher only resolves the optional native packages and never checks the materialized bin/tokenmaxxing.exe, so installs that succeeded via the preinstall fallback now fail at runtime with the recovery message. Either keep the bin pointed at the installed fallback or teach the launcher to use it when native packages are absent.

Useful? React with 👍 / 👎.

},
// npm links Windows shims after preinstall and before postinstall. Keep
// native installation here so shims see the final .exe instead of Node JS.
// Keep native installation in preinstall so package managers that skip
// optional dependencies still get one chance to materialize the host binary.
scripts: {
preinstall: "bun ./install-native.mjs || node ./install-native.mjs",
},
files: ["bin", "native-bin-launcher.cjs", "install-native.mjs", "README.md", "LICENSE"],
files: ["native-bin-launcher.cjs", "install-native.mjs", "README.md", "LICENSE"],
os: ["darwin", "linux", "win32"],
cpu: ["arm64", "x64"],
publishConfig: packageJson.publishConfig,
Expand Down
30 changes: 26 additions & 4 deletions apps/cli/script/publish.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { describe, expect, it } from "vitest";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import packageJson from "../package.json";
import { createMainPackageJson } from "./publish-manifest";
import { writeMainPackage } from "./publish-main-package";
import { npmDistTagForVersion, parsePublishCliArgs } from "./publish-options";
import { npmRegistryPackageVersionUrl } from "./publish-registry";

Expand All @@ -27,15 +33,14 @@ describe("publish script generated main package", () => {
it("publishes a native CLI installer package with new target optional dependencies", () => {
const manifest = createMainPackageJson();

expect(manifest.bin).toEqual({ tokenmaxxing: "./bin/tokenmaxxing.exe" });
// npm links Windows shims after preinstall and before postinstall; using
// postinstall here makes those shims run Node against the final native exe.
expect(manifest.bin).toEqual({ tokenmaxxing: "./native-bin-launcher.cjs" });
// Package-manager command shims should run the JS launcher. Pointing the
// package bin at a native .exe makes Bun's Windows shim ask Node to parse it.
expect(manifest.scripts).toEqual({
preinstall: "bun ./install-native.mjs || node ./install-native.mjs",
});
expect(manifest.scripts).not.toHaveProperty("postinstall");
expect(manifest.files).toEqual([
"bin",
"native-bin-launcher.cjs",
"install-native.mjs",
"README.md",
Expand All @@ -46,6 +51,23 @@ describe("publish script generated main package", () => {
"@851-labs/tokenmaxxing-service-darwin-arm64",
);
});

it("writes the JS launcher as the package bin without a native exe placeholder", async () => {
const dir = await mkdtemp(join(tmpdir(), "tokenmaxxing-publish-test-"));

try {
await writeMainPackage(dir);

const packageDir = join(dir, packageJson.name);
const manifest = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));

expect(manifest.bin).toEqual({ tokenmaxxing: "./native-bin-launcher.cjs" });
expect(existsSync(join(packageDir, "native-bin-launcher.cjs"))).toBe(true);
expect(existsSync(join(packageDir, "bin", "tokenmaxxing.exe"))).toBe(false);
} finally {
await rm(dir, { force: true, recursive: true });
}
});
});

describe("publish script registry checks", () => {
Expand Down
25 changes: 2 additions & 23 deletions apps/cli/script/publish.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env bun

import { $ } from "bun";
import { chmod, cp, mkdir, mkdtemp, rm } from "node:fs/promises";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import packageJson from "../package.json";
import { assertSafeOutputDir, buildServiceRunners } from "./build-service-runners";
import { createMainPackageJson } from "./publish-manifest";
import { writeMainPackage } from "./publish-main-package";
import {
npmDistTagForVersion,
parsePublishCliArgs,
Expand All @@ -26,7 +26,6 @@ import {
} from "../src/service-runner-targets";

const cliDir = fileURLToPath(new URL("..", import.meta.url));
const repoDir = resolve(cliDir, "../..");

async function publishCli(options: PublishCliOptions = {}): Promise<void> {
const outDir =
Expand Down Expand Up @@ -64,26 +63,6 @@ function packagePublishPaths(outDir: string): Record<string, string> {
]);
}

async function writeMainPackage(outDir: string): Promise<void> {
const packageDir = join(outDir, packageJson.name);
const binDir = join(packageDir, "bin");
await mkdir(packageDir, { recursive: true });
await mkdir(binDir, { recursive: true });
await cp(join(repoDir, "LICENSE"), join(packageDir, "LICENSE"));
await cp(join(cliDir, "README.md"), join(packageDir, "README.md"));
await cp(join(cliDir, "script", "install-native.mjs"), join(packageDir, "install-native.mjs"));
await cp(
join(cliDir, "script", "native-bin-launcher.cjs"),
join(packageDir, "native-bin-launcher.cjs"),
);
await cp(join(cliDir, "script", "native-bin-launcher.cjs"), join(binDir, "tokenmaxxing.exe"));
await chmod(join(binDir, "tokenmaxxing.exe"), 0o755);
await Bun.write(
join(packageDir, "package.json"),
`${JSON.stringify(createMainPackageJson(), null, 2)}\n`,
);
}

async function smokeTestHostRunner(outDir: string): Promise<void> {
const target = serviceRunnerTarget();
if (target === null) {
Expand Down
27 changes: 13 additions & 14 deletions apps/cli/src/commands/root.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

const cliRoot = resolve(import.meta.dirname, "../..");
const jsonHelpCommands = [
["upgrade", "--help"],
["service", "install", "--help"],
["service", "uninstall", "--help"],
["service", "status", "--help"],
["service", "doctor", "--help"],
["service", "repair", "--help"],
["service", "run", "--help"],
].map((args) => [args.join(" "), args] as const);

function runCli(args: readonly string[]) {
const result = spawnSync("bun", ["src/index.ts", ...args], {
Expand Down Expand Up @@ -37,21 +46,11 @@ describe("root command", () => {
expect(result.output).toContain("upgrade Upgrade the globally installed CLI");
});

it("exposes --json on all service subcommands and upgrade", () => {
for (const args of [
["upgrade", "--help"],
["service", "install", "--help"],
["service", "uninstall", "--help"],
["service", "status", "--help"],
["service", "doctor", "--help"],
["service", "repair", "--help"],
["service", "run", "--help"],
]) {
const result = runCli(args);
it.each(jsonHelpCommands)("exposes --json on %s", (_name, args) => {
const result = runCli(args);

expect(result.status).toBe(0);
expect(result.output).toContain("--json");
}
expect(result.status).toBe(0);
expect(result.output).toContain("--json");
});

it("exposes bootstrap as a human-only onboarding command", () => {
Expand Down
Loading