@clawplays/ospec-cli 0.1.1

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 (191) hide show
  1. package/.ospec/templates/hooks/post-merge +8 -0
  2. package/.ospec/templates/hooks/pre-commit +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +549 -0
  5. package/README.zh-CN.md +549 -0
  6. package/assets/for-ai/en-US/ai-guide.md +98 -0
  7. package/assets/for-ai/en-US/execution-protocol.md +64 -0
  8. package/assets/for-ai/zh-CN/ai-guide.md +102 -0
  9. package/assets/for-ai/zh-CN/execution-protocol.md +68 -0
  10. package/assets/git-hooks/post-merge +12 -0
  11. package/assets/git-hooks/pre-commit +12 -0
  12. package/assets/global-skills/claude/ospec-change/SKILL.md +116 -0
  13. package/assets/global-skills/codex/ospec-change/SKILL.md +117 -0
  14. package/assets/global-skills/codex/ospec-change/agents/openai.yaml +7 -0
  15. package/assets/global-skills/codex/ospec-change/skill.yaml +19 -0
  16. package/assets/project-conventions/en-US/development-guide.md +32 -0
  17. package/assets/project-conventions/en-US/naming-conventions.md +51 -0
  18. package/assets/project-conventions/en-US/skill-conventions.md +40 -0
  19. package/assets/project-conventions/en-US/workflow-conventions.md +70 -0
  20. package/assets/project-conventions/zh-CN/development-guide.md +32 -0
  21. package/assets/project-conventions/zh-CN/naming-conventions.md +51 -0
  22. package/assets/project-conventions/zh-CN/skill-conventions.md +40 -0
  23. package/assets/project-conventions/zh-CN/workflow-conventions.md +74 -0
  24. package/dist/adapters/codex-stitch-adapter.js +420 -0
  25. package/dist/adapters/gemini-stitch-adapter.js +408 -0
  26. package/dist/adapters/playwright-checkpoint-adapter.js +2260 -0
  27. package/dist/advanced/BatchOperations.d.ts +36 -0
  28. package/dist/advanced/BatchOperations.js +159 -0
  29. package/dist/advanced/CachingLayer.d.ts +66 -0
  30. package/dist/advanced/CachingLayer.js +136 -0
  31. package/dist/advanced/FeatureUpdater.d.ts +46 -0
  32. package/dist/advanced/FeatureUpdater.js +151 -0
  33. package/dist/advanced/PerformanceMonitor.d.ts +52 -0
  34. package/dist/advanced/PerformanceMonitor.js +129 -0
  35. package/dist/advanced/StatePersistence.d.ts +61 -0
  36. package/dist/advanced/StatePersistence.js +168 -0
  37. package/dist/advanced/index.d.ts +14 -0
  38. package/dist/advanced/index.js +22 -0
  39. package/dist/cli/commands/config.d.ts +5 -0
  40. package/dist/cli/commands/config.js +6 -0
  41. package/dist/cli/commands/feature.d.ts +5 -0
  42. package/dist/cli/commands/feature.js +6 -0
  43. package/dist/cli/commands/index.d.ts +5 -0
  44. package/dist/cli/commands/index.js +6 -0
  45. package/dist/cli/commands/project.d.ts +5 -0
  46. package/dist/cli/commands/project.js +6 -0
  47. package/dist/cli/commands/validate.d.ts +5 -0
  48. package/dist/cli/commands/validate.js +6 -0
  49. package/dist/cli/index.d.ts +5 -0
  50. package/dist/cli/index.js +6 -0
  51. package/dist/cli.d.ts +3 -0
  52. package/dist/cli.js +1007 -0
  53. package/dist/commands/ArchiveCommand.d.ts +14 -0
  54. package/dist/commands/ArchiveCommand.js +241 -0
  55. package/dist/commands/BaseCommand.d.ts +33 -0
  56. package/dist/commands/BaseCommand.js +46 -0
  57. package/dist/commands/BatchCommand.d.ts +5 -0
  58. package/dist/commands/BatchCommand.js +42 -0
  59. package/dist/commands/ChangesCommand.d.ts +3 -0
  60. package/dist/commands/ChangesCommand.js +71 -0
  61. package/dist/commands/DocsCommand.d.ts +5 -0
  62. package/dist/commands/DocsCommand.js +118 -0
  63. package/dist/commands/FinalizeCommand.d.ts +3 -0
  64. package/dist/commands/FinalizeCommand.js +24 -0
  65. package/dist/commands/IndexCommand.d.ts +5 -0
  66. package/dist/commands/IndexCommand.js +57 -0
  67. package/dist/commands/InitCommand.d.ts +5 -0
  68. package/dist/commands/InitCommand.js +65 -0
  69. package/dist/commands/NewCommand.d.ts +11 -0
  70. package/dist/commands/NewCommand.js +262 -0
  71. package/dist/commands/PluginsCommand.d.ts +58 -0
  72. package/dist/commands/PluginsCommand.js +2491 -0
  73. package/dist/commands/ProgressCommand.d.ts +5 -0
  74. package/dist/commands/ProgressCommand.js +103 -0
  75. package/dist/commands/QueueCommand.d.ts +10 -0
  76. package/dist/commands/QueueCommand.js +147 -0
  77. package/dist/commands/RunCommand.d.ts +13 -0
  78. package/dist/commands/RunCommand.js +200 -0
  79. package/dist/commands/SkillCommand.d.ts +31 -0
  80. package/dist/commands/SkillCommand.js +1216 -0
  81. package/dist/commands/SkillsCommand.d.ts +5 -0
  82. package/dist/commands/SkillsCommand.js +68 -0
  83. package/dist/commands/StatusCommand.d.ts +6 -0
  84. package/dist/commands/StatusCommand.js +140 -0
  85. package/dist/commands/UpdateCommand.d.ts +8 -0
  86. package/dist/commands/UpdateCommand.js +251 -0
  87. package/dist/commands/VerifyCommand.d.ts +5 -0
  88. package/dist/commands/VerifyCommand.js +278 -0
  89. package/dist/commands/WorkflowCommand.d.ts +12 -0
  90. package/dist/commands/WorkflowCommand.js +150 -0
  91. package/dist/commands/index.d.ts +43 -0
  92. package/dist/commands/index.js +85 -0
  93. package/dist/core/constants.d.ts +41 -0
  94. package/dist/core/constants.js +73 -0
  95. package/dist/core/errors.d.ts +36 -0
  96. package/dist/core/errors.js +72 -0
  97. package/dist/core/index.d.ts +7 -0
  98. package/dist/core/index.js +23 -0
  99. package/dist/core/types.d.ts +369 -0
  100. package/dist/core/types.js +3 -0
  101. package/dist/index.d.ts +11 -0
  102. package/dist/index.js +27 -0
  103. package/dist/presets/ProjectPresets.d.ts +41 -0
  104. package/dist/presets/ProjectPresets.js +190 -0
  105. package/dist/scaffolds/ProjectScaffoldPresets.d.ts +20 -0
  106. package/dist/scaffolds/ProjectScaffoldPresets.js +151 -0
  107. package/dist/services/ConfigManager.d.ts +14 -0
  108. package/dist/services/ConfigManager.js +386 -0
  109. package/dist/services/FeatureManager.d.ts +5 -0
  110. package/dist/services/FeatureManager.js +6 -0
  111. package/dist/services/FileService.d.ts +21 -0
  112. package/dist/services/FileService.js +152 -0
  113. package/dist/services/IndexBuilder.d.ts +12 -0
  114. package/dist/services/IndexBuilder.js +130 -0
  115. package/dist/services/Logger.d.ts +20 -0
  116. package/dist/services/Logger.js +48 -0
  117. package/dist/services/ProjectAssetRegistry.d.ts +12 -0
  118. package/dist/services/ProjectAssetRegistry.js +96 -0
  119. package/dist/services/ProjectAssetService.d.ts +49 -0
  120. package/dist/services/ProjectAssetService.js +223 -0
  121. package/dist/services/ProjectScaffoldCommandService.d.ts +73 -0
  122. package/dist/services/ProjectScaffoldCommandService.js +159 -0
  123. package/dist/services/ProjectScaffoldService.d.ts +44 -0
  124. package/dist/services/ProjectScaffoldService.js +507 -0
  125. package/dist/services/ProjectService.d.ts +209 -0
  126. package/dist/services/ProjectService.js +13239 -0
  127. package/dist/services/QueueService.d.ts +17 -0
  128. package/dist/services/QueueService.js +142 -0
  129. package/dist/services/RunService.d.ts +40 -0
  130. package/dist/services/RunService.js +420 -0
  131. package/dist/services/SkillParser.d.ts +30 -0
  132. package/dist/services/SkillParser.js +88 -0
  133. package/dist/services/StateManager.d.ts +16 -0
  134. package/dist/services/StateManager.js +127 -0
  135. package/dist/services/TemplateEngine.d.ts +43 -0
  136. package/dist/services/TemplateEngine.js +119 -0
  137. package/dist/services/TemplateGenerator.d.ts +40 -0
  138. package/dist/services/TemplateGenerator.js +273 -0
  139. package/dist/services/ValidationService.d.ts +19 -0
  140. package/dist/services/ValidationService.js +44 -0
  141. package/dist/services/Validator.d.ts +5 -0
  142. package/dist/services/Validator.js +6 -0
  143. package/dist/services/index.d.ts +52 -0
  144. package/dist/services/index.js +91 -0
  145. package/dist/services/templates/ExecutionTemplateBuilder.d.ts +12 -0
  146. package/dist/services/templates/ExecutionTemplateBuilder.js +300 -0
  147. package/dist/services/templates/ProjectTemplateBuilder.d.ts +38 -0
  148. package/dist/services/templates/ProjectTemplateBuilder.js +1897 -0
  149. package/dist/services/templates/TemplateBuilderBase.d.ts +19 -0
  150. package/dist/services/templates/TemplateBuilderBase.js +60 -0
  151. package/dist/services/templates/TemplateInputFactory.d.ts +16 -0
  152. package/dist/services/templates/TemplateInputFactory.js +298 -0
  153. package/dist/services/templates/templateTypes.d.ts +90 -0
  154. package/dist/services/templates/templateTypes.js +3 -0
  155. package/dist/tools/build-index.js +632 -0
  156. package/dist/utils/DateUtils.d.ts +18 -0
  157. package/dist/utils/DateUtils.js +40 -0
  158. package/dist/utils/PathUtils.d.ts +9 -0
  159. package/dist/utils/PathUtils.js +66 -0
  160. package/dist/utils/StringUtils.d.ts +26 -0
  161. package/dist/utils/StringUtils.js +47 -0
  162. package/dist/utils/helpers.d.ts +5 -0
  163. package/dist/utils/helpers.js +6 -0
  164. package/dist/utils/index.d.ts +7 -0
  165. package/dist/utils/index.js +23 -0
  166. package/dist/utils/logger.d.ts +5 -0
  167. package/dist/utils/logger.js +6 -0
  168. package/dist/utils/path.d.ts +5 -0
  169. package/dist/utils/path.js +6 -0
  170. package/dist/utils/subcommandHelp.d.ts +11 -0
  171. package/dist/utils/subcommandHelp.js +119 -0
  172. package/dist/workflow/ArchiveGate.d.ts +30 -0
  173. package/dist/workflow/ArchiveGate.js +93 -0
  174. package/dist/workflow/ConfigurableWorkflow.d.ts +89 -0
  175. package/dist/workflow/ConfigurableWorkflow.js +186 -0
  176. package/dist/workflow/HookSystem.d.ts +38 -0
  177. package/dist/workflow/HookSystem.js +66 -0
  178. package/dist/workflow/IndexRegenerator.d.ts +49 -0
  179. package/dist/workflow/IndexRegenerator.js +147 -0
  180. package/dist/workflow/PluginWorkflowComposer.d.ts +138 -0
  181. package/dist/workflow/PluginWorkflowComposer.js +239 -0
  182. package/dist/workflow/SkillUpdateEngine.d.ts +26 -0
  183. package/dist/workflow/SkillUpdateEngine.js +113 -0
  184. package/dist/workflow/VerificationSystem.d.ts +24 -0
  185. package/dist/workflow/VerificationSystem.js +116 -0
  186. package/dist/workflow/WorkflowEngine.d.ts +15 -0
  187. package/dist/workflow/WorkflowEngine.js +57 -0
  188. package/dist/workflow/index.d.ts +19 -0
  189. package/dist/workflow/index.js +32 -0
  190. package/package.json +78 -0
  191. package/scripts/postinstall.js +43 -0
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const { spawnSync } = require("child_process");
7
+ function fail(message, details, code = 'generic') {
8
+ const suffix = details ? ` ${details}` : '';
9
+ process.stderr.write(`Gemini Stitch adapter failed [${code}]: ${message}${suffix}\n`);
10
+ process.exit(1);
11
+ }
12
+ function parseArgs(argv) {
13
+ const result = {};
14
+ for (let index = 0; index < argv.length; index += 1) {
15
+ const current = argv[index];
16
+ if (!current.startsWith('--')) {
17
+ continue;
18
+ }
19
+ const next = argv[index + 1];
20
+ if (!next || next.startsWith('--')) {
21
+ result[current.slice(2)] = 'true';
22
+ continue;
23
+ }
24
+ result[current.slice(2)] = next;
25
+ index += 1;
26
+ }
27
+ return result;
28
+ }
29
+ function readFileIfExists(filePath) {
30
+ if (!filePath || !fs.existsSync(filePath)) {
31
+ return '';
32
+ }
33
+ return fs.readFileSync(filePath, 'utf8');
34
+ }
35
+ function readJsonIfExists(filePath) {
36
+ if (!filePath || !fs.existsSync(filePath)) {
37
+ return null;
38
+ }
39
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
40
+ }
41
+ function truncate(text, maxLength) {
42
+ const normalized = String(text || '').trim();
43
+ if (normalized.length <= maxLength) {
44
+ return normalized;
45
+ }
46
+ return `${normalized.slice(0, maxLength)}\n...[truncated]`;
47
+ }
48
+ function inspectGeminiStitchConfig(settings) {
49
+ const stitch = settings?.mcpServers?.stitch;
50
+ const headers = stitch?.headers && typeof stitch.headers === 'object' ? stitch.headers : {};
51
+ return {
52
+ stitchConfigured: Boolean(stitch && typeof stitch === 'object'),
53
+ stitchType: typeof stitch?.type === 'string' ? stitch.type : '',
54
+ stitchHttpUrlConfigured: typeof stitch?.httpUrl === 'string' && stitch.httpUrl.trim() === 'https://stitch.googleapis.com/mcp',
55
+ stitchAuthConfigured: typeof headers['X-Goog-Api-Key'] === 'string' && headers['X-Goog-Api-Key'].trim().length > 0,
56
+ };
57
+ }
58
+ function inspectGeminiSettings() {
59
+ const settingsPath = path.join(os.homedir(), '.gemini', 'settings.json');
60
+ if (!fs.existsSync(settingsPath)) {
61
+ return {
62
+ settingsPath,
63
+ exists: false,
64
+ stitchConfigured: false,
65
+ stitchType: '',
66
+ stitchHttpUrlConfigured: false,
67
+ stitchAuthConfigured: false,
68
+ };
69
+ }
70
+ try {
71
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
72
+ const stitchConfig = inspectGeminiStitchConfig(settings);
73
+ return {
74
+ settingsPath,
75
+ exists: true,
76
+ stitchConfigured: stitchConfig.stitchConfigured,
77
+ stitchType: stitchConfig.stitchType,
78
+ stitchHttpUrlConfigured: stitchConfig.stitchHttpUrlConfigured,
79
+ stitchAuthConfigured: stitchConfig.stitchAuthConfigured,
80
+ };
81
+ }
82
+ catch {
83
+ return {
84
+ settingsPath,
85
+ exists: true,
86
+ stitchConfigured: false,
87
+ stitchType: '',
88
+ stitchHttpUrlConfigured: false,
89
+ stitchAuthConfigured: false,
90
+ };
91
+ }
92
+ }
93
+ function resolveGeminiCommand(projectPath) {
94
+ const locator = process.platform === 'win32' ? 'where.exe' : 'which';
95
+ const lookup = spawnSync(locator, ['gemini'], {
96
+ cwd: projectPath,
97
+ encoding: 'utf8',
98
+ shell: false,
99
+ });
100
+ if (lookup.error || lookup.status !== 0) {
101
+ return '';
102
+ }
103
+ const lines = String(lookup.stdout || '').trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean);
104
+ if (process.platform === 'win32') {
105
+ return lines.find(line => /\.(cmd|exe|bat)$/i.test(line)) || lines[0] || '';
106
+ }
107
+ return lines[0] || '';
108
+ }
109
+ function escapeCmdArg(value) {
110
+ const normalized = String(value ?? '');
111
+ if (!/[\s"]/u.test(normalized)) {
112
+ return normalized;
113
+ }
114
+ return `"${normalized.replace(/"/g, '""')}"`;
115
+ }
116
+ function runGemini(geminiCommand, args, options) {
117
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(geminiCommand)) {
118
+ const commandPrefix = /\s/u.test(geminiCommand) ? `"${geminiCommand}"` : geminiCommand;
119
+ const commandLine = `${commandPrefix}${args.length > 0 ? ` ${args.map(escapeCmdArg).join(' ')}` : ''}`;
120
+ return spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', commandLine], {
121
+ ...options,
122
+ shell: false,
123
+ });
124
+ }
125
+ return spawnSync(geminiCommand, args, {
126
+ ...options,
127
+ shell: false,
128
+ });
129
+ }
130
+ function classifyGeminiCliFailure(details, requestedModel) {
131
+ const normalized = String(details || '').trim();
132
+ const lower = normalized.toLowerCase();
133
+ const modelLabel = requestedModel ? ` ${requestedModel}` : '';
134
+ if (/error authenticating|authentication|login|unauthorized|forbidden|auth/i.test(normalized)) {
135
+ return {
136
+ code: 'auth',
137
+ message: 'Gemini CLI authentication is not ready. Complete Gemini CLI login/API configuration, then retry.',
138
+ };
139
+ }
140
+ if (/enotfound|eai_again|getaddrinfo|network|timed out|timeout|socket hang up/i.test(lower)) {
141
+ return {
142
+ code: 'network',
143
+ message: 'Gemini CLI could not reach the upstream service. Check network/DNS access, then retry.',
144
+ };
145
+ }
146
+ if (/rate limit|quota|too many requests|resource exhausted|429/i.test(lower)) {
147
+ return {
148
+ code: 'rate_limit',
149
+ message: `Gemini model${modelLabel} hit a rate limit or quota. Retry later or switch models.`,
150
+ };
151
+ }
152
+ if (/unknown model|invalid model|unsupported model|model.*not found|model.*not available|model.*unavailable|permission.*model|does not support model|404/i.test(lower)) {
153
+ return {
154
+ code: 'model_unavailable',
155
+ message: `Gemini model${modelLabel} is unavailable for this environment. Choose a different model.`,
156
+ };
157
+ }
158
+ return {
159
+ code: 'generic',
160
+ message: `Gemini CLI exited with an error${modelLabel}.`,
161
+ };
162
+ }
163
+ function buildPrompt(changePath, projectPath, changeName, proposal, tasks, stateJson, canonicalProject) {
164
+ const prompt = [
165
+ 'You are the OSpec built-in Stitch adapter running in non-interactive mode.',
166
+ 'Use the MCP server named "stitch" to create or update a reviewable page design preview for the current change.',
167
+ 'Do not ask the user questions. Do not return prose outside the required JSON.',
168
+ 'Return only one JSON object with this exact shape:',
169
+ '{"preview_url":"https://...","summary_markdown":"...","notes":"...","artifacts":[]}',
170
+ 'Rules:',
171
+ '- You must use the stitch MCP path, not a fake placeholder URL.',
172
+ '- preview_url must be a real review URL if the operation succeeds.',
173
+ '- summary_markdown should be reviewer-facing markdown summarizing the generated page and what to check.',
174
+ '- notes should be short and mention important assumptions or blockers.',
175
+ '- artifacts should be an array of objects or strings if any files/screenshots/URLs were produced; otherwise [].',
176
+ '',
177
+ `Project path: ${projectPath}`,
178
+ `Change path: ${changePath}`,
179
+ `Change name: ${changeName}`,
180
+ '',
181
+ 'Change state.json:',
182
+ '```json',
183
+ truncate(JSON.stringify(stateJson || {}, null, 2), 12000),
184
+ '```',
185
+ '',
186
+ 'proposal.md:',
187
+ '```markdown',
188
+ truncate(proposal, 16000),
189
+ '```',
190
+ '',
191
+ 'tasks.md:',
192
+ '```markdown',
193
+ truncate(tasks, 12000),
194
+ '```',
195
+ ];
196
+ if (canonicalProject?.projectId) {
197
+ prompt.push('', 'Canonical Stitch project requirements:', `- Reuse the existing Stitch project ID: ${canonicalProject.projectId}`);
198
+ if (canonicalProject.projectUrl) {
199
+ prompt.push(`- Existing canonical project URL: ${canonicalProject.projectUrl}`);
200
+ }
201
+ prompt.push('- Do not create a new Stitch project.');
202
+ prompt.push('- Add or update pages/nodes inside the existing project and return a preview URL from that same project ID.');
203
+ }
204
+ return prompt.join('\n');
205
+ }
206
+ function extractJsonCandidate(text) {
207
+ const normalized = String(text || '').trim();
208
+ if (!normalized) {
209
+ return null;
210
+ }
211
+ const fencedMatch = normalized.match(/```json\s*([\s\S]*?)```/i);
212
+ if (fencedMatch && fencedMatch[1]) {
213
+ return fencedMatch[1].trim();
214
+ }
215
+ const objectMatch = normalized.match(/\{[\s\S]*\}/);
216
+ if (objectMatch) {
217
+ return objectMatch[0].trim();
218
+ }
219
+ return null;
220
+ }
221
+ function normalizeStitchPreviewUrl(previewUrl) {
222
+ const normalized = String(previewUrl || '').trim();
223
+ if (!normalized) {
224
+ return {
225
+ value: '',
226
+ original: '',
227
+ normalized: false,
228
+ };
229
+ }
230
+ try {
231
+ const parsed = new URL(normalized);
232
+ if (parsed.hostname === 'stitch.canvas.google.com') {
233
+ const match = parsed.pathname.match(/^\/projects\/([^/]+)\/screens\/([^/?#]+)/i);
234
+ if (match) {
235
+ return {
236
+ value: `https://stitch.withgoogle.com/projects/${match[1]}?node-id=${match[2]}`,
237
+ original: normalized,
238
+ normalized: true,
239
+ };
240
+ }
241
+ }
242
+ if (parsed.hostname === 'stitch.withgoogle.com' || parsed.hostname === 'stitch.google.com') {
243
+ const match = parsed.pathname.match(/^\/projects\/([^/?#]+)/i);
244
+ if (match) {
245
+ const canonical = new URL(`https://stitch.withgoogle.com/projects/${match[1]}`);
246
+ const nodeId = parsed.searchParams.get('node-id') || parsed.searchParams.get('node_id') || '';
247
+ if (nodeId) {
248
+ canonical.searchParams.set('node-id', nodeId);
249
+ }
250
+ const nextValue = canonical.toString();
251
+ return {
252
+ value: nextValue,
253
+ original: normalized,
254
+ normalized: nextValue !== normalized,
255
+ };
256
+ }
257
+ }
258
+ }
259
+ catch {
260
+ }
261
+ return {
262
+ value: normalized,
263
+ original: normalized,
264
+ normalized: false,
265
+ };
266
+ }
267
+ function normalizeResult(parsed, fallbackResponse, requestedModel) {
268
+ const result = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
269
+ const rawPreviewUrl = typeof result.preview_url === 'string'
270
+ ? result.preview_url.trim()
271
+ : typeof result.previewUrl === 'string'
272
+ ? result.previewUrl.trim()
273
+ : '';
274
+ const summaryMarkdown = typeof result.summary_markdown === 'string'
275
+ ? result.summary_markdown.trim()
276
+ : typeof result.summaryMarkdown === 'string'
277
+ ? result.summaryMarkdown.trim()
278
+ : '';
279
+ const notes = typeof result.notes === 'string'
280
+ ? result.notes.trim()
281
+ : typeof result.message === 'string'
282
+ ? result.message.trim()
283
+ : '';
284
+ const artifacts = Array.isArray(result.artifacts) ? result.artifacts : [];
285
+ if (!rawPreviewUrl) {
286
+ const urlMatch = String(fallbackResponse || '').match(/https?:\/\/\S+/i);
287
+ if (urlMatch) {
288
+ const normalizedPreviewUrl = normalizeStitchPreviewUrl(urlMatch[0]);
289
+ return {
290
+ preview_url: normalizedPreviewUrl.value,
291
+ summary_markdown: summaryMarkdown,
292
+ notes,
293
+ artifacts,
294
+ metadata: {
295
+ adapter: 'gemini-cli-stitch',
296
+ fallback_url_extracted: true,
297
+ ...(requestedModel ? { gemini_model: requestedModel } : {}),
298
+ ...(normalizedPreviewUrl.normalized
299
+ ? { original_preview_url: normalizedPreviewUrl.original }
300
+ : {}),
301
+ },
302
+ };
303
+ }
304
+ fail('Gemini response did not include preview_url.', truncate(String(fallbackResponse || ''), 1000));
305
+ }
306
+ const normalizedPreviewUrl = normalizeStitchPreviewUrl(rawPreviewUrl);
307
+ return {
308
+ preview_url: normalizedPreviewUrl.value,
309
+ summary_markdown: summaryMarkdown,
310
+ notes,
311
+ artifacts,
312
+ metadata: {
313
+ adapter: 'gemini-cli-stitch',
314
+ ...(requestedModel ? { gemini_model: requestedModel } : {}),
315
+ ...(normalizedPreviewUrl.normalized
316
+ ? { original_preview_url: normalizedPreviewUrl.original }
317
+ : {}),
318
+ },
319
+ };
320
+ }
321
+ const args = parseArgs(process.argv.slice(2));
322
+ const changePath = path.resolve(args.change || process.env.OSPEC_STITCH_CHANGE_PATH || process.cwd());
323
+ const projectPath = path.resolve(args.project || process.env.OSPEC_STITCH_PROJECT_PATH || process.cwd());
324
+ const geminiCommand = resolveGeminiCommand(projectPath);
325
+ if (!geminiCommand) {
326
+ fail('Gemini CLI is not available. Install it first with `npm install -g @google/gemini-cli`.');
327
+ }
328
+ const geminiCheck = runGemini(geminiCommand, ['--version'], {
329
+ cwd: projectPath,
330
+ encoding: 'utf8',
331
+ });
332
+ if (geminiCheck.error || geminiCheck.status !== 0) {
333
+ fail('Gemini CLI is not available. Install it first with `npm install -g @google/gemini-cli`.');
334
+ }
335
+ const geminiSettings = inspectGeminiSettings();
336
+ if (!geminiSettings.exists) {
337
+ fail(`Gemini settings not found at ${geminiSettings.settingsPath}. Configure Gemini CLI and the stitch MCP server first.`);
338
+ }
339
+ if (!geminiSettings.stitchConfigured) {
340
+ fail(`Gemini CLI stitch MCP is not configured in ${geminiSettings.settingsPath}. Add mcpServers.stitch before using the built-in adapter.`);
341
+ }
342
+ if (!geminiSettings.stitchHttpUrlConfigured) {
343
+ fail(`Gemini CLI stitch MCP in ${geminiSettings.settingsPath} must set httpUrl = "https://stitch.googleapis.com/mcp". Follow the Gemini snippet from docs/stitch-plugin-spec.zh-CN.md.`);
344
+ }
345
+ if (!geminiSettings.stitchAuthConfigured) {
346
+ fail(`Gemini CLI stitch MCP in ${geminiSettings.settingsPath} must set headers["X-Goog-Api-Key"]. Follow the Gemini snippet from docs/stitch-plugin-spec.zh-CN.md.`);
347
+ }
348
+ const statePath = path.join(changePath, 'state.json');
349
+ const proposalPath = path.join(changePath, 'proposal.md');
350
+ const tasksPath = path.join(changePath, 'tasks.md');
351
+ const stateJson = readJsonIfExists(statePath);
352
+ const changeName = stateJson?.feature || path.basename(changePath);
353
+ const requestedModel = String(args.model || process.env.OSPEC_STITCH_GEMINI_MODEL || '').trim();
354
+ const canonicalProject = {
355
+ projectId: String(process.env.OSPEC_STITCH_CANONICAL_PROJECT_ID || '').trim(),
356
+ projectUrl: String(process.env.OSPEC_STITCH_CANONICAL_PROJECT_URL || '').trim(),
357
+ };
358
+ const prompt = buildPrompt(changePath, projectPath, changeName, readFileIfExists(proposalPath), readFileIfExists(tasksPath), stateJson, canonicalProject);
359
+ const geminiArgs = ['-y', '--output-format', 'json', '--allowed-mcp-server-names', 'stitch'];
360
+ if (requestedModel) {
361
+ geminiArgs.push('--model', requestedModel);
362
+ }
363
+ const runResult = runGemini(geminiCommand, geminiArgs, {
364
+ cwd: projectPath,
365
+ encoding: 'utf8',
366
+ input: prompt,
367
+ maxBuffer: 10 * 1024 * 1024,
368
+ env: {
369
+ ...process.env,
370
+ OSPEC_STITCH_CHANGE_PATH: changePath,
371
+ OSPEC_STITCH_PROJECT_PATH: projectPath,
372
+ },
373
+ });
374
+ if (runResult.error) {
375
+ fail('Failed to execute Gemini CLI.', runResult.error.message, 'generic');
376
+ }
377
+ if (runResult.status !== 0) {
378
+ const details = String(runResult.stderr || runResult.stdout || '').trim();
379
+ const failure = classifyGeminiCliFailure(details, requestedModel);
380
+ fail(failure.message, truncate(details, 1200), failure.code);
381
+ }
382
+ const rawStdout = String(runResult.stdout || '').trim();
383
+ const jsonBlock = extractJsonCandidate(rawStdout);
384
+ if (!jsonBlock) {
385
+ fail('Gemini CLI did not return JSON output.', truncate(rawStdout, 1200));
386
+ }
387
+ let geminiEnvelope;
388
+ try {
389
+ geminiEnvelope = JSON.parse(jsonBlock);
390
+ }
391
+ catch (error) {
392
+ fail('Failed to parse Gemini CLI JSON envelope.', error instanceof Error ? error.message : String(error));
393
+ }
394
+ const responseText = typeof geminiEnvelope?.response === 'string' ? geminiEnvelope.response.trim() : '';
395
+ if (!responseText) {
396
+ fail('Gemini CLI response body is empty.', truncate(rawStdout, 1200));
397
+ }
398
+ const responseJsonBlock = extractJsonCandidate(responseText);
399
+ let parsedResponse = null;
400
+ if (responseJsonBlock) {
401
+ try {
402
+ parsedResponse = JSON.parse(responseJsonBlock);
403
+ }
404
+ catch {
405
+ parsedResponse = null;
406
+ }
407
+ }
408
+ process.stdout.write(`${JSON.stringify(normalizeResult(parsedResponse, responseText, requestedModel))}\n`);