feat: add inkreach CLI + inkreach-teable-ops skill
A self-built CLI and companion agent skill for operating a deployed
inkreach-teable instance — configuring tables, fields, records, views, and
the project's OWN automation engine.
Why not the official 'teable' CLI:
inkreach-teable's automation is self-built with a different contract:
- conditions (not filter), fieldIds (not watchFieldIds)
- recordCondition (not recordMatchesConditions)
- webhook route /api/webhook/{baseId}/{wfId} (not the /base//workflow/ form)
- scheduledTime backend honors only timing.type + timing.interval
The official CLI targets the upstream contract and 404s/misconfigures here.
Table/field/record/view APIs ARE upstream-compatible, but automation is not.
inkreach CLI (packages/inkreach-cli/):
- Node.js + commander, zero extra runtime deps
- Uses node:http (NOT fetch — Node 24 fetch drops Authorization header)
- Commands: auth (login/status/token), base/table/field/record/view,
workflow (list/get/create/update/delete/activate/deactivate/test),
trigger set (8 types, builds correct config keys), action add/update/delete
(6 types), secret, runs (list/get/rerun), fire (button-click/webhook), sql
- --json mode for agent parsing; GET->modify->PUT whole-graph for node edits
- Verified end-to-end: create workflow -> set webhook trigger -> add script
action -> activate -> fire webhook -> runs list (succeeded)
inkreach-teable-ops skill (.agents/skills/inkreach-teable-ops/):
- SKILL.md entry: module map, routing rules, 11 key constraints (the real
contract keys), execution rules, error recovery
- guides/: cli-reference, automation-guide, tables-fields, instance-access,
development (build/restart, known gotchas)
- api-reference/: trigger-types (8 triggers, exact config), action-types
(6 actions, sandbox globals), field-types (21 types incl. Button)
- metadata.json + install.sh
This commit is contained in:
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.authCommand = void 0;
|
||||
/**
|
||||
* `inkreach auth` — authentication & config commands.
|
||||
*/
|
||||
const commander_1 = require("commander");
|
||||
const ctx_1 = require("../lib/ctx");
|
||||
const config_1 = require("../lib/config");
|
||||
const output_1 = require("../lib/output");
|
||||
void ctx_1.requireBaseId;
|
||||
function authCommand() {
|
||||
const cmd = new commander_1.Command('auth');
|
||||
cmd.description('Manage authentication and CLI config');
|
||||
cmd
|
||||
.command('status')
|
||||
.description('Verify the configured token by calling GET /api/user/me')
|
||||
.action((0, ctx_1.run)(async (ctx) => {
|
||||
const me = (await ctx.client.getMe());
|
||||
const cfg = (0, config_1.readConfigFile)();
|
||||
if (ctx.json) {
|
||||
(0, output_1.printJson)({ config: cfg, user: me });
|
||||
}
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['config path', (0, config_1.configPath)()],
|
||||
['endpoint', cfg.endpoint || config_1.DEFAULT_ENDPOINT],
|
||||
['token set', cfg.token ? `yes (${cfg.token.slice(0, 14)}…)` : 'no'],
|
||||
['base id', cfg.baseId || '(none)'],
|
||||
['', ''],
|
||||
['user id', me?.id || 'unknown'],
|
||||
['name', me?.name || 'unknown'],
|
||||
['email', me?.email || 'unknown'],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('login')
|
||||
.description('Configure endpoint, token, and default base id')
|
||||
.option('--endpoint <url>', 'Backend URL (default http://localhost:3000)')
|
||||
.option('--token <pat>', 'Personal Access Token (format: teable_xxx_yyy)')
|
||||
.option('--base-id <id>', 'Default base id for subsequent commands')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
// Merge global flags (--token/--endpoint/--base-id on the program) with
|
||||
// this subcommand's own --endpoint/--token/--base-id options.
|
||||
const g = ctx.program.opts();
|
||||
const patch = {};
|
||||
const endpoint = opts.endpoint || g.endpoint;
|
||||
const tokenVal = opts.token || g.token;
|
||||
const baseId = opts.baseId || g.baseId;
|
||||
if (endpoint)
|
||||
patch.endpoint = endpoint;
|
||||
if (tokenVal)
|
||||
patch.token = tokenVal;
|
||||
if (baseId)
|
||||
patch.baseId = baseId;
|
||||
(0, config_1.writeConfigFile)(patch);
|
||||
const cfg = (0, config_1.readConfigFile)();
|
||||
if (ctx.json) {
|
||||
(0, output_1.printJson)({ ok: true, config: cfg });
|
||||
}
|
||||
else {
|
||||
process.stdout.write('✓ Configuration saved:\n');
|
||||
(0, output_1.printKv)([
|
||||
['endpoint', cfg.endpoint || config_1.DEFAULT_ENDPOINT],
|
||||
['token', cfg.token ? `set (${cfg.token.slice(0, 14)}…)` : '(not set)'],
|
||||
['base id', cfg.baseId || '(none)'],
|
||||
['path', (0, config_1.configPath)()],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
const token = cmd.command('token').description('Manage Personal Access Tokens (PAT)');
|
||||
token
|
||||
.command('list')
|
||||
.description('List your access tokens')
|
||||
.action((0, ctx_1.run)(async (ctx) => {
|
||||
const list = (await ctx.client.listAccessTokens());
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
{ key: 'scopes', header: 'SCOPES' },
|
||||
{ key: 'expiredTime', header: 'EXPIRES' },
|
||||
], list.map((t) => ({ ...t, scopes: (t.scopes || []).join(',') })));
|
||||
}));
|
||||
token
|
||||
.command('create')
|
||||
.description('Create a PAT. The full token is returned ONLY ONCE.')
|
||||
.requiredOption('--name <name>', 'Token name')
|
||||
.option('--scopes <csv>', 'Comma-separated scopes', 'automation|read,automation|create,automation|update,automation|delete,table|read,record|read,record|create,record|update')
|
||||
.option('--base-ids <csv>', 'Restrict to base ids (comma-separated). Omit for full access')
|
||||
.option('--full-access', 'Grant full access')
|
||||
.option('--expires <date>', 'Expiry date YYYY-MM-DD (default 2099-12-31)', '2099-12-31')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const scopes = opts.scopes.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
const baseIds = opts['base-ids']
|
||||
? opts['base-ids'].split(',').map((s) => s.trim())
|
||||
: undefined;
|
||||
const created = (await ctx.client.createAccessToken({
|
||||
name: opts.name,
|
||||
scopes,
|
||||
baseIds,
|
||||
hasFullAccess: !!opts['full-access'],
|
||||
expiredTime: opts.expires,
|
||||
}));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(created);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['id', created.id],
|
||||
['token', created.token],
|
||||
['', ''],
|
||||
['note', 'Save this token now — it cannot be retrieved again.'],
|
||||
['use', 'inkreach auth login --token <above>'],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
token
|
||||
.command('delete <id>')
|
||||
.description('Revoke an access token')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const id = args[0];
|
||||
await ctx.client.deleteAccessToken(id);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id });
|
||||
else
|
||||
process.stdout.write(`✓ Token ${id} revoked\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.authCommand = authCommand;
|
||||
Vendored
+288
@@ -0,0 +1,288 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.sqlCommand = exports.fireCommand = exports.runsCommand = exports.secretCommand = void 0;
|
||||
/**
|
||||
* `inkreach secret` — manage workflow automation secrets.
|
||||
* `inkreach runs` — execution history.
|
||||
* `inkreach fire` — manual trigger testing (button-click / webhook).
|
||||
* `inkreach sql` — read-only SQL queries.
|
||||
*/
|
||||
const commander_1 = require("commander");
|
||||
const ctx_1 = require("../lib/ctx");
|
||||
const output_1 = require("../lib/output");
|
||||
// ===================== secret =====================
|
||||
function secretCommand() {
|
||||
const cmd = new commander_1.Command('secret');
|
||||
cmd.description('Manage automation script secrets');
|
||||
cmd
|
||||
.command('list <workflowId>')
|
||||
.description('List secrets for a workflow. Values are never returned.')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const list = (await ctx.client.listSecrets(baseId, workflowId));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No secrets found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
{ key: 'description', header: 'DESCRIPTION' },
|
||||
], list.map((s) => ({
|
||||
id: s.id,
|
||||
name: (0, output_1.truncate)(s.name, 30),
|
||||
description: (0, output_1.truncate)(s.description, 50),
|
||||
})));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('set <workflowId>')
|
||||
.description('Create a new secret')
|
||||
.requiredOption('--name <name>', 'Secret name')
|
||||
.requiredOption('--value <value>', 'Secret value')
|
||||
.option('--description <desc>', 'Secret description')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const created = await ctx.client.createSecret(baseId, workflowId, {
|
||||
name: opts.name,
|
||||
value: opts.value,
|
||||
description: opts.description,
|
||||
});
|
||||
const s = created;
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(created);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['id', s.id],
|
||||
['name', s.name],
|
||||
['', ''],
|
||||
['note', 'Secret value is never returned. Update it if needed.'],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('update <workflowId> <secretId>')
|
||||
.description('Update a secret')
|
||||
.option('--name <name>', 'New name')
|
||||
.option('--value <value>', 'New value')
|
||||
.option('--description <desc>', 'New description')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [workflowId, secretId] = args;
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
await ctx.client.updateSecret(baseId, workflowId, secretId, {
|
||||
name: opts.name,
|
||||
value: opts.value,
|
||||
description: opts.description,
|
||||
});
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: secretId });
|
||||
else
|
||||
process.stdout.write(`✓ Secret ${secretId} updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete <workflowId> <secretId>')
|
||||
.description('Delete a secret')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [workflowId, secretId] = args;
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
await ctx.client.deleteSecret(baseId, workflowId, secretId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: secretId });
|
||||
else
|
||||
process.stdout.write(`✓ Secret ${secretId} deleted\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.secretCommand = secretCommand;
|
||||
// ===================== runs =====================
|
||||
function runsCommand() {
|
||||
const cmd = new commander_1.Command('runs');
|
||||
cmd.description('Execution history for automations');
|
||||
cmd
|
||||
.command('list <workflowId>')
|
||||
.description('List execution history')
|
||||
.option('--status <status>', 'Filter by status: failed, succeeded, or running')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
let list = (await ctx.client.listExecutions(baseId, workflowId));
|
||||
if (opts.status) {
|
||||
list = list.filter((e) => e.status === opts.status);
|
||||
}
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No executions found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'status', header: 'STATUS' },
|
||||
{ key: 'triggerType', header: 'TRIGGER' },
|
||||
{ key: 'startedAt', header: 'STARTED' },
|
||||
{ key: 'finishedAt', header: 'FINISHED' },
|
||||
], list.map((e) => ({
|
||||
id: e.id,
|
||||
status: e.status,
|
||||
triggerType: e.triggerType || '',
|
||||
startedAt: (0, output_1.truncate)(e.startedAt, 25),
|
||||
finishedAt: (0, output_1.truncate)(e.finishedAt, 25),
|
||||
})));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get <workflowId> <executionId>')
|
||||
.description('Get execution detail with node results')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [workflowId, executionId] = args;
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const exec = await ctx.client.getExecution(baseId, workflowId, executionId);
|
||||
const ex = exec;
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(exec);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['id', ex.id],
|
||||
['status', ex.status],
|
||||
['trigger', ex.triggerType || ''],
|
||||
['started', ex.startedAt || ''],
|
||||
['finished', ex.finishedAt || ''],
|
||||
]);
|
||||
if (ex.nodeResults?.length) {
|
||||
process.stdout.write('\nNODE RESULTS:\n');
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'nodeId', header: 'NODE' },
|
||||
{ key: 'status', header: 'STATUS' },
|
||||
{ key: 'error', header: 'ERROR' },
|
||||
], ex.nodeResults.map((r) => ({
|
||||
nodeId: r.nodeId,
|
||||
status: r.status,
|
||||
error: (0, output_1.truncate)(r.error, 50),
|
||||
})));
|
||||
}
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('rerun <workflowId> <executionId>')
|
||||
.description('Rerun a failed execution')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [workflowId, executionId] = args;
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const res = (await ctx.client.rerunExecution(baseId, workflowId, executionId));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(res);
|
||||
else {
|
||||
const pairs = [
|
||||
['executionId', executionId],
|
||||
['status', 'rerun requested'],
|
||||
];
|
||||
if (res.id)
|
||||
pairs.push(['newExecutionId', res.id]);
|
||||
(0, output_1.printKv)(pairs);
|
||||
}
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.runsCommand = runsCommand;
|
||||
// ===================== fire =====================
|
||||
function fireCommand() {
|
||||
const cmd = new commander_1.Command('fire');
|
||||
cmd.description('Manually fire triggers for testing');
|
||||
cmd
|
||||
.command('button-click <tableId> <recordId> <fieldId>')
|
||||
.description('Simulate a button field click')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const [tableId, recordId, fieldId] = args;
|
||||
const res = await ctx.client.fireButtonClick(tableId, recordId, fieldId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(res);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['runId', res.runId],
|
||||
['tableId', res.tableId],
|
||||
['fieldId', res.fieldId],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('webhook <baseId> <workflowId>')
|
||||
.description('Simulate a webhook call')
|
||||
.option('--body <json>', 'JSON body to send')
|
||||
.option('--method <method>', 'HTTP method (informational only)', 'POST')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [baseId, workflowId] = args;
|
||||
const opts = o;
|
||||
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
||||
const res = await ctx.client.fireWebhook(baseId, workflowId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(res);
|
||||
else {
|
||||
process.stdout.write(`✓ Webhook fired for workflow ${workflowId}\n`);
|
||||
if (typeof res === 'object' && res !== null) {
|
||||
(0, output_1.printJson)(res);
|
||||
}
|
||||
}
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.fireCommand = fireCommand;
|
||||
// ===================== sql =====================
|
||||
function sqlCommand() {
|
||||
const cmd = new commander_1.Command('sql');
|
||||
cmd.description('Execute a read-only SQL query (SELECT only)');
|
||||
cmd.option('--base-id <id>', 'Base id (overrides default)');
|
||||
cmd.argument('<sql>', 'SQL SELECT statement');
|
||||
cmd.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const sql = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const rows = (await ctx.client.sql(baseId, sql));
|
||||
if (ctx.json) {
|
||||
(0, output_1.printJson)(rows);
|
||||
}
|
||||
else {
|
||||
if (!rows.length) {
|
||||
process.stdout.write('No rows returned.\n');
|
||||
return;
|
||||
}
|
||||
const first = rows[0];
|
||||
if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
|
||||
const keys = Object.keys(first);
|
||||
(0, output_1.printTable)(keys.map((k) => ({ key: k, header: k })), rows.map((r) => {
|
||||
const obj = r;
|
||||
const row = {};
|
||||
for (const k of keys) {
|
||||
row[k] = (0, output_1.truncate)(obj[k], 40);
|
||||
}
|
||||
return row;
|
||||
}));
|
||||
process.stdout.write(`\n${rows.length} row(s).\n`);
|
||||
}
|
||||
else {
|
||||
rows.forEach((r) => process.stdout.write(JSON.stringify(r) + '\n'));
|
||||
}
|
||||
}
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.sqlCommand = sqlCommand;
|
||||
Vendored
+462
@@ -0,0 +1,462 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.baseResourceCommands = void 0;
|
||||
/**
|
||||
* `inkreach base|table|field|record|view` — resource CRUD commands.
|
||||
*
|
||||
* These endpoints are compatible with official teable open-api.
|
||||
*/
|
||||
const commander_1 = require("commander");
|
||||
const ctx_1 = require("../lib/ctx");
|
||||
const output_1 = require("../lib/output");
|
||||
function baseResourceCommands() {
|
||||
return [baseCmd(), tableCmd(), fieldCmd(), recordCmd(), viewCmd()];
|
||||
}
|
||||
exports.baseResourceCommands = baseResourceCommands;
|
||||
// ===================== base =====================
|
||||
function baseCmd() {
|
||||
const cmd = new commander_1.Command('base');
|
||||
cmd.description('Manage bases (database containers)');
|
||||
cmd
|
||||
.command('list')
|
||||
.description('List all accessible bases')
|
||||
.action((0, ctx_1.run)(async (ctx) => {
|
||||
const list = (await ctx.client.listBases());
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No bases found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
], list.map((b) => ({ id: (0, output_1.truncate)(b.id), name: (0, output_1.truncate)(b.name, 30) })));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get <baseId>')
|
||||
.description('Get base details')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const baseId = args[0];
|
||||
const base = await ctx.client.getBase(baseId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(base);
|
||||
else {
|
||||
const b = base;
|
||||
(0, output_1.printKv)(Object.entries(b).map(([k, v]) => [k, v]));
|
||||
}
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
// ===================== table =====================
|
||||
function tableCmd() {
|
||||
const cmd = new commander_1.Command('table');
|
||||
cmd.description('Manage tables within a base');
|
||||
cmd
|
||||
.command('list')
|
||||
.description('List tables in a base')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const list = (await ctx.client.listTables(baseId));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No tables found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
{ key: 'dbTableName', header: 'DB TABLE' },
|
||||
], list.map((t) => ({
|
||||
id: (0, output_1.truncate)(t.id),
|
||||
name: (0, output_1.truncate)(t.name, 30),
|
||||
dbTableName: (0, output_1.truncate)(t.dbTableName ?? ''),
|
||||
})));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get <tableId>')
|
||||
.description('Get table details')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const tableId = args[0];
|
||||
const table = await ctx.client.getTable(tableId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(table);
|
||||
else {
|
||||
const t = table;
|
||||
(0, output_1.printKv)(Object.entries(t).map(([k, v]) => [k, v]));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('create')
|
||||
.description('Create a new table')
|
||||
.requiredOption('--name <name>', 'Table name')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.option('--fields <json>', 'Field definitions JSON array, e.g. \'[{"name":"Title","type":"singleLineText"}]\'')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const body = { name: opts.name };
|
||||
if (opts.fields)
|
||||
body.fields = JSON.parse(opts.fields);
|
||||
const table = await ctx.client.createTable(baseId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(table);
|
||||
else {
|
||||
const t = table;
|
||||
(0, output_1.printKv)([['id', t.id], ['name', t.name]]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('update <tableId>')
|
||||
.description('Update table (rename)')
|
||||
.requiredOption('--name <name>', 'New table name')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const tableId = args[0];
|
||||
const opts = o;
|
||||
const table = await ctx.client.updateTable(tableId, { name: opts.name });
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(table);
|
||||
else
|
||||
process.stdout.write(`✓ Table ${tableId} updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete <tableId>')
|
||||
.description('Delete a table')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const tableId = args[0];
|
||||
await ctx.client.deleteTable(tableId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: tableId });
|
||||
else
|
||||
process.stdout.write(`✓ Table ${tableId} deleted\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
// ===================== field =====================
|
||||
function fieldCmd() {
|
||||
const cmd = new commander_1.Command('field');
|
||||
cmd.description('Manage fields (columns) in a table');
|
||||
cmd
|
||||
.command('list <tableId>')
|
||||
.description('List fields in a table')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const tableId = args[0];
|
||||
const list = (await ctx.client.listFields(tableId));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No fields found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
{ key: 'type', header: 'TYPE' },
|
||||
], list.map((f) => ({
|
||||
id: (0, output_1.truncate)(f.id),
|
||||
name: (0, output_1.truncate)(f.name, 30),
|
||||
type: f.type,
|
||||
})));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get <fieldId>')
|
||||
.description('Get field details')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const fieldId = args[0];
|
||||
const field = await ctx.client.getField(fieldId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(field);
|
||||
else {
|
||||
const f = field;
|
||||
(0, output_1.printKv)(Object.entries(f).map(([k, v]) => [k, v]));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('create')
|
||||
.description('Create a new field')
|
||||
.requiredOption('--table-id <id>', 'Table id')
|
||||
.requiredOption('--name <name>', 'Field name')
|
||||
.requiredOption('--type <type>', 'Field type (singleLineText, number, singleSelect, link, formula, etc.)')
|
||||
.option('--options <json>', 'Field options JSON (type-specific)')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const body = { name: opts.name, type: opts.type };
|
||||
if (opts.options)
|
||||
body.options = JSON.parse(opts.options);
|
||||
const field = await ctx.client.createField(opts.tableId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(field);
|
||||
else {
|
||||
const f = field;
|
||||
(0, output_1.printKv)([['id', f.id], ['name', f.name], ['type', f.type]]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('update <fieldId>')
|
||||
.description('Update a field')
|
||||
.option('--name <name>', 'New field name')
|
||||
.option('--options <json>', 'New field options JSON')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const fieldId = args[0];
|
||||
const opts = o;
|
||||
const body = {};
|
||||
if (opts.name)
|
||||
body.name = opts.name;
|
||||
if (opts.options)
|
||||
body.options = JSON.parse(opts.options);
|
||||
const field = await ctx.client.updateField(fieldId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(field);
|
||||
else
|
||||
process.stdout.write(`✓ Field ${fieldId} updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete <fieldId>')
|
||||
.description('Delete a field')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const fieldId = args[0];
|
||||
await ctx.client.deleteField(fieldId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: fieldId });
|
||||
else
|
||||
process.stdout.write(`✓ Field ${fieldId} deleted\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
// ===================== record =====================
|
||||
function recordCmd() {
|
||||
const cmd = new commander_1.Command('record');
|
||||
cmd.description('Manage records (rows) in a table');
|
||||
cmd
|
||||
.command('get')
|
||||
.description('Query records with filters, pagination, and search')
|
||||
.requiredOption('--table-id <id>', 'Table id')
|
||||
.option('--filter <json>', 'Filter conditions JSON')
|
||||
.option('--projection <json>', 'Field projection JSON array')
|
||||
.option('--page-size <n>', 'Records per page')
|
||||
.option('--page <n>', 'Page number (1-based)')
|
||||
.option('--search <kw>', 'Search keyword')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
// validate baseId is present (even though getRecords only needs tableId,
|
||||
// we follow the convention that base-scoped commands require baseId config)
|
||||
(0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const query = {};
|
||||
if (opts.filter)
|
||||
query.filter = JSON.parse(opts.filter);
|
||||
if (opts.projection)
|
||||
query.projection = JSON.parse(opts.projection);
|
||||
if (opts.search)
|
||||
query.search = opts.search;
|
||||
const page = opts.page ? parseInt(opts.page, 10) : undefined;
|
||||
const pageSize = opts.pageSize ? parseInt(opts.pageSize, 10) : undefined;
|
||||
if (pageSize)
|
||||
query.take = pageSize;
|
||||
if (page) {
|
||||
const take = pageSize || 100;
|
||||
query.skip = (page - 1) * take;
|
||||
if (!pageSize)
|
||||
query.take = take;
|
||||
}
|
||||
const result = (await ctx.client.getRecords(opts.tableId, query));
|
||||
if (ctx.json) {
|
||||
(0, output_1.printJson)(result);
|
||||
}
|
||||
else {
|
||||
if (!result.records?.length) {
|
||||
process.stdout.write('No records found.\n');
|
||||
return;
|
||||
}
|
||||
const firstFields = result.records[0].fields || {};
|
||||
const fieldKeys = Object.keys(firstFields);
|
||||
const columns = fieldKeys.map((k) => ({ key: k, header: k }));
|
||||
(0, output_1.printTable)(columns, result.records.map((rec) => {
|
||||
const f = rec.fields || {};
|
||||
const row = { id: (0, output_1.truncate)(rec.id) };
|
||||
for (const k of fieldKeys) {
|
||||
row[k] = (0, output_1.truncate)(f[k], 30);
|
||||
}
|
||||
return row;
|
||||
}));
|
||||
if (result.total !== undefined) {
|
||||
process.stdout.write(`\n${result.records.length} of ${result.total} record(s).\n`);
|
||||
}
|
||||
else {
|
||||
process.stdout.write(`\n${result.records.length} record(s).\n`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get-one <recordId>')
|
||||
.description('Get a single record by id')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const recordId = args[0];
|
||||
const record = await ctx.client.getRecord(recordId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(record);
|
||||
else {
|
||||
const rec = record;
|
||||
const fields = rec.fields || {};
|
||||
(0, output_1.printKv)([
|
||||
['id', rec.id],
|
||||
...Object.entries(fields).map(([k, v]) => [k, v]),
|
||||
]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('create')
|
||||
.description('Create record(s). Use --fields for a single record, --records for bulk.')
|
||||
.requiredOption('--table-id <id>', 'Table id')
|
||||
.option('--fields <json>', 'Fields object JSON, e.g. \'{"Title":"hello"}\'')
|
||||
.option('--records <json>', 'Records array JSON for bulk, e.g. \'[{"fields":{"Title":"a"}}]\'')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
let body;
|
||||
if (opts.records) {
|
||||
body = { records: JSON.parse(opts.records) };
|
||||
}
|
||||
else if (opts.fields) {
|
||||
body = { fields: JSON.parse(opts.fields) };
|
||||
}
|
||||
else {
|
||||
throw Object.assign(new Error('Provide --fields or --records'), {
|
||||
code: 'MISSING_FIELDS',
|
||||
});
|
||||
}
|
||||
const result = await ctx.client.createRecord(opts.tableId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(result);
|
||||
else
|
||||
process.stdout.write(`✓ Record(s) created\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('update')
|
||||
.description('Batch update records')
|
||||
.requiredOption('--table-id <id>', 'Table id')
|
||||
.requiredOption('--records <json>', 'Records JSON, e.g. \'[{"id":"recX","fields":{"Title":"b"}}]\'')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const body = { records: JSON.parse(opts.records) };
|
||||
const result = await ctx.client.updateRecord(opts.tableId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(result);
|
||||
else
|
||||
process.stdout.write(`✓ Records updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete')
|
||||
.description('Batch delete records by ids')
|
||||
.requiredOption('--table-id <id>', 'Table id')
|
||||
.requiredOption('--record-ids <csv>', 'Record ids (comma-separated)')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const ids = opts.recordIds
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const body = { records: ids };
|
||||
const result = await ctx.client.deleteRecord(opts.tableId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(result);
|
||||
else
|
||||
process.stdout.write(`✓ ${ids.length} record(s) deleted\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
// ===================== view =====================
|
||||
function viewCmd() {
|
||||
const cmd = new commander_1.Command('view');
|
||||
cmd.description('Manage views in a table');
|
||||
cmd
|
||||
.command('list <tableId>')
|
||||
.description('List views in a table')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const tableId = args[0];
|
||||
const list = (await ctx.client.listViews(tableId));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No views found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
{ key: 'type', header: 'TYPE' },
|
||||
], list.map((v) => ({
|
||||
id: (0, output_1.truncate)(v.id),
|
||||
name: (0, output_1.truncate)(v.name, 30),
|
||||
type: v.type,
|
||||
})));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get <viewId>')
|
||||
.description('Get view details')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const viewId = args[0];
|
||||
const view = await ctx.client.getView(viewId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(view);
|
||||
else {
|
||||
const v = view;
|
||||
(0, output_1.printKv)(Object.entries(v).map(([k, val]) => [k, val]));
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('create')
|
||||
.description('Create a new view')
|
||||
.requiredOption('--table-id <id>', 'Table id')
|
||||
.requiredOption('--name <name>', 'View name')
|
||||
.option('--type <type>', 'View type (grid, calendar, kanban, gallery, form)', 'grid')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const body = { name: opts.name, type: opts.type };
|
||||
const view = await ctx.client.createView(opts.tableId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(view);
|
||||
else {
|
||||
const v = view;
|
||||
(0, output_1.printKv)([['id', v.id], ['name', v.name], ['type', v.type]]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('update <viewId>')
|
||||
.description('Update a view (rename)')
|
||||
.option('--name <name>', 'New view name')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const viewId = args[0];
|
||||
const opts = o;
|
||||
const body = {};
|
||||
if (opts.name)
|
||||
body.name = opts.name;
|
||||
const view = await ctx.client.updateView(viewId, body);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(view);
|
||||
else
|
||||
process.stdout.write(`✓ View ${viewId} updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete <viewId>')
|
||||
.description('Delete a view')
|
||||
.action((0, ctx_1.run)(async (ctx, args) => {
|
||||
const viewId = args[0];
|
||||
await ctx.client.deleteView(viewId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: viewId });
|
||||
else
|
||||
process.stdout.write(`✓ View ${viewId} deleted\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
Vendored
+520
@@ -0,0 +1,520 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildActionConfig = exports.actionCommand = exports.buildTriggerConfig = exports.triggerCommand = exports.workflowCommand = void 0;
|
||||
/**
|
||||
* `inkreach workflow` — workflow CRUD + lifecycle.
|
||||
* `inkreach trigger` — set the trigger node's type & config.
|
||||
* `inkreach action` — add/update/delete action nodes.
|
||||
*
|
||||
* All node-mutating commands operate via PUT /node (whole-graph replacement),
|
||||
* so they internally GET the full graph, mutate, then PUT it back.
|
||||
*/
|
||||
const commander_1 = require("commander");
|
||||
const ctx_1 = require("../lib/ctx");
|
||||
const output_1 = require("../lib/output");
|
||||
// ===================== workflow =====================
|
||||
function workflowCommand() {
|
||||
const cmd = new commander_1.Command('workflow');
|
||||
cmd.description('Manage automations (workflows) — the self-built automation engine');
|
||||
cmd
|
||||
.command('list')
|
||||
.description('List all workflows in the base')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const list = (await ctx.client.listWorkflows(baseId));
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(list);
|
||||
else {
|
||||
if (!list.length) {
|
||||
process.stdout.write('No workflows found.\n');
|
||||
return;
|
||||
}
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'name', header: 'NAME' },
|
||||
{ key: 'isActive', header: 'ACTIVE' },
|
||||
{ key: 'hasDraft', header: 'DRAFT' },
|
||||
{ key: 'nodes', header: 'NODES' },
|
||||
], list.map((w) => ({
|
||||
id: w.id,
|
||||
name: (0, output_1.truncate)(w.name, 30),
|
||||
isActive: w.isActive ? '✓' : '',
|
||||
hasDraft: w.hasDraft ? '*' : '',
|
||||
nodes: (w.nodes || []).length,
|
||||
})));
|
||||
process.stdout.write(`\n${list.length} workflow(s). Use 'workflow get <id>' for details.\n`);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('get <workflowId>')
|
||||
.description('Get workflow detail (nodes + edges)')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(wf);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['id', wf.id],
|
||||
['name', wf.name],
|
||||
['description', wf.description || ''],
|
||||
['active', wf.isActive ? 'yes' : 'no'],
|
||||
['hasDraft', wf.hasDraft ? 'yes' : 'no'],
|
||||
]);
|
||||
process.stdout.write('\nNODES:\n');
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'type', header: 'TYPE' },
|
||||
{ key: 'category', header: 'CATEGORY' },
|
||||
{ key: 'config', header: 'CONFIG' },
|
||||
], (wf.nodes || []).map((n) => ({
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
category: n.category,
|
||||
config: (0, output_1.truncate)(JSON.stringify(n.config), 40),
|
||||
})));
|
||||
if (wf.edges?.length) {
|
||||
process.stdout.write('\nEDGES:\n');
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'id', header: 'ID' },
|
||||
{ key: 'sourceNodeId', header: 'SOURCE' },
|
||||
{ key: 'targetNodeId', header: 'TARGET' },
|
||||
], (wf.edges || []).map((e) => ({ ...e })));
|
||||
}
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('create')
|
||||
.description('Create a workflow (auto-includes a recordCreated trigger node)')
|
||||
.requiredOption('--name <name>', 'Workflow name')
|
||||
.option('--description <desc>', 'Workflow description')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const wf = await ctx.client.createWorkflow(baseId, {
|
||||
name: opts.name,
|
||||
description: opts.description,
|
||||
});
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(wf);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['id', wf.id],
|
||||
['name', wf.name],
|
||||
['baseId', wf.baseId],
|
||||
['', ''],
|
||||
['next', `inkreach trigger set ${wf.id} --type <T> ...`],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('update <workflowId>')
|
||||
.description('Update workflow name / description')
|
||||
.option('--name <name>', 'New name')
|
||||
.option('--description <desc>', 'New description')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const wf = await ctx.client.updateWorkflow(baseId, workflowId, {
|
||||
name: opts.name,
|
||||
description: opts.description,
|
||||
});
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(wf);
|
||||
else
|
||||
process.stdout.write(`✓ Workflow ${workflowId} updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete <workflowId>')
|
||||
.description('Soft-delete a workflow (cascades: base_node, auto-token, trigger unregister)')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
await ctx.client.deleteWorkflow(baseId, workflowId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: workflowId });
|
||||
else
|
||||
process.stdout.write(`✓ Workflow ${workflowId} deleted\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('activate <workflowId>')
|
||||
.description('Activate: snapshot + register trigger + create auto access token')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const res = await ctx.client.activateWorkflow(baseId, workflowId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, ...res });
|
||||
else
|
||||
(0, output_1.printKv)([['snapshotId', res.snapshotId], ['status', 'activated']]);
|
||||
}));
|
||||
cmd
|
||||
.command('deactivate <workflowId>')
|
||||
.description('Deactivate: unregister trigger')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
await ctx.client.deactivateWorkflow(baseId, workflowId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)({ ok: true, id: workflowId });
|
||||
else
|
||||
process.stdout.write(`✓ Workflow ${workflowId} deactivated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('test <workflowId>')
|
||||
.description('Test-run: execute action nodes with mock input (no trigger, no queue)')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const res = await ctx.client.testWorkflow(baseId, workflowId);
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(res);
|
||||
else {
|
||||
process.stdout.write('Test results:\n');
|
||||
(0, output_1.printTable)([
|
||||
{ key: 'nodeId', header: 'NODE' },
|
||||
{ key: 'status', header: 'STATUS' },
|
||||
{ key: 'error', header: 'ERROR' },
|
||||
], (res.nodeResults || []).map((r) => {
|
||||
const row = r;
|
||||
return {
|
||||
nodeId: row.nodeId,
|
||||
status: row.status,
|
||||
error: (0, output_1.truncate)(row.error, 50),
|
||||
};
|
||||
}));
|
||||
}
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.workflowCommand = workflowCommand;
|
||||
// ===================== trigger =====================
|
||||
const TRIGGER_TYPES = [
|
||||
'recordCreated', 'recordUpdated', 'recordCondition', 'recordCreatedOrUpdated',
|
||||
'formSubmitted', 'scheduledTime', 'buttonClick', 'webhook',
|
||||
];
|
||||
function triggerCommand() {
|
||||
const cmd = new commander_1.Command('trigger');
|
||||
cmd.description('Set the trigger node (type + config). Builds correct config keys for you.');
|
||||
cmd
|
||||
.command('set <workflowId>')
|
||||
.description('Set the trigger type and config (replaces the existing trigger node)')
|
||||
.requiredOption('--type <type>', `Trigger type: ${TRIGGER_TYPES.join('|')}`)
|
||||
.option('--table-id <id>', 'Table id (most record/button triggers)')
|
||||
.option('--field-ids <json>', 'Field ids JSON array, e.g. \'["fldXXX"]\' (recordUpdated, buttonClick)')
|
||||
.option('--form-id <id>', 'Form view id (formSubmitted)')
|
||||
.option('--conditions <json>', 'Filter conditions JSON (recordCondition/recordCreated)')
|
||||
.option('--schedule <json>', 'Schedule JSON {timing:{type,interval}} (scheduledTime)')
|
||||
.option('--batch <json>', 'Batch config JSON {enabled,batchWindow,batchSize}')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
if (!TRIGGER_TYPES.includes(opts.type)) {
|
||||
throw Object.assign(new Error(`Invalid trigger type: ${opts.type}`), { code: 'INVALID_TYPE' });
|
||||
}
|
||||
const config = buildTriggerConfig(opts.type, {
|
||||
tableId: opts.tableId,
|
||||
fieldIds: opts.fieldIds,
|
||||
formId: opts.formId,
|
||||
conditions: opts.conditions,
|
||||
schedule: opts.schedule,
|
||||
batch: opts.batch,
|
||||
});
|
||||
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
||||
const nodes = (wf.nodes || []).slice();
|
||||
// Find the trigger node (there is exactly one) and replace its category + config.
|
||||
const triggerIdx = nodes.findIndex((n) => n.type === 'trigger');
|
||||
const newTrigger = {
|
||||
...nodes[triggerIdx >= 0 ? triggerIdx : 0],
|
||||
type: 'trigger',
|
||||
category: opts.type,
|
||||
config,
|
||||
};
|
||||
if (triggerIdx >= 0)
|
||||
nodes[triggerIdx] = newTrigger;
|
||||
else
|
||||
nodes.unshift(newTrigger);
|
||||
const updated = await ctx.client.replaceNodes(baseId, workflowId, {
|
||||
nodes,
|
||||
edges: wf.edges || [],
|
||||
});
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(updated);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['workflowId', workflowId],
|
||||
['triggerType', opts.type],
|
||||
['config', JSON.stringify(config)],
|
||||
['', ''],
|
||||
['next', `inkreach action add ${workflowId} --type script ...`],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.triggerCommand = triggerCommand;
|
||||
/** Build the trigger node config JSON using the SELF-BUILT contract keys. */
|
||||
function buildTriggerConfig(type, opts) {
|
||||
const parse = (s) => (s ? JSON.parse(s) : undefined);
|
||||
const config = {};
|
||||
switch (type) {
|
||||
case 'recordCreated':
|
||||
case 'recordCreatedOrUpdated': {
|
||||
if (opts.tableId)
|
||||
config.tableId = opts.tableId;
|
||||
if (opts.conditions)
|
||||
config.conditions = parse(opts.conditions);
|
||||
break;
|
||||
}
|
||||
case 'recordUpdated': {
|
||||
if (opts.tableId)
|
||||
config.tableId = opts.tableId;
|
||||
// NOTE: key is `fieldIds` (NOT watchFieldIds). [] = watch all fields.
|
||||
config.fieldIds = opts.fieldIds ? parse(opts.fieldIds) : [];
|
||||
break;
|
||||
}
|
||||
case 'recordCondition': {
|
||||
if (opts.tableId)
|
||||
config.tableId = opts.tableId;
|
||||
// conditions is required for recordCondition
|
||||
config.conditions = opts.conditions ? parse(opts.conditions) : { conjunction: 'and', filterSet: [] };
|
||||
break;
|
||||
}
|
||||
case 'formSubmitted': {
|
||||
if (opts.tableId)
|
||||
config.tableId = opts.tableId;
|
||||
if (opts.formId)
|
||||
config.formId = opts.formId;
|
||||
break;
|
||||
}
|
||||
case 'scheduledTime': {
|
||||
// Backend ONLY honors timing.type + timing.interval (rest is ignored).
|
||||
if (opts.schedule) {
|
||||
const sched = parse(opts.schedule);
|
||||
if (sched.timing)
|
||||
config.timing = sched.timing;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'buttonClick': {
|
||||
if (opts.tableId)
|
||||
config.tableId = opts.tableId;
|
||||
config.fieldIds = opts.fieldIds ? parse(opts.fieldIds) : [];
|
||||
break;
|
||||
}
|
||||
case 'webhook': {
|
||||
// authorization is UI-only; backend does NOT validate it.
|
||||
config.authorization = { type: 'none' };
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (opts.batch && ['recordCreated', 'recordUpdated', 'recordCondition', 'recordCreatedOrUpdated'].includes(type)) {
|
||||
config.batch = parse(opts.batch);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
exports.buildTriggerConfig = buildTriggerConfig;
|
||||
// ===================== action =====================
|
||||
const ACTION_TYPES = [
|
||||
'script', 'sendEmail', 'httpRequest', 'createRecord', 'updateRecord', 'getRecords',
|
||||
];
|
||||
function actionCommand() {
|
||||
const cmd = new commander_1.Command('action');
|
||||
cmd.description('Add/update/delete action nodes');
|
||||
cmd
|
||||
.command('add <workflowId>')
|
||||
.description('Add an action node after the last existing node')
|
||||
.requiredOption('--type <type>', `Action type: ${ACTION_TYPES.join('|')}`)
|
||||
.option('--config <json>', 'Full action config JSON (overrides the granular flags below)')
|
||||
.option('--code <js>', 'Script JS code (script)')
|
||||
.option('--code-file <path>', 'Read script code from a file (script)')
|
||||
.option('--timeout <ms>', 'Script timeout ms (script, default 60000)')
|
||||
.option('--to <email>', 'Recipient (sendEmail)')
|
||||
.option('--subject <s>', 'Subject (sendEmail)')
|
||||
.option('--body <html>', 'HTML body (sendEmail)')
|
||||
.option('--url <u>', 'Request URL (httpRequest)')
|
||||
.option('--method <m>', 'HTTP method (httpRequest, default GET)')
|
||||
.option('--headers <json>', 'Request headers JSON (httpRequest)')
|
||||
.option('--req-body <json>', 'Request body JSON (httpRequest)')
|
||||
.option('--target-table <id>', 'Target table id (createRecord/updateRecord/getRecords)')
|
||||
.option('--records <json>', 'Records JSON array (createRecord/updateRecord)')
|
||||
.option('--query <json>', 'Query JSON (getRecords)')
|
||||
.option('--name <n>', 'Optional node display name')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const workflowId = args[0];
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
if (!ACTION_TYPES.includes(opts.type)) {
|
||||
throw Object.assign(new Error(`Invalid action type: ${opts.type}`), { code: 'INVALID_TYPE' });
|
||||
}
|
||||
const config = opts.config ? JSON.parse(opts.config) : buildActionConfig(opts);
|
||||
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
||||
const nodes = (wf.nodes || []).slice();
|
||||
const edges = (wf.edges || []).slice();
|
||||
const newNodeId = `node-${Date.now()}`;
|
||||
const newNode = {
|
||||
id: newNodeId,
|
||||
type: 'action',
|
||||
category: opts.type,
|
||||
config,
|
||||
name: opts.name || null,
|
||||
};
|
||||
// Chain after the last node (trigger → ... → new).
|
||||
const lastNode = nodes[nodes.length - 1];
|
||||
nodes.push(newNode);
|
||||
if (lastNode) {
|
||||
edges.push({
|
||||
id: `edge-${lastNode.id}-${newNodeId}`,
|
||||
sourceNodeId: lastNode.id,
|
||||
targetNodeId: newNodeId,
|
||||
});
|
||||
}
|
||||
const updated = await ctx.client.replaceNodes(baseId, workflowId, { nodes, edges });
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(updated);
|
||||
else {
|
||||
(0, output_1.printKv)([
|
||||
['workflowId', workflowId],
|
||||
['nodeId', newNodeId],
|
||||
['type', opts.type],
|
||||
['config', JSON.stringify(config)],
|
||||
['', ''],
|
||||
['next', `inkreach workflow activate ${workflowId}`],
|
||||
]);
|
||||
}
|
||||
}));
|
||||
cmd
|
||||
.command('update <workflowId> <nodeId>')
|
||||
.description('Update an existing action node config')
|
||||
.option('--config <json>', 'Full replacement config JSON')
|
||||
.option('--code <js>', 'Script JS code (script)')
|
||||
.option('--code-file <path>', 'Read script code from a file (script)')
|
||||
.option('--name <n>', 'New node display name')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [workflowId, nodeId] = args;
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
||||
const nodes = (wf.nodes || []).slice();
|
||||
const idx = nodes.findIndex((n) => n.id === nodeId);
|
||||
if (idx < 0)
|
||||
throw Object.assign(new Error(`Node ${nodeId} not found`), { code: 'NOT_FOUND' });
|
||||
const node = nodes[idx];
|
||||
let config = node.config || {};
|
||||
if (opts.config)
|
||||
config = JSON.parse(opts.config);
|
||||
else if (opts.code !== undefined || opts['code-file'] !== undefined) {
|
||||
config = { ...config, code: opts['code-file'] ? readFile(opts['code-file']) : opts.code };
|
||||
}
|
||||
nodes[idx] = { ...node, config, name: opts.name ?? node.name };
|
||||
const updated = await ctx.client.replaceNodes(baseId, workflowId, {
|
||||
nodes,
|
||||
edges: wf.edges || [],
|
||||
});
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(updated);
|
||||
else
|
||||
process.stdout.write(`✓ Node ${nodeId} updated\n`);
|
||||
}));
|
||||
cmd
|
||||
.command('delete <workflowId> <nodeId>')
|
||||
.description('Delete an action node and its connected edges')
|
||||
.option('--base-id <id>', 'Base id (overrides default)')
|
||||
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
||||
const [workflowId, nodeId] = args;
|
||||
const opts = o;
|
||||
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
||||
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
||||
const nodes = (wf.nodes || []).filter((n) => n.id !== nodeId);
|
||||
const edges = (wf.edges || []).filter((e) => e.sourceNodeId !== nodeId && e.targetNodeId !== nodeId);
|
||||
const updated = await ctx.client.replaceNodes(baseId, workflowId, { nodes, edges });
|
||||
if (ctx.json)
|
||||
(0, output_1.printJson)(updated);
|
||||
else
|
||||
process.stdout.write(`✓ Node ${nodeId} deleted\n`);
|
||||
}));
|
||||
return cmd;
|
||||
}
|
||||
exports.actionCommand = actionCommand;
|
||||
function readFile(p) {
|
||||
// inline require to keep imports clean
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
return require('node:fs').readFileSync(p, 'utf8');
|
||||
}
|
||||
/** Build action config from granular flags. */
|
||||
function buildActionConfig(opts) {
|
||||
const type = opts.type;
|
||||
const config = {};
|
||||
switch (type) {
|
||||
case 'script': {
|
||||
if (opts['code-file'])
|
||||
config.code = readFile(opts['code-file']);
|
||||
else if (opts.code)
|
||||
config.code = opts.code;
|
||||
if (opts.timeout)
|
||||
config.timeout = Number(opts.timeout);
|
||||
break;
|
||||
}
|
||||
case 'sendEmail': {
|
||||
if (opts.to)
|
||||
config.to = opts.to;
|
||||
if (opts.subject)
|
||||
config.subject = opts.subject;
|
||||
if (opts.body)
|
||||
config.body = opts.body;
|
||||
break;
|
||||
}
|
||||
case 'httpRequest': {
|
||||
if (opts.url)
|
||||
config.url = opts.url;
|
||||
if (opts.method)
|
||||
config.method = opts.method;
|
||||
if (opts.headers)
|
||||
config.headers = JSON.parse(opts.headers);
|
||||
if (opts['req-body'])
|
||||
config.body = JSON.parse(opts['req-body']);
|
||||
break;
|
||||
}
|
||||
case 'createRecord':
|
||||
case 'updateRecord': {
|
||||
if (opts['target-table'])
|
||||
config.tableId = opts['target-table'];
|
||||
if (opts.records)
|
||||
config.records = JSON.parse(opts.records);
|
||||
break;
|
||||
}
|
||||
case 'getRecords': {
|
||||
if (opts['target-table'])
|
||||
config.tableId = opts['target-table'];
|
||||
if (opts.query)
|
||||
config.query = JSON.parse(opts.query);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
exports.buildActionConfig = buildActionConfig;
|
||||
Reference in New Issue
Block a user