@proletariat/cli 0.3.51 → 0.3.52
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/agent/status.js +1 -0
- package/dist/commands/asana/connect.d.ts +15 -0
- package/dist/commands/asana/connect.js +267 -0
- package/dist/commands/asana/sync.d.ts +15 -0
- package/dist/commands/asana/sync.js +189 -0
- package/dist/commands/config/index.js +7 -1
- package/dist/commands/execution/list.js +3 -0
- package/dist/commands/execution/view.js +10 -0
- package/dist/commands/monday/connect.d.ts +16 -0
- package/dist/commands/monday/connect.js +212 -0
- package/dist/commands/monday/sync.d.ts +14 -0
- package/dist/commands/monday/sync.js +178 -0
- package/dist/commands/orchestrator/start.d.ts +6 -0
- package/dist/commands/orchestrator/start.js +149 -11
- package/dist/commands/session/list.js +6 -5
- package/dist/commands/work/index.js +7 -0
- package/dist/commands/work/jira.d.ts +28 -0
- package/dist/commands/work/jira.js +225 -0
- package/dist/commands/work/source/set.d.ts +12 -0
- package/dist/commands/work/source/set.js +52 -0
- package/dist/commands/work/source.d.ts +11 -0
- package/dist/commands/work/source.js +53 -0
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +73 -8
- package/dist/commands/work/start.d.ts +8 -0
- package/dist/commands/work/start.js +241 -3
- package/dist/lib/asana/client.d.ts +15 -0
- package/dist/lib/asana/client.js +120 -0
- package/dist/lib/asana/config.d.ts +9 -0
- package/dist/lib/asana/config.js +61 -0
- package/dist/lib/asana/index.d.ts +5 -0
- package/dist/lib/asana/index.js +4 -0
- package/dist/lib/asana/mapper.d.ts +13 -0
- package/dist/lib/asana/mapper.js +70 -0
- package/dist/lib/asana/sync.d.ts +13 -0
- package/dist/lib/asana/sync.js +36 -0
- package/dist/lib/asana/types.d.ts +40 -0
- package/dist/lib/asana/types.js +1 -0
- package/dist/lib/database/drizzle-schema.d.ts +393 -0
- package/dist/lib/database/drizzle-schema.js +45 -0
- package/dist/lib/execution/config.d.ts +10 -0
- package/dist/lib/execution/config.js +19 -0
- package/dist/lib/execution/runners.d.ts +10 -0
- package/dist/lib/execution/runners.js +110 -1
- package/dist/lib/execution/spawner.js +26 -0
- package/dist/lib/execution/storage.d.ts +4 -0
- package/dist/lib/execution/storage.js +8 -3
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/external-issues/adapters.d.ts +18 -1
- package/dist/lib/external-issues/adapters.js +49 -1
- package/dist/lib/external-issues/index.d.ts +4 -1
- package/dist/lib/external-issues/index.js +5 -0
- package/dist/lib/external-issues/jira.d.ts +23 -0
- package/dist/lib/external-issues/jira.js +223 -0
- package/dist/lib/external-issues/linear.js +4 -3
- package/dist/lib/external-issues/mapper.d.ts +3 -2
- package/dist/lib/external-issues/mapper.js +5 -2
- package/dist/lib/external-issues/mapping-store.d.ts +12 -0
- package/dist/lib/external-issues/mapping-store.js +164 -0
- package/dist/lib/external-issues/types.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +11 -0
- package/dist/lib/external-issues/work-start.d.ts +10 -0
- package/dist/lib/external-issues/work-start.js +12 -0
- package/dist/lib/linear/mapper.d.ts +2 -0
- package/dist/lib/linear/mapper.js +66 -2
- package/dist/lib/monday/client.d.ts +14 -0
- package/dist/lib/monday/client.js +113 -0
- package/dist/lib/monday/config.d.ts +10 -0
- package/dist/lib/monday/config.js +64 -0
- package/dist/lib/monday/index.d.ts +5 -0
- package/dist/lib/monday/index.js +4 -0
- package/dist/lib/monday/mapper.d.ts +14 -0
- package/dist/lib/monday/mapper.js +89 -0
- package/dist/lib/monday/sync.d.ts +13 -0
- package/dist/lib/monday/sync.js +45 -0
- package/dist/lib/monday/types.d.ts +38 -0
- package/dist/lib/monday/types.js +4 -0
- package/dist/lib/pmo/schema.d.ts +10 -1
- package/dist/lib/pmo/schema.js +73 -0
- package/dist/lib/pmo/storage/base.js +32 -0
- package/dist/lib/prompt-json.d.ts +11 -0
- package/dist/lib/work-source/config.d.ts +14 -0
- package/dist/lib/work-source/config.js +70 -0
- package/dist/lib/work-source/index.d.ts +1 -0
- package/dist/lib/work-source/index.js +1 -0
- package/oclif.manifest.json +2584 -2017
- package/package.json +1 -1
|
@@ -13,10 +13,14 @@ import { getWorkspaceInfo, createEphemeralAgent, getTicketTmuxSession, killTmuxS
|
|
|
13
13
|
import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
|
|
14
14
|
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, dockerCredentialsExist, getDockerCredentialInfo, isClaudeExecutor, getExecutorDisplayName } from '../../lib/execution/runners.js';
|
|
15
15
|
import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.js';
|
|
16
|
-
import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName, getAuthMethod, saveAuthMethod, getCreatePrDefault } from '../../lib/execution/config.js';
|
|
16
|
+
import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName, getAuthMethod, saveAuthMethod, getCreatePrDefault, getMirrorToPmoDefault } from '../../lib/execution/config.js';
|
|
17
17
|
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
|
|
18
18
|
import { detectRepoWorktrees, resolveWorktreePath } from '../../lib/execution/context.js';
|
|
19
19
|
import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
|
|
20
|
+
import { buildLinearMetadata, buildLinearSpawnContextMessage, buildLinearTicketDescription, getLinearIssueByIdentifier, } from '../../lib/external-issues/linear.js';
|
|
21
|
+
import { buildJiraMetadata, buildJiraSpawnContextMessage, buildJiraTicketDescription, getJiraIssueByKey, } from '../../lib/external-issues/jira.js';
|
|
22
|
+
import { resolveMirrorToPmo } from '../../lib/external-issues/work-start.js';
|
|
23
|
+
import { ExternalIssueAdapterError } from '../../lib/external-issues/types.js';
|
|
20
24
|
/**
|
|
21
25
|
* Try to execute a git command, return true if successful
|
|
22
26
|
*/
|
|
@@ -67,6 +71,49 @@ function getActiveStaffAgents(workspaceInfo, log) {
|
|
|
67
71
|
}
|
|
68
72
|
return result;
|
|
69
73
|
}
|
|
74
|
+
function parseBooleanSetting(value) {
|
|
75
|
+
if (!value)
|
|
76
|
+
return null;
|
|
77
|
+
const normalized = value.trim().toLowerCase();
|
|
78
|
+
if (normalized === 'true')
|
|
79
|
+
return true;
|
|
80
|
+
if (normalized === 'false')
|
|
81
|
+
return false;
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function isIssueSource(value) {
|
|
85
|
+
return value === 'linear' || value === 'jira';
|
|
86
|
+
}
|
|
87
|
+
function buildExternalMetadata(envelope) {
|
|
88
|
+
return envelope.source.name === 'jira'
|
|
89
|
+
? buildJiraMetadata(envelope)
|
|
90
|
+
: buildLinearMetadata(envelope);
|
|
91
|
+
}
|
|
92
|
+
function buildExternalTicketDescription(envelope) {
|
|
93
|
+
return envelope.source.name === 'jira'
|
|
94
|
+
? buildJiraTicketDescription(envelope)
|
|
95
|
+
: buildLinearTicketDescription(envelope);
|
|
96
|
+
}
|
|
97
|
+
function buildExternalSpawnContextMessage(envelope, additionalMessage) {
|
|
98
|
+
return envelope.source.name === 'jira'
|
|
99
|
+
? buildJiraSpawnContextMessage(envelope, additionalMessage)
|
|
100
|
+
: buildLinearSpawnContextMessage(envelope, additionalMessage);
|
|
101
|
+
}
|
|
102
|
+
function getTicketExternalMetadata(ticket) {
|
|
103
|
+
const metadata = (typeof ticket === 'object'
|
|
104
|
+
&& ticket !== null
|
|
105
|
+
&& 'metadata' in ticket
|
|
106
|
+
&& typeof ticket.metadata === 'object'
|
|
107
|
+
&& ticket.metadata !== null
|
|
108
|
+
? ticket.metadata
|
|
109
|
+
: {});
|
|
110
|
+
return {
|
|
111
|
+
source: typeof metadata.external_source === 'string' ? metadata.external_source : undefined,
|
|
112
|
+
key: typeof metadata.external_key === 'string' ? metadata.external_key : undefined,
|
|
113
|
+
id: typeof metadata.external_id === 'string' ? metadata.external_id : undefined,
|
|
114
|
+
url: typeof metadata.external_url === 'string' ? metadata.external_url : undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
70
117
|
export default class WorkStart extends PMOCommand {
|
|
71
118
|
static description = 'Start work on a ticket (launches an agent to implement it)';
|
|
72
119
|
static examples = [
|
|
@@ -78,6 +125,8 @@ export default class WorkStart extends PMOCommand {
|
|
|
78
125
|
'<%= config.bin %> <%= command.id %> # Interactive mode',
|
|
79
126
|
'<%= config.bin %> <%= command.id %> --all # Spawn all backlog tickets',
|
|
80
127
|
'<%= config.bin %> <%= command.id %> TKT-001 --prompt "Add unit tests for the API" # Custom prompt',
|
|
128
|
+
'<%= config.bin %> <%= command.id %> --from-issue --source linear --key ENG-123',
|
|
129
|
+
'<%= config.bin %> <%= command.id %> --from-issue --source jira --key PROJ-123 --mirror-to-pmo',
|
|
81
130
|
];
|
|
82
131
|
static args = {
|
|
83
132
|
ticketId: Args.string({
|
|
@@ -108,6 +157,21 @@ export default class WorkStart extends PMOCommand {
|
|
|
108
157
|
message: Flags.string({
|
|
109
158
|
description: 'Additional instructions appended to any action prompt',
|
|
110
159
|
}),
|
|
160
|
+
'from-issue': Flags.boolean({
|
|
161
|
+
description: 'Start from external issue source instead of internal ticket id',
|
|
162
|
+
default: false,
|
|
163
|
+
}),
|
|
164
|
+
source: Flags.string({
|
|
165
|
+
description: 'External issue source',
|
|
166
|
+
options: ['linear', 'jira'],
|
|
167
|
+
}),
|
|
168
|
+
key: Flags.string({
|
|
169
|
+
description: 'External issue key (for example: ENG-123, PROJ-456)',
|
|
170
|
+
}),
|
|
171
|
+
'mirror-to-pmo': Flags.boolean({
|
|
172
|
+
description: 'Mirror external issue data into PMO ticket (default from execution.mirror_to_pmo_default or PRLT_MIRROR_TO_PMO_DEFAULT)',
|
|
173
|
+
allowNo: true,
|
|
174
|
+
}),
|
|
111
175
|
watch: Flags.boolean({
|
|
112
176
|
char: 'w',
|
|
113
177
|
description: 'Stream output in real-time',
|
|
@@ -187,9 +251,95 @@ export default class WorkStart extends PMOCommand {
|
|
|
187
251
|
hidden: true,
|
|
188
252
|
}),
|
|
189
253
|
};
|
|
254
|
+
async findLinkedTicketByEnvelope(projectId, envelope) {
|
|
255
|
+
const tickets = await this.storage.listTickets(projectId);
|
|
256
|
+
return tickets.find((ticket) => {
|
|
257
|
+
const external = getTicketExternalMetadata(ticket);
|
|
258
|
+
return external.source === envelope.source.name
|
|
259
|
+
&& (external.key === envelope.source.externalKey || external.id === envelope.source.externalId);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async createOrUpdateLinkedTicket(projectId, envelope) {
|
|
263
|
+
const existing = await this.findLinkedTicketByEnvelope(projectId, envelope);
|
|
264
|
+
const description = buildExternalTicketDescription(envelope);
|
|
265
|
+
const metadata = buildExternalMetadata(envelope);
|
|
266
|
+
if (existing) {
|
|
267
|
+
return this.storage.updateTicket(existing.id, {
|
|
268
|
+
title: envelope.title,
|
|
269
|
+
description,
|
|
270
|
+
priority: envelope.priority ?? undefined,
|
|
271
|
+
category: envelope.category ?? undefined,
|
|
272
|
+
labels: envelope.labels,
|
|
273
|
+
metadata: {
|
|
274
|
+
...existing.metadata,
|
|
275
|
+
...metadata,
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return this.storage.createTicket(projectId, {
|
|
280
|
+
title: envelope.title,
|
|
281
|
+
description,
|
|
282
|
+
priority: envelope.priority ?? undefined,
|
|
283
|
+
category: envelope.category ?? undefined,
|
|
284
|
+
labels: envelope.labels,
|
|
285
|
+
metadata,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async fetchExternalIssue(source, key) {
|
|
289
|
+
if (source === 'jira') {
|
|
290
|
+
return getJiraIssueByKey({}, key);
|
|
291
|
+
}
|
|
292
|
+
return getLinearIssueByIdentifier({}, key);
|
|
293
|
+
}
|
|
294
|
+
async resolveIssueSourceAndKey(input, jsonMode) {
|
|
295
|
+
let source = input.source;
|
|
296
|
+
let key = input.key;
|
|
297
|
+
const sourceResolver = new FlagResolver({
|
|
298
|
+
commandName: 'work start',
|
|
299
|
+
baseCommand: 'prlt work start --from-issue',
|
|
300
|
+
jsonMode,
|
|
301
|
+
flags: {},
|
|
302
|
+
});
|
|
303
|
+
sourceResolver.addPrompt({
|
|
304
|
+
flagName: 'source',
|
|
305
|
+
type: 'list',
|
|
306
|
+
message: 'Select external issue source:',
|
|
307
|
+
default: isIssueSource(source) ? source : undefined,
|
|
308
|
+
when: () => !isIssueSource(source),
|
|
309
|
+
choices: () => [
|
|
310
|
+
{ name: 'Linear', value: 'linear', command: 'prlt work start --from-issue --source linear --json' },
|
|
311
|
+
{ name: 'Jira', value: 'jira', command: 'prlt work start --from-issue --source jira --json' },
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
const sourceResult = await sourceResolver.resolve();
|
|
315
|
+
source = source ?? sourceResult.source;
|
|
316
|
+
if (!isIssueSource(source)) {
|
|
317
|
+
throw new Error('Invalid source');
|
|
318
|
+
}
|
|
319
|
+
const keyResolver = new FlagResolver({
|
|
320
|
+
commandName: 'work start',
|
|
321
|
+
baseCommand: `prlt work start --from-issue --source ${source}`,
|
|
322
|
+
jsonMode,
|
|
323
|
+
flags: {},
|
|
324
|
+
});
|
|
325
|
+
keyResolver.addPrompt({
|
|
326
|
+
flagName: 'key',
|
|
327
|
+
type: 'input',
|
|
328
|
+
message: `Enter ${source === 'linear' ? 'Linear' : 'Jira'} issue key:`,
|
|
329
|
+
default: key,
|
|
330
|
+
when: () => !key?.trim(),
|
|
331
|
+
validate: (value) => value.trim().length > 0 ? true : 'Issue key is required',
|
|
332
|
+
});
|
|
333
|
+
const keyResult = await keyResolver.resolve();
|
|
334
|
+
const resolvedKey = (key ?? keyResult.key ?? '').trim();
|
|
335
|
+
if (!resolvedKey) {
|
|
336
|
+
throw new Error('Issue key is required');
|
|
337
|
+
}
|
|
338
|
+
return { source, key: resolvedKey };
|
|
339
|
+
}
|
|
190
340
|
async execute() {
|
|
191
341
|
const { args, flags } = await this.parse(WorkStart);
|
|
192
|
-
|
|
342
|
+
let projectId = flags.project;
|
|
193
343
|
// Check for conflicting PR flags
|
|
194
344
|
if (flags['create-pr'] && flags['no-pr']) {
|
|
195
345
|
this.error('--create-pr and --no-pr are mutually exclusive');
|
|
@@ -239,6 +389,71 @@ export default class WorkStart extends PMOCommand {
|
|
|
239
389
|
}
|
|
240
390
|
// Get ticketId - prompt if not provided
|
|
241
391
|
let ticketId = args.ticketId;
|
|
392
|
+
let externalIssueContextMessage;
|
|
393
|
+
let fromIssueMirror;
|
|
394
|
+
let fromIssueMirrorSource;
|
|
395
|
+
if (flags['from-issue']) {
|
|
396
|
+
if (ticketId) {
|
|
397
|
+
db.close();
|
|
398
|
+
return handleError('INVALID_FLAGS', 'Cannot provide a ticket ID positional argument when using --from-issue.');
|
|
399
|
+
}
|
|
400
|
+
projectId = projectId || await this.requireProject({
|
|
401
|
+
jsonMode: {
|
|
402
|
+
flags,
|
|
403
|
+
commandName: 'work start',
|
|
404
|
+
baseCommand: 'prlt work start --from-issue',
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
const sourceAndKey = await this.resolveIssueSourceAndKey({
|
|
408
|
+
source: flags.source,
|
|
409
|
+
key: flags.key,
|
|
410
|
+
}, jsonMode);
|
|
411
|
+
const envMirrorDefault = parseBooleanSetting(process.env.PRLT_MIRROR_TO_PMO_DEFAULT);
|
|
412
|
+
const configMirrorDefault = getMirrorToPmoDefault(db);
|
|
413
|
+
const mirrorResolution = resolveMirrorToPmo({
|
|
414
|
+
flagValue: flags['mirror-to-pmo'],
|
|
415
|
+
envValue: envMirrorDefault,
|
|
416
|
+
configValue: configMirrorDefault,
|
|
417
|
+
});
|
|
418
|
+
const mirrorToPmo = mirrorResolution.enabled;
|
|
419
|
+
fromIssueMirror = mirrorToPmo;
|
|
420
|
+
fromIssueMirrorSource = mirrorResolution.source;
|
|
421
|
+
if (!jsonMode) {
|
|
422
|
+
this.log(styles.muted(`External issue mirror: ${mirrorToPmo ? 'enabled' : 'disabled'} (${mirrorResolution.source})`));
|
|
423
|
+
}
|
|
424
|
+
let envelope = null;
|
|
425
|
+
try {
|
|
426
|
+
envelope = await this.fetchExternalIssue(sourceAndKey.source, sourceAndKey.key);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (error instanceof ExternalIssueAdapterError) {
|
|
430
|
+
db.close();
|
|
431
|
+
return handleError(`EXTERNAL_ISSUE_${error.code}`, `[${sourceAndKey.source}] ${error.message}`);
|
|
432
|
+
}
|
|
433
|
+
const message = error instanceof Error ? error.message : 'Failed to fetch external issue.';
|
|
434
|
+
db.close();
|
|
435
|
+
return handleError('EXTERNAL_ISSUE_REQUEST_FAILED', message);
|
|
436
|
+
}
|
|
437
|
+
if (!envelope) {
|
|
438
|
+
db.close();
|
|
439
|
+
return handleError('EXTERNAL_ISSUE_NOT_FOUND', `${sourceAndKey.source} issue "${sourceAndKey.key}" was not found.`);
|
|
440
|
+
}
|
|
441
|
+
const existingLinkedTicket = await this.findLinkedTicketByEnvelope(projectId, envelope);
|
|
442
|
+
let linkedTicket;
|
|
443
|
+
if (mirrorToPmo) {
|
|
444
|
+
linkedTicket = await this.createOrUpdateLinkedTicket(projectId, envelope);
|
|
445
|
+
await autoExportToBoard(this.pmoPath, this.storage);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
if (!existingLinkedTicket) {
|
|
449
|
+
db.close();
|
|
450
|
+
return handleError('EXTERNAL_ISSUE_NOT_MIRRORED', `No linked PMO ticket found for ${sourceAndKey.source} issue "${sourceAndKey.key}". Re-run with --mirror-to-pmo.`);
|
|
451
|
+
}
|
|
452
|
+
linkedTicket = existingLinkedTicket;
|
|
453
|
+
}
|
|
454
|
+
ticketId = linkedTicket.id;
|
|
455
|
+
externalIssueContextMessage = buildExternalSpawnContextMessage(envelope, flags.message);
|
|
456
|
+
}
|
|
242
457
|
if (!ticketId) {
|
|
243
458
|
// Get all tickets, optionally filtered by project if -P/--project flag is provided
|
|
244
459
|
const allTickets = await this.storage.listTickets(projectId);
|
|
@@ -285,6 +500,19 @@ export default class WorkStart extends PMOCommand {
|
|
|
285
500
|
: earlyConfigPrDefault === false ? 'no-pr'
|
|
286
501
|
: 'no-pr';
|
|
287
502
|
metadata.resolvedPRMode = earlyResolvedPr;
|
|
503
|
+
const externalMetadata = getTicketExternalMetadata(ticket);
|
|
504
|
+
if (externalMetadata.source || externalMetadata.key) {
|
|
505
|
+
metadata.externalIssue = {
|
|
506
|
+
source: externalMetadata.source ?? null,
|
|
507
|
+
key: externalMetadata.key ?? null,
|
|
508
|
+
id: externalMetadata.id ?? null,
|
|
509
|
+
url: externalMetadata.url ?? null,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (flags['from-issue']) {
|
|
513
|
+
metadata.mirrorToPmo = fromIssueMirror ?? null;
|
|
514
|
+
metadata.mirrorToPmoSource = fromIssueMirrorSource ?? null;
|
|
515
|
+
}
|
|
288
516
|
// Build the confirm command with --yes
|
|
289
517
|
let confirmCmd = `prlt work start ${ticketId}`;
|
|
290
518
|
if (flags.action)
|
|
@@ -796,7 +1024,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
796
1024
|
actionEndPrompt: customPrompt ? undefined : selectedAction?.endPrompt,
|
|
797
1025
|
modifiesCode: customPrompt ? true : selectedAction?.modifiesCode ?? true,
|
|
798
1026
|
// Additional instructions from --message flag
|
|
799
|
-
customMessage: flags.message,
|
|
1027
|
+
customMessage: externalIssueContextMessage ?? flags.message,
|
|
800
1028
|
};
|
|
801
1029
|
// Check if agent has devcontainer config
|
|
802
1030
|
const hasDevcontainer = hasDevcontainerConfig(agentDir);
|
|
@@ -1523,6 +1751,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
1523
1751
|
}
|
|
1524
1752
|
}
|
|
1525
1753
|
// Create execution record
|
|
1754
|
+
const ticketExternalMetadata = getTicketExternalMetadata(ticket);
|
|
1526
1755
|
const execution = executionStorage.createExecution({
|
|
1527
1756
|
ticketId: ticket.id,
|
|
1528
1757
|
agentName: assignedAgent,
|
|
@@ -1531,6 +1760,10 @@ export default class WorkStart extends PMOCommand {
|
|
|
1531
1760
|
displayMode,
|
|
1532
1761
|
permissionMode,
|
|
1533
1762
|
branch,
|
|
1763
|
+
externalSource: ticketExternalMetadata.source,
|
|
1764
|
+
externalKey: ticketExternalMetadata.key,
|
|
1765
|
+
externalId: ticketExternalMetadata.id,
|
|
1766
|
+
externalUrl: ticketExternalMetadata.url,
|
|
1534
1767
|
});
|
|
1535
1768
|
if (!jsonMode) {
|
|
1536
1769
|
this.log(styles.muted(` Work ID: ${execution.id}`));
|
|
@@ -2027,6 +2260,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
2027
2260
|
}
|
|
2028
2261
|
}
|
|
2029
2262
|
// Create execution record
|
|
2263
|
+
const ticketExternalMetadata = getTicketExternalMetadata(ticket);
|
|
2030
2264
|
const execution = executionStorage.createExecution({
|
|
2031
2265
|
ticketId: ticket.id,
|
|
2032
2266
|
agentName,
|
|
@@ -2035,6 +2269,10 @@ export default class WorkStart extends PMOCommand {
|
|
|
2035
2269
|
displayMode,
|
|
2036
2270
|
permissionMode,
|
|
2037
2271
|
branch,
|
|
2272
|
+
externalSource: ticketExternalMetadata.source,
|
|
2273
|
+
externalKey: ticketExternalMetadata.key,
|
|
2274
|
+
externalId: ticketExternalMetadata.id,
|
|
2275
|
+
externalUrl: ticketExternalMetadata.url,
|
|
2038
2276
|
});
|
|
2039
2277
|
// Note: Ticket status update moved to after successful spawn
|
|
2040
2278
|
// Load execution config
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AsanaProject, AsanaTask, AsanaTaskUpsertInput, AsanaUser, AsanaWorkspace } from './types.js';
|
|
2
|
+
export declare class AsanaClient {
|
|
3
|
+
private accessToken;
|
|
4
|
+
private readonly baseUrl;
|
|
5
|
+
constructor(accessToken: string);
|
|
6
|
+
private request;
|
|
7
|
+
verify(): Promise<AsanaUser>;
|
|
8
|
+
listWorkspaces(): Promise<AsanaWorkspace[]>;
|
|
9
|
+
getWorkspace(workspaceGid: string): Promise<AsanaWorkspace | null>;
|
|
10
|
+
listProjects(workspaceGid: string): Promise<AsanaProject[]>;
|
|
11
|
+
getProject(projectGid: string): Promise<AsanaProject | null>;
|
|
12
|
+
getTask(taskGid: string): Promise<AsanaTask | null>;
|
|
13
|
+
createTask(input: AsanaTaskUpsertInput): Promise<AsanaTask>;
|
|
14
|
+
updateTask(taskGid: string, input: AsanaTaskUpsertInput): Promise<AsanaTask>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class AsanaClient {
|
|
2
|
+
accessToken;
|
|
3
|
+
baseUrl = 'https://app.asana.com/api/1.0';
|
|
4
|
+
constructor(accessToken) {
|
|
5
|
+
this.accessToken = accessToken;
|
|
6
|
+
}
|
|
7
|
+
async request(path, init = {}) {
|
|
8
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
9
|
+
if (init.query) {
|
|
10
|
+
for (const [key, value] of Object.entries(init.query)) {
|
|
11
|
+
if (value !== undefined) {
|
|
12
|
+
url.searchParams.set(key, String(value));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- fetch is available in supported Node runtimes for this CLI
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: init.method ?? 'GET',
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
},
|
|
23
|
+
body: init.body ? JSON.stringify(init.body) : undefined,
|
|
24
|
+
});
|
|
25
|
+
const payload = await response.json();
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const errorMessage = payload?.errors?.[0]?.message ?? `Asana API request failed (${response.status})`;
|
|
28
|
+
throw new Error(errorMessage);
|
|
29
|
+
}
|
|
30
|
+
return payload.data;
|
|
31
|
+
}
|
|
32
|
+
async verify() {
|
|
33
|
+
const user = await this.request('/users/me', {
|
|
34
|
+
query: { opt_fields: 'gid,name,email,workspaces.gid,workspaces.name' },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
gid: user.gid,
|
|
38
|
+
name: user.name,
|
|
39
|
+
email: user.email,
|
|
40
|
+
workspaces: (user.workspaces ?? []).map((workspace) => ({
|
|
41
|
+
gid: workspace.gid,
|
|
42
|
+
name: workspace.name,
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async listWorkspaces() {
|
|
47
|
+
const user = await this.verify();
|
|
48
|
+
return user.workspaces;
|
|
49
|
+
}
|
|
50
|
+
async getWorkspace(workspaceGid) {
|
|
51
|
+
try {
|
|
52
|
+
const workspace = await this.request(`/workspaces/${workspaceGid}`);
|
|
53
|
+
return { gid: workspace.gid, name: workspace.name };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async listProjects(workspaceGid) {
|
|
60
|
+
const projects = await this.request('/projects', {
|
|
61
|
+
query: {
|
|
62
|
+
workspace: workspaceGid,
|
|
63
|
+
archived: false,
|
|
64
|
+
opt_fields: 'gid,name',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return projects.map((project) => ({ gid: project.gid, name: project.name }));
|
|
68
|
+
}
|
|
69
|
+
async getProject(projectGid) {
|
|
70
|
+
try {
|
|
71
|
+
const project = await this.request(`/projects/${projectGid}`, {
|
|
72
|
+
query: { opt_fields: 'gid,name' },
|
|
73
|
+
});
|
|
74
|
+
return { gid: project.gid, name: project.name };
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async getTask(taskGid) {
|
|
81
|
+
try {
|
|
82
|
+
const task = await this.request(`/tasks/${taskGid}`, {
|
|
83
|
+
query: { opt_fields: 'gid,name,completed,notes' },
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
gid: task.gid,
|
|
87
|
+
name: task.name,
|
|
88
|
+
completed: task.completed,
|
|
89
|
+
notes: task.notes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async createTask(input) {
|
|
97
|
+
const task = await this.request('/tasks', {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
body: { data: input },
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
gid: task.gid,
|
|
103
|
+
name: task.name,
|
|
104
|
+
completed: task.completed,
|
|
105
|
+
notes: task.notes,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async updateTask(taskGid, input) {
|
|
109
|
+
const task = await this.request(`/tasks/${taskGid}`, {
|
|
110
|
+
method: 'PUT',
|
|
111
|
+
body: { data: input },
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
gid: task.gid,
|
|
115
|
+
name: task.name,
|
|
116
|
+
completed: task.completed,
|
|
117
|
+
notes: task.notes,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { AsanaConfig } from './types.js';
|
|
3
|
+
export declare function isAsanaConfigured(db: Database.Database): boolean;
|
|
4
|
+
export declare function loadAsanaConfig(db: Database.Database): AsanaConfig | null;
|
|
5
|
+
export declare function saveAsanaAccessToken(db: Database.Database, accessToken: string): void;
|
|
6
|
+
export declare function saveAsanaWorkspace(db: Database.Database, workspaceGid: string, workspaceName: string): void;
|
|
7
|
+
export declare function saveAsanaProject(db: Database.Database, projectGid: string, projectName: string): void;
|
|
8
|
+
export declare function clearAsanaConfig(db: Database.Database): void;
|
|
9
|
+
export declare function getAsanaAccessToken(db: Database.Database): string | null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const SETTINGS_TABLE = 'workspace_settings';
|
|
2
|
+
const ASANA_CONFIG_KEYS = {
|
|
3
|
+
accessToken: 'asana.access_token',
|
|
4
|
+
workspaceGid: 'asana.workspace_gid',
|
|
5
|
+
workspaceName: 'asana.workspace_name',
|
|
6
|
+
projectGid: 'asana.project_gid',
|
|
7
|
+
projectName: 'asana.project_name',
|
|
8
|
+
};
|
|
9
|
+
function getSetting(db, key) {
|
|
10
|
+
const row = db
|
|
11
|
+
.prepare(`SELECT value FROM ${SETTINGS_TABLE} WHERE key = ?`)
|
|
12
|
+
.get(key);
|
|
13
|
+
return row?.value ?? null;
|
|
14
|
+
}
|
|
15
|
+
function setSetting(db, key, value) {
|
|
16
|
+
db.prepare(`
|
|
17
|
+
INSERT INTO ${SETTINGS_TABLE} (key, value)
|
|
18
|
+
VALUES (?, ?)
|
|
19
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
20
|
+
`).run(key, value);
|
|
21
|
+
}
|
|
22
|
+
function deleteSetting(db, key) {
|
|
23
|
+
db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(key);
|
|
24
|
+
}
|
|
25
|
+
export function isAsanaConfigured(db) {
|
|
26
|
+
return getSetting(db, ASANA_CONFIG_KEYS.accessToken) !== null;
|
|
27
|
+
}
|
|
28
|
+
export function loadAsanaConfig(db) {
|
|
29
|
+
const accessToken = getSetting(db, ASANA_CONFIG_KEYS.accessToken);
|
|
30
|
+
if (!accessToken)
|
|
31
|
+
return null;
|
|
32
|
+
return {
|
|
33
|
+
accessToken,
|
|
34
|
+
workspaceGid: getSetting(db, ASANA_CONFIG_KEYS.workspaceGid) ?? undefined,
|
|
35
|
+
workspaceName: getSetting(db, ASANA_CONFIG_KEYS.workspaceName) ?? undefined,
|
|
36
|
+
projectGid: getSetting(db, ASANA_CONFIG_KEYS.projectGid) ?? undefined,
|
|
37
|
+
projectName: getSetting(db, ASANA_CONFIG_KEYS.projectName) ?? undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function saveAsanaAccessToken(db, accessToken) {
|
|
41
|
+
setSetting(db, ASANA_CONFIG_KEYS.accessToken, accessToken);
|
|
42
|
+
}
|
|
43
|
+
export function saveAsanaWorkspace(db, workspaceGid, workspaceName) {
|
|
44
|
+
setSetting(db, ASANA_CONFIG_KEYS.workspaceGid, workspaceGid);
|
|
45
|
+
setSetting(db, ASANA_CONFIG_KEYS.workspaceName, workspaceName);
|
|
46
|
+
}
|
|
47
|
+
export function saveAsanaProject(db, projectGid, projectName) {
|
|
48
|
+
setSetting(db, ASANA_CONFIG_KEYS.projectGid, projectGid);
|
|
49
|
+
setSetting(db, ASANA_CONFIG_KEYS.projectName, projectName);
|
|
50
|
+
}
|
|
51
|
+
export function clearAsanaConfig(db) {
|
|
52
|
+
for (const key of Object.values(ASANA_CONFIG_KEYS)) {
|
|
53
|
+
deleteSetting(db, key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function getAsanaAccessToken(db) {
|
|
57
|
+
const envToken = process.env.PRLT_ASANA_ACCESS_TOKEN || process.env.ASANA_ACCESS_TOKEN;
|
|
58
|
+
if (envToken)
|
|
59
|
+
return envToken;
|
|
60
|
+
return getSetting(db, ASANA_CONFIG_KEYS.accessToken);
|
|
61
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AsanaClient } from './client.js';
|
|
2
|
+
export { isAsanaConfigured, loadAsanaConfig, saveAsanaAccessToken, saveAsanaWorkspace, saveAsanaProject, clearAsanaConfig, getAsanaAccessToken, } from './config.js';
|
|
3
|
+
export { AsanaMapper } from './mapper.js';
|
|
4
|
+
export { AsanaSync } from './sync.js';
|
|
5
|
+
export type { AsanaConfig, AsanaWorkspace, AsanaProject, AsanaUser, AsanaTask, AsanaTaskMap, AsanaTaskUpsertInput, } from './types.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AsanaClient } from './client.js';
|
|
2
|
+
export { isAsanaConfigured, loadAsanaConfig, saveAsanaAccessToken, saveAsanaWorkspace, saveAsanaProject, clearAsanaConfig, getAsanaAccessToken, } from './config.js';
|
|
3
|
+
export { AsanaMapper } from './mapper.js';
|
|
4
|
+
export { AsanaSync } from './sync.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { AsanaTaskMap } from './types.js';
|
|
3
|
+
export declare class AsanaMapper {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Database.Database);
|
|
6
|
+
private ensureTable;
|
|
7
|
+
createOrUpdateMapping(pmoTicketId: string, asanaTaskGid: string, asanaProjectGid?: string): void;
|
|
8
|
+
getByTicketId(ticketId: string): AsanaTaskMap | null;
|
|
9
|
+
getByTaskGid(asanaTaskGid: string): AsanaTaskMap | null;
|
|
10
|
+
listMappings(): AsanaTaskMap[];
|
|
11
|
+
updateSyncTimestamp(ticketId: string): void;
|
|
12
|
+
private rowToMap;
|
|
13
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PMO_TABLES } from '../pmo/schema.js';
|
|
2
|
+
export class AsanaMapper {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
this.ensureTable();
|
|
7
|
+
}
|
|
8
|
+
ensureTable() {
|
|
9
|
+
this.db.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS ${PMO_TABLES.asana_task_map} (
|
|
11
|
+
pmo_ticket_id TEXT NOT NULL REFERENCES ${PMO_TABLES.tickets}(id) ON DELETE CASCADE,
|
|
12
|
+
asana_task_gid TEXT NOT NULL,
|
|
13
|
+
asana_project_gid TEXT,
|
|
14
|
+
last_synced_at TIMESTAMP,
|
|
15
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
PRIMARY KEY (pmo_ticket_id),
|
|
17
|
+
UNIQUE (asana_task_gid)
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
this.db.exec(`
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_pmo_asana_task_map_task_gid
|
|
22
|
+
ON ${PMO_TABLES.asana_task_map}(asana_task_gid)
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
createOrUpdateMapping(pmoTicketId, asanaTaskGid, asanaProjectGid) {
|
|
26
|
+
this.db.prepare(`
|
|
27
|
+
INSERT INTO ${PMO_TABLES.asana_task_map}
|
|
28
|
+
(pmo_ticket_id, asana_task_gid, asana_project_gid, last_synced_at, created_at)
|
|
29
|
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
30
|
+
ON CONFLICT(pmo_ticket_id) DO UPDATE SET
|
|
31
|
+
asana_task_gid = excluded.asana_task_gid,
|
|
32
|
+
asana_project_gid = excluded.asana_project_gid,
|
|
33
|
+
last_synced_at = CURRENT_TIMESTAMP
|
|
34
|
+
`).run(pmoTicketId, asanaTaskGid, asanaProjectGid ?? null);
|
|
35
|
+
}
|
|
36
|
+
getByTicketId(ticketId) {
|
|
37
|
+
const row = this.db.prepare(`
|
|
38
|
+
SELECT * FROM ${PMO_TABLES.asana_task_map} WHERE pmo_ticket_id = ?
|
|
39
|
+
`).get(ticketId);
|
|
40
|
+
return row ? this.rowToMap(row) : null;
|
|
41
|
+
}
|
|
42
|
+
getByTaskGid(asanaTaskGid) {
|
|
43
|
+
const row = this.db.prepare(`
|
|
44
|
+
SELECT * FROM ${PMO_TABLES.asana_task_map} WHERE asana_task_gid = ?
|
|
45
|
+
`).get(asanaTaskGid);
|
|
46
|
+
return row ? this.rowToMap(row) : null;
|
|
47
|
+
}
|
|
48
|
+
listMappings() {
|
|
49
|
+
const rows = this.db.prepare(`
|
|
50
|
+
SELECT * FROM ${PMO_TABLES.asana_task_map} ORDER BY created_at DESC
|
|
51
|
+
`).all();
|
|
52
|
+
return rows.map((row) => this.rowToMap(row));
|
|
53
|
+
}
|
|
54
|
+
updateSyncTimestamp(ticketId) {
|
|
55
|
+
this.db.prepare(`
|
|
56
|
+
UPDATE ${PMO_TABLES.asana_task_map}
|
|
57
|
+
SET last_synced_at = CURRENT_TIMESTAMP
|
|
58
|
+
WHERE pmo_ticket_id = ?
|
|
59
|
+
`).run(ticketId);
|
|
60
|
+
}
|
|
61
|
+
rowToMap(row) {
|
|
62
|
+
return {
|
|
63
|
+
pmoTicketId: row.pmo_ticket_id,
|
|
64
|
+
asanaTaskGid: row.asana_task_gid,
|
|
65
|
+
asanaProjectGid: row.asana_project_gid ?? undefined,
|
|
66
|
+
lastSyncedAt: row.last_synced_at ? new Date(row.last_synced_at) : undefined,
|
|
67
|
+
createdAt: new Date(row.created_at),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Ticket } from '../pmo/types.js';
|
|
2
|
+
import { AsanaClient } from './client.js';
|
|
3
|
+
import { AsanaMapper } from './mapper.js';
|
|
4
|
+
export declare class AsanaSync {
|
|
5
|
+
private client;
|
|
6
|
+
private mapper;
|
|
7
|
+
constructor(client: AsanaClient, mapper: AsanaMapper);
|
|
8
|
+
private buildTaskName;
|
|
9
|
+
private buildTaskNotes;
|
|
10
|
+
private isCompletedStatus;
|
|
11
|
+
createTaskForTicket(ticket: Ticket, projectGid: string): Promise<string>;
|
|
12
|
+
syncTicket(ticket: Ticket, taskGid: string): Promise<void>;
|
|
13
|
+
}
|