@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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Deterministically maps an IssueEnvelope to spawn context data
|
|
5
5
|
* (prompt text + metadata) for agent execution.
|
|
6
6
|
*/
|
|
7
|
+
import { validateOrThrow } from './validation.js';
|
|
7
8
|
/**
|
|
8
9
|
* Map an IssueEnvelope to spawn context data.
|
|
9
10
|
*
|
|
@@ -14,10 +15,12 @@
|
|
|
14
15
|
* The mapping is deterministic: the same IssueEnvelope always produces
|
|
15
16
|
* the same IssueSpawnContext.
|
|
16
17
|
*
|
|
17
|
-
* @param
|
|
18
|
+
* @param input - IssueEnvelope-like input
|
|
18
19
|
* @returns Spawn context with prompt and metadata
|
|
20
|
+
* @throws ExternalIssueError when input fails IssueEnvelope validation
|
|
19
21
|
*/
|
|
20
|
-
export function mapToSpawnContext(
|
|
22
|
+
export function mapToSpawnContext(input) {
|
|
23
|
+
const envelope = validateOrThrow(input);
|
|
21
24
|
const prompt = buildPrompt(envelope);
|
|
22
25
|
const metadata = buildMetadata(envelope);
|
|
23
26
|
return { prompt, metadata };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { type ExternalExecutionMapping, type ExternalMappingProvider, type UpsertExternalExecutionMappingInput } from './types.js';
|
|
3
|
+
export declare class ExternalExecutionMappingStore {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Database.Database);
|
|
6
|
+
private ensureTables;
|
|
7
|
+
upsertMapping(input: UpsertExternalExecutionMappingInput): ExternalExecutionMapping;
|
|
8
|
+
getByExternalId(provider: ExternalMappingProvider, externalId: string): ExternalExecutionMapping | null;
|
|
9
|
+
getByExternalKey(provider: ExternalMappingProvider, externalKey: string): ExternalExecutionMapping | null;
|
|
10
|
+
findByExecutionId(executionId: string): ExternalExecutionMapping[];
|
|
11
|
+
private rowToMapping;
|
|
12
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { PMO_TABLES } from '../pmo/schema.js';
|
|
2
|
+
const T = PMO_TABLES;
|
|
3
|
+
function parseDate(value) {
|
|
4
|
+
return value ? new Date(value) : null;
|
|
5
|
+
}
|
|
6
|
+
function parseSnapshot(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(value);
|
|
11
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Keep compatibility if old/invalid values are present.
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export class ExternalExecutionMappingStore {
|
|
21
|
+
db;
|
|
22
|
+
constructor(db) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.ensureTables();
|
|
25
|
+
}
|
|
26
|
+
ensureTables() {
|
|
27
|
+
this.db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS ${T.external_execution_map} (
|
|
29
|
+
provider TEXT NOT NULL CHECK (provider IN ('linear', 'jira', 'asana', 'monday', 'pmo')),
|
|
30
|
+
external_id TEXT NOT NULL,
|
|
31
|
+
external_key TEXT,
|
|
32
|
+
canonical_url TEXT,
|
|
33
|
+
latest_state_snapshot TEXT,
|
|
34
|
+
last_synced_at TIMESTAMP,
|
|
35
|
+
last_spawned_at TIMESTAMP,
|
|
36
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
38
|
+
PRIMARY KEY (provider, external_id)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS ${T.external_execution_links} (
|
|
42
|
+
provider TEXT NOT NULL,
|
|
43
|
+
external_id TEXT NOT NULL,
|
|
44
|
+
execution_id TEXT NOT NULL,
|
|
45
|
+
linked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
PRIMARY KEY (provider, external_id, execution_id),
|
|
47
|
+
FOREIGN KEY (provider, external_id)
|
|
48
|
+
REFERENCES ${T.external_execution_map}(provider, external_id)
|
|
49
|
+
ON DELETE CASCADE
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS ${T.external_execution_prs} (
|
|
53
|
+
provider TEXT NOT NULL,
|
|
54
|
+
external_id TEXT NOT NULL,
|
|
55
|
+
pr_url TEXT NOT NULL,
|
|
56
|
+
linked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
57
|
+
PRIMARY KEY (provider, external_id, pr_url),
|
|
58
|
+
FOREIGN KEY (provider, external_id)
|
|
59
|
+
REFERENCES ${T.external_execution_map}(provider, external_id)
|
|
60
|
+
ON DELETE CASCADE
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_pmo_external_execution_map_external_key
|
|
64
|
+
ON ${T.external_execution_map}(provider, external_key);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_pmo_external_execution_links_execution_id
|
|
66
|
+
ON ${T.external_execution_links}(execution_id);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_pmo_external_execution_prs_pr_url
|
|
68
|
+
ON ${T.external_execution_prs}(pr_url);
|
|
69
|
+
`);
|
|
70
|
+
}
|
|
71
|
+
upsertMapping(input) {
|
|
72
|
+
const snapshot = input.latestStateSnapshot === undefined
|
|
73
|
+
? null
|
|
74
|
+
: JSON.stringify(input.latestStateSnapshot);
|
|
75
|
+
const lastSyncedAt = input.lastSyncedAt?.toISOString() ?? null;
|
|
76
|
+
const lastSpawnedAt = input.lastSpawnedAt?.toISOString() ?? null;
|
|
77
|
+
this.db.transaction(() => {
|
|
78
|
+
this.db.prepare(`
|
|
79
|
+
INSERT INTO ${T.external_execution_map}
|
|
80
|
+
(provider, external_id, external_key, canonical_url, latest_state_snapshot, last_synced_at, last_spawned_at, created_at, updated_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
82
|
+
ON CONFLICT(provider, external_id) DO UPDATE SET
|
|
83
|
+
external_key = COALESCE(excluded.external_key, ${T.external_execution_map}.external_key),
|
|
84
|
+
canonical_url = COALESCE(excluded.canonical_url, ${T.external_execution_map}.canonical_url),
|
|
85
|
+
latest_state_snapshot = COALESCE(excluded.latest_state_snapshot, ${T.external_execution_map}.latest_state_snapshot),
|
|
86
|
+
last_synced_at = COALESCE(excluded.last_synced_at, ${T.external_execution_map}.last_synced_at),
|
|
87
|
+
last_spawned_at = COALESCE(excluded.last_spawned_at, ${T.external_execution_map}.last_spawned_at),
|
|
88
|
+
updated_at = CURRENT_TIMESTAMP
|
|
89
|
+
`).run(input.provider, input.externalId, input.externalKey ?? null, input.canonicalUrl ?? null, snapshot, lastSyncedAt, lastSpawnedAt);
|
|
90
|
+
if (input.executionId) {
|
|
91
|
+
this.db.prepare(`
|
|
92
|
+
INSERT OR IGNORE INTO ${T.external_execution_links}
|
|
93
|
+
(provider, external_id, execution_id, linked_at)
|
|
94
|
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
95
|
+
`).run(input.provider, input.externalId, input.executionId);
|
|
96
|
+
}
|
|
97
|
+
if (input.prUrl) {
|
|
98
|
+
this.db.prepare(`
|
|
99
|
+
INSERT OR IGNORE INTO ${T.external_execution_prs}
|
|
100
|
+
(provider, external_id, pr_url, linked_at)
|
|
101
|
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
102
|
+
`).run(input.provider, input.externalId, input.prUrl);
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
return this.getByExternalId(input.provider, input.externalId);
|
|
106
|
+
}
|
|
107
|
+
getByExternalId(provider, externalId) {
|
|
108
|
+
const row = this.db.prepare(`
|
|
109
|
+
SELECT * FROM ${T.external_execution_map}
|
|
110
|
+
WHERE provider = ? AND external_id = ?
|
|
111
|
+
`).get(provider, externalId);
|
|
112
|
+
if (!row)
|
|
113
|
+
return null;
|
|
114
|
+
return this.rowToMapping(row);
|
|
115
|
+
}
|
|
116
|
+
getByExternalKey(provider, externalKey) {
|
|
117
|
+
const row = this.db.prepare(`
|
|
118
|
+
SELECT * FROM ${T.external_execution_map}
|
|
119
|
+
WHERE provider = ? AND external_key = ?
|
|
120
|
+
`).get(provider, externalKey);
|
|
121
|
+
if (!row)
|
|
122
|
+
return null;
|
|
123
|
+
return this.rowToMapping(row);
|
|
124
|
+
}
|
|
125
|
+
findByExecutionId(executionId) {
|
|
126
|
+
const rows = this.db.prepare(`
|
|
127
|
+
SELECT m.*
|
|
128
|
+
FROM ${T.external_execution_map} m
|
|
129
|
+
INNER JOIN ${T.external_execution_links} l
|
|
130
|
+
ON m.provider = l.provider
|
|
131
|
+
AND m.external_id = l.external_id
|
|
132
|
+
WHERE l.execution_id = ?
|
|
133
|
+
ORDER BY m.updated_at DESC
|
|
134
|
+
`).all(executionId);
|
|
135
|
+
return rows.map((row) => this.rowToMapping(row));
|
|
136
|
+
}
|
|
137
|
+
rowToMapping(row) {
|
|
138
|
+
const executionIds = this.db.prepare(`
|
|
139
|
+
SELECT execution_id AS value
|
|
140
|
+
FROM ${T.external_execution_links}
|
|
141
|
+
WHERE provider = ? AND external_id = ?
|
|
142
|
+
ORDER BY linked_at DESC
|
|
143
|
+
`).all(row.provider, row.external_id);
|
|
144
|
+
const prUrls = this.db.prepare(`
|
|
145
|
+
SELECT pr_url AS value
|
|
146
|
+
FROM ${T.external_execution_prs}
|
|
147
|
+
WHERE provider = ? AND external_id = ?
|
|
148
|
+
ORDER BY linked_at DESC
|
|
149
|
+
`).all(row.provider, row.external_id);
|
|
150
|
+
return {
|
|
151
|
+
provider: row.provider,
|
|
152
|
+
externalId: row.external_id,
|
|
153
|
+
externalKey: row.external_key,
|
|
154
|
+
canonicalUrl: row.canonical_url,
|
|
155
|
+
latestStateSnapshot: parseSnapshot(row.latest_state_snapshot),
|
|
156
|
+
executionIds: executionIds.map((r) => r.value),
|
|
157
|
+
prUrls: prUrls.map((r) => r.value),
|
|
158
|
+
lastSyncedAt: parseDate(row.last_synced_at),
|
|
159
|
+
lastSpawnedAt: parseDate(row.last_spawned_at),
|
|
160
|
+
createdAt: new Date(row.created_at),
|
|
161
|
+
updatedAt: new Date(row.updated_at),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -14,6 +14,10 @@ export type IssueSource = 'linear' | 'jira';
|
|
|
14
14
|
* All valid issue sources as a const array.
|
|
15
15
|
*/
|
|
16
16
|
export declare const ISSUE_SOURCES: readonly ["linear", "jira"];
|
|
17
|
+
/**
|
|
18
|
+
* Providers supported by the external execution mapping store.
|
|
19
|
+
*/
|
|
20
|
+
export type ExternalMappingProvider = 'linear' | 'jira' | 'asana' | 'monday' | 'pmo';
|
|
17
21
|
/**
|
|
18
22
|
* Canonical envelope for external issues/work items.
|
|
19
23
|
*
|
|
@@ -129,6 +133,36 @@ export interface ExternalIssueAdapter {
|
|
|
129
133
|
*/
|
|
130
134
|
fetchByQuery(query: Record<string, unknown>): Promise<IssueEnvelope[]>;
|
|
131
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Provider-agnostic external issue ↔ execution mapping record.
|
|
138
|
+
*/
|
|
139
|
+
export interface ExternalExecutionMapping {
|
|
140
|
+
provider: ExternalMappingProvider;
|
|
141
|
+
externalId: string;
|
|
142
|
+
externalKey: string | null;
|
|
143
|
+
canonicalUrl: string | null;
|
|
144
|
+
latestStateSnapshot: Record<string, unknown> | null;
|
|
145
|
+
executionIds: string[];
|
|
146
|
+
prUrls: string[];
|
|
147
|
+
lastSyncedAt: Date | null;
|
|
148
|
+
lastSpawnedAt: Date | null;
|
|
149
|
+
createdAt: Date;
|
|
150
|
+
updatedAt: Date;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Upsert payload for provider-agnostic external issue mapping.
|
|
154
|
+
*/
|
|
155
|
+
export interface UpsertExternalExecutionMappingInput {
|
|
156
|
+
provider: ExternalMappingProvider;
|
|
157
|
+
externalId: string;
|
|
158
|
+
externalKey?: string | null;
|
|
159
|
+
canonicalUrl?: string | null;
|
|
160
|
+
latestStateSnapshot?: Record<string, unknown> | null;
|
|
161
|
+
executionId?: string;
|
|
162
|
+
prUrl?: string;
|
|
163
|
+
lastSyncedAt?: Date;
|
|
164
|
+
lastSpawnedAt?: Date;
|
|
165
|
+
}
|
|
132
166
|
/**
|
|
133
167
|
* Error codes for external issue operations.
|
|
134
168
|
*/
|
|
@@ -16,6 +16,7 @@ const REQUIRED_STRING_FIELDS = [
|
|
|
16
16
|
'url',
|
|
17
17
|
'project_key',
|
|
18
18
|
];
|
|
19
|
+
const PRIORITY_VALUES = new Set(['P0', 'P1', 'P2', 'P3']);
|
|
19
20
|
/**
|
|
20
21
|
* Validate and construct an IssueEnvelope from untyped input.
|
|
21
22
|
*
|
|
@@ -145,6 +146,16 @@ export function validateIssueEnvelope(input) {
|
|
|
145
146
|
message: 'priority must be a string or null',
|
|
146
147
|
});
|
|
147
148
|
}
|
|
149
|
+
else if (typeof data.priority === 'string') {
|
|
150
|
+
const normalizedPriority = data.priority.trim().toUpperCase();
|
|
151
|
+
if (!PRIORITY_VALUES.has(normalizedPriority)) {
|
|
152
|
+
errors.push({
|
|
153
|
+
code: 'INVALID_FIELD_TYPE',
|
|
154
|
+
field: 'priority',
|
|
155
|
+
message: 'priority must be one of: P0, P1, P2, P3, or null',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
148
159
|
// Validate assignee (string or null)
|
|
149
160
|
if (!('assignee' in data) || data.assignee === undefined) {
|
|
150
161
|
errors.push({
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface MirrorResolutionInput {
|
|
2
|
+
flagValue?: boolean;
|
|
3
|
+
envValue?: boolean | null;
|
|
4
|
+
configValue?: boolean | null;
|
|
5
|
+
}
|
|
6
|
+
export interface MirrorResolution {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
source: 'flag' | 'env' | 'config' | 'default';
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveMirrorToPmo(input: MirrorResolutionInput): MirrorResolution;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function resolveMirrorToPmo(input) {
|
|
2
|
+
if (input.flagValue !== undefined) {
|
|
3
|
+
return { enabled: input.flagValue, source: 'flag' };
|
|
4
|
+
}
|
|
5
|
+
if (input.envValue !== null && input.envValue !== undefined) {
|
|
6
|
+
return { enabled: input.envValue, source: 'env' };
|
|
7
|
+
}
|
|
8
|
+
if (input.configValue !== null && input.configValue !== undefined) {
|
|
9
|
+
return { enabled: input.configValue, source: 'config' };
|
|
10
|
+
}
|
|
11
|
+
return { enabled: true, source: 'default' };
|
|
12
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { CreateTicketInput, PMOStorage, WorkflowStatus } from '../pmo/types
|
|
|
9
9
|
import type { LinearIssue, LinearIssueMap, LinearSyncResult } from './types.js';
|
|
10
10
|
export declare class LinearMapper {
|
|
11
11
|
private db;
|
|
12
|
+
private externalMappingStore;
|
|
12
13
|
constructor(db: Database.Database);
|
|
13
14
|
/**
|
|
14
15
|
* Ensure the linear_issue_map table exists.
|
|
@@ -64,4 +65,5 @@ export declare class LinearMapper {
|
|
|
64
65
|
* Convert a database row to a LinearIssueMap.
|
|
65
66
|
*/
|
|
66
67
|
private rowToMap;
|
|
68
|
+
private externalMappingToLinearMap;
|
|
67
69
|
}
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* Handles import (Linear → PMO) and reverse lookup for sync operations.
|
|
6
6
|
*/
|
|
7
7
|
import { PMO_TABLES } from '../pmo/schema.js';
|
|
8
|
+
import { ExternalExecutionMappingStore } from '../external-issues/mapping-store.js';
|
|
8
9
|
import { LINEAR_STATE_TO_PMO_CATEGORY, LINEAR_PRIORITY_TO_PMO, } from './types.js';
|
|
9
10
|
export class LinearMapper {
|
|
10
11
|
db;
|
|
12
|
+
externalMappingStore;
|
|
11
13
|
constructor(db) {
|
|
12
14
|
this.db = db;
|
|
15
|
+
this.externalMappingStore = new ExternalExecutionMappingStore(db);
|
|
13
16
|
this.ensureTable();
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
@@ -146,6 +149,18 @@ export class LinearMapper {
|
|
|
146
149
|
(pmo_ticket_id, linear_issue_id, linear_identifier, linear_team_key, linear_url, sync_direction, last_synced_at, created_at)
|
|
147
150
|
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
148
151
|
`).run(map.pmoTicketId, map.linearIssueId, map.linearIdentifier, map.linearTeamKey, map.linearUrl, map.syncDirection);
|
|
152
|
+
this.externalMappingStore.upsertMapping({
|
|
153
|
+
provider: 'linear',
|
|
154
|
+
externalId: map.linearIssueId,
|
|
155
|
+
externalKey: map.linearIdentifier,
|
|
156
|
+
canonicalUrl: map.linearUrl,
|
|
157
|
+
latestStateSnapshot: {
|
|
158
|
+
pmoTicketId: map.pmoTicketId,
|
|
159
|
+
linearTeamKey: map.linearTeamKey,
|
|
160
|
+
syncDirection: map.syncDirection,
|
|
161
|
+
},
|
|
162
|
+
lastSyncedAt: new Date(),
|
|
163
|
+
});
|
|
149
164
|
}
|
|
150
165
|
/**
|
|
151
166
|
* Get a mapping by PMO ticket ID.
|
|
@@ -163,7 +178,11 @@ export class LinearMapper {
|
|
|
163
178
|
const row = this.db.prepare(`
|
|
164
179
|
SELECT * FROM ${PMO_TABLES.linear_issue_map} WHERE linear_issue_id = ?
|
|
165
180
|
`).get(linearIssueId);
|
|
166
|
-
|
|
181
|
+
if (row) {
|
|
182
|
+
return this.rowToMap(row);
|
|
183
|
+
}
|
|
184
|
+
const mapping = this.externalMappingStore.getByExternalId('linear', linearIssueId);
|
|
185
|
+
return mapping ? this.externalMappingToLinearMap(mapping) : null;
|
|
167
186
|
}
|
|
168
187
|
/**
|
|
169
188
|
* Get a mapping by Linear identifier (e.g., "ENG-123").
|
|
@@ -172,7 +191,11 @@ export class LinearMapper {
|
|
|
172
191
|
const row = this.db.prepare(`
|
|
173
192
|
SELECT * FROM ${PMO_TABLES.linear_issue_map} WHERE linear_identifier = ?
|
|
174
193
|
`).get(identifier);
|
|
175
|
-
|
|
194
|
+
if (row) {
|
|
195
|
+
return this.rowToMap(row);
|
|
196
|
+
}
|
|
197
|
+
const mapping = this.externalMappingStore.getByExternalKey('linear', identifier);
|
|
198
|
+
return mapping ? this.externalMappingToLinearMap(mapping) : null;
|
|
176
199
|
}
|
|
177
200
|
/**
|
|
178
201
|
* List all mappings.
|
|
@@ -192,6 +215,21 @@ export class LinearMapper {
|
|
|
192
215
|
SET last_synced_at = CURRENT_TIMESTAMP
|
|
193
216
|
WHERE pmo_ticket_id = ?
|
|
194
217
|
`).run(pmoTicketId);
|
|
218
|
+
const map = this.getByTicketId(pmoTicketId);
|
|
219
|
+
if (map) {
|
|
220
|
+
this.externalMappingStore.upsertMapping({
|
|
221
|
+
provider: 'linear',
|
|
222
|
+
externalId: map.linearIssueId,
|
|
223
|
+
externalKey: map.linearIdentifier,
|
|
224
|
+
canonicalUrl: map.linearUrl,
|
|
225
|
+
latestStateSnapshot: {
|
|
226
|
+
pmoTicketId: map.pmoTicketId,
|
|
227
|
+
linearTeamKey: map.linearTeamKey,
|
|
228
|
+
syncDirection: map.syncDirection,
|
|
229
|
+
},
|
|
230
|
+
lastSyncedAt: new Date(),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
195
233
|
}
|
|
196
234
|
/**
|
|
197
235
|
* Delete a mapping by PMO ticket ID.
|
|
@@ -216,4 +254,30 @@ export class LinearMapper {
|
|
|
216
254
|
createdAt: new Date(row.created_at),
|
|
217
255
|
};
|
|
218
256
|
}
|
|
257
|
+
externalMappingToLinearMap(row) {
|
|
258
|
+
const snapshot = row.latestStateSnapshot ?? {};
|
|
259
|
+
const ticketId = typeof snapshot['pmoTicketId'] === 'string' ? snapshot['pmoTicketId'] : null;
|
|
260
|
+
if (!ticketId) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const teamKey = typeof snapshot['linearTeamKey'] === 'string'
|
|
264
|
+
? snapshot['linearTeamKey']
|
|
265
|
+
: (typeof snapshot['teamKey'] === 'string' ? snapshot['teamKey'] : null);
|
|
266
|
+
const resolvedTeamKey = teamKey
|
|
267
|
+
? teamKey
|
|
268
|
+
: (row.externalKey?.split('-')[0] ?? 'UNKNOWN');
|
|
269
|
+
const syncDirection = typeof snapshot['syncDirection'] === 'string'
|
|
270
|
+
? snapshot['syncDirection']
|
|
271
|
+
: 'inbound';
|
|
272
|
+
return {
|
|
273
|
+
pmoTicketId: ticketId,
|
|
274
|
+
linearIssueId: row.externalId,
|
|
275
|
+
linearIdentifier: row.externalKey ?? row.externalId,
|
|
276
|
+
linearTeamKey: resolvedTeamKey,
|
|
277
|
+
linearUrl: row.canonicalUrl ?? '',
|
|
278
|
+
syncDirection: syncDirection,
|
|
279
|
+
lastSyncedAt: row.lastSyncedAt ?? undefined,
|
|
280
|
+
createdAt: row.createdAt,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
219
283
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MondayBoard, MondayItem } from './types.js';
|
|
2
|
+
export declare class MondayClient {
|
|
3
|
+
private apiToken;
|
|
4
|
+
constructor(apiToken: string);
|
|
5
|
+
verify(): Promise<{
|
|
6
|
+
accountName: string;
|
|
7
|
+
userName: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
}>;
|
|
10
|
+
getBoard(boardId: string): Promise<MondayBoard | null>;
|
|
11
|
+
createItem(boardId: string, itemName: string): Promise<MondayItem>;
|
|
12
|
+
updateItemName(boardId: string, itemId: string, name: string): Promise<void>;
|
|
13
|
+
private request;
|
|
14
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const MONDAY_API_URL = 'https://api.monday.com/v2';
|
|
2
|
+
export class MondayClient {
|
|
3
|
+
apiToken;
|
|
4
|
+
constructor(apiToken) {
|
|
5
|
+
this.apiToken = apiToken;
|
|
6
|
+
}
|
|
7
|
+
async verify() {
|
|
8
|
+
const data = await this.request(`
|
|
9
|
+
query VerifyMondayConnection {
|
|
10
|
+
me {
|
|
11
|
+
id
|
|
12
|
+
name
|
|
13
|
+
email
|
|
14
|
+
}
|
|
15
|
+
account {
|
|
16
|
+
id
|
|
17
|
+
name
|
|
18
|
+
slug
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`);
|
|
22
|
+
return {
|
|
23
|
+
accountName: data.account.name,
|
|
24
|
+
userName: data.me.name,
|
|
25
|
+
email: data.me.email,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async getBoard(boardId) {
|
|
29
|
+
const data = await this.request(`
|
|
30
|
+
query GetBoard($boardIds: [ID!]) {
|
|
31
|
+
boards(ids: $boardIds) {
|
|
32
|
+
id
|
|
33
|
+
name
|
|
34
|
+
url
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`, { boardIds: [boardId] });
|
|
38
|
+
if (data.boards.length === 0)
|
|
39
|
+
return null;
|
|
40
|
+
return {
|
|
41
|
+
id: data.boards[0].id,
|
|
42
|
+
name: data.boards[0].name,
|
|
43
|
+
url: data.boards[0].url ?? undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async createItem(boardId, itemName) {
|
|
47
|
+
const data = await this.request(`
|
|
48
|
+
mutation CreateItem($boardId: ID!, $itemName: String!) {
|
|
49
|
+
create_item(board_id: $boardId, item_name: $itemName) {
|
|
50
|
+
id
|
|
51
|
+
name
|
|
52
|
+
url
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`, {
|
|
56
|
+
boardId,
|
|
57
|
+
itemName,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
id: data.create_item.id,
|
|
61
|
+
name: data.create_item.name,
|
|
62
|
+
url: data.create_item.url ?? undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async updateItemName(boardId, itemId, name) {
|
|
66
|
+
await this.request(`
|
|
67
|
+
mutation UpdateItemName($boardId: ID!, $itemId: ID!, $value: String!) {
|
|
68
|
+
change_simple_column_value(
|
|
69
|
+
board_id: $boardId
|
|
70
|
+
item_id: $itemId
|
|
71
|
+
column_id: "name"
|
|
72
|
+
value: $value
|
|
73
|
+
) {
|
|
74
|
+
id
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`, {
|
|
78
|
+
boardId,
|
|
79
|
+
itemId,
|
|
80
|
+
value: name,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async request(query, variables) {
|
|
84
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
85
|
+
const response = await fetch(MONDAY_API_URL, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: this.apiToken,
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ query, variables }),
|
|
92
|
+
});
|
|
93
|
+
const responseText = await response.text();
|
|
94
|
+
let payload;
|
|
95
|
+
try {
|
|
96
|
+
payload = JSON.parse(responseText);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error(`Monday API returned invalid JSON (${response.status}): ${responseText}`);
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const message = payload.errors?.[0]?.message ?? responseText;
|
|
103
|
+
throw new Error(`Monday API request failed (${response.status}): ${message}`);
|
|
104
|
+
}
|
|
105
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
106
|
+
throw new Error(payload.errors[0].message);
|
|
107
|
+
}
|
|
108
|
+
if (!payload.data) {
|
|
109
|
+
throw new Error('Monday API response missing data');
|
|
110
|
+
}
|
|
111
|
+
return payload.data;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { MondayConfig } from './types.js';
|
|
3
|
+
export declare function isMondayConfigured(db: Database.Database): boolean;
|
|
4
|
+
export declare function loadMondayConfig(db: Database.Database): MondayConfig | null;
|
|
5
|
+
export declare function saveMondayApiToken(db: Database.Database, apiToken: string): void;
|
|
6
|
+
export declare function saveMondayBoard(db: Database.Database, boardId: string, boardName: string): void;
|
|
7
|
+
export declare function saveMondayAccountName(db: Database.Database, accountName: string): void;
|
|
8
|
+
export declare function clearMondayConfig(db: Database.Database): void;
|
|
9
|
+
export declare function getMondayApiToken(db: Database.Database): string | null;
|
|
10
|
+
export declare function getMondayBoardId(db: Database.Database): string | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const SETTINGS_TABLE = 'workspace_settings';
|
|
2
|
+
const MONDAY_CONFIG_KEYS = {
|
|
3
|
+
apiToken: 'monday.api_token',
|
|
4
|
+
boardId: 'monday.board_id',
|
|
5
|
+
boardName: 'monday.board_name',
|
|
6
|
+
accountName: 'monday.account_name',
|
|
7
|
+
};
|
|
8
|
+
function getSetting(db, key) {
|
|
9
|
+
const row = db
|
|
10
|
+
.prepare(`SELECT value FROM ${SETTINGS_TABLE} WHERE key = ?`)
|
|
11
|
+
.get(key);
|
|
12
|
+
return row?.value ?? null;
|
|
13
|
+
}
|
|
14
|
+
function setSetting(db, key, value) {
|
|
15
|
+
db.prepare(`
|
|
16
|
+
INSERT INTO ${SETTINGS_TABLE} (key, value)
|
|
17
|
+
VALUES (?, ?)
|
|
18
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
19
|
+
`).run(key, value);
|
|
20
|
+
}
|
|
21
|
+
function deleteSetting(db, key) {
|
|
22
|
+
db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(key);
|
|
23
|
+
}
|
|
24
|
+
export function isMondayConfigured(db) {
|
|
25
|
+
return getSetting(db, MONDAY_CONFIG_KEYS.apiToken) !== null;
|
|
26
|
+
}
|
|
27
|
+
export function loadMondayConfig(db) {
|
|
28
|
+
const apiToken = getSetting(db, MONDAY_CONFIG_KEYS.apiToken);
|
|
29
|
+
if (!apiToken)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
apiToken,
|
|
33
|
+
boardId: getSetting(db, MONDAY_CONFIG_KEYS.boardId) ?? undefined,
|
|
34
|
+
boardName: getSetting(db, MONDAY_CONFIG_KEYS.boardName) ?? undefined,
|
|
35
|
+
accountName: getSetting(db, MONDAY_CONFIG_KEYS.accountName) ?? undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function saveMondayApiToken(db, apiToken) {
|
|
39
|
+
setSetting(db, MONDAY_CONFIG_KEYS.apiToken, apiToken);
|
|
40
|
+
}
|
|
41
|
+
export function saveMondayBoard(db, boardId, boardName) {
|
|
42
|
+
setSetting(db, MONDAY_CONFIG_KEYS.boardId, boardId);
|
|
43
|
+
setSetting(db, MONDAY_CONFIG_KEYS.boardName, boardName);
|
|
44
|
+
}
|
|
45
|
+
export function saveMondayAccountName(db, accountName) {
|
|
46
|
+
setSetting(db, MONDAY_CONFIG_KEYS.accountName, accountName);
|
|
47
|
+
}
|
|
48
|
+
export function clearMondayConfig(db) {
|
|
49
|
+
for (const key of Object.values(MONDAY_CONFIG_KEYS)) {
|
|
50
|
+
deleteSetting(db, key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function getMondayApiToken(db) {
|
|
54
|
+
const envToken = process.env.PRLT_MONDAY_API_TOKEN || process.env.MONDAY_API_TOKEN;
|
|
55
|
+
if (envToken)
|
|
56
|
+
return envToken;
|
|
57
|
+
return getSetting(db, MONDAY_CONFIG_KEYS.apiToken);
|
|
58
|
+
}
|
|
59
|
+
export function getMondayBoardId(db) {
|
|
60
|
+
const envBoardId = process.env.PRLT_MONDAY_BOARD_ID || process.env.MONDAY_BOARD_ID;
|
|
61
|
+
if (envBoardId)
|
|
62
|
+
return envBoardId;
|
|
63
|
+
return getSetting(db, MONDAY_CONFIG_KEYS.boardId);
|
|
64
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MondayClient } from './client.js';
|
|
2
|
+
export { isMondayConfigured, loadMondayConfig, saveMondayApiToken, saveMondayBoard, saveMondayAccountName, clearMondayConfig, getMondayApiToken, getMondayBoardId, } from './config.js';
|
|
3
|
+
export { MondayMapper } from './mapper.js';
|
|
4
|
+
export { MondaySync } from './sync.js';
|
|
5
|
+
export type { MondayBoard, MondayItem, MondayConfig, MondayItemMap, MondaySyncResult, } from './types.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { MondayClient } from './client.js';
|
|
2
|
+
export { isMondayConfigured, loadMondayConfig, saveMondayApiToken, saveMondayBoard, saveMondayAccountName, clearMondayConfig, getMondayApiToken, getMondayBoardId, } from './config.js';
|
|
3
|
+
export { MondayMapper } from './mapper.js';
|
|
4
|
+
export { MondaySync } from './sync.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { MondayItemMap } from './types.js';
|
|
3
|
+
export declare class MondayMapper {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: Database.Database);
|
|
6
|
+
private ensureTable;
|
|
7
|
+
createOrUpdateMapping(map: Omit<MondayItemMap, 'lastSyncedAt' | 'createdAt'>): void;
|
|
8
|
+
getByTicketId(ticketId: string): MondayItemMap | null;
|
|
9
|
+
getByMondayItemId(itemId: string): MondayItemMap | null;
|
|
10
|
+
listMappings(): MondayItemMap[];
|
|
11
|
+
updateSyncTimestamp(ticketId: string): void;
|
|
12
|
+
deleteMapping(ticketId: string): void;
|
|
13
|
+
private rowToMap;
|
|
14
|
+
}
|