@waron97/prbot 2.6.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -21
- package/agrippa_typings/__builtins__.pyi +99 -0
- package/agrippa_typings/b2w_entities.pyi +68 -0
- package/agrippa_typings/odoo_environment.pyi +1945 -0
- package/agrippa_typings/odoo_records.pyi +34176 -0
- package/agrippa_typings/recordset.pyi +38 -0
- package/package.json +10 -3
- package/src/agrippa/commands/clone.js +134 -0
- package/src/agrippa/commands/diff.js +96 -0
- package/src/agrippa/commands/init.js +116 -0
- package/src/agrippa/commands/initPhase.js +162 -0
- package/src/agrippa/commands/pull.js +221 -0
- package/src/agrippa/commands/push.js +126 -0
- package/src/agrippa/commands/repair.js +24 -0
- package/src/agrippa/index.js +61 -0
- package/src/agrippa/lib/api.js +106 -0
- package/src/agrippa/lib/checksum.js +7 -0
- package/src/agrippa/lib/config.js +35 -0
- package/src/agrippa/lib/workspace.js +42 -0
- package/src/commands/autopr.js +5 -0
- package/src/commands/export.js +2 -1
- package/src/commands/exportWorkflow.js +49 -0
- package/src/commands/init.js +3 -17
- package/src/commands/pr.js +8 -2
- package/src/config.js +1 -2
- package/src/index.js +11 -66
- package/.claude/settings.local.json +0 -11
- package/.prettierrc.mjs +0 -11
- package/CLAUDE.md +0 -0
- package/eslint.config.mjs +0 -16
- package/example.xml +0 -3903
- package/index.js +0 -376
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { readConfig, writeConfig, loadEffectiveEnv } from '../lib/config.js';
|
|
4
|
+
import { getPhasesByIds, getPhasesByWorkflow, listMfas } from '../lib/api.js';
|
|
5
|
+
import { getToken } from '../../lib/auth.js';
|
|
6
|
+
import { computeChecksum } from '../lib/checksum.js';
|
|
7
|
+
import { readCodeFile, writeCodeFile, fileExists, toSlug } from '../lib/workspace.js';
|
|
8
|
+
|
|
9
|
+
async function pull() {
|
|
10
|
+
const config = readConfig();
|
|
11
|
+
loadEffectiveEnv(config);
|
|
12
|
+
|
|
13
|
+
const ripUrl = process.env.RIP_URL;
|
|
14
|
+
if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
15
|
+
|
|
16
|
+
// Stale-file check
|
|
17
|
+
const stale = config.workspace.filter((e) => !fileExists(e.path));
|
|
18
|
+
if (stale.length) {
|
|
19
|
+
console.log('\nThe following tracked files no longer exist on disk:');
|
|
20
|
+
stale.forEach((e) => console.log(` - ${e.path} (${e.name})`));
|
|
21
|
+
const { cleanup } = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'confirm',
|
|
24
|
+
name: 'cleanup',
|
|
25
|
+
message: 'Remove these entries from the workspace config?',
|
|
26
|
+
default: true,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
if (cleanup) {
|
|
30
|
+
config.workspace = config.workspace.filter((e) => fileExists(e.path));
|
|
31
|
+
writeConfig(config);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const token = await getToken();
|
|
36
|
+
|
|
37
|
+
// ── pull existing tracked entries ─────────────────────────────────────────
|
|
38
|
+
if (config.workspace.length) {
|
|
39
|
+
console.log('Fetching remote code...');
|
|
40
|
+
const remoteCodeMap = await fetchRemoteCode(token, ripUrl, config.workspace);
|
|
41
|
+
|
|
42
|
+
const classified = config.workspace.map((entry) => {
|
|
43
|
+
const key = `${entry.object_type}:${entry.id}`;
|
|
44
|
+
const remoteCode = remoteCodeMap.get(key) ?? null;
|
|
45
|
+
const localCode = readCodeFile(entry.path);
|
|
46
|
+
|
|
47
|
+
const remoteChecksum = computeChecksum(remoteCode ?? '');
|
|
48
|
+
const localChecksum = computeChecksum(localCode ?? '');
|
|
49
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
50
|
+
|
|
51
|
+
let status;
|
|
52
|
+
if (pullChecksum === localChecksum && pullChecksum === remoteChecksum) {
|
|
53
|
+
status = 'unchanged';
|
|
54
|
+
} else if (pullChecksum === localChecksum) {
|
|
55
|
+
status = 'fast-forward';
|
|
56
|
+
} else {
|
|
57
|
+
status = 'conflict';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ...entry, remoteCode, status };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const changed = classified.filter((e) => e.status !== 'unchanged');
|
|
64
|
+
|
|
65
|
+
if (!changed.length) {
|
|
66
|
+
console.log('Everything is up to date.');
|
|
67
|
+
} else {
|
|
68
|
+
const selected = await selectEntries(changed, 'pull (overwrites local files)');
|
|
69
|
+
|
|
70
|
+
if (!selected.length) {
|
|
71
|
+
console.log('Nothing selected. No changes made.');
|
|
72
|
+
} else {
|
|
73
|
+
for (const entry of selected) {
|
|
74
|
+
writeCodeFile(entry.path, entry.remoteCode);
|
|
75
|
+
const idx = config.workspace.findIndex(
|
|
76
|
+
(e) => e.id === entry.id && e.object_type === entry.object_type,
|
|
77
|
+
);
|
|
78
|
+
if (idx !== -1) {
|
|
79
|
+
config.workspace[idx].checksum_at_pull = computeChecksum(entry.remoteCode);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
writeConfig(config);
|
|
83
|
+
console.log(`\nPulled ${selected.length} record(s).`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.log('No tracked resources. Run `agrippa clone` first.');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── discover new phases on tracked workflows ──────────────────────────────
|
|
91
|
+
await discoverNewPhases(token, ripUrl, config);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function discoverNewPhases(token, ripUrl, config) {
|
|
95
|
+
// Build map of workflow_id → { workflow_name, basePath } from existing phase entries
|
|
96
|
+
const workflows = new Map();
|
|
97
|
+
for (const entry of config.workspace) {
|
|
98
|
+
if (entry.object_type === 'phase' && entry.workflow_id && !workflows.has(entry.workflow_id)) {
|
|
99
|
+
workflows.set(entry.workflow_id, {
|
|
100
|
+
name: entry.workflow_name,
|
|
101
|
+
basePath: dirname(entry.path),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!workflows.size) return;
|
|
107
|
+
|
|
108
|
+
const trackedIds = new Set(
|
|
109
|
+
config.workspace.filter((e) => e.object_type === 'phase').map((e) => e.id),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
let newCount = 0;
|
|
113
|
+
for (const [wfId, { name: wfName, basePath }] of workflows) {
|
|
114
|
+
const phases = await getPhasesByWorkflow(token, ripUrl, wfId, { fromCode: true });
|
|
115
|
+
for (const phase of phases) {
|
|
116
|
+
if (trackedIds.has(phase.id)) continue;
|
|
117
|
+
const filePath = `${basePath}/${toSlug(phase.name)}.py`;
|
|
118
|
+
writeCodeFile(filePath, phase.code);
|
|
119
|
+
config.workspace.push({
|
|
120
|
+
path: filePath,
|
|
121
|
+
id: phase.id,
|
|
122
|
+
object_type: 'phase',
|
|
123
|
+
workflow_id: wfId,
|
|
124
|
+
workflow_name: wfName,
|
|
125
|
+
checksum_at_pull: computeChecksum(phase.code),
|
|
126
|
+
name: `${wfName} / ${phase.name}`,
|
|
127
|
+
});
|
|
128
|
+
console.log(` new phase: ${filePath}`);
|
|
129
|
+
newCount++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (newCount) {
|
|
134
|
+
writeConfig(config);
|
|
135
|
+
console.log(`Added ${newCount} new phase(s) from tracked workflows.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── shared helpers ────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
async function fetchRemoteCode(token, ripUrl, workspace) {
|
|
142
|
+
const map = new Map();
|
|
143
|
+
|
|
144
|
+
const phaseEntries = workspace.filter((e) => e.object_type === 'phase');
|
|
145
|
+
const mfaEntries = workspace.filter((e) => e.object_type === 'mfa');
|
|
146
|
+
|
|
147
|
+
if (phaseEntries.length) {
|
|
148
|
+
const ids = phaseEntries.map((e) => e.id);
|
|
149
|
+
const phases = await getPhasesByIds(token, ripUrl, ids);
|
|
150
|
+
phases.forEach((p) => map.set(`phase:${p.id}`, p.code));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (mfaEntries.length) {
|
|
154
|
+
const allMfas = await listMfas(token, ripUrl);
|
|
155
|
+
mfaEntries.forEach((e) => {
|
|
156
|
+
const remote = allMfas.find((m) => m.id === e.id);
|
|
157
|
+
if (remote) map.set(`mfa:${e.id}`, remote.code);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return map;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function selectEntries(changed, verb, mode = 'pull') {
|
|
165
|
+
// Group by parent folder for folder-level pre-selection
|
|
166
|
+
const folderMap = new Map();
|
|
167
|
+
for (const entry of changed) {
|
|
168
|
+
const folder = dirname(entry.path) || '.';
|
|
169
|
+
if (!folderMap.has(folder)) folderMap.set(folder, []);
|
|
170
|
+
folderMap.get(folder).push(entry);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const folders = [...folderMap.keys()];
|
|
174
|
+
|
|
175
|
+
// Folder-level pre-selection (only shown when there are multiple folders)
|
|
176
|
+
let includedFolders = new Set(folders);
|
|
177
|
+
if (folders.length > 1) {
|
|
178
|
+
const { selectedFolders } = await inquirer.prompt([
|
|
179
|
+
{
|
|
180
|
+
type: 'checkbox',
|
|
181
|
+
name: 'selectedFolders',
|
|
182
|
+
message: `Select workflows/folders to ${verb}:`,
|
|
183
|
+
choices: folders.map((f) => ({
|
|
184
|
+
name: `${f}/ (${folderMap.get(f).length} changed)`,
|
|
185
|
+
value: f,
|
|
186
|
+
checked: true,
|
|
187
|
+
})),
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
includedFolders = new Set(selectedFolders);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const candidates = changed.filter((e) => includedFolders.has(dirname(e.path) || '.'));
|
|
194
|
+
|
|
195
|
+
if (!candidates.length) return [];
|
|
196
|
+
|
|
197
|
+
const badgeFor = (status) => {
|
|
198
|
+
if (mode === 'push') {
|
|
199
|
+
return status === 'fast-forward' ? '↑ local updated' : '⚠ remote changes will be overwritten';
|
|
200
|
+
}
|
|
201
|
+
return status === 'fast-forward' ? '↑ remote updated' : '⚠ local changes will be lost';
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const { selected } = await inquirer.prompt([
|
|
205
|
+
{
|
|
206
|
+
type: 'checkbox',
|
|
207
|
+
name: 'selected',
|
|
208
|
+
message: `Fine-tune records to ${verb}: (space to toggle, a to toggle all)`,
|
|
209
|
+
choices: candidates.map((e) => ({
|
|
210
|
+
name: `[${dirname(e.path) || '.'}] ${e.name} [${badgeFor(e.status)}]`,
|
|
211
|
+
value: e,
|
|
212
|
+
checked: true,
|
|
213
|
+
})),
|
|
214
|
+
pageSize: 20,
|
|
215
|
+
},
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
return selected;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { pull, fetchRemoteCode, selectEntries };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { readConfig, writeConfig, loadEffectiveEnv } from '../lib/config.js';
|
|
5
|
+
import { updatePhase, updateMfa } from '../lib/api.js';
|
|
6
|
+
import { getToken } from '../../lib/auth.js';
|
|
7
|
+
import { computeChecksum } from '../lib/checksum.js';
|
|
8
|
+
import { readCodeFile, fileExists } from '../lib/workspace.js';
|
|
9
|
+
import { fetchRemoteCode, selectEntries } from './pull.js';
|
|
10
|
+
|
|
11
|
+
const BACKUP_DIR = '.backup';
|
|
12
|
+
|
|
13
|
+
async function push() {
|
|
14
|
+
const config = readConfig();
|
|
15
|
+
loadEffectiveEnv(config);
|
|
16
|
+
|
|
17
|
+
const ripUrl = process.env.RIP_URL;
|
|
18
|
+
if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
19
|
+
|
|
20
|
+
// Stale-file check
|
|
21
|
+
const stale = config.workspace.filter((e) => !fileExists(e.path));
|
|
22
|
+
if (stale.length) {
|
|
23
|
+
console.log('\nThe following tracked files no longer exist on disk:');
|
|
24
|
+
stale.forEach((e) => console.log(` - ${e.path} (${e.name})`));
|
|
25
|
+
const { cleanup } = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'confirm',
|
|
28
|
+
name: 'cleanup',
|
|
29
|
+
message: 'Remove these entries from the workspace config?',
|
|
30
|
+
default: true,
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
if (cleanup) {
|
|
34
|
+
config.workspace = config.workspace.filter((e) => fileExists(e.path));
|
|
35
|
+
writeConfig(config);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!config.workspace.length) {
|
|
40
|
+
console.log('No tracked resources. Run `agrippa clone` first.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log('Fetching remote code...');
|
|
45
|
+
const token = await getToken();
|
|
46
|
+
|
|
47
|
+
const remoteCodeMap = await fetchRemoteCode(token, ripUrl, config.workspace);
|
|
48
|
+
|
|
49
|
+
// Classify — concern here is remote work being overwritten
|
|
50
|
+
const classified = config.workspace.map((entry) => {
|
|
51
|
+
const key = `${entry.object_type}:${entry.id}`;
|
|
52
|
+
const remoteCode = remoteCodeMap.get(key) ?? null;
|
|
53
|
+
const localCode = readCodeFile(entry.path);
|
|
54
|
+
|
|
55
|
+
const remoteChecksum = computeChecksum(remoteCode ?? '');
|
|
56
|
+
const localChecksum = computeChecksum(localCode ?? '');
|
|
57
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
58
|
+
|
|
59
|
+
let status;
|
|
60
|
+
if (pullChecksum === localChecksum && pullChecksum === remoteChecksum) {
|
|
61
|
+
status = 'unchanged';
|
|
62
|
+
} else if (pullChecksum === remoteChecksum) {
|
|
63
|
+
// remote untouched since last pull, local has new changes
|
|
64
|
+
status = 'fast-forward';
|
|
65
|
+
} else {
|
|
66
|
+
// remote changed since last pull — pushing will overwrite remote work
|
|
67
|
+
status = 'conflict';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { ...entry, remoteCode, localCode, status };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const changed = classified.filter((e) => e.status !== 'unchanged');
|
|
74
|
+
|
|
75
|
+
if (!changed.length) {
|
|
76
|
+
console.log('Nothing to push — local files match the last-pulled state.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const selected = await selectEntries(changed, 'push (overwrites remote code)', 'push');
|
|
81
|
+
|
|
82
|
+
if (!selected.length) {
|
|
83
|
+
console.log('Nothing selected. No changes made.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Write backups of remote code before overwriting
|
|
88
|
+
const backupTs = new Date()
|
|
89
|
+
.toISOString()
|
|
90
|
+
.replace(/:/g, '-')
|
|
91
|
+
.replace('T', '_')
|
|
92
|
+
.slice(0, 19);
|
|
93
|
+
|
|
94
|
+
for (const entry of selected) {
|
|
95
|
+
if (entry.remoteCode !== null) {
|
|
96
|
+
const backupPath = join(BACKUP_DIR, backupTs, entry.path);
|
|
97
|
+
mkdirSync(dirname(backupPath), { recursive: true });
|
|
98
|
+
writeFileSync(backupPath, (entry.remoteCode ?? '').trim() + '\n', 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log(`Remote backups written to ${BACKUP_DIR}/${backupTs}/`);
|
|
102
|
+
|
|
103
|
+
// Push local code to API
|
|
104
|
+
let pushed = 0;
|
|
105
|
+
for (const entry of selected) {
|
|
106
|
+
const code = entry.localCode ?? '';
|
|
107
|
+
if (entry.object_type === 'phase') {
|
|
108
|
+
await updatePhase(token, ripUrl, entry.id, code);
|
|
109
|
+
} else {
|
|
110
|
+
await updateMfa(token, ripUrl, entry.id, code);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const idx = config.workspace.findIndex(
|
|
114
|
+
(e) => e.id === entry.id && e.object_type === entry.object_type,
|
|
115
|
+
);
|
|
116
|
+
if (idx !== -1) {
|
|
117
|
+
config.workspace[idx].checksum_at_pull = computeChecksum(code);
|
|
118
|
+
}
|
|
119
|
+
pushed++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
writeConfig(config);
|
|
123
|
+
console.log(`\nPushed ${pushed} record(s).`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { push };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from '../lib/config.js';
|
|
2
|
+
import { fileExists } from '../lib/workspace.js';
|
|
3
|
+
|
|
4
|
+
async function repair() {
|
|
5
|
+
const config = readConfig();
|
|
6
|
+
const before = config.workspace.length;
|
|
7
|
+
|
|
8
|
+
const stale = config.workspace.filter((entry) => !fileExists(entry.path));
|
|
9
|
+
config.workspace = config.workspace.filter((entry) => fileExists(entry.path));
|
|
10
|
+
|
|
11
|
+
if (!stale.length) {
|
|
12
|
+
console.log('No stale entries found.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const entry of stale) {
|
|
17
|
+
console.log(` removed: ${entry.path} (${entry.name})`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
writeConfig(config);
|
|
21
|
+
console.log(`Removed ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'} (${before} → ${config.workspace.length}).`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { repair };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { init } from './commands/init.js';
|
|
5
|
+
import { clone } from './commands/clone.js';
|
|
6
|
+
import { pull } from './commands/pull.js';
|
|
7
|
+
import { push } from './commands/push.js';
|
|
8
|
+
import { diff } from './commands/diff.js';
|
|
9
|
+
import { initPhase } from './commands/initPhase.js';
|
|
10
|
+
import { repair } from './commands/repair.js';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { version } = require('../../package.json');
|
|
14
|
+
|
|
15
|
+
process.on('unhandledRejection', (err) => {
|
|
16
|
+
console.error(`Error: ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
program.name('agrippa').version(version);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('init')
|
|
24
|
+
.description('Create agrippa.yaml workspace config in the current directory')
|
|
25
|
+
.action(() => init().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('clone')
|
|
29
|
+
.description('Clone a phase or MFA from RIP into this workspace')
|
|
30
|
+
.option('--phase', 'Clone a phase (select a workflow)')
|
|
31
|
+
.option('--mfa', 'Clone a Model Function Access record')
|
|
32
|
+
.option('--id <id>', 'Record ID to clone')
|
|
33
|
+
.option('--path <path>', 'Destination path (file for MFA, base dir for workflow)')
|
|
34
|
+
.action((opts) => clone(opts).catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('pull')
|
|
38
|
+
.description('Pull remote changes into local files')
|
|
39
|
+
.action(() => pull().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('push')
|
|
43
|
+
.description('Push local changes to RIP (backs up remote code first)')
|
|
44
|
+
.action(() => push().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command('diff [path]')
|
|
48
|
+
.description('Show differences between local files and remote code')
|
|
49
|
+
.action((path) => diff(path).catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('init-phase')
|
|
53
|
+
.description('Initialize a phase with default code template and result vars')
|
|
54
|
+
.action(() => initPhase().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('repair')
|
|
58
|
+
.description('Remove stale workspace entries where local file no longer exists')
|
|
59
|
+
.action(() => repair().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); }));
|
|
60
|
+
|
|
61
|
+
program.parse();
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
|
|
3
|
+
async function makeRequest(method, path, token, ripUrl, body) {
|
|
4
|
+
const headers = {
|
|
5
|
+
Authorization: `Bearer ${token}`,
|
|
6
|
+
...(method !== 'GET' ? { 'Content-Type': 'application/json' } : {}),
|
|
7
|
+
};
|
|
8
|
+
const res = await fetch(`${ripUrl}${path}`, {
|
|
9
|
+
method,
|
|
10
|
+
headers,
|
|
11
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`API ${method} ${path} failed with ${res.status}: ${await res.text()}`);
|
|
15
|
+
}
|
|
16
|
+
return res.json();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function listWorkflows(token, ripUrl) {
|
|
20
|
+
return makeRequest('GET', '/symple.workflow/*', token, ripUrl);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getPhasesByWorkflow(token, ripUrl, workflowId, { fromCode = false } = {}) {
|
|
24
|
+
const filter = fromCode
|
|
25
|
+
? `[('workflow_id', '=', ${workflowId}), ('set_result_automatically', '=', 'from_code')]`
|
|
26
|
+
: `[('workflow_id', '=', ${workflowId})]`;
|
|
27
|
+
return makeRequest('GET', `/symple.triplet.phase/*?_filter_=${filter}`, token, ripUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getPhasesByIds(token, ripUrl, ids) {
|
|
31
|
+
return makeRequest(
|
|
32
|
+
'GET',
|
|
33
|
+
`/symple.triplet.phase/*?_filter_=[('id', 'in', [${ids.join(',')}])]`,
|
|
34
|
+
token,
|
|
35
|
+
ripUrl,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function updatePhase(token, ripUrl, phaseId, code) {
|
|
40
|
+
return makeRequest('PUT', `/symple.triplet.phase/${phaseId}`, token, ripUrl, { code });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function listMfas(token, ripUrl) {
|
|
44
|
+
return makeRequest('GET', '/symple.workflow/get_mfas', token, ripUrl);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateMfa(token, ripUrl, mfaId, code) {
|
|
48
|
+
return makeRequest('POST', '/symple.workflow/update_mfa', token, ripUrl, { id: mfaId, code });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getPhaseResults(token, ripUrl, phaseId) {
|
|
52
|
+
try {
|
|
53
|
+
return await makeRequest(
|
|
54
|
+
'GET',
|
|
55
|
+
`/symple.triplet.phase.result/*?_filter_=[('code_phase_id', '=', ${phaseId})]`,
|
|
56
|
+
token,
|
|
57
|
+
ripUrl,
|
|
58
|
+
);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.message.includes('404')) return [];
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function initPhaseRemote(token, ripUrl, phaseId, code) {
|
|
66
|
+
return makeRequest('PUT', `/symple.triplet.phase/${phaseId}`, token, ripUrl, {
|
|
67
|
+
set_result_automatically: 'from_code',
|
|
68
|
+
code,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getPhaseConfigurators(token, ripUrl, phaseId) {
|
|
73
|
+
try {
|
|
74
|
+
return await makeRequest(
|
|
75
|
+
'GET',
|
|
76
|
+
`/result.code.configurator/*?_filter_=[('code_phase_id', '=', ${phaseId})]`,
|
|
77
|
+
token,
|
|
78
|
+
ripUrl,
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err.message.includes('404')) return [];
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deleteConfigurator(token, ripUrl, id) {
|
|
87
|
+
return makeRequest('DELETE', `/result.code.configurator/${id}`, token, ripUrl);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createConfigurator(token, ripUrl, data) {
|
|
91
|
+
return makeRequest('POST', `/result.code.configurator`, token, ripUrl, data);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
listWorkflows,
|
|
96
|
+
getPhasesByWorkflow,
|
|
97
|
+
getPhasesByIds,
|
|
98
|
+
updatePhase,
|
|
99
|
+
listMfas,
|
|
100
|
+
updateMfa,
|
|
101
|
+
getPhaseResults,
|
|
102
|
+
initPhaseRemote,
|
|
103
|
+
getPhaseConfigurators,
|
|
104
|
+
deleteConfigurator,
|
|
105
|
+
createConfigurator,
|
|
106
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { parse, stringify } from 'yaml';
|
|
3
|
+
import { configDotenv } from 'dotenv';
|
|
4
|
+
import { CONFIG_FILE } from '../../config.js';
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_FILE = 'agrippa.yaml';
|
|
7
|
+
|
|
8
|
+
function readConfig() {
|
|
9
|
+
if (!existsSync(WORKSPACE_FILE)) {
|
|
10
|
+
throw new Error('agrippa.yaml not found in current directory. Run `agrippa init` first.');
|
|
11
|
+
}
|
|
12
|
+
return parse(readFileSync(WORKSPACE_FILE, 'utf-8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeConfig(config) {
|
|
16
|
+
writeFileSync(WORKSPACE_FILE, stringify(config, { lineWidth: 0 }), 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadEffectiveEnv(localConfig) {
|
|
20
|
+
// Load global prbot config as the base (KC_URL, KC_USER, RIP_URL, etc. live here)
|
|
21
|
+
if (existsSync(CONFIG_FILE)) {
|
|
22
|
+
configDotenv({ path: CONFIG_FILE });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Overlay workspace-level overrides from agrippa.yaml's agrippa: section
|
|
26
|
+
const overrides = localConfig?.agrippa ?? {};
|
|
27
|
+
const keys = ['KC_URL', 'KC_USER', 'KC_PASSWORD', 'KC_ID', 'KC_SECRET', 'RIP_URL'];
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (overrides[key]) {
|
|
30
|
+
process.env[key] = overrides[key];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { readConfig, writeConfig, loadEffectiveEnv, WORKSPACE_FILE };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import slugify from 'slugify';
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
function toSlug(name) {
|
|
6
|
+
return slugify(name, { lower: true, strict: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function defaultPhasePath(workflowName, phaseName) {
|
|
10
|
+
return `${toSlug(workflowName)}/${toSlug(phaseName)}.py`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toMfaSlug(name) {
|
|
14
|
+
return slugify(name, { lower: true, replacement: '_' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function defaultMfaPath(modelName, mfaName) {
|
|
18
|
+
return `${modelName}/${toMfaSlug(mfaName)}.py`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureDir(filePath) {
|
|
22
|
+
const dir = dirname(filePath);
|
|
23
|
+
if (dir && dir !== '.') {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeCodeFile(filePath, content) {
|
|
29
|
+
ensureDir(filePath);
|
|
30
|
+
writeFileSync(filePath, (content ?? '').trim() + '\n', 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readCodeFile(filePath) {
|
|
34
|
+
if (!existsSync(filePath)) return null;
|
|
35
|
+
return readFileSync(filePath, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fileExists(filePath) {
|
|
39
|
+
return existsSync(filePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { toSlug, defaultPhasePath, defaultMfaPath, ensureDir, writeCodeFile, readCodeFile, fileExists };
|
package/src/commands/autopr.js
CHANGED
|
@@ -73,6 +73,10 @@ function buildPrDescription(taskIds, jiras) {
|
|
|
73
73
|
async function createDevopsPR(branch, title, description) {
|
|
74
74
|
const { DEVOPS_ORG, DEVOPS_PROJECT, DEVOPS_REPO, AUTOPR_TARGET_BRANCH } = process.env;
|
|
75
75
|
const apiUrl = `https://dev.azure.com/${DEVOPS_ORG}/${DEVOPS_PROJECT}/_apis/git/repositories/${DEVOPS_REPO}/pullrequests?api-version=7.0`;
|
|
76
|
+
const reviewers = process.env.AUTOPR_REQUIRED_REVIEWER_ID
|
|
77
|
+
? [{ id: process.env.AUTOPR_REQUIRED_REVIEWER_ID, isRequired: true }]
|
|
78
|
+
: [];
|
|
79
|
+
|
|
76
80
|
const res = await fetch(apiUrl, {
|
|
77
81
|
method: 'POST',
|
|
78
82
|
headers: devopsHeaders(),
|
|
@@ -82,6 +86,7 @@ async function createDevopsPR(branch, title, description) {
|
|
|
82
86
|
isDraft: true,
|
|
83
87
|
sourceRefName: `refs/heads/${branch}`,
|
|
84
88
|
targetRefName: `refs/heads/${AUTOPR_TARGET_BRANCH}`,
|
|
89
|
+
reviewers,
|
|
85
90
|
}),
|
|
86
91
|
});
|
|
87
92
|
const data = await res.json();
|
package/src/commands/export.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { exportPb } from './exportPb.js';
|
|
2
2
|
import { exportImperex } from './exportImperex.js';
|
|
3
3
|
import { exportEmailTemplates } from './exportEmailTemplates.js';
|
|
4
|
+
import { exportWorkflow } from './exportWorkflow.js';
|
|
4
5
|
|
|
5
6
|
function exportRip() {
|
|
6
7
|
console.log('Not implemented yet.');
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export { exportPb, exportRip, exportImperex, exportEmailTemplates };
|
|
10
|
+
export { exportPb, exportRip, exportImperex, exportEmailTemplates, exportWorkflow };
|