duoops 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +151 -63
  2. package/data/aws_machine_power_profiles.json +54 -0
  3. package/data/cpu_physical_specs.json +105 -0
  4. package/data/cpu_power_profiles.json +275 -0
  5. package/data/gcp_machine_power_profiles.json +1802 -0
  6. package/data/runtime-pue-mappings.json +183 -0
  7. package/dist/commands/ask.d.ts +3 -0
  8. package/dist/commands/ask.js +9 -3
  9. package/dist/commands/autofix-ci.d.ts +13 -0
  10. package/dist/commands/autofix-ci.js +114 -0
  11. package/dist/commands/autofix.d.ts +5 -0
  12. package/dist/commands/autofix.js +11 -0
  13. package/dist/commands/config.d.ts +10 -0
  14. package/dist/commands/config.js +47 -0
  15. package/dist/commands/init.js +50 -27
  16. package/dist/commands/mcp/deploy.d.ts +13 -0
  17. package/dist/commands/mcp/deploy.js +139 -0
  18. package/dist/commands/measure/calculate.js +2 -2
  19. package/dist/commands/portal.js +428 -11
  20. package/dist/lib/ai/agent.d.ts +6 -2
  21. package/dist/lib/ai/agent.js +51 -57
  22. package/dist/lib/ai/tools/editing.js +28 -13
  23. package/dist/lib/ai/tools/gitlab.d.ts +4 -0
  24. package/dist/lib/ai/tools/gitlab.js +166 -11
  25. package/dist/lib/ai/tools/measure.js +7 -3
  26. package/dist/lib/ai/tools/types.d.ts +3 -0
  27. package/dist/lib/ai/tools/types.js +1 -0
  28. package/dist/lib/config.d.ts +10 -0
  29. package/dist/lib/gcloud.d.ts +7 -0
  30. package/dist/lib/gcloud.js +105 -0
  31. package/dist/lib/gitlab/pipelines-service.d.ts +23 -0
  32. package/dist/lib/gitlab/pipelines-service.js +146 -0
  33. package/dist/lib/gitlab/runner-service.d.ts +11 -0
  34. package/dist/lib/gitlab/runner-service.js +15 -0
  35. package/dist/lib/portal/settings.d.ts +3 -0
  36. package/dist/lib/portal/settings.js +48 -0
  37. package/dist/lib/scaffold.d.ts +5 -0
  38. package/dist/lib/scaffold.js +32 -0
  39. package/dist/portal/assets/HomeDashboard-DlkwSyKx.js +1 -0
  40. package/dist/portal/assets/JobDetailsDrawer-7kXXMSH8.js +1 -0
  41. package/dist/portal/assets/JobsDashboard-D4pNc9TM.js +1 -0
  42. package/dist/portal/assets/MetricsDashboard-BcgzvzBz.js +1 -0
  43. package/dist/portal/assets/PipelinesDashboard-BNrSM9GB.js +1 -0
  44. package/dist/portal/assets/allPaths-CXDKahbk.js +1 -0
  45. package/dist/portal/assets/allPathsLoader-BF5PAx2c.js +2 -0
  46. package/dist/portal/assets/cache-YerT0Slh.js +6 -0
  47. package/dist/portal/assets/core-Cz8f3oSB.js +19 -0
  48. package/dist/portal/assets/{index-B6bzT1Vv.js → index-B9sNUqEC.js} +1 -1
  49. package/dist/portal/assets/index-BWa_E8Y7.css +1 -0
  50. package/dist/portal/assets/index-Bp4RqK05.js +1 -0
  51. package/dist/portal/assets/index-DW6Qp0d6.js +64 -0
  52. package/dist/portal/assets/index-Uc4Xhv31.js +1 -0
  53. package/dist/portal/assets/progressBar-C4SmnGeZ.js +1 -0
  54. package/dist/portal/assets/splitPathsBySizeLoader-C-T9_API.js +1 -0
  55. package/dist/portal/index.html +2 -2
  56. package/oclif.manifest.json +282 -93
  57. package/package.json +2 -1
  58. package/templates/.gitlab/duo/flows/duoops.yaml +114 -0
  59. package/templates/agents/agent.yml +45 -0
  60. package/templates/duoops-autofix-component.yml +52 -0
  61. package/templates/flows/flow.yml +283 -0
  62. package/dist/portal/assets/MetricsDashboard-DIsoz4Sl.js +0 -71
  63. package/dist/portal/assets/index-BP8FwWqA.css +0 -1
  64. package/dist/portal/assets/index-DkVG3jel.js +0 -70
