diff --git a/README.md b/README.md index 9e58891..b1ce42a 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,11 @@ gx doctor # setup + repair another repo without switching your current repo checkout gx setup --target /path/to/repo gx doctor --target /path/to/repo +# optional: from parent folder, generate VS Code workspace view for repo + agent worktrees +cd /path/to +gx setup --target ./repo --parent-workspace-view +# open this in VS Code to manage both base repo and .omx/agent-worktrees +code ./repo-branches.code-workspace # protected branch management gx protect list @@ -198,6 +203,7 @@ gx agents stop - `gx init` is alias of `gx setup`. - Setup/doctor can install missing global OMX/OpenSpec/codex-auth with explicit Y/N confirmation. - `gx setup` checks GitHub CLI (`gh`) and prints install guidance if missing. +- Optional parent-folder VS Code Source Control view: `gx setup --target /path/to/repo --parent-workspace-view` creates `../-branches.code-workspace`. - Interactive self-update prompt defaults to **No** (`[y/N]`). - In initialized repos, `setup`/`install`/`fix` block protected-base writes unless explicitly overridden. - Direct commits/pushes to protected branches are blocked by default. @@ -366,6 +372,13 @@ npm pack --dry-run ## Release notes +### v5.0.15 + +- Added `gx setup --parent-workspace-view` to generate a parent-folder VS Code workspace (`../-branches.code-workspace`) that shows both the base repo and `.omx/agent-worktrees` in Source Control. +- Added dry-run-safe parent workspace operations (`would-create` / `would-update`) and setup output that prints the created workspace path. +- Added regression coverage for parent workspace generation and dry-run behavior. +- Bumped package version from `5.0.14` to `5.0.15`. + ### v5.0.14 - Changed release metadata for the next npm publish by bumping package version from `5.0.13` to `5.0.14`. diff --git a/bin/multiagent-safety.js b/bin/multiagent-safety.js index 895535a..59e11eb 100755 --- a/bin/multiagent-safety.js +++ b/bin/multiagent-safety.js @@ -184,7 +184,7 @@ const SUGGESTIBLE_COMMANDS = [ ]; const CLI_COMMAND_DESCRIPTIONS = [ ['status', 'Show GuardeX CLI + service health without modifying files'], - ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore)'], + ['setup', 'Install + repair guardrails in a git repo (supports --no-gitignore, --parent-workspace-view)'], ['init', 'Alias of setup (bootstrap + repair guardrails in a git repo)'], ['doctor', 'Repair safety setup drift, then verify repo safety'], ['report', 'Generate security/safety reports (for example: OpenSSF scorecard)'], @@ -445,6 +445,7 @@ NOTES - ${TOOL_NAME} setup asks for Y/N approval before global installs - ${TOOL_NAME} setup checks GitHub CLI (gh) and prints install guidance if missing - For other repos: ${SHORT_TOOL_NAME} setup --target then ${SHORT_TOOL_NAME} doctor --target + - Optional parent-folder Source Control view: ${SHORT_TOOL_NAME} setup --target --parent-workspace-view - In initialized repos, setup/install/fix block in-place writes on protected main by default - setup/doctor auto-finish clean pending agent/* branches via PR flow into the current local base branch - doctor auto-runs in a sandbox agent branch/worktree on protected main and tries auto-finish PR flow @@ -838,6 +839,14 @@ function configureHooks(repoRoot, dryRun) { return { status: 'set', key: 'core.hooksPath', value: '.githooks' }; } +function requireValue(rawArgs, index, flagName) { + const value = rawArgs[index + 1]; + if (!value || value.startsWith('-')) { + throw new Error(`${flagName} requires a value`); + } + return value; +} + function parseCommonArgs(rawArgs, defaults) { const options = { ...defaults }; @@ -899,6 +908,76 @@ function parseCommonArgs(rawArgs, defaults) { return options; } +function parseSetupArgs(rawArgs, defaults) { + const setupDefaults = { ...defaults, parentWorkspaceView: false }; + const forwardedArgs = []; + + for (const arg of rawArgs) { + if (arg === '--parent-workspace-view') { + setupDefaults.parentWorkspaceView = true; + continue; + } + if (arg === '--no-parent-workspace-view') { + setupDefaults.parentWorkspaceView = false; + continue; + } + forwardedArgs.push(arg); + } + + return parseCommonArgs(forwardedArgs, setupDefaults); +} + +function normalizeWorkspacePath(relativePath) { + return String(relativePath || '.').replace(/\\/g, '/'); +} + +function buildParentWorkspaceView(repoRoot) { + const parentDir = path.dirname(repoRoot); + const workspaceFileName = `${path.basename(repoRoot)}-branches.code-workspace`; + const workspacePath = path.join(parentDir, workspaceFileName); + const repoRelativePath = normalizeWorkspacePath(path.relative(parentDir, repoRoot) || '.'); + const worktreesRelativePath = normalizeWorkspacePath( + path.join(repoRelativePath === '.' ? '' : repoRelativePath, '.omx', 'agent-worktrees'), + ); + + return { + workspacePath, + payload: { + folders: [ + { path: repoRelativePath }, + { path: worktreesRelativePath }, + ], + settings: { + 'scm.alwaysShowRepositories': true, + }, + }, + }; +} + +function ensureParentWorkspaceView(repoRoot, dryRun) { + const { workspacePath, payload } = buildParentWorkspaceView(repoRoot); + const operationFile = path.relative(repoRoot, workspacePath) || path.basename(workspacePath); + const nextContent = `${JSON.stringify(payload, null, 2)}\n`; + const note = 'parent VS Code workspace view'; + + if (!fs.existsSync(workspacePath)) { + if (!dryRun) { + fs.writeFileSync(workspacePath, nextContent, 'utf8'); + } + return { status: dryRun ? 'would-create' : 'created', file: operationFile, note }; + } + + const currentContent = fs.readFileSync(workspacePath, 'utf8'); + if (currentContent === nextContent) { + return { status: 'unchanged', file: operationFile, note }; + } + + if (!dryRun) { + fs.writeFileSync(workspacePath, nextContent, 'utf8'); + } + return { status: dryRun ? 'would-update' : 'updated', file: operationFile, note }; +} + function hasGuardexBootstrapFiles(repoRoot) { const required = [ 'AGENTS.md', @@ -4284,7 +4363,7 @@ function report(rawArgs) { } function setup(rawArgs) { - const options = parseCommonArgs(rawArgs, { + const options = parseSetupArgs(rawArgs, { target: process.cwd(), force: false, skipAgents: false, @@ -4331,6 +4410,9 @@ function setup(rawArgs) { assertProtectedMainWriteAllowed(options, 'setup'); const installPayload = runInstallInternal(options); installPayload.operations.push(ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun))); + if (options.parentWorkspaceView) { + installPayload.operations.push(ensureParentWorkspaceView(installPayload.repoRoot, Boolean(options.dryRun))); + } printOperations('Setup/install', installPayload, options.dryRun); const fixPayload = runFixInternal({ @@ -4349,6 +4431,11 @@ function setup(rawArgs) { return; } + if (options.parentWorkspaceView) { + const parentWorkspace = buildParentWorkspaceView(installPayload.repoRoot); + console.log(`[${TOOL_NAME}] Parent workspace view: ${parentWorkspace.workspacePath}`); + } + const scanResult = runScanInternal({ target: options.target, json: false }); const currentBaseBranch = currentBranchName(scanResult.repoRoot); const autoFinishSummary = autoFinishReadyAgentBranches(scanResult.repoRoot, { diff --git a/package-lock.json b/package-lock.json index 45ff00a..7aedcc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imdeadpool/guardex", - "version": "5.0.14", + "version": "5.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imdeadpool/guardex", - "version": "5.0.14", + "version": "5.0.15", "license": "MIT", "bin": { "guardex": "bin/multiagent-safety.js", diff --git a/package.json b/package.json index 3510278..8ed2944 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imdeadpool/guardex", - "version": "5.0.14", + "version": "5.0.15", "description": "GuardeX: the Guardian T-Rex for your repo, with hardened multi-agent git guardrails.", "license": "MIT", "preferGlobal": true, diff --git a/test/install.test.js b/test/install.test.js index 6e642ea..d37546e 100644 --- a/test/install.test.js +++ b/test/install.test.js @@ -356,6 +356,44 @@ test('setup provisions workflow files and repo config', () => { assert.equal(secondRun.status, 0, secondRun.stderr || secondRun.stdout); }); +test('setup --parent-workspace-view creates one-level-up VS Code workspace for repo + agent worktrees', () => { + const repoDir = initRepo(); + const parentDir = path.dirname(repoDir); + const workspacePath = path.join(parentDir, `${path.basename(repoDir)}-branches.code-workspace`); + + assert.equal(fs.existsSync(workspacePath), false, 'workspace file should not exist before setup'); + + const result = runNode( + ['setup', '--target', repoDir, '--no-global-install', '--parent-workspace-view'], + repoDir, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /parent VS Code workspace view/); + assert.match(result.stdout, /Parent workspace view:/); + + assert.equal(fs.existsSync(workspacePath), true, 'setup should create parent workspace file'); + const workspace = JSON.parse(fs.readFileSync(workspacePath, 'utf8')); + assert.deepEqual(workspace.folders, [ + { path: path.basename(repoDir) }, + { path: `${path.basename(repoDir)}/.omx/agent-worktrees` }, + ]); + assert.equal(workspace.settings['scm.alwaysShowRepositories'], true); +}); + +test('setup --parent-workspace-view respects dry-run and does not write parent workspace file', () => { + const repoDir = initRepo(); + const parentDir = path.dirname(repoDir); + const workspacePath = path.join(parentDir, `${path.basename(repoDir)}-branches.code-workspace`); + + const result = runNode( + ['setup', '--target', repoDir, '--no-global-install', '--parent-workspace-view', '--dry-run'], + repoDir, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /would-create\s+\.\.\/repo-branches\.code-workspace \(parent VS Code workspace view\)/); + assert.equal(fs.existsSync(workspacePath), false, 'dry run must not create parent workspace file'); +}); + test('setup refreshes existing managed AGENTS block to latest template policy', () => { const repoDir = initRepo(); const legacyAgents = `# AGENTS