dxcomplete 0.2.1 → 0.2.2
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/.env.example +0 -7
- package/README.md +17 -45
- package/dist/cli.js +0 -22
- package/dist/validate.js +10 -26
- package/docs/model.md +3 -3
- package/docs/taxonomy.md +1 -1
- package/package.json +23 -23
- package/templates/process/README.md +1 -1
- package/dist/http/service.d.ts +0 -7
- package/dist/http/service.js +0 -725
- package/dist/mcp/docs.d.ts +0 -114
- package/dist/mcp/docs.js +0 -626
- package/dist/mcp/server.d.ts +0 -20
- package/dist/mcp/server.js +0 -3059
- package/dist/runtime/auth.d.ts +0 -162
- package/dist/runtime/auth.js +0 -394
- package/dist/runtime/check.d.ts +0 -7
- package/dist/runtime/check.js +0 -16
- package/dist/runtime/config.d.ts +0 -17
- package/dist/runtime/config.js +0 -93
- package/dist/runtime/mongo.d.ts +0 -9
- package/dist/runtime/mongo.js +0 -56
- package/dist/runtime/records.d.ts +0 -427
- package/dist/runtime/records.js +0 -2092
- package/scripts/check-env-surface.mjs +0 -136
- package/scripts/check-public-copy.mjs +0 -263
- package/scripts/check-service-boundary.mjs +0 -63
- package/scripts/runtime-work-order.mjs +0 -506
- package/scripts/smoke-mcp-http.mjs +0 -4026
- package/src/cli.ts +0 -268
- package/src/http/server.ts +0 -314
- package/src/http/service.ts +0 -934
- package/src/init.ts +0 -262
- package/src/install-manifest.ts +0 -144
- package/src/mcp/docs.ts +0 -777
- package/src/mcp/server.ts +0 -4580
- package/src/package-root.ts +0 -31
- package/src/runtime/actor.ts +0 -61
- package/src/runtime/auth.ts +0 -673
- package/src/runtime/check.ts +0 -18
- package/src/runtime/config.ts +0 -128
- package/src/runtime/mongo.ts +0 -89
- package/src/runtime/records.ts +0 -3205
- package/src/runtime/workspace.ts +0 -155
- package/src/upgrade.ts +0 -356
- package/src/validate.ts +0 -141
- package/src/version.ts +0 -16
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { connectRuntime } from "../dist/runtime/mongo.js";
|
|
5
|
-
import {
|
|
6
|
-
appendDxcompleteTicketReply,
|
|
7
|
-
archiveRecord,
|
|
8
|
-
COLLECTION_NAMES,
|
|
9
|
-
DXCOMPLETE_TICKET_COLLECTION_NAME
|
|
10
|
-
} from "../dist/runtime/records.js";
|
|
11
|
-
import { ensureWorkspaceBootstrap } from "../dist/runtime/auth.js";
|
|
12
|
-
import { loadWorkspaceConfig } from "../dist/runtime/workspace.js";
|
|
13
|
-
|
|
14
|
-
const defaultEnvFile = ".env.local";
|
|
15
|
-
const runtimeActorId = "dxcomplete-runtime";
|
|
16
|
-
|
|
17
|
-
const args = parseArgs(process.argv.slice(2));
|
|
18
|
-
|
|
19
|
-
if (args.help || (!args.planPath && !args.modulePath)) {
|
|
20
|
-
printHelp();
|
|
21
|
-
process.exit(args.help ? 0 : 1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const startedAt = Date.now();
|
|
25
|
-
const runtime = await connectRuntime({ envFile: args.envFile ?? defaultEnvFile });
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const workspaceConfig = await maybeLoadWorkspaceConfig(args.workspaceConfigPath);
|
|
29
|
-
const context = createContext(runtime, workspaceConfig);
|
|
30
|
-
const results = [];
|
|
31
|
-
|
|
32
|
-
if (args.planPath) {
|
|
33
|
-
results.push(await runPlan(await readPlan(args.planPath), context));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (args.modulePath) {
|
|
37
|
-
results.push(await runModule(args.modulePath, context));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
console.log(
|
|
41
|
-
JSON.stringify(
|
|
42
|
-
{
|
|
43
|
-
ok: true,
|
|
44
|
-
databaseName: runtime.config.databaseName,
|
|
45
|
-
workspaceId: workspaceConfig?.workspaceId,
|
|
46
|
-
elapsedSeconds: elapsedSeconds(startedAt),
|
|
47
|
-
results
|
|
48
|
-
},
|
|
49
|
-
null,
|
|
50
|
-
2
|
|
51
|
-
)
|
|
52
|
-
);
|
|
53
|
-
} finally {
|
|
54
|
-
await runtime.close();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseArgs(argv) {
|
|
58
|
-
const parsed = {
|
|
59
|
-
help: false
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
63
|
-
const arg = argv[index];
|
|
64
|
-
|
|
65
|
-
if (arg === "--help" || arg === "-h") {
|
|
66
|
-
parsed.help = true;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (arg === "--plan") {
|
|
71
|
-
parsed.planPath = readRequiredArg(argv, index, "--plan");
|
|
72
|
-
index += 1;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (arg.startsWith("--plan=")) {
|
|
77
|
-
parsed.planPath = arg.slice("--plan=".length);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (arg === "--module") {
|
|
82
|
-
parsed.modulePath = readRequiredArg(argv, index, "--module");
|
|
83
|
-
index += 1;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (arg.startsWith("--module=")) {
|
|
88
|
-
parsed.modulePath = arg.slice("--module=".length);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (arg === "--env") {
|
|
93
|
-
parsed.envFile = readRequiredArg(argv, index, "--env");
|
|
94
|
-
index += 1;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (arg.startsWith("--env=")) {
|
|
99
|
-
parsed.envFile = arg.slice("--env=".length);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (arg === "--workspace-config") {
|
|
104
|
-
parsed.workspaceConfigPath = readRequiredArg(argv, index, "--workspace-config");
|
|
105
|
-
index += 1;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (arg.startsWith("--workspace-config=")) {
|
|
110
|
-
parsed.workspaceConfigPath = arg.slice("--workspace-config=".length);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return parsed;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function readRequiredArg(argv, index, name) {
|
|
121
|
-
const value = argv[index + 1];
|
|
122
|
-
if (!value) {
|
|
123
|
-
throw new Error(`${name} requires a value.`);
|
|
124
|
-
}
|
|
125
|
-
return value;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function maybeLoadWorkspaceConfig(configPath) {
|
|
129
|
-
try {
|
|
130
|
-
return await loadWorkspaceConfig(configPath ? { configPath } : {});
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (configPath) {
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function createContext(runtime, workspaceConfig) {
|
|
140
|
-
return {
|
|
141
|
-
runtime,
|
|
142
|
-
db: runtime.db,
|
|
143
|
-
workspaceConfig,
|
|
144
|
-
actorId: runtimeActorId,
|
|
145
|
-
helpers: {
|
|
146
|
-
appendDxcompleteTicketReply,
|
|
147
|
-
archiveRecord,
|
|
148
|
-
collectionNames: COLLECTION_NAMES,
|
|
149
|
-
dxcompleteTicketCollectionName: DXCOMPLETE_TICKET_COLLECTION_NAME,
|
|
150
|
-
ensureWorkspaceBootstrap,
|
|
151
|
-
getTicketOwnerActorId,
|
|
152
|
-
needsReplySummary,
|
|
153
|
-
verifyNoLinksToType
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function readPlan(planPath) {
|
|
159
|
-
const absolutePath = path.resolve(planPath);
|
|
160
|
-
return JSON.parse(await readFile(absolutePath, "utf8"));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function runPlan(plan, context) {
|
|
164
|
-
if (!plan || typeof plan !== "object" || Array.isArray(plan)) {
|
|
165
|
-
throw new Error("Plan must be a JSON object.");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const started = Date.now();
|
|
169
|
-
const result = {
|
|
170
|
-
kind: "plan",
|
|
171
|
-
operations: []
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
if (plan.ensureWorkspaceBootstrap) {
|
|
175
|
-
if (!context.workspaceConfig) {
|
|
176
|
-
throw new Error("ensureWorkspaceBootstrap requires dxcomplete/workspace.json or --workspace-config.");
|
|
177
|
-
}
|
|
178
|
-
await ensureWorkspaceBootstrap(context.db, context.workspaceConfig, context.actorId);
|
|
179
|
-
result.operations.push({
|
|
180
|
-
operation: "ensureWorkspaceBootstrap",
|
|
181
|
-
workspaceId: context.workspaceConfig.workspaceId
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (plan.ticketSummary) {
|
|
186
|
-
result.operations.push({
|
|
187
|
-
operation: "ticketSummary",
|
|
188
|
-
...(await needsReplySummary(context.db, plan.ticketSummary))
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (plan.getTickets) {
|
|
193
|
-
result.operations.push({
|
|
194
|
-
operation: "getTickets",
|
|
195
|
-
tickets: await getTickets(context.db, readStringArray(plan.getTickets, "getTickets"))
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (plan.appendTicketReplies) {
|
|
200
|
-
result.operations.push({
|
|
201
|
-
operation: "appendTicketReplies",
|
|
202
|
-
replies: await appendTicketReplies(context, readArray(plan.appendTicketReplies, "appendTicketReplies"))
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (plan.archiveRecords) {
|
|
207
|
-
result.operations.push({
|
|
208
|
-
operation: "archiveRecords",
|
|
209
|
-
archived: await archiveRecords(context, readArray(plan.archiveRecords, "archiveRecords"))
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (plan.verifyNoLinksToType) {
|
|
214
|
-
result.operations.push({
|
|
215
|
-
operation: "verifyNoLinksToType",
|
|
216
|
-
checks: await runNoLinkChecks(context, readArray(plan.verifyNoLinksToType, "verifyNoLinksToType"))
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
result.elapsedSeconds = elapsedSeconds(started);
|
|
221
|
-
return result;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function runModule(modulePath, context) {
|
|
225
|
-
const started = Date.now();
|
|
226
|
-
const moduleUrl = pathToFileURL(path.resolve(modulePath)).href;
|
|
227
|
-
const module = await import(moduleUrl);
|
|
228
|
-
|
|
229
|
-
if (typeof module.run !== "function") {
|
|
230
|
-
throw new Error(`${modulePath} must export async function run(context).`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
kind: "module",
|
|
235
|
-
modulePath,
|
|
236
|
-
result: await module.run(context),
|
|
237
|
-
elapsedSeconds: elapsedSeconds(started)
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function needsReplySummary(db, options = {}) {
|
|
242
|
-
const limit = readOptionalLimit(options.limit, 100);
|
|
243
|
-
const filter = {};
|
|
244
|
-
|
|
245
|
-
if (!options.includeArchived) {
|
|
246
|
-
filter.archivedAt = { $exists: false };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const tickets = await db
|
|
250
|
-
.collection(DXCOMPLETE_TICKET_COLLECTION_NAME)
|
|
251
|
-
.find(filter, { projection: { _id: 1, title: 1, updatedAt: 1, "fields.ownerActorId": 1, "fields.entries": 1 } })
|
|
252
|
-
.sort({ updatedAt: -1 })
|
|
253
|
-
.limit(limit)
|
|
254
|
-
.toArray();
|
|
255
|
-
|
|
256
|
-
const rows = tickets.map(ticketNeedsReplyRow);
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
activeCount: tickets.length,
|
|
260
|
-
needsReplyCount: rows.filter((row) => row.needsReply).length,
|
|
261
|
-
needsReply: rows.filter((row) => row.needsReply)
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function getTickets(db, ids) {
|
|
266
|
-
const tickets = await db
|
|
267
|
-
.collection(DXCOMPLETE_TICKET_COLLECTION_NAME)
|
|
268
|
-
.find({ _id: { $in: ids } })
|
|
269
|
-
.toArray();
|
|
270
|
-
const byId = new Map(tickets.map((ticket) => [ticket._id, ticket]));
|
|
271
|
-
|
|
272
|
-
return ids.map((id) => {
|
|
273
|
-
const ticket = byId.get(id);
|
|
274
|
-
if (!ticket) {
|
|
275
|
-
return { id, found: false };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const entries = ticket.fields?.entries ?? [];
|
|
279
|
-
return {
|
|
280
|
-
id,
|
|
281
|
-
found: true,
|
|
282
|
-
title: ticket.title,
|
|
283
|
-
ownerActorId: ticket.fields?.ownerActorId,
|
|
284
|
-
updatedAt: ticket.updatedAt,
|
|
285
|
-
entries: entries.length,
|
|
286
|
-
lastEntry: entries.at(-1)
|
|
287
|
-
};
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function appendTicketReplies(context, replies) {
|
|
292
|
-
const results = [];
|
|
293
|
-
|
|
294
|
-
for (const reply of replies) {
|
|
295
|
-
if (!reply || typeof reply !== "object" || Array.isArray(reply)) {
|
|
296
|
-
throw new Error("Each appendTicketReplies entry must be an object.");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const id = readRequiredString(reply.id, "appendTicketReplies[].id");
|
|
300
|
-
const body = await readBody(reply, "appendTicketReplies[]");
|
|
301
|
-
const addressedToActorId =
|
|
302
|
-
typeof reply.addressedToActorId === "string" && reply.addressedToActorId.trim()
|
|
303
|
-
? reply.addressedToActorId.trim()
|
|
304
|
-
: await getTicketOwnerActorId(context.db, id);
|
|
305
|
-
|
|
306
|
-
const updated = await appendDxcompleteTicketReply(
|
|
307
|
-
context.db,
|
|
308
|
-
{
|
|
309
|
-
id,
|
|
310
|
-
body,
|
|
311
|
-
addressedToActorId
|
|
312
|
-
},
|
|
313
|
-
context.actorId
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
results.push({
|
|
317
|
-
id,
|
|
318
|
-
addressedToActorId,
|
|
319
|
-
updatedAt: updated.updatedAt,
|
|
320
|
-
entries: Array.isArray(updated.fields?.entries) ? updated.fields.entries.length : undefined
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return results;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function archiveRecords(context, records) {
|
|
328
|
-
const results = [];
|
|
329
|
-
|
|
330
|
-
for (const record of records) {
|
|
331
|
-
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
332
|
-
throw new Error("Each archiveRecords entry must be an object.");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const updated = await archiveRecord(
|
|
336
|
-
context.db,
|
|
337
|
-
{
|
|
338
|
-
recordType: readRequiredString(record.recordType, "archiveRecords[].recordType"),
|
|
339
|
-
id: readRequiredString(record.id, "archiveRecords[].id"),
|
|
340
|
-
workspaceId: typeof record.workspaceId === "string" ? record.workspaceId : context.workspaceConfig?.workspaceId,
|
|
341
|
-
reason: typeof record.reason === "string" ? record.reason : undefined,
|
|
342
|
-
supersededByType: typeof record.supersededByType === "string" ? record.supersededByType : undefined,
|
|
343
|
-
supersededById: typeof record.supersededById === "string" ? record.supersededById : undefined
|
|
344
|
-
},
|
|
345
|
-
context.actorId
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
results.push({
|
|
349
|
-
recordType: updated.recordType,
|
|
350
|
-
id: updated._id,
|
|
351
|
-
archivedAt: updated.archivedAt
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return results;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function runNoLinkChecks(context, checks) {
|
|
359
|
-
const results = [];
|
|
360
|
-
|
|
361
|
-
for (const check of checks) {
|
|
362
|
-
if (!check || typeof check !== "object" || Array.isArray(check)) {
|
|
363
|
-
throw new Error("Each verifyNoLinksToType entry must be an object.");
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const toType = readRequiredString(check.toType, "verifyNoLinksToType[].toType");
|
|
367
|
-
const collections = check.collections
|
|
368
|
-
? readStringArray(check.collections, "verifyNoLinksToType[].collections")
|
|
369
|
-
: [...COLLECTION_NAMES];
|
|
370
|
-
results.push(await verifyNoLinksToType(context.db, toType, collections));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return results;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function verifyNoLinksToType(db, toType, collections = COLLECTION_NAMES) {
|
|
377
|
-
const residue = [];
|
|
378
|
-
|
|
379
|
-
for (const collectionName of collections) {
|
|
380
|
-
const count = await db.collection(collectionName).countDocuments({ "links.toType": toType });
|
|
381
|
-
if (count > 0) {
|
|
382
|
-
residue.push({ collectionName, count });
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
toType,
|
|
388
|
-
ok: residue.length === 0,
|
|
389
|
-
residue
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function getTicketOwnerActorId(db, id) {
|
|
394
|
-
const ticket = await db
|
|
395
|
-
.collection(DXCOMPLETE_TICKET_COLLECTION_NAME)
|
|
396
|
-
.findOne({ _id: id }, { projection: { "fields.ownerActorId": 1 } });
|
|
397
|
-
|
|
398
|
-
if (!ticket) {
|
|
399
|
-
throw new Error(`DX Complete Ticket not found: ${id}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const ownerActorId = ticket.fields?.ownerActorId;
|
|
403
|
-
if (typeof ownerActorId !== "string" || !ownerActorId) {
|
|
404
|
-
throw new Error(`DX Complete Ticket owner actor is missing: ${id}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return ownerActorId;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function ticketNeedsReplyRow(ticket) {
|
|
411
|
-
const entries = ticket.fields?.entries ?? [];
|
|
412
|
-
const lastReply = [...entries].reverse().find((entry) => entry.direction === "dxcomplete_reply");
|
|
413
|
-
const lastSubmitter = [...entries].reverse().find((entry) => entry.direction === "submitter_entry");
|
|
414
|
-
const needsReply = Boolean(lastSubmitter && (!lastReply || lastSubmitter.createdAt > lastReply.createdAt));
|
|
415
|
-
|
|
416
|
-
return {
|
|
417
|
-
id: ticket._id,
|
|
418
|
-
title: ticket.title,
|
|
419
|
-
ownerActorId: ticket.fields?.ownerActorId,
|
|
420
|
-
updatedAt: ticket.updatedAt,
|
|
421
|
-
entries: entries.length,
|
|
422
|
-
needsReply,
|
|
423
|
-
lastSubmitterAt: lastSubmitter?.createdAt,
|
|
424
|
-
lastReplyAt: lastReply?.createdAt
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function readBody(input, label) {
|
|
429
|
-
if (typeof input.body === "string") {
|
|
430
|
-
return input.body;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (typeof input.bodyFile === "string" && input.bodyFile.trim()) {
|
|
434
|
-
return readFile(path.resolve(input.bodyFile), "utf8");
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
throw new Error(`${label} requires body or bodyFile.`);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function readStringArray(value, label) {
|
|
441
|
-
const array = readArray(value, label);
|
|
442
|
-
return array.map((entry, index) => readRequiredString(entry, `${label}[${index}]`));
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function readArray(value, label) {
|
|
446
|
-
if (!Array.isArray(value)) {
|
|
447
|
-
throw new Error(`${label} must be an array.`);
|
|
448
|
-
}
|
|
449
|
-
return value;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function readRequiredString(value, label) {
|
|
453
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
454
|
-
throw new Error(`${label} must be a non-empty string.`);
|
|
455
|
-
}
|
|
456
|
-
return value.trim();
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function readOptionalLimit(value, fallback) {
|
|
460
|
-
if (value === undefined) {
|
|
461
|
-
return fallback;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (!Number.isInteger(value) || value < 1 || value > 500) {
|
|
465
|
-
throw new Error("limit must be an integer from 1 to 500.");
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return value;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function elapsedSeconds(started) {
|
|
472
|
-
return Number(((Date.now() - started) / 1000).toFixed(2));
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function printHelp() {
|
|
476
|
-
console.log(`runtime-work-order
|
|
477
|
-
|
|
478
|
-
Usage:
|
|
479
|
-
node scripts/runtime-work-order.mjs --plan ./work-order.json
|
|
480
|
-
node scripts/runtime-work-order.mjs --module ./one-off.mjs
|
|
481
|
-
|
|
482
|
-
Purpose:
|
|
483
|
-
Open one runtime connection and batch ticket reads, ticket replies, simple verification,
|
|
484
|
-
and optional one-off work-order modules.
|
|
485
|
-
|
|
486
|
-
Plan shape:
|
|
487
|
-
{
|
|
488
|
-
"ensureWorkspaceBootstrap": true,
|
|
489
|
-
"ticketSummary": { "limit": 100 },
|
|
490
|
-
"getTickets": ["ticket-id"],
|
|
491
|
-
"appendTicketReplies": [
|
|
492
|
-
{ "id": "ticket-id", "body": "Reply body" }
|
|
493
|
-
],
|
|
494
|
-
"archiveRecords": [
|
|
495
|
-
{ "recordType": "requirements", "id": "uuid", "reason": "Archived" }
|
|
496
|
-
],
|
|
497
|
-
"verifyNoLinksToType": [
|
|
498
|
-
{ "toType": "initiatives" }
|
|
499
|
-
]
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
Notes:
|
|
503
|
-
Ticket replies default to the ticket owner actor when addressedToActorId is omitted.
|
|
504
|
-
A module must export async function run(context).
|
|
505
|
-
`);
|
|
506
|
-
}
|