@solaqua/gji 0.1.0-beta.1
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.md +201 -0
- package/dist/clean.d.ts +12 -0
- package/dist/clean.js +80 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +294 -0
- package/dist/config-command.d.ts +8 -0
- package/dist/config-command.js +31 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +86 -0
- package/dist/git.d.ts +9 -0
- package/dist/git.js +51 -0
- package/dist/go.d.ts +13 -0
- package/dist/go.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/init.d.ts +11 -0
- package/dist/init.js +132 -0
- package/dist/ls.d.ts +8 -0
- package/dist/ls.js +26 -0
- package/dist/new.d.ts +15 -0
- package/dist/new.js +128 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +9 -0
- package/dist/pr.d.ts +6 -0
- package/dist/pr.js +17 -0
- package/dist/remove.d.ts +13 -0
- package/dist/remove.js +56 -0
- package/dist/repo.d.ts +14 -0
- package/dist/repo.js +55 -0
- package/dist/root.d.ts +5 -0
- package/dist/root.js +6 -0
- package/dist/status.d.ts +29 -0
- package/dist/status.js +84 -0
- package/dist/sync.d.ts +7 -0
- package/dist/sync.js +67 -0
- package/dist/worktree-management.d.ts +8 -0
- package/dist/worktree-management.js +18 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# gji
|
|
2
|
+
|
|
3
|
+
Context switching without the mess.
|
|
4
|
+
|
|
5
|
+
`gji` is a Git worktree CLI for people who jump between tasks all day. It gives each branch or PR its own directory, so you stop doing `stash`, `pop`, reinstall cycles, and fragile branch juggling.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Standard branch switching gets annoying when you are:
|
|
10
|
+
|
|
11
|
+
- fixing one bug while reviewing another branch
|
|
12
|
+
- hopping between feature work and PR checks
|
|
13
|
+
- using multiple terminals, editors, or AI agents at the same time
|
|
14
|
+
|
|
15
|
+
`gji` keeps those contexts isolated in separate worktrees with deterministic paths.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Install from npm:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install -g @solaqua/gji
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Confirm the CLI is available:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
gji --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The installed command is still:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
gji
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
Inside a Git repository:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
gji new feature/login-form
|
|
43
|
+
gji go feature/login-form
|
|
44
|
+
gji status
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That creates a linked worktree at a deterministic path:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
../worktrees/<repo>/<branch>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Shell setup
|
|
54
|
+
|
|
55
|
+
`gji go` can only change your current directory when shell integration is installed. Without shell integration, the raw CLI prints the target path so it stays script-friendly.
|
|
56
|
+
|
|
57
|
+
For zsh:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
echo 'eval "$(gji init zsh)"' >> ~/.zshrc
|
|
61
|
+
source ~/.zshrc
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
After that:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
gji go feature/login-form
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
changes your shell directory directly.
|
|
71
|
+
|
|
72
|
+
For scripts or explicit piping:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
gji go --print feature/login-form
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Daily workflow
|
|
79
|
+
|
|
80
|
+
Start a task:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
gji new feature/refactor-auth
|
|
84
|
+
gji go feature/refactor-auth
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Check what is active:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
gji status
|
|
91
|
+
gji ls
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Pull a PR into its own worktree:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
gji pr 123
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Sync the current worktree with the latest default branch:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
gji sync
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Sync every worktree in the repository:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
gji sync --all
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Clean up stale linked worktrees interactively:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
gji clean
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Finish a single worktree explicitly:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
gji remove feature/refactor-auth
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Commands
|
|
125
|
+
|
|
126
|
+
- `gji init [shell]` prints shell integration for `zsh`, `bash`, or `fish`
|
|
127
|
+
- `gji new [branch]` creates a branch and linked worktree; when omitted, it prompts with a placeholder branch name
|
|
128
|
+
- `gji pr <number>` fetches `origin/pull/<number>/head` and creates a linked `pr/<number>` worktree
|
|
129
|
+
- `gji go [branch]` jumps to an existing worktree when shell integration is installed, or prints the matching worktree path otherwise
|
|
130
|
+
- `gji root` prints the main repository root path from either the repo root or a linked worktree
|
|
131
|
+
- `gji status [--json]` prints repository metadata, worktree health, and upstream divergence
|
|
132
|
+
- `gji sync [--all]` fetches from the configured remote and rebases or fast-forwards worktrees onto the configured default branch
|
|
133
|
+
- `gji ls [--json]` lists active worktrees in a table or JSON
|
|
134
|
+
- `gji clean` interactively prunes one or more linked worktrees, including detached entries, while excluding the current worktree
|
|
135
|
+
- `gji remove [branch]` removes a linked worktree and deletes its branch when present
|
|
136
|
+
- `gji config` reads or updates global defaults
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
`gji` is usable without setup, but it supports defaults through:
|
|
141
|
+
|
|
142
|
+
- global config at `~/.config/gji/config.json`
|
|
143
|
+
- repo-local config at `.gji.json`
|
|
144
|
+
|
|
145
|
+
Repo-local values override global defaults.
|
|
146
|
+
|
|
147
|
+
Supported keys:
|
|
148
|
+
|
|
149
|
+
- `branchPrefix`
|
|
150
|
+
- `syncRemote`
|
|
151
|
+
- `syncDefaultBranch`
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"branchPrefix": "feature/",
|
|
158
|
+
"syncRemote": "upstream",
|
|
159
|
+
"syncDefaultBranch": "main"
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Behavior:
|
|
164
|
+
|
|
165
|
+
- if `syncRemote` is unset, `gji sync` defaults to `origin`
|
|
166
|
+
- if `syncDefaultBranch` is unset, `gji sync` resolves the remote default branch from `HEAD`
|
|
167
|
+
|
|
168
|
+
## JSON output
|
|
169
|
+
|
|
170
|
+
`gji ls --json` returns branch/path entries:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
gji ls --json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`gji status --json` returns a top-level object with:
|
|
177
|
+
|
|
178
|
+
- `repoRoot`
|
|
179
|
+
- `currentRoot`
|
|
180
|
+
- `worktrees`
|
|
181
|
+
|
|
182
|
+
Each worktree entry contains:
|
|
183
|
+
|
|
184
|
+
- `branch`: branch name or `null` for detached worktrees
|
|
185
|
+
- `current`
|
|
186
|
+
- `path`
|
|
187
|
+
- `status`: `clean` or `dirty`
|
|
188
|
+
- `upstream`: one of
|
|
189
|
+
- `{ "kind": "detached" }`
|
|
190
|
+
- `{ "kind": "no-upstream" }`
|
|
191
|
+
- `{ "kind": "tracked", "ahead": number, "behind": number }`
|
|
192
|
+
|
|
193
|
+
## Notes
|
|
194
|
+
|
|
195
|
+
- `gji` works from either the main repository root or any linked worktree
|
|
196
|
+
- the current worktree is never offered as a `gji clean` removal candidate
|
|
197
|
+
- `gji` currently uses GitHub-style PR refs for `gji pr`
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT
|
package/dist/clean.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorktreeEntry } from './repo.js';
|
|
2
|
+
export interface CleanCommandOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
stderr: (chunk: string) => void;
|
|
5
|
+
stdout: (chunk: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface CleanCommandDependencies {
|
|
8
|
+
confirmRemoval: (worktrees: WorktreeEntry[]) => Promise<boolean>;
|
|
9
|
+
promptForWorktrees: (worktrees: WorktreeEntry[]) => Promise<string[] | null>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createCleanCommand(dependencies?: Partial<CleanCommandDependencies>): (options: CleanCommandOptions) => Promise<number>;
|
|
12
|
+
export declare const runCleanCommand: (options: CleanCommandOptions) => Promise<number>;
|
package/dist/clean.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { confirm, isCancel, multiselect } from '@clack/prompts';
|
|
2
|
+
import { deleteBranch, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
|
|
3
|
+
export function createCleanCommand(dependencies = {}) {
|
|
4
|
+
const promptForWorktrees = dependencies.promptForWorktrees ?? defaultPromptForWorktrees;
|
|
5
|
+
const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval;
|
|
6
|
+
return async function runCleanCommand(options) {
|
|
7
|
+
const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
|
|
8
|
+
const cleanupCandidates = linkedWorktrees.filter((worktree) => worktree.path !== repository.currentRoot);
|
|
9
|
+
if (cleanupCandidates.length === 0) {
|
|
10
|
+
options.stderr('No linked worktrees to clean\n');
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const selections = await promptForWorktrees(cleanupCandidates);
|
|
14
|
+
if (!selections || selections.length === 0) {
|
|
15
|
+
options.stderr('Aborted\n');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
const selectedWorktrees = resolveSelectedWorktrees(cleanupCandidates, selections);
|
|
19
|
+
if (selectedWorktrees.length !== selections.length) {
|
|
20
|
+
options.stderr('Selected worktree no longer exists\n');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
if (!(await confirmRemoval(selectedWorktrees))) {
|
|
24
|
+
options.stderr('Aborted\n');
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
for (const worktree of selectedWorktrees) {
|
|
28
|
+
await removeWorktree(repository.repoRoot, worktree.path);
|
|
29
|
+
if (worktree.branch) {
|
|
30
|
+
await deleteBranch(repository.repoRoot, worktree.branch);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
options.stdout(`${repository.repoRoot}\n`);
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export const runCleanCommand = createCleanCommand();
|
|
38
|
+
function resolveSelectedWorktrees(worktrees, selections) {
|
|
39
|
+
const selectedWorktrees = [];
|
|
40
|
+
const seenPaths = new Set();
|
|
41
|
+
for (const selection of selections) {
|
|
42
|
+
const worktree = worktrees.find((entry) => entry.path === selection || entry.branch === selection);
|
|
43
|
+
if (!worktree || seenPaths.has(worktree.path)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
selectedWorktrees.push(worktree);
|
|
47
|
+
seenPaths.add(worktree.path);
|
|
48
|
+
}
|
|
49
|
+
return selectedWorktrees;
|
|
50
|
+
}
|
|
51
|
+
async function defaultPromptForWorktrees(worktrees) {
|
|
52
|
+
const choice = await multiselect({
|
|
53
|
+
message: 'Choose worktrees to clean',
|
|
54
|
+
options: worktrees.map((worktree) => ({
|
|
55
|
+
hint: worktree.path,
|
|
56
|
+
label: worktree.branch ?? '(detached)',
|
|
57
|
+
value: worktree.path,
|
|
58
|
+
})),
|
|
59
|
+
required: true,
|
|
60
|
+
});
|
|
61
|
+
return isCancel(choice) ? null : choice;
|
|
62
|
+
}
|
|
63
|
+
async function defaultConfirmRemoval(worktrees) {
|
|
64
|
+
const branchCount = worktrees.filter((worktree) => worktree.branch !== null).length;
|
|
65
|
+
const detachedCount = worktrees.length - branchCount;
|
|
66
|
+
const messageParts = [`Remove ${worktrees.length} linked worktree${worktrees.length === 1 ? '' : 's'}`];
|
|
67
|
+
if (branchCount > 0) {
|
|
68
|
+
messageParts.push(`delete ${branchCount} branch${branchCount === 1 ? '' : 'es'}`);
|
|
69
|
+
}
|
|
70
|
+
if (detachedCount > 0) {
|
|
71
|
+
messageParts.push(`remove ${detachedCount} detached worktree${detachedCount === 1 ? '' : 's'}`);
|
|
72
|
+
}
|
|
73
|
+
const choice = await confirm({
|
|
74
|
+
active: 'Yes',
|
|
75
|
+
inactive: 'No',
|
|
76
|
+
initialValue: true,
|
|
77
|
+
message: `${messageParts.join(', ')}?`,
|
|
78
|
+
});
|
|
79
|
+
return !isCancel(choice) && choice;
|
|
80
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export interface RunCliOptions {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
stderr?: (chunk: string) => void;
|
|
5
|
+
stdout?: (chunk: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface RunCliResult {
|
|
8
|
+
exitCode: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function createProgram(): Command;
|
|
11
|
+
export declare function runCli(argv: string[], options?: RunCliOptions): Promise<RunCliResult>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { runCleanCommand } from './clean.js';
|
|
3
|
+
import { runConfigCommand } from './config-command.js';
|
|
4
|
+
import { runGoCommand } from './go.js';
|
|
5
|
+
import { runInitCommand } from './init.js';
|
|
6
|
+
import { runLsCommand } from './ls.js';
|
|
7
|
+
import { runNewCommand } from './new.js';
|
|
8
|
+
import { runPrCommand } from './pr.js';
|
|
9
|
+
import { runRemoveCommand } from './remove.js';
|
|
10
|
+
import { runRootCommand } from './root.js';
|
|
11
|
+
import { runStatusCommand } from './status.js';
|
|
12
|
+
import { runSyncCommand } from './sync.js';
|
|
13
|
+
export function createProgram() {
|
|
14
|
+
const program = new Command();
|
|
15
|
+
program
|
|
16
|
+
.name('gji')
|
|
17
|
+
.description('Context switching without the mess.')
|
|
18
|
+
.showHelpAfterError()
|
|
19
|
+
.showSuggestionAfterError();
|
|
20
|
+
registerCommands(program);
|
|
21
|
+
return program;
|
|
22
|
+
}
|
|
23
|
+
export async function runCli(argv, options = {}) {
|
|
24
|
+
const program = createProgram();
|
|
25
|
+
const cwd = options.cwd ?? process.cwd();
|
|
26
|
+
const stdout = options.stdout ?? (() => undefined);
|
|
27
|
+
const stderr = options.stderr ?? (() => undefined);
|
|
28
|
+
program.configureOutput({
|
|
29
|
+
writeErr: stderr,
|
|
30
|
+
writeOut: stdout,
|
|
31
|
+
});
|
|
32
|
+
program.exitOverride();
|
|
33
|
+
if (argv.length === 0) {
|
|
34
|
+
program.outputHelp();
|
|
35
|
+
return { exitCode: 0 };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
attachCommandActions(program, { cwd, stderr, stdout });
|
|
39
|
+
await program.parseAsync(['node', 'gji', ...argv], { from: 'node' });
|
|
40
|
+
return { exitCode: 0 };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isCommanderExit(error)) {
|
|
44
|
+
return { exitCode: error.exitCode };
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function registerCommands(program) {
|
|
50
|
+
program
|
|
51
|
+
.command('new [branch]')
|
|
52
|
+
.description('create a new branch and linked worktree')
|
|
53
|
+
.action(notImplemented('new'));
|
|
54
|
+
program
|
|
55
|
+
.command('init [shell]')
|
|
56
|
+
.description('print or install shell integration')
|
|
57
|
+
.option('--write', 'write the integration to the shell config file')
|
|
58
|
+
.action(notImplemented('init'));
|
|
59
|
+
program
|
|
60
|
+
.command('pr <number>')
|
|
61
|
+
.description('fetch a pull request ref and create a linked worktree')
|
|
62
|
+
.action(notImplemented('pr'));
|
|
63
|
+
program
|
|
64
|
+
.command('go [branch]')
|
|
65
|
+
.description('print or select a worktree path')
|
|
66
|
+
.option('--print', 'print the resolved worktree path explicitly')
|
|
67
|
+
.action(notImplemented('go'));
|
|
68
|
+
program
|
|
69
|
+
.command('root')
|
|
70
|
+
.description('print the main repository root path')
|
|
71
|
+
.action(notImplemented('root'));
|
|
72
|
+
program
|
|
73
|
+
.command('status')
|
|
74
|
+
.description('summarize repository and worktree health')
|
|
75
|
+
.option('--json', 'print repository and worktree health as JSON')
|
|
76
|
+
.action(notImplemented('status'));
|
|
77
|
+
program
|
|
78
|
+
.command('sync')
|
|
79
|
+
.description('fetch and update one or all worktrees')
|
|
80
|
+
.option('--all', 'sync every worktree in the repository')
|
|
81
|
+
.action(notImplemented('sync'));
|
|
82
|
+
program
|
|
83
|
+
.command('ls')
|
|
84
|
+
.description('list active worktrees')
|
|
85
|
+
.option('--json', 'print active worktrees as JSON')
|
|
86
|
+
.action(notImplemented('ls'));
|
|
87
|
+
program
|
|
88
|
+
.command('clean')
|
|
89
|
+
.description('interactively prune linked worktrees')
|
|
90
|
+
.action(notImplemented('clean'));
|
|
91
|
+
program
|
|
92
|
+
.command('remove [branch]')
|
|
93
|
+
.description('remove a linked worktree and delete its branch when present')
|
|
94
|
+
.action(notImplemented('remove'));
|
|
95
|
+
const configCommand = program
|
|
96
|
+
.command('config')
|
|
97
|
+
.description('manage global config defaults')
|
|
98
|
+
.action(notImplemented('config'));
|
|
99
|
+
configCommand
|
|
100
|
+
.command('get [key]')
|
|
101
|
+
.description('print the global config or a single key')
|
|
102
|
+
.action(notImplemented('config get'));
|
|
103
|
+
configCommand
|
|
104
|
+
.command('set <key> <value>')
|
|
105
|
+
.description('set a global config value')
|
|
106
|
+
.action(notImplemented('config set'));
|
|
107
|
+
configCommand
|
|
108
|
+
.command('unset <key>')
|
|
109
|
+
.description('remove a global config value')
|
|
110
|
+
.action(notImplemented('config unset'));
|
|
111
|
+
}
|
|
112
|
+
function attachCommandActions(program, options) {
|
|
113
|
+
program.commands
|
|
114
|
+
.find((command) => command.name() === 'new')
|
|
115
|
+
?.action(async (branch) => {
|
|
116
|
+
const exitCode = await runNewCommand({ ...options, branch });
|
|
117
|
+
if (exitCode !== 0) {
|
|
118
|
+
throw commanderExit(exitCode);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
program.commands
|
|
122
|
+
.find((command) => command.name() === 'init')
|
|
123
|
+
?.action(async (shell, commandOptions) => {
|
|
124
|
+
const exitCode = await runInitCommand({
|
|
125
|
+
cwd: options.cwd,
|
|
126
|
+
shell,
|
|
127
|
+
stdout: options.stdout,
|
|
128
|
+
write: commandOptions.write,
|
|
129
|
+
});
|
|
130
|
+
if (exitCode !== 0) {
|
|
131
|
+
throw commanderExit(exitCode);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
program.commands
|
|
135
|
+
.find((command) => command.name() === 'pr')
|
|
136
|
+
?.action(async (number) => {
|
|
137
|
+
const exitCode = await runPrCommand({ cwd: options.cwd, number, stdout: options.stdout });
|
|
138
|
+
if (exitCode !== 0) {
|
|
139
|
+
throw commanderExit(exitCode);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
program.commands
|
|
143
|
+
.find((command) => command.name() === 'go')
|
|
144
|
+
?.action(async (branch, commandOptions) => {
|
|
145
|
+
const exitCode = await runGoCommand({
|
|
146
|
+
branch,
|
|
147
|
+
cwd: options.cwd,
|
|
148
|
+
print: commandOptions.print,
|
|
149
|
+
stderr: options.stderr,
|
|
150
|
+
stdout: options.stdout,
|
|
151
|
+
});
|
|
152
|
+
if (exitCode !== 0) {
|
|
153
|
+
throw commanderExit(exitCode);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
program.commands
|
|
157
|
+
.find((command) => command.name() === 'root')
|
|
158
|
+
?.action(async () => {
|
|
159
|
+
const exitCode = await runRootCommand({
|
|
160
|
+
cwd: options.cwd,
|
|
161
|
+
stdout: options.stdout,
|
|
162
|
+
});
|
|
163
|
+
if (exitCode !== 0) {
|
|
164
|
+
throw commanderExit(exitCode);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
program.commands
|
|
168
|
+
.find((command) => command.name() === 'status')
|
|
169
|
+
?.action(async (commandOptions) => {
|
|
170
|
+
const exitCode = await runStatusCommand({
|
|
171
|
+
cwd: options.cwd,
|
|
172
|
+
json: commandOptions.json,
|
|
173
|
+
stdout: options.stdout,
|
|
174
|
+
});
|
|
175
|
+
if (exitCode !== 0) {
|
|
176
|
+
throw commanderExit(exitCode);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
program.commands
|
|
180
|
+
.find((command) => command.name() === 'sync')
|
|
181
|
+
?.action(async (commandOptions) => {
|
|
182
|
+
const exitCode = await runSyncCommand({
|
|
183
|
+
all: commandOptions.all,
|
|
184
|
+
cwd: options.cwd,
|
|
185
|
+
stderr: options.stderr,
|
|
186
|
+
stdout: options.stdout,
|
|
187
|
+
});
|
|
188
|
+
if (exitCode !== 0) {
|
|
189
|
+
throw commanderExit(exitCode);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
program.commands
|
|
193
|
+
.find((command) => command.name() === 'ls')
|
|
194
|
+
?.action(async (commandOptions) => {
|
|
195
|
+
const exitCode = await runLsCommand({
|
|
196
|
+
cwd: options.cwd,
|
|
197
|
+
json: commandOptions.json,
|
|
198
|
+
stdout: options.stdout,
|
|
199
|
+
});
|
|
200
|
+
if (exitCode !== 0) {
|
|
201
|
+
throw commanderExit(exitCode);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
program.commands
|
|
205
|
+
.find((command) => command.name() === 'clean')
|
|
206
|
+
?.action(async () => {
|
|
207
|
+
const exitCode = await runCleanCommand({
|
|
208
|
+
cwd: options.cwd,
|
|
209
|
+
stderr: options.stderr,
|
|
210
|
+
stdout: options.stdout,
|
|
211
|
+
});
|
|
212
|
+
if (exitCode !== 0) {
|
|
213
|
+
throw commanderExit(exitCode);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const runRemovalCommand = async (branch) => {
|
|
217
|
+
const exitCode = await runRemoveCommand({
|
|
218
|
+
branch,
|
|
219
|
+
cwd: options.cwd,
|
|
220
|
+
stderr: options.stderr,
|
|
221
|
+
stdout: options.stdout,
|
|
222
|
+
});
|
|
223
|
+
if (exitCode !== 0) {
|
|
224
|
+
throw commanderExit(exitCode);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
program.commands
|
|
228
|
+
.find((command) => command.name() === 'remove')
|
|
229
|
+
?.action(runRemovalCommand);
|
|
230
|
+
const configCommand = program.commands.find((command) => command.name() === 'config');
|
|
231
|
+
configCommand?.action(async () => {
|
|
232
|
+
const exitCode = await runConfigCommand({
|
|
233
|
+
cwd: options.cwd,
|
|
234
|
+
stdout: options.stdout,
|
|
235
|
+
});
|
|
236
|
+
if (exitCode !== 0) {
|
|
237
|
+
throw commanderExit(exitCode);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
configCommand?.commands.find((command) => command.name() === 'get')?.action(async (key) => {
|
|
241
|
+
const exitCode = await runConfigCommand({
|
|
242
|
+
action: 'get',
|
|
243
|
+
cwd: options.cwd,
|
|
244
|
+
key,
|
|
245
|
+
stdout: options.stdout,
|
|
246
|
+
});
|
|
247
|
+
if (exitCode !== 0) {
|
|
248
|
+
throw commanderExit(exitCode);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
configCommand?.commands
|
|
252
|
+
.find((command) => command.name() === 'set')
|
|
253
|
+
?.action(async (key, value) => {
|
|
254
|
+
const exitCode = await runConfigCommand({
|
|
255
|
+
action: 'set',
|
|
256
|
+
cwd: options.cwd,
|
|
257
|
+
key,
|
|
258
|
+
stdout: options.stdout,
|
|
259
|
+
value,
|
|
260
|
+
});
|
|
261
|
+
if (exitCode !== 0) {
|
|
262
|
+
throw commanderExit(exitCode);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
configCommand?.commands.find((command) => command.name() === 'unset')?.action(async (key) => {
|
|
266
|
+
const exitCode = await runConfigCommand({
|
|
267
|
+
action: 'unset',
|
|
268
|
+
cwd: options.cwd,
|
|
269
|
+
key,
|
|
270
|
+
stdout: options.stdout,
|
|
271
|
+
});
|
|
272
|
+
if (exitCode !== 0) {
|
|
273
|
+
throw commanderExit(exitCode);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function notImplemented(commandName) {
|
|
278
|
+
return () => {
|
|
279
|
+
throw new Error(`'${commandName}' is not implemented yet.`);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function commanderExit(exitCode) {
|
|
283
|
+
const error = new Error(`Command exited with code ${exitCode}.`);
|
|
284
|
+
error.code = 'commander.executeSubCommandAsync';
|
|
285
|
+
error.exitCode = exitCode;
|
|
286
|
+
return error;
|
|
287
|
+
}
|
|
288
|
+
function isCommanderExit(error) {
|
|
289
|
+
return (error instanceof Error &&
|
|
290
|
+
'code' in error &&
|
|
291
|
+
'exitCode' in error &&
|
|
292
|
+
typeof error.code === 'string' &&
|
|
293
|
+
typeof error.exitCode === 'number');
|
|
294
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadGlobalConfig, parseConfigValue, unsetGlobalConfigKey, updateGlobalConfigKey, } from './config.js';
|
|
2
|
+
export async function runConfigCommand(options) {
|
|
3
|
+
switch (options.action) {
|
|
4
|
+
case undefined: {
|
|
5
|
+
const loaded = await loadGlobalConfig();
|
|
6
|
+
writeJson(options.stdout, loaded.config);
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
case 'get': {
|
|
10
|
+
const loaded = await loadGlobalConfig();
|
|
11
|
+
writeJson(options.stdout, options.key ? loaded.config[options.key] : loaded.config);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
case 'set':
|
|
15
|
+
if (options.key && options.value !== undefined) {
|
|
16
|
+
await updateGlobalConfigKey(options.key, parseConfigValue(options.value));
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
case 'unset':
|
|
21
|
+
if (options.key) {
|
|
22
|
+
await unsetGlobalConfigKey(options.key);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Invalid config arguments: ${[options.action, options.key, options.value].filter(Boolean).join(' ')}`);
|
|
28
|
+
}
|
|
29
|
+
function writeJson(stdout, value) {
|
|
30
|
+
stdout(`${JSON.stringify(value, null, 2)}\n`);
|
|
31
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const CONFIG_FILE_NAME = ".gji.json";
|
|
2
|
+
export declare const GLOBAL_CONFIG_DIRECTORY = ".config/gji";
|
|
3
|
+
export declare const GLOBAL_CONFIG_NAME = "config.json";
|
|
4
|
+
export type GjiConfig = Record<string, unknown>;
|
|
5
|
+
export interface LoadedConfig {
|
|
6
|
+
config: GjiConfig;
|
|
7
|
+
exists: boolean;
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const DEFAULT_CONFIG: GjiConfig;
|
|
11
|
+
export declare function loadConfig(root: string): Promise<LoadedConfig>;
|
|
12
|
+
export declare function loadEffectiveConfig(root: string, home?: string): Promise<GjiConfig>;
|
|
13
|
+
export declare function loadGlobalConfig(home?: string): Promise<LoadedConfig>;
|
|
14
|
+
export declare function saveGlobalConfig(config: GjiConfig, home?: string): Promise<string>;
|
|
15
|
+
export declare function unsetGlobalConfigKey(key: string, home?: string): Promise<GjiConfig>;
|
|
16
|
+
export declare function updateGlobalConfigKey(key: string, value: unknown, home?: string): Promise<GjiConfig>;
|
|
17
|
+
export declare function GLOBAL_CONFIG_FILE_PATH(home?: string): string;
|
|
18
|
+
export declare function parseConfigValue(value: string): unknown;
|