@nozomiishii/pm 0.2.1 → 0.2.4
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.ja.md +14 -0
- package/README.md +14 -0
- package/dist/cli.mjs +229 -0
- package/package.json +48 -27
- package/dist/cli.js +0 -290
package/README.ja.md
CHANGED
|
@@ -157,6 +157,20 @@ pm() {
|
|
|
157
157
|
|
|
158
158
|
この仕組みにより、プロジェクト名のタブ補完もサポートしています。
|
|
159
159
|
|
|
160
|
+
## Tips
|
|
161
|
+
|
|
162
|
+
### Ghostty のキーバインドで一発起動
|
|
163
|
+
|
|
164
|
+
[Ghostty](https://ghostty.org) を使っている場合、設定ファイルに次の行を追加するとキーバインド一発で `pm` を起動できます。
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
keybind = super+alt+j=text:pm\n
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`super+alt+j` を押すと現在のターミナルへ `pm` + Enter が送信され、fzf ピッカーが即座に開きます。
|
|
171
|
+
|
|
172
|
+
設定ファイルのパスは OS によって異なるので、[Ghostty 公式ドキュメントの Configuration](https://ghostty.org/docs/config) を参照してください。
|
|
173
|
+
|
|
160
174
|
## 謝辞
|
|
161
175
|
|
|
162
176
|
pm は以下のプロジェクトから大きな影響を受けています。僕の生産性を上げてくれて本当にありがとうございます
|
package/README.md
CHANGED
|
@@ -157,6 +157,20 @@ pm() {
|
|
|
157
157
|
|
|
158
158
|
This mechanism also provides tab completion for project names.
|
|
159
159
|
|
|
160
|
+
## Tips
|
|
161
|
+
|
|
162
|
+
### Launch with a Ghostty keybind
|
|
163
|
+
|
|
164
|
+
If you use [Ghostty](https://ghostty.org), add the following line to your config to launch `pm` with a single keybind.
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
keybind = super+alt+j=text:pm\n
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Pressing `super+alt+j` sends `pm` + Enter to the current terminal, instantly opening the fzf picker.
|
|
171
|
+
|
|
172
|
+
The config file path differs by OS, so see the [Configuration page in the Ghostty docs](https://ghostty.org/docs/config) for the exact location.
|
|
173
|
+
|
|
160
174
|
## Acknowledgments
|
|
161
175
|
|
|
162
176
|
pm is heavily inspired by these projects. Thank you so much for boosting my productivity.
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
//#region package.json
|
|
7
|
+
var version = "0.2.4";
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/expand-home.ts
|
|
10
|
+
function expandHome(p) {
|
|
11
|
+
if (p.startsWith("~/")) return path.join(process.env.HOME ?? "", p.slice(2));
|
|
12
|
+
if (p === "~") return process.env.HOME ?? "";
|
|
13
|
+
return p;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/filter-projects.ts
|
|
17
|
+
function filterProjects(projects, tags) {
|
|
18
|
+
return projects.filter((p) => {
|
|
19
|
+
if (p.enabled === false) return false;
|
|
20
|
+
if (tags.length > 0) return tags.every((t) => p.tags?.includes(t));
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/strip-emoji-label.ts
|
|
26
|
+
/** Remove emoji (incl. modifiers, ZWJ) and normalize whitespace. */
|
|
27
|
+
function stripEmojiLabel(s) {
|
|
28
|
+
let t = s.normalize("NFC");
|
|
29
|
+
for (let i = 0; i < 4; i++) t = t.replaceAll(/[\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Variation_Selector}\u{200D}]/gv, "");
|
|
30
|
+
return t.replaceAll(/\s+/g, " ").trim();
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/find-project.ts
|
|
34
|
+
/**
|
|
35
|
+
* 引数の名前から一致するプロジェクトを探す。
|
|
36
|
+
* 生の name とプレーンラベルの両方にマッチする。
|
|
37
|
+
* 重複した場合はエラーを投げる。
|
|
38
|
+
*/
|
|
39
|
+
function findProject(projects, query) {
|
|
40
|
+
const matches = projects.filter((p) => {
|
|
41
|
+
if (p.name === query) return true;
|
|
42
|
+
return stripEmojiLabel(p.name) === query;
|
|
43
|
+
});
|
|
44
|
+
if (matches.length > 1) throw new Error(`Ambiguous name "${query}" matches: ${matches.map((p) => p.name).join(", ")}`);
|
|
45
|
+
return matches[0];
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/logo/logo-color.ascii
|
|
49
|
+
var logo_color_default = "\x1B[38;2;120;180;255m zzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m\n\x1B[38;2;100;160;245m zzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m\n\x1B[38;2;80;140;235mzzzzz.----.______.zzzzzzzzz\x1B[0m\n\x1B[38;2;60;120;225mzzzzz | __________zzzzzz\x1B[0m\n\x1B[38;2;45;100;215mzzzzz | / /zzzz\x1B[0m\n\x1B[38;2;30;80;205mzzzzz | / /zzzzz\x1B[0m\n\x1B[38;2;20;65;195mzzzzz | / pm /zzzzzz\x1B[0m\n\x1B[38;2;10;50;185mzzzzz |/__________ /zzzzzzz\x1B[0m\n\x1B[38;2;10;50;185mnozozzzzzzzzzzzzzzzzzzzzzz\x1B[0m\n\x1B[38;2;10;50;185mzzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m\n";
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/cli.ts
|
|
52
|
+
var ExitError = class extends Error {
|
|
53
|
+
code;
|
|
54
|
+
constructor(message, code) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function defaultConfigPath() {
|
|
60
|
+
const home = process.env.HOME ?? "";
|
|
61
|
+
switch (process.platform) {
|
|
62
|
+
case "darwin": return path.join(home, "Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json");
|
|
63
|
+
case "win32": return path.join(process.env.APPDATA ?? "", "Code/User/globalStorage/alefragnani.project-manager/projects.json");
|
|
64
|
+
default: return path.join(home, ".config/Code/User/globalStorage/alefragnani.project-manager/projects.json");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function printLogo() {
|
|
68
|
+
process.stdout.write(`${logo_color_default}\n`);
|
|
69
|
+
}
|
|
70
|
+
function usage() {
|
|
71
|
+
process.stdout.write(`Usage: pm [options] [command]
|
|
72
|
+
|
|
73
|
+
Commands:
|
|
74
|
+
cd [name] Jump to a project (fzf if no name given)
|
|
75
|
+
ls List project names
|
|
76
|
+
logo Display the pm logo
|
|
77
|
+
uninstall Uninstall pm from your system
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--config <path> Path to projects.json (or PM_CONFIG)
|
|
81
|
+
--help Show this help
|
|
82
|
+
--version Show version
|
|
83
|
+
|
|
84
|
+
Running \`pm\` without a command opens the fzf picker.\n`);
|
|
85
|
+
}
|
|
86
|
+
const SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
87
|
+
"cd",
|
|
88
|
+
"logo",
|
|
89
|
+
"ls",
|
|
90
|
+
"uninstall"
|
|
91
|
+
]);
|
|
92
|
+
function fzfSelect(projects) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const lines = projects.map((p) => `${plainLabel(p.name)}\t${expandHome(p.rootPath)}`);
|
|
95
|
+
const proc = spawn("fzf", [
|
|
96
|
+
"--delimiter= ",
|
|
97
|
+
"--with-nth=1",
|
|
98
|
+
"--preview",
|
|
99
|
+
"bat --color=always --style=header,grid --line-range :80 {2}/README.* 2>/dev/null || echo 'No README found'"
|
|
100
|
+
], { stdio: [
|
|
101
|
+
"pipe",
|
|
102
|
+
"pipe",
|
|
103
|
+
"inherit"
|
|
104
|
+
] });
|
|
105
|
+
let stdout = "";
|
|
106
|
+
proc.stdout.on("data", (d) => {
|
|
107
|
+
stdout += d.toString();
|
|
108
|
+
});
|
|
109
|
+
proc.stdin.write(lines.join("\n") + "\n");
|
|
110
|
+
proc.stdin.end();
|
|
111
|
+
proc.on("close", (code) => {
|
|
112
|
+
if (code !== 0) {
|
|
113
|
+
resolve(void 0);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const selected = stdout.trim();
|
|
117
|
+
const idx = lines.indexOf(selected);
|
|
118
|
+
resolve(idx === -1 ? void 0 : projects[idx]);
|
|
119
|
+
});
|
|
120
|
+
proc.on("error", (err) => {
|
|
121
|
+
if (err.code === "ENOENT") {
|
|
122
|
+
process.stderr.write("fzf is not installed. Install it or pass a project name directly.\n");
|
|
123
|
+
resolve(void 0);
|
|
124
|
+
} else reject(err);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function handleArg(state, arg, nextArg) {
|
|
129
|
+
switch (arg) {
|
|
130
|
+
case "--config":
|
|
131
|
+
state.config = nextArg();
|
|
132
|
+
break;
|
|
133
|
+
case "--help":
|
|
134
|
+
state.help = true;
|
|
135
|
+
break;
|
|
136
|
+
case "--version":
|
|
137
|
+
state.version = true;
|
|
138
|
+
break;
|
|
139
|
+
default: if (!state.subcommand && SUBCOMMANDS.has(arg)) state.subcommand = arg;
|
|
140
|
+
else state.rest.push(arg);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function jumpToProject(projects, name) {
|
|
144
|
+
let target;
|
|
145
|
+
if (name) {
|
|
146
|
+
target = findProject(projects, name);
|
|
147
|
+
if (!target) throw new ExitError(`Project not found: ${name}`, 1);
|
|
148
|
+
} else {
|
|
149
|
+
target = await fzfSelect(projects);
|
|
150
|
+
if (!target) throw new ExitError("", 1);
|
|
151
|
+
}
|
|
152
|
+
const dir = expandHome(target.rootPath);
|
|
153
|
+
if (!existsSync(dir)) throw new ExitError(`Directory not found: ${dir}`, 1);
|
|
154
|
+
process.stdout.write(`${dir}\n`);
|
|
155
|
+
}
|
|
156
|
+
async function main() {
|
|
157
|
+
const args = parseArgs(process.argv.slice(2));
|
|
158
|
+
if (args.version) {
|
|
159
|
+
process.stdout.write(`${version}\n`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (args.help) {
|
|
163
|
+
usage();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (args.subcommand === "logo") {
|
|
167
|
+
printLogo();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (args.subcommand === "uninstall") {
|
|
171
|
+
await runUninstall();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const filePath = expandHome(args.config);
|
|
175
|
+
if (!existsSync(filePath)) throw new ExitError(`File not found: ${filePath}`, 1);
|
|
176
|
+
const raw = await readFile(filePath, "utf8");
|
|
177
|
+
const allProjects = JSON.parse(raw);
|
|
178
|
+
switch (args.subcommand) {
|
|
179
|
+
case "cd":
|
|
180
|
+
await jumpToProject(filterProjects(allProjects, []), args.rest[0]);
|
|
181
|
+
break;
|
|
182
|
+
case "ls": {
|
|
183
|
+
const projects = filterProjects(allProjects, []);
|
|
184
|
+
for (const p of projects) process.stdout.write(`${plainLabel(p.name)}\n`);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default:
|
|
188
|
+
await jumpToProject(filterProjects(allProjects, []));
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function parseArgs(argv) {
|
|
193
|
+
const state = {
|
|
194
|
+
config: process.env.PM_CONFIG ?? defaultConfigPath(),
|
|
195
|
+
help: false,
|
|
196
|
+
rest: [],
|
|
197
|
+
subcommand: void 0,
|
|
198
|
+
version: false
|
|
199
|
+
};
|
|
200
|
+
for (let i = 0; i < argv.length; i++) {
|
|
201
|
+
const arg = argv[i];
|
|
202
|
+
if (arg !== void 0) handleArg(state, arg, () => argv[++i] ?? "");
|
|
203
|
+
}
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
function plainLabel(name) {
|
|
207
|
+
return stripEmojiLabel(name) || name;
|
|
208
|
+
}
|
|
209
|
+
function runUninstall() {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
spawn("bash", ["-c", `curl -fsSL "https://raw.githubusercontent.com/nozomiishii/pm/main/uninstall.sh" | bash`], { stdio: "inherit" }).on("close", (code) => {
|
|
212
|
+
process.exitCode = code ?? 0;
|
|
213
|
+
resolve();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await main();
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof ExitError) {
|
|
221
|
+
if (error.message) process.stderr.write(`${error.message}\n`);
|
|
222
|
+
process.exitCode = error.code;
|
|
223
|
+
} else {
|
|
224
|
+
process.stderr.write(`${String(error)}\n`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nozomiishii/pm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Project manager CLI — jump to projects via fzf",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"homepage": "https://github.com/nozomiishii/pm#readme",
|
|
7
6
|
"bugs": {
|
|
8
7
|
"url": "https://github.com/nozomiishii/pm/issues"
|
|
@@ -13,40 +12,62 @@
|
|
|
13
12
|
},
|
|
14
13
|
"license": "MIT",
|
|
15
14
|
"author": "Nozomi Ishii",
|
|
15
|
+
"type": "module",
|
|
16
16
|
"bin": {
|
|
17
|
-
"pm": "./dist/cli.
|
|
17
|
+
"pm": "./dist/cli.mjs"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist/cli.
|
|
20
|
+
"dist/cli.mjs",
|
|
21
21
|
"src/pm.zsh"
|
|
22
22
|
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "bun build --compile src/cli.ts --outfile dist/pm",
|
|
25
|
-
"prepublishOnly": "bun build src/cli.ts --outfile dist/cli.js --target=node --banner '#!/usr/bin/env node'",
|
|
26
|
-
"prepare": "lefthook install --force",
|
|
27
|
-
"typecheck": "tsc --noEmit",
|
|
28
|
-
"dev": "bun run src/cli.ts",
|
|
29
|
-
"demo": "docker run --rm -v \"$(pwd)\":/vhs pm-vhs",
|
|
30
|
-
"demo:all": "bash demo/create.sh",
|
|
31
|
-
"demo:logo": "docker run --rm -v \"$(pwd)\":/vhs pm-vhs demo/logo.tape",
|
|
32
|
-
"logo:colorize": "bun run src/logo/create-logo-color.ts",
|
|
33
|
-
"install:local": "bash install.sh",
|
|
34
|
-
"uninstall:local": "bash uninstall.sh",
|
|
35
|
-
"test": "vitest run"
|
|
36
|
-
},
|
|
37
23
|
"devDependencies": {
|
|
38
|
-
"@nozomiishii/commitlint-config": "1.0
|
|
39
|
-
"@nozomiishii/
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
42
|
-
"
|
|
24
|
+
"@nozomiishii/commitlint-config": "1.11.0",
|
|
25
|
+
"@nozomiishii/eslint-config": "1.11.0",
|
|
26
|
+
"@nozomiishii/lefthook-config": "1.11.0",
|
|
27
|
+
"@nozomiishii/prettier-config": "1.11.0",
|
|
28
|
+
"@nozomiishii/tsconfig": "1.11.0",
|
|
29
|
+
"@tsdown/exe": "0.22.11",
|
|
30
|
+
"@types/node": "24.13.3",
|
|
31
|
+
"eslint": "10.7.0",
|
|
32
|
+
"lefthook": "2.1.10",
|
|
33
|
+
"prettier": "3.9.5",
|
|
34
|
+
"tsdown": "0.22.11",
|
|
35
|
+
"tsx": "4.23.1",
|
|
36
|
+
"typescript": "6.0.3",
|
|
37
|
+
"vitest": "4.1.10",
|
|
38
|
+
"node": "runtime:24.18.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=24"
|
|
42
|
+
},
|
|
43
|
+
"devEngines": {
|
|
44
|
+
"runtime": {
|
|
45
|
+
"name": "node",
|
|
46
|
+
"version": "24.18.0",
|
|
47
|
+
"onFail": "download"
|
|
48
|
+
}
|
|
43
49
|
},
|
|
44
50
|
"publishConfig": {
|
|
45
51
|
"access": "public",
|
|
46
52
|
"provenance": true
|
|
47
53
|
},
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsdown -F lib",
|
|
56
|
+
"build:exe": "tsdown -F exe",
|
|
57
|
+
"demo": "docker run --rm -v \"$(pwd)\":/vhs pm-vhs",
|
|
58
|
+
"demo:all": "bash demo/create.sh",
|
|
59
|
+
"demo:logo": "docker run --rm -v \"$(pwd)\":/vhs pm-vhs demo/logo.tape",
|
|
60
|
+
"dev": "tsx src/cli.ts",
|
|
61
|
+
"eslint": "eslint --max-warnings=0 --cache",
|
|
62
|
+
"format": "pnpm prettier . --check",
|
|
63
|
+
"format:fix": "pnpm prettier . --write",
|
|
64
|
+
"install:local": "bash install.sh",
|
|
65
|
+
"lint": "pnpm eslint",
|
|
66
|
+
"lint:fix": "pnpm eslint --fix",
|
|
67
|
+
"logo:colorize": "tsx src/logo/create-logo-color.ts",
|
|
68
|
+
"prettier": "prettier --ignore-unknown --cache",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"uninstall:local": "bash uninstall.sh"
|
|
51
72
|
}
|
|
52
|
-
}
|
|
73
|
+
}
|
package/dist/cli.js
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/cli.ts
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import path2 from "node:path";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
|
|
9
|
-
// src/filter-projects.ts
|
|
10
|
-
function filterProjects(projects, tags) {
|
|
11
|
-
return projects.filter((p) => {
|
|
12
|
-
if (p.enabled === false)
|
|
13
|
-
return false;
|
|
14
|
-
if (tags.length > 0) {
|
|
15
|
-
return tags.every((t) => p.tags?.includes(t));
|
|
16
|
-
}
|
|
17
|
-
return true;
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// src/expand-home.ts
|
|
22
|
-
import path from "node:path";
|
|
23
|
-
function expandHome(p) {
|
|
24
|
-
if (p.startsWith("~/")) {
|
|
25
|
-
return path.join(process.env.HOME ?? "", p.slice(2));
|
|
26
|
-
}
|
|
27
|
-
if (p === "~") {
|
|
28
|
-
return process.env.HOME ?? "";
|
|
29
|
-
}
|
|
30
|
-
return p;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// src/strip-emoji-label.ts
|
|
34
|
-
function stripEmojiLabel(s) {
|
|
35
|
-
let t = s.normalize("NFC");
|
|
36
|
-
for (let i = 0;i < 4; i++) {
|
|
37
|
-
t = t.replace(/\p{Extended_Pictographic}/gu, "").replace(/\p{Emoji_Modifier}/gu, "").replace(/\u200d/g, "").replace(/\ufe0f/g, "").replace(/\ufe0e/g, "");
|
|
38
|
-
}
|
|
39
|
-
return t.replace(/\s+/g, " ").trim();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/find-project.ts
|
|
43
|
-
function findProject(projects, query) {
|
|
44
|
-
const matches = projects.filter((p) => {
|
|
45
|
-
if (p.name === query)
|
|
46
|
-
return true;
|
|
47
|
-
const plain = stripEmojiLabel(p.name);
|
|
48
|
-
return plain === query;
|
|
49
|
-
});
|
|
50
|
-
if (matches.length > 1) {
|
|
51
|
-
throw new Error(`Ambiguous name "${query}" matches: ${matches.map((p) => p.name).join(", ")}`);
|
|
52
|
-
}
|
|
53
|
-
return matches[0];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// src/logo/logo-color.ascii
|
|
57
|
-
var logo_color_default = `\x1B[38;2;120;180;255m zzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m
|
|
58
|
-
\x1B[38;2;100;160;245m zzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m
|
|
59
|
-
\x1B[38;2;80;140;235mzzzzz.----.______.zzzzzzzzz\x1B[0m
|
|
60
|
-
\x1B[38;2;60;120;225mzzzzz | __________zzzzzz\x1B[0m
|
|
61
|
-
\x1B[38;2;45;100;215mzzzzz | / /zzzz\x1B[0m
|
|
62
|
-
\x1B[38;2;30;80;205mzzzzz | / /zzzzz\x1B[0m
|
|
63
|
-
\x1B[38;2;20;65;195mzzzzz | / pm /zzzzzz\x1B[0m
|
|
64
|
-
\x1B[38;2;10;50;185mzzzzz |/__________ /zzzzzzz\x1B[0m
|
|
65
|
-
\x1B[38;2;10;50;185mnozozzzzzzzzzzzzzzzzzzzzzz\x1B[0m
|
|
66
|
-
\x1B[38;2;10;50;185mzzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m
|
|
67
|
-
`;
|
|
68
|
-
// package.json
|
|
69
|
-
var package_default = {
|
|
70
|
-
name: "@nozomiishii/pm",
|
|
71
|
-
version: "0.2.1",
|
|
72
|
-
description: "Project manager CLI — jump to projects via fzf",
|
|
73
|
-
type: "module",
|
|
74
|
-
homepage: "https://github.com/nozomiishii/pm#readme",
|
|
75
|
-
bugs: {
|
|
76
|
-
url: "https://github.com/nozomiishii/pm/issues"
|
|
77
|
-
},
|
|
78
|
-
repository: {
|
|
79
|
-
type: "git",
|
|
80
|
-
url: "git+https://github.com/nozomiishii/pm.git"
|
|
81
|
-
},
|
|
82
|
-
license: "MIT",
|
|
83
|
-
author: "Nozomi Ishii",
|
|
84
|
-
bin: {
|
|
85
|
-
pm: "./dist/cli.js"
|
|
86
|
-
},
|
|
87
|
-
files: [
|
|
88
|
-
"dist/cli.js",
|
|
89
|
-
"src/pm.zsh"
|
|
90
|
-
],
|
|
91
|
-
scripts: {
|
|
92
|
-
build: "bun build --compile src/cli.ts --outfile dist/pm",
|
|
93
|
-
prepublishOnly: "bun build src/cli.ts --outfile dist/cli.js --target=node --banner '#!/usr/bin/env node'",
|
|
94
|
-
prepare: "lefthook install --force",
|
|
95
|
-
typecheck: "tsc --noEmit",
|
|
96
|
-
dev: "bun run src/cli.ts",
|
|
97
|
-
demo: 'docker run --rm -v "$(pwd)":/vhs pm-vhs',
|
|
98
|
-
"demo:all": "bash demo/create.sh",
|
|
99
|
-
"demo:logo": 'docker run --rm -v "$(pwd)":/vhs pm-vhs demo/logo.tape',
|
|
100
|
-
"logo:colorize": "bun run src/logo/create-logo-color.ts",
|
|
101
|
-
"install:local": "bash install.sh",
|
|
102
|
-
"uninstall:local": "bash uninstall.sh",
|
|
103
|
-
test: "vitest run"
|
|
104
|
-
},
|
|
105
|
-
devDependencies: {
|
|
106
|
-
"@nozomiishii/commitlint-config": "1.0.1",
|
|
107
|
-
"@nozomiishii/lefthook-config": "1.0.1",
|
|
108
|
-
"@types/node": "25.6.0",
|
|
109
|
-
lefthook: "2.1.6",
|
|
110
|
-
vitest: "4.1.5"
|
|
111
|
-
},
|
|
112
|
-
publishConfig: {
|
|
113
|
-
access: "public",
|
|
114
|
-
provenance: true
|
|
115
|
-
},
|
|
116
|
-
packageManager: "bun@1.3.11",
|
|
117
|
-
engines: {
|
|
118
|
-
node: "24.15.0"
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// src/cli.ts
|
|
123
|
-
function defaultConfigPath() {
|
|
124
|
-
const home = process.env.HOME ?? "";
|
|
125
|
-
switch (process.platform) {
|
|
126
|
-
case "darwin":
|
|
127
|
-
return path2.join(home, "Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json");
|
|
128
|
-
case "win32":
|
|
129
|
-
return path2.join(process.env.APPDATA ?? "", "Code/User/globalStorage/alefragnani.project-manager/projects.json");
|
|
130
|
-
default:
|
|
131
|
-
return path2.join(home, ".config/Code/User/globalStorage/alefragnani.project-manager/projects.json");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function usage() {
|
|
135
|
-
console.log(`Usage: pm [options] [command]
|
|
136
|
-
|
|
137
|
-
Commands:
|
|
138
|
-
cd [name] Jump to a project (fzf if no name given)
|
|
139
|
-
ls List project names
|
|
140
|
-
logo Display the pm logo
|
|
141
|
-
uninstall Uninstall pm from your system
|
|
142
|
-
|
|
143
|
-
Options:
|
|
144
|
-
--config <path> Path to projects.json (or PM_CONFIG)
|
|
145
|
-
--help Show this help
|
|
146
|
-
--version Show version
|
|
147
|
-
|
|
148
|
-
Running \`pm\` without a command opens the fzf picker.`);
|
|
149
|
-
}
|
|
150
|
-
function printLogo() {
|
|
151
|
-
console.log(logo_color_default);
|
|
152
|
-
}
|
|
153
|
-
var SUBCOMMANDS = new Set(["cd", "ls", "logo", "uninstall"]);
|
|
154
|
-
function parseArgs(argv) {
|
|
155
|
-
let config = process.env.PM_CONFIG ?? defaultConfigPath();
|
|
156
|
-
let help = false;
|
|
157
|
-
let version = false;
|
|
158
|
-
let subcommand;
|
|
159
|
-
const rest = [];
|
|
160
|
-
for (let i = 0;i < argv.length; i++) {
|
|
161
|
-
const arg = argv[i];
|
|
162
|
-
if (arg === "--config") {
|
|
163
|
-
config = argv[++i] ?? "";
|
|
164
|
-
} else if (arg === "--help") {
|
|
165
|
-
help = true;
|
|
166
|
-
} else if (arg === "--version") {
|
|
167
|
-
version = true;
|
|
168
|
-
} else if (!subcommand && SUBCOMMANDS.has(arg)) {
|
|
169
|
-
subcommand = arg;
|
|
170
|
-
} else {
|
|
171
|
-
rest.push(arg);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return { config, help, version, subcommand, rest };
|
|
175
|
-
}
|
|
176
|
-
function plainLabel(name) {
|
|
177
|
-
return stripEmojiLabel(name) || name;
|
|
178
|
-
}
|
|
179
|
-
function fzfSelect(projects) {
|
|
180
|
-
return new Promise((resolve, reject) => {
|
|
181
|
-
const lines = projects.map((p) => `${plainLabel(p.name)} ${expandHome(p.rootPath)}`);
|
|
182
|
-
const proc = spawn("fzf", [
|
|
183
|
-
"--delimiter=\t",
|
|
184
|
-
"--with-nth=1",
|
|
185
|
-
"--preview",
|
|
186
|
-
"bat --color=always --style=header,grid --line-range :80 {2}/README.* 2>/dev/null || echo 'No README found'"
|
|
187
|
-
], {
|
|
188
|
-
stdio: ["pipe", "pipe", "inherit"]
|
|
189
|
-
});
|
|
190
|
-
let stdout = "";
|
|
191
|
-
proc.stdout.on("data", (d) => {
|
|
192
|
-
stdout += d.toString();
|
|
193
|
-
});
|
|
194
|
-
proc.stdin.write(lines.join(`
|
|
195
|
-
`) + `
|
|
196
|
-
`);
|
|
197
|
-
proc.stdin.end();
|
|
198
|
-
proc.on("close", (code) => {
|
|
199
|
-
if (code !== 0) {
|
|
200
|
-
resolve(undefined);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const selected = stdout.trim();
|
|
204
|
-
const idx = lines.indexOf(selected);
|
|
205
|
-
resolve(idx >= 0 ? projects[idx] : undefined);
|
|
206
|
-
});
|
|
207
|
-
proc.on("error", (err) => {
|
|
208
|
-
if (err.code === "ENOENT") {
|
|
209
|
-
console.error("fzf is not installed. Install it or pass a project name directly.");
|
|
210
|
-
resolve(undefined);
|
|
211
|
-
} else {
|
|
212
|
-
reject(err);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
async function jumpToProject(projects, name) {
|
|
218
|
-
let target;
|
|
219
|
-
if (name) {
|
|
220
|
-
target = findProject(projects, name);
|
|
221
|
-
if (!target) {
|
|
222
|
-
console.error(`Project not found: ${name}`);
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
} else {
|
|
226
|
-
target = await fzfSelect(projects);
|
|
227
|
-
if (!target) {
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const dir = expandHome(target.rootPath);
|
|
232
|
-
if (!existsSync(dir)) {
|
|
233
|
-
console.error(`Directory not found: ${dir}`);
|
|
234
|
-
process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
console.log(dir);
|
|
237
|
-
}
|
|
238
|
-
async function main() {
|
|
239
|
-
const args = parseArgs(process.argv.slice(2));
|
|
240
|
-
if (args.version) {
|
|
241
|
-
console.log(package_default.version);
|
|
242
|
-
process.exit(0);
|
|
243
|
-
}
|
|
244
|
-
if (args.help) {
|
|
245
|
-
usage();
|
|
246
|
-
process.exit(0);
|
|
247
|
-
}
|
|
248
|
-
if (args.subcommand === "logo") {
|
|
249
|
-
printLogo();
|
|
250
|
-
process.exit(0);
|
|
251
|
-
}
|
|
252
|
-
if (args.subcommand === "uninstall") {
|
|
253
|
-
const url = "https://raw.githubusercontent.com/nozomiishii/pm/main/uninstall.sh";
|
|
254
|
-
const proc = spawn("bash", ["-c", `curl -fsSL "${url}" | bash`], {
|
|
255
|
-
stdio: "inherit"
|
|
256
|
-
});
|
|
257
|
-
proc.on("close", (code) => process.exit(code ?? 0));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const filePath = expandHome(args.config);
|
|
261
|
-
if (!existsSync(filePath)) {
|
|
262
|
-
console.error(`File not found: ${filePath}`);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
const raw = await readFile(filePath, "utf-8");
|
|
266
|
-
const allProjects = JSON.parse(raw);
|
|
267
|
-
switch (args.subcommand) {
|
|
268
|
-
case "ls": {
|
|
269
|
-
const projects = filterProjects(allProjects, []);
|
|
270
|
-
for (const p of projects) {
|
|
271
|
-
console.log(plainLabel(p.name));
|
|
272
|
-
}
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
case "cd": {
|
|
276
|
-
const projects = filterProjects(allProjects, []);
|
|
277
|
-
await jumpToProject(projects, args.rest[0]);
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
default: {
|
|
281
|
-
const projects = filterProjects(allProjects, []);
|
|
282
|
-
await jumpToProject(projects);
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
main().catch((err) => {
|
|
288
|
-
console.error(err);
|
|
289
|
-
process.exit(1);
|
|
290
|
-
});
|