@waron97/prbot 2.8.0 → 3.0.1
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 +79 -0
- 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/exportEmailTemplates.js +1 -1
- package/.prettierrc.mjs +0 -11
- package/CLAUDE.md +0 -0
- package/eslint.config.mjs +0 -16
- package/example.xml +0 -3903
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Shared Recordset base class for all Odoo models."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union, Iterator
|
|
4
|
+
import datetime as _dt
|
|
5
|
+
|
|
6
|
+
class Recordset:
|
|
7
|
+
"""Base class for all Odoo model record objects."""
|
|
8
|
+
|
|
9
|
+
id: int
|
|
10
|
+
ids: List[int]
|
|
11
|
+
create_date: _dt.date
|
|
12
|
+
write_date: _dt.date
|
|
13
|
+
create_uid: "Recordset"
|
|
14
|
+
write_uid: "Recordset"
|
|
15
|
+
def __getattr__(self, name: str) -> Any: ...
|
|
16
|
+
def browse(self, ids: Union[int, List[int]]) -> "Recordset": ...
|
|
17
|
+
def write(self, vals: Dict[str, Any]) -> bool: ...
|
|
18
|
+
def search(
|
|
19
|
+
self,
|
|
20
|
+
domain: List[Any],
|
|
21
|
+
limit: Optional[int] = None,
|
|
22
|
+
order: Optional[str] = None,
|
|
23
|
+
offset: int = 0,
|
|
24
|
+
) -> "Recordset": ...
|
|
25
|
+
def create(self, vals: Dict[str, Any]) -> "Recordset": ...
|
|
26
|
+
def sudo(self) -> "Recordset": ...
|
|
27
|
+
def with_context(self, ctx: Dict[str, Any] = ..., **kwargs: Any) -> "Recordset": ...
|
|
28
|
+
def filtered(self, func: Any) -> "Recordset": ...
|
|
29
|
+
def mapped(self, field: str) -> Any: ...
|
|
30
|
+
def sorted(self, key: Any = None, reverse: bool = False) -> "Recordset": ...
|
|
31
|
+
def exists(self) -> "Recordset": ...
|
|
32
|
+
def unlink(self) -> bool: ...
|
|
33
|
+
def get_payload(self) -> Any: ...
|
|
34
|
+
def __getitem__(self, key: Union[str, int]) -> Any: ...
|
|
35
|
+
def __setitem__(self, key: str, value: Any) -> None: ...
|
|
36
|
+
def __bool__(self) -> bool: ...
|
|
37
|
+
def __iter__(self) -> Iterator["Recordset"]: ...
|
|
38
|
+
def __len__(self) -> int: ...
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waron97/prbot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"prbot": "src/index.js"
|
|
10
|
+
"prbot": "src/index.js",
|
|
11
|
+
"agrippa": "src/agrippa/index.js"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
14
|
"@inquirer/search": "^2.0.1",
|
|
@@ -18,7 +19,9 @@
|
|
|
18
19
|
"inquirer-search-list": "^1.2.6",
|
|
19
20
|
"node-fetch": "^3.3.2",
|
|
20
21
|
"omelette": "^0.4.17",
|
|
21
|
-
"prettier": "^3.5.3"
|
|
22
|
+
"prettier": "^3.5.3",
|
|
23
|
+
"slugify": "^1.6.9",
|
|
24
|
+
"yaml": "^2.9.0"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"@eslint/js": "^9.21.0",
|
|
@@ -28,6 +31,10 @@
|
|
|
28
31
|
"typescript": "^5.8.2",
|
|
29
32
|
"typescript-eslint": "^8.26.0"
|
|
30
33
|
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"agrippa_typings/"
|
|
37
|
+
],
|
|
31
38
|
"keywords": [],
|
|
32
39
|
"author": "",
|
|
33
40
|
"license": "ISC",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import select from '@inquirer/select';
|
|
3
|
+
import search from '@inquirer/search';
|
|
4
|
+
import { readConfig, writeConfig, loadEffectiveEnv } from '../lib/config.js';
|
|
5
|
+
import { listWorkflows, getPhasesByWorkflow, listMfas } from '../lib/api.js';
|
|
6
|
+
import { getToken } from '../../lib/auth.js';
|
|
7
|
+
import { computeChecksum } from '../lib/checksum.js';
|
|
8
|
+
import { toSlug, defaultMfaPath, writeCodeFile } from '../lib/workspace.js';
|
|
9
|
+
import { fuzzyMatch } from '../../lib/fuzzy.js';
|
|
10
|
+
|
|
11
|
+
async function clone(opts) {
|
|
12
|
+
const config = readConfig();
|
|
13
|
+
loadEffectiveEnv(config);
|
|
14
|
+
|
|
15
|
+
const ripUrl = process.env.RIP_URL;
|
|
16
|
+
if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
17
|
+
|
|
18
|
+
// Determine object type
|
|
19
|
+
let objectType = opts.mfa ? 'mfa' : opts.phase ? 'phase' : null;
|
|
20
|
+
if (!objectType) {
|
|
21
|
+
objectType = await select({
|
|
22
|
+
message: 'What do you want to clone?',
|
|
23
|
+
choices: [
|
|
24
|
+
{ name: 'MFA', value: 'mfa' },
|
|
25
|
+
{ name: 'Phase', value: 'phase' },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('Fetching records...');
|
|
31
|
+
const token = await getToken();
|
|
32
|
+
|
|
33
|
+
let records;
|
|
34
|
+
if (objectType === 'phase') {
|
|
35
|
+
records = await listWorkflows(token, ripUrl);
|
|
36
|
+
} else {
|
|
37
|
+
records = await listMfas(token, ripUrl);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!records.length) {
|
|
41
|
+
console.log(`No ${objectType} records found.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Determine record
|
|
46
|
+
let record;
|
|
47
|
+
if (opts.id) {
|
|
48
|
+
const id = parseInt(opts.id, 10);
|
|
49
|
+
record = records.find((r) => r.id === id);
|
|
50
|
+
if (!record) throw new Error(`No ${objectType} found with id ${opts.id}`);
|
|
51
|
+
} else {
|
|
52
|
+
record = await search({
|
|
53
|
+
message: `Select a ${objectType}:`,
|
|
54
|
+
source: (input) => {
|
|
55
|
+
const filtered = input
|
|
56
|
+
? records.filter((r) => fuzzyMatch(r.name, input))
|
|
57
|
+
: records;
|
|
58
|
+
return filtered.map((r) => ({
|
|
59
|
+
name: objectType === 'mfa' ? `${r.model_name} / ${r.name}` : r.name,
|
|
60
|
+
value: r,
|
|
61
|
+
}));
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Determine path
|
|
67
|
+
let basePath = opts.path ?? null;
|
|
68
|
+
|
|
69
|
+
if (objectType === 'phase') {
|
|
70
|
+
if (!basePath) {
|
|
71
|
+
const { inputPath } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'input',
|
|
74
|
+
name: 'inputPath',
|
|
75
|
+
message: 'Base directory for phases:',
|
|
76
|
+
default: toSlug(record.name),
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
basePath = inputPath;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`Fetching phases for "${record.name}"...`);
|
|
83
|
+
const phases = await getPhasesByWorkflow(token, ripUrl, record.id, { fromCode: true });
|
|
84
|
+
|
|
85
|
+
if (!phases.length) {
|
|
86
|
+
console.log('No phases found.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const phase of phases) {
|
|
91
|
+
const filePath = `${basePath}/${toSlug(phase.name)}.py`;
|
|
92
|
+
writeCodeFile(filePath, phase.code);
|
|
93
|
+
config.workspace.push({
|
|
94
|
+
path: filePath,
|
|
95
|
+
id: phase.id,
|
|
96
|
+
object_type: 'phase',
|
|
97
|
+
workflow_id: record.id,
|
|
98
|
+
workflow_name: record.name,
|
|
99
|
+
checksum_at_pull: computeChecksum(phase.code),
|
|
100
|
+
name: `${record.name} / ${phase.name}`,
|
|
101
|
+
});
|
|
102
|
+
console.log(` wrote ${filePath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`Cloned ${phases.length} phase(s) to ${basePath}/`);
|
|
106
|
+
} else {
|
|
107
|
+
const defaultPath = defaultMfaPath(record.model_name, record.name);
|
|
108
|
+
if (!basePath) {
|
|
109
|
+
const { inputPath } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: 'input',
|
|
112
|
+
name: 'inputPath',
|
|
113
|
+
message: 'Path for MFA file:',
|
|
114
|
+
default: defaultPath,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
basePath = inputPath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
writeCodeFile(basePath, record.code);
|
|
121
|
+
config.workspace.push({
|
|
122
|
+
path: basePath,
|
|
123
|
+
id: record.id,
|
|
124
|
+
object_type: 'mfa',
|
|
125
|
+
checksum_at_pull: computeChecksum(record.code),
|
|
126
|
+
name: `${record.model_name} / ${record.name}`,
|
|
127
|
+
});
|
|
128
|
+
console.log(`Cloned MFA to ${basePath}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
writeConfig(config);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { clone };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, statSync, writeFileSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { readConfig, loadEffectiveEnv } from '../lib/config.js';
|
|
6
|
+
import { fetchRemoteCode } from './pull.js';
|
|
7
|
+
import { getToken } from '../../lib/auth.js';
|
|
8
|
+
import { readCodeFile, fileExists } from '../lib/workspace.js';
|
|
9
|
+
import { computeChecksum } from '../lib/checksum.js';
|
|
10
|
+
|
|
11
|
+
async function diff(targetArg) {
|
|
12
|
+
const config = readConfig();
|
|
13
|
+
loadEffectiveEnv(config);
|
|
14
|
+
|
|
15
|
+
const ripUrl = process.env.RIP_URL;
|
|
16
|
+
if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
17
|
+
|
|
18
|
+
if (!config.workspace.length) {
|
|
19
|
+
console.log('No tracked resources. Run `agrippa clone` first.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const entries = filterEntries(config.workspace, targetArg);
|
|
24
|
+
if (!entries.length) {
|
|
25
|
+
console.log('No tracked files match the given path.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('Fetching remote code...');
|
|
30
|
+
const token = await getToken();
|
|
31
|
+
const remoteCodeMap = await fetchRemoteCode(token, ripUrl, entries);
|
|
32
|
+
|
|
33
|
+
let diffCount = 0;
|
|
34
|
+
const tmpFiles = [];
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const key = `${entry.object_type}:${entry.id}`;
|
|
39
|
+
const remoteCode = remoteCodeMap.get(key) ?? '';
|
|
40
|
+
const localCode = readCodeFile(entry.path) ?? '';
|
|
41
|
+
|
|
42
|
+
if (computeChecksum(localCode) === computeChecksum(remoteCode)) continue;
|
|
43
|
+
|
|
44
|
+
if (!fileExists(entry.path)) {
|
|
45
|
+
console.log(`\n--- ${entry.path} (local file missing)`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write remote content to a temp file so git diff --no-index can compare
|
|
50
|
+
const tmpPath = join(tmpdir(), `agrippa-remote-${entry.object_type}-${entry.id}.py`);
|
|
51
|
+
writeFileSync(tmpPath, (remoteCode ?? '').trim() + '\n', 'utf-8');
|
|
52
|
+
tmpFiles.push(tmpPath);
|
|
53
|
+
|
|
54
|
+
console.log(`\n=== ${entry.path} [${entry.name}] ===`);
|
|
55
|
+
|
|
56
|
+
const result = spawnSync(
|
|
57
|
+
'git',
|
|
58
|
+
['diff', '--no-index', '--color=always', tmpPath, entry.path],
|
|
59
|
+
{ stdio: ['ignore', 'inherit', 'inherit'] },
|
|
60
|
+
);
|
|
61
|
+
// exit code 1 means differences found (normal), 0 means identical, >1 means error
|
|
62
|
+
if (result.status !== null && result.status > 1) {
|
|
63
|
+
console.error(`git diff failed for ${entry.path}`);
|
|
64
|
+
}
|
|
65
|
+
diffCount++;
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
for (const f of tmpFiles) {
|
|
69
|
+
try { unlinkSync(f); } catch { /* ignore */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (diffCount === 0) {
|
|
74
|
+
console.log('No differences found — all tracked files match the remote.');
|
|
75
|
+
} else {
|
|
76
|
+
console.log(`\n${diffCount} file(s) differ from remote.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function filterEntries(workspace, targetArg) {
|
|
81
|
+
if (!targetArg) return workspace;
|
|
82
|
+
|
|
83
|
+
// Normalise: strip trailing slash
|
|
84
|
+
const target = targetArg.replace(/\/$/, '');
|
|
85
|
+
|
|
86
|
+
// If it exists on disk and is a directory, filter by prefix
|
|
87
|
+
if (existsSync(target) && statSync(target).isDirectory()) {
|
|
88
|
+
const prefix = target.endsWith('/') ? target : target + '/';
|
|
89
|
+
return workspace.filter((e) => e.path.startsWith(prefix));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Otherwise treat as exact file path (whether it exists yet or not)
|
|
93
|
+
return workspace.filter((e) => e.path === target);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { diff };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { WORKSPACE_FILE } from '../lib/config.js';
|
|
6
|
+
|
|
7
|
+
const TEMPLATE = `# agrippa workspace configuration
|
|
8
|
+
# Keycloak and RIP credentials are taken from the global prbot config by default.
|
|
9
|
+
# Uncomment and set values here only to override them for this workspace.
|
|
10
|
+
agrippa: {}
|
|
11
|
+
# KC_URL: ""
|
|
12
|
+
# KC_USER: ""
|
|
13
|
+
# KC_PASSWORD: ""
|
|
14
|
+
# KC_ID: ""
|
|
15
|
+
# KC_SECRET: ""
|
|
16
|
+
# RIP_URL: ""
|
|
17
|
+
|
|
18
|
+
workspace: []
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const PYPROJECT_TOML = `[tool.ruff]
|
|
22
|
+
builtins = [
|
|
23
|
+
"case_id",
|
|
24
|
+
"request",
|
|
25
|
+
"format_exc",
|
|
26
|
+
"env",
|
|
27
|
+
"log",
|
|
28
|
+
"json_dumps",
|
|
29
|
+
"ValidationError",
|
|
30
|
+
"datetime",
|
|
31
|
+
"date",
|
|
32
|
+
"dateutil",
|
|
33
|
+
"time",
|
|
34
|
+
"pytz",
|
|
35
|
+
"body",
|
|
36
|
+
"make_response",
|
|
37
|
+
"logger",
|
|
38
|
+
"first",
|
|
39
|
+
"args",
|
|
40
|
+
"model",
|
|
41
|
+
"json_loads",
|
|
42
|
+
"record",
|
|
43
|
+
"records"
|
|
44
|
+
]
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const PYRIGHTCONFIG = JSON.stringify(
|
|
48
|
+
{
|
|
49
|
+
pythonVersion: '3.10',
|
|
50
|
+
stubPath: 'typings',
|
|
51
|
+
typeCheckingMode: 'standard',
|
|
52
|
+
reportArgumentType: 'none',
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
) + '\n';
|
|
57
|
+
|
|
58
|
+
const TYPING_FILES = [
|
|
59
|
+
'__builtins__.pyi',
|
|
60
|
+
'odoo_environment.pyi',
|
|
61
|
+
'odoo_records.pyi',
|
|
62
|
+
'recordset.pyi',
|
|
63
|
+
'b2w_entities.pyi',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
async function init() {
|
|
67
|
+
if (existsSync(WORKSPACE_FILE)) {
|
|
68
|
+
const { overwrite } = await inquirer.prompt([
|
|
69
|
+
{
|
|
70
|
+
type: 'confirm',
|
|
71
|
+
name: 'overwrite',
|
|
72
|
+
message: `${WORKSPACE_FILE} already exists. Overwrite?`,
|
|
73
|
+
default: false,
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
if (!overwrite) {
|
|
77
|
+
console.log('Aborted.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
writeFileSync(WORKSPACE_FILE, TEMPLATE, 'utf-8');
|
|
83
|
+
console.log(`Created ${WORKSPACE_FILE}`);
|
|
84
|
+
console.log(`Run 'agrippa clone' to add resources to this workspace.`);
|
|
85
|
+
console.log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
|
|
86
|
+
|
|
87
|
+
if (!existsSync('pyproject.toml')) {
|
|
88
|
+
writeFileSync('pyproject.toml', PYPROJECT_TOML, 'utf-8');
|
|
89
|
+
console.log('Created pyproject.toml (ruff builtins)');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { importTypings } = await inquirer.prompt([
|
|
93
|
+
{
|
|
94
|
+
type: 'confirm',
|
|
95
|
+
name: 'importTypings',
|
|
96
|
+
message: 'Import pyright config and type stubs?',
|
|
97
|
+
default: true,
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
if (importTypings) {
|
|
102
|
+
if (!existsSync('pyrightconfig.json')) {
|
|
103
|
+
writeFileSync('pyrightconfig.json', PYRIGHTCONFIG, 'utf-8');
|
|
104
|
+
console.log('Created pyrightconfig.json');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const typingsDir = fileURLToPath(new URL('../../../agrippa_typings', import.meta.url));
|
|
108
|
+
mkdirSync('typings', { recursive: true });
|
|
109
|
+
for (const file of TYPING_FILES) {
|
|
110
|
+
copyFileSync(join(typingsDir, file), join('typings', file));
|
|
111
|
+
}
|
|
112
|
+
console.log(`Copied ${TYPING_FILES.length} type stubs to typings/`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { init };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import search from '@inquirer/search';
|
|
3
|
+
import { readConfig, loadEffectiveEnv } from '../lib/config.js';
|
|
4
|
+
import {
|
|
5
|
+
listWorkflows,
|
|
6
|
+
getPhasesByWorkflow,
|
|
7
|
+
getPhaseResults,
|
|
8
|
+
initPhaseRemote,
|
|
9
|
+
getPhaseConfigurators,
|
|
10
|
+
deleteConfigurator,
|
|
11
|
+
createConfigurator,
|
|
12
|
+
} from '../lib/api.js';
|
|
13
|
+
import { getToken } from '../../lib/auth.js';
|
|
14
|
+
import { fuzzyMatch } from '../../lib/fuzzy.js';
|
|
15
|
+
|
|
16
|
+
const CODE_TEMPLATE = `logs = []
|
|
17
|
+
debug_logs = []
|
|
18
|
+
error_occurred = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def make_log(message, data=None, always=False):
|
|
22
|
+
log_entry = {"message": message}
|
|
23
|
+
if data is not None:
|
|
24
|
+
log_entry["data"] = data
|
|
25
|
+
|
|
26
|
+
if always:
|
|
27
|
+
logs.append(log_entry)
|
|
28
|
+
else:
|
|
29
|
+
debug_logs.append(log_entry)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
result = main()
|
|
41
|
+
except Exception as e:
|
|
42
|
+
error_occurred = True
|
|
43
|
+
make_log(
|
|
44
|
+
"Error occurred in phase",
|
|
45
|
+
{"error": str(e), "traceback": format_exc()},
|
|
46
|
+
always=True,
|
|
47
|
+
)
|
|
48
|
+
case_id.write({"error_message": str(e)})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if error_occurred and debug_logs:
|
|
52
|
+
logs.append({"message": "debug_logs", "logs": debug_logs})
|
|
53
|
+
|
|
54
|
+
if logs:
|
|
55
|
+
log(json_dumps(logs, indent=2))`;
|
|
56
|
+
|
|
57
|
+
function generateCode(results) {
|
|
58
|
+
if (!results || results.length === 0) return CODE_TEMPLATE;
|
|
59
|
+
|
|
60
|
+
const lines = [];
|
|
61
|
+
const vars = results.map((_, i) => `RES${i + 1}`);
|
|
62
|
+
results.forEach((r, i) => lines.push(`# ${vars[i]} = ${r.name}`));
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push(`${vars.join(', ')} = (${vars.map((v) => `"${v}"`).join(', ')})`);
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push(CODE_TEMPLATE);
|
|
67
|
+
return lines.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function initPhase() {
|
|
71
|
+
const config = readConfig();
|
|
72
|
+
loadEffectiveEnv(config);
|
|
73
|
+
|
|
74
|
+
const ripUrl = process.env.RIP_URL;
|
|
75
|
+
if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
|
|
76
|
+
|
|
77
|
+
console.log('Fetching workflows...');
|
|
78
|
+
const token = await getToken();
|
|
79
|
+
const workflows = await listWorkflows(token, ripUrl);
|
|
80
|
+
|
|
81
|
+
if (!workflows.length) {
|
|
82
|
+
console.log('No workflows found.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const workflow = await search({
|
|
87
|
+
message: 'Select a workflow:',
|
|
88
|
+
source: (input) => {
|
|
89
|
+
const filtered = input
|
|
90
|
+
? workflows.filter((w) => fuzzyMatch(w.name, input))
|
|
91
|
+
: workflows;
|
|
92
|
+
return filtered.map((w) => ({ name: w.name, value: w }));
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`Fetching phases for "${workflow.name}"...`);
|
|
97
|
+
const phases = await getPhasesByWorkflow(token, ripUrl, workflow.id);
|
|
98
|
+
|
|
99
|
+
if (!phases.length) {
|
|
100
|
+
console.log('No phases found for this workflow.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const phase = await search({
|
|
105
|
+
message: 'Select a phase:',
|
|
106
|
+
source: (input) => {
|
|
107
|
+
const filtered = input
|
|
108
|
+
? phases.filter((p) => fuzzyMatch(p.name, input))
|
|
109
|
+
: phases;
|
|
110
|
+
return filtered.map((p) => ({ name: p.name, value: p }));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Resolve result objects — API may return IDs or full objects
|
|
115
|
+
let results = phase.allowed_phase_result_ids || [];
|
|
116
|
+
if (results.length > 0 && typeof results[0] === 'number') {
|
|
117
|
+
results = await getPhaseResults(token, ripUrl, phase.id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const code = generateCode(results);
|
|
121
|
+
|
|
122
|
+
const { confirm } = await inquirer.prompt([
|
|
123
|
+
{
|
|
124
|
+
type: 'confirm',
|
|
125
|
+
name: 'confirm',
|
|
126
|
+
message: `Initialize phase "${phase.name}"? This will overwrite existing code.`,
|
|
127
|
+
default: true,
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
if (!confirm) {
|
|
132
|
+
console.log('Aborted.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Delete existing configurator records for this phase
|
|
137
|
+
const existing = await getPhaseConfigurators(token, ripUrl, phase.id);
|
|
138
|
+
for (const cfg of existing) {
|
|
139
|
+
await deleteConfigurator(token, ripUrl, cfg.id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Update phase code + set_result_automatically
|
|
143
|
+
await initPhaseRemote(token, ripUrl, phase.id, code);
|
|
144
|
+
|
|
145
|
+
// Create new configurator records for each result
|
|
146
|
+
const vars = results.map((_, i) => `RES${i + 1}`);
|
|
147
|
+
for (let i = 0; i < results.length; i++) {
|
|
148
|
+
await createConfigurator(token, ripUrl, {
|
|
149
|
+
result_value: vars[i],
|
|
150
|
+
code_phase_id: phase.id,
|
|
151
|
+
triplet_phase_result_id: results[i].id,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`Initialized phase "${phase.name}".`);
|
|
156
|
+
if (results.length > 0) {
|
|
157
|
+
console.log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
console.log(`Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { initPhase };
|