@@ -0,0 +1,183 @@
1
+ {
2
+ "metadata": {
3
+ "generatedAt": "2025-06-14T16:09:07.455Z",
4
+ "methodology": "Real-time Electricity Maps API with industry standard PUE",
5
+ "availableZones": [
6
+ "US-CENT-SWPP"
7
+ ]
8
+ },
9
+ "regions": {
10
+ "us-central1": {
11
+ "zone": "US-CENT-SWPP",
12
+ "industryStandardPUE": 1.1,
13
+ "realTimeCarbonIntensity": 492,
14
+ "googleOfficialIntensity": 430,
15
+ "cfe": 0.95
16
+ },
17
+ "us-east1": {
18
+ "zone": "FALLBACK",
19
+ "industryStandardPUE": 1.1,
20
+ "error": "No matching zone found in available zones"
21
+ },
22
+ "us-east4": {
23
+ "zone": "FALLBACK",
24
+ "industryStandardPUE": 1.1,
25
+ "error": "No matching zone found in available zones"
26
+ },
27
+ "us-west1": {
28
+ "zone": "FALLBACK",
29
+ "industryStandardPUE": 1.1,
30
+ "error": "No matching zone found in available zones"
31
+ },
32
+ "us-west2": {
33
+ "zone": "FALLBACK",
34
+ "industryStandardPUE": 1.1,
35
+ "error": "No matching zone found in available zones"
36
+ },
37
+ "us-west3": {
38
+ "zone": "FALLBACK",
39
+ "industryStandardPUE": 1.1,
40
+ "error": "No matching zone found in available zones"
41
+ },
42
+ "us-west4": {
43
+ "zone": "FALLBACK",
44
+ "industryStandardPUE": 1.1,
45
+ "error": "No matching zone found in available zones"
46
+ },
47
+ "us-south1": {
48
+ "zone": "FALLBACK",
49
+ "industryStandardPUE": 1.1,
50
+ "error": "No matching zone found in available zones"
51
+ },
52
+ "europe-west1": {
53
+ "zone": "FALLBACK",
54
+ "industryStandardPUE": 1.1,
55
+ "error": "No matching zone found in available zones"
56
+ },
57
+ "europe-west2": {
58
+ "zone": "FALLBACK",
59
+ "industryStandardPUE": 1.1,
60
+ "error": "No matching zone found in available zones"
61
+ },
62
+ "europe-west3": {
63
+ "zone": "FALLBACK",
64
+ "industryStandardPUE": 1.1,
65
+ "error": "No matching zone found in available zones"
66
+ },
67
+ "europe-west4": {
68
+ "zone": "FALLBACK",
69
+ "industryStandardPUE": 1.1,
70
+ "error": "No matching zone found in available zones"
71
+ },
72
+ "europe-west6": {
73
+ "zone": "FALLBACK",
74
+ "industryStandardPUE": 1.1,
75
+ "error": "No matching zone found in available zones"
76
+ },
77
+ "europe-west8": {
78
+ "zone": "FALLBACK",
79
+ "industryStandardPUE": 1.1,
80
+ "error": "No matching zone found in available zones"
81
+ },
82
+ "europe-west9": {
83
+ "zone": "FALLBACK",
84
+ "industryStandardPUE": 1.1,
85
+ "error": "No matching zone found in available zones"
86
+ },
87
+ "europe-west10": {
88
+ "zone": "FALLBACK",
89
+ "industryStandardPUE": 1.1,
90
+ "error": "No matching zone found in available zones"
91
+ },
92
+ "europe-west12": {
93
+ "zone": "FALLBACK",
94
+ "industryStandardPUE": 1.1,
95
+ "error": "No matching zone found in available zones"
96
+ },
97
+ "europe-north1": {
98
+ "zone": "FALLBACK",
99
+ "industryStandardPUE": 1.1,
100
+ "error": "No matching zone found in available zones"
101
+ },
102
+ "europe-central2": {
103
+ "zone": "FALLBACK",
104
+ "industryStandardPUE": 1.1,
105
+ "error": "No matching zone found in available zones"
106
+ },
107
+ "europe-southwest1": {
108
+ "zone": "FALLBACK",
109
+ "industryStandardPUE": 1.1,
110
+ "error": "No matching zone found in available zones"
111
+ },
112
+ "asia-east1": {
113
+ "zone": "FALLBACK",
114
+ "industryStandardPUE": 1.1,
115
+ "error": "No matching zone found in available zones"
116
+ },
117
+ "asia-east2": {
118
+ "zone": "FALLBACK",
119
+ "industryStandardPUE": 1.1,
120
+ "error": "No matching zone found in available zones"
121
+ },
122
+ "asia-northeast1": {
123
+ "zone": "FALLBACK",
124
+ "industryStandardPUE": 1.1,
125
+ "error": "No matching zone found in available zones"
126
+ },
127
+ "asia-northeast2": {
128
+ "zone": "FALLBACK",
129
+ "industryStandardPUE": 1.1,
130
+ "error": "No matching zone found in available zones"
131
+ },
132
+ "asia-northeast3": {
133
+ "zone": "FALLBACK",
134
+ "industryStandardPUE": 1.1,
135
+ "error": "No matching zone found in available zones"
136
+ },
137
+ "asia-south1": {
138
+ "zone": "FALLBACK",
139
+ "industryStandardPUE": 1.1,
140
+ "error": "No matching zone found in available zones"
141
+ },
142
+ "asia-south2": {
143
+ "zone": "FALLBACK",
144
+ "industryStandardPUE": 1.1,
145
+ "error": "No matching zone found in available zones"
146
+ },
147
+ "asia-southeast1": {
148
+ "zone": "FALLBACK",
149
+ "industryStandardPUE": 1.1,
150
+ "error": "No matching zone found in available zones"
151
+ },
152
+ "asia-southeast2": {
153
+ "zone": "FALLBACK",
154
+ "industryStandardPUE": 1.1,
155
+ "error": "No matching zone found in available zones"
156
+ },
157
+ "australia-southeast1": {
158
+ "zone": "FALLBACK",
159
+ "industryStandardPUE": 1.1,
160
+ "error": "No matching zone found in available zones"
161
+ },
162
+ "australia-southeast2": {
163
+ "zone": "FALLBACK",
164
+ "industryStandardPUE": 1.1,
165
+ "error": "No matching zone found in available zones"
166
+ },
167
+ "southamerica-east1": {
168
+ "zone": "FALLBACK",
169
+ "industryStandardPUE": 1.1,
170
+ "error": "No matching zone found in available zones"
171
+ },
172
+ "northamerica-northeast1": {
173
+ "zone": "FALLBACK",
174
+ "industryStandardPUE": 1.1,
175
+ "error": "No matching zone found in available zones"
176
+ },
177
+ "northamerica-northeast2": {
178
+ "zone": "FALLBACK",
179
+ "industryStandardPUE": 1.1,
180
+ "error": "No matching zone found in available zones"
181
+ }
182
+ }
183
+ }
@@ -4,5 +4,8 @@ export default class Ask extends Command {
4
4
  question: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
5
  };
