@dobby.ai/dobby 0.1.0 → 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 (156) hide show
  1. package/README.md +84 -39
  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/commands/doctor.js +81 -2
  5. package/dist/src/cli/commands/extension.js +3 -1
  6. package/dist/src/cli/commands/init.js +43 -173
  7. package/dist/src/cli/commands/topology.js +38 -14
  8. package/dist/src/cli/program.js +15 -137
  9. package/dist/src/cli/shared/config-io.js +3 -31
  10. package/dist/src/cli/shared/config-mutators.js +33 -9
  11. package/dist/src/cli/shared/configure-sections.js +52 -12
  12. package/dist/src/cli/shared/init-catalog.js +89 -46
  13. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  14. package/dist/src/cli/shared/schema-prompts.js +26 -2
  15. package/dist/src/core/gateway.js +3 -1
  16. package/dist/src/core/routing.js +53 -38
  17. package/dist/src/core/types.js +2 -0
  18. package/dist/src/cron/config.js +2 -2
  19. package/dist/src/cron/service.js +87 -23
  20. package/dist/src/cron/store.js +1 -1
  21. package/dist/src/main.js +0 -0
  22. package/dist/src/shared/dobby-repo.js +40 -0
  23. package/package.json +11 -4
  24. package/.env.example +0 -9
  25. package/AGENTS.md +0 -267
  26. package/ROADMAP.md +0 -34
  27. package/config/cron.example.json +0 -9
  28. package/config/gateway.example.json +0 -128
  29. package/config/models.custom.example.json +0 -27
  30. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  31. package/dist/src/cli/shared/config-path.js +0 -207
  32. package/dist/src/cli/shared/init-models-file.js +0 -65
  33. package/dist/src/cli/shared/presets.js +0 -86
  34. package/dist/src/cli/tests/config-command.test.js +0 -42
  35. package/dist/src/cli/tests/config-io.test.js +0 -64
  36. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  37. package/dist/src/cli/tests/config-path.test.js +0 -21
  38. package/dist/src/cli/tests/discord-config.test.js +0 -23
  39. package/dist/src/cli/tests/doctor.test.js +0 -107
  40. package/dist/src/cli/tests/init-catalog.test.js +0 -87
  41. package/dist/src/cli/tests/presets.test.js +0 -41
  42. package/dist/src/cli/tests/program-options.test.js +0 -92
  43. package/dist/src/cli/tests/routing-config.test.js +0 -199
  44. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  45. package/dist/src/core/tests/control-command.test.js +0 -17
  46. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  47. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  48. package/dist/src/core/tests/typing-controller.test.js +0 -103
  49. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  50. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  51. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  52. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  53. package/docs/MVP.md +0 -135
  54. package/docs/RUNBOOK.md +0 -242
  55. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  56. package/plugins/connector-discord/dobby.manifest.json +0 -18
  57. package/plugins/connector-discord/index.js +0 -1
  58. package/plugins/connector-discord/package-lock.json +0 -360
  59. package/plugins/connector-discord/package.json +0 -38
  60. package/plugins/connector-discord/src/connector.ts +0 -350
  61. package/plugins/connector-discord/src/contribution.ts +0 -21
  62. package/plugins/connector-discord/src/mapper.ts +0 -102
  63. package/plugins/connector-discord/tsconfig.json +0 -19
  64. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  65. package/plugins/connector-feishu/index.js +0 -1
  66. package/plugins/connector-feishu/package-lock.json +0 -618
  67. package/plugins/connector-feishu/package.json +0 -38
  68. package/plugins/connector-feishu/src/connector.ts +0 -343
  69. package/plugins/connector-feishu/src/contribution.ts +0 -26
  70. package/plugins/connector-feishu/src/mapper.ts +0 -401
  71. package/plugins/connector-feishu/tsconfig.json +0 -19
  72. package/plugins/plugin-sdk/index.d.ts +0 -261
  73. package/plugins/plugin-sdk/index.js +0 -1
  74. package/plugins/plugin-sdk/package-lock.json +0 -12
  75. package/plugins/plugin-sdk/package.json +0 -22
  76. package/plugins/provider-claude/dobby.manifest.json +0 -17
  77. package/plugins/provider-claude/index.js +0 -1
  78. package/plugins/provider-claude/package-lock.json +0 -3398
  79. package/plugins/provider-claude/package.json +0 -39
  80. package/plugins/provider-claude/src/contribution.ts +0 -1018
  81. package/plugins/provider-claude/tsconfig.json +0 -19
  82. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  83. package/plugins/provider-claude-cli/index.js +0 -1
  84. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  85. package/plugins/provider-claude-cli/package.json +0 -38
  86. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  87. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  88. package/plugins/provider-pi/dobby.manifest.json +0 -17
  89. package/plugins/provider-pi/index.js +0 -1
  90. package/plugins/provider-pi/package-lock.json +0 -3877
  91. package/plugins/provider-pi/package.json +0 -40
  92. package/plugins/provider-pi/src/contribution.ts +0 -476
  93. package/plugins/provider-pi/tsconfig.json +0 -19
  94. package/plugins/sandbox-core/boxlite.js +0 -1
  95. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  96. package/plugins/sandbox-core/docker.js +0 -1
  97. package/plugins/sandbox-core/package-lock.json +0 -136
  98. package/plugins/sandbox-core/package.json +0 -39
  99. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  100. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  101. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  102. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  103. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  104. package/plugins/sandbox-core/tsconfig.json +0 -19
  105. package/scripts/local-extensions.mjs +0 -168
  106. package/src/agent/event-forwarder.ts +0 -414
  107. package/src/cli/commands/config.ts +0 -328
  108. package/src/cli/commands/configure.ts +0 -92
  109. package/src/cli/commands/cron.ts +0 -410
  110. package/src/cli/commands/doctor.ts +0 -230
  111. package/src/cli/commands/extension.ts +0 -205
  112. package/src/cli/commands/init.ts +0 -396
  113. package/src/cli/commands/start.ts +0 -223
  114. package/src/cli/commands/topology.ts +0 -383
  115. package/src/cli/index.ts +0 -9
  116. package/src/cli/program.ts +0 -465
  117. package/src/cli/shared/config-io.ts +0 -277
  118. package/src/cli/shared/config-mutators.ts +0 -440
  119. package/src/cli/shared/config-schema.ts +0 -228
  120. package/src/cli/shared/config-types.ts +0 -121
  121. package/src/cli/shared/configure-sections.ts +0 -551
  122. package/src/cli/shared/discord-config.ts +0 -14
  123. package/src/cli/shared/init-catalog.ts +0 -189
  124. package/src/cli/shared/init-models-file.ts +0 -77
  125. package/src/cli/shared/runtime.ts +0 -33
  126. package/src/cli/shared/schema-prompts.ts +0 -414
  127. package/src/cli/tests/config-command.test.ts +0 -56
  128. package/src/cli/tests/config-io.test.ts +0 -92
  129. package/src/cli/tests/config-mutators.test.ts +0 -59
  130. package/src/cli/tests/doctor.test.ts +0 -120
  131. package/src/cli/tests/init-catalog.test.ts +0 -96
  132. package/src/cli/tests/program-options.test.ts +0 -113
  133. package/src/cli/tests/routing-config.test.ts +0 -209
  134. package/src/core/control-command.ts +0 -12
  135. package/src/core/dedup-store.ts +0 -103
  136. package/src/core/gateway.ts +0 -607
  137. package/src/core/routing.ts +0 -379
  138. package/src/core/runtime-registry.ts +0 -141
  139. package/src/core/tests/control-command.test.ts +0 -20
  140. package/src/core/tests/runtime-registry.test.ts +0 -140
  141. package/src/core/tests/typing-controller.test.ts +0 -129
  142. package/src/core/types.ts +0 -318
  143. package/src/core/typing-controller.ts +0 -119
  144. package/src/cron/config.ts +0 -154
  145. package/src/cron/schedule.ts +0 -61
  146. package/src/cron/service.ts +0 -249
  147. package/src/cron/store.ts +0 -155
  148. package/src/cron/types.ts +0 -60
  149. package/src/extension/loader.ts +0 -145
  150. package/src/extension/manager.ts +0 -355
  151. package/src/extension/manifest.ts +0 -26
  152. package/src/extension/registry.ts +0 -229
  153. package/src/main.ts +0 -8
  154. package/src/sandbox/executor.ts +0 -44
  155. package/src/sandbox/host-executor.ts +0 -118
  156. package/tsconfig.json +0 -18
