diff --git a/README.md b/README.md index 699a72d..35d8ac8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index e2999ea..f7e2174 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/index.ts b/src/index.ts index 756b7db..6a2e37b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -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() diff --git a/src/lib/pho-alias.ts b/src/lib/pho-alias.ts new file mode 100644 index 0000000..0b287e3 --- /dev/null +++ b/src/lib/pho-alias.ts @@ -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 ` + * (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. + * + * /node_modules/.bin (local install) + * /bin with /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 `/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: /bin + resolve(pkgRoot, "..", "..", "..", "..", "..", "bin"), // bun global: ~/.bun/bin + ]; + + 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; + } + } catch { + // best-effort + } +} + +function escapeForRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +}