@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 +21 -0
- package/README.ja.md +208 -0
- package/README.md +208 -0
- package/dist/cli.js +291 -0
- package/package.json +48 -0
- package/src/pm.zsh +26 -0
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
|
+

|
|
87
|
+
|
|
88
|
+
### pm
|
|
89
|
+
|
|
90
|
+
fzfピッカーを開いて、選択したプロジェクトへ移動します。
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pm
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+

|
|
97
|
+
|
|
98
|
+
### pm cd
|
|
99
|
+
|
|
100
|
+
プロジェクト名を指定して移動します。名前を省略するとfzfが開きます。
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
pm cd <name>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
### pm ls
|
|
109
|
+
|
|
110
|
+
プロジェクト名を一覧表示します。
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
pm ls
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+

|
|
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
|
+

|
|
87
|
+
|
|
88
|
+
### pm
|
|
89
|
+
|
|
90
|
+
Opens fzf picker and jumps to the selected project.
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
pm
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+

|
|
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
|
+

|
|
107
|
+
|
|
108
|
+
### pm ls
|
|
109
|
+
|
|
110
|
+
Lists all project names.
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
pm ls
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+

|
|
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
|