@@ -1,189 +0,0 @@
1
- import type { RawBindingConfig, RawRouteProfile } from "./config-types.js";
2
- import {
3
- DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
4
- DISCORD_CONNECTOR_CONTRIBUTION_ID,
5
- } from "./discord-config.js";
6
-
7
- export type InitProviderChoiceId = "provider.pi" | "provider.claude-cli";
8
- export type InitConnectorChoiceId = "connector.discord";
9
-
10
- interface ProviderCatalogEntry {
11
- id: InitProviderChoiceId;
12
- label: string;
13
- extensionPackage: string;
14
- instanceId: string;
15
- contributionId: string;
16
- config: Record<string, unknown>;
17
- }
18
-
19
- interface ConnectorCatalogEntry {
20
- id: InitConnectorChoiceId;
21
- label: string;
22
- extensionPackage: string;
23
- instanceId: string;
24
- contributionId: string;
25
- }
26
-
27
- export interface InitSelectionContext {
28
- routeId: string;
29
- projectRoot: string;
30
- allowAllMessages: boolean;
31
- botName: string;
32
- botToken: string;
33
- channelId: string;
34
- routeProviderChoiceId: InitProviderChoiceId;
35
- }
36
-
37
- export interface InitSelectionResult {
38
- providerChoiceIds: InitProviderChoiceId[];
39
- routeProviderChoiceId: InitProviderChoiceId;
40
- providerChoiceId: InitProviderChoiceId;
41
- connectorChoiceId: InitConnectorChoiceId;
42
- extensionPackages: string[];
43
- providerInstances: Array<{
44
- choiceId: InitProviderChoiceId;
45
- instanceId: string;
46
- contributionId: string;
47
- config: Record<string, unknown>;
48
- }>;
49
- providerInstanceId: string;
50
- providerContributionId: string;
51
- providerConfig: Record<string, unknown>;
52
- connectorInstanceId: string;
53
- connectorContributionId: string;
54
- connectorConfig: Record<string, unknown>;
55
- routeProfile: RawRouteProfile;
56
- bindingId: string;
57
- bindingConfig: RawBindingConfig;
58
- }
59
-
60
- const PROVIDER_CATALOG: Record<InitProviderChoiceId, ProviderCatalogEntry> = {
61
- "provider.pi": {
62
- id: "provider.pi",
63
- label: "Pi provider",
64
- extensionPackage: "@dobby.ai/provider-pi",
65
- instanceId: "pi.main",
66
- contributionId: "provider.pi",
67
- config: {
68
- provider: "custom-openai",
69
- model: "example-model",
70
- thinkingLevel: "off",
71
- modelsFile: "./models.custom.json",
72
- },
73
- },
74
- "provider.claude-cli": {
75
- id: "provider.claude-cli",
76
- label: "Claude CLI provider",
77
- extensionPackage: "@dobby.ai/provider-claude-cli",
78
- instanceId: "claude-cli.main",
79
- contributionId: "provider.claude-cli",
80
- config: {
81
- model: "claude-sonnet-4-5",
82
- maxTurns: 20,
83
- command: "claude",
84
- commandArgs: [],
85
- authMode: "auto",
86
- permissionMode: "bypassPermissions",
87
- streamVerbose: true,
88
- },
89
- },
90
- };
91
-
92
- const CONNECTOR_CATALOG: Record<InitConnectorChoiceId, ConnectorCatalogEntry> = {
93
- "connector.discord": {
94
- id: "connector.discord",
95
- label: "Discord connector",
96
- extensionPackage: "@dobby.ai/connector-discord",
97
- instanceId: DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
98
- contributionId: DISCORD_CONNECTOR_CONTRIBUTION_ID,
99
- },
100
- };
101
-
102
- export function listInitProviderChoices(): ProviderCatalogEntry[] {
103
- return Object.values(PROVIDER_CATALOG);
104
- }
105
-
106
- export function listInitConnectorChoices(): ConnectorCatalogEntry[] {
107
- return Object.values(CONNECTOR_CATALOG);
108
- }
109
-
110
- export function isInitProviderChoiceId(value: string): value is InitProviderChoiceId {
111
- return Object.prototype.hasOwnProperty.call(PROVIDER_CATALOG, value);
112
- }
113
-
114
- export function isInitConnectorChoiceId(value: string): value is InitConnectorChoiceId {
115
- return Object.prototype.hasOwnProperty.call(CONNECTOR_CATALOG, value);
116
- }
117
-
118
- export function createInitSelectionConfig(
119
- providerChoiceIds: InitProviderChoiceId[],
120
- connectorChoiceId: InitConnectorChoiceId,
121
- context: InitSelectionContext,
122
- ): InitSelectionResult {
123
- const dedupedProviderChoiceIds: InitProviderChoiceId[] = [];
124
- const seenProviderChoiceIds = new Set<InitProviderChoiceId>();
125
- for (const providerChoiceId of providerChoiceIds) {
126
- if (!seenProviderChoiceIds.has(providerChoiceId)) {
127
- seenProviderChoiceIds.add(providerChoiceId);
128
- dedupedProviderChoiceIds.push(providerChoiceId);
129
- }
130
- }
131
-
132
- if (dedupedProviderChoiceIds.length === 0) {
133
- throw new Error("At least one provider choice is required");
134
- }
135
-
136
- if (!dedupedProviderChoiceIds.includes(context.routeProviderChoiceId)) {
137
- throw new Error(
138
- `route provider choice '${context.routeProviderChoiceId}' must be one of selected providers: ${dedupedProviderChoiceIds.join(", ")}`,
139
- );
140
- }
141
-
142
- const providerChoices = dedupedProviderChoiceIds.map((providerChoiceId) => PROVIDER_CATALOG[providerChoiceId]);
143
- const primaryProviderChoice = PROVIDER_CATALOG[context.routeProviderChoiceId];
144
- const connectorChoice = CONNECTOR_CATALOG[connectorChoiceId];
145
-
146
- return {
147
- providerChoiceIds: dedupedProviderChoiceIds,
148
- routeProviderChoiceId: primaryProviderChoice.id,
149
- providerChoiceId: primaryProviderChoice.id,
150
- connectorChoiceId,
151
- extensionPackages: [
152
- ...new Set([...providerChoices.map((item) => item.extensionPackage), connectorChoice.extensionPackage]),
153
- ],
154
- providerInstances: providerChoices.map((providerChoice) => ({
155
- choiceId: providerChoice.id,
156
- instanceId: providerChoice.instanceId,
157
- contributionId: providerChoice.contributionId,
158
- config: structuredClone(providerChoice.config),
159
- })),
160
- providerInstanceId: primaryProviderChoice.instanceId,
161
- providerContributionId: primaryProviderChoice.contributionId,
162
- providerConfig: structuredClone(primaryProviderChoice.config),
163
- connectorInstanceId: connectorChoice.instanceId,
164
- connectorContributionId: connectorChoice.contributionId,
165
- connectorConfig: {
166
- botName: context.botName,
167
- botToken: context.botToken,
168
- reconnectStaleMs: 60_000,
169
- reconnectCheckIntervalMs: 10_000,
170
- },
171
- routeProfile: {
172
- projectRoot: context.projectRoot,
173
- tools: "full",
174
- systemPromptFile: "",
175
- mentions: context.allowAllMessages ? "optional" : "required",
176
- provider: primaryProviderChoice.instanceId,
177
- sandbox: "host.builtin",
178
- },
179
- bindingId: `${connectorChoice.instanceId}.${context.routeId}`,
180
- bindingConfig: {
181
- connector: connectorChoice.instanceId,
182
- source: {
183
- type: "channel",
184
- id: context.channelId,
185
- },
186
- route: context.routeId,
187
- },
188
- };
189
- }
@@ -1,77 +0,0 @@
1
- import { access, mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import { homedir } from "node:os";
4
-
5
- const DEFAULT_PROVIDER_PI_MODELS_FILE = "./models.custom.json";
6
-
7
- const PROVIDER_PI_MODELS_TEMPLATE = {
8
- providers: {
9
- "custom-openai": {
10
- baseUrl: "https://api.example.com/v1",
11
- api: "openai-completions",
12
- apiKey: "CUSTOM_PROVIDER_AUTH_TOKEN",
13
- models: [
14
- {
15
- id: "example-model",
16
- name: "example-model",
17
- reasoning: false,
18
- input: ["text"],
19
- contextWindow: 128000,
20
- maxTokens: 8192,
21
- cost: {
22
- input: 0,
23
- output: 0,
24
- cacheRead: 0,
25
- cacheWrite: 0,
26
- },
27
- },
28
- ],
29
- },
30
- },
31
- } as const;
32
-
33
- function expandHome(value: string): string {
34
- if (value === "~") {
35
- return homedir();
36
- }
37
- if (value.startsWith("~/") || value.startsWith("~\\")) {
38
- return resolve(homedir(), value.slice(2));
39
- }
40
-
41
- return value;
42
- }
43
-
44
- async function fileExists(path: string): Promise<boolean> {
45
- try {
46
- await access(path);
47
- return true;
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- function resolveModelsFilePath(configPath: string, value: string | undefined): string {
54
- const configDir = dirname(resolve(configPath));
55
- const resolvedValue = expandHome(value && value.trim().length > 0 ? value.trim() : DEFAULT_PROVIDER_PI_MODELS_FILE);
56
- return isAbsolute(resolvedValue) ? resolve(resolvedValue) : resolve(configDir, resolvedValue);
57
- }
58
-
59
- /**
60
- * Creates provider.pi models file only when missing.
61
- */
62
- export async function ensureProviderPiModelsFile(
63
- configPath: string,
64
- providerConfig: Record<string, unknown>,
65
- ): Promise<{ created: boolean; path: string }> {
66
- const modelsFile = typeof providerConfig.modelsFile === "string" ? providerConfig.modelsFile : undefined;
67
- const targetPath = resolveModelsFilePath(configPath, modelsFile);
68
-
69
- if (await fileExists(targetPath)) {
70
- return { created: false, path: targetPath };
71
- }
72
-
73
- await mkdir(dirname(targetPath), { recursive: true });
74
- await writeFile(targetPath, `${JSON.stringify(PROVIDER_PI_MODELS_TEMPLATE, null, 2)}\n`, "utf-8");
75
- return { created: true, path: targetPath };
76
- }
77
-
@@ -1,33 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import pino from "pino";
4
- import type { GatewayConfig } from "../../core/types.js";
5
-
6
- /**
7
- * Creates the shared gateway logger instance used across CLI commands.
8
- */
9
- export function createLogger() {
10
- return pino({
11
- name: "dobby",
12
- level: process.env.LOG_LEVEL ?? "info",
13
- });
14
- }
15
-
16
- /**
17
- * Returns the extension store directory from normalized gateway config.
18
- */
19
- export function extensionStoreDir(config: GatewayConfig): string {
20
- return join(config.data.rootDir, "extensions");
21
- }
22
-
23
- /**
24
- * Ensures required runtime data directories exist before start/doctor operations.
25
- */
26
- export async function ensureDataDirs(rootDir: string): Promise<void> {
27
- await mkdir(rootDir, { recursive: true });
28
- await mkdir(join(rootDir, "sessions"), { recursive: true });
29
- await mkdir(join(rootDir, "attachments"), { recursive: true });
30
- await mkdir(join(rootDir, "logs"), { recursive: true });
31
- await mkdir(join(rootDir, "state"), { recursive: true });
32
- await mkdir(join(rootDir, "extensions"), { recursive: true });
33
- }
@@ -1,414 +0,0 @@
1
- import {
2
- cancel,
3
- confirm,
4
- isCancel,
5
- multiselect,
6
- note,
7
- select,
8
- text,
9
- } from "@clack/prompts";
10
- import JSON5 from "json5";
11
-
12
- interface PromptConfigFromSchemaOptions {
13
- title?: string;
14
- promptDefaultedFields?: boolean;
15
- }
16
-
17
- interface FieldPromptDescriptor {
18
- key: string;
19
- schema: Record<string, unknown>;
20
- required: boolean;
21
- hasDefault: boolean;
22
- existingValue: unknown;
23
- initialValue: unknown;
24
- }
25
-
26
- function isRecord(value: unknown): value is Record<string, unknown> {
27
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
28
- }
29
-
30
- function isPrimitive(value: unknown): value is string | number | boolean {
31
- return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
32
- }
33
-
34
- function stringifyPreview(value: unknown): string {
35
- if (typeof value === "string") {
36
- return value;
37
- }
38
- if (isPrimitive(value)) {
39
- return String(value);
40
- }
41
- if (value === undefined) {
42
- return "";
43
- }
44
- return JSON.stringify(value);
45
- }
46
-
47
- function schemaType(schema: Record<string, unknown>): string | undefined {
48
- const raw = schema.type;
49
- if (typeof raw === "string") {
50
- return raw;
51
- }
52
- if (Array.isArray(raw)) {
53
- const firstStringType = raw.find((item) => typeof item === "string");
54
- return typeof firstStringType === "string" ? firstStringType : undefined;
55
- }
56
- return undefined;
57
- }
58
-
59
- function schemaEnum(schema: Record<string, unknown>): Array<string | number | boolean> | null {
60
- if (!Array.isArray(schema.enum)) {
61
- return null;
62
- }
63
-
64
- const normalized = schema.enum.filter((item) => isPrimitive(item)) as Array<string | number | boolean>;
65
- return normalized.length > 0 ? normalized : null;
66
- }
67
-
68
- function schemaRequiredSet(schema: Record<string, unknown>): Set<string> {
69
- if (!Array.isArray(schema.required)) {
70
- return new Set<string>();
71
- }
72
- return new Set(schema.required.filter((item) => typeof item === "string"));
73
- }
74
-
75
- function schemaProperties(schema: Record<string, unknown>): Record<string, Record<string, unknown>> {
76
- if (!isRecord(schema.properties)) {
77
- return {};
78
- }
79
-
80
- const result: Record<string, Record<string, unknown>> = {};
81
- for (const [key, value] of Object.entries(schema.properties)) {
82
- if (isRecord(value)) {
83
- result[key] = value;
84
- }
85
- }
86
- return result;
87
- }
88
-
89
- function hasSchemaDefault(schema: Record<string, unknown>): boolean {
90
- return Object.prototype.hasOwnProperty.call(schema, "default");
91
- }
92
-
93
- function shouldPromptInMinimalMode(field: FieldPromptDescriptor): boolean {
94
- if (!field.hasDefault && field.required) {
95
- return true;
96
- }
97
-
98
- if (!field.hasDefault && field.existingValue === undefined) {
99
- return true;
100
- }
101
-
102
- return false;
103
- }
104
-
105
- async function promptNumberField(params: {
106
- message: string;
107
- required: boolean;
108
- initialValue: unknown;
109
- integer: boolean;
110
- existingValue: unknown;
111
- }): Promise<number | undefined> {
112
- while (true) {
113
- const result = await text({
114
- message: params.message,
115
- initialValue: stringifyPreview(params.initialValue),
116
- placeholder: params.integer ? "integer" : "number",
117
- });
118
- if (isCancel(result)) {
119
- cancel("Configuration cancelled.");
120
- throw new Error("Configuration cancelled.");
121
- }
122
-
123
- const raw = String(result ?? "").trim();
124
- if (raw.length === 0) {
125
- if (params.required && params.existingValue === undefined) {
126
- await note("This field is required.", "Validation");
127
- continue;
128
- }
129
- return typeof params.existingValue === "number" ? params.existingValue : undefined;
130
- }
131
-
132
- const parsed = Number(raw);
133
- if (!Number.isFinite(parsed)) {
134
- await note("Please enter a valid number.", "Validation");
135
- continue;
136
- }
137
- if (params.integer && !Number.isInteger(parsed)) {
138
- await note("Please enter an integer.", "Validation");
139
- continue;
140
- }
141
- return parsed;
142
- }
143
- }
144
-
145
- async function promptJsonField(params: {
146
- message: string;
147
- required: boolean;
148
- initialValue: unknown;
149
- expected: "object" | "array";
150
- existingValue: unknown;
151
- }): Promise<unknown> {
152
- while (true) {
153
- const result = await text({
154
- message: params.message,
155
- initialValue: stringifyPreview(params.initialValue),
156
- placeholder: params.expected === "object" ? '{"key":"value"}' : '["value"]',
157
- });
158
- if (isCancel(result)) {
159
- cancel("Configuration cancelled.");
160
- throw new Error("Configuration cancelled.");
161
- }
162
-
163
- const raw = String(result ?? "").trim();
164
- if (raw.length === 0) {
165
- if (params.required && params.existingValue === undefined) {
166
- await note("This field is required.", "Validation");
167
- continue;
168
- }
169
- return params.existingValue;
170
- }
171
-
172
- try {
173
- const parsed = JSON5.parse(raw);
174
- if (params.expected === "object" && (!parsed || typeof parsed !== "object" || Array.isArray(parsed))) {
175
- await note("Please enter a JSON object.", "Validation");
176
- continue;
177
- }
178
- if (params.expected === "array" && !Array.isArray(parsed)) {
179
- await note("Please enter a JSON array.", "Validation");
180
- continue;
181
- }
182
- return parsed;
183
- } catch (error) {
184
- await note(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`, "Validation");
185
- }
186
- }
187
- }
188
-
189
- async function promptArrayField(params: {
190
- key: string;
191
- schema: Record<string, unknown>;
192
- required: boolean;
193
- initialValue: unknown;
194
- existingValue: unknown;
195
- }): Promise<unknown> {
196
- const itemSchema = isRecord(params.schema.items) ? params.schema.items : {};
197
- const itemEnum = schemaEnum(itemSchema);
198
- if (itemEnum && itemEnum.length > 0) {
199
- const current = Array.isArray(params.initialValue) ? params.initialValue : [];
200
- const initialValues = current.filter((item) => itemEnum.includes(item as string | number | boolean))
201
- .map((item) => String(item));
202
- const result = await multiselect({
203
- message: `${params.key}${params.required ? " (required)" : ""}`,
204
- options: itemEnum.map((item) => ({
205
- value: String(item),
206
- label: String(item),
207
- })),
208
- initialValues,
209
- required: params.required,
210
- });
211
- if (isCancel(result)) {
212
- cancel("Configuration cancelled.");
213
- throw new Error("Configuration cancelled.");
214
- }
215
-
216
- return (result as string[]).map((value) => itemEnum.find((candidate) => String(candidate) === value) ?? value);
217
- }
218
-
219
- return promptJsonField({
220
- message: `${params.key}${params.required ? " (required)" : ""} (JSON array)`,
221
- required: params.required,
222
- initialValue: params.initialValue,
223
- expected: "array",
224
- existingValue: params.existingValue,
225
- });
226
- }
227
-
228
- async function promptFieldValue(params: {
229
- key: string;
230
- schema: Record<string, unknown>;
231
- required: boolean;
232
- initialValue: unknown;
233
- existingValue: unknown;
234
- }): Promise<unknown> {
235
- const { key, schema, required, initialValue, existingValue } = params;
236
- const enumValues = schemaEnum(schema);
237
- const type = schemaType(schema);
238
- const description = typeof schema.description === "string" ? schema.description.trim() : "";
239
- const message = `${key}${required ? " (required)" : ""}${description ? ` - ${description}` : ""}`;
240
-
241
- if (enumValues && enumValues.length > 0) {
242
- const fallback = enumValues[0]!;
243
- const initialCandidate = enumValues.includes(initialValue as string | number | boolean)
244
- ? (initialValue as string | number | boolean)
245
- : enumValues.includes(existingValue as string | number | boolean)
246
- ? (existingValue as string | number | boolean)
247
- : fallback;
248
- const result = await select({
249
- message,
250
- options: enumValues.map((value) => ({
251
- value: String(value),
252
- label: String(value),
253
- })),
254
- initialValue: String(initialCandidate),
255
- });
256
- if (isCancel(result)) {
257
- cancel("Configuration cancelled.");
258
- throw new Error("Configuration cancelled.");
259
- }
260
- return enumValues.find((value) => String(value) === String(result)) ?? result;
261
- }
262
-
263
- if (type === "boolean") {
264
- const result = await confirm({
265
- message,
266
- initialValue: typeof initialValue === "boolean"
267
- ? initialValue
268
- : typeof existingValue === "boolean"
269
- ? existingValue
270
- : false,
271
- });
272
- if (isCancel(result)) {
273
- cancel("Configuration cancelled.");
274
- throw new Error("Configuration cancelled.");
275
- }
276
- return result === true;
277
- }
278
-
279
- if (type === "integer" || type === "number") {
280
- return promptNumberField({
281
- message,
282
- required,
283
- initialValue,
284
- integer: type === "integer",
285
- existingValue,
286
- });
287
- }
288
-
289
- if (type === "array") {
290
- return promptArrayField({
291
- key,
292
- schema,
293
- required,
294
- initialValue,
295
- existingValue,
296
- });
297
- }
298
-
299
- if (type === "object") {
300
- return promptJsonField({
301
- message: `${message} (JSON object)`,
302
- required,
303
- initialValue,
304
- expected: "object",
305
- existingValue,
306
- });
307
- }
308
-
309
- while (true) {
310
- const result = await text({
311
- message,
312
- initialValue: stringifyPreview(initialValue),
313
- });
314
- if (isCancel(result)) {
315
- cancel("Configuration cancelled.");
316
- throw new Error("Configuration cancelled.");
317
- }
318
-
319
- const raw = String(result ?? "").trim();
320
- if (raw.length === 0) {
321
- if (required && existingValue === undefined) {
322
- await note("This field is required.", "Validation");
323
- continue;
324
- }
325
- return existingValue;
326
- }
327
-
328
- return raw;
329
- }
330
- }
331
-
332
- /**
333
- * Prompts one extension config object from a contribution JSON Schema.
334
- * Currently supports top-level schema properties and falls back to JSON input for complex fields.
335
- */
336
- export async function promptConfigFromSchema(
337
- schema: Record<string, unknown>,
338
- currentConfig: Record<string, unknown>,
339
- options?: PromptConfigFromSchemaOptions,
340
- ): Promise<Record<string, unknown>> {
341
- const properties = schemaProperties(schema);
342
- const required = schemaRequiredSet(schema);
343
-
344
- if (options?.title) {
345
- await note(options.title, "Configure");
346
- }
347
-
348
- if (Object.keys(properties).length === 0) {
349
- return structuredClone(currentConfig);
350
- }
351
-
352
- const next = structuredClone(currentConfig);
353
- const fieldDescriptors: FieldPromptDescriptor[] = Object.entries(properties).map(([key, fieldSchema]) => {
354
- const existingValue = next[key];
355
- const defaultValue = fieldSchema.default;
356
- const initialValue = existingValue !== undefined ? existingValue : defaultValue;
357
- return {
358
- key,
359
- schema: fieldSchema,
360
- required: required.has(key),
361
- hasDefault: hasSchemaDefault(fieldSchema),
362
- existingValue,
363
- initialValue,
364
- };
365
- });
366
-
367
- const minimalFields = fieldDescriptors.filter((field) => shouldPromptInMinimalMode(field));
368
- const advancedFields = fieldDescriptors.filter((field) => !minimalFields.includes(field));
369
-
370
- for (const field of minimalFields) {
371
- const value = await promptFieldValue({
372
- key: field.key,
373
- schema: field.schema,
374
- required: field.required,
375
- initialValue: field.initialValue,
376
- existingValue: field.existingValue,
377
- });
378
- if (value === undefined) {
379
- continue;
380
- }
381
- next[field.key] = value;
382
- }
383
-
384
- if (advancedFields.length > 0) {
385
- const shouldPromptAdvanced = options?.promptDefaultedFields === true
386
- ? true
387
- : await confirm({
388
- message: "Configure advanced options (defaults can be kept)?",
389
- initialValue: false,
390
- });
391
- if (isCancel(shouldPromptAdvanced)) {
392
- cancel("Configuration cancelled.");
393
- throw new Error("Configuration cancelled.");
394
- }
395
-
396
- if (shouldPromptAdvanced === true) {
397
- for (const field of advancedFields) {
398
- const value = await promptFieldValue({
399
- key: field.key,
400
- schema: field.schema,
401
- required: field.required,
402
- initialValue: field.initialValue,
403
- existingValue: field.existingValue,
404
- });
405
- if (value === undefined) {
406
- continue;
407
- }
408
- next[field.key] = value;
409
- }
410
- }
411
- }
412
-
413
- return next;
414
- }