@stalfh233/omc-cli 0.4.18 → 0.4.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1375,6 +1375,24 @@ async function main() {
1375
1375
  const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
1376
1376
  commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
1377
1377
  });
1378
+ command.command("script [file]").option("--env <environment>", "行内无 env 时的默认环境").option("--continue", "某行失败后继续执行剩余行(默认停在首个失败)").description("NDJSON 多调脚本:每行 {\"tool\":\"<名称>\",\"args\":{...},\"env\":\"<环境>\",\"dryRun\":true}(# 注释/空行忽略;file 为 - 读 stdin);一个进程一次会话跑完全部,逐行输出 {index,tool,exitCode,result}").action(async (file, options) => {
1379
+ const { runScript, parseScript, readScriptInput } = await import("./script.js");
1380
+ let lines;
1381
+ try {
1382
+ lines = parseScript(readScriptInput(file ?? "-"));
1383
+ }
1384
+ catch (error) {
1385
+ console.error(String(error instanceof Error ? error.message : error));
1386
+ commandExitCode = CLI_EXIT_CODES.invalidInput;
1387
+ return;
1388
+ }
1389
+ if (lines.length === 0) {
1390
+ console.error("脚本为空:每行一个 {\"tool\":\"<名称>\",\"args\":{...}} 调用");
1391
+ commandExitCode = CLI_EXIT_CODES.invalidInput;
1392
+ return;
1393
+ }
1394
+ commandExitCode = await runScript(lines, { defaultEnv: options.env, continueOnError: options.continue === true });
1395
+ });
1378
1396
  command.command("artifact").command("get <id>").option("--jq <path>").option("--path-only").option("--summary").option("--keys [a,b]").option("--max-output-bytes <bytes>").action(async (id, options) => {
1379
1397
  commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(options.jq ? { jq: options.jq } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}) }));
1380
1398
  });
@@ -158,9 +158,15 @@ export function registerB2Lane(tools) {
158
158
  description: B2_TOOLS["service.get"].description,
159
159
  inputSchema: { environment: environmentArg, serviceCode: z.string(), capability: capabilityArg },
160
160
  handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
161
+ // F5: the service row and its method list are independent reads keyed
162
+ // by the same serviceCode — fire both at once. The absent path
163
+ // short-circuits AFTER the pair settles (the method read's result is
164
+ // simply discarded; absence is a fact either way).
165
+ const serviceRead = wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
166
+ const methodRead = wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } }).catch((error) => error);
161
167
  let service;
162
168
  try {
163
- service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
169
+ service = await serviceRead;
164
170
  }
165
171
  catch (error) {
166
172
  // A deleted service answers the wire-business error 500001; reading a
@@ -173,19 +179,25 @@ export function registerB2Lane(tools) {
173
179
  const serviceRow = service.data;
174
180
  let methods = null;
175
181
  let methodsError = null;
176
- try {
177
- methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data).map((row) => ({
178
- code: row.code,
179
- name: row.name,
180
- serviceCode: row.serviceCode ?? args.serviceCode,
181
- configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.configJson))),
182
- inputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.inputParametersJson)),
183
- outputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.outputParametersJson)),
184
- referenced: row.referenced === true,
185
- }));
182
+ const methodOutcome = await methodRead;
183
+ if (methodOutcome instanceof Error) {
184
+ methodsError = methodOutcome.message;
186
185
  }
187
- catch (error) {
188
- methodsError = error instanceof Error ? error.message : String(error);
186
+ else {
187
+ try {
188
+ methods = wireRows(methodOutcome.data).map((row) => ({
189
+ code: row.code,
190
+ name: row.name,
191
+ serviceCode: row.serviceCode ?? args.serviceCode,
192
+ configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.configJson))),
193
+ inputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.inputParametersJson)),
194
+ outputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.outputParametersJson)),
195
+ referenced: row.referenced === true,
196
+ }));
197
+ }
198
+ catch (error) {
199
+ methodsError = error instanceof Error ? error.message : String(error);
200
+ }
189
201
  }
