gcf-board 0.2.19 → 0.3.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/README.md +1 -1
- package/dist/server/app.js +10 -2
- package/dist/server/moves.js +29 -45
- package/dist/shared/board.js +1 -9
- package/dist/web/assets/index-Cj06vUmK.js +33 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-CgUcGPkn.js +0 -33
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A local web board for game projects on [GCF](../gcf-aif-ext): the items by statu
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js ≥ 22.18
|
|
8
|
-
- [`ai-factory`](https://www.npmjs.com/package/ai-factory) installed globally, and in each project [`gcf-aif-ext`](../gcf-aif-ext) ≥ 0.
|
|
8
|
+
- [`ai-factory`](https://www.npmjs.com/package/ai-factory) installed globally, and in each project [`gcf-aif-ext`](../gcf-aif-ext) ≥ 0.9 (`gcf board --json` with `schema: 2`: the columns and the item moves come from it as data)
|
|
9
9
|
- for runs: [`gcf-harness-aif-ext`](../gcf-harness-aif-ext) ≥ 0.8 and Claude Code (`claude -p`)
|
|
10
10
|
- gcf-board 0.2 needs `gcf-aif-ext` ≥ 0.7 (priority, questions, session marks, `run_mode`)
|
|
11
11
|
|
package/dist/server/app.js
CHANGED
|
@@ -13,7 +13,7 @@ import { briefError, runCli } from './cli.js';
|
|
|
13
13
|
import { broadcast, planRev, subscribe, watchProject } from './events.js';
|
|
14
14
|
import { followLog, readShown, shownShape } from './files.js';
|
|
15
15
|
import { HttpError } from './http-error.js';
|
|
16
|
-
import { moveArgs, runIds } from './moves.js';
|
|
16
|
+
import { isCenterMove, moveArgs, runIds } from './moves.js';
|
|
17
17
|
import { addProject, listProjects, projectOf, removeProject } from './registry.js';
|
|
18
18
|
import { cancelResume, closeReport, forgetProject, resumeQueue, runsOf, startRun, unqueue, } from './runs.js';
|
|
19
19
|
const send = (res, status, body) => {
|
|
@@ -102,12 +102,15 @@ export function createApp(o) {
|
|
|
102
102
|
watchers.set(p.name, watchProject(p));
|
|
103
103
|
};
|
|
104
104
|
const cli = (p, args, timeout) => runCli(o.entry, p.path, args, timeout);
|
|
105
|
+
// The last domain each project's board read gave: an item move is built from its form.
|
|
106
|
+
const domains = new Map();
|
|
105
107
|
/** `gcf board --json` of a project, refused when its schema is not the one this board reads. */
|
|
106
108
|
const boardOf = async (p) => {
|
|
107
109
|
const board = jsonOf(await cli(p, ['gcf', 'board', '--json']), 'gcf board');
|
|
108
110
|
if (board.schema !== BOARD_SCHEMA)
|
|
109
111
|
throw new HttpError(502, `gcf board gives schema ${board.schema ?? 'none'}, this board reads ${BOARD_SCHEMA}: ` +
|
|
110
112
|
'update gcf-aif-ext and gcf-board together');
|
|
113
|
+
domains.set(p.name, board.domain);
|
|
111
114
|
return board;
|
|
112
115
|
};
|
|
113
116
|
const routes = [
|
|
@@ -132,6 +135,7 @@ export function createApp(o) {
|
|
|
132
135
|
async (_req, res, m) => {
|
|
133
136
|
await removeProject(m[1]);
|
|
134
137
|
forgetProject(m[1]);
|
|
138
|
+
domains.delete(m[1]);
|
|
135
139
|
watchers.get(m[1])?.();
|
|
136
140
|
watchers.delete(m[1]);
|
|
137
141
|
send(res, 200, { ok: true });
|
|
@@ -210,7 +214,11 @@ export function createApp(o) {
|
|
|
210
214
|
/^\/api\/p\/([^/]+)\/move$/,
|
|
211
215
|
async (req, res, m) => {
|
|
212
216
|
const p = await projectOf(m[1]);
|
|
213
|
-
const
|
|
217
|
+
const body = await bodyOf(req);
|
|
218
|
+
const domain = isCenterMove(body.cmd)
|
|
219
|
+
? undefined
|
|
220
|
+
: (domains.get(p.name) ?? (await boardOf(p)).domain);
|
|
221
|
+
const r = await cli(p, moveArgs(body, domain?.moves));
|
|
214
222
|
broadcast({ type: 'board', project: p.name });
|
|
215
223
|
send(res, 200, { ok: r.ok, out: r.out.trim(), err: briefError(r.err) });
|
|
216
224
|
},
|
package/dist/server/moves.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// The moves a board may run, each one `ai-factory gcf` command (Methodology §4.1, §11). Anything
|
|
2
|
-
// else is refused: the board is a view, the commands own every rule. An operator's text goes
|
|
3
|
-
// as one `--flag=value` argument, so no word of it reads as a flag.
|
|
4
1
|
import { HttpError } from './http-error.js';
|
|
5
2
|
const SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
6
3
|
function id(b, key = 'id') {
|
|
@@ -23,23 +20,17 @@ function required(b, key) {
|
|
|
23
20
|
throw new HttpError(400, `${key} is required`);
|
|
24
21
|
return v;
|
|
25
22
|
}
|
|
26
|
-
function oneOf(b, key, allowed) {
|
|
27
|
-
const v = b[key];
|
|
28
|
-
if (typeof v !== 'string' || !allowed.includes(v))
|
|
29
|
-
throw new HttpError(400, `${key} must be ${allowed.join(' | ')}`);
|
|
30
|
-
return v;
|
|
31
|
-
}
|
|
32
23
|
const opt = (b, key, flag = key) => {
|
|
33
24
|
const v = text(b, key);
|
|
34
25
|
return v === undefined ? [] : [`--${flag}=${v}`];
|
|
35
26
|
};
|
|
36
|
-
const many = (b, key) => {
|
|
27
|
+
const many = (b, key, flag = key) => {
|
|
37
28
|
const v = b[key];
|
|
38
29
|
if (v === undefined)
|
|
39
30
|
return [];
|
|
40
31
|
if (!Array.isArray(v) || v.some((x) => typeof x !== 'string'))
|
|
41
32
|
throw new HttpError(400, `${key} must be a list of text`);
|
|
42
|
-
return v.filter(Boolean).map((x) => `--${
|
|
33
|
+
return v.filter(Boolean).map((x) => `--${flag}=${x}`);
|
|
43
34
|
};
|
|
44
35
|
// The core limits a tag to 40 characters too.
|
|
45
36
|
const TAG = /^[a-z0-9][a-z0-9:-]{0,39}$/;
|
|
@@ -55,7 +46,7 @@ const list = (b, key, re, what, flag = key) => {
|
|
|
55
46
|
const idList = (b, key) => list(b, key, SLUG, 'ids');
|
|
56
47
|
const tagList = (b, key, flag = key) => list(b, key, TAG, 'tags', flag);
|
|
57
48
|
/** `--remove` / `--no-remove` when the operator chose; the command's default otherwise. */
|
|
58
|
-
const remove = (b) => b
|
|
49
|
+
const remove = (b, key = 'remove', flag = key) => b[key] === true ? [`--${flag}`] : b[key] === false ? [`--no-${flag}`] : [];
|
|
59
50
|
const NEW_ITEM_OPTIONS = [
|
|
60
51
|
'gate',
|
|
61
52
|
'title',
|
|
@@ -73,35 +64,8 @@ const NEW_ITEM_OPTIONS = [
|
|
|
73
64
|
'effort',
|
|
74
65
|
];
|
|
75
66
|
const MOVES = {
|
|
76
|
-
submit: (b) => ['submit', id(b), ...opt(b, 'unverified')],
|
|
77
|
-
close: (b) => ['close', id(b), ...many(b, 'constraint'), ...opt(b, 'unverified')],
|
|
78
|
-
verdict: (b) => [
|
|
79
|
-
'verdict',
|
|
80
|
-
id(b),
|
|
81
|
-
oneOf(b, 'verdict', ['keep', 'tune', 'kill']),
|
|
82
|
-
...opt(b, 'note'),
|
|
83
|
-
...opt(b, 'tags'),
|
|
84
|
-
...many(b, 'constraint'),
|
|
85
|
-
...remove(b),
|
|
86
|
-
],
|
|
87
|
-
decide: (b) => [
|
|
88
|
-
'decide',
|
|
89
|
-
id(b),
|
|
90
|
-
oneOf(b, 'choice', ['go', 'no', 'wontfix', 'known', 'close', 'continue', 'kill']),
|
|
91
|
-
...opt(b, 'reason'),
|
|
92
|
-
...remove(b),
|
|
93
|
-
],
|
|
94
|
-
block: (b) => ['block', id(b), `--reason=${required(b, 'reason')}`],
|
|
95
|
-
unblock: (b) => ['unblock', id(b), ...opt(b, 'note')],
|
|
96
67
|
// A session's mark taken off: the item no longer shows running (Board-PRD §3.2, kind c).
|
|
97
68
|
release: (b) => ['release', id(b)],
|
|
98
|
-
drop: (b) => [
|
|
99
|
-
'drop',
|
|
100
|
-
id(b),
|
|
101
|
-
`--reason=${required(b, 'reason')}`,
|
|
102
|
-
...opt(b, 'tags'),
|
|
103
|
-
...remove(b),
|
|
104
|
-
],
|
|
105
69
|
'item-new': (b) => [
|
|
106
70
|
'item',
|
|
107
71
|
'new',
|
|
@@ -156,12 +120,32 @@ const MOVES = {
|
|
|
156
120
|
return ['permissions', ...args];
|
|
157
121
|
},
|
|
158
122
|
};
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
123
|
+
/** An item's move by its domain form: the words with the item's id, then each field's flag. */
|
|
124
|
+
function domainMoveArgs(spec, b) {
|
|
125
|
+
const words = spec.args.map((w) => (w === '{id}' ? id(b) : w));
|
|
126
|
+
const flags = spec.fields.flatMap((f) => {
|
|
127
|
+
const flag = f.flag ?? f.key;
|
|
128
|
+
if (f.kind === 'list')
|
|
129
|
+
return many(b, f.key, flag);
|
|
130
|
+
if (f.kind === 'remove')
|
|
131
|
+
return remove(b, f.key, flag);
|
|
132
|
+
return f.required ? [`--${flag}=${required(b, f.key)}`] : opt(b, f.key, flag);
|
|
133
|
+
});
|
|
134
|
+
return [...words, ...flags];
|
|
135
|
+
}
|
|
136
|
+
/** Whether a move is the center's own: its arguments need no domain. */
|
|
137
|
+
export const isCenterMove = (cmd) => typeof cmd === 'string' && Object.hasOwn(MOVES, cmd);
|
|
138
|
+
/**
|
|
139
|
+
* The `ai-factory` arguments of a move, or a 400 naming what is wrong: a center move, or one of
|
|
140
|
+
* `moves`, the domain's forms from the project's board read.
|
|
141
|
+
*/
|
|
142
|
+
export function moveArgs(b, moves = {}) {
|
|
143
|
+
const cmd = typeof b.cmd === 'string' ? b.cmd : '';
|
|
144
|
+
if (isCenterMove(cmd))
|
|
145
|
+
return ['gcf', ...MOVES[cmd](b)];
|
|
146
|
+
if (Object.hasOwn(moves, cmd))
|
|
147
|
+
return ['gcf', ...domainMoveArgs(moves[cmd], b)];
|
|
148
|
+
throw new HttpError(400, `unknown move: ${String(b.cmd)}`);
|
|
165
149
|
}
|
|
166
150
|
/** The ids of a run: one slug each, at least one. */
|
|
167
151
|
export function runIds(b) {
|
package/dist/shared/board.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
// The read model of `ai-factory gcf board --json` (Methodology §11): the board refuses a schema
|
|
2
2
|
// it does not know. Types only, plus the version this board reads.
|
|
3
|
-
export const BOARD_SCHEMA =
|
|
4
|
-
export const STATUSES = [
|
|
5
|
-
'ready',
|
|
6
|
-
'blocked',
|
|
7
|
-
'awaiting-verdict',
|
|
8
|
-
'awaiting-decision',
|
|
9
|
-
'unplayed',
|
|
10
|
-
'parked',
|
|
11
|
-
];
|
|
3
|
+
export const BOARD_SCHEMA = 2;
|