Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ photon spectrum users ls
photon billing show
```

`pho` is an alias for `photon` for high-frequency commands:
`pho` is an alias for `photon` for high-frequency commands. It's created
automatically the first time you run `photon` after installing:

```sh
pho ls # photon projects ls
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
"url": "https://github.com/photon-hq/cli/issues"
},
"type": "module",
"bin": {
"photon": "./dist/photon.js",
"pho": "./dist/photon.js"
},
"bin": "./dist/photon.js",
"files": [
"dist",
"README.md"
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import {
UnknownEnvError,
} from '~/lib/errors.ts';
import { die } from '~/lib/output.ts';
import { ensurePhoAlias } from '~/lib/pho-alias.ts';
import { startUpdateNotifier } from '~/lib/update-check.ts';
import pkg from '../package.json' with { type: 'json' };

ensurePhoAlias();
startUpdateNotifier();

const program = new Command()
Expand Down
71 changes: 71 additions & 0 deletions src/lib/pho-alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { existsSync, lstatSync, symlinkSync } from "node:fs";
import { join, resolve, sep } from "node:path";

/**
* Lazily install `pho` as a sibling of `photon` in whichever bin directory
* we were launched from.
*
* Why not declare both bins in package.json? npm 11's `npx <scoped-pkg>`
* (no version) skips bin auto-resolve when `bin` has multiple keys. Keeping
* `bin` as a single string preserves clean `npx @photon-ai/photon` usage.
*
* Why not a `postinstall` script? Bun blocks postinstall by default — that
* would silently strip `pho` from `bun add -g` (our primary install path).
*
* Approach: walk up from the running script's path to the package root, then
* try the standard bin-dir layouts adjacent to it. First match wins.
*
* <root>/node_modules/.bin (local install)
* <prefix>/bin with <prefix>/lib/node_modules/... (npm/yarn/pnpm -g)
* ~/.bun/bin with ~/.bun/install/global/node_modules/... (bun -g)
*
* Cost is two stat calls per launch after `pho` exists. Errors are swallowed
* — `pho` is convenience, never load-bearing.
*/
export function ensurePhoAlias(): void {
try {
const me = process.argv[1];
if (!me) return;

// Guard: only run when launched from an installed package layout.
// Source-mode runs (`bun run src/index.ts`) skip this entirely.
const installedShape = new RegExp(
`${escapeForRegex(sep)}node_modules${escapeForRegex(sep)}.+${escapeForRegex(sep)}dist${escapeForRegex(sep)}photon\\.js$`,
);
if (!installedShape.test(me)) return;

// process.argv[1] resolves symlinks → me is `<pkg>/dist/photon.js`.
// pkgRoot is two levels up.
const pkgRoot = resolve(me, "..", "..");

const candidates = [
resolve(pkgRoot, "..", "..", ".bin"), // local: node_modules/.bin
resolve(pkgRoot, "..", "..", "..", "..", "bin"), // npm/yarn/pnpm global: <prefix>/bin
resolve(pkgRoot, "..", "..", "..", "..", "..", "bin"), // bun global: ~/.bun/bin
];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for (const dir of candidates) {
const photon = join(dir, "photon");
const pho = join(dir, "pho");
if (!existsSync(photon)) continue;

// lstatSync detects ANY entry at `pho` — including broken symlinks
// that existsSync would miss (existsSync follows the link target).
try {
lstatSync(pho);
return; // something already lives here; leave it alone
} catch {
// pho doesn't exist; safe to create
}

symlinkSync("./photon", pho);
return;
Comment thread
lcandy2 marked this conversation as resolved.
}
} catch {
// best-effort
}
}

function escapeForRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}