sequant 1.9.0 → 1.10.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 +4 -1
- package/dist/bin/cli.js +1 -0
- package/dist/src/commands/run.d.ts +5 -0
- package/dist/src/commands/run.js +201 -24
- package/dist/src/commands/status.d.ts +18 -2
- package/dist/src/commands/status.js +321 -2
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +5 -0
- package/dist/src/lib/settings.d.ts +6 -0
- package/dist/src/lib/workflow/state-hook.d.ts +69 -0
- package/dist/src/lib/workflow/state-hook.js +166 -0
- package/dist/src/lib/workflow/state-manager.d.ts +136 -0
- package/dist/src/lib/workflow/state-manager.js +329 -0
- package/dist/src/lib/workflow/state-schema.d.ts +224 -0
- package/dist/src/lib/workflow/state-schema.js +190 -0
- package/dist/src/lib/workflow/state-utils.d.ts +66 -0
- package/dist/src/lib/workflow/state-utils.js +243 -0
- package/package.json +1 -1
- package/templates/scripts/new-feature.sh +23 -13
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for persistent workflow state tracking
|
|
3
|
+
*
|
|
4
|
+
* These schemas define the structure of `.sequant/state.json` which tracks
|
|
5
|
+
* the current state of all issues being processed through the workflow.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { WorkflowStateSchema, type WorkflowState } from './state-schema';
|
|
10
|
+
*
|
|
11
|
+
* // Validate state file
|
|
12
|
+
* const state = WorkflowStateSchema.parse(JSON.parse(stateContent));
|
|
13
|
+
*
|
|
14
|
+
* // Type-safe access
|
|
15
|
+
* const issue42 = state.issues["42"];
|
|
16
|
+
* console.log(issue42.currentPhase, issue42.status);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
/**
|
|
21
|
+
* Workflow phases in order of execution
|
|
22
|
+
*/
|
|
23
|
+
export const WORKFLOW_PHASES = [
|
|
24
|
+
"spec",
|
|
25
|
+
"security-review",
|
|
26
|
+
"exec",
|
|
27
|
+
"testgen",
|
|
28
|
+
"test",
|
|
29
|
+
"qa",
|
|
30
|
+
"loop",
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Phase status - tracks individual phase progress
|
|
34
|
+
*/
|
|
35
|
+
export const PhaseStatusSchema = z.enum([
|
|
36
|
+
"pending", // Phase not yet started
|
|
37
|
+
"in_progress", // Phase currently executing
|
|
38
|
+
"completed", // Phase finished successfully
|
|
39
|
+
"failed", // Phase finished with errors
|
|
40
|
+
"skipped", // Phase intentionally skipped (e.g., bug labels skip spec)
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Issue status - tracks overall issue progress
|
|
44
|
+
*/
|
|
45
|
+
export const IssueStatusSchema = z.enum([
|
|
46
|
+
"not_started", // Issue tracked but no work begun
|
|
47
|
+
"in_progress", // Actively being worked on
|
|
48
|
+
"ready_for_merge", // All phases passed, PR ready for review
|
|
49
|
+
"merged", // PR merged, work complete
|
|
50
|
+
"blocked", // Waiting on external input or dependency
|
|
51
|
+
"abandoned", // Work stopped, will not continue
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Phase type
|
|
55
|
+
*/
|
|
56
|
+
export const PhaseSchema = z.enum([
|
|
57
|
+
"spec",
|
|
58
|
+
"security-review",
|
|
59
|
+
"exec",
|
|
60
|
+
"testgen",
|
|
61
|
+
"test",
|
|
62
|
+
"qa",
|
|
63
|
+
"loop",
|
|
64
|
+
]);
|
|
65
|
+
/**
|
|
66
|
+
* Individual phase state within an issue
|
|
67
|
+
*/
|
|
68
|
+
export const PhaseStateSchema = z.object({
|
|
69
|
+
/** Current status of the phase */
|
|
70
|
+
status: PhaseStatusSchema,
|
|
71
|
+
/** When the phase started (if started) */
|
|
72
|
+
startedAt: z.string().datetime().optional(),
|
|
73
|
+
/** When the phase completed (if completed/failed/skipped) */
|
|
74
|
+
completedAt: z.string().datetime().optional(),
|
|
75
|
+
/** Error message if phase failed */
|
|
76
|
+
error: z.string().optional(),
|
|
77
|
+
/** Number of loop iterations (for loop phase) */
|
|
78
|
+
iteration: z.number().int().nonnegative().optional(),
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* PR information for an issue
|
|
82
|
+
*/
|
|
83
|
+
export const PRInfoSchema = z.object({
|
|
84
|
+
/** PR number */
|
|
85
|
+
number: z.number().int().positive(),
|
|
86
|
+
/** PR URL */
|
|
87
|
+
url: z.string().url(),
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* Quality loop state
|
|
91
|
+
*/
|
|
92
|
+
export const LoopStateSchema = z.object({
|
|
93
|
+
/** Whether quality loop is enabled */
|
|
94
|
+
enabled: z.boolean(),
|
|
95
|
+
/** Current iteration number */
|
|
96
|
+
iteration: z.number().int().nonnegative(),
|
|
97
|
+
/** Maximum iterations allowed */
|
|
98
|
+
maxIterations: z.number().int().positive(),
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Complete state for a single issue
|
|
102
|
+
*/
|
|
103
|
+
export const IssueStateSchema = z.object({
|
|
104
|
+
/** GitHub issue number */
|
|
105
|
+
number: z.number().int().positive(),
|
|
106
|
+
/** Issue title */
|
|
107
|
+
title: z.string(),
|
|
108
|
+
/** Overall issue status */
|
|
109
|
+
status: IssueStatusSchema,
|
|
110
|
+
/** Path to the worktree (if created) */
|
|
111
|
+
worktree: z.string().optional(),
|
|
112
|
+
/** Branch name for this issue */
|
|
113
|
+
branch: z.string().optional(),
|
|
114
|
+
/** Current phase being executed or last executed */
|
|
115
|
+
currentPhase: PhaseSchema.optional(),
|
|
116
|
+
/** State of each phase (only phases that have been started/tracked) */
|
|
117
|
+
phases: z.record(z.string(), PhaseStateSchema),
|
|
118
|
+
/** PR information (if PR created) */
|
|
119
|
+
pr: PRInfoSchema.optional(),
|
|
120
|
+
/** Quality loop state (if loop enabled) */
|
|
121
|
+
loop: LoopStateSchema.optional(),
|
|
122
|
+
/** Claude session ID (for resume) */
|
|
123
|
+
sessionId: z.string().optional(),
|
|
124
|
+
/** Most recent activity timestamp */
|
|
125
|
+
lastActivity: z.string().datetime(),
|
|
126
|
+
/** When this issue was first tracked */
|
|
127
|
+
createdAt: z.string().datetime(),
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Complete workflow state schema
|
|
131
|
+
*
|
|
132
|
+
* This is the top-level schema for `.sequant/state.json`
|
|
133
|
+
*/
|
|
134
|
+
export const WorkflowStateSchema = z.object({
|
|
135
|
+
/** Schema version for backwards compatibility */
|
|
136
|
+
version: z.literal(1),
|
|
137
|
+
/** When the state file was last updated */
|
|
138
|
+
lastUpdated: z.string().datetime(),
|
|
139
|
+
/** State for all tracked issues, keyed by issue number */
|
|
140
|
+
issues: z.record(z.string(), IssueStateSchema),
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* Default state file path
|
|
144
|
+
*/
|
|
145
|
+
export const STATE_FILE_PATH = ".sequant/state.json";
|
|
146
|
+
/**
|
|
147
|
+
* Create an empty workflow state
|
|
148
|
+
*/
|
|
149
|
+
export function createEmptyState() {
|
|
150
|
+
return {
|
|
151
|
+
version: 1,
|
|
152
|
+
lastUpdated: new Date().toISOString(),
|
|
153
|
+
issues: {},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Create initial state for a new issue
|
|
158
|
+
*/
|
|
159
|
+
export function createIssueState(issueNumber, title, options) {
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
return {
|
|
162
|
+
number: issueNumber,
|
|
163
|
+
title,
|
|
164
|
+
status: "not_started",
|
|
165
|
+
worktree: options?.worktree,
|
|
166
|
+
branch: options?.branch,
|
|
167
|
+
phases: {},
|
|
168
|
+
loop: options?.qualityLoop
|
|
169
|
+
? {
|
|
170
|
+
enabled: true,
|
|
171
|
+
iteration: 0,
|
|
172
|
+
maxIterations: options?.maxIterations ?? 3,
|
|
173
|
+
}
|
|
174
|
+
: undefined,
|
|
175
|
+
lastActivity: now,
|
|
176
|
+
createdAt: now,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create initial phase state
|
|
181
|
+
*/
|
|
182
|
+
export function createPhaseState(status = "pending") {
|
|
183
|
+
if (status === "in_progress") {
|
|
184
|
+
return {
|
|
185
|
+
status,
|
|
186
|
+
startedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { status };
|
|
190
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State utilities for rebuilding and cleaning up workflow state
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { rebuildStateFromLogs, cleanupStaleEntries } from './state-utils';
|
|
7
|
+
*
|
|
8
|
+
* // Rebuild state from run logs
|
|
9
|
+
* await rebuildStateFromLogs();
|
|
10
|
+
*
|
|
11
|
+
* // Clean up orphaned entries
|
|
12
|
+
* const result = await cleanupStaleEntries({ dryRun: true });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface RebuildOptions {
|
|
16
|
+
/** Log directory path (default: .sequant/logs) */
|
|
17
|
+
logPath?: string;
|
|
18
|
+
/** State file path (default: .sequant/state.json) */
|
|
19
|
+
statePath?: string;
|
|
20
|
+
/** Enable verbose logging */
|
|
21
|
+
verbose?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface RebuildResult {
|
|
24
|
+
/** Whether rebuild was successful */
|
|
25
|
+
success: boolean;
|
|
26
|
+
/** Number of log files processed */
|
|
27
|
+
logsProcessed: number;
|
|
28
|
+
/** Number of issues found */
|
|
29
|
+
issuesFound: number;
|
|
30
|
+
/** Error message if failed */
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Rebuild workflow state from run logs
|
|
35
|
+
*
|
|
36
|
+
* Scans all run logs in .sequant/logs/ and reconstructs state
|
|
37
|
+
* based on the most recent activity for each issue.
|
|
38
|
+
*/
|
|
39
|
+
export declare function rebuildStateFromLogs(options?: RebuildOptions): Promise<RebuildResult>;
|
|
40
|
+
export interface CleanupOptions {
|
|
41
|
+
/** State file path (default: .sequant/state.json) */
|
|
42
|
+
statePath?: string;
|
|
43
|
+
/** Only report what would be cleaned (don't modify) */
|
|
44
|
+
dryRun?: boolean;
|
|
45
|
+
/** Enable verbose logging */
|
|
46
|
+
verbose?: boolean;
|
|
47
|
+
/** Remove issues older than this many days */
|
|
48
|
+
maxAgeDays?: number;
|
|
49
|
+
}
|
|
50
|
+
export interface CleanupResult {
|
|
51
|
+
/** Whether cleanup was successful */
|
|
52
|
+
success: boolean;
|
|
53
|
+
/** Issues that were removed or would be removed */
|
|
54
|
+
removed: number[];
|
|
55
|
+
/** Issues that were marked as orphaned */
|
|
56
|
+
orphaned: number[];
|
|
57
|
+
/** Error message if failed */
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clean up stale and orphaned entries from workflow state
|
|
62
|
+
*
|
|
63
|
+
* - Removes issues with non-existent worktrees (orphaned)
|
|
64
|
+
* - Optionally removes old merged/abandoned issues
|
|
65
|
+
*/
|
|
66
|
+
export declare function cleanupStaleEntries(options?: CleanupOptions): Promise<CleanupResult>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State utilities for rebuilding and cleaning up workflow state
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { rebuildStateFromLogs, cleanupStaleEntries } from './state-utils';
|
|
7
|
+
*
|
|
8
|
+
* // Rebuild state from run logs
|
|
9
|
+
* await rebuildStateFromLogs();
|
|
10
|
+
*
|
|
11
|
+
* // Clean up orphaned entries
|
|
12
|
+
* const result = await cleanupStaleEntries({ dryRun: true });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { spawnSync } from "child_process";
|
|
18
|
+
import { StateManager } from "./state-manager.js";
|
|
19
|
+
import { createEmptyState, createIssueState, createPhaseState, } from "./state-schema.js";
|
|
20
|
+
import { RunLogSchema, LOG_PATHS } from "./run-log-schema.js";
|
|
21
|
+
/**
|
|
22
|
+
* Rebuild workflow state from run logs
|
|
23
|
+
*
|
|
24
|
+
* Scans all run logs in .sequant/logs/ and reconstructs state
|
|
25
|
+
* based on the most recent activity for each issue.
|
|
26
|
+
*/
|
|
27
|
+
export async function rebuildStateFromLogs(options = {}) {
|
|
28
|
+
const logPath = options.logPath ?? LOG_PATHS.project;
|
|
29
|
+
if (!fs.existsSync(logPath)) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
logsProcessed: 0,
|
|
33
|
+
issuesFound: 0,
|
|
34
|
+
error: `Log directory not found: ${logPath}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
// Find all log files
|
|
39
|
+
const files = fs.readdirSync(logPath).filter((f) => f.endsWith(".json"));
|
|
40
|
+
if (files.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
logsProcessed: 0,
|
|
44
|
+
issuesFound: 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Sort by timestamp (newest first)
|
|
48
|
+
files.sort().reverse();
|
|
49
|
+
// Build state from logs
|
|
50
|
+
const state = createEmptyState();
|
|
51
|
+
const issueMap = new Map();
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const filePath = path.join(logPath, file);
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
56
|
+
const logData = JSON.parse(content);
|
|
57
|
+
const log = RunLogSchema.safeParse(logData);
|
|
58
|
+
if (!log.success) {
|
|
59
|
+
if (options.verbose) {
|
|
60
|
+
console.log(`⚠️ Invalid log format: ${file}`);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const runLog = log.data;
|
|
65
|
+
// Process each issue in the log
|
|
66
|
+
for (const issueLog of runLog.issues) {
|
|
67
|
+
// Skip if we already have newer data for this issue
|
|
68
|
+
if (issueMap.has(issueLog.issueNumber)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Create issue state from log
|
|
72
|
+
const issueState = createIssueState(issueLog.issueNumber, issueLog.title);
|
|
73
|
+
// Determine status from log
|
|
74
|
+
if (issueLog.status === "success") {
|
|
75
|
+
issueState.status = "ready_for_merge";
|
|
76
|
+
}
|
|
77
|
+
else if (issueLog.status === "failure") {
|
|
78
|
+
issueState.status = "in_progress";
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
issueState.status = "in_progress";
|
|
82
|
+
}
|
|
83
|
+
// Add phase states from log
|
|
84
|
+
for (const phaseLog of issueLog.phases) {
|
|
85
|
+
const phaseState = createPhaseState(phaseLog.status === "success"
|
|
86
|
+
? "completed"
|
|
87
|
+
: phaseLog.status === "failure"
|
|
88
|
+
? "failed"
|
|
89
|
+
: phaseLog.status === "skipped"
|
|
90
|
+
? "skipped"
|
|
91
|
+
: "completed");
|
|
92
|
+
phaseState.startedAt = phaseLog.startTime;
|
|
93
|
+
phaseState.completedAt = phaseLog.endTime;
|
|
94
|
+
if (phaseLog.error) {
|
|
95
|
+
phaseState.error = phaseLog.error;
|
|
96
|
+
}
|
|
97
|
+
issueState.phases[phaseLog.phase] = phaseState;
|
|
98
|
+
// Update current phase to last executed
|
|
99
|
+
issueState.currentPhase = phaseLog.phase;
|
|
100
|
+
}
|
|
101
|
+
// Set last activity from most recent phase
|
|
102
|
+
const lastPhase = issueLog.phases[issueLog.phases.length - 1];
|
|
103
|
+
if (lastPhase) {
|
|
104
|
+
issueState.lastActivity = lastPhase.endTime;
|
|
105
|
+
}
|
|
106
|
+
issueMap.set(issueLog.issueNumber, issueState);
|
|
107
|
+
}
|
|
108
|
+
if (options.verbose) {
|
|
109
|
+
console.log(`✓ Processed: ${file}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (options.verbose) {
|
|
114
|
+
console.log(`⚠️ Error reading ${file}: ${err}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Copy issues to state
|
|
119
|
+
for (const [num, issueState] of issueMap) {
|
|
120
|
+
state.issues[String(num)] = issueState;
|
|
121
|
+
}
|
|
122
|
+
// Save rebuilt state
|
|
123
|
+
const manager = new StateManager({
|
|
124
|
+
statePath: options.statePath,
|
|
125
|
+
verbose: options.verbose,
|
|
126
|
+
});
|
|
127
|
+
await manager.saveState(state);
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
logsProcessed: files.length,
|
|
131
|
+
issuesFound: issueMap.size,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
logsProcessed: 0,
|
|
138
|
+
issuesFound: 0,
|
|
139
|
+
error: String(error),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Clean up stale and orphaned entries from workflow state
|
|
145
|
+
*
|
|
146
|
+
* - Removes issues with non-existent worktrees (orphaned)
|
|
147
|
+
* - Optionally removes old merged/abandoned issues
|
|
148
|
+
*/
|
|
149
|
+
export async function cleanupStaleEntries(options = {}) {
|
|
150
|
+
const manager = new StateManager({
|
|
151
|
+
statePath: options.statePath,
|
|
152
|
+
verbose: options.verbose,
|
|
153
|
+
});
|
|
154
|
+
if (!manager.stateExists()) {
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
removed: [],
|
|
158
|
+
orphaned: [],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const state = await manager.getState();
|
|
163
|
+
const removed = [];
|
|
164
|
+
const orphaned = [];
|
|
165
|
+
// Get list of active worktrees
|
|
166
|
+
const activeWorktrees = getActiveWorktrees();
|
|
167
|
+
for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
|
|
168
|
+
const issueNum = parseInt(issueNumStr, 10);
|
|
169
|
+
// Check if worktree exists (if issue has one)
|
|
170
|
+
if (issueState.worktree &&
|
|
171
|
+
!activeWorktrees.includes(issueState.worktree)) {
|
|
172
|
+
orphaned.push(issueNum);
|
|
173
|
+
if (options.verbose) {
|
|
174
|
+
console.log(`🗑️ Orphaned: #${issueNum} (worktree not found: ${issueState.worktree})`);
|
|
175
|
+
}
|
|
176
|
+
if (!options.dryRun) {
|
|
177
|
+
// Mark as abandoned or remove based on status
|
|
178
|
+
if (issueState.status === "merged" ||
|
|
179
|
+
issueState.status === "abandoned") {
|
|
180
|
+
removed.push(issueNum);
|
|
181
|
+
delete state.issues[issueNumStr];
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Update status to indicate orphaned state
|
|
185
|
+
issueState.status = "abandoned";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// Check age for merged/abandoned issues
|
|
191
|
+
if (options.maxAgeDays &&
|
|
192
|
+
(issueState.status === "merged" || issueState.status === "abandoned")) {
|
|
193
|
+
const lastActivity = new Date(issueState.lastActivity);
|
|
194
|
+
const ageDays = (Date.now() - lastActivity.getTime()) / (1000 * 60 * 60 * 24);
|
|
195
|
+
if (ageDays > options.maxAgeDays) {
|
|
196
|
+
removed.push(issueNum);
|
|
197
|
+
if (options.verbose) {
|
|
198
|
+
console.log(`🗑️ Stale: #${issueNum} (${Math.floor(ageDays)} days old)`);
|
|
199
|
+
}
|
|
200
|
+
if (!options.dryRun) {
|
|
201
|
+
delete state.issues[issueNumStr];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Save updated state
|
|
207
|
+
if (!options.dryRun && (removed.length > 0 || orphaned.length > 0)) {
|
|
208
|
+
await manager.saveState(state);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
removed,
|
|
213
|
+
orphaned,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
removed: [],
|
|
220
|
+
orphaned: [],
|
|
221
|
+
error: String(error),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get list of active worktree paths
|
|
227
|
+
*/
|
|
228
|
+
function getActiveWorktrees() {
|
|
229
|
+
const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
|
|
230
|
+
stdio: "pipe",
|
|
231
|
+
});
|
|
232
|
+
if (result.status !== 0) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
const output = result.stdout.toString();
|
|
236
|
+
const paths = [];
|
|
237
|
+
for (const line of output.split("\n")) {
|
|
238
|
+
if (line.startsWith("worktree ")) {
|
|
239
|
+
paths.push(line.substring(9));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return paths;
|
|
243
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
3
|
# Create a new feature worktree from a GitHub issue
|
|
4
|
-
# Usage: ./scripts/new-feature.sh <issue-number> [--stash]
|
|
4
|
+
# Usage: ./scripts/new-feature.sh <issue-number> [--base <branch>] [--stash]
|
|
5
5
|
# Example: ./scripts/new-feature.sh 4
|
|
6
6
|
# Example: ./scripts/new-feature.sh 4 --stash # Auto-stash uncommitted changes
|
|
7
|
+
# Example: ./scripts/new-feature.sh 4 --base feature/dashboard # Branch from feature branch
|
|
7
8
|
|
|
8
9
|
set -e
|
|
9
10
|
|
|
@@ -14,20 +15,27 @@ BLUE='\033[0;34m'
|
|
|
14
15
|
YELLOW='\033[1;33m'
|
|
15
16
|
NC='\033[0m' # No Color
|
|
16
17
|
|
|
17
|
-
# Parse arguments (flexible position for
|
|
18
|
+
# Parse arguments (flexible position for flags)
|
|
18
19
|
STASH_FLAG=false
|
|
19
20
|
ISSUE_NUMBER=""
|
|
21
|
+
BASE_BRANCH="main"
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
case $
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case $1 in
|
|
23
25
|
--stash)
|
|
24
26
|
STASH_FLAG=true
|
|
27
|
+
shift
|
|
28
|
+
;;
|
|
29
|
+
--base)
|
|
30
|
+
BASE_BRANCH="$2"
|
|
31
|
+
shift 2
|
|
25
32
|
;;
|
|
26
33
|
*)
|
|
27
34
|
# First non-flag argument is the issue number
|
|
28
35
|
if [ -z "$ISSUE_NUMBER" ]; then
|
|
29
|
-
ISSUE_NUMBER=$
|
|
36
|
+
ISSUE_NUMBER=$1
|
|
30
37
|
fi
|
|
38
|
+
shift
|
|
31
39
|
;;
|
|
32
40
|
esac
|
|
33
41
|
done
|
|
@@ -35,9 +43,10 @@ done
|
|
|
35
43
|
# Check if issue number is provided
|
|
36
44
|
if [ -z "$ISSUE_NUMBER" ]; then
|
|
37
45
|
echo -e "${RED}❌ Error: Issue number required${NC}"
|
|
38
|
-
echo "Usage: ./scripts/new-feature.sh <issue-number> [--stash]"
|
|
46
|
+
echo "Usage: ./scripts/new-feature.sh <issue-number> [--base <branch>] [--stash]"
|
|
39
47
|
echo "Example: ./scripts/new-feature.sh 4"
|
|
40
48
|
echo "Example: ./scripts/new-feature.sh 4 --stash"
|
|
49
|
+
echo "Example: ./scripts/new-feature.sh 4 --base feature/dashboard"
|
|
41
50
|
exit 1
|
|
42
51
|
fi
|
|
43
52
|
|
|
@@ -95,6 +104,7 @@ WORKTREE_DIR="../worktrees/${BRANCH_NAME}"
|
|
|
95
104
|
|
|
96
105
|
echo -e "${GREEN}✨ Creating worktree for issue #${ISSUE_NUMBER}${NC}"
|
|
97
106
|
echo -e "${BLUE}Branch: ${BRANCH_NAME}${NC}"
|
|
107
|
+
echo -e "${BLUE}Base: ${BASE_BRANCH}${NC}"
|
|
98
108
|
echo -e "${BLUE}Worktree: ${WORKTREE_DIR}${NC}"
|
|
99
109
|
echo ""
|
|
100
110
|
|
|
@@ -132,14 +142,14 @@ if git show-ref --verify --quiet "refs/heads/${BRANCH_NAME}"; then
|
|
|
132
142
|
fi
|
|
133
143
|
fi
|
|
134
144
|
|
|
135
|
-
# Update
|
|
136
|
-
echo -e "${BLUE}📥 Updating
|
|
137
|
-
git fetch origin
|
|
138
|
-
git checkout
|
|
139
|
-
git pull origin
|
|
145
|
+
# Update base branch
|
|
146
|
+
echo -e "${BLUE}📥 Updating ${BASE_BRANCH} branch...${NC}"
|
|
147
|
+
git fetch origin "$BASE_BRANCH"
|
|
148
|
+
git checkout "$BASE_BRANCH"
|
|
149
|
+
git pull origin "$BASE_BRANCH"
|
|
140
150
|
|
|
141
|
-
# Create worktree
|
|
142
|
-
echo -e "${BLUE}🌿 Creating new worktree...${NC}"
|
|
151
|
+
# Create worktree from base branch
|
|
152
|
+
echo -e "${BLUE}🌿 Creating new worktree from ${BASE_BRANCH}...${NC}"
|
|
143
153
|
git worktree add "$WORKTREE_DIR" -b "$BRANCH_NAME"
|
|
144
154
|
|
|
145
155
|
# Navigate to worktree
|