190
202
  return {
191
203
  serviceCode: args.serviceCode,
@@ -12,6 +12,7 @@ import { defineTool, ToolRefusalError } from "../tool-types.js";
12
12
  import { wireCall, wireRows, WireError } from "../wire.js";
13
13
  import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
14
14
  import { FIELD_LANE_CONTRACT } from "./field-contract.js";
15
+ import { mapBounded } from "../support.js";
15
16
  const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
16
17
  const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
17
18
  /** Parse a raw options JSON string for readback; policy: non-strings and unparseable strings pass through verbatim. */
@@ -304,24 +305,26 @@ export async function executeFieldPublish(session, args) {
304
305
  .map((row) => [codeOf(row), { name: String(row.name ?? ""), propertyType: Number(row.type ?? row.propertyType) }]));
305
306
  const verifyFieldDetails = async (targets) => {
306
307
  const bounded = targets.slice(0, 25);
307
- const mismatches = [];
308
- for (const code of bounded) {
308
+ const checked = await mapBounded(bounded, 5, async (code) => {
309
309
  const expected = draftSnapshot.get(code);
310
310
  if (!expected)
311
- continue;
312
- let detail;
311
+ return { kind: "skip" };
313
312
  try {
314
- detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode: args.schemaCode, propertyCode: code } })).data;
313
+ const detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode: args.schemaCode, propertyCode: code } })).data;
314
+ const actual = { name: String(detail.name ?? ""), propertyType: Number(detail.propertyType ?? detail.type) };
315
+ if (actual.name !== expected.name || actual.propertyType !== expected.propertyType)
316
+ return { kind: "mismatch", code, expected, actual };
317
+ return { kind: "ok" };
315
318
  }
316
319
  catch {
317
320
  // The detail endpoint is optional across builds: report the
318
321
  // verification as unavailable rather than failing the publish.
319
- return { mode: "unavailable", verified: false, note: "bizproperty/get 不可用,字段详情未核对(仅列表面存在性)" };
322
+ return { kind: "unavailable" };
320
323
  }
321
- const actual = { name: String(detail.name ?? ""), propertyType: Number(detail.propertyType ?? detail.type) };
322
- if (actual.name !== expected.name || actual.propertyType !== expected.propertyType)
323
- mismatches.push({ code, expected, actual });
324
- }
324
+ });
325
+ if (checked.some((entry) => entry.kind === "unavailable"))
326
+ return { mode: "unavailable", verified: false, note: "bizproperty/get 不可用,字段详情未核对(仅列表面存在性)" };
327
+ const mismatches = checked.filter((entry) => entry.kind === "mismatch");
325
328
  return { mode: "detail", verified: true, checked: bounded.length, ok: mismatches.length === 0, ...(mismatches.length > 0 ? { mismatches } : {}) };
326
329
  };
327
330
  const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
@@ -3,6 +3,7 @@ import { ENDPOINTS } from "../endpoints.js";
3
3
  import { assertFieldFamilyAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, } from "../field-families.js";
4
4
  import { fieldChangeWireBody, prepareFieldChange, sameFieldChange } from "../field-change.js";
5
5
  import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
6
+ import { mapBounded } from "../support.js";
6
7
  import { validateBusinessCode } from "../code-rules.js";
7
8
  import { writeSummary } from "../human-summary.js";
8
9
  import { capabilityArg, environmentArg } from "../args.js";
@@ -188,16 +189,25 @@ export function registerModelLane(tools) {
188
189
  }
189
190
  };
