@dobby.ai/dobby 0.1.1 → 0.1.2

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.
Files changed (136) hide show
  1. package/README.md +20 -7
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/program.js +0 -6
  5. package/dist/src/core/types.js +2 -0
  6. package/dist/src/cron/config.js +2 -2
  7. package/dist/src/cron/service.js +87 -23
  8. package/dist/src/cron/store.js +1 -1
  9. package/package.json +9 -3
  10. package/.env.example +0 -8
  11. package/AGENTS.md +0 -267
  12. package/ROADMAP.md +0 -34
  13. package/config/cron.example.json +0 -9
  14. package/config/gateway.example.json +0 -132
  15. package/dist/plugins/connector-discord/src/mapper.js +0 -75
  16. package/dist/src/cli/tests/config-command.test.js +0 -42
  17. package/dist/src/cli/tests/config-io.test.js +0 -64
  18. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  19. package/dist/src/cli/tests/discord-mapper.test.js +0 -90
  20. package/dist/src/cli/tests/doctor.test.js +0 -252
  21. package/dist/src/cli/tests/init-catalog.test.js +0 -134
  22. package/dist/src/cli/tests/program-options.test.js +0 -78
  23. package/dist/src/cli/tests/routing-config.test.js +0 -254
  24. package/dist/src/core/tests/control-command.test.js +0 -17
  25. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  26. package/dist/src/core/tests/typing-controller.test.js +0 -103
  27. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  28. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  29. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  30. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  31. package/docs/MVP.md +0 -135
  32. package/docs/RUNBOOK.md +0 -243
  33. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  34. package/plugins/connector-discord/dobby.manifest.json +0 -18
  35. package/plugins/connector-discord/index.js +0 -1
  36. package/plugins/connector-discord/package-lock.json +0 -360
  37. package/plugins/connector-discord/package.json +0 -38
  38. package/plugins/connector-discord/src/connector.ts +0 -345
  39. package/plugins/connector-discord/src/contribution.ts +0 -21
  40. package/plugins/connector-discord/src/mapper.ts +0 -101
  41. package/plugins/connector-discord/tsconfig.json +0 -19
  42. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  43. package/plugins/connector-feishu/index.js +0 -1
  44. package/plugins/connector-feishu/package-lock.json +0 -618
  45. package/plugins/connector-feishu/package.json +0 -38
  46. package/plugins/connector-feishu/src/connector.ts +0 -343
  47. package/plugins/connector-feishu/src/contribution.ts +0 -26
  48. package/plugins/connector-feishu/src/mapper.ts +0 -401
  49. package/plugins/connector-feishu/tsconfig.json +0 -19
  50. package/plugins/plugin-sdk/index.d.ts +0 -261
  51. package/plugins/plugin-sdk/index.js +0 -1
  52. package/plugins/plugin-sdk/package-lock.json +0 -12
  53. package/plugins/plugin-sdk/package.json +0 -22
  54. package/plugins/provider-claude/dobby.manifest.json +0 -17
  55. package/plugins/provider-claude/index.js +0 -1
  56. package/plugins/provider-claude/package-lock.json +0 -3398
  57. package/plugins/provider-claude/package.json +0 -39
  58. package/plugins/provider-claude/src/contribution.ts +0 -1018
  59. package/plugins/provider-claude/tsconfig.json +0 -19
  60. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  61. package/plugins/provider-claude-cli/index.js +0 -1
  62. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  63. package/plugins/provider-claude-cli/package.json +0 -38
  64. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  65. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  66. package/plugins/provider-pi/dobby.manifest.json +0 -17
  67. package/plugins/provider-pi/index.js +0 -1
  68. package/plugins/provider-pi/package-lock.json +0 -3877
  69. package/plugins/provider-pi/package.json +0 -40
  70. package/plugins/provider-pi/src/contribution.ts +0 -606
  71. package/plugins/provider-pi/tsconfig.json +0 -19
  72. package/plugins/sandbox-core/boxlite.js +0 -1
  73. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  74. package/plugins/sandbox-core/docker.js +0 -1
  75. package/plugins/sandbox-core/package-lock.json +0 -136
  76. package/plugins/sandbox-core/package.json +0 -39
  77. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  78. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  79. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  80. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  81. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  82. package/plugins/sandbox-core/tsconfig.json +0 -19
  83. package/scripts/local-extensions.mjs +0 -168
  84. package/src/agent/event-forwarder.ts +0 -414
  85. package/src/cli/commands/config.ts +0 -328
  86. package/src/cli/commands/configure.ts +0 -92
  87. package/src/cli/commands/cron.ts +0 -410
  88. package/src/cli/commands/doctor.ts +0 -331
  89. package/src/cli/commands/extension.ts +0 -207
  90. package/src/cli/commands/init.ts +0 -211
  91. package/src/cli/commands/start.ts +0 -223
  92. package/src/cli/commands/topology.ts +0 -415
  93. package/src/cli/index.ts +0 -9
  94. package/src/cli/program.ts +0 -314
  95. package/src/cli/shared/config-io.ts +0 -245
  96. package/src/cli/shared/config-mutators.ts +0 -470
  97. package/src/cli/shared/config-schema.ts +0 -228
  98. package/src/cli/shared/config-types.ts +0 -129
  99. package/src/cli/shared/configure-sections.ts +0 -595
  100. package/src/cli/shared/discord-config.ts +0 -14
  101. package/src/cli/shared/init-catalog.ts +0 -249
  102. package/src/cli/shared/local-extension-specs.ts +0 -108
  103. package/src/cli/shared/runtime.ts +0 -33
  104. package/src/cli/shared/schema-prompts.ts +0 -443
  105. package/src/cli/tests/config-command.test.ts +0 -56
  106. package/src/cli/tests/config-io.test.ts +0 -92
  107. package/src/cli/tests/config-mutators.test.ts +0 -59
  108. package/src/cli/tests/discord-mapper.test.ts +0 -128
  109. package/src/cli/tests/doctor.test.ts +0 -269
  110. package/src/cli/tests/init-catalog.test.ts +0 -144
  111. package/src/cli/tests/program-options.test.ts +0 -95
  112. package/src/cli/tests/routing-config.test.ts +0 -281
  113. package/src/core/control-command.ts +0 -12
  114. package/src/core/dedup-store.ts +0 -103
  115. package/src/core/gateway.ts +0 -609
  116. package/src/core/routing.ts +0 -404
  117. package/src/core/runtime-registry.ts +0 -141
  118. package/src/core/tests/control-command.test.ts +0 -20
  119. package/src/core/tests/runtime-registry.test.ts +0 -140
  120. package/src/core/tests/typing-controller.test.ts +0 -129
  121. package/src/core/types.ts +0 -324
  122. package/src/core/typing-controller.ts +0 -119
  123. package/src/cron/config.ts +0 -154
  124. package/src/cron/schedule.ts +0 -61
  125. package/src/cron/service.ts +0 -249
  126. package/src/cron/store.ts +0 -155
  127. package/src/cron/types.ts +0 -60
  128. package/src/extension/loader.ts +0 -145
  129. package/src/extension/manager.ts +0 -355
  130. package/src/extension/manifest.ts +0 -26
  131. package/src/extension/registry.ts +0 -229
  132. package/src/main.ts +0 -8
  133. package/src/sandbox/executor.ts +0 -44
  134. package/src/sandbox/host-executor.ts +0 -118
  135. package/src/shared/dobby-repo.ts +0 -48
  136. package/tsconfig.json +0 -18
