@tsuzuku/arise 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Arise (`arise`)
2
+
3
+ Unified, workplace-agnostic Git worktree and Herdr workspace orchestrator with pluggable project presets.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ - **Unified Lifecycle**: Handles git worktree creation, branch resolution, Herdr workspace orchestration, 4-pane quadrant terminal setup, and safe teardown/nuke.
10
+ - **Pluggable Presets**: Built-in support for **Node.js** (`npm`/`yarn`/`pnpm`), **Laravel/PHP** (`composer`, logs, permissions), and **Generic** projects.
11
+ - **Zero-Config Auto-Detection**: Automatically detects project types based on directory markers (`package.json`, `composer.json`, `artisan`, etc.).
12
+ - **Workplace & Repo Agnostic**: Supports standard Git repos and bare repositories (`--git-dir`). Custom environment paths and symlinks are completely configurable via `.ariserc.json` / `.worktreerc.json` / `arise.config.js`.
13
+ - **Full Feature Parity**: Both Node and PHP projects get full `--nuke` / `--cleanup` suites with protected branch safety and Herdr workspace auto-closing.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # Install globally via npm
21
+ npm install -g arise
22
+
23
+ # Or run directly without installing via npx
24
+ npx arise --branch <branch-name>
25
+ ```
26
+
27
+ Aliases provided: `arise`, `herdr-worktree`, `herder-worktree`, and `hwk`.
28
+
29
+ ---
30
+
31
+ ## Quick Start
32
+
33
+ ### 1. Create a Worktree Session
34
+ ```bash
35
+ # Auto-detects project type (Node, Laravel, etc.)
36
+ arise --branch feature/login
37
+
38
+ # Explicitly specify a preset
39
+ arise --branch feature/login --preset laravel
40
+
41
+ # Specify base branch or custom workspace name
42
+ arise --branch feature/login --source develop --focus agy
43
+ ```
44
+
45
+ ### 2. Nuke / Clean Up a Worktree
46
+ ```bash
47
+ # Inside a worktree directory (auto-detects current worktree):
48
+ arise --nuke
49
+
50
+ # From anywhere, by branch or directory name:
51
+ arise --nuke feature-login
52
+
53
+ # Keep branches, only remove directory:
54
+ arise --nuke feature-login --dir-only
55
+
56
+ # Delete local branch, keep remote on origin:
57
+ arise --nuke feature-login --keep-remote
58
+ ```
59
+
60
+ ### 3. Install AI Agent Skill (Antigravity `agy`, Claude Code, etc.)
61
+ ```bash
62
+ # Install globally to ~/.agents/skills and link to ~/.gemini/skills:
63
+ arise --install-skill
64
+
65
+ # Install locally to workspace (.agents/skills/arise):
66
+ arise --install-skill --local
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Customizing via `.ariserc.json` or `arise.config.js`
72
+
73
+ Place a `.ariserc.json` in your repository root, worktrees base directory, or `~/.config/arise/config.js` (also supports `.worktreerc.json` / `~/.config/herdr-worktree/` for backwards compatibility):
74
+
75
+ ```json
76
+ {
77
+ "preset": "laravel",
78
+ "repo": {
79
+ "bareRepo": "/path/to/bare/repo.git",
80
+ "worktreesBase": "/path/to/worktrees",
81
+ "defaultBaseBranch": "main"
82
+ },
83
+ "workspace": {
84
+ "labelPrefix": "[API] ",
85
+ "defaultFocus": "agy"
86
+ },
87
+ "scaffold": {
88
+ "envSource": "/path/to/shared/.env",
89
+ "symlink": "/var/www/my-app"
90
+ }
91
+ }
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Architecture & Modular Structure
97
+
98
+ ```
99
+ arise/
100
+ ├── package.json
101
+ ├── index.js # Main runner module
102
+ ├── bin/
103
+ │ └── cli.js # Executable CLI
104
+ ├── lib/
105
+ │ ├── cli.js # CLI argument parsing & help output
106
+ │ ├── config.js # Config discovery & preset merging
107
+ │ ├── git.js # Git worktree & branch operations
108
+ │ ├── herdr.js # Herdr workspace & pane operations
109
+ │ ├── layout.js # Declarative terminal layout renderer
110
+ │ ├── context.js # Lifecycle execution context & helpers
111
+ │ ├── skill.js # Agent skill installer for agy/claude
112
+ │ └── lifecycle/
113
+ │ ├── create.js # Worktree creation & layout pipeline
114
+ │ └── nuke.js # Safe teardown & branch deletion pipeline
115
+ └── presets/
116
+ ├── index.js # Preset registry & auto-detection
117
+ ├── node.js # Node / JS project preset
118
+ ├── laravel.js # Laravel / PHP project preset
119
+ └── generic.js # Fallback generic preset
120
+ ```
@@ -0,0 +1,112 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "AriseConfig",
4
+ "description": "Configuration schema for Arise (.ariserc.json / .worktreerc.json)",
5
+ "type": "object",
6
+ "properties": {
7
+ "preset": {
8
+ "type": "string",
9
+ "description": "Preset name to use ('node', 'laravel', 'generic' or custom registered preset)"
10
+ },
11
+ "repo": {
12
+ "type": "object",
13
+ "description": "Repository topology and branch configuration",
14
+ "properties": {
15
+ "bareRepo": {
16
+ "type": ["string", "null"],
17
+ "description": "Path to bare git repository (e.g. '/path/to/bare/repo.git')"
18
+ },
19
+ "worktreesBase": {
20
+ "type": ["string", "null"],
21
+ "description": "Base directory where worktrees should be placed (e.g. '/path/to/worktrees')"
22
+ },
23
+ "defaultBaseBranch": {
24
+ "type": "string",
25
+ "description": "Default base branch to branch off (e.g. 'develop', 'prod', 'main')"
26
+ },
27
+ "protectedBranches": {
28
+ "type": "array",
29
+ "items": { "type": "string" },
30
+ "description": "Array of branch names protected against deletion during --nuke"
31
+ }
32
+ },
33
+ "additionalProperties": false
34
+ },
35
+ "workspace": {
36
+ "type": "object",
37
+ "description": "Herdr workspace naming and focus configuration",
38
+ "properties": {
39
+ "labelPrefix": {
40
+ "type": "string",
41
+ "description": "Prefix added to Herdr workspace labels (e.g. '[BE] ')"
42
+ },
43
+ "defaultFocus": {
44
+ "type": "string",
45
+ "description": "Default pane to focus upon creation ('agy', 'vim', 'logs', 'server', 'shell')"
46
+ }
47
+ },
48
+ "additionalProperties": false
49
+ },
50
+ "layout": {
51
+ "type": "array",
52
+ "description": "Declarative 4-pane quadrant layout definitions",
53
+ "items": {
54
+ "type": "object",
55
+ "required": ["id", "title"],
56
+ "properties": {
57
+ "id": {
58
+ "type": "string",
59
+ "description": "Unique identifier for this pane within the layout"
60
+ },
61
+ "title": {
62
+ "type": "string",
63
+ "description": "Title displayed on the Herdr pane"
64
+ },
65
+ "cmd": {
66
+ "type": ["string", "null"],
67
+ "description": "Command to run when pane is opened (e.g. 'vim .', 'npm run dev', 'tail -f ...')"
68
+ },
69
+ "position": {
70
+ "type": "string",
71
+ "enum": ["root"],
72
+ "description": "Set to 'root' for the initial root pane"
73
+ },
74
+ "from": {
75
+ "type": "string",
76
+ "description": "ID of parent pane to split from"
77
+ },
78
+ "split": {
79
+ "type": "string",
80
+ "enum": ["right", "down"],
81
+ "description": "Split direction ('right' or 'down')"
82
+ },
83
+ "focus": {
84
+ "type": "boolean",
85
+ "description": "Whether this pane should receive focus by default"
86
+ }
87
+ },
88
+ "additionalProperties": false
89
+ }
90
+ },
91
+ "scaffold": {
92
+ "type": "object",
93
+ "description": "Scaffolding options (environment files, symlinks, dependency installation, etc.)",
94
+ "properties": {
95
+ "envSource": {
96
+ "type": ["string", "null"],
97
+ "description": "Path to .env source file to copy into new worktree"
98
+ },
99
+ "symlink": {
100
+ "type": ["string", "null"],
101
+ "description": "Path to web server symlink to update to active worktree"
102
+ },
103
+ "install": {
104
+ "type": ["string", "boolean", "null"],
105
+ "description": "Command to run to install dependencies upon creation (e.g. 'npm install --legacy-peer-deps', 'pnpm install', 'composer install --no-interaction'), or false/null to skip installation."
106
+ }
107
+ },
108
+ "additionalProperties": true
109
+ }
110
+ },
111
+ "additionalProperties": false
112
+ }
package/bin/cli.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require('../index');
4
+
5
+ run(process.argv.slice(2), process.cwd()).catch(err => {
6
+ console.error(`Fatal error: ${err.message}`);
7
+ process.exit(1);
8
+ });
package/index.js ADDED
@@ -0,0 +1,36 @@
1
+ const { parseArgs, showUsage } = require('./lib/cli');
2
+ const { resolveConfiguration } = require('./lib/config');
3
+ const { executeCreate } = require('./lib/lifecycle/create');
4
+ const { executeNuke } = require('./lib/lifecycle/nuke');
5
+
6
+ async function run(argv = process.argv.slice(2), cwd = process.cwd()) {
7
+ const flags = parseArgs(argv);
8
+
9
+ if (flags.showHelp) {
10
+ showUsage();
11
+ process.exit(0);
12
+ }
13
+
14
+ if (flags.showVersion) {
15
+ console.log('arise v1.0.0');
16
+ process.exit(0);
17
+ }
18
+
19
+ if (flags.installSkill) {
20
+ const { installSkill } = require('./lib/skill');
21
+ installSkill({ scope: flags.skillScope, cwd });
22
+ process.exit(0);
23
+ }
24
+
25
+ const config = resolveConfiguration(flags, cwd);
26
+
27
+ if (flags.isCleanup) {
28
+ await executeNuke(flags, config, cwd);
29
+ } else {
30
+ await executeCreate(flags, config, cwd);
31
+ }
32
+ }
33
+
34
+ module.exports = {
35
+ run,
36
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,118 @@
1
+ const path = require('path');
2
+
3
+ function showUsage() {
4
+ console.log(`
5
+ Arise - Unified Worktree & Herdr Workspace Orchestrator
6
+
7
+ Usage:
8
+ arise --branch <branch> [options]
9
+ arise --nuke [<worktree dir or branch>] [nuke-options]
10
+ arise --cleanup [<worktree dir or branch>] [nuke-options]
11
+
12
+ Creation Arguments:
13
+ --branch, -b <branch> (Required for creation) Git branch to create or boot into.
14
+ --dirname, -d <dirname> (Optional) Directory name of the worktree. Defaults to sanitized branch name.
15
+ --workspace, -w <name> (Optional) Custom Herdr workspace name.
16
+ --source, -s, --base <src> (Optional) Base branch if creating a new branch. Defaults to preset default (e.g. 'develop' / 'prod').
17
+ --preset, -p <preset> (Optional) Project preset ('node', 'laravel', 'generic', or custom). Auto-detected if omitted.
18
+ --focus, -f <pane> (Optional) Pane to focus ('agy', 'vim', 'logs', 'server', 'shell'). Defaults to 'agy'.
19
+
20
+ Nuke / Cleanup Arguments:
21
+ --nuke, -n [<target>] Nuke the worktree: closes Herdr workspace, removes worktree directory,
22
+ deletes local branch, and deletes remote branch on origin.
23
+ (Auto-detects current worktree if omitted inside a worktree directory).
24
+ --cleanup, -c [<target>] Alias for --nuke.
25
+ --dir-only, --keep-branch (Optional) Only remove the worktree directory; keep local and remote branches.
26
+ --keep-remote, --local-only (Optional) Delete local branch, but keep remote branch on origin.
27
+ --force, -f (Optional) Force worktree removal even if changes are uncommitted.
28
+
29
+ Agent Skill Arguments:
30
+ --install-skill, -i Install the agent skill for Antigravity (agy), Claude Code, and other AI agents.
31
+ --global (Default) Install skill globally to ~/.agents/skills and ~/.gemini/skills.
32
+ --local, --workspace Install skill locally to current workspace (.agents/skills).
33
+
34
+ General Arguments:
35
+ --help, -h Show this help message.
36
+ --version, -v Show version information.
37
+ `);
38
+ }
39
+
40
+ function parseArgs(argv = process.argv.slice(2)) {
41
+ const flags = {
42
+ isCleanup: false,
43
+ cleanupTarget: null,
44
+ dirOnly: false,
45
+ keepRemote: false,
46
+ force: false,
47
+ branch: null,
48
+ dirname: null,
49
+ workspaceName: null,
50
+ source: null,
51
+ presetName: null,
52
+ focusTarget: null,
53
+ installSkill: false,
54
+ skillScope: 'global',
55
+ showHelp: false,
56
+ showVersion: false,
57
+ rawArgs: argv,
58
+ };
59
+
60
+ for (let i = 0; i < argv.length; i++) {
61
+ const arg = argv[i];
62
+
63
+ if (arg === '--help' || arg === '-h') {
64
+ flags.showHelp = true;
65
+ } else if (arg === '--version' || arg === '-v') {
66
+ flags.showVersion = true;
67
+ } else if (arg === '--install-skill' || arg === '--install-agent-skill' || arg === '--setup-skill' || arg === '-i') {
68
+ flags.installSkill = true;
69
+ } else if (arg === '--global') {
70
+ flags.skillScope = 'global';
71
+ } else if (arg === '--local' || arg === '--workspace') {
72
+ flags.skillScope = 'local';
73
+ } else if (arg === '--nuke' || arg === '-n' || arg === 'nuke' || arg === '--cleanup' || arg === '-c' || arg === 'cleanup') {
74
+ flags.isCleanup = true;
75
+ if (argv[i + 1] && !argv[i + 1].startsWith('-')) {
76
+ flags.cleanupTarget = argv[i + 1];
77
+ i++;
78
+ }
79
+ } else if (arg === '--dir-only' || arg === '--only-dir' || arg === '--keep-branch' || arg === '--keep-branches') {
80
+ flags.dirOnly = true;
81
+ } else if (arg === '--keep-remote' || arg === '--local-only') {
82
+ flags.keepRemote = true;
83
+ } else if (arg === '--force' || arg === '-f') {
84
+ flags.force = true;
85
+ } else if (arg === '--branch' || arg === '-b') {
86
+ flags.branch = argv[i + 1];
87
+ i++;
88
+ } else if (arg === '--dirname' || arg === '-d') {
89
+ flags.dirname = argv[i + 1];
90
+ i++;
91
+ } else if (arg === '--workspace' || arg === '--workspace-name' || arg === '-w' || arg === '--name') {
92
+ flags.workspaceName = argv[i + 1];
93
+ i++;
94
+ } else if (arg === '--source' || arg === '--base' || arg === '-s') {
95
+ flags.source = argv[i + 1];
96
+ i++;
97
+ } else if (arg === '--preset' || arg === '-p') {
98
+ flags.presetName = argv[i + 1];
99
+ i++;
100
+ } else if (arg === '--focus') {
101
+ flags.focusTarget = argv[i + 1];
102
+ i++;
103
+ } else if (!arg.startsWith('-')) {
104
+ if (flags.isCleanup && !flags.cleanupTarget) {
105
+ flags.cleanupTarget = arg;
106
+ } else if (!flags.branch) {
107
+ flags.branch = arg;
108
+ }
109
+ }
110
+ }
111
+
112
+ return flags;
113
+ }
114
+
115
+ module.exports = {
116
+ parseArgs,
117
+ showUsage,
118
+ };
package/lib/config.js ADDED
@@ -0,0 +1,134 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { getPreset, detectPreset } = require('../presets');
5
+ const git = require('./git');
6
+
7
+ function findConfigFile(searchDirs = []) {
8
+ const configNames = [
9
+ '.ariserc.js',
10
+ 'arise.config.js',
11
+ '.ariserc.json',
12
+ '.ariserc',
13
+ '.worktreerc.js',
14
+ 'worktree.config.js',
15
+ '.worktreerc.json',
16
+ '.worktreerc',
17
+ ];
18
+
19
+ for (const dir of searchDirs) {
20
+ if (!dir || !fs.existsSync(dir)) continue;
21
+ for (const name of configNames) {
22
+ const fullPath = path.join(dir, name);
23
+ if (fs.existsSync(fullPath)) {
24
+ return fullPath;
25
+ }
26
+ }
27
+ }
28
+
29
+ // Check home directory ~/.config/arise then ~/.config/herdr-worktree
30
+ const homeConfigDirs = [
31
+ path.join(os.homedir(), '.config', 'arise'),
32
+ path.join(os.homedir(), '.config', 'herdr-worktree'),
33
+ ];
34
+ for (const homeDir of homeConfigDirs) {
35
+ for (const name of configNames) {
36
+ const fullPath = path.join(homeDir, name);
37
+ if (fs.existsSync(fullPath)) {
38
+ return fullPath;
39
+ }
40
+ }
41
+ }
42
+
43
+ const homeRcFiles = [
44
+ path.join(os.homedir(), '.ariserc.json'),
45
+ path.join(os.homedir(), '.worktreerc.json'),
46
+ ];
47
+ for (const rcFile of homeRcFiles) {
48
+ if (fs.existsSync(rcFile)) {
49
+ return rcFile;
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ function loadConfigFile(filePath) {
57
+ if (!filePath) return {};
58
+ try {
59
+ if (filePath.endsWith('.json') || filePath.endsWith('.worktreerc') || filePath.endsWith('.ariserc')) {
60
+ const content = fs.readFileSync(filePath, 'utf8');
61
+ return JSON.parse(content);
62
+ } else {
63
+ return require(path.resolve(filePath));
64
+ }
65
+ } catch (err) {
66
+ console.warn(`Warning: Failed to load config from "${filePath}": ${err.message}`);
67
+ return {};
68
+ }
69
+ }
70
+
71
+ function resolveConfiguration(flags = {}, cwd = process.cwd()) {
72
+ const repoRoot = git.getRepoRootDir(cwd);
73
+ const configFile = findConfigFile([cwd, repoRoot]);
74
+ const fileConfig = loadConfigFile(configFile);
75
+
76
+ // 1. Resolve Preset
77
+ let presetName = flags.presetName || fileConfig.preset;
78
+ let preset = null;
79
+ if (presetName) {
80
+ preset = getPreset(presetName);
81
+ }
82
+ if (!preset) {
83
+ preset = detectPreset(cwd);
84
+ }
85
+
86
+ // 2. Merge Repo Settings
87
+ const repoConfig = {
88
+ bareRepo: (fileConfig.repo && fileConfig.repo.bareRepo) || (preset.repo && preset.repo.bareRepo) || null,
89
+ worktreesBase: (fileConfig.repo && fileConfig.repo.worktreesBase) || (preset.repo && preset.repo.worktreesBase) || null,
90
+ defaultBaseBranch: (fileConfig.repo && fileConfig.repo.defaultBaseBranch) || (preset.repo && preset.repo.defaultBaseBranch) || 'develop',
91
+ protectedBranches: (fileConfig.repo && fileConfig.repo.protectedBranches) || (preset.repo && preset.repo.protectedBranches) || ['main', 'master', 'develop', 'prod', 'staging'],
92
+ };
93
+
94
+ // 3. Merge Workspace Settings
95
+ const workspaceConfig = {
96
+ labelPrefix: (fileConfig.workspace && fileConfig.workspace.labelPrefix !== undefined)
97
+ ? fileConfig.workspace.labelPrefix
98
+ : (preset.workspace && preset.workspace.labelPrefix !== undefined ? preset.workspace.labelPrefix : ''),
99
+ defaultFocus: (fileConfig.workspace && fileConfig.workspace.defaultFocus)
100
+ || (preset.workspace && preset.workspace.defaultFocus)
101
+ || 'agy',
102
+ };
103
+
104
+ // 4. Merge Layout
105
+ const layout = fileConfig.layout || preset.layout || [];
106
+
107
+ // 5. Merge Scaffolding Settings
108
+ const scaffoldConfig = {
109
+ ...(preset.scaffold || {}),
110
+ ...(fileConfig.scaffold || {}),
111
+ };
112
+
113
+ // 6. Merge Hooks
114
+ const hooks = {
115
+ ...(preset.hooks || {}),
116
+ ...(fileConfig.hooks || {}),
117
+ };
118
+
119
+ return {
120
+ preset,
121
+ repo: repoConfig,
122
+ workspace: workspaceConfig,
123
+ layout,
124
+ scaffold: scaffoldConfig,
125
+ hooks,
126
+ configFile,
127
+ };
128
+ }
129
+
130
+ module.exports = {
131
+ findConfigFile,
132
+ loadConfigFile,
133
+ resolveConfiguration,
134
+ };
package/lib/context.js ADDED
@@ -0,0 +1,113 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync, spawnSync } = require('child_process');
4
+
5
+ function createContext({
6
+ worktreePath,
7
+ repoRoot,
8
+ bareRepo,
9
+ branch,
10
+ source,
11
+ flags = {},
12
+ preset = {},
13
+ config = {},
14
+ }) {
15
+ const ctx = {
16
+ worktreePath,
17
+ repoRoot,
18
+ bareRepo,
19
+ branch,
20
+ source,
21
+ flags,
22
+ preset,
23
+ config,
24
+
25
+ log(msg) {
26
+ console.log(msg);
27
+ },
28
+
29
+ warn(msg) {
30
+ console.warn(msg);
31
+ },
32
+
33
+ error(msg) {
34
+ console.error(msg);
35
+ },
36
+
37
+ exec(command, options = {}) {
38
+ const cwd = options.cwd || worktreePath;
39
+ const shell = options.shell || '/bin/bash';
40
+ return execSync(command, {
41
+ cwd,
42
+ stdio: 'inherit',
43
+ shell,
44
+ ...options,
45
+ });
46
+ },
47
+
48
+ spawn(command, args = [], options = {}) {
49
+ const cwd = options.cwd || worktreePath;
50
+ const shell = options.shell !== undefined ? options.shell : true;
51
+ return spawnSync(command, args, {
52
+ cwd,
53
+ stdio: 'inherit',
54
+ shell,
55
+ ...options,
56
+ });
57
+ },
58
+
59
+ copyFile(src, dst) {
60
+ const resolvedDst = path.isAbsolute(dst) ? dst : path.join(worktreePath, dst);
61
+ if (fs.existsSync(src)) {
62
+ console.log(`Copying from "${src}" to "${resolvedDst}"...`);
63
+ const dstDir = path.dirname(resolvedDst);
64
+ if (!fs.existsSync(dstDir)) {
65
+ fs.mkdirSync(dstDir, { recursive: true });
66
+ }
67
+ fs.copyFileSync(src, resolvedDst);
68
+ return true;
69
+ } else {
70
+ console.warn(`Source file "${src}" does not exist to copy.`);
71
+ return false;
72
+ }
73
+ },
74
+
75
+ copyFromRoot(relativeSrc, relativeDst = relativeSrc) {
76
+ if (!repoRoot) {
77
+ console.warn(`Cannot copy from root: no repo root identified.`);
78
+ return false;
79
+ }
80
+ const src = path.join(repoRoot, relativeSrc);
81
+ const dst = path.join(worktreePath, relativeDst);
82
+ return ctx.copyFile(src, dst);
83
+ },
84
+
85
+ setSymlink(symlinkPath, target = worktreePath) {
86
+ console.log(`==> Updating symlink "${symlinkPath}" -> "${target}"...`);
87
+ try {
88
+ if (fs.existsSync(symlinkPath) || fs.lstatSync(symlinkPath).isSymbolicLink()) {
89
+ const lstat = fs.lstatSync(symlinkPath);
90
+ if (lstat.isSymbolicLink()) {
91
+ fs.unlinkSync(symlinkPath);
92
+ } else {
93
+ const backupPath = `${symlinkPath}_BAK_${Date.now()}`;
94
+ console.warn(`==> Warning: "${symlinkPath}" is a directory, not a symlink. Backing up to "${backupPath}"`);
95
+ fs.renameSync(symlinkPath, backupPath);
96
+ }
97
+ }
98
+ fs.symlinkSync(target, symlinkPath);
99
+ console.log(`==> Symlink updated: ${symlinkPath} -> ${target}`);
100
+ return true;
101
+ } catch (err) {
102
+ console.error(`==> Failed to update symlink: ${err.message}`);
103
+ return false;
104
+ }
105
+ },
106
+ };
107
+
108
+ return ctx;
109
+ }
110
+
111
+ module.exports = {
112
+ createContext,
113
+ };