sdd-cli 0.1.23 → 0.1.25

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.
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.runHello = runHello;
4
7
  const intent_1 = require("../router/intent");
8
+ const path_1 = __importDefault(require("path"));
5
9
  const index_1 = require("../workspace/index");
6
10
  const prompt_1 = require("../ui/prompt");
7
11
  const prompt_packs_1 = require("../router/prompt-packs");
@@ -15,6 +19,8 @@ const route_1 = require("./route");
15
19
  const test_plan_1 = require("./test-plan");
16
20
  const local_metrics_1 = require("../telemetry/local-metrics");
17
21
  const errors_1 = require("../errors");
22
+ const ai_autopilot_1 = require("./ai-autopilot");
23
+ const app_lifecycle_1 = require("./app-lifecycle");
18
24
  const autopilot_checkpoint_1 = require("./autopilot-checkpoint");
19
25
  function printStep(step, description) {
20
26
  console.log(`${step}: ${description}`);
@@ -120,6 +126,7 @@ async function runHello(input, runQuestions) {
120
126
  const autoGuidedMode = !shouldRunQuestions && (runtimeFlags.nonInteractive || hasDirectIntent);
121
127
  const dryRun = runtimeFlags.dryRun;
122
128
  const beginnerMode = runtimeFlags.beginner;
129
+ const provider = runtimeFlags.provider;
123
130
  console.log("Hello from sdd-cli.");
124
131
  console.log(`Workspace: ${workspace.root}`);
125
132
  if (beginnerMode) {
@@ -127,6 +134,7 @@ async function runHello(input, runQuestions) {
127
134
  }
128
135
  if (autoGuidedMode) {
129
136
  printWhy("Auto-guided mode active: using current workspace defaults.");
137
+ printWhy(`AI provider preference: ${provider ?? "gemini"}`);
130
138
  }
131
139
  else {
132
140
  const useWorkspace = await (0, prompt_1.confirm)("Use this workspace path? (y/n) ");
@@ -288,7 +296,7 @@ async function runHello(input, runQuestions) {
288
296
  fromStep = candidate;
289
297
  }
290
298
  }
291
- const draft = buildAutopilotDraft(text, intent.flow, intent.domain);
299
+ const draft = (0, ai_autopilot_1.enrichDraftWithAI)(text, intent.flow, intent.domain, buildAutopilotDraft(text, intent.flow, intent.domain), provider);
292
300
  draft.project_name = activeProject;
293
301
  let reqId = checkpoint?.reqId ?? "";
294
302
  const startStep = fromStep ?? "create";
@@ -389,6 +397,50 @@ async function runHello(input, runQuestions) {
389
397
  return;
390
398
  }
391
399
  (0, autopilot_checkpoint_1.clearCheckpoint)(activeProject);
400
+ const projectRoot = path_1.default.resolve(finished.doneDir, "..", "..", "..");
401
+ const codeBootstrap = (0, ai_autopilot_1.bootstrapProjectCode)(projectRoot, activeProject, text, provider);
402
+ if (!codeBootstrap.generated) {
403
+ printWhy(`Code generation blocked: ${codeBootstrap.reason || "provider did not return valid files"}.`);
404
+ printWhy("No template fallback was applied. Re-run with clearer prompt or improve provider response contract.");
405
+ printRecoveryNext(activeProject, "finish", text);
406
+ return;
407
+ }
408
+ printWhy(`Code scaffold ready at: ${codeBootstrap.outputDir} (${codeBootstrap.fileCount} files)`);
409
+ if (codeBootstrap.reason) {
410
+ printWhy(`Code scaffold note: ${codeBootstrap.reason}`);
411
+ }
412
+ let lifecycle = (0, app_lifecycle_1.runAppLifecycle)(projectRoot, activeProject, {
413
+ goalText: text,
414
+ intentSignals: intent.signals
415
+ });
416
+ lifecycle.summary.forEach((line) => printWhy(`Lifecycle: ${line}`));
417
+ const lifecycleDisabled = process.env.SDD_DISABLE_APP_LIFECYCLE === "1";
418
+ if (!lifecycleDisabled && !lifecycle.qualityPassed) {
419
+ const appDir = path_1.default.join(projectRoot, "generated-app");
420
+ const parsedAttempts = Number.parseInt(process.env.SDD_AI_REPAIR_MAX_ATTEMPTS ?? "", 10);
421
+ const maxRepairAttempts = Number.isFinite(parsedAttempts) && parsedAttempts > 0 ? parsedAttempts : 6;
422
+ printWhy("Quality gates failed. Attempting AI repair iterations.");
423
+ lifecycle.qualityDiagnostics.forEach((issue) => printWhy(`Quality issue: ${issue}`));
424
+ for (let attempt = 1; attempt <= maxRepairAttempts && !lifecycle.qualityPassed; attempt += 1) {
425
+ const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, lifecycle.qualityDiagnostics);
426
+ if (repair.attempted && repair.applied) {
427
+ printWhy(`AI repair attempt ${attempt} applied (${repair.fileCount} files). Re-running lifecycle checks.`);
428
+ lifecycle = (0, app_lifecycle_1.runAppLifecycle)(projectRoot, activeProject, {
429
+ goalText: text,
430
+ intentSignals: intent.signals
431
+ });
432
+ lifecycle.summary.forEach((line) => printWhy(`Lifecycle (retry ${attempt}): ${line}`));
433
+ }
434
+ else {
435
+ printWhy(`AI repair attempt ${attempt} skipped: ${repair.reason || "unknown reason"}`);
436
+ }
437
+ }
438
+ if (!lifecycle.qualityPassed) {
439
+ printWhy("Quality still failing after AI repair attempts. Stopping without template fallback.");
440
+ printRecoveryNext(activeProject, "finish", text);
441
+ return;
442
+ }
443
+ }
392
444
  (0, local_metrics_1.recordActivationMetric)("completed", {
393
445
  project: activeProject,
394
446
  reqId
@@ -0,0 +1 @@
1
+ export declare function runSuite(initialInput?: string): Promise<void>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runSuite = runSuite;
4
+ const prompt_1 = require("../ui/prompt");
5
+ const flags_1 = require("../context/flags");
6
+ const hello_1 = require("./hello");
7
+ function inferAppType(text) {
8
+ const lower = text.toLowerCase();
9
+ if (/\bdesktop\b|\bwindows\b|\belectron\b/.test(lower)) {
10
+ return "desktop";
11
+ }
12
+ if (/\bweb\b|\bbrowser\b|\bsite\b|\bfrontend\b/.test(lower)) {
13
+ return "web";
14
+ }
15
+ return undefined;
16
+ }
17
+ function inferStack(text) {
18
+ const lower = text.toLowerCase();
19
+ if (/\btypescript\b|\bts\b/.test(lower)) {
20
+ return "typescript";
21
+ }
22
+ if (/\bjavascript\b|\bjs\b/.test(lower)) {
23
+ return "javascript";
24
+ }
25
+ return undefined;
26
+ }
27
+ async function resolveBlockers(input) {
28
+ const flags = (0, flags_1.getFlags)();
29
+ const nonInteractive = flags.nonInteractive;
30
+ const inferredType = inferAppType(input);
31
+ const inferredStack = inferStack(input);
32
+ let appType = inferredType;
33
+ let stack = inferredStack;
34
+ if (!appType) {
35
+ if (nonInteractive) {
36
+ appType = "web";
37
+ }
38
+ else {
39
+ const answer = (await (0, prompt_1.ask)("Blocker: app type? (web/desktop) ")).trim().toLowerCase();
40
+ appType = answer === "desktop" ? "desktop" : "web";
41
+ }
42
+ }
43
+ if (!stack) {
44
+ if (nonInteractive) {
45
+ stack = "javascript";
46
+ }
47
+ else {
48
+ const answer = (await (0, prompt_1.ask)("Blocker: stack? (javascript/typescript) ")).trim().toLowerCase();
49
+ stack = answer === "typescript" ? "typescript" : "javascript";
50
+ }
51
+ }
52
+ return { appType, stack };
53
+ }
54
+ function enrichIntent(intent, context) {
55
+ return `${intent}. Build target: ${context.appType}. Preferred stack: ${context.stack}. Finish complete delivery including tests and deployment notes.`;
56
+ }
57
+ async function runSuite(initialInput) {
58
+ const startedNonInteractive = (0, flags_1.getFlags)().nonInteractive;
59
+ console.log("SDD Suite started. Type 'exit' to close.");
60
+ let current = (initialInput ?? "").trim();
61
+ while (true) {
62
+ if (!current) {
63
+ if (startedNonInteractive) {
64
+ console.log("Suite finished.");
65
+ return;
66
+ }
67
+ current = (await (0, prompt_1.ask)("suite> ")).trim();
68
+ }
69
+ if (!current) {
70
+ continue;
71
+ }
72
+ if (current.toLowerCase() === "exit" || current.toLowerCase() === "quit") {
73
+ console.log("Suite finished.");
74
+ return;
75
+ }
76
+ const context = await resolveBlockers(current);
77
+ const enriched = enrichIntent(current, context);
78
+ await (0, hello_1.runHello)(enriched, false);
79
+ console.log("Suite task completed. Enter next instruction or 'exit'.");
80
+ current = "";
81
+ }
82
+ }
@@ -0,0 +1,23 @@
1
+ export type SddModeDefault = "guided" | "non-interactive" | "beginner";
2
+ export type SddProviderDefault = "gemini" | "codex" | "auto";
3
+ export type SddConfig = {
4
+ workspace: {
5
+ default_root: string;
6
+ };
7
+ ai: {
8
+ preferred_cli: SddProviderDefault;
9
+ model: string;
10
+ };
11
+ mode: {
12
+ default: SddModeDefault;
13
+ };
14
+ git: {
15
+ publish_enabled: boolean;
16
+ };
17
+ };
18
+ export declare function configPath(): string;
19
+ export declare function defaultConfig(): SddConfig;
20
+ export declare function loadConfig(): SddConfig;
21
+ export declare function saveConfig(config: SddConfig): string;
22
+ export declare function ensureConfig(): SddConfig;
23
+ export declare function updateConfigValue(key: string, value: string): SddConfig | null;
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.configPath = configPath;
7
+ exports.defaultConfig = defaultConfig;
8
+ exports.loadConfig = loadConfig;
9
+ exports.saveConfig = saveConfig;
10
+ exports.ensureConfig = ensureConfig;
11
+ exports.updateConfigValue = updateConfigValue;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ function homeDocumentsDir() {
16
+ const home = os_1.default.homedir();
17
+ return path_1.default.join(home, "Documents");
18
+ }
19
+ function inferUserName(home) {
20
+ const normalized = home.replace(/\\/g, "/").split("/").filter((part) => part.length > 0);
21
+ return normalized.length > 0 ? normalized[normalized.length - 1] : "user";
22
+ }
23
+ function configPath() {
24
+ const override = process.env.SDD_CONFIG_PATH?.trim();
25
+ if (override) {
26
+ return path_1.default.resolve(override);
27
+ }
28
+ const root = process.env.APPDATA
29
+ ? path_1.default.join(process.env.APPDATA, "sdd-cli")
30
+ : path_1.default.join(os_1.default.homedir(), ".config", "sdd-cli");
31
+ return path_1.default.join(root, "config.yml");
32
+ }
33
+ function defaultConfig() {
34
+ return {
35
+ workspace: {
36
+ default_root: path_1.default.join(homeDocumentsDir(), "sdd-tool-projects")
37
+ },
38
+ ai: {
39
+ preferred_cli: "gemini",
40
+ model: "gemini-2.5-flash-lite"
41
+ },
42
+ mode: {
43
+ default: "guided"
44
+ },
45
+ git: {
46
+ publish_enabled: false
47
+ }
48
+ };
49
+ }
50
+ function normalizeProvider(value) {
51
+ const clean = value.trim().toLowerCase();
52
+ if (clean === "codex" || clean === "auto" || clean === "gemini") {
53
+ return clean;
54
+ }
55
+ return "gemini";
56
+ }
57
+ function normalizeMode(value) {
58
+ const clean = value.trim().toLowerCase();
59
+ if (clean === "non-interactive") {
60
+ return "non-interactive";
61
+ }
62
+ if (clean === "beginner") {
63
+ return "beginner";
64
+ }
65
+ return "guided";
66
+ }
67
+ function expandRoot(value) {
68
+ let out = value.trim();
69
+ const home = os_1.default.homedir();
70
+ const user = inferUserName(home);
71
+ out = out.replace(/\{\{user\}\}/gi, user);
72
+ out = out.replace(/\{\{home\}\}/gi, home);
73
+ if (out.startsWith("~/")) {
74
+ out = path_1.default.join(home, out.slice(2));
75
+ }
76
+ return path_1.default.resolve(out);
77
+ }
78
+ function parseSimpleYaml(raw) {
79
+ const result = {};
80
+ let section = "";
81
+ const lines = raw.split(/\r?\n/);
82
+ for (const line of lines) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed || trimmed.startsWith("#")) {
85
+ continue;
86
+ }
87
+ const sectionMatch = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*$/.exec(trimmed);
88
+ if (sectionMatch) {
89
+ section = sectionMatch[1];
90
+ continue;
91
+ }
92
+ const valueMatch = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.+)\s*$/.exec(trimmed);
93
+ if (!valueMatch || !section) {
94
+ continue;
95
+ }
96
+ const key = valueMatch[1];
97
+ const value = valueMatch[2].replace(/^["']|["']$/g, "");
98
+ if (section === "workspace" && key === "default_root") {
99
+ result.workspace = { default_root: value };
100
+ }
101
+ else if (section === "ai" && key === "preferred_cli") {
102
+ result.ai = { ...(result.ai ?? { model: defaultConfig().ai.model }), preferred_cli: normalizeProvider(value) };
103
+ }
104
+ else if (section === "ai" && key === "model") {
105
+ result.ai = { ...(result.ai ?? { preferred_cli: defaultConfig().ai.preferred_cli }), model: value.trim() };
106
+ }
107
+ else if (section === "mode" && key === "default") {
108
+ result.mode = { default: normalizeMode(value) };
109
+ }
110
+ else if (section === "git" && key === "publish_enabled") {
111
+ result.git = { publish_enabled: value.trim().toLowerCase() === "true" };
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+ function renderYaml(config) {
117
+ return [
118
+ "# sdd-cli configuration",
119
+ "# You can use {{user}} or {{home}} in workspace.default_root",
120
+ "workspace:",
121
+ ` default_root: ${config.workspace.default_root}`,
122
+ "ai:",
123
+ ` preferred_cli: ${config.ai.preferred_cli}`,
124
+ ` model: ${config.ai.model}`,
125
+ "mode:",
126
+ ` default: ${config.mode.default}`,
127
+ "git:",
128
+ ` publish_enabled: ${config.git.publish_enabled ? "true" : "false"}`,
129
+ ""
130
+ ].join("\n");
131
+ }
132
+ function mergeConfig(base, input) {
133
+ const root = input.workspace?.default_root ? expandRoot(input.workspace.default_root) : base.workspace.default_root;
134
+ return {
135
+ workspace: {
136
+ default_root: root
137
+ },
138
+ ai: {
139
+ preferred_cli: input.ai?.preferred_cli ? normalizeProvider(input.ai.preferred_cli) : base.ai.preferred_cli,
140
+ model: typeof input.ai?.model === "string" && input.ai.model.trim() ? input.ai.model.trim() : base.ai.model
141
+ },
142
+ mode: {
143
+ default: input.mode?.default ? normalizeMode(input.mode.default) : base.mode.default
144
+ },
145
+ git: {
146
+ publish_enabled: typeof input.git?.publish_enabled === "boolean" ? input.git.publish_enabled : base.git.publish_enabled
147
+ }
148
+ };
149
+ }
150
+ function loadConfig() {
151
+ const defaults = defaultConfig();
152
+ const file = configPath();
153
+ if (!fs_1.default.existsSync(file)) {
154
+ return defaults;
155
+ }
156
+ try {
157
+ const raw = fs_1.default.readFileSync(file, "utf-8");
158
+ return mergeConfig(defaults, parseSimpleYaml(raw));
159
+ }
160
+ catch {
161
+ return defaults;
162
+ }
163
+ }
164
+ function saveConfig(config) {
165
+ const file = configPath();
166
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
167
+ fs_1.default.writeFileSync(file, renderYaml(config), "utf-8");
168
+ return file;
169
+ }
170
+ function ensureConfig() {
171
+ const existing = loadConfig();
172
+ const file = configPath();
173
+ if (!fs_1.default.existsSync(file)) {
174
+ saveConfig(existing);
175
+ }
176
+ fs_1.default.mkdirSync(existing.workspace.default_root, { recursive: true });
177
+ return existing;
178
+ }
179
+ function updateConfigValue(key, value) {
180
+ const current = ensureConfig();
181
+ const next = {
182
+ workspace: { ...current.workspace },
183
+ ai: { ...current.ai },
184
+ mode: { ...current.mode },
185
+ git: { ...current.git }
186
+ };
187
+ const normalized = key.trim().toLowerCase();
188
+ if (normalized === "workspace.default_root") {
189
+ next.workspace.default_root = expandRoot(value);
190
+ }
191
+ else if (normalized === "ai.preferred_cli") {
192
+ next.ai.preferred_cli = normalizeProvider(value);
193
+ }
194
+ else if (normalized === "ai.model") {
195
+ next.ai.model = value.trim() || next.ai.model;
196
+ }
197
+ else if (normalized === "mode.default") {
198
+ next.mode.default = normalizeMode(value);
199
+ }
200
+ else if (normalized === "git.publish_enabled") {
201
+ next.git.publish_enabled = value.trim().toLowerCase() === "true";
202
+ }
203
+ else {
204
+ return null;
205
+ }
206
+ saveConfig(next);
207
+ fs_1.default.mkdirSync(next.workspace.default_root, { recursive: true });
208
+ return next;
209
+ }
@@ -10,6 +10,8 @@ export type RuntimeFlags = {
10
10
  output?: string;
11
11
  scope?: string;
12
12
  metricsLocal?: boolean;
13
+ provider?: string;
14
+ model?: string;
13
15
  };
14
16
  export declare function setFlags(next: Partial<RuntimeFlags>): void;
15
17
  export declare function getFlags(): RuntimeFlags;
@@ -13,7 +13,9 @@ const flags = {
13
13
  project: undefined,
14
14
  output: undefined,
15
15
  scope: undefined,
16
- metricsLocal: false
16
+ metricsLocal: false,
17
+ provider: process.env.SDD_AI_PROVIDER_DEFAULT ?? "gemini",
18
+ model: process.env.SDD_AI_MODEL_DEFAULT
17
19
  };
18
20
  function setFlags(next) {
19
21
  if ("approve" in next) {
@@ -49,6 +51,12 @@ function setFlags(next) {
49
51
  if ("metricsLocal" in next) {
50
52
  flags.metricsLocal = Boolean(next.metricsLocal);
51
53
  }
54
+ if ("provider" in next) {
55
+ flags.provider = typeof next.provider === "string" ? next.provider : flags.provider;
56
+ }
57
+ if ("model" in next) {
58
+ flags.model = typeof next.model === "string" ? next.model : flags.model;
59
+ }
52
60
  }
53
61
  function getFlags() {
54
62
  return { ...flags };
@@ -1,7 +1,5 @@
1
- export type CodexResult = {
2
- ok: boolean;
3
- output: string;
4
- error?: string;
5
- };
1
+ import { AIProvider, ProviderResult } from "./types";
2
+ export type CodexResult = ProviderResult;
6
3
  export declare function codexVersion(): CodexResult;
7
4
  export declare function codexExec(prompt: string): CodexResult;
5
+ export declare const codexProvider: AIProvider;
@@ -1,19 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.codexProvider = void 0;
3
4
  exports.codexVersion = codexVersion;
4
5
  exports.codexExec = codexExec;
5
6
  const child_process_1 = require("child_process");
7
+ function resolveCommand(input) {
8
+ if (process.platform !== "win32") {
9
+ return input;
10
+ }
11
+ const looksLikePath = input.includes("\\") || input.includes("/");
12
+ const hasExt = /\.[A-Za-z0-9]+$/.test(input);
13
+ if (!looksLikePath && !hasExt) {
14
+ return `${input}.cmd`;
15
+ }
16
+ return input;
17
+ }
6
18
  function codexVersion() {
7
- const result = (0, child_process_1.spawnSync)("codex", ["--version"], { encoding: "utf-8" });
19
+ const command = resolveCommand(process.env.SDD_CODEX_BIN?.trim() || "codex");
20
+ const useShell = process.platform === "win32" && command.toLowerCase().endsWith(".cmd");
21
+ const result = (0, child_process_1.spawnSync)(command, ["--version"], {
22
+ encoding: "utf-8",
23
+ shell: useShell
24
+ });
8
25
  if (result.status !== 0) {
9
26
  return { ok: false, output: "", error: result.stderr || "codex not available" };
10
27
  }
11
28
  return { ok: true, output: result.stdout.trim() };
12
29
  }
13
30
  function codexExec(prompt) {
14
- const result = (0, child_process_1.spawnSync)("codex", ["exec", prompt], { encoding: "utf-8" });
31
+ const command = resolveCommand(process.env.SDD_CODEX_BIN?.trim() || "codex");
32
+ const useShell = process.platform === "win32" && command.toLowerCase().endsWith(".cmd");
33
+ const result = useShell
34
+ ? (0, child_process_1.spawnSync)(`${command} exec "${prompt.replace(/"/g, "\"\"")}"`, {
35
+ encoding: "utf-8",
36
+ shell: true
37
+ })
38
+ : (0, child_process_1.spawnSync)(command, ["exec", prompt], {
39
+ encoding: "utf-8"
40
+ });
15
41
  if (result.status !== 0) {
16
42
  return { ok: false, output: result.stdout || "", error: result.stderr || "codex exec failed" };
17
43
  }
18
44
  return { ok: true, output: result.stdout.trim() };
19
45
  }
46
+ exports.codexProvider = {
47
+ id: "codex",
48
+ label: "Codex",
49
+ version: codexVersion,
50
+ exec: codexExec
51
+ };
@@ -0,0 +1,5 @@
1
+ import { AIProvider, ProviderResult } from "./types";
2
+ export type GeminiResult = ProviderResult;
3
+ export declare function geminiVersion(): GeminiResult;
4
+ export declare function geminiExec(prompt: string): GeminiResult;
5
+ export declare const geminiProvider: AIProvider;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.geminiProvider = void 0;
4
+ exports.geminiVersion = geminiVersion;
5
+ exports.geminiExec = geminiExec;
6
+ const child_process_1 = require("child_process");
7
+ function resolveCommand(input) {
8
+ if (process.platform !== "win32") {
9
+ return input;
10
+ }
11
+ const looksLikePath = input.includes("\\") || input.includes("/");
12
+ const hasExt = /\.[A-Za-z0-9]+$/.test(input);
13
+ if (!looksLikePath && !hasExt) {
14
+ return `${input}.cmd`;
15
+ }
16
+ return input;
17
+ }
18
+ function geminiVersion() {
19
+ const command = resolveCommand(process.env.SDD_GEMINI_BIN?.trim() || "gemini");
20
+ const useShell = process.platform === "win32" && command.toLowerCase().endsWith(".cmd");
21
+ const result = (0, child_process_1.spawnSync)(command, ["--version"], {
22
+ encoding: "utf-8",
23
+ shell: useShell
24
+ });
25
+ if (result.status !== 0) {
26
+ return { ok: false, output: "", error: result.stderr || "gemini not available" };
27
+ }
28
+ return { ok: true, output: result.stdout.trim() };
29
+ }
30
+ function geminiExec(prompt) {
31
+ const command = resolveCommand(process.env.SDD_GEMINI_BIN?.trim() || "gemini");
32
+ const model = process.env.SDD_GEMINI_MODEL?.trim();
33
+ const useShell = process.platform === "win32" && command.toLowerCase().endsWith(".cmd");
34
+ const normalizedPrompt = prompt.replace(/\r?\n/g, "\\n");
35
+ const env = {
36
+ ...process.env,
37
+ NO_COLOR: "1"
38
+ };
39
+ const modelArgs = model ? ["-m", model] : [];
40
+ const runPrimary = (withModel) => useShell
41
+ ? (0, child_process_1.spawnSync)(`${command} ${withModel && model ? `-m "${model.replace(/"/g, "\"\"")}" ` : ""}--prompt "${normalizedPrompt.replace(/"/g, "\"\"")}" --output-format json`, {
42
+ encoding: "utf-8",
43
+ shell: true,
44
+ env
45
+ })
46
+ : (0, child_process_1.spawnSync)(command, [...(withModel ? modelArgs : []), "--prompt", normalizedPrompt, "--output-format", "json"], {
47
+ encoding: "utf-8",
48
+ shell: false,
49
+ env
50
+ });
51
+ const runFallback = (withModel) => useShell
52
+ ? (0, child_process_1.spawnSync)(`${command} ${withModel && model ? `-m "${model.replace(/"/g, "\"\"")}" ` : ""}--prompt "${normalizedPrompt.replace(/"/g, "\"\"")}"`, {
53
+ encoding: "utf-8",
54
+ shell: true,
55
+ env
56
+ })
57
+ : (0, child_process_1.spawnSync)(command, [...(withModel ? modelArgs : []), "--prompt", normalizedPrompt], {
58
+ encoding: "utf-8",
59
+ shell: false,
60
+ env
61
+ });
62
+ let result = runPrimary(true);
63
+ if (result.status !== 0 && model) {
64
+ result = runPrimary(false);
65
+ }
66
+ if (result.status !== 0) {
67
+ result = runFallback(true);
68
+ }
69
+ if (result.status !== 0 && model) {
70
+ result = runFallback(false);
71
+ }
72
+ if (result.status !== 0) {
73
+ return { ok: false, output: result.stdout || "", error: result.stderr || "gemini exec failed" };
74
+ }
75
+ return { ok: true, output: result.stdout.trim() };
76
+ }
77
+ exports.geminiProvider = {
78
+ id: "gemini",
79
+ label: "Gemini",
80
+ version: geminiVersion,
81
+ exec: geminiExec
82
+ };
@@ -0,0 +1,16 @@
1
+ import { AIProvider, ProviderId, ProviderPreference } from "./types";
2
+ export declare function defaultProviderPreference(): ProviderPreference;
3
+ export declare function parseProviderPreference(input?: string): ProviderPreference | null;
4
+ export declare function listProviders(): AIProvider[];
5
+ export type ProviderResolution = {
6
+ ok: true;
7
+ provider: AIProvider;
8
+ selected: ProviderId;
9
+ requested: ProviderPreference;
10
+ } | {
11
+ ok: false;
12
+ requested: string;
13
+ reason: "invalid" | "unavailable";
14
+ details: string;
15
+ };
16
+ export declare function resolveProvider(requested?: string): ProviderResolution;