@jacebenson/jsn 4.3.7-dev.36 → 4.3.7-dev.38
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 +1 -1
- package/src/commands/actions.js +21 -4
- package/src/commands/flows.js +23 -4
- package/src/commands/midservers.js +16 -10
- package/src/helpers.js +15 -0
- package/src/midserver-direct.js +49 -0
package/package.json
CHANGED
package/src/commands/actions.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import { buildDevCmd } from './_generic.js';
|
|
2
|
-
import { parseDataArg } from '../helpers.js';
|
|
2
|
+
import { formatCommandList, parseDataArg } from '../helpers.js';
|
|
3
3
|
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
4
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
5
5
|
import { ACTION_TABLE, actionScope } from '../processflow-actions.js';
|
|
6
6
|
import { errUsage } from '../errors.js';
|
|
7
7
|
import { cancelled, pickDesignerRecord, designerScope, guidedActionCreate, guidedActionEdit, guidedActionTestInputs, interactiveDesigner, hasDefinitionData, confirmDesignerMutation } from '../flow-designer-interactive.js';
|
|
8
8
|
|
|
9
|
+
const ACTION_COMMANDS = [
|
|
10
|
+
{ name: 'list', description: 'List actions' },
|
|
11
|
+
{ name: 'show [identifier]', description: 'Show an action by name or sys_id' },
|
|
12
|
+
{ name: 'definition [identifier]', description: 'Read the full JSON definition including executable step_instances' },
|
|
13
|
+
{ name: 'step-types', description: 'Read step schema templates for a target scope' },
|
|
14
|
+
{ name: 'create', description: 'Create an action: guided in a terminal, full JSON definition in scripts' },
|
|
15
|
+
{ name: 'update [identifier]', description: 'Save a full definition through Process Flow (alias: edit)' },
|
|
16
|
+
{ name: 'test [identifier]', description: 'Execute an action and optionally wait for actual outputs' },
|
|
17
|
+
{ name: 'delete [identifier]', description: 'Delete an action parent explicitly' },
|
|
18
|
+
];
|
|
19
|
+
|
|
9
20
|
const scopeOption = y => y.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id; inferred from existing action when omitted' });
|
|
10
21
|
const dataOptions = y => y
|
|
11
22
|
.option('data-file', { type: 'string', describe: 'Full action definition JSON file, not table fields' })
|
|
@@ -131,9 +142,15 @@ export const actionsCmd = wrap => {
|
|
|
131
142
|
}),
|
|
132
143
|
}),
|
|
133
144
|
handler: wrap(async (_argv, app) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
const summary = 'Manage Flow Designer actions from the sys_hub_action_type_definition table. Create and update guide terminal users; automation passes full JSON definitions, not table fields.';
|
|
146
|
+
app.ok({
|
|
147
|
+
commands: ACTION_COMMANDS.map(command => command.name),
|
|
148
|
+
_formatted: formatCommandList({
|
|
149
|
+
title: summary,
|
|
150
|
+
commands: ACTION_COMMANDS,
|
|
151
|
+
footer: 'Run "jsn actions <command> --help" for details. In a terminal you can omit the target and pick one.',
|
|
152
|
+
}),
|
|
153
|
+
}, { summary });
|
|
137
154
|
}),
|
|
138
155
|
};
|
|
139
156
|
};
|
package/src/commands/flows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { formatRecordForDisplay, getStringField, interactiveList } from '../helpers.js';
|
|
2
|
+
import { formatRecordForDisplay, formatCommandList, getStringField, interactiveList } from '../helpers.js';
|
|
3
3
|
import { discoverFlowContextFields, normalizeFlowContext, summarizeFlowContexts, formatFlowContextSummary, aggregateFlowContextMappings, mergeFlowContextStats, buildFlowContextQuery } from '../flow-context.js';
|
|
4
4
|
import { declareCapabilities } from '../capabilities.js';
|
|
5
5
|
import { inspectFlow } from '../flow-inspection.js';
|
|
@@ -12,6 +12,19 @@ import { cancelled, pickDesignerRecord, guidedFlowCreate, guidedFlowEdit, guided
|
|
|
12
12
|
|
|
13
13
|
const pickFlow = (argv, app, query = '') => pickDesignerRecord(argv, app, argv.action ? 'sys_hub_action_type_definition' : 'sys_hub_flow', argv.action ? 'action' : 'flow', query);
|
|
14
14
|
|
|
15
|
+
const FLOW_COMMANDS = [
|
|
16
|
+
{ name: 'list', description: 'List flows' },
|
|
17
|
+
{ name: 'executions', description: 'Show flow executions from sys_flow_context' },
|
|
18
|
+
{ name: 'show [identifier]', description: 'Show a flow: structure, inputs, steps and conditions' },
|
|
19
|
+
{ name: 'status [identifier]', description: 'Check whether a flow is actually able to run' },
|
|
20
|
+
{ name: 'publish [identifier]', description: 'Publish (activate) a flow, or a custom action with --action' },
|
|
21
|
+
{ name: 'test [identifier]', description: 'Execute a record-triggered flow against one record' },
|
|
22
|
+
{ name: 'create', description: 'Create a draft flow: guided in a terminal, JSON manifest in scripts' },
|
|
23
|
+
{ name: 'update [identifier]', description: 'Update a draft flow (alias: edit)' },
|
|
24
|
+
{ name: 'doctor', description: 'Check whether this instance can publish flows at all' },
|
|
25
|
+
{ name: 'delete [identifier]', description: 'Delete a flow (not implemented: use the Flow Designer UI)' },
|
|
26
|
+
];
|
|
27
|
+
|
|
15
28
|
function flowInspectionAdapter(app) {
|
|
16
29
|
return {
|
|
17
30
|
inspectFlow: app.sdk.inspectFlow.bind(app.sdk),
|
|
@@ -336,9 +349,15 @@ export function flowsCmd(wrap) {
|
|
|
336
349
|
});
|
|
337
350
|
},
|
|
338
351
|
handler: wrap(async (_argv, app) => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
352
|
+
const summary = 'Manage Flow Designer flows from the sys_hub_flow table. Flow structure comes from the ProcessFlow API, so V1, V2 and subflow definitions all render in full.';
|
|
353
|
+
app.ok({
|
|
354
|
+
commands: FLOW_COMMANDS.map(command => command.name),
|
|
355
|
+
_formatted: formatCommandList({
|
|
356
|
+
title: summary,
|
|
357
|
+
commands: FLOW_COMMANDS,
|
|
358
|
+
footer: 'Run "jsn flows <command> --help" for details. In a terminal you can omit the target and pick one; scripts use explicit identifiers and manifests.',
|
|
359
|
+
}),
|
|
360
|
+
}, { summary });
|
|
342
361
|
}),
|
|
343
362
|
};
|
|
344
363
|
}
|
|
@@ -7,7 +7,7 @@ import { FormatAuto } from '../output.js';
|
|
|
7
7
|
import { declareCapabilities } from '../capabilities.js';
|
|
8
8
|
import { resolveRecord, unwrapSysId } from '../resolve-record.js';
|
|
9
9
|
import { detectDocker, defaultRun, installDockerMidServer, midInstallDirectory, normalizeMidName, validateMidCredentials } from '../midserver-install.js';
|
|
10
|
-
import { detectDirectPlatform, installDirectMidServer, readDirectInstallation, removeDirectInstallation, runDirectInstallation } from '../midserver-direct.js';
|
|
10
|
+
import { detectDirectPlatform, followDirectLog, installDirectMidServer, readDirectInstallation, removeDirectInstallation, resolveDirectLogFile, runDirectInstallation } from '../midserver-direct.js';
|
|
11
11
|
|
|
12
12
|
const AGENT_TABLE = 'ecc_agent';
|
|
13
13
|
const QUEUE_TABLE = 'ecc_queue';
|
|
@@ -344,14 +344,13 @@ function removeLocalMidServer(instanceURL, name, directory) {
|
|
|
344
344
|
return true;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
function showMidServerLogs(app, identifier, argv) {
|
|
347
|
+
async function showMidServerLogs(app, identifier, argv) {
|
|
348
348
|
const installDir = argv.directory || midInstallDirectory(app.getEffectiveInstance(), identifier);
|
|
349
349
|
const direct = readDirectInstallation(app.getEffectiveInstance(), identifier, argv.directory);
|
|
350
350
|
if (direct) {
|
|
351
351
|
if (!Number.isInteger(argv.tail) || argv.tail < 1 || argv.tail > 10000) throw new Error('Use --tail between 1 and 10000');
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (!fs.existsSync(logfile)) throw new Error('No MID agent log yet; the installation may only be prepared');
|
|
352
|
+
const logfile = resolveDirectLogFile(installDir);
|
|
353
|
+
if (!logfile) throw new Error('No MID agent log yet; the installation may only be prepared');
|
|
355
354
|
const stat = fs.lstatSync(logfile);
|
|
356
355
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Untrusted MID log path');
|
|
357
356
|
const fd = fs.openSync(logfile, 'r');
|
|
@@ -360,8 +359,15 @@ function showMidServerLogs(app, identifier, argv) {
|
|
|
360
359
|
const length = Math.min(stat.size, 1024 * 1024);
|
|
361
360
|
const buffer = Buffer.alloc(length);
|
|
362
361
|
fs.readSync(fd, buffer, 0, length, stat.size - length);
|
|
363
|
-
|
|
362
|
+
const all = buffer.toString('utf8').split('\n');
|
|
363
|
+
if (all.length && all[all.length - 1] === '') all.pop();
|
|
364
|
+
text = all.slice(-argv.tail).join('\n');
|
|
364
365
|
} finally { fs.closeSync(fd); }
|
|
366
|
+
if (argv.follow) {
|
|
367
|
+
// Following streams new lines directly; the JSON envelope would be unreachable.
|
|
368
|
+
process.stdout.write(`Tailing ${logfile} (Ctrl+C to stop)\n${text}\n`);
|
|
369
|
+
return followDirectLog(installDir);
|
|
370
|
+
}
|
|
365
371
|
app.ok({ installation: 'direct', logfile, text, _formatted: `${text}\n` }, { summary: 'Local MID agent log (last 1 MiB maximum)' });
|
|
366
372
|
return;
|
|
367
373
|
}
|
|
@@ -413,10 +419,10 @@ export function midserversCmd(wrap) {
|
|
|
413
419
|
describe: 'Show local native or Docker Compose MID logs',
|
|
414
420
|
builder: (b) => b
|
|
415
421
|
.option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
|
|
416
|
-
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs
|
|
422
|
+
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' })
|
|
417
423
|
.option('directory', { type: 'string', describe: 'Local installation directory override' }),
|
|
418
424
|
handler: wrap(async (argv, app) => {
|
|
419
|
-
showMidServerLogs(app, argv.identifier, argv);
|
|
425
|
+
await showMidServerLogs(app, argv.identifier, argv);
|
|
420
426
|
}),
|
|
421
427
|
})
|
|
422
428
|
.command({
|
|
@@ -424,10 +430,10 @@ export function midserversCmd(wrap) {
|
|
|
424
430
|
describe: 'Show local native or Docker Compose MID logs',
|
|
425
431
|
builder: (b) => b
|
|
426
432
|
.option('tail', { type: 'number', default: 100, describe: 'Number of log lines' })
|
|
427
|
-
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs
|
|
433
|
+
.option('follow', { type: 'boolean', default: false, describe: 'Follow the logs' })
|
|
428
434
|
.option('directory', { type: 'string', describe: 'Local installation directory override' }),
|
|
429
435
|
handler: wrap(async (argv, app) => {
|
|
430
|
-
showMidServerLogs(app, argv.identifier, argv);
|
|
436
|
+
await showMidServerLogs(app, argv.identifier, argv);
|
|
431
437
|
}),
|
|
432
438
|
})
|
|
433
439
|
.command({
|
package/src/helpers.js
CHANGED
|
@@ -32,6 +32,21 @@ export function canPrompt() {
|
|
|
32
32
|
return isTTY(process.stdout) && isTTY(process.stdin);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Render a parent command's subcommand list for its default handler.
|
|
37
|
+
*
|
|
38
|
+
* Returned through the `_formatted` side channel so a terminal prints the list
|
|
39
|
+
* while `--json`/`--quiet` keep the structured envelope. A parent command that
|
|
40
|
+
* only sets a summary leaves terminal users with no way to discover children.
|
|
41
|
+
*/
|
|
42
|
+
export function formatCommandList({ title, commands, footer }) {
|
|
43
|
+
const width = Math.max(...commands.map(command => command.name.length));
|
|
44
|
+
const lines = [title, '', 'Available subcommands:'];
|
|
45
|
+
for (const command of commands) lines.push(` ${command.name.padEnd(width)} ${command.description}`);
|
|
46
|
+
if (footer) lines.push('', footer);
|
|
47
|
+
return `${lines.join('\n')}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
51
|
* Confirm a destructive action before executing.
|
|
37
52
|
*
|
package/src/midserver-direct.js
CHANGED
|
@@ -153,6 +153,55 @@ export function readDirectInstallation(instanceURL, name, directory) {
|
|
|
153
153
|
return metadata;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// The vendor rotates agent0.log to agent0.log.0 (and upward), so the plain
|
|
157
|
+
// name may not exist on a healthy install. Newest real file wins; links and
|
|
158
|
+
// non-files are never followed.
|
|
159
|
+
export function resolveDirectLogFile(installDir) {
|
|
160
|
+
const directory = path.join(installDir, 'agent', 'logs');
|
|
161
|
+
let names;
|
|
162
|
+
try { names = fs.readdirSync(directory); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
163
|
+
const candidates = [];
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
const agent = /^agent0\.log(\.\d+)?$/.test(name);
|
|
166
|
+
if (!agent && name !== 'wrapper.log') continue;
|
|
167
|
+
const full = path.join(directory, name);
|
|
168
|
+
const stat = fs.lstatSync(full);
|
|
169
|
+
if (!stat.isFile() || stat.isSymbolicLink()) continue;
|
|
170
|
+
candidates.push({ full, agent, mtime: stat.mtimeMs });
|
|
171
|
+
}
|
|
172
|
+
candidates.sort((a, b) => (b.agent - a.agent) || (b.mtime - a.mtime));
|
|
173
|
+
return candidates.length ? candidates[0].full : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Tail a direct install's agent log, following rotation.
|
|
178
|
+
*
|
|
179
|
+
* The vendor rotates by creating a new file, so the followed path can change
|
|
180
|
+
* between polls; a shrinking file is treated as truncation and re-read from
|
|
181
|
+
* the start. Each appended chunk is handed to `writer` unchanged.
|
|
182
|
+
*/
|
|
183
|
+
export async function followDirectLog(installDir, { writer = chunk => process.stdout.write(chunk), intervalMs = 1000, iterations = Infinity, sleep } = {}) {
|
|
184
|
+
const pause = sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
185
|
+
let file = resolveDirectLogFile(installDir);
|
|
186
|
+
if (!file) throw new Error('No MID agent log yet; the installation may only be prepared');
|
|
187
|
+
let offset = fs.statSync(file).size;
|
|
188
|
+
for (let seen = 0; seen < iterations; seen++) {
|
|
189
|
+
await pause(intervalMs);
|
|
190
|
+
const next = resolveDirectLogFile(installDir) || file;
|
|
191
|
+
if (next !== file) { file = next; offset = 0; }
|
|
192
|
+
const stat = fs.lstatSync(file);
|
|
193
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return;
|
|
194
|
+
if (stat.size < offset) offset = 0;
|
|
195
|
+
if (stat.size === offset) continue;
|
|
196
|
+
const length = stat.size - offset;
|
|
197
|
+
const buffer = Buffer.alloc(length);
|
|
198
|
+
const fd = fs.openSync(file, 'r');
|
|
199
|
+
try { fs.readSync(fd, buffer, 0, length, offset); } finally { fs.closeSync(fd); }
|
|
200
|
+
offset = stat.size;
|
|
201
|
+
writer(buffer.toString('utf8'));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
156
205
|
function uncertainState(metadata) {
|
|
157
206
|
return `MID '${metadata.name}' runtime state is uncertain after an interrupted lifecycle command. Confirm no vendor MID process from ${metadata.installDir} is running, then rerun; installed files are kept.`;
|
|
158
207
|
}
|