@golden-frijoles/kit 0.2.0 → 0.4.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/config.mjs +82 -0
- package/dist/init.mjs +132 -0
- package/dist/lib/config-registry.mjs +145 -0
- package/dist/lib/config.d.mts +73 -0
- package/dist/lib/config.mjs +384 -0
- package/dist/lib/golden-onboarding.mjs +25 -0
- package/dist/lib/jev.mjs +12 -9
- package/dist/lib/project-root.mjs +24 -4
- package/dist/lib/prose-brief.mjs +10 -4
- package/dist/lib/reporting-config.mjs +12 -2
- package/dist/live-smoke.mjs +10 -2
- package/dist/skeleton/Roadmap/00-ideas/README.md +85 -0
- package/dist/skeleton/Roadmap/00-ideas/audits/.gitkeep +0 -0
- package/dist/skeleton/Roadmap/00-ideas/seeds/.gitkeep +0 -0
- package/dist/skeleton/Roadmap/LEARNINGS.md +164 -0
- package/dist/skeleton/Roadmap/README.md +67 -0
- package/dist/skeleton/Roadmap/WAYS-OF-WORKING.md +233 -0
- package/package.json +9 -1
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// config.mjs — `gf-kit config`: read and change golden-frijoles.config.json (golden-frijoles-plugin S4.1, D10).
|
|
3
|
+
//
|
|
4
|
+
// gf-kit config list [--json] every section's effective value, where it came from, any duplicates
|
|
5
|
+
// gf-kit config get <key> [--json] one key's effective value (or its registry default)
|
|
6
|
+
// gf-kit config set <key> <value> write one key; <value> is parsed as JSON when it is valid JSON
|
|
7
|
+
// gf-kit config migrate [--dry-run] fold the legacy files into golden-frijoles.config.json (never edits them)
|
|
8
|
+
//
|
|
9
|
+
// A thin front end: every rule (precedence, the secret guard, what migrate folds) lives in lib/config.mjs, which the
|
|
10
|
+
// `gf` CLI imports too. Exit 0 ok · 1 usage · 2 configuration error (a malformed file, a secret, an unknown section).
|
|
11
|
+
|
|
12
|
+
import { realpathSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { CONFIG_FILENAME, ConfigError, getKey, loadConfig, migrate, setKey } from './lib/config.mjs';
|
|
15
|
+
|
|
16
|
+
const USAGE = 'usage: gf-kit config list|get <key>|set <key> <value>|migrate [--dry-run] [--json]';
|
|
17
|
+
|
|
18
|
+
/** Pure — a CLI value: JSON when it parses (true, 3, ["a"], null), otherwise the literal string. */
|
|
19
|
+
export function parseValue(text) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
} catch {
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function main(argv, { out = (s) => process.stdout.write(s), err = (s) => process.stderr.write(s) } = {}) {
|
|
28
|
+
const json = argv.includes('--json');
|
|
29
|
+
const args = argv.filter((a) => a !== '--json' && a !== '--dry-run');
|
|
30
|
+
const [verb, key, ...rest] = args;
|
|
31
|
+
try {
|
|
32
|
+
if (verb === 'list') {
|
|
33
|
+
const c = loadConfig();
|
|
34
|
+
if (json) return out(`${JSON.stringify(c, null, 2)}\n`), 0;
|
|
35
|
+
const names = Object.keys(c.sections);
|
|
36
|
+
if (!names.length) out(`No settings yet: no ${CONFIG_FILENAME} and no legacy config files.\n`);
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
out(`${name} (${c.sources[name].map((s) => s.split('/').pop()).join(' + ')})\n`);
|
|
39
|
+
out(`${JSON.stringify(c.sections[name], null, 2).replace(/^/gm, ' ')}\n`);
|
|
40
|
+
}
|
|
41
|
+
if (c.duplicates.length) out(`\nset in both the new file and a legacy file (the new file wins): ${c.duplicates.join(', ')}\n`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
if (verb === 'get' && key) {
|
|
45
|
+
const value = getKey(key);
|
|
46
|
+
out(json ? `${JSON.stringify({ key, value })}\n` : `${typeof value === 'string' ? value : JSON.stringify(value)}\n`);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
if (verb === 'set' && key && rest.length) {
|
|
50
|
+
setKey(key, parseValue(rest.join(' ')));
|
|
51
|
+
out(json ? `${JSON.stringify({ key, value: getKey(key) })}\n` : `${key} = ${JSON.stringify(getKey(key))}\n`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
if (verb === 'migrate') {
|
|
55
|
+
const dryRun = argv.includes('--dry-run');
|
|
56
|
+
const r = migrate({ dryRun });
|
|
57
|
+
if (json) return out(`${JSON.stringify({ dryRun, ...r }, null, 2)}\n`), 0;
|
|
58
|
+
if (!r.folded.length) out('Nothing to migrate: no legacy settings the new file does not already have.\n');
|
|
59
|
+
else if (dryRun) out(`Would write ${CONFIG_FILENAME}:\n${JSON.stringify(r.config, null, 2)}\n`);
|
|
60
|
+
else out(`Wrote ${CONFIG_FILENAME} (${r.folded.length} setting(s) folded; the legacy files are untouched).\n`);
|
|
61
|
+
if (r.skipped.length) out(`Skipped (look like secrets; keep them in .env.local): ${r.skipped.join(', ')}\n`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (e instanceof ConfigError) {
|
|
66
|
+
err(`gf-kit config: ${e.message}\n`);
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
err(`${USAGE}\n`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isMain = (() => {
|
|
76
|
+
try {
|
|
77
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
if (isMain) process.exitCode = main(process.argv.slice(2));
|
package/dist/init.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// init.mjs — `gf-kit init`: adopt any repo by writing the Roadmap/ skeleton (golden-frijoles-plugin S3.2, D2).
|
|
3
|
+
//
|
|
4
|
+
// node scripts/init.mjs # write the skeleton into projectRoot(); never overwrite a file
|
|
5
|
+
//
|
|
6
|
+
// ── Why ───────────────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// A stranger who pastes the install prompt into an EXISTING repo gets the plugin, but `groom` needs
|
|
8
|
+
// somewhere to write on day one — `Roadmap/README.md`, `WAYS-OF-WORKING.md`, `LEARNINGS.md`, and the
|
|
9
|
+
// `00-ideas/` funnel. `gf-kit init` is that one step: it writes the same skeleton a project spawned
|
|
10
|
+
// from `template/` already has, into any repo, copied or installed (D2's two roots).
|
|
11
|
+
//
|
|
12
|
+
// ── Where the skeleton's SOURCE lives ─────────────────────────────────────────────────────────
|
|
13
|
+
// INSTALLED: `kitRoot()/skeleton/` — `scripts/build-kit.mjs` copies `template/<each SKELETON path>`
|
|
14
|
+
// into `kit/dist/skeleton/`, reading the very list exported below, so there is one list, not two.
|
|
15
|
+
// COPIED: `kitRoot()/../` — when this file runs as `template/scripts/init.mjs` (this repo's own dev
|
|
16
|
+
// checkout) that is `template/`, the skeleton's real source; when it runs as a spawned project's own
|
|
17
|
+
// `scripts/init.mjs`, that is the project's own root, which already has every file, so every write is
|
|
18
|
+
// a no-op `skipped … (exists)` — never a broken read.
|
|
19
|
+
//
|
|
20
|
+
// ── The one rule ──────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// It NEVER overwrites a file that already exists (it prints `skipped <path> (exists)`), so it is safe
|
|
22
|
+
// to run on a partially-adopted repo. It writes nothing outside `Roadmap/`. Its own exit code is 0
|
|
23
|
+
// whether it wrote anything or not — "already adopted" is success, not a no-op failure.
|
|
24
|
+
//
|
|
25
|
+
// ── Two guards found by cross-review on #47 ───────────────────────────────────────────────────
|
|
26
|
+
// (1) COPIED mode, source === destination: `skeletonRoot()` in copied mode is `kitRoot()/..`, the
|
|
27
|
+
// SAME directory `projectRoot()` resolves to (this repo's own dev checkout runs exactly this way).
|
|
28
|
+
// So `join(source, rel)` and `join(project, rel)` are the identical path — reproduced by deleting
|
|
29
|
+
// one SKELETON file from a template/ checkout and running `node scripts/init.mjs`: the destination
|
|
30
|
+
// "doesn't exist" (it was deleted), so the copy proceeds, and the SOURCE (the same path) doesn't
|
|
31
|
+
// exist either — `copyFileSync` throws ENOENT instead of a clean skip. A stranger in installed mode
|
|
32
|
+
// never hits this (source and project are genuinely different directories there), but the repo's
|
|
33
|
+
// own dev loop and any future copied-mode caller can. Treated as `skipped`, same as "exists" — the
|
|
34
|
+
// file not being copyable from itself is not a failure this tool should crash on.
|
|
35
|
+
// (2) INSTALLED mode, project root resolves to `$HOME`: `projectRoot()` walks up to the nearest
|
|
36
|
+
// `Roadmap/` or `.git`. A non-git folder under a dotfiles repo tracked at `$HOME` (a common setup)
|
|
37
|
+
// has no closer match, so the walk lands on `$HOME` itself — and writing `Roadmap/` there would
|
|
38
|
+
// scatter the skeleton across the operator's actual home directory. Refused outright.
|
|
39
|
+
//
|
|
40
|
+
// Zero deps — Node 18+.
|
|
41
|
+
|
|
42
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
import { dirname, join, resolve } from 'node:path';
|
|
45
|
+
import { isInstalled, kitRoot, projectRoot } from './lib/project-root.mjs';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The Roadmap skeleton `gf-kit init` writes, relative both to its source root (template/, or
|
|
49
|
+
* kit/dist/skeleton/ once built) and to the project it writes into. ONE list: `build-kit.mjs` reads
|
|
50
|
+
* this export to populate `kit/dist/skeleton/`, so there is no second copy of the file names to drift.
|
|
51
|
+
*/
|
|
52
|
+
export const SKELETON = [
|
|
53
|
+
'Roadmap/README.md',
|
|
54
|
+
'Roadmap/WAYS-OF-WORKING.md',
|
|
55
|
+
'Roadmap/LEARNINGS.md',
|
|
56
|
+
'Roadmap/00-ideas/README.md',
|
|
57
|
+
'Roadmap/00-ideas/seeds/.gitkeep',
|
|
58
|
+
'Roadmap/00-ideas/audits/.gitkeep',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** Where the skeleton's SOURCE files live: `kitRoot()/skeleton` when installed, `kitRoot()/..` when copied. */
|
|
62
|
+
export function skeletonRoot({ root = kitRoot(), installed = isInstalled({ root }) } = {}) {
|
|
63
|
+
return installed ? join(root, 'skeleton') : join(root, '..');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Write the skeleton into `project`, never overwriting. Pure given its injected fs functions.
|
|
68
|
+
* Returns `{ wrote, skipped }`, each a list of the SKELETON-relative paths.
|
|
69
|
+
*/
|
|
70
|
+
export function initSkeleton({
|
|
71
|
+
project = projectRoot(),
|
|
72
|
+
source = skeletonRoot(),
|
|
73
|
+
files = SKELETON,
|
|
74
|
+
exists = existsSync,
|
|
75
|
+
mkdir = mkdirSync,
|
|
76
|
+
copy = copyFileSync,
|
|
77
|
+
log = () => {},
|
|
78
|
+
} = {}) {
|
|
79
|
+
const wrote = [];
|
|
80
|
+
const skipped = [];
|
|
81
|
+
for (const rel of files) {
|
|
82
|
+
const dest = join(project, rel);
|
|
83
|
+
if (exists(dest)) {
|
|
84
|
+
skipped.push(rel);
|
|
85
|
+
log(`skipped ${rel} (exists)`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const from = join(source, rel);
|
|
89
|
+
// Copied mode's source and destination roots can be the identical directory (see the header) —
|
|
90
|
+
// a source that is the destination, or that simply isn't there, is a skip, never a crash. The
|
|
91
|
+
// destination not existing (checked above) does NOT imply the source exists: they can be the
|
|
92
|
+
// same absent path.
|
|
93
|
+
if (from === dest || !exists(from)) {
|
|
94
|
+
skipped.push(rel);
|
|
95
|
+
log(`skipped ${rel} (${from === dest ? 'source is the destination' : 'source is absent'})`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
mkdir(dirname(dest), { recursive: true });
|
|
99
|
+
copy(from, dest);
|
|
100
|
+
wrote.push(rel);
|
|
101
|
+
log(`wrote ${rel}`);
|
|
102
|
+
}
|
|
103
|
+
return { wrote, skipped };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* True when `project` resolves to the operator's own home directory — the one place `gf-kit init`
|
|
108
|
+
* must never write, even though `projectRoot()`'s walk-up can legitimately land there (a non-git
|
|
109
|
+
* folder under a dotfiles repo tracked at `$HOME`). Pure given its injected `home`.
|
|
110
|
+
*/
|
|
111
|
+
export function isHomeDirectory({ project = projectRoot(), home = homedir() } = {}) {
|
|
112
|
+
return resolve(project) === resolve(home);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main() {
|
|
116
|
+
const project = projectRoot();
|
|
117
|
+
if (isHomeDirectory({ project })) {
|
|
118
|
+
console.error(
|
|
119
|
+
`gf-kit init: refusing to run — the resolved project root is your home directory (${project}).\n` +
|
|
120
|
+
' This usually means no Roadmap/ or .git was found between the current directory and $HOME,\n' +
|
|
121
|
+
' and $HOME itself is a git repo (a dotfiles checkout, for example). Run this from inside a\n' +
|
|
122
|
+
' real project — a directory with its own .git — or pass --root explicitly.'
|
|
123
|
+
);
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
const { wrote, skipped } = initSkeleton({ project, log: (line) => console.log(line) });
|
|
127
|
+
console.log(`gf-kit init: ${wrote.length} written, ${skipped.length} already present.`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('init.mjs');
|
|
132
|
+
if (isMain) process.exitCode = main();
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// config-registry.mjs — every setting Golden Frijoles can ask for, in one table (golden-frijoles-plugin S4.3, D11).
|
|
2
|
+
//
|
|
3
|
+
// This is the audit §4.3 table as data. Setup reads it for its questions, a script reads it before asking
|
|
4
|
+
// just-in-time (lib/config.mjs `needSetting`), and `gf doctor` reads it to say which modules are configured. A new
|
|
5
|
+
// setting is a new row here, never a question hard-coded in a skill.
|
|
6
|
+
//
|
|
7
|
+
// key dotted, `<section>.<name>`: the path in golden-frijoles.config.json
|
|
8
|
+
// module the product module doctor groups it under (Plan · Build · Ship · Measure · Spend · Operate)
|
|
9
|
+
// askWhen 'setup' | 'first-groom' | 'first-pr' | 'first-smoke' | 'first-report' | 'first-prune'
|
|
10
|
+
// | 'first-high-risk-groom' | 'never-yet' (declared so the schema is ready; nothing reads it this wave)
|
|
11
|
+
// default what a skipped question means. `null` = "unanswered", which a rail must treat as its safest choice
|
|
12
|
+
// question the plain words an agent asks, once
|
|
13
|
+
// choices optional allowed values; `store: 'env'` means the answer lives in .env.local (gf init), never here
|
|
14
|
+
//
|
|
15
|
+
// Zero deps, no imports: config.mjs imports THIS, so it must not import back.
|
|
16
|
+
|
|
17
|
+
export const REGISTRY = Object.freeze([
|
|
18
|
+
// ── Setup (golden-frijoles-plugin S5.1: Q1, Q2, Q4 are asked; Q3, Q5 wait for their modules, X18) ──
|
|
19
|
+
{
|
|
20
|
+
key: 'project.mode',
|
|
21
|
+
module: 'Plan',
|
|
22
|
+
askWhen: 'setup',
|
|
23
|
+
required: true,
|
|
24
|
+
default: 'existing',
|
|
25
|
+
choices: ['existing', 'new', 'planning-only'],
|
|
26
|
+
question: 'What are we working on: an existing repo (adds Roadmap/, keeps everything else), a new project, or planning only (no repo changes)?',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'project.startPoint',
|
|
30
|
+
module: 'Plan',
|
|
31
|
+
askWhen: 'setup',
|
|
32
|
+
default: 'idea',
|
|
33
|
+
choices: ['idea', 'plan', 'building'],
|
|
34
|
+
question: 'Where are you starting: an idea, you know what to build, or you are already building?',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'project.account',
|
|
38
|
+
module: 'Ship',
|
|
39
|
+
askWhen: 'setup',
|
|
40
|
+
default: 'later',
|
|
41
|
+
choices: ['later', 'now'],
|
|
42
|
+
store: 'env',
|
|
43
|
+
question: 'Connect a Golden Frijoles account now (gf login + gf init), or later?',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'board.sink',
|
|
47
|
+
module: 'Plan',
|
|
48
|
+
askWhen: 'never-yet',
|
|
49
|
+
default: 'terminal',
|
|
50
|
+
choices: ['terminal', 'golden-frijoles', 'notion'],
|
|
51
|
+
question: 'Where should the board live?',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'verify.depth',
|
|
55
|
+
module: 'Build',
|
|
56
|
+
askWhen: 'never-yet',
|
|
57
|
+
default: 'light',
|
|
58
|
+
choices: ['off', 'light', 'standard', 'deep'],
|
|
59
|
+
question: 'How much proof do you want on risky stories?',
|
|
60
|
+
},
|
|
61
|
+
// ── Just in time ──
|
|
62
|
+
{
|
|
63
|
+
key: 'roadmap.areas',
|
|
64
|
+
module: 'Plan',
|
|
65
|
+
askWhen: 'first-groom',
|
|
66
|
+
default: ['09 Platform & Infra'],
|
|
67
|
+
question: 'Which areas (macro-sections) does this roadmap have? 09 Platform & Infra is reserved.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: 'ways.fillIns',
|
|
71
|
+
module: 'Plan',
|
|
72
|
+
askWhen: 'first-groom',
|
|
73
|
+
default: 'Roadmap/fill-ins.yml',
|
|
74
|
+
question: 'Where are this project\'s WAYS-OF-WORKING fill-ins?',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'review.families',
|
|
78
|
+
module: 'Build',
|
|
79
|
+
askWhen: 'first-pr',
|
|
80
|
+
default: ['claude'],
|
|
81
|
+
question: 'Which reviewer CLIs do you have (codex, agy, vibe, claude)?',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: 'review.reviewScope',
|
|
85
|
+
module: 'Build',
|
|
86
|
+
askWhen: 'first-pr',
|
|
87
|
+
default: 'security-paths-only',
|
|
88
|
+
choices: ['every-pr', 'security-paths-only'],
|
|
89
|
+
question: 'Review every PR, or only PRs that touch security paths?',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: 'review.securityPaths',
|
|
93
|
+
module: 'Build',
|
|
94
|
+
askWhen: 'first-pr',
|
|
95
|
+
default: null,
|
|
96
|
+
question: 'Which paths are security-sensitive (globs)? Skipping keeps the template\'s list.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: 'jev.egress',
|
|
100
|
+
module: 'Build',
|
|
101
|
+
askWhen: 'first-pr',
|
|
102
|
+
default: null,
|
|
103
|
+
choices: [true, false],
|
|
104
|
+
question: 'Send PR review text and report drafts to TypeSafe (Jev) to judge their quality? Nothing is sent until you say yes.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
// NOT `smoke.envs`: that is live-smoke's own {name: url} map, and a list saved there broke it (review of #49).
|
|
108
|
+
key: 'smoke.defaultEnv',
|
|
109
|
+
module: 'Build',
|
|
110
|
+
askWhen: 'first-smoke',
|
|
111
|
+
default: 'local',
|
|
112
|
+
question: 'Which environment should live-smoke check when you don\'t name one (local, preview, production)?',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: 'reporting.destination',
|
|
116
|
+
module: 'Operate',
|
|
117
|
+
askWhen: 'first-report',
|
|
118
|
+
default: null,
|
|
119
|
+
question: 'Where should standups and recaps go (Telegram, Slack, or print to the terminal)? Secrets stay in .env.local.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: 'deploy.vercelProject',
|
|
123
|
+
module: 'Operate',
|
|
124
|
+
askWhen: 'first-prune',
|
|
125
|
+
default: null,
|
|
126
|
+
question: 'Which Vercel project should stale previews be counted for?',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: 'ship.killSwitchPolicy',
|
|
130
|
+
module: 'Ship',
|
|
131
|
+
askWhen: 'first-high-risk-groom',
|
|
132
|
+
default: 'every-risk-high-story-names-its-flag',
|
|
133
|
+
question: 'Should every risk:high story name its kill-switch flag?',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: 'spend.telemetry',
|
|
137
|
+
module: 'Spend',
|
|
138
|
+
askWhen: 'never-yet',
|
|
139
|
+
default: 'off',
|
|
140
|
+
choices: ['off', 'on'],
|
|
141
|
+
question: 'Export cost telemetry?',
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
export const MODULES = Object.freeze(['Plan', 'Build', 'Ship', 'Measure', 'Spend', 'Operate']);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Types for lib/config.mjs, the one config core (golden-frijoles-plugin D10). Shipped in @golden-frijoles/kit as
|
|
2
|
+
// `@golden-frijoles/kit/config` so the TypeScript `gf` CLI imports the same rules instead of re-implementing them.
|
|
3
|
+
|
|
4
|
+
export declare const CONFIG_FILENAME: 'golden-frijoles.config.json';
|
|
5
|
+
export declare const SECTIONS: readonly string[];
|
|
6
|
+
export declare const LEGACY: Readonly<Record<string, string>>;
|
|
7
|
+
export declare const NEEDS_SETTING: 'GF-NEEDS-SETTING';
|
|
8
|
+
export declare const EXIT_NEEDS_SETTING: 7;
|
|
9
|
+
|
|
10
|
+
export declare class ConfigError extends Error {}
|
|
11
|
+
|
|
12
|
+
export interface IoOptions {
|
|
13
|
+
root?: string;
|
|
14
|
+
read?: (path: string, encoding: 'utf8') => string;
|
|
15
|
+
exists?: (path: string) => boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Section {
|
|
19
|
+
raw: Record<string, unknown> | null;
|
|
20
|
+
/** True when either file has the section, even when its value is `null` in the legacy file. */
|
|
21
|
+
present: boolean;
|
|
22
|
+
sources: string[];
|
|
23
|
+
duplicates: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The project root: GF_PROJECT_ROOT, else (installed) the nearest ancestor of `cwd` with Roadmap/ or .git, else `cwd`. */
|
|
27
|
+
export declare function projectRoot(opts?: { env?: Record<string, string | undefined>; cwd?: string }): string;
|
|
28
|
+
export declare function readConfigFile(opts?: IoOptions): Record<string, unknown> | null;
|
|
29
|
+
export declare function readSection(
|
|
30
|
+
name: string,
|
|
31
|
+
opts?: IoOptions & {
|
|
32
|
+
legacyPath?: string;
|
|
33
|
+
onLegacyError?: (path: string, error: Error) => never;
|
|
34
|
+
legacyRead?: IoOptions['read'];
|
|
35
|
+
legacyExists?: IoOptions['exists'];
|
|
36
|
+
}
|
|
37
|
+
): Section;
|
|
38
|
+
export declare function loadConfig(opts?: IoOptions): {
|
|
39
|
+
sections: Record<string, Record<string, unknown>>;
|
|
40
|
+
sources: Record<string, string[]>;
|
|
41
|
+
duplicates: string[];
|
|
42
|
+
/** Top-level keys of golden-frijoles.config.json that are not a known section (ignored on read). */
|
|
43
|
+
unknown: string[];
|
|
44
|
+
};
|
|
45
|
+
export declare function getKey(key: string, opts?: IoOptions): unknown;
|
|
46
|
+
export declare function setKey(
|
|
47
|
+
key: string,
|
|
48
|
+
value: unknown,
|
|
49
|
+
opts?: IoOptions & { write?: (path: string, obj: unknown) => void }
|
|
50
|
+
): Record<string, unknown>;
|
|
51
|
+
export declare function migrate(
|
|
52
|
+
opts?: IoOptions & { write?: (path: string, obj: unknown) => void; dryRun?: boolean }
|
|
53
|
+
): { config: Record<string, unknown>; folded: string[]; skipped: string[] };
|
|
54
|
+
export declare function looksLikeSecret(key: string, value: unknown): boolean;
|
|
55
|
+
export declare function needsSettingLine(entry: { key: string; question: string; default: unknown }): string;
|
|
56
|
+
export declare function needSetting(
|
|
57
|
+
key: string,
|
|
58
|
+
opts?: IoOptions & { blocking?: boolean; write?: (s: string) => void; exit?: (code: number) => void }
|
|
59
|
+
): unknown;
|
|
60
|
+
|
|
61
|
+
export interface RegistryEntry {
|
|
62
|
+
key: string;
|
|
63
|
+
module: 'Plan' | 'Build' | 'Ship' | 'Measure' | 'Spend' | 'Operate';
|
|
64
|
+
askWhen: string;
|
|
65
|
+
default: unknown;
|
|
66
|
+
question: string;
|
|
67
|
+
required?: boolean;
|
|
68
|
+
choices?: readonly unknown[];
|
|
69
|
+
store?: 'env';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export declare const REGISTRY: readonly RegistryEntry[];
|
|
73
|
+
export declare const MODULES: readonly ['Plan', 'Build', 'Ship', 'Measure', 'Spend', 'Operate'];
|