chore: standalone repo bootstrap after split from inkreach-teable monorepo

- untrack dist/, add .gitignore, add pnpm-lock.yaml
- inline tsconfig.base.json options (no monorepo extends)
- install.sh: CLI now at repo root; install skill to ~/.zcode/skills (user scope)
- fix stale paths in skill docs; 7 actions in help text; README
This commit is contained in:
yeuimu
2026-09-23 04:01:22 +00:00
parent a551fa5355
commit 2a186c9acc
23 changed files with 1174 additions and 3502 deletions
@@ -4,7 +4,7 @@ Complete reference of all Teable field types, aliases, option shapes, and smart
> ✅ **Verified 2026-08**: aliases, string choices, compact `--fields` format, smart > ✅ **Verified 2026-08**: aliases, string choices, compact `--fields` format, smart
> inference, and name resolution (`foreignTableName`/`linkFieldName`/`lookupFieldName`) > inference, and name resolution (`foreignTableName`/`linkFieldName`/`lookupFieldName`)
> are implemented in the CLI (`packages/inkreach-cli/src/lib/field-normalize.ts`) > are implemented in the CLI (`src/lib/field-normalize.ts`, repo root)
> and covered by unit tests (`field-normalize.spec.ts`, 20 cases). > and covered by unit tests (`field-normalize.spec.ts`, 20 cases).
--- ---
@@ -1,5 +1,10 @@
# Development & Deployment # Development & Deployment
> This guide documents the **inkreach-teable application repo**
> (`ssh://git@gitea.inkreach.cc:2222/inkreachhub/inkreach-teable.git`) — the
> deployed instance this CLI operates on — not this CLI repo. Paths like
> `apps/nestjs-backend/` refer to that repo's checkout.
How to build, restart, and debug this inkreach-teable instance. Covers the How to build, restart, and debug this inkreach-teable instance. Covers the
dual-process architecture, known gotchas, and test infrastructure. For daily dual-process architecture, known gotchas, and test infrastructure. For daily
operations (tables/automation), use the `inkreach-teable` CLI instead — this guide is operations (tables/automation), use the `inkreach-teable` CLI instead — this guide is
@@ -1,12 +1,18 @@
#!/bin/bash #!/bin/bash
# Install/upgrade the inkreach-teable CLI from the local monorepo source. # Install/upgrade the inkreach-teable CLI + agent skill from this standalone repo.
# Run from this skill's directory. Builds the CLI package and links it globally. # Run from this skill's directory (or anywhere; paths resolve from this file).
# What it does:
# 1. Builds the CLI (this repo's root IS the package).
# 2. Links the `inkreach-teable` command globally.
# 3. Installs the skill to ~/.zcode/skills/inkreach-teable (user scope —
# available in every ZCode workspace, shadows same-named workspace skills).
# Re-run this script after pulling changes to refresh both the command and the skill.
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Resolve the project root: skill lives at <root>/.agents/skills/inkreach-teable/ # Resolve the repo root: skill lives at <root>/.agents/skills/inkreach-teable/
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
CLI_DIR="$PROJECT_ROOT/packages/inkreach-cli" SKILL_DIR="$REPO_ROOT/.agents/skills/inkreach-teable"
echo "=== inkreach-teable CLI installation ===" echo "=== inkreach-teable CLI installation ==="
@@ -22,16 +28,16 @@ if [ "$NODE_VERSION" -lt 18 ]; then
fi fi
echo "✓ Node.js $(node -v)" echo "✓ Node.js $(node -v)"
# Check CLI source exists # Check repo layout
if [ ! -d "$CLI_DIR" ]; then if [ ! -f "$REPO_ROOT/package.json" ] || [ ! -d "$REPO_ROOT/src" ]; then
echo "ERROR: CLI source not found at $CLI_DIR" echo "ERROR: CLI source not found at $REPO_ROOT"
echo " Ensure the inkreach-teable repo is checked out at $PROJECT_ROOT" echo " Ensure the inkreach-teable-cli repo is checked out at $REPO_ROOT"
exit 1 exit 1
fi fi
# Build # Build
echo "Building CLI from $CLI_DIR ..." echo "Building CLI from $REPO_ROOT ..."
cd "$CLI_DIR" cd "$REPO_ROOT"
if command -v pnpm &> /dev/null; then if command -v pnpm &> /dev/null; then
pnpm install --prefer-offline 2>/dev/null || true pnpm install --prefer-offline 2>/dev/null || true
pnpm build pnpm build
@@ -49,14 +55,22 @@ elif command -v npm &> /dev/null && npm link 2>/dev/null; then
LINKED=1 LINKED=1
fi fi
# Ensure a usable wrapper exists on PATH even if global bin isn't configured. # Ensure a usable wrapper exists on PATH even if global bin isn't configured.
BIN_DIR="${PNPM_GLOBAL_BIN:-$(npm bin -g 2>/dev/null):-${HOME}/.local/bin}"
mkdir -p "$HOME/.local/bin" mkdir -p "$HOME/.local/bin"
cat > "$HOME/.local/bin/inkreach-teable" <<WRAPPER cat > "$HOME/.local/bin/inkreach-teable" <<WRAPPER
#!/bin/bash #!/bin/bash
exec node "$CLI_DIR/dist/bin.js" "\$@" exec node "$REPO_ROOT/dist/bin.js" "\$@"
WRAPPER WRAPPER
chmod +x "$HOME/.local/bin/inkreach-teable" chmod +x "$HOME/.local/bin/inkreach-teable"
# Install the skill at user scope so it works in ANY workspace, including
# checkouts of the inkreach-teable application repo (which no longer bundles it).
if [ -d "$SKILL_DIR" ]; then
mkdir -p "$HOME/.zcode/skills"
rm -rf "$HOME/.zcode/skills/inkreach-teable"
cp -r "$SKILL_DIR" "$HOME/.zcode/skills/inkreach-teable"
echo "✓ skill installed to ~/.zcode/skills/inkreach-teable (user scope)"
fi
# Verify # Verify
echo "" echo ""
echo "=== Verification ===" echo "=== Verification ==="
@@ -69,5 +83,5 @@ if command -v inkreach-teable &> /dev/null; then
echo " inkreach-teable auth token create --name my-cli" echo " inkreach-teable auth token create --name my-cli"
else else
echo "⚠ 'inkreach-teable' not on PATH. Add pnpm/npm global bin to your PATH, or run directly:" echo "⚠ 'inkreach-teable' not on PATH. Add pnpm/npm global bin to your PATH, or run directly:"
echo " node $CLI_DIR/dist/bin.js <command>" echo " node $REPO_ROOT/dist/bin.js <command>"
fi fi
+3
View File
@@ -0,0 +1,3 @@
node_modules/
dist/
*.tsbuildinfo
+50
View File
@@ -0,0 +1,50 @@
# inkreach-teable-cli
操作 [inkreach-teable](ssh://git@gitea.inkreach.cc:2222/inkreachhub/inkreach-teable.git)
实例(Teable fork + 自建自动化引擎)的独立工具项目,包含两部分:
| 内容 | 位置 | 说明 |
|---|---|---|
| **CLI**(命令 `inkreach-teable`) | 仓库根(`src/`,包即仓库) | 表、字段、记录、视图、自动化(8 触发器 + 7 动作)、密钥、执行历史、插件、订阅等全部 REST API 封装 |
| **Agent Skill** | `.agents/skills/inkreach-teable/` | ZCode/Claude 技能:模块地图、路由规则、API 参考、实例运维指南 |
设计文档见 [`docs/cli-skill-design.md`](docs/cli-skill-design.md)。
> ⚠️ 官方 `teable` CLI 与本实例的自动化契约不兼容(`conditions` 而非 `filter`、
> `fieldIds` 而非 `watchFieldIds` 等),务必使用 `inkreach-teable`。
## 安装
```bash
git clone ssh://git@gitea.inkreach.cc:2222/inkreachhub/inkreach-teable-cli.git
cd inkreach-teable-cli
bash .agents/skills/inkreach-teable/scripts/install.sh
```
安装脚本会:构建 CLI → 全局链接 `inkreach-teable` 命令 → 把 skill 安装到
`~/.zcode/skills/inkreach-teable`(用户级,任意工作区可用)。更新后重跑一次即可。
## 配置
```bash
inkreach-teable auth login --endpoint http://localhost:3003 --token <PAT> --base-id bseXXX
inkreach-teable auth status
```
PAT 从 Web UI(Settings → Personal Access Tokens)或 `auth token create` 获取。
## 开发
```bash
pnpm install
pnpm build # tsc → dist/
pnpm test # vitest 单测(field-normalize / field-display / resources)
pnpm typecheck
```
## 来源
2026-09-23 从 inkreach-teable monorepo 拆分独立(原路径 `packages/inkreach-cli/` +
`.agents/skills/inkreach-teable/`),git 历史完整保留。应用本体的开发/部署文档见
skill 内 [development.md](.agents/skills/inkreach-teable/guides/development.md)(描述的是
inkreach-teable 应用仓库,不是本仓库)。
Vendored
-46
View File
@@ -1,46 +0,0 @@
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* inkreach-teable — CLI for operating inkreach-teable.
*
* Entry point. Sets up the global commander program and mounts command groups.
*/
const commander_1 = require("commander");
const package_json_1 = require("../package.json");
const plugin_1 = require("./commands/plugin");
const auth_1 = require("./commands/auth");
const workflow_1 = require("./commands/workflow");
const resources_1 = require("./commands/resources");
const extras_1 = require("./commands/extras");
const subscription_1 = require("./commands/subscription");
const program = new commander_1.Command();
program
.name('inkreach-teable')
.description('Operate inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 6 actions).\n' +
'Use --json for machine-readable output (agent parsing).')
.version(`inkreach-teable ${package_json_1.version}`, '-v, --version');
// Global options (highest precedence over config file + env).
program
.option('--endpoint <url>', 'Backend URL (default http://localhost:3000)')
.option('--token <token>', 'Personal Access Token')
.option('--base-id <id>', 'Default base id')
.option('--json', 'Output raw JSON for agent parsing')
.option('--debug', 'Print requests/responses to stderr (verbose)');
// Command groups
program.addCommand((0, auth_1.authCommand)());
program.addCommand((0, plugin_1.pluginCommand)());
for (const c of (0, resources_1.baseResourceCommands)())
program.addCommand(c);
program.addCommand((0, workflow_1.workflowCommand)());
program.addCommand((0, workflow_1.triggerCommand)());
program.addCommand((0, workflow_1.actionCommand)());
program.addCommand((0, subscription_1.subscriptionCommand)());
program.addCommand((0, extras_1.secretCommand)());
program.addCommand((0, extras_1.runsCommand)());
program.addCommand((0, extras_1.fireCommand)());
program.addCommand((0, extras_1.sqlCommand)());
program.parseAsync(process.argv).catch((err) => {
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
process.exit(1);
});
-139
View File
@@ -1,139 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.authCommand = void 0;
/**
* `inkreach-teable 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) => {
// commander stores --kebab-case flags camelCased (--base-ids ->
// opts.baseIds, --full-access -> opts.fullAccess); accept both.
const opts = o;
const scopes = opts.scopes.split(',').map((s) => s.trim()).filter(Boolean);
const rawBaseIds = opts['base-ids'] ?? opts.baseIds;
const baseIds = rawBaseIds
? rawBaseIds.split(',').map((s) => s.trim())
: undefined;
const fullAccess = Boolean(opts['full-access'] ?? opts.fullAccess);
const created = (await ctx.client.createAccessToken({
name: opts.name,
scopes,
baseIds,
hasFullAccess: fullAccess,
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-teable 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;
-288
View File
@@ -1,288 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sqlCommand = exports.fireCommand = exports.runsCommand = exports.secretCommand = void 0;
/**
* `inkreach-teable secret` — manage workflow automation secrets.
* `inkreach-teable runs` — execution history.
* `inkreach-teable fire` — manual trigger testing (button-click / webhook).
* `inkreach-teable 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;
-273
View File
@@ -1,273 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.pluginCommand = void 0;
/**
* `inkreach-teable plugin` — plugin management commands.
*/
const commander_1 = require("commander");
const ctx_1 = require("../lib/ctx");
const output_1 = require("../lib/output");
const request_1 = require("../lib/request");
void ctx_1.requireBaseId;
// ---- developer whitelist subcommands (Feature 4) ----
function addDevCommands(cmd) {
const dev = cmd.command('dev').description('Manage plugin developer whitelist (admin)');
dev
.command('list')
.description('List all whitelisted developers')
.action((0, ctx_1.run)(async (ctx) => {
const list = (await ctx.client.listDevelopers());
if (ctx.json)
(0, output_1.printJson)(list);
else if (!list.length)
process.stdout.write('(no developers)\n');
else
(0, output_1.printTable)([
{ key: 'userId', header: 'USER_ID' },
{ key: 'email', header: 'EMAIL' },
{ key: 'createdBy', header: 'ADDED_BY' },
], list);
}));
dev
.command('allow')
.description('Add a user to the developer whitelist (userId or email)')
.argument('<userIdOrEmail>', 'User ID or email')
.action((0, ctx_1.run)(async (ctx, args) => {
const userIdOrEmail = args[0];
await ctx.client.addDeveloper(userIdOrEmail);
process.stdout.write(`✓ Developer added: ${userIdOrEmail}\n`);
}));
dev
.command('revoke')
.description('Remove a user from the developer whitelist (userId or email)')
.argument('<userIdOrEmail>', 'User ID or email')
.action((0, ctx_1.run)(async (ctx, args) => {
const userIdOrEmail = args[0];
await ctx.client.removeDeveloper(userIdOrEmail);
process.stdout.write(`✓ Developer revoked: ${userIdOrEmail}\n`);
}));
}
// ---- install / uninstall subcommands (Feature 1) ----
function addInstallCommands(cmd) {
cmd
.command('install')
.description('Install a plugin into the current base (creates a node)')
.requiredOption('--plugin <id>', 'Plugin ID')
.requiredOption('--name <name>', 'Display name for this node')
.requiredOption('--position <pos>', 'Mount position: base | panel | view | dashboard | contextMenu', 'base')
.option('--storage <json>', 'Initial storage JSON passed to the plugin')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
const body = {
pluginId: opts.plugin,
name: opts.name,
position: opts.position,
};
if (opts.storage)
body.storage = opts.storage;
const result = (await ctx.client.createInstall(ctx.baseId, body));
if (ctx.json)
(0, output_1.printJson)(result);
else
process.stdout.write(`✓ Plugin installed: ${result.id} (${opts.name} → ${opts.position})\n`);
}));
cmd
.command('uninstall')
.description('Remove a plugin install by its install ID')
.requiredOption('--install <id>', 'Install ID')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
await ctx.client.deleteInstall(ctx.baseId, opts.install);
process.stdout.write(`✓ Plugin install removed: ${opts.install}\n`);
}));
cmd
.command('installs')
.description('List plugin installs for the current base')
.action((0, ctx_1.run)(async (ctx) => {
const list = (await ctx.client.listInstalls(ctx.baseId));
if (ctx.json)
(0, output_1.printJson)(list);
else if (!list.length)
process.stdout.write('(no installs)\n');
else
(0, output_1.printTable)([
{ key: 'id', header: 'INSTALL_ID' },
{ key: 'pluginId', header: 'PLUGIN' },
{ key: 'name', header: 'NAME' },
{ key: 'position', header: 'POSITION' },
], list);
}));
}
function pluginCommand() {
const cmd = new commander_1.Command('plugin');
cmd.description('Manage plugins (list, register, config, create, install, dev)');
cmd
.command('list')
.description('List registered plugins')
.action((0, ctx_1.run)(async (ctx) => {
try {
const list = (await ctx.client.getPlugins());
if (ctx.json) {
(0, output_1.printJson)(list);
}
else {
(0, output_1.printTable)([
{ key: 'id', header: 'ID' },
{ key: 'name', header: 'NAME' },
{ key: 'status', header: 'STATUS' },
{ key: 'runtimeType', header: 'RUNTIME' },
], list.map((p) => ({
id: p.id,
name: p.name,
status: p.status,
runtimeType: p.runtimeType ?? 'url',
})));
}
}
catch (e) {
if (e instanceof request_1.ApiError && e.status === 401) {
process.stderr.write('Unauthorized — log in via "auth login" or provide --token\n');
process.exit(1);
}
throw e;
}
}));
cmd
.command('register')
.description('Register a plugin by URL')
.requiredOption('--url <url>', 'Plugin URL')
.requiredOption('--name <name>', 'Plugin name')
.option('--description <desc>', 'Plugin description')
.option('--logo <url>', 'Logo URL')
.option('--positions <json>', 'Positions array, e.g. \'["panel"]\'', '["panel"]')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
const positions = JSON.parse(opts.positions);
const created = await ctx.client.registerPlugin({
name: opts.name,
url: opts.url,
description: opts.description,
logo: opts.logo ?? '',
positions,
status: 'published',
});
if (ctx.json)
(0, output_1.printJson)(created);
else
process.stdout.write(`✓ Plugin registered: ${created.id}\n`);
}));
const config = cmd.command('config').description('Manage per-table plugin enablement');
config
.command('list')
.description('List plugin configs for the base')
.action((0, ctx_1.run)(async (ctx) => {
const list = await ctx.client.listPluginConfigs(ctx.baseId);
if (ctx.json)
(0, output_1.printJson)(list);
else {
(0, output_1.printTable)([
{ key: 'pluginId', header: 'PLUGIN' },
{ key: 'tableId', header: 'TABLE' },
{ key: 'enabled', header: 'ON' },
{ key: 'runtimeForm', header: 'FORM' },
{ key: 'writeEnabled', header: 'WRITE' },
], list.map((r) => ({
pluginId: r.pluginId,
tableId: r.tableId || '-',
enabled: r.enabled,
runtimeForm: r.runtimeForm,
writeEnabled: r.writeEnabled,
})));
}
}));
config
.command('enable')
.description('Enable a plugin for a table (or base-wide if no --table)')
.requiredOption('--plugin <id>', 'Plugin ID')
.option('--table <id>', 'Table ID (omit for base-wide)')
.option('--write', 'Allow write-back')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
const result = await ctx.client.upsertPluginConfig(ctx.baseId, opts.plugin, {
enabled: true,
runtimeForm: 'panel',
writeEnabled: !!opts.write,
tableIds: opts.table ? [opts.table] : [],
});
if (ctx.json)
(0, output_1.printJson)(result);
else
process.stdout.write(`✓ Plugin ${opts.plugin} enabled${opts.table ? ` for table ${opts.table}` : ' base-wide'}\n`);
}));
config
.command('disable')
.description('Disable a plugin for a table (or base-wide if no --table)')
.requiredOption('--plugin <id>', 'Plugin ID')
.option('--table <id>', 'Table ID')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
await ctx.client.upsertPluginConfig(ctx.baseId, opts.plugin, {
enabled: false,
runtimeForm: 'panel',
writeEnabled: false,
tableIds: opts.table ? [opts.table] : [],
});
process.stdout.write(`✓ Plugin ${opts.plugin} disabled${opts.table ? ` for table ${opts.table}` : ' base-wide'}\n`);
}));
// ---- CSP apiOrigins management ----
const csp = cmd.command('csp').description('Manage plugin CSP apiOrigins');
csp
.command('list')
.description('Show the union of all plugins\' apiOrigins (effective CSP connect-src extras)')
.action((0, ctx_1.run)(async (ctx) => {
const { origins } = await ctx.client.getCspOrigins();
if (ctx.json)
(0, output_1.printJson)(origins);
else if (!origins.length)
process.stdout.write('(no apiOrigins declared by any plugin)\n');
else
origins.forEach((o) => process.stdout.write(`${o}\n`));
}));
csp
.command('set <pluginId>')
.description('Set the external API origins a plugin may fetch (bundle plugins included)')
.requiredOption('--origins <json>', 'JSON array of origins, e.g. \'["http://192.168.124.92:8001"]\'')
.option('--clear', 'Remove all origins (pass "[]" works too)')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const opts = o;
const pluginId = args[0];
let origins;
try {
const parsed = JSON.parse(opts.origins);
if (!Array.isArray(parsed) || parsed.some((x) => typeof x !== 'string')) {
throw new Error('not a string array');
}
origins = parsed;
}
catch {
process.stderr.write(`✗ --origins must be a JSON string array, got: ${opts.origins}\n`);
process.exitCode = 1;
return;
}
if (opts.clear)
origins = [];
// updatePlugin requires name + positions (full-vo update): fetch current values.
const cur = await ctx.client.getPlugin(pluginId);
await ctx.client.updatePlugin(pluginId, {
name: cur.name,
positions: cur.positions,
description: cur.description,
detailDesc: cur.detailDesc,
url: cur.url,
logo: cur.logo,
helpUrl: cur.helpUrl,
apiOrigins: origins,
});
process.stdout.write(`✓ apiOrigins for ${pluginId} set to: ${JSON.stringify(origins)}\n`);
}));
// Wire feature subcommands
addDevCommands(cmd);
addInstallCommands(cmd);
return cmd;
}
exports.pluginCommand = pluginCommand;
-739
View File
@@ -1,739 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.presetConfigFromOptions = exports.baseResourceCommands = void 0;
/**
* `inkreach-teable 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");
const client_1 = require("../lib/client");
const field_normalize_1 = require("../lib/field-normalize");
const field_display_1 = require("../lib/field-display");
/**
* Fetch a field by ID or display name. A direct ID hit needs no extra call;
* a 400/404 miss (the API rejects non-ID paths) falls back to resolving the
* arg against the table's field list. Unresolvable input rethrows the
* original error so the user sees the underlying cause.
*/
async function getFieldFlexible(client, tableId, fieldIdOrName) {
try {
return (await client.getField(tableId, fieldIdOrName));
}
catch (err) {
if (err instanceof client_1.ApiError && (err.status === 400 || err.status === 404)) {
const list = (await client.listFields(tableId));
const hit = (0, field_normalize_1.findFieldByNameOrId)(list, fieldIdOrName);
if (hit)
return (await client.getField(tableId, hit.id));
}
throw err;
}
}
/** fieldId → display-name map of a table, to humanize visibilityFilter references. */
async function fieldNameMap(client, tableId) {
const list = (await client.listFields(tableId));
return Object.fromEntries(list.map((f) => [f.id, f.name]));
}
function baseResourceCommands() {
return [baseCmd(), tableCmd(), fieldCmd(), recordCmd(), viewCmd(), presetCmd()];
}
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 = (0, field_normalize_1.normalizeFieldRoArray)(JSON.parse(opts.fields));
// Resolve foreignTableName references against existing tables in the base.
const fields = body.fields;
const tables = await ctx.client.listTables(baseId).catch(() => []);
for (const f of fields) {
const options = f?.options;
if (options && typeof options.foreignTableName === 'string' && !options.foreignTableId) {
const t = (0, field_normalize_1.findTableByNameOrId)(tables || [], options.foreignTableName);
if (t)
options.foreignTableId = t.id;
}
}
}
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')
.option('--with-options', 'Add an OPTIONS summary column and expand select choices with their visibility conditions')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const opts = o;
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;
}
if (opts.withOptions) {
const nameMap = Object.fromEntries(list.map((f) => [String(f.id), String(f.name)]));
(0, output_1.printTable)([
{ key: 'id', header: 'ID' },
{ key: 'name', header: 'NAME' },
{ key: 'type', header: 'TYPE' },
{ key: 'options', header: 'OPTIONS' },
], list.map((f) => ({
id: (0, output_1.truncate)(f.id),
name: (0, output_1.truncate)(f.name, 30),
type: f.type,
options: (0, field_display_1.summarizeFieldOptions)(f),
})));
const selectFields = list.filter((f) => Array.isArray(f.options?.choices));
for (const f of selectFields) {
const rows = (0, field_display_1.choiceRows)(f, nameMap);
const conditional = rows.filter((r) => r.when !== '(always)').length;
process.stdout.write(`\n◇ ${f.name} (${f.id}) — ${rows.length} choices · ${conditional} conditional\n`);
for (const r of rows) {
const color = r.color ? ` [${r.color}]` : '';
process.stdout.write(` ${r.name}${color}\n`);
if (r.when !== '(always)') {
process.stdout.write(` when: ${r.when}\n`);
}
}
}
}
else {
(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 <tableId> <fieldIdOrName>')
.description('Get field details by id or name (full options; select choices rendered with visibility conditions)')
.action((0, ctx_1.run)(async (ctx, args) => {
const [tableId, fieldIdOrName] = args;
const field = await getFieldFlexible(ctx.client, tableId, fieldIdOrName);
if (ctx.json)
(0, output_1.printJson)(field);
else {
const choices = field.options?.choices;
const names = Array.isArray(choices)
? await fieldNameMap(ctx.client, tableId)
: undefined;
process.stdout.write((0, field_display_1.formatFieldDetail)(field, names) + '\n');
}
}));
cmd
.command('create')
.description('Create a new field')
.requiredOption('--table-id <id>', 'Table id')
.requiredOption('--name <name>', 'Field name')
.option('--type <type>', 'Field type — full value (singleLineText, singleSelect, link…) or alias (text, sel, num…). Omitted: inferred from --options shape / --name')
.option('--options <json>', 'Field options JSON (type-specific; select choices accept plain strings)')
.option('--description <d>', 'Field description')
.option('--lookup-options <json>', 'Lookup options JSON (names like linkFieldName/lookupFieldName are resolved to IDs)')
.option('--is-lookup', 'Mark the field as a lookup field')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
const body = { name: opts.name };
if (opts.options)
body.options = JSON.parse(opts.options);
if (opts.description)
body.description = opts.description;
if (opts.lookupOptions)
body.lookupOptions = JSON.parse(opts.lookupOptions);
if (opts.isLookup)
body.isLookup = true;
body.type = opts.type
? (0, field_normalize_1.resolveFieldType)(opts.type)
: (0, field_normalize_1.inferFieldType)(opts.name, body.options);
(0, field_normalize_1.normalizeFieldRo)(body);
const errors = await (0, field_normalize_1.resolveFieldReferences)(body, {
listTables: (baseId) => ctx.client.listTables(baseId),
listFields: (tableId) => ctx.client.listFields(tableId),
baseId: ctx.baseId,
tableId: opts.tableId,
});
if (errors.length)
throw new Error(errors.join('; '));
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 <tableId> <fieldIdOrName>')
.description('Update a field (by id or name). --name renames (PATCH); --options rewrites options via the convert API (the update API does not accept options)')
.option('--name <name>', 'New field name')
.option('--options <json>', 'New field options JSON (written via convert API)')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const [tableId, fieldIdOrName] = args;
const opts = o;
if (!opts.name && !opts.options) {
throw new Error('Nothing to update: pass --name and/or --options');
}
if (opts.name && !opts.options) {
const current = (await getFieldFlexible(ctx.client, tableId, fieldIdOrName));
const field = await ctx.client.updateField(tableId, current.id, { name: opts.name });
if (ctx.json)
(0, output_1.printJson)(field);
else
process.stdout.write(`✓ Field ${current.id} renamed\n`);
return;
}
// Options (choices / visibilityFilter / ...) must go through convert.
// Fetch the current field to reuse its type; merge name if given.
const current = (await getFieldFlexible(ctx.client, tableId, fieldIdOrName));
const body = {
type: current.type,
options: JSON.parse(opts.options),
};
if (opts.name)
body.name = opts.name;
(0, field_normalize_1.normalizeFieldRo)(body);
const field = await ctx.client.convertField(tableId, current.id, body);
if (ctx.json)
(0, output_1.printJson)(field);
else
process.stdout.write(`✓ Field ${current.id} options updated (via convert)\n`);
}));
cmd
.command('visible-choices <tableId> <recordId> <fieldIdOrName>')
.description('Evaluate select-choice visibility for a record (respects choice visibilityFilter dependencies)')
.action((0, ctx_1.run)(async (ctx, args) => {
const [tableId, recordId, fieldIdOrName] = args;
const field = await getFieldFlexible(ctx.client, tableId, fieldIdOrName);
const res = await ctx.client.getVisibleChoices(tableId, recordId, String(field.id));
if (ctx.json)
(0, output_1.printJson)(res);
else {
(0, output_1.printTable)([
{ key: 'id', header: 'ID' },
{ key: 'name', header: 'NAME' },
{ key: 'color', header: 'COLOR' },
], res.visibleChoices.map((c) => ({ ...c, color: c.color ?? '' })));
}
}));
cmd
.command('delete <tableId> <fieldId>')
.description('Delete a field')
.action((0, ctx_1.run)(async (ctx, args) => {
const [tableId, fieldId] = args;
await ctx.client.deleteField(tableId, 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 result = await ctx.client.deleteRecords(opts.tableId, ids);
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;
}
// ===================== view preset =====================
/** Parse a --filter/--sort/... JSON option; empty/absent → undefined. */
function parseJsonOption(value, optionName) {
if (value == null || value === '')
return undefined;
try {
return JSON.parse(value);
}
catch (err) {
throw new Error(`Invalid JSON for ${optionName}: ${err.message}`);
}
}
/** Collect preset config keys from options; absent keys stay out of the config. */
function presetConfigFromOptions(opts) {
const config = {};
const entries = [
['filter', '--filter'],
['sort', '--sort'],
['group', '--group'],
['options', '--options'],
['columnMeta', '--column-meta'],
];
for (const [key, optionName] of entries) {
const parsed = parseJsonOption(opts[key], optionName);
if (parsed !== undefined)
config[key] = parsed;
}
return config;
}
exports.presetConfigFromOptions = presetConfigFromOptions;
function presetCmd() {
const cmd = new commander_1.Command('preset');
cmd.description('Manage preset view configurations (named filter/sort/group bundles, shared with all collaborators)');
cmd
.command('list <tableId>')
.description('List view presets of a table (pass --view-id to scope to one view)')
.option('--view-id <id>', 'Only presets bound to this view')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const tableId = args[0];
const opts = o;
const list = (await ctx.client.listViewPresets(tableId, opts.viewId));
if (ctx.json)
(0, output_1.printJson)(list);
else {
if (!list.length) {
process.stdout.write('No view presets found.\n');
return;
}
(0, output_1.printTable)([
{ key: 'id', header: 'ID' },
{ key: 'name', header: 'NAME' },
{ key: 'config', header: 'CONFIG' },
], list.map((p) => {
const config = (p.config ?? {});
const keys = Object.keys(config).filter((k) => config[k] != null);
return {
id: (0, output_1.truncate)(p.id),
name: (0, output_1.truncate)(p.name, 30),
config: keys.join(',') || '(empty)',
};
}));
}
}));
cmd
.command('create')
.description('Create a view preset bound to one view (keys not passed stay unset — they keep the view values on apply)')
.requiredOption('--table-id <id>', 'Table id')
.requiredOption('--view-id <id>', 'View id the preset belongs to')
.requiredOption('--name <name>', 'Preset name')
.option('--description <text>', 'Preset description')
.option('--filter <json>', 'Filter config, e.g. \'{"conjunction":"and","filterSet":[{"fieldId":"fldX","operator":"is","value":"Pending"}]}\'')
.option('--sort <json>', 'Sort config, e.g. \'{"sortObjs":[{"fieldId":"fldX","order":"asc"}]}\'')
.option('--group <json>', 'Group config (JSON array of group items)')
.option('--options <json>', 'View options (JSON object)')
.option('--column-meta <json>', 'Column meta (field visibility/order, JSON object)')
.action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
const body = {
name: opts.name,
viewId: opts.viewId,
config: presetConfigFromOptions(opts),
};
if (opts.description)
body.description = opts.description;
const preset = (await ctx.client.createViewPreset(opts.tableId, body));
if (ctx.json)
(0, output_1.printJson)(preset);
else {
(0, output_1.printKv)([
['id', preset.id],
['name', preset.name],
['order', preset.order],
]);
}
}));
cmd
.command('update <presetId>')
.description('Update a view preset (rename and/or replace config keys)')
.requiredOption('--table-id <id>', 'Table id')
.option('--name <name>', 'New preset name')
.option('--description <text>', 'New preset description')
.option('--filter <json>', 'Replace filter config (JSON)')
.option('--sort <json>', 'Replace sort config (JSON)')
.option('--group <json>', 'Replace group config (JSON)')
.option('--options <json>', 'Replace view options (JSON object)')
.option('--column-meta <json>', 'Replace column meta (JSON object)')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const presetId = args[0];
const opts = o;
const body = {};
if (opts.name)
body.name = opts.name;
if (opts.description !== undefined)
body.description = opts.description;
const config = presetConfigFromOptions(opts);
if (Object.keys(config).length)
body.config = config;
await ctx.client.updateViewPreset(opts.tableId, presetId, body);
if (ctx.json)
(0, output_1.printJson)({ ok: true, id: presetId });
else
process.stdout.write(`✓ Preset ${presetId} updated\n`);
}));
cmd
.command('delete <presetId>')
.description('Delete a view preset')
.requiredOption('--table-id <id>', 'Table id')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const presetId = args[0];
const opts = o;
await ctx.client.deleteViewPreset(opts.tableId, presetId);
if (ctx.json)
(0, output_1.printJson)({ ok: true, id: presetId });
else
process.stdout.write(`✓ Preset ${presetId} deleted\n`);
}));
return cmd;
}
-323
View File
@@ -1,323 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.subscriptionCommand = void 0;
/**
* `inkreach-teable subscription` — 订阅中心全配置项 CRUD + 事件查看 + 立即发送。
*
* 覆盖订阅中心全部配置:name/table/watchCreated/watchUpdated/fieldIds/recordIds/
* displayFieldId/attachCsv/filter(原生 IFilter JSON)/triggerOnEnter(进入条件语义)/
* sendMode/recipients/notifyInApp/scheduleType/scheduleTime/scheduleWeekday/
* scheduleInterval/isActive。
*
* filter 示例(与表格视图筛选同构):
* --filter '{"conjunction":"and","filterSet":[{"fieldId":"fldX","operator":"isExactly","value":"已完成"}]}'
* 日期筛选值结构:{"mode":"exactDate","exactDate":"2026-08-21","timeZone":"Asia/Shanghai"}
* 用户筛选值:["usrXXX"]('Me' 由后端解析为订阅创建者)。
*/
const commander_1 = require("commander");
const ctx_1 = require("../lib/ctx");
const output_1 = require("../lib/output");
function parseJsonFlag(raw, flagName) {
if (raw === undefined)
return undefined;
try {
return JSON.parse(raw);
}
catch (err) {
throw new Error(`--${flagName} is not valid JSON: ${err.message}\n got: ${raw}`);
}
}
/** 'a@x.com,b@y.com' | '["a@x.com"]' → string[] */
function parseRecipients(raw) {
if (raw === undefined)
return undefined;
const trimmed = raw.trim();
if (trimmed.startsWith('[')) {
return parseJsonFlag(raw, 'recipients');
}
return trimmed
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}
/** 从 CLI 选项构建 RO body;create 时 name/tableId 必填。 */
function buildRo(opts, forCreate) {
const body = {};
if (opts.name !== undefined)
body.name = opts.name;
if (opts.tableId !== undefined)
body.tableId = opts.tableId;
if (forCreate) {
if (!body.name)
throw new Error('--name is required for create');
if (!body.tableId)
throw new Error('--table-id is required for create');
}
if (opts.watchCreated !== undefined)
body.watchCreated = opts.watchCreated;
if (opts.watchUpdated !== undefined)
body.watchUpdated = opts.watchUpdated;
const fieldIds = parseJsonFlag(opts.fieldIds, 'field-ids');
if (fieldIds !== undefined)
body.fieldIds = fieldIds;
const recordIds = parseJsonFlag(opts.recordIds, 'record-ids');
if (recordIds !== undefined)
body.recordIds = recordIds;
if (opts.displayFieldId !== undefined) {
body.displayFieldId = opts.displayFieldId === '' ? null : opts.displayFieldId;
}
if (opts.attachCsv !== undefined)
body.attachCsv = opts.attachCsv;
const filter = parseJsonFlag(opts.filter, 'filter');
if (filter !== undefined)
body.filter = filter;
if (opts.triggerOnEnter !== undefined)
body.triggerOnEnter = opts.triggerOnEnter;
if (opts.sendMode !== undefined) {
if (opts.sendMode !== 'digest' && opts.sendMode !== 'instant') {
throw new Error(`--send-mode must be digest|instant, got: ${opts.sendMode}`);
}
body.sendMode = opts.sendMode;
}
const recipients = parseRecipients(opts.recipients);
if (recipients !== undefined)
body.recipients = recipients;
if (opts.notifyInApp !== undefined)
body.notifyInApp = opts.notifyInApp;
if (opts.scheduleType !== undefined) {
const t = opts.scheduleType;
if (!['daily', 'weekly', 'hours', 'minutes'].includes(t)) {
throw new Error(`--schedule-type must be daily|weekly|hours|minutes, got: ${t}`);
}
body.scheduleType = t;
}
if (opts.scheduleTime !== undefined)
body.scheduleTime = opts.scheduleTime;
if (opts.scheduleWeekday !== undefined) {
const n = Number(opts.scheduleWeekday);
if (!Number.isInteger(n) || n < 0 || n > 6) {
throw new Error(`--schedule-weekday must be 0-6 (0=Sunday), got: ${opts.scheduleWeekday}`);
}
body.scheduleWeekday = n;
}
if (opts.scheduleInterval !== undefined) {
const n = Number(opts.scheduleInterval);
if (!Number.isInteger(n) || n < 1) {
throw new Error(`--schedule-interval must be a positive integer, got: ${opts.scheduleInterval}`);
}
body.scheduleInterval = n;
}
if (opts.isActive !== undefined)
body.isActive = opts.isActive;
return body;
}
function addConfigOptions(cmd) {
return cmd
.option('--base-id <id>', 'Base id (overrides default)')
.option('--name <name>', 'Subscription name')
.option('--table-id <id>', 'Watched table id (tblXXX)')
.option('--no-watch-created', 'Disable watching record creation (default: on)')
.option('--no-watch-updated', 'Disable watching record updates (default: on)')
.option('--field-ids <json>', 'Watched field ids as JSON array, e.g. \'["fldA","fldB"]\' (omit = all fields)')
.option('--record-ids <json>', 'Record-level subscription: tracked record ids as JSON array (omit = all records)')
.option('--display-field-id <id>', 'Field shown as record title in emails ("" = primary field)')
.option('--attach-csv', 'Attach CSV of changes to digest emails (needs export permission)')
.option('--no-attach-csv', 'Do not attach CSV')
.option('--filter <json>', 'Native IFilter JSON (same shape as view filters); null/omit = all records')
.option('--trigger-on-enter', 'Only notify on the transition INTO the filter condition (requires --filter)')
.option('--no-trigger-on-enter', 'Notify on every matching change (state semantics)')
.option('--send-mode <mode>', 'digest | instant')
.option('--recipients <list>', 'Recipient emails: comma-separated or JSON array (default = creator)')
.option('--notify-in-app', 'Also send in-app notifications (default)')
.option('--no-notify-in-app', 'Disable in-app notifications')
.option('--schedule-type <type>', 'daily | weekly | hours | minutes')
.option('--schedule-time <hh:mm>', 'Send time for daily/weekly, e.g. 09:00')
.option('--schedule-weekday <n>', '0-6 (0=Sunday), for weekly')
.option('--schedule-interval <n>', 'Interval for hours/minutes')
.option('--active', 'Subscription active (default)')
.option('--inactive', 'Pause the subscription');
}
function sendModeBadge(s) {
return s.sendMode === 'instant' ? 'instant' : 'digest';
}
function subscriptionCommand() {
const cmd = new commander_1.Command('subscription');
cmd.description('Manage the subscription center (record subscriptions) — full config support');
cmd
.command('list')
.description('List subscriptions in the base')
.option('--table-id <id>', 'Filter by table')
.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.listSubscriptions(baseId, opts.tableId);
if (ctx.json)
(0, output_1.printJson)(list);
else {
if (!list.length) {
process.stdout.write('No subscriptions found.\n');
return;
}
(0, output_1.printTable)([
{ key: 'id', header: 'ID' },
{ key: 'name', header: 'NAME' },
{ key: 'tableId', header: 'TABLE' },
{ key: 'mode', header: 'MODE' },
{ key: 'onEnter', header: 'ON-ENTER' },
{ key: 'schedule', header: 'SCHEDULE' },
{ key: 'pending', header: 'PENDING' },
{ key: 'active', header: 'ACTIVE' },
], list.map((s) => ({
id: s.id,
name: (0, output_1.truncate)(s.name, 24),
tableId: s.tableId,
mode: sendModeBadge(s),
onEnter: s.triggerOnEnter ? '✓' : '',
schedule: s.sendMode === 'instant' ? '-' : s.scheduleType,
pending: s.pendingCount ?? 0,
active: s.isActive ? '✓' : '',
})));
process.stdout.write(`\n${list.length} subscription(s). Use 'subscription get <id>' for details.\n`);
}
}));
const getCmd = cmd.command('get <subscriptionId>').description('Show full subscription config');
getCmd.option('--base-id <id>', 'Base id (overrides default)').action((0, ctx_1.run)(async (ctx, args, o) => {
const id = args[0];
const opts = o;
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
const list = await ctx.client.listSubscriptions(baseId);
const sub = list.find((s) => s.id === id);
if (!sub)
throw new Error(`Subscription ${id} not found in base ${baseId}`);
if (ctx.json)
(0, output_1.printJson)(sub);
else {
(0, output_1.printKv)([
['id', sub.id],
['name', sub.name],
['tableId', sub.tableId],
['watchCreated', String(sub.watchCreated)],
['watchUpdated', String(sub.watchUpdated)],
['fieldIds', sub.fieldIds?.length ? sub.fieldIds.join(', ') : '(all fields)'],
[
'recordIds',
sub.recordIds?.length ? `${sub.recordIds.length} record(s)` : '(all records)',
],
['displayFieldId', sub.displayFieldId ?? '(primary)'],
['attachCsv', String(sub.attachCsv)],
['filter', sub.filter ? JSON.stringify(sub.filter) : '(none)'],
['triggerOnEnter', String(sub.triggerOnEnter)],
['sendMode', sendModeBadge(sub)],
['recipients', (sub.recipients ?? []).join(', ')],
['notifyInApp', String(sub.notifyInApp)],
['scheduleType', sub.scheduleType],
['scheduleTime', sub.scheduleTime ?? ''],
['scheduleWeekday', sub.scheduleWeekday != null ? String(sub.scheduleWeekday) : ''],
['scheduleInterval', sub.scheduleInterval != null ? String(sub.scheduleInterval) : ''],
['isActive', String(sub.isActive)],
['pendingCount', String(sub.pendingCount ?? 0)],
['lastSentTime', sub.lastSentTime ?? ''],
]);
}
}));
const createCmd = cmd
.command('create')
.description('Create a subscription (--name and --table-id required)');
addConfigOptions(createCmd).action((0, ctx_1.run)(async (ctx, _args, o) => {
const opts = o;
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
const body = buildRo(opts, true);
const vo = await ctx.client.createSubscription(baseId, body);
if (ctx.json)
(0, output_1.printJson)(vo);
else
(0, output_1.printKv)([
['id', vo.id],
['name', vo.name],
['created', '✓'],
]);
}));
const updateCmd = cmd
.command('update <subscriptionId>')
.description('Update a subscription (only provided options change)');
addConfigOptions(updateCmd).action((0, ctx_1.run)(async (ctx, args, o) => {
const id = args[0];
const opts = o;
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
const body = buildRo(opts, false);
if (Object.keys(body).length === 0) {
throw new Error('No options provided — nothing to update');
}
const vo = await ctx.client.updateSubscription(baseId, id, body);
if (ctx.json)
(0, output_1.printJson)(vo);
else
(0, output_1.printKv)([
['id', vo.id],
['updated', '✓'],
]);
}));
cmd
.command('delete <subscriptionId>')
.description('Delete a subscription')
.option('--base-id <id>', 'Base id (overrides default)')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const id = args[0];
const opts = o;
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
await ctx.client.deleteSubscription(baseId, id);
if (ctx.json)
(0, output_1.printJson)({ id });
else
process.stdout.write(`Deleted ${id}\n`);
}));
cmd
.command('events <subscriptionId>')
.description('List captured events (pending + sent) of a subscription')
.option('--base-id <id>', 'Base id (overrides default)')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const id = args[0];
const opts = o;
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
const events = (await ctx.client.getSubscriptionEvents(baseId, id));
if (ctx.json)
(0, output_1.printJson)(events);
else {
if (!events.length) {
process.stdout.write('No events captured.\n');
return;
}
(0, output_1.printTable)([
{ key: 'recordId', header: 'RECORD' },
{ key: 'changeType', header: 'CHANGE' },
{ key: 'createdTime', header: 'CAPTURED' },
{ key: 'sentTime', header: 'SENT' },
], events.map((e) => ({
recordId: e.recordId,
changeType: e.changeType,
createdTime: e.createdTime,
sentTime: e.sentTime ?? '(pending)',
})));
}
}));
cmd
.command('send <subscriptionId>')
.description('Send pending digest events immediately (flush now)')
.option('--base-id <id>', 'Base id (overrides default)')
.action((0, ctx_1.run)(async (ctx, args, o) => {
const id = args[0];
const opts = o;
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
const result = await ctx.client.sendSubscriptionNow(baseId, id);
if (ctx.json)
(0, output_1.printJson)(result ?? { id });
else
(0, output_1.printKv)([
['id', id],
['sent', '✓'],
]);
}));
return cmd;
}
exports.subscriptionCommand = subscriptionCommand;
-636
View File
@@ -1,636 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildActionConfig = exports.actionCommand = exports.buildTriggerConfig = exports.triggerCommand = exports.workflowCommand = void 0;
/**
* `inkreach-teable workflow` — workflow CRUD + lifecycle.
* `inkreach-teable trigger` — set the trigger node's type & config.
* `inkreach-teable 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-teable 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, legacy single). Prefer --form-ids.')
.option('--form-ids <csv>', 'Comma-separated form view ids (formSubmitted, multi-select)')
.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,
formIds: opts.formIds,
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-teable 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;
// Prefer --form-ids (csv) which populates formIds:string[]. Fall back to
// legacy --form-id (single string) for backward compatibility.
if (opts.formIds) {
config.formIds = opts.formIds
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
else 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',
'sendNotification',
];
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('--dependencies <json>', 'npm packages JSON array for script, e.g. \'[{"name":"axios","version":"1.6.0"}]\' (script)')
.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('--to-user <id>', 'Recipient user id (sendNotification)')
.option('--title <s>', 'Notification title (sendNotification)')
.option('--message <s>', 'Notification message (sendNotification)')
.option('--severity <s>', 'Notification severity info|warning|critical (sendNotification)')
.option('--path <s>', 'Click-through path, e.g. /base/xx/table/yy (sendNotification)')
.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-teable 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('--timeout <ms>', 'Script timeout ms (script)')
.option('--dependencies <json>', 'npm packages JSON array for script, e.g. \'[{"name":"axios","version":"1.6.0"}]\' (script; replaces existing deps)')
.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 {
// Granular update: merge only the flags the user actually passed.
config = { ...config };
// commander stores --kebab-case flags camelCased (--code-file ->
// opts.codeFile); accept both forms like buildActionConfig's pick().
const codeFile = opts['code-file'] ?? opts.codeFile;
if (opts.code !== undefined || codeFile !== undefined) {
config.code = codeFile ? readFile(codeFile) : opts.code;
}
if (opts.timeout !== undefined)
config.timeout = Number(opts.timeout);
if (opts.dependencies !== undefined) {
config.dependencies = parseDependencies(opts.dependencies);
}
}
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');
}
/**
* Parse `--dependencies` into the ScriptDependency[] shape the backend expects.
* Accepts:
* - full JSON: '[{"name":"axios","version":"1.6.0"}]'
* - name list: 'axios,lodash' (version defaults to 'latest')
* - name@ver: 'axios@1.6.0,lodash@4.17.21'
* - empty '[]' or '' to clear deps
*/
function parseDependencies(raw) {
const trimmed = raw.trim();
if (!trimmed || trimmed === '[]')
return [];
// JSON form
if (trimmed[0] === '[') {
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed))
throw new Error('--dependencies JSON must be an array');
return parsed.map((d) => {
if (typeof d === 'string')
return { name: d };
if (d && typeof d === 'object' && typeof d.name === 'string') {
return { name: d.name, ...(d.version ? { version: String(d.version) } : {}) };
}
throw new Error('--dependencies entries must be strings or {name,version?} objects');
});
}
// Comma list form: axios@1.6.0,lodash
return trimmed
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((spec) => {
const at = spec.lastIndexOf('@');
if (at > 0)
return { name: spec.slice(0, at), version: spec.slice(at + 1) };
return { name: spec };
});
}
/** Build action config from granular flags. */
function buildActionConfig(opts) {
const type = opts.type;
// Helper: commander stores --kebab-case flags as camelCase in opts (e.g.
// --target-table -> opts.targetTable). Accept both forms for safety.
const pick = (k) => (opts[k] ?? opts[toCamel(k)]);
const config = {};
switch (type) {
case 'script': {
const codeFile = pick('code-file');
if (codeFile)
config.code = readFile(codeFile);
else if (opts.code)
config.code = opts.code;
if (opts.timeout)
config.timeout = Number(opts.timeout);
if (opts.dependencies)
config.dependencies = parseDependencies(opts.dependencies);
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);
const reqBody = pick('req-body');
if (reqBody)
config.body = JSON.parse(reqBody);
break;
}
case 'createRecord':
case 'updateRecord': {
const tt = pick('target-table');
if (tt)
config.tableId = tt;
if (opts.records)
config.records = JSON.parse(opts.records);
break;
}
case 'getRecords': {
const tt = pick('target-table');
if (tt)
config.tableId = tt;
if (opts.query)
config.query = JSON.parse(opts.query);
break;
}
case 'sendNotification': {
if (opts.toUser)
config.toUserId = opts.toUser;
if (opts.title)
config.title = opts.title;
if (opts.message)
config.message = opts.message;
if (opts.severity)
config.severity = opts.severity;
if (opts.path)
config.path = opts.path;
break;
}
default:
break;
}
return config;
}
exports.buildActionConfig = buildActionConfig;
function toCamel(s) {
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
-311
View File
@@ -1,311 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiError = exports.Client = void 0;
/**
* High-level API client. Each method maps to one REST endpoint of the
* inkreach-teable backend, using the SELF-BUILT contract (not official teable).
*
* Base/table/field/record/view endpoints are compatible with official teable
* open-api; workflow/automation endpoints use the project's own contract.
*/
const request_1 = require("./request");
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return request_1.ApiError; } });
class Client {
cfg;
constructor(cfg) {
this.cfg = cfg;
}
get headers() {
return this.cfg.token ? { Authorization: `Bearer ${this.cfg.token}` } : {};
}
async call(method, p, body) {
const res = await (0, request_1.request)(this.cfg.endpoint, p, { method, headers: this.headers, body });
return res.data;
}
// ---- auth / user ----
getMe() {
// GET /api/auth/user — @TokenAccess() enabled, works with PAT.
// (/api/auth/user/me is session-only and 403s for PAT.)
return this.call('GET', '/api/auth/user');
}
// ---- access tokens (PAT) ----
listAccessTokens() {
return this.call('GET', '/api/access-token');
}
createAccessToken(body) {
return this.call('POST', '/api/access-token', body);
}
deleteAccessToken(id) {
return this.call('DELETE', `/api/access-token/${id}`);
}
// ---- base ----
listBases() {
// GET /api/base/access/all — lists bases the user can access.
return this.call('GET', '/api/base/access/all');
}
getBase(baseId) {
return this.call('GET', `/api/base/${baseId}`);
}
// ---- table ----
listTables(baseId) {
return this.call('GET', `/api/base/${baseId}/table`);
}
getTable(tableId) {
return this.call('GET', `/api/table/${tableId}`);
}
createTable(baseId, body) {
return this.call('POST', `/api/base/${baseId}/table`, body);
}
updateTable(tableId, body) {
return this.call('PUT', `/api/table/${tableId}`, body);
}
deleteTable(tableId, baseId) {
const base = baseId ?? this.cfg.baseId;
return this.call('DELETE', `/api/base/${base}/table/${tableId}`);
}
// ---- field ----
listFields(tableId) {
return this.call('GET', `/api/table/${tableId}/field`);
}
getField(tableId, fieldId) {
return this.call('GET', `/api/table/${tableId}/field/${fieldId}`);
}
createField(tableId, body) {
return this.call('POST', `/api/table/${tableId}/field`, body);
}
/** Rename/describe. NOTE: the update API accepts name/description only —
* options (incl. select choice visibilityFilter) must go through convertField. */
updateField(tableId, fieldId, body) {
return this.call('PATCH', `/api/table/${tableId}/field/${fieldId}`, body);
}
/** PUT convert: the ONLY API path that persists field options (choices,
* visibilityFilter dependency config, etc.). Requires the field's current type. */
convertField(tableId, fieldId, body) {
return this.call('PUT', `/api/table/${tableId}/field/${fieldId}/convert`, body);
}
deleteField(tableId, fieldId) {
return this.call('DELETE', `/api/table/${tableId}/field/${fieldId}`);
}
/** Evaluate select-choice visibility dependency for one record+field.
* GET /api/table/:tableId/select-visibility/:recordId/:fieldId */
getVisibleChoices(tableId, recordId, fieldId) {
return this.call('GET', `/api/table/${tableId}/select-visibility/${recordId}/${fieldId}`);
}
// ---- record ----
getRecords(tableId, query) {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call('GET', `/api/table/${tableId}/record${qs}`);
}
/** Flatten search[] tuple arrays into repeated query keys (Teable expects search[]=...). */
flattenQuery(query) {
const out = {};
for (const [k, v] of Object.entries(query)) {
if (v === undefined || v === null)
continue;
if (Array.isArray(v)) {
v.forEach((item) => {
out[`${k}[]`] = typeof item === 'object' ? JSON.stringify(item) : String(item);
});
}
else if (typeof v === 'object') {
out[k] = JSON.stringify(v);
}
else if (k === 'search' && typeof v === 'string') {
// The records list API expects `search[]=value` (tuple), not a plain
// `search=value` string. https://github.com/colinhacks/zod
out['search[]'] = v;
}
else {
out[k] = String(v);
}
}
return out;
}
getRecord(recordId, query) {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call('GET', `/api/record/${recordId}${qs}`);
}
createRecord(tableId, body) {
return this.call('POST', `/api/table/${tableId}/record`, body);
}
updateRecord(tableId, body) {
return this.call('PATCH', `/api/table/${tableId}/record`, body);
}
updateRecordOne(recordId, body) {
return this.call('PATCH', `/api/record/${recordId}`, body);
}
deleteRecords(tableId, recordIds) {
const qs = recordIds.map((id) => `recordIds=${encodeURIComponent(id)}`).join('&');
return this.call('DELETE', `/api/table/${tableId}/record?${qs}`);
}
// ---- view ----
listViews(tableId) {
return this.call('GET', `/api/table/${tableId}/view`);
}
getView(viewId) {
return this.call('GET', `/api/view/${viewId}`);
}
createView(tableId, body) {
return this.call('POST', `/api/table/${tableId}/view`, body);
}
updateView(viewId, body) {
return this.call('PATCH', `/api/view/${viewId}`, body);
}
deleteView(viewId) {
return this.call('DELETE', `/api/view/${viewId}`);
}
// ---- view preset (self-built contract) ----
// Preset view configurations: VIEW-scoped named bundles of
// filter/sort/group(/options/columnMeta) shared with all collaborators.
listViewPresets(tableId, viewId) {
return this.call('GET', `/api/table/${tableId}/view-preset${viewId ? `?viewId=${encodeURIComponent(viewId)}` : ''}`);
}
createViewPreset(tableId, body) {
return this.call('POST', `/api/table/${tableId}/view-preset`, body);
}
updateViewPreset(tableId, presetId, body) {
return this.call('PUT', `/api/table/${tableId}/view-preset/${presetId}`, body);
}
deleteViewPreset(tableId, presetId) {
return this.call('DELETE', `/api/table/${tableId}/view-preset/${presetId}`);
}
// ---- workflow (self-built contract) ----
listWorkflows(baseId) {
return this.call('GET', `/api/base/${baseId}/workflow`);
}
getWorkflow(workflowId) {
return this.call('GET', `/api/base/any/workflow/${workflowId}`);
}
/**
* Get workflow by explicit baseId + workflowId. getWorkflow() above hits a
* route that resolves base from the workflow itself, but some auth/permission
* paths need the base-scoped route. Prefer this when baseId is known.
*/
getWorkflowInBase(baseId, workflowId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}`);
}
createWorkflow(baseId, body) {
return this.call('POST', `/api/base/${baseId}/workflow`, body);
}
updateWorkflow(baseId, workflowId, body) {
return this.call('PATCH', `/api/base/${baseId}/workflow/${workflowId}`, body);
}
deleteWorkflow(baseId, workflowId) {
return this.call('DELETE', `/api/base/${baseId}/workflow/${workflowId}`);
}
activateWorkflow(baseId, workflowId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/activate`);
}
deactivateWorkflow(baseId, workflowId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/deactivate`);
}
testWorkflow(baseId, workflowId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/test`);
}
/** PUT /node — whole-graph replacement (delete-all + create-all in a txn). */
replaceNodes(baseId, workflowId, body) {
return this.call('PUT', `/api/base/${baseId}/workflow/${workflowId}/node`, body);
}
// ---- secrets ----
listSecrets(baseId, workflowId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/secret`);
}
createSecret(baseId, workflowId, body) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/secret`, body);
}
updateSecret(baseId, workflowId, secretId, body) {
return this.call('PATCH', `/api/base/${baseId}/workflow/${workflowId}/secret/${secretId}`, body);
}
deleteSecret(baseId, workflowId, secretId) {
return this.call('DELETE', `/api/base/${baseId}/workflow/${workflowId}/secret/${secretId}`);
}
// ---- execution history ----
listExecutions(baseId, workflowId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/execution`);
}
getExecution(baseId, workflowId, executionId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/execution/${executionId}`);
}
rerunExecution(baseId, workflowId, executionId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/execution/${executionId}/rerun`);
}
// ---- trigger (manual fire for testing) ----
fireButtonClick(tableId, recordId, fieldId) {
return this.call('POST', `/api/table/${tableId}/record/${recordId}/${fieldId}/button-click`);
}
fireWebhook(baseId, workflowId, body) {
// NOTE: real backend route is /api/webhook/{baseId}/{workflowId} (no /base/ /workflow/ segments)
return this.call('POST', `/api/webhook/${baseId}/${workflowId}`, body);
}
// ---- SQL (read-only) ----
sql(baseId, sql) {
return this.call('POST', `/api/base/${baseId}/sql`, { sql });
}
// ---- plugin ----
getPlugins() {
return this.call('GET', '/api/plugin');
}
getPlugin(pluginId) {
return this.call('GET', `/api/plugin/${encodeURIComponent(pluginId)}`);
}
updatePlugin(pluginId, body) {
return this.call('PUT', `/api/plugin/${encodeURIComponent(pluginId)}`, body);
}
getCspOrigins() {
return this.call('GET', '/api/plugin/csp-origins');
}
registerPlugin(body) {
return this.call('POST', '/api/plugin', body);
}
listPluginConfigs(baseId) {
return this.call('GET', `/api/base/${baseId}/plugin-config`);
}
upsertPluginConfig(baseId, pluginId, body) {
return this.call('PUT', `/api/base/${baseId}/plugin-config/${pluginId}`, body);
}
// ---- plugin installs ----
listInstalls(baseId) {
return this.call('GET', `/api/base/${baseId}/plugin-installs`);
}
createInstall(baseId, body) {
return this.call('POST', `/api/base/${baseId}/plugin-installs`, body);
}
deleteInstall(baseId, installId) {
return this.call('DELETE', `/api/base/${baseId}/plugin-installs/${installId}`);
}
// ---- plugin developers ----
listDevelopers() {
return this.call('GET', '/api/plugin-developers');
}
addDeveloper(userIdOrEmail) {
// Backend reads @Body('userId') or @Body('email'). Send whichever looks right.
const isEmail = userIdOrEmail.includes('@');
const body = isEmail ? { email: userIdOrEmail } : { userId: userIdOrEmail };
return this.call('POST', '/api/plugin-developers', body);
}
removeDeveloper(userId) {
return this.call('DELETE', `/api/plugin-developers/${encodeURIComponent(userId)}`);
}
// ---- subscription center (record subscriptions) ----
listSubscriptions(baseId, tableId) {
const q = tableId ? `?tableId=${encodeURIComponent(tableId)}` : '';
return this.call('GET', `/api/base/${baseId}/subscription${q}`);
}
getSubscriptionEvents(baseId, subscriptionId) {
return this.call('GET', `/api/base/${baseId}/subscription/${subscriptionId}/event`);
}
createSubscription(baseId, body) {
return this.call('POST', `/api/base/${baseId}/subscription`, body);
}
updateSubscription(baseId, subscriptionId, body) {
return this.call('PATCH', `/api/base/${baseId}/subscription/${subscriptionId}`, body);
}
deleteSubscription(baseId, subscriptionId) {
return this.call('DELETE', `/api/base/${baseId}/subscription/${subscriptionId}`);
}
/** 立即发送(flush pending digest events) */
sendSubscriptionNow(baseId, subscriptionId) {
return this.call('POST', `/api/base/${baseId}/subscription/${subscriptionId}/send`);
}
}
exports.Client = Client;
-73
View File
@@ -1,73 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.configPath = exports.resolveConfig = exports.writeConfigFile = exports.readConfigFile = exports.DEFAULT_ENDPOINT = void 0;
/**
* Config storage for inkreach-teable CLI.
*
* Stores endpoint + token at ~/.inkreach-teable/config.json, with environment variable
* overrides INKREACH_TEABLE_ENDPOINT / INKREACH_TEABLE_TOKEN. The --endpoint / --token / --base-id
* global flags take the highest precedence (handled by resolveOptions()).
*/
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const os = __importStar(require("node:os"));
const CONFIG_DIR = path.join(os.homedir(), '.inkreach-teable');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
exports.DEFAULT_ENDPOINT = 'http://localhost:3003';
function readConfigFile() {
try {
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
return JSON.parse(raw);
}
catch {
return {};
}
}
exports.readConfigFile = readConfigFile;
function writeConfigFile(cfg) {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
const merged = { ...readConfigFile(), ...cfg };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
}
exports.writeConfigFile = writeConfigFile;
function resolveConfig(overrides) {
const file = readConfigFile();
return {
endpoint: overrides?.endpoint ||
process.env.INKREACH_TEABLE_ENDPOINT ||
file.endpoint ||
exports.DEFAULT_ENDPOINT,
token: overrides?.token || process.env.INKREACH_TEABLE_TOKEN || file.token || '',
baseId: overrides?.baseId || process.env.INKREACH_TEABLE_BASE_ID || file.baseId,
};
}
exports.resolveConfig = resolveConfig;
function configPath() {
return CONFIG_FILE;
}
exports.configPath = configPath;
-72
View File
@@ -1,72 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireBaseId = exports.run = exports.makeCtx = void 0;
const config_1 = require("./config");
const client_1 = require("./client");
const request_1 = require("./request");
/**
* Build a Ctx. `command` is the subcommand being executed (so .opts() returns
* its local options); `program` is the top-level program (for global options).
*/
function makeCtx(program, command) {
const g = program.opts();
const cfg = (0, config_1.resolveConfig)({
endpoint: g.endpoint,
token: g.token,
baseId: g.baseId,
});
return {
client: new client_1.Client(cfg),
json: !!g.json,
baseId: cfg.baseId,
command,
program,
};
}
exports.makeCtx = makeCtx;
/**
* Wrap an async command action with normalized error handling.
*
* The wrapped fn receives (ctx, positionalArgs[], options). commander passes
* positional args before the trailing Command; we detect the trailing Command
* and forward everything else as positional, plus parsed options.
*/
function run(fn) {
return async function (...cmdArgs) {
// commander convention: last arg is the Command instance (the subcommand).
const command = cmdArgs[cmdArgs.length - 1];
const program = command?.parent ? findRoot(command) : command;
const positional = cmdArgs.slice(0, -1);
const options = (command?.opts() || {});
const ctx = makeCtx(program, command);
try {
await fn(ctx, positional, options);
}
catch (err) {
const { status, code, message } = (0, request_1.describeError)(err);
if (ctx.json) {
process.stdout.write(JSON.stringify({ error: { status, code, message } }) + '\n');
}
else {
process.stderr.write(`✗ ${code}${status ? ` (${status})` : ''}: ${message}\n`);
}
process.exitCode = 1;
}
};
}
exports.run = run;
function findRoot(cmd) {
let c = cmd;
while (c.parent)
c = c.parent;
return c;
}
/** Require a baseId: from flag, else ctx default, else throw with guidance. */
function requireBaseId(ctx, flagVal) {
const baseId = flagVal || ctx.baseId;
if (!baseId) {
throw new client_1.ApiError(0, 'MISSING_BASE_ID', 'No base id. Pass --base-id <id>, set a default with `inkreach-teable auth login --base-id <id>`, or export INKREACH_TEABLE_BASE_ID.');
}
return baseId;
}
exports.requireBaseId = requireBaseId;
-136
View File
@@ -1,136 +0,0 @@
"use strict";
/**
* Human-mode display helpers for field options — make select-choice
* constraints (choices, colors, per-choice visibilityFilter) readable
* without `--json` post-processing.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatFieldDetail = exports.choiceRows = exports.describeChoiceCondition = exports.summarizeFieldOptions = void 0;
const CONDITION_MAX_LEN = 90;
/** Summarize a field's options for the `field list --with-options` column. */
function summarizeFieldOptions(field) {
const options = field.options;
const choices = options?.choices;
if (Array.isArray(choices) && choices.length) {
const conditional = choices.filter((c) => c && typeof c === 'object' && c.visibilityFilter).length;
return conditional
? `${choices.length} choices · ${conditional} conditional`
: `${choices.length} choices`;
}
if (!options || typeof options !== 'object')
return '—';
const type = field.type;
if (type === 'rating') {
return [
`max=${options.max ?? 5}`,
typeof options.icon === 'string' ? `icon=${options.icon}` : '',
]
.filter(Boolean)
.join(' ');
}
const formatting = options.formatting;
if (type === 'number' && formatting) {
return `precision=${formatting.precision ?? 0}`;
}
if ((type === 'date' || type === 'createdTime' || type === 'lastModifiedTime') && formatting) {
return [formatting.date, formatting.time && formatting.time !== 'None' ? formatting.time : '']
.filter(Boolean)
.join(' ');
}
if (typeof options.expression === 'string') {
const e = options.expression;
return `expr: ${e.length > 40 ? e.slice(0, 39) + '…' : e}`;
}
return '—';
}
exports.summarizeFieldOptions = summarizeFieldOptions;
function formatValue(value) {
if (value === null || value === undefined)
return '';
if (Array.isArray(value))
return value.map((v) => String(v)).join('|');
if (typeof value === 'object')
return JSON.stringify(value);
return String(value);
}
/** One-line human summary of a choice's visibilityFilter; `(always)` when absent. */
function describeChoiceCondition(vf, fieldNames) {
if (!vf || typeof vf !== 'object')
return '(always)';
const { conjunction, filterSet } = vf;
if (!Array.isArray(filterSet) || !filterSet.length)
return '(always)';
const joiner = conjunction === 'or' ? ' OR ' : ' AND ';
const rules = filterSet.map(({ fieldId, operator, value }) => {
const label = (fieldId && fieldNames?.[fieldId]) || fieldId || '?';
const v = formatValue(value);
return v ? `${label} ${operator ?? '?'} ${v}` : `${label} ${operator ?? '?'}`;
});
let text = '';
let kept = 0;
for (const rule of rules) {
const candidate = text ? text + joiner + rule : rule;
if (candidate.length > CONDITION_MAX_LEN && kept > 0)
break;
text = candidate;
kept++;
}
const omitted = rules.length - kept;
return omitted > 0 ? `${text}… +${omitted} more` : text;
}
exports.describeChoiceCondition = describeChoiceCondition;
/** Choices of a select field as display rows; `[]` when the field has none. */
function choiceRows(field, fieldNames) {
const choices = field.options?.choices;
if (!Array.isArray(choices))
return [];
return choices
.filter((c) => !!c && typeof c === 'object')
.map((c) => ({
name: String(c.name ?? ''),
color: typeof c.color === 'string' ? c.color : '',
when: describeChoiceCondition(c.visibilityFilter, fieldNames),
}));
}
exports.choiceRows = choiceRows;
function kvLine(key, value, keyWidth) {
const val = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '');
return `${key.padEnd(keyWidth)} ${val}`;
}
/** Full human-mode rendering for `field get`: kv lines, with select choices
* expanded into a per-choice condition listing instead of one JSON blob. */
function formatFieldDetail(field, fieldNames) {
const lines = [];
const options = field.options;
const choices = options?.choices;
const restEntries = Object.entries({ ...field });
const keyWidth = Math.max(...restEntries.map(([k]) => k.length), 'defaultValue'.length);
for (const [k, v] of restEntries) {
if (k === 'options')
continue;
lines.push(kvLine(k, v, keyWidth));
}
if (Array.isArray(choices) && choices.length) {
const conditional = choices.filter((c) => c && typeof c === 'object' && c.visibilityFilter).length;
lines.push('');
lines.push(`choices (${choices.length} · ${conditional} conditional):`);
for (const row of choiceRows(field, fieldNames)) {
const color = row.color ? ` [${row.color}]` : '';
lines.push(` ${row.name}${color}`);
lines.push(` when: ${row.when}`);
}
const otherOpts = Object.fromEntries(Object.entries(options ?? {}).filter(([k]) => k !== 'choices'));
if (Object.keys(otherOpts).length) {
lines.push('');
lines.push(kvLine('options (other)', otherOpts, keyWidth));
}
}
else if (options && Object.keys(options).length) {
lines.push(kvLine('options', options, keyWidth));
}
else {
lines.push(kvLine('options', undefined, keyWidth));
}
return lines.join('\n');
}
exports.formatFieldDetail = formatFieldDetail;
-289
View File
@@ -1,289 +0,0 @@
"use strict";
/**
* Field-definition normalization for `field create` / `table create --fields`.
*
* The raw API is strict: `type` must be the full enum value and select
* `choices` must be objects (`{name, ...}`) — plain strings 400. These helpers
* accept the friendlier documented forms (aliases like `sel`, string choices,
* `foreignTableName` / `linkFieldName` / `lookupFieldName` to be resolved to
* IDs) so the examples in the skill docs actually work.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveFieldReferences = exports.findFieldByNameOrId = exports.findTableByNameOrId = exports.inferFieldType = exports.expandCompactFieldSpec = exports.normalizeFieldRoArray = exports.normalizeFieldRo = exports.resolveFieldType = void 0;
const TYPE_ALIASES = {
text: 'singleLineText',
str: 'singleLineText',
long: 'longText',
num: 'number',
int: 'number',
check: 'checkbox',
bool: 'checkbox',
sel: 'singleSelect',
select: 'singleSelect',
multi: 'multipleSelect',
tags: 'multipleSelect',
rate: 'rating',
star: 'rating',
user: 'user',
person: 'user',
file: 'attachment',
attach: 'attachment',
mul: 'multipleSelect',
link: 'link',
rel: 'link',
rollup: 'rollup',
condrollup: 'conditionalRollup',
formula: 'formula',
auto: 'autoNumber',
created: 'createdTime',
modified: 'lastModifiedTime',
createdby: 'createdBy',
modifiedby: 'lastModifiedBy',
button: 'button',
};
/** Resolve a type alias (`sel`) to its full value (`singleSelect`). Unknown values pass through untouched. */
function resolveFieldType(type) {
return TYPE_ALIASES[type.toLowerCase()] ?? type;
}
exports.resolveFieldType = resolveFieldType;
const SELECT_TYPES = new Set(['singleSelect', 'multipleSelect']);
/** Required-option defaults for types whose Ro schema has none — applied only
* when the user supplied an options object that is missing required keys. */
function applyOptionDefaults(type, options) {
if (type === 'rating') {
if (options.icon === undefined)
options.icon = 'star';
if (options.color === undefined)
options.color = 'yellowBright';
if (options.max === undefined)
options.max = 5;
}
else if (type === 'number') {
if (options.formatting === undefined) {
options.formatting = { precision: 0, type: 'decimal' };
}
}
else if (type === 'button') {
if (options.label === undefined)
options.label = 'Button';
if (options.color === undefined)
options.color = 'teal';
}
}
/**
* Normalize one field body in place:
* - resolve `type` aliases
* - convert select `options.choices` string entries to `{name}` objects
* - backfill required-option defaults for rating/number/button
*/
function normalizeFieldRo(body) {
if (typeof body.type === 'string') {
body.type = resolveFieldType(body.type);
}
const type = body.type;
if (type &&
body.options &&
typeof body.options === 'object' &&
!Array.isArray(body.options)) {
const options = body.options;
if (SELECT_TYPES.has(type) && Array.isArray(options.choices)) {
options.choices = options.choices.map((c) => typeof c === 'string' ? { name: c } : c);
}
applyOptionDefaults(type, options);
}
return body;
}
exports.normalizeFieldRo = normalizeFieldRo;
/** Apply normalizeFieldRo to every entry of a `--fields` array (invalid input passes through so the API reports it). */
function normalizeFieldRoArray(fields) {
if (!Array.isArray(fields))
return fields;
return fields.map((f) => typeof f === 'string'
? expandCompactFieldSpec(f)
: f && typeof f === 'object' && !Array.isArray(f)
? normalizeFieldRo({ ...f })
: f);
}
exports.normalizeFieldRoArray = normalizeFieldRoArray;
/**
* Expand the compact `--fields` string form "Name[:type[:choice,choice,…]]"
* (e.g. "Status:sel:Todo,Done", "Notes:long", "Title") into a field body.
* Bare names become singleLineText; a bare type token is resolved through
* the alias map; a third segment lists select choices (strings).
*/
function expandCompactFieldSpec(spec) {
const parts = spec.split(':').map((p) => p.trim());
const name = parts[0];
const type = parts[1] ? resolveFieldType(parts[1]) : inferFieldType(name, undefined);
const body = { name, type };
if (parts.length > 2 && parts[2]) {
const choices = parts[2].split(',').map((c) => c.trim()).filter(Boolean);
body.options = { choices: choices.map((c) => ({ name: c })) };
}
return body;
}
exports.expandCompactFieldSpec = expandCompactFieldSpec;
const NAME_INFER_RULES = [
[/^(status|priority|category|type|stage|level)$/i, 'singleSelect'],
[/^(tags?|labels?)$/i, 'multipleSelect'],
[/^(description|notes|comment|content|body)$/i, 'longText'],
[/^(count|amount|price|quantity|total|number|age)$/i, 'number'],
[/^(rating|score|stars?)$/i, 'rating'],
[/^(is.+|has.+|active|enabled|done|completed)$/i, 'checkbox'],
[/^(assignee|owner|user)$/i, 'user'],
[/^(attachment|file|image|photo|document)$/i, 'attachment'],
[/^(email|url)$/i, 'singleLineText'],
[/^(created( time)?|created)$/i, 'createdTime'],
[/^(last modified|updated)$/i, 'lastModifiedTime'],
[/^(created by|author)$/i, 'createdBy'],
[/^(modified by|updated by)$/i, 'lastModifiedBy'],
];
/**
* Infer the field type when `--type` is omitted: first from options shape
* (choices/max/foreignTableName/expression/workflow), then from the field
* name, falling back to singleLineText.
*/
function inferFieldType(name, options) {
if (options && typeof options === 'object' && !Array.isArray(options)) {
const o = options;
if (Array.isArray(o.choices))
return 'singleSelect';
if (typeof o.max === 'number')
return 'rating';
if (typeof o.foreignTableName === 'string' || typeof o.foreignTableId === 'string') {
return 'link';
}
if (typeof o.expression === 'string') {
// conditionalRollup when combined with link + condition; plain formula otherwise
return typeof o.linkFieldId === 'string' && o.condition ? 'conditionalRollup' : 'formula';
}
if (o.workflow && typeof o.workflow === 'object')
return 'button';
}
if (name) {
for (const [re, type] of NAME_INFER_RULES) {
if (re.test(name))
return type;
}
}
return 'singleLineText';
}
exports.inferFieldType = inferFieldType;
/** Find a table by ID or (exact, then case-insensitive) name. Returns undefined when not found. */
function findTableByNameOrId(tables, nameOrId) {
return (tables.find((t) => t.id === nameOrId) ??
tables.find((t) => t.name === nameOrId) ??
tables.find((t) => t.name.toLowerCase() === nameOrId.toLowerCase()));
}
exports.findTableByNameOrId = findTableByNameOrId;
/** Find a field by ID or (exact, then case-insensitive) name. Returns undefined when not found. */
function findFieldByNameOrId(fields, nameOrId) {
return (fields.find((f) => f.id === nameOrId) ??
fields.find((f) => f.name === nameOrId) ??
fields.find((f) => f.name.toLowerCase() === nameOrId.toLowerCase()));
}
exports.findFieldByNameOrId = findFieldByNameOrId;
/**
* Resolve name references in link `options` and `lookupOptions` to IDs:
* - options.foreignTableName → options.foreignTableId
* - lookupOptions.linkFieldName → linkFieldId (fields of THIS table)
* - lookupOptions.lookupFieldName → lookupFieldId (fields of the foreign table)
*
* `listTables(baseId)`, `listFields(tableId)` are client calls; only invoked when
* a *Name key is present. Existing IDs are left untouched. Returns error strings
* (not throws) so the caller can decide to fail with a helpful message.
*/
async function resolveFieldReferences(body, deps) {
const errors = [];
const options = (body.options && typeof body.options === 'object' && !Array.isArray(body.options)
? body.options
: undefined);
if (options && typeof options.foreignTableName === 'string' && !options.foreignTableId) {
const tables = await deps.listTables(deps.baseId);
const t = findTableByNameOrId(tables, options.foreignTableName);
if (!t) {
errors.push(`foreignTableName "${options.foreignTableName}" not found in base ${deps.baseId}`);
}
else {
options.foreignTableId = t.id;
delete options.foreignTableName; // API rejects unknown keys
}
}
const lookup = body.lookupOptions &&
typeof body.lookupOptions === 'object' &&
!Array.isArray(body.lookupOptions)
? body.lookupOptions
: undefined;
if (lookup) {
// For lookup fields the foreign table is the target of the LINK field
// referenced by linkFieldId — there is no options.foreignTableId on the
// lookup field itself. Resolve the link field first, then use its options.
let linkForeignTableId;
const linkRef = typeof lookup.linkFieldId === 'string'
? lookup.linkFieldId
: typeof lookup.linkFieldName === 'string'
? lookup.linkFieldName
: undefined;
const hasNameHints = typeof lookup.linkFieldName === 'string' || typeof lookup.lookupFieldName === 'string';
const needForeignFromLink = hasNameHints &&
typeof lookup.foreignTableId !== 'string' &&
!(options && typeof options.foreignTableId === 'string');
if (linkRef && deps.tableId && (hasNameHints || needForeignFromLink)) {
const fields = (await deps.listFields(deps.tableId)) ?? [];
const linkField = findFieldByNameOrId(fields, linkRef);
if (!linkField && typeof lookup.linkFieldId !== 'string') {
errors.push(`linkFieldName "${lookup.linkFieldName}" not found in table ${deps.tableId}`);
}
if (linkField) {
lookup.linkFieldId = linkField.id;
const linkOptions = linkField
.options;
const fid = linkOptions && typeof linkOptions.foreignTableId === 'string'
? linkOptions.foreignTableId
: undefined;
if (fid)
linkForeignTableId = fid;
}
}
const foreignTableId = (options && typeof options.foreignTableId === 'string' && options.foreignTableId) ||
(typeof lookup.foreignTableId === 'string' && lookup.foreignTableId) ||
linkForeignTableId ||
(typeof lookup.linkFieldId === 'string' && lookup.linkFieldId); // conditional rollup: foreign table only
// Backfill foreignTableId (Ro schema requires it even for plain lookups,
// where it is derived from the link field) once resolved.
if (linkForeignTableId && typeof lookup.foreignTableId !== 'string') {
lookup.foreignTableId = linkForeignTableId;
}
if (typeof lookup.linkFieldName === 'string' && !lookup.linkFieldId) {
if (!deps.tableId) {
errors.push('linkFieldName resolution requires --table-id (creating a field)');
}
// (resolution already attempted above; if it failed an error was pushed)
}
if (typeof lookup.lookupFieldName === 'string' && !lookup.lookupFieldId) {
const foreign = (options && typeof options.foreignTableId === 'string' && options.foreignTableId) ||
foreignTableId;
if (!foreign) {
errors.push('lookupFieldName resolution requires foreignTableId (or foreignTableName)');
}
else {
const fields = await deps.listFields(foreign);
const f = findFieldByNameOrId(fields, lookup.lookupFieldName);
if (!f) {
errors.push(`lookupFieldName "${lookup.lookupFieldName}" not found in foreign table ${foreign}`);
}
else {
lookup.lookupFieldId = f.id;
}
}
}
// The API rejects unknown keys — strip the name hints (only when resolved;
// unresolved ones produced errors above and the caller will abort).
if (lookup.linkFieldId)
delete lookup.linkFieldName;
if (lookup.lookupFieldId)
delete lookup.lookupFieldName;
}
return errors;
}
exports.resolveFieldReferences = resolveFieldReferences;
-34
View File
@@ -1,34 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.truncate = exports.printKv = exports.printTable = exports.printJson = void 0;
/** Print as JSON (compact). Safe for agent parsing. */
function printJson(value) {
process.stdout.write(JSON.stringify(value) + '\n');
}
exports.printJson = printJson;
/** Print a human-readable table. columns: [{key, header, width?}]; rows: object[]. */
function printTable(columns, rows) {
const headers = columns.map((c) => c.header);
const widths = columns.map((c) => Math.max(c.header.length, ...rows.map((r) => String(r[c.key] ?? '').length)));
const sep = widths.map((w) => '-'.repeat(w)).join(' ');
const fmtRow = (r) => columns.map((c, i) => String(r[c.key] ?? '').padEnd(widths[i])).join(' ');
process.stdout.write(headers.map((h, i) => h.padEnd(widths[i])).join(' ') + '\n');
process.stdout.write(sep + '\n');
rows.forEach((r) => process.stdout.write(fmtRow(r) + '\n'));
}
exports.printTable = printTable;
/** Print key: value pairs vertically. */
function printKv(pairs) {
const width = Math.max(...pairs.map(([k]) => k.length));
pairs.forEach(([k, v]) => {
const val = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v ?? '');
process.stdout.write(`${k.padEnd(width)} ${val}\n`);
});
}
exports.printKv = printKv;
/** Truncate a long string to width, appending ellipsis. */
function truncate(s, width = 40) {
const str = String(s ?? '');
return str.length > width ? str.slice(0, width - 1) + '…' : str;
}
exports.truncate = truncate;
-124
View File
@@ -1,124 +0,0 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.describeError = exports.request = exports.ApiError = void 0;
/**
* HTTP request helper using node:http / node:https.
*
* IMPORTANT: we deliberately avoid global fetch() on Node.js — the Node 24
* undici fetch drops the Authorization header in some redirect/proxy scenarios
* (confirmed bug in this project). Using the built-in http(s) module keeps the
* Authorization: Bearer header intact end-to-end.
*/
const http = __importStar(require("node:http"));
const https = __importStar(require("node:https"));
class ApiError extends Error {
status;
body;
constructor(status, message, body) {
super(message);
this.name = 'ApiError';
this.status = status;
this.body = body;
}
}
exports.ApiError = ApiError;
async function request(baseUrl, pathName, options = {}) {
const url = new URL(pathName.startsWith('http') ? pathName : pathName.replace(/^\//, ''), baseUrl.endsWith('/') ? baseUrl : baseUrl + '/');
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const method = options.method || 'GET';
const headers = {
Accept: 'application/json',
...(options.headers || {}),
};
let bodyData;
if (options.body !== undefined && options.body !== null) {
bodyData = JSON.stringify(options.body);
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
headers['Content-Length'] = Buffer.byteLength(bodyData).toString();
}
return new Promise((resolve, reject) => {
const req = lib.request({
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method,
headers,
}, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const buf = Buffer.concat(chunks);
const text = buf.toString('utf8');
let data = text;
const ct = res.headers['content-type'] || '';
if (ct.includes('application/json') || text.trim().startsWith('{') || text.trim().startsWith('[')) {
try {
data = text ? JSON.parse(text) : null;
}
catch {
data = text;
}
}
const status = res.statusCode || 200;
if (status >= 200 && status < 300) {
resolve({ status, headers: res.headers, data: data });
}
else {
const errBody = data;
const msg = (errBody && typeof errBody === 'object' && errBody.error?.message) ||
(typeof errBody === 'object' && errBody.message) ||
`HTTP ${status}`;
reject(new ApiError(status, String(msg), errBody));
}
});
});
if (options.signal) {
options.signal.addEventListener('abort', () => req.destroy(new Error('aborted')));
}
req.on('error', reject);
if (bodyData !== undefined)
req.write(bodyData);
req.end();
});
}
exports.request = request;
/** Extract `{ code, message }` from an ApiError body for display. */
function describeError(err) {
if (err instanceof ApiError) {
const b = err.body;
return {
status: err.status,
code: b?.error?.code || `HTTP_${err.status}`,
message: b?.error?.message || err.message,
};
}
if (err instanceof Error) {
return { status: 0, code: 'ERROR', message: err.message };
}
return { status: 0, code: 'UNKNOWN', message: String(err) };
}
exports.describeError = describeError;
+9 -1
View File
@@ -1,9 +1,16 @@
{ {
"name": "@inkreach/teable-cli", "name": "@inkreach/teable-cli",
"version": "0.1.0", "version": "0.1.0",
"description": "CLI (command: inkreach-teable) for operating inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 6 actions).", "description": "CLI (command: inkreach-teable) for operating inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 7 actions).",
"license": "MIT", "license": "MIT",
"type": "commonjs", "type": "commonjs",
"repository": {
"type": "git",
"url": "ssh://git@gitea.inkreach.cc:2222/inkreachhub/inkreach-teable-cli.git"
},
"engines": {
"node": ">=18"
},
"bin": { "bin": {
"inkreach-teable": "./dist/bin.js" "inkreach-teable": "./dist/bin.js"
}, },
@@ -12,6 +19,7 @@
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json --watch", "dev": "tsc -p tsconfig.build.json --watch",
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"test": "vitest run",
"clean": "rimraf dist tsconfig.build.tsbuildinfo" "clean": "rimraf dist tsconfig.build.tsbuildinfo"
}, },
"dependencies": { "dependencies": {
+1069
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@ const program = new Command();
program program
.name('inkreach-teable') .name('inkreach-teable')
.description( .description(
'Operate inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 6 actions).\n' + 'Operate inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 7 actions).\n' +
'Use --json for machine-readable output (agent parsing).' 'Use --json for machine-readable output (agent parsing).'
) )
.version(`inkreach-teable ${version}`, '-v, --version'); .version(`inkreach-teable ${version}`, '-v, --version');
+8 -2
View File
@@ -1,5 +1,4 @@
{ {
"extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es2022", "target": "es2022",
@@ -12,7 +11,14 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true "strict": true,
"useUnknownInCatchVariables": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"isolatedModules": true,
"incremental": true,
"newLine": "lf"
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "**/*.spec.ts"]