@stalfh233/omc-cli 0.6.17 → 0.6.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/batch-call.js +21 -2
- package/dist/cli-stdout.js +1 -0
- package/dist/cli.js +24 -14
- package/dist/field-change.js +7 -1
- package/dist/field-families.js +67 -2
- package/dist/graph.js +31 -7
- package/dist/lanes/app.js +17 -4
- package/dist/lanes/field.js +120 -5
- package/dist/lanes/folder.js +29 -2
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +51 -29
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +41 -9
- package/dist/lanes/programming.js +67 -14
- package/dist/lanes/runtime-data.js +153 -16
- package/dist/mcp-graph.js +32 -13
- package/dist/programming-workflow.js +16 -3
- package/dist/read-shapes.js +3 -3
- package/dist/residue.js +116 -0
- package/dist/rollback.js +54 -19
- package/dist/write.js +6 -2
- package/package.json +3 -3
package/dist/residue.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ENDPOINTS } from "./endpoints.js";
|
|
3
|
+
import { wireCall, wireRows } from "./wire.js";
|
|
4
|
+
import { flattenTreeRows } from "./support.js";
|
|
5
|
+
import { ConfigStore, defaultPaths } from "./config-store.js";
|
|
6
|
+
import { SessionManager } from "./session-manager.js";
|
|
7
|
+
import { makeToolContext } from "./context.js";
|
|
8
|
+
import { emitInvalid, emitResult } from "./command-transport.js";
|
|
9
|
+
const MAX_ITEMS = 20;
|
|
10
|
+
function face(faceName, rows, extra = {}) {
|
|
11
|
+
return { face: faceName, ...extra, count: rows.length, items: rows.slice(0, MAX_ITEMS) };
|
|
12
|
+
}
|
|
13
|
+
function codeOf(row) {
|
|
14
|
+
return String(row.code ?? row.schemaCode ?? row.bizSchemaCode ?? "");
|
|
15
|
+
}
|
|
16
|
+
function nameOf(row, code) {
|
|
17
|
+
return String(row.name ?? row.displayName ?? row.title ?? code);
|
|
18
|
+
}
|
|
19
|
+
function codeStarts(row, prefix) {
|
|
20
|
+
return codeOf(row).startsWith(prefix);
|
|
21
|
+
}
|
|
22
|
+
function folderStarts(row, prefix) {
|
|
23
|
+
return codeStarts(row, prefix) || String(row.name ?? "").startsWith(prefix);
|
|
24
|
+
}
|
|
25
|
+
function scriptPresent(raw) {
|
|
26
|
+
if (typeof raw !== "string")
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!Array.isArray(parsed))
|
|
31
|
+
return false;
|
|
32
|
+
return parsed.some((element) => {
|
|
33
|
+
const row = element;
|
|
34
|
+
return row.id === "customScript" && typeof row.innerHTML === "string" && row.innerHTML.trim() !== "";
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function tree(session, app) {
|
|
42
|
+
return flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: app } })).data));
|
|
43
|
+
}
|
|
44
|
+
async function modelRows(session, app) {
|
|
45
|
+
return (await tree(session, app)).filter((row) => row.type === "BizModel");
|
|
46
|
+
}
|
|
47
|
+
async function customPages(session, appSet) {
|
|
48
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.customPageList, query: { modeType: "VUE_MODE" } })).data);
|
|
49
|
+
return rows.filter((row) => appSet.has(String(row.appCode ?? "")));
|
|
50
|
+
}
|
|
51
|
+
async function deepFaces(session, model) {
|
|
52
|
+
const schemaCode = codeOf(model);
|
|
53
|
+
const rules = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } })).data);
|
|
54
|
+
const datarules = wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query: { schemaCode } })).data);
|
|
55
|
+
let online = [];
|
|
56
|
+
try {
|
|
57
|
+
const sheet = (await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode: schemaCode } })).data;
|
|
58
|
+
if (sheet && (scriptPresent(sheet.draftHtmlJson) || scriptPresent(sheet.publishedHtmlJson))) {
|
|
59
|
+
const raw = typeof sheet.publishedHtmlJson === "string" && scriptPresent(sheet.publishedHtmlJson) ? sheet.publishedHtmlJson : sheet.draftHtmlJson;
|
|
60
|
+
online = [{ present: true, ...(typeof raw === "string" ? { sha256: createHash("sha256").update(raw).digest("hex") } : {}) }];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// A missing form is an absent online-js face, not a scan failure.
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
face("rule", rules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
68
|
+
face("datarule", datarules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
69
|
+
face("onlinejs", online, { model: schemaCode }),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
export async function scanResidues(session, prefix, options = {}) {
|
|
73
|
+
const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
|
|
74
|
+
.map((row) => String(row.appCode ?? row.code ?? ""))
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
const selected = options.apps && options.apps !== "all" ? options.apps.split(",").map((app) => app.trim()).filter(Boolean) : apps;
|
|
77
|
+
const appSet = new Set(selected);
|
|
78
|
+
const faces = [];
|
|
79
|
+
const models = [];
|
|
80
|
+
for (const app of selected) {
|
|
81
|
+
const rows = await tree(session, app);
|
|
82
|
+
const matchingModels = rows.filter((row) => row.type === "BizModel" && codeStarts(row, prefix));
|
|
83
|
+
models.push(...matchingModels.map((row) => ({ ...row, appCode: app })));
|
|
84
|
+
faces.push(face("model", matchingModels.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
|
|
85
|
+
faces.push(face("folder", rows.filter((row) => row.type === "Folder" && folderStarts(row, prefix)).map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
|
|
86
|
+
}
|
|
87
|
+
const pages = (await customPages(session, appSet)).filter((row) => codeStarts(row, prefix));
|
|
88
|
+
for (const app of selected) {
|
|
89
|
+
faces.push(face("custompage", pages.filter((row) => String(row.appCode ?? "") === app).map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
|
|
90
|
+
}
|
|
91
|
+
if (options.deep !== false) {
|
|
92
|
+
for (const model of models)
|
|
93
|
+
faces.push(...await deepFaces(session, model));
|
|
94
|
+
}
|
|
95
|
+
const totalResidues = faces.reduce((sum, item) => sum + item.count, 0);
|
|
96
|
+
return {
|
|
97
|
+
prefix,
|
|
98
|
+
faces,
|
|
99
|
+
totalResidues,
|
|
100
|
+
clean: totalResidues === 0,
|
|
101
|
+
note: "report 面暂停维护未扫描;workflow 模板跟模型走(模型删即删)",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export async function cmdResidue(prefix, options, transport) {
|
|
105
|
+
if (!prefix || prefix.trim() === "")
|
|
106
|
+
return emitInvalid(transport, { message: "residue 的 prefix 必填" });
|
|
107
|
+
try {
|
|
108
|
+
const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
109
|
+
const session = await context.sessions.session(options.env);
|
|
110
|
+
return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true }), transport);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return emitResult({ status: "error", blocker: "residue-scan-failed", message: error instanceof Error ? error.message : String(error) }, transport);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=residue.js.map
|
package/dist/rollback.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readJournal, resolveJournalEntry, laterWritesOnTarget } from "./journal
|
|
|
8
8
|
import { canonicalForVerify, deepDiff, pruneNullish } from "./rollback-spec.js";
|
|
9
9
|
import { sha256 } from "./support.js";
|
|
10
10
|
import { emitTransportResult } from "./command-transport.js";
|
|
11
|
+
import { acquireWriteLock } from "./write-lock.js";
|
|
11
12
|
/** Pure runtime assembly: an injected context (tests) or the default one. */
|
|
12
13
|
function runtime(context) {
|
|
13
14
|
return context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
@@ -191,25 +192,53 @@ export async function applyRollback(ref, options, context) {
|
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
const results = [];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
195
|
+
// The inverse plan is intentionally kept as the seam for future inverse
|
|
196
|
+
// extensions (for example onlinejs.compose remove:true): all new restore
|
|
197
|
+
// shapes still become governed calls here, without changing the evidence
|
|
198
|
+
// or lock protocol.
|
|
199
|
+
const waitMs = options.waitMs ?? 0;
|
|
200
|
+
const deadline = Date.now() + waitMs;
|
|
201
|
+
let lock;
|
|
202
|
+
for (;;) {
|
|
203
|
+
lock = await acquireWriteLock(entry.environment);
|
|
204
|
+
if (lock.held)
|
|
205
|
+
break;
|
|
206
|
+
if (Date.now() >= deadline) {
|
|
207
|
+
return {
|
|
208
|
+
status: "refused",
|
|
209
|
+
blocker: "write-lock-held",
|
|
210
|
+
rollbackOf: { seq: entry.seq, tool: entry.tool },
|
|
211
|
+
waitMs,
|
|
212
|
+
lockOwner: lock.owner ?? null,
|
|
213
|
+
message: `回滚写锁被占用;已等待 ${waitMs}ms。可用 --wait <ms> 有界排队后重试`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(200, Math.max(0, deadline - Date.now()))));
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
for (const [index, call] of plan.inverse.calls.entries()) {
|
|
220
|
+
const { exitCode, result } = await runToolCall(call.tool, {
|
|
221
|
+
args: { ...call.args, environment: entry.environment, ...(options.confirmProduction ? { confirmProduction: true } : {}) },
|
|
222
|
+
requestId: `omc-rollback-${entry.seq}-${index + 1}`,
|
|
223
|
+
context: ctx,
|
|
224
|
+
print: () => undefined,
|
|
225
|
+
});
|
|
226
|
+
results.push({
|
|
227
|
+
tool: call.tool,
|
|
228
|
+
exitCode,
|
|
229
|
+
status: result.status,
|
|
230
|
+
...(result.deliveryState ? { deliveryState: result.deliveryState } : {}),
|
|
231
|
+
...(result.summary ? { summary: result.summary } : {}),
|
|
232
|
+
// r55: exit-code-only results hid the reason a restore call failed
|
|
233
|
+
// (input-invalid showed as a bare "status=error"). Surface the shape.
|
|
234
|
+
...(exitCode === 0 ? {} : { blocker: result.blocker ?? null, message: result.message ?? null, ...(result.issues !== undefined ? { issues: result.issues } : {}), ...(result.diagnostic !== undefined ? { diagnostic: result.diagnostic } : {}) }),
|
|
235
|
+
});
|
|
236
|
+
if (exitCode !== 0)
|
|
237
|
+
return { status: "stopped", rollbackOf: { seq: entry.seq, tool: entry.tool }, step: index + 1, results, message: `恢复调用 ${call.tool} 未成功(status=${result.status});已执行的调用已各自记 journal,可对已完成步骤再 plan` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await lock.release();
|
|
213
242
|
}
|
|
214
243
|
// Authoritative verify: re-read the target and compare against the
|
|
215
244
|
// canonicalized before. "restored" tolerates the write pipeline
|
|
@@ -232,6 +261,12 @@ export async function applyRollback(ref, options, context) {
|
|
|
232
261
|
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target },
|
|
233
262
|
calls: results,
|
|
234
263
|
verify,
|
|
264
|
+
// Stable evidence is present on both the restored and mismatch paths.
|
|
265
|
+
// `recoveredTo` deliberately identifies the original journal target; the
|
|
266
|
+
// replay writes are separately journaled and can be rolled back again.
|
|
267
|
+
restoredSha256: restoredSha,
|
|
268
|
+
matchesBefore: verify === "restored",
|
|
269
|
+
recoveredTo: { seq: entry.seq, ts: entry.ts },
|
|
235
270
|
...(verify === "mismatch" ? { beforeSha, restoredSha, message: "恢复调用已完成但读回与 before 存在值冲突(非默认显式化):立即人工核对目标现态(omc rollback plan <seq> 的 diff 可复看差异)" } : {}),
|
|
236
271
|
hint: "本次恢复的每次调用都走了完整写闸门并已记 journal;恢复本身可再回滚(rollback plan <新 seq>)",
|
|
237
272
|
};
|
package/dist/write.js
CHANGED
|
@@ -143,7 +143,7 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEs
|
|
|
143
143
|
const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
|
|
144
144
|
return { status: executed.status, steps: executed.steps, failedStep: executed.failedStep, ...executed.envelope, ...(args.requestId ? { requestId: args.requestId } : {}), artifactFingerprint: artifactRef.detailSha256 };
|
|
145
145
|
}
|
|
146
|
-
const { performed, readback, steps, halfSuccess, irreversible } = executed;
|
|
146
|
+
const { performed, readback, steps, halfSuccess, irreversible, rollbackReceipt, runtimeVerification } = executed;
|
|
147
147
|
// Readback gate (live 2026-10-17): a tool that reports `readback.present:false`
|
|
148
148
|
// has NOT proven its own write — the platform accepted the request but the
|
|
149
149
|
// object is absent from the authoritative face. "Called without error" must
|
|
@@ -204,17 +204,21 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEs
|
|
|
204
204
|
return {
|
|
205
205
|
status: "ok",
|
|
206
206
|
deliveryState,
|
|
207
|
+
...((typeof performed.applicationCode === "string" ? { applicationCode: performed.applicationCode } : typeof readback.applicationCode === "string" ? { applicationCode: readback.applicationCode } : {})),
|
|
207
208
|
...(readOnlyEscapeNote !== undefined ? { readOnlyEscape: readOnlyEscapeNote } : {}),
|
|
208
209
|
performed,
|
|
209
210
|
readback,
|
|
210
211
|
...(readbackWarning ? { readbackWarning } : {}),
|
|
211
212
|
...(steps !== undefined ? { steps } : {}),
|
|
212
213
|
...(halfSuccess !== undefined ? { halfSuccess } : {}),
|
|
214
|
+
...(runtimeVerification !== undefined ? { runtimeVerification } : {}),
|
|
213
215
|
...(typeof irreversible === "string" ? { irreversible } : {}),
|
|
214
216
|
// r30a F1: first write → replay:"none"; duplicate only on true replay.
|
|
215
217
|
...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name, "none") } : {}),
|
|
216
218
|
...(productionConfirmed ? { productionConfirmed: true } : {}),
|
|
217
|
-
...(rollbackRef
|
|
219
|
+
...(rollbackRef
|
|
220
|
+
? { rollback: { ...(rollbackReceipt ?? {}), seq: rollbackRef.seq, ...(rollbackReceipt ? {} : { hint: rollbackRef.hint }) } }
|
|
221
|
+
: {}),
|
|
218
222
|
summary: writeSummary(tool.name, args, { performed, readback }, "done"),
|
|
219
223
|
detailFile: artifactRef.detailFile,
|
|
220
224
|
detailSha256: artifactRef.detailSha256,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.19",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"commander": "14.0.3",
|
|
58
|
-
"omc-core": "0.6.
|
|
58
|
+
"omc-core": "0.6.19",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
60
|
"write-file-atomic": "6.0.0",
|
|
61
61
|
"zod": "^3.23.8",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"directory": "packages/omc-cli"
|
|
74
74
|
},
|
|
75
75
|
"optionalDependencies": {
|
|
76
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
76
|
+
"@stalfh233/omc-fake-platform": "0.6.19"
|
|
77
77
|
}
|
|
78
78
|
}
|