@proletariat/cli 0.3.50 → 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 +2531 -1964
- package/package.json +1 -1
|
@@ -42,6 +42,7 @@ export default class Status extends PMOCommand {
|
|
|
42
42
|
outputSuccessAsJson({
|
|
43
43
|
agents: allStatuses,
|
|
44
44
|
}, createMetadata('agent status', flags));
|
|
45
|
+
return;
|
|
45
46
|
}
|
|
46
47
|
// Agent mode config for prompts
|
|
47
48
|
const agentConfig = jsonMode ? { flags, commandName: 'agent status' } : null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class AsanaConnect extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
disconnect: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
execute(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
4
|
+
import { colors } from '../../lib/colors.js';
|
|
5
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
6
|
+
import { AsanaClient, clearAsanaConfig, getAsanaAccessToken, isAsanaConfigured, loadAsanaConfig, saveAsanaAccessToken, saveAsanaProject, saveAsanaWorkspace, } from '../../lib/asana/index.js';
|
|
7
|
+
function isLikelyGid(value) {
|
|
8
|
+
return /^\d+$/.test(value);
|
|
9
|
+
}
|
|
10
|
+
export default class AsanaConnect extends PMOCommand {
|
|
11
|
+
static description = 'Authenticate with Asana and configure workspace/project defaults';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> <%= command.id %>',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> --check',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --workspace "Product Team" --project "Roadmap"',
|
|
16
|
+
'ASANA_ACCESS_TOKEN=... <%= config.bin %> <%= command.id %>',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...pmoBaseFlags,
|
|
20
|
+
check: Flags.boolean({
|
|
21
|
+
description: 'Only check if Asana credentials exist (do not prompt)',
|
|
22
|
+
default: false,
|
|
23
|
+
}),
|
|
24
|
+
force: Flags.boolean({
|
|
25
|
+
description: 'Force re-authentication even if credentials exist',
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
disconnect: Flags.boolean({
|
|
29
|
+
description: 'Remove stored Asana credentials',
|
|
30
|
+
default: false,
|
|
31
|
+
}),
|
|
32
|
+
workspace: Flags.string({
|
|
33
|
+
description: 'Default workspace gid or name',
|
|
34
|
+
}),
|
|
35
|
+
project: Flags.string({
|
|
36
|
+
description: 'Default project gid or name',
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
async execute() {
|
|
40
|
+
const { flags } = await this.parse(AsanaConnect);
|
|
41
|
+
const jsonMode = shouldOutputJson(flags);
|
|
42
|
+
const db = this.storage.getDatabase();
|
|
43
|
+
if (flags.disconnect) {
|
|
44
|
+
clearAsanaConfig(db);
|
|
45
|
+
if (jsonMode) {
|
|
46
|
+
outputSuccessAsJson({ disconnected: true, message: 'Asana credentials removed.' }, createMetadata('asana connect', flags));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.log(colors.success('Asana credentials removed.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (flags.check) {
|
|
53
|
+
if (!isAsanaConfigured(db)) {
|
|
54
|
+
if (jsonMode) {
|
|
55
|
+
outputErrorAsJson('ASANA_NOT_CONFIGURED', 'Asana is not configured. Run "prlt asana connect".', createMetadata('asana connect', flags));
|
|
56
|
+
this.exit(1);
|
|
57
|
+
}
|
|
58
|
+
this.log(colors.warning('Asana is not configured.'));
|
|
59
|
+
this.log(colors.textMuted('Run "prlt asana connect" to authenticate.'));
|
|
60
|
+
this.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const config = loadAsanaConfig(db);
|
|
63
|
+
try {
|
|
64
|
+
const client = new AsanaClient(config.accessToken);
|
|
65
|
+
const user = await client.verify();
|
|
66
|
+
if (jsonMode) {
|
|
67
|
+
outputSuccessAsJson({
|
|
68
|
+
authenticated: true,
|
|
69
|
+
user: user.name,
|
|
70
|
+
email: user.email ?? null,
|
|
71
|
+
workspace: config.workspaceName ?? config.workspaceGid ?? null,
|
|
72
|
+
project: config.projectName ?? config.projectGid ?? null,
|
|
73
|
+
}, createMetadata('asana connect', flags));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.log(colors.success('Asana connection is active'));
|
|
77
|
+
this.log(colors.textMuted(` User: ${user.name}${user.email ? ` (${user.email})` : ''}`));
|
|
78
|
+
if (config.workspaceName || config.workspaceGid) {
|
|
79
|
+
this.log(colors.textMuted(` Workspace: ${config.workspaceName ?? config.workspaceGid}`));
|
|
80
|
+
}
|
|
81
|
+
if (config.projectName || config.projectGid) {
|
|
82
|
+
this.log(colors.textMuted(` Project: ${config.projectName ?? config.projectGid}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
if (jsonMode) {
|
|
87
|
+
outputErrorAsJson('ASANA_AUTH_INVALID', 'Stored Asana token is invalid or expired.', createMetadata('asana connect', flags));
|
|
88
|
+
this.exit(1);
|
|
89
|
+
}
|
|
90
|
+
this.log(colors.error('Stored Asana token is invalid or expired.'));
|
|
91
|
+
this.log(colors.textMuted('Run "prlt asana connect --force" to re-authenticate.'));
|
|
92
|
+
this.exit(1);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const existingConfig = loadAsanaConfig(db);
|
|
97
|
+
if (existingConfig && !flags.force) {
|
|
98
|
+
try {
|
|
99
|
+
const client = new AsanaClient(existingConfig.accessToken);
|
|
100
|
+
const user = await client.verify();
|
|
101
|
+
if (jsonMode) {
|
|
102
|
+
outputSuccessAsJson({
|
|
103
|
+
authenticated: true,
|
|
104
|
+
user: user.name,
|
|
105
|
+
message: 'Already authenticated. Use --force to re-authenticate.',
|
|
106
|
+
}, createMetadata('asana connect', flags));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.log(colors.success('Already connected to Asana'));
|
|
110
|
+
this.log(colors.textMuted(` User: ${user.name}`));
|
|
111
|
+
this.log(colors.textMuted('Use --force to re-authenticate.'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Continue to re-authenticate with a new token
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let accessToken = getAsanaAccessToken(db);
|
|
119
|
+
if (!accessToken) {
|
|
120
|
+
if (jsonMode) {
|
|
121
|
+
outputErrorAsJson('ACCESS_TOKEN_REQUIRED', 'Asana access token required. Set ASANA_ACCESS_TOKEN or PRLT_ASANA_ACCESS_TOKEN.', createMetadata('asana connect', flags));
|
|
122
|
+
this.exit(1);
|
|
123
|
+
}
|
|
124
|
+
this.log('');
|
|
125
|
+
this.log(colors.primary('Asana Authentication'));
|
|
126
|
+
this.log('');
|
|
127
|
+
this.log('Create a Personal Access Token at:');
|
|
128
|
+
this.log(colors.textSecondary(' https://app.asana.com/0/my-apps'));
|
|
129
|
+
this.log('');
|
|
130
|
+
const { inputToken } = await inquirer.prompt([{
|
|
131
|
+
type: 'password',
|
|
132
|
+
name: 'inputToken',
|
|
133
|
+
message: 'Enter your Asana access token:',
|
|
134
|
+
mask: '*',
|
|
135
|
+
validate: (input) => input.trim().length > 0 || 'Access token is required',
|
|
136
|
+
}]);
|
|
137
|
+
accessToken = inputToken;
|
|
138
|
+
}
|
|
139
|
+
if (!accessToken) {
|
|
140
|
+
if (jsonMode) {
|
|
141
|
+
outputErrorAsJson('ACCESS_TOKEN_REQUIRED', 'Asana access token required. Set ASANA_ACCESS_TOKEN or PRLT_ASANA_ACCESS_TOKEN.', createMetadata('asana connect', flags));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.log(colors.error('Asana access token is required.'));
|
|
145
|
+
}
|
|
146
|
+
this.exit(1);
|
|
147
|
+
}
|
|
148
|
+
this.log('');
|
|
149
|
+
this.log(colors.textMuted('Verifying Asana access token...'));
|
|
150
|
+
if (!accessToken) {
|
|
151
|
+
if (jsonMode) {
|
|
152
|
+
outputErrorAsJson('ACCESS_TOKEN_REQUIRED', 'Asana access token required. Set ASANA_ACCESS_TOKEN or PRLT_ASANA_ACCESS_TOKEN.', createMetadata('asana connect', flags));
|
|
153
|
+
this.exit(1);
|
|
154
|
+
}
|
|
155
|
+
this.log(colors.error('Asana access token is required.'));
|
|
156
|
+
this.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const client = new AsanaClient(accessToken);
|
|
159
|
+
try {
|
|
160
|
+
const user = await client.verify();
|
|
161
|
+
saveAsanaAccessToken(db, accessToken);
|
|
162
|
+
let workspaceGid;
|
|
163
|
+
let workspaceName;
|
|
164
|
+
let projectGid;
|
|
165
|
+
let projectName;
|
|
166
|
+
if (flags.workspace) {
|
|
167
|
+
const workspaces = user.workspaces;
|
|
168
|
+
if (isLikelyGid(flags.workspace)) {
|
|
169
|
+
const workspace = await client.getWorkspace(flags.workspace);
|
|
170
|
+
if (!workspace) {
|
|
171
|
+
throw new Error(`Workspace ${flags.workspace} not found`);
|
|
172
|
+
}
|
|
173
|
+
workspaceGid = workspace.gid;
|
|
174
|
+
workspaceName = workspace.name;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const matched = workspaces.find((workspace) => workspace.name.toLowerCase() === flags.workspace.toLowerCase());
|
|
178
|
+
if (!matched) {
|
|
179
|
+
throw new Error(`Workspace "${flags.workspace}" not found`);
|
|
180
|
+
}
|
|
181
|
+
workspaceGid = matched.gid;
|
|
182
|
+
workspaceName = matched.name;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (!jsonMode && user.workspaces.length === 1) {
|
|
186
|
+
workspaceGid = user.workspaces[0].gid;
|
|
187
|
+
workspaceName = user.workspaces[0].name;
|
|
188
|
+
}
|
|
189
|
+
if (!workspaceGid && !jsonMode && user.workspaces.length > 1) {
|
|
190
|
+
const { selectedWorkspace } = await inquirer.prompt([{
|
|
191
|
+
type: 'list',
|
|
192
|
+
name: 'selectedWorkspace',
|
|
193
|
+
message: 'Select default Asana workspace (optional):',
|
|
194
|
+
choices: [
|
|
195
|
+
{ name: 'Skip', value: null },
|
|
196
|
+
...user.workspaces.map((workspace) => ({
|
|
197
|
+
name: workspace.name,
|
|
198
|
+
value: workspace.gid,
|
|
199
|
+
})),
|
|
200
|
+
],
|
|
201
|
+
default: null,
|
|
202
|
+
}]);
|
|
203
|
+
if (selectedWorkspace) {
|
|
204
|
+
const selected = user.workspaces.find((workspace) => workspace.gid === selectedWorkspace);
|
|
205
|
+
workspaceGid = selected?.gid;
|
|
206
|
+
workspaceName = selected?.name;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (workspaceGid) {
|
|
210
|
+
saveAsanaWorkspace(db, workspaceGid, workspaceName ?? workspaceGid);
|
|
211
|
+
}
|
|
212
|
+
if (flags.project) {
|
|
213
|
+
const projectScope = workspaceGid ?? loadAsanaConfig(db)?.workspaceGid;
|
|
214
|
+
if (isLikelyGid(flags.project)) {
|
|
215
|
+
const project = await client.getProject(flags.project);
|
|
216
|
+
if (!project) {
|
|
217
|
+
throw new Error(`Project ${flags.project} not found`);
|
|
218
|
+
}
|
|
219
|
+
projectGid = project.gid;
|
|
220
|
+
projectName = project.name;
|
|
221
|
+
}
|
|
222
|
+
else if (projectScope) {
|
|
223
|
+
const projects = await client.listProjects(projectScope);
|
|
224
|
+
const matched = projects.find((project) => project.name.toLowerCase() === flags.project.toLowerCase());
|
|
225
|
+
if (!matched) {
|
|
226
|
+
throw new Error(`Project "${flags.project}" not found in selected workspace`);
|
|
227
|
+
}
|
|
228
|
+
projectGid = matched.gid;
|
|
229
|
+
projectName = matched.name;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
throw new Error('A workspace must be configured before selecting a project by name.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (projectGid) {
|
|
236
|
+
saveAsanaProject(db, projectGid, projectName ?? projectGid);
|
|
237
|
+
}
|
|
238
|
+
if (jsonMode) {
|
|
239
|
+
outputSuccessAsJson({
|
|
240
|
+
authenticated: true,
|
|
241
|
+
user: user.name,
|
|
242
|
+
email: user.email ?? null,
|
|
243
|
+
workspace: workspaceName ?? null,
|
|
244
|
+
project: projectName ?? null,
|
|
245
|
+
}, createMetadata('asana connect', flags));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.log(colors.success('Connected to Asana'));
|
|
249
|
+
this.log(colors.textMuted(` User: ${user.name}${user.email ? ` (${user.email})` : ''}`));
|
|
250
|
+
if (workspaceName) {
|
|
251
|
+
this.log(colors.textMuted(` Workspace: ${workspaceName}`));
|
|
252
|
+
}
|
|
253
|
+
if (projectName) {
|
|
254
|
+
this.log(colors.textMuted(` Project: ${projectName}`));
|
|
255
|
+
}
|
|
256
|
+
this.log('');
|
|
257
|
+
this.log(colors.textMuted('Run "prlt asana sync" to sync mapped tickets to Asana tasks.'));
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (jsonMode) {
|
|
261
|
+
outputErrorAsJson('ASANA_CONNECT_FAILED', `Connection failed: ${error instanceof Error ? error.message : String(error)}`, createMetadata('asana connect', flags));
|
|
262
|
+
this.exit(1);
|
|
263
|
+
}
|
|
264
|
+
this.error(`Connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class AsanaSyncCommand extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
ticket: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
task: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
'create-missing': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
execute(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
3
|
+
import { colors } from '../../lib/colors.js';
|
|
4
|
+
import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
5
|
+
import { AsanaClient, AsanaMapper, AsanaSync, isAsanaConfigured, loadAsanaConfig, } from '../../lib/asana/index.js';
|
|
6
|
+
export default class AsanaSyncCommand extends PMOCommand {
|
|
7
|
+
static description = 'Sync PMO tickets to Asana tasks';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --ticket TKT-001 --task 123456789',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> --ticket TKT-001 --create-missing',
|
|
11
|
+
'<%= config.bin %> <%= command.id %> --dry-run',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
...pmoBaseFlags,
|
|
15
|
+
ticket: Flags.string({
|
|
16
|
+
description: 'PMO ticket ID to sync (syncs all mapped tickets if omitted)',
|
|
17
|
+
}),
|
|
18
|
+
task: Flags.string({
|
|
19
|
+
description: 'Asana task gid to map to --ticket',
|
|
20
|
+
}),
|
|
21
|
+
project: Flags.string({
|
|
22
|
+
description: 'Asana project gid used with --create-missing',
|
|
23
|
+
}),
|
|
24
|
+
'create-missing': Flags.boolean({
|
|
25
|
+
description: 'Create Asana task when no mapping exists (requires project)',
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
'dry-run': Flags.boolean({
|
|
29
|
+
description: 'Preview sync operations without making changes',
|
|
30
|
+
default: false,
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
async execute() {
|
|
34
|
+
const { flags } = await this.parse(AsanaSyncCommand);
|
|
35
|
+
const jsonMode = shouldOutputJson(flags);
|
|
36
|
+
const db = this.storage.getDatabase();
|
|
37
|
+
if (!isAsanaConfigured(db)) {
|
|
38
|
+
if (jsonMode) {
|
|
39
|
+
outputErrorAsJson('ASANA_NOT_CONFIGURED', 'Asana is not configured. Run "prlt asana connect" first.', createMetadata('asana sync', flags));
|
|
40
|
+
this.exit(1);
|
|
41
|
+
}
|
|
42
|
+
this.error('Asana is not configured. Run "prlt asana connect" first.');
|
|
43
|
+
}
|
|
44
|
+
const config = loadAsanaConfig(db);
|
|
45
|
+
const client = new AsanaClient(config.accessToken);
|
|
46
|
+
const mapper = new AsanaMapper(db);
|
|
47
|
+
const sync = new AsanaSync(client, mapper);
|
|
48
|
+
if (flags.ticket) {
|
|
49
|
+
const ticket = await this.storage.getTicket(flags.ticket);
|
|
50
|
+
if (!ticket) {
|
|
51
|
+
if (jsonMode) {
|
|
52
|
+
outputErrorAsJson('TICKET_NOT_FOUND', `Ticket ${flags.ticket} not found.`, createMetadata('asana sync', flags));
|
|
53
|
+
this.exit(1);
|
|
54
|
+
}
|
|
55
|
+
this.error(`Ticket ${flags.ticket} not found.`);
|
|
56
|
+
}
|
|
57
|
+
if (flags.task) {
|
|
58
|
+
mapper.createOrUpdateMapping(flags.ticket, flags.task, flags.project ?? config.projectGid);
|
|
59
|
+
}
|
|
60
|
+
const mapping = mapper.getByTicketId(flags.ticket);
|
|
61
|
+
if (!mapping) {
|
|
62
|
+
if (!flags['create-missing']) {
|
|
63
|
+
if (jsonMode) {
|
|
64
|
+
outputSuccessAsJson({
|
|
65
|
+
synced: false,
|
|
66
|
+
message: `Ticket ${flags.ticket} has no Asana mapping. Use --task or --create-missing.`,
|
|
67
|
+
}, createMetadata('asana sync', flags));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.log(colors.warning(`Ticket ${flags.ticket} has no Asana mapping. Use --task or --create-missing.`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const projectGid = flags.project ?? config.projectGid;
|
|
74
|
+
if (!projectGid) {
|
|
75
|
+
if (jsonMode) {
|
|
76
|
+
outputErrorAsJson('ASANA_PROJECT_REQUIRED', 'Project gid is required for --create-missing. Use --project or run "prlt asana connect --project ...".', createMetadata('asana sync', flags));
|
|
77
|
+
this.exit(1);
|
|
78
|
+
}
|
|
79
|
+
this.error('Project gid is required for --create-missing. Use --project or run "prlt asana connect --project ...".');
|
|
80
|
+
}
|
|
81
|
+
if (flags['dry-run']) {
|
|
82
|
+
if (jsonMode) {
|
|
83
|
+
outputSuccessAsJson({
|
|
84
|
+
dryRun: true,
|
|
85
|
+
action: 'create-and-sync',
|
|
86
|
+
ticketId: flags.ticket,
|
|
87
|
+
projectGid,
|
|
88
|
+
}, createMetadata('asana sync', flags));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.log(colors.textMuted(`Would create Asana task in project ${projectGid} for ${flags.ticket}`));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const taskGid = await sync.createTaskForTicket(ticket, projectGid);
|
|
95
|
+
await sync.syncTicket(ticket, taskGid);
|
|
96
|
+
if (jsonMode) {
|
|
97
|
+
outputSuccessAsJson({
|
|
98
|
+
action: 'create-and-sync',
|
|
99
|
+
ticketId: flags.ticket,
|
|
100
|
+
asanaTaskGid: taskGid,
|
|
101
|
+
synced: true,
|
|
102
|
+
}, createMetadata('asana sync', flags));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.log(colors.success(`Created and synced Asana task ${taskGid} for ${flags.ticket}`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (flags['dry-run']) {
|
|
109
|
+
if (jsonMode) {
|
|
110
|
+
outputSuccessAsJson({
|
|
111
|
+
dryRun: true,
|
|
112
|
+
action: 'sync-ticket',
|
|
113
|
+
ticketId: flags.ticket,
|
|
114
|
+
asanaTaskGid: mapping.asanaTaskGid,
|
|
115
|
+
statusCategory: ticket.statusCategory,
|
|
116
|
+
}, createMetadata('asana sync', flags));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.log(colors.textMuted(`Would sync ${flags.ticket} to Asana task ${mapping.asanaTaskGid}`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await sync.syncTicket(ticket, mapping.asanaTaskGid);
|
|
123
|
+
if (jsonMode) {
|
|
124
|
+
outputSuccessAsJson({
|
|
125
|
+
action: 'sync-ticket',
|
|
126
|
+
ticketId: flags.ticket,
|
|
127
|
+
asanaTaskGid: mapping.asanaTaskGid,
|
|
128
|
+
synced: true,
|
|
129
|
+
}, createMetadata('asana sync', flags));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.log(colors.success(`Synced ${flags.ticket} to Asana task ${mapping.asanaTaskGid}`));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const mappings = mapper.listMappings();
|
|
136
|
+
if (mappings.length === 0) {
|
|
137
|
+
if (jsonMode) {
|
|
138
|
+
outputSuccessAsJson({ synced: 0, skipped: 0, errors: 0, message: 'No mapped tickets to sync.' }, createMetadata('asana sync', flags));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.log(colors.textMuted('No mapped tickets to sync.'));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (flags['dry-run']) {
|
|
145
|
+
const preview = [];
|
|
146
|
+
for (const mapping of mappings) {
|
|
147
|
+
// eslint-disable-next-line no-await-in-loop
|
|
148
|
+
const ticket = await this.storage.getTicket(mapping.pmoTicketId);
|
|
149
|
+
preview.push({
|
|
150
|
+
ticketId: mapping.pmoTicketId,
|
|
151
|
+
asanaTaskGid: mapping.asanaTaskGid,
|
|
152
|
+
statusCategory: ticket?.statusCategory ?? null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (jsonMode) {
|
|
156
|
+
outputSuccessAsJson({ dryRun: true, action: 'sync-all', tickets: preview }, createMetadata('asana sync', flags));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const item of preview) {
|
|
160
|
+
this.log(colors.textMuted(` ${item.ticketId} -> ${item.asanaTaskGid} (${item.statusCategory ?? 'missing'})`));
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let synced = 0;
|
|
165
|
+
let skipped = 0;
|
|
166
|
+
let errors = 0;
|
|
167
|
+
for (const mapping of mappings) {
|
|
168
|
+
try {
|
|
169
|
+
// eslint-disable-next-line no-await-in-loop
|
|
170
|
+
const ticket = await this.storage.getTicket(mapping.pmoTicketId);
|
|
171
|
+
if (!ticket) {
|
|
172
|
+
skipped++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// eslint-disable-next-line no-await-in-loop
|
|
176
|
+
await sync.syncTicket(ticket, mapping.asanaTaskGid);
|
|
177
|
+
synced++;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
errors++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (jsonMode) {
|
|
184
|
+
outputSuccessAsJson({ action: 'sync-all', synced, skipped, errors }, createMetadata('asana sync', flags));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.log(colors.success(`Asana sync complete: ${synced} synced, ${skipped} skipped, ${errors} errors`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -6,7 +6,7 @@ import { PromptCommand } from '../../lib/prompt-command.js';
|
|
|
6
6
|
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
7
7
|
import { styles } from '../../lib/styles.js';
|
|
8
8
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
9
|
-
import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, saveCreatePrDefault, saveFirewallAllowlistDomains, } from '../../lib/execution/config.js';
|
|
9
|
+
import { loadExecutionConfig, saveTerminalApp, saveTerminalOpenInBackground, saveTmuxControlMode, saveShell, getMirrorToPmoDefault, saveCreatePrDefault, saveMirrorToPmoDefault, saveFirewallAllowlistDomains, } from '../../lib/execution/config.js';
|
|
10
10
|
import { shouldOutputJson, isNonTTY, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
11
11
|
export default class Config extends PromptCommand {
|
|
12
12
|
static description = 'View and update workspace configuration';
|
|
@@ -56,6 +56,7 @@ export default class Config extends PromptCommand {
|
|
|
56
56
|
try {
|
|
57
57
|
// Load current config
|
|
58
58
|
const config = loadExecutionConfig(db);
|
|
59
|
+
const mirrorToPmoDefault = getMirrorToPmoDefault(db);
|
|
59
60
|
// Handle --set flag
|
|
60
61
|
if (flags.set && flags.set.length > 0) {
|
|
61
62
|
for (const setValue of flags.set) {
|
|
@@ -94,6 +95,7 @@ export default class Config extends PromptCommand {
|
|
|
94
95
|
outputMode: config.outputMode,
|
|
95
96
|
permissionMode: config.permissionMode,
|
|
96
97
|
createPrDefault: config.createPrDefault ?? null,
|
|
98
|
+
mirrorToPmoDefault,
|
|
97
99
|
firewall: {
|
|
98
100
|
allowlistDomains: config.firewall.allowlistDomains,
|
|
99
101
|
},
|
|
@@ -120,6 +122,7 @@ export default class Config extends PromptCommand {
|
|
|
120
122
|
this.log(` outputMode: ${config.outputMode}`);
|
|
121
123
|
this.log(` permissionMode: ${config.permissionMode}`);
|
|
122
124
|
this.log(` createPrDefault: ${config.createPrDefault ?? 'not set (will prompt)'}`);
|
|
125
|
+
this.log(` mirrorToPmoDefault: ${mirrorToPmoDefault ?? 'not set (default: true)'}`);
|
|
123
126
|
this.log(` firewall.allowlistDomains: ${config.firewall.allowlistDomains.join(', ') || '(none)'}`);
|
|
124
127
|
this.log('');
|
|
125
128
|
}
|
|
@@ -300,6 +303,9 @@ export default class Config extends PromptCommand {
|
|
|
300
303
|
case 'execution.create_pr_default':
|
|
301
304
|
saveCreatePrDefault(db, value.toLowerCase() === 'true');
|
|
302
305
|
break;
|
|
306
|
+
case 'execution.mirror_to_pmo_default':
|
|
307
|
+
saveMirrorToPmoDefault(db, value.toLowerCase() === 'true');
|
|
308
|
+
break;
|
|
303
309
|
case 'firewall.allowlistdomains': {
|
|
304
310
|
const domains = value
|
|
305
311
|
.split(',')
|
|
@@ -52,6 +52,9 @@ export default class ExecutionList extends PMOCommand {
|
|
|
52
52
|
const db = new Database(dbPath);
|
|
53
53
|
const executionStorage = new ExecutionStorage(db);
|
|
54
54
|
try {
|
|
55
|
+
// Keep execution status truthful: mark missing tmux/container sessions as stopped
|
|
56
|
+
// before listing so "running" only reflects actually active work.
|
|
57
|
+
executionStorage.cleanupStaleExecutions();
|
|
55
58
|
const executions = executionStorage.listExecutions({
|
|
56
59
|
status: flags.status,
|
|
57
60
|
agentName: flags.agent,
|
|
@@ -101,6 +101,10 @@ export default class ExecutionView extends PMOCommand {
|
|
|
101
101
|
sessionId: execution.sessionId || null,
|
|
102
102
|
host: execution.host || null,
|
|
103
103
|
logPath: execution.logPath || null,
|
|
104
|
+
externalSource: execution.externalSource || null,
|
|
105
|
+
externalKey: execution.externalKey || null,
|
|
106
|
+
externalId: execution.externalId || null,
|
|
107
|
+
externalUrl: execution.externalUrl || null,
|
|
104
108
|
startedAt: execution.startedAt.toISOString(),
|
|
105
109
|
completedAt: execution.completedAt?.toISOString() || null,
|
|
106
110
|
exitCode: execution.exitCode ?? null,
|
|
@@ -128,6 +132,12 @@ export default class ExecutionView extends PMOCommand {
|
|
|
128
132
|
if (execution.branch) {
|
|
129
133
|
this.log(`${styles.muted('Branch:')} ${execution.branch}`);
|
|
130
134
|
}
|
|
135
|
+
if (execution.externalSource || execution.externalKey) {
|
|
136
|
+
this.log(`${styles.muted('External:')} ${(execution.externalSource || 'unknown')} ${execution.externalKey ? `(${execution.externalKey})` : ''}`.trimEnd());
|
|
137
|
+
if (execution.externalUrl) {
|
|
138
|
+
this.log(`${styles.muted('External URL:')} ${execution.externalUrl}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
131
141
|
this.log('');
|
|
132
142
|
// Process info
|
|
133
143
|
if (execution.pid || execution.containerId || execution.sessionId || execution.host) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class MondayConnect extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
board: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
check: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
disconnect: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
execute(): Promise<void>;
|
|
15
|
+
private checkConnection;
|
|
16
|
+
}
|