@gaia-ai/addon-remote-drupal 0.6.5 → 0.8.0
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/src/index.d.ts +17 -4
- package/dist/src/index.js +113 -20
- package/package.json +3 -3
package/dist/src/index.d.ts
CHANGED
|
@@ -23,17 +23,30 @@ export declare class DrupalGaiaRemote implements GaiaRemote {
|
|
|
23
23
|
closeTicket(uuid: string): Promise<void>;
|
|
24
24
|
markTicketCleanedUp(uuid: string): Promise<void>;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
* non-empty worktree_path), or
|
|
28
|
-
* runs in
|
|
26
|
+
* UUID + absolute worktree path of the ticket's latest run (highest run id
|
|
27
|
+
* with a non-empty worktree_path), or empty strings when none — the cwd the
|
|
28
|
+
* cleanup command runs in, plus the record its teardown is noted on
|
|
29
|
+
* (GAIA-293). The conductor persists worktree_path on markRunning, so a done
|
|
29
30
|
* ticket's run carries the path even after the run closed.
|
|
31
|
+
*
|
|
32
|
+
* The uuid rides along on the query that already resolves the path, so the
|
|
33
|
+
* reaper learns which run to write to at no extra round-trip.
|
|
30
34
|
*/
|
|
31
|
-
private
|
|
35
|
+
private latestRun;
|
|
36
|
+
appendRunNote(runUuid: string, note: string): Promise<void>;
|
|
32
37
|
resolveTicketByIdentifier(project: string, identifier: string): Promise<{
|
|
33
38
|
uuid: string;
|
|
34
39
|
title: string;
|
|
35
40
|
} | null>;
|
|
36
41
|
private readonly projectUuidCache;
|
|
42
|
+
/**
|
|
43
|
+
* The project *name* behind a project uuid — the inverse of projectUuid.
|
|
44
|
+
*
|
|
45
|
+
* A conductor row references its project; the CLI and the cockpit want to
|
|
46
|
+
* show the name. Best-effort: an unreadable project must not fail a listing,
|
|
47
|
+
* so it degrades to '' and the row still renders.
|
|
48
|
+
*/
|
|
49
|
+
private projectName;
|
|
37
50
|
private projectUuid;
|
|
38
51
|
}
|
|
39
52
|
export declare function drupalRemote(): RemotePlugin;
|
package/dist/src/index.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// `@gaia-ai/core`'s `plugins/remote/drupal.ts`, now its own conductor-surface
|
|
3
|
-
// addon. Core ships no plugin implementations; every mountable lives under
|
|
4
|
-
// `addons/*`. The surface contract comes from `@gaia-ai/conductor/contract`
|
|
5
|
-
// (runtime-light: types + the tiny selectors, no engine).
|
|
1
|
+
import { toUnixSeconds } from '@gaia-ai/core';
|
|
6
2
|
import { resolveAuth } from 'dropsh';
|
|
7
3
|
import { createHttpClient, createJsonApiClient } from 'dropsh/plugin';
|
|
8
4
|
const ACTIVE = ['claimed', 'running'];
|
|
@@ -16,6 +12,22 @@ const ACTIVE = ['claimed', 'running'];
|
|
|
16
12
|
* change, since it asks the workflow definition directly.
|
|
17
13
|
*/
|
|
18
14
|
const TERMINAL = ['done', 'cancelled'];
|
|
15
|
+
/**
|
|
16
|
+
* The process-identity attributes a registration carries, if any (GAIA-232).
|
|
17
|
+
*
|
|
18
|
+
* Spread rather than assigned so an absent value sends no key at all: the
|
|
19
|
+
* server treats a missing key as "leave the stored value alone", which is what
|
|
20
|
+
* lets `poll` and `reap` heartbeat past a running conductor without wiping the
|
|
21
|
+
* pid it needs to be stoppable.
|
|
22
|
+
*/
|
|
23
|
+
function processIdentity(reg) {
|
|
24
|
+
return {
|
|
25
|
+
...(reg.last_pid === undefined ? {} : { last_pid: reg.last_pid }),
|
|
26
|
+
...(reg.conductor_name === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { conductor_name: reg.conductor_name }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
19
31
|
export class DrupalGaiaRemote {
|
|
20
32
|
api;
|
|
21
33
|
constructor(api) {
|
|
@@ -156,6 +168,7 @@ export class DrupalGaiaRemote {
|
|
|
156
168
|
label: reg.label,
|
|
157
169
|
states: reg.states,
|
|
158
170
|
max_parallel: reg.max_parallel,
|
|
171
|
+
...processIdentity(reg),
|
|
159
172
|
},
|
|
160
173
|
relationships: {
|
|
161
174
|
owner_user_id: {
|
|
@@ -186,6 +199,7 @@ export class DrupalGaiaRemote {
|
|
|
186
199
|
max_parallel: reg.max_parallel,
|
|
187
200
|
current_load: load,
|
|
188
201
|
lease_seconds: lease,
|
|
202
|
+
...processIdentity(reg),
|
|
189
203
|
},
|
|
190
204
|
},
|
|
191
205
|
}));
|
|
@@ -215,14 +229,38 @@ export class DrupalGaiaRemote {
|
|
|
215
229
|
col = col.where('owner_user_id.id', '=', await this.api.me());
|
|
216
230
|
}
|
|
217
231
|
const rows = await col.page(200).list();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
// Resolve each distinct project once rather than per row: a machine
|
|
233
|
+
// typically runs several conductors of the same project.
|
|
234
|
+
const projectNames = new Map();
|
|
235
|
+
for (const r of rows) {
|
|
236
|
+
const uuid = r.rel('project_id');
|
|
237
|
+
if (uuid && !projectNames.has(uuid)) {
|
|
238
|
+
projectNames.set(uuid, await this.projectName(uuid));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return rows.map((r) => {
|
|
242
|
+
// Spread the optionals conditionally: an absent field must be absent,
|
|
243
|
+
// not present-and-undefined (exactOptionalPropertyTypes), and a row
|
|
244
|
+
// written before GAIA-232 legitimately carries neither.
|
|
245
|
+
const pid = r.attr('last_pid');
|
|
246
|
+
const name = r.attr('conductor_name');
|
|
247
|
+
return {
|
|
248
|
+
id: r.attr('machine_id') ?? r.id,
|
|
249
|
+
project: projectNames.get(r.rel('project_id') ?? '') ?? '',
|
|
250
|
+
label: r.attr('label') ?? '',
|
|
251
|
+
status: r.attr('status') ?? '',
|
|
252
|
+
// ISO-8601 over the wire, not the unix integer the field name implies.
|
|
253
|
+
lastSeen: toUnixSeconds(r.attr('last_seen')) ?? 0,
|
|
254
|
+
load: r.attr('current_load') ?? 0,
|
|
255
|
+
workspaceRoot: r.attr('workspace_root') ?? '',
|
|
256
|
+
leaseExpiresAt: toUnixSeconds(r.attr('lease_expires_at')) ?? 0,
|
|
257
|
+
maxParallel: r.attr('max_parallel') ?? 0,
|
|
258
|
+
...(typeof pid === 'number' ? { lastPid: pid } : {}),
|
|
259
|
+
...(typeof name === 'string' && name !== ''
|
|
260
|
+
? { conductorName: name }
|
|
261
|
+
: {}),
|
|
262
|
+
};
|
|
263
|
+
});
|
|
226
264
|
}
|
|
227
265
|
async markRunning(uuid, attrs) {
|
|
228
266
|
const t = Math.floor(Date.now() / 1000);
|
|
@@ -317,7 +355,7 @@ export class DrupalGaiaRemote {
|
|
|
317
355
|
branchName: r.attr('branch_name') ?? '',
|
|
318
356
|
state: r.attr('state') ?? '',
|
|
319
357
|
closed: r.attr('closed') ?? false,
|
|
320
|
-
|
|
358
|
+
...(await this.latestRun(r.id)),
|
|
321
359
|
})));
|
|
322
360
|
}
|
|
323
361
|
async closeTicket(uuid) {
|
|
@@ -332,12 +370,16 @@ export class DrupalGaiaRemote {
|
|
|
332
370
|
}); // NO state/closed — teardown flag only, decoupled from the lifecycle.
|
|
333
371
|
}
|
|
334
372
|
/**
|
|
335
|
-
*
|
|
336
|
-
* non-empty worktree_path), or
|
|
337
|
-
* runs in
|
|
373
|
+
* UUID + absolute worktree path of the ticket's latest run (highest run id
|
|
374
|
+
* with a non-empty worktree_path), or empty strings when none — the cwd the
|
|
375
|
+
* cleanup command runs in, plus the record its teardown is noted on
|
|
376
|
+
* (GAIA-293). The conductor persists worktree_path on markRunning, so a done
|
|
338
377
|
* ticket's run carries the path even after the run closed.
|
|
378
|
+
*
|
|
379
|
+
* The uuid rides along on the query that already resolves the path, so the
|
|
380
|
+
* reaper learns which run to write to at no extra round-trip.
|
|
339
381
|
*/
|
|
340
|
-
async
|
|
382
|
+
async latestRun(ticketUuid) {
|
|
341
383
|
const rows = await this.api
|
|
342
384
|
.collection('gaia_run')
|
|
343
385
|
.where('ticket_id.id', '=', ticketUuid)
|
|
@@ -348,10 +390,45 @@ export class DrupalGaiaRemote {
|
|
|
348
390
|
for (const r of rows) {
|
|
349
391
|
const path = r.attr('worktree_path');
|
|
350
392
|
if (path) {
|
|
351
|
-
return path;
|
|
393
|
+
return { runUuid: r.id, worktreePath: path };
|
|
352
394
|
}
|
|
353
395
|
}
|
|
354
|
-
return '';
|
|
396
|
+
return { runUuid: '', worktreePath: '' };
|
|
397
|
+
}
|
|
398
|
+
async appendRunNote(runUuid, note) {
|
|
399
|
+
if (!runUuid) {
|
|
400
|
+
return; // no run to record anything on
|
|
401
|
+
}
|
|
402
|
+
// Read `log` ALONE, through the sparse fieldset every neighbouring query in
|
|
403
|
+
// this file uses. A note append has no business pulling the run's
|
|
404
|
+
// relationships and its dozen other attributes, and `log` is already the
|
|
405
|
+
// heaviest field on the row.
|
|
406
|
+
const run = await this.api
|
|
407
|
+
.collection('gaia_run')
|
|
408
|
+
.where('id', '=', runUuid)
|
|
409
|
+
.fields(['log'])
|
|
410
|
+
.first();
|
|
411
|
+
if (!run) {
|
|
412
|
+
// Refuse rather than guess. With no row read, `existing` below would be ''
|
|
413
|
+
// and the PATCH would REPLACE the transcript with this one diagnostic
|
|
414
|
+
// line — destroying exactly what the note exists to preserve. An
|
|
415
|
+
// unreadable run is the reaper's best-effort catch (it warns and still
|
|
416
|
+
// flags the ticket), never a silent truncation.
|
|
417
|
+
throw new Error(`gaia_run ${runUuid} could not be read — run note not written`);
|
|
418
|
+
}
|
|
419
|
+
// `log` is a `text_long` field, so JSON:API sends it as
|
|
420
|
+
// `{ value, format, processed }` — not a bare string. Reading it through an
|
|
421
|
+
// `attr<string>` cast cannot fail at runtime, it just yields the object, and
|
|
422
|
+
// interpolating that wrote the literal `[object Object]` over the run's
|
|
423
|
+
// transcript. Read the main property, and keep accepting a plain string so a
|
|
424
|
+
// backend (or a double) handing one back still appends correctly.
|
|
425
|
+
const raw = run.attr('log');
|
|
426
|
+
const existing = typeof raw === 'string'
|
|
427
|
+
? raw
|
|
428
|
+
: (raw?.value ?? '');
|
|
429
|
+
await this.api.update('gaia_run', runUuid, {
|
|
430
|
+
attributes: { log: existing ? `${existing}\n${note}` : note },
|
|
431
|
+
}); // NO state/closed — the run is already closed.
|
|
355
432
|
}
|
|
356
433
|
async resolveTicketByIdentifier(project, identifier) {
|
|
357
434
|
// Scope by the project uuid: identifiers (GAIA-nnn) are unique per project,
|
|
@@ -369,6 +446,22 @@ export class DrupalGaiaRemote {
|
|
|
369
446
|
return { uuid: t.id, title: t.attr('title') ?? '' };
|
|
370
447
|
}
|
|
371
448
|
projectUuidCache = new Map();
|
|
449
|
+
/**
|
|
450
|
+
* The project *name* behind a project uuid — the inverse of projectUuid.
|
|
451
|
+
*
|
|
452
|
+
* A conductor row references its project; the CLI and the cockpit want to
|
|
453
|
+
* show the name. Best-effort: an unreadable project must not fail a listing,
|
|
454
|
+
* so it degrades to '' and the row still renders.
|
|
455
|
+
*/
|
|
456
|
+
async projectName(uuid) {
|
|
457
|
+
try {
|
|
458
|
+
const p = await this.api.resource('gaia_project', uuid);
|
|
459
|
+
return p.attr('name') ?? '';
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return '';
|
|
463
|
+
}
|
|
464
|
+
}
|
|
372
465
|
async projectUuid(name) {
|
|
373
466
|
// A project's name→uuid never changes, so cache it: gatherBatch resolves
|
|
374
467
|
// many identifiers per `gaia deployment tickets` call, each of which would
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaia-ai/addon-remote-drupal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "GAIA conductor remote addon: the Drupal/JSON:API control-plane remote.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dropsh": "^0.5.8"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@gaia-ai/conductor": "^0.
|
|
27
|
-
"@gaia-ai/core": "^0.
|
|
26
|
+
"@gaia-ai/conductor": "^0.8.0",
|
|
27
|
+
"@gaia-ai/core": "^0.8.0"
|
|
28
28
|
}
|
|
29
29
|
}
|