@waron97/prbot 3.3.0 → 3.4.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/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@waron97/prbot",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
+ "engines": {
7
+ "node": ">=20.0.0"
8
+ },
9
+ "packageManager": "npm@10.9.8",
6
10
  "scripts": {
7
11
  "test": "echo \"Error: no test specified\" && exit 1"
8
12
  },
@@ -3,6 +3,7 @@ import select from '@inquirer/select';
3
3
  import inquirer from 'inquirer';
4
4
  import { getToken } from '../../lib/auth.js';
5
5
  import { fuzzyMatch } from '../../lib/fuzzy.js';
6
+ import { log, warn } from '../../lib/logger.js';
6
7
  import { describeWorkflow, getPhasesByWorkflow, listMfas, listWorkflows } from '../lib/api.js';
7
8
  import { computeChecksum } from '../lib/checksum.js';
8
9
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
@@ -42,7 +43,7 @@ async function clone(opts) {
42
43
  if (!ripUrl)
43
44
  throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
44
45
 
45
- console.log('Fetching records...');
46
+ log('Fetching records...');
46
47
  const token = await getToken();
47
48
 
48
49
  let records;
@@ -53,7 +54,7 @@ async function clone(opts) {
53
54
  }
54
55
 
55
56
  if (!records.length) {
56
- console.log(`No ${objectType} records found.`);
57
+ log(`No ${objectType} records found.`);
57
58
  return;
58
59
  }
59
60
 
@@ -92,11 +93,11 @@ async function clone(opts) {
92
93
  basePath = inputPath;
93
94
  }
94
95
 
95
- console.log(`Fetching phases for "${record.name}"...`);
96
+ log(`Fetching phases for "${record.name}"...`);
96
97
  const phases = await getPhasesByWorkflow(token, ripUrl, record.id, { fromCode: true });
97
98
 
98
99
  if (!phases.length) {
99
- console.log('No phases found.');
100
+ log('No phases found.');
100
101
  return;
101
102
  }
102
103
 
@@ -112,19 +113,19 @@ async function clone(opts) {
112
113
  checksum_at_pull: computeChecksum(phase.code),
113
114
  name: `${record.name} / ${phase.name}`,
114
115
  });
115
- console.log(` wrote ${filePath}`);
116
+ log(` wrote ${filePath}`);
116
117
  }
117
118
 
118
119
  // Drop the workflow graph alongside the phase files as read-only context.
119
120
  try {
120
121
  const structure = await describeWorkflow(token, ripUrl, record.id);
121
122
  const docPath = writeWorkflowDoc(basePath, structure);
122
- console.log(` wrote ${docPath}`);
123
+ log(` wrote ${docPath}`);
123
124
  } catch (err) {
124
- console.warn(` could not fetch workflow structure: ${err.message}`);
125
+ warn(` could not fetch workflow structure: ${err.message}`);
125
126
  }
126
127
 
127
- console.log(`Cloned ${phases.length} phase(s) to ${basePath}/`);
128
+ log(`Cloned ${phases.length} phase(s) to ${basePath}/`);
128
129
  } else {
129
130
  const defaultPath = defaultMfaPath(record.model_name, record.name);
130
131
  if (!basePath) {
@@ -147,7 +148,7 @@ async function clone(opts) {
147
148
  checksum_at_pull: computeChecksum(record.code),
148
149
  name: `${record.model_name} / ${record.name}`,
149
150
  });
150
- console.log(`Cloned MFA to ${basePath}`);
151
+ log(`Cloned MFA to ${basePath}`);
151
152
  }
152
153
 
153
154
  writeConfig(config);
@@ -1,10 +1,10 @@
1
1
  import search from '@inquirer/search';
2
2
  import inquirer from 'inquirer';
3
3
  import { getToken } from '../../lib/auth.js';
4
- import { computeChecksum } from '../lib/checksum.js';
4
+ import { log, warn } from '../../lib/logger.js';
5
5
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
6
6
  import { getLrpXml, listLrps, resolveLrpByName } from '../lib/lrpApi.js';
7
- import { comparePayload, decompose, recompose, stableStringify } from '../lib/pbProject.js';
7
+ import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
8
8
  import { projectReader, writeProject } from '../lib/pbWorkspace.js';