190
191
  const inspectOne = async (schemaCode) => {
191
- const fieldFace = await face("field.list", { schemaCode, published: true, ...(all ? { all: true } : {}) });
192
+ // F5: the six read faces are INDEPENDENT wire reads; fan them out in
193
+ // parallel (bounded) instead of paying six sequential round-trips. Only
194
+ // workflow.get_published stays chained (it needs workflowCode from the
195
+ // draft face). Output ordering is unchanged (results land by index).
196
+ const [fieldFace, formFace, listviewFace, ruleFace, dataruleFace, draftFace] = await mapBounded([
197
+ () => face("field.list", { schemaCode, published: true, ...(all ? { all: true } : {}) }),
198
+ () => face("form.get", { schemaCode }),
199
+ () => face("listview.get", { schemaCode }),
200
+ () => face("rule.list", { schemaCode }),
201
+ () => face("datarule.list", { schemaCode }),
202
+ () => face("workflow.get_draft", { schemaCode }),
203
+ ], 6, (task) => task());
192
204
  const fieldRows = Array.isArray(fieldFace?.fields) ? fieldFace.fields : [];
193
205
  const fields = fieldRows
194
206
  .filter((field) => all || !SYSTEM_FIELD_CODES.has(String(field.code)))
195
207
  .map((field) => ({ code: field.code, type: field.type, ...(field.typeName !== undefined ? { typeName: field.typeName } : {}), name: field.name, ...(field.conditions ? { conditions: field.conditions } : {}) }));
196
- const formFace = await face("form.get", { schemaCode });
197
208
  const form = formFace && formFace.present !== undefined
198
209
  ? { present: formFace.present === true, published: formFace.published === true, controlCount: Array.isArray(formFace.draftControlCodes) ? formFace.draftControlCodes.length : 0 }
199
210
  : undefined;
200
- const listviewFace = await face("listview.get", { schemaCode });
201
211
  const listviewColumns = Array.isArray(listviewFace?.columns) ? listviewFace.columns : [];
202
212
  // Round 13: the default view is platform-generated and lists the system
203
213
  // columns (createdTime/creater/name/sequenceNo/…). In a design-time survey
@@ -208,14 +218,12 @@ export function registerModelLane(tools) {
208
218
  const listview = listviewFace && listviewFace.columns
209
219
  ? { code: listviewFace.code ?? schemaCode, columnCount: visibleColumns.length, columns: visibleColumns.map((column) => ({ propertyCode: column.propertyCode, propertyType: column.propertyType, visible: column.visible })) }
210
220
  : undefined;
211
- const ruleFace = await face("rule.list", { schemaCode });
212
221
  const ruleRows = Array.isArray(ruleFace?.rules) ? ruleFace.rules : [];
213
222
  // All rules are kept: the platform pre-creates trigger rows (Create/Update/
214
223
  // …) but user logic is AUTHORED INTO them, so `defaultRule` cannot be used
215
224
  // to hide "system" rules without dropping authored ones. `defaultRule` is
216
225
  // reported per rule so a caller can decide.
217
226
  const rules = ruleRows.map((rule) => ({ code: rule.code, name: rule.name, enabled: rule.enabled, status: rule.status, ruleVersion: rule.ruleVersion, defaultRule: rule.defaultRule }));
218
- const dataruleFace = await face("datarule.list", { schemaCode });
219
227
  const dataruleRows = Array.isArray(dataruleFace?.rules) ? dataruleFace.rules : [];
220
228
  const datarules = dataruleRows.map((rule) => ({ propertyCode: rule.propertyCode, propertyCodes: rule.propertyCodes, dataRuleType: rule.dataRuleType, name: rule.name, checkType: rule.checkType }));
221
229
  // Workflow summary: use the PUBLISHED face for the `published` flag. The
@@ -224,7 +232,6 @@ export function registerModelLane(tools) {
224
232
  // `published` from get_draft reported published:false forever (live round
225
233
  // 10: an agent's flow read as published:false while get_published was
226
234
  // templateType:PUBLISH). Activities come from whichever face exists.
227
- const draftFace = await face("workflow.get_draft", { schemaCode });
228
235
  const workflowCode = draftFace?.workflowSummary?.workflowCode ?? draftFace?.workflowCode;
229
236
  const publishedFace = workflowCode ? await face("workflow.get_published", { schemaCode }) : undefined;
230
237
  const draftSummary = draftFace?.workflowSummary;
package/dist/script.js ADDED
@@ -0,0 +1,65 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { ConfigStore, defaultPaths } from "./config-store.js";
4
+ import { SessionManager } from "./session-manager.js";
5
+ import { makeToolContext } from "./tools.js";
6
+ import { runToolCall } from "./call.js";
7
+ export function parseScript(text) {
8
+ const lines = [];
9
+ const raw = text.split("\n");
10
+ for (const [index, untrimmed] of raw.entries()) {
11
+ const line = untrimmed.trim();
12
+ if (line.length === 0 || line.startsWith("#"))
13
+ continue;
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(line);
17
+ }
18
+ catch (error) {
19
+ throw new Error(`第 ${index + 1} 行不是合法 JSON:${error instanceof Error ? error.message : String(error)}`);
20
+ }
21
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.tool !== "string") {
22
+ throw new Error(`第 ${index + 1} 行必须是 {"tool":"<名称>","args":{...},"env":"<环境>","dryRun":true} 形状(tool 必填)`);
23
+ }
24
+ const entry = parsed;
25
+ if (entry.args !== undefined && (entry.args === null || typeof entry.args !== "object" || Array.isArray(entry.args)))
26
+ throw new Error(`第 ${index + 1} 行 args 必须是 JSON 对象`);
27
+ lines.push({
28
+ tool: entry.tool,
29
+ ...(typeof entry.args === "object" && entry.args !== null ? { args: entry.args } : {}),
30
+ ...(typeof entry.env === "string" ? { env: entry.env } : {}),
31
+ ...(entry.dryRun === true ? { dryRun: true } : {}),
32
+ });
33
+ }
34
+ return lines;
35
+ }
36
+ export async function runScript(lines, options = {}) {
37
+ const print = options.print ?? ((line) => console.log(line));
38
+ const context = options.context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
39
+ let failed = false;
40
+ for (const [index, line] of lines.entries()) {
41
+ const { exitCode, result } = await runToolCall(line.tool, {
42
+ args: line.args,
43
+ environment: line.env ?? options.defaultEnv,
44
+ dryRun: line.dryRun === true,
45
+ requestId: randomUUID(),
46
+ context,
47
+ print: () => undefined,
48
+ });
49
+ print(JSON.stringify({ index, tool: line.tool, exitCode, result }));
50
+ if (exitCode !== 0) {
51
+ failed = true;
52
+ if (options.continueOnError !== true) {
53
+ print(JSON.stringify({ index, tool: line.tool, stopped: true, note: `第 ${index + 1} 行失败,后续 ${lines.length - index - 1} 行未执行;加 --continue 跑完全部` }));
54
+ return exitCode;
55
+ }
56
+ }
57
+ }
58
+ return failed ? 1 : 0;
59
+ }
60
+ export function readScriptInput(path) {
61
+ if (path === "-")
62
+ return readFileSync(0, "utf8");
63
+ return readFileSync(path, "utf8");
64
+ }
65
+ //# sourceMappingURL=script.js.map
package/dist/support.js CHANGED
@@ -95,4 +95,26 @@ export function flattenTreeRows(rows) {
95
95
  visit(row);
96
96
  return flat;
97
97
  }
98
+ /**
99
+ * F5 (r10-friction): order-preserving map with BOUNDED concurrency. The read
100
+ * lanes fan out independent wire faces (model.inspect's six faces, field
101
+ * publish verification's detail reads) that used to pay full per-call
102
+ * latency sequentially. The bound keeps the fan-out polite to the small
103
+ * environments (ticket 79 documented DB contention under 30 parallel
104
+ * publishes; reads are lighter but still bounded).
105
+ */
106
+ export async function mapBounded(items, limit, worker) {
107
+ const results = new Array(items.length);
108
+ let next = 0;
109
+ const runners = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
110
+ while (true) {
111
+ const index = next++;
112
+ if (index >= items.length)
113
+ return;
114
+ results[index] = await worker(items[index], index);
115
+ }
116
+ });
117
+ await Promise.all(runners);
118
+ return results;
119
+ }
98
120
  //# sourceMappingURL=support.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "commander": "14.0.3",
54
- "omc-core": "0.4.18",
54
+ "omc-core": "0.4.19",
55
55
  "proper-lockfile": "4.1.2",
56
56
  "write-file-atomic": "6.0.0",
57
57
  "zod": "^3.23.8",
@@ -37,6 +37,7 @@ Run in a **clean project directory** (not the OMC dev repo). Start from the goal
37
37
  omc next "<目标>" # goal → ordered route (offline)
38
38
  omc doctor # environment name and readiness
39
39
  omc call discover_environment --args '{}' # 单环境项目(.omc-env 在位)可省 --env;多环境才显式传
40
+ omc script - --env <env> <<< '{"tool":"model.list"}' # 多调链:NDJSON 每行一调,一进程一次会话跑完
40
41
  omc capability <selected-id> # only the selected capability
41
42
  omc describe <tool> --example <recipe-id> # recipe first; default describe otherwise
42
43
  omc help <topic> --section "<heading>" # one methodology section, not all