6
6
  static description: string;
7
+ static flags: {
8
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
7
10
  run(): Promise<void>;
8
11
  }
@@ -1,5 +1,5 @@
1
1
  import { search } from '@inquirer/prompts';
2
- import { Args, Command } from '@oclif/core';
2
+ import { Args, Command, Flags } from '@oclif/core';
3
3
  import { runAgent } from '../lib/ai/agent.js';
4
4
  import { PROMPT_LIBRARY } from '../lib/ai/prompt-library.js';
5
5
  export default class Ask extends Command {
@@ -10,8 +10,14 @@ export default class Ask extends Command {
10
10
  }),
11
11
  };
12
12
  static description = 'Ask questions about your CI/CD pipelines, logs, and sustainability';
13
+ static flags = {
14
+ project: Flags.string({
15
+ char: 'P',
16
+ description: 'GitLab Project ID to contextually use',
17
+ }),
18
+ };
13
19
  async run() {
14
- const { args } = await this.parse(Ask);
20
+ const { args, flags } = await this.parse(Ask);
15
21
  let { question } = args;
16
22
  if (!question) {
17
23
  question = await search({
@@ -37,7 +43,7 @@ export default class Ask extends Command {
37
43
  }
38
44
  this.log('Thinking...');
39
45
  try {
40
- const response = await runAgent(question);
46
+ const response = await runAgent(question, { projectId: flags.project });
41
47
  this.log('\n' + response);
42
48
  }
43
49
  catch (error) {
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AutofixCi extends Command {
3
+ static args: {
4
+ 'project-id': import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ mr: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ pipeline: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,114 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { runAgent } from '../lib/ai/agent.js';
3
+ import { configManager } from '../lib/config.js';
4
+ import { createGitlabClient } from '../lib/gitlab/client.js';
5
+ import { getPipelineProvider } from '../lib/gitlab/index.js';
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ export default class AutofixCi extends Command {
8
+ static args = {
9
+ 'project-id': Args.string({
10
+ description: 'GitLab project ID or path (falls back to DUOOPS_PROJECT_ID, CI_PROJECT_ID, or duoops init default)',
11
+ required: false,
12
+ }),
13
+ };
14
+ static description = 'Headless autofix: analyze the latest failing pipeline and post results to an MR or stdout';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ '<%= config.bin %> <%= command.id %> 12345 --mr 42',
18
+ '<%= config.bin %> <%= command.id %> my-group/my-project --pipeline 99999',
19
+ ];
20
+ static flags = {
21
+ mr: Flags.integer({
22
+ description: 'Merge request IID to comment the analysis on',
23
+ }),
24
+ pipeline: Flags.integer({
25
+ description: 'Pipeline ID to analyze (defaults to latest failed)',
26
+ }),
27
+ };
28
+ async run() {
29
+ const { args, flags } = await this.parse(AutofixCi);
30
+ const config = configManager.get();
31
+ const projectId = args['project-id'] ??
32
+ process.env.DUOOPS_PROJECT_ID ??
33
+ process.env.CI_PROJECT_ID ??
34
+ config.defaultProjectId;
35
+ if (!projectId) {
36
+ this.error('Project ID is required. Pass it as an argument, set DUOOPS_PROJECT_ID / CI_PROJECT_ID, or run "duoops init".');
37
+ }
38
+ const provider = getPipelineProvider();
39
+ let pipeline = null;
40
+ if (flags.pipeline) {
41
+ pipeline = await provider.getPipeline(projectId, flags.pipeline);
42
+ }
43
+ else {
44
+ this.log('Looking for the latest failing pipeline...');
45
+ const pipelines = await provider.listPipelines(projectId, { perPage: 10, status: 'failed' });
46
+ pipeline = pipelines.find((p) => p.status === 'failed') ?? null;
47
+ }
48
+ if (!pipeline) {
49
+ this.log('No failing pipelines found. Nothing to fix.');
50
+ return;
51
+ }
52
+ this.log(`Analyzing pipeline ${pipeline.id} (${pipeline.status}) on ref ${pipeline.ref}...`);
53
+ const jobs = await provider.listJobs(projectId, pipeline.id);
54
+ const prompt = buildAutofixPrompt(pipeline, jobs);
55
+ this.log('Running agent analysis...');
56
+ const analysis = await runAgent(prompt, { projectId });
57
+ if (flags.mr) {
58
+ this.log(`Posting analysis to MR !${flags.mr}...`);
59
+ await postMrNote(projectId, flags.mr, analysis, pipeline);
60
+ this.log('Done. Analysis posted to the merge request.');
61
+ }
62
+ else if (process.env.CI_MERGE_REQUEST_IID) {
63
+ const mrIid = Number(process.env.CI_MERGE_REQUEST_IID);
64
+ this.log(`Posting analysis to MR !${mrIid} (from CI_MERGE_REQUEST_IID)...`);
65
+ await postMrNote(projectId, mrIid, analysis, pipeline);
66
+ this.log('Done. Analysis posted to the merge request.');
67
+ }
68
+ else {
69
+ this.log('\n' + analysis);
70
+ }
71
+ }
72
+ }
73
+ async function postMrNote(projectId, mrIid, analysis, pipeline) {
74
+ const client = createGitlabClient();
75
+ const body = `## DuoOps Autofix Analysis
76
+
77
+ **Pipeline:** [#${pipeline.id}](${pipeline.webUrl ?? ''}) | **Ref:** \`${pipeline.ref}\` | **Status:** ${pipeline.status}
78
+
79
+ ---
80
+
81
+ ${analysis}
82
+
83
+ ---
84
+ *Generated by [DuoOps](https://gitlab.com/youneslaaroussi/duoops) autofix agent*`;
85
+ await client.MergeRequestNotes.create(projectId, mrIid, body);
86
+ }
87
+ function buildAutofixPrompt(pipeline, jobs) {
88
+ const jobSummary = jobs.length === 0
89
+ ? 'No jobs were returned for this pipeline.'
90
+ : jobs
91
+ .map((job) => `• Job ${job.id} (${job.name}) in stage ${job.stage} - status: ${job.status}${job.duration ? `, duration: ${job.duration}s` : ''}`)
92
+ .join('\n');
93
+ return `You are DuoOps, an AI pair engineer focused on CI/CD reliability and sustainability.
94
+ Inspect the following failing pipeline and propose actionable fixes.
95
+
96
+ Pipeline:
97
+ - ID: ${pipeline.id}
98
+ - Ref: ${pipeline.ref}
99
+ - Status: ${pipeline.status}
100
+ - SHA: ${pipeline.sha}
101
+ - URL: ${pipeline.webUrl ?? 'n/a'}
102
+
103
+ Jobs:
104
+ ${jobSummary}
105
+
106
+ Instructions:
107
+ 1. Use the get_job_logs tool to fetch logs for ALL failed jobs. This is critical.
108
+ 2. Identify the root cause of the failure based on the logs and job statuses.
109
+ 3. Propose concrete remediation steps, referencing jobs and stages explicitly.
110
+ 4. Suggest any GitLab CI configuration changes or code adjustments to prevent this regression.
111
+ 5. Outline validation steps once the fix is applied.
112
+
113
+ Respond in Markdown with clear sections: Root Cause, Fix Plan, Validation.`;
114
+ }
@@ -0,0 +1,5 @@
1
+ import Portal from './portal.js';
2
+ export default class Autofix extends Portal {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -0,0 +1,11 @@
1
+ import Portal from './portal.js';
2
+ const AUTO_MESSAGE = 'Please inspect the most recent failing pipeline, diagnose the issue, and propose a fix.';
3
+ export default class Autofix extends Portal {
4
+ static description = 'Launch the DuoOps portal and pre-fill an autofix request to the agent';
5
+ async run() {
6
+ process.env.DUOOPS_PORTAL_DEFAULT_TAB = 'console';
7
+ process.env.DUOOPS_PORTAL_PRESET_MESSAGE = AUTO_MESSAGE;
8
+ this.log('Launching DuoOps portal with an autofix prompt...');
9
+ await super.run();
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Config extends Command {
3
+ static args: {
4
+ key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ value: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static examples: string[];
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,47 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { bold, green } from 'kleur/colors';
3
+ import { configManager } from '../lib/config.js';
4
+ export default class Config extends Command {
5
+ static args = {
6
+ key: Args.string({
7
+ description: 'Config key (e.g., gitlabUrl, defaultProjectId)',
8
+ required: true,
9
+ }),
10
+ value: Args.string({
11
+ description: 'Config value',
12
+ required: true,
13
+ }),
14
+ };
15
+ static description = 'Set a configuration value directly';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> gitlabUrl https://gitlab.com',
18
+ '<%= config.bin %> <%= command.id %> defaultProjectId 123456',
19
+ ];
20
+ async run() {
21
+ const { args } = await this.parse(Config);
22
+ const { key, value } = args;
23
+ const currentConfig = configManager.get();
24
+ // Basic nested update support for measure.*
25
+ if (key.startsWith('measure.')) {
26
+ const subKey = key.split('.')[1];
27
+ const measure = currentConfig.measure || {
28
+ bigqueryDataset: '',
29
+ bigqueryTable: '',
30
+ googleProjectId: ''
31
+ };
32
+ // @ts-expect-error - dynamic assignment
33
+ measure[subKey] = value;
34
+ configManager.set({
35
+ ...currentConfig,
36
+ measure
37
+ });
38
+ }
39
+ else {
40
+ configManager.set({
41
+ ...currentConfig,
42
+ [key]: value,
43
+ });
44
+ }
45
+ this.log(green(`Updated ${bold(key)} to ${bold(value)}`));
46
+ }
47
+ }
@@ -9,6 +9,8 @@ import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { configManager } from '../lib/config.js';
12
+ import { detectGcpProject, enableApis, validateProjectAccess } from '../lib/gcloud.js';
13
+ import McpDeploy from './mcp/deploy.js';
12
14
  const hasBinary = (command) => {
13
15
  try {
14
16
  execSync(`${command} --version`, { stdio: 'ignore' });
@@ -32,13 +34,6 @@ const detectGitRemotePath = () => {
32
34
  }
33
35
  catch { /* ignore */ }
34
36
  };
35
- const detectGcpProject = () => {
36
- try {
37
- const value = execSync('gcloud config get-value project', { encoding: 'utf8' }).trim();
38
- return value && value !== '(unset)' ? value : undefined;
39
- }
40
- catch { }
41
- };
42
37
  const normalizeProjectPath = (project) => project.replace(/^\/+/, '');
43
38
  const normalizeGitLabUrl = (url) => url.replace(/\/+$/, '');
44
39
  const setGitlabVariable = async (auth, projectPath, variable) => {
@@ -79,23 +74,6 @@ const createServiceAccountKey = (projectId, serviceAccount) => {
79
74
  fs.unlinkSync(tmpPath);
80
75
  return contents.toString('base64');
81
76
  };
82
- const ensureServiceEnabled = (projectId, service, label, log) => {
83
- try {
84
- log?.(gray(`Ensuring ${label} API is enabled...`));
85
- execSync(`gcloud services enable ${service} --project=${projectId} --quiet`, {
86
- stdio: 'ignore',
87
- });
88
- }
89
- catch {
90
- log?.(gray(`${label} API enablement skipped (it may already be enabled).`));
91
- }
92
- };
93
- const ensureComputeApiEnabled = (projectId, log) => {
94
- ensureServiceEnabled(projectId, 'compute.googleapis.com', 'Compute Engine', log);
95
- };
96
- const ensureMonitoringApiEnabled = (projectId, log) => {
97
- ensureServiceEnabled(projectId, 'monitoring.googleapis.com', 'Cloud Monitoring', log);
98
- };
99
77
  const sleep = (ms) => new Promise((resolve) => {
100
78
  setTimeout(resolve, ms);
101
79
  });
@@ -354,7 +332,40 @@ export default class Init extends Command {
354
332
  };
355
333
  configManager.set(config);
356
334
  this.log(green('Configuration saved.'));
357
- this.log(`Try '${this.config.bin} pipelines:list <project>' to verify GitLab access.\n`);
335
+ const setupAgents = await confirm({
336
+ default: true,
337
+ message: 'Set up DuoOps agents and flows in this project?',
338
+ });
339
+ if (setupAgents) {
340
+ const { scaffoldProject } = await import('../lib/scaffold.js');
341
+ const result = scaffoldProject(process.cwd());
342
+ for (const f of result.created) {
343
+ this.log(green(` ✓ Created ${f}`));
344
+ }
345
+ for (const f of result.skipped) {
346
+ this.log(gray(` · Skipped ${f} (already exists)`));
347
+ }
348
+ }
349
+ this.log(`\nTry '${this.config.bin} pipelines:list <project>' to verify GitLab access.\n`);
350
+ if (enableMeasure) {
351
+ const deployMcp = await confirm({
352
+ default: true,
353
+ message: 'Deploy the DuoOps MCP Server to Cloud Run now?',
354
+ });
355
+ if (deployMcp && googleProjectId && bigqueryDataset && bigqueryTable) {
356
+ try {
357
+ await McpDeploy.run([
358
+ '--gcp-project', googleProjectId,
359
+ '--bq-dataset', bigqueryDataset,
360
+ '--bq-table', bigqueryTable,
361
+ '--gitlab-url', gitlabUrl,
362
+ ]);
363
+ }
364
+ catch (error) {
365
+ this.warn(`MCP Deployment failed: ${error}`);
366
+ }
367
+ }
368
+ }
358
369
  const configureProject = await confirm({
359
370
  default: true,
360
371
  message: 'Configure a GitLab project (CI variables + optional GCP runner) now?',
@@ -418,6 +429,14 @@ export default class Init extends Command {
418
429
  this.warn('GCP Project ID is required to collect metrics.');
419
430
  return setAsDefault ? projectPath : undefined;
420
431
  }
432
+ try {
433
+ validateProjectAccess(gcpProjectId);
434
+ this.log(green(` ✓ Verified access to ${gcpProjectId}`));
435
+ }
436
+ catch (error) {
437
+ this.warn(error.message);
438
+ return setAsDefault ? projectPath : undefined;
439
+ }
421
440
  let serviceAccountEmail = await input({
422
441
  default: `duoops-runner@${gcpProjectId}.iam.gserviceaccount.com`,
423
442
  message: 'Service account email for DuoOps measurements',
@@ -522,8 +541,12 @@ export default class Init extends Command {
522
541
  default: 'gcp',
523
542
  message: 'Runner tag (GitLab jobs will use this)',
524
543
  });
525
- ensureComputeApiEnabled(gcpProjectId, (message) => this.log(message));
526
- ensureMonitoringApiEnabled(gcpProjectId, (message) => this.log(message));
544
+ try {
545
+ enableApis(gcpProjectId, ['compute.googleapis.com', 'monitoring.googleapis.com'], (msg) => this.log(gray(msg)));
546
+ }
547
+ catch (error) {
548
+ this.warn(error.message);
549
+ }
527
550
  let runnerToken = tryCreateRunnerToken(projectPath, vmName, runnerTag);
528
551
  if (!runnerToken) {
529
552
  this.log(gray('Unable to create a runner token automatically. Generate one in your GitLab project (Settings → CI/CD → Runners) and paste it below.'));
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class McpDeploy extends Command {
3
+ static description: string;
4
+ static flags: {
5
+ 'bq-dataset': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ 'bq-table': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'gcp-project': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'gitlab-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ region: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ source: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }