gcf-aif-ext 0.8.0 → 0.10.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 +240 -230
- package/commands/gcf.js +17 -7
- package/extension.json +1 -1
- package/lib/aif-config.js +27 -9
- package/lib/board.js +32 -17
- package/lib/config-set.js +119 -40
- package/lib/domain.js +370 -0
- package/lib/transitions.js +8 -2
- package/package.json +1 -1
- package/templates/agents-block.md +1 -1
package/commands/gcf.js
CHANGED
|
@@ -1106,12 +1106,18 @@ function registerBoard(gcf) {
|
|
|
1106
1106
|
const tag = (i) =>
|
|
1107
1107
|
`${i.id}${i.plan?.tasks !== undefined ? ` ${i.plan.done}/${i.plan.tasks}` : ''}` +
|
|
1108
1108
|
`${i.running ? ' (running)' : ''}${i.verified ? ' (verified)' : ''}`;
|
|
1109
|
-
const byId = new Map(r.items.map((i) => [i.id, i]));
|
|
1110
1109
|
out(o.json, r, (x) =>
|
|
1111
1110
|
[
|
|
1112
1111
|
`phase: ${x.phase.phase ?? x.phase.error}`,
|
|
1113
|
-
|
|
1114
|
-
|
|
1112
|
+
// Items come in run order, so each column lists its own in that order.
|
|
1113
|
+
...x.domain.columns.map(
|
|
1114
|
+
({ status }) =>
|
|
1115
|
+
`${status}: ${
|
|
1116
|
+
x.items
|
|
1117
|
+
.filter((i) => i.status === status)
|
|
1118
|
+
.map(tag)
|
|
1119
|
+
.join(', ') || '-'
|
|
1120
|
+
}`,
|
|
1115
1121
|
),
|
|
1116
1122
|
`wip: ${x.wip.count}/${x.wip.limit}`,
|
|
1117
1123
|
`inbox: ${x.inbox.length}, open questions: ${x.questions.length}`,
|
|
@@ -1124,14 +1130,14 @@ function registerBoard(gcf) {
|
|
|
1124
1130
|
function registerSettings(gcf) {
|
|
1125
1131
|
const config = gcf
|
|
1126
1132
|
.command('config')
|
|
1127
|
-
.description("The human's settings
|
|
1133
|
+
.description("The human's settings and the agents' language: list them, or set one (commit)")
|
|
1128
1134
|
// A misspelt subcommand would read as a stray word and list the settings, as if set.
|
|
1129
1135
|
.allowExcessArguments(false)
|
|
1130
1136
|
.option('--json', 'machine output')
|
|
1131
1137
|
.action(async (o) => {
|
|
1132
|
-
const { gcfRoot, readConfig
|
|
1138
|
+
const { gcfRoot, readConfig } = await lib('gcf-paths');
|
|
1133
1139
|
await readConfig(gcfRoot()); // a config the rules refuse says so here
|
|
1134
|
-
const list = (await lib('config-set')).
|
|
1140
|
+
const list = await (await lib('config-set')).projectSettings(process.cwd());
|
|
1135
1141
|
out(o.json, list, (x) =>
|
|
1136
1142
|
x.map((s) => `${s.key}: ${s.value ?? '-'} # ${s.label}`).join('\n'),
|
|
1137
1143
|
);
|
|
@@ -1151,7 +1157,11 @@ function registerSettings(gcf) {
|
|
|
1151
1157
|
commit: o.commit,
|
|
1152
1158
|
});
|
|
1153
1159
|
const now = `${r.key}: ${r.value ?? 'default'}`;
|
|
1154
|
-
|
|
1160
|
+
// An ignored file is written but not committed: a worktree's agent does not read it.
|
|
1161
|
+
const note = r.ignored
|
|
1162
|
+
? ' (git ignores the file: not committed, and agents in a worktree will not see it)'
|
|
1163
|
+
: '';
|
|
1164
|
+
console.log(r.unchanged ? `${now} (unchanged)` : `${now}${shortHash(r.hash)}${note}`);
|
|
1155
1165
|
});
|
|
1156
1166
|
const many = (v, prev = []) => [...prev, v];
|
|
1157
1167
|
gcf
|
package/extension.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/lee-to/ai-factory/2.x/schemas/extension.schema.json",
|
|
3
3
|
"name": "gcf-aif-ext",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"description": "Game Context Framework for AI Factory",
|
|
6
6
|
"skills": [
|
|
7
7
|
"skills/gcf-init",
|
package/lib/aif-config.js
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
// The language AI Factory sets in `.ai-factory/config.yaml` for what an agent writes to the
|
|
2
2
|
// human: `language.ui`. GCF adds no setting of its own (Methodology §5.2.3); agents read the
|
|
3
|
-
// file themselves, the core reads it
|
|
3
|
+
// file themselves, the core reads it for the prompts it writes and sets it for the board
|
|
4
|
+
// (`gcf config set language.ui`).
|
|
4
5
|
import { readFile } from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import YAML from 'yaml';
|
|
7
8
|
|
|
9
|
+
/** AI Factory's config file, project-relative. */
|
|
10
|
+
export const AIF_CONFIG = '.ai-factory/config.yaml';
|
|
11
|
+
|
|
12
|
+
// A language name or code, one word in any script: anything else would break the prompt line it
|
|
13
|
+
// goes in.
|
|
14
|
+
export const LANGUAGE_RE = /^[\p{L}\p{N}_-]{2,32}$/u;
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
|
-
*
|
|
10
|
-
* language is a preference, and a broken
|
|
17
|
+
* `.ai-factory/config.yaml` as the file has it: `{}` when it is missing, unreadable or not a
|
|
18
|
+
* map. The language is a preference, and a broken file must not stop the work.
|
|
19
|
+
* @returns {Promise<Record<string, any>>}
|
|
11
20
|
*/
|
|
12
|
-
export async function
|
|
21
|
+
export async function readAifConfig(projectDir) {
|
|
13
22
|
try {
|
|
14
|
-
const
|
|
15
|
-
const ui = YAML.parse(await readFile(file, 'utf8'))?.language?.ui;
|
|
23
|
+
const raw = YAML.parse(await readFile(path.join(path.resolve(projectDir), AIF_CONFIG), 'utf8'));
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
return typeof ui === 'string' && /^[\w-]{2,32}$/.test(ui.trim()) ? ui.trim() : 'en';
|
|
25
|
+
return raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
19
26
|
} catch {
|
|
20
|
-
return
|
|
27
|
+
return {};
|
|
21
28
|
}
|
|
22
29
|
}
|
|
30
|
+
|
|
31
|
+
/** A language of the parsed file (`ui`, `artifacts`), or null when it holds no language. */
|
|
32
|
+
export function languageIn(aif, key) {
|
|
33
|
+
const v = aif?.language?.[key];
|
|
34
|
+
|
|
35
|
+
return typeof v === 'string' && LANGUAGE_RE.test(v.trim()) ? v.trim() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The project's `language.ui`, `en` by default or when the value is no language. */
|
|
39
|
+
export const uiLanguage = async (projectDir) =>
|
|
40
|
+
languageIn(await readAifConfig(projectDir), 'ui') ?? 'en';
|
package/lib/board.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { buildLedger } from './ledger.js';
|
|
8
|
-
import { readConfig
|
|
8
|
+
import { readConfig } from './gcf-paths.js';
|
|
9
9
|
import { readLive } from './live.js';
|
|
10
10
|
import { listInbox } from './inbox.js';
|
|
11
11
|
import { phaseStatus } from './phase.js';
|
|
@@ -15,17 +15,16 @@ import { wipOf } from './audit-core.js';
|
|
|
15
15
|
import { runHolder } from './main-lock.js';
|
|
16
16
|
import { ITEM_TYPES } from './validate-item.js';
|
|
17
17
|
import { movesOf } from './transitions.js';
|
|
18
|
+
import { domainOf, hasHarness, RUNS_FROM } from './domain.js';
|
|
18
19
|
import { openDepends } from './work-ops.js';
|
|
19
20
|
import { listQuestions } from './questions.js';
|
|
20
21
|
import { sessionActive } from './session-mark.js';
|
|
21
22
|
import { agentOf } from './agent-permissions.js';
|
|
22
|
-
import {
|
|
23
|
+
import { projectSettings } from './config-set.js';
|
|
23
24
|
|
|
24
25
|
// The version of the JSON below (Methodology §11): a board refuses one it does not know. Bump it
|
|
25
26
|
// when a field changes meaning or goes away; a new field does not bump it.
|
|
26
|
-
export const BOARD_SCHEMA =
|
|
27
|
-
|
|
28
|
-
const COLUMNS = ['ready', 'blocked', 'awaiting-verdict', 'awaiting-decision', 'unplayed', 'parked'];
|
|
27
|
+
export const BOARD_SCHEMA = 3;
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Plan progress `{tasks, done}` from its checkboxes and the plan's project path, both null
|
|
@@ -232,8 +231,19 @@ function runningKind(projectDir, id) {
|
|
|
232
231
|
return sessionActive(projectDir, id) ? 'session' : null;
|
|
233
232
|
}
|
|
234
233
|
|
|
234
|
+
/**
|
|
235
|
+
* What a board may offer (§4.1): nothing while a run drives the item; with the harness, the
|
|
236
|
+
* agent's run to an item that waits for one and has no green verify yet.
|
|
237
|
+
*/
|
|
238
|
+
function offered(c, { running, verified, harness }) {
|
|
239
|
+
if (running) return [];
|
|
240
|
+
const runs = harness && c.data.status === RUNS_FROM && !verified;
|
|
241
|
+
|
|
242
|
+
return [...(runs ? ['run'] : []), ...movesOf(c.data)];
|
|
243
|
+
}
|
|
244
|
+
|
|
235
245
|
/** One item of the board. */
|
|
236
|
-
async function itemOf(projectDir, c, verified, ledger) {
|
|
246
|
+
async function itemOf(projectDir, c, { verified, ledger, harness }) {
|
|
237
247
|
const id = String(c.data.id);
|
|
238
248
|
// A trailer counts only while the item waits on main for its integrate.
|
|
239
249
|
const ready = c.data.status === 'ready' && verified.has(id);
|
|
@@ -265,11 +275,13 @@ async function itemOf(projectDir, c, verified, ledger) {
|
|
|
265
275
|
resumable,
|
|
266
276
|
resume,
|
|
267
277
|
verified: ready,
|
|
268
|
-
// The dependencies the run gate would refuse a ready item on now (Board-PRD §5.2)
|
|
278
|
+
// The dependencies the run gate would refuse a ready item on now (Board-PRD §5.2); none
|
|
279
|
+
// while it runs, past the gate.
|
|
269
280
|
waits_for:
|
|
270
|
-
c.data.status === 'ready'
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
c.data.status === 'ready' && !running
|
|
282
|
+
? openDepends(ledger, c.data.depends ?? [], { waiting: true })
|
|
283
|
+
: [],
|
|
284
|
+
moves: offered(c, { running, verified: ready, harness }),
|
|
273
285
|
file: `gcf/work/${c.name}`,
|
|
274
286
|
log: logOf(projectDir, id),
|
|
275
287
|
// Filled by board() from the inbox.
|
|
@@ -282,8 +294,11 @@ export async function board(projectDir) {
|
|
|
282
294
|
const root = path.join(path.resolve(projectDir), 'gcf');
|
|
283
295
|
const ledger = await buildLedger(projectDir);
|
|
284
296
|
const verified = verifiedOf(projectDir);
|
|
297
|
+
const extensions = await extensionsOf(projectDir);
|
|
298
|
+
const harness = hasHarness(extensions);
|
|
285
299
|
const items = [];
|
|
286
|
-
for (const c of ledger.items)
|
|
300
|
+
for (const c of ledger.items)
|
|
301
|
+
items.push(await itemOf(projectDir, c, { verified, ledger, harness }));
|
|
287
302
|
inRunOrder(items);
|
|
288
303
|
// When the wait began: the time of the commit `since` names (Board-PRD §13).
|
|
289
304
|
const times = commitTimes(
|
|
@@ -306,20 +321,20 @@ export async function board(projectDir) {
|
|
|
306
321
|
run_mode: config.run_mode,
|
|
307
322
|
// What the harness hands its agent runs (Harness-PRD §7): deny rules and the context limits.
|
|
308
323
|
agent: agentOf(config),
|
|
309
|
-
// The human's settings of config.yml, each with its value and
|
|
310
|
-
|
|
324
|
+
// The human's settings of config.yml and the agents' language, each with its value and
|
|
325
|
+
// default (`gcf config set`).
|
|
326
|
+
settings: await projectSettings(projectDir),
|
|
311
327
|
context: {
|
|
312
328
|
handoff_pct: config.context_handoff_pct,
|
|
313
329
|
window: config.context_window,
|
|
314
330
|
compact_pct: config.auto_compact_pct,
|
|
315
331
|
},
|
|
316
332
|
phase: await phaseOf(projectDir),
|
|
317
|
-
extensions
|
|
333
|
+
extensions,
|
|
318
334
|
// A malformed block reads with fields missing (live check names it): null, not absent.
|
|
319
335
|
release: releaseOf(live.release),
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
),
|
|
336
|
+
// The domain as data (Board-PRD decision 16): its columns, blocks, fields, moves and terms.
|
|
337
|
+
domain: domainOf(extensions),
|
|
323
338
|
items,
|
|
324
339
|
wip: wipOf(ledger.items, config),
|
|
325
340
|
inbox,
|
package/lib/config-set.js
CHANGED
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
// `gcf config set` (GCF-PRD §12.4): the human's settings of gcf/config.yml, one key at a time,
|
|
2
2
|
// so a board edits them through the core and never writes the file itself. A change is checked
|
|
3
3
|
// by the same rules a read applies before it is written; the rest of the file keeps its keys
|
|
4
|
-
// and comments. `phases:` and `agent:` have their own commands and are not set here.
|
|
4
|
+
// and comments. `phases:` and `agent:` have their own commands and are not set here. The
|
|
5
|
+
// language agents write in is AI Factory's (`.ai-factory/config.yaml`), set here for the board
|
|
6
|
+
// too, since AI Factory has no command for it (Board-PRD decision 12).
|
|
5
7
|
import { readFile } from 'node:fs/promises';
|
|
6
8
|
import { existsSync } from 'node:fs';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
7
10
|
import path from 'node:path';
|
|
8
11
|
import { isDeepStrictEqual } from 'node:util';
|
|
9
12
|
import YAML from 'yaml';
|
|
10
13
|
import { commitMove } from './unit-ops.js';
|
|
14
|
+
import { withMainLock } from './main-lock.js';
|
|
11
15
|
import { atomicWrite } from './safe-path.js';
|
|
12
|
-
import { configOf, DEFAULT_CONFIG, gcfRoot, RUN_MODES } from './gcf-paths.js';
|
|
16
|
+
import { configOf, DEFAULT_CONFIG, gcfRoot, readRawConfig, RUN_MODES } from './gcf-paths.js';
|
|
17
|
+
import { AIF_CONFIG, LANGUAGE_RE, languageIn, readAifConfig } from './aif-config.js';
|
|
13
18
|
|
|
14
|
-
/** @typedef {'number' | 'share' | 'mode' | 'command'} Kind */
|
|
19
|
+
/** @typedef {'number' | 'share' | 'mode' | 'command' | 'language'} Kind */
|
|
15
20
|
|
|
16
21
|
// Thrown inside the move to leave without a write or a commit.
|
|
17
22
|
const NO_CHANGE = new Error('no change');
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
// The languages AI Factory's own setup offers; any other name or code is taken too.
|
|
25
|
+
const LANGUAGES = ['en', 'ru', 'de', 'fr', 'es', 'zh', 'ja', 'ko', 'pt', 'it'];
|
|
26
|
+
|
|
27
|
+
/** A YAML file as a document with a map on top: an edit keeps its comments. */
|
|
28
|
+
export function configDoc(text, name = 'gcf/config.yml') {
|
|
21
29
|
const doc = YAML.parseDocument(text);
|
|
22
|
-
if (doc.errors.length) throw new Error(
|
|
30
|
+
if (doc.errors.length) throw new Error(`${name}: ${doc.errors[0].message}`);
|
|
31
|
+
// A file of a list or a word on top is not a settings file: replacing it would lose it.
|
|
32
|
+
if (doc.contents != null && !YAML.isMap(doc.contents) && doc.contents.toJSON() != null)
|
|
33
|
+
throw new Error(`${name} must map its keys`);
|
|
23
34
|
if (!YAML.isMap(doc.contents)) doc.contents = /** @type {any} */ (doc.createNode({}));
|
|
24
35
|
|
|
25
36
|
return doc;
|
|
@@ -33,8 +44,24 @@ export function ensureMap(doc, keys, error) {
|
|
|
33
44
|
doc.setIn(keys, doc.createNode({}));
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
/** @type {{ key: string, kind: Kind, label: string }[]} */
|
|
47
|
+
/** @type {{ key: string, kind: Kind, label: string, file?: string, options?: string[] }[]} */
|
|
37
48
|
export const SETTINGS = [
|
|
49
|
+
{
|
|
50
|
+
key: 'language.ui',
|
|
51
|
+
kind: 'language',
|
|
52
|
+
file: AIF_CONFIG,
|
|
53
|
+
options: LANGUAGES,
|
|
54
|
+
label:
|
|
55
|
+
'the language agents write to you in: questions, block reasons, inbox entries, lessons, ' +
|
|
56
|
+
'titles',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'language.artifacts',
|
|
60
|
+
kind: 'language',
|
|
61
|
+
file: AIF_CONFIG,
|
|
62
|
+
options: LANGUAGES,
|
|
63
|
+
label: 'the language of plans and of the bodies of items that are not design units',
|
|
64
|
+
},
|
|
38
65
|
{
|
|
39
66
|
key: 'run_mode',
|
|
40
67
|
kind: 'mode',
|
|
@@ -95,28 +122,54 @@ export const SETTINGS = [
|
|
|
95
122
|
|
|
96
123
|
const BY_KEY = new Map(SETTINGS.map((s) => [s.key, s]));
|
|
97
124
|
|
|
125
|
+
/** The default a setting holds while its file does not set it; the artifacts follow the ui. */
|
|
126
|
+
function defaultOf(s, aif) {
|
|
127
|
+
const [head, sub] = s.key.split('.');
|
|
128
|
+
if (s.key === 'language.artifacts') return languageIn(aif, 'ui') ?? 'en';
|
|
129
|
+
if (s.kind === 'language') return 'en';
|
|
130
|
+
if (sub) return null;
|
|
131
|
+
|
|
132
|
+
return head === 'run_mode' ? 'worktree' : (DEFAULT_CONFIG[head] ?? null);
|
|
133
|
+
}
|
|
134
|
+
|
|
98
135
|
/**
|
|
99
|
-
* Each setting with what
|
|
100
|
-
* what a settings screen shows. `raw` is the parsed config.yml, before the defaults
|
|
136
|
+
* Each setting with what its file sets (null: not set, the default holds) and its default:
|
|
137
|
+
* what a settings screen shows. `raw` is the parsed config.yml, before the defaults; `aif` the
|
|
138
|
+
* parsed `.ai-factory/config.yaml`.
|
|
101
139
|
*/
|
|
102
|
-
export function settingsOf(raw) {
|
|
140
|
+
export function settingsOf(raw, aif = {}) {
|
|
103
141
|
return SETTINGS.map((s) => {
|
|
104
142
|
const [head, sub] = s.key.split('.');
|
|
105
|
-
const parent = raw?.[head];
|
|
143
|
+
const parent = (s.file ? aif : raw)?.[head];
|
|
106
144
|
const value = sub
|
|
107
145
|
? parent && typeof parent === 'object'
|
|
108
146
|
? (parent[sub] ?? null)
|
|
109
147
|
: null
|
|
110
148
|
: (parent ?? null);
|
|
111
|
-
|
|
149
|
+
// AI Factory's file is not checked by GCF's rules: a value that is no language shows unset.
|
|
150
|
+
const shown = s.file ? languageIn(aif, sub) : value;
|
|
112
151
|
|
|
113
|
-
return { ...s, value, default:
|
|
152
|
+
return { ...s, value: shown, default: defaultOf(s, aif) };
|
|
114
153
|
});
|
|
115
154
|
}
|
|
116
155
|
|
|
156
|
+
/** Every setting as the project's two files have them. */
|
|
157
|
+
export const projectSettings = async (projectDir) =>
|
|
158
|
+
settingsOf(await readRawConfig(gcfRoot(projectDir)), await readAifConfig(projectDir));
|
|
159
|
+
|
|
160
|
+
/** A language name or code; empty takes the key off, and AI Factory's default holds. */
|
|
161
|
+
function languageOf(setting, t) {
|
|
162
|
+
if (t && !LANGUAGE_RE.test(t))
|
|
163
|
+
throw new Error(`${setting.key} must be a language name or code (en, ru…), got ${t}`);
|
|
164
|
+
|
|
165
|
+
return t || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
117
168
|
/** The value of `text` for a setting of `kind`; an empty number goes back to its default. */
|
|
118
169
|
function valueOf(setting, text) {
|
|
119
170
|
const t = String(text ?? '').trim();
|
|
171
|
+
if (setting.kind === 'language') return languageOf(setting, t);
|
|
172
|
+
|
|
120
173
|
if (setting.kind === 'command') {
|
|
121
174
|
if (!t) throw new Error(`${setting.key}: name the command, or none`);
|
|
122
175
|
|
|
@@ -138,45 +191,71 @@ function valueOf(setting, text) {
|
|
|
138
191
|
return n;
|
|
139
192
|
}
|
|
140
193
|
|
|
194
|
+
/** Whether git ignores `file`: a commit cannot carry it, and a worktree does not have it. */
|
|
195
|
+
const ignoredByGit = (projectDir, file) =>
|
|
196
|
+
spawnSync('git', ['check-ignore', '-q', '--', file], { cwd: projectDir }).status === 0;
|
|
197
|
+
|
|
198
|
+
const trackedByGit = (projectDir, file) =>
|
|
199
|
+
spawnSync('git', ['ls-files', '--error-unmatch', '--', file], { cwd: projectDir }).status === 0;
|
|
200
|
+
|
|
141
201
|
/**
|
|
142
|
-
* Set one key of gcf/config.yml
|
|
143
|
-
* its default. The file is read and checked
|
|
202
|
+
* Set one key of gcf/config.yml, or the language of `.ai-factory/config.yaml`, and commit
|
|
203
|
+
* gcf(config); a number set to nothing goes back to its default. The file is read and checked
|
|
204
|
+
* under the move's lock. An AI Factory file git ignores is written, not committed (`ignored`).
|
|
144
205
|
*/
|
|
145
206
|
export async function setConfig(projectDir, key, text, { commit = true } = {}) {
|
|
146
207
|
const setting = BY_KEY.get(key);
|
|
147
208
|
if (!setting) throw new Error(`no setting ${key}: ${SETTINGS.map((s) => s.key).join(' | ')}`);
|
|
148
209
|
const value = valueOf(setting, text);
|
|
149
|
-
const
|
|
210
|
+
const name = setting.file ?? 'gcf/config.yml';
|
|
211
|
+
const file = setting.file
|
|
212
|
+
? path.join(path.resolve(projectDir), setting.file)
|
|
213
|
+
: path.join(gcfRoot(projectDir), 'config.yml');
|
|
214
|
+
const ignored = Boolean(setting.file) && ignoredByGit(projectDir, file);
|
|
215
|
+
// AI Factory's setup may leave its file uncommitted: the first change commits it whole.
|
|
216
|
+
const inputs = setting.file && !ignored && !trackedByGit(projectDir, file) ? [file] : [];
|
|
150
217
|
const keys = key.split('.');
|
|
151
|
-
let raw;
|
|
152
218
|
// A change that changes nothing (the same value, a reset of a key not set) commits nothing.
|
|
153
219
|
let unchanged = false;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
220
|
+
const move = () =>
|
|
221
|
+
commitMove(projectDir, `gcf(config): ${key}`, {
|
|
222
|
+
files: [file],
|
|
223
|
+
inputs,
|
|
224
|
+
mutate,
|
|
225
|
+
commit: commit && !ignored,
|
|
226
|
+
});
|
|
227
|
+
const mutate = async () => {
|
|
228
|
+
const text = existsSync(file) ? await readFile(file, 'utf8') : '';
|
|
229
|
+
const doc = configDoc(text, name);
|
|
230
|
+
// A reset of a key whose parent is not there changes nothing: no empty map is written.
|
|
231
|
+
if (value === null && keys.length > 1 && !YAML.isMap(doc.getIn([keys[0]], true))) {
|
|
232
|
+
unchanged = true;
|
|
233
|
+
throw NO_CHANGE;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (keys.length > 1) ensureMap(doc, [keys[0]], `${name}: ${keys[0]} must map its keys`);
|
|
237
|
+
if (value === null) doc.deleteIn(keys);
|
|
238
|
+
else doc.setIn(keys, value);
|
|
239
|
+
const raw = doc.toJS();
|
|
240
|
+
if (!setting.file) configOf(raw);
|
|
241
|
+
const out = doc.toString();
|
|
242
|
+
// The values, not the text: YAML's output may differ in form (a final newline, `{}`).
|
|
243
|
+
if (isDeepStrictEqual(YAML.parse(text) ?? {}, raw ?? {})) {
|
|
244
|
+
unchanged = true;
|
|
245
|
+
throw NO_CHANGE;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await atomicWrite(file, out);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// A move that commits nothing takes no lock in commitMove: the ignored file still needs one.
|
|
252
|
+
const hash = await (ignored && commit ? withMainLock(projectDir, move) : move()).catch((e) => {
|
|
176
253
|
if (e !== NO_CHANGE) throw e;
|
|
177
254
|
|
|
178
255
|
return null;
|
|
179
256
|
});
|
|
257
|
+
// The change is made: a broken gcf/config.yml must not read as its failure.
|
|
258
|
+
const settings = await projectSettings(projectDir).catch(() => []);
|
|
180
259
|
|
|
181
|
-
return { key, value, unchanged, settings
|
|
260
|
+
return { key, value, unchanged, ignored, settings, hash };
|
|
182
261
|
}
|