direxio-deployer 0.1.0 → 0.1.1
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/README.md +3 -1
- package/README_zh.md +3 -1
- package/SKILL.md +2 -2
- package/bin/direxio-deployer.mjs +1 -2
- package/package.json +2 -3
- package/references/deployment-lessons.md +5 -7
- package/references/deployment-workflow.md +1 -1
- package/references/tooling.md +11 -12
- package/references/user-journey.md +2 -2
- package/references/voip-turn-runbook.md +2 -2
- package/references/windows-deployment-notes.md +2 -1
- package/scripts/destroy.sh +24 -43
- package/scripts/json.mjs +841 -0
- package/scripts/lib/aws.sh +5 -1
- package/scripts/lib/json.sh +114 -0
- package/scripts/lib/operation_report.sh +8 -195
- package/scripts/lib/ops.sh +8 -21
- package/scripts/lib/state.sh +18 -44
- package/scripts/mcp-tools-list.mjs +21 -4
- package/scripts/orchestrate.sh +147 -249
- package/scripts/phases/s3_provision.sh +5 -10
- package/scripts/phases/s5_init_tokens.sh +7 -17
- package/scripts/phases/s6_wire_local.sh +9 -35
- package/scripts/phases/s7_verify_e2e.sh +5 -5
- package/scripts/pricing-estimate.sh +36 -80
- package/tests/aws_credentials_test.sh +0 -139
- package/tests/connect_daemon_runtime_check_test.sh +0 -120
- package/tests/default_paths_test.sh +0 -58
- package/tests/destroy_local_bridge_test.sh +0 -154
- package/tests/destroy_root_identity_test.sh +0 -91
- package/tests/destroy_route53_zone_test.sh +0 -80
- package/tests/domain_authoritative_dns_test.sh +0 -49
- package/tests/mcp_doctor_runtime_check_test.sh +0 -86
- package/tests/mcp_smoke_runtime_check_test.sh +0 -121
- package/tests/mcp_tools_runtime_check_test.sh +0 -123
- package/tests/npm_skill_distribution_test.sh +0 -95
- package/tests/operation_report_test.sh +0 -258
- package/tests/orchestrate_status_recovery_test.sh +0 -91
- package/tests/phase_timeout_test.sh +0 -88
- package/tests/pricing_estimate_test.sh +0 -159
- package/tests/render_userdata_remote_nodes_test.sh +0 -40
- package/tests/root_volume_tracking_test.sh +0 -41
- package/tests/route53_overwrite_guard_test.sh +0 -86
- package/tests/route53_zone_auto_create_test.sh +0 -66
- package/tests/runtime_summary_check_test.sh +0 -203
- package/tests/s6_wire_local_test.sh +0 -405
- package/tests/skill_structure_test.sh +0 -298
- package/tests/update_reset_ops_test.sh +0 -230
- package/tests/user_confirmation_gates_test.sh +0 -152
package/scripts/json.mjs
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const [command, ...args] = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
switch (command) {
|
|
9
|
+
case "get":
|
|
10
|
+
cmdGet(args);
|
|
11
|
+
break;
|
|
12
|
+
case "stdin-get":
|
|
13
|
+
cmdStdinGet(args);
|
|
14
|
+
break;
|
|
15
|
+
case "assert":
|
|
16
|
+
cmdAssert(args);
|
|
17
|
+
break;
|
|
18
|
+
case "stdin-assert":
|
|
19
|
+
cmdStdinAssert(args);
|
|
20
|
+
break;
|
|
21
|
+
case "check":
|
|
22
|
+
cmdCheck(args);
|
|
23
|
+
break;
|
|
24
|
+
case "entries":
|
|
25
|
+
cmdEntries(args);
|
|
26
|
+
break;
|
|
27
|
+
case "stdin-tsv":
|
|
28
|
+
cmdStdinTsv(args);
|
|
29
|
+
break;
|
|
30
|
+
case "stdin-join":
|
|
31
|
+
cmdStdinJoin(args);
|
|
32
|
+
break;
|
|
33
|
+
case "stdin-route53-a-values":
|
|
34
|
+
cmdStdinRoute53AValues(args);
|
|
35
|
+
break;
|
|
36
|
+
case "stdin-route53-a-present":
|
|
37
|
+
cmdStdinRoute53APresent(args);
|
|
38
|
+
break;
|
|
39
|
+
case "stdin-price-usd":
|
|
40
|
+
cmdStdinPriceUsd();
|
|
41
|
+
break;
|
|
42
|
+
case "length":
|
|
43
|
+
cmdLength(args);
|
|
44
|
+
break;
|
|
45
|
+
case "type":
|
|
46
|
+
cmdType(args);
|
|
47
|
+
break;
|
|
48
|
+
case "build":
|
|
49
|
+
cmdBuild(args);
|
|
50
|
+
break;
|
|
51
|
+
case "mutate":
|
|
52
|
+
cmdMutate(args);
|
|
53
|
+
break;
|
|
54
|
+
case "operation-report":
|
|
55
|
+
cmdOperationReport(args);
|
|
56
|
+
break;
|
|
57
|
+
case "valid":
|
|
58
|
+
readJsonFile(required(args, 0, "file"));
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
usage(command ? `unknown command: ${command}` : "missing command");
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(error.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cmdGet(args) {
|
|
69
|
+
const file = required(args, 0, "file");
|
|
70
|
+
const jsonPath = required(args, 1, "path");
|
|
71
|
+
const fallback = args.length > 2 ? args[2] : "";
|
|
72
|
+
printValue(getPath(readJsonFile(file), jsonPath, fallback));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cmdStdinGet(args) {
|
|
76
|
+
const jsonPath = required(args, 0, "path");
|
|
77
|
+
const fallback = args.length > 1 ? args[1] : "";
|
|
78
|
+
printValue(getPath(readJsonStdin(), jsonPath, fallback));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cmdAssert(args) {
|
|
82
|
+
const file = required(args, 0, "file");
|
|
83
|
+
const preset = required(args, 1, "preset");
|
|
84
|
+
const data = readJsonFile(file);
|
|
85
|
+
assertPreset(data, preset, args.slice(2));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function cmdStdinAssert(args) {
|
|
89
|
+
const preset = required(args, 0, "preset");
|
|
90
|
+
const data = readJsonStdin();
|
|
91
|
+
assertPreset(data, preset, args.slice(1));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertPreset(data, preset, rest) {
|
|
95
|
+
let ok = false;
|
|
96
|
+
|
|
97
|
+
switch (preset) {
|
|
98
|
+
case "path-equals": {
|
|
99
|
+
const jsonPath = required(rest, 0, "path");
|
|
100
|
+
const expected = required(rest, 1, "expected");
|
|
101
|
+
ok = String(getPath(data, jsonPath, "")) === expected;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "path-missing": {
|
|
105
|
+
const jsonPath = required(rest, 0, "path");
|
|
106
|
+
ok = !hasPath(data, jsonPath);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "messages-list":
|
|
110
|
+
ok = Array.isArray(data.messages) && typeof data.room_id === "undefined";
|
|
111
|
+
if (!ok && Array.isArray(data.messages) && typeof data.room_id === "string") ok = true;
|
|
112
|
+
break;
|
|
113
|
+
case "messages-response":
|
|
114
|
+
ok = Array.isArray(data.messages) && typeof data.room_id === "string";
|
|
115
|
+
break;
|
|
116
|
+
case "tools-list":
|
|
117
|
+
ok = Array.isArray(data.tools) && typeof data.tool_count === "number";
|
|
118
|
+
break;
|
|
119
|
+
case "matrix-session":
|
|
120
|
+
ok = Boolean(data.access_token && data.device_id && data.user_id && data.homeserver);
|
|
121
|
+
break;
|
|
122
|
+
case "well-known-server": {
|
|
123
|
+
const expected = required(rest, 0, "expected");
|
|
124
|
+
ok = data["m.server"] === expected;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "turn-credentials":
|
|
128
|
+
ok = Array.isArray(data.uris) &&
|
|
129
|
+
data.uris.length > 0 &&
|
|
130
|
+
data.uris.some((uri) => /^turns?:/.test(String(uri))) &&
|
|
131
|
+
String(data.username || "").length > 0 &&
|
|
132
|
+
String(data.password || "").length > 0 &&
|
|
133
|
+
Number(data.ttl) > 0;
|
|
134
|
+
break;
|
|
135
|
+
case "bootstrap-normalized":
|
|
136
|
+
ok = typeof data.password === "string" &&
|
|
137
|
+
/^[0-9]{8}$/.test(data.password) &&
|
|
138
|
+
typeof data.agent_token === "string" &&
|
|
139
|
+
data.agent_token.length > 0 &&
|
|
140
|
+
typeof data.access_token === "string" &&
|
|
141
|
+
data.access_token.length > 0;
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
usage(`unknown assert preset: ${preset}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!ok) process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function cmdCheck(args) {
|
|
151
|
+
const file = required(args, 0, "file");
|
|
152
|
+
const expression = required(args, 1, "expression");
|
|
153
|
+
const data = readJsonFile(file);
|
|
154
|
+
const ok = Boolean(Function("data", `"use strict"; return (${expression});`)(data));
|
|
155
|
+
if (!ok) process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function cmdEntries(args) {
|
|
159
|
+
const file = required(args, 0, "file");
|
|
160
|
+
const jsonPath = required(args, 1, "path");
|
|
161
|
+
const value = getPath(readJsonFile(file), jsonPath, {});
|
|
162
|
+
if (!isObject(value)) return;
|
|
163
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
164
|
+
printLine(`${key}=${formatEntryValue(entryValue)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cmdStdinTsv(args) {
|
|
169
|
+
const arrayPath = required(args, 0, "array_path");
|
|
170
|
+
const fields = args.slice(1);
|
|
171
|
+
if (fields.length === 0) usage("stdin-tsv requires at least one field");
|
|
172
|
+
const value = getPath(readJsonStdin(), arrayPath, []);
|
|
173
|
+
if (!Array.isArray(value)) return;
|
|
174
|
+
for (const entry of value) {
|
|
175
|
+
printLine(fields.map((field) => stringValue(getPath(entry, field, ""))).join("\t"));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cmdStdinJoin(args) {
|
|
180
|
+
const jsonPath = required(args, 0, "path");
|
|
181
|
+
const separator = args.length > 1 ? args[1] : ",";
|
|
182
|
+
const value = getPath(readJsonStdin(), jsonPath, []);
|
|
183
|
+
if (!Array.isArray(value)) return;
|
|
184
|
+
printLine(value.map((item) => stringValue(item)).join(separator));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function cmdStdinRoute53AValues(args) {
|
|
188
|
+
const name = required(args, 0, "record_name");
|
|
189
|
+
const data = readJsonStdin();
|
|
190
|
+
const rrsets = Array.isArray(data.ResourceRecordSets) ? data.ResourceRecordSets : [];
|
|
191
|
+
const values = [];
|
|
192
|
+
for (const rrset of rrsets) {
|
|
193
|
+
if (rrset?.Name !== name || rrset?.Type !== "A") continue;
|
|
194
|
+
for (const record of Array.isArray(rrset.ResourceRecords) ? rrset.ResourceRecords : []) {
|
|
195
|
+
if (typeof record?.Value !== "undefined") values.push(String(record.Value));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
printLine(values.join(","));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cmdStdinRoute53APresent(args) {
|
|
202
|
+
const name = required(args, 0, "record_name");
|
|
203
|
+
const ip = required(args, 1, "ip");
|
|
204
|
+
const data = readJsonStdin();
|
|
205
|
+
const rrsets = Array.isArray(data.ResourceRecordSets) ? data.ResourceRecordSets : [];
|
|
206
|
+
const present = rrsets.some((rrset) =>
|
|
207
|
+
rrset?.Name === name &&
|
|
208
|
+
rrset?.Type === "A" &&
|
|
209
|
+
(Array.isArray(rrset.ResourceRecords) ? rrset.ResourceRecords : []).some((record) => String(record?.Value) === ip)
|
|
210
|
+
);
|
|
211
|
+
printLine(String(present));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cmdStdinPriceUsd() {
|
|
215
|
+
const data = readJsonStdin();
|
|
216
|
+
const firstPrice = data.PriceList?.[0];
|
|
217
|
+
if (typeof firstPrice !== "string") return;
|
|
218
|
+
const product = JSON.parse(firstPrice);
|
|
219
|
+
const onDemand = Object.values(product.terms?.OnDemand || {})[0];
|
|
220
|
+
const dimension = Object.values(onDemand?.priceDimensions || {})[0];
|
|
221
|
+
printValue(dimension?.pricePerUnit?.USD || "");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cmdLength(args) {
|
|
225
|
+
const file = required(args, 0, "file");
|
|
226
|
+
const jsonPath = required(args, 1, "path");
|
|
227
|
+
const value = getPath(readJsonFile(file), jsonPath, null);
|
|
228
|
+
if (Array.isArray(value) || typeof value === "string") {
|
|
229
|
+
printLine(String(value.length));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (isObject(value)) {
|
|
233
|
+
printLine(String(Object.keys(value).length));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
printLine("0");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function cmdType(args) {
|
|
240
|
+
const file = required(args, 0, "file");
|
|
241
|
+
const jsonPath = required(args, 1, "path");
|
|
242
|
+
printLine(jsonType(getPath(readJsonFile(file), jsonPath, undefined)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function cmdBuild(args) {
|
|
246
|
+
const preset = required(args, 0, "preset");
|
|
247
|
+
let data;
|
|
248
|
+
|
|
249
|
+
switch (preset) {
|
|
250
|
+
case "simple-state":
|
|
251
|
+
data = {};
|
|
252
|
+
for (const [key, value] of parsePairs(args.slice(1))) setPath(data, key, parseScalar(value));
|
|
253
|
+
break;
|
|
254
|
+
case "object":
|
|
255
|
+
data = {};
|
|
256
|
+
for (const [key, value] of parsePairs(args.slice(1))) setPath(data, key, parseScalar(value));
|
|
257
|
+
break;
|
|
258
|
+
case "mcp-messages-list":
|
|
259
|
+
data = { action: "mcp.messages.list", params: { room_id: required(args, 1, "room_id"), limit: 1 } };
|
|
260
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
261
|
+
return;
|
|
262
|
+
case "matrix-session-create":
|
|
263
|
+
data = { action: "agent.matrix_session.create", params: { device_id: required(args, 1, "device_id") } };
|
|
264
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
265
|
+
return;
|
|
266
|
+
case "mcp-json-config": {
|
|
267
|
+
const serverName = required(args, 1, "server_name");
|
|
268
|
+
data = {
|
|
269
|
+
mcpServers: {
|
|
270
|
+
[serverName]: {
|
|
271
|
+
command: required(args, 2, "command"),
|
|
272
|
+
env: {
|
|
273
|
+
DIREXIO_CREDENTIALS_FILE: required(args, 3, "credentials_file"),
|
|
274
|
+
DIREXIO_AGENT_NODE_ID: args[4] || ""
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "mcp-openclaw-server-config":
|
|
282
|
+
data = {
|
|
283
|
+
command: required(args, 1, "command"),
|
|
284
|
+
env: {
|
|
285
|
+
DIREXIO_CREDENTIALS_FILE: required(args, 2, "credentials_file"),
|
|
286
|
+
DIREXIO_AGENT_NODE_ID: args[3] || ""
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
break;
|
|
290
|
+
case "credentials-profile":
|
|
291
|
+
data = {
|
|
292
|
+
profiles: {
|
|
293
|
+
default: {
|
|
294
|
+
domain: required(args, 1, "domain"),
|
|
295
|
+
password: required(args, 4, "password"),
|
|
296
|
+
access_token: required(args, 5, "access_token"),
|
|
297
|
+
agent_room_id: required(args, 6, "agent_room_id"),
|
|
298
|
+
direxio_domain: required(args, 2, "as_url"),
|
|
299
|
+
direxio_agent_token: required(args, 3, "agent_token"),
|
|
300
|
+
direxio_agent_room_id: required(args, 6, "agent_room_id"),
|
|
301
|
+
direxio_agent_node_id: required(args, 7, "node_id")
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
break;
|
|
306
|
+
case "pricing-estimate":
|
|
307
|
+
data = buildPricingEstimate(args.slice(1));
|
|
308
|
+
break;
|
|
309
|
+
case "bootstrap-normalized":
|
|
310
|
+
data = normalizeBootstrap(required(args, 1, "file"), required(args, 2, "domain"));
|
|
311
|
+
break;
|
|
312
|
+
default:
|
|
313
|
+
usage(`unknown build preset: ${preset}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function cmdMutate(args) {
|
|
320
|
+
const file = required(args, 0, "file");
|
|
321
|
+
const preset = required(args, 1, "preset");
|
|
322
|
+
const data = existsSync(file) ? readJsonFileOrEmptyObject(file) : {};
|
|
323
|
+
|
|
324
|
+
switch (preset) {
|
|
325
|
+
case "set-string": {
|
|
326
|
+
setPath(data, required(args, 2, "path"), required(args, 3, "value"));
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case "set-json": {
|
|
330
|
+
setPath(data, required(args, 2, "path"), JSON.parse(required(args, 3, "json")));
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case "state-init": {
|
|
334
|
+
const runId = required(args, 2, "run_id");
|
|
335
|
+
const region = required(args, 3, "region");
|
|
336
|
+
const ts = required(args, 4, "timestamp");
|
|
337
|
+
const phases = args.slice(5);
|
|
338
|
+
const phaseState = {};
|
|
339
|
+
for (const phase of phases) phaseState[phase] = { status: "pending" };
|
|
340
|
+
Object.assign(data, {
|
|
341
|
+
run_id: runId,
|
|
342
|
+
region: region === "" ? null : region,
|
|
343
|
+
domain_mode: null,
|
|
344
|
+
domain: null,
|
|
345
|
+
domain_confirmed_irreversible: false,
|
|
346
|
+
instance_type: null,
|
|
347
|
+
dns_ready: false,
|
|
348
|
+
existing_state_confirmed: false,
|
|
349
|
+
phase: "S0_PREREQ_AWS",
|
|
350
|
+
created_at: ts,
|
|
351
|
+
phases: phaseState,
|
|
352
|
+
resources: {}
|
|
353
|
+
});
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "phase-set": {
|
|
357
|
+
const phase = required(args, 2, "phase");
|
|
358
|
+
const status = required(args, 3, "status");
|
|
359
|
+
const ts = required(args, 4, "timestamp");
|
|
360
|
+
const evidence = args[5] || "";
|
|
361
|
+
if (!isObject(data.phases)) data.phases = {};
|
|
362
|
+
if (!isObject(data.phases[phase])) data.phases[phase] = {};
|
|
363
|
+
data.phases[phase].status = status;
|
|
364
|
+
data.phases[phase].ts = ts;
|
|
365
|
+
if (evidence !== "") data.phases[phase].evidence = evidence;
|
|
366
|
+
data.phase = phase;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case "ops-refresh-pending": {
|
|
370
|
+
const startPhase = required(args, 2, "start_phase");
|
|
371
|
+
const ts = required(args, 3, "timestamp");
|
|
372
|
+
for (const key of ["password", "access_token", "agent_token", "agent_room_id", "user_confirmations", "runtime_checks"]) {
|
|
373
|
+
delete data[key];
|
|
374
|
+
}
|
|
375
|
+
data.agent_install_status = "refresh_pending";
|
|
376
|
+
data.phase = startPhase;
|
|
377
|
+
if (!isObject(data.phases)) data.phases = {};
|
|
378
|
+
if (startPhase === "S4_BOOTSTRAP_STACK") {
|
|
379
|
+
data.phases.S4_BOOTSTRAP_STACK = {
|
|
380
|
+
status: "pending",
|
|
381
|
+
ts,
|
|
382
|
+
evidence: "existing node operation requires fresh health check"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
data.phases.S5_INIT_TOKENS = {
|
|
386
|
+
status: "pending",
|
|
387
|
+
ts,
|
|
388
|
+
evidence: "existing node operation requires fresh bootstrap credentials"
|
|
389
|
+
};
|
|
390
|
+
data.phases.S6_WIRE_LOCAL = {
|
|
391
|
+
status: "pending",
|
|
392
|
+
ts,
|
|
393
|
+
evidence: "existing node operation requires local credentials and MCP refresh"
|
|
394
|
+
};
|
|
395
|
+
data.phases.S7_VERIFY_E2E = {
|
|
396
|
+
status: "pending",
|
|
397
|
+
ts,
|
|
398
|
+
evidence: "existing node operation requires fresh verification"
|
|
399
|
+
};
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
case "delete": {
|
|
403
|
+
deletePath(data, required(args, 2, "path"));
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case "destroy-evidence": {
|
|
407
|
+
const key = required(args, 2, "key");
|
|
408
|
+
if (!isObject(data.destroy_evidence)) data.destroy_evidence = {};
|
|
409
|
+
data.destroy_evidence[key] = {
|
|
410
|
+
status: required(args, 3, "status"),
|
|
411
|
+
detail: args[4] || "",
|
|
412
|
+
checked_at: required(args, 5, "checked_at")
|
|
413
|
+
};
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
default:
|
|
417
|
+
usage(`unknown mutate preset: ${preset}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
atomicWriteJson(file, data);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function cmdOperationReport(args) {
|
|
424
|
+
const operation = required(args, 0, "operation");
|
|
425
|
+
const status = required(args, 1, "status");
|
|
426
|
+
const stateFile = required(args, 2, "state");
|
|
427
|
+
const generatedAt = required(args, 3, "generated_at");
|
|
428
|
+
const st = readJsonFile(stateFile);
|
|
429
|
+
process.stdout.write(`${JSON.stringify(buildOperationReport(operation, status, stateFile, generatedAt, st), null, 2)}\n`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildOperationReport(operation, status, stateFile, generatedAt, st) {
|
|
433
|
+
const redactedStatus = stringValue(st.password).length > 0 ? "available_in_state_password_field_redacted" : "missing";
|
|
434
|
+
const phaseStatuses = {};
|
|
435
|
+
for (const [key, value] of Object.entries(objectValue(st.phases))) {
|
|
436
|
+
phaseStatuses[key] = stringValue(value?.status || "unknown");
|
|
437
|
+
}
|
|
438
|
+
const userGate = (gate, fallback) => st.user_confirmations?.[gate]?.status || fallback;
|
|
439
|
+
const localRefreshStatus = st.agent_install_status === "refresh_pending" ? "refresh_pending" : "current_or_not_recorded";
|
|
440
|
+
const billable = compact([
|
|
441
|
+
stringValue(st.resources?.instance_id) ? `EC2 ${st.resources.instance_id}` : "",
|
|
442
|
+
stringValue(st.resources?.root_volume_id) ? `EBS root volume ${st.resources.root_volume_id}` : "",
|
|
443
|
+
stringValue(st.resources?.public_ip) ? `public IPv4 ${st.resources.public_ip}` : "",
|
|
444
|
+
stringValue(st.resources?.eip_id) ? `Elastic IP ${st.resources.eip_id}` : "",
|
|
445
|
+
stringValue(st.resources?.route53_zone_id) ? `Route53 hosted zone ${st.resources.route53_zone_id}` : ""
|
|
446
|
+
]);
|
|
447
|
+
const destroyStatus = (key) => st.destroy_evidence?.[key]?.status || "not_checked";
|
|
448
|
+
const statusNotIn = (value, safe) => !safe.includes(value);
|
|
449
|
+
const destroyBillableResidue = compact([
|
|
450
|
+
stringValue(st.resources?.instance_id) && statusNotIn(destroyStatus("ec2_instance"), ["terminated", "not_found", "skipped"])
|
|
451
|
+
? `EC2 ${st.resources.instance_id} status=${destroyStatus("ec2_instance")}` : "",
|
|
452
|
+
stringValue(st.resources?.root_volume_id) && statusNotIn(destroyStatus("ebs_root_volume"), ["deleted", "skipped"])
|
|
453
|
+
? `EBS root volume ${st.resources.root_volume_id} status=${destroyStatus("ebs_root_volume")}` : "",
|
|
454
|
+
stringValue(st.resources?.eip_id) && statusNotIn(destroyStatus("elastic_ip"), ["released", "skipped"])
|
|
455
|
+
? `Elastic IP ${st.resources.eip_id} status=${destroyStatus("elastic_ip")}` : "",
|
|
456
|
+
stringValue(st.resources?.route53_zone_id) && statusNotIn(destroyStatus("route53_hosted_zone"), ["deleted", "skipped"])
|
|
457
|
+
? `Route53 hosted zone ${st.resources.route53_zone_id} status=${destroyStatus("route53_hosted_zone")}` : ""
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
const report = {
|
|
461
|
+
operation_type: operation,
|
|
462
|
+
status,
|
|
463
|
+
generated_at: generatedAt,
|
|
464
|
+
domain: st.domain || "",
|
|
465
|
+
service_id: st.agent_service_id || st.domain || "",
|
|
466
|
+
service_dir: st.agent_service_dir || "",
|
|
467
|
+
state_json: stateFile,
|
|
468
|
+
delivery: {
|
|
469
|
+
app_domain: st.domain || "",
|
|
470
|
+
product_completion_status: status,
|
|
471
|
+
init_code_status: redactedStatus,
|
|
472
|
+
init_code_secret_redacted: true,
|
|
473
|
+
user_path: "enter app_domain and the eight-digit initialization code in the App"
|
|
474
|
+
},
|
|
475
|
+
agent: {
|
|
476
|
+
node_id: st.agent_node_id || "",
|
|
477
|
+
room_id: st.agent_room_id || "",
|
|
478
|
+
runtime: st.agent_runtime || "unknown",
|
|
479
|
+
service_id: st.agent_service_id || st.domain || "",
|
|
480
|
+
credentials_file: st.agent_credentials_file || ""
|
|
481
|
+
},
|
|
482
|
+
gates: {
|
|
483
|
+
automated: phaseStatuses,
|
|
484
|
+
user_confirmation: {
|
|
485
|
+
app_initialization: userGate("app_initialization", "pending_user_confirmation"),
|
|
486
|
+
real_chat: userGate("real_chat", "pending_user_confirmation"),
|
|
487
|
+
agent_mcp_runtime: userGate("agent_mcp_runtime", "pending_runtime_confirmation")
|
|
488
|
+
},
|
|
489
|
+
user_confirmation_details: {
|
|
490
|
+
app_initialization: userGateDetail(st, "app_initialization", "pending_user_confirmation"),
|
|
491
|
+
real_chat: userGateDetail(st, "real_chat", "pending_user_confirmation"),
|
|
492
|
+
agent_mcp_runtime: userGateDetail(st, "agent_mcp_runtime", "pending_runtime_confirmation")
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
runtime_checks: {
|
|
496
|
+
summary: st.runtime_checks?.summary || { status: "not_run" },
|
|
497
|
+
connect_daemon: st.runtime_checks?.connect_daemon || { status: "not_run" },
|
|
498
|
+
mcp_doctor: st.runtime_checks?.mcp_doctor || { status: "not_run" },
|
|
499
|
+
mcp_smoke: st.runtime_checks?.mcp_smoke || { status: "not_run" },
|
|
500
|
+
mcp_tools: st.runtime_checks?.mcp_tools || { status: "not_run" }
|
|
501
|
+
},
|
|
502
|
+
credentials: {
|
|
503
|
+
status: localRefreshStatus,
|
|
504
|
+
credentials_file: st.agent_credentials_file || "",
|
|
505
|
+
contains_secrets: true,
|
|
506
|
+
values_redacted: true
|
|
507
|
+
},
|
|
508
|
+
connect: {
|
|
509
|
+
package: st.cc_connect_npm_package || "direxio-connent@latest",
|
|
510
|
+
agent: st.cc_connect_agent || "",
|
|
511
|
+
config: st.cc_connect_config || "",
|
|
512
|
+
install_status: st.agent_install_status || ""
|
|
513
|
+
},
|
|
514
|
+
mcp: {
|
|
515
|
+
status: localRefreshStatus,
|
|
516
|
+
package: st.mcp_npm_package || "direxio-mcp@latest",
|
|
517
|
+
server_name: st.mcp_server_name || "",
|
|
518
|
+
config_dir: st.mcp_config_dir || "",
|
|
519
|
+
codex: st.mcp_codex_config || "",
|
|
520
|
+
openclaw: st.mcp_openclaw_config || "",
|
|
521
|
+
hermes: st.mcp_hermes_config || "",
|
|
522
|
+
doctor: st.mcp_doctor_command || ""
|
|
523
|
+
},
|
|
524
|
+
resources: {
|
|
525
|
+
region: st.region || "",
|
|
526
|
+
domain_mode: st.domain_mode || "",
|
|
527
|
+
instance_type: st.instance_type || "",
|
|
528
|
+
instance_id: st.resources?.instance_id || "",
|
|
529
|
+
root_volume_id: st.resources?.root_volume_id || "",
|
|
530
|
+
public_ip: st.resources?.public_ip || "",
|
|
531
|
+
eip_id: st.resources?.eip_id || "",
|
|
532
|
+
route53_zone_id: st.resources?.route53_zone_id || "",
|
|
533
|
+
route53_zone_name: st.resources?.route53_zone_name || "",
|
|
534
|
+
route53_zone_created_by_deployer: st.resources?.route53_zone_created_by_deployer || "",
|
|
535
|
+
route53_name_servers: st.resources?.route53_name_servers || "",
|
|
536
|
+
route53_existing_a_value: st.resources?.route53_existing_a_value || "",
|
|
537
|
+
route53_pending_a_value: st.resources?.route53_pending_a_value || "",
|
|
538
|
+
route53_overwrite_confirmed: st.resources?.route53_overwrite_confirmed || "",
|
|
539
|
+
sg_id: st.resources?.sg_id || "",
|
|
540
|
+
key_name: st.resources?.key_name || ""
|
|
541
|
+
},
|
|
542
|
+
billing: {
|
|
543
|
+
keeps_billing_until_destroy: operation !== "destroy",
|
|
544
|
+
recorded_billable_resources: billable,
|
|
545
|
+
cost_estimate: typeof st.cost_estimate === "undefined" ? null : st.cost_estimate,
|
|
546
|
+
destroy_cleanup_status: operation !== "destroy"
|
|
547
|
+
? "not_destroy"
|
|
548
|
+
: destroyBillableResidue.length === 0
|
|
549
|
+
? "no_recorded_billable_resource_residue"
|
|
550
|
+
: "possible_billable_resource_residue",
|
|
551
|
+
possible_remaining_billable_resources: operation === "destroy" ? destroyBillableResidue : []
|
|
552
|
+
},
|
|
553
|
+
security: {
|
|
554
|
+
secrets_included: false,
|
|
555
|
+
values_redacted: true,
|
|
556
|
+
root_access_key_allowed: true,
|
|
557
|
+
temporary_iam_cleanup_required: true,
|
|
558
|
+
temporary_iam_cleanup_action: "if a temporary DirexioDeployer access key was used, delete or disable it after deployment, or reduce it to a maintenance-only policy"
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
if (operation === "destroy") {
|
|
563
|
+
report.destroy = {
|
|
564
|
+
resources_processed_from_state: true,
|
|
565
|
+
user_managed_dns_not_removed: true,
|
|
566
|
+
purchased_domain_not_removed: true,
|
|
567
|
+
local_service_dir: st.agent_service_dir || "",
|
|
568
|
+
evidence: st.destroy_evidence || {}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return report;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function userGateDetail(st, gate, fallback) {
|
|
576
|
+
const gateState = st.user_confirmations?.[gate] || {};
|
|
577
|
+
const originalEvidence = stringValue(gateState.evidence);
|
|
578
|
+
const evidence = redactText(originalEvidence, st);
|
|
579
|
+
const detail = {
|
|
580
|
+
status: gateState.status || fallback,
|
|
581
|
+
ts: gateState.ts || "",
|
|
582
|
+
evidence,
|
|
583
|
+
evidence_redacted: evidence !== originalEvidence
|
|
584
|
+
};
|
|
585
|
+
if (gate === "agent_mcp_runtime") {
|
|
586
|
+
detail.runtime_summary_status = gateState.runtime_summary_status || "";
|
|
587
|
+
detail.runtime_probe_confirmed = gateState.runtime_probe_confirmed || false;
|
|
588
|
+
}
|
|
589
|
+
return detail;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function redactText(value, st) {
|
|
593
|
+
let result = stringValue(value);
|
|
594
|
+
for (const secret of [
|
|
595
|
+
st.password,
|
|
596
|
+
st.access_token,
|
|
597
|
+
st.agent_token,
|
|
598
|
+
st.matrix_access_token,
|
|
599
|
+
st.owner_access_token,
|
|
600
|
+
st.aws_secret_access_key,
|
|
601
|
+
st.aws_session_token
|
|
602
|
+
]) {
|
|
603
|
+
const text = stringValue(secret);
|
|
604
|
+
if (text.length > 0) result = result.split(text).join("<redacted>");
|
|
605
|
+
}
|
|
606
|
+
return result.replace(/[0-9]{8,}/g, "<redacted>");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function readJsonFile(file) {
|
|
610
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function readJsonFileOrEmptyObject(file) {
|
|
614
|
+
const raw = readFileSync(file, "utf8");
|
|
615
|
+
return raw.trim().length === 0 ? {} : JSON.parse(raw);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function readJsonStdin() {
|
|
619
|
+
return JSON.parse(readFileSync(0, "utf8"));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function atomicWriteJson(file, data) {
|
|
623
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
624
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
625
|
+
renameSync(tmp, file);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function getPath(data, jsonPath, fallback = "") {
|
|
629
|
+
const result = resolvePath(data, jsonPath);
|
|
630
|
+
return result.exists ? result.value : fallback;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function hasPath(data, jsonPath) {
|
|
634
|
+
return resolvePath(data, jsonPath).exists;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function setPath(data, jsonPath, value) {
|
|
638
|
+
const segments = parsePath(jsonPath);
|
|
639
|
+
let current = data;
|
|
640
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
641
|
+
const segment = segments[i];
|
|
642
|
+
if (!isObject(current[segment])) current[segment] = {};
|
|
643
|
+
current = current[segment];
|
|
644
|
+
}
|
|
645
|
+
current[segments[segments.length - 1]] = value;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function deletePath(data, jsonPath) {
|
|
649
|
+
const segments = parsePath(jsonPath);
|
|
650
|
+
let current = data;
|
|
651
|
+
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
652
|
+
current = current?.[segments[i]];
|
|
653
|
+
if (!isObject(current)) return;
|
|
654
|
+
}
|
|
655
|
+
delete current[segments[segments.length - 1]];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function resolvePath(data, jsonPath) {
|
|
659
|
+
if (jsonPath === "." || jsonPath === "") return { exists: true, value: data };
|
|
660
|
+
let current = data;
|
|
661
|
+
for (const segment of parsePath(jsonPath)) {
|
|
662
|
+
if (!isObject(current) && !Array.isArray(current)) return { exists: false, value: undefined };
|
|
663
|
+
if (!(segment in current)) return { exists: false, value: undefined };
|
|
664
|
+
current = current[segment];
|
|
665
|
+
}
|
|
666
|
+
return { exists: true, value: current };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function parsePath(jsonPath) {
|
|
670
|
+
return String(jsonPath)
|
|
671
|
+
.split(".")
|
|
672
|
+
.filter((segment) => segment.length > 0);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function parsePairs(args) {
|
|
676
|
+
return args.map((pair) => {
|
|
677
|
+
const index = pair.indexOf("=");
|
|
678
|
+
if (index < 0) usage(`expected key=value, got: ${pair}`);
|
|
679
|
+
return [pair.slice(0, index), pair.slice(index + 1)];
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function parseScalar(value) {
|
|
684
|
+
if (value === "true") return true;
|
|
685
|
+
if (value === "false") return false;
|
|
686
|
+
if (value === "null") return null;
|
|
687
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
688
|
+
if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith("[") && value.endsWith("]"))) {
|
|
689
|
+
return JSON.parse(value);
|
|
690
|
+
}
|
|
691
|
+
return value;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function buildPricingEstimate(args) {
|
|
695
|
+
const [
|
|
696
|
+
pricingStatus,
|
|
697
|
+
region,
|
|
698
|
+
location,
|
|
699
|
+
instanceType,
|
|
700
|
+
domainMode,
|
|
701
|
+
ec2Source,
|
|
702
|
+
gp3Source,
|
|
703
|
+
ipv4Source,
|
|
704
|
+
warningsJson,
|
|
705
|
+
hours,
|
|
706
|
+
diskGb,
|
|
707
|
+
ec2Hourly,
|
|
708
|
+
ec2Monthly,
|
|
709
|
+
gp3Rate,
|
|
710
|
+
gp3Monthly,
|
|
711
|
+
ipv4Hourly,
|
|
712
|
+
ipv4Monthly,
|
|
713
|
+
route53Monthly
|
|
714
|
+
] = args;
|
|
715
|
+
const components = {
|
|
716
|
+
ec2_instance: {
|
|
717
|
+
instance_type: required(args, 3, "instance_type"),
|
|
718
|
+
hourly_usd: numberValue(ec2Hourly),
|
|
719
|
+
monthly_usd: numberValue(ec2Monthly),
|
|
720
|
+
source: ec2Source
|
|
721
|
+
},
|
|
722
|
+
ebs_gp3: {
|
|
723
|
+
storage_gb: numberValue(diskGb),
|
|
724
|
+
gb_month_usd: numberValue(gp3Rate),
|
|
725
|
+
monthly_usd: numberValue(gp3Monthly),
|
|
726
|
+
source: gp3Source
|
|
727
|
+
},
|
|
728
|
+
public_ipv4: {
|
|
729
|
+
hourly_usd: numberValue(ipv4Hourly),
|
|
730
|
+
monthly_usd: numberValue(ipv4Monthly),
|
|
731
|
+
billed_even_when_attached: true,
|
|
732
|
+
source: ipv4Source
|
|
733
|
+
},
|
|
734
|
+
route53_hosted_zone: {
|
|
735
|
+
monthly_usd: numberValue(route53Monthly),
|
|
736
|
+
included: domainMode === "route53"
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
const total = components.ec2_instance.monthly_usd +
|
|
740
|
+
components.ebs_gp3.monthly_usd +
|
|
741
|
+
components.public_ipv4.monthly_usd +
|
|
742
|
+
components.route53_hosted_zone.monthly_usd;
|
|
743
|
+
return {
|
|
744
|
+
pricing_status: pricingStatus,
|
|
745
|
+
region,
|
|
746
|
+
location,
|
|
747
|
+
hours_per_month: numberValue(hours),
|
|
748
|
+
warnings: unique(JSON.parse(warningsJson || "[]")),
|
|
749
|
+
components,
|
|
750
|
+
notes: [
|
|
751
|
+
"Estimate excludes data transfer, TURN relay traffic, domain registration, taxes, and AWS credit eligibility.",
|
|
752
|
+
"Public IPv4 is billed hourly by AWS even when attached to a running instance.",
|
|
753
|
+
"AWS credits may reduce charges only when the account, plan, region, and service usage are eligible; verify in AWS Billing Console."
|
|
754
|
+
],
|
|
755
|
+
recommendations: [
|
|
756
|
+
"Set an AWS Budget or billing alert before leaving the node running.",
|
|
757
|
+
"Review AWS Billing Console after deployment and after destroy to confirm actual charges and remaining credits."
|
|
758
|
+
],
|
|
759
|
+
total_monthly_usd: Math.round(total * 100) / 100
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function normalizeBootstrap(file, domain) {
|
|
764
|
+
const src = readJsonFile(file);
|
|
765
|
+
const asUrl = `https://${domain}`;
|
|
766
|
+
return {
|
|
767
|
+
...src,
|
|
768
|
+
domain: src.domain || domain,
|
|
769
|
+
as_url: src.as_url || asUrl,
|
|
770
|
+
p2p_url: src.p2p_url || asUrl,
|
|
771
|
+
user_id: src.user_id || src.owner_user_id || "",
|
|
772
|
+
bot_mxid: src.bot_mxid || src.owner_user_id || src.user_id || `@owner:${domain}`,
|
|
773
|
+
access_token: src.access_token || "",
|
|
774
|
+
agent_token: src.agent_token || "",
|
|
775
|
+
agent_room_id: src.agent_room_id || ""
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function numberValue(value) {
|
|
780
|
+
const number = Number(value);
|
|
781
|
+
return Number.isFinite(number) ? number : 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function unique(values) {
|
|
785
|
+
return Array.from(new Set(values));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function printValue(value) {
|
|
789
|
+
if (value === null || typeof value === "undefined") {
|
|
790
|
+
process.stdout.write("\n");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (typeof value === "object") {
|
|
794
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
process.stdout.write(`${String(value)}\n`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function printLine(value) {
|
|
801
|
+
process.stdout.write(`${value}\n`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function formatEntryValue(value) {
|
|
805
|
+
if (value === null || typeof value === "undefined") return "";
|
|
806
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
807
|
+
return String(value);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function jsonType(value) {
|
|
811
|
+
if (Array.isArray(value)) return "array";
|
|
812
|
+
if (value === null) return "null";
|
|
813
|
+
if (typeof value === "undefined") return "missing";
|
|
814
|
+
return typeof value;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function required(args, index, name) {
|
|
818
|
+
const value = args[index];
|
|
819
|
+
if (typeof value === "undefined") usage(`missing ${name}`);
|
|
820
|
+
return value;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function isObject(value) {
|
|
824
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function usage(message) {
|
|
828
|
+
throw new Error(`${message}\nUsage: scripts/json.mjs <get|stdin-get|assert|stdin-assert|check|entries|stdin-tsv|stdin-join|stdin-route53-a-values|stdin-route53-a-present|stdin-price-usd|length|type|build|mutate|operation-report|valid> ...`);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function compact(values) {
|
|
832
|
+
return values.filter((value) => String(value || "").length > 0);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function objectValue(value) {
|
|
836
|
+
return isObject(value) ? value : {};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function stringValue(value) {
|
|
840
|
+
return typeof value === "undefined" || value === null ? "" : String(value);
|
|
841
|
+
}
|