@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.
@@ -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
- for (const [index, call] of plan.inverse.calls.entries()) {
195
- const { exitCode, result } = await runToolCall(call.tool, {
196
- args: { ...call.args, environment: entry.environment, ...(options.confirmProduction ? { confirmProduction: true } : {}) },
197
- requestId: `omc-rollback-${entry.seq}-${index + 1}`,
198
- context: ctx,
199
- print: () => undefined,
200
- });
201
- results.push({
202
- tool: call.tool,
203
- exitCode,
204
- status: result.status,
205
- ...(result.deliveryState ? { deliveryState: result.deliveryState } : {}),
206
- ...(result.summary ? { summary: result.summary } : {}),
207
- // r55: exit-code-only results hid the reason a restore call failed
208
- // (input-invalid showed as a bare "status=error"). Surface the shape.
209
- ...(exitCode === 0 ? {} : { blocker: result.blocker ?? null, message: result.message ?? null, ...(result.issues !== undefined ? { issues: result.issues } : {}), ...(result.diagnostic !== undefined ? { diagnostic: result.diagnostic } : {}) }),
210
- });
211
- if (exitCode !== 0)
212
- return { status: "stopped", rollbackOf: { seq: entry.seq, tool: entry.tool }, step: index + 1, results, message: `恢复调用 ${call.tool} 未成功(status=${result.status});已执行的调用已各自记 journal,可对已完成步骤再 plan` };
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 ? { rollback: 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.17",
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.17",
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.17"
76
+ "@stalfh233/omc-fake-platform": "0.6.19"
77
77
  }
78
78
  }