@@ -1,314 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { fileURLToPath } from "node:url";
3
- import { Command } from "commander";
4
- import {
5
- runConfigListCommand,
6
- runConfigSchemaListCommand,
7
- runConfigSchemaShowCommand,
8
- runConfigShowCommand,
9
- } from "./commands/config.js";
10
- import {
11
- runCronAddCommand,
12
- runCronListCommand,
13
- runCronPauseCommand,
14
- runCronRemoveCommand,
15
- runCronResumeCommand,
16
- runCronRunCommand,
17
- runCronStatusCommand,
18
- runCronUpdateCommand,
19
- } from "./commands/cron.js";
20
- import { runDoctorCommand } from "./commands/doctor.js";
21
- import {
22
- runExtensionInstallCommand,
23
- runExtensionListCommand,
24
- runExtensionUninstallCommand,
25
- } from "./commands/extension.js";
26
- import { runInitCommand } from "./commands/init.js";
27
- import { runStartCommand } from "./commands/start.js";
28
-
29
- function loadCliVersion(): string {
30
- const candidates = [
31
- fileURLToPath(new URL("../../package.json", import.meta.url)),
32
- fileURLToPath(new URL("../../../package.json", import.meta.url)),
33
- ];
34
- const fallbackCandidate = fileURLToPath(new URL("../../package.json", import.meta.url));
35
- const packageJsonPath = candidates.find((candidate) => existsSync(candidate)) ?? fallbackCandidate;
36
- return JSON.parse(readFileSync(packageJsonPath, "utf-8")).version as string;
37
- }
38
-
39
- const CLI_VERSION = loadCliVersion();
40
-
41
- /**
42
- * Builds the top-level dobby CLI program and registers all subcommands.
43
- */
44
- export function buildProgram(): Command {
45
- const program = new Command();
46
- program
47
- .name("dobby")
48
- .version(CLI_VERSION)
49
- .description("Discord-first local agent gateway")
50
- .showHelpAfterError()
51
- .action(async () => {
52
- await runStartCommand();
53
- });
54
-
55
- program
56
- .command("start")
57
- .description("Start the gateway")
58
- .action(async () => {
59
- await runStartCommand();
60
- });
61
-
62
- program
63
- .command("init")
64
- .description("Initialize minimal runnable gateway config")
65
- .action(async () => {
66
- await runInitCommand();
67
- });
68
-
69
- const configCommand = program.command("config").description("Inspect config");
70
-
71
- configCommand
72
- .command("show")
73
- .description("Show full config or one section")
74
- .argument("[section]", "Section: providers|connectors|routes|bindings|sandboxes|data|extensions")
75
- .option("--json", "Output JSON", false)
76
- .action(async (section: string | undefined, opts) => {
77
- await runConfigShowCommand({
78
- ...(typeof section === "string" ? { section } : {}),
79
- json: Boolean(opts.json),
80
- });
81
- });
82
-
83
- configCommand
84
- .command("list")
85
- .description("List config keys with type and preview")
86
- .argument("[section]", "Section: providers|connectors|routes|bindings|sandboxes|data|extensions")
87
- .option("--json", "Output JSON", false)
88
- .action(async (section: string | undefined, opts) => {
89
- await runConfigListCommand({
90
- ...(typeof section === "string" ? { section } : {}),
91
- json: Boolean(opts.json),
92
- });
93
- });
94
-
95
- const configSchemaCommand = configCommand.command("schema").description("Inspect extension config schemas");
96
-
97
- configSchemaCommand
98
- .command("list")
99
- .description("List loaded contributions and schema availability")
100
- .option("--json", "Output JSON", false)
101
- .action(async (opts) => {
102
- await runConfigSchemaListCommand({
103
- json: Boolean(opts.json),
104
- });
105
- });
106
-
107
- configSchemaCommand
108
- .command("show")
109
- .description("Show one contribution config schema")
110
- .argument("<contributionId>", "Contribution ID")
111
- .option("--json", "Output JSON", false)
112
- .action(async (contributionId: string, opts) => {
113
- await runConfigSchemaShowCommand({
114
- contributionId,
115
- json: Boolean(opts.json),
116
- });
117
- });
118
-
119
- const extensionCommand = program.command("extension").description("Manage extensions");
120
-
121
- extensionCommand
122
- .command("install")
123
- .description("Install extension package")
124
- .argument("<packageSpec>", "npm package spec")
125
- .option("--enable", "Enable extension in config after install", false)
126
- .option("--json", "Output JSON", false)
127
- .action(async (packageSpec: string, opts) => {
128
- await runExtensionInstallCommand({
129
- spec: packageSpec,
130
- enable: Boolean(opts.enable),
131
- json: Boolean(opts.json),
132
- });
133
- });
134
-
135
- extensionCommand
136
- .command("uninstall")
137
- .description("Uninstall extension package")
138
- .argument("<packageName>", "Package name")
139
- .action(async (packageName: string) => {
140
- await runExtensionUninstallCommand({
141
- packageName,
142
- });
143
- });
144
-
145
- extensionCommand
146
- .command("list")
147
- .description("List installed extension packages")
148
- .option("--json", "Output JSON", false)
149
- .action(async (opts) => {
150
- await runExtensionListCommand({
151
- json: Boolean(opts.json),
152
- });
153
- });
154
-
155
- program
156
- .command("doctor")
157
- .description("Validate configuration and common runtime risks")
158
- .option("--fix", "Apply conservative fixes", false)
159
- .action(async (opts) => {
160
- await runDoctorCommand({
161
- fix: Boolean(opts.fix),
162
- });
163
- });
164
-
165
- const cronCommand = program.command("cron").description("Manage scheduled cron jobs");
166
-
167
- cronCommand
168
- .command("add")
169
- .description("Create one cron job")
170
- .argument("<name>", "Job name")
171
- .requiredOption("--prompt <text>", "Prompt text for each run")
172
- .requiredOption("--connector <id>", "Connector instance ID")
173
- .requiredOption("--route <id>", "Route ID")
174
- .requiredOption("--channel <id>", "Delivery channel/chat ID")
175
- .option("--thread <id>", "Delivery thread ID")
176
- .option("--session-policy <policy>", "Session policy: stateless|shared-session", "stateless")
177
- .option("--at <iso>", "Run once at ISO timestamp")
178
- .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
179
- .option("--cron <expr>", "Cron expression")
180
- .option("--tz <tz>", "Timezone for cron expression")
181
- .option("--cron-config <path>", "Override cron config path")
182
- .action(async (name: string, opts) => {
183
- const parsedEveryMs = typeof opts.everyMs === "string" ? Number(opts.everyMs) : null;
184
- await runCronAddCommand({
185
- name,
186
- prompt: opts.prompt as string,
187
- connectorId: opts.connector as string,
188
- routeId: opts.route as string,
189
- channelId: opts.channel as string,
190
- ...(typeof opts.thread === "string" ? { threadId: opts.thread as string } : {}),
191
- sessionPolicy: opts.sessionPolicy as "stateless" | "shared-session",
192
- ...(typeof opts.at === "string" ? { at: opts.at as string } : {}),
193
- ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
194
- ...(typeof opts.cron === "string" ? { cronExpr: opts.cron as string } : {}),
195
- ...(typeof opts.tz === "string" ? { tz: opts.tz as string } : {}),
196
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
197
- });
198
- });
199
-
200
- cronCommand
201
- .command("list")
202
- .description("List cron jobs")
203
- .option("--json", "Output JSON", false)
204
- .option("--cron-config <path>", "Override cron config path")
205
- .action(async (opts) => {
206
- await runCronListCommand({
207
- json: Boolean(opts.json),
208
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
209
- });
210
- });
211
-
212
- cronCommand
213
- .command("status")
214
- .description("Show status for all jobs or one job")
215
- .argument("[jobId]", "Cron job ID")
216
- .option("--json", "Output JSON", false)
217
- .option("--cron-config <path>", "Override cron config path")
218
- .action(async (jobId: string | undefined, opts) => {
219
- await runCronStatusCommand({
220
- ...(typeof jobId === "string" ? { jobId } : {}),
221
- json: Boolean(opts.json),
222
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
223
- });
224
- });
225
-
226
- cronCommand
227
- .command("run")
228
- .description("Queue one cron job for immediate execution")
229
- .argument("<jobId>", "Cron job ID")
230
- .option("--cron-config <path>", "Override cron config path")
231
- .action(async (jobId: string, opts) => {
232
- await runCronRunCommand({
233
- jobId,
234
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
235
- });
236
- });
237
-
238
- cronCommand
239
- .command("update")
240
- .description("Update one cron job")
241
- .argument("<jobId>", "Cron job ID")
242
- .option("--name <name>", "Job name")
243
- .option("--prompt <text>", "Job prompt")
244
- .option("--connector <id>", "Connector instance ID")
245
- .option("--route <id>", "Route ID")
246
- .option("--channel <id>", "Delivery channel/chat ID")
247
- .option("--thread <id>", "Delivery thread ID")
248
- .option("--clear-thread", "Unset delivery thread", false)
249
- .option("--session-policy <policy>", "Session policy: stateless|shared-session")
250
- .option("--at <iso>", "Run once at ISO timestamp")
251
- .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
252
- .option("--cron <expr>", "Cron expression")
253
- .option("--tz <tz>", "Timezone for cron expression")
254
- .option("--cron-config <path>", "Override cron config path")
255
- .action(async (jobId: string, opts) => {
256
- const parsedEveryMs = typeof opts.everyMs === "string" ? Number(opts.everyMs) : null;
257
- await runCronUpdateCommand({
258
- jobId,
259
- ...(typeof opts.name === "string" ? { name: opts.name as string } : {}),
260
- ...(typeof opts.prompt === "string" ? { prompt: opts.prompt as string } : {}),
261
- ...(typeof opts.connector === "string" ? { connectorId: opts.connector as string } : {}),
262
- ...(typeof opts.route === "string" ? { routeId: opts.route as string } : {}),
263
- ...(typeof opts.channel === "string" ? { channelId: opts.channel as string } : {}),
264
- ...(typeof opts.thread === "string" ? { threadId: opts.thread as string } : {}),
265
- clearThread: Boolean(opts.clearThread),
266
- ...(typeof opts.sessionPolicy === "string"
267
- ? { sessionPolicy: opts.sessionPolicy as "stateless" | "shared-session" }
268
- : {}),
269
- ...(typeof opts.at === "string" ? { at: opts.at as string } : {}),
270
- ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
271
- ...(typeof opts.cron === "string" ? { cronExpr: opts.cron as string } : {}),
272
- ...(typeof opts.tz === "string" ? { tz: opts.tz as string } : {}),
273
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
274
- });
275
- });
276
-
277
- cronCommand
278
- .command("remove")
279
- .description("Remove one cron job")
280
- .argument("<jobId>", "Cron job ID")
281
- .option("--cron-config <path>", "Override cron config path")
282
- .action(async (jobId: string, opts) => {
283
- await runCronRemoveCommand({
284
- jobId,
285
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
286
- });
287
- });
288
-
289
- cronCommand
290
- .command("pause")
291
- .description("Pause one cron job")
292
- .argument("<jobId>", "Cron job ID")
293
- .option("--cron-config <path>", "Override cron config path")
294
- .action(async (jobId: string, opts) => {
295
- await runCronPauseCommand({
296
- jobId,
297
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
298
- });
299
- });
300
-
301
- cronCommand
302
- .command("resume")
303
- .description("Resume one cron job")
304
- .argument("<jobId>", "Cron job ID")
305
- .option("--cron-config <path>", "Override cron config path")
306
- .action(async (jobId: string, opts) => {
307
- await runCronResumeCommand({
308
- jobId,
309
- ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig as string } : {}),
310
- });
311
- });
312
-
313
- return program;
314
- }
@@ -1,245 +0,0 @@
1
- import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import { homedir } from "node:os";
4
- import { loadGatewayConfig } from "../../core/routing.js";
5
- import { findDobbyRepoRoot, isDobbyRepoRoot } from "../../shared/dobby-repo.js";
6
- import type { RawGatewayConfig } from "./config-types.js";
7
-
8
- /**
9
- * Default config file path used by all CLI commands.
10
- */
11
- export const DEFAULT_CONFIG_PATH = resolve(homedir(), ".dobby", "gateway.json");
12
-
13
- /**
14
- * Expands "~" prefixed paths to the current user's home directory.
15
- */
16
- function expandHome(value: string): string {
17
- if (value === "~") {
18
- return homedir();
19
- }
20
-
21
- if (value.startsWith("~/") || value.startsWith("~\\")) {
22
- return resolve(homedir(), value.slice(2));
23
- }
24
-
25
- return value;
26
- }
27
-
28
- type ConfigPathSource = "env" | "repo" | "default";
29
-
30
- interface ConfigPathResolutionInput {
31
- cwd?: string;
32
- env?: NodeJS.ProcessEnv;
33
- }
34
-
35
- interface ResolvedConfigPathInfo {
36
- path: string;
37
- source: ConfigPathSource;
38
- }
39
-
40
- function resolveConfigBaseDir(configPath: string): string {
41
- const absoluteConfigPath = resolve(configPath);
42
- const configDir = dirname(absoluteConfigPath);
43
- const repoRoot = dirname(configDir);
44
-
45
- if (absoluteConfigPath === resolve(repoRoot, "config", "gateway.json") && isDobbyRepoRoot(repoRoot)) {
46
- return repoRoot;
47
- }
48
-
49
- return configDir;
50
- }
51
-
52
- /**
53
- * Scans current directory and ancestors to find a local dobby repo config path.
54
- */
55
- function findDobbyRepoConfigPath(startDir: string): string | null {
56
- const repoRoot = findDobbyRepoRoot(startDir);
57
- return repoRoot ? resolve(repoRoot, "config", "gateway.json") : null;
58
- }
59
-
60
- /**
61
- * Resolves config path source by priority: env override -> local repo -> default home path.
62
- */
63
- function resolveConfigPathInfo(input?: ConfigPathResolutionInput): ResolvedConfigPathInfo {
64
- const env = input?.env ?? process.env;
65
- const cwd = input?.cwd ?? process.cwd();
66
- const rawOverride = env.DOBBY_CONFIG_PATH;
67
- const override = typeof rawOverride === "string" ? rawOverride.trim() : "";
68
-
69
- if (override.length > 0) {
70
- const expanded = expandHome(override);
71
- return {
72
- path: isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded),
73
- source: "env",
74
- };
75
- }
76
-
77
- const localRepoConfigPath = findDobbyRepoConfigPath(cwd);
78
- if (localRepoConfigPath) {
79
- return {
80
- path: localRepoConfigPath,
81
- source: "repo",
82
- };
83
- }
84
-
85
- return {
86
- path: DEFAULT_CONFIG_PATH,
87
- source: "default",
88
- };
89
- }
90
-
91
- /**
92
- * Resolves config path with dev-friendly detection and env override support.
93
- */
94
- export function resolveConfigPath(input?: ConfigPathResolutionInput): string {
95
- return resolveConfigPathInfo(input).path;
96
- }
97
-
98
- /**
99
- * Formats a user-facing init command hint for missing-config errors.
100
- */
101
- function initCommandHint(): string {
102
- return "dobby init";
103
- }
104
-
105
- /**
106
- * Resolves data.rootDir into an absolute path using config-file-relative semantics.
107
- */
108
- export function resolveDataRootDir(configPath: string, rawConfig: RawGatewayConfig): string {
109
- const absoluteConfigPath = resolve(configPath);
110
- const baseDir = resolveConfigBaseDir(absoluteConfigPath);
111
- const rawRootDir = typeof rawConfig.data?.rootDir === "string" && rawConfig.data.rootDir.trim().length > 0
112
- ? rawConfig.data.rootDir
113
- : "./data";
114
-
115
- const expanded = expandHome(rawRootDir);
116
- if (isAbsolute(expanded)) {
117
- return resolve(expanded);
118
- }
119
-
120
- return resolve(baseDir, expanded);
121
- }
122
-
123
- /**
124
- * Checks whether a file exists without throwing for missing paths.
125
- */
126
- async function fileExists(path: string): Promise<boolean> {
127
- try {
128
- await access(path);
129
- return true;
130
- } catch {
131
- return false;
132
- }
133
- }
134
-
135
- /**
136
- * Reads and parses the raw JSON config file.
137
- */
138
- export async function readRawConfig(configPath: string): Promise<RawGatewayConfig | null> {
139
- const absolutePath = resolve(configPath);
140
- if (!(await fileExists(absolutePath))) {
141
- return null;
142
- }
143
-
144
- const raw = await readFile(absolutePath, "utf-8");
145
- const parsed = JSON.parse(raw) as unknown;
146
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
147
- throw new Error(`Config at '${absolutePath}' must be a JSON object`);
148
- }
149
-
150
- return parsed as RawGatewayConfig;
151
- }
152
-
153
- /**
154
- * Writes config content atomically via temp file + rename.
155
- */
156
- async function writeAtomic(configPath: string, content: string): Promise<void> {
157
- const absolutePath = resolve(configPath);
158
- const configDir = dirname(absolutePath);
159
- await mkdir(configDir, { recursive: true });
160
-
161
- const tempPath = `${absolutePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
162
- await writeFile(tempPath, content, "utf-8");
163
- await rename(tempPath, absolutePath);
164
- }
165
-
166
- /**
167
- * Serializes and writes the config file with normalized JSON formatting.
168
- */
169
- export async function writeRawConfig(configPath: string, rawConfig: RawGatewayConfig): Promise<void> {
170
- const payload = `${JSON.stringify(rawConfig, null, 2)}\n`;
171
- await writeAtomic(configPath, payload);
172
- }
173
-
174
- /**
175
- * Creates a timestamped backup beside the target config file.
176
- */
177
- export async function backupConfig(configPath: string): Promise<string | null> {
178
- const absolutePath = resolve(configPath);
179
- if (!(await fileExists(absolutePath))) {
180
- return null;
181
- }
182
-
183
- const content = await readFile(absolutePath, "utf-8");
184
- const backupPath = `${absolutePath}.bak-${Date.now()}`;
185
- await writeFile(backupPath, content, "utf-8");
186
- return backupPath;
187
- }
188
-
189
- /**
190
- * Writes config, validates with loadGatewayConfig, and rolls back on any failure.
191
- */
192
- export async function writeConfigWithValidation(
193
- configPath: string,
194
- rawConfig: RawGatewayConfig,
195
- options?: {
196
- validate?: boolean;
197
- createBackup?: boolean;
198
- },
199
- ): Promise<{ backupPath: string | null }> {
200
- const absolutePath = resolve(configPath);
201
- const validate = options?.validate !== false;
202
-
203
- const previousExists = await fileExists(absolutePath);
204
- const previousContent = previousExists ? await readFile(absolutePath, "utf-8") : null;
205
- const backupPath = options?.createBackup ? await backupConfig(absolutePath) : null;
206
-
207
- try {
208
- await writeRawConfig(absolutePath, rawConfig);
209
- if (!validate) {
210
- return { backupPath };
211
- }
212
-
213
- await loadGatewayConfig(absolutePath);
214
- return { backupPath };
215
- } catch (error) {
216
- if (previousContent === null) {
217
- await rm(absolutePath, { force: true });
218
- } else {
219
- await writeAtomic(absolutePath, previousContent);
220
- }
221
- throw error;
222
- }
223
- }
224
-
225
- /**
226
- * Reads config and throws a user-oriented error when the file is missing.
227
- */
228
- export async function requireRawConfig(configPath: string): Promise<RawGatewayConfig> {
229
- const raw = await readRawConfig(configPath);
230
- if (!raw) {
231
- const resolvedConfigPath = resolve(configPath);
232
- const currentResolution = resolveConfigPathInfo();
233
- let sourceHint = "";
234
- if (resolvedConfigPath === currentResolution.path && currentResolution.source === "env") {
235
- sourceHint = ` Source: DOBBY_CONFIG_PATH='${process.env.DOBBY_CONFIG_PATH ?? ""}'.`;
236
- } else if (resolvedConfigPath === currentResolution.path && currentResolution.source === "repo") {
237
- sourceHint = " Source: detected dobby repo, using ./config/gateway.json.";
238
- }
239
-
240
- throw new Error(
241
- `Config '${resolvedConfigPath}' does not exist.${sourceHint} Run '${initCommandHint()}' first.`,
242
- );
243
- }
244
- return raw;
245
- }