pan-wizard 3.4.1 → 3.5.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.
@@ -0,0 +1,407 @@
1
+ 'use strict';
2
+
3
+ const { output, error, isGitRepo, execGit, loadConfig } = require('./core.cjs');
4
+ const { runCommitSafetyChecks, VALID_COMMIT_TYPES } = require('./commands.cjs');
5
+
6
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
7
+
8
+ function getCurrentBranch(cwd) {
9
+ const r = execGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
10
+ return r.exitCode === 0 ? r.stdout : null;
11
+ }
12
+
13
+ function getBranchList(cwd, remote) {
14
+ const args = remote
15
+ ? ['branch', '-r', '--format=%(refname:short)']
16
+ : ['branch', '--format=%(refname:short)'];
17
+ const r = execGit(cwd, args);
18
+ if (r.exitCode !== 0) return [];
19
+ return r.stdout.split('\n').map(s => s.trim()).filter(Boolean);
20
+ }
21
+
22
+ function getTagList(cwd, pattern) {
23
+ const args = pattern ? ['tag', '-l', pattern] : ['tag', '-l'];
24
+ const r = execGit(cwd, args);
25
+ if (r.exitCode !== 0) return [];
26
+ return r.stdout.split('\n').map(s => s.trim()).filter(Boolean);
27
+ }
28
+
29
+ function _notGitRepo(raw) {
30
+ output({ error: 'not_a_git_repo', hint: 'Run git init to initialize a repository' }, raw, 'not a git repo');
31
+ }
32
+
33
+ function _noRemote(remote, raw) {
34
+ output({ error: 'no_remote', remote, hint: 'Add a remote with: git remote add origin <url>' }, raw, 'no remote');
35
+ }
36
+
37
+ // ─── Subcommands ─────────────────────────────────────────────────────────────
38
+
39
+ function cmdGitCommit(cwd, opts, raw) {
40
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
41
+ const { type, message, all, amend, force, files } = opts || {};
42
+ if (type && !VALID_COMMIT_TYPES.includes(type)) {
43
+ error('Invalid commit type: ' + type + '. Valid: ' + VALID_COMMIT_TYPES.join(', '));
44
+ }
45
+ if (!message && !amend) { error('--message required (or use --amend)'); }
46
+
47
+ if (all) execGit(cwd, ['add', '.']);
48
+ else if (files && files.length > 0) {
49
+ for (const f of files) execGit(cwd, ['add', f]);
50
+ }
51
+
52
+ const config = loadConfig(cwd);
53
+ const safety = runCommitSafetyChecks(cwd, config, force);
54
+ if (safety.blocked) {
55
+ output({ committed: false, reason: safety.reason, safety_checks: safety.safetyChecks, hint: safety.hint }, raw, 'blocked');
56
+ return;
57
+ }
58
+
59
+ const finalMessage = type ? type + ': ' + message : message;
60
+ const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', finalMessage];
61
+ const r = execGit(cwd, commitArgs);
62
+ if (r.exitCode !== 0) {
63
+ if (r.stdout.includes('nothing to commit') || r.stderr.includes('nothing to commit')) {
64
+ output({ committed: false, reason: 'nothing_to_commit' }, raw, 'nothing to commit');
65
+ return;
66
+ }
67
+ output({ committed: false, reason: 'commit_failed', detail: r.stderr }, raw, 'commit failed');
68
+ return;
69
+ }
70
+ const hash = execGit(cwd, ['rev-parse', '--short', 'HEAD']).stdout || null;
71
+ output({ committed: true, hash, type: type || null, safety_checks: safety.safetyChecks }, raw, hash);
72
+ }
73
+
74
+ function cmdGitBranch(cwd, sub, opts, raw) {
75
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
76
+ const { name, phase, force } = opts || {};
77
+
78
+ if (sub === 'current') {
79
+ const branch = getCurrentBranch(cwd);
80
+ output({ branch }, raw, branch || 'unknown');
81
+ return;
82
+ }
83
+ if (sub === 'list') {
84
+ const branches = getBranchList(cwd, false);
85
+ output({ branches, count: branches.length }, raw, branches.join('\n'));
86
+ return;
87
+ }
88
+ if (sub === 'create') {
89
+ const branchName = name || (phase ? 'pan/phase-' + phase : null);
90
+ if (!branchName) { error('--name or --phase required for branch create'); }
91
+ const r = execGit(cwd, ['checkout', '-b', branchName]);
92
+ if (r.exitCode !== 0) {
93
+ output({ created: false, branch: branchName, detail: r.stderr }, raw, 'failed');
94
+ return;
95
+ }
96
+ output({ created: true, branch: branchName }, raw, branchName);
97
+ return;
98
+ }
99
+ if (sub === 'switch') {
100
+ if (!name) { error('--name required for branch switch'); }
101
+ const r = execGit(cwd, ['checkout', name]);
102
+ if (r.exitCode !== 0) {
103
+ output({ switched: false, branch: name, detail: r.stderr }, raw, 'failed');
104
+ return;
105
+ }
106
+ output({ switched: true, branch: name }, raw, name);
107
+ return;
108
+ }
109
+ if (sub === 'delete') {
110
+ if (!name) { error('--name required for branch delete'); }
111
+ const flag = force ? '-D' : '-d';
112
+ const r = execGit(cwd, ['branch', flag, name]);
113
+ if (r.exitCode !== 0) {
114
+ output({ deleted: false, branch: name, detail: r.stderr, hint: force ? null : 'Use --force to delete unmerged branches' }, raw, 'failed');
115
+ return;
116
+ }
117
+ output({ deleted: true, branch: name }, raw, name);
118
+ return;
119
+ }
120
+ error('Unknown branch subcommand. Available: create, switch, list, delete, current');
121
+ }
122
+
123
+ function cmdGitPush(cwd, opts, raw) {
124
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
125
+ const remote = (opts && opts.remote) || 'origin';
126
+ const branch = (opts && opts.branch) || getCurrentBranch(cwd);
127
+ const force = opts && opts.force;
128
+
129
+ const remotes = execGit(cwd, ['remote']).stdout.split('\n').map(s => s.trim()).filter(Boolean);
130
+ if (!remotes.includes(remote)) { _noRemote(remote, raw); return; }
131
+
132
+ const pushArgs = ['push', remote, branch];
133
+ if (force) pushArgs.splice(1, 0, '--force');
134
+
135
+ const r = execGit(cwd, pushArgs);
136
+ if (r.exitCode !== 0) {
137
+ output({ pushed: false, remote, branch, detail: r.stderr }, raw, 'push failed');
138
+ return;
139
+ }
140
+ output({ pushed: true, remote, branch, force: !!force }, raw, remote + '/' + branch);
141
+ }
142
+
143
+ function cmdGitStatus(cwd, opts, raw) {
144
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
145
+ const branch = getCurrentBranch(cwd);
146
+ const r = execGit(cwd, ['status', '--porcelain']);
147
+ if (r.exitCode !== 0) {
148
+ output({ error: 'status_failed', detail: r.stderr }, raw, 'status failed');
149
+ return;
150
+ }
151
+ const lines = r.stdout ? r.stdout.split('\n').filter(Boolean) : [];
152
+ let staged = 0, unstaged = 0, untracked = 0;
153
+ for (const line of lines) {
154
+ const xy = line.slice(0, 2);
155
+ if (xy[0] !== ' ' && xy[0] !== '?') staged++;
156
+ if (xy[1] !== ' ' && xy[1] !== '?') unstaged++;
157
+ if (xy === '??') untracked++;
158
+ }
159
+ output({ branch, clean: lines.length === 0, staged_count: staged, unstaged_count: unstaged, untracked_count: untracked, files: lines }, raw, branch + (lines.length === 0 ? ' (clean)' : ' (' + lines.length + ' changes)'));
160
+ }
161
+
162
+ function cmdGitLog(cwd, opts, raw) {
163
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
164
+ const count = (opts && opts.count) ? parseInt(opts.count, 10) : 10;
165
+ const r = execGit(cwd, ['log', '--oneline', '-' + count]);
166
+ if (r.exitCode !== 0) {
167
+ output({ error: 'log_failed', detail: r.stderr }, raw, 'log failed');
168
+ return;
169
+ }
170
+ const commits = (r.stdout || '').split('\n').filter(Boolean).map(line => {
171
+ const spaceIdx = line.indexOf(' ');
172
+ return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) };
173
+ });
174
+ output({ commits, total: commits.length }, raw, commits.map(c => c.hash + ' ' + c.message).join('\n'));
175
+ }
176
+
177
+ function cmdGitStash(cwd, sub, opts, raw) {
178
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
179
+ const name = opts && opts.name;
180
+ const index = opts && opts.index;
181
+
182
+ if (sub === 'save') {
183
+ const args = name ? ['stash', 'push', '-m', name] : ['stash', 'push'];
184
+ const r = execGit(cwd, args);
185
+ if (r.exitCode !== 0) { output({ stashed: false, detail: r.stderr }, raw, 'stash failed'); return; }
186
+ output({ stashed: true, name: name || null }, raw, 'stashed');
187
+ return;
188
+ }
189
+ if (sub === 'pop') {
190
+ const args = index != null ? ['stash', 'pop', 'stash@{' + index + '}'] : ['stash', 'pop'];
191
+ const r = execGit(cwd, args);
192
+ if (r.exitCode !== 0) { output({ popped: false, detail: r.stderr }, raw, 'pop failed'); return; }
193
+ output({ popped: true }, raw, 'popped');
194
+ return;
195
+ }
196
+ if (sub === 'list') {
197
+ const r = execGit(cwd, ['stash', 'list']);
198
+ const entries = (r.stdout || '').split('\n').filter(Boolean);
199
+ output({ stashes: entries, count: entries.length }, raw, entries.join('\n') || 'no stashes');
200
+ return;
201
+ }
202
+ if (sub === 'drop') {
203
+ const args = index != null ? ['stash', 'drop', 'stash@{' + index + '}'] : ['stash', 'drop'];
204
+ const r = execGit(cwd, args);
205
+ if (r.exitCode !== 0) { output({ dropped: false, detail: r.stderr }, raw, 'drop failed'); return; }
206
+ output({ dropped: true }, raw, 'dropped');
207
+ return;
208
+ }
209
+ error('Unknown stash subcommand. Available: save, pop, list, drop');
210
+ }
211
+
212
+ function cmdGitDiff(cwd, opts, raw) {
213
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
214
+ const staged = opts && opts.staged;
215
+ const file = opts && opts.file;
216
+ const args = ['diff'];
217
+ if (staged) args.push('--cached');
218
+ if (file) args.push('--', file);
219
+ const r = execGit(cwd, args);
220
+ if (r.exitCode !== 0) { output({ error: 'diff_failed', detail: r.stderr }, raw, 'diff failed'); return; }
221
+ const diff = r.stdout || '';
222
+ const added = (diff.match(/^\+[^+]/gm) || []).length;
223
+ const removed = (diff.match(/^-[^-]/gm) || []).length;
224
+ const filesChanged = (diff.match(/^diff --git/gm) || []).length;
225
+ output({ diff, lines_added: added, lines_removed: removed, files_changed: filesChanged }, raw, '+' + added + '/-' + removed + ' in ' + filesChanged + ' file(s)');
226
+ }
227
+
228
+ function cmdGitRollback(cwd, opts, raw) {
229
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
230
+ const dryRun = opts && opts.dryRun;
231
+ const requestedTag = opts && opts.tag;
232
+
233
+ const tags = getTagList(cwd, 'pan-rollback-*');
234
+ if (tags.length === 0) {
235
+ output({ error: 'no_rollback_tags', hint: 'Create a snapshot with: pan-tools rollback-snapshot' }, raw, 'no rollback tags found');
236
+ return;
237
+ }
238
+
239
+ const targetTag = requestedTag || tags[tags.length - 1];
240
+ if (!tags.includes(targetTag)) {
241
+ output({ error: 'tag_not_found', tag: targetTag, available: tags }, raw, 'tag not found');
242
+ return;
243
+ }
244
+
245
+ if (!dryRun) {
246
+ const statusR = execGit(cwd, ['status', '--porcelain']);
247
+ if (statusR.stdout) {
248
+ output({ error: 'dirty_working_tree', hint: 'Commit or stash changes before rollback, or use --dry-run' }, raw, 'dirty working tree');
249
+ return;
250
+ }
251
+ const r = execGit(cwd, ['reset', '--hard', targetTag]);
252
+ if (r.exitCode !== 0) {
253
+ output({ rolled_back: false, tag: targetTag, detail: r.stderr }, raw, 'rollback failed');
254
+ return;
255
+ }
256
+ }
257
+
258
+ const hash = execGit(cwd, ['rev-parse', '--short', targetTag]).stdout || null;
259
+ output({ rolled_back: !dryRun, tag: targetTag, hash, dry_run: !!dryRun }, raw, (dryRun ? '[dry-run] would reset to ' : 'rolled back to ') + targetTag);
260
+ }
261
+
262
+ function cmdGitTag(cwd, sub, opts, raw) {
263
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
264
+ const name = opts && opts.name;
265
+ const message = opts && opts.message;
266
+ const pattern = opts && opts.pattern;
267
+
268
+ if (sub === 'list') {
269
+ const tags = getTagList(cwd, pattern || null);
270
+ output({ tags, count: tags.length }, raw, tags.join('\n') || 'no tags');
271
+ return;
272
+ }
273
+ if (sub === 'create') {
274
+ if (!name) { error('--name required for tag create'); }
275
+ const args = message ? ['tag', '-m', message, name] : ['tag', name];
276
+ const r = execGit(cwd, args);
277
+ if (r.exitCode !== 0) {
278
+ output({ created: false, tag: name, detail: r.stderr }, raw, 'tag create failed');
279
+ return;
280
+ }
281
+ output({ created: true, tag: name }, raw, name);
282
+ return;
283
+ }
284
+ if (sub === 'delete') {
285
+ if (!name) { error('--name required for tag delete'); }
286
+ const r = execGit(cwd, ['tag', '-d', name]);
287
+ if (r.exitCode !== 0) {
288
+ output({ deleted: false, tag: name, detail: r.stderr }, raw, 'tag delete failed');
289
+ return;
290
+ }
291
+ output({ deleted: true, tag: name }, raw, name);
292
+ return;
293
+ }
294
+ error('Unknown tag subcommand. Available: list, create, delete');
295
+ }
296
+
297
+ function cmdGitSync(cwd, opts, raw) {
298
+ if (!isGitRepo(cwd)) { _notGitRepo(raw); return; }
299
+ const remote = (opts && opts.remote) || 'origin';
300
+ const branch = (opts && opts.branch) || getCurrentBranch(cwd);
301
+ const rebase = opts && opts.rebase;
302
+
303
+ const remotes = execGit(cwd, ['remote']).stdout.split('\n').map(s => s.trim()).filter(Boolean);
304
+ if (!remotes.includes(remote)) { _noRemote(remote, raw); return; }
305
+
306
+ const fetchR = execGit(cwd, ['fetch', remote]);
307
+ if (fetchR.exitCode !== 0) {
308
+ output({ synced: false, detail: fetchR.stderr }, raw, 'fetch failed');
309
+ return;
310
+ }
311
+
312
+ const pullArgs = rebase ? ['pull', '--rebase', remote, branch] : ['pull', remote, branch];
313
+ const pullR = execGit(cwd, pullArgs);
314
+ if (pullR.exitCode !== 0) {
315
+ output({ synced: false, detail: pullR.stderr }, raw, 'pull failed');
316
+ return;
317
+ }
318
+
319
+ const logR = execGit(cwd, ['log', 'HEAD@{1}..HEAD', '--oneline']);
320
+ const newCommits = (logR.stdout || '').split('\n').filter(Boolean);
321
+ output({ synced: true, remote, branch, rebase: !!rebase, commits_received: newCommits.length }, raw, 'synced ' + newCommits.length + ' commit(s) from ' + remote + '/' + branch);
322
+ }
323
+
324
+ // ─── Top-level dispatcher ─────────────────────────────────────────────────────
325
+
326
+ function cmdGit(cwd, subcommand, args, raw) {
327
+ const sub2 = args[1];
328
+ const getOpt = (flag, def) => {
329
+ const i = args.indexOf(flag);
330
+ return i !== -1 && i + 1 < args.length ? args[i + 1] : def;
331
+ };
332
+ const hasFlag = flag => args.includes(flag);
333
+
334
+ switch (subcommand) {
335
+ case 'commit':
336
+ return cmdGitCommit(cwd, {
337
+ type: getOpt('--type', null),
338
+ message: getOpt('--message', null),
339
+ all: hasFlag('--all'),
340
+ amend: hasFlag('--amend'),
341
+ force: hasFlag('--force'),
342
+ files: args.filter((a, i) => a !== '--type' && a !== '--message' && !a.startsWith('--') && args[i - 1] !== '--type' && args[i - 1] !== '--message'),
343
+ }, raw);
344
+ case 'branch':
345
+ return cmdGitBranch(cwd, sub2, {
346
+ name: getOpt('--name', null),
347
+ phase: getOpt('--phase', null),
348
+ force: hasFlag('--force'),
349
+ }, raw);
350
+ case 'push':
351
+ return cmdGitPush(cwd, {
352
+ remote: getOpt('--remote', null),
353
+ branch: getOpt('--branch', null),
354
+ force: hasFlag('--force'),
355
+ }, raw);
356
+ case 'status':
357
+ return cmdGitStatus(cwd, {}, raw);
358
+ case 'log':
359
+ return cmdGitLog(cwd, { count: getOpt('--count', null) }, raw);
360
+ case 'stash':
361
+ return cmdGitStash(cwd, sub2, {
362
+ name: getOpt('--name', null),
363
+ index: getOpt('--index', null),
364
+ }, raw);
365
+ case 'diff':
366
+ return cmdGitDiff(cwd, {
367
+ staged: hasFlag('--staged'),
368
+ file: getOpt('--file', null),
369
+ }, raw);
370
+ case 'rollback':
371
+ return cmdGitRollback(cwd, {
372
+ tag: getOpt('--tag', null),
373
+ dryRun: hasFlag('--dry-run'),
374
+ }, raw);
375
+ case 'tag':
376
+ return cmdGitTag(cwd, sub2, {
377
+ name: getOpt('--name', null),
378
+ message: getOpt('--message', null),
379
+ pattern: getOpt('--pattern', null),
380
+ }, raw);
381
+ case 'sync':
382
+ return cmdGitSync(cwd, {
383
+ remote: getOpt('--remote', null),
384
+ branch: getOpt('--branch', null),
385
+ rebase: hasFlag('--rebase'),
386
+ }, raw);
387
+ default:
388
+ error('Unknown git subcommand: ' + subcommand + '. Available: commit, branch, push, status, log, stash, diff, rollback, tag, sync');
389
+ }
390
+ }
391
+
392
+ module.exports = {
393
+ cmdGit,
394
+ cmdGitCommit,
395
+ cmdGitBranch,
396
+ cmdGitPush,
397
+ cmdGitStatus,
398
+ cmdGitLog,
399
+ cmdGitStash,
400
+ cmdGitDiff,
401
+ cmdGitRollback,
402
+ cmdGitTag,
403
+ cmdGitSync,
404
+ getCurrentBranch,
405
+ getBranchList,
406
+ getTagList,
407
+ };