9
9
 
10
10
  // Clone a long-running process (LRP). Structurally identical to a PB clone
@@ -32,7 +32,7 @@ async function cloneLrp(opts) {
32
32
  if (opts.name) {
33
33
  chosen = await resolveLrpByName(token, opts.name);
34
34
  } else {
35
- console.log('Fetching long-running process list...');
35
+ log('Fetching long-running process list...');
36
36
  const initial = await listLrps(token, null);
37
37
  let controller = null;
38
38
  chosen = await search({
@@ -65,7 +65,7 @@ async function cloneLrp(opts) {
65
65
  dest = inputPath;
66
66
  }
67
67
 
68
- console.log(`Fetching "${chosen.name}"...`);
68
+ log(`Fetching "${chosen.name}"...`);
69
69
  const { xml, description } = await getLrpXml(token, chosen.id);
70
70
  const payload = { built_page: xml };
71
71
 
@@ -73,7 +73,7 @@ async function cloneLrp(opts) {
73
73
  writeProject(dest, files);
74
74
 
75
75
  const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
76
- console.log(`Cloned to ${dest}/ (${scriptCount} script(s)).`);
76
+ log(`Cloned to ${dest}/ (${scriptCount} script(s)).`);
77
77
 
78
78
  // Prove the clone reconstructs the original XML (0-loss bar A). LRPs have
79
79
  // no `pages` at all — recompose always synthesizes `pages: []`, which
@@ -82,10 +82,10 @@ async function cloneLrp(opts) {
82
82
  const rebuilt = recompose(projectReader(dest));
83
83
  const diffs = comparePayload(payload, rebuilt).filter((d) => !d.startsWith('pages:'));
84
84
  if (diffs.length) {
85
- console.warn('WARNING: round-trip verification found differences:');
86
- diffs.forEach((d) => console.warn(' - ' + d));
85
+ warn('WARNING: round-trip verification found differences:');
86
+ diffs.forEach((d) => warn(' - ' + d));
87
87
  } else {
88
- console.log('Round-trip verified: recomposed payload is identical (0 information loss).');
88
+ log('Round-trip verified: recomposed payload is identical (0 information loss).');
89
89
  }
90
90
 
91
91
  // Register in the workspace for later pull/push — keyed by NAME, not id.
@@ -102,7 +102,7 @@ async function cloneLrp(opts) {
102
102
  description,
103
103
  // Baseline for push classification (see pull.js/push.js): checksum of
104
104
  // the *recomposed* payload, changes only when local files change.
105
- checksum_at_pull: computeChecksum(stableStringify(rebuilt)),
105
+ checksum_at_pull: checksumOfPayload(rebuilt),
106
106
  version: chosen.version,
107
107
  status: chosen.status,
108
108
  };
@@ -2,10 +2,10 @@ import search from '@inquirer/search';
2
2
  import inquirer from 'inquirer';
3
3
  import { getToken } from '../../lib/auth.js';
4
4
  import { fuzzyMatch } from '../../lib/fuzzy.js';
5
- import { computeChecksum } from '../lib/checksum.js';
5
+ import { log, warn } from '../../lib/logger.js';
6
6
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
7
7
  import { getProcess, listProcesses } from '../lib/pbApi.js';
8
- import { comparePayload, decompose, recompose, stableStringify } from '../lib/pbProject.js';
8
+ import { checksumOfPayload, comparePayload, decompose, recompose } from '../lib/pbProject.js';
9
9
  import { projectReader, writeProject } from '../lib/pbWorkspace.js';
10
10
 
11
11
  async function clonePb(opts) {
@@ -16,11 +16,11 @@ async function clonePb(opts) {
16
16
  throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
17
17
  }
18
18
 
19
- console.log('Fetching process list...');
19
+ log('Fetching process list...');
20
20
  const token = await getToken();
21
21
  const processes = await listProcesses(token);
22
22
  if (!processes.length) {
23
- console.log('No process-builder wizards found.');
23
+ log('No process-builder wizards found.');
24
24
  return;
25
25
  }
26
26
 
@@ -61,7 +61,7 @@ async function clonePb(opts) {
61
61
  dest = inputPath;
62
62
  }
63
63
 
64
- console.log(`Fetching "${chosen.process_name}"...`);
64
+ log(`Fetching "${chosen.process_name}"...`);
65
65
  const payload = await getProcess(token, chosen.guid);
66
66
 
67
67
  const { files } = decompose(payload);
@@ -69,17 +69,17 @@ async function clonePb(opts) {
69
69
 
70
70
  const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
71
71
  const pageCount = Object.keys(files).filter((p) => p.startsWith('pages/')).length;
72
- console.log(`Cloned to ${dest}/ (${scriptCount} script(s), ${pageCount} page(s))`);
72
+ log(`Cloned to ${dest}/ (${scriptCount} script(s), ${pageCount} page(s))`);
73
73
 
74
74
  // Prove the clone reconstructs the original payload (0-loss bar A) by
75
75
  // reading the files back from disk and recomposing.
76
76
  const rebuilt = recompose(projectReader(dest));
77
77
  const diffs = comparePayload(payload, rebuilt);
78
78
  if (diffs.length) {
79
- console.warn('WARNING: round-trip verification found differences:');
80
- diffs.forEach((d) => console.warn(' - ' + d));
79
+ warn('WARNING: round-trip verification found differences:');
80
+ diffs.forEach((d) => warn(' - ' + d));
81
81
  } else {
82
- console.log('Round-trip verified: recomposed payload is identical (0 information loss).');
82
+ log('Round-trip verified: recomposed payload is identical (0 information loss).');
83
83
  }
84
84
 
85
85
  // Register in the workspace for later pull/push.
@@ -95,7 +95,7 @@ async function clonePb(opts) {
95
95
  name: chosen.process_name,
96
96
  // Baselines for `push` classification: checksum of the *recomposed* payload
97
97
  // (changes only when local files change) + upstream updated_date/status.
98
- checksum_at_pull: computeChecksum(stableStringify(rebuilt)),
98
+ checksum_at_pull: checksumOfPayload(rebuilt),
99
99
  updated_date: payload.updated_date,
100
100
  status: payload.status,
101
101
  };
@@ -12,6 +12,7 @@ import {
12
12
  import { tmpdir } from 'os';
13
13
  import { join } from 'path';
14
14
  import { getToken } from '../../lib/auth.js';
15
+ import { error, log } from '../../lib/logger.js';
15
16
  import { computeChecksum } from '../lib/checksum.js';
16
17
  import { loadEffectiveEnv, readConfig } from '../lib/config.js';
17
18
  import { getProcess } from '../lib/pbApi.js';
@@ -25,13 +26,13 @@ async function diff(targetArg) {
25
26
  loadEffectiveEnv(config);
26
27
 
27
28
  if (!config.workspace.length) {
28
- console.log('No tracked resources. Run `agrippa clone` first.');
29
+ log('No tracked resources. Run `agrippa clone` first.');
29
30
  return;
30
31
  }
31
32
 
32
33
  const entries = filterEntries(config.workspace, targetArg);
33
34
  if (!entries.length) {
34
- console.log('No tracked files match the given path.');
35
+ log('No tracked files match the given path.');
35
36
  return;
36
37
  }
37
38
 
@@ -43,7 +44,7 @@ async function diff(targetArg) {
43
44
  if (pbEntries.length && !process.env.PB_URL)
44
45
  throw new Error('PB_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
45
46
 
46
- console.log('Fetching remote code...');
47
+ log('Fetching remote code...');
47
48
  const token = await getToken();
48
49
 
49
50
  let diffCount = 0;
@@ -65,13 +66,13 @@ async function diff(targetArg) {
65
66
  }
66
67
 
67
68
  if (diffCount === 0) {
68
- console.log('No differences found — all tracked files match the remote.');
69
+ log('No differences found — all tracked files match the remote.');
69
70
  } else {
70
71
  const combined = Buffer.concat(chunks);
71
72
  const pager = process.env.PAGER || 'less';
72
73
  const pagerArgs = pager === 'less' ? ['-R', '-F'] : [];
73
74
  spawnSync(pager, pagerArgs, { input: combined, stdio: ['pipe', 'inherit', 'inherit'] });
74
- console.log(`\n${diffCount} file(s) differ from remote.`);
75
+ log(`\n${diffCount} file(s) differ from remote.`);
75
76
  }
76
77
  }
77
78
 
@@ -90,7 +91,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
90
91
  if (computeChecksum(localCode) === computeChecksum(remoteCode)) continue;
91
92
 
92
93
  if (!fileExists(entry.path)) {
93
- console.log(`\n--- ${entry.path} (local file missing)`);
94
+ log(`\n--- ${entry.path} (local file missing)`);
94
95
  continue;
95
96
  }
96
97
 
@@ -105,7 +106,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
105
106
  );
106
107
  // exit code 1 means differences found (normal), 0 means identical, >1 means error
107
108
  if (result.status !== null && result.status > 1) {
108
- console.error(`git diff failed for ${entry.path}`);
109
+ error(`git diff failed for ${entry.path}`);
109
110
  }
110
111
  const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] ===\n`);
111
112
  chunks.push(Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]));
@@ -156,7 +157,7 @@ async function diffPbEntry(token, entry) {
156
157
  { cwd: tmpRoot, stdio: ['ignore', 'pipe', 'pipe'] }
157
158
  );
158
159
  if (result.status !== null && result.status > 1) {
159
- console.error(`git diff failed for ${entry.path}`);
160
+ error(`git diff failed for ${entry.path}`);
160
161
  }
161
162
  const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] (process-builder) ===\n`);
162
163
  const chunk = Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]);
@@ -9,6 +9,7 @@ import {
9
9
  import { join } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import inquirer from 'inquirer';
12
+ import { log } from '../../lib/logger.js';
12
13
  import { WORKSPACE_FILE } from '../lib/config.js';
13
14
 
14
15
  const TEMPLATE = `# agrippa workspace configuration
@@ -93,23 +94,23 @@ async function init() {
93
94
  },
94
95
  ]);
95
96
  if (!overwrite) {
96
- console.log('Skipped workspace file.');
97
+ log('Skipped workspace file.');
97
98
  } else {
98
99
  writeFileSync(WORKSPACE_FILE, TEMPLATE, 'utf-8');
99
- console.log(`Created ${WORKSPACE_FILE}`);
100
- console.log(`Run 'agrippa clone' to add resources to this workspace.`);
101
- console.log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
100
+ log(`Created ${WORKSPACE_FILE}`);
101
+ log(`Run 'agrippa clone' to add resources to this workspace.`);
102
+ log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
102
103
  }
103
104
  } else {
104
105
  writeFileSync(WORKSPACE_FILE, TEMPLATE, 'utf-8');
105
- console.log(`Created ${WORKSPACE_FILE}`);
106
- console.log(`Run 'agrippa clone' to add resources to this workspace.`);
107
- console.log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
106
+ log(`Created ${WORKSPACE_FILE}`);
107
+ log(`Run 'agrippa clone' to add resources to this workspace.`);
108
+ log(`Add ${WORKSPACE_FILE} to .gitignore if it contains credentials.`);
108
109
  }
109
110
 
110
111
  if (!existsSync('pyproject.toml')) {
111
112
  writeFileSync('pyproject.toml', PYPROJECT_TOML, 'utf-8');
112
- console.log('Created pyproject.toml (ruff builtins)');
113
+ log('Created pyproject.toml (ruff builtins)');
113
114
  }
114
115
 
115
116
  const { importTypings } = await inquirer.prompt([
@@ -124,7 +125,7 @@ async function init() {
124
125
  if (importTypings) {
125
126
  if (!existsSync('pyrightconfig.json')) {
126
127
  writeFileSync('pyrightconfig.json', PYRIGHTCONFIG, 'utf-8');
127
- console.log('Created pyrightconfig.json');
128
+ log('Created pyrightconfig.json');
128
129
  }
129
130
 
130
131
  const typingsDir = fileURLToPath(new URL('../../../agrippa_typings', import.meta.url));
@@ -132,7 +133,7 @@ async function init() {
132
133
  for (const file of TYPING_FILES) {
133
134
  copyFileSync(join(typingsDir, file), join('typings', file));
134
135
  }
135
- console.log(`Copied ${TYPING_FILES.length} type stubs to typings/`);
136
+ log(`Copied ${TYPING_FILES.length} type stubs to typings/`);
136
137
  }
137
138
 
138
139
  const { importInstructions } = await inquirer.prompt([
@@ -149,13 +150,13 @@ async function init() {
149
150
  const block = pbBlock(readFileSync(guidePath, 'utf-8'));
150
151
  if (!existsSync('CLAUDE.md')) {
151
152
  writeFileSync('CLAUDE.md', block, 'utf-8');
152
- console.log('Created CLAUDE.md with agrippa-pb guidance');
153
+ log('Created CLAUDE.md with agrippa-pb guidance');
153
154
  } else {
154
155
  const current = readFileSync('CLAUDE.md', 'utf-8');
155
156
  const begin = current.indexOf(PB_BEGIN);
156
157
  if (begin === -1) {
157
158
  appendFileSync('CLAUDE.md', `\n${block}`, 'utf-8');
158
- console.log('Appended agrippa-pb guidance to CLAUDE.md');
159
+ log('Appended agrippa-pb guidance to CLAUDE.md');
159
160
  } else {
160
161
  // Replace the existing managed block in place; leave the rest as-is.
161
162
  const endIdx = current.indexOf(PB_END, begin);
@@ -165,7 +166,7 @@ async function init() {
165
166
  );
166
167
  const after = current.slice(endIdx + PB_END.length).replace(/^\n/, '');
167
168
  writeFileSync('CLAUDE.md', current.slice(0, begin) + block + after, 'utf-8');
168
- console.log('Refreshed agrippa-pb guidance in CLAUDE.md');
169
+ log('Refreshed agrippa-pb guidance in CLAUDE.md');
169
170
  }
170
171
  }
171
172
  }
@@ -2,6 +2,7 @@ import search from '@inquirer/search';
2
2
  import inquirer from 'inquirer';
3
3
  import { getToken } from '../../lib/auth.js';
4
4
  import { fuzzyMatch } from '../../lib/fuzzy.js';
5
+ import { log } from '../../lib/logger.js';
5
6
  import {
6
7
  createConfigurator,
7
8
  deleteConfigurator,
@@ -75,12 +76,12 @@ async function initPhase() {
75
76
  if (!ripUrl)
76
77
  throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
77
78
 
78
- console.log('Fetching workflows...');
79
+ log('Fetching workflows...');
79
80
  const token = await getToken();
80
81
  const workflows = await listWorkflows(token, ripUrl);
81
82
 
82
83
  if (!workflows.length) {
83
- console.log('No workflows found.');
84
+ log('No workflows found.');
84
85
  return;
85
86
  }
86
87
 
@@ -92,11 +93,11 @@ async function initPhase() {
92
93
  },
93
94
  });
94
95
 
95
- console.log(`Fetching phases for "${workflow.name}"...`);
96
+ log(`Fetching phases for "${workflow.name}"...`);
96
97
  const phases = await getPhasesByWorkflow(token, ripUrl, workflow.id);
97
98
 
98
99
  if (!phases.length) {
99
- console.log('No phases found for this workflow.');
100
+ log('No phases found for this workflow.');
100
101
  return;
101
102
  }
102
103
 
@@ -111,7 +112,7 @@ async function initPhase() {
111
112
  ]);
112
113
 
113
114
  if (!selectedPhases.length) {
114
- console.log('No phases selected. Aborted.');
115
+ log('No phases selected. Aborted.');
115
116
  return;
116
117
  }
117
118
 
@@ -125,7 +126,7 @@ async function initPhase() {
125
126
  ]);
126
127
 
127
128
  if (!confirm) {
128
- console.log('Aborted.');
129
+ log('Aborted.');
129
130
  return;
130
131
  }
131
132
 
@@ -157,15 +158,13 @@ async function initPhase() {
157
158
  });
158
159
  }
159
160
 
160
- console.log(`Initialized phase "${phase.name}".`);
161
+ log(`Initialized phase "${phase.name}".`);
161
162
  if (results.length > 0) {
162
- console.log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
163
+ log(` ${results.length} configurator(s) created: ${vars.join(', ')}`);
163
164
  }
164
165
  }
165
166
 
166
- console.log(
167
- `Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`
168
- );
167
+ log(`Run 'agrippa pull' in workspaces that track this workflow to fetch the updated code.`);
169
168
  }
170
169
 
171
170
  export { initPhase };
@@ -18,6 +18,7 @@ import { dirname, join } from 'path';
18
18
  import search from '@inquirer/search';
19
19
  import { parse as yamlParse } from 'yaml';
20
20
  import { fuzzyMatch } from '../../lib/fuzzy.js';
21
+ import { log, warn } from '../../lib/logger.js';
21
22
  import { readConfig } from '../lib/config.js';
22
23
  import {
23
24
  addNode,
@@ -120,7 +121,7 @@ function validate(dir) {
120
121
  try {
121
122
  recompose(projectReader(dir));
122
123
  } catch (e) {
123
- console.warn(`WARNING: project no longer recomposes cleanly: ${e.message}`);
124
+ warn(`WARNING: project no longer recomposes cleanly: ${e.message}`);
124
125
  }
125
126
  }
126
127
 
@@ -139,13 +140,13 @@ async function pbFormat(opts) {
139
140
  if (!n.layout) missing++;
140
141
  });
141
142
  validate(dir);
142
- console.log(
143
+ log(
143
144
  `Formatted ${dir} (${nodes} node(s) laid out${missing ? `, ${missing} without layout` : ''}).`
144
145
  );
145
146
  const issues = lintAll(structure);
146
147
  if (issues.length) {
147
- console.warn('Diagram issues:');
148
- for (const w of issues) console.warn(` ! ${w}`);
148
+ warn('Diagram issues:');
149
+ for (const w of issues) warn(` ! ${w}`);
149
150
  }
150
151
  }
151
152
 
@@ -180,13 +181,13 @@ async function pbAdd(opts) {
180
181
  saveStructure(dir, structure);
181
182
  saveManifest(dir, manifest);
182
183
  validate(dir);
183
- console.log(
184
+ log(
184
185
  `Added ${result.type} ${result.id}${result.file ? ` (${result.file})` : ''} between ${opts.from} → ${opts.to}.`
185
186
  );
186
- console.log(` ${opts.from} → ${result.id} (${result.edgeId}, retargeted)`);
187
- console.log(` ${result.id} → ${opts.to} (${result.newEdgeId})`);
188
- for (const w of result.warnings || []) console.warn(` ! ${w}`);
189
- console.log('Run `agrippa pb format` to lay it out.');
187
+ log(` ${opts.from} → ${result.id} (${result.edgeId}, retargeted)`);
188
+ log(` ${result.id} → ${opts.to} (${result.newEdgeId})`);
189
+ for (const w of result.warnings || []) warn(` ! ${w}`);
190
+ log('Run `agrippa pb format` to lay it out.');
190
191
  return;
191
192
  }
192
193
 
@@ -200,10 +201,8 @@ async function pbAdd(opts) {
200
201
  saveStructure(dir, structure);
201
202
  saveManifest(dir, manifest);
202
203
  validate(dir);
203
- console.log(`Added ${result.type} ${result.id}${result.file ? ` (${result.file})` : ''}.`);
204
- console.log(
205
- 'Connect it with `agrippa pb connect`, then run `agrippa pb format` to lay it out.'
206
- );
204
+ log(`Added ${result.type} ${result.id}${result.file ? ` (${result.file})` : ''}.`);
205
+ log('Connect it with `agrippa pb connect`, then run `agrippa pb format` to lay it out.');
207
206
  }
208
207
 
209
208
  async function pbRemove(opts) {
@@ -215,7 +214,7 @@ async function pbRemove(opts) {
215
214
  saveStructure(dir, structure);
216
215
  saveManifest(dir, manifest);
217
216
  validate(dir);
218
- console.log(
217
+ log(
219
218
  `Removed ${result.removed.length} node(s) [${result.removed.join(', ')}], ` +
220
219
  `${result.removedEdges} dangling edge(s), ${deletes.length} file(s).`
221
220
  );
@@ -235,11 +234,11 @@ async function pbConnect(opts) {
235
234
  });
236
235
  saveStructure(dir, structure);
237
236
  validate(dir);
238
- console.log(
237
+ log(
239
238
  `Connected ${result.from} → ${result.to} (${result.id})${opts.default ? ' [default]' : ''}.`
240
239
  );
241
- for (const w of result.warnings || []) console.warn(` ! ${w}`);
242
- console.log('Run `agrippa pb format` to route it.');
240
+ for (const w of result.warnings || []) warn(` ! ${w}`);
241
+ log('Run `agrippa pb format` to route it.');
243
242
  }
244
243
 
245
244
  async function pbDisconnect(opts) {
@@ -250,7 +249,7 @@ async function pbDisconnect(opts) {
250
249
  const { result } = disconnect(structure, { id: opts.id, from: opts.from, to: opts.to });
251
250
  saveStructure(dir, structure);
252
251
  validate(dir);
253
- console.log(`Removed ${result.removed} edge(s)${result.id ? ` (${result.id})` : ''}.`);
252
+ log(`Removed ${result.removed} edge(s)${result.id ? ` (${result.id})` : ''}.`);
254
253
  }
255
254
 
256
255
  async function pbSetDefault(opts) {
@@ -261,11 +260,11 @@ async function pbSetDefault(opts) {
261
260
  const { result } = setDefault(structure, { id: opts.id, from: opts.from, to: opts.to });
262
261
  saveStructure(dir, structure);
263
262
  validate(dir);
264
- console.log(
263
+ log(
265
264
  `Default flow on ${result.from} is now ${result.id} (→ ${result.to})` +
266
265
  `${result.prev && result.prev !== result.id ? `, was ${result.prev}` : ''}.`
267
266
  );
268
- for (const w of result.warnings || []) console.warn(` ! ${w}`);
267
+ for (const w of result.warnings || []) warn(` ! ${w}`);
269
268
  }
270
269
 
271
270
  async function pbList(opts) {
@@ -275,7 +274,7 @@ async function pbList(opts) {
275
274
  for (const r of rows) {
276
275
  const where = r.parent ? ` [in ${r.parent}]` : '';
277
276
  const label = r.name ? ` "${r.name}"` : '';
278
- console.log(`${r.id} (${r.type})${label}${where}`);
277
+ log(`${r.id} (${r.type})${label}${where}`);
279
278
  for (const e of r.edges) {
280
279
  const tag = [
281
280
  e.isDefault && '[default]',
@@ -284,10 +283,10 @@ async function pbList(opts) {
284
283
  ]
285
284
  .filter(Boolean)
286
285
  .join(' ');
287
- console.log(` → ${e.target} (${e.id})${tag ? ` ${tag}` : ''}`);
286
+ log(` → ${e.target} (${e.id})${tag ? ` ${tag}` : ''}`);
288
287
  }
289
288
  }
290
- console.log(`\n${rows.length} node(s).`);
289
+ log(`\n${rows.length} node(s).`);
291
290
  }
292
291
 
293
292
  async function pbPreview(opts) {
@@ -296,7 +295,7 @@ async function pbPreview(opts) {
296
295
  const svg = toSvg(structure);
297
296
  const out = opts.out || join(dir, 'preview.svg');
298
297
  writeFileSync(out, svg, 'utf-8');
299
- console.log(`Wrote ${out} (${svg.length} bytes).`);
298
+ log(`Wrote ${out} (${svg.length} bytes).`);
300
299
  }
301
300
 
302
301
  async function pbLint(opts) {
@@ -304,9 +303,9 @@ async function pbLint(opts) {
304
303
  const { structure } = loadProject(dir);
305
304
  const issues = lintAll(structure);
306
305
  if (!issues.length) {
307
- console.log('No issues.');
306
+ log('No issues.');
308
307
  } else {
309
- for (const w of issues) console.warn(` ! ${w}`);
308
+ for (const w of issues) warn(` ! ${w}`);
310
309
  process.exitCode = 1;
311
310
  }
312
311
  }