@dleangen/cage-git 0.0.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/README.md ADDED
@@ -0,0 +1,360 @@
1
+ # @dleangen/cage-git
2
+
3
+ Git workflow mechanics for CAGE projects.
4
+
5
+ <!-- commands -->
6
+ * [`cage-git help [COMMAND]`](#cage-git-help-command)
7
+ * [`cage-git land`](#cage-git-land)
8
+ * [`cage-git plugins`](#cage-git-plugins)
9
+ * [`cage-git plugins add PLUGIN`](#cage-git-plugins-add-plugin)
10
+ * [`cage-git plugins:inspect PLUGIN...`](#cage-git-pluginsinspect-plugin)
11
+ * [`cage-git plugins install PLUGIN`](#cage-git-plugins-install-plugin)
12
+ * [`cage-git plugins link PATH`](#cage-git-plugins-link-path)
13
+ * [`cage-git plugins remove [PLUGIN]`](#cage-git-plugins-remove-plugin)
14
+ * [`cage-git plugins reset`](#cage-git-plugins-reset)
15
+ * [`cage-git plugins uninstall [PLUGIN]`](#cage-git-plugins-uninstall-plugin)
16
+ * [`cage-git plugins unlink [PLUGIN]`](#cage-git-plugins-unlink-plugin)
17
+ * [`cage-git plugins update`](#cage-git-plugins-update)
18
+
19
+ ## `cage-git help [COMMAND]`
20
+
21
+ Display help for cage-git.
22
+
23
+ ```
24
+ USAGE
25
+ $ cage-git help [COMMAND...] [-n]
26
+
27
+ ARGUMENTS
28
+ [COMMAND...] Command to show help for.
29
+
30
+ FLAGS
31
+ -n, --nested-commands Include all nested commands in the output.
32
+
33
+ DESCRIPTION
34
+ Display help for cage-git.
35
+ ```
36
+
37
+ _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.50/src/commands/help.ts)_
38
+
39
+ ## `cage-git land`
40
+
41
+ Safe, inspect-before-merge branch landing
42
+
43
+ ```
44
+ USAGE
45
+ $ cage-git land --from <value> --into <value> [--accept | --analyze | --preview | --reject | --cleanup]
46
+ [--force]
47
+
48
+ FLAGS
49
+ --accept Merge landing branch into target
50
+ --analyze Preflight analysis (read-only)
51
+ --cleanup Delete backup branch if present
52
+ --force Bypass pushed-branch guard (--preview only)
53
+ --from=<value> (required) Branch to land
54
+ --into=<value> (required) Target branch to land into
55
+ --preview Create landing branch with cherry-picked commits
56
+ --reject Discard landing branch; leave original untouched
57
+
58
+ DESCRIPTION
59
+ Safe, inspect-before-merge branch landing
60
+
61
+ EXAMPLES
62
+ $ cage-git land --from feat/foo --into main --analyze
63
+
64
+ $ cage-git land --from feat/foo --into main --preview
65
+
66
+ $ cage-git land --from feat/foo --into main --accept
67
+ ```
68
+
69
+ _See code: [src/commands/land.ts](https://github.com/dleangen/cage-git/blob/v0.0.1/src/commands/land.ts)_
70
+
71
+ ## `cage-git plugins`
72
+
73
+ List installed plugins.
74
+
75
+ ```
76
+ USAGE
77
+ $ cage-git plugins [--json] [--core]
78
+
79
+ FLAGS
80
+ --core Show core plugins.
81
+
82
+ GLOBAL FLAGS
83
+ --json Format output as json.
84
+
85
+ DESCRIPTION
86
+ List installed plugins.
87
+
88
+ EXAMPLES
89
+ $ cage-git plugins
90
+ ```
91
+
92
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/index.ts)_
93
+
94
+ ## `cage-git plugins add PLUGIN`
95
+
96
+ Installs a plugin into cage-git.
97
+
98
+ ```
99
+ USAGE
100
+ $ cage-git plugins add PLUGIN... [--json] [-f] [-h] [-s | -v]
101
+
102
+ ARGUMENTS
103
+ PLUGIN... Plugin to install.
104
+
105
+ FLAGS
106
+ -f, --force Force npm to fetch remote resources even if a local copy exists on disk.
107
+ -h, --help Show CLI help.
108
+ -s, --silent Silences npm output.
109
+ -v, --verbose Show verbose npm output.
110
+
111
+ GLOBAL FLAGS
112
+ --json Format output as json.
113
+
114
+ DESCRIPTION
115
+ Installs a plugin into cage-git.
116
+
117
+ Uses npm to install plugins.
118
+
119
+ Installation of a user-installed plugin will override a core plugin.
120
+
121
+ Use the CAGE_GIT_NPM_LOG_LEVEL environment variable to set the npm loglevel.
122
+ Use the CAGE_GIT_NPM_REGISTRY environment variable to set the npm registry.
123
+
124
+ ALIASES
125
+ $ cage-git plugins add
126
+
127
+ EXAMPLES
128
+ Install a plugin from npm registry.
129
+
130
+ $ cage-git plugins add myplugin
131
+
132
+ Install a plugin from a github url.
133
+
134
+ $ cage-git plugins add https://github.com/someuser/someplugin
135
+
136
+ Install a plugin from a github slug.
137
+
138
+ $ cage-git plugins add someuser/someplugin
139
+ ```
140
+
141
+ ## `cage-git plugins:inspect PLUGIN...`
142
+
143
+ Displays installation properties of a plugin.
144
+
145
+ ```
146
+ USAGE
147
+ $ cage-git plugins inspect PLUGIN...
148
+
149
+ ARGUMENTS
150
+ PLUGIN... [default: .] Plugin to inspect.
151
+
152
+ FLAGS
153
+ -h, --help Show CLI help.
154
+ -v, --verbose
155
+
156
+ GLOBAL FLAGS
157
+ --json Format output as json.
158
+
159
+ DESCRIPTION
160
+ Displays installation properties of a plugin.
161
+
162
+ EXAMPLES
163
+ $ cage-git plugins inspect myplugin
164
+ ```
165
+
166
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/inspect.ts)_
167
+
168
+ ## `cage-git plugins install PLUGIN`
169
+
170
+ Installs a plugin into cage-git.
171
+
172
+ ```
173
+ USAGE
174
+ $ cage-git plugins install PLUGIN... [--json] [-f] [-h] [-s | -v]
175
+
176
+ ARGUMENTS
177
+ PLUGIN... Plugin to install.
178
+
179
+ FLAGS
180
+ -f, --force Force npm to fetch remote resources even if a local copy exists on disk.
181
+ -h, --help Show CLI help.
182
+ -s, --silent Silences npm output.
183
+ -v, --verbose Show verbose npm output.
184
+
185
+ GLOBAL FLAGS
186
+ --json Format output as json.
187
+
188
+ DESCRIPTION
189
+ Installs a plugin into cage-git.
190
+
191
+ Uses npm to install plugins.
192
+
193
+ Installation of a user-installed plugin will override a core plugin.
194
+
195
+ Use the CAGE_GIT_NPM_LOG_LEVEL environment variable to set the npm loglevel.
196
+ Use the CAGE_GIT_NPM_REGISTRY environment variable to set the npm registry.
197
+
198
+ ALIASES
199
+ $ cage-git plugins add
200
+
201
+ EXAMPLES
202
+ Install a plugin from npm registry.
203
+
204
+ $ cage-git plugins install myplugin
205
+
206
+ Install a plugin from a github url.
207
+
208
+ $ cage-git plugins install https://github.com/someuser/someplugin
209
+
210
+ Install a plugin from a github slug.
211
+
212
+ $ cage-git plugins install someuser/someplugin
213
+ ```
214
+
215
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/install.ts)_
216
+
217
+ ## `cage-git plugins link PATH`
218
+
219
+ Links a plugin into the CLI for development.
220
+
221
+ ```
222
+ USAGE
223
+ $ cage-git plugins link PATH [-h] [--install] [-v]
224
+
225
+ ARGUMENTS
226
+ PATH [default: .] path to plugin
227
+
228
+ FLAGS
229
+ -h, --help Show CLI help.
230
+ -v, --verbose
231
+ --[no-]install Install dependencies after linking the plugin.
232
+
233
+ DESCRIPTION
234
+ Links a plugin into the CLI for development.
235
+
236
+ Installation of a linked plugin will override a user-installed or core plugin.
237
+
238
+ e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello'
239
+ command will override the user-installed or core plugin implementation. This is useful for development work.
240
+
241
+
242
+ EXAMPLES
243
+ $ cage-git plugins link myplugin
244
+ ```
245
+
246
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/link.ts)_
247
+
248
+ ## `cage-git plugins remove [PLUGIN]`
249
+
250
+ Removes a plugin from the CLI.
251
+
252
+ ```
253
+ USAGE
254
+ $ cage-git plugins remove [PLUGIN...] [-h] [-v]
255
+
256
+ ARGUMENTS
257
+ [PLUGIN...] plugin to uninstall
258
+
259
+ FLAGS
260
+ -h, --help Show CLI help.
261
+ -v, --verbose
262
+
263
+ DESCRIPTION
264
+ Removes a plugin from the CLI.
265
+
266
+ ALIASES
267
+ $ cage-git plugins unlink
268
+ $ cage-git plugins remove
269
+
270
+ EXAMPLES
271
+ $ cage-git plugins remove myplugin
272
+ ```
273
+
274
+ ## `cage-git plugins reset`
275
+
276
+ Remove all user-installed and linked plugins.
277
+
278
+ ```
279
+ USAGE
280
+ $ cage-git plugins reset [--hard] [--reinstall]
281
+
282
+ FLAGS
283
+ --hard Delete node_modules and package manager related files in addition to uninstalling plugins.
284
+ --reinstall Reinstall all plugins after uninstalling.
285
+ ```
286
+
287
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/reset.ts)_
288
+
289
+ ## `cage-git plugins uninstall [PLUGIN]`
290
+
291
+ Removes a plugin from the CLI.
292
+
293
+ ```
294
+ USAGE
295
+ $ cage-git plugins uninstall [PLUGIN...] [-h] [-v]
296
+
297
+ ARGUMENTS
298
+ [PLUGIN...] plugin to uninstall
299
+
300
+ FLAGS
301
+ -h, --help Show CLI help.
302
+ -v, --verbose
303
+
304
+ DESCRIPTION
305
+ Removes a plugin from the CLI.
306
+
307
+ ALIASES
308
+ $ cage-git plugins unlink
309
+ $ cage-git plugins remove
310
+
311
+ EXAMPLES
312
+ $ cage-git plugins uninstall myplugin
313
+ ```
314
+
315
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/uninstall.ts)_
316
+
317
+ ## `cage-git plugins unlink [PLUGIN]`
318
+
319
+ Removes a plugin from the CLI.
320
+
321
+ ```
322
+ USAGE
323
+ $ cage-git plugins unlink [PLUGIN...] [-h] [-v]
324
+
325
+ ARGUMENTS
326
+ [PLUGIN...] plugin to uninstall
327
+
328
+ FLAGS
329
+ -h, --help Show CLI help.
330
+ -v, --verbose
331
+
332
+ DESCRIPTION
333
+ Removes a plugin from the CLI.
334
+
335
+ ALIASES
336
+ $ cage-git plugins unlink
337
+ $ cage-git plugins remove
338
+
339
+ EXAMPLES
340
+ $ cage-git plugins unlink myplugin
341
+ ```
342
+
343
+ ## `cage-git plugins update`
344
+
345
+ Update installed plugins.
346
+
347
+ ```
348
+ USAGE
349
+ $ cage-git plugins update [-h] [-v]
350
+
351
+ FLAGS
352
+ -h, --help Show CLI help.
353
+ -v, --verbose
354
+
355
+ DESCRIPTION
356
+ Update installed plugins.
357
+ ```
358
+
359
+ _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/update.ts)_
360
+ <!-- commandsstop -->
package/bin/dev.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node_modules/.bin/ts-node
2
+
3
+ // eslint-disable-next-line unicorn/prefer-top-level-await
4
+ ;(async () => {
5
+ const oclif = await import('@oclif/core')
6
+ await oclif.execute({development: true, dir: __dirname})
7
+ })()
package/bin/run.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // eslint-disable-next-line unicorn/prefer-top-level-await
4
+ (async () => {
5
+ const oclif = await import('@oclif/core')
6
+ await oclif.execute({dir: __dirname})
7
+ })()
@@ -0,0 +1,22 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Land extends Command {
3
+ static args: {};
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ accept: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
8
+ analyze: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ cleanup: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ from: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ into: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
13
+ preview: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
14
+ reject: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ private runAccept;
18
+ private runAnalyze;
19
+ private runCleanup;
20
+ private runPreview;
21
+ private runReject;
22
+ }
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const core_1 = require("@oclif/core");
40
+ const node_child_process_1 = require("node:child_process");
41
+ const fs = __importStar(require("node:fs"));
42
+ const node_path_1 = __importDefault(require("node:path"));
43
+ function git(args, cwd = process.cwd()) {
44
+ return (0, node_child_process_1.execSync)(`git ${args}`, { cwd, encoding: 'utf8' }).trim();
45
+ }
46
+ function gitSafe(args, cwd = process.cwd()) {
47
+ try {
48
+ return git(args, cwd);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function isDirty(cwd = process.cwd()) {
55
+ return git('status --porcelain --untracked-files=no', cwd) !== '';
56
+ }
57
+ function branchExists(branch, cwd = process.cwd()) {
58
+ return gitSafe(`rev-parse --verify ${branch}`, cwd) !== null;
59
+ }
60
+ function mergeBase(from, into, cwd = process.cwd()) {
61
+ return git(`merge-base ${from} ${into}`, cwd);
62
+ }
63
+ function anchorDir(cwd = process.cwd()) {
64
+ return node_path_1.default.join(cwd, '.cage', 'land');
65
+ }
66
+ function anchorPath(from, cwd = process.cwd()) {
67
+ return node_path_1.default.join(anchorDir(cwd), `${from}.preview-base`);
68
+ }
69
+ function ensureAnchorDir(cwd = process.cwd()) {
70
+ fs.mkdirSync(anchorDir(cwd), { recursive: true });
71
+ }
72
+ class Land extends core_1.Command {
73
+ static args = {};
74
+ static description = 'Safe, inspect-before-merge branch landing';
75
+ static examples = [
76
+ '<%= config.bin %> <%= command.id %> --from feat/foo --into main --analyze',
77
+ '<%= config.bin %> <%= command.id %> --from feat/foo --into main --preview',
78
+ '<%= config.bin %> <%= command.id %> --from feat/foo --into main --accept',
79
+ ];
80
+ static flags = {
81
+ accept: core_1.Flags.boolean({ description: 'Merge landing branch into target', exclusive: ['analyze', 'preview', 'reject', 'cleanup'] }),
82
+ analyze: core_1.Flags.boolean({ description: 'Preflight analysis (read-only)', exclusive: ['preview', 'accept', 'reject', 'cleanup'] }),
83
+ cleanup: core_1.Flags.boolean({ description: 'Delete backup branch if present', exclusive: ['analyze', 'preview', 'accept', 'reject'] }),
84
+ force: core_1.Flags.boolean({ description: 'Bypass pushed-branch guard (--preview only)' }),
85
+ from: core_1.Flags.string({ description: 'Branch to land', required: true }),
86
+ into: core_1.Flags.string({ description: 'Target branch to land into', required: true }),
87
+ preview: core_1.Flags.boolean({ description: 'Create landing branch with cherry-picked commits', exclusive: ['analyze', 'accept', 'reject', 'cleanup'] }),
88
+ reject: core_1.Flags.boolean({ description: 'Discard landing branch; leave original untouched', exclusive: ['analyze', 'preview', 'accept', 'cleanup'] }),
89
+ };
90
+ async run() {
91
+ const { flags } = await this.parse(Land);
92
+ const { force, from, into } = flags;
93
+ const modeCount = [flags.analyze, flags.preview, flags.accept, flags.reject, flags.cleanup].filter(Boolean).length;
94
+ if (modeCount === 0) {
95
+ this.error('Exactly one mode is required: --analyze, --preview, --accept, --reject, or --cleanup');
96
+ }
97
+ const cwd = process.cwd();
98
+ if (flags.analyze)
99
+ return this.runAnalyze(from, into, cwd);
100
+ if (flags.preview)
101
+ return this.runPreview(from, into, force, cwd);
102
+ if (flags.accept)
103
+ return this.runAccept(from, into, cwd);
104
+ if (flags.reject)
105
+ return this.runReject(from, cwd);
106
+ if (flags.cleanup)
107
+ return this.runCleanup(from, cwd);
108
+ }
109
+ runAccept(from, into, cwd) {
110
+ // Dirty-tree gate
111
+ if (isDirty(cwd)) {
112
+ this.error('Working tree is dirty. Commit or stash changes before running --accept.');
113
+ }
114
+ const landingBranch = `${from}-landing`;
115
+ // Landing branch must exist
116
+ if (!branchExists(landingBranch, cwd)) {
117
+ this.error(`Landing branch '${landingBranch}' does not exist. Run --preview first.`);
118
+ }
119
+ // Drift guard
120
+ const anchor = anchorPath(from, cwd);
121
+ if (fs.existsSync(anchor)) {
122
+ const previewBase = fs.readFileSync(anchor, 'utf8').trim();
123
+ const currentTip = git(`rev-parse ${into}`, cwd);
124
+ if (previewBase !== currentTip) {
125
+ this.error(`Target '${into}' has moved since --preview. Run --reject then --preview again.`);
126
+ }
127
+ }
128
+ // Merge
129
+ git(`checkout ${into}`, cwd);
130
+ git(`merge --no-ff ${landingBranch} -m "Merge branch '${from}' into ${into}"`, cwd);
131
+ // Clean up
132
+ git(`branch -D ${landingBranch}`, cwd);
133
+ if (fs.existsSync(anchor))
134
+ fs.unlinkSync(anchor);
135
+ const tip = git(`rev-parse --short HEAD`, cwd);
136
+ this.log(`Branch '${from}' landed into '${into}'. Tip: ${tip}`);
137
+ }
138
+ runAnalyze(from, into, cwd) {
139
+ const base = mergeBase(from, into, cwd);
140
+ // Commits to land
141
+ const commits = gitSafe(`log --oneline ${into}..${from}`, cwd) ?? '';
142
+ this.log('\nCommits to land:');
143
+ this.log(commits || ' (none)');
144
+ // Drift distance
145
+ const driftCount = Number(git(`rev-list --count ${base}..${into}`, cwd));
146
+ this.log(`\nDrift distance: ${driftCount} commit(s) on ${into} since fork`);
147
+ if (driftCount > 0) {
148
+ this.log(` Warning: ${into} has advanced ${driftCount} commit(s) since the fork point.`);
149
+ }
150
+ // File overlap
151
+ const targetFiles = new Set(git(`diff --name-only ${base} ${into}`, cwd).split('\n').filter(Boolean));
152
+ const branchFiles = new Set(git(`diff --name-only ${base} ${from}`, cwd).split('\n').filter(Boolean));
153
+ const overlap = [...targetFiles].filter((f) => branchFiles.has(f));
154
+ if (overlap.length > 0) {
155
+ this.log('\nFile overlap (potential conflicts):');
156
+ for (const f of overlap)
157
+ this.log(` ${f}`);
158
+ }
159
+ else {
160
+ this.log('\nNo file overlap detected.');
161
+ }
162
+ // Remote status
163
+ const fromRemote = gitSafe(`rev-parse --verify origin/${from}`, cwd) !== null;
164
+ const intoRemote = gitSafe(`rev-parse --verify origin/${into}`, cwd) !== null;
165
+ this.log(`\nRemote tracking: ${from}=${fromRemote ? 'yes' : 'no'}, ${into}=${intoRemote ? 'yes' : 'no'}`);
166
+ }
167
+ runCleanup(from, cwd) {
168
+ const backupBranch = `${from}-backup`;
169
+ let cleaned = false;
170
+ if (branchExists(backupBranch, cwd)) {
171
+ git(`branch -D ${backupBranch}`, cwd);
172
+ this.log(`Deleted backup branch '${backupBranch}'.`);
173
+ cleaned = true;
174
+ }
175
+ const anchor = anchorPath(from, cwd);
176
+ if (fs.existsSync(anchor)) {
177
+ fs.unlinkSync(anchor);
178
+ this.log(`Removed stale drift anchor for '${from}'.`);
179
+ cleaned = true;
180
+ }
181
+ if (!cleaned) {
182
+ this.log('Nothing to clean up.');
183
+ }
184
+ }
185
+ runPreview(from, into, force, cwd) {
186
+ // Dirty-tree gate
187
+ if (isDirty(cwd)) {
188
+ this.error('Working tree is dirty. Commit or stash changes before running --preview.');
189
+ }
190
+ // Pushed-branch guard
191
+ if (!force && gitSafe(`rev-parse --verify origin/${from}`, cwd) !== null) {
192
+ this.error(`Branch '${from}' has a remote-tracking ref (origin/${from}). ` +
193
+ `Use --force to bypass this guard.`);
194
+ }
195
+ const landingBranch = `${from}-landing`;
196
+ // Abort if landing branch already exists
197
+ if (branchExists(landingBranch, cwd)) {
198
+ this.error(`Landing branch '${landingBranch}' already exists. Run --reject first.`);
199
+ }
200
+ const base = mergeBase(from, into, cwd);
201
+ // Record drift anchor
202
+ ensureAnchorDir(cwd);
203
+ const gitignorePath = node_path_1.default.join(cwd, '.gitignore');
204
+ const gitignored = fs.existsSync(gitignorePath) &&
205
+ fs.readFileSync(gitignorePath, 'utf8').split('\n').some((line) => line.trim() === '.cage' || line.trim() === '.cage/');
206
+ if (!gitignored) {
207
+ this.warn('`.cage/` is not in .gitignore — drift anchors may be accidentally committed.');
208
+ }
209
+ const targetTip = git(`rev-parse ${into}`, cwd);
210
+ fs.writeFileSync(anchorPath(from, cwd), targetTip);
211
+ // Create landing branch
212
+ git(`checkout -b ${landingBranch} ${into}`, cwd);
213
+ // Cherry-pick
214
+ try {
215
+ git(`cherry-pick ${base}..${from}`, cwd);
216
+ }
217
+ catch {
218
+ this.log(`Cherry-pick conflict on '${landingBranch}'.\n` +
219
+ `Resolve conflicts, then:\n` +
220
+ ` git cherry-pick --continue\n` +
221
+ `Or abort with:\n` +
222
+ ` git cherry-pick --abort && cage land --from ${from} --into ${into} --reject`);
223
+ this.exit(1);
224
+ }
225
+ // Return to target branch — leaves repo in a predictable state for --reject/--accept
226
+ git(`checkout ${into}`, cwd);
227
+ this.log(`Landing branch '${landingBranch}' created.`);
228
+ this.log(`Inspect with: git diff ${into}..${landingBranch}`);
229
+ }
230
+ runReject(from, cwd) {
231
+ const landingBranch = `${from}-landing`;
232
+ let cleaned = false;
233
+ if (branchExists(landingBranch, cwd)) {
234
+ git(`branch -D ${landingBranch}`, cwd);
235
+ cleaned = true;
236
+ }
237
+ const anchor = anchorPath(from, cwd);
238
+ if (fs.existsSync(anchor)) {
239
+ fs.unlinkSync(anchor);
240
+ cleaned = true;
241
+ }
242
+ if (cleaned) {
243
+ this.log(`Landing branch '${landingBranch}' discarded. '${from}' is untouched.`);
244
+ }
245
+ else {
246
+ this.log(`Nothing to reject — '${landingBranch}' did not exist.`);
247
+ }
248
+ }
249
+ }
250
+ exports.default = Land;
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ var core_1 = require("@oclif/core");
4
+ Object.defineProperty(exports, "run", { enumerable: true, get: function () { return core_1.run; } });
@@ -0,0 +1,113 @@
1
+ {
2
+ "commands": {
3
+ "land": {
4
+ "aliases": [],
5
+ "args": {},
6
+ "description": "Safe, inspect-before-merge branch landing",
7
+ "examples": [
8
+ "<%= config.bin %> <%= command.id %> --from feat/foo --into main --analyze",
9
+ "<%= config.bin %> <%= command.id %> --from feat/foo --into main --preview",
10
+ "<%= config.bin %> <%= command.id %> --from feat/foo --into main --accept"
11
+ ],
12
+ "flags": {
13
+ "accept": {
14
+ "description": "Merge landing branch into target",
15
+ "exclusive": [
16
+ "analyze",
17
+ "preview",
18
+ "reject",
19
+ "cleanup"
20
+ ],
21
+ "name": "accept",
22
+ "allowNo": false,
23
+ "type": "boolean"
24
+ },
25
+ "analyze": {
26
+ "description": "Preflight analysis (read-only)",
27
+ "exclusive": [
28
+ "preview",
29
+ "accept",
30
+ "reject",
31
+ "cleanup"
32
+ ],
33
+ "name": "analyze",
34
+ "allowNo": false,
35
+ "type": "boolean"
36
+ },
37
+ "cleanup": {
38
+ "description": "Delete backup branch if present",
39
+ "exclusive": [
40
+ "analyze",
41
+ "preview",
42
+ "accept",
43
+ "reject"
44
+ ],
45
+ "name": "cleanup",
46
+ "allowNo": false,
47
+ "type": "boolean"
48
+ },
49
+ "force": {
50
+ "description": "Bypass pushed-branch guard (--preview only)",
51
+ "name": "force",
52
+ "allowNo": false,
53
+ "type": "boolean"
54
+ },
55
+ "from": {
56
+ "description": "Branch to land",
57
+ "name": "from",
58
+ "required": true,
59
+ "hasDynamicHelp": false,
60
+ "multiple": false,
61
+ "type": "option"
62
+ },
63
+ "into": {
64
+ "description": "Target branch to land into",
65
+ "name": "into",
66
+ "required": true,
67
+ "hasDynamicHelp": false,
68
+ "multiple": false,
69
+ "type": "option"
70
+ },
71
+ "preview": {
72
+ "description": "Create landing branch with cherry-picked commits",
73
+ "exclusive": [
74
+ "analyze",
75
+ "accept",
76
+ "reject",
77
+ "cleanup"
78
+ ],
79
+ "name": "preview",
80
+ "allowNo": false,
81
+ "type": "boolean"
82
+ },
83
+ "reject": {
84
+ "description": "Discard landing branch; leave original untouched",
85
+ "exclusive": [
86
+ "analyze",
87
+ "preview",
88
+ "accept",
89
+ "cleanup"
90
+ ],
91
+ "name": "reject",
92
+ "allowNo": false,
93
+ "type": "boolean"
94
+ }
95
+ },
96
+ "hasDynamicHelp": false,
97
+ "hiddenAliases": [],
98
+ "id": "land",
99
+ "pluginAlias": "@dleangen/cage-git",
100
+ "pluginName": "@dleangen/cage-git",
101
+ "pluginType": "core",
102
+ "strict": true,
103
+ "enableJsonFlag": false,
104
+ "isESM": false,
105
+ "relativePath": [
106
+ "dist",
107
+ "commands",
108
+ "land.js"
109
+ ]
110
+ }
111
+ },
112
+ "version": "0.0.1"
113
+ }
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@dleangen/cage-git",
3
+ "description": "Git workflow mechanics for CAGE projects.",
4
+ "version": "0.0.1",
5
+ "author": "dleangen",
6
+ "bin": {
7
+ "cage-git": "bin/run.js"
8
+ },
9
+ "bugs": "https://github.com/dleangen/cage-git/issues",
10
+ "dependencies": {
11
+ "@oclif/core": "^4",
12
+ "@oclif/plugin-help": "^6",
13
+ "@oclif/plugin-plugins": "^5"
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/compat": "^1",
17
+ "@oclif/prettier-config": "^0.2.1",
18
+ "@oclif/test": "^4",
19
+ "@types/chai": "^4",
20
+ "@types/mocha": "^10",
21
+ "@types/node": "^18",
22
+ "chai": "^4",
23
+ "eslint": "^9",
24
+ "eslint-config-oclif": "^6",
25
+ "eslint-config-prettier": "^10",
26
+ "mocha": "^11",
27
+ "oclif": "^4",
28
+ "shx": "^0.3.3",
29
+ "ts-node": "^10",
30
+ "typescript": "^5"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "files": [
36
+ "./bin",
37
+ "./dist",
38
+ "./oclif.manifest.json"
39
+ ],
40
+ "homepage": "https://github.com/dleangen/cage-git",
41
+ "keywords": [
42
+ "oclif",
43
+ "cage",
44
+ "git"
45
+ ],
46
+ "license": "MIT",
47
+ "main": "dist/index.js",
48
+ "oclif": {
49
+ "bin": "cage-git",
50
+ "dirname": "cage-git",
51
+ "commands": "./dist/commands",
52
+ "plugins": [
53
+ "@oclif/plugin-help",
54
+ "@oclif/plugin-plugins"
55
+ ],
56
+ "topicSeparator": " "
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/dleangen/cage-git.git"
61
+ },
62
+ "scripts": {
63
+ "build": "shx rm -rf dist && tsc -b",
64
+ "lint": "eslint",
65
+ "postpack": "shx rm -f oclif.manifest.json",
66
+ "posttest": "npm run lint",
67
+ "prepack": "oclif manifest && oclif readme",
68
+ "test": "mocha --forbid-only \"test/**/*.test.ts\"",
69
+ "version": "oclif readme && git add README.md"
70
+ },
71
+ "types": "dist/index.d.ts"
72
+ }