@proletariat/cli 0.3.90 → 0.3.92
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/dist/commands/work/index.js +4 -0
- package/dist/commands/work/index.js.map +1 -1
- package/dist/commands/work/ship.d.ts +43 -0
- package/dist/commands/work/ship.js +587 -0
- package/dist/commands/work/ship.js.map +1 -0
- package/dist/commands/work/start.d.ts +1 -0
- package/dist/commands/work/start.js +11 -1
- package/dist/commands/work/start.js.map +1 -1
- package/dist/lib/execution/context.d.ts +20 -0
- package/dist/lib/execution/context.js +88 -0
- package/dist/lib/execution/context.js.map +1 -1
- package/dist/lib/execution/devcontainer.d.ts +6 -0
- package/dist/lib/execution/devcontainer.js +48 -1
- package/dist/lib/execution/devcontainer.js.map +1 -1
- package/dist/lib/execution/runners/devcontainer.js +11 -0
- package/dist/lib/execution/runners/devcontainer.js.map +1 -1
- package/dist/lib/execution/runners/docker-management.js +1 -1
- package/dist/lib/execution/runners/docker-management.js.map +1 -1
- package/dist/lib/execution/runners/orchestrator.js +6 -3
- package/dist/lib/execution/runners/orchestrator.js.map +1 -1
- package/dist/lib/execution/runners/prompt-builder.js +31 -0
- package/dist/lib/execution/runners/prompt-builder.js.map +1 -1
- package/dist/lib/execution/runners/shared.d.ts +2 -2
- package/dist/lib/execution/runners/shared.js +2 -2
- package/dist/lib/execution/runners/shared.js.map +1 -1
- package/dist/lib/execution/types.d.ts +23 -0
- package/dist/lib/execution/types.js.map +1 -1
- package/dist/lib/providers/index.d.ts +2 -0
- package/dist/lib/providers/index.js +2 -0
- package/dist/lib/providers/index.js.map +1 -1
- package/dist/lib/providers/state-intents.d.ts +44 -0
- package/dist/lib/providers/state-intents.js +101 -0
- package/dist/lib/providers/state-intents.js.map +1 -0
- package/dist/lib/providers/state-resolution.d.ts +137 -0
- package/dist/lib/providers/state-resolution.js +302 -0
- package/dist/lib/providers/state-resolution.js.map +1 -0
- package/dist/lib/work-lifecycle/post-execution.js +47 -28
- package/dist/lib/work-lifecycle/post-execution.js.map +1 -1
- package/oclif.manifest.json +2317 -2203
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@ export default class Work extends PMOCommand {
|
|
|
33
33
|
{ id: 'status', name: 'Status — view in-progress tickets', command: `prlt work status -P ${projectId} --json` },
|
|
34
34
|
{ id: 'spawn', name: 'Spawn — batch by column', command: `prlt work spawn -P ${projectId} --json` },
|
|
35
35
|
{ id: 'ready', name: 'Ready — mark work ready for review', command: `prlt work ready -P ${projectId} --json` },
|
|
36
|
+
{ id: 'ship', name: 'Ship — rebase, merge PR, move to Done', command: `prlt work ship -P ${projectId} --json` },
|
|
36
37
|
{ id: 'complete', name: 'Complete — mark work done', command: `prlt work complete -P ${projectId} --json` },
|
|
37
38
|
{ id: 'hooks', name: 'Hooks — manage lifecycle hooks', command: 'prlt work hooks --json' },
|
|
38
39
|
{ id: 'cancel', name: 'Cancel', command: '' },
|
|
@@ -85,6 +86,9 @@ export default class Work extends PMOCommand {
|
|
|
85
86
|
case 'ready':
|
|
86
87
|
await this.config.runCommand('work:ready', projectArgs);
|
|
87
88
|
break;
|
|
89
|
+
case 'ship':
|
|
90
|
+
await this.config.runCommand('work:ship', projectArgs);
|
|
91
|
+
break;
|
|
88
92
|
case 'complete':
|
|
89
93
|
await this.config.runCommand('work:complete', projectArgs);
|
|
90
94
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/work/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/commands/work/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,UAAU;IAC1C,MAAM,CAAC,WAAW,GAAG,yEAAyE,CAAC;IAE/F,MAAM,CAAC,QAAQ,GAAG;QAChB,qCAAqC;KACtC,CAAC;IAEF,MAAM,CAAC,KAAK,GAAG;QACb,GAAG,YAAY;KAChB,CAAC;IAEQ,aAAa;QACrB,gEAAgE;QAChE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,gEAAgE;QAChE,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAE9C,sCAAsC;QACtC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAEzC,0DAA0D;QAC1D,MAAM,WAAW,GAAG;YAClB,iCAAiC;YACjC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,4CAA4C,EAAE,OAAO,EAAE,sBAAsB,SAAS,SAAS,EAAE;YACtH,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,iDAAiD,EAAE,OAAO,EAAE,wBAAwB,SAAS,SAAS,EAAE;YAC/H,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,wCAAwC,EAAE,OAAO,EAAE,0BAA0B,SAAS,SAAS,EAAE;YAC1H,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,iCAAiC,EAAE,OAAO,EAAE,uBAAuB,SAAS,SAAS,EAAE;YAC7G,kCAAkC;YAClC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,iCAAiC,EAAE,OAAO,EAAE,qBAAqB,SAAS,SAAS,EAAE;YACzG,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,oCAAoC,EAAE,OAAO,EAAE,qBAAqB,SAAS,SAAS,EAAE;YAC5G,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,2BAA2B,EAAE,OAAO,EAAE,qBAAqB,SAAS,SAAS,EAAE;YACnG,oBAAoB;YACpB,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,mCAAmC,EAAE,OAAO,EAAE,uBAAuB,SAAS,SAAS,EAAE;YAC/G,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,sBAAsB,SAAS,SAAS,EAAE;YACnG,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,oCAAoC,EAAE,OAAO,EAAE,sBAAsB,SAAS,SAAS,EAAE;YAC9G,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,uCAAuC,EAAE,OAAO,EAAE,qBAAqB,SAAS,SAAS,EAAE;YAC/G,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,2BAA2B,EAAE,OAAO,EAAE,yBAAyB,SAAS,SAAS,EAAE;YAC3G,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,gCAAgC,EAAE,OAAO,EAAE,wBAAwB,EAAE;YAC1F,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE;SAC9C,CAAC;QACF,MAAM,OAAO,GAAG,8CAA8C,CAAC;QAE/D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC;YACvC,OAAO,EAAE,KAAK,GAAG,OAAO;YACxB,KAAK,EAAE,WAAW;YAClB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI;YACtB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;YACrB,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO;YAC5B,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI;SAC3D,CAAC,CAAC;QAEH,IAAI,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,OAAO;QACT,CAAC;QAED,8BAA8B;QAC9B,6DAA6D;QAC7D,MAAM,WAAW,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QAC7C,QAAQ,MAAM,EAAE,CAAC;YACf,oBAAoB;YACpB,KAAK,OAAO;gBACV,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;gBACxD,MAAM;YACR,KAAK,SAAS;gBACZ,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;gBAC1D,MAAM;YACR,KAAK,WAAW;gBACd,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;gBAC5D,MAAM;YACR,KAAK,QAAQ;gBACX,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;gBACzD,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;gBACvD,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;gBACvD,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;gBACvD,MAAM;YACR,YAAY;YACZ,KAAK,QAAQ;gBACX,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;gBACzD,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;gBACxD,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;gBACxD,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;gBACvD,MAAM;YACR,KAAK,UAAU;gBACb,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;gBAC3D,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;gBAC3C,MAAM;QACV,CAAC;IACH,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class WorkShip extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
ticketId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
pr: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
method: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'no-rebase': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'delete-branch': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
admin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
};
|
|
20
|
+
execute(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the linked ticket for a PR.
|
|
23
|
+
*/
|
|
24
|
+
private resolveLinkedTicket;
|
|
25
|
+
/**
|
|
26
|
+
* Wait for CI checks to pass. Returns immediately if all passed.
|
|
27
|
+
* If --wait is set, polls until all complete. Otherwise warns on pending.
|
|
28
|
+
*/
|
|
29
|
+
private waitForCI;
|
|
30
|
+
/**
|
|
31
|
+
* Check if the PR has merge conflicts with the base branch.
|
|
32
|
+
*/
|
|
33
|
+
private checkMergeConflicts;
|
|
34
|
+
/**
|
|
35
|
+
* Rebase the PR branch onto the base branch and force-push.
|
|
36
|
+
*/
|
|
37
|
+
private rebaseOntoBase;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the working directory for git operations.
|
|
40
|
+
* Tries: devcontainer repo dirs, execution worktree, agent dirs.
|
|
41
|
+
*/
|
|
42
|
+
private resolveWorktreePath;
|
|
43
|
+
}
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { PMOCommand, pmoBaseFlags, autoExportToBoard } from '../../lib/pmo/index.js';
|
|
6
|
+
import { getWorkColumnSetting, findColumnByName } from '../../lib/pmo/utils.js';
|
|
7
|
+
import { styles } from '../../lib/styles.js';
|
|
8
|
+
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
9
|
+
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
10
|
+
import { isGHInstalled, isGHAuthenticated, getPRByNumber, getPRForBranch, getPRChecks, mergePR, getGitHubRepo, } from '../../lib/pr/index.js';
|
|
11
|
+
import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
12
|
+
import { getEventBus } from '../../lib/events/event-bus.js';
|
|
13
|
+
import { validateBranchName } from '../../lib/branch/index.js';
|
|
14
|
+
import { PMO_TABLES } from '../../lib/pmo/schema.js';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
/**
|
|
17
|
+
* Sleep for the given number of milliseconds.
|
|
18
|
+
*/
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
export default class WorkShip extends PMOCommand {
|
|
23
|
+
static description = 'Ship work: rebase, merge PR, and move ticket to Done';
|
|
24
|
+
static examples = [
|
|
25
|
+
'<%= config.bin %> <%= command.id %> TKT-001',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> PRLT-1092',
|
|
27
|
+
'<%= config.bin %> <%= command.id %> --pr 929',
|
|
28
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --dry-run',
|
|
29
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --wait',
|
|
30
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --no-rebase',
|
|
31
|
+
];
|
|
32
|
+
static args = {
|
|
33
|
+
ticketId: Args.string({
|
|
34
|
+
description: 'Ticket ID - prompts with dropdown if not provided',
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
static flags = {
|
|
39
|
+
...pmoBaseFlags,
|
|
40
|
+
pr: Flags.integer({
|
|
41
|
+
description: 'PR number to merge (alternative to ticket ID lookup)',
|
|
42
|
+
required: false,
|
|
43
|
+
}),
|
|
44
|
+
method: Flags.string({
|
|
45
|
+
description: 'Merge method',
|
|
46
|
+
options: ['merge', 'squash', 'rebase'],
|
|
47
|
+
default: 'squash',
|
|
48
|
+
}),
|
|
49
|
+
wait: Flags.boolean({
|
|
50
|
+
description: 'Wait for CI to go green before merging',
|
|
51
|
+
default: false,
|
|
52
|
+
}),
|
|
53
|
+
'no-rebase': Flags.boolean({
|
|
54
|
+
description: 'Fail on conflicts instead of auto-rebasing',
|
|
55
|
+
default: false,
|
|
56
|
+
}),
|
|
57
|
+
'dry-run': Flags.boolean({
|
|
58
|
+
description: 'Show what would happen without doing it',
|
|
59
|
+
default: false,
|
|
60
|
+
}),
|
|
61
|
+
'delete-branch': Flags.boolean({
|
|
62
|
+
description: 'Delete remote branch after merging',
|
|
63
|
+
default: true,
|
|
64
|
+
allowNo: true,
|
|
65
|
+
}),
|
|
66
|
+
admin: Flags.boolean({
|
|
67
|
+
description: 'Use admin privileges to bypass branch protections',
|
|
68
|
+
default: false,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
async execute() {
|
|
72
|
+
const { args, flags } = await this.parse(WorkShip);
|
|
73
|
+
const projectId = flags.project;
|
|
74
|
+
const jsonMode = shouldOutputJson(flags);
|
|
75
|
+
const handleError = (code, message) => {
|
|
76
|
+
if (jsonMode) {
|
|
77
|
+
outputErrorAsJson(code, message, createMetadata('work ship', flags));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.error(message);
|
|
81
|
+
};
|
|
82
|
+
// Check gh CLI
|
|
83
|
+
if (!isGHInstalled()) {
|
|
84
|
+
return handleError('GH_NOT_INSTALLED', 'GitHub CLI (gh) is not installed. Install it from https://cli.github.com/');
|
|
85
|
+
}
|
|
86
|
+
if (!isGHAuthenticated()) {
|
|
87
|
+
return handleError('GH_NOT_AUTHENTICATED', 'GitHub CLI is not authenticated. Run "gh auth login" first.');
|
|
88
|
+
}
|
|
89
|
+
// Get workspace info
|
|
90
|
+
let workspaceInfo;
|
|
91
|
+
try {
|
|
92
|
+
workspaceInfo = getWorkspaceInfo();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return handleError('NOT_IN_WORKSPACE', 'Not in a workspace. Run "prlt new" first.');
|
|
96
|
+
}
|
|
97
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
98
|
+
const db = new Database(dbPath);
|
|
99
|
+
const executionStorage = new ExecutionStorage(db);
|
|
100
|
+
try {
|
|
101
|
+
// --- Step 1: Resolve ticket and PR ---
|
|
102
|
+
let ticketId = args.ticketId;
|
|
103
|
+
let prNumber = flags.pr;
|
|
104
|
+
let ticket = null;
|
|
105
|
+
let prInfo = null;
|
|
106
|
+
if (prNumber) {
|
|
107
|
+
// PR number provided directly — find the PR, then resolve the ticket
|
|
108
|
+
prInfo = getPRByNumber(prNumber);
|
|
109
|
+
if (!prInfo) {
|
|
110
|
+
db.close();
|
|
111
|
+
return handleError('PR_NOT_FOUND', `PR #${prNumber} not found.`);
|
|
112
|
+
}
|
|
113
|
+
// Try to resolve the linked ticket
|
|
114
|
+
ticket = await this.resolveLinkedTicket(prNumber, prInfo.headBranch, projectId);
|
|
115
|
+
if (ticket) {
|
|
116
|
+
ticketId = ticket.id;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Resolve ticket first
|
|
121
|
+
if (!ticketId) {
|
|
122
|
+
// Prompt for ticket selection from shippable tickets (in review or in progress)
|
|
123
|
+
const allTickets = await this.storage.listTickets(projectId);
|
|
124
|
+
const shippableTickets = allTickets.filter(t => t.statusCategory === 'started' ||
|
|
125
|
+
(t.statusName && (t.statusName.toLowerCase().includes('progress') ||
|
|
126
|
+
t.statusName.toLowerCase().includes('review'))));
|
|
127
|
+
if (shippableTickets.length === 0) {
|
|
128
|
+
db.close();
|
|
129
|
+
if (jsonMode) {
|
|
130
|
+
outputErrorAsJson('NO_SHIPPABLE_WORK', 'No in-progress or in-review work found.', createMetadata('work ship', flags));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.log(styles.info('No in-progress or in-review work found.'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const selected = await this.selectFromList({
|
|
137
|
+
message: 'Select work to ship:',
|
|
138
|
+
items: shippableTickets,
|
|
139
|
+
getName: (t) => `${t.id} - ${t.title} (${t.statusName})`,
|
|
140
|
+
getValue: (t) => t.id,
|
|
141
|
+
getCommand: (t) => `prlt work ship ${t.id} --json`,
|
|
142
|
+
jsonMode: jsonMode ? { flags, commandName: 'work ship' } : null,
|
|
143
|
+
});
|
|
144
|
+
if (!selected) {
|
|
145
|
+
db.close();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
ticketId = selected;
|
|
149
|
+
}
|
|
150
|
+
// Get ticket
|
|
151
|
+
ticket = await this.storage.getTicket(ticketId);
|
|
152
|
+
if (!ticket) {
|
|
153
|
+
db.close();
|
|
154
|
+
return handleError('TICKET_NOT_FOUND', `Ticket "${ticketId}" not found.`);
|
|
155
|
+
}
|
|
156
|
+
ticketId = ticket.id;
|
|
157
|
+
// Find PR from ticket metadata or branch
|
|
158
|
+
prNumber = ticket.metadata?.pr_number ? parseInt(ticket.metadata.pr_number, 10) : undefined;
|
|
159
|
+
if (prNumber) {
|
|
160
|
+
prInfo = getPRByNumber(prNumber);
|
|
161
|
+
}
|
|
162
|
+
if (!prInfo) {
|
|
163
|
+
// Try to find PR from execution branch
|
|
164
|
+
const runningExecution = executionStorage.getRunningExecution(ticketId);
|
|
165
|
+
const branch = runningExecution?.branch || ticket.branch;
|
|
166
|
+
if (branch) {
|
|
167
|
+
prInfo = getPRForBranch(branch);
|
|
168
|
+
if (prInfo) {
|
|
169
|
+
prNumber = prInfo.number;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!prInfo || !prNumber) {
|
|
174
|
+
db.close();
|
|
175
|
+
return handleError('NO_PR_FOUND', `No pull request found for ticket "${ticketId}". Create one with "prlt work ready ${ticketId} --pr".`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Validate PR state
|
|
179
|
+
if (prInfo.state === 'MERGED') {
|
|
180
|
+
db.close();
|
|
181
|
+
if (jsonMode) {
|
|
182
|
+
outputSuccessAsJson({ alreadyMerged: true, prNumber, title: prInfo.title, ticketId: ticketId ?? null }, createMetadata('work ship', flags));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.log(styles.info(`PR #${prNumber} is already merged.`));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (prInfo.state === 'CLOSED') {
|
|
189
|
+
db.close();
|
|
190
|
+
return handleError('PR_CLOSED', `PR #${prNumber} is closed. Reopen it first.`);
|
|
191
|
+
}
|
|
192
|
+
// Resolve the working directory for git operations
|
|
193
|
+
const cwd = this.resolveWorktreePath(workspaceInfo, executionStorage, ticketId);
|
|
194
|
+
// --- Dry run summary ---
|
|
195
|
+
if (flags['dry-run']) {
|
|
196
|
+
this.log('');
|
|
197
|
+
this.log(styles.info('Dry run — no changes will be made:'));
|
|
198
|
+
this.log('');
|
|
199
|
+
this.log(styles.muted(` PR: #${prNumber} — ${prInfo.title}`));
|
|
200
|
+
this.log(styles.muted(` Branch: ${prInfo.headBranch} → ${prInfo.baseBranch}`));
|
|
201
|
+
this.log(styles.muted(` Method: ${flags.method}`));
|
|
202
|
+
if (ticketId) {
|
|
203
|
+
this.log(styles.muted(` Ticket: ${ticketId}${ticket ? ` — ${ticket.title}` : ''}`));
|
|
204
|
+
}
|
|
205
|
+
// Check CI
|
|
206
|
+
const checks = getPRChecks(prNumber, cwd);
|
|
207
|
+
if (checks.length > 0) {
|
|
208
|
+
const allPassed = checks.every(c => c.conclusion === 'SUCCESS' || c.conclusion === 'SKIPPED');
|
|
209
|
+
const pending = checks.filter(c => !c.conclusion || c.status === 'IN_PROGRESS' || c.status === 'QUEUED');
|
|
210
|
+
const failed = checks.filter(c => c.conclusion === 'FAILURE');
|
|
211
|
+
if (allPassed) {
|
|
212
|
+
this.log(styles.muted(` CI: All checks passed (${checks.length})`));
|
|
213
|
+
}
|
|
214
|
+
else if (pending.length > 0) {
|
|
215
|
+
this.log(styles.muted(` CI: ${pending.length} pending, ${failed.length} failed`));
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.log(styles.muted(` CI: ${failed.length} failed`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.log(styles.muted(` CI: No checks configured`));
|
|
223
|
+
}
|
|
224
|
+
// Check conflicts
|
|
225
|
+
const hasConflicts = this.checkMergeConflicts(prNumber, cwd);
|
|
226
|
+
this.log(styles.muted(` Conflicts: ${hasConflicts ? 'Yes — would rebase' : 'None'}`));
|
|
227
|
+
this.log('');
|
|
228
|
+
this.log(styles.muted(' Actions that would be taken:'));
|
|
229
|
+
if (hasConflicts && !flags['no-rebase']) {
|
|
230
|
+
this.log(styles.muted(' 1. Rebase onto base branch and force-push'));
|
|
231
|
+
this.log(styles.muted(' 2. Wait for CI to rerun'));
|
|
232
|
+
}
|
|
233
|
+
this.log(styles.muted(` ${hasConflicts && !flags['no-rebase'] ? '3' : '1'}. Squash merge PR #${prNumber}`));
|
|
234
|
+
if (ticketId) {
|
|
235
|
+
this.log(styles.muted(` ${hasConflicts && !flags['no-rebase'] ? '4' : '2'}. Move ticket ${ticketId} to Done`));
|
|
236
|
+
}
|
|
237
|
+
if (flags['delete-branch']) {
|
|
238
|
+
this.log(styles.muted(` ${hasConflicts && !flags['no-rebase'] ? '5' : '3'}. Delete remote branch ${prInfo.headBranch}`));
|
|
239
|
+
}
|
|
240
|
+
db.close();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// --- Step 2: Check CI status ---
|
|
244
|
+
this.log('');
|
|
245
|
+
this.log(styles.info(`Shipping PR #${prNumber}: ${prInfo.title}`));
|
|
246
|
+
const ciResult = await this.waitForCI(prNumber, flags.wait, cwd);
|
|
247
|
+
if (!ciResult.passed) {
|
|
248
|
+
db.close();
|
|
249
|
+
return handleError('CI_FAILED', ciResult.message);
|
|
250
|
+
}
|
|
251
|
+
// --- Step 3: Check for merge conflicts and rebase if needed ---
|
|
252
|
+
const hasConflicts = this.checkMergeConflicts(prNumber, cwd);
|
|
253
|
+
if (hasConflicts) {
|
|
254
|
+
if (flags['no-rebase']) {
|
|
255
|
+
db.close();
|
|
256
|
+
return handleError('MERGE_CONFLICTS', `PR #${prNumber} has merge conflicts. Resolve them manually or run without --no-rebase to auto-rebase.`);
|
|
257
|
+
}
|
|
258
|
+
this.log(styles.muted(' Merge conflicts detected, rebasing...'));
|
|
259
|
+
const rebaseResult = this.rebaseOntoBase(prInfo.headBranch, prInfo.baseBranch, cwd);
|
|
260
|
+
if (!rebaseResult.success) {
|
|
261
|
+
db.close();
|
|
262
|
+
return handleError('REBASE_FAILED', `Rebase failed: ${rebaseResult.error}. Resolve conflicts manually.`);
|
|
263
|
+
}
|
|
264
|
+
this.log(styles.muted(' Rebased and force-pushed. Waiting for CI...'));
|
|
265
|
+
// Wait for CI to rerun after rebase
|
|
266
|
+
const postRebaseCI = await this.waitForCI(prNumber, true, cwd);
|
|
267
|
+
if (!postRebaseCI.passed) {
|
|
268
|
+
db.close();
|
|
269
|
+
return handleError('CI_FAILED_AFTER_REBASE', postRebaseCI.message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// --- Step 4: Merge the PR ---
|
|
273
|
+
const method = flags.method;
|
|
274
|
+
this.log(styles.muted(` Merging PR #${prNumber} (${method})...`));
|
|
275
|
+
const mergeResult = mergePR(prNumber, {
|
|
276
|
+
method,
|
|
277
|
+
deleteBranch: flags['delete-branch'],
|
|
278
|
+
admin: flags.admin,
|
|
279
|
+
cwd,
|
|
280
|
+
});
|
|
281
|
+
if (!mergeResult.success) {
|
|
282
|
+
db.close();
|
|
283
|
+
return handleError('MERGE_FAILED', `Failed to merge PR #${prNumber}: ${mergeResult.error}`);
|
|
284
|
+
}
|
|
285
|
+
this.log(styles.success(` PR #${prNumber} merged`));
|
|
286
|
+
// --- Step 5: Move ticket to Done ---
|
|
287
|
+
let ticketMovedToDone = false;
|
|
288
|
+
let ticketTransitionProvider;
|
|
289
|
+
let previousColumn;
|
|
290
|
+
let doneColumn;
|
|
291
|
+
if (ticket && ticketId) {
|
|
292
|
+
// Update PR metadata
|
|
293
|
+
await this.storage.updateTicket(ticketId, {
|
|
294
|
+
metadata: {
|
|
295
|
+
...ticket.metadata,
|
|
296
|
+
pr_state: 'MERGED',
|
|
297
|
+
merged_at: new Date().toISOString(),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
const targetColumnName = getWorkColumnSetting(db, 'done');
|
|
302
|
+
const board = ticket.projectId ? await this.storage.getProjectBoard(ticket.projectId) : null;
|
|
303
|
+
const columnNames = board ? board.columns.map(col => col.name) : [];
|
|
304
|
+
doneColumn = findColumnByName(columnNames, targetColumnName);
|
|
305
|
+
if (doneColumn && ticket.projectId) {
|
|
306
|
+
previousColumn = ticket.statusName;
|
|
307
|
+
await this.storage.moveTicket(ticket.projectId, ticketId, doneColumn);
|
|
308
|
+
ticketMovedToDone = true;
|
|
309
|
+
// Sync to external provider
|
|
310
|
+
try {
|
|
311
|
+
const provider = await this.resolveTicketProvider(ticketId, ticket.projectId);
|
|
312
|
+
if (provider.name !== 'pmo') {
|
|
313
|
+
const moveResult = await provider.moveTicket(ticketId, doneColumn);
|
|
314
|
+
if (moveResult.success) {
|
|
315
|
+
ticketTransitionProvider = moveResult.provider;
|
|
316
|
+
this.log(styles.muted(` Synced to ${moveResult.provider}: ${doneColumn}`));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Non-fatal
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
this.warn(`Failed to move ticket ${ticketId} to Done: ${err instanceof Error ? err.message : err}`);
|
|
327
|
+
}
|
|
328
|
+
// Auto-export board
|
|
329
|
+
await autoExportToBoard(this.pmoPath, this.storage);
|
|
330
|
+
// Mark execution as completed
|
|
331
|
+
const runningExecution = executionStorage.getRunningExecution(ticketId);
|
|
332
|
+
if (runningExecution) {
|
|
333
|
+
executionStorage.updateStatus(runningExecution.id, 'completed');
|
|
334
|
+
this.log(styles.muted(` Execution ${runningExecution.id} marked as completed`));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// --- Step 6: Emit event ---
|
|
338
|
+
if (ticketId) {
|
|
339
|
+
try {
|
|
340
|
+
const repo = getGitHubRepo(cwd);
|
|
341
|
+
const prUrl = repo ? `https://github.com/${repo}/pull/${prNumber}` : null;
|
|
342
|
+
getEventBus().emit('work:pr_merged', {
|
|
343
|
+
workItemId: ticketId,
|
|
344
|
+
source: 'github',
|
|
345
|
+
prNumber,
|
|
346
|
+
prTitle: prInfo.title,
|
|
347
|
+
prUrl,
|
|
348
|
+
mergeMethod: method,
|
|
349
|
+
timestamp: new Date(),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Non-critical
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
db.close();
|
|
357
|
+
// --- Output ---
|
|
358
|
+
if (jsonMode) {
|
|
359
|
+
outputSuccessAsJson({
|
|
360
|
+
shipped: true,
|
|
361
|
+
prNumber,
|
|
362
|
+
prTitle: prInfo.title,
|
|
363
|
+
method,
|
|
364
|
+
branchDeleted: flags['delete-branch'],
|
|
365
|
+
ticketId: ticketId ?? null,
|
|
366
|
+
ticketMovedToDone,
|
|
367
|
+
ticketTransitionProvider: ticketTransitionProvider ?? null,
|
|
368
|
+
}, createMetadata('work ship', flags));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.log('');
|
|
372
|
+
this.log(styles.success(`Shipped: ${ticketId ?? `PR #${prNumber}`}`));
|
|
373
|
+
this.log(styles.muted(` PR: #${prNumber} — ${prInfo.title}`));
|
|
374
|
+
this.log(styles.muted(` Method: ${method}`));
|
|
375
|
+
if (flags['delete-branch']) {
|
|
376
|
+
this.log(styles.muted(` Branch: ${prInfo.headBranch} deleted`));
|
|
377
|
+
}
|
|
378
|
+
if (ticketMovedToDone && ticketId) {
|
|
379
|
+
this.log(styles.muted(` Ticket: ${ticketId} → ${doneColumn}`));
|
|
380
|
+
if (previousColumn) {
|
|
381
|
+
this.log(styles.muted(` From: ${previousColumn}`));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
db.close();
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Resolve the linked ticket for a PR.
|
|
392
|
+
*/
|
|
393
|
+
async resolveLinkedTicket(prNumber, headBranch, projectId) {
|
|
394
|
+
// 1. Find by PR metadata on tickets
|
|
395
|
+
const allTickets = await this.storage.listTickets(projectId);
|
|
396
|
+
const byMetadata = allTickets.find(t => t.metadata?.pr_number === String(prNumber) ||
|
|
397
|
+
t.metadata?.pr_url?.endsWith(`/pull/${prNumber}`) ||
|
|
398
|
+
t.metadata?.pr_url?.endsWith(`/${prNumber}`));
|
|
399
|
+
if (byMetadata)
|
|
400
|
+
return byMetadata;
|
|
401
|
+
// 2. Look up from agent_work table by branch name
|
|
402
|
+
try {
|
|
403
|
+
const db = this.storage.getDatabase();
|
|
404
|
+
const row = db.prepare(`SELECT ticket_id FROM ${PMO_TABLES.agent_work} WHERE branch = ? LIMIT 1`).get(headBranch);
|
|
405
|
+
if (row?.ticket_id) {
|
|
406
|
+
const ticket = await this.storage.getTicket(row.ticket_id);
|
|
407
|
+
if (ticket)
|
|
408
|
+
return ticket;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// agent_work lookup failed
|
|
413
|
+
}
|
|
414
|
+
// 3. Parse ticket ID from branch name
|
|
415
|
+
const branchResult = validateBranchName(headBranch);
|
|
416
|
+
if (branchResult.valid && branchResult.parts?.ticketId) {
|
|
417
|
+
const branchTicketId = branchResult.parts.ticketId;
|
|
418
|
+
const directTicket = await this.storage.getTicket(branchTicketId);
|
|
419
|
+
if (directTicket)
|
|
420
|
+
return directTicket;
|
|
421
|
+
const byExternalKey = allTickets.find(t => t.metadata?.external_key === branchTicketId);
|
|
422
|
+
if (byExternalKey)
|
|
423
|
+
return byExternalKey;
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Wait for CI checks to pass. Returns immediately if all passed.
|
|
429
|
+
* If --wait is set, polls until all complete. Otherwise warns on pending.
|
|
430
|
+
*/
|
|
431
|
+
async waitForCI(prNumber, shouldWait, cwd) {
|
|
432
|
+
const maxAttempts = 120; // 30 minutes max (15s intervals)
|
|
433
|
+
let attempts = 0;
|
|
434
|
+
while (attempts < maxAttempts) {
|
|
435
|
+
const checks = getPRChecks(prNumber, cwd);
|
|
436
|
+
// No checks configured — pass through
|
|
437
|
+
if (checks.length === 0) {
|
|
438
|
+
this.log(styles.muted(' No CI checks configured'));
|
|
439
|
+
return { passed: true, message: '' };
|
|
440
|
+
}
|
|
441
|
+
const failed = checks.filter(c => c.conclusion === 'FAILURE');
|
|
442
|
+
const pending = checks.filter(c => !c.conclusion || c.status === 'IN_PROGRESS' || c.status === 'QUEUED');
|
|
443
|
+
const passed = checks.filter(c => c.conclusion === 'SUCCESS' || c.conclusion === 'SKIPPED');
|
|
444
|
+
// All done
|
|
445
|
+
if (pending.length === 0) {
|
|
446
|
+
if (failed.length > 0) {
|
|
447
|
+
const failedNames = failed.map(c => c.name).join(', ');
|
|
448
|
+
return {
|
|
449
|
+
passed: false,
|
|
450
|
+
message: `CI checks failed: ${failedNames}. Fix the failures before shipping.`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
this.log(styles.muted(` CI: ${passed.length} checks passed`));
|
|
454
|
+
return { passed: true, message: '' };
|
|
455
|
+
}
|
|
456
|
+
// Checks still running
|
|
457
|
+
if (!shouldWait) {
|
|
458
|
+
const pendingNames = pending.map(c => c.name).join(', ');
|
|
459
|
+
return {
|
|
460
|
+
passed: false,
|
|
461
|
+
message: `CI checks still running: ${pendingNames}. Use --wait to wait for them, or re-run after they complete.`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
// Wait and retry
|
|
465
|
+
if (attempts === 0) {
|
|
466
|
+
this.log(styles.muted(` Waiting for CI (${pending.length} pending)...`));
|
|
467
|
+
}
|
|
468
|
+
attempts++;
|
|
469
|
+
await sleep(15_000);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
passed: false,
|
|
473
|
+
message: 'Timed out waiting for CI checks (30 minutes). Check manually.',
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Check if the PR has merge conflicts with the base branch.
|
|
478
|
+
*/
|
|
479
|
+
checkMergeConflicts(prNumber, cwd) {
|
|
480
|
+
try {
|
|
481
|
+
const result = execSync(`gh pr view ${prNumber} --json mergeable -q .mergeable`, {
|
|
482
|
+
cwd,
|
|
483
|
+
encoding: 'utf-8',
|
|
484
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
485
|
+
}).trim();
|
|
486
|
+
return result === 'CONFLICTING';
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
// If we can't determine, assume no conflicts
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Rebase the PR branch onto the base branch and force-push.
|
|
495
|
+
*/
|
|
496
|
+
rebaseOntoBase(headBranch, baseBranch, cwd) {
|
|
497
|
+
try {
|
|
498
|
+
// Fetch latest from origin
|
|
499
|
+
execSync('git fetch origin', {
|
|
500
|
+
cwd,
|
|
501
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
502
|
+
});
|
|
503
|
+
// Check out the head branch
|
|
504
|
+
execSync(`git checkout ${headBranch}`, {
|
|
505
|
+
cwd,
|
|
506
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
507
|
+
});
|
|
508
|
+
// Rebase onto base
|
|
509
|
+
execSync(`git rebase origin/${baseBranch}`, {
|
|
510
|
+
cwd,
|
|
511
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
512
|
+
});
|
|
513
|
+
// Force-push
|
|
514
|
+
execSync(`git push --force-with-lease origin ${headBranch}`, {
|
|
515
|
+
cwd,
|
|
516
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
517
|
+
});
|
|
518
|
+
return { success: true };
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
// Abort rebase on failure
|
|
522
|
+
try {
|
|
523
|
+
execSync('git rebase --abort', {
|
|
524
|
+
cwd,
|
|
525
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
// Ignore abort failures
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
error: error instanceof Error ? error.message : 'Unknown rebase error',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Resolve the working directory for git operations.
|
|
539
|
+
* Tries: devcontainer repo dirs, execution worktree, agent dirs.
|
|
540
|
+
*/
|
|
541
|
+
resolveWorktreePath(workspaceInfo, executionStorage, ticketId) {
|
|
542
|
+
// In devcontainer, find repo directories
|
|
543
|
+
if (process.env.DEVCONTAINER === 'true') {
|
|
544
|
+
try {
|
|
545
|
+
const entries = fs.readdirSync('/workspace', { withFileTypes: true });
|
|
546
|
+
const repoDirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules');
|
|
547
|
+
if (repoDirs.length > 0) {
|
|
548
|
+
return path.join('/workspace', repoDirs[0].name);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// Fall through
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Try execution record
|
|
556
|
+
if (ticketId) {
|
|
557
|
+
const execution = executionStorage.getRunningExecution(ticketId);
|
|
558
|
+
if (execution?.agentName) {
|
|
559
|
+
const agentRecord = workspaceInfo.agents.find(a => a.name === execution.agentName);
|
|
560
|
+
let agentDir;
|
|
561
|
+
if (agentRecord?.worktree_path) {
|
|
562
|
+
agentDir = path.join(workspaceInfo.path, agentRecord.worktree_path);
|
|
563
|
+
}
|
|
564
|
+
else if (agentRecord?.type === 'ephemeral') {
|
|
565
|
+
agentDir = path.join(workspaceInfo.path, 'agents', workspaceInfo.ephemeralAgentsDir, execution.agentName);
|
|
566
|
+
}
|
|
567
|
+
else if (agentRecord) {
|
|
568
|
+
agentDir = path.join(workspaceInfo.path, 'agents', workspaceInfo.persistentAgentsDir, execution.agentName);
|
|
569
|
+
}
|
|
570
|
+
if (agentDir && fs.existsSync(agentDir)) {
|
|
571
|
+
try {
|
|
572
|
+
const entries = fs.readdirSync(agentDir, { withFileTypes: true });
|
|
573
|
+
const repoDirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules');
|
|
574
|
+
if (repoDirs.length > 0) {
|
|
575
|
+
return path.join(agentDir, repoDirs[0].name);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// Fall through
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
//# sourceMappingURL=ship.js.map
|