@nozomiishii/pm 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nozomi Ishii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,208 @@
1
+ # pm - VS Code Project Manager CLI
2
+
3
+ [English](./README.md) | 日本語
4
+
5
+ <br>
6
+ <div align="center">
7
+ <img src="demo/logo.gif" alt="logo" width="480" />
8
+ </div>
9
+ <br>
10
+
11
+ [VS Code Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) をターミナルでも使いたい!
12
+
13
+ `pm`コマンドだったらVS Code Project Managerに登録されたプロジェクトへ移動できます。
14
+
15
+ ## Prerequisites
16
+
17
+ `pm`はインタラクティブなプロジェクト選択に[fzf](https://github.com/junegunn/fzf) を使用します。pm をセットアップする前にインストールしてください。
18
+
19
+ ```sh
20
+ # macOS
21
+ brew install fzf
22
+
23
+ # Debian / Ubuntu
24
+ sudo apt install fzf
25
+
26
+ # Fedora
27
+ sudo dnf install fzf
28
+
29
+ # Arch Linux
30
+ sudo pacman -S fzf
31
+ ```
32
+
33
+ ## Install
34
+
35
+ ### macOS/Linux (推奨)
36
+
37
+ ```sh
38
+ curl -fsSL https://raw.githubusercontent.com/nozomiishii/pm/main/install.sh | bash
39
+ ```
40
+
41
+ `pm`のバイナリが `~/.pm/bin/pm` にダウンロードされ、`pm`を呼び出すラッパースクリプトが `~/.pm/pm.zsh` に配置されます。`.zshrc` への設定も自動で追加されます。
42
+
43
+ ターミナルを再起動するか、`source ~/.zshrc` を実行すると`pm`が使えるようになります。
44
+
45
+ ### npm
46
+
47
+ ```sh
48
+ npm install -g @nozomiishii/pm
49
+ ```
50
+
51
+ ## Uninstall
52
+
53
+ ```sh
54
+ pm uninstall
55
+ ```
56
+
57
+ バイナリ・設定ファイル・`.zshrc` への追記をすべて削除します。
58
+
59
+ ## Usage
60
+
61
+ ### pm --help
62
+
63
+ ```
64
+ Usage: pm [options] [command]
65
+
66
+ Commands:
67
+ cd [name] Jump to a project (fzf if no name given)
68
+ ls List project names
69
+ logo Display the pm logo
70
+ uninstall Uninstall pm from your system
71
+ create-workspace Generate a .code-workspace file
72
+ --name <name> Workspace name (outputs <name>.code-workspace)
73
+ --tag <name> Include only projects with this tag (repeatable)
74
+
75
+ Options:
76
+ --config <path> Path to projects.json (or PM_CONFIG)
77
+ --help Show this help
78
+
79
+ Running `pm` without a command opens the fzf picker.
80
+ ```
81
+
82
+ ```sh
83
+ pm --help
84
+ ```
85
+
86
+ ![pm --help](demo/pm-help.gif)
87
+
88
+ ### pm
89
+
90
+ fzfピッカーを開いて、選択したプロジェクトへ移動します。
91
+
92
+ ```sh
93
+ pm
94
+ ```
95
+
96
+ ![pm](demo/pm.gif)
97
+
98
+ ### pm cd
99
+
100
+ プロジェクト名を指定して移動します。名前を省略するとfzfが開きます。
101
+
102
+ ```sh
103
+ pm cd <name>
104
+ ```
105
+
106
+ ![pm cd](demo/pm-cd.gif)
107
+
108
+ ### pm ls
109
+
110
+ プロジェクト名を一覧表示します。
111
+
112
+ ```sh
113
+ pm ls
114
+ ```
115
+
116
+ ![pm ls](demo/pm-ls.gif)
117
+
118
+ ### pm create-workspace
119
+
120
+ `--tag` で指定したタグのプロジェクトを `.code-workspace` ファイルにまとめます。
121
+
122
+ ```sh
123
+ pm create-workspace --name <name> --tag <tag>
124
+ ```
125
+
126
+ たとえば `projects.json` に以下のプロジェクトがある場合:
127
+
128
+ ```json
129
+ [
130
+ { "name": "dotfiles", "rootPath": "~/Code/nozomiishii/dotfiles", "tags": ["personal"] },
131
+ { "name": "portfolio", "rootPath": "~/Code/nozomiishii/portfolio", "tags": ["personal"] },
132
+ { "name": "fzf", "rootPath": "~/Code/junegunn/fzf", "tags": ["oss"] }
133
+ ]
134
+ ```
135
+
136
+ 次のコマンドを実行することで
137
+
138
+ ```sh
139
+ pm create-workspace --name my-workspace --tag personal
140
+ ```
141
+
142
+ `my-workspace.code-workspace` が生成されます:
143
+
144
+ ```json
145
+ {
146
+ "folders": [
147
+ { "name": "dotfiles", "path": "../nozomiishii/dotfiles" },
148
+ { "name": "portfolio", "path": "../nozomiishii/portfolio" }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ `--tag` は複数指定でき、すべてのタグを持つプロジェクトだけが含まれます。
154
+
155
+ ## Configuration
156
+
157
+ インストーラーが `.zshrc` を自動で設定しますが、手動でセットアップする場合は以下を追加してください。
158
+
159
+ ```sh
160
+ # (任意) projects.json のパスを指定。省略すると VS Code Project Manager のデフォルトパスを使用
161
+ export PM_CONFIG="$HOME/path/to/projects.json"
162
+
163
+ # 必須設定
164
+ export PATH="$HOME/.pm/bin:$PATH"
165
+ source "$HOME/.pm/pm.zsh"
166
+ ```
167
+
168
+ ### PM_CONFIG
169
+
170
+ pm は `projects.json` ファイルからプロジェクト情報を読み込みます。`PM_CONFIG` を省略した場合は [VS Code Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) のデフォルトパスを参照します。
171
+
172
+ | OS | デフォルトパス |
173
+ | --- | --- |
174
+ | macOS | `~/Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json` |
175
+ | Linux | `~/.config/Code/User/globalStorage/alefragnani.project-manager/projects.json` |
176
+ | Windows | `%APPDATA%/Code/User/globalStorage/alefragnani.project-manager/projects.json` |
177
+
178
+ `--config` フラグで一時的にパスを指定することもできます。
179
+
180
+ ```sh
181
+ pm --config ./projects.json ls
182
+ ```
183
+
184
+ ### source "$HOME/.pm/pm.zsh"
185
+
186
+ `pm` バイナリは別プロセスで実行されるため、バイナリ内で `cd` しても呼び出し元のシェルのディレクトリは変わりません。`source "$HOME/.pm/pm.zsh"` で読み込まれるシェル関数が、バイナリの出力がディレクトリだった場合に現在のシェルで `cd` を実行します。
187
+
188
+ ```sh
189
+ # pm.zsh がやっていること (簡略版)
190
+ pm() {
191
+ local output
192
+ output="$(command pm "$@")" # バイナリを実行
193
+ [[ -d "$output" ]] && cd "$output" # 出力がディレクトリなら cd
194
+ }
195
+ ```
196
+
197
+ この仕組みにより、プロジェクト名のタブ補完もサポートしています。
198
+
199
+ ## 謝辞
200
+
201
+ pm は以下のプロジェクトから大きな影響を受けています。僕の生産性を上げてくれて本当にありがとうございます
202
+
203
+ - [VS Code Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) — `projects.json` ベースのプロジェクト切り替えを切り開いた拡張機能です。最高で最高です。
204
+ - [Raycast VSCode Project Manager](https://www.raycast.com/MarkusLanger/vscode-project-manager) — RaycastからVS Code Project Managerが使えます。アメージングです。
205
+
206
+ ## License
207
+
208
+ [MIT](LICENSE)
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # pm - VS Code Project Manager CLI
2
+
3
+ English | [日本語](./README.ja.md)
4
+
5
+ <br>
6
+ <div align="center">
7
+ <img src="demo/logo.gif" alt="logo" width="480" />
8
+ </div>
9
+ <br>
10
+
11
+ I wanted to use [VS Code Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) from the terminal too!
12
+
13
+ With the `pm` command, you can jump to any project registered in VS Code Project Manager.
14
+
15
+ ## Prerequisites
16
+
17
+ `pm` uses [fzf](https://github.com/junegunn/fzf) for interactive project selection. Install it before setting up pm.
18
+
19
+ ```sh
20
+ # macOS
21
+ brew install fzf
22
+
23
+ # Debian / Ubuntu
24
+ sudo apt install fzf
25
+
26
+ # Fedora
27
+ sudo dnf install fzf
28
+
29
+ # Arch Linux
30
+ sudo pacman -S fzf
31
+ ```
32
+
33
+ ## Install
34
+
35
+ ### macOS/Linux (recommended)
36
+
37
+ ```sh
38
+ curl -fsSL https://raw.githubusercontent.com/nozomiishii/pm/main/install.sh | bash
39
+ ```
40
+
41
+ The `pm` binary is downloaded to `~/.pm/bin/pm`, and the wrapper script that calls `pm` is placed at `~/.pm/pm.zsh`. The `.zshrc` configuration is added automatically.
42
+
43
+ Restart your terminal or run `source ~/.zshrc` to start using pm.
44
+
45
+ ### npm
46
+
47
+ ```sh
48
+ npm install -g @nozomiishii/pm
49
+ ```
50
+
51
+ ## Uninstall
52
+
53
+ ```sh
54
+ pm uninstall
55
+ ```
56
+
57
+ Removes the binary, config files, and `.zshrc` entries.
58
+
59
+ ## Usage
60
+
61
+ ### pm --help
62
+
63
+ ```
64
+ Usage: pm [options] [command]
65
+
66
+ Commands:
67
+ cd [name] Jump to a project (fzf if no name given)
68
+ ls List project names
69
+ logo Display the pm logo
70
+ uninstall Uninstall pm from your system
71
+ create-workspace Generate a .code-workspace file
72
+ --name <name> Workspace name (outputs <name>.code-workspace)
73
+ --tag <name> Include only projects with this tag (repeatable)
74
+
75
+ Options:
76
+ --config <path> Path to projects.json (or PM_CONFIG)
77
+ --help Show this help
78
+
79
+ Running `pm` without a command opens the fzf picker.
80
+ ```
81
+
82
+ ```sh
83
+ pm --help
84
+ ```
85
+
86
+ ![pm --help](demo/pm-help.gif)
87
+
88
+ ### pm
89
+
90
+ Opens fzf picker and jumps to the selected project.
91
+
92
+ ```sh
93
+ pm
94
+ ```
95
+
96
+ ![pm](demo/pm.gif)
97
+
98
+ ### pm cd
99
+
100
+ Jumps to a project by name. Falls back to fzf if no name is given.
101
+
102
+ ```sh
103
+ pm cd <name>
104
+ ```
105
+
106
+ ![pm cd](demo/pm-cd.gif)
107
+
108
+ ### pm ls
109
+
110
+ Lists all project names.
111
+
112
+ ```sh
113
+ pm ls
114
+ ```
115
+
116
+ ![pm ls](demo/pm-ls.gif)
117
+
118
+ ### pm create-workspace
119
+
120
+ Bundles projects matching a `--tag` into a `.code-workspace` file.
121
+
122
+ ```sh
123
+ pm create-workspace --name <name> --tag <tag>
124
+ ```
125
+
126
+ For example, given the following `projects.json`:
127
+
128
+ ```json
129
+ [
130
+ { "name": "dotfiles", "rootPath": "~/Code/nozomiishii/dotfiles", "tags": ["personal"] },
131
+ { "name": "portfolio", "rootPath": "~/Code/nozomiishii/portfolio", "tags": ["personal"] },
132
+ { "name": "fzf", "rootPath": "~/Code/junegunn/fzf", "tags": ["oss"] }
133
+ ]
134
+ ```
135
+
136
+ Running the following command:
137
+
138
+ ```sh
139
+ pm create-workspace --name my-workspace --tag personal
140
+ ```
141
+
142
+ Generates `my-workspace.code-workspace`:
143
+
144
+ ```json
145
+ {
146
+ "folders": [
147
+ { "name": "dotfiles", "path": "../nozomiishii/dotfiles" },
148
+ { "name": "portfolio", "path": "../nozomiishii/portfolio" }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ `--tag` can be specified multiple times — only projects matching all tags are included.
154
+
155
+ ## Configuration
156
+
157
+ The installer configures `.zshrc` automatically. For manual setup, add the following:
158
+
159
+ ```sh
160
+ # (Optional) Path to projects.json. Defaults to the VS Code Project Manager path
161
+ export PM_CONFIG="$HOME/path/to/projects.json"
162
+
163
+ # Required
164
+ export PATH="$HOME/.pm/bin:$PATH"
165
+ source "$HOME/.pm/pm.zsh"
166
+ ```
167
+
168
+ ### PM_CONFIG
169
+
170
+ pm reads project data from a `projects.json` file. If `PM_CONFIG` is omitted, it defaults to the [VS Code Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) config path.
171
+
172
+ | OS | Default path |
173
+ | --- | --- |
174
+ | macOS | `~/Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json` |
175
+ | Linux | `~/.config/Code/User/globalStorage/alefragnani.project-manager/projects.json` |
176
+ | Windows | `%APPDATA%/Code/User/globalStorage/alefragnani.project-manager/projects.json` |
177
+
178
+ You can also override the path temporarily with the `--config` flag:
179
+
180
+ ```sh
181
+ pm --config ./projects.json ls
182
+ ```
183
+
184
+ ### source "$HOME/.pm/pm.zsh"
185
+
186
+ The `pm` binary runs as a separate process, so `cd` inside the binary cannot change the calling shell's directory. The shell function loaded by `source "$HOME/.pm/pm.zsh"` runs `cd` in the current shell when the binary outputs a directory path.
187
+
188
+ ```sh
189
+ # What pm.zsh does (simplified)
190
+ pm() {
191
+ local output
192
+ output="$(command pm "$@")" # Run the binary
193
+ [[ -d "$output" ]] && cd "$output" # cd if output is a directory
194
+ }
195
+ ```
196
+
197
+ This mechanism also provides tab completion for project names.
198
+
199
+ ## Acknowledgments
200
+
201
+ pm is heavily inspired by these projects. Thank you so much for boosting my productivity.
202
+
203
+ - [VS Code Project Manager](https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager) — The extension that pioneered `projects.json`-based project switching. The best of the best.
204
+ - [Raycast VSCode Project Manager](https://www.raycast.com/MarkusLanger/vscode-project-manager) — Use VS Code Project Manager from Raycast. Amazing.
205
+
206
+ ## License
207
+
208
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,291 @@
1
+ // src/cli.ts
2
+ import { existsSync } from "node:fs";
3
+ import { readFile as readFile2, writeFile } from "node:fs/promises";
4
+ import path3 from "node:path";
5
+ import { spawn } from "node:child_process";
6
+
7
+ // src/filter-projects.ts
8
+ function filterProjects(projects, tags) {
9
+ return projects.filter((p) => {
10
+ if (p.enabled === false)
11
+ return false;
12
+ if (tags.length > 0) {
13
+ return tags.every((t) => p.tags?.includes(t));
14
+ }
15
+ return true;
16
+ });
17
+ }
18
+
19
+ // src/expand-home.ts
20
+ import path from "node:path";
21
+ function expandHome(p) {
22
+ if (p.startsWith("~/")) {
23
+ return path.join(process.env.HOME ?? "", p.slice(2));
24
+ }
25
+ if (p === "~") {
26
+ return process.env.HOME ?? "";
27
+ }
28
+ return p;
29
+ }
30
+
31
+ // src/strip-emoji-label.ts
32
+ function stripEmojiLabel(s) {
33
+ let t = s.normalize("NFC");
34
+ for (let i = 0;i < 4; i++) {
35
+ t = t.replace(/\p{Extended_Pictographic}/gu, "").replace(/\p{Emoji_Modifier}/gu, "").replace(/\u200d/g, "").replace(/\ufe0f/g, "").replace(/\ufe0e/g, "");
36
+ }
37
+ return t.replace(/\s+/g, " ").trim();
38
+ }
39
+
40
+ // src/find-project.ts
41
+ function findProject(projects, query) {
42
+ const matches = projects.filter((p) => {
43
+ if (p.name === query)
44
+ return true;
45
+ const plain = stripEmojiLabel(p.name);
46
+ return plain === query;
47
+ });
48
+ if (matches.length > 1) {
49
+ throw new Error(`Ambiguous name "${query}" matches: ${matches.map((p) => p.name).join(", ")}`);
50
+ }
51
+ return matches[0];
52
+ }
53
+
54
+ // src/create-workspace/build-folders.ts
55
+ import path2 from "node:path";
56
+ function buildFolders(projects, opts) {
57
+ const filtered = filterProjects(projects, opts.tags);
58
+ const folders = [];
59
+ for (const p of filtered) {
60
+ const absolute = path2.resolve(expandHome(p.rootPath));
61
+ let rel = path2.relative(opts.workspaceDir, absolute);
62
+ if (!rel || rel === "") {
63
+ rel = ".";
64
+ }
65
+ const relPosix = rel.split(path2.sep).join("/");
66
+ const name = stripEmojiLabel(p.name) || path2.basename(absolute);
67
+ folders.push({ name, path: relPosix });
68
+ }
69
+ return folders;
70
+ }
71
+
72
+ // src/create-workspace/load-existing-workspace.ts
73
+ import { readFile } from "node:fs/promises";
74
+ async function loadExistingWorkspace(workspacePath) {
75
+ try {
76
+ const raw = await readFile(workspacePath, "utf8");
77
+ const parsed = JSON.parse(raw);
78
+ const { folders: _f, ...rest } = parsed;
79
+ return rest;
80
+ } catch {
81
+ return {};
82
+ }
83
+ }
84
+
85
+ // src/logo/logo-color.ascii
86
+ var logo_color_default = `\x1B[38;2;120;180;255m zzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m
87
+ \x1B[38;2;100;160;245m zzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m
88
+ \x1B[38;2;80;140;235mzzzzz.----.______.zzzzzzzzz\x1B[0m
89
+ \x1B[38;2;60;120;225mzzzzz | __________zzzzzz\x1B[0m
90
+ \x1B[38;2;45;100;215mzzzzz | / /zzzz\x1B[0m
91
+ \x1B[38;2;30;80;205mzzzzz | / /zzzzz\x1B[0m
92
+ \x1B[38;2;20;65;195mzzzzz | / pm /zzzzzz\x1B[0m
93
+ \x1B[38;2;10;50;185mzzzzz |/__________ /zzzzzzz\x1B[0m
94
+ \x1B[38;2;10;50;185mnozozzzzzzzzzzzzzzzzzzzzzz\x1B[0m
95
+ \x1B[38;2;10;50;185mzzzzzzzzzzzzzzzzzzzzzzzzzz\x1B[0m
96
+ `;
97
+
98
+ // src/cli.ts
99
+ function defaultConfigPath() {
100
+ const home = process.env.HOME ?? "";
101
+ switch (process.platform) {
102
+ case "darwin":
103
+ return path3.join(home, "Library/Application Support/Code/User/globalStorage/alefragnani.project-manager/projects.json");
104
+ case "win32":
105
+ return path3.join(process.env.APPDATA ?? "", "Code/User/globalStorage/alefragnani.project-manager/projects.json");
106
+ default:
107
+ return path3.join(home, ".config/Code/User/globalStorage/alefragnani.project-manager/projects.json");
108
+ }
109
+ }
110
+ function usage() {
111
+ console.log(`Usage: pm [options] [command]
112
+
113
+ Commands:
114
+ cd [name] Jump to a project (fzf if no name given)
115
+ ls List project names
116
+ logo Display the pm logo
117
+ uninstall Uninstall pm from your system
118
+ create-workspace Generate a .code-workspace file
119
+ --name <name> Workspace name (outputs <name>.code-workspace)
120
+ --tag <name> Include only projects with this tag (repeatable)
121
+
122
+ Options:
123
+ --config <path> Path to projects.json (or PM_CONFIG)
124
+ --help Show this help
125
+
126
+ Running \`pm\` without a command opens the fzf picker.`);
127
+ }
128
+ function printLogo() {
129
+ console.log(logo_color_default);
130
+ }
131
+ var SUBCOMMANDS = new Set(["cd", "ls", "create-workspace", "logo", "uninstall"]);
132
+ function parseArgs(argv) {
133
+ let config = process.env.PM_CONFIG ?? defaultConfigPath();
134
+ let help = false;
135
+ let subcommand;
136
+ const rest = [];
137
+ for (let i = 0;i < argv.length; i++) {
138
+ const arg = argv[i];
139
+ if (arg === "--config") {
140
+ config = argv[++i] ?? "";
141
+ } else if (arg === "--help") {
142
+ help = true;
143
+ } else if (!subcommand && SUBCOMMANDS.has(arg)) {
144
+ subcommand = arg;
145
+ } else {
146
+ rest.push(arg);
147
+ }
148
+ }
149
+ return { config, help, subcommand, rest };
150
+ }
151
+ function parseCreateWorkspaceArgs(rest) {
152
+ let workspaceName = "";
153
+ const tags = [];
154
+ for (let i = 0;i < rest.length; i++) {
155
+ const arg = rest[i];
156
+ if (arg === "--name") {
157
+ workspaceName = rest[++i] ?? "";
158
+ } else if (arg === "--tag") {
159
+ tags.push(rest[++i] ?? "");
160
+ }
161
+ }
162
+ return { workspaceName, tags };
163
+ }
164
+ function plainLabel(name) {
165
+ return stripEmojiLabel(name) || name;
166
+ }
167
+ function fzfSelect(projects) {
168
+ return new Promise((resolve, reject) => {
169
+ const labels = projects.map((p) => plainLabel(p.name));
170
+ const proc = spawn("fzf", [], {
171
+ stdio: ["pipe", "pipe", "inherit"]
172
+ });
173
+ let stdout = "";
174
+ proc.stdout.on("data", (d) => {
175
+ stdout += d.toString();
176
+ });
177
+ proc.stdin.write(labels.join(`
178
+ `) + `
179
+ `);
180
+ proc.stdin.end();
181
+ proc.on("close", (code) => {
182
+ if (code !== 0) {
183
+ resolve(undefined);
184
+ return;
185
+ }
186
+ const selected = stdout.trim();
187
+ const idx = labels.indexOf(selected);
188
+ resolve(idx >= 0 ? projects[idx] : undefined);
189
+ });
190
+ proc.on("error", (err) => {
191
+ if (err.code === "ENOENT") {
192
+ console.error("fzf is not installed. Install it or pass a project name directly.");
193
+ resolve(undefined);
194
+ } else {
195
+ reject(err);
196
+ }
197
+ });
198
+ });
199
+ }
200
+ function resolveCliPath(p) {
201
+ return path3.isAbsolute(p) ? path3.normalize(p) : path3.resolve(process.cwd(), p);
202
+ }
203
+ async function createWorkspace(projects, args) {
204
+ if (!args.workspaceName) {
205
+ console.error("Error: --name is required with create-workspace.");
206
+ process.exit(1);
207
+ }
208
+ const workspacePath = resolveCliPath(`${args.workspaceName}.code-workspace`);
209
+ const workspaceDir = path3.dirname(path3.resolve(workspacePath));
210
+ const folders = buildFolders(projects, { tags: args.tags, workspaceDir });
211
+ const preserved = await loadExistingWorkspace(workspacePath);
212
+ const out = { ...preserved, folders };
213
+ await writeFile(workspacePath, JSON.stringify(out, null, 2) + `
214
+ `, "utf8");
215
+ console.log(`Wrote ${workspacePath} (${folders.length} folders)`);
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.help) {
241
+ usage();
242
+ process.exit(0);
243
+ }
244
+ if (args.subcommand === "logo") {
245
+ printLogo();
246
+ process.exit(0);
247
+ }
248
+ if (args.subcommand === "uninstall") {
249
+ const url = "https://raw.githubusercontent.com/nozomiishii/pm/main/uninstall.sh";
250
+ const proc = spawn("bash", ["-c", `curl -fsSL "${url}" | bash`], {
251
+ stdio: "inherit"
252
+ });
253
+ proc.on("close", (code) => process.exit(code ?? 0));
254
+ return;
255
+ }
256
+ const filePath = expandHome(args.config);
257
+ if (!existsSync(filePath)) {
258
+ console.error(`File not found: ${filePath}`);
259
+ process.exit(1);
260
+ }
261
+ const raw = await readFile2(filePath, "utf-8");
262
+ const allProjects = JSON.parse(raw);
263
+ switch (args.subcommand) {
264
+ case "create-workspace": {
265
+ const cwArgs = parseCreateWorkspaceArgs(args.rest);
266
+ await createWorkspace(allProjects, cwArgs);
267
+ break;
268
+ }
269
+ case "ls": {
270
+ const projects = filterProjects(allProjects, []);
271
+ for (const p of projects) {
272
+ console.log(plainLabel(p.name));
273
+ }
274
+ break;
275
+ }
276
+ case "cd": {
277
+ const projects = filterProjects(allProjects, []);
278
+ await jumpToProject(projects, args.rest[0]);
279
+ break;
280
+ }
281
+ default: {
282
+ const projects = filterProjects(allProjects, []);
283
+ await jumpToProject(projects);
284
+ break;
285
+ }
286
+ }
287
+ }
288
+ main().catch((err) => {
289
+ console.error(err);
290
+ process.exit(1);
291
+ });
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@nozomiishii/pm",
3
+ "version": "0.1.0",
4
+ "description": "Project manager CLI — jump to projects via fzf",
5
+ "type": "module",
6
+ "homepage": "https://github.com/nozomiishii/pm#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/nozomiishii/pm/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/nozomiishii/pm.git"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Nozomi Ishii",
16
+ "bin": {
17
+ "pm": "./dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist/cli.js",
21
+ "src/pm.zsh"
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",
26
+ "typecheck": "tsc --noEmit",
27
+ "dev": "bun run src/cli.ts",
28
+ "demo": "docker run --rm -v \"$(pwd)\":/vhs pm-vhs",
29
+ "demo:all": "bash demo/create.sh",
30
+ "demo:logo": "docker run --rm -v \"$(pwd)\":/vhs pm-vhs demo/logo.tape",
31
+ "logo:colorize": "bun run src/logo/create-logo-color.ts",
32
+ "install:local": "bash install.sh",
33
+ "uninstall:local": "bash uninstall.sh",
34
+ "test": "vitest run"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "22.19.15",
38
+ "vitest": "4.1.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public",
42
+ "provenance": true
43
+ },
44
+ "packageManager": "bun@1.3.11",
45
+ "engines": {
46
+ "node": "24.14.0"
47
+ }
48
+ }
package/src/pm.zsh ADDED
@@ -0,0 +1,26 @@
1
+ # pm — project manager shell wrapper
2
+ # Source this file from .zshrc to enable `pm` and tab completion.
3
+ #
4
+ # Required:
5
+ # export PM_CONFIG="$HOME/Code/nozomiishii/workspaces/projects.json"
6
+
7
+ pm() {
8
+ local output
9
+ output="$(command pm "$@")" || return
10
+ case "${1:-}" in
11
+ ls|create-workspace|--help)
12
+ printf '%s\n' "$output"
13
+ ;;
14
+ *)
15
+ [[ -d "$output" ]] && cd "$output" || printf '%s\n' "$output"
16
+ ;;
17
+ esac
18
+ }
19
+
20
+ _pm() {
21
+ local -a names
22
+ names=("${(@f)$(command pm ls 2>/dev/null)}")
23
+ compadd -a names
24
+ }
25
+
26
+ compdef _pm pm