From 699220aca8e5f3a86c9fbc58d59f257c0d76aa57 Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 20:38:34 +1100 Subject: [PATCH 1/8] fix: use default-days cooldown for github-actions ecosystem The semver-specific cooldown properties are only supported for semver ecosystems like npm, not github-actions. Co-Authored-By: Claude Opus 4.8 --- .github/dependabot.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6bf1a48..07e254f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,10 +22,9 @@ updates: directory: "/" schedule: interval: "weekly" + # github-actions does not support semver-specific cooldowns. cooldown: - semver-minor-days: 3 - semver-patch-days: 3 - semver-major-days: 30 + default-days: 3 open-pull-requests-limit: 10 groups: minor-and-patch: From e40bb3850a60a3d5c6f2d7971e06ae9133a9b737 Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 22:06:46 +1100 Subject: [PATCH 2/8] refactor: organise src/lib into item, config, and branches themes Group the lib files by responsibility, each with its own barrel: - item/ repository domain model (types, parsing, location, filter, status) - config/ CLI plumbing (configuration, commander program) - branches/ git branch operations (list, select, exclude) Update bin imports to use the new subdir barrels instead of deep paths. Co-Authored-By: Claude Opus 4.8 --- src/bin/git-list-remote.ts | 2 +- src/bin/git-merged.ts | 6 ++++-- src/bin/git-recent.ts | 3 +-- src/bin/git-squashed.ts | 6 ++++-- src/bin/git-stale.ts | 3 +-- src/bin/git-tidy.ts | 8 ++++--- src/bin/git-workspace.ts | 2 +- .../exclude-operational-branches.ts | 0 .../{ => branches}/git-raw-output-handler.ts | 0 src/lib/branches/index.ts | 20 ++++++++++++++++++ .../{ => branches}/list-recent-branches.ts | 0 .../select-and-checkout-branch.test.ts | 0 .../select-and-checkout-branch.ts | 0 .../select-and-delete-branches.test.ts | 0 .../select-and-delete-branches.ts | 0 src/lib/{ => config}/get-configuration.ts | 2 +- src/lib/config/index.ts | 17 +++++++++++++++ src/lib/{ => config}/with-program.ts | 2 +- src/lib/index.ts | 12 +++-------- src/lib/{ => item}/from-input.test.ts | 0 src/lib/{ => item}/from-input.ts | 0 src/lib/item/index.ts | 21 +++++++++++++++++++ src/lib/{ => item}/item-filter.test.ts | 0 src/lib/{ => item}/item-filter.ts | 0 src/lib/{ => item}/item-location.ts | 0 src/lib/{ => item}/item-status.test.ts | 0 src/lib/{ => item}/item-status.ts | 0 src/lib/{ => item}/link-parser.test.ts | 0 src/lib/{ => item}/link-parser.ts | 0 src/lib/{ => item}/types.ts | 0 30 files changed, 80 insertions(+), 24 deletions(-) rename src/lib/{ => branches}/exclude-operational-branches.ts (100%) rename src/lib/{ => branches}/git-raw-output-handler.ts (100%) create mode 100644 src/lib/branches/index.ts rename src/lib/{ => branches}/list-recent-branches.ts (100%) rename src/lib/{ => branches}/select-and-checkout-branch.test.ts (100%) rename src/lib/{ => branches}/select-and-checkout-branch.ts (100%) rename src/lib/{ => branches}/select-and-delete-branches.test.ts (100%) rename src/lib/{ => branches}/select-and-delete-branches.ts (100%) rename src/lib/{ => config}/get-configuration.ts (98%) create mode 100644 src/lib/config/index.ts rename src/lib/{ => config}/with-program.ts (95%) rename src/lib/{ => item}/from-input.test.ts (100%) rename src/lib/{ => item}/from-input.ts (100%) create mode 100644 src/lib/item/index.ts rename src/lib/{ => item}/item-filter.test.ts (100%) rename src/lib/{ => item}/item-filter.ts (100%) rename src/lib/{ => item}/item-location.ts (100%) rename src/lib/{ => item}/item-status.test.ts (100%) rename src/lib/{ => item}/item-status.ts (100%) rename src/lib/{ => item}/link-parser.test.ts (100%) rename src/lib/{ => item}/link-parser.ts (100%) rename src/lib/{ => item}/types.ts (100%) diff --git a/src/bin/git-list-remote.ts b/src/bin/git-list-remote.ts index b573727..da9c842 100644 --- a/src/bin/git-list-remote.ts +++ b/src/bin/git-list-remote.ts @@ -17,7 +17,7 @@ */ import { simpleGit } from 'simple-git'; -import { selectAndCheckoutBranch } from '../lib/select-and-checkout-branch'; +import { selectAndCheckoutBranch } from '../lib/branches'; const listRemoteBranches = async ({ pattern }: { pattern?: string }) => { const params = ['--remote']; diff --git a/src/bin/git-merged.ts b/src/bin/git-merged.ts index 85cb96a..584f1af 100644 --- a/src/bin/git-merged.ts +++ b/src/bin/git-merged.ts @@ -17,8 +17,10 @@ */ import { simpleGit } from 'simple-git'; -import { excludeOperationalBranches } from '../lib/exclude-operational-branches'; -import { selectAndDeleteBranches } from '../lib/select-and-delete-branches'; +import { + excludeOperationalBranches, + selectAndDeleteBranches +} from '../lib/branches'; const listMergedBranches = async ({ target }: { target?: string }) => { const params = ['--merged']; diff --git a/src/bin/git-recent.ts b/src/bin/git-recent.ts index 74eabe2..50a9144 100644 --- a/src/bin/git-recent.ts +++ b/src/bin/git-recent.ts @@ -15,8 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { listRecentBranches } from '../lib/list-recent-branches'; -import { selectAndCheckoutBranch } from '../lib/select-and-checkout-branch'; +import { listRecentBranches, selectAndCheckoutBranch } from '../lib/branches'; listRecentBranches() .then(selectAndCheckoutBranch(/\d{4}-\d{2}-\d{2} (?.*)/)) diff --git a/src/bin/git-squashed.ts b/src/bin/git-squashed.ts index dcad516..56fb983 100644 --- a/src/bin/git-squashed.ts +++ b/src/bin/git-squashed.ts @@ -17,8 +17,10 @@ */ import { simpleGit } from 'simple-git'; -import { excludeOperationalBranches } from '../lib/exclude-operational-branches'; -import { selectAndDeleteBranches } from '../lib/select-and-delete-branches'; +import { + excludeOperationalBranches, + selectAndDeleteBranches +} from '../lib/branches'; const listSquashedBranches = async ({ target }: { target?: string }) => { const params = ['--no-contains']; diff --git a/src/bin/git-stale.ts b/src/bin/git-stale.ts index 8cea6f3..e319786 100644 --- a/src/bin/git-stale.ts +++ b/src/bin/git-stale.ts @@ -15,8 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { listRecentBranches } from '../lib/list-recent-branches'; -import { selectAndDeleteBranches } from '../lib/select-and-delete-branches'; +import { listRecentBranches, selectAndDeleteBranches } from '../lib/branches'; const mainBranches = /^\d{4}-\d{2}-\d{2} (main|master|production|demo|website|development)$/; diff --git a/src/bin/git-tidy.ts b/src/bin/git-tidy.ts index f07a4f2..d46f65e 100644 --- a/src/bin/git-tidy.ts +++ b/src/bin/git-tidy.ts @@ -16,9 +16,11 @@ * limitations under the License. */ -import { excludeOperationalBranches } from '../lib/exclude-operational-branches'; -import { listRecentBranches } from '../lib/list-recent-branches'; -import { selectAndDeleteBranches } from '../lib/select-and-delete-branches'; +import { + excludeOperationalBranches, + listRecentBranches, + selectAndDeleteBranches +} from '../lib/branches'; listRecentBranches() .then(excludeOperationalBranches) diff --git a/src/bin/git-workspace.ts b/src/bin/git-workspace.ts index a6eeb6e..845974d 100644 --- a/src/bin/git-workspace.ts +++ b/src/bin/git-workspace.ts @@ -22,7 +22,7 @@ import { withProgram, WithProgram } from '../lib'; -import { itemStatus } from '../lib/item-status'; +import { itemStatus } from '../lib/item'; const listRepos: WithProgram = ({ workspacesRoot, registry }, program) => { program diff --git a/src/lib/exclude-operational-branches.ts b/src/lib/branches/exclude-operational-branches.ts similarity index 100% rename from src/lib/exclude-operational-branches.ts rename to src/lib/branches/exclude-operational-branches.ts diff --git a/src/lib/git-raw-output-handler.ts b/src/lib/branches/git-raw-output-handler.ts similarity index 100% rename from src/lib/git-raw-output-handler.ts rename to src/lib/branches/git-raw-output-handler.ts diff --git a/src/lib/branches/index.ts b/src/lib/branches/index.ts new file mode 100644 index 0000000..5cce37b --- /dev/null +++ b/src/lib/branches/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './git-raw-output-handler'; +export * from './list-recent-branches'; +export * from './exclude-operational-branches'; +export * from './select-and-checkout-branch'; +export * from './select-and-delete-branches'; diff --git a/src/lib/list-recent-branches.ts b/src/lib/branches/list-recent-branches.ts similarity index 100% rename from src/lib/list-recent-branches.ts rename to src/lib/branches/list-recent-branches.ts diff --git a/src/lib/select-and-checkout-branch.test.ts b/src/lib/branches/select-and-checkout-branch.test.ts similarity index 100% rename from src/lib/select-and-checkout-branch.test.ts rename to src/lib/branches/select-and-checkout-branch.test.ts diff --git a/src/lib/select-and-checkout-branch.ts b/src/lib/branches/select-and-checkout-branch.ts similarity index 100% rename from src/lib/select-and-checkout-branch.ts rename to src/lib/branches/select-and-checkout-branch.ts diff --git a/src/lib/select-and-delete-branches.test.ts b/src/lib/branches/select-and-delete-branches.test.ts similarity index 100% rename from src/lib/select-and-delete-branches.test.ts rename to src/lib/branches/select-and-delete-branches.test.ts diff --git a/src/lib/select-and-delete-branches.ts b/src/lib/branches/select-and-delete-branches.ts similarity index 100% rename from src/lib/select-and-delete-branches.ts rename to src/lib/branches/select-and-delete-branches.ts diff --git a/src/lib/get-configuration.ts b/src/lib/config/get-configuration.ts similarity index 98% rename from src/lib/get-configuration.ts rename to src/lib/config/get-configuration.ts index d09a80c..3e54eae 100644 --- a/src/lib/get-configuration.ts +++ b/src/lib/config/get-configuration.ts @@ -20,7 +20,7 @@ import { leveldbRegistry, Registry } from '@labset/leveldb-registry'; import camelCase from 'lodash/camelCase'; import { simpleGit, GitConfigScope } from 'simple-git'; -import { Item } from './types'; +import { Item } from '../item'; export type Configuration = { workspacesRoot: string; diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts new file mode 100644 index 0000000..5c16373 --- /dev/null +++ b/src/lib/config/index.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './get-configuration'; +export * from './with-program'; diff --git a/src/lib/with-program.ts b/src/lib/config/with-program.ts similarity index 95% rename from src/lib/with-program.ts rename to src/lib/config/with-program.ts index c3345c6..7d97b5e 100644 --- a/src/lib/with-program.ts +++ b/src/lib/config/with-program.ts @@ -35,7 +35,7 @@ export const withProgram = (fn: WithProgram) => { program.option('-s, --slug ', 'filter by name or slug'); program.option('-k, --keyword ', 'filter by keyword'); fn(configuration, program); - const packageJson = await import('../../package.json'); + const packageJson = await import('../../../package.json'); program.version(packageJson.version); program.parse(process.argv); }; diff --git a/src/lib/index.ts b/src/lib/index.ts index 1252e11..a5f7f4d 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -13,12 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './from-input'; -export * from './get-configuration'; -export * from './git-raw-output-handler'; -export * from './item-filter'; -export * from './item-location'; -export * from './link-parser'; -export * from './with-program'; - -export * from './types'; +export * from './item'; +export * from './config'; +export * from './branches'; diff --git a/src/lib/from-input.test.ts b/src/lib/item/from-input.test.ts similarity index 100% rename from src/lib/from-input.test.ts rename to src/lib/item/from-input.test.ts diff --git a/src/lib/from-input.ts b/src/lib/item/from-input.ts similarity index 100% rename from src/lib/from-input.ts rename to src/lib/item/from-input.ts diff --git a/src/lib/item/index.ts b/src/lib/item/index.ts new file mode 100644 index 0000000..69255bb --- /dev/null +++ b/src/lib/item/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './types'; +export * from './link-parser'; +export * from './from-input'; +export * from './item-location'; +export * from './item-filter'; +export * from './item-status'; diff --git a/src/lib/item-filter.test.ts b/src/lib/item/item-filter.test.ts similarity index 100% rename from src/lib/item-filter.test.ts rename to src/lib/item/item-filter.test.ts diff --git a/src/lib/item-filter.ts b/src/lib/item/item-filter.ts similarity index 100% rename from src/lib/item-filter.ts rename to src/lib/item/item-filter.ts diff --git a/src/lib/item-location.ts b/src/lib/item/item-location.ts similarity index 100% rename from src/lib/item-location.ts rename to src/lib/item/item-location.ts diff --git a/src/lib/item-status.test.ts b/src/lib/item/item-status.test.ts similarity index 100% rename from src/lib/item-status.test.ts rename to src/lib/item/item-status.test.ts diff --git a/src/lib/item-status.ts b/src/lib/item/item-status.ts similarity index 100% rename from src/lib/item-status.ts rename to src/lib/item/item-status.ts diff --git a/src/lib/link-parser.test.ts b/src/lib/item/link-parser.test.ts similarity index 100% rename from src/lib/link-parser.test.ts rename to src/lib/item/link-parser.test.ts diff --git a/src/lib/link-parser.ts b/src/lib/item/link-parser.ts similarity index 100% rename from src/lib/link-parser.ts rename to src/lib/item/link-parser.ts diff --git a/src/lib/types.ts b/src/lib/item/types.ts similarity index 100% rename from src/lib/types.ts rename to src/lib/item/types.ts From 909a507ccc93ec09883b518fcd89a4a07db9d407 Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 22:46:04 +1100 Subject: [PATCH 3/8] feat: add git-work command to manage worktrees git-work has two subcommands: - add : create a worktree under the -worktrees sibling folder - remove: list the current repo worktrees and multi-select to remove Logic lives in a new lib/worktrees theme (list/add/select-and-remove), with the main worktree derived from git so the sibling folder is always resolved against the real repo even when run from a linked worktree. Co-Authored-By: Claude Opus 4.8 --- README.md | 15 +++++ package.json | 1 + src/bin/git-work.ts | 44 ++++++++++++++ src/lib/index.ts | 1 + src/lib/worktrees/add-worktree.ts | 31 ++++++++++ src/lib/worktrees/index.ts | 18 ++++++ src/lib/worktrees/list-worktrees.test.ts | 50 ++++++++++++++++ src/lib/worktrees/list-worktrees.ts | 57 +++++++++++++++++++ .../select-and-remove-worktrees.test.ts | 48 ++++++++++++++++ .../worktrees/select-and-remove-worktrees.ts | 53 +++++++++++++++++ 10 files changed, 318 insertions(+) create mode 100644 src/bin/git-work.ts create mode 100644 src/lib/worktrees/add-worktree.ts create mode 100644 src/lib/worktrees/index.ts create mode 100644 src/lib/worktrees/list-worktrees.test.ts create mode 100644 src/lib/worktrees/list-worktrees.ts create mode 100644 src/lib/worktrees/select-and-remove-worktrees.test.ts create mode 100644 src/lib/worktrees/select-and-remove-worktrees.ts diff --git a/README.md b/README.md index 9c7f75a..ea44ee4 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,21 @@ git squashed origin/master git tidy ``` +#### working with worktrees + +- add a worktree for the current repo, created under the `-worktrees` + sibling folder + +```bash +git work add +``` + +- list the current repo worktrees, multi select the ones you want to remove + +```bash +git work remove +``` + #### setup git workspaces - start by some configuration diff --git a/package.json b/package.json index 2d3f143..2f362d7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "git-squashed": "dist/bin/git-squashed.js", "git-stale": "dist/bin/git-stale.js", "git-tidy": "dist/bin/git-tidy.js", + "git-work": "dist/bin/git-work.js", "git-workspace": "dist/bin/git-workspace.js", "git-workspace-add": "dist/bin/git-workspace-add.js", "git-workspace-clone": "dist/bin/git-workspace-clone.js", diff --git a/src/bin/git-work.ts b/src/bin/git-work.ts new file mode 100644 index 0000000..6760ddb --- /dev/null +++ b/src/bin/git-work.ts @@ -0,0 +1,44 @@ +#! /usr/bin/env node + +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Command } from 'commander'; + +import { + addWorktree, + listWorktrees, + selectAndRemoveWorktrees +} from '../lib/worktrees'; + +const program = new Command(); +program.description('manage git worktrees for the current repo'); + +program + .command('add') + .description('add a worktree under the -worktrees sibling folder') + .argument('', 'worktree name') + .action(async (name) => { + await addWorktree(name); + }); + +program + .command('remove') + .description('list worktrees and select the ones to remove') + .action(async () => { + await listWorktrees().then(selectAndRemoveWorktrees); + }); + +program.parseAsync(process.argv).catch(console.error); diff --git a/src/lib/index.ts b/src/lib/index.ts index a5f7f4d..d54ef44 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -16,3 +16,4 @@ export * from './item'; export * from './config'; export * from './branches'; +export * from './worktrees'; diff --git a/src/lib/worktrees/add-worktree.ts b/src/lib/worktrees/add-worktree.ts new file mode 100644 index 0000000..1406103 --- /dev/null +++ b/src/lib/worktrees/add-worktree.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as path from 'path'; + +import { simpleGit } from 'simple-git'; + +import { listWorktrees } from './list-worktrees'; + +export const addWorktree = async (name: string) => { + const worktrees = await listWorktrees(); + const main = worktrees.find((worktree) => worktree.main); + if (!main) return; + // Worktrees live in a sibling folder to the repo, named -worktrees. + const target = path.join(`${main.path}-worktrees`, name); + const output = await simpleGit().raw(['worktree', 'add', target]); + console.info(output); + return target; +}; diff --git a/src/lib/worktrees/index.ts b/src/lib/worktrees/index.ts new file mode 100644 index 0000000..84b5119 --- /dev/null +++ b/src/lib/worktrees/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './list-worktrees'; +export * from './add-worktree'; +export * from './select-and-remove-worktrees'; diff --git a/src/lib/worktrees/list-worktrees.test.ts b/src/lib/worktrees/list-worktrees.test.ts new file mode 100644 index 0000000..4744f07 --- /dev/null +++ b/src/lib/worktrees/list-worktrees.test.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { listWorktrees } from './list-worktrees'; + +const porcelain = [ + 'worktree /repo', + 'HEAD abc123', + 'branch refs/heads/main', + '', + 'worktree /repo-worktrees/feature', + 'HEAD def456', + 'branch refs/heads/feature', + '' +].join('\n'); + +jest.mock('simple-git', () => { + return { + simpleGit: jest.fn(() => ({ + raw: jest.fn(() => Promise.resolve(porcelain)) + })) + }; +}); + +describe('list-worktrees', () => { + it('should parse the porcelain output and mark the main worktree', async () => { + const worktrees = await listWorktrees(); + expect(worktrees).toEqual([ + { path: '/repo', head: 'abc123', branch: 'main', main: true }, + { + path: '/repo-worktrees/feature', + head: 'def456', + branch: 'feature', + main: false + } + ]); + }); +}); diff --git a/src/lib/worktrees/list-worktrees.ts b/src/lib/worktrees/list-worktrees.ts new file mode 100644 index 0000000..210d8b2 --- /dev/null +++ b/src/lib/worktrees/list-worktrees.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { simpleGit } from 'simple-git'; + +export type Worktree = { + path: string; + head?: string; + branch?: string; + // The first worktree reported by git is always the main one and cannot be + // removed. + main: boolean; +}; + +export const listWorktrees = async (): Promise => { + const output = await simpleGit().raw(['worktree', 'list', '--porcelain']); + const worktrees: Worktree[] = []; + let current: Partial | undefined; + for (const line of output.split('\n')) { + if (line.startsWith('worktree ')) { + current = { path: line.slice('worktree '.length) }; + } else if (!current) { + continue; + } else if (line.startsWith('HEAD ')) { + current.head = line.slice('HEAD '.length); + } else if (line.startsWith('branch ')) { + current.branch = line + .slice('branch '.length) + .replace(/^refs\/heads\//, ''); + } else if (line.trim() === '') { + worktrees.push({ + ...current, + main: worktrees.length === 0 + } as Worktree); + current = undefined; + } + } + if (current) { + worktrees.push({ + ...current, + main: worktrees.length === 0 + } as Worktree); + } + return worktrees; +}; diff --git a/src/lib/worktrees/select-and-remove-worktrees.test.ts b/src/lib/worktrees/select-and-remove-worktrees.test.ts new file mode 100644 index 0000000..bd29870 --- /dev/null +++ b/src/lib/worktrees/select-and-remove-worktrees.test.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { selectAndRemoveWorktrees } from './select-and-remove-worktrees'; + +jest.mock('inquirer', () => { + return { + prompt: jest.fn(() => { + return Promise.resolve({ + selectedPaths: ['/repo-worktrees/feature'] + }); + }) + }; +}); + +jest.mock('simple-git', () => { + return { + simpleGit: jest.fn(() => ({ + raw: jest.fn(() => { + return Promise.resolve('DONE'); + }) + })) + }; +}); + +jest.spyOn(console, 'table'); + +describe('select-and-remove-worktrees', () => { + it('should select and remove worktrees', async () => { + await selectAndRemoveWorktrees([ + { path: '/repo', main: true }, + { path: '/repo-worktrees/feature', branch: 'feature', main: false } + ]); + expect(jest.mocked(console.table).mock.calls).toHaveLength(1); + }); +}); diff --git a/src/lib/worktrees/select-and-remove-worktrees.ts b/src/lib/worktrees/select-and-remove-worktrees.ts new file mode 100644 index 0000000..c48a3d8 --- /dev/null +++ b/src/lib/worktrees/select-and-remove-worktrees.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import inquirer from 'inquirer'; +import { simpleGit } from 'simple-git'; + +import { Worktree } from './list-worktrees'; + +const selectForRemoveQuestion = async (worktrees: Worktree[]) => { + // The main worktree cannot be removed, so it is not offered as a choice. + const removable = worktrees.filter((worktree) => !worktree.main); + if (removable.length === 0) return undefined; + return inquirer.prompt([ + { + name: 'selectedPaths', + type: 'checkbox', + message: 'choose worktrees to remove', + choices: removable.map((worktree) => ({ + name: worktree.branch + ? `${worktree.path} (${worktree.branch})` + : worktree.path, + value: worktree.path + })), + pageSize: 10 + } + ]); +}; + +const selectForRemoveAnswer = async (answer?: { selectedPaths: string[] }) => { + if (!answer) return; + const output = await Promise.all( + answer.selectedPaths.map((target) => + simpleGit().raw(['worktree', 'remove', target]) + ) + ); + console.table(output); +}; + +export const selectAndRemoveWorktrees = async (worktrees: Worktree[]) => { + return selectForRemoveQuestion(worktrees).then(selectForRemoveAnswer); +}; From 7c1f59f1a07bf3fe7d7580ce3fa7c5c06e1fa66a Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 22:52:58 +1100 Subject: [PATCH 4/8] fix: report removed worktree paths instead of empty table git worktree remove is silent on success, so console.table printed rows with blank values. Print the removed paths and skip output when nothing is selected. Co-Authored-By: Claude Opus 4.8 --- src/lib/worktrees/select-and-remove-worktrees.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/worktrees/select-and-remove-worktrees.ts b/src/lib/worktrees/select-and-remove-worktrees.ts index c48a3d8..0e0037c 100644 --- a/src/lib/worktrees/select-and-remove-worktrees.ts +++ b/src/lib/worktrees/select-and-remove-worktrees.ts @@ -39,13 +39,14 @@ const selectForRemoveQuestion = async (worktrees: Worktree[]) => { }; const selectForRemoveAnswer = async (answer?: { selectedPaths: string[] }) => { - if (!answer) return; - const output = await Promise.all( + if (!answer || answer.selectedPaths.length === 0) return; + await Promise.all( answer.selectedPaths.map((target) => simpleGit().raw(['worktree', 'remove', target]) ) ); - console.table(output); + // `git worktree remove` is silent on success, so report what was removed. + console.table(answer.selectedPaths.map((path) => ({ removed: path }))); }; export const selectAndRemoveWorktrees = async (worktrees: Worktree[]) => { From f94278ab402c172b125d62c009390ec5226e7aae Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 22:53:17 +1100 Subject: [PATCH 5/8] simple import --- src/bin/git-work.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bin/git-work.ts b/src/bin/git-work.ts index 6760ddb..fef35e6 100644 --- a/src/bin/git-work.ts +++ b/src/bin/git-work.ts @@ -21,7 +21,7 @@ import { addWorktree, listWorktrees, selectAndRemoveWorktrees -} from '../lib/worktrees'; +} from '../lib'; const program = new Command(); program.description('manage git worktrees for the current repo'); From 47abcf7d2e12faa949eb742437d21c20ed30628b Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 23:05:02 +1100 Subject: [PATCH 6/8] refactor: consolidate git-workspace into subcommands with shared helpers Replace the separate git-workspace-* binaries with a single git-workspace command exposing add/remove/clone/sync/config subcommands (and list as the default), mirroring git-work. Invoke as `git workspace add`, etc. Extract the reusable pieces out of the bin layer: - lib/config/with-filter-options: shared commander filter options - lib/config/list-workspace-items: config load + filtered registry list - lib/config/configure-workspace: interactive workspaces config write - tasks/submit-item-tasks: run item tasks under a cli progress pool Remove the now unused withProgram helper. Config loading is deferred into each data action so `git workspace config` works before any config exists. Co-Authored-By: Claude Opus 4.8 --- README.md | 10 +- package.json | 7 +- src/bin/git-work.ts | 6 +- src/bin/git-workspace-add.ts | 37 ----- src/bin/git-workspace-clone.ts | 54 ------- src/bin/git-workspace-sync.ts | 54 ------- src/bin/git-workspace.ts | 147 +++++++++++------- .../config/configure-workspace.ts} | 18 +-- src/lib/config/index.ts | 4 +- src/lib/config/list-workspace-items.ts | 26 ++++ src/lib/config/with-filter-options.ts | 24 +++ src/lib/config/with-program.ts | 42 ----- src/tasks/index.ts | 1 + .../submit-item-tasks.ts} | 28 ++-- 14 files changed, 170 insertions(+), 288 deletions(-) delete mode 100644 src/bin/git-workspace-add.ts delete mode 100644 src/bin/git-workspace-clone.ts delete mode 100644 src/bin/git-workspace-sync.ts rename src/{bin/git-workspace-config.ts => lib/config/configure-workspace.ts} (79%) create mode 100644 src/lib/config/list-workspace-items.ts create mode 100644 src/lib/config/with-filter-options.ts delete mode 100644 src/lib/config/with-program.ts rename src/{bin/git-workspace-remove.ts => tasks/submit-item-tasks.ts} (50%) diff --git a/README.md b/README.md index ea44ee4..0c4bc1e 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ git work remove - start by some configuration ```bash -git workspace-config +git workspace config ``` it will add the following git global config entries @@ -97,8 +97,8 @@ git.workspaces.default # your default workspace, i.e open-sourc - add a repo to your workspace ```bash -git workspace-add # adds to default workspace -git workspace-add -w # adds to workspace with provided name +git workspace add # adds to default workspace +git workspace add -w # adds to workspace with provided name ``` - view your workspace @@ -111,8 +111,8 @@ git workspace -w # describes workspace with provided name - clone your entire workspace ```bash -git workspace-clone -git workspace-clone -w +git workspace clone +git workspace clone -w ``` > [!NOTE] diff --git a/package.json b/package.json index 2f362d7..5ff1fef 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,7 @@ "git-stale": "dist/bin/git-stale.js", "git-tidy": "dist/bin/git-tidy.js", "git-work": "dist/bin/git-work.js", - "git-workspace": "dist/bin/git-workspace.js", - "git-workspace-add": "dist/bin/git-workspace-add.js", - "git-workspace-clone": "dist/bin/git-workspace-clone.js", - "git-workspace-config": "dist/bin/git-workspace-config.js", - "git-workspace-remove": "dist/bin/git-workspace-remove.js", - "git-workspace-sync": "dist/bin/git-workspace-sync.js" + "git-workspace": "dist/bin/git-workspace.js" }, "scripts": { "build": "tsc", diff --git a/src/bin/git-work.ts b/src/bin/git-work.ts index fef35e6..4afc0d4 100644 --- a/src/bin/git-work.ts +++ b/src/bin/git-work.ts @@ -17,11 +17,7 @@ */ import { Command } from 'commander'; -import { - addWorktree, - listWorktrees, - selectAndRemoveWorktrees -} from '../lib'; +import { addWorktree, listWorktrees, selectAndRemoveWorktrees } from '../lib'; const program = new Command(); program.description('manage git worktrees for the current repo'); diff --git a/src/bin/git-workspace-add.ts b/src/bin/git-workspace-add.ts deleted file mode 100644 index 66b9230..0000000 --- a/src/bin/git-workspace-add.ts +++ /dev/null @@ -1,37 +0,0 @@ -#! /usr/bin/env node - -/** - * Copyright 2023 Hasnae Rehioui - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { fromInput, getConfiguration, withProgram, WithProgram } from '../lib'; - -const addRepo: WithProgram = ({ registry }, program) => { - program - .description('add repo to workspace') - .argument('') - .action(async (urlConnection, opts) => { - const workspace = opts.workspace; - const item = fromInput({ urlConnection, workspace }); - if (!item) return; - - await registry - .add(item) - .then(({ workspace, urlConnection }) => - console.info({ workspace, urlConnection }) - ); - }); -}; - -getConfiguration().then(withProgram(addRepo)).catch(console.error); diff --git a/src/bin/git-workspace-clone.ts b/src/bin/git-workspace-clone.ts deleted file mode 100644 index 6ef1c42..0000000 --- a/src/bin/git-workspace-clone.ts +++ /dev/null @@ -1,54 +0,0 @@ -#! /usr/bin/env node - -/** - * Copyright 2023 Hasnae Rehioui - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { withCliProgress } from '@labset/task-pool-executor'; - -import { - getConfiguration, - Item, - itemFilter, - WithProgram, - withProgram -} from '../lib'; -import { cloneItemTask } from '../tasks'; - -const cloneRepos: WithProgram = ({ registry, workspacesRoot }, program) => { - program.description('clone workspace repos').action(async (opts) => { - const { workspace, namespace, host, slug, keyword } = opts; - await registry - .list(itemFilter({ workspace, namespace, host, slug, keyword })) - .then((items) => { - return itemsHandler({ items, workspacesRoot }); - }); - }); -}; - -const itemsHandler = ({ - items, - workspacesRoot -}: { - items: Item[]; - workspacesRoot: string; -}) => { - return withCliProgress((taskPool) => { - items - .map(cloneItemTask({ workspacesRoot })) - .forEach((t) => t && taskPool.submit(t)); - }); -}; - -getConfiguration().then(withProgram(cloneRepos)).catch(console.error); diff --git a/src/bin/git-workspace-sync.ts b/src/bin/git-workspace-sync.ts deleted file mode 100644 index 251db4f..0000000 --- a/src/bin/git-workspace-sync.ts +++ /dev/null @@ -1,54 +0,0 @@ -#! /usr/bin/env node - -/** - * Copyright 2023 Hasnae Rehioui - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { withCliProgress } from '@labset/task-pool-executor'; - -import { - getConfiguration, - Item, - itemFilter, - withProgram, - WithProgram -} from '../lib'; -import { syncItemTask } from '../tasks'; - -const syncRepos: WithProgram = ({ registry, workspacesRoot }, program) => { - program.description('sync workspace repos').action(async (opts) => { - const { workspace, namespace, host, slug, keyword } = opts; - await registry - .list(itemFilter({ workspace, namespace, host, slug, keyword })) - .then(async (items) => { - return await itemsHandler({ items, workspacesRoot }); - }); - }); -}; - -const itemsHandler = ({ - items, - workspacesRoot -}: { - items: Item[]; - workspacesRoot: string; -}) => { - return withCliProgress((taskPool) => { - items - .map(syncItemTask({ workspacesRoot })) - .forEach((t) => t && taskPool.submit(t)); - }); -}; - -getConfiguration().then(withProgram(syncRepos)).catch(console.error); diff --git a/src/bin/git-workspace.ts b/src/bin/git-workspace.ts index 845974d..2ac74ff 100644 --- a/src/bin/git-workspace.ts +++ b/src/bin/git-workspace.ts @@ -15,67 +15,104 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Command } from 'commander'; + import { + configureWorkspace, + fromInput, getConfiguration, - itemFilter, itemLocation, - withProgram, - WithProgram + itemStatus, + listWorkspaceItems, + withFilterOptions } from '../lib'; -import { itemStatus } from '../lib/item'; +import { cloneItemTask, submitItemTasks, syncItemTask } from '../tasks'; + +const program = new Command(); +program.description('manage git workspaces'); -const listRepos: WithProgram = ({ workspacesRoot, registry }, program) => { - program - .option('--dl, --display-location', 'display location only', false) - .option('--dc, --display-connection', 'display connection only', false) - .option('--ds, --display-slug', 'display slug only', false) - .option('--cl, --cloned', 'display cloned repos only', false) - .description('list repos registered on workspace') - .action(async (opts) => { - const { workspace, namespace, host, slug, keyword, cloned } = opts; - await registry - .list(itemFilter({ workspace, namespace, host, slug, keyword })) - .then(async (items) => { - return await Promise.all( - items.map((item) => - itemStatus({ workspacesRoot, item }) - ) - ); - }) - .then((itemsWithStatus) => { - const filtered = itemsWithStatus.filter((item) => { - return cloned ? item.status !== null : true; - }); - const { displayLocation, displayConnection, displaySlug } = - opts; - const displayOptionEnabled = - displayLocation || displayConnection || displaySlug; +withFilterOptions(program.command('list', { isDefault: true })) + .option('--dl, --display-location', 'display location only', false) + .option('--dc, --display-connection', 'display connection only', false) + .option('--ds, --display-slug', 'display slug only', false) + .option('--cl, --cloned', 'display cloned repos only', false) + .description('list repos registered on workspace') + .action(async (opts) => { + const { workspacesRoot, items } = await listWorkspaceItems(opts); + const itemsWithStatus = await Promise.all( + items.map((item) => itemStatus({ workspacesRoot, item })) + ); + const filtered = itemsWithStatus.filter((item) => + opts.cloned ? item.status !== null : true + ); + const { displayLocation, displayConnection, displaySlug } = opts; + const displayOptionEnabled = + displayLocation || displayConnection || displaySlug; - if (!displayOptionEnabled) { - console.table(filtered, [ - 'workspace', - 'namespace', - 'slug', - 'status' - ]); - return; - } + if (!displayOptionEnabled) { + console.table(filtered, [ + 'workspace', + 'namespace', + 'slug', + 'status' + ]); + return; + } - filtered.forEach((item) => { - if (displayLocation) { - const target = itemLocation({ - workspacesRoot, - item - }); - console.info(target); - } else if (displayConnection) { - console.info(item.urlConnection); - } else if (displaySlug) { - console.info(item.slug); - } - }); - }); + filtered.forEach((item) => { + if (displayLocation) { + console.info(itemLocation({ workspacesRoot, item })); + } else if (displayConnection) { + console.info(item.urlConnection); + } else if (displaySlug) { + console.info(item.slug); + } }); -}; + }); + +program + .command('add') + .description('add repo to workspace') + .argument('') + .option('-w, --workspace ', 'with workspace') + .action(async (urlConnection, opts) => { + const { registry, workspacesDefault } = await getConfiguration(); + const workspace = opts.workspace ?? workspacesDefault; + const item = fromInput({ urlConnection, workspace }); + if (!item) return; + await registry + .add(item) + .then(({ workspace, urlConnection }) => + console.info({ workspace, urlConnection }) + ); + }); + +withFilterOptions(program.command('remove')) + .description('remove workspace repos') + .action(async (opts) => { + const { registry, items } = await listWorkspaceItems(opts); + await Promise.all(items.map((item) => registry.remove(item.id))); + }); + +withFilterOptions(program.command('clone')) + .description('clone workspace repos') + .action(async (opts) => { + const { workspacesRoot, items } = await listWorkspaceItems(opts); + await submitItemTasks(items, cloneItemTask({ workspacesRoot })); + }); + +withFilterOptions(program.command('sync')) + .description('sync workspace repos') + .action(async (opts) => { + const { workspacesRoot, items } = await listWorkspaceItems(opts); + await submitItemTasks(items, syncItemTask({ workspacesRoot })); + }); + +program + .command('config') + .description('configure git workspaces') + .action(async () => { + await configureWorkspace(); + }); -getConfiguration().then(withProgram(listRepos)).catch(console.error); +program.parseAsync(process.argv).catch(console.error); diff --git a/src/bin/git-workspace-config.ts b/src/lib/config/configure-workspace.ts similarity index 79% rename from src/bin/git-workspace-config.ts rename to src/lib/config/configure-workspace.ts index cbb05d2..7f3d601 100644 --- a/src/bin/git-workspace-config.ts +++ b/src/lib/config/configure-workspace.ts @@ -1,5 +1,3 @@ -#! /usr/bin/env node - /** * Copyright 2023 Hasnae Rehioui * @@ -21,10 +19,8 @@ import * as path from 'path'; import inquirer from 'inquirer'; import { simpleGit, GitConfigScope } from 'simple-git'; -import { Configuration } from '../lib'; - -const configureGitWorkspaceQuestions = async () => { - return inquirer.prompt([ +export const configureWorkspace = async () => { + const answer = await inquirer.prompt([ { name: 'workspacesRoot', type: 'input', @@ -38,12 +34,6 @@ const configureGitWorkspaceQuestions = async () => { default: 'projects' } ]); -}; - -const configureGitWorkspaceAnswers = async ( - answer: Omit | undefined -) => { - if (!answer) return; await simpleGit() .addConfig( 'labset.workspaces.root', @@ -58,7 +48,3 @@ const configureGitWorkspaceAnswers = async ( GitConfigScope.global ); }; - -configureGitWorkspaceQuestions() - .then(configureGitWorkspaceAnswers) - .catch(console.error); diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 5c16373..31bf082 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -14,4 +14,6 @@ * limitations under the License. */ export * from './get-configuration'; -export * from './with-program'; +export * from './configure-workspace'; +export * from './list-workspace-items'; +export * from './with-filter-options'; diff --git a/src/lib/config/list-workspace-items.ts b/src/lib/config/list-workspace-items.ts new file mode 100644 index 0000000..e5fc161 --- /dev/null +++ b/src/lib/config/list-workspace-items.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { itemFilter, WithOptions } from '../item'; + +import { getConfiguration } from './get-configuration'; + +export const listWorkspaceItems = async (options: WithOptions) => { + const { registry, workspacesRoot, workspacesDefault } = + await getConfiguration(); + const workspace = options.workspace ?? workspacesDefault; + const items = await registry.list(itemFilter({ ...options, workspace })); + return { registry, workspacesRoot, items }; +}; diff --git a/src/lib/config/with-filter-options.ts b/src/lib/config/with-filter-options.ts new file mode 100644 index 0000000..bc1379a --- /dev/null +++ b/src/lib/config/with-filter-options.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2023 Hasnae Rehioui + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Command } from 'commander'; + +export const withFilterOptions = (command: Command) => + command + .option('-w, --workspace ', 'with workspace') + .option('--ns, --namespace ', 'filter by namespace') + .option('-h, --host ', 'filter by host') + .option('-s, --slug ', 'filter by name or slug') + .option('-k, --keyword ', 'filter by keyword'); diff --git a/src/lib/config/with-program.ts b/src/lib/config/with-program.ts deleted file mode 100644 index 7d97b5e..0000000 --- a/src/lib/config/with-program.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2023 Hasnae Rehioui - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { Command } from 'commander'; - -import { Configuration } from './get-configuration'; - -export type WithProgram = ( - configuration: Configuration, - program: Command -) => void; - -export const withProgram = (fn: WithProgram) => { - return async (configuration: Configuration) => { - const program = new Command(); - program.option( - '-w, --workspace ', - 'with workspace', - configuration.workspacesDefault - ); - program.option('--ns, --namespace ', 'filter by namespace'); - program.option('-h, --host ', 'filter by host'); - program.option('-s, --slug ', 'filter by name or slug'); - program.option('-k, --keyword ', 'filter by keyword'); - fn(configuration, program); - const packageJson = await import('../../../package.json'); - program.version(packageJson.version); - program.parse(process.argv); - }; -}; diff --git a/src/tasks/index.ts b/src/tasks/index.ts index 856cc2d..3a63fb6 100644 --- a/src/tasks/index.ts +++ b/src/tasks/index.ts @@ -15,3 +15,4 @@ */ export * from './clone-item-task'; export * from './sync-item-task'; +export * from './submit-item-tasks'; diff --git a/src/bin/git-workspace-remove.ts b/src/tasks/submit-item-tasks.ts similarity index 50% rename from src/bin/git-workspace-remove.ts rename to src/tasks/submit-item-tasks.ts index c79250c..4f43d05 100644 --- a/src/bin/git-workspace-remove.ts +++ b/src/tasks/submit-item-tasks.ts @@ -1,5 +1,3 @@ -#! /usr/bin/env node - /** * Copyright 2023 Hasnae Rehioui * @@ -15,16 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getConfiguration, itemFilter, withProgram, WithProgram } from '../lib'; +import { + CliProgressRunContext, + Runnable, + withCliProgress +} from '@labset/task-pool-executor'; -const removeRepos: WithProgram = ({ registry }, program) => { - program.description('remove workspace repos').action(async (opts) => { - const { workspace, namespace, host, slug, keyword } = opts; - const targets = await registry.list( - itemFilter({ workspace, namespace, host, slug, keyword }) - ); - await Promise.all(targets.map((target) => registry.remove(target.id))); - }); -}; +import { Item } from '../lib'; -getConfiguration().then(withProgram(removeRepos)).catch(console.error); +export const submitItemTasks = ( + items: Item[], + task: (item: Item) => Runnable | undefined +) => + withCliProgress((taskPool) => { + items + .map(task) + .forEach((runnable) => runnable && taskPool.submit(runnable)); + }); From d189aa8b7333f112b0e88e82c0fb15e72b1b5802 Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 23:06:53 +1100 Subject: [PATCH 7/8] docs: update CLAUDE.md with lib themes and command structure Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3fe10e1..2f57a1e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,11 +28,43 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is a TypeScript CLI tool that provides Git workspace management utilities. The codebase is organized as follows: -- **src/bin/** - CLI entry points for all git commands (git-recent, git-merged, git-workspace, etc.) -- **src/lib/** - Core library functions and utilities shared across commands -- **src/tasks/** - Task implementations for workspace operations (clone, sync) +- **src/bin/** - CLI entry points, one file per installed binary (see CLI Commands below) +- **src/lib/** - Core library functions, grouped by theme, each with an `index.ts` barrel re-exported from `src/lib/index.ts`: + - `item/` - repository domain model (types, link parsing, location, filtering, status) + - `config/` - configuration and CLI plumbing (read/write git workspace config, filtered registry queries, shared commander options) + - `branches/` - git branch operations (list recent, exclude operational, select to checkout/delete) + - `worktrees/` - git worktree operations (list, add, select to remove) +- **src/tasks/** - Task implementations run on the concurrent task pool (clone/sync an item, submit item tasks) - **dist/** - Compiled JavaScript output (generated by TypeScript compiler) +Layering is one-directional: `bin → tasks → lib` and `bin → lib`. `lib` never imports from `tasks`. + +### CLI Commands + +Each binary is registered in `package.json` `bin` and invoked as a git subcommand (e.g. `git recent`). + +**Branch commands** (operate on the current repo, no workspace config required): + +- `git recent` - list recently checked-out branches, select one to check out +- `git merged` / `git squashed` - list merged/squashed branches, multi-select to delete +- `git stale` / `git tidy` - list branches, multi-select to delete +- `git list-remote` - list remote branches, select one to check out + +**Worktree command** - `git work` with subcommands (operates on the current repo): + +- `git work add ` - create a worktree under the `-worktrees` sibling folder +- `git work remove` - list worktrees, multi-select to remove + +**Workspace command** - `git workspace` with subcommands (backed by git config + registry): + +- `git workspace` / `git workspace list` - list registered repos (default subcommand) +- `git workspace add ` - register a repo +- `git workspace remove` - remove matching repos +- `git workspace clone` / `git workspace sync` - clone/sync matching repos via the task pool +- `git workspace config` - interactively write the git global workspace config + +`git work` and `git workspace` use [commander](https://github.com/tj/commander.js) subcommands in a single binary. The filter options (`-w/--workspace`, `--ns/--namespace`, `-h/--host`, `-s/--slug`, `-k/--keyword`) are shared via `withFilterOptions`. Workspace config is loaded lazily inside each action (not at startup) so `git workspace config` works before any config exists. + ### Key Components **Workspace Management**: The system uses git global config to store workspace configuration: From f2b03f3065b8aafd1eda1d4e4ac5489561cd580b Mon Sep 17 00:00:00 2001 From: Hasnae Date: Sat, 24 Oct 2026 23:14:59 +1100 Subject: [PATCH 8/8] chore: address in-scope sonarcloud issues - use node:path / node:os import prefixes (S7772) - add await in selectForRemoveQuestion so it stays a promise (S7503) Co-Authored-By: Claude Opus 4.8 --- src/lib/config/configure-workspace.ts | 4 ++-- src/lib/worktrees/add-worktree.ts | 2 +- src/lib/worktrees/select-and-remove-worktrees.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/config/configure-workspace.ts b/src/lib/config/configure-workspace.ts index 7f3d601..a03deca 100644 --- a/src/lib/config/configure-workspace.ts +++ b/src/lib/config/configure-workspace.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as os from 'os'; -import * as path from 'path'; +import * as os from 'node:os'; +import * as path from 'node:path'; import inquirer from 'inquirer'; import { simpleGit, GitConfigScope } from 'simple-git'; diff --git a/src/lib/worktrees/add-worktree.ts b/src/lib/worktrees/add-worktree.ts index 1406103..4eb09bd 100644 --- a/src/lib/worktrees/add-worktree.ts +++ b/src/lib/worktrees/add-worktree.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as path from 'path'; +import * as path from 'node:path'; import { simpleGit } from 'simple-git'; diff --git a/src/lib/worktrees/select-and-remove-worktrees.ts b/src/lib/worktrees/select-and-remove-worktrees.ts index 0e0037c..7e7872f 100644 --- a/src/lib/worktrees/select-and-remove-worktrees.ts +++ b/src/lib/worktrees/select-and-remove-worktrees.ts @@ -22,7 +22,7 @@ const selectForRemoveQuestion = async (worktrees: Worktree[]) => { // The main worktree cannot be removed, so it is not offered as a choice. const removable = worktrees.filter((worktree) => !worktree.main); if (removable.length === 0) return undefined; - return inquirer.prompt([ + return await inquirer.prompt([ { name: 'selectedPaths', type: 'checkbox',