u-foo 1.0.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/LICENSE +35 -0
- package/README.md +163 -0
- package/README.zh-CN.md +163 -0
- package/bin/uclaude +65 -0
- package/bin/ucodex +65 -0
- package/bin/ufoo +93 -0
- package/bin/ufoo.js +35 -0
- package/modules/AGENTS.template.md +87 -0
- package/modules/bus/README.md +132 -0
- package/modules/bus/SKILLS/ubus/SKILL.md +209 -0
- package/modules/bus/scripts/bus-alert.sh +185 -0
- package/modules/bus/scripts/bus-listen.sh +117 -0
- package/modules/context/ASSUMPTIONS.md +7 -0
- package/modules/context/CONSTRAINTS.md +7 -0
- package/modules/context/CONTEXT-STRUCTURE.md +49 -0
- package/modules/context/DECISION-PROTOCOL.md +62 -0
- package/modules/context/HANDOFF.md +33 -0
- package/modules/context/README.md +82 -0
- package/modules/context/RULES.md +15 -0
- package/modules/context/SKILLS/README.md +14 -0
- package/modules/context/SKILLS/uctx/SKILL.md +91 -0
- package/modules/context/SYSTEM.md +18 -0
- package/modules/context/TEMPLATES/assumptions.md +4 -0
- package/modules/context/TEMPLATES/constraints.md +4 -0
- package/modules/context/TEMPLATES/decision.md +16 -0
- package/modules/context/TEMPLATES/project-context-readme.md +6 -0
- package/modules/context/TEMPLATES/system.md +3 -0
- package/modules/context/TEMPLATES/terminology.md +4 -0
- package/modules/context/TERMINOLOGY.md +10 -0
- package/modules/resources/ICONS/README.md +12 -0
- package/modules/resources/ICONS/libraries/README.md +17 -0
- package/modules/resources/ICONS/libraries/heroicons/LICENSE +22 -0
- package/modules/resources/ICONS/libraries/heroicons/README.md +15 -0
- package/modules/resources/ICONS/libraries/heroicons/arrow-right.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/check.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/chevron-down.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/cog-6-tooth.svg +5 -0
- package/modules/resources/ICONS/libraries/heroicons/magnifying-glass.svg +4 -0
- package/modules/resources/ICONS/libraries/heroicons/x-mark.svg +4 -0
- package/modules/resources/ICONS/libraries/lucide/LICENSE +40 -0
- package/modules/resources/ICONS/libraries/lucide/README.md +15 -0
- package/modules/resources/ICONS/libraries/lucide/arrow-right.svg +15 -0
- package/modules/resources/ICONS/libraries/lucide/check.svg +14 -0
- package/modules/resources/ICONS/libraries/lucide/chevron-down.svg +14 -0
- package/modules/resources/ICONS/libraries/lucide/search.svg +15 -0
- package/modules/resources/ICONS/libraries/lucide/settings.svg +15 -0
- package/modules/resources/ICONS/libraries/lucide/x.svg +15 -0
- package/modules/resources/ICONS/rules.md +7 -0
- package/modules/resources/README.md +9 -0
- package/modules/resources/UI/ANTI-PATTERNS.md +6 -0
- package/modules/resources/UI/TONE.md +6 -0
- package/package.json +40 -0
- package/scripts/banner.sh +89 -0
- package/scripts/bus-alert.sh +6 -0
- package/scripts/bus-autotrigger.sh +6 -0
- package/scripts/bus-daemon.sh +231 -0
- package/scripts/bus-inject.sh +144 -0
- package/scripts/bus-listen.sh +6 -0
- package/scripts/bus.sh +984 -0
- package/scripts/context-decisions.sh +167 -0
- package/scripts/context-doctor.sh +72 -0
- package/scripts/context-lint.sh +110 -0
- package/scripts/doctor.sh +22 -0
- package/scripts/init.sh +247 -0
- package/scripts/skills.sh +113 -0
- package/scripts/status.sh +125 -0
- package/src/agent/cliRunner.js +190 -0
- package/src/agent/internalRunner.js +212 -0
- package/src/agent/normalizeOutput.js +41 -0
- package/src/agent/ufooAgent.js +222 -0
- package/src/chat/index.js +1603 -0
- package/src/cli.js +349 -0
- package/src/config.js +37 -0
- package/src/daemon/index.js +501 -0
- package/src/daemon/ops.js +120 -0
- package/src/daemon/run.js +41 -0
- package/src/daemon/status.js +78 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { spawnSync } = require("child_process");
|
|
3
|
+
|
|
4
|
+
function getPackageRoot() {
|
|
5
|
+
return path.resolve(__dirname, "..");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function run(cmd, args, options = {}) {
|
|
9
|
+
const res = spawnSync(cmd, args, {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
...options,
|
|
12
|
+
});
|
|
13
|
+
if (res.error) throw res.error;
|
|
14
|
+
if (typeof res.status === "number" && res.status !== 0) {
|
|
15
|
+
const e = new Error(`${cmd} exited with code ${res.status}`);
|
|
16
|
+
e.code = res.status;
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPackageScript(rel) {
|
|
22
|
+
return path.join(getPackageRoot(), rel);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireOptional(name) {
|
|
26
|
+
try {
|
|
27
|
+
// eslint-disable-next-line global-require, import/no-dynamic-require
|
|
28
|
+
return require(name);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function runCli(argv) {
|
|
35
|
+
const pkg = require(path.resolve(getPackageRoot(), "package.json"));
|
|
36
|
+
|
|
37
|
+
const commander = requireOptional("commander");
|
|
38
|
+
const chalk = requireOptional("chalk") || { cyan: (s) => s, red: (s) => s };
|
|
39
|
+
|
|
40
|
+
if (commander && commander.Command) {
|
|
41
|
+
const { Command } = commander;
|
|
42
|
+
const program = new Command();
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.name("ufoo")
|
|
46
|
+
.description("ufoo CLI (wrapper-first; prefers project-local scripts).")
|
|
47
|
+
.version(pkg.version);
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command("doctor")
|
|
51
|
+
.description("Run repo doctor checks")
|
|
52
|
+
.action(() => {
|
|
53
|
+
const repoRoot = getPackageRoot();
|
|
54
|
+
run("bash", [path.join(repoRoot, "scripts/doctor.sh")]);
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command("status")
|
|
58
|
+
.description("Show project status (banner, unread bus, open decisions)")
|
|
59
|
+
.action(() => {
|
|
60
|
+
const repoRoot = getPackageRoot();
|
|
61
|
+
run("bash", [path.join(repoRoot, "scripts/status.sh")]);
|
|
62
|
+
});
|
|
63
|
+
program
|
|
64
|
+
.command("daemon")
|
|
65
|
+
.description("Start/stop ufoo daemon")
|
|
66
|
+
.option("--start", "Start daemon")
|
|
67
|
+
.option("--stop", "Stop daemon")
|
|
68
|
+
.option("--status", "Check daemon status")
|
|
69
|
+
.action((opts) => {
|
|
70
|
+
const repoRoot = getPackageRoot();
|
|
71
|
+
const args = ["daemon"];
|
|
72
|
+
if (opts.start) args.push("start");
|
|
73
|
+
else if (opts.stop) args.push("stop");
|
|
74
|
+
else if (opts.status) args.push("status");
|
|
75
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...args]);
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command("chat")
|
|
79
|
+
.description("Launch ufoo chat UI")
|
|
80
|
+
.action(() => {
|
|
81
|
+
const repoRoot = getPackageRoot();
|
|
82
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "chat"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
program
|
|
86
|
+
.command("init")
|
|
87
|
+
.description("Initialize modules in a project")
|
|
88
|
+
.option("--modules <list>", "Comma-separated modules (context,bus,resources)", "context")
|
|
89
|
+
.option("--project <dir>", "Target project directory", process.cwd())
|
|
90
|
+
.action((opts) => {
|
|
91
|
+
const repoRoot = getPackageRoot();
|
|
92
|
+
run("bash", [
|
|
93
|
+
path.join(repoRoot, "scripts/init.sh"),
|
|
94
|
+
"--modules",
|
|
95
|
+
opts.modules,
|
|
96
|
+
"--project",
|
|
97
|
+
opts.project,
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const skills = program.command("skills").description("Manage skills templates");
|
|
102
|
+
skills
|
|
103
|
+
.command("list")
|
|
104
|
+
.description("List available skills")
|
|
105
|
+
.action(() => {
|
|
106
|
+
const repoRoot = getPackageRoot();
|
|
107
|
+
run("bash", [path.join(repoRoot, "scripts/skills.sh"), "list"]);
|
|
108
|
+
});
|
|
109
|
+
skills
|
|
110
|
+
.command("install")
|
|
111
|
+
.description("Install one skill or all skills")
|
|
112
|
+
.argument("<name>", "Skill name or 'all'")
|
|
113
|
+
.option("--target <dir>", "Install target directory")
|
|
114
|
+
.option("--codex", "Install into ~/.codex/skills")
|
|
115
|
+
.option("--agents", "Install into ~/.agents/skills")
|
|
116
|
+
.action((name, opts) => {
|
|
117
|
+
const repoRoot = getPackageRoot();
|
|
118
|
+
const args = [path.join(repoRoot, "scripts/skills.sh"), "install", name];
|
|
119
|
+
if (opts.target) args.push("--target", opts.target);
|
|
120
|
+
if (opts.codex) args.push("--codex");
|
|
121
|
+
if (opts.agents) args.push("--agents");
|
|
122
|
+
run("bash", args);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const bus = program.command("bus").description("Project bus commands");
|
|
126
|
+
bus
|
|
127
|
+
.command("alert")
|
|
128
|
+
.description("Start/stop background notification daemon")
|
|
129
|
+
.argument("<subscriber>", "Subscriber ID (e.g., claude-code:abc123)")
|
|
130
|
+
.argument("[interval]", "Poll interval in seconds", "2")
|
|
131
|
+
.option("--notify", "Enable macOS Notification Center")
|
|
132
|
+
.option("--daemon", "Run in background")
|
|
133
|
+
.option("--stop", "Stop running alert for this subscriber")
|
|
134
|
+
.option("--no-title", "Disable terminal title badge")
|
|
135
|
+
.option("--no-bell", "Disable terminal bell")
|
|
136
|
+
.allowUnknownOption(true)
|
|
137
|
+
.action((subscriber, interval, opts) => {
|
|
138
|
+
const script = getPackageScript("scripts/bus-alert.sh");
|
|
139
|
+
const args = [script, subscriber, interval];
|
|
140
|
+
if (opts.notify) args.push("--notify");
|
|
141
|
+
if (opts.daemon) args.push("--daemon");
|
|
142
|
+
if (opts.stop) args.push("--stop");
|
|
143
|
+
if (opts.title === false) args.push("--no-title");
|
|
144
|
+
if (opts.bell === false) args.push("--no-bell");
|
|
145
|
+
run("bash", args);
|
|
146
|
+
});
|
|
147
|
+
bus
|
|
148
|
+
.command("listen")
|
|
149
|
+
.description("Foreground listener for incoming messages")
|
|
150
|
+
.argument("<subscriber>", "Subscriber ID")
|
|
151
|
+
.option("--from-beginning", "Print existing queued messages first")
|
|
152
|
+
.option("--reset", "Truncate pending queue before listening")
|
|
153
|
+
.option("--auto-join", "Auto-join bus to get subscriber ID")
|
|
154
|
+
.action((subscriber, opts) => {
|
|
155
|
+
const script = getPackageScript("scripts/bus-listen.sh");
|
|
156
|
+
const args = [script, subscriber];
|
|
157
|
+
if (opts.fromBeginning) args.push("--from-beginning");
|
|
158
|
+
if (opts.reset) args.push("--reset");
|
|
159
|
+
if (opts.autoJoin) args.push("--auto-join");
|
|
160
|
+
run("bash", args);
|
|
161
|
+
});
|
|
162
|
+
bus
|
|
163
|
+
.command("daemon")
|
|
164
|
+
.description("Start/stop daemon that auto-injects /bus into terminals")
|
|
165
|
+
.option("--interval <n>", "Poll interval in seconds", "2")
|
|
166
|
+
.option("--daemon", "Run in background")
|
|
167
|
+
.option("--stop", "Stop running daemon")
|
|
168
|
+
.option("--status", "Check daemon status")
|
|
169
|
+
.action((opts) => {
|
|
170
|
+
const script = getPackageScript("scripts/bus-daemon.sh");
|
|
171
|
+
const args = [script];
|
|
172
|
+
if (opts.interval) args.push("--interval", opts.interval);
|
|
173
|
+
if (opts.daemon) args.push("--daemon");
|
|
174
|
+
if (opts.stop) args.push("--stop");
|
|
175
|
+
if (opts.status) args.push("--status");
|
|
176
|
+
run("bash", args);
|
|
177
|
+
});
|
|
178
|
+
bus
|
|
179
|
+
.command("inject")
|
|
180
|
+
.description("Inject /bus into a Terminal.app tab by subscriber ID")
|
|
181
|
+
.argument("<subscriber>", "Subscriber ID to inject into")
|
|
182
|
+
.action((subscriber) => {
|
|
183
|
+
const script = getPackageScript("scripts/bus-inject.sh");
|
|
184
|
+
run("bash", [script, subscriber]);
|
|
185
|
+
});
|
|
186
|
+
bus
|
|
187
|
+
.command("run", { isDefault: true })
|
|
188
|
+
.description("Run bus.sh commands (join, check, send, status, etc.)")
|
|
189
|
+
.allowUnknownOption(true)
|
|
190
|
+
.argument("<args...>", "Arguments passed to scripts/bus.sh")
|
|
191
|
+
.action((args) => {
|
|
192
|
+
const script = getPackageScript("scripts/bus.sh");
|
|
193
|
+
run("bash", [script, ...args]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
program
|
|
197
|
+
.command("ctx")
|
|
198
|
+
.description("Project ctx commands (delegates to ./scripts/context-*.sh)")
|
|
199
|
+
.argument("[subcmd]", "Subcommand (doctor|lint|decisions)", "doctor")
|
|
200
|
+
.allowUnknownOption(true)
|
|
201
|
+
.argument("[subargs...]", "Subcommand args")
|
|
202
|
+
.action((subcmd, subargs = []) => {
|
|
203
|
+
const map = {
|
|
204
|
+
doctor: "scripts/context-doctor.sh",
|
|
205
|
+
lint: "scripts/context-lint.sh",
|
|
206
|
+
decisions: "scripts/context-decisions.sh",
|
|
207
|
+
};
|
|
208
|
+
const rel = map[subcmd];
|
|
209
|
+
if (!rel) {
|
|
210
|
+
console.error(
|
|
211
|
+
chalk.red(
|
|
212
|
+
`Unknown ctx subcommand: ${subcmd}. Supported: ${Object.keys(map).join(", ")}`
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const script = getPackageScript(rel);
|
|
219
|
+
run("bash", [script, ...subargs]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
program.addHelpText(
|
|
223
|
+
"after",
|
|
224
|
+
`\nNotes:\n - If 'ufoo' isn't in PATH, run it via ${chalk.cyan(
|
|
225
|
+
"./bin/ufoo"
|
|
226
|
+
)} (repo) or install globally via npm.\n - For bus notifications inside Codex, prefer ${chalk.cyan(
|
|
227
|
+
"scripts/bus-alert.sh"
|
|
228
|
+
)} / ${chalk.cyan("scripts/bus-listen.sh")} (no IME issues).\n`
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
await program.parseAsync(argv);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Dependency-free fallback parser (good for local testing without npm install).
|
|
236
|
+
const cmd = argv[2] || "";
|
|
237
|
+
const rest = argv.slice(3);
|
|
238
|
+
const repoRoot = getPackageRoot();
|
|
239
|
+
|
|
240
|
+
const help = () => {
|
|
241
|
+
console.log(`ufoo ${pkg.version}`);
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log("Usage:");
|
|
244
|
+
console.log(" ufoo doctor");
|
|
245
|
+
console.log(" ufoo status");
|
|
246
|
+
console.log(" ufoo daemon --start|--stop|--status");
|
|
247
|
+
console.log(" ufoo chat");
|
|
248
|
+
console.log(" ufoo init [--modules <list>] [--project <dir>]");
|
|
249
|
+
console.log(" ufoo skills list");
|
|
250
|
+
console.log(" ufoo skills install <name|all> [--target <dir> | --codex | --agents]");
|
|
251
|
+
console.log(" ufoo bus <args...> (delegates to ./scripts/bus.sh)");
|
|
252
|
+
console.log(" ufoo ctx <subcmd> ... (doctor|lint|decisions)");
|
|
253
|
+
console.log("");
|
|
254
|
+
console.log("Notes:");
|
|
255
|
+
console.log(" - For Codex notifications, use scripts/bus-alert.sh / scripts/bus-listen.sh");
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (cmd === "" || cmd === "--help" || cmd === "-h") {
|
|
259
|
+
help();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (cmd === "doctor") {
|
|
264
|
+
run("bash", [path.join(repoRoot, "scripts/doctor.sh")]);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (cmd === "status") {
|
|
268
|
+
run("bash", [path.join(repoRoot, "scripts/status.sh")]);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (cmd === "daemon") {
|
|
272
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "daemon", ...rest]);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (cmd === "chat") {
|
|
276
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), "chat"]);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (cmd === "init") {
|
|
280
|
+
const getOpt = (name, def) => {
|
|
281
|
+
const i = rest.indexOf(name);
|
|
282
|
+
if (i === -1) return def;
|
|
283
|
+
if (i + 1 >= rest.length) throw new Error(`Missing value for ${name}`);
|
|
284
|
+
return rest[i + 1];
|
|
285
|
+
};
|
|
286
|
+
run("bash", [
|
|
287
|
+
path.join(repoRoot, "scripts/init.sh"),
|
|
288
|
+
"--modules",
|
|
289
|
+
getOpt("--modules", "context"),
|
|
290
|
+
"--project",
|
|
291
|
+
getOpt("--project", process.cwd()),
|
|
292
|
+
]);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (cmd === "skills") {
|
|
296
|
+
const sub = rest[0] || "";
|
|
297
|
+
if (sub === "list") {
|
|
298
|
+
run("bash", [path.join(repoRoot, "scripts/skills.sh"), "list"]);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (sub === "install") {
|
|
302
|
+
const name = rest[1];
|
|
303
|
+
if (!name) throw new Error("skills install requires <name|all>");
|
|
304
|
+
run("bash", [path.join(repoRoot, "scripts/skills.sh"), "install", ...rest.slice(1)]);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
help();
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (cmd === "bus") {
|
|
312
|
+
const sub = rest[0] || "";
|
|
313
|
+
if (sub === "alert") {
|
|
314
|
+
run("bash", [getPackageScript("scripts/bus-alert.sh"), ...rest.slice(1)]);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (sub === "listen") {
|
|
318
|
+
run("bash", [getPackageScript("scripts/bus-listen.sh"), ...rest.slice(1)]);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (sub === "daemon") {
|
|
322
|
+
run("bash", [getPackageScript("scripts/bus-daemon.sh"), ...rest.slice(1)]);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (sub === "inject") {
|
|
326
|
+
run("bash", [getPackageScript("scripts/bus-inject.sh"), ...rest.slice(1)]);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
run("bash", [getPackageScript("scripts/bus.sh"), ...rest]);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (cmd === "ctx") {
|
|
333
|
+
const sub = rest[0] || "doctor";
|
|
334
|
+
const map = {
|
|
335
|
+
doctor: "scripts/context-doctor.sh",
|
|
336
|
+
lint: "scripts/context-lint.sh",
|
|
337
|
+
decisions: "scripts/context-decisions.sh",
|
|
338
|
+
};
|
|
339
|
+
const rel = map[sub];
|
|
340
|
+
if (!rel) throw new Error(`Unknown ctx subcommand: ${sub}`);
|
|
341
|
+
run("bash", [getPackageScript(rel), ...rest.slice(1)]);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
help();
|
|
346
|
+
process.exitCode = 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { runCli };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
launchMode: "terminal",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function normalizeLaunchMode(value) {
|
|
9
|
+
return value === "internal" ? "internal" : "terminal";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function configPath(projectRoot) {
|
|
13
|
+
return path.join(projectRoot, ".ufoo", "config.json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadConfig(projectRoot) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(configPath(projectRoot), "utf8"));
|
|
19
|
+
return { ...DEFAULT_CONFIG, ...raw, launchMode: normalizeLaunchMode(raw.launchMode) };
|
|
20
|
+
} catch {
|
|
21
|
+
return { ...DEFAULT_CONFIG };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function saveConfig(projectRoot, config) {
|
|
26
|
+
const target = configPath(projectRoot);
|
|
27
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
28
|
+
const merged = {
|
|
29
|
+
...DEFAULT_CONFIG,
|
|
30
|
+
...config,
|
|
31
|
+
};
|
|
32
|
+
merged.launchMode = normalizeLaunchMode(merged.launchMode);
|
|
33
|
+
fs.writeFileSync(target, JSON.stringify(merged, null, 2));
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { loadConfig, saveConfig, normalizeLaunchMode };
|